Kubernetes Client-go Informer 源码分析

几乎所有的Controller manager 和CRD Controller 都会使用Client-go 的Informer 函数,这样通过Watch 或者Get List 可以获取对应的Object,下面我们从源码分析角度来看一下Client go Informer 的机制。kubeClient, err := kubernetes.NewForConfig(cfg)if err != nil { klog.Fatalf(“Error building kubernetes clientset: %s”, err.Error())}kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)controller := NewController(kubeClient, exampleClient, kubeInformerFactory.Apps().V1().Deployments(), exampleInformerFactory.Samplecontroller().V1alpha1().Foos())// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)// Start method is non-blocking and runs all registered informers in a dedicated goroutine.kubeInformerFactory.Start(stopCh)这里的例子是以https://github.com/kubernetes/sample-controller/blob/master/main.go节选,主要以 k8s 默认的Deployment Informer 为例子。可以看到直接使用Client-go Informer 还是非常简单的,先不管NewCOntroller函数里面执行了什么,顺着代码来看一下kubeInformerFactory.Start 都干了啥。// Start initializes all requested informers.func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Run(stopCh) f.startedInformers[informerType] = true } }}可以看到这里遍历了f.informers,而informers 的定义我们来看一眼数据结构type sharedInformerFactory struct { client kubernetes.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool}我们这里的例子,在运行的时候,f.informers里面含有的内容如下type *v1.Deployment informer &{0xc000379fa0 <nil> 0xc00038ccb0 {} 0xc000379f80 0xc00033bb00 30000000000 30000000000 0x28e5ec8 false false {0 0} {0 0}}也就是说,每一种k8s 类型都会有自己的Informer函数。下面我们来看一下这个函数是在哪里注册的,这里以Deployment Informer 为例。首先回到刚开始初始化kubeClient 的代码,controller := NewController(kubeClient, exampleClient, kubeInformerFactory.Apps().V1().Deployments(), exampleInformerFactory.Samplecontroller().V1alpha1().Foos()) deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.handleObject, UpdateFunc: func(old, new interface{}) { newDepl := new.(*appsv1.Deployment) oldDepl := old.(*appsv1.Deployment) if newDepl.ResourceVersion == oldDepl.ResourceVersion { // Periodic resync will send update events for all known Deployments. // Two different versions of the same Deployment will always have different RVs. return } controller.handleObject(new) }, DeleteFunc: controller.handleObject, })注意这里的传参, kubeInformerFactory.Apps().V1().Deployments(), 这句话的意思就是指创建一个只关注Deployment 的Informer.controller := &Controller{ kubeclientset: kubeclientset, sampleclientset: sampleclientset, deploymentsLister: deploymentInformer.Lister(), deploymentsSynced: deploymentInformer.Informer().HasSynced, foosLister: fooInformer.Lister(), foosSynced: fooInformer.Informer().HasSynced, workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), “Foos”), recorder: recorder, }deploymentInformer.Lister() 这里就是初始化了一个Deployment Lister,下面来看一下Lister函数里面做了什么。// NewFilteredDeploymentInformer constructs a new informer for Deployment type.// Always prefer using an informer factory to get a shared informer instead of getting an independent// one. This reduces memory footprint and number of connections to the server.func NewFilteredDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).List(options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).Watch(options) }, }, &appsv1.Deployment{}, resyncPeriod, indexers, )}func (f *deploymentInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredDeploymentInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)}func (f *deploymentInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&appsv1.Deployment{}, f.defaultInformer)}func (f *deploymentInformer) Lister() v1.DeploymentLister { return v1.NewDeploymentLister(f.Informer().GetIndexer())}注意这里的Lister 函数,它调用了Informer ,然后触发了f.factory.InformerFor ,这就最终调用了sharedInformerFactory InformerFor函数,// InternalInformerFor returns the SharedIndexInformer for obj using an internal// client.func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) f.informers[informerType] = informer return informer}这里可以看到,informer = newFunc(f.client, resyncPeriod)这句话最终完成了对于informer的创建,并且注册到了Struct object中,完成了前面我们的问题。下面我们再回到informer start // Start initializes all requested informers.func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Run(stopCh) f.startedInformers[informerType] = true } }}这里可以看到,它会遍历所有的informer,然后选择异步调用Informer 的RUN方法。我们来全局看一下Run方法func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) { defer utilruntime.HandleCrash() fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, s.indexer) cfg := &Config{ Queue: fifo, ListerWatcher: s.listerWatcher, ObjectType: s.objectType, FullResyncPeriod: s.resyncCheckPeriod, RetryOnError: false, ShouldResync: s.processor.shouldResync, Process: s.HandleDeltas, } func() { s.startedLock.Lock() defer s.startedLock.Unlock() s.controller = New(cfg) s.controller.(*controller).clock = s.clock s.started = true }() // Separate stop channel because Processor should be stopped strictly after controller processorStopCh := make(chan struct{}) var wg wait.Group defer wg.Wait() // Wait for Processor to stop defer close(processorStopCh) // Tell Processor to stop wg.StartWithChannel(processorStopCh, s.cacheMutationDetector.Run) wg.StartWithChannel(processorStopCh, s.processor.run) defer func() { s.startedLock.Lock() defer s.startedLock.Unlock() s.stopped = true // Don’t want any new listeners }() s.controller.Run(stopCh)}首先它根据得到的 key 拆分函数和Store index 创建一个FIFO队列,这个队列是一个先进先出的队列,主要用来保存对象的各种事件。func NewDeltaFIFO(keyFunc KeyFunc, knownObjects KeyListerGetter) *DeltaFIFO { f := &DeltaFIFO{ items: map[string]Deltas{}, queue: []string{}, keyFunc: keyFunc, knownObjects: knownObjects, } f.cond.L = &f.lock return f}可以看到这个队列创建的比较简单,就是使用 Map 来存放数据,String 数组来存放队列的 Key。后面根据client 创建的List 和Watch 函数,还有队列创建了一个 config,下面将根据这个config 来初始化controller. 这个controller是client-go 的Cache controller ,主要用来控制从 APIServer 获得的对象的 cache 以及更新对象。下面主要关注这个函数调用wg.StartWithChannel(processorStopCh, s.processor.run)这里进行了真正的Listering 调用。func (p *sharedProcessor) run(stopCh <-chan struct{}) { func() { p.listenersLock.RLock() defer p.listenersLock.RUnlock() for _, listener := range p.listeners { p.wg.Start(listener.run) p.wg.Start(listener.pop) } p.listenersStarted = true }() <-stopCh p.listenersLock.RLock() defer p.listenersLock.RUnlock() for _, listener := range p.listeners { close(listener.addCh) // Tell .pop() to stop. .pop() will tell .run() to stop } p.wg.Wait() // Wait for all .pop() and .run() to stop}主要看 run 方法,还记得前面已经把ADD UPDATE DELETE 注册了自定义的处理函数了吗。这里就实现了前面函数的触发func (p processorListener) run() { // this call blocks until the channel is closed. When a panic happens during the notification // we will catch it, the offending item will be skipped!, and after a short delay (one second) // the next notification will be attempted. This is usually better than the alternative of never // delivering again. stopCh := make(chan struct{}) wait.Until(func() { // this gives us a few quick retries before a long pause and then a few more quick retries err := wait.ExponentialBackoff(retry.DefaultRetry, func() (bool, error) { for next := range p.nextCh { switch notification := next.(type) { case updateNotification: p.handler.OnUpdate(notification.oldObj, notification.newObj) case addNotification: p.handler.OnAdd(notification.newObj) case deleteNotification: p.handler.OnDelete(notification.oldObj) default: utilruntime.HandleError(fmt.Errorf(“unrecognized notification: %#v”, next)) } } // the only way to get here is if the p.nextCh is empty and closed return true, nil }) // the only way to get here is if the p.nextCh is empty and closed if err == nil { close(stopCh) } }, 1time.Minute, stopCh)}可以看到当p.nexhCh channel 接收到一个对象进入的时候,就会根据通知类型的不同,选择对应的用户注册函数去调用。那么这个channel 谁来向其中传入参数呢func (p *processorListener) pop() { defer utilruntime.HandleCrash() defer close(p.nextCh) // Tell .run() to stop var nextCh chan<- interface{} var notification interface{} for { select { case nextCh <- notification: // Notification dispatched var ok bool notification, ok = p.pendingNotifications.ReadOne() if !ok { // Nothing to pop nextCh = nil // Disable this select case } case notificationToAdd, ok := <-p.addCh: if !ok { return } if notification == nil { // No notification to pop (and pendingNotifications is empty) // Optimize the case - skip adding to pendingNotifications notification = notificationToAdd nextCh = p.nextCh } else { // There is already a notification waiting to be dispatched p.pendingNotifications.WriteOne(notificationToAdd) } } }}答案就是这个pop 函数,这里会从p.addCh中读取增加的通知,然后转给p.nexhCh 并且保证每个通知只会读取一次。下面就是最终的Controller run 函数,我们来看看到底干了什么// Run begins processing items, and will continue until a value is sent down stopCh.// It’s an error to call Run more than once.// Run blocks; call via go.func (c *controller) Run(stopCh <-chan struct{}) { defer utilruntime.HandleCrash() go func() { <-stopCh c.config.Queue.Close() }() r := NewReflector( c.config.ListerWatcher, c.config.ObjectType, c.config.Queue, c.config.FullResyncPeriod, ) r.ShouldResync = c.config.ShouldResync r.clock = c.clock c.reflectorMutex.Lock() c.reflector = r c.reflectorMutex.Unlock() var wg wait.Group defer wg.Wait() wg.StartWithChannel(stopCh, r.Run) wait.Until(c.processLoop, time.Second, stopCh)}这里主要的就是wg.StartWithChannel(stopCh, r.Run),// Run starts a watch and handles watch events. Will restart the watch if it is closed.// Run will exit when stopCh is closed.func (r *Reflector) Run(stopCh <-chan struct{}) { klog.V(3).Infof(“Starting reflector %v (%s) from %s”, r.expectedType, r.resyncPeriod, r.name) wait.Until(func() { if err := r.ListAndWatch(stopCh); err != nil { utilruntime.HandleError(err) } }, r.period, stopCh)}这里就调用了r.ListAndWatch 方法,这个方法比较复杂,我们慢慢来看。// watchHandler watches w and keeps *resourceVersion up to date.func (r *Reflector) watchHandler(w watch.Interface, resourceVersion *string, errc chan error, stopCh <-chan struct{}) error { start := r.clock.Now() eventCount := 0 // Stopping the watcher should be idempotent and if we return from this function there’s no way // we’re coming back in with the same watch interface. defer w.Stop() // update metrics defer func() { r.metrics.numberOfItemsInWatch.Observe(float64(eventCount)) r.metrics.watchDuration.Observe(time.Since(start).Seconds()) }()loop: for { select { case <-stopCh: return errorStopRequested case err := <-errc: return err case event, ok := <-w.ResultChan(): if !ok { break loop } if event.Type == watch.Error { return apierrs.FromObject(event.Object) } if e, a := r.expectedType, reflect.TypeOf(event.Object); e != nil && e != a { utilruntime.HandleError(fmt.Errorf("%s: expected type %v, but watch event object had type %v", r.name, e, a)) continue } meta, err := meta.Accessor(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) continue } newResourceVersion := meta.GetResourceVersion() switch event.Type { case watch.Added: err := r.store.Add(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Modified: err := r.store.Update(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Deleted: // TODO: Will any consumers need access to the “last known // state”, which is passed in event.Object? If so, may need // to change this. err := r.store.Delete(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", r.name, event.Object, err)) } default: utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) } resourceVersion = newResourceVersion r.setLastSyncResourceVersion(newResourceVersion) eventCount++ } } watchDuration := r.clock.Now().Sub(start) if watchDuration < 1time.Second && eventCount == 0 { r.metrics.numberOfShortWatches.Inc() return fmt.Errorf(“very short watch: %s: Unexpected watch close - watch lasted less than a second and no items received”, r.name) } klog.V(4).Infof("%s: Watch close - %v total %v items received", r.name, r.expectedType, eventCount) return nil}这里就是真正调用watch 方法,根据返回的watch 事件,将其放入到前面创建的 FIFO 队列中。最终调用了controller 的POP 方法// processLoop drains the work queue.// TODO: Consider doing the processing in parallel. This will require a little thought// to make sure that we don’t end up processing the same object multiple times// concurrently.//// TODO: Plumb through the stopCh here (and down to the queue) so that this can// actually exit when the controller is stopped. Or just give up on this stuff// ever being stoppable. Converting this whole package to use Context would// also be helpful.func (c *controller) processLoop() { for { obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) if err != nil { if err == FIFOClosedError { return } if c.config.RetryOnError { // This is the safe way to re-enqueue. c.config.Queue.AddIfNotPresent(obj) } } }}前面是将 watch 到的对象加入到队列中,这里的goroutine 就是用来消费的。具体的消费函数就是前面创建的Process 函数func (s *sharedIndexInformer) HandleDeltas(obj interface{}) error { s.blockDeltas.Lock() defer s.blockDeltas.Unlock() // from oldest to newest for _, d := range obj.(Deltas) { switch d.Type { case Sync, Added, Updated: isSync := d.Type == Sync s.cacheMutationDetector.AddObject(d.Object) if old, exists, err := s.indexer.Get(d.Object); err == nil && exists { if err := s.indexer.Update(d.Object); err != nil { return err } s.processor.distribute(updateNotification{oldObj: old, newObj: d.Object}, isSync) } else { if err := s.indexer.Add(d.Object); err != nil { return err } s.processor.distribute(addNotification{newObj: d.Object}, isSync) } case Deleted: if err := s.indexer.Delete(d.Object); err != nil { return err } s.processor.distribute(deleteNotification{oldObj: d.Object}, false) } } return nil}这个函数就是根据传进来的obj,先从自己的cache 中取一下,看是否存在,如果存在就代表是Update ,那么更新自己的队列后,调用用户注册的Update 函数,如果不存在,就调用用户的 Add 函数。到此Client-go 的Informer 流程源码分析基本完毕。本文作者:xianlubird 阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 22, 2019 · 8 min · jiezi

python内置函数 map/reduce

Python内建了map()和reduce()函数。如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白map/reduce的概念。我们先看map。map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:def f(x): return x*xr = map(f,[1,2,3,4,5,6,7,8,9])print(list(r))[1, 4, 9, 16, 25, 36, 49, 64, 81]map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。你可能会想,不需要map()函数,写一个循环,也可以计算出结果:L = []for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]: L.append(f(n))print(L)的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))[‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’]只需要一行代码。再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)比方说对一个序列求和,就可以用reduce实现:from functools import reducedef add(x, y): return x + yreduce(add, [1, 3, 5, 7, 9])25当然求和运算可以直接用Python内建函数sum(),没必要动用reduce。但是如果要把序列[1, 3, 5, 7, 9]变换成整数13579,reduce就可以派上用场:from functools import reducedef fn(x, y): return x * 10 + yreduce(fn, [1, 3, 5, 7, 9])13579这个例子本身没多大用处,但是,如果考虑到字符串str也是一个序列,对上面的例子稍加改动,配合map(),我们就可以写出把str转换为int的函数:from functools import reducedef fn(x, y): return x * 10 + ydef char2num(s): digits = {‘0’: 0, ‘1’: 1, ‘2’: 2, ‘3’: 3, ‘4’: 4, ‘5’: 5, ‘6’: 6, ‘7’: 7, ‘8’: 8, ‘9’: 9} return digits[s]reduce(fn, map(char2num, ‘13579’))13579整理成一个str2int的函数就是:from functools import reduceDIGITS = {‘0’: 0, ‘1’: 1, ‘2’: 2, ‘3’: 3, ‘4’: 4, ‘5’: 5, ‘6’: 6, ‘7’: 7, ‘8’: 8, ‘9’: 9}def str2int(s): def fn(x, y): return x * 10 + y def char2num(s): return DIGITS[s] return reduce(fn, map(char2num, s))还可以用lambda函数进一步简化成:from functools import reduceDIGITS = {‘0’: 0, ‘1’: 1, ‘2’: 2, ‘3’: 3, ‘4’: 4, ‘5’: 5, ‘6’: 6, ‘7’: 7, ‘8’: 8, ‘9’: 9}def char2num(s): return DIGITS[s]def str2int(s): return reduce(lambda x, y: x * 10 + y, map(char2num, s))也就是说,假设Python没有提供int()函数,你完全可以自己写一个把字符串转化为整数的函数,而且只需要几行代码! ...

January 21, 2019 · 2 min · jiezi

2亿用户背后的Flutter应用框架Fish Redux

背景在闲鱼深度使用 Flutter 开发过程中,我们遇到了业务代码耦合严重,代码可维护性糟糕,如入泥泞。对于闲鱼这样的负责业务场景,我们需要一个统一的应用框架来摆脱当下的开发困境,而这也是 Flutter 领域空缺的一块处女地。Fish Redux 是为解决上面问题上层应用框架,它是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用。它的最大特点是配置式组装, 一方面将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现,另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。所以它会非常干净,易编写、易维护、易协作。Fish Redux 的灵感主要来自于 Redux、React、Elm、Dva 这样的优秀框架,而 Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。分层架构图架构图,主体自底而上,分三层,每一层用来解决不通层面的问题和矛盾,下面依次来展开。ReduxRedux 是来自前端社区的一个数据管理框架, 对 Native 开发同学来说可能会有一点陌生,我们做一个简单的介绍。Redux 做什么的?Redux 是一个用来做可预测易调试的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。Redux 是怎么设计和实现的?Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。贫血模型、充血模型 参考:https://en.wikipedia.org/wiki/Plain_old_Java_objectRedux 的缺点Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。在我们实际使用 Redux 中面临两个具体问题Redux 的集中和 Component 的分治之间的矛盾。Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。Fish Redux 的改良Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。我们得到了理想的集中的效果和分治的代码。对社区标准的 followState、Action、Reducer、Store、Middleware 以上概念和社区的 ReduxJS 是完全一致的。我们将原汁原味地保留所有的 Redux 的优势。如果想对 Redux 有更近一步的理解,请参考 https://github.com/reduxjs/reduxComponent组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。关于 ViewView 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget它主要包含三方面的信息视图是完全由数据驱动。视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数关于 EffectEffect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object它主要包含四方面的信息接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。比如:良好的协程的支持关于 ReducerReducer 是一个完全符合 Redux 规范的函数签名:(T,Action) => T一些符合签名的 Reducer同时我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。所以有这样的公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。一个典型的组装通过 Component 的抽象,我们得到了完整的分治,多纬度的复用,更好的解耦。AdapterAdapter 也是对局部的展示和功能的封装。它为 ListView 高性能场景而生,它是 Component 实现上的一种变化。它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题1)将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化。2)Component 无法区分 appear|disappear 和 init|dispose 。3)Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。做出这样独立一层的抽象是,我们看实际的效果, 我们对页面不使用框架,使用框架 Component,使用框架 Component+Adapter 的性能基线对比Reducer is long-lived, Effect is medium-lived, View is short-lived.我们通过不断的测试做对比,以某 android 机为例:使用框架前 我们的详情页面的 FPS,基线在 52FPS。使用框架, 仅使用 Component 抽象下,FPS 下降到 40, 遭遇“Big-Cell”的陷阱。使用框架,同时使用 Adapter 抽象后,FPS 提升到 53,回到基线以上,有小幅度的提升。Directory推荐的目录结构会是这样sample_page– action.dart– page.dart– view.dart– effect.dart– reducer.dart– state.dartcomponentssample_component– action.dart– component.dart– view.dart– effect.dart– reducer.dart– state.dart上层负责组装,下层负责实现, 同时会有一个插件提供, 便于我们快速填写。以闲鱼的详情场景为例的组装:组件和组件之间,组件和容器之间都完全的独立。Communication Mechanism组件|适配器内通信组件|适配器间内通信简单的描述:采用的是带有一段优先处理的广播, self-first-broadcast。发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的所有的通信诉求。Refresh Mechanism数据刷新局部数据修改,自动层层触发上层数据的浅拷贝,对上层业务代码是透明的。层层的数据的拷贝一方面是对 Redux 数据修改的严格的 follow。另一方面也是对数据驱动展示的严格的 follow。视图刷新扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新优点数据的集中管理通过 Redux 做集中化的可观察的数据管理。我们将原汁原味地保留所有的 Redux 的优势,同时在 Reducer 的合并上,变成由框架代理自动完成,大大简化了使用 Redux 的繁琐度。组件的分治管理组件既是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。View、Reducer、Effect 隔离将组件拆分成三个无状态的互不依赖的函数。因为是无状态的函数,它更易于编写、调试、测试、维护。同时它带来了更多的组合、复用和创新的可能。声明式配置组装组件、适配器通过自由的声明式配置组装来完成。包括它的 View、Reducer、Effect 以及它所依赖的子项。良好的扩展性核心框架保持自己的核心的三层关注点,不做核心关注点以外的事情,同时对上层保持了灵活的扩展性。框架甚至没有任何的一行的打印的代码,但我们可通过标准的 Middleware 来观察到数据的流动,组件的变化。在框架的核心三层外,也可以通过 dart 的语言特性 为 Component 或者 Adapter 添加 mixin,来灵活的组合式地增强他们的上层使用上的定制和能力。框架和其他中间件的打通,诸如自动曝光、高可用等,各中间件和框架之间都是透明的,由上层自由组装。精小、简单、完备它非常小,仅仅包含 1000 多行代码。它使用简单,完成几个小的函数,完成组装,即可运行。它是完备的。Fish Redux 目前已在阿里巴巴闲鱼技术团队内多场景,深入应用。本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 18, 2019 · 2 min · jiezi

从NeurIPS 2018看AI发展路线!

摘要: 从NeurIPS 2018看AI发展路线!去年9月份的时候,我发表过一份技术报告,阐述了我认为人工智能最重要的挑战,大概有以下四个方面:可伸缩性(Scalability)**计算或存储的成本不与神经元的数量成二次方或线性比例的神经网络;持续学习(Continual Learning)**那些必须不断地从环境中学习而不忘记之前获得的技能和重置环境能力的代理;元学习(Meta-Learning)**为了改变自己的学习算法而进行自我参照的代理;基准(Benchmarks)**具有足够复杂的结构和多样性的环境,这样智能代理就可以派上用场了,而无需对强感应偏差进行硬编码;在2018年NeurIPS会议期间,我调查了目前其他研究人员关于这些问题的方法和观点,以下是报告的具体内容:可伸缩性很明显,如果我们用人工神经网络来实现人类大脑中所发现的1000亿个神经元,标准的二维矩阵乘积并没有多大的用处。模块层由一个模块池和一个控制器组成,控制器根据输入来选择要执行的模块为了解决这个问题,我在2018年的NeurIPS上发表了研究性论文《模块化网络:学习分解神经计算》。不评估对于每个输入元素的整个ANN,而是将网络分解为一组模块,其中只使用一个子集,这要取决于输入。这个过程是受人脑结构的启发,在其中我们使用了模块化,这也是为了改善对环境变化的适应能力和减轻灾难性的遗忘。在这个方法中,我们学习到了这些模块的参数,以及决定哪些模块要一起使用。以往有关条件计算的文献都记载着许多模块崩溃的问题,即优化过程忽略了大部分可用的模块,从而导致没有用最优的解决办法。我们基于期望最大化的方法可以防止这类问题的发生。遗憾的是,强行将这种分离划分到模块有其自身的问题,我们在《模块化网络:学习分解神经计算》中相继讨论了这些问题。相反地,我们可能会像我在关于稀疏性的技术报告中讨论的那样,设法在权重和激活中利用稀疏性和局部性。简而言之,我们只想对少数非零的激活执行操作,丢弃权重矩阵中的整行。此外,如果连通性是高度稀疏的,那么我们实际上可以将二次方成本降到一个很小的常数。这种的条件计算和未合并的权值访问在当前的GPU上实现的成本非常的高,通常来说不太值得操作。Nvidia处理条件计算和稀疏性NVIDIA一个软件工程师说,目前还没有计划建造能够以激活稀疏性的形式而利用条件计算的硬件。主要原因似乎是通用性与计算速度之间的权衡。为这个用例搭建专用硬件所花费的成本太高了,因为它有可能会限制其它机器学习的应用。相反,NVIDIA目前从软件的角度更加关注权重的稀疏性。GraphCore处理的条件计算和稀疏性GraphCore搭建的硬件允许在靠近处理单元的缓存中向前迁移期间存储激活,而不是在GPU上的全局存储内存中。它还可以利用稀疏性和特定的图形结构,在设备上编译并建立一个计算图形。遗憾的是,由于编译成本太高,这个结构是固定的,不允许条件计算。作为一个整体的判断,对于范围内的条件计算似乎没有对应的硬件解决方案,目前来说我们在很大程度上必须坚持多机器并行的方式。在这方面,NeurIPS发布了一种全新的分配梯度计算方法—Mesh-Tensorflow,该方法不仅可以横跨多机进行计算,还可以跨模型计算,甚至允许更大的模型以分布式的方式进行训练。持续学习长期以来,我一直主张基于深度学习的持续学习系统,即它们能够不断地从经验中学习并积累知识,当新任务出现的时候,这些系统可以提供之前积累的知识以帮助学习。本身,它们需要能够向前迁移,以及防止灾难性的遗忘。NeurIPS的持续学习研讨会正是讨论这些问题的。虽然这两个标准也许是不完整的,但是多个研究者(Mark Ring,Raia Hadsell)提出了一个更大的列表:向前迁移向后迁移无灾难性的遗忘无灾难性的冲突可扩展(固定的存储和计算)可以处理未标记的任务边界能够处理偏移无片段无人控制无可重复状态在我看来,解决这个问题的方法有六种:(部分)重放缓冲区重新生成以前经验的生成模型减缓重要权重的训练冻结权重冗余(更大的网络->可伸缩性)条件计算(->可扩展性)以上这些方法的任何一个都不能处理上述持续学习列表里的所有问题。遗憾的是,这在实践中也是不可能的。在迁移和内存或计算之间总是有一个权衡,在灾难性遗忘和迁移或者内存或者计算之间也总是有一个权衡。因此,很难完全地、定量地衡量一个代理的成功与否。相反,我们应该建立基准环境,要求持续学习代理具备我们所需要的能力,例如,在研讨会上展示的基于星际争霸(Starcraft)的环境。此外,Raia Hadsell认为,持续学习涉及到从依赖i.i.d.(Independent and Identically distributed)数据的学习算法转向从非平稳性分布中学习。尤其是,人类擅长逐步地学习而不是IID。因此,当远离IID需求时,我们有可能能够解锁一个更强大的机器学习范式。论文《通过最大限度地迁移和最小化干扰的持续学习(Continual Learning by Maximizing Transfer and Minimizing Interference)》表明REPTILE(MAML继承者)和减少灾难性遗忘之间有着一个有趣的联系。从重放缓冲区中提取的数据点的梯度(显示在REPTILE)之间的点积导致梯度更新,从而最小化干扰并减少灾难性遗忘。讨论小组内有人认为,我们应该在控制设置环境中进行终身学习实验,而不是监督学习和无监督学习,以防止算法的开发与实际应用领域之间的任何不匹配。折现系数虽然对基于贝尔曼方程(Bellman Equation)的学习是有帮助的,但对于更现实的增强学习环境设置来说可能存在问题。此外,任何学习,特别是元学习,都会由于学分分配而受到固有的限制。因此,开发具有低成本学分分配的算法是智能代理的关键。元学习元学习就是关于改变学习算法其本身。这可能是改变一个内部优化循环的外部优化循环,一个可以改变自身的自引用算法。许多研究人员还关注着快速适应性,即正向迁移,到新的任务或者环境等等。如果我们将一个学习算法的初始参数看作它自己的一部分,则可以将其视为迁移学习或者元学习。Chelsea Finn的一个最新算法—MAML(未知模型元学习法),他对这种快速适应性算法产生了极大的兴趣。例如,MAML可以用于基于模型的强化学习,其中的模型可以快速地进行动态改变。在进化策略梯度(Evolved Policy Gradients ,EPG)中,损失函数使用随机梯度下降法优化策略的参数,同时损失函数的参数也改进了。一个有趣的想法是代理轨迹和策略输出的可区分损失函数的学习。这允许在使用SGD来训练策略时,对损失函数的几个参数进行改进。与此同时,进化策略梯度的作者们表明了,学习到的损失函数通过回报函数进行了泛化,并允许有快速适应性。它的一个主要问题是学分分配非常缓慢:代理必须使用损失函数进行完全地训练,以获得元学习者的平均回报(适合度)。对于学习过的优化器的损失情况变得更加难以控制我在元学习研讨会上的另一个有趣发现是元学习者损失情况的结构。Luke Metz在一篇关于学习优化器的论文中指出,随着更新步骤的展现,优化器参数的损失函数变得更加复杂。我怀疑这是元学习算法的普遍行为,参数值的微小改变可以关系到最终表现中的巨大变化。我对这种分析非常感兴趣。在学习优化的案例中,Luke通过变分优化(Variational Optimization)—进化策略的一种原则性解释,以此缓和损失情况来解决这个问题。基准目前大多数强化学习算法都是以游戏或模拟器为基准环境的,比如ATARI 或者是Mujoco。这些是简单的环境,与现实世界中的复杂性几乎没什么相似之处。研究人员经常唠叨的一个主要问题是,我们的算法来自低效的样本。通过非策略优化和基于模型的强化学习,可以更有效地利用现有数据,从而部分解决这一问题。然而,一个很大的因素是我们的算法没有之前在这些基准中使用过的经验。我们可以通过在算法中手工归纳偏差来避开这一问题,这些算法反映了某些先验知识,但是搭建允许在未来可以利用知识积累的环境有可能更有趣。据我所知,直到现在还没有这种基准环境。雷艇(Minecraft)模拟器可能是最接近这些要求的了。持续学习星际争霸(Starcraft)环境是一个以非常简单的任务开始的课程。对于如此丰富的环境,另外一种选择是建立明确的课程,如前面提到的星际争霸环境,它是由任务课程组成的。这在一定程度上也是Shagun Sodhani在他的论文《Environments for Lifelong Reinforcement Learning》。他在清单上列出了:环境多样性随机性自然性非平稳性多形式短期和长期目标多代理因果相互影响游戏引擎开发商Unity3D发布了一个ML-Agents工具包,用于在使用Unity的环境搭建中进行训练和评估代理。一般来说,现实环境搭建的一个主要问题是需求与游戏实际设计有本质的不同:为了防止过拟合,重要的是,在一个广阔的世界里,物体看起来都是不一样的,因此不能像在电脑游戏中经常做的那样被复制。这意味着为了真正的泛化,我们需要生成的或精心设计的环境。最后,我相信可以使用计算来生成非平稳环境,而不是通过手动来搭建。例如,这有可能是一个具有与现实世界类似环境的物理模拟器。为了节省计算资源,我们也可以从基于三维像素的简化工作开始。如果这个模拟过程呈现了正确的特性,我们有可能可以模拟一个类似于进化的过程,来引导一个非平稳的环境,开发出许多相互影响的生命形式。这个想法很好地拟合了模拟假设(simulation hypothesis)理论,并且与Conway’s Game of Life有一定的联系。这种方法的主要问题是产生的复杂性与人类已知的概念没有相似点。与此同时,由此产生的智能代理将无法迁移到现实世界中。最近,我发现Stanley和Clune的团队在他们的论文《假想:不断地生成越来越复杂和多样化的学习环境(POET: Endlessly Generating Increasingly Complex and Diverse Learning Environments)》中已经部分地实现了这个想法。环境是非平稳性的,可以被看作是一个用于最大化复杂性和代理学习进程的代理。他们将这一观点称为开放式学习,我建议你阅读一下这篇文章。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 18, 2019 · 1 min · jiezi

四大维度全景揭秘阿里巴巴智能对话开发平台

在阿里巴巴智能服务事业部的X蜂会上,小蜜北京团队的高级算法专家李永彬(水德)分享了小蜜智能对话开发平台的构建,围绕平台来源、设计理念、核心技术、业务落地情况四大维度讲述了一个较为完整的智能任务型对话开发平台的全景。以下为演讲具体内容。平台由来为什么要做一个平台?我觉得还是从一个具体的任务型对话的例子说起,在我们日常工作中,一个很高频的场景就是要约一个会议,看一下我们内部的办公助理是怎么来实现约会议的:我说“帮我约一个会议”,然后它问“你是哪一天开会?”,跟它说是“后天下午三点”,接下来它又会问“你跟谁一起开会啊?”,我会把我想约的人告诉它,这个时候它在后台发起一次服务调用,因为它要去后台拿到所有参会者的日程安排,看一下在我说的这个时间有没有共同的空闲时间,如果没有的话它会给我推荐几个时间段,我看了一下我说的那个时间段大家没有共同的空闲时间,所以我就会改一个时间。我说“上午十一点吧”,然后它会接着问,“你会持续多长时间”,我会告诉它“一个小时”,然后它接着问“会议的主题是什么”,然后我跟它说“我们讨论一下下周的上线计划”,到此为止它把所有的信息收集全了,然后它会给我一个 summary,让我确认是不是要发送会议邀约,我回复确认以后,它在后台就会调用我们的邮件系统,把整个会议邀约发出来。这是一个非常典型的任务型的对话,它满足两个条件,第一,它有一个明确的目标;第二,它通过多轮对话交互来达成这个目标。像这样的任务型对话在整个办公行业里面,除了约会议以外还有查考勤、请假、定会议室或者日程安排等等。如果我们把视野再放大一点的话,再看一下电商行业,电商行业里面就会涉及到开发票、催发货、查物流、改地址、收快递等等,也会涉及到很多很多的这样的任务型对话场景;视野再放大一下,我们再看一下电信行业或者整个运营商的行业里面,会有查话费、查流量、买套餐、报故障或者是进行密码的更改服务等,也会有大量的这种任务型的对话场景。如果我们再一步去看的话,像政务、金融、教育、文娱、健康、旅游等,在各行各业的各种场景里面我们都会发现这种任务型的对话,它是一种刚需,是一种普遍性的存在。所有的这些场景落地到我们小蜜家族的时候,是通过刚刚介绍过的三大小蜜来承载:阿里小蜜、店小蜜和云小蜜。我们不可能给每一个行业里面的每一个场景去定制一个对话流程,所以我们就沿用了阿里巴巴一贯做平台的思路,这也是我们整个智能对话开发平台的由来。这款产品在内部的名字叫对话工厂(Dialog Studio)。以上主要是给大家介绍我们为什么要做智能对话开发平台,总结起来就是我们目前面临的业务,面临的场景太宽泛了,不可能铺那么多人去把所有的场景都定制化,所以我们需要有一个平台来让开发者进来开发各行各业的各种场景对话。设计理念再看第二部分,对话工厂的一些核心设计理念。整个设计理念这块我觉得概括起来就是“一个中心,三个原则”。一个中心就是以对话为中心,这句话大家可能觉得有点莫名其妙,你做对话的,为何还要强调以对话为中心呢?这是有来源的,因为在过去几年全世界范围的技术实践以及直到今天很多巨头的对话平台里面,我们能看到的基本还是以意图为中心的设计模式,它把意图平铺在这里,比如你想完成音乐领域的一些事情,可是你看到的其实是一堆平铺的意图列表,完全看不出对话在哪里。我们在这次对话工厂的设计中彻底把它扭转回来,对话就是要以对话为中心,你在我们的产品界面里面看到的不再是一个个孤立的意图,而是关联在一起的、有业务逻辑关系的对话流程。以意图为中心的设计中,你看到的其实是一个局部视角,就只能实现一些简单的任务,比如控制一个灯,讲个笑话,或者查个天气,如果你想实现一个复杂的任务,比如开一个发票,或者去 10086 里开通一个套餐,它其实是较难实现,很难维护的。我们把整个理念转换一下,回到以对话为中心以后,就会看到全局视野,可以去做复杂的任务,可以去做无限的场景。整个对话工厂刚刚也说过了,它是一个平台,要做一个平台就会遇到很多挑战:第一个挑战就是对用户来说,希望使用门槛越低越好;第二个挑战是要面对各行各业的各种场景,就要求能做到灵活定制;第三个挑战是上线以后所有的用户肯定都希望你的机器人,你的对话系统能够越用越好,而不是停留在某一个水平就不动了。这就是我们平台所面临的三大挑战。为了应对这三个挑战,我们提出了在整个平台的设计以及实现过程中始终要遵循三个原则。第一个原则是冷启动要快,其实就是要让用户的使用门槛低一点;第二个原则是要有灵活定制的能力,只有这样才能满足各行各业的各种场景需求;第三个是要有鲁棒进化的能力,就是模型上线以后,随着时间的变化,随着各种数据的不断回流,模型效果要不断提升。冷启动,就是要把用户用到的各种能力和各种数据都尽量变成一种预置的能力,简单来说就是平台方做得越多,用户就做得越少;灵活定制,就要求我们把整个对话平台的基础元素进行高度抽象,你抽象的越好就意味着你平台的适应能力越好,就像是经典力学只要三条定律就够了;鲁棒进化,这一块就是要在模型和算法上做深度了,语言理解的模型,对话管理的模型,数据闭环,主动学习,在这些方面能够做出深度来。以上说的都是一些理念和原则,接下来给大家介绍一下具体在实现过程中是怎么来做的。核心技术讲到技术这块的话,因为我们做的是一个平台,涉及到的技术非常广,是全栈的技术,从算法到工程到前端到交互所有的技术都会涉及到。我摘取里面算法的核心部分来给大家做一个介绍。对话工厂首先是用来做对话的,人机对话有两个主体,一个是人,一个是机器,人有人的逻辑,人的逻辑使用什么来表达呢?到今天为止主要还是通过语言,所以我们需要有一个语言理解的服务来承载这一块;机器有机器的逻辑,机器的逻辑到今天为止还是通过代码来表达的,所以我们需要一个函数计算的服务;在人和机器对话的过程中,这种对话过程需要有效的管理,所以我们需要一个对话管理模块。整个对话工厂最核心的三个模块就是语言理解、对话管理和函数计算。第一个模块是语言理解。我们先看一下这个图,在整个这个图里面,横轴是意图的多样性,纵轴是频次,这样说有点抽象,我举一个具体的例子,比如说我要开发票,这是一个意图,如果去采样十万条这个意图的用户说法作为样本,把这些说法做一个频率统计,可能排在第一位的就是三个字“开发票”,它可能出现了两万次,另外排在第二位可能是“开张发票”,它可能出现了八千次,这些都是一些高频的说法,还有一些说法说的很长,比如“昨天我在你们商铺买了一条红色的裙子,你帮我开个发票呗”,这种带着前因后果的句式,在整个说法里面是比较长尾的,可能只出现了一次或两次。我们统计完以后,整个意图的说法的多样性分布符合幂律分布。这种特征可以让我们在技术上进行有效的针对性设计,首先针对这种高频的部分,我们可以上一些规则,比如上下文无关文法,可以比较好的 cover 这一块,但是基于规则的方法,大家也知道,规则是没有泛化能力的,所以这时候要上一个匹配模型,计算一个相似度来辅助规则,这两块结合在一起就可以把我们高频确定性的部分解决的比较好;对于长尾的多样性的这一部分,基本到今天为止还是上有监督的分类模型,去收集或者去标注很多数据,把这一块做好;在规则和分类模型之间,我们又做了一部分工作,就是迁移学习模型,为什么要引入这个模型呢?我们看下一张图。在冷启动阶段,用户在录入样本的时候,不会录入太多,可能录入十几条几十条就已经很多了,这个时候按照刚才那个幂律分布,二八原则的话,它的效果的话可能也就是 70% 多,它不可能再高了。但对于用户的期望来说,如果想要上线,想要很好的满足他的用户需求,其实是想要模型效果在 90% 以上,如果想要达到这个效果,就需要复杂的模型,需要标注大量数据。所以其实是存在一个 gap 的,我们引入了迁移学习模型。具体来说,我们把胶囊网络引进来和 few-shot learning 结合在一起,提出了一个网络结构叫 Induction Network,就是归纳网络。整个网络结构有三层,一层是 Encoder层,第二层是 Induction,归纳层,第三层是 Relation 层。第一层负责将每一个类的每一个样本进行编码,编码成一个向量;第二层是最核心的一层,也就是归纳层,这里面利用胶囊网络的一些方法,把同一个类的多个向量归纳成一个向量;然后第三层 Relation 层把用户新来的一句话和每一个类的归纳向量进行关系计算,输出他们的相似性打分。如果我们想要一个分类结果就输出一个 One-hot,如果不想要 One-hot,就输出一个关系的 Relation score,这是整个 Induction network 的网络结构。这个网络结构提出来以后,在学术圈里面关于 few-shot learning 的数据集上,我们以比较大的提升幅度做到了 state-of-the-art 的效果,目前是最好的,同时我们将整个网络结构上线到了我们的产品里面,这是语言理解。第二块我们看对话管理。对话管理其实我刚刚也说过了,如果想要让平台有足够的适应性的话,那么它的抽象能力一定要好。对话管理是做什么的?对话管理就是管理对话的,那么对话是什么呢?对话的最小单位就是一轮,一个 turn,我们进去看的话,一个 turn 又分为两部分,一个叫对话输入,一个叫对话输出;在输入和输出中间,有一个对话处理的过程,就像两个人互相交流一样,我问你答,但其实你在答之前是有一个思考过程的,如果你不思考就回答,那你的答案就是没有质量的,所以就会有一个中间的对话处理过程。我们把对话抽象到这种程度以后,整个平台就三个节点,一个叫触发节点,一个叫函数节点,一个叫回复节点。触发节点是和用户的对话输入对着的,函数节点是和对话处理对着的,回复节点是和对话输出对着的。有了这一层抽象以后,无论你是什么行业的什么场景,什么样的对话流程,都可以通过这三个节点通过连线把你的业务流画出来。举两个例子,先看一个简单的,你要查一个天气,很简单,先来一个触发节点,把天气流程触发起来,中间有两个函数节点,一个是调中央气象台的接口,把结果拿过来,另一个是对结果进行一次解析和封装,以一个用户可读的形式通过回复节点回复给用户。这里面稍微解释一下就是增加了一个填槽节点,填槽节点是什么意思呢?就是在任务型对话里面,几乎所有的任务都需要收集用户的信息,比如你要查天气,就需要问时间是哪一天的,地点是什么地方的,这样就叫做填槽,填槽因为太常用太普遍了,就符合我们冷启动快里面做预置的思想,所以通过三个基础节点,我们自己把它搭建成填槽的一个模板,需要填槽的时候从页面上拖一个填槽节点出来就可以了。我们再看一个复杂的场景,这是在线教育里面的一个外呼场景,家里有小孩的可能知道,这种在线教育特别火,在上课之前半小时,机器人就会主动给用户打电话,指导软件下载,指导怎么登陆,登陆进去以后怎么进入教室,所有的这些流程都可以通过机器人进行引导。通过这两个例子我们就可以看到,无论是简单还是复杂的场景,通过这三种抽象节点的连线都可以实现。有时候我们开玩笑就会说,整个这种连线就叫一生二,二生三,三生万千对话。讲了抽象以后,再看一下具体的对话管理技术。从实现上来说,这张图和大家刚才看到的语言理解那张是一模一样的,因为很多东西的分布其实是遵循着共同规律的,区别在与把意图换成了对话。举一个例子,比如像查天气这样的,如果采集十万个查天气的样本,对这些用户的说法进行一个频率统计的话,大概就是这样一个曲线,用两步能够完成的,比如说查天气,先填槽一个时间再填槽一个地点,然后返回一个结果,通过这种流程来完成的,可能有两万次;中间可能会引入一些问 A 答 B 的情况,这样的 B 可能有各种各样的,就跑到长尾上来了,这样整个对话其实也遵循一个幂律分布。对于高频确定的部分,可以用状态机进行解决,但状态机同样面临一个问题,它没有一个很好的容错能力,当问 A 答 B 的时候,机器不知道下面怎么接了。在这种情况下,需要引入一个类人能力,对状态机的能力进行补充,状态机加上类人能力以后,基本上可以把高频的对话比较好的解决了。对于长尾上的对话,目前对于整个学术界或者工业界都是一个难题,比较好的解决方式就是上线以后引入在线交互学习,不断跟用户在对话过程中学习对话。在状态机和在线交互学习之间其实是有 gap 的,因为状态机自己没有学习能力,所以需要引入增强学习。接下来我会介绍在类人能力以及增强学习方面的一些工作。先看一下类人能力。我们把人说的话,做一下分类大概可以分为三种:第一种就是用户说的话清晰明了只有一个意思,这种其实对机器来说是可理解的;第二种机器压根儿不知道在说啥,也就是 unknown 的;还有一种就是用户表达的意思可以理解,但是有歧义,有可能包含着两个意图、三个意图,就是uncertain,不确定的。确定性的,状态机其实是可以很好地捕捉和描述的,类人能力主要关注拒识的和不确定性的。对于拒识这块,比如还是在线英语的这个例子,机器人打来一个电话,问现在方不方便调试设备,这个时候从设计的角度来说希望用户回答方便或者不方便就OK了,但是一旦这个用户回答了一个比较个性化的话,比如,“呃,我刚扫完地,过会儿可能有人要来”,这时候我们的语言理解模块很难捕捉到这是什么语义,这时候需要引入一个个性化的拒识,比如说,“您好,不好意思,刚才没听明白,请问您现在是否方便调试,如果您不方便,我过会儿再给您打过来”,这个就是对话的兜底,是对 unknown 的处理。第二个我们看一下澄清,用户说的一句话里面,如果是模糊不清的怎么办?我们通过大量的数据分析发现这种模糊不清主要出现在两种情况下,一种是用户把多个意图杂糅在一段话里来表达;第二种是用户在表达一个意图之前做了很长的铺垫,对于这两种长句子现在的语言理解给出的是意图的概率分布,我们把这个概率分布放到对话管理模块以后就需要让用户进行一轮澄清。比如这个例子,这是移动领域的一个例子,这句话理解有三种意图,到底是想问花费明细,还是套餐的事情还是想问合约的低保,把这三个问题抛给用户进行澄清就可以了。从技术上来说是怎么实现的呢,我们看一下这个图,开发者负责把对话流程用流程图清晰描述出来,然后像澄清这种其实是我们系统的一种内置能力,什么时候澄清是通过下端的这两个引擎里面的能力来决定的,第一块是 Error Detection,它用来检测用户当前说的这句话是否需要触发澄清,一旦它觉得要触发澄清,就会交给下一个模块,究竟用什么样的方式澄清以及怎么生成澄清的话术,这是目前我们整个智能澄清这块做的工作。再看一下我们在增强学习方面的工作。在对话管理模型里面,经典的分成两个模块,一个是 neural belief tracker,用来做对话状态追踪的,另一个是 policy network,用来做行为决策的。在整个框架下,要去训练这个网络的时候,有两种训练方式,一种是端到端的去训练,用增强学习去训练,但这种方式一般它的收敛速度会比较慢,训练出的结果也不好;另外一种方式是先分别做预训练,这个时候用监督学习训练就好了,不用增强学习训练,训练完以后再用增强学习对监督学习预训练的模型进行调优就可以了。无论是端到端的一步训练还是先预训练再调优,只要涉及增强学习这一块,都需要有一个外部环境,所以在我们的实现架构里面,引入了模拟器的概念,就是user simulator。模拟器这主要分为三大块,一个是 user model,用来模拟人的行为的;第二个是 error model,模拟完人的行为以后经过 error model 引入一个错误扰动,用 user model 产出的只是一个概率为 1 的东西,它对网络训练是不够好的,error model 会对这个结果进行扰动并给他引进几个其他的结果,并且把概率分布进行重新计算一下,这样训练出的模型在扩展能力或者泛化能力上会更好一些;第三个模块是 reward model,用来提供 reward 值。这是我们今天在整个增强学习的对话管理这块的一些工作。最后看一下函数计算。函数计算是什么东西呢?还是举一个例子吧,比如说,10086 里面用户说要查一下话费,10086 那边的机器人就会回复一句是发短信还是播放语音,表面看来就是简单的一入一出,其实在这背后要经过多轮的服务查询,才能完成这个结果,因为当要查话费的时候,先要经过函数计算查一下现在是哪一天,如果是下账期的话是不能查话费的,就是每个月的最后一天不能查话费,如果可以查话费的话,先看一下用户是否存在话费,如果存在花费的话第三步调用的服务看是不是停机了,因为停机了的话只能语音播报不能接收短信。所以看一下在一个简单的一入一出的对话背后,是走了一个复杂的流程的,这些流程今天都是在机器端用代码来实现的。函数计算的引入,使对话工厂可以去处理复杂的任务。业务应用最后我们看一下对话工厂的业务应用情况。这是我们在浙江上线的 114 移车,当有市民举报违规停车挡路后,就会自动打一个电话让他移车。第二个是在金融领域里面关于贷款催收的例子。在刚刚过去的双十一里面,对话工厂在整个电商里面也有大量应用,主要是在店小蜜和阿里小蜜里面。店小蜜主要是一些开发票、催发货、改地址这样的流程,这里是一个开发票的例子,用户可能会先说一个开发票,进来以后要进行复杂的流程,一种是在说的时候其实他已经把它的订单号送进来了,如果没有说订单号的话需要去后台系统查订单号,查出来以后弹一个订单选择器选择订单,接下来如果是个人发票就走这个流程,如果是公司发票走另一个流程,接下来会问是普通发票还是增值税发票,如果是普通发票接着往这儿走,如果是增值税发票需要获取企业增值税的税号,最后汇总到一个节点,调用后台开发票的系统,把发票开出来。这是这次双十一里面用到的开发票的一个例子。最后看一下我们整体的落地情况。整个对话工厂在店小蜜里面主要是做像开发票这样的售后流程的处理。在云小蜜,公有云是一大块;私有云现在有很多家客户了,主要有银行、电信运营商还有金融等;钉钉是我们另一个重要的端,钉钉上也有几百万的企业;内外小蜜是我们集团用小蜜实现的一个办公助理;另外两个巨大的客户,一个是浙江省的政务,第二个是中国移动,这也是是云小蜜的业务。阿里小蜜主要是负责阿里巴巴集团内部各个 BU 的业务,手淘是一个最大的业务,进入手机淘宝以后,进入“我的”里面有一个客服小蜜,就是阿里小蜜;上个月我们刚刚在优酷上线了优酷小蜜,星巴克是 9 月份上的,是属于新零售的一个最大的尝试点,还有很多其他的场景。总结小蜜智能对话开发平台,即对话工厂(Dialog Studio),以对话为中心来设计,使得对话开发者能够看到全局视野,可以去做复杂的任务,可以去做无限的场景。同时,作为一个平台性产品,为了能够实现低门槛、适用于各行各业以及具备效果持续提升能力,在整个设计实现中,遵循冷启动快、灵活定制、鲁棒进化三大原则。技术方面,我们在语言理解、对话管理、迁移学习、增强学习以及模仿学习等各方面做了深入探索,部分成果做到了学术界state-of-the-art。业务方面,对话工厂在小蜜家族的大量业务中落地应用,带来了良好的业务价值。对话工厂,“让人和机器自由对话”!本文作者:李永彬阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 15, 2019 · 1 min · jiezi

2018最佳GAN论文回顾(上)

摘要: 受Reddit网站上讨论区的启发,我决定快速地浏览一下2018年关于GAN最有趣的文章。我很高兴今年参加了一个研究项目,这要求我必须熟悉大量用于计算机视觉方面的深度学习领域的资料。我对过去两、三年内取得的进展感到惊讶,这真的非常令人兴奋和鼓舞,所有不同的子领域,如图像修复、对抗性样本、超分辨率或是三维重建,都大大得益于近期的发展。然而,有一种神经网络,它受到了大量的宣传和炒作 — 生成性对抗网络(Generative Adversarial Networks,GANs)。我也认为这种模型是非常吸引人的,并且我也一直在寻找一些GAN的新思路。受Reddit网站上讨论区的启发,我决定快速地浏览一下2018年关于GAN最有趣的文章。这份名单非常的主观 — 我选择的研究论文不仅是最高水平的,而且也都非常的有趣。在第一章中,我将讨论其中的三篇。顺便说一下,如果你对以前的GAN论文感兴趣,这一篇文章可能会有所帮助,作者在文中提到的一篇论文排在了我的名单上的第一位。1.GAN解析:可视化和理解生成性对抗网络 — 考虑到GAN的大肆宣传,很明显这项技术迟早会被商业化应用。然而,因为我们对其内部机制了解的不多,所以我认为要生产一个可靠的产品仍然很困难。不过这项工作仍然向未来迈出了巨大的一步,在未来我们能够真正控制GAN。因此,一定要看看他们伟大的交互演示,结果是令人震惊的;2.一种用于生成性对抗网络的基于生成器体系结构 – NVIDIA(英伟达)的研究团队会定期地提出一些具有开创性的概念(2018年的关于图像修复的论文,近期的用神经网络进行图形绘制的演示)。这篇论文也不例外,加上显示结果的视频就更有吸引力了;3.进化生成性对抗网络 — 这是一个真正简单易懂的文章。进化算法和GAN一起 — 这肯定很有趣;GAN解析: 可视化和理解生成性对抗网络(GAN Dissection: Visualizing and Understanding Generative Adversarial Networks)详解该论文已于2018年11月26日提交。作者以交互式演示的方式创建了一个非常不错的项目网站。主要思想:GAN无疑证明了深度神经网络的强大。机器学习生成令人震惊的、高分辨率图像的方式是非常美妙的,就仿佛它像我们一样了解这个世界。但是,和其它的那些出色的统计模型一样,GAN最大的缺陷是缺乏可解释性。这项研究为理解GAN迈出了非常重要的一步。它允许我们在生成器中找到“负责”生成某些属于class c的对象单元。作者们声称,我们可以检查生成器的一个层,并找到导致在生成图像中形成c对象的单元子集。作者们通过两个步骤:解剖和干预,为每个类寻找一组“因果”单元。另外,这可能是第一项工作,为了解GAN的内部机制提供了系统的分析。方法:生成器G可以被看作是从潜在的向量z到一个生成的图像x=G(z)的映射。我们的目标是理解参数r,一种内部的表示,它是生成器G的特定层的输出。x=G(z)=f(r)关于c类的对象,我们想仔细看下参数r。我们知道参数r包含关于一个这些特定对象生成的编码信息。我们的目标是了解这个信息是如何在内部编码的。作者们声称有一种方法可以从参数r中提取这些单元,而r负责生成类c的对象。这里,是特定层中所有单元的集合,参数U是目标单元,参数P是像素位置。问题来了,如何进行这种分离?作者们提出了两个步骤,这两个步骤是理解GAN黑盒子的工具。就是解析和干预。解析 — 我们要识别那些有趣的类,它们在r中有一个明确的表示方法。这基本上是通过比较两个图像来完成的。首先通过计算x获得第一个图像,然后通过语义分割网络来运行。这将返回与目标类别(例如:树木)相对应的像素位置。第二个图像是通过用ru,p进行上采样,因此它与sc(x)的维度相匹配,然后再对其进行阈值处理,以便对被这个特定单元所“发亮”的像素做出艰难的决定。最后,我们计算了两个输出之间的空间一致性。值越高,单元u对类c的因果效应就越大。通过对每个单元执行这个操作,我们最终应该找出哪些类在r的结构中有一个明确的表示方法。干预 — 在这一点上,我们已经确定了相关的类。现在,我们试图为每个类找到最好的分离方式。这意味着,一方面我们抑制非受迫单元,希望目标类将从生成的图像上消失。另一方面,我们扩大了因果单元对生成图像的影响。这样我们就可以了解到他们对目标类c的存在有多大的贡献。最后,我们从两个图像中分割出类c并进行对比。语义图之间的一致性越小越好。这意味着在一个图像上,我们完全“排除”了树木的影响,而第二个图像只包含一片树林。结果:a)Progressive GAN生成的教堂图像 b)根据所给的预训练的Progressive GAN,我们确定了负责生成“树”类的单元 c)我们可以阻止那些单元“删除”图像中的树 d)扩大图像中树的密度。上述结果表明,我们对网络内部的机制有了很好的理解。这些见解可以帮助我们改善网络行为。了解图像的哪些特征来自于神经网络的哪个部分,对于理解说明、商业应用和进一步的研究都是非常有价值的。a)出于调试的目的,我们可以确定那些有伪影的单元……,b)和c)把它们去掉了,以“修复”GAN。一个可以解决的问题是在生成的图像中有看得见的伪影。即使是一个训练很好的GAN有时也能产生一个极其不现实的图像,而这些错误的原因以前是未知的。现在我们可以将这些错误与导致视觉伪影的神经元联系起来。通过识别和阻止这些单元,可以提高生成的图像质量。通过将某些单元设置为固定的平均值(例如,门),我们可以确保门将出现在图像中的某个位置。当然,这不会违反学过的分布统计(我们不能强迫门出现在空中)。另一个限制来自于这样一个事实,即一些对象与某些位置之间的联系是非常的紧密,以至于无法将它们从图像中消除。举个例子:不能简单地把椅子从会议室里删除掉,那样只会降低它们像素的密度或尺寸。一种用于生成性对抗网络的基于生成器体系结构(A Style-Based Generator Architecture for Generative Adversarial Networks详述该论文已于2018年12月12日提交,代码很快就将会发布。另外,对于那些想更多了解这种方法但并不想阅读论文的人来说,博客上发表了一篇很好的总结文章。主要思想:这项工作提出了关于GAN框架的另一个观点。更具体地说,它从样式转换设计中吸取灵感,创建了一个生成器架构,在生成的图像中可以学习高级属性(如年龄、在人脸或背景上训练时的身份、相机视角)和随机变化(雀斑、头发细节)。它不仅学习自动分离这些属性,而且还允许我们以非常直观的方式控制合成。方法:传统的GAN架构(左)与基于样式的生成器(右)。在新的框架中,我们有两个网络组件:映射网络f与综合网络g。前者将一个潜在的代码映射到一个中间的潜在空间W,W对样式的信息进行编码。后者利用生成的样式和高斯噪声来创建新的图像。块“A”是一个训练过的仿射转换,而块“B”将训练过的每个通道的比例因子应用于噪声的输入。在经典的GAN方法中,生成器以一些潜在的代码作为输入,并输出一个图像,这属于它在训练阶段所学习到的分布。作者们通过创建一个基于样式的、由两个元素组成的生成器来背离这种设计:1.一个全连接的网络,代表着非线性映射 f:Z→W;2.一个综合网络g;全连接的网络 — 通过变换一个标准化的潜在向量z∈Z,我们得到了一个中间的潜在向量w=f(z)。中间的潜在空间W有效地控制了生成器的样式。作为旁注,作者确保避免从W的低密度区域采样。虽然这可能造成w的变化损失,但据说最终会导致更好的平均的图像质量。现在,一个从中间的潜在空间采样的潜在向量w被输入到块“A”(训练的仿射变换)中,并转换成样式y=(ys,yb)。最后通过每个卷积层的自适应实例标准化(adaptive instance normalization,AdaIN)将该风格添加到合成网络中。AdaIN操作是这样定义的:合成网络 — AdaIN的操作通过对其进行标准化来改变每个特征图xi,然后使用来自样式y的分量进行比例缩放和移位。最后,生成器的特征映射也被提供了一个直接的方式来生成随机细节 — 显式的噪声输入 — 以包含不相关高斯噪声的单通道图像的形式。综上所述,虽然显式的噪声输入可以被视为在合成网络中生成过程的“种子”,但从W抽取的潜在代码试图向图像添加某种样式。结果:作者们从2017年的Progressive GAN开始重新审视NVIDIA的架构。虽然他们掌握了大部分的架构和超参数,但是生成器正在根据新的设计进行“升级”。论文内容最令人印象深刻的特点是样式的混合。上图是可视化样式混合的效果。通过让一个潜在的代码(来源)生成一个图像,我们可以覆盖另一个图像(目标)的特征子集。这里,我们覆盖对应于粗糙空间分辨率(低分辨率特征图)的层。这样我们就可以影响目标图像的高级特征了。这种新奇的生成器结构使其有能力在合成网络的不同层向同一图像添加不同的样式。在训练过程中,我们通过映射网络运行两个潜在代码z1和z2,并接收相应的w1和w2两个向量。完全由z1生成的图像被称为目标。这是一个生成的高分辨率图像,几乎与实际的分布区区分不出来。仅通过添加z2而生成的图像被称为来源。现在,在使用z1生成目标图像的过程中,在某些层,我们可以添加z2的代码了。此操作将用那些来源来覆盖目标中存在的样式子集。来源对目标的影响是由层的位置来控制的,这些层正被来源的潜在代码进行“培育”。与特定层对应的分辨率越低,来源对目标的影响越大。这样,我们就可以决定要在多大程度上影响目标图像:粗糙空间分辨率(分辨率42−82) — 高级方面,如:发型、眼镜或年龄;中间样式分辨率(分辨率162−322) — 较小比例的面部特征,如:发型样式的细节、眼睛;精细分辨率(分辨率642−10242)—只需修改一些小细节,如:头发颜色、肤色色调或皮肤结构;作者们将他们的方法进一步应用到汽车、卧室甚至是猫的图像中,得到了令人震惊的结果。我仍然困惑为什么网络的决定会影响到猫的图像中爪子的位置,而不会关心汽车图像中车轮的转动……我发现真正令人惊奇的是,这个框架可以进一步应用于不同的数据集,比如汽车和卧室的图像。进化生成性对抗网络(Evolutionary Generative Adversarial Networks)细节该论文已于2018年3月1日提交。主要思想:在传统设置中,GAN通过交替更新生成器和使用反向传播的识别器进行训练。利用在目标函数中的交叉熵机制,实现了双人minmax 游戏。E-GAN的作者们提出了一种基于进化算法的可替代GAN框架。他们以进化问题的形式重新声明了损失函数。生成器的任务是在识别器的影响下承受不断地突变。根据“适者生存”的原则,我们希望最新一代生成器以这样的方式“进化”,从而学会正确的训练样本分布。方法:原始的GAN框架(左)与E-GAN框架(右)。在E-GAN框架中,全部的G生成器在一个动态环境中进化 — 即识别器D。该算法涉及三个阶段:变化、评估和筛选。最好的子版本被保留下来以供下一次迭代的时候使用。进化算法试图在一个给定的环境(这里是指识别器)中进化全部的生成器。生成器中的每个个体都代表了生成网络参数空间中的一个可能的解决方案。进化过程归结为三个步骤:1.变化:通过根据一些突变属性而自我修改,生成器的单个G生成其子级[图片上传失败…(image-5fce9b-1547434233756)] …;2.评估:每个子级都将使用一个适应函数进行评估,该函数取决于识别器的当前状态;3.筛选:我们对每个子级进行评估,并决定它在适应函数的方面是否足够好,如果是,它将被保留,否则就会被丢弃;上述这些步骤涉及到两个应该被详细讨论的概念:突变和适应函数:突变 — 这些是在“变化”步骤中给子级引入的改变。最初的GAN训练目标激发了他们的灵感。作者们区分了三种最有效的突变类型。它们是minmax突变(鼓励将Jensen-Shannon分歧最小化)、启发式突变(添加了反向Kullback-Leibler分歧项)和最小二乘突变(受LSGAN的启发);适应函数— 在进化算法中,一个适应函数告诉我们一个给定的子级离达到设定的目标有多接近。这里,适应函数包括两个要素:质量适应得分和多样性适应得分。前者确保了生成器能够提供欺骗识别器的输出,而后者则关注生成样本的多样性。因此,一方面,培育子版本不仅要很好地接近原始分布,而且还要保持多样性,并且避免模式崩溃的陷阱。作者们声称他们的方法解决了多个众所周知的问题。E-GAN不仅在稳定性和抑制模式崩溃方面做得更好,还减轻了选择超参数和架构(对收敛至关重要)的压力。最后,作者们声称E-GAN比传统的GAN框架收敛得更快。结果:该算法不仅对合成数据进行了测试,而且还对CIFAR-10的数据集和Inception进行了测试。作者们修改了流行的GAN方法,如DCGAN,并在实际的数据集上对其进行了测试。结果表明,通过训练E-GAN,可以从目标数据分布中生成各种高质量的图像。根据作者们的想法,在每一个筛选步骤中只保留一个子级就足以成功地将参数空间遍历到最优的解决方案。我发现E-GAN的这个属性非常有趣。另外,通过对空间连续性的仔细检查,我们可以发现,E-GAN的确从潜在的噪声空间到图像空间学习了一种有意义的预测。通过在潜在向量之间进行插值,我们可以获得平稳地改变有语义意义的人脸属性的生成图像。在潜在空间中线性地插值。生成器已经从CelebA数据集中学习了图像的分布。=0.0对应着从向量z1生成一个图像,而=1.0则意味着图像来自向量z2。通过改变alpha的取值,我们可以在潜在的空间内进行插值,效果非常好。本文中所有的数据都来自于我的博客上面发布的文章。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 14, 2019 · 1 min · jiezi

路径规划之 A* 算法

算法介绍A*(念做:A Star)算法是一种很常用的路径查找和图形遍历算法。它有较好的性能和准确度。本文在讲解算法的同时也会提供Python语言的代码实现,并会借助matplotlib库动态的展示算法的运算过程。A算法最初发表于1968年,由Stanford研究院的Peter Hart, Nils Nilsson以及Bertram Raphael发表。它可以被认为是Dijkstra算法的扩展。由于借助启发函数的引导,A算法通常拥有更好的性能。广度优先搜索为了更好的理解A算法,我们首先从广度优先(Breadth First)算法讲起。正如其名称所示,广度优先搜索以广度做为优先级进行搜索。从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点。这种算法就像洪水(Flood fill)一样向外扩张,算法的过程如下图所示:在上面这幅动态图中,算法遍历了图中所有的点,这通常没有必要。对于有明确终点的问题来说,一旦到达终点便可以提前终止算法,下面这幅图对比了这种情况:在执行算法的过程中,每个点需要记录达到该点的前一个点的位置 – 可以称之为父节点。这样做之后,一旦到达终点,便可以从终点开始,反过来顺着父节点的顺序找到起点,由此就构成了一条路径。Dijkstra算法Dijkstra算法是由计算机科学家Edsger W. Dijkstra在1956年提出的。Dijkstra算法用来寻找图形中节点之间的最短路径。考虑这样一种场景,在一些情况下,图形中相邻节点之间的移动代价并不相等。例如,游戏中的一幅图,既有平地也有山脉,那么游戏中的角色在平地和山脉中移动的速度通常是不相等的。在Dijkstra算法中,需要计算每一个节点距离起点的总移动代价。同时,还需要一个优先队列结构。对于所有待遍历的节点,放入优先队列中会按照代价进行排序。在算法运行的过程中,每次都从优先队列中选出代价最小的作为下一个遍历的节点。直到到达终点为止。下面对比了不考虑节点移动代价差异的广度优先搜索与考虑移动代价的Dijkstra算法的运算结果:当图形为网格图,并且每个节点之间的移动代价是相等的,那么Dijkstra算法将和广度优先算法变得一样。最佳优先搜索在一些情况下,如果我们可以预先计算出每个节点到终点的距离,则我们可以利用这个信息更快的到达终点。其原理也很简单。与Dijkstra算法类似,我们也使用一个优先队列,但此时以每个节点到达终点的距离作为优先级,每次始终选取到终点移动代价最小(离终点最近)的节点作为下一个遍历的节点。这种算法称之为最佳优先(Best First)算法。这样做可以大大加快路径的搜索速度,如下图所示:但这种算法会不会有什么缺点呢?答案是肯定的。因为,如果起点和终点之间存在障碍物,则最佳优先算法找到的很可能不是最短路径,下图描述了这种情况。A算法对比了上面几种算法,最后终于可以讲解本文的重点:A算法了。下面的描述我们将看到,A算法实际上是综合上面这些算法的特点于一身的。A算法通过下面这个函数来计算每个节点的优先级。其中:f(n)是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。g(n) 是节点n距离起点的代价。h(n)是节点n距离终点的预计代价,这也就是A算法的启发函数。关于启发函数我们在下面详细讲解。A算法在运算过程中,每次从优先队列中选取f(n)值最小(优先级最高)的节点作为下一个待遍历的节点。另外,A算法使用两个集合来表示待遍历的节点,与已经遍历过的节点,这通常称之为open_set和close_set。完整的A算法描述如下: 初始化open_set和close_set;* 将起点加入open_set中,并设置优先级为0(优先级最高);* 如果open_set不为空,则从open_set中选取优先级最高的节点n: * 如果节点n为终点,则: * 从终点开始逐步追踪parent节点,一直达到起点; * 返回找到的结果路径,算法结束; * 如果节点n不是终点,则: * 将节点n从open_set中删除,并加入close_set中; * 遍历节点n所有的邻近节点: * 如果邻近节点m在close_set中,则: * 跳过,选取下一个邻近节点 * 如果邻近节点m也不在open_set中,则: * 设置节点m的parent为节点n * 计算节点m的优先级 * 将节点m加入open_set中启发函数上面已经提到,启发函数会影响A算法的行为。在极端情况下,当启发函数h(n)始终为0,则将由g(n)决定节点的优先级,此时算法就退化成了Dijkstra算法。如果h(n)始终小于等于节点n到终点的代价,则A算法保证一定能够找到最短路径。但是当h(n)的值越小,算法将遍历越多的节点,也就导致算法越慢。如果h(n)完全等于节点n到终点的代价,则A算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,我们很难确切算出距离终点还有多远。如果h(n)的值比节点n到终点的代价要大,则A算法不能保证找到最短路径,不过此时会很快。在另外一个极端情况下,如果h()n相较于g(n)大很多,则此时只有h(n)产生效果,这也就变成了最佳优先搜索。由上面这些信息我们可以知道,通过调节启发函数我们可以控制算法的速度和精确度。因为在一些情况,我们可能未必需要最短路径,而是希望能够尽快找到一个路径即可。这也是A算法比较灵活的地方。对于网格形式的图,有以下这些启发函数可以使用:如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离(Manhattan distance)。如果图形中允许朝八个方向移动,则可以使用对角距离。如果图形中允许朝任何方向移动,则可以使用欧几里得距离(Euclidean distance)。关于距离曼哈顿距离如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:计算曼哈顿距离的函数如下,这里的D是指两个相邻节点之间的移动代价,通常是一个固定的常数。function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return D * (dx + dy)对角距离如果图形中允许斜着朝邻近的节点移动,则启发函数可以使用对角距离。它的计算方法如下:计算对角距离的函数如下,这里的D2指的是两个斜着相邻节点之间的移动代价。如果所有节点都正方形,则其值就是function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy)欧几里得距离如果图形中允许朝任意方向移动,则可以使用欧几里得距离。欧几里得距离是指两个节点之间的直线距离,因此其计算方法也是我们比较熟悉的:其函数表示如下:function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return D * sqrt(dx * dx + dy * dy)算法实现虽然前面介绍了很多内容,但实际上A算法并不复杂,实现起来也比较简单。下面我们给出一个Python语言的代码示例。之所以使用Python语言是因为我们可以借助matplotlib库很方便的将结果展示出来。在理解了算法之后,通过其他语言实现也并非难事。算法的源码可以到我的github上下载:paulQuei/a-star-algorithm。我们的算法演示的是在一个二维的网格图形上从起点找寻终点的求解过程。坐标点与地图首先,我们创建一个非常简单的类来描述图中的点,相关代码如下:# point.pyimport sysclass Point: def init(self, x, y): self.x = x self.y = y self.cost = sys.maxsize接着,我们实现一个描述地图结构的类。为了简化算法的描述:我们选定左下角坐标[0, 0]的点是算法起点,右上角坐标[size - 1, size - 1]的点为要找的终点。为了让算法更有趣,我们在地图的中间设置了一个障碍,并且地图中还会包含一些随机的障碍。该类的代码如下:# random_map.pyimport numpy as npimport pointclass RandomMap: def init(self, size=50): ① self.size = size self.obstacle = size//8 ② self.GenerateObstacle() ③ def GenerateObstacle(self): self.obstacle_point = [] self.obstacle_point.append(point.Point(self.size//2, self.size//2)) self.obstacle_point.append(point.Point(self.size//2, self.size//2-1)) # Generate an obstacle in the middle for i in range(self.size//2-4, self.size//2): ④ self.obstacle_point.append(point.Point(i, self.size-i)) self.obstacle_point.append(point.Point(i, self.size-i-1)) self.obstacle_point.append(point.Point(self.size-i, i)) self.obstacle_point.append(point.Point(self.size-i, i-1)) for i in range(self.obstacle-1): ⑤ x = np.random.randint(0, self.size) y = np.random.randint(0, self.size) self.obstacle_point.append(point.Point(x, y)) if (np.random.rand() > 0.5): # Random boolean ⑥ for l in range(self.size//4): self.obstacle_point.append(point.Point(x, y+l)) pass else: for l in range(self.size//4): self.obstacle_point.append(point.Point(x+l, y)) pass def IsObstacle(self, i ,j): ⑦ for p in self.obstacle_point: if i==p.x and j==p.y: return True return False这段代码说明如下:构造函数,地图的默认大小是50x50;设置障碍物的数量为地图大小除以8;调用GenerateObstacle生成随机障碍物;在地图的中间生成一个斜着的障碍物;随机生成其他几个障碍物;障碍物的方向也是随机的;定义一个方法来判断某个节点是否是障碍物;算法主体有了基本的数据结构之后,我们就可以开始实现算法主体了。这里我们通过一个类来封装我们的算法。首先实现一些算法需要的基本函数,它们如下:# a_star.pyimport sysimport timeimport numpy as npfrom matplotlib.patches import Rectangleimport pointimport random_mapclass AStar: def init(self, map): self.map=map self.open_set = [] self.close_set = [] def BaseCost(self, p): x_dis = p.x y_dis = p.y # Distance to start point return x_dis + y_dis + (np.sqrt(2) - 2) * min(x_dis, y_dis) def HeuristicCost(self, p): x_dis = self.map.size - 1 - p.x y_dis = self.map.size - 1 - p.y # Distance to end point return x_dis + y_dis + (np.sqrt(2) - 2) * min(x_dis, y_dis) def TotalCost(self, p): return self.BaseCost(p) + self.HeuristicCost(p) def IsValidPoint(self, x, y): if x < 0 or y < 0: return False if x >= self.map.size or y >= self.map.size: return False return not self.map.IsObstacle(x, y) def IsInPointList(self, p, point_list): for point in point_list: if point.x == p.x and point.y == p.y: return True return False def IsInOpenList(self, p): return self.IsInPointList(p, self.open_set) def IsInCloseList(self, p): return self.IsInPointList(p, self.close_set) def IsStartPoint(self, p): return p.x == 0 and p.y ==0 def IsEndPoint(self, p): return p.x == self.map.size-1 and p.y == self.map.size-1这里的函数说明如下:init:类的构造函数。BaseCost:节点到起点的移动代价,对应了上文的g(n)HeuristicCost:节点到终点的启发函数,对应上文的h(n)。由于我们是基于网格的图形,所以这个函数和上一个函数用的是对角距离。TotalCost:代价总和,即对应上面提到的f(n)。IsValidPoint:判断点是否有效,不在地图内部或者障碍物所在点都是无效的。IsInPointList:判断点是否在某个集合中。IsInOpenList:判断点是否在open_set中。IsInCloseList:判断点是否在close_set中。IsStartPoint:判断点是否是起点。IsEndPoint:判断点是否是终点。有了上面这些辅助函数,就可以开始实现算法主逻辑了,相关代码如下:# a_star.pydef RunAndSaveImage(self, ax, plt): start_time = time.time() start_point = point.Point(0, 0) start_point.cost = 0 self.open_set.append(start_point) while True: index = self.SelectPointInOpenList() if index < 0: print(‘No path found, algorithm failed!!!’) return p = self.open_set[index] rec = Rectangle((p.x, p.y), 1, 1, color=‘c’) ax.add_patch(rec) self.SaveImage(plt) if self.IsEndPoint(p): return self.BuildPath(p, ax, plt, start_time) del self.open_set[index] self.close_set.append(p) # Process all neighbors x = p.x y = p.y self.ProcessPoint(x-1, y+1, p) self.ProcessPoint(x-1, y, p) self.ProcessPoint(x-1, y-1, p) self.ProcessPoint(x, y-1, p) self.ProcessPoint(x+1, y-1, p) self.ProcessPoint(x+1, y, p) self.ProcessPoint(x+1, y+1, p) self.ProcessPoint(x, y+1, p)这段代码应该不需要太多解释了,它就是根据前面的算法逻辑进行实现。为了将结果展示出来,我们在算法进行的每一步,都会借助于matplotlib库将状态保存成图片。上面这个函数调用了其他几个函数代码如下:# a_star.pydef SaveImage(self, plt): millis = int(round(time.time() * 1000)) filename = ‘./’ + str(millis) + ‘.png’ plt.savefig(filename)def ProcessPoint(self, x, y, parent): if not self.IsValidPoint(x, y): return # Do nothing for invalid point p = point.Point(x, y) if self.IsInCloseList(p): return # Do nothing for visited point print(‘Process Point [’, p.x, ‘,’, p.y, ‘]’, ‘, cost: ‘, p.cost) if not self.IsInOpenList(p): p.parent = parent p.cost = self.TotalCost(p) self.open_set.append(p)def SelectPointInOpenList(self): index = 0 selected_index = -1 min_cost = sys.maxsize for p in self.open_set: cost = self.TotalCost(p) if cost < min_cost: min_cost = cost selected_index = index index += 1 return selected_indexdef BuildPath(self, p, ax, plt, start_time): path = [] while True: path.insert(0, p) # Insert first if self.IsStartPoint(p): break else: p = p.parent for p in path: rec = Rectangle((p.x, p.y), 1, 1, color=‘g’) ax.add_patch(rec) plt.draw() self.SaveImage(plt) end_time = time.time() print(’===== Algorithm finish in’, int(end_time-start_time), ’ seconds’)这三个函数应该是比较容易理解的:SaveImage:将当前状态保存到图片中,图片以当前时间命名。ProcessPoint:针对每一个节点进行处理:如果是没有处理过的节点,则计算优先级设置父节点,并且添加到open_set中。SelectPointInOpenList:从open_set中找到优先级最高的节点,返回其索引。BuildPath:从终点往回沿着parent构造结果路径。然后从起点开始绘制结果,结果使用绿色方块,每次绘制一步便保存一个图片。测试入口最后是程序的入口逻辑,使用上面写的类来查找路径:# main.pyimport numpy as npimport matplotlib.pyplot as pltfrom matplotlib.patches import Rectangleimport random_mapimport a_starplt.figure(figsize=(5, 5))map = random_map.RandomMap() ①ax = plt.gca()ax.set_xlim([0, map.size]) ②ax.set_ylim([0, map.size])for i in range(map.size): ③ for j in range(map.size): if map.IsObstacle(i,j): rec = Rectangle((i, j), width=1, height=1, color=‘gray’) ax.add_patch(rec) else: rec = Rectangle((i, j), width=1, height=1, edgecolor=‘gray’, facecolor=‘w’) ax.add_patch(rec)rec = Rectangle((0, 0), width = 1, height = 1, facecolor=‘b’)ax.add_patch(rec) ④rec = Rectangle((map.size-1, map.size-1), width = 1, height = 1, facecolor=‘r’)ax.add_patch(rec) ⑤plt.axis(’equal’) ⑥plt.axis(‘off’)plt.tight_layout()#plt.show()a_star = a_star.AStar(map)a_star.RunAndSaveImage(ax, plt) ⑦这段代码说明如下:创建一个随机地图;设置图像的内容与地图大小一致;绘制地图:对于障碍物绘制一个灰色的方块,其他区域绘制一个白色的的方块;绘制起点为蓝色方块;绘制终点为红色方块;设置图像的坐标轴比例相等并且隐藏坐标轴;调用算法来查找路径;由于我们的地图是随机的,所以每次运行的结果可能会不一样,下面是我的电脑上某次运行的结果:如果感兴趣这篇文章中的动图是如何制作的,请看我的另外一篇文章:使用Matplotlib绘制3D图形 - 制作动图。算法变种A算法有不少的变种,这里我们介绍最主要的几个。更多的内容请以访问维基百科:A Variants。ARAARA 全称是Anytime Repairing A,也称为Anytime A。与其他Anytime算法一样,它具有灵活的时间成本,即使在它结束之前被中断,也可以返回路径查找或图形遍历问题的有效解决方案。方法是在逐步优化之前生成快速,非最优的结果。在现实世界的规划问题中,问题的解决时间往往是有限的。与时间相关的规划者对这种情况都会比较熟悉:他们能够快速找到可行的解决方案,然后不断努力改进,直到时间用完为止。启发式搜索ARA算法,它根据可用的搜索时间调整其性能边界。它首先使用松散边界快速找到次优解,然后在时间允许的情况下逐渐收紧边界。如果有足够的时间,它会找到可证明的最佳解决方方案。在改进其约束的同时,ARA重复使用以前的搜索工作,因此,比其他随时搜索方法更有效。与A算法不同,Anytime A算法最重要的功能是,它们可以被停止,然后可以随时重启。该方法使用控制管理器类来处理时间限制以及停止和重新启动A算法以找到初始的,可能是次优的解决方案,然后继续搜索改进的解决方案,直到达到可证明的最佳解决方案。关于ARA的更多内容可以阅读这篇论文:ARA - Anytime A with Provable Bounds on Sub-Optimality。DD是Dynamic A的简写,其算法和A类似,不同的是,其代价的计算在算法运行过程中可能会发生变化。D包含了下面三种增量搜索算法:原始的D由Anthony Stentz发表。Focussed D由Anthony Stentz发表,是一个增量启发式搜索算法,结合了A和原始D的思想。D Lite是由Sven Koenig和Maxim Likhachev基于LPA构建的算法。所有三种搜索算法都解决了相同的基于假设的路径规划问题,包括使用自由空间假设进行规划。在这些环境中,机器人必须导航到未知地形中的给定目标坐标。它假设地形的未知部分(例如:它不包含障碍物),并在这些假设下找到从当前坐标到目标坐标的最短路径。然后机器人沿着路径行进。当它观察到新的地图信息(例如以前未知的障碍物)时,它会将信息添加到其地图中,并在必要时将新的最短路径从其当前坐标重新添加到给定的目标坐标。它会重复该过程,直到达到目标坐标或确定无法达到目标坐标。在穿越未知地形时,可能经常发现新的障碍,因此重新计划需要很快。增量(启发式)搜索算法通过使用先前问题的经验来加速搜索当前问题,从而加速搜索类似搜索问题的序列。假设目标坐标没有改变,则所有三种搜索算法都比重复的A搜索更有效。D及其变体已广泛用于移动机器人和自动车辆导航。当前系统通常基于D Lite而不是原始D或Focussed D。关于D的更多内容可以阅读这两篇文章:Project “Fast Replanning (Incremental Heuristic Search)“Real-Time Replanning in Dynamic and Unknown EnvironmentsField DField D扩展了D和D* Lite,是一种基于插值( interpolation-based )的规划算法,它使用线性插值来有效地生成低成本路径,从而消除不必要的转向。在给定线性插值假设的情况下,路径是最优的,并且在实践中非常有效。该算法目前被各种现场机器人系统使用。关于Field D的详细内容可以看下面这篇论文:Field D: An Interpolation-based Path Planner and ReplannerBlock ABlock A扩展自A,但它操作是一块(block)单元而不是单个单元。其open_set中的每个条目都是已到达但尚未扩展的块,或者需要重新扩展的块。open_set中块的优先级称为其堆值(heap value)。与A类似,Block A中的基本循环是删除具有最低堆值的条目并将其展开。在扩展期间使用LDDB来计算正在扩展的块中的边界单元的g值。LDDB是一种新型数据库,它包含了本地邻域边界点之间的距离。关于Block A的更多内容可以看下面这篇论文:Block A*: Database-Driven Search with Applications in Any-angle Path-Planning参考资料与推荐读物Stanford: Introduction to AWikipedia: A search algorithmPythonRobotics: A* algorithmARA - Anytime A with Provable Bounds on Sub-Optimality本文作者:paulquei阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 10, 2019 · 4 min · jiezi

用Python玩转时序数据

摘要: 本文简要介绍了如何从零开始使用Python中的时间序列。这包括对时间序列的简单定义,以及对利用pandas访问伦敦市居民智能电表所获取数据的处理。时间序列是日常生活中最常见的数据类型之一。股票价格、销售信息、气候数据、能源使用,甚至个人身高体重都是可以用来定期收集的数据样本。几乎每个数据科学家在工作中都会遇到时间序列,能够有效地处理这些数据是数据科学领域之中的一项非常重要的技能。本文简要介绍了如何从零开始使用Python中的时间序列。这包括对时间序列的简单定义,以及对利用pandas访问伦敦市居民智能电表所获取数据的处理。可以点击此处获取本文中所使用的数据。还提供了一些我认为有用的代码。让我们从基础开始,时间序列的定义是这样的:时间序列是按时间的顺序进行索引、排列或者绘制的数据点的集合。最常见的定义是,一个时间序列是在连续的相同间隔的时间点上取得的序列,因此它是一个离散时间数据的序列。时间序列数据是围绕相对确定的时间戳而组织的。因此,与随机样本相比,可能包含我们将要尝试提取的一些相关信息。加载和控制时间序列数据集让我们使用一些关于能源消耗计费的数据作为例子,以kWh(每半小时)为单位, 在2011年11月至2014年2月期间,对参与英国电力网络领导的低碳伦敦项目的伦敦居民样本数据进行分析。我们可以从绘制一些图表开始,最好了解一下样本的结构和范围,这也将允许我们寻找最终需要纠正的缺失值。对于本文的其余部分,我们只关注DateTime和kWh两列。重采样让我们从较简单的重采样技术开始。重采样涉及到更改时间序列观测的频率。特征工程可能是你对重新采样时间序列数据感兴趣的一个原因。实际上,它可以用来为监督学习模型提供额外的架构或者是对学习问题的领会角度。pandas中的重采样方法与GroupBy方法相似,因为你基本上是按照特定时间间隔进行分组的。然后指定一种方法来重新采样。让我们通过一些例子来把重采样技术描述的更具体些。我们从每周的总结开始:data.resample()方法将用于对DataFrame的kWh列数据重新取样;“W”表示我们要按每周重新取样;sum()方法用于表示在此时间段计算kWh列的总和;我们可以对每日的数据也这么做处理,并且可以使用groupby和mean函数进行按小时处理:为了进一步进行重新采样,pandas有许多内置的选项,你甚至还可以定义自己的方法。下面两个表分别显示了时间周期选项及其缩写别名和一些可能用于重采样的常用方法。其它探索这里还有一些你可以用于处理数据而进行的其它探索:用Prophet建模Facebook Prophet于2017年发布的,可用于Python,而R.Prophet是设计用于分析在不同时间间隔上显示模式的日观测时间序列。Prophet对于数据丢失情况和趋势的变化具有很强的鲁棒性,并且通常能够很好地处理异常值。它还具有高级的功能,可以模拟假日在时间序列上产生的影响并执行自定义的变更点,但我将坚持使用基本规则来启动和运行模型。我认为Prophet可能是生产快速预测结果的一个好的选择,因为它有直观的参数,并且可以由有良好领域知识背景的但缺乏预测模型的技术技能的人来进行调整。有关Prophet的更多信息,大家可以点击这里查阅官方文档。在使用Prophet之前,我们将数据里的列重新命名为正确的格式。Date列必须称为“ds”和要预测值的列为“y”。我们在下面的示例中使用了每日汇总的数据。然后我们导入Prophet,创建一个模型并与数据相匹配。在Prophet中,changepoint_prior_scale参数用于控制趋势对变化的敏感度,越高的值会更敏感,越低的值则敏感度越低。在试验了一系列值之后,我将这个参数设置为0.10,而不是默认值0.05。为了进行预测,我们需要创建一个称为未来数据框(future dataframe)的东西。我们需要指定要预测的未来时间段的数量(在我们的例子中是两个月)和预测频率(每天)。然后我们用之前创建的Prophet模型和未来数据框进行预测。非常简单!未来数据框包含了未来两个月内的预估居民使用电量。我们可以用一个图表来进行可视化预测展示:图中的黑点代表了实际值,蓝线则代表了预测值,而浅蓝色阴影区域代表不确定性。如下图所示,不确定性区域随着我们在之后的进一步变化而扩大,因为初始的不确定性随着时间的推移而扩散和增多。Prophet还可以允许我们轻松地对整体趋势和组件模式进行可视化展示:每年的模式很有趣,因为它看起来表明了居民的电量使用在秋季和冬季会增加,而在春季和夏季则会减少。直观地说,这正是我们期望要看到的。从每周的趋势来看,周日的使用量似乎比一周中其它时间都要多。最后,总体的趋势表明,使用量增长了一年,然后才缓慢地下降。需要进行进一步的调查来解释这一趋势。在下一篇文章中,我们将尝试找出是否与天气有关。LSTM(Long Short-Term Memory,长短期记忆网络)预测LSTM循环神经网络具有学习长序列观测值的前景。博客文章《了解LSTM网络》,在以一种易于理解的方式来解释底层复杂性方面做的非常出色。以下是一个描述LSTM内部单元体系结构的示意图:LSTM似乎非常适合于对时间序列的预测。让我们再次使用一下每日汇总的数据。LSTM对输入数据的大小很敏感,特别是当使用Sigmoid或Tanh这两个激活函数的时候。通常,将数据重新调整到[0,1]或[-1,1]这个范围是一个不错的实践,也称为规范化。我们可以使用scikit-learn库中的MinMaxScaler预处理类来轻松地规范化数据集。现在我们可以将已排好序的数据集拆分为训练数据集和测试数据集。下面的代码计算出了分割点的索引,并将数据拆分为多个训练数据集,其中80%的观测值可用于训练模型,剩下的20%用于测试模型。我们可以定义一个函数来创建一个新的数据集,并使用这个函数来准备用于建模的训练数据集和测试数据集。LSTM网络要求输入的数据以如下的形式提供特定的数组结构:[样本、时间间隔、特征]。数据目前都规范成了[样本,特征]的形式,我们正在为每个样本设计两个时间间隔。可以将准备好的分别用于训练和测试的输入数据转换为所期望的结构,如下所示:就是这样,现在已经准备好为示例设计和设置LSTM网络了。从下面的损失图可以看出,该模型在训练数据集和测试数据集上都具有可比较的表现。在下图中,我们看到LSTM在拟合测试数据集方面做得非常好。聚类(Clustering)最后,我们还可以使用示例的数据进行聚类。执行聚类有很多不同的方式,但一种方式是按结构层次来形成聚类。你可以通过两种方式形成一个层次结构:从顶部开始来拆分,或从底部开始来合并。我决定先看看后者。让我们从数据开始,只需简单地导入原始数据,并为某年中的某日和某日中的某一小时添加两列。Linkage和Dendrogramslinkage函数根据对象的相似性,将距离信息和对象对分组放入聚类中。这些新形成的聚类随后相互连接,以创建更大的聚类。这个过程将会进行迭代,直到在原始数据集中的所有对象在层次树中都连接在了一起。对数据进行聚类:完成了!!!这难道不是很简单吗?当然很简单了,但是上面代码中的“ward”在那里意味着什么呢?这实际上是如何执行的?正如scipy linkage文档上告诉我们的那样,“ward”是可以用来计算新形成的聚类之间距离的一个方法。关键字“ward”让linkage函数使用Ward方差最小化算法。其它常见的linkage方法,如single、complete、average,还有不同的距离度量标准,如euclidean、manhattan、hamming、cosine,如果你想玩玩的话也可以使用一下。现在让我们来看看这个称为dendogram的分层聚类图。dendogram图是聚类的层次图,其中那些条形的长度表示到下一个聚类中心的距离。如果这是你第一次看到dendrogram图,那看起来挺复杂的,但是别担心,让我们把它分解来看:在x轴上可以看到一些标签,如果你没有指定任何其它内容,那么这些标签就是X上样本的索引;·在y轴上,你可以看到那些距离长度(在我们的例子中是ward方法);水平线是聚类的合并;那些垂线告诉你哪些聚类或者标签是合并的一部分,从而形成了新的聚类;水平线的高度是用来表示需要被“桥接”以形成新聚类的距离;即使有解释说明,之前的dendogram图看起来仍然不明显。我们可以减少一点,以便能更好地查看数据。建议查找聚类文档以便能了解更多内容,并尝试使用不同的参数。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 8, 2019 · 1 min · jiezi

Javascript 函数和变量提升

变量提升和函数提升基本上是面试必问题目//先从一个面试题说起 console.log(a) if (a) { var a = 1; console.log(a) } function a() { console.log(this); } console.log(a); a() 下面我们针对这个栗子解析一下我们知道变量和很熟定义都会提升到作用域最前边唯一需要确认的是变量和函数的先后顺序我们预想 函数是用第一公民会不会提升到最前边呢?//如果是解析完顺序是这样的 function a() { console.log(this); } var a; console.log(a) if (a) { a = 1; console.log(a) } console.log(a); a()按照我们预想的解析结果应该是// undefined// undefined// 报错理由 函数在上var在下,第一个console时a未赋值,其结果是undefined,if为false 只剩最后一个console也是undefined 最后a is not a function.不过结果是我机智的认为 预想错了?//再次测试 var a; function a() { console.log(this); } console.log(a) if (a) { a = 1; console.log(a) } console.log(a); a()这样?对比一下结果人工解析结果 :1、a()2、13、14、a() 报错浏览器执行结果:没毛病!看到这里一切完美,不过我还是重新搜索了一些高质量文章,发现我错了,虽然执行结果是对的,不过浏览器和人工解析还是不一样的,和我们最开始预想的一样,函数优先。既然标题说到了 变量 和 函数,我们就一块来说说//简单的栗子function a(){ console.log(a) }console.log(a)var a = 1a()首先上边已经说到我们预想和认为的是错的。正确解析顺序是这样的function a(){ console.log(a) }var a;console.log(a)a = 1a()但是,这个但是很重要浏览器执行结果是:why?这就要讲讲我所了解到的原理。同名变量和函数,函数会提升到最前边,变量其次,为什么不那为什么结果不是我们人工执行的undefined呢?原因是 变量会被忽略,是的是忽略。。。function a(){ console.log(a) }var a;//忽略console.log(a) //打印函数本身a = 1a()// a is not a function完美!还有呢?是的还有同名变量是怎样的顺序,同名函数是怎样的顺序。同名变量console.log(a)var a = 1console.log(a)var a = 2console.log(a)//解析完顺序是这样的var a;var a; //忽略console.log(a) // undfineda = 1console.log(a) //1a = 2console.log(a)//2*同名变量,声明会被提升,后边会忽略。同名函数function a(){console.log(1)}console.log(a)function a(){console.log(2)}console.log(a)a()//解析完function a(){console.log(1)}function a(){console.log(2)}console.log(a)console.log(a)a()执行结果我想你已经猜到了,同名函数会被覆盖。终于完了!您的点赞是我继续下去的动力,谢谢! ...

January 8, 2019 · 1 min · jiezi

Javascript中的变量提升、函数提升及变量访问原则

1、变量提升什么是变量提升?在函数体内声明的变量,无论你是在函数的最底端还是中间声明的,那么都会把该变量的声明提升到函数的最顶端(相当于第一行),但是只是提升变量的声明,不会赋值。var num = 10;fun(); //输出结果为undefinedfunction fun(){ console.log(num); var num = 20;}/上面这个函数相当于: function fun(){ var num; console.log(num); num = 20; }/2、函数提升什么是函数提升?在JavaScript中以函数声明的方式创建的函数就跟用var创建的变量一样,它们的声明都会提前声明,这就使得我们在JavaScript中可以调用函数在前面,而声明函数在后面,这就是函数提升。func();function func(){ alert(“函数执行了!”);}/上面这段代码相当于:function func(){ alert(“函数执行了!”);}func();/3、函数与变量同名时的变量提升alert(fun); // 最终输出结果为:输出fun函数体function fun(){ alert(“我是一个函数”);}var fun = “我是一个变量”;alert(fun); // 输出:我是一个变量/* 为什么第一个alert输出的是fun函数体,而第二个alert输出的是“我是一个变量”?因为用var声明的变量及function声明的函数在执行前都会将声明提升到最前面,如果变量与函数同名,那么在声明的时候会忽略变量,只提升函数声明! *//上面这段代码相当于:function fun(){ alert(“我是一个函数”);}alert(fun);fun = “我是一个变量”;alert(fun);/4、变量搜索原则(变量访问原则)在JavaScript中变量的访问(搜索)是有原则的:1)、首先在访问变量的作用域(函数)中查找该变量,如果找到直接使用2)、如果没有找到,去上一级作用域中查找,如果找到直接使用3)、如果还是没有找到,则再去上一级作用域中查找,知道全局作用域4)、如果找到了就直接使用,如果没有找到则报错var num = 123;function foo1(){ function foo2(){ console.log(num); } /当调用foo2时,会首先去foo2这个作用域中查找是否有num变量,结果没找到则去上一级作用域(即foo1)中查找是否有foo1变量, 结果还 是没找到,则再去上一级作用域(全局作用域)中查找,结果找到了,则拿来使用/ foo2();}5、变量提升、变量搜索机制经典面试题fun();console.log(b);console.log(c);console.log(a);functoin fun(){ var a = b = c = 9; console.log(a); console.log(b); console.log(c);}

January 4, 2019 · 1 min · jiezi

函数节流与函数防抖

什么是函数节流与函数防抖举个栗子,我们知道目前的一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。再举个栗子,假设电梯一次只能载一人的话,10 个人要上楼的话电梯就得走 10 次,是一种浪费资源的行为;而实际生活正显然不是这样的,当电梯里有人准备上楼的时候如果外面又有人按电梯的话,电梯会再次打开直到满载位置,从电梯的角度来说,这时一种节约资源的行为(相对于一次只能载一个人)。函数节流: 指定时间间隔内只会执行一次任务;函数防抖: 任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。函数节流(throttle)这里以判断页面是否滚动到底部为例,普通的做法就是监听 window 对象的 scroll 事件,然后再函数体中写入判断是否滚动到底部的逻辑:function onScroll() { // 判断是否滚动到底部的逻辑 const pageHeight = $(‘body’).height(); const scrollTop = $(window).scrollTop(); const winHeight = $(window).height(); const thresold = pageHeight - scrollTop - winHeight; if (thresold > -100 && thresold <= 20) { console.log(’end’); }}$(window).on(‘scroll’, onScroll);这样做的一个缺点就是比较消耗性能,因为当在滚动的时候,浏览器会无时不刻地在计算判断是否滚动到底部的逻辑,而在实际的场景中是不需要这么做的,在实际场景中可能是这样的:在滚动过程中,每隔一段时间在去计算这个判断逻辑。而函数节流所做的工作就是每隔一段时间去执行一次原本需要无时不刻地在执行的函数,所以在滚动事件中引入函数的节流是一个非常好的实践:$(window).on(‘scroll’, throttle(onScroll));加上函数节流之后,当页面再滚动的时候,每隔 300ms 才会去执行一次判断逻辑。简单来说,函数的节流就是通过闭包保存一个标记(canRun = true),在函数的开头判断这个标记是否为 true,如果为 true 的话就继续执行函数,否则则 return 掉,判断完标记后立即把这个标记设为 false,然后把外部传入的函数的执行包在一个 setTimeout 中,最后在 setTimeout 执行完毕后再把标记设置为 true(这里很关键),表示可以执行下一次的循环了。当 setTimeout 还未执行的时候,canRun 这个标记始终为 false,在开头的判断中被 return 掉。function throttle(fn, interval = 300) { let canRun = true; return function () { if (!canRun) return; canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, interval); };}函数防抖(debounce)这里以用户注册时验证用户名是否被占用为例,如今很多网站为了提高用户体验,不会再输入框失去焦点的时候再去判断用户名是否被占用,而是在输入的时候就在判断这个用户名是否已被注册:$(‘input.user-name’).on(‘input’, function () { $.ajax({ url: https://just.com/check, method: ‘post’, data: { username: $(this).val(), }, success(data) { if (data.isRegistered) { $(’.tips’).text(‘该用户名已被注册!’); } else { $(’.tips’).text(‘恭喜!该用户名还未被注册!’); } }, error(error) { console.log(error); }, });});很明显,这样的做法不好的是当用户输入第一个字符的时候,就开始请求判断了,不仅对服务器的压力增大了,对用户体验也未必比原来的好。而理想的做法应该是这样的,当用户输入第一个字符后的一段时间内如果还有字符输入的话,那就暂时不去请求判断用户名是否被占用。在这里引入函数防抖就能很好地解决这个问题:$(‘input.user-name’).on(‘input’, debounce(function () { $.ajax({ url: https://just.com/check, method: ‘post’, data: { username: $(this).val(), }, success(data) { if (data.isRegistered) { $(’.tips’).text(‘该用户名已被注册!’); } else { $(’.tips’).text(‘恭喜!该用户名还未被注册!’); } }, error(error) { console.log(error); }, });}));其实函数防抖的原理也非常地简单,通过闭包保存一个标记来保存 setTimeout 返回的值,每当用户输入的时候把前一个 setTimeout clear 掉,然后又创建一个新的 setTimeout,这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数了。function debounce(fn, interval = 300) { let timeout = null; return function () { clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(this, arguments); }, interval); };}总结其实函数节流与函数防抖的原理非常简单,巧妙地使用 setTimeout 来存放待执行的函数,这样可以很方便的利用 clearTimeout 在合适的时机来清除待执行的函数。使用函数节流与函数防抖的目的,在开头的栗子中应该也能看得出来,就是为了节约计算机资源。 ...

January 3, 2019 · 1 min · jiezi

阿里专家杜万:Java响应式编程,一文全面解读

本篇文章来自于2018年12月22日举办的《阿里云栖开发者沙龙—Java技术专场》,杜万专家是该专场第四位演讲的嘉宾,本篇文章是根据杜万专家在《阿里云栖开发者沙龙—Java技术专场》的演讲视频以及PPT整理而成。摘要:响应式宣言如何解读,Java中如何进行响应式编程,Reactor Streams又该如何使用?热衷于整合框架与开发工具的阿里云技术专家杜万,为大家全面解读响应式编程,分享Spring Webflux的实践。从响应式理解,到Reactor项目示例,再到Spring Webflux框架解读,本文带你进入Java响应式编程。演讲嘉宾简介:杜万(倚贤),阿里云技术专家,全栈工程师,从事了12年 Java 语言为主的软件开发工作,热衷于整合框架与开发工具,Linux拥趸,问题终结者。合作翻译《Elixir 程序设计》。目前负责阿里云函数计算的工具链开发,正在实践 WebFlux 和 Reactor 开发新的 Web 应用。本次直播视频精彩回顾,戳这里!https://yq.aliyun.com/live/721PPT下载地址:https://yq.aliyun.com/download/3187以下内容根据演讲嘉宾视频分享以及PPT整理而成。本文围绕以下三部分进行介绍:1.Reactive2.Project Reactor3.Spring Webflux一.Reactive1.Reactive Manifesto下图是Reactive Manifesto官方网站上的介绍,这篇文章非常短但也非常精悍,非常值得大家去认真阅读。响应式宣言是一份构建现代云扩展架构的处方。这个框架主要使用消息驱动的方法来构建系统,在形式上可以达到弹性和韧性,最后可以产生响应性的价值。所谓弹性和韧性,通俗来说就像是橡皮筋,弹性是指橡皮筋可以拉长,而韧性指在拉长后可以缩回原样。这里为大家一一解读其中的关键词:1)响应性:快速/一致的响应时间。假设在有500个并发操作时,响应时间为1s,那么并发操作增长至5万时,响应时间也应控制在1s左右。快速一致的响应时间才能给予用户信心,是系统设计的追求。2)韧性:复制/遏制/隔绝/委托。当某个模块出现问题时,需要将这个问题控制在一定范围内,这便需要使用隔绝的技术,避免连锁性问题的发生。或是将出现故障部分的任务委托给其他模块。韧性主要是系统对错误的容忍。3)弹性:无竞争点或中心瓶颈/分片/扩展。如果没有状态的话,就进行水平扩展,如果存在状态,就使用分片技术,将数据分至不同的机器上。4)消息驱动:异步/松耦合/隔绝/地址透明/错误作为消息/背压/无阻塞。消息驱动是实现上述三项的技术支撑。其中,地址透明有很多方法。例如DNS提供的一串人类能读懂的地址,而不是IP,这是一种不依赖于实现,而依赖于声明的设计。再例如k8s每个service后会有多个Pod,依赖一个虚拟的服务而不是某一个真实的实例,从何实现调用1 个或调用n个服务实例对于对调用方无感知,这是为分片或扩展做了准备。错误作为消息,这在Java中是不太常见的,Java中通常将错误直接作为异常抛出,而在响应式中,错误也是一种消息,和普通消息地位一致,这和JavaScript中的Promise类似。背压是指当上游向下游推送数据时,可能下游承受能力不足导致问题,一个经典的比喻是就像用消防水龙头解渴。因此下游需要向上游声明每次只能接受大约多少量的数据,当接受完毕再次向上游申请数据传输。这便转换成是下游向上游申请数据,而不是上游向下游推送数据。无阻塞是通过no-blocking IO提供更高的多线程切换效率。2.Reactive Programming响应式编程是一种声明式编程范型。下图中左侧显示了一个命令式编程,相信大家都比较熟悉。先声明两个变量,然后进行赋值,让两个变量相加,得到相加的结果。但接着当修改了最早声明的两个变量的值后,sum的值不会因此产生变化。而在Java 9 Flow中,按相同的思路实现上述处理流程,当初始变量的值变化,最后结果的值也同步发生变化,这就是响应式编程。这相当于声明了一个公式,输出值会随着输入值而同步变化。响应式编程也是一种非阻塞的异步编程。下图是用reactor.ipc.netty实现的TCP通信。常见的server中会用循环发数据后,在循环外取出,但在下图的实现中没有,因为这不是使用阻塞模型实现,是基于非阻塞的异步编程实现。响应式编程是一种数据流编程,关注于数据流而不是控制流。下图中,首先当页面出现点击操作时产生一个click stream,然后页面会将250ms内的clickStream缓存,如此实现了一个归组过程。然后再进行map操作,得到每个list的长度,筛选出长度大于2的,这便可以得出多次点击操作的流。这种方法应用非常广泛,例如可以筛选出双击操作。由此可见,这种编程方式是一种数据流编程,而不是if else的控制流编程。之前有提及消息驱动,那么消息驱动(Message-driven)和事件驱动(Event-driven)有什么区别呢。1)消息驱动有确定的目标,一定会有消息的接受者,而事件驱动是一件事情希望被观察到,观察者是谁无关紧要。消息驱动系统关注消息的接受者,事件驱动系统关注事件源。2)在一个使用响应式编程实现的响应式系统中,消息擅长于通讯,事件擅长于反应事实。3.Reactive StreamsReactive Streams提供了一套非阻塞背压的异步流处理标准,主要应用在JVM、JavaScript和网络协议工作中。通俗来说,它定义了一套响应式编程的标准。在Java中,有4个Reactive Streams API,如下图所示:这个API中定义了Publisher,即事件的发生源,它只有一个subscribe方法。其中的Subscriber就是订阅消息的对象。作为订阅者,有四个方法。onSubscribe会在每次接收消息时调用,得到的数据都会经过onNext方法。onError方法会在出现问题时调用,Throwable即是出现的错误消息。在结束时调用onComplete方法。Subscription接口用来描述每个订阅的消息。request方法用来向上游索要指定个数的消息,cancel方法用于取消上游的数据推送,不再接受消息。Processor接口继承了Subscriber和Publisher,它既是消息的发生者也是消息的订阅者。这是发生者和订阅者间的过渡桥梁,负责一些中间转换的处理。Reactor Library从开始到现在已经历经多代。第0代就是java包Observable 接口,也就是观察者模式。具体的发展见下图:第四代虽然仍然是RxJava2,但是相比第三代的RxJava2,其中的小版本有了不一样的改进,出现了新特性。Reactor Library主要有两点特性。一是基于回调(callback-based),在事件源附加回调函数,并在事件通过数据流链时被调用;二是声明式编程(Declarative),很多函数处理业务类似,例如map/filter/fold等,这些操作被类库固化后便可以使用声明式方法,以在程序中快速便捷使用。在生产者、订阅者都定义后,声明式方法便可以用来实现中间处理者。二.Project ReactorProject Reactor,实现了完全非阻塞,并且基于网络HTTP/TCP/UDP等的背压,即数据传输上游为网络层协议时,通过远程调用也可以实现背压。同时,它还实现了Reactive Streams API和Reactive Extensions,以及支持Java 8 functional API/Completable Future/Stream /Duration等各新特性。下图所示为Reactor的一个示例:首先定义了一个words的数组,然后使用flatMap做映射,再将每个词和s做连接,得出的结果和另一个等长的序列进行一个zipWith操作,最后打印结果。这和Java 8 Stream非常类似,但仍存在一些区别:1)Stream是pull-based,下游从上游拉数据的过程,它会有中间操作例如map和reduce,和终止操作例如collect等,只有在终止操作时才会真正的拉取数据。Reactive是push-based,可以先将整个处理数据量构造完成,然后向其中填充数据,在出口处可以取出转换结果。2)Stream只能使用一次,因为它是pull-based操作,拉取一次之后源头不能更改。但Reactive可以使用多次,因为push-based操作像是一个数据加工厂,只要填充数据就可以一直产出。3)Stream#parallel()使用fork-join并发,就是将每一个大任务一直拆分至指定大小颗粒的小任务,每个小任务可以在不同的线程中执行,这种多线程模型符合了它的多核特性。Reactive使用Event loop,用一个单线程不停的做循环,每个循环处理有限的数据直至处理完成。在上例中,大家可以看到很多Reactive的操作符,例如flatMap/concatWith/zipWith等,这样的操作符有300多个,这可能是学习这个框架最大的压力。如何理解如此繁多的操作符,可能一个归类会有所帮助:1)新序列创建,例如创建数组类序列等;2)现有序列转换,将其转换为新的序列,例如常见的map操作;3)从现有的序列取出某些元素;4)序列过滤;5)序列异常处理。6)与时间相关的操作,例如某个序列是由时间触发器定期发起事件;7)序列分割;8)序列拉至同步世界,不是所有的框架都支持异步,再需要和同步操作进行交互时就需要这种处理。上述300+操作符都有如下所示的弹珠图(Marble Diagrams),用表意的方式解释其作用。例如下图的操作符是指,随着时间推移,逐个产生了6个元素的序列,黑色竖线表示新元素产生终止。在这个操作符的作用下,下方只取了前三个元素,到第四个元素就不取了。这些弹珠图大家可以自行了解。三.Spring Webflux1.Spring Webflux框架Spring Boot 2.0相较之前的版本,在基于Spring Framework 5的构建添加了新模块Webflux,将默认的web服务器改为Netty,支持Reactive应用,并且Webflux默认运行在Netty上。而Spring Framework 5也有了一些变化。Java版本最低依赖Java 8,支持Java 9和Java 10,提供许多支持Reactive的基础设施,提供面向Netty等运行时环境的适配器,新增Webflux模块(集成的是Reactor 3.x)。下图所示为Webflux的框架:左侧是通常使用的框架,通过Servlet API的规范和Container进行交互,上一层是Spring-Webmvc,再上一层则是经常使用的一些注解。右侧为对应的Webflux层级,只要是支持NIO的Container,例如Tomcat,Jetty,Netty或Undertow都可以实现。在协议层的是HTTP/Reactive Streams。再上一层是Spring-Webflux,为了保持兼容性,它支持这些常用的注解,同时也有一套新的语法规则Router Functions。下图显示了一个调用的实例:在Client端,首先创建一个WebClient,调用其get方法,写入URL,接收格式为APPLICATION_STREAM_JSON的数据,retrieve获得数据,取得数据后用bodyToFlux将数据转换为Car类型的对象,在doOnNext中打印构造好的Car对象,block方法意思是直到回调函数被执行才可以结束。在Server端,在指定的path中进行get操作,produces和以前不同,这里是application/stream+json,然后返回Flux范型的Car对象。传统意义上,如果数据中有一万条数据,那么便直接返回一万条数据,但在这个示例返回的Flux范型中,是不包含数据的,但在数据库也支持Reactive的情况下,request可以一直往下传递,响应式的批量返回。传统方式这样的查询很有可能是一个全表遍历,这会需要较多资源和时间,甚至影响其他任务的执行。而响应式的方法除了可以避免这种情况,还可以让用户在第一时间看到数据而不是等待数据采集完毕,这在架构体验的完整性上有了很大的提升。application/stream+json也是可以让前端识别出,这些数据是分批响应式传递,而不会等待传完才显示。现在的Java web应用可以使用Servlet栈或Reactive栈。Servlet栈已经有很久的使用历史了,而现在又增加了更有优势的Reactive栈,大家可以尝试实现更好的用户体验。2.Reactive编程模型下图中是Spring实现的一个向后兼容模型,可以使用annotation来标注Container。这是一个非常清晰、支持非常细节化的模型,也非常利于同事间的交流沟通。下图是一个Functional编程模型,通过写函数的方式构造。例如下图中传入一个Request,返回Response,通过函数的方法重点关注输入输出,不需要区分状态。然后将这些函数注册至Route。这个模型和Node.js非常接近,也利于使用。3.Spring Data框架Spring Data框架支持多种数据库,如下图所示,最常用的是JPA和JDBC。在实践中,不同的语言访问不同的数据库时,访问接口是不一样的,这对编程人员来说是个很大的工作量。Spring Data便是做了另一层抽象,使你无论使用哪种数据库,都可以使用同一个接口。具体特性这里不做详谈。下图展示了一个Spring Data的使用示例。只需要写一个方法签名,然后注解为Query,这个方法不需要实现,因为框架后台已经采用一些技术,直接根据findByFirstnameAndLastname就可以查询到。这种一致的调用方式无疑提供了巨大的方便。现在Reactive对Spring Data的支持还是不完整的,只支持了MongoDB/Redis/Cassandra和Couchbase,对JPA/LDAP/Elasticsearch/Neo4j/Solr等还不兼容。但也不是不能使用,例如对JDBC数据库,将其转为同步即可使用,重点在于findAll和async两个函数,这里不再展开详述,具体代码如下图所示:Reactive不支持JDBC最根本的原因是,JDBC不是non-blocking设计。但是现在JavaOne已经在2016年9月宣布了Non-blocking JDBC API的草案,虽然还未得到Java 10的支持,但可见这已经成为一种趋势。四.总结Spring MVC框架是一个命令式逻辑,方便编写和调试。Spring WebFlux也具有众多优势,但调试却不太容易,因为它经常需要切换线程执行,出现错误的栈可能已经销毁。当然这也是现今Java的编译工具对WebFlux不太友好,相信以后会改善。下图中列出了Spring MVC和Spring WebFlux各自的特性及交叉的部分。最后也附上一些参考资料。本文作者:李博bluemind阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 28, 2018 · 1 min · jiezi

五分钟教你如何用函数计算部署钉钉群发机器人

如果你是钉钉多个群的管理员,想要在多个钉钉群群发消息的时候,是不是还在为要寻找所有的群,并不断的复制黏贴消息而烦恼?过去的你:(N 个群,N 次操作)现在的你:(N 个群,1 次操作)本篇文章适合对函数计算服务感兴趣或想要在钉钉解放双手、轻松的在多个群群发的用户。本文将通过阿里云函数计算服务,手把手教大家如何一键部署钉钉机器人群发私服,解放双手。本文分为以下几部分:主要特色使用流程部署指南小结主要特色利用函数计算,一键部署钉钉机器人群发私服,解放双手通过鉴权认证,避免私服泄漏使用流程部署指南开通 阿里云函数计算服务 并安装配置 函数计算工具 fcligit clone https://github.com/awesome-fc/dingtalk-broadcast.git在项目目录下的 urls.txt 文件中,输入要使用的群发机器人的 webhookwebhook地址:点击 钉钉桌面版 右上角的个人名片,选择 机器人管理在机器人管理页面中,可以选择新增自定义机器人,也可以在已添加的自定义机器人列表中,点击 … 按钮,获取 webhook 地址urls.txt 格式:# 可以在每一行通过 ‘#’ 号,添加注释# 群 1https://oapi.dingtalk.com/robot/send?access_token=123456# 群 2https://oapi.dingtalk.com/robot/send?access_token=456789执行项目目录下的 deploy.sh 文件,命令行将会输出 endpoint 与 token,同时会打开浏览器并跳转到 钉钉消息群发 ,endpoint 与 token 默认会填写,此时只需要填写要群发的 消息,点击发送即可可以选择自己要发送的格式,选择 @所有人 即可 @所有人此后要发送消息,只需要执行项目目录下的 start.sh 文件,或直接浏览 钉钉消息群发 并填写 endpoint 、 token 与要发送的消息即可如果要修改机器人信息,可以在 urls.txt 文件中修改机器人信息,并重新执行 deploy.sh小结利用函数计算服务部署钉钉群发机器人,一方面是将 N 个群 N 次 操作转变为 N 个群 1 次操作、解放用户双手,另一方面是通过函数计算服务,将群发功能部署在云端,避免了日后运维的操作,也方便广大用户的使用。本文作者:泽尘阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 26, 2018 · 1 min · jiezi

JS 总结之函数、作用域链

JavaScript 中,函数实际上是一个对象。???? 声明JavaScript 用 function 关键字来声明一个函数:function fn () {}变体:函数表达式:var fn = function () {}这种没有函数名的函数被称为匿名函数表达式。???? return函数可以有返回值function fn () { return true}位于 return 之后的任何代码都不会执行:function fn () { return true console.log(false) // 永远不会执行}fn() // true没有 return 或者只写 return,函数将返回 undefined:function fn () {}fn() // undefined// 或者function fn () { return}fn() // undefined⛹ 参数函数可以带有限个数或者不限个数的参数// 参数有限function fn (a, b) { console.log(a, b)}// 参数不限function fn (a, b, …, argN) { console.log(a, b, …, argN)}没有传值的命名参数,会被自动设置为 undefined// 参数有限function fn (a, b) { console.log(b) // undefined}fn(1)???? arguments函数可以通过内部属性 arguments 这个类数组的对象来访问参数,即便没有命名参数:// 有命名参数function fn (a, b) { console.log(arguments.length) // 2}fn(1, 2)// 无命名参数function fn () { console.log(arguments[0], arguments[1], arguments[2]) // 1, 2, 3}fn(1, 2, 3)⛳️ 长度arguments 的长度由传入的参数决定,并不是定义函数时决定的。function fn () { console.log(arguments.length) // 3}fn(1, 2, 3)如果按定义函数是决定个的,那么此时的 arguments.length 应该为 0 而不为 3。???? 同步arguments 对象中的值会自动反应到对应的命名参数,可以理解为同步,不过并不是因为它们读取了相同的内存空间,而只是保持值同步而已。function fn (a) { console.log(arguments[0]) // 1 a = 2 console.log(arguments[0]) // 2 arguments[0] = 3 console.log(a) // 3}fn(1)严格模式下,重写 arguments 的值会导致错误。 ???? callee通过 callee 这个指针访问拥有这个 arguments 对象的函数function fn () { console.log(arguments.callee) // fn}fn()???? 类数组长的跟数组一样,可以通过下标访问,如 arguments[0],却无法使用数组的内置方法,如 forEach 等:function fn () { console.log(arguments[0], arguments[1]) // 1, 2 console.log(arguments.forEach) // undefined}fn(1, 2)通过对象那章知道,可以用 call 或者 apply 借用函数,所以 arguments 可以借用数组的内置方法:function fn () { Array.prototype.forEach.call(arguments, function (item) { console.log(item) })}fn(1, 2)// 1// 2对于如此诡异的 arguments,我觉得还是少用为好。???? this、 prototype具体查看总结:《关于 this 应该知道的几个点》《原型》???? 按值传递引用《JavaScript 高级程序设计》4.1.3 的一句话:ECMAScript 中所有函数的参数都是按值传递的,也就是说,把函数外部的值复制给函数内部的参数,就和把一个变量复制到另一个变量一样。???? 基本类型的参数传递基本类型的传递很好理解,就是把变量复制给函数的参数,变量和参数是完全独立的两个个体:var name = ‘jon’function fn (a) { a = ‘karon’ console.log(‘a: ‘, a) // a: karon}fn(name)console.log(’name: ‘, name) // name: jon用表格模拟过程:栈内存 堆内存 name, a jon 将 a 复制为其他值后:栈内存 堆内存 name jon a karon ???? 引用类型的参数传递var obj = { name: ‘jon’}function fn (a) { a.name = ‘karon’ console.log(‘a: ‘, a) // a: { name: ‘karon’ }}fn(obj)console.log(obj) // name: { name: ‘karon’ }嗯?说好的按值传递呢?我们尝试把 a 赋值为其他值,看看会不会改变了 obj 的值:var obj = { name: ‘jon’}function fn (a) { a = ‘karon’ console.log(‘a: ‘, a) // a: karon}fn(obj)console.log(obj) // name: { name: ‘jon’ }???? 真相浮出水面参数 a 只是复制了 obj 的引用,所以 a 能找到对象 obj,自然能对其进行操作。一旦 a 赋值为其他属性了,obj 也不会改变什么。用表格模拟过程:栈内存 堆内存 obj, a 引用值 { name: ‘jon’ } 参数 a 只是 复制了 obj 的引用,所以 a 能找到存在堆内存中的对象,所以 a 能对堆内存中的对象进行修改后:栈内存 堆内存 obj, a 引用值 { name: ‘karon’ } 将 a 复制为其他值后:栈内存 堆内存 obj 引用值 { name: ‘karon’ } a ‘karon’ 因此,基本类型和引用类型的参数传递也是按值传递的???? 作用域链理解作用域链之前,我们需要理解执行环境 和 变量对象。???? 执行环境执行环境定义了变量或者函数有权访问的其它数据,可以把执行环境理解为一个大管家。执行环境分为全局执行环境和函数执行环境,全局执行环境被认为是 window 对象。而函数的执行环境则是由函数创建的。每当一个函数被执行,就会被推入一个环境栈中,执行完就会被推出,环境栈最底下一直是全局执行环境,只有当关闭网页或者推出浏览器,全局执行环境才会被摧毁。???? 变量对象每个执行环境都有一个变量对象,存放着环境中定义的所有变量和函数,是作用域链形成的前置条件。但我们无法直接使用这个变量对象,该对象主要是给 JS 引擎使用的。具体可以查看《JS 总结之变量对象》。???? 作用域链的作用而作用域链属于执行环境的一个变量,作用域链收集着所有有序的变量对象,函数执行环境中函数自身的变量对象(此时称为活动对象)放置在作用域链的最前端,如:scope: [函数自身的变量对象,变量对象1,变量对象2,…, 全局执行环境的变量对象]作用域链保证了对执行环境有权访问的所有变量和函数的有序访问。var a = 1function fn1 () { var b = 2 console.log(a,b) // 1, 2 function fn2 () { var c = 3 console.log(a, b, c) // 1, 2, 3 } fn2()}fn1()对于 fn2 来说,作用域链为: fn2 执行环境、fn1 执行环境 和 全局执行环境 的变量对象(所有变量和函数)。对于 fn1 来说,作用域链为: fn1 执行环境 和 全局执行环境 的变量对象(所有变量和函数)。总结为一句:函数内部能访问到函数外部的值,函数外部无法范围到函数内部的值。引出了闭包的概念,查看总结:《JS 总结之闭包》???? 箭头函数ES6 新语法,使用 => 定义一个函数:let fn = () => {}当只有一个参数的时候,可以省略括号:let fn = a => {}当只有一个返回值没有其他语句时,可以省略大括号:let fn = a => a// 等同于let fn = function (a) { return a}返回对象并且没有其他语句的时候,大括号需要括号包裹起来,因为 js 引擎认为大括号是代码块:let fn = a => ({ name: a })// 等同于let fn = function (a) { return { name: a }}箭头函数的特点:没有 this,函数体内的 this 是定义时外部的 this不能被 new,因为没有 this不可以使用 arguments,可以使用 rest 代替不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。???? 参考《JavaScript 深入之参数按值传递》 by 冴羽《ECMAScript 6 入门》函数的扩展 - 箭头函数 by 阮一峰《JavaScript 高级程序设计》第三章 基本概念、第四章 4.1.3 传递参数、第四章 4.2 执行环境及作用域 ...

December 24, 2018 · 3 min · jiezi

(Python)零起步数学+神经网络入门

摘要: 手把手教你用(Python)零起步数学+神经网络入门!在这篇文章中,我们将在Python中从头开始了解用于构建具有各种层神经网络(完全连接,卷积等)的小型库中的机器学习和代码。最终,我们将能够写出如下内容:假设你对神经网络已经有一定的了解,这篇文章的目的不是解释为什么构建这些模型,而是要说明如何正确实现。逐层我们这里需要牢记整个框架:1. 将数据输入神经网络2. 在得出输出之前,数据从一层流向下一层3. 一旦得到输出,就可以计算出一个标量误差。4. 最后,可以通过相对于参数本身减去误差的导数来调整给定参数(权重或偏差)。5. 遍历整个过程。最重要的一步是第四步。 我们希望能够拥有任意数量的层,以及任何类型的层。 但是如果修改/添加/删除网络中的一个层,网络的输出将会改变,误差也将改变,误差相对于参数的导数也将改变。无论网络架构如何、激活函数如何、损失如何,都必须要能够计算导数。为了实现这一点,我们必须分别实现每一层。每个层应该实现什么我们可能构建的每一层(完全连接,卷积,最大化,丢失等)至少有两个共同点:输入和输出数据。现在重要的一部分假设给出一个层相对于其输出(∂E/∂Y)误差的导数,那么它必须能够提供相对于其输入(∂E/∂X)误差的导数。è®°ä½ï¼Eæ¯æ éï¼ä¸ä¸ªæ°å­ï¼ï¼XåYæ¯ç©éµã 我们可以使用链规则轻松计算∂E/∂X的元素:为什么是∂E/∂X?对于每一层,我们需要相对于其输入的误差导数,因为它将是相对于前一层输出的误差导数。这非常重要,这是理解反向传播的关键!在这之后,我们将能够立即从头开始编写深度卷积神经网络!花样图解基本上,对于前向传播,我们将输入数据提供给第一层,然后每层的输出成为下一层的输入,直到到达网络的末端。对于反向传播,我们只是简单使用链规则来获得需要的导数。这就是为什么每一层必须提供其输出相对于其输入的导数。这可能看起来很抽象,但是当我们将其应用于特定类型的层时,它将变得非常清楚。现在是编写第一个python类的好时机。抽象基类:Layer所有其它层将继承的抽象类Layer会处理简单属性,这些属性是输入,输出以及前向和反向方法。from abc import abstractmethod# Base classclass Layer: def init(self): self.input = None; self.output = None; self.input_shape = None; self.output_shape = None; # computes the output Y of a layer for a given input X @abstractmethod def forward_propagation(self, input): raise NotImplementedError # computes dE/dX for a given dE/dY (and update parameters if any) @abstractmethod def backward_propagation(self, output_error, learning_rate): raise NotImplementedError正如你所看到的,在back_propagation函数中,有一个我没有提到的参数,它是learning_rate。 此参数应该类似于更新策略或者在Keras中调用它的优化器,为了简单起见,我们只是通过学习率并使用梯度下降更新我们的参数。全连接层现在先定义并实现第一种类型的网络层:全连接层或FC层。FC层是最基本的网络层,因为每个输入神经元都连接到每个输出神经元。前向传播每个输出神经元的值由下式计算:使用矩阵,可以使用点积来计算每一个输出神经元的值:当完成前向传播之后,现在开始做反向传播。反向传播正如我们所说,假设我们有一个矩阵,其中包含与该层输出相关的误差导数(∂E/∂Y)。 我们需要:1.关于参数的误差导数(∂E/∂W,∂E/∂B)2.关于输入的误差导数(∂E/∂X)首先计算∂E/∂W,该矩阵应与W本身的大小相同:对于ixj,其中i是输入神经元的数量,j是输出神经元的数量。每个权重都需要一个梯度:使用前面提到的链规则,可以写出:那么:这就是更新权重的第一个公式!现在开始计算∂E/∂B:同样,∂E/∂B需要与B本身具有相同的大小,每个偏差一个梯度。 我们可以再次使用链规则:得出结论:现在已经得到∂E/∂W和∂E/∂B,我们留下∂E/∂X这是非常重要的,因为它将“作用”为之前层的∂E/∂Y。再次使用链规则:最后,我们可以写出整个矩阵:æä»¬å·²ç»å¾å°FC屿éçä¸ä¸ªå ¬å¼ï¼ 编码全连接层现在我们可以用Python编写实现:from layer import Layerimport numpy as np# inherit from base class Layerclass FCLayer(Layer): # input_shape = (1,i) i the number of input neurons # output_shape = (1,j) j the number of output neurons def init(self, input_shape, output_shape): self.input_shape = input_shape; self.output_shape = output_shape; self.weights = np.random.rand(input_shape[1], output_shape[1]) - 0.5; self.bias = np.random.rand(1, output_shape[1]) - 0.5; # returns output for a given input def forward_propagation(self, input): self.input = input; self.output = np.dot(self.input, self.weights) + self.bias; return self.output; # computes dE/dW, dE/dB for a given output_error=dE/dY. Returns input_error=dE/dX. def backward_propagation(self, output_error, learning_rate): input_error = np.dot(output_error, self.weights.T); dWeights = np.dot(self.input.T, output_error); # dBias = output_error # update parameters self.weights -= learning_rate * dWeights; self.bias -= learning_rate * output_error; return input_error;激活层到目前为止所做的计算都完全是线性的。用这种模型学习是没有希望的,需要通过将非线性函数应用于某些层的输出来为模型添加非线性。现在我们需要为这种新类型的层(激活层)重做整个过程!不用担心,因为此时没有可学习的参数,过程会快点,只需要计算∂E/∂X。我们将f和f’分别称为激活函数及其导数。前向传播正如将看到的,它非常简单。对于给定的输入X,输出是关于每个X元素的激活函数,这意味着输入和输出具有相同的大小。反向传播给出∂E/∂Y,需要计算∂E/∂X注意,这里我们使用两个矩阵之间的每个元素乘法(而在上面的公式中,它是一个点积)编码实现激活层激活层的代码非常简单:from layer import Layer# inherit from base class Layerclass ActivationLayer(Layer): # input_shape = (1,i) i the number of input neurons def init(self, input_shape, activation, activation_prime): self.input_shape = input_shape; self.output_shape = input_shape; self.activation = activation; self.activation_prime = activation_prime; # returns the activated input def forward_propagation(self, input): self.input = input; self.output = self.activation(self.input); return self.output; # Returns input_error=dE/dX for a given output_error=dE/dY. # learning_rate is not used because there is no “learnable” parameters. def backward_propagation(self, output_error, learning_rate): return self.activation_prime(self.input) * output_error;可以在单独的文件中编写一些激活函数以及它们的导数,稍后将使用它们构建ActivationLayer:import numpy as np# activation function and its derivativedef tanh(x): return np.tanh(x);def tanh_prime(x): return 1-np.tanh(x)*2;损失函数到目前为止,对于给定的层,我们假设给出了∂E/∂Y(由下一层给出)。但是最后一层怎么得到∂E/∂Y?我们通过简单地手动给出最后一层的∂E/∂Y,它取决于我们如何定义误差。网络的误差由自己定义,该误差衡量网络对给定输入数据的好坏程度。有许多方法可以定义误差,其中一种最常见的叫做MSE - Mean Squared Error:其中y 和y分别表示期望的输出和实际输出。你可以将损失视为最后一层,它将所有输出神经元吸收并将它们压成一个神经元。与其他每一层一样,需要定义∂E/∂Y。除了现在,我们终于得到E!以下是两个python函数,可以将它们放在一个单独的文件中,将在构建网络时使用。import numpy as np# loss function and its derivativedef mse(y_true, y_pred): return np.mean(np.power(y_true-y_pred, 2));def mse_prime(y_true, y_pred): return 2(y_pred-y_true)/y_true.size;网络类到现在几乎完成了!我们将构建一个Network类来创建神经网络,非常容易,类似于第一张图片!我注释了代码的每一部分,如果你掌握了前面的步骤,那么理解它应该不会太复杂。from layer import Layerclass Network: def init(self): self.layers = []; self.loss = None; self.loss_prime = None; # add layer to network def add(self, layer): self.layers.append(layer); # set loss to use def use(self, loss, loss_prime): self.loss = loss; self.loss_prime = loss_prime; # predict output for given input def predict(self, input): # sample dimension first samples = len(input); result = []; # run network over all samples for i in range(samples): # forward propagation output = input[i]; for layer in self.layers: # output of layer l is input of layer l+1 output = layer.forward_propagation(output); result.append(output); return result; # train the network def fit(self, x_train, y_train, epochs, learning_rate): # sample dimension first samples = len(x_train); # training loop for i in range(epochs): err = 0; for j in range(samples): # forward propagation output = x_train[j]; for layer in self.layers: output = layer.forward_propagation(output); # compute loss (for display purpose only) err += self.loss(y_train[j], output); # backward propagation error = self.loss_prime(y_train[j], output); # loop from end of network to beginning for layer in reversed(self.layers): # backpropagate dE error = layer.backward_propagation(error, learning_rate); # calculate average error on all samples err /= samples; print(’epoch %d/%d error=%f’ % (i+1,epochs,err));构建一个神经网络最后!我们可以使用我们的类来创建一个包含任意数量层的神经网络!为了简单起见,我将向你展示如何构建……一个XOR。from network import Networkfrom fc_layer import FCLayerfrom activation_layer import ActivationLayerfrom losses import from activations import import numpy as np# training datax_train = np.array([[[0,0]], [[0,1]], [[1,0]], [[1,1]]]);y_train = np.array([[[0]], [[1]], [[1]], [[0]]]);# networknet = Network();net.add(FCLayer((1,2), (1,3)));net.add(ActivationLayer((1,3), tanh, tanh_prime));net.add(FCLayer((1,3), (1,1)));net.add(ActivationLayer((1,1), tanh, tanh_prime));# trainnet.use(mse, mse_prime);net.fit(x_train, y_train, epochs=1000, learning_rate=0.1);# testout = net.predict(x_train);print(out);同样,我认为不需要强调很多事情,只需要仔细训练数据,应该能够先获得样本维度。例如,对于xor问题,样式应为(4,1,2)。结果$ python xor.py epoch 1/1000 error=0.322980 epoch 2/1000 error=0.311174 epoch 3/1000 error=0.307195 … epoch 998/1000 error=0.000243 epoch 999/1000 error=0.000242 epoch 1000/1000 error=0.000242 [array([[ 0.00077435]]), array([[ 0.97760742]]), array([[ 0.97847793]]), array([[-0.00131305]])]卷积层这篇文章开始很长,所以我不会描述实现卷积层的所有步骤。但是,这是我做的一个实现:from layer import Layerfrom scipy import signalimport numpy as np# inherit from base class Layer# This convolutional layer is always with stride 1class ConvLayer(Layer): # input_shape = (i,j,d) # kernel_shape = (m,n) # layer_depth = output depth def init(self, input_shape, kernel_shape, layer_depth): self.input_shape = input_shape; self.input_depth = input_shape[2]; self.kernel_shape = kernel_shape; self.layer_depth = layer_depth; self.output_shape = (input_shape[0]-kernel_shape[0]+1, input_shape[1]-kernel_shape[1]+1, layer_depth); self.weights = np.random.rand(kernel_shape[0], kernel_shape[1], self.input_depth, layer_depth) - 0.5; self.bias = np.random.rand(layer_depth) - 0.5; # returns output for a given input def forward_propagation(self, input): self.input = input; self.output = np.zeros(self.output_shape); for k in range(self.layer_depth): for d in range(self.input_depth): self.output[:,:,k] += signal.correlate2d(self.input[:,:,d], self.weights[:,:,d,k], ‘valid’) + self.bias[k]; return self.output; # computes dE/dW, dE/dB for a given output_error=dE/dY. Returns input_error=dE/dX. def backward_propagation(self, output_error, learning_rate): in_error = np.zeros(self.input_shape); dWeights = np.zeros((self.kernel_shape[0], self.kernel_shape[1], self.input_depth, self.layer_depth)); dBias = np.zeros(self.layer_depth); for k in range(self.layer_depth): for d in range(self.input_depth): in_error[:,:,d] += signal.convolve2d(output_error[:,:,k], self.weights[:,:,d,k], ‘full’); dWeights[:,:,d,k] = signal.correlate2d(self.input[:,:,d], output_error[:,:,k], ‘valid’); dBias[k] = self.layer_depth * np.sum(output_error[:,:,k]); self.weights -= learning_ratedWeights; self.bias -= learning_ratedBias; return in_error;它背后的数学实际上并不复杂!这是一篇很好的文章,你可以找到∂E/∂W,∂E/∂B和∂E/∂X的解释和计算。如果你想验证你的理解是否正确,请尝试自己实现一些网络层,如MaxPooling,Flatten或DropoutGitHub库你可以在GitHub库中找到用于该文章的完整代码。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 21, 2018 · 4 min · jiezi

GAN是一种特殊的损失函数?

摘要: 从本质上来说,生成对抗网络(GAN)是一种特殊的损失函数,我们来深入探索下这句话的含义。数据科学家Jeremy Howard在fast.ai的《生成对抗网络(GAN)》课程中曾经讲过这样一句话:“从本质上来说,生成对抗网络(GAN)是一种特殊的损失函数。”你是否能够理解这句话的意思?读完本文,你会更好的理解这句话的含义。神经网络的函数逼近理论在数学中,我们可以将函数看做一个“机器”或“黑匣子”,我们为这个“机器”或“黑匣子”提供了一个或多个数字作为输入,则会输出一个或多个数字,如下图所示:一般来说,我们可以用一个数学表达式来表示我们想要的函数。但是,在一些特殊的情况下,我们就没办法将函数写成一堆加法和乘法的明确组合,比如:我们希望拥有这样一个函数,即能够判断输入图像的类别是猫还是狗。如果不能用明确的用数学表达式来表达这个函数,那么,我们可以用某种方法近似表示吗?这个近似方法就是神经网络。通用近似定理表明,如果一个前馈神经网络具有线性输出层和至少一层隐藏层,只要给予网络足够数量的神经元,便可以表示任何一个函数。作为损失函数的神经网络现在,我们希望设计一个猫和狗的分类器。但我们没办法设计一个特别明确的分类函数,所以我们另辟蹊径,构建一个神经网络,然后一步一步逐渐实现这一目标。为了更好的逼近,神经网络需要知道距离目标到底还有多远。我们使用损失函数表示误差。现在,存在很多种类型的损失函数,使用哪种损失函数则取决于手头上的任务。并且,他们有一个共同的属性,即这些损失函数必须能够用精确的数学表达式来表示,如:1.L1损失函数(绝对误差):用于回归任务。2.L2损失函数(均方误差):和L1损失函数类似,但对异常值更加敏感。3.交叉熵损失函数:通常用于分类任务。4.Dice系数损失函数:用于分割任务。5.相对熵:又称KL散度,用于测量两个分布之间的差异。在构建一个性能良好的神经网络时,损失函数非常有用。正确深入的理解损失函数,并适时使用损失函数实现目标,是开发人员必备的技能之一。如何设计一个好的损失函数,也是一个异常活跃的研究领域。比如:《密度对象检测的焦点损失函数(Focal Loss)》中就设计了一种新的损失函数,称为焦点损失函数,可以处理人脸检测模型中的差异。可明确表示损失函数的一些限制上文提到的损失函数适用于分类、回归、分割等任务,但是如果模型的输出具有多模态分布,这些损失函数就派不上用场了。比如,对黑白图像进行着色处理。如上图所示:1.输入图像是个黑白鸟类图像,真实图像的颜色是蓝色。2.使用L2损失函数计算模型输出的彩色图像和蓝色真实图像之间的差异。3.接下来,我们有一张非常类似的黑白鸟类图像,其真实图像的颜色是红色。4.L2损失函数现在尝试着将模型输出的颜色和红色的差异最小化。5.根据L2损失函数的反馈,模型学习到:对于类似的鸟类,其输出可以接近红色,也可以接近蓝色,那么,到底应该怎么做呢?6.最后,模型输出鸟类的颜色为黄色,这就是处于红色和蓝色中间的颜色,并且是差异最小化的安全选择,即便是模型以前从未见过黄色的鸟,它也会这样做。7.但是,自然界中没有黄色的鸟类,所以模型的输出并不真实。在很多情况下,这种平均效果并不理想。举个例子来说,如果需要模型预测视频中下一个帧图像,下一个帧有很多种可能,你肯定希望模型输出其中一种可能,然如果使用L1或L2损失函数,模型会将所有可能性平均化,输出一个特别模型的平均图像,这就和我们的目标相悖。生成对抗网络——一种新的损失函数如果我们没办法用明确的数学表达式来表示这个损失函数,那么,我们就可以使用神经网络进行逼近,比如,函数接收一组数字,并输出狗的真实图像。神经网络需要使用损失函数来反馈当前结果如何,但是并没有哪个损失函数可以很好的实现这一目标。会不会有这样一种方法?能够直接逼近神经网络的损失函数,但是我们没必要知道其数学表达式是什么,这就像一个“机器”或“黑匣子”,就跟神经网络一样。也就是说,如果使用一个神经网络模型替换这个损失函数,这样可以吗?对,这就是生成对抗网络(GAN)。我们来看上面两个图,就可以更好的理解损失函数。在上图中,白色框表示输入,粉色和绿色框表示我们要构建的神经网络,蓝色表示损失函数。在vanilla GAN中,只有一个损失函数,即判别器D,这本身就是一个特殊的神经网络。而在Alpha-GAN中,有3个损失函数,即输入数据的判别器D,编码潜在变量的潜在判别器C和传统的像素级L1损失函数。其中,D和C不是明确的损失函数,而是一种逼近,即一个神经网络。梯度如果使用损失函数训练生成网络(和Alpha-GAN网络中的编码器),那么,应该使用哪种损失函数来训练判别器呢?判别器的任务是区分实际数据分布和生成数据分布,使用监督的方式训练判别器比较容易,如二元交叉熵。由于判别器是生成器的损失韩式,这就意味着,判别器的二进制交叉熵损失函数产生的梯度也可以用来更新生成器。结论考虑到神经网络可以代替传统的损失函数,生成对抗网络就实现了这一目标。两个网络之间的相互作用,可以让神经网络执行一些以前无法实现的任务,比如生成逼真的图像等任务。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 21, 2018 · 1 min · jiezi

用PyTorch创建一个图像分类器?So easy!(Part 2)

摘要: 学习完了如何加载预训练神经网络,下面就让我们来看看如何训练分类器吧!在第一部分中,我们知道了为什么以及如何加载预先训练好的神经网络,我们可以用自己的分类器代替已有神经网络的分类器。那么,在这篇文章中,我们将学习如何训练分类器。训练分类器首先,我们需要为分类器提供待分类的图像。本文使用ImageFolder加载图像,预训练神经网络的输入有特定的格式,因此,我们需要用一些变换来调整图像的大小,即在将图像输入到神经网络之前,对其进行裁剪和标准化处理。具体来说,将图像大小调整为224*224,并对图像进行标准化处理,即均值为 [0.485,0.456,0.406],标准差为[0.229,0.224,0.225],颜色管道的均值设为0,标准差缩放为1。然后,使用DataLoader批量传递图像,由于有三个数据集:训练数据集、验证数据集和测试数据集,因此需要为每个数据集创建一个加载器。一切准备就绪后,就可以训练分类器了。在这里,最重要的挑战就是——正确率(accuracy)。让模型识别一个已经知道的图像,这不算啥事,但是我们现在的要求是:能够概括、确定以前从未见过的图像中花的类型。在实现这一目标过程中,我们一定要避免过拟合,即“分析的结果与特定数据集的联系过于紧密或完全对应,因此可能无法对其他数据集进行可靠的预测或分析”。隐藏层实现适当拟合的方法有很多种,其中一种很简单的方法就是:隐藏层。我们很容易陷入这样一种误区:拥有更多或更大的隐藏层,能够提高分类器的正确率,但事实并非如此。增加隐藏层的数量或大小以后,我们的分类器就需要考虑更多不必要的参数。举个例子来说,将噪音看做是花朵的一部分,这会导致过拟合,也会降低精度,不仅如此,分类器还需要更长的时间来训练和预测。因此,我建议你从数量较少的隐藏层开始,然后根据需要增加隐藏层的数量或大小,而不是一开始就使用特别多或特别大的隐藏层。在第一部分介绍的《AI Programming with Python Nanodegree》课程中的花卉分类器项目中,我只需要一个小的隐藏层,在第一个完整训练周期内,就得到了70%以上的正确率。数据增强我们有很多图像可供模型训练,这非常不错。如果拥有更多的图像,数据增强就可以发挥作用了。每个图像在每个训练周期都会作为神经网络的输入,对神经网络训练一次。在这之前,我们可以对输入图像做一些随机变化,比如旋转、平移或缩放。这样,在每个训练周期内,输入图像都会有差异。增加训练数据的种类有利于减少过拟合,同样也提高了分类器的概括能力,从而提高模型分类的整体准确度。Shuffle在训练分类器时,我们需要提供一系列随机的图像,以免引入任何误差。举个例子来说,我们刚开始训练分类器时,我们使用“牵牛花”图像对模型进行训练,这样一来,分类器在后续训练过程中将会偏向“牵牛花”,因为它只知道“牵牛花”。因此,在我们使用其他类型的花进行训练时,分类器最初的偏好也将持续一段时间。为了避免这一现象,我们就需要在数据加载器中使用不同的图像,这很简单,只需要在加载器中添加shuffle=true,代码如下:trainloader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)Dropout有的时候,分类器中的节点可能会导致其他节点不能进行适当的训练,此外,节点可能会产生共同依赖,这就会导致过拟合。Dropout技术通过在每个训练步骤中使一些节点处于不活跃状态,来避免这一问题。这样一来,在每个训练阶段都使用不同的节点子集,从而减少过拟合。除了过拟合,我们一定要记住,学习率( learning rate )是最关键的超参数。如果学习率过大,模型的误差永远都不会降到最小;如果学习率过小,分类器将会训练的特别慢,因此,学习率不能过大也不能过小。一般来说,学习率可以是0.01,0.001,0.0001……,依此类推。最后,在最后一层选择正确的激活函数会对模型的正确率会产生特别大的影响。举个例子来说,如果我们使用 negative log likelihood loss(NLLLoss),那么,在最后一层中,建议使用LogSoftmax激活函数。结论理解模型的训练过程,将有助于创建能够概括的模型,在预测新图像类型时的准确度更高。在本文中,我们讨论了过拟合将会如何降低模型的概括能力,并学习了降低过拟合的方法。另外,我们也强调了学习率的重要性及其常用值。最后,我们知道,为最后一层选择正确的激活函数非常关键。现在,我们已经知道应该如何训练分类器,那么,我们就可以用它来预测以前从未见过的花型了!本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 20, 2018 · 1 min · jiezi

深度学习目标检测系列:一文弄懂YOLO算法|附Python源码

摘要: 本文是目标检测系列文章——YOLO算法,介绍其基本原理及实现细节,并用python实现,方便读者上手体验目标检测的乐趣。在之前的文章中,介绍了计算机视觉领域中目标检测的相关方法——RCNN系列算法原理,以及Faster RCNN的实现。这些算法面临的一个问题,不是端到端的模型,几个构件拼凑在一起组成整个检测系统,操作起来比较复杂,本文将介绍另外一个端到端的方法——YOLO算法,该方法操作简便且仿真速度快,效果也不差。YOLO算法是什么?YOLO框架(You Only Look Once)与RCNN系列算法不一样,是以不同的方式处理对象检测。它将整个图像放在一个实例中,并预测这些框的边界框坐标和及所属类别概率。使用YOLO算法最大优的点是速度极快,每秒可处理45帧,也能够理解一般的对象表示。YOLO框架如何运作?在本节中,将介绍YOLO用于检测给定图像中的对象的处理步骤。首先,输入图像:然后,YOLO将输入图像划分为网格形式(例如3 X 3):最后,对每个网格应用图像分类和定位处理,获得预测对象的边界框及其对应的类概率。整个过程是不是很清晰,下面逐一详细介绍。首先需要将标记数据传递给模型以进行训练。假设已将图像划分为大小为3 X 3的网格,且总共只有3个类别,分别是行人(c1)、汽车(c2)和摩托车(c3)。因此,对于每个单元格,标签y将是一个八维向量:其中:pc定义对象是否存在于网格中(存在的概率);bx、by、bh、bw指定边界框;c1、c2、c3代表类别。如果检测对象是汽车,则c2位置处的值将为1,c1和c3处的值将为0;假设从上面的例子中选择第一个网格:由于此网格中没有对象,因此pc将为零,此网格的y标签将为:?意味着其它值是什么并不重要,因为网格中没有对象。下面举例另一个有车的网格(c2=1):在为此网格编写y标签之前,首先要了解YOLO如何确定网格中是否存在实际对象。大图中有两个物体(两辆车),因此YOLO将取这两个物体的中心点,物体将被分配到包含这些物体中心的网格中。中心点左侧网格的y标签会是这样的:由于此网格中存在对象,因此pc将等于1,bx、by、bh、bw将相对于正在处理的特定网格单元计算。由于检测出的对象是汽车,所以c2=1,c1和c3均为0。对于9个网格中的每一个单元格,都具有八维输出向量。最终的输出形状为3X3X8。使用上面的例子(输入图像:100X100X3,输出:3X3X8),模型将按如下方式进行训练:使用经典的CNN网络构建模型,并进行模型训练。在测试阶段,将图像传递给模型,经过一次前向传播就得到输出y。为了简单起见,使用3X3网格解释这一点,但通常在实际场景中会采用更大的网格(比如19X19)。即使一个对象跨越多个网格,它也只会被分配到其中点所在的单个网格。可以通过增加更多网格来减少多个对象出现在同一网格单元中的几率。如何编码边界框?如前所述,bx、by、bh和bw是相对于正在处理的网格单元计算而言的。下面通过一个例子来说明这一点。以包含汽车的右边网格为例:由于bx、by、bh和bw将仅相对于该网格计算。此网格的y标签将为:由于这个网格中有一个对象汽车,所以pc=1、c2=1。现在,看看如何决定bx、by、bh和bw的取值。在YOLO中,分配给所有网格的坐标都如下图所示:bx、by是对象相对于该网格的中心点的x和y坐标。在例子中,近似bx=0.4和by=0.3:bh是边界框的高度与相应单元网格的高度之比,在例子中约为0.9:bh=0.9,bw是边界框的宽度与网格单元的宽度之比,bw=0.5。此网格的y标签将为:请注意,bx和by将始终介于0和1之间,因为中心点始终位于网格内,而在边界框的尺寸大于网格尺寸的情况下,bh和bw可以大于1。非极大值抑制|Non-Max Suppression这里有一些思考的问题——如何判断预测的边界框是否是一个好结果(或一个坏结果)?单元格之间的交叉点,计算实际边界框和预测的边界框的并集交集。假设汽车的实际和预测边界框如下所示:其中,红色框是实际的边界框,蓝色框是预测的边界框。如何判断它是否是一个好的预测呢?IoU将计算这两个框的并集交叉区域:IoU =交叉面积/联合的面积;在本例中:IoU =黄色面积/绿色面积;如果IoU大于0.5,就可以说预测足够好。0.5是在这里采取的任意阈值,也可以根据具体问题进行更改。阈值越大,预测就越准确。还有一种技术可以显着提高YOLO的效果——非极大值抑制。对象检测算法最常见的问题之一是,它不是一次仅检测出一次对象,而可能获得多次检测结果。假设:上图中,汽车不止一次被识别,那么如何判定边界框呢。非极大值抑可以解决这个问题,使得每个对象只能进行一次检测。下面了解该方法的工作原理。1.它首先查看与每次检测相关的概率并取最大的概率。在上图中,0.9是最高概率,因此首先选择概率为0.9的方框:2.现在,它会查看图像中的所有其他框。与当前边界框较高的IoU的边界框将被抑制。因此,在示例中,0.6和0.7概率的边界框将被抑制:3.在部分边界框被抑制后,它会从概率最高的所有边界框中选择下一个,在例子中为0.8的边界框:4.再次计算与该边界框相连边界框的IoU,去掉较高IoU值的边界框:5.重复这些步骤,得到最后的边界框:以上就是非极大值抑制的全部内容,总结一下关于非极大值抑制算法的要点:丢弃概率小于或等于预定阈值(例如0.5)的所有方框;对于剩余的边界框:选择具有最高概率的边界框并将其作为输出预测;计算相关联的边界框的IoU值,舍去IoU大于阈值的边界框;重复步骤2,直到所有边界框都被视为输出预测或被舍弃;Anchor Boxes在上述内容中,每个网格只能识别一个对象。但是如果单个网格中有多个对象呢?这就行需要了解 Anchor Boxes的概念。假设将下图按照3X3网格划分:获取对象的中心点,并根据其位置将对象分配给相应的网格。在上面的示例中,两个对象的中心点位于同一网格中:上述方法只会获得两个边界框其中的一个,但是如果使用Anchor Boxes,可能会输出两个边界框!我们该怎么做呢?首先,预先定义两种不同的形状,称为Anchor Boxes。对于每个网格将有两个输出。这里为了易于理解,这里选取两个Anchor Boxes,也可以根据实际情况增加Anchor Boxes的数量:没有Anchor Boxes的YOLO输出标签如下所示:有Anchor Boxes的YOLO输出标签如下所示:前8行属于Anchor Boxes1,其余8行属于Anchor Boxes2。基于边界框和框形状的相似性将对象分配给Anchor Boxes。由于Anchor Boxes1的形状类似于人的边界框,后者将被分配给Anchor Boxes1,并且车将被分配给Anchor Boxes2.在这种情况下的输出,将是3X3X16大小。 因此,对于每个网格,可以根据Anchor Boxes的数量检测两个或更多个对象。结合思想在本节中,首先介绍如何训练YOLO模型,然后是新的图像进行预测。训练训练模型时,输入数据是由图像及其相应的y标签构成。样例如下:假设每个网格有两个Anchor Boxes,并划分为3X3网格,并且有3个不同的类别。因此,相应的y标签具有3X3X16的形状。训练过程的完成方式就是将特定形状的图像映射到对应3X3X16大小的目标。测试对于每个网格,模型将预测·3X3X16·大小的输出。该预测中的16个值将与训练标签的格式相同。前8个值将对应于Anchor Boxes1,其中第一个值将是该网络中对象的概率,2-5的值将是该对象的边界框坐标,最后三个值表明对象属于哪个类。以此类推。最后,非极大值抑制方法将应用于预测框以获得每个对象的单个预测结果。以下是YOLO算法遵循的确切维度和步骤:准备对应的图像(608,608,3);将图像传递给卷积神经网络(CNN),该网络返回(19,19,5,85)维输出;输出的最后两个维度被展平以获得(19,19,425)的输出量:19×19网格的每个单元返回425个数字;425=5 * 85,其中5是每个网格的Anchor Boxes数量;85= 5+80,其中5表示(pc、bx、by、bh、bw),80是检测的类别数;最后,使用IoU和非极大值抑制去除重叠框;YOLO算法实现本节中用于实现YOLO的代码来自Andrew NG的GitHub存储库,需要下载此zip文件,其中包含运行此代码所需的预训练权重。首先定义一些函数,这些函数将用来选择高于某个阈值的边界框,并对其应用非极大值抑制。首先,导入所需的库:import osimport matplotlib.pyplot as pltfrom matplotlib.pyplot import imshowimport scipy.ioimport scipy.miscimport numpy as npimport pandas as pdimport PILimport tensorflow as tffrom skimage.transform import resizefrom keras import backend as Kfrom keras.layers import Input, Lambda, Conv2Dfrom keras.models import load_model, Modelfrom yolo_utils import read_classes, read_anchors, generate_colors, preprocess_image, draw_boxes, scale_boxesfrom yad2k.models.keras_yolo import yolo_head, yolo_boxes_to_corners, preprocess_true_boxes, yolo_loss, yolo_body%matplotlib inline然后,实现基于概率和阈值过滤边界框的函数:def yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = .6): box_scores = box_confidencebox_class_probs box_classes = K.argmax(box_scores,-1) box_class_scores = K.max(box_scores,-1) filtering_mask = box_class_scores>threshold scores = tf.boolean_mask(box_class_scores,filtering_mask) boxes = tf.boolean_mask(boxes,filtering_mask) classes = tf.boolean_mask(box_classes,filtering_mask) return scores, boxes, classes之后,实现计算IoU的函数:def iou(box1, box2): xi1 = max(box1[0],box2[0]) yi1 = max(box1[1],box2[1]) xi2 = min(box1[2],box2[2]) yi2 = min(box1[3],box2[3]) inter_area = (yi2-yi1)(xi2-xi1) box1_area = (box1[3]-box1[1])(box1[2]-box1[0]) box2_area = (box2[3]-box2[1])(box2[2]-box2[0]) union_area = box1_area+box2_area-inter_area iou = inter_area/union_area return iou然后,实现非极大值抑制的函数:def yolo_non_max_suppression(scores, boxes, classes, max_boxes = 10, iou_threshold = 0.5): max_boxes_tensor = K.variable(max_boxes, dtype=‘int32’) K.get_session().run(tf.variables_initializer([max_boxes_tensor])) nms_indices = tf.image.non_max_suppression(boxes,scores,max_boxes,iou_threshold) scores = K.gather(scores,nms_indices) boxes = K.gather(boxes,nms_indices) classes = K.gather(classes,nms_indices) return scores, boxes, classes随机初始化下大小为(19,19,5,85)的输出向量:yolo_outputs = (tf.random_normal([19, 19, 5, 1], mean=1, stddev=4, seed = 1), tf.random_normal([19, 19, 5, 2], mean=1, stddev=4, seed = 1), tf.random_normal([19, 19, 5, 2], mean=1, stddev=4, seed = 1), tf.random_normal([19, 19, 5, 80], mean=1, stddev=4, seed = 1))最后,实现一个将CNN的输出作为输入并返回被抑制的边界框的函数:def yolo_eval(yolo_outputs, image_shape = (720., 1280.), max_boxes=10, score_threshold=.6, iou_threshold=.5): box_confidence, box_xy, box_wh, box_class_probs = yolo_outputs boxes = yolo_boxes_to_corners(box_xy, box_wh) scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = score_threshold) boxes = scale_boxes(boxes, image_shape) scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes, max_boxes, iou_threshold) return scores, boxes, classes使用yolo_eval函数对之前创建的随机输出向量进行预测:scores, boxes, classes = yolo_eval(yolo_outputs)with tf.Session() as test_b: print(“scores[2] = " + str(scores[2].eval())) print(“boxes[2] = " + str(boxes[2].eval())) print(“classes[2] = " + str(classes[2].eval()))score表示对象在图像中的可能性,boxes返回检测到的对象的(x1,y1,x2,y2)坐标,classes表示识别对象所属的类。现在,在新的图像上使用预训练的YOLO算法,看看其工作效果:sess = K.get_session()class_names = read_classes(“model_data/coco_classes.txt”)anchors = read_anchors(“model_data/yolo_anchors.txt”)yolo_model = load_model(“model_data/yolo.h5”)在加载类别信息和预训练模型之后,使用上面定义的函数来获取·yolo_outputs·。yolo_outputs = yolo_head(yolo_model.output, anchors, len(class_names))之后,定义一个函数来预测边界框并在图像上标记边界框:def predict(sess, image_file): image, image_data = preprocess_image(“images/” + image_file, model_image_size = (608, 608)) out_scores, out_boxes, out_classes = sess.run([scores, boxes, classes], feed_dict={yolo_model.input: image_data, K.learning_phase(): 0}) print(‘Found {} boxes for {}’.format(len(out_boxes), image_file)) # Generate colors for drawing bounding boxes. colors = generate_colors(class_names) # Draw bounding boxes on the image file draw_boxes(image, out_scores, out_boxes, out_classes, class_names, colors) # Save the predicted bounding box on the image image.save(os.path.join(“out”, image_file), quality=90) # Display the results in the notebook output_image = scipy.misc.imread(os.path.join(“out”, image_file)) plt.figure(figsize=(12,12)) imshow(output_image) return out_scores, out_boxes, out_classes接下来,将使用预测函数读取图像并进行预测:img = plt.imread(‘images/img.jpg’)image_shape = float(img.shape[0]), float(img.shape[1])scores, boxes, classes = yolo_eval(yolo_outputs, image_shape)最后,输出预测结果:out_scores, out_boxes, out_classes = predict(sess, “img.jpg”)以上就是YOLO算法的全部内容,更多详细内容可以关注darknet的官网。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 18, 2018 · 2 min · jiezi

二十分钟教你如何将区块链应用与函数计算相结合

前言本篇文章适合对区块链应用感兴趣或是想要通过函数计算服务进一步开发区块链应用的新人。本文将结合阿里云区块链服务、阿里云函数计算服务、阿里云日志服务 以及社区应用 Marbles,手把手教大家如何将阿里云区块链服务与阿里云函数计算服务相结合,并进一步提供业务上的结合场景,供大家开拓思路。本文分为以下几部分:函数计算与区块链Marbles 区块链应用介绍Marbles 区块链应用结合函数计算进行扩展示例区块链应用与函数计算在业务上结合的场景与价值探讨函数计算与区块链函数计算函数计算是事件驱动的全托管计算服务。使用函数计算,无需采购与管理服务器等基础设施,只需编写并上传代码。函数计算为用户准备好计算资源,弹性地可靠地运行任务,并提供日志查询、性能监控和报警等功能。借助函数计算,可以快速构建任何类型的应用和服务,并且只需为任务实际消耗的资源付费。下图为函数计算工作流程:区块链区块链可以理解为去中心的分布式记账系统,其是一种 分布式、去中心化的计算与存储架构 。区块链通过某种方式来记录数据,使用户可以信任区块链系统记录的数据。区块链中的记账节点会按照一致性协议记账。记账节点愿意按照一致性协议记账,是因为在一致性协议的设计中,诚实的记账节点会得到相应的奖赏,且诚实的记录比恶意篡改记录的收益更大。依托于区块链网络的可信度,衍生出了智能合约的概念。什么是智能合约呢?现实生活中,买家与卖家要进行一笔交易,为了保证交易的顺利进行,双方会签订一份合约,合约中会声明双方各自的身份、权利以及义务。当交易出现纠纷时,买家与卖家根据当时签订的合约通过法律的手段解决纠纷。这种方式的不足之处在于解决纠纷的过程需要第三方权威来仲裁以及需要大量时间。那么,假使我们现在有一位可信公正的交易代理人。卖家将商品交给代理人,买家与代理人双方之间一手交钱一手交货。若买家拒绝购买,代理人会将商品归还给买家。买家也不会付了钱拿不到商品。智能合约就可以充当这样的代理人,其为区块链上一个包含合约代码和存储空间的虚拟账户,合约的代码控制智能合约的行为,合约的账户存储合约的状态。由于有了智能合约,DApp (Decentralized Application 即去中心化应用)也应运而生。DApp 是运行在区块链网络上的应用软件,其上运行的代码我们称之为智能合约。Marbles 区块链应用介绍Marbles 区块链应用是一个 资产转移 应用演示。在 Marbles 区块链应用中多个用户可以创建并相互转移弹珠。 ( 即弹珠就是资产转移中的资产 )上图中:Amy、Alice、Ava 所在的小长方形是她们每个人的账户小长方形中的圆形弹珠是每个人账户中的资产,弹珠的颜色和大小是资产的属性点击小长方形中的加号是为某个账户创建弹珠(资产)将某个小长方形中的弹珠拖拽到右上方的垃圾桶中,是为某个账户删除弹珠(资产)将某个弹珠从一个小长方形拖拽到到另一个小长方形,是弹珠(资产)的转移每一步的操作会在下方的 BLOCKS 创建一个新的小正方形,这个小正方形就代表包含交易内容的区块Marbles 区块链应用代码分成三部分:链码 - 区块链网络中,对等节点所运行的代码。链码在此次介绍的 Marbles 应用中的主要作用是处理创建以及转移弹珠的逻辑。客户端 - 浏览器中运行的代码,负责 Marbles 应用页面的渲染与交互。服务端 - 服务器中运行的代码,充当 Marbles 应用与区块链网络之间的桥梁,其与客户端以及区块链网络中运行着链码的节点进行通信。在 Marbles 应用中,当客户端发送消息给服务端,服务端与区块链网络通信的时序图大体上如下图所示:( 读者对详细过程感兴趣,可以阅读 Github IBM-Blockchain/marbles )其中,Peer 节点(对等节点) 存在于区块链网络中,拥有账本并且可安装链码。Orderer 节点负责接收包含签名的交易,对未打包的交易进行排序生成区块,广播给 Peer 节点。上图中:Client 以及 Server 是上文中所说的客户端以及服务端。当用户创建或转移弹珠时,Client 客户端触发相应事件,并向 Server 服务端发起请求。Server 接收到事件信息后,首先会构建提案(也就是交易),提案是将事件信息进行封装,比如:此次交易的两方以及交易内容是什么。Server 将构建好的提案发送给区块链网络中的一个 Peer 节点,由 Peer 节点来对提案进行模拟,校验其合法性。为什么要这么做呢?因为链码的作用就是处理交易逻辑,而链码安装在 Peer 节点上,并没有安装在 Server 上。如果 Peer 节点模拟提案成功,认为其合法,则会对提案进行背书,并向 Server 返回背书后的提案。背书可以理解为,当我们去购买东西并忘记带现金,付给对方一张 别人 给的支票。对方怀疑支票不可兑现的时候,我们在支票上签字并表明若这张 他人 给的支票不能兑换,则可以来找我们要现金。这个签字的动作就是背书,声明对事物或被认可人的支持。Server 将背书后的提案发送给 Orderer 节点。Orderer 节点对提案进行排序并打包进区块,将区块广播给区块链网络中的所有 Peer 节点。Marbles 区块链应用结合函数计算进行扩展示例假设说,现在有这么一个业务场景,需要在每次 Marbles 应用有事件发生时,要对事件进行相应处理,且这部分的处理代码会 经常迭代 。那么,在区块链应用更新需要较多时间的情况下,通过函数计算来处理是较为方便的。( 具体缘由在下一节将会继续探讨 )接下来,笔者将带着大家模拟这个业务场景,扩展 Marbles Server 端代码。在每次事件发生时,将事件信息打包并通过函数计算的 HTTP 触发器,由函数计算来对事件信息做相应处理。1. 准备工作开通阿里云日志服务、函数计算服务、区块链服务2. 在阿里云区块链服务中创建组织、创建联盟3. 在阿里云区块链服务中创建通道点击相应组织点击通道点击添加通道,填写名称与组织,并创建点击新创建的通道右侧的待审批链接,同意审批4. 部署 Marbles 应用以及链码参考 阿里云区块链服务开发指南注意事项:nodejs 版本为 v85. 下载并配置阿里云函数计算开发工具 funfun 是一个 Node.js 编写的命令行工具,通过 npm 进行安装:$ npm install @alicloud/fun -g通过在命令行输入 fun config,根据提示依次配置 Account ID、Access Key Id、Access Key Secret 以及 Default Region Name。可参考:服务地址 、 创建 AccessKey配置 template.yml在项目根目录下创建一个 template.yml 文件:ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: marblesFC: # 服务名称 Type: ‘Aliyun::Serverless::Service’ Properties: Description: ‘fc test’ LogConfig: # 日志配置 Project: test-log-project # 日志 Project Logstore: test-log-store # 日志 LogStore processEvent: # 函数名 Type: ‘Aliyun::Serverless::Function’ Properties: Handler: httpTrigger.handler # 文件名.方法名 Runtime: nodejs8 CodeUri: ‘./’ Timeout: 60 Events: http-test: # 触发器名 Type: HTTP # 触发器类型 Properties: AuthType: ANONYMOUS Methods: [‘GET’, ‘POST’, ‘PUT’] test-log-project: # LogProject 名称 Type: ‘Aliyun::Serverless::Log’ Properties: Description: ‘just for test’ test-log-store: # LogStore 名称 Type: ‘Aliyun::Serverless::Log::Logstore’ Properties: TTL: 10 ShardCount: 1上述文件做了如下事项:在函数计算中创建了 marblesFC 服务为 marblesFC 服务配置了 test-log-project 日志 Project 以及 test-log-store 日志 LogStore在 marblesFC 服务中创建了 processEvent 函数,并为其设置入口函数为 httpTrigger.js 文件中的 handler 方法为 processEvent 函数配置了 HTTP 触发器,触发器名为 http-test创建了 test-log-project 日志 Project 以及 test-log-store 日志 LogStore在项目根目录下创建 httpTrigger.js 文件var getRawBody = require(‘raw-body’)module.exports.handler = function (request, response, context) { // get request info getRawBody(request, function (err, data) { var params = { path: request.path, queries: request.queries, headers: request.headers, method: request.method, body: data, url: request.url, clientIP: request.clientIP, } // you can deal with your own logic here console.log(JSON.stringify(params.queries)) // set response var respBody = new Buffer.from(JSON.stringify(params)); // var respBody = new Buffer( ) response.setStatusCode(200) response.setHeader(‘content-type’, ‘application/json’) response.send(respBody) })};在项目根目录下执行 fun deploy 进行部署在阿里云函数计算控制台中,进入到 marblesFC 服务下的 processEvent 函数,在代码执行页面记录下调用 HTTP 触发器的地址6. 修改 Marbles Server 端代码打开 Marbles 源代码根目录文件夹下的 app.js 文件添加 var https = require(‘https’); https模块修改 setupWebSocket 函数 function setupWebSocket() { console.log(’—————————————— Websocket Up ——————————————’); wss = new ws.Server({ server: server }); // start the websocket now wss.on(‘connection’, function connection(ws) { // – Process all websocket messages – // ws.on(‘message’, function incoming(message) { console.log(’ ‘); console.log(’——————————– Incoming WS Msg ——————————–’); logger.debug(’[ws] received ws msg:’, message); var data = null; try { data = JSON.parse(message); // it better be json } catch (e) { logger.debug(’[ws] message error’, message, e.stack); } // — [5] Process the ws message — // if (data && data.type == ‘setup’) { // its a setup request, enter the setup code logger.debug(’[ws] setup message’, data); startup_lib.setup_ws_steps(data); // <– open startup_lib.js to view the rest of the start up code } else if (data) { // its a normal marble request, pass it to the lib for processing https.get(“此处填写触发 HTTP 触发器的 URL 地址?type="+data.type, function(res){ console.log(’test http trigger’); }); ws_server.process_msg(ws, data); // <– the interesting “blockchainy” code is this way (websocket_server_side.js) } }); // log web socket errors ws.on(’error’, function (e) { logger.debug(’[ws] error’, e); }); // log web socket connection disconnects (typically client closed browser) ws.on(‘close’, function () { logger.debug(’[ws] closed’); }); // whenever someone connects, tell them our app’s state ws.send(JSON.stringify(ws_server.build_state_msg())); // tell client our app state }); // — Send a message to all connected clients — // wss.broadcast = function broadcast(data) { var i = 0; wss.clients.forEach(function each(client) { // iter on each connection try { logger.debug(’[ws] broadcasting to clients. ‘, (++i), data.msg); client.send(JSON.stringify(data)); // BAM, send the data } catch (e) { logger.debug(’[ws] error broadcast ws’, e); } }); }; ws_server.setup(wss, null); }7. 启动 Marbles在控制台中进入 Marbles 项目,通过 gulp marbles_baas 启动 Marbles 应用8. 创建弹珠或转移弹珠试着在浏览器中创建弹珠或者用鼠标拖拽转移弹珠9. 查看日志对 Marbles 应用做了相应操作后,进入阿里云日志服务 test-log-project Project 下的 test-log-store LogStore,查看日志我们看到当 Marbles Server 接收到事件后,会触发 HTTP 触发器,由函数计算 FC 来对事件做相应处理。( 简单起见,demo 的处理目前仅仅是记录日志,读者们可以自行扩展 )区块链应用与函数计算在业务上结合的场景与价值探讨通过制作上面的 demo,相信大家现在对于如何将区块链应用与函数计算相结合有了一定的认识。接下来,就让我们一起探讨下,区块链应用与函数计算在业务上有哪些结合的场景与价值。下图是最原始的 Fabric SDK 时序图为了让读者方便理解,我们仍旧使用 Marbles 应用的时序图,读者可以将 Marbles 应用中的 Server 理解为 Fabric SDK 时序图中的 Application,在笔者看来,函数计算可以与区块链应用相结合的场景主要有以下三点:1. 处理事件处理事件的场景刚刚在 demo 中各位读者想必已经体验过了。那么,为什么笔者倡导用函数计算来处理事件呢?通过函数计算可以将每一次的事件进行相应的处理,处理完成后发送给日志服务。同时,还可以在函数计算中设定定时触发器,在指定时间内,再次统计事件的信息,由此对区块链状态进行一个数据分析。而关于这种方式的统计逻辑,很有可能是需要经常迭代的。因此,不适合将其逻辑放入区块链应用中,而是更适合放在小巧易迭代的函数计算场景中。2. 附加业务考虑一个这样的场景:现在 X 公司推出了一款新的支付 App,为了鼓励用户使用该公司的 App,该公司对外宣传当用户用该公司的 App 成功完成交易后,该公司会送大量优惠券以及积分。若该 App 是一款区块链应用,那么把优惠活动的业务逻辑放在哪里最合适呢?笔者目前觉得是放在 Orderer 将交易打包成区块并广播的时候最合适,因为优惠活动的业务逻辑是经常会变化的,这类业务逻辑可以统称为附加业务,将附加业务抽象为一个个函数并放在函数计算中,容易更新迭代。3. 验证交易Peer 节点在模拟提案时,若是有复杂多变的逻辑,可以放入函数计算中,由 Peer 节点来负责调用。以上三点就是笔者对于如何将区块链服务与函数计算相结合的思考,有不准确的地方,欢迎大家指出。本文作者:泽尘阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 14, 2018 · 4 min · jiezi

函数计算性能福利篇(二) —— 业务冷启动优化

继前一篇《函数计算性能福利篇——系统冷启动优化》,我们再来看看近期函数计算推出的 Initializer 功能之后,带来的一波高能性能优化成果。背景函数计算是一个事件驱动的全托管 serverless 计算服务,用户可以将业务实现成符合函数计算编程模型的函数,交付给平台快速实现弹性高可用的云原生应用。用户函数调用链路包括以下几个阶段:系统为函数分配计算资源;下载代码;启动容器并加载函数代码;用户函数内部进行初始化逻辑;函数处理请求并将结果返回。其中前三步是系统层面的冷启动开销,通过对调度以及各个环节的优化,函数计算能做到负载快速增长时稳定的延时,细节详见 函数计算系统冷启动优化。第4步是函数内部初始化逻辑,属于应用业务层面的冷启动开销,例如深度学习场景下加载规格较大的模型、数据库场景下连接池构建、函数依赖库加载等等。为了减小应用层冷启动对延时的影响,函数计算推出了 initializer 接口,便于用户抽离业务初始化逻辑。这样用户就能将自身业务的初始化逻辑和请求处理逻辑分离,分别是实现在 initializer 接口和 handler 接口中,使得系统能识别用户函数的初始化逻辑,从而在调度上做相应的优化。Initializer 功能简介引入 initializer 接口的价值主要体现在如下几个方面:分离初始化逻辑和请求处理逻辑,程序逻辑更清晰,让用户更易写出结构良好,性能更优的代码;用户函数代码更新时,系统能够保证用户函数的平滑升级,规避应用层初始化冷启动带来的性能损耗。新的函数实例启动后能够自动执行用户的初始化逻辑,在初始化完成后再处理请求;在应用负载上升,需要增加更多函数实例时,系统能够识别函数应用层初始化的开销,更精准的计算资源伸缩的时机和所需的资源量,让请求延时更加平稳;即使在用户有持续的请求且不更新函数的情况下,FC系统仍然有可能将已有容器回收或更新,这时没有平台方(FC)的冷启动,但是会有业务方冷启动,Initializer可以最大限度减少这种情况;具体的 Initializer 功能设计和使用指南,请参考官方 Initiliazer 介绍 。初始化场景性能对比上一节已经简单了概括了 Initializer 的功能,这里,我们具体展示一下初始化场景下 Initializer 带来的巨大的性能提升效应。函数实现初始化应用场景,如果不使用 initializer,那么函数的主要实现方式应该是 Global variable 方式,下面提供两种实现方式的 demo ,仅供参考,下面的性能测试也是对比这两种函数实现方式进行了。使用 global variables 实现业务层初始化逻辑:– coding: utf-8 –import timeimport jsonisInit = Falsedef init_handler(): time.sleep(30) global isInit isInit = Truedef handler(event, context): evt = json.loads(event) funcSleepTime = evt[‘funcSleepTime’] if not isInit: init_handler()time.sleep(funcSleepTime)使用 initializer 的编程模型实现业务层初始化逻辑:– coding: utf-8 –import timeimport jsondef init_handler(context): time.sleep(30)def handler(event, context): evt = json.loads(event) funcSleepTime = evt[‘funcSleepTime’] time.sleep(funcSleepTime)两个 function 的逻辑相同:函数实例运行时,先执行 init_handler 逻辑,执行时间 30s,进行业务层初始化;如果已经初始化,那么就执行 handler 逻辑,执行时间 0.1s,进行请求处理;如果没有初始化,那么先进行初始化逻辑,再执行 handler 逻辑。场景对比这里根据生产用户请求场景,我们选择如下三种测试 case 来对比两种初始化函数实现的性能。负载持续增加模式波峰 burst 模式业务逻辑升级模式测试函数的特性如下:函数 handler 逻辑运行时间为 100ms;函数 初始化 逻辑运行时间为 30s;函数代码包大小为 50MB;runtime 为 python2.7;Memory 为 3GB 。这样的函数,系统层冷启动时间大约在 1s 左右,业务层冷启动在 30s,而函数自身请求执行时间为100-130ms。负载持续增加模式该模式下,用户的请求在一段时间内会持续增长。设计请求行为如下:每波请求并发数翻倍递增: 1, 2, 4, 8, 16, 32;每波请求的时间间隔为 35s。TPS情况如下,增长率为100%:注意:忽略第一批请求的完全冷启动的延时影响。不使用 initializer 实现的运行结果:从每波请求的请求延时可以看出,虽然系统层的调度能够为后来的骤增的请求分配更多的函数实例,但是因为函数实例都没有执行过业务层的初始化逻辑,所以新的函数实例花费了大量的执行时间在初始化逻辑的执行上,所以看到 99th latency 都大于 30s 。实际上,系统层的调度优化在这样长时间的初始化场景中并起不了作用。使用 initializer 实现的运行结果,可以看到使用 initializer 功能之后,请求增长率在 100% 的情况下不会再有函数实例执行初始化逻辑,相对于优化前,99th latency 下降了 30 倍以上。波峰 burst 模式波峰burst模式是指用户请求比较平稳,但是会有突然的波峰流量场景。设计请求行为如下:每波请求时间间隔 35s;每波平稳请求数 2;burst 请求数 18;TPS请求如下,burst 流量猛增 9 倍:注意:忽略第一批请求的完全冷启动的延时影响。不使用 initializer 实现的运行结果:使用 initializer 实现的运行结果,对于 burst 的流量,基本能够将 latency 的增长控制在 函数处理逻辑 6 倍以内,99th 的 latency 被优化到原来的 2.9% 。 业务逻辑升级模式业务逻辑升级模式是指用户请求比较平稳,但是用户函数会持续 UpdateFunction,变更业务逻辑,进行用户业务升级。设计请求行为如下:每波请求时间间隔 35s;每波平稳请求数 2;每 6 波请求进行一次 UpdateFunction 操作;TPS 如下:注意:忽略第一批请求的完全冷启动的延时影响。不使用 initializer 实现的运行结果,这个时候请求又会重新执行一次初始化逻辑,导致毛刺出现。使用 initializer 实现的运行结果,基本看出,UpdateFunction 操作对请求已经没有影响,业务层无感知。总结综上数据分析,函数计算的 Initializer 功能极大的优化了业务层冷启动的毛刺影响:在用户请求存在明显 burst 或者在以一定速率增长的情况下,能够极大的缓解性能影响,如上,在负载持续增加模式和波峰模式场景下,请求平均 latency 仅仅增加 3 倍,99th latency 只增加了 5 倍,99th latency 仅为优化前的 2.9% ,整整下降了 33 倍之多。在用户有持续的请求且不更新函数的情况下,优化之后更新函数,业务层能够做到无感知,平滑热升级。Initializer 功能对业务层冷启动的优化,又一次大大改善了函数计算在延时敏感场景下的表现! ...

December 13, 2018 · 1 min · jiezi

函数计算工具链新成员 —— Fun Local 发布啦

刚刚,我们发布了函数计算工具链的新成员,Fun Local。欢迎大家使用!如果你还不了解 Fun 是什么,我们来简单解释下。Fun 是什么Fun 是 have Fun with Serverless 的缩写,是一款 Serverless 应用开发的工具,可以帮助用户定义函数计算、API 网关、日志服务等资源。Fun 的更多内容 参考。Fun Local 是什么不久前发布的 Fun 新版本,已经在解决 Serverless 应用管理、交付、移植等场景做出了较多的努力。但在 Serverless 应用开发、调试方面,还有一些欠缺。为了补齐这块短板,Fun Local 应运而生。Fun Local 作为 Fun 的一个子命令存在,只要 Fun 的版本大于等于 2.6.0,即可以直接通过 fun local 命令使用。Fun Local 工具可以将函数计算中的函数在本地完全模拟运行,并提供单步调试的功能,旨在弥补函数计算相对于传统应用开发、调试体验上的短板,并为用户提供一种解决线上问题排查的新途径。今天,我们隆重的向大家宣布,内置了 Fun Local 命令的 Fun 2.6.0 正式发布啦!Fun Local 在本地开发、本地调试上添加了大量的新特性:支持本地运行函数支持本地单步调试函数支持本地事件触发函数本地开发时,支持环境变量本地开发时支持 Initializer本地开发时支持 Credentials单步调试时支持展示 IDE 配置新功能均支持所有运行时环境等等…为了让大家能够尽快上手 Fun 家族的新伙伴——Fun Local,我们提供了一个文章系列,包含了 Fun Local 基本使用、技巧、实战等各方面的内容。且该系列的文章还在持续增加中。开发函数计算的正确姿势 —— 使用 Fun Local 本地运行与调试https://yq.aliyun.com/article…开发函数计算的正确姿势 —— 爬虫https://yq.aliyun.com/article…开发函数计算的正确姿势 —— 排查超时问题https://yq.aliyun.com/article…如果想追踪 Fun Local 的最新动态,请关注 github repo 以及 云栖社区。未来展望Fun Local 对我们来说,是在 Fun 2.0 的基础上,又向前迈出了一小步。刚刚发布的 Fun Local 还有很多需要完善的地方,我们会在以下几点作出改进:支持 Api 网关本地运行、调试功能支持 Http Trigger 本地运行、调试功能支持 NAS 本地运行、调试功能等等 ...

December 13, 2018 · 1 min · jiezi

开发函数计算的正确姿势 —— 爬虫

在 《函数计算本地运行与调试 - Fun Local 基本用法》 中,我们介绍了利用 Fun Local 本地运行、调试函数的方法。但如果仅仅这样简单的介绍,并不能展现 Fun Local 对函数计算开发的巨大效率的提升。这一次,我们拿一个简单的场景来举例子——开发一个简单的爬虫函数(代码参考函数计算控制台模板),介绍如何以正确姿势,从零开始,开发一个自动伸缩、按调用次数收费的 serverless 爬虫应用。开发步骤我们将这个完整的应用拆分成多步,并且在每一步完成后,我们都会进行相应的运行验证。创建 Fun 项目首先,我们创建一个名为 image-crawler 的目录作为项目的根。然后在该目录下创建一个名为 template.yml 的文件,内容为:ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: localdemo:Type: ‘Aliyun::Serverless::Service’Properties: Description: ’local invoke demo’image-crawler: Type: ‘Aliyun::Serverless::Function’ Properties: Handler: index.handler CodeUri: code/ Description: ‘Hello world with python2.7!’ Runtime: python2.7如果不了解 Fun 定义的 Serverless Application Model,可以参考 这里。操作完成后,我们的项目目录结构如下:.└── template.yml编写 helloworld 函数代码在根目录下创建一个名为 code 的目录,并在该目录下创建一个名为 index.py 的文件,内容为一个简单的 helloworld 函数:def handler(event, context):return ‘hello world!‘在项目根目录下执行:fun local invoke image-crawler函数运行成功:操作完成后,我们的项目目录结构如下:.├── code│ └── index.py└── template.yml事件触发函数运行我们简单修改第 2 步的代码,将 event 打印到 log 中。import logginglogger = logging.getLogger()def handler(event, context):logger.info(“event: " + event)return ‘hello world!‘通过触发事件的方式运行函数,得到如下结果:可以看到,我们的函数已经能正确接收到触发事件了。Fun Local 更多帮助信息,参考。获取网页源码内容接下来,我们添加获取网页内容的代码。import loggingimport jsonimport urlliblogger = logging.getLogger()def handler(event, context):logger.info(“event: " + event)evt = json.loads(event)url = evt[‘url’]html = get_html(url)logger.info(“html content length: " + str(len(html)))return ‘Done!‘def get_html(url):page = urllib.urlopen(url)html = page.read()return html代码逻辑比较简单,我们这里直接使用了 urllib 库,读取网页内容。运行函数,得到以下输出:解析网页中的图片我们打算通过正则解析网页中包含的 jpg 图片,因此这一步会比较繁琐,因为涉及到正则表达式的微调。为了能快速的解决问题,我们决定利用 fun local 提供的 local debugging 解决问题。local debugging 方法参考: 《函数计算本地运行与调试 - Fun Local 基本用法》。首先,我们在下面这一行下个断点:logger.info(“html content length: " + str(len(html)))然后以 debug 的方式启动,vscode 调试器连接后,函数会继续运行到我们断点的这一行:我们可以直接在 Locals 一栏看到本地变量,其中包含了 html 这个变量,也就是我们获取到的 html 源码。我们可以将它的值复制出来,分析下,然后设计正则表达式。我们可以先写一个简单的,比如可以是 http://1*.jpg。怎么快速校验这段代码的正确性呢?我们可以利用调试器提供的 Watch(监视) 功能。创建一个 Watch 变量,将下面的值输入进去:re.findall(re.compile(r’http://2*.jpg’), html)回车后,即可看到代码的执行效果:这里一般不太容易一次写对,可以反复修改正则测试,直到正确为止。我们得到的正确的图片解析的逻辑添加到代码中:reg = r’http://3*.jpg’imgre = re.compile(reg)def get_img(html):return re.findall(imgre, html)然后在 handler 方法中调用即可:def handler(event, context):logger.info(“event: " + event)evt = json.loads(event)url = evt[‘url’]html = get_html(url)img_list = get_img(html)logger.info(img_list)return ‘Done!‘编写完成后,可以继续本地执行,验证下结果:echo ‘{“url”: “https://image.baidu.com/search/index?tn=baiduimage&word=%E5%A3%81%E7%BA%B8"}' | fun local invoke image-crawler可以看到,img_list 已经输出到控制台了:将图片上传到 oss解析到的图片,我们选择使用 oss 存储。首先,我们需要通过环境变量配置 OSS Endpoint 以及 OSS Bucket。在 template 中配置环境变量(需提前创建好 oss bucket):EnvironmentVariables:OSSEndpoint: oss-cn-hangzhou.aliyuncs.comBucketName: fun-local-test然后就可以直接在函数中获取到这两个环境变量了:endpoint = os.environ[‘OSSEndpoint’]bucket_name = os.environ[‘BucketName’]另外,fun local 运行函数时,还会提供一个额外的变量用来标识这是一个本地运行的函数。通过这个标识,我们可以用来做一些本地化的操作,比如我们可以在线上运行时连接 RDS,在本地运行时连接 Mysql。这里,我们用该标识以不同的的方式创建 oss client,原因是线上运行时,通过 credentials 获取到的是扮演角色的临时 ak,有有效期限制,而本地运行时,没有该限制。oss 提供了这两种方式的构造方法,我们直接使用即可:creds = context.credentialsif (local):auth = oss2.Auth(creds.access_key_id, creds.access_key_secret)else:auth = oss2.StsAuth(creds.access_key_id, creds.access_key_secret, creds.security_token) bucket = oss2.Bucket(auth, endpoint, bucket_name)接着我们遍历所有图片,将所有的图片上传到 oss:count = 0for item in img_list:count += 1logging.info(item)# Get each picturepic = urllib.urlopen(item)# Store all the pictures in oss bucket, keyed by timestamp in microsecond unitbucket.put_object(str(datetime.datetime.now().microsecond) + ‘.png’, pic) 再在本地运行一下函数:echo ‘{“url”: “https://image.baidu.com/search/index?tn=baiduimage&word=%E5%A3%81%E7%BA%B8"}' | fun local invoke image-crawler可以从日志看到,图片被一张一张的解析出来,并被上传到 oss 上了。登陆 oss 控制台,可以看到这些图片。部署本地开发完成后,我们还需要将其发布到线上,让其成为一个可被调用的服务。以往,你可能觉得比较麻烦,比如要登陆控制台,创建服务、创建函数、配置环境变量,创建角色等,现在有了 fun 后,这一切都不需要了。不过,本地与线上还是有些区别的,那就是要授权函数计算能够访问 OSS,怎么做呢?很简单,在我们的 template 中加入一行配置即可(Polices 文档,可以参考):Policies: AliyunOSSFullAccess添加后的 template.yml 内容如下:ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: localdemo:Type: ‘Aliyun::Serverless::Service’Properties: Description: ’local invoke demo’ Policies: AliyunOSSFullAccessimage-crawler: Type: ‘Aliyun::Serverless::Function’ Properties: Handler: index.handler CodeUri: code/ Description: ‘Hello world with python2.7!’ Runtime: python2.7 EnvironmentVariables: OSSEndpoint: oss-cn-hangzhou.aliyuncs.com BucketName: fun-local-test然后,使用 fun deploy 后,可以看到部署成功的日志。验证通过控制台验证登陆控制台,可以看到,我们的服务、函数、代码、环境变量等都已经就绪了。在触发事件中,写入我们用来测试的 json,然后执行:可以发现,会获得与本地一致的效果:通过 fcli 验证fcli 帮助文档 参考。在终端执行以下命令,可以获取函数列表:fcli function list –service-name localdemo可以看到我们的 image-crawler 已经创建成功了。{ “Functions”: [“image-crawler”,“java8”,“nodejs6”,“nodejs8”,“php72”,“python27”,“python3”], “NextToken”: null}使用以下命令则可以调用函数运行:fcli function invoke –service-name localdemo --function-name image-crawler --event-str ‘{“url”: “https://image.baidu.com/search/index?tn=baiduimage&word=%E5%A3%81%E7%BA%B8"}'运行成功后,会得到与控制台与 fun local 一致的结果。小结至此,我们的开发就算告一段落。本文利用 fun local 提供的本地运行、调试的能力,做到了在本地开发函数,并且通过反复的执行函数得到反馈以便于快速迭代代码。在本地开发完成后,不需要对代码进行任何修改,通过 fun deploy 命令,一键部署到云端,达到预期的效果。本文介绍的方法,并不是开发函数计算的唯一方式。本文的目的,是能够向开发者传达一种信号——开发函数计算时,只要身姿正确,就会非常享受,开发流程也会十分顺畅。祝您使用愉快。s,” ↩s,” ↩s,” ↩ ...

December 13, 2018 · 2 min · jiezi

开源 serverless 产品原理剖析(二) - Fission

摘要: Fission 是由私有云服务提供商领导开源的 serverless 产品,它借助 kubernetes 灵活强大的编排能力完成容器的管理调度工作,而将重心投入到 FaaS 功能的开发上,其发展目标是成为 AWS lambda 的开源替代品。背景本文是开源 serverless 产品原理剖析系列文章的第二篇,关于 serverless 背景知识的介绍可参考文章开源 serverless 产品原理剖析(一) - Kubeless,这里不再赘述。Fission 简介Fission 是由私有云服务提供商 Platform9 领导开源的 serverless 产品,它借助 kubernetes 灵活强大的编排能力完成容器的管理调度工作,而将重心投入到 FaaS 功能的开发上,其发展目标是成为 AWS lambda 的开源替代品。从 CNCF 视角,fission 属于 serverless 平台型产品。核心概念Fission 包含 Function、Environment 、Trigger 三个核心概念,其关系如下图所示:Function - 代表用特定语言编写的需要被执行的代码片段。Environment- 用于运行用户函数的特定语言环境。Trigger - 用于关联函数和事件源。如果把事件源比作生产者,函数比作执行者,那么触发器就是联系两者的桥梁。关键组件Fission 包含 Controller、Router、Executor 三个关键组件:Controller - 提供了针对 fission 资源的增删改查操作接口,包括 functions、triggers、environments、Kubernetes event watches 等。它是 fission CLI 的主要交互对象。Router - 函数访问入口,同时也实现了 HTTP 触发器。它负责将用户请求以及各种事件源产生的事件转发至目标函数。Executor - fission 包含 PoolManager 和 NewDeploy 两类执行器,它们控制着 fission 函数的生命周期。原理剖析本章节将从以下几个方面介绍 fission 的基本原理:函数执行器 - 它是理解 fission 工作原理的基础。Pod 特化 - 它是理解 fission 如何根据用户源码构建出可执行函数的关键。触发器 - 它是理解 fission 函数各种触发原理的入口。自动伸缩 - 它是理解 fission 如何根据负载动态调整函数个数的捷径。日志处理 - 它是理解 fission 如何处理各函数日志的有效手段。本文所做的调研基于kubeless 0.12.0和k8s 1.13。函数执行器CNCF 对函数生命周期的定义如下图所示,它描绘了函数构建、部署、运行的一般流程。要理解 fission,首先需要了解它是如何管理函数生命周期的。Fission 的函数执行器是其控制函数生命周期的关键组件。Fission 包含 PoolManager 和 NewDeploy 两类执行器,下面分别对两者进行介绍。PoolManagerPoolmgr 使用了池化技术,它通过为每个 environment 维持了一定数量的通用 pod 并在函数被触发时将 pod 特化,大大降低了函数的冷启动的时间。同时,poolmgr 会自动清理一段时间内未被访问的函数,减少闲置成本。该执行器的原理如下图所示。此时,函数的生命周期如下:使用 fission CLI 向 controller 发送请求,创建函数运行时需要的特定语言环境。例如,以下命令将创建一个 python 运行环境。fission environment create –name python –image fission/python-envPoolmgr 定期同步 environment 资源列表,参考 eagerPoolCreator。Poolmgr 遍历 environment 列表,使用 deployment 为每个 environment 创建一个通用 pod 池,参考 MakeGenericPool。使用 fission CLI 向 controller 发送创建函数的请求。此时,controller 只是将函数源码等信息持久化存储,并未真正构建好可执行函数。例如,以下命令将创建一个名为 hello 的函数,该函数选用已经创建好的 python 运行环境,源码来自 hello.py,执行器为 poolmgr。fission function create –name hello –env python –code hello.py –executortype poolmgrRouter 接收到触发函数执行的请求,加载目标函数相关信息。Router 向 executor 发送请求获取函数访问入口,参考 GetServiceForFunction。Poolmgr 从函数指定环境对应的通用 pod 池里随机选择一个 pod 作为函数执行的载体,这里通过更改 pod 的标签让其从 deployment 中“独立”出来,参考 _choosePod。K8s 发现 deployment 所管理 pod 的实际副本数少于目标副本数后会对 pod 进行补充,这样便实现了保持通用 pod 池中的 pod 个数的目的。特化处理被挑选出来的 pod,参考 specializePod。为特化后的 pod 创建 ClusterIP 类型的 service,参考 createSvc。将函数的 service 信息返回给 router,router 会将 serviceUrl 缓存以避免频繁向 executor 发送请求。Router 使用返回的 serviceUrl 访问函数。请求最终被路由至运行函数的 pod。如果该函数一段时间内未被访问会被自动清理,包括该函数的 pod 和 service,参考 idleObjectReaper。NewDeployPoolmgr 很好地平衡了函数的冷启动时间和闲置成本,但无法让函数根据度量指标自动伸缩。NewDeploy 执行器实现了函数 pod 的自动伸缩和负载均衡,该执行器的原理如下图所示。此时,函数的生命周期如下:使用 fission CLI 向 controller 发送请求,创建函数运行时需要的特定语言环境。使用 fission CLI 向 controller 发送创建函数的请求。例如,以下命令将创建一个名为 hello 的函数,该函数选用已经创建好的 python 运行环境,源码来自 hello.py,执行器为 newdeploy,目标副本数在 1 到 3 之间,目标 cpu 使用率是 50%。fission fn create –name hello –env python –code hello.py –executortype newdeploy –minscale 1 –maxscale 3 –targetcpu 50Newdeploy 会注册一个 funcController 持续监听针对 function 的 ADD、UPDATE、DELETE 事件,参考 initFuncController。Newdeploy 监听到了函数的 ADD 事件后,会根据 minscale 的取值判断是否立即为该函数创建相关资源。minscale > 0,则立即为该函数创建 service、deployment、HPA(deployment 管理的 pod 会特化)。minscale <= 0,延迟到函数被真正触发时创建。Router 接收到触发函数执行的请求,加载目标函数相关信息。Router 向 newdeploy 发送请求获取函数访问入口。如果函数所需资源已被创建,则直接返回访问入口。否则,创建好相关资源后再返回。Router 使用返回的 serviceUrl 访问函数。如果该函数一段时间内未被访问,函数的目标副本数会被调整成 minScale,但不会删除 service、deployment、HPA 等资源,参考 idleObjectReaper。执行器比较实际使用过程中,用户需要从延迟和闲置成本两个角度考虑选择何种类型的执行器。不同执行器的特点如下表所示。执行器类型最小副本数延迟闲置成本Newdeploy0高非常低 - pods 一段时间未被访问会被自动清理掉。Newdeploy>0低中等 - 每个函数始终会有一定数量的 pod 在运行。Poolmgr0低低 - 通用池中的 pod 会一直运行。小结Fission 将函数执行器的概念暴露给了用户,增加了产品的使用成本。实际上可以将 poolmgr 和 newdeploy 技术相结合,通过创建 deployment 将特化后的 pod 管理起来,这样可以很自然地利用 HPA 来实现对函数的自动伸缩。Pod 特化在介绍函数执行器时多次提到了 pod 特化,它是 fission 将环境容器变成函数容器的奥秘。Pod 特化的本质是通过向容器发送特化请求让其加载用户函数,其原理如下图所示。一个函数 pod 由下面两种容器组成:Fetcher - 下载用户函数并将其放置在共享 volume 里。不同语言环境使用了相同的 fetcher 镜像,fetcher 的工作原理可参考代码 fetcher.go。Env - 用户函数运行的载体。当它成功加载共享 volume 里的用户函数后,便可接收用户请求。具体步骤如下:容器 fetcher 接收到拉取用户函数的请求。Fetcher 从 K8s CRD 或 storagesvc 处获取用户函数。Fetcher 将函数文件放置在共享的 volume 里,如果文件被压缩还会负责解压。容器 env 接收到加载用户函数的命令。Env 从共享 volume 中加载 fetcher 为其准备好的用户函数。特化流程结束,容器 env 开始处理用户请求。触发器前面的章节介绍了 fission 函数的构建、加载和执行的逻辑,本章节主要介绍如何基于各种事件源触发 fission 函数的执行。CNCF 将函数的触发方式分成了如下图所示的几种类别,关于它们的详细介绍可参考链接 Function Invocation Types。对于 fission 函数,最简单的触发方式是使用 fission CLI,另外还支持通过各种触发器。下表展示了 fission 函数目前支持的触发方式以及它们所属的类别。触发方式类别fission CLISynchronous Req/RepHTTP TriggerSynchronous Req/RepTime TriggerJob (Master/Worker)Message Queue Trigger1. nats-streaming2. azure-storage-queue3. kafka | Async Message Queue || Kubernetes Watch Trigger | Async Message Queue |下图展示了 fission 函数部分触发方式的原理:HTTP trigger所有发往 fission 函数的请求都会由 router 转发,fission 通过为 router 创建 NodePort 或 LoadBalancer类型的 service 让其能够被外界访问。除了直接访问 router,还可以利用 K8s ingress 机制实现 http trigger。以下命令将为函数 hello 创建一个 http trigger,并指定访问路径为/echo。fission httptrigger create –url /echo –method GET –function hello –createingress –host example.com该命令会创建如下 ingress 对象,可以参考 createIngress 深入了解 ingress 的创建逻辑。apiVersion: extensions/v1beta1kind: Ingressmetadata: # 该 Ingress 的名称 name: xxx …spec: rules: - host: example.com http: paths: - backend: # 指向 router service serviceName: router servicePort: 80 # 访问路径 path: /echoIngress 只是用于描述路由规则,要让规则生效、实现请求转发,集群中需要有一个正在运行的 ingress controller。想要深入了解 ingress 原理可参考系列文章第一篇中的 HTTP trigger 章节。Time trigger如果希望定期触发函数执行,需要为函数创建 time trigger。Fission 使用 deployment 部署了组件 timer,该组件负责管理用户创建的 timer trigger。Timer 每隔一段时间会同步一次 time trigger 列表,并通过 golang 中被广泛使用的 cron 库 robfig/cron 定期触发和各 timer trigger 相关联函数的执行。以下命令将为函数 hello 创建一个名为halfhourly的 time trigger,该触发器每半小时会触发函数 hello 执行一次。这里使用了标准的 cron 语法定义执行计划。fission tt create –name halfhourly –function hello –cron “*/30 * * * *“trigger ‘halfhourly’ createdMessage queue trigger为了支持异步触发,fission 允许用户创建消息队列触发器。目前可供选择的消息队列有 nats-streaming、azure-storage-queue、kafka,下面以 kafka 为例描述消息队列触发器的使用方法和实现原理。以下命令将为函数 hello 创建一个基于 kafka 的消息队列触发器hellomsg。该触发器订阅了主题 input 中的消息,每当有消息到达它便会触发函数执行。如果函数执行成功,会将结果写入主题 output 中,否则将结果写入主题 error 中。fission mqt create –name hellomsg –function hello –mqtype kafka –topic input –resptopic output –errortopic error Fission 使用 deployment 部署了组件 mqtrigger-kafka,该组件负责管理用户创建的 kafka trigger。它每隔一段时间会同步一次 kafka trigger 列表,并为每个 trigger 创建 1 个用于执行触发逻辑的 go routine,触发逻辑如下:消费 topic 字段指定主题中的消息;通过向 router 发送请求触发函数执行并等待函数返回;如果函数执行成功,将返回结果写入 resptopic 字段指定的主题中,并确认消息已被处理;否则,将结果写入 errortopic 字段指定的主题中。小结Fission 提供了一些常用触发器,但缺少对 CNCF 规范里提到的Message/Record Streams触发方式的支持,该方式要求消息被顺序处理;如果有其它事件源想要接入可以参考 fission 触发器的设计模式自行实现。自动伸缩K8s 通过 Horizontal Pod Autoscaler 实现 pod 的自动水平伸缩。对于 fission,只有通过 newdeploy 方式创建的函数才能利用 HPA 实现自动伸缩。以下命令将创建一个名为 hello 的函数,运行该函数的 pod 会关联一个 HPA,该 HPA 会将 pod 数量控制在 1 到 6 之间,并通过增加或减少 pod 个数使得所有 pod 的平均 cpu 使用率维持在 50%。fission fn create –name hello –env python –code hello.py –executortype newdeploy –minmemory 64 –maxmemory 128 –minscale 1 –maxscale 6 –targetcpu 50Fission 使用的是autoscaling/v1版本的 HPA API,该命令将要创建的 HPA 如下:apiVersion: autoscaling/v1kind: HorizontalPodAutoscalermetadata: labels: executorInstanceId: xxx executorType: newdeploy functionName: hello … # 该 HPA 名称 name: hello-${executorInstanceId} # 该 HPA 所在命名空间 namespace: fission-function …spec: # 允许的最大副本数 maxReplicas: 6 # 允许的最小副本数 minReplicas: 1 # 该 HPA 关联的目标 scaleTargetRef: apiVersion: extensions/v1beta1 kind: Deployment name: hello-${executorInstanceId} # 目标 CPU 使用率 targetCPUUtilizationPercentage: 50想了解 HPA 的原理可参考系列文章第一篇中的自动伸缩章节,那里详细介绍了 K8s 如何获取和使用度量数据以及目前采用的自动伸缩策略。小结和 kubeless 类似,fission 避免了将创建 HPA 的复杂细节直接暴露给用户,但这是以牺牲功能为代价的;Fission 目前提供的自动伸缩功能过于局限,只对通过 newdeploy 方式创建的函数有效,且只支持基于 cpu 使用率这一种度量指标(kubeless 支出基于 cpu 和 qps)。本质上是因为 fission 目前仍然使用的是 v1 版本的 HPA,如果用户希望基于新的度量指标或者综合多项度量指标可以直接使用 hpa-v2 提供的功能;目前 HPA 的扩容缩容策略是基于既成事实被动地调整目标副本数,还无法根据历史规律预测性地进行扩容缩容。日志处理为了能更好地洞察函数的运行情况,往往需要对函数产生的日志进行采集、处理和分析。Fission 日志处理的原理如下图所示。日志处理流程如下:使用 DaemonSet 在集群中的每个工作节点上部署一个 fluentd 实例用于采集当前机器上的容器日志,参考 logger。这里,fluentd 容器将包含容器日志的宿主机目录/var/log/和/var/lib/docker/containers挂载进来,方便直接采集。Fluentd 将采集到的日志存储至 influxdb 中。用户使用 fission CLI 查看函数日志。例如,使用命令fission function logs –name hello可以查看到函数 hello 产生的日志。小结目前,fission 只做到了函数日志的集中化存储,能够提供的查询分析功能非常有限。另外,influxdb 更适合存储监控指标类数据,无法满足日志处理与分析的多样性需求。函数是运行在容器里的,因此函数日志处理本质上可归结为容器日志处理。针对容器日志,阿里云日志服务团队提供了成熟完备的解决方案,欲知详情可参考文章面向容器日志的技术实践。总结在介绍完 fission 的基本原理后,不妨从以下几个方面将其和第一篇介绍的 kubeless 作一个简单对比。触发方式 - 两款产品都支持常用的触发方式,但 kubeless 相比 fission 支持的更全面,且更方便接入新的数据源。自动伸缩 - 两款产品的自动伸缩能力都还比较基础,支持的度量指标都比较少,且底层都依赖于 K8s HPA。函数冷启动时间 - fission 通过池化技术降低了函数冷启动时间,kubeless 在这一块并未作过多优化。高级功能 - fission 支持灰度发布、自定义工作流等高级功能,kubeless 目前还不支持。参考资料Fission ArchitectureHow to Develop a Serverless Application with FissionFUNCTION EXECUTORS本文作者:吴波bruce_wu阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 11, 2018 · 4 min · jiezi

将神经网络训练成一个“放大镜”

摘要: 想不想将神经网络训练成一个“放大镜”?我们就训练了一个这样炫酷的神经网络,点击文章一起看下吧!当我们网购时,我们肯定希望有一个贴近现实的购物体验,也就是说能够全方位的看清楚产品的细节。而分辨率高的大图像能够对商品进行更加详细的介绍,这真的可以改变顾客的购物体验,让顾客有个特别棒的购物之旅。idealo.de是欧洲领先的比价网站,也是德国电子商务市场最大的门户网站之一,在此基础上,我们希望能够在此基础上为用户提供一个用户友好、有吸引力的购物平台。在这里,我们利用深度学习来评估数百万酒店图像的美学层次和技术质量,另外,那些没有任何信息的、特别难看的小的产品图像对我们来说是无效的,因此需要想办法解决。购物网站上并不是所有的商店都能为顾客提供高质量的图像,相反,商家提供的图像特别小、分辨率特别低、质量也很低。为了向用户展示高质量的高分辨率图像,我们基于2018年的论文《图像超分辨率的RDN网络》,训练了一个特别先进的卷积神经网络。我们的目标很简单:拍摄一些特别小的图像,然后就像使用放大镜一样,对图像进行放大,并且还要保持高分辨率。本文对实现这一目标做了详细介绍,另外,具体实现的细节,请查看GitHub。总概与大多数深度学习项目一样,我们的深度学习项目主要有四个步骤:1.回顾前人对该项目所做的贡献。2.实施一个或多个解决方案,然后比较预先训练的版本。3.获取数据,训练并测试模型。4.针对训练和验证结果对模型进行改进和优化。具体来说,本文主要有以下几方面的内容:1.介绍模型训练的配置,如何评估模型的性能2. 查看早期训练和测试结果,了解从哪方面进行改进。3.指出后续需要探索的方向。训练与以往“标准”的监督深度学习任务不同,我们这个“放大镜”深度学习模型输出的不仅仅是类标签或一个分数,而是一整幅图像。这就意味着训练过程以及评估会跟以往略有不同,我们要输出的是原始高分辨率图像,为了更好的对模型进行评估,我们需要一种测量缩放输出图像“质量”的方法,该方法更详细的优缺点将在后面进一步做详细阐释。损失函数损失函数是用来评估神经网络的性能究竟如何,这个有很多方法可以评估。这个问题的本质为大家留下了创造力空间,如有些聪明的人会用高级特征和对抗网络。对于第一次迭代,我们使用标准方法:网络的超分辨率(SR)输出和高分辨率输出(HR)之间的像素均方差(MSE)。评估我们用峰值信噪比(PSNR)来评估输出图像的质量,峰值信噪比是基于两个图像之间的像素均方差(MSE)。由于峰值信噪比是最常用的评估输出图像质量的方法,因此我们也使用这一评估标准,以便将本文模型与其他模型作比较。开始我们在p2.xlarge AWS EC2实例上进行训练,直到验证损失函数收敛,训练结束,这大概需要90个周期(一个周期24小时),然后使用Tensorboard跟踪训练数据集及验证数据集的损失函数和PSNR值。如上图所示,左上角为在每个周期结束时,反向传播到神经网络上的训练损失函数。右上角为跟踪泛化性能的非训练数据及的损失。左下角为训练数据集的PSNR值。右下角为验证数据集的PSNR值。结果输出的结果如下所示,我们先看看模型的输出结果,再考虑如何对该模型进行改进。左侧是验证数据集中的整个图像,中间是卷积神经网络的输出提取图像块,右侧是使用标准过程将中间输出提取图像块按比例放大后的输出,这里使用了GIMP的图像缩放功能![LR图像(左),重建SR(中),GIMP基线缩放(右)。](https://upload-images.jianshu…这个结果肯定不是特别完美:蝴蝶的天线周围有些没必要的噪声,蝴蝶的颈部和背部的毛发及翅膀上有些斑点轮廓,神经网络的输出图像(中)看起来要比GIMP基线输出图像(右)更加清晰。结果分析为了进一步理解模型有哪些优缺点,我们需要从验证数据集中提取具有高PSNR值的图像块和具有低能量度值的图像块。不出所料,性能最佳的图像块是具有较多平坦区域的图像块,而较为复杂的图像块难以准确再现。因此,我们重点关注这些较复杂的图像块,以便对结果进行训练和评估。同样的,我们也可以使用热图(heatmap)突出显示原始HR图像和神经网络输出SR图像之间的误差,颜色较暗的部分对应于较高的像素均方误差(较差的结果),颜色较浅的部分对应于较低的像素均方误差(或较好的结果)我们可以看到,具有多种模式的区域的误差会更大,但是看起来“更简单”的过渡区域则是相当黑暗的(例如云、天空),这是可以改进的,因为它与idealo的目录用例相关。浅谈深度学习任务中的非真实数据与常见的分类问题或输出为一个分值的监督式深度学习任务不同,我们用于评估神经网络输出的真实数据是原始HR图像。这既有好处,也有坏处。坏处:像Keras这样的当前较为流行的深度学习框架没有预先制定训练解决方案,比如生成器。实际上,它们通常依赖于从一维数组中获取训练和验证标签或文件,或者是直接从文件结构中派生出来的,这会涉及到一些额外的编码算法。好处:没有必要花太多时间来获得标签,给出一个HR图像池,我们可以对其进行简单的缩小,获得我们所需要的LR训练数据,并使用原始HR图像来评估损失函数通常来说,使用图像数据对神经网络进行训练时,需要从训练数据集中随机的选择多个图像来创建训练批次。然后将这些尺寸重新缩小到一个较小的尺寸,一般来说,大小约为100×100像素。我们随时使用随机变换对图像进行增强,并反馈到神经网络中。在这种情况下,没有必要向神经网络反馈整张图像,并且这也非常不可取。这是因为,我们不能将图像重新缩放到100×100的小像素点。毕竟,我们想要对图像进行放大。同时,我们也无法用较大尺寸的图像进行训练,比如大小为500×600像素的图像,因为处理这种大图像需要很长的时间。相反,我们可以从整个图像中提取一个非常小的随机色块,比如大小为16×16像素块,这样一来,我们就有了更多的数据点,因为每个图像都可以提供数百个不同的色块。我们之所以能够处理这种小色块,是因为我们不需要将一堆图像进行分类,比如:腿+尾巴+胡须+死老鼠=猫。因此,模型的末端就没有全连接层。我们只需要使用神经网络来构建这些模式的抽象表示,然后学习如何对其进行放大,除此以外,还要对块进行重新组合,使组合后的图像变得有意义。这种抽象表示由卷积层和放大层来完成,其中,卷积层是该网络中唯一的一种层类型。我们还要说明的是,全卷积结构使该网络的输入大小相互独立。也就是说,这意味着它与普通的分类卷积神经网络有所不同,你可以向完全卷积神经网络中输入任何大小的图像:无论输入图像原始大小是什么,网络都会输入一个输入图像大小2倍的图像。有关图像超分辨率的RDN网络更加详细的介绍,请查看文末链接。另一方面,我们还需要思考如何从图像中提取这些块。思路如下:从数据集中提取出n个随机图像,然后从每个图像中提取p个随机快。我们尝试了几种方法,如下图所示:首先,从一个均匀的网格中提出块,并创建一个完整的块数据集。在训练的时候,我们随机的提取其batch_size,并对其进行放大,反馈给网络。这种方法的缺点是需要静态的存储非常大的数据集,如果要用云服务器进行训练,这种方法其实并不理想:移动和提取数据集是一项相当耗时的操作,并且具有确定性定义的数据集可能并不是最佳数据集。另一种方法是随机选择batch_size大小的图像,并从中提取单个块。这种方法需要从磁盘中读取数据,这就大大降低了训练时间(我们设置的每个训练时间为15min-1h)。最后,我们将原始数据集中随机提取的单个图像块进行融合,并从中提取动态的batch_size块,这不仅能存储原始数据集,同时,也能保持较快的训练速度。拓展这是放大idealo网站产品目录的第一步,我们已经完成了。下面是我们将产品图像中低质量、低分辨率的图像进行放大,并输出。从上图中,我们可以看到,图像中较为平坦的地方会产生较为明显的噪声,文本也会略有失真。这就是我们计划要改进的地方。在下一步的探索中,我们将在自己的产品图像数据集上对神经网络进行训练。相关链接Github: Image Super ResolutionPaper: Residual Dense Network for Image Super-Resolution (Zhang et al. 2018)Dataset: DIVerse 2K resolution high quality images本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 10, 2018 · 1 min · jiezi

傻瓜神经网络入门指南

摘要: 现在网络上充斥着大量关于神经网络的消息,但是,什么是神经网络?其本质到底是什么?用几分钟阅读完这篇文章,我不能保证你能够成为这个领域的专家,不过你已经入门了。现在网络上充斥着大量关于神经网络的消息,但是,什么是神经网络?其本质到底是什么?你是不是对这个熟悉又陌生的词感到困惑?用几分钟阅读完这篇文章,我不能保证你能够成为这个领域的专家,但可以保证的是,你已经入门了。什么是神经网络?想要透彻的了解神经网络,我们首先要知道什么是机器学习。为了更好的理解机器学习,我们首先谈谈人的学习,或者说什么是“经典程序设计”。在经典的程序设计中,作为一名开发人员,我需要了解所要解决的问题的各个方面,以及我要以什么规则为基础。举个例子来说,假设我要设计一个能够区别正方形和圆形的程序。处理方法则是编写一个可以检测到角的程序,然后计算角的数量。如果程序能检测到4个角,那么图形为正方形;如果角的个人为0,则为圆形。那么这个机器学习有何关系?一般来说,机器学习=从示例中学习。在机器学习中,该如何区别正方形和圆形呢?这时候,我们就要设计一个学习系统,将许多形状及类别不同的图形作为输入,然后我们希望机器能够自己学习形状及类别,然后识别出不同图形的不同特性。一旦机器学会了这些属性,我们就可以输入一个新的图形(机器以前没见过的图形),然后机器对这些图形进行分类。什么是神经网络?在神经网络中,神经元是一个很奇特的名字,比较类似于函数。在数学和计算机领域,函数可以接受某个输入,经过一系列的逻辑运算,输出结果。更重要的是,我们可以将神经元看做一个学习单元。因此,我们需要理解什么是学习单元,然后再了解神经网络的基本构建块,即神经元。为了更好的理解,假设我们试图理解博客文章中单词数量与人们实际从博客中读取单词数量之间的关系。请记住一点,在机器学习领域,我们从示例中学习。因此,我们用x表示机器收集到文章的单词数量,用y表示人们实际读到的单词数,它们之间的关系用f表示。然后,我只需要告诉机器(程序)我希望看到的关系(比如直线关系),机器再将会理解它所需要绘制的线。我在这里得到了什么?下次我想写一篇包含x个单词的文章时,机器可以根据对应关系f找到人们真正能阅读到的单词数y。那么,神经网络到底是什么?如果一个神经元是一个函数,那么神经网络就是一个函数网络,也就是说,我们有很多个这样的功能(比如学习单元),这些学习单元的输入和输出相互交织,相互之间也有反馈。作为一名神经网络的设计人员,我的主要工作就是:1.如何建模输入和输出?例如,如果输入是文本,我可以用什么建模?数字?还是向量?2.每个神经元有哪些功能?(它们是线性?还是指数?…)3.神经网络的架构是什么?(即哪个函数的输出是哪个函数的输入?)4.我可以用哪些通俗易懂的词来描述我的网络?一旦我回答了以上这些问题,我就可以向网络“展示”大量具有正确输入和输出的例子,神经网络学习后,当我再次输入一个新的输入时,神经网路就会有个正确的输出。神经网络的学习原理超出了本文索要描述的范围,想要了解更多内容,请点击这里。另外你可以去神经网络专题,来更透彻的了解神经网络。神经网络的学习是件永无止境时,这个领域的知识呈爆炸性增长,每时每刻都会有新的知识和内容更新。最后,我贴出来一些个人认为比较好的帖子供你学习:1.Gal Yona ——我最喜欢的博主之一。她的文章涵盖了核心技术解释到半哲学评论。2.Siraj Raval——拥有大量视频的YouTuber,从理论解释到动手实践教程,应有俱有!3.Christopher Olah ——一位充满激情和洞察力的研究员,他的博客涵盖了神经网络的基础到深入探索。4.Towards Data Science 是神经网络领域中最大的Medium出版物,建议你每天抽出几分钟的时间来阅读,你会获得意想不到的收获。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 6, 2018 · 1 min · jiezi

一起来看 rxjs

更新日志2018-05-26 校正2016-12-03 第一版翻译过去你错过的 Reactive Programming 的简介你好奇于这名为Reactive Programming(反应式编程)的新事物, 更确切地说,你想了解它各种不同的实现(比如 [Rx*], [Bacon.js], RAC 以及其它各种各样的框架或库)学习它比较困难, 因为比较缺好的学习材料(译者注: 原文写就时, RxJs 还在 v4 版本, 彼时社区对 RxJs 的探索还不够完善). 我在开始学习的时候, 试图找过教程, 不过能找到的实践指南屈指可数, 而且这些教程只不过隔靴搔痒, 并不能帮助你做真正了解 RxJs 的基本概念. 如果你想要理解其中一些函数, 往往代码库自带的文档帮不到你. 说白了, 你能一下看懂下面这种文档么:Rx.Observable.prototype.flatMapLatest(selector, [thisArg])按照将元素的索引合并的方法, 把一个 “observable 队列 " 中的作为一个新的队列加入到 “observable 队列的队列” 中, 然后把 “observable 队列的队列” 中的一个 “observable 队列” 转换成一个 “仅从最近的 ‘observable 队列’ 产生的值构成的一个新队列.“这是都是什么鬼?我读了两本书, 一本只是画了个大致的蓝图, 另一本则是一节一节教你 “如何使用 Reactive Libarary” . 最后我以一种艰难的方式来学习 Reactive Programming: 一遍写, 一遍理解. 在我就职于 Futurice 的时候, 我第一次在一个真实的项目中使用它, 我在遇到问题时, 得到了来自同事的支持.学习中最困难的地方是 以 Reactive(反应式) 的方式思考. 这意思就是, 放下你以往熟悉的编程中的命令式和状态化思维习惯, 鼓励自己以一种不同的范式去思考. 至今我还没在网上找到任何这方面的指南, 而我认为世界上应该有一个说明如何以 Reactive(反应式) 的方式思考的教程, 这样你才知道要如何开始使用它. 在阅读完本文后之后. 请继续阅读代码库自带的文档来指引你之后的学习. 我希望, 这篇文档对你有所帮助.“什么是 Reactive Programming(反应式编程)?“在网上可以找到大量对此糟糕的解释和定义. Wikipedia 的 意料之中地泛泛而谈和过于理论化. Stackoverflow 的 圣经般的答案也绝对不适合初学者. Reactive Manifesto 听起来就像是要给你公司的项目经理或者是老板看的东西. 微软的 Rx 术语 “Rx = Observables + LINQ + Schedulers” 也读起来太繁重, 太微软了, 以至于你看完后仍然一脸懵逼. 类似于 “reactive” 和 “propagation” 的术语传达出的含义给人感觉无异于你以前用过的 MV* 框架和趁手的语言已经做到的事情. 我们现有的框架视图当然是会对数据模型做出反应, 任何的变化当然也是要冒泡的. 要不然, 什么东西都不会被渲染出来嘛.所以, 让我们撇开那些无用的说辞, 尝试去了解本质.Reactive programming(反应式编程) 是在以异步数据流来编程当然, 这也不是什么新东西. 事件总线或者是典型的点击事件确实就是异步事件流, 你可以对其进行 observe(观察) 或者做些别的事情. 不过, Reactive 是比之更优秀的思维模型. 你能够创建任何事物的数据流, 而不只是从点击和悬浮事件中. “流” 是普遍存在的, 一切都可能是流: 变量, 用户输入, 属性, 缓存, 数据结构等等. 比如, 想象你的 Twitter 时间线会成为点击事件同样形式的数据流.熟练掌握该思维模型之后, 你还会接触到一个令人惊喜的函数集, 其中包含对任何的数据流进行合并、创建或者从中筛选数据的工具. 它充分展现了 “函数式” 的魅力所在. 一个流可以作为另一个流的输入. 甚至多个流可以作为另一个流的输入. 你可以合并两个流. 你可以筛选出一个仅包含你需要的数据的另一个流. 你可以从一个流映射数据值到另一个流.让我们基于 “流是 Reactive 的中心” 这个设想, 来细致地做看一下整个思维模型, 就从我们熟知的 “点击一个按钮” 事件流开始.每个流是一个按时序不间断的事件序列. 它可能派发出三个东西: (某种类型的)一个数值, 一个错误, 或者一个 “完成” 信号. 说到 “完成” , 举个例子, 当包含了这个按钮的当前窗口/视图关闭时, 也就是 “完成” 信号发生时.我们仅能异步地捕捉到这些事件: 通过定义三种函数, 分别用来捕捉派发出的数值、错误以及 “完成” 信号. 有时候后两者可以被忽略, 你只需定义用来捕捉数值的函数. 我们把对流的 “侦听” 称为订阅(subscribing), 我们定义的这三种函数合起来就是观察者, 流则是被观察的主体(或者叫"被观察者”). 这正是设计模式中的观察者模式.描述这种方式的另一种方式用 ASCII 字符来画个导图, 在本教程的后续的部分也能看到这种图形.–a—b-c—d—X—|->a, b, c, d 代表被派发出的值X 代表错误| 代表"完成"信号—> 则是时间线这些都是是老生常谈了, 为了不让你感到无聊, 现在来点新鲜的东西: 我们将原生的点击事件流进行变换, 来创建新的点击事件流.首先, 我们做一个计数流, 来指明一个按钮被点击了多少次. 在一般的 Reactive 库中, 每个流都附带了许多诸如map, filter, scan 等的方法. 当你调用这些方法之一(比如比如clickStream.map(f))时, 它返回一个基于 clickStream 的新的流. 它没有对原生的点击事件做任何修改. 这种(不对原有流作任何修改的)特性叫做immutability(不可变性), 而它和 Reactive(反应式) 这个概念的契合度之高好比班戟和糖浆(译者注: 班戟就是薄煎饼, 该称呼多见于中国广东地区. 此句意为 immutability 与 Reactive 两个概念高度契合). 这样的流允许我们进行链式调用, 比如clickStream.map(f).scan(g): clickStream: —c—-c–c—-c——c–> vvvvv map(c becomes 1) vvvv —1—-1–1—-1——1–> vvvvvvvvv scan(+) vvvvvvvvvcounterStream: —1—-2–3—-4——5–>map(f) 方法根据你提供的函数f替换每个被派发的元素形成一个新的流. 在上例中, 我们将每次点击都映射为计数 1. scan(g) 方法则在内部运行x = g(accumulated, current), 以某种方式连续聚合处理该流之前所有的值, 在该例子中, g 就是简单的加法. 然后, 一次点击发生时, counterStream 就派发一个点击数的总值.为了展示 Reactive 真正的能力, 我们假设你想要做一个 “双击事件” 的流. 或者更厉害的, 我们假设我们想要得到一个 “三击事件流” , 甚至推广到更普遍的情况, “多击流”. 现在, 深呼吸, 想象一下按照传统的命令式和状态化思维习惯要如何完成这项工作? 我敢说那会烦死你了, 它必须包含各种各样用来保持状态的变量, 以及一些对周期性工作的处理.然而, 以 Reactive 的方式, 它会非常简单. 事实上, 这个逻辑只不过是四行代码. 不过让我们现在忘掉代码.无论你是个初学者还是专家, 借助导图来思考, 才是理解和构建流最好的方法.图中的灰色方框是将一个流转换成另一个流的方法. 首先, 每经过 “250毫秒” 的 “事件静默” (简单地说, 这是在 buffer(stream.throttle(250ms)) 完成的. (现在先)不必担心对这点的细节的理解, 我们主要是演示下 Reactive 的能力.), 我们就得到了一个 “点击动作” 的列表, 即, 转换的结果是一个列表的流, 而从这个流中我们应用 map() 将每个列表映射成对应该队列的长度的整数值. 最后, 我们使用 filter(x >= 2) 方法忽略掉所有的 1. 如上: 这 3 步操作将产生我们期望的流. 我们之后可以订阅(“侦听”)它, 并按我们希望的处理方式处理流中的数据.我希望你感受到了这种方式的美妙. 这个例子只是一次不过揭示了冰山一角: 你可以将相同的操作应用到不同种类的流上, 比如 API 返回的流中. 除此以外, 还有许多有效的函数.“为什么我应该采用反应式编程?“Reactive Programming (反应式编程) 提升了你代码的抽象层次, 你可以更多地关注用于定义业务逻辑的事件之间的互相依赖, 而不必写大量的细节代码来处理事件. RP(反应式编程)的代码会更简洁明了.在现代网页应用和移动应用中, 这种好处是显而易见的, 这些场景下, 与数据事件关联的大量 UI 事件需要被高频地交互. 10 年前, 和 web 页面的交互只是很基础地提交一个长长的表单给后端, 然后执行一次简单的重新渲染. 在这 10 年间, App 逐渐变得更有实时性: 修改表单中的单个字段能够自动触发一次到后端的保存动作, 对某个内容的 “点赞” 需要实时反馈到其他相关的用户……现今的 App 有大量的实时事件, 它们共同作用, 以带给用户良好的体验. 我们要能简洁处理这些事件的工具, 而 Reactive Programming 方式我们想要的.举例说明如何以反应式编程的方式思考现在我们进入到实战. 一个真实的手把手教你如何以 RP(反应式编程) 的方式来思考的例子. 注意这里不是随处抄来的例子, 不是半吊子解释的概念. 到这篇教程结束为止, 我们会在写出真正的功能性代码的同时, 理解我们做的每个动作.我选择了 JavaScript 和 RxJS 作为工具, 原因是, JavaScript 是当下最为人熟知的语言, 而 [Rx*] 支持多数语言和平台 (.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy 等等). , 无论你的工具是什么, 你可以从这篇教程中收益.实现一个"建议关注"盒子在 Twitter 上, 有一个 UI 元素是建议你可以关注的其它账户.我们将着重讲解如何模仿出它的核心特性:在页面启动时, 从 API 中加载账户数据, 并展示三个推荐关注者在点击"刷新"时, 加载另外的三个推荐关注的账户, 形成新三行在点击一个账户的 “x” 按钮时, 清除该账户并展示一个新的每一行显示账户的头像和到他们主页的链接我们可以忽略其它的特性和按钮, 它们都是次要的. 另外, Twitter 最近关闭了非认证请求接口, 作为替代, 我们使用 [Github 的 API] 来构建这个关注别人 UI.(注: 到本稿的最新的校正为止, github 的该接口对非认证用户启用了一段时间内访问频次限制)如果你想尽早看一下完整的代码, 请点击[样例代码].请求和回复你如何用 Rx 处理这个问题?首先, (几乎) 万物皆可为流 .这是 “Rx 口诀”. 让我们从最容易的特性开始: “在页面启动时, 从 API 中加载账户数据”. 这没什么难得, 只需要(1) 发一个请求, (2) 读取回复, (3) 渲染回复的中的数据. 所以我们直接把我们我们的请求当做流. 一开始就用流也许颇有"杀鸡焉用牛刀"的意味, 但为了理解, 我们需要从基本的例子开始.在应用启动的时候, 我们只需要一个请求, 因此如果我们将它作为一个数据流, 它将会只有一个派发的值. 我们知道之后我们将有更多的请求, 但刚开始时只有一个.–a——|->其中 a 是字符串 ‘https://api.github.com/users'这是一个将请求的 URL 的流. 无论请求何时发生, 它会告诉我们两件事: 请求发生的时刻和内容. 请求执行之时就是事件派发之时, 请求的内容就是被派发的值: 一个 URL 字符串.创建这样一个单值流对 [Rx*] 来说非常简单, 官方对于流的术语, 是 “Observable”(可被观察者), 顾名思义它是可被观察的, 但我觉得这名字有点傻, 所以我称呼它为 流.var requestStream = Rx.Observable.just(‘https://api.github.com/users');但现在, 这只是一个字符串流, 不包含其他操作, 所以我们需要要在值被派发的时候做一些事情. 这依靠对流的订阅.requestStream.subscribe(function(requestUrl) { // 执行该请求 jQuery.getJSON(requestUrl, function(responseData) { // … });}注意我们使用了 jQuery Ajax 回调(我们假定你应已对此有了解)来处理请求操作的异步性. 但稍等, Rx 就是处理 异步 数据流的. 难道这个请求的回复不就是一个在未来某一刻会带回返回数据的流么? 从概念上讲, 它看起来就是的, 我们来尝试写一下.requestStream.subscribe(function(requestUrl) { // 执行该请求 var responseStream = Rx.Observable.create(function (observer) { jQuery.getJSON(requestUrl) .done(function(response) { observer.onNext(response); }) .fail(function(jqXHR, status, error) { observer.onError(error); }) .always(function() { observer.onCompleted(); }); }); responseStream.subscribe(function(response) { // 对回复做一些处理 });}Rx.Observable.create() 所做的是自定义一个流, 这个流会通知其每个观察者(或者说其"订阅者” )有数据产生 (onNext()) 或发生了错误 (onError()). 我们需要做的仅仅是包装 jQuery Ajax Promise. 稍等, 这难道是说 Promise 也是一个 Observable?是的. Observable 就是一个 Promise++ 对象. 在 Rx 中, 通过运行 var stream = Rx.Observable.fromPromise(promise) 你就可以把一个 Promise 转换成一个 Observable. 仅有的区别在于 Observables 不符合 Promises/A+ 标准, 但他们在概念上是不冲突的. 一个 Promise 就是一个仅派发一个值的 Observable. Rx 流就是允许多次返回值的 Promise.这个例子很可以的, 它展示了 Observable 是如何至少有 Promise 的能力. 因此如果你喜欢 Promise, 请注意 Rx Observable 也可以做到同样的事.现在回到我们的例子上, 也许你已经注意到了, 我们在一个中 subscribe() 调用了另一个 subscribe(), 这有点像回调地狱. 另外, responseStream 的创建也依赖于 requestStream. 但正如前文所述, 在 Rx 中有简单的机制来最流作变换并支持从其他流创建一个新的流, 接下来我们来做这件事.到目前为止, 你应该知道的对流进行变换的一个基础方法是 map(f), 将 “流 A” 中的每一个元素作 f()处理, 然后在 “流 B” 中生成一一对应的值. 如果我们这样处理我们的请求和回复流, 我们可以把请求 URL 映射到回复的 Promise (被当做是流) 中.var responseMetastream = requestStream .map(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });这下我们创建了一个叫做 元流 (流的流) 的奇怪的东西. 不必对此感到疑惑, 元流, 就是其中派发值是流的流. 你可以把它想象成 指针): 每个被派发的值都是对其它另一个流的 指针 . 在我们的例子中, 每个请求的 URL 都被映射为一个指针, 指向一个个包含 URL 对应的返回数据的 promise 流.这个元流看上去有点让人迷惑, 而且对我们根本没什么用. 我们只是想要一个简单的回复流, 其中每个派发的值都应是一个 JSON 对象, 而不是一个包含 JSON 对象的 Promise. 现在来认识 Flatmap: 它类似于 map(), 但它是把 “分支” 流中派发出的的每一项值在 “主干” 流中派发出来, 如此, 它就可以对元流进行扁平化处理.(译者注: 这里, “分支” 流指的是元流中每个被派发的值, “主干” 流是指这些值有序构成的流, 由于元流中的每个值都是流, 作者不得不用 “主干” 和 “分支” 这样的比喻来描述元流与其值的关系). 在此, Flatmap 并不是起到了"修正"的作用, 元流也并不是一个 bug, 相反, 它们正是 Rx 中处理异步回复流的工具.var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });漂亮. 因为回复流是依据请求流定义的, 设想之后有更多的发生在请求流中的事件, 不难想象, 就会有对应的发生在回复流中的的回复事件:requestStream: –a—–b–c————|->responseStream: —–A——–B—–C—|->(小写的是一个请求, 大写的是一个回复)现在我们终于得到了回复流, 我们就可以渲染接收到的数据responseStream.subscribe(function(response) { // 按你设想的方式渲染 response 为 DOM});整理一下到目前为止的代码, 如下:var requestStream = Rx.Observable.just(‘https://api.github.com/users');var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });responseStream.subscribe(function(response) { // 按你设想的方式渲染 response 为 DOM});刷新按钮现在我们注意到, 回复中的 JSON 是一个包含 100 个用户的列表. [Github 的 API] 只允许我们指定一页的偏移量, 而不能指定读取的一页中的项目数量, 所以我们只用到 3 个数据对象, 剩下的 97 个只能浪费掉. 我们暂时忽略这个问题, 之后我们会看到通过缓存回复来处理它.每次刷新按钮被点击的时候, 请求流应该派发一个新的 URL, 因此我们会得到一个新的回复. 我们需要两样东西: 一个刷新按钮的点击事件流(口诀: 万物皆可成流), 并且我们需要改变请求流以依赖刷新点击流. 好在, RxJs 拥有从事件监听器产生 Observable 的工具.var refreshButton = document.querySelector(’.refresh’);var refreshClickStream = Rx.Observable.fromEvent(refreshButton, ‘click’);既然刷新点击事件自身不带任何 API URL, 我们需要映射每次点击为一个实际的 URL. 现在我们将请求流改成刷新点击流, 这个流被映射为每次带有随机的偏移参数的、到 API 的请求.var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });如果我直接这样写, 也不做自动化测试, 那这段代码其实有个特性没实现. 即请求不会在页面加载完时发生, 只有当刷新按钮被点击的时候才会. 但其实, 两种行为我们都需要: 刷新按钮被点击的时候的请求, 或者是页面刚打开时的请求.两种场景下需要不同的流:var requestOnRefreshStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });var startupRequestStream = Rx.Observable.just(‘https://api.github.com/users');但我们如何才能"合并"这两者为同一个呢? 有一个 merge() 方法. 用导图来解释的话, 它看起来像是这样的.stream A: —a——–e—–o—–>stream B: —–B—C—–D——–> vvvvvvvvv merge vvvvvvvvv —a-B—C–e–D–o—–>那我们要做的事就变得很容易了:var requestOnRefreshStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });var startupRequestStream = Rx.Observable.just(‘https://api.github.com/users');var requestStream = Rx.Observable.merge( requestOnRefreshStream, startupRequestStream);也有另外一种更干净的、不需要中间流的写法:var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; }) .merge(Rx.Observable.just(‘https://api.github.com/users'));甚至再短、再有可读性一点:var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; }) .startWith(‘https://api.github.com/users');startWith() 会照你猜的那样去工作: 给流一个起点. 无论你的输入流是怎样的, 带 startWith(x) 的输出流总会以 x 作为起点. 但我这样做还不够 [DRY], 我把 API 字符串写了两次. 一种修正的做法是把 startWith() 用在 refreshClickStream 上, 这样可以从"模拟"在页面加载时一次刷新点击事件.var requestStream = refreshClickStream.startWith(‘startup click’) .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });漂亮. 如果你现在回头去看我说 “有个特性没实现” 的那一段, 你应该能看出那里的代码和这里的代码的区别仅仅是多了一个 startWith().使用流来建立"3个推荐关注者"的模型到现在为止, 我们只是写完了一个发生在回复流的 subscribe() 中的 推荐关注者 的 UI. 对于刷新按钮, 我们要解决一个问题: 一旦你点击了"刷新”, 现在的三个推荐关注者仍然没有被清理. 新的推荐关注者只在请求内回复后才能拿到, 不过为了让 UI 看上去令人舒适, 我们需要在刷新按钮被点击的时候就清理当前的推荐关注者.refreshClickStream.subscribe(function() { // 清理 3 个推荐关注者的 DOM 元素});稍等一下. 这样做不太好, 因为这样我们就有两个会影响到推荐关注者的 DOM 元素的 subscriber (另一个是 responseStream.subscribe()), 这听起来不符合 Separation of concerns. 还记得 Reactive 口诀吗?在 “万物皆可为流” 的指导下, 我们把推荐关注者构建为一个流, 其中每个派发出来的值都是一个包含了推荐关注人数据的 JSON 对象. 我们会对三个推荐关注者的数据分别做这件事. 像这样来写:var suggestion1Stream = responseStream .map(function(listUsers) { // 从列表中随机获取一个用户 return listUsers[Math.floor(Math.random()*listUsers.length)]; });至于获取另外两个用户的流, 即 suggestion2Stream 和 suggestion3Stream, 只需要把 suggestion1Stream 复制一遍就行了. 这不够 [DRY], 不过对我们的教程而言, 这样能让我们的示例简单些, 同时我认为, 思考如何在这个场景下避免重复编写 suggestion[N]Stream 也是个好的思维练习, 就留给读者去考虑吧.我们让渲染的过程发生在回复流的 subscribe() 中, 而是这样做:suggestion1Stream.subscribe(function(suggestion) { // 渲染第 1 个推荐关注者});回想之前我们说的 “刷新的时候, 清理推荐关注者”, 我们可以简单地将刷新单击事件映射为 “null” 数据(它代表当前的推荐关注者为空), 并且在 suggestion1Stream 做这项工作, 如下:var suggestion1Stream = responseStream .map(function(listUsers) { // 从列表中随机获取一个用户 return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) );在渲染的时候, 我们把 null 解释为 “没有数据”, 隐藏它的 UI 元素.suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // 隐藏第 1 个推荐关注者元素 } else { // 显示第 1 个推荐关注者元素并渲染数据 }});整个情景是这样的:refreshClickStream: ———-o——–o—-> requestStream: -r——–r——–r—-> responseStream: —-R———R——R–> suggestion1Stream: —-s—–N—s—-N-s–> suggestion2Stream: —-q—–N—q—-N-q–> suggestion3Stream: —-t—–N—t—-N-t–>其中 N 表示 null(译者注: 注意, 当 refreshClickStream 产生新值, 即用户进行点击时, null 的产生总是立刻发生在 refreshClickStream 之后; 而 refreshClickStream => requestStream => responseStream, responseStream 中的值, 是发给 API 接口的异步请求的结果, 这个结果的产生往往会需要花一点时间, 必然在 null 之后, 因此可以达到 “为了让 UI 看上去令人舒适, 我们需要在刷新按钮被点击的时候就清理当前的推荐关注者” 的效果).稍微完善一下, 我们会在页面启动的时候也会渲染 “空” 推荐关注人. 为此可以 startWith(null) 放在推荐关注人的流里:var suggestion1Stream = responseStream .map(function(listUsers) { // 从列表中随机获取一个用户 return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);最后我们得到的流:refreshClickStream: ———-o———o—-> requestStream: -r——–r———r—-> responseStream: —-R———-R——R–> suggestion1Stream: -N–s—–N—-s—-N-s–> suggestion2Stream: -N–q—–N—-q—-N-q–> suggestion3Stream: -N–t—–N—-t—-N-t–>关闭推荐关注人, 并利用已缓存的回复数据目前还有一个特性没有实现. 每个推荐关注人格子应该有它自己的 ‘x’ 按钮来关闭它, 然后加载另一个数据来代替. 也许你的第一反应是, 用一种简单方法: 在点击关闭按钮的时候, 发起一个请求, 然后更新这个推荐人:var close1Button = document.querySelector(’.close1’);var close1ClickStream = Rx.Observable.fromEvent(close1Button, ‘click’);// close2Button 和 close3Button 重复此过程var requestStream = refreshClickStream.startWith(‘startup click’) .merge(close1ClickStream) // 把关闭按钮加在这里 .map(function() { var randomOffset = Math.floor(Math.random()500); return ‘https://api.github.com/users?since=' + randomOffset; });然而这没不对. (由于 refreshClickStream 影响了所有的推荐人流, 所以)该过程会关闭并且重新加载_所有的_推荐关注人, 而不是仅更新我们想关掉的那一个. 这里有很多方式来解决这个问题, 为了玩点炫酷的, 我们会重用之前的回复数据中别的推荐人. API 返回的数据每页包含 100 个用户, 但我们每次只用到其中的 3 个, 所以我们有很多有效的刷新数据可以用, 没必要再请求新的.再一次的, 让我们用流的思维来思考. 当一个 ‘close1’点击事件发生的时候, 我们使用 responseStream中 最近被派发的 回复来从回复的用户列表中随机获取一个用户. 如下: requestStream: –r—————> responseStream: ——R———–>close1ClickStream: ————c—–>suggestion1Stream: ——s—–s—–>在 [Rx] 中, 有一个合成器方法叫做 combineLatest, 似乎可以完成我们想做的事情. 它把两个流 A 和 B 作为其输入, 而当其中任何一个派发值的时候, combineLatest 会把两者最近派发的值 a 和 b 按照 c = f(x,y) 的方法合并处理再输出, 其中 f 是你可以定义的方法. 用图来解释也许更清楚:stream A: –a———–e——–i——–>stream B: —–b—-c——–d——-q—-> vvvvvvvv combineLatest(f) vvvvvvv —-AB—AC–EC—ED–ID–IQ—->在该例中, f 是一个转换为全大写的函数我们可以把 combineLatest() 用在 close1ClickStream 和 responseStream 上, 因此一旦 “关闭按钮1” 被点击(导致 close1ClickStream 产生新值), 我们都能得到最新的返回数据, 并在 suggestion1Stream中产生一个新的值. 由于 combineLatest() 的对称性的, 任何时候, 只要 responseStream 派发了一个新的回复, 它也将合并最新的一次 ‘关闭按钮1被点击’ 事件来产生一个新的推荐关注人. 这个特性非常有趣, 因为它允许我们简化我们之前的 suggestion1Stream , 如下:var suggestion1Stream = close1ClickStream .combineLatest(responseStream, function(click, listUsers) { return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);在上述思考中, 还有一点东西被遗漏. combineLatest() 使用了两个数据源中最近的数据, 但是如果这些源中的某个从未派发过任何东西, combineLatest() 就不能产生一个数据事件到输出流. 如果你再细看上面的 ASCII 图, 你会发现当第一个流派发 a 的时候, 不会有任何输出. 只有当第二个流派发 b 的时候才能产生一个输出值.有几种方式来解决该问题, 我们仍然采取最简单的一种, 就是在页面启动的时候模拟一次对 ‘关闭按钮1’ 按钮的点击:var suggestion1Stream = close1ClickStream.startWith(‘startup click’) // 把对"关闭按钮1"的点击的模拟加在这里 .combineLatest(responseStream, function(click, listUsers) {l return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);总结整理现在我们的工作完成了. 完整的代码如下所示:var refreshButton = document.querySelector(’.refresh’);var refreshClickStream = Rx.Observable.fromEvent(refreshButton, ‘click’);var closeButton1 = document.querySelector(’.close1’);var close1ClickStream = Rx.Observable.fromEvent(closeButton1, ‘click’);// close2 和 close3 是同样的逻辑var requestStream = refreshClickStream.startWith(‘startup click’) .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });var responseStream = requestStream .flatMap(function (requestUrl) { return Rx.Observable.fromPromise($.ajax({url: requestUrl})); });var suggestion1Stream = close1ClickStream.startWith(‘startup click’) .combineLatest(responseStream, function(click, listUsers) { return listUsers[Math.floor(Math.random()listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);// suggestion2Stream 和 suggestion3Stream 是同样的逻辑suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // 隐藏第 1 个推荐关注者元素 } else { // 显示第 1 个推荐关注者元素并渲染数据 }});你可以在这里查看完整的[样例代码]很惭愧, 这只是一个微小的代码示例, 但它的信息量很大: 它着重表现了, 如何对关注点进行适当的隔离, 从而对不同流进行管理, 甚至充分利用了返回数据的流. 这样的函数式风格使得代码像声明式多于像命令式: 我们并不用给出一个要执行的的结构化序列, 我们只是通过定义流之间的关系来表达系统中每件事物是什么. 举例来说, 通过 Rx, 我们告诉计算机 suggestion1Stream 就是 点击关闭按钮1 的流, 与最近一个API返回的(用户中随机选择的一个)的用户的流, 刷新时产生 null 的流, 和应用启动时产生 null 的流的合并流.回想一下那些你熟稔的流程控制的语句(比如 if, for, while), 以及 Javascript 应用中随处可见的基于回调的控制流. (只要你愿意, )你甚至可以在上文的 subscribe() 中不写 if 和 else, 而是(在 observable 上)使用 filter()(这一块我就不写实现细节了, 留给你作为练习). 在 Rx 中, 有很多流处理方法, 比如 map, filter, scan, merge, combineLatest, startWith, 以及非常多用于控制一个事件驱动的程序的流的方法. 这个工具集让你用更少的代码而写出更强大的效果.接下来还有什么?如果你愿意用 [Rx] 来做反应式编程, 请花一些时间来熟悉这个 函数列表, 其中涉及如何变换, 合并和创建 Observables (被观察者). 如果你想以图形的方式理解这些方法, 可以看一下 弹珠图解 RxJava. 一旦你对理解某物有困难的时候, 试着画一画图, 基于图来思考, 看一下函数列表, 再继续思考. 以我的经验, 这样的学习流程非常有用.一旦你熟悉了如何使用 [Rx] 进行变成, 理解冷热酸甜, 想吃就吃…哦不, 冷热 Observables 就很有必要了. 反正就算你跳过了这一节, 你也会回来重新看的, 勿谓言之不预也. 建议通过学习真正的函数式编程来磨练你的技巧, 并且熟悉影响各种议题, 比如"影响 [Rx] 的副作用"什么的.不过, 实现了反应式编程的库并非并非只有 [Rx]. [Bacon.js] 的运行机制就很直观, 理解它不像理解 [Rx] 那么难; [Elm Language] 在特定的应用场景有很强的生命里: 它是一种会编译到 Javascript + HTML + CSS 的反应式编程语言, 它的特色在于 [time travelling debugger]. 这些都很不错.Rx 在严重依赖事件的前端应用中表现优秀. 但它不只是只为客户端应用服务的, 在接近数据库的后端场景中也大有可为. 实际上, [RxJava 正是激活 Netflex 服务端并发能力的关键]. Rx 不是一个严格限于某种特定类型应用的框架或者是语言. 它其实是一种范式, 你可以在任何事件驱动的软件中实践它.本文作者:richardo2016阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 6, 2018 · 8 min · jiezi

卷积神经网络导览

摘要: 关于卷积神经网络最重要的概念都在这里!介绍这篇文章旨在以全面和简洁的方式介绍卷积神经网络(CNN),目标是建立对这些算法的内部工作的直观理解。因此,这项工作对于刚从这个主题开始的非数学、非计算机科学背景的读者来说意味着特别有价值。我写这篇文章的灵感来自于我正在参加的Fast.ai课程’Code Deeprs For Coders v3’,导师Jeremy Howard鼓励我们在博客上讲述我们学到的东西。我在这里分享的知识来自于阅读各种材料和参加不同的课程的成果,目的是在以医学影像的学生研究项目。这些帖子的主要部分取自我最近完成的这篇论文。在旅程开始之前,必须采取一些预防措施来对卷积神经网络进行透视。机器学习(ML)是计算机科学的子领域,通过具有学习能力的算法解决问题。“学习”通过内部组件的自动优化一个称为参数的权重。适用于ML的问题是两种形式的预测:回归-连续值的预测以及分类-通过类成员预测将对象细分为不同的组。深度学习(DL)又是ML的子域,通过将具有特定架构的算法(称为神经网络)应用于机器学习问题来区分。这种架构的灵感来自于自然界中的神经网络,并且包括-稍微简化-表示连接不同神经元的计算单元和边缘以及确保信息流动的神经元。其中存在多种不同类型的神经网络:人工神经网络(ANN),专门用于处理表格数据;用于时间序列数据的递归神经网络(RNN),如语音和卷积神经网络(CNN),特别适用于图像数据。有了这些基础知识,我们就可以开始研究后一种类型了。“解剖”CNN图1是网络的循环示例图,它的灵感来自LeNet(LeCun等,1998)第一个CNN架构。虽然CNN之间的特定架构不同,但它们的特征在于都有该示例网络所涵盖的规定元素。该算法旨在解决二元分类问题,例如猫与狗之间的区别。在这一点上,重要的是该图和紧接着的用于为演练创建框架。因此,不需要完全理解和熟悉所有术语。这些更深入的理解在即将到来的部分中逐渐形成,图1将作为便于读者大致的参考。从图1中可以看出,网络被细分为前向和后向传递。在前向传递期间,数据通过不同的层(图1:彩色箭头)。由二维阵列表示的输入图像(图1:左空矩形)被提供给给出名称的卷积层。该层识别输入数据上的轮廓和形状,并输出一组特征图(图1:垂直条纹矩形)。最大池层成为卷积层的成功之处,它成功的消除了无关紧要的部分,从而缩小了数据。此后,数据通过平均池化操作,将图像数据转换为矢量,进入完全连接的层(图1:水平条纹矩形)。这些图层在此向量中标识特定于类的模式,并在此基础上预测输入数据的类成员资格。到目前为止,还没有学习过,因为这是在向后传递中完成的。首先,通过损失函数量化分类误差,基于损失函数的结果,通过反向传播和梯度下降来优化前述层中的参数。很明显更高的迭代次数,即输入更多训练样本图像,是实现正确分类结果所必需的。完整训练数据集通过模型的点称为epoch。卷积,线性整流函数和最大池化术语“卷积”描述了特定类型的矩阵计算,其中称为滤波器的特殊目的矩阵应用于输入图像,如图2所示。滤波器(3x3矩阵)通常是较小的矩阵,在卷积运算中,它被放置在图像的子部分上:(图2中的3x3青色图像子集)。每对相应值的元素乘法和随后对所有乘积的进行求和,产生单个输出值。换句话说,图像子集的左上角值与滤波器的左上角值相乘,顶部中间值与相应的顶部中间值相乘,最后所有乘积都相加。之后,滤波器以滑动窗口的方式在输入图像的每个拟合子集上执行上述计算的图像上移动。得到的输出值被收集在称为特征图的输出矩阵中,其中特征图中的输出值的位置(图2:青色顶部中间值)对应于计算中涉及的输入图像子集的位置。过滤器在图像上的移动方式取决于步幅和填充。一个步骤描述了每个卷积运算将滤波器移动一个像素,从而产生更小的特征图(图2:步幅s=1)。通过向图像的外边界添加零像素来抵消特征图的尺寸减小,称为填充(图2:填充=0)。在卷积操作期间,大多数单独的滤波器应用于输入图像,从而产生每个滤波器的特征图(图2:滤波器和特征图后面的多个silhouttes)。换句话说,卷积层输出与其滤波器计数对应的一叠特征映射。可以将滤波器视为专用轮廓检测器,并且得到的特征图报告检测位置。如果过滤器放置在包含边缘的图像子部件上,它会将其转换为特征图中的高值。换句话说,高特征映射值表示特定位置处的输入图像中的轮廓检测。该过程如图3所示:如果过滤器到达由黄色和绿色框标记的子部分,则识别基础轮廓。因此,特征图也可以被视为图像并相应地可视化。图4显示出了对输入图像应用滤波器(图4:底行)以进行垂直或水平边缘检测的结果。过滤器值是权重、是学习的参数。它们在后向传递期间不断优化,同时更多的数据通过网络。通过这种方式,实现了调整过程:过滤器学习识别输入图像中可用的特定元素,并且可以将其可视化为图片本身。而早期图层中的过滤器(图5:左)将学习基本像素,如轮廓,后期图层中的过滤器(图5:右)将连接上游特征,并学习更复杂的构造,例如眼睛甚至脸部( Zeiler和Fergus,2014)。因此,可以小心地将滤波器与视觉皮层神经元的感受野进行比较。这个被称为线性整流单元(ReLU)的函数应用于输出特征图(图6)。在令人生畏的名称下隐藏了一个简单的阈值步骤:零以下的所有值都归零。阈值化的特征图被移交给最大池化层(图7)。这里,虽然类似于卷积,但实际上发生了更简单的矩阵计算。过滤器再次以滑动窗口方式放置在要素图子集上,并提取子集的最高值,将它们保留在精简输出要素图中。目的是丢弃多余的数据:没有表示任何轮廓检测的值被划掉,而空间信息大致保留,这导致较低的计算成本。灵感和参考Jeremy Howard和Fast.ai深入学习编码器;Andrew Ng和deeplearning.ai的神经网络和深度学习;LeCun,Y.,Bottou,L.,Bengio,Y.,Haffner,P.,1998。基于梯度的学习应用于文档识别;Zeiler,MD,Fergus,R.,2014。可视化和理解卷积网络;本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 5, 2018 · 1 min · jiezi

分析core,是从案发现场,推导案发经过

分析core不是一件容易的事情。试想,一个系统运行了很长一段时间,在这段时间里,系统会积累大量正常、甚至不正常的状态。这个时候如果系统突然出现了一个问题,那这个问题十有八九跟长时间积累下来的状态有关系。分析core,就是分析出问题时,系统产生的“快照”,追溯历史,找出问题发生源头。这有点像是从案发现场,推导案发经过一样。soft lockup!今天这个“案件”,我们从soft lockup说起。soft lockup是内核实现的夯机自我诊断功能。这个功能的实现,和线程的优先级有关系。这里我们假设有三个线程A、B、和C。他们的优先级关系是A<B<C。这意味着C优先于B执行,B优先于A执行。这个优先级关系,如果倒过来叙述,就会产生一个规则:如果C不能执行,那么B也没有办法执行,如果B不能执行,那基本上A也没法执行。soft lockup实际上就是对这个规则的实现:soft lockup使用一个内核定时器(C线程),周期性地检查,watchdog(B线程)有没有正常运行。如果没有,那就意味着普通线程(A线程)也没有办法正常运行。这时内核定时器(C线程)会输出类似上图中的soft lockup记录,来告诉用户,卡在cpu上的,有问题的线程的信息。具体到这个“案件”,卡在cpu上的线程是python,这个线程正在刷新tlb缓存。老搭档ipi和tlb如果我们对所有夯机问题的调用栈做一个统计的话,我们肯定会发现,tlb和ipi是一对形影不离的老搭档。其实这不是偶然的。系统中,相对于内存,tlb是处理器本地的cache。这样的共享内存和本地cache的架构,必然会提出一致性的要求。如果每个处理器的tlb“各自为政”的话,那系统肯定会乱套。满足tlb一致性的要求,本质上来说只需要一种操作,就是刷新本地tlb的同时,同步地刷新其他处理器的tlb。系统正是靠tlb和ipi这对老搭档的完美配合来完成这个操作的。这个操作本身的代价是比较大的。一方面,为了避免产生竞争,线程在刷新本地tlb的时候,会停掉抢占。这就导致一个结果:其他的线程,当然包括watchdog线程,没有办法被调度执行(soft lockup)。另外一方面,为了要求其他cpu同步地刷新tlb,当前线程会使用ipi和其他cpu同步进展,直到其他cpu也完成刷新为止。其他cpu如果迟迟不配合,那么当前线程就会死等。不配合的cpu为什么其他cpu不配合去刷新tlb呢?理论上来说,ipi是中断,中断的优先级是很高的。如果有cpu不配合去刷新tlb,基本上有两种可能:一种是这个cpu刷新了tlb,但是做到一半也卡住了;另外一种是,它根本没有办法响应ipi中断。通过查看系统中所有占用cpu的线程,可以看到cpu基本上在做三件事情:idle,正在刷新tlb,和正在运行java程序。其中idle的cpu,肯定能在需要的时候,响应ipi并刷新tlb。而正在刷新tlb的cpu,因为停掉了抢占,且在等待其他cpu完成tlb刷新,所以在重复输出soft lockup记录。这里问题的关键,是运行java的cpu,这个我们在下一节讲。java不是问题,踩到的坑才是问题java线程运行在0号cpu上,这个线程的调用栈,满满的都是故事。我们可以简单地把线程调用栈分为上下两部分。下边的是system call调用栈,是java从系统调用进入内核的执行记录。上边的是中断栈,java在执行系统调用的时候,正好有一个中断进来,所以这个cpu临时去处理了中断。在linux内核中,中断和系统调用使用的是不同的内核栈,所以我们可以看到第二列,上下两部分地址是不连续的。netoops持有等待分析中断处理这部分调用栈,从下往上,我们首先会发现,netoops函数触发了缺页异常。缺页异常其实就是给系统一个机会,把指令踩到的虚拟地址,和真正想要访问的物理机之间的映射关系给建立起来。但是有些虚拟地址,这种映射根本就是不存在的,这些地址就是非法地址(坑)。如果指令踩到这样的地址,会有两种后果,segment fault(进程)和oops(内核)。很显然netoops踩到了非法地址,使得系统进入了oops逻辑。系统进入oops逻辑,做的第一件事情就是禁用中断。这个非常好理解。oops逻辑要做的事情是保存现场,它当然不希望,中断在这个时候破坏问题现场。接下来,为了保存现场的需要,netoops再一次被调用,然后这个函数在几条指令之后,等在了spinlock上。要拿到这个spinlock,netoops必须要等它当前的owner线程释放它。这个spinlock的owner是谁呢?其实就是当前线程。换句话说,netoops拿了spinlock,回过头来又去要这个spinlock,导致当前线程死锁了自己。验证上边的结论,我们当然可以去读代码。但是有另外一个技巧。我们可以看到netoops函数在踩到非法地址的时候,指令rip地址是ffffffff8137ca64,而在尝试拿spinlock的时候,rip是ffffffff8137c99f。很显然拿spinlock在踩到非法地址之前。虽然代码里的跳转指令,让这种判断不是那么的准确,但是大部分情况下,这个技巧是很有用的。缺页异常,错误的时间,错误的地点这个线程进入死锁的根本原因是,缺页异常在错误的时间发生在了错误的地点。对netoops函数的汇编和源代码进行分析,我们会发现,缺页发生在ffffffff8137ca64这条指令,而这条指令是inline函数utsname的指令。下图中框出来的四条指令,就是编译后的utsname函数。而utsname函数的源代码其实就一行。return &current->nsproxy->uts_ns->name;这行代码通过当前进程的task_struct指针current,访问了uts namespace相关的内容。这一行代码,之所以会编译成截图中的四条汇编指令,是因为gs寄存器的0xcbc0项,保存的就是current指针。这四条汇编指令做的事情分别是,取current指针,读nsproxy项,读uts_ns项,以及计算name的地址。第三条指令踩到非法地址,是因为nsproxy这个值为空值。空值nsproxy我们可以在两个地方验证nsproxy为空这个结论。第一个地方是读取当前进程task_sturct的nsproxy项。另外一个是看缺页异常的时候,保存下来的rax寄存器的值。保存下来的rax寄存器值可以在图三中看到,下边是从task_struct里读出来的nsproxy值。正在退出的线程那么,为什么当前进程task_struct这个结构的nsproxy这一项为空呢?我们可以回头看一下,java线程调用栈的下半部分内容。这部分调用栈实际上是在执行exit系统调用,也就是说进程正在退出。实际上参考代码,我们可以确定,这个进程已经处于僵尸(zombie)状态了。因而nsproxy相关的资源,已经被释放了。namespace访问规则最后我们简单看一下nsproxy的访问规则。规则一共有三条,netoops踩到空指针的原因,某种意义上来说,是因为它间接地违背了第三条规则。netoops通过utsname访问进程的namespace,因为它在中断上下文,所以并不算是访问当前的进程,也就是说它应该查空。另外我加亮的部分,进一步佐证了上一小节的结论。/*``* the namespaces access rules are:``*``* 1\. only current task is allowed to change tsk-&gt;nsproxy pointer or``* any pointer on the nsproxy itself``*``* 2\. when accessing (i.e. reading) current task's namespaces - no``* precautions should be taken - just dereference the pointers``*``* 3\. the access to other task namespaces is performed like this``* rcu_read_lock();``* nsproxy = task_nsproxy(tsk);``* if (nsproxy != NULL) {``* / *``* * work with the namespaces here``* * e.g. get the reference on one of them``* * /``* } / *``* * NULL task_nsproxy() means that this task is``* * almost dead (zombie)``* * /``* rcu_read_unlock();``*``*/回顾最后我们复原一下案发经过。开始的时候,是java进程退出。java退出需要完成很多步骤。当它马上就要完成自己使命的时候,一个中断打断了它。这个中断做了一系列的动作,之后调用了netoops函数。netoops函数拿了一个锁,然后回头去访问java的一个被释放掉的资源,这触发了一个缺页。因为访问的是非法地址,所以这个缺页导致了oops。oops过程禁用了中断,然后调用netoops函数,netoops需要再次拿锁,但是这个锁已经被自己拿了,这是典型的死锁。再后来其他cpu尝试同步刷新tlb,因为java进程关闭了中断而且死锁了,它根本收不到其他cpu发来的ipi消息,所以其他cpu只能不断的报告soft lockup错误。本文作者:声东阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 1 min · jiezi

手把手教您将libreoffice移植到函数计算平台

LibreOffice是由文档基金会开发的自由及开放源代码的办公室套件.LibreOffice套件包含文字处理器,电子表格,演示文稿程序,数量库管理程序及创建和编辑数学公式的应用程序。借助LibreOffice的命令行接口可以方便地将office文件转换成pdf。如下所示:$ soffice –convert-to pdf –outdir /tmp /tmp/test.doc一个完整版本的LibreOffice大小为2 GB,而函数计算运行时缓存目录/ tmp空间限制为512M,zip程序包大小限制为50M。好在社区已经有项目aws-lambda-libreoffice成功的将libreoffice移植到AWS Lambda平台,基于前人的方法和经验,本人创建了fc-libreoffice项目,使libreoffice成功的运行在阿里云函数计算平台.fc -libreoffice 在aws-lambda-libreoffice的基础上解决了如下问题:重新编译和裁剪libreoffice,使其适配FC nodejs8 runtime内置的gcc和内核版本;安装运行时缺失的libssl3依赖;借助OSS运行时下载解压,以绕过zip程序包50M的限制;制作了一个例子项目,支持一键部署,快速体验。本文侧重于记述整个移植过程,记录关键步骤以备忘,也为类似的转换工具移植到函数计算平台提供参考。如果您对于如何快速搭建一个廉价且可扩展的词转换pdf云服务更感兴趣,可以阅读另一篇文章“五分钟上线 - 函数计算Word转PDF云服务”。准备工作在开始之前建议找一个台配置较好的Debain / Ubuntu机器,libreoffice编译比较消耗计算资源。并在机器上安装和配置如下工具:docker-ce安装方法参考官方安装文档好玩的一款函数计算的编排工具,用于快速部署函数计算应用。MacOS平台可以使用如下方法安装brew tap vangie/formulabrew install fun其他平台可以通过npm安装npm install @alicloud/fun -gossutil oss的命令行工具。将其下载并放置到&dollar; PATH所在目录。编译libreoffice我们会采用fc-aliyunfc/runtime-nodejs8:build docker 提供的docker镜像进行编译.fc-docker提供了一系列的docker镜像,这些docker镜像环境非常接近函数计算的真实环境。因为我们打算把libreoffice跑在nodejs8环境中,所以我们选用了aliyunfc/runtime-nodejs8:build,建造标签镜像相比于其他镜像会多一些构建需要的基础包。启动一个编译环境通过如下命令可启动一个用于构建libreoffice的容器。docker run –name libre-builder –rm -v $(pwd):/code -d -t –cap-add=SYS_PTRACE –security-opt seccomp=unconfined aliyunfc/runtime-nodejs8:build bash上面的命令,我们启动了一个名为libre-builder的容器并把当前目录挂载到容器内文件系统的/代码目录。附加参数–cap-add=SYS_PTRACE –security-opt seccomp=unconfined是cpp程序编译需要的,否则会报出一些警告。-d表示以后台daemon的方式启动。-t表示启动tty,配合后面的bash命令是为了卡主容器不退出。而–rm表示一旦容器停止了就自动删除容器。安装编译工具接下来进入容器安装编译工具apt-get install -y ccacheapt-get build-dep -y libreofficeccache是一个编译工具,可以加速gcc对同一个程序的多次编译。尽管第一次编译会花费长一点的时间,有了ccache,后续的编译将变得非常非常快。apt-get的build-dep子命令会建立某个要编译软件的环境。具体行为就是把所有依赖的工具和软件包都安装上。克隆源码git clone –depth=1 git://anongit.freedesktop.org/libreoffice/core libreofficecd libreoffice记得加上–depth=1参数,因为libreoffice项目比较大,进行全量克隆会比较费时间,对于编译来说git提交历史没有意义。配置并编译# 如果多次编译,该设置可以加速后续编译ccache –max-size 16 G && ccache -s通过–disable参数去掉不需要的模块,以减少最终编译产物的体积。# the most important part. Run ./autogen.sh –help to see wha each option means./autogen.sh –disable-report-builder –disable-lpsolve –disable-coinmp \ –enable-mergelibs –disable-odk –disable-gtk –disable-cairo-canvas \ –disable-dbus –disable-sdremote –disable-sdremote-bluetooth –disable-gio –disable-randr \ –disable-gstreamer-1-0 –disable-cve-tests –disable-cups –disable-extension-update \ –disable-postgresql-sdbc –disable-lotuswordpro –disable-firebird-sdbc –disable-scripting-beanshell \ –disable-scripting-javascript –disable-largefile –without-helppack-integration \ –without-system-dicts –without-java –disable-gtk3 –disable-dconf –disable-gstreamer-0-10 \ –disable-firebird-sdbc –without-fonts –without-junit –with-theme=“no” –disable-evolution2 \ –disable-avahi –without-myspell-dicts –with-galleries=“no” \ –disable-kde4 –with-system-expat –with-system-libxml –with-system-nss \ –disable-introspection –without-krb5 –disable-python –disable-pch \ –with-system-openssl –with-system-curl –disable-ooenv –disable-dependency-tracking开始编译make的名单最终compile-查询查询结果位于./instdir/目录下。精简尺寸使用strip命令去除二进制文件中的符号信息和编译信息# this will remove ~100 MB of symbols from shared objectsstrip ./instdir/**/删除不必要的文件# remove unneeded stuff for headless moderm -rf ./instdir/share/gallery \ ./instdir/share/config/images_.zip \ ./instdir/readmes \ ./instdir/CREDITS.fodt \ ./instdir/LICENSE* \ ./instdir/NOTICE验证使用如下命令,测试一下编译出来的soffice是否能正常将txt文件转换成pdf文件。echo “hello world” > a.txt./instdir/program/soffice –headless –invisible –nodefault –nofirststartwizard \ –nolockcheck –nologo –norestore –convert-to pdf –outdir $(pwd) a.txt打包# archivetar -zcvf lo.tar.gz instdir然后使用如下命令将lo.tar.gz文件从容器文件系统拷贝到宿主机文件系统。docker cp libre-builder:/code/libreoffice/lo.tar.gz ./lo.tar.gzGzip vs Zopfli vs Brotli Gzip,Zopfli和Brotli是三种开源的压缩算法,对于一个130M的铬文件,分别采用这三种压缩算法最大级别的压缩效果是文件算法MIB压缩比解压耗时铬-130.62–chromium.gzGzip已44.1366.22%0.968schromium.gzZopfli43.0067.08%0.935schromium.brBrotli33.2174.58%0.712s从上面的结果看Brotli算法的效果最优。由于aliyunfc/runtime-nodejs8:build是基于debain jessie发行版的。在debain jessie上安装brotli较为麻烦,所以我们借助ubuntu容器安装brotli工具,将tar.gz格式转为tar.br格式。docker run –name brotli-util –rm -v $(pwd):/root -w /root -d -t ubuntu:18.04 bashdocker exec -t brotli-util apt-get updatedocker exec -t brotli-util apt-get install -y brotlidocker exec -t brotli-util gzip -d lo.tar.gzdocker exec -t brotli-util brotli -q 11 -j -f lo.tar然后当前目录会多一个lo.tar.br文件。安装依赖在函数计算nodejs8环境中运行soffice,需要安装通过npm安装tar.br的解压依赖包@shelf/aws-lambda-brotli-unpacker 和通过apt-get安装libnss3依赖。先启动一个nodejs8的容器,以保证依赖的安装环境和运行时环境是一致的。docker run –rm –name libreoffice-builder -t -d -v $(pwd):/code –entrypoint /bin/sh aliyunfc/runtime-nodejs8注意:存在@shelf/aws-lambda-brotli-unpacker本机绑定,所以在开发机MacOS上npm install打包上传是无法工作。docker exec -t libreoffice-builder npm install由于函数计算运行时无法安装全局的deb包,所以需要将deb和依赖的deb包下载下来,再安装到当前工作目录而不是系统目录。当前工作目录下可以随代码一起打包上传。docker exec -t libreoffice-builder apt-get install -y -d -o=dir::cache=/code libnss3docker exec -t libreoffice-builder bash -c ‘for f in $(ls /code/archives/.deb); do dpkg -x $f $(pwd) ; done;’libnss3包含了许多.so动态链接库文件,linux系统下LD_LIBRARY_PATH环境变量里的动态链接库才能被找到,而函数计算将代码目录/代码下的lib目录默认添加到了LD_LIBRARY_PATH中。所以我们写个脚本,把所有安装的.so文件软连接到/ code / lib目录下docker exec -t libreoffice-builder bash -c “rm -rf /code/archives/; mkdir -p /code/lib;cd /code/lib; find ../usr/lib -type f ( -name ‘.so’ -o -name ‘*.chk’ ) -exec ln -sf {} . ;“下载并解压tar.br为了使用这个lo.tar.br文件,需要先上传到OSSossutil cp $SCRIPT_DIR/../node_modules/fc-libreoffice/bin/lo.tar.br oss://${OSS_BUCKET}/lo.tar.br \ -i ${ALIBABA_CLOUD_ACCESS_KEY_ID} -k ${ALIBABA_CLOUD_ACCESS_KEY_SECRET} -e oss-${ALIBABA_CLOUD_DEFAULT_REGION}.aliyuncs.com -f在函数的初始化方法中下载。module.exports.initializer = (context, callback) => { store = new OSS({ region: oss-${process.env.ALIBABA_CLOUD_DEFAULT_REGION}, bucket: process.env.OSS_BUCKET, accessKeyId: context.credentials.accessKeyId, accessKeySecret: context.credentials.accessKeySecret, stsToken: context.credentials.securityToken, internal: process.env.OSS_INTERNAL === ’true’ }); if (fs.existsSync(binPath) === true) { callback(null, “already downloaded.”); return; } co(store.get(’lo.tar.br’, binPath)).then(function (val) { callback(null, val) }).catch(function (err) { callback(err) });};然后借助于@shelf/aws-lambda-brotli-unpackernpm包解压lo.tar.brconst {unpack} = require(’@shelf/aws-lambda-brotli-unpacker’);const {execSync} = require(‘child_process’);const inputPath = path.join(__dirname, ‘..’, ‘bin’, ’lo.tar.br’);const outputPath = ‘/tmp/instdir/program/soffice’;module.exports.handler = async event => { await unpack({inputPath, outputPath}); execSync(${outputPath} --convert-to pdf --outdir /tmp /tmp/example.docx);};有趣部署函数编写一个template.yml文件,将函数计算的配置都写在该文件中,然后使用fun deploy命令部署函数。ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: libre-svc: # service name Type: ‘Aliyun::Serverless::Service’ Properties: Description: ‘fc test’ Policies: - AliyunOSSFullAccess libre-fun: # function name Type: ‘Aliyun::Serverless::Function’ Properties: Handler: index.handler Initializer: index.initializer Runtime: nodejs8 CodeUri: ‘./’ Timeout: 60 MemorySize: 640 EnvironmentVariables: ALIBABA_CLOUD_DEFAULT_REGION: ${ALIBABA_CLOUD_DEFAULT_REGION} OSS_BUCKET: ${OSS_BUCKET} OSS_INTERNAL: ’true’真实场景下,把秘钥和一起变量写在template.yml里并不合适。为了做到代码和配置相分离,上面使用了变量占位符${ALIBABA_CLOUD_DEFAULT_REGION}和${OSS_BUCKET}。然后使用envsubst进行替换SCRIPT_DIR=dirname -- "$0"source $SCRIPT_DIR/../.envexport ALIBABA_CLOUD_DEFAULT_REGION OSS_BUCKETenvsubst < $SCRIPT_DIR/../template.yml.tpl > $SCRIPT_DIR/../template.ymlcd $SCRIPT_DIR/../上面所有的配置都写在了.env文件中,dotenv是社区常见的方案,也有广泛的工具支持。小结本文重点介绍了编译libreoffice的过程,这也是移植中较为困难的部分。由于libreoffice又涉及到npm的本地绑定和apt-get安装到本地目录的问题,所以在函数计算依赖方面本例也是非常经典的场景。无论是编译还是依赖安装,本文中的步骤都强烈地依赖fc-docker镜像,正因为有了该镜像,解决了环境差异问题,大大降低了移植的难度。大文件运行时加载也是函数计算的常见问题,对于转换工具场景中常见的大文件是二进制程序,对于机器学习场景中大文件常是训练模型的数据问题,但是无论是哪一种,采用OSS下载解压的方法都是通用的,随着函数计算支持了NAS,使用NAS挂载共享网盘的方式也是一种新的路径。上文完整的源码可以在FC-LibreOffice的项目中找到。参考阅读https://zh.wikipedia.org/wiki/LibreOffice如何在AWS Lambda中运行LibreOffice以获得大规模的廉价PDFhttps://github.com/alixaxel/chrome-aws-lambdahttps://github.com/shelfio/aws-lambda-brotli-unpacker本文作者:倚贤阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 3 min · jiezi

全图化引擎(AI·OS)中的编译技术

全图化引擎又称算子执行引擎,它的介绍可以参考从HA3到AI OS - 全图化引擎破茧之路。本文从算子化的视角介绍了编译技术在全图化引擎中的运用主要内容有:1.通过脚本语言扩展通用算子上的用户订制能力,目前这些通用算子包括得分算子,过滤算子等。这一方面侧重于编译前端,我们开发了一种嵌入引擎的脚本语言cava ,解决了用户扩展引擎功能的一些痛点,包括插件的开发测试效率,兼容性,引擎版本升级效率等。2.通过代码技术优化全图化引擎性能,由于全图化引擎是基于tensorflow开发,它天生具备tensorflow xla编译能力,利用内核的熔丝提升性能,这部分内容可以参考XLA概述 .xla主要面向tensorflow内置的内核,能发挥的场景是在线预测模型算分。但是对于用户自己开发的算子,XLA很难发挥作用。本文第二部分主要介绍对于自定义算子我们是如何做的代码生成优化的。通用算子上的脚本语言静脉由于算子开发和组图逻辑对普通用户来说成本较高,全图化引擎内置了一些通用算子,比如说射手算子,过滤器算子。这些通用算子能加载C ++插件,也支持用静脉脚本写的插件。关于静脉参考可以这篇文章了解一下。和C ++插件相比,静脉插件有如下特点:1.类java的语法。扩大了插件开发的受众,让熟悉java的同学能快速上手使用引擎。2.性能高.cava是强类型,编译型语言,它能和c ++无损交互。这保证了cava插件的执行性能,在单值场景使用cava写的插件和c ++的插件性能相当。3.使用池管理内存.cava的内存管理可定制,服务端应用每个请求一个池是最高效的内存使用策略。4.安全。对数组越界,对象访问,除零异常做了保护。5.支持jit,编译快。支持upc时编译代码,插件的上线就和上线普通配置一样,极大的提升迭代效率6.兼容性:由于cava的编译过程和引擎版本是强绑定的,只要引擎提供的cava类库接口不变,cava的插件的兼容性很容易得到保证。而c ++插件兼容性很难保证,任何引擎内部对象内存布局的变动就可能带来兼容性问题。射手算子中的静脉插件cava scorer目前有如下场景在使用1.主搜海选场景,算法逻辑可以快速上线验证2.赛马引擎2.0的算分逻辑,赛马引擎重构后引入cava算分替代原先的战马算分样例如下:package test;import ha3.;/ * 将多值字段值累加,并乘以query里面传递的ratio,作为最后的分数 * /class DefaultScorer { MInt32Ref mref; double ratio; boolean init(IApiProvider provider) { IRefManager refManger = provider.getRefManager(); mref = refManger.requireMInt32(“ids”); KVMapApi kv = provider.getKVMapApi(); ratio = kv.getDoubleValue(“ratio”);//获取kvpair内参数 return true; } double process(MatchDoc doc) { int score = 0; MInt32 mint = mref.get(doc); for (int i = 0; i < mint.size(); i++) { score = score + mint.get(i); } return score * ratio; }}其中cava scorer的算分逻辑(process函数)调用次数是doc级别的,它的执行性能和c ++相比唯一的差距是多了安全保护(数组越界,对象访问,除零异常)。可以说cava是目前能嵌入C ++系统执行的性能最好的脚本语言。过滤算子中静脉插件filter算子中主要是表达式逻辑,例如filter =(0.5 * a + b)> 10.以前表达式的能力较弱,只能使用算术,逻辑和关系运算符。使用cava插件可进一步扩展表达式的能力,它支持类的Java语法,可以定义变量,使用分支循环等。计算 filter = (0.5 * a + b) > 10,用cava可定义如下:class MyFunc { public boolean init(FunctionProvider provider) { return true; } public boolean process(MatchDoc doc, double a, double b) { return (0.5 * a + b) > 10; }}filter = MyFunc(a, b)另外由于静脉是编译执行的,和原生的解释执行的表达式相比有天然的性能优势。关于静脉前端的展望静脉是全图化引擎上面向用户需求的语言,有用户定制扩展逻辑的需求都可以考虑用通用算子+静脉插件配合的模式来支持,例如全图化SQL上的UDF,规则引擎的匹配需求等等。后续静脉会进一步完善语言前端功能,完善类库,尽可能兼容的Java。依托苏伊士和全图化引擎支持更多的业务需求。自定义算子的代码生成优化过去几年,在OLAP领域codegen一直是一个比较热门的话题。原因在于大多数数据库系统采用的是Volcano Model模式。其中的下一个()通常为虚函数调用,开销较大。全图化引擎中也有类似的代码生成场景,例如统计算子,过滤算子等。此外,和XLA类似,全图化引擎中也有一些场景可以通过算子融合优化性能。目前我们的代码生成工作主要集中在CPU上对局部算子做优化,未来期望能在SQL场景做全图编译,并且在异构计算的编译器领域有所发展。单算子的代码生成优化统计算子例如统计语句:group_key:键,agg_fun:总和(VAL)#COUNT(),按键分组统计键出现的次数和缬氨酸的和在统计算子的实现中,键的取值有一次虚函数调用, sum和count的计算是两次虚函数调用,sum count计算出来的值和需要通过matchdoc存取,而matchdoc的访问有额外的开销:一次是定位到matchdoc storage,一次是通过偏移定位到存取位置。那么统计代码生成是怎么去除虚函数调用和matchdoc访问的呢?在运行时,我们可以根据用户的查询获取字段的类型,需要统计的功能等信息,根据这些信息我们可以把通用的统计实现特化成专用的统计实现。例如统计sum和count只需定义包含sum count字段的AggItem结构体,而不需要matchdoc;统计函数sum和count变成了结构体成员的+ =操作。假设键和VAL字段的类型都是整型,那么上面的统计语句最终的代码生成成的静脉代码如下:class AggItem { long sum0; long count1; int groupKey;}class JitAggregator { public AttributeExpression groupKeyExpr; public IntAggItemMap itemMap; public AggItemAllocator allocator; public AttributeExpression sumExpr0; … static public JitAggregator create(Aggregator aggregator) { …. } public void batch(MatchDocs docs, uint size) { for (uint i = 0; i < size; ++i) { MatchDoc doc = docs.get(i); //由c++实现,可被inline int key = groupKeyExpr.getInt32(doc); AggItem item = (AggItem)itemMap.get(key); if (item == null) { item = (AggItem)allocator.alloc(); item.sum0 = 0; item.count1 = 0; item.groupKey = key; itemMap.add(key, (Any)item); } int sum0 = sumExpr0.getInt32(doc); item.sum0 += sum0; item.count1 += 1; } }}这里总计数的虚函数被替换成和+ +和计数+ =,matchdoc的存取变成结构体成员的读写item.sum0和item.count0。经过llvm jit编译优化之后生成的ir如下:define void @_ZN3ha313JitAggregator5batchEP7CavaCtxPN6unsafe9MatchDocsEj(%“class.ha3::JitAggregator”* %this, %class.CavaCtx* %"@cavaCtx@", %“class.unsafe::MatchDocs”* %docs, i32 %size){entry: %lt39 = icmp eq i32 %size, 0 br i1 %lt39, label %for.end, label %for.body.lr.phfor.body.lr.ph: ; preds = %entry %wide.trip.count = zext i32 %size to i64 br label %for.bodyfor.body: ; preds = %for.inc, %for.body.lr.ph %lsr.iv42 = phi i64 [ %lsr.iv.next, %for.inc ], [ %wide.trip.count, %for.body.lr.ph ] %lsr.iv = phi %“class.unsafe::MatchDocs”* [ %scevgep, %for.inc ], [ %docs, %for.body.lr.ph ] %lsr.iv41 = bitcast %“class.unsafe::MatchDocs”* %lsr.iv to i64* // … prepare call for groupKeyExpr.getInt32 %7 = tail call i32 %5(%“class.suez::turing::AttributeExpressionTyped.64”* %1, i64 %6) // … prepare call for itemMap.get %9 = tail call i8* @_ZN6unsafe13IntAggItemMap3getEP7CavaCtxi(%“class.unsafe::IntAggItemMap”* %8, %class.CavaCtx* %"@cavaCtx@", i32 %7) %eq = icmp eq i8* %9, null br i1 %eq, label %if.then, label %if.end10// if (item == null) {if.then: ; preds = %for.body // … prepare call for allocator.alloc %15 = tail call i8* @_ZN6unsafe16AggItemAllocator5allocEP7CavaCtx(%“class.unsafe::AggItemAllocator”* %14, %class.CavaCtx* %"@cavaCtx@") // item.groupKey = key; %groupKey = getelementptr inbounds i8, i8* %15, i64 16 %16 = bitcast i8* %groupKey to i32* store i32 %7, i32* %16, align 4 // item.sum0 = 0; item.count1 = 0; call void @llvm.memset.p0i8.i64(i8* %15, i8 0, i64 16, i32 8, i1 false) // … prepare call for itemMap.add tail call void @_ZN6unsafe13IntAggItemMap3addEP7CavaCtxiPNS_3AnyE(%“class.unsafe::IntAggItemMap”* %17, %class.CavaCtx* %"@cavaCtx@", i32 %7, i8* %15) br label %if.end10if.end10: ; preds = %if.end, %for.body %item.0.in = phi i8* [ %15, %if.end ], [ %9, %for.body ] %18 = bitcast %“class.unsafe::MatchDocs”* %lsr.iv to i64* // … prepare call for sumExpr0.getInt32 %26 = tail call i32 %24(%“class.suez::turing::AttributeExpressionTyped.64”* %20, i64 %25) // item.sum0 += sum0; item.count1 += 1; %27 = sext i32 %26 to i64 %28 = bitcast i8* %item.0.in to <2 x i64>* %29 = load <2 x i64>, <2 x i64>* %28, align 8 %30 = insertelement <2 x i64> undef, i64 %27, i32 0 %31 = insertelement <2 x i64> %30, i64 1, i32 1 %32 = add <2 x i64> %29, %31 %33 = bitcast i8* %item.0.in to <2 x i64>* store <2 x i64> %32, <2 x i64>* %33, align 8 br label %for.incfor.inc: ; preds = %if.then, %if.end10 %scevgep = getelementptr %“class.unsafe::MatchDocs”, %“class.unsafe::MatchDocs”* %lsr.iv, i64 8 %lsr.iv.next = add nsw i64 %lsr.iv42, -1 %exitcond = icmp eq i64 %lsr.iv.next, 0 br i1 %exitcond, label %for.end, label %for.bodyfor.end: ; preds = %for.inc, %entry ret void}代码生成的代码中有不少函数是通过C ++实现的,如docs.get(i)中,itemMap.get(键)等。但是优化后的IR中并没有docs.get(I)的函数调用,这是由于经常调用的c ++中实现的函数会被提前编译成bc,由cava编译器加载,经过llvm inline优化pass后被消除。可以认为cava代码和llvm ir基本能做到无损映射(cava中不容易实现逻辑可由c ++实现,预编译成bc加载后被内联),有了cava这一层我们可以用常规面向对象的编码习惯来做codegen,不用关心llvm api细节,让codegen门槛进一步降低。这个例子中,统计规模是100瓦特文档1瓦特个键时,线下测试初步结论是延迟大约能降1倍左右(54ms-> 27ms),有待表达式计算进一步优化。2.过滤算子在通用过滤算子中,表达式计算是典型的可被codegen优化的场景。例如ha3的过滤语句:filter =(a + 2 * b - c)> 0:表达式计算是通过AttributeExpression实现的,AttributeExpression的评价是虚函数。对于单文档接口我们可以用和统计类似的方式,使用静脉对表达式计算做代码生成。对于批量接口,和统计不同的是,表达式的批量计算更容易运用向量化优化,利用CPU的SIMD指令,使计算效率有成倍的提升。但是并不是所有的表达式都能使用一致的向量化优化方法,比如filter = a> 0 AND b <0这类表达式,有短路逻辑,向量化会带来不必要的计算。因此表达式的编译优化需要有更好的codegen 抽象,我们发现Halide能比较好的满足我们的需求.Halide的核心思想:算法描述(做什么ir)和性能优化(怎么做schedule)解耦。种解耦能让我们更灵活的定制优化策略,比如某些场景走向量化,某些场景走普通的代码生成;更进一步,不同计算平台上使用不同的优化策略也成为可能。3.倒排召回算子在寻求算子中,倒排召回是通过QueryExecutor实现的,QueryExecutor的seek是虚函数。例如query = a AND b OR c。QueryExecutor的和或ANDNOT有比较复杂的逻辑,虚函数的开销相对占比没有表达式计算那么大,之前用VTUNE做过预估,求虚函数调用开销占比约10%(数字不一定准确,内联效果没法评估)和精确统计,表达式计算相比,查询的组合空间巨大,寻求的代码生成得更多的考虑对高性价比的查询做编译优化。海选与排序算子在HA3引擎中海选和精排逻辑中有大量比较操作例如排序= + RANK; ID字句,对应的比较函数是秩Compartor和标识Compartor的联合比较.compare的函数调用可被代码生成掉,并且还可和STL算法联合inline.std ::排序使用非在线的补偿函数带来的开销可以参考如下例子:bool myfunction (int i,int j) { return (i<j); }int docCount = 200000;std::random_device rd;std::mt19937_64 mt(rd());std::uniform_int_distribution<int> keyDist(0, 200000);std::vector<int> myvector1;for (int i = 0 ; i < docCount; i++) { myvector1.push_back(keyDist(mt));}std::vector<int> myvector2 = myvector1;std::sort (myvector1.begin(), myvector1.end()); // cost 15.475msstd::sort (myvector2.begin(), myvector2.end(), myfunction); // cost 19.757ms对20瓦特随机数排序,简单的比较直列带来30%的提升。当然在引擎场景,由于比较逻辑复杂,这部分收益可能不会太多。算子的保险丝和代码生成算子的fuse是tensorflow xla编译的核心思想,在全图化场景我们有一些自定义算子也可以运用这个思想,例如特征生成器。FG生成特征的英文模型训练中很重要的一个环节。在线FG是以子图+配置形式描述计算,这部分的代码生成能使数据从索引直接计算到张量上,省去了很多环节中间数据的拷贝。目前这部分的代码生成可以工作参考这篇文章关于编译优化的展望SQL场景全图的编译执行数据库领域全阶段代码生成早被提出并应用,例如Apache的火花作为编译器 ;还有现在比较火的GPU数据库MAPD,把整个执行计划编译成架构无关的中间表示(LLVM IR),借助LLVM编译到不同的目标执行。从实现上看,SQL场景的全图编译执行对全图化引擎还有更多意义,比如可以省去tensorflow算子执行带来的线程切换的开销,可以去除算子间matchdoc传递(matchdoc作为通用的数据布局性能较差)带来的性能损耗。面向异构计算的编译器随着摩尔定律触及天花板,未来异构计算一定是一个热门的领域.SQL大规模数据分析和在线预测就是异构计算可以发挥作用的典型场景,比如分析场景大数据量统计,在线预测场景深度模型大规模并行计算.cpu驱动其他计算平台如gpu fpga,相互配合各自做自己擅长的事情,在未来有可能是常态。这需要为开发人员提供更好的编程接口。全图化引擎已经领先了一步,集成了tensorflow计算框架,天生具备了异构计算的能力。但在编译领域,通用的异构计算编程接口还远未到成熟的地步。工业界和学术界有不少尝试,比如tensorflow的xla编译框架,TVM,Weld等等。借用焊接的概念图表达一下异构计算编译器设计的愿景:让数据分析,深度学习,图像算法等能用统一易用的编程接口充分发挥异构计算平台的算力。总结编译技术已经开始在引擎的用户体验,迭代效率,性能优化中发挥作用,后续会跟着全图化引擎的演进不断发展。能做的事情很多,挑战很大,感兴趣有同学的可以联系我们探讨交流。参考使用带宽优化存储平衡向量化查询执行,第3章有效地编译现代硬件的高效查询计划TensorFlow编译优化策略 - XLAWeld:重新思考数据密集型库之间的接口TVM:用于深度学习的自动化端到端优化编译器本文作者:sance阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 4 min · jiezi

开源 serverless 产品原理剖析 - Kubeless

背景Serverless 架构的出现让开发者不用过多地考虑传统的服务器采购、硬件运维、网络拓扑、资源扩容等问题,可以将更多的精力放在业务的拓展和创新上。随着 serverless 概念的深入人心,各大云计算厂商纷纷推出了各自的 serverless 产品,其中比较有代表性的有 AWS lambda、Azure Function、Google Cloud Functions、阿里云函数计算等。另外,CNCF 也于 2016 年创立了 Serverless Working Group,它致力于 cloud native 和 serverless 技术的结合。下图是 CNCF serverless 全景图,它将这些产品分成了工具型、安全型、框架型和平台型等类别。同时,容器以及容器编排工具的出现,大大降低了 serverless 产品的开发成本,促进了一大批优秀开源 serverless 产品的诞生,它们大多构建于 kubernetes 之上,如下图所示。Kubeless 简介本文将要介绍的 kubeless 便是这些开源 serverless 产品的典型代表。根据官方的定义,kubeless 是 kubernetes native 的无服务计算框架,它可以让用户在 kubernetes 之上使用 FaaS 构建高级应用程序。从 CNCF 视角,kubeless 属于平台型产品。Kubless 有三个核心概念:Functions - 代表需要被执行的用户代码,同时包含运行时依赖、构建指令等信息;Triggers - 代表和函数关联的事件源。如果把事件源比作生产者,函数比作执行者,那么触发器就是联系两者的桥梁;Runtime - 代表函数运行时所依赖的环境。原理剖析本章节将以 kubeless 为例介绍 serverless 产品需要具备的基本能力,以及 kubeless 是如何利用 K8s 现有功能来实现它们的。这些基本能力包括:敏捷构建 - 能够基于用户提交的源码迅速构建可执行的函数,简化部署流程;灵活触发 - 能够方便地基于各类事件触发函数的执行,并能方便快捷地集成新的事件源;自动伸缩 - 能够根据业务需求,自动完成扩容缩容,无须人工干预。本文所做的调研基于kubeless v1.0.0和k8s 1.13。敏捷构建CNCF 对函数生命周期的定义如下图所示。用户只需提供源码和函数说明,构建部署等工作通常由 serverless 平台完成。 因此,基于用户提交的源码迅速构建可执行函数是 serverless 产品必须具备的基础能力。在 kubeless 里,创建函数非常简单:kubeless function deploy hello –runtime python2.7 \ –from-file test.py \ –handler test.hello该命令各参数含义如下:hello:将要部署的函数名称;–runtime python2.7: 指定使用 python 2.7 作为运行环境。Kubeless 可供选择的运行环境请参考链接 runtimes。–from-file test.py:指定函数源码文件(支持 zip 格式)。–handler test.hello:指定使用 test.py 中的 hello 方法处理请求。函数资源与 K8s OperatorKubeless 函数是一个自定义 K8s 对象,本质上是 k8s operator。k8s operator 原理如下图所示:下面以 kubeless 函数为例,描述 K8s operator 的一般工作流程:使用 k8s 的 CustomResourceDefinition(CRD) 定义资源,这里创建了一个名为functions.kubeless.io的 CRD 来代表 kubeless 函数;创建一个 controller 监听自定义资源的 ADD、UPDATE、DELETE 事件并绑定 hander。这里创建了一个名为function-controller的 CRD controller,该 controller 会监听针对 function 的 ADD、UPDATE、DELETE 事件,并绑定 handler(参阅 AddEventHandler);用户执行创建、更新、删除自定义资源的命令;Controller 根据监听到的事件调用相应的 handler。除了函数外,下文将要介绍的 trigger 也是一个 k8s operator。函数构成Kubeless 的 function-controller监听到针对 function 的 ADD 事件后,会触发相应 handler 创建函数。一个函数由若干 K8s 对象组成,包括 ConfigMap、Service、Deployment、Pod 等,其结构如下图所示:ConfigMap函数中的 ConfigMap 用于描述函数源码和依赖。apiVersion: v1data: handler: test.hello # 函数依赖的第三方 python 库 requirements.txt: | kubernetes==2.0.0 # 函数源码 test.py: | def hello(event, context): print event return event[‘data’]kind: ConfigMapmetadata: labels: created-by: kubeless function: hello # 该 ConfigMap 名称 name: hello namespace: default…Service函数中的 Service 用于描述该函数的访问方式。该 Service 会与执行 function 逻辑的 Pods 相关联,类型是 ClusterIP。apiVersion: v1kind: Servicemetadata: labels: created-by: kubeless function: hello # 该 Service 名称 name: hello namespace: default …spec: clusterIP: 10.109.2.217 ports: - name: http-function-port port: 8080 protocol: TCP targetPort: 8080 selector: created-by: kubeless function: hello # Service 类型 type: ClusterIP…Deployment函数中的 Deployment 用于编排执行函数逻辑的 Pods,通过它可以描述函数期望的个数。apiVersion: extensions/v1beta1kind: Deploymentmetadata: labels: created-by: kubeless function: hello name: hello namespace: default …spec: # 指定函数期望的个数 replicas: 1…Pod函数中的 Pod 包含真正执行函数逻辑的容器。VolumesPod 中的 volumes 段指定了该函数的 ConfigMap。这会将 ConfigMap 中的源码和依赖添加到 volumeMounts.mountPath 指定的目录里面。从容器视角来看,文件路径为/src/test.py和 /src/requirements。… volumeMounts: - mountPath: /kubeless name: hello - mountPath: /src name: hello-depsvolumes:- emptyDir: {} name: hello- configMap: defaultMode: 420 name: hello…Init ContainerPod 中的 Init Container 主要作用如下:将源码和依赖文件拷贝到指定目录;安装第三方依赖。Func ContainerPod 中的 Func Container 会加载 Init Container 准备好的源码和依赖并执行函数。不同 runtime 加载代码的方式大同小异,可参考 kubeless.py,Handler.java。小结Kubeless 通过综合运用 K8s 中的多种组件以及利用各语言的动态加载能力实现了从用户源码到可执行的函数的构建逻辑;考虑了函数运行的安全性,通过 Security Context 机制限制容器中的进程以非 root 身份运行。灵活触发一款成熟的 serverless 产品需要具备灵活触发能力,以满足事件源的多样性需求,同时需要能够方便快捷地接入新事件源。CNCF 将函数的触发方式分成了如下图所示的几种类别,关于它们的详细介绍可参考链接 Function Invocation Types。对于 kubeless 的函数,最简单的触发方式是使用 kubeless CLI,另外还支持通过各种触发器。下表展示了 kubeless 函数目前支持的触发方式以及它们所属的类别。触发方式类别kubeless CLISynchronous Req/RepHttp TriggerSynchronous Req/RepCronjob TriggerJob (Master/Worker)Kafka TriggerAsync Message QueueNats TriggerAsync Message QueueKinesis TriggerMessage Stream下图展示了 kubeless 函数部分触发方式的原理:HTTP trigger如果希望通过发送 HTTP 请求触发函数执行,需要为函数创建 HTTP 触发器。 Kubeless 利用 K8s ingress 机制实现了 http trigger。Kubeless 创建了一个名为httptriggers.kubeless.io的 CRD 来代表 http trigger 对象。同时,kubeless 包含一个名为http-trigger-controller的 CRD controller,它会持续监听针对 http trigger 和 function 的 ADD、UPDATE、DELETE 事件,并执行对应的操作。以下命令将为函数 hello 创建一个名为http-hello的 http trigger,并指定选用 nginx 作为 gateway。kubeless trigger http create http-hello –function-name hello –gateway nginx –path echo –hostname example.com该命令会创建如下 ingress 对象,可以参考 CreateIngress 深入了解 ingress 的创建逻辑。apiVersion: extensions/v1beta1kind: Ingressmetadata: # 该 Ingress 的名字,即创建 http trigger 时指定的 name name: http-hello …spec: rules: - host: example.com http: paths: - backend: # 指向 kubeless 为函数 hello 创建的 ClusterIP 类型的 Service serviceName: hello servicePort: 8080 path: /echoIngress 只是用于描述路由规则,要让规则生效、实现请求转发,集群中需要有一个正在运行的 ingress controller。可供选择的 ingress controller 有 Contour、F5 BIG-IP Controller for Kubernetes、Kong Ingress Controllerfor Kubernetes、NGINX Ingress Controller for Kubernetes、Traefik 等。这种路由规则描述和路由功能实现相分离的思想很好地提现了 K8s 始终坚持的需求和供给分离的设计理念。上文中的命令在创建 trigger 时指定了 nginx 作为 gateway,因此需要部署一个 nginx-ingress-controller。该 controller 的基本工作原理如下:以 pod 的形式运行在独立的命名空间中;以 hostPort 的形式暴露出来供外界访问;内部运行着一个 nginx 实例;监听和 ingress、service 等资源相关的事件。如果发现这些事件最终会影响到路由规则,ingress controller 会采用向 Lua hander 发送新的 endpoints 列表或者直接修改 nginx.conf 并 reload nginx 等手段达到更新路由规则的目的。想要更深入地了解 nginx-ingress-controller 的工作原理可参考文章 how-it-works。完成上述工作后,我们便可以通过发送 HTTP 请求触发函数 hello 的执行:HTTP 请求首先会由 nginx-ingress-controller 中的 nginx 处理;Nginx 根据 nginx.conf 中的路由规则将请求转发给函数对应的 service;最后,请求会转发至挂载在 service 后的某个函数进行处理。样例如下:curl –data ‘{“Another”: “Echo”}’ \ –header “Host: example.com” \ –header “Content-Type:application/json” \ example.com/echo# 函数返回{“Another”: “Echo”}Cronjob trigger如果希望定期触发函数执行,需要为函数创建 cronjob 触发器。K8s 支持通过 CronJob 定期运行任务,kubeless 利用这个特性实现了 cronjob trigger。Kubeless 创建了一个名为cronjobtriggers.kubeless.io的 CRD 来代表 cronjob trigger 对象。同时,kubeless 包含一个名为cronjob-trigger-controller的 CRD controller,它会持续监听针对 cronjob trigger 和 function 的 ADD、UPDATE、DELETE 事件,并执行对应的操作。以下命令将为函数 hello 创建一个名为scheduled-invoke-hello的 cronjob trigger,该触发器每分钟会触发函数 hello 执行一次。kubeless trigger cronjob create scheduled-invoke-hello –function=hello –schedule="*/1 * * * *“该命令会创建如下 CronJob 对象,可以参考 EnsureCronJob 深入了解 CronJob 的创建逻辑。apiVersion: batch/v1beta1kind: CronJobmetadata: # 该 CronJob 的名字,即创建 cronjob trigger 时指定的 name name: scheduled-invoke-hello …spec: # 该 CronJob 的执行计划,即创建 cronjob trigger 时指定的 schedule schedule: */1 * * * * … jobTemplate: spec: activeDeadlineSeconds: 180 template: spec: containers: - args: - curl - -Lv # HTTP headers,包含 event-id、event-time、event-type、event-namespace 等信息 - ’ -H “event-id: xxx” -H “event-time: yyy” -H “event-type: application/json” -H “event-namespace: cronjobtrigger.kubeless.io”’ # kubeless 会为 function 创建一个 ClusterIP 类型的 Service # 可以根据 service 的 name、namespace 拼出 endpoint - http://hello.default.svc.cluster.local:8080 image: kubeless/unzip name: trigger restartPolicy: Never …自定义 trigger如果发现 kubeless 默认提供的触发器无法满足业务需求,可以自定义新的触发器。新触发器的构建流程如下:为新的事件源创建一个 CRD 来描述事件源触发器;在自定义资源对象的 spec 里描述该事件源的属性,例如 KafkaTriggerSpec、HTTPTriggerSpec;为该 CRD 创建一个 CRD controller。该 controller 需要持续监听针对事件源触发器和 function 的 CRUD 操作并作出正确的处理。例如,controller 监听到 function 的删除事件,需要把和该 function 关联的触发器一并删掉;当事件发生时,触发关联函数的执行。我们可以看到,自定义 trigger 的流程遵循了 K8s Operator 设计模式。小结Kubeless 提供了一些基本常用的触发器,如果有其他事件源也可以通过自定义触发器接入;不同事件源的接入方式不同,但最终都是通过访问函数 ClusterIP 类型的 service 触发函数执行。自动伸缩K8s 通过 Horizontal Pod Autoscaler 实现 pod 的自动水平伸缩。Kubeless 的 function 通过 K8s deployment 部署运行,因此天然可以利用 HPA 实现自动伸缩。度量数据获取自动伸缩的第一步是要让 HPA 能够获取度量数据。目前,kubeless 中的函数支持基于 cpu 和 qps 这两种指标进行自动伸缩。下图展示了 HPA 获取这两种度量数据的途径。内置度量指标 cpuCPU 使用率属于内置度量指标,对于这类指标 HPA 可以通过 metrics API 从 Metrics Server 中获取数据。Metrics Server 是 Heapster 的继承者,它可以通过kubernetes.summary_api从 Kubelet、cAdvisor 中获取度量数据。自定义度量指标 qpsQPS 属于自定义度量指标,想要获取这类指标的度量数据需要完成下列步骤。部署用于存储度量数据的系统,这里选择已经被纳入 CNCF 的 Prometheus。Prometheus 是一套开源监控&告警&时序数据解决方案,并且被 DigitalOcean、Red Hat、SUSE 和 Weaveworks 这些 cloud native 领导者广泛使用;采集度量数据,并写入部署好的 Prometheus 中。Kubeless 提供的函数框架会在函数每次被调用时,将下列度量数据 function_duration_seconds、function_calls_total、function_failures_total 写入 Prometheus(可参考 python 样例)。部署实现了 custom metrics API 的 custom API server。这里,因为度量数据被存入了 Prometheus,因此选择部署 k8s-prometheus-adapter,它可以从 Prometheus 中获取度量数据。完成上述步骤后,HPA 就可以通过 custom metrics API 从 Prometheus Adapter 中获取 qps 度量数据。详细配置步骤可参考文章 kubeless-autoscaling。K8s 度量指标简介有时基于 cpu 和 qps 这两种度量指标对函数进行自动伸缩还远远不够。如果希望基于其它度量指标,需要了解 K8s 定义的度量指标类型及其获取方式。目前,K8s 1.13 版本支持的度量指标类型如下:准备好相应的度量数据和获取数据的组件,HPA 就能基于它们对函数进行自动伸缩。更多关于 K8s 度量指标的介绍可参考文章 hpa-external-metrics。度量数据使用知道了 HPA 获取度量数据的途径后,下面描述 HPA 如何基于这些数据对函数进行自动伸缩。基于 cpu 使用率假设已经存在一个名为 hello 的函数,以下命令将为该函数创建一个基于 cpu 使用率的 HPA,它将运行该函数的 pod 数量控制在 1 到 3 之间,并通过增加或减少 pod 个数使得所有 pod 的平均 cpu 使用率维持在 70%。kubeless autoscale create hello –metric=cpu –min=1 –max=3 –value=70Kubeless 使用的是 autoscaling/v2alpha1 版本的 HPA API,该命令将要创建的 HPA 如下:kind: HorizontalPodAutoscalerapiVersion: autoscaling/v2alpha1metadata: name: hello namespace: default labels: created-by: kubeless function: hellospec: scaleTargetRef: kind: Deployment name: hello minReplicas: 1 maxReplicas: 3 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 70该 HPA 计算目标 pod 数量的公式如下:TargetNumOfPods = ceil(sum(CurrentPodsCPUUtilization) / Target)基于 qps以下命令将为函数 hello 创建一个基于 qps 的 HPA,它将运行该函数的 pod 数量控制在 1 到 5 之间,并通过增加或减少 pod 个数确保所有挂在服务 hello 后的 pod 每秒能处理的请求次数之和达到 2000。kubeless autoscale create hello –metric=qps –min=1 –max=5 –value=2k该命令将要创建的 HPA 如下:kind: HorizontalPodAutoscalerapiVersion: autoscaling/v2alpha1metadata: name: hello namespace: default labels: created-by: kubeless function: hellospec: scaleTargetRef: kind: Deployment name: hello minReplicas: 1 maxReplicas: 5 metrics: - type: Object object: metricName: function_calls target: apiVersion: autoscaling/v2beta1 kind: Service name: hello targetValue: 2k基于多项指标如果计划基于多项度量指标对函数进行自动伸缩,需要直接为运行 function 的 deployment 创建 HPA。使用如下 yaml 文件可以为函数 hello 创建一个名为hello-cpu-and-memory的 HPA,它将运行该函数的 pod 数量控制在 1 到 10 之间,并尝试让所有 pod 的平均 cpu 使用率维持在 50%,平均 memory 使用量维持在 200MB。对于多项度量指标,K8s 会计算出每项指标需要的 pod 数量,取其中的最大值作为最终的目标 pod 数量。kind: HorizontalPodAutoscalerapiVersion: autoscaling/v2alpha1metadata: name: hello-cpu-and-memory namespace: default labels: created-by: kubeless function: hellospec: scaleTargetRef: kind: Deployment name: hello minReplicas: 1 maxReplicas: 10 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 50 - type: Resource resource: name: memory targetAverageValue: 200Mi自动伸缩策略一个理想的自动伸缩策略应当处理好下列场景:当负载激增时,函数能迅速扩展以应对突发流量;当负载下降时,函数能立即收缩以节省资源消耗;具备抗噪声干扰能力,能够精确计算出目标容量;能够避免自动伸缩过于频繁造成系统抖动。Kubeless 依赖的 HPA 充分考虑了上述情形,不断改进和完善其使用的自动伸缩策略。下面以 K8s 1.13 版本为例描述该策略。如果想要更加深入地了解策略原理请参考链接 horizontal。HPA 每隔一段时间会根据获取的度量数据同步一次和该 HPA 关联的 RC / Deployment 中的 pod 个数,时间间隔通过 kube-controller-manager 的参数–horizontal-pod-autoscaler-sync-period指定,默认为 15s。在每一次同步过程中,HPA 需要经历如下图所示的计算流程。计算目标副本数分别计算 HPA 列表中每项指标需要的 pod 数量,记为 replicaCountProposal。选择其中的最大值作为 metricDesiredReplicas。在计算每项指标的 replicaCountProposal 过程中会考虑下列因素:允许目标度量值和实际度量值存在一定程度的误差,如果在误差范围内直接使用 currentReplicas 作为 replicaCountProposal。这样做是为了在可接受范围内避免伸缩过于频繁造成系统抖动,该误差值可以通过 kube-controller-manager 的参数–horizontal-pod-autoscaler-tolerance指定,默认值是 0.1。当一个 pod 刚刚启动时,该 pod 反映的度量值往往不是很准确,HPA 会将这种 pod 视为 unready。在计算度量值时,HPA 会跳过处于 unready 状态的 pod。这样做是为了消除噪声干扰,可以通过 kube-controller-manager 的参数–horizontal-pod-autoscaler-cpu-initialization-period(默认为 5 分钟)和–horizontal-pod-autoscaler-initial-readiness-delay(默认为 30 秒)调整 pod 被认为处于 unready 状态的时间。平滑目标副本数将最近一段时间计算出的 metricDesiredReplicas 记录下来,取其中的最大值作为 stabilizedRecommendation。这样做是为了让缩容过程变得平滑,消除度量数据异常波动造成的影响。该时间段可以通过参数–horizontal-pod-autoscaler-downscale-stabilization-window指定,默认为 5 分钟。规范目标副本数限制 desiredReplicas 最大为 currentReplicas * scaleUpLimitFactor,这样做是为了防止因 采集到了“虚假的”度量数据造成扩容过快。目前 scaleUpLimitFactor 无法通过参数设定,其值固定为 2。限制 desiredReplicas 大于等于 hpaMinReplicas,小于等于 hpaMaxReplicas。执行扩容缩容操作如果通过上述步骤计算出的 desiredReplicas 不等于 currentReplicas,则“执行”扩容缩容操作。这里所说的执行只是将 desiredReplicas 赋值给 RC / Deployment 中的 replicas,pod 的创建销毁会由 kube-scheduler 和 worker node 上的 kubelet 异步完成的。小结Kubeless 提供的自动伸缩功能是对 K8s HPA 的简单封装,避免了将创建 HPA 的复杂细节直接暴露给用户。Kubeless 目前提供的度量指标过少,功能过于简单。如果用户希望基于新的度量指标、综合多项度量指标或者调整自动伸缩的效果,需要深入了解 HPA 的细节。目前 HPA 的扩容缩容策略是基于既成事实被动地调整目标副本数,还无法根据历史规律预测性地进行扩容缩容。总结Kubeless 基于 K8s 提供了较为完整的 serverless 解决方案,但和一些商业 serverless 产品还存在一定差距:Kubeless 并未在镜像拉取、代码下载、容器启动等方面做过多优化,导致函数冷启动时间过长;Kubeless 并未过多考虑多租户的问题,如果希望多个用户的函数运行在同一个集群里,还需要进行二次开发。本文作者:吴波bruce_wu阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 6 min · jiezi

深度学习Trick——用权重约束减轻深层网络过拟合|附(Keras)实现代码

摘要: 深度学习小技巧,约束权重以降低模型过拟合的可能,附keras实现代码。在深度学习中,批量归一化(batch normalization)以及对损失函数加一些正则项这两类方法,一般可以提升模型的性能。这两类方法基本上都属于权重约束,用于减少深度学习神经网络模型对训练数据的过拟合,并改善模型对新数据的性能。目前,存在多种类型的权重约束方法,例如最大化或单位向量归一化,有些方法也必须需要配置超参数。在本教程中,使用Keras API,用于向深度学习神经网络模型添加权重约束以减少过拟合。完成本教程后,您将了解:如何使用Keras API创建向量范数约束;如何使用Keras API为MLP、CNN和RNN层添加权重约束;如何通过向现有模型添加权重约束来减少过度拟合;下面,让我们开始吧。本教程分为三个部分:Keras中的权重约束;图层上的权重约束;权重约束案例研究;Keras中权重约束Keras API支持权重约束,且约束可以按每层指定。使用约束通常涉及在图层上为输入权重设置kernel_constraint参数,偏差权重设置为bias_constraint。通常,权重约束方法不涉及偏差权重。一组不同的向量规范在keras.constraints模块可以用作约束:最大范数(max_norm):强制权重等于或低于给定限制;非负规范(non_neg):强制权重为正数;单位范数(unit_norm):强制权重为1.0;Min-Max范数(min_max_norm):强制权重在一个范围之间;例如,可以导入和实例化约束:# import normfrom keras.constraints import max_norm# instantiate normnorm = max_norm(3.0)图层上的权重约束权重规范可用于Keras的大多数层,下面介绍一些常见的例子:MLP权重约束下面的示例是在全连接层上设置最大范数权重约束:# example of max norm on a dense layerfrom keras.layers import Densefrom keras.constraints import max_norm…model.add(Dense(32, kernel_constraint=max_norm(3), bias_constraint==max_norm(3)))…CNN权重约束下面的示例是在卷积层上设置最大范数权重约束:# example of max norm on a cnn layerfrom keras.layers import Conv2Dfrom keras.constraints import max_norm…model.add(Conv2D(32, (3,3), kernel_constraint=max_norm(3), bias_constraint==max_norm(3)))…RNN权重约束与其他图层类型不同,递归神经网络允许我们对输入权重和偏差以及循环输入权重设置权重约束。通过图层的recurrent_constraint参数设置递归权重的约束。下面的示例是在LSTM图层上设置最大范数权重约束:# example of max norm on an lstm layerfrom keras.layers import LSTMfrom keras.constraints import max_norm…model.add(LSTM(32, kernel_constraint=max_norm(3), recurrent_constraint=max_norm(3), bias_constraint==max_norm(3)))…基于以上的基本知识,下面进行实例实践。权重约束案例研究在本节中,将演示如何使用权重约束来减少MLP对简单二元分类问题的过拟合问题。此示例只是提供了一个模板,读者可以举一反三,将权重约束应用于自己的神经网络以进行分类和回归问题。二分类问题使用标准二进制分类问题来定义两个半圆观察,每个类一个半圆。其中,每个观测值都有两个输入变量,它们具有相同的比例,输出值分别为0或1,该数据集也被称为“ 月亮”数据集,这是由于绘制时,每个类中出现组成的形状类似于月亮。可以使用make_moons()函数生成观察结果,设置参数为添加噪声、随机关闭,以便每次运行代码时生成相同的样本。# generate 2d classification datasetX, y = make_moons(n_samples=100, noise=0.2, random_state=1)可以在图表上绘制两个变量x和y坐标,并将数据点所属的类别的颜色作为观察的颜色。下面列出生成数据集并绘制数据集的完整示例:# generate two moons datasetfrom sklearn.datasets import make_moonsfrom matplotlib import pyplotfrom pandas import DataFrame# generate 2d classification datasetX, y = make_moons(n_samples=100, noise=0.2, random_state=1)# scatter plot, dots colored by class valuedf = DataFrame(dict(x=X[:,0], y=X[:,1], label=y))colors = {0:‘red’, 1:‘blue’}fig, ax = pyplot.subplots()grouped = df.groupby(’label’)for key, group in grouped: group.plot(ax=ax, kind=‘scatter’, x=‘x’, y=‘y’, label=key, color=colors[key])pyplot.show()运行该示例会创建一个散点图,可以从图中看到,对应类别显示的图像类似于半圆形或月亮形状。上图的数据集表明它是一个很好的测试问题,因为不能用直线划分,需要非线性方法,比如神经网络来解决。只生成了100个样本,这对于神经网络而言较小,也提供了过拟合训练数据集的概率,并且在测试数据集上具有更高的误差。因此,也是应用正则化的一个好例子。此外,样本具有噪声,使模型有机会学习不一致的样本的各个方面。多层感知器过拟合在机器学习力,MLP模型可以解决这类二进制分类问题。MLP模型只具有一个隐藏层,但具有比解决该问题所需的节点更多的节点,从而提供过拟合的可能。在定义模型之前,需要将数据集拆分为训练集和测试集,按照3:7的比例将数据集划分为训练集和测试集。# generate 2d classification datasetX, y = make_moons(n_samples=100, noise=0.2, random_state=1)# split into train and testn_train = 30trainX, testX = X[:n_train, :], X[n_train:, :]trainy, testy = y[:n_train], y[n_train:]接下来,定义模型。隐藏层的节点数设置为500、激活函数为RELU,但在输出层中使用Sigmoid激活函数以预测输出类别为0或1。该模型使用二元交叉熵损失函数进行优化,这类激活函数适用于二元分类问题和Adam版本梯度下降方法。# define modelmodel = Sequential()model.add(Dense(500, input_dim=2, activation=‘relu’))model.add(Dense(1, activation=‘sigmoid’))model.compile(loss=‘binary_crossentropy’, optimizer=‘adam’, metrics=[‘accuracy’])然后,设置迭代次数为4,000次,默认批量训练样本数量为32。# fit modelhistory = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=4000, verbose=0)这里将测试数据集作为验证数据集验证算法的性能:# evaluate the model_, train_acc = model.evaluate(trainX, trainy, verbose=0), test_acc = model.evaluate(testX, testy, verbose=0)print(‘Train: %.3f, Test: %.3f’ % (train_acc, test_acc))最后,绘制出模型每个时期在训练和测试集上性能。如果模型确实对训练数据集过拟合了,对应绘制的曲线将会看到,模型在训练集上的准确度继续增加,而测试集上的性能是先上升,之后下降。# plot historypyplot.plot(history.history[‘acc’], label=‘train’)pyplot.plot(history.history[‘val_acc’], label=‘test’)pyplot.legend()pyplot.show()将以上过程组合在一起,列出完整示例:# mlp overfit on the moons datasetfrom sklearn.datasets import make_moonsfrom keras.layers import Densefrom keras.models import Sequentialfrom matplotlib import pyplot# generate 2d classification datasetX, y = make_moons(n_samples=100, noise=0.2, random_state=1)# split into train and testn_train = 30trainX, testX = X[:n_train, :], X[n_train:, :]trainy, testy = y[:n_train], y[n_train:]# define modelmodel = Sequential()model.add(Dense(500, input_dim=2, activation=‘relu’))model.add(Dense(1, activation=‘sigmoid’))model.compile(loss=‘binary_crossentropy’, optimizer=‘adam’, metrics=[‘accuracy’])# fit modelhistory = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=4000, verbose=0)# evaluate the model, train_acc = model.evaluate(trainX, trainy, verbose=0), test_acc = model.evaluate(testX, testy, verbose=0)print(‘Train: %.3f, Test: %.3f’ % (train_acc, test_acc))# plot historypyplot.plot(history.history[‘acc’], label=‘train’)pyplot.plot(history.history[‘val_acc’], label=‘test’)pyplot.legend()pyplot.show()运行该示例,给出模型在训练数据集和测试数据集上的性能。可以看到模型在训练数据集上的性能优于测试数据集,这是发生过拟合的标志。鉴于神经网络和训练算法的随机性,每次仿真的具体结果可能会有所不同。因为模型是过拟合的,所以通常不会期望在相同数据集上能够重复运行得到相同的精度。Train: 1.000, Test: 0.914创建一个图,显示训练和测试集上模型精度的线图。从图中可以看到模型过拟合时的预期形状,其中测试精度达到一个临界点后再次开始减小。具有权重约束的多层感知器过拟合为了和上面做对比,现在对MLP使用权重约束。目前,有一些不同的权重约束方法可供选择。本文选用一个简单且好用的约束——简单地标准化权重,使得其范数等于1.0,此约束具有强制所有传入权重变小的效果。在Keras中可以通过使用unit_norm来实现,并且将此约束添加到第一个隐藏层,如下所示:model.add(Dense(500, input_dim=2, activation=‘relu’, kernel_constraint=unit_norm()))此外,也可以通过使用min_max_norm并将min和maximum设置为1.0 来实现相同的结果,例如:model.add(Dense(500, input_dim=2, activation=‘relu’, kernel_constraint=min_max_norm(min_value=1.0, max_value=1.0)))但是无法通过最大范数约束获得相同的结果,因为它允许规范等于或低于指定的限制; 例如:model.add(Dense(500, input_dim=2, activation=‘relu’, kernel_constraint=max_norm(1.0)))下面列出具有单位规范约束的完整代码:# mlp overfit on the moons dataset with a unit norm constraintfrom sklearn.datasets import make_moonsfrom keras.layers import Densefrom keras.models import Sequentialfrom keras.constraints import unit_normfrom matplotlib import pyplot# generate 2d classification datasetX, y = make_moons(n_samples=100, noise=0.2, random_state=1)# split into train and testn_train = 30trainX, testX = X[:n_train, :], X[n_train:, :]trainy, testy = y[:n_train], y[n_train:]# define modelmodel = Sequential()model.add(Dense(500, input_dim=2, activation=‘relu’, kernel_constraint=unit_norm()))model.add(Dense(1, activation=‘sigmoid’))model.compile(loss=‘binary_crossentropy’, optimizer=‘adam’, metrics=[‘accuracy’])# fit modelhistory = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=4000, verbose=0)# evaluate the model, train_acc = model.evaluate(trainX, trainy, verbose=0)_, test_acc = model.evaluate(testX, testy, verbose=0)print(‘Train: %.3f, Test: %.3f’ % (train_acc, test_acc))# plot historypyplot.plot(history.history[‘acc’], label=‘train’)pyplot.plot(history.history[‘val_acc’], label=‘test’)pyplot.legend()pyplot.show()运行该示例,给出模型在训练数据集和测试数据集上的性能。从下图可以看到,对权重进行严格约束确实提高了模型在验证集上的性能,并且不会影响训练集的性能。Train: 1.000, Test: 0.943从训练和测试精度曲线图来看,模型已经在训练数据集上不再过拟合了,且模型在训练和测试数据集的精度保持在一个稳定的水平。扩展本节列出了一些读者可能希望探索扩展的教程:报告权重标准:更新示例以计算网络权重的大小,并证明使用约束后,确实使得幅度更小;约束输出层:更新示例以将约束添加到模型的输出层并比较结果;约束偏置:更新示例以向偏差权重添加约束并比较结果;反复评估:更新示例以多次拟合和评估模型,并报告模型性能的均值和标准差;进一步阅读如果想进一步深入了解,下面提供一些有关该主题的其它资源:博客机器学习中矢量规范简介(Gentle Introduction to Vector Norms in Machine Learning)APIKeras Constraints APIKeras constraints.pyKerasCore Layers APIKeras Convolutional Layers APIKeras Recurrent Layers APIsklearn.datasets.make_moons API本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 30, 2018 · 2 min · jiezi

关于Flutter初始化流程,我必须告诉你的是...

引言最近在做性能优化的时候发现,在混合栈开发中,第一次启动Flutter页面的耗时总会是第二次启动Flutter页面耗时的两倍左右,这样给人感觉很不好。分析发现第一次启动Flutter页面会做一些初始化工作,借此,我梳理了下Flutter的初始化流程。2. Flutter初始化时序Flutter初始化主要分四部分,FlutterMain初始化、FlutterNativeView初始化、FlutterView初始化和Flutter Bundle初始化。我们先看下Flutter初始化的时序图,来整体把握下Flutter初始化的一般流程: Flutter初始化时序3. 具体分析3.1 FlutterMain初始化这部分初始化工作是由Application.onCreate方法中调用开始的,在Application创建的时候就会初始化完成,不会影响Flutter页面的第一次启动,所以这里只是做一个简单分析。 从FlutterMain.startInitialization方法代码中可以轻易看出来,初始化主要分四部分。 前面三部分比较类似,分别是初始化配置信息、初始化AOT编译和初始化资源,最后一部分则是加载Flutter的Native环境。 这部分感兴趣的同学可以看下FlutterMain.java源码,逻辑还是比较清晰的。public static void startInitialization(Context applicationContext, Settings settings) { // other codes … initConfig(applicationContext); initAot(applicationContext); initResources(applicationContext); System.loadLibrary(“flutter”); // other codes …}3.2 FlutterNativeView初始化先用一个图来展现FlutterNativeView构造函数的调用栈: FlutterNativeView构造函数调用栈从上图的调用栈中我们知道FlutterNativeView的初始化主要做了些什么,我们再从源码角度较为深入的了解下: FlutterNativeView的构造函数最终主要调用了一个nativeAttach方法。到这里就需要分析引擎层代码了,我们可以在JNI文件中找到对应的jni方法调用。(具体文件为platform_view_android_jni.cc)static const JNINativeMethod native_view_methods[] = { { .name = “nativeAttach”, .signature = “(Lio/flutter/view/FlutterNativeView;)J”, .fnPtr = reinterpret_cast<void*>(&shell::Attach), }, // other codes …};从代码中很容易看出FlutterNativeView.attach方法最终调用了shell::Attach方法,而shell::Attach方法主要做了两件事: 1. 创建PlatformViewAndroid。 2. 调用PlatformViewAndroid::Attach。static jlong Attach(JNIEnv* env, jclass clazz, jobject flutterView) { auto view = new PlatformViewAndroid(); // other codes … view->Attach(); // other codes …}那我们再分析下PlatformViewAndroid的构造函数和Attach方法都做了些什么呢?PlatformViewAndroid::PlatformViewAndroid() : PlatformView(std::make_unique<NullRasterizer>()), android_surface_(InitializePlatformSurface()) {}void PlatformViewAndroid::Attach() { CreateEngine(); // Eagerly setup the IO thread context. We have already setup the surface. SetupResourceContextOnIOThread(); UpdateThreadPriorities();}其中: 1. PlatformViewAndroid的构造函数主要是调用了InitializePlatformSurface方法,这个方法主要是初始化了Surface,其中Surface有Vulkan、OpenGL和Software三种类型的区别。 2. PlatformViewAndroid::Attach方法这里主要调用三个方法:CreateEngine、SetupResourceContextOnIOThread和UpdateThreadPriorities。 2.1 CreateEngine比较好理解,创建Engine,这里会重新创建一个Engine对象。 2.2 SetupResourceContextOnIOThread是在IO线程去准备资源的上下文逻辑。 2.3 UpdateThreadPriorities是设置线程优先级,这设置GPU线程优先级为-2,UI线程优先级为-1。3.3 FlutterView初始化FlutterView的初始化就是纯粹的Android层啦,所以相对比较简单。分析FlutterView.java的构造函数就会发现,整个FlutterView的初始化在确保FlutterNativeView的创建成功和一些必要的view设置之外,主要做了两件事: 1. 注册SurfaceHolder监听,其中surfaceCreated回调会作为Flutter的第一帧回调使用。 2. 初始化了Flutter系统需要用到的一系列桥接方法。例如:localization、navigation、keyevent、system、settings、platform、textinput。 FlutterView初始化流程主要如下图所示: FlutterView初始化3.4 Flutter Bundle初始化Flutter Bundle的初始化是由调用FlutterActivityDelegate.runFlutterBundle开始的,先用一张图来说明下runFlutterBundle方法的调用栈: Flutter的Bundle初始化我们再从源码角度较为深入了解下: FlutterActivity的onCreate方法在执行完FlutterActivityDelegate的onCreate方法之后会调用它的runFlutterBundle方法。FlutterActivityDelegate.runFlutterBundle代码如下:public void runFlutterBundle(){ // other codes … String appBundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext()); if (appBundlePath != null) { flutterView.runFromBundle(appBundlePath, null, “main”, reuseIsolate); }}很明显,这个runFlutterBundle并没有做太多事情,而且直接调用了FlutterView.runFromBundle方法。而后兜兜转转最后会调用到PlatformViewAndroid::RunBundleAndSnapshot方法。void PlatformViewAndroid::RunBundleAndSnapshot(JNIEnv* env, std::string bundle_path, std::string snapshot_override, std::string entrypoint, bool reuse_runtime_controller, jobject assetManager) { // other codes … blink::Threads::UI()->PostTask( [engine = engine_->GetWeakPtr(), asset_provider = std::move(asset_provider), bundle_path = std::move(bundle_path), entrypoint = std::move(entrypoint), reuse_runtime_controller = reuse_runtime_controller] { if (engine) engine->RunBundleWithAssets( std::move(asset_provider), std::move(bundle_path), std::move(entrypoint), reuse_runtime_controller); });}PlatformViewAndroid::RunBundleAndSnapshot在UI线程中调用Engine::RunBundleWithAssets,最终调用Engine::DoRunBundle。 DoRunBundle方法最后只会调用RunFromPrecompiledSnapshot、RunFromKernel和RunFromScriptSnapshot三个方法中的一个。而这三个方法最终都会调用SendStartMessage方法。bool DartController::SendStartMessage(Dart_Handle root_library, const std::string& entrypoint) { // other codes … // Get the closure of main(). Dart_Handle main_closure = Dart_GetClosure( root_library, Dart_NewStringFromCString(entrypoint.c_str())); // other codes … // Grab the ‘dart:isolate’ library. Dart_Handle isolate_lib = Dart_LookupLibrary(ToDart(“dart:isolate”)); DART_CHECK_VALID(isolate_lib); // Send the start message containing the entry point by calling // _startMainIsolate in dart:isolate. const intptr_t kNumIsolateArgs = 2; Dart_Handle isolate_args[kNumIsolateArgs]; isolate_args[0] = main_closure; isolate_args[1] = Dart_Null(); Dart_Handle result = Dart_Invoke(isolate_lib, ToDart("_startMainIsolate"), kNumIsolateArgs, isolate_args); return LogIfError(result);}而SendStartMessage方法主要做了三件事: 1. 获取Flutter入口方法(例如main方法)的closure。2. 获取FlutterLibrary。 3. 发送消息来调用Flutter的入口方法。4. 总结一下本次主要分析了下FlutterActivity的onCreate方法中的Flutter初始化部分逻辑,很明显会发现主要耗时在FlutterNativeView、FlutterView和Flutter Bundle的初始化这三块,将这三部分的初始化工作前置就可以比较容易的解决引言中提出的问题。经测试发现,这样改动之后,Flutter页面第一次启动时长和后面几次启动时长差不多一样了。 对于FlutterMain.startInitialization的初始化逻辑、SendStartMessage发送的消息如何最终调用Flutter中的入口方法逻辑没有进一步深入分析,这些内容后续再继续分析撰文分享。本文作者:闲鱼技术-然道阅读原文本文为云栖社区原创内容,未经允许不得转载。

November 26, 2018 · 2 min · jiezi

机器学习基础:(Python)训练集测试集分割与交叉验证

摘要: 本文讲述了如何用Python对训练集测试集进行分割与交叉验证。在上一篇关于Python中的线性回归的文章之后,我想再写一篇关于训练测试分割和交叉验证的文章。在数据科学和数据分析领域中,这两个概念经常被用作防止或最小化过度拟合的工具。我会解释当使用统计模型时,通常将模型拟合在训练集上,以便对未被训练的数据进行预测。在统计学和机器学习领域中,我们通常把数据分成两个子集:训练数据和测试数据,并且把模型拟合到训练数据上,以便对测试数据进行预测。当做到这一点时,可能会发生两种情况:模型的过度拟合或欠拟合。我们不希望出现这两种情况,因为这会影响模型的可预测性。我们有可能会使用具有较低准确性或不常用的模型(这意味着你不能泛化对其它数据的预测)。什么是模型的过度拟合(Overfitting)和欠拟合(Underfitting)?过度拟合过度拟合意味着模型训练得“太好”了,并且与训练数据集过于接近了。这通常发生在模型过于复杂的情况下,模型在训练数据上非常的准确,但对于未训练数据或者新数据可能会很不准确。因为这种模型不是泛化的,意味着你可以泛化结果,并且不能对其它数据进行任何推断,这大概就是你要做的。基本上,当发生这种情况时,模型学习或描述训练数据中的“噪声”,而不是数据中变量之间的实际关系。这种噪声显然不是任何新数据集的一部分,不能应用于它。欠拟合与过度拟合相反,当模型欠拟合的时候,它意味着模型不适合训练数据,因此会错过数据中的趋势特点。这也意味着该模型不能被泛化到新的数据上。你可能猜到了,这通常是模型非常简单的结果。例如,当我们将线性模型(如线性回归)拟合到非线性的数据时,也会发生这种情况。不言而喻,该模型对训练数据的预测能力差,并且还不能推广到其它的数据上。值得注意的是,欠拟合不像过度拟合那样普遍。然而,我们希望避免数据分析中的这两个问题。你可能会说,我们正在试图找到模型的欠拟合与过度拟合的中间点。像你所看到的,训练测试分割和交叉验证有助于避免过度拟合超过欠拟合。训练测试分割正如我之前所说的,我们使用的数据通常被分成训练数据和测试数据。训练集包含已知的输出,并且模型在该数据上学习,以便以后将其泛化到其它数据上。我们有测试数据集(或子集),为了测试模型在这个子集上的预测。我们将使用Scikit-Learn library,特别是其中的训练测试分割方法。我们将从导入库开始:快速地看一下导入的库:Pandas —将数据文件作为Pandas数据帧加载,并对数据进行分析;在Sklearn中,我导入了数据集模块,因此可以加载一个样本数据集和linear_model,因此可以运行线性回归;在Sklearn的子库model_selection中,我导入了train_test_split,因此可以把它分成训练集和测试集;在Matplotlib中,我导入了pyplot来绘制数据图表;好了,一切都准备就绪,让我们输入糖尿病数据集,将其转换成数据帧并定义列的名称:现在我们可以使用train_test_split函数来进行分割。函数内的test_size=0.2表明了要测试的数据的百分比,通常是80/20或70/30左右。# create training and testing varsX_train, X_test, y_train, y_test = train_test_split(df, y, test_size=0.2)print X_train.shape, y_train.shapeprint X_test.shape, y_test.shape(353, 10) (353,)(89, 10) (89,)现在我们将在训练数据上拟合模型:# fit a modellm = linear_model.LinearRegression()model = lm.fit(X_train, y_train)predictions = lm.predict(X_test)正如所看到的那样,我们在训练数据上拟合模型并尝试预测测试数据。让我们看一看都预测了什么:predictions[0:5]array([ 205.68012533, 64.58785513, 175.12880278, 169.95993301, 128.92035866])注:因为我在预测之后使用了[0:5],它只显示了前五个预测值。去掉[0:5]的限制就会使它输出我们模型创建的所有预测值。让我们来绘制模型:## The line / modelplt.scatter(y_test, predictions)plt.xlabel(“True Values”)plt.ylabel(“Predictions”)打印准确度得分:print “Score:”, model.score(X_test, y_test)Score: 0.485829586737总结:将数据分割成训练集和测试集,将回归模型拟合到训练数据,基于该数据做出预测,并在测试数据上测试预测结果。但是训练和测试的分离确实有其危险性,如果我们所做的分割不是随机的呢?如果我们数据的一个子集只包含来自某个州的人,或者具有一定收入水平但不包含其它收入水平的员工,或者只有妇女,或者只有某个年龄段的人,那该怎么办呢?这将导致过度拟合,即使我们试图避免,这就是交叉验证要派上用场了。交叉验证在前一段中,我提到了训练测试分割方法中的注意事项。为了避免这种情况,我们可以执行交叉验证。它非常类似于训练测试分割,但是被应用于更多的子集。意思是,我们将数据分割成k个子集,并训练第k-1个子集。我们要做的是,为测试保留最后一个子集。有一组交叉验证方法,我来介绍其中的两个:第一个是K-Folds Cross Validation,第二个是Leave One Out Cross Validation (LOOCV)。K-Folds 交叉验证在K-Folds交叉验证中,我们将数据分割成k个不同的子集。我们使用第k-1个子集来训练数据,并留下最后一个子集作为测试数据。然后,我们对每个子集模型计算平均值,接下来结束模型。之后,我们对测试集进行测试。这里有一个在Sklearn documentation上非常简单的K-Folds例子:fromsklearn.model_selection import KFold # import KFoldX = np.array([[1, 2], [3, 4], [1, 2], [3, 4]]) # create an arrayy = np.array([1, 2, 3, 4]) # Create another arraykf = KFold(n_splits=2) # Define the split - into 2 folds kf.get_n_splits(X) # returns the number of splitting iterations in the cross-validatorprint(kf) KFold(n_splits=2, random_state=None, shuffle=False)让我们看看结果:fortrain_index, test_index in kf.split(X): print(“TRAIN:”, train_index, “TEST:”, test_index)X_train, X_test = X[train_index], X[test_index]y_train, y_test = y[train_index], y[test_index](‘TRAIN:’, array([2, 3]), ‘TEST:’, array([0, 1]))(‘TRAIN:’, array([0, 1]), ‘TEST:’, array([2, 3]))正如看到的,函数将原始数据拆分成不同的数据子集。这是个非常简单的例子,但我认为它把概念解释的相当好。弃一法交叉验证(Leave One Out Cross Validation,LOOCV)这是另一种交叉验证的方法,弃一法交叉验证。可以在Sklearn website上查看。在这种交叉验证中,子集的数量等于我们在数据集中观察到的数量。然后,我们计算所有子集的平均数,并利用平均值建立模型。然后,对最后一个子集测试模型。因为我们会得到大量的训练集(等于样本的数量),因此这种方法的计算成本也相当高,应该在小数据集上使用。如果数据集很大,最好使用其它的方法,比如kfold。让我们看看Sklearn上的另一个例子:fromsklearn.model_selectionimportLeaveOneOutX = np.array([[1, 2], [3, 4]])y = np.array([1, 2])loo = LeaveOneOut()loo.get_n_splits(X)fortrain_index, test_indexinloo.split(X): print(“TRAIN:”, train_index, “TEST:”, test_index)X_train, X_test = X[train_index], X[test_index]y_train, y_test = y[train_index], y[test_index] print(X_train, X_test, y_train, y_test)以下是输出:(‘TRAIN:’, array([1]), ‘TEST:’, array([0]))(array([[3, 4]]), array([[1, 2]]), array([2]), array([1]))(‘TRAIN:’, array([0]), ‘TEST:’, array([1]))(array([[1, 2]]), array([[3, 4]]), array([1]), array([2]))那么,我们应该使用什么方法呢?使用多少子集呢?拥有的子集越多,我们将会由于偏差而减少误差,但会由于方差而增加误差;计算成本也会上升,显然,拥有的子集越多,计算所需的时间就越长,也将需要更多的内存。如果利用数量较少的子集,我们减少了由于方差而产生的误差,但是由于偏差引起的误差会更大。它的计算成本也更低。因此,在大数据集中,通常建议k=3。在更小的数据集中,正如我之前提到的,最好使用弃一法交叉验证。让我们看看以前用过的一个例子,这次使用的是交叉验证。我将使用cross_val_predict函数来给每个在测试切片中的数据点返回预测值。# Necessary imports: from sklearn.cross_validation import cross_val_score, cross_val_predictfrom sklearn import metrics之前,我给糖尿病数据集建立了训练测试分割,并拟合了一个模型。让我们看看在交叉验证之后的得分是多少:# Perform 6-fold cross validationscores = cross_val_score(model, df, y, cv=6)print “Cross-validated scores:”, scoresCross-validated scores: [ 0.4554861 0.46138572 0.40094084 0.55220736 0.43942775 0.56923406]正如你所看到的,最后一个子集将原始模型的得分从0.485提高到0.569。这并不是一个惊人的结果,但我们得到了想要的。现在,在进行交叉验证之后,让我们绘制新的预测图:# Make cross validated predictionspredictions = cross_val_predict(model, df, y, cv=6)plt.scatter(y, predictions)你可以看到这和原来的图有很大的不同,是原来图的点数的六倍,因为我用的cv=6。最后,让我们检查模型的R²得分(R²是一个“表示与自变量分离的可预测的因变量中方差的比例的数量”)。可以看一下我们的模型有多准确:accuracy = metrics.r2_score(y, predictions)print “Cross-Predicted Accuracy:”, accuracyCross-Predicted Accuracy: 0.490806583864本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 23, 2018 · 2 min · jiezi

函数计算 Python 连接 SQL Server 小结

python 连接数据库通常要安装第三方模块,连接 MS SQL Server 需要安装 pymssql 。由于 pymsql 依赖于 FreeTDS,对于先于 2.1.3 版本的 pymssql,需要先安装 FreeTDS。由于早期版本的 pymssql 只提供了 windows 下的 wheel 打包,其他平台(如 linux)需要从源码包编译安装,那需要先安装 freetds-dev 包,以提供必要的头文件。函数计算的 runtime 运行时的目录是只读的,所以对于需要使用 apt-get 和 pip 安装依赖的场景,需要将依赖安装在代码目录而不是系统目录。具体安装方法可以参考《函数计算安装依赖库方法小结》。而 pymssql 的老版本涉及到编译安装,比常见的二级制安装到本地目录略复杂一些。函数计算依赖安装需要有个模拟的 linux 环境,从前我们推荐使用 fcli shell 的 sbox ,启动一个接近生产环境的 docker container 进行依赖安装。因为有些依赖是平台相关的,在 mac 系统安装的动态链接库无法在函数计算的 linux 环境下运行, pymssql 恰好属于这种情况。本文我将使用 fc-docker 进行安装和本地测试。下面的例子是基于函数计算 runtime python3.6 的,对于 python2.7 也进行了测试,同样适用。准备测试环境首先使用 docker 在本机 Mac 电脑下运行一个 SQL Server 2017 服务,并初始化表结构,编辑一个 index.py 的测试文件,以验证数据库访问是否成功。$ docker pull mcr.microsoft.com/mssql/server:2017-latest$ docker run -e ‘ACCEPT_EULA=Y’ -e ‘SA_PASSWORD=Codelife.me’ \ -p 1433:1433 –name sql1 \ -d mcr.microsoft.com/mssql/server:2017-latest将 SQL Server 启动于 1433 端口,并设定 SA 账户密码为 Codelife.me$ brew tap microsoft/mssql-release https://github.com/Microsoft/homebrew-mssql-release$ brew update$ ACCEPT_EULA=y brew install –no-sandbox msodbcsql mssql-tools使用 homebrew 安装 mssql 客户端 sqlcmd。$ sqlcmd -S localhost -U SA -P ‘Codelife.me'1>CREATE DATABASE TestDB2>SELECT Name from sys.Databases3>GOName———————————————–mastertempdbmodelmsdbTestDB(5 rows affected)创建测试数据库 TestDB。1> USE TestDB2> CREATE TABLE Inventory (id INT, name NVARCHAR(50), quantity INT)3> INSERT INTO Inventory VALUES (1, ‘banana’, 150); INSERT INTO Inventory VALUES (2, ‘orange’, 154);4> GOChanged database context to ‘TestDB’.(1 rows affected)(1 rows affected)创建一张 Inventory 表,并参入一行测试数据。1> SELECT * FROM Inventory WHERE quantity > 152;2> GOid name quantity———– ————————————————– ———– 2 orange 154(1 rows affected)1> QUIT验证一下插入结果并退出。准备一个测试函数import pymssqldef handler(event, context): conn = pymssql.connect( host=r’docker.for.mac.host.internal’, user=r’SA’, password=r’Codelife.me’, database=‘TestDB’ ) cursor = conn.cursor() cursor.execute(‘SELECT * FROM inventory WHERE quantity > 152’) result = ’’ for row in cursor: result += ‘row = %r\n’ % (row,) conn.close() return result编写一个测试函数 index.py。该函数连接 mac 宿主机docker.for.mac.host.internal (这里不能是 localhost,因为 fc-docker 会将函数运行在 container 内部)的 SQL Server 服务。执行一个查询,并把结果返回出来。最新版的 pymssql创建一个空目录,存放上 index.py 文件。将命令会话的当前路径切换到 index.py 所在的目录,然后执行$ docker run –rm –name mssql-builder -t -d -v $(pwd):/code –entrypoint /bin/sh aliyunfc/runtime-python3.6$ docker exec -t mssql-builder pip install -t /code pymssql$ docker stop mssql-builder这里使用了 fc-docker 提供的 python3.6 的模拟环境:aliyunfc/runtime-python3.6第一行启动了一个不会退出的 docker container,第二行使用 docker exec 进入这个 container 安装依赖,最后一行退出该 container。因为本地路径 &dollar;(pwd) 被挂载到 container 内部的 /code 目录,所以 container 退出以后 /code 目录的内容还会保留在本地当前路径下。pip 通过 -t 参数将 wheel 包安装在 /code 目录下。$ docker run –rm -v $(pwd):/code aliyunfc/runtime-python3.6 –handler index.handlerrow = (2, ‘orange’, 154)RequestId: d66496e9-4056-492b-98d9-5bf51e448174 Billed Duration: 144 ms Memory Size: 19执行上面命令可以顺利返回结果。对于不需要使用老本 pymssql 的用户看到这里就可以结束了。早期版本的 pymssql对于早于 2.1.3 版本的 pymssql, pip install 会触发源码编译安装,对于这种情况,需要安装编译时依赖的 freetds-dev,以及运行时依赖的 libsybdb5。编译时依赖可以直接安装在系统目录里,运行时依赖必须安装在本地目录下。docker run –rm –name mssql-builder -t -d -v $(pwd):/code –entrypoint /bin/sh aliyunfc/runtime-python3.6docker exec -t mssql-builder apt-get install -y -d -o=dir::cache=/code libsybdb5docker exec -t mssql-builder bash -c ‘for f in $(ls /code/archives/*.deb); do dpkg -x $f $(pwd) ; done;‘docker exec -t mssql-builder bash -c “rm -rf /code/archives/; mkdir /code/lib;cd /code/lib; ln -sf ../usr/lib/x86_64-linux-gnu/libsybdb.so.5 .“docker exec -t mssql-builder apt-get install -y freetds-dev docker exec -t mssql-builder pip install cython docker exec -t mssql-builder pip install -t /code pymssql==2.1.3docker stop mssql-builder第一行启动一个 container,第十行停止并自动删除该 container。第二行至第三行将运行时依赖 libsybdb5 安装于本地目录。将动态链接库 libsybdb.so.5 链接到目录 /code/lib 目录下,因为该目录默认配置到了环境变量 LD_LIBRARY_PATH 下。将 freetds-dev 和 cython 安装到系统目录,用于 pymssql 编译安装,因为运行时 pymssql 不需要这两个库,所以无需安装在本地目录安装 2.1.3 版本的 pymssql,从 2.1.4 版本开始已经不需要源码安装了。$ docker run –rm -v $(pwd):/code aliyunfc/runtime-python3.6 –handler index.handlerrow = (2, ‘orange’, 154)RequestId: d66496e9-4056-492b-98d9-5bf51e448174 Billed Duration: 144 ms Memory Size: 19测试通过。小结这是一份来迟的函数计算使用 sql server 数据库的配置文档。当前版本的 pymssql 已经不再需要源码安装了。但是 pip 源码包安装的方法,对于其他类似的场景也是适用的。本文也提供了一种基于 fc-docker 的配置和调试方法,不同 fcli 的 sbox,fc-docker 可以写成脚本反复执行,并且也可以用于本地模拟执行,对于 CI 场景非常有帮助。参考阅读http://www.pymssql.org/en/latest/intro.html#installhttp://www.freetds.org/http://www.pymssql.org/en/stable/pymssql_examples.htmlhttps://docs.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-2017https://cloudblogs.microsoft.com/sqlserver/2017/05/16/sql-server-command-line-tools-for-macos-released/本文作者:倚贤阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 22, 2018 · 3 min · jiezi

ES6 系列之 Babel 是如何编译 Class 的(下)

前言在上一篇 《 ES6 系列 Babel 是如何编译 Class 的(上)》,我们知道了 Babel 是如何编译 Class 的,这篇我们学习 Babel 是如何用 ES5 实现 Class 的继承。ES5 寄生组合式继承function Parent (name) { this.name = name;}Parent.prototype.getName = function () { console.log(this.name)}function Child (name, age) { Parent.call(this, name); this.age = age;}Child.prototype = Object.create(Parent.prototype);var child1 = new Child(‘kevin’, ‘18’);console.log(child1);原型链示意图为:关于寄生组合式继承我们在 《JavaScript深入之继承的多种方式和优缺点》 中介绍过。引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。ES6 extendClass 通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。以上 ES5 的代码对应到 ES6 就是:class Parent { constructor(name) { this.name = name; }}class Child extends Parent { constructor(name, age) { super(name); // 调用父类的 constructor(name) this.age = age; }}var child1 = new Child(‘kevin’, ‘18’);console.log(child1);值得注意的是:super 关键字表示父类的构造函数,相当于 ES5 的 Parent.call(this)。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。也正是因为这个原因,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。子类的 proto在 ES6 中,父类的静态方法,可以被子类继承。举个例子:class Foo { static classMethod() { return ‘hello’; }}class Bar extends Foo {}Bar.classMethod(); // ‘hello’这是因为 Class 作为构造函数的语法糖,同时有 prototype 属性和 proto 属性,因此同时存在两条继承链。(1)子类的 proto 属性,表示构造函数的继承,总是指向父类。(2)子类 prototype 属性的 proto 属性,表示方法的继承,总是指向父类的 prototype 属性。class Parent {}class Child extends Parent {}console.log(Child.proto === Parent); // trueconsole.log(Child.prototype.proto === Parent.prototype); // trueES6 的原型链示意图为:我们会发现,相比寄生组合式继承,ES6 的 class 多了一个 Object.setPrototypeOf(Child, Parent)的步骤。继承目标extends 关键字后面可以跟多种类型的值。class B extends A {}上面代码的 A,只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任意函数。除了函数之外,A 的值还可以是 null,当 extend null 的时候:class A extends null {}console.log(A.proto === Function.prototype); // trueconsole.log(A.prototype.proto === undefined); // trueBabel 编译那 ES6 的这段代码:class Parent { constructor(name) { this.name = name; }}class Child extends Parent { constructor(name, age) { super(name); // 调用父类的 constructor(name) this.age = age; }}var child1 = new Child(‘kevin’, ‘18’);console.log(child1);Babel 又是如何编译的呢?我们可以在 Babel 官网的 Try it out 中尝试:‘use strict’;function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”); } return call && (typeof call === “object” || typeof call === “function”) ? call : self;}function _inherits(subClass, superClass) { if (typeof superClass !== “function” && superClass !== null) { throw new TypeError(“Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.proto = superClass;}function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); }}var Parent = function Parent(name) { _classCallCheck(this, Parent); this.name = name;};var Child = function(_Parent) { _inherits(Child, _Parent); function Child(name, age) { _classCallCheck(this, Child); // 调用父类的 constructor(name) var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, name)); _this.age = age; return _this; } return Child;}(Parent);var child1 = new Child(‘kevin’, ‘18’);console.log(child1);我们可以看到 Babel 创建了 _inherits 函数帮助实现继承,又创建了 _possibleConstructorReturn 函数帮助确定调用父类构造函数的返回值,我们来细致的看一看代码。_inheritsfunction _inherits(subClass, superClass) { // extend 的继承目标必须是函数或者是 null if (typeof superClass !== “function” && superClass !== null) { throw new TypeError(“Super expression must either be null or a function, not " + typeof superClass); } // 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 proto 属性指向父类的 prototype 属性 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); // 设置子类的 proto 属性指向父类 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.proto = superClass;}关于 Object.create(),一般我们用的时候会传入一个参数,其实是支持传入两个参数的,第二个参数表示要添加到新创建对象的属性,注意这里是给新创建的对象即返回值添加属性,而不是在新创建对象的原型对象上添加。举个例子:// 创建一个以另一个空对象为原型,且拥有一个属性 p 的对象const o = Object.create({}, { p: { value: 42 } });console.log(o); // {p: 42}console.log(o.p); // 42再完整一点:const o = Object.create({}, { p: { value: 42, enumerable: false, // 该属性不可写 writable: false, configurable: true }});o.p = 24;console.log(o.p); // 42那么对于这段代码:subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });作用就是给 subClass.prototype 添加一个可配置可写不可枚举的 constructor 属性,该属性值为 subClass。_possibleConstructorReturn函数里是这样调用的:var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, name));我们简化为:var _this = _possibleConstructorReturn(this, Parent.call(this, name));_possibleConstructorReturn 的源码为:function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”); } return call && (typeof call === “object” || typeof call === “function”) ? call : self;}在这里我们判断 Parent.call(this, name) 的返回值的类型,咦?这个值还能有很多类型?对于这样一个 class:class Parent { constructor() { this.xxx = xxx; }}Parent.call(this, name) 的值肯定是 undefined。可是如果我们在 constructor 函数中 return 了呢?比如:class Parent { constructor() { return { name: ‘kevin’ } }}我们可以返回各种类型的值,甚至是 null:class Parent { constructor() { return null }}我们接着看这个判断:call && (typeof call === “object” || typeof call === “function”) ? call : self;注意,这句话的意思并不是判断 call 是否存在,如果存在,就执行 (typeof call === “object” || typeof call === “function”) ? call : self因为 && 的运算符优先级高于 ? :,所以这句话的意思应该是:(call && (typeof call === “object” || typeof call === “function”)) ? call : self;对于 Parent.call(this) 的值,如果是 object 类型或者是 function 类型,就返回 Parent.call(this),如果是 null 或者基本类型的值或者是 undefined,都会返回 self 也就是子类的 this。这也是为什么这个函数被命名为 _possibleConstructorReturn。总结var Child = function(_Parent) { _inherits(Child, _Parent); function Child(name, age) { _classCallCheck(this, Child); // 调用父类的 constructor(name) var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, name)); _this.age = age; return _this; } return Child;}(Parent);最后我们总体看下如何实现继承:首先执行 _inherits(Child, Parent),建立 Child 和 Parent 的原型链关系,即 Object.setPrototypeOf(Child.prototype, Parent.prototype) 和 Object.setPrototypeOf(Child, Parent)。然后调用 Parent.call(this, name),根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this。最终,根据子类构造函数,修改 _this 的值,然后返回该值。ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。本文作者:冴羽阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 16, 2018 · 4 min · jiezi

ES6 系列之 defineProperty 与 proxy

前言我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象:var obj = {value: 1},我们该怎么知道 obj 发生了改变呢?definePropetyES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。语法Object.defineProperty(obj, prop, descriptor)参数obj: 要在其上定义属性的对象。prop: 要定义或修改的属性的名称。descriptor: 将被定义或修改的属性的描述符。举个例子:var obj = {};Object.defineProperty(obj, “num”, { value : 1, writable : true, enumerable : true, configurable : true});// 对象 obj 拥有属性 num,值为 1虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。两者均具有以下两种键值:configurable当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。enumerable当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。数据描述符同时具有以下可选键值:value该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。writable当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。存取描述符同时具有以下可选键值:get一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。set一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。值得注意的是:属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者。这就意味着你可以:Object.defineProperty({}, “num”, { value: 1, writable: true, enumerable: true, configurable: true});也可以:var value = 1;Object.defineProperty({}, “num”, { get : function(){ return value; }, set : function(newValue){ value = newValue; }, enumerable : true, configurable : true});但是不可以:// 报错Object.defineProperty({}, “num”, { value: 1, get: function() { return 1; }});此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样:var obj = Object.defineProperty({}, “num”, {});console.log(obj.num); // undefinedSetters 和 Getters之所以讲到 defineProperty,是因为我们要使用存取描述符中的 get 和 set,这两个方法又被称为 getter 和 setter。由 getter 和 setter 定义的属性称做”存取器属性“。当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。举个例子:var obj = {}, value = null;Object.defineProperty(obj, “num”, { get: function(){ console.log(‘执行了 get 操作’) return value; }, set: function(newValue) { console.log(‘执行了 set 操作’) value = newValue; }})obj.value = 1 // 执行了 set 操作console.log(obj.value); // 执行了 get 操作 // 1这不就是我们要的监控数据改变的方法吗?我们再来封装一下:function Archiver() { var value = null; // archive n. 档案 var archive = []; Object.defineProperty(this, ’num’, { get: function() { console.log(‘执行了 get 操作’) return value; }, set: function(value) { console.log(‘执行了 set 操作’) value = value; archive.push({ val: value }); } }); this.getArchive = function() { return archive; };}var arc = new Archiver();arc.num; // 执行了 get 操作arc.num = 11; // 执行了 set 操作arc.num = 13; // 执行了 set 操作console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]watch API既然可以监控数据的改变,那我可以这样设想,即当数据改变的时候,自动进行渲染工作。举个例子:HTML 中有个 span 标签和 button 标签<span id=“container”>1</span><button id=“button”>点击加 1</button>当点击按钮的时候,span 标签里的值加 1。传统的做法是:document.getElementById(‘button’).addEventListener(“click”, function(){ var container = document.getElementById(“container”); container.innerHTML = Number(container.innerHTML) + 1;});如果使用了 defineProperty:var obj = { value: 1}// 储存 obj.value 的值var value = 1;Object.defineProperty(obj, “value”, { get: function() { return value; }, set: function(newValue) { value = newValue; document.getElementById(‘container’).innerHTML = newValue; }});document.getElementById(‘button’).addEventListener(“click”, function() { obj.value += 1;});代码看似增多了,但是当我们需要改变 span 标签里的值的时候,直接修改 obj.value 的值就可以了。然而,现在的写法,我们还需要单独声明一个变量存储 obj.value 的值,因为如果你在 set 中直接 obj.value = newValue 就会陷入无限的循环中。此外,我们可能需要监控很多属性值的改变,要是一个一个写,也很累呐,所以我们简单写个 watch 函数。使用效果如下:var obj = { value: 1}watch(obj, “num”, function(newvalue){ document.getElementById(‘container’).innerHTML = newvalue;})document.getElementById(‘button’).addEventListener(“click”, function(){ obj.value += 1});我们来写下这个 watch 函数:(function(){ var root = this; function watch(obj, name, func){ var value = obj[name]; Object.defineProperty(obj, name, { get: function() { return value; }, set: function(newValue) { value = newValue; func(value) } }); if (value) obj[name] = value } this.watch = watch;})()现在我们已经可以监控对象属性值的改变,并且可以根据属性值的改变,添加回调函数,棒棒哒~proxy使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:var proxy = new Proxy(target, handler);proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。var proxy = new Proxy({}, { get: function(obj, prop) { console.log(‘设置 get 操作’) return obj[prop]; }, set: function(obj, prop, value) { console.log(‘设置 set 操作’) obj[prop] = value; }});proxy.time = 35; // 设置 set 操作console.log(proxy.time); // 设置 get 操作 // 35除了 get 和 set 之外,proxy 可以拦截多达 13 种操作,比如 has(target, propKey),可以拦截 propKey in proxy 的操作,返回一个布尔值。// 使用 has 方法隐藏某些属性,不被 in 运算符发现var handler = { has (target, key) { if (key[0] === ‘_’) { return false; } return key in target; }};var target = { _prop: ‘foo’, prop: ‘foo’ };var proxy = new Proxy(target, handler);console.log(’_prop’ in proxy); // false又比如说 apply 方法拦截函数的调用、call 和 apply 操作。apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组,不过这里我们简单演示一下:var target = function () { return ‘I am the target’; };var handler = { apply: function () { return ‘I am the proxy’; }};var p = new Proxy(target, handler);p();// “I am the proxy"又比如说 ownKeys 方法可以拦截对象自身属性的读取操作。具体来说,拦截以下操作:Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历到。let target = { _bar: ‘foo’, prop: ‘bar’, prop: ‘baz’};let handler = { ownKeys (target) { return Reflect.ownKeys(target).filter(key => key[0] !== ‘’); }};let proxy = new Proxy(target, handler);for (let key of Object.keys(proxy)) { console.log(target[key]);}// “baz"更多的拦截行为可以查看阮一峰老师的 《ECMAScript 6 入门》值得注意的是,proxy 的最大问题在于浏览器支持度不够,而且很多效果无法使用 poilyfill 来弥补。watch API 优化我们使用 proxy 再来写一下 watch 函数。使用效果如下:(function() { var root = this; function watch(target, func) { var proxy = new Proxy(target, { get: function(target, prop) { return target[prop]; }, set: function(target, prop, value) { target[prop] = value; func(prop, value); } }); if(target[name]) proxy[name] = value; return proxy; } this.watch = watch;})()var obj = { value: 1}var newObj = watch(obj, function(key, newvalue) { if (key == ‘value’) document.getElementById(‘container’).innerHTML = newvalue;})document.getElementById(‘button’).addEventListener(“click”, function() { newObj.value += 1});我们也可以发现,使用 defineProperty 和 proxy 的区别,当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。本文作者:冴羽阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 15, 2018 · 4 min · jiezi

JSON数据从OSS迁移到MaxCompute最佳实践

本文为您介绍如何利用DataWorks数据集成将JSON数据从OSS迁移到MaxCompute,并使用MaxCompute内置字符串函数GET_JSON_OBJECT提取JSON信息。数据上传OSS将您的JSON文件重命名后缀为TXT文件,并上传到OSS。本文中使用的JSON文件示例如下。{ “store”: { “book”: [ { “category”: “reference”, “author”: “Nigel Rees”, “title”: “Sayings of the Century”, “price”: 8.95 }, { “category”: “fiction”, “author”: “Evelyn Waugh”, “title”: “Sword of Honour”, “price”: 12.99 }, { “category”: “fiction”, “author”: “J. R. R. Tolkien”, “title”: “The Lord of the Rings”, “isbn”: “0-395-19395-8”, “price”: 22.99 } ], “bicycle”: { “color”: “red”, “price”: 19.95 } }, “expensive”: 10}将applog.txt文件上传到OSS,本文中OSS Bucket位于华东2区。 使用DataWorks导入数据到MaxCompute新增OSS数据源进入DataWorks数据集成控制台,新增OSS类型数据源。 具体参数如下所示,测试数据源连通性通过即可点击完成。Endpoint地址请参见OSS各区域的外网、内网地址,本例中为http://oss-cn-shanghai.aliyun… http://oss-cn-shanghai-internal.aliyuncs.com(由于本文中OSS和DataWorks项目处于同一个region中,本文选用后者,通过内网连接)。 新建数据同步任务在DataWorks上新建数据同步类型节点。 新建的同时,在DataWorks新建一个建表任务,用于存放JSON数据,本例中新建表名为mqdata。 表参数可以通过图形化界面完成。本例中mqdata表仅有一列,类型为string,列名为MQ data。 完成上述新建后,您可以在图形化界面配置数据同步任务参数,如下图所示。选择目标数据源名称为odps_first,选择目标表为刚建立的mqdata。数据来源类型为OSS,Object前缀可填写文件路径及名称。列分隔符使用TXT文件中不存在的字符即可,本文中使用 ^(对于OSS中的TXT格式数据源,Dataworks支持多字符分隔符,所以您可以使用例如 %&%#^$$^%这样很难出现的字符作为列分隔符,保证分割为一列)。 映射方式选择默认的同行映射即可。 点击左上方的切换脚本按钮,切换为脚本模式。修改fileFormat参数为: “fileFormat”:“binary”。该步骤可以保证OSS中的JSON文件同步到MaxCompute之后存在同一行数据中,即为一个字段。其他参数保持不变,脚本模式代码示例如下。{ “type”: “job”, “steps”: [ { “stepType”: “oss”, “parameter”: { “fieldDelimiterOrigin”: “^”, “nullFormat”: “”, “compress”: “”, “datasource”: “OSS_userlog”, “column”: [ { “name”: 0, “type”: “string”, “index”: 0 } ], “skipHeader”: “false”, “encoding”: “UTF-8”, “fieldDelimiter”: “^”, “fileFormat”: “binary”, “object”: [ “applog.txt” ] }, “name”: “Reader”, “category”: “reader” }, { “stepType”: “odps”, “parameter”: { “partition”: “”, “isCompress”: false, “truncate”: true, “datasource”: “odps_first”, “column”: [ “mqdata” ], “emptyAsNull”: false, “table”: “mqdata” }, “name”: “Writer”, “category”: “writer” } ], “version”: “2.0”, “order”: { “hops”: [ { “from”: “Reader”, “to”: “Writer” } ] }, “setting”: { “errorLimit”: { “record”: "" }, “speed”: { “concurrent”: 2, “throttle”: false, “dmu”: 1 } }}完成上述配置后,点击运行接即可。运行成功日志示例如下所示。 获取JSON字段信息在您的业务流程中新建一个ODPS SQL节点。 您可以首先输入 SELECT*from mqdata;语句,查看当前mqdata表中数据。当然这一步及后续步骤,您也可以直接在MaxCompute客户端中输入命令运行。 确认导入表中的数据结果无误后,您可以使用MaxCompute内建字符串函数GET_JSON_OBJECT获取您想要的JSON数据。本例中使用 SELECT GET_JSON_OBJECT(mqdata.MQdata,’$.expensive’) FROM mqdata;获取JSON文件中的 expensive值。如下图所示,可以看到已成功获取数据。 本文作者:付帅阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 15, 2018 · 1 min · jiezi