博客进阶
机器学习代码与系统中的设计模式
Design Patterns in Machine Learning Code and Systems
这是一篇博客文章,讨论机器学习代码和系统中常见的设计模式,主要参照《设计模式》一书。文中涵盖库与代码层面的工厂模式等,并以 PyTorch 的 Dataset 子类化(重写 __len__ 和 __getitem__)为例说明自定义数据集的写法。适合从事机器学习工程、想为设计讨论建立共同词汇的人阅读。
- 分级
- 值得看
- 所属主题
- 编程智能体
- 收录时间
- 2026-10-29
中文精读
原文约 2,103 词 · 7 节
引言:设计模式不只是代码结构
- 设计模式不仅是组织代码的方式,还传递了该模式所解决的问题以及代码或组件打算如何被使用。
- 文中列举的模式多来自《设计模式》(Gang of Four)一书,作者在机器学习代码与系统中观察到这些模式的应用。
- 多数开发者对这些模式已有一定熟悉度,掌握基本概念能提供共同词汇,便于讨论设计与实现。
库与代码中的设计模式
- 工厂模式(factory pattern)把训练数据等对象与它们的创建方式解耦;创建过程可能复杂(如分布式数据加载器),提供基础工厂可简化对象创建并给出防止出错的约束。基础工厂可用接口或抽象类定义,新工厂通过子类化并给出自己的实现。
- PyTorch 的 Dataset 是工厂模式例子:自定义数据集需子类化并重写 __len__(返回数据集大小)与 __getitem__(按下标取第 i 个样本)。
- Gensim 的 TextCorpus 是另一例,简化文本文件读取供下游语言建模,用户重写 get_texts() 逐行(或逐文档)读取处理并返回词序列。
- Hugging Face 的 Dataset 无需子类化,把数据加载为 Apache Arrow 以低内存实现快速查找,并提供流式、交错、打乱等方法。
- 适配器模式(adapter pattern)提升接口之间的兼容性,如 CSV、Parquet、JSON 等数据格式,使接口不兼容的对象(如已存储数据)能协作;在数据管道中常用于把不同格式数据读入标准数据对象(如 Dataframe)。Pandas 有近 20 个读取适配器,Spark 有 Parquet、JSON、CSV、Hive、Text 等适配器。
- Apache Arrow 是跨 Pandas、Spark、Parquet、Cassandra 等框架的标准列式格式;Hugging Face Dataset 用 Arrow 作本地缓存系统。
库与代码中的设计模式(续)
- 装饰器模式(decorator pattern)让用户在运行时给已有代码添加功能,而不必改动同类其他对象的结构或行为;Python 中 @decorator 作用于 method() 等价于 method = decorator(method)。
- 内置例子是 functools 的 lru_cache():用字典缓存最近 x 次调用,把输入参数映射到返回结果;文中以缓存计算斐波那契数为例,cache_info() 显示 hits=28、misses=16。
- Pytest 用装饰器定义 fixture,为测试生成数据并跨测试共享,在运行测试前调用;文中给出加载样本数据、训练好的 DecisionTree 与 RandomForest 的 fixture 示例。
- 作者常用一个 timer 装饰器测量方法调用耗时并连同结果返回,便于在调用不同延迟模型的原型中查看每次调用耗时。
- 策略模式(strategy pattern)让用户改变对象的预期行为或算法:每种策略建一个对象,运行时行为随所用策略对象而变,从而把算法与客户端解耦,提升灵活性与复用性。
- XGBoost 内置多种建树方法(exact、approx、hist、gpu_hist)与目标函数(平方误差、logistic、pairwise ranking),也允许按接口提供自定义目标函数(如 squared log 误差)。Hugging Face 的 pipeline 便于用不同策略(语言模型)做推理,支持情感分析、翻译、问答等任务。
库与代码中的设计模式(续)
- 迭代器模式(iterator pattern)提供遍历对象集合的方式,把遍历算法与数据容器解耦,用户可自定义算法(如在树结构中切换广度优先与深度优先)。PyTorch 的 DataLoader 可设定批大小、是否打乱、工作进程数,甚至提供自定义 collate 函数。
- 管道模式(pipeline pattern)让用户串联一系列变换,变换包括数据清洗、特征工程、降维等,末尾可接估计器(即机器学习模型),管道接收数据、变换后在末端训练模型。作者认为从数据准备、特征工程到模型超参都可视为可调参数,能串联和变换使管道调参更方便,例如思考对连续变量取 np.log 或给文本特征加 trigram 会如何改变指标。
- 使用管道时应注意各变换步骤的输入输出需有标准格式(如 Pandas Dataframe),且每个变换要有 .fit() 与 .transform() 等必需方法。Sklearn 的 Pipeline 接收变换列表加最终估计器,可用 TransformerMixin 创建自定义变换;Spark MLlib 的管道围绕 dataframe、transformer 与 estimator 构建。
系统中的设计模式
- 代理模式(proxy pattern)为生产数据库或服务提供替代物,代理可代表该端点执行任务。一个例子是用缓存代理服务搜索查询:搜索查询符合帕累托原则,少数查询占据大部分请求,缓存热门查询(如查询建议、查询分类、检索与排序)的结果可减少实时推理所需算力。服务先查缓存,命中则直接返回,未命中则走实时推理。
- 另一个例子是用反向代理服务模型:横向扩展时用多台机器提供服务,但仍希望只暴露单一地址,反向代理在单一地址接收请求并分发到多台服务器以分摊负载、保证低延迟。
- 文中引用说明负载均衡器与反向代理的区别:负载均衡器指在多个 Web 服务器间分摊入站请求的服务器;反向代理通常功能更多,包括负载均衡、缓存(减少后端负载并直接返回静态内容)、安全(阻止互联网直接访问后端,可混淆后端或主动检查恶意代码)、SSL 加速(作为 SSL 会话终止点,把加密处理从后端卸载)。
- 中介者模式(mediator pattern)提供服务之间的中间人,限制服务直接通信,迫使服务间接通过中介者协作,防止服务显式引用彼此并实现他人接口,从而促进松耦合。
- 若系统中已有中介者,机器学习服务应通过中介者提供给下游应用(如电商网站、交易订单),而非直接对接;中介者通常有标准要求(如请求 schema)与约束(如延迟、限流)。
- 以 Netflix 首页推荐组件为例:不让各组件自行决定如何展示,而是要求每个组件经过一个中介者,由中介者对组件排序,还可做检查(如排除条目少于 x 的组件)或更新(如跨多个组件去重条目),把组件分配到首页位置。
结语:模式即意图的传达
- 知道某个东西是工厂、装饰器还是中介者,有助于按设计意图使用它;反过来,在开发库或系统时用对模式,也能帮助用户理解开发者的意图。
- 文末向读者征询在机器学习代码与系统中还见过哪些常见设计模式。
参考与引用
- 参考书目包括《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)、《Head First Design Patterns》、Refactor Guru 设计模式站,以及 Mark Saroufim 的 ml-design patterns 仓库。
- 引用格式:Yan, Ziyou(2022 年 6 月),Design Patterns in Machine Learning Code and Systems,eugeneyan.com,并附对应 BibTeX 条目(title、author、journal、year、month、url)。