diff --git a/INFO.md b/INFO.md index 8afbf3fdf..8b11935c8 100644 --- a/INFO.md +++ b/INFO.md @@ -30,15 +30,13 @@ sudo apt-get install librsvg2-bin ``` ``` -wget https://github.com/adobe-fonts/source-han-sans/raw/release/OTF/SourceHanSansSC.zip -wget https://github.com/adobe-fonts/source-han-serif/raw/release/OTF/SourceHanSerifSC_SB-H.zip -wget https://github.com/adobe-fonts/source-han-serif/raw/release/OTF/SourceHanSerifSC_EL-M.zip +wget https://github.com/adobe-fonts/source-han-sans/releases/download/2.004R/SourceHanSansSC.zip +wget -O SourceHanSerifSC.zip https://github.com/adobe-fonts/source-han-serif/releases/download/2.001R/09_SourceHanSerifSC.zip -unzip SourceHanSansSC.zip -unzip SourceHanSerifSC_EL-M.zip -unzip SourceHanSerifSC_SB-H.zip +unzip SourceHanSansSC.zip -d SourceHanSansSC +unzip SourceHanSerifSC.zip -d SourceHanSerifSC -sudo mv SourceHanSansSC SourceHanSerifSC_EL-M SourceHanSerifSC_SB-H /usr/share/fonts/opentype/ +sudo mv SourceHanSansSC SourceHanSerifSC /usr/share/fonts/opentype/ sudo fc-cache -f -v ``` diff --git a/Jenkinsfile b/Jenkinsfile index a46a9aac3..6749c2370 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -51,6 +51,13 @@ stage("Build and Publish") { ./static/cache.sh store _build/eval_tensorflow/data """ + sh label: "Execute Notebooks [Paddlepaddle]", script: """set -ex + conda activate ${ENV_NAME} + ./static/cache.sh restore _build/eval_paddle/data + d2lbook build eval --tab paddle + ./static/cache.sh store _build/eval_paddle/data + """ + sh label:"Build HTML", script:"""set -ex conda activate ${ENV_NAME} ./static/build_html.sh diff --git a/README.md b/README.md index cb1d965ec..f13b447de 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](http://ci.d2l.ai/job/d2l-zh/job/master/badge/icon)](http://ci.d2l.ai/job/d2l-zh/job/master/) -[第一版:zh-v1.D2L.ai](https://zh-v1.d2l.ai/) | [第二版预览版:zh.D2L.ai](https://zh.d2l.ai) | 安装和使用书中源代码:[第一版](https://zh-v1.d2l.ai/chapter_prerequisite/install.html) [第二版](https://zh.d2l.ai/chapter_installation/index.html) | 当前版本: v2.0.0-beta1 +[第二版:zh.D2L.ai](https://zh.d2l.ai) | [第一版:zh-v1.D2L.ai](https://zh-v1.d2l.ai/) | 安装和使用书中源代码: [第二版](https://zh.d2l.ai/chapter_installation/index.html) [第一版](https://zh-v1.d2l.ai/chapter_prerequisite/install.html)
- +
如果本书对你有帮助,请Star (★) 本仓库或引用本书的英文版: @@ -38,13 +38,11 @@ } ``` -## 本书的第二版 +## 本书的英文版 -虽然纸质书第一版已经出版,但深度学习领域依然在迅速发展。为了得到来自更广泛的英文开源社区的帮助,从而提升本书质量,本书的第二版正在用英文写。英文版正不断被搬回中文版中。 +虽然纸质书已出版,但深度学习领域依然在迅速发展。为了得到来自更广泛的英文开源社区的帮助,从而提升本书质量,本书的新版将继续用英文编写,并搬回中文版。 -目前,英文版已超过160节(中文版共96节),例如增加了理论背景(如优化收敛分析)、硬件设计(如参数服务器)、全新篇章(如注意力机制、推荐系统、深度学习的数学、生成对抗网络)、应用种类(如自然语言推断)、模型种类(如Transformer、BERT)等,并优化重组了大量章节(如将自然语言处理篇章按从预训练表征、到模型设计、再到下游应用重构)。 - -欢迎关注本书[第二版的英文开源项目](https://github.com/d2l-ai/d2l-en)。 +欢迎关注本书的[英文开源项目](https://github.com/d2l-ai/d2l-en)。 ## 中英文教学资源 @@ -73,7 +71,7 @@ > — 余凯,地平线公司创始人 & CEO >"强烈推荐这本书!我特别赞赏这种手脑一体的学习方式。"
-> — 漆远,蚂蚁金服副总裁、首席AI科学家 +> — 漆远,复旦大学“浩清”教授、人工智能创新与产业研究院院长 >"《动手学深度学习》是一本很容易让学习者上瘾的书。"
> — 沈强,将门创投创始合伙人 @@ -82,4 +80,4 @@ 感谢[社区贡献者们](https://github.com/d2l-ai/d2l-zh/graphs/contributors)为每一位读者改进这本开源书。 -[如何贡献](https://zh-v2.d2l.ai/chapter_appendix-tools-for-deep-learning/contributing.html) | [致谢](https://zh-v2.d2l.ai/chapter_preface/index.html) | [讨论或报告问题](https://discuss.d2l.ai/c/chinese-version/16) | [其他](INFO.md) +[如何贡献](https://zh.d2l.ai/chapter_appendix-tools-for-deep-learning/contributing.html) | [致谢](https://zh.d2l.ai/chapter_preface/index.html) | [讨论或报告问题](https://discuss.d2l.ai/c/chinese-version/16) | [其他](INFO.md) diff --git a/chapter_appendix-tools-for-deep-learning/aws.md b/chapter_appendix-tools-for-deep-learning/aws.md index 054aebe55..dfc52e386 100644 --- a/chapter_appendix-tools-for-deep-learning/aws.md +++ b/chapter_appendix-tools-for-deep-learning/aws.md @@ -1,7 +1,7 @@ # 使用Amazon EC2实例 :label:`sec_aws` -在本节中,我们将向你展示如何在原始Linux机器上安装所有库。回想一下,在 :numref:`sec_sagemaker`中,我们讨论了如何使用Amazon SageMaker,而在云上自己构建实例的成本更低。本演示包括三个步骤: +本节将展示如何在原始Linux机器上安装所有库。回想一下, :numref:`sec_sagemaker`讨论了如何使用Amazon SageMaker,而在云上自己构建实例的成本更低。本演示包括三个步骤。 1. 从AWS EC2请求GPU Linux实例。 1. 安装CUDA(或使用预装CUDA的Amazon机器映像)。 @@ -44,7 +44,7 @@ :width:`700px` :label:`fig_ubuntu` -EC2提供了许多不同的实例配置可供选择。对于初学者来说,这有时会让人感到困惑。 :numref:`tab_ec2`列出了不同合适的计算机。 +EC2提供了许多不同的实例配置可供选择。对初学者来说,这有时会让人感到困惑。 :numref:`tab_ec2`列出了不同合适的计算机。 :不同的EC2实例类型 @@ -152,7 +152,7 @@ echo "export LD_LIBRARY_PATH=\${LD_LIBRARY_PATH}:/usr/local/cuda/lib64" >> ~/.ba ## 安装库以运行代码 -要运行本书的代码,只需在EC2实例上为linux用户执行 :ref:`chap_installation`中的步骤,并使用以下提示在远程linux服务器上工作: +要运行本书的代码,只需在EC2实例上为linux用户执行 :ref:`chap_installation`中的步骤,并使用以下提示在远程linux服务器上工作。 * 要在Miniconda安装页面下载bash脚本,请右击下载链接并选择“copy Link address”,然后执行`wget [copied link address]`。 * 运行`~/miniconda3/bin/conda init`, 你可能需要执行`source~/.bashrc`,而不是关闭并重新打开当前shell。 @@ -185,7 +185,7 @@ jupyter notebook 由于云服务是按使用时间计费的,你应该关闭不使用的实例。请注意,还有其他选择: -* “Stopping”(停止)实例意味着你可以重新启动它。这类似于关闭常规服务器的电源。但是,停止的实例仍将按保留的硬盘空间收取少量费用。 +* “Stopping”(停止)实例意味着你可以重新启动它。这类似于关闭常规服务器的电源。但是,停止的实例仍将按保留的硬盘空间收取少量费用; * “Terminating”(终止)实例将删除与其关联的所有数据。这包括磁盘,因此你不能再次启动它。只有在你知道将来不需要它的情况下才这样做。 如果你想要将该实例用作更多实例的模板,请右击 :numref:`fig_connect`中的例子,然后选择“Image”$\rightarrow$“Create”以创建该实例的镜像。完成后,选择“实例状态”$\rightarrow$“终止”以终止实例。下次要使用此实例时,可以按照本节中的步骤基于保存的镜像创建实例。唯一的区别是,在 :numref:`fig_ubuntu`所示的“1.选择AMI”中,你必须使用左侧的“我的AMI”选项来选择你保存的镜像。创建的实例将保留镜像硬盘上存储的信息。例如,你不必重新安装CUDA和其他运行时环境。 diff --git a/chapter_appendix-tools-for-deep-learning/contributing.md b/chapter_appendix-tools-for-deep-learning/contributing.md index 311166b48..cb23cdf16 100644 --- a/chapter_appendix-tools-for-deep-learning/contributing.md +++ b/chapter_appendix-tools-for-deep-learning/contributing.md @@ -5,7 +5,7 @@ 如果你发现笔误、无效的链接、一些你认为我们遗漏了引文的地方, 代码看起来不优雅,或者解释不清楚的地方,请回复我们以帮助读者。 在常规书籍中,两次印刷之间的间隔(即修订笔误的间隔)常常需要几年, -但这本书的改进通常需要几个小时到几天的时间。 +但这本书的改进通常需要几小时到几天的时间。 由于版本控制和持续自动集成(CI)测试,这一切颇为高效。 为此,你需要向gihub存储库提交一个 [pull request](https://github.com/d2l-ai/d2l-en/pulls)。 @@ -45,6 +45,7 @@ 请使用`#@tab`来标记代码块的起始行。 例如`#@tab pytorch`用于一个PyTorch代码块, `#@tab tensorflow`用于一个TensorFlow代码块, +`#@tab paddle`用于一个PaddlePaddle代码块, 或者`#@tab all`是所有实现的共享代码块。 你可以参考[d2lbook](http://book.d2l.ai/user/code_tabs.html)包了解更多信息。 diff --git a/chapter_appendix-tools-for-deep-learning/d2l.md b/chapter_appendix-tools-for-deep-learning/d2l.md index 37995b865..e69d4584f 100644 --- a/chapter_appendix-tools-for-deep-learning/d2l.md +++ b/chapter_appendix-tools-for-deep-learning/d2l.md @@ -22,17 +22,23 @@ ``` :end_tab: +:begin_tab:`paddle` +```eval_rst +.. currentmodule:: d2l.paddle +``` +:end_tab: + ## 模型 -```eval_rst +```eval_rst .. autoclass:: Module - :members: + :members: .. autoclass:: LinearRegressionScratch :members: .. autoclass:: LinearRegression - :members: + :members: .. autoclass:: Classification :members: @@ -40,12 +46,12 @@ ## 数据 -```eval_rst +```eval_rst .. autoclass:: DataModule - :members: + :members: .. autoclass:: SyntheticRegressionData - :members: + :members: .. autoclass:: FashionMNIST :members: @@ -53,9 +59,9 @@ ## 训练 -```eval_rst +```eval_rst .. autoclass:: Trainer - :members: + :members: .. autoclass:: SGD :members: @@ -63,7 +69,7 @@ ## 公用 -```eval_rst +```eval_rst .. autofunction:: add_to_class .. autofunction:: cpu @@ -73,7 +79,7 @@ .. autofunction:: num_gpus .. autoclass:: ProgressBoard - :members: + :members: .. autoclass:: HyperParameters :members: diff --git a/chapter_appendix-tools-for-deep-learning/index.md b/chapter_appendix-tools-for-deep-learning/index.md index 08139bae4..f5f6a843e 100644 --- a/chapter_appendix-tools-for-deep-learning/index.md +++ b/chapter_appendix-tools-for-deep-learning/index.md @@ -1,7 +1,7 @@ # 附录:深度学习工具 :label:`chap_appendix_tools` -为了充分利用《动手学深度学习》,我们将在本附录中介绍不同工具, +为了充分利用《动手学深度学习》,本书将在本附录中介绍不同工具, 例如如何运行这本交互式开源书籍和为本书做贡献。 ```toc diff --git a/chapter_appendix-tools-for-deep-learning/jupyter.md b/chapter_appendix-tools-for-deep-learning/jupyter.md index b89d30c81..5d6428cbe 100644 --- a/chapter_appendix-tools-for-deep-learning/jupyter.md +++ b/chapter_appendix-tools-for-deep-learning/jupyter.md @@ -1,4 +1,4 @@ -# 使用Jupyter Notebooks +# 使用Jupyter Notebook :label:`sec_jupyter` 本节介绍如何使用Jupyter Notebook编辑和运行本书各章中的代码。确保你已按照 :ref:`chap_installation`中的说明安装了Jupyter并下载了代码。如果你想了解更多关于Jupyter的信息,请参阅其[文档](https://jupyter.readthedocs.io/en/latest/)中的优秀教程。 @@ -60,7 +60,7 @@ 首先,安装notedown插件,运行Jupyter Notebook并加载插件: ``` -pip install mu-notedown # 你可能需要卸载原始notedown +pip install d2l-notedown # 你可能需要卸载原始notedown jupyter notebook --NotebookApp.contents_manager_class='notedown.NotedownContentsManager' ``` @@ -86,7 +86,7 @@ c.NotebookApp.contents_manager_class = 'notedown.NotedownContentsManager' ssh myserver -L 8888:localhost:8888 ``` -以上是远程服务器`myserver`的地址。然后我们可以使用http://localhost:8888 访问运行Jupyter Notebook的远程服务器`myserver`。在下一节中,我们将详细介绍如何在AWS实例上运行Jupyter Notebook。 +以上是远程服务器`myserver`的地址。然后我们可以使用http://localhost:8888 访问运行Jupyter Notebook的远程服务器`myserver`。下一节将详细介绍如何在AWS实例上运行Jupyter Notebook。 ### 执行时间 diff --git a/chapter_appendix-tools-for-deep-learning/sagemaker.md b/chapter_appendix-tools-for-deep-learning/sagemaker.md index f7ff770c4..5799beae2 100644 --- a/chapter_appendix-tools-for-deep-learning/sagemaker.md +++ b/chapter_appendix-tools-for-deep-learning/sagemaker.md @@ -1,7 +1,7 @@ # 使用Amazon SageMaker :label:`sec_sagemaker` -深度学习应用程序可能需要很多计算资源,这很容易超出你的本地计算机所能提供的范围。云计算服务允许你使用功能更强大的计算机更轻松地运行本书的GPU密集型代码。本节将介绍如何使用Amazon SageMaker运行本书的代码。 +深度学习程序可能需要很多计算资源,这很容易超出你的本地计算机所能提供的范围。云计算服务允许你使用功能更强大的计算机更轻松地运行本书的GPU密集型代码。本节将介绍如何使用Amazon SageMaker运行本书的代码。 ## 注册 @@ -79,27 +79,36 @@ SageMaker提供多个具有不同计算能力和价格的[实例类型](https:// 你可能希望在从远程存储库提取更新之前提交本地更改。否则,只需在终端中使用以下命令放弃所有本地更改: :begin_tab:`mxnet` + ```bash cd SageMaker/d2l-en-sagemaker/ git reset --hard git pull ``` + + :end_tab: :begin_tab:`pytorch` + ```bash cd SageMaker/d2l-pytorch-sagemaker/ git reset --hard git pull ``` + + :end_tab: :begin_tab:`tensorflow` + ```bash cd SageMaker/d2l-tensorflow-sagemaker/ git reset --hard git pull ``` + + :end_tab: ## 小结 diff --git a/chapter_appendix-tools-for-deep-learning/selecting-servers-gpus.md b/chapter_appendix-tools-for-deep-learning/selecting-servers-gpus.md index 792e9b860..2beaf0093 100644 --- a/chapter_appendix-tools-for-deep-learning/selecting-servers-gpus.md +++ b/chapter_appendix-tools-for-deep-learning/selecting-servers-gpus.md @@ -5,20 +5,20 @@ ## 选择服务器 -通常不需要购买具有多个线程的高端CPU,因为大部分计算都发生在GPU上。这就是说,由于Python中的全局解释器锁(GIL),CPU的单线程性能在我们有4-8个GPU的情况下可能很重要。所有的条件都是一样的,这意味着核数较少但时钟频率较高的CPU可能是更经济的选择。例如,当在6核4GHz和8核3.5GHz CPU之间进行选择时,前者更可取,即使其聚合速度较低。一个重要的考虑因素是,GPU使用大量的功率,从而消耗大量的热量。这需要非常好的冷却和足够大的机箱来使用GPU。如有可能,请遵循以下指南: +通常不需要购买具有多个线程的高端CPU,因为大部分计算都发生在GPU上。这就是说,由于Python中的全局解释器锁(GIL),CPU的单线程性能在有4-8个GPU的情况下可能很重要。所有的条件都是一样的,这意味着核数较少但时钟频率较高的CPU可能是更经济的选择。例如,当在6核4GHz和8核3.5GHz CPU之间进行选择时,前者更可取,即使其聚合速度较低。一个重要的考虑因素是,GPU使用大量的电能,从而释放大量的热量。这需要非常好的冷却和足够大的机箱来容纳GPU。如有可能,请遵循以下指南: -1. **电源**。GPU使用大量的电源。每个设备预计高达350W(检查显卡的*峰值需求*而不是一般需求,因为高效代码可能会消耗大量能源)。如果你的电源不能满足需求,你会发现系统变得不稳定。 +1. **电源**。GPU使用大量的电源。每个设备预计高达350W(检查显卡的*峰值需求*而不是一般需求,因为高效代码可能会消耗大量能源)。如果电源不能满足需求,系统会变得不稳定。 1. **机箱尺寸**。GPU很大,辅助电源连接器通常需要额外的空间。此外,大型机箱更容易冷却。 -1. **GPU散热**。如果你有大量的GPU,你可能需要投资水冷。此外,即使风扇较少,也应以“公版设计”为目标,因为它们足够薄,可以在设备之间进气。如果你购买的是多风扇GPU,安装多个GPU时,它可能太厚而无法获得足够的空气,你将遇到热气流。 -1. **PCIe插槽**。在GPU之间来回移动数据(以及在GPU之间交换数据)需要大量带宽。我们建议使用16通道的PCIe 3.0插槽。如果你安装了多个GPU,请务必仔细阅读主板说明,以确保在同时使用多个GPU时16$\times$带宽仍然可用,并且你使用的是PCIe3.0,而不是用于附加插槽的PCIe2.0。在安装多个GPU的情况下,一些主板的带宽降级到8$\times$甚至4$\times$。这部分是由于CPU提供的PCIe通道数量限制。 +1. **GPU散热**。如果有大量的GPU,可能需要投资水冷。此外,即使风扇较少,也应以“公版设计”为目标,因为它们足够薄,可以在设备之间进气。当使用多风扇GPU,安装多个GPU时,它可能太厚而无法获得足够的空气。 +1. **PCIe插槽**。在GPU之间来回移动数据(以及在GPU之间交换数据)需要大量带宽。建议使用16通道的PCIe 3.0插槽。当安装了多个GPU时,请务必仔细阅读主板说明,以确保在同时使用多个GPU时16$\times$带宽仍然可用,并且使用的是PCIe3.0,而不是用于附加插槽的PCIe2.0。在安装多个GPU的情况下,一些主板的带宽降级到8$\times$甚至4$\times$。这部分是由于CPU提供的PCIe通道数量限制。 -简而言之,以下是构建深度学习服务器的一些建议: +简而言之,以下是构建深度学习服务器的一些建议。 -* **初学者**。购买低功耗的低端GPU(适合深度学习的廉价游戏GPU,功耗150-200W)。如果幸运的话,你当前的计算机将支持它。 +* **初学者**。购买低功耗的低端GPU(适合深度学习的廉价游戏GPU,功耗150-200W)。如果幸运的话,大家现在常用的计算机将支持它。 * **1个GPU**。一个4核的低端CPU就足够了,大多数主板也足够了。以至少32 GB的DRAM为目标,投资SSD进行本地数据访问。600W的电源应足够。买一个有很多风扇的GPU。 -* **2个GPU**。一个4-6核的低端CPU就足够了。你可以瞄准64 GB的DRAM并投资于SSD。对于两个高端GPU,你将需要1000瓦的功率。对于主板,请确保它们具有*两个*PCIe 3.0 x16插槽。如果可以,请使用PCIe 3.0 x16插槽之间有两个可用空间(60毫米间距)的主板,以提供额外的空气。在这种情况下,购买两个具有大量风扇的GPU。 -* **4个GPU**。确保你购买的CPU具有相对较快的单线程速度(即较高的时钟频率)。你可能需要具有更多PCIe通道的CPU,例如AMD Threadripper。你可能需要相对昂贵的主板才能获得4个PCIe 3.0 x16插槽,因为它们可能需要一个PLX来多路复用PCIe通道。购买带有公版设计的GPU,这些GPU很窄,并且让空气进入GPU之间。你需要一个1600-2000W的电源,而你办公室的插座可能不支持。此服务器可能在运行时*声音很大,很热*。你不想把它放在桌子下面。建议使用128 GB的DRAM。获取一个用于本地存储的SSD(1-2 TB NVMe)和RAID配置的硬盘来存储数据。 -* **8 GPU**。你需要购买带有多个冗余电源的专用多GPU服务器机箱(例如,每个电源为1600W时为2+1)。这将需要双插槽服务器CPU、256 GB ECC DRAM、快速网卡(建议使用10 GBE),并且你需要检查服务器是否支持GPU的*物理外形*。用户GPU和服务器GPU之间的气流和布线位置存在显著差异(例如RTX 2080和Tesla V100)。这意味着你可能无法在服务器中安装消费级GPU,因为电源线间隙不足或缺少合适的接线(本书一位合著者痛苦地发现了这一点)。 +* **2个GPU**。一个4-6核的低端CPU就足够了。可以考虑64 GB的DRAM并投资于SSD。两个高端GPU将需要1000瓦的功率。对于主板,请确保它们具有*两个*PCIe 3.0 x16插槽。如果可以,请使用PCIe 3.0 x16插槽之间有两个可用空间(60毫米间距)的主板,以提供额外的空气。在这种情况下,购买两个具有大量风扇的GPU。 +* **4个GPU**。确保购买的CPU具有相对较快的单线程速度(即较高的时钟频率)。可能需要具有更多PCIe通道的CPU,例如AMD Threadripper。可能需要相对昂贵的主板才能获得4个PCIe 3.0 x16插槽,因为它们可能需要一个PLX来多路复用PCIe通道。购买带有公版设计的GPU,这些GPU很窄,并且让空气进入GPU之间。需要一个1600-2000W的电源,而办公室的插座可能不支持。此服务器可能在运行时*声音很大,很热*。不想把它放在桌子下面。建议使用128 GB的DRAM。获取一个用于本地存储的SSD(1-2 TB NVMe)和RAID配置的硬盘来存储数据。 +* **8 GPU**。需要购买带有多个冗余电源的专用多GPU服务器机箱(例如,每个电源为1600W时为2+1)。这将需要双插槽服务器CPU、256 GB ECC DRAM、快速网卡(建议使用10 GBE),并且需要检查服务器是否支持GPU的*物理外形*。用户GPU和服务器GPU之间的气流和布线位置存在显著差异(例如RTX 2080和Tesla V100)。这意味着可能无法在服务器中安装消费级GPU,因为电源线间隙不足或缺少合适的接线(本书一位合著者痛苦地发现了这一点)。 ## 选择GPU @@ -26,25 +26,25 @@ NVIDIA提供两种类型的GPU,针对个人用户(例如,通过GTX和RTX系列)和企业用户(通过其Tesla系列)。这两种类型的GPU提供了相当的计算能力。但是,企业用户GPU通常使用强制(被动)冷却、更多内存和ECC(纠错)内存。这些GPU更适用于数据中心,通常成本是消费者GPU的十倍。 -如果你是一个拥有100个服务器的大公司,你应该考虑英伟达Tesla系列,或者在云中使用GPU服务器。对于实验室或10+服务器的中小型公司,英伟达RTX系列可能是最具成本效益的。你可以购买超微或华硕机箱的预配置服务器,这些服务器可以有效地容纳4-8个GPU。 +如果是一个拥有100个服务器的大公司,则应该考虑英伟达Tesla系列,或者在云中使用GPU服务器。对于实验室或10+服务器的中小型公司,英伟达RTX系列可能是最具成本效益的,可以购买超微或华硕机箱的预配置服务器,这些服务器可以有效地容纳4-8个GPU。 GPU供应商通常每一到两年发布一代,例如2017年发布的GTX 1000(Pascal)系列和2019年发布的RTX 2000(Turing)系列。每个系列都提供几种不同的型号,提供不同的性能级别。GPU性能主要是以下三个参数的组合: -1. **计算能力**。通常我们追求32位浮点计算能力。16位浮点训练(FP16)也进入主流。如果你只对预测感兴趣,还可以使用8位整数。最新一代图灵GPU提供4-bit加速。不幸的是,目前训练低精度网络的算法还没有普及。 -1. **内存大小**。随着你的模型变大或训练期间使用的批量变大,你将需要更多的GPU内存。检查HBM2(高带宽内存)与GDDR6(图形DDR)内存。HBM2速度更快,但成本更高。 -1. **内存带宽**。只有当你有足够的内存带宽时,你才能最大限度地利用你的计算能力。如果使用GDDR6,请追求宽内存总线。 +1. **计算能力**。通常大家会追求32位浮点计算能力。16位浮点训练(FP16)也进入主流。如果只对预测感兴趣,还可以使用8位整数。最新一代图灵GPU提供4-bit加速。不幸的是,目前训练低精度网络的算法还没有普及; +1. **内存大小**。随着模型变大或训练期间使用的批量变大,将需要更多的GPU内存。检查HBM2(高带宽内存)与GDDR6(图形DDR)内存。HBM2速度更快,但成本更高; +1. **内存带宽**。当有足够的内存带宽时,才能最大限度地利用计算能力。如果使用GDDR6,请追求宽内存总线。 -对于大多数用户来说,只需看看计算能力就足够了。请注意,许多GPU提供不同类型的加速。例如,NVIDIA的Tensor Cores将操作符子集的速度提高了5$\times$。确保你的库支持这一点。GPU内存应不小于4GB(8GB更好)。尽量避免将GPU也用于显示GUI(改用内置显卡)。如果无法避免,请添加额外的2GB RAM以确保安全。 +对于大多数用户,只需看看计算能力就足够了。请注意,许多GPU提供不同类型的加速。例如,NVIDIA的Tensor Cores将操作符子集的速度提高了5$\times$。确保所使用的库支持这一点。GPU内存应不小于4GB(8GB更好)。尽量避免将GPU也用于显示GUI(改用内置显卡)。如果无法避免,请添加额外的2GB RAM以确保安全。 :numref:`fig_flopsvsprice`比较了各种GTX 900、GTX 1000和RTX 2000系列的(GFlops)和价格(Price)。价格是维基百科上的建议价格。 ![浮点计算能力和价格比较](../img/flopsvsprice.svg) :label:`fig_flopsvsprice` -我们可以看到很多事情: +由上图,可以看出很多事情: -1. 在每个系列中,价格和性能大致成比例。Titan因拥有大GPU内存而有相当的溢价。然而,通过比较980 Ti和1080 Ti可以看出,较新型号具有更好的成本效益。RTX 2000系列的价格似乎没有多大提高。然而,它们提供了更优秀的低精度性能(FP16、INT8和INT4)。 -2. GTX 1000系列的性价比大约是900系列的两倍。 +1. 在每个系列中,价格和性能大致成比例。Titan因拥有大GPU内存而有相当的溢价。然而,通过比较980 Ti和1080 Ti可以看出,较新型号具有更好的成本效益。RTX 2000系列的价格似乎没有多大提高。然而,它们提供了更优秀的低精度性能(FP16、INT8和INT4); +2. GTX 1000系列的性价比大约是900系列的两倍; 3. 对于RTX 2000系列,浮点计算能力是价格的“仿射”函数。 ![浮点计算能力和能耗](../img/wattvsprice.svg) @@ -55,7 +55,7 @@ GPU供应商通常每一到两年发布一代,例如2017年发布的GTX 1000 ## 小结 * 在构建服务器时,请注意电源、PCIe总线通道、CPU单线程速度和散热。 -* 如果可能,你应该购买最新一代的GPU。 +* 如果可能,应该购买最新一代的GPU。 * 使用云进行大型部署。 -* 高密度服务器可能不与所有GPU兼容。在你购买之前,请检查一下机械和散热规格。 +* 高密度服务器可能不与所有GPU兼容。在购买之前,请检查一下机械和散热规格。 * 为提高效率,请使用FP16或更低的精度。 diff --git a/chapter_attention-mechanisms/attention-cues.md b/chapter_attention-mechanisms/attention-cues.md index 695389441..443409819 100644 --- a/chapter_attention-mechanisms/attention-cues.md +++ b/chapter_attention-mechanisms/attention-cues.md @@ -1,54 +1,54 @@ # 注意力提示 :label:`sec_attention-cues` -感谢你对本书的关注,因为你的注意力是一种稀缺的资源: -此刻你正在阅读本书(而忽略了其他的书), -因此你的注意力是用机会成本(与金钱类似)来支付的。 -为了确保你现在投入的注意力是值得的, -我们尽全力(全部的注意力)创作一本好书。 +感谢读者对本书的关注,因为读者的注意力是一种稀缺的资源: +此刻读者正在阅读本书(而忽略了其他的书), +因此读者的注意力是用机会成本(与金钱类似)来支付的。 +为了确保读者现在投入的注意力是值得的, +作者们尽全力(全部的注意力)创作一本好书。 -自经济学研究稀缺资源分配以来,我们正处在“注意力经济”时代, +自经济学研究稀缺资源分配以来,人们正处在“注意力经济”时代, 即人类的注意力被视为可以交换的、有限的、有价值的且稀缺的商品。 许多商业模式也被开发出来去利用这一点: -在音乐或视频流媒体服务上,我们要么消耗注意力在广告上,要么付钱来隐藏广告; -为了在网络游戏世界的成长,我们要么消耗注意力在游戏战斗中, +在音乐或视频流媒体服务上,人们要么消耗注意力在广告上,要么付钱来隐藏广告; +为了在网络游戏世界的成长,人们要么消耗注意力在游戏战斗中, 从而帮助吸引新的玩家,要么付钱立即变得强大。 总之,注意力不是免费的。 注意力是稀缺的,而环境中的干扰注意力的信息却并不少。 -比如我们的视觉神经系统大约每秒收到$10^8$位的信息, +比如人类的视觉神经系统大约每秒收到$10^8$位的信息, 这远远超过了大脑能够完全处理的水平。 -幸运的是,我们的祖先已经从经验(也称为数据)中认识到 +幸运的是,人类的祖先已经从经验(也称为数据)中认识到 “并非感官的所有输入都是一样的”。 在整个人类历史中,这种只将注意力引向感兴趣的一小部分信息的能力, -使我们的大脑能够更明智地分配资源来生存、成长和社交, +使人类的大脑能够更明智地分配资源来生存、成长和社交, 例如发现天敌、找寻食物和伴侣。 ## 生物学中的注意力提示 注意力是如何应用于视觉世界中的呢? -我们从当今十分普及的*双组件*(two-component)的框架开始讲起: +这要从当今十分普及的*双组件*(two-component)的框架开始讲起: 这个框架的出现可以追溯到19世纪90年代的威廉·詹姆斯, 他被认为是“美国心理学之父” :cite:`James.2007`。 在这个框架中,受试者基于*非自主性提示*和*自主性提示* 有选择地引导注意力的焦点。 非自主性提示是基于环境中物体的突出性和易见性。 -想象一下,假如你面前有五个物品: +想象一下,假如我们面前有五个物品: 一份报纸、一篇研究论文、一杯咖啡、一本笔记本和一本书, 就像 :numref:`fig_eye-coffee`。 所有纸制品都是黑白印刷的,但咖啡杯是红色的。 换句话说,这个咖啡杯在这种视觉环境中是突出和显眼的, 不由自主地引起人们的注意。 -所以你把视力最敏锐的地方放到咖啡上, +所以我们会把视力最敏锐的地方放到咖啡上, 如 :numref:`fig_eye-coffee`所示。 ![由于突出性的非自主性提示(红杯子),注意力不自主地指向了咖啡杯](../img/eye-coffee.svg) :width:`400px` :label:`fig_eye-coffee` -喝咖啡后,你会变得兴奋并想读书。 -所以你转过头,重新聚焦你的眼睛,然后看看书, +喝咖啡后,我们会变得兴奋并想读书, +所以转过头,重新聚焦眼睛,然后看看书, 就像 :numref:`fig_eye-book`中描述那样。 与 :numref:`fig_eye-coffee`中由于突出性导致的选择不同, 此时选择书是受到了认知和意识的控制, @@ -62,24 +62,24 @@ ## 查询、键和值 自主性的与非自主性的注意力提示解释了人类的注意力的方式, -下面我们看看如何通过这两种注意力提示, +下面来看看如何通过这两种注意力提示, 用神经网络来设计注意力机制的框架, 首先,考虑一个相对简单的状况, 即只使用非自主性提示。 要想将选择偏向于感官输入, -我们可以简单地使用参数化的全连接层, +则可以简单地使用参数化的全连接层, 甚至是非参数化的最大汇聚层或平均汇聚层。 因此,“是否包含自主性提示”将注意力机制与全连接层或汇聚层区别开来。 -在注意力机制的背景下,我们将自主性提示称为*查询*(query)。 +在注意力机制的背景下,自主性提示被称为*查询*(query)。 给定任何查询,注意力机制通过*注意力汇聚*(attention pooling) 将选择引导至*感官输入*(sensory inputs,例如中间特征表示)。 在注意力机制中,这些感官输入被称为*值*(value)。 更通俗的解释,每个值都与一个*键*(key)配对, 这可以想象为感官输入的非自主提示。 -如 :numref:`fig_qkv`所示,我们可以设计注意力汇聚, -以便给定的查询(自主性提示)可以与键(非自主性提示)进行匹配, +如 :numref:`fig_qkv`所示,可以通过设计注意力汇聚的方式, +便于给定的查询(自主性提示)与键(非自主性提示)进行匹配, 这将引导得出最匹配的值(感官输入)。 ![注意力机制通过注意力汇聚将*查询*(自主性提示)和*键*(非自主性提示)结合在一起,实现对*值*(感官输入)的选择倾向](../img/qkv.svg) @@ -88,7 +88,7 @@ 鉴于上面所提框架在 :numref:`fig_qkv`中的主导地位, 因此这个框架下的模型将成为本章的中心。 然而,注意力机制的设计有许多替代方案。 -例如,我们可以设计一个不可微的注意力模型, +例如可以设计一个不可微的注意力模型, 该模型可以使用强化学习方法 :cite:`Mnih.Heess.Graves.ea.2014`进行训练。 ## 注意力的可视化 @@ -116,7 +116,15 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` -为了可视化注意力权重,我们定义了`show_heatmaps`函数。 +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +``` + +为了可视化注意力权重,需要定义一个`show_heatmaps`函数。 其输入`matrices`的形状是 (要显示的行数,要显示的列数,查询的数目,键的数目)。 @@ -142,7 +150,7 @@ def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), fig.colorbar(pcm, ax=axes, shrink=0.6); ``` -下面我们使用一个简单的例子进行演示。 +下面使用一个简单的例子进行演示。 在本例子中,仅当查询和键相同时,注意力权重为1,否则为0。 ```{.python .input} @@ -151,7 +159,7 @@ attention_weights = d2l.reshape(d2l.eye(10), (1, 1, 10, 10)) show_heatmaps(attention_weights, xlabel='Keys', ylabel='Queries') ``` -在后面的章节中,我们将经常调用`show_heatmaps`函数来显示注意力权重。 +后面的章节内容将经常调用`show_heatmaps`函数来显示注意力权重。 ## 小结 @@ -160,7 +168,7 @@ show_heatmaps(attention_weights, xlabel='Keys', ylabel='Queries') * 注意力机制与全连接层或者汇聚层的区别源于增加的自主提示。 * 由于包含了自主性提示,注意力机制与全连接的层或汇聚层不同。 * 注意力机制通过注意力汇聚使选择偏向于值(感官输入),其中包含查询(自主性提示)和键(非自主性提示)。键和值是成对的。 -* 我们可以可视化查询和键之间的注意力权重。 +* 可视化查询和键之间的注意力权重是可行的。 ## 练习 @@ -179,4 +187,6 @@ show_heatmaps(attention_weights, xlabel='Keys', ylabel='Queries') [Discussions](https://discuss.d2l.ai/t/5765) :end_tab: - +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11839) +:end_tab: \ No newline at end of file diff --git a/chapter_attention-mechanisms/attention-scoring-functions.md b/chapter_attention-mechanisms/attention-scoring-functions.md index 62b2a0c69..61311f21f 100644 --- a/chapter_attention-mechanisms/attention-scoring-functions.md +++ b/chapter_attention-mechanisms/attention-scoring-functions.md @@ -1,16 +1,15 @@ # 注意力评分函数 :label:`sec_attention-scoring-functions` -在 :numref:`sec_nadaraya-watson`中, -我们使用高斯核来对查询和键之间的关系建模。 -我们可以将 :eqref:`eq_nadaraya-watson-gaussian`中的 -高斯核指数部分视为*注意力评分函数*(attention scoring function), + :numref:`sec_nadaraya-watson`使用了高斯核来对查询和键之间的关系建模。 + :eqref:`eq_nadaraya-watson-gaussian`中的 +高斯核指数部分可以视为*注意力评分函数*(attention scoring function), 简称*评分函数*(scoring function), 然后把这个函数的输出结果输入到softmax函数中进行运算。 -通过上述步骤,我们将得到与键对应的值的概率分布(即注意力权重)。 +通过上述步骤,将得到与键对应的值的概率分布(即注意力权重)。 最后,注意力汇聚的输出就是基于这些注意力权重的值的加权和。 -从宏观来看,我们可以使用上述算法来实现 +从宏观来看,上述算法可以用来实现 :numref:`fig_qkv`中的注意力机制框架。 :numref:`fig_attention_output`说明了 如何将注意力汇聚的输出计算成为值的加权和, @@ -32,14 +31,14 @@ $$f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_ :eqlabel:`eq_attn-pooling` 其中查询$\mathbf{q}$和键$\mathbf{k}_i$的注意力权重(标量) -是通过注意力评分函数$a$ 将两个向量映射成标量, +是通过注意力评分函数$a$将两个向量映射成标量, 再经过softmax运算得到的: $$\alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}_i)) = \frac{\exp(a(\mathbf{q}, \mathbf{k}_i))}{\sum_{j=1}^m \exp(a(\mathbf{q}, \mathbf{k}_j))} \in \mathbb{R}.$$ :eqlabel:`eq_attn-scoring-alpha` -正如我们所看到的,选择不同的注意力评分函数$a$会导致不同的注意力汇聚操作。 -在本节中,我们将介绍两个流行的评分函数,稍后将用他们来实现更复杂的注意力机制。 +正如上图所示,选择不同的注意力评分函数$a$会导致不同的注意力汇聚操作。 +本节将介绍两个流行的评分函数,稍后将用他们来实现更复杂的注意力机制。 ```{.python .input} import math @@ -63,6 +62,15 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import math +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` ## [**掩蔽softmax操作**] @@ -71,10 +79,10 @@ import tensorflow as tf 例如,为了在 :numref:`sec_machine_translation`中高效处理小批量数据集, 某些文本序列被填充了没有意义的特殊词元。 为了仅将有意义的词元作为值来获取注意力汇聚, -我们可以指定一个有效序列长度(即词元的个数), +可以指定一个有效序列长度(即词元的个数), 以便在计算softmax时过滤掉超出指定范围的位置。 -通过这种方式,我们可以在下面的`masked_softmax`函数中 -实现这样的*掩蔽softmax操作*(masked softmax operation), +下面的`masked_softmax`函数 +实现了这样的*掩蔽softmax操作*(masked softmax operation), 其中任何超出有效长度的位置都被掩蔽并置为0。 ```{.python .input} @@ -137,6 +145,26 @@ def masked_softmax(X, valid_lens): return tf.nn.softmax(tf.reshape(X, shape=shape), axis=-1) ``` +```{.python .input} +#@tab paddle +#@save +def masked_softmax(X, valid_lens): + """通过在最后一个轴上掩蔽元素来执行softmax操作""" + # X:3D张量,valid_lens:1D或2D张量 + if valid_lens is None: + return nn.functional.softmax(X, axis=-1) + else: + shape = X.shape + if valid_lens.dim() == 1: + valid_lens = paddle.repeat_interleave(valid_lens, shape[1]) + else: + valid_lens = valid_lens.reshape((-1,)) + # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 + X = d2l.sequence_mask(X.reshape((-1, shape[-1])), valid_lens, + value=-1e6) + return nn.functional.softmax(X.reshape(shape), axis=-1) +``` + 为了[**演示此函数是如何工作**]的, 考虑由两个$2 \times 4$矩阵表示的样本, 这两个样本的有效长度分别为$2$和$3$。 @@ -156,7 +184,12 @@ masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3])) masked_softmax(tf.random.uniform(shape=(2, 2, 4)), tf.constant([2, 3])) ``` -同样,我们也可以使用二维张量,为矩阵样本中的每一行指定有效长度。 +```{.python .input} +#@tab paddle +masked_softmax(paddle.rand((2, 2, 4)), paddle.to_tensor([2, 3])) +``` + +同样,也可以使用二维张量,为矩阵样本中的每一行指定有效长度。 ```{.python .input} masked_softmax(np.random.uniform(size=(2, 2, 4)), @@ -173,11 +206,15 @@ masked_softmax(torch.rand(2, 2, 4), d2l.tensor([[1, 3], [2, 4]])) masked_softmax(tf.random.uniform(shape=(2, 2, 4)), tf.constant([[1, 3], [2, 4]])) ``` +```{.python .input} +#@tab paddle +masked_softmax(paddle.rand((2, 2, 4)), paddle.to_tensor([[1, 3], [2, 4]])) +``` + ## [**加性注意力**] :label:`subsec_additive-attention` -一般来说,当查询和键是不同长度的矢量时, -我们可以使用加性注意力作为评分函数。 +一般来说,当查询和键是不同长度的矢量时,可以使用加性注意力作为评分函数。 给定查询$\mathbf{q} \in \mathbb{R}^q$和 键$\mathbf{k} \in \mathbb{R}^k$, *加性注意力*(additive attention)的评分函数为 @@ -193,7 +230,7 @@ $\mathbf w_v\in\mathbb R^{h}$。 感知机包含一个隐藏层,其隐藏单元数是一个超参数$h$。 通过使用$\tanh$作为激活函数,并且禁用偏置项。 -下面我们来实现加性注意力。 +下面来实现加性注意力。 ```{.python .input} #@save @@ -282,7 +319,35 @@ class AdditiveAttention(tf.keras.layers.Layer): self.attention_weights, **kwargs), values) ``` -我们用一个小例子来[**演示上面的`AdditiveAttention`类**], +```{.python .input} +#@tab paddle +#@save +class AdditiveAttention(nn.Layer): + """加性注意力""" + def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): + super(AdditiveAttention, self).__init__(**kwargs) + self.W_k = nn.Linear(key_size, num_hiddens, bias_attr=False) + self.W_q = nn.Linear(query_size, num_hiddens, bias_attr=False) + self.w_v = nn.Linear(num_hiddens, 1, bias_attr=False) + self.dropout = nn.Dropout(dropout) + + def forward(self, queries, keys, values, valid_lens): + queries, keys = self.W_q(queries), self.W_k(keys) + # 在维度扩展后, + # queries的形状:(batch_size,查询的个数,1,num_hidden) + # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens) + # 使用广播方式进行求和 + features = queries.unsqueeze(2) + keys.unsqueeze(1) + features = paddle.tanh(features) + # self.w_v仅有一个输出,因此从形状中移除最后那个维度。 + # scores的形状:(batch_size,查询的个数,“键-值”对的个数) + scores = self.w_v(features).squeeze(-1) + self.attention_weights = masked_softmax(scores, valid_lens) + # values的形状:(batch_size,“键-值”对的个数,值的维度) + return paddle.bmm(self.dropout(self.attention_weights), values) +``` + +用一个小例子来[**演示上面的`AdditiveAttention`类**], 其中查询、键和值的形状为(批量大小,步数或词元序列长度,特征大小), 实际输出为$(2,1,20)$、$(2,10,2)$和$(2,10,4)$。 注意力汇聚输出的形状为(批量大小,查询的步数,值的维度)。 @@ -325,6 +390,19 @@ attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8, attention(queries, keys, values, valid_lens, training=False) ``` +```{.python .input} +#@tab paddle +queries, keys = paddle.normal(0, 1, (2, 1, 20)), paddle.ones((2, 10, 2)) +# values的小批量,两个值矩阵是相同的 +values = paddle.arange(40, dtype=paddle.float32).reshape((1, 10, 4)).tile( + [2, 1, 1]) +valid_lens = paddle.to_tensor([2, 6]) + +attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8, + dropout=0.1) +attention.eval() +attention(queries, keys, values, valid_lens) +``` 尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的, 所以[**注意力权重**]是均匀的,由指定的有效长度决定。 @@ -344,7 +422,7 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), 那么两个向量的点积的均值为$0$,方差为$d$。 为确保无论向量长度如何, 点积的方差在不考虑向量长度的情况下仍然是$1$, -我们将点积除以$\sqrt{d}$, +我们再将点积除以$\sqrt{d}$, 则*缩放点积注意力*(scaled dot-product attention)评分函数为: $$a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}.$$ @@ -359,7 +437,7 @@ $$a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}.$$ $$ \mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}.$$ :eqlabel:`eq_softmax_QK_V` -在下面的缩放点积注意力的实现中,我们使用了暂退法进行模型正则化。 +下面的缩放点积注意力的实现使用了暂退法进行模型正则化。 ```{.python .input} #@save @@ -423,6 +501,27 @@ class DotProductAttention(tf.keras.layers.Layer): return tf.matmul(self.dropout(self.attention_weights, **kwargs), values) ``` +```{.python .input} +#@tab paddle +#@save +class DotProductAttention(nn.Layer): + """缩放点积注意力""" + def __init__(self, dropout, **kwargs): + super(DotProductAttention, self).__init__(**kwargs) + self.dropout = nn.Dropout(dropout) + + # queries的形状:(batch_size,查询的个数,d) + # keys的形状:(batch_size,“键-值”对的个数,d) + # values的形状:(batch_size,“键-值”对的个数,值的维度) + # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数) + def forward(self, queries, keys, values, valid_lens=None): + d = queries.shape[-1] + # 设置transpose_b=True为了交换keys的最后两个维度 + scores = paddle.bmm(queries, keys.transpose((0,2,1))) / math.sqrt(d) + self.attention_weights = masked_softmax(scores, valid_lens) + return paddle.bmm(self.dropout(self.attention_weights), values) +``` + 为了[**演示上述的`DotProductAttention`类**], 我们使用与先前加性注意力例子中相同的键、值和有效长度。 对于点积操作,我们令查询的特征维度与键的特征维度大小相同。 @@ -449,6 +548,14 @@ attention = DotProductAttention(dropout=0.5) attention(queries, keys, values, valid_lens, training=False) ``` +```{.python .input} +#@tab paddle +queries = paddle.normal(0, 1, (2, 1, 2)) +attention = DotProductAttention(dropout=0.5) +attention.eval() +attention(queries, keys, values, valid_lens) +``` + 与加性注意力演示相同,由于键包含的是相同的元素, 而这些元素无法通过任何查询进行区分,因此获得了[**均匀的注意力权重**]。 @@ -466,7 +573,7 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), ## 练习 1. 修改小例子中的键,并且可视化注意力权重。可加性注意力和缩放的“点-积”注意力是否仍然产生相同的结果?为什么? -1. 只使用矩阵乘法,你能否为具有不同矢量长度的查询和键设计新的评分函数? +1. 只使用矩阵乘法,能否为具有不同矢量长度的查询和键设计新的评分函数? 1. 当查询和键具有相同的矢量长度时,矢量求和作为评分函数是否比“点-积”更好?为什么? :begin_tab:`mxnet` @@ -476,3 +583,7 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5752) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11841) +:end_tab: \ No newline at end of file diff --git a/chapter_attention-mechanisms/bahdanau-attention.md b/chapter_attention-mechanisms/bahdanau-attention.md index 0a22d9843..b8c09b857 100644 --- a/chapter_attention-mechanisms/bahdanau-attention.md +++ b/chapter_attention-mechanisms/bahdanau-attention.md @@ -1,7 +1,7 @@ # Bahdanau 注意力 :label:`sec_seq2seq_attention` -我们在 :numref:`sec_seq2seq`中探讨了机器翻译问题: + :numref:`sec_seq2seq`中探讨了机器翻译问题: 通过设计一个基于两个循环神经网络的编码器-解码器架构, 用于序列到序列学习。 具体来说,循环神经网络编码器将长度可变的序列转换为固定形状的上下文变量, @@ -63,9 +63,18 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + ## 定义注意力解码器 -下面我们看看如何定义Bahdanau注意力,实现循环神经网络编码器-解码器。 +下面看看如何定义Bahdanau注意力,实现循环神经网络编码器-解码器。 其实,我们只需重新定义解码器即可。 为了更方便地显示学习的注意力权重, 以下`AttentionDecoder`类定义了[**带有注意力机制解码器的基本接口**]。 @@ -85,7 +94,7 @@ class AttentionDecoder(d2l.Decoder): 接下来,让我们在接下来的`Seq2SeqAttentionDecoder`类中 [**实现带有Bahdanau注意力的循环神经网络解码器**]。 -首先,我们初始化解码器的状态,需要下面的输入: +首先,初始化解码器的状态,需要下面的输入: 1. 编码器在所有时间步的最终层隐状态,将作为注意力的键和值; 1. 上一时间步的编码器全层隐状态,将作为初始化解码器的隐状态; @@ -244,7 +253,57 @@ class Seq2SeqAttentionDecoder(AttentionDecoder): return self._attention_weights ``` -接下来,我们使用包含7个时间步的4个序列输入的小批量[**测试Bahdanau注意力解码器**]。 +```{.python .input} +#@tab paddle +class Seq2SeqAttentionDecoder(AttentionDecoder): + def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, + dropout=0, **kwargs): + super(Seq2SeqAttentionDecoder, self).__init__(**kwargs) + self.attention = d2l.AdditiveAttention( + num_hiddens, num_hiddens, num_hiddens, dropout) + self.embedding = nn.Embedding(vocab_size, embed_size) + self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, + num_layers, bias_ih_attr=True, + time_major=True, dropout=dropout) + self.dense = nn.Linear(num_hiddens, vocab_size) + + def init_state(self, enc_outputs, enc_valid_lens, *args): + # outputs的形状为(batch_size,num_steps,num_hiddens). + # hidden_state的形状为(num_layers,batch_size,num_hiddens) + outputs, hidden_state = enc_outputs + return (outputs.transpose((1, 0, 2)), hidden_state, enc_valid_lens) + + def forward(self, X, state): + # enc_outputs的形状为(batch_size,num_steps,num_hiddens). + # hidden_state的形状为(num_layers,batch_size,num_hiddens) + enc_outputs, hidden_state, enc_valid_lens = state + # 输出X的形状为(num_steps,batch_size,embed_size) + X = self.embedding(X).transpose((1, 0, 2)) + outputs, self._attention_weights = [], [] + for x in X: + # query的形状为(batch_size,1,num_hiddens) + query = paddle.unsqueeze(hidden_state[-1], axis=1) + # context的形状为(batch_size,1,num_hiddens) + context = self.attention( + query, enc_outputs, enc_outputs, enc_valid_lens) + # 在特征维度上连结 + x = paddle.concat((context, paddle.unsqueeze(x, axis=1)), axis=-1) + # 将x变形为(1,batch_size,embed_size+num_hiddens) + out, hidden_state = self.rnn(x.transpose((1, 0, 2)), hidden_state) + outputs.append(out) + self._attention_weights.append(self.attention.attention_weights) + # 全连接层变换后,outputs的形状为 + # (num_steps,batch_size,vocab_size) + outputs = self.dense(paddle.concat(outputs, axis=0)) + return outputs.transpose((1, 0, 2)), [enc_outputs, hidden_state, + enc_valid_lens] + + @property + def attention_weights(self): + return self._attention_weights +``` + +接下来,使用包含7个时间步的4个序列输入的小批量[**测试Bahdanau注意力解码器**]。 ```{.python .input} encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -285,6 +344,20 @@ output, state = decoder(X, state, training=False) output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape ``` +```{.python .input} +#@tab paddle +encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, + num_layers=2) +encoder.eval() +decoder = Seq2SeqAttentionDecoder(vocab_size=10, embed_size=8, num_hiddens=16, + num_layers=2) +decoder.eval() +X = paddle.zeros((4, 7), dtype='int64') # (batch_size,num_steps) +state = decoder.init_state(encoder(X), None) +output, state = decoder(X, state) +output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape +``` + ## [**训练**] 与 :numref:`sec_seq2seq_training`类似, @@ -311,7 +384,7 @@ d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) 模型训练后,我们用它[**将几个英语句子翻译成法语**]并计算它们的BLEU分数。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .'] fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .'] for eng, fra in zip(engs, fras): @@ -339,8 +412,8 @@ attention_weights = d2l.reshape( (1, 1, -1, num_steps)) ``` -训练结束后,下面我们通过[**可视化注意力权重**] -你会发现,每个查询都会在键值对上分配不同的权重,这说明 +训练结束后,下面通过[**可视化注意力权重**] +会发现,每个查询都会在键值对上分配不同的权重,这说明 在每个解码步中,输入序列的不同部分被选择性地聚集在注意力池中。 ```{.python .input} @@ -351,7 +424,7 @@ d2l.show_heatmaps( ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle # 加上一个包含序列结束词元 d2l.show_heatmaps( attention_weights[:, :, :, :len(engs[-1].split()) + 1].cpu(), @@ -382,3 +455,7 @@ d2l.show_heatmaps(attention_weights[:, :, :, :len(engs[-1].split()) + 1], :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5754) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11842) +:end_tab: \ No newline at end of file diff --git a/chapter_attention-mechanisms/index.md b/chapter_attention-mechanisms/index.md index 3c75ee8e2..8300f2555 100644 --- a/chapter_attention-mechanisms/index.md +++ b/chapter_attention-mechanisms/index.md @@ -8,21 +8,22 @@ 只关注一小部分信息的能力对进化更加有意义,使人类得以生存和成功。 自19世纪以来,科学家们一直致力于研究认知神经科学领域的注意力。 -本章的很多章节将涉及到这些研究: -我们将首先回顾一个经典注意力框架,解释如何在视觉场景中展开注意力。 +本章的很多章节将涉及到一些研究。 + +首先回顾一个经典注意力框架,解释如何在视觉场景中展开注意力。 受此框架中的*注意力提示*(attention cues)的启发, 我们将设计能够利用这些注意力提示的模型。 1964年的Nadaraya-Waston核回归(kernel regression)正是具有 *注意力机制*(attention mechanism)的机器学习的简单演示。 -然后,我们继续介绍的是注意力函数,它们在深度学习的注意力模型设计中被广泛使用。 +然后继续介绍的是注意力函数,它们在深度学习的注意力模型设计中被广泛使用。 具体来说,我们将展示如何使用这些函数来设计*Bahdanau注意力*。 Bahdanau注意力是深度学习中的具有突破性价值的注意力模型,它双向对齐并且可以微分。 -最后,我们将描述仅仅基于注意力机制的*transformer*架构, +最后将描述仅仅基于注意力机制的*Transformer*架构, 该架构中使用了*多头注意力*(multi-head attention) 和*自注意力*(self-attention)。 -自2017年横空出世,transformer一直都普遍存在于现代的深度学习应用中, +自2017年横空出世,Transformer一直都普遍存在于现代的深度学习应用中, 例如语言、视觉、语音和强化学习领域。 ```toc diff --git a/chapter_attention-mechanisms/multihead-attention.md b/chapter_attention-mechanisms/multihead-attention.md index 43e3f0c2e..6528a5ed1 100644 --- a/chapter_attention-mechanisms/multihead-attention.md +++ b/chapter_attention-mechanisms/multihead-attention.md @@ -73,12 +73,22 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +from paddle import nn +``` + ## 实现 -在实现过程中,我们[**选择缩放点积注意力作为每一个注意力头**]。 +在实现过程中通常[**选择缩放点积注意力作为每一个注意力头**]。 为了避免计算代价和参数代价的大幅增长, 我们设定$p_q = p_k = p_v = p_o / h$。 -值得注意的是,如果我们将查询、键和值的线性变换的输出数量设置为 +值得注意的是,如果将查询、键和值的线性变换的输出数量设置为 $p_q h = p_k h = p_v h = p_o$, 则可以并行计算$h$个头。 在下面的实现中,$p_o$是通过参数`num_hiddens`指定的。 @@ -206,6 +216,46 @@ class MultiHeadAttention(tf.keras.layers.Layer): return self.W_o(output_concat) ``` +```{.python .input} +#@tab paddle +#@save +class MultiHeadAttention(nn.Layer): + def __init__(self, key_size, query_size, value_size, num_hiddens, + num_heads, dropout, bias=False, **kwargs): + super(MultiHeadAttention, self).__init__(**kwargs) + self.num_heads = num_heads + self.attention = d2l.DotProductAttention(dropout) + self.W_q = nn.Linear(query_size, num_hiddens, bias_attr=bias) + self.W_k = nn.Linear(key_size, num_hiddens, bias_attr=bias) + self.W_v = nn.Linear(value_size, num_hiddens, bias_attr=bias) + self.W_o = nn.Linear(num_hiddens, num_hiddens, bias_attr=bias) + + def forward(self, queries, keys, values, valid_lens): + # queries,keys,values的形状: + # (batch_size,查询或者“键-值”对的个数,num_hiddens) + # valid_lens 的形状: + # (batch_size,)或(batch_size,查询的个数) + # 经过变换后,输出的queries,keys,values 的形状: + # (batch_size*num_heads,查询或者“键-值”对的个数, + # num_hiddens/num_heads) + queries = transpose_qkv(self.W_q(queries), self.num_heads) + keys = transpose_qkv(self.W_k(keys), self.num_heads) + values = transpose_qkv(self.W_v(values), self.num_heads) + if valid_lens is not None: + # 在轴0,将第一项(标量或者矢量)复制num_heads次, + # 然后如此复制第二项,然后诸如此类。 + valid_lens = paddle.repeat_interleave( + valid_lens, repeats=self.num_heads, axis=0) + + # output的形状:(batch_size*num_heads,查询的个数, + # num_hiddens/num_heads) + output = self.attention(queries, keys, values, valid_lens) + + # output_concat的形状:(batch_size,查询的个数,num_hiddens) + output_concat = transpose_output(output, self.num_heads) + return self.W_o(output_concat) +``` + 为了能够[**使多个头并行计算**], 上面的`MultiHeadAttention`类将使用下面定义的两个转置函数。 具体来说,`transpose_output`函数反转了`transpose_qkv`函数的操作。 @@ -290,7 +340,34 @@ def transpose_output(X, num_heads): return tf.reshape(X, shape=(X.shape[0], X.shape[1], -1)) ``` -下面我们使用键和值相同的小例子来[**测试**]我们编写的`MultiHeadAttention`类。 +```{.python .input} +#@tab paddle +#@save +def transpose_qkv(X, num_heads): + """为了多注意力头的并行计算而变换形状""" + # 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens) + # 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads, + # num_hiddens/num_heads) + X = X.reshape((X.shape[0], X.shape[1], num_heads, -1)) + + # 输出X的形状:(batch_size,num_heads,查询或者“键-值”对的个数, + # num_hiddens/num_heads) + X = X.transpose((0, 2, 1, 3)) + + # 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数, + # num_hiddens/num_heads) + return X.reshape((-1, X.shape[2], X.shape[3])) + + +#@save +def transpose_output(X, num_heads): + """逆转transpose_qkv函数的操作""" + X = X.reshape((-1, num_heads, X.shape[1], X.shape[2])) + X = X.transpose((0, 2, 1, 3)) + return X.reshape((X.shape[0], X.shape[1], -1)) +``` + +下面使用键和值相同的小例子来[**测试**]我们编写的`MultiHeadAttention`类。 多头注意力输出的形状是(`batch_size`,`num_queries`,`num_hiddens`)。 ```{.python .input} @@ -315,7 +392,15 @@ attention = MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens, ``` ```{.python .input} -#@tab mxnet, pytorch +#@tab paddle +num_hiddens, num_heads = 100, 5 +attention = MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens, + num_hiddens, num_heads, 0.5) +attention.eval() +``` + +```{.python .input} +#@tab mxnet, pytorch, paddle batch_size, num_queries = 2, 4 num_kvpairs, valid_lens = 6, d2l.tensor([3, 2]) X = d2l.ones((batch_size, num_queries, num_hiddens)) @@ -340,7 +425,7 @@ attention(X, Y, Y, valid_lens, training=False).shape ## 练习 1. 分别可视化这个实验中的多个头的注意力权重。 -1. 假设我们有一个完成训练的基于多头注意力的模型,现在希望修剪最不重要的注意力头以提高预测速度。如何设计实验来衡量注意力头的重要性呢? +1. 假设有一个完成训练的基于多头注意力的模型,现在希望修剪最不重要的注意力头以提高预测速度。如何设计实验来衡量注意力头的重要性呢? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/5757) @@ -349,3 +434,7 @@ attention(X, Y, Y, valid_lens, training=False).shape :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5758) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11843) +:end_tab: \ No newline at end of file diff --git a/chapter_attention-mechanisms/nadaraya-waston.md b/chapter_attention-mechanisms/nadaraya-waston.md index 4cbdeadad..5417ea8c5 100644 --- a/chapter_attention-mechanisms/nadaraya-waston.md +++ b/chapter_attention-mechanisms/nadaraya-waston.md @@ -1,10 +1,10 @@ # 注意力汇聚:Nadaraya-Watson 核回归 :label:`sec_nadaraya-watson` -上节我们介绍了框架下的注意力机制的主要成分 :numref:`fig_qkv`: -查询(自主提示)和键(非自主提示)之间的交互形成了注意力汇聚, +上节介绍了框架下的注意力机制的主要成分 :numref:`fig_qkv`: +查询(自主提示)和键(非自主提示)之间的交互形成了注意力汇聚; 注意力汇聚有选择地聚合了值(感官输入)以生成最终的输出。 -在本节中,我们将介绍注意力汇聚的更多细节, +本节将介绍注意力汇聚的更多细节, 以便从宏观上了解注意力机制在实践中的运作方式。 具体来说,1964年提出的Nadaraya-Watson核回归模型 是一个简单但完整的例子,可以用于演示具有注意力机制的机器学习。 @@ -31,6 +31,15 @@ import tensorflow as tf tf.random.set_seed(seed=1322) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + ## [**生成数据集**] 简单起见,考虑下面这个回归问题: @@ -44,8 +53,8 @@ $\{(x_1, y_1), \ldots, (x_n, y_n)\}$, $$y_i = 2\sin(x_i) + x_i^{0.8} + \epsilon,$$ 其中$\epsilon$服从均值为$0$和标准差为$0.5$的正态分布。 -我们生成了$50$个训练样本和$50$个测试样本。 -为了更好地可视化之后的注意力模式,我们将训练样本进行排序。 +在这里生成了$50$个训练样本和$50$个测试样本。 +为了更好地可视化之后的注意力模式,需要将训练样本进行排序。 ```{.python .input} n_train = 50 # 训练样本数 @@ -64,6 +73,12 @@ n_train = 50 x_train = tf.sort(tf.random.uniform(shape=(n_train,), maxval=5)) ``` +```{.python .input} +#@tab paddle +n_train = 50 # 训练样本数 +x_train = paddle.sort(paddle.rand([n_train]) * 5) # 排序后的训练样本 +``` + ```{.python .input} def f(x): return 2 * d2l.sin(x) + x**0.8 @@ -99,6 +114,18 @@ n_test = len(x_test) # 测试样本数 n_test ``` +```{.python .input} +#@tab paddle +def f(x): + return 2 * paddle.sin(x) + x**0.8 + +y_train = f(x_train) + paddle.normal(0.0, 0.5, (n_train,)) # 训练样本的输出 +x_test = d2l.arange(0, 5, 0.1, dtype='float32') # 测试样本 +y_truth = f(x_test) # 测试样本的真实输出 +n_test = len(x_test) # 测试样本数 +n_test +``` + 下面的函数将绘制所有的训练样本(样本由圆圈表示), 不带噪声项的真实数据生成函数$f$(标记为“Truth”), 以及学习得到的预测函数(标记为“Pred”)。 @@ -113,13 +140,13 @@ def plot_kernel_reg(y_hat): ## 平均汇聚 -我们先使用最简单的估计器来解决回归问题: +先使用最简单的估计器来解决回归问题。 基于平均汇聚来计算所有训练样本输出值的平均值: $$f(x) = \frac{1}{n}\sum_{i=1}^n y_i,$$ :eqlabel:`eq_avg-pooling` -如下图所示,这个估计器确实不够聪明: +如下图所示,这个估计器确实不够聪明。 真实函数$f$(“Truth”)和预测函数(“Pred”)相差很大。 ```{.python .input} @@ -139,6 +166,12 @@ y_hat = tf.repeat(tf.reduce_mean(y_train), repeats=n_test) plot_kernel_reg(y_hat) ``` +```{.python .input} +#@tab paddle +y_hat = paddle.repeat_interleave(y_train.mean(), n_test) +plot_kernel_reg(y_hat) +``` + ## [**非参数注意力汇聚**] 显然,平均汇聚忽略了输入$x_i$。 @@ -152,7 +185,7 @@ $$f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i,$$ 其中$K$是*核*(kernel)。 公式 :eqref:`eq_nadaraya-watson`所描述的估计器被称为 *Nadaraya-Watson核回归*(Nadaraya-Watson kernel regression)。 -这里我们不会深入讨论核函数的细节, +这里不会深入讨论核函数的细节, 但受此启发, 我们可以从 :numref:`fig_qkv`中的注意力机制框架的角度 重写 :eqref:`eq_nadaraya-watson`, @@ -172,7 +205,7 @@ $$f(x) = \sum_{i=1}^n \alpha(x, x_i) y_i,$$ 它们是非负的,并且总和为1。 为了更好地理解注意力汇聚, -我们考虑一个*高斯核*(Gaussian kernel),其定义为: +下面考虑一个*高斯核*(Gaussian kernel),其定义为: $$K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}).$$ @@ -191,7 +224,7 @@ $$\begin{aligned} f(x) &=\sum_{i=1}^n \alpha(x, x_i) y_i\\ &= \sum_{i=1}^n \frac 因此, :eqref:`eq_nadaraya-watson-gaussian`是 *非参数的注意力汇聚*(nonparametric attention pooling)模型。 接下来,我们将基于这个非参数的注意力汇聚模型来绘制预测结果。 -你会发现新的模型预测线是平滑的,并且比平均汇聚的预测更接近真实。 +从绘制的结果会发现新的模型预测线是平滑的,并且比平均汇聚的预测更接近真实。 ```{.python .input} # X_repeat的形状:(n_test,n_train), @@ -231,7 +264,20 @@ y_hat = tf.matmul(attention_weights, tf.expand_dims(y_train, axis=1)) plot_kernel_reg(y_hat) ``` -现在,我们来观察注意力的权重。 +```{.python .input} +#@tab paddle +# X_repeat的形状:(n_test,n_train), +# 每一行都包含着相同的测试输入(例如:同样的查询) +X_repeat = d2l.reshape(x_test.repeat_interleave(n_train), (-1, n_train)) +# x_train包含着键。attention_weights的形状:(n_test,n_train), +# 每一行都包含着要在给定的每个查询的值(y_train)之间分配的注意力权重 +attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, axis=1) +# y_hat的每个元素都是值的加权平均值,其中的权重是注意力权重 +y_hat = d2l.matmul(attention_weights, y_train) +plot_kernel_reg(y_hat) +``` + +现在来观察注意力的权重。 这里测试数据的输入相当于查询,而训练数据的输入相当于键。 因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近, 注意力汇聚的[**注意力权重**]就越高。 @@ -257,6 +303,13 @@ d2l.show_heatmaps(tf.expand_dims( ylabel='Sorted testing inputs') ``` +```{.python .input} +#@tab paddle +d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0), + xlabel='Sorted training inputs', + ylabel='Sorted testing inputs') +``` + ## [**带参数注意力汇聚**] 非参数的Nadaraya-Watson核回归具有*一致性*(consistency)的优点: @@ -269,7 +322,7 @@ d2l.show_heatmaps(tf.expand_dims( $$\begin{aligned}f(x) &= \sum_{i=1}^n \alpha(x, x_i) y_i \\&= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}((x - x_i)w)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}((x - x_j)w)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}((x - x_i)w)^2\right) y_i.\end{aligned}$$ :eqlabel:`eq_nadaraya-watson-gaussian-para` -在本节的余下部分,我们将通过训练这个模型 +本节的余下部分将通过训练这个模型 :eqref:`eq_nadaraya-watson-gaussian-para`来学习注意力汇聚的参数。 ### 批量矩阵乘法 @@ -309,6 +362,13 @@ Y = tf.ones((2, 4, 6)) tf.matmul(X, Y).shape ``` +```{.python .input} +#@tab paddle +X = paddle.ones((2, 1, 4)) +Y = paddle.ones((2, 4, 6)) +paddle.bmm(X, Y).shape +``` + 在注意力机制的背景中,我们可以[**使用小批量矩阵乘法来计算小批量数据中的加权平均值**]。 ```{.python .input} @@ -331,6 +391,13 @@ values = tf.reshape(tf.range(20.0), shape = (2, 10)) tf.matmul(tf.expand_dims(weights, axis=1), tf.expand_dims(values, axis=-1)).numpy() ``` +```{.python .input} +#@tab paddle +weights = paddle.ones((2, 10)) * 0.1 +values = paddle.arange(20, dtype='float32').reshape((2, 10)) +paddle.bmm(weights.unsqueeze(1), values.unsqueeze(-1)) +``` + ### 定义模型 基于 :eqref:`eq_nadaraya-watson-gaussian-para`中的 @@ -388,6 +455,25 @@ class NWKernelRegression(tf.keras.layers.Layer): return tf.squeeze(tf.matmul(tf.expand_dims(self.attention_weights, axis=1), tf.expand_dims(values, axis=-1))) ``` +```{.python .input} +#@tab paddle +class NWKernelRegression(nn.Layer): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.w = paddle.create_parameter((1,), dtype='float32') + + def forward(self, queries, keys, values): + # queries和attention_weights的形状为(查询个数,“键-值”对个数) + queries = queries.reshape((queries.shape[0], 1)) \ + .tile([keys.shape[1]]) \ + .reshape((-1, keys.shape[1])) + self.attention_weight = nn.functional.softmax( + -((queries - keys) * self.w)**2 / 2, axis=1) + # values的形状为(查询个数,“键-值”对个数) + return paddle.bmm(self.attention_weight.unsqueeze(1), + values.unsqueeze(-1)).reshape((-1, )) +``` + ### 训练 接下来,[**将训练数据集变换为键和值**]用于训练注意力模型。 @@ -434,6 +520,18 @@ keys = tf.reshape(X_tile[tf.cast(1 - tf.eye(n_train), dtype=tf.bool)], shape=(n_ values = tf.reshape(Y_tile[tf.cast(1 - tf.eye(n_train), dtype=tf.bool)], shape=(n_train, -1)) ``` +```{.python .input} +#@tab paddle +# X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入 +X_tile = x_train.tile([n_train, 1]) +# Y_tile的形状:(n_train,n_train),每一行都包含着相同的训练输出 +Y_tile = y_train.tile([n_train, 1]) +# keys的形状:('n_train','n_train'-1) +keys = X_tile[(1 - paddle.eye(n_train)).astype(paddle.bool)].reshape((n_train, -1)) +# values的形状:('n_train','n_train'-1) +values = Y_tile[(1 - paddle.eye(n_train)).astype(paddle.bool)].reshape((n_train, -1)) +``` + [**训练带参数的注意力汇聚模型**]时,使用平方损失函数和随机梯度下降。 ```{.python .input} @@ -485,7 +583,23 @@ for epoch in range(5): animator.add(epoch + 1, float(loss)) ``` -如下所示,训练完带参数的注意力汇聚模型后,我们发现: +```{.python .input} +#@tab paddle +net = NWKernelRegression() +loss = nn.MSELoss(reduction='none') +trainer = paddle.optimizer.SGD(learning_rate=0.5, parameters=net.parameters()) +animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5]) + +for epoch in range(5): + trainer.clear_grad() + l = loss(net(x_train, keys, values), y_train) + l.sum().backward() + trainer.step() + print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}') + animator.add(epoch + 1, float(l.sum())) +``` + +如下所示,训练完带参数的注意力汇聚模型后可以发现: 在尝试拟合带噪声的训练数据时, [**预测结果绘制**]的线不如之前非参数模型的平滑。 @@ -518,8 +632,18 @@ y_hat = net(x_test, keys, values) plot_kernel_reg(y_hat) ``` +```{.python .input} +#@tab paddle +# keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键) +keys = x_train.tile([n_test, 1]) +# value的形状:(n_test,n_train) +values = y_train.tile([n_test, 1]) +y_hat = net(x_test, keys, values).unsqueeze(1).detach() +plot_kernel_reg(y_hat) +``` + 为什么新的模型更不平滑了呢? -我们看一下输出结果的绘制图: +下面看一下输出结果的绘制图: 与非参数的注意力汇聚模型相比, 带参数的模型加入可学习的参数后, [**曲线在注意力权重较大的区域变得更不平滑**]。 @@ -546,6 +670,13 @@ d2l.show_heatmaps(tf.expand_dims( ylabel='Sorted testing inputs') ``` +```{.python .input} +#@tab paddle +d2l.show_heatmaps(net.attention_weight.unsqueeze(0).unsqueeze(0), + xlabel='Sorted training inputs', + ylabel='Sorter testing, inputs') +``` + ## 小结 * Nadaraya-Watson核回归是具有注意力机制的机器学习范例。 @@ -554,7 +685,7 @@ d2l.show_heatmaps(tf.expand_dims( ## 练习 -1. 增加训练数据的样本数量,你能否得到更好的非参数的Nadaraya-Watson核回归模型? +1. 增加训练数据的样本数量,能否得到更好的非参数的Nadaraya-Watson核回归模型? 1. 在带参数的注意力汇聚的实验中学习得到的参数$w$的价值是什么?为什么在可视化注意力权重时,它会使加权区域更加尖锐? 1. 如何将超参数添加到非参数的Nadaraya-Watson核回归中以实现更好地预测结果? 1. 为本节的核回归设计一个新的带参数的注意力汇聚模型。训练这个新模型并可视化其注意力权重。 @@ -566,3 +697,7 @@ d2l.show_heatmaps(tf.expand_dims( :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5760) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11840) +:end_tab: \ No newline at end of file diff --git a/chapter_attention-mechanisms/self-attention-and-positional-encoding.md b/chapter_attention-mechanisms/self-attention-and-positional-encoding.md index c6ccdf1ed..ea2554e34 100644 --- a/chapter_attention-mechanisms/self-attention-and-positional-encoding.md +++ b/chapter_attention-mechanisms/self-attention-and-positional-encoding.md @@ -1,15 +1,15 @@ # 自注意力和位置编码 :label:`sec_self-attention-and-positional-encoding` -在深度学习中,我们经常使用卷积神经网络(CNN)或循环神经网络(RNN)对序列进行编码。 -想象一下,有了注意力机制之后,我们将词元序列输入注意力汇聚中, +在深度学习中,经常使用卷积神经网络(CNN)或循环神经网络(RNN)对序列进行编码。 +想象一下,有了注意力机制之后,我们将词元序列输入注意力池化中, 以便同一组词元同时充当查询、键和值。 具体来说,每个查询都会关注所有的键-值对并生成一个注意力输出。 由于查询、键和值来自同一组输入,因此被称为 *自注意力*(self-attention) :cite:`Lin.Feng.Santos.ea.2017,Vaswani.Shazeer.Parmar.ea.2017`, 也被称为*内部注意力*(intra-attention) :cite:`Cheng.Dong.Lapata.2016,Parikh.Tackstrom.Das.ea.2016,Paulus.Xiong.Socher.2017`。 -在本节中,我们将使用自注意力进行序列编码,以及如何使用序列的顺序作为补充信息。 +本节将使用自注意力进行序列编码,以及如何使用序列的顺序作为补充信息。 ```{.python .input} from d2l import mxnet as d2l @@ -34,6 +34,16 @@ import numpy as np import tensorflow as tf ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import math +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + ## [**自注意力**] 给定一个由词元组成的输入序列$\mathbf{x}_1, \ldots, \mathbf{x}_n$, @@ -70,7 +80,15 @@ attention = d2l.MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens, ``` ```{.python .input} -#@tab mxnet, pytorch +#@tab paddle +num_hiddens, num_heads = 100, 5 +attention = d2l.MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens, + num_hiddens, num_heads, 0.5) +attention.eval() +``` + +```{.python .input} +#@tab mxnet, pytorch, paddle batch_size, num_queries, valid_lens = 2, 4, d2l.tensor([3, 2]) X = d2l.ones((batch_size, num_queries, num_hiddens)) attention(X, X, X, valid_lens).shape @@ -86,14 +104,14 @@ attention(X, X, X, valid_lens, training=False).shape ## 比较卷积神经网络、循环神经网络和自注意力 :label:`subsec_cnn-rnn-self-attention` -让我们比较下面几个架构,目标都是将由$n$个词元组成的序列映射到另一个长度相等的序列,其中的每个输入词元或输出词元都由$d$维向量表示。具体来说,我们将比较的是卷积神经网络、循环神经网络和自注意力这几个架构的计算复杂性、顺序操作和最大路径长度。请注意,顺序操作会妨碍并行计算,而任意的序列位置组合之间的路径越短,则能更轻松地学习序列中的远距离依赖关系 :cite:`Hochreiter.Bengio.Frasconi.ea.2001`。 +接下来比较下面几个架构,目标都是将由$n$个词元组成的序列映射到另一个长度相等的序列,其中的每个输入词元或输出词元都由$d$维向量表示。具体来说,将比较的是卷积神经网络、循环神经网络和自注意力这几个架构的计算复杂性、顺序操作和最大路径长度。请注意,顺序操作会妨碍并行计算,而任意的序列位置组合之间的路径越短,则能更轻松地学习序列中的远距离依赖关系 :cite:`Hochreiter.Bengio.Frasconi.ea.2001`。 ![比较卷积神经网络(填充词元被忽略)、循环神经网络和自注意力三种架构](../img/cnn-rnn-self-attention.svg) :label:`fig_cnn-rnn-self-attention` 考虑一个卷积核大小为$k$的卷积层。 -我们将在后面的章节中提供关于使用卷积神经网络处理序列的更多详细信息。 -目前,我们只需要知道,由于序列长度是$n$,输入和输出的通道数量都是$d$, +在后面的章节将提供关于使用卷积神经网络处理序列的更多详细信息。 +目前只需要知道的是,由于序列长度是$n$,输入和输出的通道数量都是$d$, 所以卷积层的计算复杂度为$\mathcal{O}(knd^2)$。 如 :numref:`fig_cnn-rnn-self-attention`所示, 卷积神经网络是分层的,因此为有$\mathcal{O}(1)$个顺序操作, @@ -112,7 +130,7 @@ $d \times d$权重矩阵和$d$维隐状态的乘法计算复杂度为$\mathcal{O 其中$n \times d$矩阵乘以$d \times n$矩阵。 之后输出的$n \times n$矩阵乘以$n \times d$矩阵。 因此,自注意力具有$\mathcal{O}(n^2d)$计算复杂性。 -正如我们在 :numref:`fig_cnn-rnn-self-attention`中看到的那样, +正如在 :numref:`fig_cnn-rnn-self-attention`中所讲, 每个词元都通过自注意力直接连接到任何其他词元。 因此,有$\mathcal{O}(1)$个顺序操作可以并行计算, 最大路径长度也是$\mathcal{O}(1)$。 @@ -126,10 +144,10 @@ $d \times d$权重矩阵和$d$维隐状态的乘法计算复杂度为$\mathcal{O 在处理词元序列时,循环神经网络是逐个的重复地处理词元的, 而自注意力则因为并行计算而放弃了顺序操作。 -为了使用序列的顺序信息,我们通过在输入表示中添加 +为了使用序列的顺序信息,通过在输入表示中添加 *位置编码*(positional encoding)来注入绝对的或相对的位置信息。 位置编码可以通过学习得到也可以直接固定得到。 -接下来,我们描述的是基于正弦函数和余弦函数的固定位置编码 +接下来描述的是基于正弦函数和余弦函数的固定位置编码 :cite:`Vaswani.Shazeer.Parmar.ea.2017`。 假设输入表示$\mathbf{X} \in \mathbb{R}^{n \times d}$ @@ -205,9 +223,30 @@ class PositionalEncoding(tf.keras.layers.Layer): return self.dropout(X, **kwargs) ``` +```{.python .input} +#@tab paddle +#@save +class PositionalEncoding(nn.Layer): + """位置编码""" + def __init__(self, num_hiddens, dropout, max_len=1000): + super(PositionalEncoding, self).__init__() + self.dropout = nn.Dropout(dropout) + # 创建一个足够长的P + self.P = paddle.zeros((1, max_len, num_hiddens)) + X = paddle.arange(max_len, dtype=paddle.float32).reshape( + (-1, 1)) / paddle.pow(paddle.to_tensor([10000.0]), paddle.arange( + 0, num_hiddens, 2, dtype=paddle.float32) / num_hiddens) + self.P[:, :, 0::2] = paddle.sin(X) + self.P[:, :, 1::2] = paddle.cos(X) + + def forward(self, X): + X = X + self.P[:, :X.shape[1], :] + return self.dropout(X) +``` + 在位置嵌入矩阵$\mathbf{P}$中, [**行代表词元在序列中的位置,列代表位置编码的不同维度**]。 -在下面的例子中,我们可以看到位置嵌入矩阵的第$6$列和第$7$列的频率高于第$8$列和第$9$列。 +从下面的例子中可以看到位置嵌入矩阵的第$6$列和第$7$列的频率高于第$8$列和第$9$列。 第$6$列和第$7$列之间的偏移量(第$8$列和第$9$列相同)是由于正弦函数和余弦函数的交替。 ```{.python .input} @@ -241,11 +280,22 @@ d2l.plot(np.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)', figsize=(6, 2.5), legend=["Col %d" % d for d in np.arange(6, 10)]) ``` +```{.python .input} +#@tab paddle +encoding_dim, num_steps = 32, 60 +pos_encoding = PositionalEncoding(encoding_dim, 0) +pos_encoding.eval() +X = pos_encoding(paddle.zeros((1, num_steps, encoding_dim))) +P = pos_encoding.P[:, :X.shape[1], :] +d2l.plot(paddle.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)', + figsize=(6, 2.5), legend=["Col %d" % d for d in paddle.arange(6, 10)]) +``` + ### 绝对位置信息 为了明白沿着编码维度单调降低的频率与绝对位置信息的关系, 让我们打印出$0, 1, \ldots, 7$的[**二进制表示**]形式。 -正如我们所看到的,每个数字、每两个数字和每四个数字上的比特值 +正如所看到的,每个数字、每两个数字和每四个数字上的比特值 在第一个最低位、第二个最低位和第三个最低位上分别交替。 ```{.python .input} @@ -278,6 +328,13 @@ d2l.show_heatmaps(P, xlabel='Column (encoding dimension)', ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues') ``` +```{.python .input} +#@tab paddle +P = P[0, :, :].unsqueeze(0).unsqueeze(0) +d2l.show_heatmaps(P, xlabel='Column (encoding dimension)', + ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues') +``` + ### 相对位置信息 除了捕获绝对位置信息之外,上述的位置编码还允许模型学习得到输入序列中相对位置信息。 @@ -305,12 +362,12 @@ $2\times 2$投影矩阵不依赖于任何位置的索引$i$。 * 在自注意力中,查询、键和值都来自同一组输入。 * 卷积神经网络和自注意力都拥有并行计算的优势,而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。 -* 为了使用序列的顺序信息,我们可以通过在输入表示中添加位置编码,来注入绝对的或相对的位置信息。 +* 为了使用序列的顺序信息,可以通过在输入表示中添加位置编码,来注入绝对的或相对的位置信息。 ## 练习 -1. 假设我们设计一个深度架构,通过堆叠基于位置编码的自注意力层来表示序列。可能会存在什么问题? -1. 你能设计一种可学习的位置编码方法吗? +1. 假设设计一个深度架构,通过堆叠基于位置编码的自注意力层来表示序列。可能会存在什么问题? +1. 请设计一种可学习的位置编码方法。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/5761) @@ -319,3 +376,7 @@ $2\times 2$投影矩阵不依赖于任何位置的索引$i$。 :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5762) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11844) +:end_tab: \ No newline at end of file diff --git a/chapter_attention-mechanisms/transformer.md b/chapter_attention-mechanisms/transformer.md index 2e984b4a5..9c0411669 100644 --- a/chapter_attention-mechanisms/transformer.md +++ b/chapter_attention-mechanisms/transformer.md @@ -1,21 +1,21 @@ # Transformer :label:`sec_transformer` -我们在 :numref:`subsec_cnn-rnn-self-attention`中比较了卷积神经网络(CNN)、循环神经网络(RNN)和自注意力(self-attention)。值得注意的是,自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此,使用自注意力来设计深度架构是很有吸引力的。对比之前仍然依赖循环神经网络实现输入表示的自注意力模型 :cite:`Cheng.Dong.Lapata.2016,Lin.Feng.Santos.ea.2017,Paulus.Xiong.Socher.2017`,transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层 :cite:`Vaswani.Shazeer.Parmar.ea.2017`。尽管transformer最初是应用于在文本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。 + :numref:`subsec_cnn-rnn-self-attention`中比较了卷积神经网络(CNN)、循环神经网络(RNN)和自注意力(self-attention)。值得注意的是,自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此,使用自注意力来设计深度架构是很有吸引力的。对比之前仍然依赖循环神经网络实现输入表示的自注意力模型 :cite:`Cheng.Dong.Lapata.2016,Lin.Feng.Santos.ea.2017,Paulus.Xiong.Socher.2017`,Transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层 :cite:`Vaswani.Shazeer.Parmar.ea.2017`。尽管Transformer最初是应用于在文本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。 ## 模型 -Transformer作为编码器-解码器架构的一个实例,其整体架构图在 :numref:`fig_transformer`中展示。正如所见到的,transformer是由编码器和解码器组成的。与 :numref:`fig_s2s_attention_details`中基于Bahdanau注意力实现的序列到序列的学习相比,transformer的编码器和解码器是基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的*嵌入*(embedding)表示将加上*位置编码*(positional encoding),再分别输入到编码器和解码器中。 +Transformer作为编码器-解码器架构的一个实例,其整体架构图在 :numref:`fig_transformer`中展示。正如所见到的,Transformer是由编码器和解码器组成的。与 :numref:`fig_s2s_attention_details`中基于Bahdanau注意力实现的序列到序列的学习相比,Transformer的编码器和解码器是基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的*嵌入*(embedding)表示将加上*位置编码*(positional encoding),再分别输入到编码器和解码器中。 ![transformer架构](../img/transformer.svg) :width:`500px` :label:`fig_transformer` -图 :numref:`fig_transformer`中概述了transformer的架构。从宏观角度来看,transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(子层表示为$\mathrm{sublayer}$)。第一个子层是*多头自注意力*(multi-head self-attention)汇聚;第二个子层是*基于位置的前馈网络*(positionwise feed-forward network)。具体来说,在计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。受 :numref:`sec_resnet`中残差网络的启发,每个子层都采用了*残差连接*(residual connection)。在transformer中,对于序列中任何位置的任何输入$\mathbf{x} \in \mathbb{R}^d$,都要求满足$\mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$,以便残差连接满足$\mathbf{x} + \mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$。在残差连接的加法计算之后,紧接着应用*层规范化*(layer normalization) :cite:`Ba.Kiros.Hinton.2016`。因此,输入序列对应的每个位置,transformer编码器都将输出一个$d$维表示向量。 +图 :numref:`fig_transformer`中概述了Transformer的架构。从宏观角度来看,Transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(子层表示为$\mathrm{sublayer}$)。第一个子层是*多头自注意力*(multi-head self-attention)汇聚;第二个子层是*基于位置的前馈网络*(positionwise feed-forward network)。具体来说,在计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。受 :numref:`sec_resnet`中残差网络的启发,每个子层都采用了*残差连接*(residual connection)。在Transformer中,对于序列中任何位置的任何输入$\mathbf{x} \in \mathbb{R}^d$,都要求满足$\mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$,以便残差连接满足$\mathbf{x} + \mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$。在残差连接的加法计算之后,紧接着应用*层规范化*(layer normalization) :cite:`Ba.Kiros.Hinton.2016`。因此,输入序列对应的每个位置,Transformer编码器都将输出一个$d$维表示向量。 Transformer解码器也是由多个相同的层叠加而成的,并且层中使用了残差连接和层规范化。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入了第三个子层,称为*编码器-解码器注意力*(encoder-decoder attention)层。在编码器-解码器注意力中,查询来自前一个解码器层的输出,而键和值来自整个编码器的输出。在解码器自注意力中,查询、键和值都来自上一个解码器层的输出。但是,解码器中的每个位置只能考虑该位置之前的所有位置。这种*掩蔽*(masked)注意力保留了*自回归*(auto-regressive)属性,确保预测仅依赖于已生成的输出词元。 -我们已经描述并实现了基于缩放点积多头注意力 :numref:`sec_multihead-attention`和位置编码 :numref:`subsec_positional-encoding`。接下来,我们将实现transformer模型的剩余部分。 +在此之前已经描述并实现了基于缩放点积多头注意力 :numref:`sec_multihead-attention`和位置编码 :numref:`subsec_positional-encoding`。接下来将实现Transformer模型的剩余部分。 ```{.python .input} from d2l import mxnet as d2l @@ -43,6 +43,17 @@ import pandas as pd import tensorflow as tf ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import math +import pandas as pd +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + ## [**基于位置的前馈网络**] 基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是*基于位置的*(positionwise)的原因。在下面的实现中,输入`X`的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小,时间步数,`ffn_num_outputs`)的输出张量。 @@ -92,6 +103,22 @@ class PositionWiseFFN(tf.keras.layers.Layer): return self.dense2(self.relu(self.dense1(X))) ``` +```{.python .input} +#@tab paddle +#@save +class PositionWiseFFN(nn.Layer): + """基于位置的前馈网络""" + def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs, + **kwargs): + super(PositionWiseFFN, self).__init__(**kwargs) + self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens) + self.relu = nn.ReLU() + self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs) + + def forward(self, X): + return self.dense2(self.relu(self.dense1(X))) +``` + 下面的例子显示,[**改变张量的最里层维度的尺寸**],会改变成基于位置的前馈网络的输出尺寸。因为用同一个多层感知机对所有位置上的输入进行变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。 ```{.python .input} @@ -113,11 +140,18 @@ ffn = PositionWiseFFN(4, 8) ffn(tf.ones((2, 3, 4)))[0] ``` +```{.python .input} +#@tab paddle +ffn = PositionWiseFFN(4, 4, 8) +ffn.eval() +ffn(d2l.ones((2, 3, 4)))[0] +``` + ## 残差连接和层规范化 -现在让我们关注 :numref:`fig_transformer`中的“*加法和规范化*(add&norm)”组件。正如在本节开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键。 +现在让我们关注 :numref:`fig_transformer`中的*加法和规范化*(add&norm)组件。正如在本节开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键。 -在 :numref:`sec_batch_norm`中,我们解释了在一个小批量的样本内基于批量规范化对数据进行重新中心化和重新缩放的调整。层规范化和批量规范化的目标相同,但层规范化是基于特征维度进行规范化。尽管批量规范化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量规范化通常不如层规范化的效果好。 + :numref:`sec_batch_norm`中解释了在一个小批量的样本内基于批量规范化对数据进行重新中心化和重新缩放的调整。层规范化和批量规范化的目标相同,但层规范化是基于特征维度进行规范化。尽管批量规范化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量规范化通常不如层规范化的效果好。 以下代码[**对比不同维度的层规范化和批量规范化的效果**]。 @@ -149,7 +183,16 @@ X = tf.constant([[1, 2], [2, 3]], dtype=tf.float32) print('layer norm:', ln(X), '\nbatch norm:', bn(X, training=True)) ``` -现在我们可以[**使用残差连接和层规范化**]来实现`AddNorm`类。暂退法也被作为正则化方法使用。 +```{.python .input} +#@tab paddle +ln = nn.LayerNorm(2) +bn = nn.BatchNorm1D(2) +X = d2l.tensor([[1, 2], [2, 3]], dtype=paddle.float32) +# 在训练模式下计算X的均值和方差 +print('layer norm:', ln(X), '\nbatch norm:', bn(X)) +``` + +现在可以[**使用残差连接和层规范化**]来实现`AddNorm`类。暂退法也被作为正则化方法使用。 ```{.python .input} #@save @@ -192,6 +235,20 @@ class AddNorm(tf.keras.layers.Layer): return self.ln(self.dropout(Y, **kwargs) + X) ``` +```{.python .input} +#@tab paddle +#@save +class AddNorm(nn.Layer): + """残差连接后进行层规范化""" + def __init__(self, normalized_shape, dropout, **kwargs): + super(AddNorm, self).__init__(**kwargs) + self.dropout = nn.Dropout(dropout) + self.ln = nn.LayerNorm(normalized_shape) + + def forward(self, X, Y): + return self.ln(self.dropout(Y) + X) +``` + 残差连接要求两个输入的形状相同,以便[**加法操作后输出张量的形状相同**]。 ```{.python .input} @@ -201,7 +258,7 @@ add_norm(d2l.ones((2, 3, 4)), d2l.ones((2, 3, 4))).shape ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle add_norm = AddNorm([3, 4], 0.5) add_norm.eval() add_norm(d2l.ones((2, 3, 4)), d2l.ones((2, 3, 4))).shape @@ -215,12 +272,12 @@ add_norm(tf.ones((2, 3, 4)), tf.ones((2, 3, 4)), training=False).shape ## 编码器 -有了组成transformer编码器的基础组件,现在可以先[**实现编码器中的一个层**]。下面的`EncoderBlock`类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。 +有了组成Transformer编码器的基础组件,现在可以先[**实现编码器中的一个层**]。下面的`EncoderBlock`类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。 ```{.python .input} #@save class EncoderBlock(nn.Block): - """transformer编码器块""" + """Transformer编码器块""" def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, use_bias=False, **kwargs): super(EncoderBlock, self).__init__(**kwargs) @@ -239,7 +296,7 @@ class EncoderBlock(nn.Block): #@tab pytorch #@save class EncoderBlock(nn.Module): - """transformer编码器块""" + """Transformer编码器块""" def __init__(self, key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, dropout, use_bias=False, **kwargs): @@ -261,7 +318,7 @@ class EncoderBlock(nn.Module): #@tab tensorflow #@save class EncoderBlock(tf.keras.layers.Layer): - """transformer编码器块""" + """Transformer编码器块""" def __init__(self, key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_hiddens, num_heads, dropout, bias=False, **kwargs): super().__init__(**kwargs) @@ -276,7 +333,29 @@ class EncoderBlock(tf.keras.layers.Layer): return self.addnorm2(Y, self.ffn(Y), **kwargs) ``` -正如我们所看到的,[**transformer编码器中的任何层都不会改变其输入的形状**]。 +```{.python .input} +#@tab paddle +#@save +class EncoderBlock(nn.Layer): + """transformer编码器块""" + def __init__(self, key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, + dropout, use_bias=False, **kwargs): + super(EncoderBlock, self).__init__(**kwargs) + self.attention = d2l.MultiHeadAttention( + key_size, query_size, value_size, num_hiddens, num_heads, dropout, + use_bias) + self.addnorm1 = AddNorm(norm_shape, dropout) + self.ffn = PositionWiseFFN( + ffn_num_input, ffn_num_hiddens, num_hiddens) + self.addnorm2 = AddNorm(norm_shape, dropout) + + def forward(self, X, valid_lens): + Y = self.addnorm1(X, self.attention(X, X, X, valid_lens)) + return self.addnorm2(Y, self.ffn(Y)) +``` + +正如从代码中所看到的,[**Transformer编码器中的任何层都不会改变其输入的形状**]。 ```{.python .input} X = d2l.ones((2, 100, 24)) @@ -287,7 +366,7 @@ encoder_blk(X, valid_lens).shape ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle X = d2l.ones((2, 100, 24)) valid_lens = d2l.tensor([3, 2]) encoder_blk = EncoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5) @@ -304,12 +383,12 @@ encoder_blk = EncoderBlock(24, 24, 24, 24, norm_shape, 48, 8, 0.5) encoder_blk(X, valid_lens, training=False).shape ``` -在实现下面的[**transformer编码器**]的代码中,我们堆叠了`num_layers`个`EncoderBlock`类的实例。由于我们使用的是值范围在$-1$和$1$之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。 +下面实现的[**Transformer编码器**]的代码中,堆叠了`num_layers`个`EncoderBlock`类的实例。由于这里使用的是值范围在$-1$和$1$之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。 ```{.python .input} #@save class TransformerEncoder(d2l.Encoder): - """transformer编码器""" + """Transformer编码器""" def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, num_layers, dropout, use_bias=False, **kwargs): super(TransformerEncoder, self).__init__(**kwargs) @@ -339,7 +418,7 @@ class TransformerEncoder(d2l.Encoder): #@tab pytorch #@save class TransformerEncoder(d2l.Encoder): - """transformer编码器""" + """Transformer编码器""" def __init__(self, vocab_size, key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, num_layers, dropout, use_bias=False, **kwargs): @@ -371,7 +450,7 @@ class TransformerEncoder(d2l.Encoder): #@tab tensorflow #@save class TransformerEncoder(d2l.Encoder): - """transformer编码器""" + """Transformer编码器""" def __init__(self, vocab_size, key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_hiddens, num_heads, num_layers, dropout, bias=False, **kwargs): @@ -398,7 +477,39 @@ class TransformerEncoder(d2l.Encoder): return X ``` -下面我们指定了超参数来[**创建一个两层的transformer编码器**]。 +```{.python .input} +#@tab paddle +#@save +class TransformerEncoder(d2l.Encoder): + """transformer编码器""" + def __init__(self, vocab_size, key_size, query_size, value_size, + num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, + num_heads, num_layers, dropout, use_bias=False, **kwargs): + super(TransformerEncoder, self).__init__(**kwargs) + self.num_hiddens = num_hiddens + self.embedding = nn.Embedding(vocab_size, num_hiddens) + self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) + self.blks = nn.Sequential() + for i in range(num_layers): + self.blks.add_sublayer(str(i), + EncoderBlock(key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, + num_heads, dropout, use_bias)) + + def forward(self, X, valid_lens, *args): + # 因为位置编码值在-1和1之间, + # 因此嵌入值乘以嵌入维度的平方根进行缩放, + # 然后再与位置编码相加。 + X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) + self.attention_weights = [None] * len(self.blks) + for i, blk in enumerate(self.blks): + X = blk(X, valid_lens) + self.attention_weights[ + i] = blk.attention.attention.attention_weights + return X +``` + +下面我们指定了超参数来[**创建一个两层的Transformer编码器**]。 Transformer编码器输出的形状是(批量大小,时间步数目,`num_hiddens`)。 ```{.python .input} @@ -421,9 +532,17 @@ encoder = TransformerEncoder(200, 24, 24, 24, 24, [1, 2], 48, 8, 2, 0.5) encoder(tf.ones((2, 100)), valid_lens, training=False).shape ``` +```{.python .input} +#@tab paddle +encoder = TransformerEncoder( + 200, 24, 24, 24, 24, [100, 24], 24, 48, 8, 2, 0.5) +encoder.eval() +encoder(d2l.ones((2, 100), dtype=paddle.int64), valid_lens).shape +``` + ## 解码器 -如 :numref:`fig_transformer`所示,[**transformer解码器也是由多个相同的层组成**]。在`DecoderBlock`类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。 +如 :numref:`fig_transformer`所示,[**Transformer解码器也是由多个相同的层组成**]。在`DecoderBlock`类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。 正如在本节前面所述,在掩蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于*序列到序列模型*(sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数`dec_valid_lens`,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。 @@ -569,6 +688,55 @@ class DecoderBlock(tf.keras.layers.Layer): return self.addnorm3(Z, self.ffn(Z), **kwargs), state ``` +```{.python .input} +#@tab paddle +class DecoderBlock(nn.Layer): + """解码器中第i个块""" + def __init__(self, key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, + dropout, i, **kwargs): + super(DecoderBlock, self).__init__(**kwargs) + self.i = i + self.attention1 = d2l.MultiHeadAttention( + key_size, query_size, value_size, num_hiddens, num_heads, dropout) + self.addnorm1 = AddNorm(norm_shape, dropout) + self.attention2 = d2l.MultiHeadAttention( + key_size, query_size, value_size, num_hiddens, num_heads, dropout) + self.addnorm2 = AddNorm(norm_shape, dropout) + self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, + num_hiddens) + self.addnorm3 = AddNorm(norm_shape, dropout) + + def forward(self, X, state): + enc_outputs, enc_valid_lens = state[0], state[1] + # 训练阶段,输出序列的所有词元都在同一时间处理, + # 因此state[2][self.i]初始化为None。 + # 预测阶段,输出序列是通过词元一个接着一个解码的, + # 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示 + if state[2][self.i] is None: + key_values = X + else: + key_values = paddle.concat((state[2][self.i], X), axis=1) + state[2][self.i] = key_values + if self.training: + batch_size, num_steps, _ = X.shape + # dec_valid_lens的开头:(batch_size,num_steps), + # 其中每一行是[1,2,...,num_steps] + dec_valid_lens = paddle.arange( + 1, num_steps + 1).tile((batch_size, 1)) + else: + dec_valid_lens = None + + # 自注意力 + X2 = self.attention1(X, key_values, key_values, dec_valid_lens) + Y = self.addnorm1(X, X2) + # 编码器-解码器注意力。 + # enc_outputs的开头:(batch_size,num_steps,num_hiddens) + Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens) + Z = self.addnorm2(Y, Y2) + return self.addnorm3(Z, self.ffn(Z)), state +``` + 为了便于在“编码器-解码器”注意力中进行缩放点积计算和残差连接中进行加法计算,[**编码器和解码器的特征维度都是`num_hiddens`。**] ```{.python .input} @@ -580,7 +748,7 @@ decoder_blk(X, state)[0].shape ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle decoder_blk = DecoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5, 0) decoder_blk.eval() X = d2l.ones((2, 100, 24)) @@ -596,7 +764,7 @@ state = [encoder_blk(X, valid_lens), valid_lens, [None]] decoder_blk(X, state, training=False)[0].shape ``` -现在我们构建了由`num_layers`个`DecoderBlock`实例组成的完整的[**transformer解码器**]。最后,通过一个全连接层计算所有`vocab_size`个可能的输出词元的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。 +现在我们构建了由`num_layers`个`DecoderBlock`实例组成的完整的[**Transformer解码器**]。最后,通过一个全连接层计算所有`vocab_size`个可能的输出词元的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。 ```{.python .input} class TransformerDecoder(d2l.AttentionDecoder): @@ -708,9 +876,49 @@ class TransformerDecoder(d2l.AttentionDecoder): return self._attention_weights ``` +```{.python .input} +#@tab paddle +class TransformerDecoder(d2l.AttentionDecoder): + def __init__(self, vocab_size, key_size, query_size, value_size, + num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, + num_heads, num_layers, dropout, **kwargs): + super(TransformerDecoder, self).__init__(**kwargs) + self.num_hiddens = num_hiddens + self.num_layers = num_layers + self.embedding = nn.Embedding(vocab_size, num_hiddens) + self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) + self.blks = nn.Sequential() + for i in range(num_layers): + self.blks.add_sublayer(str(i), + DecoderBlock(key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, + num_heads, dropout, i)) + self.dense = nn.Linear(num_hiddens, vocab_size) + + def init_state(self, enc_outputs, enc_valid_lens, *args): + return [enc_outputs, enc_valid_lens, [None] * self.num_layers] + + def forward(self, X, state): + X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) + self._attention_weights = [[None] * len(self.blks) for _ in range (2)] + for i, blk in enumerate(self.blks): + X, state = blk(X, state) + # 解码器自注意力权重 + self._attention_weights[0][ + i] = blk.attention1.attention.attention_weights + # “编码器-解码器”自注意力权重 + self._attention_weights[1][ + i] = blk.attention2.attention.attention_weights + return self.dense(X), state + + @property + def attention_weights(self): + return self._attention_weights +``` + ## [**训练**] -依照transformer架构来实例化编码器-解码器模型。在这里,指定transformer的编码器和解码器都是2层,都使用4头注意力。与 :numref:`sec_seq2seq_training`类似,为了进行序列到序列的学习,我们在“英语-法语”机器翻译数据集上训练transformer模型。 +依照Transformer架构来实例化编码器-解码器模型。在这里,指定Transformer的编码器和解码器都是2层,都使用4头注意力。与 :numref:`sec_seq2seq_training`类似,为了进行序列到序列的学习,下面在“英语-法语”机器翻译数据集上训练Transformer模型。 ```{.python .input} num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10 @@ -770,10 +978,32 @@ net = d2l.EncoderDecoder(encoder, decoder) d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ``` -训练结束后,使用transformer模型[**将一些英语句子翻译成法语**],并且计算它们的BLEU分数。 +```{.python .input} +#@tab paddle +num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10 +lr, num_epochs, device = 0.005, 200, d2l.try_gpu() +ffn_num_input, ffn_num_hiddens, num_heads = 32, 64, 4 +key_size, query_size, value_size = 32, 32, 32 +norm_shape = [32] + +train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps) + +encoder = TransformerEncoder( + len(src_vocab), key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, + num_layers, dropout) +decoder = TransformerDecoder( + len(tgt_vocab), key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, + num_layers, dropout) +net = d2l.EncoderDecoder(encoder, decoder) +d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) +``` + +训练结束后,使用Transformer模型[**将一些英语句子翻译成法语**],并且计算它们的BLEU分数。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .'] fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .'] for eng, fra in zip(engs, fras): @@ -794,7 +1024,7 @@ for eng, fra in zip(engs, fras): f'bleu {d2l.bleu(translation, fra, k=2):.3f}') ``` -当进行最后一个英语到法语的句子翻译工作时,让我们[**可视化transformer的注意力权重**]。编码器自注意力权重的形状为(编码器层数,注意力头数,`num_steps`或查询的数目,`num_steps`或“键-值”对的数目)。 +当进行最后一个英语到法语的句子翻译工作时,让我们[**可视化Transformer的注意力权重**]。编码器自注意力权重的形状为(编码器层数,注意力头数,`num_steps`或查询的数目,`num_steps`或“键-值”对的数目)。 ```{.python .input} #@tab all @@ -814,14 +1044,14 @@ d2l.show_heatmaps( ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle d2l.show_heatmaps( enc_attention_weights.cpu(), xlabel='Key positions', ylabel='Query positions', titles=['Head %d' % i for i in range(1, 5)], figsize=(7, 3.5)) ``` -[**为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。**]例如,我们用零填充被掩蔽住的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以*序列开始词元*(beginning-of-sequence,BOS)打头,再与后续输出的词元共同组成序列。 +[**为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。**]例如用零填充被掩蔽住的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以*序列开始词元*(beginning-of-sequence,BOS)打头,再与后续输出的词元共同组成序列。 ```{.python .input} dec_attention_weights_2d = [d2l.tensor(head[0]).tolist() @@ -865,6 +1095,20 @@ dec_self_attention_weights, dec_inter_attention_weights = tf.transpose( print(dec_self_attention_weights.shape, dec_inter_attention_weights.shape) ``` +```{.python .input} +#@tab paddle +dec_attention_weights_2d = [head[0].tolist() + for step in dec_attention_weight_seq + for attn in step for blk in attn for head in blk] +dec_attention_weights_filled = paddle.to_tensor( + pd.DataFrame(dec_attention_weights_2d).fillna(0.0).values) +dec_attention_weights = dec_attention_weights_filled.reshape(( + -1, 2, num_layers, num_heads, num_steps)) +dec_self_attention_weights, dec_inter_attention_weights = \ + dec_attention_weights.transpose((1, 2, 3, 0, 4)) +dec_self_attention_weights.shape, dec_inter_attention_weights.shape +``` + 由于解码器自注意力的自回归属性,查询不会对当前位置之后的“键-值”对进行注意力计算。 ```{.python .input} @@ -886,23 +1130,23 @@ d2l.show_heatmaps( figsize=(7, 3.5)) ``` -尽管transformer架构是为了“序列到序列”的学习而提出的,但正如我们将在本书后面提及的那样,transformer编码器或transformer解码器通常被单独用于不同的深度学习任务中。 +尽管Transformer架构是为了*序列到序列*的学习而提出的,但正如本书后面将提及的那样,Transformer编码器或Transformer解码器通常被单独用于不同的深度学习任务中。 ## 小结 -* transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。 -* 在transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。 -* transformer中的残差连接和层规范化是训练非常深度模型的重要工具。 -* transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。 +* Transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。 +* 在Transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。 +* Transformer中的残差连接和层规范化是训练非常深度模型的重要工具。 +* Transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。 ## 练习 -1. 在实验中训练更深的transformer将如何影响训练速度和翻译效果? -1. 在transformer中使用加性注意力取代缩放点积注意力是不是个好办法?为什么? -1. 对于语言模型,我们应该使用transformer的编码器还是解码器,或者两者都用?如何设计? -1. 如果输入序列很长,transformer会面临什么挑战?为什么? -1. 如何提高transformer的计算速度和内存使用效率?提示:可以参考论文 :cite:`Tay.Dehghani.Bahri.ea.2020`。 -1. 如果不使用卷积神经网络,如何设计基于transformer模型的图像分类任务?提示:可以参考Vision Transformer :cite:`Dosovitskiy.Beyer.Kolesnikov.ea.2021`。 +1. 在实验中训练更深的Transformer将如何影响训练速度和翻译效果? +1. 在Transformer中使用加性注意力取代缩放点积注意力是不是个好办法?为什么? +1. 对于语言模型,应该使用Transformer的编码器还是解码器,或者两者都用?如何设计? +1. 如果输入序列很长,Transformer会面临什么挑战?为什么? +1. 如何提高Transformer的计算速度和内存使用效率?提示:可以参考论文 :cite:`Tay.Dehghani.Bahri.ea.2020`。 +1. 如果不使用卷积神经网络,如何设计基于Transformer模型的图像分类任务?提示:可以参考Vision Transformer :cite:`Dosovitskiy.Beyer.Kolesnikov.ea.2021`。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/5755) @@ -911,3 +1155,7 @@ d2l.show_heatmaps( :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5756) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11845) +:end_tab: diff --git a/chapter_computational-performance/async-computation.md b/chapter_computational-performance/async-computation.md index 15daa2dcc..5d2a1fa7f 100644 --- a/chapter_computational-performance/async-computation.md +++ b/chapter_computational-performance/async-computation.md @@ -1,9 +1,9 @@ # 异步计算 :label:`sec_async` -今天的计算机是高度并行的系统,由多个CPU核、多个GPU、多个处理单元组成。通常每个CPU核有多个线程,每个设备通常有多个GPU,每个GPU有多个处理单元。总之,我们可以同时处理许多不同的事情,并且通常是在不同的设备上。不幸的是,Python并不善于编写并行和异步代码,至少在没有额外帮助的情况下不是好选择。归根结底,Python是单线程的,将来也是不太可能改变的。因此在诸多的深度学习框架中,MXNet和TensorFlow之类则采用了一种*异步编程*(asynchronous programming)模型来提高性能,而PyTorch则使用了Python自己的调度器来实现不同的性能权衡。对于PyTorch来说GPU操作在默认情况下是异步的。当你调用一个使用GPU的函数时,操作会排队到特定的设备上,但不一定要等到以后才执行。这允许我们并行执行更多的计算,包括在CPU或其他GPU上的操作。 +今天的计算机是高度并行的系统,由多个CPU核、多个GPU、多个处理单元组成。通常每个CPU核有多个线程,每个设备通常有多个GPU,每个GPU有多个处理单元。总之,我们可以同时处理许多不同的事情,并且通常是在不同的设备上。不幸的是,Python并不善于编写并行和异步代码,至少在没有额外帮助的情况下不是好选择。归根结底,Python是单线程的,将来也是不太可能改变的。因此在诸多的深度学习框架中,MXNet和TensorFlow之类则采用了一种*异步编程*(asynchronous programming)模型来提高性能,而PyTorch则使用了Python自己的调度器来实现不同的性能权衡。对PyTorch来说GPU操作在默认情况下是异步的。当调用一个使用GPU的函数时,操作会排队到特定的设备上,但不一定要等到以后才执行。这允许我们并行执行更多的计算,包括在CPU或其他GPU上的操作。 -因此,了解异步编程是如何工作的,通过主动地减少计算需求和相互依赖,有助于我们开发更高效的程序。这使我们能够减少内存开销并提高处理器利用率。 +因此,了解异步编程是如何工作的,通过主动地减少计算需求和相互依赖,有助于我们开发更高效的程序。这能够减少内存开销并提高处理器利用率。 ```{.python .input} from d2l import mxnet as d2l @@ -21,14 +21,29 @@ import torch from torch import nn ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import numpy, os, subprocess +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +d2l.try_gpu() +``` + ## 通过后端异步处理 :begin_tab:`mxnet` -作为热身,考虑一个简单问题:我们要生成一个随机矩阵并将其相乘。让我们在NumPy和`mxnet.np`中都这样做,看看有什么不同。 +作为热身,考虑一个简单问题:生成一个随机矩阵并将其相乘。让我们在NumPy和`mxnet.np`中都这样做,看看有什么不同。 :end_tab: :begin_tab:`pytorch` -作为热身,考虑一个简单问题:我们要生成一个随机矩阵并将其相乘。让我们在NumPy和PyTorch张量中都这样做,看看它们的区别。请注意,PyTorch的`tensor`是在GPU上定义的。 +作为热身,考虑一个简单问题:生成一个随机矩阵并将其相乘。让我们在NumPy和PyTorch张量中都这样做,看看它们的区别。请注意,PyTorch的`tensor`是在GPU上定义的。 +:end_tab: + +:begin_tab:`paddle` +作为热身,考虑一个简单问题:我们要生成一个随机矩阵并将其相乘。让我们在NumPy和飞桨张量中都这样做,看看它们的区别。请注意,飞桨的`tensor`是在GPU上定义的。 :end_tab: ```{.python .input} @@ -61,6 +76,23 @@ with d2l.Benchmark('torch'): b = torch.mm(a, a) ``` +```{.python .input} +#@tab paddle +# GPU计算热身 +a = paddle.randn(shape=(1000, 1000)) +b = paddle.mm(a, a) + +with d2l.Benchmark('numpy'): + for _ in range(10): + a = numpy.random.normal(size=(1000, 1000)) + b = numpy.dot(a, a) + +with d2l.Benchmark('paddle'): + for _ in range(10): + a = paddle.randn(shape=(1000, 1000)) + b = paddle.mm(a, a) +``` + :begin_tab:`mxnet` 通过MXNet的基准输出比较快了几个数量级。由于两者都在同一处理器上执行,因此一定有其他原因。强制MXNet在返回之前完成所有后端计算,这种强制说明了之前发生的情况:计算是由后端执行,而前端将控制权返回给了Python。 :end_tab: @@ -69,6 +101,10 @@ with d2l.Benchmark('torch'): 通过PyTorch的基准输出比较快了几个数量级。NumPy点积是在CPU上执行的,而PyTorch矩阵乘法是在GPU上执行的,后者的速度要快得多。但巨大的时间差距表明一定还有其他原因。默认情况下,GPU操作在PyTorch中是异步的。强制PyTorch在返回之前完成所有计算,这种强制说明了之前发生的情况:计算是由后端执行,而前端将控制权返回给了Python。 :end_tab: +:begin_tab:`paddle` +通过飞桨的基准输出比较快了几个数量级。NumPy点积是在CPU上执行的,而飞桨矩阵乘法是在GPU上执行的,后者的速度要快得多。但巨大的时间差距表明一定还有其他原因。默认情况下,GPU操作在飞桨中是异步的。强制飞桨在返回之前完成所有计算,这种强制说明了之前发生的情况:计算是由后端执行,而前端将控制权返回给了Python。 +:end_tab: + ```{.python .input} with d2l.Benchmark(): for _ in range(10): @@ -86,6 +122,15 @@ with d2l.Benchmark(): torch.cuda.synchronize(device) ``` +```{.python .input} +#@tab paddle +with d2l.Benchmark(): + for _ in range(10): + a = paddle.randn(shape=(1000, 1000)) + b = paddle.mm(a, a) + paddle.device.cuda.synchronize() +``` + :begin_tab:`mxnet` 广义上说,MXNet有一个用于与用户直接交互的前端(例如通过Python),还有一个由系统用来执行计算的后端。如 :numref:`fig_frontends`所示,用户可以用各种前端语言编写MXNet程序,如Python、R、Scala和C++。不管使用的前端编程语言是什么,MXNet程序的执行主要发生在C++实现的后端。由前端语言发出的操作被传递到后端执行。后端管理自己的线程,这些线程不断收集和执行排队的任务。请注意,要使其工作,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化相互依赖的操作。 :end_tab: @@ -94,11 +139,15 @@ with d2l.Benchmark(): 广义上说,PyTorch有一个用于与用户直接交互的前端(例如通过Python),还有一个由系统用来执行计算的后端。如 :numref:`fig_frontends`所示,用户可以用各种前端语言编写PyTorch程序,如Python和C++。不管使用的前端编程语言是什么,PyTorch程序的执行主要发生在C++实现的后端。由前端语言发出的操作被传递到后端执行。后端管理自己的线程,这些线程不断收集和执行排队的任务。请注意,要使其工作,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化相互依赖的操作。 :end_tab: +:begin_tab:`paddle` +广义上说,飞桨有一个用于与用户直接交互的前端(例如通过Python),还有一个由系统用来执行计算的后端。如 :numref:`fig_frontends`所示,用户可以用各种前端语言编写Python程序,如Python和C++。不管使用的前端编程语言是什么,飞桨程序的执行主要发生在C++实现的后端。由前端语言发出的操作被传递到后端执行。后端管理自己的线程,这些线程不断收集和执行排队的任务。请注意,要使其工作,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化相互依赖的操作。 +:end_tab: + ![编程语言前端和深度学习框架后端](../img/frontends.png) :width:`300px` :label:`fig_frontends` -让我们看另一个简单例子,以便更好地理解依赖关系图。 +接下来看看另一个简单例子,以便更好地理解依赖关系图。 ```{.python .input} x = np.ones((1, 2)) @@ -115,6 +164,14 @@ z = x * y + 2 z ``` +```{.python .input} +#@tab paddle +x = paddle.ones((1, 2)) +y = paddle.ones((1, 2)) +z = x * y + 2 +z +``` + ![后端跟踪计算图中各个步骤之间的依赖关系](../img/asyncgraph.svg) :label:`fig_asyncgraph` @@ -128,10 +185,10 @@ z :begin_tab:`mxnet` 有许多操作用于强制Python等待完成: -* 最明显的是,`npx.waitall()`不考虑计算指令的发出时间,等待直到所有计算完成。除非绝对必要,否则在实践中使用此运算符不是个好主意,因为它可能会导致较差的性能。 +* 最明显的是,`npx.waitall()`不考虑计算指令的发出时间,等待直到所有计算完成。除非绝对必要,否则在实践中使用此运算符不是个好主意,因为它可能会导致较差的性能; * 如果只想等待一个特定的变量可用,我们可以调用`z.wait_to_read()`。在这种情况下,MXNet阻止程序返回Python,直到计算出变量`z`为止。`z`之后的其他计算才可能很好地继续。 -让我们看看这在实践中是如何运作的。 +接下来看看这在实践中是如何运作的。 :end_tab: ```{.python .input} @@ -145,7 +202,7 @@ with d2l.Benchmark('wait_to_read'): ``` :begin_tab:`mxnet` -两个操作的完成时间大致相同。除了显式地阻塞操作之外,我们还建议你注意*隐式*的阻塞器。打印变量就是一个阻塞器,因为其要求变量可用。最后,通过`z.asnumpy()`转换为NumPy类型的变量和通过`z.item()`转换为标量也是阻塞器。因为NumPy中没有异步的概念,因此它需要像`print`函数(等待变量可用)一样访问这些值。 +两个操作的完成时间大致相同。除了显式地阻塞操作之外,建议注意*隐式*的阻塞器。打印变量就是一个阻塞器,因为其要求变量可用。最后,通过`z.asnumpy()`转换为NumPy类型的变量和通过`z.item()`转换为标量也是阻塞器。因为NumPy中没有异步的概念,因此它需要像`print`函数(等待变量可用)一样访问这些值。 频繁地将少量数据从MXNet的作用域复制到NumPy,可能会破坏原本高效代码的性能,因为每一个这样的操作都需要使用计算图来求得所有的中间结果,从而获得相关项,然后才能做其他事情。 :end_tab: @@ -163,7 +220,7 @@ with d2l.Benchmark('scalar conversion'): ## 改进计算 :begin_tab:`mxnet` -在重度多线程的系统中(即使普通笔记本电脑也有4个或更多线程,然而在多插槽服务器上这个数字可能超过256),调度操作的开销可能会变得非常大。这也是极度希望计算和调度是异步和并行的原因。为了说明这样做的好处,让我们看看按顺序(同步执行)或异步执行多次将变量递增$1$会发生什么情况。我们通过在每个加法之间插入`wait_to_read`障碍器来模拟同步执行。 +在重度多线程的系统中(即使普通笔记本电脑也有4个或更多线程,然而在多插槽服务器上这个数字可能超过256),调度操作的开销可能会变得非常大。这也是极度希望计算和调度是异步和并行的原因。为了说明这样做的好处,让我们看看按顺序(同步执行)或异步执行多次将变量递增$1$会发生什么情况。这里通过在每个加法之间插入`wait_to_read`障碍器来模拟同步执行。 :end_tab: ```{.python .input} @@ -178,14 +235,14 @@ with d2l.Benchmark('asynchronous'): npx.waitall() ``` -:begin_tab:`mxnet` Python前端线程和C++后端线程之间的简化交互可以概括如下: -1. 前端命令后端将计算任务`y = x + 1`插入队列。 -1. 然后后端从队列接收计算任务并执行。 +1. 前端命令后端将计算任务`y = x + 1`插入队列; +1. 然后后端从队列接收计算任务并执行; 1. 然后后端将计算结果返回到前端。 + 假设这三个阶段的持续时间分别为$t_1, t_2, t_3$。如果不使用异步编程,执行10000次计算所需的总时间约为$10000 (t_1+ t_2 + t_3)$。如果使用异步编程,因为前端不必等待后端为每个循环返回计算结果,执行$10000$次计算所花费的总时间可以减少到$t_1 + 10000 t_2 + t_3$(假设$10000 t_2 > 9999t_1$)。 -:end_tab: + ## 小结 @@ -200,11 +257,15 @@ Python前端线程和C++后端线程之间的简化交互可以概括如下: ## 练习 :begin_tab:`mxnet` -1. 我们上面提到,使用异步计算可以将执行$10000$次计算所需的总时间减少到$t_1 + 10000 t_2 + t_3$。为什么我们要假设这里是$10000 t_2 > 9999 t_1$? +1. 上面提到使用异步计算可以将执行$10000$次计算所需的总时间减少到$t_1 + 10000 t_2 + t_3$。为什么要假设这里是$10000 t_2 > 9999 t_1$? 1. 测量`waitall`和`wait_to_read`之间的差值。提示:执行多条指令并同步以获得中间结果。 :end_tab: :begin_tab:`pytorch` +1. 在CPU上,对本节中相同的矩阵乘法操作进行基准测试,仍然可以通过后端观察异步吗? +:end_tab: + +:begin_tab:`paddle` 1. 在CPU上,对本节中相同的矩阵乘法操作进行基准测试。你仍然可以通过后端观察异步吗? :end_tab: @@ -215,3 +276,7 @@ Python前端线程和C++后端线程之间的简化交互可以概括如下: :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2791) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11858) +:end_tab: \ No newline at end of file diff --git a/chapter_computational-performance/auto-parallelism.md b/chapter_computational-performance/auto-parallelism.md index 3d98006a4..4992f16cf 100644 --- a/chapter_computational-performance/auto-parallelism.md +++ b/chapter_computational-performance/auto-parallelism.md @@ -1,11 +1,11 @@ # 自动并行 :label:`sec_auto_para` -深度学习框架(例如,MxNet和PyTorch)会在后端自动构建计算图。利用计算图,系统可以了解所有依赖关系,并且可以选择性地并行执行多个不相互依赖的任务以提高速度。例如, :numref:`sec_async`中的 :numref:`fig_asyncgraph`独立初始化两个变量。因此,系统可以选择并行执行它们。 +深度学习框架(例如,MxNet、飞桨和PyTorch)会在后端自动构建计算图。利用计算图,系统可以了解所有依赖关系,并且可以选择性地并行执行多个不相互依赖的任务以提高速度。例如, :numref:`sec_async`中的 :numref:`fig_asyncgraph`独立初始化两个变量。因此,系统可以选择并行执行它们。 -通常情况下单个操作符将使用所有CPU或单个GPU上的所有计算资源。例如,即使在一台机器上有多个CPU处理器,`dot` 操作符也将使用所有CPU上的所有核心(和线程)。这样的行为同样适用于单个GPU。因此,并行化对于单设备计算机来说并不是很有用,而并行化对于多个设备就很重要了。虽然并行化通常应用在多个GPU之间,但增加本地CPU以后还将提高少许性能。例如, :cite:`Hadjis.Zhang.Mitliagkas.ea.2016`则把结合GPU和CPU的训练应用到计算机视觉模型中。借助自动并行化框架的便利性,我们可以依靠几行Python代码实现相同的目标。更广泛地考虑,我们对自动并行计算的讨论主要集中在使用CPU和GPU的并行计算上,以及计算和通信的并行化内容。 +通常情况下单个操作符将使用所有CPU或单个GPU上的所有计算资源。例如,即使在一台机器上有多个CPU处理器,`dot`操作符也将使用所有CPU上的所有核心(和线程)。这样的行为同样适用于单个GPU。因此,并行化对单设备计算机来说并不是很有用,而并行化对于多个设备就很重要了。虽然并行化通常应用在多个GPU之间,但增加本地CPU以后还将提高少许性能。例如, :cite:`Hadjis.Zhang.Mitliagkas.ea.2016`则把结合GPU和CPU的训练应用到计算机视觉模型中。借助自动并行化框架的便利性,我们可以依靠几行Python代码实现相同的目标。对自动并行计算的讨论主要集中在使用CPU和GPU的并行计算上,以及计算和通信的并行化内容。 -请注意,我们至少需要两个GPU来运行本节中的实验。 +请注意,本节中的实验至少需要两个GPU来运行。 ```{.python .input} from d2l import mxnet as d2l @@ -19,9 +19,18 @@ from d2l import torch as d2l import torch ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import numpy as np +``` + ## 基于GPU的并行计算 -让我们从定义一个具有参考性的用于测试的工作负载开始:下面的`run`函数将执行$10$ 次“矩阵-矩阵”乘法时需要使用的数据分配到两个变量(`x_gpu1`和`x_gpu2`)中,这两个变量分别位于我们选择的不同设备上。 +从定义一个具有参考性的用于测试的工作负载开始:下面的`run`函数将执行$10$次*矩阵-矩阵*乘法时需要使用的数据分配到两个变量(`x_gpu1`和`x_gpu2`)中,这两个变量分别位于选择的不同设备上。 ```{.python .input} devices = d2l.try_all_gpus() @@ -42,12 +51,28 @@ x_gpu1 = torch.rand(size=(4000, 4000), device=devices[0]) x_gpu2 = torch.rand(size=(4000, 4000), device=devices[1]) ``` +```{.python .input} +#@tab paddle +devices = d2l.try_all_gpus() +def run(x, index=0): + paddle.set_device(f"gpu:{index}") + return [x.matmul(x) for _ in range(50)] + +data = np.random.rand(4000, 4000) +x_gpu1 = paddle.to_tensor(data, place=devices[0]) +x_gpu2 = paddle.to_tensor(data, place=devices[1]) +``` + :begin_tab:`mxnet` -现在我们使用函数来处理数据。我们通过在测量之前预热设备(对设备执行一次传递)来确保缓存的作用不影响最终的结果。 +现在使用函数来处理数据。通过在测量之前需要预热设备(对设备执行一次传递)来确保缓存的作用不影响最终的结果。 :end_tab: :begin_tab:`pytorch` -现在我们使用函数来数据。我们通过在测量之前预热设备(对设备执行一次传递)来确保缓存的作用不影响最终的结果。`torch.cuda.synchronize()`函数将会等待一个CUDA设备上的所有流中的所有核心的计算完成。函数接受一个`device`参数,代表是哪个设备需要同步。如果device参数是`None`(默认值),它将使用`current_device()`找出的当前设备。 +现在使用函数来处理数据。通过在测量之前需要预热设备(对设备执行一次传递)来确保缓存的作用不影响最终的结果。`torch.cuda.synchronize()`函数将会等待一个CUDA设备上的所有流中的所有核心的计算完成。函数接受一个`device`参数,代表是哪个设备需要同步。如果device参数是`None`(默认值),它将使用`current_device()`找出的当前设备。 +:end_tab: + +:begin_tab:`paddle` +现在我们使用函数来数据。我们通过在测量之前预热设备(对设备执行一次传递)来确保缓存的作用不影响最终的结果。`paddle.device.cuda.synchronize()`函数将会等待一个CUDA设备上的所有流中的所有核心的计算完成。函数接受一个`device`参数,代表是哪个设备需要同步。如果device参数是`None`(默认值),它将使用`current_device()`找出的当前设备。 :end_tab: ```{.python .input} @@ -80,11 +105,31 @@ with d2l.Benchmark('GPU2 time'): torch.cuda.synchronize(devices[1]) ``` +```{.python .input} +#@tab paddle +run(x_gpu1, 0) +run(x_gpu2, 1) # 预热设备 +paddle.device.cuda.synchronize(devices[0]) +paddle.device.cuda.synchronize(devices[1]) + +with d2l.Benchmark('GPU1 time'): + run(x_gpu1, 0) + paddle.device.cuda.synchronize(devices[0]) + +with d2l.Benchmark('GPU2 time'): + run(x_gpu2, 1) + paddle.device.cuda.synchronize(devices[1]) +``` + :begin_tab:`mxnet` -如果我们删除两个任务之间的`waitall`语句,系统就可以在两个设备上自动实现并行计算。 +如果删除两个任务之间的`waitall`语句,系统就可以在两个设备上自动实现并行计算。 :end_tab: :begin_tab:`pytorch` +如果删除两个任务之间的`synchronize`语句,系统就可以在两个设备上自动实现并行计算。 +:end_tab: + +:begin_tab:`paddle` 如果我们删除两个任务之间的`synchronize`语句,系统就可以在两个设备上自动实现并行计算。 :end_tab: @@ -103,11 +148,19 @@ with d2l.Benchmark('GPU1 & GPU2'): torch.cuda.synchronize() ``` +```{.python .input} +#@tab paddle +with d2l.Benchmark('GPU1 & GPU2'): + run(x_gpu1, 0) + run(x_gpu2, 1) + paddle.device.cuda.synchronize() +``` + 在上述情况下,总执行时间小于两个部分执行时间的总和,因为深度学习框架自动调度两个GPU设备上的计算,而不需要用户编写复杂的代码。 ## 并行计算与通信 -在许多情况下,我们需要在不同的设备之间移动数据,比如在CPU和GPU之间,或者在不同的GPU之间。例如,当我们打算执行分布式优化时,就需要移动数据来聚合多个加速卡上的梯度。让我们通过在GPU上计算,然后将结果复制回CPU来模拟这个过程。 +在许多情况下,我们需要在不同的设备之间移动数据,比如在CPU和GPU之间,或者在不同的GPU之间。例如,当执行分布式优化时,就需要移动数据来聚合多个加速卡上的梯度。让我们通过在GPU上计算,然后将结果复制回CPU来模拟这个过程。 ```{.python .input} def copy_to_cpu(x): @@ -136,12 +189,30 @@ with d2l.Benchmark('复制到CPU'): torch.cuda.synchronize() ``` +```{.python .input} +#@tab paddle +def copy_to_cpu(x): + return [paddle.to_tensor(y, place=paddle.CPUPlace()) for y in x] + +with d2l.Benchmark('在GPU1上运行'): + y = run(x_gpu1, 0) + paddle.device.cuda.synchronize() + +with d2l.Benchmark('复制到CPU'): + y_cpu = copy_to_cpu(y) + paddle.device.cuda.synchronize() +``` + :begin_tab:`mxnet` -这种方式效率不高。注意到当列表中的其余部分还在计算时,我们可能就已经开始将`y`的部分复制到CPU了。例如,当我们计算一个小批量的梯度时,某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时就开始使用PCI-Express总线带宽来移动数据对我们是有利的。删除这两个部分之间的`waitall`让我们模拟这个场景。 +这种方式效率不高。注意到当列表中的其余部分还在计算时,我们可能就已经开始将`y`的部分复制到CPU了。例如,当计算一个小批量的梯度时,某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时就开始使用PCI-Express总线带宽来移动数据是有利的。删除这两个部分之间的`waitall`以模拟这个场景。 :end_tab: :begin_tab:`pytorch` -这种方式效率不高。注意到当列表中的其余部分还在计算时,我们可能就已经开始将`y`的部分复制到CPU了。例如,当我们计算一个小批量的(反传)梯度时。某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时就开始使用PCI-Express总线带宽来移动数据对我们是有利的。在PyTorch中,`to()`和`copy_()`等函数都允许显式的`non_blocking`参数,这允许在不需要同步时调用方可以绕过同步。设置`non_blocking=True`让我们模拟这个场景。 +这种方式效率不高。注意到当列表中的其余部分还在计算时,我们可能就已经开始将`y`的部分复制到CPU了。例如,当计算一个小批量的(反传)梯度时。某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时就开始使用PCI-Express总线带宽来移动数据是有利的。在PyTorch中,`to()`和`copy_()`等函数都允许显式的`non_blocking`参数,这允许在不需要同步时调用方可以绕过同步。设置`non_blocking=True`以模拟这个场景。 +:end_tab: + +:begin_tab:`paddle` +这种方式效率不高。注意到当列表中的其余部分还在计算时,我们可能就已经开始将`y`的部分复制到CPU了。例如,当我们计算一个小批量的(反传)梯度时。某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时就开始使用PCI-Express总线带宽来移动数据对我们是有利的。 :end_tab: ```{.python .input} @@ -159,9 +230,17 @@ with d2l.Benchmark('在GPU1上运行并复制到CPU'): torch.cuda.synchronize() ``` +```{.python .input} +#@tab paddle +with d2l.Benchmark('在GPU1上运行并复制到CPU'): + y = run(x_gpu1) + y_cpu = copy_to_cpu(y) + paddle.device.cuda.synchronize() +``` + 两个操作所需的总时间少于它们各部分操作所需时间的总和。请注意,与并行计算的区别是通信操作使用的资源:CPU和GPU之间的总线。事实上,我们可以在两个设备上同时进行计算和通信。如上所述,计算和通信之间存在的依赖关系是必须先计算`y[i]`,然后才能将其复制到CPU。幸运的是,系统可以在计算`y[i]`的同时复制`y[i-1]`,以减少总的运行时间。 -最后,我们给出了一个简单的两层多层感知机在CPU和两个GPU上训练时的计算图及其依赖关系的例子,如 :numref:`fig_twogpu`所示。手动调度由此产生的并行程序将是相当痛苦的。这就是基于图的计算后端进行优化的优势所在。 +最后,本节给出了一个简单的两层多层感知机在CPU和两个GPU上训练时的计算图及其依赖关系的例子,如 :numref:`fig_twogpu`所示。手动调度由此产生的并行程序将是相当痛苦的。这就是基于图的计算后端进行优化的优势所在。 ![在一个CPU和两个GPU上的两层的多层感知机的计算图及其依赖关系](../img/twogpu.svg) :label:`fig_twogpu` @@ -177,7 +256,7 @@ with d2l.Benchmark('在GPU1上运行并复制到CPU'): 1. 在本节定义的`run`函数中执行了八个操作,并且操作之间没有依赖关系。设计一个实验,看看深度学习框架是否会自动地并行地执行它们。 1. 当单个操作符的工作量足够小,即使在单个CPU或GPU上,并行化也会有所帮助。设计一个实验来验证这一点。 1. 设计一个实验,在CPU和GPU这两种设备上使用并行计算和通信。 -1. 使用诸如NVIDIA的[Nsight](https://developer.nvidia.com/nsight-compute-2019_5) 之类的调试器来验证你的代码是否有效。 +1. 使用诸如NVIDIA的[Nsight](https://developer.nvidia.com/nsight-compute-2019_5)之类的调试器来验证代码是否有效。 1. 设计并实验具有更加复杂的数据依赖关系的计算任务,以查看是否可以在提高性能的同时获得正确的结果。 :begin_tab:`mxnet` @@ -187,3 +266,7 @@ with d2l.Benchmark('在GPU1上运行并复制到CPU'): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2794) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11859) +:end_tab: \ No newline at end of file diff --git a/chapter_computational-performance/hardware.md b/chapter_computational-performance/hardware.md index c8edb8ca8..fcb892b02 100644 --- a/chapter_computational-performance/hardware.md +++ b/chapter_computational-performance/hardware.md @@ -6,16 +6,16 @@ ![每个程序员都应该知道的延迟数字](../img/latencynumbers.png) :label:`fig_latencynumbers` -你也可以通过 :numref:`fig_latencynumbers`进行简单的了解,图片源自科林·斯科特的[互动帖子](https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html),在帖子中很好地概述了过去十年的进展。原始的数字是取自于杰夫迪恩的[Stanford讲座](https://static.googleusercontent.com/media/research.google.com/en//people/jeff/Stanford-DL-Nov-2010.pdf)。下面的讨论解释了这些数字的一些基本原理,以及它们如何指导我们去设计算法。下面的讨论是非常笼统和粗略的。很显然,它并不能代替一门完整的课程,而只是为了给统计建模者提供足够的信息,让他们做出合适的设计决策。对于计算机体系结构的深入概述,我们建议读者参考 :cite:`Hennessy.Patterson.2011`或关于该主题的最新课程,例如[Arste Asanovic](http://inst.eecs.berkeley.edu/~cs152/sp19/)。 +也可以通过 :numref:`fig_latencynumbers`进行简单的了解,图片源自科林·斯科特的[互动帖子](https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html),在帖子中很好地概述了过去十年的进展。原始的数字是取自于杰夫迪恩的[Stanford讲座](https://static.googleusercontent.com/media/research.google.com/en//people/jeff/Stanford-DL-Nov-2010.pdf)。下面的讨论解释了这些数字的一些基本原理,以及它们如何指导我们去设计算法。下面的讨论是非常笼统和粗略的。很显然,它并不能代替一门完整的课程,而只是为了给统计建模者提供足够的信息,让他们做出合适的设计决策。对于计算机体系结构的深入概述,建议读者参考 :cite:`Hennessy.Patterson.2011`或关于该主题的最新课程,例如[Arste Asanovic](http://inst.eecs.berkeley.edu/~cs152/sp19/)。 ## 计算机 大多数深度学习研究者和实践者都可以使用一台具有相当数量的内存、计算资源、某种形式的加速器(如一个或者多个GPU)的计算机。计算机由以下关键部件组成: -* 一个处理器(也被称为CPU),它除了能够运行操作系统和许多其他功能之外,还能够执行我们给它的程序,通常由$8$个或更多个核心组成。 -* 内存(随机访问存储,RAM)用于存储和检索计算结果,如权重向量和激活参数,以及训练数据。 -* 一个或多个以太网连接,速度从1GB/s到100GB/s不等。在高端服务器上可能用到更高级的互连。 -* 高速扩展总线(PCIe)用于系统连接一个或多个GPU。服务器最多有$8$个加速卡,通常以更高级的拓扑方式连接,而桌面系统则有$1$个或$2$个加速卡,具体取决于用户的预算和电源负载的大小。 +* 一个处理器(也被称为CPU),它除了能够运行操作系统和许多其他功能之外,还能够执行给定的程序。它通常由$8$个或更多个核心组成; +* 内存(随机访问存储,RAM)用于存储和检索计算结果,如权重向量和激活参数,以及训练数据; +* 一个或多个以太网连接,速度从1GB/s到100GB/s不等。在高端服务器上可能用到更高级的互连; +* 高速扩展总线(PCIe)用于系统连接一个或多个GPU。服务器最多有$8$个加速卡,通常以更高级的拓扑方式连接,而桌面系统则有$1$个或$2$个加速卡,具体取决于用户的预算和电源负载的大小; * 持久性存储设备,如磁盘驱动器、固态驱动器,在许多情况下使用高速扩展总线连接。它为系统需要的训练数据和中间检查点需要的存储提供了足够的传输速度。 ![计算机组件的连接](../img/mobo-symbol.svg) @@ -23,23 +23,21 @@ 如 :numref:`fig_mobo-symbol`所示,高速扩展总线由直接连接到CPU的多个通道组成,将CPU与大多数组件(网络、GPU和存储)连接在一起。例如,AMD的Threadripper3有$64$个PCIe4.0通道,每个通道都能够双向传输16Gbit/s的数据。内存直接连接到CPU,总带宽高达100GB/s。 -当我们在计算机上运行代码时,我们需要将数据转移到处理器上(CPU或GPU)执行计算,然后将结果从处理器移回到随机访问存储和持久存储器中。因此,为了获得良好的性能,我们需要确保每一步工作都能无缝链接,而不希望系统中的任何一部分成为主要的瓶颈。例如,如果不能快速加载图像,那么处理器就无事可做。同样地,如果不能快速移动矩阵到CPU(或GPU)上,那么CPU(或GPU)就会无法全速运行。最后,如果希望在网络上同步多台计算机,那么网络就不应该拖累计算速度。一种选择是通信和计算交错进行。接下来,我们将详细地了解各个组件。 +当我们在计算机上运行代码时,需要将数据转移到处理器上(CPU或GPU)执行计算,然后将结果从处理器移回到随机访问存储和持久存储器中。因此,为了获得良好的性能,需要确保每一步工作都能无缝链接,而不希望系统中的任何一部分成为主要的瓶颈。例如,如果不能快速加载图像,那么处理器就无事可做。同样地,如果不能快速移动矩阵到CPU(或GPU)上,那么CPU(或GPU)就会无法全速运行。最后,如果希望在网络上同步多台计算机,那么网络就不应该拖累计算速度。一种选择是通信和计算交错进行。接下来将详细地介绍各个组件。 ## 内存 最基本的内存主要用于存储需要随时访问的数据。目前,CPU的内存通常为[DDR4](https://en.wikipedia.org/wiki/DDR4_SDRAM)类型,每个模块提供20-25Gb/s的带宽。每个模块都有一条$64$位宽的总线。通常使用成对的内存模块来允许多个通道。CPU有$2$到$4$个内存通道,也就是说,它们内存带宽的峰值在40GB/s到100GB/s之间。一般每个通道有两个物理存储体(bank)。例如AMD的Zen 3 Threadripper有$8$个插槽。 -虽然这些数字令人印象深刻,但实际上它们只能说明了一部分故事。当我们想要从内存中读取一部分内容时,我们需要先告诉内存模块在哪里可以找到信息。也就是说,我们需要先将*地址*(address)发送到RAM。然后我们可以选择只读取一条$64$位记录还是一长串记录。后者称为*突发读取*(burst read)。概括地说,向内存发送地址并设置传输大约需要100ns(细节取决于所用内存芯片的特定定时系数),每个后续传输只需要0.2ns。总之,第一次读取的成本是后续读取的500倍!请注意,我们每秒最多可以执行一千万次随机读取。这说明应该尽可能地避免随机内存访问,而是使用突发模式读取和写入。 +虽然这些数字令人印象深刻,但实际上它们只能说明了一部分故事。当我们想要从内存中读取一部分内容时,需要先告诉内存模块在哪里可以找到信息。也就是说,我们需要先将*地址*(address)发送到RAM。然后我们可以选择只读取一条$64$位记录还是一长串记录。后者称为*突发读取*(burst read)。概括地说,向内存发送地址并设置传输大约需要100ns(细节取决于所用内存芯片的特定定时系数),每个后续传输只需要0.2ns。总之,第一次读取的成本是后续读取的500倍!请注意,每秒最多可以执行一千万次随机读取。这说明应该尽可能地避免随机内存访问,而是使用突发模式读取和写入。 -当考虑到我们拥有多个物理存储体时,事情就更加复杂了。每个存储体大部分时候都可以独立地读取内存。这意味着两件事。一方面,如果随机读操作均匀分布在内存中,那么有效的随机读操作次数将高达4倍。这也意味着执行随机读取仍然不是一个好主意,因为突发读取的速度也快了4倍。另一方面,由于内存对齐是$64$位边界,因此最好将任何数据结构与相同的边界对齐。当设置了适当的标志时,编译器基本上就是[自动化](https://en.wikipedia.org/wiki/Data_structure_alignment)地执行对齐操作。我们鼓励好奇的读者回顾一下[Zeshan Chishti关于DRAM的讲座](http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf)。 - -因为GPU的处理单元比CPU多得多,因此它对内存带宽的需要也更高。解决这种问题大体上有两种选择。首要方法是使内存总线变得更宽。例如:NVIDIA的RTX 2080Ti有一条$352$位宽的总线,这样就可以同时传输更多的信息。再有方法就是在GPU中使用特定的高性能内存。一种选择是如NVIDIA的消费级设备RTX和Titan系列中通常使用[GDDR6](https://en.wikipedia.org/wiki/GDDR6_SDRAM)芯片,其总带宽超过500GB/s。另一种选择是使用HBM(高带宽存储器)模块。这些模块使用截然不同的接口在专用硅片上与GPU直接连在一起。这导致其非常昂贵,通常仅限于在高端服务器的芯片上使用,如NVIDIA Volta V100系列的加速卡。 +当考虑到拥有多个物理存储体时,事情就更加复杂了。每个存储体大部分时候都可以独立地读取内存。这意味着两件事。一方面,如果随机读操作均匀分布在内存中,那么有效的随机读操作次数将高达4倍。这也意味着执行随机读取仍然不是一个好主意,因为突发读取的速度也快了4倍。另一方面,由于内存对齐是$64$位边界,因此最好将任何数据结构与相同的边界对齐。当设置了适当的标志时,编译器基本上就是[自动化](https://en.wikipedia.org/wiki/Data_structure_alignment)地执行对齐操作。我们鼓励好奇的读者回顾一下[Zeshan Chishti关于DRAM的讲座](http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf)。 GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得多。总的来说,解决这些问题有两种选择。首先是使内存总线变得更宽。例如,NVIDIA的RTX 2080Ti有一条352位宽的总线。这样就可以同时传输更多的信息。其次,GPU使用特定的高性能内存。消费级设备,如NVIDIA的RTX和Titan系列,通常使用[GDDR6](https://en.wikipedia.org/wiki/GDDR6_SDRAM)芯片,总带宽超过500GB/s。另一种选择是使用HBM(高带宽存储器)模块。它们使用截然不同的接口,直接与专用硅片上的GPU连接。这使得它们非常昂贵,通常仅限于高端服务器芯片,如NVIDIA Volta V100系列加速卡。毫不意外的是GPU的内存通常比CPU的内存小得多,因为前者的成本更高。就目的而言,它们的性能与特征大体上是相似的,只是GPU的速度更快。就本书而言,我们完全可以忽略细节,因为这些技术只在调整GPU核心以获得高吞吐量时才起作用。 ## 存储器 -我们看到随机访问存储的一些关键特性是 *带宽*(bandwidth)和 *延迟*(latency)。存储设备也是如此,只是不同设备之间的特性差异可能更大。 +随机访问存储的一些关键特性是 *带宽*(bandwidth)和 *延迟*(latency)。存储设备也是如此,只是不同设备之间的特性差异可能更大。 ### 硬盘驱动器 @@ -49,7 +47,7 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 ### 固态驱动器 -固态驱动器(solid state drives,SSD)使用闪存持久地存储信息。这允许更快地访问存储的记录。现代的固态驱动器的IOPs可以达到$10$万到$50$万,比硬盘驱动器快3个数量级。而且,它们的带宽可以达到1-3GB/s,比硬盘驱动器快一个数量级。这些改进听起来好的难以置信,而事实上受固态驱动器的设计方式,它仍然存在下面的附加条件: +固态驱动器(solid state drives,SSD)使用闪存持久地存储信息。这允许更快地访问存储的记录。现代的固态驱动器的IOPs可以达到$10$万到$50$万,比硬盘驱动器快3个数量级。而且,它们的带宽可以达到1-3GB/s,比硬盘驱动器快一个数量级。这些改进听起来好的难以置信,而事实上受固态驱动器的设计方式,它仍然存在下面的附加条件。 * 固态驱动器以块的方式(256KB或更大)存储信息。块只能作为一个整体来写入,因此需要耗费大量的时间,导致固态驱动器在按位随机写入时性能非常差。而且通常数据写入需要大量的时间还因为块必须被读取、擦除,然后再重新写入新的信息。如今固态驱动器的控制器和固件已经开发出了缓解这种情况的算法。尽管有了算法,写入速度仍然会比读取慢得多,特别是对于QLC(四层单元)固态驱动器。提高性能的关键是维护操作的“队列”,在队列中尽可能地优先读取和写入大的块。 * 固态驱动器中的存储单元磨损得比较快(通常在几千次写入之后就已经老化了)。磨损程度保护算法能够将退化平摊到许多单元。也就是说,不建议将固态驱动器用于交换分区文件或大型日志文件。 @@ -57,16 +55,16 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 ### 云存储 -云存储提供了一系列可配置的性能。也就是说,虚拟机的存储在数量和速度上都能根据用户需要进行动态分配。我们建议用户在延迟太高时(例如,在训练期间存在许多小记录时)增加IOPs的配置数。 +云存储提供了一系列可配置的性能。也就是说,虚拟机的存储在数量和速度上都能根据用户需要进行动态分配。建议用户在延迟太高时(例如,在训练期间存在许多小记录时)增加IOPs的配置数。 ## CPU -中央处理器(central processing unit,CPU)是任何计算机的核心。它们由许多关键组件组成:*处理器核心*(processor cores)用于执行机器代码的、*总线*(bus)用于连接不同组件(注意,总线会因为处理器型号、各代产品和供应商之间的特定拓扑结构有明显不同)和*缓存*(cach)相比主内存实现更高的读取带宽和更低的延迟内存访问。最后,因为高性能线性代数和卷积运算常见于媒体处理和机器学习中,所以几乎所有的现代CPU都包含*向量处理单元*(vector processing unit)为这些计算提供辅助。 +中央处理器(central processing unit,CPU)是任何计算机的核心。它们由许多关键组件组成:*处理器核心*(processor cores)用于执行机器代码的;*总线*(bus)用于连接不同组件(注意,总线会因为处理器型号、各代产品和供应商之间的特定拓扑结构有明显不同);*缓存*(cach)相比主内存实现更高的读取带宽和更低的延迟内存访问。最后,因为高性能线性代数和卷积运算常见于媒体处理和机器学习中,所以几乎所有的现代CPU都包含*向量处理单元*(vector processing unit)为这些计算提供辅助。 ![Intel Skylake消费级四核CPU](../img/skylake.svg) :label:`fig_skylake` - :numref:`fig_skylake`描述了Intel Skylake消费级四核CPU。它包含一个集成GPU、缓存和一个连接四个核心的环总线。例如:以太网、WiFi、蓝牙、SSD控制器和USB这些外围设备要么是芯片组的一部分,要么通过PCIe直接连接到CPU。 + :numref:`fig_skylake`描述了Intel Skylake消费级四核CPU。它包含一个集成GPU、缓存和一个连接四个核心的环总线。例如,以太网、WiFi、蓝牙、SSD控制器和USB这些外围设备要么是芯片组的一部分,要么通过PCIe直接连接到CPU。 ### 微体系结构 @@ -88,7 +86,7 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 ### 缓存 -考虑以下情况:我们有一个中等规模的$4$核心的CPU,如 :numref:`fig_skylake`所示,运行在2GHz频率。此外,假设向量处理单元启用了$256$位带宽的AVX2,其IPC(指令/时钟)计数为1。进一步假设从内存中获取用于AVX2操作的指令至少需要一个寄存器。这意味着CPU每个时钟周期需要消耗$4 \times 256 \text{ bit} = 128 \text{ bytes}$的数据。除非我们能够每秒向处理器传输$2 \times 10^9 \times 128 = 256 \times 10^9$字节,否则用于处理的数据将会不足。不幸的是,这种芯片的存储器接口仅支持20-40Gb/s的数据传输,即少了一个数量级。解决方法是尽可能避免从内存中加载新数据,而是将数据放在CPU的缓存上。这就是使用缓存的地方。通常使用以下名称或概念: +考虑以下情况:我们有一个中等规模的$4$核心的CPU,如 :numref:`fig_skylake`所示,运行在2GHz频率。此外,假设向量处理单元启用了$256$位带宽的AVX2,其IPC(指令/时钟)计数为1。进一步假设从内存中获取用于AVX2操作的指令至少需要一个寄存器。这意味着CPU每个时钟周期需要消耗$4 \times 256 \text{ bit} = 128 \text{ bytes}$的数据。除非我们能够每秒向处理器传输$2 \times 10^9 \times 128 = 256 \times 10^9$字节,否则用于处理的数据将会不足。不幸的是,这种芯片的存储器接口仅支持20-40Gb/s的数据传输,即少了一个数量级。解决方法是尽可能避免从内存中加载新数据,而是将数据放在CPU的缓存上。这就是使用缓存的地方。通常使用以下名称或概念。 * **寄存器**,严格来说不是缓存的一部分,用于帮助组织指令。也就是说,寄存器是CPU可以以时钟速度访问而没有延迟的存储位置。CPU有几十个寄存器,因此有效地使用寄存器取决于编译器(或程序员)。例如,C语言有一个`register`关键字。 * **一级缓存**是应对高内存带宽要求的第一道防线。一级缓存很小(常见的大小可能是32-64KB),内容通常分为数据和指令。当数据在一级缓存中被找到时,其访问速度非常快,如果没有在那里找到,搜索将沿着缓存层次结构向下寻找。 @@ -97,7 +95,7 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 预测下一步需要哪个存储设备是优化芯片设计的关键参数之一。例如,建议以*向前*的方向遍历内存,因为大多数缓存算法将试图*向前读取*(read forward)而不是向后读取。同样,将内存访问模式保持在本地也是提高性能的一个好方法。 -添加缓存是一把双刃剑。一方面,它能确保处理器核心不缺乏数据。但同时,它也增加了芯片尺寸,消耗了原本可以用来提高处理能力的面积。此外,*缓存未命中* 的代价可能会很昂贵。考虑最坏的情况,如 :numref:`fig_falsesharing`所示的*错误共享*(false sharing)。当处理器$1$上的线程请求数据时,内存位置缓存在处理器$0$上。为了满足获取需要,处理器$0$需要停止它正在做的事情,将信息写回主内存,然后让处理器$1$从内存中读取它。在此操作期间,两个处理器都需要等待。与高效的单处理器实现相比,这种代码在多个处理器上运行的速度可能要慢得多。这就是为什么缓存大小(除了物理大小之外)有实际限制的另一个原因。 +添加缓存是一把双刃剑。一方面,它能确保处理器核心不缺乏数据。但同时,它也增加了芯片尺寸,消耗了原本可以用来提高处理能力的面积。此外,*缓存未命中*的代价可能会很昂贵。考虑最坏的情况,如 :numref:`fig_falsesharing`所示的*错误共享*(false sharing)。当处理器$1$上的线程请求数据时,内存位置缓存在处理器$0$上。为了满足获取需要,处理器$0$需要停止它正在做的事情,将信息写回主内存,然后让处理器$1$从内存中读取它。在此操作期间,两个处理器都需要等待。与高效的单处理器实现相比,这种代码在多个处理器上运行的速度可能要慢得多。这就是为什么缓存大小(除了物理大小之外)有实际限制的另一个原因。 ![错误共享(图片由英特尔提供)](../img/falsesharing.svg) :label:`fig_falsesharing` @@ -114,7 +112,7 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 :width:`150px` :label:`fig_turing_processing_block` -接下来,将$12$ 个流式多处理器分组为图形处理集群,这些集群构成了高端TU102处理器。充足的内存通道和二级缓存完善了配置。 :numref:`fig_turing`有相关的细节。设计这种设备的原因之一是可以根据需要独立地添加或删除模块,从而满足设计更紧凑的芯片和处理良品率问题(故障模块可能无法激活)的需要。幸运的是,在CUDA和框架代码层之下,这类设备的编程对深度学习的临时研究员隐藏得很好。特别是,只要有可用的资源GPU上就可以同时执行多个程序。尽管如此,了解设备的局限性是值得的,以避免对应的设备内存的型号不合适。 +接下来,将$12$个流式多处理器分组为图形处理集群,这些集群构成了高端TU102处理器。充足的内存通道和二级缓存完善了配置。 :numref:`fig_turing`有相关的细节。设计这种设备的原因之一是可以根据需要独立地添加或删除模块,从而满足设计更紧凑的芯片和处理良品率问题(故障模块可能无法激活)的需要。幸运的是,在CUDA和框架代码层之下,这类设备的编程对深度学习的临时研究员隐藏得很好。特别是,只要有可用的资源GPU上就可以同时执行多个程序。尽管如此,了解设备的局限性是值得的,以避免对应的设备内存的型号不合适。 ![NVIDIA Turing架构(图片由英伟达提供)](../img/turing.png) :width:`350px` @@ -130,12 +128,12 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 ## 网络和总线 -每当单个设备不足以进行优化时,我们就需要来回传输数据以实现同步处理,于是网络和总线就派上了用场。我们有许多设计参数:带宽、成本、距离和灵活性。应用的末端我们有WiFi,它有非常好的使用范围,非常容易使用(毕竟没有线缆),而且还便宜,但它提供的带宽和延迟相对一般。头脑正常的机器学习研究人员都不会用它来构建服务器集群。在接下来的内容中,我们将重点关注适合深度学习的互连方式。 +每当单个设备不足以进行优化时,我们就需要来回传输数据以实现同步处理,于是网络和总线就派上了用场。我们有许多设计参数:带宽、成本、距离和灵活性。应用的末端有WiFi,它有非常好的使用范围,非常容易使用(毕竟没有线缆),而且还便宜,但它提供的带宽和延迟相对一般。头脑正常的机器学习研究人员都不会用它来构建服务器集群。接下来的内容中将重点关注适合深度学习的互连方式。 -* **PCIe**,一种专用总线,用于每个通道点到点连接的高带宽需求(在$16$通道插槽中的PCIe4.0上高达32GB/s),延迟时间为个位数的微秒(5μs)。PCIe链接非常宝贵。处理器拥有的数量:AMD的EPYC 3有$128$个通道,Intel的Xeon每个芯片有$48$个通道;在桌面级CPU上,数字分别是$20$(Ryzen9)和$16$(Core i9)。由于GPU通常有$16$个通道,这就限制了以全带宽与CPU连接的GPU数量。毕竟,它们还需要与其他高带宽外围设备(如存储和以太网)共享链路。与RAM访问一样,由于减少了数据包的开销,因此更适合大批量数据传输。 -* **以太网**,连接计算机最常用的方式。虽然它比PCIe慢得多,但它的安装成本非常低,而且具有很强的弹性,覆盖的距离也要长得多。低级服务器的典型带宽为1GBit/s。高端设备(如云中的[C5实例](https://aws.amazon.com/ec2/instance-types/c5/))提供10到100GBit/s的带宽。与以前所有的情况一样,数据传输有很大的开销。请注意,原始以太网几乎从不被直接使用,而是在物理互连之上使用执行的协议(例如UDP或TCP/IP)。这进一步增加了开销。与PCIe类似,以太网旨在连接两个设备,例如计算机和交换机。 +* **PCIe**,一种专用总线,用于每个通道点到点连接的高带宽需求(在$16$通道插槽中的PCIe4.0上高达32GB/s),延迟时间为个位数的微秒(5μs)。PCIe链接非常宝贵。处理器拥有的数量:AMD的EPYC 3有$128$个通道,Intel的Xeon每个芯片有$48$个通道;在桌面级CPU上,数字分别是$20$(Ryzen9)和$16$(Core i9)。由于GPU通常有$16$个通道,这就限制了以全带宽与CPU连接的GPU数量。毕竟,它们还需要与其他高带宽外围设备(如存储和以太网)共享链路。与RAM访问一样,由于减少了数据包的开销,因此更适合大批量数据传输。 +* **以太网**,连接计算机最常用的方式。虽然它比PCIe慢得多,但它的安装成本非常低,而且具有很强的弹性,覆盖的距离也要长得多。低级服务器的典型带宽为1GBit/s。高端设备(如云中的[C5实例](https://aws.amazon.com/ec2/instance-types/c5/))提供10~100GBit/s的带宽。与以前所有的情况一样,数据传输有很大的开销。请注意,原始以太网几乎从不被直接使用,而是在物理互连之上使用执行的协议(例如UDP或TCP/IP)。这进一步增加了开销。与PCIe类似,以太网旨在连接两个设备,例如计算机和交换机。 * **交换机**,一种连接多个设备的方式,该连接方式下的任何一对设备都可以同时执行(通常是全带宽)点对点连接。例如,以太网交换机可能以高带宽连接$40$台服务器。请注意,交换机并不是传统计算机网络所独有的。甚至PCIe通道也可以是[可交换的](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches),例如:[P2实例](https://aws.amazon.com/ec2/instance-types/p2/)就是将大量GPU连接到主机处理器。 -* **NVLink**,是PCIe的替代品,适用于非常高带宽的互连。它为每条链路提供高达300Gbit/s的数据传输速率。服务器GPU(Volta V100)有六个链路。而消费级GPU(RTX 2080Ti)只有一个链路,运行速度也降低到100Gbit/s。我们建议使用[NCCL](https://github.com/NVIDIA/nccl)来实现GPU之间的高速数据传输。 +* **NVLink**,是PCIe的替代品,适用于非常高带宽的互连。它为每条链路提供高达300Gbit/s的数据传输速率。服务器GPU(Volta V100)有六个链路。而消费级GPU(RTX 2080Ti)只有一个链路,运行速度也降低到100Gbit/s。建议使用[NCCL](https://github.com/NVIDIA/nccl)来实现GPU之间的高速数据传输。 ## 更多延迟 @@ -192,11 +190,11 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 ## 小结 * 设备有运行开销。因此,数据传输要争取量大次少而不是量少次多。这适用于RAM、固态驱动器、网络和GPU。 -* 矢量化是性能的关键。确保充分了解你的加速器的特定功能。例如,一些Intel Xeon CPU特别适用于INT8操作,NVIDIA Volta GPU擅长FP16矩阵操作,NVIDIA Turing擅长FP16、INT8和INT4操作。 +* 矢量化是性能的关键。确保充分了解加速器的特定功能。例如,一些Intel Xeon CPU特别适用于INT8操作,NVIDIA Volta GPU擅长FP16矩阵操作,NVIDIA Turing擅长FP16、INT8和INT4操作。 * 在训练过程中数据类型过小导致的数值溢出可能是个问题(在推断过程中则影响不大)。 * 数据混叠现象会导致严重的性能退化。$64$位CPU应该按照$64$位边界进行内存对齐。在GPU上建议保持卷积大小对齐,例如:与张量核对齐。 * 将算法与硬件相匹配(例如,内存占用和带宽)。将命中参数装入缓存后,可以实现很大数量级的加速比。 -* 在验证实验结果之前,我们建议先在纸上勾勒出新算法的性能。关注的原因是数量级及以上的差异。 +* 在验证实验结果之前,建议先在纸上勾勒出新算法的性能。关注的原因是数量级及以上的差异。 * 使用调试器跟踪调试寻找性能的瓶颈。 * 训练硬件和推断硬件在性能和价格方面有不同的优点。 @@ -205,8 +203,8 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 1. 编写C语言来测试访问对齐的内存和未对齐的内存之间的速度是否有任何差异。(提示:小心缓存影响。) 1. 测试按顺序访问或按给定步幅访问内存时的速度差异。 1. 如何测量CPU上的缓存大小? -1. 如何在多个内存通道中分配数据以获得最大带宽?如果你有许多小的线程,你会怎么布置? -1. 一个企业级硬盘正在以10000转/分的速度旋转。在最坏的情况下,硬盘读取数据所需的最短时间是多少(你可以假设磁头几乎是瞬间移动的)?为什么2.5英寸硬盘在商用服务器上越来越流行(相对于3.5英寸硬盘和5.25英寸硬盘)? +1. 如何在多个内存通道中分配数据以获得最大带宽?如果有许多小的线程,会怎么布置? +1. 一个企业级硬盘正在以10000转/分的速度旋转。在最坏的情况下,硬盘读取数据所需的最短时间是多少(假设磁头几乎是瞬间移动的)?为什么2.5英寸硬盘在商用服务器上越来越流行(相对于3.5英寸硬盘和5.25英寸硬盘)? 1. 假设HDD制造商将存储密度从每平方英寸1 Tbit增加到每平方英寸5 Tbit。在一个2.5英寸的硬盘上,多少信息能够存储一个环中?内轨和外轨有区别吗? 1. 从$8$位数据类型到$16$位数据类型,硅片的数量大约增加了四倍,为什么?为什么NVIDIA会在其图灵GPU中添加INT4运算? 1. 在内存中向前读比向后读快多少?该数字在不同的计算机和CPU供应商之间是否有所不同?为什么?编写C代码进行实验。 @@ -214,6 +212,6 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 1. 测量通过以太网发送消息时的数据包开销。查找UDP和TCP/IP连接之间的差异。 1. 直接内存访问允许CPU以外的设备直接向内存写入(和读取)。为什么要这样? 1. 看看Turing T4GPU的性能数字。为什么从FP16到INT8和INT4的性能只翻倍? -1. 一个网络包从旧金山到阿姆斯特丹的往返旅行需要多长时间?提示:你可以假设距离为10000公里。 +1. 一个网络包从旧金山到阿姆斯特丹的往返旅行需要多长时间?提示:可以假设距离为10000公里。 [Discussions](https://discuss.d2l.ai/t/5717) diff --git a/chapter_computational-performance/hybridize.md b/chapter_computational-performance/hybridize.md index 5ef67617e..553da9cd9 100644 --- a/chapter_computational-performance/hybridize.md +++ b/chapter_computational-performance/hybridize.md @@ -30,8 +30,8 @@ Python是一种*解释型语言*(interpreted language)。因此,当对上 考虑另一种选择*符号式编程*(symbolic programming),即代码通常只在完全定义了过程之后才执行计算。这个策略被多个深度学习框架使用,包括Theano和TensorFlow(后者已经获得了命令式编程的扩展)。一般包括以下步骤: -1. 定义计算流程。 -1. 将流程编译成可执行的程序。 +1. 定义计算流程; +1. 将流程编译成可执行的程序; 1. 给定输入,调用编译好的程序执行。 这将允许进行大量的优化。首先,在大多数情况下,我们可以跳过Python解释器。从而消除因为多个更快的GPU与单个CPU上的单个Python线程搭配使用时产生的性能瓶颈。其次,编译器可以将上述代码优化和重写为`print((1 + 2) + (3 + 4))`甚至`print(10)`。因为编译器在将其转换为机器指令之前可以看到完整的代码,所以这种优化是可以实现的。例如,只要某个变量不再需要,编译器就可以释放内存(或者从不分配内存),或者将代码转换为一个完全等价的片段。下面,我们将通过模拟命令式编程来进一步了解符号式编程的概念。 @@ -64,7 +64,7 @@ exec(y) 命令式(解释型)编程和符号式编程的区别如下: -* 命令式编程更容易使用。在Python中,命令式编程的大部分代码都是简单易懂的。命令式编程也更容易调试,这是因为无论是获取和打印所有的中间变量值,或者使用Python的内置调试工具都更加简单。 +* 命令式编程更容易使用。在Python中,命令式编程的大部分代码都是简单易懂的。命令式编程也更容易调试,这是因为无论是获取和打印所有的中间变量值,或者使用Python的内置调试工具都更加简单; * 符号式编程运行效率更高,更易于移植。符号式编程更容易在编译期间优化代码,同时还能够将程序移植到与Python无关的格式中,从而允许程序在非Python环境中运行,避免了任何潜在的与Python解释器相关的性能问题。 ## 混合式编程 @@ -82,7 +82,11 @@ exec(y) :end_tab: :begin_tab:`tensorflow` -命令式编程现在是TensorFlow2的默认选择,对于那些刚接触该语言的人来说是一个很好的改变。不过,符号式编程技术和计算图仍然存在于TensorFlow中,并且可以通过易于使用的装饰器`tf.function`进行访问。这为TensorFlow带来了命令式编程范式,允许用户定义更加直观的函数,然后使用被TensorFlow团队称为[autograph](https://www.tensorflow.org/api_docs/python/tf/autograph)的特性将它们封装,再自动编译成计算图。 +命令式编程现在是TensorFlow2的默认选择,对那些刚接触该语言的人来说是一个很好的改变。不过,符号式编程技术和计算图仍然存在于TensorFlow中,并且可以通过易于使用的装饰器`tf.function`进行访问。这为TensorFlow带来了命令式编程范式,允许用户定义更加直观的函数,然后使用被TensorFlow团队称为[autograph](https://www.tensorflow.org/api_docs/python/tf/autograph)的特性将它们封装,再自动编译成计算图。 +:end_tab: + +:begin_tab:`paddle` +如上所述,飞桨是基于命令式编程并且使用动态计算图。为了能够利用符号式编程的可移植性和效率,开发人员思考能否将这两种编程模型的优点结合起来,于是就产生了飞桨2.0版本。飞桨2.0及以上版本允许用户使用纯命令式编程进行开发和调试,同时能够将大多数程序转换为符号式程序,以便在需要产品级计算性能和部署时使用。 :end_tab: ## `Sequential`的混合式编程 @@ -148,6 +152,33 @@ net = get_net() net(x) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +from paddle.jit import to_static +from paddle.static import InputSpec + +# 生产网络的工厂模式 +def get_net(): + blocks = [ + nn.Linear(512, 256), + nn.ReLU(), + nn.Linear(256, 128), + nn.ReLU(), + nn.Linear(128, 2) + ] + net = nn.Sequential(*blocks) + return net + +x = paddle.randn((1, 512)) +net = get_net() +net(x) +``` + :begin_tab:`mxnet` 通过调用`hybridize`函数,我们就有能力编译和优化多层感知机中的计算,而模型的计算结果保持不变。 :end_tab: @@ -160,6 +191,10 @@ net(x) 一开始,TensorFlow中构建的所有函数都是作为计算图构建的,因此默认情况下是JIT编译的。但是,随着TensorFlow2.X和EargeTensor的发布,计算图就不再是默认行为。我们可以使用tf.function重新启用这个功能。tf.function更常被用作函数装饰器,如下所示,它也可以直接将其作为普通的Python函数调用。模型的计算结果保持不变。 :end_tab: +:begin_tab:`paddle` +通过使用`paddle.jit.to_static`函数来转换模型,我们就有能力编译和优化多层感知机中的计算,而模型的计算结果保持不变。 +:end_tab: + ```{.python .input} net.hybridize() net(x) @@ -177,6 +212,12 @@ net = tf.function(net) net(x) ``` +```{.python .input} +#@tab paddle +net = paddle.jit.to_static(net) +net(x) +``` + :begin_tab:`mxnet` 我们只需将一个块指定为`HybridSequential`,然后编写与之前相同的代码,再调用`hybridize`,当完成这些任务后,网络就将得到优化(我们将在下面对性能进行基准测试)。不幸的是,这种魔法并不适用于每一层。也就是说,如果某个层是从`Block`类而不是从`HybridBlock`类继承的,那么它将不会得到优化。 :end_tab: @@ -186,7 +227,11 @@ net(x) :end_tab: :begin_tab:`tensorflow` -我们编写与之前相同的代码,再使用`tf.function`简单地转换模型,当完成这些任务后,网络将以TensorFlow的MLIR中间表示形式构建为一个计算图,并在编译器级别进行大量优化以满足快速执行的需要(我们将在下面对性能进行基准测试)。通过将`jit_compile = True`标志添加到`tf.function()`的函数调用中可以显式地启用TensorFlow中的XLA(线性代数加速)功能。在某些情况下,XLA可以进一步优化JIT的编译代码。如果没有这种显式定义,图形模式将会被启用,但是XLA可以使某些大规模的线性代数的运算速度更快(与我们在深度学习应用程序中看到的操作类似),特别是在GPU环境中。 +我们编写与之前相同的代码,再使用`tf.function`简单地转换模型,当完成这些任务后,网络将以TensorFlow的MLIR中间表示形式构建为一个计算图,并在编译器级别进行大量优化以满足快速执行的需要(我们将在下面对性能进行基准测试)。通过将`jit_compile = True`标志添加到`tf.function()`的函数调用中可以显式地启用TensorFlow中的XLA(线性代数加速)功能。在某些情况下,XLA可以进一步优化JIT的编译代码。如果没有这种显式定义,图形模式将会被启用,但是XLA可以使某些大规模的线性代数的运算速度更快(与我们在深度学习程序中看到的操作类似),特别是在GPU环境中。 +:end_tab: + +:begin_tab:`paddle` +我们编写与之前相同的代码,再使用`paddle.jit.to_static`简单地转换模型,当完成这些任务后,网络就将得到优化(我们将在下面对性能进行基准测试)。 :end_tab: ### 通过混合式编程加速 @@ -221,6 +266,10 @@ class Benchmark: 现在我们可以调用网络三次,一次使用eager模式,一次是使用图模式,一次使用JIT编译的XLA。 :end_tab: +:begin_tab:`paddle` +现在我们可以调用网络两次,一次使用动态图命令式编程,一次使用静态图符号式编程。 +:end_tab: + ```{.python .input} net = get_net() with Benchmark('无混合式'): @@ -255,6 +304,19 @@ with Benchmark('Graph模式'): for i in range(1000): net(x) ``` +```{.python .input} +#@tab paddle +net = get_net() +with Benchmark('飞桨动态图命令式编程'): + for i in range(1000): net(x) + +# InputSpec用于描述模型输入的签名信息,包括shape、dtype和name +x_spec = InputSpec(shape=[-1, 512], name='x') +net = paddle.jit.to_static(get_net(),input_spec=[x_spec]) +with Benchmark('飞桨静态图符号式编程'): + for i in range(1000): net(x) +``` + :begin_tab:`mxnet` 如以上结果所示,在`HybridSequential`的实例调用`hybridize`函数后,通过使用符号式编程提高了计算性能。 :end_tab: @@ -267,6 +329,10 @@ with Benchmark('Graph模式'): 如以上结果所示,在`tf.keras.Sequential`的实例被函数`tf.function`脚本化后,通过使用TensorFlow中的图模式执行方式实现的符号式编程提高了计算性能。 :end_tab: +:begin_tab:`paddle` +如以上结果所示,在`nn.Sequential`的实例被函数`paddle.jit.to_static`脚本化后,通过使用符号式编程提高了计算性能。 +:end_tab: + ### 序列化 :begin_tab:`mxnet` @@ -281,6 +347,10 @@ with Benchmark('Graph模式'): 编译模型的好处之一是我们可以将模型及其参数序列化(保存)到磁盘。这允许这些训练好的模型部署到其他设备上,并且还能方便地使用其他前端编程语言。同时,通常编译模型的代码执行速度也比命令式编程更快。在TensorFlow中保存模型的底层API是`tf.saved_model`,让我们来看看`saved_model`的运行情况。 :end_tab: +:begin_tab:`paddle` +编译模型的好处之一是我们可以将模型及其参数序列化(保存)到磁盘。这允许这些训练好的模型部署到其他设备上,并且还能方便地使用其他前端编程语言。同时,通常编译模型的代码执行速度也比命令式编程更快。让我们看看`paddle.jit.save`的实际功能。 +:end_tab: + ```{.python .input} net.export('my_mlp') !ls -lh my_mlp* @@ -299,6 +369,12 @@ tf.saved_model.save(net, 'my_mlp') !ls -lh my_mlp* ``` +```{.python .input} +#@tab paddle +paddle.jit.save(net, './my_mlp') +!ls -lh my_mlp* +``` + :begin_tab:`mxnet` 模型被分解成两个文件,一个是大的二进制参数文件,一个是执行模型计算所需要的程序的JSON描述文件。这些文件可以被其他前端语言读取,例如C++、R、Scala和Perl,只要这些语言能够被Python或者MXNet支持。让我们看看模型描述中的前几行。 :end_tab: @@ -375,11 +451,11 @@ net(x) :begin_tab:`mxnet` 1. 在本节的`HybridNet`类的`hybrid_forward`函数的第一行中添加`x.asnumpy()`,执行代码并观察遇到的错误。为什么会这样? 1. 如果我们在`hybrid_forward`函数中添加控制流,即Python语句`if`和`for`,会发生什么? -1. 回顾前几章中你感兴趣的模型,你能通过重新实现它们来提高它们的计算性能吗? +1. 回顾前几章中感兴趣的模型,能通过重新实现它们来提高它们的计算性能吗? :end_tab: :begin_tab:`pytorch,tensorflow` -1. 回顾前几章中你感兴趣的模型,你能提高它们的计算性能吗? +1. 回顾前几章中感兴趣的模型,能提高它们的计算性能吗? :end_tab: :begin_tab:`mxnet` @@ -393,3 +469,7 @@ net(x) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2787) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11857) +:end_tab: \ No newline at end of file diff --git a/chapter_computational-performance/index.md b/chapter_computational-performance/index.md index c49395675..130090850 100644 --- a/chapter_computational-performance/index.md +++ b/chapter_computational-performance/index.md @@ -5,7 +5,7 @@ 因此,计算的性能非常重要。 本章将集中讨论影响计算性能的主要因素:命令式编程、符号编程、 异步计算、自动并行和多GPU计算。 -通过学习本章,对于前几章中实现的那些模型,你可以进一步提高它们的计算性能。 +通过学习本章,对于前几章中实现的那些模型,可以进一步提高它们的计算性能。 例如,我们可以在不影响准确性的前提下,大大减少训练时间。 ```toc diff --git a/chapter_computational-performance/multiple-gpus-concise.md b/chapter_computational-performance/multiple-gpus-concise.md index 0ee1c7d10..17b95ea3c 100644 --- a/chapter_computational-performance/multiple-gpus-concise.md +++ b/chapter_computational-performance/multiple-gpus-concise.md @@ -1,7 +1,7 @@ # 多GPU的简洁实现 :label:`sec_multi_gpu_concise` -每个新模型的并行计算都从零开始实现是无趣的。此外,优化同步工具以获得高性能也是有好处的。下面我们将展示如何使用深度学习框架的高级API来实现这一点。数学和算法与 :numref:`sec_multi_gpu`中的相同。不出所料,你至少需要两个GPU来运行本节的代码。 +每个新模型的并行计算都从零开始实现是无趣的。此外,优化同步工具以获得高性能也是有好处的。下面我们将展示如何使用深度学习框架的高级API来实现这一点。数学和算法与 :numref:`sec_multi_gpu`中的相同。本节的代码至少需要两个GPU来运行。 ```{.python .input} from d2l import mxnet as d2l @@ -17,6 +17,15 @@ import torch from torch import nn ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + ## [**简单网络**] 让我们使用一个比 :numref:`sec_multi_gpu`的LeNet更有意义的网络,它依然能够容易地和快速地训练。我们选择的是 :cite:`He.Zhang.Ren.ea.2016`中的ResNet-18。因为输入的图像很小,所以稍微修改了一下。与 :numref:`sec_resnet`的区别在于,我们在开始时使用了更小的卷积核、步长和填充,而且删除了最大汇聚层。 @@ -79,10 +88,42 @@ def resnet18(num_classes, in_channels=1): return net ``` +```{.python .input} +#@tab paddle +#@save +def resnet18(num_classes, in_channels=1): + """稍加修改的ResNet-18模型""" + def resnet_block(in_channels, out_channels, num_residuals, + first_block=False): + blk = [] + for i in range(num_residuals): + if i == 0 and not first_block: + blk.append(d2l.Residual(in_channels, out_channels, + use_1x1conv=True, strides=2)) + else: + blk.append(d2l.Residual(out_channels, out_channels)) + return nn.Sequential(*blk) + + # 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层 + net = nn.Sequential( + nn.Conv2D(in_channels, 64, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2D(64), + nn.ReLU()) + net.add_sublayer("resnet_block1", resnet_block( + 64, 64, 2, first_block=True)) + net.add_sublayer("resnet_block2", resnet_block(64, 128, 2)) + net.add_sublayer("resnet_block3", resnet_block(128, 256, 2)) + net.add_sublayer("resnet_block4", resnet_block(256, 512, 2)) + net.add_sublayer("global_avg_pool", nn.AdaptiveAvgPool2D((1, 1))) + net.add_sublayer("fc", nn.Sequential(nn.Flatten(), + nn.Linear(512, num_classes))) + return net +``` + ## 网络初始化 :begin_tab:`mxnet` -`initialize`函数允许我们在所选设备上初始化参数。请参阅 :numref:`sec_numerical_stability`复习初始化方法。这个函数在多个设备上初始化网络时特别方便。让我们在实践中试一试它的运作方式。 +`initialize`函数允许我们在所选设备上初始化参数。请参阅 :numref:`sec_numerical_stability`复习初始化方法。这个函数在多个设备上初始化网络时特别方便。下面在实践中试一试它的运作方式。 :end_tab: :begin_tab:`pytorch` @@ -105,6 +146,14 @@ devices = d2l.try_all_gpus() # 我们将在训练代码实现中初始化网络 ``` +```{.python .input} +#@tab paddle +net = resnet18(10) +# 获取GPU列表 +devices = d2l.try_all_gpus() +# 我们将在训练代码实现中初始化网络 +``` + :begin_tab:`mxnet` 使用 :numref:`sec_multi_gpu`中引入的`split_and_load`函数可以切分一个小批量数据,并将切分后的分块数据复制到`devices`变量提供的设备列表中。网络实例自动使用适当的GPU来计算前向传播的值。我们将在下面生成$4$个观测值,并在GPU上将它们拆分。 :end_tab: @@ -156,9 +205,9 @@ def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): 如前所述,用于训练的代码需要执行几个基本功能才能实现高效并行: -* 需要在所有设备上初始化网络参数。 -* 在数据集上迭代时,要将小批量数据分配到所有设备上。 -* 跨设备并行计算损失及其梯度。 +* 需要在所有设备上初始化网络参数; +* 在数据集上迭代时,要将小批量数据分配到所有设备上; +* 跨设备并行计算损失及其梯度; * 聚合梯度,并相应地更新参数。 最后,并行地计算精确度和发布网络的最终性能。除了需要拆分和聚合数据外,训练代码与前几章的实现非常相似。 @@ -220,14 +269,46 @@ def train(net, num_gpus, batch_size, lr): f'在{str(devices)}') ``` -让我们看看这在实践中是如何运作的。我们先[**在单个GPU上训练网络**]进行预热。 +```{.python .input} +#@tab paddle +def train(net, num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + + init_normal = nn.initializer.Normal(mean=0.0, std=0.01) + for i in net.sublayers(): + if type(i) in [nn.Linear, nn.Conv2D]: + init_normal(i.weight) + + # 在多个 GPU 上设置模型 + net = paddle.DataParallel(net) + trainer = paddle.optimizer.SGD(parameters=net.parameters(), learning_rate=lr) + loss = nn.CrossEntropyLoss() + timer, num_epochs = d2l.Timer(), 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + for epoch in range(num_epochs): + net.train() + timer.start() + for X, y in train_iter: + trainer.clear_grad() + X, y = paddle.to_tensor(X, place=devices[0]), paddle.to_tensor(y, place=devices[0]) + l = loss(net(X), y) + l.backward() + trainer.step() + timer.stop() + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),)) + print(f'测试精度:{animator.Y[0][-1]:.2f}, {timer.avg():.1f}秒/轮,' + f'在{str(devices)}') +``` + +接下来看看这在实践中是如何运作的。我们先[**在单个GPU上训练网络**]进行预热。 ```{.python .input} train(num_gpus=1, batch_size=256, lr=0.1) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle train(net, num_gpus=1, batch_size=256, lr=0.1) ``` @@ -251,7 +332,7 @@ train(net, num_gpus=2, batch_size=512, lr=0.2) * 优化算法在多个GPU上自动聚合。 :end_tab: -:begin_tab:`pytorch` +:begin_tab:`pytorch, paddle` * 神经网络可以在(可找到数据的)单GPU上进行自动评估。 * 每台设备上的网络需要先初始化,然后再尝试访问该设备上的参数,否则会遇到错误。 * 优化算法在多个GPU上自动聚合。 @@ -262,10 +343,10 @@ train(net, num_gpus=2, batch_size=512, lr=0.2) :begin_tab:`mxnet` 1. 本节使用ResNet-18,请尝试不同的迭代周期数、批量大小和学习率,以及使用更多的GPU进行计算。如果使用$16$个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么? 1. 有时候不同的设备提供了不同的计算能力,我们可以同时使用GPU和CPU,那应该如何分配工作?为什么? -1. 如果去掉`npx.waitall()`会怎样?你将如何修改训练,以使并行操作最多有两个步骤重叠? +1. 如果去掉`npx.waitall()`会怎样?该如何修改训练,以使并行操作最多有两个步骤重叠? :end_tab: -:begin_tab:`pytorch` +:begin_tab:`pytorch, paddle` 1. 本节使用ResNet-18,请尝试不同的迭代周期数、批量大小和学习率,以及使用更多的GPU进行计算。如果使用$16$个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么? 1. 有时候不同的设备提供了不同的计算能力,我们可以同时使用GPU和CPU,那应该如何分配工作?为什么? :end_tab: @@ -277,3 +358,7 @@ train(net, num_gpus=2, batch_size=512, lr=0.2) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2803) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11861) +:end_tab: \ No newline at end of file diff --git a/chapter_computational-performance/multiple-gpus.md b/chapter_computational-performance/multiple-gpus.md index eecc82b03..d382c3e2a 100644 --- a/chapter_computational-performance/multiple-gpus.md +++ b/chapter_computational-performance/multiple-gpus.md @@ -24,7 +24,7 @@ 然而,GPU的接口之间需要的密集同步可能是很难办的,特别是层之间计算的工作负载不能正确匹配的时候, 还有层之间的接口需要大量的数据传输的时候(例如:激活值和梯度,数据量可能会超出GPU总线的带宽)。 -此外,计算密集型操作的顺序对于拆分来说也是非常重要的,这方面的最好研究可参见 :cite:`Mirhoseini.Pham.Le.ea.2017`,其本质仍然是一个困难的问题,目前还不清楚研究是否能在特定问题上实现良好的线性缩放。 +此外,计算密集型操作的顺序对拆分来说也是非常重要的,这方面的最好研究可参见 :cite:`Mirhoseini.Pham.Le.ea.2017`,其本质仍然是一个困难的问题,目前还不清楚研究是否能在特定问题上实现良好的线性缩放。 综上所述,除非存框架或操作系统本身支持将多个GPU连接在一起,否则不建议这种方法。 第二种方法,拆分层内的工作。 @@ -69,10 +69,10 @@ 一般来说,$k$个GPU并行训练过程如下: -* 在任何一次训练迭代中,给定的随机的小批量样本都将被分成$k$个部分,并均匀地分配到GPU上。 -* 每个GPU根据分配给它的小批量子集,计算模型参数的损失和梯度。 -* 将$k$个GPU中的局部梯度聚合,以获得当前小批量的随机梯度。 -* 聚合梯度被重新分发到每个GPU中。 +* 在任何一次训练迭代中,给定的随机的小批量样本都将被分成$k$个部分,并均匀地分配到GPU上; +* 每个GPU根据分配给它的小批量子集,计算模型参数的损失和梯度; +* 将$k$个GPU中的局部梯度聚合,以获得当前小批量的随机梯度; +* 聚合梯度被重新分发到每个GPU中; * 每个GPU使用这个小批量随机梯度,来更新它所维护的完整的模型参数集。 @@ -98,6 +98,17 @@ from torch import nn from torch.nn import functional as F ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +from paddle.nn import functional as F +``` + ## [**简单网络**] 我们使用 :numref:`sec_lenet`中介绍的(稍加修改的)LeNet, @@ -170,6 +181,38 @@ def lenet(X, params): loss = nn.CrossEntropyLoss(reduction='none') ``` +```{.python .input} +#@tab paddle +# 初始化模型参数 +scale = 0.01 +W1 = paddle.randn(shape=[20, 1, 3, 3]) * scale +b1 = paddle.zeros(shape=[20]) +W2 = paddle.randn(shape=[50, 20, 5, 5]) * scale +b2 = paddle.zeros(shape=[50]) +W3 = paddle.randn(shape=[800, 128]) * scale +b3 = paddle.zeros(shape=[128]) +W4 = paddle.randn(shape=[128, 10]) * scale +b4 = paddle.zeros(shape=[10]) +params = [W1, b1, W2, b2, W3, b3, W4, b4] + +# 定义模型 +def lenet(X, params): + h1_conv = F.conv2d(x=X, weight=params[0], bias=params[1]) + h1_activation = F.relu(h1_conv) + h1 = F.avg_pool2d(x=h1_activation, kernel_size=(2, 2), stride=(2, 2)) + h2_conv = F.conv2d(x=h1, weight=params[2], bias=params[3]) + h2_activation = F.relu(h2_conv) + h2 = F.avg_pool2d(x=h2_activation, kernel_size=(2, 2), stride=(2, 2)) + h2 = h2.reshape([h2.shape[0], -1]) + h3_linear = paddle.mm(h2, params[4]) + params[5] + h3 = F.relu(h3_linear) + y_hat = paddle.mm(h3, params[6]) + params[7] + return y_hat + +# 交叉熵损失函数 +loss = nn.CrossEntropyLoss(reduction='none') +``` + ## 数据同步 对于高效的多GPU训练,我们需要两个基本操作。 @@ -194,6 +237,15 @@ def get_params(params, device): return new_params ``` +```{.python .input} +#@tab paddle +def get_params(params, device): + new_params = [paddle.to_tensor(p, place=device) for p in params] + for p in new_params: + p.stop_gradient = False + return new_params +``` + 通过将模型参数复制到一个GPU。 ```{.python .input} @@ -224,6 +276,16 @@ def allreduce(data): data[i][:] = data[0].to(data[i].device) ``` +```{.python .input} +#@tab paddle +def allreduce(data): + paddle.set_device("gpu:0") + for i in range(1, len(data)): + data[0] += paddle.to_tensor(data[i], place=data[0].place) + for i in range(1, len(data)): + data[i] = paddle.to_tensor(data[0], place=data[i].place) +``` + 通过在不同设备上创建具有不同值的向量并聚合它们。 ```{.python .input} @@ -241,6 +303,17 @@ allreduce(data) print('allreduce之后:\n', data[0], '\n', data[1]) ``` +```{.python .input} +#@tab paddle +num_gpus = 2 +devices = [d2l.try_gpu(i) for i in range(num_gpus)] + +data = [paddle.to_tensor(paddle.ones(shape=[1, 2]) * (i + 1), place=devices[i]) for i in range(2)] +print('allreduce之前:\n', data[0], '\n', data[1]) +allreduce(data) +print('allreduce之后:\n', data[0], '\n', data[1]) +``` + ## 数据分发 我们需要一个简单的工具函数,[**将一个小批量数据均匀地分布在多个GPU上**]。 @@ -266,6 +339,21 @@ print('load into', devices) print('output:', split) ``` +```{.python .input} +#@tab paddle +def paddlescatter(XY, devices): + xy = XY.shape[0]//len(devices) # 根据GPU数目计算分块大小 + return [paddle.to_tensor(XY[i*xy:(i+1)*xy], place=device) for i,device in enumerate(devices)] + +# 数据分发 +data = paddle.arange(20).reshape([4, 5]) +split = paddlescatter(data, devices) + +print('input :', data) +print('load into', devices) +print('output:', split) +``` + 为了方便以后复用,我们定义了可以同时拆分数据和标签的`split_batch`函数。 ```{.python .input} @@ -287,6 +375,16 @@ def split_batch(X, y, devices): nn.parallel.scatter(y, devices)) ``` +```{.python .input} +#@tab paddle +#@save +def split_batch(X, y, devices): + """将X和y拆分到多个设备上""" + assert X.shape[0] == y.shape[0] + return (paddlescatter(X, devices), + paddlescatter(y, devices)) +``` + ## 训练 现在我们可以[**在一个小批量上实现多GPU训练**]。 @@ -331,6 +429,31 @@ def train_batch(X, y, device_params, devices, lr): d2l.sgd(param, lr, X.shape[0]) # 在这里,我们使用全尺寸的小批量 ``` +```{.python .input} +#@tab paddle +def train_batch(X, y, device_params, devices, lr): + X_shards, y_shards = split_batch(X, y, devices) + # 在每个GPU上分别计算损失 + for i, (X_shard, y_shard, device_W) in enumerate(zip( + X_shards, y_shards, device_params)) : + # 设定全局变量,以便在指定的GPU执行计算 + paddle.set_device(f"gpu:{i}") + y_shard = paddle.squeeze(y_shard) + l = loss(lenet(X_shard, device_W), y_shard).sum() + # 反向传播在每个GPU上分别执行 + l.backward() + # 将每个GPU的所有梯度相加,并将其广播到所有GPU + with paddle.no_grad(): + for i in range(len(device_params[0])): + allreduce( + [device_params[c][i].grad for c in range(len(devices))]) + # 在每个GPU上分别更新模型参数 + for i in range(len(device_params)): + paddle.set_device(f"gpu:{i}") + param = device_params[i] + d2l.sgd(param, lr, X.shape[0]) # 在这里,我们使用全尺寸的小批量 +``` + 现在,我们可以[**定义训练函数**]。 与前几章中略有不同:训练函数需要分配GPU并将所有模型参数复制到所有设备。 显然,每个小批量都是使用`train_batch`函数来处理多个GPU。 @@ -383,6 +506,30 @@ def train(num_gpus, batch_size, lr): f'在{str(devices)}') ``` +```{.python .input} +#@tab paddle +def train(num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + # 将模型参数复制到num_gpus个GPU + device_params = [get_params(params, d) for d in devices] + num_epochs = 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + timer = d2l.Timer() + for epoch in range(num_epochs): + timer.start() + for X, y in train_iter: + # 为单个小批量执行多GPU训练 + train_batch(X, y, device_params, devices, lr) + paddle.device.cuda.synchronize() + timer.stop() + # 在GPU0上评估模型 + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu( + lambda x: lenet(x, device_params[0]), test_iter, devices[0]),)) + print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,' + f'在{str(devices)}') +``` + 让我们看看[**在单个GPU上运行**]效果得有多好。 首先使用的批量大小是$256$,学习率是$0.2$。 @@ -393,12 +540,12 @@ train(num_gpus=1, batch_size=256, lr=0.2) 保持批量大小和学习率不变,并[**增加为2个GPU**],我们可以看到测试精度与之前的实验基本相同。 不同的GPU个数在算法寻优方面是相同的。 -不幸的是,这里没有任何有意义的加速:模型实在太小了;而且数据集也太小了,在这个数据集中,我们实现的多GPU训练的简单方法受到了巨大的Python开销的影响。 +不幸的是,这里没有任何有意义的加速:模型实在太小了;而且数据集也太小了。在这个数据集中,我们实现的多GPU训练的简单方法受到了巨大的Python开销的影响。 在未来,我们将遇到更复杂的模型和更复杂的并行化方法。 尽管如此,让我们看看Fashion-MNIST数据集上会发生什么。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch train(num_gpus=2, batch_size=256, lr=0.2) ``` @@ -423,3 +570,7 @@ train(num_gpus=2, batch_size=256, lr=0.2) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2800) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11860) +:end_tab: \ No newline at end of file diff --git a/chapter_computational-performance/parameterserver.md b/chapter_computational-performance/parameterserver.md index 2ed1286f0..b6000fc0c 100644 --- a/chapter_computational-performance/parameterserver.md +++ b/chapter_computational-performance/parameterserver.md @@ -9,7 +9,7 @@ 让我们回顾一下在分布式架构中数据并行的训练方法,因为在实践中它的实现相对简单,因此本节将排除其他内容只对其进行介绍。由于当今的GPU拥有大量的显存,因此在实际场景中(不包括图深度学习)只有数据并行这种并行训练策略值得推荐。图 :numref:`fig_parameterserver`描述了在 :numref:`sec_multi_gpu`中实现的数据并行的变体。其中的关键是梯度的聚合需要在GPU 0上完成,然后再将更新后的参数广播给所有GPU。 -![左图:单GPU训练;右图:多GPU训练的一个变体:(1)计算损失和梯度,(2)所有梯度聚合在一个GPU上,(3)发生参数更新,并将参数重新广播给所有GPU](../img/ps.svg) +![左图是单GPU训练;右图是多GPU训练的一个变体:(1)计算损失和梯度,(2)所有梯度聚合在一个GPU上,(3)发生参数更新,并将参数重新广播给所有GPU](../img/ps.svg) :label:`fig_parameterserver` 回顾来看,选择GPU 0进行聚合似乎是个很随便的决定,当然也可以选择CPU上聚合,事实上只要优化算法支持,在实际操作中甚至可以在某个GPU上聚合其中一些参数,而在另一个GPU上聚合另一些参数。例如,如果有四个与参数向量相关的梯度$\mathbf{g}_1, \ldots, \mathbf{g}_4$,还可以一个GPU对一个$\mathbf{g}_i (i = 1, \ldots, 4$)地进行梯度聚合。 @@ -64,7 +64,7 @@ ![多机多GPU分布式并行训练](../img/ps-multimachine.svg) :label:`fig_ps_multimachine` -以上这些操作似乎都相当简单,而且事实上它们可以在一台机器内高效地执行,但是当我们考虑多台机器时,就会发现中央的参数服务器成为了瓶颈。毕竟,每个服务器的带宽是有限的,因此对于$m$个工作节点来说,将所有梯度发送到服务器所需的时间是$\mathcal{O}(m)$。我们也可以通过将参数服务器数量增加到$n$来突破这一障碍。此时,每个服务器只需要存储$\mathcal{O}(1/n)$个参数,因此更新和优化的总时间变为$\mathcal{O}(m/n)$。这两个数字的匹配会产生稳定的伸缩性,而不用在乎我们需要处理多少工作节点。在实际应用中,我们使用同一台机器既作为工作节点还作为服务器。设计说明请参考 :numref:`fig_ps_multips`(技术细节请参考 :cite:`Li.Andersen.Park.ea.2014`)。特别是,确保多台机器只在没有不合理延迟的情况下工作是相当困难的。我们在下面忽略了关于阻塞的细节,只简单介绍一下同步和异步(unsynchronized)更新。 +以上这些操作似乎都相当简单,而且事实上它们可以在一台机器内高效地执行,但是当我们考虑多台机器时,就会发现中央的参数服务器成为了瓶颈。毕竟,每个服务器的带宽是有限的,因此对$m$个工作节点来说,将所有梯度发送到服务器所需的时间是$\mathcal{O}(m)$。我们也可以通过将参数服务器数量增加到$n$来突破这一障碍。此时,每个服务器只需要存储$\mathcal{O}(1/n)$个参数,因此更新和优化的总时间变为$\mathcal{O}(m/n)$。这两个数字的匹配会产生稳定的伸缩性,而不用在乎我们需要处理多少工作节点。在实际应用中,我们使用同一台机器既作为工作节点还作为服务器。设计说明请参考 :numref:`fig_ps_multips`(技术细节请参考 :cite:`Li.Andersen.Park.ea.2014`)。特别是,确保多台机器只在没有不合理延迟的情况下工作是相当困难的。我们在下面忽略了关于阻塞的细节,只简单介绍一下同步和异步(unsynchronized)更新。 ![上图:单参数服务器是一个瓶颈,因为它的带宽是有限的;下图:多参数服务器使用聚合带宽存储部分参数](../img/ps-multips.svg) :label:`fig_ps_multips` @@ -79,11 +79,11 @@ $$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{ 其中$\mathbf{g}_{ijk}$是在工作节点$k$的GPU$j$上拆分的梯度$i$的一部分。这个运算的关键在于它是一个*交换归约*(commutative reduction),也就是说,它把许多向量变换成一个向量,而运算顺序在完成向量变换时并不重要。这对实现我们的目标来说是非常好的,因为不需要为何时接收哪个梯度进行细粒度的控制。此外,请注意,这个操作在不同的$i$之间是独立的。 -这就允许我们定义下面两个操作:*push*(用于累积梯度)和*pull*(用于取得聚合梯度)。因为我们有很多层,也就有很多不同的梯度集合,因此需要用一个键$i$来对梯度建索引。这个与Dynamo :cite:`DeCandia.Hastorun.Jampani.ea.2007`中引入的“键-值存储”之间存在相似性并非巧合。它们两个定义都拥有许多相似的性质,特别是在多个服务器之间分发参数时。 +这就允许我们定义下面两个操作:*push*(用于累积梯度)和*pull*(用于取得聚合梯度)。因为我们有很多层,也就有很多不同的梯度集合,因此需要用一个键$i$来对梯度建索引。这个与Dynamo :cite:`DeCandia.Hastorun.Jampani.ea.2007`中引入的*键-值存储*之间存在相似性并非巧合。它们两个定义都拥有许多相似的性质,特别是在多个服务器之间分发参数时。 -“键-值存储”的push与pull操作描述如下: +*键-值存储*的push与pull操作描述如下: -* **push(key,value)**将特定的梯度值从工作节点发送到公共存储,在那里通过某种方式(例如,相加)来聚合值。 +* **push(key,value)**将特定的梯度值从工作节点发送到公共存储,在那里通过某种方式(例如,相加)来聚合值; * **pull(key,value)**从公共存储中取得某种方式(例如,组合来自所有工作节点的梯度)的聚合值。 通过将同步的所有复杂性隐藏在一个简单的push和pull操作背后,我们可以将统计建模人员(他们希望能够用简单的术语表达优化)和系统工程师(他们需要处理分布式同步中固有的复杂性)的关注点解耦。 @@ -96,7 +96,7 @@ $$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{ ## 练习 -1. 你能进一步提高环同步的性能吗?(提示:你可以双向发送消息。) +1. 请尝试进一步提高环同步的性能吗。(提示:可以双向发送消息。) 1. 在计算仍在进行中,可否允许执行异步通信?它将如何影响性能? 1. 怎样处理在长时间运行的计算过程中丢失了一台服务器这种问题?尝试设计一种容错机制来避免重启计算这种解决方案? diff --git a/chapter_computer-vision/anchor.md b/chapter_computer-vision/anchor.md index 604164562..0f05e71a7 100644 --- a/chapter_computer-vision/anchor.md +++ b/chapter_computer-vision/anchor.md @@ -26,11 +26,23 @@ import torch torch.set_printoptions(2) # 精简输出精度 ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import numpy as np + +paddle.set_printoptions(2) # 精简输出精度 +``` + ## 生成多个锚框 假设输入图像的高度为$h$,宽度为$w$。 我们以图像的每个像素为中心生成不同形状的锚框:*缩放比*为$s\in (0, 1]$,*宽高比*为$r > 0$。 -那么[**锚框的宽度和高度分别是$ws\sqrt{r}$和$hs/\sqrt{r}$。**] +那么[**锚框的宽度和高度分别是$hs\sqrt{r}$和$hs/\sqrt{r}$。**] 请注意,当中心位置给定时,已知宽和高的锚框是确定的。 要生成多个不同形状的锚框,让我们设置许多缩放比(scale)取值$s_1,\ldots, s_n$和许多宽高比(aspect ratio)取值$r_1,\ldots, r_m$。 @@ -43,7 +55,7 @@ $$(s_1, r_1), (s_1, r_2), \ldots, (s_1, r_m), (s_2, r_1), (s_3, r_1), \ldots, (s **) 也就是说,以同一像素为中心的锚框的数量是$n+m-1$。 -对于整个输入图像,我们将共生成$wh(n+m-1)$个锚框。 +对于整个输入图像,将共生成$wh(n+m-1)$个锚框。 上述生成锚框的方法在下面的`multibox_prior`函数中实现。 我们指定输入图像、尺寸列表和宽高比列表,然后此函数将返回所有的锚框。 @@ -131,7 +143,49 @@ def multibox_prior(data, sizes, ratios): return output.unsqueeze(0) ``` -我们可以看到[**返回的锚框变量`Y`的形状**]是(批量大小,锚框的数量,4)。 +```{.python .input} +#@tab paddle +#@save +def multibox_prior(data, sizes, ratios): + """生成以每个像素为中心具有不同形状的锚框""" + in_height, in_width = data.shape[-2:] + place, num_sizes, num_ratios = data.place, len(sizes), len(ratios) + boxes_per_pixel = (num_sizes + num_ratios - 1) + size_tensor = paddle.to_tensor(sizes, place=place) + ratio_tensor = paddle.to_tensor(ratios, place=place) + + # 为了将锚点移动到像素的中心,需要设置偏移量。 + # 因为一个像素的的高为1且宽为1,我们选择偏移我们的中心0.5 + offset_h, offset_w = 0.5, 0.5 + steps_h = 1.0 / in_height # 在y轴上缩放步长 + steps_w = 1.0 / in_width # 在x轴上缩放步长 + + # 生成锚框的所有中心点 + center_h = (paddle.arange(in_height) + offset_h) * steps_h + center_w = (paddle.arange(in_width) + offset_w) * steps_w + shift_y, shift_x = paddle.meshgrid(center_h, center_w) + shift_y, shift_x = shift_y.reshape([-1]), shift_x.reshape([-1]) + + # 生成“boxes_per_pixel”个高和宽, + # 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax) + w = paddle.concat((size_tensor * paddle.sqrt(ratio_tensor[0]), + sizes[0] * paddle.sqrt(ratio_tensor[1:])))\ + * in_height / in_width # 处理矩形输入 + h = paddle.concat((size_tensor / paddle.sqrt(ratio_tensor[0]), + sizes[0] / paddle.sqrt(ratio_tensor[1:]))) + # 除以2来获得半高和半宽 + anchor_manipulations = paddle.tile(paddle.stack((-w, -h, w, h)).T, + (in_height * in_width, 1)) / 2 + + # 每个中心点都将有“boxes_per_pixel”个锚框, + # 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次 + out_grid = paddle.stack([shift_x, shift_y, shift_x, shift_y], axis=1) + out_grid = paddle.tile(out_grid, repeat_times=[boxes_per_pixel]).reshape((-1, out_grid.shape[1])) + output = out_grid + anchor_manipulations + return output.unsqueeze(0) +``` + +可以看到[**返回的锚框变量`Y`的形状**]是(批量大小,锚框的数量,4)。 ```{.python .input} img = image.imread('../img/catdog.jpg').asnumpy() @@ -154,18 +208,35 @@ Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]) Y.shape ``` +```{.python .input} +#@tab paddle +img = d2l.plt.imread('../img/catdog.jpg') +h, w = img.shape[:2] + +print(h, w) +X = paddle.rand(shape=(1, 3, h, w)) +Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]) +Y.shape +``` + 将锚框变量`Y`的形状更改为(图像高度,图像宽度,以同一像素为中心的锚框的数量,4)后,我们可以获得以指定像素的位置为中心的所有锚框。 在接下来的内容中,我们[**访问以(250,250)为中心的第一个锚框**]。 它有四个元素:锚框左上角的$(x, y)$轴坐标和右下角的$(x, y)$轴坐标。 -将两个轴的坐标各分别除以图像的宽度和高度后,所得的值介于0和1之间。 +输出中两个轴的坐标各分别除以了图像的宽度和高度。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch boxes = Y.reshape(h, w, 5, 4) boxes[250, 250, 0, :] ``` -为了[**显示以图像中以某个像素为中心的所有锚框**],我们定义了下面的`show_bboxes`函数来在图像上绘制多个边界框。 +```{.python .input} +#@tab paddle +boxes = Y.reshape([h, w, 5, 4]) +boxes[250, 250, 0, :] +``` + +为了[**显示以图像中以某个像素为中心的所有锚框**],定义下面的`show_bboxes`函数来在图像上绘制多个边界框。 ```{.python .input} #@tab all @@ -192,10 +263,10 @@ def show_bboxes(axes, bboxes, labels=None, colors=None): bbox=dict(facecolor=color, lw=0)) ``` -正如你所看到的,变量`boxes`中$x$轴和$y$轴的坐标值已分别除以图像的宽度和高度。 +正如从上面代码中所看到的,变量`boxes`中$x$轴和$y$轴的坐标值已分别除以图像的宽度和高度。 绘制锚框时,我们需要恢复它们原始的坐标值。 -因此,我们在下面定义了变量`bbox_scale`。 -现在,我们可以绘制出图像中所有以(250,250)为中心的锚框了。 +因此,在下面定义了变量`bbox_scale`。 +现在可以绘制出图像中所有以(250,250)为中心的锚框了。 如下所示,缩放比为0.75且宽高比为1的蓝色锚框很好地围绕着图像中的狗。 ```{.python .input} @@ -212,21 +283,21 @@ show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale, 我们刚刚提到某个锚框“较好地”覆盖了图像中的狗。 如果已知目标的真实边界框,那么这里的“好”该如何如何量化呢? -直观地说,我们可以衡量锚框和真实边界框之间的相似性。 -我们知道*杰卡德系数*(Jaccard)可以衡量两组之间的相似性。 +直观地说,可以衡量锚框和真实边界框之间的相似性。 +*杰卡德系数*(Jaccard)可以衡量两组之间的相似性。 给定集合$\mathcal{A}$和$\mathcal{B}$,他们的杰卡德系数是他们交集的大小除以他们并集的大小: $$J(\mathcal{A},\mathcal{B}) = \frac{\left|\mathcal{A} \cap \mathcal{B}\right|}{\left| \mathcal{A} \cup \mathcal{B}\right|}.$$ 事实上,我们可以将任何边界框的像素区域视为一组像素。通 过这种方式,我们可以通过其像素集的杰卡德系数来测量两个边界框的相似性。 -对于两个边界框,我们通常将它们的杰卡德系数称为*交并比*(intersection over union,IoU),即两个边界框相交面积与相并面积之比,如 :numref:`fig_iou`所示。 +对于两个边界框,它们的杰卡德系数通常称为*交并比*(intersection over union,IoU),即两个边界框相交面积与相并面积之比,如 :numref:`fig_iou`所示。 交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框完全重合。 ![交并比是两个边界框相交面积与相并面积之比。](../img/iou.svg) :label:`fig_iou` -在接下来部分中,我们将使用交并比来衡量锚框和真实边界框之间、以及不同锚框之间的相似度。 +接下来部分将使用交并比来衡量锚框和真实边界框之间、以及不同锚框之间的相似度。 给定两个锚框或边界框的列表,以下`box_iou`函数将在这两个列表中计算它们成对的交并比。 ```{.python .input} @@ -279,6 +350,31 @@ def box_iou(boxes1, boxes2): return inter_areas / union_areas ``` +```{.python .input} +#@tab paddle +#@save +def box_iou(boxes1, boxes2): + """计算两个锚框或边界框列表中成对的交并比""" + box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) * + (boxes[:, 3] - boxes[:, 1])) + # boxes1,boxes2,areas1,areas2的形状: + # boxes1:(boxes1的数量,4), + # boxes2:(boxes2的数量,4), + # areas1:(boxes1的数量,), + # areas2:(boxes2的数量,) + areas1 = box_area(boxes1) + areas2 = box_area(boxes2) + # inter_upperlefts,inter_lowerrights,inters的形状: + # (boxes1的数量,boxes2的数量,2) + inter_upperlefts = paddle.maximum(boxes1[:, None, :2], boxes2[:, :2]) + inter_lowerrights = paddle.minimum(boxes1[:, None, 2:], boxes2[:, 2:]) + inters = (inter_lowerrights - inter_upperlefts).clip(min=0) + # inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量) + inter_areas = inters[:, :, 0] * inters[:, :, 1] + union_areas = areas1[:, None] + areas2 - inter_areas + return inter_areas / union_areas +``` + ## 在训练数据中标注锚框 :label:`subsec_labeling-anchor-boxes` @@ -286,22 +382,22 @@ def box_iou(boxes1, boxes2): 为了训练目标检测模型,我们需要每个锚框的*类别*(class)和*偏移量*(offset)标签,其中前者是与锚框相关的对象的类别,后者是真实边界框相对于锚框的偏移量。 在预测时,我们为每个图像生成多个锚框,预测所有锚框的类别和偏移量,根据预测的偏移量调整它们的位置以获得预测的边界框,最后只输出符合特定条件的预测边界框。 -我们知道,目标检测训练集带有“真实边界框”的位置及其包围物体类别的标签。 +目标检测训练集带有*真实边界框*的位置及其包围物体类别的标签。 要标记任何生成的锚框,我们可以参考分配到的最接近此锚框的真实边界框的位置和类别标签。 -在下文中,我们将介绍一个算法,它能够把最接近的真实边界框分配给锚框。 +下文将介绍一个算法,它能够把最接近的真实边界框分配给锚框。 ### [**将真实边界框分配给锚框**] 给定图像,假设锚框是$A_1, A_2, \ldots, A_{n_a}$,真实边界框是$B_1, B_2, \ldots, B_{n_b}$,其中$n_a \geq n_b$。 让我们定义一个矩阵$\mathbf{X} \in \mathbb{R}^{n_a \times n_b}$,其中第$i$行、第$j$列的元素$x_{ij}$是锚框$A_i$和真实边界框$B_j$的IoU。 -该算法包含以下步骤: +该算法包含以下步骤。 1. 在矩阵$\mathbf{X}$中找到最大的元素,并将它的行索引和列索引分别表示为$i_1$和$j_1$。然后将真实边界框$B_{j_1}$分配给锚框$A_{i_1}$。这很直观,因为$A_{i_1}$和$B_{j_1}$是所有锚框和真实边界框配对中最相近的。在第一个分配完成后,丢弃矩阵中${i_1}^\mathrm{th}$行和${j_1}^\mathrm{th}$列中的所有元素。 1. 在矩阵$\mathbf{X}$中找到剩余元素中最大的元素,并将它的行索引和列索引分别表示为$i_2$和$j_2$。我们将真实边界框$B_{j_2}$分配给锚框$A_{i_2}$,并丢弃矩阵中${i_2}^\mathrm{th}$行和${j_2}^\mathrm{th}$列中的所有元素。 -1. 此时,矩阵$\mathbf{X}$中两行和两列中的元素已被丢弃。我们继续,直到丢弃掉矩阵$\mathbf{X}$中$n_b$列中的所有元素。此时,我们已经为这$n_b$个锚框各自分配了一个真实边界框。 +1. 此时,矩阵$\mathbf{X}$中两行和两列中的元素已被丢弃。我们继续,直到丢弃掉矩阵$\mathbf{X}$中$n_b$列中的所有元素。此时已经为这$n_b$个锚框各自分配了一个真实边界框。 1. 只遍历剩下的$n_a - n_b$个锚框。例如,给定任何锚框$A_i$,在矩阵$\mathbf{X}$的第$i^\mathrm{th}$行中找到与$A_i$的IoU最大的真实边界框$B_j$,只有当此IoU大于预定义的阈值时,才将$B_j$分配给$A_i$。 -让我们用一个具体的例子来说明上述算法。 +下面用一个具体的例子来说明上述算法。 如 :numref:`fig_anchor_label`(左)所示,假设矩阵$\mathbf{X}$中的最大值为$x_{23}$,我们将真实边界框$B_3$分配给锚框$A_2$。 然后,我们丢弃矩阵第2行和第3列中的所有元素,在剩余元素(阴影区域)中找到最大的$x_{71}$,然后将真实边界框$B_1$分配给锚框$A_7$。 接下来,如 :numref:`fig_anchor_label`(中)所示,丢弃矩阵第7行和第1列中的所有元素,在剩余元素(阴影区域)中找到最大的$x_{54}$,然后将真实边界框$B_4$分配给锚框$A_5$。 @@ -367,6 +463,34 @@ def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): return anchors_bbox_map ``` +```{.python .input} +#@tab paddle +#@save +def assign_anchor_to_bbox(ground_truth, anchors, place, iou_threshold=0.5): + """将最接近的真实边界框分配给锚框""" + num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0] + # 位于第i行和第j列的元素x_ij是锚框i和真实边界框j的IoU + jaccard = box_iou(anchors, ground_truth) + # 对于每个锚框,分配的真实边界框的张量 + anchors_bbox_map = paddle.full((num_anchors,), -1, dtype=paddle.int64) + # 根据阈值,决定是否分配真实边界框 + max_ious = paddle.max(jaccard, axis=1) + indices = paddle.argmax(jaccard, axis=1) + anc_i = paddle.nonzero(max_ious >= 0.5).reshape([-1]) + box_j = indices[max_ious >= 0.5] + anchors_bbox_map[anc_i] = box_j + col_discard = paddle.full((num_anchors,), -1) + row_discard = paddle.full((num_gt_boxes,), -1) + for _ in range(num_gt_boxes): + max_idx = paddle.argmax(jaccard) + box_idx = paddle.cast((max_idx % num_gt_boxes), dtype='int64') + anc_idx = paddle.cast((max_idx / num_gt_boxes), dtype='int64') + anchors_bbox_map[anc_idx] = box_idx + jaccard[:, box_idx] = col_discard + jaccard[anc_idx, :] = row_discard + return anchors_bbox_map +``` + ### 标记类别和偏移量 现在我们可以为每个锚框标记类别和偏移量了。 @@ -374,9 +498,8 @@ def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): 一方面,锚框$A$的类别将被标记为与$B$相同。 另一方面,锚框$A$的偏移量将根据$B$和$A$中心坐标的相对位置以及这两个框的相对大小进行标记。 鉴于数据集内不同的框的位置和大小不同,我们可以对那些相对位置和大小应用变换,使其获得分布更均匀且易于拟合的偏移量。 -在这里,我们介绍一种常见的变换。 -[**给定框$A$和$B$,中心坐标分别为$(x_a, y_a)$和$(x_b, y_b)$,宽度分别为$w_a$和$w_b$,高度分别为$h_a$和$h_b$。 -我们可以将$A$的偏移量标记为: +这里介绍一种常见的变换。 +[**给定框$A$和$B$,中心坐标分别为$(x_a, y_a)$和$(x_b, y_b)$,宽度分别为$w_a$和$w_b$,高度分别为$h_a$和$h_b$,可以将$A$的偏移量标记为: $$\left( \frac{ \frac{x_b - x_a}{w_a} - \mu_x }{\sigma_x}, \frac{ \frac{y_b - y_a}{h_a} - \mu_y }{\sigma_y}, @@ -399,8 +522,8 @@ def offset_boxes(anchors, assigned_bb, eps=1e-6): return offset ``` -如果一个锚框没有被分配真实边界框,我们只需将锚框的类别标记为“背景”(background)。 -背景类别的锚框通常被称为“负类”锚框,其余的被称为“正类”锚框。 +如果一个锚框没有被分配真实边界框,我们只需将锚框的类别标记为*背景*(background)。 +背景类别的锚框通常被称为*负类*锚框,其余的被称为*正类*锚框。 我们使用真实边界框(`labels`参数)实现以下`multibox_target`函数,来[**标记锚框的类别和偏移量**](`anchors`参数)。 此函数将背景类别的索引设置为零,然后将新类别的整数索引递增一。 @@ -422,7 +545,7 @@ def multibox_target(anchors, labels): assigned_bb = d2l.zeros((num_anchors, 4), dtype=np.float32, ctx=device) # 使用真实边界框来标记锚框的类别。 - # 如果一个锚框没有被分配,我们标记其为背景(值为零) + # 如果一个锚框没有被分配,标记其为背景(值为零) indices_true = np.nonzero(anchors_bbox_map >= 0)[0] bb_idx = anchors_bbox_map[indices_true] class_labels[indices_true] = label[bb_idx, 0].astype('int32') + 1 @@ -458,7 +581,7 @@ def multibox_target(anchors, labels): assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32, device=device) # 使用真实边界框来标记锚框的类别。 - # 如果一个锚框没有被分配,我们标记其为背景(值为零) + # 如果一个锚框没有被分配,标记其为背景(值为零) indices_true = torch.nonzero(anchors_bbox_map >= 0) bb_idx = anchors_bbox_map[indices_true] class_labels[indices_true] = label[bb_idx, 0].long() + 1 @@ -474,9 +597,44 @@ def multibox_target(anchors, labels): return (bbox_offset, bbox_mask, class_labels) ``` +```{.python .input} +#@tab paddle +#@save +def multibox_target(anchors, labels): + """使用真实边界框标记锚框""" + batch_size, anchors = labels.shape[0], anchors.squeeze(0) + batch_offset, batch_mask, batch_class_labels = [], [], [] + place, num_anchors = anchors.place, anchors.shape[0] + for i in range(batch_size): + label = labels[i, :, :] + anchors_bbox_map = assign_anchor_to_bbox( + label[:, 1:], anchors, place) + bbox_mask = paddle.tile(paddle.to_tensor((anchors_bbox_map >= 0), dtype='float32').unsqueeze(-1), (1, 4)) + # 将类标签和分配的边界框坐标初始化为零 + class_labels = paddle.zeros(paddle.to_tensor(num_anchors), dtype=paddle.int64) + assigned_bb = paddle.zeros(paddle.to_tensor((num_anchors, 4)), dtype=paddle.float32) + # 使用真实边界框来标记锚框的类别。 + # 如果一个锚框没有被分配,我们标记其为背景(值为零) + indices_true = paddle.nonzero(anchors_bbox_map >= 0).numpy() + bb_idx = anchors_bbox_map[indices_true].numpy() + class_labels[indices_true] = label.numpy()[bb_idx, 0][:] + 1 + assigned_bb[indices_true] = label.numpy()[bb_idx, 1:] + class_labels = paddle.to_tensor(class_labels) + assigned_bb = paddle.to_tensor(assigned_bb) + # 偏移量转换 + offset = offset_boxes(anchors, assigned_bb) * bbox_mask + batch_offset.append(offset.reshape([-1])) + batch_mask.append(bbox_mask.reshape([-1])) + batch_class_labels.append(class_labels) + bbox_offset = paddle.stack(batch_offset) + bbox_mask = paddle.stack(batch_mask) + class_labels = paddle.stack(batch_class_labels) + return (bbox_offset, bbox_mask, class_labels) +``` + ### 一个例子 -让我们通过一个具体的例子来说明锚框标签。 +下面通过一个具体的例子来说明锚框标签。 我们已经为加载图像中的狗和猫定义了真实边界框,其中第一个元素是类别(0代表狗,1代表猫),其余四个元素是左上角和右下角的$(x, y)$轴坐标(范围介于0和1之间)。 我们还构建了五个锚框,用左上角和右下角的坐标进行标记:$A_0, \ldots, A_4$(索引从0开始)。 然后我们[**在图像中绘制这些真实边界框和锚框**]。 @@ -509,6 +667,12 @@ labels = multibox_target(anchors.unsqueeze(dim=0), ground_truth.unsqueeze(dim=0)) ``` +```{.python .input} +#@tab paddle +labels = multibox_target(anchors.unsqueeze(axis=0), + ground_truth.unsqueeze(axis=0)) +``` + 返回的结果中有三个元素,都是张量格式。第三个元素包含标记的输入锚框的类别。 让我们根据图像中的锚框和真实边界框的位置来分析下面返回的类别标签。 @@ -548,7 +712,7 @@ labels[0] :label:`subsec_predicting-bounding-boxes-nms` 在预测时,我们先为图像生成多个锚框,再为这些锚框一一预测类别和偏移量。 -一个“预测好的边界框”则根据其中某个带有预测偏移量的锚框而生成。 +一个*预测好的边界框*则根据其中某个带有预测偏移量的锚框而生成。 下面我们实现了`offset_inverse`函数,该函数将锚框和偏移量预测作为输入,并[**应用逆偏移变换来返回预测的边界框坐标**]。 ```{.python .input} @@ -571,7 +735,7 @@ def offset_inverse(anchors, offset_preds): 对于一个预测边界框$B$,目标检测模型会计算每个类别的预测概率。 假设最大的预测概率为$p$,则该概率所对应的类别$B$即为预测的类别。 具体来说,我们将$p$称为预测边界框$B$的*置信度*(confidence)。 -在同一张图像中,所有预测的非背景边界框都按置信度降序排序,以生成列表$L$。然后我们通过以下步骤操作排序列表$L$: +在同一张图像中,所有预测的非背景边界框都按置信度降序排序,以生成列表$L$。然后我们通过以下步骤操作排序列表$L$。 1. 从$L$中选取置信度最高的预测边界框$B_1$作为基准,然后将所有与$B_1$的IoU超过预定阈值$\epsilon$的非基准预测边界框从$L$中移除。这时,$L$保留了置信度最高的预测边界框,去除了与其太过相似的其他预测边界框。简而言之,那些具有*非极大值*置信度的边界框被*抑制*了。 1. 从$L$中选取置信度第二高的预测边界框$B_2$作为又一个基准,然后将所有与$B_2$的IoU大于$\epsilon$的非基准预测边界框从$L$中移除。 @@ -615,8 +779,26 @@ def nms(boxes, scores, iou_threshold): return d2l.tensor(keep, device=boxes.device) ``` +```{.python .input} +#@tab paddle +#@save +def nms(boxes, scores, iou_threshold): + """对预测边界框的置信度进行排序""" + B = paddle.argsort(scores, axis=-1, descending=True) + keep = [] # 保留预测边界框的指标 + while B.numel().item() > 0: + i = B[0] + keep.append(i.item()) + if B.numel().item() == 1: break + iou = box_iou(boxes[i.numpy(), :].reshape([-1, 4]), + paddle.to_tensor(boxes.numpy()[B[1:].numpy(), :]).reshape([-1, 4])).reshape([-1]) + inds = paddle.nonzero(iou <= iou_threshold).numpy().reshape([-1]) + B = paddle.to_tensor(B.numpy()[inds + 1]) + return paddle.to_tensor(keep, place=boxes.place, dtype='int64') +``` + 我们定义以下`multibox_detection`函数来[**将非极大值抑制应用于预测边界框**]。 -如果你发现实现有点复杂,请不要担心。我们将在实现之后,马上用一个具体的例子来展示它是如何工作的。 +这里的实现有点复杂,请不要担心。我们将在实现之后,马上用一个具体的例子来展示它是如何工作的。 ```{.python .input} #@save @@ -689,12 +871,49 @@ def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, return d2l.stack(out) ``` +```{.python .input} +#@tab paddle +#@save +def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, + pos_threshold=0.009999999): + """使用非极大值抑制来预测边界框""" + batch_size = cls_probs.shape[0] + anchors = anchors.squeeze(0) + num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2] + out = [] + for i in range(batch_size): + cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape([-1, 4]) + conf = paddle.max(cls_prob[1:], 0) + class_id = paddle.argmax(cls_prob[1:], 0) + predicted_bb = offset_inverse(anchors, offset_pred) + keep = nms(predicted_bb, conf, nms_threshold) + + # 找到所有的non_keep索引,并将类设置为背景 + all_idx = paddle.arange(num_anchors, dtype='int64') + combined = paddle.concat((keep, all_idx)) + uniques, counts = combined.unique(return_counts=True) + non_keep = uniques[counts == 1] + all_id_sorted = paddle.concat([keep, non_keep]) + class_id[non_keep.numpy()] = -1 + class_id = class_id[all_id_sorted] + conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted] + # pos_threshold是一个用于非背景预测的阈值 + below_min_idx = (conf < pos_threshold) + class_id[below_min_idx.numpy()] = -1 + conf[below_min_idx.numpy()] = 1 - conf[below_min_idx.numpy()] + pred_info = paddle.concat((paddle.to_tensor(class_id, dtype='float32').unsqueeze(1), + paddle.to_tensor(conf, dtype='float32').unsqueeze(1), + predicted_bb), axis=1) + out.append(pred_info) + return paddle.stack(out) +``` + 现在让我们[**将上述算法应用到一个带有四个锚框的具体示例中**]。 为简单起见,我们假设预测的偏移量都是零,这意味着预测的边界框即是锚框。 对于背景、狗和猫其中的每个类,我们还定义了它的预测概率。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch anchors = d2l.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95], [0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]]) offset_preds = d2l.tensor([0] * d2l.size(anchors)) @@ -703,6 +922,16 @@ cls_probs = d2l.tensor([[0] * 4, # 背景的预测概率 [0.1, 0.2, 0.3, 0.9]]) # 猫的预测概率 ``` +```{.python .input} +#@tab paddle +anchors = d2l.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95], + [0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]]) +offset_preds = d2l.tensor([0] * anchors.numel().item()) +cls_probs = d2l.tensor([[0] * 4, # 背景的预测概率 + [0.9, 0.8, 0.7, 0.1], # 狗的预测概率 + [0.1, 0.2, 0.3, 0.9]]) # 猫的预测概率 +``` + 我们可以[**在图像上绘制这些预测边界框和置信度**]。 ```{.python .input} @@ -738,6 +967,15 @@ output = multibox_detection(cls_probs.unsqueeze(dim=0), output ``` +```{.python .input} +#@tab paddle +output = multibox_detection(cls_probs.unsqueeze(axis=0), + offset_preds.unsqueeze(axis=0), + anchors.unsqueeze(axis=0), + nms_threshold=0.5) +output +``` + 删除-1类别(背景)的预测边界框后,我们可以[**输出由非极大值抑制保存的最终预测边界框**]。 ```{.python .input} @@ -758,14 +996,14 @@ for i in d2l.numpy(output[0]): * 我们以图像的每个像素为中心生成不同形状的锚框。 * 交并比(IoU)也被称为杰卡德系数,用于衡量两个边界框的相似性。它是相交面积与相并面积的比率。 * 在训练集中,我们需要给每个锚框两种类型的标签。一个是与锚框中目标检测的类别,另一个是锚框真实相对于边界框的偏移量。 -* 在预测期间,我们可以使用非极大值抑制(NMS)来移除类似的预测边界框,从而简化输出。 +* 预测期间可以使用非极大值抑制(NMS)来移除类似的预测边界框,从而简化输出。 ## 练习 1. 在`multibox_prior`函数中更改`sizes`和`ratios`的值。生成的锚框有什么变化? 1. 构建并可视化两个IoU为0.5的边界框。它们是怎样重叠的? 1. 在 :numref:`subsec_labeling-anchor-boxes`和 :numref:`subsec_predicting-bounding-boxes-nms`中修改变量`anchors`,结果如何变化? -1. 非极大值抑制是一种贪心算法,它通过*移除*来抑制预测的边界框。是否存在一种可能,被移除的一些框实际上是有用的?如何修改这个算法来柔和地抑制?你可以参考Soft-NMS :cite:`Bodla.Singh.Chellappa.ea.2017`。 +1. 非极大值抑制是一种贪心算法,它通过*移除*来抑制预测的边界框。是否存在一种可能,被移除的一些框实际上是有用的?如何修改这个算法来柔和地抑制?可以参考Soft-NMS :cite:`Bodla.Singh.Chellappa.ea.2017`。 1. 如果非手动,非最大限度的抑制可以被学习吗? :begin_tab:`mxnet` @@ -775,3 +1013,7 @@ for i in d2l.numpy(output[0]): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2946) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11804) +:end_tab: diff --git a/chapter_computer-vision/bounding-box.md b/chapter_computer-vision/bounding-box.md index 4994fb56f..4e82718c2 100644 --- a/chapter_computer-vision/bounding-box.md +++ b/chapter_computer-vision/bounding-box.md @@ -1,7 +1,7 @@ # 目标检测和边界框 :label:`sec_bbox` -在前面的章节(例如 :numref:`sec_alexnet`— :numref:`sec_googlenet`)中,我们介绍了各种图像分类模型。 +前面的章节(例如 :numref:`sec_alexnet`— :numref:`sec_googlenet`)介绍了各种图像分类模型。 在图像分类任务中,我们假设图像中只有一个主要物体对象,我们只关注如何识别其类别。 然而,很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。 在计算机视觉里,我们将这类任务称为*目标检测*(object detection)或*目标识别*(object recognition)。 @@ -10,7 +10,7 @@ 例如,在无人驾驶里,我们需要通过识别拍摄到的视频图像里的车辆、行人、道路和障碍物的位置来规划行进线路。 机器人也常通过该任务来检测感兴趣的目标。安防领域则需要检测异常目标,如歹徒或者炸弹。 -在接下来的几节中,我们将介绍几种用于目标检测的深度学习方法。 +接下来的几节将介绍几种用于目标检测的深度学习方法。 我们将首先介绍目标的*位置*。 ```{.python .input} @@ -35,6 +35,15 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +``` + 下面加载本节将使用的示例图像。可以看到图像左边是一只狗,右边是一只猫。 它们是这张图像里的两个主要目标。 @@ -45,7 +54,7 @@ d2l.plt.imshow(img); ``` ```{.python .input} -#@tab pytorch, tensorflow +#@tab pytorch, tensorflow, paddle d2l.set_figsize() img = d2l.plt.imread('../img/catdog.jpg') d2l.plt.imshow(img); @@ -143,3 +152,7 @@ fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red')); :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2944) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11803) +:end_tab: diff --git a/chapter_computer-vision/fcn.md b/chapter_computer-vision/fcn.md index e8a01bc8b..1e329cbfc 100644 --- a/chapter_computer-vision/fcn.md +++ b/chapter_computer-vision/fcn.md @@ -25,6 +25,18 @@ from torch import nn from torch.nn import functional as F ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +from paddle.nn import functional as F +import paddle.vision as paddlevision +``` + ## 构造模型 下面我们了解一下全卷积网络模型最基本的设计。 @@ -48,6 +60,12 @@ pretrained_net = torchvision.models.resnet18(pretrained=True) list(pretrained_net.children())[-3:] ``` +```{.python .input} +#@tab paddle +pretrained_net = paddlevision.models.resnet18(pretrained=True) +list(pretrained_net.children())[-3:] +``` + 接下来,我们[**创建一个全卷积网络`net`**]。 它复制了ResNet-18中大部分的预训练层,除了最后的全局平均汇聚层和最接近输出的全连接层。 @@ -58,7 +76,7 @@ for layer in pretrained_net.features[:-2]: ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net = nn.Sequential(*list(pretrained_net.children())[:-2]) ``` @@ -75,8 +93,14 @@ X = torch.rand(size=(1, 3, 320, 480)) net(X).shape ``` -接下来,我们[**使用$1\times1$卷积层将输出通道数转换为Pascal VOC2012数据集的类数(21类)。**] -最后,我们需要(**将特征图的高度和宽度增加32倍**),从而将其变回输入图像的高和宽。 +```{.python .input} +#@tab paddle +X = paddle.rand(shape=(1, 3, 320, 480)) +net(X).shape +``` + +接下来[**使用$1\times1$卷积层将输出通道数转换为Pascal VOC2012数据集的类数(21类)。**] +最后需要(**将特征图的高度和宽度增加32倍**),从而将其变回输入图像的高和宽。 回想一下 :numref:`sec_padding`中卷积层输出形状的计算方法: 由于$(320-64+16\times2+32)/32=10$且$(480-64+16\times2+32)/32=15$,我们构造一个步幅为$32$的转置卷积层,并将卷积核的高和宽设为$64$,填充为$16$。 我们可以看到如果步幅为$s$,填充为$s/2$(假设$s/2$是整数)且卷积核的高和宽为$2s$,转置卷积核会将输入的高和宽分别放大$s$倍。 @@ -96,6 +120,14 @@ net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes, num_classes, kernel_size=64, padding=16, stride=32)) ``` +```{.python .input} +#@tab paddle +num_classes = 21 +net.add_sublayer('final_conv', nn.Conv2D(512, num_classes, kernel_size=1)) +net.add_sublayer('transpose_conv', nn.Conv2DTranspose(num_classes, num_classes, + kernel_size=64, padding=16, stride=32)) +``` + ## [**初始化转置卷积层**] 在图像处理中,我们有时需要将图像放大,即*上采样*(upsampling)。 @@ -103,11 +135,12 @@ net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes, num_classes, 是常用的上采样方法之一,它也经常用于初始化转置卷积层。 为了解释双线性插值,假设给定输入图像,我们想要计算上采样输出图像上的每个像素。 -首先,将输出图像的坐标$(x,y)$映射到输入图像的坐标$(x',y')$上。 + +1. 将输出图像的坐标$(x,y)$映射到输入图像的坐标$(x',y')$上。 例如,根据输入与输出的尺寸之比来映射。 请注意,映射后的$x′$和$y′$是实数。 -然后,在输入图像上找到离坐标$(x',y')$最近的4个像素。 -最后,输出图像在坐标$(x,y)$上的像素依据输入图像上这4个像素及其与$(x',y')$的相对距离来计算。 +2. 在输入图像上找到离坐标$(x',y')$最近的4个像素。 +3. 输出图像在坐标$(x,y)$上的像素依据输入图像上这4个像素及其与$(x',y')$的相对距离来计算。 双线性插值的上采样可以通过转置卷积层实现,内核由以下`bilinear_kernel`函数构造。 限于篇幅,我们只给出`bilinear_kernel`函数的实现,不讨论算法的原理。 @@ -146,6 +179,24 @@ def bilinear_kernel(in_channels, out_channels, kernel_size): return weight ``` +```{.python .input} +#@tab paddle +def bilinear_kernel(in_channels, out_channels, kernel_size): + factor = (kernel_size + 1) // 2 + if kernel_size % 2 == 1: + center = factor - 1 + else: + center = factor - 0.5 + og = (paddle.arange(kernel_size).reshape([-1, 1]), + paddle.arange(kernel_size).reshape([1, -1])) + filt = (1 - paddle.abs(og[0] - center) / factor) * \ + (1 - paddle.abs(og[1] - center) / factor) + weight = paddle.zeros((in_channels, out_channels, + kernel_size, kernel_size)) + weight[range(in_channels), range(out_channels), :, :] = filt + return weight +``` + 让我们用[**双线性插值的上采样实验**]它由转置卷积层实现。 我们构造一个将输入的高和宽放大2倍的转置卷积层,并将其卷积核用`bilinear_kernel`函数初始化。 @@ -161,6 +212,13 @@ conv_trans = nn.ConvTranspose2d(3, 3, kernel_size=4, padding=1, stride=2, conv_trans.weight.data.copy_(bilinear_kernel(3, 3, 4)); ``` +```{.python .input} +#@tab paddle +conv_trans = nn.Conv2DTranspose(3, 3, kernel_size=4, padding=1, stride=2, + bias_attr=False) +conv_trans.weight.set_value(bilinear_kernel(3, 3, 4)); +``` + 读取图像`X`,将上采样的结果记作`Y`。为了打印图像,我们需要调整通道维的位置。 ```{.python .input} @@ -178,6 +236,14 @@ Y = conv_trans(X) out_img = Y[0].permute(1, 2, 0).detach() ``` +```{.python .input} +#@tab paddle +img = paddlevision.transforms.ToTensor()(d2l.Image.open('../img/catdog.jpg')) +X = img.unsqueeze(0) +Y = conv_trans(X) +out_img = Y[0].transpose([1, 2, 0]).detach() +``` + 可以看到,转置卷积层将图像的高和宽分别放大了2倍。 除了坐标刻度不同,双线性插值放大的图像和在 :numref:`sec_bbox`中打印出的原图看上去没什么两样。 @@ -198,7 +264,16 @@ print('output image shape:', out_img.shape) d2l.plt.imshow(out_img); ``` -在全卷积网络中,我们[**用双线性插值的上采样初始化转置卷积层。对于$1\times 1$卷积层,我们使用Xavier初始化参数。**] +```{.python .input} +#@tab paddle +d2l.set_figsize() +print('input image shape:', img.transpose([1, 2, 0]).shape) +d2l.plt.imshow(img.transpose([1, 2, 0])); +print('output image shape:', out_img.shape) +d2l.plt.imshow(out_img); +``` + +全卷积网络[**用双线性插值的上采样初始化转置卷积层。对于$1\times 1$卷积层,我们使用Xavier初始化参数。**] ```{.python .input} W = bilinear_kernel(num_classes, num_classes, 64) @@ -212,17 +287,43 @@ W = bilinear_kernel(num_classes, num_classes, 64) net.transpose_conv.weight.data.copy_(W); ``` +```{.python .input} +#@tab paddle +W = bilinear_kernel(num_classes, num_classes, 64) +net.transpose_conv.weight.set_value(W); +``` + ## [**读取数据集**] 我们用 :numref:`sec_semantic_segmentation`中介绍的语义分割读取数据集。 指定随机裁剪的输出图像的形状为$320\times 480$:高和宽都可以被$32$整除。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch batch_size, crop_size = 32, (320, 480) train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size) ``` +```{.python .input} +#@tab paddle +import os +def load_data_voc(batch_size, crop_size): + """加载VOC语义分割数据集 + Defined in :numref:`sec_semantic_segmentation`""" + voc_dir = d2l.download_extract('voc2012', os.path.join( + 'VOCdevkit', 'VOC2012')) + train_iter = paddle.io.DataLoader( + d2l.VOCSegDataset(True, crop_size, voc_dir), batch_size=batch_size, + shuffle=True, return_list=True, drop_last=True, num_workers=0) + test_iter = paddle.io.DataLoader( + d2l.VOCSegDataset(False, crop_size, voc_dir), batch_size=batch_size, + drop_last=True, return_list=True, num_workers=0) + return train_iter, test_iter + +batch_size, crop_size = 32, (320, 480) +train_iter, test_iter = load_data_voc(batch_size, crop_size) +``` + ## [**训练**] 现在我们可以训练全卷积网络了。 @@ -248,6 +349,16 @@ trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=wd) d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) ``` +```{.python .input} +#@tab paddle +def loss(inputs, targets): + return F.cross_entropy(inputs.transpose([0, 2, 3, 1]), targets, reduction='none').mean(1).mean(1) + +num_epochs, lr, wd, devices = 5, 0.001, 1e-3, d2l.try_all_gpus() +trainer = paddle.optimizer.SGD(learning_rate=lr, parameters=net.parameters(), weight_decay=wd) +d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices[:1]) +``` + ## [**预测**] 在预测时,我们需要将输入图像在各个通道做标准化,并转成卷积神经网络所需要的四维输入格式。 @@ -268,6 +379,14 @@ def predict(img): return pred.reshape(pred.shape[1], pred.shape[2]) ``` +```{.python .input} +#@tab paddle +def predict(img): + X = paddle.to_tensor(test_iter.dataset.normalize_image(img),dtype='float32').unsqueeze(0) + pred = net(X).argmax(axis=1) + return pred.reshape([pred.shape[1], pred.shape[2]]) +``` + 为了[**可视化预测的类别**]给每个像素,我们将预测类别映射回它们在数据集中的标注颜色。 ```{.python .input} @@ -285,6 +404,14 @@ def label2image(pred): return colormap[X, :] ``` +```{.python .input} +#@tab paddle +def label2image(pred): + colormap = paddle.to_tensor(d2l.VOC_COLORMAP) + X = pred.astype(paddle.int32) + return colormap[X] +``` + 测试数据集中的图像大小和形状各异。 由于模型使用了步幅为32的转置卷积层,因此当输入图像的高或宽无法被32整除时,转置卷积层输出的高或宽会与输入图像的尺寸有偏差。 为了解决这个问题,我们可以在图像中截取多块高和宽为32的整数倍的矩形区域,并分别对这些区域中的像素做前向传播。 @@ -321,6 +448,21 @@ for i in range(n): d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2); ``` +```{.python .input} +#@tab paddle +voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012') +test_images, test_labels = d2l.read_voc_images(voc_dir, False) +n, imgs = 4, [] +for i in range(n): + crop_rect = (0, 0, 320, 480) + X = paddlevision.transforms.crop(test_images[i], *crop_rect) + pred = label2image(predict(X)) + imgs += [X.transpose([1,2,0]).astype('uint8'), pred, + paddlevision.transforms.crop( + test_labels[i], *crop_rect).transpose([1, 2, 0]).astype("uint8")] +d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2); +``` + ## 小结 * 全卷积网络先使用卷积神经网络抽取图像特征,然后通过$1\times 1$卷积层将通道数变换为类别个数,最后通过转置卷积层将特征图的高和宽变换为输入图像的尺寸。 @@ -340,3 +482,7 @@ d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2); :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/3297) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11811) +:end_tab: diff --git a/chapter_computer-vision/fine-tuning.md b/chapter_computer-vision/fine-tuning.md index e96075cba..cc1ae4f68 100644 --- a/chapter_computer-vision/fine-tuning.md +++ b/chapter_computer-vision/fine-tuning.md @@ -1,7 +1,7 @@ # 微调 :label:`sec_fine_tuning` -在前面的一些章节中,我们介绍了如何在只有6万张图像的Fashion-MNIST训练数据集上训练模型。 +前面的一些章节介绍了如何在只有6万张图像的Fashion-MNIST训练数据集上训练模型。 我们还描述了学术界当下使用最广泛的大规模图像数据集ImageNet,它有超过1000万的图像和1000类的物体。 然而,我们平常接触到的数据集的规模通常在这两者之间。 @@ -22,7 +22,7 @@ ## 步骤 -在本节中,我们将介绍迁移学习中的常见技巧:*微调*(fine-tuning)。如 :numref:`fig_finetune`所示,微调包括以下四个步骤: +本节将介绍迁移学习中的常见技巧:*微调*(fine-tuning)。如 :numref:`fig_finetune`所示,微调包括以下四个步骤。 1. 在源数据集(例如ImageNet数据集)上预训练神经网络模型,即*源模型*。 1. 创建一个新的神经网络模型,即*目标模型*。这将复制源模型上的所有模型设计及其参数(输出层除外)。我们假定这些模型参数包含从源数据集中学到的知识,这些知识也将适用于目标数据集。我们还假设源模型的输出层与源数据集的标签密切相关;因此不在目标模型中使用该层。 @@ -60,6 +60,18 @@ import torchvision import os ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +from paddle import nn +import paddle +import paddle.vision as paddlevision +import os +``` + ### 获取数据集 我们使用的[**热狗数据集来源于网络**]。 @@ -94,8 +106,13 @@ train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train')) test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test')) ``` -下面显示了前8个正类样本图片和最后8张负类样本图片。 -正如你所看到的,[**图像的大小和纵横比各有不同**]。 +```{.python .input} +#@tab paddle +train_imgs = paddlevision.datasets.DatasetFolder(os.path.join(data_dir, 'train')) +test_imgs = paddlevision.datasets.DatasetFolder(os.path.join(data_dir, 'test')) +``` + +下面显示了前8个正类样本图片和最后8张负类样本图片。正如所看到的,[**图像的大小和纵横比各有不同**]。 ```{.python .input} #@tab all @@ -142,17 +159,36 @@ train_augs = torchvision.transforms.Compose([ normalize]) test_augs = torchvision.transforms.Compose([ - torchvision.transforms.Resize(256), + torchvision.transforms.Resize([256, 256]), torchvision.transforms.CenterCrop(224), torchvision.transforms.ToTensor(), normalize]) ``` +```{.python .input} +#@tab paddle +# 使用RGB通道的均值和标准差,以标准化每个通道 +normalize = paddle.vision.transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + +train_augs = paddlevision.transforms.Compose([ + paddlevision.transforms.RandomResizedCrop(224), + paddlevision.transforms.RandomHorizontalFlip(), + paddlevision.transforms.ToTensor(), + normalize]) + +test_augs = paddlevision.transforms.Compose([ + paddlevision.transforms.Resize(256), + paddlevision.transforms.CenterCrop(224), + paddlevision.transforms.ToTensor(), + normalize]) +``` + ### [**定义和初始化模型**] 我们使用在ImageNet数据集上预训练的ResNet-18作为源模型。 在这里,我们指定`pretrained=True`以自动下载预训练的模型参数。 -如果你首次使用此模型,则需要连接互联网才能下载。 +如果首次使用此模型,则需要连接互联网才能下载。 ```{.python .input} pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True) @@ -163,6 +199,11 @@ pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True) pretrained_net = torchvision.models.resnet18(pretrained=True) ``` +```{.python .input} +#@tab paddle +pretrained_net = paddlevision.models.resnet18(pretrained=True) +``` + :begin_tab:`mxnet` 预训练的源模型实例包含两个成员变量:`features`和`output`。 前者包含除输出层以外的模型的所有层,后者是模型的输出层。 @@ -176,12 +217,18 @@ pretrained_net = torchvision.models.resnet18(pretrained=True) 下面给出了源模型的成员变量`fc`。 :end_tab: +:begin_tab:`paddle` +预训练的源模型实例包含许多特征层和一个输出层`fc`。 +此划分的主要目的是促进对除输出层以外所有层的模型参数进行微调。 +下面给出了源模型的成员变量`fc`。 +:end_tab: + ```{.python .input} pretrained_net.output ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle pretrained_net.fc ``` @@ -210,6 +257,13 @@ finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2) nn.init.xavier_uniform_(finetune_net.fc.weight); ``` +```{.python .input} +#@tab paddle +finetune_net = paddlevision.models.resnet18(pretrained=True) +finetune_net.fc = nn.Linear(pretrained_net.fc.state_dict()['weight'].shape[0], 2) +nn.initializer.XavierUniform(pretrained_net.fc.state_dict()['weight']); +``` + ### [**微调模型**] 首先,我们定义了一个训练函数`train_fine_tuning`,该函数使用微调,因此可以多次调用。 @@ -257,6 +311,32 @@ def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5, devices) ``` +```{.python .input} +#@tab paddle +# 如果param_group=True,输出层中的模型参数将使用十倍的学习率 +def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5, + param_group=True): + train_iter = paddle.io.DataLoader(paddle.vision.datasets.DatasetFolder( + os.path.join(data_dir, 'train'), transform=train_augs), + batch_size=batch_size, shuffle=True) + test_iter = paddle.io.DataLoader(paddle.vision.datasets.DatasetFolder( + os.path.join(data_dir, 'test'), transform=test_augs), + batch_size=batch_size) + devices = d2l.try_all_gpus() + loss = nn.CrossEntropyLoss(reduction="none") + if param_group: + params_1x = [param for name, param in net.named_parameters() + if name not in ["fc.weight", "fc.bias"]] + trainer = paddle.optimizer.SGD(learning_rate=learning_rate, parameters=[{'params': params_1x}, + {'params': net.fc.parameters(), + 'learning_rate': learning_rate * 10}], + weight_decay=0.001) + else: + trainer = paddle.optimizer.SGD(learning_rate=learning_rate, parameters=net.parameters(), + weight_decay=0.001) + d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) +``` + 我们[**使用较小的学习率**],通过*微调*预训练获得的模型参数。 ```{.python .input} @@ -264,7 +344,7 @@ train_fine_tuning(finetune_net, 0.01) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle train_fine_tuning(finetune_net, 5e-5) ``` @@ -284,11 +364,18 @@ scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2) train_fine_tuning(scratch_net, 5e-4, param_group=False) ``` +```{.python .input} +#@tab paddle +scratch_net = paddlevision.models.resnet18() +scratch_net.fc = nn.Linear(pretrained_net.fc.state_dict()['weight'].shape[0], 2) +train_fine_tuning(scratch_net, 5e-4, param_group=False) +``` + 意料之中,微调模型往往表现更好,因为它的初始参数值更有效。 ## 小结 -* 迁移学习将从源数据集中学到的知识“迁移”到目标数据集,微调是迁移学习的常见技巧。 +* 迁移学习将从源数据集中学到的知识*迁移*到目标数据集,微调是迁移学习的常见技巧。 * 除输出层外,目标模型从源模型中复制所有模型设计及其参数,并根据目标数据集对这些参数进行微调。但是,目标模型的输出层需要从头开始训练。 * 通常,微调参数使用较小的学习率,而从头开始训练输出层可以使用更大的学习率。 @@ -296,7 +383,7 @@ train_fine_tuning(scratch_net, 5e-4, param_group=False) 1. 继续提高`finetune_net`的学习率,模型的准确性如何变化? 2. 在比较实验中进一步调整`finetune_net`和`scratch_net`的超参数。它们的准确性还有不同吗? -3. 将输出层`finetune_net`之前的参数设置为源模型的参数,在训练期间不要更新它们。模型的准确性如何变化?你可以使用以下代码。 +3. 将输出层`finetune_net`之前的参数设置为源模型的参数,在训练期间不要更新它们。模型的准确性如何变化?提示:可以使用以下代码。 ```{.python .input} finetune_net.features.collect_params().setattr('grad_req', 'null') @@ -308,6 +395,12 @@ for param in finetune_net.parameters(): param.requires_grad = False ``` +```{.python .input} +#@tab paddle +for param in finetune_net.parameters(): + param.stop_gradient = True +``` + 4. 事实上,`ImageNet`数据集中有一个“热狗”类别。我们可以通过以下代码获取其输出层中的相应权重参数,但是我们怎样才能利用这个权重参数? ```{.python .input} @@ -323,6 +416,13 @@ hotdog_w = torch.split(weight.data, 1, dim=0)[934] hotdog_w.shape ``` +```{.python .input} +#@tab paddle +weight = pretrained_net.fc.weight +hotdog_w = paddle.split(weight.T, 1000, axis=0)[713] +hotdog_w.shape +``` + :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/2893) :end_tab: @@ -330,3 +430,7 @@ hotdog_w.shape :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2894) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11802) +:end_tab: diff --git a/chapter_computer-vision/image-augmentation.md b/chapter_computer-vision/image-augmentation.md index 56ef513a0..dc343c3ce 100644 --- a/chapter_computer-vision/image-augmentation.md +++ b/chapter_computer-vision/image-augmentation.md @@ -1,12 +1,12 @@ # 图像增广 :label:`sec_image_augmentation` -在 :numref:`sec_alexnet`中,我们提到过大型数据集是成功应用深度神经网络的先决条件。 + :numref:`sec_alexnet`提到过大型数据集是成功应用深度神经网络的先决条件。 图像增广在对训练图像进行一系列的随机变化之后,生成相似但不同的训练样本,从而扩大了训练集的规模。 此外,应用图像增广的原因是,随机改变训练样本可以减少模型对某些属性的依赖,从而提高模型的泛化能力。 例如,我们可以以不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,减少模型对于对象出现位置的依赖。 我们还可以调整亮度、颜色等因素来降低模型对颜色的敏感度。 -可以说,图像增广技术对于AlexNet的成功是必不可少的。在本节中,我们将讨论这项广泛应用于计算机视觉的技术。 +可以说,图像增广技术对于AlexNet的成功是必不可少的。本节将讨论这项广泛应用于计算机视觉的技术。 ```{.python .input} %matplotlib inline @@ -26,6 +26,17 @@ import torchvision from torch import nn ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.vision as paddlevision +from paddle import nn +``` + ## 常用的图像增广方法 在对常用图像增广方法的探索时,我们将使用下面这个尺寸为$400\times 500$的图像作为示例。 @@ -37,7 +48,7 @@ d2l.plt.imshow(img.asnumpy()); ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle d2l.set_figsize() img = d2l.Image.open('../img/cat1.jpg') d2l.plt.imshow(img); @@ -67,6 +78,11 @@ apply(img, gluon.data.vision.transforms.RandomFlipLeftRight()) apply(img, torchvision.transforms.RandomHorizontalFlip()) ``` +```{.python .input} +#@tab paddle +apply(img, paddlevision.transforms.RandomHorizontalFlip()) +``` + [**上下翻转图像**]不如左右图像翻转那样常用。但是,至少对于这个示例图像,上下翻转不会妨碍识别。接下来,我们创建一个`RandomFlipTopBottom`实例,使图像各有50%的几率向上或向下翻转。 ```{.python .input} @@ -78,12 +94,17 @@ apply(img, gluon.data.vision.transforms.RandomFlipTopBottom()) apply(img, torchvision.transforms.RandomVerticalFlip()) ``` +```{.python .input} +#@tab paddle +apply(img, paddlevision.transforms.RandomVerticalFlip()) +``` + 在我们使用的示例图像中,猫位于图像的中间,但并非所有图像都是这样。 在 :numref:`sec_pooling`中,我们解释了汇聚层可以降低卷积层对目标位置的敏感性。 另外,我们可以通过对图像进行随机裁剪,使物体以不同的比例出现在图像的不同位置。 这也可以降低模型对目标位置的敏感性。 -在下面的代码中,我们[**随机裁剪**]一个面积为原始面积10%到100%的区域,该区域的宽高比从0.5到2之间随机取值。 +下面的代码将[**随机裁剪**]一个面积为原始面积10%到100%的区域,该区域的宽高比从0.5~2之间随机取值。 然后,区域的宽度和高度都被缩放到200像素。 在本节中(除非另有说明),$a$和$b$之间的随机数指的是在区间$[a, b]$中通过均匀采样获得的连续值。 @@ -100,6 +121,13 @@ shape_aug = torchvision.transforms.RandomResizedCrop( apply(img, shape_aug) ``` +```{.python .input} +#@tab paddle +shape_aug = paddlevision.transforms.RandomResizedCrop( + (200, 200), scale=(0.1, 1), ratio=(0.5, 2)) +apply(img, shape_aug) +``` + ### 改变颜色 另一种增广方法是改变颜色。 @@ -116,6 +144,12 @@ apply(img, torchvision.transforms.ColorJitter( brightness=0.5, contrast=0, saturation=0, hue=0)) ``` +```{.python .input} +#@tab paddle +apply(img, paddlevision.transforms.ColorJitter( + brightness=0.5, contrast=0, saturation=0, hue=0)) +``` + 同样,我们可以[**随机更改图像的色调**]。 ```{.python .input} @@ -128,6 +162,12 @@ apply(img, torchvision.transforms.ColorJitter( brightness=0, contrast=0, saturation=0, hue=0.5)) ``` +```{.python .input} +#@tab paddle +apply(img, paddlevision.transforms.ColorJitter( + brightness=0, contrast=0, saturation=0, hue=0.5)) +``` + 我们还可以创建一个`RandomColorJitter`实例,并设置如何同时[**随机更改图像的亮度(`brightness`)、对比度(`contrast`)、饱和度(`saturation`)和色调(`hue`)**]。 ```{.python .input} @@ -143,6 +183,13 @@ color_aug = torchvision.transforms.ColorJitter( apply(img, color_aug) ``` +```{.python .input} +#@tab paddle +color_aug = paddlevision.transforms.ColorJitter( + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5) +apply(img, color_aug) +``` + ### [**结合多种图像增广方法**] 在实践中,我们将结合多种图像增广方法。比如,我们可以通过使用一个`Compose`实例来综合上面定义的不同的图像增广方法,并将它们应用到每个图像。 @@ -160,6 +207,13 @@ augs = torchvision.transforms.Compose([ apply(img, augs) ``` +```{.python .input} +#@tab paddle +augs = paddlevision.transforms.Compose([ + paddle.vision.transforms.RandomHorizontalFlip(), color_aug, shape_aug]) +apply(img, augs) +``` + ## [**使用图像增广进行训练**] 让我们使用图像增广来训练模型。 @@ -179,9 +233,16 @@ all_images = torchvision.datasets.CIFAR10(train=True, root="../data", d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8); ``` +```{.python .input} +#@tab paddle +all_images = paddlevision.datasets.Cifar10(mode='train' , download=True) +print(len(all_images)) +d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8); +``` + 为了在预测过程中得到确切的结果,我们通常对训练样本只进行图像增广,且在预测过程中不使用随机操作的图像增广。 在这里,我们[**只使用最简单的随机左右翻转**]。 -此外,我们使用`ToTensor`实例将一批图像转换为深度学习框架所要求的格式,即形状为(批量大小,通道数,高度,宽度)的32位浮点数,取值范围为0到1。 +此外,我们使用`ToTensor`实例将一批图像转换为深度学习框架所要求的格式,即形状为(批量大小,通道数,高度,宽度)的32位浮点数,取值范围为0~1。 ```{.python .input} train_augs = gluon.data.vision.transforms.Compose([ @@ -202,6 +263,16 @@ test_augs = torchvision.transforms.Compose([ torchvision.transforms.ToTensor()]) ``` +```{.python .input} +#@tab paddle +train_augs = paddlevision.transforms.Compose([ + paddlevision.transforms.RandomHorizontalFlip(), + paddlevision.transforms.ToTensor()]) + +test_augs = paddlevision.transforms.Compose([ + paddlevision.transforms.ToTensor()]) +``` + :begin_tab:`mxnet` 接下来,我们定义了一个辅助函数,以便于读取图像和应用图像增广。Gluon数据集提供的`transform_first`函数将图像增广应用于每个训练样本的第一个元素(由图像和标签组成),即应用在图像上。有关`DataLoader`的详细介绍,请参阅 :numref:`sec_fashion_mnist`。 :end_tab: @@ -228,6 +299,16 @@ def load_cifar10(is_train, augs, batch_size): return dataloader ``` +```{.python .input} +#@tab paddle +def load_cifar10(is_train, augs, batch_size): + dataset = paddlevision.datasets.Cifar10(mode="train", + transform=augs, download=True) + dataloader = paddle.io.DataLoader(dataset, batch_size=batch_size, + num_workers=d2l.get_dataloader_workers(), shuffle=is_train) + return dataloader +``` + ### 多GPU训练 我们在CIFAR-10数据集上训练 :numref:`sec_resnet`中的ResNet-18模型。 @@ -260,7 +341,7 @@ def train_batch_ch13(net, features, labels, loss, trainer, devices, def train_batch_ch13(net, X, y, loss, trainer, devices): """用多GPU进行小批量训练""" if isinstance(X, list): - # 微调BERT中所需(稍后讨论) + # 微调BERT中所需 X = [x.to(devices[0]) for x in X] else: X = X.to(devices[0]) @@ -276,6 +357,30 @@ def train_batch_ch13(net, X, y, loss, trainer, devices): return train_loss_sum, train_acc_sum ``` +```{.python .input} +#@tab paddle +#@save +def train_batch_ch13(net, X, y, loss, trainer, devices): + """用多GPU进行小批量训练 + 飞桨不支持在notebook上进行多GPU训练 + Defined in :numref:`sec_image_augmentation`""" + if isinstance(X, list): + # 微调BERT中所需(稍后讨论) + X = [paddle.to_tensor(x, place=devices[0]) for x in X] + else: + X = paddle.to_tensor(X, place=devices[0]) + y = paddle.to_tensor(y, place=devices[0]) + net.train() + trainer.clear_grad() + pred = net(X) + l = loss(pred, y) + l.sum().backward() + trainer.step() + train_loss_sum = l.sum() + train_acc_sum = d2l.accuracy(pred, y) + return train_loss_sum, train_acc_sum +``` + ```{.python .input} #@save def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, @@ -336,6 +441,38 @@ def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, f'{str(devices)}') ``` +```{.python .input} +#@tab paddle +#@save +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus()): + """用多GPU进行模型训练 + Defined in :numref:`sec_image_augmentation`""" + timer, num_batches = d2l.Timer(), len(train_iter) + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['train loss', 'train acc', 'test acc']) + net = paddle.DataParallel(net) + for epoch in range(num_epochs): + # 4个维度:储存训练损失,训练准确度,实例数,特点数 + metric = d2l.Accumulator(4) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = train_batch_ch13( + net, features, labels, loss, trainer, devices) + metric.add(l, acc, labels.shape[0], labels.numel()) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[3], + None)) + test_acc = d2l.evaluate_accuracy_gpu(net, test_iter) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {metric[0] / metric[2]:.3f}, train acc ' + f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' + f'{str(devices)}') +``` + 现在,我们可以[**定义`train_with_data_aug`函数,使用图像增广来训练模型**]。该函数获取所有的GPU,并使用Adam作为训练的优化算法,将图像增广应用于训练集,最后调用刚刚定义的用于训练和评估模型的`train_ch13`函数。 ```{.python .input} @@ -369,6 +506,24 @@ def train_with_data_aug(train_augs, test_augs, net, lr=0.001): train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices) ``` +```{.python .input} +#@tab paddle +batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3) + +def init_weights(m): + if type(m) in [nn.Linear, nn.Conv2D]: + nn.initializer.XavierUniform(m.weight) + +net.apply(init_weights) + +def train_with_data_aug(train_augs, test_augs, net, lr=0.001): + train_iter = load_cifar10(True, train_augs, batch_size) + test_iter = load_cifar10(False, test_augs, batch_size) + loss = nn.CrossEntropyLoss(reduction="none") + trainer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) + train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices[:1]) +``` + 让我们使用基于随机左右翻转的图像增广来[**训练模型**]。 ```{.python .input} @@ -395,3 +550,7 @@ train_with_data_aug(train_augs, test_augs, net) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2829) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11801) +:end_tab: diff --git a/chapter_computer-vision/kaggle-cifar10.md b/chapter_computer-vision/kaggle-cifar10.md index 7b25ff207..87e89f4ed 100644 --- a/chapter_computer-vision/kaggle-cifar10.md +++ b/chapter_computer-vision/kaggle-cifar10.md @@ -3,13 +3,13 @@ 之前几节中,我们一直在使用深度学习框架的高级API直接获取张量格式的图像数据集。 但是在实践中,图像数据集通常以图像文件的形式出现。 -在本节中,我们将从原始图像文件开始,然后逐步组织、读取并将它们转换为张量格式。 +本节将从原始图像文件开始,然后逐步组织、读取并将它们转换为张量格式。 我们在 :numref:`sec_image_augmentation`中对CIFAR-10数据集做了一个实验。CIFAR-10是计算机视觉领域中的一个重要的数据集。 -在本节中,我们将运用我们在前几节中学到的知识来参加CIFAR-10图像分类问题的Kaggle竞赛,(**比赛的网址是https://www.kaggle.com/c/cifar-10**)。 +本节将运用我们在前几节中学到的知识来参加CIFAR-10图像分类问题的Kaggle竞赛,(**比赛的网址是https://www.kaggle.com/c/cifar-10**)。 :numref:`fig_kaggle_cifar10`显示了竞赛网站页面上的信息。 -为了能提交结果,你需要首先注册Kaggle账户。 +为了能提交结果,首先需要注册一个Kaggle账户。 ![CIFAR-10 图像分类竞赛页面上的信息。竞赛用的数据集可通过点击“Data”选项卡获取。](../img/kaggle-cifar10.png) :width:`600px` @@ -43,6 +43,21 @@ import pandas as pd import shutil ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import collections +import math +import os +import pandas as pd +import shutil +import paddle +from paddle import nn +import paddle.vision as paddlevision +``` + ## 获取并组织数据集 比赛数据集分为训练集和测试集,其中训练集包含50000张、测试集包含300000张图像。 @@ -54,7 +69,7 @@ import shutil ### 下载数据集 登录Kaggle后,我们可以点击 :numref:`fig_kaggle_cifar10`中显示的CIFAR-10图像分类竞赛网页上的“Data”选项卡,然后单击“Download All”按钮下载数据集。 -在`../data`中解压下载的文件并在其中解压缩`train.7z`和`test.7z`后,你将在以下路径中找到整个数据集: +在`../data`中解压下载的文件并在其中解压缩`train.7z`和`test.7z`后,在以下路径中可以找到整个数据集: * `../data/cifar-10/train/[1-50000].png` * `../data/cifar-10/test/[1-300000].png` @@ -65,7 +80,7 @@ import shutil `sample_submission.csv`是提交文件的范例。 为了便于入门,[**我们提供包含前1000个训练图像和5个随机测试图像的数据集的小规模样本**]。 -要使用Kaggle竞赛的完整数据集,你需要将以下`demo`变量设置为`False`。 +要使用Kaggle竞赛的完整数据集,需要将以下`demo`变量设置为`False`。 ```{.python .input} #@tab all @@ -73,7 +88,7 @@ import shutil d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip', '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') -# 如果你使用完整的Kaggle竞赛的数据集,设置demo为False +# 如果使用完整的Kaggle竞赛的数据集,设置demo为False demo = True if demo: @@ -186,7 +201,7 @@ transform_train = gluon.data.vision.transforms.Compose([ # 在高度和宽度上将图像放大到40像素的正方形 gluon.data.vision.transforms.Resize(40), # 随机裁剪出一个高度和宽度均为40像素的正方形图像, - # 生成一个面积为原始图像面积0.64到1倍的小正方形, + # 生成一个面积为原始图像面积0.64~1倍的小正方形, # 然后将其缩放为高度和宽度均为32像素的正方形 gluon.data.vision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), ratio=(1.0, 1.0)), @@ -203,7 +218,7 @@ transform_train = torchvision.transforms.Compose([ # 在高度和宽度上将图像放大到40像素的正方形 torchvision.transforms.Resize(40), # 随机裁剪出一个高度和宽度均为40像素的正方形图像, - # 生成一个面积为原始图像面积0.64到1倍的小正方形, + # 生成一个面积为原始图像面积0.64~1倍的小正方形, # 然后将其缩放为高度和宽度均为32像素的正方形 torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), ratio=(1.0, 1.0)), @@ -214,6 +229,23 @@ transform_train = torchvision.transforms.Compose([ [0.2023, 0.1994, 0.2010])]) ``` +```{.python .input} +#@tab paddle +transform_train = paddlevision.transforms.Compose([ + # 在高度和宽度上将图像放大到40像素的正方形 + paddlevision.transforms.Resize(40), + # 随机裁剪出一个高度和宽度均为40像素的正方形图像, + # 生成一个面积为原始图像面积0.64到1倍的小正方形, + # 然后将其缩放为高度和宽度均为32像素的正方形 + paddlevision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), + ratio=(1.0, 1.0)), + paddlevision.transforms.RandomHorizontalFlip(), + paddlevision.transforms.ToTensor(), + # 标准化图像的每个通道 + paddlevision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + 在测试期间,我们只对图像执行标准化,以消除评估结果中的随机性。 ```{.python .input} @@ -231,6 +263,14 @@ transform_test = torchvision.transforms.Compose([ [0.2023, 0.1994, 0.2010])]) ``` +```{.python .input} +#@tab paddle +transform_test = paddlevision.transforms.Compose([ + paddlevision.transforms.ToTensor(), + paddlevision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + ## 读取数据集 接下来,我们[**读取由原始图像组成的数据集**],每个样本都包括一张图片和一个标签。 @@ -253,6 +293,17 @@ valid_ds, test_ds = [torchvision.datasets.ImageFolder( transform=transform_test) for folder in ['valid', 'test']] ``` +```{.python .input} +#@tab paddle +train_ds, train_valid_ds = [paddlevision.datasets.DatasetFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_train) for folder in ['train', 'train_valid']] + +valid_ds, test_ds = [paddlevision.datasets.DatasetFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_test) for folder in ['valid', 'test']] +``` + 在训练期间,我们需要[**指定上面定义的所有图像增广操作**]。 当验证集在超参数调整过程中用于模型评估时,不应引入图像增广的随机性。 在最终预测之前,我们根据训练集和验证集组合而成的训练模型进行训练,以充分利用所有标记的数据。 @@ -284,6 +335,19 @@ test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False, drop_last=False) ``` +```{.python .input} +#@tab paddle +train_iter, train_valid_iter = [paddle.io.DataLoader( + dataset, batch_size=batch_size, shuffle=True, drop_last=True) + for dataset in (train_ds, train_valid_ds)] + +valid_iter = paddle.io.DataLoader(valid_ds, batch_size=batch_size, shuffle=False, + drop_last=True) + +test_iter = paddle.io.DataLoader(test_ds, batch_size=batch_size, shuffle=False, + drop_last=False) +``` + ## 定义[**模型**] :begin_tab:`mxnet` @@ -348,6 +412,10 @@ def resnet18(num_classes): 我们定义了 :numref:`sec_resnet`中描述的Resnet-18模型。 :end_tab: +:begin_tab:`paddle` +我们定义了 :numref:`sec_resnet`中描述的Resnet-18模型。 +:end_tab: + ```{.python .input} def get_net(devices): num_classes = 10 @@ -368,6 +436,16 @@ def get_net(): loss = nn.CrossEntropyLoss(reduction="none") ``` +```{.python .input} +#@tab paddle +def get_net(): + num_classes = 10 + net = d2l.resnet18(num_classes, 3) + return net + +loss = nn.CrossEntropyLoss(reduction="none") +``` + ## 定义[**训练函数**] 我们将根据模型在验证集上的表现来选择模型并调整超参数。 @@ -450,6 +528,45 @@ def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, f' examples/sec on {str(devices)}') ``` +```{.python .input} +#@tab paddle +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + scheduler = paddle.optimizer.lr.StepDecay(lr, lr_period, lr_decay) + trainer = paddle.optimizer.Momentum(learning_rate=scheduler, momentum=0.9, parameters=net.parameters(), + weight_decay=wd) + num_batches, timer = len(train_iter), d2l.Timer() + legend = ['train loss', 'train acc'] + if valid_iter is not None: + legend.append('valid acc') + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=legend) + net = paddle.DataParallel(net) + for epoch in range(num_epochs): + net.train() + metric = d2l.Accumulator(3) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = d2l.train_batch_ch13(net, features, labels, + loss, trainer, devices) + metric.add(l, acc, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[2], + None)) + if valid_iter is not None: + valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter) + animator.add(epoch + 1, (None, None, valid_acc)) + scheduler.step() + measures = (f'train loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}') + if valid_iter is not None: + measures += f', valid acc {valid_acc:.3f}' + print(measures + f'\n{metric[2] * num_epochs / timer.sum():.1f}' + f' examples/sec on {str(devices)}') +``` + ## [**训练和验证模型**] 现在,我们可以训练和验证模型了,而以下所有超参数都可以调整。 @@ -472,6 +589,14 @@ train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay) ``` +```{.python .input} +#@tab paddle +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 20, 2e-4, 5e-4 +lr_period, lr_decay, net = 4, 0.9, get_net() +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + ## 在 Kaggle 上[**对测试集进行分类并提交结果**] 在获得具有超参数的满意的模型后,我们使用所有标记的数据(包括验证集)来重新训练模型并对测试集进行分类。 @@ -508,6 +633,22 @@ df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x]) df.to_csv('submission.csv', index=False) ``` +```{.python .input} +#@tab paddle +net, preds = get_net(), [] +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +for X, _ in test_iter: + y_hat = net(X) + preds.extend(y_hat.argmax(axis=1).astype(paddle.int32).numpy()) +sorted_ids = list(range(1, len(test_ds) + 1)) +sorted_ids.sort(key=lambda x: str(x)) +df = pd.DataFrame({'id': sorted_ids, 'label': preds}) +df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x]) +df.to_csv('submission.csv', index=False) +``` + 向Kaggle提交结果的方法与 :numref:`sec_kaggle_house`中的方法类似,上面的代码将生成一个 `submission.csv`文件,其格式符合Kaggle竞赛的要求。 @@ -523,10 +664,14 @@ df.to_csv('submission.csv', index=False) * 我们可以在图像分类竞赛中使用卷积神经网络和图像增广。 :end_tab: +:begin_tab:`paddle` +* 我们可以在图像分类竞赛中使用卷积神经网络和图像增广。 +:end_tab: + ## 练习 -1. 在这场Kaggle竞赛中使用完整的CIFAR-10数据集。将超参数设为`batch_size = 128`,`num_epochs = 100`,`lr = 0.1`,`lr_period = 50`,`lr_decay = 0.1`。看看你在这场比赛中能达到什么准确度和排名。或者你能进一步改进吗? -1. 不使用图像增广时,你能获得怎样的准确度? +1. 在这场Kaggle竞赛中使用完整的CIFAR-10数据集。将超参数设为`batch_size = 128`,`num_epochs = 100`,`lr = 0.1`,`lr_period = 50`,`lr_decay = 0.1`。看看在这场比赛中能达到什么准确度和排名。能进一步改进吗? +1. 不使用图像增广时,能获得怎样的准确度? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/2830) @@ -535,3 +680,7 @@ df.to_csv('submission.csv', index=False) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2831) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11814) +:end_tab: diff --git a/chapter_computer-vision/kaggle-dog.md b/chapter_computer-vision/kaggle-dog.md index eb1e2f0d9..7be7b7848 100644 --- a/chapter_computer-vision/kaggle-dog.md +++ b/chapter_computer-vision/kaggle-dog.md @@ -3,13 +3,13 @@ 本节我们将在Kaggle上实战狗品种识别问题。 本次(**比赛网址是https://www.kaggle.com/c/dog-breed-identification**)。 :numref:`fig_kaggle_dog`显示了鉴定比赛网页上的信息。 -你需要一个Kaggle账户才能提交结果。 +需要一个Kaggle账户才能提交结果。 在这场比赛中,我们将识别120类不同品种的狗。 这个数据集实际上是著名的ImageNet的数据集子集。与 :numref:`sec_kaggle_cifar10`中CIFAR-10数据集中的图像不同, ImageNet数据集中的图像更高更宽,且尺寸不一。 -![狗的品种鉴定比赛网站,你可以通过单击“数据”选项卡来获得比赛数据集。](../img/kaggle-dog.jpg) +![狗的品种鉴定比赛网站,可以通过单击“数据”选项卡来获得比赛数据集。](../img/kaggle-dog.jpg) :width:`400px` :label:`fig_kaggle_dog` @@ -31,6 +31,17 @@ from torch import nn import os ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.vision as paddlevision +from paddle import nn +import os +``` + ## 获取和整理数据集 比赛数据集分为训练集和测试集,分别包含RGB(彩色)通道的10222张、10357张JPEG图像。 @@ -38,7 +49,7 @@ import os ### 下载数据集 -登录Kaggle后,你可以点击 :numref:`fig_kaggle_dog`中显示的竞赛网页上的“数据”选项卡,然后点击“全部下载”按钮下载数据集。在`../data`中解压下载的文件后,你将在以下路径中找到整个数据集: +登录Kaggle后,可以点击 :numref:`fig_kaggle_dog`中显示的竞争网页上的“数据”选项卡,然后点击“全部下载”按钮下载数据集。在`../data`中解压下载的文件后,将在以下路径中找到整个数据集: * ../data/dog-breed-identification/labels.csv * ../data/dog-breed-identification/sample_submission.csv @@ -46,9 +57,10 @@ import os * ../data/dog-breed-identification/test -你可能已经注意到,上述结构与 :numref:`sec_kaggle_cifar10`的CIFAR-10竞赛类似,其中文件夹`train/`和`test/`分别包含训练和测试狗图像,`labels.csv`包含训练图像的标签。 +上述结构与 :numref:`sec_kaggle_cifar10`的CIFAR-10类似,其中文件夹`train/`和`test/`分别包含训练和测试狗图像,`labels.csv`包含训练图像的标签。 + 同样,为了便于入门,[**我们提供完整数据集的小规模样本**]:`train_valid_test_tiny.zip`。 -如果你要在Kaggle比赛中使用完整的数据集,则需要将下面的`demo`变量更改为`False`。 +如果要在Kaggle比赛中使用完整的数据集,则需要将下面的`demo`变量更改为`False`。 ```{.python .input} #@tab all @@ -56,7 +68,7 @@ import os d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip', '0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d') -# 如果你使用Kaggle比赛的完整数据集,请将下面的变量更改为False +# 如果使用Kaggle比赛的完整数据集,请将下面的变量更改为False demo = True if demo: data_dir = d2l.download_extract('dog_tiny') @@ -90,7 +102,7 @@ reorg_dog_data(data_dir, valid_ratio) ```{.python .input} transform_train = gluon.data.vision.transforms.Compose([ - # 随机裁剪图像,所得图像为原始面积的0.08到1之间,高宽比在3/4和4/3之间。 + # 随机裁剪图像,所得图像为原始面积的0.08~1之间,高宽比在3/4和4/3之间。 # 然后,缩放图像以创建224x224的新图像 gluon.data.vision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), ratio=(3.0/4.0, 4.0/3.0)), @@ -110,7 +122,7 @@ transform_train = gluon.data.vision.transforms.Compose([ ```{.python .input} #@tab pytorch transform_train = torchvision.transforms.Compose([ - # 随机裁剪图像,所得图像为原始面积的0.08到1之间,高宽比在3/4和4/3之间。 + # 随机裁剪图像,所得图像为原始面积的0.08~1之间,高宽比在3/4和4/3之间。 # 然后,缩放图像以创建224x224的新图像 torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), ratio=(3.0/4.0, 4.0/3.0)), @@ -126,6 +138,25 @@ transform_train = torchvision.transforms.Compose([ [0.229, 0.224, 0.225])]) ``` +```{.python .input} +#@tab paddle +transform_train = paddlevision.transforms.Compose([ + # 随机裁剪图像,所得图像为原始面积的0.08到1之间,高宽比在3/4和4/3之间。 + # 然后,缩放图像以创建224x224的新图像 + paddlevision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), + ratio=(3.0/4.0, 4.0/3.0)), + paddlevision.transforms.RandomHorizontalFlip(), + # 随机更改亮度,对比度和饱和度 + paddlevision.transforms.ColorJitter(brightness=0.4, + contrast=0.4, + saturation=0.4), + # 添加随机噪声 + paddlevision.transforms.ToTensor(), + # 标准化图像的每个通道 + paddlevision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + 测试时,我们只使用确定性的图像预处理操作。 ```{.python .input} @@ -149,6 +180,17 @@ transform_test = torchvision.transforms.Compose([ [0.229, 0.224, 0.225])]) ``` +```{.python .input} +#@tab paddle +transform_test = paddlevision.transforms.Compose([ + paddlevision.transforms.Resize(256), + # 从图像中心裁切224x224大小的图片 + paddlevision.transforms.CenterCrop(224), + paddlevision.transforms.ToTensor(), + paddlevision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + ## [**读取数据集**] 与 :numref:`sec_kaggle_cifar10`一样,我们可以读取整理后的含原始图像文件的数据集。 @@ -171,6 +213,17 @@ valid_ds, test_ds = [torchvision.datasets.ImageFolder( transform=transform_test) for folder in ['valid', 'test']] ``` +```{.python .input} +#@tab paddle +train_ds, train_valid_ds = [paddlevision.datasets.DatasetFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_train) for folder in ['train', 'train_valid']] + +valid_ds, test_ds = [paddlevision.datasets.DatasetFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_test) for folder in ['valid', 'test']] +``` + 下面我们创建数据加载器实例的方式与 :numref:`sec_kaggle_cifar10`相同。 ```{.python .input} @@ -200,6 +253,19 @@ test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False, drop_last=False) ``` +```{.python .input} +#@tab paddle +train_iter, train_valid_iter = [paddle.io.DataLoader( + dataset, batch_size=batch_size, shuffle=True, drop_last=True) + for dataset in (train_ds, train_valid_ds)] + +valid_iter = paddle.io.DataLoader(valid_ds, batch_size=batch_size, shuffle=False, + drop_last=True) + +test_iter = paddle.io.DataLoader(test_ds, batch_size=batch_size, shuffle=False, + drop_last=False) +``` + ## [**微调预训练模型**] 同样,本次比赛的数据集是ImageNet数据集的子集。 @@ -244,6 +310,21 @@ def get_net(devices): return finetune_net ``` +```{.python .input} +#@tab paddle +def get_net(devices): + finetune_net = nn.Sequential() + finetune_net.features = paddlevision.models.resnet34(pretrained=True) + # 定义一个新的输出网络,共有120个输出类别 + finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256), + nn.ReLU(), + nn.Linear(256, 120)) + # 冻结参数 + for param in finetune_net.features.parameters(): + param.stop_gradient = True + return finetune_net +``` + 在[**计算损失**]之前,我们首先获取预训练模型的输出层的输入,即提取的特征。 然后我们使用此特征作为我们小型自定义输出网络的输入来计算损失。 @@ -278,6 +359,20 @@ def evaluate_loss(data_iter, net, devices): return (l_sum / n).to('cpu') ``` +```{.python .input} +#@tab paddle +loss = nn.CrossEntropyLoss(reduction='none') + +def evaluate_loss(data_iter, net, devices): + l_sum, n = 0.0, 0 + for features, labels in data_iter: + outputs = net(features) + l = loss(outputs, labels) + l_sum += l.sum() + n += labels.numel() + return l_sum / n +``` + ## 定义[**训练函数**] 我们将根据模型在验证集上的表现选择模型并调整超参数。 @@ -368,6 +463,48 @@ def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, f' examples/sec on {str(devices)}') ``` +```{.python .input} +#@tab paddle +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + # 只训练小型自定义输出网络 + net = paddle.DataParallel(net) + scheduler = paddle.optimizer.lr.StepDecay(lr, lr_period, lr_decay) + trainer = paddle.optimizer.Momentum(learning_rate=scheduler, + parameters=(param for param in net.parameters() if not param.stop_gradient), + momentum=0.9, + weight_decay=wd) + num_batches, timer = len(train_iter), d2l.Timer() + legend = ['train loss'] + if valid_iter is not None: + legend.append('valid loss') + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=legend) + for epoch in range(num_epochs): + metric = d2l.Accumulator(2) + for i, (features, labels) in enumerate(train_iter): + timer.start() + trainer.clear_grad() + output = net(features) + l = loss(output, labels).sum() + l.backward() + trainer.step() + metric.add(l, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[1], None)) + measures = f'train loss {metric[0] / metric[1]:.3f}' + if valid_iter is not None: + valid_loss = evaluate_loss(valid_iter, net, devices) + animator.add(epoch + 1, (None, valid_loss.detach())) + scheduler.step() + if valid_iter is not None: + measures += f', valid loss {float(valid_loss):.3f}' + print(measures + f'\n{metric[1] * num_epochs / timer.sum():.1f}' + f' examples/sec on {str(devices)}') +``` + ## [**训练和验证模型**] 现在我们可以训练和验证模型了,以下超参数都是可调的。 @@ -391,6 +528,14 @@ train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay) ``` +```{.python .input} +#@tab paddle +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 10, 1e-4, 1e-4 +lr_period, lr_decay, net = 2, 0.9, get_net(devices) +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + ## [**对测试集分类**]并在Kaggle提交结果 与 :numref:`sec_kaggle_cifar10`中的最后一步类似,最终所有标记的数据(包括验证集)都用于训练模型和对测试集进行分类。 @@ -424,7 +569,7 @@ train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, preds = [] for data, label in test_iter: - output = torch.nn.functional.softmax(net(data.to(devices[0])), dim=0) + output = torch.nn.functional.softmax(net(data.to(devices[0])), dim=1) preds.extend(output.cpu().detach().numpy()) ids = sorted(os.listdir( os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))) @@ -435,6 +580,25 @@ with open('submission.csv', 'w') as f: [str(num) for num in output]) + '\n') ``` +```{.python .input} +#@tab paddle +net = get_net(devices) +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +preds = [] +for data, label in test_iter: + output = paddle.nn.functional.softmax(net(data), axis=0) + preds.extend(output.detach().numpy()) +ids = sorted(os.listdir( + os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))) +with open('submission.csv', 'w') as f: + f.write('id,' + ','.join(train_valid_ds.classes) + '\n') + for i, output in zip(ids, preds): + f.write(i.split('.')[0] + ',' + ','.join( + [str(num) for num in output]) + '\n') +``` + 上面的代码将生成一个`submission.csv`文件,以 :numref:`sec_kaggle_house`中描述的方式提在Kaggle上提交。 ## 小结 @@ -444,8 +608,8 @@ with open('submission.csv', 'w') as f: ## 练习 -1. 试试使用完整Kaggle比赛数据集,增加`batch_size`(批量大小)和`num_epochs`(迭代轮数),或者设计其它超参数为`lr = 0.01`,`lr_period = 10`,和`lr_decay = 0.1`时,你能取得什么结果? -1. 如果你使用更深的预训练模型,会得到更好的结果吗?如何调整超参数?能进一步改善结果吗? +1. 试试使用完整Kaggle比赛数据集,增加`batch_size`(批量大小)和`num_epochs`(迭代轮数),或者设计其它超参数为`lr = 0.01`,`lr_period = 10`,和`lr_decay = 0.1`时,能取得什么结果? +1. 如果使用更深的预训练模型,会得到更好的结果吗?如何调整超参数?能进一步改善结果吗? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/2832) @@ -454,3 +618,7 @@ with open('submission.csv', 'w') as f: :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2833) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11815) +:end_tab: diff --git a/chapter_computer-vision/multiscale-object-detection.md b/chapter_computer-vision/multiscale-object-detection.md index 3ef5e2973..775f3161d 100644 --- a/chapter_computer-vision/multiscale-object-detection.md +++ b/chapter_computer-vision/multiscale-object-detection.md @@ -9,7 +9,7 @@ ## 多尺度锚框 :label:`subsec_multiscale-anchor-boxes` -你可能会意识到,减少图像上的锚框数量并不困难。 +减少图像上的锚框数量并不困难。 比如,我们可以在输入图像中均匀采样一小部分像素,并以它们为中心生成锚框。 此外,在不同尺度下,我们可以生成不同数量和不同大小的锚框。 直观地说,比起较大的目标,较小的目标在图像上出现的可能性更多样。 @@ -42,6 +42,19 @@ h, w = img.shape[:2] h, w ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle + +img = d2l.plt.imread('../img/catdog.jpg') +h, w = img.shape[:2] +h, w +``` + 回想一下,在 :numref:`sec_conv_layer`中,我们将卷积图层的二维数组输出称为特征图。 通过定义特征图的形状,我们可以确定任何图像上均匀采样锚框的中心。 @@ -76,6 +89,18 @@ def display_anchors(fmap_w, fmap_h, s): anchors[0] * bbox_scale) ``` +```{.python .input} +#@tab paddle +def display_anchors(fmap_w, fmap_h, s): + d2l.set_figsize() + # 前两个维度上的值不影响输出 + fmap = paddle.zeros(shape=[1, 10, fmap_h, fmap_w]) + anchors = d2l.multibox_prior(fmap, sizes=s, ratios=[1, 2, 0.5]) + bbox_scale = paddle.to_tensor((w, h, w, h)) + d2l.show_bboxes(d2l.plt.imshow(img).axes, + anchors[0] * bbox_scale) +``` + 首先,让我们考虑[**探测小目标**]。 为了在显示时更容易分辨,在这里具有不同中心的锚框不会重叠: 锚框的尺度设置为0.15,特征图的高度和宽度设置为4。 @@ -138,7 +163,7 @@ display_anchors(fmap_w=1, fmap_h=1, s=[0.8]) 1. 根据我们在 :numref:`sec_alexnet`中的讨论,深度神经网络学习图像特征级别抽象层次,随网络深度的增加而升级。在多尺度目标检测中,不同尺度的特征映射是否对应于不同的抽象层次?为什么? 1. 在 :numref:`subsec_multiscale-anchor-boxes`中的实验里的第一个尺度(`fmap_w=4, fmap_h=4`)下,生成可能重叠的均匀分布的锚框。 -1. 给定形状为$1 \times c \times h \times w$的特征图变量,其中$c$、$h$和$w$分别是特征图的通道数、高度和宽度。你怎样才能将这个变量转换为锚框类别和偏移量?输出的形状是什么? +1. 给定形状为$1 \times c \times h \times w$的特征图变量,其中$c$、$h$和$w$分别是特征图的通道数、高度和宽度。怎样才能将这个变量转换为锚框类别和偏移量?输出的形状是什么? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/2947) @@ -147,3 +172,7 @@ display_anchors(fmap_w=1, fmap_h=1, s=[0.8]) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2948) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11805) +:end_tab: diff --git a/chapter_computer-vision/neural-style.md b/chapter_computer-vision/neural-style.md index 3a2931d2d..dfd8353c6 100644 --- a/chapter_computer-vision/neural-style.md +++ b/chapter_computer-vision/neural-style.md @@ -1,8 +1,8 @@ # 风格迁移 -如果你是一位摄影爱好者,你也许接触过滤波器。它能改变照片的颜色风格,从而使风景照更加锐利或者令人像更加美白。但一个滤波器通常只能改变照片的某个方面。如果要照片达到理想中的风格,你可能需要尝试大量不同的组合。这个过程的复杂程度不亚于模型调参。 +摄影爱好者也许接触过滤波器。它能改变照片的颜色风格,从而使风景照更加锐利或者令人像更加美白。但一个滤波器通常只能改变照片的某个方面。如果要照片达到理想中的风格,可能需要尝试大量不同的组合。这个过程的复杂程度不亚于模型调参。 -在本节中,我们将介绍如何使用卷积神经网络,自动将一个图像中的风格应用在另一图像之上,即*风格迁移*(style transfer) :cite:`Gatys.Ecker.Bethge.2016`。 +本节将介绍如何使用卷积神经网络,自动将一个图像中的风格应用在另一图像之上,即*风格迁移*(style transfer) :cite:`Gatys.Ecker.Bethge.2016`。 这里我们需要两张输入图像:一张是*内容图像*,另一张是*风格图像*。 我们将使用神经网络修改内容图像,使其在风格上接近风格图像。 例如, :numref:`fig_style_transfer`中的内容图像为本书作者在西雅图郊区的雷尼尔山国家公园拍摄的风景照,而风格图像则是一幅主题为秋天橡树的油画。 @@ -25,9 +25,11 @@ 接下来,我们通过前向传播(实线箭头方向)计算风格迁移的损失函数,并通过反向传播(虚线箭头方向)迭代模型参数,即不断更新合成图像。 风格迁移常用的损失函数由3部分组成: -(i)*内容损失*使合成图像与内容图像在内容特征上接近; -(ii)*风格损失*使合成图像与风格图像在风格特征上接近; -(iii)*全变分损失*则有助于减少合成图像中的噪点。 + +1. *内容损失*使合成图像与内容图像在内容特征上接近; +1. *风格损失*使合成图像与风格图像在风格特征上接近; +1. *全变分损失*则有助于减少合成图像中的噪点。 + 最后,当模型训练结束时,我们输出风格迁移的模型参数,即得到最终的合成图像。 在下面,我们将通过代码来进一步了解风格迁移的技术细节。 @@ -63,13 +65,26 @@ content_img = d2l.Image.open('../img/rainier.jpg') d2l.plt.imshow(content_img); ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import paddle +import paddle.vision as paddlevision +import paddle.nn as nn + +d2l.set_figsize() +content_img = d2l.Image.open('../img/rainier.jpg') +d2l.plt.imshow(content_img); +``` + ```{.python .input} style_img = image.imread('../img/autumn-oak.jpg') d2l.plt.imshow(style_img.asnumpy()); ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle style_img = d2l.Image.open('../img/autumn-oak.jpg') d2l.plt.imshow(style_img); ``` @@ -79,7 +94,7 @@ d2l.plt.imshow(style_img); 下面,定义图像的预处理函数和后处理函数。 预处理函数`preprocess`对输入图像在RGB三个通道分别做标准化,并将结果变换成卷积神经网络接受的输入格式。 后处理函数`postprocess`则将输出图像中的像素值还原回标准化之前的值。 -由于图像打印函数要求每个像素的浮点数值在0到1之间,我们对小于0和大于1的值分别取0和1。 +由于图像打印函数要求每个像素的浮点数值在0~1之间,我们对小于0和大于1的值分别取0和1。 ```{.python .input} rgb_mean = np.array([0.485, 0.456, 0.406]) @@ -113,6 +128,24 @@ def postprocess(img): return torchvision.transforms.ToPILImage()(img.permute(2, 0, 1)) ``` +```{.python .input} +#@tab paddle +rgb_mean = paddle.to_tensor([0.485, 0.456, 0.406]) +rgb_std = paddle.to_tensor([0.229, 0.224, 0.225]) + +def preprocess(img, image_shape): + transforms = paddlevision.transforms.Compose([ + paddlevision.transforms.Resize(image_shape), + paddlevision.transforms.ToTensor(), + paddlevision.transforms.Normalize(mean=rgb_mean, std=rgb_std)]) + return transforms(img).unsqueeze(0) + +def postprocess(img): + img = img[0] + img = paddle.clip(img.transpose((1, 2, 0)) * rgb_std + rgb_mean, 0, 1) + return img +``` + ## [**抽取图像特征**] 我们使用基于ImageNet数据集预训练的VGG-19模型来抽取图像特征 :cite:`Gatys.Ecker.Bethge.2016`。 @@ -126,6 +159,11 @@ pretrained_net = gluon.model_zoo.vision.vgg19(pretrained=True) pretrained_net = torchvision.models.vgg19(pretrained=True) ``` +```{.python .input} +#@tab paddle +pretrained_net = paddlevision.models.vgg19(pretrained=True) +``` + 为了抽取图像的内容特征和风格特征,我们可以选择VGG网络中某些层的输出。 一般来说,越靠近输入层,越容易抽取图像的细节信息;反之,则越容易抽取图像的全局信息。 为了避免合成图像过多保留内容图像的细节,我们选择VGG较靠近输出的层,即*内容层*,来输出图像的内容特征。 @@ -149,7 +187,7 @@ for i in range(max(content_layers + style_layers) + 1): ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net = nn.Sequential(*[pretrained_net.features[i] for i in range(max(content_layers + style_layers) + 1)]) ``` @@ -201,6 +239,19 @@ def get_styles(image_shape, device): return style_X, styles_Y ``` +```{.python .input} +#@tab paddle +def get_contents(image_shape): + content_X = preprocess(content_img, image_shape) + contents_Y, _ = extract_features(content_X, content_layers, style_layers) + return content_X, contents_Y + +def get_styles(image_shape): + style_X = preprocess(style_img, image_shape) + _, styles_Y = extract_features(style_X, content_layers, style_layers) + return style_X, styles_Y +``` + ## [**定义损失函数**] 下面我们来描述风格迁移的损失函数。 @@ -224,12 +275,20 @@ def content_loss(Y_hat, Y): return torch.square(Y_hat - Y.detach()).mean() ``` +```{.python .input} +#@tab paddle +def content_loss(Y_hat, Y): + # 我们从动态计算梯度的树中分离目标: + # 这是一个规定的值,而不是一个变量。 + return paddle.square(Y_hat - Y.detach()).mean() +``` + ### 风格损失 风格损失与内容损失类似,也通过平方误差函数衡量合成图像与风格图像在风格上的差异。 为了表达风格层输出的风格,我们先通过`extract_features`函数计算风格层的输出。 假设该输出的样本数为1,通道数为$c$,高和宽分别为$h$和$w$,我们可以将此输出转换为矩阵$\mathbf{X}$,其有$c$行和$hw$列。 -这个矩阵可以被看作是由$c$个长度为$hw$的向量$\mathbf{x}_1, \ldots, \mathbf{x}_c$组合而成的。其中向量$\mathbf{x}_i$代表了通道$i$上的风格特征。 +这个矩阵可以被看作由$c$个长度为$hw$的向量$\mathbf{x}_1, \ldots, \mathbf{x}_c$组合而成的。其中向量$\mathbf{x}_i$代表了通道$i$上的风格特征。 在这些向量的*格拉姆矩阵*$\mathbf{X}\mathbf{X}^\top \in \mathbb{R}^{c \times c}$中,$i$行$j$列的元素$x_{ij}$即向量$\mathbf{x}_i$和$\mathbf{x}_j$的内积。它表达了通道$i$和通道$j$上风格特征的相关性。我们用这样的格拉姆矩阵来表达风格层输出的风格。 需要注意的是,当$hw$的值较大时,格拉姆矩阵中的元素容易出现较大的值。 @@ -257,6 +316,12 @@ def style_loss(Y_hat, gram_Y): return torch.square(gram(Y_hat) - gram_Y.detach()).mean() ``` +```{.python .input} +#@tab paddle +def style_loss(Y_hat, gram_Y): + return paddle.square(gram(Y_hat) - gram_Y.detach()).mean() +``` + ### 全变分损失 有时候,我们学到的合成图像里面有大量高频噪点,即有特别亮或者特别暗的颗粒像素。 @@ -320,6 +385,18 @@ class SynthesizedImage(nn.Module): return self.weight ``` +```{.python .input} +#@tab paddle +class SynthesizedImage(nn.Layer): + def __init__(self, img_shape, **kwargs): + super(SynthesizedImage, self).__init__(**kwargs) + self.weight = paddle.create_parameter(shape=img_shape, + dtype="float32") + + def forward(self): + return self.weight +``` + 下面,我们定义`get_inits`函数。该函数创建了合成图像的模型实例,并将其初始化为图像`X`。风格图像在各个风格层的格拉姆矩阵`styles_Y_gram`将在训练前预先计算好。 ```{.python .input} @@ -342,6 +419,16 @@ def get_inits(X, device, lr, styles_Y): return gen_img(), styles_Y_gram, trainer ``` +```{.python .input} +#@tab paddle +def get_inits(X, lr, styles_Y): + gen_img = SynthesizedImage(X.shape) + gen_img.weight.set_value(X) + trainer = paddle.optimizer.Adam(parameters = gen_img.parameters(), learning_rate=lr) + styles_Y_gram = [gram(Y) for Y in styles_Y] + return gen_img(), styles_Y_gram, trainer +``` + ## [**训练模型**] 在训练模型进行风格迁移时,我们不断抽取合成图像的内容特征和风格特征,然后计算损失函数。下面定义了训练循环。 @@ -395,6 +482,31 @@ def train(X, contents_Y, styles_Y, device, lr, num_epochs, lr_decay_epoch): return X ``` +```{.python .input} +#@tab paddle +def train(X, contents_Y, styles_Y, lr, num_epochs, step_size): + scheduler = paddle.optimizer.lr.StepDecay(learning_rate=lr, gamma=0.8, step_size=step_size) + X, styles_Y_gram, trainer = get_inits(X, scheduler, styles_Y) + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[10, num_epochs], + legend=['content', 'style', 'TV'], + ncols=2, figsize=(7, 2.5)) + for epoch in range(num_epochs): + trainer.clear_grad() + contents_Y_hat, styles_Y_hat = extract_features( + X, content_layers, style_layers) + contents_l, styles_l, tv_l, l = compute_loss( + X, contents_Y_hat, styles_Y_hat, contents_Y, styles_Y_gram) + l.backward() + trainer.step() + scheduler.step() + if (epoch + 1) % 10 == 0: + animator.axes[1].imshow(postprocess(X)) + animator.add(epoch + 1, [float(sum(contents_l)), + float(sum(styles_l)), float(tv_l)]) + return X +``` + 现在我们[**训练模型**]: 首先将内容图像和风格图像的高和宽分别调整为300和450像素,用内容图像来初始化合成图像。 @@ -415,11 +527,19 @@ _, styles_Y = get_styles(image_shape, device) output = train(content_X, contents_Y, styles_Y, device, 0.3, 500, 50) ``` +```{.python .input} +#@tab paddle +device, image_shape = d2l.try_gpu(),(300, 450) +content_X, contents_Y = get_contents(image_shape) +_, styles_Y = get_styles(image_shape) +output = train(content_X, contents_Y, styles_Y, 0.3, 500, 50) +``` + 我们可以看到,合成图像保留了内容图像的风景和物体,并同时迁移了风格图像的色彩。例如,合成图像具有与风格图像中一样的色彩块,其中一些甚至具有画笔笔触的细微纹理。 ## 小结 -* 风格迁移常用的损失函数由3部分组成:(i)内容损失使合成图像与内容图像在内容特征上接近;(ii)风格损失令合成图像与风格图像在风格特征上接近;(iii)全变分损失则有助于减少合成图像中的噪点。 +* 风格迁移常用的损失函数由3部分组成:(1)内容损失使合成图像与内容图像在内容特征上接近;(2)风格损失令合成图像与风格图像在风格特征上接近;(3)全变分损失则有助于减少合成图像中的噪点。 * 我们可以通过预训练的卷积神经网络来抽取图像的特征,并通过最小化损失函数来不断更新合成图像来作为模型参数。 * 我们使用格拉姆矩阵表达风格层输出的风格。 @@ -427,8 +547,8 @@ output = train(content_X, contents_Y, styles_Y, device, 0.3, 500, 50) 1. 选择不同的内容和风格层,输出有什么变化? 1. 调整损失函数中的权重超参数。输出是否保留更多内容或减少更多噪点? -1. 替换实验中的内容图像和风格图像,你能创作出更有趣的合成图像吗? -1. 我们可以对文本使用风格迁移吗?提示:你可以参阅调查报告 :cite:`Hu.Lee.Aggarwal.ea.2020`。 +1. 替换实验中的内容图像和风格图像,能创作出更有趣的合成图像吗? +1. 我们可以对文本使用风格迁移吗?提示:可以参阅调查报告 :cite:`Hu.Lee.Aggarwal.ea.2020`。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/3299) @@ -437,3 +557,7 @@ output = train(content_X, contents_Y, styles_Y, device, 0.3, 500, 50) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/3300) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11813) +:end_tab: diff --git a/chapter_computer-vision/object-detection-dataset.md b/chapter_computer-vision/object-detection-dataset.md index ccb2ab897..6e54a611b 100644 --- a/chapter_computer-vision/object-detection-dataset.md +++ b/chapter_computer-vision/object-detection-dataset.md @@ -31,6 +31,18 @@ import os import pandas as pd ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import os +import pandas as pd +import paddle +import paddle.vision as paddlevision +``` + ```{.python .input} #@tab all #@save @@ -85,6 +97,27 @@ def read_data_bananas(is_train=True): return images, torch.tensor(targets).unsqueeze(1) / 256 ``` +```{.python .input} +#@tab paddle +#@save +def read_data_bananas(is_train=True): + """读取香蕉检测数据集中的图像和标签""" + data_dir = d2l.download_extract('banana-detection') + csv_fname = os.path.join(data_dir, 'bananas_train' if is_train + else 'bananas_val', 'label.csv') + csv_data = pd.read_csv(csv_fname) + csv_data = csv_data.set_index('img_name') + images, targets = [], [] + for img_name, target in csv_data.iterrows(): + paddle.vision.set_image_backend('cv2') + images.append(paddlevision.image_load(os.path.join(data_dir, 'bananas_train' if is_train else + 'bananas_val', 'images', f'{img_name}'))[..., ::-1]) + # 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y) + # 其中所有图像都具有相同的香蕉类(索引为0) + targets.append(list(target)) + return images, paddle.to_tensor(targets).unsqueeze(1) / 256 +``` + 通过使用`read_data_bananas`函数读取图像和标签,以下`BananasDataset`类别将允许我们[**创建一个自定义`Dataset`实例**]来加载香蕉检测数据集。 ```{.python .input} @@ -121,6 +154,23 @@ class BananasDataset(torch.utils.data.Dataset): return len(self.features) ``` +```{.python .input} +#@tab paddle +#@save +class BananasDataset(paddle.io.Dataset): + """一个用于加载香蕉检测数据集的自定义数据集""" + def __init__(self, is_train): + self.features, self.labels = read_data_bananas(is_train) + print('read ' + str(len(self.features)) + (f' training examples' if + is_train else f' validation examples')) + + def __getitem__(self, idx): + return (paddle.to_tensor(self.features[idx], dtype='float32').transpose([2, 0, 1]), self.labels[idx]) + + def __len__(self): + return len(self.features) +``` + 最后,我们定义`load_data_bananas`函数,来[**为训练集和测试集返回两个数据加载器实例**]。对于测试集,无须按随机顺序读取它。 ```{.python .input} @@ -146,6 +196,18 @@ def load_data_bananas(batch_size): return train_iter, val_iter ``` +```{.python .input} +#@tab paddle +#@save +def load_data_bananas(batch_size): + """加载香蕉检测数据集""" + train_iter = paddle.io.DataLoader(BananasDataset(is_train=True), + batch_size=batch_size, return_list=True, shuffle=True) + val_iter = paddle.io.DataLoader(BananasDataset(is_train=False), + batch_size=batch_size, return_list=True) + return train_iter, val_iter +``` + 让我们[**读取一个小批量,并打印其中的图像和标签的形状**]。 图像的小批量的形状为(批量大小、通道数、高度、宽度),看起来很眼熟:它与我们之前图像分类任务中的相同。 标签的小批量的形状为(批量大小,$m$,5),其中$m$是数据集的任何图像中边界框可能出现的最大数量。 @@ -154,7 +216,7 @@ def load_data_bananas(batch_size): 通常来说,图像可能拥有不同数量个边界框;因此,在达到$m$之前,边界框少于$m$的图像将被非法边界框填充。 这样,每个边界框的标签将被长度为5的数组表示。 数组中的第一个元素是边界框中对象的类别,其中-1表示用于填充的非法边界框。 -数组的其余四个元素是边界框左上角和右下角的($x$,$y$)坐标值(值域在0到1之间)。 +数组的其余四个元素是边界框左上角和右下角的($x$,$y$)坐标值(值域在0~1之间)。 对于香蕉数据集而言,由于每张图像上只有一个边界框,因此$m=1$。 ```{.python .input} @@ -186,6 +248,14 @@ for ax, label in zip(axes, batch[1][0:10]): d2l.show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w']) ``` +```{.python .input} +#@tab paddle +imgs = (batch[0][0:10].transpose([0, 2, 3, 1])) / 255 +axes = d2l.show_images(imgs, 2, 5, scale=2) +for ax, label in zip(axes, batch[1][0:10]): + d2l.show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w']) +``` + ## 小结 * 我们收集的香蕉检测数据集可用于演示目标检测模型。 @@ -203,3 +273,7 @@ for ax, label in zip(axes, batch[1][0:10]): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/3202) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11806) +:end_tab: diff --git a/chapter_computer-vision/rcnn.md b/chapter_computer-vision/rcnn.md index 0cd010578..be05fcfd0 100644 --- a/chapter_computer-vision/rcnn.md +++ b/chapter_computer-vision/rcnn.md @@ -3,7 +3,7 @@ 除了 :numref:`sec_ssd`中描述的单发多框检测之外, 区域卷积神经网络(region-based CNN或regions with CNN features,R-CNN) :cite:`Girshick.Donahue.Darrell.ea.2014`也是将深度模型应用于目标检测的开创性工作之一。 -在本节中,我们将介绍R-CNN及其一系列改进方法:快速的R-CNN(Fast R-CNN) :cite:`Girshick.2015`、更快的R-CNN(Faster R-CNN) :cite:`Ren.He.Girshick.ea.2015`和掩码R-CNN(Mask R-CNN) :cite:`He.Gkioxari.Dollar.ea.2017`。 +本节将介绍R-CNN及其一系列改进方法:快速的R-CNN(Fast R-CNN) :cite:`Girshick.2015`、更快的R-CNN(Faster R-CNN) :cite:`Ren.He.Girshick.ea.2015`和掩码R-CNN(Mask R-CNN) :cite:`He.Gkioxari.Dollar.ea.2017`。 限于篇幅,我们只着重介绍这些模型的设计思路。 ## R-CNN @@ -16,9 +16,9 @@ :numref:`fig_r-cnn`展示了R-CNN模型。具体来说,R-CNN包括以下四个步骤: -1. 对输入图像使用*选择性搜索*来选取多个高质量的提议区域 :cite:`Uijlings.Van-De-Sande.Gevers.ea.2013`。这些提议区域通常是在多个尺度下选取的,并具有不同的形状和大小。每个提议区域都将被标注类别和真实边界框。 -1. 选择一个预训练的卷积神经网络,并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸,并通过前向传播输出抽取的提议区域特征。 -1. 将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别。 +1. 对输入图像使用*选择性搜索*来选取多个高质量的提议区域 :cite:`Uijlings.Van-De-Sande.Gevers.ea.2013`。这些提议区域通常是在多个尺度下选取的,并具有不同的形状和大小。每个提议区域都将被标注类别和真实边界框; +1. 选择一个预训练的卷积神经网络,并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸,并通过前向传播输出抽取的提议区域特征; +1. 将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别; 1. 将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型来预测真实边界框。 尽管R-CNN模型通过预训练的卷积神经网络有效地抽取了图像特征,但它的速度很慢。 @@ -36,9 +36,9 @@ R-CNN的主要性能瓶颈在于,对每个提议区域,卷积神经网络的 :numref:`fig_fast_r-cnn`中描述了Fast R-CNN模型。它的主要计算如下: -1. 与R-CNN相比,Fast R-CNN用来提取特征的卷积神经网络的输入是整个图像,而不是各个提议区域。此外,这个网络通常会参与训练。设输入为一张图像,将卷积神经网络的输出的形状记为$1 \times c \times h_1 \times w_1$。 -1. 假设选择性搜索生成了$n$个提议区域。这些形状各异的提议区域在卷积神经网络的输出上分别标出了形状各异的兴趣区域。然后,这些感兴趣的区域需要进一步抽取出形状相同的特征(比如指定高度$h_2$和宽度$w_2$),以便于连结后输出。为了实现这一目标,Fast R-CNN引入了*兴趣区域汇聚层*(RoI pooling):将卷积神经网络的输出和提议区域作为输入,输出连结后的各个提议区域抽取的特征,形状为$n \times c \times h_2 \times w_2$。 -1. 通过全连接层将输出形状变换为$n \times d$,其中超参数$d$取决于模型设计。 +1. 与R-CNN相比,Fast R-CNN用来提取特征的卷积神经网络的输入是整个图像,而不是各个提议区域。此外,这个网络通常会参与训练。设输入为一张图像,将卷积神经网络的输出的形状记为$1 \times c \times h_1 \times w_1$; +1. 假设选择性搜索生成了$n$个提议区域。这些形状各异的提议区域在卷积神经网络的输出上分别标出了形状各异的兴趣区域。然后,这些感兴趣的区域需要进一步抽取出形状相同的特征(比如指定高度$h_2$和宽度$w_2$),以便于连结后输出。为了实现这一目标,Fast R-CNN引入了*兴趣区域汇聚层*(RoI pooling):将卷积神经网络的输出和提议区域作为输入,输出连结后的各个提议区域抽取的特征,形状为$n \times c \times h_2 \times w_2$; +1. 通过全连接层将输出形状变换为$n \times d$,其中超参数$d$取决于模型设计; 1. 预测$n$个提议区域中每个区域的类别和边界框。更具体地说,在预测类别和边界框时,将全连接层的输出分别转换为形状为$n \times q$($q$是类别的数量)的输出和形状为$n \times 4$的输出。其中预测类别时使用softmax回归。 在Fast R-CNN中提出的兴趣区域汇聚层与 :numref:`sec_pooling`中介绍的汇聚层有所不同。在汇聚层中,我们通过设置汇聚窗口、填充和步幅的大小来间接控制输出形状。而兴趣区域汇聚层对每个区域的输出形状是可以直接指定的。 @@ -76,6 +76,17 @@ X = torch.arange(16.).reshape(1, 1, 4, 4) X ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.vision as paddlevision + +X = paddle.reshape(paddle.arange(16, dtype='float32'), (1,1,4,4)) +X +``` + 让我们进一步假设输入图像的高度和宽度都是40像素,且选择性搜索在此图像上生成了两个提议区域。 每个区域由5个元素表示:区域目标类别、左上角和右下角的$(x, y)$坐标。 @@ -88,6 +99,11 @@ rois = np.array([[0, 0, 0, 20, 20], [0, 0, 10, 30, 30]]) rois = torch.Tensor([[0, 0, 0, 20, 20], [0, 0, 10, 30, 30]]) ``` +```{.python .input} +#@tab paddle +rois = paddle.to_tensor([[0, 0, 20, 20], [0, 10, 30, 30]]).astype('float32') +``` + 由于`X`的高和宽是输入图像高和宽的$1/10$,因此,两个提议区域的坐标先按`spatial_scale`乘以0.1。 然后,在`X`上分别标出这两个兴趣区域`X[:, :, 0:3, 0:3]`和`X[:, :, 1:4, 0:4]`。 最后,在$2\times 2$的兴趣区域汇聚层中,每个兴趣区域被划分为子窗口网格,并进一步抽取相同形状$2\times 2$的特征。 @@ -101,6 +117,12 @@ npx.roi_pooling(X, rois, pooled_size=(2, 2), spatial_scale=0.1) torchvision.ops.roi_pool(X, rois, output_size=(2, 2), spatial_scale=0.1) ``` +```{.python .input} +#@tab paddle +boxes_num = paddle.to_tensor([len(rois)]).astype('int32') +paddlevision.ops.roi_pool(X, rois, boxes_num, output_size=(2, 2), spatial_scale=0.1) +``` + ## Faster R-CNN 为了较精确地检测目标结果,Fast R-CNN模型通常需要在选择性搜索中生成大量的提议区域。 @@ -144,8 +166,8 @@ torchvision.ops.roi_pool(X, rois, output_size=(2, 2), spatial_scale=0.1) ## 练习 -1. 我们能否将目标检测视为回归问题(例如预测边界框和类别的概率)?你可以参考YOLO模型 :cite:`Redmon.Divvala.Girshick.ea.2016`的设计。 -1. 将单发多框检测与本节介绍的方法进行比较。他们的主要区别是什么?你可以参考 :cite:`Zhao.Zheng.Xu.ea.2019`中的图2。 +1. 我们能否将目标检测视为回归问题(例如预测边界框和类别的概率)?可以参考YOLO模型 :cite:`Redmon.Divvala.Girshick.ea.2016`的设计。 +1. 将单发多框检测与本节介绍的方法进行比较。他们的主要区别是什么?可以参考 :cite:`Zhao.Zheng.Xu.ea.2019`中的图2。 :begin_tab:`mxnet` [讨论区](https://discuss.d2l.ai/t/3206) @@ -154,3 +176,7 @@ torchvision.ops.roi_pool(X, rois, output_size=(2, 2), spatial_scale=0.1) :begin_tab:`pytorch` [讨论区](https://discuss.d2l.ai/t/3207) :end_tab: + +:begin_tab:`paddle` +[讨论区](https://discuss.d2l.ai/t/11808) +:end_tab: diff --git a/chapter_computer-vision/semantic-segmentation-and-dataset.md b/chapter_computer-vision/semantic-segmentation-and-dataset.md index d1af2fc3e..9cc5fc5d8 100644 --- a/chapter_computer-vision/semantic-segmentation-and-dataset.md +++ b/chapter_computer-vision/semantic-segmentation-and-dataset.md @@ -41,6 +41,17 @@ import torchvision import os ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.vision as paddlevision +import os +``` + 数据集的tar文件大约为2GB,所以下载可能需要一段时间。 提取出的数据集位于`../data/VOCdevkit/VOC2012`。 @@ -99,6 +110,30 @@ def read_voc_images(voc_dir, is_train=True): train_features, train_labels = read_voc_images(voc_dir, True) ``` +```{.python .input} +#@tab paddle +#@save +def read_voc_images(voc_dir, is_train=True): + """Defined in :numref:`sec_semantic_segmentation`""" + """读取所有VOC图像并标注 + Defined in :numref:`sec_semantic_segmentation`""" + txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation', + 'train.txt' if is_train else 'val.txt') + with open(txt_fname, 'r') as f: + images = f.read().split() + features, labels = [], [] + for i, fname in enumerate(images): + features.append(paddle.vision.image.image_load(os.path.join( + voc_dir, 'JPEGImages', f'{fname}.jpg'), backend='cv2')[..., ::-1].transpose( + [2, 0, 1])) + labels.append(paddle.vision.image.image_load(os.path.join( + voc_dir, 'SegmentationClass', f'{fname}.png'), backend='cv2')[..., ::-1].transpose( + [2, 0, 1])) + return features, labels + +train_features, train_labels = read_voc_images(voc_dir, True) +``` + 下面我们[**绘制前5个输入图像及其标签**]。 在标签图像中,白色和黑色分别表示边框和背景,而其他颜色则对应不同的类别。 @@ -116,6 +151,14 @@ imgs = [img.permute(1,2,0) for img in imgs] d2l.show_images(imgs, 2, n); ``` +```{.python .input} +#@tab paddle +n = 5 +imgs = train_features[0:n] + train_labels[0:n] +imgs = [img.transpose([1, 2, 0]) for img in imgs] +d2l.show_images(imgs, 2, n); +``` + 接下来,我们[**列举RGB颜色值和类名**]。 ```{.python .input} @@ -177,6 +220,26 @@ def voc_label_indices(colormap, colormap2label): return colormap2label[idx] ``` +```{.python .input} +#@tab paddle +#@save +def voc_colormap2label(): + """构建从RGB到VOC类别索引的映射""" + colormap2label = paddle.zeros([256 ** 3], dtype=paddle.int64) + for i, colormap in enumerate(VOC_COLORMAP): + colormap2label[ + (colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i + return colormap2label + +#@save +def voc_label_indices(colormap, colormap2label): + """将VOC标签中的RGB值映射到它们的类别索引""" + colormap = colormap.transpose([1, 2, 0]).astype('int32') + idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + + colormap[:, :, 2]) + return colormap2label[idx] +``` + [**例如**],在第一张样本图像中,飞机头部区域的类别索引为1,而背景索引为0。 ```{.python .input} @@ -214,6 +277,18 @@ def voc_rand_crop(feature, label, height, width): return feature, label ``` +```{.python .input} +#@tab paddle +#@save +def voc_rand_crop(feature, label, height, width): + """随机裁剪特征和标签图像""" + rect = paddle.vision.transforms.RandomCrop((height, width))._get_param( + img=feature, output_size=(height, width)) + feature = paddle.vision.transforms.crop(feature, *rect) + label = paddle.vision.transforms.crop(label, *rect) + return feature, label +``` + ```{.python .input} imgs = [] for _ in range(n): @@ -231,6 +306,16 @@ imgs = [img.permute(1, 2, 0) for img in imgs] d2l.show_images(imgs[::2] + imgs[1::2], 2, n); ``` +```{.python .input} +#@tab paddle +imgs = [] +for _ in range(n): + imgs += voc_rand_crop(train_features[0].transpose([1, 2, 0]), train_labels[0].transpose([1, 2, 0]), 200, 300) + +imgs = [img for img in imgs] +d2l.show_images(imgs[::2] + imgs[1::2], 2, n); +``` + ### [**自定义语义分割数据集类**] 我们通过继承高级API提供的`Dataset`类,自定义了一个语义分割数据集类`VOCSegDataset`。 @@ -305,6 +390,43 @@ class VOCSegDataset(torch.utils.data.Dataset): return len(self.features) ``` +```{.python .input} +#@tab paddle +#@save +class VOCSegDataset(paddle.io.Dataset): + """一个用于加载VOC数据集的自定义数据集 + Defined in :numref:`sec_semantic_segmentation`""" + + def __init__(self, is_train, crop_size, voc_dir): + self.transform = paddle.vision.transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + self.crop_size = crop_size + features, labels = read_voc_images(voc_dir, is_train=is_train) + self.features = [self.normalize_image(feature) + for feature in self.filter(features)] + self.labels = self.filter(labels) + self.colormap2label = voc_colormap2label() + print('read ' + str(len(self.features)) + ' examples') + + def normalize_image(self, img): + return self.transform(img.astype("float32") / 255) + + def filter(self, imgs): + return [img for img in imgs if ( + img.shape[1] >= self.crop_size[0] and + img.shape[2] >= self.crop_size[1])] + + def __getitem__(self, idx): + feature = paddle.to_tensor(self.features[idx],dtype='float32') + label = paddle.to_tensor(self.labels[idx],dtype='float32') + feature, label = voc_rand_crop(feature,label, + *self.crop_size) + return (feature, voc_label_indices(label, self.colormap2label)) + + def __len__(self): + return len(self.features) +``` + ### [**读取数据集**] 我们通过自定义的`VOCSegDataset`类来分别创建训练集和测试集的实例。 @@ -344,6 +466,19 @@ for X, Y in train_iter: break ``` +```{.python .input} +#@tab paddle +batch_size = 64 +train_iter = paddle.io.DataLoader(voc_train, batch_size=batch_size, shuffle=True, + drop_last=True, + return_list=True, + num_workers=d2l.get_dataloader_workers()) +for X, Y in train_iter: + print(X.shape) + print(Y.shape) + break +``` + ### [**整合所有组件**] 最后,我们定义以下`load_data_voc`函数来下载并读取Pascal VOC2012语义分割数据集。 @@ -382,6 +517,23 @@ def load_data_voc(batch_size, crop_size): return train_iter, test_iter ``` +```{.python .input} +#@tab paddle +#@save +def load_data_voc(batch_size, crop_size): + """加载VOC语义分割数据集""" + voc_dir = d2l.download_extract('voc2012', os.path.join( + 'VOCdevkit', 'VOC2012')) + num_workers = d2l.get_dataloader_workers() + train_iter = paddle.io.DataLoader( + VOCSegDataset(True, crop_size, voc_dir), batch_size=batch_size, + shuffle=True, return_list=True, drop_last=True, num_workers=num_workers) + test_iter = paddle.io.DataLoader( + VOCSegDataset(False, crop_size, voc_dir), batch_size=batch_size, + drop_last=True, return_list=True, num_workers=num_workers) + return train_iter, test_iter +``` + ## 小结 * 语义分割通过将图像划分为属于不同语义类别的区域,来识别并理解图像中像素级别的内容。 @@ -400,3 +552,7 @@ def load_data_voc(batch_size, crop_size): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/3295) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11809) +:end_tab: diff --git a/chapter_computer-vision/ssd.md b/chapter_computer-vision/ssd.md index 12bab9d0e..1fe2c9e1d 100644 --- a/chapter_computer-vision/ssd.md +++ b/chapter_computer-vision/ssd.md @@ -69,6 +69,22 @@ def cls_predictor(num_inputs, num_anchors, num_classes): kernel_size=3, padding=1) ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +from paddle.nn import functional as F +import paddle.vision as paddlevision + +def cls_predictor(num_inputs, num_anchors, num_classes): + return nn.Conv2D(num_inputs, num_anchors * (num_classes + 1), + kernel_size=3, padding=1) +``` + ### (**边界框预测层**) 边界框预测层的设计与类别预测层的设计类似。 @@ -85,6 +101,12 @@ def bbox_predictor(num_inputs, num_anchors): return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1) ``` +```{.python .input} +#@tab paddle +def bbox_predictor(num_inputs, num_anchors): + return nn.Conv2D(num_inputs, num_anchors * 4, kernel_size=3, padding=1) +``` + ### [**连结多尺度的预测**] 正如我们所提到的,单发多框检测使用多尺度特征图来生成锚框并预测其类别和偏移量。 @@ -115,6 +137,16 @@ Y2 = forward(torch.zeros((2, 16, 10, 10)), cls_predictor(16, 3, 10)) Y1.shape, Y2.shape ``` +```{.python .input} +#@tab paddle +def forward(x, block): + return block(x) + +Y1 = forward(paddle.zeros((2, 8, 20, 20)), cls_predictor(8, 5, 10)) +Y2 = forward(paddle.zeros((2, 16, 10, 10)), cls_predictor(16, 3, 10)) +Y1.shape, Y2.shape +``` + 正如我们所看到的,除了批量大小这一维度外,其他三个维度都具有不同的尺寸。 为了将这两个预测输出链接起来以提高计算效率,我们将把这些张量转换为更一致的格式。 @@ -138,6 +170,15 @@ def concat_preds(preds): return torch.cat([flatten_pred(p) for p in preds], dim=1) ``` +```{.python .input} +#@tab paddle +def flatten_pred(pred): + return paddle.flatten(pred.transpose([0, 2, 3, 1]), start_axis=1) + +def concat_preds(preds): + return paddle.concat([flatten_pred(p) for p in preds], axis=1) +``` + 这样一来,尽管`Y1`和`Y2`在通道数、高度和宽度方面具有不同的大小,我们仍然可以在同一个小批量的两个不同尺度上连接这两个预测输出。 ```{.python .input} @@ -178,6 +219,20 @@ def down_sample_blk(in_channels, out_channels): return nn.Sequential(*blk) ``` +```{.python .input} +#@tab paddle +def down_sample_blk(in_channels, out_channels): + blk = [] + for _ in range(2): + blk.append(nn.Conv2D(in_channels, out_channels, + kernel_size=3, padding=1)) + blk.append(nn.BatchNorm2D(out_channels)) + blk.append(nn.ReLU()) + in_channels = out_channels + blk.append(nn.MaxPool2D(2)) + return nn.Sequential(*blk) +``` + 在以下示例中,我们构建的高和宽减半块会更改输入通道的数量,并将输入特征图的高度和宽度减半。 ```{.python .input} @@ -189,6 +244,11 @@ forward(np.zeros((2, 3, 20, 20)), down_sample_blk(10)).shape forward(torch.zeros((2, 3, 20, 20)), down_sample_blk(3, 10)).shape ``` +```{.python .input} +#@tab paddle +forward(paddle.zeros((2, 3, 20, 20)), down_sample_blk(3, 10)).shape +``` + ### [**基本网络块**] 基本网络块用于从输入图像中抽取特征。 @@ -217,6 +277,18 @@ def base_net(): forward(torch.zeros((2, 3, 256, 256)), base_net()).shape ``` +```{.python .input} +#@tab paddle +def base_net(): + blk = [] + num_filters = [3, 16, 32, 64] + for i in range(len(num_filters) - 1): + blk.append(down_sample_blk(num_filters[i], num_filters[i+1])) + return nn.Sequential(*blk) + +forward(paddle.zeros((2, 3, 256, 256)), base_net()).shape +``` + ### 完整的模型 [**完整的单发多框检测模型由五个模块组成**]。每个块生成的特征图既用于生成锚框,又用于预测这些锚框的类别和偏移量。在这五个模块中,第一个是基本网络块,第二个到第四个是高和宽减半块,最后一个模块使用全局最大池将高度和宽度都降到1。从技术上讲,第二到第五个区块都是 :numref:`fig_ssd`中的多尺度特征块。 @@ -246,6 +318,20 @@ def get_blk(i): return blk ``` +```{.python .input} +#@tab paddle +def get_blk(i): + if i == 0: + blk = base_net() + elif i == 1: + blk = down_sample_blk(64, 128) + elif i == 4: + blk = nn.AdaptiveMaxPool2D((1,1)) + else: + blk = down_sample_blk(128, 128) + return blk +``` + 现在我们[**为每个块定义前向传播**]。与图像分类任务不同,此处的输出包括:CNN特征图`Y`;在当前尺度下根据`Y`生成的锚框;预测的这些锚框的类别和偏移量(基于`Y`)。 ```{.python .input} @@ -267,6 +353,16 @@ def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor): return (Y, anchors, cls_preds, bbox_preds) ``` +```{.python .input} +#@tab paddle +def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor): + Y = blk(X) + anchors = d2l.multibox_prior(Y, sizes=size, ratios=ratio) + cls_preds = cls_predictor(Y) + bbox_preds = bbox_predictor(Y) + return (Y, anchors, cls_preds, bbox_preds) +``` + 回想一下,在 :numref:`fig_ssd`中,一个较接近顶部的多尺度特征块是用于检测较大目标的,因此需要生成更大的锚框。 在上面的前向传播中,在每个多尺度特征块上,我们通过调用的`multibox_prior`函数(见 :numref:`sec_anchor`)的`sizes`参数传递两个比例值的列表。 在下面,0.2和1.05之间的区间被均匀分成五个部分,以确定五个模块的在不同尺度下的较小值:0.2、0.37、0.54、0.71和0.88。 @@ -340,6 +436,36 @@ class TinySSD(nn.Module): return anchors, cls_preds, bbox_preds ``` +```{.python .input} +#@tab paddle +class TinySSD(nn.Layer): + def __init__(self, num_classes, **kwargs): + super(TinySSD, self).__init__(**kwargs) + self.num_classes = num_classes + idx_to_in_channels = [64, 128, 128, 128, 128] + for i in range(5): + # 即赋值语句self.blk_i=get_blk(i) + setattr(self, f'blk_{i}', get_blk(i)) + setattr(self, f'cls_{i}', cls_predictor(idx_to_in_channels[i], + num_anchors, num_classes)) + setattr(self, f'bbox_{i}', bbox_predictor(idx_to_in_channels[i], + num_anchors)) + + def forward(self, X): + anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5 + for i in range(5): + # getattr(self,'blk_%d'%i)即访问self.blk_i + X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward( + X, getattr(self, f'blk_{i}'), sizes[i], ratios[i], + getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}')) + anchors = paddle.concat(anchors, axis=1) + cls_preds = concat_preds(cls_preds) + cls_preds = cls_preds.reshape( + (cls_preds.shape[0], -1, self.num_classes + 1)) + bbox_preds = concat_preds(bbox_preds) + return anchors, cls_preds, bbox_preds +``` + 我们[**创建一个模型实例,然后使用它**]对一个$256 \times 256$像素的小批量图像`X`(**执行前向传播**)。 如本节前面部分所示,第一个模块输出特征图的形状为$32 \times 32$。 @@ -368,6 +494,17 @@ print('output class preds:', cls_preds.shape) print('output bbox preds:', bbox_preds.shape) ``` +```{.python .input} +#@tab paddle +net = TinySSD(num_classes=1) +X = paddle.zeros((32, 3, 256, 256)) +anchors, cls_preds, bbox_preds = net(X) + +print('output anchors:', anchors.shape) +print('output class preds:', cls_preds.shape) +print('output bbox preds:', bbox_preds.shape) +``` + ## 训练模型 现在,我们将描述如何训练用于目标检测的单发多框检测模型。 @@ -398,6 +535,14 @@ device, net = d2l.try_gpu(), TinySSD(num_classes=1) trainer = torch.optim.SGD(net.parameters(), lr=0.2, weight_decay=5e-4) ``` +```{.python .input} +#@tab paddle +device, net = d2l.try_gpu(), TinySSD(num_classes=1) +trainer = paddle.optimizer.SGD(learning_rate=0.2, + parameters=net.parameters(), + weight_decay=5e-4) +``` + ### [**定义损失函数和评价函数**] 目标检测有两种类型的损失。 @@ -431,6 +576,20 @@ def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks): return cls + bbox ``` +```{.python .input} +#@tab paddle +cls_loss = nn.CrossEntropyLoss(reduction='none') +bbox_loss = nn.L1Loss(reduction='none') + +def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks): + batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2] + cls = cls_loss(cls_preds.reshape((-1, num_classes)), + cls_labels.reshape([-1])).reshape((batch_size, -1)).mean(axis=1) + bbox = bbox_loss(bbox_preds * bbox_masks, + bbox_labels * bbox_masks).mean(axis=1) + return cls + bbox +``` + 我们可以沿用准确率评价分类结果。 由于偏移量使用了$L_1$范数损失,我们使用*平均绝对误差*来评价边界框的预测结果。这些预测结果是从生成的锚框及其预测偏移量中获得的。 @@ -455,6 +614,17 @@ def bbox_eval(bbox_preds, bbox_labels, bbox_masks): return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum()) ``` +```{.python .input} +#@tab paddle +def cls_eval(cls_preds, cls_labels): + # 由于类别预测结果放在最后一维,argmax需要指定最后一维。 + return float((cls_preds.argmax(axis=-1).astype( + cls_labels.dtype) == cls_labels).sum()) + +def bbox_eval(bbox_preds, bbox_labels, bbox_masks): + return float((paddle.abs((bbox_labels - bbox_preds) * bbox_masks)).sum()) +``` + ### [**训练模型**] 在训练模型时,我们需要在模型的前向传播过程中生成多尺度锚框(`anchors`),并预测其类别(`cls_preds`)和偏移量(`bbox_preds`)。 @@ -528,6 +698,39 @@ print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on ' f'{str(device)}') ``` +```{.python .input} +#@tab paddle +num_epochs, timer = 20, d2l.Timer() +animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['class error', 'bbox mae']) +for epoch in range(num_epochs): + # 训练精确度的和,训练精确度的和中的示例数 + # 绝对误差的和,绝对误差的和中的示例数 + metric = d2l.Accumulator(4) + net.train() + for features, target in train_iter: + timer.start() + trainer.clear_grad() + X, Y = features, target + # 生成多尺度的锚框,为每个锚框预测类别和偏移量 + anchors, cls_preds, bbox_preds = net(X) + # 为每个锚框标注类别和偏移量 + bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y) + # 根据类别和偏移量的预测和标注值计算损失函数 + l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, + bbox_masks) + l.mean().backward() + trainer.step() + metric.add(cls_eval(cls_preds, cls_labels), cls_labels.numel(), + bbox_eval(bbox_preds, bbox_labels, bbox_masks), + bbox_labels.numel()) + cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3] + animator.add(epoch + 1, (cls_err, bbox_mae)) +print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}') +print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on ' + f'{str(device)}') +``` + ## [**预测目标**] 在预测阶段,我们希望能把图像里面所有我们感兴趣的目标检测出来。在下面,我们读取并调整测试图像的大小,然后将其转成卷积层需要的四维格式。 @@ -544,6 +747,16 @@ X = torchvision.io.read_image('../img/banana.jpg').unsqueeze(0).float() img = X.squeeze(0).permute(1, 2, 0).long() ``` +```{.python .input} +#@tab paddle +X = paddle.to_tensor( + paddlevision.image.image_load( + '../img/banana.jpg', backend="cv2" + )[..., ::-1].transpose([2,0,1]) + ).unsqueeze(0).astype(paddle.float32) +img = X.squeeze(0).transpose([1, 2, 0]).astype(paddle.int64) +``` + 使用下面的`multibox_detection`函数,我们可以根据锚框及其预测偏移量得到预测边界框。然后,通过非极大值抑制来移除相似的预测边界框。 ```{.python .input} @@ -570,6 +783,19 @@ def predict(X): output = predict(X) ``` +```{.python .input} +#@tab paddle +def predict(X): + net.eval() + anchors, cls_preds, bbox_preds = net(X) + cls_probs = F.softmax(cls_preds, axis=2).transpose([0, 2, 1]) + output = d2l.multibox_detection(cls_probs, bbox_preds, anchors) + idx = [i for i, row in enumerate(output[0]) if row[0] != -1] + return output[0, :][idx] + +output = predict(X) +``` + 最后,我们[**筛选所有置信度不低于0.9的边界框,做为最终输出**]。 ```{.python .input} @@ -603,6 +829,22 @@ def display(img, output, threshold): display(img, output.cpu(), threshold=0.9) ``` +```{.python .input} +#@tab paddle +def display(img, output, threshold): + d2l.set_figsize((5, 5)) + fig = d2l.plt.imshow(img) + for row in output: + score = float(row[1]) + if score < threshold: + continue + h, w = img.shape[0:2] + bbox = [row[2:6] * paddle.to_tensor((w, h, w, h))] + d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w') + +display(img, output.cpu(), threshold=0.9) +``` + ## 小结 * 单发多框检测是一种多尺度目标检测模型。基于基础网络块和各个多尺度特征块,单发多框检测生成不同数量和不同大小的锚框,并通过预测这些锚框的类别和偏移量检测不同大小的目标。 @@ -610,7 +852,7 @@ display(img, output.cpu(), threshold=0.9) ## 练习 -1. 你能通过改进损失函数来改进单发多框检测吗?例如,将预测偏移量用到的$L_1$范数损失替换为平滑$L_1$范数损失。它在零点附近使用平方函数从而更加平滑,这是通过一个超参数$\sigma$来控制平滑区域的: +1. 能通过改进损失函数来改进单发多框检测吗?例如,将预测偏移量用到的$L_1$范数损失替换为平滑$L_1$范数损失。它在零点附近使用平方函数从而更加平滑,这是通过一个超参数$\sigma$来控制平滑区域的: $$ f(x) = @@ -656,7 +898,29 @@ for l, s in zip(lines, sigmas): d2l.plt.legend(); ``` -此外,在类别预测时,实验中使用了交叉熵损失:设真实类别$j$的预测概率是$p_j$,交叉熵损失为$-\log p_j$。我们还可以使用焦点损失 :cite:`Lin.Goyal.Girshick.ea.2017`:给定超参数$\gamma > 0$和$\alpha > 0$,此损失的定义为: +```{.python .input} +#@tab paddle +def smooth_l1(data, scalar): + out = [] + for i in data.numpy(): + if abs(i) < 1 / (scalar ** 2): + out.append(((scalar * i) ** 2) / 2) + else: + out.append(abs(i) - 0.5 / (scalar ** 2)) + return paddle.to_tensor(out) + +sigmas = [10, 1, 0.5] +lines = ['-', '--', '-.'] +x = paddle.arange(-2.0, 2.0, 0.1, dtype=paddle.float32) +d2l.set_figsize() + +for l, s in zip(lines, sigmas): + y = smooth_l1(x, scalar=s) + d2l.plt.plot(x, y, l, label='sigma=%.1f' % s) +d2l.plt.legend(); +``` + +此外,在类别预测时,实验中使用了交叉熵损失:设真实类别$j$的预测概率是$p_j$,交叉熵损失为$-\log p_j$。我们还可以使用焦点损失 :cite:`Lin.Goyal.Girshick.ea.2017`。给定超参数$\gamma > 0$和$\alpha > 0$,此损失的定义为: $$ - \alpha (1-p_j)^{\gamma} \log p_j.$$ @@ -684,10 +948,21 @@ for l, gamma in zip(lines, [0, 1, 5]): d2l.plt.legend(); ``` -2. 由于篇幅限制,我们在本节中省略了单发多框检测模型的一些实现细节。你能否从以下几个方面进一步改进模型: - 1. 当目标比图像小得多时,模型可以将输入图像调大。 - 1. 通常会存在大量的负锚框。为了使类别分布更加平衡,我们可以将负锚框的高和宽减半。 - 1. 在损失函数中,给类别损失和偏移损失设置不同比重的超参数。 +```{.python .input} +#@tab paddle +def focal_loss(gamma, x): + return -(1 - x) ** gamma * paddle.log(x) + +x = paddle.arange(0.01, 1, 0.01, dtype=paddle.float32) +for l, gamma in zip(lines, [0, 1, 5]): + y = d2l.plt.plot(x, focal_loss(gamma, x), l, label='gamma=%.1f' % gamma) +d2l.plt.legend(); +``` + +2. 由于篇幅限制,我们在本节中省略了单发多框检测模型的一些实现细节。能否从以下几个方面进一步改进模型: + 1. 当目标比图像小得多时,模型可以将输入图像调大; + 1. 通常会存在大量的负锚框。为了使类别分布更加平衡,我们可以将负锚框的高和宽减半; + 1. 在损失函数中,给类别损失和偏移损失设置不同比重的超参数; 1. 使用其他方法评估目标检测模型,例如单发多框检测论文 :cite:`Liu.Anguelov.Erhan.ea.2016`中的方法。 :begin_tab:`mxnet` @@ -697,3 +972,7 @@ d2l.plt.legend(); :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/3204) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11807) +:end_tab: diff --git a/chapter_computer-vision/transposed-conv.md b/chapter_computer-vision/transposed-conv.md index 646410179..6b270d565 100644 --- a/chapter_computer-vision/transposed-conv.md +++ b/chapter_computer-vision/transposed-conv.md @@ -6,7 +6,7 @@ 例如,输出像素所处的通道维可以保有输入像素在同一位置上的分类结果。 为了实现这一点,尤其是在空间维度被卷积神经网络层缩小后,我们可以使用另一种类型的卷积神经网络层,它可以增加上采样中间层特征图的空间维度。 -在本节中,我们将介绍 +本节将介绍 *转置卷积*(transposed convolution) :cite:`Dumoulin.Visin.2016`, 用于逆转下采样导致的空间尺寸减小。 @@ -25,6 +25,13 @@ from torch import nn from d2l import torch as d2l ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import paddle +from paddle import nn +``` + ## 基本操作 让我们暂时忽略通道,从基本的转置卷积开始,设步幅为1且没有填充。 @@ -81,6 +88,16 @@ tconv.weight.data = K tconv(X) ``` +```{.python .input} +#@tab paddle +X, K = X.reshape([1, 1, 2, 2]), K.reshape([1, 1, 2, 2]) +tconv = nn.Conv2DTranspose(1, 1, kernel_size=2, bias_attr=False) +K = paddle.create_parameter(shape=K.shape, dtype="float32", + default_initializer=paddle.nn.initializer.Assign(K)) +tconv.weight = K +tconv(X) +``` + ## [**填充、步幅和多通道**] 与常规卷积不同,在转置卷积中,填充被应用于的输出(常规卷积将填充应用于输入)。 @@ -99,6 +116,13 @@ tconv.weight.data = K tconv(X) ``` +```{.python .input} +#@tab paddle +tconv = nn.Conv2DTranspose(1, 1, kernel_size=2, padding=1, bias_attr=False) +tconv.weight = K +tconv(X) +``` + 在转置卷积中,步幅被指定为中间结果(输出),而不是输入。 使用 :numref:`fig_trans_conv`中相同输入和卷积核张量,将步幅从1更改为2会增加中间张量的高和权重,因此输出张量在 :numref:`fig_trans_conv_stride2`中。 @@ -120,6 +144,13 @@ tconv.weight.data = K tconv(X) ``` +```{.python .input} +#@tab paddle +tconv = nn.Conv2DTranspose(1, 1, kernel_size=2, stride=2, bias_attr=False) +tconv.weight = K +tconv(X) +``` + 对于多个输入和输出通道,转置卷积与常规卷积以相同方式运作。 假设输入有$c_i$个通道,且转置卷积为每个输入通道分配了一个$k_h\times k_w$的卷积核张量。 当指定多个输出通道时,每个输出通道将有一个$c_i\times k_h\times k_w$的卷积核。 @@ -144,6 +175,14 @@ tconv = nn.ConvTranspose2d(20, 10, kernel_size=5, padding=2, stride=3) tconv(conv(X)).shape == X.shape ``` +```{.python .input} +#@tab paddle +X = paddle.rand(shape=(1, 10, 16, 16)) +conv = nn.Conv2D(10, 20, kernel_size=5, padding=2, stride=3) +tconv = nn.Conv2DTranspose(20, 10, kernel_size=5, padding=2, stride=3) +tconv(conv(X)).shape == X.shape +``` + ## [**与矩阵变换的联系**] :label:`subsec-connection-to-mat-transposition` @@ -152,18 +191,26 @@ tconv(conv(X)).shape == X.shape 在下面的示例中,我们定义了一个$3\times 3$的输入`X`和$2\times 2$卷积核`K`,然后使用`corr2d`函数计算卷积输出`Y`。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch X = d2l.arange(9.0).reshape(3, 3) K = d2l.tensor([[1.0, 2.0], [3.0, 4.0]]) Y = d2l.corr2d(X, K) Y ``` +```{.python .input} +#@tab paddle +X = d2l.arange(9.0, dtype="float32").reshape((3, 3)) +K = d2l.tensor([[1.0, 2.0], [3.0, 4.0]]) +Y = d2l.corr2d(X, K) +Y +``` + 接下来,我们将卷积核`K`重写为包含大量0的稀疏权重矩阵`W`。 权重矩阵的形状是($4$,$9$),其中非0元素来自卷积核`K`。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch def kernel2matrix(K): k, W = d2l.zeros(5), d2l.zeros((4, 9)) k[:2], k[3:5] = K[0, :], K[1, :] @@ -174,25 +221,48 @@ W = kernel2matrix(K) W ``` +```{.python .input} +#@tab paddle +def kernel2matrix(K): + k, W = d2l.zeros([5]), d2l.zeros((4, 9)) + k[:2], k[3:5] = K[0, :], K[1, :] + W[0, :5], W[1, 1:6], W[2, 3:8], W[3, 4:] = k, k, k, k + return W + +W = kernel2matrix(K) +W +``` + 逐行连结输入`X`,获得了一个长度为9的矢量。 然后,`W`的矩阵乘法和向量化的`X`给出了一个长度为4的向量。 重塑它之后,可以获得与上面的原始卷积操作所得相同的结果`Y`:我们刚刚使用矩阵乘法实现了卷积。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch Y == d2l.matmul(W, d2l.reshape(X, -1)).reshape(2, 2) ``` +```{.python .input} +#@tab paddle +Y == d2l.matmul(W, d2l.reshape(X, [-1])).reshape((2, 2)) +``` + 同样,我们可以使用矩阵乘法来实现转置卷积。 在下面的示例中,我们将上面的常规卷积$2 \times 2$的输出`Y`作为转置卷积的输入。 想要通过矩阵相乘来实现它,我们只需要将权重矩阵`W`的形状转置为$(9, 4)$。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch Z = trans_conv(Y, K) Z == d2l.matmul(W.T, d2l.reshape(Y, -1)).reshape(3, 3) ``` +```{.python .input} +#@tab paddle +Z = trans_conv(Y, K) +Z == d2l.matmul(W.T, d2l.reshape(Y, [-1])).reshape((3, 3)) +``` + 抽象来看,给定输入向量$\mathbf{x}$和权重矩阵$\mathbf{W}$,卷积的前向传播函数可以通过将其输入与权重矩阵相乘并输出向量$\mathbf{y}=\mathbf{W}\mathbf{x}$来实现。 由于反向传播遵循链式法则和$\nabla_{\mathbf{x}}\mathbf{y}=\mathbf{W}^\top$,卷积的反向传播函数可以通过将其输入与转置的权重矩阵$\mathbf{W}^\top$相乘来实现。 因此,转置卷积层能够交换卷积层的正向传播函数和反向传播函数:它的正向传播和反向传播函数将输入向量分别与$\mathbf{W}^\top$和$\mathbf{W}$相乘。 @@ -215,3 +285,7 @@ Z == d2l.matmul(W.T, d2l.reshape(Y, -1)).reshape(3, 3) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/3302) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11810) +:end_tab: diff --git a/chapter_convolutional-modern/alexnet.md b/chapter_convolutional-modern/alexnet.md index 49d1886e3..18bef2729 100644 --- a/chapter_convolutional-modern/alexnet.md +++ b/chapter_convolutional-modern/alexnet.md @@ -14,7 +14,7 @@ 3. 通过标准的特征提取算法,如SIFT(尺度不变特征变换) :cite:`Lowe.2004`和SURF(加速鲁棒特征) :cite:`Bay.Tuytelaars.Van-Gool.2006`或其他手动调整的流水线来输入数据。 4. 将提取的特征送入最喜欢的分类器中(例如线性模型或其它核方法),以训练分类器。 -如果你和机器学习研究人员交谈,你会发现他们相信机器学习既重要又美丽:优雅的理论去证明各种模型的性质。机器学习是一个正在蓬勃发展、严谨且非常有用的领域。然而,如果你和计算机视觉研究人员交谈,你会听到一个完全不同的故事。他们会告诉你图像识别的诡异事实————推动领域进步的是数据特征,而不是学习算法。计算机视觉研究人员相信,从对最终模型精度的影响来说,更大或更干净的数据集、或是稍微改进的特征提取,比任何学习算法带来的进步要大得多。 +当人们和机器学习研究人员交谈时,会发现机器学习研究人员相信机器学习既重要又美丽:优雅的理论去证明各种模型的性质。机器学习是一个正在蓬勃发展、严谨且非常有用的领域。然而,当人们和计算机视觉研究人员交谈,会听到一个完全不同的故事。计算机视觉研究人员会告诉一个诡异事实————推动领域进步的是数据特征,而不是学习算法。计算机视觉研究人员相信,从对最终模型精度的影响来说,更大或更干净的数据集、或是稍微改进的特征提取,比任何学习算法带来的进步要大得多。 ## 学习表征 @@ -52,7 +52,7 @@ CPU的每个核心都拥有高时钟频率的运行能力,和高达数MB的三 虽然每个GPU核心都相对较弱,有时甚至以低于1GHz的时钟频率运行,但庞大的核心数量使GPU比CPU快几个数量级。 例如,NVIDIA最近一代的Ampere GPU架构为每个芯片提供了高达312 TFlops的浮点性能,而CPU的浮点性能到目前为止还没有超过1 TFlops。 之所以有如此大的差距,原因其实很简单:首先,功耗往往会随时钟频率呈二次方增长。 -对于一个CPU核心,假设它的运行速度比GPU快4倍,你可以使用16个GPU内核取代,那么GPU的综合性能就是CPU的$16 \times 1/4 = 4$倍。 +对于一个CPU核心,假设它的运行速度比GPU快4倍,但可以使用16个GPU核代替,那么GPU的综合性能就是CPU的$16 \times 1/4 = 4$倍。 其次,GPU内核要简单得多,这使得它们更节能。 此外,深度学习中的许多操作需要相对较高的内存带宽,而GPU拥有10倍于CPU的带宽。 @@ -65,16 +65,17 @@ CPU的每个核心都拥有高时钟频率的运行能力,和高达数MB的三 AlexNet使用了8层卷积神经网络,并以很大的优势赢得了2012年ImageNet图像识别挑战赛。 AlexNet和LeNet的架构非常相似,如 :numref:`fig_alexnet`所示。 -注意,这里我们提供了一个稍微精简版本的AlexNet,去除了当年需要两个小型GPU同时运算的设计特点。 +注意,本书在这里提供的是一个稍微精简版本的AlexNet,去除了当年需要两个小型GPU同时运算的设计特点。 ![从LeNet(左)到AlexNet(右)](../img/alexnet.svg) :label:`fig_alexnet` AlexNet和LeNet的设计理念非常相似,但也存在显著差异。 -首先,AlexNet比相对较小的LeNet5要深得多。 -AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。 -其次,AlexNet使用ReLU而不是sigmoid作为其激活函数。 -下面,让我们深入研究AlexNet的细节。 + +1. AlexNet比相对较小的LeNet5要深得多。AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。 +2. AlexNet使用ReLU而不是sigmoid作为其激活函数。 + +下面的内容将深入研究AlexNet的细节。 ### 模型设计 @@ -87,7 +88,7 @@ AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全 在最后一个卷积层后有两个全连接层,分别有4096个输出。 这两个巨大的全连接层拥有将近1GB的模型参数。 由于早期GPU显存有限,原版的AlexNet采用了双数据流设计,使得每个GPU只负责存储和计算模型的一半参数。 -幸运的是,现在GPU显存相对充裕,所以我们现在很少需要跨GPU分解模型(因此,我们的AlexNet模型在这方面与原始论文稍有不同)。 +幸运的是,现在GPU显存相对充裕,所以现在很少需要跨GPU分解模型(因此,本书的AlexNet模型在这方面与原始论文稍有不同)。 ### 激活函数 @@ -103,7 +104,7 @@ AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全 AlexNet通过暂退法( :numref:`sec_dropout`)控制全连接层的模型复杂度,而LeNet只使用了权重衰减。 为了进一步扩充数据,AlexNet在训练时增加了大量的图像增强数据,如翻转、裁切和变色。 这使得模型更健壮,更大的样本量有效地减少了过拟合。 -我们将在 :numref:`sec_image_augmentation`中更详细地讨论数据扩增。 +在 :numref:`sec_image_augmentation`中更详细地讨论数据扩增。 ```{.python .input} from d2l import mxnet as d2l @@ -114,7 +115,7 @@ npx.set_np() net = nn.Sequential() net.add( - # 这里,我们使用一个11*11的更大窗口来捕捉对象。 + # 这里使用一个11*11的更大窗口来捕捉对象。 # 同时,步幅为4,以减少输出的高度和宽度。 # 另外,输出通道的数目远大于LeNet nn.Conv2D(96, kernel_size=11, strides=4, activation='relu'), @@ -143,7 +144,7 @@ import torch from torch import nn net = nn.Sequential( - # 这里,我们使用一个11*11的更大窗口来捕捉对象。 + # 这里使用一个11*11的更大窗口来捕捉对象。 # 同时,步幅为4,以减少输出的高度和宽度。 # 另外,输出通道的数目远大于LeNet nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(), @@ -175,7 +176,7 @@ import tensorflow as tf def net(): return tf.keras.models.Sequential([ - # 这里,我们使用一个11*11的更大窗口来捕捉对象。 + # 这里使用一个11*11的更大窗口来捕捉对象。 # 同时,步幅为4,以减少输出的高度和宽度。 # 另外,输出通道的数目远大于LeNet tf.keras.layers.Conv2D(filters=96, kernel_size=11, strides=4, @@ -206,6 +207,38 @@ def net(): ]) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn as nn + +net = nn.Sequential( + # 这里,我们使用一个11*11的更大窗口来捕捉对象。 + # 同时,步幅为4,以减少输出的高度和宽度。 + # 另外,输出通道的数目远大于LeNet + nn.Conv2D(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(), + nn.MaxPool2D(kernel_size=3, stride=2), + # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数 + nn.Conv2D(96, 256, kernel_size=5, padding=2), nn.ReLU(), + nn.MaxPool2D(kernel_size=3, stride=2), + # 使用三个连续的卷积层和较小的卷积窗口。 + # 除了最后的卷积层,输出通道的数量进一步增加。 + # 在前两个卷积层之后,池化层不用于减少输入的高度和宽度 + nn.Conv2D(256, 384, kernel_size=3, padding=1), nn.ReLU(), + nn.Conv2D(384, 384, kernel_size=3, padding=1), nn.ReLU(), + nn.Conv2D(384, 256, kernel_size=3, padding=1), nn.ReLU(), + nn.MaxPool2D(kernel_size=3, stride=2), nn.Flatten(), + # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过度拟合 + nn.Linear(6400, 4096), nn.ReLU(), nn.Dropout(p=0.5), + nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(p=0.5), + # 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000 + nn.Linear(4096, 10) +) +``` + [**我们构造一个**]高度和宽度都为224的(**单通道数据,来观察每一层输出的形状**)。 它与 :numref:`fig_alexnet`中的AlexNet架构相匹配。 @@ -233,12 +266,20 @@ for layer in net().layers: print(layer.__class__.__name__, 'output shape:\t', X.shape) ``` +```{.python .input} +#@tab paddle +X = paddle.randn(shape=(1, 1, 224, 224)) +for layer in net: + X=layer(X) + print(layer.__class__.__name__,'output shape:\t',X.shape) +``` + ## 读取数据集 -尽管本文中AlexNet是在ImageNet上进行训练的,但我们在这里使用的是Fashion-MNIST数据集。因为即使在现代GPU上,训练ImageNet模型,同时使其收敛可能需要数小时或数天的时间。 +尽管原文中AlexNet是在ImageNet上进行训练的,但本书在这里使用的是Fashion-MNIST数据集。因为即使在现代GPU上,训练ImageNet模型,同时使其收敛可能需要数小时或数天的时间。 将AlexNet直接应用于Fashion-MNIST的一个问题是,[**Fashion-MNIST图像的分辨率**]($28 \times 28$像素)(**低于ImageNet图像。**) -为了解决这个问题,(**我们将它们增加到$224 \times 224$**)(通常来讲这不是一个明智的做法,但我们在这里这样做是为了有效使用AlexNet架构)。 -我们使用`d2l.load_data_fashion_mnist`函数中的`resize`参数执行此调整。 +为了解决这个问题,(**我们将它们增加到$224 \times 224$**)(通常来讲这不是一个明智的做法,但在这里这样做是为了有效使用AlexNet架构)。 +这里需要使用`d2l.load_data_fashion_mnist`函数中的`resize`参数执行此调整。 ```{.python .input} #@tab all @@ -248,7 +289,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224) ## [**训练AlexNet**] -现在,我们可以开始训练AlexNet了。与 :numref:`sec_lenet`中的LeNet相比,这里的主要变化是使用更小的学习速率训练,这是因为网络更深更广、图像分辨率更高,训练卷积神经网络就更昂贵。 +现在AlexNet可以开始被训练了。与 :numref:`sec_lenet`中的LeNet相比,这里的主要变化是使用更小的学习速率训练,这是因为网络更深更广、图像分辨率更高,训练卷积神经网络就更昂贵。 ```{.python .input} #@tab all @@ -266,7 +307,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) ## 练习 1. 试着增加迭代轮数。对比LeNet的结果有什么不同?为什么? -1. AlexNet对于Fashion-MNIST数据集来说可能太复杂了。 +1. AlexNet对Fashion-MNIST数据集来说可能太复杂了。 1. 尝试简化模型以加快训练速度,同时确保准确性不会显著下降。 1. 设计一个更好的模型,可以直接在$28 \times 28$图像上工作。 1. 修改批量大小,并观察模型精度和GPU显存变化。 @@ -287,3 +328,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1862) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11788) +:end_tab: diff --git a/chapter_convolutional-modern/batch-norm.md b/chapter_convolutional-modern/batch-norm.md index e26de82a3..94d23e962 100644 --- a/chapter_convolutional-modern/batch-norm.md +++ b/chapter_convolutional-modern/batch-norm.md @@ -2,7 +2,7 @@ :label:`sec_batch_norm` 训练深层神经网络是十分困难的,特别是在较短的时间内使他们收敛更加棘手。 -在本节中,我们将介绍*批量规范化*(batch normalization) :cite:`Ioffe.Szegedy.2015`,这是一种流行且有效的技术,可持续加速深层网络的收敛速度。 +本节将介绍*批量规范化*(batch normalization) :cite:`Ioffe.Szegedy.2015`,这是一种流行且有效的技术,可持续加速深层网络的收敛速度。 再结合在 :numref:`sec_resnet`中将介绍的残差块,批量规范化使得研究人员能够训练100层以上的网络。 ## 训练深层网络 @@ -49,7 +49,7 @@ $$\begin{aligned} \hat{\boldsymbol{\mu}}_\mathcal{B} &= \frac{1}{|\mathcal{B}|} \hat{\boldsymbol{\sigma}}_\mathcal{B}^2 &= \frac{1}{|\mathcal{B}|} \sum_{\mathbf{x} \in \mathcal{B}} (\mathbf{x} - \hat{\boldsymbol{\mu}}_{\mathcal{B}})^2 + \epsilon.\end{aligned}$$ 请注意,我们在方差估计值中添加一个小的常量$\epsilon > 0$,以确保我们永远不会尝试除以零,即使在经验方差估计值可能消失的情况下也是如此。估计值$\hat{\boldsymbol{\mu}}_\mathcal{B}$和${\hat{\boldsymbol{\sigma}}_\mathcal{B}}$通过使用平均值和方差的噪声(noise)估计来抵消缩放问题。 -你可能会认为这种噪声是一个问题,而事实上它是有益的。 +乍看起来,这种噪声是一个问题,而事实上它是有益的。 事实证明,这是深度学习中一个反复出现的主题。 由于尚未在理论上明确的原因,优化中的各种噪声源通常会导致更快的训练和较少的过拟合:这种变化似乎是正则化的一种形式。 @@ -173,6 +173,39 @@ def batch_norm(X, gamma, beta, moving_mean, moving_var, eps): return Y ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn as nn + +def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum, is_training=True): + # 训练模式还与预测模式的BN处理不同 + if not is_training: + # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差 + X_hat = (X - moving_mean) / (moving_var + eps) ** 0.5 + else: + assert len(X.shape) in (2, 4) + if len(X.shape) == 2: + # 使用全连接层的情况,计算特征维上的均值和方差 + mean = paddle.mean(X) + var = paddle.mean(((X - mean) ** 2)) + else: + # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持 + # X的形状以便后面可以做广播运算 + mean = paddle.mean(X, axis=(0, 2, 3), keepdim=True) + var = paddle.mean(((X - mean) ** 2), axis=(0, 2, 3), keepdim=True) + # 训练模式下用当前的均值和方差做标准化 + X_hat = (X - mean) / (var + eps) ** 0.5 + # 更新移动平均的均值和方差 + moving_mean = momentum * moving_mean + (1.0 - momentum) * mean + moving_var = momentum * moving_var + (1.0 - momentum) * var + Y = gamma * X_hat + beta # 缩放和移位 + return Y, moving_mean, moving_var +``` + 我们现在可以[**创建一个正确的`BatchNorm`层**]。 这个层将保持适当的参数:拉伸`gamma`和偏移`beta`,这两个参数将在训练过程中更新。 此外,我们的层将保存均值和方差的移动平均值,以便在模型预测期间随后使用。 @@ -294,6 +327,39 @@ class BatchNorm(tf.keras.layers.Layer): return output ``` +```{.python .input} +#@tab paddle +class BatchNorm(nn.Layer): + def __init__(self, num_features, num_dims=4): + super(BatchNorm, self).__init__() + if num_dims == 2: + shape = (1, num_features) + else: + shape = (1, num_features, 1, 1) + # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0 + self.gamma = self.create_parameter( + attr=None, + shape=shape, + dtype='float32', + is_bias=False, + default_initializer=nn.initializer.Assign(paddle.ones(shape=shape, dtype='float32'))) + self.beta = self.create_parameter( + attr=None, + shape=shape, + dtype='float32', + is_bias=False, + default_initializer=nn.initializer.Assign(paddle.zeros(shape=shape, dtype='float32'))) + self.moving_mean = paddle.zeros(shape=shape, dtype='float32') + self.moving_var = paddle.zeros(shape=shape, dtype='float32') + + def forward(self, X): + # 保存更新过的moving_mean和moving_var + Y, self.moving_mean, self.moving_var = batch_norm( + X, self.gamma, self.beta, self.moving_mean, + self.moving_var, eps=1e-5, momentum=0.9, is_training=self.training) + return Y +``` + ## 使用批量规范化层的 LeNet 为了更好理解如何[**应用`BatchNorm`**],下面我们将其应用(**于LeNet模型**)( :numref:`sec_lenet`)。 @@ -356,11 +422,23 @@ def net(): ) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential( + nn.Conv2D(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(), + nn.MaxPool2D(kernel_size=2, stride=2), + nn.Conv2D(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(), + nn.MaxPool2D(kernel_size=2, stride=2), + nn.Flatten(), nn.Linear(16 * 4 * 4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(), + nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(), + nn.Linear(84, 10)) +``` + 和以前一样,我们将[**在Fashion-MNIST数据集上训练网络**]。 这个代码与我们第一次训练LeNet( :numref:`sec_lenet`)时几乎完全相同,主要区别在于学习率大得多。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle lr, num_epochs, batch_size = 1.0, 10, 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) @@ -389,6 +467,13 @@ net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,)) tf.reshape(net.layers[1].gamma, (-1,)), tf.reshape(net.layers[1].beta, (-1,)) ``` +```{.python .input} +#@tab paddle +param = net.parameters() +print('gamma:', param[2].numpy().reshape(-1)) +print('beta:', param[3].numpy().reshape(-1)) +``` + ## [**简明实现**] 除了使用我们刚刚定义的`BatchNorm`,我们也可以直接使用深度学习框架中定义的`BatchNorm`。 @@ -449,6 +534,19 @@ def net(): ]) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential( + nn.Conv2D(1, 6, kernel_size=5), nn.BatchNorm2D(6, momentum=0.1), nn.Sigmoid(), + nn.MaxPool2D(kernel_size=2, stride=2), + nn.Conv2D(6, 16, kernel_size=5), nn.BatchNorm2D(16, momentum=0.1), nn.Sigmoid(), + nn.MaxPool2D(kernel_size=2, stride=2), + nn.Flatten(), + nn.Linear(256, 120), nn.BatchNorm1D(120, momentum=0.1), nn.Sigmoid(), + nn.Linear(120, 84), nn.BatchNorm1D(84, momentum=0.1), nn.Sigmoid(), + nn.Linear(84, 10)) +``` + 下面,我们[**使用相同超参数来训练模型**]。 请注意,通常高级API变体运行速度快得多,因为它的代码已编译为C++或CUDA,而我们的自定义代码由Python实现。 @@ -465,15 +563,15 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) 即使在暂退法和权重衰减的情况下,它们仍然非常灵活,因此无法通过常规的学习理论泛化保证来解释它们是否能够泛化到看不见的数据。 在提出批量规范化的论文中,作者除了介绍了其应用,还解释了其原理:通过减少*内部协变量偏移*(internal covariate shift)。 -据推测,作者所说的“内部协变量转移”类似于上述的投机直觉,即变量值的分布在训练过程中会发生变化。 +据推测,作者所说的*内部协变量转移*类似于上述的投机直觉,即变量值的分布在训练过程中会发生变化。 然而,这种解释有两个问题: -1、这种偏移与严格定义的*协变量偏移*(covariate shift)非常不同,所以这个名字用词不当。 +1、这种偏移与严格定义的*协变量偏移*(covariate shift)非常不同,所以这个名字用词不当; 2、这种解释只提供了一种不明确的直觉,但留下了一个有待后续挖掘的问题:为什么这项技术如此有效? 本书旨在传达实践者用来发展深层神经网络的直觉。 然而,重要的是将这些指导性直觉与既定的科学事实区分开来。 最终,当你掌握了这些方法,并开始撰写自己的研究论文时,你会希望清楚地区分技术和直觉。 -随着批量规范化的普及,“内部协变量偏移”的解释反复出现在技术文献的辩论,特别是关于“如何展示机器学习研究”的更广泛的讨论中。 +随着批量规范化的普及,*内部协变量偏移*的解释反复出现在技术文献的辩论,特别是关于“如何展示机器学习研究”的更广泛的讨论中。 Ali Rahimi在接受2017年NeurIPS大会的“接受时间考验奖”(Test of Time Award)时发表了一篇令人难忘的演讲。他将“内部协变量转移”作为焦点,将现代深度学习的实践比作炼金术。 他对该示例进行了详细回顾 :cite:`Lipton.Steinhardt.2018`,概述了机器学习中令人不安的趋势。 此外,一些作者对批量规范化的成功提出了另一种解释:在某些方面,批量规范化的表现出与原始论文 :cite:`Santurkar.Tsipras.Ilyas.ea.2018`中声称的行为是相反的。 @@ -494,12 +592,12 @@ Ali Rahimi在接受2017年NeurIPS大会的“接受时间考验奖”(Test of 1. 在使用批量规范化之前,我们是否可以从全连接层或卷积层中删除偏置参数?为什么? 1. 比较LeNet在使用和不使用批量规范化情况下的学习率。 1. 绘制训练和测试准确度的提高。 - 1. 你的学习率有多高? + 1. 学习率有多高? 1. 我们是否需要在每个层中进行批量规范化?尝试一下? -1. 你可以通过批量规范化来替换暂退法吗?行为会如何改变? +1. 可以通过批量规范化来替换暂退法吗?行为会如何改变? 1. 确定参数`beta`和`gamma`,并观察和分析结果。 1. 查看高级API中有关`BatchNorm`的在线文档,以查看其他批量规范化的应用。 -1. 研究思路:想想你可以应用的其他“规范化”转换?你可以应用概率积分变换吗?全秩协方差估计可以么? +1. 研究思路:可以应用的其他“规范化”转换?可以应用概率积分变换吗?全秩协方差估计可以么? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1876) @@ -512,3 +610,7 @@ Ali Rahimi在接受2017年NeurIPS大会的“接受时间考验奖”(Test of :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1875) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11792) +:end_tab: diff --git a/chapter_convolutional-modern/densenet.md b/chapter_convolutional-modern/densenet.md index 1d02e0ee7..50a79bc3c 100644 --- a/chapter_convolutional-modern/densenet.md +++ b/chapter_convolutional-modern/densenet.md @@ -93,6 +93,20 @@ class ConvBlock(tf.keras.layers.Layer): return y ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn as nn + +def conv_block(input_channels, num_channels): + return nn.Sequential( + nn.BatchNorm2D(input_channels), nn.ReLU(), + nn.Conv2D(input_channels, num_channels, kernel_size=3, padding=1)) +``` + 一个*稠密块*由多个卷积块组成,每个卷积块使用相同数量的输出通道。 然而,在前向传播中,我们将每个卷积块的输入和输出在通道维上连结。 @@ -146,6 +160,25 @@ class DenseBlock(tf.keras.layers.Layer): return x ``` +```{.python .input} +#@tab paddle +class DenseBlock(nn.Layer): + def __init__(self, num_convs, input_channels, num_channels): + super(DenseBlock, self).__init__() + layer = [] + for i in range(num_convs): + layer.append( + conv_block(num_channels * i + input_channels, num_channels)) + self.net = nn.Sequential(*layer) + + def forward(self, X): + for blk in self.net: + Y = blk(X) + # 连接通道维度上每个块的输入和输出 + X = paddle.concat(x=[X, Y], axis=1) + return X +``` + 在下面的例子中,我们[**定义一个**]有2个输出通道数为10的(**`DenseBlock`**)。 使用通道数为3的输入时,我们会得到通道数为$3+2\times 10=23$的输出。 卷积块的通道数控制了输出通道数相对于输入通道数的增长,因此也被称为*增长率*(growth rate)。 @@ -174,6 +207,14 @@ Y = blk(X) Y.shape ``` +```{.python .input} +#@tab paddle +blk = DenseBlock(2, 3, 10) +X = paddle.randn([4, 3, 8, 8]) +Y = blk(X) +Y.shape +``` + ## [**过渡层**] 由于每个稠密块都会带来通道数的增加,使用过多则会过于复杂化模型。 @@ -215,6 +256,15 @@ class TransitionBlock(tf.keras.layers.Layer): return self.avg_pool(x) ``` +```{.python .input} +#@tab paddle +def transition_block(input_channels, num_channels): + return nn.Sequential( + nn.BatchNorm2D(input_channels), nn.ReLU(), + nn.Conv2D(input_channels, num_channels, kernel_size=1), + nn.AvgPool2D(kernel_size=2, stride=2)) +``` + 对上一个例子中稠密块的输出[**使用**]通道数为10的[**过渡层**]。 此时输出的通道数减为10,高和宽均减半。 @@ -225,7 +275,7 @@ blk(Y).shape ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle blk = transition_block(23, 10) blk(Y).shape ``` @@ -265,6 +315,14 @@ def block_1(): tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')]) ``` +```{.python .input} +#@tab paddle +b1 = nn.Sequential( + nn.Conv2D(1, 64, kernel_size=7, stride=2, padding=3), + nn.BatchNorm2D(64), nn.ReLU(), + nn.MaxPool2D(kernel_size=3, stride=2, padding=1)) +``` + 接下来,类似于ResNet使用的4个残差块,DenseNet使用的是4个稠密块。 与ResNet类似,我们可以设置每个稠密块使用多少个卷积层。 这里我们设成4,从而与 :numref:`sec_resnet`的ResNet-18保持一致。 @@ -322,6 +380,22 @@ def block_2(): return net ``` +```{.python .input} +#@tab paddle +# num_channels为当前的通道数 +num_channels, growth_rate = 64, 32 +num_convs_in_dense_blocks = [4, 4, 4, 4] +blks = [] +for i, num_convs in enumerate(num_convs_in_dense_blocks): + blks.append(DenseBlock(num_convs, num_channels, growth_rate)) + # 上一个稠密块的输出通道数 + num_channels += num_convs * growth_rate + # 在稠密块之间添加一个转换层,使通道数量减半 + if i != len(num_convs_in_dense_blocks) - 1: + blks.append(transition_block(num_channels, num_channels // 2)) + num_channels = num_channels // 2 +``` + 与ResNet类似,最后接上全局汇聚层和全连接层来输出结果。 ```{.python .input} @@ -353,6 +427,16 @@ def net(): return net ``` +```{.python .input} +#@tab paddle +net = nn.Sequential( + b1, *blks, + nn.BatchNorm2D(num_channels), nn.ReLU(), + nn.AdaptiveMaxPool2D((1, 1)), + nn.Flatten(), + nn.Linear(num_channels, 10)) +``` + ## [**训练模型**] 由于这里使用了比较深的网络,本节里我们将输入高和宽从224降到96来简化计算。 @@ -376,7 +460,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) 1. DenseNet的优点之一是其模型参数比ResNet小。为什么呢? 1. DenseNet一个诟病的问题是内存或显存消耗过多。 1. 真的是这样吗?可以把输入形状换成$224 \times 224$,来看看实际的显存消耗。 - 1. 你能想出另一种方法来减少显存消耗吗?你需要如何改变框架? + 1. 有另一种方法来减少显存消耗吗?需要改变框架么? 1. 实现DenseNet论文 :cite:`Huang.Liu.Van-Der-Maaten.ea.2017`表1所示的不同DenseNet版本。 1. 应用DenseNet的思想设计一个基于多层感知机的模型。将其应用于 :numref:`sec_kaggle_house`中的房价预测任务。 @@ -391,3 +475,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1881) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11794) +:end_tab: diff --git a/chapter_convolutional-modern/googlenet.md b/chapter_convolutional-modern/googlenet.md index bfc7263c8..abf521e45 100644 --- a/chapter_convolutional-modern/googlenet.md +++ b/chapter_convolutional-modern/googlenet.md @@ -6,7 +6,7 @@ GoogLeNet吸收了NiN中串联网络的思想,并在此基础上做了改进 这篇论文的一个重点是解决了什么样大小的卷积核最合适的问题。 毕竟,以前流行的网络使用小到$1 \times 1$,大到$11 \times 11$的卷积核。 本文的一个观点是,有时使用不同大小的卷积核组合是有利的。 -在本节中,我们将介绍一个稍微简化的GoogLeNet版本:我们省略了一些为稳定训练而添加的特殊特性,现在有了更好的训练方法,这些特性不是必要的。 +本节将介绍一个稍微简化的GoogLeNet版本:我们省略了一些为稳定训练而添加的特殊特性,现在有了更好的训练方法,这些特性不是必要的。 ## (**Inception块**) @@ -119,6 +119,40 @@ class Inception(tf.keras.Model): return tf.keras.layers.Concatenate()([p1, p2, p3, p4]) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +class Inception(nn.Layer): + # c1--c4是每条路径的输出通道数 + def __init__(self, in_channels, c1, c2, c3, c4, **kwargs): + super(Inception, self).__init__(**kwargs) + # 线路1,单1x1卷积层 + self.p1_1 = nn.Conv2D(in_channels, c1, kernel_size=1) + # 线路2,1x1卷积层后接3x3卷积层 + self.p2_1 = nn.Conv2D(in_channels, c2[0], kernel_size=1) + self.p2_2 = nn.Conv2D(c2[0], c2[1], kernel_size=3, padding=1) + # 线路3,1x1卷积层后接5x5卷积层 + self.p3_1 = nn.Conv2D(in_channels, c3[0], kernel_size=1) + self.p3_2 = nn.Conv2D(c3[0], c3[1], kernel_size=5, padding=2) + # 线路4,3x3最大池化层后接1x1卷积层 + self.p4_1 = nn.MaxPool2D(kernel_size=3, stride=1, padding=1) + self.p4_2 = nn.Conv2D(in_channels, c4, kernel_size=1) + + def forward(self, x): + p1 = F.relu(self.p1_1(x)) + p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) + p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) + p4 = F.relu(self.p4_2(self.p4_1(x))) + # 在通道维度上连结输出 + return paddle.concat(x=[p1, p2, p3, p4], axis=1) +``` + 那么为什么GoogLeNet这个网络如此有效呢? 首先我们考虑一下滤波器(filter)的组合,它们可以用各种滤波器尺寸探索图像,这意味着不同大小的滤波器可以有效地识别不同范围的图像细节。 同时,我们可以为不同的滤波器分配不同数量的参数。 @@ -155,6 +189,13 @@ def b1(): tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')]) ``` +```{.python .input} +#@tab paddle +b1 = nn.Sequential(nn.Conv2D(1, 64, kernel_size=7, stride=2, padding=3), + nn.ReLU(), + nn.MaxPool2D(kernel_size=3, stride=2,padding=1)) +``` + 第二个模块使用两个卷积层:第一个卷积层是64个通道、$1\times 1$卷积层;第二个卷积层使用将通道数量增加三倍的$3\times 3$卷积层。 这对应于Inception块中的第二条路径。 @@ -183,6 +224,15 @@ def b2(): tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')]) ``` +```{.python .input} +#@tab paddle +b2 = nn.Sequential(nn.Conv2D(64, 64, kernel_size=1), + nn.ReLU(), + nn.Conv2D(64, 192, kernel_size=3, padding=1), + nn.ReLU(), + nn.MaxPool2D(kernel_size=3, stride=2, padding=1)) +``` + 第三个模块串联两个完整的Inception块。 第一个Inception块的输出通道数为$64+128+32+32=256$,四个路径之间的输出通道数量比为$64:128:32:32=2:4:1:1$。 第二个和第三个路径首先将输入通道的数量分别减少到$96/192=1/2$和$16/192=1/12$,然后连接第二个卷积层。第二个Inception块的输出通道数增加到$128+192+96+64=480$,四个路径之间的输出通道数量比为$128:192:96:64 = 4:6:3:2$。 @@ -211,6 +261,13 @@ def b3(): tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')]) ``` +```{.python .input} +#@tab paddle +b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32), + Inception(256, 128, (128, 192), (32, 96), 64), + nn.MaxPool2D(kernel_size=3, stride=2, padding=1)) +``` + 第四模块更加复杂, 它串联了5个Inception块,其输出通道数分别是$192+208+48+64=512$、$160+224+64+64=512$、$128+256+64+64=512$、$112+288+64+64=528$和$256+320+128+128=832$。 这些路径的通道数分配和第三模块中的类似,首先是含$3×3$卷积层的第二条路径输出最多通道,其次是仅含$1×1$卷积层的第一条路径,之后是含$5×5$卷积层的第三条路径和含$3×3$最大汇聚层的第四条路径。 @@ -249,6 +306,16 @@ def b4(): tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')]) ``` +```{.python .input} +#@tab paddle +b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64), + Inception(512, 160, (112, 224), (24, 64), 64), + Inception(512, 128, (128, 256), (24, 64), 64), + Inception(512, 112, (144, 288), (32, 64), 64), + Inception(528, 256, (160, 320), (32, 128), 128), + nn.MaxPool2D(kernel_size=3, stride=2, padding=1)) +``` + 第五模块包含输出通道数为$256+320+128+128=832$和$384+384+128+128=1024$的两个Inception块。 其中每条路径通道数的分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。 需要注意的是,第五模块的后面紧跟输出层,该模块同NiN一样使用全局平均汇聚层,将每个通道的高和宽变成1。 @@ -291,6 +358,16 @@ def net(): tf.keras.layers.Dense(10)]) ``` +```{.python .input} +#@tab paddle +b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128), + Inception(832, 384, (192, 384), (48, 128), 128), + nn.AdaptiveAvgPool2D((1, 1)), + nn.Flatten()) + +net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10)) +``` + GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。 [**为了使Fashion-MNIST上的训练短小精悍,我们将输入的高和宽从224降到96**],这简化了计算。下面演示各个模块输出的形状变化。 @@ -318,6 +395,14 @@ for layer in net().layers: print(layer.__class__.__name__, 'output shape:\t', X.shape) ``` +```{.python .input} +#@tab paddle +X = paddle.rand(shape=(1, 1, 96, 96)) +for layer in net: + X = layer(X) + print(layer.__class__.__name__,'output shape:\t', X.shape) +``` + ## [**训练模型**] 和以前一样,我们使用Fashion-MNIST数据集来训练我们的模型。在训练之前,我们将图片转换为$96 \times 96$分辨率。 @@ -338,9 +423,9 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) ## 练习 1. GoogLeNet有一些后续版本。尝试实现并运行它们,然后观察实验结果。这些后续版本包括: - * 添加批量规范化层 :cite:`Ioffe.Szegedy.2015`(batch normalization),在 :numref:`sec_batch_norm`中将介绍。 - * 对Inception模块进行调整 :cite:`Szegedy.Vanhoucke.Ioffe.ea.2016`。 - * 使用标签平滑(label smoothing)进行模型正则化 :cite:`Szegedy.Vanhoucke.Ioffe.ea.2016`。 + * 添加批量规范化层 :cite:`Ioffe.Szegedy.2015`(batch normalization),在 :numref:`sec_batch_norm`中将介绍; + * 对Inception模块进行调整 :cite:`Szegedy.Vanhoucke.Ioffe.ea.2016`; + * 使用标签平滑(label smoothing)进行模型正则化 :cite:`Szegedy.Vanhoucke.Ioffe.ea.2016`; * 加入残差连接 :cite:`Szegedy.Ioffe.Vanhoucke.ea.2017`。( :numref:`sec_resnet`将介绍)。 1. 使用GoogLeNet的最小图像大小是多少? 1. 将AlexNet、VGG和NiN的模型参数大小与GoogLeNet进行比较。后两个网络架构是如何显著减少模型参数大小的? @@ -356,3 +441,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1872) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11791) +:end_tab: diff --git a/chapter_convolutional-modern/index.md b/chapter_convolutional-modern/index.md index 807c3ed6c..02b4233d5 100644 --- a/chapter_convolutional-modern/index.md +++ b/chapter_convolutional-modern/index.md @@ -1,7 +1,7 @@ # 现代卷积神经网络 :label:`chap_modern_cnn` -上一章我们介绍了卷积神经网络的基本原理,本章我们将带你了解现代的卷积神经网络架构,许多现代卷积神经网络的研究都是建立在这一章的基础上的。 +上一章我们介绍了卷积神经网络的基本原理,本章将介绍现代的卷积神经网络架构,许多现代卷积神经网络的研究都是建立在这一章的基础上的。 在本章中的每一个模型都曾一度占据主导地位,其中许多模型都是ImageNet竞赛的优胜者。ImageNet竞赛自2010年以来,一直是计算机视觉中监督学习进展的指向标。 这些模型包括: @@ -15,7 +15,7 @@ 虽然深度神经网络的概念非常简单——将神经网络堆叠在一起。但由于不同的网络架构和超参数选择,这些神经网络的性能会发生很大变化。 本章介绍的神经网络是将人类直觉和相关数学见解结合后,经过大量研究试错后的结晶。 -我们会按时间顺序介绍这些模型,在追寻历史的脉络的同时,帮助你培养对该领域发展的直觉。这将有助于你研究开发自己的架构。 +我们会按时间顺序介绍这些模型,在追寻历史的脉络的同时,帮助培养对该领域发展的直觉。这将有助于研究开发自己的架构。 例如,本章介绍的批量规范化(batch normalization)和残差网络(ResNet)为设计和训练深度神经网络提供了重要思想指导。 ```toc diff --git a/chapter_convolutional-modern/nin.md b/chapter_convolutional-modern/nin.md index fed49c68d..71b668876 100644 --- a/chapter_convolutional-modern/nin.md +++ b/chapter_convolutional-modern/nin.md @@ -67,6 +67,24 @@ def nin_block(num_channels, kernel_size, strides, padding): activation='relu')]) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn as nn + +def nin_block(in_channels, out_channels, kernel_size, strides, padding): + return nn.Sequential( + nn.Conv2D(in_channels, out_channels, kernel_size, strides, padding), + nn.ReLU(), + nn.Conv2D(out_channels, out_channels, kernel_size=1), + nn.ReLU(), + nn.Conv2D(out_channels, out_channels, kernel_size=1), + nn.ReLU()) +``` + ## [**NiN模型**] 最初的NiN网络是在AlexNet后不久提出的,显然从中得到了一些启示。 @@ -130,6 +148,22 @@ def net(): ]) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential( + nin_block(1, 96, kernel_size=11, strides=4, padding=0), + nn.MaxPool2D(3, stride=2), + nin_block(96, 256, kernel_size=5, strides=1, padding=2), + nn.MaxPool2D(3, stride=2), + nin_block(256, 384, kernel_size=3, strides=1, padding=1), + nn.MaxPool2D(3, stride=2), nn.Dropout(0.5), + # 标签类别数是10 + nin_block(384, 10, kernel_size=3, strides=1, padding=1), + nn.AdaptiveAvgPool2D((1, 1)), + # 将四维的输出转成二维的输出,其形状为(批量大小,10) + nn.Flatten()) +``` + 我们创建一个数据样本来[**查看每个块的输出形状**]。 ```{.python .input} @@ -156,6 +190,14 @@ for layer in net().layers: print(layer.__class__.__name__,'output shape:\t', X.shape) ``` +```{.python .input} +#@tab paddle +X = paddle.rand(shape=(1, 1, 224, 224)) +for layer in net: + X = layer(X) + print(layer.__class__.__name__,'output shape:\t', X.shape) +``` + ## [**训练模型**] 和以前一样,我们使用Fashion-MNIST来训练模型。训练NiN与训练AlexNet、VGG时相似。 @@ -196,3 +238,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1868) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11790) +:end_tab: diff --git a/chapter_convolutional-modern/resnet.md b/chapter_convolutional-modern/resnet.md index 9345abea3..b2924aa94 100644 --- a/chapter_convolutional-modern/resnet.md +++ b/chapter_convolutional-modern/resnet.md @@ -145,6 +145,41 @@ class Residual(tf.keras.Model): #@save return tf.keras.activations.relu(Y) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn as nn +from paddle.nn import functional as F + +class Residual(nn.Layer): #@save + def __init__(self, input_channels, num_channels, use_1x1conv=False, + strides=1): + super(Residual, self).__init__() + self.conv1 = nn.Conv2D(input_channels, num_channels, kernel_size=3, + padding=1, stride=strides) + self.conv2 = nn.Conv2D(num_channels, num_channels, kernel_size=3, + padding=1) + if use_1x1conv: + self.conv3 = nn.Conv2D(input_channels, num_channels, + kernel_size=1, stride=strides) + else: + self.conv3 = None + self.bn1 = nn.BatchNorm2D(num_channels) + self.bn2 = nn.BatchNorm2D(num_channels) + self.relu = nn.ReLU() + + def forward(self, X): + Y = F.relu(self.bn1(self.conv1(X))) + Y = self.bn2(self.conv2(Y)) + if self.conv3: + X = self.conv3(X) + Y += X + return F.relu(Y) +``` + 如 :numref:`fig_resnet_block`所示,此代码生成两种类型的网络: 一种是当`use_1x1conv=False`时,应用ReLU非线性函数之前,将输入添加到输出。 另一种是当`use_1x1conv=True`时,添加通过$1 \times 1$卷积调整通道和分辨率。 @@ -177,6 +212,14 @@ Y = blk(X) Y.shape ``` +```{.python .input} +#@tab paddle +blk = Residual(3, 3) +X = paddle.rand([4, 3, 6, 6]) +Y = blk(X) +Y.shape +``` + 我们也可以在[**增加输出通道数的同时,减半输出的高和宽**]。 ```{.python .input} @@ -197,6 +240,12 @@ blk = Residual(6, use_1x1conv=True, strides=2) blk(X).shape ``` +```{.python .input} +#@tab paddle +blk = Residual(3, 6, use_1x1conv=True, strides=2) +blk(X).shape +``` + ## [**ResNet模型**] ResNet的前两层跟之前介绍的GoogLeNet中的一样: @@ -226,6 +275,13 @@ b1 = tf.keras.models.Sequential([ tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')]) ``` +```{.python .input} +#@tab paddle +b1 = nn.Sequential(nn.Conv2D(1, 64, kernel_size=7, stride=2, padding=3), + nn.BatchNorm2D(64), nn.ReLU(), + nn.MaxPool2D(kernel_size=3, stride=2, padding=1)) +``` + GoogLeNet在后面接了4个由Inception块组成的模块。 ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。 第一个模块的通道数同输入通道数一致。 @@ -279,6 +335,21 @@ class ResnetBlock(tf.keras.layers.Layer): return X ``` +```{.python .input} +#@tab paddle +def resnet_block(input_channels, num_channels, num_residuals, + first_block=False): + blk = [] + for i in range(num_residuals): + if i == 0 and not first_block: + blk.append( + Residual(input_channels, num_channels, use_1x1conv=True, + strides=2)) + else: + blk.append(Residual(num_channels, num_channels)) + return blk +``` + 接着在ResNet加入所有残差块,这里每个模块使用2个残差块。 ```{.python .input} @@ -289,7 +360,7 @@ net.add(resnet_block(64, 2, first_block=True), ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True)) b3 = nn.Sequential(*resnet_block(64, 128, 2)) b4 = nn.Sequential(*resnet_block(128, 256, 2)) @@ -339,6 +410,13 @@ def net(): tf.keras.layers.Dense(units=10)]) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential(b1, b2, b3, b4, b5, + nn.AdaptiveAvgPool2D((1, 1)), + nn.Flatten(), nn.Linear(512, 10)) +``` + 每个模块有4个卷积层(不包括恒等映射的$1\times 1$卷积层)。 加上第一个$7\times 7$卷积层和最后一个全连接层,共有18层。 因此,这种模型通常被称为ResNet-18。 @@ -376,6 +454,14 @@ for layer in net().layers: print(layer.__class__.__name__,'output shape:\t', X.shape) ``` +```{.python .input} +#@tab paddle +X = paddle.rand(shape=(1, 1, 224, 224)) +for layer in net: + X = layer(X) + print(layer.__class__.__name__,'output shape:\t', X.shape) +``` + ## [**训练模型**] 同之前一样,我们在Fashion-MNIST数据集上训练ResNet。 @@ -398,8 +484,8 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) 1. :numref:`fig_inception`中的Inception块与残差块之间的主要区别是什么?在删除了Inception块中的一些路径之后,它们是如何相互关联的? 1. 参考ResNet论文 :cite:`He.Zhang.Ren.ea.2016`中的表1,以实现不同的变体。 -1. 对于更深层次的网络,ResNet引入了“bottleneck”架构来降低模型复杂性。请你试着去实现它。 -1. 在ResNet的后续版本中,作者将“卷积层、批量规范化层和激活层”架构更改为“批量规范化层、激活层和卷积层”架构。请你做这个改进。详见 :cite:`He.Zhang.Ren.ea.2016*1`中的图1。 +1. 对于更深层次的网络,ResNet引入了“bottleneck”架构来降低模型复杂性。请试着去实现它。 +1. 在ResNet的后续版本中,作者将“卷积层、批量规范化层和激活层”架构更改为“批量规范化层、激活层和卷积层”架构。请尝试做这个改进。详见 :cite:`He.Zhang.Ren.ea.2016*1`中的图1。 1. 为什么即使函数类是嵌套的,我们仍然要限制增加函数的复杂性呢? :begin_tab:`mxnet` @@ -413,3 +499,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1878) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11793) +:end_tab: diff --git a/chapter_convolutional-modern/vgg.md b/chapter_convolutional-modern/vgg.md index c7fcdb0d0..d8671c3ef 100644 --- a/chapter_convolutional-modern/vgg.md +++ b/chapter_convolutional-modern/vgg.md @@ -6,7 +6,7 @@ 与芯片设计中工程师从放置晶体管到逻辑元件再到逻辑块的过程类似,神经网络架构的设计也逐渐变得更加抽象。研究人员开始从单个神经元的角度思考问题,发展到整个层,现在又转向块,重复层的模式。 -使用块的想法首先出现在牛津大学的[视觉几何组(visualgeometry group)](http://www.robots.ox.ac.uk/~vgg/)的*VGG网络*中。通过使用循环和子程序,可以很容易地在任何现代深度学习框架的代码中实现这些重复的架构。 +使用块的想法首先出现在牛津大学的[视觉几何组(visual geometry group)](http://www.robots.ox.ac.uk/~vgg/)的*VGG网络*中。通过使用循环和子程序,可以很容易地在任何现代深度学习框架的代码中实现这些重复的架构。 ## (**VGG块**) @@ -73,6 +73,25 @@ def vgg_block(num_convs, num_channels): return blk ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn as nn + +def vgg_block(num_convs, in_channels, out_channels): + layers = [] + for _ in range(num_convs): + layers.append( + nn.Conv2D(in_channels, out_channels, kernel_size=3, padding=1)) + layers.append(nn.ReLU()) + in_channels = out_channels + layers.append(nn.MaxPool2D(kernel_size=2, stride=2)) + return nn.Sequential(*layers) +``` + ## [**VGG网络**] 与AlexNet、LeNet一样,VGG网络可以分为两部分:第一部分主要由卷积层和汇聚层组成,第二部分由全连接层组成。如 :numref:`fig_vgg`中所示。 @@ -148,6 +167,25 @@ def vgg(conv_arch): net = vgg(conv_arch) ``` +```{.python .input} +#@tab paddle +def vgg(conv_arch): + conv_blks = [] + in_channels = 1 + # 卷积层部分 + for (num_convs, out_channels) in conv_arch: + conv_blks.append(vgg_block(num_convs, in_channels, out_channels)) + in_channels = out_channels + + return nn.Sequential(*conv_blks, nn.Flatten(), + # 全连接层部分 + nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), + nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(), + nn.Dropout(0.5), nn.Linear(4096, 10)) + +net = vgg(conv_arch) +``` + 接下来,我们将构建一个高度和宽度为224的单通道数据样本,以[**观察每个层输出的形状**]。 ```{.python .input} @@ -174,14 +212,22 @@ for blk in net.layers: print(blk.__class__.__name__,'output shape:\t', X.shape) ``` -正如你所看到的,我们在每个块的高度和宽度减半,最终高度和宽度都为7。最后再展平表示,送入全连接层处理。 +```{.python .input} +#@tab paddle +X = paddle.randn(shape=(1, 1, 224, 224)) +for blk in net: + X = blk(X) + print(blk.__class__.__name__,'output shape:\t',X.shape) +``` + +正如从代码中所看到的,我们在每个块的高度和宽度减半,最终高度和宽度都为7。最后再展平表示,送入全连接层处理。 ## 训练模型 [**由于VGG-11比AlexNet计算量更大,因此我们构建了一个通道数较少的网络**],足够用于训练Fashion-MNIST数据集。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle ratio = 4 small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch] net = vgg(small_conv_arch) @@ -228,3 +274,7 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1865) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11789) +:end_tab: diff --git a/chapter_convolutional-neural-networks/channels.md b/chapter_convolutional-neural-networks/channels.md index a9413802a..86caaf2ae 100644 --- a/chapter_convolutional-neural-networks/channels.md +++ b/chapter_convolutional-neural-networks/channels.md @@ -5,7 +5,7 @@ 但是到目前为止,我们仅展示了单个输入和单个输出通道的简化例子。 这使得我们可以将输入、卷积核和输出看作二维张量。 -当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有$3\times h\times w$的形状。我们将这个大小为$3$的轴称为*通道*(channel)维度。在本节中,我们将更深入地研究具有多输入和多输出通道的卷积核。 +当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有$3\times h\times w$的形状。我们将这个大小为$3$的轴称为*通道*(channel)维度。本节将更深入地研究具有多输入和多输出通道的卷积核。 ## 多输入通道 @@ -34,7 +34,15 @@ import torch ``` ```{.python .input} -#@tab mxnet, pytorch +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +``` + +```{.python .input} +#@tab mxnet, pytorch, paddle def corr2d_multi_in(X, K): # 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起 return sum(d2l.corr2d(x, k) for x, k in zip(X, K)) @@ -63,7 +71,7 @@ corr2d_multi_in(X, K) ## 多输出通道 -到目前为止,不论有多少输入通道,我们还只有一个输出通道。然而,正如我们在 :numref:`subsec_why-conv-channels`中所讨论的,每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作是对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。 +到目前为止,不论有多少输入通道,我们还只有一个输出通道。然而,正如我们在 :numref:`subsec_why-conv-channels`中所讨论的,每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。 用$c_i$和$c_o$分别表示输入和输出通道的数目,并让$k_h$和$k_w$为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为$c_i\times k_h\times k_w$的卷积核张量,这样卷积核的形状是$c_o\times c_i\times k_h\times k_w$。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。 @@ -105,7 +113,7 @@ $1 \times 1$卷积,即$k_h = k_w = 1$,看起来似乎没有多大意义。 :numref:`fig_conv_1x1`展示了使用$1\times 1$卷积核与$3$个输入通道和$2$个输出通道的互相关计算。 这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。 -我们可以将$1\times 1$卷积层看作是在每个像素位置应用的全连接层,以$c_i$个输入值转换为$c_o$个输出值。 +我们可以将$1\times 1$卷积层看作在每个像素位置应用的全连接层,以$c_i$个输入值转换为$c_o$个输出值。 因为这仍然是一个卷积层,所以跨像素的权重是一致的。 同时,$1\times 1$卷积层需要的权重维度为$c_o\times c_i$,再额外加上一个偏置。 @@ -130,7 +138,7 @@ def corr2d_multi_in_out_1x1(X, K): 当执行$1\times 1$卷积运算时,上述函数相当于先前实现的互相关函数`corr2d_multi_in_out`。让我们用一些样本数据来验证这一点。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle X = d2l.normal(0, 1, (3, 3, 3)) K = d2l.normal(0, 1, (2, 3, 1, 1)) ``` @@ -181,3 +189,7 @@ assert float(d2l.reduce_sum(d2l.abs(Y1 - Y2))) < 1e-6 :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1853) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11785) +:end_tab: diff --git a/chapter_convolutional-neural-networks/conv-layer.md b/chapter_convolutional-neural-networks/conv-layer.md index 16accf59b..7a4069003 100644 --- a/chapter_convolutional-neural-networks/conv-layer.md +++ b/chapter_convolutional-neural-networks/conv-layer.md @@ -48,7 +48,16 @@ from torch import nn ``` ```{.python .input} -#@tab mxnet, pytorch +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + +```{.python .input} +#@tab mxnet, pytorch, paddle def corr2d(X, K): #@save """计算二维互相关运算""" h, w = K.shape @@ -132,6 +141,18 @@ class Conv2D(tf.keras.layers.Layer): return corr2d(inputs, self.weight) + self.bias ``` +```{.python .input} +#@tab paddle +class Conv2D(nn.Layer): + def __init__(self, kernel_size): + super().__init__() + self.weight = paddle.ParamAttr(paddle.rand(kernel_size)) + self.bias = paddle.ParamAttr(paddle.zeros(1)) + + def forward(self, x): + return corr2d(x, self.weight) + self.bias +``` + 高度和宽度分别为$h$和$w$的卷积核可以被称为$h \times w$卷积或$h \times w$卷积核。 我们也将带有$h \times w$卷积核的卷积层称为$h \times w$卷积层。 @@ -141,7 +162,7 @@ class Conv2D(tf.keras.layers.Layer): 首先,我们构造一个$6\times 8$像素的黑白图像。中间四列为黑色($0$),其余像素为白色($1$)。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle X = d2l.ones((6, 8)) X[:, 2:6] = 0 X @@ -258,6 +279,29 @@ for i in range(10): print(f'epoch {i+1}, loss {tf.reduce_sum(l):.3f}') ``` +```{.python .input} +#@tab paddle +# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核 +conv2d = nn.Conv2D(1, 1, kernel_size=(1, 2)) + +# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度), +# 其中批量大小和通道数都为1 +X = X.reshape((1, 1, 6, 8)) +Y = Y.reshape((1, 1, 6, 7)) +lr = 3e-2 # 学习率 + +for i in range(10): + Y_hat = conv2d(X) + l = (Y_hat - Y) ** 2 + conv2d.clear_gradients() + l.sum().backward() + # 迭代卷积核 + with paddle.no_grad(): + conv2d.weight[:] -= lr * conv2d.weight.grad + if (i + 1) % 2 == 0: + print(f'epoch {i+1}, loss {l.sum().item():.3f}') +``` + 在$10$次迭代之后,误差已经降到足够低。现在我们来看看我们[**所学的卷积核的权重张量**]。 ```{.python .input} @@ -274,7 +318,12 @@ d2l.reshape(conv2d.weight.data, (1, 2)) d2l.reshape(conv2d.get_weights()[0], (1, 2)) ``` -细心的你一定会发现,我们学习到的卷积核权重非常接近我们之前定义的卷积核`K`。 +```{.python .input} +#@tab paddle +d2l.reshape(conv2d.weight, (1, 2)) +``` + +细心的读者一定会发现,我们学习到的卷积核权重非常接近我们之前定义的卷积核`K`。 ## 互相关和卷积 @@ -317,7 +366,7 @@ d2l.reshape(conv2d.get_weights()[0], (1, 2)) 1. 如果转置`K`会发生什么? 1. 在我们创建的`Conv2D`自动求导时,有什么错误消息? 1. 如何通过改变输入张量和卷积核张量,将互相关运算表示为矩阵乘法? -1. 手工设计一些卷积核: +1. 手工设计一些卷积核。 1. 二阶导数的核的形式是什么? 1. 积分的核的形式是什么? 1. 得到$d$次导数的最小核的大小是多少? @@ -333,3 +382,7 @@ d2l.reshape(conv2d.get_weights()[0], (1, 2)) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1847) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11783) +:end_tab: diff --git a/chapter_convolutional-neural-networks/lenet.md b/chapter_convolutional-neural-networks/lenet.md index eea38e70d..d357bd249 100644 --- a/chapter_convolutional-neural-networks/lenet.md +++ b/chapter_convolutional-neural-networks/lenet.md @@ -7,7 +7,7 @@ 而现在,我们已经掌握了卷积层的处理方法,我们可以在图像中保留空间结构。 同时,用卷积层代替全连接层的另一个好处是:模型更简洁、所需的参数更少。 -在本节中,我们将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。 +本节将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。 这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像 :cite:`LeCun.Bottou.Bengio.ea.1998`中的手写数字。 当时,Yann LeCun发表了第一篇通过反向传播成功训练卷积神经网络的研究,这项工作代表了十多年来神经网络研究开发的成果。 @@ -31,7 +31,7 @@ LeNet被广泛用于自动取款机(ATM)机中,帮助识别处理支票的 为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。LeNet的稠密块有三个全连接层,分别有120、84和10个输出。因为我们在执行分类任务,所以输出层的10维对应于最后输出结果的数量。 -通过下面的LeNet代码,你会相信用深度学习框架实现此类模型非常简单。我们只需要实例化一个`Sequential`块并将需要的层连接在一起。 +通过下面的LeNet代码,可以看出用深度学习框架实现此类模型非常简单。我们只需要实例化一个`Sequential`块并将需要的层连接在一起。 ```{.python .input} from d2l import mxnet as d2l @@ -87,6 +87,25 @@ def net(): tf.keras.layers.Dense(10)]) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn, optimizer + +net = nn.Sequential( + nn.Conv2D(1, 6, kernel_size=5, padding=2), nn.Sigmoid(), + nn.AvgPool2D(kernel_size=2, stride=2), + nn.Conv2D(6, 16, kernel_size=5), nn.Sigmoid(), + nn.AvgPool2D(kernel_size=2, stride=2), + nn.Flatten(), + nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), + nn.Linear(120, 84), nn.Sigmoid(), + nn.Linear(84, 10)) +``` + 我们对原始模型做了一点小改动,去掉了最后一层的高斯激活。除此之外,这个网络与最初的LeNet-5一致。 下面,我们将一个大小为$28 \times 28$的单通道(黑白)图像通过LeNet。通过在每一层打印输出的形状,我们可以[**检查模型**],以确保其操作与我们期望的 :numref:`img_lenet_vert`一致。 @@ -118,6 +137,14 @@ for layer in net().layers: print(layer.__class__.__name__, 'output shape: \t', X.shape) ``` +```{.python .input} +#@tab paddle +X = paddle.rand((1, 1, 28, 28), 'float32') +for layer in net: + X = layer(X) + print(layer.__class__.__name__, 'output shape: \t', X.shape) +``` + 请注意,在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了。 第一个卷积层使用2个像素的填充,来补偿$5 \times 5$卷积核导致的特征减少。 相反,第二个卷积层没有填充,因此高度和宽度都减少了4个像素。 @@ -135,7 +162,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size) ``` 虽然卷积神经网络的参数较少,但与深度的多层感知机相比,它们的计算成本仍然很高,因为每个参数都参与更多的乘法。 -如果你有机会使用GPU,可以用它加快训练。 +通过使用GPU,可以用它加快训练。 :begin_tab:`mxnet, pytorch` 为了进行评估,我们需要[**对**] :numref:`sec_softmax_scratch`中描述的(**`evaluate_accuracy`函数进行轻微的修改**)。 @@ -176,6 +203,29 @@ def evaluate_accuracy_gpu(net, data_iter, device=None): #@save return metric[0] / metric[1] ``` +```{.python .input} +#@tab paddle +def evaluate_accuracy_gpu(net, data_iter, device=None): #@save + """使用GPU计算模型在数据集上的精度""" + if isinstance(net, nn.Layer): + net.eval() # 设置为评估模式 + if not device: + device = next(iter(net.parameters())).place + paddle.set_device("gpu:{}".format(str(device)[-2])) + # 正确预测的数量,总预测的数量 + metric = d2l.Accumulator(2) + with paddle.no_grad(): + for X, y in data_iter: + if isinstance(X, list): + # BERT微调所需的 + X = [paddle.to_tensor(x, place=device) for x in X] + else: + X = paddle.to_tensor(X, place=device) + y = paddle.to_tensor(y, place=device) + metric.add(d2l.accuracy(net(X), y), d2l.size(y)) + return metric[0] / metric[1] +``` + [**为了使用GPU,我们还需要一点小改动**]。 与 :numref:`sec_softmax_scratch`中定义的`train_epoch_ch3`不同,在进行正向和反向传播之前,我们需要将每一小批量数据移动到我们指定的设备(例如GPU)上。 @@ -315,6 +365,50 @@ def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): return net ``` +```{.python .input} +#@tab paddle +#@save +def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): + """用GPU训练模型(在第六章定义)""" + def init_weights(m): + if type(m) == nn.Linear or type(m) == nn.Conv2D: + nn.initializer.XavierUniform(m.weight) + net.apply(init_weights) + print('training on', device) + net.to(device) + optimizer = paddle.optimizer.SGD(learning_rate=lr, parameters=net.parameters()) + loss = nn.CrossEntropyLoss() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'train acc', 'test acc']) + timer, num_batches = d2l.Timer(), len(train_iter) + for epoch in range(num_epochs): + # 训练损失之和,训练准确率之和,样本数 + metric = d2l.Accumulator(3) + net.train() + for i, (X, y) in enumerate(train_iter): + timer.start() + optimizer.clear_grad() + X, y = paddle.to_tensor(X, place=device), paddle.to_tensor(y, place=device) + y_hat = net(X) + l = loss(y_hat, y) + l.backward() + optimizer.step() + with paddle.no_grad(): + metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0]) + timer.stop() + train_l = metric[0] / metric[2] + train_acc = metric[1] / metric[2] + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (train_l, train_acc, None)) + test_acc = evaluate_accuracy_gpu(net, test_iter) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, ' + f'test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(device)}') +``` + 现在,我们[**训练和评估LeNet-5模型**]。 ```{.python .input} @@ -355,3 +449,7 @@ train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1859) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11787) +:end_tab: diff --git a/chapter_convolutional-neural-networks/padding-and-strides.md b/chapter_convolutional-neural-networks/padding-and-strides.md index 21a3f12f2..e078ebaaa 100644 --- a/chapter_convolutional-neural-networks/padding-and-strides.md +++ b/chapter_convolutional-neural-networks/padding-and-strides.md @@ -6,7 +6,7 @@ 因此,卷积的输出形状取决于输入形状和卷积核的形状。 还有什么因素会影响输出的大小呢?本节我们将介绍*填充*(padding)和*步幅*(stride)。假设以下情景: -有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于$1$所导致的。比如,一个$240 \times 240$像素的图像,经过$10$层$5 \times 5$的卷积后,将减少到$200 \times 200$像素。如此一来,原始图像的边界丢失了许多有用信息。而*填充*是解决此问题最有效的方法。 +有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于$1$所导致的。比如,一个$240 \times 240$像素的图像,经过$10$层$5 \times 5$的卷积后,将减少到$200 \times 200$像素。如此一来,原始图像的边界丢失了许多有用信息。而*填充*是解决此问题最有效的方法; 有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。*步幅*则可以在这类情况下提供帮助。 ## 填充 @@ -102,6 +102,27 @@ X = tf.random.uniform(shape=(8, 8)) comp_conv2d(conv2d, X).shape ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +from paddle import nn + +# 为了方便起见,我们定义了一个计算卷积层的函数。 +# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数 +def comp_conv2d(conv2d, X): + # 这里的(1,1)表示批量大小和通道数都是1 + X = paddle.reshape(X, [1, 1] + X.shape) + Y = conv2d(X) + return Y.reshape(Y.shape[2:]) # 排除不关心的前两维:批量和通道 + +# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列 +conv2d = nn.Conv2D(in_channels=1, out_channels=1, kernel_size=3, padding=1) +X = paddle.rand((8, 8)) +comp_conv2d(conv2d, X).shape +``` + 当卷积核的高度和宽度不同时,我们可以[**填充不同的高度和宽度**],使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。 ```{.python .input} @@ -121,6 +142,12 @@ conv2d = tf.keras.layers.Conv2D(1, kernel_size=(5, 3), padding='same') comp_conv2d(conv2d, X).shape ``` +```{.python .input} +#@tab paddle +conv2d = nn.Conv2D(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1)) +comp_conv2d(conv2d, X).shape +``` + ## 步幅 在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 @@ -162,6 +189,12 @@ conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same', strides=2) comp_conv2d(conv2d, X).shape ``` +```{.python .input} +#@tab paddle +conv2d = nn.Conv2D(1, 1, kernel_size=3, padding=1, stride=2) +comp_conv2d(conv2d, X).shape +``` + 接下来,看(**一个稍微复杂的例子**)。 ```{.python .input} @@ -182,6 +215,11 @@ conv2d = tf.keras.layers.Conv2D(1, kernel_size=(3,5), padding='valid', comp_conv2d(conv2d, X).shape ``` +```{.python .input} +#@tab paddle +conv2d = nn.Conv2D(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4)) +comp_conv2d(conv2d, X).shape +``` 为了简洁起见,当输入高度和宽度两侧的填充数量分别为$p_h$和$p_w$时,我们称之为填充$(p_h, p_w)$。当$p_h = p_w = p$时,填充是$p$。同理,当高度和宽度上的步幅分别为$s_h$和$s_w$时,我们称之为步幅$(s_h, s_w)$。特别地,当$s_h = s_w = s$时,我们称步幅为$s$。默认情况下,填充为0,步幅为1。在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有$p_h = p_w$和$s_h = s_w$。 ## 小结 @@ -208,3 +246,7 @@ comp_conv2d(conv2d, X).shape :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1850) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11784) +:end_tab: diff --git a/chapter_convolutional-neural-networks/pooling.md b/chapter_convolutional-neural-networks/pooling.md index 9d83d7342..e82895926 100644 --- a/chapter_convolutional-neural-networks/pooling.md +++ b/chapter_convolutional-neural-networks/pooling.md @@ -55,7 +55,16 @@ from torch import nn ``` ```{.python .input} -#@tab mxnet, pytorch +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + +```{.python .input} +#@tab mxnet, pytorch, paddle def pool2d(X, pool_size, mode='max'): p_h, p_w = pool_size Y = d2l.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)) @@ -122,6 +131,12 @@ X = d2l.reshape(d2l.arange(16, dtype=d2l.float32), (1, 4, 4, 1)) X ``` +```{.python .input} +#@tab paddle +X = paddle.arange(16, dtype="float32").reshape((1, 1, 4, 4)) +X +``` + 默认情况下,(**深度学习框架中的步幅与汇聚窗口的大小相同**)。 因此,如果我们使用形状为`(3, 3)`的汇聚窗口,那么默认情况下,我们得到的步幅形状为`(3, 3)`。 @@ -143,6 +158,12 @@ pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3]) pool2d(X) ``` +```{.python .input} +#@tab paddle +pool2d = nn.MaxPool2D(3, stride=3) +pool2d(X) +``` + [**填充和步幅可以手动设定**]。 ```{.python .input} @@ -165,6 +186,12 @@ pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='valid', pool2d(X_padded) ``` +```{.python .input} +#@tab paddle +pool2d = nn.MaxPool2D(3, padding=1, stride=2) +pool2d(X) +``` + :begin_tab:`mxnet` 当然,我们可以设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度。 :end_tab: @@ -177,6 +204,10 @@ pool2d(X_padded) 当然,我们可以设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度。 :end_tab: +:begin_tab:`paddle` +当然,我们可以设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度。 +:end_tab: + ```{.python .input} pool2d = nn.MaxPool2D((2, 3), padding=(0, 1), strides=(2, 3)) pool2d(X) @@ -197,6 +228,12 @@ pool2d = tf.keras.layers.MaxPool2D(pool_size=[2, 3], padding='valid', pool2d(X_padded) ``` +```{.python .input} +#@tab paddle +pool2d = nn.MaxPool2D((2, 3), padding=(0, 1), stride=(2, 3)) +pool2d(X) +``` + ## 多个通道 在处理多通道输入数据时,[**汇聚层在每个输入通道上单独运算**],而不是像卷积层一样在通道上对输入进行汇总。 @@ -209,7 +246,7 @@ pool2d(X_padded) :end_tab: ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle X = d2l.concat((X, X + 1), 1) X ``` @@ -241,6 +278,12 @@ pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='valid', pool2d(X_padded) ``` +```{.python .input} +#@tab paddle +pool2d = paddle.nn.MaxPool2D(3, padding=1, stride=2) +pool2d(X) +``` + :begin_tab:`tensorflow` 请注意,上面的输出乍一看似乎有所不同,但MXNet和PyTorch的结果从数值上是相同的。 不同之处在于维度,垂直读取输出会产生与其他实现相同的输出。 @@ -256,8 +299,8 @@ pool2d(X_padded) ## 练习 -1. 你能将平均汇聚层作为卷积层的特殊情况实现吗? -1. 你能将最大汇聚层作为卷积层的特殊情况实现吗? +1. 尝试将平均汇聚层作为卷积层的特殊情况实现。 +1. 尝试将最大汇聚层作为卷积层的特殊情况实现。 1. 假设汇聚层的输入大小为$c\times h\times w$,则汇聚窗口的形状为$p_h\times p_w$,填充为$(p_h, p_w)$,步幅为$(s_h, s_w)$。这个汇聚层的计算成本是多少? 1. 为什么最大汇聚层和平均汇聚层的工作方式不同? 1. 我们是否需要最小汇聚层?可以用已知函数替换它吗? @@ -274,3 +317,7 @@ pool2d(X_padded) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1856) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11786) +:end_tab: diff --git a/chapter_convolutional-neural-networks/why-conv.md b/chapter_convolutional-neural-networks/why-conv.md index 038388b7a..e96d6eca1 100644 --- a/chapter_convolutional-neural-networks/why-conv.md +++ b/chapter_convolutional-neural-networks/why-conv.md @@ -17,7 +17,7 @@ ## 不变性 -想象一下,假设你想从一张图片中找到某个物体。 +想象一下,假设我们想从一张图片中找到某个物体。 合理的假设是:无论哪种方法找到这个物体,都应该和物体的位置无关。 理想情况下,我们的系统应该能够利用常识:猪通常不在天上飞,飞机通常不在水里游泳。 但是,如果一只猪出现在图片顶部,我们还是应该认出它。 @@ -32,7 +32,7 @@ :width:`400px` :label:`img_waldo` -现在,我们将上述想法总结一下,从而帮助我们设计适合于计算机视觉的神经网络架构: +现在,我们将上述想法总结一下,从而帮助我们设计适合于计算机视觉的神经网络架构。 1. *平移不变性*(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。 1. *局部性*(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。 @@ -87,7 +87,7 @@ $$[\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Del $$(f * g)(\mathbf{x}) = \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}.$$ 也就是说,卷积是当把一个函数“翻转”并移位$\mathbf{x}$时,测量$f$和$g$之间的重叠。 -当为离散对象时,积分就变成求和。例如:对于由索引为$\mathbb{Z}$的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义: +当为离散对象时,积分就变成求和。例如,对于由索引为$\mathbb{Z}$的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义: $$(f * g)(i) = \sum_a f(a) g(i-a).$$ @@ -111,14 +111,14 @@ $$(f * g)(i, j) = \sum_a\sum_b f(a, b) g(i-a, j-b).$$ 然而这种方法有一个问题:我们忽略了图像一般包含三个通道/三种原色(红色、绿色和蓝色)。 实际上,图像不是二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如包含$1024 \times 1024 \times 3$个像素。 -前两个轴与像素的空间位置有关,而第三个轴可以看作是每个像素的多维表示。 +前两个轴与像素的空间位置有关,而第三个轴可以看作每个像素的多维表示。 因此,我们将$\mathsf{X}$索引为$[\mathsf{X}]_{i, j, k}$。由此卷积相应地调整为$[\mathsf{V}]_{a,b,c}$,而不是$[\mathbf{V}]_{a,b}$。 此外,由于输入图像是三维的,我们的隐藏表示$\mathsf{H}$也最好采用三维张量。 换句话说,对于每一个空间位置,我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。 因此,我们可以把隐藏表示想象为一系列具有二维张量的*通道*(channel)。 这些通道有时也被称为*特征映射*(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。 -直观上你可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。 +直观上可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。 为了支持输入$\mathsf{X}$和隐藏表示$\mathsf{H}$中的多个通道,我们可以在$\mathsf{V}$中添加第四个坐标,即$[\mathsf{V}]_{a, b, c, d}$。综上所述, diff --git a/chapter_deep-learning-computation/custom-layer.md b/chapter_deep-learning-computation/custom-layer.md index 338eb1d86..ac773e2f2 100644 --- a/chapter_deep-learning-computation/custom-layer.md +++ b/chapter_deep-learning-computation/custom-layer.md @@ -3,13 +3,13 @@ 深度学习成功背后的一个因素是神经网络的灵活性: 我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。 例如,研究人员发明了专门用于处理图像、文本、序列数据和执行动态规划的层。 -未来,你会遇到或要自己发明一个现在在深度学习框架中还不存在的层。 -在这些情况下,你必须构建自定义层。在本节中,我们将向你展示如何构建。 +有时我们会遇到或要自己发明一个现在在深度学习框架中还不存在的层。 +在这些情况下,必须构建自定义层。本节将展示如何构建自定义层。 ## 不带参数的层 首先,我们(**构造一个没有任何参数的自定义层**)。 -如果你还记得我们在 :numref:`sec_model_construction`对块的介绍, +回忆一下在 :numref:`sec_model_construction`对块的介绍, 这应该看起来很眼熟。 下面的`CenteredLayer`类要从其输入中减去均值。 要构建它,我们只需继承基础层类并实现前向传播功能。 @@ -53,6 +53,22 @@ class CenteredLayer(tf.keras.Model): return inputs - tf.reduce_mean(inputs) ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +from paddle import nn +import paddle.nn.functional as F + +class CenteredLayer(nn.Layer): + def __init__(self): + super().__init__() + + def forward(self, X): + return X - X.mean() +``` + 让我们向该层提供一些数据,验证它是否能按预期工作。 ```{.python .input} @@ -72,6 +88,12 @@ layer = CenteredLayer() layer(tf.constant([1, 2, 3, 4, 5])) ``` +```{.python .input} +#@tab paddle +layer = CenteredLayer() +layer(paddle.to_tensor([1, 2, 3, 4, 5], dtype='float32')) +``` + 现在,我们可以[**将层作为组件合并到更复杂的模型中**]。 ```{.python .input} @@ -81,7 +103,7 @@ net.initialize() ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net = nn.Sequential(nn.Linear(8, 128), CenteredLayer()) ``` @@ -110,6 +132,12 @@ Y = net(tf.random.uniform((4, 8))) tf.reduce_mean(Y) ``` +```{.python .input} +#@tab paddle +Y = net(paddle.rand([4, 8])) +Y.mean() +``` + ## [**带参数的层**] 以上我们知道了如何定义简单的层,下面我们继续定义具有参数的层, @@ -167,6 +195,20 @@ class MyDense(tf.keras.Model): linear = tf.matmul(X, self.weight) + self.bias return tf.nn.relu(linear) ``` + +```{.python .input} +#@tab paddle +class MyLinear(nn.Layer): + def __init__(self, in_units, units): + super().__init__() + self.weight = paddle.create_parameter(shape=(in_units, units), dtype='float32') + self.bias = paddle.create_parameter(shape=(units,), dtype='float32') + + def forward(self, X): + linear = paddle.matmul(X, self.weight) + self.bias + return F.relu(linear) +``` + :begin_tab:`mxnet, tensorflow` 接下来,我们实例化`MyDense`类并访问其模型参数。 :end_tab: @@ -175,13 +217,17 @@ class MyDense(tf.keras.Model): 接下来,我们实例化`MyLinear`类并访问其模型参数。 :end_tab: +:begin_tab:`paddle` +接下来,我们实例化`MyLinear`类并访问其模型参数。 +:end_tab: + ```{.python .input} dense = MyDense(units=3, in_units=5) dense.params ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle linear = MyLinear(5, 3) linear.weight ``` @@ -210,6 +256,11 @@ linear(torch.rand(2, 5)) dense(tf.random.uniform((2, 5))) ``` +```{.python .input} +#@tab paddle +linear(paddle.randn([2, 5])) +``` + 我们还可以(**使用自定义层构建模型**),就像使用内置的全连接层一样使用自定义层。 ```{.python .input} @@ -232,6 +283,12 @@ net = tf.keras.models.Sequential([MyDense(8), MyDense(1)]) net(tf.random.uniform((2, 64))) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1)) +net(paddle.rand([2, 64])) +``` + ## 小结 * 我们可以通过基本层类设计自定义层。这允许我们定义灵活的新层,其行为与深度学习框架中的任何现有层不同。 @@ -254,3 +311,7 @@ net(tf.random.uniform((2, 64))) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1836) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11780) +:end_tab: diff --git a/chapter_deep-learning-computation/deferred-init.md b/chapter_deep-learning-computation/deferred-init.md index 88a946b54..1c8a28082 100644 --- a/chapter_deep-learning-computation/deferred-init.md +++ b/chapter_deep-learning-computation/deferred-init.md @@ -7,7 +7,7 @@ * 我们添加层时没有指定前一层的输出维度。 * 我们在初始化参数时,甚至没有足够的信息来确定模型应该包含多少参数。 -你可能会对我们的代码能运行感到惊讶。 +有些读者可能会对我们的代码能运行感到惊讶。 毕竟,深度学习框架无法判断网络的输入维度是什么。 这里的诀窍是框架的*延后初始化*(defers initialization), 即直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小。 @@ -112,9 +112,9 @@ net(X) ## 练习 -1. 如果你指定了第一层的输入尺寸,但没有指定后续层的尺寸,会发生什么?是否立即进行初始化? +1. 如果指定了第一层的输入尺寸,但没有指定后续层的尺寸,会发生什么?是否立即进行初始化? 1. 如果指定了不匹配的维度会发生什么? -1. 如果输入具有不同的维度,你需要做什么?提示:查看参数绑定的相关内容。 +1. 如果输入具有不同的维度,需要做什么?提示:查看参数绑定的相关内容。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/5770) @@ -127,3 +127,7 @@ net(X) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1833) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11779) +:end_tab: diff --git a/chapter_deep-learning-computation/index.md b/chapter_deep-learning-computation/index.md index 41ef4b302..9d345b14b 100644 --- a/chapter_deep-learning-computation/index.md +++ b/chapter_deep-learning-computation/index.md @@ -20,7 +20,7 @@ 在本章中,我们将深入探索深度学习计算的关键组件, 即模型构建、参数访问与初始化、设计自定义层和块、将模型读写到磁盘, 以及利用GPU实现显著的加速。 -这些知识将使你从深度学习“基础用户”变为“高级用户”。 +这些知识将使读者从深度学习“基础用户”变为“高级用户”。 虽然本章不介绍任何新的模型或数据集, 但后面的高级模型章节在很大程度上依赖于本章的知识。 diff --git a/chapter_deep-learning-computation/model-construction.md b/chapter_deep-learning-computation/model-construction.md index 8499af677..5a9de5df5 100644 --- a/chapter_deep-learning-computation/model-construction.md +++ b/chapter_deep-learning-computation/model-construction.md @@ -94,6 +94,20 @@ X = tf.random.uniform((2, 20)) net(X) ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +from paddle import nn +from paddle.nn import functional as F + +net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) + +X = paddle.rand([2, 20]) +net(X) +``` + :begin_tab:`mxnet` 在这个例子中,我们通过实例化`nn.Sequential`来构建我们的模型, 返回的对象赋给`net`变量。 @@ -140,10 +154,24 @@ net(X) 这是通过Block类的`__call__`函数实现的一个Python技巧。 :end_tab: +:begin_tab:`paddle` +在这个例子中,我们通过实例化`nn.Sequential`来构建我们的模型, +层的执行顺序是作为参数传递的。 +简而言之,(**`nn.Sequential`定义了一种特殊的`Layer`**), +即在PaddlePaddle中表示一个块的类, +它维护了一个由`Layer`组成的有序列表。 +注意,两个全连接层都是`Linear`类的实例, +`Linear`类本身就是`Layer`的子类。 +另外,到目前为止,我们一直在通过`net(X)`调用我们的模型来获得模型的输出。 +这实际上是`net.__call__(X)`的简写。 +这个前向传播函数非常简单: +它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。 +:end_tab: + ## [**自定义块**] 要想直观地了解块是如何工作的,最简单的方法就是自己实现一个。 -在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能: +在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能。 :begin_tab:`mxnet, tensorflow` 1. 将输入数据作为其前向传播函数的参数。 @@ -153,7 +181,7 @@ net(X) 1. 根据需要初始化模型参数。 :end_tab: -:begin_tab:`pytorch` +:begin_tab:`pytorch, paddle` 1. 将输入数据作为其前向传播函数的参数。 1. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。 1. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。 @@ -216,6 +244,23 @@ class MLP(tf.keras.Model): return self.out(self.hidden((X))) ``` +```{.python .input} +#@tab paddle +class MLP(nn.Layer): + # 用模型参数声明层。这里,我们声明两个全连接的层 + def __init__(self): + # 调用`MLP`的父类Layer的构造函数来执行必要的初始化。 + # 这样,在类实例化时也可以指定其他函数参数,例如模型参数`params`(稍后将介绍) + super().__init__() + self.hidden = nn.Linear(20, 256) # 隐藏层 + self.out = nn.Linear(256, 10) # 输出层 + + # 定义模型的正向传播,即如何根据输入`X`返回所需的模型输出 + def forward(self, X): + # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。 + return self.out(F.relu(self.hidden(X))) +``` + 我们首先看一下前向传播函数,它以`X`作为输入, 计算带有激活函数的隐藏表示,并输出其未规范化的输出值。 在这个`MLP`实现中,两个层都是实例变量。 @@ -243,7 +288,7 @@ net(X) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net = MLP() net(X) ``` @@ -267,7 +312,7 @@ net(X) 为了构建我们自己的简化的`MySequential`, 我们只需要定义两个关键函数: -1. 一种将块逐个追加到列表中的函数。 +1. 一种将块逐个追加到列表中的函数; 1. 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。 下面的`MySequential`类提供了与默认`Sequential`类相同的功能。 @@ -321,9 +366,30 @@ class MySequential(tf.keras.Model): return X ``` +```{.python .input} +#@tab paddle +class MySequential(nn.Layer): + def __init__(self, *layers): + super(MySequential, self).__init__() + # 如果传入的是一个tuple + if len(layers) > 0 and isinstance(layers[0], tuple): + for name, layer in layers: + # add_sublayer方法会将layer添加到self._sub_layers(一个tuple) + self.add_sublayer(name, layer) + else: + for idx, layer in enumerate(layers): + self.add_sublayer(str(idx), layer) + + def forward(self, X): + # OrderedDict保证了按照成员添加的顺序遍历它们 + for layer in self._sub_layers.values(): + X = layer(X) + return X +``` + :begin_tab:`mxnet` `add`函数向有序字典`_children`添加一个块。 -你可能会好奇为什么每个Gluon中的`Block`都有一个`_children`属性? +读者可能会好奇为什么每个Gluon中的`Block`都有一个`_children`属性? 以及为什么我们使用它而不是自己定义一个Python列表? 简而言之,`_children`的主要优点是: 在块的参数初始化过程中, @@ -332,13 +398,22 @@ Gluon知道在`_children`字典中查找需要初始化参数的子块。 :begin_tab:`pytorch` `__init__`函数将每个模块逐个添加到有序字典`_modules`中。 -你可能会好奇为什么每个`Module`都有一个`_modules`属性? +读者可能会好奇为什么每个`Module`都有一个`_modules`属性? 以及为什么我们使用它而不是自己定义一个Python列表? 简而言之,`_modules`的主要优点是: 在模块的参数初始化过程中, 系统知道在`_modules`字典中查找需要初始化参数的子块。 :end_tab: +:begin_tab:`paddle` +`__init__`函数将每个模块逐个添加到有序字典`_sub_layers`中。 +你可能会好奇为什么每个`Layer`都有一个`_sub_layers`属性? +以及为什么我们使用它而不是自己定义一个Python列表? +简而言之,`_sub_layers`的主要优点是: +在模块的参数初始化过程中, +系统知道在`_sub_layers`字典中查找需要初始化参数的子块。 +:end_tab: + 当`MySequential`的前向传播函数被调用时, 每个添加的块都按照它们被添加的顺序执行。 现在可以使用我们的`MySequential`类重新实现多层感知机。 @@ -352,7 +427,7 @@ net(X) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) net(X) ``` @@ -453,6 +528,27 @@ class FixedHiddenMLP(tf.keras.Model): return tf.reduce_sum(X) ``` +```{.python .input} +#@tab paddle +class FixedHiddenMLP(nn.Layer): + def __init__(self): + super().__init__() + # 不计算梯度的随机权重参数。因此其在训练期间保持不变。 + self.rand_weight = paddle.rand([20, 20]) + self.linear = nn.Linear(20, 20) + + def forward(self, X): + X = self.linear(X) + # 使用创建的常量参数以及relu和mm函数。 + X = F.relu(paddle.tensor.mm(X, self.rand_weight) + 1) + # 复用全连接层。这相当于两个全连接层共享参数。 + X = self.linear(X) + # 控制流 + while X.abs().sum() > 1: + X /= 2 + return X.sum() +``` + 在这个`FixedHiddenMLP`模型中,我们实现了一个隐藏层, 其权重(`self.rand_weight`)在实例化时被随机初始化,之后为常量。 这个权重不是一个模型参数,因此它永远不会被反向传播更新。 @@ -463,7 +559,7 @@ class FixedHiddenMLP(tf.keras.Model): 将输出向量除以$2$,直到它满足条件为止。 最后,模型返回了`X`中所有项的和。 注意,此操作可能不会常用于在任何实际任务中, -我们只是向你展示如何将任意代码集成到神经网络计算的流程中。 +我们只展示如何将任意代码集成到神经网络计算的流程中。 ```{.python .input} net = FixedHiddenMLP() @@ -472,7 +568,7 @@ net(X) ``` ```{.python .input} -#@tab pytorch, tensorflow +#@tab pytorch, tensorflow, paddle net = FixedHiddenMLP() net(X) ``` @@ -534,10 +630,26 @@ chimera.add(FixedHiddenMLP()) chimera(X) ``` +```{.python .input} +#@tab paddle +class NestMLP(nn.Layer): + def __init__(self): + super().__init__() + self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), + nn.Linear(64, 32), nn.ReLU()) + self.linear = nn.Linear(32, 16) + + def forward(self, X): + return self.linear(self.net(X)) + +chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP()) +chimera(X) +``` + ## 效率 :begin_tab:`mxnet` -你可能会开始担心操作效率的问题。 +读者可能会开始担心操作效率的问题。 毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 代码执行和许多其他的Python代码。 Python的问题[全局解释器锁](https://wiki.python.org/moin/GlobalInterpreterLock) @@ -554,7 +666,7 @@ Gluon运行时记录正在发生的事情,以及下一次它将对Python调用 :end_tab: :begin_tab:`pytorch` -你可能会开始担心操作效率的问题。 +读者可能会开始担心操作效率的问题。 毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 代码执行和许多其他的Python代码。 Python的问题[全局解释器锁](https://wiki.python.org/moin/GlobalInterpreterLock) @@ -563,6 +675,15 @@ Python的问题[全局解释器锁](https://wiki.python.org/moin/GlobalInterpret :end_tab: :begin_tab:`tensorflow` +读者可能会开始担心操作效率的问题。 +毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 +代码执行和许多其他的Python代码。 +Python的问题[全局解释器锁](https://wiki.python.org/moin/GlobalInterpreterLock) +是众所周知的。 +在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。 +:end_tab: + +:begin_tab:`paddle` 你可能会开始担心操作效率的问题。 毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 代码执行和许多其他的Python代码。 @@ -571,6 +692,7 @@ Python的问题[全局解释器锁](https://wiki.python.org/moin/GlobalInterpret 在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。 :end_tab: + ## 小结 * 一个块可以由许多层组成;一个块可以由许多块组成。 @@ -582,7 +704,7 @@ Python的问题[全局解释器锁](https://wiki.python.org/moin/GlobalInterpret 1. 如果将`MySequential`中存储块的方式更改为Python列表,会出现什么样的问题? 1. 实现一个块,它以两个块为参数,例如`net1`和`net2`,并返回前向传播中两个网络的串联输出。这也被称为平行块。 -1. 假设你想要连接同一网络的多个实例。实现一个函数,该函数生成同一个块的多个实例,并在此基础上构建更大的网络。 +1. 假设我们想要连接同一网络的多个实例。实现一个函数,该函数生成同一个块的多个实例,并在此基础上构建更大的网络。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1828) @@ -595,3 +717,7 @@ Python的问题[全局解释器锁](https://wiki.python.org/moin/GlobalInterpret :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1826) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11777) +:end_tab: diff --git a/chapter_deep-learning-computation/parameters.md b/chapter_deep-learning-computation/parameters.md index 41929f6b2..77afe5804 100644 --- a/chapter_deep-learning-computation/parameters.md +++ b/chapter_deep-learning-computation/parameters.md @@ -11,8 +11,8 @@ 而忽略了操作参数的具体细节。 本节,我们将介绍以下内容: -* 访问参数,用于调试、诊断和可视化。 -* 参数初始化。 +* 访问参数,用于调试、诊断和可视化; +* 参数初始化; * 在不同模型组件间共享参数。 (**我们首先看一下具有单隐藏层的多层感知机。**) @@ -55,6 +55,18 @@ X = tf.random.uniform((2, 4)) net(X) ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +from paddle import nn + +net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1)) +X = paddle.rand([2, 4]) +net(X) +``` + ## [**参数访问**] 我们从已有模型中访问参数。 @@ -68,7 +80,7 @@ print(net[1].params) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle print(net[2].state_dict()) ``` @@ -110,7 +122,14 @@ print(net.layers[2].weights[1]) print(tf.convert_to_tensor(net.layers[2].weights[1])) ``` -:begin_tab:`mxnet,pytorch` +```{.python .input} +#@tab paddle +print(type(net[2].bias)) +print(net[2].bias) +print(net[2].bias.value) +``` + +:begin_tab:`mxnet,pytorch,paddle` 参数是复合的对象,包含值、梯度和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。 @@ -122,7 +141,7 @@ net[1].weight.grad() ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net[2].weight.grad == None ``` @@ -139,7 +158,7 @@ print(net.collect_params()) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle print(*[(name, param.shape) for name, param in net[0].named_parameters()]) print(*[(name, param.shape) for name, param in net.named_parameters()]) ``` @@ -166,6 +185,11 @@ net.state_dict()['2.bias'].data net.get_weights()[1] ``` +```{.python .input} +#@tab paddle +net.state_dict()['2.bias'] +``` + ### [**从嵌套块收集参数**] 让我们看看,如果我们将多个块相互嵌套,参数命名约定是如何工作的。 @@ -230,6 +254,23 @@ rgnet.add(tf.keras.layers.Dense(1)) rgnet(X) ``` +```{.python .input} +#@tab paddle +def block1(): + return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), + nn.Linear(8, 4), nn.ReLU()) + +def block2(): + net = nn.Sequential() + for i in range(4): + # 在这里嵌套 + net.add_sublayer(f'block {i}', block1()) + return net + +rgnet = nn.Sequential(block2(), nn.Linear(4, 1)) +rgnet(X) +``` + [**设计了网络后,我们看看它是如何工作的。**] ```{.python .input} @@ -238,7 +279,7 @@ print(rgnet.collect_params()) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle print(rgnet) ``` @@ -264,6 +305,11 @@ rgnet[0][1][0].bias.data rgnet.layers[0].layers[1].layers[1].weights[1] ``` +```{.python .input} +#@tab paddle +print(rgnet[0].state_dict()['block 0.0.bias']) +``` + ## 参数初始化 知道了如何访问参数后,现在我们看看如何正确地初始化参数。 @@ -291,6 +337,12 @@ PyTorch的`nn.init`模块提供了多种预置初始化方法。 TensorFlow在根模块和`keras.initializers`模块中提供了各种初始化方法。 :end_tab: +:begin_tab:`paddle` +默认情况下,PaddlePaddle会使用Xavier初始化权重矩阵, +偏置参数设置为0。 +PaddlePaddle的`nn.initializer`模块提供了多种预置初始化方法。 +:end_tab: + ### [**内置初始化**] 让我们首先调用内置的初始化器。 @@ -327,6 +379,16 @@ net(X) net.weights[0], net.weights[1] ``` +```{.python .input} +#@tab paddle +def init_normal(m): + if type(m) == nn.Linear: + paddle.nn.initializer.Normal(mean=0.0, std=0.01) + paddle.zeros(m.bias) +net.apply(init_normal) +net[0].weight[0],net[0].state_dict()['bias'] +``` + 我们还可以将所有参数初始化为给定的常数,比如初始化为1。 ```{.python .input} @@ -359,6 +421,16 @@ net(X) net.weights[0], net.weights[1] ``` +```{.python .input} +#@tab paddle +def init_constant(m): + if type(m) == nn.Linear: + paddle.nn.initializer.Constant(value = 1) + paddle.zeros(m.bias) +net.apply(init_constant) +net[0].weight[0],net[0].state_dict()['bias'] +``` + 我们还可以[**对某些块应用不同的初始化方法**]。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。 @@ -402,6 +474,21 @@ print(net.layers[1].weights[0]) print(net.layers[2].weights[0]) ``` +```{.python .input} +#@tab paddle +def xavier(m): + if type(m) == nn.Linear: + paddle.nn.initializer.XavierUniform(m.weight) +def init_42(m): + if type(m) == nn.Linear: + paddle.nn.initializer.Constant(42) + +net[0].apply(xavier) +net[2].apply(init_42) +print(net[0].weight[0]) +print(net[2].weight) +``` + ### [**自定义初始化**] 有时,深度学习框架没有提供我们需要的初始化方法。 @@ -433,6 +520,10 @@ $$ 该函数返回给定形状和数据类型的所需张量。 :end_tab: +:begin_tab:`paddle` +同样,我们实现了一个`my_init`函数来应用到`net`。 +:end_tab: + ```{.python .input} class MyInit(init.Initializer): def _init_weight(self, name, data): @@ -479,6 +570,22 @@ net(X) print(net.layers[1].weights[0]) ``` +```{.python .input} +#@tab paddle +def my_init(m): + if type(m) == nn.Linear: + print("Init", *[(name, param.shape) + for name, param in m.named_parameters()][0]) + paddle.nn.initializer.XavierUniform(m.weight, -10, 10) + h = paddle.abs(m.weight) >= 5 + h = paddle.to_tensor(h) + m = paddle.to_tensor(m.weight) + m *= h + +net.apply(my_init) +net[0].weight[:2] +``` + 注意,我们始终可以直接设置参数。 ```{.python .input} @@ -501,6 +608,15 @@ net.layers[1].weights[0][0, 0].assign(42) net.layers[1].weights[0] ``` +```{.python .input} +#@tab paddle +net[0].weight.set_value(net[0].weight.numpy() + 1) +val = net[0].weight.numpy() +val[0, 0] = 42 +net[0].weight.set_value(val) +net[0].weight[0] +``` + :begin_tab:`mxnet` 高级用户请注意:如果要在`autograd`范围内调整参数, 则需要使用`set_data`,以避免误导自动微分机制。 @@ -563,11 +679,24 @@ net(X) print(len(net.layers) == 3) ``` +```{.python .input} +#@tab paddle +# 我们需要给共享层一个名称,以便可以引用它的参数。 +shared = nn.Linear(8, 8) +net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), + shared, nn.ReLU(), + shared, nn.ReLU(), + nn.Linear(8, 1)) +net(X) +# 检查参数是否相同 +print(net[2].weight[0] == net[4].weight[0]) +``` + :begin_tab:`mxnet` 这个例子表明第二层和第三层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。 -你可能会思考:当参数绑定时,梯度会发生什么情况? +这里有一个问题:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度, 因此在反向传播期间第二个隐藏层和第三个隐藏层的梯度会加在一起。 :end_tab: @@ -576,7 +705,7 @@ print(len(net.layers) == 3) 这个例子表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。 -你可能会思考:当参数绑定时,梯度会发生什么情况? +这里有一个问题:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。 :end_tab: @@ -604,3 +733,7 @@ print(len(net.layers) == 3) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1830) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11778) +:end_tab: diff --git a/chapter_deep-learning-computation/read-write.md b/chapter_deep-learning-computation/read-write.md index 74017ae84..9a0ad9681 100644 --- a/chapter_deep-learning-computation/read-write.md +++ b/chapter_deep-learning-computation/read-write.md @@ -42,6 +42,18 @@ x = tf.range(4) np.save('x-file.npy', x) ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +from paddle import nn +from paddle.nn import functional as F + +x = paddle.arange(4) +paddle.save(x, 'x-file') +``` + 我们现在可以将存储在文件中的数据读回内存。 ```{.python .input} @@ -61,6 +73,12 @@ x2 = np.load('x-file.npy', allow_pickle=True) x2 ``` +```{.python .input} +#@tab paddle +x2 = paddle.load('x-file') +x2 +``` + 我们可以[**存储一个张量列表,然后把它们读回内存。**] ```{.python .input} @@ -86,6 +104,14 @@ x2, y2 = np.load('xy-files.npy', allow_pickle=True) (x2, y2) ``` +```{.python .input} +#@tab paddle +y = paddle.zeros([4]) +paddle.save([x,y], 'x-file') +x2, y2 = paddle.load('x-file') +(x2, y2) +``` + 我们甚至可以(**写入或读取从字符串映射到张量的字典**)。 当我们要读取或写入模型中的所有权重时,这很方便。 @@ -112,6 +138,14 @@ mydict2 = np.load('mydict.npy', allow_pickle=True) mydict2 ``` +```{.python .input} +#@tab paddle +mydict = {'x': x, 'y': y} +paddle.save(mydict, 'mydict') +mydict2 = paddle.load('mydict') +mydict2 +``` + ## [**加载和保存模型参数**] 保存单个权重向量(或其他张量)确实有用, @@ -177,6 +211,22 @@ X = tf.random.uniform((2, 20)) Y = net(X) ``` +```{.python .input} +#@tab paddle +class MLP(nn.Layer): + def __init__(self): + super().__init__() + self.hidden = nn.Linear(20, 256) + self.output = nn.Linear(256, 10) + + def forward(self, x): + return self.output(F.relu(self.hidden(x))) + +net = MLP() +X = paddle.randn(shape=[2, 20]) +Y = net(X) +``` + 接下来,我们[**将模型的参数存储在一个叫做“mlp.params”的文件中。**] ```{.python .input} @@ -193,6 +243,11 @@ torch.save(net.state_dict(), 'mlp.params') net.save_weights('mlp.params') ``` +```{.python .input} +#@tab paddle +paddle.save(net.state_dict(), 'mlp.pdparams') +``` + 为了恢复模型,我们[**实例化了原始多层感知机模型的一个备份。**] 这里我们不需要随机初始化模型参数,而是(**直接读取文件中存储的参数。**) @@ -214,6 +269,13 @@ clone = MLP() clone.load_weights('mlp.params') ``` +```{.python .input} +#@tab paddle +clone = MLP() +clone.set_state_dict(paddle.load('mlp.pdparams')) +clone.eval() +``` + 由于两个实例具有相同的模型参数,在输入相同的`X`时, 两个实例的计算结果应该相同。 让我们来验证一下。 @@ -224,7 +286,7 @@ Y_clone == Y ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle Y_clone = clone(X) Y_clone == Y ``` @@ -244,8 +306,8 @@ Y_clone == Y ## 练习 1. 即使不需要将经过训练的模型部署到不同的设备上,存储模型参数还有什么实际的好处? -1. 假设我们只想复用网络的一部分,以将其合并到不同的网络架构中。比如说,如果你想在一个新的网络中使用之前网络的前两层,你该怎么做? -1. 如何同时保存网络架构和参数?你会对架构加上什么限制? +1. 假设我们只想复用网络的一部分,以将其合并到不同的网络架构中。比如想在一个新的网络中使用之前网络的前两层,该怎么做? +1. 如何同时保存网络架构和参数?需要对架构加上什么限制? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1840) @@ -258,3 +320,7 @@ Y_clone == Y :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1838) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11781) +:end_tab: diff --git a/chapter_deep-learning-computation/use-gpu.md b/chapter_deep-learning-computation/use-gpu.md index 57d02c044..280157273 100644 --- a/chapter_deep-learning-computation/use-gpu.md +++ b/chapter_deep-learning-computation/use-gpu.md @@ -9,7 +9,7 @@ 首先是如何使用单个GPU,然后是如何使用多个GPU和多个服务器(具有多个GPU)。 我们先看看如何使用单个NVIDIA GPU进行计算。 -首先,确保你至少安装了一个NVIDIA GPU。 +首先,确保至少安装了一个NVIDIA GPU。 然后,下载[NVIDIA驱动和CUDA](https://developer.nvidia.com/cuda-downloads) 并按照提示设置适当的路径。 当这些准备工作完成,就可以使用`nvidia-smi`命令来(**查看显卡信息。**) @@ -20,14 +20,14 @@ ``` :begin_tab:`mxnet` -之前,你可能已经注意到MXNet张量看起来与NumPy的`ndarray`几乎相同。 +读者可能已经注意到MXNet张量看起来与NumPy的`ndarray`几乎相同。 但有一些关键区别,其中之一是MXNet支持不同的硬件设备。 -在MXNet中,每个数组都有一个上下文(context)。 +在MXNet中,每个数组都有一个环境(context)。 默认情况下,所有变量和相关的计算都分配给CPU。 -有时上下文可能是GPU。 +有时环境可能是GPU。 当我们跨多个服务器部署作业时,事情会变得更加棘手。 -通过智能地将数组分配给上下文, +通过智能地将数组分配给环境, 我们可以最大限度地减少在设备之间传输数据的时间。 例如,当在带有GPU的服务器上训练神经网络时, 我们通常希望模型的参数在GPU上。 @@ -35,13 +35,24 @@ 接下来,我们需要确认是否安装了MXNet的GPU版本。 如果已经安装了MXNet的CPU版本,我们需要先卸载它。 例如,使用`pip uninstall mxnet`命令, -然后根据你的CUDA版本安装相应的MXNet的GPU版本。 -例如,假设你已经安装了CUDA10.0, -你可以通过`pip install mxnet-cu100`安装支持CUDA10.0的MXNet版本。 +然后根据CUDA版本安装相应的MXNet的GPU版本。 +例如,假设已经安装了CUDA10.0,可以通过`pip install mxnet-cu100`安装支持CUDA10.0的MXNet版本。 :end_tab: :begin_tab:`pytorch` 在PyTorch中,每个数组都有一个设备(device), +我们通常将其称为环境(context)。 +默认情况下,所有变量和相关的计算都分配给CPU。 +有时环境可能是GPU。 +当我们跨多个服务器部署作业时,事情会变得更加棘手。 +通过智能地将数组分配给环境, +我们可以最大限度地减少在设备之间传输数据的时间。 +例如,当在带有GPU的服务器上训练神经网络时, +我们通常希望模型的参数在GPU上。 +:end_tab: + +:begin_tab:`paddle` +在PaddlePaddle中,每个张量都有一个设备(device), 我们通常将其称为上下文(context)。 默认情况下,所有变量和相关的计算都分配给CPU。 有时上下文可能是GPU。 @@ -50,11 +61,16 @@ 我们可以最大限度地减少在设备之间传输数据的时间。 例如,当在带有GPU的服务器上训练神经网络时, 我们通常希望模型的参数在GPU上。 + +接下来,我们需要确认安装了PaddlePaddle的GPU版本。 +如果已经安装了PaddlePaddle的CPU版本,我们需要先卸载它。 +然后根据你的CUDA版本安装相应的PaddlePaddle的GPU版本。 +例如,假设你安装了CUDA10.1,你可以通过`conda install paddlepaddle-gpu==2.2.2 cudatoolkit=10.1 --channel https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/Paddle/`安装支持CUDA10.1的PaddlePaddle版本。 :end_tab: 要运行此部分中的程序,至少需要两个GPU。 -注意,对于大多数桌面计算机来说,这可能是奢侈的,但在云中很容易获得。 -例如,你可以使用AWS EC2的多GPU实例。 +注意,对大多数桌面计算机来说,这可能是奢侈的,但在云中很容易获得。 +例如可以使用AWS EC2的多GPU实例。 本书的其他章节大都不需要多个GPU, 而本节只是为了展示数据如何在不同的设备之间传递。 @@ -83,6 +99,17 @@ 另外,`cuda:0`和`cuda`是等价的。 :end_tab: +:begin_tab:`paddle` +在飞桨中,CPU和GPU可以用`paddle.device.set_device('cpu')` +和`paddle.device.set_device('gpu')`表示。 +应该注意的是,`cpu`设备意味着所有物理CPU和内存, +这意味着飞桨的计算将尝试使用所有CPU核心。 +然而,`gpu`设备只代表一个卡和相应的显存。 +如果有多个GPU,我们使用`paddle.device.get_device()` +其中输出的数字是表示的是卡号(比如`gpu:3`,表示的是卡3,注意GPU的卡号是从0开始的)。 +另外,`gpu:0`和`gpu`是等价的。 +:end_tab: + ```{.python .input} from mxnet import np, npx from mxnet.gluon import nn @@ -106,6 +133,14 @@ import tensorflow as tf tf.device('/CPU:0'), tf.device('/GPU:0'), tf.device('/GPU:1') ``` +```{.python .input} +#@tab paddle +import paddle +from paddle import nn + +paddle.device.set_device("cpu"), paddle.CUDAPlace(0), paddle.CUDAPlace(1) +``` + 我们可以(**查询可用gpu的数量。**) ```{.python .input} @@ -122,6 +157,11 @@ torch.cuda.device_count() len(tf.config.experimental.list_physical_devices('GPU')) ``` +```{.python .input} +#@tab paddle +paddle.device.cuda.device_count() +``` + 现在我们定义了两个方便的函数, [**这两个函数允许我们在不存在所需所有GPU的情况下运行代码。**] @@ -172,6 +212,25 @@ def try_all_gpus(): #@save try_gpu(), try_gpu(10), try_all_gpus() ``` +```{.python .input} +#@tab paddle +#@save +def try_gpu(i=0): + """如果存在,则返回gpu(i),否则返回cpu()。""" + if paddle.device.cuda.device_count() >= i + 1: + return paddle.CUDAPlace(i) + return paddle.CPUPlace() + +#@save +def try_all_gpus(): + """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]。""" + devices = [paddle.CUDAPlace(i) + for i in range(paddle.device.cuda.device_count())] + return devices if devices else paddle.CPUPlace() + +try_gpu(),try_gpu(10),try_all_gpus() +``` + ## 张量与GPU 我们可以[**查询张量所在的设备。**] @@ -194,6 +253,12 @@ x = tf.constant([1, 2, 3]) x.device ``` +```{.python .input} +#@tab paddle +x = paddle.to_tensor([1, 2, 3]) +x.place +``` + 需要注意的是,无论何时我们要对多个项进行操作, 它们都必须在同一个设备上。 例如,如果我们对两个张量求和, @@ -227,7 +292,13 @@ with try_gpu(): X ``` -假设你至少有两个GPU,下面的代码将在(**第二个GPU上创建一个随机张量。**) +```{.python .input} +#@tab paddle +X = paddle.to_tensor(paddle.ones(shape=[2, 3]), place=try_gpu()) +X +``` + +假设我们至少有两个GPU,下面的代码将在(**第二个GPU上创建一个随机张量。**) ```{.python .input} Y = np.random.uniform(size=(2, 3), ctx=try_gpu(1)) @@ -247,6 +318,12 @@ with try_gpu(1): Y ``` +```{.python .input} +#@tab paddle +Y = paddle.to_tensor(paddle.rand([2, 3]), place=try_gpu(1)) +Y +``` + ### 复制 如果我们[**要计算`X + Y`,我们需要决定在哪里执行这个操作**]。 @@ -267,7 +344,7 @@ print(Z) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle Z = X.cuda(1) print(X) print(Z) @@ -296,7 +373,7 @@ Y + Z 有时,我们只想在变量存在于不同设备中时进行复制。 在这种情况下,我们可以调用`as_in_ctx`。 如果变量已经存在于指定的设备中,则这不会进行任何操作。 -除非你特别想创建一个复制,否则选择`as_in_ctx`方法。 +除非我们特别想创建一个复制,否则选择`as_in_ctx`方法。 :end_tab: :begin_tab:`pytorch` @@ -316,7 +393,7 @@ Z.as_in_ctx(try_gpu(1)) is Z ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle Z.cuda(1) is Z ``` @@ -335,11 +412,11 @@ Z2 is Z 然后才能继续进行更多的操作。 这就是为什么拷贝操作要格外小心。 根据经验,多个小操作比一个大操作糟糕得多。 -此外,一次执行几个操作比代码中散布的许多单个操作要好得多(除非你确信自己在做什么)。 +此外,一次执行几个操作比代码中散布的许多单个操作要好得多。 如果一个设备必须等待另一个设备才能执行其他操作, 那么这样的操作可能会阻塞。 这有点像排队订购咖啡,而不像通过电话预先订购: -当你到店的时候,咖啡已经准备好了。 +当客人到店的时候,咖啡已经准备好了。 最后,当我们打印张量或将张量转换为NumPy格式时, 如果数据不在内存中,框架会首先将其复制到内存中, @@ -371,6 +448,12 @@ with strategy.scope(): tf.keras.layers.Dense(1)]) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential(nn.Linear(3, 1)) +net=net.to(try_gpu()) +``` + 在接下来的几章中, 我们将看到更多关于如何在GPU上运行模型的例子, 因为它们将变得更加计算密集。 @@ -398,6 +481,11 @@ net[0].weight.data.device net.layers[0].weights[0].device, net.layers[0].weights[1].device ``` +```{.python .input} +#@tab paddle +net[0].weight.place +``` + 总之,只要所有的数据和参数都在同一个设备上, 我们就可以有效地学习模型。 在下面的章节中,我们将看到几个这样的例子。 @@ -413,7 +501,7 @@ net.layers[0].weights[0].device, net.layers[0].weights[1].device 1. 尝试一个计算量更大的任务,比如大矩阵的乘法,看看CPU和GPU之间的速度差异。再试一个计算量很小的任务呢? 1. 我们应该如何在GPU上读写模型参数? 1. 测量计算1000个$100 \times 100$矩阵的矩阵乘法所需的时间,并记录输出矩阵的Frobenius范数,一次记录一个结果,而不是在GPU上保存日志并仅传输最终结果。 -1. 测量同时在两个GPU上执行两个矩阵乘法与在一个GPU上按顺序执行两个矩阵乘法所需的时间。提示:你应该看到近乎线性的缩放。 +1. 测量同时在两个GPU上执行两个矩阵乘法与在一个GPU上按顺序执行两个矩阵乘法所需的时间。提示:应该看到近乎线性的缩放。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1843) @@ -426,3 +514,7 @@ net.layers[0].weights[0].device, net.layers[0].weights[1].device :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1842) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11782) +:end_tab: diff --git a/chapter_installation/index.md b/chapter_installation/index.md index 739586ed5..6fa4930d2 100644 --- a/chapter_installation/index.md +++ b/chapter_installation/index.md @@ -6,21 +6,21 @@ ## 安装 Miniconda 最简单的方法就是安装依赖Python 3.x的[Miniconda](https://conda.io/en/latest/miniconda.html)。 -如果已安装conda,则可以跳过以下步骤。访问Miniconda网站,根据Python3.x版本确定适合你的系统的版本。 +如果已安装conda,则可以跳过以下步骤。访问Miniconda网站,根据Python3.x版本确定适合的版本。 -如果你使用macOS,假设你的Python版本是3.8(我们的测试版本),你将下载名称包含字符串“MacOSX”的bash脚本,并执行以下操作: +如果我们使用macOS,假设Python版本是3.9(我们的测试版本),将下载名称包含字符串“MacOSX”的bash脚本,并执行以下操作: ```bash -# 文件名可能会更改 -sh Miniconda3-py38_4.10.3-MacOSX-x86_64.sh -b +# 以Intel处理器为例,文件名可能会更改 +sh Miniconda3-py39_4.12.0-MacOSX-x86_64.sh -b ``` -如果你使用Linux,假设你的Python版本是3.8(我们的测试版本),你将下载名称包含字符串“Linux”的bash脚本,并执行以下操作: +如果我们使用Linux,假设Python版本是3.9(我们的测试版本),将下载名称包含字符串“Linux”的bash脚本,并执行以下操作: ```bash # 文件名可能会更改 -sh Miniconda3-py38_4.10.3-Linux-x86_64.sh -b +sh Miniconda3-py39_4.12.0-Linux-x86_64.sh -b ``` @@ -31,10 +31,10 @@ sh Miniconda3-py38_4.10.3-Linux-x86_64.sh -b ``` -现在关闭并重新打开当前的 shell。你应该能用下面的命令创建一个新的环境: +现在关闭并重新打开当前的shell。并使用下面的命令创建一个新的环境: ```bash -conda create --name d2l python=3.8 -y +conda create --name d2l python=3.9 -y ``` @@ -47,17 +47,17 @@ conda activate d2l ## 安装深度学习框架和`d2l`软件包 -在安装深度学习框架之前,请先检查你的计算机上是否有可用的GPU。 -例如,你可以查看计算机是否装有NVIDIA GPU并已安装[CUDA](https://developer.nvidia.com/cuda-downloads)。 -如果你的机器没有任何GPU,没有必要担心,因为你的CPU在前几章完全够用。 -但是,如果你想流畅地学习全部章节,请提早获取GPU并且安装深度学习框架的GPU版本。 +在安装深度学习框架之前,请先检查计算机上是否有可用的GPU。 +例如可以查看计算机是否装有NVIDIA GPU并已安装[CUDA](https://developer.nvidia.com/cuda-downloads)。 +如果机器没有任何GPU,没有必要担心,因为CPU在前几章完全够用。 +但是,如果想流畅地学习全部章节,请提早获取GPU并且安装深度学习框架的GPU版本。 :begin_tab:`mxnet` -安装MXNet的GPU版本,你首先需要知道已安装的CUDA版本。 -(你可以通过运行`nvcc --version`或`cat /usr/local/cuda/version.txt`来检验。) -假设你已安装CUDA 10.1版本,请执行以下命令: +安装MXNet的GPU版本,首先需要知道已安装的CUDA版本。 +(可以通过运行`nvcc --version`或`cat /usr/local/cuda/version.txt`来检验。) +假设已安装CUDA 10.1版本,请执行以下命令: ```bash # 对于Linux和macOS用户 @@ -68,12 +68,11 @@ pip install mxnet-cu101==1.7.0 -f https://dist.mxnet.io/python ``` -你可以根据你的CUDA版本更改如上`mxnet-cu101`的最后一位数字, +可以根据CUDA版本更改如上`mxnet-cu101`的最后一位数字, 例如:CUDA 10.0是`cu100`, CUDA 9.0是`cu90`。 -如果你的机器没有NVIDIA GPU或CUDA, -你可以按如下方式MXNet的CPU版本: +如果机器没有NVIDIA GPU或CUDA,可以按如下方式MXNet的CPU版本: ```bash pip install mxnet==1.7.0.post1 @@ -85,18 +84,18 @@ pip install mxnet==1.7.0.post1 :begin_tab:`pytorch` -你可以按如下方式安装PyTorch的CPU或GPU版本: +我们可以按如下方式安装PyTorch的CPU或GPU版本: ```bash -pip install torch==1.11.0 -pip install torchvision==0.12.0 +pip install torch==1.12.0 +pip install torchvision==0.13.0 ``` :end_tab: :begin_tab:`tensorflow` -你可以按如下方式安装TensorFlow的CPU或GPU版本: +我们可以按如下方式安装TensorFlow的CPU或GPU版本: ```bash pip install tensorflow==2.8.0 @@ -104,20 +103,38 @@ pip install tensorflow-probability==0.16.0 ``` +:end_tab: + +:begin_tab:`paddle` +安装PaddlePaddle的GPU版本,首先需要知道已安装的CUDA版本。 +(可以通过运行`nvcc --version`或`cat /usr/local/cuda/version.txt`来检验。) +假设已安装CUDA 11.2版本,请执行以下命令: + +```bash +python -m pip install paddlepaddle-gpu==2.3.2.post112 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html +``` + + +如果机器没有NVIDIA GPU或CUDA,可以按如下方式PaddlePaddle的CPU版本: + +```bash +python -m pip install paddlepaddle==2.3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + + :end_tab: 我们的下一步是安装`d2l`包,以方便调取本书中经常使用的函数和类: ```bash -pip install d2l==0.17.5 +pip install d2l==0.17.6 ``` ## 下载 D2L Notebook 接下来,需要下载这本书的代码。 -你可以点击本书HTML页面顶部的“Jupyter 记事本”选项下载后解压代码。 -或者,你可以按照如下方式进行下载: +可以点击本书HTML页面顶部的“Jupyter 记事本”选项下载后解压代码,或者可以按照如下方式进行下载: :begin_tab:`mxnet` @@ -130,7 +147,7 @@ cd mxnet ``` -注意:如果你没有安装`unzip`,则可以通过运行`sudo apt install unzip`进行安装。 +注意:如果没有安装`unzip`,则可以通过运行`sudo apt install unzip`进行安装。 :end_tab: @@ -145,7 +162,7 @@ cd pytorch ``` -注意:如果你没有安装`unzip`,则可以通过运行`sudo apt install unzip`进行安装。 +注意:如果没有安装`unzip`,则可以通过运行`sudo apt install unzip`进行安装。 :end_tab: @@ -160,20 +177,35 @@ cd tensorflow ``` -注意:如果你没有安装`unzip`,则可以通过运行`sudo apt install unzip`进行安装。 +注意:如果没有安装`unzip`,则可以通过运行`sudo apt install unzip`进行安装。 :end_tab: -安装完成后,你可以通过运行以下命令打开Jupyter笔记本(在Window系统的命令行窗口中运行以下命令前,需先将当前路径定位到刚下载的本书代码解压后的目录): +:begin_tab:`paddle` + +```bash +mkdir d2l-zh && cd d2l-zh +curl https://zh-v2.d2l.ai/d2l-zh-2.0.0.zip -o d2l-zh.zip +unzip d2l-zh.zip && rm d2l-zh.zip +cd paddle +``` + + +注意:如果没有安装`unzip`,则可以通过运行`sudo apt install unzip`进行安装。 + +:end_tab: + + +安装完成后我们可以通过运行以下命令打开Jupyter笔记本(在Window系统的命令行窗口中运行以下命令前,需先将当前路径定位到刚下载的本书代码解压后的目录): ```bash jupyter notebook ``` -现在,你可以在Web浏览器中打开第二版预览版
+第二版
跳转第一版
面向中文读者的能运行、可讨论的深度学习教科书
-含 NumPy/MXNet、PyTorch 和 TensorFlow 实现
-被全球 55 个国家 300 所大学用于教学
+含 PyTorch、NumPy/MXNet、TensorFlow 和 PaddlePaddle 实现
+被全球 60 多个国家 400 多所大学用于教学