2018年4月30日星期一

如何写一手漂亮的模型:面向对象编程的设计原则综述

面向对象的编程在实现想法乃至系统的过程中都非常重要,我们不论是使用 TensorFlow 还是 PyTorch 来构建模型都或多或少需要使用类和方法。而采用类的方法来构建模型会令代码非常具有可读性和条理性,本文介绍了算法实现中使用类和方法来构建模型所需要注意的设计原则,它们可以让我们的机器学习代码更加美丽迷人。

大多数现代编程语言都支持并且鼓励面向对象编程(OOP)。即使我们最近似乎看到了一些偏离,因为人们开始使用不太受 OOP 影响的编程语言(例如 Go, Rust, Elixir, Elm, Scala),但是大多数还是具有面向对象的属性。我们在这里概括出的设计原则也适用于非 OOP 编程语言。

为了成功地写出清晰的、高质量的、可维护并且可扩展的代码,我们需要以 Python 为例了解在过去数十年里被证明是有效的设计原则。

对象类型

因为我们要围绕对象来建立代码,所以区分它们的不同责任和变化是有用的。一般来说,面向对象的编程有三种类型的对象。

1. 实体对象

这类对象通常对应着问题空间中的一些现实实体。比如我们要建立一个角色扮演游戏(RPG),那么简单的 Hero 类就是一个实体对象。

class Hero:    def __init__(self, health, mana):        self._health = health        self._mana = mana    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return 1    def take_damage(self, damage: int):        self._health -= damage    def is_alive(self):        return self._health > 0

这类对象通常包含关于它们自身的属性(例如 health 或 mana),这些属性根据具体的规则都是可修改的。

2. 控制对象(Control Object)

控制对象(有时候也称作管理对象)主要负责与其它对象的协调,这是一些管理并调用其它对象的对象。我们上面的 RPG 案例中有一个很棒的例子,Fight 类控制两个英雄,并让它们对战。

class Fight:    class FightOver(Exception):        def __init__(self, winner, *args, **kwargs):            self.winner = winner            super(*args, **kwargs)    def __init__(self, hero_a: Hero, hero_b: Hero):        self._hero_a = hero_a        self._hero_b = hero_b        self.fight_ongoing = True        self.winner = None    def fight(self):        while self.fight_ongoing:            self._run_round()        print(f'The fight has ended! Winner is #{self.winner}')    def _run_round(self):        try:            self._run_attack(self._hero_a, self._hero_b)            self._run_attack(self._hero_b, self._hero_a)        except self.FightOver as e:            self._finish_round(e.winner)    def _run_attack(self, attacker: Hero, victim: Hero):        damage = attacker.attack()        victim.take_damage(damage)        if not victim.is_alive():            raise self.FightOver(winner=attacker)    def _finish_round(self, winner: Hero):        self.winner = winner        self.fight_ongoing = False

在这种类中,为对战封装编程逻辑可以给我们提供多个好处:其中之一就是动作的可扩展性。我们可以很容易地将参与战斗的英雄传递给非玩家角色(NPC),这样它们就能利用相同的 API。我们还可以很容易地继承这个类,并复写一些功能来满足新的需要。

3. 边界对象(Boundary Object)

这些是处在系统边缘的对象。任何一个从其它系统获取输入或者给其它系统产生输出的对象都可以被归类为边界对象,无论那个系统是用户,互联网或者是数据库。

class UserInput:    def __init__(self, input_parser):        self.input_parser = input_parser    def take_command(self):        """        Takes the user's input, parses it into a recognizable command and returns it        """        command = self._parse_input(self._take_input())        return command    def _parse_input(self, input):        return self.input_parser.parse(input)    def _take_input(self):        raise NotImplementedError() class UserMouseInput(UserInput):    pass class UserKeyboardInput(UserInput):    pass class UserJoystickInput(UserInput):    pass

这些边界对象负责向系统内部或者外部传递信息。例如对要接收的用户指令,我们需要一个边界对象来将键盘输入(比如一个空格键)转换为一个可识别的域事件(例如角色的跳跃)。

Bonus:值对象(Value Object)

价值对象代表的是域(domain)中的一个简单值。它们无法改变,不恒一。

如果将它们结合在我们的游戏中,Money 类或者 Damage 类就表示这种对象。上述的对象让我们容易地区分、寻找和调试相关功能,然而仅使用基础的整形数组或者整数却无法实现这些功能。

class Money:    def __init__(self, gold, silver, copper):        self.gold = gold        self.silver = silver        self.copper = copper    def __eq__(self, other):        return self.gold == other.gold and self.silver == other.silver and self.copper == other.copper    def __gt__(self, other):        if self.gold == other.gold and self.silver == other.silver:            return self.copper > other.copper        if self.gold == other.gold:            return self.silver > other.silver        return self.gold > other.gold    def __add__(self, other):        return Money(gold=self.gold + other.gold, silver=self.silver + other.silver, copper=self.copper + other.copper)    def __str__(self):        return f'Money Object(Gold: {self.gold}; Silver: {self.silver}; Copper: {self.copper})'    def __repr__(self):        return self.__str__() print(Money(1, 1, 1) == Money(1, 1, 1)) # => True print(Money(1, 1, 1) > Money(1, 2, 1)) # => False print(Money(1, 1, 0) + Money(1, 1, 1)) # => Money Object(Gold: 2; Silver: 2; Copper: 1)

它们可以归类为实体对象的子类别。

关键设计原则

设计原则是软件设计中的规则,过去这些年里已经证明它们是有价值的。严格地遵循这些原则有助于软件达到一流的质量。

抽象(Abstraction)

抽象就是将一个概念在一定的语境中简化为原始本质的一种思想。它允许我们拆解一个概念来更好的理解它。

上面的游戏案例阐述了抽象,让我们来看一下 Fight 类是如何构建的。我们以尽可能简单的方式使用它,即在实例化的过程中给它两个英雄作为参数,然后调用 fight() 方法。不多也不少,就这些。

代码中的抽象过程应该遵循最少意外(POLA)的原则,抽象不应该用不必要和不相关的行为/属性。换句话说,它应该是直观的。

注意,我们的 Hero#take_damage() 函数不会做一些异常的事情,例如在还没死亡的时候删除角色。但是如果他的生命值降到零以下,我们可以期望它来杀死我们的角色。

封装

封装可以被认为是将某些东西放在一个类以内,并限制了它向外部展现的信息。在软件中,限制对内部对象和属性的访问有助于保证数据的完整性。

将内部编程逻辑封装成黑盒子,我们的类将更容易管理,因为我们知道哪部分可以被其它系统使用,哪些不行。这意味着我们在保留公共部分并且保证不破坏任何东西的同时能够重用内部逻辑。此外,我们从外部使用封装功能变得更加简单,因为需要考虑的事情也更少。

在大多数编程语言中,封装都是通过所谓的 Access modifiers(访问控制修饰符)来完成的(例如 private,protected 等等)。Python 并不是这方面的最佳例子,因为它不能在运行时构建这种显式修饰符,但是我们使用约定来解决这个问题。变量和函数前面的_前缀就意味着它们是私有的。

举个例子,试想将我们的 Fight#_run_attack 方法修改为返回一个布尔变量,这意味着战斗结束而不是发生了意外。我们将会知道,我们唯一可能破坏的代码就是 Fight 类的内部,因为我们是把这个函数设置为私有的。

请记住,代码更多的是被修改而不是重写。能够尽可能清晰、较小影响的方式修改代码对开发的灵活性很重要。

分解

分解就是把一个对象分割为多个更小的独立部分,这些独立的部分更易于理解、维护和编程。

试想我们现在希望 Hero 类能结合更多的 RPG 特征,例如 buffs,资产,装备,角色属性。

class Hero:    def __init__(self, health, mana):        self._health = health        self._mana = mana        self._strength = 0        self._agility = 0        self._stamina = 0        self.level = 0        self._items = {}        self._equipment = {}        self._item_capacity = 30        self.stamina_buff = None        self.agility_buff = None        self.strength_buff = None        self.buff_duration = -1    def level_up(self):        self.level += 1        self._stamina += 1        self._agility += 1        self._strength += 1        self._health += 5    def take_buff(self, stamina_increase, strength_increase, agility_increase):        self.stamina_buff = stamina_increase        self.agility_buff = agility_increase        self.strength_buff = strength_increase        self._stamina += stamina_increase        self._strength += strength_increase        self._agility += agility_increase        self.buff_duration = 10  # rounds    def pass_round(self):        if self.buff_duration > 0:            self.buff_duration -= 1        if self.buff_duration == 0:  # Remove buff            self._stamina -= self.stamina_buff            self._strength -= self.strength_buff            self._agility -= self.agility_buff            self._health -= self.stamina_buff * 5            self.buff_duration = -1            self.stamina_buff = None            self.agility_buff = None            self.strength_buff = None    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return 1 + (self._agility * 0.2) + (self._strength * 0.2)    def take_damage(self, damage: int):        self._health -= damage    def is_alive(self):        return self._health > 0    def take_item(self, item: Item):        if self._item_capacity == 0:            raise Exception('No more free slots')        self._items[item.id] = item        self._item_capacity -= 1    def equip_item(self, item: Item):        if item.id not in self._items:            raise Exception('Item is not present in inventory!')        self._equipment[item.slot] = item        self._agility += item.agility        self._stamina += item.stamina        self._strength += item.strength        self._health += item.stamina * 5 # 缺乏分解的案例

我们可能会说这份代码已经开始变得相当混乱了。我们的 Hero 对象一次性设置了太多的属性,结果导致这份代码变得相当脆弱。

例如,我们的耐力分数为 5 个生命值,如果将来要修改为 6 个生命值,我们就要在很多地方修改这个实现。

解决方案就是将 Hero 对象分解为多个更小的对象,每个小对象可承担一些功能。下面展示了一个逻辑比较清晰的架构:

from copy import deepcopy class AttributeCalculator:    @staticmethod    def stamina_to_health(self, stamina):        return stamina * 6    @staticmethod    def agility_to_damage(self, agility):        return agility * 0.2    @staticmethod    def strength_to_damage(self, strength):        return strength * 0.2 class HeroInventory:    class FullInventoryException(Exception):        pass    def __init__(self, capacity):        self._equipment = {}        self._item_capacity = capacity    def store_item(self, item: Item):        if self._item_capacity < 0:            raise self.FullInventoryException()        self._equipment[item.id] = item        self._item_capacity -= 1    def has_item(self, item):        return item.id in self._equipment class HeroAttributes:    def __init__(self, health, mana):        self.health = health        self.mana = mana        self.stamina = 0        self.strength = 0        self.agility = 0        self.damage = 1    def increase(self, stamina=0, agility=0, strength=0):        self.stamina += stamina        self.health += AttributeCalculator.stamina_to_health(stamina)        self.damage += AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility)        self.agility += agility        self.strength += strength    def decrease(self, stamina=0, agility=0, strength=0):        self.stamina -= stamina        self.health -= AttributeCalculator.stamina_to_health(stamina)        self.damage -= AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility)        self.agility -= agility        self.strength -= strength class HeroEquipment:    def __init__(self, hero_attributes: HeroAttributes):        self.hero_attributes = hero_attributes        self._equipment = {}    def equip_item(self, item):        self._equipment[item.slot] = item        self.hero_attributes.increase(stamina=item.stamina, strength=item.strength, agility=item.agility) class HeroBuff:    class Expired(Exception):        pass    def __init__(self, stamina, strength, agility, round_duration):        self.attributes = None        self.stamina = stamina        self.strength = strength        self.agility = agility        self.duration = round_duration    def with_attributes(self, hero_attributes: HeroAttributes):        buff = deepcopy(self)        buff.attributes = hero_attributes        return buff    def apply(self):        if self.attributes is None:            raise Exception()        self.attributes.increase(stamina=self.stamina, strength=self.strength, agility=self.agility)    def deapply(self):        self.attributes.decrease(stamina=self.stamina, strength=self.strength, agility=self.agility)    def pass_round(self):        self.duration -= 0        if self.has_expired():            self.deapply()            raise self.Expired()    def has_expired(self):        return self.duration == 0 class Hero:    def __init__(self, health, mana):        self.attributes = HeroAttributes(health, mana)        self.level = 0        self.inventory = HeroInventory(capacity=30)        self.equipment = HeroEquipment(self.attributes)        self.buff = None    def level_up(self):        self.level += 1        self.attributes.increase(1, 1, 1)    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return self.attributes.damage    def take_damage(self, damage: int):        self.attributes.health -= damage    def take_buff(self, buff: HeroBuff):        self.buff = buff.with_attributes(self.attributes)        self.buff.apply()    def pass_round(self):        if self.buff:            try:                self.buff.pass_round()            except HeroBuff.Expired:                self.buff = None    def is_alive(self):        return self.attributes.health > 0    def take_item(self, item: Item):        self.inventory.store_item(item)    def equip_item(self, item: Item):        if not self.inventory.has_item(item):            raise Exception('Item is not present in inventory!')        self.equipment.equip_item(item)

现在,在将 Hero 对象分解为 HeroAttributes、HeroInventory、HeroEquipment 和 HeroBuff 对象之后,未来新增功能就更加容易、更具有封装性、具有更好的抽象,这份代码也就越来越清晰了。

下面是三种分解关系:

  • 关联:在两个组成部分之间定义一个松弛的关系。两个组成部分不互相依赖,但是可以一起工作。例如 Hero 对象和 Zone 对象。

  • 聚合:在整体和部分之间定义一个弱「包含」关系。这种关系比较弱,因为部分可以在没有整体的时候存在。例如 HeroInventory(英雄财产)和 Item(条目)。HeroInventory 可以有很多 Items,而且一个 Items 也可以属于任何 HeroInventory(例如交易条目)。

  • 组成:一个强「包含」关系,其中整体和部分不能彼此分离。部分不能被共享,因为整体要依赖于这些特定的部分。例如 Hero(英雄)和 HeroAttributes(英雄属性)。

泛化

泛化可能是最重要的设计原则,即我们提取共享特征,并将它们结合到一起的过程。我们都知道函数和类的继承,这就是一种泛化。

做一个比较可能会将这个解释得更加清楚:尽管抽象通过隐藏非必需的细节减少了复杂性,但是泛化通过用一个单独构造体来替代多个执行类似功能的实体。


# Two methods which share common characteristics def take_physical_damage(self, physical_damage):    print(f'Took {physical_damage} physical damage')    self._health -= physical_damage def take_spell_damage(self, spell_damage):    print(f'Took {spell_damage} spell damage')    self._health -= spell_damage # vs. # One generalized method def take_damage(self, damage, is_physical=True):    damage_type = 'physical' if is_physical else 'spell'    print(f'Took {damage} {damage_type} damage')    self._health -= damage    

以上是函数示例,这种方法缺少泛化性能,而下面展示了具有泛化性能的案例。

class Entity:    def __init__(self):        raise Exception('Should not be initialized directly!')    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return self.attributes.damage    def take_damage(self, damage: int):        self.attributes.health -= damage    def is_alive(self):        return self.attributes.health > 0 class Hero(Entity):    pass class NPC(Entity):    pass

在给出的例子中,我们将常用的 Hero 类和 NPC 类泛化为一个共同的父类 Entity,并通过继承简化子类的构建。

这里,我们通过将它们的共同功能移动到基本类中来减少复杂性,而不是让 NPC 类和 Hero 类将所有的功能都实现两次。

我们可能会过度使用继承,因此很多有经验的人都建议我们更偏向使用组合(Composition)而不是继承(https://stackoverflow.com/a/53354)。

继承常常被没有经验的程序员滥用,这可能是由于继承是他们首先掌握的 OOP 技术。

组合

组合就是把多个对象结合为一个更复杂对象的过程。这种方法会创建对象的示例,并且使用它们的功能,而不是直接继承它。

使用组合原则的对象就被称作组合对象(composite object)。这种组合对象在要比所有组成部分都简单,这是非常重要的一点。当把多个类结合成一个类的时候,我们希望把抽象的层次提高一些,让对象更加简单。

组合对象的 API 必须隐藏它的内部模块,以及内部模块之间的交互。就像一个机械时钟,它有三个展示时间的指针,以及一个设置时间的旋钮,但是它内部包含很多运动的独立部件。

正如我所说的,组合要优于继承,这意味着我们应该努力将共用功能移动到一个独立的对象中,然后其它类就使用这个对象的功能,而不是将它隐藏在所继承的基本类中。

让我们阐述一下过度使用继承功能的一个可能会发生的问题,现在我们仅仅向游戏中增加一个行动:

class Entity:    def __init__(self, x, y):        self.x = x        self.y = y        raise Exception('Should not be initialized directly!')    def attack(self) -> int:        """        Returns the attack damage of the Hero        """        return self.attributes.damage    def take_damage(self, damage: int):        self.attributes.health -= damage    def is_alive(self):        return self.attributes.health > 0    def move_left(self):        self.x -= 1    def move_right(self):        self.x += 1 class Hero(Entity):    pass class NPC(Entity):    pass

正如我们所学到的,我们将 move_right 和 move_left 移动到 Entity 类中,而不是直接复制代码。

好了,如果我们想在游戏中引入坐骑呢?坐骑也应该需要左右移动,但是它没有攻击的能力,甚至没有生命值。

我们的解决方案可能是简单地将 move 逻辑移动到独立的 MoveableEntity 或者 MoveableObject 类中,这种类仅仅含有那项功能。

那么,如果我们想让坐骑具有生命值,但是无法攻击,那该怎么办呢?希望你可以看到类的层次结构是如何变得复杂的,即使我们的业务逻辑还是相当简单。

一个从某种程度来说比较好的方法是将动作逻辑抽象为 Movement 类(或者其他更好的名字),并且在可能需要的类里面把它实例化。这将会很好地封装函数,并使其在所有种类的对象中都可以重用,而不仅仅局限于实体类。

批判性思考

尽管这些设计原则是在数十年经验中形成的,但盲目地将这些原则应用到代码之前进行批判性思考是很重要的。

任何事情都是过犹不及!有时候这些原则可以走得很远,但是实际上有时会变成一些很难使用的东西。

作为一个工程师,我们需要根据独特的情境去批判地评价最好的方法,而不是盲目地遵从并应用任意的原则。

关注点的内聚、耦合和分离

内聚(Cohesion)

内聚代表的是模块内部责任的分明,或者是模块的复杂度。

如果我们的类只执行一个任务,而没有其它明确的目标,那么这个类就有着高度内聚性。另一方面,如果从某种程度而言它在做的事情并不清楚,或者具有多于一个的目标,那么它的内聚性就非常低。

我们希望代码具有较高的内聚性,如果发现它们有非常多的目标,或许我们应该将它们分割出来。

耦合

耦合获取的是连接不同类的复杂度。我们希望类与其它的类具有尽可能少、尽可能简单的联系,所以我们就可以在未来的事件中交换它们(例如改变网络框架)。

在很多编程语言中,这都是通过大量使用接口来实现的,它们抽象出处理特定逻辑的类,然后表征为一种适配层,每个类都可以嵌入其中。

分离关注点

分离关注点(SoC)是这样一种思想:软件系统必须被分割为功能上互不重叠的部分。或者说关注点必须分布在不同的地方,其中关注点表示能够为一个问题提供解决方案。

网页就是一个很好的例子,它具有三个层(信息层、表示层和行为层),这三个层被分为三个不同的地方(分别是 HTML,CSS,以及 JS)。

如果重新回顾一下我们的 RPG 例子,你会发现它在最开始具有很多关注点(应用 buffs 来计算袭击伤害、处理资产、装备条目,以及管理属性)。我们通过分解将那些关注点分割成更多的内聚类,它们抽象并封装了它们的细节。我们的 Hero 类现在仅仅作为一个组合对象,它比之前更加简单。

结语

对小规模的代码应用这些原则可能看起来很复杂。但是事实上,对于未来想要开发和维护的任何一个软件项目而言,这些规则都是必须的。在刚开始写这种代码会有些成本,但是从长期来看,它会回报以几倍增长。

这些原则保证我们的系统更加:

  • 可扩展:高内聚使得不用关心不相关的功能就可以更容易地实现新模块。

  • 可维护:低耦合保证一个模块的改变通常不会影响其它模块。高内聚保证一个系统需求的改变只需要更改尽可能少的类。

  • 可重用:高内聚保证一个模块的功能是完整的,也是被妥善定义的。低耦合使得模块尽可能少地依赖系统的其它部分,这使得模块在其它软件中的重用变得更加容易。

在本文中,我们首先介绍了一些高级对象的类别(实体对象、边界对象以及控制对象)。然后我们了解了一些构建对象时使用的关键原则,比如抽象、泛化、分解和封装等。最后,我们引入了两个软件质量指标(耦合和内聚),然后学习了使用这些原则能够带来的好处。

我希望这篇文章提供了一些关于设计原则的概览,如果我们希望自己能够在这个领域获得更多的进步,我们还需要了解更多具体的操作。


原文地址:https://ift.tt/2GXuQ6D

]]> 原文: https://ift.tt/2JFZy57
RSS Feed

机器知心

IFTTT

DeepMind论文:CNN的变形稳定性和池化无关,滤波器平滑度才是关键

传统观点认为,CNN 中的池化层导致了对微小平移和变形的稳定性。在本文中,DeepMind 的研究者提出了一个反直觉的结果:CNN 的变形稳定性仅在初始化时和池化相关,在训练完成后则无关;并指出,滤波器的平滑度才是决定变形稳定性的关键因素。

1. 引言

近年来,卷积神经网络(CNN)在计算机视觉的物体识别方面取得了巨大的成功(Krizhevsky et al., 2012; Simonyan & Zisserman, 2014; He et al., 2016; Russakovsky et al., 2015),然而目前尚不清楚这些模型如此成功的原因。

直到最近,人们才对 CNN 成功的原因有了一个普遍的解释,解释说是因为交错地引入池化层(interleaved pooling layer)才使这些模型对小的平移和变形(translation and deformation)不敏感。视觉域中的许多变化来自视图、物体位置、旋转、尺寸和非刚体变形的微小变化。因此,「对这些变化不太敏感」这一描述有用,但只是看起来合理。此外,长久以来人们都假设引入交错池化层将这种偏差构建到的模型中是有益的 (LeCun et al., 1990; Krizhevsky et al., 2012; Simonyan & Zisserman, 2014; LeCun et al., 2015; Giusti et al., 2013)。然而,这个假设的解释还没有被彻底地验证。

图 1. 变形的 ImageNet 图像示例。左:原始图像,右:变形图像。虽然图像变化了很多,比如,在 L2 度量下,他们可能会被人类赋予相同的标签。

事实上,最近证明,由交错池化层提供的归纳偏差(inductive bia)并不是良好性能的必要条件,因为最近的一些架构已经减少了交错池化层,而且仍然实现了强大的性能(Springenberg et al., 2014; He et al., 2016)。这引出了本文中的以下问题:

 1. 池化是否对学习的变形稳定性是否有影响?

2. 在没有池化的情况下是否能实现变形稳定性?

3. 如果可以,是如何实现的?

关于池化作用的传统推理是假设混淆变量(nuisance variables)的不变性在总体上有帮助。这里,本文对其有效性做出了一个猜想,并进一步定义了本文将讨论的特定类别的混淆变形(nuisance deformations)。

本文的主要贡献是:

  • 学习稳定性:本文展示了没有池化的网络在初始化时对变形敏感,但经过训练学习表征的过程之后对变形是稳定的。作者还表明,即使在有池化的网络中,训练过程中变形稳定性模式也会发生显著变化。此外,在训练过程中,变形稳定性有时会下降,这表明这种稳定性不是单方面的有用(3.2 节)。

  • 收敛稳定性:本文表明池化和非池化训练网络的层间变形稳定性模式最终会收敛到相似的结构(3.3 小节)。

  • 稳定性的实现:本文表明无论池化还是非池化网络,都可通过滤波器的平滑性实现和调节变形稳定性(第 4 节)。

此外,从理解神经网络的权重和层如何影响整个网络行为的角度来看,此工作提供了一个有潜在价值的重要例子,解释各层中权重的简单性质如何影响网络的整体计算。

从设计神经网络模型的角度来看,这项工作提供了对「指导设计神经网络 20 多年的重要归纳偏差」的洞察。长期以来人们认为池化对实现变形稳定性很重要,认为池化是 CNN 成功的主要因素。这项工作表明,无论看起来多么合理,并通过经验和理论验证来加强,我们对神经网络工作原理的直觉往往是不准确的。

图 2:生成变形图像:为了使图像随机变形,我们:(a)从固定均匀间隔的控制点网格开始(这里是 4x4 个控制点),然后在点邻域内为每个控制点选择一个随机源;(b) 然后使用薄板插值平滑得到的矢量场;(c) 矢量场叠加在原始图像上:使用原始图像中箭头尾部附近的值的双线性插值计算箭头顶端的最终结果中的值;(d) 最终结果。

3. 在池化和非池化的网络中学习变形稳定性是相似的


图 3:池化在初始化时赋予变形稳定性,但在训练过程中稳定性发生显着变化,而且无论是否池化,都会收敛到类似的稳定性。(a) 在初始化时,最大池化的网络对变形较不敏感。(b) 训练后,池化和非池化的网络对层的变形有非常相似的敏感模式。CIFAR10 有类似的模式: (c) 初始化时,池化对变形的敏感性有显着影响,但 (d) 训练后,下采样层的选择对所有层的变形稳定性几乎没有影响。图层 0 对应于输入图像;这些层包括下采样层;最后一层对应于最后的下采样层。因此对于 CIFAR10,我们一共有 13 层,包括 1 个输入层、8 个卷积层和 4 个池化层。

4. 滤波器的平滑度有助于提高变形稳定性

图 4:使用更平滑的随机滤波器进行初始化会使变形稳定性更好。使用标准偏差σ的高斯滤波器对滤波器进行平滑处理,然后测量对变形的敏感度。当增加σ来增加滤波器的平滑度时,表征对变形的敏感度下降。较深的线条代表更平滑的随机滤波器。

图 5:需要更高变形稳定性的任务要用更平滑的滤波器。(a) 生成一个合成任务,每个类基于单个 MNIST 图像,每类的示例通过应用该类图像的强度 C 的随机变形生成。左边的图像使用强度 3 的变形生成,右列图像分别使用强度为 1、2、3、4 的变形生成。(b) 训练后,在强变形训练任务上得到的网络滤波器更平滑。黑色虚线表示初始化的平均值。

5:滤波平滑性取决于监督任务类型

图 6:训练得到更平滑的滤波器。(a) 和 (b) 在训练之后,滤波器明显更加平滑,不同的架构收敛到类似的滤波平滑度。(c)对随机标签进行训练时,滤波器的平滑性很大程度取决于选择的下采样层。有趣的是,(a)训练 ImageNet 时滤波器的平滑度逐层递增,(b)CIFAR10 则是逐层递减。黑色虚线表示初始化的平均值。

图 7:训练随机标签时,变形稳定性依赖于架构类型。

论文:Learned Deformation Stability in Convolutional Neural Networks(卷积神经网络的学习变形稳定性)

论文地址:https://ift.tt/2I4azju

传统观点认为,卷积神经网络中的池化层导致了对微小平移和变形的稳定性。此项工作中,我们根据经验探索了这一观点。我们发现,虽然池化层在初始化时赋予网络变形稳定形,但在训练的过程中每层的变形稳定性变化显著,一些层甚至有所减小,这表明变形稳定性不是单方面有帮助的。令人惊讶的是,训练完成之后,层间的变形稳定性模式很大程度上与是否引入池化无关。然后我们在本文展示了决定变形稳定性的一个重要因素是滤波器的平滑度。此外,滤波器的平滑度和变形稳定性不仅是输入图像分布的结果,而且关键地取决于图像和标签的联合分布。本项工作展示了学习变形稳定性等偏差的一种方法,并提供了「理解学习网络权重的简单性质如何有助于对整体网络的计算」的一个例子。

]]> 原文: https://ift.tt/2jkpisR
RSS Feed

机器知心

IFTTT

生物神经网络基础:从脉冲放电神经元谈起

神经元是细胞——基本上由水、离子、氨基酸和蛋白质构成的小体具有引人注目的电化学特性。它们是大脑的主要功能单元。我们的心智体验(感知、记忆和想法)源自神经元双层脂膜上的盐分水平的涨落和神经元之间的突触传递。理解神经元和神经计算的方式有助于阐释我们丰富的心智体验的构建和表法方式、我们的行为和决策的基本原理,并能为处理信息和实现人工智能的新方法提供生物学启发。

在这篇文章中,我将以交互式的方式概述建模神经元的方法。这篇文章不可能全面覆盖所有内容,也不会给出新的见解。但是,我希望这篇文章能为好奇的读者提供有关建模神经元的直观知识。文中的图表可以让你尝试不同的模型和参数。暂时先不要疑虑动态系统、分岔(bifurcation)、动作电位(action potential)是什么,后文会给出简要的解释。

起步:神经元的生物学基础

在我们深入数学模型之前,我们首先应该简要了解一下神经元的生物学基础,这是所有一切的基础(如果你已经学过神经科学课程,你当然可以跳过这部分)。

神经元的解剖结构

我们首先要了解的是神经元的特殊形态,即它们的形状和结构。这种形态让神经元可以从其它数千个细胞接收信息并快速进行长距离通信——从脊髓到脚可长达 1 米!

卡哈尔(Cajal)手绘的浦肯野细胞。注意其中树突分支的复杂性和数量

神经元通过位于(但不排除其它位置)其树突树(dendritic tree)上的突触接收来自其它神经元的信号。树突树是一种复杂的、有分支的、扩散式的结构。如果你想知道树突复杂度最高的是什么细胞,那就是浦肯野细胞(Purkinje cell),它能够接收多达 100000 个其它连接。树突上有树突棘(dendritic spine),这是树突上微小的凸起,是其它神经元与该树突接触的位置。

来自树突的信号会传播和汇聚到神经元胞体(soma)——神经元细胞的主体,是细胞核和其它典型细胞器所在的位置。

神经元的解剖结构。图片来自 OpenStax College 生物学《Neurons and glial cells》,CC BY-NC-SA 3.0 License

胞体会延伸出轴丘(axon hillock),并进一步延展成轴突(axon)。轴突与其它神经元的树突相连。这让神经元可以快速长距离通信,同时不损失信号完整性。为了让信号快速传播,轴突是有髓鞘的——其覆盖有分散的绝缘体,让神经元的信号可以在绝缘的区域之间跳跃。为了维持信号完整性,轴突中的神经元信号是「要么全有要么全无的」——这是有点像比特(bit)的脉冲信号,我们后面会讨论。

神经元的生理机能

关于神经元,要了解的第二件事是它们特殊的生理机能——神经元的细胞功能。神经细胞功能最显著的特征是动作电位(action potential)。这种机制让神经元可以可靠地长距离传递信息,而不会出现传输衰减。

有一个需要记住的重点:神经元沉浸在主要由水、盐和蛋白质构成的细胞外液中。由盐进出细胞以及这些盐的浓度差异所产生的力是神经元这种显著行为的物理基础。神经元中有将钠排出细胞和将钾引入细胞的钠钾泵(sodium-potassium pump),这会使得细胞外的钠浓度高于细胞内,细胞外的钾浓度又低于细胞内。

动作电位是离散事件,其中膜电位会快速上升(去极化)然后回落(极化)。这种离散事件是「要么全有要么全无的」,也就是说如果神经元膜上一个部分出现了一个动作电位,那么其邻近的部分也会出现,如此继续扩散直到到达轴突末端。动作电位一般不会反向传播,因为一旦膜上一个区域发出了一个动作电位,电化学作用力就会将膜的这个区域超极化(hyper-polarize),导致之前开放的通道关闭,并使该区域在一段时间内不活跃。

动作电位源自细胞膜上不同种类的离子经由通道的运动以及这些通道在不同时间尺度上的激活和失活。典型的动作电位会按以下方式发生:

  1. 平衡(Equilibrium):神经元的平衡膜电位大约为 -70 mV,接近能斯特平衡(Nernst Equilibrium)的 EK+≈−75。在平衡时,净电流为 0——即向内和向外的电流是平衡的。

  2. 去极化(Depolarization):输入的激励信号去除膜的极化。快速响应门控 Na+ 通道激活,Na+ 涌入,使膜的电位更高。响应速度更慢的 K+ 通道开启,K+ 涌入,使膜的电位更低。

  3. 放大(Amplification):如果神经元受到的刺激变多或受到了快速刺激,那么激活的 Na+ 通道就会远多于 K+ 通道。这会导致形成一种反馈回路:Na+ 的流入会导致更多 Na+ 通道激活。

  4. 重新极化(Repolarization):最后,膜电位会接近 Na+ 的能斯特平衡,因为钠通道已经开到了最大。更慢的 K+ 通道会赶上 Na+ 通道,使得膜电位重新极化。与此同时,Na+ 通道处于失活状态。

  5. 超极化(Hyper-polarization):K+ 通道开启而 Na+ 通道失活,会导致膜电位降至典型平衡点之下,接近 K+ 能斯特平衡。

  6. 不应期(Refractory Period):Na+ 通道需要一段时间才能脱离失活状态,这意味着在一个动作电位之后,它们在一段时间内将一直无法再次开启。这期间,大多数 Na+ 通道失活时被称为绝对不应期(absolute refractory period,不管刺激强度如何,神经元都不会放电),而很多 Na+ 通道失活时则被称为相对不应期(relative refractory period,当刺激足够强时神经元可以放电)。

正如我们下面会看到的那样,某些模型(尤其是 Hodgkin-Huxley 模型)能很好地描述动作电位的本质。借助这些理论模型,你可以探索动作电位的计算性质或其它有趣的神经现象。

尖峰队列(spike train)

尖峰队列是神经元的语言。人们往往会将尖峰看作是点事件,将尖峰队列看作是点过程。我们使用神经响应函数来描述:

其中一次脉冲定义为:

一般而言,假设尖峰队列是由随机过程生成的,这会有助于分析。假设尖峰之间相互独立,我们可以将这个点过程建模为一个泊松过程(Poisson process),所以我们知道在区间 ΔT 中出现 n 个尖峰的概率为:

为了根据泊松点过程产生尖峰,要在一个足够小的时间区间内生成一个随机数 r,这样应该就只会出现 1 个尖峰,并且检查 r

起步:动态系统

分岔与动态系统

很多神经元模型都是动态系统——它们将神经元的状态描述成某个向量,并且会根据某些方程随时间变化。该模型的空间中每一个点都有一个对应的梯度,即其变化的方向。关于模型的很多情况都可以通过查看其相图(phase portrait)推断得到。下面给出了两个相图的案例。这些相图看起来很密集——这一节会尝试解释如何理解它们。

Fitzhugh-Nagumo 神经元的相图。相图看起来可能很嘈杂,但如果你知道如何解读,其中包含了大量信息:(1)向量场(黑色箭头)表示状态空间中的轨迹,(2)nullcline(黑色实线)表示偏导数为 0 的点,(3)固定点(黑点)在这里是所有点都朝向的点,(4)轨迹(红色)表示穿过状态空间的轨迹。

Simple Model 神经元的相图。这里还给出了一个吸引盆(basin of attraction,蓝色区域),其中所有点都往往指向一个稳定节点(黑点)。分界线(蓝色实线)直接连接到鞍点(白点),并且也定义了吸引盆的边界。

平衡

当动态系统不再随时间改变时,就达到了平衡。形式上讲,如果 f(x)=0,则 x 是系统 x′=f(x) 的一个平衡点。在动态系统中,我们只会考虑 5 类固定点。

  • 稳定节点(stable node)吸引邻近区域内的所有点。当所有特征值都为负且为实数时,就会出现稳定节点。

  • 不稳定节点(unstable node)排斥邻近区域内的所有点。当所有特征值都为正且为实数时,就会出现不稳定节点。

  • 稳定焦点(stable foci)通过向内盘旋吸引邻近区域内的所有点。当特征值为复数且与负实数部分共轭时,就会出现稳定焦点。

  • 不稳定焦点(unstable foci)通过向外盘旋排斥邻近区域内的所有点。当特征值为复数且与正实数部分共轭时,就会出现不稳定焦点。

  • 鞍点(saddle)沿两个方向吸引且沿两个方向排斥。当特征值为实数且符号相反时,就会出现鞍点。

根据雅可比矩阵(Jaccobian matrix)的轨迹 τ 和行列式 δ 得到的固定点的分类。图片来自 Eugene Izhikevich,根据 CC BY-NC-SA 3.0 License 发布。

nullcline

如上述模型那样的二维神经元模型有两条 nullcline——这两条线上的偏导数为 0。当越过一条 nullcline 时,一个变量的演化会改变方向。只需查看动态系统的 nullcline,就能明白有关该动态系统的很多情况,包括固定点的位置。

分岔

分岔是动态系统中的质变——平衡点或极限环(limit cycle)的出现或消失这种质变。这些变化会在一个参数变化时发生——这就是所谓的分岔参数(bifurcation parameter)。在神经元模型中,分岔参数往往是输入电流 I。我们只会考虑 4 种分岔:

  • 鞍点节点分岔:在鞍点节点分岔处,稳定节点和不稳定节点消除彼此,融合成一个鞍点节点。在神经元模型中,鞍点节点分岔之前会有稳定平衡的消失。

  • 在极限环上的鞍点节点分岔:与鞍点节点分岔类似,但分岔点出现在极限环上。这个细节很重要,因为在平衡点处,极限环具有无限的周期长度,让周期性可以随分岔参数连续变化。当鞍点节点没出现在极限环上时,周期性是不连续的。这对神经元的计算特性有影响。

  • 超临界 Andronov-Hopf 分岔:在超临界 Andronov-Hopf 分岔处,随着极限环的出现和增长,稳定平衡会向不稳定平衡转变。接近超临界 Andronov-Hopf 分岔时,如果其输入是周期性的,那神经元就会尖峰放电。

  • 亚临界 Andronov-Hopf 分岔:在亚临界 Andronov-Hopf 分岔处,随着极限环的收缩然后消失,不稳定平衡会向稳定平衡转变。

鞍点节点分岔示例。注意这里的稳定固定点和不稳定固定点在融合成鞍点节点后彼此消除的方式。

发生在极限环上的鞍点节点分岔示例。注意这里的稳定固定点和不稳定固定点彼此消除的方式。图片来自 Eugene Izhikevich,根据 CC BY-NC-SA 3.0 License 发布。

超临界 Andronov-Hopf 分岔示例。注意这里稳定极限环的出现和增长。图片来自 Yuri A. Kuznetsov,根据 CC BY-NC-SA 3.0 License 发布。

亚临界 Andronov-Hopf 分岔示例。注意这里不稳定极限环的出现。图片来自 Yuri A. Kuznetsov,根据 CC BY-NC-SA 3.0 License 发布。

现在让我们继续深入!

模型

Hodgkin-Huxley 模型神经元

Hodgkin-Huxley 模型神经元是生理学建模领域内一项非常了不起的成就,这让 Alan Hodgkin 和 Andrew Huxley 获得了 1963 年的诺贝尔生理学或医学奖。这是计算神经科学领域内的一种重要模型。有趣的是,这个模型是通过研究鱿鱼巨轴突(squid giant axon)构建的,这控制了鱿鱼的喷水推进反射——其直径很大,易于实验。

你会看到其中有很多数学内容,但本质上讲,Hodgkin-Huxley 模型是将神经元的动态描述成了一个电路,其中电流的激活和失活有不同的时间尺度和不同的电压。

该模型包含 3 种电流 INa、IK 和 Ileak,都会影响膜电位。Ileak 不是门控的,而是 Cl- 离子缓慢渗透穿过膜。IK 是由 n 门控的,n 是一个无单位的变量 [0,1],表示 K+ 电压门控通道的激活状态(增加钾的流量)。INa 电流是由 m 和 h 门控的,其中 m 是一个无单位的变量 [0,1](增加钠的流量),表示 Na+ 电压门控通道的激活状态,h 也是一个无单位的变量 [0,1],表示 Na+ 通道的失活状态(停止钠流动)。

其中

其中

αx 是指门 x 转换为打开的速率,而 βx 是指门 x 转换为关闭的速率。

Hodgkin-Huxley 模型是接受范围最广的模型。这也是我们在本文中会谈到的最复杂的模型。后面的模型会更加抽象,并且缺乏 Hodgkin-Huxley 模型的一些特征,但它们分析和模拟起来更容易。

人工神经元

在人工神经网络中所使用的人工神经元是对生物神经元的绝妙约简。它们是对神经行为的抽象,即将神经行为约简成几个关键特征:(a)它们整合所有输入突触上的信号(求总和),(b)它们根据一个非线性函数对整合后的信号进行变换处理。

我们可以用数学方式描述(a):

其中神经元 j 是突触前神经元,i 是突触后神经元,vj 是从 j 到 i 的信号,wij 是该突触的强度。

用数学方式描述(b):

其中 f(Ii) 通常是 sigmoid 函数或线性整流函数。

相当简单,但有点投机取巧。这些最好是称为单元,而不是神经元,因为它们并不进行尖峰放电。尽管如此,这些单元的输出可以被解读成是放电率。尽管很简单,但这些单元在探索神经计算的性质上很有用。不仅如此,它们也被用在了当前最佳的人工智能算法中。事实证明,只有一个隐藏层的人工神经网络就能近似任意函数。

这一类神经元的强度源自可微分性——即使在网络内堆叠时也是如此。这个性质可以最小化或最大化某些目标来学习任意函数。但是,因为比较简单(比如缺乏尖峰放电机制和持久状态),它们并不是生物神经元的实际情况的模型。

整合放电模型

整合放电模型(integrate and fire model)是一种应用广泛的模型,常被用于探索网络的行为。这种简单模型抓住了神经行为的几个特征:(a)神经元尖峰放电和复位重置所遵循的膜阈值,(b)神经元无法放电的不应期,(c)状态——这是一种动态系统,膜电位(状态)会随时间演变。

但是,这个模型缺乏用于尖峰生成的真实机制。实际上在神经元尖峰放电时,我们也就只画一条竖线。此外,其膜阈值和不应期是绝对的,而在真实神经元中,它们会根据神经元的状态而变化。这些神经元也不能共振(resonate)。不是说这个模型无用——整合放电神经元有尖峰放电神经元的最基本特征,并且很容易推理。很多对网络行为的有趣探索都使用了这个模型。

基于电导的模型

基于电导的模型是一组类似 Hodgkin-Huxley 模型的方程——它们使用这些参数表示神经元:(1)各种离子通道的电导率,(2)细胞膜的电容。基于电导的模型很简单,但种类很多。和整合放电模型不同,基于电导的模型可以生成尖峰。有些还能进行共振、振荡、反弹尖峰、双稳态尖峰等。

展示 Na、K 和漏电电流的 RC 电路。离子通道用可变电阻器表示(带有箭头的电阻符号)。

这些模型这样描述流过神经元膜的电流:

使用了基尔霍夫电流定律(Kirchhoff's law)。

这些模型的动态的基本形式可以描述为:

正如去罗马有很多不同路径一样,实现模型尖峰放电的方式也很多。我们前面已经看了 Hodgkin-Huxley 模型,但因为那是一个 4 维系统,所以分析起来不容易。下面我们来看看一个 1 维系统和一个 2 维系统,其分别基于钠通道的动态和钠&钾通道的动态。

INa,p

持续钠模型(INa,p)只有一个钠通道,且这个钠通道能立即响应当前的膜电位;由于钠通道动态的速度相对较快,所以这种模型还算不错。这让我们有了一个非常简单的 1 维模型。现在,如果我们回想一下动作电位的生理过程,那么缺乏钾通道就意味着这个模型的动态本身应该无法实现复位重置。实际上我们需要添加一个条件来在尖峰放电后人工重置神经元。

看看状态空间,下面是一个特定的输入电流,有两个固定点——一个对应于平衡的膜电位,另一个对应于该尖峰放电的峰值膜电位。当然,有一个源点(source point)分开这两者。

在模型经历了一次分岔之后就会出现尖峰放电:平衡固定点和源点彼此消除,会导致神经元倾向于尖峰放电,不管其初始状态如何。

INa,p+IK

持续钠+钾模型(INa,p+IK)是在 INa,p 的基础上加上钾通道。不同于钠通道,钾通道不会即时激活,其激活速度比钠通道慢。不同于 INa,p,这个模型是 2 维的。

建模基于电导的模型的常见方法是将一整个神经元当作单个隔离单元——也就是忽略树突的所有结构复杂度。源自树突分支的信号的传播和合并方式很复杂,其建模的计算成本通常非常高。尽管基于电导的模型比我们前面看到的其它模型更接近实际,但你也应该清楚这个短板。为了解释这些特征,可能会用到电缆方程(cable equation)。

简单的动态模型

如果你不想受限在思考离子通道上——也就是说,如果你暂时想略过湿件(生物硬件)——那么你能在获取与神经计算相关的细胞行为情况的同时维持可追踪的分析和模拟所需的简洁性吗?Eugene Izhikevich 的简单模型背后的直观思想是好模型需要 1)一个描述膜电位快速变量,2)一个描述恢复动态过程的慢速变量。恢复变量无需明确代表特定的离子电流,而是可以描述所有恢复电流的总和。这种简单模型的构造取决于正确调整其参数,它可能接近鞍点节点分岔或 Andronof-Hopf 分岔。这在模拟神经元网络时有很大的优势。这种方法不仅能通过将动态过程简化为相对简单的方程描述的 2 个变量来降低计算成本,而且还可以使用一个模型表示多种神经性质。

神经元的计算性质

神经元可以解码来自其它神经元的信息,转换这些信息,然后将这些信息编码成尖峰的形式发送给其它神经元。神经元的计算性质让它们可以执行这种操作。下面给出了一些计算性质的示例(这个列表当然并不全面)。

1、2 和 3 类神经元

了解神经元的计算能力的一种方式是查看它们的 F-I 函数——即增益函数。Hodgkin 基于这一指标提出了 3 类神经元,如下面的图片所示:

1 类神经元的 F-I 曲线示例。注意这个曲线是平滑的。这些神经元可以生成任意低频率的尖峰队列。在模型中,当鞍点节点分岔发生在极限环上时,这就可能发生。在分岔之后,极限环的周期是无限的。

2 类神经元的示例。其放电率有不连续的跳变。2 类神经元不能生成任意低频率的尖峰队列。

3 类神经元没有有趣的 F-I 曲线,因为它们仅在输入开始时放电。也就是说,它们在给定一个恒定输入时只会生成单个尖峰。

但是,这些类别可以使用动态系统进行更丰富的描述。1 类神经元的行为是在极限环上的鞍点节点分岔的结果,而 2 类神经元的行为是其它三种分岔的结果。3 类神经元的行为会在固定点突然移动(比膜电位变化的速度快)时出现,使得神经元尖峰放电,然后返回变化后的平衡状态。

应该说还有很多其它「类型」的神经元。也就是说,神经元的区分不仅是在增益函数方面。实际上艾伦研究所(Allen Institute)目前就有一个研究神经元分类的项目。他们有一个非常棒的开源大脑细胞数据库:http://celltypes.brain-map.org。

积分器和共振器

我们也可以看神经元是积分器还是共振器,从动态系统的角度看,即神经元是经历鞍点节点分岔还是 Andronof-Hopf 分岔。

积分器会累积信号,直到它们的膜电位达到定义好的阈值(定义吸引盆的分界线)。时间相近的信号加到一起可以产生比相隔较远时更大的效果。积分器是并发检测器(coincidence detector)。某些积分器是 1 类神经元。

积分器示例。这个神经元会随时间累积持续的输入。

上面是一个接近鞍点节点分岔的模型。注意沿吸引盆的定义良好的阈值(蓝色线)。正输入的到来会驱使电流状态远离吸引盆,直到其最终避免引发动作电位。

共振器会对输入信号产生共振(振荡)。共振器不一定要有明确定义的阈值。在接近超临界 Andronof-Hopf 分岔的模型情况下,某些螺旋轨迹指向吸引盆之外,但其它轨迹不会。其阈值很模糊。不同于积分器,共振器对某个频率窄带最敏感。共振器也是并发检测器,但它们还是频率检测器。所有的共振器都是 2 类神经元。

积分器示例。我们可以使用三个幅度非常小的脉冲构建一个共振器神经元。因为这些脉冲在合适的共振频率上,所以会将膜电位越推越高。这类似于在秋千上按适当的节奏摆动你的腿。

FitzHugh-Nagumo 模型是一种共振器。在一次尖峰放电后,膜电位类似于衰减的振荡器。

这是一个接近超临界 Andronof-Hopf 分岔的共振器

结语

神经元处理信息的方式非常迷人和复杂,其中还有很多我们仍不清楚的地方(任何充满激情的科学家都会觉得振奋)。本文中所讨论模型还远不够完美。这些模型还没有考虑到神经元的一些重要方面,比如树突树的复杂结构或局部场电位(但确实存在这样的模型)。但是,要记住未来一段时间内,我们很可能都将无法获悉大脑中与认知相关的所有相关情况,模拟是一种研究神经计算的非常实用和有趣的方法。我希望这个系列文章能激励能人巧匠、黑客和程序开发者研究这些模型并找到可能的新发现。

参考文献

  • Eugene M. Izhikevich (2007) Equilibrium. Scholarpedia, 2(10):2014.

  • Yuri A. Kuznetsov (2006) Andronov-Hopf bifurcation. Scholarpedia, 1(10):1858.

  • Yuri A. Kuznetsov (2006) Saddle-node bifurcation. Scholarpedia, 1(10):1859.

  • Izhikevich, Eugene M. Dynamical systems in neuroscience. MIT press, 2007.

  • Dayan, Peter, and Laurence F. Abbott. Theoretical neuroscience. Vol. 806. Cambridge, MA: MIT Press, 2001.

原文链接:https://ift.tt/2Hwrt82

]]> 原文: https://ift.tt/2Kq8EEp
RSS Feed

机器知心

IFTTT

JavaScript 之父联手近万名开发者集体讨伐 Oracle:给 JavaScript 一条活路吧!- InfoQ 每周精要848期

「每周精要」 NO. 848 2024/09/21 头条 HEADLINE JavaScript 之父联手近万名开发者集体讨伐 Oracle:给 JavaScript 一条活路吧! 精选 SELECTED C++ 发布革命性提案 "借鉴"Rust...