本专题将通过 两篇系列文章,介绍与体系结构和设计有关的软件工程基础知识,以及如何在机器学习管道(ML Pipeline)的每个步骤中应用这些基础知识:
第 1 部分:问题陈述 |架构风格 |设计模式 |SOLID
第 2 部分:构建机器学习管道
就像以前在 Steven Geringer 著名的 维恩图 中看到的,数据科学是 3 个学科的交集:计算机科学、数学 / 统计学和特定的领域知识。
数据科学维恩图 [©Steven Geringer]
拥有基本(甚至是高级)的编程技能,是将端到端的经验结合在一起的关键,但还不足以创建一个可以发布的应用程序。除非从 IT 背景 进入数据科学和机器学习(ML),并且在构建企业级的、分布式的、可靠的系统方面拥有实际经验,否则你的 Jupyter 笔记本(译者注:可创建和共享自己的文学化程序文档)没资格成为优秀的软件,遗憾的是,你也不能成为软件工程师!
虽然你打算构建的是一个很好的预测产品的 原型,但仍然需要通过工程技术的路线图来推动它。所需要的是一个由专业软件工程师组成的团队,可以将(一次性)概念验证转化为 高性能、可靠、松耦合 和 可扩展的 系统!
一切都是设计出来的 ; 但设计得好的东西很少!
在本系列中,我们将展示如何实现良好设计的一些想法。第一部分从基础知识开始,第二部分逐步设计整体架构。这里建议的架构是 技术无关的, 机器学习管道将被分解成责任明确的不同层,可以从许多技术堆栈中选择如何来实现每一层。
接下来看看成功的解决方案应该是如何实现的。
我们的主要目标是建立一个系统,具备以下特性:
减少延迟;
集成系统的其他部分(例如数据存储,报告,图形用户界面),但松耦合;
可以水平和垂直伸缩;
消息驱动,即系统通过异步、非阻塞的消息传递进行通信;
提供工作负载管理相关的高效计算;
容错和自我修复,即故障管理;
支持批量和实时处理。
我们将首先介绍一个反应式系统,并快速浏览最流行的架构模式。
反应式系统设计范式采用前后一致的方法来建立更好的系统,这些系统是根据“反应式宣言” 的原则进行设计的。每个反应式原则对应一个可扩展性的重要系统维度:
易响应的 → 时间
易扩展的 → 负载
可恢复的 → 错误
消息驱动的 → 通信
反应系统的特性
SOA 将业务问题分解为服务,这些服务通过网络来共享信息。它们还共享代码(即公共组件),以保持反应式系统的一致性和特性,从而减少开发工作。
服务 提供者 发布合约,规定服务的特征以及如何使用服务。服务 使用者 可以在注册中心中找到服务元数据,开发所需的客户端组件来绑定和使用它。
协调器(Orchestrator)是一个综合服务,负责调用和组合其他服务。此外,编排(Choreography)采用去中心化的方法组合服务,服务通过消息 / 事件的交换进行交互。
SOA
流式架构包括以下组件:
生产者:生成和发送消息的应用程序
消费者:订阅和使用消息的应用程序
主题:类别特定的记录流,存储成有序和不可变的记录序列,通过分布式集群进行分区和复制
流处理器:以特定方式处理消息的应用程序(例如数据转换、ML 模型等)。
流式架构
Lambda(λ)架构旨在以集成的方式处理 实时 和过去聚合的 批量数据。它分离了实时和批处理的职责,查询层提供了所有数据的统一视图。
这个概念很简单:生成数据时,会在存储之前对其进行处理,因此分析可以包括最后一秒、最后一分钟或最后一小时生成的数据,只处理传入的数据,而不是所有的数据。
Lambda 架构
微服务是一种架构风格,它将应用程序构造为小型、自主、松耦合和协作的服务集合,围绕业务领域进行建模。这些服务使用同步协议(HTTP/REST)或异步协议(AMQP)进行通信,可以彼此 独立 地开发和 部署。每个服务都有自己的数据库,以便与其他服务分离。
微服务架构
REST 是一种用于开发 Web 服务 的架构风格,它建立在 Internet HTTP 的现有特性之上,允许以无状态的方式传输、访问和操作文本数据,即应用程序可以在不知道状态的情况下进行通信。
RESTful API 服务通过统一资源定位器(URL)公开,提供了创建、请求、更新或删除(CRUD)数据的功能。它最适合处理解耦(生成 / 消费的 ) 信息和(生成 / 使用信息的 ) 技术的系统。
REST 架构
我们将浅显地介绍这个主题,并且只讨论我在本系列的第二部分中可能提到的那些模式。(虽然我每天都在使用它们,但很难用简单的语言全部解释清楚)
软件设计模式是针对软件工程中常见问题的优化的、可重复的解决方案。它是一个解决问题的模板,可以在许多不同的情况下使用。
策略模式定义了一系列算法,将每个算法放在一个单独的类中,相互之间 可互换。将行为封装在单独的类中,消除了任何条件语句,在运行时可以选择正确的算法(即策略)。
使用说明:业务规则有不同的实现,或者需要算法的不同变体。
策略模式
模板方法旨在从不同的过程中抽象出一个通用的过程。它定义了算法的 骨架,将某些步骤推迟到子类。子类可以覆盖某些行为,但不能更改骨架。
使用说明:遵循一系列一致的步骤,各个步骤可能有不同的实现。
⭐ 与策略模式的区别:
模板:子类 在 编译时 选择算法。
策略:控制 在 运行时 选择算法。
模板方法模式
责任链模式通过启用一个或多个 处理程序 来满足请求,避免将客户端(请求的发送者)与接收者耦合。这些处理程序连接到一个链中,每个处理程序都包含对链中下一个处理程序的引用。
使用说明:多个对象可以处理一个请求,处理程序(或序列)的优先级事先不知道。
责任链模式
观察者模式(缩写为 Publish/Subscribe 或简称 PubSub)通过定义对象之间的一对多依赖关系,可以轻松地进行通信 广播,当一个对象的状态发生变化时,它的所有依赖关系都会自动得到通知和更新。观察者负责注册它们所“观察”的事件。
使用说明:当改变某个对象时同时需要改变其他对象,不知道多少对象需要改变。
观察者模式
建造者模式旨在 逐步 构造一个复杂的对象,分离构造与表示。实质上,它允许使用相同的代码,生成对象的不同类型和表示形式。
使用说明:虽然各个构造步骤有所不同,但可以使用相同的整体构建过程来构建多种复杂对象。
建造者模式
工厂方法定义了一个用于 创建对象 的接口,由子类来完成实例化。
使用说明:对象的具体类型和依赖关系事先不知道。
抽象工厂关注如何创建 相关产品的系列, 不指定它们具体的类。
使用说明:不同的规则采用不同的实现,这些规则要么是未知的,要么是可扩展的。
⭐️ 与抽象方法的区别:
抽象工厂:创建其他工厂,这些工厂反过来创建从基类派生的对象。
抽象方法:创建从特定基类派生的对象。
装饰模式动态地将新的职责附加到对象上,将对象放置在包含行为的特殊包装类(wrapper)中,因此不会影响原来方法的签名(继承上的组合)。
使用说明:运行时为对象分配额外的行为,不会破坏使用这些对象的代码。
仓库模式解决了数据检索和持久化的代码集中化问题,并为 数据访问 操作提供了 抽象, 类似内存中域对象的集合,允许执行 CRUD 方法,消除了各种数据库问题。
使用说明:分离业务逻辑和访问数据的代码。
想进一步了解模式?可以先从“Gang of Four”的书《设计模式:可重用的面向对象软件的基础》开始。下面的图展示了模式之间的关系,非常重要:
设计模式之间的关系
我们唯一的设计原则是 SOLID,它们对于每个软件开发人员来说都是必不可少的。
正如 Bob 大叔 所说:“它们不是法律,也不是完美的真理。它们类似于一个规则:每天一个苹果,医生远离我 ”。
这意味着,它们不是某种“魔法”,不是带来牛奶、蜂蜜和优质软件的应许之地,它们是健壮、持久软件的关键贡献者。
简而言之,这些原则围绕两个主要的概念展开,是成功企业应用程序的基石:耦合 是类了解另一个类并与之交互的程度;而 内聚 表示类具有单一用途的程度。换一种说法:
耦合是关于类如何相互作用的;
内聚关注单个类的设计方式。
一个类应该有一个,而且只有一个变化的理由
这是不言自明的,但说起来容易做起来难。我们总是想在现有类中添加新的行为,但这是一个引发灾难的处方:每个行为都可能成为未来变化的理由,因此行为越少,在变化时产生错误的机会就越少。
能够扩展类的行为,而无需对其进行修改
类应该对扩展“开放”,但是对修改“关闭”。要实现这一点可以通过继承,即创建一个子类,关闭对原始类进行修改,将自定义的代码添加到子类来引入新的行为。
派生类必须可替代基类
当类 A 的行为扩展到子类 B 时,可以确保在不造成任何破坏的情况下用 B 替换 A。这点可能比较吸引人,特别是当把这一原则与开闭原则结合起来时。
创建特定于客户端的细粒度接口
接口和类必须尽可能特定,客户端的调用不依赖未使用的方法。这与单一责任原则是一致的。
依赖抽象,而不是具体实现
高层的类不应该依赖于低层的类。它们都应该依赖于抽象。同样,抽象不依赖于细节,细节依赖于抽象。
我创建了下面这个快速参考图。如果想知道左边小符号的灵感来自哪里,请参看:“SOLID 原则,用励志海报解释” —— 我喜欢作者在这些原则上增加的有趣改变。
这里无法涵盖所有软件工程概念的详尽列表,它只是阅读下一篇文章的基础,我希望它能让读者了解构建可扩展软件的主要贡献因素。应用程序设计是否能够 适应变化,是构建成功解决方案的关键,如果设计过程很仓促,项目结束时,一定会为犯下的错误交付学费。
好的设计是易懂的。伟大的设计是透明的。
原文链接:
https://towardsdatascience.com/being-a-data-scientist-does-not-make-you-a-software-engineer-c64081526372
你也「在看」吗?👇