SegmentFault 好未来技术团队最新的文章
2021-05-08T15:44:35+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Java并发编程-知识前瞻(第一章)
https://segmentfault.com/a/1190000039966303
2021-05-08T15:44:35+08:00
2021-05-08T15:44:35+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<p><strong>前言:</strong><br>Java并发编程学习分享的目标:</p><p>Java并发编程中常用的工具用途与用法;</p><p>Java并发编程工具实现原理与设计思路;</p><p>并发编程中遇到的常见问题与解决方案;</p><p>根据实际情景选择更合适的工具完成高效的设计方案</p><p>学习分享团队:<br>学而思培优-运营研发团队<br>Java并发编程分享小组:<br>@沈健 @曹伟伟 @张俊勇 @田新文 @张晨<br>本章分享人:@张晨</p><p>学习分享大纲:<br><img src="/img/bVcRQ98" alt="image.png" title="image.png"></p><h2>01初识并发</h2><p>什么是并发,什么是并行? </p><p>用个JVM的例子来讲解,在垃圾回收器做并发标记的时候,这个时候JVM不仅可以做垃圾标记,还可以处理程序的一些需求,这个叫并发。在做垃圾回收时,JVM多个线程同时做回收,这叫并行。</p><h2>02为什么要学习并发编程</h2><p><strong>直观原因</strong><br><strong>1)JD的强制性要求</strong><br>随着互联网行业的飞速发展,并发编程已经成为非常热门的领域,也是各大企业服务端岗位招聘的必备技能。</p><p><strong>2)从小牛通往大牛的必经之路</strong><br>架构师是软件开发团队中非常重要的角色,成为一名架构师是许多搞技术人奋斗的目标,衡量一个架构师的能力指标就是设计出一套解决高并发的系统,由此可见高并发技术的重要性,而并发编程是底层的基础。无论游戏还是互联网行业,无论软件开发还是大型网站,都对高并发技术人才存在巨大需求,因此,为了工作为了提升自己,学习高并发技术刻不容缓。</p><p><strong>3)面试过程中极容易踩坑</strong><br>面试的时候为了考察对并发编程的掌握情况,经常会考察并发安全相关的知识和线程交互的知识。例如在并发情况下如何实现一个线程安全的单例模式,如何完成两个线程中的功能交互执行。</p><p><strong>以下是使用双检索实现一个线程安全的单例懒汉模式,当然也可以使用枚举或者单例饿汉模式。</strong></p><pre><code>private static volatile Singleton singleton;
private Singleton(){};
public Singleton getSingleton(){
if(null == singleton){
synchronized(Singleton.class){
if(null == singleton){
singleton = new Singleton();
}
}
}
return singleton;
}</code></pre><p>在这里第一层空判断是为了减少锁控制的粒度,使用volatile修饰是因为在jvm中new Singleton()会出现指令重排,volatile避免happens before,避免空指针的问题。从一个线程安全的单例模式可以引申出很多,volatile和synchronized的实现原理,JMM模型,MESI协议,指令重排,关于JMM模型后序会给出更详细的图解。</p><p><strong>除了线程安全问题,还会考察线程间的交互。</strong> 例如使用两个线程交替打印出A1B2C3…Z26<br><img src="/img/bVcRRam" alt="image.png" title="image.png"></p><p>考察的重点并不是要简单的实现这个功能,通过此面试题,可以考察知识的整体掌握情况,多种方案实现,可以使用Atomicinteger、ReentrantLock、CountDownLat ch。下图是使用LockSupport控制两个线程交替打印的示例,LockSupport内部实现的原理是使用UNSAFE控制一个信号量在0和1之间变动,从而可以控制两个线程的交替打印。</p><p><strong>4)并发在我们工作使用的框架中处处可见,tom cat,netty,jvm,Disruptor</strong></p><p>熟悉JAVA并发编程基础是掌握这些框架底层知识的基石,这里简单介绍下高并发框架Disruptor的底层实现原理,做一个勾勒的作用:<br>Martin Fowler在一篇LMAX文章中介绍,这一个高性能异步处理框架,其单线程一秒的吞吐量可达六百万</p><p><strong>Disruptor核心概念</strong><br><img src="/img/bVcRRaG" alt="image.png" title="image.png"></p><p>Disruptor特征</p><ul><li>基于事件驱动</li><li><li>基于"观察者"模式、"生产者-消费者"模型</li><li><li>可以在无锁的情况下实现网络的队列操作</li></ul><p><strong>RingBuffer执行流程</strong><br><img src="/img/bVcRRaL" alt="image.png" title="image.png"></p><p>Disruptor底层组件,RingBuffer密切相关的对象:Sequ enceBarrier和Sequencer;</p><p>SequenceBarrier是消费者和RingBuffer之间的桥梁。在Disruptor中,消费者直接访问的是SequenceBarrier,由SequenceBarrier减少RingBuffer的队列冲突。</p><p>SequenceBarrier 通过waitFor方法当消费者速度大于生产者的生产速度时,消费者可通过waitFor方法给予生产者一定的缓冲时间,协调生产者和消费者的速度问题,waitFor执行时机:<br><img src="/img/bVcRRaO" alt="image.png" title="image.png"></p><p>Sequencer是生产者和缓冲区RingBuffer之间的桥梁,生产者通过Sequencer向RingBuffer申请数据存放空间,通过WaitStrategy使用publish方法通知消费者,WaitStrategy是消费者没有数据可以消费时的等待策略。每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据,整个过程通过原子变量CAS,保证操作的线程安全,这就是Disruptor的无锁设计。</p><p>以下是五大常用等待策略:</p><ul><li>BlockingWaitStrategy:Disruptor的默认策略是BlockingWaitStrategy。在BlockingWaitStrategy内部是使用锁和condition来控制线程的唤醒。BlockingWaitStrategy是最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现。</li><li><li>SleepingWaitStrategy:SleepingWaitStrategy 的性能表现跟 BlockingWaitStrategy 差不多,对 CPU 的消耗也类似,但其对生产者线程的影响最小,通过使用LockSupport.parkNanos(1)来实现循环等待。</li><li><li>YieldingWaitStrategy:YieldingWaitStrategy是可以使用在低延迟系统的策略之一。YieldingWaitStrategy将自旋以等待序列增加到适当的值。在循环体内,将调用Thread.yield()以允许其他排队的线程运行。在要求极高性能且事件处理线数小于 CPU 逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。</li><li><li>BusySpinWaitStrategy:性能最好,适合用于低延迟的系统。在要求极高性能且事件处理线程数小于CPU逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。</li></ul><p>目前,包括Apache Storm、Camel、Log4j2在内的很多知名项目都应用了Disruptor以获取高性能。</p><p><strong>5)JUC是并发大神Doug Lea灵魂力作,堪称典范(第一个主流尝试,它将线程,锁和事件之外的抽象层次提升到更平易近人的方式:并发集合, fork/join 等等)</strong></p><p><strong>通过并发编程设计思维的学习,发挥使用多线程的优势</strong></p><ul><li>发挥多处理器的强大能力</li><li><li>建模的简单性</li><li><li>异步事件的简化处理</li><li><li>响应更灵敏的用户界面</li></ul><p>那么学不好并发编程基础会带来什么问题呢</p><p>1)多线程在日常开发中运用中处处都是,jvm、tomcat、netty,学好java并发编程是更深层次理解和掌握此类工具和框架的前提由于计算机的cpu运算速度和内存io速度有几个数量级的差距,因此现代计算机都不得不加入一层尽可能接近处理器运算速度的高速缓存来做缓冲:将内存中运算需要使用的数据先复制到缓存中,当运算结束后再同步回内存。如下图:</p><p><img src="/img/bVcRRa7" alt="image.png" title="image.png"></p><p>因为jvm要实现跨硬件平台,因此jvm定义了自己的内存模型,但是因为jvm的内存模型最终还是要映射到硬件上,因此jvm内存模型几乎与硬件的模型一样:</p><p><img src="/img/bVcRRbd" alt="image.png" title="image.png"></p><p>操作系统底层数据结构,每个CPU对应的高速缓存中的数据结构是一个个bucket存储的链表,其中tag代表的是主存中的地址,cache line是偏移量,flag对应的MESI缓存一致性协议中的各个状态。</p><p><strong>MESI缓存一致性状态分别为:</strong></p><p>M:Modify,代表修改</p><p>E:Exclusive,代表独占</p><p>S:Share,代表共享</p><p>I:Invalidate,代表失效</p><p><strong>以下是一次cpu0数据写入的流程:</strong></p><p>在CPU0执行一次load,read和write时,在做write之前flag的状态会是S,然后发出invalidate消息到总线;</p><p>其他cpu会监听总线消息,将各cpu对应的cache entry中的flag状态由S修改为I,并且发送invalidate ack给总线</p><p>cpu0收到所有cpu返回的invalidate ack后,cpu0将flag变为E,执行数据写入,状态修改为M,类似于一个加锁过程</p><p>考虑到性能问题,这样写入修改数据的效率太过漫长,因此引入了写缓冲器和无效队列,所有的修改操作会先写入写缓冲器,其他cpu接收到消息后会先写入无效队列,并返回ack消息,之后再从无效队列消费消息,采用异步的形式。当然,这样就会产生有序性问题,例如某些entry中的flag还是S,但实际上应该标识为I,这样访问到的数据就会有问题。运用volitale是为了解决指令重排带来的无序性问题,volitale是jvm层面的关键字,MESI是cpu层面的,两者是差了几个层次的。<br><img src="/img/bVcRRbu" alt="image.png" title="image.png"></p><p><strong>2)性能不达标,找不到解决思路。</strong></p><p><strong>3)工作中可能会写出线程不安全的方法</strong><br>以下是一个多线程打印时间的逐步优化案例</p><pre><code>new Thread(new Runnable() {
@Override
public void run() {
System.out.println(new ThreadLocalDemo01().date(10));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(new ThreadLocalDemo01().date(1007));
}
}).start();</code></pre><p>优化1,多个线程运用线程池复用</p><pre><code>for(int i = 0; i < 1000; i++){
int finalI = i;
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(new ThreadLocalDemo01().date2(finalI));
}
});
}
executorService.shutdown();
public String date2(int seconds){
Date date = new Date(1000 * seconds);
String s = null;
// synchronized (ThreadLocalDemo01.class){
// s = simpleDateFormat.format(date);
// }
s = simpleDateFormat.format(date);
return s;
}</code></pre><p>优化2,线程池结合ThreadLocal</p><pre><code>public String date2(int seconds){
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return simpleDateFormat.format(date);
}</code></pre><p>在多线程服用一个SimpleDateFormat时会出现线程安全问题,执行结果会打印出相同的时间,在优化2中使用线程池结合ThreadLocal实现资源隔离,线程安全。</p><p><strong>4)许多问题无法正确定位</strong><br>踩坑:crm仿真定时任务阻塞,无法继续执行<br>问题:crm仿真运用schedule配置的定时任务在某个时间节点后的所有定时任务均未执行<br>原因:定时任务配置导致的问题,@Schedule配置的定时任务如果未配置线程池,在启动类使用@EnableScheduling启用定时任务时会默认使用单线程,后端配置了多定时任务,会出现问题.配置了两定时任务A和B,在A先占用资源后如果一直未释放,B会一直处于等待状态,直到A任务释放资源后,B开始执行,若要避免多任务执行带来的问题,需要使用以下方法配置:</p><pre><code>@Bean
public ThreadPoolTaskScheduler taskScheduler(){
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
return scheduler;
}</code></pre><p>crm服务由于定时任务配置的不多,并且在资源足够的情况下,任务执行速度相对较快,并未设置定时任务的线程池</p><p>定时任务里程序方法如何造成线程一直未释放,导致阻塞。</p><p>在问题定位时,产生的问题来自CountDownLatch无法归零,导致整个主线程hang在那里,无法释放。</p><p>在api中当调用await时候,调用线程处于等待挂起状态,直至count变成0再继续,大致原理如下:<br><img src="/img/bVcRRck" alt="image.png" title="image.png"></p><p>因此将目光焦点转移至await方法,使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。如果当前计数为零,则此方法立刻返回true 值。如果当前计数大于零,则出于线程调度目的,将禁用当前线程,且在发生以下三种情况之一前,该线程将一直处于休眠状态:由于调用 countDown() 方法,计数到达零;或者其他某个线程中断当前线程;或者已超出指定的等待时间。</p><p>Executors.newFixedThreadPool这是个有固定活动线程数。当提交到池中的任务数大于固定活动线程数时,任务就会放到阻塞队列中等待。CRM该定时任务里为了加快任务处理,运用多线程处理,设置的CountDownLatch的count大于ThreadPoolExecutor的固定活动线程数导致任务一直处于等待状态,计数无法归零,导致主线程一直无法释放,从而导致crm一台仿真服务的定时任务处于瘫痪状态。</p><h2>03如何学习java并发编程</h2><p>为了学习好并发编程基础,我们需要有一个上帝视角,一个宏观的概念,然后由点及深,掌握必备的知识点。我们可以从以下两张思维导图列举出来的逐步进行学习。<br><img src="/img/bVcRRcs" alt="image.png" title="image.png"></p><p><strong>必备知识点</strong><br><img src="/img/bVcRRct" alt="image.png" title="image.png"></p><h2>04线程</h2><p>列举了如此多的案例都是围绕线程展开的,所以我们需要更深地掌握线程,它的概念,它的原则,它是如何实现交互通信的。</p><p><strong>以下的一张图可以更通俗地解释进程、线程的区别</strong><br><img src="/img/bVcRRcz" alt="image.png" title="image.png"></p><p><strong>进程</strong>: 一个进程好比是一个程序,它是 资源分配的最小单位 。同一时刻执行的进程数不会超过核心数。不过如果问单核CPU能否运行多进程?答案又是肯定的。单核CPU也可以运行多进程,只不过不是同时的,而是极快地在进程间来回切换实现的多进程。电脑中有许多进程需要处于「同时」开启的状态,而利用CPU在进程间的快速切换,可以实现「同时」运行多个程序。而进程切换则意味着需要保留进程切换前的状态,以备切换回去的时候能够继续接着工作。所以进程拥有自己的地址空间,全局变量,文件描述符,各种硬件等等资源。操作系统通过调度CPU去执行进程的记录、回复、切换等等。</p><p><strong>线程</strong>:线程是独立运行和独立调度的基本单位(CPU上真正运行的是线程),线程相当于一个进程中不同的执行路径。</p><p><strong>单线程</strong>:单线程就是一个叫做“进程”的房子里面,只住了你一个人,你可以在这个房子里面任何时间去做任何的事情。你是看电视、还是玩电脑,全都有你自己说的算。想干什么干什么,想什么时间做什么就什么时间做什么。</p><p><strong>多线程:</strong>但是如果你处在一个“多人”的房子里面,每个房子里面都有叫做“线程”的住户:线程1、线程2、线程3、线程4,情况就不得不发生变化了。</p><p>在多线程编程中有”锁”的概念,在你的房子里面也有锁。如果你的老婆在上厕所并锁上门,她就是在独享这个“房子(进程)”里面的公共资源“卫生间”,如果你的家里只有这一个卫生间,你作为另外一个线程就只能先等待。<br><img src="/img/bVcRRcZ" alt="image.png" title="image.png"></p><p>线程最为重要也是最为麻烦的就是线程间的交互通信过程,下图是线程状态的变化过程:<br><img src="/img/bVcRRc4" alt="image.png" title="image.png"></p><p>为了阐述线程间的通信,简单模拟一个生产者消费者模型:</p><p>生产者</p><pre><code>CarStock carStock;
public CarProducter(CarStock carStock){
this.carStock = carStock;
}
@Override
public void run() {
while (true){
carStock.produceCar();
}
}
public synchronized void produceCar(){
try {
if(cars < 20){
System.out.println("生产者..." + cars);
Thread.sleep(100);
cars++;
notifyAll();
}else {
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}</code></pre><p><strong>消费者</strong></p><pre><code>CarStock carStock;
public CarConsumer(CarStock carStock){
this.carStock = carStock;
}
@Override
public void run() {
while (true){
carStock.consumeCar();
}
}
public synchronized void consumeCar(){
try {
if(cars > 0){
System.out.println("销售车..." + cars);
Thread.sleep(100);
cars--;
notifyAll();
}else {
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}</code></pre><p><strong>消费过程</strong><br><img src="/img/bVcRRdb" alt="image.png" title="image.png"></p><p><strong>通信过程</strong></p><p>对于此简单的生产者消费者模式可以运用队列、线程池等技术对程序进行改进,运用BolckingQueue队列共享数据,改进后的消费过程</p><p><img src="/img/bVcRRdf" alt="image.png" title="image.png"></p><h2>05并发编程三大特性</h2><p>并发编程实现机制大多都是围绕以下三点:原子性、可见性、有序性</p><p>1)原子性问题</p><pre><code>for(int i = 0; i < 20; i++){
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
res++;
normal++;
atomicInteger.incrementAndGet();
}
});
thread.start();
}</code></pre><p>运行结果:</p><p>volatile: 170797<br>atomicInteger:200000<br>normal:182406</p><p>这就是原子性问题,原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。<br>如果一个操作是原子性的,那么多线程并发的情况下,就不会出现变量被修改的情况。</p><p>2)可见性问题</p><pre><code>class MyThread extends Thread{
public int index = 0;
@Override
public void run() {
System.out.println("MyThread Start");
while (true) {
if (index == -1) {
break;
}
}
System.out.println("MyThread End");
}
}</code></pre><p>main线程将index修改为-1,myThread线程并不可见,这就是可见性问题导致的线程安全,可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。</p><p><strong>3)有序性问题</strong></p><p><strong>双检索单例懒汉模式</strong><br><img src="/img/bVcRRdr" alt="image.png" title="image.png"></p><p><strong>有序性</strong>: Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。</p><h2>06思考题</h2><p>有时为了尽快释放资源,避免无意义的耗费,会令部分功能提前结束,例如许多抢名额问题,这里出一个思考题供大家参考实现:<br>题:8人百米赛跑,要求前三名跑到终点后停止运行,设计该问题的实现。</p><p>参考资料:<br>1.亿级流量Java高并发与网络编程实战<br>2.LMAX文章(<a href="https://link.segmentfault.com/?enc=xch13qvYhk1C2pS4wdqUvQ%3D%3D.2ZtG4Qa53Z9%2FiIqO%2FEbbbd7QHZgTxPy0BazGC0kmgqg%3D" rel="nofollow">http://ifeve.com/lmax/)</a></p><p>下章预告:</p><ul><li>Volatile和Syncronize关键字</li><li>Volatile关键字</li><li>Synchronized关键字Volatile关键字</li><li>Synchronized关键字</li></ul><p>关于好未来技术更多内容请:微信扫码关注「好未来技术」微信公众号<br><img src="/img/bVcRRdH" alt="image.png" title="image.png"></p>
Flutter Package 开发、发布、使用三部曲
https://segmentfault.com/a/1190000039893914
2021-04-25T11:55:21+08:00
2021-04-25T11:55:21+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p><strong>前言:</strong>批改移动端在此次重构的时候,技术选型上我们团队选择了Flutter来进行本次移动端的重构,因为使用Flutter可以在UI的开发效率上充分节省人效,可以把更多的时间放在其他方面的研究上。但是在实际开发中,有部分的功能Flutter是不支持的,还需要native端来进行支持,而如何使native端支持的功能可以方便的、高效的服务于我们自己的同时又可以对外提供便利,在其他项目或者是其他团队在需要使用同样功能的时候可以不需要重复的造轮子,如Java的jar包,Android的aar包,Web的npm包, iOS的pod包等,而在Flutter中,Flutter也是支持使用由其他开发者贡献给Flutter和Dart生态系统的共享软件包,使我们可以快速构建应用程序,而无需从头开始开发所有应用程序,这就是我们今天要介绍的Package。 </p><p>所以针对package的了解上就不能仅仅止步于会使用的层次,更需要了解如何开发一个属于自己的package以及如何将自己的package成功发布到pub仓库或者是私有仓库供他人使用。</p><h2>01 package介绍</h2><p><strong>什么是Package </strong></p><p>使用Package可以创建能轻松共享的模块化代码,而一个最小的Package包括:<br>一个pubspec.yaml文件:声明了package的名称、版本、作者等的元数据文件。<br>一个 lib 文件夹:包括包中公开的(public)代码,最少应有一个<package-name>.dart文件。 </p><p><strong>Package类型: </strong></p><p>Flutter Package(dart包):Flutter纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget<br>Flutter Plugin(插件包):一种专用的Dart包,其中包含用Dart代码编写的API,以及针对Android(使用Java或Kotlin)和针对iOS(使用OC或Swift)平台的特定实现<br>Plugin其实就是一个特殊的Package。Flutter Plugin提供Android或者iOS的底层封装,在Flutter层提供组件功能,使Flutter可以较方便的调取Native的模块。很多平台相关性或者对于Flutter实现起来比较复杂的部分,都可以封装成Plugin 。Flutter与native之间的通信原理此篇文章我们不深入讨论,通信原理可以参考下图:<br><img src="/img/remote/1460000039893916" alt="" title=""></p><h2>02 package 开发</h2><p>我们了解到了Package是什么之后,那如何开发我们自己的package包呢?<br><strong>创建一个包或者是插件一般有两种方式:</strong></p><ul><li>可视化方式</li><li>命令行方式<br><strong>可视化方式</strong></li><li>打开Android Studio -> 选择Create New Flutter Project;</li><li><img src="/img/remote/1460000039893917" alt="" title=""></li><li>如果想要创建插件包 则选择Flutter Plugin;</li><li>如果想要创建Dart包 则选择Flutter Package;<br><img src="/img/remote/1460000039893918" alt="" title=""></li><li>根据需要修改相关内容,然后点击Next;<br><img src="/img/remote/1460000039893919" alt="" title=""></li><li>根据需要修改package name以及选择package开发中需要的语言 (选择pulgin包才会有此步骤)<br><img src="/img/remote/1460000039893920" alt="" title=""></li><li>创建成功 <br><strong>命令行方式</strong></li><li>创建Dart包<br>flutter create --template=package dartPackageNam</li><li>创建plugin包<br>flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin -i swift pluginPackageName<br>参数:<br> --org:指定您的组织,使用反向域名表示法 <br> --template: 指定是纯Dart包还是Plugin包 <br> --platforms: 代表指定插件支持的平台 可用的平台有:android、ios、web、linux、macos 和windows <br> -a 指定Android平台所使用的语言 java、kotlin <br> -i 指定iOS平台所支持的语言 objc、swift</li></ul><p><strong>Package包创建成功之后,查看工程下的文件: </strong><br><strong>纯Dart包 包含的文件:</strong></p><ul><li><strong>LICENSE 文件</strong> # 许可证文件。</li><li><strong>test/flutter_package_first_test.dart文件</strong> ## Package 的 单元测试 文件。</li><li><strong>.gitignore 文件</strong> ## 告诉 git 系统应该隐藏哪些文件或文件夹的一个隐藏文件。</li><li><strong>pubspec.yaml 文件</strong> ## pub 工具需要使用的,包含 package 依赖的 yaml 格式的文件。</li><li><strong>README.md 文件</strong> ## 起步文档,用于描述 package。</li><li><strong>lib/flutter_package_first.dart 文件</strong> ## package 的 Dart 实现代码</li><li><strong>CHANGELOG.md 文件</strong> ## 用于记录 package 的版本变更<br><img src="/img/remote/1460000039893921" alt="" title=""><br><strong>Plugin包 包含的文件:</strong></li><li><strong>lib/flutter_plugin.dart 文件</strong> ## Dart 插件 API 实现。</li><li><strong>android/src/main/java/com/example/flutter_plugin/FlutterPlugin 文件</strong> ## Android 平台原生插件 API 实现(使用 Kotlin 编程语言)。</li><li><strong>ios/Classes/FlutterPlugin.m 文件</strong> ## iOS 平台原生插件 API 实现(使用 Objective-C 编程语言)。</li><li><strong>example/ 文件</strong> ## 一个依赖于该插件并说明了如何使用它的 Flutter 应用。<br><img src="/img/remote/1460000039893922" alt="" title=""><br>如果要编辑iOS目录下相关文件或者是Android目录下的相关文件 需要去特定的编辑器中打开各端的文件进行编辑</li><li>iOS端: <br><img src="/img/remote/1460000039893923" alt="" title=""></li><li><p>Android端:<br><img src="/img/remote/1460000039893924" alt="" title=""></p><h2>03 package 发布</h2><p>我们已经了解了package包是什么,以及如何制作不同的package包,那如何发布Package包呢?(集团pub仓库发布文档)<br><strong> package发布位置可以选择:</strong></p><ul><li>pub.dev</li><li>私有pub仓库(集团pub仓库)</li><li>git仓库</li></ul><p>在发布package之前,要先检查这几个文件:pubspec.yaml、README.md、CHANGELOG.md 确保这几个文件完整。</p></li><li>pubspec.yaml配置文件的配置:</li><li>name: xxxx ## pub库名称</li><li>version: 0.0.1 ## 当前插件版本 确保每次发布与之前的版本不一致</li><li>description: xxxxxxxxx ## pub库简介</li><li>homepage:<a href="https://link.segmentfault.com/?enc=Oonb1B8VfvStHW5zhVTr6g%3D%3D.2yYpbQQWgx4id6NaFMeRQ6GDHLan3658qS8LZgUdGkw%3D" rel="nofollow">https://github.com/xxxx</a> ## pub库git地址</li><li>publish_to:<a href="https://link.segmentfault.com/?enc=1hFBCGJp8RiDdvKQ2T1BGw%3D%3D.g9FQcpk%2BjZe%2FFV2ZsKk6srH4DXxA5KRYwtTHuwXzhZs%3D" rel="nofollow">http://pub.100tal.com/</a> ## 私有pub库上传地址 如果不是私有pub仓库 不用配置</li><li>author:xxx<xxx@100tal.com> ## pub库作者</li><li>检测配置文件:<br>flutter packages pub publish --dry-run</li><li><p>发布<br>flutter packages pub publish</p><h2>04 package 使用</h2><p>package包发布成功之后,我们该如何在项目中使用呢?<br><strong>首先是在pubSpec.yaml中添加依赖</strong></p></li><li>依赖pub.dev仓库的package包 <br> #实例1 在该项目中,此插件指定版本为1.4.0 <br> flutter_plugin: ^1.4.0 <br> #实例2 在该项目中,此插件最低版本为1.2.3 <br> flutter_plugin: ">=1.2.3" <br> #实例3 在该项目中,此插件最低支持2.0.0版本,但不支持3.0.0以上版本 <br> flutter_plugin: ">=2.0.0 <3.0.0" <br> #实例4 在该项目中,此插件最高支持到1.2.3以下版本 <br> flutter_plugin: "<1.2.3"</li><li><p>依赖git仓库的package包</p><ul><li><p>flutter_plugin:</p><ul><li>git: url: <a href="https://link.segmentfault.com/?enc=5PisFVx0zfOR4nB0j7V%2FTQ%3D%3D.6BgBmJHmyQzBLc%2BUeZlUlkV%2BDYSBz%2Fmmd0mE%2FrlaU1DDSK7tM2qDcZVLd8e12tx%2B" rel="nofollow">https://github.com/xxxxxx/xxx...</a> #git仓库地址</li><li>path: xxxxx #如果项目不是在git地址的根目录 则需要指定path</li><li>ref: ‘1.0.0' #指定的版本 对应git仓库中的tag标签 也可以指定分支 ref: some-branch</li></ul></li></ul></li><li><p>依赖私有pub仓库的package包<br>xes_recorder:</p><pre><code> hosted:
name: xes_recorder
url: http://pub.100tal.com
version: 0.0.1</code></pre></li><li><p>依赖本地package<br> flutter_pub:</p><pre><code> path: ../ #可以是相对路径 也可以是绝对路径</code></pre><h2>05 发布过程中可能会遇到的问题记录</h2><p>1)author 在最新版本进行‘flutter packages pub publish --dry-run’检测的时候 会报警告 需要移除掉才能检测通过。<br>2)执行发布命令的时候 会遇到需要登录以下地址去授权的情况 需要登录google账号 请使用chrome打开去验证。<br><img src="/img/remote/1460000039893925" alt="" title=""></p></li><li>验证通过-开始上传-上传成功去 pub.dev 或者自己的私有pub仓库 查看是否发布成功 。<br><img src="/img/remote/1460000039893926" alt="" title=""></li><li>验证成功-开始上传-如果超时未成功则需要绕过google授权重新上传。<br>3)跳过google验证方法;</li><li>下载pub项目[下载地址:<a href="https://link.segmentfault.com/?enc=A9K8U8H4Df1behuoY2BPhw%3D%3D.zKyEqefPj5iFf9r38xPgG8UaXJ7BQNaPPMhKjQViaxtdxUH0RU96RhMnom4pVUab" rel="nofollow">https://github.com/ameryzhu/pub]</a></li><li>使用Android Studio打开下载的项目 并在Terminal顺序执行:<br>pub get;<br>dart--snapshot=mypub.dart.snapshot bin/pu b.dart #执行完这个命令会在pub项目根目录下生成一个mypub.dart.snapshot文件 ;<br><img src="/img/remote/1460000039893927" alt="" title=""><br>4)把这个文件放到 ${flutterSDK Path}/bin/cache /dart-sdk/bin/snapshots/ 目录下;<br>5)然后用编辑器打开${flutterSDKPath}/bin/cache /da rt-sdk/bin/pub 文件;<br>6)把文件倒数第三行的:pub.dart.snapshot 替换为 mypub.dart.snapshot ;<br>7)保存-退出<br>8)重新执行发布命令<br><strong>如果执行了上述方法,在项目中执行pub get的时候会有版本冲突的错误,需要将上述方法修改的pub文件中的 mypub.dart.snapshot 恢复改为pub.dart.snaps hot 。</strong></li></ul><p>4月23日世界读书日,微信公众号免费送书啦~<br>扫码关注「好未来技术」微信公众号回复「送书」即可参与本次活动<br>专属宠粉福利,还在等什么快来关注吧<br><img src="/img/remote/1460000039893928" alt="image.png" title="image.png"></p>
简述「培优呼叫平台」系统演进过程
https://segmentfault.com/a/1190000039402867
2021-03-12T15:50:40+08:00
2021-03-12T15:50:40+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<h2>前言</h2><p>我们知道,在一个技术领域里,随着技术的发展,最优解永远在变化。同样,一个业务系统也一样,从初期创建到承接的业务量和业务场景的不断变化,在不同阶段时,面临的问题和系统的设计思路也是不断变化的。</p><p>之所以想分享一下系统演进过程,是因为每个系统的业务场景和发展阶段虽然不同,但在技术实现上,回归到系统本身的设计和优化思想是万变不离其宗的,不同发展阶段的技术变化也基本是遵循一定规律。所以,本篇文章通过结合培优呼叫平台随着业务发展阶段的变化而采用的技术手段和架构优化,去看一下系统架构设计一般会经历的阶段,以及每个阶段都解决了哪些问题,同时会引出哪些问题。</p><h2>系统发展</h2><p>总的来说,培优呼叫平台起步较晚,目前所处的阶段也是比较初级的(1.X),虽然系统启动建设时行业中服务化、数据分布式存储、缓存、异步等技术都已经非常成熟了,但也遵循着系统设计的规律进行的,是基于自己的系统建设背景、业务量级的变化,在不同阶段有不同的架构设计侧重点,并不是盲目的在项目初期就去为了应用服务化架构技术而直接应用微服务架构、分布式数据存储架构等。(盲目的/过度的追从复杂的技术架构,会出现问题难以追查、难以维护等多方面问题)<br><strong>罗马不是一天建成的,系统的设计也是如此。</strong><br>呼叫平台发展历程<br><img src="/img/bVcPum6" alt="image" title="image"><br><strong>先简单说一下一般的系统架构的演进的方式:</strong></p><ol><li>单体服务器部署的应用与数据一体</li></ol><p> 2. 应用与数据分离<br> 3. 纵向扩展/横向扩展与服务器集群<br> 4. 增加缓存和异步<br> 5. 数据库读写分离<br> 6. 隔离部署<br> 7. 分布式数据库和分库分表<br> 8. 分布式服务架构(业务服务拆分:拆分出来的一个个业务服务随着发展一般又会重复这个过程)<br> 9. 微服务<br> 10. ……<br>(行业发展至今,1、2基本已经没有系统系统会再经历了)</p><p>呼叫平台就是老业务系统(老三代,一个巨型的涵盖培优整个业务功能的,面向各个方向使用者的核心业务系统)业务拆分后的一个独立业务单元(非服务单元),因此当平台从0到1.X的建设过程中,也算或多或少经历了这些系统架构演进的一些环节。</p><h3>01 业务服务拆分-客服专属呼叫平台建设</h3><p><strong>业务背景:</strong>客服人员没有一个统一的工作台,处理一个客户问题通常需要跨多个系统去查询去操作,还需要结合手工记录等人工方式去解决,业务效率低、客户问题解决率低。</p><p><strong>技术背景:</strong> 随着业务量的增长,原有的几十人的客服团队也在迅速扩张,客服伙伴需要的系统能力也不断提高,而原有的各系统除了能力不具备(功能层面暂且不谈),在技术层面,业务高峰时任意一个系统或者系统的某一个功能点出现瓶颈,都将导致整个系统的不可用。</p><p><em>例:报名高峰、上课高峰系统故障时,最焦躁的是客户,最困难就是我们的客服伙伴了,因为此时家长的第一个动作往往就是联系客服伙伴去解决问题,而客服老师也无法使用系统,不仅面临爆线的咨询量,还面临无法解决问题,只能安抚学员的尴尬情况。</em></p><p><strong>系统架构变化(业务服务拆分):冗余复杂的业务系统 ——> 轻量独立的呼叫系统</strong></p><p><strong>阶段特点和目标:</strong> 需要一个独立的、功能聚焦的、用户量和业务量不太高、业务场景简单的专属平台,保证这个平台的服务稳定性和易用性。<br><img src="/img/bVcPuof" alt="image" title="image"><br>可以看到,在这个系统的承载力、稳定性、扩展性、横向扩展、纵向扩展都已经成为瓶颈的阶段,需要把一个耦合各业务的系统拆解成一个个独立的业务服务。其实到这个「拆服务」阶段前,业务系统本身也已经经过几个阶段的系统架构变化,度过并解决了当时阶段的问题,但却已经无法解决现阶段的问题了。</p><p>原有的业务系统已经经历过系统架构的几个阶段的演进:<br>数据与应用分离<br>服务的纵向扩展(服务器、数据库、中间件等硬件的一次次增配)<br>增加缓存和异步<br>数据库读写分离</p><p>而「业务服务拆分」阶段的架构变化(也可以说是系统重构),正是一个新系统的诞生的方式之一。</p><p>呼叫平台也就应运而生,从0到1的建设客服伙伴使用的一站式的客服平台,聚焦在高效的、高质量的、高体验的、高扩展的客服专有系统,除了更精准的支持客服业务,也要能够承接持续增高的并发量、业务量,保证系统的可持续发展。</p><p>从呼叫1.0的系统架构可以看到,并不是从单体应用与数据、数据与应用分离、单一DB逐步演进,而在架构初期就已经建立了集群部署、读写分离、缓存与异步的基础,跳过了这些阶段。因为互联网技术发展到此阶段,这些是常规应用的必备基础了。</p><p><strong>【关键演进:系统架构演进之业务服务拆分】</strong></p><p><strong>基本定义:</strong>将一个应用拆成多个,部署到不同的服务器中。是当下相当常见的系统架构演进方式。</p><p><strong>解决的问题:</strong>代码耦合度高的问题、系统可维护性低、系统稳定性问题(鸡蛋不能放在一个篮子)、系统的承载能力问题,通过业务服务拆分可以更好做到业务模块的高内聚低耦合,每个业务由独立的团队维护,资源更聚焦,技术依赖和服务稳定性更高。</p><p><strong>引出的问题:</strong>服务间通信、分布式事务、业务数据聚合查询</p><p><strong><em>PS:</em></strong>这里不去深入赘述业务服务拆分的时机选择、服务拆分的原则和方法、服务拆分的规范等方法论,这些问题因业务不同,也都有一定的差异,需要基于标准方法论去case by case的参照。</p><h3>02 服务治理与隔离部署-保证系统基本稳定性</h3><p>矛盾的说,这个阶段很多系统都不会经历但也存在这样的问题,因为确实算是呼叫平台阶段性的一个架构优化,并稳定支撑系统到Step3,所以也呈现一下。</p><p>为什么说是保证系统的基本稳定性呢?对一个新的系统就需要服务治理了?又是什么原因就要隔离部署了呢?这就比较惭愧了,引用一句话来说真的是 「技术难度不高,但侮辱性极强」 ,因为这真不应该是一个技术难题,完全是系统架构本身就该具备的基础,但就是使用过程中暴露了系统设计与产品设计、业务调研的冲突,导致系统能力有所偏离,这也是Step1在落地过程中很常见的问题,在系统架构进行服务拆分阶段时需要注意的问题:</p><p><strong>业务类型:</strong>呼叫平台不仅仅需要满足常规的操作类业务,还有一个需要实时高频关注数据报表业务的场景;</p><p><strong>系统需要承载能力:</strong>实际系统使用并发量和业务量远高于预期;</p><p><strong>系统数据量:</strong>多次确认的没有历史数据,到刚使用就必须迁移大量的历史数据进来,打破承载三年数据量规划的初步设定;</p><p><strong>系统可观测性:</strong>当期未设计。</p><p>因此,直接导致了呼叫平台快速进入了新一阶段的架构优化。</p><p><strong>业务背景:</strong> 并发量和业务量快速增长,高于预期。</p><p><strong>技术背景:</strong> 核心业务与报表业务部署耦合,且历史迁移数据量大,系统稳定性不达标;平台依赖的底层各业务服务较多,出现问题时缺少监测和降级能力。</p><p><strong>系统架构变化:</strong> 完善服务监测和降级能力,第一是透明化业务问题,进行短期的核心业务服务与报表服务隔离部署。</p><p><strong>阶段特点和目标:</strong> 稳定性提高,系统承载能力的瓶颈问题短期得到解决。<br><img src="/img/bVcPuow" alt="image" title="image"><br>基于服务监测和快速的隔离部署,使得平台过渡到了一个可以支撑业务使用的平稳期,达到系统诞生的预期。其实这里也是隐含应用的一个系统演进的场景手段「横向扩展」,对老的业务系统来说,横向扩展虽然已经无法解决问题了,但是对呼叫平台来说,就是业务系统的婴儿版,所以又可以进行新一轮的架构演进,在这一阶段使用「横向扩展」这一策略就比较合适。</p><p><strong>【关键演进:系统架构演进之横向扩展】</strong></p><p><strong>基本定义:</strong>增加服务器、数据库等资源节点。是应对突增业务流量的常见手段。</p><p><strong>解决的问题:</strong>通过更多的服务节点去分担负载压力,可以有效的提高性能和可用性。</p><p><strong>引出的问题:</strong>无状态的应用一般没什么问题,有状态的应用需要考虑节点增删带来的业务一致性等问题。</p><h3>03 分布式数据库-保障系统的可持续发展</h3><p>这一阶段是很多系统随着业务的发展都必然要经历或者设计初期就要考虑的,是保证系统有能够应对接下来3年、5年甚至更久业务流量的护身符,也是系统扩展性的基础,呼叫平台之所以也快速进入了这个阶段,原因之一除了Step2中提到的系统历史数据量、业务并发量的因素外,更重要的是随着教育行业的快速发展,客服团队及客服团队的业务范围也都在飞速增长,是业务重要的抓手之一,通过系统和数据的观测,到了需要进行「数据库分库分表」的阶段,来奠定系统持续发展的基础。</p><p><strong>业务背景:</strong>业务范围和团队飞速增长。</p><p><strong>技术背景:</strong>业务数据增长过快,数据量增长较快。</p><p><strong>系统架构变化:</strong>数据库分库分表。</p><p><strong>阶段特点和目标:</strong>数据分布式存储,保障系统可持续的发展,解决业务量增长过快可能的瓶颈问题。<br><img src="/img/bVcPuoX" alt="image" title="image"><br>通过深入的业务分析和评估后,基于客服业务的特点,对呼叫平台的几大核心模块进行分表架构改造(水平拆分),这个「分布式数据存储」阶段的演进是原老业务系统没有经历也不具备可行性的阶段,原业务系统更重要的问题是业务庞大且耦合严重,不是数据分布式存储能解决的(也不具备实施条件),而拆分后的各个业务服务在应对快速发展的行业背景下,刚好非常适合、非常方便的对自有业务数据分布式存储的设计和演进。</p><p><strong>【关键演进:系统架构演进-分布式数据库和分库分表】</strong></p><p>基本定义:将原本存储在一个库/表的数据,分块的存储到多个库/表中。</p><p>解决的问题:减少数据库的负担,保证系统的高稳定性和高性能,支持更多的业务数据存储和应用。</p><p>引出的问题:事务一致性问题、跨节点关联join/分页/排序/函数问题、全局唯一主键问题、数据迁移/扩容问题等。</p><h3>04 微服务-呼叫自身业务更细粒度的模块化</h3><p>微服务是目前正要进行的一个阶段,随着业务的发展、场景和能力的不断拓展,呼叫平台本身也将要步入自己的微服务化建设,这本身与初期呼叫业务服务拆分,提供独立的服务平台并不冲突,是分布式服务架构的进化版,因为每个服务本身就是随着业务一步步生长、不断积累的。在当时,相对于原有业务系统,呼叫本身就是一个客服模块、就是一个最小业务单元,但随着客服体系的发展,现阶段呼叫已经成为与原业务系统同级别的系统了,已经有了自己的工单、任务、流程等业务单元/模块,各模块之间的可伸缩性、可扩展性及高性能需要更精细化的管理。</p><p><strong>【关键演进:系统架构演进之微服务】</strong></p><p>微服务虽然也是一种业务服务拆分,都是进行「拆」的操作,但也不完全等同,相对于业务拆分粒度更细,有一定的区别:</p><p><strong>拆分方式不同:</strong></p><p>业务服务拆分是将系统按照业务划分进行拆分,让拆分之后的服务区分担原单体服务的业务。</p><p>微服务则是在业务拆分基础上进行更细粒度的拆分,通过将服务拆成更小的模块,让一个方向下业务的每个小模块都可以独立运行,又能高效协同、管理。</p><p><strong>部署方式不同:</strong></p><p>业务服务拆分后,与常规服务部署一样,一般也是独立的服务器集群进行部署。</p><p>微服务不一定部署在多个服务器上,可以同时部署在统一服务器上。</p><p><strong>目的/作用不同:</strong></p><p>业务服务拆分:分散压力,是为了解决单体应用资源有限且耦合的问题,在一个服务器上无法支撑不同业务维度的用户更稳定更高的访问,然后通过服务拆分的方式部署到不同的服务集群,从而分担业务、分别处理业务。</p><p>微服务架构:分散能力,是需要对服务组件进行精细化设计,更好的进行服务解耦,让服务之间通过组合的方式完成高性能、高可用、可伸缩、可扩展的建设。</p><p>其次,分布式服务架构强调的是服务化以及服务的分散化,微服务架构通常是一种分布式服务架构,但微服务则更强调服务的专业化和精细分工,需要一定的专业知识和技术积累。所以,选择微服务通常意味着需要解决分布式架构的各种难题,在业务没有规模有没有太多变化的情况下,贸然采用/为了用而用微服务架构,会引入各种复杂性(如:部署工作、链路监控、服务治理等问题),得不偿失。</p><h2>总结</h2><p>系统的演进应该是一个循序渐进的过程,要以解决系统中存在的问题为目的和驱动力。需要根据实际业务所处的阶段进行选择,不能盲目的追从,要能压住技术伙伴的追求技术的躁动的心,脚踏实地一步一步的随需而变的进行。适合的才是最好的!</p><p>在系统演进过程中,可以根据实际情况遵循一定的思路进行:选择最熟悉的技术体系,通过最简单的系统设计满足业务需求和流量现状。</p><p>优先通过团队熟悉的技术组件和系统架构优化,去解决随着业务量的增加、业务场景的不断演进而出现的问题(如:单点问题、扩展平台、性能问题、存储问题等)。</p><p>然后才是通过系统重构去重新整体业务和架构调整解决问题。</p><p>本文是根据实际系统经历进行一些总结,路径和方案并不是标准和最佳实践,欢迎交流学习!</p>
速围观!好未来网校柯南平台开源啦!
https://segmentfault.com/a/1190000039346772
2021-03-05T16:05:27+08:00
2021-03-05T16:05:27+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<h2>背景</h2><p>经过在线教育业务中的持续打磨与迭代,柯南平台终于开源,旨在为行业内更多的的质效保障团队提供更专业更稳定的质效保障方案。随着业务与技术架构的不断变化,服务端的质量保障工作变得越来越复杂。近几年流量回放的方案在行业内落地生根,但大部分以工具为主并且使用成本与二次开发生成本较高,柯南平台应运而生。</p><h2>目标</h2><p>基于线上真实用户流量的录制回放能力与结果校验能力,为冒烟测试,集成回归测试,线上验证与线上巡检提供解决方案。</p><h2>核心功能</h2><p><strong>流量采集</strong></p><p>基于ES日志源的流量录制采集,平台化配置接入,降低使用成本,并且提供详细的流量采集数据。</p><p><strong>流量回放</strong></p><p>分布式的后端架构,为流量回放提升执行效率,支持服务鉴权配置,基于http协议的回放符合真实业务场景。</p><p><strong>结果校验</strong></p><p>流量回放的常规校验方式基本上是以流量结果的DIFF为主,但大量的流量噪声(时间戳,自增数据...)一直影响结果的准确性,柯南平台在回放中基于配置的jsonSchema做第一层校验,再结合自研的降噪比对服务进行流量DIFF的第二层校验,从而保障了结果校验的准确性,大大提升了流量回放结果的可信度。</p><p><img src="/img/remote/1460000039346774" alt="回放结果校验.png" title="回放结果校验.png"> </p><p>回归规则校验 <br> </p><p><img src="/img/remote/1460000039346778" alt="流量结果校验.png" title="流量结果校验.png"></p><p> 流量比对<br> </p><h2>平台优势与应用场景</h2><p><strong>优势</strong></p><ul><li>解决传统自动化覆盖率低,维护成本高的问题</li><li>多规则的流量结果断言校验</li><li>多规则的流量结果比对支持</li><li>流量数据可用于自动化测试与性能测试</li><li>交互简单,配置化接入</li><li>开源共建,持续优化</li></ul><p><strong>应用场景</strong></p><ul><li>提测质量卡点</li><li>CI/CD流水线质量卡点</li><li>服务线上监控巡检</li></ul><p><strong>平台业务架构</strong><br><img src="/img/remote/1460000039346776" alt="后端业务架构.png" title="后端业务架构.png"> </p><p>业务架构 </p><p><strong>平台技术架构</strong></p><p><img src="/img/remote/1460000039346777" alt="后端技术架构.png" title="后端技术架构.png"></p><p>服务端架构</p><p><strong>平台能力及功能</strong></p><p><img src="/img/remote/1460000039346779" alt="柯南能力图.png" title="柯南能力图.png"></p><p><strong>使用须知</strong></p><ul><li>流量采集: ES日志;</li><li>回放协议: http协议;</li><li>具体环境可参考开源详细技术文档</li></ul><h2>写在最后</h2><p>质效的提升也许不能单单通过一个平台,技术与人的结合才能带来更大的突破。善于利用技术创新才能从容的面对越来越频繁的需求,越来越复杂的业务,柯南平台的技术方案产出于学而思网校的大班业务并且逐步通用化,平台现已开源,希望更多优秀的人或团队参与进来,为质效保障工作提供更多的解决方案。</p><p><strong>平台官网</strong> <a href="https://link.segmentfault.com/?enc=JxCbsdHM9i3YbI0ndG%2BkEw%3D%3D.JZv0VhAL8QnowxknjrTd%2BN13ntMppGGZHyOBq9nj4J2nkv84lkmdzAj8EVyCfoNyEV4kBzwTBUdWXtOImwbakV0cBr4dLxY8hw4I3QwvLiWMT5Ypw6JeIMlDoxoEd5GN" rel="nofollow">https://tal-tech.github.io/conan</a></p><p><strong>Github</strong> <a href="https://link.segmentfault.com/?enc=ITlulgCEt8ohfJIlXLTrhA%3D%3D.KbF6lgBd13%2F%2FDma%2Bc%2Fh%2BDgcu5mux7NV7iUbB7VSD5o1n2SqQK4yIBnZkSjdWszlf" rel="nofollow">https://github.com/tal-tech/c...</a></p><p><strong>更多详细介绍</strong> <a href="https://link.segmentfault.com/?enc=Kwggg8BOiU%2Bj8qUBxFBicA%3D%3D.%2B4M6cqKTBRempZbcSSxZ3d9Hp%2Bg%2FUuT%2B5oGqxWb4dgX%2BzMz7gj001t3II45VC8tgrqtzn7huMJcmU3aCBdcT0Gnj5KHqss7309cBfXb%2Bho4l1HQ%2FwbazVKH0tzNK1HHnJ3lrnRIEqp7YRYppcJLwBw%3D%3D" rel="nofollow">https://mp.weixin.qq.com/s/1Cvi5kkqfF9y1rBi97qLwg</a></p><p><strong>柯南官方QQ群</strong></p><p><img src="/img/remote/1460000039346775" alt="" title=""></p>
Clickhouse架构及应用
https://segmentfault.com/a/1190000039292250
2021-02-26T16:27:19+08:00
2021-02-26T16:27:19+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
7
<p><strong>内容大纲:</strong></p><ol><li>背景;</li><li>Clickhouse介绍;</li><li>Clickhouse架构及性能;</li><li>Clickhouse在好未来的实践;</li><li>建设与规划;</li><li>参考文献。</li></ol><h2>背景</h2><p>在日志中心发展早期,日志检索分析主要基于elasticsearch进行,随着日志中心接入的业务越来越多,数据量也逐渐增长,基于日志进行分析和监控告警的需求变得越来越复杂,很难用elasticsearch来满足,所以需要根据需求场景来选择合适数据库。我们需要的:</p><ul><li>数据量会很大,因此需要分布式;</li><li>支持实时写入,支持快速计算,在较短时间内能完成计算;</li><li>强大的sql能力,实时指标sql化;</li><li>人力有限,运维需要简单;</li><li>高效的压缩比存储,服务器有限,可以用更少的服务器存储更多的数据;</li></ul><p>基于以上特点,我们选择了Clickhouse,接下来会介绍Clickhouse的特点、系统架构以及使用情况。</p><h2>Clickhouse介绍</h2><h3>1、Clickhouse特点</h3><p>===============<br><img src="/img/bVcO1Qh" alt="image" title="image"></p><p>图2-1 Clickhouse特点图</p><p>可以看到,Clickhouse的特点恰是我们所需要的。接下来详细的介绍一下核心特性:</p><p><strong>1)完备的DBMS功能:</strong></p><p>ClickHouse拥有完备的管理功能,所以它称得上是一个DBMS ( Database Management System,数据库管理系统 ),而不仅是一个数据库。</p><p>作为一个DBMS,它具备了一些基本功能,如:</p><ul><li>DDL ( 数据定义语言 ):可以动态地创建、修改或删除数据库、表和视图,而无须重启服务;</li><li>DML ( 数据操作语言 ):可以动态查询、插入、修改或删除数据;</li><li>权限控制:可以按照用户粒度设置数据库或者表的操作权限,保障数据的安全性;</li><li>数据备份与恢复:提供了数据备份导出与导入恢复机制,满足生产环境的要求;</li><li>分布式管理:提供集群模式,能够自动管理多个数据库节点。</li></ul><p><strong>2) 列式存储与数据压缩</strong></p><p>列式存储和数据压缩,对于一款高性能数据库来说是必不可少的特性。想让查询变得更快,最简单且有效的方法是减少数据扫描范围和数据传输时的大小,而列式存储和数据压缩就可以帮助我们实现上述两点。由于Clickhouse是真正意义上的列式存储,每一列都在不同的文件下,所以该文件数据类型一致,可以更有效的压缩。</p><p><strong>3) 向量化执行引擎</strong></p><p>向量化执行以列存为前提,主要思想是每次从磁盘上读取一批列,这些列以数组形式组织。每次next都通过for循环处理列数组。这么做可以大幅减少next的调用次数。相应的CPU的利用率得到了提高,另外数据被组织在一起。</p><p>可以进一步利用CPU硬件的特性,如SIMD,将所有数据加载到CPU的缓存当中去,提高缓存命中率,提升效率。在列存储与向量化执行引擎的双重优化下,查询执行的速度会有一个非常巨大的飞跃。</p><p><strong>4) 关系模型与SQL查询</strong></p><p>ClickHouse是一个关系型数据库。它几乎可以支持近百分之九十的sql作为查询语句,比如group by,order by等。</p><p><strong>5) 多样化的表引擎</strong></p><p>ClickHouse和mysql一样,也将存储部分进行了抽象,把存储引擎作为一层独立的接口。所以说Clickhouse实现了很多种表引擎,比如mergetree,log,memory等类型的引擎,每一种表引擎都有着各自的特点,用户可以根据实际业务场景的要求,选择合适的表引擎使用。</p><p><strong>6) 多线程与分布式</strong><br>ClickHouse几乎具备现代化高性能数据库的所有典型特征,对于可以提升性能的手段可谓是一一用尽,对于多线程和分布式这类被广泛使用的技术,自然更是不在话下。</p><p><strong>7) 多主架构</strong></p><p>HDFS、Spark、HBase和Elasticsearch这类分布式系统,都采用了Master-Slave主从架构,由一个管控节点作为Leader统筹全局。而ClickHouse则由于它的集群架构和其他数据库不同,这种架构使得它是一个多主架构。</p><p><strong>8) 在线查询</strong></p><p>ClickHouse采用了LSM树结构,所以使得Clickhouse的插入量可以很大。同时,Clickhouse的内部优化,使得在复杂查询的场景下,它也能够做到极快响应,且无须对数据进行任何预处理加工。达到了实时数仓的效果</p><p><strong>9) 数据分片与分布式查询</strong></p><p>Clickhouse拥有分布式能力,自然支持数据分片,数据分片是将数据进行横向切分,这是一种在面对海量数据的场景下,解决存储和查询瓶颈的有效手段。ClickHouse并不像其他分布式系统那样,拥有高度自动化的分片功能。ClickHouse提供了本地表 ( Local Table ) 与分布式表 ( Distributed Table ) 的概念。一张本地表等同于一份数据的分片。而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。</p><h3><strong>2、C</strong><strong>lickhouse常见应用场景</strong></h3><ul><li>电信行业用于存储数据和统计数据使用;</li><li>新浪微博用于用户行为数据记录和分析工作;</li><li>用于广告网络和RTB,电子商务的用户行为分析;</li><li>日志分析;</li><li>检测和遥感信息的挖掘;</li><li>商业智能;</li><li>网络游戏以及物联网的数据处理和价值数据分析;</li><li>最大的应用来自于Yandex的统计分析服务Yandex.Metri ca。</li></ul><h2>Clickhouse架构及性能</h2><p>Clickhouse的集群架构是和其他的数据集群有一定的区别,他的集群能力是表级别的,而我们熟知的大数据体系,比如hadoop系列的集群都是服务级别的。例如,一个hdfs集群,所有文件都会切片、备份;而Clickhouse集群中,建表时也可以自己决定用不用,也就是说其实Clickhouse单节点就能存活。可能有其他的大数据经验的人对这种设计会有点奇怪,后面会从单机架构到集群架构,详细的去介绍。</p><p>=======================================================================================================================================================================================================</p><h3>1、Clickhouse单机架构设计</h3><p>官方介绍Clickhouse架构的资料比较匮乏,依据已有的经验结合外部资料,根据自己的理解还原Clickhouse的架构如下:</p><p><img src="/img/bVcO1Qt" alt="image" title="image"><br>图3-1 clickhouse单机架构图</p><p><strong>1)Parser与Interpreter</strong></p><p>Parser和Interpreter是非常重要的两组接口:Parser分析器是将sql语句已递归的方式形成AST语法树的形式,并且不同类型的sql都会调用不同的parse实现类。而Interpreter解释器则负责解释AST,并进一步创建查询的执行管道。Interpreter解释器的作用就像Service服务层一样,起到串联整个查询过程的作用,它会根据解释器的类型,聚合它所需要的资源。首先它会解析AST对象;然后执行"业务逻辑" ( 例如分支判断、设置参数、调用接口等 );最终返回IBlock对象,以线程的形式建立起一个查询执行管道。</p><p><strong>2)表引擎</strong></p><p>表引擎是ClickHouse的一个显著特性,上文也有提到,clickhouse有很多种表引擎。不同的表引擎由不同的子类实现。表引擎是使用IStorage接口的,该接口定义了DDL ( 如ALTER、RENAME、OPTIMIZE和DROP等 ) 、read和write方法,它们分别负责数据的定义、查询与写入。</p><p><strong>3)DataType</strong></p><p>数据的序列化和反序列化工作由DataType负责。根据不同的数据类型,IDataType接口会有不同的实现类。DataType虽然会对数据进行正反序列化,但是它不会直接和内存或者磁盘做交互,而是转交给Column和Filed处理。</p><p><strong>4)Column与Field</strong></p><p>Column和Field是ClickHouse数据最基础的映射单元。作为一款百分之百的列式存储数据库,ClickHouse按列存储数据,内存中的一列数据由一个Column对象表示。Column对象分为接口和实现两个部分,在IColumn接口对象中,定义了对数据进行各种关系运算的方法,例如插入数据的insertRangeFrom和insertFrom方法、用于分页的cut,以及用于过滤的filter方法等。而这些方法的具体实现对象则根据数据类型的不同,由相应的对象实现,例如ColumnString、ColumnArray和ColumnTuple等。在大多数场合,ClickHouse都会以整列的方式操作数据,但凡事也有例外。如果需要操作单个具体的数值 ( 也就是单列中的一行数据 ),则需要使用Field对象,Field对象代表一个单值。与Column对象的泛化设计思路不同,Field对象使用了聚合的设计模式。在Field对象内部聚合了Null、UInt64、String和Array等13种数据类型及相应的处理逻辑。</p><p><strong>5)Block</strong></p><p>ClickHouse内部的数据操作是面向Block对象进行的,并且采用了流的形式。虽然Column和Filed组成了数据的基本映射单元,但对应到实际操作,它们还缺少了一些必要的信息,比如数据的类型及列的名称。于是ClickHouse设计了Block对象,Block对象可以看作数据表的子集。Block对象的本质是由数据对象、数据类型和列名称组成的三元组,即Column、DataType及列名称字符串。Column提供了数据的读取能力,而DataType知道如何正反序列化,所以Block在这些对象的基础之上实现了进一步的抽象和封装,从而简化了整个使用的过程,仅通过Block对象就能完成一系列的数据操作。在具体的实现过程中,Block并没有直接聚合Column和DataType对象,而是通过ColumnWith TypeAndName对象进行间接引用。</p><h3>2、Clickhouse集群架构设计</h3><p>Clickhouse是集群是通过配置clickhouse_remote_servers来管理集群的。在配置中,可以配置集群名字,集群所需要节点的信息,通过这些节点可以配置分片和副本机制。</p><p><strong>简单的配置为例:</strong></p><pre><code><yandex>
<clickhouse_remote_servers>
<cluster1>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse-node1</host>
<port>9000</port>
</replica>
<replica>
<host>clickhouse-node2</host>
<port>9001</port>
</replica>
</shard>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse-node3</host>
<port>9000</port>
</replica>
<replica>
<host>clickhouse-node4</host>
<port>9001</port>
</replica>
</shard>
...
</cluster1>
...
</clickhouse_remote_servers>
...
</yandex></code></pre><p>以上集群配置完之后,想要用到Clickhouse的集群能力,还需要使用Replicated <em>MergeTree+Distributed引擎,该引擎是"本地表 + 分布式表"的方式,因此可以实现多分片多副本;下面具体介绍下Replicated</em> MergeTree引擎和Distributed引擎。</p><h4><strong>1)Replicated*MergeTree引擎</strong></h4><p>首先需要介绍下MergeTree引擎,这也是Clickhouse存储数据的最核心引擎,之前所说的特点主要就是针对该引擎所描述的。MergeTree引擎则是在MergeTree基础上中扩展了一些功能引擎,包括支持ReplacingMergeTree,SummingMergeTree等等MergeTree家族引擎,详细了解可看官网mergetree引擎介绍,不带replication的MergeTree引擎都可以看成单机引擎,也就是说它们是在单节点上存在的。</p><p>而使用Replicated<em>MergeTree就是将MergeTree引擎的数据通过Zookeeper调节,达到副本的效果。比如上述配置中,我们首先可以在cluster1中的每个节点上创建Replicated</em>MergeTr ee表,通过配置文件,可以看到Clickhouse-node1和Clickho use-node2是在同一个shard里的,每个shard标签里的replica就代表复制节点。这时我们创建表时将两个副本指定在同一个zo okeeper目录下,那么写入到node1的数据会复制到node2,写入node2的数据会同步到node1,达到预计的复制效果。</p><p>到这里,每个节点上的本地表已经完成,但是多个分片的数据如何进行汇总,则需要下面的Distributed引擎。</p><h4><strong>2)</strong><strong>Distributed引擎</strong></h4><p>使用Distributed引擎的表本身不存储任何数据,但允许在多个服务器上进行分布式查询处理,读取是自动并行的。在读取期间,会使用远程服务器上的表索引(也就是我们上述使用的Replicate d*MergeTree引擎)。</p><p>在读写数据时,如果使用Distributed表,可以按照配置文件配置的分片方案,从不同分片(shard)中读写数据,做到数据分片的效果。比如我们读取数据时,是通过Distributed引擎表读取,这个时候它会读取集群中的每个分片的数据做汇总计算。注意,这一块会有深度分页的情况,有些sql可以先分散在每个节点上执行完再在查询节点做结果聚合,而有些则无法做结果聚合,必须将所有数据同步到查询节点,由查询节点统一汇总,这种情况就需要根据具体的数据情况进行优化。</p><p><img src="/img/bVcO1QI" alt="image" title="image"><br>图3-2 本地表加分布式表的查询流程图</p><p>图3-2是一个2分片2副本的架构,使用的是Replicated*Merge Tree + Distributed引擎模式。红色的数字代表节点的话,也就是节点1和2互为副本,3和4互为副本。</p><p>图中events为Distributed引擎表,也叫分布式表;events_loc al为Replicated*MergeTree引擎表,也叫本地表。该图中,分布式表只在节点3中创建,线上环境一般会在每个节点上都创建一个分布式表(不会消耗资源,因为分布式表不会存储数据)。</p><p>执行查询时,会访问一个节点的分布式表,该图中访问的是节点3中分布式表。然后分布式表会分别的读取2个分片的数据,在这里,它读取了节点3和节点2的本地表数据,这两个节点加在一块就是完整的数据。汇总查询后将结果(Result Set)返回。</p><h3>3、Clickhouse性能</h3><p><strong>1)插入:</strong>单机100-150M/s的插入速度;</p><p><strong>2)查询:</strong>单字段groupby没有索引,1亿数据查询需要2.324s。有索引下,查询时间为0.101秒。可以看到Clickhouse的查询速度是及其快的,市面上常见的数据库基本都达不到这种性能;</p><p><strong>3)其他:</strong>并发,官网默认配置为100。由于是大数据分析数据库主要适用于olap场景,对并发支持略差多个大数据查询可能会直接将cpu等资源占满,故并发实际达不到100。</p><h2>Clickhouse在好未来的实践</h2><p><img src="/img/bVcO1QL" alt="image" title="image"><br>图4-1 clickhouse线上架构图</p><h3>1、业务场景</h3><p>目前在好未来除了我们部门,在其他部门也已经有了多个业务方。</p><p><strong>1) 本部门</strong></p><p>使用平台:日志中心,猫头鹰,土拨鼠,grafana等。</p><p>使用方式:我们将需要的数据通过flink,spark,gohangout等工具消费kakfa,写入clickhouse,然后通过clickhouse做聚合查询,将数据展示出来。</p><p>比如,土拨鼠主要是通过网关数据做的聚合,可以看到各个域名,url或者服务器的调用次数,请求耗时等情况。再比如直播数据则是消费直播上报日志,用grafana将数据展示出来。</p><p><strong>2) 其他部门</strong></p><p>除了在本部门,还有其他业务方,数据研发部,数据中台等也都在使用,数据研发部主要会将hive中的热点数据通过spark/dataX同步至Clickhouse。然后将数据通过天枢天璇等平台展示给分析师使用,提高了查询速度。</p><p><img src="/img/bVcO1QN" alt="image" title="image"><br>图4-2:clickhouse使用图</p><h3>2、存储现状</h3><p><img src="/img/bVcO1QP" alt="image" title="image"><br>图4-3 单节点数据存储情况</p><p>以上数据第一列为库名,第二列为行数,第三列为压缩前大小,第四列为压缩后大小。</p><p>可以看到单个节点已有多个达到TB级别和百亿级别行数的数据库。目前数据节点是6个,也就是说在集群中,数据量需要再乘以6,代表有个别库库行数已达到千亿行,容量达到百T级别。</p><h2>建设与规划</h2><h3>1、监控</h3><p>Clickhouse官方目前没有提供直接的监控界面,但是所需要的监控数据在system库中都会记录下来,网上已有人通过grafana展示出来,目前的监控图如图5-1所示。<br><img src="/img/bVcO1Q9" alt="image" title="image"><br>图5-1 clickhouse监控图</p><p>除此之外,还写了脚本,定期的对每个节点的探活及故障重启。同时也使用神树平台查看各个节点的硬件信息及告警。</p><h3>2、遇到的问题</h3><p>Clickhouse作为olap数据库,在使用过程中或多或少会出现一些问题,例如版本bug,使用不规范,混部出现的问题等,现在主要介绍几个需要持续优化或者业务配合的问题。其他遇到的一些问题也会在wiki上持续更新:</p><p><strong>1) 大量查询导致服务器负载高情况,影响业务查询</strong></p><p><strong>分析:</strong>多个复杂查询,并且命中数据量极大的情况下。每个查询都会占用大量的cpu和内存。会导致服务器负载被打满的情况。尤其是内存被打满,会造成节点挂掉的现象。<br><strong>解决:</strong></p><ul><li>用户限制,避免内存被打满,可以配置max_memory_us age_ for_all_queries,使其低于服务器实际内存。并且还可以限制用户的并发,每次查询的数据量等;</li><li>某些业务下无法限制查询数据量,可以添加缓存机制,避免大量大数据查询。</li></ul><p><strong>2) ddl语句卡死</strong></p><p><strong>分析:C</strong>lickhouse主要支持的是追加及查询,但是使用mergetr ee引擎的表,是可以做update和delete的。更新和删除操作是用alter table语句的,也就是说其实这是需要ddl的权限的。每一次这种操作,数据库都会发生加锁,更新表等操作,并且数据量大的情况下,做更新操作,整个数据都会根据索引情况重新排序,这是一个漫长的过程。Clickhouse的ddl语句底层应该是个队列,一个没执行完,也会导致其他ddl卡住。</p><p><strong>解决:</strong>尽量减少ddl语句的执行频率以及增加执行间隔,或者尽量不要执行。</p><p><strong>3) zookeeper失联</strong></p><p><strong>分析:</strong>Clickhouse集群方案很依赖zk,副本同步机制都是依赖zk的,导致每次插入数据都会和zk做交互,并且在zk中做写操作,插入并发高的情况下,可能会导致zk暂时失联。</p><p><strong>解决:</strong>之前zookeeper是和Clickhouse混部,后期做了拆分,情况有一部分好转。但是目前依然会偶尔出现问题,后续会继续对这块优化,zookeeper的服务器性能也会去尽量完善,申请高配服务器,提高zookeeper的性能,使得不会因为zookeeper性能而影响到Clickhouse。</p><h2><strong>3、未来规划</strong></h2><p>想要打造一个高性能高稳定的大数据场景数据库,需要的是持续不断地学习以及和业务方的配合。</p><ul><li>深入了解业务,根据业务场景建表;</li><li>持续学习clickhouse,根据clickhouse特性优化sql;</li><li>同时也需要业务方配合,如果大查询频率较高,可以考虑使用缓存等机制,或者特定场景可以使用近似计算。同时特殊场景特殊对待,实现合适的sql;</li><li>数据持续增长,查询压力也是越来越大,进行集群之间的隔离,使其重要业务之间不会互相影响;</li><li>持续完善监控和告警机制;</li><li>clickhouse还有很多强大的功能,未来也会去尝试使用。</li></ul><h2>参考文献</h2><p>[1] <a href="https://link.segmentfault.com/?enc=vRzomdN48uKg7tZROFkYsQ%3D%3D.aL%2FwKSu1lwcXKcJOXkQ%2F%2B8yn6%2F7EBu%2FssHk%2FSDuKOzc%3D" rel="nofollow">https://clickhouse.tech/docs</a><br>[2] <a href="https://link.segmentfault.com/?enc=PPn7knRPqyA2QcyNkfMTdQ%3D%3D.fJOOfnyYmTs%2BQl5Y7DkyxWh946kHT86K5lotlstW6nH1la6TJE%2BQdt0k7iuj%2F63wSEbDmbEUOCHSPr4qlZnA%2FQ%3D%3D" rel="nofollow">https://blog.csdn.net/tzs_104...</a><br>[3] <a href="https://link.segmentfault.com/?enc=JUF9hpmzn7mRgaLc%2ByfOGA%3D%3D.InYXG1XSYhCYsNsdlJJ9pCrkPfDZYQvNlEjjwMk%2B5BMBbLzmJDcMalyPfEmrfIhI" rel="nofollow">https://www.jianshu.com/p/ab8...</a></p>
好未来数据中台实时数据平台演进
https://segmentfault.com/a/1190000039137345
2021-01-31T20:55:16+08:00
2021-01-31T20:55:16+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<blockquote><p>摘要:本文由好未来资深数据平台工程师毛祥溢分享,主要介绍批流融合在教育行业的实践。内容包括两部分,第一部分是好未来在做实时平台中的几点思考,第二部分主要分享教育行业中特有数据分析场景。大纲如下:</p><ol><li>背景介绍</li><li>好未来 T-Streaming 实时平台</li><li>K12 教育典型分析场景</li><li>展望与规划</li></ol><p><strong>Tips:</strong>点击文末【链接】即可下载作者分享 PPT 并回顾原版分享视频~</p></blockquote><p><strong>1.背景介绍</strong></p><p><strong>好未来介绍</strong></p><p><strong><img src="/img/remote/1460000039137354" alt="" title=""></strong></p><p>好未来是一家 2003 年成立教育科技公司,旗下有品牌学而思,现在大家听说的学而思培优、学而思网校都是该品牌的衍生,2010 年公司在美国纳斯达克上市,2013 年更名为好未来。2016 年,公司的业务范围已经覆盖负一岁到 24 岁的用户。目前公司主营业务单元有智慧教育、教育领域的开放平台、K12 教育以及海外留学等业务。</p><p><strong>好未来数据中台全景图</strong></p><p><img src="/img/remote/1460000039137353" alt="" title=""></p><p>上图为好未来数据中台的全景图,主要分为三层:</p><ul><li>第一层是数据赋能层</li><li>第二层是全域数据层</li><li>第三层是数据开发层</li></ul><p>首先,数据赋能层。主要是商业智能、智慧决策的应用,包括一些数据工具、数据能力以及专题分析体系,数据工具主要包括埋点数据分析工具、AB 测试工具、大屏工具;数据能力分析主要包括未来画像服务、未来增长服务、未来用户服务以及新校区的选址服务;专题分析体系主要包企业经营类专题分析等等。</p><p>其次,数据全域层。我们期望将全集团所有的事业部的数据进行深入的拉通和融合,打通不同业务线、产品线的用户池,从而盘活全集团的数据。具体的手段是 IDMapping,将设备 id、自然人、家庭三个层级的 id 映射关系挖掘出来,将不同产品上的用户数据关联起来。这样就能够形成一个大的用户池,方便我们更好的赋能用户。</p><p>最后,数据开发层。数据开发通过一些列的平台承载了全集团所有的数据开发工程,主要包括数据集成、数据开发、数据质量、数据服务、数据治理等服务。我们今天要分享的实时平台就是在数据开发中。</p><p><strong>2.好未来 T-Streaming 实时平台</strong></p><p><strong>实时平台构建前的诉求</strong></p><p><img src="/img/remote/1460000039137349" alt="" title=""></p><p>实时平台在构建之初,我们梳理了四个重要的诉求。</p><ul><li>第一个诉求是期望有一套<strong>统一的集群</strong>,通过提供多租户,资源隔离的方式提高资源利用率,解决多个事业部多套集群的问题。</li><li>第二个诉求是期望通过平台的方式<strong>降低实时数据开发的门槛</strong>,从而能够覆盖更多的开发者。</li><li>第三个诉求是期望能够提供<strong>通用场景的解决解方案</strong>,提高项目的复用性,避免每个事业部都开发相同场景的分析工具。</li><li>第四个诉求是对作业进行<strong>全方位的生命周期管理</strong>,包括元数据和血缘,一旦有一个作业出现异常,我们可以快速分析和定位影响范围。</li></ul><p><strong>实时平台功能概述</strong></p><p><img src="/img/remote/1460000039137348" alt="" title=""></p><p>现在我们平台已经是一个一站式的实时数据分析平台,包括了数据集成、数据开发、作业保障、资源管理、数据安全等功能。</p><ul><li>在<strong>数据集成</strong>方面,我们支持数据库、埋点数据、服务端日志数据的集成,为了能够提高数据集成的效率,我们提供了很多的通用模板作业,用户只需要配置即可快速实现数据的集成。</li><li>在<strong>数据开发</strong>方面,我们支持两种方式的作业开发,一种是 Flink SQL 作业开发、一种是 Flink Jar 包托管,在 Flink SQL 开发上我们内置了很多 UDF 函数,比如可以通过 UDF 函数实现维表 join,也支持用户自定义 UDF,并且实现了 UDF 的热加载。除此之外,我们也会记录用户在作业开发过程中的元数据信息,方便血缘系统的建设。</li><li>在<strong>作业保障</strong>方面,我们支持作业状态监控、异常告警、作业失败之后的自动拉起,作业自动拉起我们会自动选择可用的 checkpoint 版本进行拉起,同时也支持作业在多集群之间的切换。</li><li>在<strong>资源管理</strong>方面,我们支持平台多租户,每个租户使用 namespace 进行隔离、实现了不同事业部、不同用户、不同版本的 Flink 客户端隔离、实现了计算资源的隔离。</li><li>在<strong>数据安全</strong>方面,我们支持角色权限管理、表级别权限管理、操作审计日志查询等功能。</li></ul><p>以上就是我们平台的功能,在赋能业务的同时,我们也还在快速迭代中,期望平台简单好用,稳定可信赖。</p><p><strong>实时平台的批流融合</strong></p><p><img src="/img/remote/1460000039137350" alt="" title=""></p><p>接下来说一下平台建设中的一些实践,第一个是批流融合。</p><p>我们先理清楚批流融合是什么?</p><p>批流融合可以分为两个概念,一个是 Flink 提出的批流融合,具体的理解就是一个 Flink SQL 既可以作用于流数据、也可以作用于批数据,通过保证计算引擎一致从而减少结果数据的差异,这是一个技术层面上的批流融合。另个一概念是我们内部提出来的,那就是架构层面的批流融合。具体的操作手法就是通过 Flink 作业保证数据仓库 ODS 层的实时化,然后提供小时级别、分钟级别的调度,从而提高数据分析的实时化。</p><p>为什么我们会提出架构上的批流融合,主要我们看到行业发展的两个趋势。</p><ul><li>第一个趋势是数据集成的实时化和组件化,比如 Flink 集成 Hive、Flink CDC 的持续完善和增强,这样我们做数据集成的时候就会变得非常简单。</li><li>第二个趋势是实时 OLAP 引擎越来越成熟,比如 Kudu+impala、阿里云的 Hologres、湖仓一体的方案。</li></ul><p>这两个趋势让用户开发实时数据会变得越来越简单,用户只需要关注 SQL 本身就可以。</p><p>如上图所示,我们有三个类型的实时数仓,一个是基于 Hive 的、一个是基于实时 OLAP 引擎的、一个是基于 Kafka 的。其中,蓝色线条就是我们 ODS 层实时化的具体实现。我们提供了一个统一的工具,可以将实时的将数据写入到 Hive、实时 OLAP 引擎、当然还有 Kafka。这个工具使用起来比较简单,如果是 MySQL 数据的同步,用户只需要输入数据库名称和表名就可以了。</p><p>通过 ODS 层实时化的工具,我们就可以在 Hive、实时 OLAP 引擎、Kafka 中构建实时数仓。</p><ul><li>如果是 <strong>Hive 实时数仓</strong>,我们会使用 Flink 将实时的增量数据写入到 ODS 层,然后提供一个定时 merge 的脚本,用来 merge 增量数据和历史数据,从而保证 ODS 层的数据是最新最全的。配合 airflow 小时级别的调度能力,用户就可以得到一个小时级别的数仓了。</li><li>如果是类似于 <strong>Kudu / Hologres 这样的实时 OLAP 引擎</strong>,我们会先把离线数据从 Hive 中导入到实时 OLAP 引擎中,然后使用 Flink 将实时的增量数据写入到 ODS 层,写入的方式推荐使用 upsert 这样的特性,这样用户就能够得到一个纯实时的数仓了。配合 airflow 分钟级别的调度能力,用户就可以得到一个分钟级别的数仓了。</li><li>基于 <strong>Kafka 构建实时数仓</strong>,就是非常经典的架构了,开发成本也比较高一些,除了必须要秒级更新的分析场景,我们不太建议用户使用。当然在 2021 年的时候,我们也会去做 Flink 批流一体解决方案,让用户有更多选择方式的同时,让整个实时数仓变得更加简单。</li></ul><p>以上就是我们对批流融合的思考和实践,通过这种架构层面的批流融合,<strong>原来需要开发一个月的实时需求,现在 2 天就差不多能完成</strong>。大大降低了开发实时数据的门槛,提高了数据分析的效率。</p><p><strong>实时平台 ODS 层实时化</strong></p><p><img src="/img/remote/1460000039137356" alt="" title=""></p><p>说一下 ODS 层实时化我们具体是怎么做的。</p><p>要想把 ODS 层数据实时化,我们需要解决两个问题,第一个是离线数据的初始化问题,第二个是增量数据如何写入的问题。离线数据导入比较好做,如果数据源是 MySQL,我们可以使用 DataX 或者 Spark 作业的方式将 MySQL 的全量数据导入到 Hive 中,而实时增量数据的写入我们需要有两个步骤,第一个步骤是将 MySQL 的 binlog 采集到 Kafka,第二个步骤是将 Kafka 的数据使用Flink作业导入到 Hive。这样算下来,要解决 ODS 层实时化的问题,我们就需要一个离线初始化的作业,一个增量数据采集的作业,一个增量数据写入的作业,也就是需要 3 个作业。</p><p>在我们的平台上,我们对 ODS 层的 3 个作业进行了封装和统一调度,用户只需要输入一个数据库名称和表的名称就能完成 ODS 层实时化的工作。</p><p>以上就是我们批流融合中 ODS 层实时化的实现过程。</p><p><strong>实时平台 Flink SQL 开发流程</strong></p><p><strong><img src="/img/remote/1460000039137352" alt="" title=""></strong></p><p>我们另外一个实践,就是对 Flink SQL 的作业封装。先看一下,在我们平台上进行 Flink SQL 开发的整体流程。</p><p>从左往右看,数据源中的数据会通过 Maxwell、canal 这样的工具采集到 Kafka,采集到 Kafka 的原始数据格式并不是统一的,所以我们需要将 Kafka 中的数据进行统一格式化处理,我们默认支持埋点数据格式、canal 数据格式、maxwell 数据的解析,也支持用户自己上传 Jar 包进行数据解析,解析得到的标准化数据就会再次发送到 Kafka。</p><p>然后我们会使用 Flink SQL 作业来消费 Kafka 的数据,进行 SQL 脚本的开发。这里的 SQL 脚本开发和原生的 Flink SQL 的脚本开发有一点不一样,原生的 SQL 脚本开发用户需要编写 Source 信息、Sink 信息,在我们平台上用户只需要写具体的 SQL 逻辑就可以了。</p><p>那用户写完 SQL 之后,会将 SQL 作业信息提交到我们封装好的 Flink SQL 执行作业上,最后通过我们封装的 SQL 引擎将作业提交的 Flink 集群上去运行。后面将介绍我们是怎么封装的。</p><p>以上就是在我们平台上进行 Flink SQL 开发的流程,除了 Flink 作业本身的开发和提交,平台也会保留与作业有关的各种输入、输出的 schema 信息。比如业务数据库表的 schema 信息,经过同意加工之后的 schema 信息,数据输出的表的 schema 信息,通过这些记录,后期我们排查问题的时候就能够快速梳理出作业的来龙去脉和影响范围。</p><p><strong>实时平台 Flink SQL 开发过程</strong></p><p><img src="/img/remote/1460000039137357" alt="" title=""></p><p>在我们平台上开发 Flink SQL 作业,只需要三个步骤:</p><ul><li>第一个步骤确认 Kafka 的 Topic 是否已经注册过了,如果没有注册就需要用户手动注册下,完成注册后,我们会把 Topic 的数据解析出来,将字段信息保存起来。</li><li>第二步使用户编写 SQL,刚才说过,用户只需要写具体的 SQL 逻辑,不需要写 Source 和 Sink 信息。</li><li>第三步是用户指定将数据输出到哪里,现在平台可以支持同时指定多个 Sink 存储设备,比如将计算好的数据同时写入到 Hive、Holo 等存储。</li></ul><p>通过以上三个步骤的配置,用户就可以提交作业了。</p><p>接下来说一下,我们是怎么做的,我把整个执行过程分为 2 个阶段 10 个步骤。</p><p>第一个阶段就是作业准备阶段,第二个阶段就是 SQL 执行阶段。</p><p><strong>■ 作业准备阶段</strong></p><ul><li>第一步,用户在页面数据 SQL 和指定 Sink 信息。</li><li>第二步,SQL 解析及校验过程,当用户提交 SQL 时,我们会对 SQL 进行解析,看看 SQL 中用到的 Source 表和 UDF 是否在平台中注册过。</li><li>第三步,推测建表,我们会先运用下用户的 SQL,然后得到 SQL 的返回结果,根据结果数据生成一些建表语句,最后通过程序自动到目标 Sink 存储上去建表。</li><li>第四步,拼装 Flink SQL 的脚本文件,得到一个有 Source、SQL、Sink 三要素的脚本文件。</li><li>第五步,作业提交,这里会把 Flink SQL 文件提交到我们自己执行引擎中。</li></ul><p><strong>■ SQL 执行阶段</strong></p><ul><li>第一步是会初始化 StreamTableAPI,然后使用 connect 方法注册 Kafka Source,Kafka 的 Source 信息需要指定数据解析的规则和字段的 schema 信息,我们会根据元数据自动生成。</li><li>第二步是使用 StreamTableAPI 注册 SQL 中使用到的维表和 UDF 函数,UDF 函数包括用户自己上传的 UDF 函数。</li><li>第三步是使用 StreamTable API 执行 SQL 语句,如果有视图也可以执行视图。</li><li>第四步是一个比较关键的步骤,我们会把 StreamTabAPI 转成 DataStream API。</li><li>第五步就是在 DataStream 的基础上 addSink 信息了。</li></ul><p>以上是两个阶段的执行过程,通过第二个阶段,用户的 SQL 作业就会真正的运行起来。</p><p><strong>实时平台原生作业与模板任务</strong></p><p><img src="/img/remote/1460000039137359" alt="" title=""></p><p>上面分享了我们的 Flink SQL 作业如何开发和运行,接下来说一下我们平台对 JAR 包类型作业的支持。</p><p>在我们平台上,我们支持用户自己上传 JAR 包作业,然后在我们平台上进行管理。与此同时,为了提高代码通常场景的复用性,我们开发了很多模板作业,比如支持 Maxwell 采集的 binlog 直接写入到 Hive、Kudu、Holo 等存储设备,支持阿里云 SLS 日志写入到各种 OLAP 引擎。</p><p><strong>实时平台混合云部署方案</strong></p><p><img src="/img/remote/1460000039137347" alt="" title=""></p><p>讲一下混合云部署方案和平台技术架构。</p><p>我们平台现在支持将作业提交到阿里云机房、自建机房中,并且作业可以在两个机房中来回切换。为了要有这个功能呢?</p><p>今年年初,随着疫情的爆发,互联网在线教育涌入了大量的流量,为了应对暴增的流量,春节期间我们采购了上千台机器进行紧急的部署和上线,后来疫情稳定住了之后,这些机器的利用率就比较低了,为了解决这个问题,我们平台就支持了混合云部署方案,高峰期的时候作业可以迁移到阿里云上运行,平常就在自己的集群上运行,既节约了资源又保证了弹性扩容。</p><p><strong>实时平台技术架构</strong></p><p><img src="/img/remote/1460000039137351" alt="" title=""></p><p>接下来说一下平台的技术架构。</p><p>我们是一个前后端分离的项目,前端使用 vue+elmentui、服务端使用 springboot,不同的机房里面我们会部署一个后端服务的实例。任务提交到不同的机房主要通过转发层的 nginx+lua 来实现的。平台上任务的提交、暂停、下线操作,都是通过驱动层来完成的,驱动层主要是一些 shell 脚本。最后就是客户端了,在客户端上我们做了 Namespace/用户/Flink 版本的隔离。</p><p><strong>3.K12 教育典型分析场景</strong></p><p><strong>续报业务介绍</strong></p><p><strong><img src="/img/remote/1460000039137355" alt="" title=""></strong></p><p>我们聊一个具体的案例,案例是 K12 教育行业中典型的分析场景,用户续报业务。</p><p>先说下什么是续报,续报就是重复购买,用户购买了一年的课程,我们期望用户购买二年的课程。为了用户购买课程,我们会有一个集中的时间段用来做续报,每次持续一周左右,一年四次。</p><p>因为续报周期比较集中,时间比较短暂,每次做续报业务老师对实时续报数据的需求就特别迫切。</p><p>为此我们做了一个通用的续报解决方案,来支持各事业部的续报动作。要做实时续报,有几个挑战。</p><ul><li>第一个挑战是计算一个用户的订单是否是续报,需要依赖这个用户历史上所有的订单,也就是需要历史数据参与计算。</li><li>第二个挑战就是一个订单的变化会影响其它订单的变化,是一个连锁效应。比如用户有 5 个订单,编号为 345 的订单都是续报状态,如果用户取消了编号为 3 的订单,订单 4 和订单5的续报状态就需要重新计算。</li><li>第三个挑战是维度变化很频繁,比如用户上午的分校状态是北京,下午的分校状态可能就是上海,上午的辅导老师是张三,下午的辅导老师就是李四,频繁变化的维度给实时汇总数据带来了挑战。</li></ul><p>依赖历史数据、订单改变的连锁效应、频繁变化的维度,这些挑战如果单个看都不算什么,如果放在一起就会变得比较有意思了。</p><p><strong>实时续报解决方案</strong></p><p><img src="/img/remote/1460000039137358" alt="" title=""></p><p>先说下整体架构,我们采用的批流融合方式来做的,分成两条线,一条线是分钟级实时续报数据计算,一条是秒级实时续报数据计算。计算好的数据放在 MYSQL 中,用来做大屏和 BI 看板。</p><p>先看下蓝色的这条线,我们会把 Hive 中的离线数据导入到 Kudu 中,离线数据都是计算好的订单宽表。然后会使用 Flink 作业把新增的订单做成宽表写入到 Kudu 中,这样 Kudu 里面就会有最新最全的数据。配合 4 分钟的调度,我们就提供了分钟级的实时续报数据。</p><p>在看第一条橙色的线条,这条线上有两个 Flink 作业,一个是 ETL Job,一个是 Update Job。</p><p>ETL job 会负责静态维度的拼接与续报状态的计算,静态维度拼接我们是直接访问 MySQL,然后缓存在 JVM 中。续报状态的计算需要依赖历史数据,ETL Job 会将所有的订单数据加载到 JVM 中,具体的实现方法是我们自定义了一个 partitioncustom 方法,对所有的历史数据进行了分片,下游的每个 Task 缓存一个分片的数据。通过将数据加载到内存中,我们大大的加快了 Flink 实时计算的速度。</p><p>ETL Job 的计算的数据,会有两个输出,一个是输出到 Kudu,用来保证 Kudu 中的数据最新最全,两个一个数据是 Kafka,Kafka 中有一个 Topic 记录的是是当前订单的变化导致了哪些订单或者维度变化的信息。</p><p>接在 Kafka 后面的程序就是 Update Job,专门用来处理受影响的订单或者维度,直接去修改 MySQL 中相关的统计数据。</p><p>这样我们就通过 2 个 Flink 作业实现的实时续报的计算。</p><p>最下面的一条线是实时维度的数据变更的处理,维度变更的数据会发送到 Kafka中,然后使用 Flink 进行处理,看看维度的变化影响了哪些数据的统计,最后将受影响的订单发送到受影响的 Topic 中,由 Update Job 来重新计算。</p><p>以上就是我们实时续报的整体解决方案,如果有教育行业的朋友听到这个分享,或许可以参考下。</p><p><strong>实时续报稳定性保障</strong></p><p><img src="/img/remote/1460000039137361" alt="" title=""></p><p>我们看看这个通用的解决方案上线之后有哪些保障。</p><ul><li>第一个保障是<strong>异地双活</strong>,我们在阿里云和自建机房都部署了一套续报程序,如果其中一套有异常,我们切换前端接口就可以了。如果两个机房的程序都挂了,我们从零开始启动程序,也只需要 10 分钟。</li><li>第二个保障是<strong>作业容错</strong>,我们有两个 Flink 作业,这两个作业随停随启,不影响数据的准确性。另外一点就是我们缓存了所有订单数据在 JVM 中,如果数据量暴涨,我们只需要改变 ETL 程序的并行度就可以,不用担心 JVM 内存溢出。</li><li>第三个保障是<strong>作业监控</strong>,我们支持作业的异常告警和失败后的自动拉起,也支持消费数据延迟告警。</li></ul><p>通过以上保障措施,实时续报程序经过了几次续报周期,都比较平稳,让人很省心。</p><p><strong>4.展望与规划</strong></p><p>上述内容详细介绍了好未来当前业务及技术方案,总结而言我们通过多租户实现各事业部资源隔离、通过批流融合的架构方案解决分析实时化、通过 ODS 层实时化解决数据源到 OLAP 的数据集成问题、通过 Flink SQL 封装降低实时数据开发门槛、通过模板任务提供通用场景解决方案、通过混合云部署方案解决资源的弹性扩容、通过实时续报解决方案覆盖相同场景的数据分析。</p><p><img src="/img/remote/1460000039137360" alt="" title=""></p><p>最后,来看一下我们展望和规划。接下来我们要继续深化批流融合,强化混合云部署,提高数据分析的时效性和稳定性。支持算法平台的实时化,数据应用的实时化,提高数据决策的时效性。</p>
【年度硬核干货】 好未来2020年技术合辑新鲜出炉!
https://segmentfault.com/a/1190000039064670
2021-01-22T19:12:46+08:00
2021-01-22T19:12:46+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p>时光荏苒,2020年倏然而逝。</p><p>这一年注定是极不寻常的一年。全球新冠肺炎疫情的暴发,无论是给个人、企业还是给整个国家乃至世界,都带来了复杂而深刻的变化。</p><p>疫情加速了在线教育的发展,但也给在线教育行业带来了挑战。作为科技教育企业代表,好未来集团积极发挥在线教育的优势,推出全国在线免费公益课。面对挑战,好未来技术人更是不断自我更新,通过一次次技术升级,逼近“好”的极限。</p><p>这一年,好未来技术人第一次冲在了业务的最前线。他们用代码帮助校长和主讲老师扫清了线上教育的技术难题,向千万因疫情居家学习的孩子们传递知识。他们与业务伙伴共同打造了“停课不停学”的“避风港”,诠释了自己对教育的坚守与热爱。</p><p>吸引人们注意力的在线教育、AI人工智能以及双师课堂,都离不开技术人的默默耕耘与付出。他们用一个个通宵、一行行代码,让更多人看到<strong><em><em>“教育+科技”的无限可能。</em></em></strong></p><p>我们希望好未来技术人的这些在教育一线历经淬炼、在各教育场景中反复得到验证的技术,能给热爱科技和教育的你一点启迪。于是,我们编纂了这本《2020年度好未来技术合辑》。</p><p>本书共530多页,近20万字,汇集了好未来2020年一整年的技术精华,涵盖了好未来业务实践中的优秀案例、教育行业最全的中台组件技术知识以及通用基础技术能力方向的沉淀内容。<br><img src="https://image-static.segmentfault.com/289/545/2895453785-600ab27db860c" alt="image" title="image"></p><p>本书分为三个篇章,分别为:</p><p><strong>业务实践篇</strong></p><p>业务实践篇共精选了6篇业务实战记录的技术沉淀文章,内容不仅涵盖好未来旗下学而思网校在提升用户体验方面的技术实践及数据指标体系的建设实践,也有关于具体教育业务场景下的问题攻坚和思考。<br><img src="https://image-static.segmentfault.com/311/377/3113775460-600ab29216a6e" alt="image" title="image"></p><p>我们期望读者通过这几个具体的业务实践案例,对好未来技术在业务应用方面有一些了解。</p><p><strong>中台服务篇</strong></p><p>疫情将科技与教育的融合创新迅速推向了一个新阶段。好未来的技术中台化进程在2020年走上了快车道,集全集团“技术火力”,推动对技术门槛较高、需要规模效应、对用户体验有较大影响、有长期复利的技术攻坚。</p><p>这部分精选了好未来技术中台优秀的技术组件案例,如强大的题库系统“内容云”、各个业务线使用的消息推送系统、前端各种好用的高效率框架,还有在大数据、算法、测试等方面的支持。<br><img src="https://image-static.segmentfault.com/400/244/4002447339-600e3f725d56a" alt="image.png" title="image.png"></p><p><strong>通用技术能力篇</strong></p><p>除了业务实践以及中台建设,好未来在基础技术方面也有很多实践。</p><p>例如,在后台服务端、用户展现前端方面,好未来都进行了针对教育场景的通用技术建设;同样我们在技术支撑方面,如测试技术、运维技术方面也有一些实践积累;核心技术大数据算法方面,好未来对一些教育场景有探索落地;还有在在线教育必不可少的直播方面,我们也有精细地钻研。</p><p>除此之外,好未来还坚持在教育场景方面的“脑认知”高精尖项目上持续探索学习,致力于探究如何开发人类神奇大脑,让人更好地学习。</p><p>以上部分在本篇中主要以技术栈的维度分别介绍,具体涉及服务端、大前端、算法、脑认知、直播、测试以及运维,共七章。<br><img src="https://image-static.segmentfault.com/284/091/2840911317-600e3f62141cc" alt="image.png" title="image.png"></p><p><strong>最后</strong></p><p>本书内容非常丰富。我们抱着开放的交流心态,输出好未来工程师对技术和教育行业的思考和理解。我们真诚期望在更丰富的场景中与大家交流,探讨未来教育行业的科技生态。</p><p>我们也希望本合辑对你有所帮助,欢迎小伙伴与身边的同事、朋友分享,让我们一起切磋,共同成长。</p><p>合辑下载方式:关注“好未来技术”微信公号,后台私信“好未来技术年刊”字样即可获得下载链接。</p><p><img src="https://image-static.segmentfault.com/346/593/3465935721-600ab3030f84c" alt="image.png" title="image.png"></p>
辣眼睛,前端已经这么逆天啦?web前端智能化在线推理的应用场景和实现原理
https://segmentfault.com/a/1190000038516500
2020-12-17T18:54:00+08:00
2020-12-17T18:54:00+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<p>作者:王群 赵辉 刘东东</p><blockquote>随着AI能力从实验室逐渐走向市场,需要在特定应用场景下将神经网络模型执行预测到相应的结果。针对不同业务背景,云端智能化方案和用户端智能化法案也都处于快速发展的阶段。想要在网页上实现智能化的能力,达到特别效果,你会发现浏览器等载体下实现本地智能化实时预测是非常必要的。很多业界一线公司也都在布局web智能化,比如Google的TensorFlow.js,百度的paddle.js以及阿里淘系技术团队正在酝酿MNN.js,W3C也开始联合各大公司深入讨论WebNN规范的设计,未来AI能力将会渗透在更多的应用领域。作为国内第一个开源的web前端智能化开源方案paddle.js的发起人和设计者,王老师非常荣幸与大家一起分享web前端机器学习引擎的应用场景和实现原理。</blockquote><p>对于实现人工智能有很多方式实现,如机器学习就是一种特别接近于人的认知方式,就像田老师所描述的,“神经网络Neural Network,是最接近于人脑工作机制的算法,我们的人脑里有上百亿个神经元,单个神经元的表达能力很有限,但大量的神经元组合到一起,就产生了丰富的表达和和推理能力。我们看到什么,听到什么,都是输入,然后经由这么多神经元层层运算,输出我们的决策结果。神经网络,其实就是在模拟人脑的这套机制,从输入到最终的输出,中间还有很多层隐层,每层都有多个计算单元(神经元),这些计算单元层层紧密连接在一起就构成了网络。你可以认为一个计算单元就是一个简单的小函数,那这么多计算单元组合到一起之后,就形成了一个复杂的不可解释的函数。所谓深度神经网络Deep Neural Network,就是有很多层隐层的神经网络,隐层越多,就能表达越复杂的函数,也就越逼近于人脑。”</p><p><img src="/img/remote/1460000038516515" alt="'图:CNN神经网络'" title="'图:CNN神经网络'"><br>图:CNN神经网络</p><p>从人脑的识别过程,我们很容易发现三个重要的环节,即输入,输出,映射过程。抽象来看,整个过程就是给一个输入X,经过映射过程F,输出Y。</p><p><img src="/img/remote/1460000038516519" alt="图:抽象表示过程" title="图:抽象表示过程"><br>图:抽象表示过程</p><p>知道其中两个变量从理论上是可以推导出来另外一个。即给出X和Y,推出F。可以重复很多样本进行计算,得到F’(n)。标注的越准确、样本越多,机器大脑F’(n)就越接近人脑F,我们把这个寻找F‘的过程神经网络训练。</p><p><img src="/img/remote/1460000038516505" alt="图:神经网络训练和预测推理" title="图:神经网络训练和预测推理"><br>图:神经网络训练和预测推理</p><p>那么,形成神经网络的过程就是训练,使用神经网络的过程就是预测推理。</p><h2>Web端智能是什么?</h2><p>通过了解了机器学习的基础知识,端智能也就不再难以理解了。如果在云端进行训练和推理将结果呈现出来的就是云智能或者服务端智能,在客户端(Native / web)进行训练或者在线推理预测将结果呈现出来的过程就是端智能。</p><p><img src="/img/remote/1460000038516506" alt="图:智能化的服务端实现与客户端实现" title="图:智能化的服务端实现与客户端实现"><br>图:智能化的服务端实现与客户端实现</p><p>这个时候大家可能觉得在前端使用神经网络进行计算需要占用大量的计算资源,前端是否能够运行起来如此的庞然大物。其实,随着PC和移动设备上算力的稳步增强、算法逐步成熟以及各种创新需求的不断催生,在浏览器中实现端智能已经具备了良好的基础条件。</p><p><img src="/img/remote/1460000038516504" alt="AI4QQ202012151427242x.png" title="AI4QQ202012151427242x.png"></p><h2>Web端智能可以做什么?</h2><p>为什么会出现端智能,以及他底层演化逻辑是什么?这两年有一个明显趋势就是机器学习从实验室往产业落地方向演进,海量终端设备成为落地最佳载体。对于web前端使用智能化的场景一般具有实时性强、数据隐私性、创新交互和有效降低server压力等特点,比如人脸实时美颜、目标实时跟随与识别、AR、数据实时检测、media pipe、实时人像分割等场景都有着大量的应用。web方案能够面向全网用户使用,浏览器内实现是不错的一种方案,保证分享出去后其他用户也能够无差别的直接使用,不需要安装指定的应用APP,而且实现热更新,更新迭代不需要发版。</p><p><img src="/img/remote/1460000038516512" alt="人脸关键点" title="人脸关键点"><br>人脸关键点</p><p><img src="/img/remote/1460000038516511" alt="手势识别" title="手势识别"><br>手势识别</p><p><img src="/img/remote/1460000038516503" alt="实时识别" title="实时识别"><br>实时识别</p><p><img src="/img/remote/1460000038516516" alt="实时Hair Segmentation" title="实时Hair Segmentation"><br>实时Hair Segmentation</p><p><img src="/img/remote/1460000038516517" alt="肢体识别" title="肢体识别"><br>肢体识别</p><p><img src="/img/remote/1460000038516508" alt="物体实时跟随识别场景图片" title="物体实时跟随识别场景图片"><br>物体实时跟随识别场景</p><p><img src="/img/remote/1460000038516507" alt="AR场景图片" title="AR场景图片"><br>AR场景</p><p>很多业界一线公司也都在布局web智能化,比如Google的TensorFlow.js,百度的paddle.js以及阿里淘系技术团队正在酝酿的MNN.js,W3C也开始联合各大公司深入讨论WebNN规范的设计,Intel在API规范化上也做出了大量贡献。</p><h2>实现web端智能落地需要做什么?</h2><p>一般情况下,从实验室模型训练到实际应用落地有非常悠长的链路要完成,应用智能全链路核心流程包括数据采集、算法设计、模型训练、模型优化量化、模型部署、输入层处理、推理预测执行、输出层结果处理以及业务化等环节。无论是云端进行还是客户端(Native / web)端进行业务落地,这些环节都是不能缺少的。</p><p><img src="/img/remote/1460000038516510" alt="图:智能化全链路" title="图:智能化全链路"><br>图:智能化全链路</p><p>因为存在技术边界,算法工程师和工程研发工程师的协同就变得异常的复杂,所以出现了各种自动化的平台在淡化边界、将人工操作的距离缩短,减少全链路中的人为问题出现。例如,AI中台就建设了专业化的平台集合,例如专业化的标注平台vegas、计算资源调度与模型训练平台Axer、数据审核管理平台Guardian、AI能力接入平台Paas等。</p><p><img src="/img/remote/1460000038516509" alt="图:在线推理部分" title="图:在线推理部分"><br>图:在线推理部分</p><p>不同于服务端智能化应用,web端侧环境复杂性与用户设备息息相关,用户机器浏览器对于WebGL版本支持不同、操作系统的不同、webAssembly支持情况不同、webRTC支持情况不同、浏览器的不同都有可能造成web端智能化兼容实现的问题,而且工程师的技术能力也对业务落地效果有着一定的影响。因此,要实现web端侧在线推理不仅需要高性能、易用的端侧在线推理库,也需要有业务框架加持。比如对于实时视频流场景的实时在线推理预测就需要有相应的输入处理、分帧优化以、并行化计算和输出处理的通用业务封装,其实media pipe也就是针对这中场景下产生的web端智能业务框架。</p><h2>Web端智能的实现原理是什么?</h2><p>Web端侧智能化的实现方案主要包括离线工具部分、在线推理部分以及模型集。那么,为什么会有这些部分呢?我来一一跟大家分享一下这其中的“奥妙”。</p><p><img src="/img/remote/1460000038516514" alt="图:web智能化解决方案设计" title="图:web智能化解决方案设计"><br>图:web智能化解决方案设计</p><p><strong>离线部分</strong>主要工作就是神经网络模型的处理,这部分放在离线处理具有更多的优势,比如模型的优化、剪枝、精度量化以及模型格式的转换在离线时处理能够保证运行时不去做额外的资源开销。Web运行的主要载体是浏览器,在浏览器中能够解码的数据有限,所以需要把模型转换成web友好的格式是web端智能化的需要特意强调的,而且模型要尽可能的小,减少计算成本和网络加载负载成本。</p><p><strong>在线部分</strong>主要包括在线推理库和业务框架两大部分。在线推理库主要作用是进行神经网络模型加载(网络加载 / 本地加载)、神经网络在运行时重建以及神经网络计算等,通过将输入数据(图像、语音、本文等)进行输入层计算,再到神经网络各个隐藏层的计算,直到输出结果的输出层的计算最终给业务呈现效果。这其中每层都需要有相应的算子来完成相应的计算,以CNN卷积神经网络为例,需要卷积、池化、全连接以及激活函数等算子的实现,而这些算子的实现又要考虑性能的极致性。</p><p><img src="/img/remote/1460000038516518" alt="图:在线部分结构设计" title="图:在线部分结构设计"><br>图:在线部分结构设计</p><p>实现方式不同<strong>性能效果</strong>也有显著的差异,比如使用webGL利用GPU进行计算可以利用材质单一像素进行4通道的并行计算,这种方案速度能够满足视频实时场景需求;不同于webGL的方式,WASM是一种快速移植的方式,是能够将其他平台的代码编译到浏览器可用方案,但是计算性能相比webGL以Face Detector模型为例要有将近2倍左右的性能劣势,纯JavaScript利用CPU进行计算的性能就更加相差悬殊,所以在兼容性等基础上一般优选webGL方案。不过W3C也正在积极商讨webGPU的draft,更加快速的计算方案正在路上。</p><p><img src="/img/remote/1460000038516513" alt="图:backends性能对比情况" title="图:backends性能对比情况"><br>图:backends性能对比情况</p><p>在线部分的另一个重要部分就是业务框架,业务方往往更加专注于业务,很难有精力去处理性能问题和使用调试,对于视频流场景、语音场景、文本场景更加希望“拿来即用,用之有效”,所以对于相应的场景在业内目前较为优秀的方案就是封装成不同场景的业务抽象框架,让业务研发更加专注于业务实现。</p><h2>小结</h2><p>本期的Web前端智能化在线推理的应用场景和实现原理就先介绍到这里吧,也欢迎关注我的后续更新。帮助web智能化不断成长的,除了在性能上的不断压榨,还有众多场景的哺育。众多的人工智能应用必将在web端落地,通过人工智能来提升工作能效、优化用户体验,在你我不经意之间,改变了我们的生活。</p><p>如果你对AI及AI工程化有着浓厚的兴趣,想要和我们一起探索,欢迎加入AI中台-产研中心,让我们一起通过AI改变世界~!</p><p><em>【简历投递邮箱:jiangyajie@tal.com】</em></p>
好未来举办首届PHP开源技术大会
https://segmentfault.com/a/1190000038451427
2020-12-11T15:48:46+08:00
2020-12-11T15:48:46+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<p>12月5日,好未来第一届PHP开源技术大会在北京举行。大会以“开源·分享·共建”为主题,由智慧教育国家新一代人工智能开放创新平台和好未来联合主办,开源中国技术社区协办。</p><p>本次大会邀请了多位PHP领域的技术大咖,为千余名开发者和技术爱好者进行全天的前沿技术干货分享,详尽展示了当下炙手可热的PHP技术全貌和技术开源发展新态势,为推动科技教育长线发展注入新动能。 <img src="/img/remote/1460000038451431" alt="" title=""></p><p>(好未来第一届PHP开源技术大会在京举行)</p><p>作为智慧教育国家新一代人工智能开放创新平台建设单位,好未来倡导开源、分享、共建的理念,致力于用技术推动行业发展。从2020年8月,好未来正式上线首个开源项目并公布开源路线图开始,截至目前,已累计上线了30多个开源组件,在全球某主流开源社区获得的Star数量已超3000个,并面向行业开放了超过10项智慧教育解决方案,超过5类教学数据集。</p><p>作为大会主办方代表,好未来集团CTO田密表示,好未来重视技术的长期价值,希望通过开源放大技术的能量,实现人才聚合与协同创新,将经过教育行业复杂业务考验的技术和经验向开发者赋能,帮助更多技术人进入行业,收获成长,共同构建一个“共生”“互生”“创生”的智慧教育新生态,这也是好未来举办开源技术大会的初心。<img src="/img/remote/1460000038451433" alt="" title=""></p><p>(好未来集团CTO田密发表开场致辞)</p><p>会上,好未来旗下学而思网校首席架构师、Swoole开源项目创始人韩天峰对改变PHP未来的新技术做了展望,涉及PHP 8、JIT、Docker、Composer2、Swoole、Golang、K8s、Service Mesh、QUIC、云原生、物联网等12项技术。他表示,学而思网校也将第一时间应用这些新技术,实现响应性能、稳定性和处理效率的提升,为学生和老师带来更“丝滑流畅”的在线教育体验。</p><p>因为疫情无法来到现场的德国PHP内核核心开发者Nikita,借助视频分享了值得开发者重点关注的PHP 8——PHP全新一代大版本的新特性,包括JIT,注解、联合类型,以及更严格的错误处理和增强的类型安全。长期活跃于各大开源社区的PHP和Swoole内核开发者陈曹奇昊也从实操的角度“现场教学”,详细介绍了PHP 8与下一代协程技术的演进。KK集团技术负责人、Hyperf开源项目负责人黄朝晖分享了启动Hyperf项目的初心及核心逻辑,揭秘一个成功的开源项目是如何灵活满足不同生命周期的企业需求的实战经验。</p><p>开发者每天都在和数据库打交道,“玩转”数据库是技术人从入门到精通的必经之路。阿里云数据库产品事业部高级技术专家陈宗志详细介绍了PolarDB云原生数据库的核心技术和内核优化。知数堂联合创始人吴炳锡将15年MySQL数据库从业经验娓娓道来,用技术分析和业务实例阐释了如何利用MySQL 8.0帮业务增效的秘诀。PingCAP联合创始人兼CTO黄东旭则带开发者“遍历”当下主流分布式数据库的设计模式,并大开“脑洞”畅想了下一代数据库的新形态。</p><p>运维是产品能够稳定、安全、高效的基础保障,以教育行业为例,疫情期间面对在线教育流量的暴增,运维的技术响应能力是保障学而思网校面对百万并发、千万日均流量,实现“停课不停学”的关键因素之一。大会现场,云智慧技术副总裁高驰涛带领参会人员深入浅出地速览了AIOPS实现智能运维的关键技术、应用场景和未来展望。</p><p>PHP如何与流行的技术趋势相结合也是开发者关注的热门话题,其中“容器化”更是技术趋势中的“顶流”。好未来后端资深专家陈朝飞以学而思网校后端“网校云”的容器化落地实践为案例,分享如何从传统PHP应用向容器化迁移的实战经验,以及用软件工程的方法论解决生产系统稳定问题。</p><p>腾讯高级工程师,《PHP 7内核剖析》一书的作者秦朋以自身的成长为例,分享了如何从只会PHP的技术小白成长为合格工程师,从技术人才发展的角度帮助更多开发者。<img src="/img/remote/1460000038451430" alt="" title=""></p><p>(大会现场技术人热情高涨)</p><p>活动最后,好未来开源项目负责人谢华亮于大会闭幕发言中总结,以本次开源技术大会为起点,好未来希望为技术人搭建专业、纯粹、高水准的技术分享与成长平台,共同用技术推动教育进步。</p><p><strong>补充资料:</strong>关于PHP开源技术大会的全部PPT资料及直播回放视频已上传至“好未来技术”公众号。<br>各位有兴趣的老师可以扫描下方二维码关注“好未来技术”公众号,回复"PHP"获取大会第一手资料!<br> <img src="/img/remote/1460000038451432" alt="" title=""></p>
好未来斩获NeurIPS2020国际竞赛冠军
https://segmentfault.com/a/1190000038321149
2020-11-29T19:14:20+08:00
2020-11-29T19:14:20+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p>近日,国际人工智能顶级会议NeurIPS2020(2020 Conference on Neural Information Processing Systems,即神经信息处理系统大会,以下简称“NeurIPS 2020”)公布竞赛成绩。</p><p>好未来AI中台机器学习团队在NeurIPS 2020“教育挑战”竞赛四项单项任务中,荣获一个单项冠军、一个单项亚军及综合评比亚军,向世界展现了中国“AI+教育”的前沿探索。</p><p>作为全球人工智能和机器学习领域顶级会议之一,NeurIPS旨在促进神经信息处理系统在生物学、技术、数学和理论方面的研究交流,备受行业内外瞩目。本届NeurIPS首次举办与教育相关的挑战赛,吸引了来自北京大学、中国科技大学、宾夕法尼亚州立大学等80多支专业团队参加。<br><img src="/img/bVcKXfb" alt="image" title="image"></p><p>(NeurIPS 2020是国际人工智能顶级会议之一)</p><p>荣获国际顶级竞赛冠军,彰显好未来“AI+教育”实力</p><p>NeurIPS2020“教育挑战”竞赛紧密围绕教育中的“诊断性问题”,要求参赛队伍分析学生在过去两年中的近千道数学选择题作答记录,从而准确预测学生的答案以及哪些问题的作答质量较高,并为每个学生确定一个最能预测其答案的个性化问题序列。</p><p>针对赛题,好未来AI中台机器学习团队采用自然语言处理(Natural Language Processing, 以下简称“NLP”)中最前沿的预训练语言模型,将赛题转化成自适应学习问题进行预测,有效地利用了学生练习的序列信息,最终在“评价题目质量”任务、“通过极少量题目对新学生进行能力诊断”任务以及综合排名上,分别取得冠军、亚军、亚军的成绩。</p><p><img src="/img/bVcKXfc" alt="image" title="image"></p><p>(好未来AI中台机器学习团队获奖邮件) </p><p>随着数字技术在教育中的深层次应用,全球学生对个性化、高质量教育资源的需求日益迫切。作为国际人工智能顶级会议,NeurIPS 2020“教育挑战”竞赛在AI领域的成果研究,将有可能对全球数百万学生的个性化教育产生持续、真实的影响。好未来AI中台机器学习团队通过长期在NLP和数据挖掘领域的研究积累,能更准确地了解学生的学习状况,并给出个性化学习建议,为数据分析、内容识别、自动批改等技术在教育场景中的应用打下了良好基础。</p><p>加大科研及人才投入,推动“AI+教育”落地应用</p><p>凭借扎实的技术积累和对智慧教育的深入理解,2019年8月,好未来获科技部批准,承建智慧教育国家新一代人工智能开放创新平台,以智慧教育重大需求为牵引,促进行业开放共享,助力中小微企业成长。与之同时,好未来不断加强底层科研能力的构建,与清华大学、中科院计算所等6所高校院所建立联合实验室或开展联合科研项目,不断加大科技研发和科研学术合作的力度。</p><p>同时,好未来不断加大对教育人才的培养,汇聚了大批心怀教育理想、致力于追求爱和科技让教育更美好的高精尖科技人才。好未来AI团队参与的数十项学术成果入选NeurIPS、AAAI、WWW、EMNLP、AIED、NCME等国际顶级学术会议,并接连斩获世界计算机视觉领域顶级会议CVPR-EmotioNet竞赛冠军、世界人机交互与普适计算领域顶级会议UbiComp竞赛冠军、中国计算语言学大会CCL2020竞赛冠军等荣誉。在纽约国际人工智能顶级会议AAAI上,好未来AI团队成功组织了首届AI for Education学术研讨会,推动“AI+教育”领域的国际学术交流。</p><p>在加大科研和人才投入的同时,好未来也在不断将AI技术大量应用到真实的教育场景中。近年来,好未来不断取得前沿核心技术突破,并在语音技术、视觉理解、知识图谱等AI能力持续积累的基础上,实现多项技术的产品化应用,打造了包括AI课堂、教学过程评估、口语表达能力评测、作业拍搜批改等创新产品解决方案。目前,好未来已探索形成了覆盖“教、学、测、练、评”各教学环节的100余项AI能力、10余项教育场景应用AI解决方案。</p><p>例如,好未来自主研发的“中英文口语表达能力评测解决方案”,基于业内领先的幼儿语音识别技术和多维度口语表达评测算法,从流利度、情感、内容相关度、语意逻辑、语言运用等维度输出评测报告,不仅广泛应用于好未来旗下学而思培优、学而思网校等品牌,也开放给行业其他的教育从业者。好未来旗下拍照搜题软件题拍拍,以“题目搜不到,老师免费答”为核心功能,拥有强大的AI技术及海量优质题库,一键拍照即可快速获得解题思路和详细的步骤分析,并持续推出更智能的AI功能。</p><p>接连荣获国际权威竞赛大奖,标志着国际学术界对好未来科研实力的认可,也凸显出好未来AI技术在教育场景地中的实用价值。未来,好未来将继续推进教育科技领域的深入研究,并基于智慧教育国家新一代人工智能开放创新平台,为教育从业者提供全场景、全过程、全周期的技术及服务支持,为孩子们提供更加公平、更有质量的教育体验。</p>
十位业内顶尖大牛告诉你,PHP技术及未来
https://segmentfault.com/a/1190000038251325
2020-11-22T20:32:51+08:00
2020-11-22T20:32:51+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p><strong>一、为什么我们要办一个技术大会?</strong></p><p>每位程序员在职业规划的起点都曾问过自己一个问题:到底该专精于哪类计算机语言?是应用广泛的C语言,底层开发的C++,适于平台移植的Java还是效率出色的PHP? </p><p>而在走出新手村后,疲于应对新需求、忙忙碌碌改Bug的PHPer,又该如何摆脱“工具人”阶段、去哪儿看清技术最佳实践和发展动向呢?</p><p>“听君一席话,胜读十年书。”12月5日,智慧教育国家新一代人工智能开放创新平台联合教育科技公司好未来、开源中国技术社区,特邀PHP领域顶尖大咖,将为全国PHP开发者和技术爱好者打造一场开源届的饕餮盛宴。 </p><p>PHP的第一行代码在1994年由Rasmus Lerdorf敲下,并于1997年正式由开发团队推出。历经20多年的发展,如今已站在了8.0版本的前沿。在2001年,PHP的官方文档写下了“PHP是世界上最好的语言“,这句话也成为了响彻开发者群体的一个梗。</p><p>基于以上原因,我们想举办第一届PHP开源技术大会,让我们的PHPer从前台到后台整个技术栈未来哪些新的东西,值得我们去学习。</p><p><strong>二、这个技术大会你能够收获什么?</strong></p><p>这次大会邀请了很多PHP和数据库领域的重磅大咖。被无数爱好者开发出诸如Yahoo、Facebook、新浪微博、贴吧等应用于PC端和手机端的优秀产品和App的后端语言PHP,仍在不断地超越着自己。在12月5日的大会上,<strong>Swoole创始人韩天峰</strong>、<strong>阿里云MVP陈宗志</strong>,与来自<strong>德国的PHP内核核心开发者Nikita</strong>将详细讲解包括<strong>PHP8.0中有什么新特性</strong>、<strong>PolarDB云原生数据库内核揭秘、Swoole开源项目未来的迭代方向</strong>等行业重磅议题。</p><p>面对着群雄并起的其它语言,PHP同样也在提升着自己的竞争优势,获取面向未来的更多可能性。云智慧技术副总裁高驰涛老师将带来<strong>《智能运维AIOPS关键技术速览》</strong>,帮助程序员摆脱埋头写Bug的苦恼。</p><p>基于PHP的性能问题,<strong>KK集团技术负责人黄朝晖老师开发了Hyperf框架</strong>。基于此框架,对于职场程序员而言,不必多次切换语言,就能保证运行速度。</p><p>这场峰会还将汇聚来自<strong>教育、电商、视频</strong>等行业龙头大厂的PHP技术实战干货。例如,PHP容器化便是领域一大发展方向。好未来成功用容器化解决了网课的峰值数据差异问题。在观看人数进入黄金时段,由流量低谷向流量高峰的转化中,容器化可智能扩容,对保证网课的稳定性至为关键。<strong>好未来后端资深专家陈朝飞</strong>将带来<strong>《PHP项目容器化线上实践》</strong>的精彩讲解。</p><p>当然,这场大会介绍的不仅是技术的突破,也是个人路线的发展启示。<strong>腾讯高级工程师、《PHP7内核剖析》作者秦朋</strong>老师将带来<strong>《一个PHP工程师的进阶之路》</strong>,面向PHPer答疑解惑。</p><p>这是一场大咖云集、头脑风暴的大会,更多大咖的演讲内容详见海报;这也是一场从早到晚、持续输出技术干货的PHP峰会,你将与行业大咖面对面交流关于PHP的方方面面。总之,对于使用PHP语言的工程师而言万万不可错过。</p><p><strong>三、参加这个大会值不值?</strong></p><p>本次大会将以OMO的形式,在线上、线下同步进行,<strong>线下参会99元</strong>,<strong>线上直播9.9元</strong>。在行业内,这是<strong>“交个朋友”</strong>的超低良心票价,但你将收获技术峰会的全面体验。而对于主办方和分享嘉宾来说,则是以更好地推进PHP行业在国内的蓬勃发展为初衷。目前报名通道已正式开启,<strong>通过大会官方网站、开源中国等平台均可报名参会。</strong> </p><p>你不仅可以在活动现场与行业大咖面对面交流,开启更多合作机会。我们还将为每位参会观众准备午餐,更有精美大会主题伴手礼与现场福利抽奖等环节为活动增彩。</p><p>这是后疫情时代首个PHP技术人的线下聚会。欢迎各界技术伙伴踊跃参与,共绘技术开源开放生态图景。 </p><p><strong>四、大会具体安排</strong></p><p>会议时间:2020年12月5日(周六)</p><p>会议地点:北京市-黄河京都会议中心-3号楼</p><p>官方及报名入口:<a href="https://link.segmentfault.com/?enc=0R19S9f0adU2GEblhw6soQ%3D%3D.UAM25tOz2pntpJ%2Bq5KGJbAg16aQWXK4vgtydBDV6fs8%3D" rel="nofollow">https://oscon.edu.com</a></p><p><strong>大会嘉宾阵容</strong></p><p><img src="/img/remote/1460000038251328" alt="" title=""></p><p><img src="/img/remote/1460000038251329" alt="" title=""></p><p><img src="/img/remote/1460000038251331" alt="" title=""></p><p><img src="/img/remote/1460000038251330" alt="" title=""></p>
PHP大咖齐聚,首届好未来开源技术大会报名启动
https://segmentfault.com/a/1190000038157648
2020-11-13T11:41:13+08:00
2020-11-13T11:41:13+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p>在新一代信息技术快速发展和开源生态体系不断完善的当下,开源在技术创新、生态构建、数字变革等方面,都发挥着重要的引领作用。根据中国信息通信研究院发布的《开源生态白皮书(2020年)》显示,我国87.4%的企业正在使用开源技术。受益于开源技术的个人开发者更不计其数。</p><p>科技部于2019年发布的《国家新一代人工智能开放创新平台建设工作指引》中也强调,着力提升技术创新研发实力和基础软硬件开放共享服务能力,鼓励各类通用软件和技术的开源开放,支撑全社会创新创业人员、团队和中小微企业投身前沿技术研发。</p><p> 为更好的推动教育行业开源文化的形成,提升底层技术应用开源水准,智慧教育国家新一代人工智能开放创新平台、好未来教育集团、开源中国技术社区将于12月5日在北京联合举办首届PHP开源技术大会。本届技术大会以“开源·分享·共建”为主题,聚焦2020各领域最炙手可热的PHP技术方案,为全国PHP开发者和技术爱好者打造开源届的饕餮盛宴。</p><p><strong>预邀嘉宾</strong></p><p>大会遍邀PHP开源社区最具影响力的技术大咖与开源领域学术专家,带来一整天的前沿技术实战干货分享。来自教育、电商、出行、直播等各行业头部企业嘉宾同台论道,呈现各行业PHP技术热门实践。本次大会特邀远在德国的PHP内核贡献者, 目前就职于JetBrains的Nikita Popov,带来PHP8.0的重要变化及产生的影响;还邀请到PingCAP联合创始人兼CTO黄东旭,为大家分享系统化分析云带来的改变,以及云基础设施和软件的融合方向;学而思网校首席架构师、Swoole开源项目创始人韩天峰也将在会上为大家带来精彩分享,可谓PHP届半壁江山齐聚,亮点多多,诚意满满。</p><p><strong>参会报名</strong></p><p>本次大会将以OMO的形式,于线上、线下同步进行。目前报名通道已正式开启,在大会官方网站(好未来PHP开源技术大会)、开源中国等平台均可报名参会。虽然受疫情影响,大会组委会还是坚持在线下保留了一部分名额限量发售,力争给技术伙伴创造一个零距离热烈交流学习的技术氛围。无法线下赴会的技术伙伴也欢迎登记报名,通过线上直播参与到这场技术盛宴中来。</p><p><strong>参会指南</strong></p><p><strong>会议地点:北京·黄河京都会议中心</strong></p><p><strong>会议时间:2020年12月5日</strong></p><p><strong>报名入口:oscon.edu.com</strong></p><p><strong>交通方式:地铁五号线天通苑南站下车,步行1.2公里即可。</strong></p><p>作为大会主办方之一,科技教育公司好未来一直致力于用科技成就教育美好,依托智慧教育国家新一代人工智能开放创新平台,加速技术与产业的融合创新,助推行业高质量发展。从今年8月正式公布首个开源项目以来,好未来始终秉持着“开源·分享·共建”的开源理念,密集上线近二十个开源组件,积极拥抱开源社区。</p><p>好未来也期待以首届PHP开源技术大会为契机,加强业界交流,提升国内教育科技的整体技术实力。欢迎各界技术伙伴踊跃参与,共绘教育行业生机勃勃的技术开源开放生态图景。</p><p><img src="/img/remote/1460000038157652" alt="" title=""></p><p><img src="/img/remote/1460000038157651" alt="" title=""></p><p><img src="/img/remote/1460000038157653" alt="" title=""></p><p><img src="/img/remote/1460000038157654" alt="" title=""></p><p><img src="/img/remote/1460000038157656" alt="" title=""></p><p><img src="/img/remote/1460000038157655" alt="" title=""></p>
程序员都应该知道的URI,一文帮你全面了解
https://segmentfault.com/a/1190000037763452
2020-11-08T18:41:22+08:00
2020-11-08T18:41:22+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
20
<p>URI 是每个程序员都应该了解的概念,同时相关联的还有 URL, URN 等概念簇。了解这些概念,可以帮助我们更好地窥探万维网(WWW)的设计,同时也能帮我们在工作中有效解决跟 URI 相关概念的问题,更加理解 encode,decode 工作原理,更好地助力网络编程!</p><h3>1.URI</h3><p>URI(Uniform Resource Identifier) ,意为统一资源标识符,提供了一套简单可扩展的方式对资源进行标识。</p><h4>1.1 URI 的前世今生</h4><p>为什么会有 URI?<br>随着万维网的发展,需要有各种不同类型的资源被在网络上查找以及传输。因此,也就需要一种唯一的可在万维网上传播的标识,这样的统一资源标识就称为 URI。当然,资源在这里是一种笼统概念,或者抽象概念,可以泛指可以被标识的实体,就像一个网页,一本e-book, 一份 pdf 等等,只要有需要被呈现或者传输,都可以称为一种资源。</p><p>万维网奠基人Tim Berners-Lee关于超文本(hypertext)的提案中间接提出了用来标识超链接的想法–URL(Uniform Resource Locator)。因此,URL 也就最早被用来进行网络上可以提供访问的地址表示。随着HTTP, HTML 以及浏览器的逐步发展,越来越需要把标识资源可访问地址以及单出命名表示资源这两种方式分开,因此也就提出了 URN(Uniform Resource Name),并用来表示后者。<br><img src="/img/remote/1460000037763456" alt="1601481963133af1d4cf9300e41299cdd8e76d4ce49a3.png" title="1601481963133af1d4cf9300e41299cdd8e76d4ce49a3.png"></p><p>IETF(网络工程任务小组)主要负责 URI 相关标准制订。<br><img src="/img/remote/1460000037763455" alt="160151617244204c7ab6d7b584c66aaa5fb4ee94d6e20.png" title="160151617244204c7ab6d7b584c66aaa5fb4ee94d6e20.png"></p><ul><li>1994年发布RFC1630, 指出了 URL 和 URN 的存在,同时定义了 URI 的正式语法。</li><li>94年12月,RFC1738正式提出了 absolute 和relative URL, RFC2141则补充了URN 相关的文法和语法定义。</li><li>1999年的 RFC2732允许 URI 使用 IPv6地址</li><li>在2005年发布的RFC3986标准,解决了上述标准提出的一些短板,同时标志着URI 通用语法正式称为官方互联网协议</li><li>RFC3305标准指出,虽然 URL 名词被广泛使用,但是其本身可能被逐渐废弃,并且只用来做为一些 URI 作为间接提供该资源访问地址的提示。并且指出资源标识符不需要表示该资源通过网络的访问地址,或者根本不需要隐含该资源是基于网络提供的。(这里相当矛盾,其实 URL 已经作为民间事实标准并被广泛使用,也不是标准想推翻就能立刻推翻的 - -)</li></ul><h4>1.2 URI 和 URL,URN 比较</h4><p>了解到 URI 和 URL,URN 整体的历史,可以看出来最早 URI和 URL 其实是一脉相源的。后来为了兼容单纯通过命名或者名称来标识某个资源(并不是可被网络直接访达或者包含包含网络访问地址)的情况,提出了 URN标准。由此可见,这三个名称都可以表示对一项资源的定位标识。比较有意思的问题是,在平常的工作沟通中,如何区分,并且在什么样的场景下该使用哪个名称?<br><img src="/img/remote/1460000037763458" alt="160281482081581bfcde4ae77465ca7966aa28b6e005c.png" title="160281482081581bfcde4ae77465ca7966aa28b6e005c.png"></p><h5>1.2.1 基本概念</h5><p>先具体了解每个名称的基本概念:<br>1.URI<br>统一资源标识符。<br>用来表示某个特定资源。设计出来可以进行任何实体或者非实体的标识,但是目前被经常用于在网络上可传输内容的标识。URI 是由一串特定字符集的字符组成,并且由 IETF 制订的标准定义了一组语法规则,用来保证某个资源的统一和唯一标识。</p><p>2.URL<br>统一资源定位符。<br>也可以被称为网络地址。在万维网上,每个资源都有可以有唯一地址指向该资源,同时,通过该地址可以进行资源的读写,这样的地址标识就称为 URL。URL 包含了目前网络上常见的格式,包括 web 站点地址 http, 文件传输协议ftp, emal 地址协议 mailto以及数据库访问地址 JDBC 等。</p><p>3.URN<br>统一资源名称。<br>URN用来通过名称标识在特定命名空间的某个资源,同时希望为资源可以提供一种较持久的,与位置和存取方式无关的表示方式。URN 并不关注这个表示名称里是否隐含了该资源的位置,或者如何获取它,也不一定代表该资源一定可用。<br><strong>举个例子</strong>,在ISBN(Internal Standard Book Number)系统中,一个编号(类似9971-5-0210-0)代表了一个书本资源,该编号在 URN 中可以表示为 urn:isbn:9971-5-0210-0, 但是这个编号并没有给出在哪里或者如何找到这本书的信息,它只能唯一标识了这本书。</p><h5>1.2.1 三者之间的关系</h5><p>先上图来说明 URI,URL 和 URN 之间的关系。<br><img src="/img/remote/1460000037763457" alt="1601520706993142c193f0da14e8bb6acaf3b173cabf2.png" title="1601520706993142c193f0da14e8bb6acaf3b173cabf2.png"><br>URI 可以认为是一个抽象的概念,所有的 URL 以及 URN 都是 URI。RFC3986标准中有这样一段:</p><blockquote><strong>A URI can be further classified as a locator, a name, or both</strong>. 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”).<br>rfc 3986, section 1.1.3</blockquote><p>URI 可以被分类成 locator 或者对应的名称表示,也就是包含了 URL 和 URN 的概念。因此,平常我们在说 URL 的时候,它其实也可以被称为 URI。</p><p>同样,这里有个非常有意思的问题,URN 其实比较好区分开,在使用唯一标识资源名称时可以使用,但是 URI 和 URL 如何区分在哪个场景进行使用?<br>这个问题其实和 RFC3986标准定义的不够清楚有关,请再看下面这一段:</p><blockquote>The URI itself only provides identification; <strong>access to the resource is neither guaranteed nor implied by the presence of a URI</strong>.<p>rfc 3986, section 1.2.2</p></blockquote><p>URI 不保证提供该资源的访问方式,或者隐含保证该资源是否存在(其实语义就是该 URI 就是一个名称表示),但是在上一段中又声明了URI 会被分类成name 或者 locator,表示 URI 应该包含locator 这种访问方式。再看下面这一段:</p><blockquote><strong>Each URI begins with a scheme name</strong>, as defined in Section 3.1, that refers to a specification for assigning identifiers within that scheme.<p>rfc 3986, section 1.1.1</p></blockquote><p>每个 URI 都需要包含有起始 scheme 名称。比如:<a href="https://link.segmentfault.com/?enc=4QLzO%2F7oyuCFt81DYOXEBA%3D%3D.D0JC7jeg%2Fhg7dnBY4zzuoHILvtQ5sf5jOJpfcT9BG6Q%3D" rel="nofollow">https://www.example.com</a>,这样的一串字符串就可以称为 URI,但是明确标识了应该如何去访问这个资源,同时它也是 URL,因为 URL 是用来告知接收方获取该资源的方式。</p><p>IETF在RFC3986中也有一段关于 URI 和 URL 使用方式的说明:</p><blockquote>Future specifications and related documentation should use the general term “URI” rather than the more restrictive terms “URL” and “URN”<p>rfc 3986, section 1.1.3</p></blockquote><p>这样看来,好像IETF 更支持使用 URI 来代替 URL 这个称呼。但是考虑到 URL 目前已经成为用来描述网络上资源定位的事实名称,而且 RFC3986已经诞生超过15年了(有些条目确实跟不上时代发展速度),所以在针对互联网资源定位(即网络地址)的时候,URL 可以算是更贴切的名称。当然,如果对方跟你谈 URI等等,这也没问题,因为 URI 算是超类,并且也可以代表该资源。</p><p>下面是这个问题结论:</p><ul><li>URI 是一种标记符</li><li>URL 是可以告诉你如何去访问或者获取该资源的一种标记符</li><li>在描述网络资源地址的时候,用哪种都没问题,需要明确的原则就是最好和你的信息接收方用同样的称呼,方便理解</li><li>如果觉得不好拿捏属于 URL 或者 URN,那就可以直接使用 URI 描述</li></ul><h3>2.URI 字符集</h3><h4>2.1 URI的设计点</h4><p>URI 需要提供一种简单,可扩展的方式来唯一标识资源。同时,又需要考虑到在不同媒介上进行传播的表示形式。因此,URI 在设计时需要考虑到以下几点:</p><ul><li>URI 需要是可移植的。</li></ul><p>不同的系统,或者不同的接收方之间都可以使用 URI 协议来标识资源。URI 可以被表示成多种形式,比如说在纸上书写的字符串,或者屏幕上的像素,或者一系列通过编码的二进制流等。URI 的解析只跟这些呈现方式所关联的字符串有关,而跟具体表现方式,载体无关。<br>考虑到 URI 更多需要在网络场景传输,因此:</p><ul><li>URI 是由一串字符序列组成</li><li>URI 可能会从非网络环境中移植到网络环境下,但是网络环境的输入一般受制于键盘,鼠标等输入载体,因此最好由可以被这些物理载体方便输入的字符呈现</li><li>URI 一般需要被人们记住并使用,所以这些字符最好是人们经常使用并且熟悉的内容</li></ul><p>基于上述考虑,URI 为一串受限的字符所组成的字符串,并选择 US-ASCII 作为字符集。US-ASCII 字符集基本上被所有系统支持,而且兼容性良好,能够支持 URI 所需要的移植性。</p><ul><li>URI 需要将标识和动作分开</li></ul><p>这一层思想其实是需要将表示和表现分开。URI 只关注某个资源的标识,如果进行这个资源的存取或者访问不做任何方式的保证。同资源相关的动作,引用等,在设计时被交给具体实现 URI 下 scheme 的协议来制订,例如,http 协议会具体关心一个用’http’ scheme 表示的资源如何进行’get’, ‘update’,'delete’等一系列操作等。<br>这样可以保证 URI 协议的相对稳定,以及比较好的扩展性</p><ul><li>层级标识</li></ul><p>由于资源经常具有层级关系,比如在一个 example.com 站点下可能会挂有多个资源,或者下面会有一个目录’dir’, 该目录下会包含多个资源,这就意味着URI 需要有一种层级的组织方式。<br>在设计中也考虑到了这样类型的资源组织方式,允许 URI 按照层级组织,并且在字符串上按照从左到右的顺序拆分组件。<br>类似于常用操作系统的文件系统一样,URI 可以用来还原具有层级关系的资源系统的组织结构。</p><h4>2.2 URI 所选择的字符集</h4><p>如上所属,URI 选择 通过US-ASCII 字符集来进行表示,并限制使用从其中所挑选的一部分字符,数字以及符号。而且,由于需要支持层级结构,以及 URI 自身包含了不同的部分,因此也需要保留一些字符用来做这些有语义的部分的分隔。</p><p>Note: 由于需要对字符集或者语法进行描述,下文都是用 IETF使用的通用描述系统ABNF(Augmented Backus-Naur form), 即增强巴科斯范式。<br>增强巴科斯范式所定义的语法结构一般如下:</p><blockquote>rule = definition / definition; comment CR LF<br>rule = <a>*<b>element</b></a></blockquote><p>表示一组规则由一系列字符串组成的定义来描述,第一组 rule通过’/‘来表示定义中’或者’的关系。如果该条规则需要增加注释,那么需要通过’;'来标识注释的开始<br>第二组 rule 表示重复规则,其中 a标识最少重复次数,b 标识最多重复次数。例如,2*3element标识 element 最少出现两次,最多出现三次</p><p>关于增强巴科斯范式的具体内容请参照:<br><a href="https://link.segmentfault.com/?enc=IVxgGFgox7MBNDd%2Bl1SHmg%3D%3D.mYnip9%2BJonBegmTqzBvxKe5cmP56cqboKsF0M2aSbowjHLmho%2B2WfXff1GFLQXrsvCHbDQ%2Fe34M8hXyRw8RoCaqgddFEO1VB9QbCuYvck3c%3D" rel="nofollow">https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_form</a></p><h5>2.2.1 Percent-Encoding</h5><p>由于 URI 在协议中只挑选了部分ASCII 字符,数字以及符号,那么当需要表示不在这个范围之内的符号,字符,或者该字符在 URI 中被用来分隔符等特殊用途时,就需要对这个字符进行%编码。百分号编码也可以叫做URLEncode,其一般格式为:</p><pre><code>pct-encoded = "%" HEXDIG HEXDIG </code></pre><p>将不能直接使用的字符先转为字节流表示(一般为 utf-8编码,需要具体看上下文和 URI scheme 协议制订),然后每个字节转换为%加两个十六进制字符来表示。例如:<br>“00101011” 该字节需要编码为 “%2B” ,在 ASCII 码表中表示为 "+"号</p><p>Note: 百分号编码不关心大小写,但是为了统一和一致,最好应该使用大写字符</p><h5>2.2.2 Reserved Characters</h5><p>URI 保留字符集。<br>URI 自身定义时包含了 components以及 subcomponents,那么这些不同的 components 就需要通过分隔符来进行标识。这些被用来进行表示分隔的字符就成为保留字符集,这些字符集可能会被用作(或者将来会被用作)URI 不同部分的分隔符。<br>以下为 reserved character 所涉及的字符集表示:</p><pre><code>reserved = gen-delims / sub-delims
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "=" </code></pre><p>gen-delims 字符集用来表示 URI component 之间的分隔符,考虑到 component 内会由不同的 subcomponents 组成,因此需要 sub-delims 字符集来定义 subcomponents之间的分隔符。</p><p>Note:这些字符在 URI 中一般具有特殊语义,因此不能被编码。同时,如果在进行两个 URI 相等性比较时,如果其中一个对协议中component 部分不能编码的保留字符进行编码,即使解码后两个 URI 字符相同,也会被认为是两个不同的 URI</p><h5>2.2.3 Unreserved Characters</h5><p>允许出现在URI 中,并且不会被拿来用作保留字符集的字符集合成为 Unreserved Characters。所涉及到字符ABNF 表示为:</p><pre><code>unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
ALPHA = a-z / A-Z
DIGIT = 0-9 </code></pre><p>这些字符为非保留字符,在 URI 使用过程中是不需要进行编码的。</p><p>Note: 如果在 URI 比较中包含这些字符,那么该字符本身或者其编码格式都应该认为是相等的,即这些字符编码不编码不会影响相等性。另外,这些字符在使用时最好不要编码,即使已经被编码,那么在使用时也应该先对这些字符进行解码。</p><h5>2.2.4 总结</h5><p>一图来表示在 URI 中所涉及到的保留和非保留字符,需要注意的是保留字符在不做分隔符或者具有特殊含义的时候是需要编码的。<br><img src="/img/remote/1460000037763459" alt="alphadigit.png" title="alphadigit.png"></p><h3>3.URI Component</h3><p>URI 语法规则由一系列 component 组成,并且在设计时需要考虑到扩展性以及对各个资源定位类型的兼容,因此在其起始都会有一个 scheme 头来特定标识这个 URI 所定义的资源类型标识符。另外,URI 由于是所有资源类型的超集(会细分为 URL 和 URN),所以 URI 所涉及的定义都是需要被遵守的基本定义。<br>URI component 一般由以下 component 组成(使用 ABNF 描述):</p><pre><code>URI = scheme ":" [ //authority ] path [ "?" query ] [ "#" fragment ]
authority = [ userinfo@ ] host [ :port ] </code></pre><p>Note:</p><blockquote>schme 和 path 为 required</blockquote><p>有了上述语法规则的定义,举个例子来说明 URI 下两种不同的标识符所定义的各个 component 部分<br><img src="/img/remote/1460000037763460" alt="image.png" title="image.png"></p><p>下文将详细介绍各个组件部分,以及相应的语法规则。</p><h4>3.1 URI component</h4><h5>3.1.1 Scheme</h5><p>component</p><p>scheme</p><p>允许字符集</p><p>a-z A-Z 0-9 + . -</p><p>是否 case-sensitive</p><p>否</p><p>component 结束标识符</p><p>:</p><blockquote><p>Note:</p><ul><li>表中字符集为了呈现清晰,因此正则中通过非必要空格进行分隔,并且表或者关系</li><li>结束标识符表示语法解析时该 component 解析结束符</li></ul></blockquote><p>scheme用来标识URI 所对应的具体协议。每个 URI 都必须以 scheme 开头。URI 的语法规则如下(使用 ABNF 描述):</p><pre><code>scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) </code></pre><p>如上文所说,URI 定义通用的语法规则,scheme 所标识的具体协议会定义通用规则外的具体语法规则。例如,以 <em>geo</em> 为scheme 的协议 URI,表示特定地理位置标识,其语法规则如下:</p><pre><code>geo:<lat>,<lon>[<alt>][u=<uncertainty>] </code></pre><blockquote>参考自 RFC 5870</blockquote><p>URI scheme 的官方注册信息目前由 IANA(Internet Assigned Numbers Authority) 组织进行添加和维护,目前约包含了335种不同协议 scheme,具体可参考<a href="https://link.segmentfault.com/?enc=AOfey7Eb%2BjmX%2FT%2B72mBTAA%3D%3D.UcalfTrrIQpaLd2YmVQKtva637p5C7z8rtsvQuPdZ36SftzNdZOMdsKdSm16NghThFkHij3NyrKXnezltFZ%2FDg%3D%3D" rel="nofollow">https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml</a></p><h5>3.1.2 Authority</h5><p>component</p><p>Authority</p><p>component 开始标识符</p><p>//</p><p>component 结束标识符</p><p>/ ? #</p><p>authority component 设计的目的为设定一个命名空间,并且标识这个命名空间被哪个机构所管理,例如 baidu.com, google.com 等等。authority 一般由三部分组成,包含了可选的 userinfo, port 以及必选的 host 部分。<br>关于为什么 Authority 部分会选择 // 作为起始符号的原因,Tim Berners-Lee 曾回答过:</p><ol><li>需要选择一个命名系统来进行资源的层级化命名,/ 作为 unix 系统通用的分隔符可以在 URI 的设计中得到复用,因此使用 / 来作为 relative URI 的分隔符</li><li>需要有符号将 host 部分(类似 www.example.com)同URI 的其他部分进行区分,这部分设计参考了当时 Apollo domain system (其使用//computername/file/path进行命名)的设计方式</li><li>现在来看,他认为这个语法是比较冗余的,更喜欢直接通过:来进行域名分隔,例如 <a href="https://link.segmentfault.com/?enc=UbHU83SNykGr07W3%2FJ69XQ%3D%3D.0crq61jckgnSXCEFVdYRDyacYaWqZaQKReufhncRVUE%3D" rel="nofollow">http://www.example.com/foo/bar</a> 转写为 <a href="http:www.example.com/foo/bar">http:www.example.com/foo/bar</a>, 这样写同样可以识别到server 并且更为简化</li></ol><p>由此可见,标准的设计也是需要再不断地迭代和试验中前进 :)</p><h5>3.1.2.1 Userinfo</h5><p>component</p><p>Userinfo</p><p>允许字符集</p><p>pct-encode字符集 unreserved字符集 sub-delims字符集 :</p><p>是否 case-sensitive</p><p>是</p><p>component 结束标识符</p><p>@</p><p>userinfo 包含了用户相关信息(一般为名称,旧式格式 user:password 由于涉及安全风险已被弃用),同时需要通过@符合和 host 进行分隔。Userinfo 部分的语法规则如下(使用 ABNF 描述):</p><pre><code>userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) </code></pre><h5>3.1.2.2 Host</h5><p>component</p><p>Host</p><p>允许字符集</p><p>pct-encode字符集 unreserved字符集 sub-delims字符集</p><p>是否 case-sensitive</p><p>否</p><p>component 结束标识符</p><p>/ :</p><p>服务提供商通过 host来提供服务,同时基于 dns 域名解析, server 和 host 之间可以做到非一一对应。host 部分可以有三种表示方式,IPv6, IPv4或者 registered name。registered name host的语法规则如下(通过 ABNF 描述):</p><pre><code>host = IPv6address / IPv4address / reg-name
IPv6address = [ HEXDIG *( :: HEXDIG ) ]
IPv4address = DIGIT "." DIGIT "." DIGIT "." DIGIT
reg-name = *( unreserved / pct-encoded / sub-delims ) </code></pre><h5>3.1.2.2 Port</h5><p>component</p><p>Port</p><p>允许字符集</p><p>0-9</p><p>component 结束标识符</p><p>/</p><p>port 为可选项,同时通过十进制进行表示。在URI语法中,port 需要跟在 : 后。port 的语法规则如下(使用 ABNF 描述):</p><pre><code>port = *DIGIT </code></pre><p>每种 scheme 一般会定义一个默认端口。例如, http 定义80默认端口,https 定义443默认端口等。</p><h5>3.1.3 Path</h5><p>component</p><p>Path</p><p>允许字符集</p><p>pct-encode字符集 unreserved字符集 sub-delims字符集 @ :</p><p>component 结束标识符</p><p>? # EOF</p><p>path标识了 host 下特定的资源路径,包含了一系列通过 / 分隔的 segments。需要注意的是,如果URI已经包含了 authority 部分,那么 path部分或者为空,或者需要以 / 来开头。另外,URI还允许 relative-path 的使用方式,这样的方式第一段 path segment 不能包含 :(如果包含,会被 parser 认为是 authority 部分)。以下是简化的 path 语法规则(使用 ABNF 描述):</p><pre><code>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 / "@" </code></pre><h5>3.1.4 Query</h5><p>component</p><p>Query</p><p>允许字符集</p><p>pct-encode字符集 unreserved字符集 sub-delims字符集 @ :</p><p>component 开始标识符</p><p>?</p><p>component 结束标识符</p><h2> EOF</h2><p>query 部分提供了定位资源的辅助信息,query其内部语法并没有明确定义,但是一般由name-value 键值对组成的字符串组成,中间通过分隔符 & 进行分隔。例如:name1=value1&name2=value2。query 的语法规则如下(使用 ABNF 描述):</p><pre><code>query = *( pchar / "/" / "?" )
pchar = unreserved / pct-encoded / sub-delims / ":" / "@" </code></pre><h5>3.1.5 Fragment</h5><p>component</p><p>Query</p><p>允许字符集</p><p>pct-encode字符集 unreserved字符集 sub-delims字符集 @ : / ?</p><p>component 开始标识符</p><p>component 结束标识符</p><p>EOF</p><p>fragment 为段落标识符,一般用来标识一个 resource 的特定部分(一个资源子集或者一部分,或者通过这个资源来描述的一些其他资源)。 fragment 以 # 作为起始标识符,其语法规则如下(通过 ABNF 描述):</p><pre><code>fragment = *( pchar / "/" / "?" )
pchar = unreserved / pct-encoded / sub-delims / ":" / "@" </code></pre><h5>3.1.6 小结</h5><p>各个component 允许的字符集部分是我们需要特别关注的,需要注意在五个 component 之间允许使用 gen-delims 字符集,在每个 component 内(即小组件间)允许使用 sub-delims 字符集。</p><h4>3.2 解析 URI</h4><p>如何通过程序来解析 URI, 并得到 URI 各个 component?<br>如上一节 ABNF 语法规则描述,URI 满足上下文无关文法。因此,我们可以通过语法图来呈现整体 URI 的解析规则,如下:<br><img src="/img/remote/1460000037763461" alt="16025072234541dc24351cc2b4627a4d6fac6257dbdda.png" title="16025072234541dc24351cc2b4627a4d6fac6257dbdda.png"></p><p>有了上图,使用递归下降,解析的伪代码就非常好写了:</p><p>`/**</p><ul><li>读取下一个字符</li></ul><p>**/<br>function next() {<br> skip space;<br> read next char and return;<br>}</p><p>/**</p><ul><li>预扫描,查看对应的 input 字符串是否包含有 special_char,</li><li>以及其位置</li></ul><p>**/<br>function contains(input, special_char) {<br> start = input.start, end = input.end;<br> while (start < end) then</p><pre><code> if special_char equals start then return;</code></pre><p>end<br> return start<br>}</p><p>/**</p><ul><li>对 uri 的解析函数</li><li>具体的解析 component 方法为 parse_*, 需要匹配的字符集以及语法规则可参照上文中各个 ABNF</li></ul><p>**/<br>function parse(string uri) {<br> parse_scheme;<br> skip next ';' ;<br> if next() == "//" then</p><pre><code> if contains(substring_uri(// until path), '@') then
parse_userinfo;
end
parse_host;
if next() == ':' then
parse_port;
end</code></pre><p>end<br> parse_path;<br> if next() == '?' then</p><pre><code> parse_query;</code></pre><p>end<br> if next() == '#' then</p><pre><code> parse_fragment;</code></pre><p>end<br>}`</p><h3>5.再论Encode 和 Decode</h3><p>什么时候该 encode 或者 decode?<br>先说 URI 的设计目的,URI 被设计出并可在万维网上进行广泛传播,因此对各个子系统,浏览器等媒介的兼容性是最重要的,因此被设计使用被广泛使用的 ASCII 码进行承载。<br>因此,<strong>在生成 URI 过程中,应该先完成各个 componet 部分的编码,然后在联合 gen-delimiter 拼接成 URI</strong>。由于各个 scheme 的具体协议不同,因此只有在生成 URI 的过程中,才可以知道具体哪些 delimiter 会需要被编码,或者会被使用作为真正的 delimiter。一旦 URI 被生成,该 URI 在传播时就应该保持其 百分号 encode 的格式。<br>当百分号编码的 URI 在解码时,<strong>应该先通过 gen-delimiter 以及 sub-delimiter 将各个 component 进行分离,然后再对各个 component 进行分别解码</strong>。这样可以保证按照生成的 URI 被完整解码。<br>另外,需要注意的是,2.2.3中提到的 unreserved 字符集可以在任意时刻被编码和解码,但是推荐在生成 URI 时不对这些字符集进行编码,同时在解码时应该优先对这些字符集的百分号编码格式进行解码。</p><blockquote>Note: 不应该对同一个 URI 重复次编码或者解码,这样会导致 URI所代表的语义失效。例如,对已经进行百分号编码的 URI 再进行编码时,又会再次对其中的百分号进行二次编码,从而导致 URI 在进行解码时含义错误。</blockquote><h4>5.1 实现 encode 和 decode</h4><p>按照上文的说法,encode 需要先根据对应的 component 部分来组成不需要进行 escape(即不需要编码) 字符的规则,然后再进行逐一的判断和编码,之后再将编码过后的 component 拼接称为 URI(当然,如果所有的 delimiter 都不需要进行编码,那可以直接对整个 URI 进行编码,不需要 escape 的字符集直接包含这些 delimiter 字符)。 decode 则需要先将各个 component 按照 delimiter 进行拆分,然后分别对各个 component 在需要解码的字符规则下进行解码。</p><blockquote>Note: 在标识 ASCII 以外的字符集时,一般是用 Unicode 字符集,编码方式为 UTF-8。<br>因此,在编码和解码过程中,如果编程语言层面使用 UTF-16进行字符编码(类似于 Java 和 JavaScript),那么需要将其转为 UTF-8编码,同时需要针对 UTF-16带来的 surrogate pair 进行额外处理。<br>关于surrogate pair 描述,可以参考<br><a href="https://link.segmentfault.com/?enc=DwId4wbIt1OlpbAJpIkKqw%3D%3D.euTYaeWQJhL0sz6i6jwSqlz1cM5%2FXz9%2FzSfbTr5DcVfdXNSk3sdabl0CSfs4wIWb53KZtYiDKIbCTkq96b3uBVXJhUTEk5YMUdzgz%2FeNPG7Z1RUXM47wI0xcxHA%2BCkONEDJp6umw8lDsWUwCCT2ZMEocETCSrfsZUnWKvB5WDPY49n6318%2FoSZRDPMtT4nvFoHjjVe1MBAmQLIgJS6inbUBgaDZx4RUeMzufhNqF4%2BOCdlshhTw7%2BjL1cRkCVOOEc7mnHFeMoWY7vrkYVWU6TrlIG94fYdV8TKCKT5wIo2myA9Hs1Ws9TQST%2F%2B3PD14hX2BL839a42bbviO9ONYAqezPHlQ86zqLaEDWWG8XFyc%3D" rel="nofollow">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.</a></blockquote><h5>5.1.1 encode</h5><p>encode 的实现中需要注意的就是对需要编码的字节进行%编码,伪代码如下:</p><p>`/**</p><ul><li>对某一段 string s 进行 URI encode 编码</li><li>传入 s 以及不需要编码的字符集 dontNeedEncodingSet, 返回 URI encode后的string</li></ul><p>*</p><ul><li>dontNeedEncodingSet 字符集需要根据3.1中的 component描述来定,例如 Path 中的不需要编码字符集</li><li>一般为 unreserved字符集 sub-delims字符集 @ :(sub-delims 字符集以及@ : 如果其本身需要出现在</li><li>component 中而不是用来做分隔语义,那么同样需要进行 encode),另外不同的语言实现在不需要编码字符集</li><li>上可能会有不同的选择</li></ul><p>**/<br>function encode(s, dontNeedEncodingSet) {<br> // 声明 R 为结果字符串<br> def R, index = 0, strLen = s.length();<br> while index < strLen then</p><pre><code> 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;</code></pre><p>end<br> return R;<br>}`</p><h5>5.1.2 decode</h5><p>decode 的实现中需要注意在遇到%号时读取后续字符进行解码,同时如果语言实现使用 utf-16编码那么需要对 surrogate pair 进行还原(这部分语言本身一般都提供方法来对 utf-8进行转换),伪代码如下:</p><p>`/**</p><ul><li>对 s 进行解码,返回解码后的 string</li></ul><p>**/<br>function decode(s) {</p><pre><code>// 声明 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;</code></pre><p>}`</p><h5>5.1.3 小结</h5><p>相信各位已经对 URI 有了一个相对全面的了解,在实际工作的使用中,还需要根据语言所提供的对应 encode,decode 方法文档来进一步了解其编解码所定义的 component 部分特殊保留字符,这样会对所使用语言提供的 encode/decode 有更深入的了解 :)<br>**<br><strong>Enjoy your coding trip~</strong></p><p>作者:王阳(好未来Java开发专家)</p>
好未来拥抱技术开源 共建智慧教育开放生态
https://segmentfault.com/a/1190000037674616
2020-10-30T22:16:48+08:00
2020-10-30T22:16:48+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<p>2020年8月,好未来正式公布首个开源项目。随后,在短短的一个月内,好未来密集上线近二十个开源组件,积极拥抱开源技术社区。</p><p>作为教育行业代表性企业,好未来为何选择开源?开源之路将如何走?现阶段进展怎样?</p><p><strong>从内生到开源</strong></p><p>好未来的前身学而思成立于2003年,经过17年的高速发展,已成为国内领先的科技教育公司。好未来始终秉持「爱和科技让教育更美好」的使命,不断加大研发投入,设立了人工智能、大数据、脑认知等多个科技研发部门,并获国家人社部批准设立博士后科研工作站。</p><p>好未来先后与清华大学、中科院计算所等高校院所建立了联合实验室,与北京师范大学等签署了教育科学、脑与认知领域的战略合作协议,并与斯坦福大学、华东师范大学等开展联合科研项目。</p><p>好未来沉淀了大量优质教育资源和海量教学数据,还拥有丰富的教学场景和应用需求,并利用技术推动了多项智慧教育解决方案的落地。为实现技术资源共享与技术落地经验复用,好未来内部建立起跨业务的技术互通机制,积极推进技术中台建设,推动内部开源。而这也为好未来后续的对外开源打下了坚实的基础。</p><p>对此,好未来集团CTO田密表示,「代码开源和技术协同可以大幅减少企业和开发者的重复投入,提升研发效率,降低运营成本。好未来希望拥抱开源,赋能更多开发者协同创新,让更多技术人能够站在前人经验的基础上更进一步,帮助更多开发者进入行业生态,共创价值。这也是好未来对外开源的初衷。」</p><p><strong>当技术情怀遇到国家战略</strong></p><p>2019年8月,科技部发布《国家新一代人工智能开放创新平台建设工作指引》,着力提升技术创新研发实力和基础软硬件开放共享服务能力,鼓励各类通用软件和技术的开源开放,支撑全社会创新创业人员、团队和中小微企业投身前沿技术研发。在此背景下,好未来成为了智慧教育国家新一代人工智能开放创新平台承建单位。这也是好未来积极拥抱开源的重要原因。</p><p>田密解释,「开源将推动教育行业的技术落地和升级,为教育发展带来更多新动能。作为智慧教育国家新一代人工智能开放创新平台建设单位,好未来会将更多技术能力通过开源开放的形式与行业共享,加速技术与产业的融合创新,成就智慧教育的美好未来。」</p><p>此外,好未来也期待通过智慧教育国家开放创新平台的建设运行,协同各界伙伴,夯实教育领域的基础技术,全力为智慧教育关键技术创新提供高质量的服务和支撑,助力构建从开发者到业务场景的生态体系,加码行业创新的推动力。</p><p><strong>开源路线图全面公开</strong></p><p>好未来开源项目负责人谢华亮表示,「教育行业受众广泛、需求多样、场景复杂,相比互联网通用技术,教育科技有其自身的特点与要求。目前在各大技术领域都缺少适配教育行业的高水准开源项目与资源,好未来计划逐步开源内部积累的一系列实用技术组件、教育AI能力和教育场景数据集,填补这一行业空白。」</p><p>从AI能力开源的角度来看,好未来将推动在语音、图像等多个方向AI能力开源,为更多教育领域技术创新开发者提供数据基础。智慧教育国家开放创新平台也将提供数据标注、模型训练等AI能力的开源服务接口,并与国家开源平台联动,推动教育AI能力开源项目建设。</p><p>据了解,好未来积累的大量教育场景数据集,也会随着智慧教育国家开放创新平台的建设,逐步开源开放,并通过建立数据标准和数据服务机制,吸引更多的教育机构和科技企业,通过平台开放共享更多类型的教育数据。</p><p><img src="/img/remote/1460000037674620" alt="" title=""></p><p>(好未来AI能力开放架构图)</p><p>同时,好未来希望通过推动网络服务、数据存储、直播和框架服务等6大方向的组件开源,在教育各场景通用技术能力方面进行开源,促进平台生态的参与者共享平台成果。</p><p><img src="/img/remote/1460000037674619" alt="" title=""></p><p>(好未来通用技术能力未来开源架构图)</p><p><strong>用开源成就教育美好未来</strong></p><p>自启动首个开源项目以来,好未来已对外开源了接近二十个项目和技术组件,覆盖多个领域,包括通用编程语言框架、通用微服务治理、通用前端框架、教育行业通用技术组件等。</p><p>好未来积极拥抱开源,也赢得了开源社区的认可。Star是在开源社区上收藏开源项目的功能,Star数量的多少体现了项目的受欢迎程度。开源首月,好未来在全球某主流开源社区获得的Star数量已超两千多个。其中,关键开源组件获得的Star数量约为1500个。同时,专注服务开发者的好未来开源社群,在短短一个月内便吸引了近两千名开源技术爱好者的加入,且有着很高的活跃度和参与度。这也体现了好未来开源项目的优质性和高潜性。</p><p>可以说,作为教育行业代表性企业,好未来首次技术开源的阶段性进展和未来规划,是教育行业进一步信息化、数字化的标志。对于致力于投身教育行业的开发者而言,这也是一个新的契机。</p>
一文读懂特征工程
https://segmentfault.com/a/1190000024522693
2020-09-20T18:34:50+08:00
2020-09-20T18:34:50+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<p><strong>特征工程(feature engineering)</strong>:利用领域知识和现有数据,创造出新的特征,用于机器学习算法;可以手动(manual)或自动(automated)。神经网络的自动特征工程,常常不适用于现实中其他的复杂任务。因此,本文主要针对数据挖掘以及传统的机器学习,不会涉及图像识别、自然语言处理等深度学习领域。<br>俗话说:<strong>数据与特征工程决定了模型的上限,改进算法只不过是逼近这个上限而已</strong>。<br>在豆瓣图书频道,搜索‘特征工程’关键词,搜索结果是仅有两本评分在7分以上数据,分别是<a href="https://link.segmentfault.com/?enc=Y9SndMAezpPG%2FdnQ7FVLEQ%3D%3D.279Kf9qlu6wlPFrE3Ec9%2Fi18LKpYzldSQyfNj2pxjqXtzq7MzpvlnSAC8%2BGzOLty" rel="nofollow">精通特征工程</a>、<a href="https://link.segmentfault.com/?enc=OCZ3pl03Gl2s%2FERCysBILA%3D%3D.0K8K0SV70O5MdGY84U2WyMEXWCl4BiYNokUyLbhHjuRUr9N0UqU4m1QvSIO6Kiod" rel="nofollow">特征工程入门与实践</a><br><img src="https://pic4.zhimg.com/v2-ae77d6647d5905e7ea1c6e243a06b7d3_b.png" alt="img" title="img"><br>当数据维度有限,那‘特征工程’是非常重要的,但发表这方面文章较少,更没全局化、系统化讲解特征工程理论和实战等,所以我想结合工作经验、相关书籍、优秀文章等总结的一套通用的数据科学-特征工程方法论。<br>从这 <img src="https://www.zhihu.com/equation?tex=%5Cnewcommand%5Ccolorful%7B%5Ccolor%7Bred%7D%7D%20%5Ccolorful%7B%E7%89%B9%E5%BE%81%E7%90%86%E8%A7%A3%E3%80%81%E7%89%B9%E5%BE%81%E6%B8%85%E6%B4%97%E3%80%81%E7%89%B9%E5%BE%81%E6%9E%84%E9%80%A0%E3%80%81%E7%89%B9%E5%BE%81%E5%8F%98%E6%8D%A2%7D" alt="newcommandcolorful{color{red}} colorful{特征理解、特征清洗、特征构造、特征变换}" title="newcommandcolorful{color{red}} colorful{特征理解、特征清洗、特征构造、特征变换}">newcommandcolorful{color{red}} colorful{特征理解、特征清洗、特征构造、特征变换} 等维度展开,逐步讲解<strong>理论</strong>和<strong>代码实现</strong>等,针对代码实现部分 不能公开公司相关数据,所以选择用泰坦尼克号公开数据。</p><pre><code>import pandas as pd
import numpy as np
import seaborn as sns
df_titanic = sns.load_dataset('titanic')</code></pre><p>数据字段描述如下:<br><img src="https://pic3.zhimg.com/v2-0409d08923f6324aaa691fd81eaf1eca_b.png" alt="img" title="img"></p><h2>一、特征理解</h2><p>1.1 <strong>区分结构化数据与非结构化数据</strong><br>如一些以表格形式进行存储的数据,都是结构化数据;而非结构化数据就是一堆数据,类似于文本、报文、日志之类的。<br>1.2 <strong>区分定量和定性数据</strong></p><ul><li>定量数据:指的是一些数值,用于衡量某件东西的数量;</li><li>定性数据:指的是一些类别,用于描述某件东西的性质。</li></ul><p><img src="https://pic1.zhimg.com/v2-ab91b1bce987e33096fa52df1bab44c0_b.png" alt="img" title="img"></p><h2>二、特征清洗</h2><p>目标是提高数据质量,降低算法错误建模的风险。<br>现实的业务建模过程中,数据常常存在各种问题,数据存在不完全的、有噪声的、不一致的等各种情况。而这些带有错误信息的数据会对模型造成不利的影响。<br>数据清洗过程包括<strong>数据对齐、缺失值处理、异常值处理、数据转化等</strong>数据处理。<br><img src="https://pic1.zhimg.com/v2-bdd10e10aa307015f0cb7a0378753738_b.png" alt="img" title="img"><br><strong>2.1 数据对齐</strong><br>主要有时间、字段以及相关量纲的对齐。<br><strong>1) 时间:</strong></p><ul><li>日期格式不一致【’2019-07-20’、’20190720’、’2019/07/20’、’20/07/2019’】</li><li>时间戳单位不一致,有的用秒表示,有的用毫秒表示;</li><li>使用无效时间表示,时间戳使用 0 表示,结束时间戳使用 FFFF 表示。</li></ul><p><strong>2) 字段:</strong></p><ul><li>姓名写了性别,身份证号写了手机号等</li></ul><p><strong>3) 量纲:</strong></p><ul><li>数值类型统一 【如 1、2.0、3.21E3、四】</li><li>单位统一【如 180cm、1.80m】</li></ul><h2><strong>2.2 缺失处理</strong></h2><p>主要包括<strong>少量缺失</strong>的情况下,考虑<strong>不处理</strong>或<strong>删除缺失数据</strong>或者<strong>采用均值、中位数、众数、同类均值填充。</strong><br>当缺失值对模型影响比较大,存在比较多的不缺失数据的情况下,可以采用<strong>模型预测</strong>或者<strong>插值</strong>的方式。当缺失值过多时,可以对<strong>缺失值</strong>进行编码操作。<br> 对每个字段都计算其缺失值比例,然后按照缺失比例和字段重要性,分别制定策略,可用下图表示:<br><img src="https://pic3.zhimg.com/v2-44cfe5bed189f76aa8d6c4a45c03bd12_b.png" alt="img" title="img"><br><strong>空值汇总分布</strong></p><pre><code>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</code></pre><p><strong>1) 删除元组</strong><br>将存在遗漏信息属性值的对象(元组,记录)删除,从而得到一个完备的信息表。<br><strong>优点:</strong><br>简单易行,在对象有多个属性缺失值、被删除的含缺失值的对象与初始数据集的数据量相比非常小的情况下有效;<br><strong>不足:</strong><br>当缺失数据所占比例较大,特别当遗漏数据非随机分布时,这种方法可能导致数据发生偏离,从而引出错误的结论。<br><strong>代码实现</strong><br>embark_town 字段 有 2 个空值,可以考虑删除缺失处理下</p><pre><code>df_titanic[df_titanic["embark_town"].isnull()]
df_titanic.dropna(axis=0,how='any',subset=['embark_town'],inplace=True)</code></pre><p><img src="https://pic3.zhimg.com/v2-d636f2d727a438a001ec311d75af7f2a_b.png" alt="img" title="img"><br><strong>2)</strong> <strong>数据填充</strong><br>用一定的值去填充空值,从而使信息表完备化。通常基于统计学原理,根据初始数据集中其余对象取值的分布情况来对一个缺失值进行填充。<br><strong>(a) 人工填充(filling manually</strong>)<br>根据业务知识来进行人工填充。<br><strong>(b) 特殊值填充(Treating Missing Attribute values as Special values)</strong><br>将空值作为一种特殊的属性值来处理,它不同于其他的任何属性值。如所有的空值都用“unknown”填充。一般作为临时填充或中间过程。<br><strong>代码实现</strong></p><pre><code>df_titanic['embark_town'].fillna('unknown', inplace=True)</code></pre><p><strong>(c) 统计量填充</strong><br>若缺失率较低(小于 95%)且重要性较低,则根据数据分布的情况进行填充。<br><strong>常用填充统计量:</strong><br><strong>平均值</strong>:对于数据符合均匀分布,用该变量的均值填补缺失值。<br><strong>中位数</strong>:对于数据存在倾斜分布的情况,采用中位数填补缺失值。<br><strong>众数</strong>:离散特征可使用众数进行填充缺失值。</p><ul><li><strong>中位数填充</strong></li></ul><p><strong>fare:缺失值较多,使用中位数填充。</strong><br><img src="https://pic2.zhimg.com/v2-419cf448bff6e4767dfef0c0f0a11989_b.png" alt="img" title="img"></p><pre><code>df_titanic['fare'].fillna(df_titanic['fare'].median(), inplace=True) </code></pre><ul><li><strong>众数填充</strong></li></ul><p><strong>embarked:只有两个缺失值,使用众数填充</strong></p><pre><code>df_titanic['embarked'].isnull().sum()
执行结果:2
df_titanic['embarked'].fillna(df_titanic['embarked'].mode(), inplace=True)
df_titanic['embarked'].value_counts()
执行结果:
S 64</code></pre><ul><li><strong>用 imputer 填充缺失值</strong></li></ul><p>imputer 类提供了缺失数值处理的基本策略,比如使用缺失数值所在行或列的均值、中位数、众数来替代缺失值。该类也兼容不同的缺失值编码。<br>填补缺失值:<strong>sklearn.preprocessing.Imputer(missing_values=’NaN’, strategy=’mean’, axis=0, verbose=0, copy=True)</strong><br><strong>主要参数说明:</strong></p><blockquote>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 时,报没有该属性错误</blockquote><ul><li><strong>同类均值填充</strong></li></ul><p><strong>age:根据 sex、pclass 和 who 分组,如果落在相同的组别里,就用这个组别的均值或中位数填充。</strong></p><pre><code>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</code></pre><p><img src="https://pic3.zhimg.com/v2-61d0911fcd0855ab110bfdcba30015fa_b.png" alt="img" title="img"><br><strong>(d) 模型预测填充</strong><br>使用待填充字段作为 Label,没有缺失的数据作为训练数据,建立分类/回归模型,对待填充的缺失字段进行预测并进行填充。<br><strong>最近距离邻法(KNN)</strong><br>先根据欧式距离或相关分析来确定距离具有缺失数据样本最近的 K 个样本,将这 K 个值加权平均/投票来估计该样本的缺失数据。<br><strong>回归(Regression)</strong><br>基于完整的数据集,建立回归方程。对于包含空值的对象,将已知属性值代入方程来估计未知属性值,以此估计值来进行填充。当变量不是线性相关时会导致有偏差的估计,常用线性回归。 <br><strong>代码实现</strong><br><strong>age:缺失量较大,用 sex、pclass、who、fare、parch、sibsp 六个特征构建随机森林模型,填充年龄缺失值。</strong></p><pre><code>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)</code></pre><p><img src="https://pic2.zhimg.com/v2-d170a5b23651b27dc3f4eb18710de2a9_b.png" alt="img" title="img"><br><strong>(e) 插值法填充</strong><br>包括随机插值,多重插补法,热平台插补,拉格朗日插值,牛顿插值等。</p><ul><li><strong>线性插值法</strong></li></ul><p>使用插值法可以计算缺失值的估计值,所谓的插值法就是通过两点(x0,y0),(x1,y1)估计中间点的值,假设 y=f(x)是一条直线,通过已知的两点来计算函数 f(x),然后只要知道 x 就能求出 y,以此方法来估计缺失值。<br>.interpolate(method = 'linear', axis) 方法将通过 linear 插值使用沿着给定 axis 的值替换 NaN 值, 这个差值也就是前后或者上下的中间值</p><pre><code>df_titanic['fare'].interpolate(method = 'linear', axis = 0)</code></pre><p>同时,也可用行值插入</p><pre><code>df_titanic['fare'].interpolate(method = 'linear', axis = 1)</code></pre><p><strong>代码实现</strong></p><pre><code>df_titanic['fare'].interpolate()</code></pre><ul><li><strong>多重插补(Multiple Imputation)</strong></li></ul><p>多值插补的思想来源于贝叶斯估计,认为待插补的值是随机的,它的值来自于已观测到的值。具体实践上通常是估计出待插补的值,然后再加上不同的噪声,形成多组可选插补值。根据某种选择依据,选取最合适的插补值。<br>多重插补方法分为三个步骤:<br>Step1:为每个空值产生一套可能的插补值,这些值反映了无响应模型的不确定性;<br>每个值都可以被用来插补数据集中的缺失值,产生若干个完整数据集合;<br>Step2:每个插补数据集合都用针对完整数据集的统计方法进行统计分析;<br>Step3:对来自各个插补数据集的结果,根据评分函数进行选择,产生最终的插补值。<br><strong>(f) 哑变量填充</strong><br>若变量是离散型,且不同值较少,可转换成哑变量,例如性别 SEX 变量,存在 male,fameal,NA 三个不同的值,可将该列转换成 IS_SEX_MALE、IS_SEX_FEMALE、IS_SEX_NA。若某个变量存在十几个不同的值,可根据每个值的频数,将频数较小的值归为一类’other’,降低维度。此做法可最大化保留变量的信息。<br><strong>代码实现</strong></p><pre><code>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 </code></pre><p><strong>(g) 当特征值缺失超过 80%以上,建议删除【或变成是否变量】,容易影响模型效果</strong></p><pre><code>df_titanic.drop(["deck"],axis=1)</code></pre><p><strong>2.3 异常处理:</strong><br><strong>1) 异常值识别</strong></p><ul><li><strong>箱线法</strong></li></ul><pre><code>sns.catplot(y="fare",x="survived", kind="box", data=df_titanic,palette="Set2");</code></pre><p><img src="https://pic4.zhimg.com/v2-83d14255da5fa386986656eaf3e7335b_b.png" alt="img" title="img"></p><ul><li><strong>正态分布</strong></li></ul><pre><code>sns.distplot(df_titanic.age)</code></pre><p><img src="https://pic1.zhimg.com/v2-8b116825ce7e728301eea676fad0d4ec_b.png" alt="img" title="img"></p><ul><li><strong>异常值检测方法</strong></li></ul><p><strong>(a) 基于统计分析</strong><br>通常用户用某个统计分布对数据点进行建模,再以假定的模型,根据点的分布来确定是否异常。<br>如通过分析统计数据的散度情况,即数据变异指标,对数据的分布情况有所了解,进而通过数据变异指标来发现数据中的异常点数据。<br>常用的数据变异指标有极差、四分位数间距、均差、标准差、变异系数等等,如变异指标的值大表示变异大、散布广;值小表示离差小,较密集。<br>譬如最大最小值可以用来判断这个变量的取值是否超过了合理的范围,如客户的年龄为-20 岁或 200 岁,为异常值。<br><strong>(b) 3σ原则</strong><br>若数据存在正态分布,在 3σ原则下,异常值为一组测定值中与平均值的偏差超过3倍标准差的值。如果数据服从正态分布,距离平均值3σ之外的值出现的概率为P(|x - μ| > 3σ) <= 0.003,属于极个别的小概率事件。如果数据不服从正态分布,也可以用远离平均值的多少倍标准差来描述。<br><img src="https://pic3.zhimg.com/v2-9edfad30cda1eaac5898f7c8d304339e_b.png" alt="img" title="img"><br><strong>(c) 箱线图分析</strong><br>箱线图提供了识别异常值的一个标准:如果一个值小于 Q1-1.5IQR 或大于 Q3+1.5IQR 的值,则被称为异常值。<br>Q1 为下四分位数,表示全部观察值中有四分之一的数据取值比它小;<br>Q4 为上四分位数,表示全部观察值中有四分之一的数据取值比它大;<br>IQR 为四分位数间距,是上四分位数 Q1 与下四分位数 Q3 的差值,包含了全部观察值的一半。<br>箱型图判断异常值的方法以四分位数和四分位距为基础,四分位数具有鲁棒性:25%的数据可以变得任意远并且不会干扰四分位数,所以异常值不能对这个标准施加影响。因此箱型图识别异常值比较客观,在识别异常值时有一定的优越性。 <br><img src="https://pic1.zhimg.com/v2-a3e90da8d867e6d8ee2d8e7d681def8c_b.png" alt="img" title="img"><br><strong>(d) 基于模型检测</strong><br>首先建立一个数据模型,异常是那些同模型不能完美拟合的对象;如果模型是簇的集合,则异常是不显著属于任何簇的对象;在使用回归模型时,异常是相对远离预测值的对象。<br><strong>优点:</strong><br>有坚实的统计学理论基础,当存在充分的数据和所用的检验类型的知识时,这些检验可能非常有效。<br><strong>缺点:</strong><br>对于多元数据,可用的选择少一些,并且对于高维数据,这些检测可能性很差。<br><strong>(e) 基于距离</strong><br>基于距离的方法是基于下面这个假设:即若一个数据对象和大多数点距离都很远,那这个对象就是异常。通过定义对象之间的临近性度量,根据距离判断异常对象是否远离其他对象,主要使用的距离度量方法有绝对距离(曼哈顿距离)、欧氏距离和马氏距离等方法。<br><strong>优点</strong>:<br>基于距离的方法比基于统计类方法要简单得多;<br>因为为一个数据集合定义一个距离的度量要比确定数据集合的分布容易的多。<br><strong>缺点:</strong><br>基于邻近度的方法需要 O(m2)时间,大数据集不适用;<br>该方法对参数的选择也是敏感的;<br>不能处理具有不同密度区域的数据集,因为它使用全局阈值,不能考虑这种密度的变化。<br><strong>(f) 基于密度</strong><br>考察当前点周围密度,可以发现局部异常点,离群点的局部密度显著低于大部分近邻点,适用于非均匀的数据集。<br><strong>优点:</strong><br>给出了对象是离群点的定量度量,并且即使数据具有不同的区域也能够很好的处理。<br><strong>缺点:</strong><br>与基于距离的方法一样,这些方法必然具有 O(m2)的时间复杂度。<br>对于低维数据使用特定的数据结构可以达到 O(mlogm);<br>参数选择困难。<br>虽然算法通过观察不同的 k 值,取得最大离群点得分来处理该问题,但是,仍然需要选择这些值的上下界。<br><strong>(g) 基于聚类</strong><br>对象是否被认为是异常点可能依赖于簇的个数(如 k 很大时的噪声簇)。该问题也没有简单的答案。一种策略是对于不同的簇个数重复该分析。另一种方法是找出大量小簇,其想法是:<br>较小的簇倾向于更加凝聚;<br>如果存在大量小簇时一个对象是异常点,则它多半是一个真正的异常点。<br>不利的一面是一组异常点可能形成小簇而逃避检测。<br><strong>优点</strong>:<br>基于线性和接近线性复杂度(k 均值)的聚类技术来发现离群点可能是高度有效的;<br>簇的定义通常是离群点的补,因此可能同时发现簇和离群点。<br><strong>缺点:</strong><br>产生的离群点集和它们的得分可能非常依赖所用的簇的个数和数据中离群点的存在性;<br>聚类算法产生的簇的质量对该算法产生的离群点的质量影响非常大。<br><strong>(h) 基于邻近度的异常点检测</strong><br>一个对象是异常的,如果它远离大部分点。这种方法比统计学方法更一般、更容易使用,因为确定数据集的有意义的邻近性度量比确定它的统计分布更容易。一个对象的异常点得分由到它的 k-最近邻的距离给定。异常点得分对 k 的取值高度敏感。如果 k 太小(例如 1),则少量的邻近异常异常点可能导致较异常低的异常点得分;如果 K 太大,则点数少于 k 的簇中所有的对象可能都成了异常异常点。为了使该方案对于 k 的选取更具有鲁棒性,可以使用 k 个最近邻的平均距离。<br><strong>优点</strong>:<br>简单<br><strong>缺点</strong>:<br>基于邻近度的方法需要 O(m2)时间,大数据集不适用;<br>该方法对参数的选择也是敏感的;<br>不能处理具有不同密度区域的数据集,因为它使用全局阈值,不能考虑这种密度的变化。<br><strong>总结:</strong><br>在数据处理阶段将离群点作为影响数据质量的异常点考虑,而不是作为通常所说的异常检测目标点,一般采用较为简单直观的方法,结合箱线图和 MAD 的统计方法判断变量的离群点。</p><pre><code> sns.scatterplot(x="fare", y="age", hue="survived",data=df_titanic,palette="Set1")</code></pre><p><img src="https://pic4.zhimg.com/v2-9d07309e6658d480b91b79e07e4ccfcb_b.png" alt="img" title="img"><br><strong>2) 处理方法</strong><br>对异常值处理,需要具体情况具体分析,异常值处理的方法常用有四种:</p><ul><li>删除含有异常值的记录;</li><li>某些筛选出来的异常样本是否真的是不需要的异常特征样本,最好找懂业务的再确认一下,防止我们将正常的样本过滤掉了。</li><li>将异常值视为缺失值,交给缺失值处理方法来处理;</li><li>使用均值/中位数/众数来修正;</li><li>不处理。</li></ul><h2>三、特征构造</h2><h2><strong>3.1</strong> 特征构造</h2><p><strong>目标</strong>是增强数据表达,添加先验知识。<br>如果我们对变量进行处理之后,效果仍不是非常理想,就需要进行特征构建了,也就是衍生新变量。 </p><h2><strong>3.3.1</strong> 统计量构造:</h2><p>1) 基于业务规则、先验知识等构建新特征<br>2) 四分位数、中位数、平均值、标准差、偏差、偏度、偏锋、离散系统<br>3) 构造长、短期统计量(如 周、月)<br>4) 时间衰减(越靠近观测权重值高)</p><ul><li><strong>年龄分段</strong>:child、young、midlife、old</li></ul><pre><code>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)</code></pre><ul><li><strong>抽取 title 特征</strong></li></ul><pre><code>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</code></pre><ul><li><strong>抽取家庭规模</strong></li></ul><pre><code>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</code></pre><h2><strong>3.3.2 周期值</strong>:</h2><p>1) 前n个周期/天/月/年的周期值,如过去5天分位数、平均值等<br>2) 同比/环比 </p><h2><strong>3.3.3 数据分桶</strong>:</h2><h3><strong>1) 等频、等距分桶</strong></h3><p><strong>(a) 自定义分箱</strong><br>指根据业务经验或者常识等自行设定划分的区间,然后将原始数据归类到各个区间中。<br><strong>(b) 等距分箱</strong><br>按照相同宽度将数据分成几等份。<br>从最小值到最大值之间,均分为 N 等份, 这样, 如果 A,B 为最小最大值, 则每个区间的长度为 W=(B−A)/N , 则区间边界值为A+W,A+2W,….A+(N−1)W 。这里只考虑边界,每个等份里面的实例数量可能不等。<br>缺点是受到异常值的影响比较大<br><img src="https://pic1.zhimg.com/v2-d2bbfd9a9777dfb2dd5e0eecfb965558_b.png" alt="img" title="img"><br><strong>(c) 等频分箱</strong><br>将数据分成几等份,每等份数据里面的个数是一样的。<br>区间的边界值要经过选择,使得每个区间包含大致相等的实例数量。比如说 N=10 ,每个区间应该包含大约10%的实例。<br><img src="https://pic4.zhimg.com/v2-e6d7e328681d059aa533989b8482844f_b.png" alt="img" title="img"></p><ul><li><strong>数值变量分箱</strong></li></ul><pre><code># 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</code></pre><h3><strong>2) Best-KS分桶</strong></h3><p>1.将特征值值进行从小到大的排序。<br>2.计算出KS最大的那个值,即为切点,记为D。然后把数据切分成两部分。<br>3.重复步骤2,进行递归,D左右的数据进一步切割。直到KS的箱体数达到我们的预设阈值即可。<br>4.连续型变量:分箱后的KS值<=分箱前的KS值<br>5.分箱过程中,决定分箱后的KS值是某一个切点,而不是多个切点的共同作用。这个切点的位置是原始KS值最大的位置。<br> 注:代码实现请从网上查阅<br><strong>3) 卡方分桶</strong><br>自底向上的(即基于合并的)数据离散化方法。它依赖于卡方检验:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。<br><strong>基本思想</strong><br>对于精确的离散化,相对类频率在一个区间内应当完全一致。因此,如果两个相邻的区间具有非常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。而低卡方值表明它们具有相似的类分布。<br>实现步骤<br>Step 1:预先定义一个卡方的阈值;<br>Step 2:初始化;根据要离散的属性对实例进行排序,每个实例属于一个区间;<br>Step 3:合并区间;<br>计算每一对相邻区间的卡方值;<br>将卡方值最小的一对区间合并;<br>Aij:第i区间第j类的实例的数量;Eij:Aij的期望频率(=(Ni*Cj)/N),N是总样本数,Ni是第i组的样本数,Cj是第j类样本在全体中的比例;<br><strong>阈值的意义</strong><br>类别和属性独立时,有90%的可能性,计算得到的卡方值会小于4.6。大于阈值4.6的卡方值就说明属性和类不是相互独立的,不能合并。如果阈值选的大,区间合并就会进行很多次,离散后的区间数量少、区间大。<br><strong>注意</strong></p><blockquote>ChiMerge算法推荐使用0.90、0.95、0.99置信度,最大区间数取10到15之间; 也可以不考虑卡方阈值,此时可以考虑最小区间数或者最大区间数。 指定区间数量的上限和下限,最多几个区间,最少几个区间; 对于类别型变量,需要分箱时需要按照某种方式进行排序。<br><strong>代码实现</strong><br><a href="https://link.segmentfault.com/?enc=Foz5rMPqW%2BnY8UWtuynJnQ%3D%3D.KCZthAu2ZckyEWZN%2FYFDDEnl0GMewRpl6iBxtRMbMo3v2C80hgH9YrJtvu5rS7weeOltGS8MKPlS1cK3oaBPow%3D%3D" rel="nofollow">https://github.com/tatsumiw/C...</a></blockquote><h3><strong>3) 最小熵法分箱</strong></h3><p>需要使总熵值达到最小,也就是使分箱能够最大限度地区分因变量的各类别。<br>熵是信息论中数据无序程度的度量标准,提出信息熵的基本目的是找出某种符号系统的信息量和冗余度之间的关系,以便能用最小的成本和消耗来实现最高效率的数据存储、管理和传递。<br>数据集的熵越低,说明数据之间的差异越小,最小熵划分就是为了使每箱中的数据具有最好的相似性。给定箱的个数,如果考虑所有可能的分箱情况,最小熵方法得到的箱应该是具有最小熵的分箱。</p><h2><strong>3.3.4 特征组合</strong></h2><p><strong>注</strong>:有限考虑强特征维度<br><strong>1) 离散+离散</strong>:笛卡尔积<br><strong>2) 离散+连续</strong>:连续特征分桶后进行笛卡尔积或基于类别特征 group by,类似于聚类特征构造<br><strong>3) 连续+连续</strong>:加减乘除,二阶差分等</p><ul><li><strong>多项式生成新特征【针对连续值】</strong></li></ul><pre><code>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</code></pre><p><img src="https://pic1.zhimg.com/v2-13f3f30068f1b4c701a83c23c426c9c4_b.png" alt="img" title="img"><br>查看下衍生新变量后的相关性情况,颜色越深相关性越大:</p><pre><code>sns.heatmap(pd.DataFrame(df_titanic_numerical_poly, columns=poly.get_feature_names()).corr())</code></pre><p><img src="https://pic4.zhimg.com/v2-40ac3e5b5247fed6cb8c7dd31784ad77_b.png" alt="img" title="img"></p><h2>3.4 特征选择</h2><p><strong>目标</strong>是降低噪声,平滑预测能力和计算复杂度,增强模型预测性能<br>当数据预处理完成后,我们需要选择有意义的特征输入机器学习的算法和模型进行训练。<br>通常来说,从两个方面考虑来选择特征:</p><ul><li><strong>特征是否发散</strong>:如果一个特征不发散,例如方差接近于 0,也就是说样本在这个特征上基本上没有差异,这个特征对于样本的区分并没有什么用。</li><li><strong>特征与目标的相关性</strong>:这点比较显见,与目标相关性高的特征,应当优选选择。除方差法外,本文介绍的其他方法均从相关性考虑。</li></ul><p>根据特征选择的形式又可以将特征选择方法分为 3 种:</p><ul><li><strong>Filter</strong>:过滤法,按照<strong>发散性</strong>或者<strong>相关性</strong>对各个特征进行评分,设定<strong>阈值或者待选择阈值</strong>的个数来选择特征。</li><li><strong>Wrapper</strong>:包装法,根据目标函数(通常是预测效果评分),每次选择若干特征或者排除若干特征。</li><li><strong>Embedded</strong>:嵌入法,先使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据系数从大到小选择特征。类似于 Filter 方法,但是是通过训练来确定特征的优劣。 我们使用 sklearn 中的 feature_selection 库来进行特征选择。</li></ul><h3>3.4.1 过滤式</h3><p><strong>1) 方差过滤</strong><br>这是通过特征本身的方差来筛选特征的类。比如一个特征本身的方差很小,就表示样本在这个特征上基本没有差异,可能特征中的大多数值都一样,甚至整个特征的取值都相同,那这个特征对于样本区分没有什么作用。所以无论接下来的特征工程要做什么,都要优先消除方差为 0 的特征。VarianceThreshold 有重要参数 threshold,表示方差的阈值,表示舍弃所有方差小于 threshold 的特征,不填默认为 0,即删除所有的记录都相同的特征。</p><pre><code>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() #获得删除</code></pre><p>然 而,如果我们知道我们需要多少个特征,方差也可以帮助我们将特征选择一步到位。<br>比如说,我们希望留下一半的特征,那可以设定一个让特征总数减半的方差阈值,只要找到特征方差的中位数,再将这个<strong>中位数</strong>作为参数 threshold 的值输入就好了:</p><pre><code>df_titanic_numerical_fsvar = VarianceThreshold(np.median(df_titanic_numerical.var().values)).fit_transform(df_titanic_numerical)</code></pre><p>当特征是二分类时,特征的取值就是伯努利随机变,假设 p=0.8,即二分类特征中某种分类占到 80%以上的时候删除特征</p><pre><code>X_bvar = VarianceThreshold(.8 * (1 - .8)).fit_transform(df_titanic_numerical)
X_bvar.shape
执行结果:
(891, 5)</code></pre><p><strong>2) 卡方过滤</strong><br>卡方检验,专用于分类算法,捕捉相关性 追求 p 小于显著性水平的特征<br>卡方过滤是专门针对<strong>离散型标签(即分类问题</strong>)的相关性过滤。<br><img src="https://pic1.zhimg.com/v2-26e610775556bf70784d6cfbe2a8491c_b.png" alt="img" title="img"><br>卡方检验类 <strong>feature_selection.chi2</strong> 计算每个<strong>非负特征和标签之间</strong>的卡方统计量,并依照卡方统计量由高到低为特征排名</p><pre><code>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</code></pre><p><strong>3) F 检验</strong><br>只能捕捉线性相关性 要求数据服从正态分布,追求 P 值小于显著性水平特征。<br>F 检验,又称 ANOVA,方差齐性检验,是用来捕捉每个特征与标签之间的线性关系的过滤方法。它即可以做回归也可以做分类,因此包含 feature_selection.f_classif(F 检验分类)和 feature_selection.f_regression(F 检验回归)两个类。其中 F 检验分类用于标签是离散型变量的数据,而 F 检验回归用于标签是连续型变量的数据。<br>F 检验的本质是寻找两组数据之间的线性关系,其原假设是”数据不存在显著的线性关系“。</p><pre><code>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)</code></pre><p><strong>4) 互信息法</strong><br><strong>可以捕捉任何相关性 不能用于稀疏矩阵,追求互信息大于 0 的特征</strong><br>互信息法是用来捕捉每个特征与标签之间的任意关系(<strong>包括线性和非线性关系</strong>)的过滤方法。和 F 检验相似,它既可以做回归也可以做分类,并且包含两个类:<br>feature_selection.mutual_info_classif(互信息分类)feature_selection.mutual_info_regression(互信息回归)<br>这两个类的用法和参数都和 F 检验一模一样,不过 互信息法比 F 检验更加强大,F 检验只能够找出线性关系,而互信息法可以找出任意关系。 互信息法不返回 p 值或 F 值类似的统计量,它返回“每个特征与目标之间的互信息量的估计”,这个估计量在[0,1]之间取值,为 0 则表示两个变量独立,为 1 则表示两个变量完全相关。</p><pre><code>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)</code></pre><h3>3.4.2 包裹式</h3><p><strong>1) 递归特征消除法</strong><br>递归消除特征法使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,再基于新的特征集进行下一轮训练。使用 feature_selection 库的 RFE 类来选择特征的代码如下:</p><pre><code>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)</code></pre><p><strong>2) 重要性评估</strong></p><pre><code>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</code></pre><p><strong>3) 排列重要性评估</strong><br><strong>优点:</strong>快速计算;易于使用和理解;特征重要性度量的属性;追求特征稳定性<br><strong>原理:</strong>在训练机器学习模型之后计算置换重要性。这种方法在向模型提出假设,如果在保留目标和所有其他列的同时随机打乱一列验证集特征数据,对预测机器学习模型的准确性的影响程度。对于一个具有高度重要性的特征,random-reshuffle会对机器学习模型预测的准确性造成更大的损害。<br><strong>结果解读:</strong>每一行的第一个数字表示模型性能(例子中用的是准确率)衰减了多少,±后面的数字表示多次打乱的标准差。</p><pre><code>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())</code></pre><p><img src="https://pic3.zhimg.com/v2-e71826864a4cd0c646f1ab04898ec95e_b.png" alt="img" title="img"></p><h3>3.4.3 嵌入式</h3><p><strong>1) 基于惩罚项的特征选择法</strong><br>使用带惩罚项的基模型,除了筛选出特征外,同时也进行了降维。<br>使用 feature_selection 库的 SelectFromModel 类结合带 L1 惩罚项的逻辑回归模型,来选择特征的代码如下:</p><pre><code>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</code></pre><p>使用 feature_selection 库的 SelectFromModel 类结合 SVM 模型,来选择特征的代码如下:</p><pre><code>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</code></pre><p><strong>2) 基于树模型</strong><br> 树模型中 GBDT 也可用来作为基模型进行特征选择,使用 feature_selection 库的 SelectFromModel 类结合 GBDT 模型,来选择特征的代码如下:</p><pre><code>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)</code></pre><p><strong>总结一下,有几点做特征选择的方法经验:</strong><br>(1)如果特征是分类变量,那么可以从 SelectKBest 开始,用卡方或者基于树的选择器来选择变量;<br>(2)如果特征是定量变量,可以直接用线性模型和基于相关性的选择器来选择变量;<br>(3)如果是二分类问题,可以考虑使用 SelectFromModel 和 SVC;<br>(4)在进行特征选择前,还是需要做一下 EDA。</p><h2>四、特征变换</h2><p><img src="https://pic3.zhimg.com/v2-433e58e36c8de3167e5e6873e12b40b6_b.png" alt="img" title="img"></p><h3><strong>1) 标准化(Standardization)</strong></h3><p>转换为 Z-score,使数值特征列的算数平均为 0,方差(以及标准差)为 1。不免疫 outlier。 <br><strong>注</strong>:如果数值特征列中存在数值极大或极小的 outlier(通过 EDA 发现),应该使用更稳健(robust)的统计数据:用<strong>中位数</strong>而不是算术平均数,用<strong>分位数</strong>(quantile)而不是方差。这种标准化方法有一个重要的参数:(分位数下限,分位数上限),最好通过 EDA 的数据可视化确定。免疫 outlier。<br><img src="https://pic3.zhimg.com/v2-c480f67fafa5f77760fdd413a3d29b9e_b.png" alt="img" title="img"></p><pre><code>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') #写入文件</code></pre><h3><strong>2) 归一化(Normalization)</strong></h3><p>把每一行数据归一化,使之有 unit norm,norm 的种类可以选 l1、l2 或 max。不免疫 outlier。 <br><img src="https://pic3.zhimg.com/v2-0934ccbef22d553ac4fe9192559dc922_b.png" alt="img" title="img"><br>,其中 <img src="https://www.zhihu.com/equation?tex=%5Ciota" alt="iota" title="iota">iota 表示 norm 函数。 </p><h3><strong>3) 区间缩放(scaling)</strong></h3><p>将一列的数值,除以这一列的最大绝对值。<br>MinMaxScaler:线性映射到 [ 0,1 ] ,不免疫 outlier。 <br><img src="https://pic4.zhimg.com/v2-a0271a388fdd5954e9f75e0c85ef5af3_b.png" alt="img" title="img"><br>MaxAbsScaler:线性映射到 [ -1,1 ] ,不免疫 outlier。 <br><img src="https://pic1.zhimg.com/v2-c353c61e6c0216abac00a6a2d102cf68_b.png" alt="img" title="img"></p><pre><code>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') #写入文件</code></pre><p><strong>注</strong>:如果数值特征列中存在数值极大或极小的 outlier(通过 EDA 发现),应该使用更稳健(robust)的统计数据:用<strong>中位数</strong>而不是算术平均数,用<strong>分位数</strong>(quantile)而不是方差。这种标准化方法有一个重要的参数:(分位数下限,分位数上限),最好通过 EDA 的数据可视化确定。免疫 outlier。<br><strong>归一化与标准化区别</strong></p><blockquote>(a) 目的不同,归一化是为了消除纲量压缩到[0,1]区间;标准化只是调整特征整体的分布。 (b) 归一化与最大,最小值有关;标准化与均值,标准差有关。 (c) 归一化输出在[0,1]之间;标准化无限制。<br><strong>归一化与标准化应用场景</strong><br>(a) 在分类、聚类算法中,需要使用距离来度量相似性的时候(如 SVM、KNN)或者使用 PCA 技术进行降维的时候,标准化(Z-score standardization)表现更好。 (b) 在不涉及距离度量、协方差计算、数据不符合正太分布的时候,可以使用第一种方法或其他归一化方法。比如图像处理中,将 RGB 图像转换为灰度图像后将其值限定在[0 255]的范围。 (c) 基于树的方法不需要进行特征的归一化。例如<strong>随机森林,bagging 与 boosting</strong> 等方法。如果是基于参数的模型或者基于距离的模型,因为需要对参数或者距离进行计算,都需要进行归一化。</blockquote><h2>3.5.2 非线性变换【统计变换】</h2><p>利用统计或数学变换来减轻数据分布倾斜的影响。使原本密集的区间的值尽可能的分散,原本分散的区间的值尽量的聚合。<br>这些变换函数都属于幂变换函数簇,通常用来创建单调的数据变换。它们的主要作用在于它能帮助稳定方差,始终保持分布接近于正态分布并使得数据与分布的平均值无关。</p><h3><strong>1) log 变换</strong></h3><p>log 变换通常用来创建单调的数据变换。它的主要作用在于帮助<strong>稳定方差</strong>,始终保持分布接近于<strong>正态分布</strong>并使得数据与分布的平均值无关。因为 log 变换倾向于拉伸那些落在较低的幅度范围内自变量值的范围,倾向于压缩或减少更高幅度范围内的自变量值的范围。从而使得倾斜分布尽可能的接近正态分布。 所以针对一些数值连续特征的方差不稳定,特征值重尾分布我们需要采用 log 化来调整整个数据分布的方差,属于方差稳定型数据转换。<br>log 变换属于<strong>幂变换函数簇</strong>。该函数用数学表达式表示为 <br><img src="https://pic3.zhimg.com/v2-63c9621b4b9c41321fc0c4b32555444e_b.png" alt="img" title="img"><br>自然对数使用 b=e,e=2.71828,通常叫作欧拉常数。你可以使用通常在十进制系统中使用的 b=10 作为底数。<br><strong>代码实现</strong></p><pre><code>sns.distplot(df_titanic.fare,kde=False)</code></pre><p><img src="https://pic3.zhimg.com/v2-a31015cf24366c9a3d9a030b4235e1b6_b.png" alt="img" title="img"></p><pre><code>df_titanic['fare_log'] = np.log((1+df_titanic['fare']))
sns.distplot(df_titanic.fare_log,kde=False)</code></pre><p><img src="https://pic4.zhimg.com/v2-17d8dfcfb3a82bc1797fa1c5506cedeb_b.png" alt="img" title="img"></p><h3><strong>2) box-cox 变换</strong></h3><p>box-cox 变换是另一个流行的幂变换函数簇中的一个函数。该函数有一个前提条件,即<strong>数值型值必须先变换为正数</strong>(与 log 变换所要求的一样)。万一出现数值是负的,使用一个常数对数值进行偏移是有帮助的。<br>box-cox 变换是 box 和 cox 在 1964 年提出的一种广义幂变换方法,是统计建模中常用的一种数据变换,用于<strong>连续的响应变量不满足正态分布</strong>的情况。box-cox 变换之后,可以一定程度上减小不可观测的误差和预测变量的相关性。box-cox 变换的主要特点是引入一个参数,通过数据本身估计该参数进而确定应采取的数据变换形式,box-cox 变换可以明显地改善数据的<strong>正态性、对称性和方差相等性</strong>,对许多实际数据都是行之有效的。<br><strong>box-cox</strong> 变换函数:<br><img src="https://pic4.zhimg.com/v2-f5a4ecfba013b0a36d19accd06ed8313_b.png" alt="img" title="img"><br>生成的变换后的输出 y 是输入 x 和变换参数的函数;当 λ=0 时,该变换就是自然对数 log 变换,前面我们已经提到过了。λ 的最佳取值通常由最大似然或最大对数似然确定。<br><strong>代码实现</strong></p><pre><code># 从数据分布中移除非零值
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</code></pre><p><img src="https://pic3.zhimg.com/v2-ac8a629b7ff9378ec5b7a2a697b495e2_b.png" alt="img" title="img"></p><h2>3.5.3 离散变量处理</h2><h3><strong>1) 标签编码(label encoder)</strong></h3><p>LabelEncoder 是对<strong>不连续的数字或者文本进行</strong>编号,编码值介于 <strong>0 和 n_classes-1</strong> 之间的标签。<br>例如:比如有[dog,cat,dog,mouse,cat],我们把其转换为[1,2,1,3,2]。这里就产生了一个奇怪的现象:dog 和 mouse 的平均值是 cat。<br><strong>优点</strong>:相对于 OneHot 编码,LabelEncoder 编码占用内存空间小,并且支持文本特征编码。<br><strong>缺点</strong>:它隐含了一个假设:不同的类别之间,存在一种顺序关系。在具体的代码实现里,LabelEncoder 会对定性特征列中的所有独特数据进行一次排序,从而得出从原始输入到整数的映射。所以目前还没有发现标签编码的广泛使用,一般在树模型中可以使用。<br><strong>代码实现</strong></p><pre><code>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]))))
# 输出 特征标签值反转:['二线', '二线', '三线</code></pre><h3><strong>2) 独热编码(one hot encoder )</strong></h3><p>OneHotEncoder 用于将表示分类的数据扩维。最简单的理解用 N 位状态寄存器编码 N 个状态,每个状态都有独立的寄存器位,且这些寄存器位中只有一位有效,只能有一个状态。<br><strong>为什么要使用独热编码?</strong><br>独热编码是因为大部分算法是基于向量空间中的度量来进行计算的,为了使非偏序关系的变量取值不具有偏序性,并且到圆点是等距的。使用 one-hot 编码,将离散特征的取值扩展到了欧式空间,离散特征的某个取值就对应欧式空间的某个点。将离散型特征使用 one-hot 编码,会让特征之间的距离计算更加合理。 <br><strong>为什么特征向量要映射到欧式空间?</strong><br>将离散特征通过 one-hot 编码映射到欧式空间,是因为在回归、分类、聚类等机器学习算法中,特征之间距离或相似度的计算是非常重要的,而我们常用的距离或相似度的计算都是在欧式空间的相似度计算。<br>举个例子-假如有三种颜色特征:红、黄、蓝。<br>在利用机器学习的算法时一般需要进行向量化或者数字化。那么你可能想 假设 红=1,黄=2,蓝=3,那么这样实现了标签编码,即给不同类别以标签。然而这意味着机器可能会学习到“红<黄<蓝”,但这并不是我们的让机器学习的本意,只是想让机器区分它们,并无大小比较之意。<br>所以这时标签编码是不够的,需要进一步转换。因为有三种颜色状态,所以就有 3 个比特。即红色:1 0 0,黄色: 0 1 0,蓝色:0 0 1。如此一来每两个向量之间的距离都是根号 2,在向量空间距离都相等,所以这样不会出现偏序性,基本不会影响基于向量空间度量算法的效果。<br><strong>优点</strong>:独热编码解决了分类器不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用。它的值只有 0 和 1,不同的类型存储在垂直的空间。<br><strong>缺点</strong>:只能对数值型变量二值化,无法直接对字符串型的类别变量编码。当类别的数量很多时,特征空间会变得非常大。在这种情况下,一般可以用 PCA 来减少维度。而且 one hot encoding+PCA 这种组合在实际中也非常有用。<br><strong>代码实现</strong></p><ul><li><strong>使用 pandas 实现:</strong></li></ul><pre><code>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)</code></pre><p><img src="https://pic2.zhimg.com/v2-4532de4b36b0df9ca6373be96c5d44d5_b.png" alt="img" title="img"></p><ul><li><strong>使用 sklearn 实现:</strong></li></ul><pre><code>注:当特征是字符串类型时,需要先用 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.]])</code></pre><h3><strong>3) 标</strong>签二值化(LabelBinarizer)</h3><p> 功能与 OneHotEncoder 一样,但是 OneHotEncoder 只能对数值型变量二值化,无法直接对字符串型的类别变量编码,而 LabelBinarizer 可以直接对字符型变量二值化。</p><h2>3.5.4 降维</h2><p><strong>读取数据&数据展示</strong></p><pre><code>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</code></pre><p><img src="https://pic4.zhimg.com/v2-fffd3c8a6e2d4710d28b130594eea81b_b.png" alt="img" title="img"></p><h3><strong>1) P</strong>CA(Principal Component Analysis)</h3><p>作用:降维、压缩<br>步骤:</p><ul><li>求 <img src="https://www.zhihu.com/equation?tex=X" alt="X" title="X">X 均值 </li><li>将 <img src="https://www.zhihu.com/equation?tex=X" alt="X" title="X">X 减去均值计算协方差矩阵 <img src="https://www.zhihu.com/equation?tex=C%20%3D%20%5Cfrac%7B1%7D%7Bm%7DXX%5ET" alt="C = frac{1}{m}XX^T" title="C = frac{1}{m}XX^T">C = frac{1}{m}XX^T </li><li>对协方差矩阵 <img src="https://www.zhihu.com/equation?tex=C" alt="C" title="C">C 特征值分解 </li><li>从大到小排列 <img src="https://www.zhihu.com/equation?tex=C" alt="C" title="C">C 的特征值取前 <img src="https://www.zhihu.com/equation?tex=k" alt="k" title="k">k 个特征值对应的特征向量按行组成矩阵即为变换矩阵 <img src="https://www.zhihu.com/equation?tex=P_%7Bk%5Ctimes%20n%7D" alt="P_{ktimes n}" title="P_{ktimes n}">P_{ktimes n} </li></ul><p><strong>(a) 手动实现 PCA</strong></p><pre><code>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</code></pre><p><img src="https://pic3.zhimg.com/v2-9c97735662500913d108da8f364ec2de_b.png" alt="img" title="img"><br><strong>(b) sklearn 的 PCA</strong></p><pre><code>import numpy as np
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
newX = pca.fit_transform(X)
draw_result(newX, y)</code></pre><p><img src="https://pic1.zhimg.com/v2-264fa45bf8b1b0a135a676bf7a9dd5d4_b.png" alt="img" title="img"></p><h3><strong>2)</strong> SVD(Singular Value Decomposition)</h3><p><strong>作用</strong>:特征分解、降维<br><strong>步骤</strong>:<br><img src="https://pic2.zhimg.com/v2-c86c763ce10aef3a6ef6fe7aa10a023d_b.png" alt="img" title="img"><br><strong>(a)手动实现 SVD</strong></p><pre><code>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)</code></pre><p><img src="https://pic3.zhimg.com/v2-9c97735662500913d108da8f364ec2de_b.png" alt="img" title="img"><br><strong>(b) sklearn 的 SVD</strong><br>TruncatedSVD,截断奇异值分解(当数据量非常大,svd 跑不出来时使用此方法)。</p><pre><code>from sklearn.decomposition import TruncatedSVD
iris_2d = TruncatedSVD(2).fit_transform(X)
draw_result(iris_2d, y)</code></pre><p><img src="https://pic1.zhimg.com/v2-4fb97b419b9d078b9c19094734233aec_b.png" alt="img" title="img"></p><h3><strong>3)</strong> PCA 和 SVD 的关系</h3><p><img src="https://pic2.zhimg.com/v2-394bdfca1472fe60198864c8a82aa0bd_b.png" alt="img" title="img"></p><h3><strong>4) Fisher</strong> <strong>线性判别分析</strong>(Linear Discriminant Analysis,LDA)</h3><p>是有监督的降维,通过最小化类内离散度与最大化类间离散度来获得最优特征子集。 <br><img src="https://pic3.zhimg.com/v2-1571167b9b6e5148ad3f6b325d57fd1a_b.png" alt="img" title="img"></p><blockquote>LD1 通过线性判定,可以很好的将呈正态分布的两个类分开。 LD2 的线性判定保持了数据集的较大方差,但 LD2 无法提供关于类别的信息,因此 LD2 不是一个好的线性判定。</blockquote><pre><code>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)</code></pre><p><img src="https://pic1.zhimg.com/v2-47bdf77da094c2f5db31eeb703b4bfdc_b.png" alt="img" title="img"><br>LDA 与 PCA 相似:</p><blockquote>PCA 试图寻找到方差最大的正交的主成分分量轴 LDA 发现可以最优化分类的特征子空间 LDA 和 PCA 都是可用于降低数据集维度的线性转换技巧 PCA 是无监督算法 LDA 是监督算法 LDA 是一种更优越的用于分类的特征提取技术</blockquote><h3><strong>5) T</strong>-SNE</h3><pre><code>from sklearn.manifold import TSNE
tsne = TSNE(n_components=2)
iris_2d = tsne.fit_transform(X)
draw_result(iris_2d, y)</code></pre><p><img src="https://pic3.zhimg.com/v2-499298024b1cf8f4404aa017a4015156_b.png" alt="img" title="img"></p><h2>五 总结</h2><p>切忌:一开始就把所有的特征一股脑地扔进模型,容易被一些没用的特征误导。<br><strong>1) EDA</strong><br>plot,plot,plot,重要的事情说三遍<br><strong>2) 特征预处理</strong><br><strong>时间序列</strong>:把昨天的特征加入今天的特征,或者把和昨天相比,特征数值的改变量加入今天的特征。<br><strong>连续特征离散化</strong>(决策树类型的模型没意义):一种挺有趣的变种,就是限制浮点数特征的精度,异常数据有很强的鲁棒性,模型也会更稳定。<br><strong>clipping</strong>:可以用 pandas dataframe 的.clip(low, upper)方法,把特征值的取值限制在一定范围内<br><strong>3) 数据清洗</strong><br>要合情合理,不可盲目填充缺失值、删除异常值,要建立在统计科学基础上。<br><strong>4) 特征变换</strong><br>除非万不得已,不要用 PCA 或者 LDA 降维,建议直接减原始特征。</p><ul><li><strong>特征变换要切记</strong>:</li></ul><p>任何针对单独特征列的单调变换(如对数):不适用于决策树类算法。<br>对于决策树而言, <img src="https://www.zhihu.com/equation?tex=X%E3%80%81X%5E3%E3%80%81X%5E5" alt="X、X^3、X^5" title="X、X^3、X^5">X、X^3、X^5 之间没有差异 , <img src="https://www.zhihu.com/equation?tex=%7CX%7C%E3%80%81X%5E2%E3%80%81X%5E4" alt="|X|、X^2、X^4" title="|X|、X^2、X^4">|X|、X^2、X^4 之间没有差异,除非发生了舍入误差。<br><strong>线性组合(linear combination)</strong>:仅适用于决策树以及基于决策树的 ensemble(如 gradient boosting, random forest),因为常见的 axis-aligned split function 不擅长捕获不同特征之间的相关性;不适用于 SVM、线性回归、神经网络等。</p><ul><li><strong>类别特征与数值特征的组合</strong>:</li></ul><p>用 N1 和 N2 表示数值特征,用 C1 和 C2 表示类别特征,利用 pandas 的 groupby 操作,可以创造出以下几种有意义的新特征:(其中,C2 还可以是离散化了的 N1)</p><pre><code>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) 频数</code></pre><p>仅仅将已有的类别和数值特征进行以上的有效组合,就能够大量增加优秀的可用特征。<br>将这种方法和线性组合等基础特征工程方法结合(仅用于决策树),可以得到更多有意义的特征,如:</p><pre><code>N1 - median(N1)_by(C1)
N1 - mean(N1)_by(C1)</code></pre><ul><li><strong>用基因编程创造新特征</strong></li></ul><p>基于 genetic programming 的 symbolic regression【python 环境下首推基因编程库为 <strong>gplearn</strong>】。<br>基因编程的两大用法:<br><strong>转换(transformation)</strong>:把已有的特征进行组合转换,组合的方式(一元、二元、多元算子)可以由用户自行定义,也可以使用库中自带的函数(如加减乘除、min、max、三角函数、指数、对数)。组合的目的,是创造出和目标 y 值最“相关”的新特征。<br>spearman 多用于决策树(免疫单特征单调变换),pearson 多用于线性回归等其他算法。<br><strong>回归(regression</strong>):原理同上,只不过直接用于回归而已。</p><ul><li><strong>用决策树创造新特征:</strong></li></ul><p>在决策树系列的算法中(单棵决策树、gbdt、随机森林),每一个样本都会被映射到决策树的一片叶子上。因此,我们可以把样本经过每一棵决策树映射后的 index(自然数)或 one-hot-vector(哑编码得到的稀疏矢量)作为一项新的特征,加入到模型中。<br>具体实现:apply()以及 decision_path()方法,在 scikit-learn 和 xgboost 里都可以用。<br><strong>5) 模型</strong></p><ul><li>树模型:</li></ul><blockquote>对特征数值幅度不敏感,可以不进行无量纲化和统计变换处理; 由于数模型依赖于样本距离来进行学习,可以不进行类别特征编码(但字符型特征不能直接作为输入,所以需要至少要进行标签编码)。 LightGBM 和 XGBoost 都能将 NaN 作为数据的一部分进行学习,所以不需要处理缺失值。其他情况下,我们需要使用。</blockquote><ul><li>依赖样本距离来学习的模型(如线性回归、SVM、深度学习等):</li></ul><blockquote>对于数值型特征需要进行无量纲化处理; 对于一些长尾分布的数据特征,可以做统计变换,使得模型能更好优化; 对于线性模型,特征分箱可以提升模型表达能力;<br>注:结合工作内容、学习总结以上内容,如有错误,请指出,诚心请教</blockquote><h2>参考资料</h2><p>光喻:【持续更新】机器学习特征工程实用技巧大全zhuanlan.zhihu.com<img src="https://pic2.zhimg.com/v2-ad285f0ed73415d09a61eeab1f37d599_180x120.jpg" alt="图标" title="图标"><br>特征工程系列:数据清洗 - 大咖驾到 - 博客园www.cnblogs.com<img src="https://pic1.zhimg.com/v2-8e985a16daa5b1542cb415e24761ad94_180x120.jpg" alt="图标" title="图标"></p><p>作者:王岳 好未来机器学习算法专家</p>
好未来学而思网校如何实现1小时内发布一个新项目
https://segmentfault.com/a/1190000024419906
2020-09-11T09:15:24+08:00
2020-09-11T09:15:24+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<h2>背景</h2><p>在互联网高速迭代的浪潮下,相信每位研发人员都经历过新项目的需求研发、测试验收、部署上线、功能迭代、甚至项目下线的全过程。其中“需求研发”、“测试验收”、“功能迭代”、“项目下线”这类环节对于研发人员来说,基本上可以有效的把控,在整个项目的生命周期内,也是投入最多的几个环节,因此可以提前预估出合理的时间计划。</p><p>这几个关键的环节中,将项目部署上线,往往是最后一个环节。对项目组成员和需求方来说,软件研发完成以后,投入生产环境,是一个水到渠成的事情,需要立马完成。</p><p>然而在网校大背景下,一个新项目投入到生产环境是一个非常严肃的事情,实际上需要经历的几个环节如下:</p><ol><li>新域名备案(准备必要的资料)</li><li>服务器申请(机房选择、单机硬件配置、系统选型、网段规划、机器命名、容量预估)</li><li>服务器生产(必要软件初装、参数校对)</li><li>研发授权(准备dev or guest人员名单、准备机器标签进行批量授权)</li><li>投入发布系统(创建发布项目、填充机器信息、项目人员分工)</li><li>配置网关(集群选择、vhost配置、upstream配置)</li><li>域名解析(是否经过全站加速/高防等防护产品、提供解析地址生效)</li><li>若是容量预估不足,还需要再经历一遍#2~#6(多批生产的机器存在环境配置不一致的隐患)</li><li>如果遇到项目下线,往往域名又被容易忽略掉,造成一些无用且已经备案的域名,不易管理</li></ol><p>整个过程最快也要以天为单位,隔天交付生产;慢的话要数天才能完成生产环境的部署,并且需要跨多个部门协同完成。2020年公益直播高峰前夕,“连轴转”完成服务扩容的伙伴,尤其铭心刻骨。</p><p>因此,为了解决新项目部署周期长的问题,基础架构部启动了两个项目:《域名收敛》、《容器平台》。</p><p>《域名收敛》项目,主要为了缩短:#1、#7、#9这3个环节上消耗的时间和精力。</p><p>《容器平台》项目,主要为了缩短:#2、#3、#4、#5、#6、#8这6个环节上消耗的时间和精力。</p><p>并且现在两个项目已经无缝结合,在同一个平台上完成。</p><p>下面将详细的介绍,我们如何给开发完成的新项目,在1小时内发布上线提供便利,实现小时级交付生产,以及当前的项目应用规模。</p><h2>容器平台</h2><p>物理机与虚拟机时代整个新服务上线流程严重依赖机房资源、运维部署、多个系统的环境配置、以及性能压测。</p><p>漫长的处理流程</p><blockquote>资源申请流程 -〉 资源采购 -〉 机器授权 -〉 服务运行环境配置(环境、网络) -〉 配置发布系统 -〉 配置网关/日志 -〉 性能压测</blockquote><p>硬件资源不足时,需要排队采购审批,到资源上架初始化过程可能长达数月,漫长的处理流程非常打击开发人员的热情。</p><p>在资源充足的时候,也需要数天的处理流程。我们需要去解决这些漫长的流程的问题 ,在互联网时代敏捷性决定了业务的生与死。</p><p>我们是如何做到便捷性的呢?</p><ol><li>通过容器的环境一致性</li><li>网校云的统一调度硬件资源使我们能有效盘活有效固定资产池</li><li>通过打通网关、日志体系,每位研发老师完全自助的完成所有配置。</li></ol><h3>应用快速发布</h3><p>驱动于git devops的设计,用户只需要提供git仓库地址及项目域名,平台会调用pipeline引擎构建镜像,通过配置基于git事件的触发全自动构建镜像,然后按照模板创建 deployment,service,及ingress的kubernets等应用及资源。流水线作业设计,一气呵成完成应用发布与镜像构建。</p><p>与网关进行了自动化打通, 应用部署完成后我们将集群的网关ingress的地址提交给网关, 触发网关的upstream更新操作,从而实现了服务的发现以分流。</p><p><img src="/img/remote/1460000024419909" alt="快速发布.png" title="快速发布.png"></p><p>同时与公司发布系统进行打通,在用户发布完应用后,可以获得一个hook地址,发布系统设置hook后,在发布的时候出发k8s的滚动升级,以实现混合部署应用的同步升级。</p><h3>云原生的PIPELINE引擎</h3><p>gitlab事件触发充分借鉴了K8S 自带的CI/CD系统Prow的系统设计,实现gitlab的自动事件通知支持:</p><ol><li>Tag事件</li><li>Push事件</li><li>Merge事件</li></ol><p>引擎通过向注册相关事件并监听, 当事件到来时会根据事件类型分发事件给相应的插件来处理。已经实现了和网校云容器平台的无缝连接, 通过在简单的web配置就可以构建出基于指定Tag或指定Commit的镜像。</p><p>引擎完全运行在容器之上,多插件和PIPELIE、灵活多变的设计让应用的发布不会拘泥于特定的运行与构建环境,助力新项目的新环境的快速发布于迭代。</p><h3>弹性扩容</h3><p>以基于horizontal pod autoscalers实现的 集群应用的自动弹性伸缩, 根据CPU使用量指标, metrics server从kubelet中的cAdvisor组件中获取cpu的资源消耗,默认情况下,每10s采样一次,每30s计算一次cpu使用量。当业务高峰来临, 监控到pod的CPU占用量超过预定的阈值, 会自动增加服务的副本数量(在设定范围),反之会减小副本数量,释放集群资源,由此实现集群资源的高效利用。</p><p><img src="/img/remote/1460000024419910" alt="hpa.png" title="hpa.png"></p><p><img src="/img/remote/1460000024419912" alt="krt.png" title="krt.png"></p><h3>自动化日志收集</h3><p>我们采用了filebeat作为日志采集工具。</p><ol><li>filebeat以daemon set模式运行,保证了每个节点都会有一个filebeat容器运行。</li><li>filbeat容器可以采集宿主机以及该宿主机上其他容器的日志。</li><li>可以在我们的容器平台,选择需要收集日志的应用,即可完成日志采集。</li><li>通过configMap(配置中心)的方式,对采集配置进行管理</li></ol><p>因为我们已经和网校的日志中心打通,我们所采集的日志将均被采集到kafka中,然后落地到日志中心ES集群。</p><h2>域名收敛</h2><h3>一、为什么要做域名收敛</h3><p>随着网校用户体量飞速发展,业务越来越多样化,复杂化,各服务之间的耦合度也越来越高,依赖严重,相互影响。为了减轻依赖,各项目都希望有独立域名来提供服务,业务方也开始频繁申请新域名,使网校对外暴露的域名越来越多。这种做法逐渐造成了网校域名泛滥现象。据不完全统计,截止2019年年中,网校申请的域名有数千个,而且以每周数十的速率增长,其中经过网关的域名就已经破千,而且很多域名已经处于无人认领的状态,没有人敢确认这些服务是否在线上running,因此也不敢轻易去修改或下线。除此之外,域名的泛滥成灾也给网校的研发与运维带来了很多痛点问题:</p><ol><li>研发人员:</li></ol><ul><li>上线困难: <strong>域名申请、域名备案、域名解析、网关创建应用、vhost、upstream</strong>、申请后端机器、后端nginx+fpm配置、发布代码。周期长(1周~1月),流程冗长,效率极低,研发人员将大量精力耗费在项目上线上。</li><li>开发困难: 无论是app、H5、web等客户端,开发需要适配不同域名的不同接口,代码臃肿,难以维护,单app端依赖的接口就有涵盖了近百域名,开发人员已经不堪重负。</li></ul><ol><li>运维人员:</li></ol><ul><li>管理维护: 上千的域名维护管理困难,只增不减,很多域名没有访问量,无人维护,无人认领。</li><li>配置困难: 运维对接各种开发配置需求,造成网关层配置毫无规范,复杂冗长,频繁修改,出错率高,风险性大。</li><li>安全问题: 暴露域名过多降低了系统的安全性,大量域名需要备案审计、安全扫描,也暴露出了很多安全漏洞。</li></ul><p>2020年2月网校公益直播期间,由网校技术委员会发起的‘竞品分析’专项进行了多项技术评测指标,其中在服务端域名管控上,我们实施的力度远远不及对手,网校的研发和运维人员还没有形成域名收敛的意识,不仅没有制定相关的规范文档,也没有对应的技术手段实现域名复用。</p><p>针对以上这些痛点问题,网校服务端技术委员会、运维团队、集团安全组携手发起了《网校域名收敛专项》,项目目的是要解决网校域名泛滥的问题,一方面要对现有的域名进行收敛与规范,另一方面对后续新上线项目流程进行调整与优化,加速推进项目的自动化与标准化上线流程,为网校云的建设铺路。</p><h3>二、如何实现域名收敛</h3><h4>2.1 域名规划</h4><p>那么该如何实现域名收敛呢?首先我们将网校的服务大致分为进行了分类,大致如下:</p><ul><li>按照服务类型分类:WEB服务与API服务,前者输出的是html,后者输出的是json数据。</li><li>按照服务范围分类: 对内服务与对外服务,前者只提供内网或者办公区服务,后者提供公网服务。</li></ul><p>如此一来,就有四种类型的服务: 对外WEB,对内WEB, 对外API,对内API。</p><p>因此我们设计了四个域名与之对应: app.xueersi.com; app.xesv5.com; api.xueersi.com;api.xesv5.com; 这四个域名的作用如下。</p><ul><li>app.xueersi.com : 主要代理对外WEB应用,面向公网用户,例如站点首页。</li><li>app.xesv5.com : 主要代理对内WEB应用,面向内部用户,例如admin管理后台,各种监控平台,告警平台等对内系统。</li><li>api.xueersi.com : 主要代理对外API服务,例如app,ios,pc端调用的api服务,阿里云、微信等第三方回调api。</li><li>api.xesv5.com : 主要代理内部api服务,例如用户数据等中台类api接口。</li></ul><p><img src="/img/remote/1460000024419911" alt="收敛域名的作用" title="收敛域名的作用"></p><h4>2.2 方案制定</h4><h5>2.2.1 服务标签化</h5><p>以网校的站点架构特点来说,几乎所有的服务都由统一的Nginx网关进行反向代理,一个域名对应一个vhost与upstream,通过Nginx的proxy_pass进行路由转发。为了实现域名收敛,我们决定对网关层进行技术改造,实现基于收敛域名的动态路由分发,所谓的收敛域名就是指上面提到的四个专用域名app.xueersi.com,app.xesv5.com,api.xueersi.com, api.xesv5.com, 除特殊情况外,尽量让所有的服务都能够复用这四个收敛域名(也可以叫做网关的专用’代理域名’)。</p><p>如果大家都共用同一个域名,怎么识别不同的服务呢? 很简单,根据url来识别, 我们会将url中第一个’/'后面的字段作为每个项目的标签,<strong>网关层会根据"标签"识别不同服务</strong>,比如新上线一个学习应用,想使用study.xueersi.com, 则业务方可以拿"study"作为项目标签,同时复用app.xueersi.com这个域名,当用户访问<a href="https://link.segmentfault.com/?enc=MVjybrMamBHBtrnFm2zBZA%3D%3D.eh2g0ssYRjY%2Bs12Tnm86RTTwlZ3coT1XvmquVzVEBgwfsIu3QpKvSfEIGKCFf36bbgJWGjl%2FX4t0szUi9wfv0g%3D%3D" rel="nofollow">http://app.xueersi.com/study/...</a>,网关会通过改写host,upstream,uri来实现以下转发规则:</p><ul><li>改写upstream:从proxy_pass <a href="https://link.segmentfault.com/?enc=6IP%2BZ8zWf5xLnI70Xp7wPQ%3D%3D.gyudmghLjoWmCjA0NmMRx32XJrT2QJOM4Ehsamb3jxo%3D" rel="nofollow">http://app.xueersi.com</a> 改写成 proxy_pass <a href="https://link.segmentfault.com/?enc=%2FarIXcu6mJiBnJOE1x%2BNqQ%3D%3D.UjGId9lVote5Z5Oh5A0J3ALu4XhJLBGZXq9xH64Bcfo%3D" rel="nofollow">http://study.xueersi.com</a></li><li>改写host : 从 app.xueersi.com 改写成 study.xueersi.com</li><li>改写uri : 从 /study/course/getInfo?a=1 改写成 /course/getInfo?a=1</li></ul><p>此时对于后端服务来说,用户请求的是<a href="https://link.segmentfault.com/?enc=nEXbCm6XztIE2jYKKL8Qbw%3D%3D.3m4bE9Fkb2%2BAXuM6JjOJNHNAaQIRITMb1XNR47e0Pl%2BH9SFDYNuvEmTPLe5LARzq" rel="nofollow">http://app.xueersi.com/study/...</a>,经过网关转化后,后端真正接收到的请求为: <a href="https://link.segmentfault.com/?enc=VcCxiU8pTbpdOxioVo4jRg%3D%3D.UUYz%2FWsGeoqmAKMSUuX5LARRLcUmCmxPHIfRdUg%2BG9CFj2%2BdjlEBoKqg1GXW39p3" rel="nofollow">http://study.xueersi.com/cour...</a></p><p>同理,其他服务也是类似,复用同一域名,独占一个标签。</p><h5>2.2.2 上线流程简化</h5><p>在服务标签化之后,服务的暴露形式就从‘域名’依赖转换成了‘标签’依赖,那么对于研发人员则省去了域名申请,域名解析,网关创建vhost等步骤,以部署在kvm的服务为例,上线流程简化为:</p><p><img src="/img/remote/1460000024419914" alt="image20191225_20589.png" title="image20191225_20589.png"></p><p>其中开发需要做的只有两件事情:</p><ul><li>域名备案 :从立项到上线之间任意时间备份即可(安全部门硬性要求,公网域名必须备案,私网域名可以不用)。</li><li>创建upstream: 通过网关后台创建即可。</li></ul><p>对于接入了网校云的服务,依赖于k8s的容器运行环境,这种服务的上线会更快,网关已经和k8s进行了打通,只需要在k8s上进行应用创建,选择好对应的网关集群,一键切流即可上线一个新项目。</p><h4>2.3 技术实现</h4><p>从上述的技术方案中不难看出,实现的关键点在于网关需要对每一个以收敛域名打头的请求,按照url进行切割,提取出来请求的"标签",同时改写请求的host、upstream、uri,于是在网关层我们开发了dyroute插件实现了上述功能。该插件不仅支持host,url,header,body,ip,referer,cookie等十几种筛选条件,也支持URL的多段分割和提取,可以按需对host、upstream、uri进行定制。</p><h3>三、问题与解决</h3><p>理想很丰满,现实很骨感。真正推进域名收敛的过程,我们遇到了很多棘手的问题,这里记录了主要的几个。</p><h4>3.1 path池</h4><p>按照网关的这套转换规则会存在一个漏洞,以 app.xueersi.com 为例,如果有人请求 app.xueersi.com/A 则会间接的访问到 A.xueersi.com,访问 app.xueersi.com/B 则会访问到 B.xueersi.com,也就是他可以任意试探访问我们的不通服务。你可能会问,app.xueersi.com 的设计初衷不就是代理公网服务吗?就算试探中了也没有关系,本来就是面向公网用户的。网校的域名有一个"不成文"的规则,就是 .xueersi.com 都是对外的,.xesv5.com 都是对内的,但是由于历史原因,很多域名并没有准守这套规则,例如 oa.xueersi.com 本来应该是对内的OA系统,但是却用了 .xueersi.com 的对外域名。如果有用户通过 app.xueersi.com/oa 发送请求,则可以毫无阻拦的访问到我们对内的系统。同理,对于本来对外确用了 .xesv5.com 的服务,也会有类似的问题。</p><p>针对此问题,我们设计了path池机制,每一个收敛域名需要和一个path池进行绑定,我们会在创建upstream的时候把对应的标签加入到path池,只有加入到了对应path池的url才能正常通过网关,其他胡乱试探的/A,/B,/C的请求会被网关拦截。</p><h4>3.2 日志切割</h4><p>以 app.xueersi.com 为例,在网关上会配置一个对应的vhost,当请求到来时Nginx会匹配中这个vhost,也就是所有接入到这个域名的服务都会共用vhost。一个vhost就有一份log文件的问题,如果后期这个域名接入了上百个服务,那这上百个服务的access log全部会记录到 <br>app.xueersi.com_access.log 这份文件中,势必会造成单份日志文件过大的问题,同时会对ELK的搜索造成困扰,用户很难找到自己服务对应的日志。</p><p>针对此问题,我们采取了一种"变量式"日志记录方式,即"access_log /home/nginx/logs/app.xueersi.com_{host}_access.log", 当接入app.xueersi.com的请求到来时,网关层会识别标签信息并将host进行对应的改写,到了Nginx的log阶段,会识别当前的host变量决定往哪份log文件里写日志。例如 app.xueeris.com/A 则会写入app.xueersi.com_A_access_log,而 app.xueersi.com/B 则会写入app.xueersi.com_B_access_log。</p><h4>3.3 cookie爆炸</h4><p>当多个服务共用一个域名时另一个棘手的问题是cookie,服务端如果都往app.xueersi.com里set cookie,一是不同cookie key可能会重复,二是如果没有规范后期很可能会超出浏览器的最大cookie长度限制。</p><p>针对此问题,我们从两方面着手解决。一方面设计了一套cookie管理系统,后端服务如果想要在 app.xueersi.com 里设置cookie,都必须在管理系统上进行申请,如果该cookie已经有人申请过,则不允许重复申请,cookie管理系统上会记录每个cookie对应的服务信息。 另一方面对dyroute插件进行改造,在发送响应给客户端之前,会对cookie进行校验,不属于该域名的cookie会被拦截。</p><h4>3.4 跨域</h4><p>跨域问题也是令人头疼的一个问题,集团安全组反馈,这些收敛域名会存在跨域风险,需要在有跨域限制。但是有的服务希望能够在网关层统一处理跨域请求,有的服务则希望后端自己处理跨域,有的服务希望后端自行处理,有的希望能够任意origin可以跨域访问,有的则希望只能‘http_origin’的访问,甚至会有希望自定义添加allow header头的业务。要想在一个域名vhost里cover所有的需求很难做到,我们和业务方以及安全组讨论了一个折中方案,即所有接入域名收敛的服务统一在网关层进行跨域处理,网关会允许集团内的域名相互访问,例如’.xesv5.com,.xueersi.com, .100tal.com’之间的跨域访问没有限制,而外部域名则会被拦截。另外我们在allow header里添加了浏览器识别的通用header头,同时设置有其他变量供业务方选定作为自定义字段。</p><h4>3.5 双活</h4><p>接入了域名收敛的服务中,有一部分服务是有双活需求的,但是这些域名是解析到了世纪互联IDC,无法满足双活需求,于是我们将这些域名的公网解析前移到了切流网关上,并利用切流网关的能力实现了双活需求,默认会直接转发到世纪互联, 如果有业务需要转发到阿里云会根据提前设定好的路由规则进行阿里云转发。</p><p>其实拦在我们面前的问题远远不止于此,例如前端js动态配置问题,网关插件执行顺序问题,多环境、多事业部域名收敛规划、多协议支持问题、接入全站加速问题、静态资源cdn缓存问题等等。而且面临的阻力也非常大,并不是所有人都乐意接受这个事情,特别是针对一些老的服务,没有人愿意去改造,变革总会面临着走出舒适区,适应新的规则,因此很多人会有抗拒。我们要推动全网校的前端、后端、运维、集团安全以及各部门的项目负责人,制定一套网校级的域名收敛规范,同时将这套历史经验推广到全集团。</p><p>经过了半年的努力,我们也取得了一点小小的成果,不仅在网校落地了一套域名收敛规范,以网关和容器为核心,成功实践于网校云平台,收敛的域名总数也突破500大关。</p><p><img src="/img/remote/1460000024419913" alt="image2020812_151117.png" title="image2020812_151117.png"></p><p>我们相信,既然认定了是对的,就一直干下去,解决麻烦的最好方法就是不怕麻烦,总有一天会看到收获。</p><p><strong>作者简介</strong></p><p>陈朝飞为好未来PHP/Golang开发高级专家</p>
想了解一个异地多校平台的架构演进过程吗? 让我来告诉你!
https://segmentfault.com/a/1190000023812011
2020-08-28T21:58:11+08:00
2020-08-28T21:58:11+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
6
<p>一、背景</p><p><strong>项目介绍</strong></p><p><strong>励步双师课堂是以录播视频和线下中教老师结合的 AI 智能化面授教学课程。课堂中有三个角色:</strong></p><ul><li>主播老师: 视频哈佛外教老师,带着小朋友进行英文知识点的教学。主要承载“教”的职能。</li><li>主教老师: 主要负责引导,陪伴,激励小朋友,组织课堂纪律,关注小朋友上课的情况,和家长进行一对一沟通等等。主要承载“育”的职能。</li><li>学生: 上课小朋友,2-8 岁。</li></ul><p>目前整体的教学组织架构是以深圳研发中心示范班+加盟校区的方式进行教学研发、培训、常规授课。通常新功能会先预发布到深圳示范班预授,稳定后才会发布到其他加盟校区。这样既能保证其他加盟校区稳定教学、又能快速迭代新功能。以下一个简化的组织架构图。</p><p><img src="/img/remote/1460000023812015" alt="" title=""> </p><p>上文已经提到双师课堂是以录播的形式进行教学,必然需要课件视频。其中课件视频会经历几个流程:录制、上传、打点、审核、教学使用。早期起步阶段只有深圳研发中示范班授课,因此课件视频存储在本地机房,在同一内网下能正常使用。随着产品逐渐打磨成形,必然要落地到加盟校区使用。可是一个迫切要解决的问题摆在我们面前:加盟校区如何获取课件视频?</p><p>要解决的几个问题:</p><ol><li>一个课件视频的容量比较大: <strong>1-2G</strong>, 一节课的课件视频总和: <strong>2-3G</strong>。研发中心的教研人员如何快速上传课件视频和预览课件视频,并且支持加盟校区主播端播放线上课件视频</li><li>加盟校区外网环境不稳定的情况下,如何保证课件视频流畅地播放.</li><li>如何在研发中心示范班和加盟校区间针对课件视频做灰度发布和A/B测试</li></ol><p>在这样背景下,蒲公英发布平台在内部开始推进。总得来说大概经历3个阶段:</p><ul><li>阶段1: 课件视频从 “研发中心本地机房” => “云端OSS”</li><li>阶段2: 课件视频从 “研发中心本地机房” => “云端OSS” => “校区机房”</li><li>阶段3: 教学资源从 “研发中心本地机房” => “云端OSS” => “发布平台(管理)” => “校区机房”</li></ul><p> <img src="/img/remote/1460000023812016" alt="" title=""> </p><p>二、蒲公英总体架构图</p><p><img src="/img/remote/1460000023812014" alt="" title=""> </p><p>上方是蒲公英的总体架构图。最上层是现阶段支持的发布资源类型:课件视频/图片、APP安装包、页面静态资源、互动多媒体资源、docker镜像、脚本文件、Nodejs扩展库DLL等 </p><p>下半部分是云端和校区的系统:</p><ul><li>CMDB:管理校区资产信息。蒲公英将发布的资源和cmdb的资产信息(校区城市、分校、教室、教学设备、校区服务器)关联一起.</li><li>版本管理:记录各终端使用的各类资源的当前版本号、历史版本、版本升级依赖.</li><li>发布平台:后台系统,负责资源的手动发布、自动发布、灰度发布、回滚等发布策略</li><li>Mercedes Server: 负责接收上游发布平台的资源分发消息、转发消息给相应校区节点的Agent服务</li><li>Beetle: 负责上传资源/下载资源</li><li>Talgate:负责Mercedes Server和所有校区节点Agent服务地注册,会话连接管理,消息通信等</li><li>Mercedes Agent: 负责接收Server的分发消息、调度Beetle从云端OSS下载资源、同校区两台服务器的资源同步</li><li>Cadillac: 负责接受校区教室主播端访问内网资源请求、校区教学服务的api gateway.</li></ul><p>三、起源</p><p>痛点:课件视频文件很大,教研老师上传视频时间长、上传过程出现网络波动或者关闭页面需重新上传。课件打点审核通过后,如何快速提供给示范班使用。</p><p>解法:</p><ul><li>课件系统是一个提供给教研老师制作课件、管理课件的后台系统。它是一个BS架构的WEB系统,上传文件的方式是通过http直传,上传过程中一旦有中断,只能重新上传, 这样大大降低教研老师工作效率。经过调研决定采用tus协议实现断点续传上传大文件。tus协议是一种基于HTTP/1.1和HTTP/2机制用于文件断点续传。这里画了一个大致上传流程图,详细内容请查看tus官方文档(<a href="https://link.segmentfault.com/?enc=FiEHdJoYVQ9aD75A4w7pmQ%3D%3D.I9aIKlaZms7rAaOsQEGzqA%3D%3D" rel="nofollow">https://tus.io/</a>)。<p><img src="/img/remote/1460000023812017" alt="" title=""></p></li><li>教研老师上传课件后,还需经过审核员预览、审核通过后才能交付使用。因此考虑可以先上传到研发中心的资源服务器,因为是在同一个内网环境,不需要经过外网,这样加快了上传速度。待课件经审核员审核通过后,再经由资源服务器上传到云端OSS。</li><li>课件视频上传问题已经解决了。但加盟校区的主播端播放视频的优化方案还未到位,考虑到示范班和加盟校区想尽早使用,前线业务不能耽搁的情况下。我们评估了走外网播放视频方案的可行性:校区教学网络外接两条电信线路上网,一条为30M专线网络,一条为200M拨号光纤。互为备份,避免单线故障。我们在研发中心模拟校区的实际网络带宽,使用10台PC 通过有线网卡,同时播放视频,通过监控防火墙出口流量峰值、查看cacti 流量实时状况,并实际在PC 体验视频播放的流畅度<p><img src="/img/remote/1460000023812018" alt="" title=""></p></li></ul><p>测试结果:流量下行峰值为173.8M,平均值为50M。 流量上行峰值为5M. PC播放视频的实际体验效果不错,流畅度良好. </p><p>就这样这套方案平稳地帮我们过滤到下一个阶段。</p><p>下面给出早期版本的简化架构图:</p><p><img src="/img/remote/1460000023812020" alt="" title=""> </p><p>相应的简化流程:课件 => 断点续传 => 在线预览播放 => 审核通过,触发上传任务 => 异步上传到oss => 主播端播放课件视频</p><p>落地效果:基本满足早期业务需求</p><p><strong>四、资源分发</strong></p><p>痛点:各校区当地外网环境无法100%保证全天候稳定,主播端在线播放课件视频出现卡顿、加载中现象。极端情况下影响正常授课。</p><p>解法:课件资源如果在授课前已经存储在校区的服务器、开始授课时主播端只需要从内网拉取资源,这样就不依赖于外网环境。源着这个思路开始构思一个异地多校的资源分发系统。</p><p>Q:资源分发系统的云端Server和分校节点Agent如何端到端的实时通信?</p><p>A:复用双师教学系统的长连接网关服务Talgate,各个节点(server, agent)需要先往网关中心Talgate注册,建立长连接数据通道。</p><p>Q:如何保证长连接通信双方消息不丢失?</p><p>A:ACK+ 超时重传 + 去重</p><ul><li>Server推送消息时,携带一个标识 SID,推送出消息后会将当前消息添加到“待 ACK 消息列表”。Agent成功接收消息后,会给Server回一个业务层的 ACK 包,包中携带有本条接收消息的 SID。Server接收后,会从“待 ACK 消息列表”记录中删除此条消息。【ACK】</li><li>如果Agent接收不到消息,Server的“待 ACK 消息列表”会维护一个超时计时器,一定时间内如果没有收到Agent的 ACK 包,会从“待 ACK 消息列表”中重新取出那条消息进行重推。【超时重传】</li><li>如果Agent接收到消息了,but ACK包丢了,导致Server重传消息,可能会让Agent收到重复的消息,这时Agent需要根据SID来进行业务层的去重。 【去重】</li></ul><p> <img src="/img/remote/1460000023812019" alt="" title=""> </p><p>Q:Server或者Agent宕机可能导致重传失效。下一次重新连接上,Agent之前有若干条消息丢失,怎么办?</p><p>A:Server需要进行完整性检查,利用“时间戳比对”机制,发现Agent“有消息丢失”的情况,可以重新同步丢失的数据。</p><ul><li>Server给接收方Agent推送 task1,顺便带上一个最新的时间戳 timestamp1,接收方 Agent 收到 task1 后,更新本地最新消息的时间戳为 timestamp1。</li><li>Server推送第二条消息 task2,带上一个当前最新的时间戳 timestamp2,task2 在推送过程中由于某种原因接收方 Agent 和 Server连接断开,导致 task2 没有成功送达到接收方 Agent。</li><li>Agent 重新连上Server,携带本地最新的时间戳 timestamp1,Server将Agent 暂存的消息中时间戳大于 timestamp1 的所有消息返回给Agent,其中就包括之前没有成功的 task2。</li><li>Agent 收到 task2 后,更新本地最新消息的时间戳为 timestamp2。通过时间戳机制,Agent 可以成功地让丢失的 task2 进行补偿发送。 </li></ul><p> <img src="/img/remote/1460000023812023" alt="" title=""> </p><p>Q: 校区Agent收到任务后,开始从云端OSS下载资源,如何避免重复下载,下载资源失败怎么办?</p><p>A:Beetle是上传/下载服务组件,部署在校区,负责接收Agent的下载指令,执行实际的从云端下载资源文件的工作。</p><ul><li>Beetle先从本地文件列表检查文件是否已经下载或者下载中,若已经下载则返回成功,若正在下载中则返回下载中,否则执行下一步。</li><li>Beetle计算待下载文件大小 检查是否大于 可用容量(磁盘剩余容量-下载中文件大小),若大于的情况,现在方案是返回失败。后续迭代方案是给资源文件按等级、文件大小做加权值,优先下载权值高的文件。</li><li>Beetle对于大文件的下载,按断点续传下载文件,若出现网络不稳定下载失败,根据配置文件的重试次数,执行重试机制。若重试后无效,返回失败。</li><li>Agent收到下载失败结果,更新分发任务状态为“下载失败”,通知Server任务失败。并触发告警到质量监控平台。</li></ul><p> <img src="/img/remote/1460000023812024" alt="" title=""> </p><p>Q: 如果校区存储资源的一台服务器出现故障,如何保证资源正常加载?</p><p>A:每个校区部署两台服务器,使用LVS做主机冗余,避免一台机器宕机,出现单点故障。</p><ul><li>Mercedes Server根据校区节在网关注册的先后顺序选举Master,先给校区Master 服务器A发送分发任务,服务器A完成资源分发后,需同步资源给Slave服务器B。</li><li>使用类Rsync+Sesync的机制实现服务器文件双向同步,双方比对文件列表,若发现缺失则同步资源。</li><li>如果服务器A出现故障无法使用,服务器B被选举为Master,接管资源分发任务,待故障机器A恢复后,服务器B检查是否有新资源要同步给服务器A, 有则同步资源。<p><img src="/img/remote/1460000023812026" alt="" title=""></p></li></ul><p>Q: 如果资源没有及时分发到校区节点,如何保证主播端正常加载资源?</p><p>A:每个校区的网关服务Cadillac提供查询课件资源文件URL,Cadillac会检查资源文件是否发布,如果发布则返回内网URL地址,若未发布则返回外网URL地址。</p><p><img src="/img/remote/1460000023812021" alt="" title=""> </p><p>另外避免一些误操作导致已经发布的资源丢失的情况,而无法提前发现,建立了一套预警机制:Cadillac每晚11点会查询往后7天所有校区课表的课件资源URL列表,通过http head方法批量检查资源是否存在于内网,若不存在则触发告警、流转到质量监控报警平台。</p><p>Q: 如果校区两台服务器都出现硬件故障或者长时间断电的情况下,如何保证主播端正常加载课件资源?</p><p>A:主播端当主动探测到校区内网服务器无法工作后,自动切换到外网访问云端服务,加载外网课件资源。</p><p>下面给出相应的简化架构图:</p><p><img src="/img/remote/1460000023812022" alt="" title=""> </p><p>相应的简化流程:课件 => 断点续传 => 在线预览播放 => 审核通过,触发上传任务 => 异步上传到oss => Mercedes Server创建课件分发任务 => 选取指定校区的Master服务器、给它发送任务 => Mercedes Agent接收到任务、记录任务、给Beetle发送下载任务、Beetle接到任务、记录下载任务、下载课件 => 校区服务器双向同步资源 => 内网不可用时切换到外网.</p><p>落地效果:课件资源不依赖外网环境,主播端正常播放课件视频,提高教学系统的可用性。</p><p>五、多类型资源分发、版本管理</p><p>痛点:现有课件分发策略粒度比较粗,仅支持自动分发到所有校区。 一旦课件内容有问题,必将影响所有校区的正常教学。</p><p>解法:搭建统一发布平台,打通上游业务系统、CI/CD系统和下游资源分发系统。管理资源版本号、发布记录、制定按城市、校区、版本三个维度的发布策略。监控发布流程,出现失败的任务触发告警。</p><p>Q: 蒲公英发布平台除了支持课件视频发布外,也支持包括互动多媒体资源、安装包、Nodejs扩展库DLL、页面静态资源、脚本文件、Docker镜像等多种类型资源的发布。不同类型资源是否有共性,能不能抽象成通用的一种资源,实现统一管理?</p><p>A:多种类型资源都有一些必要的属性:类型、发布源、版本号、文件名称、文件大小、文件存放OSS地址、MD5值、创建时间等,由此可以抽象成:“资源”、“发布任务”、“发布策略”、”发布任务历史“ 四个实体对象组成蒲公英的基础单元。</p><p>Q:其他类型资源的分发方式是否跟课件资源一样:由蒲公英主动推送分发任务给校区节点,再由校区Agent执行下载资源操作?</p><p>A:对于主播端、主教端、电子班牌、手表这类安装包,需要客户端执行下载安装操作。显然无法保证客户端24小时在线,蒲公英有发布任务时,很大概率是客户端不在线或者正在授课使用中,无法实时升级。因此被动接受发布任务的方式不适用于客户端升级。所以蒲公英需要提供接口给客户端检查是否有新版本更新,由客户端定期检查和执行升级。另外考虑到所有客户端都是运行在校区内,为了减少外网环境的依赖,大文件的安装包先分发到校区服务器,再由校区网关Cadillac提供版本更新接口,提高客户端升级成功率。</p><p><img src="/img/remote/1460000023812025" alt="" title=""> </p><p>相应的简化流程:课件 => 断点续传 => 在线预览播放 => 审核通过,触发上传任务 => 异步上传到oss => 创建课件分发任务 => 蒲公英发布 => 选取指定校区的Master服务器、给它发送任务 => Mercedes Agent接收到任务、记录任务、给Beetle发送下载任务、Beetle接到任务、记录下载任务、下载课件 => 校区服务器同步资源 => 内网不可用、切换外网.</p><p>落地效果:蒲公英统一发布平台上线1个月左右来共发布250+个课件、1000+个互动多媒体资源、20+次客户端版本(主播端、主教端、手表)升级。 </p><p><img src="/img/remote/1460000023812027" alt="" title=""> </p><p>六、未来规划</p><p>持续优化各服务组件,优化系统使用交互体验,优化用户角色权限 ,解藕业务侧逻辑,解藕部署环境的依赖,引入多租户的组织结构,开放能力给集团其他伙伴使用。</p><p>七、经验总结</p><p>从技术架构角度来说,励步双师教学系统是一个异地多校的分布式架构,它复杂度来源主要包括:高可用,可扩展性。这里主要介绍了分布式资源分发(蒲公英)平台的架构演进过程,基于业务发展阶段,分别引入资源分发、资源双备、版本管理,灰度发布等功能。<br><strong>招聘信息</strong></p><p>好未来技术团队正在热招前端、算法、后台开发等各个方向高级开发工程师岗位,大家可点击本公众号“技术招聘”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p><p><strong>也许你还想看</strong></p><p><a href="https://link.segmentfault.com/?enc=Ls%2BYWexsVl2I57SOmbxQUg%3D%3D.4gFbD6u3CZ5tq%2FZhVABjxnzC8ZgbzzhDSYw4VQOAlqRzxMLiSNVa3ezqrKT4c614tarx9GtBtSfJKT2VinI8k%2F%2FpL7vyNtYS81HIH3JlXhTU4y1DrAJIUP%2FR501CisaiDsUn1fh510YXLt%2BgHK94BVNIyWqweQ48G%2B00usk2kXaUhk5lSvI6pS%2FdnLQR57tY58Y1zRb0p1d4CXBrJRegJmwv%2BiSMBhl7cY7rOiDHS4O3exStOWtPoiQZz0ZaGBAqRN75Rkb5QCQTzTZbQTyaRIi8kBPxU%2BGRYbdtvXJDkb6%2FO4Re82n2Hv%2BjXN0C5bVm" rel="nofollow">DStack--基于flutter的混合开发框架</a></p><p><a href="https://link.segmentfault.com/?enc=XWzzLxv8kqjT0y7d4E5%2Bkw%3D%3D.8XK0oGXbYMrbpG2v6uRO1vmvcauTo9d%2F0MeOfdPyqwVHDu1%2BFuxEdT7Ndp2SDQYxpzahLadQewylaWFy%2BnkcmiyHeYCjQb6JsV6v%2BngUAb%2FvAdihrZbFqh56LL1r3FUsplMzbEI3k6aIPWZSUUrlSPKL4agljfW%2BCpaAwxY8gHUCt%2BmJ%2FrZ6VVAGJKNBlgmoOrVZEEuCs9wvNpl2P1s3qqBTPA%2FUaoO6POKOjtV4y%2B6mVQuIk1hcr8OaOD9RtwEMWJXWM0afgPP%2BSyREvgkiOFLIzfLquu%2BO5RDC4VKXrzNposEKm4R%2FEG7NJbfhfTf4" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p><p><a href="https://link.segmentfault.com/?enc=2kR7EhNLOdAXOt1KNGs4UA%3D%3D.2x0WiT5M6MiOsoUKkD7C8joHsn9tbqpHbzbFTAijr9OatZyM7zbbArLAvfD2kfzyOtaVe0Vkk2xuxAN%2FHvJz6MOoAaJSDb5iUOehrbsd%2FADzOt%2BTftauM0K2rWw%2FmydVnoru56Nxs5jyHnl4Cgzq8%2BV64RfskLZDMcLayoFLiTtj1OemCpYcq1yRaU0cO9SGhQC2HVrJOxMD5K5Px0iUKJe4nDw%2BqvDOH73ivPvGuiwuQ6No5C9al0ClqhuuLe4wG0KC8PaqC19UrWceYDHCR0Ac0bCPfpvLT1LJCQxS9xGjE5RsQnyI2hb116e%2Bl4d2" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p><p><img src="/img/remote/1460000023812028" alt="" title=""></p>
重磅丨科技教育公司“好未来”正式对外开源高性能PHP框架Fend
https://segmentfault.com/a/1190000023636701
2020-08-14T18:59:08+08:00
2020-08-14T18:59:08+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
14
<p><strong>好未来是一家以智慧教育和开放平台为主体,以素质教育和课外辅导为载体,在全球范围内服务公办教育,助力民办教育,探索未来教育新模式的科技教育公司。</strong></p><p><strong>截至目前,好未来集团已围绕教育场景需求,累计研发包括图像、语音、数据挖掘、自然语言处理等8大类型、100多项AI能力,打造10余项教育场景应用AI解决方案。</strong></p><p><strong>在技术不断提升的道路上,好未来技术线提出坚持“大中台、小前台”的技术战略,统一基础服务设施建设,推进公司技术组件落地,增强企业技术人才内生,不断提升企业的技术实力及技术影响力。</strong></p><p><strong>除此之外,好未来内部坚持开源共享,通过“开放、共享、合力开发”的模式,推动开源文化氛围的形成与技术组织变革,为中台建设提供了另外一种抓手。同时通过开源文化的建设,促进整个教育生态技术共享,提升教育科技实力,更好的为“科技与爱让教育更美好”的愿景奠定基础。</strong></p><p><strong>近期由“好未来”技术团队开源的高性能PHP框架Fend PHP正式上线!该框架单机QPS可达到4000个,好未来内部目前超过30个团队项目在使用该PHP框架!</strong></p><p><img src="/img/remote/1460000023636705" alt="" title=""></p><h2>前言</h2><p>PHP是一款简单方便的语言,而行业开源框架为了<code>后续灵活</code> 而变得过于繁重</p><p>Fend框架是一款很有历史的框架、初代发布后一直在好未来坊间传播使用、衍生出大量分支版本</p><p>这是一款很有意思的框架、普通的框架内隐藏着大型互联网经验的精华、也同时存在大量历史痕迹</p><p>2019年7月 我们对Fend进行整理、封装、推广、目前在好未来内部有大量的用户在使用、维护</p><p>2020年7月 开源、以此共建交流</p><p>我们崇尚 <code>脚踏实地、仰望星空</code> 精神 欢迎小伙伴一起参与开源共建</p><h2>设计方向</h2><p>Fend 框架是一款以企业快速实现业务为主要目标的框架,但与复杂的行业流行框架追求不同:</p><ul><li><code>简单实用</code>:追求快速上手,扩展功能一步到位、大量降低功能的复杂度、框架更注重简单实用实现</li><li><code>单层内核</code>:追求一个函数能实现的功能绝不继承封装,不追求框架自身功能的继承可复用</li><li><code>内聚归类</code>:高度集中归类功能,降低底层复杂度,减少底层组件关注度、更多时间在业务</li><li><code>持续积累</code>:持续积累大型互联网线上运营经验,持续探索企业实用技巧,深度来自于积累而非AOP带来的灵活性</li><li><code>内核设计</code>:高内聚简单内核,放开业务自封装空间,留下更多空间给业务</li><li><code>开源心态</code>:开放公开,接受任何符合价值观源码奉献、但有严格代码审核</li></ul><h2>功能简介</h2><ul><li>Swoole/FPM 双引擎平滑切换(协程版本还在整理稍晚放出)</li><li>统一使用 Composer Autoload PSR4</li><li>请求Debug 模式,请求网址wxdebug=1可查看debug模式查看异常分析性能</li><li>协程模式下对变量域做了更好的封装,降低协程使用难度</li><li>支持压测使用灰度影子库</li><li>高速map映射路由 + FastRouter正则路由</li><li>符合大数据挖掘设计的Trace日志,方便ELK分析、ClickHouse、HBase、实时预警</li><li>throw new Exception方式处理业务异常、能够快速发现异常</li></ul><h2>性能压测</h2><p>目前是在KVM虚拟机上压测、后续会找一台阿里云进行压测</p><h3>FPM性能</h3><p>服务器配置</p><ul><li>CPU 4 核 Xeon 2.2</li><li>内存 12G</li><li>KVM + CentOS 7.6</li><li>FPM 开启进程数 500</li></ul><p><img src="/img/remote/1460000023636704" alt="" title=""></p><p>QPS 5331 (分析:fpm空跑hello 1w、引入composer autoload 后 7000、开启日志trace 6000、框架内echo 5000)</p><h3>Swoole 1.10.x 性能</h3><p>服务器配置</p><ul><li>CPU 4 核 Xeon 2.2</li><li>内存 12G</li><li>KVM + CentOS 7.6</li><li>FPM 开启进程数 500</li></ul><p><img src="/img/remote/1460000023636706" alt="" title=""></p><p>QPS 24000、协程版本稍晚放出</p><h3>发行版本介绍</h3><p>Fend有两个版本</p><ul><li>Tag版本为 1.2.x FPM/Swoole 1.10.x 平滑切换版本</li><li>Tag版本为 1.3.x FPM/Swoole 4.5.x Coroutine 协程 平滑切换版本 <code>此版本还在调整</code></li></ul><h2>以下为1.2.x版本安装</h2><h4>FPM Engine Start</h4><p>master is 1.2.x version</p><pre><code>composer create-project fend/fend-skeleton:~1.2.0 project_name
复制代码</code></pre><p>Ref <a href="https://link.segmentfault.com/?enc=uYsfBaSCY6sMLkb7UAlyzg%3D%3D.9td%2B%2F7rvryRtuzyJTrhAeBYz8K2znMD5DwYmIAJ%2FaL5RIr9nTApjqCOivvzA78EXwWm5MxfGpWFf%2B247uzkHrQ%3D%3D" rel="nofollow">nginx.conf</a> to configure Nginx and <a href="https://link.segmentfault.com/?enc=4A52lM0VmYKKv%2BxrgnIvHQ%3D%3D.WIZcf0D4z8eXhqiE6%2FUKLD5UVrn5vauHrNwwWWDkLfE%3D" rel="nofollow">http://127.0.0.1/</a> on browser</p><h4>Swoole Engine Start</h4><pre><code>composer create-project fend/fend-skeleton:~1.2.0 project_name
# swoole start ( /bin/fend depend on composer require symfony/console )
php /bin/fend Swoole -c app/Config/Swoole.php start
php /bin/start.php -c app/Config/Swoole.php start
复制代码</code></pre><p>browser <a href="https://link.segmentfault.com/?enc=38TQg4ugf%2FF8s5Fhk80CNQ%3D%3D.YIQKDGqv%2FkUxFGrdTStQ%2FAYolzqkCLte3Prra2TzQxw%3D" rel="nofollow">http://127.0.0.1:9572/</a></p><h2>1.3.0协程版本 安装</h2><pre><code>composer create-project fend/fend-skeleton:~1.3.0
复制代码</code></pre><h2>软件作者贡献列表</h2><p><img src="/img/bVbLk9V" alt="image" title="image"></p><p>(其他贡献者、请详见文档鸣谢)</p><h2>合作伙伴</h2><p>好未来教育集团90%在线业务在使用本框架</p><ul><li><img src="/img/remote/1460000023636708" alt="" title=""></li><li><img src="/img/remote/1460000023636707" alt="" title=""></li><li><img src="/img/remote/1460000023636710" alt="xiaohouai.png" title="xiaohouai.png"><p>xiaohouai.png</p></li><li><img src="/img/remote/1460000023636709" alt="" title=""></li></ul><h2>共建规则</h2><p>欢迎挑战组件功能、允许同类功能同时发布竞争、以 性能好 + 实用及实现简单 + 功能实用 评判</p><h2>联系我们</h2><p>issue: <a href="https://link.segmentfault.com/?enc=Q6TK12dDOgon3V5qa8aGzg%3D%3D.z0QK7J5%2FB2rmkH7qkc1GnBHJF8977kpvkw7HNCTUJ0FC1x29vWaIC3atPNwl4ocf" rel="nofollow">github.com/tal-tech/fe…</a></p><p>加群请加微信:</p><p><img src="/img/remote/1460000023636711" alt="contactus.png" title="contactus.png"></p><p><strong>也许你还想看</strong></p><p><a href="https://link.segmentfault.com/?enc=f6cngWjXACkMOiGhxAkb%2Fw%3D%3D.wR0kMMyv%2BJGk5Ra%2FcsHbkEa3OlO%2BHk4qNDvRXAAofzk7EaIvhVPdUUZqBslZNBgn%2FISzt8C8XPKL5r2tBr0FBW5%2BkKPckhUlPryFrad9BTaM1dyuuOP1p63CjnxhK0lWjwEQg3ChpkPFp8afVODd4wFcZBguDRRqFigZB6ijfZ3Rwpx8rJP0vUhXoeGnLuWNqjLUmp1dps%2Bokar%2BnuN1w7mX%2B5%2BxD9F%2FDgARBVabDFvoPF7PtcWe2vCHlPH7TJKlbDpN0jIoA3T5HguKRQP1JykbmNVRwIEgwfJoOuIipuUGTJzHUrpdwtqMdbL6BxxF" rel="nofollow">DStack--基于flutter的混合开发框架</a></p><p><a href="https://link.segmentfault.com/?enc=utyNygHlcoGNv6QTZfd7SQ%3D%3D.agutVK1lckv2IDN2%2B8hPWHqW1w4NfevE3lpoIIu4PTm0yrMQQuvf8Mea%2Bmo76o5vjFz3uypJ6m3xVUv7nQ5iPQndgN6GLi41WRl1I9Q76OAG4Kzy9%2BzwFC0BHJ4F6OEeJy%2BIK%2FavXO90vUBad7Z8RkjTAGUIdqFGY5LGqdMu50nCrmQOG1nNMZyoS8HkDOWKis5IIMTP8UnDIArseAYq0oOcI%2BSDYt4T7cAYvr5FX4B6V5vMyPmASpWH1%2BgDwAfgxB27CI%2BoMyFaWDXt7qvZdR7n3bvnIlWKDggg1vfK5hu744sL6uCaD%2BNuxcNAeYPZ" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p><p><a href="https://link.segmentfault.com/?enc=%2F90LLPm89emxfxbhdJyd7g%3D%3D.kNGqMtjFuvJJIywogFLB2wBZUiGNpFjcaFZwoZjW00nmKMc9cmhd2fou2MSIHYa4qgPSIg02TGy3vArMsojGYetow7JlP2wI2P7QbfplONaqdaOUfxZTCCnMc7VJXSa%2B728A6KBmjck964GCh%2FwhTm%2B%2F5Zw79vGLIIuR8xB1KgS03JOvR1GJDJl6bo7kNeJKYMn6RGwmdPf3TxEEDnWnUTFGh43qPzRAqwgJYxuIrIXvE0YA4I4qT89pnmsdJWgu%2FW05H5uV2MyeyPkQnIcTQbadZlqTs8rnZ1hbEeJBywRO8oRJpnwmPHgLo1gTbuRM" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p><p><img src="/img/remote/1460000023636712" alt="" title=""></p>
【脑电硬件】教育相关的商业可穿戴脑电设备概览
https://segmentfault.com/a/1190000023540951
2020-08-07T16:36:34+08:00
2020-08-07T16:36:34+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p>脑电是一门古老的脑成像技术,但迄今为止也依然是最具生命力的主流脑成像技术之一。这主要得益于相比其他脑成像技术,脑电设备具有便携性好,使用成本低,无创,高时间分辨率,和丰富的频率信息等优势。对于在教育领域的实际应用,<strong>当前正处于蓬勃发展中的可穿戴式脑电设备</strong>,是最具潜力进入真实教育场景的脑成像技术门类,因为它在便携性,成本,使用的简易性方面拥有更显著的优势。</p><p>本文的主要目的是针对现有不同类型的商业可穿戴式脑电设备及其应用做一个简单分析和介绍。<strong>根据不同的脑电设备在认知神经科学和教育交叉领域的普及程度和潜在适用性,我们对比了以下6种可穿戴脑电设备。</strong></p><ul><li>MUSE-2</li><li>EMOTIV-EPOC X</li><li>Wearable Sensing-DSI 24</li><li>OpenBCI-Cyton</li><li>CGX-Quick 30</li><li>mBrainTrain-SMARTING mobi</li></ul><h3><strong>区分脑电设备的基本要素通常包含以下四点:</strong></h3><p><strong>1.电极数:</strong>电极即小的金属传感器,可以探测到大脑的信号,一般来说,电极数越多,检测到的脑电波效果越好,可以执行的脑电分析也越为广泛深入。</p><p><strong>2.湿电极或干电极:</strong>湿电极需要在实验前在每个电极上涂上导电凝胶或盐水,而干电极则不需要,干电极具有更舒适的体验感。</p><p><strong>3.有线或无线:</strong>本文中提到的所有设备都是无线设备,也可以称为‘移动的’设备,有线脑电设备主要用于前沿研究和医学领域。设备的移动性和便携性对实际应用非常重要。</p><p><strong>4.佩戴的舒适/可接受程度:</strong>脑电设备佩戴的舒适性会直接影响被试者的实验状态,佩戴较舒适的脑电设备会带来更好的实验效果,才有机会走入实际应用。</p><p>除了以上基本因素,脑电设备的另一个指标是<strong>采样率</strong>,采样率即每秒可采集到的数据点,较高的采样率支持较高的时间分辨率。此外,电池可连续使用时间也是一个重要因素,该因素会限制实验可进行的时间,对实验的完整性具有重要意义。</p><p>表1对本文要介绍的6种脑电设备主要特点进行了汇总。</p><p> <img src="/img/remote/1460000023540955" alt="" title=""></p><p>表1. 几种可穿戴式脑电设备主要技术指标</p><h2>一、MUSE-2</h2><p>MUSE-2是加拿大多伦多的一家科技公司InteraXon开发的一款可穿戴脑电设备,用户佩戴此设备,并使用在移动设备中安装的特定软件便可完成脑电波的检测活动。据InteraXon公司介绍,<strong>当用户戴上这款设备时,可以非常直观地看到自己的脑部活动变化情况</strong>,进而可以更加容易地对自身进行情绪训练。</p><p>MUSE-2专注于服务用户,主要针对冥想和睡眠分析。它能够与指定应用程序进行无线实时连接,从而进行神经反馈训练,可连续使用5小时,采样率为256Hz。MUSE-2的主要优势在于可用性(支持蓝牙连接,并且可与内部应用程序轻松交互)、舒适性(轻薄、干电极)以及具备可检测心跳、运动和呼吸的传感器(PPG、加速度计和陀螺仪)。MUSE-2的主要缺点是电极数比较少,只有4个电极,空间分辨率差,通过利用如此少的电极检测到的脑电波来确定大脑活动的位置很困难,对于较复杂的脑认知活动缺乏刻画的能力(相对于电极数目较多的脑电设备)。</p><p>MUSE-2的使用范围已经覆盖从健康到娱乐的各种场合,拥有近200篇与其相关的出版物。由于MUSE-2通常被称为是“冥想脑电设备”,下面介绍一个对尼泊尔僧侣进行研究的例子[1]。</p><p>来自维多利亚大学和英属哥伦比亚大学的研究人员前往位于珠峰基地的一座修道院,对僧侣在冥想和进行其他活动时的脑电波进行测量和研究。研究结果显示,僧侣的大脑在冥想时仍然非常活跃,与休息时相比,他们的大脑在冥想时会更容易放松、集中和同步。研究人员还发现,僧侣在冥想之后玩电子游戏,他们的脑神经元对视觉的感知会更加敏感。</p><p> <img src="/img/remote/1460000023540954" alt="" title=""> </p><p>图1. MUSE-2脑电设备示意图;僧侣头戴MUSE-2进行冥想活动</p><h2>二、EMOTIV-EPOC X</h2><p>EMOTIV-EPOC X(以下简称EPOC X)无线便携脑电系统是美国加州旧金山的神经科技公司开发的一个全新的人机界面控制系统,该设备利用一部能够测量脑电活动的装置,来实时探测和处理脑电波模式。EPOC X为EPOC+的提升版,最大的改进在于添加了旋转头带,减少补液时间。</p><p>与MUSE-2类似,EPOC X也是一种用户友好型头戴式脑电设备,不同的是EPOC X有14个电极,可连续使用时间为6小时并且采样率高达2048Hz。EPOC X的主要优势为它是一款<strong>用户友好型的轻量级头戴设备,佩戴很舒适,并支持无线蓝牙连接</strong>。主要缺点是用户在使用前需要在传感器上添加特定溶剂,属于湿电极,所以虽然结构上看似简易,但实际使用并不方便。此外,若需要从脑电信号中获取原始数据,用户需要付费订阅。这给科研性质的用户会带来很大的不便。</p><p> <img src="/img/remote/1460000023540956" alt="" title=""> </p><p>图2. EPOC X脑电设备示意图</p><p>由于EPOC X的消费者不是来自非科学界,就是来自超出心理学和医疗保健等传统脑电应用领域之外的研究领域,所以EPOC X的应用场景非常多样。</p><p>以下是一些例子:</p><p>通过脑机接口(BCI)控制的微型汽车[2],见图3;</p><p>由人脑控制的老鼠“电子人”(例如在老鼠的大脑中植入电极)[3]</p><p>情绪共鸣。例如播放的音乐类型与脑电图检测到的情绪一致[4];</p><p>研究人在城市中行走时对不同地点之间的距离的感知[5]。</p><p> <img src="/img/remote/1460000023540958" alt="" title=""> </p><p>图3. 使用EMOTIV-EPOC X通过BCI控制微型汽车</p><h2>三、Wearable Sensing-DSI 24</h2><p>DSI-24是美国Wearable Sensing公司研发的一款干电极头戴式脑电设备,电极里面加了弹簧,可以更好的贴合头皮,减少电极对头皮的硬性压力。设计非常便捷,可以在5分钟内记录用户的脑电图,适合在办公室或实验室环境中轻便移动。</p><p>DSI-24包含24个电极,采样率为300Hz,通过蓝牙进行无线连接,非常容易构建实验环境。并且在实验结束后,脑电信号的原始数据便可以立即下载,而且可以通过提供的软件进行持续监测。DSI-24的一个突出优点是它包含2个电池盒,也就是在实验过程中可以打开其中一个电池盒更换备用电池,该操作不会中断信号采集,由此便可认为DSI-24可以不限时长的使用。</p><p> <img src="/img/remote/1460000023540957" alt="" title=""> </p><p>图4. DSI-24脑电设备示意图;用户头戴DSI-24脑电设备</p><p>尽管DSI-24电极数较多,但佩戴依然有较好的舒适性,设备大小可调节,能够适应各种头部尺寸,实际的测试表明,对于大部分普通被试,连续佩戴一小时以上的可接受程度高。此外,该设备的一个最大的优点是电极属于干电极,使用和维护上相当简易。<strong>需要注意的是,对于处理通过DSI-24采集的脑电数据,使用者必须具备相关的脑电技术知识才能胜任。</strong></p><p>在2017年,英属哥伦比亚大学的研究团队发表了一项研究,他们在教育背景中使用DSI-24,通过检测学生的脑电信号来评估学生在生物课上回答问题时的认知负荷[6]。</p><h2>四、OpenBCI-Cyton</h2><p>Cyton是OpenBCI公司设计的一款干电极头戴式脑电设备。OpenBCI是一家开发低成本开源EEG的公司,<strong>目标是帮助对脑科学和脑机接口感兴趣的业余爱好者,以低成本的方式获取脑电信号和数据并进行研究</strong>,OpenBCI的产品专注脑机接口来驱动机器和绘制大脑活动。</p><p>Cyton设备有16个电极,通过蓝牙进行无线连接,可连续运行24个小时,采样率为250Hz。OpenBCI产品的最大优势是DIY特性,用户可以灵活地改变电极的数量、连接其它类型的传感器或访问其它的开源工具。Cyton的局限性与DSI-24类似,用户需要具备一些编码和神经科学方面的专业知识才能够获得更好的使用效果。</p><p>近期,巴西的UTFPR大学在一项研究中使用了OpenBCI的Cyton系统来评估学生通过智能辅导系统(ITS)学习时的情绪,学生情绪的变化会反馈给ITS, ITS可以选择根据学生情绪的变化相应地调整教学内容,以期提高教学质量[7]。在2019年,埃及的一所大学使用OpenBCI脑电设备与VR结合帮助中风患者恢复运动能力,该项目将患者的大脑信号输入到游戏中,通过VR显示,游戏内容会根据患者的大脑信号相应地进行调整,以优化患者的康复运动[8]。天津大学的一个研究团队正在使用OpenBCI脑电设备将BCI连接到机器人可书写的手臂,当受试者看到汉字时,来自受试者大脑的信号就会指示机器人写出所看到的汉字[9]。</p><p> <img src="/img/remote/1460000023540961" alt="" title=""> 图5. 头戴式OpenBCI脑电设备;通过BCI控制机器人书写汉字</p><h2>五、CGX-Quick 30</h2><p>Quick-30是美国CGX公司生产的一款头戴式干电极脑电设备,与Quick-20r相比,Quick-30增加了电极数、增大了采样率并且延长了待机时间。Quick-30结合了大量的机械和结构改进,加快了安装速度,显著提高了耐用性和磨损时间。</p><p>Quick-30脑电设备包含30个电极,通过蓝牙进行无线连接,能够连续运行16个小时,采样率可达到1000Hz。与本文提到的其它设备相比,<strong>Quick-30的主要优势是电极数最多,并且具有较高的采样率,可以更好地识别受试者大脑的位置和信号产生的时间,即可以得到更高的空间和时间分辨率。</strong>Quick-30的短板是舒适度,它的重量是一些用户友好型设备的两倍以上,如MUSE-2和EPOC X。</p><p>CGX公司的产品应用范围广泛,在全球已有数百家研究机构使用他们的脑电设备。密苏里大学的研究人员使用CGX设备来评估受试者在玩电子游戏时的情绪状态,他们试图区分三种不同的情绪状态:交流、无聊和焦虑,该研究可以为游戏设计者提供反馈,提升游戏的体验感[10]。广州科技大学的研究人员使用CGX设备来评估车辆驾驶员的睡意,通过对受试者的脑电数据进行收集和分析,观察受试者的警觉性水平,评估他们是否感觉疲惫并伴有睡意,这项研究可能有助于降低交通事故的风险概率[11]。</p><p> <img src="/img/remote/1460000023540960" alt="" title=""> 图6. Quick-30脑电设备示意图 ;驾驶员头戴Quick-30脑电设备</p><h2>六、mBrainTrain-SMARTING mobi</h2><p>SMARTING mobi是mBrainTrain公司推出的一款小而轻的头戴式湿电极脑电设备,可以随时记录脑电图,与配套的移动应用程序结合使用可以完成高质量的数据记录,能够在实验室和日常生活中实时监测大脑活动。</p><p>SMARTING mobi脑电设备包含24个电极,通过蓝牙进行无线连接,可以连续运行5个小时,采样频率为500Hz。<strong>SMARTING mobi的主要优势是轻量级,重量不到60克,对于包含24个电极的脑电设备来说是非常轻便的</strong>。SMARTING mobi的缺点是用户在使用前需要在传感器上添加特定溶剂,属于湿电极。与上述设备不同的是SMARTING mobi是一个覆盖整个头皮和耳朵的帽子,并不是独立分散的可调节电极的设备。</p><p> <img src="/img/remote/1460000023540963" alt="" title=""> </p><p>图7. SMARTING mobi脑电设备示意图</p><p>比利时著名大学鲁汶大学使用SMARTING mobi研究受试者居家进行的自然活动,实验对象需要轻松完成各种日常活动,包括看书、看视频、玩数独游戏等,研究人员能够根据受试者的脑电信号检测出敬业度、警觉性和精神负荷指标,该实验的目的是收集在自然环境和实验室环境下的对比数据[12]。维也纳大学和东京大学进行了一项跨文化研究,通过佩戴SMARTING mobi设备观察母子之间的互动,研究结果显示,当奥地利母亲看到一个环境中的物体时,她们更关注物体,而日本母亲则更关注环境[13]。</p><p> <img src="/img/remote/1460000023540962" alt="" title=""> </p><p>图8. 头戴SMARTING mobi进行居家自然活动实验</p><h2>小结:</h2><p>在本篇文章中,我们结合认知神经科学和教育领域,简单介绍了六种不同类型的商业可穿戴移动脑电设备,包括MUSE-2,EMOTIV-EPOC X,Wearable Sensing-DSI 24,OpenBCI-Cyton,CGX-Quick 30和mBrainTrain-SMARTING mobi,并对这六种脑电设备在实际中的应用进行举例介绍。这些设备各具特点,优势和不足,<strong>好的脑电设备不仅能检测到高质量的脑电信号,并且具备较好的佩戴舒适性和实验体验感</strong>。不同的脑电设备适用场景不同,对于在教育领域的实际应用,可穿戴式脑电设备是最具潜力进入真实教育场景的脑成像技术门类,<strong>基于可穿戴脑电设备的教育研究发展,也非常值得我们共同期待。</strong></p><h2>参考文献</h2><ol><li><a href="https://link.segmentfault.com/?enc=oEzKjkbrhLv1hassHK%2FkDg%3D%3D.vU9K6c4jEYvbL1cj2uayEd0dvpyEdDkUcRCKwQf1Ul0%2FEeKa5OFpGkG5A%2FZzJHODR2hYcHpdhs90Efx7i3nm1co%2F59JIbI8slL%2BnOtScbkcrwYv4y75sl3ksssPDlnofwYpyBoLs5TxME5WqrhcYJg%3D%3D" rel="nofollow">https://www.ctvnews.ca/health/neurologist-treks-to-everest-to-study-monks-in-meditation-1.3015389</a></li><li><a href="https://link.segmentfault.com/?enc=JlBAzY6SoBv1%2BioBTjn1Jw%3D%3D.Bcss%2Bm1wVATlT6A8g0NmQZ6J2kNSX807xmKfbbpXwn0ysE7jvpV8VK8kW%2F7Cwnxory4XW553bRe%2FtbbkFi%2BYpdLtM1BIrpDOLY4J7ucSJIO1zrf9qJKAK3PuFDOxfMp9" rel="nofollow">https://www.emotiv.com/blog/developer-project-eeg-controlled-rover-a-brain-computer-interface/</a></li><li><a href="https://link.segmentfault.com/?enc=5xTP9T1WtJLKytWAPmV08g%3D%3D.5%2BjMJESUDk1Mt19EmwI6yKUT9Vva4D4TCctzNaEJ%2Byep0FkShzLjclU0Mpt8a71qO5Gv2aTzKxvgQP4bAGXHqtKbriGeTP%2Bgu7HinvgF9xH1ygObIT3Idh0hmBg0%2Ba5r6ImULm3l%2BZMB%2FRTa%2B%2FQy%2BD2ZY3lt1rFm9wSwsK7nR1no8Vts4j2ADVgGeNuykobJ" rel="nofollow">https://www.emotiv.com/independent-studies/human-mind-control-of-rat-cyborgs-continuous-locomotion-with-wireless-brain-to-brain-interface</a></li><li><a href="https://link.segmentfault.com/?enc=U%2Fxab2dvHdGs0zu6mn%2BbFQ%3D%3D.HfqGR9D%2BiSBFGdv%2FtA3Ryb%2FBdbCch61t2i7Sfrx1SmURNwNhIj49bJwWMiz2JiIog3ZXpbV%2Ba%2BZuBnhulvJuttyn0%2BmYL8rJma3lMHZbWk%2FygbsL2afPouJw1Hrf5vzK" rel="nofollow">https://www.emotiv.com/independent-studies/music-emotion-capture-sonifying-emotions-in-eeg-data</a></li><li><a href="https://link.segmentfault.com/?enc=K9EWSk4tVayl877bZ%2FEq6Q%3D%3D.B4g4H50pR4XJTxP725Qw5Htbz%2BMjFRmZsWhAuEXUc%2FnA5D8RCYfbwpKJ4Ukxacj9HyOvpB9clZUOhZnRcNB%2F4M2l3QS6izn6kM0M4fncI%2Bndb6B%2FMf57iA%2FLIppQJdBm574zLXkNVHvu5jJwbDoF0A%3D%3D" rel="nofollow">https://www.emotiv.com/independent-studies/exploring-distance-perception-in-urban-environments-with-mobile-eeg</a></li><li>Mills, Caitlin & Fridman, Igor & Soussou, Walid & Waghray, Disha & Olney, Andrew & D'Mello, Sidney. (2017). Put your thinking cap on: detecting cognitive load using EEG during learning. 80-89. 10.1145/3027385.3027431.</li><li><a href="https://link.segmentfault.com/?enc=z80eIyau1%2B4DClQGOUAvZA%3D%3D.DI2F14SFzR%2BFwjSEWnOHy6AXJdLPN0khSSWkr3w18dejAJN4uQ4AMq%2FvAUi6Y1f1kY9rIwfh%2FAg%2BYmxcQr0Xbg%3D%3D" rel="nofollow">https://link.springer.com/chapter/10.1007/978-3-030-49663-0_8</a></li><li><a href="https://link.segmentfault.com/?enc=EptbcpdKJimKiI8jNOcWMA%3D%3D.AEA9WBrIHZxF4Egm%2FbYaxqJmEOl4hbxyemjj%2ByuPG7SBvlp56QXwY3gLzWTV0eeQzRKa3wIb%2BtII4GqtvCZfrA%3D%3D" rel="nofollow">https://ieeexplore.ieee.org/abstract/document/9021752</a></li><li><a href="https://link.segmentfault.com/?enc=2X5DTx4gROVTkRUbbBwchA%3D%3D.neqEChd7dGZ0M9yTZEok9bkEWYjI2haJ4hbXuYYCEWSgaIXJPQuRZ1woYH%2F5wG%2Bj" rel="nofollow">https://ieeexplore.ieee.org/document/9071613</a></li><li><a href="https://link.segmentfault.com/?enc=BdrpAe9oD5J7WOMfolCJ9g%3D%3D.sfuxqXqil%2BORjcKQ9XaSZ2S1FvpCgNPT%2BK8eg6vAOBGvuyOMWyOyZSLMgJG4DRhP3nkUpf1f4%2Bdz54d6rUhV07b6M7QlqB9U5CnGIeVZgP4%3D" rel="nofollow">https://aisel.aisnet.org/cgi/viewcontent.cgi?article=1002&context=mwais2017</a></li></ol><p>11.<a href="https://link.segmentfault.com/?enc=LnqZS1wdocDVbOTZkt2SqA%3D%3D.Ohi%2F6AXAZWUHbyuI2NWkcp0Gp8x7PVzgaXNdjzaSmr53U0bA6H7GqDEaHYrwYwLnLt608IQ%2FO9oZ41Sf5v%2F8Gg%3D%3D" rel="nofollow">ieeexplore.ieee.org/abstract/do…</a></p><p>12.Zink, Rob & Vos, M. (2018). An afternoon of natural activities at home through the eyes of mobile EEG. 10.13140/RG.2.2.27862.40007.</p><p>13.<a href="https://link.segmentfault.com/?enc=YMLWOSuMnY5aQG6EYb6ruA%3D%3D.5OAQrwmOJ56S2V9iLBoNFG7N0DnDuzRCWiZBz40q3GM%3D" rel="nofollow">osf.io/g7meu/downl…</a></p><p><strong>招聘信息</strong></p><p>好未来技术团队正在热招前端、算法、后台开发等各个方向高级开发工程师岗位,大家可微信搜索<strong>“好未来技术</strong>”或者扫描下方二维码,点击本公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p><p><strong>也许你还想看</strong></p><p><a href="https://link.segmentfault.com/?enc=U6U8%2F2QR2HlYK%2F2ZlA7D4A%3D%3D.H39b2vPXpx%2F1ac%2ByEves64s3Oi0iEVTl5R1LhlU4E5gr1oaRxWCKq8x9vaAxu03UzO%2FAUm9YMDbfmNMhMbxDCYkFi0UpfWIHih3dw4StlyvHdtaKUvB1ge%2BmXlprdB8216j9eT%2B1eOivbGNMF5sWUGdJRY0fgaG1DoosHqpreqKSnYqeVt9vH1LgB3dcrQ8UxcSViB0sw4KUbWy0nyJu5U3z0azPJejse6Y1RxrJwl9Y6UtfeaizyEVDdoFAfDpEkNnF%2FZeCMMrE1z1jpLhdmrmaslWRXAgQ80m87CjuhxXgtWtW69CY3CWqbhSmptmy" rel="nofollow">DStack--基于flutter的混合开发框架</a></p><p><a href="https://link.segmentfault.com/?enc=gk9gJXlnpouPp5NcCmhipw%3D%3D.1Z60NtzzjkqNSAI8gxut1%2Bdf7XLb53fW7hVuAXZSLlgKrFtecuzt2KLQkA%2Fqi%2BhTtrvYXxxghj8VeQ5kK354hV8rDA%2Bo0dmdHsaabTd25glYjjcCQbHGMBWQi7HTYTlqypgQ2%2BbgYFdPoHP7FhAMRHNT%2BEChZCsvubP53TzXDirrYeDhCjkxAMpX6MbbZ%2BS1wj5nsjPR9c%2BkUaRlNM593bc56%2FHLeUnXy8%2BGe%2F553zTDO6O7DtMwcmP%2BgJTKSBltYtuiwKqeMyknUP3Kolrm6%2FX5Ufo63t3UsfnR903tMx5RxwZTMA6HxGSF8%2B4aB3Et" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p><p><a href="https://link.segmentfault.com/?enc=5P%2FefDSdtqUpZPjM%2BA9HnQ%3D%3D.NcsOMHGZ739xzNOqOaPXPM%2BbU2yf1gqIPAo1RcK8RcDbSWI83CdYI2JG2tBaAjoFD3SRDbibIlaXzJzUQtDT4yU%2FtB%2B0SEbADqSrJpFvWQ8abcReR6Pc6UaO7jCPZ8D%2BZokxT3DDBMMF9kaIXH6Wamd6u5vhOjruA%2FSV9%2FcWjPk2DYAsNowIz69BeLoeqt5BR%2B0giz1pO2z8eVAs0a7owGyCIQAjSfY5M31M6R1GCSXNaLpnleSuzypEhYXKmHZxWn71kdxNLh0rNWMKrdSQ2ZItrFCUXJP7Fmn2hFjk7T2FzeDNhGbPmaeEULvs7BHU" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p><p><img src="/img/remote/1460000023540964" alt="" title=""></p>
基于Nodejs打造Web架构中间层
https://segmentfault.com/a/1190000023456210
2020-07-31T20:38:54+08:00
2020-07-31T20:38:54+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
4
<h2>前言</h2><p>Node.js自2009年诞生以来,发展速度相当惊人,目前各种开发框架层出不穷,国内外各大公司都在使用,如国内的阿里的淘宝、天猫、阿里云、蚂蚁金服,腾讯视频、携程、百度、网易、苏宁、京东、爱奇艺、去哪儿、有赞、贝壳找房等等好多企业都在使用,大部分企业把Node.js作为中间层去应用,今天和大家简单说说关于基于Nodejs打造Web架构中间层的一些知识。</p><h2>一、中间层与中间件</h2><h3>1、什么是中间层</h3><p>中间层(Middle Tier)也称作应用程序服务器层或应用服务层,是用户接口或 Web 客户端与数据库之间的逻辑层。典型情况下 Web 服务器位于该层,业务对象在此实例化。中间层是生成并操作接收信息的业务规则和函数的集合。它们通过业务规则(可以频繁更改)完成该任务,并由此被封装到在物理上与应用程序程序逻辑本身相独立的组件中。</p><h4>1.1 Node作为中间层模式</h4><p>以Node作为中间层,当客户端打开一个网站时,先请求到node服务器这一层,通过node服务器转发请求到后端的服务器,获取数据,然后返给node的模板引擎,根据视图模板渲染好模板字符串页面,再返回给客户端,直接展示页面,如图:</p><p><img src="/img/remote/1460000023456214" alt="18163329db355a3d7e5abdf3.webp.jpg" title="18163329db355a3d7e5abdf3.webp.jpg"></p><h4>1.2 负载均衡器-Nginx</h4><p>Nginx是一个高性能的WEB服务器和反向代理服务器,最常用的软件负载均衡器。</p><p>当访问量比较大时,频繁的请求,会给服务带来很大压力,通过负载均衡、分流,减轻服务器的压力;另一方面,网站部署在多台服务器,当某台服务器故障的时候,可以马上切换到其它服务器,还能保证网站能正常访问,这就是负载均衡的优势。</p><p><img src="/img/remote/1460000023456213" alt="181633291fcbdf31f8f23fe2.webp.jpg" title="181633291fcbdf31f8f23fe2.webp.jpg"></p><h3>2、什么是中间件</h3><h4>2.1 中间件概念</h4><p>中间件(MiddleWare)是一种独立的系统软件服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源,中间件位于客户机服务器的操作系统之上,管理计算资源和网络通信。从这个意义上可以用一个等式来表示中间件:中间件=平台+通信,这也就限定了只有用于分布式系统中才能叫中间件,同时也把它与支撑软件和实用软件区分开来。</p><p>在NodeJS中,中间件主要是指封装所有Http请求细节处理的方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。中间件可以理解为一个对用户请求进行过滤和预处理的东西,它一般不会直接对客户端进行相应,而是将处理之后的结果传递下去。简单来说就是实现某种功能的函数。</p><p>Express是一个自身功能极简,完全是路由和中间件构成一个web开发框架:从本质上来说,一个Express应用就是在调用各种中间件,中间件机制如图所示: <br><img src="/img/remote/1460000023456215" alt="中间件.png" title="中间件.png"></p><h4>2.2 中间件机制核心实现</h4><p>中间件是从Http请求发起到响应结束过程中的处理方法,通常需要对请求和响应进行处理,因此一个基本的中间件的形式如下:</p><p>`const middleware = (req, res, next) => {<br> // TODO<br> next()<br>}`</p><h2>二、中间层的意义</h2><p>Node.js是一个Javascript运行环境。Node.js 使用事件驱动, 非阻塞I/O 模型而得以轻量和高效,非常适合在分布式设备上运行数据密集型的实时应用。Node.js是单进程、单线程运行机制,通过事件轮询(event loop)来实现并发操作,而且性能很好。</p><p>Node.js最大的改良架构就是"增加了中间层",前后端分离,使用Node.js来做‘BBF(backend of frontend)’在传统后端加入了Node.js这一层,通过此有两点好处,前端接管了view层,后端渲染也开始全部由前端掌控,另一个就是接口层增加了一层。在前后端分离的天然选择下,Node.js中间层可以承担更多的责任。</p><h3>1、Node.js中间层可做的工作</h3><ul><li>代理:在开发环境下,我们可以利用代理来,解决最常见的跨域问题;在线上环境下,我们可以利用代理,转发请求到多个服务端。</li><li>缓存:缓存其实是更靠近前端的需求,用户的动作触发数据的更新,Node.js中间层可以直接处理一部分缓存需求。</li><li>限流:Node.js中间层,可以针对接口或者路由做响应的限流。</li><li>日志:相比其他服务端语言,Node.js中间层的日志记录,能更方便快捷的定位问题(是在浏览器端还是服务端)。</li><li>监控:擅长高并发的请求处理,做监控也是合适的选项。</li><li>鉴权:有一个中间层去鉴权,也是一种单一职责的实现。</li><li>路由:前端更需要掌握页面路由的权限和逻辑。</li><li>服务端渲染:Node.js中间层的解决方案更灵活,比如SSR、模板直出、利用一些JS库做预渲染等等。</li></ul><h3>2、Node.js中间层带来的好处</h3><ul><li>通过PC Web自己的中间层,可以按照业务定制化接口,扩大前端展现的能力和范围;</li><li>中间层接口由使用接口的前端工程师开发,对展现和接口的功能更加熟悉,避免了以前的工作模式中接口方跟各方的需求对接、沟通、联调时间,这样使得项目的推进更加顺利,项目迭代会更快;</li><li>中间层使用NodeJS,开发语言是JavaScript,跟现在前端工程师的工作语言一样,减少了学习成本;</li><li>中间层接口的开发由前端工程师同时负责开发,既节省了人力成本,同时又提高了前端开发人员的技术能力,使得前端工程师向全栈工程师迈进。</li></ul><h3>3、Node.js中间层的优势</h3><ul><li>功能分离,减轻板块负担;</li><li>跨系统、跨终端均可重用页面数据校验、逻辑代码,无需因为新系统、终端的接入而重写校验;</li><li>只在中间件中做一次数据校验,避免了前端做数据校验的同时后端也要做校验的重复,在有效保证数据的有效性的同时降低了团队整体的工作量;</li><li>处理数据逻辑,解放了前端既要做页面渲染又要写复杂的逻辑,使得页面开发人员专注于页面渲染,不仅使得分工更为明确,项目协作效率更高,更重要的是快速响应页面使得页面加载更快,用户体验更好,避免了浏览器长时间显示空白页面的不友好体验,真正的前后端分离。</li></ul><h2>三、中间层的实现</h2><p>前面写了很多理论方面的知识,接下来自己手动来简单实现Node.js基于Koa框架实现的中间层。</p><h3>1、后端提供的接口</h3><p>先了解一下后端提供的一个接口,根据前端页面输入不同账号信息,后端接口会返回不同的值,如图:</p><p><img src="/img/remote/1460000023456217" alt="php.png" title="php.png"> <br>这段PHP代码是根据前端传给不同的用户名和密码状态返回不同的状态码。</p><h3>2、搭建前端页面</h3><p>前端页面用了ejs模板引擎采用服务端渲染方式来进行。前端页面主要有三个代码的文件,app.js,admin.js,admin.ejs。</p><h4>2.1 项目代码结构</h4><p><img src="/img/remote/1460000023456218" alt="WX202003240944192x.png" title="WX202003240944192x.png"></p><h4>2.2 项目代码展示</h4><p>1、是app.js代码</p><p>`const Koa = require('koa');<br>// 路由<br>const Router = require('koa-router');<br>// 模板引擎<br>const ejs = require('koa-ejs');<br>// 数据解析<br>const body = require('koa-bodyparser');<br>// 处理静态文件<br>const static = require("koa-static");<br>const path = require('path');<br>const app = new Koa();<br>ejs(app,{</p><pre><code>root:path.resolve(__dirname,"template"),
layout:false,
viewExt:"ejs",
cache:false,
debug:false</code></pre><p>})<br>const router = new Router();<br>app.use(body());<br>router.get("/",ctx => {</p><pre><code>ctx.body = '主页';</code></pre><p>})<br>router.use("/admin",require("./router/admin"));<br>app.use(static('./static'));<br>app.use(router.routes());<br>app.listen(3000);` </p><p>2、登录页面文件,用ejs模板引擎来处理</p><p>`<!DOCTYPE html><br><html lang="en"><br><head></p><pre><code><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>我是管理页面</title>
<script src="/js/jquery.min.js"></script></code></pre><p></head><br><body></p><pre><code><p>用户:<input type="text"></p>
<p>密码:<input type="password"></p>
<button>提交</button>
<script>
$(function(){
$('button').click(function(){
var username = $(':text').val();
var password = $(':password').val();
$.ajax({
url:'/admin/login',
method:'post',
data:{
username,
password
},
success(data){
console.log(data);
}
})
})
})
</script></code></pre><p></body><br></html>`</p><h3>3、代码逐步实现逻辑</h3><p>1、使用上面的登录页代码,然后admin.js页面代码如下</p><p>`const Router = require('koa-router');<br>const querystring = require('querystring');<br>const router = new Router();<br>router.get('/',async ctx=>{</p><pre><code>await ctx.render('admin/admin')</code></pre><p>})<br>router.post("/login",async ctx => {</p><pre><code>const { username,password } = ctx.request.body;
console.log(username,password);</code></pre><p>})<br>module.exports = router.routes();` </p><p>此时登录页面输入用户名和密码点击提交时会输出结构如下代码:</p><p>``^CedzdeMacBook-Pro-5:0323 edz$ nodemon app.js<br>[nodemon] 2.0.2<br>[nodemon] to restart at any time, enter <code>rs</code><br>[nodemon] watching dir(s): <em>.</em><br>[nodemon] watching extensions: js,mjs,json<br>[nodemon] starting <code>node app.js</code><br>admin 123456 //后端拿到了前端传给的数据`` </p><p>此时传过的数据后端会拿他与数据库作比对,进行处理,而Node.js只充当中介作用,不做数据的处理。</p><p>2、我们接着看,把要的数据传个服务端,使用axios发送数据到服务端,其中里面用到了数据格式的转换:</p><p>`const Router = require('koa-router');<br>const querystring = require('querystring');<br>const router = new Router();<br>router.get('/',async ctx=>{</p><pre><code>await ctx.render('admin/admin')</code></pre><p>})<br>//此处为中间层,起到中介作用,会把数据发给后端接口<br>router.post("/login",async ctx => {</p><pre><code>const { username,password } = ctx.request.body;
//console.log(username,password);
//要数据传给服务端,使用axios发送数据到服务端
const {data} = await axios({
url:'http://localhost/login/check.php',
method:'post',
data:{
username,
password
},
// username=admin&password=123456查询字符串
//数据格式转换,前端是个JSON数据格式,后端拿到的是表单数据
transformRequest:[
data =>{
return querystring.stringify(data)
}
]
})</code></pre><p>})<br>module.exports = router.routes();` </p><p>此时登录页面输入用户名和不同的密码和空密码时点击提交时会输出结构如下代码:</p><p>``^CedzdeMacBook-Pro-5:0323 edz$ nodemon app.js<br>[nodemon] 2.0.2<br>[nodemon] to restart at any time, enter <code>rs</code><br>[nodemon] watching dir(s): <em>.</em><br>[nodemon] watching extensions: js,mjs,json<br>[nodemon] starting <code>node app.js</code><br>{ code:1 }<br>{ code:0 }<br>{ code:3 }`` </p><p>3、这时我们把后端传回的接口数据在进行重新包装一下,添加如下代码:</p><p>`// 重新包装</p><pre><code>if(data.code !== 1){
// return中断条件
return ctx.body = {
code:401,
message:'未经授权'
}
}
// 前端自己定义的提示语 ,后端专注逻辑开发,不用在和前端定义接口
ctx.body = {
code:200,
message:'校验成功'
}`
</code></pre><p>这时页面会返回经过前端包装的提示语,如图所示:</p><p><img src="/img/remote/1460000023456216" alt="结果.png" title="结果.png"></p><p>总的处理路由的admin.js代码文件如下:</p><p>`const Router = require('koa-router');<br>const axios = require('axios');<br>const querystring = require('querystring'); <br>const router = new Router();<br>router.get('/',async ctx=>{</p><pre><code>await ctx.render('admin/admin')</code></pre><p>})<br>//此处为中间层,起到中介作用,会把数据发给后端接口<br>router.post("/login",async ctx => {</p><pre><code>const { username,password } = ctx.request.body;
//console.log(username,password);
//要数据传给服务端,使用axios发送数据到服务端
const {data} = await axios({
url:'http://localhost/login/check.php', //后端提供的一个接口文件
method:'post',
data:{
username,
password
},
// username=admin&password=123456查询字符串
//数据格式转换,前端是个JSON数据格式,后端拿到的是表单数据
transformRequest:[
data =>{
return querystring.stringify(data)
}
]
})
// 重新包装
// console.log(data);
if(data.code !== 1){
// return中断条件
return ctx.body = {
code:401,
message:'未经授权'
}
}
// 前端自己定义的提示语 ,后端专注逻辑开发,不用在和前端定义接口
ctx.body = {
code:200,
message:'校验成功'
}</code></pre><p>})<br>module.exports = router.routes();` </p><p>服务端不要暴露太多给用户信息,不用提示用户名正确或者密码错误,防止被别人猜,前端根据后端提供的状态码重新定义输出提示内容,对用户来说特别的友好,不论后端给前端什么样的接口内容,前端都可以包装接口,所以后端只要返回给前端数据就可以了,接口的定义前端自己可以进行包装。</p><h2>四、总结</h2><p>中间层已经为越来越多的大公司所应用,进入中间层后,前端能做的事情越来越多,将触角伸向了服务器,除了前后端分离外,还能做redis缓存,负载均衡策略。另一方面,不止是Node.js能做中间层,PHP也可以,因为Node.js是用js写的,Node.js的生态很成熟,对于前端人员来说,比较容易上手。</p><p>Web端的开发团队是需求链中的最上游、数据链的下游,很多产品功能都受限于业务接口,中间层提供了一种可能,让我们Web前端开发工作有了自己的接口开发能力可以对接最原始数据,既减少了前端开发中的局限性,也让前端团队在开发过程中有了更多的想象力,能更好的根据业务需要快速开展项目。<br><strong>招聘信息</strong></p><p>好未来技术团队正在热招前端、算法、流媒体后台开发等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索关注“<strong>好未来技术</strong>”,点击本公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p><p><strong>也许你还想看</strong></p><p><a href="https://link.segmentfault.com/?enc=Y1%2F%2BnrtgVNbP34Sy%2Byqumg%3D%3D.KfjNaW9dKy1n184%2BTo6aFN65GNJy3TZY8h1Vb3oU1raNCqdDAEk4ytNemrDVblMIO2IT4jUevHpUig9wgHGCsndeTSX4rivIytx4cj5EvD6pRatkd3vWYOKpH8OFDLKDyPKqTBOlPJjTgSsWyhctZFLQ%2FE48GsA0aMRjV56Rjh4gkqLGG466F54c5HmmMud%2FpwPkgeoBo68Ypfcw6f0WrEFwz1Ezbx2ed%2BMqRlZuyPxpEZecahAvPWKLl2arkEziTr8LxzxI4oLM5pS8a%2F72OTKP6JQ%2Bpyhz6i81VbIrN15yrCV72UJJQ3R9sNMRUaog" rel="nofollow">DStack--基于flutter的混合开发框架</a></p><p><a href="https://link.segmentfault.com/?enc=QiO4uY3fuXiCPvpYRr15nw%3D%3D.HOKXLJHc9Obu2%2BV1AjicvEJngOu3C%2F1Efdor0Crnkuymy47bbDD2H%2F3T9Ul1TqE7yGGKRCLZVAWwJPJwMS%2B7w14hs6PAaZHXeMpTNXSdK2nFXg0hhF0xpQ%2F%2BMm6M6g1uZkeDMhEHXzh2Jn4VuzAoqSc%2BqQkZrYL5tstNnV8QKGbO83oKIPx%2BcAc0p8EnaQRFVxxYS4qLx1pVqky86gBnfvBlKk5OBSvLGBQsofG%2FwqT6sTJswM5hTXfcgJGFtq%2Bdrs%2BxKwkyqnwHDLp%2FH7l6y63SWZvQ7oRB2Lo9L%2FeVCpo%2FlhS3rSfI2zKCDPTbVzbe" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p><p><a href="https://link.segmentfault.com/?enc=O4IZQvjnvsKUfx2CUarmbg%3D%3D.hpBNEybyWorYhF7pNrybuqeHtdCtO7l0HH5CsMQG3%2BulhTwVHr46YtojoxU8DWCQhs2wdCBM8s2HuPHPtH6mwbfUt2SNhLA3pivNtIbXtfLRXik1NfeQDoID60jIijm6RJTqojbOKMunfZOffXjvMpnRcpDwXWa6SeHZvywzBPFTfdfkACXKLInZqLlvFAFvvXmeaOm%2F%2BPSdtdfL3%2Fj29z3mjbuqRnB%2BWYb58XzU3rrstdXLkLRYRN5%2FsHUzjEcGhPiyGrijAk5hZDQMiCpn1GQXEYiiuvuIWaPrOrQ1DoyRLEqUBJ%2FklnYAlygJbG6b" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p><p><img src="/img/remote/1460000023456219" alt="" title=""></p>
知识图谱 综述,构建,存储与应用
https://segmentfault.com/a/1190000023366451
2020-07-24T17:30:38+08:00
2020-07-24T17:30:38+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
4
<p>本文介绍知识图谱,首先会讲一段知识图谱的综述作为开场,然后就知识图谱的构建,存储,还有应用进行具体说明。 <br><img src="/img/remote/1460000023366462" alt="幻灯片1.JPG" title="幻灯片1.JPG"></p>
<p>知识图谱和我们的资源页比较类似,都是需要先构建,然后存储,之后应用。 <br>知识图谱应用广泛,我会以推荐系统为例子,说明知识图谱在推荐系统中的应用。 <br><img src="/img/remote/1460000023366455" alt="幻灯片2.JPG" title="幻灯片2.JPG"></p>
<h3>知识图谱综述</h3>
<p>我们首先对知识图谱做一个简短的综述。 <br><img src="/img/remote/1460000023366457" alt="幻灯片3.JPG" title="幻灯片3.JPG"></p>
<p>计算机为什么需要知识? <br>比如数字110,对机器来说,110就是一个字符串,与其他数字没有太大的差别。 <br>当然可以借助关联分析,分析出110跟警察,抢劫等相关。但是关联分析比较复杂,需要借助数据挖掘等相关技术。 <br>如果采用知识库,只需要构建一条知识,即110是报警电话。</p>
<p>人工智能分为三个层次,分别是运算智能,感知智能和认知智能。 <br>运算智能是让机器能存会算;感知智能是让机器能听会说、能看会认;认知智能是解决机器能理解会思考的问题。</p>
<p>认知智能需要知识图谱。 <br><img src="/img/remote/1460000023366461" alt="幻灯片4.JPG" title="幻灯片4.JPG"></p>
<p>知识图谱是一个大规模语义网,包含实体和关系,比如章子怡的丈夫是汪峰; <br>也包含实体和属性,比如章子怡的出生日期是1979年2月9日。 <br>还包含实体和概念,比如章子怡是一个女演员; <br>还包含概念之间的关系,比如女演员是演员的子类。演员是人物的子类。 <br><img src="/img/remote/1460000023366460" alt="幻灯片5.JPG" title="幻灯片5.JPG"></p>
<p>百科图谱一般由<strong>标题,摘要,信息框,标签,图片</strong>等部分组成。 <br>可抽取信息框的内容构建知识图谱,并进行可视化展示。 <br>其中,对于题目理解来讲,函数的提出者,提出时间这些属性不是我们所关心的。 <br>表达式,表示法,三要素是我们关心的属性。 <br><img src="/img/remote/1460000023366459" alt="幻灯片6.JPG" title="幻灯片6.JPG"></p>
<p>知识图谱可以使能搜索与问答,比如搜索函数的三要素,可以直接得到结果:定义域,值域,对应法则。 <br><img src="/img/remote/1460000023366458" alt="幻灯片7.JPG" title="幻灯片7.JPG"></p>
<p>知识图谱还可以使能Query补全,比如输入函数的,推荐的候选Queries为函数的定义,函数的三种表示方法等。 <br><img src="/img/remote/1460000023366454" alt="幻灯片8.JPG" title="幻灯片8.JPG"></p>
<p>知识图谱强调世界是由<strong>实体</strong>而不是字符串组成的,比如题目中的函数,f(x), 定义域等在知识图谱中都是一个个实体,而不是字符串。 <br><img src="/img/remote/1460000023366479" alt="幻灯片9.JPG" title="幻灯片9.JPG"></p>
<p>那么什么是知识图谱呢? <br>知识图谱是一个大规模语义网,由<strong>实体,概念</strong>等节点和<strong>属性,关系,类型</strong>等边构成。 <br>是许多三元组的集合。每一个三元组是由主语(subject),谓语(predicate),宾语(object)构成。 <br>比如中国,上海,姚明,叶莉等是实体。 <br>比如地理位置,人物是概念。 <br>比如姚明的身高226厘米,身高是属性。 <br>比如姚明的配偶是叶莉,配偶是关系。 <br><img src="/img/remote/1460000023366469" alt="幻灯片10.JPG" title="幻灯片10.JPG"></p>
<p>知识图谱三元组基本类型有四种,分别是实体,关系,实体,比如好未来,创始人,张邦鑫老师; <br>还有实体,属性,属性值,比如好未来,成立时间,2003年; <br>还是实体,is-a,概念,比如好未来,is-a,上市公司; <br>还有子概念,subclass-of,父概念,比如上市公式,subclass-of,公司;</p>
<p>不能把is-a和subclass-of进行混淆,就好像不能把集合的属于和包含进行混淆一样。 <br><img src="/img/remote/1460000023366467" alt="幻灯片11.JPG" title="幻灯片11.JPG"></p>
<p>知识图谱分为<strong>模式层</strong>和<strong>数据层</strong>。 <br>模式层是数据的模式,是对数据层的提炼。 <br>数据层是具体的数据。 <br><img src="/img/remote/1460000023366456" alt="幻灯片12.JPG" title="幻灯片12.JPG"></p>
<p>模式层是知识图谱的数据模型,是对数据层的约束。 <br>我们以教学图谱为例,可以有staff, professor, course, laborary, student, PhD student等概念,以及professor 和course之间的联系,professor teach course。 <br>还有professor 和PhD student之间的联系 Professor supervise PhD student。 <br>这些概念以及概念之间的关系,构成了知识图谱的模式层。 <br>然后在模式层下添加实体,比如Professor Xu和PhD student Wang,以及实体之间的关系,比如Xu supervise Wang. <br><img src="/img/remote/1460000023366463" alt="幻灯片13.JPG" title="幻灯片13.JPG"></p>
<h3>知识图谱的构建</h3>
<p>接下来,我们介绍知识图谱的构建。 <br><img src="/img/remote/1460000023366464" alt="幻灯片14.JPG" title="幻灯片14.JPG"></p>
<p>知识图谱的构建,从<strong>数据来源</strong>来说,包括从结构化,半结构化和非结构化的海量数据中抽取知识,构建图谱。 <br>按<strong>构建者</strong>分,可以分为众包构建和自动化构建。众包构建,就是利用许多人进行编辑,构建知识图谱,维基百科,百度百科都是众包构建的。 <br>自动化构建,就是利用机器进行自动构建。 <br>按<strong>构建方式</strong>分,可以分为自上而下的构建和自下而上的构建。 <br><img src="/img/remote/1460000023366465" alt="幻灯片15.JPG" title="幻灯片15.JPG"></p>
<p>自上而下的构建先确定模式层,然后添加实体数据到知识库。 <br>自下而上的构建先确定知识图谱的数据层,然后提取数据的模式。 <br>行业知识图谱规模小,比如容易确定模式层,多采用自上而下的构建方式。 <br>通用知识图谱规模大,数据模式随数据的增长而变化,多采用自下而上的构建方式。 <br>知识图谱可以只有数据层,没有模式层。 <br><img src="/img/remote/1460000023366466" alt="幻灯片16.JPG" title="幻灯片16.JPG"></p>
<p>知识图谱模式层构建,也叫本体(ontology)构建。需要先确定知识图谱的领域,比如大学领域。 <br>然后列出领域内的术语,比如教职工,行政人员,技术支持人员,本科生,研究生等术语。 <br>然后确定类和类之间的层级关系,比如教职工是在职教师和研究人员的父类。学生是本科生,研究生的父类。 <br>然后定义术语外延的规则。比如概念的属性,概念之间的关系,属性或者关系的定义域(domain)和值域(range)等。 <br><img src="/img/remote/1460000023366474" alt="幻灯片17.JPG" title="幻灯片17.JPG"></p>
<p>知识图谱的构建分为众包构建和自动化构建。由于众包构建涉及技术较少。我们这里主要介绍自动化构建。 <br>这是知识图谱自动化构建的流程。 <br>首先从数据库,百科网站,垂直网站等数据来源获取结构化,半结构化,和非结构化数据。 <br>对非结构化数据和半结构化数据进行实体抽取,关系抽取,属性抽取,并与结构化数据进行整合,形成初步的三元组知识。 <br>然后通过实体消歧得到标准知识表示。 <br>对标准知识构建本体,形成数据模型。 <br>对知识进行推理,发现新的知识。 <br>对知识进行质量评估,从而进行质量控制。 <br>对知识图谱添加新的实体,或者修改旧的实体,对知识图谱进行更新。 <br>对构建好的知识图谱进行存储,方便下游应用。 <br>对知识图谱进行表示学习,将知识图谱离散的符号转化为连续的数值。 <br>对知识图谱进行应用,主要包括内容理解,搜索,推荐,问答等应用。 <br><img src="/img/remote/1460000023366475" alt="幻灯片18.JPG" title="幻灯片18.JPG"></p>
<p>这里描述了从半结构化数据抽取三元组的例子,主要涉及网页爬取与解析。这里根据信息框(infobox)和标签(tags)来抽取三元组。 <br><img src="/img/remote/1460000023366476" alt="幻灯片19.JPG" title="幻灯片19.JPG"></p>
<p>对于非结构化数据,先经过预处理,比如全角转半角等,然后进行分词,词性标注,语法解析,依存分析等NLP工具对文本进行解析,进一步进行实体识别,然后关系抽取,实体消歧,事件抽取等构成三元组知识。 <br>比如文本:已知函数f(x)的定义域,抽取出函数,f(x), 定义域等实体,然后对实体对进行关系分类;比如函数和f(x)分类为表达式,比如函数和定义域分类为要素。 <br>形成三元组知识:函数,表达式,f(x),函数,要素,定义域。 <br>由于函数可能是数学中的函数,也可能是计算机中的函数,还需要进行实体消歧。根据上下文判断函数为数学中的函数。 <br><img src="/img/remote/1460000023366470" alt="幻灯片20.JPG" title="幻灯片20.JPG"></p>
<p>实体识别是识别出文本中的<strong>人名,地名,组织机构名,时间,日期,货币</strong>等类型的字符串。 <br>比如左边这条新闻,识别出时间3月23日0时50分,识别出人名特朗普等。 <br>对于学科图谱来讲,需要识别出相关术语。比如函数,数集等术语。 <br><img src="/img/remote/1460000023366468" alt="幻灯片21.JPG" title="幻灯片21.JPG"></p>
<p>实体识别一般建模成序列标注任务。输入一个序列,经过词嵌入,和双向LSTM编码,然后用CRF进行解码。 <br>其中函数预测的标签是B-Noun, E-Noun, B和E分别表示mention的开始和结束,Noun表示类型。 <br>f(x)预测为表达式,其中I-Expr, I表示Inside, Expr表示表达式。 <br><img src="/img/remote/1460000023366471" alt="幻灯片22.JPG" title="幻灯片22.JPG"></p>
<p>当BERT出现后,由于BERT效果好,常采用BERT来对句子进行编码。 <br><img src="/img/remote/1460000023366472" alt="幻灯片23.JPG" title="幻灯片23.JPG"></p>
<p>当识别出了文本中的实体,还需要对文本中的实体,两两进行关系分类。 <br>一般我们会收集并标注一个关系分类的训练集,来训练一个模型,然后用模型对测试数据进行预测。 <br>比如我们训练好模型后,对测试数据,集合中的元素有多种特性,包括确定性,互异性,无序性进行预测。 <br>我们需要预测元素与确定性之间的关系,预测结果为特性。也就是集合有一个特性是确定性。 <br><img src="/img/remote/1460000023366473" alt="幻灯片24.JPG" title="幻灯片24.JPG"></p>
<p>由于BERT的兴起,常用BERT来做关系分类。 <br><img src="/img/remote/1460000023366477" alt="幻灯片25.JPG" title="幻灯片25.JPG"></p>
<p>识别出文本中的mention后,比如识别出函数,定义域等mention,还需要对mention进行实体消歧(entity disambiguation)。也就是这个mention提及的是哪一个实体。 <br>比如函数可以是数学术语,也可以是计算机中的函数,根据上下文,判断指代的是数学中的函数。 <br><img src="/img/remote/1460000023366478" alt="幻灯片26.JPG" title="幻灯片26.JPG"></p>
<p>注意到实体链接就是先识别出来文本中的mention,然后将识别出来mention链接到知识库中实体,所以说<strong>实体链接=实体识别+实体消歧</strong>。 <br><img src="/img/remote/1460000023366480" alt="幻灯片27.JPG" title="幻灯片27.JPG"></p>
<p>除了实体链接以外,还可以有公式链接。比如题目或者解析中某个公式使用了某个定理,可以将这个公式链接到它运用的定理上。 <br><img src="/img/remote/1460000023366481" alt="幻灯片28.JPG" title="幻灯片28.JPG"></p>
<p>构建好了知识图谱后,我们需要对构建好的知识图谱进行规模和质量的评估。 <br>规模一般用<strong>知识图谱中有多少个实体,有多少个关系</strong>来描述。 <br>质量一般可以用<strong>准确率</strong>来衡量。由于知识图谱三元组数量多,我们一般抽取若干个三元组,比如500个,对每一个三元组进行真假判断,然后统计准确率。 <br>右表展示了抽取8个三元组,计算准确率的过程。 <br>一般来讲,众包构建的准确率较高,自动化构建的准确率相对较低。 <br>领域知识图谱准确率较高,通用知识图谱准确率相对较低。 <br><img src="/img/remote/1460000023366482" alt="幻灯片29.JPG" title="幻灯片29.JPG"></p>
<h3>知识图谱存储与查询</h3>
<p>接下来我们介绍知识图谱存储与查询。 <br>知识图谱存储和查询可以分为两类,一类是基于图数据库的,一类是基于关系型数据库的。 <br><img src="/img/remote/1460000023366483" alt="幻灯片30.JPG" title="幻灯片30.JPG"></p>
<p>我们先介绍基于图数据库的管理系统。 <br>属性图(property graph)是图数据库中最常用的数据模型,由节点和边构成。 <br>比如下面这幅图,有三个节点,每个节点表示一个对象。 <br>第一个节点的标签是Employee,这个节点的属性用键值对存储,比如姓名为Amy peters, 出生日期为1984年3月1日,ID为1。 <br>Company 和 Employee之间有边HAS_CEO,边上也可以有属性,比如Company has CEO 开始日期为2008年。 <br><img src="/img/remote/1460000023366491" alt="幻灯片31.JPG" title="幻灯片31.JPG"></p>
<p>下面是一个图数据库查询1号节点认识的节点中,年龄大于30的节点参加过的项目。 <br>其中Gremlin和Cypher是图数据库两种查询语言。 <br>Gremlin是过程式(procedural)语言;用户需指明具体的导航步骤,也就是在图上怎么走;它是业界标准查询语言,除了Neo4j外,几乎所有图数据库均支持。 <br>Cypher是Neo4j专用语言,它是声明式(declarative)语言;用户只需声明“查什么”, 无需关心“怎么查”; <br><img src="/img/remote/1460000023366493" alt="幻灯片32.JPG" title="幻灯片32.JPG"></p>
<p>当然我们也可以利用关系型数据库对知识图谱进行存储。我们可以将图数据用三元组表示,将每一个三元组作为表中的一行记录。 <br>下面是查询生于1850年,死于1934年,创建过公司的人。 <br>采用关系型数据库存储,多跳查询会产生自连接(self-join)操作。 <br>比如A->B为一跳,A->B->C为两跳。 <br><img src="/img/remote/1460000023366492" alt="幻灯片33.JPG" title="幻灯片33.JPG"></p>
<p>也可以采用水平表的方式进行存储,每一行存储一个主语对应的所有的谓语和宾语。 <br>这种存储方式适合于谓词较少的知识图谱。 <br>主语一般只在极少的列上有值,导致存储空间浪费。 <br>并且这种存储方式很难存储多值属性或者一对多关系。 <br>比如函数的三要素是定义域、值域和对应法则,用水平表存储这种多值属性,需要对值拼接后才能存储。 <br><img src="/img/remote/1460000023366485" alt="幻灯片34.JPG" title="幻灯片34.JPG"></p>
<p>也可以按照实体的类型对知识图谱进行划分,这种方式适合于实体类别较少的情况。 <br>同样地,存储多值属性或一对多关系需要对值进行拼接。 <br><img src="/img/remote/1460000023366484" alt="幻灯片35.JPG" title="幻灯片35.JPG"></p>
<p>也可以根据谓词对知识图谱进行划分。对每一个谓词创建一张表。这种方式解决了数据存储稀疏性问题,也可以存储多值属性。 <br>但是涉及多个谓词的查询会导致多表连接操作。 <br><img src="/img/remote/1460000023366486" alt="幻灯片36.JPG" title="幻灯片36.JPG"></p>
<h3>知识图谱的应用</h3>
<p>知识图谱的应用有很多,我们主要介绍知识图谱在推荐系统中的应用。 <br><img src="/img/remote/1460000023366487" alt="幻灯片37.JPG" title="幻灯片37.JPG"></p>
<p>如图是一个新闻推荐的例子,假设某个用户看过一条新闻,这个新闻的内容是: <br>Boris Johnson Has Warned Donald Trump To Stick To The Iran Nuclear Deal(鲍里斯·约翰逊警告唐纳德·特朗普坚持伊朗核协议)。 <br>从这条新闻中提取出4个实体,然后对这些实体做一跳,两跳,三跳扩展,会发现这些实体都指向另外一条新闻: <br>North Korean EMP Attack Would Cause Mass U.S. Starvation, Says Congressional Report(国会报告称,朝鲜电磁脉冲攻击将导致美国大规模饥荒)。 <br>这2条新闻的单词都不一样,利用知识图谱可以发现他们底层之间的关联。 <br><img src="/img/remote/1460000023366488" alt="幻灯片38.JPG" title="幻灯片38.JPG"></p>
<p><strong>KG能给推荐系统带来什么?</strong> <br>首先知识图谱可以提高推荐系统的<strong>精度(Precision)</strong>,更准确地发现item之间的关联,比如Cast Away 和 Forrest Gump 都是Tom Hanks 主演的。 <br><img src="/img/remote/1460000023366489" alt="幻灯片39.JPG" title="幻灯片39.JPG"></p>
<p>知识图谱还可以提高推荐系统的<strong>多样性(Diversity)</strong>。电影可以通过主演扩展,也可以通过电影类型扩展,还可以通过导演来扩展,找到相似的电影。 <br><img src="/img/remote/1460000023366490" alt="幻灯片40.JPG" title="幻灯片40.JPG"></p>
<p>知识图谱还可以提高推荐系统的<strong>可解释性(Explainability)</strong>,知识图谱中的路径可以用来解释为什么会推荐这部电影。比如某个用户喜欢Cast Away这部电影,系统推荐了The Terminal这部电影,因为他们有相同的主演。 <br><img src="/img/remote/1460000023366494" alt="幻灯片41.JPG" title="幻灯片41.JPG"></p>
<p>已知一个用户的集合Users,一个物品的集合Items,用户和物品的交互矩阵YYY,yuv=1y_{uv}=1yuv=1表示用户点击过某个物品,0表示未点击。 <br>每个物品vvv在KG中对应一个实体。物品是实体的一个子集。 <br>目标是学习一个函数FFF,给定uuu,vvv,预测点击率y^uv\hat y_{uv}y^uv,Θ\ThetaΘ是模型的参数。 <br><img src="/img/remote/1460000023366495" alt="幻灯片42.JPG" title="幻灯片42.JPG"></p>
<p><strong>DKN方法</strong>是给出一段新闻,提取新闻中的实体,根据这些实体,构建一个知识图谱子图,对子图做embedding,得到每个实体的embedding。 <br><img src="/img/remote/1460000023366496" alt="幻灯片43.JPG" title="幻灯片43.JPG"></p>
<p>另外,实体的邻居节点可以作为该实体的上下文信息。将这些邻居实体的embedding求平均,得到该实体的上下文表示。如上图公式中e¯\bar ee¯就是实体eie_iei的上下文embedding。 <br><img src="/img/remote/1460000023366497" alt="幻灯片44.JPG" title="幻灯片44.JPG"></p>
<p>前面介绍了实体表示,实体上下文表示,另外结合词向量,形成三个通道,进行卷积和池化,得到这个句子的表示,我们管这个方法叫KCNN。 <br><img src="/img/remote/1460000023366498" alt="幻灯片45.JPG" title="幻灯片45.JPG"></p>
<p>假设用户点击过3条新闻,来了一个候选新闻,需要预测用户对候选新闻的点击概率。 <br>用KCNN对这4条新闻做embedding,得到4个特征向量。 <br>用Attention Net计算用户看过的每一条新闻与候选新闻之间的相似性。 <br>用相似性得分对观看记录求加权平均,得到用户表示(User embedding)。 <br>将用户表示和候选新闻表示拼接,用多层感知机(MLP)预测的点击率。 <br><img src="/img/remote/1460000023366500" alt="幻灯片46.JPG" title="幻灯片46.JPG"></p>
<p>刚才DKN模型仅融入了实体的一跳信息,<strong>RippleNet</strong>除了融入一跳信息外,还融入了实体的两跳,三跳信息。Ripple是水波的意思。 <br><img src="/img/remote/1460000023366499" alt="幻灯片47.JPG" title="幻灯片47.JPG"></p>
<p>另外还有直接利用**图神经网络(GNN)**对知识图谱进行表示。 <br>用图神经网络处理知识图谱需要先将知识图谱中的关系转化为数值。对于每一个用户,引入一个打分函数,用于对知识图谱中每一个关系进行打分。不同用户同一个关系打分不一样,分值高低跟用户的偏好相关。 <br>然后利用图神经网络进行前向传播。其中AuA_uAu是某个用户uuu对应的邻接矩阵。 <br>DuD_uDu是顶点的度矩阵,这是一个对角矩阵。 <br>WlW_lWl是训练参数矩阵。 <br>HlH_lHl,Hl+1H_{l+1}Hl+1是实体对应的embedding矩阵。 <br>σσσ是一个非线性函数。 <br><img src="/img/remote/1460000023366503" alt="幻灯片48.JPG" title="幻灯片48.JPG"></p>
<p>我们总结一下。 <br><img src="/img/remote/1460000023366501" alt="幻灯片49.JPG" title="幻灯片49.JPG"></p>
<p>谢谢阅读!如有错误,请批评指正~</p>
<h3>资源推荐</h3>
<ul>
<li><a href="https://link.segmentfault.com/?enc=QobYgeP1BByNOnZwcEep%2Fg%3D%3D.h4vqSd5h7fAB6IP%2Fe1pR79XpTJFlepvW9hwAUdkcYxfEWP5NXfhM0btFThrL1Gkcl7lirodyM%2B03wHUBpnv2GQ%3D%3D" rel="nofollow">知识图谱相关学习资料合集</a></li>
<li><a href="https://link.segmentfault.com/?enc=d0D%2BYfbzpfwtZE34LWIrzQ%3D%3D.ZKGMyP5PxzqsRuDfd78H9jw2QHCfrgKm3afCiclp313BtNdEZtYUdDoxwq2roDav" rel="nofollow">东南大学《知识图谱》研究生课程</a></li>
</ul>
<p><strong>作者简介</strong> </p>
<p>岳祥为好未来自然语言处理高级工程师</p>
<p><strong>招聘信息</strong></p>
<p>好未来技术团队正在热招前端、算法、后台开发等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索“好未来技术”,点击本公众号“技术招聘”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=ORJ9eDVm1056GXqwJ8LgVQ%3D%3D.%2FeezKaCSWPSLf9V2RZ0rVENrvTA4cquZHwzs9XQoQ0PlO6batrwd4p3jG7iRjPXWprRKr19mCo9Tnw%2B3j1%2FVHkv1qqk0ahwqjQ6SXe9ZatM0l4fn681nB%2BRn9rzsxVdTxbVlbDwlNPTjg1MLaMqfTBtvyFD7mvYkmgYnUwJAXCI5xt3EZLmiO9Ucoz1hRVihbRCA9J%2FNyjPE2OgUCWcv1EuK0A2OLmRl4CPlPACW6pO9mklPGQMchiPBbZtxxtW4dTP6ttGk4tfWhcjVvFpQQNY3HPeg9P0FJXMVpTLGivUrGjmW2axR0BuwuzXqcQvT" rel="nofollow">DStack--基于flutter的混合开发框架</a></p>
<p><a href="https://link.segmentfault.com/?enc=hqzF690RpIEiusVu2Sphkw%3D%3D.P%2BK88BwrBAZxc%2FANM3n89bvwXRQJcVB%2B%2F1IyTehMayZ5bOmwQqFm4drUfFp2B61o8LhZqtHYiq3L8mzZJmU9WvZ9Sx7nc84l1L8qT3xm49boP3QU8Ddp0oMGjKxwfDtmhyVEalZS%2Fkka5Yg1yeM6j%2BIqXOQ3Ro4WIJNDI5gtONl4L3NjRqmx%2B6JvCWentRN5IiwL%2BphokgVmZ%2FnbbUJh%2FeXxi5BO0abF5qEKptsXgHMRIbzFQS8%2FoVNxPNaA2ueF80Iswy9TxasWB0yA2MNmdbaomQJXefXF1Q7yHX%2FHdy0hwn%2FAUReqdJENeMS3lsT2" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p>
<p><a href="https://link.segmentfault.com/?enc=OJ3lNtMhLzOlawhkFDcm8A%3D%3D.wfuOszj3rYjbEY32fWhCGRyRpjgcJxdCeSwPLqrB0hJfEUkYK820tr63sCFw94HrGsRW%2BSCA8Rl3TGipywCWBWsklzXC6Hd39dFfqCrtG%2Fwf5If70hcpdjjL0DkyMlZCAdQmTEh95%2F8I1YqW6vK0rhh%2Fam1%2FPJRk%2Fcf5OD%2BR4qBaavI%2B9lmox7JGNAScvkNWj9%2FRPYGRsBSB6xu6I7gVmlvsXQpnIPmNlDJPs%2B1dF%2BHEDQsw9wb%2BsVzw7wFvVr5CqbrXFZAXNJPiI%2Fvl%2B5nWoBLcx7aDbeyZKCRVLe%2FHZH3MU3ZbwJxSy3F8GiFcS78d" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><img src="/img/remote/1460000023366502" alt="" title=""></p>
Chrome Performance 页面性能分析指南
https://segmentfault.com/a/1190000023272526
2020-07-17T14:36:07+08:00
2020-07-17T14:36:07+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
9
<h4>1.背景</h4><p>性能优化是前端开发一个非常重要的组成部分,如何更好地进行网络传输,如何优化浏览器渲染过程,来定位项目中存在的问题。Chrome DevTools给我们提供了2种常用方式 Audits和Performance,Audits可以对页面进行性能评分,同时,还会给我们提供一些优化建议。而Performance提供了非常多的运行时数据,能让我们看到更多细节数据。下面主要介绍一下如何使用DevTools中的Performance来进行性能分析</p><h4>2.Performance介绍</h4><p>首先在新的无痕窗口打开网页,打开Chrome DevTools 切换到 Performance 下可以看到以下画面</p><p><img src="/img/remote/1460000023272534" alt="image.png" title="image.png"></p><p>上图1、3区域按钮可以用来触发性能数据记录,黑色按钮可以记录交互阶段的性能数据,圆形箭头按钮用来记录加载阶段的性能数据。 <br>上图2区域 可以设置当前页面的网络加载速度与cpu运算速度。</p><p>下面我们点击黑色按钮来生成一份交互阶段的性能报告</p><p><img src="/img/remote/1460000023272533" alt="image.png" title="image.png"></p><h5>第一部分:概览</h5><p>这里最主要是整体的界面渲染的时候,每个时间段执行的事件顺序,通过上图我们就能知道我们每个时间段(精确到毫秒)都做了什么,当鼠标放上去的时候,我们还可以大图的形式去查看我们每个时间段界面的渲染情况,Performance 就会将几个关键指标,诸如页面帧速 (FPS)、CPU 资源消耗、网络请求流量、V8 内存使用量 (堆内存) 等,按照时间顺序做成图表的形式展现出来。</p><h5>第二部分:性能面板</h5><p>性能面板主要包括以下几部分 <br>1.Network 这里我们可以直观的看到资源加载的顺序与时长 <br>2.Interactions 用来记录用户交互操作,比如点击鼠标、输入文字、动画等 <br>3.Timings 用来记录一些关键的时间节点在何时产生的数据信息,诸如 FP、FCP、LCP 等 <br>4.Main 是Performance工具中比较重要的部分,记录了渲染进程中主线程的执行记录,点击main可以看到某个任务执行的具体情况 <br>5.Compositor 合成线程的执行记录,用来记录html绘制阶段 (Paint)结束后的图层合成操作 <br>6.Raster 光栅化线程池,用来让 GPU 执行光栅化的任务 <br>7.GPU GPU进程主线程的执行过程记录,如 可以直观看到何时启动GPU加速… <br>Memory 选项,在勾选后,就会显示该折线图,通过该图可以看出我们在不同的时间段的执行情况。我们可以看到页面中的内存使用的情况,比如 JS Heap(堆),如果曲线一直在增长,则说明存在内存泄露,如果相当长的一段时间,内存曲线都是没有下降的,这里是有发生内存泄露的可能的。 <br>通过对性能面板各个部分的分析与问题定位,可以更深刻的理解浏览器是如何工作的</p><h5>第三部分:Summary(性能摘要)</h5><p>它是一个用来统计在我们检测性能的时间范围内,都做了哪些事情: <br>Loading :加载时间 <br>Scripting :js计算时间 <br>Rendering :渲染时间 <br>Painting :绘制时间 <br>Other :其他时间 <br>Idle :浏览器闲置时间</p><h4>3.Performance实践</h4><p>下面举例来说明一下性能面板的使用,在无痕窗口下点击自动重启页面,并记录整个页面加载的过程,然后来分析结果~</p><h5>网络&&白屏</h5><p>性能面板,有很多很多的参数,我们要看一些比较常见的。首先看白屏时间和网络加载情况,如下图 <br><img src="/img/remote/1460000023272532" alt="image.png" title="image.png"></p><p>上图,我们可以看几点信息: <br>本次页面加载的白屏时间约为 150 ms <br>从网络资源加载情况来看,图片没有启用 http2,因此每次可以同时加载的图片数有限,未被加载的图片有等待过程 <br>资源的加载时间也可以看到 <br>另外,我们可以看一下资源加载有没有空白期,虽然上图没有,但是如果资源加载之间存在空白期,说明没有充分利用资源加载的空闲时间,可以调整一下。</p><h5>火焰图</h5><p>火焰图,主要在 Main 面板中,是我们分析具体函数耗时最常看的面板,我们来看一下,如图:</p><p><img src="/img/remote/1460000023272529" alt="image.png" title="image.png"></p><p>首先,面板中会有很多的 Task,如果是耗时长的 Task,其右上角会标红,这个时候,我们可以选中标红的 Task,然后放大,看其具体的耗时点。 <br>放大后,这里可以看到都在做哪些操作,哪些函数耗时了多少,这里代码有压缩,看到的是压缩后的函数名。然后我们点击一下某个函数,在面板最下面,就会出现代码的信息,是哪个函数,耗时多少,在哪个文件上的第几行等。这样我们就很方便地定位到耗时函数了。 <br>同时也可以查看 Main 指标分析代码里面是否存在强制同步布局等操作,分析出来这些原因之后,我们可以有针对性地去优化我们的程序</p><h5>时间线&&内存情况</h5><p>在 Timings 的区域,我们可以看到本次加载的一些关键时间,分别有:</p><p>FCP: First Contentful Paint <br>LCP: Largest Contentful Paint <br>FMP: First Meaningful Paint <br>DCL: DOMContentLoaded Event <br>L: Onload Event <br>我们可以选区(选择从白屏到有内容的区域,代表本次的页面加载过程),可以对照着看一下上面的时间,截图如下:</p><p><img src="/img/remote/1460000023272531" alt="image.png" title="image.png"></p><p>另外,我们可以看到页面中的内存使用的情况,比如 JS Heap(堆),如果曲线一直在增长,则说明存在内存泄露。如果Nodes和Listeners不断增加说明可能存在重复添加节点或者事件的情况。</p><p>最下方就是页面的一个整体耗时概况,如果 Scripting 时间过长,则说明 js执行的逻辑太多,可以考虑优化js,如果渲染时间过长,则考虑优化渲染过程,如果空闲时间过多,则可以考虑充分利用起来,比如把一些上报操作放到页面空闲时间再上报等。</p><h4>4.最后</h4><p>大家可以自己去尝试一下performance的使用,通过performance可以更直观的理解浏览器的渲染原理与工作流程,同时也能够将浏览器的系统架构、消息循环机制、渲染流水线等前端概念联系到一起,加深理解。</p><p><strong>作者简介</strong> </p><p>李长江为好未来前端开发高级专家</p><p><strong>招聘信息</strong></p><p>好未来技术团队正在热招前端、算法、流媒体后台开发等各个方向高级开发工程师岗位,大家可扫描下方二维码或者微信搜索关注“<strong>好未来技术</strong>”,点击本公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p><p><strong>也许你还想看</strong></p><p><a href="https://link.segmentfault.com/?enc=oLbGC3EBuNjsn%2BByB%2BnQug%3D%3D.RMOW0cBjlngneeAEMiBIAqpfYZwj52bkR9EUR0Y8XxdozZ%2BYYKqCsVkWRy8Pzxz14y0H4fh9l8CaN7SXS%2BFw6EFqd4O8lbIzvbyqoi2Iq5sN9kLDuha9bm25ppX1mErPdTD32uJY5XMbZqcVkEkin6v%2Frc60kLhgPrxcoTsdepqbYZsMHc89hz5EF%2FSYj4FYrs0nAj%2BhMKSkolryDxVEsy0GMthHEP%2FgXNSJv2IxpvCRCnxR3MICVtmK36lKJVeZ%2F0HR%2B8vT%2B4%2FLRU2b%2FH3wo580PA8CbXOMC8qEzxAT31UHjd4KrzQzXuH9jislGxMl" rel="nofollow">DStack--基于flutter的混合开发框架</a></p><p><a href="https://link.segmentfault.com/?enc=d3PF1lEONydot47Z9T%2FO9A%3D%3D.uldqbEnni8RAka5LcUDVm1%2Bs1q0h9KwdlOZnfoBl54hTj7vys4On9s5OLMbWlK8u06UTp2g1SmWJDwT30%2Bf0jRGsX%2FSy9%2FxqUPQcwVWZa7JFdmzw256Lgj%2FyKT1S4wc5dbRoQVGW5AB%2FvLiSEXB28CcgcGnVdWn3CBICatvOGqAEnxaHPC0XUKZW2cvQvz3IxZ54j%2BUNwDRxtL5L33XevKL%2BSNxOlTXH%2BsQvy%2BGNdngotdSMsZuDNKQYzEE2VU5NAT47%2Bu0T5dhpuE1czD2mHw5QzYr9bI26ydBCf3DH%2FTTt8swp2WSirB9mNKqP7Xkf" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p><p><a href="https://link.segmentfault.com/?enc=U9%2B2Hmar2QTQZBl%2BLknhJg%3D%3D.cnw8uYV3x0Vf3cVAIaA77bPWJbS%2B6vuJFyAZd%2F04ulGNUajzCWjOIfVz5J9bTrWLQFsYolHIvs8o4F0emGtKxy5YMxVOZjcxb55x7YYx8nrz1EVi9Gp7Iuwuq3c3xN3fPUiZtord7J3ulPcscTrApF5WXq1q%2FX%2Fx5%2Bo13FhBo2x9rrh0VomAxEAWH7k9VBs%2BOURqujM1dNYq1jz8IeNUDvTGgATeANnS1d0Gvj%2Fp2L9sel5hFZONd4PJuhbet5OuATwcMenIeXJo%2B9VMEVz5yuMqmcCubpKuGM1kzVpJz9J0PZbukaOLwbJjAwYL3gdH" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p><p><img src="/img/remote/1460000023272530" alt="" title=""></p>
【脑电硬件】大脑探测之旅继续,脑成像技术概览(下)
https://segmentfault.com/a/1190000023161441
2020-07-10T14:47:06+08:00
2020-07-10T14:47:06+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p>在上一篇文章里,我们介绍了脑电图(EEG),脑磁图(MEG),经颅磁刺激(TMS) 和功能性近红外光谱成像(NIRS) 的脑成像技术。这四种脑成像技术各自利用了与脑活动相关的电、磁和光信号来达到记录脑皮层活动的目的。这几种技术的一个共同的内在不足是对大脑功能记录的空间分辨率低,仅基本限于在头皮部分探测大脑皮层的活动,无法准确有效的探测脑皮层深部和皮层下脑组织的结构和功能活动。</p>
<p><strong>在本篇里,我们将介绍三种核脑影像成像技术,它们体现着完全不同的脑成像物理原理和思路。</strong></p>
<ul>
<li>PET: positron emission tomography 正电子发射成像</li>
<li>fMRI: functional magnetic resonance imaging 功能性核磁共振成像</li>
<li>MRI: magnetic resonance imaging核磁共振成像</li>
</ul>
<h3>一.正电子发射成像(PET)</h3>
<p>正电子发射成像(PET)是核医学领域比较先进的临床检查影像技术。<strong>它的原理是将人体代谢所必需的物质,如葡萄糖、蛋白质、核酸、脂肪酸等标记上短寿命的放射性核素制成显像剂(如氟代脱氧葡萄糖,简称FDG)注入人体后进行扫描成像。</strong>成像的物理机理利用了放射性同位素的正电子放射衰变特性。这个衰变过程会释放出一个正电子(即一个电子相对应的反粒子),正电子会与生物体中的一个电子遭遇产生电子对湮灭,并产生一个湮灭光子,这一信号可以被PET扫描器捕获。由于人体不同组织的代谢状态不同,这些被核素标记了的物质在人体各种组织中的分布、聚集状态也是不一样的。显影剂可以持续一段时间存在于整个大脑中,因而我们可以获取整个大脑的三维关于结构和功能活动的图像。PET就是通过对这些指标的定量刻画来反映生命代谢活动的情况,达到研究和诊断的目的。</p>
<p><img src="/img/bVbJlus" alt="图1.png" title="图1.png"> 图1. 正电子发射成像(PET)的物理成像机制</p>
<p><img src="/img/bVbJluv" alt="图2.jpg" title="图2.jpg"></p>
<p>图2. 医疗机构中常见的正电子发射成像(PET)设备</p>
<p>PET是惟一可在活体上显示生物分子代谢的影像技术,被广泛用于多种疾病的诊断与鉴别诊断、病情判断、疗效评价、脏器功能研究和新药开发等方面。例如:利用恶性肿瘤组织的高代谢特点可对病变进行诊断和分析(如下图所示)。 </p>
<p><img src="/img/bVbJluD" alt="图3.png" title="图3.png"></p>
<p>图3. PET成像在肿瘤诊断上的应用,常与CT(computerized tomography)共同使用,利用PET可以观察到肿瘤组织的代谢情况,从而提高诊断和治疗的效果。</p>
<p>PET早期在探测负责认知活动的大脑激活区域方面得到了广泛应用,但这一方面的功能随着功能性核磁共振成像(fMRI)的出现逐渐被取代,但在医疗诊断上依然有无法替代的优势。 </p>
<p><img src="/img/bVbJluW" alt="图4.png" title="图4.png"><br>图4. 看某视觉场景(左)和听故事时(右)的PET大脑功能活动成像</p>
<p><strong>PET成像技术的优点在于灵敏度高,特异性强,但与核磁共振技术相比,PET空间分辨率并不是很好,而且还需要注射轻微放射性的物质,所以患者不能持续进行PET扫描,原则上,出于患者安全的考虑,一年之内禁止两次及以上的PET扫描。</strong></p>
<h3>二.核磁共振成像(Magnetic Resonance Imaging,MRI)</h3>
<p>核磁共振现象的发现、成像技术的发明和应用上走向成熟是上世纪最伟大的科学技术成就之一。从核磁共振现象的发现到MRI技术成熟这几十年期间,有关核磁共振的研究曾在物理学、化学、生理学或医学领域内获得过6次诺贝尔奖。</p>
<p><img src="/img/bVbJlu2" alt="图5.jpg" title="图5.jpg"></p>
<p>图5. MRI/fMRI 设备和使用场景</p>
<p>核磁共振成像利用了核磁共振(nuclear magnetic resonance imaging,简称NMRI)的原理成像,但一个外界不太熟知的背景故事是,出于一般大众对核的恐惧感,”nuclear”一词在对技术的英文称谓中被故意隐去,变成了现今我们熟悉的MRI或者fMRI 等。</p>
<p><strong>核磁共振成像,顾名思义,包含了三个关键要素:核、磁、共振。</strong></p>
<p>核,指的是氢原子核。人体各种组织含有大量的水和碳氢化合物, 所以氢原子无处不在,含量最多,最适宜于成像需求。氢原子核微粒带一个正电荷,具有自旋的特性,旋转时会产生微小磁场,可视为一个个小磁针。氢原子核的自旋并不完全与磁场趋向一致,而是倾斜一个角度θ,称之为进动(precession)。微粒进动的频率取决于磁场强度,也与原子核类型有关。它们之间的关系满足拉莫尔关系:ω0=γB0,即进动角频率ω0是磁场强度B0与磁旋比γ的积。γ是每种核素的一个基本物理常数,也即进动的拉莫尔频率与磁场强度成比例。氢的主要同位素,质子,在人体中丰度大,而且它的磁矩便于检测,因此最适合从它得到核磁共振图像。</p>
<p><img src="/img/bVbJlu4" alt="图6.jpg" title="图6.jpg"></p>
<p>图6. 施加衡定强磁场前后氢原子核粒子的运动变化</p>
<p>磁,指的是加上衡定外强磁场(B0)后,大部分粒子的磁场方向与磁场方向相同, 少部分相反,即取向为“平行”和“反向平行”,他们分别对应于粒子的低能和高能两种状态。核磁共振成像利用了一般是借助超导形成的强磁场,如1.5T (tesla, 磁场强度单位),3T,和7T,把生物体内氢原子核进动形成的一个个小磁针从原本无序的排列(整体无磁性)变成一种有序排列的状态(整体呈磁性)。MRI设备一般体积巨大,花费昂贵,最主要原因是需要产生一个高强的磁场。</p>
<p>共振,指的是在已有外在衡定强磁场的条件下,施加另外一个短时的射频磁场B1来使氢原子核微粒进动形成的集体磁化向量发生偏转或旋转,这只有在射频磁场B1的频率和氢原子核微粒进动的拉莫频率一致(共振)时才有可能发生,才能实现能量的传递。射频磁场B1的作用方向一般与主磁场B0垂直。所以这里的共振指的是和氢原子核微粒进动的拉莫频率共振。</p>
<p><img src="/img/bVbJlu7" alt="图7.jpg" title="图7.jpg"></p>
<p>图7. 核磁共振基本原理示意图</p>
<p>在射频磁场作用下,氢原子核微粒的集体磁化向量可以分为两个分支:垂直z分量,水平xy分量。射频磁场的作用时间通常是非常短暂的,射频磁场撤去后,在自由进动阶段,磁化向量经过一个称为“弛豫”的过程,恢复它的原始静止位置。弛豫过程的特性由时间常数T1和T2来描述纵向弛豫 (z) 和横向弛豫(xy) 的特征。人体大脑和身体不同组织或组织液具有不同的纵向和横向弛豫特性,核磁共振成像技术最为关键的物理原理就是利用了这一点的特性分别不同的脑组织结构和功能。另外,NMR信号强度与样品中氢核的密度有关,人体中各种组织间含水比例不同,即含氢核数的多少不同,他们之间就存在NMR信号强度的差异。利用这种差异作为特征量,可以把各种组织分开,这就是氢核密度的核磁共振图像。</p>
<p>在核磁共振成像的技术发展方面,美国科学家Paul Lauterbur于1973年发明了在静磁场中使用梯度场去获得磁共振信号的位置,从而可以得到物体的二维图像;英国科学家Peter Mansfield进一步发展了使用梯度场的方法,指出磁共振信号可以用数学方法精确描述,从而使磁共振成像技术成为可能,他发明的快速成像方法为医学磁共振成像临床诊断打下了基础。他俩因在磁共振成像技术方面的突破性成就,获得了2003年诺贝尔医学奖。</p>
<p><img src="/img/bVbJlvb" alt="图8.jpg" title="图8.jpg"></p>
<p>图8. 磁共振扫描中大脑T1加权成像和T2加权成像</p>
<p>核磁共振成像安全,不涉及X射线或使用电离辐射,不需要注射放射性物质,且成像后的软组织结构清晰,能够提供更多的解剖结构信息。但核磁共振成像噪音很大,一般扫描时间较长(单纯结构成像一般需要7分钟左右,高空间分辨率的成像耗时更长)。<strong>此外,MRI检测要求体内不能存在金属,有幽闭恐惧症的人也无法参加。</strong></p>
<p><strong>核磁共振成像具有丰富的成像序列和功能发展潜力,</strong>这方面至今仍然是非常活跃的研究和应用领域,例如可以通过巧妙的安排成像序列和射频磁场的作用方式来刻画大脑白质,神经纤维的聚集和链接特性,这一成像技术被称为磁共振弥散张量成像技术 (Diffusion Tensor Imaging,DTI), 在科学研究和医疗诊断上有广泛的应用。</p>
<p><img src="/img/bVbJlvf" alt="图9.jpg" title="图9.jpg"></p>
<p>图9. 磁共振弥散张量成像形成的大脑纤维追踪</p>
<h3>三.功能性核磁共振成像(fMRI)</h3>
<p>最后,我们讨论一下从九十年代中期以来,在科研领域应用非常广泛深入的功能性核磁成像技术。在大脑整体成像的要求下,探测各个脑组织的功能性活动,且具有良好的空间分辨率和可接受的时间分辨率,功能性核磁成像技术在这些方面具有无可比拟的优势。<strong>功能性核磁共振成像吸收了MRI和PET的技术优势, 通过检测脑组织血流和含氧量变化引起的磁场变化,将原本的结构成像技术MRI发展到了功能成像。</strong></p>
<p>大脑受到外来刺激初期或者处于自发活动的需求,局部脑活动开始增强,耗氧量增加,随之脱氧血红蛋白在刺激开始后快速地上升;之后,由于大脑区域功能被激活,引起局部脑血管扩张,血流量增加,导致大量含氧丰富的血液流入该局部区域,含氧血红蛋白所占比例升高,脱氧血红蛋白比例降低;结束刺激,含氧血红蛋白含量下降,脱氧血红蛋白上升,均趋于平衡状态。</p>
<p>氧合血红蛋白是抗磁性的,与组织的磁化率非常接近,它的浓度改变不影响磁场的均匀性,脱氧血红蛋白是顺磁性的,在血管周边及内部会产生局部梯度磁场,明显缩短横向弛豫时间(T2),引起 T2 加权信号降低(顺磁性物质存在会引起所在环境磁场分布不均匀,导致核磁信号降低)。功能性核磁共振成像的信号依赖于代谢和认知活动相关的局部组织血管的血红蛋白氧含量的变化,所以叫血氧水平依赖脑功能磁共振成像(Blood oxygen-level dependent fMRI,BOLD-fMRI)</p>
<p><img src="/img/bVbJlvo" alt="图10.jpg" title="图10.jpg"><br>图10. 血氧动力学函数描述了在单一刺激下BOLD 信号的随时间变化</p>
<p><img src="/img/bVbJlvp" alt="图11.jpg" title="图11.jpg"></p>
<p>图11. 利用fMRI采集的脑活动信号来刻画听句子和看句子时的大脑活动强度的不同</p>
<p>功能性核磁共振成像(fMRI)的独特性在于,<strong>比起现有其他大脑功能成像技术,fMRI在识别“认知活动中的大脑”时,不仅时间分辨率更高,就连空间分辨率也可达到毫米水平。</strong>借助功能性核磁共振成像,对大脑的研究便可扩展至记忆、注意力、决定做出过程,意识,认知障碍等。在某些情况下,fMRI技术甚至能够识别研究对象所见到的图像或者阅读的词语。</p>
<p>fMRI是一种没有放射性、无创性的检测脑功能动态活动的手段,一次成像可以同时获取功能和解剖图像, 已经被广泛应用于脑的基础研究和临床治疗。利用fMRI,可以对脑功能激活区进行准确的定位。利用静息态功能性核磁共振成像 (Resting-State fMRI)还可以研究不同脑区之间的功能相关性 (functional connectivity)。脑部在静息状态下自发的低频活动的同步化现象广泛存在于听觉、视觉,工作记忆,和执行系统中。许多可重复的研究已经揭示了大脑存在多个即相对独立又相互连接的感知和高级功能执行系统。这些系统的自我状态和相互连接极大程度上和人体的健康和疾病状态相关。</p>
<p><img src="/img/bVbJlvv" alt="图12.jpg" title="图12.jpg"></p>
<p>图12. 利用静息态 fMRI的区域相关性刻画大脑的内在基本网络系统[Raichle 2011]</p>
<p><strong>此外,fMRI与弥散张量成像 (DTI) 和我们之前介绍的脑磁图 (MEG),经颅磁刺激 (TMS) 等技术相结合,可得到更多的脑功能活动信息。</strong>弥散张量成像可在三维空间内定量分析,无创跟踪脑白质纤维束,fMRI与弥散张量成像技术可以建立激活区域的功能连接网络图,有利于解释结构与功能之间的关系。脑磁图反映神经细胞在不同功能状态下产生的磁场变化,可以提供脑功能的即时信息和组织定位,fMRI与脑磁图技术相结合可以弥补其时间分辨率的不足,可解决脑区域性活动的时间问题;经颅磁刺激可以无创地在皮层产生可传导性电流,从而对刺激位点或有突触联系的皮层兴奋性产生抑制或易化,通过整合fMRI的结果,可以应用于脑损伤和其它疾病的功能神经外科手术。随着fMRI和图像后处理技术的不断改进和完善、高磁场强度MRI的发展,能够使fMRI试验的可重复性和空间定位的准确性大大提高,在脑神经科学、认知和心理等方面的临床和基础研究中的应用将更加深入与广泛。</p>
<h2>参考文献</h2>
<ol>
<li>Ogawa, S., Lee, T.M., Nayak, A.S., and Glynn, P. (1990). Oxygenation-sensitive contrast in magnetic resonance image of rodent brain at high magnetic fields. Magn Reson Med 14, 68-78</li>
<li>Bandettini, P.A.; Jesmanowicz, A.; Wong, E.C.; Hyde, J.S. Processing strategies for time-course data sets in functional MRI of the human brain. Magnetic Resonance in Medicine. 1993, 30 (2): 161–173.</li>
<li>McRobbie DW, Moore EA, Graves MJ, Prince MR (2007). MRI from Picture to Proton. Cambridge University Press. p. 1. ISBN 978-1-139-45719-4.</li>
<li>Raichle, M. E., MacLeod, A. M., Snyder, A. Z., Powers, W. J., Gusnard, D. A., & Shulman, G. L. (2001). A default mode of brain function. Proc Natl Acad Sci U S A, 98(2), 676-682.</li>
<li>Raichle ME. (2011). The restless brain. Brain Connect, 1(1), 3-12. doi: 10.1089/brain.2011.0019</li>
<li>Bernard Baars,Nicole Gage. Cognition, Brain, and Consciousness - Introduction to Cognitive Neuroscience, 2nd Edition, Academic Press, February 2010</li>
<li>Gary H. Glover. Overview of Functional Magnetic Resonance Imaging, Neurosurg Clin N Am. 2011 Apr; 22(2): 133–139. doi: 10.1016/j.nec.2010.11.001</li>
<li>Abi Berger. How Does It Work? Positron emission tomography, BMJ. 2003 Jun 28; 326(7404): 1449. doi: 10.1136/bmj.326.7404.1449</li>
<li>
<a href="https://link.segmentfault.com/?enc=oUa08TKIwGX2RXYLxzLglA%3D%3D.3UP%2B3x8d7L%2BkFDOXaA%2FHjgdNv9mD9dBsLanfT3YUfPRSUM7HJLPsTxFsuH292TynWiVgfRmROelNjToWg%2FVs3LyQdxpLC9k0pdaqdsngxX4w2bljY9MG9wfyHs8rLDeZ" rel="nofollow">https://zh.wikipedia.org/wiki...</a>://en.wikipedia.org/wiki/Magnetic_resonance_imaging </li>
</ol>
<ul><li><strong>招聘信息</strong></li></ul>
<p> 好未来技术团队正在热招前端、算法、流媒体后台开发等各个方向高级开发工程师岗位,大 家可扫描下方二维码或微信搜索“<strong>好未来技术</strong>”,点击“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<ul><li><strong>也许你还想看</strong></li></ul>
<p> <a href="https://link.segmentfault.com/?enc=jiF60HoxzHFGCkYi%2F8ixHQ%3D%3D.ab%2FENk8NmUPkj%2Fv7UmJ3wN4z%2FymhFeknj2Pm9u%2FLqeaGCNINrqOnY%2FZm%2Ffs5aDyiUWnFBGIJh3J%2FZUTVk5ARy6FNzgEUbOQuNydmU6iVVhqjzSrxRoEN5ah%2FmWBYDNL0pYmT3pN2LzM%2BxDRiR1tW1amms5N63%2B45QnnmB%2FVdJipsqW1NeufV7cTIcje%2BFLhY2phd5gORpqf4WwHcu%2B%2FnP7B6W1c1zRp2MCyQU9FfedI%2F8BoVyiA793Sw7cOAXrTYmcpt5gCMSXecYwYmqnk7BdRRjM4qbukXvZD3bB0AlE%2FZeRD1WfriNVf7HeG4Zc%2BQ" rel="nofollow">DStack--基于flutter的混合开发框架</a></p>
<p> <a href="https://link.segmentfault.com/?enc=p6p9BgwtaJjuUW3E2%2FPgdg%3D%3D.o4GTo%2F2hAIMMI0cFvqGrqSSXqrs6vbtRVdbUbGvs3WOdl%2FCpATjAXOHKewRT7QxypfozD7vXEbvbAyHnkjr0yweFyw3wUeInl%2Ft%2BLniVqxSI8BbdNHBg%2BGaondABSqSRDwYVsjoBU8KvKuaemQdc3%2F4FO%2BflXilU64XMxpQy8HUfdoHd8Sc%2F2GQEbpCC%2FCHQWzqgAvrKBGYQ6Z%2FlqwI70qagVY4VJ7JHxHo4rRiLXGNJKI%2FulFWo5eVOtwaE9PtsbxsYIlK2PyloFv2sH%2BdevR%2FYcAN%2FfxzLuM8I%2Bi4Ma1LQSIsvZgCXcUbQCbQ29EAk" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p>
<p> <a href="https://link.segmentfault.com/?enc=m3cwPcmN7acmJvGj3w8joA%3D%3D.8PgL7ZByijpEqn60MCtPcC5VnxJHvLtL2ixI9xakJnlSrvSSUNSzJj%2BcU23HXUzEOEejlHrZ0xttXUSkZFi4z0u98qNDoibUbSQ0H7FsANohwLEI%2BRCEpCVtDTKtT%2BJZCRO0pKMo5GN0FfEWpSar8VQxEGzJl%2FHBXoQ4Ap32Ulu%2BVveIQGH9YgHmMkxaHr18AtttVO1HoGUUv6Rha2zmOcAY275qKj1kvOahbRiHTUrokapn8s0xidA6fmLjcDsZSMZsQWTYCnH0yB61T%2FoxW6fIvL%2F4nRQRG0I3tML%2BtdWw3LOxNCU0cUmoi3EV8mXq" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><img src="/img/bVbG6Nn" alt="公众号底图.png" title="公众号底图.png"></p>
脑电硬件丨人类如何观测大脑?脑成像技术概览(上)
https://segmentfault.com/a/1190000023083425
2020-07-03T12:46:29+08:00
2020-07-03T12:46:29+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<ul><li><h2>前言</h2></li></ul>
<p>人类的大脑只有大概1400克左右的重量,但却构成了这个世界上最为复杂、精密的机器。大脑在结构上是一个由神经突触联结而成的神经元网络,包含了百亿级的神经元和百万亿级的神经突触连接。大脑在功能上极具多样性,我们所有的智能活动,如注意、学习、记忆、沟通,情感和决策等,都依赖于大脑神经细胞有组织的活动和功能。时至今日,人类对大脑的核心功能,如意识的产生,情绪和情感,记忆和创造能力等依然知之不多。意识的生物学基础更是位列《科学》十大人类未解答的科学问题。鉴于脑科学研究在科学、经济、社会和军事领域的重大价值和意义,美国、欧盟和日本都先后出台了“脑计划”,旨在探索人类大脑工作机制、绘制出人脑活动图谱,了解人脑的运行机理,从而取得脑与认知科技的战略制高点,为医疗,认知科学,人工智能,以及新兴产业的发展提供关键基础和支撑。 </p>
<p><img src="/img/remote/1460000023083428" alt="" title=""></p>
<p>图1. 神经元和神经元的链接</p>
<p>大脑的认知规律和教育与学习的联系是脑科学实验室重点关注的研究方向。<strong>脑科学的发展为我们认识理解学习机制和过程提供了重要科学手段。</strong>学习和认知发展最终体现在大脑神经活动层面上变化,本质上就是对脑的再塑造。对脑发育的研究,对学习过程中脑机制的研究,对发育障碍机制和干预的研究,这些科学成果都能够为改进和完善教育方法、人才培养机制提供证据和指导。</p>
<ul><li><h2>主流的脑成像技术</h2></li></ul>
<p>要想研究大脑的发育和活动规律,就不得不提到用于观察大脑的仪器和脑成像技术,例如我们经常听到的脑电,CT、核磁等。<strong>一些在医疗和科研上应用比较广泛和成熟的脑成像技术包含了以下几种:</strong></p>
<ul>
<li>EEG:脑电图</li>
<li>MEG:脑磁图</li>
<li>TMS: Transcranial Magnetic Stimulation 经颅磁刺激</li>
<li>ECoG: Electrocorticography 皮层脑电图</li>
<li>LFP: Local Field Potential 局部场电位阵列</li>
<li>fNIRS: Functional Near-infrared spectroscopy 功能性近红外分光光谱成像</li>
<li>PET: Positron Emission Tomography 正电子发射成像</li>
<li>MRI: magnetic resonance imaging核磁共振成像</li>
<li>fMRI: functional magnetic resonance imaging 功能性核磁共振成像</li>
</ul>
<p>这些主流的脑成像技术可以按它们在<strong>时间、空间分辨率</strong>上的特性来区分。时空分辨率往往和某一种脑成像技术的功能特性直接相关。而在<strong>测量性质</strong>上这些脑成像技术又可以分为侵入式测量,非侵入式测量,经颅磁刺激,和需要示踪剂注射等。 <img src="/img/remote/1460000023083430" alt="" title=""></p>
<p>图2. 常见脑成像技术在时间和空间分辨率和测量性质上的特性</p>
<ul><li><h2>脑成像技术概览(上)</h2></li></ul>
<p><strong>在本系列文章里,我们将对主流的脑成像技术做一简要的介绍。</strong>在本篇文章里,我们首先来关注脑电图(EEG),脑磁图(MEG),经颅磁刺激(TMS),和功能性近红外光谱成像(NIRS) 技术。</p>
<h3>一、脑电图(EEG)</h3>
<p><strong>1929年德国神经精神病学家Hans Berge首次记录到大脑神经元的动作电位在人体头部表皮产生的电信号。</strong>此后,他的研究成果不断得到电生理及神经生理学家的证实,使得EEG学得以发展,沿用至今。</p>
<p><img src="/img/remote/1460000023083429" alt="" title=""></p>
<p>图3. Dr. Hans Berge和他的脑电记录设备以及最初发表的脑电信号 </p>
<p>脑电信号源自于由神经元放电产生的uv级别的电流,其传导到头皮要经过软脑膜 、蛛网膜下腔、蛛网膜、硬脑膜、颅骨,头皮,所以非常容易受到干扰。通过精密的电子仪器,我们可以在头皮上将神经元集群产生的生物电位加以放大记录而获得脑电信号。大脑神经元的电活动具有自发性、节律性和综合性的特点。<strong>脑电主要是通过波幅、潜伏期和电位变动或电流的空间分布等指标来提供大脑工作过程的信息</strong>,在医疗和科研上有广泛的应用,比如对睡眠、昏迷、麻醉中意识变化的监测,理解不同认知任务时大脑的活动规律,情感计算等。</p>
<p><img src="/img/remote/1460000023083431" alt="" title=""></p>
<p>图4. 脑电信号的产生及人类头部皮层结构示意图</p>
<p>需要强调的是,脑电技术虽然是一门古老的脑成像技术,但依然是当今最具生命力的主流脑成像技术之一。这主要得益于相比其他脑成像技术,<strong>脑电设备的便携性好,使用成本低,无创,高时间分辨率,和丰富的频率信息等显著优势。</strong></p>
<p><img src="/img/remote/1460000023083433" alt="" title=""></p>
<p>图5. 心理学研究中涉及脑电信号应用的研究占比</p>
<p><img src="/img/remote/1460000023083432" alt="" title=""></p>
<p>图6. 脑电在脑机接口相关研究中的应用占比</p>
<h3>二、脑磁图(MEG)</h3>
<p>在物理学上,我们知道变化的电场产生磁场,两者可以相互转换。大脑的活动直接体现的是神经元动作电位的变化,这是一种电信号。脑电设备采集的就是神经元电活动在人头部表皮形成的动态电场变化。同样,变化的电场在人的颅脑周围产生着磁场,称为脑磁场。这种磁场强度很微弱,需要建立一个严密的电磁场屏蔽室,将受检者的头部置于特别敏感的超冷电磁测定器中,<strong>通过特殊的仪器可测出颅脑的极微弱的脑磁波,这样形成的多通道信号阵列便称作脑磁图。</strong></p>
<p>尽管脑电信号和脑磁信号同源于大脑皮层神经元的电活动,两者之间还是有差别的。脑磁信号主要源于神经元细胞内电流产生的磁场,而脑电信号来自锥体细胞产生的兴奋性突触后电位。从信号产生的空间特性上讲,脑磁图检测的是脑沟内锥体细胞产生的磁场, 而脑电图检测的是脑回内锥体细胞电活动。 </p>
<p><strong>脑磁信号最显著的一个优点是在传导过程中介质的影响小,不受颅骨的影响,抗干扰性强,信号没有扭曲,空间分辨率高。</strong>通过与MRI影像融合,利用脑磁信号可对信号源进行精确定位。脑电信号则受介质的影响大,空间分辨率低,定位能力较差。 </p>
<p>脑磁信号探测的缺点在于信号强度随与发生源距离的增加而迅速衰减,<strong>所以脑磁图很难探测大脑深部的磁信号,而脑电图则有探测大脑深部的电活动的能力。</strong>此外,脑磁图设备昂贵,故而对环境要求苛刻,需要建立专业的实验室和脑磁设备操作团队来从事这方面的学术研究。相比之下,脑电图的获得相当廉价,对环境要求相对宽松,在科研和商业市场中得到了广泛的实际应用。</p>
<p><img src="/img/remote/1460000023083435" alt="" title=""></p>
<p>图7.脑磁图设备和脑磁信号分析</p>
<h3>三.经颅磁刺激(TMS)</h3>
<p>经颅磁刺激(TMS)是一种利用脉冲磁场,作用于大脑中枢神经系统,改变大脑皮层神经细胞的膜电位,使之产生感应电流,影响脑内代谢和神经电活动,从而引起的一系列生理、生化反应的磁刺激技术。经颅磁刺激具有无痛、无创的物理特性,提供了探索脑功能及高级脑认知活动规律的一种高级手段,与PET、fMRI、MEG并称为“二十一世纪四大脑科学技术“。</p>
<p><img src="/img/remote/1460000023083434" alt="" title=""></p>
<p>图8. 经颅磁刺激(TMS)原理示意图(左)和实际实验图(右)</p>
<p><strong>经颅磁刺激(TMS)也可以是一种无痛、无创的绿色治疗方法,可以通过不同频率的磁刺激来达到治疗目的。</strong>例如高频(>1Hz)主要是兴奋的作用,低频(≤1Hz)则是抑制的作用。 TMS不使用电极,不用直接接触人体,相对电刺激是一项无创且简便的技术。通过神经网络之间的联系和互相作用,重复TMS 刺激产生的效应可以对多个脑部位功能产生影响,从而达到治疗效果。对于不同病人的大脑功能状况,需用不同的强度、频率、刺激部位、线圈方向调整来取得最佳的治疗效果。经颅磁刺激(TMS)正在临床精神病、神经疾病及康复领域获得越来越多的应用和认可。经颅磁刺激(TMS)不仅是一种刺激技术,还是一项大脑神经调控技术,给临床治疗和科研创造了广阔的空间,在未来还会有更多的新用途被开发出来。</p>
<p>图9展示的就是一种利用经颅磁刺激(TMS)+高密度脑电阵列研究在睡眠和清醒状态下头脑皮层对信息处理不同的科学实验设计。可以看到,在premotor cortex 施加TMS刺激(图中圆圈处)引发的头皮电流强度的峰值(图中+所示)在时间和空间维度上在两种状态的分布是明显不一致的。在清醒状态下,大脑皮层对外部刺激所带来的信息会表达为涉及多个不同脑区的激活和互动,这可以被理解为对信息的代表和综合 (information and integration)。而在深度睡眠状态,同样刺激引发的反应只是集中在刺激施加脑区,且随时间呈单调衰减趋势。这证明在睡眠和清醒这两种意识状态下,大脑活动对信息的表达和综合/集成特性是不同的,这样的实验结果提示我们可以利用信息论的数学工具来对不同意识状态下的脑活动的特性加以表征和刻画。</p>
<p><img src="/img/remote/1460000023083436" alt="" title=""></p>
<p>图9. 使用经颅磁刺激(TMS)+高密度脑电阵列探测睡眠时人脑的活动规律和特点</p>
<h3>四.功能性近红外分光光谱成像 (fNIRS)</h3>
<p>大脑的工作依赖于血液的新陈代谢为神经元活动提供所需的氧。氧的消耗又刺激大脑局部血管的舒张,导致局部脑血流和脑血容的增加,表现为大脑血氧水平的迅速提高,<strong>这就是神经与血管匹配的机制 (Neurovascular Coupling)</strong> 。在这个机制的作用下,驱动某一种认知活动的大脑神经活动区域的血氧含量水平将大大超过大脑活动所需的氧。氧是通过血液中的血红蛋白进行传输,因此,在认知活动过程中,大脑活动区域会出现血液中氧合血红蛋白浓度的上升,脱氧血红蛋白浓度的下降。<strong>功能性近红外分光光谱成像(fNIRS)和功能磁共振成像技术 (fMRI) 等脑成像技术,</strong><strong>都是利用认知活动中脑局部的血红蛋白浓度的变化导致的光学或磁性变化来获得与大脑功能相关的脑活动信号。</strong></p>
<p><img src="/img/remote/1460000023083439" alt="" title=""></p>
<p>图10. 功能性近红外分光光谱成像原理和物理实现</p>
<p>功能性近红外光谱技术利用了血液的主要成分对6000-900NM近红外光良好的散射性,从而获得大脑活动时氧合血红蛋白和脱氧血红蛋白的变化情况。成像装置一般由光源,光源探测器、数据采集器等组成。光源通过发光二极管或者是与被试头型匹配起来的光纤束向特定大脑区域发射近红外光,光以香蕉型的路径进行散射,离光束 2-7cm 的光源探测器可以收集到被组织散射回来的光。当光源和探测器的距离设置在4cm时,fNIRS 信号对皮层表面 2-3mm的血氧血红蛋白散射的光最为敏感。</p>
<p><img src="/img/remote/1460000023083438" alt="" title=""></p>
<p>图11. 功能性近红外分光光谱成像实际实验室实现</p>
<p><strong>功能性近红外光谱脑成像技术主要应用在自然情境下高级认知、 发展 、心理学、 异常心理学等多个领域的研究。</strong> fNIRS的研究可以与fMRI等其他成像技术进行结合,开展婴幼儿和特殊人群的认知神经科学研究以及自然情境下大脑认知的神经机制研究。近红外光谱脑成像技术的一个显著优点是无噪音、 无创性和对实验过程中被试动作不会过份敏感,所以抗干扰性强,对自然场景中的应用有相当大的潜力。但近红外光谱脑成像技术也存在空间分辨率不高和校正算法有待进一步完善等方面的不足。另外,比起正在兴起的可穿戴式脑电设备,近红外光谱成像的硬件设备的便携性和可实用性仍有明显差距。</p>
<h2>小结:</h2>
<p>在本篇文章里,我们简单回顾了脑电图(EEG),脑磁图(MEG),经颅磁刺激(TMS),和功能性近红外光谱成像(NIRS) 的脑成像技术。这些脑成像技术各具特点,优势和不足。在脑科学研究中经常可以见到脑成像技术组合式的应用。对于在教育领域的实际应用,<strong>当前正在发展中的可穿戴式脑电设备是最具潜力进入真实教育场景的脑成像技术门类,</strong>因为它在便携性,成本,无创,时间分辨率和频谱信息方面具有巨大的优势。<strong>在接下来的一篇里,我们将进入核脑影像成像技术的世界(例如PET,MRI等),</strong>去领略那里丰富的物理世界,技术特点,和发明者们令人赞叹的奇思妙想。</p>
<h2>参考文献</h2>
<p>1.Ball, Philip. "Brain Imaging Explained." Online at <a href="https://link.segmentfault.com/?enc=daHEQjoleIqbx1YGYg68iw%3D%3D.DfeXFZ8VGLfOqrNi6ZXSAjrXABWja1o3f80oC69vjEuhnkO4Lm4RHOp%2B7iPQzBhT" rel="nofollow">http://www.nature.com/nsu/010712/010712-13.html</a></p>
<p>2.雷旭,尧德中,同步脑电-功能磁共振(EEG-fMRI)原理与技术, 科学出版社,2014-03</p>
<p>3.经颅磁刺激技术,心里学科知识2012-010-8Hudspeth, A. J., Jessell, T. M., Kandel, E. R., Schwartz, J. H., & Siegelbaum, S. A. (Eds.). (2013). Principles of neural science. McGraw-Hill, Health Professions Division.</p>
<p>4.Philip Ball. Brain Imaging Explained.</p>
<p>5.Bernard Baars,Nicole Gage. Cognition, Brain, and Consciousness - Introduction to Cognitive Neuroscience, 2nd Edition, Academic Press, February 2010</p>
<p>6.Massimini M, Ferrarelli F, Sarasso S, Tononi G, others (2012). Cortical mechanisms of loss of consciousness: insight from TMS/EEG studies. Arch Ital Biol, 150 (2-3), pp. 44–55.</p>
<p>7.<a href="https://link.segmentfault.com/?enc=pNNmR%2BCTu4fVUsiskkOb1Q%3D%3D.wL4EZkkDpSvte65UJwnDG0EwRO1AVN3H8h7vJ3XSlZeJQqxxaMZsBnL2Mwn4GRowAjUViTWwFLlbkkzeSxX7klRpNaIHxaKMGvZNeOGe6qg%3D" rel="nofollow">https://en.wikipedia.org/wiki/Functional_near-infrared_spectroscopy</a></p>
<p>8.<a href="https://link.segmentfault.com/?enc=9pjHeMnDOatDxr3SUD38Aw%3D%3D.WBdDf39NfenOU%2Bm4d2VslmptqaLM94OCqIeYdKDqZh0MzMS%2F1luwaP%2B%2BkuLqMKru%2BhthubUIl12Gzc435AMSWjuRvzh84edmkae%2BoH7xHOCPWQGwHnhSBvjzNnC37bW5%2FXvLuooAmnR5%2FgqfmObTaXmqKN87kEwfGeH0nHEy9j95t%2B8Fp1qLPGok21BnvX04" rel="nofollow">https://baike.baidu.com/item/脑磁图技术</a>9.<a href="https://link.segmentfault.com/?enc=zt9Egaj2OJ16tuERyRFjtw%3D%3D.%2BK2Zm%2Bb95RpUed7XWEvftRTk79ZQSUquHF25z07H4eTl0Maz1qe8UepZW6xLBEruM02EwPGT0cjfGJpNiGeozhBX4CwZi%2F5iLGYRwKfGk2diY2HQcH3zgWB33Q8MuskKKXnYH11%2Bf3722v6t%2ByLr%2BAHWHbrLqg33t6t%2BM%2BcDeVrlqr4HLQnWbIY1rs%2BmbWrs" rel="nofollow">https://en.wikipedia.org/wiki/Magnetic_resonance_imaging</a></p>
<p>10.<a href="https://link.segmentfault.com/?enc=6%2B9wb89NqBsc0ZPM%2BNBl2w%3D%3D.FrYFCTxDVRZTT1jlcfYdeRNc0CFxjID7P%2BULiQktUDApT%2B9LB2RaFpl7x%2Fv%2B7XBV0h1cdF%2FVjSiA8bFWLuCaHw%3D%3D" rel="nofollow">https://en.wikipedia.org/wiki...</a> </p>
<p><strong>招聘信息</strong></p>
<p>好未来技术团队正在热招前端、算法、流媒体后台开发等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索关注“<strong>好未来技术</strong>”,点击本公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=17yv9t8s2y4AFevN4%2Fec1g%3D%3D.ivYmVnZcOjIfWKVOBpq4v%2Bzx6qWl2xgSDkILC3ie3%2FDla6Bf9VNXcJpZIAxVyALLJeZB5ZkV2Phomvv9NqsUb1HSewErqOe9Vn%2BuTru0Xh8uPQizBz4ta3vrwoGc4SipTJdp1SQyxF%2FFEkytNkwnVPyqscDGmtupeBmNp6Ck3zT0krWmvS%2BLs3edRJGmp2FgHSRmQcQCL%2FEaYW74sPx5xPV1kLUmkGhjvQh8nUuQiRMwdoYQpSqihguOlflDwB6SZQtGRgWTMWdsDltH%2B%2BWbRHCXhw5btZ2vds9f%2BtMAvxSjKhxPpy8CSsK%2F0RHBTjg1" rel="nofollow">DStack--基于flutter的混合开发框架</a></p>
<p><a href="https://link.segmentfault.com/?enc=ue5Zn3bwydswmW%2Ff2YGEvQ%3D%3D.MeaeZIXWog9t8ZBCu6T39AtfFU7XB6SebHZY8FIhVOX2jdsSgE4PjEQ%2FH24zGffuWrNWmoeCZI%2BmDgNe4dauavJgrPWpNonmx%2Bh4q5KO%2F4oXhTSqN3nRq0WtcpNjJbUpdtCFkHrVWbTw1MVmYUg8VHc41%2BWxpGZagsilxvVdMiQ5gUoUnJ3fpHNL0CCR9%2B0rM%2Fnt3GZ9wmQThqd2nVw3DSmVHVVhho1qyNCPkGCQ3E%2FOk5uB5PVUJ75sLomdnWvse3FrFjc4vEFg96j4vUwGhg6XzVnW6Deis4p3tgOiL9%2BugeiMNuEy5pRGHP5FjilX" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p>
<p><a href="https://link.segmentfault.com/?enc=lm4okHcjoTGbxhCEDqj0EA%3D%3D.p9XTolnkDB97Da3LmkCHx8fCbfjTFMH5KAWHtXw3DglozkwBtlRJbq2fzQa8jn8cfL8%2FYP5uCkLD8zoCIpG1AZjCt798n2fQ6pzS39KdpcDI%2FWOLyZkoL2sMuQvMxnIdKwcdMFL%2FP155bXFwJHl9jWN0lTpI%2FZYfXHRAsJlIsVYW13bja961ZXqT%2F%2BhSpf2wk7baRcIEG3yhTiEiAtLNlV6a%2Bk9MkGsCbJcmePb%2FUQcdrghtC9EDH%2F09Z1KC3gH%2BVworskuLkTYgIgVBTYoWKJbeGHzrRkmpcFQLa%2F4nRo7YIaCTaBTAVwaqfybJC3Lx" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><img src="/img/remote/1460000023083437" alt="" title=""></p>
虚拟现实与增强现实的基础原理及应用
https://segmentfault.com/a/1190000022976161
2020-06-19T16:03:04+08:00
2020-06-19T16:03:04+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<h2>什么是虚拟现实?</h2>
<p>虚拟现实(VR)是利用计算机技术创造一个模拟的生态环境。与传统的用户界面不同,VR将用户放置在场景中体验。用户不是在自己面前观看屏幕,而是沉浸在其中,能够与3D世界进行交互。通过尽可能多的模拟感官,如视觉、听觉、触觉、甚至嗅觉,看世界的眼睛。虚拟现实体验的现阶段限制是内容的可用性。</p>
<h2>虚拟现实和增强现实有什么区别?</h2>
<p>虚拟现实增强现实是同一枚硬币的两面。你可以把增强现实想象成现实世界中一只脚的虚拟现实:增强现实模拟真实环境中的人造物体;虚拟现实创造了一个居住的人工环境。</p>
<p>在增强现实中,计算机使用传感器和算法来确定摄像机的位置和方向。然后,AR技术将3D图形从摄像机的角度呈现出来,将计算机生成的图像叠加在用户对真实世界的视图上,达到增强的效果。</p>
<p>在虚拟现实中,用户的眼睛位置不是在物理环境中定位,而是在模拟环境中定位。如果用户的头部转动,图形就会相应地做出反应,然后通过3d引擎生成对应的效果环境。虚拟现实技术不是合成虚拟物体和真实场景(合成虚拟物体和真实场景在用户无感知的情况下被称为 混合现实MR),而是为用户创造一个令人信服的、交互式(感知手套等等)的世界。</p>
<h2>虚拟现实技术</h2>
<p>虚拟现实中最容易识别的组件是头挂式显示器(HMD).人类是视觉生物,而显示技术往往是沉浸式虚拟现实系统与传统用户界面之间最大的区别。例如,洞穴自动虚拟环境在房间大小的屏幕上主动显示虚拟内容.虽然对大学和大型实验室的人来说很有趣,但消费者和工业上的可穿戴设备的不方便性和价格现阶段还在优化。</p>
<p>带着多种新兴硬件在软件选择方面,可穿戴设备的未来正在发展,但仍是未知的。HTC、Oculus Quest和PlayStation, VR等概念正在引领潮流,但谷歌、苹果、三星(Samsung)、联想(Lenovo),华为等厂商可能会以更高层次的沉浸感和可用性令业界大吃一惊。无论谁走在前面,只要买到头盔大小的设备,就可以在起居室、办公室或工厂的地板上工作,这使得HMD在虚拟现实技术方面占据了中心地位,今年如果华为vr glass的意见开始引领虚拟世界的进程。</p>
<h2>研究方法</h2>
<p>如何获得图像中物体的深度?</p>
<p>主要问题是能否可以在立体图像中找到相对应的点。那么,什么是立体图像呢?当一个图像被称为一组或一对立体声图像,只要它是通过在不同位置安装的多个摄像机同时为同一对象或目标提取的的图像。对应的点是什么?它们是物体在3D空间中不同位置的某个点的相对投影。对图像中两个对应点的位置差称为视差。这种差异与空间中相应点的位置、相机的方位和物理特性有关。如果摄像机的参数已知,则可以根据图像计算物体的深度。首先,我们解释了空间中的点是如何投影到图像平面上的。假定任意点的坐标值在相对于ccd中心的空间中,它被想象成投影后在图像中的点。它相对于图像中心的坐标值是,而图像相对于ccd坐标值的中心点是,在哪里从CCD的中心点到传感场的距离。立体视觉图像机制空间的概念视图是立体视觉系统空间的概念视图。本研究以立体视觉系统为主轴。用两个镜头来确定图像的深度。这部分是单目视觉无法实现的特点。那么通过使用两眼聚焦函数计算相应的角度以获得目标物体的图像深度,立体视觉是增强所必需的:</p>
<h3>1. 眼睛识别分析</h3>
<p>眼睛识别一般包括预处理、特征提取、样本学习、识别等。实现眼球识别技术的方法有以下几种。</p>
<p>(1)<strong>投影法</strong>:在投影法中,根据投影图像在其中一定方向上进行的分布特征,而对眼睛位置进行检测。投影法是一种统计方法,它利用眼部的灰度信息,分别通过水平投影和垂直投影来检测瞳孔的纵横坐标。所以,可以才能对人眼进行准确的定位。</p>
<p>(2)<strong>强积金和方差投影函数(VPF)-Hough变换方法</strong>:Hough变换是图像的基础算法之一,是将图像从空域变换到参数域。图像中的曲线以大多数边界点所满足的某种参数形式而表示。这个瞳孔被用作标准圆。通过圆的标准方程,通过Hough变换可以精确地定位眼睛瞳孔的位置:。由于表观几何解析性,Hough变换大大提高。</p>
<p>(3)<strong>AdaBoost分类方法</strong>:AdaBoost算法是机器人学习领域中一种高效的迭代运算算法。它针对同一个训练集训练不同的弱分类器,然后将这些弱分类器集合起 来组成一个强分类器。该算法具有分类精度高、人眼识别速度快等优点。然而,这类算法的有效性取决于分类器的选择。在快速人眼检测方面有着非常重要的应用。</p>
<p>(4)<strong>样本匹配方法</strong>:根据瞳孔形状使用圆形样本,在图像窗口从左到右、从上到下动态搜索瞳孔的位置。样本匹配从较大的图像中搜索小图像。通过对样本和匹配区域的相似度计算,以最相似的位置作为匹配点来识别目标位置。样本匹配算法属于机器人学习领域的范畴,是一种有效的眼睛识别算法。</p>
<p><img src="/img/remote/1460000022976164" alt="" title=""></p>
<p><img alt="" title=""></p>
<p> 神经网络应用训练图</p>
<h3>2.边缘检Sobel</h3>
<p>obel算子是差分运算和低通运算相结合的结果。它除了具有降噪的优点外,还具有边缘检测的效果。由于导数可以降低噪声强度,所以Sobel算子对噪声的滤波尤为有利。Sobel算子掩码的导数用下列公式表示:</p>
<p>所述方法的输入是对应于1280×1024 RGB彩色图像的深度图像,即立体视觉可以提供的信息。肤色将从产生的最大肤色,色斑进一步检测。从保守估计出发,计算眼睛空间扩展的方法包括一个圆形掩模展开,其半径为。鉴于先前对估计的3D位置的跟踪,肤色的3D点在预定的深度范围内(25毫米)。估计是保留的,而其他深度设置为零: 借用下网上图片这个大概就是看到的效果。</p>
<p> <img src="/img/remote/1460000022976165" alt="" title=""></p>
<p>全局优化:更新方程,重新评估每个像素的速度和位置: </p>
<p>对该目标函数进行了优化,并假设一帧进行眼定位。因此,这种方法和跟踪人的眼睛所必需的序列优化的获得了每个点的特征值。</p>
<p>空间连续性取决于期望观测运动图像的采样频率。</p>
<h2>虚拟现实与音频的重要性</h2>
<p>令人信服的虚拟现实应用程序需要的不仅仅是图形。听觉和视觉都是一个人的空间感的核心。事实上,人类对音频信号的反应比对视觉暗示的反应更快。为了创造真正的沉浸式虚拟现实体验,准确的环境声音和空间特征是必不可少的。这些都为虚拟世界提供了一种强大的存在感。要体验双耳音频细节,进入虚拟现实体验,戴上一些耳机和修补这个音频信很久以前。</p>
<p>虽然视听信息最容易在虚拟现实中复制,但积极的研究和开发工作仍在其他感官中进行。像全方位跑步机这样的触觉输入让用户感觉自己实际上是在模拟中行走,而不是坐在椅子上或沙发上。</p>
<p>触觉技术(VRTRIX的数据手套),也被称为动觉或触摸反馈技术,已经从简单的旋转重量“隆隆”马达发展到未来超声技术。</p>
<p>现在可以听到和感受到真实到生活的感觉,以及视觉虚拟现实体验.</p>
<h2>大学教育中沉浸式虚拟现实的应用</h2>
<p>沉浸式虚拟现实提供了一种符合多感官学习风格的现代学习渠道,有时比传统的学习方法更有效也更。然而,有些分析文献指出,没有足够的证据支持将学习方式评估纳入普通教育实践。</p>
<p>在中学后教育系统中采用沉浸式虚拟现实的最有说服力的论据可能是已将这种模拟纳入其课程的现有学科,例如在国外的,外科教育中的全室模拟机器人辅助(da vinci手术)血管内程序。不幸的是,这些模拟血管内程序造成的医疗伤害,由于错误的模拟训练,导致因个别产品责任案件而引起的数百起诉讼。用户手术技能从模拟(da Vinci手术)应用程序转移到现实世界环境的证据数量有时被发现是不够的。</p>
<p>在负担能力方面,将身临其境的虚拟现实纳入专上教育系统,最初受到所用设备成本的限制,但消费者耳机的商业化却大大降低了成本。移动电话技术已经达到了一个水平,沉浸式虚拟现实可以很容易地适应hmd格式,只需使用低成本的google cardboard或Samsung Gear VR耳机即可。。根据2015年美国教育研究中心(ECAR)发布的一项调查,92%的美国大学生拥有能够访问企业级系统和虚拟现实软件应用的手机。</p>
<p>课程如何将身临其境的虚拟现实融入教育课程。它的重点涉及跨学科的考虑,因为沉浸式虚拟现实的适用性广泛的各种学科。核心假设是学生通过体验学习和亲身体验来优化学习和实际技能学习,从而在适用的情况下简要总结身临其境的VR的积极效果。重点是教育及其相关目标,即技能培训,目的是进一步了解沉浸式虚拟现实在高级思维条件下培训用户的潜在能力。</p>
<p>VR整个系统结构的原理图</p>
<p>将三维立体虚拟图像技术与空间规划和场景设计相结合,可以模拟各种天气条件和四季、气候条件和水景、雾效应等的变化。或者,它可以通过模拟计划中的实际情况来改进现有的场景。并对其实用性进行了研究和评价。不仅可以方便地获得各种不同的视觉效果,而且可以有效地降低实验的误码率。并且大大提高了程序的可靠性和现实性。有了这样的技术,教学中很多危险实验、高经费实验、等也可以结合起来来模拟和评估不同的情况设计。</p>
<p>end</p>
<p><strong>作者简介</strong> </p>
<p>杨海旭为好未来高级AR/VR工程师</p>
<p><strong>招聘信息</strong></p>
<p>好未来技术团队正在热招前端、算法、后端等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索“<strong>好未来技术</strong>”,点击本公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=XIo24RENLTMCDxfrBSs1Mg%3D%3D.UPH4PF%2BZJJl41qwHLaB6kuHY546Z2VXbjERKAH3GeJIlieA07oPmSO7Qd7prUXzNnuUROHBFkvFg%2BWEyta2hSoPXqeFwLZAylnwoxOacB3mLVLkgipHyzhWmggBz%2FX8I5rNotVvq%2B6uMQZY5ck0awfVSeifINQCYk4gP0YYrfkyAXq27hJKwChV47mQ4Zrnw4%2F8rZ72BjrOMu4gWlj02VnN9Shq%2FyMSQSDEXbXmp35STZO5MWW81D32sGyhly2j9IyxTrG25G%2FOA1moBTCp9OoRCgQ6UuUlVWJpuPzYDxYgHmyOHu0bfKjug7JnQdV89" rel="nofollow">DStack--基于flutter的混合开发框架</a></p>
<p><a href="https://link.segmentfault.com/?enc=%2B%2FJyIipz3fEP4O%2BBrLdIHg%3D%3D.DXGrEYn12AUX8U%2FwcHgAXptvCDRV%2FHRMRd3Utw7P0zB8RNjh6Wn3TzkZybvXW4R0h094lINtIl3nxUgQaynSrZXP1JSYK1z1KYlUA1aTKzQ2u0S3Yeg3Qil95f%2FswT6UTLIHXpTYbpTllkOFhnUqoBonBuE6XETgD6nPHVusvjiKss2yUq3yiFKLPXpEhsmMR68k4aR%2Bln28BkhRl55g4fVGBA9LNcqMFC0lrxpV4n5%2Fc7obd4rNWBWHA6KqiaU2SNdp8SiRtnBs0hpVRTSnbP%2FsSeFa2PvfKSMXZb2%2BrMDobyWceBLiXi2SrBUediLp" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p>
<p><a href="https://link.segmentfault.com/?enc=mAUSukJ8SytKR6sb7y8ZvA%3D%3D.x64n9yKQ9%2BH3PAf0ejdpjDA8MhikQcbXaUEX3C5xqnmIj5Nmh3sTyyjzrHkoFa42eIXd%2BVtd4jMLOsEkU6FdLGy31CG17G9CZ%2B%2FVVqbsobjgo2M%2FS9qy53i%2BKiR%2B4qXLzZ1Nfbk7uGeEMS8zIsnYvkZN2zpFlMQzoBH4%2FQAuD0GBKYzwGQhuigMhaZMsT9Uk52G%2BRfUwphdO30fdRfc6kW5LN521UAKUAs9wvZmZ4NRw2OBB7krY9jQGPtbj%2F9QnyhejrVuGimTm3P5zFr9It4W5MlQ8S7%2BnEw%2B9PxHga8OQk1h9XQoGUcGwnrTbcWVD" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><img src="/img/remote/1460000022976166" alt="" title=""></p>
揭秘丨大数据时代,数据背后那些事儿你知道多少?
https://segmentfault.com/a/1190000022918315
2020-06-12T18:19:33+08:00
2020-06-12T18:19:33+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<h2>一、前言</h2>
<p>随着大数据时代的到来,越来越多的行业开始注重数据,并且使用数据为业务赋能,数据质量是数据仓库和数据挖掘的基础,也是数据驱动业务的前提,同时数据质量是数据治理建设的重要一环,与元数据管理,数据标准化及数据服务管理等共同构建了数据治理的体系框架,建设一个完整的质量监控平台,需要从监控、标注、流程制度等方面提升信息管理能力,优先解决所面临的数据质量和数据服务问题。</p>
<h2>二、数据质量评估标准</h2>
<p><img src="/img/remote/1460000022918320" alt="" title=""></p>
<p>1、完整性 <br>完整性是指数据的记录和信息是否完整,是否存在缺失的情况。 <br>数据的缺失主要包括库表变更时没有及时同步所造层:记录中某个字段信息的缺失,造成统计结果不准确,所以说完整性是数据质量最基础的保障。</p>
<p>2、准确性 <br>准确性是指数据中记录的信息和数据是否准确,是否存在异常或者错误的信息。 <br>直观来讲就是看数据是否上准确的。一般准确性的监控多集中在对业务结果数据的监控,比如每日的活跃、收入等数据是否正常。</p>
<p>3、一致性 <br>一致性是指同一指标在不同地方的结果是否一致,是否存在较波动。 <br>数据不一致的情况,多出现在数据系统达到一定的复杂度后,同一指标会在多处进行计算,由于计算口径或者开发人员的不同,容易造成同一指标出现的不同的结果。</p>
<p>4、及时性 <br>在确保数据的完整性、准确性和一致性后,接下来就要保障数据能够及时产出,这样才能体现数据的价值。 <br>及时性很容易理解,主要就是数据计算出来的速度是否够快,这点在数据质量监控中可以体现在监控结果数据数据是否在指定时间点前计算完成。</p>
<h2>三、数据监控包括哪些</h2>
<p>数据监控主要包括:性能监控、日常监控、数据对账,其中性能监控主要指:数据的读写,资源队列使用、节点消耗等运维层面的监控,本次主要讨论日常监控、数据对账,</p>
<p>可以从以下几点思考</p>
<p>:</p>
<ol>
<li>监控数据资产质量状态(同步表数据是否一致),为优化数据平台和数仓性能、合理配置数据存储资源提供决策支持;</li>
<li>实现推动数据质量监控预警(提前告知),不仅包括离线,目前在建设的实时也需要提前布局,做到监控预知;</li>
<li>规范问题故障跟踪、Review、后续改进的优化方案,需有计划执行;</li>
<li>由技术检测到业务监督,形成闭环工作流机制,提高整体数据质量,全面提升服务业务水平;</li>
</ol>
<h2>四、技术方案</h2>
<p>对于培优目前的3600多张表,1.5w 个任务,一些业务持续不断变化,我们需要关注哪些点,保障上层业务的稳定性:</p>
<ol>
<li>从业务入手,从中提炼中间层,所使用到哪些表,进行归纳总结,其主要目的:把核心表抽象抽象出来,重点对这些表数据进行监控,核心的业务,比如:选址、渠道、校区等指标所使用到的中间表,尽量做到电话报警,及时反馈,及时处理。</li>
<li>从底层数据展开,因为这一层数据是底层,是重中之重,上层所有的业务,画像,洞察等业务数据都依赖,稍微一个字段变更,都可能会引发故障,所以这一层需监控表、字段的变更。</li>
<li>任务的监控, airflow每天晚上从凌晨开始调度,抽取数据,大批量的数据都开始同步,所以对于核心表进行拆分:做增量同步,减少全量同步的压力。</li>
<li>最后伙伴每周的值周,值周生需重点关注: <br>1.1. 知音楼群中报警 <br>1.2. 赋能群里面反馈问题 <br>1.3. 增加核心表报警数据波动变化 <br><img src="/img/remote/1460000022918319" alt="" title="">
</li>
</ol>
<h2>五、困难点</h2>
<ol>
<li>告警信息太多了,太容易被忽略怎么办? <br>思路:提高告警的准确率,避免无用告警: <br>a: 加入反馈机制,如果告警是正常的,就打上正常的tag,后续告警规则根据反馈进行优化; <br>b: 在报警时,对核心业务报警加上特殊字体标示;</li>
<li>对于指标准确性的思考,通常数据的链路比较长,最终的指标计算完,中间需要经过好几步,怎么保证每个环节都是正确的,且最终结果是正确的? <br>思路:可以对每个环节加监控,从数据量来对比查看(方案1) <br>a: 每一层代码有 Code Review,保证代码逻辑正常</li>
</ol>
<p><strong><em>数据质量监控是一个不断迭代优化的过程,目前我们也是在探索阶段,希望和大家交流和学习,一起做好对数据监控,持续为业务赋能。</em></strong></p>
<p>end</p>
<p><strong>作者简介</strong> </p>
<p>习沛为好未来数据仓库专家</p>
<p><strong>招聘信息</strong></p>
<p>好未来技术团队正在热招前端、算法、后端等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索“<strong>好未来技术</strong>”公众号,点击“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=iRMgkw7F8%2FM4x%2BnaFRGbAQ%3D%3D.mix5IH2Nwbnx6jVkrdfQwnpgb1BDEr3rHigMsS%2B70ycXaZs2XOe%2FqnJhOg0vIHO2cIWgxtmzJyPhOOit7zVV2YpbknP4F%2FTJLNsYLHE0%2BUqw%2FYCMRnAHcIq8ezDQj4hAwoLeMt1KPsy2fhrQDDrIrSZ1KL0LNnXip2cMphs%2BBzykx5OV44O7fBaWxBR2owHPvMve2ifal91AHB0Bs2E7W7kFRrotmcOpiuWu%2BN6hR2fZsOM8vy8dEySCcdKvtgd4fd88xn6mvWtQa8bIpoCPptJvW0T%2BXRfRWAcgz69Ia61KF%2FxKcJ7FQYJd9rIe0cBf" rel="nofollow">DStack--基于flutter的混合开发框架</a></p>
<p><a href="https://link.segmentfault.com/?enc=yk7kLUtceocNpzbyi833zw%3D%3D.mFA8F7woJdcT1a3itgvLnwRzARTmbTm0l2DZQo5%2BUDQow3NELlFl8Yeg1UYmr4G5Ho0%2B1rSybDK35yC7tNDNactVP0glyvj0t%2BlMGxphzeHpfVIib%2BvH6xIVtUxaphUEXeafFWa3A2Cq1H2HIZUE263DdiPLIShv3eg%2FFEzeMkwgqWWn1Tyo2pGzZRthjiWpZTuBeGxAElGjsEo77VFp0DRw7hyPiD71LJ%2Fag2G5CgVVpE7%2BxIabMohr%2FBOU674geU7UZ2WYYEjVOIOE5hRvoQ3qwA5hN8p9KAz1A%2BHokB6Pb6tIdbUvIhGITes5BKsx" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a></p>
<p><a href="https://link.segmentfault.com/?enc=nXkML%2FzMs40t2Z13VNc5iw%3D%3D.iTlnP5EF%2FoBQS9hwYjpyyGlDVTXRhmPsZ7thJdMjXZ3QG8dYDaGX8u635g2kgIrjXwOTL3uoHfaEv0Dk306i4mgK62orMVlOjqZPAKGqr7n4vE8Y9KDGuDaYc0x6xCSKFuYW5RfIaXVunfLVtflgEIOBp0GkPCFKhhIpDCerji80ho0BiIjnxrWAsj%2F0nd1K2YQ80Kyk21HhfSAIhiS%2FXNqIdp4mszI%2Filc%2FihzGkdMn73CvOsWfoWS5zNAi9doBXJRw1M7gxFEEEdyMyX6fltrujUatFqzEyfTe6Jgmts%2BrjBSxvo5gq2NTtie3AjO%2B" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><img src="/img/remote/1460000022918318" alt="" title=""></p>
140M到67M,学而思网校如何在一周内构建一套可持续的瘦身系统
https://segmentfault.com/a/1190000022850638
2020-06-05T16:37:06+08:00
2020-06-05T16:37:06+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
3
<h2>APP为什么要减包?</h2>
<p>APP体积越大推广转化成本越高,因为平台功能众多,学而思网校的APP体积是在144m左右,疫情期间由于公益直播涌入大量用户,转换率上的硬伤更加暴露出来。同时移动部设定了自我突破的若干指标,转换率是关键指标,背负紧急军令我们开始了减包任务,一定要做到70m。</p>
<h2>为什么不用插件化?</h2>
<p>19年团队曾经尝试过插件化技术,经过两个项目试水碰到一系列问题,最终放弃使用插件化,原因如下:</p>
<ol>
<li><p>插件技术原理是通过Hook或者Reflect技术修改系统libs和framework代码,Android系统版本 设备 ROM众多,Hook Reflect很难100%兼容。</p></li>
<li><p>学而思网校平台有20+的二级工程,一个工程变更重新打包时,插件资源id的重新分配,整体工程变更导致20多插件变动需要重新维护,维护人力成本有点大。</p></li>
<li><p>插件技术使用时存在数据传递问题 自定义UI显示问题,权限重复申请等问题。</p></li>
<li><p>插件化的核心是ClassLoader,按照谷歌的文档,最快Android 12将会被限制, 未来有不确定性。</p></li>
</ol>
<h2>减包计划实施难度?</h2>
<ol>
<li>涉及到20+的二级工程 资源类型众多 调用代码分布广泛,要求在底层框架统一实现核心技术。</li>
<li>需要兼容Android4.4到最新的版本系统,同时核心技术兼容后续系统迭代。设备上需要兼容各个手机品牌的高中低,兼容任务繁重。</li>
<li>产品迭代迅速,为了避免后续开发导致APP慢慢滋长,需要设计统一的技术框架保持持久轻量。</li>
<li>总体开发时间一周,测试一周,各个业务线还在并行开发,为了保障时间节点,技术框架需要做到最小的业务代码代动。</li>
</ol>
<h3>减包前APP体积汇总。</h3>
<p><img src="/img/bVbHKZX" alt="ttc.01.png" title="ttc.01.png"></p>
<p>通过数据统计发现,20多个工程的res图片资源 assets的lottie动效资源 libs下的so文件合计约有70m。其他零散的100kb文件有6m左右。20多个二级功能,其资源一次性打进APP里是不合理的,毕竟用户常用的就那么几个。为什么不把资源分离出来托管到云端,使用时再拉取呢?想法很简单,但是面临一系列的问题,我们有6000多张图片,托管CDN的话,业务代码都要修改访问链接不现实。一个想法在内心产生,可以做一个离线附件的技术框架嘛。</p>
<h2>附件框架的方案</h2>
<p>附件框架:开发时资源打进APP不影响业务方开发调试预览;发版时指定的资源统一分离出来托管到云端,进入对应功能前确保资源包下载完了,运行阶段不受影响。文字虽短,框架层需要支持一下特性:</p>
<ol>
<li><p>资源分离需要做到脚本自动化,并且只分离指定目录的资源文件,分离出来的zip应该是多个,并且和20多个工程形成一一对应关系。</p></li>
<li><p>资源下载需要做到按需下载,进入哪个功能下载哪个资源,避免一次性全部下载导致的loading时间太长。为了减少loading出现,需要根据业务权重做后台预加载机制。</p></li>
<li><p>框架层面在保证按需下载的前提下,实现业务层面的统一拦截下载,以避免大量的业务代码修改和调试,做到业务方无感知框架。</p></li>
<li><p>以前资源在APP内,附件框架的资源在下载后,框架代码需要做到全方面的资源访问替换技术,以避免大量的业务代码变动,做到业务层面无感知。</p></li>
<li><p>考虑到存量用户基数大,各个业务版本迭代资源变动小,为了进一步避免或减少loading出现的概率时间,附件框架可以做增量更新技术。保证存量用户更新资源时,资源包体积减少95%。</p></li>
<li><p>20多个离线zip增量迭代10个版本,会产生上百个资源文件,对应的人力维护成本也大。需要配套的自动化附件包发布脚本,一是减轻负重,二是避免人为性失误。</p></li>
<li>框架需要考虑失败重试机制 需要做到多云备份预防网络事故 需要做到内置外置卡双存储避免极端情况。需要完整的日志链条以持续优化。</li>
</ol>
<h2>资源分离技术说明</h2>
<p><img src="/img/bVbHK0c" alt="ttc.02.png" title="ttc.02.png"></p>
<ol>
<li><p>首先规定了附件目录attach, gradle脚本会给每个二级工程生成该目录。业务方只需要把lottie so以及其他大文件移动到附件目录,不需要修改代码。</p></li>
<li><p>Jekins打release包时,分离脚本启用了,gradle脚本会自动遍历二级工程:每个工程res下的图片文件会打到zip,源文件会用xml文件占位替换,每个工程的attach文件会打包到zip中。</p></li>
<li>最终Jekins产生了20+的zip文件,打包完成后命令行运行脚本,自动化发布资源文件到云端。</li>
</ol>
<h2>资源发布自动化技术</h2>
<p><img src="/img/bVbHK0d" alt="ttc.03.png" title="ttc.03.png"></p>
<ol>
<li>批量编译点九图 确保APP使用时无失真拉伸</li>
<li>批量使用熊猫 WEBP技术对图片文件优化 以减少资源体积</li>
<li>自动对比历史版本归档记录 产生对应的增量更新文件</li>
<li>同时发布多个资源包到案例云和腾讯云 双云避免网络事故</li>
</ol>
<p>使用python脚本自动化发布做到人力不及的流程,避免了类似于插件化维护的管理成本。</p>
<h2>抽象统一的下载框架</h2>
<p><img src="/img/bVbHK0e" alt="ttc.04.png" title="ttc.04.png"></p>
<ol>
<li><p>底层框架统一拦截跳转,确定需要进入的二级模块,检查下载对应资源文件,下载后继续跳转。统一实现了20+业务的核心代码,避免业务改动。</p></li>
<li><p>下载环节做到网络错误感知,阿里云腾讯云自动切换,4次失败重试避免网络事故。文件存储时优先内置卡,次要外置卡存储,避免极端的文件读写问题。</p></li>
<li>框架层面统一文件管理,版本迭代管理,避免修改业务代码。同时增量更新确保用户最小的下载量。</li>
</ol>
<h2>资源访问的无缝替换</h2>
<p>附件资源分离做到自动化 发布做到自动化 下载做到了抽象统一。再做到无缝替换技术,基本上业务代码变更就很微小。所谓无缝替换,就是从关键接口层面统一APP内置资源 下载资源的访问。核心技术一处实现,业务代码无需变更。下面列举res无缝替换 lottie无缝替换 Glide无缝替换。</p>
<p><img src="/img/bVbHK0f" alt="ttc.05 (1).png" title="ttc.05 (1).png"></p>
<p><img src="/img/bVbHK0k" alt="ttc.07.png" title="ttc.07.png"></p>
<p>如你所见,无缝替换技术是重写关键接口而非Hook的方式,这让网校APP做到100%兼容;从内核层面进行流替换技术,一处变更全场景生效,避免了大量的业务改动。</p>
<h2>祛除Unity 3D内核的历程。</h2>
<p><img src="/img/bVbHK0t" alt="ppte.jpg" title="ppte.jpg"></p>
<p>在APP多个业务中,互动环节要显示3D粒子效果的机器人,阿丘之类的动画。因为制作3D粒子效果的成本比较大,团队起初定的技术方案是采用Unity 3D渲染模型。发现Unity 3D本身是很出色的特别是对于游戏,但是对于我们网校APP这个大平台而言,却不是那么合身,原因如下:U3D的library bin文件占据着15M的APP体积;U3D是不开源的碰到一个手机崩溃无从解决;载入释放U3D内核内存需要5秒产品体验差;使用U3D时内存多开销170m。这种场景让想起几年前在使用Cocos渲染时,为了减少40m的内核库,居然花费了一周时间精简编译Cocos的艰辛历程。这种场景代表某种尴尬:为了特效引入了一个太重的技术方式,这种技术无法做到轻量化,不大适合平台化的APP。</p>
<p>偶然在使用一个录屏软件时,产生点灵感,3D特效复杂如果设计动画帧成本太大所以设计部不接受,如果我们做个截屏小工具,运行这些特效连续截屏,截取指定区域,生成动画帧,网校APP直接使用程序截屏的动画帧,就可以祛除U3D Cocos这种重量级内核了吧,毕竟用户看的是屏幕,产品要的是实现了而不是怎么实现。抱着试一试的心态,开始编写这个工具,中途也遇到了些问题。</p>
<ol>
<li>时间平滑问题:动画效果很重要一点就是帧之间的时间平滑度,起初的程序控制设定在30ms一帧采集,但是发现实际的采集结果有的是30ms,有得是200ms,时间平滑度出入太大效果不理想。通过时间数据采集,发现采集后编码PNG时间,文件IO时间变动,中间又有系统内存回收导致的。再次修改采集方法,采用双线程模型加高缓存策略,保证了时间平滑度在30ms左右。</li>
<li>祛除背景问题:截屏窗体采用纯白背景0XFFFFFFFF,设想对截屏图片使用程序去除白色部分,然而发现有些色素是有Alpha通道的。理论上讲白色可以和任意Alpha通道色值混合成目标色值。这就意味着还原Alpha通道色值有些不现实,再次陷入困境。。。查阅了颜色混合公式 Dst = (Src * Alpha + (256 – Src.Alpha * Alpha / 255) * Dst ) / 255, 联想到对于同一帧如果分别采用白色背景和红色背景,利用混合模式对比不就能还原出色素的Alpha和RGB值嘛。于是再次修改采集程序,一个动作分别用红色背景和白色背景采集,生成两套动作。编写相似度算法分别找出每一帧的红色图和白色图,反向色素混合,果然能还原Alpha通道和RGB值~</li>
<li>祛除噪点问题:在祛除背景还原Alpha通道后,自以为没问题了,后来发现少量图片有零星噪点,深入分析代码发现,每一帧的白色帧和红色帧不是100%的吻合,图片边缘合起来对比还是有那么一两个像素的误差。开始各种尝试解决这种误差,祛除噪点,最终找到合适的算法,类似于卷积思想:以白色为基础帧,红色为对比帧,还原白色(X Y)的色素时,通过红色(X Y)周围9个点卷积还原,质量无损失,噪点完美祛除~</li>
</ol>
<p>解决三面三个问题,Unity 3D截取转动画实现了,每个动作帧生成时间在4分钟左右。后续编写独立的动画组件把内存控制在15m以内,成功在两个项目中实际应用。本次瘦身方案采用这个策略,祛除掉了Unity 3D内核减掉15m体积,功能依然满足,成功达成目标!本次减包的主要方案就是资源分离下发,祛除Unity 3D,顺便删除少量冗余资源,媒体库合并等方式。</p>
<p>提醒:可以理解做了个工具,可以截取指定区域的画面,通过算法生成了设计级别的动效,这种方式可以应用在多个场景,比如cocos等其他特效技术替换。</p>
<h2>我们遇到过哪些困难?</h2>
<p><strong>踩坑一:怎么分离drawable/image附件</strong></p>
<p>安卓最常见的图片是drawable/image,系统调用的方式就那么几种,实现起来会相对轻松些。先从drawable分离着手,开发Android的小伙伴都知道,gradle在编译时会把drawable/images存放在build目录下。起初想添加一个脚本,编译时把这些drawable/images图片替换成占位小文件。经过两天的重复试验,虽然脚本替换成了小占位文件,但是APP编译失不通过了,没办法只能去查阅Gradle编译流程,发现一旦Gradle完成编译前准备,随意更改build是不行的,其中编译环节过多不再赘述。编译中替换不行,那就换成编译前替换试试看。修复脚本,以工程为单位,识别sourceSet.res,把sourceSet.res copy出一份新的目录,命名为dir。替换dir中所有的drawable/images为占位文件,编译前动态重置sourceSet.res = dir成功了。经过两天多的探索,初步找到图片分离占位的脚本方式,开头还算可以~</p>
<p><strong>踩坑二:怎么无缝替换drawable/image</strong></p>
<p>这个技术是最关键的环节,只有做到无缝才能确保不需要变更各业务代码,从底层确保质量。按照起初设想,进入某个功能前下发本模块的zip文件并解压,显示drawable时无缝替换掉,实际显示占位文件描述的真实图片。为实现无缝替换技术,浏览Android Framework的系统源码,发现可以使用Drawable Tag扩展,扩展ReplaceDrawable新类,在xml文件定义 <com.parentsmettins.drawable.ReplaceDrawable file=“project/imagePath”/>,系统内核会反射package包下的ReplaceDrawable实例,可以在实例化载入真实图片显示,运行起来还不错,不用修改业务代码,就能无缝替换显示。忍不住爽了下,赶紧在云平台选择不同的设备和系统测试兼容性,几台手机崩溃了。失落之余发现,这些手机普遍在6.0以下系统,开始漫长的下载Android各种版本的FrameWork源码做对比, 最后确认:Drawable Tag扩展特性在6.0以前的系统版本是不支持的!想到判定属于6.0以下的系统,Hook Resouces类Cache的get方法扩展支持Drawable Tag,又开始漫长的Resouces Hook测试验证工作,终于算支持6.0以下的系统了,随后在两个独立模块中测试无缝替换显示技术,妥妥的。然而应用到第三个工程测试,APP奔溃了。。。追踪下去发现有个混合drawable载入ReplceDrawable Tag时报错,那个业务的混合drawable使用到了无法Hook的API,这样的API还有几处。困难的工作总是这么意外,暂停编码,再次浏览系统代码。结论如下:不能使用Hook方式兼容,因为总会有不能Hook的地方,实现必须遵守Android标准这样才能稳妥。回顾了Framework对于BitmapDrawable NinePathDrawable的所有API,找到 标准兼容方式。就是修改占位文件内容如下 ,同时重写Resources类的流读取方法,实现方式是获取资源id的类型,如果是xml文件,判断是否有file属性,有就认为是占位文件,返回file指定的已下载文件流。这种全新的方式既遵守Android标准,也不需要Hook,完美兼容各种drawable调用场景。因为我们的资源描述是标准的Android API,各种版本都支持,替换是从最底层的流层面完成的,各种API追踪都适用。完成这个最核心的无缝替换显示技术,隐约感觉到方案是可行的!</p>
<p><strong>踩坑三:怎么无缝显示lotties/image</strong></p>
<p>APP第二大资源是丰富的lottie动效,动效执行环节可能要修饰渲染素材,这样的动效场景遍布各个模块并且数量巨大,不同伙伴的调用还有不少差异。打包时分离到zip附件中轻松实现,但是无缝替换有些困难。起初设计方式是提供一套兼容API给各个业务方,各个业务方修改自身代码适配。刚开始实施,各个业务方反馈修改代码太多,完成兼容API替换会耗费大量时间,出现BUG的可能性也随之提高,调用兼容API方式实施困难,调整技术方案做到类似drawable/image的无缝实现非常必要。又开始耗费时间阅读lottie源码,发现内核代码会根据images路径和data.json信息从assets中寻找素材文件,猜想可以在lottie内核层面重写资源寻址实现,优先从下载目录中寻址,最终技术验证通过。因为不需要修改对应功能代码,原本计划多人一周的lottie方案,在一天内完成了。这个细节也提醒了我们,熟悉源码思想的重要性,技术层面深入一点多想一点,整体工作量小很多。</p>
<p><strong>踩坑四:为什么附件library执行崩溃</strong></p>
<p>随着drawable lotties分离无缝接入成功,基本完成了编译链 发布链脚本,也可以把so等library库采用统一的流程来做呀。随后添加library的分离流程,载入so时采用Compat的方式从本地存储卡载入,本以为是个简单的事情,发现几乎所有的手机执行so程序崩溃。。。</p>
<p>又开始追踪各个系统System.load(path)的源码实现,发现在高版本的系统中,Android的权限更加严格,特别是执行权限。起初library下载到/Android/data目录下,这个目录是没有执行权限的,修改为/data/data目录下,该目录有执行权限,解决了这个问题。</p>
<p><strong>踩坑五:怎么构造抽象统一的下载</strong></p>
<p>目前学而思很多业务中有不少下载代码,下载校验,文件管理等,如果离线资源,20多个业务都要添加下载代码,这对于精简代码非常不利,还需要测试成本确保质量。起初发现几乎所有的模块跳转都在架构组设计的Dispatcher类中实现,便设计在个业务的Dispatcher入口处拦截并下载对应功能资源。忙碌了20多个小时修改了这么多业务的Dispatcher类并检查,跑码测试,突然发现有个模块的没有资源拦截和下载,导致整个功能素材显示出问题。CR整个代码,发现跳转除了Dispatcher 还有少量的Arouter Scheme 以及原生的Start方式,最初的想法不全面还修改了业务代码,只能回退梳理代码流。发现不管Arouter Dispatcher Scheme最终都调用了Activity的startActivity方法,查阅Android系统的Activity源代码确定可以用参数Intent的ComponetName来判断要跳转的模块,临时拦截跳转下载本模块资源。因为各个模块的package都是prefix + businessName方式,这为我们抽象实现20多个业务资源下载提供了可能。编码完毕后,测试起来还不错。然而在全功能测试流程中,又碰到了loading不显示,或者进入直播时直接失败,追踪下去原来是绑定下载服务失败,主要是跨进程问题还有系统差异问题,再次对比不同版本的Service差异,修正下载服务代码支持跨进程问题。自以为方案没啥问题,又遇到从学习中心进入模块时,没有走到拦截流程,原因是拦截代码写在Base类中,绝大部分的业务都继承了Base类,少量的业务没有继承Base类,为了避免人为疏漏就编写代码检查脚本,编译时检查全工程的业务Activity如果不是继承基类,就报错停止编译提醒业务方修改继承。有了这个脚本检查,确保了无遗漏才敢进入下一个技术环节。</p>
<p><strong>踩坑六:非离线的首页素材显示问题</strong></p>
<p>在我们的方案设计中特殊模块工程不分离资源,比如首页,发现,个人中心,其他独立模块是分离附件离线的。应用方案后发现首页等模块少量的素材显示有问题,只能再次开启埋坑之旅。发现出现显示的问题的素材,其名称和其他分离工程的素材重名,gradle打包时选择了占位文件,而首页的原始图片不会编译到APP中。如果与首页资源重名的工程资源还没下发,框架代码找不到下载文件,会显示纯黑 或者纯蓝。因为不知道这种重名资源有多少个,又开始编写脚本统一检查,发现156处重名,共计312个素材!耗费大半天一个个修改名称避免重名,好在这些drawable类修改后,code也能快速识别出来修改资源符。</p>
<p><strong>踩坑七:浏览器WebView怎么崩溃了</strong></p>
<p>在测试中意外发现,应用技术方案后,在WebView中长按,程序崩溃。让人陷入懵逼状态,APP只是无缝替换显示离线资源,WebView只是加载URL链接也不会使用本地资源,怎么会崩溃?事情做到这个地步只能去排查,又开始艰辛的阅读webkit源代码。原来长按WebView时,webkit要弹出选择菜单,菜单的素材是在系统中,在载入WebView组件时,系统Resouces实例会把webkit的素材路径加入进来。起初我们为了做到无缝替换重写并替换Resources实例,重写后没有载入webkit素材路径导致资源找不到崩溃, 而APP又没法获取不同版本不同手机的webkit素材路径一时陷入混沌。经过多次尝试验证,我们发现不能简单重写Resources,应该采用装饰者模式重写,这样访问APP资源时返回已下载文件流,访问其他资源如webkit素材,采用System原有的Resources实例实现,这样解决了问题。</p>
<p><strong>踩坑八:Glide为什么显示不了本地素材</strong></p>
<p>熟悉Glide的伙伴们都知道,Glide是图片加载显示框架,可以包括url图片,文件图片,APP本地素材等。按照开始的设想,Glide会调用Resources实例载入本地素材显示,我们的Resources实例重写过可以确保替换显示占位drawable/image,测试中发现一旦使用Glide载入本地素材,就显示一片空白,为避免修改众多的业务代码导致测试周期拉长,又开始埋头阅读Glidde源代码。熟悉内核代码后发现,Glide载入本地图片不是使用Resources实例,而是Uri定位符,Glide之所以这么写是为了统一代码框架便于扩展。认真阅读Glide扩展规则,重写了Local Uri方法,优先从已下载文件中寻址素材,返回 File Uri解决了问题。</p>
<p><strong>踩坑九:自动化打包脚本的编写历程</strong></p>
<p>如果觉得资源发布管理还算问题嘛,不就是上传下配置下嘛,请看看起初的经历。绝大部分工作完成后,着手准备20多个zip文件,计算低版本增量更新包,,获取各个zip文件的md5,最后把这么多信息写进配置文件里,上传到云端。就这么简单的人力工作,耗费了大量的时间精力,做完了心里还忐忑不安,如果手动发布配置出错,线上一定出事故,还需要考虑不清楚技术细节的小伙伴也能快速发布依赖附件包。这种场景类似于当初尝试插件化碰到的问题,非技术问题:版本迭代管理成本。</p>
<p>考虑打Release包时通过Jekins托管,打包完毕后Jekins上已经输出20多个业务的zip文件,为什么不写个Python脚本,命令行运行,自动发布附件包到云端?有了想法开始各种倒腾,首先配置Jekins Web环境确保HTTP可以访问,Python脚本大约流程如下,按照配置清单从Jekins上下载20多个工程的zip附件,对比历史版本zip附件产生低版本增量包,计算各包md5校验值,批量自动化上传到OSS,汇总各个文件链接 校验信息 增量信息产生config文件在发布到云端。经过3天反复的编码,测试确保脚本OK了,开始使用完整的流程。一切看似正常,突然发现若干素材显示变形失真了。再次埋头去定位问题,发现失真的图片是 ninePatch图片,熟悉安卓的小伙伴知道ninePatch是特殊的png图片,在studio中按照规则编辑边缘就能使用最小尺寸的图片显示大尺寸确不失真。想这种特殊图片一定在正常编译中有特殊处理,再次开始研究gradle编译流程,发现对于ninePatch素材,gradle会调用aapt程序计算chunk信息保存在图片的metadata中,那python是否可以调用aapt工具对附件的ninePatch素材进行编译呢,又耗费精力在Python脚本中加入aapt编译再次尝试,问题解决了。自我感觉是没问题了,然而几天后运行Python脚本时发现,整个运行了2个多小时才发布完毕。。。又开始逐步调试,发现随着迭代版本增多,计算6500多张图片增量包IO操作太多,最终优化算法减少IO次数解决问题。</p>
<h2>方案能成功的经验总结</h2>
<p>1.基于Android 标准接口重写,避免Hook技术获得很好的兼容性,特别是后续系统兼容上。</p>
<p>2.发版阶段不需要各个业务方独立打附件包,而插件化的方式需要独自打附件包</p>
<p>3.在资源下载更新上我们做到了存量用户增量更新,而插件化的方式无法做到</p>
<p>4.除了技术本身我们做到了打包 发布 优化 增量等环节的自动化实现,节约迭代成本</p>
<p>5.我们在图片资源替换显示上做到了无缝替换,最大程度的降低了业务代码修改量</p>
<p>6.方案实施完毕后,后续的新增项目和需求不再导致APP持续增长,长期稳定。</p>
<p>7.我们在构造下发框架做到抽象统一 针对Bug修改时也在底层完成兼容,降低成本</p>
<p>8.释放了开发资源,大规模的自测确保质量。</p>
<p>虽然我们砍掉了一大半的体积,但是持续减包,持续减少资源体积,优化产品体验还需要坚持下去。后期进入深水区,可以推荐如下研究方向:</p>
<ol>
<li>短期拆分直播工程,把原本50m的直播资源分散开来,进入不同的直播课时loading的时间会更少。</li>
<li>中期项目组需要筹划混淆实施方案,尽量统一素材,动效统一,在UI设计上最大化统一。同时考虑脚本化分析代码,祛除无用代码,统一相似代码。</li>
<li>长期考虑dex优化,目前考虑到APP的稳定性,没有对dex启动混淆。</li>
<li>补充优化,可以考虑引用运动适量还原技术替换现有的帧动画 gif动画,大约能减少60%的动效体积。</li>
<li>补充优化,研究轻量超分重建,难度大收益大</li>
<li>end</li>
</ol>
<p><strong>作者简介</strong> </p>
<p>袁威为好未来高级Android工程师III</p>
<p><strong>招聘信息</strong> </p>
<p>好未来技术团队正在热招测试、后台、运维、客户端等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索“<strong>好未来技术</strong>”,点击本公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=sj6iMarNYh69ocOEDLH83g%3D%3D.VM%2Fe3xptdeGG0AT1USQnDYmckPIk4T%2BPc8LN0ymx05cDU1G94EYLfGqkz43ImcqwINgNlIs06%2BWnGUCkcrON5Anb5mvi0Tm8hi9pIczeob0uIDBuqdBWlVffd3dvuTC%2BhGGFtEyl0KAMKbLj13xsUrR6qPRxCDa3WyauCkELFg7OK9tzKEBfs1gQuqscYwF%2BUmch1aCJTXZAPEiDXaHmbcmlBwKcKy5INJVRVz7tgDSpp%2BxN7wcWRD%2F7Yn1zNSMkZ8IiiqPsUo5d7VcxQvFrFm8lXyfUKp3nSYlanICKhR4zDuy1PCMIe%2FWJcbZaMxWZ" rel="nofollow">DStack--基于flutter的混合开发框架</a></p>
<p><a href="https://link.segmentfault.com/?enc=vHHVdq%2FIQK%2F8pCJ1iDqqoA%3D%3D.LEvzqq5h5%2BygsdjlqGS64KNPT5PqBF%2FBoZblzZrUWO2alwWdNOdYg9%2Fq%2Bg89aKwyG%2BJcKthRcxAF1d%2BgKtK3R3l4shMeaEVZ4EWR0h%2F9XWZsZau2eY1syDUqZrywt7ZYKcCCchKPE5UmEaqpWt3Eli2hkQihDXDcI2OzKTTIEB5QxSkRZ9LM%2Fs5So659YR7894c%2BEVMLsnZwF12zlr2ZmD9iKUwVkg4kWaQ5uFnW0NXdUFaqfm4pqfPCQ0%2FcERFmiekLjTfmkWighoX%2BabirI%2FuXI1JJ1%2BJ%2BfR9okcruUGYNjpbkDraDSr8AlyGzYK9P" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a> </p>
<p><a href="https://link.segmentfault.com/?enc=Yb%2F6aoR3db0bAh5uCLfUsQ%3D%3D.f4fFwW%2BUa%2BgYIwvNvsNVNT0MH8Defxoiqeep5EEO0YAnnmzAqhQGSidyno4SXXhQMjSsYEdKCMAWNgvGTjZgu3pLcioCMGgalgb11JUBTCRxqjcns9fwsowpYHVoMpBLjAiaGF%2BOYZuyhSfKEFkYI9Nz1n8gdLOng8pMfqJe9ToA3ohI2LoEiXZYveVzFFPuuU5OHnJnEWaFeN3Lp3Q8ZLdVFjnOy19xRgH1YuPp9tD8xs5r9R2yWioIRvM96xYUt8K0M3yVwZjgKX41ma0P4KyCHAfdLOIK1M3bGjSa4vv1hMXk25CXHXE5Pcai9edZ" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><img src="/img/bVbG6Nn" alt="公众号底图.png" title="公众号底图.png"></p>
DStack--基于flutter的混合开发框架
https://segmentfault.com/a/1190000022720416
2020-05-22T20:55:34+08:00
2020-05-22T20:55:34+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
10
<p>混合开发这项技术由来已久,目前市面上主流的有Hybird,ReactNative,Weex,Flutter等。其中,Flutter以他独有的实现方式,优秀的性能,成为近两年最火的混合开发方案,我们学而思网校1v1客户端团队也是比较早的开始了Flutter技术的研究,在学而思网校1v1家长端和学而思网校1v1教师端两个App进行了大规模的实践尝试,由此也沉淀出了一套自己的混合方案DStack。</p>
<p><strong>为什么要有混合方案?</strong></p>
<ol>
<li>学而思网校1v1家长端是个纯Flutter工程,虽然用Flutter开发App能大幅度的提高人效,但是纯Flutter工程还是有些页面需要用native来实现比较合适,比如说webView,视频页面等;</li>
<li>学而思网校1v1教师端是个有一定规模的原生App,只是部分模块接入了Flutter来实现。</li>
</ol>
<p>上述两种情况都存在native页面和flutter页面进行交互的行为,当两种页面进行交互,比如,混合页面之间随意跳转、 页面间数据传递、手势滑动、内存资源控制、路由管理,这些都是需要解决的问题,基于此,我们参考了官方的解决方案,和阿里闲鱼团队的flutter_boost框架等,针对我们的业务和工程的具体情况进行了DStack的方案选型和具体实现。</p>
<h2>一、DStack定义</h2>
<p><strong>DStack是什么?</strong></p>
<p>学而思网校1V1客户端团队<strong>自研的</strong>,基于<strong>节点</strong>进行管理的,<strong>使用简单,易于集成,性能优秀</strong>的混合开发框架。 <br>目前框架已经在学而思网校家长端和教师端App上线,内存性能明显提升,稳定性表现良好。DStack也给Flutter社区提供了混合栈管理的新思路,改变了固有的移动研发模式。</p>
<h2>二、DStack的实现和特点</h2>
<p><strong>什么是混合栈</strong>?</p>
<p>当进行混合开发时,native页面和flutter页面依次打开时形成的栈结构,存在多种页面类型,以下图为例 <br><img src="/img/remote/1460000022720420" alt="image20200516171621915.png" title="image20200516171621915.png"><br>蓝色方块NA代表native页面,橘色方块F代表flutter页面 <br><strong>DStack对标flutter_boost</strong></p>
<p>我们可以看到,页面栈结构存在native页面和flutter页面交替的情况,关于如何处理这种不同页面间打开关闭的场景,目前flutter社区开源的此类框架只有flutter_boost,flutter_boost是阿里闲鱼团队自研的Flutter混合开发栈管理框架,该项目在github有3.9k的star。</p>
<p><img src="/img/remote/1460000022720419" alt="image-20200516183654129" title="image-20200516183654129"></p>
<p>那看到这儿可能会有疑问,既然社区有成熟的解决方案,我们为什么不用?主要有以下几点原因,一是flutter_boost的实现原理不适用于我们的纯Flutter工程,二是为了我们团队后续的mac,ipad,pc端进行混合开发做准备,三是我们对性能有很高要求,flutter_boost的实现方式决定了它没有利用flutter技术的特性,性能方面不够好。综上所述,我们需要自研适用性更强的混合开发框架DStack。</p>
<p><strong>怎么做?</strong></p>
<p><strong>1.基于“节点”进行混合栈管理</strong></p>
<p><img src="/img/remote/1460000022720421" alt="image20200516181118354.png" title="image20200516181118354.png"></p>
<p><img src="/img/remote/1460000022720426" alt="image20200516181122507.png" title="image20200516181122507.png"></p>
<p>在DStack框架实现中,我们把每个native页面和flutter页面抽象成了“节点”数据结构,每个页面对应一个节点,节点有页面的若干信息,通过节点这种数据结构,我们就在实现底层屏蔽掉了页面的具体类型差异。</p>
<p>基于节点有什么好处?</p>
<ul>
<li>抽象了具体的页面实现,便于管理;</li>
<li>提供了更强大的扩展性。</li>
</ul>
<p><img src="/img/remote/1460000022720422" alt="image20200516182100758.png" title="image20200516182100758.png"></p>
<p>NA代表native页面,F代表flutter页面,H代表Hybird页面。</p>
<p>因为我们已经把不同类型的页面抽象成了“节点”,所以后续如果除了flutter页面和native页面,我们甚至还可以接入ReactNative页面或者Hybird页面。</p>
<p><strong>2.确定节点与页面行为的关系</strong> <br><img src="/img/remote/1460000022720423" alt="image-20200516183654129" title="image-20200516183654129"></p>
<p>图片中的pop表示返回上一个页面,popTo表示返回指定页面,popToRoot表示返回根页面,popSkip表示返回指定的模块,如图就是把“登录”模块的所有页面都返回。 <br><img src="/img/remote/1460000022720424" alt="image-20200516183654129" title="image-20200516183654129"><br>每个页面返回和打开,都对应一次的节点记录,用户的行为触发节点管理,节点管理驱动页面跳转(即栈管理),考虑到Android和 iOS实现的差异性,节点管理放在了native侧处理。</p>
<p><strong>3.设计使用简单的api</strong> <br><img src="/img/remote/1460000022720425" alt="image20200516183344148.png" title="image20200516183344148.png"><br><strong>4.设计便于集成的框架接入方式</strong></p>
<p><img src="/img/remote/1460000022720427" alt="image20200516183440612.png" title="image20200516183440612.png"></p>
<p><img src="/img/remote/1460000022720428" alt="image20200516183513652.png" title="image20200516183513652.png"></p>
<p>我们已经把DStack做成了flutter侧的pub库,只需要在flutter工程直接引用依赖即可。</p>
<p><strong>5.利用引擎复用,框架内存优秀</strong> <br><img src="/img/remote/1460000022720430" alt="image-20200516183654129" title="image-20200516183654129"><br>在flutter的1.12版本之后,我们运用了flutter官方提供的flutter engin复用机制,做到了不同的flutter控制器共享同一个flutter engin,内存性能优秀。</p>
<h2>三、目前取得的成果</h2>
<p><strong>1.业务上</strong> <br><img src="/img/remote/1460000022720429" alt="image-20200516183654129" title="image-20200516183654129"></p>
<p><strong>2.性能上</strong></p>
<p><img src="/img/remote/1460000022720432" alt="image-20200516183654129" title="image-20200516183654129"></p>
<p>性能上我们主要对比了flutter_boost框架,我们可以看到不管是iOS侧还是android侧,flutter_boost每打开一个新页面内存都会涨,而我们的除非新打开flutter控制器会有内存消耗,其他情况内存数据很稳定。</p>
<p><img src="/img/remote/1460000022720431" alt="image-20200516183654129" title="image-20200516183654129"></p>
<p>这是android侧页面打开速度对比,我们可以看到除非是新打开了flutter控制器时,flutter_boost和DStack的页面打开速度差不多相同,其他情况下DStack的页面打开速度明显优于flutter_boost。</p>
<p><strong>3.功能上</strong> <br><img src="/img/remote/1460000022720433" alt="image-20200516183654129" title="image-20200516183654129"></p>
<p>这是DStack和官方方案与FlutterBoost在功能上的一些对比,Y代表有,N代表没有。</p>
<h2>四、后续计划</h2>
<p><strong>1.持续输出文章</strong></p>
<p>这是DStack投稿的第一篇文章,只是简单介绍了一下框架,后续我们会把详细的实现和采坑指南等持续的投稿,把我们的技术共享给整个集团。</p>
<p><strong>2.内部开源</strong></p>
<p>我们有计划把DStack在集团内部开源,也希望得到其他事业部老师们的意见和建议。</p>
<p><strong>3.外部开源</strong></p>
<p>我们有计划把DStack进行外部开源,回馈整个Flutter技术社区。</p>
<p><strong>招聘信息</strong></p>
<p>好未来技术团队正在热招测试、后台、运维、客户端等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索关注“<strong>好未来技术</strong>“,点击“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=SSp3r4%2BIw2sZqOZPMIOTAw%3D%3D.hDyOqh2pWBhhhEMAOechMUlkNTts%2BzdP%2F9TS1KYMLbX7JIQCde8Nq9ranr0ywnnBmbu7ZPs7%2BR3YxW%2BYm3obvVuOOSr2gWAownxklwmljqtRAiNWzOIa7s%2BtEB4r4%2FgrR7UuEo5%2FjYum5QoI4dFyJrvjUMNr6cebsswsgjSu%2FHbfjj37aaUHrh0PyOfrkqGSK5bm9LpjZHis%2FWn7sVIrKtF5E0hywuZhb0JryFmgruousCQYC3UJGyQ%2BByfRfGuPox3apYvG7Ke%2FPCazsrTeEnbH7x6CZtGcvRsshuJdruwZRF0xVJXmZBtHq5SKOMZl" rel="nofollow">GPU计算的基本概念</a></p>
<p><a href="https://link.segmentfault.com/?enc=%2FJilaqi2NAQ6YRpb7WnSZA%3D%3D.2kbxck5JVPZ9JcHsNw9Zn1%2FMMu8l9xiPM2K%2BcAKFVBJf38tF3dDL%2FQUQoJh4SxQHzQyxBhKWvBipjUc2L8lrcK7SZJllVSfQmwa7y%2FIcQIHU2gJAaBbyjrulkJ4E%2BbVQHq6tT1ETWIxkSRMZgw8fvDgwlnWKRqk7J7xn%2FouwXZaalC0ip78w2Q5TIR9bWcy8EG8XLwYfTzKfoWudgyjhva1wSPmXr%2BTUXwuq2PT%2BBS9W3kdfzzM4%2FOK1CbeW%2F%2FQixfQ8JkQIOsoKOaYk7c29CWlypml2O5fX366Di7CblrQyEhVC6It3KpTBVxhLLBKR" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a> </p>
<p><a href="https://link.segmentfault.com/?enc=YFO0xO9TO4HocgnVIwxilw%3D%3D.romCzMqbo1wY8CmzGk5sDTEFB3KgP%2BmYh3FUqG18wddNTmkStgK3OJ9SOi9oWQ%2FhY0Nk1ftIP1f9u%2Bl2FowYSkRwPy7ENsC3qXLptqSGi1Epx4BKe6zDZLm%2BIko4I%2BsGN7x%2F3EmkOUItfiUy123ESyfGBwNNAvdPBIur9J8QbS8%2FCnrlcMoyNh6V0DO1baqjUf9BhxhG1DvUIAfj0TKiNdh1AO2sFf4MPEAhZGa%2Bcnx%2FlhkzRjqv%2Bt2jl0fbtIXn0kFe4YAhqz2fFz2YuWz6bz37LdUB5ZfIInszwUQ5ThL7ZYh9KfCigUxkAhAExf83" rel="nofollow">浅析深度知识追踪如何助力智能教育</a> </p>
<p><a href="https://link.segmentfault.com/?enc=2A9kbHRsmtaz6s8AtcyIfA%3D%3D.QUxMdmx68I%2F6613%2Bat6p3yOcrn5tfnsoKNR2BBDE64g7Zq1MBxhLQE%2ByIN3qdWWsaCkDjEsSHzhVuDqQYJLy%2BIS81L73H%2B1Mv3qxJBb3Y99%2F6ovxe%2FTHlv7gB24R7kDFNkhs86bc61jIklLHWy7O5bnowU7uHMj3d0BtP9PIiMw4KCW4ZVmA%2FbgC%2BKUXmGFWqH%2BA4DcKvcTmX3jTStr62RQ0llxLEBZ4eUj0LLVNhbgj%2BmERgBtDJjReUlUUBNXVAsYe7yZvir6gIDFGCFVsSoz2turH8%2BpBm6eK7SspX%2B9G7J7npCDsnZbBa3ZZrLR2" rel="nofollow">轻量型TV端遥控器交互类库最佳实践</a></p>
<p><a href="https://link.segmentfault.com/?enc=XPMFwKMmRzIb8boA3QO1Og%3D%3D.3jOM9W2ZWi0x%2F7HWR8tFvIxbspCx3Tspu2n3ZUFW4RFxMIiZZ1QanNata0LfIul%2FGEQgNVwUOz%2FX%2B3vxiU%2Fu%2FycEWBrOfY%2Fi%2BF71OTTLgPt1E4kyitIBUY51F2%2B22b5zm5IbeDLaHECdu5iPnj%2Fq5tstCgMR%2FsesM3iQ1vm3unJq2xd%2FvNiXikol5voQU8CdySb6dao3OdQbGoWAFERssBbwVxfs7Tc9SK6zkRTrkqR4ZdI%2FunAmhaKxS8zM1o4XXcMKCUDn4imJvghUTZRkuDose4n01yGoDDY2Tkh9ei3ksX%2BriYoSCDkxIlx3FqlG" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><a href="https://link.segmentfault.com/?enc=kfsKgGOtyAvsv%2FLJi5XVeg%3D%3D.18i5LDL%2FqorAGJpQ%2BNkJbKuSnP5e6RCT1H2hWrcy8QWGZ4kMsDq12N1A6tArh7u5v2q%2F9jOVBo0avJ03yscV0Bs1jjQUHZf0Lzfk6uGvFdEOCU06ReT6wNAQgFWmH4Di9r4api1BDpwKiF901gRKQNyA4Ir8ob7BUZNos89bnMZVUkV0Iv8fv1Irw%2BtkMiFiSvYkzLCQw6jm9D9Xj43k9tfK9Il7s85fJciiJGbl6HoXab3%2Bj663TgC4drUDxJfvp%2FzuzE4HeBgR1jVF5wsxKZ%2BgHsrA1KX58N4%2BHL50YEd9PJ0w2eTI2QFcxRfRfHSc" rel="nofollow">用技术助力教育 | 一起感受榜样的力量</a></p>
<p><a href="https://link.segmentfault.com/?enc=xw9epA3j8d5YVhz%2FjekeyA%3D%3D.B69%2BGxmfMb4nv3yWgM7fpysBGVo%2B%2BJ2%2FpBjLpuCjYUDmAJXbYoUogvNFOzARnczCrtc6poy67xt5PUqMyYLsz%2FuKX0TYoBUTV7lYuwBoXU2PIvTxoRdr2iNJVPwuvqPkXUN35eeYnk0BVRV4ZvzszQ9ySrtGKHIGfyvkyFN4cULu%2FG8X%2B1WqUG6r1j45D%2BM%2BiWtFfydeJjVOgyWhpRH1azCD8JFDvT1V7y3N%2FFp4BT%2FSWstevnb8924Uz893zZMybAqCPqePtBfDd5xo8g4NYx3FYKu2IVOVwP%2FAnoZbhbMrYrn9PPVBSzzl63LF9Pc2" rel="nofollow">想了解一个异地多校平台的架构演进过程吗?让我来告诉你!</a></p>
<p><a href="https://link.segmentfault.com/?enc=%2BbCml%2BLECBxoo3D%2FVpHCTQ%3D%3D.gZY%2FGPqwk74S8b%2FFLc7nYO84q0eZtL2udsOo%2BSMRXiQJD%2BrUKX4CPNAC4ajry%2FBIrkrX78goHltA7Rt5VBNcj2n9swqaNRXzyGUrRvQvLV0zZNcO%2F7wKn2%2BacmBjbk2yIBs%2BDnGGFdM5d4%2FLK%2FAiaY9bj%2BmXkJxTVj9QStzgeP99gh8B3yZJzYLHK%2FqQtKz5HG3lyTt56gTHcQWOVjpTrjkoIxsyAhKO0FuIRBc6nqb383L6JW6nLuDIg6nlrF%2Bgp8m4HP7t8RrFOL22o5detm5WtTKY5qLomFVzbEa5nbk48RMAbO9z1Na4FbOVhFux" rel="nofollow">摩比秀换装游戏系统设计与实现(基于Egret+DragonBones龙骨动画)</a></p>
<p><a href="https://link.segmentfault.com/?enc=YVUnmRVBef8wCuix6Ie0Iw%3D%3D.vvoOCBuF0NXf5Wcn%2FJm%2FPmqSP1G6J3z9NINDgkmszsf%2BaGE2uRJoqZw%2Bcs%2BnC9QhpE7Jo1zKwW0fFrwUstljTA9gP9lzEyJC2N5xDsNT3Ll%2Bc4X1T22WCPymH%2B4BbEA8cRJVMNB2DK9rsPlRnOzeb4tVQeFML8llvx4E77EZsDGfmFHrmiy8L5DgfhOcmVevlLZqKXtBT%2BDgHCXoOBj3FImkI51SHHvYopqrAf3cNb6FlqDSCWYdz%2B60a69ZD3rMqGICQsvj5JAfVCfjOuRGNhYEwWBwBr3kzSnKPXHmvDuCJtfO827ig6WH%2BtuhpAYg" rel="nofollow">如何实现一个翻页笔插件</a></p>
<p><a href="https://link.segmentfault.com/?enc=VyWlkcd7bDqA%2B4NQ8jybVw%3D%3D.CIVLrXz5%2FUYs9hZF7bsxiyQ0DaQAMf%2BJlVeEtHa6nQVyqjn73rT6Mkj52tQE%2BLoT9IllC2bioGUWInJRqvQQPVjn0BG4BLXbxMDtTTekgdqf4I5NL8peN%2BIdYe43H2bLWWAkc0XaAd3SKSEAVFCVCY%2FZ57hDSMTvb4sMjhGETm8r3sQ0dxV0ATB1xfzAnRoaR00Nn32WDmR8%2Fq9bMT2KtQM9mOk5VNAII%2F6W9HhphCVCn7LtwmbwABCA9PQsbZfFBEclbGJhM0gTRtWg5IEatbO1aYVYLBi8Zcl0APeArb65Y%2FHXD4ySHzxoY6rqmzZf" rel="nofollow">产研人的疫情战事,没有一点儿的喘息</a></p>
<p><img src="/img/remote/1460000022720434" alt="" title=""></p>
"考试"背后的科学:教育测量中的理论与模型(IRT篇)
https://segmentfault.com/a/1190000022651799
2020-05-15T14:18:19+08:00
2020-05-15T14:18:19+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
3
<h2><strong>前 言</strong></h2>
<p>我们一说到教育,就不可避免地会谈到中考、高考,这些高风险考试(high-stakes tests)。虽然大家对“教育测量”这个概念可能不那么熟悉,但关心教育行业的伙伴对以下问题可能会比较有共鸣。比如:我们如何决定一张试卷是不是适合当年、当地的考生?我们如何为不同学科的试题赋值,应该使用等级、原始分数还是转换分数?新高考的计分逻辑和原始分数有什么不同?选科高考后,大家选择的科目都不一样,分数可比性怎么解决?这些问题背后的逻辑都与教育测量学密不可分。</p>
<p>其实,教育测量的理论和技术,不仅仅会应用在大型高风险考试,还可以应用在老师们日常教学的闭环中。比如,在面对一个新生的时候,我们如何知道学生初始的知识掌握水平?在教学期间,我们如何知道学生对一个知识点有没有掌握、掌握到什么程度?在一段学习之后,我们如何知道学生相比较刚来报班的时候有没有水平的进步或变化?学科能力水平这样的抽象概念,我们很难一眼看到,不像我们的身高、体重那么直观。所以我们就要依赖测量工具来对这些抽象、潜在的心理维度进行外化和量化,获得关键的学情信息,让我们直观地透过学生的作答信息和作答结果来回答这些问题,牵引着老师们在日常教学过程中的每一步动作。</p>
<h3>1. 教育测量是什么?</h3>
<p>那么,教育测量(Educational Measurement)到底是干什么的呢?实际上,教育测量要做的事儿就是对各种与教育相关的事物进行量化,给这些事物指派数字,最终来实现不同的教育决策(例如:选拔、评价、因材施教等)。以评价为例,我们可以通过各种不同形式的“考试”把学生的学习表现量化,用数字或者等级来代表,进而评价学生的学习效果。我们也可以通过对老师平时的教学行为进行量化,用数字或者等级来代表,来评价老师的教学效果。中国著名心理学家张厚粲老师说,“一个人的经验再丰富,也难免带有一定的局限性。再好的售货员不用尺或秤,而仅凭经验卖布卖糖时也会出错”。教育测量学就是希望可以用科学方法保证试卷的质量,确保可以精准地测量与教育相关的事务,保证根据分数做出的决策是合理的、公平的。 </p>
<p>在教育测量学中,衡量测评工具最重要的两个指标是信度(reliability)和效度(validity)。其中,信度是指这个测量工具要可靠、稳定地测查我们关注的维度,比如:学生的学科能力。效度是指这个测量工具确实是在测试我们所关注的维度,而不是其他不相关的维度。比如:数学考试就是测试学生的数学能力,而不是学生的英语能力。这两个概念,会在我们后续的文章中为大家详细介绍。</p>
<p>在这篇文章中,我们将具体介绍在教育测量领域中被广泛使用和研究的一种现代测量理论,名为项目反应理论(Item Response Theory,IRT)以及这个理论下的常用技术和模型,让我们从一个科学、技术的眼光看看考试背后的故事。</p>
<h3>2.项目反应理论(IRT)概述</h3>
<p>在介绍测验理论之前,我们先从大家的做题和考试经验来入手体会一下不同理论的差异。传统考试里大家做一份题,做完以后老师反馈试卷总分,如果我们忽略每个题目的分值,其实每个人的考分可以表达为作答正确的百分比。比如,一份试卷20题,对了15题,那么最后试卷得分就是75%。那么,75%的正确率代表什么呢?首先,我们日常在出试卷的时候,一定不是只关心学生在这张试卷上表现怎么样,而是我们想通过这张试卷的20题,去推断他能力到底怎么样。这张试卷的20题是对学生知识掌握情况的抽样,如果再给这位学员40题,他是否可以做对75%的试题,也就是30题?如果是80题,他是否能够做对60题(依然是75%正确率)?这里隐含的假设是,我们老师抽选的20个题是无穷无尽的题海中的一个有代表性的样本。 </p>
<p>但是,当老师们组出的20个题并不是对于一个年级有代表性的样本时,或者试卷间考察的知识点本身就不同时,则没有办法认为一个考生在试卷A的正确率是75%,他在试卷B上的正确率也是75%。这样只通过总体试卷正确率去评价学生的方法是有一个测量理论支持的,叫做经典测验模型(Classical Test Theory,CTT)。</p>
<p>要了解项目反应理论(Item Response Theory, IRT),我们首先需要认识一下CTT——因为正是CTT的局限性,才有了IRT产生的契机。CTT是在随机抽样理论基础上建立的一套心理与教育测量理论体系,其核心假定是:在测验水平上,观察得分(observed score;也就是我们通常的考试得分)等于真分数(true score;真实能力应该体现的分数)加上随机误差分数(error score;其他不相干因素导致的误差)。由于我们假设误差是正态分布上的随机变量(均值为0的),因此,如果同一个测验或平行测验可以反复测量同一个人足够多次,观察分数的均值就会接近考生的真分数,随机误差的均值为0。那这样的理论主要有以下几个局限性:</p>
<ul>
<li>在CTT下,用许多彼此平行的测验或同一个测验反复测量同一个人的同一种心理特质的做法在实际操作中往往是很难实现的,因此对个体真分数的精确估计也就主要停留在理论的层面上。</li>
<li>CTT的信度估计精度并不高。在CTT中,测验信度被定义为真分数方差与原始分数方差之比。虽然我们可以获取原始分数,但真分数方差在实际中却无从获取,哪怕是使用平行测验估计信度,完美的平行测验也是不存在的,因此实际估计的信度也不可避免地存在误差。</li>
<li>CTT各种参数(如:信度、效度、难度、区分度)的估计对样本的依赖性很大。例如:对于同一题目,若考生样本的群体水平较低,我们就会得到较高的难度估计值;反之,则得到较低的难度估计值。为了避免样本偏颇造成参数估计误差过大,CTT特别强调抽样时要注意保证样本对总体的代表性。</li>
<li>CTT中,测验对考生的评价指标主要为测验总分,而测验总分是考生在各个项目上的观察分数的总和。在用总分评价考生时,不同考生之间水平的比较只能在他们考了同一份测验的情形下进行,但是如果不同的考生参加的测验不同,那么这些总分之间就是不可比的,也就限制了我们对测验分数的应用。</li>
<li>在CTT下试卷的难度量表和考生的能力量表之间的关系是不一致的。在CTT中,题目难度的参照系是考生群体。例如:难度0.8表示该试题有80%的考生得分,但难度会随着受试群体的变化而变化。考生能力参数的参照系是试题集合。例如:百分制试卷中某考生卷面得分是80分,表明该考生在此特定试卷上得分率为80%,但是该考生是否能答对某个难度为0.8的题目呢?一个能力水平参数已知的考生完成一份所有项目参数均已知的测验,其在各个项目上的反应情况又如何呢?由于在CTT中,项目难度参数和考生能力参数定义在不相关的两个度量系统上,所以两者之间无法进行比较,也就无法进行预测,对测验编制活动的指导价值是有限的。</li>
</ul>
<p>既然CTT存在那么多局限性,是否有更科学、更实用的测量理论来弥补这些不足呢?我们接下来要介绍的项目反应理论(IRT)就是为解决这些局限应运而生的。</p>
<p><strong>2.1 IRT的基本框架</strong></p>
<p>IRT全称为Item Response Theory, 译为项目反应理论。其中所谓“项目”(item)其实就是指的我们试卷中的题目,“项目反应”(item response)就是考生在具体题目上的作答。简而言之,IRT就是建立在学生能力和作答正确率的关系上的。我们知道,影响考生在项目上作答结果的主要因素有两个方面:第一个方面是考生本身的能力水平;第二个方面是试题项目的测量学属性,如项目难度、区分度、猜测性。在日常教学活动中,我们都有这样的经验:对于一道编制质量很好的题目,全卷总分较低的考生在该题目上的正确作答概率较小,而全卷总分较高的考生在该题目上的正确作答概率相应较高。这种伴随着总分的由低到高,题目正确作答概率由小到大变化的过程基本上是一种连续性变化的曲线。在经典测量理论中(CTT),卷面总分可以被视作学生能力的代表,但是学生卷面总分是随测验的许多特性而变的。例如,随着试卷难度的改变,同一考生的卷面总分也会随之改变。那么能否用一种稳定反映考生水平的潜在特质(latent traits)变量来代替卷面总分呢?</p>
<p>假设这种潜在特质(即考生的能力)是存在且可被测量的,我们用θ来表示,那么随着考生的能力水平的变化,考生答对某题目的概率P(θ)也相应变化。这种描述考生能力水平与项目作答结果之间关系的数学模型被称为项目特征函数(item characteristic function, ICF),以图像表示则称为项目特征曲线(item characteristic curve, ICC)。下图1为一典型的ICC:横轴表示考生的能力水平,纵轴表示答对某题目的概率。每一个题目会有自己的ICC。</p>
<p><img src="/img/remote/1460000022651802" alt="" title=""></p>
<p>图1. 项目特征曲线(ICC)</p>
<p>考生潜在特质θ在特征函数ICF中是一个自变量,从理论上说θ的定义域是无穷的,从负无穷到正无穷都可取。P(θ)的值随着θ的增大而增大,但以P(θ) = 1为它的上渐近线。参数θ与卷面总分有一定的联系,正常情况下两者呈正相关。但是,θ是考生水平更为本质、精确的描写。习惯上θ采用标准Z分数的表达形式,其上下限一般设定为[-3,3]。 </p>
<p>ICC的走势除了受到考生潜在特质的影响外,还受到三个反映测验试题特征的未知题目参数alphaα、betaβ、c的影响,这三个参数决定了S形曲线的走向 (图2)。alphaα参数被称为题目的区分度,它刻画了测验题目对考生水平区分能力的高低。在题目的ICC中,alphaα值是曲线拐点处切线斜率的函数值。曲线在拐点处越陡峭,值则越大,同时意味着能力θ在拐点处稍有变化,则在该题目上正确作答的概率差别较大,因此也就说明该试题起到了精细区分考生的作用。</p>
<p><img src="/img/bVbHcKE" alt="图2-新.png" title="图2-新.png"><br>图2. 不同参数在项目特征曲线上的含义</p>
<p>参数c被称为猜测参数,是指实际测验中考生纯凭猜测而作答成功的概率。直线P(θ) = c是ICC的下渐近线。若题目的猜测参数为c,则意味着θ为负无穷的考生在该题上正确作答的概率也为c。</p>
<p>betaβ参数被称为题目难度。难度为betaβ的题目,若排除c的影响,潜在特质θ恰好等于betaβ的考生,TA在该题目上正确作答的概率为0.5。随着题目betaβ值的升高,ICC在横轴方向上向右平移,这时只有潜在特质更高的考生才可能在新题目上获得相同的正确作答概率。因此,betaβ值确定了,ICC在横轴上的位置也就确定了。与CTT中的难度参数不同,这里的位置参数是定义在考生能力量尺下的,而不是单纯考虑题目的作答情况。</p>
<p><strong>2.2 IRT理论下的不同模型</strong></p>
<p>项目反应理论(IRT)中题目参数和潜在特质水平参数共同影响测验的结果和精度。题目参数越多,对题目性质刻画越精细,但相对来说,模型也就越复杂,应用越困难。那么什么样的函数形式可以整合考生潜在特质和题目特征呢?研究者根据大量、可观测到的作答反应曲线,拟合提出了IRT的两个基础模型——正态肩型模型(the normal ogive model)和逻辑回归模型(logistic model)。</p>
<p>由于正态概率分布曲线是一S形曲线,因此研究者(Lord, 1952)首先想到了用它来拟合ICC,而正态肩型模型也从理论上奠定了IRT初始模型的基本形式。但是由于其模型中采用了积分函数的形式,在实际参数估计和使用中不方便,因此在1957年,Birnbaum将其改换成了logistic形式 (如下公式)。 </p>
<p><img src="/img/remote/1460000022651804" alt="" title=""></p>
<p>其中, θ为考生能力,alphaα为题目区分度参数,betaβ为题目难度参数,c为猜测参数,D为常量。P为能力为θ的考生正确作答某题目的概率。当D取值为1.702时,此函数的概率密度与正态肩型曲线的差异小于0.01。由于计算方便,目前多用此函数形式来描述ICC曲线。以上方程被称为三参数模型,当c=0时,该方程简化为双参数模型;当c=0且alphaα值一致时,该方程只有项目难度参数betaβ,因此被简化为单参数模型。有一种特殊并被广泛应用的单参数项目特征曲线被称为Rasch模型,由丹麦学者Rasch (1960)独立提出,对于不同的题目,其alphaα值恒定为1。 </p>
<p><strong>2.3 IRT模型参数估计</strong></p>
<p>当我们精心设计了一张试卷,并大费周章地得到学员的作答数据后,应该怎样利用这些数据估计学员的能力呢?针对具体的模型,IRT参数估计的过程就是要通过实测数据(即考生的作答数据),有时可能还需要借助一些人们积累的经验信息,获取测验中每个项目参数的估计值,以及参加测验的考生能力水平参数估计值。然而,在参数估计中,我们只有考生的得分矩阵和一些先验信息,考生的能力参数和项目参数均未知,我们要如何估计这些参数呢?一种经典的估计方法需要用到一种名为联合极大似然估计(Joint Maximum Likelihood Estimation, JMLE)的方法对考生能力水平参数和项目参数进行联合估计。 </p>
<p>所谓联合估计,具体来说就是首先以考生能力初始估计值作为已知条件,利用极大似然估计的方法估计项目参数;然后以该估计的项目参数为已知条件,重新校正初始考生能力参数;将能力估计值标准化,并且将项目参数做相应变换,即将两类参数放到同一量尺下;然后又以校正后的能力参数进一步校准项目参数,如此循环递推新值,直至两类参数达到某个预先设定的标准为止。 </p>
<p>尽管JMLE的方法可以同时估计考生参数和项目参数,但这种方法在实际运用中也存在很大的问题。例如:为了更精确地估计项目参数,一个常用的方法是增加项目样本量,但是增加样本量的同时也会导致考生参数估计量的增加,因此就会有更多没有额外项目信息的考生参数需要估计。同时,把考生参数和项目参数绑定在一起也不是一种有效的计算方法,因为只要一个项目的模型拟合没有做好,就需要重新进行整个项目参数和考生参数的估计。因此,在实际操作中,研究者普遍采用一种更有效的项目参数估计方法——边际极大似然估计(Marginal Maximum Likelihood Estimation, MMLE)。MMLE的方法是把考生看成是来自于某个已知分布总体的代表性随机群体,可以通过基于对该已知分布进行积分的方式来估计项目参数。 </p>
<p>已有考生作答数据信息,且项目参数确定的情形下,一种常用的能力参数估计方法为贝叶斯后验期望估计的方法 (Expected a Posterior Estimation, EAPE)。EAPE的方法与极大似然估计的过程不一样,可以通过直接计算就得到期望估计值,因此计算过程更简单,速度更快,也符合传统的贝叶斯思想,使它成为能力参数估计的一个上佳选择。</p>
<p><strong>2.4 IRT的优势</strong></p>
<p>在以上内容中,我们介绍了IRT的理论框架、相关模型以及参数估计的内容,可以看出IRT和CTT有很大的不同,那么IRT是怎么克服CTT的局限的呢?它的优势又体现在哪里?</p>
<p><strong>2.4.1 项目参数与考生能力参数具有不变性的特征</strong></p>
<p>我们在本节的开头提到CTT参数的估计对参测样本的依赖性很大,但是在IRT中测验的题目参数具有跨群体不变性,即题目参数估计独立于参测样本。具体来说,只要测试同一特质的测验项目的参数具有足够宽的覆盖,也就是测验中既有难的题目,又有中等难的题目,也有容易的题目,那么不管题目分布形态如何,考生能力参数的估计就不依赖具体的题目。同时,只要在同一维度上考生的能力水平分布足够宽,也就是在考生样本中,既要有部分能答对该题目的考生,也要有些无法答对的考生。那么,不管考生分布形态如何,项目参数的估计也不会依赖于具体的考生样本群体及其分布形态。</p>
<p><strong>2.4.2 项目参数与考生能力参数具有统一的量表</strong> </p>
<p>根据IRT模型估计出来的考生能力参数与项目难度参数具有统一的量表,即考生参数与项目参数可以被标定在同一个参照尺度上。例如,能力估计值为0.5的考生答对难度值为0.4的题目的概率大于答错的概率,而答对难度估计值为0.6的题目的概率则小于答错的概率。同时,在实际应用中,用于测试能力水平为0.5的考生的最佳题目的难度也应该在0.5左右。距离0.5太远的题目,对该考生来说或者太容易或者太难,并不能有效测量出考生的水平。</p>
<p><strong>2.4.3 可以针对不同考生精确估计每个项目和测验的测量误差</strong></p>
<p>IRT相比于CTT引进了题目信息函数的概念,并用信息量来替代信度的概念。信度与测量标准误差之间存在反比关系,一个试题提供的信息函数越大,测试的误差就越小。信息函数不仅与参测题目性质有关,还与参测群体的水平有关,即对不同能力的考生施测相同试题,其测验误差并不相同。同时,测验题目信息函数具有可加性,一个测验包含多个题目,它们的信息函数的累加值可以被称为测验信息函数。有了不同题目对不同考生单独计算信息量值的方法,我们就可以对每个考生的特质水平估计误差进行主动控制,从而更加有利于指导测验的编制。</p>
<h3>3. 结论</h3>
<p>综上,我们为大家简单介绍了教育测量的含义,并深入描述了教育测量中广泛应用的现代测验理论IRT(项目反应理论),包括其背后的逻辑和涵盖的不同模型。相较于老师们主观组合、实施的考试和经典测验理论,应用IRT理论和技术可以更加精准地测量学生的学科水平。其实,关于IRT的相关技术还有很多,能帮助我们实现各种不同的测评目的,指引我们的测评设计。而在应用场景方面,IRT除了应用在大型测评中的具体测验设计和计分中以外(如:我国大学英语四六级考试),IRT的技术理论还可以用于题库建设和自适应测评的开发,感兴趣的伙伴可以持续关注硅谷研发部发表的文章,我们会在之后的专题文章中和大家分享不同的测验理论和技术的应用。欢迎大家持续关注! </p>
<p><strong>参考文献</strong></p>
<ol>
<li>Birnbaum, A. (1957). Efficient design and use of tests of a mental ability for various decision-making problems. (Series Report no. 58-16, Project no. 7755-23, USAF School of Aviation Medicine, Randolph Air Force Base, Texas.)</li>
<li>De Ayala, R. J. (2008). The theory and practice of item response theory. Guilford Publications.</li>
<li>Lord, F. (1952). A theory of test scores. Psychometric monographs.</li>
<li>Rasch, G. (1960). Probabilistic models for some intelligence and attainment tests. Copenhagen: Danish Institute for Educational Research.</li>
<li>戴海崎, 张锋. (2018). 心理与教育测量. 暨南大学出版社.</li>
<li>罗照盛. (2012). 项目反应理论基础. 北京师范大学出版社.</li>
<li>张厚粲. (2017). 教育测量学: 高考科学化的技术保障. 中国考试, (8), 4.</li>
</ol>
<p><img src="/img/remote/1460000022651803" alt="" title=""></p>
<p><strong>招聘信息</strong></p>
<p>好未来技术团队正在热招测试、后台、运维、客户端等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索“<strong>好未来技术</strong>”,点击公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=nmVuGaNnGSLRnlVHkl8hLg%3D%3D.xHV8kxqPgphjC2ToBn8LgQJYQCvf0Zsn0LF2Lcn1U422174TLY9C4OjmWinPiD9%2B00ZSWSTPOcYevSRhIBjkcHiV%2B4%2FMu8nICPIH4XNJBv%2F5vuLnZQ0gIgKrSV5NQWuU6IviehXAL8Hkv0x6f5%2BkrmsJScBlZqs6qK5LxFq0PsEwTHeTZ7LKeYf%2BOg4b0T3jybQMrqA8ec07PPfnKjT0ASn8uEtNnkVEQ0UzjgZaHBvd3PeoiGfCVF18q1mQEubJKgP%2B0g18JY66wEoK1yNH3POjzuLsZd8TZIBeg2vdLdefarNMcdtSmf%2F2DnybOlah" rel="nofollow">WebRTC源码分析——视频流水线建立(上)</a> </p>
<p><a href="https://link.segmentfault.com/?enc=Zlr6NpOhcJVEmYrC2%2FzSiw%3D%3D.9EuVZC%2Bp4ie93zz9tgzDcU7ITfGx1lR%2FNiL0Ss19lKGSixMPGQNia8q7Wh%2FDQyxlxp3chniqcOxUnKzOWeU1Y%2BVjly%2FMkapeLjP7%2FLxCx%2BAVAGb%2FROsOLIpRuGu%2B%2BwZmvgu8R77fonh7eRNvvdTdoBtv2HStYSmyUQyoK%2Bgb7qD36LLSajbKcE%2B%2BoDeWWGeEydNjBkMrgSG4%2F88QpnG%2FG6QHNaDeywFd7YOLjURklmi5kdUKD7LHCUsCX5oOONvPAADg%2BqgggelN8SToApzV8FLDtRsOSdd%2FR%2Bf8s3dy6p34pzWXVBZqN%2FwkYoE0upGL" rel="nofollow">浅析深度知识追踪如何助力智能教育</a> </p>
<p><a href="https://link.segmentfault.com/?enc=O%2B9JsOTgDCoGEu3UqJLDbQ%3D%3D.O8uYHToVAE5Px8f0TXUy3NFNM%2BNtzEoamGsYL35Monh4kfapM10nMH3hwCpgYMKFDJdQU%2BKbvkJmUE04u%2FsZ2bAkFciH%2FuUcYRY4zlrBg7fP9t1nlVz7DUevEvKsroUvF0uvy%2Bb%2FIzOEX4dbdKEGNDZwEfcMb62m2Wm2P6By%2FWqS8eLYgbd%2BkfOUJD91%2FMbgjtYaBjyFDfxuqG1luYgdfHKXf6yWCIXDhxGAXm2ccX6RNH%2Fq5Jyj%2BNRAPl0YOc8ajXI7k2BlS8fBbiSd8Ihc3vU0LgNfXQVVohlmIFK%2FxZF99RynJQBzDqCBZgUSEVJf" rel="nofollow">轻量型TV端遥控器交互类库最佳实践</a></p>
<p><a href="https://link.segmentfault.com/?enc=mQkmN8wxlVIdgJxuqA3oNA%3D%3D.jRkmj7AVLPwFflph582TFSpAXWDaFXNtmBNc29pUM5HS%2FoWyrJ6Lji6izOdIwhgFXwwGeClGbkFFeRPN2Xw6WeVGIIzh5cP01dJ17YKknj1uFRsl%2FR%2F1XW6CTX5ifj7UQQLp%2F8IGEautRjmq%2FkBFIvojego8wnrIBKgoZzg8bmZOrL8tO1N%2Bk6J1ZovqZY6OZQ4vNIxb7bgKqGn2gCjeQEL%2B8qp9oFAlKep342%2BUulfwGKZOXUnWnS7XHOTfvT4Ct4kBOPkmBbUcIIY6j6oKRSNGvluwZPCx%2BmX5qMHTpM85UUaVMHt6TEqpfsK%2FeNK7" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><a href="https://link.segmentfault.com/?enc=AmhHk35qDBDXjn1z4rVLrQ%3D%3D.%2B2TbC81MPbmJbUHx53jxv5sSBFbGqxxC%2BYflm1E%2FGanOODJIov1%2FoZEz2G3X4xZbrwdEcIgljHzTV5xdb9Uk71n32y%2FqRXunF7gALEwfDgJafmuiGZ8Hj3CNxFwxn%2B5j74M9qt868SauJLtTSli5meJ5jHVb4g7SMxlfBuD0DiFTkK4GlE5mL4bA%2BxNsoOlnLwdxIuMixRYSnU5%2BgN9Lf7pSQYjQaAhMTP%2FvxiP6tLiK89f0mxy8fL3%2F1%2FnwMgvftlHg62lK1Fsi7pr5rTxXKZapmGuFRl5b2rowHAwblOjsa5RXAVGJmjCRRolbkypK" rel="nofollow">用技术助力教育 | 一起感受榜样的力量</a></p>
<p><a href="https://link.segmentfault.com/?enc=mHeDwesmpAGIvVgRilCLMg%3D%3D.%2BP1vBuZZrYlIHi78GEhzJ%2BF1PdfxoRliQ%2Bd%2FmFm5Kx5MVWRl9psDOfvF1B9YA7Y0c4gKyO%2F3Iqb6BSROw%2F6D7X%2Fl6l51%2FBwQUo91vQeo29tVAcMNojm5cL7fRha4tk8pJJbpa%2FJuK1ZPxb00IgixorsyHuUwRox3RQQiBykodb0Kg5wbzmkCrPY%2FrNLVIqHy%2F%2Bi3errREMdI%2F7ODxDx7FDPaR2vLQ1t8UHnR4qstP142xdLZPgdGUEE2BuwEqD0uPb5BObQfTTMdvxJYdiMcDzi3RhCobvmpDky4iOVLBLoJiQVaNRBn%2BlZC4%2F48VTko" rel="nofollow">想了解一个异地多校平台的架构演进过程吗?让我来告诉你!</a></p>
<p><a href="https://link.segmentfault.com/?enc=L9N64Cxfl9Bv2nb%2FkT7KUw%3D%3D.dBLxlK%2BqwjlRA2CZYrCBZvXESEmlzrnWHkd1%2FbXUhgIQvRdF7pti9U6I%2FzEXeFI6FX1%2Fl%2BuU1Jb9eBCDW2Ilv8vFIQ5qnOAWhjVhu15OShy3YTtgjEbzr%2Fwej1a1qugwLd5KqnLi2lVHH930T8CEXJHfhP6PMBvY0sIkrdwXztgFAJu4GRBL0xgIb2OJEkaOaXCOCHWm9mQkGlpTc%2BmRLqkphCjIIjhfdXZEtoMYtDlOd2V08PuiMjmyb5lDl6q75H%2BFOVLPkkEJB2qx8V2%2FdNfMp0MNemdLrm%2FGTfeEaUZtNr3rNae41fclv3nQqfax" rel="nofollow">摩比秀换装游戏系统设计与实现(基于Egret+DragonBones龙骨动画)</a></p>
<p><a href="https://link.segmentfault.com/?enc=j079pEfKPOWRIVNJnDKFfQ%3D%3D.4cE%2F3HyPKL3EFbLCrWHnJ7Jk4As2ReLlLoZeMbcHOxc6ONqiocmC%2B1Iv1i3FSaY4ja7b9zwark3TiCyIkCNk5Equ0uth042yqTPjkOuHqeIiEbBOw8moi4Jmir1lZpUMnbl4oFLr0t6Z3Ns7k172k3B45tX4zlVj0fJ5JYxeMVROLT8qEKYiWQ4j7Qjwd16b81z6XlmmkRe6mrH2DFZ3XCEaoYD2GWbjUv5gOfimgWf5w%2BpBNQhah1dU0GJjTLmQdRdNOy%2F%2FU0c0d6wRpg9K%2FA%3D%3D" rel="nofollow">如何实现一个翻页笔插件</a></p>
<p><a href="https://link.segmentfault.com/?enc=9dtMQgZBNLAxnyo%2BlUj0GA%3D%3D.4esplPxaP1LkukTR6onWE%2BewEMoihoBvAqPirEX1SNlNU5jiy0K7F3kauIaCfCKhBQCmmBYAlsfj1YWZ7HMl0JwEekSvxaNkx1wXIel1fGJFbMsRuQlbMTSDDaTxrx5WKP7Wyp1xeeQNPwOe%2BeRxek1lRW6PfQAPxSyqz7L5n3qmzvZgSE7Fo4kfKYeuah5%2FnSjBuEpwU4oqRD8Ha85YxNAKsEKDZi%2Fl55HS1obwUNjqqS2NAC5nI9nfSdzJ3xw9u4twWWwMmMuFdzGZTPYlGICamRqIHxBFoblwonT%2BrrICkpFMEnmFpsTgfKiUK1Ue" rel="nofollow">产研人的疫情战事,没有一点儿的喘息</a></p>
<p><img src="/img/bVbG6Nn" alt="公众号底图.png" title="公众号底图.png"></p>
WebRTC源码分析——视频流水线建立(上)
https://segmentfault.com/a/1190000022578652
2020-05-08T11:49:30+08:00
2020-05-08T11:49:30+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
4
<h2>1. 引言</h2>
<p>常见的音视频会话中,一端将本地的音视频数据传输给对端将至少经历3个步骤:采集->编码->传输,将数据从采集模块到发送模块的流动称为音视频数据的流水线。接下来几篇文章中将以视频数据为本来讨WebRTC是如何建立此视频流水线的:数据如何采集,如何从采集模块一步步流向网络发送模块,最终传输出去的。</p>
<h2>2. 采集</h2>
<p>视频采集模块是数据流水线的起始点,负责从视频源采集原始视频帧,推送给流水线的下一站:可以是本地渲染模块进行本地回显,也可以是编码模块进行数据编码压缩。</p>
<p>视频源可以是摄像头,也可以是桌面、窗口抓屏(远程桌面,基于视频流的电子白板等应用),甚至可以是磁盘上的视频文件,图片文件。WebRTC中提供了基于摄像头的视频采集框架,是本文要讨论的重点。当然WebRTC也提供了桌面,窗口抓屏框架,这套框架对外所提供的接口与基于摄像头的采集接口有所不同。整个视频流水线建立是以摄像头采集接口为基础的,从而导致这么个问题:当需要将抓屏数据当做视频源往外推送时,需要使用适配器模式来实现一套基于摄像头的视频采集接口。在基于视频流的互动白板中曾如此实现过,但不是本文讨论重点,因此,将放在别的文章中进行阐述。</p>
<p>视频采集模块是平台相关的模块,MacOS/IOS一般使用AVFoundation框架或者QuikTime框架,Linux平台一般使用V4L2库,Android上一般使用Camera1或者Camera2框架,Windows平台则使用DS(DirectShow)或者是MF(MediaFoundation)。由于WebRTC是个非常活跃的工程,代码架构一直在不停的变动之中,比如2019年4月份的代码还有VideoCaptureMF的代码,并且还注释着Vista及以上的版本建议使用MediaFoundation采集框架,而2019年11月份的代码MediaFoundation相关的代码已经被移除。再比如MacOs/IOS,Android的相关代码已经被移动到sdk/objc和sdk/android目录下。本文以modules/video_capture下的代码来做阐述,平台无关的代码在该直接目录下,平台相关的实现在modules/video_capture/windows,modules/video_capture/linux目录下,如图所示:</p>
<p><img src="/img/bVbGTEV" alt="1.png" title="1.png"></p>
<h3>2.1 视频采集相关UML</h3>
<p><img src="/img/bVbGTE7" alt="2.png" title="2.png"></p>
<p>DeviceInfo接口提供了设备枚举相关功能,其平台相关子类实例以组合的形式提供给VideoCapture。</p>
<ul>
<li>枚举设备个数,获取某个设备名称。</li>
<li>枚举某个设备所支持的所有能力(VideoCaptureCapability: 分辨率,最大帧率,颜色空间,是否逐行扫描)</li>
<li>获取某个设备的所有能力中与外部设置的能力最匹配的那个能力。</li>
</ul>
<p>VideoCaptureModule视频采集模块的虚基类,它定义一系列视频采集的通用接口函数:</p>
<ul>
<li>Start/StopCapture用来开始/结束视频采集(平台相关);</li>
<li>CaptureStarted用来判断当前capture运行状态(平台相关);</li>
<li>Register/DeCaptureDataCallback用来注册/注销数据回调模块(平台无关);</li>
<li>Set/GetApplyRotation用来设置视频旋转角度(平台无关)。</li>
</ul>
<p>VideoCaptureImpl类是VideoCaptureModule的实现子类。做了3个事:</p>
<ul>
<li>声明静态Ctreate方法,用于创建平台相关的VideoCaptureImpl子类,在Windows平台上为VideoCaptureDS,在Linux平台上实现的子类是VideoCaptureV4L2。该方法一处声明,多处实现,在相应平台编译时,只会加载对应平台的实现代码;</li>
<li>平台相关的接口,留待平台相关的子类中实现,主要是开始/结束视频采集;</li>
<li>实现平台无关的接口:注册视频数据回调,应用视频旋转相关函数。其中注册数据回调将一个实现了VideoSinkInterface<VideoFrame>接口的对象赋予VideoCaptureImpl::_dataCallBack成员。当采集模块得到一帧视频数据,就可以通过该对象的OnFrame()方法推送出来。</li>
</ul>
<h3>2.2 采集模块的内部数据流</h3>
<p>1. 以VideoCaptureDS为例,平台相关的采集模块采集到一帧视频后,平台相关的函数ProcessCapturedFrame()方法进行处理。ProcessCapturedFrame()将视频帧直接传递给VideoCaptureImpl::IncomingFrame()方法</p>
<p>2. VideoCaptureImpl::IncomingFrame()方法将对视频帧按需求进行旋转,并利用libyuv库转换成I420类型,再给视频帧加上ntp时间戳。经过上述处理后,IncomingFrame()将视频帧进一步传递给VideoCaptureImpl::DeliverCapturedFrame()</p>
<p>3. VideoCaptureImpl::DeliverCapturedFrame()将调用VideoSinkInterface::OnFrame(),将视频帧传递给回调对象_dataCallBack,即数据的下一站,从而将视频帧推送出采集模块。</p>
<h2>3 流水线建立</h2>
<p>视频采集模块作为底层模块,需要和上层模块协作才能把采集到的视频数据发送到上层的显示和编码模块,为数据流水线提供源源不断的视频数据。从控制流来讲,视频采集模块在初始化阶段由上层模块进行创建并开启视频采集,在结束的时候由上层模块停止视频采集并销毁模块。从数据流来讲,采集到的视频数据通过回调接口传递到上层模块,进行数据流水线上的下一步处理。</p>
<h3>3.1 VideoCapture->VideoTrack的流水线</h3>
<p>不论视频流最终目的地是流向本地渲染模块还是要流向编码器,首先都要经过VideoTrack这个对象。从控制流上来讲:一个VideoTrack对象的创建过程就是VideoCapture->VideoTrack流水线建立过程:</p>
<p><img src="/img/bVbGTFa" alt="3.png" title="3.png"></p>
<p>从数据流来讲:而视频数据流动方向正好和创建的方向相反:</p>
<p><img src="/img/bVbGTFz" alt="image.png" title="image.png"></p>
<p>相关的类图如下:</p>
<p><img src="/img/bVbGTFF" alt="5.png" title="5.png"></p>
<p><strong>1. VideoCaptureModule->VideoSource</strong></p>
<p>VideoCaptureModule作为数据源头组合到VideoSource对象中,同时VideoSource又实现了VideoSinkInterface<VideoFrame>接口,可以把自己注册到VideoCaptureModule中。从而实现了视频帧从VideoCaptureModule->VideoSource的流动。</p>
<p>VideoSource持有一个非常重要的成员VideoBroadcaster对象,该对象的UML类图如下。<br><img src="/img/bVbGTGa" alt="6.png" title="6.png"></p>
<p>一方面VideoBroadcaster实现了VideoSinkInterface接口,成为一个Sink,这样VideoSource得到采集模块的视频帧后,首先会流入到内部的VideoBroadcaster成员对象,而非直接从VideoSource流出;另一方面VideoSource和VideoBroadcaster都实现了VideoSourceInterface接口,对外VideoSource作为视频源存在,向数据流下一站提供注册方法AddOrUpdateSink();该方法内部调用VideoBroadcaster的AddOrUpdateSink(),从而将数据流下一站VideoSink注册到VideoBroadcaster,存入成员std::vector<SinkPair> sinks_中。到此,应该不难想到VideoBroadcaster既有了数据流入,还知道数据的下一站(可能多个),那么VideoBroadcaster::OnFrame()中就可以通过循环调用下一站的OnFrame方法将视频帧广播出去。</p>
<p>为什么要如此设计?因为,在WebRTC 1.0的官方规范中说明了一个视频源是可以被多个视频轨共用的。通过上述方式可以实现共用的概念。</p>
<p><strong>2. VideoSource->VideoTrackSource</strong></p>
<p>VideoTrackSource没有实现VideoSinkInterface接口,因此,实质上视频数据是不会流入到VideoTrackSource中的,但其组合了VideoSource对象,并且实现了VideoSourceInterface接口,添加到VideoTrackSource中的VideoSink会被添加到VideoSource,然后进一步添加到VideoBroadcast中。对外部来说,VideoTrackSource就是视频源。</p>
<p>VideoTrackSource另外实现了视频源状态相关的接口,以及状态通告相关的接口NotifierInterface,用于向更高一层(VideoTrack)通告视频源的状态。由于与数据流的讨论无关,此处只提及,不详述。</p>
<p><strong>3. VideoTrackSource->VideoTrack</strong></p>
<p>如同VideoTrackSource一般,VideoTrack也没有实现VideoSinkInterface接口,因此,视频数据也不会流入到VideoTrack中,但其组合了VideoTrackSource,并且间接实现了VideoSourceInterface接口。想要从VideoTrack中获取视频流的站点,只要实现VideoSinkInterface接口,通过VideoTrack的AddOrUpdateSink()注册进来即可,因为该VideoSink会经过VideoTrackSource->VideoSource->VideoBroadcaster,最终可以从VideoBroadcaster获得视频流。</p>
<p>VideoTrack另外实现了ObserverInterface接口,用于以观察者的身份来接收响应VideoTrackSource关于视频源状态的报告。</p>
<p>VideoTrack还实现了VideoTrackInterface接口,其中提供了一个重要的属性:ContentHint。这个属性告知编码器在码率降低时,应该如何应对:降低帧率?降低分辨率?对于桌面采集应用来说,我们应该设置该属性为kDetailed或者是kText,这样编码器编码该视频流的时候不会降低分辨率,量化参数qp值也不会设置的过大。</p>
<h3>3.2 VideoTrack到本地渲染</h3>
<p>从之前的描述,我们很清楚的知道视频帧是如何流动到VideoTrack的(虽然实质上并没有流动到VideoTrack类),我们也知道该如何从VideoTrack中获取视频数据:1)实现VideoSinkInterface接口,2)通过VideoTrack的AddOrUpdateSink()注册进去即可。事实上,本地渲染就是如此做的:要么直接使用WebRTC提供的平台相关的渲染类,这些类都实现了VideoSinkInterface接口;要么可以自己实现Renderer类,并实现VideoSinkInterface接口,在OnFrame方法中获取视频帧,并进行渲染操作。render通过VideoTrack的AddOrUpdateSink()注册进去时,会一直被投递到VideoBroadcaster被其持有,从VideoBroadcaster处直接得到视频帧。</p>
<p>WebRTC中提供的渲染类相关的UML类图:</p>
<p><img src="/img/bVbGTGA" alt="7.png" title="7.png"></p>
<h3>3.3 VideoTrack到编码器</h3>
<p>要说清楚VideoTrack中的视频帧如何到达编码器的,首要问题是搞清楚在WebRTC中哪个类代表了编码器,这才好研究视频数据的流向。</p>
<p>在WebRTC中VideoStreamEncoder类表征着一个视频编码器,接收原始视频帧作为输入,产生编码后的比特流作为输出。该类位于src/video/video_stream_encoder.h中,如下截图为该类的说明:</p>
<p><img src="/img/bVbGTGJ" alt="8.png" title="8.png"></p>
<p>搞清楚了目的地后,接下来就是分析视频流如何从VideoTrack一步步流向VideoStreamEncoder,这条流水线又是如何建立起来的。</p>
<p>从数据流来讲,数据从VideoTrack->VideoStreamEncoder过程中大概经历了这么几个对象:<br><img src="/img/bVbGTGK" alt="9.png" title="9.png"></p>
<p>这几个对象的UML类图及其关系如下所示:按照之前的分析,我们知道要正真获得视频帧,该类需要实现VideoSinkInterface接口,在OnFrame()在该方法中得到上一站传来的视频帧。通过下面类图,我们可以看到实质上只有VideoStreamEncoder是一个VideoSink对象。而VideoTrack通过以对象成员的方式一直被传递到VideoStreamEncoder。由于VideoTrack实现了VideoSourceInterface,VideoStreamEncoder又可以反向设置到VideoTrack中,根据之前的结论,VideoStreamEncoder最终会存储在VideoBroadcaster中,由VideoBroadcaster将视频帧直接传递给VideoStreamEncoder。</p>
<p><img src="/img/bVbGTGO" alt="10.png" title="10.png"></p>
<p>从控制流来讲,如果不深入研究细节,仅从WebRTC的外层API来看,通过PeerConnection->AddTrack();PeerConnection->CreateOffer();PeerConnection->SetLocalDescription()这三步就建立起了这条流水线。后续简要分析这3个方法内部对建立上述视频流水线做出的贡献。</p>
<p><strong>1. AddTrack()</strong></p>
<p>在创建出VideoTrack后,通过PeerConnection->AddTrack()接口会为每个要发送的视频Track创建一个VideoRtpSender对象,视频Track成为VideoRtpSender的成员,实现逻辑上视频流向VideoTrack->VideoRtpSender流动。 另外,如果SDP使用kUnifiedPlan方式,还会为每个track创建一个独立的</p>
<p>RtpTranceiver对象,组合包含该track的VideoRtpSender,并添加到PC的成员RtpTranceiver数组中。</p>
<p>VideoRtpSender对象有两个重要的成员是与本文的讨论相关的track_和media_channel_。分别就是VideoTrack和WebRtcVideoChannel对象,是视频流的上一站和下一站。执行AddTrack()并不会将二者关联起来,只会将VideoTrack添加到VideoRtpSender中。但最终VideoRtpSender->SetSsrc()方法被调用时完成二者绑定。</p>
<ul>
<li>VideoRtpSender->SetSsrc()被调用的时机?</li>
<li>如果SDP使用kUnifiedPlan方式,VideoRtpSender被创建时,media_channel_并没有跟随一起被创建,那么何时何地media_channel_会被创建。</li>
</ul>
<p><strong>2. CreateOffer()</strong></p>
<p>PeerConnection->CreateOffer()方法的详细过程是非常复杂的,它收集本地的音视频能力和网络层传输能力形成SDP描述结构。虽然该方法没有直接参与视频流水线构建,但是其为下一步PeerConnection->SetLocalDescription()操作提供了必要信息,使得其能完成视频流水线的构建。</p>
<p>下面简要分析PeerConnection->CreateOffer()的过程中与视频相关的部分,大致的调用过程如下:</p>
<p><img src="/img/bVbGTGT" alt="11.png" title="11.png"></p>
<p>图中特殊标记有两个函数:</p>
<p>PeerConnection::GetOptionsForUnifiedPlanOffer()会遍历PC中所有的RtpTransceiver,为每个RtpTransceiver创建一个媒体描述信息对象MediaDescriptionOptions,在最终的生成的SDP对象中,一个MediaDescriptionOptions就是一个m-line。 根据由于之前的分析,一个Track对应一个RtpTransceiver,实质上在SDP中一个track就会对应到一个m-line。上述遍历形成所有媒体描述信息MediaDescriptionOptions会存入到MediaSessionOptions对象中,该对象在后续过程中一路传递,最终在MediaSessionDescriptionFactory::CreateOffer()方法中被用来完成SDP创建。</p>
<p>另外MediaSessionDescriptionFactory::CreateOffer()创建SDP过程中,会为每个媒体对象,即每个track:audio、video、data创建对应的MediaContent。上图右边展示了为视频track创建VideoContent过程,标黄的静态方法CreateStreamParamsForNewSenderWithSsrcs()会为每个RtpSender生成唯一的ssrc值。ssrc是个关键信息,正如之前分析,但需要说明的一点是此处并不会调用RtpSender->SetSsrc()方法,ssrc当前只存在于SDP信息中,等待SetLocalDescription()的解析。</p>
<p><strong>3. SetLocalDescription()</strong></p>
<p>在CreateOffer()成功的回调中,一方面,我们会通过信令将Offer SDP发送给对端;另一方面调用SetLocalDescription()进行本地设置操作。</p>
<p>SetLocalDescription()的大致步骤如下:</p>
<p><img src="/img/bVbGTG3" alt="12.png" title="12.png"></p>
<p>如上图, SetLocalDescription()过程是相当复杂的,我们抓住视频流水线上关键节点的创建以及关联过程来进行重点描述。重点函数在上图中都标黄显示。</p>
<p><strong>流水线上对象的创建:</strong></p>
<p>1) PeerConnection::UpdateTransceiverChannel()方法中检查PC中的每个RtpTranceiver是存在MediaChannel,不存在的会调用WebRtcVideoEngine::CreateMediaChannel()创建WebRtcVideoChannel对象,并赋值给RtpTranceiver的RtpSender和RtpReceiver,这儿解决了VideoRtpSender的media_channel_成员为空的问题;</p>
<p>2) PeerConnection::UpdateSessionState()方法中,将SDP中的信息应用到上一步创建的视频媒体通道对象WebRtcVideoChannel上,调用WebRtcVideoChannel::AddSendStream()方法为通道创建WebRtcVideoSendStream,如果有多个视频Track,会有多个WebRtcVideoSendStream分别与之对应。WebRtcVideoSendStream对象存入WebRtcVideoChannel的std::map<uint32_t, WebRtcVideoSendStream*> send_streams_成员,以ssrc为key。创建WebRtcVideoSendStream,其构造函数中会进一步创建VideoSendStream,VideoSendStream的构造中会进一步创建</p>
<p>VideoStreamEncoder对象。 到此,所有有关的对象都已经创建完成。</p>
<p><strong>流水线的建立:</strong></p>
<p>之前就分析过VideoRtpSender->SetSsrc()方法非常重要,该方法在PeerConnection::ApplyLocalDescription()中最后被调用。会触发Track被传递,从VideoRtpSender传递到WebRtcVideoChannel,再传递到WebRtcVideoSendStream,成为WebRtcVideoSendStream的成员source_。 从而实现了逻辑上VideoRtpSender->WebRtcVideoChannel->WebRtcVideoSendStream流水线的建立;</p>
<p>WebRtcVideoSendStream::SetVideoSend()方法紧接着又触发调用VideoSendStream的SetSource()方法,以WebRtcVideoSendStream为视频源参数(看之前的类图,WebRtcVideoSendStream实现了VideoSourceInterface接口)一路传递给VideoStreamEncoder的成员VideoSourceProxy。在这个VideoSourceProxy::SetSource方法中,反向调用WebRtcVideoSendStream::AddOrUpdateSink()方法将VideoStreamEncoder作为VideoSink(看之前的类图,VideoStreamEncoder实现了VideoSinkInterface接口)添加到了WebRtcVideoSendStream。注意,在WebRtcVideoSendStream::AddOrUpdateSink()中会调用source_->AddOrUpdateSink()进一步将VideoStreamEncoder添加到了VideoTrack(如之前的描述VideoTrack已经被传递到WebRtcVideoSendStream成为WebRtcVideoSendStream的成员source_)。在逻辑上实现了视频流从WebRtcVideoSendStream->VideoSendStream->VideoStreamEncoder这段流水线。</p>
<p>至此,以发送端角度来看,从采集到编码器的整个流水线都已建立完毕。</p>
<h2>4 总结</h2>
<p>1. 从WebRTC提供的API角度看,从CreateVideoTrack(),AddTrack(),CreateOffer(),SetLocalDescription()这四步就建立起了发端从采集到编码器的视频流水线。当然具体细节比较复杂。</p>
<p>2. 虽然涉及的类很多,实质上一个视频帧从采集模块开始,流向编码器模块并没有经过太多的对象。接收数据的对象都实现了VideoSinkInterface接口,视频帧就在这几个对象的OnFrame方法中源源不断流动。WebRTC中数据总是从Source向Sink流动。</p>
<p>end<br> </p>
<pre><code>
</code></pre>
<p><strong>作者简介</strong></p>
<p> 黎意为好未来高级C/C++工程Ⅲ</p>
<p><strong>招聘信息</strong> </p>
<p>好未来技术团队正在热招测试、后台、运维、客户端等各个方向高级开发工程师岗位,大家可扫描下方二维码或微信搜索关注“<strong>好未来技术</strong>”公众号,点击“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=c2ql18pJQupi3OL0Df5Y%2FQ%3D%3D.30olAGDfVgGR%2BL1jg78v7xn00JQMTF70tlbETDL7ahOhEHWRLtKOPO%2BkQi01Kq9xg6srlrqlZV4L9Eewea7dB0LJf5IEnbPZkCkN%2FUfj6%2Bp27MTfKnODjt1UBzeL1XY2xwkzaNoLfMnyjwknJmUlYPdEosKki%2F4YlyMkdDLdddB8S7e%2BOJvQyfrgZdjPXTha1ExeZwOV6VBZ1zN0Bhg6KdZ7sy78eaJdHeegwpWi1M0uLaTslygoyA4yaZ0hupevLPnQMbo3x%2FAnbBKcjY%2BbPAEFdPgcXEM8kPbv8dGrpiiNYf%2FjLMtgvMgUyomEwxcz" rel="nofollow">浅析深度知识追踪如何助力智能教育</a> </p>
<p><a href="https://link.segmentfault.com/?enc=GmnTu0keLS1XgZSLWnULoQ%3D%3D.VUkJmX39%2B%2FqvGJ%2Bgc9p7IUBUCc0MGEzS5qMU%2FfCn2Eh%2F0uW5IuHIYDc7ilJKFX8gxwo7EZIy8XXEctUxRqWoxAnbLLMKzR%2FQul%2B3o8XA4VuR2izzCb%2FpAliwpR9iBW4skm0Zyna0IZ%2B26jcrK6lxfmlFRdXN9GyfLZt9xXBWkveRYuu9SB1cfElgGSDDYUGbGp2CH86MoUHZAr27pSGMFsqz0TbDcoeWQnEk4ggIXQo4XlXlLvjJF8r4jgKJHq6Mqo%2BRUCI7ReR2TQIDA0fo%2F%2FFQ4X%2FOm6MaQgtaa6X7Q6EGur4F9Whq0s1K82chdeNX" rel="nofollow">轻量型TV端遥控器交互类库最佳实践</a></p>
<p><a href="https://link.segmentfault.com/?enc=BSTu06Eb92JZ6ATuUTLGPg%3D%3D.7qHpknQT74rZ2gRUPnPkRGJMuwA4NTSjfpZDryrj4azUvKHQZMqbVv1mf3pNR0LO2lN6513r%2B%2B%2B8MPj5rb7reZ%2B4KTyD%2B5NpeZrZ7c30bfWEGLOX2I%2FRCZjqyif1JYADwOCzlrovFb9N%2FA6ZEW%2FRYjNyUAtclLQyrYDU%2Bz9IyEZsugP7MVtqRj1xm7DFZUKE8GfyIOR2u%2BTfJ8P9xFiUT5rSA7icdQl50zteM1zvkwCjvu52j%2BKQNuKopdYucI1vktZQXOFJYF33IBic5TQc0MR9aeML50wyxgraaSiAA2EcRgyKwRPAQXm6QhOjOp9c" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><a href="https://link.segmentfault.com/?enc=BryqQZ2NoJYIdBnrFjROpw%3D%3D.Q9wqCa41E7PcQX1QmkGcx0KjqeQN5kpK7fyGTCURFSCnForpVwG8efaXPfy40FdnPc0JiLjUlJQvdIpmKuP0pD9bawS%2B%2Frt%2FFfMqsBVRI1n%2FhJS5ksMhZpgXLO%2BAU5IQmM%2FY3Denb3dQHnJniKol4%2F7N60hPtctgaAXEqm14%2Bc4Z%2Fq5A1PneW91Oip7HG%2BAsc5BBjKiV6TUD3b3kqIP1YKx9lm8lsKTUGW9OGRk3N79tlNMz5v6xfAXaBnm042J%2BAkFlDRNjh%2FVJiN8icRRLkYx3DSL%2F%2BesIT9NCh6bc6jw6zp3May1ZxWSVuBBhp7IP" rel="nofollow">用技术助力教育 | 一起感受榜样的力量</a></p>
<p><a href="https://link.segmentfault.com/?enc=Dlb6qN6gdVhrsvuhj32Hjg%3D%3D.zNXLsHJmSvvvQOf4lCfH8b4rEn%2BHnLdyvu3QsLSlaecjAskTyzVeY%2BDkg9vZ04x44IO%2F5YSvxemZK1U6uf5R%2ByHCxkzqBWfV2L8aTeF2AEf5XtaMje%2BPiQIVQ2fZn8WwDr2Rxqx6I%2F5DApWO8elfN6lkdzWuZdUSwqeqY%2BIE%2FktRg3WFwJuCS8zza05b6G3xAqTwTOBt9tAkg5%2B%2FZq4bqk7lxc4gafsoa%2BPWyOjtnV6Ea%2F6ptygoAlhycuetqbglqVIErPZ3lc7IaY27stOj4ClrjtODEmraE4sZHNIYqJVKkjjxCI4%2FAMzHpP2ncXV7" rel="nofollow">想了解一个异地多校平台的架构演进过程吗?让我来告诉你!</a></p>
<p><a href="https://link.segmentfault.com/?enc=DogtA%2BQQv7SCSIP2p4EQag%3D%3D.t2jxFbYKH%2Fk5eAxoTTMJAuCypcXufjubVMhWaFO%2FQefU5jO2j%2FatzN0AHfQB0k%2FLPFL4eNsAgbWLoeSDYZRZN0aQ8F%2FSQCmIM2P9ls4QptVc4Zs9YRtemJmrevBam74VmC8Mi3KKFEKYyvnrm1kFEf5Otl0gPXsrO5SsSdZXtxwk4sKUTpbLq4%2F8HPQXptIGB9eUlPjypnoM07b91s%2Fi3FOWuRt2oPX%2Bc6ClH8bfbQ4YSQ1jGmQ7%2FH%2FVaTo2%2FNGCCxNV1U6JSiQJkXvMMdLaaE8CXEOFAYZgfwiPYXP2A%2FUpjS6neG703dxeQWLpDqKL" rel="nofollow">摩比秀换装游戏系统设计与实现(基于Egret+DragonBones龙骨动画)</a></p>
<p><a href="https://link.segmentfault.com/?enc=7%2FaUS6QbwJ8RXa9dLn7EOg%3D%3D.BAJaD4xHrqm5H8aWScJ1bslkzhdrCQM4vy5EuscxbimY14wWiFCf%2B0MOZAslYQYjU9Gv8rnXhXOwFt8%2B90OJusaHPc2S%2BfRc6qKqAlVAOZaR%2B8sZIf5amhVOPe0fSKUKfnlaZBft3sFLFgNx4AItmGifYvFeKYIy8YxPyuQvbucTMgnI6SFtISDcMNUtpJS9Yt6gl4LwS%2BAtjWWTu%2BCZki19P9GuLRWv2kkz%2FRW1KieHJkn9gu79%2FrlQ5Y2F0lOWghV%2F59q93zvuSmgPCjOOnZbg2X1KoR99Cs45vZAfmgdrgyj6GAhyTaIOiTt5TqgX" rel="nofollow">如何实现一个翻页笔插件</a></p>
<p><a href="https://link.segmentfault.com/?enc=gd%2FVIKXqest5Hpln74JkOA%3D%3D.MNIjqGmza8lj8kyhfAyXRLg%2FdaUppRr1Cj5jG3n8ICjHkqwQDMxMZlYphrBt%2FJliMPxM5UaSXPHSKeOLu2zBjAXMoejQTfVB%2FXcBRL3Lxs6uTLmw7TQnddHAggXaqJcH%2BA8NiXNE6q6%2Fz2JydH1kusmP%2BsPzcIA6Y9wguRo6eYoOGK28OBrDIS08fPd9kuUGzBVy%2F0xTwUy0S2Lm4hHPpsVRStfnaIm3frlwF30ddg7nKa9%2FKhq8CwlKYgvs5p0U325EKcSzqPCUkRu9rAsxalmYUHBbd1OfMaF1SBYfqzac6OSNRPL7ZpSt8ylpKISQ" rel="nofollow">产研人的疫情战事,没有一点儿的喘息</a> </p>
<p><img src="/img/remote/1460000022578655" alt="" title=""></p>
浅析深度知识追踪如何助力智能教育
https://segmentfault.com/a/1190000022550037
2020-05-05T21:35:29+08:00
2020-05-05T21:35:29+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
3
<h3>什么是知识追踪 ?</h3>
<p>知识追踪的主要任务是根据学生的历史学习轨迹来自动评价学生随着时间推移的知识点掌握程度变化。在了解学生的知识点掌握水平后,便可以为学生提供符合自身学情的个性化辅导。在教育行业中,利用科学方法有针对性地对学生的知识点掌握程度进行追踪还是十分有必要的。依据学生的海量历史学习数据可以完成对学生的学习过程建模,使模型能够自动追踪学生每个阶段的学习状态,从而达成自适应学习的目的。有学者认为,学生所掌握的知识点集合与其外在的做题表现间是有十分密切关联的,所以我们可以尝试通过学生的做题表现来建模学生的知识点掌握状态[1]。</p>
<p>一般来说,知识追踪任务可以表达成以下的数学形式:给定学生A在特定学习任务上的历史学习序列 $X_t=(x_1,x_2,...x_t)$,来预测学生A的在$x_{t+1}$上的表现。通常$x_t$可以表示为一个有序对$(q_t , a_t)$,该有序对中$q_t$表示学生在$t$时刻回答的问题$q_t$,而$a_t$则代表了学生在$q_t$上的回答情况,一般而言$a_t$取值为0 (回答错误)或者1(回答正确)。其实,如果站在概率的角度上来看知识追踪,本质上是利用学生历史作答表现来预测学生在下一个时间点正确回答问题$q_{t+1}$的概率,即$P(a_{t+1}=1|q_{t+1},X_t)$,可以被解释为在给定学生历史学习表现序列$X_t$ 和在$t+1$时刻会做的题目$q_{t+1}$的情况下,学生做对题目$q_{t+1}$的概率大小。</p>
<h3>什么是深度知识追踪 ?</h3>
<p>在初步了解什么是知识追踪的概念后,我们接下来要介绍我们的主角——DKT(深度知识追踪)[2]。目前世界上有许多智能教育公司在使用知识追踪的相关模型,如BKT(贝叶斯知识追踪)和DKT(深度知识追踪),深度知识追踪简单直观来说就是利用深度学习方法来做知识追踪。从DKT论文中的实验结果发现,DKT在不需要过多专家经验和大量特征工程的情况下,精度优于传统方法。下面我们就来介绍下深度知识追踪算法DKT究竟是如何工作的。</p>
<p>为了让各个背景的伙伴都可以对DKT有一个好的了解,我们首先要简单说一下什么是DKT中使用的循环神经网络(RNN)[3]。传统的循环神经网络接受$x_1,...x_T$作为输入,然后输入映射到$y_1,...y_T$。这个可以通过隐变量$h_1,...h_T$达成,每个时刻下的隐变量都可以被看成过去所有信息的某种编码,用于和当下的输入一起与预测未来输出的结果。我们可以用以下公式来进行表达:</p>
<p>$$
\begin{aligned}
h_t&=tanh(W_{xh}x_t+W_{hh}h_{t-1}+b_h) \\
y_t&=\sigma(W_{hy}h_t+b_y)
\end{aligned}
$$</p>
<p>在上面的公式中, $tanh()$与$\sigma()$是激活函数,模型的主要参数包含输入权重$W_{xh}$、循环权重$W_{hh}$、初始隐变量$h_0$、输出权重$W_{hy}$以及隐变量与输出的截距$b_h$与$b_y$。循环神经网络也可以简化成下图:<br><img src="/img/remote/1460000022550040" alt="Picture1.png" title="Picture1.png"><br>当然,由于纯RNN容易受梯度消失等因素影响,在实际操作中一般使用长短期记忆网络即LSTM对RNN进行一个替代,这里我们就不针对LSTM展开来讲了,对LSTM细节有兴趣的伙伴可以通过这个<a href="https://link.segmentfault.com/?enc=7LEIot2rCWOfAkEf60MXjw%3D%3D.ZT%2FwJNMNJHxa9bPTaUJ65qyiONIO%2BTbs0ZrY1FoDei%2FT5JfUbmmTPfAZm1W3GMCY" rel="nofollow">链接</a>来了解下,这里我们可以把LSTM当做RNN的一个加强版来看待。</p>
<p>接下来我们需要的是利用这个循环神经网络来帮我们建立知识追踪的模型。我们要考虑的第一点是如何把学生的历史学习轨迹数据表示成循环网络可以接受的形式。这里面谈一个比较直接的表示方法,即 One-Hot Encoding(独热编码)。按上面介绍知识追踪时提到的,我们可以把$x_t$表示成$x_t=\{q_t,a_t\}$。假设我们要追踪 $M$ 个不同的知识点,我们可以令 $x_t$ 是一个长度为 $2M$ 的向量(向量的前 $M$ 位描述学生做的题是哪一个知识点,后 $M$ 位描述学生是否做对该知识点),其中量的可能取值可能是0或者1(如果学生做对第 $k$ 个知识点,则第 $k$ 位是1,第 $M+k$ 也是1,其余是0;若学生做错第 $k$ 个知识点,则仅有第 $k$ 位为1,其余是0),则数学上 $x_t$可以表示成 $x_t=\{0,1\}^{2M}$,这样 $x_t$就可以作为循环神经网络的输入了。而输出 $y_t$ 就是一个长度为 $M$ 的连续向量了,其中第 $k$ 位代表如果学生在$t+1$时刻做对第$k$个知识点的概率,所以我们从$y_t$就可以判断当前学生在各个知识点上的掌握水平了。</p>
<p>至于训练的损失函数,我们就可以考虑使用经典的二值交叉熵。使用独热向量 $\delta(q_{t+1})$ 来表示在 $t+1$ 时刻哪一个知识点会被回答,同时令 $l$ 代表二值交叉熵函数。那么对一个学生而言损失函数便是:<br><img src="/img/bVbGBvo" alt="image.png" title="image.png"><br>通俗来讲,比如我们在 $t+1$ 时刻做了第 $k$ 个知识点的题,我们就去找输出结果 $y_t$ 中对知识点 $k$ 的预测值,然后把这个预测值与真正学生在第 $t+1$ 时刻知识点 $k$ 的作答结果 $a_{t+1}$进行对比,预测值越是接近这个真实的 $a_{t+1}$ 则对应的损失值越小,反之则越大。我们的训练目标是使得最终的损失 $L$ 最小,可以通过梯度下降的方式降低损失 $L$,从而达到对网络参数进行优化的目的,为了防止过拟合,也可以在训练中采取一些 dropout 等常用方式。</p>
<p>我们现在来看一下DKT训练好后的效果图(该图是针对一个学生而言):<br><img src="/img/remote/1460000022550041" alt="dkt.png" title="dkt.png"><br>上图左边每个彩色圆圈我们可以看成不同的知识点,从图中可以获取我们要测试$M=6$ 个不同的知识点的信息,横着的彩色圆圈代表学生在不同时刻真实做的题是什么样知识点的题以及回答情况,其中若圆圈是空心代表学生答错该题,而实心代表学生答对该题,矩形底下的数字代表该生回答到了第几题(即作答时间点,上图中学生历史共回答了50题)。而矩形中的第 $t$ 列的6个方格颜色深浅代表每个作答时间点模型对学生6个知识点掌握的预测(越绿则代表掌握程度越高),即模型中的输出 $y_t$。我们可以根据 $y_t$ 去预测 $t+1$时刻学生的作答表现,同时,$y_t$也可以用来当做第t时刻学生知识点掌握熟练程度的描述。</p>
<h3>深度知识追踪有什么应用场景 ?</h3>
<p>训练知识追踪的目标是利用学生的历史学习数据去预测学生在未来的表现情况。如果深度知识追踪可以真的能达到理想的效果,我们便可以利用学生日常练习数据去判断一个学生现在的能力是怎么样的,那我们组织统一考试的必要性与频率就可以大大地降低了。</p>
<ul><li><h4>自适应学习提升学习效率</h4></li></ul>
<p>深度知识追踪最大的一个潜在应用是帮助学生优化知识点的学习效率,根据学生实时的知识点掌握水平来帮助学生选择最好的学习的顺序。比如 $t$ 时刻学生在知识点$A$的掌握程度最差,即$y_t$在知识点$A$对应的维度上的数值最低,则我们可以尝试提高为学生讲解$A$知识点的优先级,这样可以帮助学生有针对性补他目前知识上最大的短板。</p>
<ul><li><h4>发现不同知识点间的联系</h4></li></ul>
<p>DKT模型也可以用来发现不同知识点间的联系。对于知识点 $i$ 和知识点 $j$ 而言我们可以用 $J_{ij}$ 来代表两个知识点间的关联强度,</p>
<p>$$
J_{ij}=\frac{y(j|i)}{\sum_{k}y(j|k)}
$$</p>
<p>其中 $y(j|i)$ 代表的意思是如果学生在此刻做对了知识点 $i$,他下一个时间点做对知识点 $j$ 的概率,上面的式子通俗地可以理解成知识点 $i$ 和知识点 $j$ 的关系其实就是做完知识点 $i$ 后做对知识点 $j$ 的概率在所有做完知识点 $k$ 然后再做对知识点 $j$ 的概率的占比。因为如果知识点 $i$ 和 知识点 $j$ 的关系越紧密,则做完知识点 $i$ 后做对知识点 $j$ 的概率相比于做完其他知识点后再做对知识点 $j$ 的概率来的相对较高,所以对应的 $J_{ij}$ 也会较高。这样我们就可以通过 $J$ 的值来对两个知识点间的关系进行判断了。</p>
<h3>总结与展望</h3>
<p>相比贝叶斯知识追踪(BKT)深度知识追踪(DKT)有不少优势:</p>
<ul>
<li>模型可以反映出长时间的知识掌握程度,相比传统贝BKT假设知识一旦掌握了就不再会被遗忘,深度知识追踪引入循环神经网络模型可以很好地模拟知识长时间不做会被遗忘的行为,更加符合人们的认知。</li>
<li>能够对复杂的知识点间的联系进行建模,从而发现不同知识点间的内在联系。</li>
<li>不同于BKT用0/1来表示学生知识点掌握状态,DKT输出的 $y_t$ 是连续值,DKT可以反映出学生连续的知识水平变化。</li>
</ul>
<p>当然深度知识追踪模型也是存在着缺点的 [4]:</p>
<ul>
<li>模型存在无法重构的可能性,比如学生在此刻做对 $i$ 知识点,但是某些情况下,模型认为下一刻对 $i$ 知识点的掌握水平反而下降。</li>
<li>在时间序列上,学生存在对知识点掌握程度不连续的情况,部分学生的波动可能过大。</li>
</ul>
<p>上述两个缺点可以通过修改损失函数进行解决,已有相关的论文对深度知识追踪模型进行改良,提出了对应的解决方案,并获得精度上进一步提升,同时对上面缺点中提到的问题有了很好的提升与修复。</p>
<p>整体来说,深度知识追踪模型(DKT)可以作为一种人工智能手段自动发现知识点间相互关系同时可以智能设计学生学习路径达到提高学习效率的目的。DKT也是目前相应领域中相对前沿的模型,各种相关的DKT+也如雨后春笋般不断出现,大家可以对领域中新的研究多加关注,遇到相关问题也欢迎大家随时一起沟通讨论,让我们一起借模型和数据的力量为业务智能化打出更大的价值!</p>
<h3>参考文献</h3>
<p>[1]. LIU Heng-yu, ZHANG Tian-cheng, WU Pei-wen, et al. A review of knowledge tracking[J]. Journal of East China Normal University (Natural Science), 2019, (5): 1-15. DOI: 10.3969/j.issn.1000-5641.2019.05.001.<br>[2].Piech, C., Bassen, J., Huang, J., Ganguli, S., Sahami, M., Guibas, L. J., & Sohl-Dickstein, J. (2015). Deep Knowledge Tracing. In Advances in Neural Information Processing Systems (pp. 505-513). <br>[3]. Z. Cui, R. Ke, and Y. Wang, “Deep Stacked Bidirectional and Unidirectional LSTM Recurrent Neural Network for Network-wide Traffic Speed Prediction,” in 6th International Workshop on Urban Computing (UrbComp 2017), 2016. <br>[4]. Chun-Kit Yeung and Dit-Yan Yeung. Addressing two problems in deep knowledge tracing via predictionconsistent regularization. In Proceedings of the 5th ACM Conference on Learning @ Scale, pages 5:1–5:10. ACM, 2018.</p>
<p>文中部分图片来源于网络,若侵权删</p>
<p>END </p>
<p><strong>招聘信息</strong> </p>
<p>好未来技术团队正在热招测试、后台、运维、客户端等各个方向高级开发工程师岗位,大家可扫描下方二维码或直接搜索关注“<strong>好未来技术</strong>”公众号,点击“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=fcKj6w38n2d9Qp2DfCVplQ%3D%3D.7wiKA5P3ety08dfRM202xben1A8jbPoBrVmbtseGr5gV6YJ61elySN2VhM2BvevcRng6IY6hbebVTa55qdj1FC2R7QxLNOvMMVReTr02f09WEuKgA2Z03EttS62PqBe5dQc7mZJzIm0m0v0Gjhaw8A7YeVuJYYFdevzStknotN6u3EOaQUw98ouFAFczjBDTaSJf4CTaCnjGL5NtRbtRyK03iq%2F1flQKypHi7BtHegLfmwxkwvcPXQYay1RdiG49h1O7ViksH4TOmj0h21VcTRHgPRMvsVe%2FkPudra6%2FprWwEJrxvcd5As9Is2za%2FHmt" rel="nofollow">轻量型TV端遥控器交互类库最佳实践</a></p>
<p><a href="https://link.segmentfault.com/?enc=%2BbVeyVzlcEIKfIm4eyeZaA%3D%3D.9d7%2FnwCd%2Bvo6vnsqC7HOpXKSzsHTB5vOJvkctPnRm4XbtEkjWJYBY3LyjwJbiBQgQogOLMaVeMbkR5JmgpJDpI34%2B4bwfmzMbUMWWQoySMZxBoAdejbtuKqLvVpLIISya%2FXl9UI73RUMCGMc561ahRGpiOqJ6%2F3NFXL5ghNAFmRI7lh1YBSMvW8CL%2BblewG4CxD2aeo5oqFlNyKTWc35xQy8B0PHK%2Fb56hfGDVWa79%2BvWxs2CTArHI8OWGYAsayQKAI%2BpTrxvhz7QDr5OZQegrr0%2F2mYOirL%2BnwvyswnwAt7iPwhNqNYIUH1UsWbZCsU" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><a href="https://link.segmentfault.com/?enc=HjclFD3hm5U9LGfgkeTl9Q%3D%3D.PL2EbyUPoZxJkHlw46%2FLymp%2FAsU3JHHRpaE9SzWPcaZV9YEqqTqgkW4r8tpmAcSxPh6adJFaAkW1uUJaUzpql3u%2FPf0Tvr4pd4A0ui7BGX9g08pV1mT%2BfP%2BZrtjH64asl1B%2BLhS2FpBae%2BBzP%2Fb1IY%2FJcutYSYfFhi2zzALfiBdjt%2FRQj0B8JsEGvPGwdKHAO9Zmnk6uYl623Hsc9buC4%2Bc%2BOBQfV4Tl8BghOvFYfFGrvZsYPwg9tz6GqR%2FSOHl4KuWWgsAevIARPU1WyH9XOOMheJoL%2FzhkI3CuQSx8915lqQbOpLUeT2pESgvLrbBr" rel="nofollow">用技术助力教育 | 一起感受榜样的力量</a></p>
<p><a href="https://link.segmentfault.com/?enc=QnokuNstWyF%2Fs%2BUmIXzPfg%3D%3D.KEeB1m0fIl7mPJyyft0aliZ7V48diA7066RssfzHeqp02yRD3k2PjJ2To42x7nsuQ%2BkVGgbwZhXUndIgd1gCBvUrHp4wvy259F8fSfHGxGeAokPrfV5daoT1MhH6mCQ3ag%2BhLXwFX%2BYzangqqtoMHEAdxbgD4nBJB52bvqfBds0qbE8Qbr8bpyf5zRTNKVGvz7CaQOJa00yrKktQdvA%2B73WWO4dp5nfU4%2FdCXvQaabH9Ehov5zM5AMs8PmbB8JXtoECZoqpsCZLZM5IChwbO7xMq1SDZ3SPz9UbZ4O9gUOlIsryEQz%2Fuc%2FwQJXeL%2Bp36" rel="nofollow">想了解一个异地多校平台的架构演进过程吗?让我来告诉你!</a></p>
<p><a href="https://link.segmentfault.com/?enc=wj4gNy%2FDvkN6T2p2sv4DbQ%3D%3D.3DHSabIgeY9NICmqCpeuCeWybc8lWMg7gb5mB3K6DqCDQm081gNMeDzcsyWx4wS8Yu5QL1pAf50XofFX4nU9cDKhc2drmC0WJ5BrMIkrwmB%2F%2FoTbuYfQaaXKeUsKjWJgqJNpr%2Fa6%2BCNH46qQqi8oYYjQDpH5LxwI%2FNun7Kneh%2FICruBSnDNGjuH1Psf5ZNp2ilOWy6IBcJc9CYhS%2FLolSZZCNHDmbwEsonIg9V3YriGz9%2F%2FKPLY99SK%2FUE3iwtd9hA0q8fthJHkGXy0d%2BDbNcrjkd2kLbt44xTxkGoFmYyGbFDAltHB9iVBNELPBpR1X" rel="nofollow">摩比秀换装游戏系统设计与实现(基于Egret+DragonBones龙骨动画)</a></p>
<p><a href="https://link.segmentfault.com/?enc=2FcZaxj%2FfwPomj0TmzYujw%3D%3D.GOP0%2BF%2BGU06DLYwss47mqPrK4zuWKI977y7aYp14Nhn2ydtZ5%2FHPc0n4TsZg6mc9lKdUOo4C4eWQw8E6MIddCweDKkDPl3xzw0L8%2FsHNAs%2FPDjC834sW4ChLdrvtfK1Lwn4HcGZ2lVZt5Ksg5MOoGGeD%2FoTfNpv70n%2Bm8ZIvUGLBQebcFqttC7U%2FXCaxczVZPw5xDzKiSs1AeiojmuzGEdTqfJO%2F0sQNly80JFuIb2Ao30wNqdSrRLWqUJM2MW10zgeuIRGvFfDxjjuFE%2FUjmz9Rm7cSPYloPI5PwfLhegVGGPUOacNjpmVaPLt6cMb4" rel="nofollow">如何实现一个翻页笔插件</a></p>
<p><a href="https://link.segmentfault.com/?enc=hJM2Mej4Coq0XsIVYF4h8g%3D%3D.V7YUFdOwbi1HV%2BoDO3Dl%2B8d3Vhnfd%2B3FcSeqzA9u7ZMGooZwY6Zeg1FHFb1d6LYALKp0hamKgq6bsU08AfZ6tL4Z2H5OQe2kqPmiq3aCo14xLwYFVeQ1bkhJua8nftZddtzFBx56VaL60xlr7PSyF0YhiJIqBWhhF%2FMiYsHRA41ZXPYbT7QZRDBiEIk1OpuqFwOQJdLdzRV1s1ZuUJVk1KS6i7jkdU5iNlbc%2BudRVzwRnyV8c4FnfyiMooSqSATH87fsgLFXy5GO0EsVgyIKh1EhPvc5PKbbjy9XMeOx47hBv06%2BPCiJV3pnTET19rU%2B" rel="nofollow">产研人的疫情战事,没有一点儿的喘息</a> </p>
<p><img src="/img/remote/1460000022458136" alt="" title=""></p>
轻量型TV端遥控器交互类库最佳实践
https://segmentfault.com/a/1190000022458131
2020-04-24T17:09:08+08:00
2020-04-24T17:09:08+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<h2>一、介绍</h2>
<p>各位小伙伴,大家好,今天给大家分享的是一个TV端遥控器的交互类库的实现。</p>
<p>好未来从去年的4月底到7月中旬从零到一的开发了我们的第一款智能硬件 - 未来宝盒。未来宝盒类似小米盒子,卡通造型,可以很方便的卡在电视机的屏幕上方,用高清线和电视机连接,内置了好未来的优质课程资源,是由luncher + APP组成的,App里必然的会有一些web页面,交互方式和大家平时开发的H5或者web页面不同,未来宝盒使用遥控器与H5页面进行交互(对,你没听错)。遥控器核心交互按键有上下左右四个方向键,OK键和返回键。在早期的时候,TV端内只有两个简单的页面使用到了遥控器,订单页和直播课程列表。但是里面的跳转元素都是 A标签实现的,并且只有简单的上下滚动且遥控器的交互逻辑和业务耦合在一起,无法复用。然而随着运营伙伴有了新的想法要在TV端上去验证的时候, web或h5是一个很好的方案,但是目前市面上也没有成熟的解决方案,所以需要自己撸一个。</p>
<h2>二、关于适配</h2>
<p>在开始讲交互类库的实现之前,先讲一下TV端内页面的适配,因为在实现类库的过程中,需要计算各个元素之间的位置关系。TV端的页面和大家经常写的H5页面也没有特别大的区别,TV端App是由安卓端开发,承载页面的容器依然是webview,内核是Chromium(目前我们盒子里的版本是65), 跟我们开发安卓内的H5应用是一样的,只不过尺寸更大了(1920 x 1080)。屏幕适配依然使用移动端的利器-rem; rem的简单实现。</p>
<pre><code class="javascript">function initRem(opt) {
let oWidth = document.documentElement.clientWidth,
_designW = opt && opt.hasOwnProperty('designWidth') ? opt.designWidth : 1920,
_scale = opt && opt.hasOwnProperty('nScale') ? opt.nScale : 100;
document.documentElement.style.fontSize = oWidth / _designW * _scale + "px";
}
initRem();</code></pre>
<p>调用initRem()时,opt是个对象,可以传可不传。如果不传 默认设计稿宽度1920,缩放比例100(写样式的时候便于计算)</p>
<pre><code class="js">opt = {
designWidth: 1920, // 设计稿宽度
nScale: 100 // px2rem的缩放比例
}</code></pre>
<p>比如 在1920宽的设计稿上获取的某元素的宽度是100px, 在写样式的时候, width:1rem;就可以了。防止页面跳动,把这个方法外链出去,放到 head 标签中即可。</p>
<h2>三、核心技术点</h2>
<ul>
<li>确定主体内容需要向上滚动或者向下滚动的临界点的确定</li>
<li>使用CSS3处理位置偏移让滚动更平滑</li>
<li>水平和竖直方向上筛选最近元素的逻辑</li>
<li>getBoundingClientRect().left 获取元素距浏览器左侧的准确距离</li>
<li>
<p>获取当前滚动内容滚动的距离(以下两种方法根据场景选用)</p>
<ul>
<li>dom.style.transform</li>
<li>getComputedStyle(dom).transform</li>
</ul>
</li>
<li>抹平由于布局差异导致的计算偏差</li>
<li>所有TV端的页面都有一个刷新按钮,所以会涉及到原生组件和H5组件间移交焦点控制权的问题。</li>
<li>使用电脑的方向键、回车和ESC键模拟遥控器的按键以实现TV端调试。</li>
</ul>
<h2>四、遥控器对象实现</h2>
<p>遥控器对象采用经典的构造函数 + 原型的混成方式实现。构造函数内的方法每个实例独享,原型内的方法各实例共享以节省内存。</p>
<h4>1. 遥控器对象概览</h4>
<p><img src="/img/remote/1460000022458135" alt="脑图.png" title="脑图.png"></p>
<h4>2. 遥控器对象的Constructor</h4>
<p>在写HTML结构的时候,给需要获取焦点的元素增加 <code>autofocus</code>的属性,或者用过js动态生成HTML结构的时候加上<code>autofocus</code>,通过 <code>querySelectorAll("*[autofocus]")</code>来选择我们需要的元素集合。</p>
<p>如上图所示,遥控器对象的构造函数由如下一些核心属性构成</p>
<pre><code class="js">this.focusArea = opt.allFocusParent || document; // 当前界面下所有需要获取的焦点的父级DOM
this.focusGroup = []; // 所有需要获取焦点的DOM合集
this.focusData = []; // 所有需要获取焦点的DOM的x,y,中心点及index
this.curDom = null; // 当前DOM对象
this.index = 0; // 当前高亮的index
this.leftRes = null; // 当前元素左侧的按钮集合
this.topRes = null; // 当前元素上方的按钮集合
this.rightRes = null; // 当前元素右侧的按钮集合
this.bottomRes = null; // 当前元素下方的按钮集合
this.key = "kindex"; // 自定义属性 用于快速定位DOM
this.canuse = true; // 标记当前实例是否可用
this.highlightClass = opt.highlightClass; //高亮的样式
this.modifyDis = opt.modifyDis || 0; // 用于修正偏移(主要是固定定位的头部)
this.onconfirm = opt.onconfirm; // 确认的回调
this.onback = opt.onback; // 返回的回调
this.scrollContainer = opt.scrollContainer || document.documentElement || document.documentElement.body; // 滚动DOM对象容器
this.scrollObj = opt.scrollObj || document.getElementsByTagName("body")[0]; // 需要滚动的DOM对象
this.scrollBar = opt.scrollBar; // 自定义滚动条对象
this.scrollBarCtl = null; // 滚动条控制滑块
this.barMove = 0; // 滚动条滑块动的距离
this.lastPos = 0; //记录内容部分上次的位置
this.stopPropagation = opt.stopPropagation || false; // 按上方向键且上方没有焦点元素的时候 是否允许调用TV端的方法
this.init(); // 初始化</code></pre>
<h4>3. 遥控器对象的Prototye</h4>
<p>遥控器对象的原型分为如下几个部分</p>
<h5>I 事件监听</h5>
<p>通过监听<code>document</code>的<code>keycode</code>,给遥控的四个方向键、enter键和back键绑定相关的事件回调</p>
<pre><code class="js">// 绑定事件
bindEvent(){
let _this = this;
document.addEventListener('keydown', function(e) {
if (!_this.canuse) {
return false;
}
let keycode = e.keyCode;
// 37,38,39,40,13,27 90 为电脑键盘上的keycode
// 21,19,22,20,23,4 为 遥控器上的keycode
if (keycode == 37 || keycode == 21) {
// left
_this.leftFn(e);
} else if (keycode == 38 || keycode == 19) {
// up
_this.upFn(e);
} else if (keycode == 39 || keycode == 22) {
// right
_this.rightFn(e);
} else if (keycode == 40 || keycode == 20) {
// down
_this.downFn(e);
} else if (keycode == 13 || keycode == 23) {
// enter
_this.enterFn(e);
} else if (keycode == 27 || keycode == 90 || keycode == 4 ) { // 90是 字母 z; 4是遥控器的返回键
// 27 为 ESC, 但是ESC首先要执行系统的事件,再执行html的事件。可能导致 按一下esc 执行返回上一页不起作用。
_this.backFn(e);
}
}, true);
}
// 左键回调
leftFn(e) {
this.index = this.getNextIndex('left');
this.highlight();
}
// 上键回调
upFn(e) {
this.index = this.getNextIndex('up');
if (!this.topRes.length && this.stopPropagation == false) {
console.log('H5当前按钮上方已经没有可供获取的焦点,即将把焦点的控制权移交给TV端。');
try {
// 调用TV端上的方法
qkJsCallAndroid.onTopFocusNone();
console.log('H5的焦点控制权成功移交给TV端。');
this.dropFocus();
} catch (e) {
console.log(e);
}
return false;
}
this.highlight();
}
// 右键回调
rightFn(e) {
this.index = this.getNextIndex('right');
this.highlight();
}
// 下键回调
downFn(e) {
this.index = this.getNextIndex('down');
this.highlight();
}
// OK 回调
enterFn(e) {
if (this.onconfirm && typeof this.onconfirm == 'function') {
// 执行 回调并传入当前DOM对象
this.onconfirm(this.focusGroup[this.index]);
}
}
// 回退回调
backFn(e) {
if (this.onback && typeof this.onback == 'function') {
this.onback(this.focusGroup[this.index]);
}
}</code></pre>
<h5>II 核心函数</h5>
<ul><li>init() 顾名思义 初始化函数,初始化的内容分为如下几个部分:</li></ul>
<pre><code class="js">init(){
// 初始页面到顶端
window.scrollTo(0, 0);
// 开启GPU执行动画
this.scrollObj.style.transition = "all .3s ease";
this.setTranslateY(this.scrollObj, 0);
if (this.scrollBar) {
// 初始化自定义滚动条
let containerH = this.scrollContainer.clientHeight * 1, //滚动对象容易的高度
scrollObjH = this.scrollObj.clientHeight * 1, // 动的DOM对象的高度
scrollBarH = this.scrollBar.clientHeight * 1; // 滚动条的高度
this.scrollBarCtl = this.scrollBar.firstElementChild; // 滚动条指示块对象
if(scrollObjH < containerH){
this.scrollBar.style.display = 'none'
}else{
this.scrollBar.style.display = 'block';
this.scrollBarCtl.style.height = parseInt((scrollBarH * containerH) / this.scrollObj.clientHeight) + 'px';
this.scrollBarCtl.style.transition = 'all .3s ease';
this.scrollBarCtl.style.top = 0;
}
}
this.refresh(); // 遍历对应DOM结构内具有 autofocus 的元素
this.highlight(); // 默认第一个选中
this.bindEvent(); // 绑定遥控器事件
}</code></pre>
<ul><li>contentScroll() 内容主体滚动逻辑,主要是模拟页面的上下滚动。让主体内容向上滚动或者向下滚动的临界点的图示如下:</li></ul>
<p><img src="/img/remote/1460000022458134" alt="临界点.png" title="临界点.png"></p>
<p>代码逻辑如下:</p>
<pre><code class="js">// 内容滚动逻辑
contentScroll(){
let tempST = window.getComputedStyle(this.scrollObj).transform.toString();
if (tempST == 'none' || tempST == '0') {
tempST = 0;
} else {
tempST = tempST.substring(7);
tempST = tempST.substring(0, tempST.length - 1).split(',')[5];
}
// 高亮后做判断 获得焦点的元素是否在可视区内
let scrollObjST = Math.abs(tempST), // 滚动对象上移的距离
containerH = this.scrollContainer.clientHeight, //滚动对象容易的高度
curObjH = this.curDom.offsetHeight, // 当前获得焦点对象的高度
curObjOffsetTop = this.curDom.offsetTop, //
ScrollY = 0; // y方向上需要滚动的距离
if ((curObjOffsetTop + curObjH) > (containerH + scrollObjST)) {
// console.log('在浏览器下方不可见');
ScrollY = curObjOffsetTop + curObjH * 1.4 - containerH;
if (Math.abs(ScrollY) > (this.scrollObj.clientHeight - this.scrollContainer.clientHeight)) {
ScrollY = this.scrollObj.clientHeight - this.scrollContainer.clientHeight;
}
// 优化一下离顶部的距离,
ScrollY = parseInt(ScrollY) + curObjH * 0.2;
// 滚动条移动的距离barMove的计算方法 ScrollY / (scrollObjH - containerH) = barMove / (this.scrollBar.clientHeight - scrollBarCtl.clientHeight)
// 自定义滚动条逻辑
if (this.scrollBar) {
this.barScroll('up',ScrollY)
}
this.setTranslateY(this.scrollObj, -ScrollY);
this.lastPos = Math.abs(ScrollY);
}
if (scrollObjST > 0 && (scrollObjST + this.modifyDis) > curObjOffsetTop) {
// console.log('在浏览器上方不可见');
ScrollY = curObjOffsetTop - curObjH * 0.6 - this.modifyDis;
if (ScrollY < 0) {
ScrollY = 0;
}
// 自定义滚动条逻辑
if (this.scrollBar) {
this.barScroll('down',ScrollY)
}
this.setTranslateY(this.scrollObj, -ScrollY);
this.lastPos = Math.abs(ScrollY);
}
}</code></pre>
<ul><li>barScroll(): 滚动条逻辑。在init()中通过实际内容与可视区的高度比值动态计算出滚动条滑块的实际高度,在内容滚动的处理逻辑中,如果需要显示滚动条则执行滚动条逻辑。</li></ul>
<pre><code class="js">// 滚动条逻辑
barScroll(scrollDirection, ScrollY) {
let containerH = parseInt(this.scrollContainer.clientHeight);
if(scrollDirection == 'up'){
// scrollDirection 内容即将向上滚动
this.barMove = 0; // 修复一下滚动条滑块的位置
this.barMove += parseInt(ScrollY * (this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight) / (this.scrollObj.clientHeight - containerH));
if (this.barMove > (this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight)) {
this.barMove = this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight
}
this.setTranslateY(this.scrollBarCtl, this.barMove);
}else if(scrollDirection == 'down'){
// scrollDirection 内容即将向下滚动
this.barMove -= parseInt(Math.abs(ScrollY - this.lastPos) * (this.scrollBar.clientHeight - this.scrollBarCtl.clientHeight) / (this.scrollObj.clientHeight - containerH));
if (this.barMove <= 5) {
this.barMove = 0
}
this.setTranslateY(this.scrollBarCtl, this.barMove);
}
}</code></pre>
<ul><li>getNextIndex() 获取下一个元素的index。水平和竖直的获取逻辑稍不同。竖直方向从上向下要让每一行的按钮依次获得焦点,所以筛选逻辑是找到与当前获得焦点的元素竖直方向上最近的元素并且中心点间距最小的。水平方向上先筛选与当前获得焦点的元素在同一水平线上且中心点间距最小的。</li></ul>
<pre><code class="js">// 获取下一个元素的index 水平和竖直的获取逻辑不同
getNextIndex(direction) {
let theNearest = null,
allResult = [], // 获取对应方向上所有的按钮
curParam = this.focusData[this.index]; // 当前元素对应的参数
if (direction == 'left') {
// 分别筛选出 当前高亮元素 左侧水平方向上的所有需要高亮的元素存储于 allResult
this.focusData.forEach(item => {
if (item.cx < curParam.x && item.cy > curParam.y && item.cy < (curParam.y + curParam.h)) {
allResult.push(item);
}
});
if (allResult.length > 0) {
this.leftRes = allResult;
theNearest = this.leftRes[this.getMinIndex(this.leftRes)];
}
} else if (direction == 'up') {
// 筛选出 当前高亮元素 下方所有需要高亮的元素存储于 allResult
this.focusData.forEach(item => {
if (item.cy < curParam.cy) {
allResult.push(item);
}
});
theNearest = this.getNearDataVertical(allResult, 'cy', 'max', 'up');
} else if (direction == 'down') {
// 筛选出 当前高亮元素 下方所有需要高亮的元素存储于 allResult
this.focusData.forEach(item => {
if (item.cy > curParam.cy) {
allResult.push(item);
}
});
theNearest = this.getNearDataVertical(allResult, 'cy', 'min');
} else if (direction == 'right') {
// 筛选出 当前高亮元素 右侧水平方向上的所有需要高亮的元素存储于 allResult
this.focusData.forEach(item => {
if (item.cx > (curParam.x*1 + curParam.w*1) && item.cy > curParam.y && item.cy < (curParam.y + curParam.h)) {
allResult.push(item);
}
});
if (allResult.length > 0) {
this.rightRes = allResult;
theNearest = this.rightRes[this.getMinIndex(this.rightRes)];
}
}
// theNearest是 focusData中的一个元素
if (theNearest) {
return theNearest.index;
} else {
return this.index;
}
}
// 获取竖直方向上最近的元素数据
getNearDataVertical(arr, state, direction){
let tempArr = [], // 临时数组
resArr = [], // 结果
tempVal = 0; // 中间值
arr.forEach(item => {
tempArr.push(item.cy);
})
tempArr = this.unique(tempArr);
tempVal = Math[state].apply(null, tempArr);
arr.forEach(item => {
if(item.cy == tempVal){
resArr.push(item);
}
});
if(direction && direction == 'up'){
this.topRes = resArr;
}
return resArr[this.getMinIndex(resArr)];
}
// 返回 与 curobj 距离最近的index
getMinIndex(arr) {
let arrDis = [],
curPoint = this.focusData[this.index];
arr.forEach(item => {
arrDis.push(this.getDis(item, curPoint));
})
let minval = Math.min.apply(null, arrDis);
return arrDis.indexOf(minval);
}</code></pre>
<h5>III 高阶方法</h5>
<ul><li>refresh() 主要是用来解决布局dom刷新后丢失掉获取焦点的Bug,函数内要处理的是给所有具有 <code>audofocus</code> 属性的元素绑定事件,并收集一些数据为接下来的筛选按钮做准备。</li></ul>
<pre><code class="js">// 刷新
refresh() {
let _this = this,
objs = _this.focusArea.querySelectorAll('*[autofocus]');
this.focusGroup = []; // 所有需要获取焦点的DOM合集
this.focusData = []; // 所有需要获取焦点的DOM的x,y,中心点及index
this.curDom = null; // 当前DOM对象
if (!objs.length) {
console.warn('没有获取到焦点元素集合');
return false;
}
objs.forEach((item, i) => {
item.setAttribute(this.key, i);
this.focusGroup.push(item);
this.focusData.push({
txt: item.innerHTML.replace(/<.*?>/g,"").replace(/[\r\n]/g,"").replace(/[ ]/g,"").trim(),
w: parseInt(item.offsetWidth),
h: parseInt(item.offsetHeight),
x: parseInt(item.getBoundingClientRect().left),
y: this.formatInt(parseInt(item.getBoundingClientRect().top)),
cx: this.formatInt(parseInt(item.getBoundingClientRect().left) + parseInt(item.offsetWidth / 2)),
cy: this.formatInt(parseInt(item.getBoundingClientRect().top) + parseInt(item.offsetHeight / 2)),
index: i
});
});
}</code></pre>
<ul><li>highlight() 定位到当前元素并给与高亮样式</li></ul>
<pre><code class="js">// 高亮
highlight(){
this.focusGroup.forEach(item => {
item.classList.remove(this.highlightClass);
});
this.curDom = this.focusGroup[this.index];
if(this.curDom){
this.curDom.classList.add(this.highlightClass);
this.contentScroll();
}
}</code></pre>
<ul><li>disable() / enable() 主要是为了解决弹层出现的时候 禁用和启动主体内容的滚动。canuse为false的时候,暂停对遥控器实例的事件监听。详见上面的 <code>事件监听</code> 的逻辑</li></ul>
<pre><code class="js">// 禁用
disable() {
this.canuse = false;
}
// 启用
enable() {
this.canuse = true;
}</code></pre>
<ul><li>dropFocus() / getFocus() 失去焦点,获得焦点</li></ul>
<pre><code class="js">// 失去焦点
dropFocus() {
this.focusGroup.forEach(item => {
item.classList.remove(this.highlightClass);
});
}
// 获取焦点 todo
getFocus() {
this.highlight();
}</code></pre>
<ul><li>go(index) 定位到期望元素</li></ul>
<pre><code class="js">go(index){
if (index == isNaN) {
console.log(index + '不是数字呢');
return false;
}
this.index = index;
this.highlight();
}</code></pre>
<h5>IV 工具方法</h5>
<pre><code class="js">// 返回两点间的最短距离
getDis(p1, p2) {
let dx = Math.abs(p1.cx - p2.cx),
dy = Math.abs(p1.cy - p2.cy);
return parseInt(Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)));
}
// 数组去重
unique(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] == arr[j]) { //第一个等同于第二个,splice方法删除第二个
arr.splice(j, 1);
j--;
}
}
}
return arr;
}
// 设置 translateY
setTranslateY(obj, val){
obj.style.transform = "translate3d(0," + val + "px,0)";
obj.style.webkitTransform = "translate3d(0," + val + "px,0)";
}
// 格式化数据为10的整倍数 以抹平布局上的轻微差异
formatInt(num, prec = 1){
const len = String(num).length;
if (len <= prec) { return num };
const mult = Math.pow(10, prec);
return Math.floor(num / mult) * mult;
}</code></pre>
<h2>五、遥控器对象的调用</h2>
<pre><code class="js">let mainKB = new RController({
highlightClass: 'highlight', // 高亮样式
allFocusParent: oWrap, // 所有需要获取焦点的父级DOM对象
scrollObj: oIndex, // 滚动DOM对象
scrollContainer: '', // 滚动DOM对象容器
modifyDis: oHeader.height()// 用于修正偏移(主要是固定定位的头部)
});
mainKB.onfirm = function(curObj){
// 按enter键的回调 返回的curObj为原生DOM对象, 集合第三方框架或者类库实现 跳转/ajax 等业务逻辑。
}
mainKB.onback = function(){
// 按返回键的回调
}
</code></pre>
<h2>六、学习与反思</h2>
<ol>
<li>第一次做遥控器的逻辑的时候,都比较简单,然而这次的逻辑比较复杂,而且未来的尝试预计也会越来越复杂,所以<strong>当类似逻辑出现超过两次就要考虑把功能抽象出来做成基础库</strong>,既方便了自己,沉淀了技术,更可以方便大家,提升开发效率。</li>
<li>TV端的调试时,没有盒子和遥控器,完全依赖浏览器,<strong>办法总比问题多</strong>。</li>
<li>尽管交互类库不会过多的限制UI,但是<strong>UI最好也要有一定的规范</strong>,这样能规避一些莫名的问题。</li>
</ol>
<p>end</p>
<p><strong>招聘信息</strong> </p>
<p>好未来技术团队正在热招测试、后台、运维、客户端等各个方向高级开发工程师岗位,大家可点击“<strong>好未来技术</strong>”公众号“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=2vYhPZ1y7uS32mYO4ob2EA%3D%3D.XIzmN6RieT%2BIa5x7sNgwHVFPEgXgnXSFRJBN4V5IyD0BkTa7NcYlFh9vem77PfdCnw3Mcx0eEUJRx1j2yrgTnl3sCLu5P8fVhS%2Bg7IaTRpCXL6gWM6dPeknJ2NRU6myt7PJpiGPYXUVClWoO4l7cdXWuDwT0N3O%2FoKclbb7NIA8JS3Ks%2BDt9HITf0RfvMbgN3Oh0gGz62eKehbEQ5u0JcQOU4dySHRpVxBauUMQIuUhS8BM%2F2GIhTPRjwDdfybFqTZmCpVyqA%2FJRsg5YX6b2A0hyjIgI3qgGTku2V57LcEXHQd2Ie1EeBe5Uh9jinXBC" rel="nofollow">"考试"背后的科学:教育测量中的理论与模型(IRT篇)</a></p>
<p><a href="https://link.segmentfault.com/?enc=LsL80qxbvKHxgIE3pPbvrg%3D%3D.NoyME9kRNi3qTX89D2YZQhOOmXVSdk9eQMC9U6yK7h0Z%2FXOWZCj9nXto13NuBRSm49xYPmonH7pRyFF8JzVTAfSbXVQmAZLV54u7YxqQ2svEnel0Ytvejnn3JSpKj04KPfytRjXHl4n2Gp1Hk%2FYk%2BS2kY6Cht51gVg8Gk6WS7U7z7wlYiBljLU6VLDAF2PSX0GNVKWMnunKD5jRdYNOp4fx2o%2BAi%2Bku%2F6M8tqvQWxY19yn%2FiDNqskxpBtIrbXMWLkXrqYecM09L0lNF7feXf3sXM6WZeKffJWmXjC2GzZZAPoyLNKF6dNpPt198en8Ti" rel="nofollow">用技术助力教育 | 一起感受榜样的力量</a></p>
<p><a href="https://link.segmentfault.com/?enc=FIAqE6lg%2FujoJ2ZRr6VPGw%3D%3D.wSGcCB%2BRu0z%2FytzqxuY%2Fkk9EEGj7v1giL4VyCBaBsUd0fQV7kBzSwD8I9S%2BMmb2l5EBXJI2HH80bzpMhG2gIoL6DH1Qtp9yWXZcoi7uiJySCB2e8eal9rRG0VNyQxW3Hc6sL8WEGP9ncwzNhviNiCUmRt5ORnbvB%2BLW6NX38W6ACpneMbdWoqF7QX3uHkQQIZmgVXvma6Kje6L8CfbT9%2FWVxGRBxn1Lho94XLZF%2BPYsMIAYD8xYL7c5Ri8ab2Ole6O64tUHfhr5V%2BCnGwIXwwwcVHRTPN9qN99n1lVmoidi8hiVwia2Ks9vo2ebwZ4gn" rel="nofollow">想了解一个异地多校平台的架构演进过程吗?让我来告诉你!</a></p>
<p><a href="https://link.segmentfault.com/?enc=ct81nmjLBL6XdPzBI3DbTQ%3D%3D.CdIV1vEAUK1zEQWZdePjFC2NgECklsFPDQSjh6XAVwczL4w2oAqZOJVfGlV6GZEwdn12eOjrznDBV8UYCp1gSkjHJdJHWJu91GQRzCW01i8JfLJJOKE4RRQtA5HbJfvP2fn4NZM%2ByJkxV8oFGZhBpDRekPfeeJquJGAyoMtdidF%2FQ95QADNtjfJb5gGfAwt2gw8JWy8dDKKCdYgfUl0%2BCTRU8N2uHjGtOWB9RDmo%2B7J8sgUWQ4HdIJqAIgoH6%2BIT%2F0SfVvDGfY%2F9rLADJymSdOjhiDUjTfZxoViPbI3IOUjOYjVzaDokHZI4TUZGkGP0" rel="nofollow">摩比秀换装游戏系统设计与实现(基于Egret+DragonBones龙骨动画)</a></p>
<p><a href="https://link.segmentfault.com/?enc=dq2Y1XksIix64sk18Qg%2FRg%3D%3D.TagOnYEh8DIL33WEPI6s%2BqQQ14PcyqyTxEP4oham4U73UTr8DqQwQ7MdZTWVksZRDffz%2FXZqyHC%2FaHtwv0pDoibdxSbWKEyJOH3t%2BtkjnLGfwEG%2BPMToH2%2B%2BUUdSFZphWopxubotmRuVkeNXd%2ByFbmmep4V5E%2F9K6aYku7jIXH7iqWhRz3QRgzRpVjFbwVfaZZQIgB56iVKHUEA%2FZjvg%2Ber2LKB2mFPpRSEvYU93riwkdmpAT%2BYWElxszxy9o4QhV9uOS4GRazZoCXUkvkWF%2B6wciZU6A8HPA9eeiibgxHo0MlwnWcddHUrmZJSJftS2" rel="nofollow">如何实现一个翻页笔插件</a></p>
<p><a href="https://link.segmentfault.com/?enc=69b5rt1m1z4TLUZreqhEbg%3D%3D.UWZZLpNG2fuoz73qlfr14u4RjCzqxzja4Ty7i6T2TqzMhfNUboOu0iLmNvnUOCW%2FHFGGmZpmlIoMqewd9%2F%2F98qxnF3jader5o%2B03hEK4b5tG4SPF0Nj08cJ3E%2BVf1JcK4%2BSg8cPRelKlpO8GJNxowKCXkEbNlp%2B3bsgIH4pDXfcHWqTjQrE5IFQqiN%2BwUgv2Xj3c3Gd186kg0RPd3gqTnmaPmZQb425EC5TdcSeejbSo2%2FoZ5kdg7aQC7GFMfRaW%2F%2BuvWvMYutIO7WBl9Virow31ML0HozVVU0bGUczOK%2FcLfRixkNwDV1BtZUm5mPlm" rel="nofollow">产研人的疫情战事,没有一点儿的喘息</a> </p>
<p><img src="/img/remote/1460000022458136" alt="" title=""></p>
摩比秀换装游戏系统设计与实现
https://segmentfault.com/a/1190000022409933
2020-04-17T20:31:46+08:00
2020-04-17T20:31:46+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<blockquote>如何开发一个可维护性,可扩展,跨平台的换装游戏呢,本文从产品设计、龙骨动画、前端再到后端如何管理等角度,来介绍骨骼换装游戏从0到1的实现过程。</blockquote>
<h2>项目背景</h2>
<p> 我们一直在思考,如何能激励学员自主学习的积极性?从上课表现良好、app上及时完成课后练习等途径挣的星星,但这些学员获得的星星多了也没有兴趣了,我们需要一个途径能让学员的星星有价值,这样就能产生一个良性的循环来激励学习。这样的背景下,我们设计了一套这样的系统。</p>
<p><img src="/img/remote/1460000022409937" alt="1.jpg" title="1.jpg"></p>
<p> 我们的学员可以在通过星星来兑换不同虚拟服装,穿上后在课前点名环节就可以穿着这件衣服展示在我们的IPS上,同学们都可以看到,同时还可以领养小宠物,领养宠物之后就可以进行各种各样的学习了,比如:“编程启蒙”,让孩子们感受程序的魅力。</p>
<p> 今天就来说一说换装这一块具体的实现方案。</p>
<p><strong>体验地址:</strong></p>
<p>小demo :<a href="https://link.segmentfault.com/?enc=0reYnP%2FtlazkidBHcNmr7Q%3D%3D.h9zlx2QMv6gD9JG9VvQCty8Evi1xamSXyop00TJULV1MKB1yMDBTLJOsG5A38STmmcu%2BWdecCYEB%2BwaT4f0V9w%3D%3D" rel="nofollow">https://activity.firstleap.cn/egretLeapDemo/index.html</a></p>
<h2>技术选型</h2>
<p>我们考虑到跨平台性,需要在App里、服务号、白板端等场景下使用,决定用H5来开发,然后又对比了一些游戏引擎Cocos2d-js和Egret等,最终决定采用Egret+Dragonbones来实现。</p>
<p><img src="/img/remote/1460000022240631" alt="image" title="image"></p>
<p>官网:<a href="https://link.segmentfault.com/?enc=jH2rFPMHYYz3OqJI%2FlLWVA%3D%3D.zJSY%2BU0VYq5ho1SyTVbJ6KKClbH%2BaotaR0%2Fno0uxcdE%3D" rel="nofollow">https://egret.com/</a></p>
<h2>基本概念</h2>
<p>首先来说一下骨骼动画里的一些基本概念,只有了解了这些,才能进行后面的游戏开发、系统管理的设计等。</p>
<h3>1)骨架</h3>
<p>骨架是骨骼的集合,骨架中至少包含一个骨骼。<br>下图中的root及其以下的树状结构便是一个典型的骨架。</p>
<p><img src="/img/remote/1460000022240632" alt="image" title="image"></p>
<h3>2)骨骼</h3>
<p>骨骼是骨骼动画的基本组成部分。骨骼可以旋转,缩放,平移。下图中的body、mouth等都是骨骼。<br>上图中root下的指针形式的就是一个个的骨骼。</p>
<h3>3)插槽</h3>
<p>插槽是图片的容器,是骨骼和图片的桥梁。换句话来说就是换装的衣服、武器等的图片是放在插槽里的。一个骨骼下面可以有多个插槽,一个插槽下可以有多张图片,但同一时间只能有一张图片处于显示状态,其他的图片会处于隐藏状态。插槽内的图片也可以全部处于隐藏状态。插槽的位置,缩放,旋转那么图片也跟着进行相应的变化。</p>
<p><img src="/img/remote/1460000022240633" alt="image" title="image"></p>
<p>插槽的这几个属性值很重要,每一个物品的位置可能都不是一样的,比如金箍棒这个的坐标是x: -190,y:78,scale:0.8,其他的物品的坐标可能就不是这个了,比如这个西瓜扇子,</p>
<p><img src="/img/remote/1460000022240634" alt="image" title="image"></p>
<p>他的这几个属性值就和金箍棒的不一样了。所以我们后边新建物品(服装、道具等)的时候,除了传图片这几个值也是要可编辑的。</p>
<p>另外一个值得注意的是插槽是有层级的,比如说,帽子按道理讲是要在头发的上面,眼镜是要在眼睛眉毛的上面等。</p>
<p><img src="/img/remote/1460000022240635" alt="image" title="image"></p>
<p>但是这个帽子放上来后呢,后面会镂空一部分,看上去假假的,不立体。这种情况就需要在脑袋的后面在加一个插槽,用来放帽子的后面部分,把脑袋围起来。</p>
<p><img src="/img/remote/1460000022240637" alt="image" title="image"></p>
<p>这样是不是就自然多了,所以我们后边新建一个物品的时候,需要填写他都包含了哪些插槽,然后对每个插槽的图片(影片剪辑、龙骨)进行编辑。</p>
<h3>4)图片</h3>
<p>图片就很好理解啦,换装基本的就是通过更换图片的方式了。Dragonbones支持纹理集和单个的图片。</p>
<h3>5)骨骼动画</h3>
<p><img src="/img/remote/1460000022240636" alt="image" title="image"></p>
<p>动画设计这块就不多说了,跟Flash很类似,交给我们的UI老师就行了,我们只需要调用动画名播放就可以了</p>
<pre><code class="js">animation.play("stand");</code></pre>
<h3>6)数据格式</h3>
<p>那一个龙骨的人物建好之后选择导出,导出界面如下:<br><img src="/img/remote/1460000022240638" alt="image" title="image"></p>
<p>会得到3个文件,一个是纹理集,一个是纹理集的配置,一个是龙骨的骨骼数据。之后后台创建角色的时候就需要上传这三个文件。</p>
<h2>前端H5程序实现</h2>
<p>我们拿到文件后,该怎么显示到我们的页面上呢?这个文档里也写的挺清楚了,不算太复杂。</p>
<h4>1)实例化资源</h4>
<pre><code class="js">let dragonbonesData = RES.getRes( "mobi_ske_json" );
let textureData = RES.getRes( "mobi_tex_json" );
let texture = RES.getRes( "mobi_tex_png" );</code></pre>
<h4>2)DragonBones动画由工厂类进行管理,可以使用EgretFactory对象来处理所有的动画数据以及贴图。解析数据添加进工厂</h4>
<pre><code class="js">let egretFactory: dragonBones.EgretFactory = dragonBones.EgretFactory.factory;
egretFactory.parseDragonBonesData(dragonbonesData);
egretFactory.parseTextureAtlasData(textureData, texture);</code></pre>
<h4>3)提取出需要的骨架系统</h4>
<pre><code class="js">let armatureDisplay: dragonBones.EgretArmatureDisplay = egretFactory.buildArmatureDisplay("robot");</code></pre>
<h4>4)添加到舞台</h4>
<pre><code class="js">this.addChild(armatureDisplay);
armatureDisplay.x = 200;
armatureDisplay.y = 300;</code></pre>
<h4>5)播放动画</h4>
<pre><code class="js">armatureDisplay.animation.play("Walk");</code></pre>
<h4>6)换装</h4>
<pre><code class="javascript">let hairSlot = armatureDisplay.armature.getSlot("hair");
var hairImg:egret.Bitmap = new egret.Bitmap();
hairImg.texture = RES.getRes("yang_png" );
hairImg.x = hairSlot.display.x;
hairImg.y = hairSlot.display.y;
hairImg.anchorOffsetX = hairImg.width/2;
hairImg.anchorOffsetY = hairImg.height/2;
hairSlot.setDisplay(hairImg);</code></pre>
<p>到这里就显示出来我们刚建立的一个龙骨动画,并且给他换了件行头。</p>
<h2>设计后台管理系统</h2>
<p>那么知道了前面这些,后台该怎么设计出来一个可扩展、易维护的系统呢?<br>首先我们要知道要管理什么。<br>首先管理角色,就是希望切换不同的体型,比如小豆豆和机器人之间切换,这么就创建角色的时候需要把龙骨的三个文件上传。</p>
<p><img src="/img/remote/1460000022409938" alt="10.jpg" title="10.jpg"></p>
<p><img src="/img/remote/1460000022240642" alt="image" title="image"></p>
<p>体型创建完后我们需要方便的管理该体型下的服装,就先需要加个分类,比如帽子、衣服、裤子、武器等分类</p>
<p><img src="/img/remote/1460000022409939" alt="12.jpg" title="12.jpg"></p>
<p>分类该拥有什么属性呢?<br>就拿我们前面说的帽子,肯定要有分类名和缩略图,还需要有他下面包含的插槽。</p>
<p><img src="/img/remote/1460000022240644" alt="image" title="image"></p>
<p>还有一个比较重要的属性就是这个是否可以为空,为什么要有这个设置呢?前面换装的时候我们点击穿上,再点击会脱下该物品,那么如果不允许为空的话,就无法脱下了。举个栗子,比如想换个眼睛、嘴巴这种,那我们就不能给脱掉,否则脑补一下就知道。</p>
<p>添加好这些后我们就可以添加一个个的物品了,比如我们上面说的西瓜帽子。</p>
<p><img src="/img/remote/1460000022240641" alt="image" title="image"></p>
<p>把我们刚才配的资源信息填上,这里的资源类型可以是图片、影片剪辑或者龙骨等。这样一个物品就创建成功了。</p>
<p>这一层基本是开发人员配置的。<br>配完之后就可以添加商品了</p>
<p><img src="/img/remote/1460000022240643" alt="image" title="image"></p>
<p>商品是什么概念呢,其实就是物品拿去卖,加上了商品的一些属性,比如价格折扣促销等等。这样设置完上架就可以在前端商店看到该商品了。</p>
<p>可以看到我们的商店还支持套装,套装是把每个分类的商品组合打包一起卖。</p>
<p><img src="/img/remote/1460000022240639" alt="image" title="image"></p>
<p>每个分类商品最多选一个,其他的属性跟商品是一样的。</p>
<p>到这里基本上我们的换装游戏前端+管理系统就说完了。</p>
<p><img src="/img/remote/1460000022240640" alt="image" title="image"></p>
<p><strong>附:</strong></p>
<p>龙骨导出文件:<br><a href="https://link.segmentfault.com/?enc=9jRevLdp2K2OkYp40cH0Fg%3D%3D.M2ko2YwlK0ZbrCnpKLjK97J2QLdT7WzJ4kXYzxHpRW1Lvb%2FNMWj7hX4l34DsFDgwEzuG6RTfH8DDy%2FkNM5svgCfk4IBdgxRQZisXEDSe0E2D6vQY1kV0%2Frj6BRg6squG" rel="nofollow">https://fcs-activity.oss-cn-beijing.aliyuncs.com/egretLeapDemo/mobbyDragonDemo.zip</a><br>感兴趣的道友可以试一下。</p>
<p><strong>作者简介</strong> </p>
<p>姚辉涛为好未来前端开发工程师</p>
<p>---------- END ----------</p>
<p><strong>招聘信息</strong> </p>
<p>好未来技术团队正在测试、后台、运维、大前端开发等高级工程师岗位,大家可微信搜索/扫描下方”好未来技术”公众号查看其中的“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们! </p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=T%2B06r4C%2Blwl11Wl4UYwo5A%3D%3D.Xp8f1qHBT31%2BJCAURG%2Fki2z0Hd8zGUT1JadKUEbZCSKZZMvwY7nmbDhBXsV2eDkfD4zryfmfYU07ThIQKl7BJU6KzvATcIxH2%2BAtxbelhGvi5Uh%2Ffc6aPdWleUcwQkoLMPKoaKOAlYzRS%2BmtvsgfLRh2lQ0usfUs7ZmgQi3MSEHwwAR6pmDZtBY6sZ4FMIu2PPMTBaXsBtnN6iHpKIMTuP1qZ01qa881hGk0lUdrEmwHaaGIcQKzuk04K40pmpizQFj9deXldYc22mbCG4wK35iaZMdESBTAJCHXl3tROW7EnfGC9KDb3bAfTWYUtsA4" rel="nofollow">如何实现一个翻页笔插件</a> </p>
<p><a href="https://link.segmentfault.com/?enc=BKFNtV1X9oLRogJxQHXOcA%3D%3D.%2F1FvDvWxqTk0sDoV1jzN38QIb2LQ5KwGp98pSSxVTzLs26Z3J7ZXCSXPBGsDOWltNn9uIovzcl1J8azlqm%2FxaGS4109z2LZv526rpCgcoCv7FedQOMPh5cNePpukY0FKwjey2GhW5qS9jGafucESUcYkmoJL2p6%2FSybxRgvhpJb16jls0X%2BMUeyJWAeB%2FQiXh9DIFK9SeNFPnenmhEGSvIHYnYYeTwS9YLsZOJh8VuLmqWuQFloeL6sKD%2B8y5ftMVBK1dtdFKH70qswaSj4ChH5wGYbO5R0YLZL5V3SAm32UXbw2Ao44ZL2fa22ljjoC" rel="nofollow">产研人的疫情战事,没有一点儿的喘息</a></p>
<p><a href="https://link.segmentfault.com/?enc=k8o9eLHaahhiidT8LogJXQ%3D%3D.BYXIXtp6boFzmwWb5clb%2B4W2fV54%2BYphoRRdrhW%2BHvsRy%2BHV0IdYSwfMtk6RMN4JmUVOgVo%2BC4pEor03LeWYJMJaw%2F2uJIThaSguwbiIvrsehjmXf7mfQf69ugkBu1xWr7rmuIWTDKYFu4R%2FPdr6igJKY9p9ljgVVtQd4YK%2FWvXIvMCrZ3NK5NhFWhgppqUdlX%2BD8C5U5sLRtG1wAzK2xAtjcAdKoGtlEF%2FZ7HYwClBRSTsF436Rq%2BgSkadKRR9IzVk15ehPlWNok3GM6OGqDiHWaZhuQcOV%2Ft7wmjzGa%2BhCYPNjTzBjfaOlGR6%2B%2FwsQ" rel="nofollow">DIY技术资讯抓取工具的实践与研究</a></p>
<p><a href="https://link.segmentfault.com/?enc=SDpzWIgKwbxD9lP4bK8h5A%3D%3D.RbdfSiiiGGxf3RgS3yKqOYbtAmE%2FRPPQEU7wuC04XC3upW0MGksNGmP4z0KdwrN3tjRBFv2Rm38gbh7nByIpT4Rc1l5014PX9MYQxZpaEXP6rOv81RuwuBAwdxEfvdpAB%2F8R%2BPUPU10hlQ4zP%2Bz9a9W0%2FKWpSX43Ll35JhsELDDk5a0TE0hkDZ3A8FDm0CCeW5Whc1B5w5UUB0MGWfp0Rhk25EjxbkBBx7l1%2BC9b0Jhsm1zFIo%2BJYn7OYBR%2F2bnpmOmnP%2FuTH%2BFYRTmmmyG4Y4Mmu31H0dpSnnqyQ1TYp%2B5ljFF9MpFlRgwdh95ZMFpV" rel="nofollow">基于TPNN的儿童英语声学模型训练</a> </p>
<p><img src="/img/remote/1460000022409940" alt="" title=""></p>
教你如何实现一个翻页笔插件?
https://segmentfault.com/a/1190000022329922
2020-04-10T16:20:25+08:00
2020-04-10T16:20:25+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<h2>前言</h2>
<p>2020 年的第一篇文章,技术源于生活,作为码农,我觉得最得意的事情大概就是解决一个真实可见的问题了。前段时间我在团队做分享的时候,订了一个比较大的培训室,电脑离的远,所以就需要翻页笔了,而奈何年关将近,口袋吃紧,不舍得买,所以决定自己开发一个。 </p>
<p>因为我当时的 ppt 写在了<a href="https://link.segmentfault.com/?enc=roL9YYfvi2N1zjmf0ciUdg%3D%3D.srjaxCJgjcFfIQcLZuqb2MKXvwpdkMfBYVo7ZzQkpDhm1PdPk1Jx3k7EerggwG3LE7zVwq1MvOHU2fFvOaG%2BDg%3D%3D" rel="nofollow">slides.com</a>上,最先想到的就是搞个 Chrome 插件吧。所以,这篇文章会记录“翻页笔”插件的实现,及 Chrome 插件的简单介绍。</p>
<h2>Chrome 插件介绍</h2>
<p>这部分内容作为对于 Chrome 插件的简单介绍,主要介绍下面几方面:</p>
<ol>
<li>Chrome 插件组成部分</li>
<li>API模块简介</li>
<li>开发及调试</li>
<li>安装</li>
</ol>
<h3>主要组成部分</h3>
<p>我们首先要了解 Chrome 插件的几个重要的组成部分:</p>
<ul>
<li>Background Scripts</li>
<li>UI Elements</li>
<li>Popup.html</li>
<li>Options.html</li>
<li>Content Scripts</li>
<li>Manifest.json</li>
</ul>
<h4>Background Scripts</h4>
<p>Background Scripts 我们可以理解成它是伴随着浏览器运行的,主要处理插件相关的事件,比如:</p>
<ul>
<li>插件第一次安装或者更新</li>
<li>监听 popup 中或者 content scripts 中的消息</li>
</ul>
<h4>UI Elements</h4>
<p>插件能让用户看到的地方,都算是UI Elements,比如右上角的Logo,右键菜单,点击 Logo 提供的弹窗页面等。我们通过 Chrome 提供的 API,能够控制它们的展现形式,比如:</p>
<ul>
<li>为 Logo 添加一个标记 <br><img src="/img/remote/1460000021519716" alt="" title="">
</li>
<li>根据当前页面是否可用,置灰图标 <br><img src="/img/remote/1460000021519715" alt="" title="">
</li>
<li>设置不同大小的图标</li>
<li>Tooltip <br><img src="/img/remote/1460000021519717" alt="" title="">
</li>
<li>右键菜单 <br><img src="/img/remote/1460000021519718" alt="" title="">
</li>
<li>Popup(下面会介绍) <br><img src="/img/remote/1460000021519720" alt="" title="">
</li>
</ul>
<h4>Popup.html</h4>
<p>如上面那张图,实际点击每个插件显示出的弹窗,都是一个 html 文件,它和普通的 html 没什么区别,我们同样可以写样式,引用 js 处理逻辑等。也可以与<code>Background Scripts</code>相互通信: <br><img src="/img/remote/1460000021519719" alt="" title=""></p>
<h4>Content Scripts</h4>
<p>Content Scripts 是指要注入到页面中运行的js,我们可以在这里做一些页面操作,比如操作dom,连接 websocket 等。同样,<code>content scripts</code> 是可以与<code>background scripts</code>,<code>popup.js</code>相互通信:<br><img src="/img/remote/1460000021519721" alt="" title=""></p>
<h4>Options.html</h4>
<p>这个页面提供一些插件的配置信息,同样是个普通的html,具体需要用户配置哪些,需要我们自己去实现。这个页面的入口:<code>chrome://extensions/</code> -> 找到相应插件 -> 扩展程序选项。</p>
<h4>Manifest.json</h4>
<p>以上说了几个重点的组成部分,那么浏览器需要知道我们的脚本文件在哪里,以及其他的一些配置信息,这就需要我们提供个<code>menifest.json</code>配置文件了,浏览器提供给我们的配置信息有很多,引用 chrome 官网的示例,大家用到哪个可以去<a href="https://link.segmentfault.com/?enc=yAbHv2mrPLAfzSqYS4oMIQ%3D%3D.I5is17AF6BNiJbM976A1z9rittNGRXKBXsETWT%2Fkufaj8MppdEraTFrIJRX%2BhH0YGRZp9xAUHQtluAaPOxyRSQ%3D%3D" rel="nofollow">去查询</a>。</p>
<pre><code>{
// Required
"manifest_version": 2,
"name": "My Extension",
"version": "versionString",
// Recommended
"default_locale": "en",
"description": "A plain text description",
"icons": {...},
// Pick one (or none)
"browser_action": {...},
"page_action": {...},
// Optional
"action": ...,
"author": ...,
"automation": ...,
"background": {
// Recommended
"persistent": false,
// Optional
"service_worker":
},
"chrome_settings_overrides": {...},
"chrome_ui_overrides": {
"bookmarks_ui": {
"remove_bookmark_shortcut": true,
"remove_button": true
}
},
"chrome_url_overrides": {...},
"commands": {...},
"content_capabilities": ...,
"content_scripts": [{...}],
"content_security_policy": "policyString",
"converted_from_user_script": ...,
"current_locale": ...,
"declarative_net_request": ...,
"devtools_page": "devtools.html",
"event_rules": [{...}],
"externally_connectable": {
"matches": ["*://*.example.com/*"]
},
"file_browser_handlers": [...],
"file_system_provider_capabilities": {
"configurable": true,
"multiple_mounts": true,
"source": "network"
},
"homepage_url": "http://path/to/homepage",
"import": [{"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
"incognito": "spanning, split, or not_allowed",
"input_components": ...,
"key": "publicKey",
"minimum_chrome_version": "versionString",
"nacl_modules": [...],
"oauth2": ...,
"offline_enabled": true,
"omnibox": {
"keyword": "aString"
},
"optional_permissions": ["tabs"],
"options_page": "options.html",
"options_ui": {
"chrome_style": true,
"page": "options.html"
},
"permissions": ["tabs"],
"platforms": ...,
"replacement_web_app": ...,
"requirements": {...},
"sandbox": [...],
"short_name": "Short Name",
"signature": ...,
"spellcheck": ...,
"storage": {
"managed_schema": "schema.json"
},
"system_indicator": ...,
"tts_engine": {...},
"update_url": "http://path/to/updateInfo.xml",
"version_name": "aString",
"web_accessible_resources": [...]
}</code></pre>
<h3>API模块</h3>
<p>以上介绍了 Chrome 插件的几个重要的组成部分,相信大家大致知道了哪些代码要到哪里写,那么回归到主题,想要了解 Chrome 插件都能做什么,最简单的方式就是看它提供的API,下面我大致列出几个重要模块:</p>
<ul>
<li>accessibilityFeatures:调用 Chrome 提供的无障碍功能,比如放大器,语音等。</li>
<li>alarms:消息通知相关。</li>
<li>bookmarks:书签相关。</li>
<li>browserAction:插件图标相关。</li>
<li>browsingData:浏览器数据处理,比如定期清除 cookie 等本地数据。</li>
<li>certificateProvider:证书相关。</li>
<li>commands:快捷键相关。</li>
<li>contentSettings:定制化浏览器设置,比如针对某个网站禁用cookie。</li>
<li>contextMenus:处理浏览器右键菜单。</li>
<li>cookies:cookie相关,比如修改或监听变化等。</li>
<li>debugger:定制chrome调试。</li>
<li>declarativeContent:定制插件可用状态</li>
<li>devtools:开发者工具相关</li>
<li>downloads:chrome下载相关</li>
<li>storage:插件本地数据</li>
<li>tabs:浏览器tab相关</li>
<li>tts/ttsEngine:语音引擎</li>
<li>vpnProvider:VPN相关</li>
<li>webRequest:网络请求相关</li>
<li>system.memory/system.cpu/system.storage:设备内存,cpu,硬盘等信息</li>
<li>... ...</li>
</ul>
<p>以上列出了部分重要的API,更详细内容请参考<a href="https://link.segmentfault.com/?enc=7FmtNkRrvAmOyNsztUAlAg%3D%3D.721iJh4bPDzLVqG41ELXmf1OqC5taADiSZarrvQyXxjfmoVKPVLSQ13ltozmQ%2FnK%2B5MqwGL4QWuOF3TkQ4HiJA%3D%3D" rel="nofollow">https://developer.chrome.com/extensions/api_index#stable_apis</a>。通过API的这些功能,我们大致了解到了chrome插件都能做哪些事情了,我们可以利用插件去提供更强大的功能。</p>
<h3>开发及调试</h3>
<p>相信大家看了上面的内容可以了解到,其实插件开发是很简单的,用我们熟悉的 html,css,javascript 就可以解决,只需要写出上面介绍的几个组件:background scripts,content scritps,popup.html,options.html,然后在提供一份 Manifest.json 的配置文件就可以了。 </p>
<p>对于这种通用的项目结构,肯定会有一个通用的代码模板了,<a href="https://link.segmentfault.com/?enc=fVgGkjfRmrj0tH01QWzlUQ%3D%3D.IN9wum1%2FzAozBH6owZjN7wSbYrVGv13tJykrr%2FdpK6t6KQjqBxS5HGNuZcvfP8cWNZPh6iVRFTimrKxXObirSg%3D%3D" rel="nofollow">在这里</a>提供了一个可以跨浏览器的插件开发模板,我们 clone 下来直接在里面开发就可以了。 </p>
<p>插件调试稍微麻烦些,对于<code>background scripts</code>,<code>popup.html</code>,<code>content scripts</code>我们要分别以三个不同的方式调试。</p>
<ul>
<li>background scripts:我们需要在插件管理页面(<code>chrome://extensions</code>),点击对应插件的“背景图”按钮,弹出我们熟悉的开发者工具,如图: <br><img src="/img/remote/1460000021519722" alt="" title="">
</li>
<li>popup.html:在弹窗上点击右键->检查即可,如图: <br><img src="/img/remote/1460000021519724" alt="" title="">
</li>
<li>content scritps:因为 <code>content scritps</code> 是注入到页面中的,所以我们可以任意打开一个页面,然后打开开发者工具即可。</li>
</ul>
<h3>安装</h3>
<p>对于开发好的插件,我们可以通过三种方式去安装:</p>
<ol>
<li>发布到 Chrome 商店,当然,这种方式国内大多数用户是访问不到的</li>
<li>打包成<code>.crx</code>文件</li>
<li>直接导入源码文件夹</li>
</ol>
<p>后两种方式用户都需要手动安装,具体步骤如下:</p>
<ol>
<li>打开一个新的浏览器tab,在地址栏输入:<code>chrome://extensions</code>
</li>
<li>打开右上角的开发者模式</li>
</ol>
<p><img src="/img/remote/1460000021519723" alt="" title=""></p>
<ol><li>直接拖入<code>.crx</code>文件,或者点击左上角"加载已解压的扩展程序"导入插件源码文件夹。</li></ol>
<h2>翻页笔插件开发(flip-pen)</h2>
<p>以上简单介绍了 Chrome 插件的几个概念,以及开发及调试方式,相信大家已经有了一个简单的认识,那么我们回过头考虑,要实现翻页笔这个插件,都需要考虑什么?首先我们要确定用户的使用流程,<a href="https://link.segmentfault.com/?enc=Ah4kYfWt02u8kh5BW6QQ7w%3D%3D.XlDPfS36UEszQsZyIb%2BP5Lnx3v4z7eEXvFmaCZzAhov0S46F%2BHVx1qeXcydFRDVRRhqrywwC0GejOkwWue9A0PTTztGYKi82JLiWgUZJpOA%3D" rel="nofollow">这里我录了个视频</a>,展示了使用流程。 </p>
<p>确定了使用流程,那么我们都需要考虑哪些?翻页需要即时通信,也就是要 websocket,我们思考一下这个流程:</p>
<ol>
<li>用户点击插件Logo</li>
<li>当前页面建立 websocket 连接,通知服务端要新建一个一对一连接</li>
<li>服务端生成唯一的 socketId 并记录,同时在 content scripts 中也可以获取到socketId,生成带有 socketId 参数的链接,发送给 popup.html</li>
<li>popup.html 接收消息,将接收到的 url 生成二维码并显示</li>
<li>手机扫码进入遥控器页面,实现发送消息,消息体中要带有链接中的 socketId 参数</li>
<li>服务端收到消息,发送给指定 socketId 的连接</li>
</ol>
<p>流程梳理清楚,就好实现了,下面我们一步一步来。</p>
<h3>实现 websocket 服务,保证一对一连接</h3>
<p>这里我们用 node 实现,借助 <code>socket.io</code> 很好实现,唯一需要注意的是,我们需要保证一对一连接,以免两个页面互相影响,这里我们定义一个全局变量保存所有的 socket 连接,并在 断开连接的时候清除掉。相关代码如下:</p>
<pre><code class="js">
var sockets = {};
io.on('connection', function(socket) {
socket.on('message', function(data) {
console.log(data);
if (sockets[data.id]) {
sockets[data.id].emit('message', data.data);
}
});
// 创建会话
socket.on('new', () => {
const id = socket.id;
sockets[id] = socket;
});
// 移除
socket.on('disconnect', () => {
const id = socket.id;
if (sockets[id]) {
delete sockets[id];
}
});
});
</code></pre>
<h3>在 content scripts 中实现 websocket 连接及键盘消息</h3>
<p>参照上面说的流程,<code>content scripts</code>中我们需要实现的功能:</p>
<ol>
<li>接收从 popup.html 传来的消息,建立 websocket 连接</li>
<li>获取建立成功的 socketId,生成带有 socketId 参数的链接,发送给 popup.html</li>
<li>实现向页面发送上,下,左,右的键盘消息</li>
</ol>
<p>相关代码如下:</p>
<pre><code class="js">import ext from "./utils/ext";
import io from "socket.io-client";
const conf = {
up: 38,
down: 40,
left: 37,
right: 39
};
const URL = "https://pen.zhengqingxin.com";
var flipPen = {};
function onRequest(request) {
if (request.action === "process-page") {
if (flipPen.socket) {
chrome.runtime.sendMessage({ url: flipPen.url });
return;
}
var socket = io(URL);
socket.on("connect", function() {
// 获取建立成功的 socketId,生成带有 socketId 参数的链接,发送给 popup.html
socket.emit("new");
var rcUrl = `${URL}/index.html?id=${socket.id}`;
chrome.runtime.sendMessage({ url: rcUrl });
flipPen = {
url: rcUrl,
socket
};
// 实现向页面发送上,下,左,右的键盘消息
socket.on("message", function(data) {
if (data.action) {
var keyCode = conf[data.action];
var evt = new KeyboardEvent("keydown", {
keyCode: keyCode,
which: keyCode
});
document.dispatchEvent(evt);
}
});
});
}
}
// 接收从 popup.html 传来的消息,建立 websocket 连接
ext.runtime.onMessage.addListener(onRequest);</code></pre>
<h3>popup.html 中生成二维码</h3>
<p>这里我们需要处理两件事:</p>
<ol>
<li>给 <code>content scripts</code> 发消息,通知其用户点了我们的插件,可以建立 socket 连接了</li>
<li>接收回传的链接,生成二维码并显示</li>
</ol>
<p>相关代码如下:</p>
<pre><code class="js">import ext from "./utils/ext";
import QRCode from "qrcode";
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
const { url } = message;
var canvas = document.getElementById("canvas");
QRCode.toCanvas(canvas, url);
});
ext.tabs.query({ active: true, currentWindow: true }, function(tabs) {
var activeTab = tabs[0];
chrome.tabs.sendMessage(activeTab.id, { action: "process-page" });
});</code></pre>
<h3>遥控器页面</h3>
<p>遥控器页面就更简单了,我们只需要建立 websocket 连接,按方向按钮的时候发送对应消息即可,注意要带上接收到的socketId,这样服务端才知道这个遥控器是控制哪个页面的。这里就不列代码了。</p>
<h2>写在最后</h2>
<p>到此就列出了翻页笔插件的实现流程,是不是很简单?估计 100 行代码都不到,<a href="https://link.segmentfault.com/?enc=x7pcPIAjFj1qwssbbNlZ0A%3D%3D.%2FQc9MQiA3xifJA7QXHs2AWAUva9Iunv%2BBu4PmqRWNy8JVqHQUU5q4S1XhL6v5v9c" rel="nofollow">这里可以获取完整代码</a>。如果想直接使用翻页笔插件,也可以 <a href="https://link.segmentfault.com/?enc=B%2FHRlnOr%2B%2FuFOKOSXOH7Dw%3D%3D.GxO3%2BFI93Fiogd8iSDKR61Cpnmhm1lKCydPHBcBwk%2BdP9d9LPeAtXxFxGAfEepLQm%2F%2Fa9zr%2BVP8%2FHRTb8E2n%2Fx%2FBfgAlSmeozm%2F8F0B6MoI%3D" rel="nofollow">直接下载</a> 使用。</p>
<p>以上就是对于 chrome 插件的介绍及翻页笔插件的实现介绍,才疏学浅,文中如有错误欢迎批评指正。</p>
<blockquote>参考文献: <br><a href="https://link.segmentfault.com/?enc=LNL5fD16pfoN899Nf3DsYg%3D%3D.yE8f1KXMs4XCOGuxozX8KRSAOHXjjNPfZZeXpakL7Y%2BnyW%2BLeEr9IWc%2Bht3OJ5Wn" rel="nofollow">https://developer.chrome.com/...</a> <br><a href="https://link.segmentfault.com/?enc=1nCThI9fzAAIhhGWRWCU1w%3D%3D.2sudRjprojgjPgGlCJOaClf05mYj8H1eFUpjbCCwbPpe16BsFF7hhhiy%2BJa70ENmeN17Apngmv%2BDTXS013PAWg%3D%3D" rel="nofollow">https://developer.chrome.com/...</a>
</blockquote>
<p><strong>作者简介</strong> </p>
<p>郑庆鑫为好未来前端开发工程师</p>
<p>---------- END ----------</p>
<p><strong>招聘信息</strong> </p>
<p>好未来技术团队正在热招测试、后台、运维、大前端开发等高级工程师岗位,大家可微信搜索/扫描下方”好未来技术”公众号查看其中的“<strong>技术招聘</strong>”栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=LGtt%2BAGsSnw2VgBks%2BjNHg%3D%3D.3MGbO%2Bg7kmXfRaYa%2F1BuUXNKoiXK00DYSumOq7vq3yTXqYYCfuxhfNh2Ym75Tme4aUMjtbcBN%2F4vfyLsRtAAcFIs83PqkIJKATdvB16JBjQ%2FSpZjGYMJXXGM1YSpSuMGZRbm9KEiPjWsRpZUaO1qBvR%2BJatPlDQwAAvLvxE8q0z%2F1YMNSs9vnF9t5rQnbAXycjOoCzB2%2FbHr03qcgf7AzZtqIXB%2BsYz9mU5FaxH0yzonQfAxPULnA5%2BeavqtBgQ4j4X%2FZxhjbJNtQ9dxEDt3k9DiQ6GuA3PedumbcRJLyq3WWLLuQmYVf2Rn53nDJVxf" rel="nofollow">未来魔法校的微前端实践</a> </p>
<p><a href="https://link.segmentfault.com/?enc=JP9C1lQXgMF%2BsBQRlrDC%2BQ%3D%3D.FOPJsaV0rzPFJHvGScDpknW8SHsAuv4VSOg0KUyStcTrriIM4KNChyzyxiRILPtDPFvJkO9ysXTVkMqMGGR3UiJvOx%2FPP6ci8shnlQZpWkLZg7KgK0kEmgK5TDAVdDRyTCUIaejYLHMEuXjVKFnKWfKhQg9Tti%2FmhqAnPgc9ZHUcbB5iuwQHaXYWA5HsMCPGgu2EpqflDLvy3ifbCLHpx4S%2FEHr8wxcc2mvlcgcgnaUbZGr%2FOOv8Xf46kZYUy6zyRsG1WM0H%2BhZ2NWVwP3AoadmQZx0qEliuOxkpk0lgpurFcOAJ89XNjKJqLIUpHuA3" rel="nofollow">基于TPNN的儿童英语声学模型训练</a> </p>
<p><a href="https://link.segmentfault.com/?enc=o147Ylx7qGgnJ6uCcBSsHg%3D%3D.gl%2BErlrP83F4i6jIZkWTAXCYg3uf%2FVDWz4KaPZpWBDB6Vb2n9b%2Fmu7Say6V1U7LSoTRzO71pwvfHrflemHHX9zYP%2FIGWw3uf%2BZub6sGLRSLZZoRxP1c8oxRCYYfImRdX2ziBwH3qwqlZXYh%2By6qpKdxUUNs%2BJCrtutJzOqBB1mobWg1g6g4cMz4E%2FDGx%2BN3UHYd3VcFpOM922gNyGC1AWsO9Ao97JcF2hZVHu%2BjBIgW9j9hitXdZP89wbHGtIa4rRi9fSF0yeB%2FalObId0t%2FzTt5dPRI5A%2Bpxl8UJ4DTNQXtDMyyNisfvNhikbkHjoJ6" rel="nofollow">DIY技术资讯抓取工具的实践与研究</a> </p>
<p><a href="https://link.segmentfault.com/?enc=L8786NY%2BJhyN0ujtDguU5w%3D%3D.cRBKqpqnlw%2FRz5gTxQiQaZw9WqxdSvvhiW2Rgdnj10bUqkv4u7F8nyjxzVGVRoPZDIzsmIIOc1FsakaEop6kYHcolU%2Bd8SivEUbpSi6vVX9asVpMceuz6FOEVCQJoYS1m4S3GSqbHUFy5K%2BkQmNfpW6Wtl71S0ATql97B4XeiZLjvrkI88kMUA72z5x2CoLOHiSeFVYIYYjT%2FSgraqkghlfs8DbBA7HAeEqxlqGLVgKLBYlSR%2FC4XgT%2BvNI1QdmUuclBU%2BbdBPLRkxZV09dOVIZE7%2Ba4Qr%2FEywO2%2BK%2FpxUYfxSPy1YC6RaVvEvEOji5z" rel="nofollow">产研人的疫情战事,没有一点儿的喘息</a><br><img src="/img/remote/1460000022329926" alt="" title=""></p>
DIY技术资讯抓取工具的实践与研究
https://segmentfault.com/a/1190000022256189
2020-04-03T15:58:31+08:00
2020-04-03T15:58:31+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<p><strong>前言</strong></p>
<p>相信每一个技术人员都有周期性获取技术资讯的诉求,而获取的方式也多种多样。例如,用资讯类APP,进行RSS订阅,参加行业大会,深入技术社区,订阅期刊杂志、公众号,等等,都是可选的方式。这些方式看到信息的成本都很低,有“开箱即得”的感觉。但缺点也很明显,有点像“大班课”,可以满足一类人的需求,但难较好地满足每个参与者的个性化诉求。通过这些方式,要想真正拿到自己所需要的信息的成本并不低(虽然智能推荐在往满足个性化诉求方面迭代,但离期待仍有较大的差距)。对于个性化诉求,最简单的方式就是你感兴趣哪方面的内容就去逐一主动检索或者浏览,但这种方式的成本显然太高。</p>
<p>核心的问题是,上面的两大类路径,都不是很懂你(了解你的意图和诉求)。而你需要一个既懂你,成本又不是太高的方式。</p>
<p><strong>一、对于技术资讯获取DIY的框架性思考</strong></p>
<p>相信在当前相当一段时期内,最适合的个性化资讯获取方式仍然是工具+人工相组合的方式。相比纯工具的算法推荐,一些付费资讯渠道已经在(智能)工具的基础上,对信息进行了人工的筛选、加工处理,质量会更好。如果你是程序员,自己编写一些小爬虫,在其中注入自己的喜好与智慧,不失为一种懂你且成本不高的方式。而且通过这种方式,你将获得很好的自我掌控感。本文中,笔者就着重介绍这种方式。<strong>值得提醒的是,本文所涉内容,仅为学习讨论技术,切勿用作非法用途。</strong></p>
<p>具体来说,分为四部分(如图1.1所示):</p>
<p> <img src="/img/remote/1460000022257131" alt="" title=""> </p>
<p>图1.1</p>
<p><strong>第一,自己控制消息来源</strong></p>
<p>你可以根据自己的经验积累,在合法合规的前提下来选取消息来源。这个选择的维度可以很多样,包括质量可靠性、信息的前瞻性、兴趣匹配度、研究方向匹配度、信息生产频率、信息的新颖度,等等。</p>
<p><strong>第二,自己编写采集和筛选算法</strong></p>
<p>选定了一些采集渠道,你就可以自己编写采集和筛选算法了。采集周期、筛选规则、所需内容项,等等,都可以自己控制。如果你对数据处理、人工智能等很了解,相信还有更多的发挥空间。</p>
<p><strong>第三,自己控制阅读和交互体验</strong></p>
<p>由于阅读是一个长期的过程,对于优质的体验其实有着很强的需求。难受的阅读体验是非常不利于信息的快速获取的,甚至会打消获取信息的兴趣。比如,下面这两张图,图1.2左边是某头条的资讯界面,右边是微信读书的阅读界面。</p>
<p> <img src="/img/remote/1460000022257130" alt="" title=""> </p>
<p>图1.2</p>
<p>相形之下,作为阅读者,我个人更喜欢微信阅读的简洁,而不太喜欢某头条那些次要元素的干扰。</p>
<p><strong>第四,自己控制迭代优化</strong></p>
<p>自己既是消费者也一定程度是信息流通控制者的好处就是:自己可以站在结果环节对信息获取全流程进行自主评价,回溯作用到前面的环节,从而形成正向作用闭环。</p>
<p><strong>这么做有什么收益呢?</strong></p>
<p><strong>首先,是获得有价值的信息。</strong></p>
<p>这一点无需多言。</p>
<p><strong>其次,有助于信息获取能力的提升。</strong></p>
<p>就拿技术人员来说,这么做可以更高效地、持续地获取满足个性化诉求的高价值信息,在对外部技术世界持续保持关注中获得持续性地成长与提升。</p>
<p>1)关于信息来源:你将自己总结出一份最有价值的信息的来源渠道列表,提高信息的获取效率,能以较快的速度接触到相对可靠的信息。</p>
<p>2)关于信息处理:你将沉淀出自己的一份或简单或复杂的信息采集和筛选算法,提升信息的鉴别能力,增强信息处理的能力。</p>
<p>3)关于信息体验:你将获得适合你自己的信息获取、阅读、交互体验,增强阅读兴趣和减少疲劳。</p>
<p><strong>第三,有助于进行技术探索,提升技术应用能力。</strong></p>
<p>在这个过程中,实际上也是自己在运用技术解决实际问题的探索过程,可以作为技术甚至产品建构探索的实验田。比如说,Flutter这一技术有很多公司在进行尝试和应用,但是你所做的项目暂时还是用的Electron做的,目前并没有迁移到Flutter的打算。那么如果你对Flutter感兴趣的话,完全可以把采集到的技术资讯尝试用Flutter做成一个APP,先试水一下怎么用(只是举个“栗子”,如果你恰好真感兴趣的话,后面有彩蛋一枚,继续往下看准能找到?)。这样就相当于是先期业余做了一些储备和实践。</p>
<p><strong>二、对于技术资讯获取DIY的实践探索</strong></p>
<p>上面啰嗦了这么多,还是讲点实在的吧。咱们来真实地爬取点技术资讯。要抓取的内容存在形式是多种多样的,有的被内容服务端直接渲染到了HTML页面上,有的则是在页面中通过JavaScript请求数据,然后再渲染出来的。</p>
<p>首先来看第一种。</p>
<p><strong>1、HTML页面中内容的抓取</strong></p>
<p><strong>第一步,信息来源的选择。</strong></p>
<p>要不我们就比较有代表性的互联网公司BAT里随便找一家吧,看看他们都有些什么高价值的技术资讯。不如就选那个比较高调(非常乐于向业界分享自己技术)的阿里巴巴,因为高调的可能比较好找。他们有个云栖社区,里面有个栏目叫阿里技术(<a href="https://link.segmentfault.com/?enc=EmWv%2FeqUH0cUEAfr5Y%2BRPQ%3D%3D.ul0B61aHix6Wj%2FWFmPLZS88Stdy2bCvUZfEiCqbZ3rc3HyFj2e5TZApwjOZ3GcQC" rel="nofollow">https://yq.aliyun.com/articles/721143</a>),这是一个一直在有规律更新,而且文章质量不错的栏目,界面如下所示。</p>
<p> <img src="/img/remote/1460000022257132" alt="" title=""></p>
<p>图2.1</p>
<p><strong>第二步,信息的采集和筛选。</strong></p>
<p>假设我们准备爬取最近一周阿里技术这个栏目下都有些什么新的文章发布。我们主要获取其标题、文章链接地址、发布时间和文章简介,希望只抓取最近7天内发布的文章。即期望爬取出来的结果如图2.2所示。</p>
<p> <img src="/img/remote/1460000022257133" alt="" title=""> 图2.2</p>
<p>目标清楚了,下一步就是怎么实现,笔者选择使用Node.js。这里需要介绍用到的两个工具:request-promise(<a href="https://link.segmentfault.com/?enc=r16RlA7JRNdxBUKld9pzhw%3D%3D.r4q10vlkB1rWwfwbGyChpJi6ab44hdMUHNglKphV2mj6vdNHYTV5mv1lz2fRDTmD" rel="nofollow">https://www.npmjs.com/package/request-promise</a>)和cheerio(<a href="https://link.segmentfault.com/?enc=7oqTRamG8cS%2BobcMT4eu1w%3D%3D.hPQgq6cXdjFBHE1gTyX0o8rgkYblUte%2FiifPn2Wmbn6jDsTK9Sy9PippycgyKraB" rel="nofollow">https://www.npmjs.com/package/cheerio</a>)。所以首先你需要用 yarn init 命令创建一个项目,再用 yarn add request request-promise cheerio 命令安装上这几个依赖模块。 </p>
<p>关于request-promise,官方介绍是:</p>
<p><em>The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.</em></p>
<p>通过request-promise,可以很轻易地抓到页面的HTML,如下所示:</p>
<p>const rp = require('request-promise');<br>rp(' // 略去了地址 <br> .then(function (htmlString) {<br> // Process html... <br> })<br> .catch(function (err) {<br> // Crawling failed... <br> });</p>
<p>抓到HTML后,我们还是希望对其进行处理,把其中的我们所需要的标题、文章链接地址和文章简介等信息提取出来。这时需要用到另一个工具——cheerio。用它与request-promise结合,可以让对于抓取到的HTML的处理基本上像用jQuery那样进行。因为cheerio实现了jQuery的核心子集。两者结合后的用法如下:</p>
<p>`const rp = require('request-promise');<br>const cheerio = require('cheerio');<br>const targetURL = ' // 略去了地址 <br>const options = {<br> uri: targetURL,<br> transform: (body) => {<br> return cheerio.load(body);<br> }<br>};</p>
<p>function getArticles() {<br> rp(options)<br> .then(($) => {<br> // Process html like you would with jQuery... <br> console.log($('title').text());<br> })<br> .catch((err) => {<br> // Crawling failed or Cheerio choked...<br> });<br>}</p>
<p>// 入口<br>getArticles();<br>`</p>
<p>上面代码中,</p>
<p>console.log($('title').text())</p>
<p>会log出来页面title标签内部的文字,就像使用jQuery操作页面DOM一样。</p>
<p>接着我们就可以用Chrome打开阿里技术(<a href="https://link.segmentfault.com/?enc=zJ%2B2gmcmeZ7K7IV5XjBb1w%3D%3D.6aTD3IXr95Gyr5R7xrtge5qFs3uZZsYIlteyS7kLTGoD5%2Fvs%2F6NBULMorrQKvscN" rel="nofollow">https://yq.aliyun.com/articles/721143</a>)页面,借助Chrome DevTools轻而易举找到文章的标题所对应的HTML元素(如图2.3所示)。进而通过将上述代码中的</p>
<p>console.log($('title').text())</p>
<p>这一行替换为:</p>
<p>console.log console.log($('.yq-new-item h3 a').eq(1).text())($('.yq-new-item h3 a').eq(1).text())</p>
<p>从而log出来其中一篇技术资讯文章的标题。</p>
<p> <img src="/img/remote/1460000022257134" alt="" title=""></p>
<p>图2.3</p>
<p>举一反三,用同样的方法可以获取到文章链接地址和文章简介。但是,我们还想获取到每篇文章的发布时间,但是当前页面中并没有,怎么办呢?点进去每篇文章的链接,我们发现文章内部是有这个信息的(如图2.4)。于是,实现思路就有了。每抓取到一篇文章的链接之后,再针对抓到的链接地址再进行一次抓取,把该篇文章中的发布时间也抓取出来。</p>
<p> <img src="/img/remote/1460000022257137" alt="" title="">图2.4</p>
<p>另外,因为Promise在代码中用多了之后,看起有点丑陋,所以我们将之改成用async和await的方式实现。并且把抓取到的信息写入到一个JSON文件(result.json)中。最终实现的演示代码如下:</p>
<p>/**<br> * 爬取技术资讯学习举例1<br> */<br>const fs = require('fs');<br>const rp = require('request-promise');<br>const cheerio = require('cheerio');<br>const targetURL = '<a href="https://link.segmentfault.com/?enc=HydprysJ5HD%2FqE855XpvvQ%3D%3D.Tfvkq2YKhhlRXak7B8TfJHe5gYy7zOGo3Y4%2BqkLJoyY%3D" rel="nofollow">https://xxxxxxxxxxxxxx</a>'; // 略去了地址<br>const maxDeltaDay = 7;</p>
<p>/**<br> * 抓取目标网页中的技术资讯<br> * @param {string} url - 抓取的目标网页的网址 <br> * @param {number} maxDeltaDay - 抓取距离当前时间多少天以内的资讯 <br> */<br> async function getArticles(url, maxDeltaDay) { <br> const options = generateOptions(url);<br> const $ = await rp(options); <br> const elements = $('.yq-new-item h3 a');<br> <br> // 拿到包含文章标题、链接等的标签 <br> const result = []; <br> const promises = [];<br> elements.map((index, el) => { <br> const $el = $(el); <br> const linkObj = {}; <br> <br> // 获取标题和链接 <br> linkObj.title = $el.text(); <br> const link = $el.attr('href'); <br> linkObj.link = `<a href="https://link.segmentfault.com/?enc=6X2oTsF49DIQjlZV%2BBIltg%3D%3D.Uhl%2BS3d6mCY4DjBQrv%2B%2BQHtSHv862gIJ2kNXQKWNPvA%3D" rel="nofollow">https://yq.aliyun.com</a>${link}`; <br> <br> // 处理文章简介<br> let brief = $el.parent().parent().find('.new-desc-two').text(); <br> brief = brief.replace(/\s*/g, ''); <br> linkObj.brief = brief; <br> promises.push( <br> getDeltaDay(linkObj.link).then((deltaDay) => { <br> if (deltaDay < maxDeltaDay) { <br> linkObj.deltaDay = deltaDay; <br> result.push(linkObj); <br> } <br> })<br> );<br> }); <br> <br> Promise.all(promises).then(() => { <br> if (result.length) { <br> console.log(result); <br> result.sort((a, b) => { <br> return a.deltaDay - b.deltaDay; <br> }) <br> <br> fs.writeFileSync('./result.json', JSON.stringify(result)); <br> } <br> });<br>}</p>
<p>/** <br> * 生成用于发起request-promise抓取用的options参数 <br> * @param {string} url - 抓取的目标地址 <br> */<br>function generateOptions(url) { <br> return { <br> uri: url, <br> transform: (body) => { <br> return cheerio.load(body); <br> } <br> };<br>}</p>
<p>/** <br> * 抓取文章的发布时间 <br> * @param {string} url - 文章的地址 <br> */<br>async function getDeltaDay(url) { <br> const options = generateOptions(url); <br> const $ = await rp(options); <br> const $time = $('.yq-blog-detail .b-time'); <br> const dateTime = $time.text(); <br> let deltaDay = (new Date() - new Date(dateTime)) / (24 * 60 * 60 * 1000); <br> deltaDay = deltaDay.toFixed(1); <br> return deltaDay;<br>}</p>
<p>// 入口<br>getArticles(targetURL, maxDeltaDay);</p>
<p>其中,getDeltaDay函数就是用来处理发布时间抓取的。我们最终的目的不是抓取该文章的发布时间,而是看该发布时间距离当前时间之间的差值是不是在7天之内。当然,如果想进一步筛选的话,你还可以抓取到阅读量、点赞量、收藏量等来进行判断。</p>
<p><strong>2、数据接口中内容的抓取</strong></p>
<p>上面这个是对于静态HTML页面上数据的抓取。下面再来看第二种,对于接口中数据的抓取。这里以对知名技术社区掘金的数据抓取为例。</p>
<p><img src="/img/remote/1460000022257135" alt="" title=""> 图2.5</p>
<p>如图2.5所示,掘金的资讯分了推荐、后端、前端、Android、iOS、人工智能、开发工具、代码人生、阅读等多个类目。通过Chrome DevTools查看网络请求我们发现,页面中的文章列表数据是通过<a href="https://link.segmentfault.com/?enc=r4tM%2Bxk%2FyoWEVL4ENpPdnw%3D%3D.638WC%2F7a%2B265Sn%2FbDMtY34j%2Fr8aayxTXf0rY%2B%2BguCO4TyihKy3zbhlSAtEzkMQNoidOiODCqzqp7%2BweGT%2FGEz2wNNIostPozwjcBRXhn1eQDA9F%2FV19YvK%2FME4ThG4OXFnRSGq2RQeorNgRM1sU1284NmwussAuW3OuE5J%2FXiNI%3D" rel="nofollow">https://web-api.juejin.im/que...</a>。且每个类目下的文章列表数据都是来自这同一个接口,只是请求的时候,Request Payload中的variables下的category(类目ID)字段不一样,如图2.6、图2.7所示。</p>
<p> <img src="/img/remote/1460000022257139" alt="" title=""></p>
<p>图2.6</p>
<p> <img src="/img/remote/1460000022257138" alt="" title=""> 图2.7</p>
<p>所以,整体思路就是,建立一个类目名称和类目ID的map,使用不同的类目ID逐一去调用上述接口。具体的抓取工具仍然采用上面用过的request-promise。由于事先同样并不复杂,所以不做过多解释,直接贴上代码:</p>
<p>/**<br> * 爬取技术资讯学习举例2<br> */<br>const rp = require('request-promise');<br>const fs = require('fs');</p>
<p>// 类目对应的ID<br>const categoryIDMap= { <br> '推荐': '',<br> '后端': '5562b419e4b00c57d9b94ae2', <br> '前端': '5562b415e4b00c57d9b94ac8', <br> 'Android': '5562b410e4b00c57d9b94a92', <br> 'iOS': '5562b405e4b00c57d9b94a41', <br> '人工智能': '57be7c18128fe1005fa902de', <br> '开发工具': '5562b422e4b00c57d9b94b53', <br> '代码人生': '5c9c7cca1b117f3c60fee548', <br> '阅读': '5562b428e4b00c57d9b94b9d'<br>};</p>
<p>/**<br> * 生成request-promise用到的options参数 <br> * @param {string} categoryID - 类目ID <br> */<br>function generateOptions(categoryID) { <br> return { <br> method: 'POST', <br> uri: ' // 略去了地址 <br> body: { <br> 'operationName': '', <br> 'query': '', <br> 'variables': { <br> 'tags': [], <br> 'category': categoryID, <br> 'first': 20, <br> 'after': '', <br> 'order': 'POPULAR' <br> }, <br> 'extensions': { <br> 'query': { <br> 'id': '653b587c5c7c8a00ddf67fc66f989d42' <br> } <br> } <br> }, <br> json: true, <br> headers: { <br> 'X-Agent': 'Juejin/Web' <br> }, <br> }<br>};</p>
<p>/**<br> * 获取某一类目下的资讯数据 <br> * @param {string} categoryID - 类目ID <br> */<br>async function getArtInOneCategory(categoryID, categoryName) { <br> const options = generateOptions(categoryID); <br> const res = await rp(options); <br> const data = res.data.articleFeed.items.edges; <br> let currentCategoryResult = []; <br> data.map((item) => { <br> const linkObj = {}; <br> const { <br> title, <br> originalUrl, <br> updatedAt, <br> likeCount<br> } = item.node; <br> <br> linkObj.title = title; <br> linkObj.link = originalUrl; <br> linkObj.likeCount = likeCount; <br> linkObj.category = categoryName; <br> <br> let deltaDay = (new Date() - new Date(updatedAt)) / (24 * 60 * 60 * 1000); <br> deltaDay = deltaDay.toFixed(1); <br> <br> if (deltaDay < 7) { <br> linkObj.deltaDay = deltaDay; <br> currentCategoryResult.push(linkObj); <br> } <br> }); <br> return currentCategoryResult;<br>}</p>
<p>/**<br> * 获取所有类目下的资讯数据 <br> */<br>function getAllArticles() { <br> const promises = []; <br> let result = []; <br> Object.keys(categoryIDMap).map((key) => { <br> const categoryID = categoryIDMap[key]; <br> promises.push(getArtInOneCategory(categoryID, key).then((res) => { <br> result = result.concat(res); <br> })); <br> }); <br> <br> Promise.all(promises).then(() => { <br> fs.writeFileSync('./result2.json', JSON.stringify(result)); <br> });<br>}<br> <br>// 入口<br>getAllArticles();</p>
<p>抓取到的结果如图2.8所示,主要抓取了标题、链接、点赞数、类目以及发布距离当前的时间差(天为单位):</p>
<p> <img src="/img/remote/1460000022257136" alt="" title=""> </p>
<p>图2.8 </p>
<p><strong>3、微信公众号内容的抓取</strong></p>
<p>除了上述两类内容的抓取外,还有一类资讯的抓取可能也是比较常遇到的,就是对于微信公众号内容的抓取。比如,以对于“xx早读课”这一公众号的抓取为例。微信公众号的内容如果直接从微信平台抓,需要登录,估计很容易被封号。因此,可以尝试另一种方法——通过搜狗搜索所提供的对于微信公众号的搜索结果进行抓取。</p>
<p>首先,通过<a href="https://link.segmentfault.com/?enc=BNf%2FBDCNrOT12%2B1AMSRtGA%3D%3D.yaqp7BAiP8D%2FF7hTdPORwf7xwbNuh6cG6ft5Y9mq4sbAfuypIbMEv9OorrZGxDCfoD3GZk%2Bkp66IDwozmow6eiQMpXse7diHe1VTANT7niLsSVjS0wcn2%2B0XPstN2ZQdfhUT5cBsSH5np63Clo5Lbm2%2FwcbA8dSEjy%2B309QjmQTxydFcZh%2FCaltwwmXOqHNaA5b%2BMBCcqV5AgB7sUovuuoJFMvtdmg3an9LHQ19kd5f5mda%2Bz9gPCGwTv1vP90A7Mw2V5ce4Agrt9DFREvN%2FwWxp4R9hawItAQGRMXFIs7A%3D" rel="nofollow">https://weixin.sogou.com/weixin?type=1&s_from=input&query=%E5%89%8D%E7%AB%AF%E6%97%A9%E8%AF%BB%E8%AF%BE&ie=utf8&_sug_=y&_sug_type_=&w=01019900&sut=6202&sst0=1571574212479&lkt=0%2C0%2C0</a>检索得到该公众号的英文ID。如图2.9所示。</p>
<p> <img src="/img/remote/1460000022257142" alt="" title=""> 图2.9</p>
<p>接着用该公众号英文ID搜索该公众号的最新文章,并通过点击“搜索工具”弹出的筛选面板中选择“一周内”,过滤出最近一周的文章(如图2.10所示)。之所以要用英文ID是为了让搜出来的结果只来自于该公众号,信息更为纯粹。</p>
<p> <img src="/img/remote/1460000022257140" alt="" title=""> 图2.10</p>
<p>不过,很遗憾,这些数据都是服务器直接渲染在了HTML页面中,而不是从接口中返回来的。而且,在呈现这些信息之前,还得经过图2.10所示的几步交互操作。所以不能像上面两种方法那样抓取数据。具体实现上可以采用可以puppeteer。puppeteer 是一个Chrome官方出品的headless Chrome node库。它提供了一系列的API, 可以在无UI的情况下调用Chrome的功能, 适用于爬虫、自动化处理等各种场景(如自动化测试),详细的使用可以参考官方文档(<a href="https://link.segmentfault.com/?enc=WWT%2FBoa4MGR%2BUUw80bwbSQ%3D%3D.5XxGhLW64cuWtcrPa43Z7MJBXrJrcEge%2Bl2Mmr8pVztrBHlcP1lvE3VYZso4yA2N" rel="nofollow">https://github.com/GoogleChrome/puppeteer</a>)。篇幅所限,这里就不展开介绍具体实现了。值得注意的是,搜狗搜索做了很多反爬虫的工作,所以需要注意:</p>
<p>1)在puppteer的lunch的时候,需要加上headless: false选项,避免要你输入验证码。如下所示:</p>
<p>const browser = await puppeteer.launch({ <br> headless: false<br>});</p>
<p>2)抓取次数宜尽量少,否则当你频繁抓取时,对方就会要求你输入验证码,这时候抓取工作就无法继续了。</p>
<p>即便你注意了这两点,你仍然可能遇到被识别为爬虫的情况。所以,权当是对puppeteer的学习尝试吧,毕竟这个工具功能还挺强大的,在前端自动化测试等领域,大有可为。</p>
<p>三、延申性的思考</p>
<p>上面对于信息的采集做了一些具体的介绍。对于信息可以做进一步的加工处理,以便更好地自己进行学习和研究,这里提供一点思路。</p>
<p> <img src="/img/remote/1460000022257141" alt="" title=""> </p>
<p>图3.1</p>
<p>如图3.1所示,通过后台服务从消息来源池里采集到数据之后,可以把数据建立一个库存储起来,提供一些数据服务接口供前端业务使用。你可以对数据进行处理、加工,可视化出来,比如直接以前端Web页面的形式呈现,也可以做一个原生的APP。甚至加上一些反馈渠道,对信息进行评价,从而从评价数据反推消息来源渠道的质量。</p>
<p>至于根据喜好来控制阅读和交互体验方面,一般来说,有一些共同的准则。比如,简洁的整体风格,突出内容本身的沉浸式、无打扰感受;合适的字号、行间距;优美的字体;可调、护眼的背景颜色;操作的流畅性;提供互动渠道,让阅读过程中有人共同参与而不孤独。这方面感兴趣的话可以参考下这篇文章对于微信阅读的分析(<a href="https://link.segmentfault.com/?enc=w0Zw3qp7555iUcq%2B65KdsQ%3D%3D.el4q%2BRog%2BlNg7Wr1IIaVP7jrjysNkHsecro9NmVnxJGrP1zDHDOk%2BLB5JXmGhrI5" rel="nofollow">http://www.woshipm.com/evaluating/977491.html</a>),这里不过多赘述。</p>
<p>总结</p>
<p>本文首先分析一些常见资讯获取方式的优缺点,分享了进行技术资讯获取DIY的框架性思考,阐明了其价值。然后借助三个具体的抓取案例剖析了抓取思路,并做了部分演示性的代码举例。最后就该主题进行了延申行的思考,基于此可以DIY出来一款简单的产品,甚至一个系统。</p>
<p>末了,关于Flutter的彩蛋找到了吗?(在图2.2中第二条信息哦)?</p>
基于TPNN的儿童英语声学模型训练
https://segmentfault.com/a/1190000022071571
2020-03-19T15:10:50+08:00
2020-03-19T15:10:50+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
1
<p><img src="/img/bVbELSC" alt="1+.png" title="1+.png"></p>
<p><strong>前言</strong></p>
<p>TPNN作为学而思网校自主研发的深度学习平台,专门针对声学模型训练进行了架构优化,可以轻松帮助研发人员完成语音特征和解码器的无缝对接,同时在此框架下,我们也实现了主流的声学模型架构和高效的多卡训练技术,在TPNN的框架下,我们进行了大规模数据下儿童声学模型的技术研发。</p>
<p>通过大量实验,包括模型结构,特征维度,建模单元等,结合n-gram语言模型,融入了上万小时的儿童英语数据,最终实现了最适合中国儿童的英语识别的声学模型架构,我们的儿童声学模型可以达到92%以上的识别精度,拥有领先业界的性能。</p>
<p>同时考虑到业务的需要,我们也实现了儿童声学模型的离线识别方案,利用8bit量化,neon优化,混合精度运算等技术,我们可以在损失少量的性能的情况下,在移动端达到接近服务器的计算速度。</p>
<p>本文将从TPNN的“多卡训练技术” “声学模型训练” “移动端的模型优化”这几个方面为大家介绍学而思网校的儿童声学模型训练技术。</p>
<p><strong>一、TPNN的多卡加速技术</strong></p>
<p>基于深度学习的的声学模型在语音识别领域取得了巨大的成功,但这些模型的训练都必须建立在海量的数据训练上,面对海量的训练数据,模型的训练时间大大增加,识别会严重拖慢研究和开发进度。 因此高效的多卡训练方案对于一个深度学习框架是一个非常重要的环节。</p>
<p>TPNN拥有在NVidia的NCCL通信框架基础上,利用BMUF技术,搭建了一套高效率的多卡加速框架。</p>
<p><strong>二、数据同步并行框架</strong></p>
<p>目前深度学习领域,已经有若干并行训练技术的成功尝试,基本解决方案主要是基于<strong>数据并行</strong>和<strong>基于模型并行</strong>两种。</p>
<p>其中模型并行针对GPU显存有限的问题,将大模型分层拆分到各个gpu进行流水线运算,而数据并行则是拆分训练数据进行子路运算,根据数据交换的策略不同,数据并行方法又可以分为<strong>同步并行</strong>和<strong>异步并行</strong>。</p>
<p>异步并行算法最著名的实现当属google公司开发的DistBelief框架,根据google公布的结果显示,随着并行机器的增多,AGSD的性能会存在不稳定的现象,该方法训练的模型与minibatch-sgd孰优孰劣尚无定论。</p>
<p><strong>TPNN采用的基于数据并行的同步训练方案</strong>,利用改进的BMUF算法进行全局模型迭代更新,从大量实验数据来看,多路性能相对单路基本持平,并且可以实现接近线性的加速比。</p>
<p><img src="/img/bVbELSL" alt="2+.png" title="2+.png"></p>
<p><strong>Figure1 数据同步并行的基本架构</strong></p>
<p>数据同比并行是指对训练数据做切分,同时采用多个模型实例,对多个分片的数据并行训练,并在定期时间内将多个模型的梯度同步到parameter server进行求和平均,在分发到各路子模型。</p>
<p>这里同步的时机可以采用一个batch内的拆分同步,这样可以基本可以模拟单路的训练过程,但这种方法有两个缺点,就是当batch比较小时,多路gpu之间的同步开销将会很大,所以这种方法通常需要加大batch重新超参数进行训练,为此TPNN的多卡同步才用的是N个batch同步策略,具体训练过程中我们可以调整N达到最大的性能和速度之间的平衡。</p>
<p>图二显示了我们采用这种训练方式的数据组织方法,我们将数据拆分成M*N个块,其中N为并行gpu的数量,M为数据同步的block频率,每个block包含n个batch的数据。</p>
<p><img src="/img/bVbELSN" alt="3+.jfif" title="3+.jfif"></p>
<p><strong> Figure2 数据训练集的拆分结构</strong></p>
<p>TPNN的底层数据通信采用基于NCCL的方案,NCCL核心提供了gpu层面的all-reduce算法,这项技术也帮助我们实现了gpu在训练过程中进行频繁数据交换的可行性。</p>
<p>同时NCCL本身也提供了跨机通信能力,多机间通过Sockets (Ethernet)或者InfiniBand with GPU Direct RDMA通信。这对于我们可以较为轻松的将单机多卡技术进一步扩展到多机多卡技术。</p>
<p><img src="/img/bVbELSO" alt="4+.jfif" title="4+.jfif"></p>
<p><strong>Figure3 NCCL all-reduce通信技术</strong></p>
<p><strong>三、BM</strong><strong>UF算法</strong></p>
<p><strong>(Blockwise Model-Update Filtering)</strong></p>
<p>将多路计算的梯度同步到再进行分发的过程通常成为MA(模型平均)算法,这种算法实现简单,但是随着并行机器的增长,模型的性能会差过单机模型。</p>
<p>我们发现,每个block合并后的梯度贡献为单路SGD算法的1/N(公式1,其中G为全局梯度,g(t)为每一路梯度)因此MA训练中需要将将学习率调大N倍,但这并不总是奏效的。</p>
<p><img src="/img/bVbELSS" alt="5+.png" title="5+.png"></p>
<p>针对MA的缺点,论文[2]提出了一种基于块冲量的全局更新算法(Blockwise Model-Update Filtering,BMUF),利用每一层block增加的冲量梯度更新,可以维持梯度更新量不会发生损失,详细的证明有兴趣可以参见一下论文。</p>
<p><img src="/img/bVbELST" alt="6+.jfif" title="6+.jfif"></p>
<p>其中ƞ称为block一级别的冲量,δ称为块学习率。</p>
<p>TPNN正是结合底层NCCL技术和BMUF算法,打造了高效、高性能的训练算法框架,我们在此基础上完成了上万小时的大规模儿童数据训练,利用四张卡并行,我们可以达到3.6倍的加速比,在三天左右即可完成上万小时以上的LSTM模型声学迭代。</p>
<p><strong>四、基于LSTM-CTC技术的儿童声学模型训练</strong></p>
<p><strong>1、LSTM模型结构简介</strong></p>
<p> 深度学习模型在数据特征学习领域的地位越来越无可替代,目前比较典型的神经网络主要有:DNN、CNN、RNN、LSTM、TDNN、FSMN等,其中DNN和CNN网络结构属于静态分类网络,尤其CNN(convolution neural networks,卷积神经网络),能够非常好地学习到具有空间关系的数据特征,如图像的识别与分类。</p>
<p><img src="/img/bVbELSX" alt="7+.jfif" title="7+.jfif"></p>
<p><strong>Figure4 CNN网络结构</strong></p>
<p>而时序数据是一种时序关系非常重要的变长输入序列,因此动态分类网络将更适合于时序数据的学习,RNN(recurrent neural networks,循环神经网络)、LSTM(Long Short-Term Memory)正是处理这种时序关系非常有效的动态网络结构。</p>
<p><img src="/img/bVbELS0" alt="8+.jfif" title="8+.jfif"></p>
<p><strong>Figure5 RNN网络时间展开图</strong></p>
<p>RNN的时间展开图如上图所示,在进行误差反向传播(error backpropagation)时,当我们已知损失函数对时刻隐状态向量的偏导数时,根据链式法则,我们计算损失函数对时刻隐状态向量的偏导数。</p>
<p><img src="/img/bVbELS9" alt="9+.png" title="9+.png"></p>
<p>当t很大时,该偏导数取决于矩阵Whh的最大的奇异值是大于1还是小于1,要么结果太大,要么结果太小,对应的训练结果则是梯度的爆炸或消失。</p>
<p><img src="/img/bVbELTa" alt="10+.png" title="10+.png"></p>
<p><img src="/img/bVbELTd" alt="11+.png" title="11+.png"></p>
<p><strong>Figure6 rnn梯度爆炸</strong></p>
<p>基于RNN的不足,LSTM网络通过引入3个门电路对长短期的时序数据进行选择性的遗忘、记忆,其中黄色方框表示神经网络层,粉色圆圈表示点数据操作运算,其数学形式:</p>
<p><img src="/img/bVbELTk" alt="12+.png" title="12+.png"></p>
<p>对应的网络结构如下图:</p>
<p><img src="/img/bVbELTp" alt="13+.png" title="13+.png"></p>
<p><img src="/img/bVbELTq" alt="14+.jfif" title="14+.jfif"></p>
<p><strong>Figure7 LSTM模型结构</strong></p>
<p>LSTM目前有很多变体,在我们的TPNN训练平台中,我们采用了当前最流行的一种变体,即加入了peephole connection,利用上一时刻的细胞记忆状态对忘记门和记忆门进行修正,利用当前的记忆状态对输出门进行修正,其peephole的修正公式及修正结构如下图所示:</p>
<p><img src="/img/bVbELYz" alt="15+.png" title="15+.png"></p>
<p><strong>Figure8 LSTM with peephole</strong></p>
<p><strong>2、CTC训练准则</strong></p>
<p>训练准则在模型训练中起着至关重要的作用,直接决定模型训练最终效果的好坏,我们采用CE(cross-entropy 交叉熵)、CTC(connectionist temporal classification)[4]两种训练准则进行试验。</p>
<p>CE训练准则的本质上是模型将输入序列与标注序列进行一一对应,然而,语音的每一帧特征并非都有准确的Label,因此基于帧级别的CE训练准则很难使模型达到最佳的识别性能。</p>
<p>CTC是一种让网络学会自动对齐的方法,connectionist表示序列联结,temporal表示时序类别,classification表示分类,其直观理解即为连接性的时序分类准则,它更符合语音识别的真实问题。</p>
<p> 如下图所示,Waveform中的每一帧语音并不能完全准确的找到真实的Label,但是其某一段时序帧系列则很容易能确定它的对应的真实Label,即图中的speak(尖峰),其它区域则用blank代替。</p>
<p><img src="/img/bVbELT5" alt="16+.jfif" title="16+.jfif"></p>
<p><strong>Figure9 CTC blank 尖峰</strong></p>
<p>CTC准则的序列化映射数学表现形式为:</p>
<p><img src="/img/bVbELT9" alt="17+.png" title="17+.png"></p>
<p>CTC准则的训练流程与传统的神经网络训练过程类似,先构建loss function,然后根据BP算法进行训练,不同之处在于CE准则针对每一帧的数据,即每帧训练误差最小,CTC准则是基于序列(如语音的一句话),最大化的值,序列化的求解相对CE更加复杂,因为一个输出序列可能对应很多条路径,所以引入前后向算法来简化计算。这里只对前向算法进行简要介绍,后向与前向算法相似。</p>
<p>首先需要弄清两个概率符号的关系:p()和p(),p()表示给定输入x输出路径为π的概率,p()表示给定输入x输出序列为的概率,因此输出的序列为的概率可以表示为所有输出的路径映射后的序列为的概率之和:</p>
<p><img src="/img/bVbELT9" alt="18+.png" title="18+.png"></p>
<p>由于输出序列与目标序列的直接去重映射存在两个问题:无法预测有连续重复字母的单词,无法预测一整句话,单词间无法分割。因此,前向计算前,我们需要对输出序列进行预处理,插入blank操作。</p>
<p>输出路径与目标序列的序列映射关系可用下图来理解,白色圆圈表示一个label,黑色圆点表示插入的blank,纵向一列表示插入blank后的输出序列,横向表示路径总帧长度。</p>
<p>图中长度为T的输出路径映射到序列:C A T,可以由第T步为label:T的所有路径和第T步为空格的所有路径的概率之和来表示。</p>
<p><img src="/img/bVbELXQ" alt="19+.png" title="19+.png"></p>
<p><strong>Figure10 ctc loss的路径概率计算</strong></p>
<p>递推公式的前面变量为:</p>
<p><img src="/img/bVbELXS" alt="20+.png" title="20+.png"></p>
<p>反向传播与前向类似,不再介绍,由于前后向计算均涉及大量概率计算,因此在计算时需要对其做取对数处理,将乘法转化为加法,不仅可以避免underflow问题,还可简化计算。</p>
<p>CTC损失函数的计算公式:</p>
<p><img src="/img/bVbELXW" alt="21+.png" title="21+.png"></p>
<p>损失函数可以解释为:给定样本后输出正确label的概率的乘积,再取负对数。</p>
<p>在TPNN平台训练中,我们采用LSTM模型作为基础模型,基于LSTM的结构,我们进行了大量的对比试验,包括训练集对齐状态数、训练数据组合策略、网络深度和宽度、记忆单元维度、学习率调整策略、输入特征维度等。</p>
<p>最终我们采用了3层的LSTM变体结构和CTC训练准则,在不损失识别性能的前提下,压缩网络结构的宽度,选取适中的记忆单元维度。</p>
<p>在儿童真实线上数据集上,我们的模型的表现性能达到了92.48%的识别精度。</p>
<p><strong>五、移动端声学模型优化</strong></p>
<p>由于移动端资源的限制,大部分深度学习引擎都部署在云端,移动设备获取到输入数据,经过简单的加工,发送给云端,云端服务器经过深度神经网络推断运算,得到结果并反馈给移动端,完成整个过程。</p>
<p>显而易见,这种交互方式有很多弊端,比如依赖网络,流量过大,延迟太长,更重要的是,云端服务器必须有足够大的并行计算能力,如果移动端请求量太大,超过负荷,容易导致服务器宕机,特别是在儿童教育场景模式下,存在很多瞬时并发非常高的业务场景,这个时候,一旦服务器算力不够,将会造成非常不好的教学体验。</p>
<p>因此,将语音识别能力部署到移动设备上,是满足当下儿童业务场景的一项必须的工作。然而,成功将深度学习引擎部署到移动端并非易事。运行速度,内存占用,各类机型覆盖,都是必须逾越的障碍。</p>
<p>接下来我们将介绍我们是如何将儿童声学模型成功应用到手机上的,其中包括了模型压缩,定点化,解码加速,neon计算优化以及混合精度计算优化等技术。</p>
<p><strong>1、模型裁剪和压缩</strong></p>
<p>传统的LSTM拥有比较大的计算量,这种LSTM结构在堆叠几层后,训练的声学模型很难在线上环境中使用,为此google提出了LSTM with projection layer的结构[3]。通过在最后一层增加线性投影变换层,可以大幅度减少堆叠LSTM层的计算量。</p>
<p>在实际应用中,我们在google的基础上进一步实验发现,我们可以将线性层压到非常小的维度而几乎不损失性能。</p>
<p>在实际上线的过程中,我们将投影层控制到非常小的维度,而将cell的维度在调整到1000维以上,相对于不加投影层,我们在原始LSTM模型的基础上减小了3~4的计算量。</p>
<p><img src="/img/bVbELXX" alt="22+.jfif" title="22+.jfif"></p>
<p><strong>Figure11 LSTM with projection layer结构</strong></p>
<p><strong>2、模型的8bit量化</strong></p>
<p>由于移动平台的浮点计算能力相对较弱,我们在对模型进行了定点化压缩,将权重压缩到8bit进行存储,存储大小缩小为原来的四分之一,并同时实现了LSTM模型8bit计算推理,通过在arm平台的深度定制优化,我们的声学模型在牺牲少量性能的情况下达到服务器的计算速度。</p>
<p><strong>3、低精度矩阵的计算优化</strong></p>
<p>声学模型运算最耗时的部分就是是矩阵乘法,所以要实现8bit计算推理,首先需要考虑的就是矩阵的低精度快速乘法,arm平台上提供了基于neon的并行计算指令集,我们的优化都在并行计算指令集的基础上进行。</p>
<p>这里介绍一下我们在arm平台的一些优化思路: </p>
<p>饱和溢出的处理至关重要!我们在优化过程中采用了8bit->16bit,16bit*16bit->32bit的方案,溢出过程中的处理借助于neon提供的饱和指令集完成。参数的定点化和char转回浮点都需要使用neon指令加速,防止定点化带来太多多余的计算开销。</p>
<p>参数计算由float的4*4分块改成4*8分块,这里主要考虑两点,分块矩阵可以更大限度利用cache,将整个矩阵计算过程维持在高速cache中进行,同时neon位宽128bit,一次最多可以同时计算八个char,最终采用4x8分块。</p>
<p>我们在定点化的时候输出矩阵采用转置输出,这样两个矩阵在运算的时候可以进行连续的内存访问,提高cache的访问效率。</p>
<p><img src="/img/bVbELX1" alt="23+.jfif" title="23+.jfif"></p>
<p><strong>Figure12 低精度矩阵计算优化</strong></p>
<p>除了通用的矩阵的矩阵运算结构,我们还针对LSTM的计算特性,对特殊的矩阵运算结构,比如Matrix*vector进行了定制优化。</p>
<p><strong>4、混合精度计算</strong></p>
<p>众所周知,由于LSTM模型的相对复杂的结构,已经存在好几个非线性的门结构,要实现LSTM的全量8bit运算是很困难的,为此我们通过研究和实验了一种8bit和float32的混合精度计算结构,可以在精度保持的情况下,最大限度的利用定点优化提升计算速度。</p>
<p>实验过程中,我们将输出X和前一个时刻的隐转态的线性变换转换为定点计算,同时将计算输出转换为浮点结果,送入门结构,保持门结构的浮点计算能力。</p>
<p>最后我们看下在移动平台的测试效果,测试cpu为骁龙710,可以看到通过优化,我们的离线声学模型可以达到0.3左右的计算实时率,完全可以比肩服务器的计算速度,也成功实现了手机上的离线识别,在网校的各个教育业务场景完成了落地。</p>
<p><img src="/img/bVbELX2" alt="24+.jfif" title="24+.jfif"></p>
<p><strong>Figure14 计算实时率优化曲线</strong></p>
<p><strong>参考文献</strong></p>
<p>[1]Asynchronous stochastic optimization for sequence training of deep neural networks.</p>
<p>[2]Scalable Training of Deep Learning Machines by Incremental Block Training with Intra-block Parallel Optimization and Blockwise Model-Update Filtering.</p>
<p>[3]Long Short-Term Memory Recurrent Neural Network Architectures for Large Scale Acoustic Modeling.</p>
<p>[4]Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks.</p>
<p><strong>作者简介</strong></p>
<p>唐凯、孟凡昌老师,均为好未来学而思网校事业部智能研发部高级数据算法工程师</p>
<p>END</p>
<p><strong>招聘信息</strong> </p>
<p>好未来技术团队正在热招视觉/图像算法、后台开发、运维开发、后端开发Golang、web前端开发等高级工程师岗位,大家可点击本公众号<strong>“技术招聘”</strong>栏目了解详情,欢迎感兴趣的伙伴加入我们!</p>
<p><strong>也许你还想看</strong></p>
<p><a href="https://link.segmentfault.com/?enc=9mtQ2OufGQ1yiNfOZvmMoQ%3D%3D.lKrDbHhvtAHpcFZJyKjl%2Bd6c00soKPMLHctqh%2BrgD0N%2B1rhAYR9wji3JJdllVGY4RQCEdJEMfkYvHfD%2FTQFzR9CVVfhj5LXTED8p98VpMzrE0EWb9Y42MfbpffTRbuSi%2FCVRCmGDNXNjfQqMU4p8psbEOmGwqOOoAJTnRSdEdM%2Fz%2FeuWP6S2LHjysPpsIK0fVNGUpfOMdgp6IFnMNZ3b2xCqk4EeSu6o1vLgy0FKr4FJc1b%2FF%2BnpVv1N5v%2BisREJRyUsqGY6tehnf46bG1w8lXqi6EAfybQWuX5B9xR9yJ1nfoJWZzs2%2FeCDj5BFACnB" rel="nofollow">重磅总结|9大技术方向,30+篇好文!2019好未来技术合辑新鲜出炉!</a> </p>
<p><a href="https://link.segmentfault.com/?enc=VcY%2Ba3rQyLhesGtVq5JpdA%3D%3D.8s8A3%2FF9X4JShhp9QbxSdJbXtqVXlpW47m0XOVlw9dQrBY%2FK2kGL%2BZBl7wbmERmLg0MXcUpTs%2BpHtaqbhSeWQFB7Iq3uSqulJBvsZYZPz3r9HYAEPNHChSTPHRwGxMNF4cJcJUo5YqXGXCeDiCY612oimg80ChakUNeiFUhDzo21bNQymCqVs4gQKCYNym61FgMDpq%2BYi%2BhBnUG6Kmoz9dXrocNXMg3Vfy18BA0YAZWYf6jduuEDh%2FhrdVFkUg9ABwq2cn7IsDZ2dorI3FpjKnVGF8P8W21uPQte4yb5cIETRh7tW%2BAAJ5F3jPAxXg%2FA" rel="nofollow">前端组件化-高质高效协作利器</a></p>
<p><a href="https://link.segmentfault.com/?enc=a6vSh0EVlIPtZNlQWMG7BQ%3D%3D.q5K%2FVALCvYkrwquvH9RlIFvY8NI%2BkK53sD76CrJdSirGnUlJYXzkxFl4SFRlejhXghplCzEGIVtxrtf9VFPNdAlU3GWri8ZOdDNgiAtUMYQN0chjUVLkssuffoEIizMFBBQcsuEiuS7GHneoFT8BFOSj2w4q4ifr%2FbQ7%2F5%2Fz9d%2BB2HAo4q4%2FJAXsoWia34Atxh0p8PUo4Rmw8PuG8%2FQb5pvXnN21Fhwxy6psOxDSYyEYL6o6nWcQQ9bnowEsmNyyoZOVXcUaBRw8Y0H6XjOUNov2q2MxabCrUZCgosoukY4CSPVjbGqc%2BmTAClwBPm%2B1" rel="nofollow">纪实|我是一名程序员,我用代码为你守候</a></p>
<p><img src="/img/bVbELX4" alt="1.jfif" title="1.jfif"></p>
未来魔法校的微前端实践
https://segmentfault.com/a/1190000021995261
2020-03-12T18:07:21+08:00
2020-03-12T18:07:21+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<p><strong>一、 背景</strong></p>
<p>魔法校是tob起家,众所周知tob业务很容易做成巨石应用,近两年来魔法校飞速发展,我们的某个主要的前端项目遇到了瓶颈,那就是项目太大了。</p>
<p>为了减少耦合度加快打包速度,我们选择将一些功能提出来新建项目,然后通过iframe的方式引入到主项目中去。虽然项目体积大的问题得到了解决,但用户体验却随之下降。</p>
<p>因为每次用户切换到iframe的tab时不管优化的再好总要有一瞬间的白屏,整个系统使用起来没有连贯性,而且在iframe里切换页面浏览器的地址栏url并不会变化,给人的感觉就是两个系统。</p>
<p>业务的快速发展迫使我们去寻找一种新的方式-微前端。</p>
<p><strong>二、微前端的基本概念</strong></p>
<p><strong>1、什么是微前端</strong></p>
<p>微前端是近两年比较火的一个概念,这个术语最初来自 2016 年的 ThoughtWorks 技术雷达,它将微服务的概念扩展到了前端领域。目前的趋势是构建一个功能丰富且强大的前端应用,即单页面应用(SPA)。前端层通常由一个单独的团队开发,随着时间的推移,会变得越来越庞大而难以维护。这就是传说中的前端巨无霸Frontend Monolith。</p>
<p>微前端背后的理念是将一个网站或者 Web App 当成特性的组合体,每个特性都由一个独立的团队负责。每个团队都有擅长的特定业务领域或是它关心的任务。这里,一个团队是跨职能的,它可以端到端,从数据库到用户界面完整的开发它所负责的功能。</p>
<p>然而,这个概念并不新鲜,过去它叫针对垂直系统的前端一体化或独立系统。只不过微前端显然是一个更加友好并且不那么笨重的术语。</p>
<p><strong>2、微前端的优势</strong></p>
<p>◾复杂度可控:每一个UI业务模块由独立的前端团队开发,避免代码巨无霸,保持开发时的高速编译,保持较低的复杂度,便于维护与开发效率。</p>
<p>◾独立部署:每一个模块可单独存放,单独部署,不对其他模块有任何影响。</p>
<p>◾技术选型灵活:也是最具吸引力的,在同一项目下可以使用如今市面上所有前端技术栈,也包括未来的前端技术栈。</p>
<p>◾容错:单个模块发生错误,不影响全局。</p>
<p>◾扩展:每一个服务可以独立横向扩展以满足业务伸缩性,与资源的不必要消耗。</p>
<p><strong>3、我们何时需要前端微服务化</strong></p>
<p>◾项目技术栈过于老旧,相关技能的开发人员少,功能扩展吃力,重构成本高,维护成本高。</p>
<p>◾项目过于庞大,代码编译慢,开发体差,需要一种更高维度的解耦方案。</p>
<p>◾单一技术栈无法满足你的业务需求。</p>
<p>◾需要将其它现有项目整合到主项目中。</p>
<p><strong>4、实现微前端的几种方式</strong></p>
<p>◾iframe:iframe是最简单也是最直接的办法,iframe自带沙箱隔离,可使多个应用同时运行在一个用户界面上。</p>
<p>◾路由分发式微前端:即通过路由将不同的业务分发到不同、独立前端应用上,典型代表阿里云。</p>
<p>◾结合Web Components 技术构建:使用 Web Components 构建独立于框架的组件,随后在对应的框架中引入这些组件,适合较小的模块。</p>
<p>◾自制框架兼容应用:代表框架有 single-spa、阿里的 qiankun 在页面合适的地方引入或者创建 DOM,用户操作时,加载对应的应用(触发应用的启动),并能卸载应用。</p>
<p><strong>5.单体应用与微前端架构对比</strong></p>
<p><strong>◾单体应用</strong></p>
<p><img src="/img/bVbEr6K" alt="1.jfif" title="1.jfif"></p>
<p>◾<strong>微前端</strong></p>
<p><img src="/img/bVbEr6M" alt="2.jfif" title="2.jfif"></p>
<p><strong>三、如何实现微前端应用</strong></p>
<p><strong>1、基本概念</strong></p>
<p>实现一套微前端架构,可以把其分成四部分参考:<a href="https://link.segmentfault.com/?enc=AY07ciGQgvD6mYQHWlUzYw%3D%3D.vsrbRV0KtIEtXIBWEIokmhTapuXI5v1ITOU2F5wASerxgmYojkybkfoiLSDTg2Sa" rel="nofollow">https://alili.tech/archive/11...</a></p>
<p>◾加载器:也就是微前端架构的核心,主要用来调度子应用,决定何时展示哪个子应用, 可以把它理解成电源。</p>
<p>◾包装器:有了加载器,可以把现有的应用包装,使得加载器可以使用它们,它相当于电源适配器。</p>
<p>◾主应用:一般是包含所有子应用公共部分的项目—— 它相当于电器底座。</p>
<p>◾子应用:众多展示在主应用内容区的应用—— 它相当于你要使用的电器。</p>
<p>所以是这么个概念:电源(加载器)→电源适配器(包装器)→️电器底座(主应用)→️电器(子应用)️。</p>
<p>总的来说是这样一个流程:用户访问index.html后,浏览器运行加载器的js文件,加载器去配置文件,然后注册配置文件中配置的各个子应用后,首先加载主应用(菜单等),再通过路由判定,动态远程加载子应用。</p>
<p><strong>2.预备知识</strong></p>
<p><strong>✅SystemJs</strong></p>
<p>SystemJS提供通用的模块导入途径,支持传统模块和ES6的模块。SystemJS有两个版本,6.x版本是在浏览器中使用的,0.21版本的是在浏览器和node环境中使用的,两者的使用方式不同。</p>
<p>参考:<a href="https://link.segmentfault.com/?enc=zjfPLbGvIt0gkdNinmtQMw%3D%3D.kzbE0fwZvmpyXxdUnnPpCL6drAoOXStX1EXv0sWnPXYrG7v9JLrsVpBs0mFekZo%2B" rel="nofollow">https://github.com/systemjs/s...</a></p>
<p>在微服务中主要充当加载器的角色。</p>
<p>值得注意的是我们暂时没有选择用system.js,因为经过调研,目前需要加载的资源很少,有点杀鸡焉用宰牛刀的感觉,目前我们的做法是动态创建script去引用资源,当然正统的做法还是使用system.js。</p>
<p><strong>✅singleSpa</strong></p>
<p>single-spa是一个在前端应用程序中将多个javascript应用集合在一起的框架。主要充当包装器的角色。</p>
<p>参考:<a href="https://link.segmentfault.com/?enc=V4HZ6%2Fp2%2BLNsheWfpNBvVA%3D%3D.WiqQ5xuezC%2FsjP9zPHHByMCK5gLLTjk3wfiWQnYxufyPeYS1MhfLkP4bDmgiv5hTgBduCae0Hp3GNSw1k8ITyg%3D%3D" rel="nofollow">https://single-spa.js.org/doc...</a></p>
<p><strong>四、具体实现步骤</strong></p>
<p>首先需要两个前端项目,一个主项目,一个子项目,主项目有基本的登录、导航模块,剩下的主要业务逻辑在子项目中。</p>
<p>因为我们好未来主要的前端技术栈是vue,我们就用vue来举例说明,当然react和angular也都适用。 </p>
<p><strong>1.子项目</strong></p>
<p>◾<strong>首先我们来看下vue.config.js里的修改</strong></p>
<p><img src="/img/bVbEr6P" alt="3.png" title="3.png"></p>
<p>publicPath这个选项一般我们做项目的时候写’/'就行了,这个选项表示资源文件从哪个地址进行加载,一般都是从网站的根目录加载,但是现在要写成子项目的存储地址,可以是阿里云oss,也可以是cdn上。</p>
<p><img src="/img/bVbEr61" alt="4.png" title="4.png"></p>
<p>devServer需要新增header头用来跨域,主要是我们在本地开发的时候从主项目的端口号里加载子项目端口号的资源也会跨域。</p>
<p><img src="/img/bVbEr66" alt="5.jfif" title="5.jfif"></p>
<p>output选项要指明资源包的名称以及运行环境,这里我们选择了window,注意如果使用system.js的话这里要指明是umd模式。</p>
<p>stats-webpack-plugin这个插件原本是用来描述各个依赖之间的相互引用关系,现在我们用它生成manifest.json文件,这个manifest.json文件里有什么东西呢?我们来看一下。</p>
<p><img src="/img/bVbEr68" alt="6.jfif" title="6.jfif"></p>
<p>这里面我们主要用到entrypoints,可以看到这就是启动子项目所需要加载的入口文件,也就是说我们在主项目里请求这个manifest.json文件就知道都需要加载哪些js。</p>
<p><img src="/img/bVbEr7c" alt="7.jfif" title="7.jfif"><br>这边我们还给css加了个作用域single-spa-vue,子项目的所以css只有在这个样式前缀下才能生效,保证子项目的样式不会影响到主项目。</p>
<p><strong>◾然后是main.js的修改</strong></p>
<p><img src="/img/bVbEr7d" alt="8.jfif" title="8.jfif"></p>
<p>这边我们用了一个小插件single-spa-vue,这个插件的主要作用就是导出single-spa框架所需要的三个生命周期bootstrap、mount、unmount,对应的还有single-spa-react、single-spa-angular。 <br>可以看到我们这样的写法可以确保子项目作为一个模块在主项目里运行,也可以单独拿出来打包部署,甚至可以两者并行存在。</p>
<p><strong>◾router.js修改</strong></p>
<p><img src="/img/bVbEr7i" alt="9.jfif" title="9.jfif"></p>
<p>router要加一个baseUrl,我们的项目叫’learnSystem’,前后都要加’/'这个url前缀是我们在主项目的模块路由前缀,如果不加在主项目刷新页面的时候会匹配不到子项目。</p>
<p><strong>2.主项目</strong></p>
<p><strong>◾新增single-spa-config.js文件</strong></p>
<p><img src="/img/bVbEr7C" alt="10.jfif" title="10.jfif"></p>
<p>主要步骤就是动态创建script标签,远程加载子项目所需js文件,然后注册微服务模块’learnSystem’。</p>
<p>一开始这个文件是放在main.js中加载的,后来发现如果子项目又可以比主项目加载的还快,这样就会出问题,然后我们把它放在里App.vue的created钩子函数里,用require引入,确保主项目加载完毕之后再加载子项目。</p>
<p>上图中LEARN_URL可以根据环境配置不同的值,如开发环境可以配置localhost+子项目的端口号,生产环境可以配置线上的链接。</p>
<p><strong>◾router.js</strong></p>
<p><img src="/img/bVbEr7D" alt="11.png" title="11.png"></p>
<p>router需要加模糊匹配*,否则在子模块当前页面直接刷新会先进入到主项目的404页面</p>
<p><strong>◾App.vue</strong></p>
<p><img src="/img/bVbEr7E" alt="12.jfif" title="12.jfif"><br>一开始我们的App.vue只有一个router-view,现在我们要提供一个容器供子项目渲染,还记得single-spa-vue这个作用域吗?确保子项目的样式只会在这个div里生效</p>
<p><strong>3.主项目与子项目通信</strong></p>
<p>通常,我们建议尝试避免这种情况-将这些应用程序耦合在一起。如果您发现自己经常在应用程序之间执行此操作,则可能要考虑那些单独的应用程序实际上应该只是一个应用程序。但是有一些场景是需要用到通信的,比如子项目请求接口发现token过期,需要通知主项目退出登录并跳转到登录界面。</p>
<p>我们用了一种简单的方法,既然这两个项目同属一个窗口,那他们也共有一个window对象,在主项目里注册一个方法并挂载到window对象上,在子项目里调用此方法就能达到目的。</p>
<p><strong>4.优化打包配置</strong></p>
<p>因为主项目与子项目都用到了相同的一部分依赖,可以考虑将公用的依赖不打包进去,改为在主项目引入来提高打包效率 <br>修改vue.config.js</p>
<p><img src="/img/bVbEr7G" alt="13.png" title="13.png"></p>
<p>请注意,可共用的资源要保证主项目和子项目所用的依赖版本号一致,或者可以兼容到同一版本,假如某个依赖两个项目之间版本相差过大,那么这就不是一个可以共用的资源。</p>
<p><strong>五、结语</strong></p>
<p>此套方案只能说是一套可行性方案,其实还有很多可以优化的地方,我们可以一起来探讨。</p>
<p><strong>比如:</strong></p>
<p><strong>◾</strong>上述我们解决了子应用与主应用之间css相互之间影响,但是别忘了还有js呢,如何创造像iframe那样的沙箱,使得应用之间互不影响包括全局变量事件等处理,是一个比较重要的点。</p>
<p><strong>◾</strong>我们用vue避免不了使用vuex,那么不同的应用之间能否共享vuex的数据?因为有一些权限之类的数据可能是通用的。或者更深一步,应用能否自身来控制某个state是应用的私有变量或者是其它应用可以访问的公共变量。</p>
<p><strong>◾</strong>我们现在子项目的资源是懒加载,也就是当路由匹配到’learnSystem’时才去加载子项目的资源。其实预加载可能会更好一点,也就是应用空闲时去加载子应用资源。</p>
<p><strong>◾</strong>子应用嵌套,微前端如何嵌套微前端。</p>
<p><strong>◾</strong>主应用如何下发状态给子应用。</p>
<p>虽然它并不完美,但是并不妨碍我们去体验微前端带来的好处,我们将以上这些东西分享给大家,也请大家关注到未来魔法校近年来的成长。 </p>
<p><img src="/img/bVbEr7I" alt="14.gif" title="14.gif"></p>
纪实|我是一名程序员,我用代码为你守候
https://segmentfault.com/a/1190000021960705
2020-03-09T18:05:37+08:00
2020-03-09T18:05:37+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
0
<p><img src="/img/bVbEi8l" alt="微信图片_20200309155718.png" title="微信图片_20200309155718.png"><br>网校有这么一群伙伴,</p>
<p>神秘的是外界可能这么看他们:</p>
<p>格子衫、双肩包、代码……</p>
<p><img src="/img/bVbEi8q" alt="微信图片_202003091557181.png" title="微信图片_202003091557181.png"><br>他们的标签有很多,</p>
<p>然而这却仅仅是停留在表面的认识,</p>
<p>但是我们网校伙伴们心里都知道,</p>
<p>他们可不是一般的人,</p>
<p>是具有某种超能力的人!</p>
<p><img src="/img/remote/1460000021960709" alt="" title=""></p>
<p>在本次疫情期间,</p>
<p>我们对孩子们开启《学而思第二课堂》学习项目,</p>
<p>也对员工们启动了全员的在家办公的举措。</p>
<p>我们看到了学员线上体验着优质的学习课程,</p>
<p>体验着与老师之间无卡顿的交互。</p>
<p>同时,</p>
<p>伙伴们也在家进展着高效的云办公</p>
<p>大家感受到了:高稳定,高质量,高体验</p>
<p>的远程线上教学&办公服务!</p>
<p>...</p>
<p>在这背后,</p>
<p>有这样一群平凡而伟大的网校程序员小哥哥小姐姐</p>
<p>在背后为孩子,为我们,</p>
<p>用爱与代码撑起了一片天!</p>
<p>对于他们来说:</p>
<p>生活就是二进制,一个个0101组成的。</p>
<p>每一次抉择,选了就是1,不选就是0。</p>
<p>他们写代码的理由就是:</p>
<p>一切为了孩子,一切为了伙伴!</p>
<p>回看过往的春节:</p>
<p>几乎每个春节年前的一周,都是许多程序员们会开启拼假的周期。人人都会放下手中的事情,与家人共享天伦。而今年,却有了不一样的状态:他们用一台电脑开启了战斗模式。</p>
<p><strong>从一台电脑,</strong></p>
<p><strong>走向一个战场</strong></p>
<p><em>2020</em></p>
<p>研发部的所有伙伴从大年初一就针对疫情动作开始各种视频会议,为应对高并发公益直播、主讲老师在家直播、辅导老师在家直播、大班系统扩容、公立学校对接等做各种方案讨论和准备工作安排,在直播等工作正式开始后,很多伙伴都在通宵排查和解决问题,并在第一时间组织会议进行复盘,连轴转的工作节奏只为保障用户有更好的上课体验。</p>
<p><img src="/img/bVbEi8s" alt="微信图片_202003091557183.png" title="微信图片_202003091557183.png"></p>
<p>?研发部W老师克服身体不适,毫不犹豫的也加入了保障直播的视频会议中。</p>
<p><img src="/img/bVbEi8t" alt="微信图片_202003091557184.png" title="微信图片_202003091557184.png"></p>
<p>前端小伙伴们也在与直播的小伙伴们一起感受疫情给在线教学带来的挑战,全组开启开挂模式进入了工作状态!</p>
<p><img src="/img/bVbEi8C" alt="微信图片_202003091557185.png" title="微信图片_202003091557185.png"></p>
<p>你看,老师们全力以赴,为了保证公益直播的顺利进行,相继在家中奋战到一线!</p>
<p>我们是熬夜的</p>
<p>程序员</p>
<p><em>2020</em></p>
<p>??? 和疫情赛跑的这些天,网校的程序员伙伴们用代码24小时诠释着这份坚守与热爱,忘记了是白天还是黑夜。你看,他们认真的样子很酷!</p>
<p><img src="/img/bVbEi8F" alt="微信图片_202003091557186.png" title="微信图片_202003091557186.png"></p>
<p>?研发部的D老师和Z老师,为保障大家网关等重要工作的稳定性及监控数据,也都在24小时站在一线,尤其是Z老师身在湖北老家,疫情最前线,也毫不放松!</p>
<p><img src="/img/bVbEi8I" alt="微信图片_202003091557187.png" title="微信图片_202003091557187.png"></p>
<p>?伙伴们为保障学生停课不停学,支持网校面向全国的免费讲座直播项目,从大年初一就开始讨论研究测试方案,多次集体连续加班到深夜....</p>
<p>你们也是</p>
<p>我们最坚实的后盾</p>
<p><em>2020</em></p>
<p>小编寄语: </p>
<p>这场”战疫“,程序员虽未与白衣天使并肩作战,却以自己特别的方式在我们身后默默支持!</p>
<p>好未来技术侧除了我们提到的部分网校老师在停课不停学这个方向上夜以继日的工作,同时也有培优、家长帮等事业部的程序员老师在不断发力。让我们一起为他们点赞!同时也以此短片向所有程序员小哥哥小姐姐致敬!视频地址:<a href="https://link.segmentfault.com/?enc=vdD%2BfR795Q%2B%2FYLDkZxwpsg%3D%3D.Di5g9w8XR%2F5lKdM3bPPpVTkFy%2BxyP94krRtElBSBs%2FJXo%2BYorySjA3SPnecHkOrm" rel="nofollow">https://v.qq.com/x/page/n0931...</a></p>
<p>号外宣传~我们不仅有技术,还有文化,也欢迎大家踊跃关注"<strong>好未来技术</strong>"的重要伙伴“<strong>文化行者</strong>”公众号。</p>
<p><img src="/img/bVbEi8K" alt="3B2090F9-3DC4-4f35-AEE4-F603074ADEE9.png" title="3B2090F9-3DC4-4f35-AEE4-F603074ADEE9.png"></p>
前端组件化-高质高效协作利器
https://segmentfault.com/a/1190000021960636
2020-03-09T17:59:04+08:00
2020-03-09T17:59:04+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
2
<p>小编寄语</p>
<p>在这个“不出门就是给国家做贡献”的日子里,技术委员会提前祝大家元宵节快乐,祝我们的生活像汤圆一样甜蜜圆满。也请伙伴们少出门,勤洗手,戴口罩,为自己和家人的健康保驾护航,同心协力,共克时艰!我们相信阳光会驱散阴霾,一切都会好起来的!</p>
<p>小编今天分享一篇由家长帮倪思远老师带来的技术好文《前端组件化-高质高效协作利器》,本文主要介绍了作者对于组件化的理解以及组内是如何实现组件化的,希望能够对大家有所启发和帮助~</p>
<p><strong>前言</strong> </p>
<p>项目开发过程中,随着业务的不断迭代,很容易暴露以下问题: <br>1、代码体积会不断增加,代码的冗余会越来越大; <br>2、业务逻辑复杂度会不断增加,逻辑的可拓展性、可维护性越来越脆弱;</p>
<p>问题的主要原因在于: <br>1、功能代码的复用方式是简单粗暴拷贝和粘贴; <br>2、多人协作导致代码耦合度高,后期维护拓展方式不合理;</p>
<p>针对以上问题,在前端项目工程化的基础上,引入前端组件化,从功能模块的复用、通信,及多人协作的层面进行解耦。</p>
<p>预期效果:</p>
<p>1.通过组件化的编码,实现功能的封装和复用,减少冗余</p>
<p>2.通过组件化的协作,实现功能和业务的解耦,高效协作</p>
<p><strong>一.什么是前端组件化?</strong></p>
<p>前端组件化以前经历的过程:面向过程–面向对象–面向模块。</p>
<p>前端业务复杂度的不断提升,传统的以HTML+JQ为主的面向过程的开发方式已经很难满足大型项目的迭代和维护。面向对象方式(继承、封装、多态、实例化构造)、借助模块化加载器(require、sea等)以及后来的工程化工具(wp、gulp等)实现的面向模块的方式,都是为了做封装、配置、复用、业务解耦、协作解耦等提高质量和效率的优化,本文不做过多赘述。</p>
<p><strong>前端的组件化,是指通过对项目进行自顶而下的拆分,对项目内(间)通用的、可复用的功能通过组件的形式和业务逻辑进行解耦,以实现代码的高内聚、低耦合,实现功能模块的可配置、可复用、可拓展,再由这些组件组合更复杂的组件/页面,同时实现多人协作过程中依赖解耦的一种项目设计和实施方式。</strong></p>
<p>对比面向过程、面向对象、面向模块,组件化(面向组件)在兼具了低耦合、高内聚、可配置、可复用、可拓展等优势外,给项目的实施提供了一种类似"搭积木"的方式,具有更高的灵活性,可拓展性和可维护性。</p>
<p><strong>二、为什么实施前端组件化?</strong></p>
<p><strong>1.编码层面提效</strong></p>
<p><strong>· 通过组件化的开发方式,实现功能模块的封装</strong></p>
<p><strong>· 通过组件的复用,减少代码冗余</strong></p>
<p><strong>· 通过组件化的开发方式,实现功能和业务逻辑的解耦</strong></p>
<p><strong>· 通过组件的拓展支持项目迭代,提高可拓展性、可维护性</strong></p>
<p><strong>2.协作层面提效</strong></p>
<p><strong>· 通过组件化的设计,自顶而下对项目进行拆分</strong></p>
<p><strong>· 从按页面分工变为按功能组件分工</strong></p>
<p><strong>· 通过组件的接口化、配置化实现功能和业务整合</strong></p>
<p><strong>3.符合主流框架设计初衷</strong></p>
<p><strong>Vue-组件化构建应用(Vue中万物皆组件)</strong></p>
<p><img src="/img/bVbEi5t" alt="1.png" title="1.png"></p>
<p>React-主要特性之一 </p>
<p><img src="/img/bVbEi5u" alt="2.png" title="2.png"><br><strong>三、什么时候实施前端组件化?</strong></p>
<p><strong>1.如果项目不是一次性开发,需要长期迭代维护:</strong><strong>组件化能实现功能和业务逻辑的解耦,实现功能模块的复用,已便于基于现有功能快速响应,通过拼接组合的方式快速实现迭代。</strong></p>
<p><strong>2.需要对项目包体积进行优化:</strong><strong>组件化能很大程度提高代码和功能模块的复用性,减少代码冗余。</strong></p>
<p><strong>3.多人协作完成同一项目:<strong><em><em>组件化的项目拆分和分工,能够避免不同页面相同/相似功能的开发成本、统一维护和拓展成本;</em></em></strong>同时还能将伙伴间的协作解耦,分工更清晰,代码提交不易发生冲突。</strong></p>
<p><strong>四、如何实施前端组件化?</strong></p>
<p><strong>1.项目的组件化拆分</strong></p>
<p><strong>· 如何拆分?</strong> <br><strong>需要整体把握项目需求,将通用性的功能进行抽离;<strong><em><em>或者从现有功能出发,按照功能进行组件拆分;</em></em></strong>或则从协作角度,从分工和配合层面进行组件拆分。</strong> <br><strong>项目的拆分思路借鉴Vue官网的一个图形象说明:</strong></p>
<p><img src="/img/remote/1460000021960639" alt="" title=""></p>
<p><strong>· 拆分的两个维度</strong> <br><strong>a.项目内可全局公用</strong> <br><strong>基础组件包括不限于(下拉,tap切换,弹窗,toast提示,loading,空页面,页面布局),可参考ElementUI、AntD组件库的实现思路</strong> <br><strong>b.项目内可复用</strong> <br><strong>业务组件多</strong><strong>次调用,即同页面或多页面都会用到的功能模块(如内容发布、评论等业务组件)</strong></p>
<p><strong>· 组件的分类和抽象程度</strong> <br><strong>a.基础组件(全局可用的底层组件)-最大程度通用化开发,通用化配置</strong> <br><strong>b.通用组件(页面级别的复用功能)–满足多场景复用业务</strong> <br><strong>b.业务组件(无复用,为方便协作)–组件化封装抽离,可备后期拓展及复用</strong></p>
<p><strong>· 从协作的角度进行拆分</strong> <br><strong>a.分模块开发,避免多人重复工作</strong> <br><strong>b.相同功能,统一维护</strong> <br><strong>b.相似功能基于现有模块快速拼装</strong></p>
<p><strong><em>*· </em></strong>举个栗子:*</p>
<p><img src="/img/bVbEi7D" alt="4.jpg" title="4.jpg"></p>
<p>上图是实际项目(已上线)中部分功能拆分的示例:<strong>需求</strong>:视频详情页(左1)、图文详情页(左2)、弹窗(左3)、弹层(左4,下)</p>
<p><strong>拆分思路</strong>:视频页、图文页,除了顶部视频和图文展示不同,其他功能相同,可按功能实现组件化拆分以实现页面复用。</p>
<p><strong>组件拆分</strong>:如图右下角所示</p>
<p><strong>组件的分类</strong>: <br>a.基础组件-弹窗组件、登录弹层 <br>b.通用组件-评论组件、瀑布流 <br>b.业务组件-视频播放、图片预览</p>
<p><strong>协作层面</strong>: <br>a.多人开发 <br>b.先开发组件–并行开发 <br>c.专人组合页面逻辑–无提交代码冲突 <br>d.不同页面复用–复用组件,无冗余</p>
<p><strong>Tips:</strong>可能有人会觉得的复用一个页面是可以的,没必要做复杂的拆分。但是如果后期又有图文混排页、热门讨论页…等跟这两个页面功能、结构都类似的页面呢?实际情况就是这样的,通过现有功能组件,在开发n个页面也是"搭积木"的思路。</p>
<p><strong>2.组件设计</strong></p>
<p><strong>· 组件交互配合方式设计</strong></p>
<p>a.沟通数据传递方式 <br>b.沟通事件交互方式 <br>c.沟通样式适配方式</p>
<p><strong>· 组件接口设计(前三个即可满足大多数场景)</strong></p>
<p>a.用户自定义类名(extraClass) <br>b.源数据(data) <br>c.交互事件(onTap) <br>d.钩子函数(组件实例化、组件销毁、onShow、onHide) <br>e.组件实例方法(hide、refresh) <br>f.通信参数(全局状态管理中依赖数据的key)</p>
<p><strong><em><strong><em>· </em></strong>举个栗子:</em></strong></p>
<p><img src="/img/bVbEi7E" alt="5.png" title="5.png"></p>
<p>上图是瀑布流组件开发前设计:</p>
<p>瀑布流组件主要被设计为接受数据源、翻页时增量数据(性能层面考虑)、item点击上报。</p>
<p>这样就实现了功能的封装内聚,用户使用时只需要做透明化配置即可,同时方便复用,后期如需拓展可以参数的方式进行拓展支持。</p>
<p><strong><em><em><em></em>3.组建的开发</em></em></strong>**</p>
<p><strong><em><em></em></em></strong>·<strong><em></em></strong>核心:**</p>
<p>数据交互-数据绑定</p>
<p>事件交互-事件上报</p>
<p><strong><em><em></em></em></strong>·举个栗子(Vue技术栈下):<strong><em><em></em></em></strong></p>
<p>A.下拉组件的实现:</p>
<p><img src="/img/bVbEi7I" alt="6.png" title="6.png"></p>
<p>props: <br>a.list-下拉选项数组 <br>b.def-默认选项 <br>c.map-可以配置list里需要展示字段的key <br>event: <br>onItemTap:下拉项点击回调 <br>B.组件的复用</p>
<p><img src="/img/bVbEi7L" alt="7.png" title="7.png"></p>
<p>同一页面的三个下拉,是同一组件的三个实例,互相独立。 <br>另外中部的tap切换和下面的列表都是组件,这样整个页面涉及的功能集成度都很高,复用性、统一拓展维护都很方便,这个页面的搭建还很干净整洁。 </p>
<p><strong>五、组件化思想的延申</strong></p>
<p>在Vue技术栈下,每个.vue文件都是一个单文件组件。 <br>本质上说,Vue中的页面也是组件。也可以说,页面,也可以用组件化的思路去构建或使用。</p>
<p><strong><em><strong><em>· </em></strong>举个栗子:</em></strong></p>
<p><img src="/img/bVbEi7P" alt="8.png" title="8.png"><br>feed-home/index.vue 是一个页面,出于分包需求,需要在主包和子包都有一个该页面,简单操作,可以直接复制一个。前期简单了,后期的维护就不简单了!所以从单文件组件的本质出发,取巧,通过组件引入的方式在子包引用了主包的一个实例。项目实测可以满足。</p>
<p><strong>结语</strong></p>
<p>在中大型的、多人协作的项目中,通过组件化的方式进行项目的拆解、设计、开发,通过组件化的方式进行分工协作,能够极大的减少代码冗余,降低不同模块的耦合度,同时也实现开发协作层面的解耦。</p>
<p>组件化的落地不是一蹴而就的,需要不断的磨合配合,需要建立一致的认知和协作方式。 <br>另外,在统一的技术规范和技术栈体系下,跨项目间也是可以通过npm私有源或git-submodule来实现组件化复用的,当然受业务的限制,目前这个还有些难度,这个也是我们正在努力的方向。</p>
<p><img src="/img/bVbEi62" alt="9.jpg" title="9.jpg"></p>
重磅总结|9大技术方向,30+篇好文!2019好未来技术合辑新鲜出炉!
https://segmentfault.com/a/1190000021956785
2020-03-09T12:40:18+08:00
2020-03-09T12:40:18+08:00
好未来技术团队
https://segmentfault.com/u/haoweilaijishutuandui
4
<p><img src="/img/bVbEiuH" alt="-1.png" title="-1.png"></p>
<p>岁月如梭,2020年是好未来成立的第十七年。作为中国最大的民营教培企业之一,好未来正在经历一场蝶变,用科技推动教育进步。</p>
<p> 科技从来不是洪水猛兽,它以其创造性的力量推动着社会方方面面的发展和变革。教育行业也不例外,未来教育一定会和科技结合,它将是促进教育自我完善进步的重要手段。</p>
<p>好未来作为一家科技驱动的教育企业,始终坚持“用科技推动教育进步”的使命。在过去数年,好未来在科技与教育的融合创新上有了诸多的探索,也有了一些落地实践成果可与行业分享。</p>
<p> 1月21日,好未来第一本技术合辑与大家见面了。希望我们的技术分享,能够对教育从业者有所帮助。 </p>
<p>也欢迎大家多关注“好未来技术”微信公众号,随时和我们交流。如发现任何问题,可通过好未来技术微信公众号反馈给我们。</p>
<p><strong>目录如下:</strong><br><img src="/img/bVbEivJ" alt="-2.jpg" title="-2.jpg"><br>以上我们精选了来自好未来学而思网校、学而思培优、硅谷研发部、脑科学实验室等各个事业部分享的前端、后端、算法、数据、运维等多方面的优秀技术案例在教育中的实践应用给大家。希望小伙伴们在新的一年收获多多!</p>
<p>欢迎大家将本合辑分享给兴趣相同的同事、朋友。一起学习,一起切磋! </p>
<p>新的一年,好未来技术团队祝大家万事顺意、未来可期、新年快乐!</p>
<p><strong>如何获取?</strong></p>
<p>长按并识别文末的二维码,关注“<strong>好未来技术</strong>”官方公众号,回复 “<strong>技术年刊</strong>”,即可免费下载好未来技术精选好文。</p>
<p><img src="/img/bVbEivT" alt="-3.jpg" title="-3.jpg"></p>