main 39ca6c9a6e7e cached
19 files
599.4 KB
324.4k tokens
1 requests
Download .txt
Showing preview only (1,079K chars total). Download the full file or copy to clipboard to get everything.
Repository: datawhalechina/intro-mathmodel
Branch: main
Commit: 39ca6c9a6e7e
Files: 19
Total size: 599.4 KB

Directory structure:
gitextract_hfcu50jj/

├── README.md
└── docs/
    ├── .nojekyll
    ├── CH0/
    │   └── 绪论-走进数学建模的大门.md
    ├── CH1/
    │   └── 第1章-解析方法与几何模型.md
    ├── CH10/
    │   └── 第10章-图像、文本与信号数据.md
    ├── CH2/
    │   └── 第2章-微分方程与动力系统.md
    ├── CH3/
    │   └── 第三章-函数极值与规划模型.md
    ├── CH4/
    │   └── 第4章-复杂网络与图论模型.md
    ├── CH5/
    │   └── 第五章-进化计算与群体智能.md
    ├── CH6/
    │   └── 第六章-数据处理与拟合模型.md
    ├── CH7/
    │   ├── 第7章-权重生成与评价模型.md
    │   └── 第7章-评价类模型.docx
    ├── CH8/
    │   ├── 第8章-时间序列(1).docx
    │   └── 第8章-时间序列.md
    ├── CH9/
    │   ├── 副本第9章-统计与机器学习(1).docx
    │   └── 第九章-机器学习与统计模型.md
    ├── README.md
    ├── _sidebar.md
    └── index.html

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# 数学建模导论【🧪 Beta公测版】
【🧪 Beta公测版本提示:教程主体已完成,正在优化细节,欢迎大家提Issue反馈问题或建议。】

项目教程地址:https://datawhalechina.github.io/intro-mathmodel/#/

- 项目背景:本项目基于我的课程《数学建模导论——基于Python语言》整理而来,是为了帮助学生更好的学习数学建模、数据科学知识所用。可用于大学生参加数学建模竞赛,也可用作工作中的平时学习。万物皆可建模,人工智能的本质也是数学模型,所以我们开放这门课程的教程。

- 项目内容目录:本项目一共包含十章内容,包括解析几何与方程模型、微分方程与动力系统模型、函数极值与规划模型、复杂网络与图论模型、进化计算与群体智能算法、数据处理与拟合模型、权重生成与评价模型、时间序列与投资模型、机器学习与统计模型、多模态数据处理模型等十个方面内容,旨在尽可能多地向大家展示数学建模中所用到的数学基础与算法知识,打造属于自己的数学建模宇宙。

  ---

  ## RoadMap

  目前稿件基本已经完工

  ---

  ## 参与贡献

  - 如果你想参与到项目中来欢迎查看项目的 [Issue](https://github.com/datawhalechina/repo-template/blob/main/docs) 查看没有被分配的任务。
  - 如果你发现了一些问题,欢迎在 [Issue](https://github.com/datawhalechina/repo-template/blob/main/docs) 中进行反馈🐛。
  - 如果你对本项目感兴趣想要参与进来可以通过 [Discussion](https://github.com/datawhalechina/repo-template/blob/main/docs) 进行交流💬。

  如果你对 Datawhale 很感兴趣并想要发起一个新的项目,欢迎查看 [Datawhale 贡献指南](https://github.com/datawhalechina/DOPMC#为-datawhale-做出贡献)。

  ---

  ## 贡献者名单

  > 项目负责人&主编:马世拓(若冰,马马老师,B站id:平成最强假面骑士)
  >
  > 编辑&校对:
  >
  > - 陈思州(小红花):第1章,第2章,第10章
  > - 刘旭(卡拉比丘流形):第3章,第5章
  > - 萌弟:第6章,第8章
  > - 聂雄伟(牧小熊):第7章,第8章
  > - 邢硕(Susan):第4章,第9章
  >
  > 排版&美工:何瑞杰(拟身怪乖宝宝),聂雄伟(牧小熊),陈思州(小红花),骆秀韬(Epsilon Luoo)

## 关注我们



扫描下方二维码关注公众号:Datawhale

[![img](https://raw.githubusercontent.com/datawhalechina/pumpkin-book/master/res/qrcode.jpeg)](https://raw.githubusercontent.com/datawhalechina/pumpkin-book/master/res/qrcode.jpeg)

## LICENSE



[![知识共享许可协议](https://camo.githubusercontent.com/9a588afd926871cf6caaabf8f36acf441a53ed69540c3808e77861fbd3711203/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d434325323042592d2d4e432d2d5341253230342e302d6c6967687467726579)](http://creativecommons.org/licenses/by-nc-sa/4.0/)
本作品采用[知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议](http://creativecommons.org/licenses/by-nc-sa/4.0/)进行许可。

# 目录

- 绪论:走进数学建模的大门
- 第1章:解析几何与方程模型
  - 1.1 向量表示法与几何建模基本案例
  - 1.2 Numpy与线性代数
  - 1.3 平面几何模型的构建
  - 1.4 立体几何模型的构建
  - 1.5 使用Python解方程与方程组
- 第2章:微分方程与动力系统
  - 2.1 微分方程的理论基础
  - 2.2 使用Scipy与Sympy解微分方程
  - 2.3 偏微分方程的数值求解
  - 2.4 微分方程的应用案例
  - 2.5 差分方程的应用案例
  - 2.6 元胞自动机与仿真模拟
  - 2.7 数值计算方法与微分方程求解
- 第3章:函数极值与规划模型
  - 3.1 从线性代数到线性规划
  - 3.2 使用Numpy进行矩阵计算
  - 3.3 线性规划的算法原理
  - 3.4 线性规划的建模案例
  - 3.5 从线性规划到非线性规划
  - 3.6 非线性规划的建模案例
  - 3.7 整数规划与指派问题
  - 3.8 使用scipy与cvxpy解决规划问题
  - 3.9 动态规划与贪心算法
  - 3.10 博弈论与排队论初步
  - 3.11 多目标规划
  - 3.12 蒙特卡洛模拟
- 第4章:复杂网络与图论模型
  - 4.1 复杂网络概念与理论
  - 4.2 图论算法:遍历,二分图与最小生成树
  - 4.3 图论算法:最短路径与最大流问题
  - 4.4 使用Networkx完成复杂网络建模
  - 4.5 TSP问题与VRP问题
- 第5章:进化计算与群体智能
  - 5.1 遗传算法理论与实现
  - 5.2 粒子群算法理论与实现
  - 5.3 蚁群算法理论与实现
  - 5.4 模拟退火算法理论与实现
  - 5.5 使用sci-opt实现群体智能算法
- 第6章:数据处理与拟合模型
  - 6.1 什么是数据
  - 6.2 数据的预处理
  - 6.3 使用Pandas进行数据处理与分析
  - 6.4 插值模型
  - 6.5 回归与拟合模型
  - 6.6 数据可视化与matplotlib、seaborn
- 第7章:权重生成与评价模型
  - 7.1 层次分析法
  - 7.2 熵权分析法
  - 7.3 TOPSIS分析法
  - 7.4 CRITIC法
  - 7.5 模糊综合分析法
  - 7.6 秩和比分析法
  - 7.7 主成分分析法
  - 7.8 因子分析法
  - 7.9 数据包络分析法
  - 7.10 评价模型总结
- 第8章:时间序列与投资模型
  - 8.1 时间序列的基本概念
  - 8.2 移动平均法与指数平滑法
  - 8.3 ARIMA系列模型
  - 8.4 GARCH系列模型
  - 8.5 灰色系统模型
  - 8.6 组合投资问题的一些策略
  - 8.7 马尔可夫模型
- 第9章:机器学习与统计模型
  - 9.1 统计分布与假设检验
  - 9.2 回归不止用于预测
  - 9.3 使用scipy与statsmodels完成统计建模
  - 9.4 机器学习的研究范畴
  - 9.5 使用scikit-learn完成机器学习任务
  - 9.6 基于距离的KNN模型
  - 9.7 基于优化的LDA和SVM模型
  - 9.8 基于树形结构的模型
  - 9.9 集成学习模型与GBDT
  - 9.10 从神经网络到深度学习
  - 9.11 使用PyTorch构造神经网络
  - 9.12 聚类算法
  - 9.13 关联关系挖掘模型
  - 9.14 图数据与PageRank算法
  - 9.15 朴素贝叶斯模型
- 第10章:多模数据与智能模型
  - 10.1 数字图像处理与计算机视觉
  - 10.2 计算语言学与自然语言处理
  - 10.3 数字信号处理与智能感知
  - 10.4 多模态数据与人工智能



================================================
FILE: docs/.nojekyll
================================================



================================================
FILE: docs/CH0/绪论-走进数学建模的大门.md
================================================
<center><h1>绪论 走进数学建模的大门</h1></center>

> 项目负责人&主编:马世拓(若冰,马马老师,B站id:平成最强假面骑士)
>
> 编辑&校对:
>
> - 陈思州(小红花):第1章,第2章,第10章
> - 刘旭(卡拉比丘流形):第3章,第5章
> - 萌弟:第6章,第8章
> - 聂雄伟(牧小熊):第7章,第8章
> - 邢硕(Susan):第4章,第9章
>
> 排版&美工:何瑞杰(拟身怪乖宝宝),陈思州(小红花),聂雄伟(牧小熊)
>
> 特别鸣谢:CSDN博主youcans_(<https://blog.csdn.net/youcans>)和 洋洋菜鸟(<https://blog.csdn.net/qq_25990967>)的博客教程内容对本教程的支持与帮助!

何谓“数学建模”?我想这个问题其实并没有一个明确的定义。老宗师姜启源先生说,“数学建模就是建立数学模型解决实际问题”,我却总感觉这个定义还是抽象了些。“建立数学模型”,什么是“数学”的“模型”?怎样建这个“模型”?用什么去建这个“模型”?在我刚开始读本科的时候,我也对这个问题感到很疑惑,于是去拜读了很多人的作品,诸如姜启源、谢金星二位先生的《数学模型》、司守奎先生的《数学建模算法与应用》等很多经典教材,隐约是能够觉得,这个“数学”的“模型”本质上还是一些数学知识,而建这个“模型”则不能手算,须用计算机去设计程序。至少姜先生告诉了我数模的本质,司先生告诉了我数模的方法。

![alt text](image.png)

<center>图0.1 人类认识世界的三种模型 </center>

可道理和工具都有了,应该如何建立这个“模型”呢?

这个问题我从20年就在思考,一直思考至今。朋友,请不要被“数学建模”这四个字劝退,它的本质还是解应用题,只是曾经的“小明买糖”变成了如今的“嫦娥探月”。可数学建模若是仅仅作为解题的手段而存在,那我便不会愿意花上四年时间去学它。数学建模作为一种思想,它是真真切切在工程问题中有所应用的。

我想,一些照本宣科的老师若是来上数学建模这门课,给你们讲的第一个例子大概率是船划水或包饺子。我不愿意照本宣科,也并不打算去介绍一些你们听着简单无聊可在实际工程中却没多大用处的例子。我便以身边最近的一个例子介绍吧:

2020年初,新冠病毒的到来打乱了我们的正常生活。在新冠肺炎的防治过程中就充满了数学建模的影子:如果读者翻到第三章,我会介绍SEIR模型,也就是传染病模型,我们把人群分为易感人群、密切接触者、感染者和康复者四类人群,这四类人群的新增、减少都遵循着动力系统的一些规律,我们可以列出一个微分方程组对其进行模拟:

$$
\begin{array}{c}
N\frac{{ds}}{{dt}} =  - N\lambda si\\
N\frac{{de}}{{dt}} = N\lambda si - N\delta e\\
N\frac{{di}}{{dt}} = N\delta e - N\mu i\\
N\frac{{dr}}{{dt}} = N\mu i\\
s(t) + e(t) + i(t) + r(t) = 1
\end{array}
$$

读者现在可能并不知道这个方程组是什么意思,没有关系,在后续的学习中我们会一点点解释。而除了解微分方程组以外,我想很多读者还见过“一个教室里面的同学如果封锁了教室传染病会如何传染”这样的动画,而这样的动画可以通过元胞自动机去进行模拟。医生在研发新药、在患者身上进行试验的时候,会收集相应的实验数据,而对这些实验数据的分析、假设检验则又充满了统计学知识的应用……

所以,你看到了,这个过程充满了数学建模。

一个好的模型,它能够准确地反映问题,但又不失简洁性。我历来信奉一条最根本的道义,那就是“大道至简”。什么样的模型可以算得上一个好模型呢?我认为它需要遵循以下要点:

- 形式简洁:模型不至于太冗长,大道至简。
- 精度到位:求解精度符合工程实际的要求。
- 理论创新:在理论层面上进行一些创新。
- 排除干扰:能够排除一些无关紧要的干扰项。
- 可解释性:模型的结果有良好的可解释性。
- 求解方便:模型能够利用MATLAB等求解工具进行求解。

> 注意:好的模型需要不断进行调整,前人的一些好模型我们仍然可以进行改进。

数学建模是一门十分注重理论联系实际的课程,它有助于培养学生的数学思维与想象力;有助于培养学生的工程应用能力;有助于学生了解量化研究的一般方法。但在高等院校数学教学中,数学建模这门学科的学习是一门比较艰难的事情,是一个需要长期积累与总结的工作。对于这门学科,学生不好学、教师不好教的问题由来已久,其原因归根结底,仍然是由于缺乏对这门学科课程框架的统一共识与系统化的课程架构。数学建模课上教什么?怎么教?学生在下面准备数学建模竞赛备什么?怎么备?要达到一个怎样的标准才算是备好了?可能很多的学生和教师们都还有这样一个疑问。

数学模型不同于数学建模,二者是有区别的。而数学建模这门学科要是说想用一套统一的方法去学是很难。这也就是说,数学建模领域的研究方法很难形成一套绝对统一的研究范式。不同应用领域、不同数学知识相互交融,所使用的建模方法论也不可能做到一言以蔽之。诚然,数学建模没有一个完整的通用的体系可循,但就本科阶段的教学与竞赛需求而言,建模不同于模型案例的拼盘,它是有法可循的。

春秋时期著名的哲学家老子在《道德经》一书中说到:“人法地,地法天,天法道,道法自然”,这一思想是体现了“天人合一”的思想。在长期实践中,总结出以中国道家古典哲学为思想内核的“道与理”课程框架。以学生对数学建模课程认知过程为基础,将数学建模知识的教学划分为“道——法——术——器”四个阶段,对现有数学建模课程知识内容进行了整合与重构。

![](image-1.png)

<center>图0.2 数学建模的四个境界 </center>

纵观多年全国大学生数学建模竞赛赛题,问题类型大致可以分为“以模型为核心”的优化类、过程类问题,与“以数据为核心”的统计类、分析类问题。这为数学建模课程教学提供了一个很好的思路:以模型与数据为教学中的两条核心主线,将数学建模课程的知识进行串联、归纳与整合。为此,构建了“以数为本”和“以模为本”的数学建模知识体系架构,如图所示:

![](image-2.png)

<center>图0.3 数学建模导论教程的章节逻辑 </center>

在双主线的数学建模知识框架下,将学生需要掌握与学习的模型大体分为了十个章节。十章内容环环相扣,能够很系统地串联起数学建模教学中的主要知识体系。例如,从向量与几何谈起,讨论到几何图形中位置关系与数量关系的抽取与方程构建和求解;再从一般方程的求解到微分方程的求解,介绍函数与多元函数的微分和微分方程,从数学理论到代码实现,再到应用案例,并创新性地将元胞自动机融合于本章教学;随后从微分方程一章的数值方法出发引出函数的极值问题,从而引出规划模型的原理、求解与建模案例,以及博弈论、排队论等同样具有优化性质的模型;再介绍图论的基本概念与典型问题后,将重点聚焦于最短路径、TSP问题等的离散优化模型在图论中的应用,从而又引出新需求;智能计算与优化从TSP问题的解法谈起,引出遗传算法等智能优化方法。“以模为本”能够较好地串联起运筹学、微分方程、动力系统等知识内容。

而“以数为本”的主线则强调培养学生的“数据驱动”思维。随着人工智能与大数据技术的发展,数学建模的竞赛教学与研究得到了极大的推动,有越来越多的数学建模竞赛题开始融入“大数据分析”类的问题,而为了应对这一变化,也有越来越多的高校在数学建模课程中融入了数据分析的内容。将前面的模型应用于数据中于是有了最小二乘拟合的优化等数据处理与拟合方法;在数据处理的基础上可以对数据进行评价;将时间序列这一特殊形式的数据抽象为回归问题介绍预测处理方法,并介绍投资组合问题的优化形式;在数据处理和优化背景的基础上对数据进行挖掘建模,重点介绍传统数学课程中难以展开的机器学习方法;对于一些非常规数据,也创新性地借鉴了计算机视觉、自然语言处理领域的方式方法,对其进行有针对性选择融入到课程教学中。

这门课程起源于我在2022年最早的一版数学建模导论教程<https://www.bilibili.com/video/BV12W4y1C7Tr>,在网络上的反馈还是很不错的。如今我把它放到datawhale的平台,希望能让更多的学习者参与学习。新的导论在以往内容的基础上新增了许多补充性内容,也参考了很多大师的作品。其中非常感谢CSDN博主洋洋菜鸟和youcans_二位的数学建模博客,有部分章节的案例参考过二位的作品,二位也对本教程表示非常支持。也非常感谢datawhale数学建模导论项目组团队成员以及开源项目保姆团队,让本教程顺利孵化。

现在,同学们,让我们一同走进数学建模的大门吧!


================================================
FILE: docs/CH1/第1章-解析方法与几何模型.md
================================================
<center><h1>第1章  解析方法与几何模型</h1></center>

> 内容:@若冰(马世拓)
>
> 审稿:@陈思州
>
> 排版&校对:@何瑞杰

读者朋友们,在这一章当中我们将会主要学习几何模型的相关知识。你们应该都还记得一些基本的几何原理知识,用具体的几何形状来抽丝剥茧为大家介绍数学建模这再合适不过了。而在这一章中,我们不仅会学习到如何分析问题、构造数学模型,更重要的是学会如何使用代码求解一些简单的模型。本章我们希望各位能够:

* **了解一些常见的几何模型构造的思想方法**
* **对常见立体几何模型与平面几何模型能够有自己的思考**
* **可以使用Python求解一些简单方程或方程组的解**

> 注意:本章内容不会太难,大家注意在简单案例中蕴含的数学思想即可。

## 1.1  向量表示法与几何建模基本案例

### 1.1.1  几何建模的思想

人类对于数学世界的探索起源于两样东西:一是计数,二是丈量。计数用以表示多少,要计算多了多少少了多少,于是有了数字的概念和四则运算,也就有了后来的代数学;丈量是测量土地的长宽面积、测量角度、分析几何关系等,也就有了后来的几何学。高中毕业以后,我们对于数学模型的理解其实还很浅薄,但几何模型对我们来讲是最直观的东西。有一张图,我们就可以直观地感受:哪两个平面平行,哪两条线垂直……通过一系列的几何定理还可以推算线段的长度等等。所以,我们就以几何模型作为了解数学建模的切入点。

但是大家可万万不要小看几何模型,虽然一些基本的数学公式我们在初中高中就学过了,这类问题还是大有讲究的。几何模型中充斥了不同的数学关系,根据关系的分类可以把它们分为位置关系和数量关系两种。位置关系就包括平行、垂直、异面、相交等,数量关系则是需要具体求解边长、角度、面积等。那么既然涉及到求解,就必然会涉及到等量关系也就是方程与函数。以往的这些方程与函数是可以手动计算的,但在一个复杂的几何框架中,量与量之间的关系,还会有那么容易计算吗?

分析几何问题,在现在这个阶段我们所掌握的方法大体上可以分为三种:

* **传统几何的演绎-证明体系**:这种体系下的方法都是基于已经被证明了的公理与定理体系(例如勾股定理、正弦定理、圆幂定理等),在解决问题的过程中更强调分析而非计算,往往是通过构造辅助线、辅助平面等利用严密的逻辑推理步步为营推导出最后的结果。这种方法往往分析起来会更加困难,但减少了计算量。
* **基于向量的计算化几何**:向量被引入几何当中最初的目的是为了表示有向线段,但后来大家发现基于向量的一些运算特性可以把一些数量问题统一化。几何图形中的边长、角度、面积可以转化为向量的模长、内积等求解,平行、垂直等可以转化为向量共线、内积为0等求解,就可以把所有几何问题都变成可计算的问题。这样的方法更加重视计算,并且除了传统的几何向量外,还可以构造直角坐标系从而获得坐标向量,运算更加方便。
* **基于极坐标与方程的解析几何**:这种方法其实可以回溯到当初学圆锥曲线的时期,把几何图形的相交、相切、相离抽象成方程解的问题。后来又学习过极坐标和参数方程,就会发现利用极坐标和参数方程去表示曲线实在是太方便了。这样的方法就可以把几何问题转化成一个代数问题来求解,大大提高了求解的效率。

同学们在中学阶段还记得都用过哪些几何方法去进行建模吗?我想,那真的太多了,但我们总能够总结出一些常见的公理与定理,例如: 

* **三角形中的角度关系**:这是几何学中的基本概念之一,涉及到三角形内角和定理(三角形的三个内角之和等于$180\circ$)、直角三角形的角度关系(一个角为$90^{\circ}$,另外两个角之和为$90^{\circ}$)等。
* **勾股定理**:这是直角三角形中最著名的定理之一,表明直角三角形的两条直角边的平方和等于斜边的平方。即如果直角三角形的直角边长分别为$a$和$b$,斜边长为$c$,则有 $a^{2} + b^{2} = c^{2}$。
* **正弦定理**:这是解决三角形问题时非常有用的定理,它表明在任意三角形中,各边的长度与其对应角的正弦值之比相等。即如果三角形的边长分别为$a$、$b$、$c$,对应的角分别为$A$、$B$、$C$,则有 $\displaystyle \frac{a}{\sin A} = \frac{b}{\sin B} = \frac{c}{\sin C} = 2R$,其中$R$是三角形外接圆的半径。
* **余弦定理**:在任意三角形中,任意一边的平方等于其他两边的平方和减去两倍的其他两边长度乘以它们夹角的余弦值。即如果三角形的边长分别为$a$、$b$、$c$,对应的角分别为$A$、$B$、$C$,则有 $c^{2} = a^{2} + b^{2} - 2ab\cos C$。余弦定理在解决非直角三角形的问题中非常有用,尤其是在涉及边长和角度关系的问题中。
* **圆幂定理**:对于任意一点到圆的两条切线,它们的长度是相等的。对于任意一条经过该点的割线,割线的两段长度的乘积是一个常数,这个常数等于该点到圆心的距离的平方减去圆的半径的平方。圆幂定理在解决与圆有关的几何问题中非常有用。
* **切割线定理**(也称为割线-切线定理):如果一条直线从外部点P切割圆,形成一条切线段$PT$和一条割线段$PAB$(A和B是割线与圆的交点),那么$PT^{2} = PA \cdot PB$。这个定理在解决圆和直线关系的几何问题中很有帮助。
* **四点共圆**:四点共圆是一个判断四个点是否能在同一个圆上的几何概念。如果四个点$A$、$B$、$C$、$D$满足某种特定的关系,那么它们可以位于同一个圆上。这个概念在解决几何问题时非常重要,尤其是在证明四个点共圆的时候。常用的判断四点共圆的方法包括使用圆的方程、利用角的性质(如对顶角、圆周角等)等。
* **圆锥曲线的几何性质**:圆锥曲线(包括椭圆、双曲线和抛物线)拥有许多独特的几何性质。例如,椭圆的每一点到两个焦点的距离之和是一个常数;双曲线的每一点到两个焦点的距离之差的绝对值是一个常数;抛物线上的每一点到焦点的距离等于该点到准线的距离。
* **圆锥曲线的光学性质**:圆锥曲线的光学性质是指光线在圆锥曲线上的反射和折射特性。例如,椭圆上的任意一点反射到两个焦点的光线路径长度相等;抛物线上的任意一点反射到焦点的光线都平行于对称轴;双曲线上的任意一点反射到一个焦点的光线将经过另一个焦点。
* 圆锥曲线的离心率:圆锥曲线的离心率(eccentricity)是一个非负实数,它描述了圆锥曲线的形状。对于椭圆,离心率$e$满足$0\leqslant e<1$;对于双曲线,$e>1$;对于抛物线,$e=1$。离心率越小,椭圆越接近圆形;离心率越大,双曲线的两支越开。

当然,常见的其实远不止这么一点,这里罗列的是笔者觉得比较重要的几何定理,特地做一个总结。实际运用的时候有很多东西都是可以用的。

### 1.1.2  向量表示与坐标变换

我相信各位同学在高中肯定是知道向量这个概念的,但是你们最多认识到三维,并且还依赖于画图,中学阶段我们也仅仅是接触到了三维向量。现在如果我告诉你,一个向量可以有不止三个轴,可以有$5$维,有$10$维,有$10000$维,你现在还能依靠图理解向量吗?事实上向量的维数可以是很多维,从代数的意义上你可以认为向量是一个集合,从几何的意义上你又可以认为向量是一个n维欧几里得空间中的一个点:
$$
x = [x_{1}, x_{2}, \dots, x_{n}]^{\top}. \tag{1.1.1}
$$
和二维、三维空间中的向量一样,高维空间中的向量同样可以进行加减运算、数量乘运算和数乘运算。但毕竟这是一门应用数学课程,我们不打算把太多精力放在任何一本线代课本里面都能找到的公式上,使用 Python 的NumPy 库举例子恐怕会更加直观。从程序设计的角度来看,如果读者接触过C语言应该会了解数组的概念,而在 C++ 语言中 STL 里面已经包含了 `vector` 类型。在 Python 中我们可以使用 NumPy 库来创建和操作向量,例如:

```python
import numpy as np
x = np.array([1, 2, 3, 5, 8])
# array([1, 2, 3, 5, 8])
```

这样我们便创建了一个向量。随后便可以进行各种操作。

> 注意:分隔符也可以用分号或空格,但得到向量的形状是不同的。

引入向量的目的并不仅仅是为了在几何图形中更好地表示方向和距离,而是为了利用代数的方法来解决几何问题。向量提供了一种将几何概念转化为代数表达式的方式,从而使得几何问题的解决变得更加简单和直接。

例如,在解决物理问题时,力、速度和加速度等物理量都可以用向量表示。通过向量的加减和数乘运算,我们可以直接计算出合力、相对速度等结果,而不需要借助复杂的几何图形。在计算机图形学中,向量被广泛用于表示和处理图形和动画。通过向量运算,我们可以实现图形的旋转、缩放、平移等变换,以及计算光线的反射和折射等效果。

解析几何法的本质就是利用函数与方程来表示不同的几何曲线。在中学阶段我们都学习过圆锥曲线的方程形式,但在实际问题中我们面临的曲线会更加复杂。尤其是在三维空间中的曲线与曲面,可能会用到多元函数去进行表示,也可能用极坐标或参数方程更加方便,但不管怎么说,解析几何方法的本质就是把各种几何问题都转化为代数问题求解。解方程比起复杂的分析,更依靠计算,而这恰恰是程序所擅长的。

在数学中,坐标变换通常涉及到一系列的矩阵运算,这些矩阵描述了一个坐标系相对于另一个坐标系的位置和方向。旋转变换就是其中的一个典型例子。当我们说一个坐标系相对于另一个坐标系进行了旋转,我们通常是指它绕着一个轴或者点旋转了一定的角度。二维空间的旋转可以简化为点绕原点旋转,而三维空间则涉及到更复杂的轴向旋转。

![700](./attachments/1-1.png)

<center>图1 二维坐标系中的旋转</center>

在二维空间中,如果我们要将坐标系绕原点旋转一个角度,就可以通过旋转矩阵来实现。旋转矩阵是一个非常简单而又强大的工具,它可以将原始坐标系中的点通过线性变换映射到新坐标系中。对于逆时针旋转,二维旋转矩阵的形式是

$$
\left[ \begin{matrix}
\cos \theta & -\sin \theta\\ \sin \theta & \cos \theta
\end{matrix} \right] . \tag{1.1.2}
$$

这里,$\theta$是旋转角度,当应用这个旋转矩阵于一个点$(x, y)$,它会给出新的坐标$(x', y')$,这表示了原始点在新坐标系中的位置。

使用NumPy进行这样的变换非常简单。首先,我们创建一个表示点坐标的NumPy数组,然后创建表示旋转矩阵的二维数组。通过对这两个数组进行点积运算(也就是矩阵乘法),我们就可以得到新的坐标,在Python中可以这样实现:

```python
import numpy as np

# 设定旋转角度,这里我们以30度为例
theta = np.radians(30)  # 将30度转换为弧度

# 创建旋转矩阵
rotation_matrix = np.array([
    [np.cos(theta), -np.sin(theta)],
    [np.sin(theta), np.cos(theta)]
])

# 假设我们有一个点 (a, b)
point = np.array([a, b])

# 通过旋转矩阵变换这个点的坐标
rotated_point = rotation_matrix.dot(point)

print("原坐标为:", point)
print("旋转后的坐标为:", rotated_point)
# 原坐标为: [5 3]
# 旋转后的坐标为: [2.83012702 5.09807621]
```

此示例代码中,旋转角度是预设的,你可以根据实际情况调整。通过这种方式,我们能够将几何问题通过坐标变换转化为代数问题,使用编程方法来进行高效的计算。这不仅仅适用于理论数学问题,同样也适用于工程、物理学、计算机图形学以及机器人技术等多个领域中。

![](./attachments/1-2.png)

<center>图2 三维坐标系中绕三个坐标轴的旋转</center>

在三维空间中,物体的旋转可以围绕三个主轴进行:$\mathrm{x}$轴, $\mathrm{y}$轴和$\mathrm{z}$轴。这些轴旋转代表了不同方向的运动,并且可以通过旋转矩阵来数学描述。例如,一个点$P(x, y, z)$绕$\mathrm{z}$轴旋转角度$\alpha$可以表示为

$$
R_{\mathrm{z}}(\alpha) = \left[ \begin{matrix}
\cos\alpha & -\sin\alpha & 0\\sin\alpha & \cos\alpha & 0\\0 & 0 &1
\end{matrix} \right], \tag{1.1.3}
$$

这个旋转保持$\mathrm{z}$坐标不变,同时在$XY$平面上变换$\mathrm{x}$和$\mathrm{y}$坐标。相似地,点$P$绕$\mathrm{y}$轴旋转角度$\beta$的旋转矩阵为

$$
R_{\mathrm{y}}(\beta) = \left[ \begin{matrix}
\cos\beta & 0 & \sin\beta\\
0 & 1 & 0\\
-\sin\beta & 0 & \cos\beta
\end{matrix} \right], \tag{1.1.4}
$$

这个旋转保持$\mathrm{y}$坐标不变,同时在$XZ$平面上变换$\mathrm{x}$和$\mathrm{z}$坐标。而点$P$绕$\mathrm{x}$轴旋转角度$\gamma$的旋转矩阵为

$$
R_{\mathrm{x}}(\gamma) = \left[ \begin{matrix}
1 & 0 & 0\\
0 & \cos\gamma & -\sin\gamma\\
0 & \sin\gamma & \cos\gamma
\end{matrix} \right], \tag{1.1.5}
$$

这个旋转保持$\mathrm{x}$坐标不变,同时在$YZ$平面上变换$\mathrm{y}$和$\mathrm{z}$坐标。若点$P$需同时围绕三个轴旋转,则最终旋转矩阵$R$为这三个矩阵的乘积,即$R = R_{\mathrm{z}}(\alpha)R_{\mathrm{y}}(\beta)R_{\mathrm{x}}(\gamma)$。需要注意的是,由于矩阵乘法的非交换性,旋转的顺序会影响最终结果。

在实际应用中,如机器人学、航空航天和计算机图形学,旋转顺序对于模拟和预测物体如何移动至关重要。例如,飞机的姿态控制就极依赖于绕不同轴的旋转顺序,以精确地模拟和控制飞机的行动。在三维建模和动画制作中,这些旋转变换同样是创建动态、逼真场景的基础。

在Python中,利用NumPy库,我们可以使用如下代码片段来实现三维旋转变换:

```python
import numpy as np

# 定义旋转角度(以弧度为单位)
alpha = np.radians(30)  # 绕 Z 轴旋转
beta = np.radians(45)   # 绕 Y 轴旋转
gamma = np.radians(60)  # 绕 X 轴旋转

# 定义旋转矩阵
R_z = np.array([[np.cos(alpha), -np.sin(alpha), 0],
                [np.sin(alpha), np.cos(alpha), 0],
                [0, 0, 1]])

R_y = np.array([[np.cos(beta), 0, np.sin(beta)],
                [0, 1, 0],
                [-np.sin(beta), 0, np.cos(beta)]])

R_x = np.array([[1, 0, 0],
                [0, np.cos(gamma), -np.sin(gamma)],
                [0, np.sin(gamma), np.cos(gamma)]])

# 总旋转矩阵
R = R_z @ R_y @ R_x

# 定义点P的坐标
P = np.array([1, 2, 3])

# 计算旋转后的坐标
P_rotated = R @ P

print("旋转后P点的坐标为:", P_rotated)
# 旋转后P点的坐标为: [3.39062937 0.11228132 1.57829826]
```

在该代码中,点$P$经过由$\alpha$,$\beta$,和$\gamma$定义的旋转后,其新坐标由$P_{\text{rotated}}$给出。该代码首先创建了绕$\mathrm{z}$,$\mathrm{y}$,和$\mathrm{x}$轴的三个旋转矩阵,然后将它们相乘得到一个总的旋转矩阵$R$,并应用这个矩阵来转换点$P$的坐标。

正是通过这些准确的数学变换和编程实现,我们能够在计算机模拟和实际应用中处理复杂的三维空间问题。无论是设计复杂的机械系统、创建逼真的三维动画,还是开发高级的虚拟现实环境,三维旋转都是不可或缺的基础。

![](./attachments/1-3.png)

<center>图3 欧拉角图示</center>

欧拉角是三维空间中用于表示一个物体相对于一个固定坐标系(通常是参考坐标系或世界坐标系)的方向的一组角。这种表示方法定义了三次旋转,将物体从其初始方向旋转到期望方向。欧拉角通常表示为三个角度:$\alpha$,$\beta$,和$\gamma$分别对应于绕$\mathrm{z}$轴,$\mathrm{x}$轴(或$\mathrm{y}$轴),以及再次$\mathrm{y}$轴(或$\mathrm{x}$轴)的旋转。

欧拉角旋转顺序的不同,定义了不同的旋转方式,最常见的是:

* **Z-Y-X(Roll-Pitch-Yaw)**:首先绕$\mathrm{z}$轴旋转$\alpha$(Yaw),然后绕*新位置的*$\mathrm{y}$轴旋转$\beta$(Pitch),最后绕*新位置的*$\mathrm{x}$轴旋转$\gamma$(Roll)。
* **Z-X-Y(Yaw-Pitch-Roll)**:首先绕$\mathrm{z}$轴旋转$\alpha$(Yaw),然后绕*新位置的*$\mathrm{x}$轴旋转$\beta$(Pitch),最后再次绕$\mathrm{y}$轴旋转$\gamma$(Roll)。

每次旋转都是围绕变换后的轴,而不是初始的固定轴。这些旋转通常通过旋转矩阵进行计算,并且可以合成为一个单一的矩阵,它描述了总的旋转。在实际应用中,如飞行动力学和计算机图形学,欧拉角非常重要。

这里是Python中计算欧拉角旋转的代码示例,假设采用$Z$-$Y$-$X$顺序:

```python
import numpy as np

# 定义欧拉角(以弧度为单位)
alpha = np.radians(30)  # 绕 z 轴的 Yaw 角度
beta = np.radians(45)   # 绕 y 轴的 Pitch 角度
gamma = np.radians(60)  # 绕 x 轴的 Roll 角度

# 构建对应的旋转矩阵
R_z = np.array([[np.cos(alpha), -np.sin(alpha), 0],
                [np.sin(alpha), np.cos(alpha), 0],
                [0, 0, 1]])
R_y = np.array([[np.cos(beta), 0, np.sin(beta)],
                [0, 1, 0],
                [-np.sin(beta), 0, np.cos(beta)]])
R_x = np.array([[1, 0, 0],
                [0, np.cos(gamma), -np.sin(gamma)],
                [0, np.sin(gamma), np.cos(gamma)]])

# 总旋转矩阵,注意乘法的顺序
R = np.dot(R_x, np.dot(R_y, R_z))

print("组合旋转矩阵为:")
print(R)
# 组合旋转矩阵为:
# [[ 0.61237244 -0.35355339  0.70710678]
#  [ 0.78033009  0.12682648 -0.61237244]
#  [ 0.12682648  0.9267767   0.35355339]]
```

当你运行这段代码,它会打印出综合所有三次旋转的旋转矩阵R。

## 1.2 Numpy 与线性代数

在本节中,我们将探讨Python中最强大的科学计算库之一:NumPy。在深入学习之前,我们需要弄清楚线性代数在实际应用中的重要性。线性代数不仅是理解数据结构、解决数学问题的基础,也是计算机图形学、机器学习等高级领域不可或缺的工具。NumPy库在这里扮演了至关重要的角色,因为它为我们提供了一个高效、便捷的平台来处理数值计算和线性代数运算。接下来的内容,将通过实际的例子展示如何使用NumPy来执行一些基础但强大的线性代数操作。

### 1.2.1  Numpy向量与矩阵的操作

在科学计算的世界里,NumPy的数组对象是我们解决问题的得力助手。它能让我们轻松地执行向量化运算,这意味着可以一次性处理数据集而不需要使用循环。这种处理方式不仅代码更加简洁,而且运行速度也远快于传统的Python循环。在Numpy中,向量和矩阵都可以用二维数组表示,让我们来看看基本操作。

**创建向量和矩阵**

```python
import numpy as np
# 创建向量
vector = np.array([1, 2, 3])
# 创建矩阵
matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])
```

**向量和矩阵的基本属性**

```python
# 向量的维度
print(vector.shape) # (3, )
# 矩阵的维度
print(matrix.shape) # (3, 3)
# 矩阵的行数和列数
print(matrix.shape[0])  # 行数, 3
print(matrix.shape[1])  # 列数, 3
```

**索引和切片**

```python
# 索引
print(vector[0])  # 输出第一个元素, 1
print(matrix[1, 1])  # 输出第二行第二列的元素, 5

# 切片
print(vector[0:2])  # 输出前两个元素, [1, 2]
print(matrix[0:2, 0:2])  # 输出左上角的2x2子矩阵, [[1, 2], [4, 5]]
```

**向量和矩阵的运算**

```python
# 向量加法
vector1 = np.array([1, 2, 3])
vector2 = np.array([4, 5, 6])
print(np.add(vector1, vector2))
# [5, 7, 9]

# 矩阵乘法
matrix1 = np.array([[1, 2], [3, 4]])
matrix2 = np.array([[5, 6], [7, 8]])
print(np.dot(matrix1, matrix2))  # 或使用 matrix1 @ matrix2
# [[19, 22],
#  [43, 50]]
```

### 1.2.2  利用Numpy进行线性代数基本运算

在NumPy中,我们可以利用数组的**广播机制**来进行各种线性代数运算。例如,你可以轻松地将一个标量与一个向量或矩阵相乘,而不需要编写任何循环。NumPy也提供了计算矩阵的转置、行列式、逆矩阵等常见操作的函数。

```python
import numpy as np

# 数量乘法示例
scalar = 5
scaled_vector = scalar * vector
print("Scaled vector:", scaled_vector)
# Scaled vector: [ 5 10 15]

# 矩阵的转置示例
transposed_matrix = matrix.T
print("Transposed matrix:\n", transposed_matrix)
# Transposed matrix:
# [[1, 4, 7]
#  [2, 5, 8]
#  [3, 6, 9]]

# 计算行列式示例
matrix_determinant = np.linalg.det(matrix)
print("Matrix determinant:", matrix_determinant)
# Matrix determinant: 0.0

# 求解线性方程组示例
A = np.array([[3, 1], [1, 2]])
b = np.array([9, 8])
solution = np.linalg.solve(A, b)
print("Solution of the linear system:", solution)
# Solution of the linear system: [2. 3.]

```

### 1.2.3  `numpy.linalg` 的使用

最后,我们不能不提NumPy中的 `linalg` 子模块,它包含了一系列关于线性代数的函数。无论是求解方程组,还是计算特征值、特征向量,乃至执行奇异值分解,`linalg` 模块都能够提供帮助。通过这些工具,我们可以探索矩阵的深层属性,并应用于各种数学和工程问题。下面是一些 `numpy.linalg` 模块的使用示例:

**计算逆矩阵**

```python
import numpy as np
# If the matrix is singular, use the pseudo-inverse
pseudo_inverse_matrix = np.linalg.pinv(matrix)
print("Pseudo-inverse of the matrix:")
print(pseudo_inverse_matrix)
# Pseudo-inverse of the matrix:
# [[-6.38888889e-01 -1.66666667e-01  3.05555556e-01]
#  [-5.55555556e-02  4.20756436e-17  5.55555556e-02]
#  [ 5.27777778e-01  1.66666667e-01 -1.94444444e-01]]

```

**特征值和特征向量**

```python
eigenvalues, eigenvectors = np.linalg.eig(matrix)
print(eigenvalues)
# [ 1.61168440e+01 -1.11684397e+00 -1.30367773e-15]

print(eigenvectors)
# [[-0.23197069 -0.78583024  0.40824829]
#  [-0.52532209 -0.08675134 -0.81649658]
#  [-0.8186735   0.61232756  0.40824829]]

```

**奇异值分解**

```python
U, S, V = np.linalg.svd(matrix)
print(U)
# [[-0.21483724  0.88723069  0.40824829]
#  [-0.52058739  0.24964395 -0.81649658]
#  [-0.82633754 -0.38794278  0.40824829]]

print(S)
# [1.68481034e+01 1.06836951e+00 4.41842475e-16]

print(V)
# [[-0.47967118 -0.57236779 -0.66506441]
#  [-0.77669099 -0.07568647  0.62531805]
#  [-0.40824829  0.81649658 -0.40824829]]

```

> 在np的计算中存在浮点数误差,特征向量形成矩阵转置乘以特征向量形成矩阵并不完全等于单位矩阵,故按步骤计算和直接调用函数存在一定误差。感谢学习者提问,这里回答一下。

**范数计算**

```python
norm = np.linalg.norm(vector)
print(norm)
# 3.7416573867739413

```

## 1.3  平面几何模型的构建

### 1.3.1 问题背景介绍

我们以2023 年高教社杯全国大学生数学建模竞赛B题为例:

单波束测深是利用声波在水中的传播特性来测量水体深度的技术。声波在均匀介质中作匀速直线传播,在不同界面上产生反射,利用这一原理,从测量船换能器垂直向海底发射声波信号,并记录从声波发射到信号接收的传播时间,通过声波在海水中的传播速度和传播时间计算出海水的深度,其工作原理如图 4 所示。由于单波束测深过程中采取单点连续的测量方法,因此,其测深数据分布的特点是,沿航迹的数据十分密集,而在测线间没有数据。 

![500](./attachments/Pasted%20image%2020240417112927.png)

<center>图4 单波束(左)和多波束(右)探测的工作原理</center>

多波束测深系统是在单波束测深的基础上发展起来的,该系统在与航迹垂直的平面内一次能发射出数十个乃至上百个波束,再由接收换能器接收由海底返回的声波,其工作原理如图 4所示。多波束测深系统克服了单波束测深的缺点,在海底平坦的海域内,能够测量出以测量船测线为轴线且具有一定宽度的全覆盖水深条带(图 5)。 

![500](./attachments/Pasted%20image%2020240417113056.png)

<center>图5 条带的重叠区域及其与覆盖宽度、测线间距的关系</center>

多波束测深条带的覆盖宽度$W$随换能器开角$\theta$和水深$D$的变化而变化。若测线相互平行且海底地形平坦, 则相邻条带之间的重叠率定义为$\displaystyle\eta = 1 - \frac{d}{W}$, 其中$d$为相邻两条测线的间距,$W$为条带的覆盖宽度 (图 5)。若$\eta < 0$,则表示漏测。为保证测量的便利性和数据的完整性, 相邻条带之间应有$10\% \sim 20\%$的重叠率。

但真实海底地形起伏变化大, 若采用海区平均水深设计测线间隔, 虽然条带之间的平均重叠率可以满足要求, 但在水深较浅处会出现漏测的情况 (图 6), 影响测量质量; 若采用海区最浅处水深设计测线间隔, 虽然最浅处的重叠率可以满足要求, 但在水深较深处会出现重叠过多的情况 (图 6), 数据冗余量大, 影响测量效率。

与测线方向垂直的平面和海底坡面的交线构成一条与水平面夹角为$\alpha$的斜线(图 6),称$\alpha$为坡度。请建立多波束测深的覆盖宽度及相邻条带之间重叠率的数学模型。

![500](./attachments/Pasted%20image%2020240417113542.png)

<center>图6 问题一示意图</center>

若多波束换能器的开角为 $120^{\circ}$,坡度为 $1.5^{\circ}$,海域中心点处的海水深度为 $70 ~\mathrm{m}$,利用上述模型计算表 1 中所列位置的指标值,将结果以表 1 的格式放在正文中,同时保存到 `result1.xlsx`文件中。

<center>表1 问题一的计算结果</center>

![700](./attachments/Pasted%20image%2020240417113713.png)

### 1.3.2 问题一的分析

对于问题一,这是一个几何模型问题。已知开角$120^{\circ}$,坡度为 $1.5^{\circ}$,$D=70$,求覆盖宽度,这个问题本质上就是一个已知三角形角度和角平分线长度求对边的几何问题。可以反复利用三角形中的**正弦定理**求解左右两条波束落点的距离也就是带宽,随后通过几何关系求解重复部分。

### 1.3.3 问题一的模型建立

将问题一中的几何模型进行抽象构造如图2所示。图2中船的行进方向垂直纸面向外,船的位置为$D$点。$DF$为重力方向竖直向下,而$AB$是水平面。$D$点发射出两条波束$DA$和$DE$交海底平面于$A$、$E$两点。由于$DF$竖直向下,结合问题背景可知$DF$为$\angle ADE$的角平分线。过$A$作$AC$,这是海底平面,$\angle CAB=1.5^{\circ}$。整体如图7所示:

![500](./attachments/Pasted%20image%2020240417113913.png)

<center>图7 单一探测船的示意图</center>

在图7中显然,由于$DF$竖直向下而$AB$为水平面,故$\angle DAB+\angle ADF=90^{\circ}$,$\angle CAB=1.5^{\circ}$,$\angle ADF$为开角的一半也就是$60^{\circ}$,故有:

$$
\angle DAF = 90^{\circ} - \angle CAB - \angle ADF. \tag{1.3.1}
$$
这可以推算出$\angle DAF$为$28.5^\circ$,由三角形的内角和可知$\angle DEA$为$31.5^{\circ}$。已知$DF$长度为$70\text{ m}$,且知道对角角度,可以在$\Delta DAF$和$\Delta DFE$中分别使用正弦定理:
$$
\frac{DF}{\sin 28.5^{\circ}} = \frac{AF}{\sin 60^{\circ}}, \quad \frac{DF}{\sin 31.5^{\circ}} = \frac{EF}{\sin 60^{\circ}}. \tag{1.3.2}
$$
所以待求带宽的长度也就是$AF+EF$。经过代换,可以解得在已知海底深度$D$的条件下,覆盖宽度的计算方法为:
$$
W = D \left( \frac{\sin 60^{\circ}}{\sin 28.5^{\circ}} + \frac{\sin 60^{\circ}}{\sin 31.5^{\circ}} \right). \tag{1.3.3}
$$
为了求解相邻测线的覆盖率,考虑两条相邻测线的情况,如图8所示:

![500](./attachments/Pasted%20image%2020240417114003.png)

<center>图8 多艘探测船的示意图</center>

从图8中可以看到,$G$点在$D$点左侧$d$米处。且由于船在水面上行走,$GD // AB$。那么同样地,由于$GJ$同样竖直向下,所以$GH//DF$, $GI//DF$,$\Delta GHI$和$\Delta DFE$是相似的。对于$G$点和$D$点而言,波束重叠部分的重叠覆盖宽度$\lambda=EH$。若过$J$作$AB$平行线,并延长$DK$相交于$P$,则二者形成的$\Delta JKP$中$\angle KJP=1.5^{\circ}$且$JP=d$。那么$EH$的长度显然有下面的关系:
$$
EH = JH + EK - JK. \tag{1.3.4}
$$
根据前面通过正弦定理得到的推论,有:
$$
\begin{align}
JH &= \frac{GJ}{\sin 31.5^{\circ}} \cdot \sin 60^{\circ},\tag{1.3.5}\\
EK &= \frac{DK}{\sin 28.5^{\circ}} \cdot \sin 60^{\circ}, \tag{1.3.6}\\
JK &= \frac{d}{\cos 1.5^{\circ}}, \tag{1.3.7}\\
GJ &= DK + d\tan 1.5^{\circ}. \tag{1.3.8}
\end{align}
$$

通过对上述表达式的求解,可以得到重叠部分的宽度为:

$$
\begin{aligned}
\lambda &= (DK + d\tan 1.5^{\circ})\cdot \frac{\sin 60^{\circ}}{\sin 31.5^{\circ}} + DK \cdot \frac{\sin 60^{\circ}}{\sin 28.5^{\circ}} - \frac{d}{\cos 1.5^{\circ}}\\
&= DK \cdot \left( \frac{\sin 60^{\circ}}{\sin 31.5^{\circ}} + \frac{\sin 60^{\circ}}{\sin 28.5^{\circ}} \right) + d \cdot \left( \frac{\sin 60^{\circ} \tan 1.5^{\circ}}{\sin 31.5^{\circ}} - \frac{1}{\cos 1.5^{\circ}} \right) 
\end{aligned}\tag{1.3.9}
$$

利用公式可以解得若测线距离中心位置左侧$d\text{ m}$或右侧$d\text{ m}$对应的覆盖宽度。而对应测线的海底深度为$GJ$,同样给出了对应海底深度的测算方法。而若知道对应海底深度,由于$\Delta GHI$与$\Delta DFE$相似,可以利用相似求对应位置的覆盖宽度:

$$
\frac{W'}{D + d \tan\alpha} = \frac{W}{D}. \tag{1.3.10}
$$

经过一系列变换,可以达成对问题一的求解。

### 1.3.3 问题一的模型求解与讨论

通过建模与编程求解,可以得到计算结果如表2所示:

<center>表2 问题一的解答</center>

| $d/\text{m}$   | $-800$   | $-600$   | $-400$    | $-200$   | $0$    | $200$   | $400$     | $600$     | $800$     |
| -------------- | -------- | -------- | --------- | -------- | ------ | ------- | --------- | --------- | --------- |
| $D/\mathrm{m}$ | $90.94$  | $85.711$ | $80.474$  | $75.237$ | $70$   | $64.76$ | $59.526$  | $54.289$  | $49.051$  |
| $W/\mathrm{m}$ | $315.81$ | $297.62$ | $279.441$ | $261.25$ | $243$  | $224.8$ | $206.698$ | $188.513$ | $170.328$ |
| $\eta/\%$<br>  | $—$      | $0.3569$ | $0.3151$  | $0.267$  | $0.21$ | $0.148$ | $0.0740$  | $0.0153$  | $0.12365$ |

在表2中,距离是自变量,表示观测点到某个参考点的距离,它在整个数据集中从-800m到800m变化。随着距离的增加,海水深度逐渐减小。这可能与沿着某一方向移动远离海岸线导致海水深度减小有关。随着距离的增加,覆盖宽度逐渐减小。这可能是由于覆盖物在距离远离某一基准点时逐渐减小,也与相似形有关。覆盖率与距离和海水深度相关,并且在大约向右偏移600m左右时出现最小值。经探究,二者呈现典型的凸函数关系,当D=563m附近时覆盖率取得最小。

**问题一的Python实现**

该脚本计算从海底反射的超声波束的覆盖宽度,考虑各种线路测量的海底坡度和深度。

```python
import numpy as np
from scipy.optimize import fsolve

# 常量定义
theta = 2 * np.pi / 3  # 全开角
alpha = 1.5 / 180 * np.pi  # 海底坡度
htheta = theta / 2  # 半开角
h = 70  # 中心点的海水深度
d = 200  # 测线距离
k = np.tan(np.pi / 2 - htheta)  # 超声波直线的斜率
k0 = np.tan(alpha)  # 海底斜率

# 初始化
Aleft = []  # 左端点坐标
Aright = []  # 右端点坐标
Acenter = []  # 中心点坐标
W = []  # 覆盖宽度

# 求解交点
for n in range(-4, 5):
    leftsolve = lambda t: k * (t - n * d) - k0 * t + h
    rightsolve = lambda t: -k * (t - n * d) - k0 * t + h
    tleft = fsolve(leftsolve, 0)
    tright = fsolve(rightsolve, 0)
    Aleft.append([tleft[0], k0 * tleft[0] - h])
    Aright.append([tright[0], k0 * tright[0] - h])
    Acenter.append([200 * n, k0 * 200 * n - h])
Aleft = np.array(Aleft)
Aright = np.array(Aright)
Acenter = np.array(Acenter)
D = Acenter[:, 1]  # 海水深度
W = np.sqrt((Aleft[:, 0] - Aright[:, 0]) ** 2 + (Aleft[:, 1] - Aright[:, 1]) ** 2)  # 覆盖宽度

# 计算重合部分
cover = np.zeros(8)
for i in range(8):
    cover[i] = np.sqrt((Aright[i, 0] - Aleft[i + 1, 0]) ** 2 + (Aright[i, 1] - Aleft[i + 1, 1]) ** 2)
    # 判断是否有重叠,如果没有,则重叠率为负值
    if Aright[i, 0] - Aleft[i + 1, 0] < 0: 
        cover[i] = -cover[i]
eta = cover / W[1:]

# 打印结果
print("海水深度 D:", D)
# 海水深度 D: [-90.94873726 -85.71155294 -80.47436863 -75.23718431 -70.
# -64.76281569 -59.52563137 -54.28844706 -49.05126274]

print("覆盖宽度 W:", W)
# 覆盖宽度 W: [315.81332829 297.62756059 279.44179288 261.25602517 243.07025746
# 224.88448975 206.69872205 188.51295434 170.32718663]

print("重合部分比例 eta:", eta)
# 重合部分比例 eta: [0.35695443 0.31510572 0.26743092 0.21262236 0.14894938 0.07407224
# 0.01525164 0.12364966]

```

## 1.4  立体几何模型的构建

背景同上,问题如下:问题 2 考虑一个矩形待测海域(图 9),测线方向与海底坡面的法向在水平面上投影的夹角为 𝛽,请建立多波束测深覆盖宽度的数学模型。

![500](./attachments/Pasted%20image%2020240417121230.png)

<center>图9 问题二的示意图</center>

若多波束换能器的开角为$120^{\circ}$,坡度为$1.5^{\circ}$,海域中心点处的海水深度为 $120 \text{ m}$,利用上述模型计算表 2 中所列位置多波束测深的覆盖宽度,将结果以表 2 的格式放在正文中,同时保存到 `result2.xlsx` 文件中。

<center>表2 问题二的计算结果</center>

![700](./attachments/Pasted%20image%2020240417121340.png)

### 1.4.1问题二的分析

问题二是问题一的延续。第二个问题将场景从二维向三维扩展,在三维空间内利用正弦定理求解投影长度。可以通过做辅助平面的方式构造四棱锥,利用三面角定理求解。通过坡度和倾斜角可以解得测线与平面夹角关系,再通过波束的最大开角$120^{\circ}$可以得到直线与平面夹角,从而变换模型得到四棱锥的几何构型。通过解方程的方法求最优解。

### 1.4.2问题二模型的建立

对于第二问,问题变成了一个立体几何问题,如图10所示。

![500](./attachments/Pasted%20image%2020240417121419.png)

<center>图10 问题二的三维示意图</center>

图10中$KJ$为测线。倾斜角$\beta$为测线与坡面法向投影的夹角,平面$ABC$是垂直于直线$KJ$的(线面垂直),$CD$为垂面与海底形成的交线,也就是所需要求解的长度。平面$BEC$是平行于水平面,也平行于直线$JK$的。同时$CE$垂直于平面$ABE$,且$CE//FH$。那么很显然,直线$KJ$与直线$BC$构成异面垂直,角度为$\beta$。并且,$AE$垂直于$CE$,$AB$垂直于$BE$,$AB$垂直于$BC$,构成一个典型的四面体几何模型。

在图所示的情况中,$\beta$是一个钝角。如果过$B$作$KJ$在平面$BEC$中的投影$BK’$,$\angle EBK’$的角度为$\beta$,补角为$180-\beta$。那么很显然,由于$BK’$垂直于$BC$,$BE$垂直于$CE$,那么$\angle BCE$与$\angle CBE$互余,$\angle CBE$和$180-\beta$互余,也就是说$\angle BCE$的角度也就是$180-\beta$。
注意到$\displaystyle \cos\angle ACB = \frac{CB}{AC}$, $\displaystyle \cos\angle BCE=\frac{CE}{BC}$, $\displaystyle cos\angle ACE=\frac{CE}{AC}$,这实际上就构成了三面角公式:

$$
\cos \angle  ACB \cdot \cos\angle BCE = \cos \angle  ACE. \tag{1.4.1}
$$

因为$\angle ACB=30^{\circ}$,$\angle BCE=45^{\circ}$,所以$\angle ACE$是可以求解的,$\angle EAC$也是可以求解的(二者互余)。

然后另外再使用一次三面角公式,$\displaystyle \cos\angle EAC=\frac{EA}{AC}$, $\displaystyle \cos\angle EAB=\frac{AB}{EA}$, $\displaystyle \cos\angle BAC=\frac{AB}{AC}$,所以可以得到:

$$
\cos \angle  EAC \cdot \cos\angle EAB = \cos \angle  BAC. \tag{1.4.2}
$$

故$\angle EAB$也是可以求出来的。而$\angle DEB$为坡面角$1.5^{\circ}$,同样也就能求出$\angle ABD$。若在$\Delta ABD$中使用正弦定理,因为已知$AD$和对角,则$AE$的长满足方程:

$$
\frac{AE}{\sin\angle ADE} = \frac{d}{\sin\angle AED}. \tag{1.4.3}
$$

同时,由于前面求解过$\angle EAC$,即可求$AC$的长。因为$EC$垂直于平面$ABE$,所以$\Delta EAC$构成一个直角三角形。$AC$长度满足:

$$
EA = EC \cos \angle CAE. \tag{1.4.4}
$$

另外,在$\Delta CAD$中知道了$AC$、$AD$的长和夹角($60^{\circ}$),就可以使用余弦定理求解$CD$的值:

$$
CD^{2} = AC^{2} + AD^{2} - 2 \cdot AC \cdot AD \cdot \cos60 ^{\circ}.\tag{1.4.5}
$$

同理,对于另一条波束射线,分析方法同上。只需要将$\angle BCE$的角度从$180-\beta$变为$\beta$即可求解。所以带宽$DC+DC’$可以求解。

### 1.4.2问题二模型的求解与讨论

通过建模与编程求解,可以得到计算结果如表3所示:

<center>表3 问题二的解答</center>

![700](./attachments/Pasted%20image%2020240417122656.png)

从表3中可以看出如下规律:

1. **测量船距海域中心点处的距离与覆盖宽度之间的关系**: 从数据可以看出,随着测量船距海域中心点处的距离增加,覆盖宽度似乎也在增加。这可能表示在不同距离下,测量覆盖区域的宽度不同,这是一个直观的趋势。
2. **测量船距海域中心点处的距离与测线方向夹角之间的关系**: 表格中的数据似乎显示了不同距离下的测线方向夹角的变化。这可能表示在不同距离下,测量船需要采取不同的测线方向。这也是一个趋势。
3. **部分数据存在对称性**: 通过观察,可以看到表格的一部分数据在特定角度(如90度)下具有对称性,即某些角度下的数据在正负方向上相等。这可能反映了某种对称性或规律。

**问题二的Python实现**

```python
import numpy as np
from scipy.optimize import fsolve

# 常量定义
theta = 2 * np.pi / 3  # 全开角
alpha = -1.5 / 180 * np.pi  # 坡度
htheta = theta / 2  # 半开角
h = 120  # 正中心的海水深度
unit = 1852  # 一海里等于1852米
k0 = np.tan(alpha)  # 海底坡面直线的斜率

# 初始化覆盖宽度矩阵
W = np.zeros((8, 8))

# 计算不同角度和位置的声呐波束的覆盖宽度
for i in range(1, 9):
    for j in range(1, 9):
        beta = (i - 1) * np.pi / 4
        d = (j - 1) * 0.3 * unit  # 换算为米
        v = np.array([np.cos(beta), np.sin(beta), 0])  # 直线的法向量
        origin = v * d  # 发射位置
        
        # 波束的方向向量
        v1 = np.array([-np.sin(beta) * np.sin(htheta), np.cos(beta) * np.sin(htheta), -np.cos(htheta)])
        v2 = np.array([np.sin(beta) * np.sin(htheta), -np.cos(beta) * np.sin(htheta), -np.cos(htheta)])
        
        # 用于找到波束与海底交点的函数
        leftsolve = lambda t: (v1[0] * t + origin[0]) * k0 - h - (v1[2] * t + origin[2])
        rightsolve = lambda t: (v2[0] * t + origin[0]) * k0 - h - (v2[2] * t + origin[2])
        
        # 解方程找到交点
        tleft = fsolve(leftsolve, 0)
        tright = fsolve(rightsolve, 0)
        
        # 计算左右交点的坐标
        pleft = v1 * tleft + origin
        pright = v2 * tright + origin
        
        # 计算并存储覆盖宽度
        W[i-1, j-1] = np.linalg.norm(pleft - pright)
        
# 打印覆盖宽度矩阵
print("覆盖宽度矩阵 W:\n", W)
# 覆盖宽度矩阵 W: 
# [[415.69219382 466.09105496 516.4899161  566.88877725 617.28763839
#  667.68649953 718.08536067 768.48422182]
# [416.19152335 451.87170745 487.55189155 523.23207565 558.91225975
#  594.59244385 630.27262795 665.95281205]
# [416.69186994 416.69186994 416.69186994 416.69186994 416.69186994
#  416.69186994 416.69186994 416.69186994]
# [416.19152335 380.51133925 344.83115515 309.15097105 273.47078694
#  237.79060284 202.11041874 166.43023464]
# [415.69219382 365.29333267 314.89447153 264.49561039 214.09674925
#  163.6978881  113.29902696  62.90016582]
# [416.19152335 380.51133925 344.83115515 309.15097105 273.47078694
#  237.79060284 202.11041874 166.43023464]
# [416.69186994 416.69186994 416.69186994 416.69186994 416.69186994
#  416.69186994 416.69186994 416.69186994]
# [416.19152335 451.87170745 487.55189155 523.23207565 558.91225975
#  594.59244385 630.27262795 665.95281205]]

```


### 1.4.3 问题二的第二种解法

在问题中,以海面中央为原点,南北方向为$\mathrm{x}$轴,东西方向为$\mathrm{y}$轴,竖直向上为$\mathrm{z}$轴正方向构建三维直角坐标系如图11所示:

![500](./attachments/Pasted%20image%2020240417123011.png)

<center>图11 问题二解法2的三维示意图</center>

对于测线,若已知倾斜角,则测线的方向向量为:

$$
\boldsymbol{v} = [\cos\beta, \sin\beta, 0]^{\top}. \tag{1.4.6}
$$

而沿着测线行进的船只发射出的两道波束,因最大开角已知为$120^{\circ}$,可以求得左右两道波束的方向向量分别为:

$$
\begin{aligned}
\boldsymbol{v}_{1} &= \left[ -\cos \frac{\theta}{2} \sin\beta, \cos \frac{\theta}{2}\cos\beta, -\sin \frac{\alpha}{2} \right]^{\top}, \\
\boldsymbol{v}_{2} &= \left[ \cos \frac{\theta}{2} \sin\beta, -\cos \frac{\theta}{2}\cos\beta, -\sin \frac{\alpha}{2} \right]^{\top}.
\end{aligned}\tag{1.4.7}
$$

海底坡面在三维坐标系中的解析方程可以写作:

$$
z = -x\tan \alpha - h, \quad x \in [-1852, 1852],~y \in [-3704, 3704]. \tag{1.4.8}
$$

三维空间中的直线方程,是以方向向量为核心构造的。若以参数方程的形式写出测线的解析式,在测线通过点$P(x_{0},y_{0},0)$的情况下,有下面一组等式成立:
$$
\left\{  
\begin{align}
x &= t\cos\beta + x_{0},\\
y &= t\sin\beta + y_{0},\\
z &= z_{0}.
\end{align}
\right.\tag{1.4.9}
$$


同样的,在通过$P$点的条件下,从$P$点发射出两条测线的参数方程分别为
左测线:
$$
\left\{  
\begin{align}
x &= -t\cos \frac{\theta}{2}\sin\beta + x_{0},\\
y &= t\cos \frac{\theta}{2}\cos\beta + y_{0},\\
z &= -t\sin \frac{\theta}{2} + z_{0};
\end{align}
\right.\tag{1.4.10}
$$

右测线:
$$
\left\{  
\begin{align}
x &= t\cos \frac{\theta}{2}\sin\beta + x_{0},\\
y &= -t\cos \frac{\theta}{2}\cos\beta + y_{0},\\
z &= -t\sin \frac{\theta}{2} + z_{0};
\end{align}
\right.\tag{1.4.10}
$$

将左测线和右测线与平面方程联立,即可求得船只在测线上$P$点处发射波束的落点坐标。而对于每一条测线,它与通过原点的测线都平行,且相邻两条测线的距离为$d$。根据距离条件,可以计算出相邻测线必定通过的一个点。例如,对于通过原点的测线而言,其右侧测线上必定存在一个点$P$,使得$OP=d$的同时$OP$垂直于测线。可以解得这个$P$点坐标为:

$$
P = [-d\sin\beta, d\cos\beta, 0]^{\top}. \tag{1.4.12}
$$

联立上述条件,可以求解出对于每一条刻线$l_{i}$,当船只P在测线上移动时形成的落点坐标集合$\{w_{i,1}\}$和$\{w_{i,2}\}$,进而可以求解所有落点形成的长度$L$。

## 1.5  使用Python解方程与方程组

在科学计算和工程应用中,经常会遇到需要求解方程或方程组的问题。Python提供了强大的数学库,如Numpy和Sympy,可以帮助我们轻松地解决这些问题。

### 1.5.1  利用Numpy求线性方程(组)的数值解

Numpy是Python中一个用于数值计算的库,它提供了很多用于矩阵运算的功能。我们可以使用Numpy中的linalg.solve函数来解线性方程组。例如,我们有以下方程组:

$$
\begin{align}
10x - y - 2z &= 72,\\
-x + 10y - 2z &= 83,\\
-x - y + 5z &= 42.
\end{align}\tag{1.5.1}
$$

我们可以使用以下代码来求解这个方程组:

```python
import numpy as np
a = np.array([[10, -1, -2], [-1, 10, -2], [-1, -1, 5]])
b = np.array([[72], [83], [42]])
c = np.linalg.solve(a, b)
print(c)
#[[11.]
# [12.]
# [13.]]

此外,我们还可以使用矩阵的逆来求解方程组,即:
x = np.linalg.inv(a).dot(b)
print(x)
# [[11.]
# [12.]
# [13.]]

```

### 1.5.2  利用Sympy求方程(组)的解析解

Sympy是Python中一个用于符号计算的库,它可以提供方程的解析解,而不仅仅是数值解。什么是解析解和数值解呢?简单来说,解析解是指用数学符号(如$x$、$y$、$\pi$等)表示的解,而数值解是指用具体的数字表示的解。

在Sympy中,我们首先需要创建符号变量,然后使用 `solve` 函数来求解方程或方程组。例如:

```python
from sympy import symbols, solve, nonlinsolve

x, y = symbols('x y')
print(solve(x * 2 - 2, x))  # 解方程2x - 2 = 0
# [1]

print(solve([x + y - 35, x * 2 + y * 4 - 94], x, y))  # 解方程组x + y = 35, 2x + 4y = 94
# {x:23, y:12}

print(solve(x**2 + x - 20, x))  # 解方程x^2 + x - 20 = 0
# [-5, 4]

a, b, c, d = symbols('a b c d', real=True)
print(nonlinsolve([a**2 + a + b, a - b], [a, b]))  # 解非线性方程组a^2 + a + b = 0, a - b = 0
# {(-2, -2), (0, 0)}

```

以下是一个具体案例:

![500](./attachments/Pasted%20image%2020240417124525.png)

<center>图12 平面型Stewart平台示意图</center>

图12给出的是平面型Stewart平台示意图,它模拟一个操作装置,其中包括一个三角形($ABC$)平台,平台位于一个由3个支柱($P_{1}$, $P_{2}$和$P_{3}$)控制的固定平面中。图中的三角形($ABC$)表示平面型Stewart平台,它的尺寸由3个长度$L_{1}$,$L_{2}$,$L_{3}$确定。平台的位置由$3$个支柱的可变长度的$3$个参数$P_{1}$,$P_{2}$,$P_{3}$所控制。需要解决的问题是,在给定一组参数$P_{1}$,$P_{2}$,$P_{3}$的值后,计算出$A$点的坐标$(x, y)$和角度$\theta$的值($\theta$是$L_{3}$与$x$轴的夹角)。请你完成:

1. 数学建模:参数$L_{1}, L_{2}, L_{3}, x_{1}, x_{2}, y_{2}$是固定常数,在给定一组参数$P_{1}, P_{2}, P_{3}$的值后,判断能否得到Stewart平台的一个位置,即能否得到$A$点坐标$(x, y)$和角度$\theta$的值。如果能,则称它为Stewart平台的一个**位姿**。但位姿并不一定是唯一的,如何让你的模型能够计算出一组固定参数下的全部位姿。
2. 模型检验:假设有如下参数: $x_{1}=5$, $(x_{2}, y_{2})=(0,6)$,$L_{1}=L_{3}=3$, $L_{2}=3$, $p_{1}=p_{2}=5$, $p_{3}=3$,请根据你的模型,计算出Stewart平台的全部位姿,即计算出每个Stewart平台中的$A$点坐标$(x, y)$和角度$\theta$的值。

**定义问题的几何关系**

$A$点的坐标:由于$A$点位于支柱$P_{1}$的顶端,我们可以使用距离公式确定$A$点的坐标。这是一个圆的方程,表示所有与原点距离为$P_{1}$的点集合。方程如下:

$$
x^{2} + y^{2} = P_{1}^{2}. \tag{1.5.2}
$$
角度$\beta$的计算:角度是$\Delta ABC$的内角,根据余弦定理,我们有:

$$
\cos \beta = \frac{L_{2}^{2} + L_{3}^{2} - L_{1}^{2}}{2L_{2}L_{3}}. \tag{1.5.3}
$$

$B$点的坐标:$B$点相对于$A$点的位置可以用角度$\theta$表示,根据$A$点的坐标和角度$\theta$,我们可以写出$B$点的坐标

$$
B(x_{B}, y_{B}) = \Big(x + L^{3}\cos \theta, y + L^{3} \sin \theta\Big). \tag{1.5.4}
$$

$C$点的坐标:同样地,$C$点相对于$A$点的位置可以用角度$\beta + \theta$来确定,于是$C$点的坐标为:

$$
C(x_{C}, y_{C}) = \Big(x + L^{2}\cos(\beta + \theta), y + L^{2}\sin(\beta + \theta)\Big)
$$

**建立方程组**

$B$点与$P_{2}$的距离关系:由于$B$点还位于支柱的顶端,所以我们有第二个方程:

$$
(x_{B} - x_{1})^{2} + y_{B}^{2} = P_{2}^{2}.\tag{1.5.6}
$$

$C$点与$P_{3}$的距离关系:同理,$C$点位于支柱的顶端,所以我们有第三个方程:

$$
(x_{C} - x_{2})^{2} + (y_{C} - y_{2})^{2} = P_{3}^{2}. \tag{1.5.7}$$

**解方程组以确定$A$点坐标和角度$\theta$**

* 方程求解:我们现在有三个方程和两个未知数 $x$ 和 $y$。为了求解 $x$ 和 $y$,我们可以用任何一个方程消去  后解一个二元一次方程组。
* 多解的情况:这组方程可能有多个解,对应于不同的物理位置和Stewart平台的不同位姿。
* 数值求解:在实践中,通常需要通过数值方法来解这类方程组,例如使用牛顿法或者数值优化算法。
* 模型检验:给定的参数  $x_{1}=5$, $(x_{2}, y_{2})=(0,6)$,$L_{1}=L_{3}=3$, $L_{2}=3$, $p_{1}=p_{2}=5$, $p_{3}=3$ 可以代入上述方程组中进行求解。这将验证模型的正确性,并给出所有可能的Stewart平台位姿。

通过上述步骤,我们不仅可以找到$A$点的坐标和角度$\theta$ ,而且还可以确定Stewart平台的多个可能位姿,这些位姿对应于不同的解集。这是一个典型的运动学问题,其解决方案涉及几何、三角函数以及数值计算。

### 1.5.3  利用Scipy求方程(组)的数值解

在进行数值求解时,`fsolve` 是 Scipy 库中用于解决非线性方程组的一个非常有用的函数。它通常用于查找方程组的根,其中方程组可以是非线性的,并且不保证有解析解。接下来我们将会对1.5.2小节例题进行求解。

首先,尝试使用 Sympy 库解方程组时,我们发现问题无法解决。这是因为所面临的方程组可能没有简洁的解析解,或者是解析解超出了Sympy库的计算范围。以下是尝试解方程的代码和得到的结果:

```python
from sympy import symbols, cos, sin, pi, nonlinsolve
import numpy as np
x, y, theta = symbols('x y theta', real=True)
L1, L2, L3 = 3, 3, 3
p1, p2, p3 = 5, 5, 3
x1, x2, y2 = 5, 0, 6
# 计算内角β
b = np.arccos((L2**2 + L3**2 - L1**2) / (2 * L2 * L3))
print(b)
# 尝试解方程组
solution = nonlinsolve([
    (x + L3 * cos(theta) - x1)**2 + (y + L3 * sin(theta))**2 - p1**2,
    x**2 + y**2 - p2**2,
    (x + L2 * cos(pi/3 + theta))**2 + (y + L2 * sin(pi/3 + theta) - y2)**2 - p3**2
], [x, y, theta])
print(solution)
# 1.0471975511965979

```

得到的输出表明,我们没有找到方程组的解析解。在这种情况下,我们转向数值解法,特别是Scipy库中的`fsolve`函数,来找到方程组的数值解。以下是使用`fsolve`的案例:

```python
from scipy.optimize import fsolve
from math import sin, cos, pi
# 定义方程组
def equations(vars):
    x, y, theta = vars
    L1, L2, L3 = 3, 3, 3
    p1, p2, p3 = 5, 5, 3
    x1, x2, y2 = 5, 0, 6
    # 根据问题描述定义的方程
    eq1 = (x + L3*cos(theta) - x1)**2 + (y + L3*sin(theta))**2 - p2**2
    eq2 = x**2 + y**2 - p1**2
    eq3 = (x + L2*cos(pi/3 + theta))**2 + (y + L2*sin(pi/3 + theta) - y2)**2 - p3**2
    return [eq1, eq2, eq3]
# 初始猜测值
initial_guess = [-1.37, 4.80, 0.12]
# 使用fsolve求解方程组
result = fsolve(equations, initial_guess)
print(result)
# [1.15769945 4.86412705 0.02143414]

```

执行以上代码,我们得到了方程组的一组数值解。

上面的代码中,`equations` 函数定义了一个方程组,它接受一个变量列表(在这里是 `x`, `y`, 和 `theta`),然后返回一个方程组列表。然后,我们使用 `fsolve` 来求解这个方程组,并且提供了一个初始猜测值列表 `initial_guess`。`fsolve` 会尝试找到这些方程的根,这意味着它会寻找满足方程组为零的 `x`, `y`, 和 `theta` 的值。

在 Scipy 库中,除了 `fsolve`,还有其他几个函数可以用于解决类似的问题,比如:

* `brentq` 或 `bisection`:这些函数是用于求解单变量方程的根的,适用于在指定区间内具有一个根的情况。
* `root`:这个函数提供了一个更加通用的接口来求解多变量方程组的根,它允许选择不同的算法,比如 `hybr`, `lm`, `broyden1`, 等等。
* `newton`:用于求解单变量方程的根,当你有方程的导数信息时特别有用。

对于复杂的方程组,尤其是当没有解析解时,使用数值方法通常是解决问题的可行方式。**在使用数值方法时,很重要的一点是要有一个合理的初始猜测,因为这些方法很大程度上依赖于起始点,并且可能收敛到局部解,或者在某些情况下可能根本不收敛。**

## 本章小结

本章是数学建模的一个导引章节。在这一章节中,我们一起探索了几何模型在数学建模中的基础应用和实际操作。从向量表示法的初步了解到复杂立体几何模型的构建,再到运用Python编程语言中的 Numpy、Sympy、Scipy 等库求解方程和方程组,我们一步步深入了解了几何模型的精髓。

我们首先回顾了几何建模的基本思想,并探讨了向量表示和坐标变换的重要性。这不仅巩固了我们的几何基础知识,也为后续的学习打下了坚实的基础。接着,通过Numpy的强大功能,我们学习了线性代数中向量与矩阵的操作,以及如何运用Numpy.linalg库来进行基本的线性代数运算。

随后,我们针对平面几何模型和立体几何模型分别进行了深入的分析和建模。在每个问题中,我们不仅建立了数学模型,还通过模型的求解与讨论,实践了数学建模的全过程,这无疑增强了我们解决实际问题的能力。

在使用Python解方程与方程组的部分,我们学习了如何利用不同的Python库来解决具体的数学问题。这些内容不仅仅是理论上的,更多的是实践操作,让我们在解决实际问题时更加得心应手。

作为本章的结束,希望大家不仅理解了几何模型的构建方法,而且能够领会背后的数学原理,并将这些知识应用到现实生活中去。在接下来的章节中,我们将进一步学习更多高级的数学建模技巧和方法。请大家继续保持好奇心和探索精神,相信在数学建模的道路上会有更多的发现和成就。

祝愿各位读者在学习的旅途中收获知识,享受创造的乐趣,不断提升自己的问题解决能力。在这个以数据和模型驱动的时代,让我们共同成长为理解世界的数学建模者。


================================================
FILE: docs/CH10/第10章-图像、文本与信号数据.md
================================================
<center><h1>第10章  多模数据与智能模型</h1></center>

> 内容:@若冰(马世拓)
> 
> 审稿:@陈思州
> 
> 排版&校对:@陈思州

随着我们进入本书的第十章,我们将探讨多模数据与智能模型的前沿技术。在这一章中,我们将深入了解数字图像处理、计算语言学、数字信号处理等领域,以及它们在人工智能中的应用。这些技术不仅是现代科技进步的基石,也是处理复杂数据分析问题的关键。本章主要涉及到的知识点有:

- 数字图像处理与计算机视觉

- 计算语言学与自然语言处理

- 数字信号处理与智能感知

注意:除了常用的Python之外,还有其他多种软件和工具,如Matlab等,都可用于这些任务的高效处理。

## 10.1  数字图像处理与计算机视觉

视觉信息是我们第一种非常规的数据模式,在Python当中可以使用opencv处理数字图像,并提取出视觉特征用于数学建模。

### **10.1.1  数字图像的表示与处理**

计算机可以存储数字图像,可以展示数字图像,也可以对图像进行分析与处理。但一张图是怎样放到计算机中的呢?其实本质上还是依靠线性代数这个工具。计算机把数字图像看作一个很大的矩阵,把图像中每个像素看作矩阵的一个元素,用矩阵元素的值表示图像对应位置的像素点明暗程度以及颜色情况。

我们可以通过opencv来实现图像处理的Python编程。OpenCV是一个开源的计算机视觉库。它包含了大量图像和视频处理的通用算法,比如图像滤波、特征检测、目标跟踪、人脸识别等。对于想要做图像处理或计算机视觉研究、开发的人来说,OpenCV就像一个强大的工具箱,能帮助他们更轻松、高效地实现各种复杂的视觉任务。无论你是做科学研究、商业应用还是个人项目,OpenCV都能为你提供很大的帮助。例如,读取项目中的一张图并展示,可以用如下代码表示:

```python
# These imports let you use opencv

import cv2 #opencv itself

import common #some useful opencv functions

import numpy as np # matrix manipulations

# the following are to do with this interactive notebook code

%matplotlib inline 

from matplotlib import pyplot as plt # this lets you draw inline pictures in the notebooks

import pylab # this allows you to control figure size 

pylab.rcParams['figure.figsize'] = (10.0, 8.0) # this controls figure size in the notebook

input_image=cv2.imread('noidea.jpg')

input_image=cv2.imread('noidea.jpg')
```



就可以看到这张图片的状态:

![img](./src/wps1.jpg) 

图10.1 彩色图片状态展示

 

对于一张黑白的图像,它可以表示为一个M*N的矩阵。而对于一张彩色图像,就不是一个矩阵这么简单了。对于彩色图像而言,主要有RGB和HSV两种表示方法。RGB中彩色图像被分为三个通道,因为在物理学中所有的色彩都可以用红、绿、蓝三种颜色按不同比例组合而成。因此,对于彩色图像中,每个像素点的值也是一个三维数组,每一项表示对应通道的亮度值。例如,对于上述图像而言,我们可以看看它的尺寸:

```python
print(input_image.shape)

结果为(414,625,3),表明图像的大小为414*625,有三个通道。在这张图里面,有b,g,r三个通道。可以将它作分解并重新组合,我们来看看一个通道究竟长什么样子:

# split channels

b,g,r=cv2.split(input_image)

plt.imshow(r, cmap='gray')

merged=cv2.merge([r,g,b])

# merge takes an array of single channel matrices

plt.imshow(merged)
```



![img](./src/wps2.jpg)![img](./src/wps3.jpg) 

图10.2 黑白彩色图片对比图

 

 

可以看到,每个通道的图都可以当做一张单独的黑白图像,但经过rgb重排以后的图终于可以显示为正常的彩色。这一操作也可以用下面的代码实现:

```python
opencv_merged=cv2.cvtColor(input_image, cv2.COLOR_BGR2RGB)

plt.imshow(opencv_merged)
```



HSV表示法是一种描述颜色的方式,其中H代表色调(Hue),S代表饱和度(Saturation),V代表明度(Value)。这种颜色空间与人类对颜色的感知方式更为接近,因此,在图像处理中,HSV空间经常用于颜色分割、颜色识别等任务。

色调是颜色的一种属性,它表示的是纯色的类型,比如红色、蓝色或黄色等。在HSV颜色空间中,色调以角度来表示,从0到360度。比如0(或360)度表示红色,120度表示绿色,240度表示蓝色。饱和度表示颜色的纯度或强度。一个颜色的饱和度越高,它就越鲜艳;饱和度越低,颜色就越接近灰色。在HSV中,饱和度是一个百分比值,从0%(灰色)到100%(完全饱和)。明度表示颜色的亮度或明暗程度。它与颜色的强度或发光量有关。在HSV中,明度也是一个百分比值,从0%(黑色)到100%(白色)。

RGB是一种加色模型,它是通过红、绿、蓝三种颜色的组合来表示颜色的。每种颜色的强度都通过一个0-255的数值来表示。RGB模型在显示设备上使用得很广泛,但它不太直观地表示人类对颜色的感知,尤其是色调。而HSV则是一种更接近人类感知的颜色空间,更适合用于颜色分割等任务。

在OpenCV中,你可以使用cvtColor函数将图像从RGB空间转换到HSV空间。以下是一个简单的示例:

```python 
# 读取图像  

image = cv2.imread('path_to_your_image.jpg')  

# 将图像从RGB转换到HSV  

hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)   

# 在HSV空间中设置一个颜色范围(比如蓝色)  

lower_blue = np.array([110, 50, 50])  

upper_blue = np.array([130, 255, 255])  

# 创建一个掩码,只保留在指定颜色范围内的像素  

mask = cv2.inRange(hsv_image, lower_blue, upper_blue)   

# 使用掩码对原图像进行颜色分割  

result = cv2.bitwise_and(image, image, mask=mask)   

# 显示结果  

cv2.imshow('Original Image', image)  

cv2.imshow('Mask', mask)  

cv2.imshow('Result', result)  

cv2.waitKey(0)  

cv2.destroyAllWindows()
```



这段代码首先读取一个图像,然后将其从RGB空间转换到HSV空间。接着,它定义了一个蓝色的HSV范围,并创建了一个掩码,只保留在这个范围内的像素。最后,它使用这个掩码对原图像进行颜色分割,并显示结果。

使用Opencv实现图像的裁剪可以通过数组索引的方式实现,例如,可以通过下面的代码切分图像中的某个部分:

```python
dogface = input_image[60:250, 70:350]

plt.imshow(dogface)
```

我们还可以把切分出来的部分粘贴到原图上去:

```python
fresh_image=cv2.imread('noidea.jpg') 

fresh_image[200:200+dogface.shape[0], 200:200+dogface.shape[1]]=dogface

print(dogface.shape[0])

print(dogface.shape[1])

plt.imshow(fresh_image)

如果想要实现翻转,可以使用flip函数。通过axis参数控制是水平翻转还是竖直翻转:

flipped_code_0=cv2.flip(input_image,0) # vertical flip

plt.imshow(flipped_code_0)

和numpy.array类似,opencv也提供了数组转置的transpose函数,这被用于翻转行列并生成镜像效果:

transposed=cv2.transpose(input_image)

plt.imshow(transposed)
```



高斯平滑(Gaussian Blurring)是图像处理中常用的一种技术,用于减少图像噪声和细节层次。它通过对图像中的每一个像素点,取其本身和邻域内的其他像素点的加权平均值来实现平滑效果。权重由高斯函数计算得出,距离中心越远的像素点,权重越小。可以通过GaussianBlur实现高斯平滑的效果:

```python
d=5

img_blur5 = cv2.GaussianBlur(input_image, (2*d+1, 2*d+1), -1)[d:-d,d:-d]

plt.imshow(cv2.cvtColor(img_blur5, cv2.COLOR_BGR2RGB))
```

腐蚀操作是一种形态学操作,它用来消除图像中的小物体、断开连接在一起的物体等。腐蚀操作的基本思想是将结构元素在图像上滑动,如果结构元素下的所有像素都是前景像素(通常为白色),则该位置的中心像素被认为是前景像素,否则为背景像素。

```python
import cv2 

import numpy as np 

 

# 读取图像,转换为灰度图像  

image = cv2.imread('path_to_your_image.jpg', 0)  

 

# 定义结构元素(核),这里使用5x5的矩形核  

kernel = np.ones((5, 5), np.uint8)  

 

# 进行腐蚀操作  

eroded_image = cv2.erode(image, kernel, iterations=1)  

 

# 显示原图像和腐蚀后的图像  

cv2.imshow('Original Image', image)  

cv2.imshow('Eroded Image', eroded_image)  

cv2.waitKey(0)  

cv2.destroyAllWindows()
```

锐化操作是一种用于增强图像边缘和细节的技术。在OpenCV中,通常通过使用滤波器来实现锐化,比如拉普拉斯滤波器或者自定义的卷积核。

```python
import cv2 

import numpy as np  

# 读取图像  

image = cv2.imread('path_to_your_image.jpg')  

# 定义锐化卷积核  

kernel = np.array([[-1,-1,-1],  

          [-1,9,-1],  

          [-1,-1,-1]])  

# 应用卷积核进行锐化  

sharpened_image = cv2.filter2D(image, -1, kernel)  

# 显示原图像和锐化后的图像  

cv2.imshow('Original Image', image)  

cv2.imshow('Sharpened Image', sharpened_image)  

cv2.waitKey(0)  

cv2.destroyAllWindows()
```



### **10.1.2  数字图像的特征点**

特征点是数字图像处理中用于图像分析和处理的关键信息点,它们可以代表图像中的显著特征,如角点、边缘和其他结构。有效的特征点检测是很多计算机视觉任务的基础,包括图像匹配、物体识别和追踪等。

Sobel特征点主要基于Sobel算子,这是一种常用于边缘检测的算子。其核心思想是将图像分成水平和垂直两个方向,然后在每个方向上计算梯度并求和,最后将两个方向上的梯度幅值差作为边缘强度。Sobel特征点的计算方法主要是通过应用两个3x3的卷积核对图像进行卷积操作,分别计算图像在水平和垂直方向上的梯度变化,从而识别出图像中的边缘。其主要作用是用于边缘检测,能够突出图像中的边缘特征,有效减少数据量,保留图像的重要结构属性,为后续图像处理和分析提供基础。例如:

```python
sobelimage=cv2.cvtColor(input_image,cv2.COLOR_BGR2GRAY)

 

sobelx = cv2.Sobel(sobelimage,cv2.CV_64F,1,0,ksize=9)

sobely = cv2.Sobel(sobelimage,cv2.CV_64F,0,1,ksize=9)

plt.imshow(sobelx,cmap = 'gray') 
```





![img](./src/wps4.jpg) 

图10.3 Sobel特征点图

Canny算子是由John F. Canny于1986年开发出来的一个多级边缘检测算法,被认为是最好的边缘检测算法之一。它采用了一系列步骤来提取图像中的边缘信息,包括噪声抑制、梯度计算、非极大值抑制、双阈值检测以及边缘连接。

 

首先,Canny算子利用高斯滤波器对输入图像进行平滑处理,以减少图像中的噪声。然后,通过对平滑后的图像应用Sobel或Prewitt等算子,计算每个像素点的梯度幅值和方向。接下来,非极大值抑制过程会在梯度图像上比较每个像素点在其梯度方向上的值,并保留局部最大值点,抑制非边缘像素。之后,Canny算子使用双阈值检测,根据设定的高阈值和低阈值,将梯度图像中的像素点分为强边缘、弱边缘和非边缘三个部分。最后,通过连接强边缘像素和与之相连的弱边缘像素,形成完整的边缘。

Canny算子相比其他边缘检测算法具有更高的准确性和更低的错误率,能够产生单一像素宽度的边缘响应,并尽量减少将噪声和细节等误判为边缘的情况。同时,Canny算子提出的基于边缘梯度方向的非极大值抑制和双阈值的滞后阈值处理,改进了传统算子如Sobel和Prewitt等的不足。

```python
th1=30

th2=60 # Canny recommends threshold 2 is 3 times threshold 1 - you could try experimenting with this...

d=3 # gaussian blur

 

edgeresult=input_image.copy()

edgeresult = cv2.GaussianBlur(edgeresult, (2*d+1, 2*d+1), -1)[d:-d,d:-d]

gray = cv2.cvtColor(edgeresult, cv2.COLOR_BGR2GRAY)

edge = cv2.Canny(gray, th1, th2)

edgeresult[edge != 0] = (0, 255, 0) # this takes pixels in edgeresult where edge non-zero colours them bright green

plt.imshow(cv2.cvtColor(edgeresult, cv2.COLOR_BGR2RGB))
```



![img](./src/wps5.jpg) 

图10.4 Harris角点图

 

Harris角点检测是一种计算机视觉中常用的角点检测算法,用于在图像中检测出角点特征。角点通常被定义为两条边的交点,或者说,角点的局部邻域应该具有两个不同区域的不同方向的边界。

Harris角点检测的原理主要基于图像的灰度变化和局部窗口的协方差矩阵。具体步骤包括:首先,将彩色图像转换为灰度图像;然后,根据灰度值计算每个像素的梯度,通常使用Sobel算子;接着,计算每个像素周围窗口内梯度的自相关矩阵;最后,根据自相关矩阵计算Harris响应函数,用于评估每个像素周围区域是否为角点。当Harris响应函数的值较大时,表示该像素点处存在角点。

 

Harris角点检测具有对图像旋转、尺度变化和亮度变化的不变性,且计算简单快速。它在计算机视觉领域中有广泛的应用,如目标跟踪、运动检测、视频剪辑、三维建模以及目标识别等。通过结合图像的多个角度,Harris角点检测可以恢复图像的完整形状,从而准确地识别和追踪目标。

```python
harris_test=input_image.copy()

#greyscale it

gray = cv2.cvtColor(harris_test,cv2.COLOR_BGR2GRAY)

 

gray = np.float32(gray)

blocksize=4 # 

kernel_size=3 # sobel kernel: must be odd and fairly small

# run the harris corner detector

dst = cv2.cornerHarris(gray,blocksize,kernel_size,0.05) # parameters are blocksize, Sobel parameter and Harris threshold

#result is dilated for marking the corners, this is visualisation related and just makes them bigger

dst = cv2.dilate(dst,None)

#we then plot these on the input image for visualisation purposes, using bright red

harris_test[dst>0.01*dst.max()]=[0,0,255]

plt.imshow(cv2.cvtColor(harris_test, cv2.COLOR_BGR2RGB))
```



![img](./src/wps6.jpg) 

图10.5 ORB特征点图

 

ORB(Oriented FAST and Rotated BRIEF)特征点是一种快速且高效的局部特征点提取和描述方法,它结合了FAST特征点检测算法和BRIEF描述子算法,并通过一系列改进和融合实现了更高的效率和鲁棒性。

ORB特征点检测的过程首先利用FAST算法来快速检测图像中的角点。这些角点通常是图像中像素值发生急剧变化的区域,如边缘、角等。为了提高特征点的旋转不变性,ORB算法会对检测到的角点进行方向计算,为每个角点分配一个主方向。接着,ORB算法使用BRIEF描述子算法为每个角点生成一个紧凑的二进制特征向量。这个特征向量仅包含0和1,其顺序根据特定角点和其周围像素区域的灰度强度变化而定。这种二进制特征向量可以节省内存空间和计算时间,同时保持较高的特征匹配准确性。

ORB算法在实时视觉应用,如SLAM(同时定位与地图构建)和无人机视觉等领域中得到了广泛应用。它提供了一种有效的方式来从图像中提取和描述关键特征点,以便进行后续的匹配、跟踪和三维重建等任务。

```python
orbimg=input_image.copy()

orb = cv2.ORB_create()

# find the keypoints with ORB

kp = orb.detect(orbimg,None)

# compute the descriptors with ORB

kp, des = orb.compute(orbimg, kp)

# draw keypoints

cv2.drawKeypoints(orbimg,kp,orbimg)

plt.imshow(cv2.cvtColor(orbimg, cv2.COLOR_BGR2RGB))
```



![img](./src/wps7.jpg) 

图10.6 ORB特征点匹配图

 

与此同时,ORB特征点还具有平移不变性,简单的几何变换不影响ORB特征点的计算与匹配。下例展示了ORB特征点的匹配操作:

```python
kp2 = orb.detect(img2match,None)

# compute the descriptors with ORB

kp2, des2 = orb.compute(img2match, kp2)

# create BFMatcher object: this is a Brute Force matching object

bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)

# Match descriptors.

matches = bf.match(des,des2)

# Sort them by distance between matches in feature space - so the best matches are first.

matches = sorted(matches, key = lambda x:x.distance)


# Draw first 50 matches.

oimg = cv2.drawMatches(orbimg,kp,img2match,kp2,matches[:50], orbimg)


plt.imshow(cv2.cvtColor(oimg, cv2.COLOR_BGR2RGB))
```



![img](./src/wps8.jpg) 

图10.7 ORB特征点匹配操作图

 

### **10.1.3  计算机视觉**

生物的视觉能够让它们看到现实世界并感知,那么计算机如果想要感受这个世界首先就需要看到这个世界。计算机视觉就是让计算机看到世界的一门学科,它主要聚焦的就是图像信息如何在计算机中存储、表示、处理、分析、理解并应用。在传统的计算机视觉研究中,大家主要是利用一些数学方法实现对计算机图像的计算与处理,还远远达不到真正的理解信息。但随着神经网络的发展,计算机视觉终于迎来了一场革命。

以卷积神经网络和注意力机制为代表的深度学习方法在图像分类、目标检测、语义分割、图像超分辨率、图像生成等领域有着重要作用。如果不知道什么是卷积,可以理解为一个小矩阵在图片上扫描,每一次扫描小矩阵(也叫卷积核)会对扫描到的区域执行按位乘并求和,然后生成一个新的图像,过程如图10.8所示:

![img](./src/wps9.jpg) 

图10.8 图像上执行卷积操作

在图像上执行卷积操作如图所示,卷积核扫描后按照按位乘求和的方式组织了新的图像。这一方法能够降低计算量,但保留图像的有效特征,也是特征提取的一种方法。卷积神经网络把这个卷积核中每一项的值看作一个待学习的权重从而构建神经网络。

注意:卷积神经网络是图像处理的经典方法。它的常见模型包括LeNet-5、ResNet、VGG、GoogleNet、GhostNet等多种结构,也是后继很多网络的Backbone例如Faster RCNN、YOLO、FCN等也都有卷积的影子在里面。

图像分类任务是计算机视觉领域中的一项基本任务,其目标是将输入的图像自动分配到预先定义的类别中。例如,一个图像分类系统可能将输入的图片识别为动物、植物、建筑或其他类别。这种分类依赖于从图像中提取的特征,这些特征可能包括颜色、纹理、形状等信息。研究者们一直在试图寻找不需要手工设计特征的分类模型。

ImageNet数据集的提出,标志着计算机视觉领域进入了一个新的时代。ImageNet是一个大型图像数据库,包含了数以百万计的手动标注的图像,涵盖了上千个不同的类别。这个数据集的提出极大地推动了深度学习在图像分类任务中的应用,因为它提供了足够的数据来训练复杂的神经网络模型。随着深度学习的到来,研究者们开始构建能够自动学习图像特征的神经网络,而不是依赖手工设计的特征。

在深度学习的浪潮中,面向图像分类的深度学习模型经历了多次迭代。最初的模型,如AlexNet,采用了卷积神经网络(CNN)的结构,通过堆叠多个卷积层来提取图像的特征。随后,VGGNet和GoogLeNet等模型进一步提升了性能,通过增加网络深度和宽度,以及引入新的卷积结构,如Inception模块。而ResNet模型则通过引入残差连接,解决了深度神经网络中的梯度消失问题,从而实现了更高的分类准确率。这些模型的迭代不仅推动了图像分类任务的性能提升,也为其他计算机视觉任务提供了强大的基础。

![img](./src/wps10.jpg) 

图10.9 图像分类任务

目标检测任务是计算机视觉领域的一个重要分支,旨在识别图像或视频中的特定目标,并给出它们的位置信息。这个任务通常涉及定位目标物体的边界框,并识别其类别。然而,目标检测面临着一些挑战,其中最突出的是目标尺寸变化、形状多样、遮挡情况以及背景的复杂性。这些因素使得准确检测目标变得困难,尤其是在复杂场景中。

为了评估目标检测算法的性能,我们采用一系列评价指标。这些指标主要包括准确率(Precision)、召回率(Recall)、F1分数以及mAP(mean Average Precision)。在目标检测领域,存在许多经典的数据集用于训练和评估算法。其中,PASCAL VOC、COCO(Common Objects in Context)和ImageNet Detection Challenge等数据集广受欢迎。这些数据集包含了大量标注好的图像,涵盖了各种目标类别和场景,为研究者提供了丰富的实验资源。

随着技术的不断发展,目标检测方法也在不断演进。经典的目标检测方法如YOLO、SSD、Faster R-CNN、DETR等,通过引入深度学习技术,实现了对目标的高效检测。YOLO系列算法通过改进网络结构和损失函数,逐渐提高了检测速度和精度。而Faster R-CNN则通过引入区域提议网络(RPN),实现了对目标区域的快速提取和分类,进一步提高了目标检测的性能。这些经典方法的技术演化和迭代不仅推动了目标检测领域的进步,也为后续研究提供了宝贵的经验和启示。随着技术的不断创新和发展,相信未来目标检测任务将会取得更加卓越的成果。

![img](./src/wps11.jpg)

图10.10 YOLO模型基本结构

 

图像分割任务是指将图像划分为若干个互不相交的区域,每个区域都代表图像中的一个物体或场景的一部分。这一任务在计算机视觉中至关重要,因为它有助于从复杂的图像中提取出有意义的信息。然而,图像分割面临着诸多难点。首先,图像中的物体可能具有复杂的形状、纹理和颜色,使得准确区分不同物体变得困难。其次,光照条件、噪声、遮挡等因素也可能对分割结果产生负面影响。此外,处理不同尺度和分辨率的图像也是一大挑战。

在图像分割模型的技术演化与迭代方面,经典的模型如FCN(全卷积网络)为后续的研究奠定了基础。FCN通过引入全卷积层来替代传统卷积神经网络中的全连接层,从而实现了端到端的像素级预测。随后,U-Net模型进一步推动了图像分割技术的发展。U-Net采用编码器-解码器结构,通过跳跃连接将低层特征与高层特征相结合,提高了分割的准确性和细节保留能力。随着深度学习技术的不断发展,越来越多的模型如DeepLab、Mask R-CNN等不断涌现,它们在特征提取、上下文信息融合、多尺度处理等方面进行了改进和创新,进一步提升了图像分割的性能和鲁棒性。

![img](./src/wps12.jpg) 

图10.11 U-Net模型基本结构

 

图像生成任务是计算机视觉领域的一个重要分支,它涉及到生成具有特定属性的图像。这个任务的目标是根据给定的输入信息,生成一张新的图像。生成对抗网络(GAN)则是这一领域出色的模型。GAN网络,即生成对抗网络,是一种在图像生成任务中表现尤为出色的深度学习模型。GAN网络由两个主要部分构成:生成器和判别器。生成器的任务是生成新的数据样本,如图像,而判别器则负责判断这些生成的样本与真实数据之间的差异。这两个部分通过对抗学习的方式相互竞争,使生成器不断提高其生成逼真样本的能力,同时判别器也持续提高其辨别真伪样本的能力。它的核心思想正是我们在第3章中讲到的博弈论。由于GAN网络具有出色的表现,它已被广泛应用于计算机视觉、自然语言处理和创意艺术等多个领域。

![img](./src/wps13.jpg) 

图10.12 GAN模型基本结构

 

现在,随着人工智能的发展,我们也有了很多面向计算机视觉的大模型。比如midjourney和stable diffusion,SORA……如今,随着人工智能技术的飞速进步,计算机视觉领域也迎来了前所未有的发展机遇。在这一浪潮中,我们见证了众多面向计算机视觉的大模型的涌现,它们如同璀璨的明星,点亮了科技创新的天空。展望未来,随着人工智能技术的不断发展,我们相信会有更多面向计算机视觉的大模型涌现出来,为我们带来更多的惊喜和突破。这些模型将在图像识别、目标检测、图像生成等多个方面发挥越来越重要的作用,推动计算机视觉领域不断向前发展。

## 10.2  计算语言学与自然语言处理

语言同样是可以被计算的。这第二种非常规数据就是对文本的处理与分析。

### **10.2.1  Python处理字符串**

Python处理字符串的功能非常全面和强大,无论是基本的字符串操作还是复杂的文本处理任务,Python都提供了丰富的工具和方法。

 

首先,Python支持多种方式来创建和操作字符串。我们可以使用单引号、双引号或三引号来定义字符串。例如:

 ```python
 str1 = 'Hello, World!'
 
 str2 = "Welcome to Python"
 
 str3 = '''This is a
 
 multi-line string.'''
 ```

 

其次,Python提供了丰富的字符串方法,这些方法允许我们执行各种字符串操作。例如,使用`upper()`和`lower()`方法可以将字符串转换为大写或小写形式:

```python
str_lower = str1.lower()  # 转换为小写

str_upper = str1.upper()  # 转换为大写

print(str_lower)  # 输出: hello, world!

print(str_upper)  # 输出: HELLO, WORLD!
```



此外,Python还支持字符串的切片操作,允许我们提取字符串的子串。切片操作使用冒号分隔起始索引和结束索引,并可以指定步长:

 ```python
 substring = str2[7:13]  # 提取索引7到12之间的字符(不包括13)
 
 print(substring)  # 输出: to Python
 ```



Python还提供了查找和替换字符串中特定子串的功能。例如,使用`find()`方法可以查找子串的索引,而`replace()`方法则可以替换字符串中的特定部分:

 ```python
 index = str1.find('World')  # 查找'World'的索引
 
 print(index)  # 输出: 7
 
  
 
 new_str = str1.replace('World', 'Python')  # 将'World'替换为'Python'
 
 print(new_str)  # 输出: Hello, Python!
 ```

 

对于更复杂的文本处理任务,Python支持正则表达式,通过`re`模块提供了一套强大的工具。正则表达式允许我们定义复杂的字符串模式,并进行匹配、查找和替换操作:

 ```python
 import re
 
 # 使用正则表达式查找所有数字
 
 numbers = re.findall(r'\d+', str3)
 
 print(numbers)  # 输出: ['1', '2'](假设str3中包含数字)
 
 # 使用正则表达式替换字符串中的模式
 
 new_str4 = re.sub(r'\s+', '_', str2)  # 将所有空格替换为下划线
 
 print(new_str4)  # 输出: Welcome_to_Python
 ```

 

通过这些示例,我们可以看到Python处理字符串的灵活性和强大性。无论是简单的字符串拼接和切片,还是复杂的文本搜索和替换,Python都提供了直观且易于使用的工具和方法。这使得Python成为处理文本数据和字符串操作的理想选择。

### **10.2.2  文本的嵌入表示**

图像因为在计算机中以数值张量的形式存储,它的计算我们还可以理解。文本又是为什么可以作为数据而计算呢?这就要提到自然语言处理技术啦。

在自然处理领域,对文本的编码也就是向量化将其转化为序列模型是进行所有任务的基本预处理操作。文本的向量化方法有很多,从最早的基于统计自然语言处理的向量化方法,到后来的基于机器学习的向量化方法,再到目前应用广泛的基于深度学习的向量化方法(尤其是基于大规模预训练模型的向量化),向量化方法的技术路径按照一条有规律的主线向前推进。

最原始的文本向量化方法即为独热编码(One-Hot Encoding)。独热编码将所有文本中的单词进行统计,将每个单词转化为1个0-1向量。显然,独热编码方法仅适合处理单个词、短语等极短文本,不适合对长文本进行建模。基于统计方法的TF-IDF算法则很好地改进了这一问题。TF的意思是词频,IDF的意思是逆文本频率指数,它基于这样一个事实:某个单词在某一篇文章中出现频次越高,同时在其他文章中出现频次越低,则这个单词就越可能是该文章的一个关键词。TF-IDF的基本表达式形如:

| ![img](./src/wps14.png) | (10.1) |
| ----------------------- | ------ |

其中,TF(t,d)词语t 在 文档d 中出现的频率,IDF(t)是逆文本频率指数,它可以衡量 单词t 用于区分这篇文档和其他文档的重要性。IDF的公式如式3.2所示,其中Ntext表示文章总数,Ntext(t)表示含单词t的文章数,分母加1是为了避免分母为0:

| ![img](./src/wps15.png) | (10.2) |
| ----------------------- | ------ |

Word2vec是基于神经网络的模型,引入了机器学习因素,它有两类典型的模型,即:用一个词语作为输入,来预测它周围的上下文的skip-gram模型,和拿一个词语的上下文作为输入,来预测这个词语本身的CBOW模型。CBOW对小型语料比较合适,而Skip-Gram在大型语料中表现更好。图10.13为两种典型的word2vec架构:

![img](./src/wps16.jpg) 

图10.13 两种典型的word2vec架构

 

2018年底微软提出的BERT(Bidirectional Encoder Representation from Transformers)相较于Elmo和GPT-2取得了更好的表现,目前也是应用最广泛的文本向量化方法之一,因为在 BERT 中,特征提取器也是使用的Transformer,且 BERT 模型是真正在双向上深度融合特征的语言模型[[[] Devlin J, Chang M W, Lee K, et al. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding[J]. 2018.]]。

BERT架构如图10.14所示,与GPT、ELMo模型的区别如图所示。BERT与GPT的区别就在于BERT采用的是Transformer Encoder,也就是说每个时刻的Attention计算都能够得到全部时刻的输入,而OpenAI GPT采用了Transformer Decoder,每个时刻的Attention计算只能依赖于该时刻前的所有时刻的输入,因为OpenAI GPT是采用了单向语言模型。

![img](./src/wps17.jpg) 

图10.14 BERT的架构

![img](./src/wps18.jpg) 

图10.15 BERT的嵌入表示形式

![img](./src/wps19.jpg) 

图10.16 BERT、GPT和ELMo的对比

图10.16 BERT预处理进行下游任务的输入是三个嵌入表示叠加,如图10.15,Token embedding 表示当前词的embedding,Segment Embedding 表示当前词所在句子的index embedding,Position Embedding 表示当前词所在位置的index embedding。

在 BERT 出现之前的词嵌入技术,如 Word2Vec 中,一个句子的嵌入表示,往往简单的使用 Word2Vec 得到的各个单词的词嵌入表示进行平均或加和得到,这就导致无法得到包含深层语义和语序信息的词嵌入表示,实际任务中效果也较差。而通过BERT 得到的词嵌入表示融入了更多的语法、词法以及语义信息,而且动态的改变词嵌入也能够让单词在不同语境下具有不同的词嵌入表示。

### **10.2.3  文本的分类与话题模型**

对于文本分类任务,我们可以将其向量化后转化为一般的机器学习分类任务后用机器学习算法解决,也可以用深度学习算法解决。TextCNN网络是2014年提出的用来做文本分类的卷积神经网络,由于其结构简单、效果好,在文本分类、推荐等NLP领域应用广泛。图10.8就是一个TextCNN的模型图:

![img](./src/wps20.jpg) 

图10.17 TextCNN的模型结构

图10.17代表了TextCNN中的模型结构。与图像当中CNN的网络相比,TextCNN 最大的不同便是在输入数据的不同。图像是二维数据, 图像的卷积核是从左到右, 从上到下进行滑动,然后通过卷积核映射来进行特征抽取,而以自然语言为代表的序列模型是一维数据, 虽然经过word-embedding 生成了二维向量,但是对词向量而言无法进行从左到右的滑动卷积。TextCNN的成功更多的是发掘序列模型的卷积模式,通过引入已经训练好的词向量来在多个数据集上达到了非常良好的表现,进一步证明了构造更好的embedding是提升自然语言处理领域各项任务的关键能力。

  循环神经网络被用于文本等序列模型的建模中。文本分类问题就是对输入的文本字符串进行分析判断,之后再输出结果,但字符串无法直接输入到RNN网络,因此在输入之前需要先对文本拆分成单个词组,将词组编码成一个向量,每轮输入一个词组,得到输出结果也是一个向量。嵌入表示将一个词对应为一个向量,向量的每一个维度对应一个浮点值,动态调整这些浮点值使得编码和词的意思相关。这样网络的输入输出都是向量,再最后进行全连接操作对应到不同的分类即可。

![img](./src/wps21.jpg) 

图10.18 使用RNN进行文本分类

如图10.18,RNN进行文本分类时将问题抽象为序列,最后使用softmax进行分类预判。RNN网络不可避免地带来问题就是最后的输出结果受最近的输入较大,而之前较远的输入可能无法影响结果,这就是信息瓶颈问题。可以使用双向LSTM,不仅增加了反向信息传播,而且每一轮的都会有一个输出,将这些输出进行组合之后再传给全连接层。

注意:卷积神经网络应用在文本问题上的一个最大的特点就是快。

自然语言处理模型在处理中文的时候一个很大的问题就是分词,因为中文不像英文单词用空格分隔,并且中文分词的时候要注意歧义的问题。另外,如果想自动识别语句中的专有名词,例如人名、地名、时间等,这一任务叫命名实体识别。对于分词和命名实体识别,都可以将其抽象为序列标注问题解决。

传统的命名实体识别方法依赖于手工规则的系统,结合命名实体库,对每条规则进行权重辅助,然后通过实体与规则的相符情况来进行类型判断。大多数时候,规则往往依赖具体语言领域和文本风格,难以覆盖所有的语言现象。基于机器学习的命名实体识别方法广泛应用了判别式的条件随机场和生成式的隐马尔可夫模型。目前,文本中的序列标注任务通常会采用循环神经网络与条件随机场的融合模型,必要时还可以加入BERT等大规模预训练模型等内容。

LDA(隐狄利克雷分布)在文本话题模型中的应用原理,其实可以理解为一种帮助我们自动找出文本中隐藏话题的“魔法”。想象一下,你有一堆文章,但是你不知道它们主要讲了哪些话题。LDA就能帮你把这些话题找出来。LDA是怎么做到的呢?首先,它认为每篇文章都是由几个不同的话题混合而成的。比如说,一篇文章可能同时讨论了“旅游”和“美食”这两个话题,但可能“旅游”的话题更多一些,“美食”的话题稍微少一些。然后,LDA又认为每个话题都是由一堆特定的词语组成的。比如“旅游”这个话题,就可能会有“风景”、“旅行”、“酒店”等词语;而“美食”这个话题,则可能会有“菜肴”、“口感”、“餐厅”等词语。LDA的工作就是找出每篇文章中各个话题的比例,以及每个话题中各个词语的比例。它会反复地学习、尝试,直到找到一个最合理的解释,即这些文章是如何由这些话题和词语组成的。这样,当我们再次看到一篇新的文章时,LDA就能告诉我们这篇文章主要讨论了哪些话题,以及每个话题在文章中的重要程度。注意, LDA在分析文本的话题模型的时候词汇的语序对话题模型并没有什么显著影响。

例如,我们爬取了B站上有关觉醒年代的所有视频有关数据信息,首先可以绘制一幅它的词云图:

```python
import pandas as pd

df=pd.read_excel("觉醒年代所有视频.xlsx")

import jieba

import wordcloud

# 读取文本

s=''.join(str(i) for i in df['description'])

s=s.replace('nan','')

print(s)

ls = jieba.lcut(s) # 生成分词列表

text = ' '.join(ls) # 连接成字符串

stopwords = ["的","是","了","得","将","不","导致","引起","【","】","http","https","quot",'q','u','o','t','他','你','我'] # 去掉不需要显示的词

wc = wordcloud.WordCloud(font_path="msyh.ttc",

             width = 2000,

             height = 1400,

             background_color='white',

             max_words=500,stopwords=s)

# msyh.ttc电脑本地字体,写可以写成绝对路径

wc.generate(text) # 加载词云文本

wc.to_file("词云1.png") # 保存词云文件

 
```



 

![img](./src/wps22.jpg) 

图10.19 词云图

 

接着,使用gensim包中的LDA模块构造话题模型:

```python
import gensim

from gensim import corpora

import matplotlib.pyplot as plt

import matplotlib

import numpy as np

import warnings

warnings.filterwarnings('ignore')  # To ignore all warnings that arise here to enhance clarity

 

from gensim.models.coherencemodel import CoherenceModel

from gensim.models.ldamodel import LdaModel

dictionary = corpora.Dictionary([ls])  # 构建词典

corpus = [dictionary.doc2bow([text]) for text in ls]  #表示为第几个单词出现了几次

ldamodel = LdaModel(corpus, num_topics=15, id2word = dictionary, passes=30,random_state = 1)  #分为15个主题

print(ldamodel.print_topics(num_topics=15, num_words=20))  #每个主题输出20个单词
```

通过话题模型可以看到,文本可以分为以下话题:

![img](./src/wps23.jpg) 

最后,我们利用snownlp分析文本的情感倾向与极性:

```python
import snownlp

data=pd.DataFrame(strings,columns=['弹幕'])

data['情感极性']=data['弹幕'].apply(lambda x: snownlp.SnowNLP(x).sentiments)

data
```



得到结果如图所示:

![img](./src/wps24.jpg) 

SnowNLP的情感分析功能通常会返回一个情感分数,这个分数可以用来表示文本的情感倾向。通常,分数越高表示情感越积极,分数越低表示情感越消极。你需要了解这个分数的具体含义和取值范围。SnowNLP可能还提供了对文本中特定词汇的情感分析。你可以查看这些词汇及其对应的情感分数,了解哪些词汇对整体情感倾向的影响最大。这有助于你深入理解文本的情感内容。

## 10.3  数字信号处理与智能感知

第三种非常规数据类型是对数字信号的变换与处理。在数字信号处理的世界中,我们经常需要转换和处理各种信号,以便更好地理解和利用这些数据。本教程不准备抢走信号与系统老师的饭碗,所以对数字信号的处理只是简单补充,想要深入了解,请参阅相关的数字信号处理课程。

### **10.3.1  数字信号的傅里叶变换**

傅里叶变换是一种非常有力的数学工具,它让我们能够从一种视角(时域)切换到另一种视角(频域)。通过这种变换,我们可以揭示信号的频率内容,这对于许多应用来说是至关重要的。

 

想象一下,当你听音乐时,你所听到的声音随时间变化,展现出不同的音高和节奏。这种随时间变化的声音就是一个典型的时域信号,即我们可以看到音乐在时间上的波形。然而,仅仅通过波形图观察,我们很难分辨出音乐中包含的各种不同音符或频率成分。

 

这时,傅里叶变换就显得非常有用。它允许我们将这些时域信号转换成频域信号,从而清晰地揭示出音乐中各个不同频率成分的具体信息。在信号处理领域,我们常常将信号视为随时间变化的函数。时域分析关注的是信号随时间的变化,而频域分析则关注的是信号中各个频率成分的表现,这有助于我们深入理解信号的复杂频率特性。

 

傅里叶级数告诉我们,任何周期函数都可以被分解为一系列正弦和余弦函数的和。这些正弦和余弦函数具有不同的频率和相位,通过它们的组合,我们能逼近或完整表达原始的周期函数。对于那些非周期信号,我们可以将其视为周期无限大的信号,通过傅里叶变换进行分析。

 

傅里叶变换的数学公式如下所示:

 

| ![img](./src/wps25.jpg) | (0.3) |
| ----------------------- | ----- |

 

其中:

 ![img](./src/wps26.jpg) 表示信号在频率为 ![img](./src/wps27.jpg) 时的频谱分量;

 ![img](./src/wps28.jpg) 表示信号在时刻 ![img](./src/wps29.jpg) 的值;

 ![img](./src/wps30.jpg) 是复指数函数,其中 ![img](./src/wps31.jpg) 是虚数单位。

这个公式表示的是将时域信号 ![img](./src/wps32.jpg) 转换为频域信号 ![img](./src/wps33.jpg) 的过程。通过积分运算,我们可以得到信号在各个频率上的分量。简单来说,傅里叶变换就是将时间上的信号转换成显示其频率成分的信号。通过这种转换,我们能够计算并分析不同频率上的信号强度。

 

傅里叶变换的结果通常表示为复数,这是因为复数可以同时包含幅度和相位信息。这里的幅度表示频率成分的强度,而相位则显示了这些频率成分在时间轴上的位置。通过分析这些复数结果,我们可以得到信号的幅度谱和相位谱,进而全面理解信号的频率特征。

 

傅里叶变换在许多技术领域都有广泛应用。在信号处理领域,它可以帮助我们过滤或减少噪音。在图像处理中,傅里叶变换用于分析图像的频率属性,帮助进行图像增强和边缘检测。此外,它也是通信技术中不可或缺的一部分,用于编码和传输信号。总之,傅里叶变换是一种强大的数学工具,它能够将信号从时域转换到频域,帮助我们深入理解信号的频率特性并进行相应的处理。

 

如果你想在实际编程中实现傅里叶变换,Python提供了非常便捷的工具。例如,使用NumPy库中的fft模块,你可以轻松进行一维离散傅里叶变换。这里有一个简单的示例,演示了如何用NumPy进行这样的变换:

```python
import numpy as np

import matplotlib.pyplot as plt

# 创建一个包含一些频率成分的信号

fs = 150.0;  # 采样频率

ts = 1.0/fs; # 采样间隔

t = np.arange(0,1,ts) # 时间向量

ff = 5;  # 频率

y = np.sin(2*np.pi*ff*t)

# 对信号进行傅里叶变换

n = len(y) # 长度

k = np.arange(n)

T = n/fs

frq = k/T # 两侧的频率范围

Y = np.fft.fft(y)/n # fft计算并归一化

# 由于对称性,我们只取一半

Y = Y[range(n//2)]

frq = frq[range(n//2)]

# 绘图

plt.subplot(2,1,1)

plt.plot(t,y)

plt.xlabel('Time')

plt.ylabel('Amplitude')

 

plt.subplot(2,1,2)

plt.plot(frq,abs(Y),'r') # 绘制频谱图

plt.xlabel('Freq (Hz)')

plt.ylabel('|Y(freq)|')

plt.tight_layout()

plt.show()
```



 

![img](./src/wps34.jpg) 

图10.20 原始信号与频谱图展示

 

在这个例子中,我们首先创建了一个包含单一频率成分的信号。然后,我们使用`np.fft.fft`函数对该信号进行傅里叶变换,并绘制了原始信号和频谱图。值得注意的是,由于傅里叶变换的结果在频率轴上是对称的,我们通常只展示一半的频谱图。

 

这只是一个入门级的示例。在更复杂的实际应用中,你可能需要处理包含多个频率成分的信号,或者进行更高级的操作,如窗函数处理和滤波等。

 

此外,如果你要处理的数据是二维的(如图像),你可以使用二维傅里叶变换,这可以通过np.fft.fft2函数来实现。二维傅里叶变换在图像处理中特别有用,比如在图像增强或边缘检测等应用中。

 

### **10.3.2  数字信号的统计指标**

在处理信号数据时,尽管其处理方式相比图像和文本数据可能显得更为直接,通常涉及各种类型的滤波技术,我们还需要深入理解信号的内在特性。这些特性通常在时域、频域以及时频域内分析,以提供全面的信号表征。但在介绍信号的滤波之前,我需要先列举一些信号的统计特性,包括时域、频域、时频域:

 

时域分析是信号分析中最直观的方法,它涉及信号随时间变化的特性。在时域中,我们可以提取以下几种关键的统计特性:

 

我们提取的时域特征包括:

- 最大值:信号在观测期间的最高点,表示信号能够达到的最大幅度。

- 最小值:与最大值相对,表示信号在观测期间的最低点。

- 峰值:信号最大值和最小值的差值,常用于描述信号的振幅。

- 偏度:度量信号分布的对称性。正偏度意味着信号的尾部向右延伸较长,负偏度则表示尾部向左延伸较长。

| ![img](./src/wps35.png) | (10.4) |
| ----------------------- | ------ |

其中![img](./src/wps36.png)表示偏度,用于表示统计数据分布偏斜方向和程度。

- 整流平均值

| ![img](./src/wps37.png) | (10.5) |
| ----------------------- | ------ |

其中Xarv表示整流平均值。

- 均值:即为信号中心值,随机信号在均值附近波动,其定义为:

| ![img](./src/wps38.png) | (10.6) |
| ----------------------- | ------ |

其中N为样本大小。

- 标准差:反映出信号的波动程度,其大小与波动程度正相关:

| ![img](./src/wps39.png) | (10.7) |
| ----------------------- | ------ |

 

- 均方根值:即有效值,能作为振动信号振动幅度大小的一个量度,也可以度量故障的严重程度:

| ![img](./src/wps40.png) | (10.8) |
| ----------------------- | ------ |

 

- 峰值指标:用于表示信号中是否存在冲击:

| ![img](./src/wps41.png) | (10.9) |
| ----------------------- | ------ |

C即为峰值指标,Xp为信号的峰值。

- 峭度指标:用于反映信号中冲击的特征:

| ![img](./src/wps42.png) | (10.10) |
| ----------------------- | ------- |

 

- 波形指标:用于检测信号中是否有冲击:

| ![img](./src/wps43.png) | (10.11) |
| ----------------------- | ------- |

 

- 裕度指标:用于检测设备的磨损情况:

| ![img](./src/wps44.png) | (10.12) |
| ----------------------- | ------- |

 

- 脉冲指标:用于检测信号中是否存在冲击:

| ![img](./src/wps45.png) | (10.13) |
| ----------------------- | ------- |

这些特性帮助我们把握信号在时间上的基本行为和波动特点。

当我们分析信号时,确实需要考虑其复杂的自然属性,尤其是当信号的幅值、频率、和相位随机变化时。这些变化给直接的傅里叶变换带来了挑战,因为傅里叶变换需要信号具有稳定的周期性,这在许多实际应用中是不现实的。因此,我们转向功率谱密度分析,它不要求信号具有长期的稳定性,而是关注于信号功率如何在各个频率上分布,从而适用于分析广泛的信号类型,包括随机和非平稳信号。

在频域分析中,除了传统的频率和幅度分析,我们还可以提取一些更为细致的特征,来描述和理解信号的行为和状态:

- 重心频率:当设备发生故障时,可推知某一处频率的振动幅值会发生变化,进而导致功率谱的重心位置发生变化,而重心频率可以反映功率谱的重心位置,故可用重心频率来判断故障状态。

| ![img](./src/wps46.png) | (10.14) |
| ----------------------- | ------- |

其中![img](./src/wps47.png)为重心频率,![img](./src/wps48.png)和![img](./src/wps49.png)分别为时刻![img](./src/wps50.png)对应的频率值与幅值。

- 均方频率:这是一个评估功率谱重心稳定性的指标,可用于追踪功率谱中心的动态变化。其计算方法通常涉及到功率谱的二阶矩。

| ![img](./src/wps51.png) | (10.15) |
| ----------------------- | ------- |

- 频率方差:此参数反映了频率谱能量的分散程度,是评价信号频率分布稳定性的一个关键指标。频率方差越大,表明信号的能量分布越分散。 

| ![img](./src/wps52.png) | (10.16) |
| ----------------------- | ------- |

时频域分析提供了一种同时观察信号在时间和频率两个维度变化的方法,适合分析那些在短时间内频率特性快速变化的信号:

- 频带能量:通过计算特定频带内的总能量,可以帮助我们理解信号在特定频段的能量分布情况。

- 相对功率谱熵:这是度量功率谱分布均匀性的指标。高的功率谱熵意味着信号的能量较为均匀地分布在不同的频率上,而低的功率谱熵则表明信号的能量集中在少数几个频率上。这一指标对于分析信号的复杂性和预测其行为模式非常有用。

| ![img](./src/wps53.png) | (10.17) |
| ----------------------- | ------- |

通过深入分析这些特性,我们不仅能更好地理解信号的基本行为和波动特点,还能在实际应用中,如设备维护和故障预测等领域,提供更准确的数据支持。

### **10.3.3  数字信号的滤波与分解**

数字信号处理(DSP)是一门研究信号的表示、变换和信息提取的学科。在众多DSP应用中,滤波是一项基本而重要的技术,用于去除信号中不需要的成分或提取有用的信息。Python 提供了多个强大的库来支持数字信号处理,其中 scipy 和 numpy 是最常用的工具。

在Python中,你可以使用多种库来对信号进行滤波,其中最常见的库包括`scipy`和`numpy`。

 

准备工作:安装必要的库。在Python中开始信号处理之前,我们需要确保已经安装了必要的库。通过以下命令,您可以快速安装numpy和scipy:

```python
pip install numpy scipy
```

 

然后,我们从创建一个包含两个不同频率成分的合成信号开始。这种信号可以帮助我们演示滤波效果的直观性,并应用不同类型的滤波器:

 ```python
 import numpy as np
 
 import matplotlib.pyplot as plt
 
  
 
 # 设置采样频率
 
 fs = 1000 # Hz,表示每秒采样1000次
 
  
 
 # 生成时间向量,持续1秒
 
 t = np.linspace(0, 1, fs, endpoint=False)
 
  
 
 # 定义信号中的两个频率
 
 f1, f2 = 5, 120 # Hz
 
  
 
 # 使用正弦波叠加生成信号
 
 x = np.sin(2 * np.pi * f1 * t) + 0.5 * np.sin(2 * np.pi * f2 * t)
 
  
 
 # 绘制这个复合信号
 
 plt.figure(figsize=(10, 4))
 
 plt.plot(t[:100], x[:100], label='Original Signal')
 
 plt.title('原始信号')
 
 plt.xlabel('时间 [秒]')
 
 plt.ylabel('振幅')
 
 plt.legend()
 
 plt.show()
 ```



![img](./src/wps54.jpg) 

图10.21 原始信号图

 

​	我们的目标是去除信号中的高频部分。为此,我们将设计一个低通滤波器,只允许频率低于60Hz的部分通过。

```python
from scipy import signal

# 设计一个低通滤波器,截止频率为60Hz

b, a = signal.iirfilter(N=8, Wn=60 / (fs / 2), btype='low', ftype='butter')

# 现在,我们使用刚刚设计的滤波器处理信号,并观察滤波效果。

# 通过滤波器处理信号

y = signal.lfilter(b, a, x)

# 绘制滤波后的信号与原始信号对比

plt.figure(figsize=(10, 8))

plt.subplot(2, 1, 1)

plt.plot(t[:100], x[:100])

plt.title('原始信号')

plt.xlabel('时间 [秒]')

plt.ylabel('振幅')

 

plt.subplot(2, 1, 2)

plt.plot(t[:100], y[:100])

plt.title('经过低通滤波的信号')

plt.xlabel('时间 [秒]')

plt.ylabel('振幅')

plt.tight_layout()

plt.show()
```



![img](./src/wps55.jpg) 

图10.22 原始信号与经过滤波信号对比图

 

在这个例子中,我们创建了一个包含两个频率成分(5Hz和120Hz)的信号。然后,我们设计了一个8阶的IIR低通滤波器,其截止频率为60Hz,并使用`signal.lfilter`函数将滤波器应用于信号。最后,我们绘制了原始信号和滤波后的信号,如图10.22所示。

 

除了低通滤波器外,scipy.signal模块还为我们提供了一系列强大的工具,可以用来设计不同类型的滤波器,从基本的低通和高通滤波器到更复杂的带通和带阻滤波器。你可以通过调整滤波器的类型和参数来满足你的需求。在这里举出一些所支持的高级滤波。

 

巴特沃斯滤波器:通过scipy.signal.butter函数设计。这种滤波器以其平坦的通带特性而闻名,能够在通带内保持较一致的幅度响应,非常适合需要避免频率失真的应用场合。

 

切比雪夫滤波器:通过scipy.signal.cheby1(类型I)和scipy.signal.cheby2(类型II)函数设计。这些滤波器在通带或阻带中具有等波纹性能,使得它们在特定的频率范围内可以实现更快的衰减速率,适用于对滤波器性能要求较高的情况。

 

椭圆滤波器:通过scipy.signal.ellip设计,这类滤波器在通带和阻带都具有等波纹特性,并且能够在较低的滤波器阶数下实现非常陡峭的截止特性,非常适合对过渡带有严格要求的应用。

 

贝塞尔滤波器:通过scipy.signal.bessel设计,这种滤波器在所有滤波器中最注重相位特性的线性,使之成为处理音频和其他需要精确相位信息的信号的理想选择。

 

FIR滤波器设计:scipy.signal还提供了firwin和firwin2函数,用于设计具有指定频率响应的有限脉冲响应(FIR)滤波器。这类滤波器通常更易于设计并且能够完全实现线性相位特性。

 

除了基本的滤波器设计,scipy.signal还支持更高级的功能,如使用窗函数法设计滤波器、优化滤波器系数等。这些高级技术允许用户在保证滤波性能的同时,优化滤波器的结构和效率。使用这些滤波方法时,你可以通过调整滤波器的阶数、截止频率、类型等参数来优化滤波器的性能。此外,`scipy.signal`还提供了其他功能强大的信号处理函数,如卷积、相关、频谱分析等,以支持更复杂的信号处理任务。

 

如果你需要更高级的滤波功能,例如窗函数设计、滤波器系数优化等,你可能需要深入了解数字信号处理的理论,并查阅`scipy.signal`模块的文档以获取更多信息。请注意,设计滤波器时需要根据具体的应用需求选择合适的滤波器类型和参数。在设计滤波器之前,了解数字信号处理的基本原理和滤波器的特性是非常重要的。

 

在数字信号处理的实践中,我们不仅需要考虑如何设计和优化滤波器,还经常需要处理信号的非线性和非平稳特性。这就引出了一种非常有用的分析方法:经验模态分解(Empirical Mode Decomposition,简称EMD)是一种用于分析非线性、非平稳信号的方法。它可以将一个复杂的信号分解成一系列本征模态函数(Intrinsic Mode Functions,简称IMF),这些IMF代表了信号在不同时间尺度上的特征。

 

简单来说,EMD就像是一种“筛”信号的方法。想象你手里有一堆不同大小的沙子,你想要把它们分开。EMD就像是一个筛子,它可以帮助你逐步把大颗粒、中颗粒和小颗粒的沙子分开。对于信号来说,这个“筛子”就是EMD算法,而“沙子”就是信号中的不同成分。这使得EMD成为分析复杂信号的有效工具,尤其是在处理音频和生物医学信号等领域中非常有用。

 

接下来我们会进行EMD的编程实战,你需要安装 PyEMD 库。打开你的命令行工具,并输入以下命令来安装:

```python
pip install EMD-signal
```

这行命令会从 Python 包索引(PyPI)下载并安装 PyEMD 库,让你可以在你的代码中使用它。接下来,我们将编写一个 Python 脚本来执行经验模态分解。这里是详细的步骤和代码解释:

首先,我们需要导入几个 Python 库,包括用于数据操作的 numpy,用于数据可视化的 matplotlib.pyplot,以及 PyEMD 库中的 EMD 类。我们还使用 scipy.signal 来添加噪声,增强示例的实用性。

```python
import numpy as np

import matplotlib.pyplot as plt

from PyEMD import EMD

import scipy.signal as signal

 

我们将生成一个简单的测试信号,包括两个不同频率的正弦波,并加上随机噪声。这个信号将作为 EMD 分解的输入。

t = np.linspace(0, 1, 200)  # 时间向量

s = np.sin(11*2*np.pi*t) + np.sin(22*2*np.pi*t) + 0.2*np.random.normal(size=t.size)  # 信号生成

 

使用 EMD 类初始化一个分解器,并对信号 s 进行分解,提取出多个内在模态函数(IMF)。

emd = EMD()

IMFs = emd(s)

 

最后,我们使用 matplotlib 库来绘制原始信号和每个分解出的 IMF。这有助于可视化分解的效果,更好地理解每个 IMF 的物理意义。

plt.figure(figsize=(12, 9))

plt.subplot(len(IMFs)+1, 1, 1)

plt.plot(t, s, 'r')

plt.title("Input signal: 's(t)'")

plt.xlabel("Time [s]")

for n, imf in enumerate(IMFs):

  plt.subplot(len(IMFs)+1, 1, n+2)

  plt.plot(t, imf, 'g')

  plt.title("IMF "+str(n+1))

  plt.xlabel("Time [s]")

plt.tight_layout()

plt.show()
```

![img](./src/wps56.jpg) 

 图10.23 原始信号与IMF图

 

如图10.23所示,这段代码首先生成了一个包含两个不同频率正弦波和噪声的信号。然后,它使用`EMD`类来初始化一个经验模态分解器,并将信号传递给分解器。分解器会返回一系列IMFs,这些IMFs代表了信号中的不同成分。最后,代码使用matplotlib库来绘制原始信号和每个IMF的图形。

通过这个教程,你应该能够理解如何使用 Python 和 PyEMD 库来处理和分析信号。这种技能在许多领域都非常有用,比如声音分析、经济数据处理和其他需要信号分解的场合。

## 10.4  多模态数据与人工智能

### **10.4.1  多模态概念与意义**

模态是一个指生物通过感知器官(如眼睛、耳朵)和经验来接收和处理信息的方式的概念。人类有视觉、听觉、触觉、味觉和嗅觉五种基本的感知方式,这些都可以被视为不同的模态。

多模态是指结合多种感知方式或信息来源的概念。在技术领域,这意味着通过结合声音、图片、视频、文字等多种形式的信息来让机器更好地理解和与人类交流。例如,一个多模态的人工智能系统可以同时理解语音指令和用户的表情,从而做出更合适的反应。

多模态学习的核心在于它能够让机器通过多种方式理解世界,这对于构建高效且自然的人机交互系统尤为重要。例如,多模态系统可以在自动驾驶车辆中同时处理视觉数据和传感器数据,或者在智能助手中同时理解语音和文本输入。

传统的深度学习模型通常专注于单一模态的数据处理,如仅处理图像或文本。多模态学习则通过融合不同的数据类型(如图像和文本),在许多领域都有广泛的应用,应用方向不限于自然语言处理、计算机视觉、音频处理等。具体任务又可以分为文本和图像的语义理解、图像描述、视觉定位、对话问答、视觉问答、视频的分类和识别、音频的情感分析和语音识别等。

![img](./src/wps57.jpg) 

图10.24 多模态任务介绍

 

未来,多模态学习的发展将重点在于增强模型的跨模态处理能力,使其能够更好地整合和理解来自不同模态的信息。这不仅能提升机器的理解能力,还能推动AI技术在自动化和智能化应用中的广泛应用。接下来我们将介绍多模态模型目前的发展情况。

### **10.4.2  多模态模型发展关系及时间线**

![img](./src/wps58.jpg) 

图10.25 多模态模型发展时间

引用来源:论文《MMLLMs: Recent Advances in MultiModal Large Language Models》

上述的大多多模态模型结构可以总结为五个主要关键组件,具体如下图所示:

![img](./src/wps59.jpg) 

图10.26多模态模型基本结构

引用来源:论文《MMLLMs: Recent Advances in MultiModal Large Language Models》

在多模态模型的发展中,主要可以分为以下五个核心组件:

 

***1. 模态编码器(Modality Encoder, ME)***:

  图像编码器(Image Encoder):用于将图像数据转化为机器可处理的特征表示。

  视频编码器(Video Encoder):专门处理视频数据,提取时间和空间特征。

  音频编码器(Audio Encoder):转换音频输入为特征,便于后续的处理和分析。

 

***2. 输入投影器(Input Projector, IP)***:

  线性投影器(Linear Projector):通过简单的线性变换将输入数据映射到一个新的空间。

  多层感知器(MultiLayer Perceptron, MLP):使用深层网络结构进行更复杂的数据转换。

  交叉注意力(CrossAttention):允许模型关注输入数据中的关键部分,以提高信息的相关性和准确性。

  Q-Former:一种基于Transformer的结构,用于处理和优化多模态数据的交互。

 

***3. 大模型基座(LLM Backbone)***:

  ChatGLM、LLaMA、Qwen、Vicuna等:这些大模型为多模态学习提供了强大的基础架构,支持高效的信息处理和复杂任务的执行。

 

***4. 输出投影器(Output Projector, OP)***:

  Tiny Transformer:一个轻量级的Transformer模型,专注于高效的输出生成。

  Multi-Layer Perceptron (MLP):用于将内部表示转换为最终输出,如文本、图像或音频。

 

***5. 模态生成器(Modality Generator, MG)***:

  Stable Diffusion、Zeroscope、AudioLDM等:这些生成器用于创造新的多模态输出,如图像、音频或其它类型的合成数据。

 

按上述五部分结构对经典多模态模型进行总结,结果如下:

![img](./src/wps60.jpg) 

图10.27多模态现有模型类型总结

引用来源:论文《MMLLMs: Recent Advances in MultiModal Large Language Models》

以上各部分的具体应用示例包括基于VIT(Vision Transformer)的视觉预训练模型,这类模型通过Transformers架构有效地对视觉信息进行表征,逐渐成为视觉信息编码的主流方式。这部分主要梳理了以VIT为基础的预训练及其在多模态对齐中的应用,具体分类如下:

![img](./src/wps61.jpg) 

图10.28 VIT视觉预训练模型 

通过这些技术组件的综合应用,多模态预训练模型能够在多种数据类型之间建立深入的联系和理解,从而在各种复杂环境下提供更为准确和自然的交互体验。上述多模态预训练模型发展关系如下:

![img](./src/wps62.jpg) 

图10.28 多模态预训练模型发展关系

### **10.4.3  多模态基础知识--Transformer**

目前,主流的多模态大模型大多以Transformer为基础。Transformer是一种由谷歌在2017年提出的深度学习模型,主要用于自然语言处理(NLP)任务,特别是序列到序列(Sequence-to-Sequence)的学习问题,如文本生成。Transformer彻底改变了之前基于循环神经网络(RNNs)和长短期记忆网络(LSTMs)的序列建模范式,并且在性能提升上取得了显著成效。Transformer结构如下图所示:

![img](./src/wps63.jpg) 

图10.29 Transformer架构图

 

***Transformer的核心构成包括:***

***自注意力机制(Self-Attention Mechanism):*** Transformer模型摒弃了传统RNN结构的时间依赖性,通过自注意力机制实现对输入序列中任意两个位置之间的直接关联建模。每个词的位置可以同时关注整个句子中的其他所有词,计算它们之间的相关性得分,然后根据这些得分加权求和得到该位置的上下文向量表示。这种全局信息的捕获能力极大地提高了模型的表达力。

***多头注意力(Multi-Head Attention):*** Transformer进一步将自注意力机制分解为多个并行的“头部”,每个头部负责从不同角度对输入序列进行关注,从而增强了模型捕捉多种复杂依赖关系的能力。最后,各个头部的结果会拼接并经过线性变换后得到最终的注意力输出。

***位置编码(Positional Encoding):*** 由于Transformer不再使用RNN的顺序处理方式,为了引入序列中词的位置信息,它采用了一种特殊的位置编码方法。这种方法对序列中的每个位置赋予一个特定的向量,该向量的值与位置有关,确保模型在处理过程中能够区分不同的词语顺序。

***编码器-解码器架构(Encoder-Decoder Architecture):*** Transformer采用了标准的编码器-解码器结构,其中,编码器负责理解输入序列,将其转换成高级语义表示;解码器则依据编码器的输出,结合自身产生的隐状态逐步生成目标序列。在解码过程中,解码器还应用了自注意力机制以及一种称为“掩码”(Masking)的技术来防止提前看到未来要预测的部分。

***残差连接(Residual Connections):*** Transformer沿用了ResNet中的残差连接设计,以解决随着网络层数加深带来的梯度消失或爆炸问题,有助于训练更深更复杂的模型。

***层归一化(Layer Normalization):*** Transformer使用了层归一化而非批量归一化,这使得模型在小批量训练时也能获得良好的表现,并且有利于模型收敛。

### **10.4.4  多模态任务对齐**

本节主要介绍文本和图像的多模态数据如何进行对齐。首先,我们将从文本数据的处理开始。

### **文本转Embedding**

Tokenization(分词):Tokenization 是将文本切分为模型可处理的token或子词的过程。此步骤通常采用 BPE 或 WordPiece 分词算法,不仅帮助控制词典大小,同时保留了表达文本序列的能力。相关的关键点总结如下:

![img](./src/wps64.jpg) 

图10.30 Tokenizer分词总结

Embedding(嵌入):Embedding 过程将 token 或子词映射到多维空间中的向量,以捕捉其语义含义。这些连续的向量使模型能处理离散的 token,并学习单词间的复杂关系。使用 Tramsformer (BERT) 模型的步骤包括:

· 输入文本:"thank you very much"

· Tokenization后: ["thank", "you", "very","much"]

· Embedding:假设每个token被映射到一个2048维的向量,“thank you very much”被转换成4*2048的embeddings

### **图像转换Embedding**

图像数据也采用与文本类似的处理方法,通过 Vit Transformer 模型进行处理。首先,把图像分成固定大小的patch,类比于LLM中的Tokenization操作;然后通过线性变换得到patch embedding,类比LLM中的Embedding操作。由于Transformer的输入就是token embeddings序列,所以将图像的patch embedding送入Transformer后就能够直接进行特征提取,得到图像全局特征的embeddings。具体步骤如下:

· 输入图像大小:224x224像素,3个颜色通道(RGB)+ 预处理:归一化,但不改变图像大小图像切分:

· 假设每个patch大小为14x14像素,图像被切分成(224/14) × (224/14) =256个patches 线性嵌入:

· 将每个14x14x3的patch展平成一个一维向量,向量大小为 14×14×3=588

· 通过一个线性层将每个patch的向量映射到其他维的空间(假设是D维),例如D=768 , 每个patch被表示为一个D维的向量。最后,由于Transformer内部不改变这些向量的大小,就可以用256*768的embeddings表示一张图像。

![img](./src/wps65.jpg) 

图10.31 图像Embedding处理

 

### **模态对齐**

模态对齐是指将来自不同模态的数据转化为能够相互对应的统一形式,从而使得不同模态之间可以协同工作,共同完成任务。在处理图像和文本的任务中,模态对齐特别关键,因为它允许模型理解并关联视觉信息和语言信息。

例如,在图像标注任务中,模型需要不仅识别出图像中的物体,如"小狗",还需要将其与相应的文本描述对齐。如果图像中的"小狗"和文本中的"小狗"在各自的模态空间中被不同地表示,模型就需要通过某种方式来桥接这种差异,使得两者能够匹配。常用的模态对齐方法包括但不限于使用联合嵌入空间、对齐损失函数和跨模态转换网络。

![img](./src/wps66.jpg) 

图10.32 图片对齐语言示意图

 

这种对齐通常涉及到以下几个步骤:

1. 特征提取: 这就像是从故事和图片中各自提炼出关键信息。对于计算机,这意味着使用算法从图片中识别出像是轮廓、颜色和形状这样的视觉元素,同时从文本中识别出关键词和语义结构。这样做的结果是,无论是图片还是文字,都转换成了计算机能理解的数字列表——也就是向量。
2. 特征变换: 接下来,就像是将故事的翻译成插画的“语言”,或者反过来。这个过程需要确保提取的特征可以在一个共同的“向量空间”中进行比较。有时候,这就像是在不同的文化中找到共同的表达方式,让一种表达形式能够反映另一种的含义。
3. 对齐策略: 现在,我们需要一种方法来确认故事和插图确实是对应的。在计算机的世界里,这可以通过增加两者之间相似度的方法来实现。想象一下,你通过比较故事和插图之间的相似点,来确认它们是匹配的。
4. 端到端训练: 最后,就像在不断的游戏中学习,计算机通过不停地尝试和调整来更好地匹配故事和图片。它会逐渐学会如何提取特征、变换它们,并找到最佳的对齐方式。

 

通过这种方式,计算机最终能够处理并理解多种形式的信息,无论是看到的图片还是读到的文本。这项技术不仅应用于图像和文本,还可以扩展到视频、音频和更多其他类型的数据。模态对齐让计算机能够在更复杂、更接近人类理解世界的方式上工作,这对于创造聪明、灵活的人工智能系统来说至关重要。

### **10.4.5  多模态模型训练流程**

在这个领域,我们的教程继续深入了解多模态大型语言模型的训练,这个过程分成两个紧密联系的阶段:多模态预训练(Multimodal Pre-Training, MM PT)和多模态指令调优(Multimodal Instruction Tuning, MM IT)。让我们一起走近这两个阶段的具体细节:

 

(1)多模态预训练(MM PT):

目标:在这个阶段,我们专注于训练输入投影器(Input Projector, IP)和输出投影器(Output Projector, OP),目的是让模型学会如何把视觉、听觉和语言等不同的信息模态转换成统一的表示形式。简单来说,就是教会大型语言模型(Large Language Model, LLM)的主干网络如何理解和处理这些不同来源的信息。

数据集:通常,这里涉及的是像X-Text这样的多模态数据集,其中包含了图像-文本对(Image-Text),视频-文本对(Video-Text)以及音频-文本对(Audio-Text)。这样的数据集让模型有机会学习如何把不同形式的信息对应起来,就好比学习多种语言的翻译。

优化:在预训练的过程中,核心的任务是优化IP和OP的参数,以最小化条件文本生成损失(conditional text generation loss)。这涉及到一个过程,让模型学会如何将模态编码器(Modality Encoder)输出的特征和文本特征相对齐,从而生成可以直接喂给LLM主干网络的特征。

 

(2)多模态指令调优(MM IT):

目标:当模型基本理解了不同模态信息后,我们进入指令调优阶段。这里的目标是细化模型的能力,使其能够更好地根据特定的、格式化的指令来执行任务。就像在预训练后教会模型如何根据具体的操作手册来行动。

方法:这个阶段的方法分为两种。首先是监督式微调(Supervised Fine-Tuning, SFT),它将前一阶段学习到的数据转换成特定的指令格式,然后对模型进行进一步训练。另外一种方法是基于人类反馈的强化学习(Reinforcement Learning from Human Feedback, RLHF),这种方法依赖于对模型产生的输出给予反馈,进一步改进模型的性能。

数据集:在这个阶段使用的数据集一般包括了视觉问答(Visual Question Answering, VQA)任务、基于指令的执行任务等,这些可以是简单的单轮问答形式,也可以是更复杂的多轮对话形式。

 

通过以上两个阶段,我们不仅让模型学会了如何处理和理解不同的信息模态,还教会了它如何根据特定指令行动。这使得模型在面对多样化的实际任务时,能够有着更为出色的表现和更高的灵活性。

================================================
FILE: docs/CH2/第2章-微分方程与动力系统.md
================================================

<center><h1>第2章 微分方程与动力系统</h1></center>
> 内容:@若冰(马世拓)
> 
> 审稿:@陈思州
> 
> 排版&校对:@何瑞杰

这一章我们主要介绍微分方程与一些动力系统模型。数学上对微分方程的研究是一项热点问题,在工程当中微分方程与动力系统也有着广泛的应用。我们除了会从高等数学与计算数值方法的角度分析微分方程的求解方法与底层逻辑,还会介绍在数学模型当中被广泛应用的微分方程模型。本章主要涉及到的知识点有:

* 微分方程的解法
* 如何用python解微分方程
* 偏微分方程及其求解方法
* 微分方程的基本案例
* 差分方程的求解
* 数值计算方法
* 元胞自动机

注意:本章的重点是理解微分方程在实际工程中的应用。如果对数学基础有疑问,可以参考相关的高等数学和数值分析教材。

## 2.1  微分方程的理论基础

微分方程是什么?如果你参加过高考,可能在高三备考中遇到过这样的问题:给定函数$f(x)$及其导数之间的等式,然后分析函数的性质,如单调性、零点等,但没有给出函数的解析式。这时你可能会想,如果能通过这个方程求出函数的通项形式该多好!微分方程的目的就是这样,它通过将函数$f$和它的若干阶导数联系起来形成一个方程(组),来求出函数的解析式或函数值随自变量变化的曲线。

### 2.1.1  函数、导数与微分

微分和导数其实是紧密相关的概念。我们通常将导数理解为函数在某一点处切线的斜率。而微分则描述的是当我们对自变量x施加一个非常小的增量$\mathrm dx$时,函数值相应的变化量与$\mathrm dx$之间的关系。当$\mathrm dx$非常小的时候,函数的变化量就接近于在该点处切线的变化量$\mathrm{d}y$。因此,我们可以用这种方式来理解微分:
$$
\frac{\mathrm{d}y}{\mathrm{d}x} = f'(x). \tag{2.1.1}
$$
在图2.1.1中,我们展示了函数、导数和微分之间的关系。微分实际上描述的是点$M$处切线的斜率;导数则描述的是割线$MN$的斜率。但当$\mathrm{d}x$足够小的时候,切线的斜率和割线的斜率就会非常接近,这就是微分的核心概念。而微分方程,就是描述函数与其导数之间关系的方程。

![500](./attachments/Pasted%20image%2020240423220303.png)
<center>图2.1.1  函数、导数和微分之间的关系</center>
相对于求微分,我们还有求积分的概念。积分本质上是根据已知的导数反推出原函数,这就是不定积分。而定积分则是在反推出原函数后,还需要计算该函数在特定区间内的值的差异。通常情况下,我们可以通过查阅常见函数的导数表来进行微分和不定积分的计算。

> 注意:割线斜率等于切线斜率的前提是dx非常小,这是一种极限思想的体现。虽然它们之间存在一个无穷小量PN的差距,但当我们在考虑dx时,这种差异就可以忽略不计了。这就是微分和积分的基本思想。

### 2.1.2  一阶线性微分方程的解

一阶线性微分方程描述的是怎么一回事呢?它是指形如下方的方程:
$$
\frac{\mathrm{d}y}{\mathrm{d}x} + yP(x) = Q(x). \tag{2.1.2}
$$
这里的$y$是一个未知函数,而$P$和$Q$是已知的函数。我们的目标是找出$y$的解,即它的通解形式。为了解这个方程,我们通常会使用分离变量积分法和常数变易法这两种方法。首先,我们尝试解一个特殊情况的齐次方程,即当$Q(x)=0$时:
$$
\frac{\mathrm{d}y}{\mathrm{d}x} + yP(x) = 0. \tag{2.1.3}
$$
通过变量分离和变形,我们可以得到:
$$
\frac{1}{y} \mathrm{d}y = P(x)\mathrm{d}x.\tag{2.1.4}
$$
接着,对两边进行不定积分,我们可以得到解的通式为$\displaystyle y = C\exp\left\{-\int {P(x)} \, \mathrm d{x}\right\}$,其中$C$是一个常数。但在一般情况下,$Q(x)$不一定为$0$,所以我们需要将常数$C$替换为一个函数$C(x)$,然后对$y$求导并将其代入原方程中以求得$C(x)$的通解。这就是所谓的常数变易法。有兴趣的读者可以进一步推导出方程的通解为(其中$C$为常数):
$$
y = \exp \left\{ -\int {P(x)} \, \mathrm d{x} \right\} \left[ \int {Q(x)}\exp \left\{ \int {P(x)} \, \mathrm d{x} \right\} \, \mathrm d{x} + C \right]. \tag{2.1.5} 
$$

> 注意:这里的定积分符号用于求原函数。这就是为什么我们在高中学习的积分符号应该按照这种方式书写的原因。齐次方程指的是方程右边等于$0$的情况,而非齐次方程则是方程右边不恒等于$0$的情况。解非齐次方程更具有一般性,但很多非齐次方程的解也是基于齐次方程的解进行拓展的。


### 2.1.3  二阶常系数线性微分方程的解

二阶常系数线性微分方程可以表示为:
$$
f''(x) + pf'(x) + qf(x) = C(x). \tag{2.1.6}
$$
这个方程关联了二阶导数、一阶导数和函数本身。解决这个方程的一般策略是先考虑对应的齐次方程,即让$C(x)$为$0$:
$$
f''(x) + pf'(x) + qf(x) = 0. \tag{2.1.7}
$$
解这种二阶常系数齐次线性微分方程时,我们通常使用特征根法。这个方法的关键是求解特征方程:
$$
r^{2} + pr + q = 0. \tag{2.1.8}
$$
这个齐次方程的解的形式取决于特征方程的根。根据特征方程的不同实根、相同实根、或共轭复根,齐次微分方程的解会有不同的形式:
$$
\left\{  
\begin{align}
y &= C_{1}e^{\alpha_{1}x} + C_{2}e^{\alpha_{2}x} & r_{1} = \alpha_{1}, r_{2} = \alpha_{2}\\[0.5em]
y &= (C_{1}x + C_{2})e^{\alpha x}, & r_{1} = r_{2} = \alpha\\[0.5em]
y &= e^{\alpha x}\Big[C_{1}\sin\big( \beta  x\big) + C_{2}\cos \big(\beta  x\big)\Big]. & r = \alpha \pm \beta \,\mathrm{i}
\end{align}
\right.\tag{2.1.9}
$$

> 注意:这里可能有些读者不太明白为什么二次方程的根与齐次方程的解之间会有联系,这正是数学之美的体现之一。如果想检验这个方程的解是否正确,实际上并不难,可以使用 Vieta 定理将 $p$ 和 $q$ 代入,将两个方程统一起来,再通过换元法将其降为一阶微分方程进行验证。

对于一般的二阶非齐次线性微分方程,我们可以根据右侧$C(x)$的形式推导出一个特解。非齐次方程的通解等于齐次方程的通解加上非齐次方程的特解。求微分方程的特解有时需要观察法,但幸运的是,存在两种特殊形式:
$$
\begin{align}
C(x) &= P_{m}(x) e^{\lambda x},\\[0.5em]
C(x) &= e^{\lambda x} \Big[ P_{m}\cos \big( \omega x \big) + Q_{n}(x)\sin \big( \omega x \big)  \Big]
\end{align}
$$
其中,$P_m(x)$是一个$m$次多项式,$Q_n(x)$是一个$n$次多项式。这两种形式的特解分别为:
$$
\begin{align}
f(x) &= x_{k} P_{m}(x)e^{\lambda x},\\[0.5em]
f(x) &= x^{k} e^{\lambda x} \Big[ P_{i}\cos \big( \omega x \big) + Q_{i}(x)\sin \big( \omega x \big) \Big]. & i = \max \left\{ m,n \right\} 
\end{align} \tag{2.1.11}
$$
其中$k$的取值取决于特征方程根的个数:如果有两个不同的实根,则$k=2$;如果有两个相同的实根,则$k=1$;如果没有实根,则$k=0$。通过上述形式,我们可以解出二阶线性微分方程。

特征根法和“特解+通解”的策略不仅适用于二阶线性微分方程,也适用于一般的高阶线性微分方程。只要特征方程是多项式,它至少满足韦达定理。在后续的差分方程中,特征根法同样会发挥重要作用。

### 2.1.4  利用Python求函数的微分与积分

在Python中,我们可以使用Numpy和SciPy这两个库来进行函数的微分和积分计算。下面将通过具体示例来说明如何使用这些库来求解函数的微分和积分。
假设我们需要计算函数`f(x) = cos(2πx) * exp(-x) + 1.2`在区间`[0, 0.7]`上的定积分。我们可以使用SciPy库中的`quad`函数来完成这个任务:

```python
import numpy as np
from scipy.integrate import quad
# 定义函数
def f(x):
    return np.cos(2 * np.pi * x) * np.exp(-x) + 1.2
# 计算定积分
integral, error = quad(f, 0, 0.7)
print(f'定积分的结果是:{integral}')
# 定积分的结果是:0.7951866427656943 
```

除了使用SciPy库中的`quad`函数求解定积分外,我们还可以使用数值积分的方法来近似计算。一种常见的数值积分方法是梯形法则。下面我们将通过一个示例来说明如何使用梯形法则来近似计算函数的定积分。
假设我们需要计算函数`f(x) = cos(2πx) * exp(-x) + 1.2`在区间`[0, 0.7]`上的定积分。我们可以使用梯形法则来近似求解:

```python
h=x[1]-x[0]
xn=0.7
s=0
for i in range(1000):
    xn1=xn+h
    yn=np.cos(2*np.pi*xn)*np.exp(-xn)+1.2
    yn1=np.cos(2*np.pi*xn1)*np.exp(-xn1)+1.2
    s0=(yn+yn1)*h/2
    s+=s0
    xn=xn1
s
# 24.31183595181452
```

对于函数的微分,我们可以使用Numpy库中的`gradient`函数来近似求解。例如,我们想要求解函数`f(x) = x^2`在点`x = 1`处的导数:

```python
import numpy as np
# 定义x的取值范围和步长
x = np.linspace(0, 2, 100)
y = x**2
# 计算导数
dydx = np.gradient(y, x)
# 在x=1处的导数值
derivative_at_1 = dydx[np.argmin(abs(x - 1))]
print(f'在x=1处的导数值是:{derivative_at_1}')
# 在x=1处的导数值是:1.9797979797979792
```

以上示例展示了如何在Python中求解函数的积分和微分。在实际应用中,可以根据具体问题调整函数表达式、积分区间和微分点等参数。

## 2.2  使用Scipy和Sympy解微分方程

前面我们见过了求微分方程解析解的一些方法,我们知道,微分方程的解本质上是通过给定函数与微分之间的关系求解出函数的表达式。但是事实上,大多数微分方程是没有解析解的,也就是无法求解出函数的具体解析式。这是不是意味着这样的微分方程不可解呢?也不尽然。在上一章中我们已经见过了,以前我们难以求解的超越方程也是可能给出数值解的,那么微分方程是否也会存在数值解呢?

### 2.2.1  使用sympy求解微分方程解析解

我们此前介绍的一阶、二阶常系数线性微分方程通解的形式就是一种解析解,但在科学与工程实际中我们遇到的微分方程形式会比这些基本形式更为复杂,条件也更多。事实上多数情况下,大多数微分方程其实是求不出解析解的,只能在不同取值条件下求一个数值解。那么如何编写算法去求数值解才能使精度尽可能提高呢?数值解会随着初始条件而变化,怎么变化呢?函数值又与自变量之间怎么变化呢?

在回答这些问题之前,请让我们先了解一番:如何使用python求解微分方程的解析解呢?但凡涉及到符号运算,通常都是使用sympy库实现。Sympy是一个数学符号运算库。能解决积分、微分方程等各种数学运算方法,用起来也是很简单,效果可以和Matlab媲美。其中内置的Sympy.dsolve方法是解微分方程解析解的一种良好方式,而对于有初始值的微分方程问题,我们通常在求出其通解形式后通过解方程组的方法得到参数。这个方法通过声明符号变量的方式求得最优解。

例如,我们看下面这个例子:
**例2.1** 使用sympy解下面这个微分方程:
$$
y'' + 2y' + y = x^{2}. \tag{2.2.1}
$$
若使用sympy,我们首先要声明两个符号变量,其中变量`y`是变量`x`的函数。代码如下:

```python
from sympy import *
y = symbols('y', cls=Function)
x = symbols('x')
eq = Eq(y(x).diff(x,2)+2*y(x).diff(x,1)+y(x), x*x)
## y''+4y'+29y=0
print(dsolve(eq, y(x)))
```

这段代码通过sympy中的`symbols`类创建两个实例化的符号变量`x`和`y`,在`y`中我们通过`cls`参数声明`y`是一个`scipy.Function`对象(也就是说,`y`是一个函数)。表达微分方程解析解的方法是通过创建一个`Eq`对象,这个对象分别存储方程左右两边。其中,`y(x).diff(x,2)`表明`y`是`x`的函数,然后需要取函数对`x`的2阶导数。最后,若想求解函数`y`的解析式,只需要调用`dsolve(eq,y(x))`函数即可。代码返回结果:

```python
Eq(y(x), x**2 - 4*x + (C1 + C2*x)*exp(-x) + 6)
```

可以看到,代码能够给出完整的解析式。之所以还保留了参数`C1`和`C2`是因为在求解过程中没有给微分方程指定初值。

我们再来看一个例子,这个例子是使用sympy解一个常微分方程组:

**例2.2** 使用sympy解下面这个常微分方程组:
$$
\left\{ 
\begin{align}
\frac{\mathrm{d}x_{1}}{\mathrm{d}t} &= 2x_{1} - 3x_{2} + 3x_{3}, & x_{1}(0) = 1\\
\frac{\mathrm{d}x_{2}}{\mathrm{d}t} &= 4x_{1} - 5x_{2} + 3x_{3}, & x_{2}(0) = 2\\
\frac{\mathrm{d}x_{3}}{\mathrm{d}t} &= 4x_{1} - 4x_{2} + 2x_{3}. & x_{3}(0) = 3\\
\end{align}
\right. \tag{2.2.2}
$$
这个方程组里面的$x_{1}, x_{2}, x_{3}$都是关于$t$的函数,所以需要声明四个符号变量。不同的是,在这里每个函数都指定了初始值,并且三个函数的导数高度相关,该怎么描述这种相关呢?我们来看下面的例子:

```python
t=symbols('t')
x1,x2,x3=symbols('x1,x2,x3',cls=Function)
eq=[x1(t).diff(t)-2*x1(t)+3*x2(t)-3*x3(t),
    x2(t).diff(t)-4*x1(t)+5*x2(t)-3*x3(t),
    x3(t).diff(t)-4*x1(t)+4*x2(t)-2*x3(t)]
con={x1(0):1, x2(0):2, x3(0):3}
s=dsolve(eq,ics=con)
print(s)
```

sympy当中内置的`symbols`工具是可以通过字符串批量创建变量的,这为我们带来了很大的方便。如果需要求解的是一个方程组,则使用列表将每一个方程表达出来即可。这里我们采取了不创建对象的方式,而是直接将方程组移项使每个方程右侧都为`0`。通过字典的方式保存函数的初始值,并利用`ics`参数传入`dsolve`从而得到方程的解。

```python
[Eq(x1(t), 2*exp(2*t) - exp(-t)), Eq(x2(t), 2*exp(2*t) - exp(-t) + exp(-2*t)), Eq(x3(t), 2*exp(2*t) + exp(-2*t))]
```

结果返回的是一个`Eq`对象构成的列表,每个对象代表了一个函数的解析式。对于这个例子,大家可以发现:它是一个线性的微分方程组,而针对线性方程我们还可以使用矩阵的形式去表示。所以,这个问题还有第二种写法:

```python
x=Matrix([x1(t),x2(t),x3(t)])
A=Matrix([[2,-3,3],[4,-5,3],[4,-4,2]])
eq=x.diff(t)-A*x
s=dsolve(eq,ics={x1(0):1, x2(0):2, x3(0):3})
print(s)
```

通过sympy中内置的符号矩阵`Matrix`对象构造函数向量和系数矩阵,通过对方程组矩阵化也可以得出一样的结果。返回值同上。使用sympy中的符号函数绘图得到结果如下:

```python
from sympy.plotting import plot
from sympy import *
t=Symbol('t')
plot(2*exp(2*t) - exp(-t), line_color='red')
plot(2*exp(2*t) - exp(-t) + exp(-2*t), line_color='blue')
plot(2*exp(2*t) + exp(-2*t), line_color='green')
```

![](./attachments/Pasted%20image%2020240423223752.png)
<center>图2.1.2  sympy求解图</center>
sympy通过plotting下面的plot功能可以进行一些符号函数的绘图,但每一次调用都会创建一个独立的图窗,难以在同一张图上绘制多个函数的曲线。若要绘制多个函数则需要使用matplotlib来完成。

### 2.2.2  使用scipy求解微分方程数值解

微分方程的数值解是什么样子的呢?虽然大多数微分方程没有解析解,但解析式也并不是唯一可以表示函数的形式。函数的表示还可以用列表法和作图法来表示,而微分方程的数值解也正是像列表一样针对自变量数组中的每一个取值给出相对精确的因变量值。

Python求解微分方程数值解可以使用scipy库中的`integrate`包。在这当中有两个重要的函数:`odeint`和`solve_ivp`。但本质上,从底层来讲求解微分方程数值解的核心原理都是Euler 法和Runge-Kutta 法。关于这两个方法,我们会在后面进行进一步探讨。

我们先来了解一下`odeint`的用法吧。`odeint()`函数需要至少三个变量,第一个是微分方程函数,第二个是微分方程初值,第三个是微分的自变量。为了具体了解它的用法,我们通过一个例子来分析:

**例2.3** 使用scipy解下面这个微分方程的数值解:
$$
y' = \frac{1}{1 + x^{2}} - 2y^{2}, \quad y(0) = 0.\tag{2.2.3}
$$

首先需要通过`def`语句或者`lambda`表达式定义微分方程的表达式,然后定义微分方程的初值。代码如下:

```python
import matplotlib.pyplot as plt
dy=lambda y,x: 1/(1+x**2)-2*y**2 # y'=1/(1+x^2)-2y^2
'''
def dy(y,x):
    return 1/(1+x**2)-2*y**2
'''
x=np.arange(0,10.5,0.1) #从0开始,每次增加0.1,到10.5为止(取不到10.5)
sol=odeint(dy,0,x) # odeint输入:微分方程dy,y的首项(y(0)等于多少),自变量列表
print("x={}\n对应的数值解y={}".format(x,sol.T))
plt.plot(x,sol)
plt.show()
```

这里`odeint`函数传入的三个参数分别是函数表达式,函数的初值与自变量。自变量是一个数组,通过`numpy.arange`生成一个范围在`[0, 10.5)`的等差数列,公差为`0.1`。返回的结果`sol`是针对数组`x`中每个值的对应函数值,可以通过`matplotlib.pyplot`绘图得到函数的结果。函数的图像如图所示:

![500](./attachments/Pasted%20image%2020240423224135.png)
<center>图2.2.1  odeint函数求解图</center>
我们再来看一个例子,这个例子是一个不可积函数的积分问题:

**例2.4** 使用scipy解下面这个微分方程的数值解:
$$
y'(t) = \sin t^{2}, \quad y(0) = 1 \tag{2.2.4}
$$

仿照例2.3中的代码,这个问题可以改写为:

```python
def dy_dt(y,t):
    return np.sin(t**2)
y0=[1]
t = np.arange(-10,10,0.01)
y=odeint(dy_dt,y0,t)
plt.plot(t, y)
plt.show()
```

得到的结果必然是一个奇函数,图像为:

![500](./attachments/Pasted%20image%2020240423224303.png)
<center>图2.2.2  scipy函数求解图</center>
刚刚两个例子都是讲述了一阶微分方程如何求解,那么二阶及以上的高阶微分方程如何求解呢?事实上,Python求解微分方程数值解的时候是无法直接求解高阶微分方程的,必须通过换元降次的方法实现低阶化,把一个高阶微分方程替换成若干个一阶微分方程组成的微分方程组才能求解。具体的,我们可以看下面这个例子:

**例2.5** 使用scipy解下面这个高阶微分方程的数值解:
$$
\begin{align}
y'' - 20(1 - y^{2})y' + y = 0, \quad y(0) = 0, y'(0) = 2 \tag{2.2.5}
\end{align}
$$

这很显然是个二阶微分方程,并且不是常系数所以不能直接给出解析解。为了给这个方程做降次,令$u=y'$,那么$y''=u'$,式子就可以代换为:

$$
\left\{
\begin{align}
u &= y',\\[0.5em]
u' - 20(1 - y^{2})u + y &= 0,\\[0.5em]
y(0) &= 0,\\[0.5em]
u(0) &= 2.
\end{align}
\right. \tag{2.2.6}
$$

对于微分方程组,我们传入`[y,u]`两个函数的原函数值,返回的函数值为`[y’,u’]`。所以,只需要对每个微分表达式给出解析形式就可以了。代码如下:

```python
# odeint是通过把二阶微分转化为一个方程组的形式求解高阶方程的
# y''=20(1-y^2)y'-y
def fvdp(y,t):
    '''
    要把y看出一个向量,y = [dy0,dy1,dy2,...]分别表示y的n阶导,那么
    y[0]就是需要求解的函数,y[1]表示一阶导,y[2]表示二阶导,以此类推
    '''
    dy1 = y[1]      # y[1]=dy/dt,一阶导                     y[0]表示原函数
    dy2 = 20*(1-y[0]**2) * y[1] - y[0]                    # y[1]表示一阶微分
    # y[0]是最初始,也就是需要求解的函数
    # 注意返回的顺序是[一阶导, 二阶导],这就形成了一阶微分方程组
    return [dy1, dy2] 
    
# 求解的是一个二阶微分方程,所以输入的时候同时输入原函数y和微分y'
# y[0]表示原函数, y[1]表示一阶微分
# dy1表示一阶微分, dy2表示的是二阶微分
# 可以发现,dy1和y[1]表示的是同一个东西
# 把y''分离变量分离出来: dy2=20*(1-y[0]**2)*y[1]-y[0]
def solve_second_order_ode():
    '''
    求解二阶ODE
    '''
    x = np.arange(0,0.25,0.01)#给x规定范围
    y0 = [0.0, 2.0] # 初值条件
    # 初值[3.0, -5.0]表示y(0)=3,y'(0)=-5
    # 返回y,其中y[:,0]是y[0]的值,就是最终解,y[:,1]是y'(x)的值
    y = odeint(fvdp, y0, x)
    
    y1, = plt.plot(x,y[:,0],label='y')
    y1_1, = plt.plot(x,y[:,1],label='y‘')             
    plt.legend(handles=[y1,y1_1])   #创建图例
    
    plt.show()
solve_second_order_ode()
```

定义函数`fvdp`,传入`y`的原函数值和一阶导数值(列表传入),返回`y`的一阶导数值和二阶导数值。初值条件`y(0)=0`和`y'(0)=2`传入`odeint`函数中,自变量是取值`[0, 0.25)`的一个等距数组。解得的`y`其实包含两列,第一列是函数值,第二列是导数值。结果的图像如下。

![500](./attachments/Pasted%20image%2020240423224853.png)
<center>图2.2.3  fvdp函数求解图</center>
图2.1.5展示的是原函数$y(x)$与一阶导数$y'(x)$的图像。从图像中可以看到,原函数$y(x)$呈现出一种振荡衰减的趋势,随着$x$的增加,$y(x)$的振幅逐渐减小,最终趋于稳定。这是因为二阶微分方程中的非线性项起到了阻尼作用,当y的绝对值接近$1$时,该项的值变小,从而减弱了$y$的增长速率,导致振荡的衰减。

同时,一阶导数$y'(x)$的图像显示出与原函数相似的振荡衰减模式,但相比之下,其变化更加剧烈。这是因为直接受到非线性阻尼项的影响,而$y$则是间接受到影响。

总的来说,这个微分方程组描述了一个非线性阻尼振荡系统,其解的行为随着初始条件和时间的变化而发生变化。在这个例子中,初始条件$y(0) = 0$和$y'(0) = 2$导致了一个振荡衰减的解,这种解在物理学和工程学中很常见,用于描述许多实际系统的动态行为。

我们再来看一个更高阶函数的求解的案例。

**例2.6** 使用scipy解下面这个高阶微分方程的数值解:
$$
\begin{align}
y''' + y'' - y' + y = \cos t, \quad y(0) = 0,~y'(0) = \pi,~y''(0) = 0 \tag{2.2.7}
\end{align}
$$

这个案例当然可以和上面一样如法炮制,输入`[y, y', y'']`返回`[y', y'', y''']`。这里再次介绍一个案例是想引出Python求微分方程数值解的另一个函数`solve_ivp`的用法。

首先,仍然是通过换元法对函数进行定义:

```python
def f(y,t):
    dy1 = y[1]
    dy2 = y[2]
    dy3 = -y[0]+dy1-dy2-np.cos(t)
    return [dy1,dy2,dy3]
```

`Solve_ivp`函数的用法与`odeint`非常类似,只不过比`odeint`多了两个参数。一个是`t_span`参数,表示自变量的取值范围;另一个是`method`参数,可以选择多种不同的数值求解算法。常见的内置方法包括`RK45`, `RK23`, `DOP853`, `Radau`, `BDF`等多种方法,通常使用`RK45`多一些。它的使用方法与`odeint`对比起来很类似,对这个问题进行代码实现如下:

```python
def solve_high_order_ode():
    '''
    求解高阶ODE
    '''
    t = np.linspace(0,6,1000)
    tspan = (0.0, 6.0)
    y0 = [0.0, pi, 0.0]
    # 初值[0,1,0]表示y(0)=0,y'(0)=1,y''(0)=0
    # 返回y, 其中y[:,0]是y[0]的值 ,就是最终解 ,y[:,1]是y'(x)的值
    y = odeint(f, y0, t)
    y_ = solve_ivp(f,t_span=tspan, y0=y0, t_eval=t)
    plt.subplot(211)
    l1, = plt.plot(t,y[:,0],label='y(0) Initial Function')
    l2, = plt.plot(t,y[:,1],label='y(1) The first order of Initial Function')
    l3, = plt.plot(t,y[:,2],label='y(2) The second order of Initial Function')
    plt.legend(handles=[l1,l2,l3])
    plt.grid('on')
    plt.subplot(212)
    l4, = plt.plot(y_.t, y_.y[0,:],'r--', label='y(0) Initial Function')
    l5,= plt.plot(y_.t,y_.y[1,:],'g--', label='y(1) The first order of Initial Function')
    l6, = plt.plot(y_.t,y_.y[2,:],'b-', label='y(2) The second order of Initial Function')
    plt.legend(handles=[l4,l5,l6]) # 显示图例
    plt.grid('on')
    plt.show()
solve_high_order_ode()
```

> solve_ivp 和 odeint 两个函数都可以解偏微分方程,但是solve_ivp可以不考虑参数顺序,odeint必须要考虑参数顺序(经验之谈)。感谢学习者提问&勘误。

这里通过`matplotlib.pyplot`中提供的绘图接口绘制了两个数值解的图像。由于没有设置`method`参数,这里默认`solve_ivp`使用`RK45`(4-5阶 Runge-Kutta 法)方法进行求解。所解得的结果如图所示:

![500](./attachments/Pasted%20image%2020240423225734.png)
<center>图2.2.4  Runge-Kutta 法求解图</center>
图中上半部分是使用`odeint`求解得到的结果,下半部分是由`solve_ivp`得到的结果,二者大差不差。一般来说对于普通的微分方程`odeint`与`solve_ivp`得到的结果差异不会太大,但有些情况下函数的微分容易发散,就会导致求解结果出现比较大的差异。`odeint`内置的原理也是4-5阶 Runge-Kutta 法,版本比较早所以求解也相对较为稳定。但`solve_ivp`则是后来新增的方法,有可能出现不太稳定的现象,内置方法较多所以也更加灵活。

Python求解微分方程组的模式有两种:一是采用基于基本原理自己写相关函数,这样操作比较繁琐,但是对于整个的求解过程会比较清晰明了;第二就是利用python下面的ode求解器,熟悉相关的输入输出,就可以完成数值求解。基于这个demo,在不同方向领域可以套用不同的微分方程组模型,进行仿真求解。但无论是常微分方程组还是偏微分方程组,使用的都是同一套思路,就是用差分代替微分。

**例2.7** 使用scipy解下面这个微分方程组的数值解:
$$
\left\{ 
\begin{align}
x'(t) &= - x^{3} - y, \\
y'(t) &= -y^{3} + x.
\end{align}
\right. 
$$

这个例子和例2.2很像,但不同的是这是也一个非线性方程组。那么,输入就需要以数组的形式传入`[x, y]`两个函数的函数值,返回它们的导数值。这里使用`solve_ivp`对这个方程组进行求解如下:

```python
def fun(t, w):
    x = w[0]
    y = w[1]
    return [-x**3-y,-y**3+x]
# 初始条件
y0 = [1,0.5]
yy = solve_ivp(fun, (0,100), y0, method='RK45',t_eval = np.arange(0,100,0.2) )
t = yy.t
data = yy.y
plt.plot(t, data[0, :])
plt.plot(t, data[1, :])
plt.xlabel("时间s")
plt.show()
```

绘制图像如图所示:

![500](./attachments/Pasted%20image%2020240423230056.png)
<center>图2.2.5  solve_ivp函数求解图</center>
可以看到二者处于相互干扰的震荡状态,振幅随着时间的推移逐渐收敛并趋于稳定。
如果在微分方程组里面还出现了高阶微分,我们又应该怎么做呢?来看下面这个例子:

**例2.8** 使用 scipy 解下面这个微分方程组的数值解:
$$
\left\{ 
\begin{align}
x''(t) + y'(t) + 3x(t) &= \cos(2t), \\[0.5em]
y''(t) - 4x'(t) + 3y(t) &= \sin(2t), \\[0.5em]
x'(0) = \frac{1}{5}, y'(0) = \frac{6}{5}, x(0) &= y(0) = 0.
\end{align}
\right. 
$$

这个例子就比较有趣了,在方程组里面涉及到了两个函数的导数怎么求解呢?本质上还是要使用换元法。完全可以令$u=x’$, $v=y’$,然后带入到方程组中把一个二元二阶微分方程组变为四元一阶微分方程组。代码实现如下所示:

```python
def fun(t, w):
    x = w[0]
    y = w[1]
    dx = w[2]
    dy = w[3]
    # 求导以后[x,y,dx,dy]变为[dx,dy,d2x,d2y]
    # d2x为w[2],d2y为w[5]
    return [dx,dy,-dy-3*x+np.cos(2*t),4*dx-3*y+np.sin(2*t)]
# 初始条件
y0 = [0,0,1/5,1/6]
yy = solve_ivp(fun, (0,100), y0, method='RK45',t_eval = np.arange(0,100,0.2) )
t = yy.t
data = yy.y
plt.figure(figsize=(12,6))
plt.subplot(1,2,1)
plt.plot(t, data[0, :])
plt.plot(t, data[1, :])
plt.legend(['x','y'])
plt.xlabel("时间s")
plt.subplot(1,2,2)
plt.plot(t, data[2, :])
plt.plot(t, data[3, :])
plt.legend(["x' ","y' "])
plt.xlabel("时间s")
plt.show()
```

得到的图像如图所示:

![600](./attachments/Pasted%20image%2020240423230519.png)
<center>图2.2.6  四元一阶方程求解图</center>
可以看到,图像呈现出一定的周期规律但并不是简谐运动。这样的方程解往往在物理学中有着实际意义,例如,这样的方程可以描述物体同时出现平动和摆动的过程中,位移-速度-加速度与角度-角速度-角加速度之间存在的关系。这样的例子曾出现在2022年全国大学生数学建模竞赛A题中,我们后面可以看到。
最后一个例子是对蝴蝶效应的求解。

例2.9 使用scipy求解洛伦兹系统的数值解,参数与初始值自设:
$$
\left\{
\begin{align}
\frac{\mathrm{d}x}{\mathrm{d}t} &= p(y - x), \\
\frac{\mathrm{d}x}{\mathrm{d}t} &= x(r - z), \\
\frac{\mathrm{d}z}{\mathrm{d}t} &= xy - bz,
\end{align}
\right. 
$$

有前面的例子作为经验,我们知道x y z都是函数其余的都是参数。利用solve_ivp很容易求解这个系统的代码:

```python
import numpy as np
from scipy.integrate import odeint
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
def dmove(Point, t, sets):
    p, r, b = sets
    x, y, z = Point
    return np.array([p*(y-x), x*(r-z), x*y-b*z])
t = np.arange(0, 30, 0.001)
P1 = odeint(dmove, (0., 1., 0.), t, args=([10., 28., 3.],))
P2 = odeint(dmove, (0., 1.01, 0.), t, args=([10., 28., 3.],)) 
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot(P1[:,0], P1[:,1], P1[:,2])
ax.plot(P2[:,0], P2[:,1], P2[:,2])
plt.show()
```

`Mpl_toolkits.mplot3d`提供了进行三维曲线、曲面绘制的函数,这里使用里面提供的三维坐标系绘制洛伦兹系统中点的运动轨迹。我们这里基于不同的初值绘制了两个点的轨迹`P1`和`P2`,并展示在图中:

![500](./attachments/Pasted%20image%2020240423231009.png)
<center>图2.2.7  洛伦兹系统中点的运动轨迹图</center>
可以看到,曲线的形状呈现双螺旋状,有些像蝴蝶的翅膀。所以洛伦兹系统又被叫做“蝴蝶效应”。蝴蝶效应本质上就是指,即使给这个系统的初始值一点微小的变化,曲线的形状也会出现很大不同。仅仅是把$y$改变了$0.01$,曲线的密集程度与蝴蝶翅膀的大小也是有所不同的。这是个混沌系统里面的典型案例。

## 2.3  偏微分方程的数值求解

偏微分方程是针对多元函数来说的,它在物理学中有着很深刻的现实意义。但是,偏微分方程往往比常微分方程更难求解,并且Python也没有提供偏微分方程的专用工具包。怎么求解这些偏微分方程呢?我们要始终把握一个思想:就是把连续的问题离散化。这一节会通过一系列的物理案例来看到Python如何求解一些典型的偏微分方程。

### 2.3.1  偏微分方程数值解的理论基础

偏微分方程实际上就是由多元函数、自变量与多元函数的偏导数及高阶偏导数之间构成的方程。它在工程中很多地方都有深刻应用,比如波动力学、热学、电磁学等。我们常研究的就是二元函数的二阶偏微分方程,其基本形式为:
$$
A \frac{ \partial^{2} f }{ \partial x^{2} } + 2B \frac{ \partial^{2} f }{ \partial x\partial y } + C \frac{ \partial^{2} f }{ \partial y^{2} } + D\frac{ \partial f }{ \partial x } + E \frac{ \partial f }{ \partial y } + Ff = 0. \tag{2.3.1}
$$
在方程中,如果$A$、$B$、$C$三个常系数不全为$0$,定义判别式$\Delta = B^{2} - 4AC$,当判别式大于$0$称其为双曲线式方程;若判别式等于$0$,则称其为抛物线式方程;若判别式小于$0$,则称其为椭圆式方程。有关于这几类方程的基本性质与边界条件等内容请感兴趣的读者自行参考偏微分方程领域的书籍,我们的主要目光更多的聚焦在它的应用上。

刚刚我们说到,二阶偏微分方程主要有三类:椭圆方程,抛物方程和双曲方程。双曲方程描述变量以一定速度沿某个方向传播,常用于描述振动与波动问题。椭圆方程描述变量以一定深度沿所有方向传播,常用于描述静电场、引力场等稳态问题。抛物方程描述变量沿下游传播,常用于描述热传导和扩散等瞬态问题。它们都在工程中有实际应用。

偏微分方程的定解问题通常很难求出解析解,只能通过数值计算方法对偏微分方程的近似求解。常用偏微分方程数值解法有包括有限差分方法、有限元方法、有限体方法、共轭梯度法,等等。在使用这些数值方法时通常先对问题的求解区域进行网格剖分,然后将定解问题离散为代数方程组,求出在离散网格点上的近似值。

偏微分方程的典型应用有很多。描述热源传热过程中温度变化的热传导方程本质上是一个抛物类微分方程。大名鼎鼎的韦东奕大神所着重研究的纳维-斯托克斯方程,所描述的是流体流速与流体密度、压力、外阻力之间的关系,在机械工程、能源工程等制造领域有着重要应用。还有电磁场中非常重要的麦克斯韦方程组,本质上也是偏微分方程。下面我们会根据一系列的具体应用来看如何去构建与求解微分方程。

### 2.3.2  偏微分方程数值解的应用案例

> 本节参考https://youcans.blog.csdn.net/article/details/119755450,特别鸣谢!

解偏微分方程由于缺少特定的工具包,更多的情况下需要自己写代码求解。这就迫使我们不得不理解微分方程求解的核心思想:以离散代替连续,用差分逼近微分。
一类有限差分法求解偏微分方程数值解的步骤包括如下几步:

1. 定义初始条件函数 $U(x,0)$;
2. 输入模型参数,对于待求解函数$F(x,t)$定义求解的时间域$(\mathrm{tStart}, \mathrm{tEnd})$和空间域$(\mathrm{xMin}, \mathrm{xMax})$,设置差分步长$\mathrm{d}t$, $\mathrm{nNodes}$;
3. 初始化解空间;
4. 根据递推规则递推求解差分方程在区间`[xa, xb]`的数值解,获得网格节点的处的函数值,最终填满整个解空间。

我们首先用一个常微分方程的应用揭示这一过程。

**例2.10** 使用偏微分方程对RC电路进行建模,分析电容放电过程中电量随时间的变化。
![400](./attachments/Pasted%20image%2020240423231638.png)

<center>图2.3.1  洛伦兹系统中点的运动轨迹图</center>
$$
\left\{ 
\begin{align}
IR + \frac{Q}{C} &= 0, \\
I &= \frac{\mathrm{d}Q}{\mathrm{d}t}, 
\end{align}
\right. \tag{2.3.2}
$$

式子中$I$代表了电流,$R$代表电阻值,$Q$代表电容电量,$C$代表电容值。为什么这个电路叫$RC$电路?这不仅仅是由于电路中包括电阻和电容两个核心元件,也是指$\tau = RC$的乘积是一个重要的参数。令,这个常微分方程存在很明显的解析解:
$$
Q(t) = Q_{0} e^{- t/\tau}. \tag{2.3.3}
$$
那么我们为什么还要来谈这个案例?我想通过这个案例来给大家讲述一下把一个连续问题离散化的方法。我们先从一阶微分方程的计算原理说起。将一阶微分方程离散化,实际上也就是把它写成迭代、差分的形式。对于一个连续的问题有
$$
\left\{  
\begin{align}
\frac{\mathrm{d}y}{\mathrm{d}t} &= f(y, t), \\[0.5em]
y_{0}(t_{0}) &= y_{0}.
\end{align}
\right. \tag{2.3.4}
$$
画出$y(t)$的图像如图所示:

![](./attachments/Pasted%20image%2020240423232711.png)
<center>图2.3.2  y(t)图象</center>
在图中,如果求解区间为$[x_0, x_0+h]$,已知点$(x_{0},f(x_{0}))$,要求解$f(x_0+h)$应该怎么做?根据微分方程$f(x,y)=0$可以很容易地解得每个点的导数值。在距离$h$很小的情况下,我们说,切线与割线是可以逼近的,也就可以用切线的对应值逼近函数值。但在上图中,我们发现:如果使用$x_0$点处的导数作为切线斜率,那么得到的估计值是比实际值要小的;但如果使用$x_0+h$点的导数作为斜率,那么估计值比实际值又大一些。能不能取一个折中的方案呢?很简单,把二者进行一个平均就可以了:
$$
y_{n+1} = y_{n} + \frac{f(y_{n}, t_{n}) + f(y_{n+1}, t_{n+1})}{2} h. \tag{2.3.5}
$$
那么对于例2.10中的电容放电过程,这个问题的代码可以这样写:

```python
import numpy as np
import matplotlib.pyplot as plt
rc = 2.0 #设置常数
dt = 0.5 #设置步长
n = 1000 #设置分割段数
t = 0.0 #设置初始时间
q = 1.0 #设置初始电量
#先定义三个空列表
qt=[] #用来盛放差分得到的q值
qt0=[] #用来盛放解析得到的q值
time = [] #用来盛放时间值
for i in range(n):
    t = t + dt
    q1 = q - q*dt/rc #qn+1的近似值
    q = q - 0.5*(q1*dt/rc + q*dt/rc) #差分递推关系
    q0 = np.exp(-t/rc) #解析关系
    qt.append(q) #差分得到的q值列表
    qt0.append(q0) #解析得到的q值列表
    time.append(t) #时间列表
plt.plot(time,qt,'o',label='Euler-Modify') #差分得到的电量随时间的变化
plt.plot(time,qt0,'r-',label='Analytical') #解析得到的电量随时间的变化
plt.xlabel('time')
plt.ylabel('charge')
plt.xlim(0,20)
plt.ylim(-0.2,1.0)
plt.legend(loc='upper right')
plt.show()
```

这个案例我们没有用任何包里面的微分方程求解器,纯手写的情况下解了这个微分方程。它的结果如图所示:

![500](./attachments/Pasted%20image%2020240423233132.png)
<center>图2.3.3  电容放电曲线</center>
这个方法被称为**Euler 法**,是在求解常微分方程中的一种常见数值方法。

**例2.11** 一维热传导方程是一个典型的抛物型二阶偏微分方程。设$u(x,t)$表示在时间$t$,空间$x$处的温度,则根据傅里叶定律(单位时间内流经单位面积的热量和该处温度的负梯度成正比),可以导出热传导方程:
$$
\frac{ \partial u }{ \partial t } = \lambda \frac{ \partial^{2} u }{ \partial x^{2} } 
$$
其中$\lambda$称为热扩散率,$k,C,p$分别为热导率,比热和质量密度,是由系统本身确定的常量。问题的形式为:
$$
\begin{align}
&\frac{ \partial u(x, t) }{ \partial t } = \frac{ \partial^{2} u(x,t) }{ \partial x^{2} } \\[0.5em]
& 0 \leqslant t \leqslant 1000, 0 \leqslant x \leqslant 3\\[0.5em]
& u(x,0) = 4x(3-x)\\[0.5em]
& u(0,t) = u(3, t) = 0\\[0.5em]
\end{align} \tag{2.3.7}
$$
请求解这个问题的数值解。

一元函数的微分方程可以绘制曲线,那么二元函数的偏微分方程应该就可以绘制曲面。那么,怎么对这个问题进行离散化呢?对于一个一般的二阶抛物型偏微分方程:
$$
\begin{align}
&\frac{ \partial u(x, t) }{ \partial t } = \frac{ \partial^{2} u(x,t) }{ \partial x^{2} } \\[0.5em]
& 0 \leqslant t \leqslant T, 0 \leqslant x \leqslant l\\[0.5em]
& u(x,0) = f(x)\\[0.5em]
& u(0,t) = g_{1}(t)\\[0.5em]
& u(3, t) = g_{2}(t)\\[0.5em]
\end{align} \tag{2.3.7}
$$
对时间和空间的一阶偏导数微分是容易离散化的:
$$
\begin{align}
\frac{ \partial u(x_{i}, t_{k}) }{ \partial t_{k} } &\implies \frac{u(x_{i}, t_{k+1}) - u(x_{i}, t_{k})}{\Delta t}, \\
\frac{ \partial u(x_{i}, t_{k}) }{ \partial x_{i} } &\implies \frac{u(x_{i+1}, t_{k}) - u(x_{i}, t_{k})}{\Delta x}. \\
\end{align}
$$
那么,对于空间的二阶微分,可以看作是一阶微分的再微分:
$$
\frac{ \partial^{2} u(x_{i}, t_{k}) }{ \partial x_{i}^{2} } \implies \frac{\displaystyle \frac{u(x_{i+1}, t_{k}) - u(x_{i}, t_{k})}{\Delta x} - \frac{u(x_{i}, t_{k}) - u(x_{i-1}, t_{k})}{\Delta x}}{\Delta x}. \tag{2.3.10}
$$
也就是$\displaystyle \frac{u(x_{i+1}, t_{k}) + u(x_{i-1}, t_{k}) - 2u(x_{i}, t_{k})}{(\Delta x)^{2}}$。

了解了这个原理,我们将例2.11中的边界条件和迭代规则进行翻译如下:

```python
import numpy as np
import matplotlib.pyplot as plt

h = 0.1#空间步长
N =30#空间步数
dt = 0.0001#时间步长
M = 10000#时间的步数
A = dt/(h**2) #lambda*tau/h^2
U = np.zeros([N+1,M+1])#建立二维空数组
Space = np.arange(0,(N+1)*h,h)#建立空间等差数列,从0到3,公差是h
#边界条件
for k in np.arange(0,M+1):
    U[0,k] = 0.0
    U[N,k] = 0.0
#初始条件
for i in np.arange(0,N):
    U[i,0]=4*i*h*(3-i*h)
#递推关系
for k in np.arange(0,M):
    for i in np.arange(1,N):
        U[i,k+1]=A*U[i+1,k]+(1-2*A)*U[i,k]+A*U[i-1,k]
```

将解空间抽象为以时间为横坐标、空间为纵坐标的网格,翻译时间与空间的边界条件,对于网格内每个点使用二重循环遍历每个点,根据差分后的迭代方程进行演化。不同时刻的温度随空间坐标的变化图像如下:

```python
plt.plot(Space,U[:,0], 'g-', label='t=0',linewidth=1.0)
plt.plot(Space,U[:,3000], 'b-', label='t=3/10',linewidth=1.0)
plt.plot(Space,U[:,6000], 'k-', label='t=6/10',linewidth=1.0)
plt.plot(Space,U[:,9000], 'r-', label='t=9/10',linewidth=1.0)
plt.plot(Space,U[:,10000], 'y-', label='t=1',linewidth=1.0)
plt.ylabel('u(x,t)', fontsize=20)
plt.xlabel('x', fontsize=20)
plt.xlim(0,3)
plt.ylim(-2,10)
plt.legend(loc='upper right')
plt.show()
```

![500](./attachments/Pasted%20image%2020240423234214.png)
<center>图2.3.4  温度稳态分布图</center>
在图中可以看到,随着时间的推进,温度分布呈现出一种动态变化的过程。在$t = 0$时(绿线),我们看到起始的温度分布情况;随着时间的推移,图中的曲线显示出温度在不同位置的变化。

* 在 $\displaystyle t = \frac{3}{10}$(蓝线)时,曲线稍微上升,表明温度在增加。
* 到了 $\displaystyle t=\frac{6}{10}$(黑线),温度继续上升,但上升的速度似乎开始减缓。
* $\displaystyle t = \frac{9}{10}$(红线)时,曲线达到高点之后开始回落,这可能意味着一个冷却过程的开始。
* 到 $t = 1$(黄线)时,温度分布与时相比$t=0$明显增高,但比$\displaystyle t = \frac{9}{10}$时有所降低,显示了经过一段时间后温度分布的稳定状态。

整体上,这些曲线描述了温度如何随着时间从初始状态演变到一个稳态分布。这种分析对于理解热传导、扩散过程,以及如何在时间上控制温度分布都是非常有用的。图形的$\mathrm{y}$轴显示的是$u(x,t)$,即位置$x$在时间$t$的温度,而$\mathrm{x}$轴表示空间坐标。曲线下方较深的颜色表示较低的温度值,而曲线顶部较浅的颜色表示较高的温度值。通过设置坐标轴的范围和图例的位置,这张图为观察者提供了清晰的数据解读。

图中的$\mathrm{y}$轴标记为 ,表示在位置$x$和时间$t$的温度,$\mathrm{x}$轴标记为$x$表示空间坐标。可视窗口的设置为$x$值从$0$到$3$,$u(x,t)$的值从$-2$到$10$。

将整个网格空间的温度分布热力图绘制如图所示:

```python
#温度等高线随时空坐标的变化,温度越高,颜色越偏红
extent = [0,1,0,3] #时间和空间的取值范围
levels = np.arange(0,10,0.1)#温度等高线的变化范围0-10,变化间隔为0.1
plt.contourf(U,levels,origin='lower',extent=extent,cmap=plt.cm.jet)
plt.ylabel('x', fontsize=20)
plt.xlabel('t', fontsize=20)
plt.show()
```

![500](./attachments/Pasted%20image%2020240423234553.png)
<center>图2.3.5  温度分布热力图</center>
从图中可以看到的是一个温度分布的热力图,其中颜色的变化表明了不同温度的区域。温度越高,颜色越偏向红色,温度较低的区域则显现为蓝色。这种热力图通常用于显示温度如何在空间内分布以及如何随时间变化。
图中的$\mathrm{y}$轴(标记为$x$)代表空间坐标,而$\mathrm{x}$轴(标记为$t$)代表时间。热力图覆盖的范围是时间从 $0$ 到 $1$,空间从 $0$ 到 $3$。可以清晰地看到,在图的左侧(时间较早)温度整体较低,而在图的右侧(时间较晚)温度较高,这表示随着时间的推移,整体温度有所上升。

等温线的密集区表示温度变化较大的区域,而等温线的稀疏区则表示温度变化较小的区域。从热力图中我们可以推断,最高温区域集中在图的右上角,而最低温区域则在左下角。通过色彩的深浅变化,我们可以直观地看到温度在空间中如何变化以及时间对这种分布的影响。

**例2.12** 平流过程是大气运动中重要的过程。平流方程(Advection equation)描述某一物理量的平流作用而引起局地变化的物理过程,最简单的形式是一维平流方程。
$$
\begin{align}
& \frac{ \partial u }{ \partial t } + v \frac{ \partial u }{ \partial x } = 0,\\[0.5em]
& u(x,0) = F(x).
\end{align} \tag{2.3.11}
$$
式中$u$为某物理量,$v$为系统速度,$x$为水平方向分量,$t$为时间。该方程可以求得解析解:
$$
u(x,t) = F(x-vt). \tag{2.3.12}
$$
考虑一维线性平流偏微分方程的数值解法,采用有限差分法求解。简单地, 采用一阶迎风格式的差分方法(First-order Upwind),一阶导数的差分表达式为:
$$
\begin{align}
\left. \frac{ \partial u }{ \partial x }  \right|_{i,j} &\implies \frac{u_{i+1,j} - u_{i,j}}{\Delta x}, \\[0.5em]
u_{i,j+1} &= u_{i,j} - v\frac{\Delta t}{\Delta x}\big( u_{i,j} - u_{i-1,j} \big).
\end{align} \tag{2.3.13}
$$
代码实现如下:

```python
# eg.3. 
import numpy as np
import matplotlib.pyplot as plt
# 初始条件函数 U(x,0)
def funcUx_0(x, p): 
    u0 = np.sin(2 * (x-p)**2)
    return u0
# 输入参数
v1 = 1.0  # 平流方程参数,系统速度
p = 0.25  # 初始条件函数 u(x,0) 中的参数
tc = 0  # 开始时间
te = 1.0  # 终止时间: (0, te)
xa = 0.0  # 空间范围: (xa, xb)
xb = np.pi
dt = 0.02  # 时间差分步长
nNodes = 100  # 空间网格数
# 初始化
nsteps = round(te/dt)
dx = (xb - xa) / nNodes
x = np.arange(xa-dx, xb+2*dx, dx)
ux_0 = funcUx_0(x, p)
u = ux_0.copy()  # u(j)
ujp = ux_0.copy()  # u(j+1)
# 时域差分
for i in range(nsteps):
    plt.clf()  # 清除当前 figure 的所有axes, 但是保留当前窗口
    # 计算 u(j+1)
    for j in range(nNodes + 2):
        ujp[j] = u[j] - (v1 * dt/dx) * (u[j] - u[j-1])
    # 更新边界条件
    u = ujp.copy()
    u[0] = u[nNodes + 1]
    u[nNodes+2] = u[1]
    tc += dt
    # 绘图
plt.plot(x, u, 'b-', label="v1= 1.0")
plt.axis((xa-0.1, xb + 0.1, -1.1, 1.1))
plt.xlabel("x")
plt.ylabel("U(x)")
plt.legend(loc=(0.05,0.05))
plt.show()
```

![500](./attachments/Pasted%20image%2020240423235312.png)
<center>图2.3.6  平流大气运动图</center>
从图中可以发现,函数$U(x)$显示了随空间$x$变化的波动性质。这个波动可能代表一维平流方程在某一特定时间$t$的数值解。图中蓝色的线表示速度$v_{1} = 1.0$下的解,而波形的变化暗示了初态条件$U(x,0) = \sin \big[ 2(x-p)^{2} \big]$随时间的演化。

注意到曲线在$\mathrm{x}$轴的不同位置出现了波峰和波谷,这表明了函数值随位置的变化并非均匀。由于是一阶迎风格式的数值解法,我们可能会观察到与理论解相比有一定程度的数值扩散或者数值耗散,这是由于一阶方法在数值传输过程中的固有特性。

在这个示例中,曲线的形状可能表示了经过一段时间演化后,平流作用在初始条件$U(x,0)$上的效果。如果初始波形移动的速度是$v_{1}$,那么这个图形可能表明波形随着时间的推进而向右移动。然而,由于是一阶迎风差分,我们也可以预期波形会有一定程度的变形,这在实际中表现为波峰变得不那么尖锐,以及波形整体变得更加平坦。这是数值方法的离散化误差导致的结果。

这个数值解可以帮助理解平流方程在数值模拟中的行为,特别是当理论解难以获得时,数值方法提供了一种有效的途径来近似解决实际问题。

**例2.13** 波动方程(wave equation)是典型的双曲偏微分方程,广泛应用于声学,电磁学,和流体力学等领域,描述自然界中的各种的波动现象,包括横波和纵波,例如声波、光波和水波。考虑如下二维波动方程的初边值问题:
$$
\begin{align}
\frac{ \partial^{2} u }{ \partial t^{2} } &= c^{2} \left( \frac{ \partial^{2} u }{ \partial x^{2} } + \frac{ \partial^{2} u }{ \partial y^{2} }  \right), \\[0.5em]
\frac{ \partial u(0,x,y) }{ \partial t } &= 0,\\[0.5em]
u(x,y,0) &= u_{0}(x,y),\\[0.5em]
u(0,y,t) &= u_{a}(t), \quad u(1,y,t) = u_{b}(t),\\[0.5em]
u(x,0,t) &= u_{c}(t), \quad u(x,1,t) = u_{d}(t),\\[0.5em]
\end{align}\tag{2.3.14}
$$
式中:$u$ 是振幅;$c$ 为波的传播速率,$c$ 可以是固定常数,或位置的函数 $c(x,y)$,也可以是振幅的函数 $c(u)$。

考虑二维波动偏微分方程的数值解法,采用有限差分法求解。简单地, 采用迎风法的三点差分格式, 将上述的偏微分方程离散为差分方程:
$$
\begin{align}
&\frac{u_{i,j,k} + u_{i,j,k+1} - 2u_{i,j,k}}{(\Delta t)^{2}} \\&= c^{2} \left[ \frac{u_{i-1,j,k} + u_{i+1,j,k} - 2u_{i,j,k}}{(\Delta x)^{2}} + \frac{u_{i,j-1,k} + u_{i,j+1,k} - 2u_{i,j,k}}{(\Delta y)^{2}} \right]. \tag{2.3.15} 
\end{align}
$$
可以得到迭代规则为:
$$
\begin{align}
r_{x} &= c^{2} \frac{(\Delta t)^{2}}{(\Delta x)^{2}}, \\[0.5em]
r_{y} &= c^{2} \frac{(\Delta t)^{2}}{(\Delta y)^{2}}, \\[0.5em]
u_{i,j,k+1} &= r_{x}\big( u_{i-1,j,k} + u_{i+1,j,k} \big) \\ &+ 2\big( 1 - r_{x} - r_{y} \big) u_{i,j,k} + r_{y} \big( u_{i,j-1,k} + u_{i,j+1,k} \big) - u_{i,j,k-1}.
\end{align} \tag{2.3.16}
$$
它的代码实现如下:

```python
import numpy as np
import matplotlib.pyplot as plt
# 模型参数
c = 1.0  # 波的传播速率
tc, te = 0.0, 1.0  # 时间范围,0<t<te
xa, xb = 0.0, 1.0  # 空间范围,xa<x<xb
ya, yb = 0.0, 1.0  # 空间范围,ya<y<yb
# 初始化
c2 = c*c  # 方程参数
dt = 0.01  # 时间步长
dx = dy = 0.02  # 空间步长
tNodes = round(te/dt)  # t轴 时序网格数
xNodes = round((xb-xa)/dx)  # $\mathrm{x}$轴 空间网格数
yNodes = round((yb-ya)/dy)  # $\mathrm{y}$轴 空间网格数
tZone = np.arange(0, (tNodes+1)*dt, dt)  # 建立空间网格
xZone = np.arange(0, (xNodes+1)*dx, dx)  # 建立空间网格
yZone = np.arange(0, (yNodes+1)*dy, dy)  # 建立空间网格
xx, yy = np.meshgrid(xZone, yZone)  # 生成网格点的坐标 xx,yy (二维数组)
# 步长比检验(r>1 则算法不稳定)
r = 4 * c2 * dt*dt / (dx*dx+dy*dy)
print("dt = {:.2f}, dx = {:.2f}, dy = {:.2f}, r = {:.2f}".format(dt,dx,dy,r))
assert r < 1.0, "Error: r>1, unstable step ratio of dt2/(dx2+dy2) ."
rx = c*c * dt**2/dx**2
ry = c*c * dt**2/dy**2
# 绘图
fig = plt.figure(figsize=(8,6))
ax1 = fig.add_subplot(111, projection='3d')
# 计算初始值
U = np.zeros([tNodes+1, xNodes+1, yNodes+1])  # 建立三维数组
U[0] = np.sin(6*np.pi*xx)+np.cos(4*np.pi*yy)  # U[0,:,:]
U[1] = np.sin(6*np.pi*xx)+np.cos(4*np.pi*yy)  # U[1,:,:]
surf = ax1.plot_surface(xx, yy, U[0,:,:], rstride=2, cstride=2, cmap=plt.cm.coolwarm)
# 有限差分法求解
for k in range(2,tNodes+1):
    for i in range(1,xNodes):
        for j in range(1,yNodes):
            U[k,i,j] = rx*(U[k-1,i-1,j]+U[k-1,i+1,j]) + ry*(U[k-1,i,j-1]+U[k-1,i,j+1])\
                     + 2*(1-rx-ry)*U[k-1,i,j] -U[k-2,i,j]
surf = ax1.plot_surface(xx, yy, U[k,:,:], rstride=2, cstride=2, cmap='rainbow')
ax1.set_xlim3d(0, 1.0)
ax1.set_ylim3d(0, 1.0)
ax1.set_zlim3d(-2, 2)
ax1.set_title("2D wave equationt (t= %.2f)" % (k*dt))
ax1.set_xlabel("x")
ax1.set_ylabel("y")
plt.show()
```

这个函数是一个三元函数,实际上是可以做出一个曲面随时间变化的动画的。大家可以尝试使用matplotlib提供的动画功能进行绘制,这里展示其中一个瞬时状态:

![500](./attachments/Pasted%20image%2020240424000918.png)
<center>图2.3.7  波形方程求解图</center>
**例2.14** 热传导方程(heat equation)是典型的抛物形偏微分方程,也成为扩散方程。广泛应用于声学,电磁学,和流体力学等领域,描述自然界中的各种的波动现象,包括横波和纵波,例如声波、光波和水波。之前的例2.11我们已经看到了一维热传导方程的求解,现在考虑如下二维热传导方程的初边值问题:
$$
\begin{align}
\frac{ \partial u }{ \partial t } &= \lambda \left( \frac{ \partial^{2} u }{ \partial x^{2} + \frac{ \partial^{2} u }{ \partial y^{2} } }  \right) + q_{v}\\[0.5em]
\frac{ \partial u(0, x, y) }{ \partial t } &= 0\\[0.5em]
u(x,y,0) &= u_{0}(x,y),\\[0.5em]
u(0,y,t) &= u_{a}(t), \quad u(1,y,t) = u_{b}(t),\\[0.5em]
u(x,0,t) &= u_{c}(t), \quad u(x,1,t) = u_{d}(t).\\[0.5em]
\end{align} \tag{2.3.17}
$$
类似的,同上一个例子中的有限差分法,我们将这个方程离散化可以得到:
$$
\begin{align}
r_{x} &= \lambda^{2} \frac{(\Delta t)^{2}}{(\Delta x)^{2}}, \\[0.5em]
r_{y} &= \lambda^{2} \frac{(\Delta t)^{2}}{(\Delta y)^{2}}, \\[0.5em]
u_{i,j,k+1} &= r_{x}\big( u_{i-1,j,k} + u_{i+1,j,k} \big) \\ &+ 2\big( 1 - r_{x} - r_{y} \big) u_{i,j,k} + r_{y} \big( u_{i,j-1,k} + u_{i,j+1,k} \big) - u_{i,j,k-1}.
\end{align} \tag{2.3.18}
$$
事实上,这个方程还有一种矩阵形式:
$$
U_{k+1} = U_{k} + r_{x}AU_{k} + r_{y}BU_{k} + q_{v}\Delta t, \tag{2.3.19}
$$
其中
$$
A = \left[ \begin{matrix}
-2 & -1 & 0 & \cdots & 0 & 0\\
1 & -2 & 1 & \cdots & 0 & 0\\
0 & 1 & -2 & \cdots & 0 & 0\\
\vdots & \vdots & \vdots & \ddots & \vdots & \vdots\\
0 & 0 & 0 & \cdots & 1 & -2
\end{matrix} \right]_{N_{x} \times N_{x}}, \quad
B = \left[ \begin{matrix}
-2 & -1 & 0 & \cdots & 0 & 0\\
1 & -2 & 1 & \cdots & 0 & 0\\
0 & 1 & -2 & \cdots & 0 & 0\\
\vdots & \vdots & \vdots & \ddots & \vdots & \vdots\\
0 & 0 & 0 & \cdots & 1 & -2
\end{matrix} \right]_{N_{y} \times N_{y}}. \tag{2.3.20}
$$
我们这次就用这个矩阵形式去进行偏微分方程的求解:

```python
import numpy as np
import matplotlib.pyplot as plt
def showcontourf(zMat, xyRange, tNow):  # 绘制等温云图
    x = np.linspace(xyRange[0], xyRange[1], zMat.shape[1])
    y = np.linspace(xyRange[2], xyRange[3], zMat.shape[0])
    xx,yy = np.meshgrid(x,y)
    zMax = np.max(zMat)
    yMax, xMax = np.where(zMat==zMax)[0][0], np.where(zMat==zMax)[1][0]
    levels = np.arange(0,100,1)
    showText = "time = {:.1f} s\nhotpoint = {:.1f} C".format(tNow, zMax)
    plt.plot(x[xMax],y[yMax],'ro')  # 绘制最高温度点
    plt.contourf(xx, yy, zMat, 100, cmap=plt.cm.get_cmap('jet'), origin='lower', levels = levels)
    plt.annotate(showText, xy=(x[xMax],y[yMax]), xytext=(x[xMax],y[yMax]),fontsize=10)
    plt.colorbar()
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title('Temperature distribution of the plate')
    plt.show()
# 模型参数
uIni = 25  # 初始温度值
uBound = 25.0  # 边界条件
c = 1.0  # 热传导参数
qv = 50.0  # 热源功率
x_0, y0 = 0.0, 3.0  # 热源初始位置
vx, vy = 2.0, 1.0  # 热源移动速度
# 求解范围
tc, te = 0.0, 5.0  # 时间范围,0<t<te
xa, xb = 0.0, 16.0  # 空间范围,xa<x<xb
ya, yb = 0.0, 12.0  # 空间范围,ya<y<yb
# 初始化
dt = 0.002  # 时间步长
dx = dy = 0.1  # 空间步长
tNodes = round(te/dt)  # t轴 时序网格数
xNodes = round((xb-xa)/dx)  # $\mathrm{x}$轴 空间网格数
yNodes = round((yb-ya)/dy)  # $\mathrm{y}$轴 空间网格数
xyRange = np.array([xa, xb, ya, yb])
xZone = np.linspace(xa, xb, xNodes+1)  # 建立空间网格
yZone = np.linspace(ya, yb, yNodes+1)  # 建立空间网格
xx,yy = np.meshgrid(xZone, yZone)  # 生成网格点的坐标 xx,yy (二维数组)
# 计算 差分系数矩阵 A、B (三对角对称矩阵),差分系数 rx,ry,ft
A = (-2) * np.eye(xNodes+1, k=0) + (1) * np.eye(xNodes+1, k=-1) + (1) * np.eye(xNodes+1, k=1)
B = (-2) * np.eye(yNodes+1, k=0) + (1) * np.eye(yNodes+1, k=-1) + (1) * np.eye(yNodes+1, k=1)
rx, ry, ft = c*dt/(dx*dx), c*dt/(dy*dy), qv*dt
# 计算 初始值
U = uIni * np.ones((yNodes+1, xNodes+1))  # 初始温度 u0
# 前向Euler 法一阶差分求解
for k in range(tNodes+1):
    t = k * dt  # 当前时间
    # 热源条件
    # (1) 恒定热源:Qv(x_0,y0,t) = qv, 功率、位置 恒定
    # Qv = qv
    # (2) 热源功率随时间变化 Qv(x_0,y0,t)=f(t)
    # Qv = qv*np.sin(t*np.pi) if t<2.0 else qv
    # (3) 热源位置随时间变化 Qv(x,y,t)=f(x(t),y(t))
    xt, yt = x_0+vx*t, y0+vy*t  # 热源位置变化
    Qv = qv * np.exp(-((xx-xt)**2+(yy-yt)**2))  # 热源方程
    # 边界条件
    U[:,0] = U[:,-1] = uBound
    U[0,:] = U[-1,:] = uBound
    # 差分求解
    U = U + rx * np.dot(U,A) + ry * np.dot(B,U) + Qv*dt
    if k % 100 == 0:
        print('t={:.2f}s\tTmax={:.1f}  Tmin={:.1f}'.format(t, np.max(U), np.min(U)))
showcontourf(U, xyRange, k*dt)  # 绘制等温云图
```

这段代码只需要把`showcontourf`放在循环体内即可实现动画效果,可以清晰看到热源在空间中的分布。我这里展示最后状态下的温度分布图:

![500](./attachments/Pasted%20image%2020240424222139.png)
<center>图2.3.8  温度空间分布图</center>
最终状态下的温度分布图显示了热源随时间在平板中移动的情况,并将热量传递给周围材料。图像中的红点标记了最高温度点,即‘热点’,对应于最后时间步骤中热源的当前位置。颜色渐变代表温度分布,红色是最热的区域,蓝色是最冷的。等温线表示温度相等的水平线。

热点周围的温度梯度平滑,这表明热量通过平板均匀扩散。这样的模拟在许多应用中都很有用,例如在散热器设计、理解制造过程中的热梯度,甚至在诸如地热源传播热量的自然现象中。

**例2.15** 椭圆偏微分方程是一类重要的偏微分方程,主要用来描述物理的平衡稳定状态,如定常状态下的电磁场、引力场和反应扩散现象等,广泛应用于流体力学、弹性力学、电磁学、几何学和变分法中。
考虑如下二维 Poisson 方程:
$$
\frac{ \partial^{2} u }{ \partial x^{2} } + \frac{ \partial^{2} u }{ \partial y^{2} } = f(x,y), \tag{2.3.21}
$$
这个方程怎么解呢?考虑二维椭圆偏微分方程的数值解法,采用有限差分法求解。简单地,采用五点差分格式表示二阶导数的差分表达式,将上述的偏微分方程离散为差分方程:
$$
\frac{u_{i-1,j}+u_{i+1,j} - 2u_{i,j}}{(\Delta x)^{2}} + \frac{u_{i,j-1}+u_{i,j+1} - 2u_{i,j}}{(\Delta y)^{2}} = f(x_{i}, y_{j}), \tag{2.3.22}
$$
椭圆型偏微分描述不随时间变化的均衡状态,没有初始条件,因此不能沿时间步长递推求解。对上式的差分方程,可以通过矩阵求逆方法求解,但当$h$较小时网格很多,矩阵求逆的内存占用和计算量极大。于是,可以使用迭代松弛法递推求得二维椭圆方程的数值解。假定$x$和$y$的间距都为$h$,松弛系数为$w$,则迭代过程可以表示为:
$$
u_{i,j}^{k+1} \leftarrow (1-w)u_{i,j}^{k} + \frac{w}{4} \Big[ u_{i,j+1}^{k} + u_{i,j-1}^{k} + u_{i+1,j}^{k} + u_{i-1,j}^{k} - h^{2}f(x_{i},x_{j}) \Big], \tag{2.3.23}
$$
考虑一个特殊情况,也就是当$f(x,y)=0$的情况下,迭代松弛法的代码如下:

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 求解范围
xa, xb = 0.0, 1.0  # 空间范围,xa<x<xb
ya, yb = 0.0, 1.0  # 空间范围,ya<y<yb
# 初始化
h = 0.01  # 空间步长, dx = dy = 0.01
w = 0.5  # 松弛因子
nodes = round((xb-xa)/h)  # $\mathrm{x}$轴 空间网格数
# 边值条件
u = np.zeros((nodes+1, nodes+1))
for i in range(nodes+1):
    u[i, 0] = 1.0 + np.sin(0.5*(i-50)/np.pi)
    u[i, -1] = -1.0 + 0.5*np.sin((i-50)/np.pi)
    u[0, i] = -1.0 + 0.5*np.sin((i-50)/np.pi)
    u[-1, i] = 1.0 + np.sin(0.5*(50-i)/np.pi)
# 迭代松弛法求解
for iter in range(100):
    for i in range(1, nodes):
        for j in range(1, nodes):
            u[i, j] = w/4 * (u[i-1, j] + u[i+1, j] + u[i, j-1] + u[i, j+1]) + (1-w) * u[i, j]
# 绘图
x = np.linspace(0, 1, nodes+1)
y = np.linspace(0, 1, nodes+1)
xx, yy = np.meshgrid(x, y)
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(xx, yy, u, cmap=plt.get_cmap('rainbow'))
fig.colorbar(surf, shrink=0.5)
ax.set_xlim3d(0, 1.0)
ax.set_ylim3d(0, 1.0)
ax.set_zlim3d(-2, 2.5)
ax.set_title("2D elliptic partial differential equation")
ax.set_xlabel("X")
ax.set_ylabel("Y")
plt.show()
```

![500](./attachments/Pasted%20image%2020240424222706.png)
<center>图2.3.9  二维椭圆偏微分方程求解图</center>
从图中可以看到,解呈现出一系列波峰和波谷,这与边界条件的正弦函数有关。每个波峰和波谷都是颜色映射中的热点和冷点,分别代表着方程解的局部最大值和最小值。波峰出现在和轴的中间位置,而波谷则出现在四个角和边缘。颜色的变化代表了解的幅度,从红色的高值到蓝色的低值。整体上,解的形状表现出了椭圆方程特有的对称性和周期性。

sympy中的`pdsolve`给出了一些简单偏微分方程的解析解法,但sympy目前不支持二阶偏微分方程的求解。`pdsolve`的调用格式形如`pdsolve(eq, func=None, hint='default', dict=False, solvefun=None, **kwargs)`,具体使用我们可以看到下面的例子:

**例2.16** 使用sympy中的`pdsolve`解下面这个偏微分方程:
$$
-2 \frac{ \partial f }{ \partial x } + 4 \frac{ \partial f }{ \partial y } + 5f = e^{x+3y}, \tag{2.3.24}
$$
可以给出如下代码:

```python
from sympy.solvers.pde import pdsolve
from sympy import Function, pprint, exp
from sympy.abc import x,y
f = Function('f')
eq = -2*f(x,y).diff(x) + 4*f(x,y).diff(y) + 5*f(x,y) - exp(x + 3*y)
pdsolve(eq)
```

给出的结果为:

```python
Eq(f(x, y), (F(4*x + 2*y)*exp(x/2) + exp(x + 4*y)/15)*exp(-y))
```

![500](./attachments/Pasted%20image%2020240424222941.png)
<center>图2.3.10  pdsolve求解方程图</center>
## 2.4  微分方程的应用案例

在本节中,我们将探讨微分方程在现实世界中的应用,特别是在工业和日常生活中的一些实例。通过这些案例,我们可以加深对微分方程建模过程的理解。

### 2.4.1  人口增长的两种经典模型—— Malthus 模型和 Logistic 模型

人口增长是微分方程建模的一个经典例子。我们可能在高中生物课上已经接触过指数增长(J型曲线)和 Logistic 增长(S型曲线),但可能没有深入了解它们的计算方法和曲线的具体特性。实际上,这些曲线可以通过微分方程来描述。不仅生物种群的增长遵循这些规律,人口增长也是如此。

最简单的模型是 Malthus 模型,它假定人口增长率是一个恒定的常数,因此每年的人口增加量与当年的人口总数成正比:
$$
\frac{\mathrm{d}x}{\mathrm{d}t} = rx, \quad x(t_{0}) = x_{0}. \tag{2.4.1}
$$
即使不用Python,我们也可以轻松手动求解这个微分方程:
$$
x(t) = x_{0}e^{r(t-t_{0})}, \tag{2.4.2}
$$
其中,增长率$r$是一个恒定的值。这个模型的假设包括:

1. 忽略死亡率对人口增长的影响,只考虑净增长率
2. 忽略人口迁移对问题的影响,只关注自然增长
3. 忽略重大突发事件对人口的突变性影响
4. 忽略人口增长率变化的滞后性

然而,仔细想一想,无论是人还是动物,增长率怎么可能持续不变地指数增长呢?如果一直稳步增长,地球很快就会人满为患。实际上,增长率会受到许多外部因素的影响,比如食物供应、天敌、疾病等突发事件,甚至种群内部的竞争也会降低增长率。那么,这个模型是否完全没有解释价值呢?并非如此,至少在没有天敌、疾病且食物充足的时期,物种的增长确实接近指数增长。一个典型的例子是澳大利亚的野兔。

因此,我们可以对增长率进行一些调整:假设某个地区的人口存在一个最大承载量$x_{m}$(也就是我们以前接触到的$K$值),随着人口增长,增长率呈线性下降。这样,原始的 Malthus 模型就被修正为:
$$
\frac{\mathrm{d}x}{\mathrm{d}t} = r \left( 1 - \frac{x}{x_{m}} \right), \quad x(t_{0}) = x_{0}. \tag{2.4.3}
$$
该模型被称为 Logistic 模型。事实上,这个方程可以求解出解析解,并且如果绘制它的曲线,会呈现出S形。方程的解析解为:
$$
x(t) = \frac{x_{m}}{\displaystyle 1 + \left( \frac{x_{m}}{x_{0}} - 1 \right) e^{-r(t-t_{0})}}. \tag{2.4.4}
$$

> 注意:这两个模型都是人口预测中的经典模型,需要重点掌握。然而,这两个模型都是从宏观角度出发,没有考虑诸如人口结构、人口迁移等更微观的因素。此外,这些模型对数据量的要求并不高,如果数据量太大,可能需要使用时间序列方法进行分析。

接下来,我们可以通过一个例子来进一步了解这些模型。

**例3.7** 假设我们有某地1980年至2010年间的人口变化数据,我们想比较 Malthus 模型和 Logistic 模型对人口变化的拟合效果,并预测2011年的人口数据。假设数据已经存储在文件中,我们可以将其读取并保存到变量中进行分析。

```python
import pandas as pd
import numpy as np
from scipy import optimize
data=pd.read_excel("总人口.xls")
data1=data[['年份','年末总人口(万人)']].values
y=data['年末总人口(万人)'].values.astype(float)
x_0=y[0]
#Logistic
def f2(x,r,K):
    return K/(1+(K/x_0-1)*np.exp(-r*x))
#Malthus
def f(x,r):
    return x_0*np.exp(r*x)
x=np.arange(0,30,1)
x_00=np.arange(1990,2020,1)
plt.figure(figsize=(16,9))
plt.grid()
fita,fitb=optimize.curve_fit(f,x,y)
print(fita)
plt.plot(x_00,10000*y,'.')
x1=f(x,fita[0])
plt.plot(x_00,10000*x1,'r*-')
fita,fitb=optimize.curve_fit(f2,x,y)
print(fita)
x1=f2(x,0.0578,11019)
plt.plot(x_00,10000*x1,'b^-')
plt.legend(["Origin","Malthus","Logistic"])
plt.xlabel("Year")
plt.ylabel("Population")
plt.title('Population Prediction')
plt.show()
```

最终可以画出模型的拟合图,如图2.4.1所示。

![600](./attachments/Pasted%20image%2020240424223554.png)
<center>图2.4.1   Logistic 模型拟合效果图</center>
从图2.4.1中可以看出,与 Malthus 模型相比, Logistic 模型的拟合效果更接近实际数据。当然,模型的参数或形式可能有更好的选择,但在这里,我们可以认为该模型已经足够接近实际情况,可以用于人口预测。在代码中,我们可以看到, Logistic 模型中测试的最大人口为11000人,人口增长率的初始值为0.001。将这些值代入公式,我们可以用它来预测2011年的人口。

### 2.4.2  波浪能受力系统

本节内容改编自2022年全国大学生数学建模竞赛A题。

随着社会的进步和经济的增长,对绿色能源技术的需求日益增加。波浪能,作为一种清洁的可再生能源,具有巨大的潜力和广阔的发展前景。波浪能的有效利用是能源技术研究的重要方向之一。一个典型的波浪能装置由浮标、振荡器、中心轴和能量转换系统(PTO,包括弹簧和阻尼器)组成。在这种装置中,振荡器、中心轴和PTO被封装在浮标内部。当浮标受到周期性波浪的作用时,它会受到波浪激励力、附加质量力、波浪阻尼力和静水复原力等多种力的影响。

![500](./attachments/Pasted%20image%2020240424223648.png)
<center>图2.4.2  波浪能受力系统图</center>
我们需要解决以下问题,以了解系统的动力学行为及其参数:
1. 假设浮标和振荡器仅在垂直方向上进行一维振动。计算在波浪激励力作用下,前40个波浪周期内,每隔0.2秒浮标和振荡器的垂直位移和速度。
2. 若考虑浮标和振荡器不仅有一维振动,还有平面内的摆动。此时,波浪的输入除了提供振动的波浪激励力外,还会提供一个用于摆动的波浪激励力矩。此外,除了直线阻尼器,转轴上还安装有旋转阻尼器和扭转弹簧,这些元件共同输出能量。计算在波浪激励力和波浪激励力矩作用下,前40个波浪周期内,每隔0.2秒浮标与振荡器的垂直位移和速度以及纵摇角位移和角速度。

这是一个动力学系统建模问题。对于浮标和振荡器组成的整体系统,其受到的外力包括浮力、波浪激励力、波浪阻尼力和附加质量力的综合作用,这些力共同决定了浮标和振荡器系统的加速度。而浮力与系统的重力之间的差值实际上就是静水复原力。对于振荡器来说,其受到的力仅包括重力、阻尼力和弹簧力。在初始状态下,由于系统处于静止状态,初始阻尼力为零,弹簧力与振荡器的重力相平衡。在运动过程中,这三种力的合力提供了合加速度。通过求解二阶微分方程组,我们可以模拟整个动力学过程。对于不同的阻尼系数,我们只需进行分类讨论即可。相比于问题一,问题二考虑了转动问题。为了研究方便,我们将运动分解为沿轴方向的垂直振动和平面内的纵摇摆动。纵摇和垂直振动的方程实际上非常相似,本质上是外力矩提供了振荡器和浮标的角速度。

在平衡状态下,如果将浮标和振荡器视为一个整体系统,系统的浮力与两者的重力相平衡。在系统内部,弹簧力和阻尼器的阻尼力作为系统的内力。但由于在平衡状态下浮标和振荡器的相对速度为零,因此阻尼器不提供阻尼力。在这种状态下,弹簧力与振荡器的重力平衡,阻尼器由于速度差为零而不提供阻尼力。根据胡克定律,我们可以得到:

$$
mg - kl_{0} = 0. \tag{2.4.5}
$$

带入$k=80000\text{ N/m}$,我们可以得到初始弹簧的初始压缩量$l_{0}$为$0.2980\text{ m}$。
在初始状态下,由于浮力与系统的重力平衡,根据阿基米德原理,浮力等于排出的液体重力,我们可以得到:

$$
\rho gV_{0} = (m+M)g. \tag{2.4.6}
$$

解得初始平衡状态下浮标浸没在水面下的体积为$7.1210$立方米。已知浮标的几何参数,我们可以求解其浸没在水面下的深度。经过计算,锥体部分的体积为$0.8378$立方米,而柱体部分半径为$1$米,解得柱体浸没在水面下的深度为$2$米,加上锥体高度$0.8$米,因此整体浸没在水面下的高度为$2.8$米。

如果想让系统进入垂直振动状态,需要给浮标一个初始速度或加速度。波浪为系统提供动力,但由于波浪在提供动力的同时也为周围的海水提供动能,海水反过来作用于浮标,因此产生了附加质量力。所以,附加质量力的作用对象是海水,它与浮标一起以相同的加速度运动。在运动过程中,我们取竖直向上为正方向,浮标和振荡器的位移分别为和\。对于振荡器来说,其受力始终只包括弹簧力、阻尼力和自身的重力。根据 Newton第二定律,我们可以得到振荡器的运动方程:
$$
k(l_{0} + x_{1} - x_{2}) + \eta \left( \frac{\mathrm{d}x_{1}}{\mathrm{d}t} - \frac{\mathrm{d}x_{2}}{\mathrm{d}t_{2}} \right) - mg = m \frac{\mathrm{d}^{2}x_{2}}{\mathrm{d}t^{2}}. \tag{2.4.8}
$$
在初始状态下,振子的速度和加速度均为零。同时,由于初始状态下振子的重力与弹簧的弹力处于平衡状态,我们可以简化相关的方程为:
$$
k(x_{1} - x_{2}) + \eta \left( \frac{\mathrm{d}x_{1}}{\mathrm{d}t} - \frac{\mathrm{d}x_{2}}{\mathrm{d}t_{2}} \right)= m \frac{\mathrm{d}^{2}x_{2}}{\mathrm{d}t^{2}}. \tag{2.4.9}
$$
考虑将振子和浮子作为一个整体系统,阻尼器提供的阻尼力与弹簧的弹力构成系统的内力。根据 Newton第二定律,系统的外力包括波浪提供的激励力、兴波阻尼力、系统受到的浮力以及系统的重力。同时,考虑到虚拟质量对浮子产生的惯性力,我们在计算加速度时需要将虚拟质量考虑在内。因此,系统的合力决定了浮子、振子以及虚拟质量的加速度:
$$
\rho g V + f \cos (\omega t) - \psi \frac{\mathrm{d}x_{1}}{\mathrm{d}t} - (m + M)g = (M + m_{0}) \frac{\mathrm{d}^{2}x_{1}}{\mathrm{d} t^{2}} + m \frac{\mathrm{d}^{2}x_{2}}{\mathrm{d} t^{2}}. \tag{2.4.9}
$$
由于初始平衡状态下浮力与系统重力相互抵消,因此在运动状态中,浮力与重力的差值即静水恢复力等于浸没在水下的体积变化所对应的重力:
$$
\rho g V - (m + M) g = \rho g \int_{d}^{x_{1}} \pi z^{2}(h) \, \mathrm d{h}. \tag{2.4.10} 
$$
那么,方程(7)可以简化为:
$$
\rho g \int_{d}^{x_{1}} {\pi z^{2}(h)} \, \mathrm d{h}  + f \cos (wt) - \psi \frac{\mathrm{d}x_{1}}{\mathrm{d}t} = (M + m_{0}) \frac{\mathrm{d}^{2}x_{1}}{\mathrm{d} t^{2}} + m \frac{\mathrm{d}^{2}x_{2}}{\mathrm{d}t^{2}}. \tag{2.4.11}
$$
而对于下面两种不同的情况,阻尼器的阻尼系数不同。

**情形一**:阻尼器的阻尼系数为常数:
$$
\eta = 10000. \tag{2.4.12}
$$

**情形二**:阻尼器的阻尼系数与速度差的绝对值有关,幂指数为$0.5$:
$$
\eta = 1000 \left[ \frac{\mathrm{d}x_{1}}{\mathrm{d}t} - \frac{\mathrm{d}x_{2}}{\mathrm{d}t} \right]^{0.5}. \tag{2.4.13} 
$$

给出代码实现如下:
> 代码实现参考过https://github.com/Solus-sano/2022_cumcm_A,特别鸣谢!

```python
import numpy as np
from matplotlib import rcParams
from math import *
from sympy.abc import t
from scipy.integrate import odeint
import matplotlib.pyplot as plt

def pfun_1(ip,t):
    """第1小问微分方程模型"""
    x,y,z,w=ip
    return np.array([z,
                    w,
                    (-k*(x-y)-beta*(z-w))/m2,
                    (k*(x-y)+beta*(z-w)+f*cos(omega*t)-gama*y-Ita*w)/(m1+m_d)
                    ])
def pfun_2(ip,t):
    """第2小问微分方程模型"""
    x,y,z,w=ip
    return np.array([z,
                    w,
                    (-k*(x-y)-beta*sqrt(abs(z-w))*(z-w))/m2,
                    (k*(x-y)+beta*sqrt(abs(z-w))*(z-w)+f*cos(omega*t)-gama*y-Ita*w)/(m1+m_d)
                    ])
if __name__=='__main__':
    dt=0.01
    m1=4866#浮子质量
    m2=2433#振子质量
    m_d=1335.535#垂荡附加质量 (kg)
    k=80000#弹簧刚度 (N/m)
    beta=10000#平动阻尼系数
    f=6250#垂荡激励力振幅 (N)
    omega=1.4005#入射波浪频率 (s-1)
    T_max=(2*pi/omega)*40#模拟最大时间
    gama=1025*9.8*pi#静水恢复力系数
    Ita=656.3616#垂荡兴波阻尼系数 (N·s/m)
    t_lst=np.arange(0,T_max,dt)
    pfun=pfun_2#选择计算第1小问还是第2小问
    sol=odeint(pfun,[0.0,0.0,0.0,0.0],t_lst)
    rcParams['font.sans-serif']=['SimHei']
    plt.figure()
    plt.plot(t_lst,sol[:,0],label='振子位移')
    plt.plot(t_lst,sol[:,1],label='浮子位移')
    plt.legend()
    plt.figure()
    plt.plot(t_lst,sol[:,2],label='振子速度')
    plt.plot(t_lst,sol[:,3],label='浮子速度')
    plt.legend()
```

得到结果如图所示:

![600](./attachments/Pasted%20image%2020240424225810.png)
<center>图2.4.3  振子和浮子位移对比图</center>
从图2.4.3中可以看到,在运动初期,振子和浮子开始阶段存在明显的位移差异,且两者的位移均不超过总高度($3.8\text{ m}$)。在$20$秒后,系统达到稳定状态,此时浮子与振子的相对位移振幅约为$0.12\text{ m}$,呈现出一定的周期性。系统开始运动时,波浪的冲击力为浮子提供了向上的初始加速度,导致浮子开始上升,进而使弹簧和阻尼器进一步压缩,对振子产生更大的推力,使振子开始振动。但当系统趋于稳定时,尽管两者的波形大体相似,但都不是简谐振动,并且存在一定的相位差。这种现象的原因在于,两者的运动受到多种力的影响,其中弹簧和阻尼器的系数以及波浪频率各自提供了不同的频率成分。如果需要进一步分析这些成分,可以进行傅里叶变换。我们将情形一和情形二的位移变化和速度变化数据都放在附件表格中。经模拟,在几个重要时间节点对应的位移和速度如表1所示:

<center>表1  部分时间点对应的位移和速度</center>
![600](./attachments/Pasted%20image%2020240424225940.png)

系统的摇荡过程包括沿自身轴线的垂荡运动和在一个平面内的纵摇运动。系统各部分在运动过程中受到的力如图2.4.4所示:

![500](./attachments/Pasted%20image%2020240424230016.png)
<center>图2.4.4  振子和浮子位移对比图</center>
在纵摇运动的平衡位置,我们将竖直方向作为参考。对于振子而言,其运动可以分解为沿杆的垂荡分量和平面内的纵摇分量。在垂荡方向上,各自沿着自身轴线的方向为正方向,满足与问题一中类似的关系式。但需要注意的是,重力在杆的方向上存在一个分量,这一分量与振子的摆动角度有关:
$$
k(l_{0} + x_{1} - x_{2}) + \eta \left[  \frac{\mathrm{d}x_{1}}{\mathrm{d}t} - \frac{\mathrm{d}x_{2}}{\mathrm{d}t} \right]+ mg (1 - \cos \theta_{2}) = m \frac{\mathrm{d}^{2}x_{2}}{\mathrm{d}t^{2}}, \tag{2.4.14} 
$$
对于纵摇方向上的运动,重力在垂直于杆的方向上提供了力矩,加上弹簧的扭矩和旋转阻尼力矩,我们可以得到一个基于角动量定理的表达式:
$$
K(\theta_2 - \theta_{1}) + \lambda \left[ \frac{\mathrm{d}\theta_{2}}{\mathrm{d}t} - \frac{\mathrm{d}\theta_{1}}{\mathrm{d}t} \right] - mgL_{2} \sin \theta_{2} = J_{2} \frac{\mathrm{d}^{2}\theta_{2}}{\mathrm{\mathrm{d}t^{2}}}. \tag{2.4.15} 
$$
对于由浮子和振子构成的系统,垂荡运动的表达式类似于问题一:
$$
\begin{align}
&\rho gV \cos \theta_{1} + f \cos (\omega t) - \psi \frac{\mathrm{d}x_{1}}{\mathrm{d}t} - (mg \cos \theta_{2} + MgL_{1}\cos \theta_{1}) \\&= (M + m_{0}) \frac{\mathrm{d}^{2}x}{\mathrm{d}t^{2}} + m \frac{\mathrm{d}^{2}x_{2}}{\mathrm{d}t^{2}}. 
\end{align} \tag{2.4.16}
$$
在海水中进行纵摇运动时,静水恢复力矩可以使浮体转正,其大小与浮体相对于静水面的转角成正比,比例系数称为静水恢复力矩系数。因此,我们假设静水恢复力矩、总体重力力矩和浮力力矩的矢量和为零:
$$
\rho g V x_{1} \sin \theta_{1} - (mgL_{2} \sin \theta_{2} + MgL_{1} \sin \theta_{1}) = \varepsilon \theta_{1}, \tag{2.4.17}
$$
其中$\varepsilon$为静水恢复力矩系数。在纵摇方向上,浮力和重力在垂直于竖直方向上提供力矩,加上外部的波浪激励力矩和兴波阻尼力矩:
$$
\varepsilon \theta_{1} + L \cos(\omega t) - \phi \frac{\mathrm{d}\theta_{1}}{\mathrm{d}t} = (J_{1} + J_{0}) \frac{\mathrm{d}^{2}\theta_{1}}{\mathrm{d}t^{2}} + J_{2} \frac{\mathrm{d}^{2}\theta_{2}}{\mathrm{d}t}. \tag{2.4.18}
$$
实现这个模型的代码如下:

```python
import numpy as np
from matplotlib import rcParams
from math import *
from sympy.abc import t
from scipy.integrate import odeint
import matplotlib.pyplot as plt

def pfun(ip,t):
    """第3问微分方程模型"""
    x,y,z,w,th1,th2,ph1,ph2=ip
    A=np.array([[1,0,0,0,0,0,0,0],
                [0,1,0,0,0,0,0,0],
                [0,0,m_v,m_v*cos(th2),0,0,0,0],
                [0,0,0,m_f+m_d,0,0,0,0],
                [0,0,0,0,1,0,0,0],
                [0,0,0,0,0,1,0,0],
                [0,0,0,0,0,0,J_f+J_d,0],
                [0,0,0,-m_v*(l0+x+0.5*h_v)*sin(th2),0,0,0,J_v+m_v*(l0+x+0.5*h_v)**2]])
    b=np.array([[z],
                [w],
                [m_v*g*(1-cos(th2))+m_v*(l0+x+0.5*h_v)*ph2**2-k1*x-beta_1*z],
                [m_v*g*(1-cos(th2))+(k1*x+beta_1*z)*cos(th2)+f*cos(omega*t)-Ita_1*w-gama_1*y],
                [ph1],
                [ph2],
                [k2*(th2-th1)+beta_2*(ph2-ph1)-Ita_2*ph1-(((2+15*(2-y)**2)/(8+30*(2-y)))*((m_v+m_f)*g-1025*g*pi*y)+gama_2)*th1+L*cos(omega*t)],
                [m_v*g*(l0+x+0.5*h_v)*sin(th2)-2*z*ph2*m_v*(l0+x+0.5*h_v)-k2*(th2-th1)-beta_2*(ph2-ph1)]])
    Op=np.dot(np.linalg.inv(A),b)
    return Op.reshape((-1,))
if __name__=='__main__':
    dt=0.01
    g=9.8
    l0=0.2019575#弹簧初始长度
    m_f=4866#浮子质量
    m_v=2433#振子质量
    m_d=1028.876#垂荡附加质量 (kg)
    J_d=7001.914#纵摇附加转动惯量 (kg·m^2)
    J_v=202.75#振子绕质心转动惯量
    J_f=16137.73119#浮子转动惯量
    k1=80000#弹簧刚度 (N/m)
    k2=250000#扭转弹簧刚度 (N·m)
    beta_1=10000#直线阻尼器阻尼系数
    beta_2=1000#旋转阻尼器阻尼系数
    f=3640#垂荡激励力振幅 (N)
    L=1690#纵摇激励力矩振幅 (N·m)
    omega=1.7152#入射波浪频率 (s-1)
    T_max=(2*pi/omega)*40#模拟最大时间
    gama_1=1025*g*pi#静水恢复力系数
    gama_2=8890.7#静水恢复力矩系数 (N·m)
    Ita_1=683.4558#垂荡兴波阻尼系数 (N·s/m)
    Ita_2=654.3383#纵摇兴波阻尼系数 (N·m·s)
    h_v=0.5#振子高度
    t_lst=np.arange(0,T_max,dt)
    sol=odeint(pfun,[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],t_lst)
    
    """将振子相对铰链中心的径向位移、速度转换成垂荡位移、垂荡速度"""
    xx=np.zeros(sol.shape[0])
    vx=np.zeros(sol.shape[0])
    for idx in range(xx.shape[0]):
        xx[idx]=(l0+sol[idx,0])*cos(sol[idx,5])-l0+sol[idx,1]
        vx[idx]=sol[idx,2]*cos(sol[idx,5])+sol[idx,3]
    rcParams['font.sans-serif']=['SimHei']
    plt.figure()
    plt.title('')
    plt.plot(t_lst,xx,label='振子垂荡位移')
    plt.plot(t_lst,sol[:,1],label='浮子垂荡位移')
    plt.legend()
    plt.figure()
    plt.plot(t_lst,vx,label='振子垂荡速度')
    plt.plot(t_lst,sol[:,3],label='浮子垂荡速度')
    plt.legend()
    plt.figure()
    plt.plot(t_lst,sol[:,4],label='浮子纵摇角度')
    plt.plot(t_lst,sol[:,5],label='振子纵摇角度')
    plt.legend()
    plt.figure()
    plt.plot(t_lst,sol[:,6],label='浮子纵摇角速度')
    plt.plot(t_lst,sol[:,7],label='振子纵摇角速度')
    plt.legend()
```

得到结果图像如图所示:

![700](./attachments/Pasted%20image%2020240424231213.png)
<center>图2.4.5  振子和浮子位移对比图</center>
通过对比图2.4.5中的上面两张子图,我们发现在摇摆过程中,两者的位移差最大不超过$0.15$米,并在$40$秒后稳定在$0.05$米左右。在一个完整的摆动周期内,仍然存在轻微的波动。初始阶段,相对位移的波动由于受到冲击的影响较大,振幅较大,但随着时间的推移,经过两次冲击后,逐渐趋于稳定。其波形与问题一中的波形有所不同,主要是因为重力在不同摆动方向上的分量发生了周期性的变化。

值得注意的是,类似于问题一,图13展示了振子和浮子质心高度随时间的变化曲线。初始状态以平衡态浮子的锥顶高度作为$0$高度基准,而位移表示的是相对变化量。因此,在实际求解过程中,需要减去初始高度。

如图2.4.5右上子图所示,振子和浮子的速度变化情况可以观察到,浮子的运动速度变化范围大于振子,其上下速度波动范围介于$-1.5$至$1.5$米/秒。尽管速度差异不显著,但可以看出振子的速度有时会超过$1.5$米/秒,而浮子的速度则不会超过这一值。因此,尽管两者的速度波形相似,但仍存在一定的波动和相位差。

在分析纵摇运动时,我们也考察了振子和浮子的转动情况。以竖直方向作为平衡位置,若开始时两者均以逆时针方向运动,并以此方向为正方向定义,则物体的纵摇运动和垂荡运动构成了一个整体,不能单独对纵摇或垂荡进行分析,因为重力的分量不同,且这一分量的变化是周期性的。

从图2.4.5中可以看出,在初始阶段,浮子的纵摇角速度会突然增加,而振子的增加速度相对缓慢。这是因为海浪给予的初始力矩较大,使浮子获得了较大的角加速度。在模拟过程中,由于$t=0.2$秒这一时间间隔可能略长,导致模拟的速度在初始阶段增长非常快。浮子通过旋转压缩弹簧和阻尼器,提供的弹力迫使振子和浮子保持相同的旋转方向运动。

通过求解,我们得到了几个重要时间节点的垂荡运动和纵摇运动情况,如表2所示。

<center>表2  部分时间点对应相关变量值表</center>
![800](./attachments/Pasted%20image%2020240424231420.png)

### 2.4.3  新冠病毒的传播模型—— SI、SIS、SIR、SEIR模型

自2020年起,新冠疫情的爆发彻底打乱了我们的日常生活。在这场全球性的疫情中,不仅医院的医生和科研人员在前线奋斗,许多互联网公司也开发了健康码和行程码来帮助防疫。此外,高校的数学研究者和学生们也发挥了重要作用,他们通过数学建模来预测和分析新冠病毒的传播趋势,比如英国的帝国理工学院、中国的西安交通大学和武汉大学等。

为了有效控制疫情,模型需要能够预测病毒在不同阶段的传播情况。新冠病毒传播迅速,感染周期短,变异速度快,这使得疫苗研发面临巨大挑战。因此,我们需要考虑易感人群、无症状感染者和已感染者之间的动态平衡。

在编写代码时,只需明确传染速率和恢复速率以及人员流动情况即可。可以将人群分为绿码、黄码和红码三种状态,这种分类具有以下特点:

1. 传播速度快,影响范围广,但可以连续变化。
2. 绿码、黄码、红码人群的比例总和为1。
3. 绿码减少的数量等于黄码增加的数量,黄码减少的数量等于红码增加的数量,红码康复的数量等于绿码增加的数量,即总体保持平衡

SI模型是一种简单的传播模型,将人群分为易感者(S类)和感染者(I类)两类。通过SI模型,我们可以预测疫情高峰的到来。通过提高卫生标准和加强防控措施,可以延缓疫情高峰的出现。

![500](./attachments/Pasted%20image%2020240424231539.png)
<center>图2.4.6  SI模型的模型图</center>
如图所示,SI模型将易感者与感染者的有效接触视为感染事件,无潜伏期、无治愈情况、无免疫力。模型以一天为时间单位,假设总人数N固定,不考虑人口的出生、死亡、迁入和迁出。在任意时刻$t$,易感者和感染者占总人口的比例分别为$S(t)$、$I(t)$,其数量分别为$S(t)$、$I(t)$。初始时刻,各类人群的比例为$s_0$、$i_0$。每个感染者每天有效接触的易感者平均数为$\lambda$。

数学模型为
$$
\begin{align}
N \frac{\mathrm{d}i(t)}{\mathrm{d}t} &= \lambda N s(t) i(t), \\[0.5em]
s(t) + i(t) &= 1.
\end{align} \tag{2.4.19}
$$
你可能已经注意到,这实际上是一个 Logistic 模型。如果尝试编程解决这个模型,最终结果会是一条S型曲线,即所有人最终都会被感染。这个结论显然不合理,说明模型中还有一些未考虑到的因素。

SI模型的仿真代码如下。

```python
# 设置模型参数
lamda = 1.0  # 日接触率, 患病者每天有效接触的易感者的平均人数
y0 = 1e-6  # 患病者比例的初值
tEnd = 50  # 预测日期长度
t = np.arange(0.0,tEnd,1)  # (start,stop,step)

def dy_dt(y, t):  # 定义导数函数 f(y,t)
    dy_dt = lamda*y*(1-y)  # di/dt = lamda*i*(1-i)
    return dy_dt
yAnaly = 1/(1+(1/i0-1)*np.exp(-lamda*t))  # 微分方程的解析解
yInteg = odeint(dy_dt, y0, t)  # 求解微分方程初值问题
yDeriv = lamda * yInteg *(1-yInteg)
# 绘图
plt.plot(t, yAnaly, '-ob', label='analytic')
plt.plot(t, yInteg, ':.r', label='numerical')
plt.plot(t, yDeriv, '-g', label='dy_dt')
plt.title("Comparison between analytic and numerical solutions")
plt.legend(loc='right')
plt.axis([0, 50, -0.1, 1.1])
plt.show()
```

SI模型的仿真结果如图2.4.7所示。

![500](./attachments/Pasted%20image%2020240424231831.png)
<center>图2.4.7  SI模型的仿真结果</center>
在SI模型基础上考虑病愈免疫的康复者(R类)就得到SIR模型,对应疾病被治愈或死亡的状态,感染人群以单位时间传染概率由感染状态转移至移除状态,如图2.4.8所示。

![600](./attachments/Pasted%20image%2020240424231855.png)
<center>图2.4.8  SIR模型的模型图</center>
除了日感染率$\lambda$,还引入一个概念叫日治愈率$\mu$。模型修正为
$$
\begin{align}
N \frac{\mathrm{d}s}{\mathrm{d}t} &= -N \lambda si, \\[0.5em]
N \frac{\mathrm{d}i}{\mathrm{t}} &= N\lambda si - N \mu i, \\[0.5em]
s(t) + i(t) + r(t) &= 1.
\end{align} \tag{2.4.20}
$$

SIR模型的仿真代码如下。

```python
def dySIR(y, t, lamda, mu):  # SIR 模型,导数函数
    i, s = y
    di_dt = lamda*s*i - mu*i  # di/dt = lamda*s*i-mu*i
    ds_dt = -lamda*s*i  # ds/dt = -lamda*s*i
    return [di_dt,ds_dt]

# 设置模型参数
lamda = 0.2  # 日接触率, 患病者每天有效接触的易感者的平均人数
sigma = 2.5  # 传染期接触数
mu = lamda/sigma  # 日治愈率, 每天被治愈的患病者人数占患病者总数的比例
fsig = 1-1/sigma
tEnd = 200  # 预测日期长度
t = np.arange(0.0,tEnd,1)  # (start,stop,step)
i0 = 1e-6  # 患病者比例的初值
s0 = 1-i0  # 易感者比例的初值
Y0 = (i0, s0)  # 微分方程组的初值
print("lamda={}\tmu={}\tsigma={}\t(1-1/sig)={}".format(lamda,mu,sigma,fsig))
ySIR = odeint(dySIR, Y0, t, args=(lamda,mu))  # SIR 模型
# 绘图
#plt.title("Comparison among SI, SIS and SIR models")
plt.xlabel('t')
plt.axis([0, tEnd, -0.1, 1.1])
plt.axhline(y=0,ls="--",c='c')  # 添加水平直线
plt.plot(t, ySIR[:,0], '-r', label='i(t)-SIR')
plt.plot(t, ySIR[:,1], '-b', label='s(t)-SIR')
plt.plot(t, 1-ySIR[:,0]-ySIR[:,1], '-m', label='r(t)-SIR')
plt.legend(loc='best')  # youcans
plt.show()
```

SIR模型的仿真结果如图2.4.9所示。

![500](./attachments/Pasted%20image%2020240424232737.png)
<center>图2.4.9  SIR模型的仿真结果</center>
当我们在使用SIR模型探索疾病传播时,我们近乎触及了模拟真实世界传染病动态的边缘。然而,对于具有潜伏期的疾病,如COVID-19,我们需要引入一个额外的群体——那些高风险接触者,或者说是疑似病例。让我们假设易感个体变成这类高风险接触者的转变速率是$\lambda$,而疑似病例确诊为感染者的转化速率是$\delta$,最后,感染者康复的速率是$\mu$。通过这样的设置,我们得到了一个更加贴近现实的模型,即SEIR模型。

![](./attachments/Pasted%20image%2020240424232842.png)
<center>图2.4.10  SEIR模型的模型图</center>
模型形式形如:
$$
\begin{align}
N \frac{\mathrm{d}s}{\mathrm{d}t} &= -N \lambda si, \\[0.5em]
N \frac{\mathrm{d}e}{\mathrm{d}t} &= N \lambda si - N \delta e, \\[0.5em]
N \frac{\mathrm{d}i}{\mathrm{d}t} &= N \delta e - N \mu i, \\[0.5em]
N \frac{\mathrm{d}r}{\mathrm{d}t} &= -N \mu si, \\[0.5em]
s(t) + e(t) + i(t) + r(t) &= 1.
\end{align} \tag{2.4.21}
$$

> 注意:为了使模型更加全面,我们可以考虑加入更多的转变路径。比如,康复者可能由于病毒的变异重新变成易感个体;或者易感个体通过接种疫苗而直接获得免疫,成为康复者。这些补充路径让模型更加精细,更贴近于COVID-19的实际传播模式,为我们预测疾病的转折点提供了工具。

SEIR模型的仿真代码如下。

```python
def dySEIR(y, t, lamda, delta, mu):  # SEIR 模型,导数函数
    s, e, i = y  # youcans
    ds_dt = -lamda*s*i  # ds/dt = -lamda*s*i
    de_dt = lamda*s*i - delta*e  # de/dt = lamda*s*i - delta*e
    di_dt = delta*e - mu*i  # di/dt = delta*e - mu*i
    return np.array([ds_dt,de_dt,di_dt])

# 设置模型参数
lamda = 0.3  # 日接触率, 患病者每天有效接触的易感者的平均人数
delta = 0.03  # 日发病率,每天发病成为患病者的潜伏者占潜伏者总数的比例
mu = 0.06  # 日治愈率, 每天治愈的患病者人数占患病者总数的比例
sigma = lamda / mu  # 传染期接触数
fsig = 1-1/sigma
tEnd = 300  # 预测日期长度
t = np.arange(0.0,tEnd,1)  # (start,stop,step)
i0 = 1e-3  # 患病者比例的初值
e0 = 1e-3  # 潜伏者比例的初值
s0 = 1-i0  # 易感者比例的初值
Y0 = (s0, e0, i0)  # 微分方程组的初值
# odeint 数值解,求解微分方程初值问题
ySEIR = odeint(dySEIR, Y0, t, args=(lamda,delta,mu))  # SEIR 模型
# 输出绘图
print("lamda={}\tmu={}\tsigma={}\t(1-1/sig)={}".format(lamda,mu,sigma,fsig))
plt.title("Comparison among SI, SIS, SIR and SEIR models")
plt.xlabel('t')
plt.axis([0, tEnd, -0.1, 1.1])
plt.plot(t, ySEIR[:,0], '--', color='r', label='s(t)-SEIR')
plt.plot(t, ySEIR[:,1], '-.', color='g', label='e(t)-SEIR')
plt.plot(t, ySEIR[:,2], '-', color='b', label='i(t)-SEIR')
plt.plot(t, 1-ySEIR[:,0]-ySEIR[:,1]-ySEIR[:,2], ':', color='k', label='r(t)-SEIR')
plt.legend(loc='right')  # youcans
plt.show()
```

SEIR模型的仿真结果如图2.4.11所示。

![](./attachments/Pasted%20image%2020240424233530.png)
<center>图2.4.11  SEIR模型的仿真结果</center>
从图2.4.11中可以看到,此时模型已经非常接近实际的新冠传播情况,可以用此预判新冠的拐点。
注意:值得注意的是,随着疫情的发展,研究人员发现,COVID-19在最初爆发时的参数并非固定不变。相反,它们应当根据疾病传播的不同阶段进行调整,以更准确地反映疾病的传播情况。这种阶段性的模拟方法为我们深入理解疾病传播提供了新的视角。

### 2.4.4  被捕食者-捕食者模型 —— Volterra 模型

考虑一个关于森林中狼和羊共存的扩展模型。在这个模型中,狼是捕食者,羊是食草动物。我们之前已经讨论了 Logistic 生长模型,现在我们将进一步考虑不同物种之间的相互影响。
假设羊有充足的食物资源,如果没有狼的捕食,羊的数量将按照Malthus模型以固定的增长率r1呈指数增长。然而,由于狼的捕食,羊的数量会减少,我们假设这种减少的速率与狼和羊的数量成正比:
$$
\begin{align}
\frac{\mathrm{d}x_{1}}{\mathrm{d}t} &= x_{1}(r_{1} - \lambda_{1}x_{2}), \\[0.5em]
\frac{\mathrm{d}x_{2}}{\mathrm{d}t} &= x_{2}(r_{2} - \lambda_{2}x_{1}).
\end{align}
$$
可以对这一模型进行仿真模拟,代码如下。

```python
alpha = 1. 
beta = 1.
delta = 1.
gamma = 1.
x_0 = 4.
y0 = 2.

def derivative(X, t, alpha, beta, delta, gamma):
    x, y = X
    dotx = x * (alpha - beta * y)
    doty = y * (-delta + gamma * x)
    return np.array([dotx, doty]
Nt = 1000
tmax = 30.
t = np.linspace(0.,tmax, Nt)
x_0 = [x_0, y0]
res = odeint(derivative, x_0, t, args = (alpha, beta, delta, gamma))
x, y = res.T
plt.figure(figsize=(16,9))
plt.grid()
plt.title("odeint method")
plt.plot(t, x, 'xb-', label = 'Deer')
plt.plot(t, y, '+r-', label = "Wolves")
plt.xlabel('Time t, [days]')
plt.ylabel('Population')
plt.legend()
plt.show()
plt.figure()
IC = np.linspace(1.0, 6.0, 9) # initial conditions for deer population (prey)
# np.linspace和arange不同,从1.0开始,到6.0结束,均分为9段
for deer in IC:
    x_0 = [deer, 1.0]
    Xs = odeint(derivative, x_0, t, args = (alpha, beta, delta, gamma))
    plt.plot(Xs[:,0], Xs[:,1], "-", label = "$x_0 =$"+str(x_0[0]))
plt.xlabel("Deer")
plt.ylabel("Wolves")
plt.legend()
plt.title("Deer vs Wolves")
plt.show()
```

得到狼羊的物种数量变化曲线与相轨线,如图2.4.12所示。

![700](./attachments/Pasted%20image%2020240424233809.png)
<center>图2.4.12  物种数量变化曲线和相轨线</center>
在这个模型中,我们关注的是系统的平衡点,即两个物种的数量不再变化的点。显然,当狼和羊的数量都为零时,系统处于一个平衡点,但这个点没有实际意义。我们更关心的是另一个平衡点,即。如图2.4.12所示,相轨线是以狼和羊的数量为坐标绘制的闭合曲线,它有助于我们分析系统的稳定性。感兴趣的读者可以尝试改变模型参数,观察相轨线的变化。

## 2.5  差分方程的典型案例

接下来,我们讨论差分方程,它与微分方程的关系就像离散与连续的关系。实际上,数列的差分问题本身就可以视为差分方程的一个应用案例。无论是差分方程还是微分方程,它们的某些思想、理论和背景都是相通的。

### 2.5.1  差分方程与微分方程建模的异同

差分方程可以看作是微分方程的离散形式,其解法通常采用递推方法。值得注意的是,常微分方程的通解方法与相应的差分方程的解法是相似的。微分方程以其简洁的思想和纯粹的条件,通过微元分析,使得建模变得容易,能够处理多变量系统。然而,微分方程的模型相对原始,求解并不总是容易。相比之下,差分方程通过对连续系统的离散化处理,能够考虑更多因素,尽管有时求解同样困难,但在应用上更为广泛。


### 2.5.2  人口模型的新讨论 —— Leslie模型

以前我们讨论的 Logistic 模型和 Malthus 模型主要关注增长率,没有考虑人口结构、性别比例和人口迁移等因素。Leslie模型则对这些因素进行了考虑和改进。

在正常的社会或自然条件下,生育率和死亡率与群体的年龄结构密切相关。因此,我们需要根据年龄对整个群体进行层次划分,建立与年龄相关的人口模型。Leslie模型是一种基于不同年龄段人群生育率差异的模型,它通过构建变化矩阵进行分析,能够充分考虑种群内的性别比和年龄段差异。
我们将女性人口按年龄等间隔划分为n个年龄组,并将时间离散化,间隔与年龄组相同。假设各个年龄组的生育率b和存活率s不随时间变化,我们可以建立一个模型来描述这种情况。
$$
\left\{ 
\begin{align}
f_{i}(t+1) &= \sum\limits_{i=1}^{n}  b_{i}f_{i}(t), \\[0.5em]
f_{i+1}(t+1) &= s_{i}f_{i}(t).
\end{align}
\right. \tag{2.5.1}
$$
其中$i=1,2,...,n-1$。
在式(2.5.1)中,假设中已扣除了在t时段以后出生而活不到t+1时段的婴儿,记Leslie矩阵:
$$
\boldsymbol{L} = \left[ \begin{matrix}
b_{1} & b_{2} & \cdots & b_{n}\\
s_{1} & 0 & \cdots & 0\\
0 & s_{2} & \cdots & 0\\
\vdots & \vdots & \ddots & \vdots\\
0 & 0 & \cdots & s_{n}
\end{matrix} \right], \tag{2.5.2}
$$
记$\boldsymbol f(t)=[f_{1}(t),f_{2}(t),...,f_{n}(t)]^{\top}$,则式(3.31)可以写作:
$$
\boldsymbol{f}(t+1) = \boldsymbol{Lf}(t). \tag{2.5.3}
$$
通过计算 Leslie 矩阵$\boldsymbol{L}$并使用初始的人口分布向量$\boldsymbol f(0)$,我们能够预测任何给定时刻$t$的女性人口分布$\boldsymbol f(t)$。然后,将这些预测值除以女性在总人口中的比例,就可以得到全国总人口$N(t)$在时刻$t$的估计值。

利用2021年的人口统计数据,我们可以对中国未来一段时间内的人口变化进行预测。在此,要感谢华中科技大学电信学院2020级的邓立桐同学,他协助我们从国家统计年鉴中获取了所需的数据,并进行了相关仿真分析。仿真结果显示了全国人口总数以及不同年龄段人口比例的预期变化趋势,如图2.5.1所示。

![](./attachments/Pasted%20image%2020240424234329.png)
<center>图2.5.1  经模拟仿真的人口变化趋势</center>
> 注意:模拟仿真用数据可以从国家统计局和人口年鉴中查找。

图2.5.1展示了经过模拟仿真得到的人口变化趋势。需要注意的是,这些仿真数据可以从国家统计局和人口年鉴中获得。从图中我们可以看出,在实施三胎政策之后,未来十五年内,15至64岁的人口数量及其在总人口中的比重仍将继续减少,老龄化问题依旧难以得到有效缓解。预计到2035年,65岁及以上的人口将占总人口的$23.7\%$。这一趋势反映了当前人口老龄化已经相当严重,加之生活压力较大,放宽生育政策对于不愿生育的人群和只有一个孩子的家庭影响有限,主要对有两个孩子的家庭产生一定影响。

仿真代码如下:

```python
import pandas as pd
import numpy as np
a=pd.read_excel("data3.xlsx")
p=0.48764
N00=a['总人口(万人)']
N00=np.array(N00)
A=np.eye(90)
b=a['生存率'][0:90]
b1=b/100
for i in range(90):
    A[i,:]*=b1[i]
c=a['调节后生育率'][0:90]
c1=c/1000
M=sum(c1)
d=np.zeros((91,1))
B=np.vstack([c1,A])
L=np.hstack([B,d])
G=[]
K=[]
S1=[]
S2=[]
S3=[]
for i in range(1,13):
    L0=np.power(L,i)
    X=N00@L0
    G.append(X)
    Z=X/p
    K.append(sum(Z))
    S1.append(sum(Z[0:15]))
    S2.append(sum(Z[15:65]))
    S3.append(sum(Z[65:-1]))
K=np.array(K)
S1=np.array(S1)
S2=np.array(S2)
S3=np.array(S3)
x=range(2023,2035)
y1=S1/K
y2=S2/K
y3=S3/K
plt.figure(2)
plt.plot(x,y1,'-or')
plt.plot(x,y2,'-ob')
plt.plot(x,y3,'-og')
plt.title('我国全国各年龄段人口变化趋势')
plt.xlabel('时间(单位:年)')
plt.ylabel('人口数量(单位:万人)')
plt.legend(['年龄在0-14岁总人数','年龄在15-64岁总人数','年龄在65及65岁以上总人数'])
plt.show()
```

另外,补充一个有趣的小事,这个问题正是笔者在2021年冬季为笔者学校数学建模校赛所命制的F题,2022年夏季的中青杯也考的几乎一模一样。


## 2.6  元胞自动机与仿真模拟

在之前的讨论中,我们主要关注了如何针对连续问题建立函数和微分方程模型进行求解。对于一些离散问题,我们尝试使用了差分方程模型。然而,差分方程模型往往难以有效地模拟大多数离散模型。因此,我们将介绍一种强大的工具——元胞自动机。

### 2.6.1  元胞自动机的理论

元胞自动机是一种具有时间、空间和状态离散性的网格动力学模型,其中空间相互作用和时间因果关系局部化。它能够模拟复杂系统的时空演化过程。元胞自动机的一个显著特点是它不依赖于固定的数学公式。因此,在数学建模中,有一个俗语:“遇到难题时,就用元胞自动机”。

自从元胞自动机被提出以来,对其分类的研究成为了一个重要的研究课题和核心理论。根据不同的角度和标准,元胞自动机可以有多种分类方式。1990年,Howard A. Gutowitz 提出了一种基于元胞自动机行为的马尔可夫概率测量的层次化、参数化分类体系。S. Wolfram 对[一维元胞自动机](https://www.netlogoweb.org/launch#https://www.netlogoweb.org/assets/modelslib/Sample%20Models/Computer%20Science/Cellular%20Automata/CA%201D%20Elementary.nlogo)的演化行为进行了详细分析,并在大量计算机实验的基础上,将所有元胞自动机的动力学行为归纳为四大类:

* 平稳(Stable):从任何初始状态开始,经过一定时间运行后,元胞空间趋于一个空间平稳的构形。这里的空间平稳指的是每个元胞都处于固定状态,不随时间变化,例如规则254;
* 周期(Periodic):经过一定时间运行后,元胞空间趋于一系列简单的固定结构或周期结构。这些结构可以被视为一种滤波器,因此可用于图像处理研究,例如规则90;
* 混沌(Chaos):从任何初始状态开始,经过一定时间运行后,元胞自动机表现出混沌的非周期行为。所生成的结构的统计特征保持不变,通常表现为分形维特征,例如规则110;
* 复杂(Complex):出现复杂的局部结构,或者说是局部的混沌,其中有些会不断地传播,例如规则30。

理论上,元胞空间应该是无限的,但实际上这是不可能的。因此,我们需要像处理偏微分方程一样为其添加一些“邻居”,即所谓的边界条件。
对于固定型边界,我们在扩展边界时将扩展值设置为相同的常数值,如图所示:

![300](Pasted%20image%2020240424234850.png)
<center>图2.6.1  固定型边界图</center>
周期型边界则是按照对应行或列的另一侧端点值进行填充,使之呈现出周期变化:

![](./attachments/Pasted%20image%2020240424234901.png)
<center>图2.6.2  周期型边界图</center>
绝热型边界是指按照扩充时最近邻的格点值进行扩展,如图所示:

![](./attachments/Pasted%20image%2020240424234942.png)
<center>图2.6.3  绝热型边界图</center>
映射型边界是针对对应的行或列按照特定规则映射生成填充值的边界方法。例如,定义映射规则为:这一行或这一列中待扩充节点相邻的下一个节点值,则边界如图所示:

![](./attachments/Pasted%20image%2020240424234953.png)
<center>图2.6.4  映射型边界图</center>
### 2.6.2  元胞自动机的应用——生命游戏

生命游戏(Game of Life)是由英国数学家约翰·康威(John Horton Conway)于1970年发明的一种元胞自动机。它是一个零玩家游戏,意味着游戏的演化是由初始状态决定的,不需要玩家的进一步输入。生命游戏的规则非常简单,但它能够产生极其复杂的行为,被誉为“元胞自动机的代表作”。生命游戏的规则如下:

* (Exposure)当前细胞为存活状态时,当周围的存活细胞低于2个时(不包含2个),该细胞变成死亡状态
* (Survive)当前细胞为存活状态时,当周围有2个或3个存活细胞时,该细胞保持原样
* (Overcrowding)当前细胞为存活状态时,当周围有超过3个存活细胞时,该细胞变成死亡状态
* (Reproduction)当前细胞为死亡状态时,当周围有3个存活细胞时,该细胞变成存活状态

尽管规则简单,生命游戏却能够展示出类似于生物系统中的复杂行为,如细胞的出生、死亡和繁衍。它不仅在数学和计算机科学中被广泛研究,还成为了人工生命和复杂系统理论的一个重要模型。

生命游戏的一个有趣特点是,它能够产生各种各样的图案,包括静态图案、振荡图案和移动图案(如滑翔机)。这些图案的演化可以被看作是计算过程的一种形式,因此生命游戏也被认为是一种基本的计算模型。

下面的Python代码示例展示了如何使用NumPy和Matplotlib库来模拟生命游戏。该示例包括创建初始随机网格、添加滑翔机图案以及实现环形边界条件的功能。通过调整参数和初始条件,可以探索生命游戏中各种有趣的行为和图案。

```python
## eg.1.
import sys,argparse #argparse是python的一个命令行解析包
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.colors import ListedColormap
yeah =('purple','yellow')
cmap = ListedColormap(yeah)
 
ON = 255
OFF = 0
vals = [ON, OFF]
 
def randomGrid(N):
    """returns a grid of NxN random values"""
    return np.random.choice(vals, N*N, p=[0.2, 0.8]).reshape(N, N) #采用随机的初始状态
 
def addGlider(i, j, grid):
    """adds a glider with top-left cell at (i, j)"""
    glider = np.array([[0, 0, 255],        
                      [255, 0, 255],
                      [0, 255, 255]])   
    # 3×3 的 numpy 数组定义了滑翔机图案(看上去是一种在网格中平稳穿越的图案)。
    grid[i:i+3, j:j+3] = glider       
    #可以看到如何用 numpy 的切片操作,将这种图案数组复制到模拟的二维网格中
    # 它的左上角放在 i和 j指定的坐标,即用这个方法在网格的特定行和列增加一个图案,
 
#实现环形边界条件
def update(frameNum, img, grid, N):
    newGrid = grid.copy()
    for i in range(N):
        for j in range(N):
            total = int((grid[i, (j-1)%N] + grid[i, (j+1)%N] +
                    grid[(i-1)%N, j] + grid[(i+1)%N, j] +
                    grid[(i-1)%N, (j-1)%N] + grid[(i-1)%N, (j+1)%N] +
                    grid[(i+1)%N, (j-1)%N] + grid[(i+1)%N, (j+1)%N])/255)     
           # 因为需要检测网格的 8个边缘。更简洁的方式是用取模(%)运算符
           # 可以用这个运算符让值在边缘折返
        
           # Conway实现规则 :生命游戏的规则基于相邻细胞的 ON 或 OFF 数目
           # 为了简化这些规则的应用,可以计算出处于 ON 状态的相邻细胞总数。
            if grid[i, j] == ON:
                if (total < 2) or (total > 3):
                    newGrid[i, j] = OFF
            else:
                if total == 3:
                    newGrid[i, j] = ON
    
    # update data
    img.set_data(newGrid)
    grid[:] = newGrid[:]
    return img,
 
#向程序发送命令行参数,mian()
def main():
    # command line arguments are in sys.argv[1], sys.argv[2], ...
    # sys.argv[0] is the script name and can be ignored
    # parse arguments
    parser = argparse.ArgumentParser(description="Runs Conway's Game of Life simulation.")
    # add arguments
    parser.add_argument('--grid-size', dest='N', required=False)  #定义了可选参数,指定模拟网格大小N
    parser.add_argument('--mov-file', dest='movfile', required=False)  #指定保存.mov 文件的名称
    parser.add_argument('--interval', dest='interval', required=False)  #设置动画更新间隔的毫秒数
    parser.add_argument('--glider', action='store_true', required=False) #用滑翔机图案开始模拟
    parser.add_argument('--gosper', action='store_true', required=False)
    args = parser.parse_args()
 
    #初始化模拟   
    # set grid size
    N = 100
    if args.N and int(args.N) > 8:
        N = int(args.N)
    
    # set animation update interval
    updateInterval = 50
    if args.interval:
        updateInterval = int(args.interval)
    
    # declare grid
    grid = np.array([])
    # check if "glider" demo flag is specified,设置初始条件,要么是默认的随机图案,要么是滑翔机图案。
    if args.glider:
        grid = np.zeros(N*N).reshape(N, N) #创建 N×N 的零值数组,
        addGlider(1, 1, grid) #调用 addGlider()方法,初始化带有滑翔机图案的网格
    else:
    # populate grid with random on/off - more off than on
        grid = randomGrid(N)
    
    # 设置动画
    fig, ax = plt.subplots(facecolor='pink') #配置 matplotlib 的绘图和动画参数   
    img = ax.imshow(grid,cmap=cmap, interpolation='nearest')  #用plt.show()方法将这个矩阵的值显示为图像,并给 interpolation 选项传入'nearest'值,以得到尖锐的边缘(否则是模糊的)
    ani = animation.FuncAnimation(fig, update, fargs=(img, grid, N, ),#animation.FuncAnimation()调用函数 update(),该函数在前面的程序中定义,根据 Conway 生命游戏的规则,采用环形边界条件来更新网格。
                                  frames=10,
                                  interval=updateInterval,
                                  save_count=50)
    # number of frames?
    # set the output file
    if args.movfile:
        ani.save(args.movfile, fps=30, extra_args=['-vcodec', 'libx264'])
    plt.show()
    
 
# call main
if __name__ == '__main__':
    main()
```

![](./attachments/Pasted%20image%2020240424235216.png)
<center>图2.6.5  元胞自动机生命游戏的初始演化状态</center>
图2.6.5显示的是元胞自动机在某一特定时间点的状态,可以用来说明生命游戏中的复杂动态。在这个矩阵中,黄色的点代表活细胞(ON状态),紫色背景代表死细胞(OFF状态)。从图中可以观察到,活细胞聚集在一起形成了多种结构,包括一些小的集群和更大的复杂图案。

### 2.6.3  元胞自动机的应用 —— 森林山火扩散模型

在此小节中,我们利用元胞自动机模拟了森林火灾的扩散过程。通过建立一个简单的模型,我们能够观察火势如何在森林中蔓延,以及如何受到植被分布、环境因素和偶发事件(如闪电)的影响。本模型的基本假设包括:

* 空地(黑色)、树木(绿色)、火焰(红色)三种状态;
* 绿色树木可以因相邻的火焰而燃烧;
* 空地上有一定的概率生长出新的树木;
* 闪电可以随机引燃树木。

通过设定不同的参数,如树木密度、燃烧概率、生长率和闪电概率,我们可以模拟出多种火灾情景。该模拟为理解火灾的动力学和预防措施提供了一个直观的平台。代码执行后,展示了500个模拟步骤,每一步都在更新显示火灾蔓延的状态。模拟的动态演示不仅可以帮助研究人员理解和预测真实世界中的火灾行为,也可用于教育和展示目的,以提高公众对森林火灾危险性的认识。

在本节的代码中,我们使用numpy和matplotlib库来创建并迭代森林的状态,numpy用于高效的矩阵计算,而matplotlib则用于可视化每一步的结果。森林的每个单元格都可以根据邻居的状态和随机事件来更新其状态,从而产生一个动态且引人入胜的模拟过程。

```python
import numpy as np#科学计算库 处理多维数据(矩阵)
import matplotlib.pyplot as plt#绘图工具库
import matplotlib as mpl
#设置基本参数
global Row,Col#定义全局变量行和列
Row=100#行
Col=100#列
forest_area=0.8#初始化这个地方是树木的概率
firing=0.8#绿树受到周围树木引燃的概率
grow=0.001#空地上生长出树木的概率
lightning=0.00006#闪电引燃绿树的概率
forest=(np.random.rand(Row,Col)<forest_area).astype(np.int8)
#初始化作图
plt.title("step=1",fontdict={"family":'Times New Roman',"weight":'bold',"fontsize":20})#字体,加粗,字号
colors=[(0,0,0),(0,1,0),(1,0,0)]#黑色空地 绿色树 红色火
bounds=[0,1,2,3]#类数组,单调递增的边界序列
cmap=mpl.colors.ListedColormap(colors)#从颜色列表生成颜色的映射对象
w=plt.imshow(forest,cmap=cmap,norm=mpl.colors.BoundaryNorm(bounds,cmap.N))

#迭代
T=500#迭代500次
for t in range(T):
    temp=forest#上一个状态的森林
    temp=np.where(forest==2,0,temp)#燃烧的树变成空地
    p0=np.random.rand(Row,Col)#空位变成树木的概率
    temp=np.where((forest==0)*(p0<grow),1,temp)#如果这个地方是空位,满足长成绿树的条件,那就变成绿树
    fire=(forest==2).astype(np.int8)#找到燃烧的树木
    firepad=np.pad(fire,(1,1),'wrap')#上下边界,左右边界相连接
    numfire=firepad[0:-2,1:-1]+firepad[2:,1:-1]+firepad[1:-1,0:-2]+firepad[1:-1,2:]
    p21=np.random.rand(Row,Col)#绿树因为引燃而变成燃烧的树
    p22=np.random.rand(Row,Col)#绿树因为闪电而变成燃烧的树
    #Temp = np.where((forest == 1)&(((numfire>0)&(rule1prob<firing))|((numfire==0)&(rule3prob<lightning))),2,Temp)
    temp=np.where(   (forest==1)&( ( (numfire>0)&(p21<firing)    ) | ((numfire==0)&(p22<lightning)     ) )                           ,2,temp)
    
    forest=temp#更新森林的状态
    plt.title("step="+str(t),fontdict={"family":'Times New Roman',"weight":'bold',"fontsize":20})#字体,加粗,字号
    w.set_data(forest)
    plt.pause(0.1)
plt.show()
```

![](./attachments/Pasted%20image%2020240424235340.png)
<center>图2.6.6  森林火灾模拟的森林初始状态</center>
图2.6.6展示了元胞自动机模型中森林火灾模拟的起始状态,步骤为$0$。图中绿色单元代表树木,黑色空间代表没有树木的空地。在此状态下,森林尚未遭遇火灾,绿色树木的分布随机且密集,这提供了一个丰富的模拟环境,用于观察未来火势的蔓延路径和行为。随着模拟的进行,我们将能够见证由外界因素(如闪电)或树木间相互影响导致的火灾爆发和蔓延过程。

### 2.6.4  元胞自动机的应用——蒲公英的生长

在本节中,我们使用元胞自动机模型来探索蒲公英的生长模式,以及各种环境因素如何影响其生存和扩散。

* **年度温度波动**:温度受多种因素影响,包括纬度、地形以及靠近水体的距离等。然而,在特定地点,年平均温度和最高最低温差已确定的情况下,温度变化主要由季节变化决定。数据分析表明,特定月份的温度可以通过在年平均温度上加上一个正弦波函数来确定。
$$
\text{Temp} = \text{Temp\_Avg} + \text{Temp\_Diff} \cdot \cos \left( 2\pi \cdot \frac{\text{time}}{12} \right). \tag{2.6.1} 
$$
* **日照时长的年度变化**:与受天气条件影响的日照时长不同,日照主要由季节决定。由于地球围绕太阳的周期性运动,各季节的日出和日落时间不同。日照时长用基础时长表示,即平均时长,并加上一个正弦波函数。
$$
\text{Light} = \text{Daylight\_Avg} + \text{Daylight\_Diff} \cdot \cos \left( 2\pi \cdot \frac{\text{time}}{12} \right) . \tag{2.6.2}
$$
* **降水和风**:由于降水和风受到水体近程、温度、山脉存在、海流以及彼此相互影响等多种因素的影响,它们很难预测。然而,降水和风条件存在年度模式。因此,我们编制了三组数据集,每组包含12个元素,代表特定月份的平均降水和风条件。

蒲公英种群和可用养分通过以下公式计算:
$$
\begin{align}
\text{Fertility}(t + \Delta t) &= \text{Fertility}(t) + \text{Area} \cdot \text{Repletion\_Con} \cdot \text{Rain} \\&~ ~ ~ ~- \text{Population\_Adult} \cdot \text{Comsumption\_Rate\_Adult} \\&~ ~ ~ ~- \text{Population\_Seed} \cdot \text{Comsumption\_Rate\_Seed}.
\end{align} \tag{2.6.3}
$$


* **生育力**:土地中的总养分。最初,土地含有一定量的养分。
* **面积**:土地的总面积。它与养分恢复率成正比,因为大块土地含有更多有机物和细菌。
* **恢复_常数**:该常数用于评估每平方米面积的恢复率。
* **降雨**:降雨量与养分恢复率成正比,因为单个细菌在给定时间内消耗一定量的水,并将一定量的有机物分解为养分。因此,能够存活的细菌数量与降雨量成正比。从而,降雨量与养分恢复率成正比。
* **成熟植株消耗率** vs. **种子植株消耗率**:成熟植株需要产生种子,这是一个消耗大量能量的过程,而未成熟植株则没有繁殖压力。因此,成熟植株和未成熟植株的养分消耗率不同,应存储在不同的变量中。

#### 解决问题—— 蔓延预测模型建立

**蔓延率**:考虑一块土地。来自源头$S_1$、$S_2$、$S_3$等的种子落在它上面的概率分别是$P_{1}$、$P_{2}$、$P_{3}$等。因此,它不接收到来自源头$S_{1}$、$S_{2}$、$S_{3}$等任何一个种子的概率就是$(1-P_{1})$、$(1-P_{2})$、$(1-P_{3})$等。据此,该土地不接收到任何种子的概率是$(1-P_{1}) \cdot (1-P_{2}) \cdot (1-P_{3})$等的乘积。

而该土地至少接收到一个种子,从而让蒲公英生长的概率是$1$减去上述概率:$1-(1-P_{1}) \cdot (1-P_{2}) \cdot (1-P_{3})$等。

在没有风的情况下,蒲公英种子的扩散与距离呈指数关系递减,这是由下式建模的:
$$
\text{Possibility} = \text{Spread\_Calm}^{-\text{Distance}}. \tag{2.6.4}
$$
风的存在能将种子传播。我们假设种子源与风向一致,并以矢量加法简化此过程,其中风被视为一个矢量。因此,特定数量的种子被分布在变化的面积$A = \pi \cdot \text{Distance}^2$上。该可能性由下式建模:
$$
\text{Possibility} = \frac{\text{Spread\_Wind}}{\text{Distance}^{2}}. \tag{2.6.5}
$$
**生长率**:种子要发育到蒲公英蒴果阶段,需要一定的生长期。在更多养分和有利条件下,未成熟植株可以更快生长。我们创建了变量$K$来确定未成熟植株每月的生长率。
蒲公英生长的最适温度区间能带来最佳结果。然而,在这一区间之外,蒲公英的生长速率呈指数级下降。

$$
\begin{aligned}\\
    &\rule{200mm}{0.4pt}                                                                 \\
    &\textbf{Algorithm 1 蒲公英的生长速率}\\[-1.ex]
    &\rule{200mm}{0.4pt}                                                                 \\
    &\textbf{input} : 
        \text{Temp}(环境温度)
    \\
    &\textbf{output} :  
        k_\text{Temp}(温度影响的生长速率)
        \\
    &\mathbf{initialize}: T_\text{high}(高温阈值),~ T_\text{low}(低温阈值),~ \text{Temp\_Coeff}(温度参数)
    \\[-1.ex]
    &\rule{200mm}{0.4pt}                                                                 
    \\
    &\mathbf{if}~ T_{\text{high}} > \text{Temp} > T_{\text{low}} \\
    &\hspace{2em} k_{\text{Temp}} \leftarrow  1 \\
    &\mathbf{else~if}~ T_{\text{high}} < \text{Temp} \\
    &\hspace{2em} k_{\text{Temp}} \leftarrow  \text{Temp\_Coeff}^{T_{\text{high}} - \text{Temp}} \\
    &\mathbf{else}\\
    &\hspace{2em} k_{\text{Temp}} \leftarrow  \text{Temp\_Coeff}^{\text{Temp} - T_{\text{low}}} \\
    &\rule{200mm}{0.4pt}  \\               
    &\mathbf{return~}k_{\text{Temp}}\\
    &\rule{200mm}{0.8pt}                 
\end{aligned}
$$


同样,蒲公英生长的最佳降水也是一个区间。在这个区间之外,生长速率呈指数级下降。

$$
\begin{aligned}\\
    &\rule{200mm}{0.4pt}                                                                 \\
    &\textbf{Algorithm 2 根据降水计算蒲公英的生长速率}\\[-1.ex]
    &\rule{200mm}{0.4pt}                                                                 \\
    &\textbf{input} : 
        \text{Rain}(环境降水)
    \\
    &\textbf{output} :  
        k_\text{Rain}(降水影响的生长速率)
        \\
    &\mathbf{initialize}: R_\text{high}(高降水阈值),~ R_\text{low}(低降水阈值),~ \text{Rain\_Coeff}(降水参数)
    \\[-1.ex]
    &\rule{200mm}{0.4pt}                                                                 
    \\
    &\mathbf{if}~ R_{\text{Rain}} > \text{Rain} > R_{\text{low}} \\
    &\hspace{2em} k_{\text{Temp}} \leftarrow  1 \\
    &\mathbf{else~if}~ R_{\text{high}} < \text{Rain} \\
    &\hspace{2em} k_{\text{Rain}} \leftarrow  \text{Rain\_Coeff}^{R_{\text{high}} - \text{Rain}} \\
    &\mathbf{else}\\
    &\hspace{2em} k_{\text{Rain}} \leftarrow  \text{Rain\_Coeff}^{\text{Rain} - R_{\text{low}}} \\
    &\rule{200mm}{0.4pt}  \\               
    &\mathbf{return~}k_{\text{Rain}}\\
    &\rule{200mm}{0.8pt}                 
\end{aligned}
$$

日照对于光合作用至关重要,这意味着蒲公英的生长将随着平均每日日照时数的增加而增加。然而,效果并非线性,因为 $k_\text{Lit}$ 的最大值为:
$$
k_{\text{Lit}} = \frac{\text{Light}}{\text{Light} + \text{Daylight\_Coeff}}. \tag{2.6.6}
$$
生长率($K$)可以使用以下公式计算:
$$
K = k_\text{Rain} \cdot k_{\text{Temp}} \cdot k_{\text{Lit}} \cdot \left(  \frac{\text{Fertility}}{\text{Area}} - \text{Comsumption\_Rate\_Seed} \right). \tag{2.6.7} 
$$
我们定义蒲公英的起始状态为$0$,结束状态为$1$。每个月,每株蒲公英的生长状态都会增加该月的$K$值。当生长状态超过$1$时,就将其标记为$1$,表示蒲公英已达到蒴果阶段。

 **死亡率**:蒲公英可能因营养不足、极端温度、过多或过少的降雨以及日照不足而死亡。为了评估每月死亡的可能性,我们开发了一个函数。值得注意的是,生长系数与存活系数不同,因为在极端条件下,植物的新陈代谢可以被减缓以提高其生存概率。

$$
\begin{aligned}\\
    &\rule{200mm}{0.4pt}                                                                 \\
    &\textbf{Algorithm 3 计算蒲公英种群的生存率}\\[-1.ex]
    &\rule{200mm}{0.4pt}                                                                 \\
    &\textbf{input} : 
        \text{Temp}(环境温度),~ \text{Rain}(环境降水)
    \\
    &\textbf{output} :  
         \text{Survival\_Rate\_Seed}(幼年体生存率, \text{SS}),~ \text{Survival\_Rate\_Adult}(成年体生存率, \text{SA}) 
        \\
    &\mathbf{initialize}: T_\text{high}(高温阈值),~ T_\text{low}(低温阈值),~ \text{Temp\_Coeff}(温度参数), \\ 
    &\hspace{5em}R_\text{high}(高降水阈值),~ R_\text{low}(低降水阈值),~ \text{Rain\_Coeff}(降水参数)
    \\[-1.ex]
    &\rule{200mm}{0.4pt}                                                                 
    \\
    &\mathbf{if}~ T_\text{high} > \text{Temp} > T_\text{low}: \\
    &\hspace{2em} k_\text{Temp} \leftarrow  1 \\
    &\mathbf{else~if}~ T_\text{high} < \text{Temp}: \\
    &\hspace{2em} k_\text{Temp} \leftarrow \text{Temp\_Coeff}^{T_\text{high}-\text{Temp}}\\
    &\mathbf{else}\\
    &\hspace{2em} k_\text{Temp} \leftarrow \text{Temp\_Coeff}^{\text{Temp}-T_\text{low}} \\ 
    \\
    &\mathbf{if}~  R_\text{high} > \text{Rain} > R_\text{low}: \\
    &\hspace{2em} k_{\text{Rain}} \leftarrow 1\\
    &\mathbf{else~if}~  R_\text{high} < \text{Rain}:\\
    &\hspace{2em} k_{\text{Rain}} \leftarrow \text{Temp\_Coeff}^{R_\text{high}-\text{Rain}}\\
    &\mathbf{else}~  R_\text{low} > \text{Rain}:\\
    &\hspace{2em} k_{\text{Rain}} \leftarrow \text{Temp\_Coeff}^{\text{Rain}-R_\text{low}}\\
    \\
    &k_{\text{Lit}}  \leftarrow  \frac{\text{Light}}{\text{Light} + \text{Daylight\_Coeff}}\\
    \\
    &\text{Survival\_Rate\_Seed}  \leftarrow  k_{\text{Rain}} \cdot k_\text{Temp} \cdot k_{\text{Lit}} \cdot \left(  \frac{\text{Fertility}}{\text{Area}} - \text{Comsumption\_Rate\_Seed} \right)\\
    &\text{Survival\_Rate\_Adult}  \leftarrow  k_{\text{Rain}} \cdot k_\text{Temp} \cdot k_{\text{Lit}} \cdot \left(  \frac{\text{Fertility}}{\text{Area}} - \text{Comsumption\_Rate\_Adult} \right)\\
    &\rule{200mm}{0.4pt}  \\               
    &\mathbf{return~}\text{Survival\_Rate\_Seed},~ \text{Survival\_Rate\_Adult} \\
    &\rule{200mm}{0.8pt}                 
\end{aligned}
$$


每个月,每株未成熟的蒲公英都有一个$\text{SS}$的死亡可能性,而每株成熟的蒲公英有一个$\text{SA}$的死亡可能性。

现在,让我们看一下生成这些图像的代码。代码使用Python编写,通过模拟环境因素如温度、日照、降雨以及风向对蒲公英生长和死亡率的影响来预测蒲公英种群在特定区域的扩散。通过对光合作用、温度和降雨的最佳范围进行建模,这个程序能够模拟蒲公英如何在不同月份内生长并影响土地的肥沃程度。此外,模拟还考虑了成熟植株和种子植株的养分消耗率差异,以及不同生长阶段植株的生存率。这些参数合作模拟了蒲公英如何在一个假设的生态系统中扩散,最终达到一个动态平衡。

```python
import random
import matplotlib.pyplot as plt
#spread factors
Spread_Calm = math.e
Spread_wind = 0.5
#a variable that describe the fertility of land
Fertility = 10000
#a constant that describe rate of repletion per area
Repletion_Con = 0.006
#a constant that describe the rate of energy use of a dandelion
Consumption_rate_Adult = 1
Consumption_rate_Seed = 0.5
#a variable that describe the population of dandelion
population_adult = 0
population_seed = 0
#a constant describe effect of temperature on germination
Temp_Coefficient = 1.125
#a constant describe effect of temperature on survival
Temp_Survival = 1.01
#a constant describe effect of rainfall on germination
Rain_Coefficient = 1.03
#a constant describe effect of rainfall on survival
Rain_Survival = 1.005
#a constant describe effect of daylight on germination
Daylight_Coefficient = 1
#a constant describe effect of daylight on survival
Daylight_Survival = 0.3
#upper limit of optimum temperature
Thigh = 25
#lower limit of optimum temperature
Tlow = 15
#upper limit of optimum rainfall
Rhigh = 200
#lower limit of optimum rainfall
Rlow = 50
#initialize a variable that describe possibility of germination
K = 0
#initialize a variable that describe possibility of survival
SA = 0
SS = 0
#initialize a variable that describe growth rate
Rate = 0
#a constant that describe the size of a single plant in metre
width = 1
#initialize time of a year in month
time = 1
#number of months want to predict
Period = 48
#time counter
Tcounter = 0
#wind factor of west-east wind in each month
W_west_east = [0,0,0,0,0,0,0,0,0,0,0,0]
#wind factor of south-north wind in each month
W_south_north = [0,0,0,0,0,0,0,0,0,0,0,0]
Rainfall = [200,200,200,200,200,200,200,200,200,200,200,200]
Daylight_Avg = 12
Daylight_Diff = 0
Temperature_Avg = 16
Temperature_Diff = 0
def Temperature(Avg,Diff,T):
    temperature = Avg - Diff*math.cos(2*(math.pi)*(T/12))
    return temperature
def Daylight(Avg,Diff,T):
    daylight = Avg - Diff*math.cos(2*(math.pi)*(T/12))
    return daylight
def Germination(Tem,Rain,Lit):
    k_Tem = 0
    k_Rain = 0
    k_Lit = 0
    if Tem < Tlow:
        k_Tem = (Temp_Coefficient)**(Tem-Tlow)
    elif Tem > Thigh:
        k_Tem = (Temp_Coefficient)**(Thigh-Tem)
    else:
        k_Tem = 1
    if Rain < Rlow:
        k_Rain = (Rain_Coefficient)**(Rain-Rlow)
    elif Rain > Rhigh:
        k_Rain = (Rain_Coefficient)**(Rhigh-Rain)
    else:
        k_Rain = 1
    k_Lit = Lit/(Lit+Daylight_Coefficient)
    k = k_Tem*k_Rain*k_Lit*((Fertility/10000)-Consumption_rate_Seed)
    return k
def Survival(Tem,Rain,Lit,type):
    k_Tem = 0
    k_Rain = 0
    k_Lit = 0
    if Tem < Tlow:
        k_Tem = (Temp_Survival)**(Tem-15)
    elif Tem > Thigh:
        k_Tem = (Temp_Survival)**(25-Tem)
    else:
        k_Tem = 1
    if Rain < Rlow:
        k_Rain = (Rain_Survival)**(Rain-Rlow)
    elif Rain > Rhigh:
        k_Rain = (Rain_Survival)**(Rhigh-Rain)
    else:
        k_Rain = 1
    k_Lit = Lit/(Lit+Daylight_Survival)
    if type == 0:
        s = k_Tem*k_Rain*k_Lit*((Fertility/10000)-Consumption_rate_Adult)
    else:
        s = k_Tem*k_Rain*k_Lit*((Fertility/10000)-Consumption_rate_Seed)
    return s
#initialize the field
#(0,0,0) represent empty land, (0,255,0) represent seed, (255,255,255) represent dandelion
field = [[(0,0,0) for i in range(int(100/width))] for j in range(int(100/width))]
#initialize the field for growth time
growth_table = [[0 for i in range(int(100/width))] for j in range(int(100/width))]
#set the first dandelion
field[0][0] = (255,255,255)
growth_table[0][0] = 1
for Tcounter in range(Period):
    population_adult = 0
    population_seed = 0
    for Column_index in range(int(100/width)):
        for Row_index in range(int(100/width)):
            if field[Column_index][Row_index] == (255,255,255):
                population_adult = population_adult + 1
            if field[Column_index][Row_index] == (0,255,0):
                population_seed = population_seed + 1
    temp = Temperature(Temperature_Avg,Temperature_Diff,time)
    light = Daylight(Daylight_Avg,Daylight_Diff,time)
    rain = Rainfall[time-1]
    Fertility = Fertility + 10000*Repletion_Con*rain
    Fertility = Fertility - population_adult*Consumption_rate_Adult - population_seed*Consumption_rate_Seed
    K = Germination(temp,rain,light)
    SA = Survival(temp,rain,light,0)
    SS = Survival(temp,rain,light,1)
    for Column_index in range(int(100/width)):
        for Row_index in range(int(100/width)):
            if growth_table[Column_index][Row_index] >= 1 and field[Column_index][Row_index] == (0,255,0):
                field[Column_index][Row_index] = (255,255,255)
                growth_table[Column_index][Row_index] = 1
    for Column_index in range(int(100/width)):
        for Row_index in range(int(100/width)):
            if field[Column_index][Row_index] == (0,0,0):
                Spreading = 1
                no_seed = 1
                for inner_column in range(int(100/width)):
                    for inner_row in range(int(100/width)):
                        if field[inner_column][inner_row] == (255,255,255):
                            Distance = ((Column_index-(inner_column+W_south_north[time-1]))**2+(Row_index-(inner_row+W_west_east[time-1]))**2)**(0.5)
                            if W_south_north[time-1] != 0 or W_west_east[time-1] != 0:
                                if Distance == 0:
                                    have_seed = Spread_wind
                                else:
                                    have_seed = Spread_wind/((Distance)**2)
                            else:
                                have_seed = Spread_Calm**(-1*Distance)
                            no_seed = no_seed*(1-have_seed)
                Spreading = 1-no_seed
                Possibility_factor = random.random()
                if Spreading>=Possibility_factor:
                    field[Column_index][Row_index] = (0,255,0)
                    growth_table[Column_index][Row_index] = 0
    for Column_index in range(int(100/width)):
        for Row_index in range(int(100/width)):
            if field[Column_index][Row_index] == (0,255,0):
                growth_table[Column_index][Row_index] = growth_table[Column_index][Row_index] + K
    for Column_index in range(int(100/width)):
        for Row_index in range(int(100/width)):
            if field[Column_index][Row_index] == (0,255,0):
                Possibility_factor = random.random()
                if SS <= Possibility_factor:
                    field[Column_index][Row_index] = (0,0,0)
                    growth_table[Column_index][Row_index] = 0
            elif field[Column_index][Row_index] == (255,255,255):
                Possibility_factor = random.random()
                if SA <= Possibility_factor:
                    field[Column_index][Row_index] = (0,0,0)
                    growth_table[Column_index][Row_index] = 0
    if time <12:
        time = time + 1
    else:
        time = 1
    plt.imshow(field)
    plt.title('Field')
    plt.xlabel('distance/m')
    plt.ylabel('distance/m')
    plt.show()
```

![](./attachments/Pasted%20image%2020240425122903.png)
<center>图2.6.7  蒲公英生长和扩散模拟图</center>
图2.6.7展示了蒲公英在不同月份的生长状态。从左上角的“January”开始,显示了一片黑暗的土地,仅有一点绿色表示蒲公英的存在。随着时间的推移,我们可以看到蒲公英的种子开始扩散(如“July”和“August”所示),覆盖区域逐渐增大。到了“October”和“November”,绿色区域大幅增加,表明蒲公英种群迅速增长。在“December”和次年的“January”中,整个区域几乎完全被蒲公英所覆盖。进入次年“February”,蒲公英的覆盖面积略有减少,可能是由于模型中设定的环境因素影响。整体上,这些图像展现了从一片几乎未被侵占的土地到蒲公英占据主导地位的过程,以及不同月份条件下生态系统的动态变化。

### 2.6.5  元胞自动机的应用——新冠病毒的扩散

> 本节代码参考自https://github.com/Windxy/CA_SEIR/,特别鸣谢

在本节中,我们探讨使用元胞自动机模型模拟新冠病毒(COVID-19)的扩散。在这个模型中,每个细胞代表一个人,可以处于以下状态之一:易感者(S0),感染者(S1),治愈者(S2),或潜伏者(S3)。模型中包含以下转换规则:

* 易感者(S0)根据邻近感染者的数量以一定概率变成潜伏者(S3)或直接变成感染者(S1);
* 感染者(S1)具有传播病毒的能力,经过一段时间后自动变成治愈者(S2);
* 治愈者(S2)拥有暂时免疫力,但在本模型中,我们假设治愈者获得永久免疫;
* 潜伏者(S3)在经过一定潜伏期后变成感染者(S1)。

通过这个模型,我们能够模拟和观察疾病如何在人群中传播,以及不同的干预措施如何影响疾病的扩散。例如,可以通过调整感染率、潜伏期和治愈时间的参数来模拟不同的公共卫生策略,如社交距离、戴口罩和隔离措施的效果。此模型为疾病控制决策提供了一个有力的工具,并有助于公共卫生专家和政策制定者更好地理解和应对疫情。

我们使用Python编写了一个简单的模拟程序,其中包括初始化各状态的人群分布、定义状态转换规则以及视觉展示模拟过程。pygame库用于绘制和更新模拟的图形界面,而matplotlib用于绘制随时间变化的人群状态图表。

```python
# 状态:
# S0表示易感者S
# S1表示感染者I
# S2表示治愈者R
# S3表示潜伏者E

# 规则:
# 1. 当S=S0时,为易感者,被感染率为p=k*(上下左右邻居的感染者)/4+l*(左上左下右上右下邻居的感染者)/4,概率c1变为潜伏者,概率1-c1变为感染者
# 2. 当S=S1时,为感染者,具有一定的感染能力,等待t_max时间,会自动变为S2治愈者,染病时间初始化为0
# 3. 当S=S2时,为治愈者,[具有一定的免疫能力,等待T_max时间,会自动变为S0易感者,免疫时间初始化为0],改为永久免疫
# 4. 当S=S3时,为潜伏者,等待t1_max时间(潜伏时间),会自动变为S1感染者,潜伏时间初始化为0,
from pylab import *
import random
import numpy as np
import pygame
import sys
import matplotlib.pyplot as plt
# 初始化相关数据
k=0.85  #上下左右概率
l=0.55  #其它概率
c1=0.4  #c1概率变为潜伏者
t1_max=15 #潜伏时间
t_max=50 #治愈时间
T_max=75 #免疫时间   
# p = np.array([0.6, 0.4])
# probability = np.random.choice([True, False],
# p=p.ravel())  # p(感染or潜伏)=(self.s1_0*k+self.s1_1*l) p(易感)=1-(self.s1_0*k+self.s1_1*l)
def probablity_fun():
    np.random.seed(0)
    # p = np.array([(self.s1_0*k+self.s1_1*l),1-(self.s1_0*k+self.s1_1*l)])
    p = np.array([0.6, 0.4])
    probability = np.random.choice([True, False],
                                   p=p.ravel())  # p(感染or潜伏)=(self.s1_0*k+self.s1_1*l) p(易感)=1-(self.s1_0*k+self.s1_1*l)
    return probability
RED = (255, 0, 0)
GREY = (127, 127, 127)
Green = (0, 255, 0)
BLACK = (0, 0, 0)
"""细胞类,单个细胞"""
class Cell:
    # 初始化
    stage = 0
    def __init__(self, ix, iy, stage):
        self.ix = ix
        self.iy = iy
        self.stage = stage            #状态,初始化默认为0,易感者
        # self.neighbour_count = 0    #周围细胞的数量
        self.s1_0 = 0                 #上下左右为感染者的数量
        self.s1_1 = 0                 #左上左下右上右下感染者的数量
        self.T_ = 0                  #免疫时间
        self.t_ = 0                  #患病时间
        self.t1_ = 0                 #潜伏时间
    # 计算周围有多少个感染者
    def calc_neighbour_count(self):
        count_0 = 0
        count_1 = 0
        pre_x = self.ix - 1 if self.ix > 0 else 0
        for i in range(pre_x, self.ix+1+1):
            pre_y = self.iy - 1 if self.iy > 0 else 0
            for j in range(pre_y, self.iy+1+1):
                if i == self.ix and j == self.iy:   # 判断是否为自身
                    continue
                if self.invalidate(i, j):           # 判断是否越界
                    continue
                if CellGrid.cells[i][j].stage == 1 or CellGrid.cells[i][j] == 3 :  #此时这个邻居是感染者
                    #如果是在上下左右
                    if (i==self.ix and j==self.iy-1) or \
                       (i==self.ix and j==self.iy+1) or \
                       (i==self.ix-1 and j==self.iy) or \
                       (i==self.ix+1 and j==self.iy):
                        count_0+=1
                    else:
                        count_1+=1
        # print(count_0)
        self.s1_0 = count_0
        # if self.s1_1!=0:
        #     print(count_1,count_0,self.ix,self.iy)
        self.s1_1 = count_1
    # 判断是否越界
    def invalidate(self, x, y):
        if x >= CellGrid.cx or y >= CellGrid.cy:
            return True
        if x < 0 or y < 0:
            return True
        return False
    # 定义规则
    def next_iter(self):
        # 规则1,易感者
        if self.stage==0:
            probability=random.random()#生成0到1的随机数
            s1_01 = self.s1_0 * k + self.s1_1 * l
            if (s1_01>probability) and (s1_01!=0):
                p1 = random.random()
                if p1>c1:
                    self.stage=1
                else:
                    self.stage=3
            else:
                self.stage = 0
        # 规则2,感染者
        elif self.stage == 1:
            if self.t_ >= t_max:
                self.stage = 2
            else:
                self.t_ = self.t_ + 1
        # 规则3,治愈者(永久免疫规则)
        elif self.stage == 2:
            if self.T_ >= T_max:
                self.stage = 0
            else:
                self.T_ = self.T_ + 1
        # 规则4,潜伏者
        elif self.stage == 3:
            if self.t1_ >= t1_max:
                self.stage = 1  # 转变为感染者
            else:
                self.t1_ += 1
"""细胞网格类,处在一个长cx,宽cy的网格中"""
class CellGrid:
    cells = []
    cx = 0
    cy = 0
    # 初始化
    def __init__(self, cx, cy):
        CellGrid.cx = cx
        CellGrid.cy = cy
        for i in range(cx):
            cell_list = []
            for j in range(cy):
                cell = Cell(i, j, 0)            #首先默认为全是易感者
                if (i == cx/2 and j ==cy/2) or (i==cx/2+1 and j==cy/2) or (i==cx/2+1 and j==cy/2+1):#看26行就可以了
                    cell_list.append(Cell(i,j,1))
                else:
                    cell_list.append(cell)
            CellGrid.cells.append(cell_list)
    def next_iter(self):
        for cell_list in CellGrid.cells:
            for item in cell_list:
                item.next_iter()
    def calc_neighbour_count(self):
        for cell_list in CellGrid.cells:
            for item in cell_list:
                item.calc_neighbour_count()
    def num_of_nonstage(self):
        # global count0_,count1_,count2_
        count0 = 0
        count1 = 0
        count2 = 0
        count3 = 0
        for i in range(self.cx):
            for j in range(self.cy):
                # 计算全部的方格数
                cell = self.cells[i][j].stage
                if cell == 0:
                    count0 += 1
                elif cell == 1:
                    count1 += 1
                elif cell == 2:
                    count2 += 1
                elif cell == 3:
                    count3 += 1
        return count0, count1, count2, count3
'''界面类'''
class Game:
    screen = None
    count0 = 0
    count1 = 9
    count2 = 0
    count3 = 0
    def __init__(self, width, height, cx, cy):#屏幕宽高,细胞生活区域空间大小
        self.width = width
        self.height = height
        self.cx_rate = int(width / cx)
        self.cy_rate = int(height / cy)
        self.screen = pygame.display.set_mode([width, height])#
        self.cells = CellGrid(cx, cy)
    def show_life(self):
        for cell_list in self.cells.cells:
            for item in cell_list:
                x = item.ix
                y = item.iy
                if item.stage == 0:
                    pygame.draw.rect(self.screen, GREY,
                                     [x * self.cx_rate, y * self.cy_rate, self.cx_rate, self.cy_rate])
                elif item.stage == 2:
                    pygame.draw.rect(self.screen, Green,
                                     [x * self.cx_rate, y * self.cy_rate, self.cx_rate, self.cy_rate])
                elif item.stage == 1:
                    pygame.draw.rect(self.screen, RED,
                                     [x * self.cx_rate, y * self.cy_rate, self.cx_rate, self.cy_rate])
                elif item.stage == 3:
                    pygame.draw.rect(self.screen, BLACK,
                                     [x * self.cx_rate, y * self.cy_rate, self.cx_rate, self.cy_rate])
    # def count_num(self):
    #     self.count0, self.count1, self.count2,self.count3 = self.cells.num_of_nonstage()
mpl.rcParams['font.sans-serif'] = ['FangSong'] # 指定默认字体
mpl.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题
if __name__ == '__main__':
    count0_ = []
    count1_ = []
    count2_ = []
    count3_ = []
    pygame.init()
    pygame.display.set_caption("传染病模型")
    game = Game(800, 800, 200, 200)
    clock = pygame.time.Clock()
    k1 = 0
    while True:
        k1 += 1
        print(k1)
        # game.screen.fill(GREY)#底部全置灰
        clock.tick(100)  # 每秒循环10次
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
        game.cells.calc_neighbour_count()
        count0, count1, count2,count3 = game.cells.num_of_nonstage()
        # count0,count1,count2 = game.count_num()
        count0_.append(count0)
        count1_.append(count1)
        count2_.append(count2)
        count3_.append(count3)
        if count2 > 200*190:  # 退出条件
            break
        plt.plot(count0_, color='y', label='易感者')
        plt.plot(count3_, color='b', label='潜伏者')
        plt.plot(count1_, color='r', label='感染者')
        plt.plot(count2_, color='g', label='治愈者')
        # plt.ylim([0,80000])
        plt.legend()
        plt.xlabel('时间单位')
        plt.ylabel('人数单位')
        plt.pause(0.1)#0.1秒停一次
        plt.clf()#清除
        # plt.close()#退出
        game.show_life()
        pygame.display.flip()
        game.cells.next_iter()
    plt.show() #显示
```

![500](./attachments/Pasted%20image%2020240425123007.png)
<center>图2.6.8  新冠病毒扩散模拟及人群状态动态</center>
图2.6.8左侧展示了一个使用元胞自动机模拟新冠病毒扩散过程的界面。在模拟环境中,红色区域代表感染者(S1),绿色区域代表治愈者(S2),黑色点代表潜伏者(S3),而易感者(S0)则未在图中标出。我们可以看到,随着时间的推移,感染区从中心向外扩散,形成了明显的红色圆环,而治愈者则位于环的内部。

右侧图是随着时间推移的人群状态变化曲线图,横轴表示时间,纵轴表示人数。不同颜色的曲线代表不同状态的人群数量:黄色曲线代表易感者(S0),蓝色曲线代表潜伏者(S3),红色曲线代表感染者(S1),绿色曲线代表治愈者(S2)。从曲线图可以看出,随着时间的推移,感染者人数先增加后减少,治愈者人数逐渐增加,潜伏者人数有一个短暂的峰值,而易感者人数则持续减少。这反映了疾病传播和人群免疫状态的动态变化。

## 2.7  数值计算方法与微分方程求解

在使用Python求微分方程的数值解或函数极值时,你可能会发现其与MATLAB在使用上有所不同。但你有没有想过,Python是如何求出这些数值解的呢?这背后涉及到的算法,通常会在工程数学和数值分析课程中学到。为了帮助大家更好地理解这些计算的基本原理,我们特别增加了一节内容,介绍一些基础的数值计算方法。如果你感兴趣,甚至可以尝试自己编写一个求解器。

### 2.7.1  Python通过什么求数值解

在Python中,数值计算方法主要依赖于一些专门的库,如NumPy、SciPy和SymPy等。这些库提供了丰富的数学函数和算法,用于处理线性代数、微积分、优化问题等。例如,SciPy中的`scipy.optimize`模块可以用于求解函数的极值,`scipy.integrate`模块可以用于数值积分,而`scipy.linalg`模块则提供了线性代数的相关功能。通过这些工具,我们可以在Python中有效地进行数值计算和求解微分方程。

数值计算方法是一种求解数学问题的近似方法,它在计算机上被广泛应用于各种数学问题的求解。无论是在科学研究还是工程技术中,计算方法都扮演着重要的角色。随着计算机技术的快速发展,计算方法已经成为理工科学生的一门必修课程。计算方法主要研究微积分、线性代数和常微分方程中的数学问题,内容涵盖插值和拟合、数值微分和积分、线性方程组的求解、矩阵特征值和特征向量的计算,以及常微分方程的数值解等。

在Python中,类似MATLAB中的`fsolve`和`odeint`这样的函数,正是依赖于这些数值计算方法来工作的。下面,我们将介绍几种经典的数值计算方法。

### 2.7.2  梯度下降法

梯度下降法是解决优化问题的一种普遍手段,尤其在机器学习和深度学习领域中。其核心思路是:从一个初始位置开始,沿着目标函数梯度的相反方向(即最速下降方向)逐步更新位置,直至达到最小值点。在Python中,我们通常利用NumPy等工具包来计算梯度,并按照梯度下降的更新规则调整变量值。

其基本原理简洁明了,即从一个初始点出发,计算当前点的梯度,并依照以下迭代规则进行更新:
$$
x_{t+1} \leftarrow  x_{t} - \alpha \cdot \text{grad}(f). \tag{2.7.1}
$$
当前后两次迭代的函数值之差满足一个很小的阈值(误差的容许范围)时我们认为迭代基本成功。或者从另一个角度,由于极值点的偏导数为$0$,那么当梯度的模接近$0$时,也可认为找到了极小值点。不过,个人建议使用函数值差异作为更准确的判断标准。

> 注意:事实上,在机器学习的实际应用中,由于数据量庞大,梯度下降通常有三种变体:随机梯度下降、批量梯度下降和小批量梯度下降。此外,还可以引入动量等技术来优化梯度下降过程。这些内容将在后续章节详细介绍。


以函数的极值为例,我们利用梯度下降函数去进行求解:

```python
import numpy as np
import matplotlib.pyplot as plt
x=np.linspace(-6,4,100)
y=x**2+2*x+5

可以编写如下的梯度下降函数。
#将迭代的点描绘出来更直观形象
x_iter=1#设置x的初始值
yita=0.06#步长
count=0#记录迭代次数
while True:
    count+=1
    y_last=x_iter**2+2*x_iter+5
    x_iter=x_iter-yita*(2*x_iter+2)
    y_next=x_iter**2+2*x_iter+5
    plt.scatter(x_iter,y_last)
    if abs(y_next-y_last)<1e-100:
        break
print('最小值点x=',x_iter,'最小值y',y_next,'迭代次数n=',count)
x=np.linspace(-4,6,100)
y=x**2+2*x+5
plt.plot(x,y,'--')
plt.show()
最终解得:
最小值点x= -0.9999999616185459 最小值y 4.000000000000002 迭代次数n= 139
```

已经非常接近精确解$-1$,虽然有一定误差但很小,结果也确实满足了我们预先设置的要求。图像如下:

![](./attachments/Pasted%20image%2020240425123215.png)
<center>图2.7.1  梯度下降的迭代结果</center>
### 2.7.3  Newton法

Newton法,也称为切线法,是一种寻找函数零点的有效方法。其思路是从一个初始估计值开始,不断迭代更新,直到找到零点或达到预定精度。在求解函数极值问题时, Newton法通过寻找函数导数的零点来实现。它的原理如图3.16所示:

![](./attachments/Pasted%20image%2020240425123400.png)
<center>图2.7.2 Newton法示意图</center>
如图2.7.2,假设我们想要求解方程$y=x^{2}-C$的零点,,我们从一个初始点$x_0$开始。 Newton法首先在$x=x_0$处求函数的切线,并找到切线与$\mathrm{x}$轴的交点$x_{1}$,然后在$x=x_{1}$处再次求切线,找到新的交点$x_{2}$,如此不断迭代下去,最终会逼近$x_0$附近的零点。以下是用Python实现的 Newton法示例代码:

```python
import numpy as np
def f(x):
    y=x**3-x-1#求根方程的表达式
    return y
def g(x):
    y=3*x**2-1#求根方程的导函数
    return y
def main():
    x_0=1.5 #取初值
    e=10**(-9) #误差要求
    L=0 #初始化迭代次数
    while L<100: #采用残差来判断
        x1=x_0-f(x_0)/g(x_0) #迭代公式,x(n+1)=x(n)-f(x(n))/f'(x(n))
        x_0=x1
        L=L+1 #统计迭代次数
        if abs(f(x_0)-0)<e:
            break
    print(f"x1={x1}") #输出数值解
    print(f(x_0)-0)  # 验证解的正确性
    print(f"L={L}") #输出迭代次数
if __name__ == '__main__':
   main()
```

注意:Python中不需要像MATLAB那样使用特殊函数来控制结果的精度,因为Python的浮点数运算本身就具有较高的精度。

最终解得:
 ```python
 x1=1.3247179572447898 1.865174681370263e-13 L=4
 ```

程序能够利用 Newton法搜索到起始点最近的一个零点。

### 2.7.4  Euler 法与 Runge-Kutta 法

在数值计算微分方程的过程中,一个核心的概念是用差分方法来近似微分。Euler 法和Runge-Kutta 法都是基于这一思想发展而来的。图2.7.3给出了 Runge-Kutta 法的示意图:

![](./attachments/Pasted%20image%2020240425123457.png)
<center>图2.7.3  Runge-Kutta 法示意图</center>
如图2.7.3所示,我们考虑一个小的区间$h$。在数值微分中,我们希望在这个小区间内,函数的差分增量(即直线的斜率)能够尽可能接近其微分增量(即曲线的斜率)。但是,如图所示,如果我们仅仅使用x_0点处的斜率,差分增量会比微分增量小;而如果使用$(x_0+h)$点处的斜率,差分增量又会比微分增量大。那么,我们能否找到一种折中的方法呢?

* 第一种思路是取$x_0$点和$(x_0+h)$点处斜率的平均值作为差分增量的斜率。这样,我们得到的差分增量就会更接近微分增量,这是改进 Euler 法的基本思想。

> 注意:使用当前点和下一点的导数值进行迭代的方法称为前向 Euler 法和后向 Euler 法。

* 另一种思路是在x_0和(x_0+h)之间取不同点的斜率进行平均,然后进行迭代。这是 Runge-Kutta 法的基本思想

典型的四阶 Runge-Kutta 法会使用四个不同点处的斜率进行迭代计算。经典四阶Runge-Kutta 法的迭代斜率如下:
$$
\begin{align}
K_{1} &\leftarrow f(x_{i}, y_{i}), \\[0.5em]
K_{2} &\leftarrow f\left( x_{i} + \frac{h}{2}, y_{i} + \frac{K_{1}}{2}\cdot h\right), \\[0.5em]
K_{4} &\leftarrow f\left( x_{i} + \frac{h}{2}, y_{i} + \frac{K_{2}}{2}\cdot h\right), \\[0.5em]
K_{4} &\leftarrow f(x_{i} + h, y_{i} + K_{3}h), \\[0.5em]
y_{i+1} &\leftarrow y_{i} + \frac{h}{6}(K_{1} + 2K_{2} + 2K_{3} + K_{4}).
\end{align} \tag{2.7.2}
$$
我们可以编写一个程序自己实现 Runge-Kutta 法:

```python
import math
import numpy as np
import matplotlib.pyplot as plt

def runge_kutta(y, x, dx, f):
    """ y is the initial value for y
        x is the initial value for x
        dx is the time step in x
        f is derivative of function y(t)
    """
    k1 = dx * f(y, x)
    k2 = dx * f(y + 0.5 * k1, x + 0.5 * dx)
    k3 = dx * f(y + 0.5 * k2, x + 0.5 * dx)
    k4 = dx * f(y + k3, x + dx)
    return y + (k1 + 2 * k2 + 2 * k3 + k4) / 6.
t = 0.
y = 0.
dt = .1
ys, ts = [0], [0]
def func(y, t):
    return  1/(1+t**2)-2*y**2
while t <= 10:
    y = runge_kutta(y, t, dt, func)
    t += dt
    ys.append(y)
    ts.append(t)
YS=odeint(func,y0=0, t=np.arange(0,10.2,0.1))
plt.plot(ts, ys, label='runge_kutta')
plt.plot(ts, YS, label='odeint')
plt.legend()
plt.show()
```

![](./attachments/Pasted%20image%2020240425124126.png)
<center>图2.7.4  使用 Python 实现的 Runge-Kutta 法与<code>odeint</code>函数的对比</center>
图2.7.4展示了使用两种不同数值解法求解微分方程的结果对比:Runge-Kutta 方法(标记为 `runge_kutta`)和 Python 的`odeint`函数。水平轴代表时间变量`t`,范围从`0`到`10`,垂直轴代表函数`y(t)`的值。

图中,我们可以看到两条曲线:一条代表 Runge-Kutta 法,另一条代表`odeint`函数。两条曲线从`t = 0`时的同一个初始`y`值开始,随着时间的推移而分开。Runge-Kutta 方法的曲线呈现出开始时的急剧上升,达到顶点后逐渐下降,并在`t`接近`10`时趋近于零。`odeint`函数的曲线紧跟 Runge-Kutta 方法的曲线,表明两种方法提供了相似的微分方程解,虽然存在轻微的变化。

我们可以用自己写的龙格库塔测试洛伦兹系统的结果:

```python
import numpy as np
def move(P, steps, sets):
    x, y, z = P
    sgima, rho, beta = sets
    # 各方向的速度近似
    dx = sgima * (y - x)
    dy = x * (rho - z)
    dz = x * y - beta * z
    return [x + dx * steps, y + dy * steps, z + dz * steps]

# 设置sets参数
sets = [10., 28., 3.]
t = np.arange(0, 30, 0.01)
# 位置1:
P0 = [0., 1., 0.]
P = P0
d = []
for v in t:
    P = move(P, 0.01, sets)
    d.append(P)
dnp = np.array(d)
# 位置2:
P02 = [0., 1.01, 0.]
P = P02
d = []
for v in t:
    P = move(P, 0.01, sets)
    d.append(P)
dnp2 = np.array(d)
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot(dnp[:, 0], dnp[:, 1], dnp[:, 2])
ax.plot(dnp2[:, 0], dnp2[:, 1], dnp2[:, 2])
plt.show()
```


在洛伦兹方程上测试得到结果如图2.7.5所示。洛伦兹方程实际上就是“蝴蝶效应”的数学原理,它的曲线非常像一只蝴蝶。这是在混沌系统研究中得到的结论,当给这个系统一个微小的扰动就会引起极大的变化。也证明这一系统是不稳定的。

![](./attachments/Pasted%20image%2020240425124338.png)
<center>图2.7.5  使用 Runge-Kutta 法模拟洛伦兹系统</center>
读者若有兴趣,还可以试着改写上面的函数实现改进 Euler 法。

### 2.7.5  Crank-Nilkson 法在热传导问题中的应用

> 本节参考自 [【数学建模之Python】13.手撕抛物型方程的差分解法(如一维热传导方程)_crank-nicolson差分格式-CSDN博客](https://blog.csdn.net/m0_53392188/article/details/120116474) ,特别鸣谢!

Crank-Nicolson 法是数值分析中一种用于求解热传导方程和类似偏微分方程的时间推进技术。与纯显式或隐式方法相比,它通过时间中心差分的方式,既稳定又准确。在本节中,我们将探讨如何使用Crank-Nicolson 法求解一维热传导问题,并将其与传统的显式和隐式方法进行比较。

首先,我们需要设定一些初始参数,包括热传导系数、空间和时间的最大范围、步长,以及相关的网格比。在给定这些参数后,我们用Python创建一个矩阵来初始化温度分布,并设定边界条件。在边界处,温度随时间的变化采用指数函数来模拟。本文所示的代码包含几个不同的数值方法:

* **古典显式方法**:一种简单但当时间步长较大时可能不稳定的方法;
* **古典隐式方法**:利用逆矩阵求解,计算上更稳定,适用于更大的时间步长;
* **古典隐式方法**(追赶法):一个更高效的算法,避免了直接计算矩阵的逆;
* **Crank-Nicolson 法**(逆矩阵):结合显式和隐式方法的优点,提高了稳定性和精确度;
* **Crank-Nicolson 法**(追赶法):同样结合了显式和隐式的优点,但使用追赶法提高计算效率;

除此之外,我们还提供了一个精确解函数,用于验证数值解的准确性。所有的数值方法均以Python函数的形式实现,并在结束时将结果输出到Excel文件中,便于分析和对比。

```python
import numpy as np
import pandas as pd
import datetime
start_time = datetime.datetime.now()
np.set_printoptions(suppress=True)
def left_boundary(t):  # 左边值
    return np.exp(t)
def right_boundary(t):  # 右边值
    return np.exp(t + 1)
def initial_T(x_max, t_max, delta_x, delta_t, m, n):  # 给温度T初始化
    T = np.zeros((n + 1, m + 1))
    for i in range(m + 1):  # 初值
        T[0, i] = np.exp(i * delta_x)
 
    for i in range(1, n + 1):  # 注意不包括T[0,0]与T[0,-1]
        T[i, 0] = left_boundary(i * delta_t)  # 左边值
        T[i, -1] = right_boundary(i * delta_t)  # 右边值
    return T


# 一、古典显格式
def one_dimensional_heat_conduction1(T, m, n, r):
    # 可以发现当r>=0.5时就发散了
    for k in range(1, n + 1):  # 时间层
        for i in range(1, m):  # 空间层
            T[k, i] = (1 - 2 * r) * T[k - 1, i] + r * (T[k - 1, i - 1] + T[k - 1, i + 1])
    return T.round(6)


# 二、古典隐格式(乘逆矩阵法)
def one_dimensional_heat_conduction2(T, m, n, r):
    A = np.eye(m - 1, k=0) * (1 + 2 * r) + np.eye(m - 1, k=1) * (-r) + np.eye(m - 1, k=-1) * (-r)
    a = np.ones(m - 1) * (-r)
    a[0] = 0
    b = np.ones(m - 1) * (1 + 2 * r)
    c = np.ones(m - 1) * (-r)
    c[-1] = 0
 
    F = np.zeros(m - 1)  # m-1个元素,索引0~(m-2)
    for k in range(1, n + 1):  # 时间层range(1, n + 1)
        F[0] = T[k - 1, 1] + r * T[k, 0]
        F[-1] = T[k - 1, m - 1] + r * T[k, m]
        for i in range(1, m - 2):  # 空间层
            F[i] = T[k - 1, i + 1]  # 给F赋值
        for i in range(1, m - 1):
            T[k, 1:-1] = np.linalg.inv(A) @ F  # 左乘A逆
    return T.round(6)


# 三、古典隐格式(追赶法)
def one_dimensional_heat_conduction3(T, m, n, r):
    a = np.ones(m - 1) * (-r)
    a[0] = 0
    b = np.ones(m - 1) * (1 + 2 * r)
    c = np.ones(m - 1) * (-r)
    c[-1] = 0
 
    F = np.zeros(m - 1)  # m-1个元素,索引0~(m-2)
    for k in range(1, n + 1):  # 时间层range(1, n + 1)
        F[0] = T[k - 1, 1] + r * T[k, 0]
        F[-1] = T[k - 1, m - 1] + r * T[k, m]
        y = np.zeros(m - 1)
        beta = np.zeros(m - 1)
        x = np.zeros(m - 1)
        y[0] = F[0] / b[0]
        d = b[0]
        for i in range(1, m - 2):  # 空间层
            F[i] = T[k - 1, i + 1]  # 给F赋值
        for i in range(1, m - 1):
            beta[i - 1] = c[i - 1] / d
            d = b[i] - a[i] * beta[i - 1]
            y[i] = (F[i] - a[i] * y[i - 1]) / d
        x[-1] = y[-1]
        for i in range(m - 3, -1, -1):
            x[i] = y[i] - beta[i] * x[i + 1]
        T[k, 1:-1] = x
    return T.round(6)


# 四、Crank-Nicolson(乘逆矩阵法)
def one_dimensional_heat_conduction4(T, m, n, r):
    A = np.eye(m - 1, k=0) * (1 + r) + np.eye(m - 1, k=1) * (-r * 0.5) + np.eye(m - 1, k=-1) * (-r * 0.5)
    C = np.eye(m - 1, k=0) * (1 - r) + np.eye(m - 1, k=1) * (0.5 * r) + np.eye(m - 1, k=-1) * (0.5 * r)
 
    for k in range(0, n):  # 时间层
        F = np.zeros(m - 1)  # m-1个元素,索引0~(m-2)
        F[0] = r / 2 * (T[k, 0] + T[k + 1, 0])
        F[-1] = r / 2 * (T[k, m] + T[k + 1, m])
        F = C @ T[k, 1:m] + F
        T[k + 1, 1:-1] = np.linalg.inv(A) @ F
    return T.round(6)



# 五、Crank-Nicolson(追赶法)
def one_dimensional_heat_conduction5(T, m, n, r):
    C = np.eye(m - 1, k=0) * (1 - r) + np.eye(m - 1, k=1) * (0.5 * r) + np.eye(m - 1, k=-1) * (0.5 * r)
    a = np.ones(m - 1) * (-0.5 * r)
    a[0] = 0
    b = np.ones(m - 1) * (1 + r)
    c = np.ones(m - 1) * (-0.5 * r)
    c[-1] = 0
 
    for k in range(0, n):  # 时间层
        F = np.zeros(m - 1)  # m-1个元素,索引0~(m-2)
        F[0] = r * 0.5 * (T[k, 0] + T[k + 1, 0])
        F[-1] = r * 0.5 * (T[k, m] + T[k + 1, m])
        F = C @ T[k, 1:m] + F
        y = np.zeros(m - 1)
        beta = np.zeros(m - 1)
        x = np.zeros(m - 1)
        y[0] = F[0] / b[0]
        d = b[0]
        for i in range(1, m - 1):
            beta[i - 1] = c[i - 1] / d
            d = b[i] - a[i] * beta[i - 1]
            y[i] = (F[i] - a[i] * y[i - 1]) / d
        x[-1] = y[-1]
        for i in range(m - 3, -1, -1):
            x[i] = y[i] - beta[i] * x[i + 1]
        T[k + 1, 1:-1] = x
    return T.round(6)


def exact_solution(T, m, n, r, delta_x, delta_t):  # 偏微分方程精确解
    for i in range(n + 1):
        for j in range(m + 1):
            T[i, j] = np.exp(i * delta_t + j * delta_x)
    return T.round(6)
a = 1  # 热传导系数
x_max = 1
t_max = 1
delta_x = 0.1  # 空间步长
delta_t = 0.1  # 时间步长
m = int((x_max / delta_x).__round__(4))  # 长度等分成m份
n = int((t_max / delta_t).__round__(4))  # 时间等分成n份
t_grid = np.arange(0, t_max + delta_t, delta_t)  # 时间网格
x_grid = np.arange(0, x_max + delta_x, delta_x)  # 位置网格
r = (a * delta_t / (delta_x ** 2)).__round__(6)  # 网格比
T = initial_T(x_max, t_max, delta_x, delta_t, m, n)
print('长度等分成{}份'.format(m))
print('时间等分成{}份'.format(n))
print('网格比=', r)
 
p = pd.ExcelWriter('有限差分法-一维热传导-题目1.xlsx')
 
T1 = one_dimensional_heat_conduction1(T, m, n, r)
T1 = pd.DataFrame(T1, columns=x_grid, index=t_grid)  # colums是列号,index是行号
T1.to_excel(p, '古典显格式')
 
T2 = one_dimensional_heat_conduction2(T, m, n, r)
T2 = pd.DataFrame(T2, columns=x_grid, index=t_grid)  # colums是列号,index是行号
T2.to_excel(p, '古典隐格式(乘逆矩阵法)')
 
T3 = one_dimensional_heat_conduction3(T, m, n, r)
T3 = pd.DataFrame(T3, columns=x_grid, index=t_grid)  # colums是列号,index是行号
T3.to_excel(p, '古典隐格式(追赶法)')
 
T4 = one_dimensional_heat_conduction4(T, m, n, r)
T4 = pd.DataFrame(T4, columns=x_grid, index=t_grid)  # colums是列号,index是行号
T4.to_excel(p, 'Crank-Nicolson格式(乘逆矩阵法)')
 
T5 = one_dimensional_heat_conduction5(T, m, n, r)
T5 = pd.DataFrame(T5, columns=x_grid, index=t_grid)  # colums是列号,index是行号
T5.to_excel(p, 'Crank-Nicolson格式(追赶法)')
 
T6 = exact_solution(T, m, n, r, delta_x, delta_t)
T6 = pd.DataFrame(T6, columns=x_grid, index=t_grid)  # colums是列号,index是行号
T6.to_excel(p, '偏微分方程精确解')
 
p.save()
 
end_time = datetime.datetime.now()
print('运行时间为', (end_time - start_time))
```

通过比较这些方法,我们可以更好地了解不同算法在求解偏微分方程时的性能和适用场景。Crank-Nicolson方法的引入提供了一个稳定且准确的解决方案,特别适合于那些对时间步长有限制的问题。
我们的程序记录了计算过程的开始和结束时间,这样可以衡量不同方法的运算效率。这个案例不仅展示了如何在Python中实现复杂的数值算法,而且还揭示了数值解法在工程和科学研究中的重要性,特别是在解决现实世界问题时。










================================================
FILE: docs/CH3/第三章-函数极值与规划模型.md
================================================
<center><h1>第3章  函数极值与规划模型</h1></center>

> 内容:@若冰(马世拓)
> 
> 审稿:@刘旭
> 
> 排版&校对:@何瑞杰

在这一章中我们将介绍函数极值与规划模型。约束条件下的极值求解是优化问题和运筹学研究的重点,也是各大数学建模竞赛中考察的重难点。它主要针对的就是及目标函数在约束条件下的极值,以及多种方案中的最优方案。本章主要涉及到的知识点有:

* 线性规划的基本模型与求解
* 非线性规划的基本模型与求解
* 整数规划的基本模型与求解
* 动态规划的基本模型与求解
* 多目标规划的一般策略

> 注意:本章内容也可以在凸优化理论相关资料中学习到,是比较难的一章。除了Matlab、Python以外有很多优化求解软件例如Lingo、gurobi等也适合做优化问题。

## 3.1  从线性代数到线性规划

我想各位在中学阶段其实已经是对线性规划有所了解了,不过如果有些学校没学的话也没关系。在这一节当中我们会回顾以前中学接触到的线性规划,补充一些线性代数的知识和Python解线性代数问题的指令,最后引出线性规划的基本形式。数学建模是一门应用数学课程,与基础数学不同,我这里不打算抢读者线性代数老师的饭碗太严重,但我们会尽可能多补充一些基础的常用的有关理论。

### 3.1.1  中学阶段线性规划的局限性

在高中阶段我们其实就学习过线性规划的知识,但是当时我们可能没有觉得它有多重要所以可能忘掉了。我们通过一个例子回忆一下:

**例3.1** 若$x$, $y$满足条件$\displaystyle \left\{\begin{align} &x + y \geqslant 4\\ &x - y \leqslant 2 \\ &y \leqslant 3 \end{align}\right.$,则$z = 3x + y$ 的最小值为()
A. 18			B. 10			C. 6				D. 4

我想这个问题大家不会感到陌生了。答案很简单,选择C。血脉觉醒的高中生们会将不等式组中的不等式两两配对成三个二元一次方程组求解,得到三个点,最后把三个点分别代入$z=3x+y$最后得到最小值。这是比较快的方法,在90%的情况下这种策略是很奏效的。而学霸们也会采取画图的策略,平移直线来求解。这个问题的可行域如图2.1所示:

![](./attachments/Pasted%20image%2020240428181722.png)
<center>图2.1 例题2.1的可行域</center>

这个题目的解其实很明显,就是把不等式组里面每个不等式在平面直角坐标系中表示出来,然后根据不等号的方向确定可行域。将目标函数进行移项以后转化为$y=-3x+z$,通过平移直线的方式找到可行域内使目标函数截距最大的点,就是正确答案。大家回忆起来了吗?

> 注意:平移直线法是一种通用方法,解方程组得到的最优解很多情况下确实有效但不排除有些特殊情况下可行域是开放的,这个时候不一定存在最小值或最大值。

我为什么把线性规划作为第一个知识点呢?因为线性规划是真的非常重要也非常实用,也最贴近一个入门级小白认识数学建模所具备的数学基础。线性规划的实际应用有很多,比如说:我们可能见过这样一种问题,去运输一批货物的时候大车能运五箱,小车只能运三箱,但我们大车和小车数量都有限,怎么安排运输方案能够在车辆够用的情况下运费还能最小?如果我们把大车数量记为$x$,小车数量记为$y$,那么除了$x$和$y$的范围,$5x+3y$也有自己的一个范围,算上运费作为优化目标,这不就构成了一个线性规划吗?背景熟悉吧,甚至于有一种小学应用题的恐惧感。

再打个比方,生产原料问题,生产产品A、B需要原材料甲、乙、丙;生产一吨A需要多少多少甲、多少多少乙、多少多少丙,这样就有了对AB的三个原料约束。再来一个利润最大作为目标,这也是线性规划。我猜很多读者看到这些例子可能就会暗想:“这就是数学建模?我怎么感觉梦回小学应用题?难不成我被骗了?”,是的,数学建模其实没有那么恐怖,小时候我们做的是应用题,到大了我们只不过是需要用更多知识和编程方法解背景更产业化更学术化的应用题。因为这是一门应用数学学科。

我们大概可以总结出,中学的线性规划通常就两个**变量**$x$和$y$,**约束条件**三个不等式,最后一个线性的形如$z=ax+by$(这里$a$和$b$都是常数)的**目标函数**。这样的式子我们解方程可以解,画图也可以解,总能在两分钟之内算出正确结果。但在实际情况中,问题真的有这么容易吗?其实不然。同样是拿生产问题做文章,如果我们这里生产的原料不止甲乙丙三种呢(通常在有机化合物合成的时候原料可能有十几种甚至上百种),产出的产品也不止AB而是能够产出数十种化合物,还能简单地用高中的方法写吗?

所以我们说,中学的规划存在这样一些局限性:

* 决策变量(如果不好理解暂且称之为自变量吧)往往不止两三个;
* 当变量个数超过三个的时候还能在直角坐标系里面画图吗?不能了;
* 约束条件往往不止三个不等式,不等式可能比变量更多一些;
* 当变量较多的时候,还可能出现方程形式下的约束;
* 中学阶段我们只讨论了线性规划,但如果不等式或者目标函数非线性呢。

这么多情况不知读者朋友会不会被吓到。如果十几个甚至几十个不等式方程组成约束条件,那我草稿纸甚至不知道写不写的下,况且中学阶段没有接触过高维问题。于是,为了以更简单的形式描述更一般的线性规划,我们需要借助一样数学工具——线性代数。

有关于线性代数基本知识的回顾与编程实现,大家可以参考下一节中的内容。如果有学习过线性代数的读者,可以跳过3.2.1节。

### 3.1.2  线性规划的基本形式

前面我们已经看到,中学线性规划的局限性在于难以描述多约束、多变量,但无论是目标函数还是方程还是不等式,我们都可以看成是一个系数向量与变量向量在做乘法(例如,$2a+3b-c$实际上可以看成向量$[2,3,-1]$与向量$[a,b,c]$做内积)。多个约束无非就是把多个向量拼接在一块做成了一个矩阵而已。

我们把所有的方程约束中系数做成系数矩阵$A_{\text{eq}}$,等号右边的常数作为列向量$b_\text{eq}$;不等式约束中的系数矩阵$A$和不等号右边的常数$b$,为了方便起见通常将不等式统一为小于等于;变量$x$在向量$l_b$到$u_b$之间取值;目标函数的系数向量为$c$,那么线性规划的标准形式就如下所示:
$$
\begin{align}
\text{minimize}~&f = c^{\top}x\\[0.5em]
\text{subject to}~& Ax \leqslant b\\
& A_{\text{eq}}\,x = b_{\text{eq}}\\
&l_{b} \leqslant x \leqslant u_{b}
\end{align} \tag{3.1.1}
$$
> 这里的$\leqslant$是指两边向量的对应分量的小于等于关系,即对任意$i$满足$(Ax)_{i} \leqslant b_{i}$。第二行的 subject to 也常简写为 **s.t.**

为了方便matlab编程,我们通常将问题统一为函数极小值问题,不等约束统一为小于等于。如果原问题是最大值或者有大于等于,那就乘$-1$进行取反即可。可能我说这么半天读者并不一定能理解,这里我举一个例子,在matlab中整理一个线性规划的标准形式:

**例3.2** 将该线性规划整理为标准形式并将各矩阵存储在matlab变量区中:
$$
\begin{align}
\text{minimize}~&f = 3x_{1} + 2x_{2} - x_{3}\\[0.5em]
\text{s.t.}~
& 3x_{1} - x_{2} + x_{3} \leqslant 18\\
& x_{1} + 2x_{2} \leqslant 16\\
&x_{1} + x_{2} + x_{3} \geqslant 2\\
&x_{1} + 2x_{2} + 3x_{3} = 15\\
&x_{1} - x_{3} = 4\\
& 0 \leqslant x_{i} \leqslant 16, ~i=1,2,3.
\end{align} \tag{3.1.2}
$$
我们注意到有一个不等式里面是大于等于,所以左右两边乘-1;一个不等式一个方程里面缺少了一项,这是因为缺少的那一项系数是$0$。把各个矩阵和向量存在matlab里面:

==这里缺了东西==

## 3.2  使用Numpy进行矩阵运算

### 3.2.1  线性代数基本知识回顾

线性代数是一门基础数学科目,基本上所有理工科学生大一的时候都得学线性代数。如果是数学系可能学的就是高等代数了。这门课主要是研究矩阵与向量的数学理论,也会探究线性方程组的解等相关问题。我并不是一个线代老师,这里也不打算抢线代老师的饭碗,这一小节我们仅仅引入线性代数中比较重要的一些定义和计算方法。

我相信大多数同学高中毕业是记得向量这个概念的,但中学阶段我们也仅仅是接触到了三维向量。事实上向量的维数可以是很多维,从代数的意义上你可以认为向量是一个集合,从几何的意义上你又可以认为向量是一个$n$维 Euclid 空间中的一个点:
$$
\boldsymbol{x} = [x_{1}, x_{2}, \dots, x_{n}]^{\top} \tag{3.2.1}
$$
> 注意:向量常用粗体($\boldsymbol{a}$)或带箭头标识的字母($\vec{a}$)表示。在本教程中统一用前者;在不引起歧义的情形下,不对向量和矩阵的符号加粗。

和二维、三维空间中的向量一样,高维空间中的向量同样可以进行加减运算、数量乘运算和数乘运算。但毕竟这是一门应用数学课程,我们不打算把太多精力放在任何一本线代课本里面都能找到的公式上,使用matlab举例子恐怕会更加直观。从程序设计的角度来看,如果读者接触过C语言应该会了解数组的概念,而在C++语言中STL里面已经包含了`vector`类型。

数通过集合形成了向量,那向量集合以后又会变成什么呢?如果向量只是沿着同一个方向进行拼接,那么得到的只不过是一个更长一些的向量。但如果是在纵向上做拼接,那么我们或许可以把一个向量排成表格:
$$
A = \left[ \begin{matrix}
a_{1,1} & \cdots & a_{1,n}\\
\vdots & \ddots & \vdots\\
a_{m,1} & \cdots & a_{m,n}
\end{matrix} \right]_{m \times n} \tag{3.2.2}
$$
> 注意:
> 1. 本教程中的矩阵统一使用方括号;
> 2. 右下角的 $m \times n$ 表示矩阵的**形状**,在上文的例子中,若所有分量均为实数,则 $A \in \mathbb{R}^{m \times n}$;
> 3. 矩阵下标常用 $a_{ij}$ 表示。为避免歧义,本教程统一记为 $a_{i,j}$。

这个数表要求每个矩阵的维度相同,排成的这个表格就可以称作一个矩阵。那么矩阵作为向量的集合,自然也保留了向量的一些特性,包括行列相同的矩阵的加减法、数量乘。比较有趣的是矩阵的乘法,它把两个矩阵分别按行、列规约:
$$
A_{m \times n} B_{n \times k} = \left[ \begin{matrix}
\boldsymbol a_{1}^{\top}\\
\boldsymbol a_{2}^{\top}\\
\vdots\\
\boldsymbol{a}_{m}^{\top}
\end{matrix} \right] 
[\boldsymbol{b}_{1}, \dots, \boldsymbol{b}_{k}] = 
\left[ \begin{matrix}
\boldsymbol{a}_{1}^{\top}\boldsymbol{b}_{1} & \cdots & \boldsymbol{a}_{1}^{\top}\boldsymbol{b}_{k}\\
\vdots & \ddots & \vdots \\
\boldsymbol{a}_{m}^{\top}\boldsymbol{b}_{1} & \cdots & \boldsymbol{a}_{m}^{\top}\boldsymbol{b}_{k}
\end{matrix} \right]_{m \times k},\tag{3.2.3}
$$
其中对所有的$i$和$j$,都有$\boldsymbol a_{i}, \boldsymbol{b}_{j} \in \mathbb{R}_{n}$。矩阵排列好以后除了按行可以规约为一群向量的纵向分布,也可以按列规约成一群向量的横向分布。于是才有了(2.10)中矩阵乘法的这个计算方法。每一项都是两个同为$n$维的向量数乘。

> 注意:矩阵乘法的维度要求第一个矩阵的列数和第二个矩阵的行数相等,因此要注意维度问题。另外**矩阵的乘法没有交换律**,在$AB$和$BA$都存在时,一般有$\color{red} AB \ne BA$ 。

==需要修改,改成“平行多面体的体积”==
那我们能否类比向量的模,提出“矩阵的模”这样一个概念呢?在线性代数中确实存在这样一个类似的概念,这个概念叫行列式:

行列式虽然同样是排成了一个表,但注意,矩阵是一个表格,行列式是一个数,它的值是可以算出来的!有关行列式的计算方法有很多,但最经典最通用的方法是代数余子式展开法。代数余子式的本质就是递归式求解,将行列式A中下标为(i,j)的元素所在行和所在列全部去除以后求新的行列式,再乘上对应的符号。而对于行列式A,计算定义为:

> 注意:行列式除了按某一行展开也可以按某一列展开,这一展开行或展开列的选取是任意的,方便计算即可。另外,矩阵可以不要求行列数相等但行列式必须行列数相等!

而递归到最后,我们逃不开最低的二阶行列式求解。二阶行列式的定义为:

将$n$阶行列式降低到$n-1$阶,$n-1$阶再降低为n-2阶,逐层展开到最后二阶,整个行列式求解就可以完成了。不过高阶行列式如果不是特殊行列式计算会有些复杂,我们可以将这个过程交给计算机程序来完成。

这样我们就可以在命令行输出矩阵A对应的行列式的值。有了行列式的概念,我们可以用它定义矩阵的逆矩阵计算方法。一个行列数均为n的矩阵的逆矩阵满足这样的定义:

**定义2.1.2** 方阵$A$的逆$A^{-1}$若存在,则满足下面的等式:
$$
A^{-1}A = AA^{-1} = I = \left[ \begin{matrix}
1 \\
&1\\
&&\ddots&\\
&&&1
\end{matrix} \right] = \text{diag}(1, \dots, 1).\tag{3.2.4}
$$

### 3.2.2  numpy基本使用
使用numpy之前首先需要导入模块:

```python
import numpy as np
```

  接下来我们需要了解一下numpy的几种属性:
* `ndim`:维度
* `shape`:行数和列数
* `size`:元素个数

```python
print('number of dim:',array.ndim)  # 维度
# number of dim: 2
print('shape :',array.shape)    # 行数和列数
# shape : (2, 3)
print('size:',array.size)   # 元素个数
# size: 6
```

#### 1 Numpy 的创建`array`

* `array`:创建数组
* `dtype`:指定数据类型
* `zeros`:创建数据全为`0`
* `ones`:创建数据全为`1`
* `empty`:创建数据接近`0`
* `arrange`:按指定范围创建数据
* `linspace`:创建线段

创建数组

```python
a = np.array([2,23,4])  # list 1d
print(a)
# [2 23 4]
```

指定数据类型

```python
a = np.array([2,23,4],dtype=np.int64)
print(a.dtype)
# int 64
```

创建特定数据

```python
a = np.array([[2,23,4],[2,32,4]])  # 2d 矩阵 2行3列
print(a)
"""
[[ 2 23  4]
 [ 2 32  4]]
"""
```

#### 2 numpy 的几种基本运算

```python
import numpy as np
a=np.array([10,20,30,40])   # array([10, 20, 30, 40])
b=np.arange(4)              # array([0, 1, 2, 3])c=a-b  # array([10, 19, 28, 37])
c=a+b   # array([10, 21, 32, 43])
c=a*b   # array([  0,  20,  60, 120])
c=b**2  # array([0, 1, 4, 9])
c=10*np.sin(a)  
# array([-5.44021111,  9.12945251, -9.88031624,  7.4511316 ])
print(b<3)  
# array([ True,  True,  True, False], dtype=bool)
a=np.array([[1,1],[0,1]])
b=np.arange(4).reshape((2,2))
 
print(a)
# array([[1, 1],
#       [0, 1]])
 
print(b)
# array([[0, 1],
#       [2, 3]])
c_dot = np.dot(a,b)
# array([[2, 4],
#       [2, 3]])
c_dot_2 = a.dot(b)
# array([[2, 4],
#       [2, 3]])

a=np.random.random((2,4))
print(a)
# array([[ 0.94692159,  0.20821798,  0.35339414,  0.2805278 ],
#       [ 0.04836775,  0.04023552,  0.44091941,  0.21665268]])
np.sum(a)   # 4.4043622002745959
np.min(a)   # 0.23651223533671784
np.max(a)   # 0.90438450240606416
print("a =",a)
# a = [[ 0.23651224  0.41900661  0.84869417  0.46456022]
# [ 0.60771087  0.9043845   0.36603285  0.55746074]]
 
print("sum =",np.sum(a,axis=1))
# sum = [ 1.96877324  2.43558896]
 
print("min =",np.min(a,axis=0))
# min = [ 0.23651224  0.41900661  0.36603285  0.46456022]
 
print("max =",np.max(a,axis=1))
# max = [ 0.84869417  0.9043845 ]

A = np.arange(2,14).reshape((3,4)) 
# array([[ 2, 3, 4, 5]
#        [ 6, 7, 8, 9]
#        [10,11,12,13]])
         
print(np.argmin(A))    # 0
print(np.argmax(A))    # 11
 
print(np.mean(A))        # 7.5
print(np.average(A))     # 7.5
 
print(np.cumsum(A)) 
# [2 5 9 14 20 27 35 44 54 65 77 90]
 
print(np.diff(A))    
# [[1 1 1]
#  [1 1 1]
#  [1 1 1]]
 
A = np.arange(14,2, -1).reshape((3,4)) 
# array([[14, 13, 12, 11],
#       [10,  9,  8,  7],
#       [ 6,  5,  4,  3]])
 
print(np.sort(A))
# array([[11,12,13,14]
#        [ 7, 8, 9,10]
#        [ 3, 4, 5, 6]])
 
print(np.transpose(A))    
print(A.T)
# array([[14,10, 6]
#        [13, 9, 5]
#        [12, 8, 4]
#        [11, 7, 3]])
# array([[14,10, 6]
#        [13, 9, 5]
#        [12, 8, 4]
#        [11, 7, 3]])
```

#### 3 Numpy `array` 合并

```python
import numpy as np
A = np.array([1,1,1])
B = np.array([2,2,2])
         
print(np.vstack((A,B)))    # vertical stack
"""
[[1,1,1]
 [2,2,2]]
"""
 
D = np.hstack((A,B))       # horizontal stack
print(D)
# [1,1,1,2,2,2]
 
C = np.concatenate((A,B,B,A),axis=0)
print(C)
"""
array([[1],
       [1],
       [1],
       [2],
       [2],
       [2],
       [2],
       [2],
       [2],
       [1],
       [1],
       [1]])
"""
 
D = np.concatenate((A,B,B,A),axis=1)
print(D)
"""
array([[1, 2, 2, 1],
       [1, 2, 2, 1],
       [1, 2, 2, 1]])
"""
```


#### 4 Numpy `array`的分割

```python
import numpy as np
A = np.arange(12).reshape((3, 4))
print(A)
"""
array([[ 0,  1,  2,  3],
    [ 4,  5,  6,  7],
    [ 8,  9, 10, 11]])
"""

print(np.split(A, 2, axis=1))
"""
[array([[0, 1],
        [4, 5],
        [8, 9]]), array([[ 2,  3],
        [ 6,  7],
        [10, 11]])]
"""

print(np.split(A, 3, axis=0))
# [array([[0, 1, 2, 3]]), array([[4, 5, 6, 7]]), array([[ 8,  9, 10, 11]])]

print(np.array_split(A, 3, axis=1)) #不等量分割
"""
[array([[0, 1],
        [4, 5],
        [8, 9]]), array([[ 2],
        [ 6],
        [10]]), array([[ 3],
        [ 7],
        [11]])]
```


## 3.3  线性规划的算法原理

前面我们简单的回顾了线性规划和线性代数的基本知识,接下来我们将具体的介绍线性规划的算法原理。

### 3.3.1  单纯形法

在2.1节的最后我们提出了线性规划的标准形式,但注意这种标准形式是针对程序设计工具而言的。如果读者有凸优化理论的背景可能会感到狐疑,说:为什么我看到的标准形式和你这里写的不太一样?是的,经典的凸优化教材会把模型写成另外一种形式:
$$
\begin{align}
\text{maximize}~& f = c^{\top}X\\[0.5em]
\text{s.t.}~
&A^{*}\tilde{X} = b^{*}\\
& X \geqslant 0.
\end{align}\tag{3.3.1}
$$
为了和2.1当中提出的标准形式区分,我们暂且把这种形式称作**规范形式**。规范形式求的是函数的极大值,并且把不等关系和等式关系统一为等式关系方便求解。读者朋友可能会有些疑惑,说:不等式怎么可以充当为方程呢?这就是一种数学思想。可能读者朋友可以理解方程是不等式的特例,但不一定理解不等式也可以视作方程的特例,我举个例子。比如对于不等式$2a+3b+c<10$,左边比右边小,但是小多少呢?我们把这个差额记作d,左边如果补上这个差额就可以写作$2a+3b+c+d=10$,这样就转化成了等式。这里的d被称为**松弛变量**。包括决策变量的上下界$l_b$和$u_b$也会被转化为不等关系引入松弛量。

在单纯形法中,我们解决问题通常从理论上都会把问题转换为规范形式来求解,对每一个不等式都引入一个松弛变量去增广我们的原问题。但这些松弛变量不会出现在目标函数当中。

> 注意:在程序设计中我们输入的是它的标准形式,而在matlab底层以规范形式进行运算,只是从标准形式到规范形式这个操作我们看不见。不等式条件中增广了n个松弛变量的同时等式条件也会增广,只不过在增广后的等式条件中松弛变量的系数都是0。


单纯形法的步骤包括如下几个步骤:

1. 确定初始可行基和初始基可行解,并建立初始单纯形表。
2. 在当前表的目标函数对应的行中,若所有非基变量的系数非正,则得到最优解,算法终止;否则进入下一步。
3. 若单纯形表中$1 \cdots m$列构成单位矩阵,在$j=m+1\cdots n$列中,若有某个对应$x_k$的系数列向量$P_k \leqslant 0$,则停止计算。否则,转入下一步。
4. 挑选目标函数对应行中系数最大的非基变量作为进基变量。假设$x_{k}$为进基变量,按如下规则计算,可确定$x_{u}$为出基变量,转下一步:$$\theta = \min \left.\left\{ \frac{b_{i}}{a_{i,k}} \right| a_{i,k} > 0 \right\} = \frac{b_{u}}{a_{u,k}}, \tag{3.3.2}$$    其中$b_{i}$是规范型规划的常数项,$a_{i,k}$即为在第$i$个约束中变量$k$的系数。
5. 以$a_{u,k}$为主元素进行迭代,对$x_k$所对应的列向量进行如下变换:$$P_{k} = [a_{1,k}, a_{2,k}, \dots, a_{u,k}, \dots, a_{m,k}]^{\top} = [0, 0, \dots, 1, \dots, 0]^{\top}\tag{3.3.3}$$    也即令向量$P_{k}$除了第$u$个元素为$1$外其他元素都为零。
6. 重复2-5步,直到所有检验数非正后终止,得到最优解。

**例3.3** 利用单纯形法解下面这个简单的规划案例:
$$
\begin{align}
\text{minimize}~&f = -5x_{1} - x_{2}\\[0.5em]
\text{s.t.}~
& x_{1} + x_{2} \leqslant 5\\
& 2x_{1} + 0.5x_{2} \leqslant 8\\
& x_{1},x_{2} \geqslant {0.}
\end{align} \tag{3.3.4}
$$

首先我们利用松弛变量将这个问题变成规范型:
$$
\begin{align}
\text{minimize}~&f = -5x_{1} - x_{2}\\[0.5em]
\text{s.t.}~
& x_{1} + x_{2} + x_{3} = 5\\
& 2x_{1} + 0.5x_{2} + x_{4} = 8\\
& x_{1},x_{2}, x_{3}, x_{4} \geqslant {0.}
\end{align} \tag{3.3.5}
$$

针对这个问题,列出如图所示的规划表:
![](./attachments/Pasted%20image%2020240428185800.png)


最上面一行写下目标函数的系数,然后写下两行等式约束的系数与常数项。第一次迭代时令$z=0$, $u=f-z$,初始状况下$u$为目标函数的系数(后面会进行迭代变化),选择$u$中最大的一项也就是$5$。故选择$5$对应的元素作为入基向量,计算常数项与基向量的系数之比,发现$5/1>8/2$,选择第二行对应的松弛向量$x_4$出基。迭代后的新表格为:
![](./attachments/Pasted%20image%2020240428185803.png)


此时,将$x_4$替换为$x_1$,将$x_1$系数$5$填入表格中,原来的表格都减去第二个约束条件除以$x_1$在第二行约束中的系数$2$得到新的系数与常数项。此时,$z$等于$x_3$的系数$0$乘以第一行新约束加x_1的系数5乘以第二行新约束,得到新的z结果。$u=f-z$得到$u$的结果,所有的数都是非正数,此时迭代就结束了。常数项对应的$1,4$其实就是解,计算可以得到极值。
单纯形法的实现代码如下:

```python
import numpy as np
def pivot(d,bn):
    l = list(d[0][:-2])
    jnum = l.index(max(l)) #转入编号
    m = []
    for i in range(bn):
        if d[i][jnum] == 0:
            m.append(0.)
        else:
            m.append(d[i][-1]/d[i][jnum])
    inum = m.index(min([x for x in m[1:] if x!=0]))  #转出下标
    s[inum-1] = jnum
    r = d[inum][jnum]
    d[inum] /= r
    for i in [x for x in range(bn) if x !=inum]:
        r = d[i][jnum]
        d[i] -= r * d[inum]  
#定义基变量函数
def solve(d,bn):
    flag = True
    while flag:
        if max(list(d[0][:-1])) <= 0: #直至所有系数小于等于0
            flag = False
        else:
            pivot(d,bn)            
def printSol(d,cn):
    for i in range(cn - 1):
        if i in s:
            print("x"+str(i)+"=%.2f" % d[s.index(i)+1][-1])
        else:
            print("x"+str(i)+"=0.00")
    print("objective is %.2f"%(-d[0][-1]))
d = np.array([[5,1,0,0,0],[1,1,1,0,5],[2,1/2,0,1,8]])
(bn,cn) = d.shape
s = list(range(cn-bn,cn-1)) #基变量列表
solve(d,bn)
printSol(d,cn)

```

此时我们可以看到$x_1=4$,$x_2=0$,$x_3=1$,$x_4=0$,极值为$20$。


### 3.3.2  内点法


单纯形法之所以需要遍历所有顶点才能获得最优解,归根结底还是在于单纯形算法的搜索过程是从一个顶点出发,然后到下一个顶点,就这样一个顶点一个顶点的去搜寻最优解。单纯形算法的搜索路径始终是沿着多面体的边界的。显然,当初始点离最优点的距离很远的时候单纯形算法的搜索效率就会大大降低。
能否直接从多边形内部打进来呢?这就需要用到内点法,如图所示:
![](./attachments/Pasted%20image%2020240428185812.png)

内点法的算法原理:
* 令 $k=0$,获得初始可行解$x_0$,满足如下条件: $Ax_0>b$,$x_0>b$
* 计算损失:$$r_{k} = c - A^{\top}w_{k}, \quad w_{k} = (AX_{k}^{2}A)^{-1}AX_{k}^{2}c \tag{3.3.6}$$
* 检查是否为最优解,若满足,则输出最优解;若不满足,执行下一步
* 计算搜索方向:$$d_{y, k} = -\Big[ I - X_{k}A^{\top}(AX_{k}^{2}A)^{-1}AX_{k} \Big]X_{k}c = -X_{k}r_{k} \tag{3.3.7}$$
* 若梯度大于$0$,表明该问题无解;若存在$0$项,则存在最优解
* 计算步长$$a_{k} = \min \left.\left\{ \frac{a}{-(\boldsymbol{d}_{y, k})_{i}} \right| (\boldsymbol{d}_{y,k})_{i} < 0 \right\}, \quad 0 \leqslant a \leqslant 1 \tag{3.3.8}$$
* 更新当前解$$x_{k+1} = x_{k} + a_{k}X_{k}d_{y,k} \tag{3.3.9}$$

使用内点法实现上述用例的代码如下:
```python
import numpy as np
def Interior_Point(c,A,b):

    # 当输入的c,A,b有缺失值时,输出错误原因,函数执行结束

    if c.shape[0] != A.shape[1]:
        print("A和C形状不匹配")
        return 0
    if b.shape[0] != A.shape[0]:
        print("A和b形状不匹配")
        return 0

    # 初值的设置

    x=np.ones((A.shape[1],1)) # 将x的初值设为1
    v=np.ones((b.shape[0],1))  # 将v的初值设为1
    lam=np.ones((x.shape[0],1))  # 将lam的初值设为1
    one=np.ones((x.shape[0],1))
    mu=1 # 将mu的初值设为1
    n=A.shape[1]
    x_=np.diag(x.flatten()) # 将x转换为对角矩阵
    lam_=np.diag(lam.flatten()) # 将lam转换为对角矩阵

    # 初始的F,r=F

    r1=np.matmul(A,x)-b
    r2=np.matmul(np.matmu
Download .txt
gitextract_hfcu50jj/

├── README.md
└── docs/
    ├── .nojekyll
    ├── CH0/
    │   └── 绪论-走进数学建模的大门.md
    ├── CH1/
    │   └── 第1章-解析方法与几何模型.md
    ├── CH10/
    │   └── 第10章-图像、文本与信号数据.md
    ├── CH2/
    │   └── 第2章-微分方程与动力系统.md
    ├── CH3/
    │   └── 第三章-函数极值与规划模型.md
    ├── CH4/
    │   └── 第4章-复杂网络与图论模型.md
    ├── CH5/
    │   └── 第五章-进化计算与群体智能.md
    ├── CH6/
    │   └── 第六章-数据处理与拟合模型.md
    ├── CH7/
    │   ├── 第7章-权重生成与评价模型.md
    │   └── 第7章-评价类模型.docx
    ├── CH8/
    │   ├── 第8章-时间序列(1).docx
    │   └── 第8章-时间序列.md
    ├── CH9/
    │   ├── 副本第9章-统计与机器学习(1).docx
    │   └── 第九章-机器学习与统计模型.md
    ├── README.md
    ├── _sidebar.md
    └── index.html
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,112K chars).
[
  {
    "path": "README.md",
    "chars": 3629,
    "preview": "# 数学建模导论【🧪 Beta公测版】\r\n【🧪 Beta公测版本提示:教程主体已完成,正在优化细节,欢迎大家提Issue反馈问题或建议。】\r\n\r\n项目教程地址:https://datawhalechina.github.io/intro-m"
  },
  {
    "path": "docs/.nojekyll",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "docs/CH0/绪论-走进数学建模的大门.md",
    "chars": 3724,
    "preview": "<center><h1>绪论 走进数学建模的大门</h1></center>\n\n> 项目负责人&主编:马世拓(若冰,马马老师,B站id:平成最强假面骑士)\n>\n> 编辑&校对:\n>\n> - 陈思州(小红花):第1章,第2章,第10章\n> -"
  },
  {
    "path": "docs/CH1/第1章-解析方法与几何模型.md",
    "chars": 31790,
    "preview": "<center><h1>第1章  解析方法与几何模型</h1></center>\n\n> 内容:@若冰(马世拓)\n>\n> 审稿:@陈思州\n>\n> 排版&校对:@何瑞杰\n\n读者朋友们,在这一章当中我们将会主要学习几何模型的相关知识。你们应该都还"
  },
  {
    "path": "docs/CH10/第10章-图像、文本与信号数据.md",
    "chars": 37608,
    "preview": "<center><h1>第10章  多模数据与智能模型</h1></center>\r\n\r\n> 内容:@若冰(马世拓)\r\n> \r\n> 审稿:@陈思州\r\n> \r\n> 排版&校对:@陈思州\r\n\r\n随着我们进入本书的第十章,我们将探讨多模数据与智能"
  },
  {
    "path": "docs/CH2/第2章-微分方程与动力系统.md",
    "chars": 107077,
    "preview": "\n<center><h1>第2章 微分方程与动力系统</h1></center>\n> 内容:@若冰(马世拓)\n> \n> 审稿:@陈思州\n> \n> 排版&校对:@何瑞杰\n\n这一章我们主要介绍微分方程与一些动力系统模型。数学上对微分方程的研究是"
  },
  {
    "path": "docs/CH3/第三章-函数极值与规划模型.md",
    "chars": 72375,
    "preview": "<center><h1>第3章  函数极值与规划模型</h1></center>\n\n> 内容:@若冰(马世拓)\n> \n> 审稿:@刘旭\n> \n> 排版&校对:@何瑞杰\n\n在这一章中我们将介绍函数极值与规划模型。约束条件下的极值求解是优化问题"
  },
  {
    "path": "docs/CH4/第4章-复杂网络与图论模型.md",
    "chars": 46313,
    "preview": "<center><h1>第4章  复杂网络与图论模型</h1></center>\r\n\r\n> 内容:@若冰(马世拓)\r\n> \r\n> 审稿:@邢硕\r\n> \r\n> 排版&校对:@若冰\r\n\r\n这一章我们主要介绍复杂网络与图论模型。图论模型不同于以前"
  },
  {
    "path": "docs/CH5/第五章-进化计算与群体智能.md",
    "chars": 30613,
    "preview": "\n<center><h1>第5章 进化计算与群体智能</h1></center>\n\n> 内容:@若冰(马世拓)\n> \n> 审稿:@刘旭\n> \n> 排版&校对:@何瑞杰\n\n这一章我们主要介绍进化计算和群体智能算法中四个最常用的算法。传统的优化"
  },
  {
    "path": "docs/CH6/第六章-数据处理与拟合模型.md",
    "chars": 55140,
    "preview": "<center><h1>第6章 数据处理与拟合模型</h1></center>\r\n\r\n> 内容:@若冰(马世拓)\r\n> \r\n> 审稿:@萌弟\r\n> \r\n> 排版&校对:@若冰\r\n\r\n从这一章开始我们就在数据的海洋中遨游了。在这一章中我们将介"
  },
  {
    "path": "docs/CH7/第7章-权重生成与评价模型.md",
    "chars": 49036,
    "preview": "<center><h1>第7章 权重生成与评价模型</h1></center>\r\n> 内容:@若冰(马世拓)\r\n> \r\n> 审稿:@牧小熊(聂雄伟)\r\n> \r\n> 排版&校对:@牧小熊(聂雄伟)\r\n\r\n这一章我们主要介绍评价模型。评价类模型"
  },
  {
    "path": "docs/CH8/第8章-时间序列.md",
    "chars": 34290,
    "preview": "<center><h1>第8章 时间序列与投资模型</h1></center>\r\n> 内容:@若冰(马世拓)\r\n> \r\n> 审稿:@牧小熊(聂雄伟)\r\n> \r\n> 排版&校对:@牧小熊(聂雄伟)\r\n\r\n这一章主要介绍时间序列与投资模型。时间"
  },
  {
    "path": "docs/CH9/第九章-机器学习与统计模型.md",
    "chars": 135281,
    "preview": "<center><h1>第9章  机器学习与统计模型</h1></center>\r\n\r\n> 内容:@若冰(马世拓)\r\n> \r\n> 审稿:@邢硕\r\n> \r\n> 排版&校对:@牧小熊(聂雄伟)\r\n\r\n这一章重点探讨统计模型和机器学习模型,两个"
  },
  {
    "path": "docs/README.md",
    "chars": 3503,
    "preview": "# 数学建模导论\r\n\r\n- 项目背景:本项目基于我的课程《数学建模导论——基于Python语言》整理而来,是为了帮助学生更好的学习数学建模、数据科学知识所用。可用于大学生参加数学建模竞赛,也可用作工作中的平时学习。万物皆可建模,人工智能的本"
  },
  {
    "path": "docs/_sidebar.md",
    "chars": 438,
    "preview": "- 绪论 [走进数学建模的大门](CH0/绪论-走进数学建模的大门.md)\n- 第1章 [解析方法与几何模型](CH1/第1章-解析方法与几何模型.md)\n- 第2章 [微分方程与动力系统](CH2/第2章-微分方程与动力系统.md)\n- "
  },
  {
    "path": "docs/index.html",
    "chars": 3014,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Datawhale 数学建模教程</title>\n  <meta http-equiv=\""
  }
]

// ... and 3 more files (download for full content)

About this extraction

This page contains the full source code of the datawhalechina/intro-mathmodel GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (599.4 KB), approximately 324.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!