C--神经网络变成实用指南-全-

C# 神经网络变成实用指南(全)

原文:zh.annas-archive.org/md5/a1bce117e6e7fb1600cbc7fe1b24ba5e

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章

使用 C#进行神经网络的实战编程

为您的 C#企业应用程序添加强大的神经网络功能

马特·R·科尔

伯明翰 - 孟买

第二章:《使用 C#进行神经网络的实战编程》

版权© 2018 Packt Publishing

版权所有。未经出版者事先书面许可,本书的任何部分不得以任何形式或通过任何手段进行复制、存储在检索系统中或进行传输,除非在评论或评论文章中嵌入的简短引用。

在准备本书的过程中,已尽一切努力确保所提供信息的准确性。然而,本书中的信息销售不附带任何明示或暗示的保证。作者、Packt Publishing 或其经销商和分销商不对由此书直接或间接造成的任何损害承担责任。

Packt Publishing 已尽力通过适当使用大写字母提供本书中提到的所有公司和产品的商标信息。然而,Packt Publishing 不能保证此信息的准确性。

采购编辑: Pravin Dhandre

采购编辑: Divya Poojari

内容开发编辑: Unnati Guha

技术编辑: Dinesh Chaudhary

校对编辑: Safis Editing

项目协调员: Manthan Patel

校对: Safis Editing

索引员: Rekha Nair

图形设计: Jisha Chirayil

生产协调员: Nilesh Mohite

首次出版: 2018 年 9 月

生产参考号: 1270918

由 Packt Publishing Ltd. 出版

Livery Place

35 Livery Street

伯明翰

B3 2PB, UK.

ISBN 978-1-78961-201-1

www.packtpub.com

第三章

本书献给我的始终支持我的爱妻,Nedda。

我还想感谢 Packt 出版社那些勤奋且专业的团队。

对于他们辛勤的工作和对将我的所有书籍推向市场的奉献。

图片mapt.io

Mapt 是一个在线数字图书馆,为您提供超过 5,000 本书籍和视频的全面访问权限,以及领先的行业工具,帮助您规划个人发展并推进职业生涯。如需更多信息,请访问我们的网站。

第四章:为什么订阅?

  • 使用来自超过 4,000 位行业专业人士的实用电子书和视频,花更少的时间学习,更多的时间编码

  • 通过为您量身定制的技能计划提高您的学习效果

  • 每月免费获得一本电子书或视频

  • Mapt 完全可搜索

  • 复制粘贴、打印和收藏内容

Packt.com

您知道 Packt 为每本书都提供了电子书版本,并提供 PDF 和 ePub 文件吗?您可以在www.packt.com升级到电子书版本,并且作为印刷版书籍的顾客,您有权获得电子书副本的折扣。如需了解更多详情,请联系我们customercare@packtpub.com

www.packt.com,您还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠

第五章:贡献者

关于作者

Matt R. Cole 是一位拥有 30 年经验的开发者和作家。Matt 是 Evolved AI Solutions 的所有者,这是一家提供高级机器学习/生物人工智能、微服务和群集技术的供应商。Matt 被认为是微服务和人工智能开发和设计领域的领导者。作为 VOIP 的早期先驱,Matt 为国际空间站和航天飞机开发了 VOIP 系统。他还开发了第一个完全集成镜像和规范神经元的生物人工智能框架。在业余时间,Matt 撰写书籍,并继续他的教育,参加所有可用的高级数学、AI/ML/DL、量子力学/物理学、弦理论和计算神经科学课程。

关于审稿人

Gaurav Aroraa 拥有计算机科学硕士学位。他是微软 MVP,认证为敏捷教练/培训师,XEN for ITIL-F,以及 APMG for PRINCE-F 和 PRINCE-P。Gaurav 在 IndiaMentor 担任导师,在 dotnetspider 担任网站管理员,并共同创立了 Innatus Curo Software LLC。在他超过 19 年的职业生涯中,他指导了超过一千名行业学生和专业人士。您可以通过 Twitter @g_arora 联系 Gaurav。

Rich Pizzo 在软件和系统的设计和开发方面拥有多年的经验。他曾是一名高级架构师和项目负责人,特别是在金融工程和交易系统领域。他在两家公司担任首席技术官。他在数字电子方面的知识和专长在软件领域留下了印记,同时在解决复杂的优化问题中提供了异构解决方案。他提出了许多独特的解决方案,以最大化计算性能,利用 Altera FPGAs 和 Quartus 开发环境及测试套件。

Packt 正在寻找像你这样的作者

如果你对成为 Packt 的作者感兴趣,请访问 authors.packtpub.com 并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们与全球技术社区分享他们的见解。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。

目录

  1. 标题页

  2. 版权和致谢

    1. 使用 C# 进行神经网络编程实战
  3. 献词

  4. Packt Upsell

    1. 为什么订阅?

    2. Packt.com

  5. 贡献者

    1. 关于作者

    2. 关于审稿人

    3. Packt 正在寻找像你这样的作者

  6. 前言

    1. 本书面向读者

    2. 本书涵盖内容

    3. 充分利用本书

      1. 下载示例代码文件

      2. 下载彩色图像

    4. 代码实战

      1. 使用的约定
    5. 联系我们

      1. 评论
  7. 快速回顾

    1. 技术要求

    2. 神经网络概述

      1. 神经网络训练

      2. 神经网络视觉指南

    3. 神经网络在当今企业中的作用

    4. 学习类型

      1. 监督学习

      2. 无监督学习

      3. 强化学习

    5. 理解感知器

      1. 这有用吗?
    6. 理解激活函数

      1. 可视化激活函数绘图

      2. 函数绘图

    7. 理解反向传播

      1. 正向和反向传播的区别
    8. 总结

    9. 参考文献

  8. 一起构建我们的第一个神经网络

    1. 技术要求

    2. 我们的神经网络

    3. 神经网络训练

      1. 突触

      2. 神经元

      3. 正向传播

      4. Sigmoid 函数

      5. 反向传播

      6. 计算误差

      7. 计算梯度

      8. 更新权重

      9. 计算值

    4. 神经网络函数

      1. 创建新网络

      2. 导入现有网络

      3. 导入数据集

      4. 测试网络

      5. 导出网络

      6. 训练网络

      7. 测试网络

      8. 计算前向传播

      9. 导出网络

      10. 导出数据集

    5. 神经网络

      1. 神经元连接
    6. 示例

      1. 训练至最小

      2. 训练至最大

    7. 总结

  9. 决策树和随机森林

    1. 技术要求

    2. 决策树

      1. 决策树优势

      2. 决策树缺点

      3. 我们应该何时使用决策树?

    3. 随机森林

      1. 随机森林优势

      2. 随机森林缺点

      3. 我们应该何时使用随机森林?

    4. SharpLearning

      1. 术语

      2. 加载和保存模型

    5. 示例代码和应用

      1. 保存模型

      2. 均方误差回归指标

      3. F1 分数

      4. 优化

      5. 样本应用 1

        1. 代码
      6. 样本应用 2 – 葡萄酒质量

        1. 代码
    6. 总结

    7. 参考文献

  10. 人脸和动作检测

    1. 技术要求

    2. 人脸检测

    3. 运动检测

      1. 代码
    4. 总结

  11. 使用 ConvNetSharp 训练 CNN

    1. 技术要求

    2. 熟悉

    3. 过滤器

    4. 创建网络

      1. 示例 1 —— 一个简单的示例

      2. 示例 2 —— 另一个简单的示例

      3. 示例 3 —— 我们最后的简单示例

      4. 使用 Fluent API

    5. GPU

    6. 使用 MNIST 数据库进行流畅训练

    7. 训练网络

      1. 测试数据

      2. 预测数据

      3. 计算图

    8. 总结

    9. 参考文献

  12. 使用 RNNSharp 训练自动编码器

    1. 技术要求

    2. 什么是自动编码器?

    3. 不同类型的自动编码器

      1. 标准自动编码器

      2. 变分自动编码器

      3. 降噪自动编码器

      4. 稀疏自动编码器

    4. 创建自己的自动编码器

    5. 总结

    6. 参考文献

  13. 用 PSO 代替反向传播

    1. 技术要求

    2. 基本理论

      1. 群体智能

      2. 粒子群优化

        1. 粒子群优化类型

        2. 原始粒子群优化策略

        3. 粒子群优化搜索策略

          1. 粒子群优化搜索策略伪代码
        4. 参数对优化效果的影响

    3. 用粒子群优化代替反向传播

    4. 总结

  14. 函数优化:如何以及为什么

    1. 技术要求

    2. 开始使用

    3. 函数的最小化和最大化

      1. 什么是粒子?

      2. 群体初始化

      3. 图表初始化

      4. 状态初始化

      5. 控制随机性

      6. 更新群体位置

      7. 更新群体速度

      8. 主程序初始化

      9. 运行粒子群优化

      10. 我们的用户界面

        1. 运行按钮

        2. 倒退按钮

        3. 后退按钮

        4. 播放按钮

        5. 暂停按钮

        6. 前进按钮

    4. 超参数和调整

      1. 功能

      2. 策略

      3. 尺寸

      4. 上限

      5. 下限

      6. 上限速度

      7. 下限速度

      8. 小数位数

      9. 群体大小

      10. 最大迭代次数

      11. 惯性

      12. 社会权重

      13. 认知权重

      14. 惯性权重

    5. 理解可视化

      1. 理解二维可视化

      2. 理解三维可视化

    6. 绘制结果

      1. 回放结果

      2. 更新信息树

    7. 添加新的优化函数

      1. 函数的目的

      2. 添加新函数

      3. 让我们添加一个新函数

    8. 总结

  15. 寻找最优参数

    1. 技术要求

    2. 优化

      1. 什么是适应度函数?

        1. 最大化

        2. 基于梯度的优化

        3. 启发式优化

      2. 约束

        1. 边界

        2. 惩罚函数

        3. 一般约束

        4. 约束优化阶段

        5. 约束优化困难

        6. 实现

      3. 元优化

        1. 适应度归一化

        2. 多个问题的适应度权重

        3. 建议

        4. 约束和元优化

        5. 元元优化

    3. 优化方法

      1. 选择一个优化器

      2. 梯度下降(GD)

        1. 它是如何工作的

        2. 缺点

      3. 模式搜索(PS)

        1. 它是如何工作的
      4. 局部单峰采样(LUS)

        1. 它是如何工作的
      5. 微分进化(DE)

        1. 它是如何工作的
      6. 粒子群优化(PSO)

        1. 它是如何工作的
      7. 许多优化联络员(MOL)

      8. 网格(MESH)

    4. 并行性

      1. 并行化优化问题

      2. 并行优化方法

        1. 必要的参数调整
      3. 最后,是代码

      4. 执行元优化

      5. 计算适应度

      6. 测试自定义问题

      7. 基本问题

      8. 创建自定义问题

      9. 我们的自定义问题

    5. 摘要

    6. 参考文献

  16. 使用 TensorFlowSharp 进行目标检测

    1. 技术要求

    2. 与张量一起工作

      1. TensorFlowSharp
    3. 开发自己的 TensorFlow 应用程序

    4. 检测图像

      1. 对象高亮的最低分数
    5. 摘要

    6. 参考文献

  17. 使用 CNTK 进行时间序列预测和 LSTM

    1. 技术要求

    2. 长短期记忆

      1. LSTM 变体

      2. LSTM 的应用

    3. CNTK 术语

    4. 我们的示例

      1. 编码我们的应用程序

        1. 加载数据和图表

        2. 加载训练数据

        3. 填充图表

        4. 分割数据

      2. 运行应用程序

      3. 训练网络

      4. 创建模型

      5. 获取下一个数据批次

      6. 创建数据批次

    5. LSTM 表现如何?

    6. 摘要

    7. 参考文献

  18. GRU 与 LSTM、RNN 和前馈网络的比较

    1. 技术要求

    2. QuickNN

    3. 理解 GRU

    4. LSTM 与 GRU 之间的区别

      1. 使用 GRU 与 LSTM 的比较
    5. 编码不同的网络

      1. 编码 LSTM

      2. 编码 GRU

    6. 比较 LSTM、GRU、前馈和 RNN 操作

    7. 网络差异

    8. 摘要

  19. 激活函数计时

  20. 函数优化参考

    1. Curry 指数函数

      1. 描述

      2. 输入域

      3. 修改和替代形式

    2. Webster 函数

      1. 描述

      2. 输入分布

    3. Oakley & O'Hagan 函数

      1. 描述

      2. 输入域

    4. 语法函数

      1. 描述

      2. 输入域

    5. Franke 函数

      1. 描述

      2. 输入域

    6. 极限函数

      1. 描述

      2. 输入域

    7. Ackley 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    8. Bukin 函数 N6

      1. 描述

      2. 输入域

      3. 全局最小值

    9. 交叉-中转功能

      1. 描述

      2. 输入域

      3. 全局最小值

    10. 滴波函数

      1. 描述

      2. 输入域

      3. 全局最小值

    11. Eggholder 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    12. Holder 表格函数

      1. 描述

      2. 输入域

      3. 全局最小值

    13. 利维函数

      1. 描述

      2. 输入域

      3. 全局最小值

    14. 利维函数 N13

      1. 描述

      2. 输入域

      3. 全局最小值

    15. 拉斯特里金函数

      1. 描述

      2. 输入域

      3. 全局最小值

    16. 沙弗函数 N.2

      1. 描述

      2. 输入域

      3. 全局最小值

    17. 沙弗函数 N.4

      1. 描述

      2. 输入域

    18. 舒伯特函数

      1. 描述

      2. 输入域

      3. 全局最小值

    19. 旋转超椭球函数

      1. 描述

      2. 输入域

      3. 全局最小值

    20. 平方和函数

      1. 描述

      2. 输入域

      3. 全局最小值

    21. 博斯函数

      1. 描述

      2. 输入域

      3. 全局最小值

    22. 麦科密克函数

      1. 描述

      2. 输入域

      3. 全局最小值

    23. 幂和函数

      1. 描述

      2. 输入域

    24. 三峰骆驼函数

      1. 描述

      2. 输入域

      3. 全局最小值

    25. Easom 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    26. Michalewicz 函数

      1. 描述

      2. 输入域

      3. 全局极小值

    27. Beale 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    28. Goldstein-Price 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    29. Perm 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    30. Griewank 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    31. Bohachevsky 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    32. Sphere 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    33. Rosenbrock 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    34. Styblinski-Tang 函数

      1. 描述

      2. 输入域

      3. 全局最小值

    35. 摘要

    36. 继续阅读

  21. 你可能喜欢的其他书籍

    1. 留下评论 - 让其他读者了解你的想法

前言

本书将帮助用户学习如何在 C#中开发和编程神经网络,以及如何将这项激动人心且强大的技术添加到自己的应用程序中。我们将使用许多开源包以及定制软件,从简单概念和理论到每个人都可以使用的强大技术,逐步进行。

本书面向对象

本书面向 C# .NET 开发者,旨在学习如何将神经网络技术和技巧添加到他们的应用程序中。

本书涵盖内容

第一章,快速回顾,为你提供神经网络的基本复习。

第二章,一起构建我们的第一个神经网络,展示了激活是什么,它们的目的,以及它们如何以视觉形式出现。我们还将展示一个小型 C#应用程序,使用开源包如 Encog、Aforge 和 Accord 来可视化每个部分。

第三章,决策树和随机森林,帮助你理解决策树和随机森林是什么,以及它们如何被使用。

第四章,人脸和动作检测,将指导你使用 Accord.Net 机器学习框架连接到你的本地视频录制设备,并捕获摄像头视野内的实时图像。视野内的任何人脸都将随后被跟踪。

第五章,使用 ConvNetSharp 训练 CNNs,将专注于如何使用开源包 ConvNetSharp 来训练 CNNs。将通过示例来阐述用户的概念。

第六章,使用 RNNSharp 训练自动编码器,将指导你使用开源包 RNNSharp 中的自动编码器来解析和处理各种文本语料库。

第七章,用 PSO 替换反向传播,展示了粒子群优化如何替换用于训练神经网络的反向传播等神经网络训练方法。

第八章,函数优化:如何以及为什么,介绍了函数优化,这是每个神经网络不可或缺的一部分。

第九章,寻找最优参数,将展示你如何使用数值和启发式优化技术轻松找到神经网络函数的最优参数。

第十章,使用 TensorFlowSharp 进行目标检测,将向读者介绍开源包 TensorFlowSharp。

第十一章,使用 CNTK 进行时间序列预测和 LSTM,将展示你如何使用微软认知工具包(CNTK),以及长短期记忆(LSTM)来完成时间序列预测。

第十二章,GRUs 与 LSTMs、RNNs 和前馈网络的比较,讨论了门控循环单元(GRUs),包括它们与其他类型神经网络的比较。

附录 A,激活函数时间表,展示了不同的激活函数及其相应的图表。

附录 B,函数优化参考,包括不同的优化函数。

要充分利用本书

在本书中,我们假设读者具备基本的 C# .NET 软件开发知识和熟悉度,并且知道如何在 Microsoft Visual Studio 中操作。

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择 SUPPORT 标签页。

  3. 点击 Code Downloads & Errata。

  4. 在搜索框中输入书名,并遵循屏幕上的说明。

文件下载完成后,请确保使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Neural-Network-Programming-with-CSharp。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789612011_ColorImages.pdf

代码实战

访问以下链接查看代码运行的视频:bit.ly/2DlRfgO

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”

代码块设置如下:

m_va.Copy(vtmp, m_bestVectors[i])
m_va.Sub(vtmp, particlePosition);
m_va.MulRand(vtmp, m_c1);
m_va.Add(m_velocities[i], vtmp);

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

BasicNetworknetwork = EncogUtility.SimpleFeedForward(2, 2, 0, 1, false);
///Create a scoring/fitness object
ICalculateScore score = new TrainingSetScore(trainingSet);

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中会这样显示。以下是一个例子:“从管理面板中选择“系统信息”。”

警告或重要提示会像这样显示。

小贴士和技巧会像这样显示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com给我们发送邮件。

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,我们将非常感激您能提供位置地址或网站名称。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评价

请留下您的评价。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

想了解更多关于 Packt 的信息,请访问packt.com

第六章:快速回顾

欢迎来到《使用 C#进行动手神经网络开发》。我要感谢您购买这本书,并与我们一同踏上这段旅程。似乎无论你转向何方,无论你走到哪里,你所听到和读到的都是机器学习、人工智能、深度学习、神经元这个、人工那个,等等。而且,为了增加所有的兴奋感,你与每个人交谈时,每个人对每个这些术语的含义都有略微不同的看法。

在本章中,我们将介绍一些非常基础的神经网络术语,为后续章节做好准备。我们需要确保我们说的是同一种语言,以确保我们在后续章节中所做的一切都清晰明了。

我还应该让你知道,这本书的目标是尽可能快地将你,一个 C#开发者,培养起来。为了做到这一点,我们将尽可能多地使用开源库。我们必须做一些自定义应用程序,但我们已经提供了这些应用程序的源代码。在所有情况下,我们都希望你能以最快速度、最少的努力将这个功能添加到你的应用程序中。

好的,让我们开始吧。

神经网络已经存在了很长时间,但在过去几年中又重新兴起,现在成为了热门话题。这就是为什么这本书要被写出来的原因。我们的目标是帮助您摆脱困境,进入开阔地带,以便您能够成功导航您的神经网络之路。这本书特别关注 C# .NET 开发者。我想确保那些 C#开发者能够手头上有一些有用的资源,可以在他们的项目中提供帮助,而不是我们更常见的 Python、R 和 MATLAB 代码。如果你已经安装了 Visual Studio,并且有强烈的求知欲望,你就可以开始你的旅程了。

首先,让我们确保我们对一些事情有清晰的认识。在编写这本书时,假设读者你对神经网络接触有限。如果你有一些接触,那很好;你可以自由地跳到你最感兴趣的章节。我还假设你是一位经验丰富的 C#开发者,并使用 C#、.NET 和 Visual Studio 构建了应用程序,尽管我没有假设你使用过哪些版本。目标不是关于 C#语法、.NET 框架或 Visual Studio 本身。再次强调,目的是将尽可能多的宝贵资源交给开发者,让他们能够丰富他们的代码并创建世界级的应用程序。

现在我们已经解决了这些问题,我知道你迫不及待地想要直接开始编码,但为了让你更有效率,我们首先必须花一些时间来复习一些基础知识。一点理论,一些关于原因和目的的迷人洞察,我们还会在过程中加入一些视觉元素来帮助理解那些枯燥的内容。别担心,我们不会在理论上走得太深,而且,从现在起几页之后,你将开始编写和审查源代码!

此外,请记住,这个领域的研究正在迅速发展。今天最新和最伟大的东西,下个月可能就变成了旧闻。因此,请将这本书视为不同研究和观点的概述。它不是关于神经网络相关一切的终极圣经,也不应该被这样看待。你很可能遇到持有不同观点的人。你会遇到那些会以不同方式编写应用程序和函数的人。那很好——收集你能收集到的所有信息,并根据自己的判断做出明智的选择。只有通过那样做,你才能增加你的知识库。

本章将包括以下主题:

  • 神经网络概述

  • 神经网络在当今企业中的作用

  • 学习类型

  • 理解感知

  • 理解激活函数

  • 理解反向传播

技术要求

为了理解我们将在本书中开发的应用程序,必须具备 C#的基本知识。此外,Microsoft Visual Studio(任何版本)是开发应用程序的首选软件。

神经网络概述

让我们先明确一下我们将如何称呼神经网络。首先,我要指出,你可能也会听到神经网络被称为人工神经网络ANN)。虽然我个人不喜欢“人工”这个术语,但在这本书中,我们将交替使用这些术语。

“让我们明确指出,在它的最简单形式中,神经网络是一个由几个简单但高度互联的元素组成的系统;每个元素都根据对外部输入的反应来处理信息。”

你知道吗,神经网络通常更常见地,但不是很严格地,是根据哺乳动物大脑的皮层来建模的吗?为什么我没有说它们是根据人类来建模的呢?因为有很多情况下,生物和计算研究使用了老鼠、猴子,甚至是人类的脑部。一个大的神经网络可能有数百甚至数千个处理单元,而哺乳动物大脑有数十亿个。是神经元在创造奇迹,实际上我们可以就这个话题写一本书。

正如我所说,它们之所以能做所有这些魔法般的事情:如果给你看一张 Halle Berry 的照片,你会立刻认出她。你不会有时间去分析事物;你会根据一生积累的知识立刻知道。同样,如果我对你说出“pizza”这个词,你会有一个立即的脑中图像,甚至可能开始感到饿。这一切是如何突然发生的?是神经元!尽管今天的神经网络在力量和速度上仍在不断进步,但它们与人类大脑这个终极神经网络相比,相形见绌。我们对这个神经网络了解和理解的还很少;等着看当我们了解后,神经网络会变成什么样子!

神经网络被组织成由所谓的节点神经元组成的。这些节点就是神经元本身,并且相互连接(在这本书中,我们交替使用节点神经元这两个术语)。信息被呈现给输入层,由一个或多个隐藏层进行处理,然后输出给输出层进行最终(或进一步)处理——重复这个过程!

"但什么是神经元呢?"你可能会问?使用以下图表,让我们这样表述:

"神经元是神经网络中的基本计算单元"

如我之前提到的,神经元有时也被称为节点或单元。它从其他节点或外部来源接收输入并计算输出。每个输入都有一个相关的权重(以下称为w1 和 w2),这个权重是根据其相对于其他输入的重要性分配的。节点将其输入的加权和应用于一个函数f(一个激活函数,我们稍后会了解更多),这就是神经元的基本功能。尽管这只是一个极端简化的神经元是什么以及它能做什么的描述,但基本上就是这样。

让我们直观地看看从单个神经元到非常深的深度学习网络的演变过程。以下是根据我们的描述,单个神经元的外观:

更复杂的神经网络图

接下来,以下图表展示了一个由几个神经元组成的非常简单的神经网络:

神经网络图

这是一个稍微复杂一些,或者说更深层次的网络:

神经网络训练图

神经网络训练

现在我们已经知道了神经网络和神经元是什么,我们应该谈谈它们的功能以及它们是如何工作的。神经网络是如何学习的?那些已经有孩子的你们已经知道这个答案了。如果你想让孩子知道猫是什么,你会怎么做?你给他们看猫(图片或真实的)。如果你想让孩子知道狗是什么,就给他们看狗。从概念上讲,神经网络并没有什么不同。它有一种学习规则,会修改从输入层传入的权重,通过隐藏层进行处理,然后通过激活函数,并希望能够在我们的案例中识别出猫和狗。而且,如果做得正确,猫不会变成狗!

神经网络中最常见的学习规则之一是所谓的delta 规则。这是一个监督规则,每次网络被呈现另一个学习模式时都会被调用。每次发生这种情况时,它被称为周期时代。规则的调用将在输入模式通过一个或多个正向传播层,然后通过一个或多个反向传播层时发生。

更简单地说,当神经网络被呈现一个图像时,它会试图确定可能的答案。正确答案和我们的猜测之间的差异是误差误差率。我们的目标是使误差率最小化或最大化。在最小化的情况下,我们需要误差率尽可能接近 0,对于每一个猜测。我们越接近 0,就越接近成功。

随着我们不断前进,我们进行所谓的梯度下降,这意味着我们继续朝着被称为全局最小值的方向前进,这是我们的最低可能的误差,希望这对成功至关重要。我们朝着全局最小值下降。

一旦网络本身被训练,并且你满意,训练周期就可以结束,然后你可以进入测试周期。在测试周期中,只使用正向传播层。这个过程的结果将产生用于进一步分析的模型。同样,在测试期间不会发生反向传播。

神经网络视觉指南

在本节中,我可能需要输入成千上万的字来描述所有神经网络组合及其外观。然而,无论多少文字都不如下面的图表来得有效:

图片

经 Asimov Institute 许可重印,版权所有

来源:http://www.asimovinstitute.org/neural-network-zoo/

让我们谈谈前一个图中的一些更常见的网络:

  • 感知器:这是最简单的前馈神经网络,正如你所见,它不包含任何隐藏层:

图片

  • 前馈网络:这种网络可能是设计得最简单的人工神经网络。它包含多个按排列的神经元(节点)。相邻层的节点之间有连接。每个连接都与权重相关联:

  • 循环神经网络(RNN):RNN 被称为循环,因为它们对序列中的每个元素执行相同的任务,输出取决于之前的计算。它们还能够回顾之前的步骤,这形成了一种短期记忆

神经网络在当今企业中的作用

作为开发者,我们主要关心的是如何将我们正在学习的内容应用到现实世界的场景中。更具体地说,在企业环境中,使用神经网络的机遇有哪些?以下是一些(许多)神经网络应用想法:

  • 在一个变量之间的关系不为人知的场景中

  • 在一个关系难以描述的场景中

  • 在一个目标是发现数据中不规则模式的场景中

  • 对数据进行分类以识别动物、车辆等模式

  • 信号处理

  • 图像识别(情感、观点、年龄、性别等)

  • 文本翻译

  • 手写识别

  • 自动驾驶汽车

  • 以及更多!

学习类型

既然我们谈到了我们的神经网络学习,让我们简要地谈谈你应该了解的三种不同类型的学习。它们是监督无监督强化

监督学习

如果你有一个与已知结果匹配的大型测试数据集,那么监督学习可能是一个不错的选择。神经网络将处理数据集;将其输出与已知结果进行比较,调整,然后重复。很简单,对吧?

无监督学习

如果你没有测试数据,并且能够从数据的行为中推导出某种成本函数,那么无监督学习可能是一个不错的选择。神经网络将处理数据集,使用成本函数来告知错误率,调整参数,然后重复。所有这些都在实时进行!

强化学习

我们最后一种学习类型是强化学习,在某些圈子中更广为人知的是胡萝卜加大棒。神经网络将处理数据集,从数据中学习,如果我们的错误率下降,我们就得到胡萝卜。如果错误率上升,我们就得到大棒。说得够多了,对吧?

理解感知器

我们将要处理的最基本元素被称为神经元。如果我们考虑神经元将使用的最基本形式的激活函数,我们将得到一个只有两种可能结果的函数,1 和 0。从视觉上看,这样的函数将表示如下:

这个函数如果输入是正的或 0,则返回 1,否则返回 0。具有这种激活函数的神经元被称为感知器。这是我们能够开发的最简单的神经网络形式。从视觉上看,它看起来如下:

图片

感知器遵循前馈模型,这意味着输入被发送到神经元,处理,然后产生输出。输入进来,输出出去。让我们用一个例子来说明。

假设我们有一个具有两个输入的单个感知器,如图所示。为了本例的目的,输入 0 将是 x1,输入 1 将是 x2。如果我们分配这两个变量值,它们将类似于以下内容:

输入 0: x1 = 12

输入 1: x2 = 4

每个输入都必须加权,也就是说,乘以某个值,这个值通常在-1 和 1 之间。当我们创建我们的感知器时,我们首先给它们分配随机权重。例如,输入 0(x1)将有一个我们将其标记为w1的权重,而输入 1(x2)将有一个我们将其标记为w2的权重。据此,以下是这个感知器的权重:

权重 0: 0.5 权重 1: -1

图片

一旦输入被加权,现在需要将它们相加。使用之前的例子,我们将有如下内容:

6 + -4 = 2

这个总和然后将通过一个激活函数,我们将在后面的章节中更详细地介绍。这将生成感知器的输出。激活函数将最终告诉感知器它是否可以触发,也就是说,激活。

现在,对于我们的激活函数,我们将只使用一个非常简单的函数。如果总和是正的,输出将是 1。如果总和是负的,输出将是-1。这不能再简单了,对吧?

因此,在伪代码中,我们单个感知器的算法如下所示:

  • 对于每个输入,将输入乘以它的权重

  • 求所有加权输入的总和

  • 根据激活函数(即总和的符号)计算感知器基于该总和的输出

这有用吗?

是的,事实上是有用的,让我们来展示一下。考虑一个输入向量作为点的坐标。对于一个具有n个元素的向量,点将位于一个 n 维空间中。取一张纸,在这张纸上画一组点。现在用一条直线将这两个点分开。你的纸张现在应该看起来像以下这样:

图片

如您所见,点现在被分为两组,一组在直线的每一侧。如果我们能用一条直线清楚地分离所有点,那么这两组就是所谓的线性可分。

相信或不相信,我们的单个感知器将能够学习这条线的位置,当你的程序完成时,感知器也将能够判断一个点是在线的上方还是下方(或者根据线的绘制方式,在左侧或右侧)。

让我们快速编写一个Perceptron类,以便让那些喜欢阅读代码而不是文字的你们(像我一样)更清晰地理解!目标是创建一个简单的感知器,可以确定一个点应该在直线的哪一侧,就像之前的图示一样:

class Perceptron {

float[] weights;

构造函数可以接收一个参数,表示输入的数量(在这种情况下是三个:xy和偏置),并相应地调整数组的大小:

Perceptron(int n) {
    weights = new float[n];
    for (int i = 0; i<weights.length; i++) {

初始时,weights是随机选择的:

      weights[i] = random(-1,1);
    }
}

接下来,我们需要为感知器编写一个接收其信息的函数,其长度将与权重数组相同,然后返回输出值给我们。我们将称之为feedforward

int feedforward(float[] inputs) {
    float sum = 0;
    for (int i = 0; i<weights.length; i++) {
      sum += inputs[i]*weights[i];
    }

结果是总和的符号,它将是-1 或+1。在这种情况下,感知器正在尝试猜测输出应该在直线的哪一侧:

 return activate(sum);
 }

到目前为止,我们有一个功能最基本但应该能够做出有根据的猜测的感知器。

创建Perceptron

Perceptron p = new Perceptron(3);

输入是 3 个值:xy和偏置:

float[] point = {5,-2,19};

获得答案:

int result = p.feedforward(point);

使我们的感知器更有价值的唯一事情是能够训练它而不是让它做出有根据的猜测。我们通过创建一个train函数来实现这一点,如下所示:

  1. 我们将引入一个新变量来控制学习率:
float c = 0.01;
  1. 我们还将提供输入和已知答案:
void train(float[] inputs, int desired) {
  1. 根据提供的输入,我们将做出一个有根据的猜测:
  int guess = feedforward(inputs);
  1. 我们将计算error,即答案和我们的guess之间的差异:
float error = desired - guess;
  1. 最后,我们将根据误差和学习常数调整所有权重:
  for (int i = 0; i<weights.length; i++) {
    weights[i] += c * error * inputs[i];

因此,现在你已经知道并看到了感知器是什么,让我们加入激活函数,并将其提升到下一个层次!

理解激活函数

激活函数被添加到神经网络的输出端,以确定输出。它通常将结果值映射到-1 到 1 的范围内的某个地方,具体取决于函数。它最终用于确定神经元是否会触发激活,就像灯泡打开或关闭一样。

激活函数是网络输出前的最后一部分,可以被认为是输出值的供应商。可以使用许多种激活函数,此图仅突出显示这些函数的一小部分:

图片

激活函数有两种类型——线性和非线性:

  • 线性:线性函数是位于或几乎位于直线上的函数,如图所示:

图片

  • 非线性:非线性函数是指那些不在直线上的函数,如图中所示:

可视化激活函数绘图

在处理激活函数时,重要的是在使用之前,您能够直观地理解激活函数的形状。我们将为您绘制并基准测试几个激活函数,以便您可以看到:

这就是逻辑阶跃近似和 Swish 激活函数单独绘制时的样子。由于存在许多类型的激活函数,以下展示了当它们一起绘制时,所有我们的激活函数将呈现的样子:

注意:您可以从 GitHub 上的 SharpNeat 项目下载生成之前输出的程序github.com/colgreen/sharpneat

到目前为止,您可能想知道我们为什么关心这些图的样子——这是一个很好的问题。我们关心,因为一旦您进入实际操作经验,深入神经网络时,您将大量使用这些。知道您的激活函数将把神经元的值置于开启或关闭状态,以及它将保持或需要的值范围是非常有用的。毫无疑问,您作为机器学习开发者,在职业生涯中会遇到并/或使用激活函数,了解 Tanh 和 LeakyRelu 激活函数之间的区别非常重要。

函数绘图

在这个例子中,我们将使用开源包SharpNeat。它是最强大的机器学习平台之一,并且它包含一个特殊的激活函数绘图器。您可以在github.com/colgreen/sharpneat找到 SharpNeat 的最新版本。在这个例子中,我们将使用如图所示包含的ActivationFunctionViewer项目:

一旦打开该项目,搜索PlotAllFunctions函数。正是这个函数处理了之前展示的所有激活函数的绘图。让我们详细了解一下这个函数:

private void PlotAllFunctions()
{
    Clear everything out.
    MasterPane master = zed.MasterPane;
    master.PaneList.Clear();
    master.Title.IsVisible = true;
    master.Margin.All = 10;

    Here is the section that will plot each individual function.
    PlotOnMasterPane(Functions.LogisticApproximantSteep, "Logistic 
    Steep (Approximant)");

    PlotOnMasterPane(Functions.LogisticFunctionSteep, "Logistic Steep 
    (Function)");

    PlotOnMasterPane(Functions.SoftSign, "Soft Sign");

    PlotOnMasterPane(Functions.PolynomialApproximant, "Polynomial 
    Approximant");

    PlotOnMasterPane(Functions.QuadraticSigmoid, "Quadratic Sigmoid");

    PlotOnMasterPane(Functions.ReLU, "ReLU");

    PlotOnMasterPane(Functions.LeakyReLU, "Leaky ReLU");

    PlotOnMasterPane(Functions.LeakyReLUShifted, "Leaky ReLU 
    (Shifted)");

    PlotOnMasterPane(Functions.SReLU, "S-Shaped ReLU");

    PlotOnMasterPane(Functions.SReLUShifted, "S-Shaped ReLU 
    (Shifted)");

    PlotOnMasterPane(Functions.ArcTan, "ArcTan");

    PlotOnMasterPane(Functions.TanH, "TanH");

    PlotOnMasterPane(Functions.ArcSinH, "ArcSinH");

    PlotOnMasterPane(Functions.ScaledELU, "Scaled Exponential Linear 
    Unit");

    Reconfigure the Axis
    zed.AxisChange();

    Layout the graph panes using a default layout
    using (Graphics g = this.CreateGraphics()) 
    {
        master.SetLayout(g, PaneLayout.SquareColPreferred);
    }

    MainPlot Function

    Behind the scenes, the ‘Plot' function is what is responsible for 
    executing     and plotting each function.

    private void Plot(Func<double, double> fn, string fnName, Color 
    graphColor, GraphPane gpane = null)
    {
        const double xmin = -2.0;
        const double xmax = 2.0;
        const int resolution = 2000;
        zed.IsShowPointValues = true;
        zed.PointValueFormat = "e";

        var pane = gpane ?? zed.GraphPane;
        pane.XAxis.MajorGrid.IsVisible = true;
        pane.YAxis.MajorGrid.IsVisible = true;
        pane.Title.Text = fnName;
        pane.YAxis.Title.Text = string.Empty;
        pane.XAxis.Title.Text = string.Empty;

        double[] xarr = new double[resolution];
        double[] yarr = new double[resolution];
        double incr = (xmax - xmin) / resolution;
        doublex = xmin;

        for(int i=0; i<resolution; i++, x += incr)
        {
            xarr[i] = x;
            yarr[i] = fn(x);
        }

        PointPairList list1 = new PointPairList(xarr, yarr);
        LineItem li = pane.AddCurve(string.Empty, list1, graphColor, 
        SymbolType.None);
        li.Symbol.Fill = new Fill(Color.White);
        pane.Chart.Fill = new Fill(Color.White, 
        Color.LightGoldenrodYellow, 45.0F);
}

早期代码中引起关注的主要点用黄色突出显示。这就是我们传递的激活函数被执行并用于y轴绘图值的地方。著名的开源绘图包ZedGraph用于所有图形绘图。一旦每个函数执行完毕,相应的绘图将被制作。

理解反向传播

反向传播,即误差的反向传播,是一种使用梯度下降进行神经网络监督学习的算法。它计算了所谓的误差函数的梯度,相对于网络的权重。它是感知器从单层到多层前馈神经网络的 delta 规则的一般形式。

与前向传播不同,反向传播通过在网络中向后移动来计算梯度。首先计算权重最后一层的梯度,然后最后计算第一层的梯度。随着深度学习在图像和语音识别中的最近流行,反向传播再次成为焦点。就其目的而言,它是一个高效的算法,今天的版本利用 GPU 来进一步提高性能。

最后,因为反向传播的计算依赖于前向阶段(包括隐藏层在内的所有层的非误差项)的激活和输出,所以所有这些值必须在反向阶段开始之前被计算。因此,对于梯度下降的每一次迭代,前向阶段必须先于反向阶段进行。

前向和反向传播的区别

让我们花一点时间来澄清前向传播和反向传播之间的区别。一旦你理解了这个,你就可以更好地可视化和理解整个神经网络的流程。

在神经网络中,你通过前向传播数据以获取输出,然后将其与实际预期的值进行比较以获得误差,这个误差是数据应该是什么与你的机器学习算法实际认为它是什么之间的差异。为了最小化这个误差,你现在必须通过找到误差相对于每个权重的导数来反向传播,然后从这个权重本身减去这个值。

在神经网络中进行的基学习是训练神经元何时被激活、何时放电以及何时处于开启关闭状态。每个神经元应该只为某些类型的输入激活,而不是所有输入。因此,通过前向传播,你可以看到你的神经网络表现如何以及找到误差。在你了解到你的网络误差率后,你进行反向传播并使用梯度下降的形式来更新权重的新的值。再次,你将数据前向传播以查看这些权重表现如何,然后反向传播数据以更新权重。这将一直持续到达到误差值的某个最小值(希望是全球最小值而不是局部最小值)。再次,重复这个过程!

摘要

在本章中,我们简要概述了各种神经网络术语。我们回顾了感知器、神经元和反向传播等内容。在我们下一章中,我们将直接进入编写一个完整的神经网络!

我们将涵盖诸如神经网络训练、术语、突触、神经元、前向传播、反向传播、Sigmoid 函数、反向传播以及错误计算等主题。

所以,请系好你的帽子;代码即将到来!

参考文献

第七章:一起构建我们的第一个神经网络

现在我们对神经网络有了快速的复习,我认为从代码的角度来看,一个好的起点可能是我们编写一个非常简单的神经网络。我们不会做得太过分;我们只是为几个函数搭建基本框架,这样你就可以了解许多 API 背后的场景。从头到尾,我们将开发这个网络应用程序,以便你熟悉神经网络中包含的所有基本组件。这个实现并不完美或全面,也不是这个意思。正如我提到的,这仅仅提供了一个框架,我们可以在本书的其余部分中使用。这是一个非常基本的神经网络,增加了保存和加载网络和数据的功能。无论如何,你将有一个基础,从那里你可以编写自己的神经网络,并改变世界,如果你愿意的话。

在本章中,我们将涵盖以下主题:

  • 神经网络训练

  • 术语

  • 突触

  • 神经元

  • 前向传播

  • Sigmoid 函数

  • 反向传播

  • 错误计算

技术要求

你需要在系统上安装 Microsoft Visual Studio。

查看以下视频以查看代码的实际应用:bit.ly/2NYJa5G

我们的神经网络

让我们先向您展示一个简单的神经网络的外观,从视觉上。它由一个包含2个输入的输入层、一个包含3个神经元(有时称为节点)的隐藏层以及一个由单个神经元组成的最终输出层组成。当然,神经网络可以包含更多层(以及每层的神经元),一旦你进入深度学习,你会看到更多这样的例子,但就目前而言,这已经足够了。记住,每个节点,用N标记,都是一个单独的神经元——它自己的小处理大脑,如果你愿意这样想的话:

图片

让我们将神经网络分解为其三个基本部分:输入、隐藏层和输出:

输入:这是我们的网络的初始数据。每个输入都是一个值,其输出到隐藏层的值是初始输入值。

隐藏层:这是我们网络的灵魂和核心,所有的魔法都发生在这里。这个层中的神经元为它们的每个输入分配权重。这些权重最初是随机的,并在网络训练过程中进行调整,以便神经元的输出更接近预期的结果(如果我们幸运的话)。

输出:这些是我们网络在执行计算后得到的结果。在我们的简单案例中,输出将是真或假,开或关。神经元为它们的每个输入分配一个权重,这些权重来自前一个隐藏层。尽管通常只有一个输出神经元是常见的,但如果需要或想要更多,你完全可以拥有更多。

神经网络训练

我们如何训练神经网络?基本上,我们将提供一组输入数据以及我们期望看到的结果,这些结果与输入相对应。然后,这些数据将通过网络运行,直到网络理解我们在寻找什么。我们将进行训练、测试、训练、测试、训练、测试,如此循环,直到我们的网络理解我们的数据(或者不理解,但这又是另一个话题)。我们继续这样做,直到满足某个指定的停止条件,例如错误率阈值。让我们快速了解一下我们在训练神经网络时将使用的术语。

反向传播:在数据通过网络运行后,我们需要验证这些数据,看看我们期望得到的是正确的输出。我们通过将数据反向传播(因此称为反向传播或反向传播)通过网络的每个隐藏层来实现这一点。最终结果是调整分配给隐藏层中每个神经元输入的权重以及我们的错误率。

在理想情况下,每个反向传播层都应该使我们的网络输出更接近我们期望的值,我们的错误率将越来越接近 0。我们可能永远无法达到精确的错误率为 0,所以尽管这可能看起来差别不大,但错误率为 0.0000001 可能对我们来说已经足够可接受。

偏置:偏置允许我们修改我们的函数,以便我们可以为网络中的每个神经元生成更好的输出。简而言之,偏置允许我们将激活函数的值向左或向右移动。改变权重会改变 Sigmoid 的陡峭程度或垂直方面。

动量:动量简单地将前一次权重更新的部分加到当前更新上。动量用于防止系统收敛到局部最小值而不是全局最小值。高动量可以用来帮助增加系统的收敛速度;然而,你必须小心,因为设置此参数过高可能导致超过最小值,这将导致系统不稳定。另一方面,动量太低可能无法可靠地避免局部最小值,它也可能真的减慢系统的训练速度。因此,正确获取此值对于成功至关重要,你将花费相当多的时间来做这件事。

Sigmoid 函数:激活函数定义了每个神经元的输出。Sigmoid 函数可能是最常用的激活函数。它将输入转换为介于 0 和 1 之间的值。此函数用于生成我们的初始权重。典型的 Sigmoid 函数能够接受一个输入值,并从这个值提供输出值和导数。

学习率:学习率通过控制网络在学习阶段对权重和偏置所做的变化大小,从而改变系统的整体学习速度。

现在我们有了这些术语,让我们开始深入代码。你应该已经下载了书中提供的配套软件的解决方案,并在 Visual Studio 中打开它。我们使用 Visual Studio 的社区版,但你也可以使用你有的任何版本。

随意下载软件,实验它,如果你需要或想要的话,还可以对其进行美化。在你的世界里,你的神经网络可以是任何你喜欢或需要的样子,所以让它发生吧。你有源代码。仅仅因为你看某件事的方式不一定是真理或刻在石头上的!从这些伟大的开源贡献者为我们提供的东西中学习!记住,这个神经网络旨在给你一些想法,关于你可以做很多事情,同时也会教你一些神经网络的基础知识。

让我们从一些简短的代码片段开始,这些代码片段将为本章的其余部分奠定基础。我们首先从一个叫做突触的小东西开始,它连接一个神经元到另一个。接下来,我们将开始编写单个神经元的代码,最后讨论前向和反向传播以及这对我们意味着什么。我们将以代码片段的形式展示一切,以便更容易理解。

突触

你可能会问,什么是突触?简单地说,它连接一个神经元到另一个,同时也是一个容器,用来存放权重和权重增量值,如下所示:

public class Synapse
{
    public Guid Id{ get; set; }
    public Neuron InputNeuron{ get; set; }    
    public Neuron OutputNeuron{ get; set; }
    public double Weight{ get; set; }
    public double WeightDelta{ get; set; }
}

神经元

我们已经讨论了什么是神经元,现在该用代码来表达了,这样像我们这样的开发者就能更好地理解它了!正如你所见,我们既有输入和输出突触,还有偏置偏置增量梯度以及神经元的实际值。神经元计算其输入的加权和,加上偏置,然后决定输出是否应该'fire' - '开启'

public class Neuron
{
  public Guid Id{ get; set; }

The synapse that connects to our input side
  public List<Synapse> InputSynapses{ get; set; }

The synapse that connects to our output side
  public List<Synapse> OutputSynapses{ get; set; }
  public double Bias{ get; set; }

Our bias values
  public double BiasDelta{ get; set; }
  public double Gradient{ get; set; }

The input value to the neuron
  public double Value{ get; set; }

Is the neuron a mirror neuron
public bool IsMirror{ get; set; }

Is the neuron a canonical neuron
public bool IsCanonical{ get; set; }

}

前向传播

下面是我们基本前向传播过程的代码:

private void ForwardPropagate(params double[] inputs)
{
  var i = 0;
  InputLayer?.ForEach(a =>a.Value = inputs[i++]);
  HiddenLayers?.ForEach(a =>a.ForEach(b =>b.CalculateValue()));
  OutputLayer?.ForEach(a =>a.CalculateValue());
}

要进行ForwardPropagation,我们基本上将每个突触的所有输入相加,然后将结果通过 Sigmoid 函数来得到输出。CalculateValue函数为我们做这件事。

Sigmoid 函数

Sigmoid 函数是一个激活函数,正如我们之前所提到的,可能是今天最广泛使用的之一。下面是一个 Sigmoid 函数的样子(你记得我们关于激活函数的部分,对吧?)它的唯一目的(非常抽象地)是将外部边缘的值拉近 0 和 1,而不用担心值会大于这个。这将防止边缘上的值离我们而去:

图片

你可能会问,C#代码中的 Sigmoid 函数是什么样的?就像下面的这样:

public static class Sigmoid
{
  public static double Output(double x)
  {
    return x < -45.0 ?0.0 : x > 45.0 ? 1.0 : 1.0 / (1.0 + Math.Exp(-x));
  }

  public static double Derivative(double x)
  {
    return x * (1 - x);
  }
}

我们的Sigmoid类将产生输出和导数。

反向传播

对于反向传播(backprop),我们首先从输出层计算梯度,将这些值通过隐藏层(反转我们在正向传播中采取的方向),更新权重,最后将这些值通过输出层,如下所示:

private void BackPropagate(params double[] targets)
{
  var i = 0;
  OutputLayer?.ForEach(a =>a.CalculateGradient(targets[i++]));
  HiddenLayers?.Reverse();
  HiddenLayers?.ForEach(a =>a.ForEach(b =>b.CalculateGradient()));
  HiddenLayers?.ForEach(a =>a.ForEach(b =>b.UpdateWeights(LearningRate, Momentum)));
  HiddenLayers?.Reverse();
  OutputLayer?.ForEach(a =>a.UpdateWeights(LearningRate, Momentum));
}

计算误差

为了计算我们的误差,我们从实际值中减去预期值。我们越接近 0,效果越好。请注意,我们几乎不可能达到 0,尽管理论上可能发生这种情况:

public double CalculateError(double target)
{
  return target - Value;
}

计算梯度

梯度是通过考虑Sigmoid函数的导数来计算的:

public double CalculateGradient(double? target = null)
{
  if (target == null)
    return Gradient = OutputSynapses.Sum(a =>a.OutputNeuron.Gradient * 
    a.Weight) * Sigmoid.Derivative(Value);

  return Gradient = CalculateError(target.Value) * Sigmoid.Derivative(Value);
}

更新权重

我们通过将学习率乘以我们的梯度,然后加上动量并乘以之前的 delta 来更新权重。然后通过每个输入突触运行以计算最终值:

public void UpdateWeights(double learnRate, double momentum)
{
  var prevDelta = BiasDelta;
  BiasDelta = learnRate * Gradient;
  Bias += BiasDelta + momentum * prevDelta;

  foreach (var synapse in InputSynapses)
  {
    prevDelta = synapse.WeightDelta;
    synapse.WeightDelta = learnRate * Gradient * synapse.InputNeuron.Value;
        synapse.Weight += synapse.WeightDelta + momentum * prevDelta;
  }
}

计算值

为了计算值,我们从Sigmoid函数的输出中取出,并加上偏置项:

public virtual double CalculateValue()
{
  return Value = Sigmoid.Output(InputSynapses.Sum(a =>a.Weight * 
  a.InputNeuron.Value) + Bias);
}

神经网络函数

以下基本列表包含我们将开发的功能,以便建立我们的神经网络基础:

  • 创建新的网络

  • 导入网络

  • 手动输入用户数据

  • 导入数据集

  • 训练我们的网络

  • 测试我们的网络

在此基础上,让我们开始编码!

创建一个新的网络

此菜单选项将允许我们从零开始创建一个新的网络:

public NNManager SetupNetwork()
{
    _numInputParameters = 2;

    int[] hidden = new int[2];
    hidden[0] = 3;
    hidden[1] = 1;
    _numHiddenLayers = 1;
    _hiddenNeurons = hidden;
    _numOutputParameters = 1;
    _network = new Network(_numInputParameters, _hiddenNeurons,         
    _numOutputParameters);
    return this;
}

注意这个函数中的返回值。这是一个流畅的接口,意味着可以将各种函数链接到单个语句中。许多人更喜欢这种接口而不是传统的接口,但您可以随意修改代码。以下是一个流畅接口的示例。信不信由你,这是一个完整的神经网络:

NNManagermgr = new NNManager();
Mgr
.SetupNetwork()
.GetTrainingDataFromUser()
.TrainNetworkToMinimum()
.TestNetwork();

导入现有网络

此功能将允许我们导入之前保存的网络。再次注意返回值,这使得它成为一个流畅的接口:

public static Network ImportNetwork()
{

获取之前保存的网络文件名。一旦打开,将其反序列化为我们将处理的网络结构。(如果由于某种原因不起作用,则中止!):

  var dn = GetHelperNetwork();
  if (dn == null) 
  return null;

创建一个新的Network和一个神经元列表来填充:

var network = new Network();
  var allNeurons = new List<Neuron>();

复制之前保存的学习率和动量:

network.LearningRate = dn.LearningRate;
  network.Momentum = dn.Momentum;

从我们导入的网络数据中创建输入层:

foreach (var n in dn.InputLayer)
  {
    var neuron = new Neuron
    {
      Id = n.Id,
      Bias = n.Bias,
      BiasDelta = n.BiasDelta,
      Gradient = n.Gradient,
      Value = n.Value
    };

    network.InputLayer?.Add(neuron);
    allNeurons.Add(neuron);
  }

从我们导入的网络数据中创建我们的隐藏层:

  foreach (var layer in dn.HiddenLayers)
  {
    var neurons = new List<Neuron>();
    foreach (var n in layer)
    {
      var neuron = new Neuron
      {
        Id = n.Id,
        Bias = n.Bias,
        BiasDelta = n.BiasDelta,
        Gradient = n.Gradient,
        Value = n.Value
      };

      neurons.Add(neuron);
      allNeurons.Add(neuron);
    }
    network.HiddenLayers?.Add(neurons);
  }

从我们导入的数据中创建OutputLayer神经元:

foreach (var n in dn.OutputLayer)
  {
    var neuron = new Neuron
    {
      Id = n.Id,
      Bias = n.Bias,
      BiasDelta = n.BiasDelta,
      Gradient = n.Gradient,
      Value = n.Value
    };

    network.OutputLayer?.Add(neuron);
    allNeurons.Add(neuron);
  }

最后,创建将所有内容连接起来的突触:


  foreach (var syn in dn.Synapses)
  {
    var synapse = new Synapse{ Id = syn.Id };
    var inputNeuron = allNeurons.First(x =>x.Id==syn.InputNeuronId);
    var outputNeuron = allNeurons.First(x =>x.Id==syn.OutputNeuronId);
    synapse.InputNeuron = inputNeuron;
    synapse.OutputNeuron = outputNeuron;
    synapse.Weight = syn.Weight;
    synapse.WeightDelta = syn.WeightDelta;

    inputNeuron?.OutputSynapses?.Add(synapse);
    outputNeuron?.InputSynapses?.Add(synapse);
  }
  return network;
}

以下是我们手动输入网络将使用的数据:

public NNManager GetTrainingDataFromUser()
{
var numDataSets = GetInput("\tHow many datasets are you going to enter? ", 1, int.MaxValue);

  var newDatasets = new List<NNDataSet>();
  for (var i = 0; i<numDataSets; i++)
  {
    var values = GetInputData($"\tData Set {i + 1}: ");
    if (values == null)
    {
      return this;
    }

    var expectedResult = GetExpectedResult($"\tExpected Result for Data 
    Set {i + 1}: ");
    if (expectedResult == null)
    {
      return this;
    }

    newDatasets.Add(newNNDataSet(values, expectedResult));
  }

  _dataSets = newDatasets;
  return this;
}

导入数据集

以下是我们如何处理数据集:

public static List<DataSet>ImportDatasets()
{
  var dialog = new OpenFileDialog
  {
    Multiselect = false,
    Title = "Open Dataset File",
    Filter = "Text File|*.txt;"
  };

  using (dialog)
  {
    if (dialog.ShowDialog() != DialogResult.OK) 
    return null;

    using (var file = File.OpenText(dialog.FileName))
    {

反序列化数据并返回它:

      return JsonConvert.DeserializeObject<List<DataSet>>(file.ReadToEnd());
    }
  }
}

测试网络

为了测试网络,我们需要进行简单的正向和反向传播,如下所示:

public double[] Compute(params double[] inputs)
{

按如下方式进行正向传播:

  ForwardPropagate(inputs);

按如下方式返回数据:

  return OutputLayer.Select(a =>a.Value).ToArray();
}

导出网络

按如下方式导出当前网络信息:

public NNManager ExportNetwork()
{
  Console.WriteLine("\tExporting Network...");
  ExportHelper.ExportNetwork(_network);
  Console.WriteLine("\t**Exporting Complete!**", Color.Green);
  return this;
}

训练网络

训练网络有两种方式。一种是将错误值降至最小,另一种是将错误值升至最大。这两个函数都有默认值,尽管你可能希望为你的训练设置不同的阈值,如下所示:

public NNManager TrainNetworkToMinimum()
{
var minError = GetDouble("\tMinimum Error: ", 0.000000001, 1.0);
Console.WriteLine("\tTraining...");
_network.Train(_dataSets, minError);
Console.WriteLine("\t**Training Complete**", Color.Green);
return this;
}

public NNManager TrainNetworkToMaximum()
{
varmaxEpoch = GetInput("\tMax Epoch: ", 1, int.MaxValue);
if(!maxEpoch.HasValue)
       {
  return this;
       }

Console.WriteLine("\tTraining...");
_network.Train(_dataSets, maxEpoch.Value);
Console.WriteLine("\t**Training Complete**", Color.Green);
return this;
}

在上述两个函数定义中,神经网络Train函数被调用以执行实际训练。此函数随后在训练循环的每次迭代中为每个数据集调用前向和反向传播函数,如下所示:

public void Train(List<DataSet>dataSets, int numEpochs)
{
  for (var i = 0; i<numEpochs; i++)
  {
    foreach (var dataSet in dataSets)
    {
      ForwardPropagate(dataSet.Values);
      BackPropagate(dataSet.Targets);
    }
  }
}

测试网络

此函数允许我们测试我们的网络。再次注意返回值,这使得它成为一个流畅的接口。对于在更高、更抽象层上最常用的函数,我尽量在最有益的地方提供流畅的接口,如下所示:

public NNManager TestNetwork()
{
Console.WriteLine("\tTesting Network", Color.Yellow);

  while (true)
  {

从用户处获取输入数据,如下所示:


    var values = GetInputData($"\tType{_numInputParameters} inputs: ");
    if (values == null)
    {
      return this;
    }

进行计算,如下所示:

    var results = _network?.Compute(values);

打印结果,如下所示:

    foreach (var result in results)
    {
    Console.WriteLine("\tOutput: " + 
    DoubleConverter.ToExactString(result), Color.Aqua);
    }

    return this;
  }
}

计算前向传播

此函数是根据提供的值Compute前向传播值的,如下所示:

public double[] Compute(params double[] inputs)
{
  ForwardPropagate(inputs);
  return OutputLayer.Select(a =>a.Value).ToArray();
}

导出网络

此函数是我们导出网络的地方。对我们来说,导出意味着将数据序列化为 JSON 可读格式,如下所示:

public NNManager ExportNetwork()
{
  Console.WriteLine("\tExporting Network...");
  ExportHelper.ExportNetwork(_network);
  Console.WriteLine("\t**Exporting Complete!**", Color.Green);
  return this;
}

导出数据集

此函数是我们导出数据集信息的地方。与导出网络一样,这将以 JSON 可读格式完成:

public NNManager ExportDatasets()
{
      Console.WriteLine("\tExporting Datasets...");
      ExportHelper.ExportDatasets(_dataSets);
      Console.WriteLine("\t**Exporting Complete!**", Color.Green);
      return this;
}

神经网络

在编写了许多辅助但重要的函数之后,我们现在将注意力转向神经网络的核心,即网络本身。在神经网络中,网络部分是一个包罗万象的宇宙。一切都在其中。在这个结构中,我们需要存储输入、输出和隐藏层的神经元,以及学习率和动量,如下所示:

public class Network
{
      public double LearningRate{ get; set; }
      public double Momentum{ get; set; }
      public List<Neuron>InputLayer{ get; set; }
      public List<List<Neuron>>HiddenLayers{ get; set; }
      public List<Neuron>OutputLayer{ get; set; }
      public List<Neuron>MirrorLayer {get; set; }
      public List<Neuron>CanonicalLayer{ get; set; }

神经元连接

每个神经元都必须连接到其他神经元,我们的神经元构造函数将处理将所有输入神经元与突触连接,如下所示:

public Neuron(IEnumerable<Neuron> inputNeurons) : this()
{
Ensure.That(inputNeurons).IsNotNull();

  foreach (var inputNeuron in inputNeurons)
  {
    var synapse = new Synapse(inputNeuron, this);
    inputNeuron?.OutputSynapses?.Add(synapse);
    InputSynapses?.Add(synapse);
  }
}

示例

现在我们已经创建了代码,让我们用几个例子来看看它如何使用。

训练至最小值

在此示例中,我们将使用我们编写的代码来训练一个网络以达到最小值或阈值。对于每个步骤,网络都会提示你输入正确数据,从而避免在我们的示例代码中添加这些数据。在生产环境中,你可能希望在不进行用户干预的情况下传递参数,以防这个程序作为服务或微服务运行:

图片

训练至最大值

在此示例中,我们将训练网络以达到最大值,而不是最小值,如图所示。我们手动输入我们希望处理的数据以及预期的结果。然后我们允许训练完成。一旦完成,我们输入测试输入并测试网络:

图片

概述

在本章中,我们学习了如何从头开始编写一个完整的神经网络。尽管我们省略了很多内容,但它涵盖了基础知识,并且我们已经看到了它作为纯 C#代码的样子!现在,我们应该比刚开始时对神经网络是什么以及它包含什么有更深入的理解。

在下一章中,我们将开始探索更复杂的网络结构,例如循环神经网络和卷积神经网络。内容很多,所以请系好你们的编程帽子!

第八章:决策树和随机森林

决策树和随机森林是强大的技术,您可以使用它们来增强您的应用程序。让我们浏览一些概念和一些代码,并希望您能迅速上手。

在本章中,我们将学习决策树和随机森林。我们将:

  • 通过大量的代码示例向您展示如何将此强大的功能添加到您的应用程序中

  • 讨论决策树

  • 讨论随机森林

技术要求

您的系统需要安装 Microsoft Visual Studio。您可能还需要参考开源 SharpLearning 框架的 GitHub 仓库,网址为 github.com/mdabros/SharpLearning

查看以下视频以查看代码的实际应用:bit.ly/2O1Lbhr

决策树

决策树可以用于分类和回归。决策树通过是/否、真/假的响应来回答连续问题。基于这些响应,树按照预定的路径达到其目标。从更正式的角度来看,树是已知的有向无环图的一种版本。最后,决策树是使用整个数据集和所有特征构建的。

这里是一个决策树的例子。您可能不知道它是一个决策树,但肯定知道这个过程。有人想要甜甜圈吗?

图片

如您所见,决策树流程从顶部开始,向下工作,直到达到特定的结果。树的根是第一个分割数据集的决定。树根据每个节点上所谓的分割度量递归地分割数据集。最流行的两个度量是基尼不纯度信息增益

这里是另一个决策树的表示,尽管没有那些美味的甜甜圈!

图片

决策树的深度表示到目前为止已经问过多少问题。这是树可以到达的最深程度(可能问的问题总数),即使使用更少的问题也可以得到一些结果。例如,使用前面的图,一些结果在 1 个问题后就可以得到,一些在 2 个问题后。因此,该决策树的深度为 2。

决策树优点

使用决策树的一些优点如下:

  • 容易解释。

  • 直观、易于理解的可视化。

  • 可以轻松复制。

  • 可以处理数值和分类数据。

  • 在非常大的数据集上表现良好。

  • 通常运行非常快。

  • 在深度上,树的位置允许轻松可视化哪些特征很重要。重要性由树的深度表示。

决策树缺点

使用决策树的一些缺点如下:

  • 在每个节点,算法需要确定正确的选择。一个节点的最佳选择可能并不一定是整棵树的最佳选择。

  • 如果树很深,它可能会倾向于所谓的过拟合。

  • 决策树可以记住训练集。

我们应该在什么情况下使用决策树?

以下是使用决策树的一些例子:

  • 当你想要一个简单且可解释的模型时

  • 当你的模型应该是非参数时

  • 当你不想担心特征选择时

随机森林

我们已经讨论了决策树,现在是时候讨论随机森林了。非常基本地说,随机森林是一系列决策树的集合。在随机森林中,随机选择了一部分总行数和特征数来训练。然后在这个子集上构建决策树。这个集合的结果将汇总成一个单一的结果。

随机森林还可以减少偏差和方差。它们是如何做到这一点的?通过在不同的数据样本上训练,或者通过使用特征的随机子集。让我们举一个例子。假设我们有 30 个特征。随机森林可能只使用其中的 10 个特征。这留下了 20 个未使用的特征,但其中一些 20 个特征可能很重要。记住,随机森林是一系列决策树的集合。因此,在每一棵树中,如果我们利用 10 个特征,随着时间的推移,大多数甚至所有我们的特征都会被包括在内,这仅仅是因为平均法则。所以,正是这种包含帮助我们限制由于偏差和方差引起的错误。

对于大型数据集,树的数量可能会非常大,有时达到数万甚至更多,这取决于你使用的特征数量,因此你需要注意性能。

这是一个随机森林可能看起来像的图示:

图片

随机森林的优点

以下是使用随机森林的一些优点:

  • 比单个决策树更稳健

  • 随机森林包含许多决策树,因此能够限制过拟合和错误

  • 在深度方向上,位置显示了哪些特征对分类或回归有贡献以及它们的相对重要性

  • 可以用于回归和分类

  • 默认参数可能足够

  • 训练速度快

随机森林的缺点

以下是使用随机森林的一些缺点:

  • 随机森林需要并行处理以提高速度

  • 一旦训练完成,预测可能创建得较慢

  • 更高的精度需要更多的树,这可能会导致模型变慢

我们应该在什么情况下使用随机森林?

以下是使用随机森林的一些例子:

  • 当模型解释不是最重要的标准时。解释可能不会像单棵树那样简单。

  • 当模型精度是最重要的。

  • 当你想要稳健的分类、回归和特征选择分析时。

  • 为了防止过拟合。

  • 图像分类。

  • 推荐引擎。

SharpLearning

现在,让我们将注意力转向一个令人难以置信的开源包,SharpLearning。SharpLearning 是一个出色的机器学习框架,个人可以从中了解许多机器学习的方面,包括我们在前几节中描述的决策树和随机森林。在我们深入研究代码示例和示例应用之前,让我们花几分钟时间熟悉一些事情。

术语

在本章中,您将看到以下术语的使用。以下是每个术语的含义背景:

  • 学习器:这指的是一个机器学习算法。

  • 模型:这指的是一个机器学习模型。

  • 超参数:这些是用于调整和调节(希望是)机器学习模型的参数。

  • 目标:这些更常见地被称为因变量。在大多数记号中,这将表示为 y。这些是我们试图建模的值。

  • 观测值:这些是特征矩阵,其中包含我们目前关于目标的所有信息。在大多数记号中,这将表示为 x

在我们的大多数示例中,我们将关注 SharpLearning 中的两个命名空间。它们是:

  • SharpLearning.DecisionTrees

  • SharpLearning.RandomForest

在此基础上,让我们开始深入研究 SharpLearning,向您展示一些与它的工作方式相关的基本概念。

加载和保存模型

SharpLearning 使将模型加载和保存到磁盘变得非常容易。这是机器学习库的一个重要部分,而 SharpLearning 是最容易实现的部分之一。

SharpLearning 中的所有模型都有一个 Save 和一个 Load 方法。这些方法为我们完成了保存和加载模型的繁重工作。

例如,这里我们将保存一个我们学习到的模型到磁盘:

model.Save(() => StreamWriter(@"C:\randomforest.xml"));

如果我们想重新加载此模型,我们只需使用 Load 方法:

varloadedModel = RegressionForestModel.Load(() => newStreamReader(@"C:\randomforest.xml"));

是的,加载和保存数据模型既简单又容易。您还可以使用序列化来保存模型。这将使我们能够在 XML 和二进制格式之间进行选择。SharpLearning 的另一个非常棒的设计特性是,序列化模型允许我们将其序列化为 IPredictorModel 接口。这使得如果每个模型都符合该接口,替换模型变得更加容易。以下是这样做的方法:

varxmlSerializer = new GenericXmlDataContractSerializer();
xmlSerializer.Serialize<IPredictorModel<double>>(model, 
 () => new StreamWriter(@"C:\randomforest.xml"));
var loadedModelXml = xmlSerializer
.Deserialize<IPredictorModel<double>>(() => new StreamReader(@"C:\randomforest.xml"));
算法 训练误差 测试误差
回归决策树学习器(默认) 0.0518 0.4037

如此一来,立即得到训练和测试误差。

当报告您模型的性能时,您应该始终使用测试误差,即使训练误差更低,因为那是模型对新数据泛化能力的估计。

现在,让我们谈谈超参数。超参数是影响机器学习算法学习过程的参数。你可以调整它们来调整过程并提高性能和可靠性。同时,你也可以错误地调整参数,得到一个不符合预期的工作结果。让我们看看一个错误调整的超参数可能会发生的一些事情:

  • 如果模型过于复杂,你可能会得到所谓的高方差,或称为过拟合

  • 如果模型最终变得过于简单,你将得到所谓的高偏差,或称为欠拟合

对于那些还没有这样做的人来说,手动调整超参数,这个过程几乎在每一个用例中都会发生,可能会占用你相当多的时间。随着模型超参数数量的增加,调整时间和努力也会增加。解决这个问题最好的办法是使用一个优化器,让它为你完成工作。为此,SharpLearning 可以为我们提供巨大的帮助,因为它提供了许多可用的优化器。以下是一些其中的例子:

  • 网格搜索

  • 随机搜索

  • 粒子群(我们将在第七章中讨论,用 PSO 替换反向传播

  • 贝叶斯优化

  • 全球化边界内尔德梅德

让我们从例子开始。

让我们创建一个学习器并使用默认参数,这些参数很可能已经足够好了。一旦我们找到参数并创建学习器,我们需要创建模型。然后我们将预测训练集和测试集。一旦所有这些都完成,我们将测量测试集上的误差并记录下来:

// create learner with default parameters
var learner = new RegressionSquareLossGradientBoostLearner(runParallel: false);
// learn model with found parameters
var model = learner.Learn(trainSet.Observations, trainSet.Targets);
// predict the training and test set.
var trainPredictions = model.Predict(trainSet.Observations);
var testPredictions = model.Predict(testSet.Observations);
// since this is a regression problem we are using square error as metric
// for evaluating how well the model performs.
var metric = new MeanSquaredErrorRegressionMetric();
// measure the error on the test set.
var testError = metric.Error(testSet.Targets, testPredictions);

这里是我们的测试集误差

算法 测试误差
回归平方损失梯度提升学习器(默认) 0.4984

那部分完成之后,我们现在已经建立了基线。让我们使用RandomSearchOptimizer来调整超参数,看看我们是否能得到更好的结果。为此,我们需要确定超参数的范围,这样我们的优化器就知道如何调整。让我们看看我们是如何做到这一点的:

var parameters = new ParameterBounds[]
{
 new ParameterBounds(min: 80, max: 300, 
 transform: Transform.Linear, parameterType: ParameterType.Discrete), 
 new ParameterBounds(min: 0.02, max: 0.2, 
 transform: Transform.Logarithmic, parameterType: ParameterType.Continuous), 
 new ParameterBounds(min: 8, max: 15, 
 transform: Transform.Linear, parameterType: ParameterType.Discrete), 
 new ParameterBounds(min: 0.5, max: 0.9, 
 transform: Transform.Linear, parameterType: ParameterType.Continuous), 
 new ParameterBounds(min: 1, max: numberOfFeatures, 
 transform: Transform.Linear, parameterType: ParameterType.Discrete), 
};

你注意到我们使用了学习率的对数变换吗?你知道为什么这样做吗?答案是:确保在整个值域上有一个更均匀的分布。我们的最小值和最大值之间有一个很大的范围差异(0.02 -> 0.2),因此对数变换将是最好的。

我们现在需要一个验证集来帮助我们衡量模型在优化过程中对未见数据的泛化能力。为此,我们需要进一步分割训练数据。为此,我们将把当前的测试集排除在优化过程之外。如果不这样做,我们可能会在最终的误差估计上产生正偏差,而这并不是我们想要的:

var validationSplit = new RandomTrainingTestIndexSplitter<double>(trainingPercentage: 0.7, seed: 24)
.SplitSet(trainSet.Observations, trainSet.Targets);

优化器还需要另一件事,那就是目标函数。该函数将接受一个 double 数组作为输入(包含超参数集)并返回一个OptimizerResult,其中包含验证错误和相应的超参数集:

Func<double[], OptimizerResult> minimize = p =>
 {
 var candidateLearner = new RegressionSquareLossGradientBoostLearner(
 iterations: (int)p[0],
learningRate: p[1], 
maximumTreeDepth: (int)p[2], 
subSampleRatio: p[3], 
featuresPrSplit: (int)p[4],
runParallel: false);
 var candidateModel = candidateLearner.Learn(validationSplit.TrainingSet.Observations,
validationSplit.TrainingSet.Targets);
 var validationPredictions = candidateModel.Predict(validationSplit.TestSet.Observations);
 var candidateError = metric.Error(validationSplit.TestSet.Targets, validationPredictions);
 return new OptimizerResult(p, candidateError);
};

一旦定义了objective函数,我们现在可以创建并运行优化器以找到最佳参数集。让我们先运行我们的优化器 30 次迭代,并尝试 30 个不同的超参数集:

// create our optimizer
var optimizer = new RandomSearchOptimizer(parameters, iterations: 30, runParallel: true);
// find the best hyperparameters for use
var result = optimizer.OptimizeBest(minimize);
var best = result.ParameterSet;

一旦我们运行这个,我们的优化器应该找到最佳的超参数集。让我们看看它找到了什么:

  • Trees: 277

  • 学习率: 0.035

  • maximumTreeDepth: 15

  • subSampleRatio: 0.838

  • featuresPrSplit: 4

进度。现在我们已经有一组在验证集上测量的最佳超参数,我们可以使用这些参数创建一个学习器,并使用整个数据集学习一个新的模型:

var learner = new RegressionSquareLossGradientBoostLearner(
 iterations: (int)best[0],
learningRate: best[1], 
maximumTreeDepth: (int)best[2], 
subSampleRatio: best[3],
featuresPrSplit: (int)best[4], 
runParallel: false);
var model = learner.Learn(trainSet.Observations, trainSet.Targets);

现在我们有了最终的超参数集,我们将这些传递给我们的学习器,能够显著降低测试错误。如果我们手动做这件事,将花费我们无尽的时光和更多!

算法 测试错误
回归平方损失梯度提升学习器(默认) 0.4984
回归平方损失梯度提升学习器(优化器) 0.3852

示例代码和应用

在接下来的几节中,我们将查看一些没有所有冗余的代码示例。这将完全是 C#代码,所以它应该是所有人都容易理解的。

让我们快速看一下我们如何使用 SharpLearning 来预测观察值。我将向您展示一个完整的代码示例,而不包含冗余:

var parser = new CsvParser(() =>new StringReader(Resources.AptitudeData));
var observations = parser.EnumerateRows(v => v != "Pass").ToF64Matrix();
var targets = parser.EnumerateRows("Pass").ToF64Vector();
var rows = targets.Length;
var learner = new ClassificationDecisionTreeLearner(100, 1, 2, 0.001, 42);
varsut = learner.Learn(observations, targets);
var predictions = sut.Predict(observations);
var evaluator = new TotalErrorClassificationMetric<double>();
var error = evaluator.Error(targets, predictions);
Assert.AreEqual(0.038461538461538464, error, 0.0000001);

保存模型

这里是一个代码示例,将向您展示保存模型是多么容易:

var parser = new CsvParser(() =>new StringReader(Resources.AptitudeData));
var observations = parser.EnumerateRows(v => v != "Pass").ToF64Matrix();
var targets = parser.EnumerateRows("Pass").ToF64Vector();
var learner = new ClassificationDecisionTreeLearner(2);
var sut = learner.Learn(observations, targets);
var writer = new StringWriter();
sut.Save(() => writer);

均方误差回归指标

均方误差(MSE)是一个衡量误差平方平均值的指标。更具体地说,它衡量估计值与估计值之间的平均距离。均方误差始终为非负值,接近零的值被认为是更可接受的。SharpLearning 使得计算此误差指标变得极其简单,如下面的代码所示:

var targets = new double[] { 1.0, 2.3, 3.1, 4.4, 5.8 };
var predictions = new double[] { 1.0, 2.0, 3.0, 4.0, 5.0 };
var sut = new MeanSquaredErrorRegressionMetric();
var actual = sut.Error(targets, predictions);

F1 分数

要谈论 f1 分数,我们必须首先谈论精确度召回率

精确度是正确预测的正观察值与总预测正观察值的比率。不太正式地说,在所有说他们会来的人中,有多少人真的来了?

回忆(灵敏度)是正确预测的正观察值与总观察值的比率。

F1 分数是精确度和召回率的加权平均值。

这是使用 SharpLearning 计算 f1 分数的方法:

var targets = new double[] { 0, 1, 1 };
var predictions = new double[] { 0, 1, 1 };
var sut = new F1ScoreMetric<double>(1);
var actual = sut.Error(targets, predictions);
Assert.AreEqual(0.0, actual);

优化

以下是如何使用粒子群优化器返回最佳最小化提供的函数的结果的方法:

var parameters = new ParameterBounds[]
{
new ParameterBounds(-10.0, 10.0, Transform.Linear),
new ParameterBounds(-10.0, 10.0, Transform.Linear),
new ParameterBounds(-10.0, 10.0, Transform.Linear),
}; 
var sut = new ParticleSwarmOptimizer(parameters, 100);
var actual = sut.OptimizeBest(Minimize);

以下是如何使用网格搜索优化器通过尝试提供的参数的所有组合来优化的方法:

var parameters = new double[][] { new double[]{ 10.0, 20.0, 30.0, 35.0, 37.5, 40.0, 50.0, 60.0 } };
var sut = new GridSearchOptimizer(parameters);
var actual = sut.OptimizeBest(Minimize);

以下是如何使用 随机搜索优化器 在提供的最小值和最大值之间初始化随机参数的方法。将返回最小化提供函数的最佳结果:

var parameters = new ParameterBounds[] 
{
new ParameterBounds(0.0, 100.0, Transform.Linear)
};
var sut = new RandomSearchOptimizer(parameters, 100);
var actual = sut.OptimizeBest(Minimize);

样本应用 1

在掌握所有这些知识之后,让我们继续编写我们的第一个样本程序。程序本身非常简单,旨在展示如何以最少的代码实现这些技术。为了展示我的意思,以下是程序输出的样子:

代码

这里是实现我们的样本程序并产生之前输出的代码。如您所见,代码非常简单,这里面的所有代码(除了索引的洗牌)都是我们之前已经介绍过的。我们将保持详尽性到最小,以便您能专注于代码本身。这个样本将读取我们的数据,将其解析成观察和目标样本,然后使用 1,000 棵树创建一个学习器。然后我们将使用这个学习器来学习和创建我们的模型。一旦完成,我们将计算均方误差指标并在屏幕上显示:

var parser = new CsvParser(() =>new StringReader(Resources.Glass));
var observations = parser.EnumerateRows(v => v != "Target").ToF64Matrix();
var targets = parser.EnumerateRows("Target").ToF64Vector();
int trees = 1000;
var sut = new RegressionExtremelyRandomizedTreesLearner(trees, 1, 100, 1, 0.0001, 1.0, 42, false);
var indices = Enumerable.Range(0, targets.Length).ToArray();
indices.Shuffle(new Random(42));
indices = indices.Take((int)(targets.Length * 0.7)).ToArray();
var model = sut.Learn(observations, targets, indices);
var predictions = model.Predict(observations);
var evaluator = new MeanSquaredErrorRegressionMetric();
var error = evaluator.Error(targets, predictions);
Console.WriteLine("Error: " + error);

样本应用 2 – 葡萄酒质量

在我们的下一个应用程序中,我们将使用我们的知识来确定基于创建的模型,葡萄酒最重要的特征。以下是完成它后我们的输出将看起来像什么:

代码

这里是我们应用程序的代码。和往常一样,我们首先将数据加载并解析成观察和目标样本集。由于这是一个回归样本,我们将数据样本分成 70/30 的比例:70%用于训练,30%用于测试。然后,我们创建我们的随机森林学习者和模型。之后,我们计算训练和测试错误,并按模型找到的重要性顺序打印出特征重要性:

var parser = new CsvParser(() =>new StreamReader(Application.StartupPath + "\\winequality-white.csv"));
var targetName = "quality";
// read in our feature matrix
var observations = parser.EnumerateRows(c => c != targetName).ToF64Matrix();
// read in our regression targets
var targets = parser.EnumerateRows(targetName).ToF64Vector();
// Since this is a regression problem, we use the random training/test set splitter. 30 % of the data is used for the test set. 
var splitter = new RandomTrainingTestIndexSplitter<double>(trainingPercentage: 0.7, seed: 24);
var trainingTestSplit = splitter.SplitSet(observations, targets);
var trainSet = trainingTestSplit.TrainingSet;
var testSet = trainingTestSplit.TestSet;
var learner = new RegressionRandomForestLearner(trees: 100);
var model = learner.Learn(trainSet.Observations, trainSet.Targets);
var trainPredictions = model.Predict(trainSet.Observations);
var testPredictions = model.Predict(testSet.Observations);
// since this is a regression problem we are using squared error as the metric
// for evaluating how well the model performs.
var metric = new MeanSquaredErrorRegressionMetric();
var trainError = metric.Error(trainSet.Targets, trainPredictions);
var testError = metric.Error(testSet.Targets, testPredictions);
Trace.WriteLine($"Train error: {trainError:0.0000} - Test error: {testError:0.0000}");
System.Console.WriteLine($"Train error: {trainError:0.0000} - Test error: {testError:0.0000}");

// the variable importance requires the featureNameToIndex from the dataset. 
// This mapping describes the relation from the column name to the associated 
// index in the feature matrix.
var featureNameToIndex = parser.EnumerateRows(c => c != targetName).First().ColumnNameToIndex;

var importances = model.GetVariableImportance(featureNameToIndex);

var importanceCsv = new StringBuilder();
importanceCsv.Append("FeatureName;Importance");
System.Console.WriteLine("FeatureName\tImportance");
foreach (var feature in importances)
{
importanceCsv.AppendLine();
importanceCsv.Append($"{feature.Key};{feature.Value:0.00}");
System.Console.WriteLine($"{feature.Key}\t{feature.Value:0.00}");
}
Trace.WriteLine(importanceCsv);

摘要

在本章中,我们学习了决策树和随机森林。我们还学习了如何使用开源框架 SharpLearn 将这些强大的功能添加到我们的应用程序中。在下一章中,我们将学习关于面部和动作检测的内容,并展示您如何使用这项令人兴奋的技术来启用您的应用程序!您将见到我的宠物法国斗牛犬 Frenchie,他将演示我们将展示的大多数样本。此外,我们还有一位嘉宾,您绝对会想见一见!

参考文献

第九章:面部与动作检测

现在是我们深入一个真正有趣的应用的时候了。我们将从使用开源包www.aforgenet.com/来构建一个面部和动作检测应用开始。为此,你需要在你的系统中安装一个摄像头来查看实时视频流。从那里,我们将使用这个摄像头来检测面部和动作。在本章中,我们将展示两个独立的示例:一个用于面部检测,另一个用于动作检测。我们将向你展示具体发生了什么,以及你如何快速将这些功能添加到你的应用中。

在本章中,我们将涵盖以下主题:

  • 面部检测

  • 动作检测

  • 如何使用本地视频集成摄像头

  • 图像滤波/算法

让我们从面部检测开始。在我们的示例中,我将使用我友好的小法国斗牛犬来为我们摆姿势。在我这样做之前,请重新阅读章节标题。无论你读多少次,你可能会错过这里的关键点。注意它说的是面部检测而不是面部识别。这一点非常重要,我想停下来再次强调。我们不是试图识别 Joe、Bob 或 Sally。我们试图验证,通过我们的摄像头看到的所有事物中,我们能否检测到有面孔存在。我们并不关心这是谁的面孔,只是它是一个面孔!这一点非常重要,在我们继续之前,我们必须理解这一点,否则你的期望可能会被错误地偏向,这会让你感到困惑和沮丧,而我们不想看到这种情况发生!

面部检测,就像我稍后会再次强调的那样,是面部识别的第一步,这是一个更为复杂的任务。如果你不能从屏幕上所有的事物中识别出一张或更多面孔,那么你永远无法识别出这是谁的面孔!

技术要求

作为先决条件,你需要在你的系统上安装 Microsoft Visual Studio(任何版本)。你还需要访问开源的 accord 框架,网址为github.com/accord-net/framework

查看以下视频以了解代码的实际应用:bit.ly/2xH0thh

面部检测

现在,让我们快速看一下我们的应用。你应该已经将示例解决方案加载到 Microsoft Visual Studio 中:

图片

下面是我们的示例应用运行时的样子。大家好,向 Frenchie 问好!

图片

如你所见,我们有一个非常简单的屏幕,专门用于我们的视频捕获设备。在这种情况下,笔记本电脑的摄像头是我们的视频捕获设备。Frenchie 友好地站在摄像头前为我们摆姿势,一旦我们启用面部跟踪,看看会发生什么:

图片

Frenchie 的面部特征现在正在被跟踪。你看到围绕 Frenchie 的是跟踪容器(白色方框),以及我们的角度检测器(红色线条)显示。当我们移动 Frenchie 时,跟踪容器和角度检测器将跟踪他。那很好,但如果我们在真实的人类脸上启用面部跟踪会怎样呢?正如你在下面的截图中所见,跟踪容器和角度正在跟踪我们客座摆姿势者的面部,就像对 Frenchie 做的那样。

图片

当我们的摆姿势者从一侧移动头部时,相机跟踪这个动作,你可以看到角度检测器正在调整以识别为面部角度。在这种情况下,你会注意到色彩空间是黑白而不是彩色。这是一个直方图反向投影,这是一个你可以更改的选项:

图片

即使当我们远离相机,其他物体进入视野时,面部检测器也能在噪声中跟踪我们的面部。这正是电影中你看到的面部识别系统的工作方式,尽管更为简单,而且几分钟内你就可以启动自己的面部识别应用!

图片

既然我们已经看到了外部情况,那么让我们来看看引擎盖下正在发生的事情。

我们需要确切地问问自己,我们在这里试图解决什么问题。嗯,我们正在尝试检测(注意我没有说识别)面部图像。虽然对人类来说很容易,但计算机需要非常详细的指令集来完成这项任务。幸运的是,有一个非常著名的算法叫做 Viola-Jones 算法,它将为我们完成繁重的工作。我们为什么选择这个算法呢?

  • 非常高的检测率和极低的误报率。

  • 非常擅长实时处理。

  • 非常擅长从非面部检测面部。检测面部是面部识别的第一步。

此算法要求相机有一个正面的正面视角。为了被检测到,面部需要直接朝向相机,不能倾斜,不能向上或向下看。记住,目前我们只对面部检测感兴趣。

要深入了解技术方面,我们的算法需要四个阶段来完成其任务。它们是:

  • Haar 特征选择

  • 创建积分图像

  • Adaboost 训练

  • 级联分类器

我们必须首先声明,所有人类面部都有一些相似的特征,例如眼睛比上脸颊暗,鼻梁比眼睛亮,你的额头可能比你的脸的其他部分亮,等等。我们的算法通过使用所谓的Haar 特征来匹配这些特征。我们可以通过观察眼睛、嘴巴和鼻梁的位置和大小来得出可匹配的面部特征。然而,这里是我们的问题。

在一个 24x24 像素的窗口中,总共有 162,336 个可能的特征。显然,尝试评估所有这些特征将是非常昂贵的,如果它甚至能工作的话。因此,我们将使用一种称为自适应提升的技术,或者更常见地,AdaBoost。这是你词汇表中的一个新词,你无处不在都能听到它,也许甚至读过它。我们的学习算法将使用 AdaBoost 来选择最佳特征并训练分类器来使用它们。让我们停下来谈谈它。

AdaBoost 可以与许多类型的学习算法一起使用,被认为是许多任务的最好现成算法。你通常不会注意到它有多好、有多快,直到你切换到另一个算法并对其进行计时。我已经这样做过无数次,我可以告诉你差别非常明显。

Boosting 从其他弱学习算法的输出中提取信息,并将它们与加权求和结合起来,这是提升分类器的最终输出。AdaBoost 的自适应部分来自于后续的学习者被调整以有利于那些被先前分类器错误分类的实例。然而,我们必须小心我们的数据准备,因为 AdaBoost 对噪声数据和异常值很敏感(记得我们在第一章,快速复习中强调了这些)。该算法比其他算法更容易过拟合数据,这就是为什么在我们早期的章节中我们强调了缺失数据和异常值的数据准备。最后,如果学习算法比随机猜测更好,AdaBoost 可以成为我们流程中的一个宝贵补充。

在有了这个简短的描述之后,让我们来看看底层发生了什么。在这个例子中,我们再次使用 Accord 框架,我们将与 Vision Face Tracking 示例一起工作。你可以从其 GitHub 位置下载这个框架的最新版本:github.com/accord-net/framework

我们首先创建一个FaceHaarCascade对象。这个对象包含了一组 Haar-like 特征的弱分类阶段,或者说是阶段。将提供许多阶段,每个阶段包含一组将在决策过程中使用的分类树。我们现在实际上是在处理一个决策树。Accord 框架的美丽之处在于FaceHaarCascade会自动为我们创建所有这些阶段和树,而不会暴露给我们细节。

让我们看看一个特定阶段可能的样子:

List<HaarCascadeStage> stages = new List<HaarCascadeStage>();
List<HaarFeatureNode[]> nodes;
HaarCascadeStage stage;
stage = new HaarCascadeStage(0.822689414024353); nodes = new List<HaarFeatureNode[]>();
nodes.Add(new[] { new HaarFeatureNode(0.004014195874333382, 0.0337941907346249, 0.8378106951713562, new int[] { 3, 7, 14, 4, -1 }, new int[] { 3, 9, 14, 2, 2 }) });
nodes.Add(new[] { new HaarFeatureNode(0.0151513395830989, 0.1514132022857666, 0.7488812208175659, new int[] { 1, 2, 18, 4, -1 }, new int[] { 7, 2, 6, 4, 3 }) });
nodes.Add(new[] { new HaarFeatureNode(0.004210993181914091, 0.0900492817163467, 0.6374819874763489, new int[] { 1, 7, 15, 9, -1 }, new int[] { 1, 10, 15, 3, 3 }) });
stage.Trees = nodes.ToArray(); stages.Add(stage);

如你所见,我们通过为每个阶段的节点提供每个特征的数值,在底层构建了一个决策树。

一旦创建,我们可以使用我们的级联对象来创建我们的HaarObjectDetector,这是我们用于检测的工具。它需要:

  • 我们的面部级联对象

  • 搜索对象时使用的最小窗口大小

  • 我们搜索的模式——在我们的例子中,我们只搜索单个对象

  • 在搜索过程中重新调整搜索窗口大小时使用的缩放因子

HaarCascade cascade = new FaceHaarCascade();
detector = new HaarObjectDetector(cascade, 25, ObjectDetectorSearchMode.Single, 1.2f,
ObjectDetectorScalingMode.GreaterToSmaller);

一旦创建,我们就准备好处理视频源的话题。在我们的示例中,我们将简单地使用本地相机来捕获所有图像。然而,Accord.Net 框架使得使用其他图像捕获源变得容易,例如 .avi 文件,动画 .jpg 文件等等。

我们连接到相机,选择分辨率,然后准备出发:

foreach (var cap in device?.VideoCapabilities)
 {
if (cap.FrameSize.Height == 240)
return cap;
if (cap.FrameSize.Width == 320)
return cap;
 }
return device?.VideoCapabilities.Last();

应用程序现在正在运行,并且已选择视频源,我们的应用程序将如下所示。再次,请输入法尼奇这只斗牛犬!请原谅这儿的混乱,法尼奇不是最整洁的宠物!:

图片

在这个演示中,您会注意到法尼奇正对着相机,背景中有 2 个 55 英寸的显示器,以及许多其他我妻子喜欢称之为垃圾(我们将正式称之为噪声)的东西!这是为了展示面部检测算法如何从其他所有东西中区分出法尼奇的脸!如果我们的检测器无法处理这种情况,它将迷失在噪声中,对我们几乎没有用处。

现在我们有了视频源,我们需要在接收到新帧时得到通知,以便我们可以处理它,应用我们的标记等。我们通过将视频源播放器的 NewFrameReceived 事件处理程序附加到它来完成此操作。.NET 开发者应该非常熟悉这一点:

this.videoSourcePlayer.NewFrameReceived += new Accord.Video.NewFrameEventHandler(this.videoSourcePlayer_NewFrame);

让我们看看每次我们被通知有新的视频帧可用时会发生什么。

我们需要做的第一件事是将图像下采样,使其更容易处理:

ResizeNearestNeighbor resize = new ResizeNearestNeighbor(160, 120);
UnmanagedImagedownsample = resize.Apply(im);

当图像大小更易于处理时,我们将处理该帧。如果没有找到面部区域,我们将保持在跟踪模式中等待一个可检测到面部的帧。如果找到了面部区域,我们将重置跟踪器,定位面部,减小其大小以清除任何背景噪声,初始化跟踪器,并将标记窗口应用于图像。所有这些操作都通过以下代码完成:

if (regions != null&&regions.Length>0)
 {
tracker?.Reset();
// Will track the first face found
Rectangle face = regions[0];
// Reduce the face size to avoid tracking background
Rectangle window = new Rectangle((int)((regions[0].X + regions[0].Width / 2f) * xscale),
 (int)((regions[0].Y + regions[0].Height / 2f) * yscale), 1, 1);
window.Inflate((int)(0.2f * regions[0].Width * xscale), (int)(0.4f * regions[0].Height * yscale));
if (tracker != null)
 {
tracker.SearchWindow = window;
tracker.ProcessFrame(im);
 }
marker = new RectanglesMarker(window);
marker.ApplyInPlace(im);
args.Frame = im.ToManagedImage();
tracking = true;
 }
else
 {
detecting = true;
 }

如果检测到面部,我们的图像帧现在看起来如下:

图片

如果法尼奇将头倾斜到一边,我们的图像帧现在看起来如下:

图片

运动检测

现在,我们将关注范围扩大一些,检测任何运动,而不仅仅是面部运动。再次,我们将使用 Accord.Net 并使用 运动检测 示例。就像面部识别一样,您将看到如何简单地将此功能添加到您的应用程序中,并立即成为工作中的英雄!让我们确保您已将正确的项目加载到 Microsoft Visual Studio 中:

图片

使用运动检测,屏幕上移动的任何东西都会用红色突出显示,所以使用以下屏幕截图,你可以看到手指在移动,但其他一切保持静止:

图片

在下面的屏幕截图中,你可以看到更多的运动,由沿着这个匿名手的红色方块表示:

图片

在下面的屏幕截图中,你可以看到整个手在移动:

图片

如果我们不希望处理整个屏幕区域中的运动,我们可以定义运动区域,运动检测将仅在这些区域内发生。在下面的屏幕截图中,你可以看到我定义了一个运动区域。你将在接下来的屏幕截图中注意到,这是唯一一个将处理运动的区域:

图片

现在,如果我们为相机创建一些运动,你会看到只有我们定义区域内的运动被处理,如下所示:

图片

你也可以看到,在定义了运动区域,并且彼得这个冥想的哥布林站在区域前面时,我们仍然能够检测到他后面的运动,但他的脸不是识别的一部分。当然,你可以将这两个过程结合起来,以获得最好的效果,如下所示:

图片

另一个我们可以使用的选项是网格运动高亮。它根据定义的网格以红色方块的形式突出显示检测到的运动区域。基本上,运动区域现在是一个红色框,正如你所见:

图片

代码

以下代码片段展示了添加视频识别到你的应用所需做的所有事情。正如你所见,这 couldn’t be any easier:

// create motion detector
MotionDetector detector = new MotionDetector(
 new SimpleBackgroundModelingDetector( ),
 new MotionAreaHighlighting( ) );
// continuously feed video frames to motion detector
while ( ... )
{
 // process new video frame and check motion level
 if ( detector.ProcessFrame( videoFrame ) > 0.02 )
 {
 // ring alarm or do something else
 }
}

我们现在打开我们的视频源:

videoSourcePlayer.VideoSource = new AsyncVideoSource(source);

当我们接收到一个新的视频帧时,所有的魔法就会发生。以下是将处理新的视频帧成功所需的全部代码:

private void videoSourcePlayer_NewFrame(object sender, NewFrameEventArgsargs)
 {
lock (this)
 {
if (detector != null)
 {
floatmotionLevel = detector.ProcessFrame(args.Frame);
if (motionLevel > motionAlarmLevel)
 {
// flash for 2 seconds
flash = (int)(2 * (1000 / alarmTimer.Interval));
 }
// check objects' count
if (detector.MotionProcessingAlgorithm is BlobCountingObjectsProcessing)
 {
BlobCountingObjectsProcessing countingDetector = (BlobCountingObjectsProcessing)detector.MotionProcessingAlgorithm;
detectedObjectsCount = countingDetector.ObjectsCount;
 }
else
 {
detectedObjectsCount = -1;
 }
// accumulate history
motionHistory.Add(motionLevel);
if (motionHistory.Count> 300)
                    {
motionHistory.RemoveAt(0);
                    }

if (showMotionHistoryToolStripMenuItem.Checked)
DrawMotionHistory(args.Frame);
                }
            }

关键在于检测帧中发生的运动量,这是通过以下代码完成的。对于这个例子,我们使用运动警报级别为 0.2,但你可以使用任何你喜欢的。一旦这个阈值被超过,你可以执行任何你喜欢的逻辑,比如发送电子邮件警报,短信,启动视频捕获操作等等:

float motionLevel = detector.ProcessFrame(args.Frame);
if (motionLevel > motionAlarmLevel)
{
// flash for 2 seconds
flash = (int)(2 * (1000 / alarmTimer.Interval));
}

摘要

在本章中,我们学习了图像和运动检测(不是识别!)我们以 Accord.Net 作为开源工具为我们提供添加应用功能的一个例子。

在下一章中,我们继续探讨图像主题,但使用开源包 ConvNetSharp 训练卷积神经网络。

第十章:使用 ConvNetSharp 训练 CNN

在本章中,我们将使用 Cédric Bovar 的杰出开源包ConvNetSharp,来展示如何训练我们的卷积神经网络CNN)。在本章中,我们将探讨以下主题:

  • 常见的神经网络模块

  • 与 CNN 相关的各种术语和概念

  • 处理图像的卷积网络

技术要求

您需要 Microsoft Visual Studio 和 ConvNetSharp 框架来完成本章内容。

熟悉环境

在我们开始深入代码之前,让我们先了解一下一些基本术语,这样当我们提到这些术语时,我们都在同一页面上。这个术语适用于 CNN 以及ConvNetSharp框架。

卷积: 在数学中,卷积是对两个函数执行的操作。这个操作产生第三个函数,它表达了其中一个形状如何被另一个形状修改的表达式。这在以下图中以视觉方式表示:

图片

重要的是要注意,卷积层本身是 CNN 的构建块。这个层的参数由一组可学习的过滤器(有时称为)组成。这些核具有小的感受野,这是一个对整个图像的较小视图,并且这个视图扩展到输入体积的整个深度。在前向传播阶段,每个过滤器在整个输入体积的宽度和高度上卷积。正是这种卷积计算了过滤器和输入之间的点积。然后产生一个二维图(有时称为激活图)的过滤器。这有助于网络学习在检测到相应输入位置的特征时应该激活哪些过滤器。

点积计算: 以下图是当我们说点积计算时我们指的是什么的一个可视化:

图片

  • Vol 类: 在 ConvNetSharp 中,Vol类简单地说是一个围绕一维数字列表、它们的梯度以及维度(即宽度、深度和高度)的包装。

  • Net 类: 在 ConvNetSharp 中,Net是一个非常简单的类,它包含一个层的列表。当一个Vol通过Net类时,Net会遍历所有其层,通过调用forward()函数逐个前向传播,并返回最后一层的输出。在反向传播过程中,Net会调用每个层的backward()函数来计算梯度。

  • : 如我们所知,每个神经网络都是一个层的线性列表,我们的也不例外。对于一个神经网络,第一层必须是输入层,我们的最后一层必须是输出层。每一层都接受一个输入Vol并产生一个新的输出Vol

  • 全连接层:全连接层可能是最重要的层,并且在功能上肯定是最有趣的。它包含一个执行所有输入加权加权的神经元层。然后,这些输入通过一个非线性激活函数,如 ReLU。

  • 损失层分类器层:当我们需要为我们的数据预测一组离散类别时,这些层非常有用。你可以使用 softmax、SVM 和许多其他类型的层。像往常一样,你应该对你的特定问题进行实验,看看哪一个效果最好。

  • 损失层L2 回归层:这一层接受一个目标列表,并通过它们反向传播 L2 损失。

  • 卷积层:这一层几乎与全连接层是镜像关系。这里的区别在于神经元仅与层中的一些神经元局部连接,而不是与所有神经元连接。它们也共享参数。

  • 训练器Trainer类接受一个网络和一组参数。它将这些参数通过网络传递,查看预测结果,并调整网络权重以使提供的标签对特定输入更加准确。随着时间的推移,这个过程将改变网络,并将所有输入映射到正确的输出。

在我们解决了这些问题之后,现在让我们谈谈 CNN 本身。CNN 由输入层和输出层组成;这并不令人惊讶。将有一到多个隐藏层,这些隐藏层由卷积层、池化层、全连接层或归一化层组成。魔法就发生在这些隐藏层中。卷积层对输入应用卷积操作,并将结果传递到下一层。我们稍后会更多地讨论这一点。

随着我们前进,激活图将被堆叠,以处理沿着深度维度的所有过滤器。这反过来将形成层的完整输出体积。该层上的每个神经元只处理其自身的感受野(它可以看到的数据)。这些信息与其他神经元共享。

在 CNN 中,我们必须始终牢记输入大小,这可能会根据图像的分辨率需要处理极大量的神经元。这可能会在架构上变得不方便,甚至无法处理,因为每个像素都是一个需要处理的可变因素。

让我们来看一个例子。如果我们有一个 100 x 100 像素的图像,我们都会同意这是一个小图像。然而,这个图像总共有 10,000 个像素(100 x 100),这些像素是第二层中每个神经元的权重。卷积是解决这个问题的关键,因为它减少了参数的数量,并允许网络在更少的参数下更深入地学习。如果有 10,000 个可学习的参数,解决方案可能是完全不可行的;然而,如果我们把这个图像缩小到 5 x 5 的区域,例如,我们现在有 25 个不同的神经元来处理,而不是 10,000 个,这要现实得多。这也有助于我们消除,或者至少大大减少,我们在训练多层网络时有时会遇到梯度消失或爆炸问题。

现在我们快速看一下这是如何视觉化的。如图所示,我们将使用数字 6 并通过 CNN 运行它,看看我们的网络是否能够检测到我们试图绘制的数字。以下屏幕截图底部的图像是我们将要绘制的。当我们把所有卷积都做到顶部时,我们应该能够点亮表示数字 6 的单个神经元,如下所示:

图片

在前面的屏幕截图中,我们可以看到一个输入层(我们单独的数字 6),卷积层,下采样层,以及输出层。我们的进展如下:我们从一个 32 x 32 的图像开始,这给我们留下了 1,024 个神经元。然后我们减少到 120 个神经元,然后到 100 个神经元,最后在我们的输出层有 10 个神经元 – 那就是每个 10 个数字中的一个神经元。你可以看到,随着我们向输出层前进,图像的维度在减小。正如我们所看到的,我们的第一个卷积层是 32 x 32,第二个是 10 x 10,第二个池化层是 5 x 5。

值得注意的是,输出层中的每个神经元都与它前面的全连接层中的所有 100 个节点完全连接;因此,称之为全连接层。

如果我们把这个网络的三维图画出来并翻转它,我们可以更好地看到卷积是如何发生的。以下图解正是如此,因为激活的神经元颜色更亮。层继续卷积,直到决定我们画的是哪个数字,如下所示:

图片

过滤器

CNN 的另一个独特特征是许多神经元可以共享相同的权重和偏差向量,或者更正式地说,相同的过滤器。那为什么这很重要呢?因为每个神经元通过将一个函数应用于前一层的输入值来计算输出值。对这些权重和偏差的增量调整有助于网络学习。如果相同的过滤器可以被重用,那么所需的内存占用将大大减少。这变得非常重要,尤其是在图像或感受野变得更大时。

CNN 具有以下特点:

  • 神经元的体积:CNN 的层在三个维度上排列神经元:宽度、高度和深度。每个层内的神经元连接到其前一层的较小区域,称为它们的感受野。不同类型的连接层堆叠形成实际的卷积架构,如下所示:

卷积

  • 共享权重:在卷积神经网络中,每个感受野(滤波器)在整个视觉场中复制,如前述图像所示。这些滤波器共享相同的权重向量和偏置参数,形成了通常所说的 特征图。这意味着给定卷积层中的所有神经元对其特定区域内的同一特征做出响应。以这种方式复制单元允许无论特征在视觉场中的位置如何都能检测到特征。以下图是这一含义的简单示例:

这是一个示例

这是一个示例

这是一个示例

这是一个示例

创建一个网络

使用 ConvNetSharp 框架,有三种创建神经网络的方法。首先,我们可以使用 Core.LayersFlow.Layers 对象创建卷积网络(带或不带计算图),如下所示:

或者,我们可以创建一个如下所示的计算图:

示例 1 – 一个简单示例

让我们看看我们的第一个示例。这是一个最简单的示例,我们将定义一个 两层 神经网络,并在单个数据点上对其进行训练。我们故意使这个例子详细,以便我们可以一起逐步了解,以加深我们的理解:

var net = new Net<double>();

InputLayer 变量声明了输入的大小。如前述代码所示,我们使用二维数据。需要三维体积(宽度、高度和深度),但如果您不处理图像,则可以将前两个维度(宽度和高度)设置为 1,如下例所示:

net.AddLayer(new InputLayer(1, 1, 2));

声明一个包含 20 个神经元的全连接层,如下所示:

net.AddLayer(new FullyConnLayer(20));

接下来,我们需要声明一个 Rectified Linear Unit 非线性层(ReLU),如下所示:

net.AddLayer(new ReluLayer());

然后,使用以下代码声明一个全连接层,该层将被 SoftmaxLayer 使用:

net.AddLayer(new FullyConnLayer(10));

在上一个隐藏层之上声明线性分类器,如下所示:

net.AddLayer(new SoftmaxLayer(10));
var x = BuilderInstance.Volume.From(new[] { 0.3, -0.5 }, new Shape(2));

然后,我们需要通过一个随机数据点在网络中前进,如下所示:

var prob = net.Forward(x);

prob 是一个体积。体积具有属性权重,用于存储原始数据,以及权重梯度,用于存储梯度。以下代码打印出约 0.50101,如下所示:

Console.WriteLine("probability that x is class 0: " + prob.Get(0));

接下来,我们需要训练网络,指定 x 是类别零,并使用随机梯度下降训练器,如下所示:

var trainer = new SgdTrainer(net)
{
LearningRate = 0.01, L2Decay = 0.001
};
trainer.Train(x,BuilderInstance.Volume.From(new[]{ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, new Shape(1, 1, 10, 1)));
var prob2 = net.Forward(x);
Console.WriteLine("probability that x is class 0: " + prob2.Get(0));

现在的输出应该是 0.50374,这略高于之前的 0.50101。这是因为网络权重已经被trainer调整,给训练网络时我们训练的类别(即零)更高的概率。

示例 2 – 另一个简单示例

如前文所述,以下示例同样解决了一个简单问题,同时展示了如何保存和加载一个图:

var cns = new ConvNetSharp<float>();

要创建一个图,请输入以下代码:

Op<float> cost;
Op<float> fun;
if (File.Exists("test.graphml"))
{
Console.WriteLine("Loading graph from disk.");
var ops = SerializationExtensions.Load<float>("test", true);
fun = ops[0];
cost = ops[1];
}
else
{
var x = cns.PlaceHolder("x");
var y = cns.PlaceHolder("y");
var W = cns.Variable(1.0f, "W", true);
var b = cns.Variable(2.0f, "b", true);
fun = x * W + b;
cost = (fun - y) * (fun - y);
}
var optimizer = new AdamOptimizer<float>(cns, 0.01f, 0.9f, 0.999f, 1e-08f);
using (var session = new Session<float>())
{

接下来,为了计算图中每个节点的 dCost/dW,我们使用以下代码:

session.Differentiate(cost);
float currentCost;
do
{
 var dico = new Dictionary> { { "x", -2.0f }, { "y", 1.0f } };
currentCost = session.Run(cost, dico);
Console.WriteLine($"cost: {currentCost}");
var result = session.Run(fun, dico);
session.Run(optimizer, dico);
}
while (currentCost > 1e-5);
float finalW = session.GetVariableByName(fun, "W").Result;
float finalb = session.GetVariableByName(fun, "b").Result;
Console.WriteLine($"fun = x * {finalW} + {finalb}");
fun.Save("test", cost);

要显示图,请输入以下代码:

var vm = new ViewModel<float>(cost);
var app = new Application();
app.Run(new GraphControl { DataContext = vm });
}

示例 3 – 我们最后的简单示例

以下示例执行了一个简单的计算,并显示了结果计算图。所需的代码如下:

var cns = new ConvNetSharp<float>();

要创建一个图,请使用以下代码:

var x = cns.PlaceHolder("x");
var fun = 2.0f * x;
using (var session = new Session<float>())
{

接下来,为了计算图中每个节点的 dCost/dW,我们使用以下代码:

session.Differentiate(fun);

最后,为了显示图,请输入以下代码:

var vm = new ViewModel<float>(x.Derivate);
var app = new Application();
app.Run(new GraphControl { DataContext = vm });
}

使用 Fluent API

对于那些有 Fluent API 错误的朋友,ConvNetSharp 已经为您提供了一个解决方案。

只需查看以下示例,看看使用 Fluent DSL 添加层是多么简单:

varnet=FluentNet<double>.Create(24, 24, 1)
.Conv(5, 5, 8).Stride(1).Pad(2)
.Relu()
.Pool(2, 2).Stride(2)
.Conv(5, 5, 16).Stride(1).Pad(2)
.Relu()
.Pool(3, 3).Stride(3)
.FullyConn(10)
.Softmax(10)
.Build();

GPU

为了在您的软件中使用 ConvNetSharp 的 GPU 功能,您必须安装 CUDA 版本 8 和 Cudnn 版本 6.0(2017 年 4 月 27 日)。同时,Cudnn bin path也应该在PATH环境变量中引用。

使用 MNIST 数据库进行 Fluent 训练

在以下示例中,我们将训练我们的 CNN 对抗MNIST图像数据库。

声明一个函数,请使用以下代码:

private void MnistDemo()
{

接下来,使用以下命令下载训练和测试datasets

var datasets = new DataSets();

使用以下命令加载100个验证集:

if (!datasets.Load(100))
{
return;
}

现在是时候使用 Fluent API 创建神经网络了,如下所示:

this._net = FluentNet<double>.Create(24, 24, 1)
.Conv(5, 5, 8).Stride(1).Pad(2)
.Relu()
.Pool(2, 2).Stride(2)
.Conv(5, 5, 16).Stride(1).Pad(2)
.Relu()
.Pool(3, 3).Stride(3)
.FullyConn(10)
.Softmax(10)
.Build();

使用以下命令从网络创建随机梯度下降训练器:

this._trainer = new SgdTrainer<double>(this._net)
{
LearningRate = 0.01,
BatchSize = 20,
L2Decay = 0.001,
Momentum = 0.9
};
do
{

接下来,使用以下代码获取数据的NextBatch

var trainSample = datasets.Train.NextBatch(this._trainer.BatchSize);

使用以下命令Train接收到的数据:

Train(trainSample.Item1, trainSample.Item2, trainSample.Item3);

现在是时候获取数据的NextBatch;为此,请使用以下命令:

var testSample = datasets.Test.NextBatch(this._trainer.BatchSize);

可以使用以下命令测试代码:

Test(testSample.Item1, testSample.Item3, this._testAccWindow);

要报告accuracy,请输入以下命令:

Console.WriteLine("Loss: {0} Train accuracy: {1}% Test accuracy: {2}%", this._trainer.Loss, Math.Round(this._trainAccWindow.Items.Average() * 100.0, 2),
Math.Round(this._testAccWindow.Items.Average() * 100.0, 2));
} while (!Console.KeyAvailable);

训练网络

为了训练卷积网络,我们必须执行前向和反向传播,如下所示:

public virtual void Train(Volume<T> x, Volume<T> y)
{
Forward(x);
Backward(y);
}

以下截图展示了我们的训练过程:

测试数据

本节详细介绍了Test函数,它将向我们展示如何测试我们已训练的数据。我们通过以下命令获取网络预测并跟踪每个标签的准确性:

private void Test(Volume x, int[] labels, CircularBuffer<double> accuracy, bool forward = true)
{
if (forward)
{

Forward动量可以通过以下代码找到:

this._net.Forward(x);
}
var prediction = this._net.GetPrediction();
for (var i = 0; i < labels.Length; i++)
{

要跟踪accuracy,请输入以下代码:

accuracy.Add(labels[i] == prediction[i] ? 1.0 : 0.0);
}
}

预测数据

在这个例子中,预测数据意味着预测argmax值。为此,我们假设网络的最后一层是一个SoftmaxLayer。当调用GetPrediction()函数时,就会发生预测,如下所示:

public int[] GetPrediction()
{
var softmaxLayer = this._lastLayer as SoftmaxLayer<T>;
if (softmaxLayer == null)
{
throw new Exception("Function assumes softmax as last layer of the net!");
}
var activation = softmaxLayer.OutputActivation;
var N = activation.Shape.Dimensions[3];
var C = activation.Shape.Dimensions[2];
var result = new int[N];
for (varn = 0; n < N; n++)
{
varmaxv = activation.Get(0, 0, 0, n);
varmaxi = 0;
for (vari = 1; i < C; i++)
{
var output = activation.Get(0, 0, i, n);
if (Ops<T>.GreaterThan(output, maxv))
{
maxv = output;
maxi = i;
}
}
result[n] = maxi;
}
return result;
}

计算图

以下是我们基于示例应用创建的两个计算图截图:

图片

图片

摘要

在本章中,我们使用了开源包 ConvNetSharp 来解释卷积神经网络。我们探讨了如何测试和训练这些网络,并学习了为什么它们是卷积的。我们与几个示例应用合作,解释了 ConvNetSharp 如何工作和操作。在下一章中,我们将探讨自编码器和 RNNSharp,进一步向您介绍循环神经网络。

参考文献

第十一章:使用 RNNSharp 训练自动编码器

在本章中,我们将讨论自动编码器及其用法。我们将讨论什么是自动编码器,不同类型的自动编码器,并展示不同的示例,以帮助您更好地理解如何在应用程序中使用这项技术。到本章结束时,您将了解如何设计自己的自动编码器,从磁盘加载和保存它,以及如何训练和测试它。

本章涵盖了以下主题:

  • 什么是自动编码器?

  • 不同类型的自动编码器

  • 创建自己的自动编码器

技术要求

您需要安装 Microsoft Visual Studio。

什么是自动编码器?

自动编码器是一种无监督学习算法,它应用反向传播并设置目标值等于输入。自动编码器学习将输入层的数据压缩成更短的代码,然后将其解压缩成与原始数据非常接近的东西。这更广为人知的是降维

以下是对自动编码器的描述。原始图像被编码,然后解码以重建原始图像:

图片

不同类型的自动编码器

以下介绍了不同类型的自动编码器:

图片

让我们简要讨论自动编码器和我们已经看到的变体。请注意,还有其他变体;这些只是我认为您至少应该熟悉的可能最常见的。

标准自动编码器

自动编码器学习将输入层的数据压缩成更小的代码,然后将该代码解压缩成(希望)与原始数据匹配的东西。标准自动编码器背后的基本思想是自动编码信息,因此得名。整个网络在形状上总是类似于沙漏,其隐藏层比输入层和输出层少。中间层之前的所有内容称为编码部分,中间层之后的所有内容称为解码部分,中间层本身被称为,正如你可能猜到的,代码。您可以通过提供输入数据和设置错误状态为输入和输出之间的差异来训练自动编码器。可以构建自动编码器,使得编码权重和解码权重相同。

变分自动编码器

变分自编码器与自编码器具有相同的架构,但它们学习的内容不同:输入样本的近似概率分布。这是因为它们与玻尔兹曼机和受限玻尔兹曼机有更紧密的联系。然而,它们确实依赖于贝叶斯数学以及一个重新参数化技巧来实现这种不同的表示。基本原理可以归结为:考虑影响。如果在一处发生某事,而在另一处发生另一件事,它们不一定相关。如果它们不相关,那么错误传播应该考虑这一点。这是一个有用的方法,因为神经网络是大型图(在某种程度上),所以当你深入到更深的层时,能够排除某些节点对其他节点的影响是有帮助的。

去噪自编码器

去噪自编码器从自身被损坏的版本中重建输入。这做了两件事。首先,它试图编码输入,同时尽可能保留信息。其次,它试图撤销损坏过程的影响。在随机移除一定比例的数据后,输入被重建,这迫使网络学习鲁棒的特征,这些特征往往具有更好的泛化能力。去噪自编码器是我们在输入数据中添加噪声(例如使图像更粗糙)的自编码器。我们以相同的方式计算误差,因此网络的输出与原始无噪声输入进行比较。这鼓励网络学习不是细节而是更广泛的特征,这些特征通常更准确,因为它们不受不断变化的噪声的影响。

稀疏自编码器

稀疏自编码器在某种程度上是自编码器的对立面。不是教网络以更少的空间或节点表示信息,而是尝试在更多的空间中编码信息。不是网络在中间收敛然后扩展回输入大小,而是将中间部分膨胀。这类网络可以用来从数据集中提取许多小的特征。如果你以与自编码器相同的方式训练稀疏自编码器,你几乎在所有情况下都会得到一个相当无用的恒等网络(即输入的是什么,输出的也是什么,没有任何转换或分解)。为了防止这种情况,我们反馈输入,以及所谓的稀疏驱动器。这可以采取阈值滤波器的形式,其中只有一定错误的误差被反馈并训练。其他错误对于那次传递将是无关紧要的,并将被设置为零。在某种程度上,这类似于脉冲神经网络,其中不是所有神经元都始终在放电。

创建自己的自编码器

现在你已经成为自动编码器的专家,让我们转向更少的理论,更多的实践。让我们在这件事上采取一条不同的路线。而不是使用开源包并展示如何使用它,让我们编写我们自己的自动编码器框架,你可以增强它来制作自己的。我们将讨论和实现所需的基本组件,然后编写一些示例代码来展示如何使用它。我们将使这一章节独特,因为我们不会完成使用示例;我们只会做足够的,让你开始自己的自动编码器创建之路。考虑到这一点,让我们开始。

让我们从思考自动编码器是什么以及我们想要包含哪些内容开始。首先,我们需要跟踪我们有多少层。这些层肯定是受限玻尔兹曼机。只是让你知道,当我们需要简洁时,我们也会将 受限玻尔兹曼机 简称为 RBMs

因此,我们知道我们需要跟踪我们的自动编码器有多少层。我们还将需要监控我们将需要使用的权重:学习率、识别权重和生成权重。当然,训练数据很重要,错误也是如此。我认为现在应该就足够了。让我们创建一个类来专门做这件事。

让我们从创建一个 interface 开始,我们将使用它来计算错误。我们只需要一个方法,这个方法会为我们计算错误。RBM 将负责做这件事,但我们会稍后讨论:

public interface IErrorObserver
{
void CalculateError(double PError);
}

在我们定义我们的 RBM 类之前,我们需要查看它将使用的层。为了最好地表示这一点,我们将创建一个 abstract 类。我们需要跟踪层的状态、使用的偏置、偏置变化量、活动本身以及它将有多少个神经元。我们不会区分镜像神经元和规范神经元,而将所有神经元类型表示为一个单一的对象。我们还需要有多个类型的 RBM 层。高斯和二进制是两个可以想到的类型,所以以下将是这些层的基类:

public abstract class RestrictedBoltzmannMachineLayer
{
protected double[] state;
protected double[] bias;
protected double[] biasChange;
protected double[] activity;
protected int numNeurons = 0;
}

我们必须记住,我们的 RBM 需要跟踪其权重。由于权重是通过称为 突触 的层应用的,我们将创建一个类来表示我们想要对权重做的所有事情。由于我们需要跟踪权重、它们的变化以及前向和后向大小,让我们创建一个封装所有这些内容的类:

public class RestrictedBoltzmannMachineWeightSet
{
private int preSize;
private int postSize;
private double[][] weights;
private double[][] weightChanges;
}

接下来,由于我们的学习率包括权重、偏置和动量等特征,我们最好创建一个单独的类来表示所有这些:

public struct RestrictedBoltzmannMachineLearningRate
{
internal double weights;
internal double biases;
internal double momentumWeights;
internal double momentumBiases;
}

最后,让我们创建一个包含我们的训练数据的类:

public struct TrainingData
{
public double[] posVisible;
public double[] posHidden;
public double[] negVisible;
public double[] negHidden;
}

在所有这些都定义好之后,让我们继续工作在我们的 RestrictedBoltzmannMachine 类上。对于这个类,我们需要跟踪我们有多少个可见层和隐藏层,我们将使用的权重和学习率,以及我们的训练数据:

public class RestrictedBoltzmannMachine
{
private RestrictedBoltzmannMachineLayer visibleLayers;
private RestrictedBoltzmannMachineLayer hiddenLayers;
private RestrictedBoltzmannMachineWeightSet weights;
private RestrictedBoltzmannMachineLearningRate learnrate;
private TrainingData trainingdata;
private int numVisibleLayers;
private int numHiddenLayers;
}

最后,在所有其他内容都就绪之后,让我们创建我们的 Autoencoder 类:

public class Autoencoder
{
private int numlayers;
private bool pretraining = true;
private RestrictedBoltzmannMachineLayer[] layers;
private AutoencoderLearningRate learnrate;
private AutoencoderWeights recognitionweights;
private AutoencoderWeights generativeweights;
private TrainingData[] trainingdata;
private List<IErrorObserver> errorobservers;
}

尽管我们知道这些类中的一些将需要更多的功能,但这是我们开始构建其余代码所需的基本框架。为了做到这一点,我们应该考虑一些事情。

由于权重是我们自动编码器的一个突出方面,我们将不得不经常使用和初始化权重。但是我们应该如何初始化我们的权重,以及使用什么值?我们将提供两种不同的选择。我们将要么将所有权重初始化为零,要么使用高斯分布。我们还将不得不初始化偏差。让我们继续创建一个接口来完成这项工作,这样以后选择我们想要的初始化类型(零或高斯)会更容易:

public interface IWeightInitializer
{
double InitializeWeight();
double InitializeBias();}

我们之前提到,我们需要有多个类型的 RBM 层来使用。高斯和二进制是两个想到的类型。我们已经为这个创建了接口,所以让我们继续将我们的基类放入形式,因为我们很快就会需要它们。为此,我们需要扩展我们的 RBM 层类并添加两个抽象方法,这样它们就可以被克隆,并且我们可以设置层的状态:

public abstract void SetState(int PWhich, double PInput);
public abstract object Clone();

我们的RestrictedBoltzmannMachineLayer类现在看起来是这样的:

public abstract class RestrictedBoltzmannMachineLayer
{
protected double[] state;
protected double[] bias;
protected double[] biasChange;
protected double[] activity;
protected int numNeurons = 0;
public abstract void SetState(int PWhich, double PInput);
public abstract object Clone();
}

在我们建立了非常基础的自动编码器之后,我们现在应该将注意力转向我们如何构建自动编码器。让我们尽量使事物尽可能模块化,并考虑到这一点,让我们创建一个AutoEncoderBuilder类,它可以封装诸如权重初始化、添加层等事物。它看起来可能如下所示:

public class AutoencoderBuilder
{
private List<RestrictedBoltzmannMachineLayer> layers = new List<RestrictedBoltzmannMachineLayer>();
private AutoencoderLearningRate learnrate = new AutoencoderLearningRate();
private IWeightInitializer weightinitializer = new GaussianWeightInitializer();
}

现在我们已经将这个类建立起来,让我们开始通过函数的形式给它添加一些内容。我们知道当我们构建一个自动编码器时,我们将需要添加层。我们可以用这个函数来做。我们将传递层,然后更新我们内部的学习率层:

private void AddLayer(RestrictedBoltzmannMachineLayer PLayer)
{
learnrate.preLearningRateBiases.Add(0.001);
learnrate.preMomentumBiases.Add(0.5);
learnrate.fineLearningRateBiases.Add(0.001);
if (layers.Count >= 1)
{
learnrate.preLearningRateWeights.Add(0.001);
learnrate.preMomentumWeights.Add(0.5);
learnrate.fineLearningRateWeights.Add(0.001);
}
layers.Add(PLayer);
}

一旦我们有了这个基本函数,我们就可以添加一些更高级的函数,这将使我们更容易向自动编码器添加层:

public void AddBinaryLayer (int size)
{
AddLayer (new RestrictedBoltzmannMachineBinaryLayer(size));
}
public void AddGaussianLayer (int size)
{
AddLayer (new RestrictedBoltzmannMachineGaussianLayer(size));
}

最后,让我们在我们的自动编码器构建器中添加一个Build()方法,使其更容易构建:

public Autoencoder Build()
{
return new Autoencoder(layers, learnrate, weightinitializer);
}

现在,让我们将注意力转向我们的自动编码器本身。我们需要一个函数来帮助我们初始化偏差:

private void InitializeBiases(IWeightInitializer PWInitializer)
{
for (int i = 0; i < numlayers; i++)
{
for (int j = 0; j < layers[i].Count; j++)
{
layers[i].SetBias(j, PWInitializer.InitializeBias());
}
}
}

接下来,我们需要初始化我们的训练数据。这基本上涉及到创建我们需要的所有数组并将它们的初始值设置为零,如下所示:

private void InitializeTrainingData()
{
trainingdata = new TrainingData[numlayers - 1];
for (inti = 0; i < numlayers - 1; i++)
{
trainingdata[i].posVisible = new double[layers[i].Count];
Utility.SetArrayToZero(trainingdata[i].posVisible);
trainingdata[i].posHidden = new double[layers[i + 1].Count];
Utility.SetArrayToZero(trainingdata[i].posHidden);
trainingdata[i].negVisible = new double[layers[i].Count];
Utility.SetArrayToZero(trainingdata[i].negVisible);
trainingdata[i].negHidden = new double[layers[i + 1].Count];
Utility.SetArrayToZero(trainingdata[i].negHidden);
}
}

在这之后,我们已经有一个良好的开端。让我们开始使用软件,看看我们缺少什么。让我们创建我们的builder对象,添加一些二进制和高斯层,看看效果如何:

AutoencoderBuilder builder = new AutoencoderBuilder();
builder.AddBinaryLayer(4);
builder.AddBinaryLayer(3);
builder.AddGaussianLayer(3);
builder.AddGaussianLayer(1);

还不错,对吧?那么接下来是什么?嗯,我们已经创建了自动编码器并添加了层。我们现在缺少允许我们微调和训练学习率和动量的函数。让我们看看如果我们按照以下方式添加它们会是什么样子:

builder.SetFineTuningLearningRateBiases(0, 1.0);
builder.SetFineTuningLearningRateWeights(0, 1.0);
builder.SetPreTrainingLearningRateBiases(0, 1.0);
builder.SetPreTrainingLearningRateWeights(0, 1.0);
builder.SetPreTrainingMomentumBiases(0, 0.1);
builder.SetPreTrainingMomentumWeights(0, .05);

看起来差不多。在这个阶段,我们应该将这些函数添加到我们的autoencoderbuilder对象中,以便我们可以使用它们。让我们看看那会是什么样子。记住,随着我们的 builder 对象,我们自动创建了学习率对象,所以现在我们只需要使用它来填充诸如权重和偏差、动量权重和偏差等东西:

public void SetPreTrainingLearningRateWeights(int PWhich, double PLR)
{
learnrate.preLearningRateWeights[PWhich] = PLR;
}
public void SetPreTrainingLearningRateBiases(int PWhich, double PLR)
{
learnrate.preLearningRateBiases[PWhich] = PLR;
}
public void SetPreTrainingMomentumWeights(int PWhich, double PMom)
{
learnrate.preMomentumWeights[PWhich] = PMom;
}
public void SetPreTrainingMomentumBiases(int PWhich, double PMom)
{
learnrate.preMomentumBiases[PWhich] = PMom;
}
public void SetFineTuningLearningRateWeights(int PWhich, double PLR)
{
learnrate.fineLearningRateWeights[PWhich] = PLR;
}
public void SetFineTuningLearningRateBiases(int PWhich, double PLR)
{
learnrate.fineLearningRateBiases[PWhich] = PLR;
}

好吧,现在让我们停下来看看我们的示例程序看起来像什么:

AutoencoderBuilder builder = new AutoencoderBuilder();
builder.AddBinaryLayer(4);
builder.AddBinaryLayer(3);
builder.AddGaussianLayer(3);
builder.AddGaussianLayer(1);
builder.SetFineTuningLearningRateBiases(0, 1.0);
builder.SetFineTuningLearningRateWeights(0, 1.0);
builder.SetPreTrainingLearningRateBiases(0, 1.0);
builder.SetPreTrainingLearningRateWeights(0, 1.0);
builder.SetPreTrainingMomentumBiases(0, 0.1);
builder.SetPreTrainingMomentumWeights(0, .05);

还不错。现在我们只需要在builder上调用我们的Build()方法,就应该有我们框架的第一版了:

Autoencoder encoder = builder.Build();

现在所有这些都已完成,回顾前面的代码,我认为在某个时候我们将需要能够访问我们的单个层;你怎么看?以防万一,我们最好提供一个函数来做这件事。让我们看看那会是什么样子:

RestrictedBoltzmannMachineLayer layer = encoder.GetLayer(0);
RestrictedBoltzmannMachineLayer layerHidden = encoder.GetLayer(1);

由于我们的内部层是RestrictedBoltzmannMachine层,这就是我们应该返回的类型,正如您可以从前面的代码中看到的那样。GetLayer()函数需要位于自动编码器对象内部,而不是 builder。所以,让我们现在就添加它。我们需要成为优秀的开发者,确保我们在尝试使用它之前,有一个边界检查来确保我们传递了一个有效的层索引。我们将所有这些小巧的实用函数存储在一个自己的类中,我们可以称之为Utility,因为这个名字是有意义的。我不会深入讲解如何编写这个函数,因为我相当确信每个读者都已经知道如何进行边界检查,所以你可以自己编写或者查看附带的源代码来了解在这个例子中是如何实现的:

public RestrictedBoltzmannMachineLayer GetLayer(int PWhichLayer)
{
Utility.WithinBounds("Layer index out of bounds!", PWhichLayer, numlayers);
return layers[PWhichLayer];
}

好的,现在我们可以创建我们的自动编码器,设置权重和偏差,并访问单个层。我认为接下来我们需要开始考虑的是训练和测试。当然,我们需要分别处理它们,那么为什么不从训练开始呢?

我们需要能够训练我们的 RBM,那么为什么不创建一个专门用于此的对象呢。我们将它命名为,不出所料,RestrictedBoltzmannMachineTrainer。再次强调,我们还需要处理我们的LearningRate对象和权重集,所以让我们立即将它们作为变量添加:

public static class RestrictedBoltzmannMachineTrainer
{
private static RestrictedBoltzmannMachineLearningRate learnrate;
private static RestrictedBoltzmannMachineWeightSet weightset;
}

现在,你认为我们需要为我们的训练器添加哪些功能呢?显然,需要一个Train()方法;否则,我们给对象命名就不正确了。我们还需要训练我们的权重和层偏差:

private static void TrainWeight(int PWhichVis, int PWhichHid, double PTrainAmount);
private static void TrainBias(RestrictedBoltzmannMachineLayer PLayer, int PWhich, double PPosPhase, double PNegPhase);

最后,但同样重要的是,我们可能需要一个helper函数,它让我们知道训练量,对我们来说,这涉及到将正可见量乘以正隐藏量,然后从负可见量乘以负隐藏量中减去:

private static double CalculateTrainAmount(double PPosVis, double PPosHid, double PNegVis, double PNegHid)
{
return ((PPosVis * PPosHid) - (PNegVis * PNegHid));
}

好的,让我们看看我们的程序现在处于什么位置:

AutoencoderBuilder builder = new AutoencoderBuilder();
builder.AddBinaryLayer(4);
builder.AddBinaryLayer(3);
builder.AddGaussianLayer(3);
builder.AddGaussianLayer(1);
builder.SetFineTuningLearningRateBiases(0, 1.0);
builder.SetFineTuningLearningRateWeights(0, 1.0);
builder.SetPreTrainingLearningRateBiases(0, 1.0);
builder.SetPreTrainingLearningRateWeights(0, 1.0);
builder.SetPreTrainingMomentumBiases(0, 0.1);
builder.SetPreTrainingMomentumWeights(0, .05);
Autoencoder encoder = builder.Build();
RestrictedBoltzmannMachineLayer layer = encoder.GetLayer(0);
RestrictedBoltzmannMachineLayer layerHidden = encoder.GetLayer(1);

很好。你能看到所有这些是如何开始整合的吗?现在是我们考虑如何将数据添加到我们的网络中的时候了。在我们对网络进行任何类型的训练之前,我们需要加载数据。我们将如何做?让我们考虑预训练的概念。这是在我们训练之前手动将数据加载到网络中的行为。在我们的程序上下文中,这个函数会是什么样子?比如这样?

encoder.PreTrain(0, new double[] {0.1, .05, .03, 0.8});

我们只需要告诉我们的自动编码器我们想要用数据填充哪一层,然后提供数据。这应该对我们有效。如果我们这样做,那么我们的程序将像这样发展:

AutoencoderBuilder builder = new AutoencoderBuilder();
builder.AddBinaryLayer(4);
builder.AddBinaryLayer(3);
builder.AddGaussianLayer(3);
builder.AddGaussianLayer(1);
builder.SetFineTuningLearningRateBiases(0, 1.0);
builder.SetFineTuningLearningRateWeights(0, 1.0);
builder.SetPreTrainingLearningRateBiases(0, 1.0);
builder.SetPreTrainingLearningRateWeights(0, 1.0);
builder.SetPreTrainingMomentumBiases(0, 0.1);
builder.SetPreTrainingMomentumWeights(0, .05);
Autoencoder encoder = builder.Build();
RestrictedBoltzmannMachineLayer layer = encoder.GetLayer(0);
RestrictedBoltzmannMachineLayer layerHidden = encoder.GetLayer(1);
encoder.PreTrain(0, new double[] {0.1, .05, .03, 0.8});
encoder.PreTrain(1, new double[] { 0.1, .05, .03, 0.9 });
encoder.PreTrain(2, new double[] { 0.1, .05, .03, 0.1 });
encoder.PreTrainingComplete();

到目前为止,你怎么看?有了这个代码,我们就能用数据填充三个层。我加入了一个额外的函数PreTrainingComplete,作为一种让我们的程序知道我们已经完成了预训练的好方法。现在,让我们弄清楚这些函数是如何结合在一起的。

对于预训练,我们将分批进行。我们可以有从 1 到n个批次的数量。在许多情况下,批次的数量将是 1。一旦我们确定我们想要使用的批次数量,我们将遍历每一批数据。

对于每一批数据,我们将处理数据并确定我们的神经元是否被激活。然后我们根据这个设置层状态。我们将向前和向后通过网络,设置我们的状态。使用以下图表,我们将像这样通过层 Y -> V -> W -> (Z)

图片

一旦激活被设置,我们必须执行实际的预训练。我们在预突触层中这样做,从层 0 开始。当我们预训练时,我们调用我们之前创建的培训对象Train方法,然后传递层(s)、训练数据、我们的识别权重和学习率。为此,我们需要创建我们的实际函数,我们将称之为PerformPreTraining()。以下是这个代码的样子:

private void PerformPreTraining(int PPreSynapticLayer)
{
RestrictedBoltzmannMachineLearningRate sentlearnrate = new RestrictedBoltzmannMachineLearningRate(learnrate.preLearningRateWeights[PPreSynapticLayer],learnrate.preLearningRateBiases[PPreSynapticLayer],learnrate.preMomentumWeights[PPreSynapticLayer],learnrate.preMomentumBiases[PPreSynapticLayer]);RestrictedBoltzmannMachineTrainer.Train(layers[PPreSynapticLayer], layers[PPreSynapticLayer + 1],trainingdata[PPreSynapticLayer], sentlearnrate, recognitionweights.GetWeightSet(PPreSynapticLayer));
}

一旦预训练完成,我们现在将需要根据正负可见数据属性计算错误率。这将完成我们的pretraining函数,我们的示例程序现在将如下所示:

AutoencoderBuilder builder = new AutoencoderBuilder();
builder.AddBinaryLayer(4);
builder.AddBinaryLayer(3);
builder.AddGaussianLayer(3);
builder.AddGaussianLayer(1);
builder.SetFineTuningLearningRateBiases(0, 1.0);
builder.SetFineTuningLearningRateWeights(0, 1.0);
builder.SetPreTrainingLearningRateBiases(0, 1.0);
builder.SetPreTrainingLearningRateWeights(0, 1.0);
builder.SetPreTrainingMomentumBiases(0, 0.1);
builder.SetPreTrainingMomentumWeights(0, .05);
Autoencoder encoder = builder.Build();
RestrictedBoltzmannMachineLayer layer = encoder.GetLayer(0);
RestrictedBoltzmannMachineLayer layerHidden = encoder.GetLayer(1);
encoder.PreTrain(0, new double[] {0.1, .05, .03, 0.8});
encoder.PreTrain(1, new double[] { 0.1, .05, .03, 0.9 });
encoder.PreTrain(2, new double[] { 0.1, .05, .03, 0.1 });
encoder.PreTrainingComplete();

在我们有了所有这些代码之后,我们现在需要做的就是保存自动编码器,我们应该就绪了。我们将通过在自动编码器中创建一个Save()函数并按如下方式调用它来完成这项工作:

encoder.Save("testencoder.txt");

要实现这个功能,让我们看看我们需要做什么。首先,我们需要一个用于自动编码器名称的文件名。一旦我们打开一个.NET TextWriter对象,我们就保存学习率、识别权重和生成权重。接下来,我们遍历所有层,写出层类型,然后保存数据。如果你决定实现比我们创建的更多类型的 RBM 层,确保你相应地更新Save()Load()方法,以便你的新层数据能够正确保存和重新加载。

让我们看看我们的 Save 函数:

public void Save(string PFilename)
{
TextWriter file = new StreamWriter(PFilename);
learnrate.Save(file);
recognitionweights.Save(file);
generativeweights.Save(file);
file.WriteLine(numlayers);
for (inti = 0; i < numlayers; i++)
{
if(layers[i].GetType() == typeof(RestrictedBoltzmannMachineGaussianLayer))
{
file.WriteLine("RestrictedBoltzmannMachineGaussianLayer");
}
else if (layers[i].GetType() == typeof(RestrictedBoltzmannMachineBinaryLayer))
{
file.WriteLine("RestrictedBoltzmannMachineBinaryLayer");
}
layers[i].Save(file);
}
file.WriteLine(pretraining);
file.Close();
}

现在我们已经将自动编码器保存到磁盘上,我们应该真正处理将数据重新加载到内存中并从中创建自动编码器的功能。因此,我们现在需要一个 Load() 函数。我们需要基本上遵循我们写入磁盘时所做的步骤,但这次我们将读取它们,而不是写入它们。我们的权重、学习率和层也将有一个 Load() 函数,就像前面的每个项目都有一个 Save() 函数一样。

我们的 Load() 函数在声明上会有所不同。由于我们正在加载一个已保存的自动编码器,我们必须假设在调用此函数时,自动编码器对象尚未创建。因此,我们将在这个自动编码器对象本身上将其函数声明为 static(),因为它将为我们返回一个新创建的自动编码器。我们的函数将看起来是这样的:

public static Autoencoder Load(string PFilename)
{
TextReader file = new StreamReader(PFilename);
Autoencoder retval = new Autoencoder();
retval.learnrate = new AutoencoderLearningRate();
retval.learnrate.Load(file);
retval.recognitionweights = new AutoencoderWeights();
retval.recognitionweights.Load(file);
retval.generativeweights = new AutoencoderWeights();
retval.generativeweights.Load(file);
retval.numlayers = int.Parse(file.ReadLine());
retval.layers = new RestrictedBoltzmannMachineLayer[retval.numlayers];
for (inti = 0; i < retval.numlayers; i++)
{
string type = file.ReadLine();
if (type == "RestrictedBoltzmannMachineGaussianLayer")
{
retval.layers[i] = new RestrictedBoltzmannMachineGaussianLayer();
}
else if (type == "RestrictedBoltzmannMachineBinaryLayer")
{
retval.layers[i] = new RestrictedBoltzmannMachineBinaryLayer();
}
retval.layers[i].Load(file);
}
retval.pretraining = bool.Parse(file.ReadLine());
retval.InitializeTrainingData();
retval.errorobservers = new List<IErrorObserver>();
file.Close();
return retval;
}

做完这些后,让我们看看我们如何调用我们的 Load() 函数。它应该像以下这样:

Autoencoder newAutoencoder = Autoencoder.Load("testencoder.txt");

那么,让我们在这里停下来,看看我们取得了哪些成果。让我们看看我们的程序能做什么,如下所示:

AutoencoderBuilder builder = new AutoencoderBuilder();
builder.AddBinaryLayer(4);
builder.AddBinaryLayer(3);
builder.AddGaussianLayer(3);
builder.AddGaussianLayer(1);
builder.SetFineTuningLearningRateBiases(0, 1.0);
builder.SetFineTuningLearningRateWeights(0, 1.0);
builder.SetPreTrainingLearningRateBiases(0, 1.0);
builder.SetPreTrainingLearningRateWeights(0, 1.0);
builder.SetPreTrainingMomentumBiases(0, 0.1);
builder.SetPreTrainingMomentumWeights(0, .05);
Autoencoder encoder = builder.Build();
RestrictedBoltzmannMachineLayer layer = encoder.GetLayer(0);
RestrictedBoltzmannMachineLayer layerHidden = encoder.GetLayer(1);
encoder.PreTrain(0, new double[] {0.1, .05, .03, 0.8});
encoder.PreTrain(1, new double[] { 0.1, .05, .03, 0.9 });
encoder.PreTrain(2, new double[] { 0.1, .05, .03, 0.1 });
encoder.PreTrainingComplete();
encoder.Save("testencoder.txt");
Autoencoder newAutoencoder = Autoencoder.Load("testencoder.txt");

摘要

好吧,朋友们,我认为是时候结束这一章并继续前进了。您应该为自己感到自豪,因为您已经从开始(几乎)完成了完整的自动编码器。在配套的源代码中,我添加了更多函数来使其更加完整,并为您提供一个更好的起点,以便将其构建成一个强大的框架供您使用。在增强这个框架的过程中,请考虑您需要自动编码器执行的任务,将这些函数块在里,然后像我们一样完成它们。您不是学习如何使用开源框架,而是构建了自己的——恭喜您!

我已经利用提供的源代码对我们的自动编码器框架进行了更多的发展。您可以自由使用、丢弃或修改它以满足您的需求。它很有用,但正如我提到的,请随意对其进行装饰,使其成为您自己的东西,即使只是为了教育目的。

那么,让我们简要回顾一下本章我们学到了什么:我们学习了自动编码器和不同的变体,我们编写了自己的自动编码器并创建了一些强大的功能。在下一章,我们将继续探讨我最大的热情,我希望它很快也会成为您的热情,那就是群体智能。当然,会有一些理论,但一旦我们讨论了这些,我想你会对粒子群优化算法能做什么感到印象深刻!

参考文献

  • Vincent P, La Rochelle H, Bengio Y, 和 Manzagol P A (2008),使用去噪自动编码器提取和组合鲁棒特征,第 25 届国际机器学习会议(ICML,2008)论文集,第 1,096 - 1,103 页,ACM。

  • Vincent, Pascal, 等人,使用去噪自动编码器提取和组合鲁棒特征,第 25 届国际机器学习会议论文集。ACM,2008。

  • Kingma, Diederik P 和 Max Welling*, 自编码变分贝叶斯, arXiv 预印本 arXiv:1312.6114 (2013).

  • Marc'Aurelio Ranzato, Christopher Poultney, Sumit Chopra, 和 Yann LeCun, 基于能量模型的稀疏表示的高效学习,NIPS 会议论文,2007.

  • Bourlard, Hervé, 和 Yves Kamp, 多层感知器和奇异值分解的多层感知器自联想,生物控制论 59.4–5 (1988): 291-294.

第十二章:将反向传播替换为 PSO

神经网络领域最新的成功案例之一是被称为群智能的研究领域。尽管这个研究领域已经存在多年,但计算机硬件的进步以及我们对动物研究的理解帮助我们把这个迷人的领域从实验室带到了许多不同的方向和现实世界场景中。

在本章中,我们将涵盖以下内容:

  • 粒子群优化(PSO)的基本理论

  • 开源机器学习框架 Encog

  • 将传统的反向传播替换为 PSO

技术要求

你将需要 Microsoft Visual Studio,也可能需要参考 github.com/encog

查看以下视频以查看代码的实际应用: bit.ly/2QPd6Qo

基本理论

好的,现在进行一个小测验时间。一群鸟、一群鱼和一群蜜蜂有什么共同之处?群智能——知道如何合作地生活和工作在一起,同时最优地实现相同的目标。这并不是关于个体的智能,而是群体的成就。没有哪个个体有明确的路径或指令,没有人处于最高层下达命令,然而最优目标总是能够实现。蜜蜂通过摇摆舞找到新的巢穴。鸟儿以极大的和谐飞行,轮流担任领导者。鱼儿以我们称之为鱼群的美丽架构集体游泳。但如果我们作为人类总是需要有人处于最高层下达命令,而我们集体又并不总是意见一致,那么这些小小的生物是如何年复一年地做到这一点的,而我们却做不到呢?哎呀,我跑题了,抱歉!

让我们从一些快速的定义开始,这些定义将在整个过程中使用,以确保我们都在同一页面上继续前进。

群智能

群智能是自组织系统的集体行为,本质上具有去中心化的特点。群体本身表现出社会认知行为,并实现单个贡献者无法单独实现的目标。集体实现目标,而不是任何单个贡献者的努力。这引导我们到 PSO 本身。

粒子群优化

粒子群优化是一种方法(一种基于群体的算法),通过迭代优化问题,同时尝试改进关于其最优质量的潜在解决方案。PSO 算法中的每一个粒子都会从自身和另一个具有良好适应度的粒子那里学习。每个粒子,代表一个解决方案,以动态调整的速度在搜索空间中飞行,这种速度根据其自身及其伴侣的历史行为进行调整。在搜索过程中,粒子倾向于飞向更好的搜索区域。

粒子群优化的类型

以下是一些粒子群优化算法的变体:

  • 传统粒子群优化

  • 标准粒子群优化

  • 完全信息粒子群优化

现在我们来简单谈谈群智能背后的理论,然后我们将进入该领域两个更专业的研究类型:粒子群优化和蚂蚁群优化,这两种优化方法都是直接替代反向传播的解决方案!

无论你发现这多么迷人且引人入胜,请记住,没有什么是完美的,也没有任何一种万能的解决方案适用于所有情况。这是一个迷人的研究领域,关于这个主题已经写出了整本书。然而,我们始终需要牢记优化中的无免费午餐定理

优化中的无免费午餐定理指出,没有人可以提出任何一种特定的算法来解决所有优化问题。一个算法在解决某一特定问题上的成功并不能保证它能解决所有优化问题。更具体地说,如果考虑所有优化问题,那么所有优化技术平均表现都相同,尽管在问题子集上的表现可能不同。

在一篇非常优秀的论文《移动设备中粒子群优化的时间性能比较》中,由 Luis Antonio Beltrán Prieto、Zuzana Komínkova-Oplatková、Rubén Torres Frías 和 Juan Luis Escoto Hernández 撰写,对粒子群优化进行了如下描述:

"PSO 是一种由 Kennedy 和 Eberhart 开发的优化技术,它受到动物群体集体行为(如昆虫群)的启发,构建了一群粒子,即一组候选解,这些解在参数空间中流动,生成由最佳个体驱动的轨迹。初始种群(群体)由问题的随机解(粒子)组成,被视为一个同质代理种群,它们与其他个体进行局部交互,没有任何中央控制。因此,产生了集体行为,进化依赖于个体之间的合作和竞争,通过不同的时代(代)。每个粒子根据运动函数在参数空间中定义轨迹,该函数受速度、惯性、认知系数和社会系数的影响。目标是通过对上述元素进行随机加权来找到全局最佳解。该过程是迭代的,直到满足停止标准。"

粒子群优化的更直观类比是鸟类如何协同合作,或者蜜蜂群如何决定访问哪些花朵或攻击哪些人类!如果你曾经观察过一群鸟飞翔或者无意中撞倒了一个蜂巢,那么你就知道我指的是什么了。

现在,让我们不再只处理理论,而是进行一次假设的旅行,一次寻宝活动。我将故意使它尽可能详细,以确保类比适合问题空间。它大致是这样的。

你和你的几个朋友在一个多山地区试图找到价值连城的隐藏宝藏。我们并不确定宝藏在哪里,但我们知道它在地区的最深处山谷中。这在海拔高度方面等同于最低点。

让我们再声明一下,我们所有的朋友都可以使用他们的手机相互通信(让我们假设我们这里有手机信号!)。现在也假设我们的手机上装有 GPS 应用,告诉我们我们当前所在的高度。我们将每天寻找宝藏,直到找到它,并在每天结束时,我们要么找到了宝藏并且变得富有,要么我们需要更新我们的信息并在第二天再次尝试。所以,每个人都有:

  • 一部带有 GPS 应用的手机来确定高度。

  • 使用笔和纸在每天结束时跟踪我们的信息。在这张纸上,我们将写下我们找到的最佳位置(个人最佳),这是我们个人的最佳,或称为PBEST。我们还将写下迄今为止整个团队找到的最佳位置,这是我们全局最佳值或GBEST

以下是我们搜索中需要遵循的规则:

  • 每个人将从随机位置和随机方向开始。我们立即确定我们的高度并将它写在我们的纸上。如果每个人尽可能分散,对我们来说会更好,这样我们可以更有效率地覆盖更多地面,但这不是必需的。

  • 我们的旅程将持续T天,到目前为止,我们已经知道这个值是什么或将会是什么。

  • 每天早上,我们将计划我们的日程。

  • 通信只能在每天结束时进行。

  • 每天早上,每个人比较他们所在的高度,并在他们的纸上更新GBEST

  • GBEST是每个人可以分享的唯一信息(位置和高度)。

  • 每个人如果找到更好的位置,都会更新他们论文上的PBEST

  • PBEST信息不共享;没有人关心除了GBEST之外的其他任何事情。

  • 记住这一点;为了每天移动,每个人(例如)在昨天的方向上走x步,向PBEST的方向走y步,以及向GBEST的方向走z步。困惑了吗?

  • 步骤是随机的,因为我们需要在搜索中引入某种形式的随机性,以便为每个人作为一个集体群体(即一群人或一群人)形成一个随机搜索模式。

在掌握这些规则之后,我们可以开始我们的寻宝之旅。作为一个集体,团队将不断定位不同的区域,同时观察迄今为止找到的GBEST位置。当然,我们无法保证找到宝藏,或者以最少的日期找到它,但一般来说,我们的搜索应该是有效的。记住,没有个人知道宝藏的确切位置,但他们与群体合作,发展集体智慧,以帮助更快地找到宝藏。当然,这比完全随机的搜索要好得多!

让我们尝试用伪代码绘制我们的步骤:

  1. 初始化一组随机解。对于 x 个决策变量,我们有一个 x 维空间,我们的解作为粒子存在于其中。每个粒子有 n 个变量,并存储自身和团队的最佳适应度。

  2. 对于每个迭代(无论是数字还是适应度值),计算适应度并存储最佳适应度变量(PBEST),并将其传达给群体。

  3. 通过比较我们从集体群体中收到的所有信息来识别GBEST

  4. 确定在考虑我们的PBESTGBEST的情况下,将我们引向GBEST方向的因素。

  5. 在特定的时间步长内向速度向量的方向移动。

  6. 随着时间的推移,每个团队成员(群体中的粒子)将识别更好的GBEST变量,并朝着它们导航,从而同时提高他们的PBEST

在粒子群优化中,我们有三个基本组件,我们应该简要讨论。它们没有特定的顺序:

  • 位置:类似于前一个类比中的位置,指的是参数值。这指的是粒子(鸟、蜜蜂、鱼等)在 x 维搜索空间中的位置。

  • 速度:类似于前一个类比中的移动方向,用于存储速度,这将更新每个粒子的位置。

  • 适应度:类似于前一个类比中的海拔,这显示了粒子的适应度。

速度是我们粒子群优化算法的主要部分。它考虑了粒子的当前位置、群体找到的最佳位置(GBEST)(所有粒子)以及当前粒子的最佳位置(PBEST)。从数学上讲,它可以分解如下:

还有三个超参数我们应该提及,因为你将经常听到它们。

惯性权重W):惯性权重控制先前历史速度对当前速度的影响。它调节全局和局部探索能力之间的权衡。如果惯性高,粒子在改变方向时受到限制,因此转弯速度较慢。这意味着更大的探索区域和更少的可能性收敛到最优解。

如果惯性小,则从上一个时间步中只有少量动量存在;这允许方向发生更快的变化。这里的问题是,它可能需要相当长的时间才能收敛。

通过降低惯性权重,更容易获得更好的全局搜索能力,并使粒子更早地进入最佳值区域。这意味着它将更容易拥有更好的搜索能力和最佳值。

  • C1: 认知智能

  • C2: 社会智能

应该注意的是,C1 和 C2 是控制单个粒子在单次迭代中可以移动多远的正常数。较低的值将允许粒子在重新控制之前远离目标区域。较高的值将导致向目标区域或超过目标区域的更短、更突然的运动。默认情况下,我们将这两个值都设置为 2.0。

你应该尝试实验认知智能和社会智能的值,因为有时不同的值会导致性能提升。

原始粒子群优化策略

随着粒子(蜜蜂、鸟类、鱼类、白蚁)在预先指定的搜索空间中移动以确定最佳位置,在循环的每次迭代中(其中“循环”可能被称为“最大迭代次数”),每个粒子都会更新其速度和位置。一旦确定了新的速度,它就会用于计算下一个时间步的粒子新位置。

粒子群优化搜索策略

对于每个粒子随时间的变化,我们将跟踪惯性(当前速度)、个人最佳(称为 PBEST)和全局最佳(称为GBEST)。正如我们提到的,随着我们通过时间移动到全局最小值,我们将跟踪个人最佳位置以及群体的全局最佳位置。这些信息将被传达给其他成员,以便在每次迭代完成后将群体的最佳位置信息传达回群体。我们需要要么跟随群体,要么领导群体,才能实现我们的目标。

粒子群优化搜索策略伪代码

以下是我们将使用的伪代码逻辑,用于找到我们的全局最小值(宝藏的位置):

Initialize our hyperparameters
Initialize the population of particles
Do
For each particle
Calculate the objective
Update PBEST Update GBEST End for
Update Inertia weight
For each particle
Update Velocity (V)
Update Position (X)
End for
While the end condition is not satisfied
Return GBEST as the best global optimum estimation

参数对优化的影响

对于粒子群优化中的每个变量应该是什么样子,有许多不同的理论。有理论上的可接受值,然后有时间测试确定的值。以下是我为你提供的建议中的一些。

粒子群优化算法的原始(规范)版本分别使用惯性、C1 和 C2 的值为 1、2 和 2。这些值似乎工作得相当好。我也通过测试发现,其他人也有同样的发现,即使用 0.5、1.5 和 1.5 的值效果甚至更好,这取决于所使用的函数和策略,提供了最佳收敛速度。其他值会导致收敛速度变慢或完全无法收敛。您,作为读者,应该根据您喜欢的策略和函数进行自己的测试,并确定哪些值适合您的目的。

请注意,根据您选择的策略和函数,您的值应该不同,以提供适当的收敛。例如,使用最小化策略和步进函数,我观察到使用全局惯性值为 0.729 时可以实现最佳收敛。认知智能(C1)通常与社交智能(C2)相同,预定的值为 2。然而,我应该指出的是,当我们到达构建和使用可视化工作台章节时,我用于 C1 和 C2 的默认值是 1.49445。

重要的是要注意,这里显示的任何值都不是凭空想出来的。它们来自大量的优化测试。此外,它们与 Clerc 和 Kennedy(2002)为实施收缩系数所测试的值非常接近。请随意使用您自己的值,并始终牢记无免费午餐定理。

以下是一个示例,说明群优化如何受到权重、社会和认知参数的影响:

迭代=31,w=0,c1=c2=2 迭代=31,w=0.59,c1=c2=2 迭代=31,w=1,c1=c2=2

用粒子群优化算法替换反向传播

现在我们来到了真相的时刻。这一切如何应用到我的代码中呢?为了回答这个问题,我们将使用开源的 Encog 机器学习框架进行我们的下一个演示。您可以按照书籍中文件的网络位置说明下载我们的示例项目。请确保在继续之前,您已经在 Visual Studio 中加载并打开它:

我们将创建一个示例应用程序,演示如何用粒子群优化算法替换反向传播。如果一切顺利,从外部看进来,你不会注意到任何区别。

您将能够直接运行此示例并跟随。我们将使用 XOR 问题求解器,但将使用我们一直在讨论的粒子群优化,而不是反向传播。让我们更深入地看看代码。以下是我们将用于实现此示例的数据:

/// Input for the XOR function.
public static double[][] XORInput = {new[] {0.0, 0.0},new[] {1.0, 0.0},new[] {0.0, 1.0},new[] {1.0, 1.0}};
/// Ideal output for the XOR function.
public static double[][] XORIdeal = {new[] {0.0},new[] {1.0},new[] {1.0},new[] {0.0}};

非常直接。

现在让我们来看看这个示例应用本身。以下是如何实现 XORPSO 的:

///Create a basic training data set using the supplied data shown above
IMLDataSet trainingSet = new BasicMLDataSet(XORInput, XORIdeal);
///Create a simple feed forward network
BasicNetworknetwork = EncogUtility.SimpleFeedForward(2, 2, 0, 1, false);
///Create a scoring/fitness object
ICalculateScore score = new TrainingSetScore(trainingSet);
///Create a network weight initializer
IRandomizer randomizer = new NguyenWidrowRandomizer();
///Create the NN PSO trainer. This is our replacement function from back prop
IMLTrain train = new NeuralPSO(network, randomizer, score, 20);
///Train the application until it reaches an error rate of 0.01
EncogUtility.TrainToError(train, 0.01);
network = (BasicNetwork)train.Method;
///Print out the results
EncogUtility.Evaluate(network, trainingSet);

当我们在这里运行这个示例应用时,看起来是这样的。你会注意到,从外部看,它看起来与正常的 XOR 示例完全一样:

图片

你会注意到,当训练完成时,我们非常接近我们的理想分数。

现在,让我们谈谈内部结构。让我们看看一些用于使这起作用的内部变量。以下是你将看到我们为什么在早期花费时间在基本理论上的地方。现在所有这些都应该对你很熟悉。

声明变量 m_populationSize。对于许多问题,一个典型的范围是 20 - 40。更困难的问题可能需要一个更高的值。它必须足够低,以保持训练过程计算效率:

protected int m_populationSize = 30;

这决定了搜索空间的大小。粒子的位置分量将被限制在 [-maxPos, maxPos] 范围内。一个恰当选择的范围可以提高性能。-1 是一个特殊值,表示无界搜索空间:

protected double m_maxPosition = -1;

这个粒子在一次迭代中可以接受的最大变化量限制了粒子速度分量的最大绝对值,并影响了搜索的粒度。如果太高,粒子可能会飞过最佳解。如果太低,粒子可能会陷入局部最小值。它通常设置为搜索空间动态范围的一小部分(10% 已被证明对于高维问题来说很好)。-1 是一个特殊值,表示无界速度:

protected double m_maxVelocity = 2;

对于 c1,认知学习率 >= 0(返回到个人最佳位置的趋势):

protected double m_c1 = 2.0;

对于 c2,社会学习率 >= 0(趋向于移动到群体最佳位置):

protected double m_c2 = 2.0;

惯性权重,w,控制全局(高值)与局部搜索空间的探索。它与模拟退火中的温度类似,必须仔细选择或随时间逐渐减少。值通常在 0 和 1 之间:

protected double m_inertiaWeight = 0.4;

所有这些变量都应该对你很熟悉。接下来,我们正在做的事情的核心涉及到 UpdateParticle 函数,如下所示。这个函数负责更新粒子的速度、位置和个人最佳位置:

public void UpdateParticle(int particleIndex, bool init)
{
int i = particleIndex;
double[] particlePosition = null;
if (init)
{

创建一个新的粒子,使用随机值(除了第一个粒子,它的值与传递给算法的网络相同):

if (m_networks[i] == null)
{
m_networks[i] = (BasicNetwork)m_bestNetwork.Clone();
if (i > 0) m_randomizer.Randomize(m_networks[i]);
}
particlePosition = GetNetworkState(i);
m_bestVectors[i] = particlePosition;

随机化速度:

m_va.Randomise(m_velocities[i], m_maxVelocity);
}
else
{
particlePosition = GetNetworkState(i);
UpdateVelocity(i, particlePosition);

速度钳位:

m_va.ClampComponents(m_velocities[i], m_maxVelocity);

新位置 图片

m_va.Add(particlePosition, m_velocities[i]);

将粒子固定在搜索空间的边界上(仅对超过 maxPosition 的分量):

m_va.ClampComponents(particlePosition, m_maxPosition);
SetNetworkState(i, particlePosition);
}
UpdatePersonalBestPosition(i, particlePosition);
}

每个粒子都需要更新其速度,正如你可以在前面的代码中看到的那样。这个函数将使用惯性权重、认知和社会项来计算粒子的速度。这个函数包含了我们在本章前面伪代码中描述的标准粒子群优化公式:

protected void UpdateVelocity(int particleIndex, double[] particlePosition)
{
int i = particleIndex;
double[] vtmp = new double[particlePosition.Length];

惯性权重的标准 PSO 公式:

m_va.Mul(m_velocities[i], m_inertiaWeight);

标准 PSO 公式用于认知术语:

m_va.Copy(vtmp, m_bestVectors[i])
m_va.Sub(vtmp, particlePosition);
m_va.MulRand(vtmp, m_c1);
m_va.Add(m_velocities[i], vtmp);

标准 PSO 公式用于社会术语:

if (i != m_bestVectorIndex)
{
m_va.Copy(vtmp, m_pseudoAsynchronousUpdate ? m_bestVectors[m_bestVectorIndex] : m_bestVector);
m_va.Sub(vtmp, particlePosition);
m_va.MulRand(vtmp, m_c2);
m_va.Add(m_velocities[i], vtmp);
}
}

这就是我们将粒子群优化替换为标准反向传播的方式。简单,对吧?

概述

在本章中,我们学习了一些粒子群优化背后的基本理论。我们了解了该算法如何应用于,以及受到鸟群、蜜蜂群、鱼群等的影响。我们还看到了如何用粒子群优化来替换标准的反向传播公式。

在下一章中,我们将学习如何深入函数优化,并展示你如何找到最优参数,这个过程将为你节省无数小时的测试时间!

第十三章:函数优化:如何以及为什么

b 现在是我们享受乐趣的时候了。我们将开发一个非常强大、三维的应用程序,您在其他地方找不到这样的应用程序。此应用程序将允许您以二维和三维图形的方式可视化单个函数随时间优化。此应用程序的源代码位于本书源代码访问说明中。此应用程序将非常独特,因为我们将在创建一个令人难以置信的强大应用程序时使用开源和第三方控件。开源并不总是处理所有事情,对于那些对图形认真的您,我想让您接触到一些除了开源标准(如 ZedGraph、Microsoft Charting Controls 等)之外的控制。您将很快看到,这种差异令人震惊,值得这一趟旅程。您可以在以后决定是否要将任何内容改回完全开源。

我们还将向您展示如何使用粒子群优化来增强函数优化的可视化。通过这样做,您将看到群中的每个粒子如何收敛到最优解。

在本章中,我们将完成以下任务:

  • 构建一个 Visual Studio WinForms 项目

  • 创建一个函数优化测试器

  • 实现用于可视化的图形控件

  • 讨论在此项目中使用的各种第三方控件

  • 了解可用的各种超参数

  • 学习如何调整和调整超参数

  • 学习调整超参数的影响

  • 了解函数的目的

  • 学习如何添加新函数

  • 展示添加新函数并运行测试

技术要求

您需要具备使用 Microsoft Visual Studio 和 C# 进行 .NET 开发的基本知识。您需要从本书的网站上下载本章的代码。

查看以下视频以查看代码的实际应用:bit.ly/2ppBmvI

入门

在我们开始之前,让我向您展示我们将要创建的产品。完成之后,您将拥有一个应用程序,允许您以图形方式查看函数在迭代过程中的最小化或最大化。这与典型的基于文本的系统表示相反,如下所示:

3D 绘图

如您所见,这是一个非常直观的应用程序。让我们继续将其分解为我们在进展过程中将引用的各个部分。

第一部分是三维图,位于我们的主页上。三维图可以提供更多关于群中每个粒子所走的路径以及群本身所走的路径的洞察。当粒子或群收敛到全局最小值时,也更容易看到。对于这个图,我们将使用令人难以置信的 Nevron 图表控件。您可以在www.nevron.com/products-open-vision-nov-chart-control-overview.aspx找到更多关于此图表控件的信息。主用户界面是用 DotNetBar 开发的。对于那些寻找具有所有功能,如面包屑栏、标签、网格、列表视图、图表、Sparklines 等不同用户界面的人来说,这比 Infragistics 或 DevExpress 是一个更棒且更经济的选择。您可以在www.devcomponents.com/dotnetbar/找到更多关于这个控件套件的信息。

图片

主页

第二部分是二维图,位于我们的第四页,图表标签页。有些人也会称这种类型的图为意大利面图。它的任务是绘制群在二维平面上的位置。对于这个图,我们将使用 Microsoft 图表控件。如您所见,当尝试在二维表面上绘制时,此控件会变得非常繁忙。群中的粒子越多,您的图表就会越繁忙:

图片

2D 可视化

第三部分是信息树,位于我们的第三页,详情标签页。此树包含每个迭代的详细信息。迭代总数是我们将要讨论的超参数之一。每个迭代将跟踪所有群粒子信息,如位置、速度、最佳位置和适应度,如下所示:

图片

信息树

第四部分是函数超参数,位于我们的主页上。这些参数控制函数和函数优化,对于绘制二维和三维图是必不可少的。这些参数本身将在稍后的部分进行讨论:

图片

参数

第五部分是绘图回放控件,也位于我们的主页底部,在超参数之下。除了运行主函数优化迭代循环外,它们还负责回放二维和三维图的功能优化图。您可以播放、暂停、倒退、快进和后退,如下所示:

图片

运行控件

在了解细节之后,让我们继续讨论我们如何创建应用程序的确切方法。让我们开始享受乐趣吧!

函数的最小化和最大化

函数的最小化和最大化是寻找给定函数的最小值和最大值的过程。让我们简要地谈谈这个值。

如果值在一个给定的范围内,那么它被称为局部极值;如果它在函数的整个定义域内,那么它被称为全局极值。假设我们有一个函数 f,它定义在域 X 上。在 x* 处的极值,即 f(x),对于域 X 中的所有 x 都大于或等于 f(x)。相反,函数在 x 处的全局最小值是 f(x*),对于域 X 中的所有 x 都小于或等于 f(x)。

以更简单的方式,最大点也被称为最大值,最小点被称为最小值,函数。全局最大值或最小值是整个域空间(搜索空间)中的最高或最低函数值,局部最大值或最小值是在该搜索空间内定义的某个邻域中的最高或最低值(不允许位于边界上),如下所示:

图片

全局和局部最优解

在这个简单的示意图中,D 是全局最小值,G 是全局最大值。ACE 是局部最大值(重要的是要注意,一个函数可以有一个以上的全局或局部最大值或最小值)。BF 被认为是局部最小值。XYZ 存在于最小值 F 附近,因为 Y 的值小于 XZ

图片

3D 磁带图

让我们举一个真实的例子。假设我们使用函数 sin(x)。这个函数的最大值是 +1,最小值会是 -1。因此,我们有了全局最小值和最大值。sin(x) 可以取负无穷大到正无穷大之间的任何值,但在这个所有值中,最大值只能是 +1,最小值只能是 -1。

如果我们将搜索空间(全局域)限制在 0 到 90 之间(有时人们称之为区间),那么 sin(x) 现在的最小值将是 0,其值也将是 0。然而,现在的全局或最大值将是 90,其值是 1,因为我们限制了我们的搜索空间在 0 到 90 之间。sin(x) 的所有值都将位于 0 到 1 之间,在 0 到 90 的区间内。

什么是粒子?

我们将要处理的主要组成部分之一是所谓的 粒子——因此,粒子群优化。为了简要地提供一个关于什么是粒子的类比,让我们这样看。假设我们看到一群鸟在天空中飞翔。这群鸟中的每一只鸟都是一个粒子。我们看到一群鱼在水中游动。每条鱼都是一个粒子。我们撞倒了那个蜂巢,被数百只蜜蜂攻击。攻击我们的每一只蜜蜂,没错,都是一个粒子!

每个粒子都有适应度值,一旦通过要优化的适应度函数评估,就会告诉我们它在群体中的排名。此外,我们还有速度,这些速度指导每个粒子的飞行。粒子,就像鸟儿一样,通过跟随最优粒子(鸟群中的领导者)在我们的问题空间中飞行。

现在我们已经确切地知道了一个粒子是什么,我们如何用计算术语来描述它?我们将定义如下结构:

/// <summary> A particle. </summary>
public struct Particle
{
/// <summary> The position of the particle. </summary>
public double[] Position;
/// <summary> The speed of the particle. </summary>
public double[] Speed;
/// <summary> The fitness value of the particle. </summary>
public double FitnessValue;
/// <summary> The best position of the particle. </summary>
public double[] BestPosition;
/// <summary> The best fitness value of the particle. </summary>
public double BestFitnessValue;
}

在此基础上,让我们继续创建我们的项目。你应该已经安装并打开了 Microsoft Visual Studio。如果你还没有安装 Microsoft Visual Studio,你可以从 Microsoft 网站安装免费的社区版。一旦完成,打开 Microsoft Visual Studio 并创建一个如图所示的 Windows 表单项目。在我们的例子中,我们使用的是.NET 版本 4.7.1。你可以自由使用你有的任何版本,但需要至少是 4.5.2 或更高版本:

图片

新项目窗口

接下来,让我提一下,我们的用户界面是用一个名为DotNetBar的第三方产品创建的。这是一个出色的轻量级用户界面库。它可以在以下位置找到:www.devcomponents.com/dotnetbar/。我们现在可以开始专注于我们项目的公式化。我们需要初始化我们程序的一些通用区域,例如群体、图表和状态。](http://www.devcomponents.com/dotnetbar/)

群体初始化

首先,我们需要初始化我们的群体以及与之相关的所有变量和属性。

为了开始这个过程,让我们创建一个名为GlobalBest的粒子(我在整本书中会将其称为gbest),并将其最佳适应度值初始化为正无穷或负无穷,具体取决于用户是否选择了最小化最大化策略。我们这样做:

GlobalBest.BestFitnessValue = PSO_Type == PSOType.Minimization ? double.PositiveInfinity : double.NegativeInfinity;

接下来,我们将确定用户想要的群体大小,然后初始化群体中的所有粒子。每个粒子将需要初始化几个属性。它们是:

位置:

Swarm[i].Position = Swarm_Random(lb_domXi, ub_domXi, dimSize);

速度:

Swarm[i].Speed = Swarm_Random(lb_domXi, ub_domXi, dimSize);

适应度值:

Swarm[i].FitnessValue = PSO_Round(Fitness(Swarm[i].Position));

最佳适应度值:

Swarm[i].BestFitnessValue = Swarm[i].FitnessValue;

最佳位置:

Swarm[i].BestPosition = (double[])Swarm[i].Position.Clone();

完成这些后,我们需要检查个体粒子的最佳适应度值(pbest)是否优于全局粒子(团队)的适应度值(gbest)。如果是这样,我们将更新全局粒子到那个最佳位置和适应度值,其他粒子将跟随它。我们这样做:

if (IsBetterPosition(Swarm[i].BestFitnessValue, GlobalBest.BestFitnessValue, PSO_Type))
{
GlobalBest.BestPosition = (double[])Swarm[i].Position.Clone();
GlobalBest.BestFitnessValue = Swarm[i].BestFitnessValue;
}

接下来,我们将填充群体和全局图矩阵,如下所示:

FillPlotSwarmPosition(0, i, Swarm[i].Position);
FillPlotGlobalPosition(0, GlobalBest.BestPosition);

一旦完成,我们将使用与群相关的所有详细信息更新我们的信息树。为此,我们需要遍历整个群并记录我们的信息以供显示。由于我们使用的是 Windows 树控件,我们将每个群粒子作为单独的节点绘制,标识为 PSODisplayType.Swarm。每个节点下的信息将由标识符 PSODisplayType.SwarmPosition 指示。我们是这样完成的:

DisplayResult(PSODispType.GlobalBest, "Iter : 0 " + GlobalBest.BestFitnessValue + " :: " + sResult(GlobalBest.BestPosition));
for (int i = 0; i < SwarmSize; i++)
{
DisplayResult(PSODispType.Swarm, "Swarm [" + i + "] : " + Swarm[i].FitnessValue);
DisplayResult(PSODispType.SwarmPosition, "Position : " + sResult(Swarm[i].Position));
DisplayResult(PSODispType.SwarmPosition, "Speed : " + sResult(Swarm[i].Speed));
DisplayResult(PSODispType.SwarmPosition, "Best Pos : " + sResult(Swarm[i].BestPosition));
}

图表初始化

在我们的应用程序中,我们将简称为工作台,我们正在处理两个图表。第一个图表是三维的,第二个是二维的。每个图表都反映了相同的数据,尽管是从不同的角度。在我们的图表初始化函数中,我们将同时初始化两个图表。

  • chartPSO 是我们二维 Microsoft Chart 控件图表的名称

  • nChartControl2 是我们三维 Nevron 图表的名称

为什么不使用相同的控件进行两种可视化呢?这当然可能是一个案例,但这样你可以,读者,接触到两种不同类型的控件,并决定你更喜欢哪一种。

我们将要做的第一件事是创建一个名为 _MarkerStyle 的随机变量。每个粒子在二维图中将具有不同的标记样式,我们将使用这个随机变量来控制样式的正确创建,如下所示:

FastRandom _MarkerStyle = new FastRandom();

接下来,在我们的待办事项列表中,我们需要清除两个控件中的系列数据,以防有数据残留。我们使用以下两行代码来完成此操作。记住,chartPSO 是我们的二维图表,而 nChartControl2 是我们的三维图表控件:

chartPSO?.Series?.Clear();
nChartControl2?.Charts?[0]?.Series?.Clear();

为了从我们的三维控制中获得最佳的可视化效果,我们需要确保它适合整个图表区域。我们通过设置边界模式如下来实现:

nChartControl2.Charts[0].BoundsMode = BoundsMode.Stretch;

现在,我们需要确保群中的每个粒子在两个图表中都有一个表示区域。我们通过迭代群的大小并正确设置每个变量来实现这一点。我们首先添加二维图表配置:

for (int i = 0; i < maxSwarm; i++)
{
chartPSO?.Series?.Add("Swarm(" + i + ")");
chartPSO.Series[i].ChartType = SeriesChartType.Spline;
chartPSO.Series[i].MarkerStyle = (MarkerStyle)_MarkerStyle.Next(1, 10);
chartPSO.Series[i].MarkerSize = 10;

然后是三维图表配置,如下所示:

for (int i = 0; i < maxSwarm; i++)
{
NLineSeries m_Line1 = (NLineSeries)nChartControl2.Charts[0].Series.Add(SeriesType.Line);
m_Line1.MultiLineMode = MultiLineMode.Series;
m_Line1.LineSegmentShape = LineSegmentShape.Tape;
m_Line1.DataLabelStyle.Visible = false;
m_Line1.DepthPercent = 50;
m_Line1.Name = "Swarm(" + i + ")";

接下来,让我们按照以下方式设置二维图表的最终变量:

chartPSO?.Series?.Add("GlobalPosition");
chartPSO.Series[maxSwarm].ChartType = SeriesChartType.Point;
chartPSO.Series[maxSwarm].MarkerStyle = MarkerStyle.Diamond;
chartPSO.Series[maxSwarm].Color = Color.Black;
chartPSO.Series[maxSwarm].MarkerSize = 20;

最后,为了使我们的三维图表在使用时具有最大的灵活性,我们需要添加以下工具栏:

nChartControl2.Controller?.Tools?.Add(new NTrackballTool());
nChartControl2.Controller?.Tools?.Add(new NZoomTool());
nChartControl2.Controller?.Tools?.Add(new NOffsetTool());
nChartControl2.Controller?.Tools?.Add(new NAxisScrollTool());
NPanelSelectorTool selector = new NPanelSelectorTool();
selector.Focus = true;
nChartControl2.Controller.Tools.Add(selector);
nChartControl2.Controller.Tools.Add(new NDataZoomTool());

状态初始化

群和图表创建并初始化后,我们现在专注于初始化应用程序本身的状态。这意味着我们将收集所有用户定义的值并使用它们来初始化超参数本身。我们将在关于超参数的章节中详细探讨每个参数,但就目前而言,你只需要知道它们存在。让我们就它们与状态初始化的关系逐一讨论。

首先,我们将确定我们将用于初始化函数优化的策略。我们将把这个选择存储在一个标记为PSO_Type的变量中。我们的两种策略选择是最小化最大化。我们是这样确定类型的:

switch (combType.SelectedIndex)
{
case 0:
PSO_Type = PSOType.Minimization;
break;
case 1:
PSO_Type = PSOType.Maximization;
break;
}

接下来,我们将初始化维度数量、上限和下限以及速度限制:

dimSize = Convert.ToInt32(txtdimSize.Text);
ub_domXi = Convert.ToDouble(txtUBXi.Text);
lb_domXi = Convert.ToDouble(txtLBXi.Text);
ub_SpeedXi = Convert.ToDouble(txtUbSpeedXi.Text);
lb_SpeedXi = Convert.ToDouble(txtLbSpeedXi.Text);
decP = Convert.ToInt32(txtdecP.Text);

我们继续初始化惯性、认知和社会智能权重:

maxIter = Convert.ToInt32(txtmaxIter.Text);
Intertia = Convert.ToDouble(txtW.Text);
CognitiveWeight = Convert.ToDouble(txtC1.Text);
SocialWeight = Convert.ToDouble(txtC2.Text);
wMira = Convert.ToDouble(txtwMira.Text);

我们最重要的超参数之一与我们的群体及其种群大小有关——群体中将有多少个粒子。记住,尽管我们在源代码本身中没有放置边界检查,但这个值理想情况下应该在 5 到 40 之间。我通常使用 5 作为开始测试的值。我们通过查看用户输入的值来确定群体大小,如下所示:

SwarmSize = Convert.ToInt32(txtSwarmSize.Text);
Swarm = new Particle[SwarmSize];

最后,我们初始化全局变量以跟踪群体的最大效率:

PlotSwarm = new double[maxIter, SwarmSize, 2];
PlotGlobal = new double[maxIter, 2];

控制随机性

随着初始化过程的继续,位置、速度和适应度在群体初始化部分被初始化。以下是我们如何进行随机化的简要概述。我们从每个超参数开始,然后在我们超参数中指定的上限和下限之间随机化值:

public double[] Swarm_Random(double a, double b, int n)
{
double[] x = new double[n];
for (int i = 0; i < n; i++)
{
x[i] = Swarm_Random(a, b);
}
return x;
}
public double Swarm_Round(double x) => decP != -1 ? Math.Round(x, decP) : x;
public double Swarm_Random() => Swarm_Round(Randd.NextDouble());
public double Swarm_Random(double a, double b)
{
Return (a + (b - a) * Swarm_Random());
}

更新群体位置

群体位置是群体相对于全局最优解(在这种情况下,隐藏的宝藏)的当前位置。它被限制在上限和下限域界限内,如下所示。但记住,这两个是在同一面板中输入的超参数!:

private double UpdateSwarmPosition(double Pos, double Speed)
{
double OutPos = Pos + Speed;
return Math.Max(Math.Min(OutPos, upperBoundDomain), lowerBoundDomain);
}

更新群体速度

群体速度是指整个群体朝向全局最优解(即隐藏的宝藏)前进的速度。它首先根据以下公式计算得出,然后被限制在上限和下限速度超参数值内。正如你所见,我们还应用了各种权重和随机化值来计算和调整速度,如下所示:

// Update Swarm Speed
Swarm[i].Speed[j] = Inertia * Swarm[i].Speed[j] + CognitiveWeight * Swarm_Random() * (Swarm[i].BestPosition[j] - Swarm[i].Position[j]) + SocialWeight * Swarm_Random() * (GlobalBest.BestPosition[j] - Swarm[i].Position[j]);
// Bound Speed
Swarm[i].Speed[j] = UpdateSwarmSpeed(Swarm[i].Speed[j]);
private double UpdateSwarmSpeed(double Speed)
{
return Math.Max(Math.Min(Speed, upperBoundSpeed), lowerBoundSpeed);
}

主程序初始化

当主窗体首次加载时,这是我们的主要初始化过程开始的时候。让我们来分析这个方法,并讨论到底发生了什么。在这个方法中,我们关注的是三维图表的初始化。

首先,我们为图表建立一些通用参数:

// setup chart general parameters
NChart m_Chart = nChartControl2.Charts[0];
m_Chart.Enable3D = true;
m_Chart.Width = 60;
m_Chart.Height = 25;
m_Chart.Depth = 45;
m_Chart.Projection.Type = ProjectionType.Perspective;
m_Chart.Projection.Elevation = 28;
m_Chart.Projection.Rotation = -17;
m_Chart.LightModel.SetPredefinedLightModel(PredefinedLightModel.GlitterLeft);

接下来,我们处理显示背左墙上的交错条纹:

// add interlaced stripe to the Y axis
NScaleStripStyle stripStyle = new NScaleStripStyle(new NColorFillStyle(Color.Beige), null, true, 0, 0, 1, 1);
stripStyle.SetShowAtWall(ChartWallType.Back, true);
stripStyle.SetShowAtWall(ChartWallType.Left, true);
stripStyle.Interlaced = true;
((NStandardScaleConfigurator)m_Chart.Axis(StandardAxis.PrimaryY).ScaleConfigurator).StripStyles.Add(stripStyle);

最后,我们处理显示x轴网格线,如下所示:

// show the X axis gridlines
NOrdinalScaleConfigurator ordinalScale = m_Chart.Axis(StandardAxis.PrimaryX).ScaleConfigurator as NOrdinalScaleConfigurator;
ordinalScale.MajorGridStyle.SetShowAtWall(ChartWallType.Back, true);
ordinalScale.MajorGridStyle.SetShowAtWall(ChartWallType.Floor, true);

运行粒子群优化

在我们的主要函数就位并且从我们的超参数中初始化了一切之后,我们现在可以专注于能够运行高级函数。我们 PSO 函数中的一个函数是PSORun方法。当用户点击运行按钮时,这个方法会被执行图片。现在让我们来分析这个高级函数。

我们首先通过调用我们的InitState函数来初始化我们的状态:

InitState();

在此之后,我们将清除我们的信息树,创建一个新的计时器,用于计时我们的函数,然后运行我们的Swarm_Run方法。这将在幕后执行实际的功能优化,这通常只需要毫秒,具体取决于群体大小、迭代次数和维度:

advTree1.Nodes.Clear();
SW = new Stopwatch();
SW.Start();
Swarm_Run();
SW.Stop();

接下来,我们创建负责跟踪整个群体全局和个体位置的变量:

plotSwarmPositions = (double[,,])PlotSwarm.Clone();
plotGlobalPositions = (double[,])PlotGlobal.Clone();
maxIter = plotSwarmPositions.GetLength(0);
maxSwarm = plotSwarmPositions.GetLength(1);

最后,我们初始化我们的图表,以较慢的速度回放群体的绘图,以便最终用户可以看到发生了什么。我们通过以下三种方法的调用来完成此操作:

InitChart();
InitializeMeshSurfaceChart();
PlaybackPlot();

我们的用户界面

当我们最初启动我们的应用程序时,我们有一个典型的空白石板。初始化后,我们已经完成了以下项目。请注意,数字与显示屏幕的截图相关,如下所示:

  • 我们的参数初始化为默认值

  • 我们的三维图表初始化为默认值,没有系列数据:

图片

空白 3D 图表

运行按钮

运行按钮根据所选函数和策略执行粒子群优化器:

图片

快退按钮

快退按钮将 PSO 图完全倒退到开始位置:

图片

后退按钮

后退按钮在粒子群优化测试运行中向后迈出一步:

图片

播放按钮

播放按钮从头开始回放粒子群优化运行:

图片

暂停按钮

暂停按钮暂停粒子群优化运行的回放:

图片

前进按钮

前进按钮在粒子群优化测试运行中向前迈出一步:

图片

超参数和调整

超参数通常用于调整各种机器学习函数的参数。在我们的应用程序中也是如此。

在我们应用程序的前端屏幕上,以下是我们超参数面板的外观。我们将详细讨论每个超参数:

图片

参数

函数

这是一个所有可用优化函数的列表。只需选择您想使用的函数,设置策略和其他参数,然后点击运行按钮。请参阅函数优化参考,以获取有关每个函数的更详细信息。在撰写本文时,有超过 50 种不同的函数可用,我们将在后面的章节中介绍如何添加您自己的函数,如下所示:

图片

函数

策略

可以将两种策略应用于函数优化。您可以选择最大化或最小化函数优化,如下所示:

图片

我们这里所说的“最大化”或“最小化”一个函数,指的是该函数的最小值或最大值可能是什么。这通常是在全局范围或局部范围内进行讨论的。

全局范围意味着我们想要确定函数在整个定义域上的最小值或最大值。这通常被称为函数的定义域。

另一方面,局部范围意味着我们想要确定函数在给定局部范围内的最小值或最大值,这将是全局范围的一个子集。

Dim size

维度大小在主循环(最内层循环)中用于处理所选函数的优化。默认值为 2:

图片

相关代码如下:

// Main Loop
for (int iter = 1; iter < maxIter; iter++)
{
for (int i = 0; i < SwarmSize; i++)
{
for (int j = 0; j < dimSize; j++)

上限

上限是群必须遵守的约束的上限。这用于更新群的位置并在范围内进行缩放。默认值为 10:

图片

请注意,根据你正在优化的函数,上限和下限可能与默认值有很大不同。请参考你函数的参考指南,看看上限和下限约束是什么:

return Math.Max(Math.Min(OutPos, upperBoundDomain), lowerBoundDomain);

上限 = 10:

图片

3D 图

上限 = 20:

图片

3D 图

下限

这是群必须遵守的约束的下限。这用于更新群的位置并在范围内进行缩放。默认值为 -10:

图片

相关代码如下:

return Math.Max(Math.Min(OutPos, upperBoundDomain), lowerBoundDomain);

上限速度

上限速度用于帮助确定群速度。默认值为 10:

图片

代码如下:

return Math.Max(Math.Min(Speed, upperBoundSpeed), lowerBoundSpeed);

上限速度 = 10:

图片

3D 图

上限速度 = 20:

图片

3D 图

下限速度

下限速度用于帮助确定群速度。默认值为 -10

图片

代码如下:

return Math.Max(Math.Min(Speed, upperBoundSpeed), lowerBoundSpeed);

小数位数

这是四舍五入时发生的总小数位数。默认值为 5:

图片

代码如下:

public double Swarm_Round(double x) => decimalPlaces != -1 ? Math.Round(x, decimalPlaces)

群体大小

群体总大小。这等于可用于优化的总粒子数。关于这里应该使用多少的理论有很多。记住,正如我们之前所说的,没有免费的午餐!一般来说,20-40 似乎是最广泛接受的值。默认值为 20:

图片

代码如下:

for (int i = 0; i < SwarmSize; i++)

群体大小 = 10:

图片

3D 图

群体大小 = 3:

图片

3D 图

最大迭代次数

用于测试的总迭代次数。默认值为 100:

图片

代码如下:

for (int iter = 1; iter < maxIter; iter++)

最大迭代次数 = 100:

图片

3D 图

最大迭代次数 = 25:

图片

3D 图

惯性

惯性权重最初被引入以平衡全局和局部搜索能力之间的优化。在我们的情况下,惯性乘以惯性权重以调整群体速度。通常,这个变量的接受值范围在 0.4 到 1 之间。默认值为 0.729:

图片

代码如下:

Swarm[i].Speed[j] = Inertia * Swarm[i].Speed[j]

惯性 = 0.729:

图片

3D 图

惯性 = 0.4:

图片

3D 图

社会权重

社会权重用于调整群体速度。它是决定粒子将跟随群体最佳解的程度的一个因素。1.49445是默认值:

图片

代码如下:

// Update Swarm Speed
Swarm[i].Speed[j] = Intertia * Swarm[i].Speed[j]+ CognitiveWeight * Swarm_Random() * (Swarm[i].BestPosition[j] - Swarm[i].Position[j])+ SocialWeight * Swarm_Random() * (GlobalBest.BestPosition[j] - Swarm[i].Position[j]);

社会权重 = 1.49445:

图片

3D 图

社会权重 = 1.19445:

图片

3D 图

认知权重

认知权重也用于调整群体速度。它是决定粒子将跟随其自身最佳解的程度的一个因素。1.49445是默认值:

图片

代码如下:

// Update Swarm Speed
Swarm[i].Speed[j] = Intertia * Swarm[i].Speed[j] + CognitiveWeight * Swarm_Random() * (Swarm[i].BestPosition[j] - Swarm[i].Position[j])+ SocialWeight * Swarm_Random() * (GlobalBest.BestPosition[j] - Swarm[i].Position[j]);

认知权重 = 1.49445:

图片

3D 图

认知权重 = 1.19445:

图片

3D 图

惯性权重

惯性权重在每次函数优化迭代中乘以惯性。0.99是默认值:

图片

代码如下:

Inertia *= InertiaWeight;

惯性权重 = 0.99:

图片

3D 图

惯性权重 = 0.75:

图片

3D 图

理解可视化

在本节中,我们将介绍您将在我们的程序中看到的一些众多内容。这包括二维和三维图。

理解二维可视化

对于我们的应用,我们需要解释几个二维可视化。首先是函数优化的二维图,无论是最大化还是最小化。这个可视化如下所示。记住,为此我们使用的是 Microsoft Chart 控件,它可以从以下链接获取:www.microsoft.com/en-us/download/details.aspx?id=14422:

图片

2D 可视化

对于我们正在绘制的每个粒子(最多 10 个),我们将使用不同的标记样式。标记就是您在前面的图中看到的菱形、圆形、x 等。我们还将根据每个粒子使用不同的颜色来旋转颜色。如果您没有用彩色查看这本书,您可能会有灰度的阴影。

您在前面的图中看到的线条(或者,更准确地说,您在上面看到的样条曲线)是群中每个粒子的轨迹。全局最优值(gbest)是图表区域中间的黑钻石。如您所见,我们始终保持在超参数的界限内。

理解三维可视化

在我看来,三维视图最容易解释,也是最直观的,尤其是如果它是您将展示给别人的模型验证包的一部分。您可以在下面的图中轻松地看到每个粒子何时(如果到达)到达全局最优。结果是 0 轴上的一条或多条线段表示的平坦线:

图片

3D 可视化

有几个工具栏可用于处理三维视图,其中包括旋转选项、放置选项、颜色选项等:

图片

您可以使用Trackball旋转图表,从几乎任何方向查看视图:

图片

只需选择 Trackball,然后选择图表,点击左鼠标按钮并像这样拖动图表到您的新视图:

图片

旋转视图

您可以通过更改预定义光模型来更改图表的照明:

图片

预定义光模型

您可以选择以下任何预定义的模型:

图片

预定义的光模型

深度和宽度控件允许您更改图表上的两个维度以满足您的需求:

图片

简单点击您想要的按钮,并继续点击以应用变换,如下所示:

图片

变换

微调允许您对图表区域和位置进行细微调整。您可以通过简单地点击所需的按钮,向上、向下、向左或向右微调,以及这些组合中的任何一种,如下所示:

图片

在主工具栏中,您可以打开、保存和打印图表,如果您需要基于测试的图像来制作报告,这是一个非常有用的功能。您还可以使用图表编辑器和图表向导,如下所示:

图片

简单选择您想要的按钮,在这种情况下是显示图表向导,向导对话框将出现:

图表向导

你也可以使用 3D 按钮在二维和三维视图之间切换同一图表。

二维视图:

2D 视图

这可以通过单击单个按钮变为 3D:

3D 视图

绘制结果

以下部分详细说明了一旦获得结果,我们的信息是如何显示的。

播放结果

一旦群体优化完成,播放结果的工作就变得突出。我们在重新播放图表中的主要函数称为 PlaybackPlot。让我们详细讨论这个函数:

private void PlaybackPlot()
{

获取我们的当前迭代,如下所示:

int iterN = GetCurrentIteration();
_Closing = false;

如果我们已经播放了所有点,那么就离开,如下所示:

if (iterN >= maxIter)
return;
PlotStatusN = PlotStatus.Play;

更新进度条,如下所示:

progressBarX1.Visible = true;
progressBarX1.Minimum = 0;
progressBarX1.Maximum = maxIter;

遍历所有迭代,如下所示:

for (int iter = iterN; iter < maxIter; iter++)
{

更新进度条值,如下所示:

progressBarX1.Value = iter;
if (_Closing)
{
_Closing = false;
progressBarX1.Visible = false;
return;
}

绘制单个群体迭代点,如下所示:

PlotSwarmIterationPoint();

简单暂停以允许 UI 保持响应,如下所示:

PauseForMilliSeconds(1);
ShowTitle();
}
PlotStatusN = PlotStatus.Pause;
progressBarX1.Visible = false;
}

你会在前面的函数中注意到对 PlotSwarmIterationPoint 的调用。这个函数调用(或者如果你更喜欢,方法)负责绘制粒子的单个运动。一步,如果可以这样说的話。让我们带你了解这个函数,并描述正在发生的事情,如下所示:

private void PlotSwarmIterationPoint()
{

如果我们已经达到最终迭代,那么就离开,如下所示:

intiterN = GetCurrentIteration();
if (iterN >= maxIter)
return;
NChart chart = nChartControl2.Charts[0];
chart.Axis(StandardAxis.PrimaryX).ScaleConfigurator = new NLinearScaleConfigurator();

我们需要为群体中的每个粒子绘制一个单独的点,如下所示:

for (int Swarm = 0; Swarm < maxSwarm; Swarm++)
{

为每个点添加一个序列,如下所示:

chartPSO.Series[Swarm].Points.AddXY(plotSwarmPositions[iterN, Swarm, 0], plotSwarmPositions[iterN, Swarm, 1]);

为我们刚刚创建的序列添加一个数据点,如下所示:

NLineSeries m_Line1 = (NLineSeries)nChartControl2.Charts[0].Series[Swarm];
m_Line1.AddDataPoint(new NDataPoint(plotSwarmPositions[iterN, Swarm, 0], plotSwarmPositions[iterN, Swarm, 1]));

根据每个粒子所在的范围值动态处理颜色,如下所示:

ApplyLineColorRanges(new NRange1DD[] { new NRange1DD(-10, -5), new NRange1DD(5, 10) },
new Color[] { Color.Red, Color.Yellow }, m_Line1);
}

现在,添加一个表示最优全局位置的点,如下所示:

chartPSO.Series[maxSwarm].Points.Clear();
chartPSO.Series[maxSwarm].Points.AddXY(plotGlobalPositions[iterN, 0], plotGlobalPositions[iterN, 1]);

获取下一个迭代,绘制控件,并显示正在发生的事情的文本:

iterN = Math.Min(iterN + 1, maxIter - 1);
nChartControl2.Refresh();
pictureBox1.Invalidate();
ShowTitle();
}

更新信息树

信息树位于我们用户界面的详细信息标签页上。它包含了信息树视图控件。根据 PSODispType,我们将创建一个新的节点或使用之前创建的节点来写入我们的文本:

private void DisplayResult(PSODispType DispType, string Text)
{
switch (DispType)
{

在树中创建一个新的节点。这是树中粒子的最高级别,代表找到的全局最佳值,如下所示:

case PSODispType.GlobalBest:
Node n1 = new Node();
n1.Text = Text;
lastNode = advTree1.Nodes.Add(n1);
break;

向前面的节点添加详细信息。这是一个群体中的单个粒子,其子详细信息将在我们的下一个函数中绘制,如下所示:

case PSODispType.Swarm:
Node n2 = new Node();
n2.Text = Text;
advTree1?.Nodes?[lastNode]?.Nodes?.Add(n2);
break;

向前面的节点添加详细信息。这些是粒子的确切详细信息,并形成群体中该粒子的底层节点,如下所示:

case PSODispType.SwarmPosition:
Node n = new Node();
n.Text = Text;
advTree1?.Nodes?[lastNode]?.Nodes?.Add(n);
break;
}
}

就这样。我们现在有一个完全填充的信息树!

添加新的优化函数

我们视觉测试工作台的一个美妙之处在于,我们可以轻松地添加新的优化函数进行测试。

函数的目的

一些问题是通过质量与正确或错误来评估的。这类问题被称为优化问题,因为目标是识别最优值。函数(有时称为成本函数、目标函数、误差函数等)通过将 n 维实值项映射到一维实值项来实现这一目标(有些人可能更喜欢使用“空间”而不是“项”,因为它与我们讨论过的总搜索空间更接近)。

我们将处理两种类型的函数。它们是:

  • 最小化:寻找具有最小值的解决方案

  • 最大化:寻找具有最大值的解决方案

并非总是能够找到最小值或最大值,有时我们必须满足于一个我们认为足够好的值,以实现我们的目标。

添加新功能

添加新功能是一个非常简单的流程,只需遵循几个步骤。这些步骤是:

  1. 根据以下签名创建一个新函数

  2. 将新函数名称添加到GetFitnessValue函数中

  3. 将新函数名称添加到用户界面

让我们逐一介绍这些步骤。首先,我们将处理函数签名本身。函数签名如下:

Public double xxxxxx(double[] data)
{
}

在这里,xxxxxx 是将在 UI 中显示的函数名称,正如您在以下内容中可以看到的:

优化函数窗口

在此之后,你必须更新GetFitnessValue函数,使其知道如何将用户界面上的显示内容与实际函数相关联。以下是一个该函数的示例。我们将在“让我们添加一个新函数”部分中填写这些信息:

internal double GetFitnessValue(double x, double y)
{
double[] data = new double[2];
data[0] = x;
data[1] = y;

fitnessFunction文本是用户界面中显示的内容:

if (fitnessFunction == "Sphere")
return Swarm_Round(SphereFunction(data));
else if (fitnessFunction == "FitnessTest")
return Swarm_Round(FitnessTestFunction(data));
else if (fitnessFunction == "MSphere")
return Swarm_Round(MSphereFunction(data));
}

让我们添加一个新函数

现在是我们展示如何添加新函数的时候了。我们将处理的函数是原始Levy函数的修改版,是已知存在的第 13 版。这是一个最小化函数。

函数本身,您可以在可视化工作台源代码中找到,看起来如下:

public double LevyFunction13(double[] data)
{
double x1 = data[0];
double x2 = data[1];
double term1 = Math.Pow(Math.Sin(3 * Math.PI * x1), 2);
double term2 = Math.Pow((x1 - 1), 2) * (1 + (Math.Pow(Math.Sin(3 * Math.PI * x2),
2)));
double term3 = Math.Pow((x2 - 1), 2) * (1 + (Math.Pow(Math.Sin(2 * Math.PI * x2), 2)));
return term1 + term2 + term3;
}

当然,这里有很多数学知识,对吧?很多时候,这样的函数会在使数学更容易查看的编辑器中创建。例如,如果我要用数学方式表示这段代码,它看起来会是这样:

f(x,y)=sin2(3πx)+(x−1)2(1+sin2(3πy))+(y−1)2(1+sin2(2πy))

如果我们用 MATLAB 之类的工具来绘制这个图表,它看起来会是这样:

我们在 MATLAB 中的视角

为什么我要说和展示所有那些内容呢?因为,当你使用这个工具来验证你的测试时,你需要能够传达,有时还需要为他人辩护,这些信息。毫无疑问,仅仅展示 C#代码是不够的,数学和可视化在很多情况下是其他人期望看到的。不要因此而气馁;你会看到创建这些函数是多么容易,我们的应用程序使得生成所需的信息变得非常简单。

让我们回到正轨,继续添加那个函数。

一旦我们添加了我们的新函数,现在我们需要将其添加到GetFitnessValue函数中,以便用户界面中选择的选项可以与我们特定的函数相关联:

else if (fitnessFunction == "Shubert")
return Swarm_Round(ShubertFunction(data));
else if (fitnessFunction == "Levy13")
return Swarm_Round(LevyFunction13(data));

一旦完成这个步骤,我们需要将其添加到用户界面的下拉函数列表框中。只需在用户界面上选择组合框,转到“Items”属性,然后点击按钮:

图片

接下来,简单地添加你想要显示的文本,如下所示:

图片

字符串收集编辑器

一旦完成这个步骤,构建项目,运行它,你应该会看到函数在下拉列表中显示:

图片

新函数

在你选择了 Levy13 函数后,点击运行按钮, voilà,你已经成功添加了一个新函数并测试了它的执行。你可以查看二维和三维图来验证你的成功。在你达到最大迭代次数(在本例中为 100)之前,你应该已经达到了全局最优解 0(三维图右侧的平坦带状区域):

图片

3D 视图

摘要

在本章中,我们讨论了函数,它们是什么,以及为什么我们使用它们。我们开发了一个非常强大且灵活的应用程序来测试函数优化。我们还展示了添加新函数的完整过程以及添加后如何运行它。你现在可以自由地添加你喜欢的任何新函数;只需遵循此处概述的过程,你应该不会有任何问题。

在下一章中,我们将学习如何用粒子群优化算法替换反向传播,所以请系好你的帽子!

第十四章:寻找最优参数

在本章中,我们将使用开源软件包 SwarmOps,版本 4.0,帮助您更好地理解如何使用此工具为您函数找到最优参数。您可以从以下位置获取 SwarmOps 的最新版本:github.com/mattcolefla/SwarmOps.

再次强调,我们必须花一点时间在理论上,我们将带你回到学术时代,打下基础,以便我们都使用相同的语言。需要注意的是,SwarmOps 是一个高度研究导向的工具,应如此使用。我们努力使这个产品开源,最新版本有超过 60 种不同的优化函数供您使用。

本章将涵盖以下主题:

  • 适应度函数

  • 约束条件

  • 元优化

  • 优化方法

  • 并行性

准备好了吗?我们开始了!

技术要求

您需要具备使用 Microsoft Visual Studio 和 C# 进行 .NET 开发的基本知识。您需要从本书的网站上下载本章的代码:SwarmOps (github.com/mattcolefla/SwarmOps).

查看以下视频以查看代码的实际应用:bit.ly/2QPddLO.

优化

一些问题的解决方案并不像 正确错误 那样简单明了,而是根据质量进行评分。这类问题被称为 优化问题,因为目标是找到最佳、即 最优 质量的候选解。

适应度函数是什么?

SwarmOps 适用于实值和单目标优化问题,即从 -维实值空间映射到一维实值空间的优化问题。从数学的角度讲,我们考虑优化问题为以下形式的函数

在 SwarmOps 中,假设 是一个最小化问题,这意味着我们正在寻找具有最小值 的候选解 。从数学上讲,这可以写成以下形式:

找到 ,使得 .

然而,通常情况下,我们无法找到确切的优化最佳点;我们必须满足于一个足够高质量的候选解,可能并非完全最优。在本章中,我们将优化问题图片称为“适应度”函数,但它也可以被称为成本函数、目标函数、误差函数、质量度量等等。我们还可以将候选解称为位置、代理或粒子,将所有可能的候选解称为搜索空间。

最大化

SwarmOps 也可以用于最大化问题。如果图片是一个最大化问题,那么等价的最小化问题如下:图片

基于梯度的优化

优化适应度函数图片的经典方法首先是推导其梯度,即图片,它由图片的偏导数组成,即:

图片

然后迭代地沿着最速下降方向跟踪梯度;如果需要,也可以使用准牛顿优化器。这个优化器要求不仅适应度函数图片是可微分的,还需要时间和耐心。这是因为梯度的推导可能非常耗时,执行也可能非常耗时。

启发式优化

基于梯度的优化方法的替代方案是让优化完全由适应度值引导。这种优化没有关于适应度景观外观的明确知识,而只是将适应度函数视为一个黑盒,它接受候选解作为输入并产生适应度值作为输出。在本章中,这种优化被称为无导数优化、直接搜索、启发式优化、元启发式、黑盒优化等等。我们将大量使用这些术语!

约束

约束将搜索空间分割成可行候选解和不可行候选解的区域。例如,一个工程问题可能有一个需要优化的数学模型,但在现实世界中产生解决方案可能会对可行性施加一些约束。在启发式优化中,有不同方式来支持和处理约束。

边界

约束的一种简单形式是搜索空间边界。而不是让 ![img/4b827c98-c3b5-4f2a-a2e3-22a8f3cd3d14.png] 从整个 ![img/19fe84b2-bee4-4df9-9731-e4c5979c7344.png] 维的实值空间映射,通常只使用这个庞大搜索空间的一部分是实用的。构成搜索空间的上下边界表示为 ![img/fd613b81-7202-46fa-a065-f4a9e7d46c66.png] 和 ![img/bc508a18-3f8e-460f-994a-a9a844344e7e.png],因此适应性函数的形式如下:

![img/a9c3f595-e542-4b73-a111-d269ef1cfee6.png]

这种边界通常通过将候选解移动回边界值来在优化方法中强制执行,如果它们已经超过了边界。这是 SwarmOps 中可用的默认约束类型。

惩罚函数

任何启发式优化器通过惩罚不可行的候选解(即在适应性函数中添加惩罚函数)透明地支持更复杂的约束。示例可以在 SwarmOps 源代码的惩罚基准问题部分找到。

一般约束

SwarmOps 通过在比较候选解时考虑可行性(约束满足)来支持一般约束。通常,我们通过比较它们的适应性 ![img/0c872fc8-9935-4e18-bc4e-251e8c91cece.png] 来确定候选解 ![img/b7d1afed-2b91-4ede-92ad-7d844093c95c.png] 是否比 ![img/6815653e-4aad-427b-bc3c-42cd1411241f.png] 更好,但也可以考虑可行性。可行性是一个布尔值;候选解要么是可行的,要么是不可行的。比较运算符如下图中所示:

![img/06b8c937-53e5-41e6-98f1-721e4ec3c124.png]

注意在前面的图中,这种比较的实际实现被简化了一些。还请注意,当 ![img/0dd53fa0-41cc-4d49-bfe8-cb86d4e72111.png] 是可行的而 ![img/606727bc-e8d5-4fcc-8c0f-e8b40205a94f.png] 是不可行的时候,它们的适应性不需要计算。这是因为 ![img/04942206-ca8b-4c51-893f-41b045d9c844.png] 由于它们的相互可行性而比 ![img/7dd24064-e485-4ab1-a789-218fe581c80c.png] 更差。这在实现中用于在可能的情况下避免适应性计算。

约束优化阶段

使用早期的比较运算符意味着优化有两个阶段。首先,优化器可能会只找到不可行的候选解,因此它优化不可行解的适应性。然后,在某个时刻,优化器希望发现一个可行的候选解;无论其适应性如何,它将成为优化器找到的最佳解,并成为进一步搜索的基础。这本质上是对可行解适应性的优化。

约束优化困难

虽然 SwarmOps 允许你实现任何可想象的约束,但约束本身会使优化器越来越难以找到可行的最优解,因为约束缩小了搜索空间的可行区域。因此,你应该也将初始化和搜索空间边界尽可能靠近可行区域。

实现

problem类中有两种方法可以用来实现约束;它们如下所示:

  • EnforceConstraints()允许你在评估候选解的可行性和适应度之前对其进行修复。例如,当使用搜索空间边界作为约束时,修复将包括将候选解移动回边界之间,如果它们越界了。这是默认行为。

  • Feasible()评估并返回候选解的可行性,而不会改变它。

元优化

优化方法通常有几个用户定义的参数,这些参数控制着优化方法的行为和有效性。这些被称为优化器的行为参数或控制参数。寻找这些行为参数的良好选择以前是通过手动调整完成的,有时甚至通过粗略的数学分析。研究人员中普遍认为,行为参数可以在优化过程中进行调整以改善整体优化性能;然而,这已被证明大部分情况下不太可能。调整行为参数可以被视为一个优化问题,因此可以通过叠加优化方法来解决。在这里,这被称为元优化,但在本章中也被称为元进化、超级优化、参数校准等等。SwarmOps 在元优化中的成功主要依赖于以下三个因素:

  1. SwarmOps 具有一种特别适合作为叠加元优化器的优化方法,因为它能快速发现表现良好的行为参数(这是本章中描述的 LUS 方法)。

  2. SwarmOps 采用一种简单的技术来减少计算时间,称为抢占性适应度评估。

  3. SwarmOps 使用相同的函数接口来处理优化问题和优化方法。有几篇科学出版物使用了 SwarmOps 进行元优化,并且比这里给出的描述更为详细,包括文献综述和实验结果。元优化的概念可以用以下示意图来表示:

图片

在前面的图中,将要调整行为参数的优化器被带到 DE 方法中,我们将在本章后面讨论。SwarmOps 框架允许针对多个优化问题调整参数,这在某些情况下是必要的,以便使行为参数的性能更好地响应更一般的问题。

在前面的例子中,DE 参数针对两个特定问题进行了调整。

适应度归一化

在 SwarmOps 中与元优化正常工作,适应度函数必须是非负的。这是因为预防性适应度评估是通过累加多次优化运行的适应度值来工作的,当适应度总和变得比考虑新候选解决方案作为改进所需的适应度总和更差时,就终止累加。这意味着适应度值必须是非负的,因此适应度总和只能变得更差,从而可以安全地终止评估。SwarmOps for C#会自动进行这种归一化,前提是你准确实现了problem类的MinFitness字段。例如,你可能有一个适应度函数![img/e06c6075-b237-4bd1-9991-78e53f0daf3f.png],它映射到,例如,![img/6395813e-55ae-4fe8-8fad-868fc3b68984.png]。在这种情况下,你必须将MinFitness设置为![img/c1279116-97cc-4e9c-b33d-18fd0264cc45.png]。最好使MinFitness准确,以便![img/b5f6c6b2-1db6-4603-b18f-d789f786eadc.png]对于最优解![img/4c08bc18-d933-418f-bcfe-9e672b621b2d.png],也就是说,MinFitness应该是最优解的适应度。你应该能够估计大多数现实世界问题的较低适应度边界,如果你不确定理论边界值是多少,你可以选择一些充足但不过分的边界适应度值。

多个问题的适应度权重

如果你正在元优化中使用多个问题,你可能需要针对每个问题进行权重实验,以使它们对元优化过程的影响更加均衡。

建议

通常推荐使用LUS方法作为叠加的元优化器。教程源代码包含了一些实验设置的建议,这些设置已被证明效果良好。最好你在针对你最终将使用优化方法的实际问题进行元优化。然而,如果你的适应度函数评估成本非常高,那么在元优化你的优化器的行为参数时,你可以尝试使用基准问题作为临时的替代——前提是你使用多个基准问题,并且优化设置与实际应用中使用的设置相同。换句话说,你应该使用与实际问题中使用的相似维度和优化迭代次数的基准问题。

约束和元优化

在元优化中关于约束的两个问题应该被提及;它们如下:

  • 可以通过在优化器的类中实现EnforceConstraints()Feasible()方法,以与优化问题相同的方式对优化器的控制参数施加约束。这意味着元优化器将搜索可行最优的控制参数,允许你搜索满足某些标准(例如,它们之间存在某种关系,如一个参数比另一个小等)的控制参数;例如,MOL 优化器的源代码就是这样一个例子。

  • 在确定优化器在构建元适应度度量方面的表现如何时,会忽略约束满足情况。这是一个开放的研究课题,但实验表明,优化器的控制参数应该为无约束问题进行元优化。这也会在约束问题上有良好的性能。

元元优化

当使用元优化来寻找优化器的最佳性能参数时,人们自然会想知道元优化器本身的最佳性能参数是什么。如果经常使用它,找到最佳的元优化器是有意义的。元优化器的最佳参数可以通过采用另一层优化来找到,这可以被称为元元优化。

优化方法

本节将简要介绍 SwarmOps 提供的优化方法,以及一些使用建议。

选择优化器

面对一个新优化问题时,你可能首先想尝试的是PS方法,这通常足够并且具有快速收敛(或停滞)的优势。此外,PS没有需要调整的行为参数,所以它要么有效,要么无效。如果PS方法在优化你的问题时失败,你可能想尝试LUS方法。你可能需要多次运行PSLUS,因为它们可能会收敛到次优解。如果PSLUS都失败了,你可能想尝试DEMOLPSO方法,并实验它们的行为参数。

按照惯例,PSLUS方法很快就会停滞,比如说在次迭代后,其中是搜索空间的维度。另一方面,DEMOLPSO方法需要更多的迭代,比如说,有时甚至更多。

如果这些优化器失败,你可能需要使用元优化来调整它们的行为参数,或者完全使用另一个优化器。

梯度下降(GD)

最小化适应度函数 (适应度函数图片) 的经典方法是在最速下降方向上反复跟随梯度。梯度函数 梯度函数图片 定义为 偏导数图片 的偏导数向量,表示如下:

LUS 图片

它是如何工作的

位置 位置图片 首先从搜索空间中随机选择,然后根据以下公式迭代更新,无论是否改善适应度:

模式搜索图片

如前公式所示,步长图片 是步长。当 最小化问题图片 是一个最小化问题时,遵循下降方向,即我们从当前位置减去梯度,而不是像最大化问题那样添加它。

缺点

GD 方法有一些缺点,即它需要定义梯度 梯度图片。梯度可能也难以计算,并且 GD 可能太慢地接近最优解。

模式搜索(PS)

优化方法称为 模式搜索 (PS),最初由 Fermi 和 Metropolis 提出,如 [6] 中所述,与 Hooke 和 Jeeves [7] 使用的方法类似。这里提出的是 [4] 中的变体。

它是如何工作的

PS 使用一个代理或搜索空间中的位置,该位置正在移动。令位置表示为 位置表示图片,它最初从整个搜索空间中随机选择。初始采样范围是整个搜索空间:初始采样图片。新的潜在位置表示为 潜在位置图片 并按以下方式采样。

首先,随机选择一个索引 索引图片,并让 随机选择图片更新图片 对所有 所有图片。如果 改进图片 改善了 适应度图片 的适应度,则移动到 移动图片。否则,将 维度图片 的采样范围减半并反转,使用 反转图片。重复此操作几次。

局部单峰采样(LUS)

LUS 优化方法通过在搜索空间中移动单个代理进行局部采样,以在优化过程中减少采样范围。LUS 方法在 [4] [8] 中提出。

它是如何工作的

代理的当前位置表示为!图片,最初是从整个搜索空间中随机选择的。潜在的新的位置表示为!图片,通过让!图片从!图片的邻域中采样,其中!图片是从范围!图片中均匀选择的随机向量,该范围最初为!图片。换句话说,整个搜索空间的完整范围由其上边界!图片和下边界!图片定义。LUS在任何改进适应度的情况下,从位置!图片移动到位置!图片未能改进!图片的适应度时,采样范围通过乘以一个因子!图片减少,如下所示:

图片

这里,减少因子!图片定义为以下:

图片

前面的公式表示!图片为搜索空间的维度,!图片为一个用户定义的参数,用于调整采样范围减少的速率。研究发现,!图片的值对于许多优化问题都表现良好。

差分进化 (DE)

知名的多代理优化方法差分进化(DE)最初由 Storn 和 Price [9] 设计。存在许多 DE 变体,其中简单的一个在 DE 类中实现。通过 DE Suite 和 JDE 类提供了几种不同的 DE 变体。

它是如何工作的

DE使用一组代理。令!图片表示正在更新的代理的位置,该位置是从整个种群中随机选择的。令!图片为其新潜在位置,计算如下(这就是所谓的DE/rand/1/bin变体):

图片

在这里,向量 是从种群中随机选择的独立代理的位置。索引 是随机选择的, 对于每个维度也是随机选择的,。如果移动到新位置 可以提高 的适应性,则进行移动。用户定义的参数包括微分权重 、交叉概率 和种群大小

粒子群优化(PSO)

被称为粒子群优化PSO)的优化方法最初由 Kennedy、Eberhart 和 Shi [10] [11] 设计。它通过拥有一群称为粒子的候选解决方案来实现,每个粒子都有一个速度,该速度会反复更新并加到粒子的当前位置上,以将其移动到新的位置。

它是如何工作的

表示从群体中粒子的当前位置。粒子的速度 然后按以下方式更新:

图片

在这里,用户定义的参数 被称为惯性权重,用户定义的参数 是对粒子自身已知最佳位置 和群体已知最佳位置 的吸引力权重。这些也通过随机数 加权。此外,用户还确定群体大小,。在 SwarmOps 实现中,速度被限制在搜索空间的整个范围内,因此一个代理在一次移动中不能移动超过一个搜索空间边界到另一个边界。

一旦计算了代理的速度,就将其加到代理的位置上,如下所示:

图片

许多优化联络(MOL)

PSO 的一种简化称为许多优化联络MOL),最初由 Kennedy [12]提出,他称之为仅社交 PSO。MOL 这个名字在[5]中使用,那里进行了更深入的研究。MOL 与 PSO 的不同之处在于它消除了粒子的最佳已知位置, 。这已被发现可以提高某些问题的性能,并使调整行为参数变得更容易。

网格(MESH)

可以使用MESH方法在搜索空间的常规间隔内计算适应度。对于增加搜索空间维度,这会导致网格点呈指数增长以保持相似的间隔大小。这种现象被称为维度诅咒。MESH方法在 SwarmOps 中用作任何其他优化方法,并且确实会返回找到的最佳适应度的网格点作为其解。这个解的质量将取决于网格的粗细。MESH方法主要用于绘制简单优化问题的适应度景观图,或研究不同的行为参数选择如何影响优化方法的表现,即元适应度景观看起来如何。

MESH方法不打算用作优化器。

并行性

拥有多个处理单元的计算机越来越受欢迎,有不同方式来利用这种并行性。

并行化优化问题

一些优化问题可以在内部并行化。这种方法的优点是 SwarmOps 中的所有优化方法都可以不加修改地使用。缺点是每个优化问题都必须并行化,这个过程没有利用基于群体的优化器的自然并行结构。

并行优化方法

SwarmOps 提供了DEPSOMOL方法的并行版本,这些方法仅假设适应度函数的实现是线程安全的。这些并行优化器最适合计算耗时的适应度函数,否则并行化开销会抵消收益。

必要的参数调整

并行优化器的实现方式与其顺序版本略有不同。并行化多智能体优化器的典型方式是在一个执行线程上维护和更新智能体群体,然后将适应度计算仅分配到多个执行线程。这使得同步访问数据变得更容易。然而,这也意味着必须处理整个群体,才能使改进变得有效并用于计算新的候选解。这改变了优化器的动态行为,意味着它需要不同的行为参数才能有效工作,这不一定像优化器的顺序版本那样有效。

最后,代码

假设你已经下载了本章开头描述的代码,现在让我们看看发生了什么。首先,让我们打开 TestParallelMetaBenchmarks 项目并打开 main.cs 文件。这是我们将在以下代码中工作的文件。

首先,我们需要创建一些非常重要的变量,这些变量将成为优化层的设置。我们对每个变量都进行了注释,以便你知道它们的作用,如下所示:

// Set this close to 50 and a multiple of the number of processors, e.g. 8.
static readonly int NumRuns = 64;
// The total dimensions.
static readonly int Dim = 5;
// The dimension factor.
static readonly int DimFactor = 2000;
// The total number of times we will loop to determine optimal parameters.
static readonly int NumIterations = DimFactor * Dim;

接下来,我们将创建我们的优化器。SwarmOps 包含了几个优化器,但为了我们的目的,我们将使用 MOL 优化器。MOL 代表 Many Optimizing Liaisons,它是 Eberhart 等人原始粒子群优化方法的一种简化。Many Optimizing Liaisons 方法不对粒子的已知最佳位置产生吸引力,并且算法还随机选择更新哪个粒子,而不是迭代整个群体。它与 Kennedy 提出的 Social Only Particle Swarm Optimization 类似,并由 Pedersen 等人进行了更深入的研究 [3][4],他们发现它可以优于标准的粒子群优化方法,并且具有更容易调整的控制参数。哇,这听起来是不是有点复杂?

// The optimizer whose control parameters are to be tuned.
static Optimizer Optimizer = new MOL();

接下来是我们想要优化的问题(s)。你可以选择同时解决一个或多个问题,但通常一次解决一个优化调整问题更容易。

优化器正在调整其控制参数以在包含的问题(s)上良好工作,如下所示。数字表示在调整中问题之间的相互重要性。权重越高,其重要性越大,如下面的代码所示:

static WeightedProblem[] WeightedProblems = new WeightedProblem[]
{
new WeightedProblem(1.0, new Sphere(Dim, NumIterations)),
};
Next we have our settings for the meta-optimization layer.
static readonly int MetaNumRuns = 5;
static readonly int MetaDim = Optimizer.Dimensionality;
static readonly int MetaDimFactor = 20;
static readonly int MetaNumIterations = MetaDimFactor * MetaDim;

元适应度方面包括在多个优化运行中计算我们列出的问题的优化性能,并将结果相加。为了方便使用,我们用 MetaFitness 对象包装优化器,如下所示:

static SwarmOps.Optimizers.Parallel.MetaFitness MetaFitness = new SwarmOps.Optimizers.Parallel.MetaFitness(Optimizer, WeightedProblems, NumRuns, MetaNumIterations);

现在我们需要创建我们的元优化器对象,如下面的代码片段所示。为此,我们将使用 Pedersen 原创的 Local Unimodal SamplingLUS)优化器。该对象使用指数递减的采样范围进行局部采样。它适用于许多优化问题,尤其是在仅使用或允许短运行时效果很好。它特别适合作为调整另一个优化器参数时的叠加元优化器:

static Optimizer MetaOptimizer = new LUS(LogSolutions);

最后,我们将使用 Statistics 对象包装元优化器以记录我们的结果。然后,我们使用 MetaRepeat 对象重复进行多次元优化运行,如下所示:

static readonly bool StatisticsOnlyFeasible = true;
static Statistics Statistics = new Statistics(MetaOptimizer, StatisticsOnlyFeasible);
static Repeat MetaRepeat = new RepeatMin(Statistics, MetaNumRuns);

执行元优化

如果你查看项目,我们的优化器中的主要方法似乎是一个执行元优化运行的大方法,但事实上它只包含以下一行代码:

double fitness = MetaRepeat.Fitness(MetaParameters);

就这些!其余的都涉及到记录和向用户打印结果和信息。

计算适应度

我们接下来应该查看的代码块是如何计算我们的解决方案的。我们的主循环如下调用我们的适应度函数:

Statistics.Compute();

现在,让我们深入了解Fitness函数。为了方便,我们将整个函数放在以下代码片段中。我们将根据其在函数中的重要性逐行分析。我们在这里的最终目标是通过对我们的优化器传递参数来计算元适应度度量。我们在问题数组上执行优化运行,直到适应度超过fitnessLimit参数:

public override double Fitness(double[] parameters, double fitnessLimit)
{
double fitnessSum = 0;
// Iterate over the problems.
for (int i = 0; i < ProblemIndex.Count && fitnessSum<fitnessLimit; i++)
{
// Assign the problem to the optimizer.
Optimizer.Problem = ProblemIndex.GetProblem(i);
// Get the weight associated with this problem.
double weight = ProblemIndex.GetWeight(i);
// Use another fitness summation because we need to keep
// track of the performance on each problem.
double fitnessSumInner = 0;
// Perform a number of optimization runs.
for (int j = 0; j < NumRuns && fitnessSum < fitnessLimit; j++)
{
// Perform one optimization run on the problem.
Result result = Optimizer.Optimize(parameters, fitnessLimit -fitnessSum);
// Get the best fitness result from optimization and adjust it
// by subtracting its minimum possible value.
double fitness = result.Fitness;
double fitnessAdjusted = fitness - Optimizer.MinFitness;
// Ensure adjusted fitness is non-negative, otherwise Preemptive
// Fitness Evaluation does not work.
Debug.Assert(fitnessAdjusted >= 0);
// Apply weight to the adjusted fitness.
fitnessAdjusted *= weight;
// Accumulate both fitness sums.
fitnessSumInner += fitnessAdjusted;
fitnessSum += fitnessAdjusted;
}
// Set the fitness result achieved on the problem.
// This was why we needed an extra summation variable.
ProblemIndex.SetFitness(i, fitnessSumInner);
}
// Sort the optimization problems so that the worst
// performing will be attempted optimized first, when
// this method is called again.
ProblemIndex.Sort();
return fitnessSum;
}

现在,让我们看看我们的代码在实际中的应用,如下面的截图所示:

图片

如您所见,程序的目标是输出最优化参数,以便您可以使用相同的功能优化来调整您的网络。

但如果你有一个不在 SwarmOps 中包含的函数,你能做什么呢?幸运的是,你可以定义一个自己的自定义问题并使用它。让我们看看它是如何使用的。首先,让我们看看TestCustomProblem项目,如下面的截图所示:

图片

TestCustomProblem 项目

测试自定义问题

在我们创建和测试自己的自定义问题之前,让我们讨论一个更一般的问题。我们已经在本章前面概述了我们将什么定义为问题,但现在是在我们设计自己的基对象Problem之前展示代码的好时机。因此,让我们继续前进。

基础问题

以下是在每次优化中使用的基类Problem

public abstract class Problem
{
public Problem() : this(0, true)
{
}
public Problem(int maxIterations) : this(maxIterations, true)
{
}
public Problem(int maxIterations, bool requireFeasible)
{
MaxIterations = maxIterations;
RequireFeasible = requireFeasible;
}

执行优化的最大迭代次数如下:

public int MaxIterations

以下命令检查解决方案是否可行(即它是否满足约束):

public bool RequireFeasible

然后,使用以下命令返回优化问题的名称:

public abstract string Name

这包括一个包含参数名称的数组,如下所示:

public virtual string[] ParameterName => null;

要降低搜索空间边界,请使用以下命令:

public abstract double[] LowerBound

要增加上界搜索空间边界,请使用以下命令:

public abstract double[] UpperBound

如果与搜索空间边界不同,则下界初始化边界如下表示:

public virtual double[] LowerInit => LowerBound;

如果与搜索空间边界不同,则上界初始化边界如下表示:

public virtual double[] UpperInit => UpperBound;

以下命令详细说明了可能的最大(即最差)适应度,如下所示:

public virtual double MaxFitness => double.MaxValue;

以下命令详细说明了可能的最小(即最佳)适应度。如果使用元优化且假设适应度非负,这在所有我们元优化的问题中应该是大致相当的:

public abstract double MinFitness

可接受的适应度值的阈值如下表示:


public virtual double AcceptableFitness => MinFitness;

要返回问题的维度,即候选解中的参数数量,请使用以下命令:

public abstract int Dimensionality

以下行检查梯度是否已实现:

public virtual bool HasGradient => false;

以下命令计算并返回给定参数的适应度:

public virtual double Fitness(double[] parameters)
{
return Fitness(parameters, true);
}

如果适应度变得高于fitnessLimit()(即更差),或者无法提高适应度,则预先终止适应度评估,如下所示:

public virtual double Fitness(double[] parameters, double fitnessLimit){
return Fitness(parameters);
}

我们计算并返回给定参数的适应度。如果新候选解的可行性与旧候选解相同或更好,或者如果适应度变得高于fitnessLimit()且无法提高适应度,则预先终止适应度评估,如下所示:

public virtual double Fitness(double[] parameters, double fitnessLimit, bool oldFeasible, bool newFeasible)
{
return Tools.BetterFeasible(oldFeasible, newFeasible)? Fitness(parameters, fitnessLimit) : Fitness(parameters);
}

按如下方式计算并返回给定参数的适应度:

public virtual double Fitness(double[] parameters, bool feasible)
{
return Fitness(parameters, MaxFitness, feasible, feasible);
}

使用以下命令计算适应度函数的梯度,该命令与计算时间复杂度因子相关。例如,如果适应度计算的时间复杂度为 O(n),则梯度计算的时间复杂度为 O(n*n),则return n.</returns>

public virtual int Gradient(double[] x, ref double[] v)
{
throw new NotImplementedException();
}

使用以下命令强制约束并评估可行性。如果您不想强制约束,应调用Feasible()

public virtual bool EnforceConstraints(ref double[] parameters)
{

默认情况下,我们将候选解限制在搜索空间边界内,如下所示:

Tools.Bound(ref parameters, LowerBound, UpperBound);

由于我们知道候选解现在在范围内,而这正是可行性所需的所有内容,我们在这里可以直接返回true。如下所示,Feasible是为了教育目的而调用的,因为大多数优化器都会调用EnforceConstraints()

return Feasible(parameters);
}

使用以下代码评估可行性(约束满足):

public virtual bool Feasible(double[] parameters)
{
return Tools.BetweenBounds(parameters, LowerBound, UpperBound);
}

以下是在优化运行开始时调用的:

public virtual void BeginOptimizationRun()

以下是在优化运行结束时调用的:

public virtual void EndOptimizationRun()

要返回是否允许优化继续,请使用以下代码:

public virtual bool Continue(int iterations, double fitness, bool feasible)
{
return (iterations < MaxIterations &&!(fitness <= AcceptableFitness && (!RequireFeasible || feasible)));
}
}

创建自定义问题

现在我们已经解决了这些问题,让我们基于基类创建一个自定义问题。代码将类似于以下示例。

以下是一个二维 Rosenbrock 问题及其示例约束;其最优可行解似乎如下所示:

<summary>
a ~ 1.5937
b ~ 2.5416
 </summary>
Class CustomProblem :Problem
{
public double GetA(double[] parameters)
{
return parameters[0];
}
public double GetB(double[] parameters)
{
return parameters[1];
}

在这里,基类覆盖了优化器的名称,如下所示:

public override string Name => "CustomProblem";

问题的维度如下所示:

public override int Dimensionality => 2;
double[] _lowerBound = { -100, -100 };

以下为下界搜索空间边界:

public override double[] LowerBound => _lowerBound;
double[] _upperBound = { 100, 100 };

以下为上界搜索空间边界:

public override double[] UpperBound => _upperBound;

下界初始化边界如下所示:

public override double[] LowerInit => LowerBound;

上界初始化边界如下所示:

public override double[] UpperInit => UpperBound;

使用以下行计算出此问题的可能最小适应度:

public override double MinFitness => 0;

可接受的适应度阈值如下所示:

public override double AcceptableFitness => 0.4;
string[] _parameterName = { "a", "b" };

问题的参数名称如下所示:

public override string[] ParameterName => _parameterName;

要计算并返回给定参数的适应度,请使用以下代码:

public override double Fitness(double[] x)
{
Debug.Assert(x != null && x.Length == Dimensionality);
double a = GetA(x);
double b = GetB(x);
double t1 = 1 - a;
double t2 = b - a * a;
return t1 * t1 + 100 * t2 * t2;
}

要强制和评估约束,请使用以下代码:

public override bool EnforceConstraints(ref double[] x)
{
// Enforce boundaries.
SwarmOps.Tools.Bound(ref x, LowerBound, UpperBound);
return Feasible(x);
}
// Evaluate constraints.
public override bool Feasible(double[] x)
{
Debug.Assert(x != null && x.Length == Dimensionality);
double a = GetA(x);
double b = GetB(x);
// Radius.
double r = Math.Sqrt(a * a + b * b);
return ((r < 0.7) || ((r > 3) && (r < 5))) && (a < b * b);
}
}
}

我们的定制问题

现在,创建一个自定义问题对象,如下所示:

static Problem Problem = new CustomProblem();

优化设置应如下所示:

static readonly int NumRuns = 50;
static readonly int DimFactor = 4000;
static readonly int Dim = Problem.Dimensionality;
static readonly int NumIterations = DimFactor * Dim;

按如下方式创建优化器对象:

static Optimizer Optimizer = new DE(Problem);

优化器的控制参数应如下所示:

static readonly double[] Parameters = Optimizer.DefaultParameters;

将优化器包装在结果统计日志器中,如下所示:

static readonly bool StatisticsOnlyFeasible = true;
static Statistics Statistics = new Statistics(Optimizer, StatisticsOnlyFeasible); 

再次用以下重复器包裹:

static Repeat Repeat = new RepeatSum(Statistics, NumRuns);
static void Main(string[] args)
{

然后,初始化并行随机数生成器,如下所示:

Globals.Random = new RandomOps.MersenneTwister();

然后,设置要执行的优化迭代次数的最大值,如下所示:

Problem.MaxIterations = NumIterations;

使用以下代码创建一个适应度跟踪,以追踪优化的进度:

int NumMeanIntervals = 3000;
FitnessTrace fitnessTrace = new FitnessTraceMean(NumIterations, NumMeanIntervals);
FeasibleTrace feasibleTrace = new FeasibleTrace(NumIterations, NumMeanIntervals, fitnessTrace);

然后,按照以下方式将适应度跟踪分配给优化器:

Optimizer.FitnessTrace = feasibleTrace;

按照以下步骤进行优化:

double fitness = Repeat.Fitness(Parameters);
if (Statistics.FeasibleFraction > 0)
{

使用以下行计算结果统计:

Statistics.Compute();

使用以下代码输出最佳结果以及结果统计:

Console.WriteLine("Best feasible solution found:", Color.Yellow);
Tools.PrintParameters(Problem, Statistics.BestParameters);
Console.WriteLine();
Console.WriteLine("Result Statistics:", Color.Yellow);
Console.WriteLine("\tFeasible: \t{0} of solutions found.", Tools.FormatPercent(Statistics.FeasibleFraction), Color.Yellow);
Console.WriteLine("\tBest Fitness: \t{0}", Tools.FormatNumber(Statistics.FitnessMin), Color.Yellow);
Console.WriteLine("\tWorst: \t\t{0}", Tools.FormatNumber(Statistics.FitnessMax), Color.Yellow);
Console.WriteLine("\tMean: \t\t{0}", Tools.FormatNumber(Statistics.FitnessMean), Color.Yellow);
Console.WriteLine("\tStd.Dev.: \t{0}", Tools.FormatNumber(Statistics.FitnessStdDev), Color.Yellow);
Console.WriteLine();
Console.WriteLine("Iterations used per run:", Color.Yellow);
Console.WriteLine("\tMean: {0}", Tools.FormatNumber(Statistics.IterationsMean), Color.Yellow);
}
else
{
Console.WriteLine("No feasible solutions found.", Color.Red);
}
}

当我们运行我们的程序时,它应该看起来像以下截图:

图片

我们问题的输出结果

摘要

在本章中,我们学习了如何使用 SwarmOps 帮助我们优化函数优化的参数。我们学习了如何使用 SwarmOps 的内置函数,以及如何定义我们自己的函数。在下一章中,我们将继续学习图像检测,并使用伟大的开源包 TensorFlowSharp。

参考文献

  • J. Kennedy 和 R. Eberhart. 载于 IEEE 国际神经网络会议论文集,第 IV 卷,第 1942-1948 页,珀特,澳大利亚,1995 年

  • Y. Shi 和 R.C. Eberhart. 一种改进的粒子群优化器。载于 IEEE 国际进化计算会议论文集,第 69-73 页,安克雷奇,阿拉斯加,美国,1998 年。

  • J. Kennedy. 粒子群:知识的社交适应。载于 IEEE 国际进化计算会议论文集,印第安纳波利斯,美国,1997 年。

  • M.E.H Pederson 和 A.J. Chipperfield. 简化的粒子群优化。应用软计算,第 10 卷,第 618-628 页,2010 年。

  • 简化粒子群优化。Pedersen, M.E.H. 和 Chipperfield, A.J. s.l. : 应用软计算,2010 年,第 10 卷,第 618-628 页。

  • 最小化变量的度量方法。Davidon, W.C. 1, s.l. : SIAM 优化杂志,1991 年,第 1 卷,第 1-17 页。

  • “直接搜索”解决方案用于数值和统计问题。Hooke, R. 和 Jeeves, T.A. 2, s.l. : 计算机协会(ACM)杂志,1961 年,第 8 卷,第 212-229 页。

  • Pedersen, M.E.H. 和 Chipperfield, A.J. 局部单峰采样。s.l. : Hvass 实验室,2008. HL0801。

  • 差分进化 - 一种在连续空间上全局优化的简单而有效的方法。Storn, R. 和 Price, K. s.l. : 全球优化杂志,1997 年,第 11 卷,第 341-359 页。

  • 粒子群优化。Kennedy, J. 和 Eberhart, R. 澳大利亚珀斯 : IEEE 国际神经网络会议,1995 年。

  • 改进的粒子群优化器。Shi, Y. 和 Eberhart, R. 安克雷奇,阿拉斯加,美国 : IEEE 国际进化计算会议,1998 年。

  • 粒子群:知识的社交适应。Kennedy, J. 印第安纳波利斯,美国 : IEEE 国际进化计算会议论文集,1997 年。

第十五章:使用 TensorFlowSharp 进行对象检测

在本章中,我们将向您介绍一个名为 TensorFlowSharp 的开源软件包。更具体地说,我们将使用 TensorFlow[1] 对象检测 API,这是一个基于 TensorFlow 的开源框架,它使得构建、训练和部署各种形式的对象检测模型变得容易。

对于不熟悉 TensorFlow 的人来说,以下是从 TensorFlow 网站摘录的内容[2]:

"TensorFlow 是一个用于高性能数值计算的开源软件库。其灵活的架构允许轻松地将计算部署到各种平台(如 CPU、GPU 和 TPU),从桌面到服务器集群,再到移动和边缘设备。最初由 Google Brain 团队的研究人员和工程师在 Google 人工智能组织内部开发,它提供了强大的机器学习和深度学习支持,并且灵活的数值计算核心被广泛应用于许多其他科学领域。"

TensorFlowSharp 为 TensorFlow 库提供了 .NET 绑定,这些绑定在此发布,以防您将来需要它们:github.com/tensorflow/tensorflow

本章包含以下主题:

  • 使用张量

  • TensorFlowSharp

  • 开发自己的 TensorFlow 应用程序

  • 检测图像

  • 对象高亮的最小分数

技术要求

您需要具备使用 Microsoft Visual Studio 和 C# 进行 .NET 开发的基本知识。您需要从本书的网站下载本章的代码:TensorFlowSharp (github.com/migueldeicaza/TensorFlowSharp)。

查看以下视频以查看代码的实际应用:bit.ly/2pqEiZ9

使用张量

让我们通过讨论张量究竟是什么来设定场景。为此,我们也应该稍微谈谈向量和矩阵。如果您已经熟悉这些,您可以跳过这一部分,但它是简短的,如果您已经了解矩阵和向量,谁知道呢,您可能会记得一些您已经忘记的东西!所以,无论如何,都请继续阅读!

现在,在我们交谈之前,让我向您展示一个可能使事情更容易可视化的图形:

图片

向量是一个数字数组,正如您在这里可以看到的:

图片

矩阵是一个 n x m 的数字网格,一个二维数组。只要大小兼容,我们可以在矩阵上执行各种操作,例如加法和减法:

图片

如果我们愿意,我们可以相乘矩阵,如下所示:

图片

并且矩阵可以相加,如下所示:

图片

在这两种情况下,我们都在二维空间内工作。那么,如果我们的需求是在一个n维空间(其中n > 2)内工作,我们该怎么办?这就是张量发挥作用的地方。

张量基本上是一个矩阵,但它不是二维的(尽管它可以是)。它可能是一个三维矩阵(向量是一个张量,也是一个矩阵)或者是我们尚未学会如何可视化的某些极其疯狂的维度。为了展示张量的真正强大之处,一个张量可以在一个维度上是协变的,而在另一个维度上是反变的。张量的维度通常被称为其

更正式地说,张量实际上是被称为数学实体的东西,它存在于一个结构中,并与该结构内的其他实体相互作用。如果一个实体发生了变换,张量必须遵守所谓的相关变换规则。这正是矩阵与张量之间的区别。张量必须允许实体在变换发生时移动。

现在我们已经把所有这些都整理好了,让我们看看我们如何通过一些示例代码来处理张量:

void BasicVariables ()
{
Console.WriteLine ("Using placerholders");
using (var g = new TFGraph ())
{
var s = new TFSession (g);

注意变量类型必须与TFTensor中的转换匹配:

var var_a = g.Placeholder (TFDataType.Int16);
var var_b = g.Placeholder (TFDataType.Int16);

我们将要进行加法和乘法操作:

var add = g.Add (var_a, var_b);
var mul = g.Mul (var_a, var_b);
varrunner = s.GetRunner ();

让我们相加两个张量(这是之前提到的变量类型转换):

runner.AddInput (var_a, new TFTensor ((short)3));
runner.AddInput (var_b, new TFTensor ((short)2));
Console.WriteLine ("a+b={0}", runner.Run (add).GetValue ());

现在我们来乘以两个张量:

runner = s.GetRunner ();
runner.AddInput (var_a, new TFTensor ((short)3));
runner.AddInput (var_b, new TFTensor ((short)2));
Console.WriteLine ("a*b={0}", runner.Run (mul).GetValue ());
}
}

TensorFlowSharp

在我们讨论并向您展示了张量之后,让我们看看我们通常会如何使用 TensorFlowSharp API 本身。

您的应用程序通常会创建一个图(TFGraph),在那里设置操作,然后从它创建一个会话(TFSession)。然后这个会话将使用会话运行器来设置输入和输出并执行管道。让我们看看一个快速示例,看看它可能如何流动:

using(var graph = new TFGraph ())
{
graph.Import (File.ReadAllBytes ("MySavedModel"));
var session = new TFSession (graph);
var runner = session.GetRunner ();
runner.AddInput (graph ["input"] [0], tensor);
runner.Fetch (graph ["output"] [0]);
var output = runner.Run ();

从输出中获取结果:

TFTensor result = output [0];
}

在您不需要独立设置图的情况下,会话会自动为您创建一个。以下示例显示了如何使用 TensorFlow 计算两个数字的和。我们将让会话自动为我们创建图:

using (var session = new TFSession())
{
var graph = session.Graph;
var a = graph.Const(2);
var b = graph.Const(3);
Console.WriteLine("a=2 b=3");

添加两个常数:

var addingResults = session.GetRunner().Run(graph.Add(a, b));
var addingResultValue = addingResults.GetValue();
Console.WriteLine("a+b={0}", addingResultValue);

乘以两个常数:

var multiplyResults = session.GetRunner().Run(graph.Mul(a, b));
var multiplyResultValue = multiplyResults.GetValue();
Console.WriteLine("a*b={0}", multiplyResultValue);
}

开发自己的 TensorFlow 应用程序

现在我们已经向您展示了一些初步的代码示例,让我们继续我们的示例项目——如何从控制台应用程序中使用 TensorFlowSharp 检测图像中的对象。这段代码足够简单,您可以将其添加到您的解决方案中,如果您愿意的话。只需调整输入和输出名称,也许允许用户调整超参数,然后就可以开始了!

要运行此解决方案,您应该已从网站下载此章节的源代码并在 Microsoft Visual Studio 中打开。请按照以下说明下载本书的代码:

在我们深入代码之前,让我们讨论一个非常重要的变量:

private static double MIN_SCORE_FOR_OBJECT_HIGHLIGHTING = 0.5;

这个变量是我们识别和突出显示基础图像中物体的阈值。在 0.5 时,检测可靠性和准确性之间有合理的同步性。随着我们降低这个数字,我们会发现识别到的物体更多,然而,识别的准确性开始下降。我们降得越低,错误识别物体的可能性就越大。我们会识别它们,但它们可能不是我们想要的,您很快就会看到。

现在,让我们快速看一下这个样本的主要功能,并了解一下它在做什么:

static void Main(string[] args)
{

加载默认模型和数据:

_catalogPath = DownloadDefaultTexts(_currentDir);
_modelPath = DownloadDefaultModel(_currentDir);
_catalog = CatalogUtil.ReadCatalogItems(_catalogPath);
var fileTuples = new List<(string input, string output)>() { (_input, _output) };
string modelFile = _modelPath;

让我们在这里创建我们的 TensorFlowSharp 图对象:

using (var graph = new TFGraph())
{

将所有数据读入我们的图对象:

graph.Import(new TFBuffer(File.ReadAllBytes(modelFile)));

创建一个新的 TensorFlowSharp 会话来工作:

using (var session = new TFSession(graph))
{
Console.WriteLine("Detecting objects", Color.Yellow);
foreach (var tuple in fileTuples)
{

从我们的图像文件创建我们的张量:

var tensor = ImageUtil.CreateTensorFromImageFile(tuple.input, TFDataType.UInt8);
var runner = session.GetRunner();
runner.AddInput(graph["image_tensor"][0], tensor).Fetch(graph["detection_boxes"][0],graph["detection_scores"][0],graph["detection_classes"][0],graph["num_detections"][0]);var output = runner.Run();
var boxes = (float[,,])output[0].GetValue();
var scores = (float[,])output[1].GetValue();
var classes = (float[,])output[2].GetValue();
Console.WriteLine("Highlighting object...", Color.Green);

在处理完所有变量后,让我们在我们的样本图像上识别并绘制我们检测到的物体的框:

DrawBoxesOnImage(boxes, scores, classes, tuple.input, tuple.output, MIN_SCORE_FOR_OBJECT_HIGHLIGHTING);
Console.WriteLine($"Done. See {_output_relative}. Press any key", Color.Yellow);
Console.ReadKey();
}
}
}

好吧,对于一个简单的操作来说,这一切都很好,但如果我们真正需要做的是一个更复杂的操作,比如说乘法一个矩阵呢?我们可以这样做:

void BasicMultidimensionalArray ()
{

创建我们的 TFGraph 对象:

using (var g = new TFGraph ())
{

创建我们的 TFSession 对象:

var s = new TFSession (g);

为我们的乘法变量创建一个占位符:

var var_a = g.Placeholder (TFDataType.Int32);
var mul = g.Mul (var_a, g.Const (2));

进行乘法:

var a = new int[,,] { { { 0, 1 } , { 2, 3 } } , { { 4, 5 }, { 6, 7 } } };
var result = s.GetRunner ().AddInput (var_a, a).Fetch (mul).Run () [0];

测试结果:

var actual = (int[,,])result.GetValue ();
var expected = new int[,,] { { {0, 2} , {4, 6} } , { {8, 10}, {12, 14} } };
Console.WriteLine ("Actual: " + RowOrderJoin (actual));
Console.WriteLine ("Expected: " + RowOrderJoin (expected));
Assert(expected.Cast<int> ().SequenceEqual (actual.Cast<int> ()));
};
}
private static string RowOrderJoin(int[,,] array) => string.Join (", ", array.Cast<int> ());

检测图像

现在是时候转向一个真实的项目了。在这个例子中,我们将使用我们的基础图像(如下所示)并让计算机检测图像中的物体。如您所见,照片中有几个人的实例和风筝。这是所有 TensorFlowSharp 示例中使用的相同基础图像。您将看到随着我们改变我们的最小允许阈值,检测和突出显示的进展如何变化。

这里是我们的基础样本图像,一张照片:

物体突出显示的最小分数

我们之前谈到了突出显示的最小分数。让我们通过查看当我们使用不同的最小分数进行物体突出显示时会发生什么来确切了解这意味着什么。让我们从一个值为 0.5 的值开始,看看在我们的照片中检测到了哪些物体:

如您所见,我们已选择了两个风筝,并且每个风筝都附有一个相当好的准确度分数。绿色框表示高置信度目标。不错。但还有很多其他物体我认为我们应该检测到。还有几个风筝和几个人应该很容易检测到。为什么我们没有这样做呢?

如果我们将最小阈值从 0.5 降低到 0.3 呢?让我们看看结果:

好吧,如您所见,我们确实检测到了其他风筝,尽管由于它们在照片中的距离,置信度分数较低,但我们更重要的是,现在已经开始识别人。任何用红色画出的框都是低置信度目标,绿色是高置信度,黄色是中等置信度。

现在,如果我们再进一步,将我们的最小阈值降低到 0.1 呢?如果我们的模式遵循,我们应该能够识别更多的图像,当然,置信度分数会较低。

如果你看看照片的以下版本,你会发现我们确实选择了更多的对象。不幸的是,正如我们所怀疑的,准确性大幅下降,风筝被误认为是人,在一种情况下,一棵树也被误认为是人。但积极的方面是,我们的识别会随着我们调整阈值而改变。这能在更高级的应用中自适应地进行吗?绝对可以,这正是我想培养你思考的方式,以便你可以丰富代码并创造出真正震撼的应用:

图片

好的,这里有一个我认为你会喜欢的最终示例。在这个例子中,我将最小阈值降低到了 0.01。如果我们猜测正确,屏幕现在应该会亮起低置信度目标。让我们看看我们是否猜对了:

图片

看起来我们的猜测是正确的。我知道屏幕上的标记很混乱,但重点是我们的目标检测增加了,尽管是对于较低的置信度阈值。

你现在应该花些时间考虑这种技术的所有激动人心的应用。从人脸和物体检测到自动驾驶汽车,张量今天无处不在,这是你应该熟悉的东西。

摘要

TensorFlowSharp 是一个令人兴奋的开源项目,它使得使用张量和 TensorFlow 变得非常容易。在本章中,我们向您展示了张量是什么以及如何使用它们。我们还构建了一个功能强大的示例应用程序,允许您在图片中检测和标记图像。

在下一章中,我们将学习关于长短期记忆网络以及如何使用它们来增强您的应用程序和流程。

参考文献

  • [1]"现代卷积目标检测器的速度/精度权衡。"Huang J, Rathod V, Sun C, Zhu M, Korattikara A, Fathi A, Fischer I, Wojna Z, Song Y, Guadarrama S, Murphy K, CVPR 2017

  • [2] www.tensorflow.org

  • [3] JEAN, Hadrien. 深度学习书籍系列 2.1 标量、向量、矩阵和张量 网络博客文章。hadrienj.github.io. 26 Mar. 2018.

第十六章:使用 CNTK 进行时间序列预测和 LSTM

本章致力于帮助你更好地了解 Microsoft 认知工具包,或 CNTK。本章中包含的示例灵感来源于 CNTK 106 的 Python 版本:A 部分 – 使用 LSTM 进行时间序列预测(基础)。作为 C#开发者,我们不会使用 Python 代码(尽管有几种方法可以实现这一点),因此我们制作了自己的 C#示例来模仿那个教程。为了使我们的示例简单直观,我们将使用正弦函数来预测未来的时间序列数据。具体来说,我们将使用长短期记忆循环神经网络,有时称为LSTM-RNN或简称为LSTM。LSTM 有很多变体;我们将使用原始版本。

在本章中,我们将涵盖以下主题:

  • LSTM

  • 张量

  • 静态和动态轴

  • 加载数据集

  • 绘制数据

  • 创建模型

  • 创建小批量

  • 以及更多...

技术要求

你需要具备使用 Microsoft Visual Studio 和 C#进行.NET 开发的基本知识。你需要从本书的网站上下载本章的代码。

查看以下视频以查看代码的实际应用:bit.ly/2xtDTto

长短期记忆

长短期记忆LSTM)网络是一种特殊的循环神经网络。它们能够保留它们过去遇到的事情的长期记忆。在 LSTM 中,每个神经元都被称为记忆单元的东西所取代。这个记忆单元在适当的时候被激活和去激活,实际上就是所谓的循环自连接

如果我们退一步,看看常规循环网络的反向传播阶段,梯度信号可能会被神经元之间隐藏层中突触的权重矩阵多次相乘。这究竟意味着什么呢?嗯,这意味着这些权重的幅度可以对学习过程产生更强的影响。这既有好的一面,也有不好的一面。

如果权重很小,可能会导致所谓的梯度消失,在这种情况下,信号变得非常小,以至于学习速度减慢到难以忍受,甚至更糟,完全停止。另一方面,如果权重很大,这可能会导致信号变得非常大,导致学习发散而不是收敛。这两种情况都是不希望的,但可以通过 LSTM 模型中的一个项目来处理,即记忆单元。现在让我们来谈谈这个记忆单元。

一个记忆单元有四个不同的部分。它们是:

  • 输入门,具有恒定的权重 1.0

  • 自循环连接神经元

  • 遗忘门,允许细胞记住或忘记其先前状态

  • 输出门,允许记忆单元状态对其他神经元产生影响(或没有影响)

让我们来看看这个,并尝试将其全部整合起来:

存储单元

LSTM 变体

LSTM 网络有许多变体。其中一些变体包括:

  • 门控循环神经网络

  • LSTM4

  • LSTM4a

  • LSTM5

  • LSTM5a

  • LSMT6

训练和测试准确度,σ = relu,η = 1e −4

这些变体中的一种,LSTM 的一个稍微更戏剧化的版本,被称为门控循环单元,或 GRU/GRNN。它将 LSTM 的忘记门和输入门合并成一个称为更新门的单个门。这使得它比标准的 LSTM 更简单,并且越来越受欢迎。

这里是一个 LSTM 的样子:

LSTM

如您所见,LSTM 中有各种记忆,而 RNN 没有。这使得它能够轻松地保留长期和短期记忆。因此,如果我们想要理解文本并需要向前或向后查看时间,LSTM 正是为此而生的场景。让我们暂时谈谈不同的门。正如我们提到的,有三个。让我们用以下短语来解释每个是如何工作的。

Bob 住在纽约市。John 整天打电话与人交谈,并乘坐火车通勤。

忘记门:当我们到达单词 City 之后的时期,忘记门意识到可能存在上下文的变化。结果,主题 Bob 被遗忘,主题所在的位置现在为空。当句子转向 John 时,主题现在是 John。这个过程是由忘记门引起的。

输入门:所以重要的信息是 Bob 住在纽约市,John 整天乘坐火车并与人们交谈。然而,他通过电话与人交谈的事实并不那么重要,可以忽略。添加新信息的过程是通过输入门完成的。

输出门:如果我们有一个句子 Bob 是一位伟大的人。我们向他致敬 ____。在这个句子中,我们有一个空格,有很多可能性。我们知道的是,我们将向这个空格中的任何东西致敬,这是一个描述名词的动词。因此,我们可以安全地假设这个空格将被名词填充。所以,一个好的候选者可以是 Bob。选择从当前细胞状态中提取哪些信息有用并将其显示为输出的工作是由输出门完成的。

LSTM 的应用

以下只是 LSTM 堆叠网络的几个应用示例:

  • 语音识别

  • 手写识别

  • 时间序列预测和异常检测

  • 业务流程管理

  • 机器人控制

  • 以及更多...

时间序列预测本身可以对企业的底线产生重大影响。我们可能需要预测某些大额支出将在哪一天、哪个季度或哪一年发生。我们可能还会对我们的业务相对于时间序列的消费者价格指数表示关注。预测精度的提高可以肯定地改善我们的底线。

CNTK 术语

理解 Microsoft CNTK 工具包中使用的某些术语非常重要。现在让我们看看这些术语中的一些:

  • 张量:CNTK 的所有输入、输出和参数都组织为张量。还应注意的是,小批量也是张量。

  • :每个张量都有一个秩。标量是秩为 0 的张量,向量是秩为 1 的张量,矩阵是秩为 2 的张量。

  • 静态轴:在 2 中列出的尺寸被称为。每个张量都有静态动态轴。静态轴在其整个生命周期中长度保持不变。

  • 动态轴:然而,动态轴的长度可以从实例到实例变化。通常在呈现每个小批量之前不知道它们的长度。此外,它们可能是有序的。

  • 小批量:小批量也是一个张量。它有一个动态轴,称为批量轴。这个轴的长度可以从一个小批量变化到另一个小批量。

在撰写 CNTK 时,支持一个额外的单动态轴,也称为序列轴。这个轴允许用户以更抽象、更高级的方式处理序列。序列的美丽之处在于,每当对序列执行操作时,CNTK 工具包都会进行类型检查操作以确定安全性:

图片

序列轴

我们的示例

因此,现在我们已经介绍了 CNTK 和 LSTM 的一些基础知识,是时候深入我们的示例应用程序了。您可以在本书附带的代码中找到这个项目。在继续之前,请确保您已经在 Microsoft Visual Studio 中将其打开。如果您需要进一步说明,可以参考即将到来的代码部分。

我们正在创建的示例使用 Microsoft CNTK 作为后端,并将使用简单的正弦波作为我们的函数。正弦波之前已经绘制过,并且因其广为人知而被选用。

下面是我们示例应用程序的截图:

图片

主页 – 训练数据

上述截图显示了我们的主屏幕,显示了我们的正弦波数据点,我们的训练数据。目标是使我们的训练数据(蓝色)在形状上尽可能接近红色,如下所示:

图片

主页 - 图表

以下屏幕允许我们绘制我们的损失函数:

图片

绘制损失值(损失值选项卡)

以下屏幕允许我们绘制观察值与预测值。目标是使预测值(蓝色)尽可能接近实际值(以红色显示):

在测试数据选项卡中绘制观察值与预测值

编写我们的应用程序

现在让我们看看我们的代码。为此,您需要参考随本书附带的LSTMTimeSeriesDemo项目。在 Microsoft Visual Studio 中打开项目。所有必需的 CNTK 库已经在此项目中为您引用,并包含在 Debug/bin 目录中。我们将使用的主要库是Cntk.Core.Managed。对于这个例子,我们使用的是 2.5.1 版本,以防您想知道!

加载数据和图表

为了加载数据和图表,我们有两个主要函数,我们将

使用;LoadTrainingData()PopulateGraphs()。非常直接,对吧?:

private void Example_Load(object sender, EventArgs e)
{
 LoadTrainingData(DataSet?["features"].train, DataSet?["label"].train);
PopulateGraphs(DataSet?["label"].train, DataSet?["label"].test);
}

加载训练数据

对于这个例子,我们只是即时创建我们的测试和训练数据。LoadTrainingData()函数正是如此:

private void LoadTrainingData(float[][] X, float[][] Y)
 {
 //clear the list first
 listView1.Clear();
 listView1.GridLines = true;
 listView1.HideSelection = false;
 if (X == null || Y == null )
 return;

 //add features
 listView1.Columns.Add(new ColumnHeader() {Width=20});
 for (int i=0; i < inDim ;i++)
 {
 var col1 = new ColumnHeader
 {
 Text = $"x{i + 1}",
 Width = 70
 };
 listView1.Columns.Add(col1);
 }

 //Add label
 var col = new ColumnHeader
 {
 Text = $"y",
 Width = 70
 };
 listView1.Columns.Add(col);
for (int i = 0; i < 100; i++)
 {
 var itm = listView1.Items.Add($"{(i+1).ToString()}");
 for (int j = 0; j < X[i].Length; j++)
 itm.SubItems.Add(X[i][j].ToString(CultureInfo.InvariantCulture));
 itm.SubItems.Add(Y[i][0].ToString(CultureInfo.InvariantCulture));
 }
 }

填充图表

此函数使用训练数据和测试数据填充图表:

private void PopulateGraphs(float[][] train, float[][] test)
 {
 if (train == null)
 throw new ArgumentException("TrainNetwork parameter cannot be null");
 if (test == null)
 throw new ArgumentException("test parameter cannot be null");
for (int i=0; i<train.Length; i++)
trainingDataLine?.AddPoint(new PointPair(i + 1, train[i][0]));
for (int i = 0; i < test.Length; i++)
testDataLine?.AddPoint(new PointPair(i + 1, test[i][0]));
zedGraphControl1?.RestoreScale(zedGraphControl1.GraphPane);
zedGraphControl3?.RestoreScale(zedGraphControl3.GraphPane);
}

分割数据

使用此函数,我们模仿了 Python 等框架,这使得从主数据集中分割训练数据和测试数据变得非常容易。我们已创建了自己的函数来完成相同的事情:

static (float[][] train, float[][] valid, float[][] test) SplitDataForTrainingAndTesting(float[][] data, float valSize = 0.1f, float testSize = 0.1f)
{
 if (data == null)
 throw new ArgumentException("data parameter cannot be null");
//Calculate the data needed
var posTest = (int)(data.Length * (1 - testSize));
 var posVal = (int)(posTest * (1 - valSize));
 return (
 data.Skip(0).Take(posVal).ToArray(), 
 data.Skip(posVal).Take(posTest - posVal).ToArray(), 
data.Skip(posTest).ToArray());
}

运行应用程序

一旦我们点击运行按钮,我们将执行以下概述的函数。我们首先确定用户想要使用的迭代次数以及批次大小。在设置进度条和一些内部变量之后,我们调用我们的TrainNetwork()函数:

private void btnStart_Click(object sender, EventArgs e)
{
int iteration = int.Parse(textBox1.Text);
 batchSize = int.Parse(textBox2.Text);
progressBar1.Maximum = iteration;
progressBar1.Value = 1;
inDim = 5;
 ouDim = 1;
 int hiDim = 1;
 int cellDim = inDim;
Task.Run(() => TrainNetwork(DataSet, hiDim, cellDim, iteration, batchSize, ReportProgress));
}

训练网络

在每个神经网络中,我们必须训练网络,以便它能够识别我们提供给它的任何内容。我们的TrainNetwork()函数正是如此:

private void TrainNetwork(Dictionary dataSet, int hiDim, int cellDim, int iteration, int batchSize, Action<Trainer, Function, int, DeviceDescriptor> progressReport)
{
Split the dataset on TrainNetwork into validate and test parts
var featureSet = dataSet["features"];
var labelSet = dataSet["label"];

创建模型,如下所示:

var feature = Variable.InputVariable(new int[] { inDim }, DataType.Float, featuresName, null, false /*isSparse*/);
 var label = Variable.InputVariable(new int[] { ouDim }, DataType.Float, labelsName, new List<CNTK.Axis>() { CNTK.Axis.DefaultBatchAxis() }, false);
 var lstmModel = LSTMHelper.CreateModel(feature, ouDim, hiDim, cellDim, DeviceDescriptor.CPUDevice, "timeSeriesOutput");
 Function trainingLoss = CNTKLib.SquaredError(lstmModel, label, "squarederrorLoss");
 Function prediction = CNTKLib.SquaredError(lstmModel, label, "squarederrorEval");

准备训练:

TrainingParameterScheduleDouble learningRatePerSample = new TrainingParameterScheduleDouble(0.0005, 1);
TrainingParameterScheduleDouble momentumTimeConstant = CNTKLib.MomentumAsTimeConstantSchedule(256);
IList<Learner> parameterLearners = new List<Learner>()
{
Learner.MomentumSGDLearner(lstmModel?.Parameters(), learningRatePerSample, momentumTimeConstant, /*unitGainMomentum = */true)
};

创建训练器,如下所示:

       var trainer = Trainer.CreateTrainer(lstmModel, trainingLoss, prediction, parameterLearners);

训练模型,如下所示:

for (int i = 1; i <= iteration; i++)
{

获取下一个 minibatch 数量的数据,如下所示:

foreach (var batchData infrom miniBatchData in GetNextDataBatch(featureSet.train, labelSet.train, batchSize)
let xValues = Value.CreateBatch(new NDShape(1, inDim), miniBatchData.X, DeviceDescriptor.CPUDevice)
let yValues = Value.CreateBatch(new NDShape(1, ouDim), miniBatchData.Y, DeviceDescriptor.CPUDevice)
select new Dictionary<Variable, Value>
{
{ feature, xValues },
{ label, yValues }})
{

训练,如下所示:

trainer?.TrainMinibatch(batchData, DeviceDescriptor.CPUDevice);
} 
if (InvokeRequired)
{
Invoke(new Action(() => progressReport?.Invoke(trainer, lstmModel.Clone(), i, DeviceDescriptor.CPUDevice)));
}
else
{
progressReport?.Invoke(trainer, lstmModel.Clone(), i, DeviceDescriptor.CPUDevice);
}
}
}

创建模型

要创建一个模型,我们将构建一个包含长短期记忆LSTM)细胞的单向循环神经网络,如下所示:

public static Function CreateModel(Variable input, int outDim, int LSTMDim, int cellDim, DeviceDescriptor device, string outputName)
{
Func<Variable, Function> pastValueRecurrenceHook = (x) => CNTKLib.PastValue(x);

为每个输入变量创建一个 LSTM 细胞,如下所示:

Function LSTMFunction = LSTMPComponentWithSelfStabilization<float>(input,  new[] { LSTMDim }, new[] { cellDim }, pastValueRecurrenceHook, pastValueRecurrenceHook, device)?.Item1;

在创建 LSTM 序列之后,返回最后一个细胞以便继续生成网络,如下所示:

pre>       Function lastCell = CNTKLib.SequenceLast(LSTMFunction);

实现 dropout 为 20%,如下所示:

       var dropOut = CNTKLib.Dropout(lastCell,0.2, 1);

在输出之前创建最后一个密集层,如下所示:

 return FullyConnectedLinearLayer(dropOut, outDim, device, outputName);
}

获取下一个数据批次

我们以可枚举的方式获取下一批数据。我们首先验证参数,然后调用CreateBatch()函数,该函数列在此函数之后:

private static IEnumerable<(float[] X, float[] Y)> GetNextDataBatch(float[][] X, float[][] Y, int mMSize)
{
if (X == null)
 throw new ArgumentException("X parameter cannot be null");
 if (Y == null)
 throw new ArgumentException("Y parameter cannot be null");
for (int i = 0; i <= X.Length - 1; i += mMSize)
 {
 var size = X.Length - i;
 if (size > 0 && size > mMSize)
 size = mMSize;
var x = CreateBatch(X, i, size);
 var y = CreateBatch(Y, i, size);
yield return (x, y);
 }
}

创建数据批次

给定一个数据集,此函数将创建用于在更可管理的段中遍历整个数据集的批次。您可以从之前显示的GetNextDataBatch()函数中看到它是如何被调用的:

internal static float[] CreateBatch(float[][] data, int start, int count)
{
 var lst = new List<float>();
 for (int i = start; i < start + count; i++)
 {
 if (i >= data.Length)
 break;
lst.AddRange(data[i]);
}
return lst.ToArray();
}

LSTM 的表现如何?

我们测试了 LSTM 对太阳黑子数据预测的预测和历史值,这是深度学习中的一个非常著名的测试。正如您所看到的,红色曲线,即我们的预测,与趋势完全融合,这是一个非常鼓舞人心的迹象:

图片

预测与性能比较

摘要

在本章中,我们学习了长短期记忆循环神经网络。我们编写了一个示例应用程序,展示了如何使用它们,并且在过程中学习了一些基本术语。我们涵盖了诸如 LSTM、张量、静态和动态轴、加载数据集、绘制数据、创建模型和创建小批量等主题。在我们下一章中,我们将访问 LSTM 网络的一个非常近的表亲:门控循环单元。

参考文献

  • Hochreiter, S., & Schmidhuber, J. (1997). 长短期记忆。神经计算,第 9 卷,第 8 期,第 1735-1780 页。

  • Gers, F. A., Schmidhuber, J., & Cummins, F. (2000). 学习遗忘:使用 LSTM 的持续预测。神经计算,第 12 卷,第 10 期,第 2451-2471 页。

  • Graves, Alex. 使用循环神经网络进行监督序列标注。Springer, 第 385 卷,2012 年。

  • Y. Bengio, P. Simard, 和 P. Frasconi. 使用梯度下降学习长期依赖性是困难的。IEEE 神经网络 Transactions,第 5 卷,1994 年。

  • F. Chollet. Keras github.

  • J. Chung, C. Gulcehre, K. Cho, 和 Y. Bengio. 对序列建模的门控循环神经网络的实证评估,2014 年。

  • S. Hochreiter 和 J. Schmidhuber. 长短期记忆。神经计算,第 9 卷,第 1735-1780 页,1997 年。

  • Q. V. Le, N. Jaitly, 和 H. G. E. 矩形线性单元循环网络的一个简单初始化方法。2015 年。

  • Y. Lu 和 F. Salem. 长短期记忆(LSTM)循环神经网络中的简化门控。arXiv:1701.03441,2017 年。

  • F. M. Salem. 基本循环神经网络模型。arXiv 预印本 arXiv:1612.09022,2016 年。

  • F. M. Salem. 门控循环神经网络的参数化减少。MSU 备忘录,2016 年 11 月 7 日。

第十七章:与 LSTMs、RNNs 和前馈网络相比的 GRUs

在本章中,我们将讨论门控循环单元GRU)。我们还将将其与我们在上一章中学到的 LSTMs 进行比较。正如您所知,LSTMs 自 1987 年以来一直存在,并且是目前在深度学习 NLP 中最广泛使用的模型之一。然而,GRUs 首次在 2014 年提出,是 LSTMs 的一个更简单的变体,具有许多相同的属性,训练更容易、更快,并且通常具有更少的计算复杂度。

在本章中,我们将学习以下内容:

  • GRUs

  • GRU 与 LSTM 的不同之处

  • 如何实现 GRU

  • GRU、LTSM、RNN 和前馈比较

  • 网络差异

技术要求

您需要具备使用 Microsoft Visual Studio 和 C#进行.NET 开发的基本知识。您需要从本书网站下载本章的代码。

查看以下视频以查看代码的实际应用:bit.ly/2OHd7o5

QuickNN

要跟随代码,您应该在 Microsoft Visual Studio 中打开 QuickNN 解决方案。我们将使用此代码来详细解释一些细微之处以及不同网络之间的比较。以下是您应该加载的解决方案:

Solution

理解 GRUs

GRUs 是长短期记忆循环神经网络的一个分支。LSTM 和 GRU 网络都有额外的参数来控制它们内部记忆何时以及如何更新。两者都可以捕捉序列中的长短期依赖关系。然而,GRU 网络涉及的参数比它们的 LSTM 表亲要少,因此训练速度更快。GRU 学习如何使用其重置和遗忘门来做出长期预测,同时执行记忆保护。让我们看看一个简单的 GRU 图示:

GRU

LSTM 和 GRU 之间的差异

虽然 LSTM 和 GRU 之间有一些细微的区别,但坦白说,它们之间的相似之处要多于不同之处!首先,GRU 比 LSTM 少一个门。如图所示,LSTM 有一个输入门、一个遗忘门和一个输出门。另一方面,GRU 只有两个门,一个是重置门,一个是更新门。重置门决定了如何将新输入与之前的记忆结合,而更新门定义了之前记忆保留的程度:

LSTM 与 GRU 的比较

另一个有趣的事实是,如果我们把重置门设为全 1,更新门设为全 0,你知道我们有什么吗?如果你猜到是一个普通的循环神经网络,你就对了!

下面是 LSTM 和 GRU 之间的关键区别:

  • 一个 GRU 有两个门,一个 LSTM 有三个。

  • GRU 没有与暴露的隐藏状态不同的内部记忆单元。这是因为 LSTM 有输出门。

  • 输入门和遗忘门通过一个更新门耦合,该更新门权衡新旧内容。

  • 重置门直接应用于前一个隐藏状态。

  • 在计算 GRU 输出时,我们不应用第二个非线性函数。

  • 没有输出门,加权求和就是输出。

使用 GRU 与 LSTM 的比较

由于 GRU 相对较新,通常会出现一个问题,即何时使用哪种网络类型。说实话,这里真的没有明确的赢家,因为 GRU 尚未成熟,而 LSTM 变体似乎每个月都会出现。GRU 确实有更少的参数,理论上可能训练得比 LSTM 快一些。它也可能理论上需要比 LSTM 更少的数据。另一方面,如果你有大量数据,LSTM 的额外功能可能更适合你。

编写不同的网络

在本节中,我们将查看本章前面描述的示例代码。我们将特别查看我们如何构建不同的网络。NetworkBuilder是我们构建本练习所需的四种不同类型网络的主要对象。你可以随意修改它并添加额外的网络,如果你愿意的话。目前,它支持以下网络:

  • LSTM

  • RNN

  • GRU

  • 前馈

你会在我们的示例网络中注意到的一件事是,网络之间的唯一区别在于网络本身是通过NetworkBuilder创建的。所有其余的代码都保持不变。如果你查看示例源代码,你还会注意到 GRU 示例中的迭代次数或周期数要低得多。这是因为 GRU 通常更容易训练,因此需要更少的迭代。虽然我们的正常 RNN 训练大约在 50,000 次迭代(我们让它达到 100,000 次以防万一)完成,但我们的 GRU 训练循环通常在 10,000 次迭代以下完成,这是一个非常大的计算节省。

编写 LSTM

要构建一个 LSTM,我们只需调用我们的NetworkBuilderMakeLstm()函数。这个函数将接受几个输入参数,并返回给我们一个网络对象:

INetwork nn = NetworkBuilder.MakeLstm(inputDimension,
hiddenDimension,hiddenLayers,outputDimension,data.GetModelOutputUnitToUse(),
initParamsStdDev, rng);

如您所见,内部它调用了NetworkBuilder对象内部的MakeLSTM()函数。以下是该代码的查看:

public static NeuralNetwork MakeLstm(int inputDimension, int hiddenDimension, int hiddenLayers, int outputDimension, INonlinearity decoderUnit, double initParamsStdDev, Random rng)
{
List<ILayer> layers = new List<ILayer>();
for (int h = 0; h<hiddenLayers; h++)
{

添加所有隐藏层:

layers.Add(h == 0? new LstmLayer(inputDimension, hiddenDimension, initParamsStdDev, rng): new LstmLayer(hiddenDimension, hiddenDimension, initParamsStdDev, rng));
}

添加前馈层:

layers.Add(new FeedForwardLayer(hiddenDimension, outputDimension, decoderUnit, initParamsStdDev, rng));

创建网络:

return new NeuralNetwork(layers);
}

编写 GRU

要构建一个门控循环单元,我们只需像这里所示的那样调用我们的NetworkBuilderMakeGru()函数:

INetwork nn = NetworkBuilder.MakeGru(inputDimension,
hiddenDimension,
hiddenLayers,
outputDimension,
data.GetModelOutputUnitToUse(),
initParamsStdDev, rng);

MakeGru()函数在内部调用同名的函数来构建我们的 GRU 网络。以下是它是如何做到这一点的:

public static NeuralNetwork MakeGru(int inputDimension, int hiddenDimension, int hiddenLayers, int outputDimension, INonlinearity decoderUnit, double initParamsStdDev, Random rng)
{
List<ILayer> layers = new List<ILayer>();
for (int h = 0; h<hiddenLayers; h++)
 {
 layers.Add(h == 0
 ? newGruLayer(inputDimension, hiddenDimension, initParamsStdDev, rng)
 : newGruLayer(hiddenDimension, hiddenDimension, initParamsStdDev, rng));
 }
layers.Add(new FeedForwardLayer(hiddenDimension, outputDimension, decoderUnit, initParamsStdDev, rng));
return new NeuralNetwork(layers);
}

比较 LSTM、GRU、前馈和 RNN 操作

为了帮助您看到我们一直在处理的所有网络对象在创建和结果上的差异,我创建了下面的示例代码。这个示例将让您看到我们这里四种网络类型在训练时间上的差异。正如之前所述,GRU 是最容易训练的,因此将比其他网络更快完成(迭代次数更少)。在执行代码时,您将看到 GRU 通常在 10,000 次迭代以下就达到最佳错误率,而传统的 RNN 和/或 LSTM 可能需要 50,000 次或更多迭代才能正确收敛。

下面是我们的示例代码的样子:

static void Main(string[] args)
{
Console.WriteLine("Running GRU sample", Color.Yellow);
Console.ReadKey();
ExampleGRU.Run();
Console.ReadKey();
Console.WriteLine("Running LSTM sample", Color.Yellow);
Console.ReadKey();
ExampleLSTM.Run();
Console.ReadKey();
Console.WriteLine("Running RNN sample", Color.Yellow);
Console.ReadKey();
ExampleRNN.Run();
Console.ReadKey();
Console.WriteLine("Running Feed Forward sample", Color.Yellow);
Console.ReadKey();
ExampleFeedForward.Run();
Console.ReadKey();
}

下面是示例运行的结果:

输出 1

输出 2

现在,让我们看看我们如何创建 GRU 网络并运行程序。在下面的代码段中,我们将使用我们的 XOR 数据集生成器为我们生成随机数据。对于我们的网络,我们将有 2 个输入,1 个包含 3 个神经元的隐藏层,以及 1 个输出。我们的学习率设置为 0.001,我们的标准差设置为 0.08。

我们调用我们的NetworkBuilder对象,该对象负责创建我们所有的网络变体。我们将所有描述的参数传递给NetworkBuilder。一旦我们的网络对象创建完成,我们就将这个变量传递给我们的训练器并训练网络。一旦网络训练完成,我们就测试我们的网络以确保我们的结果是令人满意的。当我们创建用于测试的 Graph 对象时,我们确保将 false 传递给构造函数,以让它知道我们不需要反向传播:

public class ExampleGRU
 {
public static void Run()
 {
Random rng = new Random();
DataSet data = new XorDataSetGenerator();
int inputDimension = 2;
int hiddenDimension = 3;
int outputDimension = 1;
int hiddenLayers = 1;
double learningRate = 0.001;
double initParamsStdDev = 0.08;

INetwork nn = NetworkBuilder.MakeGru(inputDimension,
hiddenDimension, hiddenLayers, outputDimension, newSigmoidUnit(),
initParamsStdDev, rng);

int reportEveryNthEpoch = 10;
int trainingEpochs = 10000; // GRU's typically need less training
Trainer.train<NeuralNetwork>(trainingEpochs, learningRate, nn, data, reportEveryNthEpoch, rng);
Console.WriteLine("Training Completed.", Color.Green);
Console.WriteLine("Test: 1,1", Color.Yellow);
Matrix input = new Matrix(new double[] { 1, 1 });
Matrix output = nn.Activate(input, new Graph(false));
Console.WriteLine("Test: 1,1\. Output:" + output.W[0], Color.Yellow);
Matrix input1 = new Matrix(new double[] { 0, 1 });
Matrix output1 = nn.Activate(input1, new Graph(false));
Console.WriteLine("Test: 0,1\. Output:" + output1.W[0], Color.Yellow);
Console.WriteLine("Complete", Color.Yellow);
 }
 }

网络差异

如前所述,我们网络之间的唯一区别是创建并添加到网络对象中的层。在 LSTM 中,我们将添加 LSTM 层,在 GRU 中,不出所料,我们将添加 GRU 层,依此类推。以下显示了所有四种创建函数,供您比较:

public static NeuralNetwork MakeLstm(int inputDimension, int hiddenDimension, int hiddenLayers, int outputDimension, INonlinearity decoderUnit, double initParamsStdDev, Random rng)
{
    List<ILayer> layers = new List<ILayer>();
    for (int h = 0; h<hiddenLayers; h++)
    {
        layers.Add(h == 0
         ? new LstmLayer(inputDimension, hiddenDimension, initParamsStdDev, rng)
         : new LstmLayer(hiddenDimension, hiddenDimension, initParamsStdDev, rng));
    }
    layers.Add(new FeedForwardLayer(hiddenDimension, outputDimension, decoderUnit,         initParamsStdDev, rng));
    return new NeuralNetwork(layers);
}

public static NeuralNetwork MakeFeedForward(int inputDimension, int hiddenDimension, inthiddenLayers, int outputDimension, INonlinearity hiddenUnit, INonlinearity decoderUnit, double initParamsStdDev, Random rng)
{
    List<ILayer> layers = new List<ILayer>();
    for (int h = 0; h<hiddenLayers; h++)
    {
        layers.Add(h == 0? new FeedForwardLayer(inputDimension, hiddenDimension,         hiddenUnit, initParamsStdDev, rng): new FeedForwardLayer(hiddenDimension,         hiddenDimension, hiddenUnit, initParamsStdDev, rng));
    }
    layers.Add(new FeedForwardLayer(hiddenDimension, outputDimension,             decoderUnit, initParamsStdDev, rng));
    return new NeuralNetwork(layers);
 }

public static NeuralNetwork MakeGru(int inputDimension, int hiddenDimension, int hiddenLayers, int outputDimension, INonlinearity decoderUnit, double initParamsStdDev, Random rng)
{
    List<ILayer> layers = new List<ILayer>();
    for (int h = 0; h<hiddenLayers; h++)
    {
        layers.Add(h == 0? new GruLayer(inputDimension, hiddenDimension, initParamsStdDev, rng): new GruLayer(hiddenDimension, hiddenDimension, initParamsStdDev, rng));
    }
    layers.Add(new FeedForwardLayer(hiddenDimension, outputDimension, decoderUnit, initParamsStdDev, rng));
    return new NeuralNetwork(layers);
}

public static NeuralNetwork MakeRnn(int inputDimension, int hiddenDimension, int hiddenLayers, int outputDimension, INonlinearity hiddenUnit, INonlinearity decoderUnit, double initParamsStdDev, Random rng)
{
    List<ILayer> layers = new List<ILayer>();
    for (int h = 0; h<hiddenLayers; h++)
    {
        layers.Add(h == 0? new RnnLayer(inputDimension, hiddenDimension, hiddenUnit, initParamsStdDev, rng)
        : new RnnLayer(hiddenDimension, hiddenDimension, hiddenUnit, initParamsStdDev, rng));
    }
    layers.Add(new FeedForwardLayer(hiddenDimension, outputDimension, decoderUnit, initParamsStdDev, rng));
    return new NeuralNetwork(layers);
}

摘要

在本章中,我们学习了 GRU。我们展示了它们与 LSTM 网络的比较和区别。我们还向您展示了一个示例程序,该程序测试了我们讨论的所有网络类型并生成了它们的输出。我们还比较了这些网络的创建方式。

我希望您在这本书的旅程中过得愉快。作为作者,我们试图更好地了解读者想要看到和听到什么,我欢迎您的建设性评论和反馈,这将有助于使本书和源代码更加完善。直到下一本书,祝您编码愉快!

第十八章:激活函数时间

以下图像展示了不同的激活函数及其相应的图表。在处理单个函数图表时,请以此作为视觉参考:

图片

图片

图片

图片

第十九章:函数优化参考

Currin 指数函数

下面是 Currin 指数函数的图示:

图片

Currin 指数函数

描述

维度:2

这是一个简单的二维示例,在计算机实验文献中多次出现。

输入域

该函数在 xi ∈ [0, 1] 的正方形上评估,对于所有 i = 1, 2。

修改和替代形式

为了多保真度模拟,Xiong 等人(2013)为低保真度代码使用了以下函数:

图片

Webster 函数

下面是 Webster 函数:

图片

Webster 函数

描述

维度:2

该函数由 Webster 等人(1996)使用,假设 A、B 和 Y 之间的关系是一个黑盒。

输入分布

输入随机变量的分布为 A ~ Uniform[1, 10],B ~ N(μ=2, σ=1)。

Oakley & O'Hagan 函数

Oakley & O'Hagan 函数的图示如下:

图片

Oakley & O'Hagan 函数

描述

维度:2

该函数被 Oakley & O'Hagan(2002)用作一个简单的说明性示例,以展示输出分布函数后验均值的计算中的不连续性。

输入域

随机输入变量的域是 xi ∈ [-0.01, 0.01] 的正方形,对于所有 i = 1, 2。

Grammacy 函数

下面是 Grammacy 函数:

图片

Grammacy 函数

描述

维度:2

该函数是一个简单的二维示例,用于说明建模计算机实验输出的方法。

输入域

该函数在 xi ∈ [-2, 6] 的正方形上评估,对于所有 i = 1, 2。

Franke 函数

Franke 函数如下所示:

图片

Franke 函数

描述

维度:2

Franke 函数有两个不同高度的高斯峰和一个较小的凹陷。它被用作插值问题中的测试函数。

输入域

该函数在 xi ∈ [0, 1] 的正方形上评估,对于所有 i = 1, 2。

Lim 函数

下面是 Lim 函数的图示:

图片

Lim 函数

描述

维度:2

这是一个在两个维度上的多项式函数,最高次数为 5。它是非线性的,尽管复杂,但很平滑,这在计算机实验函数中很常见(Lim 等人,2002)。

输入域

该函数在 xi ∈ [0, 1] 的正方形上评估,对于所有 i = 1, 2。

Ackley 函数

让我们看看 Ackley 函数:

图片

Ackley 函数

图片

Ackley 函数

描述

维度:d

Ackley 函数广泛用于测试优化算法。在其二维形式中,如前图所示,它具有几乎平坦的外部区域,以及中心的一个大洞。该函数对优化算法构成风险,尤其是对陷入其许多局部最小值之一的爬山算法。

推荐的变量值为 a = 20, b = 0.2, 和 c = 2π。

输入域

该函数通常在 xi ∈ [-32.768, 32.768] 的超立方体区域内进行评估,对于所有 i = 1, …, d,尽管它也可能被限制在较小的域内。

全局最小值

Bukin 函数 N6

Bulkin 函数 N6 如下所示:

Bulkin 函数 N6

描述

维度:2

第六个 Bukin 函数具有许多局部最小值,它们都位于一个脊上。

输入域

该函数通常在 x1 ∈ [-15, -5],x2 ∈ [-3, 3] 的矩形区域内进行评估。

全局最小值

Cross-In-Tray 函数

Cross-In-Tray 函数看起来如下:

Cross-In-Tray 函数

描述

维度:2

Cross-in-Tray 函数具有多个全局最小值。在第二个图中,它以较小的域显示,以便其特征 交叉 可以被看到。

输入域

该函数通常在 xi ∈ [-10, 10] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

Drop-Wave 函数

Drop-Wave 函数显示如下:

Drop-wave 函数

描述

维度:2

Drop-Wave 函数是多模态且高度复杂的。右侧的先前图显示了在较小的输入域上的函数,以说明其特征。

输入域

该函数通常在 xi ∈ [-5.12, 5.12] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

Eggholder 函数

以图示方式表示的 Eggholder 函数:

Eggholder 函数

描述

维度:2

Eggholder 函数是一个难以优化的函数,因为其具有大量的局部最小值。

输入域

该函数通常在 xi ∈ [-512, 512] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

Holder 表函数

Holder 表函数看起来就像它的名字一样:

Holder 表函数

描述

维度:2

Holder 表函数具有许多局部最小值,有四个全局最小值。

输入域

该函数通常在 xi ∈ [-10, 10] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

Levy 函数

Levy 函数如下所示:

列维函数

描述

维度:d

输入域

函数通常在 xi ∈ [-10, 10] 的超立方体区域内进行评估,对于所有 i = 1, …, d。

全局最小值

列维函数 N13

列维函数 N13 如此展示:

列维函数 N13

描述

维度:2

输入域

函数通常在 xi ∈ [-10, 10] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

拉斯特林函数

拉斯特林函数如下所示:

拉斯特林函数

描述

维度:d

拉斯特林函数有几个局部最小值。它是高度多模态的,但最小值的位置分布规律。它在前面的图中以二维形式展示。

输入域

函数通常在 xi ∈ [-5.12, 5.12] 的超立方体区域内进行评估,对于所有 i = 1, …, d。

全局最小值

沙弗函数 N.2

这里,沙弗函数 N.2 如下所示:

沙弗函数 N.2

描述

维度:2

第二个沙弗函数。它在右侧的较小输入域的图中展示,以显示细节。

输入域

函数通常在 xi ∈ [-100, 100] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

沙弗函数 N.4

沙弗函数 N.4 如下所示:

沙弗函数 N.4

描述

维度:2

第四个沙弗函数。它在右侧的较小输入域的图中展示,以显示细节。

输入域

函数通常在 xi ∈ [-100, 100] 的正方形区域内进行评估,对于所有 i = 1, 2。

沙伯特函数

沙伯特函数在此展示:

沙伯特函数

描述

维度:2

沙伯特函数有几个局部最小值和许多全局最小值。右侧的图显示了在较小输入域上的函数,以便更容易查看。

输入域

函数通常在 xi ∈ [-10, 10] 的正方形区域内进行评估,对于所有 i = 1, 2, 虽然这可能限制在 xi ∈ [-5.12, 5.12] 的正方形区域内,对于所有 i = 1, 2。

全局最小值

旋转超椭球函数

以下为旋转超椭球函数

旋转超椭球函数

描述

维度:d

旋转超椭球函数是连续的、凸的且单峰的。它是轴平行超椭球函数的扩展,也称为和平方函数。图显示了其二维形式。

输入域

函数通常在 xi ∈ [-65.536, 65.536] 的超立方体区域内进行评估,对于所有 i = 1, …, d。

全局最小值

图片

和平方函数

和平方函数如下所示:

图片

和平方函数

描述

维度:d

和平方函数,也称为轴平行超椭球函数,除了全局最小值外没有局部最小值。它是连续的、凸的且单峰的。这里以二维形式展示。

输入域

函数通常在 xi ∈ [-10, 10] 的超立方体区域内进行评估,对于所有 i = 1, …, d,尽管这可能限制在 xi ∈ [-5.12, 5.12] 的超立方体区域内,对于所有 i = 1, …, d。

全局最小值

图片

博斯函数

博斯函数的表示如下:

图片

博斯函数

描述

维度:2

输入域

函数通常在 xi ∈ [-10, 10] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

图片

麦克马科伊函数

麦克马科伊函数如下所示:

图片

麦克马科伊函数

描述

维度:2

输入域

函数通常在 x1 ∈ [-1.5, 4],x2 ∈ [-3, 4] 的矩形区域内进行评估。

全局最小值

图片

功和函数

这里展示了功和函数:

图片

功和函数

描述

维度:d

功和函数。这里以二维形式展示。当 d = 4 时,b 向量的推荐值为:b = (8, 18, 44, 114)。

输入域

函数通常在 xi ∈ [0, d] 的超立方体区域内进行评估,对于所有 i = 1, …, d。

三峰驼函数

三峰驼函数的图形表示如下:

图片

三峰驼函数

描述

维度:2

左侧的图显示了三峰驼函数在其推荐输入域上的图像,右侧的图仅显示了该域的一部分,以便更容易地观察函数的关键特征。该函数有三个局部最小值。

输入域

函数通常在 xi ∈ [-5, 5] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

图片

Easom 函数

下面的 Easom 函数:

图片

Easom 函数

描述

维度:2

Easom 函数有几个局部最小值。它是单峰的,全局最小值相对于搜索空间来说面积较小。

输入域

函数通常在 xi ∈ [-100, 100] 的正方形区域内进行评估,对于所有 i = 1, 2。

全局最小值

图片

米哈伊莱维奇函数

米哈伊莱维奇函数如下所示:

图片

Michalewicz 函数

描述

维度:d

Michalewicz 函数有 d! 个局部极小值,并且是多模态的。m 参数定义了山谷和脊的陡峭程度;较大的 m 导致搜索更困难。m 的推荐值是 m = 10。函数的二维形式在先前的图表中显示。

输入域

该函数通常在 xi ∈ [0, π] 的超立方体上评估,对于所有 i = 1, …, d。

全局极小值

图片

Beale 函数

Beale 函数如下所示:

图片

Beale 函数

描述

维度:2

Beale 函数是多模态的,在输入域的角落有尖锐的峰值。

输入域

该函数通常在 xi ∈ [-4.5, 4.5] 的正方形上评估,对于所有 i = 1, 2。

全局最小值

图片

Goldstein-Price 函数

Goldstein-Price 函数如下所示:

图片

Goldstein-Price 函数

描述

维度:2

Goldstein-Price 函数有几个局部极小值。

输入域

该函数通常在 xi ∈ [-2, 2] 的正方形上评估,对于所有 i = 1, 2。

全局最小值

图片

Perm 函数

Perm 函数如下所示:

图片

Perm 函数

描述

维度:d

Perm d, β 函数。

输入域

该函数通常在 xi ∈ [-d, d] 的超立方体上评估,对于所有 i = 1, …, d。

全局最小值

图片

Griewank 函数

Griewank 函数如下所示:

图片

Griewank 函数

描述

维度:d

Griewank 函数有许多分布广泛的局部极小值,这些极小值分布规律。复杂性在放大图中显示。

输入域

该函数通常在 xi ∈ [-600, 600] 的超立方体上评估,对于所有 i = 1, …, d。

全局最小值

图片

Bohachevsky 函数

Bohachevsky 函数在此处展示:

图片

Bohachevsky 函数

描述

维度:2

Bohachevsky 函数都具有相同的类似碗状形状。前一张图片中显示的是第一个函数。

输入域

这些函数通常在 xi ∈ [-100, 100] 的正方形上评估,对于所有 i = 1, 2。

全局最小值

图片

球函数

图形函数的图形表示如下:

图片

球函数

描述

维度:d

球函数除了全局极小值外,还有 d 个局部极小值。它是连续的、凸的且单峰的。图显示了其二维形式。

输入域

该函数通常在 xi ∈ [-5.12, 5.12] 超立方体上评估,对于所有 i = 1, …, d。

全局最小值

图片

图片

罗森布鲁克函数

罗森布鲁克函数如下所示:

图片

罗森布鲁克函数

描述

维度:d

罗森布鲁克函数,也称为山谷或香蕉函数,是梯度优化算法的一个流行测试问题。它在前面图表中展示了其二维形式。

该函数是单峰的,全局最小值位于一个狭窄的抛物线山谷中。然而,尽管这个山谷容易找到,但收敛到最小值是困难的(Picheny 等人,2012 年)。

输入域

该函数通常在 xi ∈ [-5, 10] 超立方体上评估,对于所有 i = 1, …, d,尽管它可能被限制在 xi ∈ [-2.048, 2.048] 超立方体上,对于所有 i = 1, …, d。

全局最小值

图片

斯蒂布林斯基-唐函数

斯蒂布林斯基-唐函数如下所示:

图片

斯蒂布林斯基-唐函数

描述

维度:d

斯蒂布林斯基-唐函数在此以二维形式展示。

输入域

该函数通常在 xi ∈ [-5, 5] 超立方体上评估,对于所有 i = 1, …, d。

全局最小值

图片

摘要

在本章中,我们向您展示了单个优化函数及其视觉表示、全局和局部最小值以及数学变体。

继续阅读

[1] Surjanovic, S. & Bingham, D. (2013)。虚拟仿真实验库:测试函数和数据集。2018 年 6 月 26 日检索,来自 www.sfu.ca/~ssurjano。经许可重印

Adorio, E. P. 和 Diliman, U. P. MVF - C 语言中用于无约束全局优化的多元测试函数库 (2005)。2013 年 6 月检索,来自 www.geocities.ws/eadorio/mvf.pdf

Molga, M. 和 Smutnicki, C. 测试函数用于优化需求 (2005)。2013 年 6 月检索,来自 www.zsd.ict.pwr.wroc.pl/files/docs/functions.pdf

Back, T. (1996)。进化算法在理论与实践中的应用:进化策略、进化编程、遗传算法。牛津大学出版社需求。

posted @ 2025-10-07 17:57  绝不原创的飞龙  阅读(28)  评论(0)    收藏  举报