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)
理解深度学习的最佳方法是学以致用。
@@ -24,7 +24,7 @@
将本书(中英文版)用作教材或参考书的大学

- +

如果本书对你有帮助,请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浏览器中打开(通常会自动打开)。 -由此,你可以运行这本书中每个部分的代码。 +现在可以在Web浏览器中打开(通常会自动打开)。 +由此,我们可以运行这本书中每个部分的代码。 在运行书籍代码、更新深度学习框架或`d2l`软件包之前,请始终执行`conda activate d2l`以激活运行时环境。 要退出环境,请运行`conda deactivate`。 @@ -190,3 +222,7 @@ jupyter notebook :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2084) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11679) +:end_tab: diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index 579bd8beb..3374953b2 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -1,108 +1,104 @@ -# 前言 +# 引言 :label:`chap_introduction` -时至今日,我们常用的计算机程序几乎都是软件开发人员从零编写的。 -比如,现在我们要编写一个程序来管理网上商城。 -经过思考,我们可能提出如下一个解决方案: -首先,用户通过Web浏览器(或移动应用程序)与应用程序进行交互。 -紧接着,应用程序与数据库引擎进行交互,以保存交易历史记录并跟踪每个用户的动态。 -其中,这个程序的核心——“业务逻辑”,详细说明了程序在各种情况下进行的操作。 +时至今日,人们常用的计算机程序几乎都是软件开发人员从零编写的。 +比如,现在开发人员要编写一个程序来管理网上商城。 +经过思考,开发人员可能提出如下一个解决方案: +首先,用户通过Web浏览器(或移动应用程序)与应用程序进行交互; +紧接着,应用程序与数据库引擎进行交互,以保存交易历史记录并跟踪每个用户的动态; +其中,这个应用程序的核心——“业务逻辑”,详细说明了应用程序在各种情况下进行的操作。 -为了完善业务逻辑,我们必须细致地考虑应用程序所有可能遇到的边界情况,并为这些边界情况设计合适的规则。 -当买家单击将商品添加到购物车时,我们会向购物车数据库表中添加一个条目,将该用户ID与商品ID关联起来。 -虽然一次编写出完美应用程序的可能性微乎其微,但在大多数情况下,我们可以从基本原理出发编写这样的程序,并不断测试直到满足用户的需求。 -我们能够根据第一原则设计自动化系统,驱动正常运行的产品和系统,是一个人类认知上的非凡壮举。 +为了完善业务逻辑,开发人员必须细致地考虑应用程序所有可能遇到的边界情况,并为这些边界情况设计合适的规则。 +当买家单击将商品添加到购物车时,应用程序会向购物车数据库表中添加一个条目,将该用户ID与商品ID关联起来。 +虽然一次编写出完美应用程序的可能性微乎其微,但在大多数情况下,开发人员可以从上述的业务逻辑出发,编写出符合业务逻辑的应用程序,并不断测试直到满足用户的需求。 +根据业务逻辑设计自动化系统,驱动正常运行的产品和系统,是一个人类认知上的非凡壮举。 -幸运的是,对于日益壮大的机器学习科学家群体来说,实现很多任务的自动化并不再屈从于人类的聪明才智。 -想象一下,你正和你最聪明的一群朋友围绕着白板,试图解决以下问题之一: +幸运的是,对日益壮大的机器学习科学家群体来说,实现很多任务的自动化并不再屈从于人类所能考虑到的逻辑。 +想象一下,假如开发人员要试图解决以下问题之一: -* 编写一个程序,给出地理信息、卫星图像和一些历史天气信息,来预测明天的天气。 -* 编写一个程序,给出自然文本表示的问题,并正确回答该问题。 -* 编写一个程序,给出一张图像,识别出图像所包含的人,并在每个人周围绘制轮廓。 -* 编写一个程序,向用户推荐他们可能喜欢,但在自然浏览过程中不太可能遇到的产品。 +* 编写一个应用程序,接受地理信息、卫星图像和一些历史天气信息,并预测明天的天气; +* 编写一个应用程序,接受自然文本表示的问题,并正确回答该问题; +* 编写一个应用程序,接受一张图像,识别出该图像所包含的人,并在每个人周围绘制轮廓; +* 编写一个应用程序,向用户推荐他们可能喜欢,但在自然浏览过程中不太可能遇到的产品。 -在这些情况下,即使是顶级程序员也无法提出完美的解决方案。 -原因可能各不相同,有时我们的任务可能遵循一种随着时间推移而变化的模式,我们需要程序来自动调整。 +在这些情况下,即使是顶级程序员也无法提出完美的解决方案, +原因可能各不相同。有时任务可能遵循一种随着时间推移而变化的模式,我们需要程序来自动调整。 有时任务内的关系可能太复杂(比如像素和抽象类别之间的关系),需要数千或数百万次的计算。 -即使我们的眼睛能毫不费力地完成任务,这些计算也超出了我们的意识理解。 +即使人类的眼睛能毫不费力地完成这些难以提出完美解决方案的任务,这其中的计算也超出了人类意识理解范畴。 *机器学习*(machine learning,ML)是一类强大的可以从经验中学习的技术。 通常采用观测数据或与环境交互的形式,机器学习算法会积累更多的经验,其性能也会逐步提高。 -相反,对比刚刚所说的电子商务平台,如果它一直执行相同的业务逻辑,无论积累多少经验,都不会自动提高(直到开发人员认识到并更新软件)。 -在这本书中,我们将带你开启机器学习之旅,并特别关注*深度学习*(deep learning,DL)的基础知识。 +相反,对于刚刚所说的电子商务平台,如果它一直执行相同的业务逻辑,无论积累多少经验,都不会自动提高,除非开发人员认识到问题并更新软件。 +本书将带读者开启机器学习之旅,并特别关注*深度学习*(deep learning,DL)的基础知识。 深度学习是一套强大的技术,它可以推动计算机视觉、自然语言处理、医疗保健和基因组学等不同领域的创新。 ## 日常生活中的机器学习 -机器学习应用在日常生活的方方面面。 -现在,假设你正和本书的作者们一起,驱车去咖啡店。 -亚历山大拿起一部iPhone,对它说道“Hey Siri”--手机的语音识别系统主动唤醒了。 -接着,李沐对Siri说道“去星巴克咖啡店”--语音识别系统自动触发语音转文字功能,并启动地图应用程序来满足我们的请求。 -地图应用程序在启动后确定了若干条路线:每条路线都显示了预计的通行时间...... -由此可见,机器学习渗透在生活中的方方面面,在短短几秒钟的时间里,我们与智能手机的日常互动就可以涉及几种机器学习模型。 +机器学习应用在日常生活中的方方面面。 +现在,假设本书的作者们一起驱车去咖啡店。 +阿斯顿拿起一部iPhone,对它说道:“Hey Siri!”手机的语音识别系统就被唤醒了。 +接着,李沐对Siri说道:“去星巴克咖啡店。”语音识别系统就自动触发语音转文字功能,并启动地图应用程序, +地图应用程序在启动后筛选了若干条路线,每条路线都显示了预计的通行时间...... +由此可见,机器学习渗透在生活中的方方面面,在短短几秒钟的时间里,人们与智能手机的日常互动就可以涉及几种机器学习模型。 -现在,请你从基本原则出发,编写一个程序来响应一个“唤醒词”(比如“Alexa”、“小爱同学”和“Hey Siri”)。 -试着用一台计算机和一个代码编辑器自己编写代码,如 :numref:`fig_wake_word`中所示。 -问题看似很难解决:麦克风每秒钟将收集大约44000个样本,每个样本都是声波振幅的测量值。 -如何编写程序,令其输入原始音频片段,输出$\{\text{是}, \text{否}\}$(表示该片段是否包含唤醒词)的可靠预测呢? -如果你毫无头绪,别担心,我们也不知道如何从头开始编写这个程序,这就是我们需要机器学习的原因。 +现在,假如需要我们编写程序来响应一个“唤醒词”(比如“Alexa”“小爱同学”和“Hey Siri”)。 +我们试着用一台计算机和一个代码编辑器编写代码,如 :numref:`fig_wake_word`中所示。 +问题看似很难解决:麦克风每秒钟将收集大约44000个样本,每个样本都是声波振幅的测量值。而该测量值与唤醒词难以直接关联。那又该如何编写程序,令其输入麦克风采集到的原始音频片段,输出$\{\text{是}, \text{否}\}$(表示该片段是否包含唤醒词)的可靠预测呢?我们对编写这个程序毫无头绪,这就是需要机器学习的原因。 ![识别唤醒词](../img/wake-word.svg) :label:`fig_wake_word` -通常,即使我们不知道如何明确地告诉计算机如何从输入映射到输出,我们的大脑仍然能够自己执行认知功能。 -换句话说,即使你不知道如何编写计算机程序来识别“Alexa”这个词,你的大脑自己也能够识别它。 -有了这一能力,我们就可以收集一个包含音频样本的巨大的*数据集*(dataset),并对包含和不包含唤醒词的样本进行标记。 -通过机器学习算法,我们不需要设计一个“明确地”识别唤醒词的系统。 -相反,我们定义一个灵活的程序算法,其输出由许多*参数*(parameter)决定。 -然后我们使用数据集来确定当下的“最佳参数集”,这些参数通过某种性能度量来获取完成任务的最佳性能。 +通常,即使我们不知道怎样明确地告诉计算机如何从输入映射到输出,大脑仍然能够自己执行认知功能。 +换句话说,即使我们不知道如何编写计算机程序来识别“Alexa”这个词,大脑自己也能够识别它。 +有了这一能力,我们就可以收集一个包含大量音频样本的*数据集*(dataset),并对包含和不包含唤醒词的样本进行标记。 +利用机器学习算法,我们不需要设计一个“明确地”识别唤醒词的系统。 +相反,我们只需要定义一个灵活的程序算法,其输出由许多*参数*(parameter)决定,然后使用数据集来确定当下的“最佳参数集”,这些参数通过某种性能度量方式来达到完成任务的最佳性能。 那么到底什么是参数呢? -你可以把参数看作是旋钮,我们可以转动旋钮来调整程序的行为。 -任一调整参数后的程序,我们称为*模型*(model)。 +参数可以被看作旋钮,旋钮的转动可以调整程序的行为。 +任一调整参数后的程序被称为*模型*(model)。 通过操作参数而生成的所有不同程序(输入-输出映射)的集合称为“模型族”。 使用数据集来选择参数的元程序被称为*学习算法*(learning algorithm)。 -在我们开始用机器学习算法解决问题之前,我们必须精确地定义问题,确定*输入*(input)和*输出*(output)的性质,并选择合适的模型族。 -在本例中,我们的模型接收一段音频作为输入,然后模型在$\{\text{是}, \text{否}\}$中生成一个选择作为输出。 +在开始用机器学习算法解决问题之前,我们必须精确地定义问题,确定*输入*(input)和*输出*(output)的性质,并选择合适的模型族。 +在本例中,模型接收一段音频作为输入,然后在是或否中生成一个选择作为输出。 如果一切顺利,经过一番训练,模型对于“片段是否包含唤醒词”的预测通常是正确的。 -现在我们的模型每次听到“Alexa”这个词时都会发出“是”的声音。 +现在模型每次听到“Alexa”这个词时都会发出“是”的声音。 由于这里的唤醒词是任意选择的自然语言,因此我们可能需要一个足够丰富的模型族,使模型多元化。 比如,模型族的另一个模型只在听到“Hey Siri”这个词时发出“是”。 理想情况下,同一个模型族应该适合于“Alexa”识别和“Hey Siri”识别,因为从直觉上看,它们似乎是相似的任务。 -然而,如果我们想处理完全不同的输入或输出,比如:从图像映射到字幕,或从英语映射到中文,我们可能需要一个完全不同的模型族。 +然而,如果我们想处理完全不同的输入或输出,比如:从图像映射到字幕,或从英语映射到中文,可能需要一个完全不同的模型族。 -正如你可能猜到的,如果我们只是随机设置所有按钮(模型参数),我们的模型不太可能识别出“Alexa”、“Hey Siri”或任何其他单词。 +但如果模型所有的按钮(模型参数)都被随机设置,就不太可能识别出“Alexa”“Hey Siri”或任何其他单词。 在机器学习中,*学习*(learning)是一个训练模型的过程。 通过这个过程,我们可以发现正确的参数集,从而使模型强制执行所需的行为。 -换句话说,我们用数据*训练*(train)我们的模型。 +换句话说,我们用数据*训练*(train)模型。 如 :numref:`fig_ml_loop`所示,训练过程通常包含如下步骤: -1. 从一个随机初始化参数的模型开始,这个模型基本毫不“智能”。 -1. 获取一些数据样本(例如,音频片段以及对应的$\{\text{是}, \text{否}\}$标签)。 -1. 调整参数,使模型在这些样本中表现得更好。 -1. 重复第2步和第3步,直到模型在任务中的表现令你满意。 +1. 从一个随机初始化参数的模型开始,这个模型基本没有“智能”; +1. 获取一些数据样本(例如,音频片段以及对应的是或否标签); +1. 调整参数,使模型在这些样本中表现得更好; +1. 重复第(2)步和第(3)步,直到模型在任务中的表现令人满意。 ![一个典型的训练过程](../img/ml-loop.svg) :label:`fig_ml_loop` 总而言之,我们没有编写唤醒词识别器,而是编写了一个“学习”程序。 如果我们用一个巨大的带标签的数据集,它很可能可以“学习”识别唤醒词。 -你可以将这种“通过用数据集来确定程序行为”的方法看作是“用数据编程”(programming with data)。 +这种“通过用数据集来确定程序行为”的方法可以被看作*用数据编程*(programming with data)。 比如,我们可以通过向机器学习系统,提供许多猫和狗的图片来设计一个“猫图检测器”。 -通过这种方式,检测器最终可以学会:如果输入是猫的图片就输出一个非常大的正数,如果输入是狗的图片就会得出一个非常大的负数。 -如果检测器不确定,它会输出接近于零的数...... -这个例子仅仅是机器学习常见应用的冰山一角。 -而深度学习是机器学习的一个主要分支,我们稍后将对其进行更详细的解析。 +检测器最终可以学会:如果输入是猫的图片就输出一个非常大的正数,如果输入是狗的图片就会输出一个非常小的负数。 +如果检测器不确定输入的图片中是猫还是狗,它会输出接近于零的数...... +这个例子仅仅是机器学习常见应用的冰山一角, +而深度学习是机器学习的一个主要分支,本节稍后的内容将对其进行更详细的解析。 -## 关键组件 +## 机器学习中的关键组件 -首先,我们想让大家更清楚地了解一些核心组件。 -无论我们遇到什么类型的机器学习问题,这些组件都将伴随我们左右: +首先介绍一些核心组件。无论什么类型的机器学习问题,都会遇到这些组件: -1. 我们可以学习的*数据*(data)。 -1. 如何转换数据的*模型*(model)。 -1. 一个*目标函数*(objective function),用来量化模型的有效性。 +1. 可以用来学习的*数据*(data); +1. 如何转换数据的*模型*(model); +1. 一个*目标函数*(objective function),用来量化模型的有效性; 1. 调整模型参数以优化目标函数的*算法*(algorithm)。 ### 数据 @@ -113,23 +109,23 @@ 机器学习模型会根据这些属性进行预测。 在上面的监督学习问题中,要预测的是一个特殊的属性,它被称为*标签*(label,或*目标*(target))。 -假设我们处理的是图像数据,每一张单独的照片即为一个样本,它的特征由每个像素数值的有序列表表示。 +当处理图像数据时,每一张单独的照片即为一个样本,它的特征由每个像素数值的有序列表表示。 比如,$200\times200$彩色照片由$200\times200\times3=120000$个数值组成,其中的“3”对应于每个空间位置的红、绿、蓝通道的强度。 -再比如,对于一组医疗数据,给定一组标准的特征(如年龄、生命体征和诊断),我们可能用此数据尝试预测患者是否会存活。 +再比如,对于一组医疗数据,给定一组标准的特征(如年龄、生命体征和诊断),此数据可以用来尝试预测患者是否会存活。 当每个样本的特征类别数量都是相同的时候,其特征向量是固定长度的,这个长度被称为数据的*维数*(dimensionality)。 -固定长度的特征向量是一个方便的属性,它有助于我们量化学习大量样本。 +固定长度的特征向量是一个方便的属性,它可以用来量化学习大量样本。 然而,并不是所有的数据都可以用“固定长度”的向量表示。 以图像数据为例,如果它们全部来自标准显微镜设备,那么“固定长度”是可取的; 但是如果图像数据来自互联网,它们很难具有相同的分辨率或形状。 -这时,我们可以考虑将图像裁剪成标准尺寸,但这种办法很局限,有丢失信息的风险。 +这时,将图像裁剪成标准尺寸是一种方法,但这种办法很局限,有丢失信息的风险。 此外,文本数据更不符合“固定长度”的要求。 比如,对于亚马逊等电子商务网站上的客户评论,有些文本数据很简短(比如“好极了”),有些则长篇大论。 与传统机器学习方法相比,深度学习的一个主要优势是可以处理不同长度的数据。 -一般来说,我们拥有的数据越多,我们的工作就越容易。 -当我们有了更多的数据,我们通常可以训练出更强大的模型,从而减少对预先设想假设的依赖。 +一般来说,拥有越多数据的时候,工作就越容易。 +更多的数据可以被用来训练出更强大的模型,从而减少对预先设想假设的依赖。 数据集的由小变大为现代深度学习的成功奠定基础。 在没有大数据集的情况下,许多令人兴奋的深度学习模型黯然失色。 就算一些深度学习模型在小数据集上能够工作,但其效能并不比传统方法高。 @@ -140,7 +136,7 @@ 此外,糟糕的预测性能甚至会加倍放大事态的严重性。 在一些敏感应用中,如预测性监管、简历筛选和用于贷款的风险模型,我们必须特别警惕垃圾数据带来的后果。 一种常见的问题来自不均衡的数据集,比如在一个有关医疗的训练数据集中,某些人群没有样本表示。 -想象一下,假设你要训练一个皮肤癌识别模型,但它(在训练数据集中)从未“见过”黑色皮肤的人群,这个模型就会顿时束手无策。 +想象一下,假设我们想要训练一个皮肤癌识别模型,但它(在训练数据集中)从未“见过”黑色皮肤的人群,这个模型就会顿时束手无策。 再比如,如果用“过去的招聘决策数据”来训练一个筛选简历的模型,那么机器学习模型可能会无意中捕捉到历史残留的不公正,并将其自动化。 然而,这一切都可能在不知情的情况下发生。 @@ -150,21 +146,21 @@ ### 模型 大多数机器学习会涉及到数据的转换。 -比如,我们建立一个“摄取照片并预测笑脸”的系统。再比如,我们摄取一组传感器读数,并预测读数的正常与异常程度。 +比如一个“摄取照片并预测笑脸”的系统。再比如通过摄取到的一组传感器读数预测读数的正常与异常程度。 虽然简单的模型能够解决如上简单的问题,但本书中关注的问题超出了经典方法的极限。 深度学习与经典方法的区别主要在于:前者关注的功能强大的模型,这些模型由神经网络错综复杂的交织在一起,包含层层数据转换,因此被称为*深度学习*(deep learning)。 -在讨论深度模型的过程中,我们也将提及一些传统方法。 +在讨论深度模型的过程中,本书也将提及一些传统方法。 ### 目标函数 -前面,我们将机器学习介绍为“从经验中学习”。 +前面的内容将机器学习介绍为“从经验中学习”。 这里所说的“学习”,是指自主提高模型完成某些任务的效能。 但是,什么才算真正的提高呢? -在机器学习中,我们需要定义模型的优劣程度的度量,这个度量在大多数情况是“可优化”的,我们称之为*目标函数*(objective function)。 +在机器学习中,我们需要定义模型的优劣程度的度量,这个度量在大多数情况是“可优化”的,这被称之为*目标函数*(objective function)。 我们通常定义一个目标函数,并希望优化它到最低点。 因为越低越好,所以这些函数有时被称为*损失函数*(loss function,或cost function)。 -但这只是一个惯例,你也可以取一个新的函数,优化到它的最高点。 +但这只是一个惯例,我们也可以取一个新的函数,优化到它的最高点。 这两个函数本质上是相同的,只是翻转一下符号。 当任务在试图预测数值时,最常见的损失函数是*平方误差*(squared error),即预测值与实际值之差的平方。 @@ -173,181 +169,176 @@ 在这些情况下,通常会优化*替代目标*。 通常,损失函数是根据模型参数定义的,并取决于数据集。 -在一个数据集上,我们通过最小化总损失来学习模型参数的最佳值。 +在一个数据集上,我们可以通过最小化总损失来学习模型参数的最佳值。 该数据集由一些为训练而收集的样本组成,称为*训练数据集*(training dataset,或称为*训练集*(training set))。 -然而,在训练数据上表现良好的模型,并不一定在“新数据集”上有同样的效能,这里的“新数据集”通常称为*测试数据集*(test dataset,或称为*测试集*(test set))。 +然而,在训练数据上表现良好的模型,并不一定在“新数据集”上有同样的性能,这里的“新数据集”通常称为*测试数据集*(test dataset,或称为*测试集*(test set))。 -综上所述,我们通常将可用数据集分成两部分:训练数据集用于拟合模型参数,测试数据集用于评估拟合的模型。 -然后我们观察模型在这两部分数据集的效能。 -你可以把“一个模型在训练数据集上的效能”想象成“一个学生在模拟考试中的分数”。 +综上所述,可用数据集通常可以分成两部分:训练数据集用于拟合模型参数,测试数据集用于评估拟合的模型。 +然后我们观察模型在这两部分数据集的性能。 +“一个模型在训练数据集上的性能”可以被想象成“一个学生在模拟考试中的分数”。 这个分数用来为一些真正的期末考试做参考,即使成绩令人鼓舞,也不能保证期末考试成功。 换言之,测试性能可能会显著偏离训练性能。 -当一个模型在训练集上表现良好,但不能推广到测试集时,我们说这个模型是“过拟合”(overfitting)的。 +当一个模型在训练集上表现良好,但不能推广到测试集时,这个模型被称为*过拟合*(overfitting)的。 就像在现实生活中,尽管模拟考试考得很好,真正的考试不一定百发百中。 ### 优化算法 -一旦我们获得了一些数据源及其表示、一个模型和一个合适的损失函数,我们接下来就需要一种算法,它能够搜索出最佳参数,以最小化损失函数。 +当我们获得了一些数据源及其表示、一个模型和一个合适的损失函数,接下来就需要一种算法,它能够搜索出最佳参数,以最小化损失函数。 深度学习中,大多流行的优化算法通常基于一种基本方法--*梯度下降*(gradient descent)。 -简而言之,在每个步骤中,梯度下降法都会检查每个参数,看看如果你仅对该参数进行少量变动,训练集损失会朝哪个方向移动。 +简而言之,在每个步骤中,梯度下降法都会检查每个参数,看看如果仅对该参数进行少量变动,训练集损失会朝哪个方向移动。 然后,它在可以减少损失的方向上优化参数。 ## 各种机器学习问题 在机器学习的广泛应用中,唤醒词问题只是冰山一角。 -在前面的例子中,只是机器学习可以解决的众多问题中的一个。 -下面,我们将列出一些常见机器学习问题和应用,为之后本书的讨论做铺垫。 -我们将不断引用前面提到的概念,如数据、模型和训练技术。 +前面唤醒词识别的例子,只是机器学习可以解决的众多问题中的一个。 +下面将列出一些常见的机器学习问题和应用,为之后本书的讨论做铺垫。 +接下来会经常引用前面提到的概念,如数据、模型和优化算法。 ### 监督学习 *监督学习*(supervised learning)擅长在“给定输入特征”的情况下预测标签。 每个“特征-标签”对都称为一个*样本*(example)。 有时,即使标签是未知的,样本也可以指代输入特征。 -我们的目标是生成一个模型,能够将任何输入特征映射到标签,即预测。 +我们的目标是生成一个模型,能够将任何输入特征映射到标签(即预测)。 举一个具体的例子: -假设我们需要预测患者是否会心脏病发作,那么观察结果“心脏病发作”或“心脏病没有发作”将是我们的标签。 -输入特征可能是生命体征,如心率、舒张压和收缩压。 +假设我们需要预测患者的心脏病是否会发作,那么观察结果“心脏病发作”或“心脏病没有发作”将是样本的标签。 +输入特征可能是生命体征,如心率、舒张压和收缩压等。 -监督学习之所以发挥作用,是因为在训练参数时,我们为模型提供了一个数据集,其中每个样本都有真实的标签。 +监督学习之所以能发挥作用,是因为在训练参数时,我们为模型提供了一个数据集,其中每个样本都有真实的标签。 用概率论术语来说,我们希望预测“估计给定输入特征的标签”的条件概率。 -虽然监督学习只是几大类机器学习问题之一,但是在工业中,大部分机器学习的成功应用都是监督学习。 -这是因为在一定程度上,许多重要的任务可以清晰地描述为:在给定一组特定的可用数据的情况下,估计未知事物的概率。比如: - -* 根据计算机断层扫描(CT)肿瘤图像,预测是否为癌症。 -* 给出一个英语句子,预测正确的法语翻译。 -* 根据本月的财务报告数据,预测下个月股票的价格。 - -非正式地说,监督学习的学习过程如下所示。 -首先,从已知大量数据样本中随机选取一个子集,为每个样本获取基本的真实标签。 -有时,这些样本已有标签(例如,患者是否在下一年内康复?); -有时,我们可能需要人工标记数据(例如,将图像分类)。 -这些输入和相应的标签一起构成了训练数据集。 -随后,我们选择有监督的学习算法,它将训练数据集作为输入,并输出一个“完成学习模型”。 -最后,我们将之前没见过的样本特征放到这个“完成学习模型”中,使用模型的输出作为相应标签的预测。 -整个监督学习过程在 :numref:`fig_supervised_learning` 中绘制。 +虽然监督学习只是几大类机器学习问题之一,但是在工业中,大部分机器学习的成功应用都使用了监督学习。 +这是因为在一定程度上,许多重要的任务可以清晰地描述为,在给定一组特定的可用数据的情况下,估计未知事物的概率。比如: + +* 根据计算机断层扫描(Computed Tomography,CT)肿瘤图像,预测是否为癌症; +* 给出一个英语句子,预测正确的法语翻译; +* 根据本月的财务报告数据,预测下个月股票的价格; + +监督学习的学习过程一般可以分为三大步骤: + +1. 从已知大量数据样本中随机选取一个子集,为每个样本获取真实标签。有时,这些样本已有标签(例如,患者是否在下一年内康复?);有时,这些样本可能需要被人工标记(例如,图像分类)。这些输入和相应的标签一起构成了训练数据集; +2. 选择有监督的学习算法,它将训练数据集作为输入,并输出一个“已完成学习的模型”; +3. 将之前没有见过的样本特征放到这个“已完成学习的模型”中,使用模型的输出作为相应标签的预测。 + +整个监督学习过程如 :numref:`fig_supervised_learning` 所示。 ![监督学习](../img/supervised-learning.svg) :label:`fig_supervised_learning` -综上所述,即使使用简单的描述“给定输入特征的预测标签”,监督学习也可以采取多种形式的模型,并且需要大量不同的建模决策,这取决于输入和输出的类型、大小和数量。 +综上所述,即使使用简单的描述给定输入特征的预测标签,监督学习也可以采取多种形式的模型,并且需要大量不同的建模决策,这取决于输入和输出的类型、大小和数量。 例如,我们使用不同的模型来处理“任意长度的序列”或“固定长度的序列”。 -我们将在本书中深入探讨这些问题。 #### 回归 *回归*(regression)是最简单的监督学习任务之一。 -比方说,假设我们有一组房屋销售数据表格,其中每行对应于每个房子,每列对应于一些相关的属性,例如房屋的面积、卧室的数量、浴室的数量以及到镇中心的步行分钟数等等。 -对机器学习来说,每个样本即为一个特定的房屋,相应的特征向量将是表中的一行。 -如果你住在纽约或旧金山,而且你不是亚马逊、谷歌、微软或Facebook的首席执行官,那么你家中的(平方英尺、卧室数量、浴室数量、步行距离)特征向量可能类似于:$[600, 1, 1, 60]$。 -然而,如果你住在匹兹堡,这个特征向量可能看起来更像$[3000, 4, 3, 10]$...... +假设有一组房屋销售数据表格,其中每行对应一个房子,每列对应一个相关的属性,例如房屋的面积、卧室的数量、浴室的数量以及到镇中心的步行距离,等等。 +每一行的属性构成了一个房子样本的特征向量。 +如果一个人住在纽约或旧金山,而且他不是亚马逊、谷歌、微软或Facebook的首席执行官,那么他家的特征向量(房屋面积,卧室数量,浴室数量,步行距离)可能类似于:$[600, 1, 1, 60]$。 +如果一个人住在匹兹堡,这个特征向量可能更接近$[3000, 4, 3, 10]$...... +当人们在市场上寻找新房子时,可能需要估计一栋房子的公平市场价值。 为什么这个任务可以归类为回归问题呢?本质上是输出决定的。 -假设你在市场上寻找新房子,你可能需要估计一栋房子的公平市场价值。 -销售价格,即标签,是一个数值。 -当标签取任意数值时,我们称之为*回归*问题。 -我们的目标是生成一个模型,它的预测非常接近实际标签值。 +销售价格(即标签)是一个数值。 +当标签取任意数值时,我们称之为*回归*问题,此时的目标是生成一个模型,使它的预测非常接近实际标签值。 生活中的许多问题都可归类为回归问题。 -比如,预测用户对一部电影的评分可以被认为是一个回归问题。 -这里有一个小插曲,如果你在2009年设计了一个很棒的算法来预测电影评分,你可能会赢得[100万美元的奈飞奖](https://en.wikipedia.org/wiki/Netflix_Prize)。 +比如,预测用户对一部电影的评分可以被归类为一个回归问题。 +这里有一个小插曲:在2009年,如果有人设计了一个很棒的算法来预测电影评分,那可能会赢得[100万美元的奈飞奖](https://en.wikipedia.org/wiki/Netflix_Prize)。 再比如,预测病人在医院的住院时间也是一个回归问题。 -总而言之,判断回归问题的一个很好的经验法则是,任何有关“多少”的问题很可能就是回归问题。比如: +总而言之,判断回归问题的一个很好的经验法则是,任何有关“有多少”的问题很可能就是回归问题。比如: -* 这个手术需要多少小时? -* 在未来六小时,这个镇会有多少降雨量? +* 这个手术需要多少小时; +* 在未来6小时,这个镇会有多少降雨量。 -你可能发现,即使你以前从未使用过机器学习,可能在不经意间,你已经解决了一些回归问题。 -例如,你让人修理了排水管,你的承包商花了3个小时清除污水管道中的污物,然后他寄给你一张350美元的账单。 -而你的朋友雇了同一个承包商两个小时,他收到了250美元的账单。 -如果有人请你估算清理污物的费用,你可以假设承包商有一些基本费用,然后按小时收费。 -如果这些假设成立,那么给出这两个数据样本,你就已经可以确定承包商的定价结构:每小时100美元,外加50美元上门服务费。 -你看,在不经意间,你就已经理解并应用了线性回归的本质。 +即使你以前从未使用过机器学习,可能在不经意间,已经解决了一些回归问题。 +例如,你让人修理了排水管,承包商花了3小时清除污水管道中的污物,然后他寄给你一张350美元的账单。 +而你的朋友雇了同一个承包商2小时,他收到了250美元的账单。 +如果有人请你估算清理污物的费用,你可以假设承包商收取一些基本费用,然后按小时收费。 +如果这些假设成立,那么给出这两个数据样本,你就已经可以确定承包商的定价结构:50美元上门服务费,另外每小时100美元。 +在不经意间,你就已经理解并应用了线性回归算法。 -然而以上假设有时并不可取。 -例如,如果一些差异是由于两个特征之外的几个因素造成的。 +然而,以上假设有时并不可取。 +例如,一些差异是由于两个特征之外的几个因素造成的。 在这些情况下,我们将尝试学习最小化“预测值和实际标签值的差异”的模型。 -在本书大部分章节中,我们将关注最小化平方误差损失函数。 -正如我们稍后将看到的,这种损失对应于我们的数据被高斯噪声破坏的假设。 +本书大部分章节将关注平方误差损失函数的最小化。 #### 分类 -虽然回归模型可以很好地解决“有多少?”的问题,但是很多问题并非如此。 +虽然回归模型可以很好地解决“有多少”的问题,但是很多问题并非如此。 例如,一家银行希望在其移动应用程序中添加支票扫描功能。 具体地说,这款应用程序能够自动理解从图像中看到的文本,并将手写字符映射到对应的已知字符之上。 -这种“哪一个?”的问题叫做*分类*(classification)问题。 -在*分类*问题中,我们希望模型能够预测样本属于哪个*类别*(category,正式称为*类*(class))。 -例如,对于手写数字,我们可能有10类,分别数字0到9。 -最简单的分类问题是只有两类,我们称之为“二元分类”。 +这种“哪一个”的问题叫做*分类*(classification)问题。 +*分类*问题希望模型能够预测样本属于哪个*类别*(category,正式称为*类*(class))。 +例如,手写数字可能有10类,标签被设置为数字0~9。 +最简单的分类问题是只有两类,这被称之为*二项分类*(binomial classification)。 例如,数据集可能由动物图像组成,标签可能是$\mathrm{\{猫, 狗\}}$两类。 -在回归中,我们训练一个回归函数来输出一个数值; -而在分类中,我们训练一个分类器,它的输出即为预测的类别。 +回归是训练一个回归函数来输出一个数值; +分类是训练一个分类器来输出预测的类别。 然而模型怎么判断得出这种“是”或“不是”的硬分类预测呢? 我们可以试着用概率语言来理解模型。 -给定一个样本特征,我们的模型为每个可能的类分配一个概率。 +给定一个样本特征,模型为每个可能的类分配一个概率。 比如,之前的猫狗分类例子中,分类器可能会输出图像是猫的概率为0.9。 0.9这个数字表达什么意思呢? -我们可以这样解释:分类器90%确定图像描绘的是一只猫。 -预测类别的概率的大小传达了一种模型的不确定性,我们将在后面章节中讨论其他运用不确定性概念的算法。 +可以这样理解:分类器90%确定图像描绘的是一只猫。 +预测类别的概率的大小传达了一种模型的不确定性,本书后面章节将讨论其他运用不确定性概念的算法。 -当我们有两个以上的类别时,我们把这个问题称为*多元分类*(multiclass classification)问题。 +当有两个以上的类别时,我们把这个问题称为*多项分类*(multiclass classification)问题。 常见的例子包括手写字符识别 $\mathrm{\{0, 1, 2, ... 9, a, b, c, ...\}}$。 -与解决回归问题不同,分类问题的常见损失函数被称为*交叉熵*(cross-entropy),我们将在后面的章节中详细阐述。 +与解决回归问题不同,分类问题的常见损失函数被称为*交叉熵*(cross-entropy),本书 :numref:`sec_softmax` 将详细阐述。 -请注意,最常见的类别不一定是你将用于决策的类别。 -举个例子,假设你在后院发现了一个美丽的蘑菇,如 :numref:`fig_death_cap` 所示。 +请注意,最常见的类别不一定是最终用于决策的类别。 +举个例子,假设后院有一个如 :numref:`fig_death_cap` 所示的蘑菇。 ![死帽蕈——不能吃!!](../img/death-cap.jpg) :width:`200px` :label:`fig_death_cap` -现在,请你训练一个毒蘑菇检测分类器,根据照片预测蘑菇是否有毒。 +现在,我们想要训练一个毒蘑菇检测分类器,根据照片预测蘑菇是否有毒。 假设这个分类器输出 :numref:`fig_death_cap` 包含死帽蕈的概率是0.2。 -换句话说,分类器80%确定我们的蘑菇不是死帽蕈。 -尽管如此,我们也不会吃它,因为我们不值得冒20%的死亡风险。 +换句话说,分类器80%确定图中的蘑菇不是死帽蕈。 +尽管如此,我们也不会吃它,因为不值得冒20%的死亡风险。 换句话说,不确定风险的影响远远大于收益。 -因此,我们需要将“预期风险”作为损失函数。 -也就是说,我们需要将结果的概率乘以与之相关的收益(或伤害)。 +因此,我们需要将“预期风险”作为损失函数,即需要将结果的概率乘以与之相关的收益(或伤害)。 在这种情况下,食用蘑菇造成的损失为$0.2 \times \infty + 0.8 \times 0 = \infty$,而丢弃蘑菇的损失为$0.2 \times 0 + 0.8 \times 1 = 0.8$。 -事实上,我们的谨慎是有道理的, :numref:`fig_death_cap`中的蘑菇实际上是一个死帽蕈。 +事实上,谨慎是有道理的, :numref:`fig_death_cap`中的蘑菇实际上是一个死帽蕈。 -分类可能变得比二元分类、多元分类复杂得多。 +分类可能变得比二项分类、多项分类复杂得多。 例如,有一些分类任务的变体可以用于寻找层次结构,层次结构假定在许多类之间存在某种关系。 因此,并不是所有的错误都是均等的。 -我们宁愿错误地分入一个相关的类别,也不愿错误地分入一个遥远的类别,这通常被称为*层次分类*(hierarchical classification)。 +人们宁愿错误地分入一个相关的类别,也不愿错误地分入一个遥远的类别,这通常被称为*层次分类*(hierarchical classification)。 早期的一个例子是[卡尔·林奈](https://en.wikipedia.org/wiki/Carl_Linnaeus),他对动物进行了层次分类。 在动物分类的应用中,把一只狮子狗误认为雪纳瑞可能不会太糟糕。 -但如果我们的模型将狮子狗与恐龙混淆,就滑稽至极了。 -层次结构相关性可能取决于你计划如何使用模型。 +但如果模型将狮子狗与恐龙混淆,就滑稽至极了。 +层次结构相关性可能取决于模型的使用者计划如何使用模型。 例如,响尾蛇和乌梢蛇血缘上可能很接近,但如果把响尾蛇误认为是乌梢蛇可能会是致命的。 因为响尾蛇是有毒的,而乌梢蛇是无毒的。 #### 标记问题 -有些分类问题很适合于二元分类或多元分类。 -例如,我们可以训练一个普通的二元分类器来区分猫和狗。 -运用最前沿的计算机视觉的算法,我们可以轻松地训练这个模型。 -尽管如此,无论我们的模型有多精确,当分类器遇到新的动物时可能会束手无策。 -比如这张“不来梅的城市音乐家”的图像 :numref:`fig_stackedanimals` (这是一个流行的德国童话故事),图中有一只猫,一只公鸡,一只狗,一头驴,背景是一些树。 -取决于我们最终想用我们的模型做什么,将其视为二元分类问题可能没有多大意义。 -取而代之,我们可能想让模型描绘输入图像的内容,一只猫、一只狗、一头驴,还有一只公鸡。 +有些分类问题很适合于二项分类或多项分类。 +例如,我们可以训练一个普通的二项分类器来区分猫和狗。 +运用最前沿的计算机视觉的算法,这个模型可以很轻松地被训练。 +尽管如此,无论模型有多精确,当分类器遇到新的动物时可能会束手无策。 +比如 :numref:`fig_stackedanimals`所示的这张“不来梅的城市音乐家”的图像 (这是一个流行的德国童话故事),图中有一只猫、一只公鸡、一只狗、一头驴,背景是一些树。 +取决于我们最终想用模型做什么,将其视为二项分类问题可能没有多大意义。 +取而代之,我们可能想让模型描绘输入图像的内容,一只猫、一只公鸡、一只狗,还有一头驴。 -![一头驴,一只狗,一只猫和一只公鸡](../img/stackedanimals.png) +![一只猫、一只公鸡、一只狗、一头驴](../img/stackedanimals.png) :width:`300px` :label:`fig_stackedanimals` 学习预测不相互排斥的类别的问题称为*多标签分类*(multi-label classification)。 -举个例子,人们在技术博客上贴的标签,比如“机器学习”、“技术”、“小工具”、“编程语言”、“Linux”、“云计算”、“AWS”。 -一篇典型的文章可能会用5-10个标签,因为这些概念是相互关联的。 +举个例子,人们在技术博客上贴的标签,比如“机器学习”“技术”“小工具”“编程语言”“Linux”“云计算”“AWS”。 +一篇典型的文章可能会用5~10个标签,因为这些概念是相互关联的。 关于“云计算”的帖子可能会提到“AWS”,而关于“机器学习”的帖子也可能涉及“编程语言”。 此外,在处理生物医学文献时,我们也会遇到这类问题。 正确地标记文献很重要,有利于研究人员对文献进行详尽的审查。 -在国家医学图书馆,一些专业的注释员会检查每一篇在PubMed中被索引的文章,以便将其与Mesh中的相关术语相关联(Mesh是一个大约有28000个标签的集合)。 +在美国国家医学图书馆(The United States National Library of Medicine),一些专业的注释员会检查每一篇在PubMed中被索引的文章,以便将其与Mesh中的相关术语相关联(Mesh是一个大约有28000个标签的集合)。 这是一个十分耗时的过程,注释器通常在归档和标记之间有一年的延迟。 这里,机器学习算法可以提供临时标签,直到每一篇文章都有严格的人工审核。 事实上,近几年来,BioASQ组织已经[举办比赛](http://bioasq.org/)来完成这项工作。 @@ -355,10 +346,10 @@ #### 搜索 -有时,我们不仅仅希望输出为一个类别或一个实值。 +有时,我们不仅仅希望输出一个类别或一个实值。 在信息检索领域,我们希望对一组项目进行排序。 -以网络搜索为例,我们的目标不是简单的“查询(query)-网页(page)”分类,而是在海量搜索结果中找到用户最需要的那部分。 -搜索结果的排序也十分重要,我们的学习算法需要输出有序的元素子集。 +以网络搜索为例,目标不是简单的“查询(query)-网页(page)”分类,而是在海量搜索结果中找到用户最需要的那部分。 +搜索结果的排序也十分重要,学习算法需要输出有序的元素子集。 换句话说,如果要求我们输出字母表中的前5个字母,返回“A、B、C、D、E”和“C、A、B、E、D”是不同的。 即使结果集是相同的,集内的顺序有时却很重要。 @@ -395,19 +386,19 @@ #### 序列学习 以上大多数问题都具有固定大小的输入和产生固定大小的输出。 -例如,在预测房价的问题中,我们考虑从一组固定的特征:平方英尺、卧室数量、浴室数量、步行到市中心的时间; +例如,在预测房价的问题中,我们考虑从一组固定的特征:房屋面积、卧室数量、浴室数量、步行到市中心的时间; 图像分类问题中,输入为固定尺寸的图像,输出则为固定数量(有关每一个类别)的预测概率; 在这些情况下,模型只会将输入作为生成输出的“原料”,而不会“记住”输入的具体内容。 如果输入的样本之间没有任何关系,以上模型可能完美无缺。 -但是如果输入是连续的,我们的模型可能就需要拥有“记忆”功能。 +但是如果输入是连续的,模型可能就需要拥有“记忆”功能。 比如,我们该如何处理视频片段呢? 在这种情况下,每个视频片段可能由不同数量的帧组成。 通过前一帧的图像,我们可能对后一帧中发生的事情更有把握。 语言也是如此,机器翻译的输入和输出都为文字序列。 再比如,在医学上序列输入和输出就更为重要。 -设想一下,假设我们用一个模型来监控重症监护病人,如果他们在未来24小时内死亡的风险超过某个阈值,这个模型就会发出警报。 +设想一下,假设一个模型被用来监控重症监护病人,如果他们在未来24小时内死亡的风险超过某个阈值,这个模型就会发出警报。 我们绝不希望抛弃过去每小时有关病人病史的所有信息,而仅根据最近的测量结果做出预测。 这些问题是序列学习的实例,是机器学习最令人兴奋的应用之一。 @@ -417,7 +408,7 @@ **标记和解析**。这涉及到用属性注释文本序列。 换句话说,输入和输出的数量基本上是相同的。 -例如,我们可能想知道动词和主语在哪里,或者,我们可能想知道哪些单词是命名实体。 +例如,我们可能想知道动词和主语在哪里,或者可能想知道哪些单词是命名实体。 通常,目标是基于结构和语法假设对文本进行分解和注释,以获得一些注释。 这听起来比实际情况要复杂得多。 下面是一个非常简单的示例,它使用“标记”来注释一个句子,该标记指示哪些单词引用命名实体。 @@ -464,17 +455,16 @@ Ent - - - Ent - Ent ### 无监督学习 -到目前为止,所有的例子都与监督学习有关,即我们向模型提供巨大数据集:每个样本包含特征和相应标签值。 +到目前为止,所有的例子都与监督学习有关,即需要向模型提供巨大数据集:每个样本包含特征和相应标签值。 打趣一下,“监督学习”模型像一个打工仔,有一份极其专业的工作和一位极其平庸的老板。 老板站在身后,准确地告诉模型在每种情况下应该做什么,直到模型学会从情况到行动的映射。 取悦这位老板很容易,只需尽快识别出模式并模仿他们的行为即可。 -相反,如果你的工作没有十分具体的目标,你就需要“自发”地去学习了。 -(如果你打算成为一名数据科学家,你最好培养这个习惯。) -比如,你的老板可能会给你一大堆数据,然后让你用它做一些数据科学研究,却没有对结果有要求。 -我们称这类数据中不含有“目标”的机器学习问题为*无监督学习*(unsupervised learning), -我们将在后面的章节中讨论无监督学习技术。 -那么无监督学习可以回答什么样的问题呢?我们来看看下面的例子: +相反,如果工作没有十分具体的目标,就需要“自发”地去学习了。 +比如,老板可能会给我们一大堆数据,然后要求用它做一些数据科学研究,却没有对结果有要求。 +这类数据中不含有“目标”的机器学习问题通常被为*无监督学习*(unsupervised learning), +本书后面的章节将讨论无监督学习技术。 +那么无监督学习可以回答什么样的问题呢?来看看下面的例子。 * *聚类*(clustering)问题:没有标签的情况下,我们是否能给数据分类呢?比如,给定一组照片,我们能把它们分成风景照片、狗、婴儿、猫和山峰的照片吗?同样,给定一组用户的网页浏览记录,我们能否将具有相似行为的用户聚类呢? * *主成分分析*(principal component analysis)问题:我们能否找到少量的参数来准确地捕捉数据的线性相关属性?比如,一个球的运动轨迹可以用球的速度、直径和质量来描述。再比如,裁缝们已经开发出了一小部分参数,这些参数相当准确地描述了人体的形状,以适应衣服的需要。另一个例子:在欧几里得空间中是否存在一种(任意结构的)对象的表示,使其符号属性能够很好地匹配?这可以用来描述实体及其关系,例如“罗马” $-$ “意大利” $+$ “法国” $=$ “巴黎”。 @@ -484,7 +474,7 @@ Ent - - - Ent - Ent ### 与环境互动 -你可能一直心存疑虑:机器学习的输入(数据)来自哪里?机器学习的输出又将去往何方? +有人一直心存疑虑:机器学习的输入(数据)来自哪里?机器学习的输出又将去往何方? 到目前为止,不管是监督学习还是无监督学习,我们都会预先获取大量数据,然后启动模型,不再与环境交互。 这里所有学习都是在算法与环境断开后进行的,被称为*离线学习*(offline learning)。 对于监督学习,从环境中收集数据的过程类似于 :numref:`fig_data_collection`。 @@ -495,12 +485,12 @@ Ent - - - Ent - Ent 这种简单的离线学习有它的魅力。 好的一面是,我们可以孤立地进行模式识别,而不必分心于其他问题。 但缺点是,解决的问题相当有限。 -如果你更有雄心壮志,那么你可能会期望人工智能不仅能够做出预测,而且能够与真实环境互动。 +这时我们可能会期望人工智能不仅能够做出预测,而且能够与真实环境互动。 与预测不同,“与真实环境互动”实际上会影响环境。 这里的人工智能是“智能代理”,而不仅是“预测模型”。 因此,我们必须考虑到它的行为可能会影响未来的观察结果。 -考虑“与真实环境互动”将打开一整套新的建模问题。以下只是几个例子: +考虑“与真实环境互动”将打开一整套新的建模问题。以下只是几个例子。 * 环境还记得我们以前做过什么吗? * 环境是否有助于我们建模?例如,用户将文本读入语音识别器。 @@ -509,41 +499,41 @@ Ent - - - Ent - Ent * 环境是否变化?例如,未来的数据是否总是与过去相似,还是随着时间的推移会发生变化?是自然变化还是响应我们的自动化工具而发生变化? 当训练和测试数据不同时,最后一个问题提出了*分布偏移*(distribution shift)的问题。 -接下来,我们将简要描述强化学习问题,这是一类明确考虑与环境交互的问题。 +接下来的内容将简要描述强化学习问题,这是一类明确考虑与环境交互的问题。 ### 强化学习 -如果你对使用机器学习开发与环境交互并采取行动感兴趣,那么你最终可能会专注于*强化学习*(reinforcement learning)。 +如果你对使用机器学习开发与环境交互并采取行动感兴趣,那么最终可能会专注于*强化学习*(reinforcement learning)。 这可能包括应用到机器人、对话系统,甚至开发视频游戏的人工智能(AI)。 *深度强化学习*(deep reinforcement learning)将深度学习应用于强化学习的问题,是非常热门的研究领域。 突破性的深度*Q网络*(Q-network)在雅达利游戏中仅使用视觉输入就击败了人类, 以及 AlphaGo 程序在棋盘游戏围棋中击败了世界冠军,是两个突出强化学习的例子。 -在强化学习问题中,agent在一系列的时间步骤上与环境交互。 -在每个特定时间点,agent从环境接收一些*观察*(observation),并且必须选择一个*动作*(action),然后通过某种机制(有时称为执行器)将其传输回环境,最后agent从环境中获得*奖励*(reward)。 -此后新一轮循环开始,agent接收后续观察,并选择后续操作,依此类推。 +在强化学习问题中,智能体(agent)在一系列的时间步骤上与环境交互。 +在每个特定时间点,智能体从环境接收一些*观察*(observation),并且必须选择一个*动作*(action),然后通过某种机制(有时称为执行器)将其传输回环境,最后智能体从环境中获得*奖励*(reward)。 +此后新一轮循环开始,智能体接收后续观察,并选择后续操作,依此类推。 强化学习的过程在 :numref:`fig_rl-environment` 中进行了说明。 请注意,强化学习的目标是产生一个好的*策略*(policy)。 -强化学习agent选择的“动作”受策略控制,即一个从环境观察映射到行动的功能。 +强化学习智能体选择的“动作”受策略控制,即一个从环境观察映射到行动的功能。 ![强化学习和环境之间的相互作用](../img/rl-environment.svg) :label:`fig_rl-environment` 强化学习框架的通用性十分强大。 例如,我们可以将任何监督学习问题转化为强化学习问题。 -假设我们有一个分类问题,我们可以创建一个强化学习agent,每个分类对应一个“动作”。 -然后,我们可以创建一个环境,该环境给予agent的奖励。 +假设我们有一个分类问题,可以创建一个强化学习智能体,每个分类对应一个“动作”。 +然后,我们可以创建一个环境,该环境给予智能体的奖励。 这个奖励与原始监督学习问题的损失函数是一致的。 当然,强化学习还可以解决许多监督学习无法解决的问题。 例如,在监督学习中,我们总是希望输入与正确的标签相关联。 -但在强化学习中,我们并不假设环境告诉agent每个观测的最优动作。 -一般来说,agent只是得到一些奖励。 -此外,环境甚至可能不会告诉我们是哪些行为导致了奖励。 +但在强化学习中,我们并不假设环境告诉智能体每个观测的最优动作。 +一般来说,智能体只是得到一些奖励。 +此外,环境甚至可能不会告诉是哪些行为导致了奖励。 以强化学习在国际象棋的应用为例。 -唯一真正的奖励信号出现在游戏结束时:当agent获胜时,agent可以得到奖励1;当agent失败时,agent将得到奖励-1。 +唯一真正的奖励信号出现在游戏结束时:当智能体获胜时,智能体可以得到奖励1;当智能体失败时,智能体将得到奖励-1。 因此,强化学习者必须处理*学分分配*(credit assignment)问题:决定哪些行为是值得奖励的,哪些行为是需要惩罚的。 就像一个员工升职一样,这次升职很可能反映了前一年的大量的行动。 要想在未来获得更多的晋升,就需要弄清楚这一过程中哪些行为导致了晋升。 @@ -553,16 +543,16 @@ Ent - - - Ent - Ent 比方说,一个清洁机器人发现自己被困在一个许多相同的壁橱的房子里。 推断机器人的精确位置(从而推断其状态),需要在进入壁橱之前考虑它之前的观察结果。 -最后,在任何时间点上,强化学习agent可能知道一个好的策略,但可能有许多更好的策略从未尝试过的。 -强化学习agent必须不断地做出选择:是应该利用当前最好的策略,还是探索新的策略空间(放弃一些短期回报来换取知识)。 +最后,在任何时间点上,强化学习智能体可能知道一个好的策略,但可能有许多更好的策略从未尝试过的。 +强化学习智能体必须不断地做出选择:是应该利用当前最好的策略,还是探索新的策略空间(放弃一些短期回报来换取知识)。 一般的强化学习问题是一个非常普遍的问题。 -agent的动作会影响后续的观察,而奖励只与所选的动作相对应。 +智能体的动作会影响后续的观察,而奖励只与所选的动作相对应。 环境可以是完整观察到的,也可以是部分观察到的,解释所有这些复杂性可能会对研究人员要求太高。 此外,并不是每个实际问题都表现出所有这些复杂性。 因此,学者们研究了一些特殊情况下的强化学习问题。 -当环境可被完全观察到时,我们将强化学习问题称为*马尔可夫决策过程*(markov decision process)。 +当环境可被完全观察到时,强化学习问题被称为*马尔可夫决策过程*(markov decision process)。 当状态不依赖于之前的操作时,我们称该问题为*上下文赌博机*(contextual bandit problem)。 当没有状态,只有一组最初未知回报的可用动作时,这个问题就是经典的*多臂赌博机*(multi-armed bandit problem)。 @@ -570,17 +560,17 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 ## 起源 为了解决各种各样的机器学习问题,深度学习提供了强大的工具。 -虽然许多深度学习方法都是最近的才有重大突破,但使用数据和神经网络编程的核心思想已经研究了几个世纪。 +虽然许多深度学习方法都是最近才有重大突破,但使用数据和神经网络编程的核心思想已经研究了几个世纪。 事实上,人类长期以来就有分析数据和预测未来结果的愿望,而自然科学大部分都植根于此。 -例如,伯努利分布是以[雅各布•伯努利(1654--1705)](https://en.wikipedia.org/wiki/Jacob\uBernoulli)命名的。 -而高斯分布是由[卡尔•弗里德里希•高斯(1777—1855)](https://en.wikipedia.org/wiki/Carl_Friedrich_Gauss)发现的, +例如,伯努利分布是以[雅各布•伯努利(1654-1705)](https://en.wikipedia.org/wiki/Jacob\uBernoulli)命名的。 +而高斯分布是由[卡尔•弗里德里希•高斯(1777-1855)](https://en.wikipedia.org/wiki/Carl_Friedrich_Gauss)发现的, 他发明了最小均方算法,至今仍用于解决从保险计算到医疗诊断的许多问题。 这些工具算法催生了自然科学中的一种实验方法——例如,电阻中电流和电压的欧姆定律可以用线性模型完美地描述。 即使在中世纪,数学家对*估计*(estimation)也有敏锐的直觉。 例如,[雅各布·克贝尔 (1460--1533)](https://www.maa.org/press/periodicals/convergence/mathematical-treasures-jacob-kobels-geometry)的几何学书籍举例说明,通过平均16名成年男性的脚的长度,可以得出一英尺的长度。 -![估计一英尺的长度。](../img/koebel.jpg) +![估计一英尺的长度](../img/koebel.jpg) :width:`500px` :label:`fig_koebel` @@ -610,10 +600,10 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 随着时间的推移,对生物学的解释变得不再肤浅,但这个名字仍然存在。 其核心是当今大多数网络中都可以找到的几个关键原则: -* 线性和非线性处理单元的交替,通常称为*层*(layers)。 +* 线性和非线性处理单元的交替,通常称为*层*(layers); * 使用链式规则(也称为*反向传播*(backpropagation))一次性调整网络中的全部参数。 -在最初的快速发展之后,神经网络的研究从1995年左右一直开始停滞不前,直到到2005年才稍有起色。 +经过最初的快速发展,神经网络的研究从1995年左右开始停滞不前,直到2005年才稍有起色。 这主要是因为两个原因。 首先,训练网络(在计算上)非常昂贵。 在上个世纪末,随机存取存储器(RAM)非常强大,而计算能力却很弱。 @@ -625,10 +615,10 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 -## 深度学习之路 +## 深度学习的发展 大约2010年开始,那些在计算上看起来不可行的神经网络算法变得热门起来,实际上是以下两点导致的: -其一,随着互联网的公司的出现,为数亿在线用户提供服务,大规模数据集变得触手可及。 +其一,随着互联网的公司的出现,为数亿在线用户提供服务,大规模数据集变得触手可及; 另外,廉价又高质量的传感器、廉价的数据存储(克莱德定律)以及廉价计算(摩尔定律)的普及,特别是GPU的普及,使大规模算力唾手可得。 这一点在 :numref:`tab_intro_decade` 中得到了说明。 @@ -653,7 +643,7 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 最近十年,在统计模型、应用和算法方面的进展就像寒武纪大爆发——历史上物种飞速进化的时期。 事实上,最先进的技术不仅仅是将可用资源应用于几十年前的算法的结果。 -下面列举了帮助研究人员在过去十年中取得巨大进步的想法(虽然只触及了皮毛): +下面列举了帮助研究人员在过去十年中取得巨大进步的想法(虽然只触及了皮毛)。 * 新的容量控制方法,如*dropout* :cite:`Srivastava.Hinton.Krizhevsky.ea.2014`,有助于减轻过拟合的危险。这是通过在整个神经网络中应用噪声注入 :cite:`Bishop.1995` 来实现的,出于训练目的,用随机变量来代替权重。 @@ -665,11 +655,11 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 * 深度学习框架在传播思想方面发挥了至关重要的作用。允许轻松建模的第一代框架包括[Caffe](https://github.com/BVLC/caffe)、[Torch](https://github.com/torch)和[Theano](https://github.com/Theano/Theano)。许多开创性的论文都是用这些工具写的。到目前为止,它们已经被[TensorFlow](https://github.com/tensorflow/tensorflow)(通常通过其高级API [Keras](https://github.com/keras-team/keras)使用)、[CNTK](https://github.com/Microsoft/CNTK)、[Caffe 2](https://github.com/caffe2/caffe2)和[Apache MXNet](https://github.com/apache/incubator-mxnet)所取代。第三代工具,即用于深度学习的命令式工具,可以说是由[Chainer](https://github.com/chainer/chainer)率先推出的,它使用类似于Python NumPy的语法来描述模型。这个想法被[PyTorch](https://github.com/pytorch/pytorch)、MXNet的[Gluon API](https://github.com/apache/incubator-mxnet)和[Jax](https://github.com/google/jax)都采纳了。 “系统研究人员构建更好的工具”和“统计建模人员构建更好的神经网络”之间的分工大大简化了工作。 -例如,在2014年,对于卡内基梅隆大学机器学习博士生来说,训练线性回归模型曾经是一个不容易的作业问题。 +例如,在2014年,对卡内基梅隆大学机器学习博士生来说,训练线性回归模型曾经是一个不容易的作业问题。 而现在,这项任务只需不到10行代码就能完成,这让每个程序员轻易掌握了它。 -## 成功案例 +## 深度学习的成功案例 人工智能在交付结果方面有着悠久的历史,它能带来用其他方法很难实现的结果。例如,使用光学字符识别的邮件分拣系统从20世纪90年代开始部署,毕竟,这是著名的手写数字MNIST数据集的来源。这同样适用于阅读银行存款支票和对申请者的信用进行评分。系统会自动检查金融交易是否存在欺诈。这成为许多电子商务支付系统的支柱,如PayPal、Stripe、支付宝、微信、苹果、Visa和万事达卡。国际象棋的计算机程序已经竞争了几十年。机器学习在互联网上提供搜索、推荐、个性化和排名。换句话说,机器学习是无处不在的,尽管它经常隐藏在视线之外。 @@ -694,17 +684,17 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 虽然他们的行为可能会给人一种通用智能的错觉,但设计的基础是规则、启发式和统计模型的结合。 其次,目前还不存在能够自我改进、自我推理、能够在试图解决一般任务的同时,修改、扩展和改进自己的架构的“人工通用智能”工具。 -一个更紧迫的问题是人工智能在我们日常生活中的应用: +一个更紧迫的问题是人工智能在日常生活中的应用。 卡车司机和店员完成的许多琐碎的工作很可能也将是自动化的。 农业机器人可能会降低有机农业的成本,它们也将使收割作业自动化。 工业革命的这一阶段可能对社会的大部分地区产生深远的影响,因为卡车司机和店员是许多国家最常见的工作之一。 此外,如果不加注意地应用统计模型,可能会导致种族、性别或年龄偏见,如果自动驱动相应的决策,则会引起对程序公平性的合理关注。 重要的是要确保小心使用这些算法。 -就我们今天所知,这比恶意超级智能毁灭人类的风险更令我们担忧。 +就我们今天所知,这比恶意超级智能毁灭人类的风险更令人担忧。 ## 特点 -到目前为止,我们已经广泛地讨论了机器学习,它既是人工智能的一个分支,也是人工智能的一种方法。 +到目前为止,本节已经广泛地讨论了机器学习,它既是人工智能的一个分支,也是人工智能的一种方法。 虽然深度学习是机器学习的一个子集,但令人眼花缭乱的算法和应用程序集让人很难评估深度学习的具体成分是什么。 这就像试图确定披萨所需的配料一样困难,因为几乎每种成分都是可以替代的。 @@ -714,7 +704,7 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 例如,靠近输入的层可以表示数据的低级细节,而接近分类输出的层可以表示用于区分的更抽象的概念。 由于*表示学习*(representation learning)目的是寻找表示本身,因此深度学习可以称为“多级表示学习”。 -到目前为止,我们讨论的问题,例如从原始音频信号中学习,图像的原始像素值,或者任意长度的句子与外语中的对应句子之间的映射,都是深度学习优于传统机器学习方法的问题。 +本节到目前为止讨论的问题,例如从原始音频信号中学习,图像的原始像素值,或者任意长度的句子与外语中的对应句子之间的映射,都是深度学习优于传统机器学习方法的问题。 事实证明,这些多层模型能够以以前的工具所不能的方式处理低级的感知数据。 毋庸置疑,深度学习方法中最显著的共同点是使用端到端训练。 也就是说,与其基于单独调整的组件组装系统,不如构建系统,然后联合调整它们的性能。 @@ -727,7 +717,7 @@ Canny边缘检测器 :cite:`Canny.1987` 和SIFT特征提取器 :cite:`Lowe.2004` 因此,深度学习的一个关键优势是它不仅取代了传统学习管道末端的浅层模型,而且还取代了劳动密集型的特征工程过程。 此外,通过取代大部分特定领域的预处理,深度学习消除了以前分隔计算机视觉、语音识别、自然语言处理、医学信息学和其他应用领域的许多界限,为解决各种问题提供了一套统一的工具。 -除了端到端的训练,我们正在经历从参数统计描述到完全非参数模型的转变。 +除了端到端的训练,人们正在经历从参数统计描述到完全非参数模型的转变。 当数据稀缺时,人们需要依靠简化对现实的假设来获得有用的模型。 当数据丰富时,可以用更准确地拟合实际情况的非参数模型来代替。 在某种程度上,这反映了物理学在上个世纪中叶随着计算机的出现所经历的进步。 @@ -738,7 +728,7 @@ Canny边缘检测器 :cite:`Canny.1987` 和SIFT特征提取器 :cite:`Lowe.2004` 尽管在许多情况下,这是以修改和重新发明存在了数十年的工具为代价的。 最后,深度学习社区引以为豪的是,他们跨越学术界和企业界共享工具,发布了许多优秀的算法库、统计模型和经过训练的开源神经网络。 -正是本着这种精神,本书免费分发和使用。我们努力降低每个人了解深度学习的门槛,我们希望我们的读者能从中受益。 +正是本着这种精神,本书免费分发和使用。我们努力降低每个人了解深度学习的门槛,希望读者能从中受益。 ## 小结 diff --git a/chapter_linear-networks/image-classification-dataset.md b/chapter_linear-networks/image-classification-dataset.md index 7fe20d2de..5f9d165ab 100644 --- a/chapter_linear-networks/image-classification-dataset.md +++ b/chapter_linear-networks/image-classification-dataset.md @@ -5,7 +5,6 @@ (**是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集**) :cite:`Xiao.Rasul.Vollgraf.2017`。 - ```{.python .input} %matplotlib inline from d2l import mxnet as d2l @@ -36,6 +35,19 @@ import tensorflow as tf d2l.use_svg_display() ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import sys +import paddle +from paddle.vision import transforms + +d2l.use_svg_display() +``` + ## 读取数据集 我们可以[**通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中**]。 @@ -48,7 +60,7 @@ mnist_test = gluon.data.vision.FashionMNIST(train=False) ```{.python .input} #@tab pytorch # 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式, -# 并除以255使得所有像素的数值均在0到1之间 +# 并除以255使得所有像素的数值均在0~1之间 trans = transforms.ToTensor() mnist_train = torchvision.datasets.FashionMNIST( root="../data", train=True, transform=trans, download=True) @@ -61,6 +73,14 @@ mnist_test = torchvision.datasets.FashionMNIST( mnist_train, mnist_test = tf.keras.datasets.fashion_mnist.load_data() ``` +```{.python .input} +#@tab paddle +trans = transforms.ToTensor() +mnist_train = paddle.vision.datasets.FashionMNIST(mode="train", + transform=trans) +mnist_test = paddle.vision.datasets.FashionMNIST(mode="test", transform=trans) +``` + Fashion-MNIST由10个类别的图像组成, 每个类别由*训练数据集*(train dataset)中的6000张图像 和*测试数据集*(test dataset)中的1000张图像组成。 @@ -68,7 +88,7 @@ Fashion-MNIST由10个类别的图像组成, 测试数据集不会用于训练,只用于评估模型性能。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle len(mnist_train), len(mnist_test) ``` @@ -139,6 +159,28 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save return axes ``` +```{.python .input} +#@tab paddle +#@save +def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): + """绘制图像列表""" + figsize = (num_cols * scale, num_rows * scale) + _, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize) + axes = axes.flatten() + for i, (ax, img) in enumerate(zip(axes, imgs)): + if paddle.is_tensor(img): + # 图片张量 + ax.imshow(img.numpy()) + else: + # PIL图片 + ax.imshow(img) + ax.axes.get_xaxis().set_visible(False) + ax.axes.get_yaxis().set_visible(False) + if titles: + ax.set_title(titles[i]) + return axes +``` + 以下是训练数据集中前[**几个样本的图像及其相应的标签**]。 ```{.python .input} @@ -161,6 +203,12 @@ y = tf.constant(mnist_train[1][:18]) show_images(X, 2, 9, titles=get_fashion_mnist_labels(y)); ``` +```{.python .input} +#@tab paddle +X, y = next(iter(paddle.io.DataLoader(mnist_train, batch_size=18))) +show_images(X.reshape([18, 28, 28]), 2, 9, titles=get_fashion_mnist_labels(y)); +``` + ## 读取小批量 为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。 @@ -175,7 +223,7 @@ def get_dataloader_workers(): #@save return 0 if sys.platform.startswith('win') else 4 # 通过ToTensor实例将图像数据从uint8格式变换成32位浮点数格式,并除以255使得所有像素的数值 -# 均在0到1之间 +# 均在0~1之间 transformer = gluon.data.vision.transforms.ToTensor() train_iter = gluon.data.DataLoader(mnist_train.transform_first(transformer), batch_size, shuffle=True, @@ -201,6 +249,21 @@ train_iter = tf.data.Dataset.from_tensor_slices( mnist_train).batch(batch_size).shuffle(len(mnist_train[0])) ``` +```{.python .input} +#@tab paddle +batch_size = 256 + +def get_dataloader_workers(): #@save + """使用4个进程来读取数据""" + return 4 + +train_iter = paddle.io.DataLoader(dataset=mnist_train, + batch_size=batch_size, + shuffle=True, + return_list=True, + num_workers=get_dataloader_workers()) +``` + 我们看一下读取训练数据所需的时间。 ```{.python .input} @@ -269,6 +332,31 @@ def load_data_fashion_mnist(batch_size, resize=None): #@save batch_size).map(resize_fn)) ``` +```{.python .input} +#@tab paddle +#@save +def load_data_fashion_mnist(batch_size, resize=None): + """下载Fashion-MNIST数据集,然后将其加载到内存中""" + trans = [transforms.ToTensor()] + if resize: + trans.insert(0, transforms.Resize(resize)) + trans = transforms.Compose(trans) + mnist_train = paddle.vision.datasets.FashionMNIST(mode="train", + transform=trans) + mnist_test = paddle.vision.datasets.FashionMNIST(mode="test", + transform=trans) + return (paddle.io.DataLoader(dataset=mnist_train, + batch_size=batch_size, + shuffle=True, + return_list=True, + num_workers=get_dataloader_workers()), + paddle.io.DataLoader(dataset=mnist_test, + batch_size=batch_size, + return_list=True, + shuffle=True, + num_workers=get_dataloader_workers())) +``` + 下面,我们通过指定`resize`参数来测试`load_data_fashion_mnist`函数的图像大小调整功能。 ```{.python .input} @@ -290,7 +378,7 @@ for X, y in train_iter: ## 练习 1. 减少`batch_size`(如减少到1)是否会影响读取性能? -1. 数据迭代器的性能非常重要。你认为当前的实现足够快吗?探索各种选择来改进它。 +1. 数据迭代器的性能非常重要。当前的实现足够快吗?探索各种选择来改进它。 1. 查阅框架的在线API文档。还有哪些其他数据集可用? :begin_tab:`mxnet` @@ -304,3 +392,7 @@ for X, y in train_iter: :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1786) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11692) +:end_tab: diff --git a/chapter_linear-networks/linear-regression-concise.md b/chapter_linear-networks/linear-regression-concise.md index 14ec5a16d..7c52228f2 100644 --- a/chapter_linear-networks/linear-regression-concise.md +++ b/chapter_linear-networks/linear-regression-concise.md @@ -10,7 +10,7 @@ 实际上,由于数据迭代器、损失函数、优化器和神经网络层很常用, 现代深度学习库也为我们实现了这些组件。 -在本节中,我们将介绍如何(**通过使用深度学习框架来简洁地实现**) +本节将介绍如何(**通过使用深度学习框架来简洁地实现**) :numref:`sec_linear_scratch`中的(**线性回归模型**)。 ## 生成数据集 @@ -38,6 +38,15 @@ import numpy as np import tensorflow as tf ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import numpy as np +import paddle +``` + ```{.python .input} #@tab all true_w = d2l.tensor([2, -3.4]) @@ -77,6 +86,17 @@ def load_array(data_arrays, batch_size, is_train=True): #@save return dataset ``` +```{.python .input} +#@tab paddle +#@save +def load_array(data_arrays, batch_size, is_train=True): + """构造一个Paddle数据迭代器""" + dataset = paddle.io.TensorDataset(data_arrays) + return paddle.io.DataLoader(dataset, batch_size=batch_size, + shuffle=is_train, + return_list=True) +``` + ```{.python .input} #@tab all batch_size = 10 @@ -95,9 +115,9 @@ next(iter(data_iter)) 当我们在 :numref:`sec_linear_scratch`中实现线性回归时, 我们明确定义了模型参数变量,并编写了计算的代码,这样通过基本的线性代数运算得到输出。 -但是,如果模型变得更加复杂,且当你几乎每天都需要实现模型时,你会想简化这个过程。 +但是,如果模型变得更加复杂,且当我们几乎每天都需要实现模型时,自然会想简化这个过程。 这种情况类似于为自己的博客从零开始编写网页。 -做一两次是有益的,但如果每个新博客你就花一个月的时间重新开始编写网页,那并不高效。 +做一两次是有益的,但如果每个新博客就需要工程师花一个月的时间重新开始编写网页,那并不高效。 对于标准深度学习模型,我们可以[**使用框架的预定义好的层**]。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。 我们首先定义一个模型变量`net`,它是一个`Sequential`类的实例。 @@ -119,7 +139,7 @@ next(iter(data_iter)) 所以在这里,我们不需要告诉Gluon有多少输入进入这一层。 当我们第一次尝试通过我们的模型传递数据时,例如,当后面执行`net(X)`时, Gluon会自动推断每个层输入的形状。 -我们稍后将详细介绍这种工作机制。 +本节稍后将详细介绍这种工作机制。 :end_tab: :begin_tab:`pytorch` @@ -132,11 +152,18 @@ Gluon会自动推断每个层输入的形状。 在Keras中,全连接层在`Dense`类中定义。 由于我们只想得到一个标量输出,所以我们将该数字设置为1。 + 值得注意的是,为了方便使用,Keras不要求我们为每个层指定输入形状。 所以在这里,我们不需要告诉Keras有多少输入进入这一层。 当我们第一次尝试通过我们的模型传递数据时,例如,当后面执行`net(X)`时, Keras会自动推断每个层输入的形状。 -我们稍后将详细介绍这种工作机制。 +本节稍后将详细介绍这种工作机制。 +:end_tab: + +:begin_tab:`paddle` +在PaddlePaddle中,全连接层在`Linear`类中定义。 +值得注意的是,我们将两个参数传递到`nn.Linear`中。 +第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。 :end_tab: ```{.python .input} @@ -160,6 +187,13 @@ net = tf.keras.Sequential() net.add(tf.keras.layers.Dense(1)) ``` +```{.python .input} +#@tab paddle +# nn是神经网络的缩写 +from paddle import nn +net = nn.Sequential(nn.Linear(2, 1)) +``` + ## (**初始化模型参数**) 在使用`net`之前,我们需要初始化模型参数。 @@ -189,6 +223,15 @@ TensorFlow中的`initializers`模块提供了多种模型参数初始化方法 在这里,我们重新创建了`net`。 :end_tab: +:begin_tab:`paddle` +正如我们在构造`nn.Linear`时指定输入和输出尺寸一样, +现在我们能直接访问参数以设定它们的初始值。 +我们通过`net[0]`选择网络中的第一个图层, +然后使用`weight`和`bias`方法访问参数。 +我们可以通过调用`nn.initializer.Normal(0, 0.01)`来指定初始化权重的方法。 +默认情况下,偏置参数初始化为零。 +:end_tab: + ```{.python .input} from mxnet import init net.initialize(init.Normal(sigma=0.01)) @@ -207,8 +250,17 @@ net = tf.keras.Sequential() net.add(tf.keras.layers.Dense(1, kernel_initializer=initializer)) ``` +```{.python .input} +#@tab paddle +weight_attr = paddle.ParamAttr(initializer= + paddle.nn.initializer.Normal(0, 0.01)) +bias_attr = paddle.ParamAttr(initializer=None) +net = nn.Sequential(nn.Linear(2, 1, weight_attr=weight_attr, + bias_attr=bias_attr)) +``` + :begin_tab:`mxnet` -上面的代码可能看起来很简单,但是你应该注意到这里的一个细节: +上面的代码可能看起来很简单,但是这里有一个应该注意到的细节: 我们正在为网络初始化参数,而Gluon还不知道输入将有多少维! 网络的输入可能有2维,也可能有2000维。 Gluon让我们避免了这个问题,在后端执行时,初始化实际上是*推迟*(deferred)执行的, @@ -221,7 +273,7 @@ Gluon让我们避免了这个问题,在后端执行时,初始化实际上是 :end_tab: :begin_tab:`tensorflow` -上面的代码可能看起来很简单,但是你应该注意到这里的一个细节: +上面的代码可能看起来很简单,但是这里有一个应该注意到的细节: 我们正在为网络初始化参数,而Keras还不知道输入将有多少维! 网络的输入可能有2维,也可能有2000维。 Keras让我们避免了这个问题,在后端执行时,初始化实际上是*推迟*(deferred)执行的。 @@ -246,6 +298,11 @@ Keras让我们避免了这个问题,在后端执行时,初始化实际上是 默认情况下,它返回所有样本损失的平均值。 :end_tab: +:begin_tab:`paddle` +[**计算均方误差使用的是`MSELoss`类,也称为平方$L_2$范数**]。 +默认情况下,它返回所有样本损失的平均值。 +:end_tab: + ```{.python .input} loss = gluon.loss.L2Loss() ``` @@ -260,6 +317,11 @@ loss = nn.MSELoss() loss = tf.keras.losses.MeanSquaredError() ``` +```{.python .input} +#@tab paddle +loss = nn.MSELoss() +``` + ## 定义优化算法 :begin_tab:`mxnet` @@ -285,6 +347,12 @@ Keras在`optimizers`模块中实现了该算法的许多变种。 小批量随机梯度下降只需要设置`learning_rate`值,这里设置为0.03。 :end_tab: +:begin_tab:`paddle` +小批量随机梯度下降算法是一种优化神经网络的标准工具, +PaddlePaddle在`optimizer`模块中实现了该算法的许多变种。 +小批量随机梯度下降只需要设置`learning_rate`值,这里设置为0.03。 +:end_tab: + ```{.python .input} from mxnet import gluon trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03}) @@ -300,6 +368,12 @@ trainer = torch.optim.SGD(net.parameters(), lr=0.03) trainer = tf.keras.optimizers.SGD(learning_rate=0.03) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.SGD(learning_rate=0.03, + parameters=net.parameters()) +``` + ## 训练 通过深度学习框架的高级API来实现我们的模型只需要相对较少的代码。 @@ -355,6 +429,19 @@ for epoch in range(num_epochs): print(f'epoch {epoch + 1}, loss {l:f}') ``` +```{.python .input} +#@tab paddle +num_epochs = 3 +for epoch in range(num_epochs): + for i,(X, y) in enumerate (data_iter()): + l = loss(net(X) ,y) + trainer.clear_grad() + l.backward() + trainer.step() + l = loss(net(features), labels) + print(f'epoch {epoch + 1},'f'loss {l}') +``` + 下面我们[**比较生成数据集的真实参数和通过有限数据训练获得的模型参数**]。 要访问参数,我们首先从`net`访问所需的层,然后读取该层的权重和偏置。 正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。 @@ -382,6 +469,14 @@ b = net.get_weights()[1] print('b的估计误差:', true_b - b) ``` +```{.python .input} +#@tab paddle +w = net[0].weight +print('w的估计误差:', true_w - w.reshape(true_w.shape)) +b = net[0].bias +print('b的估计误差:', true_b - b) +``` + ## 小结 :begin_tab:`mxnet` @@ -406,10 +501,10 @@ print('b的估计误差:', true_b - b) ## 练习 -1. 如果将小批量的总损失替换为小批量损失的平均值,你需要如何更改学习率? +1. 如果将小批量的总损失替换为小批量损失的平均值,需要如何更改学习率? 1. 查看深度学习框架文档,它们提供了哪些损失函数和初始化方法?用Huber损失代替原损失,即 $$l(y,y') = \begin{cases}|y-y'| -\frac{\sigma}{2} & \text{ if } |y-y'| > \sigma \\ \frac{1}{2 \sigma} (y-y')^2 & \text{ 其它情况}\end{cases}$$ -1. 你如何访问线性回归的梯度? +1. 如何访问线性回归的梯度? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1782) @@ -422,3 +517,7 @@ print('b的估计误差:', true_b - b) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1780) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11690) +:end_tab: diff --git a/chapter_linear-networks/linear-regression-scratch.md b/chapter_linear-networks/linear-regression-scratch.md index f8698c28f..ce7309e87 100644 --- a/chapter_linear-networks/linear-regression-scratch.md +++ b/chapter_linear-networks/linear-regression-scratch.md @@ -4,7 +4,7 @@ 在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。 在这一节中,(**我们将从零开始实现整个方法, 包括数据流水线、模型、损失函数和小批量随机梯度下降优化器**)。 -虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保你真正知道自己在做什么。 +虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道自己在做什么。 同时,了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。 在这一节中,我们将只使用张量和自动求导。 在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。 @@ -33,6 +33,16 @@ import tensorflow as tf import random ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import random +import paddle +``` + ## 生成数据集 为了简单起见,我们将[**根据带有噪声的线性模型构造一个人造数据集。**] @@ -48,13 +58,13 @@ import random $$\mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon.$$ **) -你可以将$\epsilon$视为模型预测和标签时的潜在观测误差。 +$\epsilon$可以视为模型预测和标签时的潜在观测误差。 在这里我们认为标准假设成立,即$\epsilon$服从均值为0的正态分布。 为了简化问题,我们将标准差设为0.01。 下面的代码生成合成数据集。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle def synthetic_data(w, b, num_examples): #@save """生成y=Xw+b+噪声""" X = d2l.normal(0, 1, (num_examples, len(w))) @@ -110,7 +120,7 @@ d2l.plt.scatter(d2l.numpy(features[:, 1]), d2l.numpy(labels), 1); 每个小批量包含一组特征和标签。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle def data_iter(batch_size, features, labels): num_examples = len(features) indices = list(range(num_examples)) @@ -152,7 +162,7 @@ for X, y in data_iter(batch_size, features, labels): ``` 当我们运行迭代时,我们会连续地获得不同的小批量,直至遍历完整个数据集。 -上面实现的迭代对于教学来说很好,但它的执行效率很低,可能会在实际问题上陷入麻烦。 +上面实现的迭代对教学来说很好,但它的执行效率很低,可能会在实际问题上陷入麻烦。 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多, 它可以处理存储在文件中的数据和数据流提供的数据。 @@ -184,6 +194,15 @@ w = tf.Variable(tf.random.normal(shape=(2, 1), mean=0, stddev=0.01), b = tf.Variable(tf.zeros(1), trainable=True) ``` +```{.python .input} +#@tab paddle +w = d2l.normal(0, 0.01, shape=(2,1)) +b = d2l.zeros(shape=[1]) +# w和b为创建的模型参数,stop_gradient默认为True,即梯度不更新,因此需要指定为False已更新梯度 +w.stop_gradient = False +b.stop_gradient = False +``` + 在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。 @@ -258,11 +277,23 @@ def sgd(params, grads, lr, batch_size): #@save param.assign_sub(lr*grad/batch_size) ``` +```{.python .input} +#@tab paddle +#@save +def sgd(params, lr, batch_size): + """小批量随机梯度下降""" + with paddle.no_grad(): + for i, param in enumerate(params): + param -= lr * params[i].grad / batch_size + params[i].set_value(param) + params[i].clear_gradient() +``` + ## 训练 现在我们已经准备好了模型训练所有需要的要素,可以实现主要的[**训练过程**]部分了。 理解这段代码至关重要,因为从事深度学习后, -你会一遍又一遍地看到几乎相同的训练过程。 +相同的训练过程几乎一遍又一遍地出现。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。 最后,我们调用优化算法`sgd`来更新模型参数。 @@ -328,6 +359,20 @@ for epoch in range(num_epochs): print(f'epoch {epoch + 1}, loss {float(tf.reduce_mean(train_l)):f}') ``` +```{.python .input} +#@tab paddle +for epoch in range(num_epochs): + for X, y in data_iter(batch_size, features, labels): + l = loss(net(X, w, b), y) # X和y的小批量损失 + # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起, + # 并以此计算关于[w,b]的梯度 + l.sum().backward() + sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数 + with paddle.no_grad(): + train_l = loss(net(features, w, b), labels) + print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}') +``` + 因为我们使用的是自己合成的数据集,所以我们知道真正的参数是什么。 因此,我们可以通过[**比较真实参数和通过训练学到的参数来评估训练的成功程度**]。 事实上,真实参数和通过训练学到的参数确实非常接近。 @@ -351,9 +396,9 @@ print(f'b的估计误差: {true_b - b}') ## 练习 1. 如果我们将权重初始化为零,会发生什么。算法仍然有效吗? -1. 假设你是[乔治·西蒙·欧姆](https://en.wikipedia.org/wiki/Georg_Ohm),试图为电压和电流的关系建立一个模型。你能使用自动微分来学习模型的参数吗? -1. 您能基于[普朗克定律](https://en.wikipedia.org/wiki/Planck%27s_law)使用光谱能量密度来确定物体的温度吗? -1. 如果你想计算二阶导数可能会遇到什么问题?你会如何解决这些问题? +1. 假设试图为电压和电流的关系建立一个模型。自动微分可以用来学习模型的参数吗? +1. 能基于[普朗克定律](https://en.wikipedia.org/wiki/Planck%27s_law)使用光谱能量密度来确定物体的温度吗? +1. 计算二阶导数时可能会遇到什么问题?这些问题可以如何解决? 1. 为什么在`squared_loss`函数中需要使用`reshape`函数? 1. 尝试使用不同的学习率,观察损失函数值下降的快慢。 1. 如果样本个数不能被批量大小整除,`data_iter`函数的行为会有什么变化? @@ -369,3 +414,7 @@ print(f'b的估计误差: {true_b - b}') :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1777) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11689) +:end_tab: diff --git a/chapter_linear-networks/linear-regression.md b/chapter_linear-networks/linear-regression.md index dec8319ca..da4dcfc30 100644 --- a/chapter_linear-networks/linear-regression.md +++ b/chapter_linear-networks/linear-regression.md @@ -197,7 +197,7 @@ $\eta$表示*学习率*(learning rate)。 因为算法会使得损失向最小值缓慢收敛,但却不能在有限的步数内非常精确地达到最小值。 线性回归恰好是一个在整个域中只有一个最小值的学习问题。 -但是对于像深度神经网络这样复杂的模型来说,损失平面上通常包含多个最小值。 +但是对像深度神经网络这样复杂的模型来说,损失平面上通常包含多个最小值。 深度学习实践者很少会去花费大力气寻找这样一组参数,使得在*训练集*上的损失达到最小。 事实上,更难做到的是找到一组参数,这组参数能够在我们从未见过的数据上实现较低的损失, 这一挑战被称为*泛化*(generalization)。 @@ -247,6 +247,18 @@ import numpy as np import time ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import numpy as np +import time +import paddle +``` + 为了说明矢量化为什么如此重要,我们考虑(**对向量相加的两种方法**)。 我们实例化两个全为1的10000维向量。 在一种方法中,我们将使用Python的for循环遍历向量; @@ -255,8 +267,8 @@ import time ```{.python .input} #@tab all n = 10000 -a = d2l.ones(n) -b = d2l.ones(n) +a = d2l.ones([n]) +b = d2l.ones([n]) ``` 由于在本书中我们将频繁地进行运行时间的基准测试,所以[**我们定义一个计时器**]: @@ -313,6 +325,15 @@ for i in range(n): f'{timer.stop():.5f} sec' ``` +```{.python .input} +#@tab paddle +c = d2l.zeros([n]) +timer = Timer() +for i in range(n): + c[i] = a[i] + b[i] +f'{timer.stop():.5f} sec' +``` + (**或者,我们使用重载的`+`运算符来计算按元素的和**)。 ```{.python .input} @@ -361,7 +382,7 @@ d2l.plot(x.asnumpy(), [normal(x, mu, sigma).asnumpy() for mu, sigma in params], ``` ```{.python .input} -#@tab pytorch, tensorflow +#@tab pytorch, tensorflow, paddle # 再次使用numpy进行可视化 x = np.arange(-7, 7, 0.01) @@ -441,7 +462,7 @@ $$-\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma 他们为什么将线性模型作为一个起点呢? 我们来看一张图片 :numref:`fig_Neuron`: 这是一张由*树突*(dendrites,输入终端)、 -*细胞核*(nucleu,CPU)组成的生物神经元图片。 +*细胞核*(nucleus,CPU)组成的生物神经元图片。 *轴突*(axon,输出线)和*轴突端子*(axon terminal,输出端子) 通过*突触*(synapse)与其他神经元连接。 @@ -483,8 +504,8 @@ $$-\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma 1. 什么时候可能比使用随机梯度下降更好?这种方法何时会失效? 1. 假定控制附加噪声$\epsilon$的噪声模型是指数分布。也就是说,$p(\epsilon) = \frac{1}{2} \exp(-|\epsilon|)$ 1. 写出模型$-\log P(\mathbf y \mid \mathbf X)$下数据的负对数似然。 - 1. 你能写出解析解吗? - 1. 提出一种随机梯度下降算法来解决这个问题。哪里可能出错?(提示:当我们不断更新参数时,在驻点附近会发生什么情况)你能解决这个问题吗? + 1. 请试着写出解析解。 + 1. 提出一种随机梯度下降算法来解决这个问题。哪里可能出错?(提示:当我们不断更新参数时,在驻点附近会发生什么情况)请尝试解决这个问题。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1774) @@ -497,3 +518,7 @@ $$-\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1776) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11688) +:end_tab: diff --git a/chapter_linear-networks/softmax-regression-concise.md b/chapter_linear-networks/softmax-regression-concise.md index 426e5edec..33f1d4452 100644 --- a/chapter_linear-networks/softmax-regression-concise.md +++ b/chapter_linear-networks/softmax-regression-concise.md @@ -29,6 +29,15 @@ 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 +``` + ```{.python .input} #@tab all batch_size = 256 @@ -72,6 +81,17 @@ weight_initializer = tf.keras.initializers.RandomNormal(mean=0.0, stddev=0.01) net.add(tf.keras.layers.Dense(10, kernel_initializer=weight_initializer)) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) + +def init_weights(m): + if type(m) == nn.Linear: + nn.initializer.Normal(m.weight, std=0.01) + +net.apply(init_weights); +``` + ## 重新审视Softmax的实现 :label:`subsec_softmax-implementation-revisited` @@ -91,7 +111,7 @@ $o_j$是未规范化的预测$\mathbf{o}$的第$j$个元素。 解决这个问题的一个技巧是: 在继续softmax计算之前,先从所有$o_k$中减去$\max(o_k)$。 -你可以看到每个$o_k$按常数进行的移动不会改变softmax的返回值: +这里可以看到每个$o_k$按常数进行的移动不会改变softmax的返回值: $$ \begin{aligned} @@ -130,7 +150,7 @@ loss = gluon.loss.SoftmaxCrossEntropyLoss() ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle loss = nn.CrossEntropyLoss(reduction='none') ``` @@ -158,6 +178,11 @@ trainer = torch.optim.SGD(net.parameters(), lr=0.1) trainer = tf.keras.optimizers.SGD(learning_rate=.1) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.SGD(learning_rate=0.1, parameters=net.parameters()) +``` + ## 训练 接下来我们[**调用**] :numref:`sec_softmax_scratch`中(~~之前~~) @@ -192,3 +217,7 @@ d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1792) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11761) +:end_tab: diff --git a/chapter_linear-networks/softmax-regression-scratch.md b/chapter_linear-networks/softmax-regression-scratch.md index da09a50d6..daa33345f 100644 --- a/chapter_linear-networks/softmax-regression-scratch.md +++ b/chapter_linear-networks/softmax-regression-scratch.md @@ -2,7 +2,7 @@ :label:`sec_softmax_scratch` (**就像我们从零开始实现线性回归一样,**) -我们认为softmax回归也是重要的基础,因此(**你应该知道实现softmax回归的细节**)。 +我们认为softmax回归也是重要的基础,因此(**应该知道实现softmax回归的细节**)。 本节我们将使用刚刚在 :numref:`sec_fashion_mnist`中引入的Fashion-MNIST数据集, 并设置数据迭代器的批量大小为256。 @@ -27,6 +27,15 @@ import tensorflow as tf from IPython import display ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from IPython import display +``` + ```{.python .input} #@tab all batch_size = 256 @@ -37,7 +46,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) 和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是$28 \times 28$的图像。 -在本节中,我们[**将展平每个图像,把它们看作长度为784的向量。**] +本节[**将展平每个图像,把它们看作长度为784的向量。**] 在后面的章节中,我们将讨论能够利用图像空间结构的特征, 但现在我们暂时只把每个像素位置看作一个特征。 @@ -76,6 +85,17 @@ W = tf.Variable(tf.random.normal(shape=(num_inputs, num_outputs), b = tf.Variable(tf.zeros(num_outputs)) ``` +```{.python .input} +#@tab paddle +num_inputs = 784 +num_outputs = 10 + +W = paddle.normal(0, 0.01, shape=(num_inputs, num_outputs)) +b = paddle.zeros(shape=(num_outputs,)) +W.stop_gradient=False +b.stop_gradient=False +``` + ## 定义softmax操作 在实现softmax回归模型之前,我们简要回顾一下`sum`运算符如何沿着张量中的特定维度工作。 @@ -89,7 +109,7 @@ b = tf.Variable(tf.zeros(num_outputs)) 这将产生一个具有形状`(1, 3)`的二维张量。 ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle X = d2l.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) d2l.reduce_sum(X, 0, keepdim=True), d2l.reduce_sum(X, 1, keepdim=True) ``` @@ -126,18 +146,18 @@ def softmax(X): ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle def softmax(X): X_exp = d2l.exp(X) partition = d2l.reduce_sum(X_exp, 1, keepdim=True) return X_exp / partition # 这里应用了广播机制 ``` -正如你所看到的,对于任何随机输入,[**我们将每个元素变成一个非负数。 +正如上述代码,对于任何随机输入,[**我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1**]。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle X = d2l.normal(0, 1, (2, 5)) X_prob = softmax(X) X_prob, d2l.reduce_sum(X_prob, 1) @@ -181,7 +201,7 @@ def net(X): 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle y = d2l.tensor([0, 2]) y_hat = d2l.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]]) y_hat[[0, 1], y] @@ -213,12 +233,20 @@ def cross_entropy(y_hat, y): cross_entropy(y_hat, y) ``` +```{.python .input} +#@tab paddle +def cross_entropy(y_hat, y): + return - paddle.log(y_hat[[i for i in range(len(y_hat))], y.squeeze()]) + +cross_entropy(y_hat, y) +``` + ## 分类精度 给定预测概率分布`y_hat`,当我们必须输出硬预测(hard prediction)时, 我们通常选择预测概率最高的类。 许多应用都要求我们做出选择。如Gmail必须将电子邮件分类为“Primary(主要邮件)”、 -“Social(社交邮件)”、“Updates(更新邮件)”或“Forums(论坛邮件)”。 +“Social(社交邮件)”“Updates(更新邮件)”或“Forums(论坛邮件)”。 Gmail做分类时可能在内部估计概率,但最终它必须在类中选择一个。 当预测与标签分类`y`一致时,即是正确的。 @@ -245,6 +273,20 @@ def accuracy(y_hat, y): #@save return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) ``` +```{.python .input} +#@tab paddle +#@save +def accuracy(y_hat, y): + """计算预测正确的数量""" + if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: + y_hat = y_hat.argmax(axis=1) + if len(y_hat.shape) < len(y.shape): + cmp = y_hat.astype(y.dtype) == y.squeeze() + else: + cmp = y_hat.astype(y.dtype) == y + return float(cmp.astype(y.dtype).sum()) +``` + 我们将继续使用之前定义的变量`y_hat`和`y`分别作为预测的概率分布和标签。 可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。 第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。 @@ -281,6 +323,20 @@ def evaluate_accuracy(net, data_iter): #@save return metric[0] / metric[1] ``` +```{.python .input} +#@tab paddle +#@save +def evaluate_accuracy(net, data_iter): + """计算在指定数据集上模型的精度""" + if isinstance(net, paddle.nn.Layer): + net.eval() # 将模型设置为评估模式 + metric = Accumulator(2) # 正确预测数、预测总数 + with paddle.no_grad(): + for X, y in data_iter: + metric.add(accuracy(net(X), y), d2l.size(y)) + return metric[0] / metric[1] +``` + 这里定义一个实用程序类`Accumulator`,用于对多个变量进行累加。 在上面的`evaluate_accuracy`函数中, 我们在(**`Accumulator`实例中创建了2个变量, @@ -315,7 +371,7 @@ evaluate_accuracy(net, test_iter) ## 训练 -如果你看过 :numref:`sec_linear_scratch`中的线性回归实现, +在我们看过 :numref:`sec_linear_scratch`中的线性回归实现, [**softmax回归的训练**]过程代码应该看起来非常眼熟。 在这里,我们重构训练过程的实现以使其可重复使用。 首先,我们定义一个函数来训练一个迭代周期。 @@ -398,6 +454,34 @@ def train_epoch_ch3(net, train_iter, loss, updater): #@save return metric[0] / metric[2], metric[1] / metric[2] ``` +```{.python .input} +#@tab paddle +#@save +def train_epoch_ch3(net, train_iter, loss, updater): + """训练模型一个迭代周期(定义见第3章)""" + # 将模型设置为训练模式 + if isinstance(net, paddle.nn.Layer): + net.train() + # 训练损失总和、训练准确度总和、样本数 + metric = Accumulator(3) + + for X, y in train_iter: + # 计算梯度并更新参数 + y_hat = net(X) + l = loss(y_hat, y) + if isinstance(updater, paddle.optimizer.Optimizer): + # 使用PaddlePaddle内置的优化器和损失函数 + updater.clear_grad() + l.mean().backward() + updater.step() + else: + # 使用定制的优化器和损失函数 + l.sum().backward() + updater(X.shape[0]) + metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) + return metric[0] / metric[2], metric[1] / metric[2] +``` + 在展示训练函数的实现之前,我们[**定义一个在动画中绘制数据的实用程序类**]`Animator`, 它能够简化本书其余部分的代码。 @@ -470,7 +554,7 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save [**小批量随机梯度下降来优化模型的损失函数**],设置学习率为0.1。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle lr = 0.1 def updater(batch_size): @@ -528,10 +612,10 @@ predict_ch3(net, test_iter) ## 练习 -1. 在本节中,我们直接实现了基于数学定义softmax运算的`softmax`函数。这可能会导致什么问题?提示:尝试计算$\exp(50)$的大小。 +1. 本节直接实现了基于数学定义softmax运算的`softmax`函数。这可能会导致什么问题?提示:尝试计算$\exp(50)$的大小。 1. 本节中的函数`cross_entropy`是根据交叉熵损失函数的定义实现的。它可能有什么问题?提示:考虑对数的定义域。 -1. 你可以想到什么解决方案来解决上述两个问题? -1. 返回概率最大的分类标签总是最优解吗?例如,医疗诊断场景下你会这样做吗? +1. 请想一个解决方案来解决上述两个问题。 +1. 返回概率最大的分类标签总是最优解吗?例如,医疗诊断场景下可以这样做吗? 1. 假设我们使用softmax回归来预测下一个单词,可选取的单词数目过多可能会带来哪些问题? :begin_tab:`mxnet` @@ -545,3 +629,7 @@ predict_ch3(net, test_iter) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1790) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11760) +:end_tab: diff --git a/chapter_linear-networks/softmax-regression.md b/chapter_linear-networks/softmax-regression.md index a8dc141b2..ab7b61ecb 100644 --- a/chapter_linear-networks/softmax-regression.md +++ b/chapter_linear-networks/softmax-regression.md @@ -26,7 +26,7 @@ 我们从一个图像分类问题开始。 假设每次输入是一个$2\times2$的灰度图像。 我们可以用一个标量表示每个像素值,每个图像对应四个特征$x_1, x_2, x_3, x_4$。 -此外,假设每个图像属于类别“猫”,“鸡”和“狗”中的一个。 +此外,假设每个图像属于类别“猫”“鸡”和“狗”中的一个。 接下来,我们要选择如何表示标签。 我们有两个明显的选择:最直接的想法是选择$y \in \{1, 2, 3\}$, @@ -244,7 +244,7 @@ $$ 它是所有标签分布的预期损失值。 此损失称为*交叉熵损失*(cross-entropy loss),它是分类问题最常用的损失之一。 本节我们将通过介绍信息论基础来理解交叉熵损失。 -如果你想了解更多信息论的细节,你可以进一步参考 +如果想了解更多信息论的细节,请进一步参考 [本书附录中关于信息论的一节](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/information-theory.html)。 ## 信息论基础 @@ -285,7 +285,7 @@ $$H[P] = \sum_j - P(j) \log P(j).$$ 如果把熵$H(P)$想象为“知道真实概率的人所经历的惊异程度”,那么什么是交叉熵? 交叉熵*从*$P$*到*$Q$,记为$H(P, Q)$。 -你可以把交叉熵想象为“主观概率为$Q$的观察者在看到根据概率$P$生成的数据时的预期惊异”。 +我们可以把交叉熵想象为“主观概率为$Q$的观察者在看到根据概率$P$生成的数据时的预期惊异”。 当$P=Q$时,交叉熵达到最低。 在这种情况下,从$P$到$Q$的交叉熵是$H(P, P)= H(P)$。 @@ -313,7 +313,7 @@ $$H[P] = \sum_j - P(j) \log P(j).$$ 1. 计算$\mathrm{softmax}(\mathbf{o})$给出的分布方差,并与上面计算的二阶导数匹配。 1. 假设我们有三个类发生的概率相等,即概率向量是$(\frac{1}{3}, \frac{1}{3}, \frac{1}{3})$。 1. 如果我们尝试为它设计二进制代码,有什么问题? - 1. 你能设计一个更好的代码吗?提示:如果我们尝试编码两个独立的观察结果会发生什么?如果我们联合编码$n$个观测值怎么办? + 1. 请设计一个更好的代码。提示:如果我们尝试编码两个独立的观察结果会发生什么?如果我们联合编码$n$个观测值怎么办? 1. softmax是对上面介绍的映射的误称(虽然深度学习领域中很多人都使用这个名字)。真正的softmax被定义为$\mathrm{RealSoftMax}(a, b) = \log (\exp(a) + \exp(b))$。 1. 证明$\mathrm{RealSoftMax}(a, b) > \mathrm{max}(a, b)$。 1. 证明$\lambda^{-1} \mathrm{RealSoftMax}(\lambda a, \lambda b) > \mathrm{max}(a, b)$成立,前提是$\lambda > 0$。 diff --git a/chapter_multilayer-perceptrons/backprop.md b/chapter_multilayer-perceptrons/backprop.md index 446a0ee08..2ed10098c 100644 --- a/chapter_multilayer-perceptrons/backprop.md +++ b/chapter_multilayer-perceptrons/backprop.md @@ -8,7 +8,7 @@ 梯度的自动计算(自动微分)大大简化了深度学习算法的实现。 在自动微分之前,即使是对复杂模型的微小调整也需要手工重新计算复杂的导数, 学术论文也不得不分配大量页面来推导更新规则。 -在本节中,我们将通过一些基本的数学和计算图, +本节将通过一些基本的数学和计算图, 深入探讨*反向传播*的细节。 首先,我们将重点放在带权重衰减($L_2$正则化)的单隐藏层多层感知机上。 @@ -175,9 +175,9 @@ $\mathbf{W}^{(2)}$的当前值。 1. 画出相应的计算图。 1. 推导正向和反向传播方程。 1. 计算本节所描述的模型,用于训练和预测的内存占用。 -1. 假设你想计算二阶导数。计算图发生了什么?你预计计算需要多长时间? -1. 假设计算图对于你的GPU来说太大了。 - 1. 你能把它划分到多个GPU上吗? +1. 假设想计算二阶导数。计算图发生了什么?预计计算需要多长时间? +1. 假设计算图对当前拥有的GPU来说太大了。 + 1. 请试着把它划分到多个GPU上。 1. 与小批量训练相比,有哪些优点和缺点? [Discussions](https://discuss.d2l.ai/t/5769) diff --git a/chapter_multilayer-perceptrons/dropout.md b/chapter_multilayer-perceptrons/dropout.md index aff9294ad..e3032776a 100644 --- a/chapter_multilayer-perceptrons/dropout.md +++ b/chapter_multilayer-perceptrons/dropout.md @@ -96,7 +96,7 @@ $$ 回想一下 :numref:`fig_mlp`中带有1个隐藏层和5个隐藏单元的多层感知机。 当我们将暂退法应用到隐藏层,以$p$的概率将隐藏单元置为零时, -结果可以看作是一个只包含原始神经元子集的网络。 +结果可以看作一个只包含原始神经元子集的网络。 比如在 :numref:`fig_dropout2`中,删除了$h_2$和$h_5$, 因此输出的计算不再依赖于$h_2$或$h_5$,并且它们各自的梯度在执行反向传播时也会消失。 这样,输出层的计算不能过度依赖于$h_1, \ldots, h_5$的任何一个元素。 @@ -174,6 +174,30 @@ def dropout_layer(X, dropout): return tf.cast(mask, dtype=tf.float32) * X / (1.0 - dropout) ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +from paddle import nn +import random + +warnings.filterwarnings("ignore", category=DeprecationWarning) +from d2l import paddle as d2l + +def dropout_layer(X, dropout): + assert 0 <= dropout <= 1 + # 在本情况中,所有元素都被丢弃。 + if dropout == 1: + return paddle.zeros_like(X) + # 在本情况中,所有元素都被保留。 + if dropout == 0: + return X + + mask = (paddle.to_tensor(paddle.uniform(X.shape)) > dropout).astype('float32') + return mask * X / (1.0 - dropout) +``` + 我们可以通过下面几个例子来[**测试`dropout_layer`函数**]。 我们将输入`X`通过暂退法操作,暂退概率分别为0、0.5和1。 @@ -202,6 +226,15 @@ print(dropout_layer(X, 0.5)) print(dropout_layer(X, 1.)) ``` +```{.python .input} +#@tab paddle +X= paddle.arange(16, dtype = paddle.float32).reshape((2, 8)) +print(X) +print(dropout_layer(X, 0.)) +print(dropout_layer(X, 0.5)) +print(dropout_layer(X, 1.)) +``` + ### 定义模型参数 同样,我们使用 :numref:`sec_fashion_mnist`中引入的Fashion-MNIST数据集。 @@ -223,7 +256,7 @@ for param in params: ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256 ``` @@ -318,6 +351,37 @@ class Net(tf.keras.Model): net = Net(num_outputs, num_hiddens1, num_hiddens2) ``` +```{.python .input} +#@tab paddle +dropout1, dropout2 = 0.2, 0.5 + +class Net(nn.Layer): + def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, + is_training = True): + super(Net, self).__init__() + self.num_inputs = num_inputs + self.training = is_training + self.lin1 = nn.Linear(num_inputs, num_hiddens1) + self.lin2 = nn.Linear(num_hiddens1, num_hiddens2) + self.lin3 = nn.Linear(num_hiddens2, num_outputs) + self.relu = nn.ReLU() + + def forward(self, X): + H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs)))) + # 只有在训练模型时才使用dropout + if self.training == True: + # 在第一个全连接层之后添加一个dropout层 + H1 = dropout_layer(H1, dropout1) + H2 = self.relu(self.lin2(H1)) + if self.training == True: + # 在第二个全连接层之后添加一个dropout层 + H2 = dropout_layer(H2, dropout2) + out = self.lin3(H2) + return out + +net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2) +``` + ### [**训练和测试**] 这类似于前面描述的多层感知机训练和测试。 @@ -348,6 +412,15 @@ trainer = tf.keras.optimizers.SGD(learning_rate=lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) ``` +```{.python .input} +#@tab paddle +num_epochs, lr, batch_size = 10, 0.5, 256 +loss = nn.CrossEntropyLoss(reduction='none') +train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) +trainer = paddle.optimizer.SGD(learning_rate=lr, parameters=net.parameters()) +d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) +``` + ## [**简洁实现**] 对于深度学习框架的高级API,我们只需在每个全连接层之后添加一个`Dropout`层, @@ -401,6 +474,22 @@ net = tf.keras.models.Sequential([ ]) ``` +```{.python .input} +#@tab paddle +weight_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.Normal(std=0.01)) + +net = nn.Sequential(nn.Flatten(), + nn.Linear(784, 256, weight_attr=weight_attr), + nn.ReLU(), + # 在第一个全连接层之后添加一个dropout层 + nn.Dropout(dropout1), + nn.Linear(256, 256, weight_attr=weight_attr), + nn.ReLU(), + # 在第二个全连接层之后添加一个dropout层 + nn.Dropout(dropout2), + nn.Linear(256, 10, weight_attr=weight_attr)) +``` + 接下来,我们[**对模型进行训练和测试**]。 ```{.python .input} @@ -420,6 +509,12 @@ trainer = tf.keras.optimizers.SGD(learning_rate=lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.SGD(learning_rate=0.5, parameters=net.parameters()) +d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) +``` + ## 小结 * 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。 @@ -429,7 +524,7 @@ d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) ## 练习 -1. 如果更改第一层和第二层的暂退法概率,会发生什么情况?具体地说,如果交换这两个层,会发生什么情况?设计一个实验来回答这些问题,定量描述你的结果,并总结定性的结论。 +1. 如果更改第一层和第二层的暂退法概率,会发生什么情况?具体地说,如果交换这两个层,会发生什么情况?设计一个实验来回答这些问题,定量描述该结果,并总结定性的结论。 1. 增加训练轮数,并将使用暂退法和不使用暂退法时获得的结果进行比较。 1. 当应用或不应用暂退法时,每个隐藏层中激活值的方差是多少?绘制一个曲线图,以显示这两个模型的每个隐藏层中激活值的方差是如何随时间变化的。 1. 为什么在测试时通常不使用暂退法? @@ -448,3 +543,7 @@ d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1811) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11774) +:end_tab: diff --git a/chapter_multilayer-perceptrons/environment.md b/chapter_multilayer-perceptrons/environment.md index 762606667..3a1cc4564 100644 --- a/chapter_multilayer-perceptrons/environment.md +++ b/chapter_multilayer-perceptrons/environment.md @@ -20,7 +20,7 @@ 通过将基于模型的决策引入环境,我们可能会破坏模型。 虽然我们不可能在一节中讨论全部的问题,但我们希望揭示一些常见的问题, -并激发你的批判性思考,以便及早发现这些情况,减轻灾难性的损害。 +并激发批判性思考,以便及早发现这些情况,减轻灾难性的损害。 有些解决方案很简单(要求“正确”的数据),有些在技术上很困难(实施强化学习系统), 还有一些解决方案要求我们完全跳出统计预测,解决一些棘手的、与算法伦理应用有关的哲学问题。 @@ -108,9 +108,8 @@ $P(y \mid \mathbf{x})$的分布可能会因我们的位置不同而得到不同 ### 医学诊断 -假设你想设计一个检测癌症的算法。 -你从健康人和病人那里收集数据,然后训练你的算法。 -它工作得很好,有很高的精度,然后你得出了你已经准备好在医疗诊断上取得成功的结论。 +假设我们想设计一个检测癌症的算法,从健康人和病人那里收集数据,然后训练算法。 +它工作得很好,有很高的精度,然后我们得出了已经准备好在医疗诊断上取得成功的结论。 请先别着急。 收集训练数据的分布和在实际中遇到的数据分布可能有很大的不同。 @@ -150,8 +149,8 @@ $P(y \mid \mathbf{x})$的分布可能会因我们的位置不同而得到不同 *非平稳分布*(nonstationary distribution)。 以下是一些典型例子: -* 训练一个计算广告模型,但却没有经常更新(例如,一个2009年训练的模型不知道一个叫iPad的不知名新设备刚刚上市)。 -* 建立一个垃圾邮件过滤器,它能很好地检测到所有垃圾邮件。但是,垃圾邮件发送者们变得聪明起来,制造出新的信息,看起来不像我们以前见过的任何垃圾邮件。 +* 训练一个计算广告模型,但却没有经常更新(例如,一个2009年训练的模型不知道一个叫iPad的不知名新设备刚刚上市); +* 建立一个垃圾邮件过滤器,它能很好地检测到所有垃圾邮件。但是,垃圾邮件发送者们变得聪明起来,制造出新的信息,看起来不像我们以前见过的任何垃圾邮件; * 建立一个产品推荐系统,它在整个冬天都有效,但圣诞节过后很久还会继续推荐圣诞帽。 ### 更多轶事 @@ -343,8 +342,8 @@ $$\mathbf{C} p(\mathbf{y}) = \mu(\hat{\mathbf{y}}),$$ 相反,通常情况下,概念的变化总是缓慢的。 比如下面是一些例子: -* 在计算广告中,新产品推出后,旧产品变得不那么受欢迎了。这意味着广告的分布和受欢迎程度是逐渐变化的,任何点击率预测器都需要随之逐渐变化。 -* 由于环境的磨损,交通摄像头的镜头会逐渐退化,影响摄像头的图像质量。 +* 在计算广告中,新产品推出后,旧产品变得不那么受欢迎了。这意味着广告的分布和受欢迎程度是逐渐变化的,任何点击率预测器都需要随之逐渐变化; +* 由于环境的磨损,交通摄像头的镜头会逐渐退化,影响摄像头的图像质量; * 新闻内容逐渐变化(即新新闻的出现)。 在这种情况下,我们可以使用与训练网络相同的方法,使其适应数据的变化。 @@ -431,9 +430,9 @@ $$ ## 机器学习中的公平、责任和透明度 -最后,重要的是,当你部署机器学习系统时, -你不仅仅是在优化一个预测模型, -而你通常是在提供一个会被用来(部分或完全)进行自动化决策的工具。 +最后,重要的是,当我们部署机器学习系统时, +不仅仅是在优化一个预测模型, +而通常是在提供一个会被用来(部分或完全)进行自动化决策的工具。 这些技术系统可能会通过其进行的决定而影响到每个人的生活。 从考虑预测到决策的飞跃不仅提出了新的技术问题, @@ -449,9 +448,9 @@ $$ 例如,考虑预测性警务系统,它将巡逻人员分配到预测犯罪率较高的地区。 很容易看出一种令人担忧的模式是如何出现的: - 1. 犯罪率高的社区会得到更多的巡逻。 - 2. 因此,在这些社区中会发现更多的犯罪行为,输入可用于未来迭代的训练数据。 - 3. 面对更多的积极因素,该模型预测这些社区还会有更多的犯罪。 + 1. 犯罪率高的社区会得到更多的巡逻; + 2. 因此,在这些社区中会发现更多的犯罪行为,输入可用于未来迭代的训练数据; + 3. 面对更多的积极因素,该模型预测这些社区还会有更多的犯罪; 4. 下一次迭代中,更新后的模型会更加倾向于针对同一个地区,这会导致更多的犯罪行为被发现等等。 通常,在建模纠正过程中,模型的预测与训练数据耦合的各种机制都没有得到解释, @@ -459,7 +458,7 @@ $$ 此外,我们首先要注意我们是否解决了正确的问题。 比如,预测算法现在在信息传播中起着巨大的中介作用, 个人看到的新闻应该由他们喜欢的Facebook页面决定吗? -这些只是你在机器学习职业生涯中可能遇到的令人感到“压力山大”的道德困境中的一小部分。 +这些只是在机器学习职业生涯中可能遇到的令人感到“压力山大”的道德困境中的一小部分。 ## 小结 diff --git a/chapter_multilayer-perceptrons/kaggle-house-price.md b/chapter_multilayer-perceptrons/kaggle-house-price.md index 455b95e2b..1a4ce5d63 100644 --- a/chapter_multilayer-perceptrons/kaggle-house-price.md +++ b/chapter_multilayer-perceptrons/kaggle-house-price.md @@ -102,15 +102,13 @@ def download_all(): #@save 有些研究人员短视地专注于预处理步骤,而不是考虑基础性问题。 但一个客观的平台有巨大的价值:该平台促进了竞争方法之间的直接定量比较,以及代码共享。 这便于每个人都可以学习哪些方法起作用,哪些没有起作用。 -如果你想参加Kaggle比赛,你首先需要注册一个账户(见 :numref:`fig_kaggle`)。 +如果我们想参加Kaggle比赛,首先需要注册一个账户(见 :numref:`fig_kaggle`)。 ![Kaggle网站](../img/kaggle.png) :width:`400px` :label:`fig_kaggle` -在房价预测比赛页面(如 :numref:`fig_house_pricing` 所示), -你在"Data"选项卡下可以找到数据集。 -你可以通过下面的网址提交预测,并查看排名: +在房价预测比赛页面(如 :numref:`fig_house_pricing` 所示)的"Data"选项卡下可以找到数据集。我们可以通过下面的网址提交预测,并查看排名: >https://www.kaggle.com/c/house-prices-advanced-regression-techniques @@ -132,11 +130,11 @@ def download_all(): #@save 开始之前,我们将[**使用`pandas`读入并处理数据**], 这是我们在 :numref:`sec_pandas`中引入的。 -因此,在继续操作之前,你需要确保已安装`pandas`。 -幸运的是,如果你正在用Jupyter阅读该书,你可以在不离开笔记本的情况下安装`pandas`。 +因此,在继续操作之前,我们需要确保已安装`pandas`。 +幸运的是,如果我们正在用Jupyter阅读该书,可以在不离开笔记本的情况下安装`pandas`。 ```{.python .input} -# 如果你没有安装pandas,请取消下一行的注释 +# 如果没有安装pandas,请取消下一行的注释 # !pip install pandas %matplotlib inline @@ -149,7 +147,7 @@ npx.set_np() ```{.python .input} #@tab pytorch -# 如果你没有安装pandas,请取消下一行的注释 +# 如果没有安装pandas,请取消下一行的注释 # !pip install pandas %matplotlib inline @@ -162,7 +160,7 @@ import numpy as np ```{.python .input} #@tab tensorflow -# 如果你没有安装pandas,请取消下一行的注释 +# 如果没有安装pandas,请取消下一行的注释 # !pip install pandas %matplotlib inline @@ -172,6 +170,22 @@ import pandas as pd import numpy as np ``` +```{.python .input} +#@tab paddle +# 如果你没有安装pandas,请取消下一行的注释 +# !pip install pandas + +%matplotlib inline +import warnings +import pandas as pd +import numpy as np +warnings.filterwarnings(action='ignore') +import paddle +from paddle import nn +warnings.filterwarnings("ignore", category=DeprecationWarning) +from d2l import paddle as d2l +``` + 为方便起见,我们可以使用上面定义的脚本下载并缓存Kaggle房屋数据集。 ```{.python .input} @@ -263,7 +277,7 @@ all_features = pd.get_dummies(all_features, dummy_na=True) all_features.shape ``` -你可以看到,此转换会将特征的总数量从79个增加到331个。 +可以看到此转换会将特征的总数量从79个增加到331个。 最后,通过`values`属性,我们可以 [**从`pandas`格式中提取NumPy格式,并将其转换为张量表示**]用于训练。 @@ -296,7 +310,7 @@ def get_net(): ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle loss = nn.MSELoss() in_features = train_features.shape[1] @@ -361,6 +375,16 @@ def log_rmse(y_true, y_pred): tf.math.log(y_true), tf.math.log(clipped_preds)))) ``` +```{.python .input} +#@tab paddle +def log_rmse(net, features, labels): + # 为了在取对数时进一步稳定该值,将小于1的值设置为1 + clipped_preds = paddle.clip(net(features), 1, float('inf')) + rmse = paddle.sqrt(loss(paddle.log(clipped_preds), + paddle.log(labels))) + return rmse.item() +``` + 与前面的部分不同,[**我们的训练函数将借助Adam优化器**] (我们将在后面章节更详细地描述它)。 Adam优化器的主要吸引力在于它对初始学习率不那么敏感。 @@ -430,9 +454,31 @@ def train(net, train_features, train_labels, test_features, test_labels, return train_ls, test_ls ``` +```{.python .input} +#@tab paddle +def train(net, train_features, train_labels, test_features, test_labels, + num_epochs, learning_rate, weight_decay, batch_size): + train_ls, test_ls = [], [] + train_iter = d2l.load_array((train_features, train_labels), batch_size) + # 这里使用的是Adam优化算法 + optimizer = paddle.optimizer.Adam(learning_rate=learning_rate*1.0, + parameters=net.parameters(), + weight_decay=weight_decay*1.0) + for epoch in range(num_epochs): + for X, y in train_iter: + l = loss(net(X), y) + l.backward() + optimizer.step() + optimizer.clear_grad() + train_ls.append(log_rmse(net, train_features, train_labels)) + if test_labels is not None: + test_ls.append(log_rmse(net, test_features, test_labels)) + return train_ls, test_ls +``` + ## $K$折交叉验证 -你可能还记得,我们在讨论模型选择的部分( :numref:`sec_model_selection`) +本书在讨论模型选择的部分( :numref:`sec_model_selection`) 中介绍了[**K折交叉验证**], 它有助于模型选择和超参数调整。 我们首先需要定义一个函数,在$K$折交叉验证过程中返回第$i$折的数据。 @@ -499,11 +545,11 @@ print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, ' 请注意,有时一组超参数的训练误差可能非常低,但$K$折交叉验证的误差要高得多, 这表明模型过拟合了。 -在整个训练过程中,你将希望监控训练误差和验证误差这两个数字。 +在整个训练过程中,我们希望监控训练误差和验证误差这两个数字。 较少的过拟合可能表明现有数据可以支撑一个更强大的模型, 较大的过拟合可能意味着我们可以通过正则化技术来获益。 -## [**提交你的Kaggle预测**] +## [**提交Kaggle预测**] 既然我们知道应该选择什么样的超参数, 我们不妨使用所有数据对其进行训练 @@ -541,12 +587,12 @@ train_and_pred(train_features, test_features, train_labels, test_data, 接下来,如 :numref:`fig_kaggle_submit2`中所示, 我们可以提交预测到Kaggle上,并查看在测试集上的预测与实际房价(标签)的比较情况。 -步骤非常简单: +步骤非常简单。 * 登录Kaggle网站,访问房价预测竞赛页面。 * 点击“Submit Predictions”或“Late Submission”按钮(在撰写本文时,该按钮位于右侧)。 -* 点击页面底部虚线框中的“Upload Submission File”按钮,选择你要上传的预测文件。 -* 点击页面底部的“Make Submission”按钮,即可查看你的结果。 +* 点击页面底部虚线框中的“Upload Submission File”按钮,选择要上传的预测文件。 +* 点击页面底部的“Make Submission”按钮,即可查看结果。 ![向Kaggle提交数据](../img/kaggle-submit2.png) :width:`400px` @@ -562,9 +608,9 @@ train_and_pred(train_features, test_features, train_labels, test_data, ## 练习 -1. 把你的预测提交给Kaggle,它有多好? -1. 你能通过直接最小化价格的对数来改进你的模型吗?如果你试图预测价格的对数而不是价格,会发生什么? -1. 用平均值替换缺失值总是好主意吗?提示:你能构造一个不随机丢失值的情况吗? +1. 把预测提交给Kaggle,它有多好? +1. 能通过直接最小化价格的对数来改进模型吗?如果试图预测价格的对数而不是价格,会发生什么? +1. 用平均值替换缺失值总是好主意吗?提示:能构造一个不随机丢失值的情况吗? 1. 通过$K$折交叉验证调整超参数,从而提高Kaggle的得分。 1. 通过改进模型(例如,层、权重衰减和dropout)来提高分数。 1. 如果我们没有像本节所做的那样标准化连续的数值特征,会发生什么? @@ -580,3 +626,7 @@ train_and_pred(train_features, test_features, train_labels, test_data, :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1825) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11775) +:end_tab: diff --git a/chapter_multilayer-perceptrons/mlp-concise.md b/chapter_multilayer-perceptrons/mlp-concise.md index 69b4bb1ea..873910828 100644 --- a/chapter_multilayer-perceptrons/mlp-concise.md +++ b/chapter_multilayer-perceptrons/mlp-concise.md @@ -1,7 +1,7 @@ # 多层感知机的简洁实现 :label:`sec_mlp_concise` -正如你所期待的,我们可以(**通过高级API更简洁地实现多层感知机**)。 +本节将介绍(**通过高级API更简洁地实现多层感知机**)。 ```{.python .input} from d2l import mxnet as d2l @@ -23,6 +23,15 @@ 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 +``` + ## 模型 与softmax回归的简洁实现( :numref:`sec_softmax_concise`)相比, @@ -59,6 +68,20 @@ net = tf.keras.models.Sequential([ tf.keras.layers.Dense(10)]) ``` +```{.python .input} +#@tab paddle +net = nn.Sequential(nn.Flatten(), + nn.Linear(784, 256), + nn.ReLU(), + nn.Linear(256, 10)) + + +for layer in net: + if type(layer) == nn.Linear: + weight_attr = paddle.framework.ParamAttr(initializer=paddle.nn.initializer.Normal(mean=0.0, std=0.01)) + layer.weight_attr = weight_attr +``` + [**训练过程**]的实现与我们实现softmax回归时完全相同, 这种模块化设计使我们能够将与模型架构有关的内容独立出来。 @@ -82,6 +105,13 @@ loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) trainer = tf.keras.optimizers.SGD(learning_rate=lr) ``` +```{.python .input} +#@tab paddle +batch_size, lr, num_epochs = 256, 0.1, 10 +loss = nn.CrossEntropyLoss(reduction='none') +trainer = paddle.optimizer.SGD(parameters=net.parameters(), learning_rate=lr) +``` + ```{.python .input} #@tab all train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) @@ -110,3 +140,7 @@ d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1801) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11770) +:end_tab: diff --git a/chapter_multilayer-perceptrons/mlp-scratch.md b/chapter_multilayer-perceptrons/mlp-scratch.md index 9f8ac1d73..2b544ec87 100644 --- a/chapter_multilayer-perceptrons/mlp-scratch.md +++ b/chapter_multilayer-perceptrons/mlp-scratch.md @@ -27,6 +27,15 @@ 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 +``` + ```{.python .input} #@tab all batch_size = 256 @@ -92,6 +101,22 @@ b2 = tf.Variable(tf.zeros(num_outputs)) params = [W1, b1, W2, b2] ``` +```{.python .input} +#@tab paddle +num_inputs, num_outputs, num_hiddens = 784, 10, 256 + +W1 = paddle.randn([num_inputs, num_hiddens]) * 0.01 +W1.stop_gradient = False +b1 = paddle.zeros([num_hiddens]) +b1.stop_gradient = False +W2 = paddle.randn([num_hiddens, num_outputs]) * 0.01 +W2.stop_gradient = False +b2 = paddle.zeros([num_outputs]) +b2.stop_gradient = False + +params = [W1, b1, W2, b2] +``` + ## 激活函数 为了确保我们对模型的细节了如指掌, @@ -116,6 +141,13 @@ def relu(X): return tf.math.maximum(X, 0) ``` +```{.python .input} +#@tab paddle +def relu(X): + a = paddle.zeros_like(X) + return paddle.maximum(X, a) +``` + ## 模型 因为我们忽略了空间结构, @@ -145,6 +177,14 @@ def net(X): return tf.matmul(H, W2) + b2 ``` +```{.python .input} +#@tab paddle +def net(X): + X = X.reshape((-1, num_inputs)) + H = relu(X@W1 + b1) # 这里“@”代表矩阵乘法 + return (H@W2 + b2) +``` + ## 损失函数 由于我们已经从零实现过softmax函数( :numref:`sec_softmax_scratch`), @@ -158,7 +198,7 @@ loss = gluon.loss.SoftmaxCrossEntropyLoss() ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle loss = nn.CrossEntropyLoss(reduction='none') ``` @@ -195,6 +235,13 @@ updater = d2l.Updater([W1, W2, b1, b2], lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater) ``` +```{.python .input} +#@tab paddle +num_epochs, lr = 10, 0.1 +updater = paddle.optimizer.SGD(learning_rate=lr, parameters=params) +d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater) +``` + 为了对学习到的模型进行评估,我们将[**在一些测试数据上应用这个模型**]。 ```{.python .input} @@ -213,7 +260,7 @@ d2l.predict_ch3(net, test_iter) 1. 改变学习速率会如何影响结果?保持模型架构和其他超参数(包括轮数)不变,学习率设置为多少会带来最好的结果? 1. 通过对所有超参数(学习率、轮数、隐藏层数、每层的隐藏单元数)进行联合优化,可以得到的最佳结果是什么? 1. 描述为什么涉及多个超参数更具挑战性。 -1. 如果要构建多个超参数的搜索方法,你能想到的最聪明的策略是什么? +1. 如果想要构建多个超参数的搜索方法,请想出一个聪明的策略。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1800) @@ -226,3 +273,7 @@ d2l.predict_ch3(net, test_iter) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1798) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11769) +:end_tab: diff --git a/chapter_multilayer-perceptrons/mlp.md b/chapter_multilayer-perceptrons/mlp.md index 8784a0106..d3fc1bfb4 100644 --- a/chapter_multilayer-perceptrons/mlp.md +++ b/chapter_multilayer-perceptrons/mlp.md @@ -36,8 +36,8 @@ 然而我们可以很容易找出违反单调性的例子。 例如,我们想要根据体温预测死亡率。 -对于体温高于37摄氏度的人来说,温度越高风险越大。 -然而,对于体温低于37摄氏度的人来说,温度越高风险就越低。 +对体温高于37摄氏度的人来说,温度越高风险越大。 +然而,对体温低于37摄氏度的人来说,温度越高风险就越低。 在这种情况下,我们也可以通过一些巧妙的预处理来解决问题。 例如,我们可以使用与37摄氏度的距离作为特征。 @@ -107,8 +107,7 @@ $$ $$ 注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。 -可我们能从中得到什么好处呢? -你可能会惊讶地发现:在上面定义的模型里,我们没有好处! +可我们能从中得到什么好处呢?在上面定义的模型里,我们没有好处! 原因很简单:上面的隐藏单元由输入的仿射函数给出, 而输出(softmax操作前)只是隐藏单元的仿射函数。 仿射函数的仿射函数本身就是仿射函数, @@ -143,7 +142,7 @@ $$ 即一次计算一个样本。 我们在 :numref:`subsec_softmax_vectorization`中 以相同的方式使用了softmax符号来表示按行操作。 -但是在本节中,我们应用于隐藏层的激活函数通常不仅按行操作,也按元素操作。 +但是本节应用于隐藏层的激活函数通常不仅按行操作,也按元素操作。 这意味着在计算每一层的线性部分之后,我们可以计算每个活性值, 而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。 @@ -198,6 +197,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 +``` + ### ReLU函数 最受欢迎的激活函数是*修正线性单元*(Rectified linear unit,*ReLU*), @@ -233,6 +241,14 @@ y = tf.nn.relu(x) d2l.plot(x.numpy(), y.numpy(), 'x', 'relu(x)', figsize=(5, 2.5)) ``` +```{.python .input} +#@tab paddle +x = paddle.arange(-8.0, 8.0, 0.1, dtype='float32') +x.stop_gradient = False +y = paddle.nn.functional.relu(x) +d2l.plot(x.detach().numpy(), y.detach().numpy(), 'x', 'relu(x)', figsize=(5, 2.5)) +``` + 当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。 @@ -260,6 +276,12 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of relu', figsize=(5, 2.5)) ``` +```{.python .input} +#@tab paddle +y.backward(paddle.ones_like(x), retain_graph=True) +d2l.plot(x.detach().numpy(), x.grad.numpy(), 'x', 'grad of relu', figsize=(5, 2.5)) +``` + 使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。 这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题(稍后将详细介绍)。 @@ -286,7 +308,7 @@ $$\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x).$$ sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似。 当我们想要将输出视作二元分类问题的概率时, sigmoid仍然被广泛用作输出单元上的激活函数 -(你可以将sigmoid视为softmax的特例)。 +(sigmoid可以视为softmax的特例)。 然而,sigmoid在隐藏层中已经较少使用, 它在大部分时候被更简单、更容易训练的ReLU所取代。 在后面关于循环神经网络的章节中,我们将描述利用sigmoid单元来控制时序信息流的架构。 @@ -312,6 +334,12 @@ y = tf.nn.sigmoid(x) d2l.plot(x.numpy(), y.numpy(), 'x', 'sigmoid(x)', figsize=(5, 2.5)) ``` +```{.python .input} +#@tab paddle +y = paddle.nn.functional.sigmoid(x) +d2l.plot(x.detach().numpy(), y.detach().numpy(), 'x', 'sigmoid(x)', figsize=(5, 2.5)) +``` + sigmoid函数的导数为下面的公式: $$\frac{d}{dx} \operatorname{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \operatorname{sigmoid}(x)\left(1-\operatorname{sigmoid}(x)\right).$$ @@ -341,6 +369,14 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of sigmoid', figsize=(5, 2.5)) ``` +```{.python .input} +#@tab paddle +# 清除以前的梯度。 +x.clear_gradient() +y.backward(paddle.ones_like(x), retain_graph=True) +d2l.plot(x.detach().numpy(), x.grad.numpy(), 'x', 'grad of sigmoid', figsize=(5, 2.5)) +``` + ### tanh函数 与sigmoid函数类似, @@ -372,6 +408,12 @@ y = tf.nn.tanh(x) d2l.plot(x.numpy(), y.numpy(), 'x', 'tanh(x)', figsize=(5, 2.5)) ``` +```{.python .input} +#@tab paddle +y = paddle.tanh(x) +d2l.plot(x.detach().numpy(), y.detach().numpy(), 'x', 'tanh(x)', figsize=(5, 2.5)) +``` + tanh函数的导数是: $$\frac{d}{dx} \operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x).$$ @@ -402,6 +444,14 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of tanh', figsize=(5, 2.5)) ``` +```{.python .input} +#@tab paddle +# 清除以前的梯度。 +x.clear_gradient() +y.backward(paddle.ones_like(x), retain_graph=True) +d2l.plot(x.detach().numpy(), x.grad.numpy(), 'x', 'grad of tanh', figsize=(5, 2.5)) +``` + 总结一下,我们现在了解了如何结合非线性函数来构建具有更强表达能力的多层神经网络架构。 顺便说一句,这些知识已经让你掌握了一个类似于1990年左右深度学习从业者的工具。 在某些方面,你比在20世纪90年代工作的任何人都有优势, @@ -418,7 +468,7 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of tanh', 1. 计算pReLU激活函数的导数。 1. 证明一个仅使用ReLU(或pReLU)的多层感知机构造了一个连续的分段线性函数。 1. 证明$\operatorname{tanh}(x) + 1 = 2 \operatorname{sigmoid}(2x)$。 -1. 假设我们有一个非线性单元,将它一次应用于一个小批量的数据。你认为这会导致什么样的问题? +1. 假设我们有一个非线性单元,将它一次应用于一个小批量的数据。这会导致什么样的问题? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1797) @@ -431,3 +481,7 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of tanh', :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1795) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11768) +:end_tab: diff --git a/chapter_multilayer-perceptrons/numerical-stability-and-init.md b/chapter_multilayer-perceptrons/numerical-stability-and-init.md index 9a7c1fe46..3d5cab639 100644 --- a/chapter_multilayer-perceptrons/numerical-stability-and-init.md +++ b/chapter_multilayer-perceptrons/numerical-stability-and-init.md @@ -2,15 +2,14 @@ :label:`sec_numerical_stability` 到目前为止,我们实现的每个模型都是根据某个预先指定的分布来初始化模型的参数。 -你可能认为初始化方案是理所当然的,忽略了如何做出这些选择的细节。 -你甚至可能会觉得,初始化方案的选择并不是特别重要。 +有人会认为初始化方案是理所当然的,忽略了如何做出这些选择的细节。甚至有人可能会觉得,初始化方案的选择并不是特别重要。 相反,初始化方案的选择在神经网络学习中起着举足轻重的作用, 它对保持数值稳定性至关重要。 此外,这些初始化方案的选择可以与非线性激活函数的选择有趣的结合在一起。 我们选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度有多快。 糟糕选择可能会导致我们在训练时遇到梯度爆炸或梯度消失。 -在本节中,我们将更详细地探讨这些主题,并讨论一些有用的启发式方法。 -你会发现这些启发式方法在你的整个深度学习生涯中都很有用。 +本节将更详细地探讨这些主题,并讨论一些有用的启发式方法。 +这些启发式方法在整个深度学习生涯中都很有用。 ## 梯度消失和梯度爆炸 @@ -98,7 +97,24 @@ d2l.plot(x.numpy(), [y.numpy(), t.gradient(y, x).numpy()], legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5)) ``` -正如你所看到的,当sigmoid函数的输入很大或是很小时,它的梯度都会消失。 +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle + +x = paddle.arange(start=-8.0, end=8.0, step=0.1, dtype='float32') +x.stop_gradient = False +y = paddle.nn.functional.sigmoid(x) +y.backward(paddle.ones_like(x)) + +d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()], + legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5)) +``` + +正如上图,当sigmoid函数的输入很大或是很小时,它的梯度都会消失。 此外,当反向传播通过许多层时,除非我们在刚刚好的地方, 这些地方sigmoid函数的输入接近于零,否则整个乘积的梯度可能会消失。 当我们的网络有很多层时,除非我们很小心,否则在某一层可能会切断梯度。 @@ -141,6 +157,16 @@ for i in range(100): print('乘以100个矩阵后\n', M.numpy()) ``` +```{.python .input} +#@tab paddle +M = paddle.normal(0, 1, shape=(4,4)) +print('一个矩阵 \n',M) +for i in range(100): + M = paddle.mm(M, paddle.normal(0, 1, shape=(4, 4))) + +print('乘以100个矩阵后\n', M) +``` + ### 打破对称性 神经网络设计中的另一个问题是其参数化所固有的对称性。 @@ -237,9 +263,9 @@ $$U\left(-\sqrt{\frac{6}{n_\mathrm{in} + n_\mathrm{out}}}, \sqrt{\frac{6}{n_\mat :cite:`Xiao.Bahri.Sohl-Dickstein.ea.2018`, 可以无须架构上的技巧而训练10000层神经网络的可能性。 -如果你对该主题感兴趣,我们建议你深入研究本模块的内容, +如果有读者对该主题感兴趣,我们建议深入研究本模块的内容, 阅读提出并分析每种启发式方法的论文,然后探索有关该主题的最新出版物。 -也许你会偶然发现甚至发明一个聪明的想法,并为深度学习框架提供一个实现。 +也许会偶然发现甚至发明一个聪明的想法,并为深度学习框架提供一个实现。 ## 小结 @@ -251,7 +277,7 @@ $$U\left(-\sqrt{\frac{6}{n_\mathrm{in} + n_\mathrm{out}}}, \sqrt{\frac{6}{n_\mat ## 练习 -1. 除了多层感知机的排列对称性之外,你能设计出其他神经网络可能会表现出对称性且需要被打破的情况吗? +1. 除了多层感知机的排列对称性之外,还能设计出其他神经网络可能会表现出对称性且需要被打破的情况吗? 2. 我们是否可以将线性回归或softmax回归中的所有权重参数初始化为相同的值? 3. 在相关资料中查找两个矩阵乘积特征值的解析界。这对确保梯度条件合适有什么启示? 4. 如果我们知道某些项是发散的,我们能在事后修正吗?看看关于按层自适应速率缩放的论文 :cite:`You.Gitman.Ginsburg.2017` 。 @@ -267,3 +293,7 @@ $$U\left(-\sqrt{\frac{6}{n_\mathrm{in} + n_\mathrm{out}}}, \sqrt{\frac{6}{n_\mat :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1817) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11776) +:end_tab: diff --git a/chapter_multilayer-perceptrons/underfit-overfit.md b/chapter_multilayer-perceptrons/underfit-overfit.md index 01979745c..d7745188e 100644 --- a/chapter_multilayer-perceptrons/underfit-overfit.md +++ b/chapter_multilayer-perceptrons/underfit-overfit.md @@ -28,8 +28,8 @@ 将模型在训练数据上拟合的比在潜在分布中更接近的现象称为*过拟合*(overfitting), 用于对抗过拟合的技术称为*正则化*(regularization)。 -在前面的章节中,你可能在用Fashion-MNIST数据集做实验时已经观察到了这种过拟合现象。 -在实验中调整模型架构或超参数时,你会发现: +在前面的章节中,有些读者可能在用Fashion-MNIST数据集做实验时已经观察到了这种过拟合现象。 +在实验中调整模型架构或超参数时会发现: 如果有足够多的神经元、层数和训练迭代周期, 模型最终可以在训练集上达到完美的精度,此时测试集的准确性却下降了。 @@ -96,7 +96,7 @@ 并不比抽取的第2个样本和第200万个样本的相关性更强。 要成为一名优秀的机器学习科学家需要具备批判性思考能力。 -你应该已经从这个假设中找出漏洞,即很容易找出假设失效的情况。 +假设是存在漏洞的,即很容易找出假设失效的情况。 如果我们根据从加州大学旧金山分校医学中心的患者数据训练死亡风险预测模型, 并将其应用于马萨诸塞州综合医院的患者数据,结果会怎么样? 这两个数据的分布可能不完全一样。 @@ -132,7 +132,7 @@ 例如,具有更多参数的模型可能被认为更复杂, 参数有更大取值范围的模型可能更为复杂。 通常对于神经网络,我们认为需要更多训练迭代的模型比较复杂, -而需要“早停”(early stopping)的模型(即较少训练迭代周期)就不那么复杂。 +而需要*早停*(early stopping)的模型(即较少训练迭代周期)就不那么复杂。 我们很难比较本质上不同大类的模型之间(例如,决策树与神经网络)的复杂性。 就目前而言,一条简单的经验法则相当有用: @@ -144,11 +144,11 @@ 也就是说,我们在观察事实之后进行估计,因此容易受到相关谬误的影响。 目前,我们将把哲学放在一边,坚持更切实的问题。 -在本节中,为了给你一些直观的印象,我们将重点介绍几个倾向于影响模型泛化的因素: +本节为了给出一些直观的印象,我们将重点介绍几个倾向于影响模型泛化的因素。 1. 可调整参数的数量。当可调整参数的数量(有时称为*自由度*)很大时,模型往往更容易过拟合。 1. 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。 -1. 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。 +1. 训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。 ## 模型选择 @@ -285,6 +285,17 @@ import numpy as np import math ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +import numpy as np +import math +``` + ### 生成数据集 给定$x$,我们将[**使用以下三阶多项式来生成训练和测试数据的标签:**] @@ -321,7 +332,7 @@ labels += np.random.normal(scale=0.1, size=labels.shape) 第一个值是与偏置相对应的常量特征。 ```{.python .input} -#@tab pytorch, tensorflow +#@tab pytorch, tensorflow, paddle # NumPy ndarray转换为tensor true_w, features, poly_features, labels = [d2l.tensor(x, dtype= d2l.float32) for x in [true_w, features, poly_features, labels]] @@ -360,6 +371,19 @@ def evaluate_loss(net, data_iter, loss): #@save return metric[0] / metric[1] ``` +```{.python .input} +#@tab paddle +def evaluate_loss(net, data_iter, loss): #@save + """评估给定数据集上模型的损失。""" + metric = d2l.Accumulator(2) # 损失的总和, 样本数量 + for X, y in data_iter: + out = net(X) + y = y.reshape(out.shape) + l = loss(out, y) + metric.add(l.sum(), l.numel()) + return metric[0] / metric[1] +``` + 现在[**定义训练函数**]。 ```{.python .input} @@ -437,6 +461,32 @@ def train(train_features, test_features, train_labels, test_labels, print('weight:', net.get_weights()[0].T) ``` +```{.python .input} +#@tab paddle +def train(train_features, test_features, train_labels, test_labels, + num_epochs=400): + loss = nn.MSELoss() + input_shape = train_features.shape[-1] + # 不设置偏置,因为我们已经在多项式特征中实现了它 + net = nn.Sequential(nn.Linear(input_shape, 1, bias_attr=False)) + batch_size = min(10, train_labels.shape[0]) + print(batch_size) + train_iter = d2l.load_array(((train_features, train_labels.reshape([-1,1]))), + batch_size) + test_iter = d2l.load_array((test_features, test_labels.reshape([-1,1])), + batch_size, is_train=False) + trainer = paddle.optimizer.SGD(parameters=net.parameters(), learning_rate=0.01) + animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log', + xlim=[1, num_epochs], ylim=[1e-3, 1e2], + legend=['train', 'test']) + for epoch in range(num_epochs): + d2l.train_epoch_ch3(net, train_iter, loss, trainer) + if epoch == 0 or (epoch + 1) % 20 == 0: + animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss), + evaluate_loss(net, test_iter, loss))) + print('weight:', net[0].weight.numpy()) +``` + ### [**三阶多项式函数拟合(正常)**] 我们将首先使用三阶多项式函数,它与数据生成函数的阶数相同。 @@ -489,13 +539,13 @@ train(poly_features[:n_train, :], poly_features[n_train:, :], ## 练习 -1. 你能准确地解出这个多项式回归问题吗?提示:使用线性代数。 -1. 考虑多项式的模型选择: - 1. 绘制训练损失与模型复杂度(多项式的阶数)的关系图。你观察到了什么?需要多少阶的多项式才能将训练损失减少到0? +1. 这个多项式回归问题可以准确地解出吗?提示:使用线性代数。 +1. 考虑多项式的模型选择。 + 1. 绘制训练损失与模型复杂度(多项式的阶数)的关系图。观察到了什么?需要多少阶的多项式才能将训练损失减少到0? 1. 在这种情况下绘制测试的损失图。 1. 生成同样的图,作为数据量的函数。 -1. 如果你不对多项式特征$x^i$进行标准化($1/i!$),会发生什么事情?你能用其他方法解决这个问题吗? -1. 你能期待看到泛化误差为零吗? +1. 如果不对多项式特征$x^i$进行标准化($1/i!$),会发生什么事情?能用其他方法解决这个问题吗? +1. 泛化误差可能为零吗? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1807) @@ -508,3 +558,7 @@ train(poly_features[:n_train, :], poly_features[n_train:, :], :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1805) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11771) +:end_tab: diff --git a/chapter_multilayer-perceptrons/weight-decay.md b/chapter_multilayer-perceptrons/weight-decay.md index 677990bd1..246166567 100644 --- a/chapter_multilayer-perceptrons/weight-decay.md +++ b/chapter_multilayer-perceptrons/weight-decay.md @@ -9,7 +9,7 @@ 回想一下,在多项式回归的例子( :numref:`sec_model_selection`)中, 我们可以通过调整拟合多项式的阶数来限制模型的容量。 实际上,限制特征的数量是缓解过拟合的一种常用技术。 -然而,简单地丢弃特征对于这项工作来说可能过于生硬。 +然而,简单地丢弃特征对这项工作来说可能过于生硬。 我们继续思考多项式回归的例子,考虑高维输入可能发生的情况。 多项式对多变量数据的自然扩展称为*单项式*(monomials), 也可以说是变量幂的乘积。 @@ -69,12 +69,12 @@ $$L(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2,$$ 对于$\lambda > 0$,我们限制$\| \mathbf{w} \|$的大小。 这里我们仍然除以$2$:当我们取一个二次函数的导数时, $2$和$1/2$会抵消,以确保更新表达式看起来既漂亮又简单。 -你可能会想知道为什么我们使用平方范数而不是标准范数(即欧几里得距离)? +为什么在这里我们使用平方范数而不是标准范数(即欧几里得距离)? 我们这样做是为了便于计算。 通过平方$L_2$范数,我们去掉平方根,留下权重向量每个分量的平方和。 这使得惩罚的导数很容易计算:导数的和等于和的导数。 -此外,你可能会问为什么我们首先使用$L_2$范数,而不是$L_1$范数。 +此外,为什么我们首先使用$L_2$范数,而不是$L_1$范数。 事实上,这个选择在整个统计领域中都是有效的和受欢迎的。 $L_2$正则化线性模型构成经典的*岭回归*(ridge regression)算法, $L_1$正则化线性回归是统计学中类似的基本模型, @@ -134,6 +134,16 @@ 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 +from paddle import nn +``` + 首先,我们[**像以前一样生成一些数据**],生成公式如下: (**$$y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \text{ where } @@ -187,6 +197,16 @@ def init_params(): return [w, b] ``` +```{.python .input} +#@tab paddle +def init_params(): + w = paddle.normal(0, 1, shape=(num_inputs, 1)) + w.stop_gradient = False + b = paddle.zeros(shape=[1]) + b.stop_gradient = False + return [w, b] +``` + ### (**定义$L_2$范数惩罚**) 实现这一惩罚最方便的方法是对所有项求平方后并将它们求和。 @@ -208,6 +228,12 @@ def l2_penalty(w): return tf.reduce_sum(tf.pow(w, 2)) / 2 ``` +```{.python .input} +#@tab paddle +def l2_penalty(w): + return paddle.sum(w.pow(2)) / 2 +``` + ### [**定义训练代码实现**] 下面的代码将模型拟合训练数据集,并在测试数据集上进行评估。 @@ -279,6 +305,27 @@ def train(lambd): print('w的L2范数是:', tf.norm(w).numpy()) ``` +```{.python .input} +#@tab paddle +def train(lambd): + w, b = init_params() + net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss + num_epochs, lr = 100, 0.003 + animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log', + xlim=[5, num_epochs], legend=['train', 'test']) + for epoch in range(num_epochs): + for X, y in train_iter(): + # 增加了L2范数惩罚项, + # 广播机制使l2_penalty(w)成为一个长度为`batch_size`的向量 + l = loss(net(X), y) + lambd * l2_penalty(w) + l.sum().backward() + d2l.sgd([w, b], lr, batch_size) + if (epoch + 1) % 5 == 0: + animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss), + d2l.evaluate_loss(net, test_iter, loss))) + print('w的L2范数是:', paddle.norm(w).item()) +``` + ### [**忽略正则化直接训练**] 我们现在用`lambd = 0`禁用权重衰减后运行这个代码。 @@ -407,6 +454,30 @@ def train_concise(wd): print('w的L2范数:', tf.norm(net.get_weights()[0]).numpy()) ``` +```{.python .input} +#@tab paddle +def train_concise(wd): + weight_attr = paddle.framework.ParamAttr(initializer=paddle.nn.initializer.Normal(mean=0.0, std=1.0)) + bias_attr = paddle.framework.ParamAttr(initializer=paddle.nn.initializer.Normal(mean=0.0, std=1.0)) + net = nn.Sequential(nn.Linear(num_inputs, 1, weight_attr=weight_attr, bias_attr=bias_attr)) + loss = nn.MSELoss() + num_epochs, lr = 100, 0.003 + # 偏置参数没有衰减。 + trainer = paddle.optimizer.SGD(parameters=net[0].parameters(), learning_rate=lr, weight_decay=wd*1.0) + animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log', + xlim=[5, num_epochs], legend=['train', 'test']) + for epoch in range(num_epochs): + for X, y in train_iter: + l = loss(net(X), y) + l.backward() + trainer.step() + trainer.clear_grad() + if (epoch + 1) % 5 == 0: + animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss), + d2l.evaluate_loss(net, test_iter, loss))) + print('w的L2范数:', net[0].weight.norm().item()) +``` + [**这些图看起来和我们从零开始实现权重衰减时的图相同**]。 然而,它们运行得更快,更容易实现。 对于更复杂的问题,这一好处将变得更加明显。 @@ -437,11 +508,11 @@ train_concise(3) ## 练习 -1. 在本节的估计问题中使用$\lambda$的值进行实验。绘制训练和测试精度关于$\lambda$的函数。你观察到了什么? +1. 在本节的估计问题中使用$\lambda$的值进行实验。绘制训练和测试精度关于$\lambda$的函数。观察到了什么? 1. 使用验证集来找到最佳值$\lambda$。它真的是最优值吗?这有关系吗? 1. 如果我们使用$\sum_i |w_i|$作为我们选择的惩罚($L_1$正则化),那么更新方程会是什么样子? -1. 我们知道$\|\mathbf{w}\|^2 = \mathbf{w}^\top \mathbf{w}$。你能找到类似的矩阵方程吗(见 :numref:`subsec_lin-algebra-norms` 中的Frobenius范数)? -1. 回顾训练误差和泛化误差之间的关系。除了权重衰减、增加训练数据、使用适当复杂度的模型之外,你还能想出其他什么方法来处理过拟合? +1. 我们知道$\|\mathbf{w}\|^2 = \mathbf{w}^\top \mathbf{w}$。能找到类似的矩阵方程吗(见 :numref:`subsec_lin-algebra-norms` 中的Frobenius范数)? +1. 回顾训练误差和泛化误差之间的关系。除了权重衰减、增加训练数据、使用适当复杂度的模型之外,还能想出其他什么方法来处理过拟合? 1. 在贝叶斯统计中,我们使用先验和似然的乘积,通过公式$P(w \mid x) \propto P(x \mid w) P(w)$得到后验。如何得到带正则化的$P(w)$? :begin_tab:`mxnet` @@ -455,3 +526,7 @@ train_concise(3) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1809) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11773) +:end_tab: diff --git a/chapter_natural-language-processing-applications/finetuning-bert.md b/chapter_natural-language-processing-applications/finetuning-bert.md index 04e9157c8..c964c3b21 100644 --- a/chapter_natural-language-processing-applications/finetuning-bert.md +++ b/chapter_natural-language-processing-applications/finetuning-bert.md @@ -1,4 +1,4 @@ -# 针对序列级和词元级应用程序微调BERT +# 针对序列级和词元级应用微调BERT :label:`sec_finetuning-bert` 在本章的前几节中,我们为自然语言处理应用设计了不同的模型,例如基于循环神经网络、卷积神经网络、注意力和多层感知机。这些模型在有空间或时间限制的情况下是有帮助的,但是,为每个自然语言处理任务精心设计一个特定的模型实际上是不可行的。在 :numref:`sec_bert`中,我们介绍了一个名为BERT的预训练模型,该模型可以对广泛的自然语言处理任务进行最少的架构更改。一方面,在提出时,BERT改进了各种自然语言处理任务的技术水平。另一方面,正如在 :numref:`sec_bert-pretraining`中指出的那样,原始BERT模型的两个版本分别带有1.1亿和3.4亿个参数。因此,当有足够的计算资源时,我们可以考虑为下游自然语言处理应用微调BERT。 @@ -26,7 +26,7 @@ * "A woman is eating something."(“一个女人在吃东西。”),"A woman is eating meat."(“一个女人在吃肉。”),3.000分; * "A woman is dancing."(一个女人在跳舞。),"A man is talking."(“一个人在说话。”),0.000分。 -![文本对分类或回归应用程序的BERT微调,如自然语言推断和语义文本相似性(假设输入文本对分别有两个词元和三个词元)](../img/bert-two-seqs.svg) +![文本对分类或回归应用的BERT微调,如自然语言推断和语义文本相似性(假设输入文本对分别有两个词元和三个词元)](../img/bert-two-seqs.svg) :label:`fig_bert-two-seqs` 与 :numref:`fig_bert-one-seq`中的单文本分类相比, :numref:`fig_bert-two-seqs`中的文本对分类的BERT微调在输入表示上有所不同。对于文本对回归任务(如语义文本相似性),可以应用细微的更改,例如输出连续的标签值和使用均方损失:它们在回归中很常见。 diff --git a/chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md b/chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md index 16a4c710d..9bf54565b 100644 --- a/chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +++ b/chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md @@ -71,6 +71,24 @@ d2l.DATA_HUB['SNLI'] = ( data_dir = d2l.download_extract('SNLI') ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +import os +import re + +#@save +d2l.DATA_HUB['SNLI'] = ( + 'https://nlp.stanford.edu/projects/snli/snli_1.0.zip', + '9fcde07509c7e87ec61c640c1b2753d9041758e4') + +data_dir = d2l.download_extract('SNLI') +``` + ### [**读取数据集**] 原始的SNLI数据集包含的信息比我们在实验中真正需要的信息丰富得多。因此,我们定义函数`read_snli`以仅提取数据集的一部分,然后返回前提、假设及其标签的列表。 @@ -99,7 +117,7 @@ def read_snli(data_dir, is_train): return premises, hypotheses, labels ``` -现在让我们[**打印前3对**]前提和假设,以及它们的标签(“0”、“1”和“2”分别对应于“蕴涵”、“矛盾”和“中性”)。 +现在让我们[**打印前3对**]前提和假设,以及它们的标签(“0”“1”和“2”分别对应于“蕴涵”“矛盾”和“中性”)。 ```{.python .input} #@tab all @@ -110,7 +128,7 @@ for x0, x1, y in zip(train_data[0][:3], train_data[1][:3], train_data[2][:3]): print('标签:', y) ``` -训练集约有550000对,测试集约有10000对。下面显示了训练集和测试集中的三个[**标签“蕴涵”、“矛盾”和“中性”是平衡的**]。 +训练集约有550000对,测试集约有10000对。下面显示了训练集和测试集中的三个[**标签“蕴涵”“矛盾”和“中性”是平衡的**]。 ```{.python .input} #@tab all @@ -184,6 +202,37 @@ class SNLIDataset(torch.utils.data.Dataset): return len(self.premises) ``` +```{.python .input} +#@tab paddle +#@save +class SNLIDataset(paddle.io.Dataset): + """用于加载SNLI数据集的自定义数据集""" + def __init__(self, dataset, num_steps, vocab=None): + self.num_steps = num_steps + all_premise_tokens = d2l.tokenize(dataset[0]) + all_hypothesis_tokens = d2l.tokenize(dataset[1]) + if vocab is None: + self.vocab = d2l.Vocab(all_premise_tokens + \ + all_hypothesis_tokens, min_freq=5, reserved_tokens=['']) + else: + self.vocab = vocab + self.premises = self._pad(all_premise_tokens) + self.hypotheses = self._pad(all_hypothesis_tokens) + self.labels = paddle.to_tensor(dataset[2]) + print('read ' + str(len(self.premises)) + ' examples') + + def _pad(self, lines): + return paddle.to_tensor([d2l.truncate_pad( + self.vocab[line], self.num_steps, self.vocab['']) + for line in lines]) + + def __getitem__(self, idx): + return (self.premises[idx], self.hypotheses[idx]), self.labels[idx] + + def __len__(self): + return len(self.premises) +``` + ### [**整合代码**] 现在,我们可以调用`read_snli`函数和`SNLIDataset`类来下载SNLI数据集,并返回训练集和测试集的`DataLoader`实例,以及训练集的词表。值得注意的是,我们必须使用从训练集构造的词表作为测试集的词表。因此,在训练集中训练的模型将不知道来自测试集的任何新词元。 @@ -225,6 +274,29 @@ def load_data_snli(batch_size, num_steps=50): return train_iter, test_iter, train_set.vocab ``` +```{.python .input} +#@tab paddle +#@save +def load_data_snli(batch_size, num_steps=50): + """下载SNLI数据集并返回数据迭代器和词表""" + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('SNLI') + train_data = read_snli(data_dir, True) + test_data = read_snli(data_dir, False) + train_set = SNLIDataset(train_data, num_steps) + test_set = SNLIDataset(test_data, num_steps, train_set.vocab) + train_iter = paddle.io.DataLoader(train_set,batch_size=batch_size, + shuffle=True, + num_workers=num_workers, + return_list=True) + + test_iter = paddle.io.DataLoader(test_set, batch_size=batch_size, + shuffle=False, + num_workers=num_workers, + return_list=True) + return train_iter, test_iter, train_set.vocab +``` + 在这里,我们将批量大小设置为128时,将序列长度设置为50,并调用`load_data_snli`函数来获取数据迭代器和词表。然后我们打印词表大小。 ```{.python .input} @@ -252,7 +324,7 @@ for X, Y in train_iter: ## 练习 -1. 机器翻译长期以来一直是基于翻译输出和翻译真实值之间的表面$n$元语法匹配来进行评估的。你能设计一种用自然语言推断来评价机器翻译结果的方法吗? +1. 机器翻译长期以来一直是基于翻译输出和翻译真实值之间的表面$n$元语法匹配来进行评估的。可以设计一种用自然语言推断来评价机器翻译结果的方法吗? 1. 我们如何更改超参数以减小词表大小? :begin_tab:`mxnet` @@ -262,3 +334,7 @@ for X, Y in train_iter: :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5722) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11828) +:end_tab: diff --git a/chapter_natural-language-processing-applications/natural-language-inference-attention.md b/chapter_natural-language-processing-applications/natural-language-inference-attention.md index 6f6d5e163..388d84049 100644 --- a/chapter_natural-language-processing-applications/natural-language-inference-attention.md +++ b/chapter_natural-language-processing-applications/natural-language-inference-attention.md @@ -1,7 +1,7 @@ # 自然语言推断:使用注意力 :label:`sec_natural-language-inference-attention` -我们在 :numref:`sec_natural-language-inference-and-dataset`中介绍了自然语言推断任务和SNLI数据集。鉴于许多模型都是基于复杂而深度的架构,Parikh等人提出用注意力机制解决自然语言推断问题,并称之为“可分解注意力模型” :cite:`Parikh.Tackstrom.Das.ea.2016`。这使得模型没有循环层或卷积层,在SNLI数据集上以更少的参数实现了当时的最佳结果。在本节中,我们将描述并实现这种基于注意力的自然语言推断方法(使用MLP),如 :numref:`fig_nlp-map-nli-attention`中所述。 +我们在 :numref:`sec_natural-language-inference-and-dataset`中介绍了自然语言推断任务和SNLI数据集。鉴于许多模型都是基于复杂而深度的架构,Parikh等人提出用注意力机制解决自然语言推断问题,并称之为“可分解注意力模型” :cite:`Parikh.Tackstrom.Das.ea.2016`。这使得模型没有循环层或卷积层,在SNLI数据集上以更少的参数实现了当时的最佳结果。本节将描述并实现这种基于注意力的自然语言推断方法(使用MLP),如 :numref:`fig_nlp-map-nli-attention`中所述。 ![将预训练GloVe送入基于注意力和MLP的自然语言推断架构](../img/nlp-map-nli-attention.svg) :label:`fig_nlp-map-nli-attention` @@ -31,6 +31,16 @@ from torch import nn from torch.nn import functional as F ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +from paddle.nn import functional as F +``` + ### 注意(Attending) 第一步是将一个文本序列中的词元与另一个序列中的每个词元对齐。假设前提是“我确实需要睡眠”,假设是“我累了”。由于语义上的相似性,我们不妨将假设中的“我”与前提中的“我”对齐,将假设中的“累”与前提中的“睡眠”对齐。同样,我们可能希望将前提中的“我”与假设中的“我”对齐,将前提中的“需要”和“睡眠”与假设中的“累”对齐。请注意,这种对齐是使用加权平均的“软”对齐,其中理想情况下较大的权重与要对齐的词元相关联。为了便于演示, :numref:`fig_nli_attention`以“硬”对齐的方式显示了这种对齐方式。 @@ -69,6 +79,23 @@ def mlp(num_inputs, num_hiddens, flatten): return nn.Sequential(*net) ``` +```{.python .input} +#@tab paddle +def mlp(num_inputs, num_hiddens, flatten): + net = [] + net.append(nn.Dropout(0.2)) + net.append(nn.Linear(num_inputs, num_hiddens)) + net.append(nn.ReLU()) + if flatten: + net.append(nn.Flatten(start_axis=1)) + net.append(nn.Dropout(0.2)) + net.append(nn.Linear(num_hiddens, num_hiddens)) + net.append(nn.ReLU()) + if flatten: + net.append(nn.Flatten(start_axis=1)) + return nn.Sequential(*net) +``` + 值得注意的是,在 :eqref:`eq_nli_e`中,$f$分别输入$\mathbf{a}_i$和$\mathbf{b}_j$,而不是将它们一对放在一起作为输入。这种*分解*技巧导致$f$只有$m + n$个次计算(线性复杂度),而不是$mn$次计算(二次复杂度) 对 :eqref:`eq_nli_e`中的注意力权重进行规范化,我们计算假设中所有词元向量的加权平均值,以获得假设的表示,该假设与前提中索引$i$的词元进行软对齐: @@ -130,9 +157,32 @@ class Attend(nn.Module): return beta, alpha ``` +```{.python .input} +#@tab paddle +class Attend(nn.Layer): + def __init__(self, num_inputs, num_hiddens, **kwargs): + super(Attend, self).__init__(**kwargs) + self.f = mlp(num_inputs, num_hiddens, flatten=False) + + def forward(self, A, B): + # A/B的形状:(批量大小,序列A/B的词元数,embed_size) + # f_A/f_B的形状:(批量大小,序列A/B的词元数,num_hiddens) + f_A = self.f(A) + f_B = self.f(B) + # e的形状:(批量大小,序列A的词元数,序列B的词元数) + e = paddle.bmm(f_A, f_B.transpose([0, 2, 1])) + # beta的形状:(批量大小,序列A的词元数,embed_size), + # 意味着序列B被软对齐到序列A的每个词元(beta的第1个维度) + beta = paddle.bmm(F.softmax(e, axis=-1), B) + # beta的形状:(批量大小,序列B的词元数,embed_size), + # 意味着序列A被软对齐到序列B的每个词元(alpha的第1个维度) + alpha = paddle.bmm(F.softmax(e.transpose([0, 2, 1]), axis=-1), A) + return beta, alpha +``` + ### 比较 -在下一步中,我们将一个序列中的词元与与该词元软对齐的另一个序列进行比较。请注意,在软对齐中,一个序列中的所有词元(尽管可能具有不同的注意力权重)将与另一个序列中的词元进行比较。为便于演示, :numref:`fig_nli_attention`对词元以*硬*的方式对齐。例如,上述的“注意”(attending)步骤确定前提中的“need”和“sleep”都与假设中的“tired”对齐,则将对“疲倦-需要睡眠”进行比较。 +在下一步中,我们将一个序列中的词元与与该词元软对齐的另一个序列进行比较。请注意,在软对齐中,一个序列中的所有词元(尽管可能具有不同的注意力权重)将与另一个序列中的词元进行比较。为便于演示, :numref:`fig_nli_attention`对词元以*硬*的方式对齐。例如,上述的*注意*(attending)步骤确定前提中的“need”和“sleep”都与假设中的“tired”对齐,则将对“疲倦-需要睡眠”进行比较。 在比较步骤中,我们将来自一个序列的词元的连结(运算符$[\cdot, \cdot]$)和来自另一序列的对齐的词元送入函数$g$(一个多层感知机): @@ -167,6 +217,19 @@ class Compare(nn.Module): return V_A, V_B ``` +```{.python .input} +#@tab paddle +class Compare(nn.Layer): + def __init__(self, num_inputs, num_hiddens, **kwargs): + super(Compare, self).__init__(**kwargs) + self.g = mlp(num_inputs, num_hiddens, flatten=False) + + def forward(self, A, B, beta, alpha): + V_A = self.g(paddle.concat([A, beta], axis=2)) + V_B = self.g(paddle.concat([B, alpha], axis=2)) + return V_A, V_B +``` + ### 聚合 现在我们有两组比较向量$\mathbf{v}_{A,i}$($i = 1, \ldots, m$)和$\mathbf{v}_{B,j}$($j = 1, \ldots, n$)。在最后一步中,我们将聚合这些信息以推断逻辑关系。我们首先求和这两组比较向量: @@ -216,6 +279,23 @@ class Aggregate(nn.Module): return Y_hat ``` +```{.python .input} +#@tab paddle +class Aggregate(nn.Layer): + def __init__(self, num_inputs, num_hiddens, num_outputs, **kwargs): + super(Aggregate, self).__init__(**kwargs) + self.h = mlp(num_inputs, num_hiddens, flatten=True) + self.linear = nn.Linear(num_hiddens, num_outputs) + + def forward(self, V_A, V_B): + # 对两组比较向量分别求和 + V_A = V_A.sum(axis=1) + V_B = V_B.sum(axis=1) + # 将两个求和结果的连结送到多层感知机中 + Y_hat = self.linear(self.h(paddle.concat([V_A, V_B], axis=1))) + return Y_hat +``` + ### 整合代码 通过将注意步骤、比较步骤和聚合步骤组合在一起,我们定义了可分解注意力模型来联合训练这三个步骤。 @@ -262,6 +342,28 @@ class DecomposableAttention(nn.Module): return Y_hat ``` +```{.python .input} +#@tab paddle +class DecomposableAttention(nn.Layer): + def __init__(self, vocab, embed_size, num_hiddens, num_inputs_attend=100, + num_inputs_compare=200, num_inputs_agg=400, **kwargs): + super(DecomposableAttention, self).__init__(**kwargs) + self.embedding = nn.Embedding(len(vocab), embed_size) + self.attend = Attend(num_inputs_attend, num_hiddens) + self.compare = Compare(num_inputs_compare, num_hiddens) + # 有3种可能的输出:蕴涵、矛盾和中性 + self.aggregate = Aggregate(num_inputs_agg, num_hiddens, num_outputs=3) + + def forward(self, X): + premises, hypotheses = X + A = self.embedding(premises) + B = self.embedding(hypotheses) + beta, alpha = self.attend(A, B) + V_A, V_B = self.compare(A, B, beta, alpha) + Y_hat = self.aggregate(V_A, V_B) + return Y_hat +``` + ## 训练和评估模型 现在,我们将在SNLI数据集上对定义好的可分解注意力模型进行训练和评估。我们从读取数据集开始。 @@ -271,11 +373,37 @@ class DecomposableAttention(nn.Module): 我们使用 :numref:`sec_natural-language-inference-and-dataset`中定义的函数下载并读取SNLI数据集。批量大小和序列长度分别设置为$256$和$50$。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch batch_size, num_steps = 256, 50 train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps) ``` +```{.python .input} +#@tab paddle +def load_data_snli(batch_size, num_steps=50): + """下载SNLI数据集并返回数据迭代器和词表 + + Defined in :numref:`sec_natural-language-inference-and-dataset`""" + data_dir = d2l.download_extract('SNLI') + train_data = d2l.read_snli(data_dir, True) + test_data = d2l.read_snli(data_dir, False) + train_set = d2l.SNLIDataset(train_data, num_steps) + test_set = d2l.SNLIDataset(test_data, num_steps, train_set.vocab) + train_iter = paddle.io.DataLoader(train_set,batch_size=batch_size, + shuffle=True, + num_workers=0, + return_list=True) + + test_iter = paddle.io.DataLoader(test_set, batch_size=batch_size, + shuffle=False, + num_workers=0, + return_list=True) + return train_iter, test_iter, train_set.vocab + +batch_size, num_steps = 256, 50 +train_iter, test_iter, vocab = load_data_snli(batch_size, num_steps) +``` + ### 创建模型 我们使用预训练好的100维GloVe嵌入来表示输入词元。我们将向量$\mathbf{a}_i$和$\mathbf{b}_j$在 :eqref:`eq_nli_e`中的维数预定义为100。 :eqref:`eq_nli_e`中的函数$f$和 :eqref:`eq_nli_v_ab`中的函数$g$的输出维度被设置为200.然后我们创建一个模型实例,初始化它的参数,并加载GloVe嵌入来初始化输入词元的向量。 @@ -298,6 +426,15 @@ embeds = glove_embedding[vocab.idx_to_token] net.embedding.weight.data.copy_(embeds); ``` +```{.python .input} +#@tab paddle +embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus() +net = DecomposableAttention(vocab, embed_size, num_hiddens) +glove_embedding = d2l.TokenEmbedding('glove.6b.100d') +embeds = glove_embedding[vocab.idx_to_token] +net.embedding.weight.set_value(embeds); +``` + ### 训练和评估模型 与 :numref:`sec_multi_gpu`中接受单一输入(如文本序列或图像)的`split_batch`函数不同,我们定义了一个`split_batch_multi_inputs`函数以小批量接受多个输入,如前提和假设。 @@ -330,6 +467,15 @@ d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) ``` +```{.python .input} +#@tab paddle +lr, num_epochs = 0.001, 4 +trainer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) +loss = nn.CrossEntropyLoss(reduction="none") +d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices[:1]) +``` + ### 使用模型 最后,定义预测函数,输出一对前提和假设之间的逻辑关系。 @@ -360,6 +506,21 @@ def predict_snli(net, vocab, premise, hypothesis): else 'neutral' ``` +```{.python .input} +#@tab paddle +#@save +def predict_snli(net, vocab, premise, hypothesis): + """预测前提和假设之间的逻辑关系""" + net.eval() + premise = paddle.to_tensor(vocab[premise], place=d2l.try_gpu()) + hypothesis = paddle.to_tensor(vocab[hypothesis], place=d2l.try_gpu()) + label = paddle.argmax(net([premise.reshape((1, -1)), + hypothesis.reshape((1, -1))]), axis=1) + + return 'entailment' if label == 0 else 'contradiction' if label == 1 \ + else 'neutral' +``` + 我们可以使用训练好的模型来获得对示例句子的自然语言推断结果。 ```{.python .input} @@ -376,9 +537,9 @@ predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.']) ## 练习 -1. 使用其他超参数组合训练模型。你能在测试集上获得更高的准确度吗? +1. 使用其他超参数组合训练模型,能在测试集上获得更高的准确度吗? 1. 自然语言推断的可分解注意模型的主要缺点是什么? -1. 假设我们想要获得任何一对句子的语义相似级别(例如,0到1之间的连续值)。我们应该如何收集和标注数据集?你能设计一个有注意力机制的模型吗? +1. 假设我们想要获得任何一对句子的语义相似级别(例如,0~1之间的连续值)。我们应该如何收集和标注数据集?请尝试设计一个有注意力机制的模型。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/5727) @@ -387,3 +548,7 @@ predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.']) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5728) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11829) +:end_tab: diff --git a/chapter_natural-language-processing-applications/natural-language-inference-bert.md b/chapter_natural-language-processing-applications/natural-language-inference-bert.md index 835a9e889..5c3b3d3f1 100644 --- a/chapter_natural-language-processing-applications/natural-language-inference-bert.md +++ b/chapter_natural-language-processing-applications/natural-language-inference-bert.md @@ -6,7 +6,7 @@ ![将预训练BERT提供给基于多层感知机的自然语言推断架构](../img/nlp-map-nli-bert.svg) :label:`fig_nlp-map-nli-bert` -在本节中,我们将下载一个预训练好的小版本的BERT,然后对其进行微调,以便在SNLI数据集上进行自然语言推断。 +本节将下载一个预训练好的小版本的BERT,然后对其进行微调,以便在SNLI数据集上进行自然语言推断。 ```{.python .input} from d2l import mxnet as d2l @@ -29,6 +29,18 @@ from torch import nn import os ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import json +import multiprocessing +import paddle +from paddle import nn +import os +``` + ## [**加载预训练的BERT**] 我们已经在 :numref:`sec_bert-dataset`和 :numref:`sec_bert-pretraining`WikiText-2数据集上预训练BERT(请注意,原始的BERT模型是在更大的语料库上预训练的)。正如在 :numref:`sec_bert-pretraining`中所讨论的,原始的BERT模型有数以亿计的参数。在下面,我们提供了两个版本的预训练的BERT:“bert.base”与原始的BERT基础模型一样大,需要大量的计算资源才能进行微调,而“bert.small”是一个小版本,以便于演示。 @@ -48,6 +60,13 @@ d2l.DATA_HUB['bert.small'] = (d2l.DATA_URL + 'bert.small.torch.zip', 'c72329e68a732bef0452e4b96a1c341c8910f81f') ``` +```{.python .input} +#@tab paddle +d2l.DATA_HUB['bert_small'] = ('https://paddlenlp.bj.bcebos.com/models/bert.small.paddle.zip', '9fcde07509c7e87ec61c640c1b277509c7e87ec6153d9041758e4') + +d2l.DATA_HUB['bert_base'] = ('https://paddlenlp.bj.bcebos.com/models/bert.base.paddle.zip', '9fcde07509c7e87ec61c640c1b27509c7e87ec61753d9041758e4') +``` + 两个预训练好的BERT模型都包含一个定义词表的“vocab.json”文件和一个预训练参数的“pretrained.params”文件。我们实现了以下`load_pretrained_model`函数来[**加载预先训练好的BERT参数**]。 ```{.python .input} @@ -91,16 +110,48 @@ def load_pretrained_model(pretrained_model, num_hiddens, ffn_num_hiddens, return bert, vocab ``` +```{.python .input} +#@tab paddle +def load_pretrained_model(pretrained_model, num_hiddens, ffn_num_hiddens, + num_heads, num_layers, dropout, max_len, devices): + data_dir = d2l.download_extract(pretrained_model) + # 定义空词表以加载预定义词表 + vocab = d2l.Vocab() + vocab.idx_to_token = json.load(open(os.path.join(data_dir, + 'vocab.json'))) + vocab.token_to_idx = {token: idx for idx, token in enumerate( + vocab.idx_to_token)} + bert = d2l.BERTModel(len(vocab), num_hiddens, norm_shape=[256], + ffn_num_input=256, ffn_num_hiddens=ffn_num_hiddens, + num_heads=4, num_layers=2, dropout=0.2, + max_len=max_len, key_size=256, query_size=256, + value_size=256, hid_in_features=256, + mlm_in_features=256, nsp_in_features=256) + # 加载预训练BERT参数 + bert.set_state_dict(paddle.load(os.path.join(data_dir, + 'pretrained.pdparams'))) + + return bert, vocab +``` + 为了便于在大多数机器上演示,我们将在本节中加载和微调经过预训练BERT的小版本(“bert.small”)。在练习中,我们将展示如何微调大得多的“bert.base”以显著提高测试精度。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch devices = d2l.try_all_gpus() bert, vocab = load_pretrained_model( 'bert.small', num_hiddens=256, ffn_num_hiddens=512, num_heads=4, num_layers=2, dropout=0.1, max_len=512, devices=devices) ``` +```{.python .input} +#@tab paddle +devices = d2l.try_all_gpus() +bert, vocab = load_pretrained_model( + 'bert_small', num_hiddens=256, ffn_num_hiddens=512, num_heads=4, + num_layers=2, dropout=0.1, max_len=512, devices=devices) +``` + ## [**微调BERT的数据集**] 对于SNLI数据集的下游任务自然语言推断,我们定义了一个定制的数据集类`SNLIBERTDataset`。在每个样本中,前提和假设形成一对文本序列,并被打包成一个BERT输入序列,如 :numref:`fig_bert-two-seqs`所示。回想 :numref:`subsec_bert_input_rep`,片段索引用于区分BERT输入序列中的前提和假设。利用预定义的BERT输入序列的最大长度(`max_len`),持续移除输入文本对中较长文本的最后一个标记,直到满足`max_len`。为了加速生成用于微调BERT的SNLI数据集,我们使用4个工作进程并行生成训练或测试样本。 @@ -210,6 +261,64 @@ class SNLIBERTDataset(torch.utils.data.Dataset): return len(self.all_token_ids) ``` +```{.python .input} +#@tab paddle +class SNLIBERTDataset(paddle.io.Dataset): + def __init__(self, dataset, max_len, vocab=None): + all_premise_hypothesis_tokens = [[ + p_tokens, h_tokens] for p_tokens, h_tokens in zip( + *[d2l.tokenize([s.lower() for s in sentences]) + for sentences in dataset[:2]])] + + self.labels = paddle.to_tensor(dataset[2]) + self.vocab = vocab + self.max_len = max_len + (self.all_token_ids, self.all_segments, + self.valid_lens) = self._preprocess(all_premise_hypothesis_tokens) + print('read ' + str(len(self.all_token_ids)) + ' examples') + + def _preprocess(self, all_premise_hypothesis_tokens): + # pool = multiprocessing.Pool(1) # 使用4个进程 + # out = pool.map(self._mp_worker, all_premise_hypothesis_tokens) + out = [] + for i in all_premise_hypothesis_tokens: + tempOut = self._mp_worker(i) + out.append(tempOut) + + all_token_ids = [ + token_ids for token_ids, segments, valid_len in out] + all_segments = [segments for token_ids, segments, valid_len in out] + valid_lens = [valid_len for token_ids, segments, valid_len in out] + return (paddle.to_tensor(all_token_ids, dtype='int64'), + paddle.to_tensor(all_segments, dtype='int64'), + paddle.to_tensor(valid_lens)) + + def _mp_worker(self, premise_hypothesis_tokens): + p_tokens, h_tokens = premise_hypothesis_tokens + self._truncate_pair_of_tokens(p_tokens, h_tokens) + tokens, segments = d2l.get_tokens_and_segments(p_tokens, h_tokens) + token_ids = self.vocab[tokens] + [self.vocab['']] \ + * (self.max_len - len(tokens)) + segments = segments + [0] * (self.max_len - len(segments)) + valid_len = len(tokens) + return token_ids, segments, valid_len + + def _truncate_pair_of_tokens(self, p_tokens, h_tokens): + # 为BERT输入中的''、''和''词元保留位置 + while len(p_tokens) + len(h_tokens) > self.max_len - 3: + if len(p_tokens) > len(h_tokens): + p_tokens.pop() + else: + h_tokens.pop() + + def __getitem__(self, idx): + return (self.all_token_ids[idx], self.all_segments[idx], + self.valid_lens[idx]), self.labels[idx] + + def __len__(self): + return len(self.all_token_ids) +``` + 下载完SNLI数据集后,我们通过实例化`SNLIBERTDataset`类来[**生成训练和测试样本**]。这些样本将在自然语言推断的训练和测试期间进行小批量读取。 ```{.python .input} @@ -237,6 +346,17 @@ test_iter = torch.utils.data.DataLoader(test_set, batch_size, num_workers=num_workers) ``` +```{.python .input} +#@tab paddle +# 如果出现显存不足错误,请减少“batch_size”。在原始的BERT模型中,max_len=512 +batch_size, max_len, num_workers = 512, 128, d2l.get_dataloader_workers() +data_dir = d2l.download_extract('SNLI') +train_set = SNLIBERTDataset(d2l.read_snli(data_dir, True), max_len, vocab) +test_set = SNLIBERTDataset(d2l.read_snli(data_dir, False), max_len, vocab) +train_iter = paddle.io.DataLoader(train_set, batch_size=batch_size, shuffle=True, return_list=True) +test_iter = paddle.io.DataLoader(test_set, batch_size=batch_size, return_list=True) +``` + ## 微调BERT 如 :numref:`fig_bert-two-seqs`所示,用于自然语言推断的微调BERT只需要一个额外的多层感知机,该多层感知机由两个全连接层组成(请参见下面`BERTClassifier`类中的`self.hidden`和`self.output`)。[**这个多层感知机将特殊的“<cls>”词元**]的BERT表示进行了转换,该词元同时编码前提和假设的信息(**为自然语言推断的三个输出**):蕴涵、矛盾和中性。 @@ -270,6 +390,21 @@ class BERTClassifier(nn.Module): return self.output(self.hidden(encoded_X[:, 0, :])) ``` +```{.python .input} +#@tab paddle +class BERTClassifier(nn.Layer): + def __init__(self, bert): + super(BERTClassifier, self).__init__() + self.encoder = bert.encoder + self.hidden = bert.hidden + self.output = nn.Linear(256, 3) + + def forward(self, inputs): + tokens_X, segments_X, valid_lens_x = inputs + encoded_X = self.encoder(tokens_X, segments_X, valid_lens_x.squeeze(1)) + return self.output(self.hidden(encoded_X[:, 0, :])) +``` + 在下文中,预训练的BERT模型`bert`被送到用于下游应用的`BERTClassifier`实例`net`中。在BERT微调的常见实现中,只有额外的多层感知机(`net.output`)的输出层的参数将从零开始学习。预训练BERT编码器(`net.encoder`)和额外的多层感知机的隐藏层(`net.hidden`)的所有参数都将进行微调。 ```{.python .input} @@ -278,7 +413,7 @@ net.output.initialize(ctx=devices) ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net = BERTClassifier(bert) ``` @@ -303,6 +438,15 @@ d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) ``` +```{.python .input} +#@tab paddle +lr, num_epochs = 1e-4, 5 +trainer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) +loss = nn.CrossEntropyLoss(reduction='none') +d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices) +``` + ## 小结 * 我们可以针对下游应用对预训练的BERT模型进行微调,例如在SNLI数据集上进行自然语言推断。 @@ -320,3 +464,7 @@ d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5718) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11831) +:end_tab: diff --git a/chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md b/chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md index e3c393600..6fed6731d 100644 --- a/chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md +++ b/chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md @@ -24,6 +24,16 @@ from torch import nn import os ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +import os +``` + ## 读取数据集 首先,下载并提取路径`../data/aclImdb`中的IMDb评论数据集。 @@ -116,6 +126,17 @@ for X, y in train_iter: print('小批量数目:', len(train_iter)) ``` +```{.python .input} +#@tab paddle +train_iter = d2l.load_array((train_features, + d2l.tensor(train_data[1])), 64) + +for X, y in train_iter: + print('X:', X.shape, ', y:', y.shape) + break +print('小批量数目:', len(train_iter)) +``` + ## 整合代码 最后,我们将上述步骤封装到`load_data_imdb`函数中。它返回训练和测试数据迭代器以及IMDb评论数据集的词表。 @@ -163,6 +184,29 @@ def load_data_imdb(batch_size, num_steps=500): return train_iter, test_iter, vocab ``` +```{.python .input} +#@tab paddle +#@save +def load_data_imdb(batch_size, num_steps=500): + """返回数据迭代器和IMDb评论数据集的词表""" + data_dir = d2l.download_extract('aclImdb', 'aclImdb') + train_data = read_imdb(data_dir, True) + test_data = read_imdb(data_dir, False) + train_tokens = d2l.tokenize(train_data[0], token='word') + test_tokens = d2l.tokenize(test_data[0], token='word') + vocab = d2l.Vocab(train_tokens, min_freq=5) + train_features = d2l.tensor([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in train_tokens]) + test_features = d2l.tensor([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in test_tokens]) + train_iter = d2l.load_array((train_features, d2l.tensor(train_data[1])), + batch_size) + test_iter = d2l.load_array((test_features, d2l.tensor(test_data[1])), + batch_size, + is_train=False) + return train_iter, test_iter, vocab +``` + ## 小结 * 情感分析研究人们在文本中的情感,这被认为是一个文本分类问题,它将可变长度的文本序列进行转换转换为固定长度的文本类别。 @@ -171,7 +215,7 @@ def load_data_imdb(batch_size, num_steps=500): ## 练习 1. 我们可以修改本节中的哪些超参数来加速训练情感分析模型? -1. 你能实现一个函数来将[Amazon reviews](https://snap.stanford.edu/data/web-Amazon.html)的数据集加载到数据迭代器中进行情感分析吗? +1. 请实现一个函数来将[Amazon reviews](https://snap.stanford.edu/data/web-Amazon.html)的数据集加载到数据迭代器中进行情感分析。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/5725) @@ -180,3 +224,7 @@ def load_data_imdb(batch_size, num_steps=500): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5726) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11825) +:end_tab: diff --git a/chapter_natural-language-processing-applications/sentiment-analysis-cnn.md b/chapter_natural-language-processing-applications/sentiment-analysis-cnn.md index 427f78f05..7b92cf1b5 100644 --- a/chapter_natural-language-processing-applications/sentiment-analysis-cnn.md +++ b/chapter_natural-language-processing-applications/sentiment-analysis-cnn.md @@ -3,7 +3,7 @@ 在 :numref:`chap_cnn`中,我们探讨了使用二维卷积神经网络处理二维图像数据的机制,并将其应用于局部特征,如相邻像素。虽然卷积神经网络最初是为计算机视觉设计的,但它也被广泛用于自然语言处理。简单地说,只要将任何文本序列想象成一维图像即可。通过这种方式,一维卷积神经网络可以处理文本中的局部特征,例如$n$元语法。 -在本节中,我们将使用*textCNN*模型来演示如何设计一个表示单个文本 :cite:`Kim.2014`的卷积神经网络架构。与 :numref:`fig_nlp-map-sa-rnn`中使用带有GloVe预训练的循环神经网络架构进行情感分析相比, :numref:`fig_nlp-map-sa-cnn`中唯一的区别在于架构的选择。 +本节将使用*textCNN*模型来演示如何设计一个表示单个文本 :cite:`Kim.2014`的卷积神经网络架构。与 :numref:`fig_nlp-map-sa-rnn`中使用带有GloVe预训练的循环神经网络架构进行情感分析相比, :numref:`fig_nlp-map-sa-cnn`中唯一的区别在于架构的选择。 ![将GloVe放入卷积神经网络架构进行情感分析](../img/nlp-map-sa-cnn.svg) :label:`fig_nlp-map-sa-cnn` @@ -28,6 +28,18 @@ batch_size = 64 train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn + +batch_size = 64 +train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size) +``` + ## 一维卷积 在介绍该模型之前,让我们先看看一维卷积是如何工作的。请记住,这只是基于互相关运算的二维卷积的特例。 @@ -40,7 +52,7 @@ train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size) 我们在下面的`corr1d`函数中实现了一维互相关。给定输入张量`X`和核张量`K`,它返回输出张量`Y`。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch def corr1d(X, K): w = K.shape[0] Y = d2l.zeros((X.shape[0] - w + 1)) @@ -49,6 +61,16 @@ def corr1d(X, K): return Y ``` +```{.python .input} +#@tab paddle +def corr1d(X, K): + w = K.shape[0] + Y = d2l.zeros([X.shape[0] - w + 1], dtype=X.dtype) + for i in range(Y.shape[0]): + Y[i] = (X[i: i + w] * K).sum() + return Y +``` + 我们可以从 :numref:`fig_conv1d`构造输入张量`X`和核张量`K`来验证上述一维互相关实现的输出。 ```{.python .input} @@ -175,6 +197,41 @@ class TextCNN(nn.Module): return outputs ``` +```{.python .input} +#@tab paddle +class TextCNN(nn.Layer): + def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels, + **kwargs): + super(TextCNN, self).__init__(**kwargs) + self.embedding = nn.Embedding(vocab_size, embed_size) + # 这个嵌入层不需要训练 + self.constant_embedding = nn.Embedding(vocab_size, embed_size) + self.dropout = nn.Dropout(0.5) + self.decoder = nn.Linear(sum(num_channels), 2) + # 最大时间汇聚层没有参数,因此可以共享此实例 + self.pool = nn.AdaptiveAvgPool1D(1) + self.relu = nn.ReLU() + # 创建多个一维卷积层 + self.convs = nn.LayerList() + for c, k in zip(num_channels, kernel_sizes): + self.convs.append(nn.Conv1D(2 * embed_size, c, k)) + + def forward(self, inputs): + # 沿着向量维度将两个嵌入层连结起来, + # 每个嵌入层的输出形状都是(批量大小,词元数量,词元向量维度)连结起来 + embeddings = paddle.concat(( + self.embedding(inputs), self.constant_embedding(inputs)), axis=2) + # 根据一维卷积层的输入格式,重新排列张量,以便通道作为第2维 + embeddings = embeddings.transpose([0, 2, 1]) + # 每个一维卷积层在最大时间汇聚层合并后,获得的张量形状是(批量大小,通道数,1) + # 删除最后一个维度并沿通道维度连结 + encoding = paddle.concat([ + paddle.squeeze(self.relu(self.pool(conv(embeddings))), axis=-1) + for conv in self.convs], axis=1) + outputs = self.decoder(self.dropout(encoding)) + return outputs +``` + 让我们创建一个textCNN实例。它有3个卷积层,卷积核宽度分别为3、4和5,均有100个输出通道。 ```{.python .input} @@ -197,6 +254,21 @@ def init_weights(m): net.apply(init_weights); ``` +```{.python .input} +#@tab paddle +embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100] +devices = d2l.try_all_gpus() +net = TextCNN(len(vocab), embed_size, kernel_sizes, nums_channels) + +def init_weights(net): + init_normal = nn.initializer.XavierUniform() + for i in net.sublayers(): + if type(i) in [nn.Linear, nn.Conv1D]: + init_normal(i.weight) + +init_weights(net) +``` + ### 加载预训练词向量 与 :numref:`sec_sentiment_rnn`相同,我们加载预训练的100维GloVe嵌入作为初始化的词元表示。这些词元表示(嵌入权重)在`embedding`中将被训练,在`constant_embedding`中将被固定。 @@ -218,6 +290,15 @@ net.constant_embedding.weight.data.copy_(embeds) net.constant_embedding.weight.requires_grad = False ``` +```{.python .input} +#@tab paddle +glove_embedding = d2l.TokenEmbedding('glove.6b.100d') +embeds = glove_embedding[vocab.idx_to_token] +net.embedding.weight.set_value(embeds) +net.constant_embedding.weight.set_value(embeds) +net.constant_embedding.weight.stop_gradient = True +``` + ### 训练和评估模型 现在我们可以训练textCNN模型进行情感分析。 @@ -237,6 +318,14 @@ loss = nn.CrossEntropyLoss(reduction="none") d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) ``` +```{.python .input} +#@tab paddle +lr, num_epochs = 0.001, 5 +trainer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) +loss = nn.CrossEntropyLoss(reduction="none") +d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) +``` + 下面,我们使用训练好的模型来预测两个简单句子的情感。 ```{.python .input} @@ -259,7 +348,7 @@ d2l.predict_sentiment(net, vocab, 'this movie is so bad') ## 练习 1. 调整超参数,并比较 :numref:`sec_sentiment_rnn`中用于情感分析的架构和本节中用于情感分析的架构,例如在分类精度和计算效率方面。 -1. 你能不能用 :numref:`sec_sentiment_rnn`练习中介绍的方法进一步提高模型的分类精度? +1. 请试着用 :numref:`sec_sentiment_rnn`练习中介绍的方法进一步提高模型的分类精度。 1. 在输入表示中添加位置编码。它是否提高了分类的精度? :begin_tab:`mxnet` @@ -269,3 +358,7 @@ d2l.predict_sentiment(net, vocab, 'this movie is so bad') :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5720) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11827) +:end_tab: diff --git a/chapter_natural-language-processing-applications/sentiment-analysis-rnn.md b/chapter_natural-language-processing-applications/sentiment-analysis-rnn.md index c6f872575..c4c6485a4 100644 --- a/chapter_natural-language-processing-applications/sentiment-analysis-rnn.md +++ b/chapter_natural-language-processing-applications/sentiment-analysis-rnn.md @@ -26,6 +26,18 @@ batch_size = 64 train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn + +batch_size = 64 +train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size) +``` + ## 使用循环神经网络表示单个文本 在文本分类任务(如情感分析)中,可变长度的文本序列将被转换为固定长度的类别。在下面的`BiRNN`类中,虽然文本序列的每个词元经由嵌入层(`self.embedding`)获得其单独的预训练GloVe表示,但是整个序列由双向循环神经网络(`self.encoder`)编码。更具体地说,双向长短期记忆网络在初始和最终时间步的隐状态(在最后一层)被连结起来作为文本序列的表示。然后,通过一个具有两个输出(“积极”和“消极”)的全连接层(`self.decoder`),将此单一文本表示转换为输出类别。 @@ -86,6 +98,35 @@ class BiRNN(nn.Module): return outs ``` +```{.python .input} +#@tab paddle +class BiRNN(nn.Layer): + def __init__(self, vocab_size, embed_size, num_hiddens, + num_layers, **kwargs): + super(BiRNN, self).__init__(**kwargs) + self.embedding = nn.Embedding(vocab_size, embed_size) + # 将direction设置为'bidirect'或'bidirectional'以获取双向循环神经网络 + self.encoder = nn.LSTM(embed_size, num_hiddens, num_layers=num_layers, + direction='bidirect',time_major=True) + self.decoder = nn.Linear(4 * num_hiddens, 2) + + def forward(self, inputs): + # inputs的形状是(批量大小,时间步数) + # 因为长短期记忆网络要求其输入的第一个维度是时间维, + # 所以在获得词元表示之前,输入会被转置。 + # 输出形状为(时间步数,批量大小,词向量维度) + embeddings = self.embedding(inputs.T) + self.encoder.flatten_parameters() + # 返回上一个隐藏层在不同时间步的隐状态, + # outputs的形状是(时间步数,批量大小,2*隐藏单元数) + outputs, _ = self.encoder(embeddings) + # 连结初始和最终时间步的隐状态,作为全连接层的输入, + # 其形状为(批量大小,4*隐藏单元数) + encoding = paddle.concat((outputs[0], outputs[-1]), axis=1) + outs = self.decoder(encoding) + return outs +``` + 让我们构造一个具有两个隐藏层的双向循环神经网络来表示单个文本以进行情感分析。 ```{.python .input} @@ -111,6 +152,19 @@ def init_weights(m): net.apply(init_weights); ``` +```{.python .input} +#@tab paddle +def init_weights(layer): + if isinstance(layer,(nn.Linear, nn.Embedding)): + if isinstance(layer.weight, paddle.Tensor): + nn.initializer.XavierUniform()(layer.weight) + if isinstance(layer, nn.LSTM): + for n, p in layer.named_parameters(): + if "weigth" in n: + nn.initializer.XavierUniform()(p) +net.apply(init_weights) +``` + ## 加载预训练的词向量 下面,我们为词表中的单词加载预训练的100维(需要与`embed_size`一致)的GloVe嵌入。 @@ -141,6 +195,12 @@ net.embedding.weight.data.copy_(embeds) net.embedding.weight.requires_grad = False ``` +```{.python .input} +#@tab paddle +net.embedding.weight.set_value(embeds) +net.embedding.weight.stop_gradient = False +``` + ## 训练和评估模型 现在我们可以训练双向循环神经网络进行情感分析。 @@ -162,6 +222,15 @@ d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) ``` +```{.python .input} +#@tab paddle +lr, num_epochs = 0.01, 2 +trainer = paddle.optimizer.Adam(learning_rate=lr,parameters=net.parameters()) +loss = nn.CrossEntropyLoss(reduction="none") +d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices) +``` + 我们定义以下函数来使用训练好的模型`net`预测文本序列的情感。 ```{.python .input} @@ -183,6 +252,16 @@ def predict_sentiment(net, vocab, sequence): return 'positive' if label == 1 else 'negative' ``` +```{.python .input} +#@tab paddle +#@save +def predict_sentiment(net, vocab, sequence): + """预测文本序列的情感""" + sequence = paddle.to_tensor(vocab[sequence.split()], place=d2l.try_gpu()) + label = paddle.argmax(net(sequence.reshape((1, -1))), axis=1) + return 'positive' if label == 1 else 'negative' +``` + 最后,让我们使用训练好的模型对两个简单的句子进行情感预测。 ```{.python .input} @@ -202,9 +281,9 @@ predict_sentiment(net, vocab, 'this movie is so bad') ## 练习 -1. 增加迭代轮数。你能提高训练和测试的准确性吗?调优其他超参数怎么样? +1. 增加迭代轮数可以提高训练和测试的准确性吗?调优其他超参数怎么样? 1. 使用较大的预训练词向量,例如300维的GloVe嵌入。它是否提高了分类精度? -1. 是否可以通过spaCy词元化来提高分类精度?你需要安装Spacy(`pip install spacy`)和英语语言包(`python -m spacy download en`)。在代码中,首先导入Spacy(`import spacy`)。然后,加载Spacy英语软件包(`spacy_en = spacy.load('en')`)。最后,定义函数`def tokenizer(text): return [tok.text for tok in spacy_en.tokenizer(text)]`并替换原来的`tokenizer`函数。请注意GloVe和spaCy中短语标记的不同形式。例如,短语标记“new york”在GloVe中的形式是“new-york”,而在spaCy词元化之后的形式是“new york”。 +1. 是否可以通过spaCy词元化来提高分类精度?需要安装Spacy(`pip install spacy`)和英语语言包(`python -m spacy download en`)。在代码中,首先导入Spacy(`import spacy`)。然后,加载Spacy英语软件包(`spacy_en = spacy.load('en')`)。最后,定义函数`def tokenizer(text): return [tok.text for tok in spacy_en.tokenizer(text)]`并替换原来的`tokenizer`函数。请注意GloVe和spaCy中短语标记的不同形式。例如,短语标记“new york”在GloVe中的形式是“new-york”,而在spaCy词元化之后的形式是“new york”。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/5723) @@ -213,3 +292,7 @@ predict_sentiment(net, vocab, 'this movie is so bad') :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5724) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11826) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/bert-dataset.md b/chapter_natural-language-processing-pretraining/bert-dataset.md index e657666bc..d6ce8b6cc 100644 --- a/chapter_natural-language-processing-pretraining/bert-dataset.md +++ b/chapter_natural-language-processing-pretraining/bert-dataset.md @@ -22,6 +22,16 @@ import random import torch ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import os +import random +import paddle +``` + 在WikiText-2数据集中,每行代表一个段落,其中在任意标点符号及其前面的词元之间插入空格。保留至少有两句话的段落。为了简单起见,我们仅使用句号作为分隔符来拆分句子。我们将更复杂的句子拆分技术的讨论留在本节末尾的练习中。 ```{.python .input} @@ -203,6 +213,36 @@ def _pad_bert_inputs(examples, max_len, vocab): all_mlm_weights, all_mlm_labels, nsp_labels) ``` +```{.python .input} +#@tab paddle +#@save +def _pad_bert_inputs(examples, max_len, vocab): + max_num_mlm_preds = round(max_len * 0.15) + all_token_ids, all_segments, valid_lens, = [], [], [] + all_pred_positions, all_mlm_weights, all_mlm_labels = [], [], [] + nsp_labels = [] + for (token_ids, pred_positions, mlm_pred_label_ids, segments, + is_next) in examples: + all_token_ids.append(paddle.to_tensor(token_ids + [vocab['']] * ( + max_len - len(token_ids)), dtype=paddle.int64)) + all_segments.append(paddle.to_tensor(segments + [0] * ( + max_len - len(segments)), dtype=paddle.int64)) + # valid_lens不包括''的计数 + valid_lens.append(paddle.to_tensor(len(token_ids), dtype=paddle.float32)) + all_pred_positions.append(paddle.to_tensor(pred_positions + [0] * ( + max_num_mlm_preds - len(pred_positions)), dtype=paddle.int64)) + # 填充词元的预测将通过乘以0权重在损失中过滤掉 + all_mlm_weights.append( + paddle.to_tensor([1.0] * len(mlm_pred_label_ids) + [0.0] * ( + max_num_mlm_preds - len(pred_positions)), + dtype=paddle.float32)) + all_mlm_labels.append(paddle.to_tensor(mlm_pred_label_ids + [0] * ( + max_num_mlm_preds - len(mlm_pred_label_ids)), dtype=paddle.int64)) + nsp_labels.append(paddle.to_tensor(is_next, dtype=paddle.int64)) + return (all_token_ids, all_segments, valid_lens, all_pred_positions, + all_mlm_weights, all_mlm_labels, nsp_labels) +``` + 将用于生成两个预训练任务的训练样本的辅助函数和用于填充输入的辅助函数放在一起,我们定义以下`_WikiTextDataset`类为用于预训练BERT的WikiText-2数据集。通过实现`__getitem__ `函数,我们可以任意访问WikiText-2语料库的一对句子生成的预训练样本(遮蔽语言模型和下一句预测)样本。 最初的BERT模型使用词表大小为30000的WordPiece嵌入 :cite:`Wu.Schuster.Chen.ea.2016`。WordPiece的词元化方法是对 :numref:`subsec_Byte_Pair_Encoding`中原有的字节对编码算法稍作修改。为简单起见,我们使用`d2l.tokenize`函数进行词元化。出现次数少于5次的不频繁词元将被过滤掉。 @@ -282,6 +322,44 @@ class _WikiTextDataset(torch.utils.data.Dataset): return len(self.all_token_ids) ``` +```{.python .input} +#@tab paddle +#@save +class _WikiTextDataset(paddle.io.Dataset): + def __init__(self, paragraphs, max_len): + # 输入paragraphs[i]是代表段落的句子字符串列表; + # 而输出paragraphs[i]是代表段落的句子列表,其中每个句子都是词元列表 + paragraphs = [d2l.tokenize( + paragraph, token='word') for paragraph in paragraphs] + sentences = [sentence for paragraph in paragraphs + for sentence in paragraph] + self.vocab = d2l.Vocab(sentences, min_freq=5, reserved_tokens=[ + '', '', '', '']) + # 获取下一句子预测任务的数据 + examples = [] + for paragraph in paragraphs: + examples.extend(_get_nsp_data_from_paragraph( + paragraph, paragraphs, self.vocab, max_len)) + # 获取遮蔽语言模型任务的数据 + examples = [(_get_mlm_data_from_tokens(tokens, self.vocab) + + (segments, is_next)) + for tokens, segments, is_next in examples] + # 填充输入 + (self.all_token_ids, self.all_segments, self.valid_lens, + self.all_pred_positions, self.all_mlm_weights, + self.all_mlm_labels, self.nsp_labels) = _pad_bert_inputs( + examples, max_len, self.vocab) + + def __getitem__(self, idx): + return (self.all_token_ids[idx], self.all_segments[idx], + self.valid_lens[idx], self.all_pred_positions[idx], + self.all_mlm_weights[idx], self.all_mlm_labels[idx], + self.nsp_labels[idx]) + + def __len__(self): + return len(self.all_token_ids) +``` + 通过使用`_read_wiki`函数和`_WikiTextDataset`类,我们定义了下面的`load_data_wiki`来下载并生成WikiText-2数据集,并从中生成预训练样本。 ```{.python .input} @@ -311,6 +389,20 @@ def load_data_wiki(batch_size, max_len): return train_iter, train_set.vocab ``` +```{.python .input} +#@tab paddle +#@save +def load_data_wiki(batch_size, max_len): + """加载WikiText-2数据集""" + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('wikitext-2', 'wikitext-2') + paragraphs = _read_wiki(data_dir) + train_set = _WikiTextDataset(paragraphs, max_len) + train_iter = paddle.io.DataLoader(dataset=train_set, batch_size=batch_size, return_list=True, + shuffle=True, num_workers=num_workers) + return train_iter, train_set.vocab +``` + 将批量大小设置为512,将BERT输入序列的最大长度设置为64,我们打印出小批量的BERT预训练样本的形状。注意,在每个BERT输入序列中,为遮蔽语言模型任务预测$10$($64 \times 0.15$)个位置。 ```{.python .input} @@ -340,7 +432,7 @@ len(vocab) ## 练习 -1. 为简单起见,句号用作拆分句子的唯一分隔符。尝试其他的句子拆分技术,比如Spacy和NLTK。以NLTK为例。你需要先安装NLTK:`pip install nltk`。在代码中先`import nltk`。然后下载Punkt语句词元分析器:`nltk.download('punkt')`。要拆分句子,比如`sentences = 'This is great ! Why not ?'`,调用`nltk.tokenize.sent_tokenize(sentences)`将返回两个句子字符串的列表:`['This is great !', 'Why not ?']`。 +1. 为简单起见,句号用作拆分句子的唯一分隔符。尝试其他的句子拆分技术,比如Spacy和NLTK。以NLTK为例,需要先安装NLTK:`pip install nltk`。在代码中先`import nltk`。然后下载Punkt语句词元分析器:`nltk.download('punkt')`。要拆分句子,比如`sentences = 'This is great ! Why not ?'`,调用`nltk.tokenize.sent_tokenize(sentences)`将返回两个句子字符串的列表:`['This is great !', 'Why not ?']`。 1. 如果我们不过滤出一些不常见的词元,词量会有多大? :begin_tab:`mxnet` @@ -350,3 +442,7 @@ len(vocab) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5738) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11822) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/bert-pretraining.md b/chapter_natural-language-processing-pretraining/bert-pretraining.md index 12c073013..2862c0088 100644 --- a/chapter_natural-language-processing-pretraining/bert-pretraining.md +++ b/chapter_natural-language-processing-pretraining/bert-pretraining.md @@ -17,14 +17,40 @@ 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 +``` + 首先,我们加载WikiText-2数据集作为小批量的预训练样本,用于遮蔽语言模型和下一句预测。批量大小是512,BERT输入序列的最大长度是64。注意,在原始BERT模型中,最大长度是512。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch batch_size, max_len = 512, 64 train_iter, vocab = d2l.load_data_wiki(batch_size, max_len) ``` +```{.python .input} +#@tab paddle +def load_data_wiki(batch_size, max_len): + """加载WikiText-2数据集 + + Defined in :numref:`subsec_prepare_mlm_data`""" + data_dir = d2l.download_extract('wikitext-2', 'wikitext-2') + paragraphs = d2l._read_wiki(data_dir) + train_set = d2l._WikiTextDataset(paragraphs, max_len) + train_iter = paddle.io.DataLoader(dataset=train_set, batch_size=batch_size, return_list=True, + shuffle=True, num_workers=0) + return train_iter, train_set.vocab + +batch_size, max_len = 512, 64 +train_iter, vocab = load_data_wiki(batch_size, max_len) +``` + ## 预训练BERT 原始BERT :cite:`Devlin.Chang.Lee.ea.2018`有两个不同模型尺寸的版本。基本模型($\text{BERT}_{\text{BASE}}$)使用12层(Transformer编码器块),768个隐藏单元(隐藏大小)和12个自注意头。大模型($\text{BERT}_{\text{LARGE}}$)使用24层,1024个隐藏单元和16个自注意头。值得注意的是,前者有1.1亿个参数,后者有3.4亿个参数。为了便于演示,我们定义了一个小的BERT,使用了2层、128个隐藏单元和2个自注意头。 @@ -38,7 +64,7 @@ loss = gluon.loss.SoftmaxCELoss() ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle net = d2l.BERTModel(len(vocab), num_hiddens=128, norm_shape=[128], ffn_num_input=128, ffn_num_hiddens=256, num_heads=2, num_layers=2, dropout=0.2, key_size=128, query_size=128, @@ -103,6 +129,27 @@ def _get_batch_loss_bert(net, loss, vocab_size, tokens_X, return mlm_l, nsp_l, l ``` +```{.python .input} +#@tab paddle +#@save +def _get_batch_loss_bert(net, loss, vocab_size, tokens_X, + segments_X, valid_lens_x, + pred_positions_X, mlm_weights_X, + mlm_Y, nsp_y): + # 前向传播 + _, mlm_Y_hat, nsp_Y_hat = net(tokens_X, segments_X, + valid_lens_x.reshape([-1]), + pred_positions_X) + # 计算遮蔽语言模型损失 + mlm_l = loss(mlm_Y_hat.reshape([-1, vocab_size]), mlm_Y.reshape([-1])) *\ + mlm_weights_X.reshape([-1, 1]) + mlm_l = mlm_l.sum() / (mlm_weights_X.sum() + 1e-8) + # 计算下一句子预测任务的损失 + nsp_l = loss(nsp_Y_hat, nsp_y) + l = mlm_l + nsp_l + return mlm_l, nsp_l, l +``` + 通过调用上述两个辅助函数,下面的`train_bert`函数定义了在WikiText-2(`train_iter`)数据集上预训练BERT(`net`)的过程。训练BERT可能需要很长时间。以下函数的输入`num_steps`指定了训练的迭代步数,而不是像`train_ch13`函数那样指定训练的轮数(参见 :numref:`sec_image_augmentation`)。 ```{.python .input} @@ -189,13 +236,53 @@ def train_bert(train_iter, net, loss, vocab_size, devices, num_steps): f'{str(devices)}') ``` +```{.python .input} +#@tab paddle +def train_bert(train_iter, net, loss, vocab_size, devices, num_steps): + trainer = paddle.optimizer.Adam(parameters=net.parameters(), learning_rate=0.01) + step, timer = 0, d2l.Timer() + animator = d2l.Animator(xlabel='step', ylabel='loss', + xlim=[1, num_steps], legend=['mlm', 'nsp']) + # 遮蔽语言模型损失的和,下一句预测任务损失的和,句子对的数量,计数 + metric = d2l.Accumulator(4) + num_steps_reached = False + while step < num_steps and not num_steps_reached: + for tokens_X, segments_X, valid_lens_x, pred_positions_X,\ + mlm_weights_X, mlm_Y, nsp_y in train_iter: + trainer.clear_grad() + timer.start() + mlm_l, nsp_l, l = _get_batch_loss_bert( + net, loss, vocab_size, tokens_X, segments_X, valid_lens_x, + pred_positions_X, mlm_weights_X, mlm_Y, nsp_y) + l.backward() + trainer.step() + metric.add(mlm_l, nsp_l, tokens_X.shape[0], 1) + timer.stop() + animator.add(step + 1, + (metric[0] / metric[3], metric[1] / metric[3])) + step += 1 + if step == num_steps: + num_steps_reached = True + break + + print(f'MLM loss {metric[0] / metric[3]:.3f}, ' + f'NSP loss {metric[1] / metric[3]:.3f}') + print(f'{metric[2] / timer.sum():.1f} sentence pairs/sec on ' + f'{str(devices)}') +``` + 在预训练过程中,我们可以绘制出遮蔽语言模型损失和下一句预测损失。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch train_bert(train_iter, net, loss, len(vocab), devices, 50) ``` +```{.python .input} +#@tab paddle +train_bert(train_iter, net, loss, len(vocab), devices[:1], 50) +``` + ## 用BERT表示文本 在预训练BERT之后,我们可以用它来表示单个文本、文本对或其中的任何词元。下面的函数返回`tokens_a`和`tokens_b`中所有词元的BERT(`net`)表示。 @@ -222,6 +309,18 @@ def get_bert_encoding(net, tokens_a, tokens_b=None): return encoded_X ``` +```{.python .input} +#@tab paddle +def get_bert_encoding(net, tokens_a, tokens_b=None): + tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b) + token_ids = paddle.to_tensor(vocab[tokens]).unsqueeze(0) + segments = paddle.to_tensor(segments).unsqueeze(0) + valid_len = paddle.to_tensor(len(tokens)) + + encoded_X, _, _ = net(token_ids, segments, valid_len) + return encoded_X +``` + 考虑“a crane is flying”这句话。回想一下 :numref:`subsec_bert_input_rep`中讨论的BERT的输入表示。插入特殊标记“<cls>”(用于分类)和“<sep>”(用于分隔)后,BERT输入序列的长度为6。因为零是“<cls>”词元,`encoded_text[:, 0, :]`是整个输入语句的BERT表示。为了评估一词多义词元“crane”,我们还打印出了该词元的BERT表示的前三个元素。 ```{.python .input} @@ -267,3 +366,7 @@ encoded_pair.shape, encoded_pair_cls.shape, encoded_pair_crane[0][:3] :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5743) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11824) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/bert.md b/chapter_natural-language-processing-pretraining/bert.md index 57084c150..be5177967 100644 --- a/chapter_natural-language-processing-pretraining/bert.md +++ b/chapter_natural-language-processing-pretraining/bert.md @@ -43,6 +43,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 +``` + ## 输入表示 :label:`subsec_bert_input_rep` @@ -130,6 +139,37 @@ class BERTEncoder(nn.Module): return X ``` +```{.python .input} +#@tab paddle +#@save +class BERTEncoder(nn.Layer): + """BERT编码器""" + def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, + ffn_num_hiddens, num_heads, num_layers, dropout, + max_len=1000, key_size=768, query_size=768, value_size=768, + **kwargs): + super(BERTEncoder, self).__init__(**kwargs) + self.token_embedding = nn.Embedding(vocab_size, num_hiddens) + self.segment_embedding = nn.Embedding(2, num_hiddens) + self.blks = nn.Sequential() + for i in range(num_layers): + self.blks.add_sublayer(f"{i}", d2l.EncoderBlock( + key_size, query_size, value_size, num_hiddens, norm_shape, + ffn_num_input, ffn_num_hiddens, num_heads, dropout, True)) + # 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数 + x = paddle.randn([1, max_len, num_hiddens]) + self.pos_embedding = paddle.create_parameter(shape=x.shape, dtype=str(x.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign(x)) + + def forward(self, tokens, segments, valid_lens): + # 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens) + X = self.token_embedding(tokens) + self.segment_embedding(segments) + X = X + self.pos_embedding[:, :X.shape[1], :] + for blk in self.blks: + X = blk(X, valid_lens) + return X +``` + 假设词表大小为10000,为了演示`BERTEncoder`的前向推断,让我们创建一个实例并初始化它的参数。 ```{.python .input} @@ -141,7 +181,7 @@ encoder.initialize() ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle vocab_size, num_hiddens, ffn_num_hiddens, num_heads = 10000, 768, 1024, 4 norm_shape, ffn_num_input, num_layers, dropout = [768], 768, 2, 0.2 encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape, ffn_num_input, @@ -165,6 +205,14 @@ encoded_X = encoder(tokens, segments, None) encoded_X.shape ``` +```{.python .input} +#@tab paddle +tokens = paddle.randint(0, vocab_size, (2, 8)) +segments = paddle.to_tensor([[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 1, 1, 1, 1, 1]]) +encoded_X = encoder(tokens, segments, None) +encoded_X.shape +``` + ## 预训练任务 :label:`subsec_bert_pretraining_tasks` @@ -237,6 +285,32 @@ class MaskLM(nn.Module): return mlm_Y_hat ``` +```{.python .input} +#@tab paddle +#@save +class MaskLM(nn.Layer): + """BERT的掩蔽语言模型任务""" + def __init__(self, vocab_size, num_hiddens, num_inputs=768, **kwargs): + super(MaskLM, self).__init__(**kwargs) + self.mlp = nn.Sequential(nn.Linear(num_inputs, num_hiddens), + nn.ReLU(), + nn.LayerNorm(num_hiddens), + nn.Linear(num_hiddens, vocab_size)) + + def forward(self, X, pred_positions): + num_pred_positions = pred_positions.shape[1] + pred_positions = pred_positions.reshape([-1]) + batch_size = X.shape[0] + batch_idx = paddle.arange(0, batch_size) + # 假设batch_size=2,num_pred_positions=3 + # 那么batch_idx是np.array([0,0,0,1,1]) + batch_idx = paddle.repeat_interleave(batch_idx, num_pred_positions) + masked_X = X[batch_idx, pred_positions] + masked_X = masked_X.reshape((batch_size, num_pred_positions, -1)) + mlm_Y_hat = self.mlp(masked_X) + return mlm_Y_hat +``` + 为了演示`MaskLM`的前向推断,我们创建了其实例`mlm`并对其进行了初始化。回想一下,来自`BERTEncoder`的正向推断`encoded_X`表示2个BERT输入序列。我们将`mlm_positions`定义为在`encoded_X`的任一输入序列中预测的3个指示。`mlm`的前向推断返回`encoded_X`的所有掩蔽位置`mlm_positions`处的预测结果`mlm_Y_hat`。对于每个预测,结果的大小等于词表的大小。 ```{.python .input} @@ -255,6 +329,14 @@ mlm_Y_hat = mlm(encoded_X, mlm_positions) mlm_Y_hat.shape ``` +```{.python .input} +#@tab paddle +mlm = MaskLM(vocab_size, num_hiddens) +mlm_positions = paddle.to_tensor([[1, 5, 2], [6, 1, 5]]) +mlm_Y_hat = mlm(encoded_X, mlm_positions) +mlm_Y_hat.shape +``` + 通过掩码下的预测词元`mlm_Y`的真实标签`mlm_Y_hat`,我们可以计算在BERT预训练中的遮蔽语言模型任务的交叉熵损失。 ```{.python .input} @@ -272,6 +354,14 @@ mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape(-1)) mlm_l.shape ``` +```{.python .input} +#@tab paddle +mlm_Y = paddle.to_tensor([[7, 8, 9], [10, 20, 30]]) +loss = nn.CrossEntropyLoss(reduction='none') +mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape([-1])) +mlm_l.shape +``` + ### 下一句预测(Next Sentence Prediction) :label:`subsec_nsp` @@ -306,6 +396,20 @@ class NextSentencePred(nn.Module): return self.output(X) ``` +```{.python .input} +#@tab paddle +#@save +class NextSentencePred(nn.Layer): + """BERT的下一句预测任务""" + def __init__(self, num_inputs, **kwargs): + super(NextSentencePred, self).__init__(**kwargs) + self.output = nn.Linear(num_inputs, 2) + + def forward(self, X): + # X的形状:(batchsize,num_hiddens) + return self.output(X) +``` + 我们可以看到,`NextSentencePred`实例的前向推断返回每个BERT输入序列的二分类预测。 ```{.python .input} @@ -324,6 +428,15 @@ nsp_Y_hat = nsp(encoded_X) nsp_Y_hat.shape ``` +```{.python .input} +#@tab paddle +encoded_X = paddle.flatten(encoded_X, start_axis=1) +# NSP的输入形状:(batchsize,num_hiddens) +nsp = NextSentencePred(encoded_X.shape[-1]) +nsp_Y_hat = nsp(encoded_X) +nsp_Y_hat.shape +``` + 还可以计算两个二元分类的交叉熵损失。 ```{.python .input} @@ -339,6 +452,13 @@ nsp_l = loss(nsp_Y_hat, nsp_y) nsp_l.shape ``` +```{.python .input} +#@tab paddle +nsp_y = paddle.to_tensor([0, 1]) +nsp_l = loss(nsp_Y_hat, nsp_y) +nsp_l.shape +``` + 值得注意的是,上述两个预训练任务中的所有标签都可以从预训练语料库中获得,而无需人工标注。原始的BERT已经在图书语料库 :cite:`Zhu.Kiros.Zemel.ea.2015`和英文维基百科的连接上进行了预训练。这两个文本语料库非常庞大:它们分别有8亿个单词和25亿个单词。 ## 整合代码 @@ -402,6 +522,38 @@ class BERTModel(nn.Module): return encoded_X, mlm_Y_hat, nsp_Y_hat ``` +```{.python .input} +#@tab paddle +#@save +class BERTModel(nn.Layer): + """BERT模型""" + def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, + ffn_num_hiddens, num_heads, num_layers, dropout, + max_len=1000, key_size=768, query_size=768, value_size=768, + hid_in_features=768, mlm_in_features=768, + nsp_in_features=768): + super(BERTModel, self).__init__() + self.encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape, + ffn_num_input, ffn_num_hiddens, num_heads, num_layers, + dropout, max_len=max_len, key_size=key_size, + query_size=query_size, value_size=value_size) + self.hidden = nn.Sequential(nn.Linear(hid_in_features, num_hiddens), + nn.Tanh()) + self.mlm = MaskLM(vocab_size, num_hiddens, mlm_in_features) + self.nsp = NextSentencePred(nsp_in_features) + + def forward(self, tokens, segments, valid_lens=None, + pred_positions=None): + encoded_X = self.encoder(tokens, segments, valid_lens) + if pred_positions is not None: + mlm_Y_hat = self.mlm(encoded_X, pred_positions) + else: + mlm_Y_hat = None + # 用于下一句预测的多层感知机分类器的隐藏层,0是“”标记的索引 + nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :])) + return encoded_X, mlm_Y_hat, nsp_Y_hat +``` + ## 小结 * word2vec和GloVe等词嵌入模型与上下文无关。它们将相同的预训练向量赋给同一个词,而不考虑词的上下文(如果有的话)。它们很难处理好自然语言中的一词多义或复杂语义。 @@ -424,3 +576,7 @@ class BERTModel(nn.Module): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5750) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11820) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/index.md b/chapter_natural-language-processing-pretraining/index.md index 2ac1dd4ea..09cd357b5 100644 --- a/chapter_natural-language-processing-pretraining/index.md +++ b/chapter_natural-language-processing-pretraining/index.md @@ -27,7 +27,7 @@ “go to the bank to deposit some money”(去银行存点钱) 和“go to the bank to sit down”(去河岸坐下来)中是相同的。 因此,许多较新的预训练模型使相同词元的表示适应于不同的上下文, -其中包括基于transformer编码器的更深的自监督模型BERT。 +其中包括基于Transformer编码器的更深的自监督模型BERT。 在本章中,我们将重点讨论如何预训练文本的这种表示, 如 :numref:`fig_nlp-map-pretrain`中所强调的那样。 diff --git a/chapter_natural-language-processing-pretraining/similarity-analogy.md b/chapter_natural-language-processing-pretraining/similarity-analogy.md index 2b1479ac6..332d74103 100644 --- a/chapter_natural-language-processing-pretraining/similarity-analogy.md +++ b/chapter_natural-language-processing-pretraining/similarity-analogy.md @@ -19,6 +19,16 @@ from torch import nn import os ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +import os +``` + ## 加载预训练词向量 以下列出维度为50、100和300的预训练GloVe嵌入,可从[GloVe网站](https://nlp.stanford.edu/projects/glove/)下载。预训练的fastText嵌入有多种语言。这里我们使用可以从[fastText网站](https://fasttext.cc/)下载300维度的英文版本(“wiki.en”)。 @@ -131,6 +141,17 @@ def knn(W, x, k): return topk, [cos[int(i)] for i in topk] ``` +```{.python .input} +#@tab paddle +def knn(W, x, k): + # 增加1e-9以获得数值稳定性 + cos = paddle.mv(W, x) / ( + paddle.sqrt(paddle.sum(W * W, axis=1) + 1e-9) * + paddle.sqrt((x * x).sum())) + _, topk = paddle.topk(cos, k=k) + return topk, [cos[int(i)] for i in topk] +``` + 然后,我们使用`TokenEmbedding`的实例`embed`中预训练好的词向量来搜索相似的词。 ```{.python .input} @@ -227,3 +248,7 @@ get_analogy('do', 'did', 'go', glove_6b50d) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5746) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11819) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/subword-embedding.md b/chapter_natural-language-processing-pretraining/subword-embedding.md index 5718f7faf..51ad69a40 100644 --- a/chapter_natural-language-processing-pretraining/subword-embedding.md +++ b/chapter_natural-language-processing-pretraining/subword-embedding.md @@ -1,7 +1,7 @@ # 子词嵌入 :label:`sec_fasttext` -在英语中,“helps”、“helped”和“helping”等单词都是同一个词“help”的变形形式。“dog”和“dogs”之间的关系与“cat”和“cats”之间的关系相同,“boy”和“boyfriend”之间的关系与“girl”和“girlfriend”之间的关系相同。在法语和西班牙语等其他语言中,许多动词有40多种变形形式,而在芬兰语中,名词最多可能有15种变形。在语言学中,形态学研究单词形成和词汇关系。但是,word2vec和GloVe都没有对词的内部结构进行探讨。 +在英语中,“helps”“helped”和“helping”等单词都是同一个词“help”的变形形式。“dog”和“dogs”之间的关系与“cat”和“cats”之间的关系相同,“boy”和“boyfriend”之间的关系与“girl”和“girlfriend”之间的关系相同。在法语和西班牙语等其他语言中,许多动词有40多种变形形式,而在芬兰语中,名词最多可能有15种变形。在语言学中,形态学研究单词形成和词汇关系。但是,word2vec和GloVe都没有对词的内部结构进行探讨。 ## fastText模型 @@ -10,7 +10,7 @@ 让我们来说明如何以单词“where”为例获得fastText中每个中心词的子词。首先,在词的开头和末尾添加特殊字符“<”和“>”,以将前缀和后缀与其他子词区分开来。 然后,从词中提取字符$n$-gram。 例如,值$n=3$时,我们将获得长度为3的所有子词: -“<wh”、“whe”、“her”、“ere”、“re>”和特殊子词“<where>”。 +“<wh”“whe”“her”“ere”“re>”和特殊子词“<where>”。 在fastText中,对于任意词$w$,用$\mathcal{G}_w$表示其长度在3和6之间的所有子词与其特殊子词的并集。词表是所有词的子词的集合。假设$\mathbf{z}_g$是词典中的子词$g$的向量,则跳元模型中作为中心词的词$w$的向量$\mathbf{v}_w$是其子词向量的和: @@ -28,12 +28,23 @@ fastText的其余部分与跳元模型相同。与跳元模型相比,fastText 首先,我们将符号词表初始化为所有英文小写字符、特殊的词尾符号`'_'`和特殊的未知符号`'[UNK]'`。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch +import collections + +symbols = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '_', '[UNK]'] + +``` + +```{.python .input} +#@tab paddle import collections symbols = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '[UNK]'] + ``` 因为我们不考虑跨越词边界的符号对,所以我们只需要一个字典`raw_token_freqs`将词映射到数据集中的频率(出现次数)。注意,特殊符号`'_'`被附加到每个词的尾部,以便我们可以容易地从输出符号序列(例如,“a_all er_man”)恢复单词序列(例如,“a_all er_man”)。由于我们仅从单个字符和特殊符号的词开始合并处理,所以在每个词(词典`token_freqs`的键)内的每对连续字符之间插入空格。换句话说,空格是词中符号之间的分隔符。 @@ -93,7 +104,7 @@ for i in range(num_merges): print(symbols) ``` -对于在词典`raw_token_freqs`的键中指定的同一数据集,作为字节对编码算法的结果,数据集中的每个词现在被子词“fast_”、“fast”、“er_”、“tall_”和“tall”分割。例如,单词“fast er_”和“tall er_”分别被分割为“fast er_”和“tall er_”。 +对于在词典`raw_token_freqs`的键中指定的同一数据集,作为字节对编码算法的结果,数据集中的每个词现在被子词“fast_”“fast”“er_”“tall_”和“tall”分割。例如,单词“fast er_”和“tall er_”分别被分割为“fast er_”和“tall er_”。 ```{.python .input} #@tab all @@ -150,4 +161,8 @@ print(segment_BPE(tokens, symbols)) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5748) -:end_tab: \ No newline at end of file +:end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11818) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/word-embedding-dataset.md b/chapter_natural-language-processing-pretraining/word-embedding-dataset.md index 00c90b0a4..9ce0c4036 100644 --- a/chapter_natural-language-processing-pretraining/word-embedding-dataset.md +++ b/chapter_natural-language-processing-pretraining/word-embedding-dataset.md @@ -1,7 +1,7 @@ # 用于预训练词嵌入的数据集 :label:`sec_word2vec_data` -现在我们已经了解了word2vec模型的技术细节和大致的训练方法,让我们来看看它们的实现。具体地说,我们将以 :numref:`sec_word2vec`的跳元模型和 :numref:`sec_approx_train`的负采样为例。在本节中,我们从用于预训练词嵌入模型的数据集开始:数据的原始格式将被转换为可以在训练期间迭代的小批量。 +现在我们已经了解了word2vec模型的技术细节和大致的训练方法,让我们来看看它们的实现。具体地说,我们将以 :numref:`sec_word2vec`的跳元模型和 :numref:`sec_approx_train`的负采样为例。本节从用于预训练词嵌入模型的数据集开始:数据的原始格式将被转换为可以在训练期间迭代的小批量。 ```{.python .input} from d2l import mxnet as d2l @@ -20,7 +20,18 @@ import os import random ``` -## 正在读取数据集 +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +import os +import random +``` + +## 读取数据集 我们在这里使用的数据集是[Penn Tree Bank(PTB)](https://catalog.ldc.upenn.edu/LDC99T42)。该语料库取自“华尔街日报”的文章,分为训练集、验证集和测试集。在原始格式中,文本文件的每一行表示由空格分隔的一句话。在这里,我们将每个单词视为一个词元。 @@ -53,7 +64,7 @@ f'vocab size: {len(vocab)}' ## 下采样 -文本数据通常有“the”、“a”和“in”等高频词:它们在非常大的语料库中甚至可能出现数十亿次。然而,这些词经常在上下文窗口中与许多不同的词共同出现,提供的有用信息很少。例如,考虑上下文窗口中的词“chip”:直观地说,它与低频单词“intel”的共现比与高频单词“a”的共现在训练中更有用。此外,大量(高频)单词的训练速度很慢。因此,当训练词嵌入模型时,可以对高频单词进行*下采样* :cite:`Mikolov.Sutskever.Chen.ea.2013`。具体地说,数据集中的每个词$w_i$将有概率地被丢弃 +文本数据通常有“the”“a”和“in”等高频词:它们在非常大的语料库中甚至可能出现数十亿次。然而,这些词经常在上下文窗口中与许多不同的词共同出现,提供的有用信息很少。例如,考虑上下文窗口中的词“chip”:直观地说,它与低频单词“intel”的共现比与高频单词“a”的共现在训练中更有用。此外,大量(高频)单词的训练速度很慢。因此,当训练词嵌入模型时,可以对高频单词进行*下采样* :cite:`Mikolov.Sutskever.Chen.ea.2013`。具体地说,数据集中的每个词$w_i$将有概率地被丢弃 $$ P(w_i) = \max\left(1 - \sqrt{\frac{t}{f(w_i)}}, 0\right),$$ @@ -322,6 +333,43 @@ def load_data_ptb(batch_size, max_window_size, num_noise_words): return data_iter, vocab ``` +```{.python .input} +#@tab paddle +#@save +def load_data_ptb(batch_size, max_window_size, num_noise_words): + """下载PTB数据集,然后将其加载到内存中""" + num_workers = d2l.get_dataloader_workers() + sentences = read_ptb() + vocab = d2l.Vocab(sentences, min_freq=10) + subsampled, counter = subsample(sentences, vocab) + corpus = [vocab[line] for line in subsampled] + all_centers, all_contexts = get_centers_and_contexts( + corpus, max_window_size) + all_negatives = get_negatives( + all_contexts, vocab, counter, num_noise_words) + + class PTBDataset(paddle.io.Dataset): + def __init__(self, centers, contexts, negatives): + assert len(centers) == len(contexts) == len(negatives) + self.centers = centers + self.contexts = contexts + self.negatives = negatives + + def __getitem__(self, index): + return (self.centers[index], self.contexts[index], + self.negatives[index]) + + def __len__(self): + return len(self.centers) + + dataset = PTBDataset(all_centers, all_contexts, all_negatives) + + data_iter = paddle.io.DataLoader( + dataset, batch_size=batch_size, shuffle=True, return_list=True, + collate_fn=batchify, num_workers=num_workers) + return data_iter, vocab +``` + 让我们打印数据迭代器的第一个小批量。 ```{.python .input} @@ -351,3 +399,7 @@ for batch in data_iter: :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5735) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11816) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/word2vec-pretraining.md b/chapter_natural-language-processing-pretraining/word2vec-pretraining.md index 8373842ce..1906fdc48 100644 --- a/chapter_natural-language-processing-pretraining/word2vec-pretraining.md +++ b/chapter_natural-language-processing-pretraining/word2vec-pretraining.md @@ -27,6 +27,20 @@ data_iter, vocab = d2l.load_data_ptb(batch_size, max_window_size, num_noise_words) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +from paddle import nn + +batch_size, max_window_size, num_noise_words = 512, 5, 5 +data_iter, vocab = d2l.load_data_ptb(batch_size, max_window_size, + num_noise_words) +``` + ## 跳元模型 我们通过嵌入层和批量矩阵乘法实现了跳元模型。首先,让我们回顾一下嵌入层是如何工作的。 @@ -48,6 +62,13 @@ print(f'Parameter embedding_weight ({embed.weight.shape}, ' f'dtype={embed.weight.dtype})') ``` +```{.python .input} +#@tab paddle +embed = nn.Embedding(num_embeddings=20, embedding_dim=4) +print(f'Parameter embedding_weight ({embed.weight.shape}, ' + f'dtype={embed.weight.dtype})') +``` + 嵌入层的输入是词元(词)的索引。对于任何词元索引$i$,其向量表示可以从嵌入层中的权重矩阵的第$i$行获得。由于向量维度(`output_dim`)被设置为4,因此当小批量词元索引的形状为(2,3)时,嵌入层返回具有形状(2,3,4)的向量。 ```{.python .input} @@ -77,6 +98,15 @@ def skip_gram(center, contexts_and_negatives, embed_v, embed_u): return pred ``` +```{.python .input} +#@tab paddle +def skip_gram(center, contexts_and_negatives, embed_v, embed_u): + v = embed_v(center) + u = embed_u(contexts_and_negatives) + pred = paddle.bmm(v, u.transpose(perm=[0, 2, 1])) + return pred +``` + 让我们为一些样例输入打印此`skip_gram`函数的输出形状。 ```{.python .input} @@ -89,6 +119,12 @@ skip_gram(torch.ones((2, 1), dtype=torch.long), torch.ones((2, 4), dtype=torch.long), embed, embed).shape ``` +```{.python .input} +#@tab paddle +skip_gram(paddle.ones((2, 1), dtype='int64'), + paddle.ones((2, 4), dtype='int64'), embed, embed).shape +``` + ## 训练 在训练带负采样的跳元模型之前,我们先定义它的损失函数。 @@ -116,16 +152,39 @@ class SigmoidBCELoss(nn.Module): loss = SigmoidBCELoss() ``` +```{.python .input} +#@tab paddle +class SigmoidBCELoss(nn.Layer): + # 带掩码的二元交叉熵损失 + def __init__(self): + super().__init__() + + def forward(self, inputs, target, mask=None): + out = nn.functional.binary_cross_entropy_with_logits( + logit=inputs, label=target, weight=mask, reduction="none") + return out.mean(axis=1) + +loss = SigmoidBCELoss() +``` + 回想一下我们在 :numref:`subsec_word2vec-minibatch-loading`中对掩码变量和标签变量的描述。下面计算给定变量的二进制交叉熵损失。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch pred = d2l.tensor([[1.1, -2.2, 3.3, -4.4]] * 2) label = d2l.tensor([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]]) mask = d2l.tensor([[1, 1, 1, 1], [1, 1, 0, 0]]) loss(pred, label, mask) * mask.shape[1] / mask.sum(axis=1) ``` +```{.python .input} +#@tab paddle +pred = d2l.tensor([[1.1, -2.2, 3.3, -4.4]] * 2) +label = d2l.tensor([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]]) +mask = d2l.tensor([[1, 1, 1, 1], [1, 1, 0, 0]], dtype='float32') +loss(pred, label, mask) * mask.shape[1] / mask.sum(axis=1) +``` + 下面显示了如何使用二元交叉熵损失中的Sigmoid激活函数(以较低效率的方式)计算上述结果。我们可以将这两个输出视为两个规范化的损失,在非掩码预测上进行平均。 ```{.python .input} @@ -149,7 +208,7 @@ net.add(nn.Embedding(input_dim=len(vocab), output_dim=embed_size), ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle embed_size = 100 net = nn.Sequential(nn.Embedding(num_embeddings=len(vocab), embedding_dim=embed_size), @@ -222,6 +281,39 @@ def train(net, data_iter, lr, num_epochs, device=d2l.try_gpu()): f'{metric[1] / timer.stop():.1f} tokens/sec on {str(device)}') ``` +```{.python .input} +#@tab paddle +def train(net, data_iter, lr, num_epochs, device=d2l.try_gpu()): + def init_weights(m): + if type(m) == nn.Embedding: + nn.initializer.XavierUniform(m.weight) + net.apply(init_weights) + optimizer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[1, num_epochs]) + # 规范化的损失之和,规范化的损失数 + metric = d2l.Accumulator(2) + for epoch in range(num_epochs): + timer, num_batches = d2l.Timer(), len(data_iter) + for i, batch in enumerate(data_iter): + optimizer.clear_grad() + center, context_negative, mask, label = [ + paddle.to_tensor(data, place=device) for data in batch] + + pred = skip_gram(center, context_negative, net[0], net[1]) + l = (loss(pred.reshape(label.shape), paddle.to_tensor(label, dtype='float32'), + paddle.to_tensor(mask, dtype='float32')) + / mask.sum(axis=1) * mask.shape[1]) + l.sum().backward() + optimizer.step() + metric.add(l.sum(), l.numel()) + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[1],)) + print(f'loss {metric[0] / metric[1]:.3f}, ' + f'{metric[1] / timer.stop():.1f} tokens/sec on {str(device)}') +``` + 现在,我们可以使用负采样来训练跳元模型。 ```{.python .input} @@ -264,6 +356,21 @@ def get_similar_tokens(query_token, k, embed): get_similar_tokens('chip', 3, net[0]) ``` +```{.python .input} +#@tab paddle +def get_similar_tokens(query_token, k, embed): + W = embed.weight + x = W[vocab[query_token]] + # 计算余弦相似性。增加1e-9以获得数值稳定性 + cos = paddle.mv(W, x) / paddle.sqrt(paddle.sum(W * W, axis=1) * + paddle.sum(x * x) + 1e-9) + topk = paddle.topk(cos, k=k+1)[1].numpy().astype('int32') + for i in topk[1:]: # 删除输入词 + print(f'cosine sim={float(cos[i]):.3f}: {vocab.to_tokens(i)}') + +get_similar_tokens('chip', 3, net[0]) +``` + ## 小结 * 我们可以使用嵌入层和二元交叉熵损失来训练带负采样的跳元模型。 @@ -281,3 +388,7 @@ get_similar_tokens('chip', 3, net[0]) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/5740) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11817) +:end_tab: diff --git a/chapter_natural-language-processing-pretraining/word2vec.md b/chapter_natural-language-processing-pretraining/word2vec.md index 29c642a8f..b95ef3f30 100644 --- a/chapter_natural-language-processing-pretraining/word2vec.md +++ b/chapter_natural-language-processing-pretraining/word2vec.md @@ -1,4 +1,4 @@ -# 词嵌入(Word2vec) +# 词嵌入(word2vec) :label:`sec_word2vec` 自然语言是用来表达人脑思维的复杂系统。 @@ -20,14 +20,14 @@ $$\frac{\mathbf{x}^\top \mathbf{y}}{\|\mathbf{x}\| \|\mathbf{y}\|} \in [-1, 1].$ ## 自监督的word2vec -[word2vec](https://code.google.com/archive/p/word2vec/)工具是为了解决上述问题而提出的。它将每个词映射到一个固定长度的向量,这些向量能更好地表达不同词之间的相似性和类比关系。word2vec工具包含两个模型,即*跳元模型*(skip-gram) :cite:`Mikolov.Sutskever.Chen.ea.2013`和*连续词袋*(CBOW) :cite:`Mikolov.Chen.Corrado.ea.2013`。对于在语义上有意义的表示,它们的训练依赖于条件概率,条件概率可以被看作是使用语料库中一些词来预测另一些单词。由于是不带标签的数据,因此跳元模型和连续词袋都是自监督模型。 +[word2vec](https://code.google.com/archive/p/word2vec/)工具是为了解决上述问题而提出的。它将每个词映射到一个固定长度的向量,这些向量能更好地表达不同词之间的相似性和类比关系。word2vec工具包含两个模型,即*跳元模型*(skip-gram) :cite:`Mikolov.Sutskever.Chen.ea.2013`和*连续词袋*(CBOW) :cite:`Mikolov.Chen.Corrado.ea.2013`。对于在语义上有意义的表示,它们的训练依赖于条件概率,条件概率可以被看作使用语料库中一些词来预测另一些单词。由于是不带标签的数据,因此跳元模型和连续词袋都是自监督模型。 下面,我们将介绍这两种模式及其训练方法。 ## 跳元模型(Skip-Gram) :label:`subsec_skip-gram` -跳元模型假设一个词可以用来在文本序列中生成其周围的单词。以文本序列“the”、“man”、“loves”、“his”、“son”为例。假设*中心词*选择“loves”,并将上下文窗口设置为2,如图 :numref:`fig_skip_gram`所示,给定中心词“loves”,跳元模型考虑生成*上下文词*“the”、“man”、“him”、“son”的条件概率: +跳元模型假设一个词可以用来在文本序列中生成其周围的单词。以文本序列“the”“man”“loves”“his”“son”为例。假设*中心词*选择“loves”,并将上下文窗口设置为2,如图 :numref:`fig_skip_gram`所示,给定中心词“loves”,跳元模型考虑生成*上下文词*“the”“man”“him”“son”的条件概率: $$P(\textrm{"the"},\textrm{"man"},\textrm{"his"},\textrm{"son"}\mid\textrm{"loves"}).$$ @@ -71,7 +71,7 @@ $$\begin{aligned}\frac{\partial \text{log}\, P(w_o \mid w_c)}{\partial \mathbf{v ## 连续词袋(CBOW)模型 -*连续词袋*(CBOW)模型类似于跳元模型。与跳元模型的主要区别在于,连续词袋模型假设中心词是基于其在文本序列中的周围上下文词生成的。例如,在文本序列“the”、“man”、“loves”、“his”、“son”中,在“loves”为中心词且上下文窗口为2的情况下,连续词袋模型考虑基于上下文词“the”、“man”、“him”、“son”(如 :numref:`fig_cbow`所示)生成中心词“loves”的条件概率,即: +*连续词袋*(CBOW)模型类似于跳元模型。与跳元模型的主要区别在于,连续词袋模型假设中心词是基于其在文本序列中的周围上下文词生成的。例如,在文本序列“the”“man”“loves”“his”“son”中,在“loves”为中心词且上下文窗口为2的情况下,连续词袋模型考虑基于上下文词“the”“man”“him”“son”(如 :numref:`fig_cbow`所示)生成中心词“loves”的条件概率,即: $$P(\textrm{"loves"}\mid\textrm{"the"},\textrm{"man"},\textrm{"his"},\textrm{"son"}).$$ @@ -111,7 +111,7 @@ $$\frac{\partial \log\, P(w_c \mid \mathcal{W}_o)}{\partial \mathbf{v}_{o_i}} = ## 小结 -* 词向量是用于表示单词意义的向量,也可以看作是词的特征向量。将词映射到实向量的技术称为词嵌入。 +* 词向量是用于表示单词意义的向量,也可以看作词的特征向量。将词映射到实向量的技术称为词嵌入。 * word2vec工具包含跳元模型和连续词袋模型。 * 跳元模型假设一个单词可用于在文本序列中,生成其周围的单词;而连续词袋模型假设基于上下文词来生成中心单词。 diff --git a/chapter_notation/index.md b/chapter_notation/index.md index 082765e53..c0148f538 100644 --- a/chapter_notation/index.md +++ b/chapter_notation/index.md @@ -18,8 +18,8 @@ * $\mathcal{X}$: 集合 * $\mathbb{Z}$: 整数集合 * $\mathbb{R}$: 实数集合 -* $\mathbb{R}^n$: $n$维实数向量 -* $\mathbb{R}^{a\times b}$: 包含$a$行和$b$列的实数矩阵 +* $\mathbb{R}^n$: $n$维实数向量集合 +* $\mathbb{R}^{a\times b}$: 包含$a$行和$b$列的实数矩阵集合 * $\mathcal{A}\cup\mathcal{B}$: 集合$\mathcal{A}$和$\mathcal{B}$的并集 * $\mathcal{A}\cap\mathcal{B}$:集合$\mathcal{A}$和$\mathcal{B}$的交集 * $\mathcal{A}\setminus\mathcal{B}$:集合$\mathcal{A}$与集合$\mathcal{B}$相减,$\mathcal{B}$关于$\mathcal{A}$的相对补集 diff --git a/chapter_optimization/adadelta.md b/chapter_optimization/adadelta.md index 6856d94ec..f9fe133eb 100644 --- a/chapter_optimization/adadelta.md +++ b/chapter_optimization/adadelta.md @@ -107,6 +107,34 @@ def adadelta(params, grads, states, hyperparams): delta[:].assign(rho * delta + (1 - rho) * g * g) ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle + +def init_adadelta_states(feature_dim): + s_w, s_b = d2l.zeros(shape=(feature_dim, 1)), d2l.zeros(shape=(1, )) + delta_w, delta_b = d2l.zeros(shape=(feature_dim, 1)), d2l.zeros(shape=(1, )) + return ((s_w, delta_w), (s_b, delta_b)) + +def adadelta(params, states, hyperparams): + a = [] + rho, eps = hyperparams['rho'], 1e-5 + for p, (s, delta) in zip(params, states): + with paddle.no_grad(): + # In-placeupdatesvia[:] + s[:] = rho * s + (1 - rho) * paddle.square(p.grad) + g = (paddle.sqrt(delta + eps) / paddle.sqrt(s + eps)) * p.grad + p[:] -= g + delta[:] = rho * delta + (1 - rho) * g * g + p.grad.zero_() + a.append(p) + return a +``` + 对于每次参数更新,选择$\rho = 0.9$相当于10个半衰期。由此我们得到: ```{.python .input} @@ -136,6 +164,12 @@ trainer = tf.keras.optimizers.Adadelta d2l.train_concise_ch11(trainer, {'learning_rate':5.0, 'rho': 0.9}, data_iter) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.Adadelta +d2l.train_concise_ch11(trainer, {'rho': 0.9}, data_iter) +``` + ## 小结 * Adadelta没有学习率参数。相反,它使用参数本身的变化率来调整学习率。 @@ -146,7 +180,7 @@ d2l.train_concise_ch11(trainer, {'learning_rate':5.0, 'rho': 0.9}, data_iter) 1. 调整$\rho$的值,会发生什么? 1. 展示如何在不使用$\mathbf{g}_t'$的情况下实现算法。为什么这是个好主意? -1. Adadelta真的是学习率为0吗?你能找到Adadelta无法解决的优化问题吗? +1. Adadelta真的是学习率为0吗?能找到Adadelta无法解决的优化问题吗? 1. 将Adadelta的收敛行为与AdaGrad和RMSProp进行比较。 :begin_tab:`mxnet` @@ -160,3 +194,7 @@ d2l.train_concise_ch11(trainer, {'learning_rate':5.0, 'rho': 0.9}, data_iter) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/5773) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11854) +:end_tab: \ No newline at end of file diff --git a/chapter_optimization/adagrad.md b/chapter_optimization/adagrad.md index 9882bc694..0fb347992 100644 --- a/chapter_optimization/adagrad.md +++ b/chapter_optimization/adagrad.md @@ -30,7 +30,7 @@ AdaGrad算法 :cite:`Duchi.Hazan.Singer.2011`通过将粗略的计数器$s(i, t) ## 预处理 凸优化问题有助于分析算法的特点。 -毕竟对于大多数非凸问题来说,获得有意义的理论保证很难,但是直觉和洞察往往会延续。 +毕竟对大多数非凸问题来说,获得有意义的理论保证很难,但是直觉和洞察往往会延续。 让我们来看看最小化$f(\mathbf{x}) = \frac{1}{2} \mathbf{x}^\top \mathbf{Q} \mathbf{x} + \mathbf{c}^\top \mathbf{x} + b$这一问题。 正如在 :numref:`sec_momentum`中那样,我们可以根据其特征分解$\mathbf{Q} = \mathbf{U}^\top \boldsymbol{\Lambda} \mathbf{U}$重写这个问题,来得到一个简化得多的问题,使每个坐标都可以单独解出: @@ -45,7 +45,7 @@ $$f(\mathbf{x}) = \bar{f}(\bar{\mathbf{x}}) = \frac{1}{2} \bar{\mathbf{x}}^\top 遗憾的是,情况并非如此。 虽然$\mathbf{c}$的微小变化导致了$\bar{\mathbf{c}}$同样的微小变化,但$f$的(以及$\bar{f}$的)最小化器并非如此。 每当特征值$\boldsymbol{\Lambda}_i$很大时,我们只会看到$\bar{x}_i$和$\bar{f}$的最小值发声微小变化。 -相反,对于小的$\boldsymbol{\Lambda}_i$来说,$\bar{x}_i$的变化可能是剧烈的。 +相反,对小的$\boldsymbol{\Lambda}_i$来说,$\bar{x}_i$的变化可能是剧烈的。 最大和最小的特征值之比称为优化问题的*条件数*(condition number)。 $$\kappa = \frac{\boldsymbol{\Lambda}_1}{\boldsymbol{\Lambda}_d}.$$ @@ -142,6 +142,16 @@ import math import tensorflow as tf ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +``` + ```{.python .input} #@tab all def adagrad_2d(x1, x2, s1, s2): @@ -216,6 +226,25 @@ def adagrad(params, grads, states, hyperparams): p[:].assign(p - hyperparams['lr'] * g / tf.math.sqrt(s + eps)) ``` +```{.python .input} +#@tab paddle +def init_adagrad_states(feature_dim): + s_w = d2l.zeros((feature_dim, 1)) + s_b = d2l.zeros(shape=(1, )) + return (s_w, s_b) + +def adagrad(params, states, hyperparams): + a = [] + eps = 1e-6 + for p, s in zip(params, states): + with paddle.no_grad(): + s[:] += paddle.square(p.grad) + p[:] -= hyperparams['lr'] * p.grad / paddle.sqrt(s + eps) + p.grad.zero_() + a.append(p) + return a +``` + 与 :numref:`sec_minibatch_sgd`一节中的实验相比,这里使用更大的学习率来训练模型。 ```{.python .input} @@ -245,6 +274,12 @@ trainer = tf.keras.optimizers.Adagrad d2l.train_concise_ch11(trainer, {'learning_rate' : 0.1}, data_iter) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.Adagrad +d2l.train_concise_ch11(trainer, {'learning_rate': 0.1}, data_iter) +``` + ## 小结 * AdaGrad算法会在单个坐标层面动态降低学习率。 @@ -261,7 +296,7 @@ d2l.train_concise_ch11(trainer, {'learning_rate' : 0.1}, data_iter) 1. 证明[格什戈林圆盘定理](https://en.wikipedia.org/wiki/Gershgorin_circle_theorem),其中提到,矩阵$\mathbf{M}$的特征值$\lambda_i$在至少一个$j$的选项中满足$|\lambda_i - \mathbf{M}_{jj}| \leq \sum_{k \neq j} |\mathbf{M}_{jk}|$的要求。 1. 关于对角线预处理矩阵$\mathrm{diag}^{-\frac{1}{2}}(\mathbf{M}) \mathbf{M} \mathrm{diag}^{-\frac{1}{2}}(\mathbf{M})$的特征值,格什戈林的定理告诉了我们什么? 1. 尝试对适当的深度网络使用AdaGrad算法,例如,:numref:`sec_lenet`中应用于Fashion-MNIST的深度网络。 -1. 你要如何修改AdaGrad算法,才能使其在学习率方面的衰减不那么激进? +1. 要如何修改AdaGrad算法,才能使其在学习率方面的衰减不那么激进? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/4318) @@ -274,3 +309,7 @@ d2l.train_concise_ch11(trainer, {'learning_rate' : 0.1}, data_iter) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/4320) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11852) +:end_tab: \ No newline at end of file diff --git a/chapter_optimization/adam.md b/chapter_optimization/adam.md index 9bd7833ea..67e178e60 100644 --- a/chapter_optimization/adam.md +++ b/chapter_optimization/adam.md @@ -131,6 +131,36 @@ def adam(params, grads, states, hyperparams): / tf.math.sqrt(s_bias_corr) + eps) ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle + +def init_adam_states(feature_dim): + v_w, v_b = d2l.zeros((feature_dim, 1)), d2l.zeros((1, )) + s_w, s_b = d2l.zeros((feature_dim, 1)), d2l.zeros((1, )) + return ((v_w, s_w), (v_b, s_b)) + +def adam(params, states, hyperparams): + beta1, beta2, eps = 0.9, 0.999, 1e-6 + a = [] + for p, (v, s) in zip(params, states): + with paddle.no_grad(): + v[:] = beta1 * v + (1 - beta1) * p.grad + s[:] = beta2 * s + (1 - beta2) * paddle.square(p.grad) + v_bias_corr = v / (1 - beta1 ** hyperparams['t']) + s_bias_corr = s / (1 - beta2 ** hyperparams['t']) + p[:] -= hyperparams['lr'] * v_bias_corr / (paddle.sqrt(s_bias_corr) + + eps) + p.grad.zero_() + a.append(p) + hyperparams['t'] += 1 + return a +``` + 现在,我们用以上Adam算法来训练模型,这里我们使用$\eta = 0.01$的学习率。 ```{.python .input} @@ -158,6 +188,12 @@ trainer = tf.keras.optimizers.Adam d2l.train_concise_ch11(trainer, {'learning_rate': 0.01}, data_iter) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.Adam +d2l.train_concise_ch11(trainer, {'learning_rate': 0.01}, data_iter) +``` + ## Yogi Adam算法也存在一些问题: @@ -232,6 +268,30 @@ d2l.train_ch11(yogi, init_adam_states(feature_dim), {'lr': 0.01, 't': 1}, data_iter, feature_dim); ``` +```{.python .input} +#@tab paddle +def yogi(params, states, hyperparams): + beta1, beta2, eps = 0.9, 0.999, 1e-3 + a=[] + for p, (v, s) in zip(params, states): + with paddle.no_grad(): + v[:] = beta1 * v + (1 - beta1) * p.grad + s[:] = s + (1 - beta2) * paddle.sign( + paddle.square(p.grad) - s) * paddle.square(p.grad) + v_bias_corr = v / (1 - beta1 ** hyperparams['t']) + s_bias_corr = s / (1 - beta2 ** hyperparams['t']) + p[:] -= hyperparams['lr'] * v_bias_corr / (paddle.sqrt(s_bias_corr) + + eps) + p.grad.zero_() + a.append(p) + hyperparams['t'] += 1 + return a + +data_iter, feature_dim = d2l.get_data_ch11(batch_size=10) +d2l.train_ch11(yogi, init_adam_states(feature_dim), + {'lr': 0.01, 't': 1}, data_iter, feature_dim); +``` + ## 小结 * Adam算法将许多优化算法的功能结合到了相当强大的更新规则中。 @@ -242,8 +302,8 @@ d2l.train_ch11(yogi, init_adam_states(feature_dim), ## 练习 1. 调节学习率,观察并分析实验结果。 -1. 你能重写动量和二次矩更新,从而使其不需要偏差校正吗? -1. 当我们收敛时,为什么你需要降低学习率$\eta$? +1. 试着重写动量和二次矩更新,从而使其不需要偏差校正。 +1. 收敛时为什么需要降低学习率$\eta$? 1. 尝试构造一个使用Adam算法会发散而Yogi会收敛的例子。 :begin_tab:`mxnet` @@ -257,3 +317,7 @@ d2l.train_ch11(yogi, init_adam_states(feature_dim), :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/4332) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11855) +:end_tab: diff --git a/chapter_optimization/convexity.md b/chapter_optimization/convexity.md index 80e614398..49c489829 100644 --- a/chapter_optimization/convexity.md +++ b/chapter_optimization/convexity.md @@ -2,12 +2,12 @@ :label:`sec_convexity` *凸性*(convexity)在优化算法的设计中起到至关重要的作用, -这主要是由于在这种情况下对算法进行分析和测试要容易得多。 -换言之,如果该算法甚至在凸性条件设定下的效果很差, -通常我们很难在其他条件下看到好的结果。 +这主要是由于在这种情况下对算法进行分析和测试要容易。 +换言之,如果算法在凸性条件设定下的效果很差, +那通常我们很难在其他条件下看到好的结果。 此外,即使深度学习中的优化问题通常是非凸的, 它们也经常在局部极小值附近表现出一些凸性。 -这可能会产生一些像 :cite:`Izmailov.Podoprikhin.Garipov.ea.2018`这样比较有意思的新的优化变体。 +这可能会产生一些像 :cite:`Izmailov.Podoprikhin.Garipov.ea.2018`这样比较有意思的新优化变体。 ```{.python .input} %matplotlib inline @@ -35,6 +35,17 @@ from mpl_toolkits import mplot3d import tensorflow as tf ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import numpy as np +from mpl_toolkits import mplot3d +import paddle +``` + ## 定义 在进行凸分析之前,我们需要定义*凸集*(convex sets)和*凸函数*(convex functions)。 @@ -42,7 +53,7 @@ import tensorflow as tf ### 凸集 *凸集*(convex set)是凸性的基础。 -简单地说,如果对于任何$a,b \in \mathcal{X}$,连接$a$和$b$的线段也位于$\mathcal{X}$中,则向量空间中的一个集合$\mathcal{X}$是*凸*(convex)的。 +简单地说,如果对于任何$a, b \in \mathcal{X}$,连接$a$和$b$的线段也位于$\mathcal{X}$中,则向量空间中的一个集合$\mathcal{X}$是*凸*(convex)的。 在数学术语上,这意味着对于所有$\lambda \in [0, 1]$,我们得到 $$\lambda a + (1-\lambda) b \in \mathcal{X} \text{ 当 } a, b \in \mathcal{X}.$$ @@ -53,8 +64,7 @@ $$\lambda a + (1-\lambda) b \in \mathcal{X} \text{ 当 } a, b \in \mathcal{X}. ![第一组是非凸的,另外两组是凸的。](../img/pacman.svg) :label:`fig_pacman` -有了定义做什么呢? -我们来看一下交集 :numref:`fig_convex_intersect`。 +接下来来看一下交集 :numref:`fig_convex_intersect`。 假设$\mathcal{X}$和$\mathcal{Y}$是凸集,那么$\mathcal {X} \cap \mathcal{Y}$也是凸集的。 现在考虑任意$a, b \in \mathcal{X} \cap \mathcal{Y}$, 因为$\mathcal{X}$和$\mathcal{Y}$是凸集, @@ -77,12 +87,12 @@ $$\lambda a + (1-\lambda) b \in \mathcal{X} \text{ 当 } a, b \in \mathcal{X}. 通常,深度学习中的问题是在凸集上定义的。 例如,$\mathbb{R}^d$,即实数的$d$-维向量的集合是凸集(毕竟$\mathbb{R}^d$中任意两点之间的线存在$\mathbb{R}^d$)中。 -在某些情况下,我们使用有界长度的变量,例如球的半径定义为$\{\mathbf{x} | \mathbf{x} \in \mathbb{R}^d \text{ and } \| \mathbf{x} \| \leq r\}$。 +在某些情况下,我们使用有界长度的变量,例如球的半径定义为$\{\mathbf{x} | \mathbf{x} \in \mathbb{R}^d \text{ 且 } \| \mathbf{x} \| \leq r\}$。 ### 凸函数 现在我们有了凸集,我们可以引入*凸函数*(convex function)$f$。 -给定一个凸集$\mathcal{X}$,如果对于所有$x, x' \in \mathcal{X}$和所有$\lambda \in [0, 1]$,一个函数$f: \mathcal{X} \to \mathbb{R}$是凸的,我们可以得到 +给定一个凸集$\mathcal{X}$,如果对于所有$x, x' \in \mathcal{X}$和所有$\lambda \in [0, 1]$,函数$f: \mathcal{X} \to \mathbb{R}$是凸的,我们可以得到 $$\lambda f(x) + (1-\lambda) f(x') \geq f(\lambda x + (1-\lambda) x').$$ @@ -90,7 +100,7 @@ $$\lambda f(x) + (1-\lambda) f(x') \geq f(\lambda x + (1-\lambda) x').$$ 下面我们定义一些函数,包括凸函数和非凸函数。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow f = lambda x: 0.5 * x**2 # 凸函数 g = lambda x: d2l.cos(np.pi * x) # 非凸函数 h = lambda x: d2l.exp(0.5 * x) # 凸函数 @@ -102,6 +112,19 @@ for ax, func in zip(axes, [f, g, h]): d2l.plot([x, segment], [func(x), func(segment)], axes=ax) ``` +```{.python .input} +#@tab paddle +f = lambda x: 0.5 * x**2 # 凸函数 +g = lambda x: d2l.cos(np.pi * x) # 非凸函数 +h = lambda x: d2l.exp(0.5 * x) # 凸函数 + +x, segment = d2l.arange(-2, 2, 0.01, dtype='float32'), d2l.tensor([-1.5, 1]) +d2l.use_svg_display() +_, axes = d2l.plt.subplots(1, 3, figsize=(9, 3)) +for ax, func in zip(axes, [f, g, h]): + d2l.plot([x, segment], [func(x), func(segment)], axes=ax) +``` + 不出所料,余弦函数为非凸的,而抛物线函数和指数函数为凸的。 请注意,为使该条件有意义,$\mathcal{X}$是凸集的要求是必要的。 否则可能无法很好地界定$f(\lambda x + (1-\lambda) x')$的结果。 @@ -166,7 +189,7 @@ d2l.plot([x, segment], [f(x), f(segment)], 'x', 'f(x)') 凸函数的局部极小值同时也是全局极小值这一性质是很方便的。 这意味着如果我们最小化函数,我们就不会“卡住”。 -但是,请注意,这并不意味着不能有多个全局最小值,或者可能不存在一个全局最小值。 +但是请注意,这并不意味着不能有多个全局最小值,或者可能不存在一个全局最小值。 例如,函数$f(x) = \mathrm{max}(|x|-1, 0)$在$[-1,1]$区间上都是最小值。 相反,函数$f(x) = \exp(x)$在$\mathbb{R}$上没有取得最小值。对于$x \to -\infty$,它趋近于$0$,但是没有$f(x) = 0$的$x$。 @@ -326,7 +349,7 @@ $$\mathrm{Proj}_\mathcal{X}(\mathbf{x}) = \mathop{\mathrm{argmin}}_{\mathbf{x}' 图中有两个凸集,一个圆和一个菱形。 两个集合内的点(黄色)在投影期间保持不变。 两个集合(黑色)之外的点投影到集合中接近原始点(黑色)的点(红色)。 -虽然对于$L_2$的球面来说,方向保持不变,但一般情况下不需要这样。 +虽然对$L_2$的球面来说,方向保持不变,但一般情况下不需要这样。 凸投影的一个用途是计算稀疏权重向量。 在本例中,我们将权重向量投影到一个$L_1$的球上, @@ -345,26 +368,21 @@ $$\mathrm{Proj}_\mathcal{X}(\mathbf{x}) = \mathop{\mathrm{argmin}}_{\mathbf{x}' ## 练习 -1. 假设我们想要通过绘制集合内点之间的所有直线并检查这些直线是否包含来验证集合的凸性。 -i.证明只检查边界上的点是充分的。 -ii.证明只检查集合的顶点是充分的。 +1. 假设我们想要通过绘制集合内点之间的所有直线并检查这些直线是否包含来验证集合的凸性。i.证明只检查边界上的点是充分的。ii.证明只检查集合的顶点是充分的。 + 2. 用$p$-范数表示半径为$r$的球,证明$\mathcal{B}_p[r] := \{\mathbf{x} | \mathbf{x} \in \mathbb{R}^d \text{ and } \|\mathbf{x}\|_p \leq r\}$,$\mathcal{B}_p[r]$对于所有$p \geq 1$是凸的。 3. 已知凸函数$f$和$g$表明$\mathrm{max}(f, g)$也是凸函数。证明$\mathrm{min}(f, g)$是非凸的。 4. 证明Softmax函数的规范化是凸的,即$f(x) = \log \sum_i \exp(x_i)$的凸性。 -5. 证明线性子空间$\mathcal{X} = {\mathbf{X} | \mathbf{W} \mathbf{X} = \mathbf{b}}$是凸集。 +5. 证明线性子空间$\mathcal{X} = \{\mathbf{x} | \mathbf{W} \mathbf{x} = \mathbf{b}\}$是凸集。 6. 证明在线性子空间$\mathbf{b} = \mathbf{0}$的情况下,对于矩阵$\mathbf{M}$的投影$\mathrm {Proj} \mathcal{X}$可以写成$\mathbf{M} \mathbf{X}$。 7. 证明对于凸二次可微函数$f$,对于$\xi \in [0, \epsilon]$,我们可以写成$f(x + \epsilon) = f(x) + \epsilon f'(x) + \frac{1}{2} \epsilon^2 f''(x + \xi)$。 -8. 给定一个向量$\mathbf{w} \in \mathbb{R}^d$与$|\mathbf{w}| 1 > 1$计算在$L_1$单位球上的投影。 -i.作为中间步骤,写出惩罚目标$|\mathbf{w} - \mathbf{w}'|_2^2 + \lambda |\mathbf{w}'|_1$,计算给定$\lambda > 0$的解。 -ii.你能无须反复试错就找到$\lambda$的“正确”值吗? - -9. 给定一个凸集$\mathcal{X}$和两个向量$\mathbf{X}$和$\mathbf{y}$证明了投影不会增加距离,即$\|\mathbf{x} - \mathbf{y}\| \geq \|\mathrm{Proj}_\mathcal{X}(\mathbf{x}) - \mathrm{Proj}_\mathcal{X}(\mathbf{y})\|$。 +8. 给定一个凸集$\mathcal{X}$和两个向量$\mathbf{x}$和$\mathbf{y}$证明了投影不会增加距离,即$\|\mathbf{x} - \mathbf{y}\| \geq \|\mathrm{Proj}_\mathcal{X}(\mathbf{x}) - \mathrm{Proj}_\mathcal{X}(\mathbf{y})\|$。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/3814) @@ -377,3 +395,7 @@ ii.你能无须反复试错就找到$\lambda$的“正确”值吗? :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/3816) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11847) +:end_tab: diff --git a/chapter_optimization/gd.md b/chapter_optimization/gd.md index 90d4da07f..d90806296 100644 --- a/chapter_optimization/gd.md +++ b/chapter_optimization/gd.md @@ -68,6 +68,16 @@ import numpy as np import tensorflow as tf ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import numpy as np +import paddle +``` + ```{.python .input} #@tab all def f(x): # 目标函数 @@ -81,7 +91,7 @@ def f_grad(x): # 目标函数的梯度(导数) 使用梯度下降法迭代$x$共10次,我们可以看到,$x$的值最终将接近最优解。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow def gd(eta, f_grad): x = 10.0 results = [x] @@ -94,10 +104,24 @@ def gd(eta, f_grad): results = gd(0.2, f_grad) ``` +```{.python .input} +#@tab paddle +def gd(eta, f_grad): + x = 10.0 + results = [x] + for i in range(10): + x -= eta * f_grad(x) + results.append(float(x)) + print(f'epoch 10, x: {float(x):f}') + return results + +results = gd(0.2, f_grad) +``` + 对进行$x$优化的过程可以绘制如下。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow def show_trace(results, f): n = max(abs(min(results)), abs(max(results))) f_line = d2l.arange(-n, n, 0.01) @@ -108,6 +132,18 @@ def show_trace(results, f): show_trace(results, f) ``` +```{.python .input} +#@tab paddle +def show_trace(results, f): + n = max(abs(min(results)), abs(max(results))) + f_line = d2l.arange(-n, n, 0.01, dtype='float32') + d2l.set_figsize() + d2l.plot([f_line, results], [[f(x) for x in f_line], [ + f(x) for x in results]], 'x', 'f(x)', fmts=['-', '-o']) + +show_trace(results, f) +``` + ### 学习率 :label:`subsec_gd-learningrate` @@ -156,7 +192,7 @@ show_trace(gd(2, f_grad), f) 现在我们对单变量的情况有了更好的理解,让我们考虑一下$\mathbf{x} = [x_1, x_2, \ldots, x_d]^\top$的情况。 即目标函数$f: \mathbb{R}^d \to \mathbb{R}$将向量映射成标量。 -相应地,它的梯度也是多元的:它是一个由$d$个偏导数组成的向量: +相应地,它的梯度也是多元的,它是一个由$d$个偏导数组成的向量: $$\nabla f(\mathbf{x}) = \bigg[\frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_d}\bigg]^\top.$$ @@ -185,7 +221,7 @@ $$\mathbf{x} \leftarrow \mathbf{x} - \eta \nabla f(\mathbf{x}).$$ 第二个函数会显示$\mathbf{x}$的轨迹。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow def train_2d(trainer, steps=20, f_grad=None): #@save """用定制的训练机优化2D目标函数""" # s1和s2是稍后将使用的内部状态变量 @@ -211,6 +247,33 @@ def show_trace_2d(f, results): #@save d2l.plt.ylabel('x2') ``` +```{.python .input} +#@tab paddle +def train_2d(trainer, steps=20, f_grad=None): #@save + """用定制的训练机优化2D目标函数""" + # s1和s2是稍后将使用的内部状态变量 + x1, x2, s1, s2 = -5, -2, 0, 0 + results = [(x1, x2)] + for i in range(steps): + if f_grad: + x1, x2, s1, s2 = trainer(x1, x2, s1, s2, f_grad) + else: + x1, x2, s1, s2 = trainer(x1, x2, s1, s2) + results.append((x1, x2)) + print(f'epoch {i + 1}, x1: {float(x1):f}, x2: {float(x2):f}') + return results + +def show_trace_2d(f, results): #@save + """显示优化过程中2D变量的轨迹""" + d2l.set_figsize() + d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e') + x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1, dtype='float32'), + d2l.arange(-3.0, 1.0, 0.1, dtype='float32')) + d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4') + d2l.plt.xlabel('x1') + d2l.plt.ylabel('x2') +``` + 接下来,我们观察学习率$\eta = 0.1$时优化变量$\mathbf{x}$的轨迹。 可以看到,经过20步之后,$\mathbf{x}$的值接近其位于$[0, 0]$的最小值。 虽然进展相当顺利,但相当缓慢。 @@ -400,15 +463,15 @@ $$\mathbf{x} \leftarrow \mathbf{x} - \eta \mathrm{diag}(\mathbf{H})^{-1} \nabla 1. 用不同的学习率和目标函数进行梯度下降实验。 1. 在区间$[a, b]$中实现线搜索以最小化凸函数。 - 1. 你是否需要导数来进行二分搜索,即决定选择$[a, (a+b)/2]$还是$[(a+b)/2, b]$。 + 1. 是否需要导数来进行二分搜索,即决定选择$[a, (a+b)/2]$还是$[(a+b)/2, b]$。 1. 算法的收敛速度有多快? 1. 实现该算法,并将其应用于求$\log (\exp(x) + \exp(-2x -3))$的最小值。 1. 设计一个定义在$\mathbb{R}^2$上的目标函数,它的梯度下降非常缓慢。提示:不同坐标的缩放方式不同。 -1. 使用预处理实现牛顿方法的轻量级版本: - 1. 使用对角Hessian作为预条件。 +1. 使用预处理实现牛顿方法的轻量版本。 + 1. 使用对角Hessian作为预条件子。 1. 使用它的绝对值,而不是实际值(可能有符号)。 1. 将此应用于上述问题。 -1. 将上述算法应用于多个目标函数(凸或非凸)。如果你把坐标旋转$45$度会怎么样? +1. 将上述算法应用于多个目标函数(凸或非凸)。如果把坐标旋转$45$度会怎么样? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/3834) @@ -421,3 +484,7 @@ $$\mathbf{x} \leftarrow \mathbf{x} - \eta \mathrm{diag}(\mathbf{H})^{-1} \nabla :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/3835) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11848) +:end_tab: \ No newline at end of file diff --git a/chapter_optimization/index.md b/chapter_optimization/index.md index 91c01d40a..394d6f63e 100644 --- a/chapter_optimization/index.md +++ b/chapter_optimization/index.md @@ -1,11 +1,11 @@ # 优化算法 :label:`chap_optimization` -如果您在此之前按顺序阅读这本书,则已经使用了许多优化算法来训练深度学习模型。这些工具使我们能够继续更新模型参数,并使损失函数的值最小化,正如在训练集上评估的那样。事实上,任何满足于将优化视为黑盒装置,以便在简单的设置中最小化目标函数的人,都可能会知道存在着一系列此类程序的咒语(名称如“SGD”和“Adam”)。 +截止到目前,本书已经使用了许多优化算法来训练深度学习模型。优化算法使我们能够继续更新模型参数,并使损失函数的值最小化。这就像在训练集上评估一样。事实上,任何满足于将优化视为黑盒装置,以在简单的设置中最小化目标函数的人,都可能会知道存在着一系列此类“咒语”(名称如“SGD”和“Adam”)。 -但是,为了做得好,还需要更深入的知识。优化算法对于深度学习非常重要。一方面,训练复杂的深度学习模型可能需要数小时、几天甚至数周。优化算法的性能直接影响模型的训练效率。另一方面,了解不同优化算法的原则及其超参数的作用将使我们能够以有针对性的方式调整超参数,以提高深度学习模型的性能。 +但是,为了做得更好,还需要更深入的知识。优化算法对于深度学习非常重要。一方面,训练复杂的深度学习模型可能需要数小时、几天甚至数周。优化算法的性能直接影响模型的训练效率。另一方面,了解不同优化算法的原则及其超参数的作用将使我们能够以有针对性的方式调整超参数,以提高深度学习模型的性能。 -在本章中,我们深入探讨常见的深度学习优化算法。深度学习中出现的几乎所有优化问题都是*非凸*的。尽管如此,在*凸*问题背景下设计和分析算法是非常有启发性的。正是出于这个原因,本章包括了凸优化的入门,以及凸目标函数上非常简单的随机梯度下降算法的证明。 +在本章中,我们深入探讨常见的深度学习优化算法。深度学习中出现的几乎所有优化问题都是*非凸*的。尽管如此,在*凸问题*背景下设计和分析算法是非常有启发性的。正是出于这个原因,本章包括了凸优化的入门,以及凸目标函数上非常简单的随机梯度下降算法的证明。 ```toc :maxdepth: 2 diff --git a/chapter_optimization/lr-scheduler.md b/chapter_optimization/lr-scheduler.md index 2f6f141ab..3db88b631 100644 --- a/chapter_optimization/lr-scheduler.md +++ b/chapter_optimization/lr-scheduler.md @@ -182,6 +182,72 @@ def train(net_fn, train_iter, test_iter, num_epochs, lr, return net ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +from paddle import nn +from paddle.optimizer import lr as lr_scheduler + +def net_fn(): + model = nn.Sequential( + nn.Conv2D(1, 6, kernel_size=5, padding=2), nn.ReLU(), + nn.MaxPool2D(kernel_size=2, stride=2), + nn.Conv2D(6, 16, kernel_size=5), nn.ReLU(), + nn.MaxPool2D(kernel_size=2, stride=2), + nn.Flatten(), + nn.Linear(16 * 5 * 5, 120), nn.ReLU(), + nn.Linear(120, 84), nn.ReLU(), + nn.Linear(84, 10)) + + return model + +loss = nn.CrossEntropyLoss() +device = d2l.try_gpu() + +batch_size = 256 +train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size) + +# 代码几乎与d2l.train_ch6定义在卷积神经网络一章LeNet一节中的相同 +def train(net, train_iter, test_iter, num_epochs, loss, trainer, device, + scheduler=None): + animator = d2l.Animator(xlabel='epoch', xlim=[0, num_epochs], + legend=['train loss', 'train acc', 'test acc']) + + for epoch in range(num_epochs): + metric = d2l.Accumulator(3) # train_loss,train_acc,num_examples + for i, (X, y) in enumerate(train_iter): + net.train() + trainer.clear_grad() + y_hat = net(X) + l = loss(y_hat, y) + l.backward() + trainer.step() + with paddle.no_grad(): + metric.add(l * X.shape[0], d2l.accuracy(y_hat,y), X.shape[0]) + train_loss = metric[0] / metric[2] + train_acc = metric[1] / metric[2] + if (i + 1) % 50 == 0: + animator.add(epoch + i / len(train_iter), + (train_loss, train_acc, None)) + + test_acc = d2l.evaluate_accuracy_gpu(net, test_iter) + animator.add(epoch+1, (None, None, test_acc)) + + if scheduler: + if scheduler.__module__ == lr_scheduler.__name__: + # UsingPaddleIn-Builtscheduler + scheduler.step() + else: + # Usingcustomdefinedscheduler + trainer.set_lr(scheduler(epoch)) + print(f'train loss {train_loss:.3f}, train acc {train_acc:.3f}, 'f'test acc {test_acc:.3f}') +``` + 让我们来看看如果使用默认设置,调用此算法会发生什么。 例如设学习率为$0.3$并训练$30$次迭代。 留意在超过了某点、测试准确度方面的进展停滞时,训练准确度将如何继续提高。 @@ -208,6 +274,14 @@ lr, num_epochs = 0.3, 30 train(net, train_iter, test_iter, num_epochs, lr) ``` +```{.python .input} +#@tab paddle +lr, num_epochs = 0.3, 30 +net = net_fn() +trainer = paddle.optimizer.SGD(learning_rate=lr, parameters=net.parameters()) +train(net, train_iter, test_iter, num_epochs, loss, trainer, device) +``` + ## 学习率调度器 我们可以在每个迭代轮数(甚至在每个小批量)之后向下调整学习率。 @@ -233,6 +307,13 @@ dummy_model.compile(tf.keras.optimizers.SGD(learning_rate=lr), loss='mse') print(f'learning rate is now ,', dummy_model.optimizer.lr.numpy()) ``` +```{.python .input} +#@tab paddle +lr = 0.1 +trainer.set_lr(lr) +print(f'learning rate is now {trainer.get_lr():.2f}') +``` + 更通常而言,我们应该定义一个调度器。 当调用更新次数时,它将返回学习率的适当值。 让我们定义一个简单的方法,将学习率设置为$\eta = \eta_0 (t + 1)^{-\frac{1}{2}}$。 @@ -278,6 +359,14 @@ train(net, train_iter, test_iter, num_epochs, lr, custom_callback=LearningRateScheduler(scheduler)) ``` +```{.python .input} +#@tab paddle +net = net_fn() +trainer = paddle.optimizer.SGD(learning_rate=lr , parameters=net.parameters()) +train(net, train_iter, test_iter, num_epochs, loss, trainer, device, + scheduler) +``` + 这比以前好一些:曲线比以前更加平滑,并且过拟合更小了。 遗憾的是,关于为什么在理论上某些策略会导致较轻的过拟合,有一些观点认为,较小的步长将导致参数更接近零,因此更简单。 但是,这并不能完全解释这种现象,因为我们并没有真正地提前停止,而只是轻柔地降低了学习率。 @@ -360,6 +449,21 @@ scheduler = MultiFactorScheduler(step=[15, 30], factor=0.5, base_lr=0.5) d2l.plot(d2l.arange(num_epochs), [scheduler(t) for t in range(num_epochs)]) ``` +```{.python .input} +#@tab paddle +net = net_fn() +scheduler =paddle.optimizer.lr.MultiStepDecay(learning_rate=0.5, milestones=[15,30], gamma=0.5) +trainer = paddle.optimizer.SGD(learning_rate=scheduler, parameters=net.parameters()) +def get_lr(trainer, scheduler): + lr=trainer.state_dict()['LR_Scheduler']['last_lr'] + trainer.step() + scheduler.step() + return lr + +d2l.plot(paddle.arange(num_epochs), [get_lr(trainer, scheduler) + for t in range(num_epochs)]) +``` + 这种分段恒定学习率调度背后的直觉是,让优化持续进行,直到权重向量的分布达到一个驻点。 此时,我们才将学习率降低,以获得更高质量的代理来达到一个良好的局部最小值。 下面的例子展示了如何使用这种方法产生更好的解决方案。 @@ -382,6 +486,12 @@ train(net, train_iter, test_iter, num_epochs, lr, custom_callback=LearningRateScheduler(scheduler)) ``` +```{.python .input} +#@tab paddle +train(net, train_iter, test_iter, num_epochs, loss, trainer, device, + scheduler) +``` + ### 余弦调度器 余弦调度器是 :cite:`Loshchilov.Hutter.2016`提出的一种启发式算法。 @@ -401,7 +511,7 @@ d2l.plot(d2l.arange(num_epochs), [scheduler(t) for t in range(num_epochs)]) ``` ```{.python .input} -#@tab pytorch, tensorflow +#@tab pytorch, tensorflow, paddle class CosineScheduler: def __init__(self, max_update, base_lr=0.01, final_lr=0, warmup_steps=0, warmup_begin_lr=0): @@ -453,10 +563,18 @@ train(net, train_iter, test_iter, num_epochs, lr, custom_callback=LearningRateScheduler(scheduler)) ``` +```{.python .input} +#@tab paddle +net = net_fn() +trainer = paddle.optimizer.SGD(learning_rate=0.3, parameters=net.parameters()) +train(net, train_iter, test_iter, num_epochs, loss, trainer, device, + scheduler) +``` + ### 预热 在某些情况下,初始化参数不足以得到良好的解。 -这对于某些高级网络设计来说尤其棘手,可能导致不稳定的优化结果。 +这对某些高级网络设计来说尤其棘手,可能导致不稳定的优化结果。 对此,一方面,我们可以选择一个足够小的学习率, 从而防止一开始发散,然而这样进展太缓慢。 另一方面,较高的学习率最初就会导致发散。 @@ -472,7 +590,7 @@ d2l.plot(np.arange(num_epochs), [scheduler(t) for t in range(num_epochs)]) ``` ```{.python .input} -#@tab pytorch, tensorflow +#@tab pytorch, tensorflow, paddle scheduler = CosineScheduler(20, warmup_steps=5, base_lr=0.3, final_lr=0.01) d2l.plot(d2l.arange(num_epochs), [scheduler(t) for t in range(num_epochs)]) ``` @@ -499,9 +617,17 @@ train(net, train_iter, test_iter, num_epochs, lr, custom_callback=LearningRateScheduler(scheduler)) ``` +```{.python .input} +#@tab paddle +net = net_fn() +trainer = paddle.optimizer.SGD(learning_rate=0.3, parameters=net.parameters()) +train(net, train_iter, test_iter, num_epochs, loss, trainer, device, + scheduler) +``` + 预热可以应用于任何调度器,而不仅仅是余弦。 有关学习率调度的更多实验和更详细讨论,请参阅 :cite:`Gotmare.Keskar.Xiong.ea.2018`。 -其中,这篇论文的点睛之笔的发现:预热阶段限制了非常深的网络中参数的发散量。 +其中,这篇论文的点睛之笔的发现:预热阶段限制了非常深的网络中参数的发散程度 。 这在直觉上是有道理的:在网络中那些一开始花费最多时间取得进展的部分,随机初始化会产生巨大的发散。 ## 小结 @@ -514,11 +640,11 @@ train(net, train_iter, test_iter, num_epochs, lr, ## 练习 -1. 试验给定固定学习率的优化行为。这种情况下你可以获得的最佳模型是什么? -1. 如果你改变学习率下降的指数,收敛性会如何改变?在实验中方便起见,使用`PolyScheduler`。 +1. 试验给定固定学习率的优化行为。这种情况下可以获得的最佳模型是什么? +1. 如果改变学习率下降的指数,收敛性会如何改变?在实验中方便起见,使用`PolyScheduler`。 1. 将余弦调度器应用于大型计算机视觉问题,例如训练ImageNet数据集。与其他调度器相比,它如何影响性能? 1. 预热应该持续多长时间? -1. 你能把优化和采样联系起来吗?首先,在随机梯度朗之万动力学上使用 :cite:`Welling.Teh.2011`的结果。 +1. 可以试着把优化和采样联系起来吗?首先,在随机梯度朗之万动力学上使用 :cite:`Welling.Teh.2011`的结果。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/4333) @@ -531,3 +657,7 @@ train(net, train_iter, test_iter, num_epochs, lr, :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/4335) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11856) +:end_tab: diff --git a/chapter_optimization/minibatch-sgd.md b/chapter_optimization/minibatch-sgd.md index 60fb8f784..e8dea527c 100644 --- a/chapter_optimization/minibatch-sgd.md +++ b/chapter_optimization/minibatch-sgd.md @@ -92,6 +92,22 @@ B = tf.Variable(d2l.normal([256, 256], 0, 1)) C = tf.Variable(d2l.normal([256, 256], 0, 1)) ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +import numpy as np + +timer = d2l.Timer() +A = d2l.zeros((256, 256)) +B = d2l.randn((256, 256)) +C = d2l.randn((256, 256)) +``` + 按元素分配只需遍历分别为$\mathbf{B}$和$\mathbf{C}$的所有行和列,即可将该值分配给$\mathbf{A}$。 ```{.python .input} @@ -124,6 +140,16 @@ for i in range(256): timer.stop() ``` +```{.python .input} +#@tab paddle +# 逐元素计算A=BC +timer.start() +for i in range(256): + for j in range(256): + A[i, j] = paddle.dot(B[i, :], C[:, j]) +timer.stop() +``` + 更快的策略是执行按列分配。 ```{.python .input} @@ -152,6 +178,15 @@ for j in range(256): timer.stop() ``` +```{.python .input} +#@tab paddle +# 逐列计算A=BC +timer.start() +for j in range(256): + A[:, j] = paddle.mv(B, C[:, j]) +timer.stop() +``` + 最有效的方法是在一个区块中执行整个操作。让我们看看它们各自的操作速度是多少。 ```{.python .input} @@ -193,6 +228,19 @@ print(f'performance in Gigaflops: element {gigaflops[0]:.3f}, ' f'column {gigaflops[1]:.3f}, full {gigaflops[2]:.3f}') ``` +```{.python .input} +#@tab paddle +# 一次性计算A=BC +timer.start() +A = paddle.mm(B, C) +timer.stop() + +# 乘法和加法作为单独的操作(在实践中融合) +gigaflops = [2/i for i in timer.times] +print(f'performance in Gigaflops: element {gigaflops[0]:.3f}, ' + f'column {gigaflops[1]:.3f}, full {gigaflops[2]:.3f}') +``` + ## 小批量 :label:`sec_minibatches` @@ -246,6 +294,15 @@ timer.stop() print(f'performance in Gigaflops: block {2 / timer.times[3]:.3f}') ``` +```{.python .input} +#@tab paddle +timer.start() +for j in range(0, 256, 64): + A[:, j:j+64] = paddle.mm(B, C[:, j:j+64]) +timer.stop() +print(f'performance in Gigaflops: block {2 / timer.times[3]:.3f}') +``` + 显而易见,小批量上的计算基本上与完整矩阵一样有效。 需要注意的是,在 :numref:`sec_batch_norm`中,我们使用了一种在很大程度上取决于小批量中的方差的正则化。 随着后者增加,方差会减少,随之而来的是批量规范化带来的噪声注入的好处。 @@ -305,6 +362,22 @@ def get_data_ch11(batch_size=10, n=1500): return data_iter, data.shape[1]-1 ``` +```{.python .input} +#@tab paddle +#@save +d2l.DATA_HUB['airfoil'] = (d2l.DATA_URL + 'airfoil_self_noise.dat', + '76e5be1548fd8222e5074cf0faae75edff8cf93f') + +#@save +def get_data_ch11(batch_size=10, n=1500): + data = np.genfromtxt(d2l.download('airfoil'), + dtype=np.float32, delimiter='\t') + data = d2l.tensor((data - data.mean(axis=0)) / data.std(axis=0)) + data_iter = d2l.load_array((data[:n, :-1], data[:n, -1]), + batch_size, is_train=True) + return data_iter, data.shape[1]-1 +``` + ## 从零开始实现 :numref:`sec_linear_scratch`一节中已经实现过小批量随机梯度下降算法。 @@ -333,6 +406,18 @@ def sgd(params, grads, states, hyperparams): param.assign_sub(hyperparams['lr']*grad) ``` +```{.python .input} +#@tab paddle +def sgd(params, states, hyperparams): + a = [] + with paddle.no_grad(): + for p in params: + p = p - hyperparams['lr'] * p.grad + p.stop_gradient = False + a.append(p) + return a +``` + 下面实现一个通用的训练函数,以方便本章后面介绍的其他优化算法使用。 它初始化了一个线性回归模型,然后可以使用小批量随机梯度下降以及后续小节介绍的其他算法来训练模型。 @@ -430,6 +515,34 @@ def train_ch11(trainer_fn, states, hyperparams, data_iter, return timer.cumsum(), animator.Y[0] ``` +```{.python .input} +#@tab paddle +#@save +def train_ch11(trainer_fn, states, hyperparams, data_iter, + feature_dim, num_epochs=2): + # 初始化模型 + w = d2l.tensor(d2l.normal(mean=0.0, std=0.01, shape=(feature_dim, 1)), stop_gradient=False) + b = d2l.tensor(d2l.zeros((1,)), stop_gradient=False) + net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss + # 训练模型 + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + l = loss(net(X), y).mean() + l.backward() + w, b = trainer_fn([w, b], states, hyperparams) + n += X.shape[0] + if n % 200 == 0: + timer.stop() + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss),)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + return timer.cumsum(), animator.Y[0] +``` + 让我们来看看批量梯度下降的优化是如何进行的。 这可以通过将小批量设置为1500(即样本总数)来实现。 因此,模型参数每个迭代轮数只迭代一次。 @@ -585,6 +698,41 @@ def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=2): print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') ``` +```{.python .input} +#@tab paddle +#@save +def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=4): + # 初始化模型 + net = nn.Sequential(nn.Linear(5, 1)) + def init_weights(m): + if type(m) == nn.Linear: + paddle.nn.initializer.Normal(m.weight, std=0.01) + + net.apply(init_weights) + + optimizer = trainer_fn(parameters=net.parameters(), **hyperparams) + loss = nn.MSELoss(reduction='none') + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + optimizer.clear_grad() + out = net(X) + y = y.reshape(out.shape) + l = loss(out, y) + l.mean().backward() + optimizer.step() + n += X.shape[0] + if n % 200 == 0: + timer.stop() + # MSELoss计算平方误差时不带系数1/2 + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss) / 2,)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') +``` + 下面使用这个训练函数,复现之前的实验。 ```{.python .input} @@ -606,6 +754,13 @@ trainer = tf.keras.optimizers.SGD train_concise_ch11(trainer, {'learning_rate': 0.05}, data_iter) ``` +```{.python .input} +#@tab paddle +data_iter, _ = get_data_ch11(10) +trainer = paddle.optimizer.SGD +train_concise_ch11(trainer, {'learning_rate': 0.01}, data_iter) +``` + ## 小结 * 由于减少了深度学习框架的额外开销,使用更好的内存定位以及CPU和GPU上的缓存,向量化使代码更加高效。 @@ -618,7 +773,7 @@ train_concise_ch11(trainer, {'learning_rate': 0.05}, data_iter) 1. 修改批量大小和学习率,并观察目标函数值的下降率以及每个迭代轮数消耗的时间。 1. 将小批量随机梯度下降与实际从训练集中*取样替换*的变体进行比较。会看出什么? -1. 一个邪恶的精灵在没通知你的情况下复制了你的数据集(即每个观测发生两次,你的数据集增加到原始大小的两倍,但没有人告诉你)。随机梯度下降、小批量随机梯度下降和梯度下降的表现将如何变化? +1. 一个邪恶的精灵在没通知你的情况下复制了你的数据集(即每个观测发生两次,数据集增加到原始大小的两倍,但没有人告诉你)。随机梯度下降、小批量随机梯度下降和梯度下降的表现将如何变化? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/4324) @@ -631,3 +786,7 @@ train_concise_ch11(trainer, {'learning_rate': 0.05}, data_iter) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/4326) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11850) +:end_tab: \ No newline at end of file diff --git a/chapter_optimization/momentum.md b/chapter_optimization/momentum.md index 493ef0339..12859a5ff 100644 --- a/chapter_optimization/momentum.md +++ b/chapter_optimization/momentum.md @@ -8,7 +8,7 @@ ## 基础 -在本节中,我们将探讨更有效的优化算法,尤其是针对实验中常见的某些类型的优化问题。 +本节将探讨更有效的优化算法,尤其是针对实验中常见的某些类型的优化问题。 ### 泄漏平均值 @@ -109,6 +109,23 @@ def gd_2d(x1, x2, s1, s2): d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d)) ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle + +eta = 0.4 +def f_2d(x1, x2): + return 0.1 * x1 ** 2 + 2 * x2 ** 2 +def gd_2d(x1, x2, s1, s2): + return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0) + +d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d)) +``` + 从构造来看,$x_2$方向的梯度比水平$x_1$方向的梯度大得多,变化也快得多。 因此,我们陷入两难:如果选择较小的学习率,我们会确保解不会在$x_2$方向发散,但要承受在$x_1$方向的缓慢收敛。相反,如果学习率较高,我们在$x_1$方向上进展很快,但在$x_2$方向将会发散。 下面的例子说明了即使学习率从$0.4$略微提高到$0.6$,也会发生变化。 @@ -195,7 +212,7 @@ d2l.plt.legend(); 在下面的实现中,我们称这些变量为`states`。 ```{.python .input} -#@tab mxnet,pytorch +#@tab mxnet, pytorch def init_momentum_states(feature_dim): v_w = d2l.zeros((feature_dim, 1)) v_b = d2l.zeros(1) @@ -210,6 +227,14 @@ def init_momentum_states(features_dim): return (v_w, v_b) ``` +```{.python .input} +#@tab paddle +def init_momentum_states(feature_dim): + v_w = d2l.zeros((feature_dim, 1)) + v_b = d2l.zeros([1]) + return (v_w, v_b) +``` + ```{.python .input} def sgd_momentum(params, states, hyperparams): for p, v in zip(params, states): @@ -235,6 +260,19 @@ def sgd_momentum(params, grads, states, hyperparams): p[:].assign(p - hyperparams['lr'] * v) ``` +```{.python .input} +#@tab paddle +def sgd_momentum(params, states, hyperparams): + a = [] + for p, v in zip(params, states): + with paddle.no_grad(): + v[:] = hyperparams['momentum'] * v + p.grad + p[:] -= hyperparams['lr'] * v + p.grad.zero_() + a.append(p) + return a +``` + 让我们看看它在实验中是如何运作的。 ```{.python .input} @@ -285,6 +323,12 @@ d2l.train_concise_ch11(trainer, {'learning_rate': 0.005, 'momentum': 0.9}, data_iter) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.Momentum +d2l.train_concise_ch11(trainer, {'learning_rate': 0.005, 'momentum': 0.9}, data_iter) +``` + ## 理论分析 $f(x) = 0.1 x_1^2 + 2 x_2^2$的2D示例似乎相当牵强。 @@ -381,7 +425,7 @@ $$ ## 练习 1. 使用动量超参数和学习率的其他组合,观察和分析不同的实验结果。 -1. 试试梯度下降和动量法来解决一个二次问题,其中你有多个特征值,即$f(x) = \frac{1}{2} \sum_i \lambda_i x_i^2$,例如$\lambda_i = 2^{-i}$。绘制出$x$的值在初始化$x_i = 1$时如何下降。 +1. 试试梯度下降和动量法来解决一个二次问题,其中有多个特征值,即$f(x) = \frac{1}{2} \sum_i \lambda_i x_i^2$,例如$\lambda_i = 2^{-i}$。绘制出$x$的值在初始化$x_i = 1$时如何下降。 1. 推导$h(\mathbf{x}) = \frac{1}{2} \mathbf{x}^\top \mathbf{Q} \mathbf{x} + \mathbf{x}^\top \mathbf{c} + b$的最小值和最小化器。 1. 当我们执行带动量法的随机梯度下降时会有什么变化?当我们使用带动量法的小批量随机梯度下降时会发生什么?试验参数如何? @@ -396,3 +440,7 @@ $$ :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/4329) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11851) +:end_tab: \ No newline at end of file diff --git a/chapter_optimization/optimization-intro.md b/chapter_optimization/optimization-intro.md index 43bca4dea..cacee88f3 100644 --- a/chapter_optimization/optimization-intro.md +++ b/chapter_optimization/optimization-intro.md @@ -1,10 +1,10 @@ # 优化和深度学习 -在本节中,我们将讨论优化与深度学习之间的关系以及在深度学习中使用优化的挑战。对于深度学习问题,我们通常会先定义*损失函数*。一旦我们有了损失函数,我们就可以使用优化算法来尝试最小化损失。在优化中,损失函数通常被称为优化问题的*目标函数*。按照传统和惯则,大多数优化算法都关注的是*最小化*。如果我们需要最大化目标,那么有一个简单的解决方案:在目标函数前加负号即可。 +本节将讨论优化与深度学习之间的关系以及在深度学习中使用优化的挑战。对于深度学习问题,我们通常会先定义*损失函数*。一旦我们有了损失函数,我们就可以使用优化算法来尝试最小化损失。在优化中,损失函数通常被称为优化问题的*目标函数*。按照传统惯例,大多数优化算法都关注的是*最小化*。如果我们需要最大化目标,那么有一个简单的解决方案:在目标函数前加负号即可。 ## 优化的目标 -尽管优化提供了一种最大限度地减少深度学习损失函数的方法,但实质上,优化和深度学习的目标是根本不同的。前者主要关注的是最小化目标,后者则关注在给定有限数据量的情况下寻找合适的模型。在 :numref:`sec_model_selection`中,我们详细讨论了这两个目标之间的区别。例如,训练误差和泛化误差通常不同:由于优化算法的目标函数通常是基于训练数据集的损失函数,因此优化的目标是减少训练误差。但是,深度学习(或更广义地说,统计推断)的目标是减少泛化误差。为了实现后者,除了使用优化算法来减少训练误差之外,我们还需要注意过拟合。 +尽管优化提供了一种最大限度地减少深度学习损失函数的方法,但本质上,优化和深度学习的目标是根本不同的。前者主要关注的是最小化目标,后者则关注在给定有限数据量的情况下寻找合适的模型。在 :numref:`sec_model_selection`中,我们详细讨论了这两个目标之间的区别。例如,训练误差和泛化误差通常不同:由于优化算法的目标函数通常是基于训练数据集的损失函数,因此优化的目标是减少训练误差。但是,深度学习(或更广义地说,统计推断)的目标是减少泛化误差。为了实现后者,除了使用优化算法来减少训练误差之外,我们还需要注意过拟合。 ```{.python .input} %matplotlib inline @@ -32,7 +32,18 @@ from mpl_toolkits import mplot3d import tensorflow as tf ``` -为了说明上述不同的目标,让我们考虑经验风险和风险。如 :numref:`subsec_empirical-risk-and-risk`所述,经验风险是训练数据集的平均损失,而风险则是整个数据群的预期损失。下面我们定义了两个函数:风险函数`f`和经验风险函数`g`。假设我们只有有限量的训练数据。因此,这里的`g`不如`f`平滑。 +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import numpy as np +from mpl_toolkits import mplot3d +import paddle +``` + +为了说明上述不同的目标,引入两个概念*风险*和*经验风险*。如 :numref:`subsec_empirical-risk-and-risk`所述,经验风险是训练数据集的平均损失,而风险则是整个数据群的预期损失。下面我们定义了两个函数:风险函数`f`和经验风险函数`g`。假设我们只有有限的训练数据。因此,这里的`g`不如`f`平滑。 ```{.python .input} #@tab all @@ -46,7 +57,7 @@ def g(x): 下图说明,训练数据集的最低经验风险可能与最低风险(泛化误差)不同。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow def annotate(text, xy, xytext): #@save d2l.plt.gca().annotate(text, xy=xy, xytext=xytext, arrowprops=dict(arrowstyle='->')) @@ -58,15 +69,28 @@ annotate('min of\nempirical risk', (1.0, -1.2), (0.5, -1.1)) annotate('min of risk', (1.1, -1.05), (0.95, -0.5)) ``` +```{.python .input} +#@tab paddle +def annotate(text, xy, xytext): #@save + d2l.plt.gca().annotate(text, xy=xy, xytext=xytext, + arrowprops=dict(arrowstyle='->')) + +x = d2l.arange(0.5, 1.5, 0.01, dtype='float32') +d2l.set_figsize((4.5, 2.5)) +d2l.plot(x, [f(x), g(x)], 'x', 'risk') +annotate('min of\nempirical risk', (1.0, -1.2), (0.5, -1.1)) +annotate('min of risk', (1.1, -1.05), (0.95, -0.5)) +``` + ## 深度学习中的优化挑战 -在本章中,我们将特别关注优化算法在最小化目标函数方面的性能,而不是模型的泛化误差。在 :numref:`sec_linear_regression`中,我们区分了优化问题中的解析解和数值解。在深度学习中,大多数目标函数都很复杂,没有解析解。相反,我们必须使用数值优化算法。本章中的优化算法都属于此类别。 +本章将关注优化算法在最小化目标函数方面的性能,而不是模型的泛化误差。在 :numref:`sec_linear_regression`中,我们区分了优化问题中的解析解和数值解。在深度学习中,大多数目标函数都很复杂,没有解析解。相反,我们必须使用数值优化算法。本章中的优化算法都属于此类别。 -深度学习优化存在许多挑战。其中一些最令人烦恼的是局部最小值、鞍点和梯度消失。让我们来看看它们。 +深度学习优化存在许多挑战。其中最令人烦恼的是局部最小值、鞍点和梯度消失。 ### 局部最小值 -对于任何目标函数$f(x)$,如果在$x$处对应的$f(x)$值小于在$x$附近任何其他点的$f(x)$值,那么$f(x)$可能是局部最小值。如果$f(x)$在$x$处的值是整个域上目标函数的最小值,那么$f(x)$是全局最小值。 +对于任何目标函数$f(x)$,如果在$x$处对应的$f(x)$值小于在$x$附近任意其他点的$f(x)$值,那么$f(x)$可能是局部最小值。如果$f(x)$在$x$处的值是整个域中目标函数的最小值,那么$f(x)$是全局最小值。 例如,给定函数 @@ -75,27 +99,42 @@ $$f(x) = x \cdot \text{cos}(\pi x) \text{ for } -1.0 \leq x \leq 2.0,$$ 我们可以近似该函数的局部最小值和全局最小值。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow x = d2l.arange(-1.0, 2.0, 0.01) d2l.plot(x, [f(x), ], 'x', 'f(x)') annotate('local minimum', (-0.3, -0.25), (-0.77, -1.0)) annotate('global minimum', (1.1, -0.95), (0.6, 0.8)) ``` -深度学习模型的目标函数通常有许多局部最优解。当优化问题的数值解接近局部最优值时,随着目标函数解的梯度接近或变为零,通过最终迭代获得的数值解可能仅使目标函数*局部*最优,而不是*全局*最优。只有一定程度的噪声可能会使参数超出局部最小值。事实上,这是小批量随机梯度下降的有利特性之一,在这种情况下,小批量上梯度的自然变化能够将参数从局部极小值中移出。 +```{.python .input} +#@tab paddle +x = d2l.arange(-1.0, 2.0, 0.01, dtype='float32') +d2l.plot(x, [f(x), ], 'x', 'f(x)') +annotate('local minimum', (-0.3, -0.25), (-0.77, -1.0)) +annotate('global minimum', (1.1, -0.95), (0.6, 0.8)) +``` + +深度学习模型的目标函数通常有许多局部最优解。当优化问题的数值解接近局部最优值时,随着目标函数解的梯度接近或变为零,通过最终迭代获得的数值解可能仅使目标函数*局部*最优,而不是*全局*最优。只有一定程度的噪声可能会使参数跳出局部最小值。事实上,这是小批量随机梯度下降的有利特性之一。在这种情况下,小批量上梯度的自然变化能够将参数从局部极小值中跳出。 ### 鞍点 -除了局部最小值之外,鞍点也是梯度消失的另一个原因。*鞍点*(saddle point)是指函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。考虑这个函数$f(x) = x^3$。它的一阶和二阶导数在$x=0$时消失。这时优化可能会停止,尽管它不是最小值。 +除了局部最小值之外,鞍点是梯度消失的另一个原因。*鞍点*(saddle point)是指函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。考虑这个函数$f(x) = x^3$。它的一阶和二阶导数在$x=0$时消失。这时优化可能会停止,尽管它不是最小值。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow x = d2l.arange(-2.0, 2.0, 0.01) d2l.plot(x, [x**3], 'x', 'f(x)') annotate('saddle point', (0, -0.2), (-0.52, -5.0)) ``` -如下例所示,较高维度的鞍点甚至更加隐蔽。考虑这个函数$f(x, y) = x^2 - y^2$。它的鞍点为$(0, 0)$。这是关于$y$的最大值,也是关于$x$的最小值。此外,它*看起来*像马鞍,这就是这个数学属性的名字由来。 +```{.python .input} +#@tab paddle +x = d2l.arange(-2.0, 2.0, 0.01, dtype='float32') +d2l.plot(x, [x**3], 'x', 'f(x)') +annotate('saddle point', (0, -0.2), (-0.52, -5.0)) +``` + +如下例所示,较高维度的鞍点甚至更加隐蔽。考虑这个函数$f(x, y) = x^2 - y^2$。它的鞍点为$(0, 0)$。这是关于$y$的最大值,也是关于$x$的最小值。此外,它看起来像个马鞍,这就是鞍点的名字由来。 ```{.python .input} #@tab mxnet @@ -115,7 +154,7 @@ d2l.plt.ylabel('y'); ``` ```{.python .input} -#@tab pytorch, tensorflow +#@tab pytorch, tensorflow, paddle x, y = d2l.meshgrid( d2l.linspace(-1.0, 1.0, 101), d2l.linspace(-1.0, 1.0, 101)) z = x**2 - y**2 @@ -131,32 +170,39 @@ d2l.plt.xlabel('x') d2l.plt.ylabel('y'); ``` -我们假设函数的输入是$k$维向量,其输出是标量,因此其Hessian矩阵(也称黑塞矩阵)将有$k$特征值(参考[online appendix on eigendecompositions](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/eigendecomposition.html))。函数的解决方案可以是局部最小值、局部最大值或函数梯度为零的位置处的鞍点: +我们假设函数的输入是$k$维向量,其输出是标量,因此其Hessian矩阵(也称黑塞矩阵)将有$k$个特征值(参考[特征分解的在线附录](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/eigendecomposition.html))。函数的解可能是局部最小值、局部最大值或函数梯度为零位置处的鞍点: -* 当函数在零梯度位置处的Hessian矩阵的特征值全部为正值时,我们有该函数的局部最小值。 -* 当函数在零梯度位置处的Hessian矩阵的特征值全部为负值时,我们有该函数的局部最大值。 -* 当函数在零梯度位置处的Hessian矩阵的特征值为负值和正值时,我们对函数有一个鞍点。 +* 当函数在零梯度位置处的Hessian矩阵的特征值全部为正值时,我们有该函数的局部最小值; +* 当函数在零梯度位置处的Hessian矩阵的特征值全部为负值时,我们有该函数的局部最大值; +* 当函数在零梯度位置处的Hessian矩阵的特征值为负值和正值时,我们有该函数的一个鞍点。 -对于高维度问题,至少*部分*特征值为负的可能性相当高。这使得鞍点比局部最小值更有可能。我们将在下一节介绍凸性时讨论这种情况的一些例外情况。简而言之,凸函数是Hessian函数的特征值永远不是负值的函数。不幸的是,大多数深度学习问题并不属于这个类别。尽管如此,它还是研究优化算法的一个很好的工具。 +对于高维度问题,至少*部分*特征值为负的可能性相当高。这使得鞍点比局部最小值更有可能出现。我们将在下一节介绍凸性时讨论这种情况的一些例外。简而言之,凸函数是Hessian函数的特征值永远不为负值的函数。不幸的是,大多数深度学习问题并不属于这一类。尽管如此,它还是研究优化算法的一个很好的工具。 ### 梯度消失 -可能遇到的最隐蔽的问题是梯度消失。回想一下我们在 :numref:`subsec_activation_functions`中常用的激活函数及其衍生函数。例如,假设我们想最小化函数$f(x) = \tanh(x)$,然后我们恰好从$x = 4$开始。正如我们所看到的那样,$f$的梯度接近零。更具体地说,$f'(x) = 1 - \tanh^2(x)$,因此是$f'(4) = 0.0013$。因此,在我们取得进展之前,优化将会停滞很长一段时间。事实证明,这是在引入ReLU激活函数之前训练深度学习模型相当棘手的原因之一。 +可能遇到的最隐蔽问题是梯度消失。回想一下我们在 :numref:`subsec_activation_functions`中常用的激活函数及其衍生函数。例如,假设我们想最小化函数$f(x) = \tanh(x)$,然后我们恰好从$x = 4$开始。正如我们所看到的那样,$f$的梯度接近零。更具体地说,$f'(x) = 1 - \tanh^2(x)$,因此是$f'(4) = 0.0013$。因此,在我们取得进展之前,优化将会停滞很长一段时间。事实证明,这是在引入ReLU激活函数之前训练深度学习模型相当棘手的原因之一。 ```{.python .input} -#@tab all +#@tab mxnet, pytorch, tensorflow x = d2l.arange(-2.0, 5.0, 0.01) d2l.plot(x, [d2l.tanh(x)], 'x', 'f(x)') annotate('vanishing gradient', (4, 1), (2, 0.0)) ``` -正如我们所看到的那样,深度学习的优化充满挑战。幸运的是,有一系列强大的算法表现良好,即使对于初学者也很容易使用。此外,没有必要找到最佳解决方案。局部最优解或其近似解仍然非常有用。 +```{.python .input} +#@tab paddle +x = d2l.arange(-2.0, 5.0, 0.01, dtype='float32') +d2l.plot(x, [d2l.tanh(x)], 'x', 'f(x)') +annotate('vanishing gradient', (4, 1), (2, 0.0)) +``` + +正如我们所看到的那样,深度学习的优化充满挑战。幸运的是,有一系列强大的算法表现良好,即使对于初学者也很容易使用。此外,没有必要找到最优解。局部最优解或其近似解仍然非常有用。 ## 小结 * 最小化训练误差并*不能*保证我们找到最佳的参数集来最小化泛化误差。 * 优化问题可能有许多局部最小值。 -* 问题可能有更多的鞍点,因为通常问题不是凸的。 +* 一个问题可能有很多的鞍点,因为问题通常不是凸的。 * 梯度消失可能会导致优化停滞,重参数化通常会有所帮助。对参数进行良好的初始化也可能是有益的。 ## 练习 @@ -168,7 +214,7 @@ annotate('vanishing gradient', (4, 1), (2, 0.0)) 1. 你能想到深度学习优化还涉及哪些其他挑战? 1. 假设你想在(真实的)鞍上平衡一个(真实的)球。 1. 为什么这很难? - 1. 你也能利用这种效应来优化算法吗? + 1. 能利用这种效应来优化算法吗? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/3840) @@ -181,3 +227,7 @@ annotate('vanishing gradient', (4, 1), (2, 0.0)) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/3842) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11846) +:end_tab: \ No newline at end of file diff --git a/chapter_optimization/rmsprop.md b/chapter_optimization/rmsprop.md index 228e1b494..e0209d6e8 100644 --- a/chapter_optimization/rmsprop.md +++ b/chapter_optimization/rmsprop.md @@ -10,7 +10,7 @@ 因此,由于缺乏规范化,没有约束力,$\mathbf{s}_t$持续增长,几乎上是在算法收敛时呈线性递增。 解决此问题的一种方法是使用$\mathbf{s}_t / t$。 -对于$\mathbf{g}_t$的合理分布来说,它将收敛。 +对$\mathbf{g}_t$的合理分布来说,它将收敛。 遗憾的是,限制行为生效可能需要很长时间,因为该流程记住了值的完整轨迹。 另一种方法是按动量法中的方式使用泄漏平均值,即$\mathbf{s}_t \leftarrow \gamma \mathbf{s}_{t-1} + (1-\gamma) \mathbf{g}_t^2$,其中参数$\gamma > 0$。 保持所有其它部分不变就产生了RMSProp算法。 @@ -63,6 +63,15 @@ import tensorflow as tf import math ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import math +``` + ```{.python .input} #@tab all d2l.set_figsize() @@ -99,13 +108,21 @@ d2l.show_trace_2d(f_2d, d2l.train_2d(rmsprop_2d)) 接下来,我们在深度网络中实现RMSProp算法。 ```{.python .input} -#@tab mxnet,pytorch +#@tab mxnet, pytorch def init_rmsprop_states(feature_dim): s_w = d2l.zeros((feature_dim, 1)) s_b = d2l.zeros(1) return (s_w, s_b) ``` +```{.python .input} +#@tab paddle +def init_rmsprop_states(feature_dim): + s_w = d2l.zeros((feature_dim, 1)) + s_b = d2l.zeros([1]) + return (s_w, s_b) +``` + ```{.python .input} #@tab tensorflow def init_rmsprop_states(feature_dim): @@ -142,6 +159,20 @@ def rmsprop(params, grads, states, hyperparams): p[:].assign(p - hyperparams['lr'] * g / tf.math.sqrt(s + eps)) ``` +```{.python .input} +#@tab paddle +def rmsprop(params, states, hyperparams): + a = [] + gamma, eps = hyperparams['gamma'], 1e-6 + for p, s in zip(params, states): + with paddle.no_grad(): + s[:] = gamma * s + (1 - gamma) * paddle.square(p.grad) + p[:] -= hyperparams['lr'] * p.grad / paddle.sqrt(s + eps) + p.grad.zero_() + a.append(p) + return a +``` + 我们将初始学习率设置为0.01,加权项$\gamma$设置为0.9。 也就是说,$\mathbf{s}$累加了过去的$1/(1-\gamma) = 10$次平方梯度观测值的平均值。 @@ -175,6 +206,13 @@ d2l.train_concise_ch11(trainer, {'learning_rate': 0.01, 'rho': 0.9}, data_iter) ``` +```{.python .input} +#@tab paddle +trainer = paddle.optimizer.RMSProp +d2l.train_concise_ch11(trainer, {'learning_rate': 0.01, 'rho': 0.9}, + data_iter) +``` + ## 小结 * RMSProp算法与Adagrad算法非常相似,因为两者都使用梯度的平方来缩放系数。 @@ -200,3 +238,7 @@ d2l.train_concise_ch11(trainer, {'learning_rate': 0.01, 'rho': 0.9}, :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/4323) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11853) +:end_tab: \ No newline at end of file diff --git a/chapter_optimization/sgd.md b/chapter_optimization/sgd.md index bdf1749be..17e1f8f90 100644 --- a/chapter_optimization/sgd.md +++ b/chapter_optimization/sgd.md @@ -1,7 +1,7 @@ # 随机梯度下降 :label:`sec_sgd` -但是,在前面的章节中,我们一直在训练过程中使用随机梯度下降,但没有解释它为什么起作用。为了澄清这一点,我们刚在 :numref:`sec_gd`中描述了梯度下降的基本原则。在本节中,我们继续更详细地说明*随机梯度下降*(stochastic gradient descent)。 +在前面的章节中,我们一直在训练过程中使用随机梯度下降,但没有解释它为什么起作用。为了澄清这一点,我们刚在 :numref:`sec_gd`中描述了梯度下降的基本原则。本节继续更详细地说明*随机梯度下降*(stochastic gradient descent)。 ```{.python .input} %matplotlib inline @@ -27,6 +27,16 @@ import math import tensorflow as tf ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +``` + ## 随机梯度更新 在深度学习中,目标函数通常是训练数据集中每个样本的损失函数的平均值。给定$n$个样本的训练数据集,我们假设$f_i(\mathbf{x})$是关于索引$i$的训练样本的损失函数,其中$\mathbf{x}$是参数向量。然后我们得到目标函数 @@ -61,7 +71,7 @@ def f_grad(x1, x2): # 目标函数的梯度 ``` ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle def sgd(x1, x2, s1, s2, f_grad): g1, g2 = f_grad(x1, x2) # 模拟有噪声的梯度 @@ -92,7 +102,7 @@ lr = constant_lr # 常数学习速度 d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50, f_grad=f_grad)) ``` -正如我们所看到的,随机梯度下降中变量的轨迹比我们在 :numref:`sec_gd`中观察到的梯度下降中观察到的轨迹嘈杂得多。这是由于梯度的随机性质。也就是说,即使我们接近最小值,我们仍然受到通过$\eta \nabla f_i(\mathbf{x})$的瞬间梯度所注入的不确定性的影响。即使经过50次迭代,质量仍然不那么好。更糟糕的是,经过额外的步骤,它不会得到改善(我们鼓励你尝试更多的步骤来确认这一点)。这给我们留下了唯一的选择:改变学习率$\eta$。但是,如果我们选择的学习率太小,我们一开始就不会取得任何有意义的进展。另一方面,如果我们选择的学习率太大,我们将无法获得一个好的解决方案,如上所示。解决这些相互冲突的目标的唯一方法是在优化过程中*动态*降低学习率。 +正如我们所看到的,随机梯度下降中变量的轨迹比我们在 :numref:`sec_gd`中观察到的梯度下降中观察到的轨迹嘈杂得多。这是由于梯度的随机性质。也就是说,即使我们接近最小值,我们仍然受到通过$\eta \nabla f_i(\mathbf{x})$的瞬间梯度所注入的不确定性的影响。即使经过50次迭代,质量仍然不那么好。更糟糕的是,经过额外的步骤,它不会得到改善。这给我们留下了唯一的选择:改变学习率$\eta$。但是,如果我们选择的学习率太小,我们一开始就不会取得任何有意义的进展。另一方面,如果我们选择的学习率太大,我们将无法获得一个好的解决方案,如上所示。解决这些相互冲突的目标的唯一方法是在优化过程中*动态*降低学习率。 这也是在`sgd`步长函数中添加学习率函数`lr`的原因。在上面的示例中,学习率调度的任何功能都处于休眠状态,因为我们将相关的`lr`函数设置为常量。 @@ -140,11 +150,11 @@ lr = polynomial_lr d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50, f_grad=f_grad)) ``` -关于如何设置学习率,还有更多的选择。例如,我们可以从较小的学习率开始,然后使其迅速上涨,再让它降低,尽管这会更慢。我们甚至可以在较小和较大的学习率之间切换。这样的计划各种各样。现在,让我们专注于可以进行全面理论分析的学习率计划,即凸环境下的学习率。对于一般的非凸问题,很难获得有意义的收敛保证,因为总的来说,最大限度地减少非线性非凸问题是NP困难的。有关的研究调查,请参阅例如2015年Tibshirani的优秀[讲义笔记](https://www.stat.cmu.edu/~ryantibs/convexopt-F15/lectures/26-nonconvex.pdf)。 +关于如何设置学习率,还有更多的选择。例如,我们可以从较小的学习率开始,然后使其迅速上涨,再让它降低,尽管这会更慢。我们甚至可以在较小和较大的学习率之间切换。现在,让我们专注于可以进行全面理论分析的学习率计划,即凸环境下的学习率。对一般的非凸问题,很难获得有意义的收敛保证,因为总的来说,最大限度地减少非线性非凸问题是NP困难的。有关的研究调查,请参阅例如2015年Tibshirani的优秀[讲义笔记](https://www.stat.cmu.edu/~ryantibs/convexopt-F15/lectures/26-nonconvex.pdf)。 ## 凸目标的收敛性分析 -以下对凸目标函数的随机梯度下降的收敛性分析是可选的,主要用于传达对问题的更多直觉。我们只限于最简单的证明之一 :cite:`Nesterov.Vial.2000`。存在着明显更先进的证明技术,例如,当目标函数表现特别好时。 +以下对凸目标函数的随机梯度下降的收敛性分析是可选读的,主要用于传达对问题的更多直觉。我们只限于最简单的证明之一 :cite:`Nesterov.Vial.2000`。存在着明显更先进的证明技术,例如,当目标函数表现特别好时。 假设所有$\boldsymbol{\xi}$的目标函数$f(\boldsymbol{\xi}, \mathbf{x})$在$\mathbf{x}$中都是凸的。更具体地说,我们考虑随机梯度下降更新: @@ -231,9 +241,9 @@ $${n \choose 1} \frac{1}{n} \left(1-\frac{1}{n}\right)^{n-1} = \frac{n}{n-1} \le 1. 尝试不同的随机梯度下降学习率计划和不同的迭代次数进行实验。特别是,根据迭代次数的函数来绘制与最优解$(0, 0)$的距离。 1. 证明对于函数$f(x_1, x_2) = x_1^2 + 2 x_2^2$而言,向梯度添加正态噪声等同于最小化损失函数$f(\mathbf{x}, \mathbf{w}) = (x_1 - w_1)^2 + 2 (x_2 - w_2)^2$,其中$\mathbf{x}$是从正态分布中提取的。 -1. 当你从$\{(x_1, y_1), \ldots, (x_n, y_n)\}$分别使用替换方法以及不替换方法进行采样时,比较随机梯度下降的收敛性。 -1. 如果某些梯度(或者更确切地说与之相关的某些坐标)始终比所有其他梯度都大,你将如何更改随机梯度下降求解器? -1. 假设$f(x) = x^2 (1 + \sin x)$。$f$有多少局部最小值?你能改变$f$以尽量减少它需要评估所有局部最小值的方式吗? +1. 从$\{(x_1, y_1), \ldots, (x_n, y_n)\}$分别使用替换方法以及不替换方法进行采样时,比较随机梯度下降的收敛性。 +1. 如果某些梯度(或者更确切地说与之相关的某些坐标)始终比所有其他梯度都大,将如何更改随机梯度下降求解器? +1. 假设$f(x) = x^2 (1 + \sin x)$。$f$有多少局部最小值?请试着改变$f$以尽量减少它需要评估所有局部最小值的方式。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/3837) @@ -246,3 +256,7 @@ $${n \choose 1} \frac{1}{n} \left(1-\frac{1}{n}\right)^{n-1} = \frac{n}{n-1} \le :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/3839) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11849) +:end_tab: \ No newline at end of file diff --git a/chapter_preface/index.md b/chapter_preface/index.md index 8667df566..ba6796904 100644 --- a/chapter_preface/index.md +++ b/chapter_preface/index.md @@ -1,12 +1,12 @@ -# 序言 +# 前言 -几年前,在大公司和初创公司中,并没有大量的深度学习科学家开发智能产品和服务。我们中年轻人(作者)进入这个领域时,机器学习并没有在报纸上获得头条新闻。我们的父母根本不知道什么是机器学习,更不用说为什么我们可能更喜欢机器学习,而不是从事医学或法律职业。机器学习是一门具有前瞻性的学科,在现实世界的应用范围很窄。而那些应用,例如语音识别和计算机视觉,需要大量的领域知识,以至于它们通常被认为是完全独立的领域,而机器学习对于这些领域来说只是一个小组件。因此,神经网络——我们在本书中关注的深度学习模型的前身,被认为是过时的工具。 +几年前,在大公司和初创公司中,并没有大量的深度学习科学家开发智能产品和服务。我们中年轻人(作者)进入这个领域时,机器学习并没有在报纸上获得头条新闻。我们的父母根本不知道什么是机器学习,更不用说为什么我们可能更喜欢机器学习,而不是从事医学或法律职业。机器学习是一门具有前瞻性的学科,在现实世界的应用范围很窄。而那些应用,例如语音识别和计算机视觉,需要大量的领域知识,以至于它们通常被认为是完全独立的领域,而机器学习对这些领域来说只是一个小组件。因此,神经网络——我们在本书中关注的深度学习模型的前身,被认为是过时的工具。 -就在过去的五年里,深度学习给世界带来了惊喜,推动了计算机视觉、自然语言处理、自动语音识别、强化学习和统计建模等领域的快速发展。有了这些进步,我们现在可以制造比以往任何时候都更自主的汽车(不过可能没有一些公司试图让你相信的那么自主),可以自动起草普通邮件的智能回复系统,帮助人们从令人压抑的大收件箱中解放出来。在围棋等棋类游戏中,软件超越了世界上最优秀的人,这曾被认为是几十年后的事。这些工具已经对工业和社会产生了越来越广泛的影响,改变了电影的制作方式、疾病的诊断方式,并在基础科学中扮演着越来越重要的角色——从天体物理学到生物学。 +就在过去的五年里,深度学习给世界带来了惊喜,推动了计算机视觉、自然语言处理、自动语音识别、强化学习和统计建模等领域的快速发展。有了这些进步,我们现在可以制造比以往任何时候都更自主的汽车(不过可能没有一些公司试图让大家相信的那么自主),可以自动起草普通邮件的智能回复系统,帮助人们从令人压抑的大收件箱中解放出来。在围棋等棋类游戏中,软件超越了世界上最优秀的人,这曾被认为是几十年后的事。这些工具已经对工业和社会产生了越来越广泛的影响,改变了电影的制作方式、疾病的诊断方式,并在基础科学中扮演着越来越重要的角色——从天体物理学到生物学。 ## 关于本书 -这本书代表了我们的尝试——让深度学习可平易近人,教会你*概念*、*背景*和*代码*。 +这本书代表了我们的尝试——让深度学习可平易近人,教会人们*概念*、*背景*和*代码*。 ### 一种结合了代码、数学和HTML的媒介 @@ -14,7 +14,7 @@ 测试深度学习的潜力带来了独特的挑战,因为任何一个应用都会将不同的学科结合在一起。应用深度学习需要同时了解(1)以特定方式提出问题的动机;(2)给定建模方法的数学;(3)将模型拟合数据的优化算法;(4)能够有效训练模型、克服数值计算缺陷并最大限度地利用现有硬件的工程方法。同时教授表述问题所需的批判性思维技能、解决问题所需的数学知识,以及实现这些解决方案所需的软件工具,这是一个巨大的挑战。 -在我们开始写这本书的时候,没有资源能够同时满足一些条件:(1)是最新的;(2)涵盖了现代机器学习的所有领域,技术深度丰富;(3)在一本引人入胜的教科书中,你可以在实践教程中找到干净的可运行代码,并从中穿插高质量的阐述。我们发现了大量关于如何使用给定的深度学习框架(例如,如何对TensorFlow中的矩阵进行基本的数值计算)或实现特定技术的代码示例(例如,LeNet、AlexNet、ResNet的代码片段),这些代码示例分散在各种博客帖子和GitHub库中。但是,这些示例通常关注如何实现给定的方法,但忽略了为什么做出某些算法决策的讨论。虽然一些互动资源已经零星地出现以解决特定主题。例如,在网站[Distill](http://distill.pub)上发布的引人入胜的博客帖子或个人博客,但它们仅覆盖深度学习中的选定主题,并且通常缺乏相关代码。另一方面,虽然已经出现了几本教科书,其中最著名的是 :cite:`Goodfellow.Bengio.Courville.2016`(中文名《深度学习》),它对深度学习背后的概念进行了全面的调查,但这些资源并没有将这些概念的描述与这些概念的代码实现结合起来。有时会让读者对如何实现它们一无所知。此外,太多的资源隐藏在商业课程提供商的付费壁垒后面。 +在我们开始写这本书的时候,没有资源能够同时满足一些条件:(1)是最新的;(2)涵盖了现代机器学习的所有领域,技术深度丰富;(3)在一本引人入胜的教科书中,人们可以在实践教程中找到干净的可运行代码,并从中穿插高质量的阐述。我们发现了大量关于如何使用给定的深度学习框架(例如,如何对TensorFlow中的矩阵进行基本的数值计算)或实现特定技术的代码示例(例如,LeNet、AlexNet、ResNet的代码片段),这些代码示例分散在各种博客帖子和GitHub库中。但是,这些示例通常关注如何实现给定的方法,但忽略了为什么做出某些算法决策的讨论。虽然一些互动资源已经零星地出现以解决特定主题。例如,在网站[Distill](http://distill.pub)上发布的引人入胜的博客帖子或个人博客,但它们仅覆盖深度学习中的选定主题,并且通常缺乏相关代码。另一方面,虽然已经出现了几本教科书,其中最著名的是 :cite:`Goodfellow.Bengio.Courville.2016`(中文名《深度学习》),它对深度学习背后的概念进行了全面的调查,但这些资源并没有将这些概念的描述与这些概念的代码实现结合起来。有时会让读者对如何实现它们一无所知。此外,太多的资源隐藏在商业课程提供商的付费壁垒后面。 我们着手创建的资源可以:(1)每个人都可以免费获得;(2)提供足够的技术深度,为真正成为一名应用机器学习科学家提供起步;(3)包括可运行的代码,向读者展示如何解决实践中的问题;(4)允许我们和社区的快速更新;(5)由一个[论坛](http://discuss.d2l.ai)作为补充,用于技术细节的互动讨论和回答问题。 @@ -22,7 +22,7 @@ ### 在实践中学习 -许多教科书教授一系列的主题,每一个都非常详细。例如,Chris Bishop的优秀教科书 :cite:`Bishop.2006` ,对每个主题都教得很透彻,以至于要读到线性回归这一章需要大量的工作。虽然专家们喜欢这本书正是因为它的透彻性,但对于初学者来说,这一特性限制了它作为介绍性文本的实用性。 +许多教科书教授一系列的主题,每一个都非常详细。例如,Chris Bishop的优秀教科书 :cite:`Bishop.2006` ,对每个主题都教得很透彻,以至于要读到线性回归这一章需要大量的工作。虽然专家们喜欢这本书正是因为它的透彻性,但对初学者来说,这一特性限制了它作为介绍性文本的实用性。 在这本书中,我们将适时教授大部分概念。换句话说,你将在实现某些实际目的所需的非常时刻学习概念。虽然我们在开始时花了一些时间来教授基础的背景知识,如线性代数和概率,但我们希望你在思考更深奥的概率分布之前,先体会一下训练模型的满足感。 @@ -30,7 +30,7 @@ 我们将根据需要将可运行代码与背景材料交错。通常,在充分解释工具之前,我们常常会在提供工具这一方面犯错误(我们将在稍后解释背景)。例如,在充分解释*随机梯度下降*为什么有用或为什么有效之前,我们可以使用它。这有助于给从业者提供快速解决问题所需的弹药,同时需要读者相信我们的一些决定。 -这本书将从头开始教授深度学习的概念。有时,我们想深入研究模型的细节,这些的细节通常会被深度学习框架的高级抽象隐藏起来。特别是在基础教程中,我们希望你了解在给定层或优化器中发生的一切。在这些情况下,我们通常会提供两个版本的示例:一个是我们从零开始实现一切,仅依赖于NumPy接口和自动微分;另一个是更实际的示例,我们使用深度学习框架的高级API编写简洁的代码。一旦我们教了您一些组件是如何工作的,我们就可以在随后的教程中使用高级API了。 +这本书将从头开始教授深度学习的概念。有时,我们想深入研究模型的细节,这些的细节通常会被深度学习框架的高级抽象隐藏起来。特别是在基础教程中,我们希望读者了解在给定层或优化器中发生的一切。在这些情况下,我们通常会提供两个版本的示例:一个是我们从零开始实现一切,仅依赖张量操作和自动微分;另一个是更实际的示例,我们使用深度学习框架的高级API编写简洁的代码。一旦我们教了您一些组件是如何工作的,我们就可以在随后的教程中使用高级API了。 ### 内容和结构 @@ -40,10 +40,10 @@ :label:`fig_book_org` * 第一部分包括基础知识和预备知识。 -:numref:`chap_introduction` 提供深度学习的入门课程。然后在 :numref:`chap_preliminaries` 中,我们将快速向你介绍实践深度学习所需的前提条件,例如如何存储和处理数据,以及如何应用基于线性代数、微积分和概率基本概念的各种数值运算。 :numref:`chap_linear` 和 :numref:`chap_perceptrons` 涵盖了深度学习的最基本概念和技术,例如线性回归、多层感知机和正则化。 +:numref:`chap_introduction` 提供深度学习的入门课程。然后在 :numref:`chap_preliminaries` 中,我们将快速介绍实践深度学习所需的前提条件,例如如何存储和处理数据,以及如何应用基于线性代数、微积分和概率基本概念的各种数值运算。 :numref:`chap_linear` 和 :numref:`chap_perceptrons` 涵盖了深度学习的最基本概念和技术,例如线性回归、多层感知机和正则化。 * 接下来的五章集中讨论现代深度学习技术。 -:numref:`chap_computation` 描述了深度学习计算的各种关键组件,并为我们随后实现更复杂的模型奠定了基础。接下来,在 :numref:`chap_cnn` 和 :numref:`chap_modern_cnn` 中,我们介绍了卷积神经网络(convolutional neural network,CNN),这是构成大多数现代计算机视觉系统骨干的强大工具。随后,在 :numref:`chap_rnn` 和 :numref:`chap_modern_rnn` 中,我们引入了循环神经网络(recurrent neural network,RNN),这是一种利用数据中的时间或序列结构的模型,通常用于自然语言处理和时间序列预测。在 :numref:`chap_attention` 中,我们介绍了一类新的模型,它采用了一种称为注意力机制的技术,最近它们已经开始在自然语言处理中取代循环神经网络。这一部分将帮助你快速了解大多数现代深度学习应用背后的基本工具。 +:numref:`chap_computation` 描述了深度学习计算的各种关键组件,并为我们随后实现更复杂的模型奠定了基础。接下来,在 :numref:`chap_cnn` 和 :numref:`chap_modern_cnn` 中,我们介绍了卷积神经网络(convolutional neural network,CNN),这是构成大多数现代计算机视觉系统骨干的强大工具。随后,在 :numref:`chap_rnn` 和 :numref:`chap_modern_rnn` 中,我们引入了循环神经网络(recurrent neural network,RNN),这是一种利用数据中的时间或序列结构的模型,通常用于自然语言处理和时间序列预测。在 :numref:`chap_attention` 中,我们介绍了一类新的模型,它采用了一种称为注意力机制的技术,最近它们已经开始在自然语言处理中取代循环神经网络。这一部分将帮助读者快速了解大多数现代深度学习应用背后的基本工具。 * 第三部分讨论可伸缩性、效率和应用程序。 首先,在 :numref:`chap_optimization` 中,我们讨论了用于训练深度学习模型的几种常用优化算法。下一章 :numref:`chap_performance` 将探讨影响深度学习代码计算性能的几个关键因素。在 :numref:`chap_cv` 中,我们展示了深度学习在计算机视觉中的主要应用。在 :numref:`chap_nlp_pretrain` 和 :numref:`chap_nlp_app` 中,我们展示了如何预训练语言表示模型并将其应用于自然语言处理任务。 @@ -81,21 +81,21 @@ d2l = sys.modules[__name__] :begin_tab:`mxnet` 本书中的大部分代码都是基于Apache MXNet的。MXNet是深度学习的开源框架,是亚马逊以及许多大学和公司的首选。本书中的所有代码都通过了最新MXNet版本的测试。但是,由于深度学习的快速发展,一些在印刷版中代码 可能在MXNet的未来版本无法正常工作。 -但是,我们计划使在线版本保持最新。如果你遇到任何此类问题,请查看 :ref:`chap_installation` 以更新你的代码和运行时环境。 +但是,我们计划使在线版本保持最新。如果读者遇到任何此类问题,请查看 :ref:`chap_installation` 以更新代码和运行时环境。 下面是我们如何从MXNet导入模块。 :end_tab: :begin_tab:`pytorch` 本书中的大部分代码都是基于PyTorch的。PyTorch是一个开源的深度学习框架,在研究界非常受欢迎。本书中的所有代码都在最新版本的PyTorch下通过了测试。但是,由于深度学习的快速发展,一些在印刷版中代码可能在PyTorch的未来版本无法正常工作。 -但是,我们计划使在线版本保持最新。如果你遇到任何此类问题,请查看 :ref:`chap_installation` 以更新你的代码和运行时环境。 +但是,我们计划使在线版本保持最新。如果读者遇到任何此类问题,请查看 :ref:`chap_installation` 以更新代码和运行时环境。 下面是我们如何从PyTorch导入模块。 :end_tab: :begin_tab:`tensorflow` 本书中的大部分代码都是基于TensorFlow的。TensorFlow是一个开源的深度学习框架,在研究界和产业界都非常受欢迎。本书中的所有代码都在最新版本的TensorFlow下通过了测试。但是,由于深度学习的快速发展,一些在印刷版中代码可能在TensorFlow的未来版本无法正常工作。 -但是,我们计划使在线版本保持最新。如果你遇到任何此类问题,请查看 :ref:`chap_installation` 以更新你的代码和运行时环境。 +但是,我们计划使在线版本保持最新。如果读者遇到任何此类问题,请查看 :ref:`chap_installation` 以更新代码和运行时环境。 下面是我们如何从TensorFlow导入模块。 :end_tab: @@ -126,17 +126,37 @@ import numpy as np import tensorflow as tf ``` +```{.python .input} +#@tab paddle +#@save +import numpy as np +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +from paddle.nn import functional as F +from paddle.vision import transforms +import paddle.vision as paddlevision +from PIL import Image +paddle.disable_signal_handler() +``` + ### 目标受众 -本书面向学生(本科生或研究生)、工程师和研究人员,他们希望扎实掌握深度学习的实用技术。因为我们从头开始解释每个概念,所以不需要过往的深度学习或机器学习背景。全面解释深度学习的方法需要一些数学和编程,但我们只假设你了解一些基础知识,包括线性代数、微积分、概率和非常基础的Python编程。此外,在附录中,我们提供了本书所涵盖的大多数数学知识的复习。大多数时候,我们会优先考虑直觉和想法,而不是数学的严谨性。有许多很棒的书可以引导感兴趣的读者走得更远。Bela Bollobas的《线性分析》 :cite:`Bollobas.1999` 对线性代数和函数分析进行了深入的研究。 :cite:`Wasserman.2013` 是一本很好的统计学指南。如果你以前没有使用过Python语言,那么你可能想要仔细阅读这个[Python教程](http://learnpython.org/)。 +本书面向学生(本科生或研究生)、工程师和研究人员,他们希望扎实掌握深度学习的实用技术。因为我们从头开始解释每个概念,所以不需要过往的深度学习或机器学习背景。全面解释深度学习的方法需要一些数学和编程,但我们只假设读者了解一些基础知识,包括线性代数、微积分、概率和非常基础的Python编程。此外,在附录中,我们提供了本书所涵盖的大多数数学知识的复习。大多数时候,我们会优先考虑直觉和想法,而不是数学的严谨性。有许多很棒的书可以引导感兴趣的读者走得更远。Bela Bollobas的《线性分析》 :cite:`Bollobas.1999` 对线性代数和函数分析进行了深入的研究。 :cite:`Wasserman.2013` 是一本很好的统计学指南。如果读者以前没有使用过Python语言,那么可以仔细阅读这个[Python教程](http://learnpython.org/)。 ### 论坛 -与本书相关,我们已经启动了一个论坛,在[discuss.d2l.ai](https://discuss.d2l.ai/)。当你对本书的任何一节有疑问时,你可以在每一节的末尾找到相关的讨论页链接。 +与本书相关,我们已经启动了一个论坛,在[discuss.d2l.ai](https://discuss.d2l.ai/)。当对本书的任何一节有疑问时,请在每一节的末尾找到相关的讨论页链接。 ## 致谢 -我们感谢中英文草稿的数百位撰稿人。他们帮助改进了内容并提供了宝贵的反馈。特别地,我们要感谢这份中文稿的每一位撰稿人,是他们的无私奉献让这本书变得更好。他们的GitHub ID或姓名是(没有特定顺序):alxnorden, avinashingit, bowen0701, brettkoonce, Chaitanya Prakash Bapat, +感谢中英文草稿的数百位撰稿人。他们帮助改进了内容并提供了宝贵的反馈。 +感谢Anirudh Dagar和唐源将部分较早版本的MXNet实现分别改编为PyTorch和TensorFlow实现。 +感谢百度团队将较新的PyTorch实现改编为PaddlePaddle实现。 +感谢张帅将更新的LaTeX样式集成进PDF文件的编译。 + +特别地,我们要感谢这份中文稿的每一位撰稿人,是他们的无私奉献让这本书变得更好。他们的GitHub ID或姓名是(没有特定顺序):alxnorden, avinashingit, bowen0701, brettkoonce, Chaitanya Prakash Bapat, cryptonaut, Davide Fiocco, edgarroman, gkutiel, John Mitro, Liang Pu, Rahul Agarwal, Mohamed Ali Jamaoui, Michael (Stu) Stewart, Mike Müller, NRauschmayr, Prakhar Srivastav, sad-, sfermigier, Sheng Zha, sundeepteki, @@ -171,7 +191,7 @@ thebesttv, Quanshangze Du, Yanbo Chen。 ## 小结 * 深度学习已经彻底改变了模式识别,引入了一系列技术,包括计算机视觉、自然语言处理、自动语音识别。 -* 要成功地应用深度学习,你必须知道如何抛出一个问题、建模的数学方法、将模型与数据拟合的算法,以及实现所有这些的工程技术。 +* 要成功地应用深度学习,必须知道如何抛出一个问题、建模的数学方法、将模型与数据拟合的算法,以及实现所有这些的工程技术。 * 这本书提供了一个全面的资源,包括文本、图表、数学和代码,都集中在一个地方。 * 要回答与本书相关的问题,请访问我们的论坛[discuss.d2l.ai](https://discuss.d2l.ai/). * 所有Jupyter记事本都可以在GitHub上下载。 @@ -179,8 +199,8 @@ thebesttv, Quanshangze Du, Yanbo Chen。 ## 练习 1. 在本书[discuss.d2l.ai](https://discuss.d2l.ai/)的论坛上注册帐户。 -1. 在你的计算机上安装Python。 -1. 沿着本节底部的链接进入论坛,在那里你可以寻求帮助、讨论这本书,并通过与作者和社区接触来找到问题的答案。 +1. 在计算机上安装Python。 +1. 沿着本节底部的链接进入论坛,在那里可以寻求帮助、讨论这本书,并通过与作者和社区接触来找到问题的答案。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/2085) @@ -193,3 +213,7 @@ thebesttv, Quanshangze Du, Yanbo Chen。 :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2087) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11678) +:end_tab: diff --git a/chapter_preliminaries/autograd.md b/chapter_preliminaries/autograd.md index 684c297c0..1ce57cc4f 100644 --- a/chapter_preliminaries/autograd.md +++ b/chapter_preliminaries/autograd.md @@ -1,12 +1,12 @@ # 自动微分 :label:`sec_autograd` -正如我们在 :numref:`sec_calculus`中所说的那样,求导是几乎所有深度学习优化算法的关键步骤。 +正如 :numref:`sec_calculus`中所说,求导是几乎所有深度学习优化算法的关键步骤。 虽然求导的计算很简单,只需要一些基本的微积分。 但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。 深度学习框架通过自动计算导数,即*自动微分*(automatic differentiation)来加快求导。 -实际中,根据我们设计的模型,系统会构建一个*计算图*(computational graph), +实际中,根据设计好的模型,系统会构建一个*计算图*(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,*反向传播*(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。 @@ -40,15 +40,25 @@ x = tf.range(4, dtype=tf.float32) x ``` -[**在我们计算$y$关于$\mathbf{x}$的梯度之前,我们需要一个地方来存储梯度。**] +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle + +x = paddle.arange(4, dtype='float32') +x +``` + +[**在我们计算$y$关于$\mathbf{x}$的梯度之前,需要一个地方来存储梯度。**] 重要的是,我们不会在每次对一个参数求导时都分配新的内存。 因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。 注意,一个标量函数关于向量$\mathbf{x}$的梯度是向量,并且与$\mathbf{x}$具有相同的形状。 ```{.python .input} -# 我们通过调用attach_grad来为一个张量的梯度分配内存 +# 通过调用attach_grad来为一个张量的梯度分配内存 x.attach_grad() -# 在我们计算关于x的梯度后,我们将能够通过'grad'属性访问它,它的值被初始化为0 +# 在计算关于x的梯度后,将能够通过'grad'属性访问它,它的值被初始化为0 x.grad ``` @@ -63,7 +73,13 @@ x.grad # 默认值是None x = tf.Variable(x) ``` -(**现在让我们计算$y$。**) +```{.python .input} +#@tab paddle +x = paddle.to_tensor(x, stop_gradient=False) +x.grad # 默认值是None +``` + +(**现在计算$y$。**) ```{.python .input} # 把代码放到autograd.record内,以建立计算图 @@ -86,8 +102,14 @@ with tf.GradientTape() as t: y ``` +```{.python .input} +#@tab paddle +y = 2 * paddle.dot(x, x) +y +``` + `x`是一个长度为4的向量,计算`x`和`x`的点积,得到了我们赋值给`y`的标量输出。 -接下来,我们[**通过调用反向传播函数来自动计算`y`关于`x`每个分量的梯度**],并打印这些梯度。 +接下来,[**通过调用反向传播函数来自动计算`y`关于`x`每个分量的梯度**],并打印这些梯度。 ```{.python .input} y.backward() @@ -106,6 +128,12 @@ x_grad = t.gradient(y, x) x_grad ``` +```{.python .input} +#@tab paddle +y.backward() +x.grad +``` + 函数$y=2\mathbf{x}^{\top}\mathbf{x}$关于$\mathbf{x}$的梯度应为$4\mathbf{x}$。 让我们快速验证这个梯度是否计算正确。 @@ -123,7 +151,12 @@ x.grad == 4 * x x_grad == 4 * x ``` -[**现在让我们计算`x`的另一个函数。**] +```{.python .input} +#@tab paddle +x.grad == 4 * x +``` + +[**现在计算`x`的另一个函数。**] ```{.python .input} with autograd.record(): @@ -148,17 +181,26 @@ with tf.GradientTape() as t: t.gradient(y, x) # 被新计算的梯度覆盖 ``` +```{.python .input} +#@tab paddle +# 在默认情况下,PaddlePaddle会累积梯度,我们需要清除之前的值 +x.clear_gradient() +y = paddle.sum(x) +y.backward() +x.grad +``` + ## 非标量变量的反向传播 当`y`不是标量时,向量`y`关于向量`x`的导数的最自然解释是一个矩阵。 对于高阶和高维的`y`和`x`,求导的结果可以是一个高阶张量。 然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括[**深度学习中**]), -但当我们调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 +但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 这里(**,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。**) ```{.python .input} -# 当我们对向量值变量y(关于x的函数)调用backward时,将通过对y中的元素求和来创建 +# 当对向量值变量y(关于x的函数)调用backward时,将通过对y中的元素求和来创建 # 一个新的标量变量。然后计算这个标量变量相对于x的梯度 with autograd.record(): y = x * x # y是一个向量 @@ -169,7 +211,7 @@ x.grad # 等价于y=sum(x*x) ```{.python .input} #@tab pytorch # 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。 -# 在我们的例子中,我们只想求偏导数的和,所以传递一个1的梯度是合适的 +# 本例只想求偏导数的和,所以传递一个1的梯度是合适的 x.grad.zero_() y = x * x # 等价于y.backward(torch.ones(len(x))) @@ -184,14 +226,22 @@ with tf.GradientTape() as t: t.gradient(y, x) # 等价于y=tf.reduce_sum(x*x) ``` +```{.python .input} +#@tab paddle +x.clear_gradient() +y = x * x +paddle.sum(y).backward() +x.grad +``` + ## 分离计算 有时,我们希望[**将某些计算移动到记录的计算图之外**]。 例如,假设`y`是作为`x`的函数计算的,而`z`则是作为`y`和`x`的函数计算的。 -想象一下,我们想计算`z`关于`x`的梯度,但由于某种原因,我们希望将`y`视为一个常数, +想象一下,我们想计算`z`关于`x`的梯度,但由于某种原因,希望将`y`视为一个常数, 并且只考虑到`x`在`y`被计算后发挥的作用。 -在这里,我们可以分离`y`来返回一个新变量`u`,该变量与`y`具有相同的值, +这里可以分离`y`来返回一个新变量`u`,该变量与`y`具有相同的值, 但丢弃计算图中如何计算`y`的任何信息。 换句话说,梯度不会向后流经`u`到`x`。 因此,下面的反向传播函数计算`z=u*x`关于`x`的偏导数,同时将`u`作为常数处理, @@ -229,6 +279,17 @@ x_grad = t.gradient(z, x) x_grad == u ``` +```{.python .input} +#@tab paddle +x.clear_gradient() +y = x * x +u = y.detach() +z = u * x + +paddle.sum(z).backward() +x.grad == u +``` + 由于记录了`y`的计算结果,我们可以随后在`y`上调用反向传播, 得到`y=x*x`关于的`x`的导数,即`2*x`。 @@ -249,6 +310,13 @@ x.grad == 2 * x t.gradient(y, x) == 2 * x ``` +```{.python .input} +#@tab paddle +x.clear_gradient() +paddle.sum(y).backward() +x.grad == 2 * x +``` + ## Python控制流的梯度计算 使用自动微分的一个好处是: @@ -293,6 +361,19 @@ def f(a): return c ``` +```{.python .input} +#@tab paddle +def f(a): + b = a * 2 + while paddle.norm(b) < 1000: + b = b * 2 + if paddle.sum(b) > 0: + c = b + else: + c = 100 * b + return c +``` + 让我们计算梯度。 ```{.python .input} @@ -319,10 +400,16 @@ d_grad = t.gradient(d, a) d_grad ``` +```{.python .input} +#@tab paddle +a = paddle.to_tensor(paddle.randn(shape=[1]), stop_gradient=False) +d = f(a) +d.backward() +``` + 我们现在可以分析上面定义的`f`函数。 请注意,它在其输入`a`中是分段线性的。 -换言之,对于任何`a`,存在某个常量标量`k`,使得`f(a)=k*a`,其中`k`的值取决于输入`a`。 -因此,我们可以用`d/a`验证梯度是否正确。 +换言之,对于任何`a`,存在某个常量标量`k`,使得`f(a)=k*a`,其中`k`的值取决于输入`a`,因此可以用`d/a`验证梯度是否正确。 ```{.python .input} a.grad == d / a @@ -338,15 +425,20 @@ a.grad == d / a d_grad == d / a ``` +```{.python .input} +#@tab paddle +a.grad == d / a +``` + ## 小结 -* 深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上。然后我们记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。 +* 深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。 ## 练习 1. 为什么计算二阶导数比一阶导数的开销要更大? 1. 在运行反向传播函数之后,立即再次运行它,看看会发生什么。 -1. 在控制流的例子中,我们计算`d`关于`a`的导数,如果我们将变量`a`更改为随机向量或矩阵,会发生什么? +1. 在控制流的例子中,我们计算`d`关于`a`的导数,如果将变量`a`更改为随机向量或矩阵,会发生什么? 1. 重新设计一个求控制流梯度的例子,运行并分析结果。 1. 使$f(x)=\sin(x)$,绘制$f(x)$和$\frac{df(x)}{dx}$的图像,其中后者不使用$f'(x)=\cos(x)$。 @@ -361,3 +453,7 @@ d_grad == d / a :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1757) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11684) +:end_tab: diff --git a/chapter_preliminaries/calculus.md b/chapter_preliminaries/calculus.md index e5e3d8eca..181a1b30a 100644 --- a/chapter_preliminaries/calculus.md +++ b/chapter_preliminaries/calculus.md @@ -9,8 +9,7 @@ ![用逼近法求圆的面积](../img/polygon-circle.svg) :label:`fig_circle_area` -事实上,逼近法就是*积分*(integral calculus)的起源, -我们将在 :numref:`sec_integral_calculus`中详细描述。 +事实上,逼近法就是*积分*(integral calculus)的起源。 2000多年后,微积分的另一支,*微分*(differential calculus)被发明出来。 在微分学最重要的应用是优化问题,即考虑如何把事情做到最好。 正如在 :numref:`subsec_norms_and_objectives`中讨论的那样, @@ -18,7 +17,7 @@ 在深度学习中,我们“训练”模型,不断更新它们,使它们在看到越来越多的数据时变得越来越好。 通常情况下,变得更好意味着最小化一个*损失函数*(loss function), -即一个衡量“我们的模型有多糟糕”这个问题的分数。 +即一个衡量“模型有多糟糕”这个问题的分数。 最终,我们真正关心的是生成一个模型,它能够在从未见过的数据上表现良好。 但“训练”模型只能将模型与我们实际能看到的数据相拟合。 因此,我们可以将拟合模型的任务分解为两个关键问题: @@ -26,15 +25,15 @@ * *优化*(optimization):用模型拟合观测数据的过程; * *泛化*(generalization):数学原理和实践者的智慧,能够指导我们生成出有效性超出用于训练的数据集本身的模型。 -为了帮助你在后面的章节中更好地理解优化问题和方法, -本节提供了一个非常简短的入门教程,帮你快速掌握深度学习中常用的微分知识。 +为了帮助读者在后面的章节中更好地理解优化问题和方法, +本节提供了一个非常简短的入门教程,帮助读者快速掌握深度学习中常用的微分知识。 ## 导数和微分 我们首先讨论导数的计算,这是几乎所有深度学习优化算法的关键步骤。 在深度学习中,我们通常选择对于模型参数可微的损失函数。 简而言之,对于每个参数, -如果我们把这个参数*增加*或*减少*一个无穷小的量,我们可以知道损失会以多快的速度增加或减少, +如果我们把这个参数*增加*或*减少*一个无穷小的量,可以知道损失会以多快的速度增加或减少, 假设我们有一个函数$f: \mathbb{R} \rightarrow \mathbb{R}$,其输入和输出都是标量。 (**如果$f$的*导数*存在,这个极限被定义为**) @@ -83,8 +82,19 @@ def f(x): return 3 * x ** 2 - 4 * x ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +from matplotlib_inline import backend_inline +import numpy as np + +def f(x): + return 3 * x ** 2 - 4 * x +``` + [**通过令$x=1$并让$h$接近$0$,**] :eqref:`eq_derivative`中(**$\frac{f(x+h)-f(x)}{h}$的数值结果接近$2$**)。 -虽然这个实验不是一个数学证明,但我们稍后会看到,当$x=1$时,导数$u'$是$2$。 +虽然这个实验不是一个数学证明,但稍后会看到,当$x=1$时,导数$u'$是$2$。 ```{.python .input} #@tab all @@ -130,7 +140,7 @@ $$\frac{d}{dx} \left[\frac{f(x)}{g(x)}\right] = \frac{g(x) \frac{d}{dx} [f(x)] - 现在我们可以应用上述几个法则来计算$u'=f'(x)=3\frac{d}{dx}x^2-4\frac{d}{dx}x=6x-4$。 令$x=1$,我们有$u'=2$:在这个实验中,数值结果接近$2$, -这一点得到了我们在本节前面的实验的支持。 +这一点得到了在本节前面的实验的支持。 当$x=1$时,此导数也是曲线$u=f(x)$切线的斜率。 [**为了对导数的这种解释进行可视化,我们将使用`matplotlib`**], @@ -149,7 +159,7 @@ def use_svg_display(): #@save ``` 我们定义`set_figsize`函数来设置图表大小。 -注意,这里我们直接使用`d2l.plt`,因为导入语句 +注意,这里可以直接使用`d2l.plt`,因为导入语句 `from matplotlib import pyplot as plt`已标记为保存到`d2l`包中。 ```{.python .input} @@ -178,7 +188,7 @@ def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): axes.grid() ``` -通过这三个用于图形配置的函数,我们定义了`plot`函数来简洁地绘制多条曲线, +通过这三个用于图形配置的函数,定义一个`plot`函数来简洁地绘制多条曲线, 因为我们需要在整个书中可视化许多曲线。 ```{.python .input} @@ -269,33 +279,33 @@ $$\nabla_{\mathbf{x}} f(\mathbf{x}) = \bigg[\frac{\partial f(\mathbf{x})}{\parti 然而,上面方法可能很难找到梯度。 这是因为在深度学习中,多元函数通常是*复合*(composite)的, -所以我们可能没法应用上述任何规则来微分这些函数。 -幸运的是,链式法则使我们能够微分复合函数。 +所以难以应用上述任何规则来微分这些函数。 +幸运的是,链式法则可以被用来微分复合函数。 让我们先考虑单变量函数。假设函数$y=f(u)$和$u=g(x)$都是可微的,根据链式法则: $$\frac{dy}{dx} = \frac{dy}{du} \frac{du}{dx}.$$ -现在让我们把注意力转向一个更一般的场景,即函数具有任意数量的变量的情况。 +现在考虑一个更一般的场景,即函数具有任意数量的变量的情况。 假设可微分函数$y$有变量$u_1, u_2, \ldots, u_m$,其中每个可微分函数$u_i$都有变量$x_1, x_2, \ldots, x_n$。 注意,$y$是$x_1, x_2, \ldots, x_n$的函数。 对于任意$i = 1, 2, \ldots, n$,链式法则给出: -$$\frac{dy}{dx_i} = \frac{dy}{du_1} \frac{du_1}{dx_i} + \frac{dy}{du_2} \frac{du_2}{dx_i} + \cdots + \frac{dy}{du_m} \frac{du_m}{dx_i}$$ +$$\frac{\partial y}{\partial x_i} = \frac{\partial y}{\partial u_1} \frac{\partial u_1}{\partial x_i} + \frac{\partial y}{\partial u_2} \frac{\partial u_2}{\partial x_i} + \cdots + \frac{\partial y}{\partial u_m} \frac{\partial u_m}{\partial x_i}$$ ## 小结 * 微分和积分是微积分的两个分支,前者可以应用于深度学习中的优化问题。 * 导数可以被解释为函数相对于其变量的瞬时变化率,它也是函数曲线的切线的斜率。 * 梯度是一个向量,其分量是多变量函数相对于其所有变量的偏导数。 -* 链式法则使我们能够微分复合函数。 +* 链式法则可以用来微分复合函数。 ## 练习 1. 绘制函数$y = f(x) = x^3 - \frac{1}{x}$和其在$x = 1$处切线的图像。 1. 求函数$f(\mathbf{x}) = 3x_1^2 + 5e^{x_2}$的梯度。 1. 函数$f(\mathbf{x}) = \|\mathbf{x}\|_2$的梯度是什么? -1. 你可以写出函数$u = f(x, y, z)$,其中$x = x(a, b)$,$y = y(a, b)$,$z = z(a, b)$的链式法则吗? +1. 尝试写出函数$u = f(x, y, z)$,其中$x = x(a, b)$,$y = y(a, b)$,$z = z(a, b)$的链式法则。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1755) @@ -308,3 +318,7 @@ $$\frac{dy}{dx_i} = \frac{dy}{du_1} \frac{du_1}{dx_i} + \frac{dy}{du_2} \frac{du :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1754) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11684) +:end_tab: diff --git a/chapter_preliminaries/index.md b/chapter_preliminaries/index.md index c805332c3..c49c3746a 100644 --- a/chapter_preliminaries/index.md +++ b/chapter_preliminaries/index.md @@ -7,7 +7,7 @@ 机器学习通常需要处理大型数据集。 我们可以将某些数据集视为一个表,其中表的行对应样本,列对应属性。 -线性代数为我们提供了一些用来处理表格数据的方法。 +线性代数为人们提供了一些用来处理表格数据的方法。 我们不会太深究细节,而是将重点放在矩阵运算的基本原理及其实现上。 深度学习是关于优化的学习。 @@ -16,13 +16,13 @@ 本章将简要介绍这些知识。 幸运的是,`autograd`包会自动计算微分,本章也将介绍它。 -机器学习还涉及如何做出预测:给定我们观察到的信息,某些未知属性可能的值是多少? +机器学习还涉及如何做出预测:给定观察到的信息,某些未知属性可能的值是多少? 要在不确定的情况下进行严格的推断,我们需要借用概率语言。 最后,官方文档提供了本书之外的大量描述和示例。 -在本章的结尾,我们将向你展示如何在官方文档中查找所需信息。 +在本章的结尾,我们将展示如何在官方文档中查找所需信息。 -本书对读者数学基础无过分要求,只要你可以正确理解深度学习所需的数学知识即可。 +本书对读者数学基础无过分要求,只要可以正确理解深度学习所需的数学知识即可。 但这并不意味着本书中不涉及数学方面的内容,本章会快速介绍一些基本且常用的数学知识, 以便读者能够理解书中的大部分数学内容。 如果读者想要深入理解全部数学内容,可以进一步学习本书数学附录中给出的数学基础知识。 diff --git a/chapter_preliminaries/linear-algebra.md b/chapter_preliminaries/linear-algebra.md index 2fa06dab0..8eb369786 100644 --- a/chapter_preliminaries/linear-algebra.md +++ b/chapter_preliminaries/linear-algebra.md @@ -1,29 +1,29 @@ # 线性代数 :label:`sec_linear-algebra` -在你已经可以存储和操作数据后,让我们简要地回顾一下部分基本线性代数内容。 -这些内容能够帮助你了解和实现本书中介绍的大多数模型。 -本节我们将介绍线性代数中的基本数学对象、算术和运算,并用数学符号和相应的代码实现来表示它们。 +在介绍完如何存储和操作数据后,接下来将简要地回顾一下部分基本线性代数内容。 +这些内容有助于读者了解和实现本书中介绍的大多数模型。 +本节将介绍线性代数中的基本数学对象、算术和运算,并用数学符号和相应的代码实现来表示它们。 ## 标量 -如果你曾经在餐厅支付餐费,那么你已经知道一些基本的线性代数,比如在数字间相加或相乘。 -例如,北京的温度为$52^{\circ}F$(除了摄氏度外,另一种温度计量单位)。 -严格来说,我们称仅包含一个数值的叫*标量*(scalar)。 + +如果你曾经在餐厅支付餐费,那么应该已经知道一些基本的线性代数,比如在数字间相加或相乘。 +例如,北京的温度为$52^{\circ}F$(华氏度,除摄氏度外的另一种温度计量单位)。 +严格来说,仅包含一个数值被称为*标量*(scalar)。 如果要将此华氏度值转换为更常用的摄氏度, 则可以计算表达式$c=\frac{5}{9}(f-32)$,并将$f$赋为$52$。 在此等式中,每一项($5$、$9$和$32$)都是标量值。 符号$c$和$f$称为*变量*(variable),它们表示未知的标量值。 -在本书中,我们采用了数学表示法,其中标量变量由普通小写字母表示(例如,$x$、$y$和$z$)。 -我们用$\mathbb{R}$表示所有(连续)*实数*标量的空间。 -我们之后将严格定义*空间*(space)是什么, -但现在你只要记住表达式$x\in\mathbb{R}$是表示$x$是一个实值标量的正式形式。 +本书采用了数学表示法,其中标量变量由普通小写字母表示(例如,$x$、$y$和$z$)。 +本书用$\mathbb{R}$表示所有(连续)*实数*标量的空间,之后将严格定义*空间*(space)是什么, +但现在只要记住表达式$x\in\mathbb{R}$是表示$x$是一个实值标量的正式形式。 符号$\in$称为“属于”,它表示“是集合中的成员”。 -我们可以用$x, y \in \{0,1\}$来表明$x$和$y$是值只能为$0$或$1$的数字。 +例如$x, y \in \{0,1\}$可以用来表明$x$和$y$是值只能为$0$或$1$的数字。 (**标量由只有一个元素的张量表示**)。 -在下面的代码中,我们实例化两个标量,并执行一些熟悉的算术运算,即加法、乘法、除法和指数。 +下面的代码将实例化两个标量,并执行一些熟悉的算术运算,即加法、乘法、除法和指数。 ```{.python .input} from mxnet import np, npx @@ -55,19 +55,31 @@ y = tf.constant(2.0) x + y, x * y, x / y, x**y ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle + +x = paddle.to_tensor([3.0]) +y = paddle.to_tensor([2.0]) + +x + y, x * y, x / y, x**y +``` + ## 向量 -[**你可以将向量视为标量值组成的列表**]。 -我们将这些标量值称为向量的*元素*(element)或*分量*(component)。 +[**向量可以被视为标量值组成的列表**]。 +这些标量值被称为向量的*元素*(element)或*分量*(component)。 当向量表示数据集中的样本时,它们的值具有一定的现实意义。 -例如,如果我们正在训练一个模型来预测贷款违约风险,我们可能会将每个申请人与一个向量相关联, +例如,如果我们正在训练一个模型来预测贷款违约风险,可能会将每个申请人与一个向量相关联, 其分量与其收入、工作年限、过往违约次数和其他因素相对应。 -如果我们正在研究医院患者可能面临的心脏病发作风险,我们可能会用一个向量来表示每个患者, +如果我们正在研究医院患者可能面临的心脏病发作风险,可能会用一个向量来表示每个患者, 其分量为最近的生命体征、胆固醇水平、每天运动时间等。 -在数学表示法中,我们通常将向量记为粗体、小写的符号 +在数学表示法中,向量通常记为粗体、小写的符号 (例如,$\mathbf{x}$、$\mathbf{y}$和$\mathbf{z})$)。 -我们通过一维张量处理向量。一般来说,张量可以具有任意长度,取决于机器的内存限制。 +人们通过一维张量表示向量。一般来说,张量可以具有任意长度,取决于机器的内存限制。 ```{.python .input} x = np.arange(4) @@ -86,8 +98,13 @@ x = tf.range(4) x ``` -我们可以使用下标来引用向量的任一元素。 -例如,我们可以通过$x_i$来引用第$i$个元素。 +```{.python .input} +#@tab paddle +x = paddle.arange(4) +x +``` + +我们可以使用下标来引用向量的任一元素,例如可以通过$x_i$来引用第$i$个元素。 注意,元素$x_i$是一个标量,所以我们在引用它时不会加粗。 大量文献认为列向量是向量的默认方向,在本书中也是如此。 在数学中,向量$\mathbf{x}$可以写为: @@ -111,11 +128,16 @@ x[3] x[3] ``` +```{.python .input} +#@tab paddle +x[3] +``` + ### 长度、维度和形状 向量只是一个数字数组,就像每个数组都有一个长度一样,每个向量也是如此。 在数学表示法中,如果我们想说一个向量$\mathbf{x}$由$n$个实值标量组成, -我们可以将其表示为$\mathbf{x}\in\mathbb{R}^n$。 +可以将其表示为$\mathbf{x}\in\mathbb{R}^n$。 向量的长度通常称为向量的*维度*(dimension)。 与普通的Python数组一样,我们可以通过调用Python的内置`len()`函数来[**访问张量的长度**]。 @@ -134,6 +156,11 @@ len(x) len(x) ``` +```{.python .input} +#@tab paddle +len(x) +``` + 当用张量表示一个向量(只有一个轴)时,我们也可以通过`.shape`属性访问向量的长度。 形状(shape)是一个元素组,列出了张量沿每个轴的长度(维数)。 对于(**只有一个轴的张量,形状只有一个元素。**) @@ -152,6 +179,11 @@ x.shape x.shape ``` +```{.python .input} +#@tab paddle +x.shape +``` + 请注意,*维度*(dimension)这个词在不同上下文时往往会有不同的含义,这经常会使人感到困惑。 为了清楚起见,我们在此明确一下: *向量*或*轴*的维度被用来表示*向量*或*轴*的长度,即向量或轴的元素数量。 @@ -165,7 +197,7 @@ x.shape (例如,$\mathbf{X}$、$\mathbf{Y}$和$\mathbf{Z}$), 在代码中表示为具有两个轴的张量。 -在数学表示法中,我们使用$\mathbf{A} \in \mathbb{R}^{m \times n}$ +数学表示法使用$\mathbf{A} \in \mathbb{R}^{m \times n}$ 来表示矩阵$\mathbf{A}$,其由$m$行和$n$列的实值标量组成。 我们可以将任意矩阵$\mathbf{A} \in \mathbb{R}^{m \times n}$视为一个表格, 其中每个元素$a_{ij}$属于第$i$行第$j$列: @@ -198,6 +230,12 @@ A = tf.reshape(tf.range(20), (5, 4)) A ``` +```{.python .input} +#@tab paddle +A = paddle.reshape(paddle.arange(20), (5, 4)) +A +``` + 我们可以通过行索引($i$)和列索引($j$)来访问矩阵中的标量元素$a_{ij}$, 例如$[\mathbf{A}]_{ij}$。 如果没有给出矩阵$\mathbf{A}$的标量元素,如在 :eqref:`eq_matrix_def`那样, @@ -207,7 +245,7 @@ A 例如$a_{2,3j}$和$[\mathbf{A}]_{2i-1,3}$。 当我们交换矩阵的行和列时,结果称为矩阵的*转置*(transpose)。 -我们用$\mathbf{a}^\top$来表示矩阵的转置,如果$\mathbf{B}=\mathbf{A}^\top$, +通常用$\mathbf{a}^\top$来表示矩阵的转置,如果$\mathbf{B}=\mathbf{A}^\top$, 则对于任意$i$和$j$,都有$b_{ij}=a_{ji}$。 因此,在 :eqref:`eq_matrix_def`中的转置是一个形状为$n \times m$的矩阵: @@ -221,7 +259,7 @@ $$ \end{bmatrix}. $$ -现在我们在代码中访问(**矩阵的转置**)。 +现在在代码中访问(**矩阵的转置**)。 ```{.python .input} A.T @@ -237,8 +275,13 @@ A.T tf.transpose(A) ``` +```{.python .input} +#@tab paddle +paddle.transpose(A, perm=[1, 0]) +``` + 作为方阵的一种特殊类型,[***对称矩阵*(symmetric matrix)$\mathbf{A}$等于其转置:$\mathbf{A} = \mathbf{A}^\top$**]。 -这里我们定义一个对称矩阵$\mathbf{B}$: +这里定义一个对称矩阵$\mathbf{B}$: ```{.python .input} B = np.array([[1, 2, 3], [2, 0, 4], [3, 4, 5]]) @@ -257,6 +300,12 @@ B = tf.constant([[1, 2, 3], [2, 0, 4], [3, 4, 5]]) B ``` +```{.python .input} +#@tab paddle +B = paddle.to_tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]]) +B +``` + 现在我们将`B`与它的转置进行比较。 ```{.python .input} @@ -273,19 +322,24 @@ B == B.T B == tf.transpose(B) ``` +```{.python .input} +#@tab paddle +B == paddle.transpose(B, perm=[1, 0]) +``` + 矩阵是有用的数据结构:它们允许我们组织具有不同模式的数据。 例如,我们矩阵中的行可能对应于不同的房屋(数据样本),而列可能对应于不同的属性。 -如果你曾经使用过电子表格软件或已阅读过 :numref:`sec_pandas`,这应该听起来很熟悉。 +曾经使用过电子表格软件或已阅读过 :numref:`sec_pandas`的人,应该对此很熟悉。 因此,尽管单个向量的默认方向是列向量,但在表示表格数据集的矩阵中, 将每个数据样本作为矩阵中的行向量更为常见。 -我们将在后面的章节中讲到这点,这种约定将支持常见的深度学习实践。 +后面的章节将讲到这点,这种约定将支持常见的深度学习实践。 例如,沿着张量的最外轴,我们可以访问或遍历小批量的数据样本。 ## 张量 [**就像向量是标量的推广,矩阵是向量的推广一样,我们可以构建具有更多轴的数据结构**]。 -张量(本小节中的“张量”指代数对象)为我们提供了描述具有任意数量轴的$n$维数组的通用方法。 +张量(本小节中的“张量”指代数对象)是描述具有任意数量轴的$n$维数组的通用方法。 例如,向量是一阶张量,矩阵是二阶张量。 张量用特殊字体的大写字母表示(例如,$\mathsf{X}$、$\mathsf{Y}$和$\mathsf{Z}$), 它们的索引机制(例如$x_{ijk}$和$[\mathsf{X}]_{1,2i-1,3}$)与矩阵类似。 @@ -293,7 +347,7 @@ B == tf.transpose(B) 当我们开始处理图像时,张量将变得更加重要,图像以$n$维数组形式出现, 其中3个轴对应于高度、宽度,以及一个*通道*(channel)轴, 用于表示颜色通道(红色、绿色和蓝色)。 -现在,我们先将高阶张量暂放一边,而是专注学习其基础知识。 +现在先将高阶张量暂放一边,而是专注学习其基础知识。 ```{.python .input} X = np.arange(24).reshape(2, 3, 4) @@ -312,10 +366,16 @@ X = tf.reshape(tf.range(24), (2, 3, 4)) X ``` +```{.python .input} +#@tab paddle +X = paddle.reshape(paddle.arange(24), (2, 3, 4)) +X +``` + ## 张量算法的基本性质 标量、向量、矩阵和任意数量轴的张量(本小节中的“张量”指代数对象)有一些实用的属性。 -例如,你可能已经从按元素操作的定义中注意到,任何按元素的一元运算都不会改变其操作数的形状。 +例如,从按元素操作的定义中可以注意到,任何按元素的一元运算都不会改变其操作数的形状。 同样,[**给定具有相同形状的任意两个张量,任何按元素二元运算的结果都将是相同形状的张量**]。 例如,将两个相同形状的矩阵相加,会在这两个矩阵上执行元素加法。 @@ -339,11 +399,17 @@ B = A # 不能通过分配新内存将A克隆到B A, A + B ``` +```{.python .input} +#@tab paddle +A = paddle.reshape(paddle.arange(20, dtype=paddle.float32), (5, 4)) +B = A.clone() # 通过分配新内存,将A的一个副本分配给B +A, A + B +``` + 具体而言,[**两个矩阵的按元素乘法称为*Hadamard积*(Hadamard product)(数学符号$\odot$)**]。 对于矩阵$\mathbf{B} \in \mathbb{R}^{m \times n}$, 其中第$i$行和第$j$列的元素是$b_{ij}$。 矩阵$\mathbf{A}$(在 :eqref:`eq_matrix_def`中定义)和$\mathbf{B}$的Hadamard积为: - $$ \mathbf{A} \odot \mathbf{B} = \begin{bmatrix} @@ -368,6 +434,11 @@ A * B A * B ``` +```{.python .input} +#@tab paddle +A * B +``` + 将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘。 ```{.python .input} @@ -390,13 +461,21 @@ X = tf.reshape(tf.range(24), (2, 3, 4)) a + X, (a * X).shape ``` +```{.python .input} +#@tab paddle +a = 2 +X = paddle.reshape(paddle.arange(24), (2, 3, 4)) +a + X, (a * X).shape +``` + ## 降维 + :label:`subseq_lin-alg-reduction` 我们可以对任意张量进行的一个有用的操作是[**计算其元素的和**]。 -在数学表示法中,我们使用$\sum$符号表示求和。 +数学表示法使用$\sum$符号表示求和。 为了表示长度为$d$的向量中元素的总和,可以记为$\sum_{i=1}^dx_i$。 -在代码中,我们可以调用计算求和的函数: +在代码中可以调用计算求和的函数: ```{.python .input} x = np.arange(4) @@ -415,6 +494,12 @@ x = tf.range(4, dtype=tf.float32) x, tf.reduce_sum(x) ``` +```{.python .input} +#@tab paddle +x = paddle.arange(4, dtype=paddle.float32) +x, x.sum() +``` + 我们可以(**表示任意形状张量的元素和**)。 例如,矩阵$\mathbf{A}$中元素的和可以记为$\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}$。 @@ -432,9 +517,14 @@ A.shape, A.sum() A.shape, tf.reduce_sum(A) ``` +```{.python .input} +#@tab paddle +A.shape, A.sum() +``` + 默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。 我们还可以[**指定张量沿哪一个轴来通过求和降低维度**]。 -以矩阵为例,为了通过求和所有行的元素来降维(轴0),我们可以在调用函数时指定`axis=0`。 +以矩阵为例,为了通过求和所有行的元素来降维(轴0),可以在调用函数时指定`axis=0`。 由于输入矩阵沿0轴降维以生成输出向量,因此输入轴0的维数在输出形状中消失。 ```{.python .input} @@ -454,6 +544,12 @@ A_sum_axis0 = tf.reduce_sum(A, axis=0) A_sum_axis0, A_sum_axis0.shape ``` +```{.python .input} +#@tab paddle +A_sum_axis0 = A.sum(axis=0) +A_sum_axis0, A_sum_axis0.shape +``` + 指定`axis=1`将通过汇总所有列的元素降维(轴1)。因此,输入轴1的维数在输出形状中消失。 ```{.python .input} @@ -473,20 +569,31 @@ A_sum_axis1 = tf.reduce_sum(A, axis=1) A_sum_axis1, A_sum_axis1.shape ``` +```{.python .input} +#@tab paddle +A_sum_axis1 = A.sum(axis=1) +A_sum_axis1, A_sum_axis1.shape +``` + 沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和。 ```{.python .input} -A.sum(axis=[0, 1]) # SameasA.sum() +A.sum(axis=[0, 1]) # 结果和A.sum()相同 ``` ```{.python .input} #@tab pytorch -A.sum(axis=[0, 1]) # SameasA.sum() +A.sum(axis=[0, 1]) # 结果和A.sum()相同 ``` ```{.python .input} #@tab tensorflow -tf.reduce_sum(A, axis=[0, 1]) # Sameastf.reduce_sum(A) +tf.reduce_sum(A, axis=[0, 1]) # 结果和tf.reduce_sum(A)相同 +``` + +```{.python .input} +#@tab paddle +A.sum(axis=[0, 1]) ``` [**一个与求和相关的量是*平均值*(mean或average)**]。 @@ -507,6 +614,11 @@ A.mean(), A.sum() / A.numel() tf.reduce_mean(A), tf.reduce_sum(A) / tf.size(A).numpy() ``` +```{.python .input} +#@tab paddle +A.mean(), A.sum() / A.numel() +``` + 同样,计算平均值的函数也可以沿指定轴降低张量的维度。 ```{.python .input} @@ -523,7 +635,13 @@ A.mean(axis=0), A.sum(axis=0) / A.shape[0] tf.reduce_mean(A, axis=0), tf.reduce_sum(A, axis=0) / A.shape[0] ``` +```{.python .input} +#@tab paddle +A.mean(axis=0), A.sum(axis=0) / A.shape[0] +``` + ### 非降维求和 + :label:`subseq_lin-alg-non-reduction` 但是,有时在调用函数来[**计算总和或均值时保持轴数不变**]会很有用。 @@ -545,6 +663,12 @@ sum_A = tf.reduce_sum(A, axis=1, keepdims=True) sum_A ``` +```{.python .input} +#@tab paddle +sum_A = paddle.sum(A, axis=1, keepdim=True) +sum_A +``` + 例如,由于`sum_A`在对每行进行求和后仍保持两个轴,我们可以(**通过广播将`A`除以`sum_A`**)。 ```{.python .input} @@ -561,8 +685,13 @@ A / sum_A A / sum_A ``` +```{.python .input} +#@tab paddle +A / sum_A +``` + 如果我们想沿[**某个轴计算`A`元素的累积总和**], -比如`axis=0`(按行计算),我们可以调用`cumsum`函数。 +比如`axis=0`(按行计算),可以调用`cumsum`函数。 此函数不会沿任何轴降低输入张量的维度。 ```{.python .input} @@ -579,6 +708,11 @@ A.cumsum(axis=0) tf.cumsum(A, axis=0) ``` +```{.python .input} +#@tab paddle +A.cumsum(axis=0) +``` + ## 点积(Dot Product) 我们已经学习了按元素操作、求和及平均值。 @@ -607,6 +741,12 @@ y = tf.ones(4, dtype=tf.float32) x, y, tf.tensordot(x, y, axes=1) ``` +```{.python .input} +#@tab paddle +y = paddle.ones(shape=[4], dtype='float32') +x, y, paddle.dot(x, y) +``` + 注意,(**我们可以通过执行按元素乘法,然后进行求和来表示两个向量的点积**): ```{.python .input} @@ -623,6 +763,11 @@ torch.sum(x * y) tf.reduce_sum(x * y) ``` +```{.python .input} +#@tab paddle +paddle.sum(x * y) +``` + 点积在很多场合都很有用。 例如,给定一组由向量$\mathbf{x} \in \mathbb{R}^d$表示的值, 和一组由$\mathbf{w} \in \mathbb{R}^d$表示的权重。 @@ -631,11 +776,11 @@ $\mathbf{x}$中的值根据权重$\mathbf{w}$的加权和, 当权重为非负数且和为1(即$\left(\sum_{i=1}^{d}{w_i}=1\right)$)时, 点积表示*加权平均*(weighted average)。 将两个向量规范化得到单位长度后,点积表示它们夹角的余弦。 -我们将在本节的后面正式介绍*长度*(length)的概念。 +本节后面的内容将正式介绍*长度*(length)的概念。 ## 矩阵-向量积 -现在我们知道如何计算点积,我们可以开始理解*矩阵-向量积*(matrix-vector product)。 +现在我们知道如何计算点积,可以开始理解*矩阵-向量积*(matrix-vector product)。 回顾分别在 :eqref:`eq_matrix_def`和 :eqref:`eq_vec_def`中定义的矩阵$\mathbf{A} \in \mathbb{R}^{m \times n}$和向量$\mathbf{x} \in \mathbb{R}^n$。 让我们将矩阵$\mathbf{A}$用它的行向量表示: @@ -667,9 +812,9 @@ $$ \end{bmatrix}. $$ -我们可以把一个矩阵$\mathbf{A} \in \mathbb{R}^{m \times n}$乘法看作是一个从$\mathbb{R}^{n}$到$\mathbb{R}^{m}$向量的转换。 -这些转换是非常有用的。例如,我们可以用方阵的乘法来表示旋转。 -我们将在后续章节中讲到,我们也可以使用矩阵-向量积来描述在给定前一层的值时, +我们可以把一个矩阵$\mathbf{A} \in \mathbb{R}^{m \times n}$乘法看作一个从$\mathbb{R}^{n}$到$\mathbb{R}^{m}$向量的转换。 +这些转换是非常有用的,例如可以用方阵的乘法来表示旋转。 +后续章节将讲到,我们也可以使用矩阵-向量积来描述在给定前一层的值时, 求解神经网络每一层所需的复杂计算。 :begin_tab:`mxnet` @@ -679,7 +824,7 @@ $$ :end_tab: :begin_tab:`pytorch` -在代码中使用张量表示矩阵-向量积,我们使用与点积相同的`mv`函数。 +在代码中使用张量表示矩阵-向量积,我们使用`mv`函数。 当我们为矩阵`A`和向量`x`调用`torch.mv(A, x)`时,会执行矩阵-向量积。 注意,`A`的列维数(沿轴1的长度)必须与`x`的维数(其长度)相同。 :end_tab: @@ -704,12 +849,17 @@ A.shape, x.shape, torch.mv(A, x) A.shape, x.shape, tf.linalg.matvec(A, x) ``` +```{.python .input} +#@tab paddle +A.shape, x.shape, paddle.mv(A, x) +``` + ## 矩阵-矩阵乘法 -如果你已经掌握了点积和矩阵-向量积的知识, +在掌握点积和矩阵-向量积的知识后, 那么**矩阵-矩阵乘法**(matrix-matrix multiplication)应该很简单。 -假设我们有两个矩阵$\mathbf{A} \in \mathbb{R}^{n \times k}$和$\mathbf{B} \in \mathbb{R}^{k \times m}$: +假设有两个矩阵$\mathbf{A} \in \mathbb{R}^{n \times k}$和$\mathbf{B} \in \mathbb{R}^{k \times m}$: $$\mathbf{A}=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1k} \\ @@ -737,7 +887,6 @@ $$\mathbf{A}= \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{bmatrix}. $$ - 当我们简单地将每个元素$c_{ij}$计算为点积$\mathbf{a}^\top_i \mathbf{b}_j$: $$\mathbf{C} = \mathbf{AB} = \begin{bmatrix} @@ -757,7 +906,7 @@ $$\mathbf{C} = \mathbf{AB} = \begin{bmatrix} \end{bmatrix}. $$ -[**我们可以将矩阵-矩阵乘法$\mathbf{AB}$看作是简单地执行$m$次矩阵-向量积,并将结果拼接在一起,形成一个$n \times m$矩阵**]。 +[**我们可以将矩阵-矩阵乘法$\mathbf{AB}$看作简单地执行$m$次矩阵-向量积,并将结果拼接在一起,形成一个$n \times m$矩阵**]。 在下面的代码中,我们在`A`和`B`上执行矩阵乘法。 这里的`A`是一个5行4列的矩阵,`B`是一个4行3列的矩阵。 两者相乘后,我们得到了一个5行3列的矩阵。 @@ -779,13 +928,19 @@ B = tf.ones((4, 3), tf.float32) tf.matmul(A, B) ``` +```{.python .input} +#@tab paddle +B = paddle.ones(shape=[4, 3], dtype='float32') +paddle.mm(A, B) +``` + 矩阵-矩阵乘法可以简单地称为**矩阵乘法**,不应与"Hadamard积"混淆。 ## 范数 :label:`subsec_lin-algebra-norms` 线性代数中最有用的一些运算符是*范数*(norm)。 -非正式地说,一个向量的*范数*告诉我们一个向量有多大。 +非正式地说,向量的*范数*是表示一个向量有多大。 这里考虑的*大小*(size)概念不涉及维度,而是分量的大小。 在线性代数中,向量范数是将向量映射到标量的函数$f$。 @@ -795,7 +950,7 @@ tf.matmul(A, B) $$f(\alpha \mathbf{x}) = |\alpha| f(\mathbf{x}).$$ -第二个性质是我们熟悉的三角不等式: +第二个性质是熟悉的三角不等式: $$f(\mathbf{x} + \mathbf{y}) \leq f(\mathbf{x}) + f(\mathbf{y}).$$ @@ -808,8 +963,8 @@ $$f(\mathbf{x}) \geq 0.$$ $$\forall i, [\mathbf{x}]_i = 0 \Leftrightarrow f(\mathbf{x})=0.$$ -你可能会注意到,范数听起来很像距离的度量。 -如果你还记得欧几里得距离和毕达哥拉斯定理,那么非负性的概念和三角不等式可能会给你一些启发。 +范数听起来很像距离的度量。 +欧几里得距离和毕达哥拉斯定理中的非负性概念和三角不等式可能会给出一些启发。 事实上,欧几里得距离是一个$L_2$范数: 假设$n$维向量$\mathbf{x}$中的元素是$x_1,\ldots,x_n$,其[**$L_2$*范数*是向量元素平方和的平方根:**] @@ -835,8 +990,13 @@ u = tf.constant([3.0, -4.0]) tf.norm(u) ``` -在深度学习中,我们更经常地使用$L_2$范数的平方。 -你还会经常遇到[**$L_1$范数,它表示为向量元素的绝对值之和:**] +```{.python .input} +#@tab paddle +u = paddle.to_tensor([3.0, -4.0]) +paddle.norm(u) +``` + +深度学习中更经常地使用$L_2$范数的平方,也会经常遇到[**$L_1$范数,它表示为向量元素的绝对值之和:**] (**$$\|\mathbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|.$$**) @@ -857,6 +1017,11 @@ torch.abs(u).sum() tf.reduce_sum(tf.abs(u)) ``` +```{.python .input} +#@tab paddle +paddle.abs(u).sum() +``` + $L_2$范数和$L_1$范数都是更一般的$L_p$范数的特例: $$\|\mathbf{x}\|_p = \left(\sum_{i=1}^n \left|x_i \right|^p \right)^{1/p}.$$ @@ -882,7 +1047,13 @@ torch.norm(torch.ones((4, 9))) tf.norm(tf.ones((4, 9))) ``` +```{.python .input} +#@tab paddle +paddle.norm(paddle.ones(shape=[4, 9], dtype='float32')) +``` + ### 范数和目标 + :label:`subsec_norms_and_objectives` 在深度学习中,我们经常试图解决优化问题: @@ -893,14 +1064,14 @@ tf.norm(tf.ones((4, 9))) ## 关于线性代数的更多信息 -仅用一节,我们就教会了你所需的、用以理解现代深度学习的线性代数。 +仅用一节,我们就教会了阅读本书所需的、用以理解现代深度学习的线性代数。 线性代数还有很多,其中很多数学对于机器学习非常有用。 例如,矩阵可以分解为因子,这些分解可以显示真实世界数据集中的低维结构。 机器学习的整个子领域都侧重于使用矩阵分解及其向高阶张量的泛化,来发现数据集中的结构并解决预测问题。 -我们相信,一旦你开始动手尝试并在真实数据集上应用了有效的机器学习模型,你会更倾向于学习更多数学。 -因此,这一节到此结束,我们保留在后面介绍更多数学知识的权利。 +当开始动手尝试并在真实数据集上应用了有效的机器学习模型,你会更倾向于学习更多数学。 +因此,这一节到此结束,本书将在后面介绍更多数学知识。 -如果你渴望了解有关线性代数的更多信息,你可以参考[线性代数运算的在线附录](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/geometry-linear-algebraic-ops.html)或其他优秀资源 :cite:`Strang.1993,Kolter.2008,Petersen.Pedersen.ea.2008`。 +如果渴望了解有关线性代数的更多信息,可以参考[线性代数运算的在线附录](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/geometry-linear-algebraic-ops.html)或其他优秀资源 :cite:`Strang.1993,Kolter.2008,Petersen.Pedersen.ea.2008`。 ## 小结 @@ -917,9 +1088,9 @@ tf.norm(tf.ones((4, 9))) 1. 证明一个矩阵$\mathbf{A}$的转置的转置是$\mathbf{A}$,即$(\mathbf{A}^\top)^\top = \mathbf{A}$。 1. 给出两个矩阵$\mathbf{A}$和$\mathbf{B}$,证明“它们转置的和”等于“它们和的转置”,即$\mathbf{A}^\top + \mathbf{B}^\top = (\mathbf{A} + \mathbf{B})^\top$。 1. 给定任意方阵$\mathbf{A}$,$\mathbf{A} + \mathbf{A}^\top$总是对称的吗?为什么? -1. 我们在本节中定义了形状$(2,3,4)$的张量`X`。`len(X)`的输出结果是什么? +1. 本节中定义了形状$(2,3,4)$的张量`X`。`len(X)`的输出结果是什么? 1. 对于任意形状的张量`X`,`len(X)`是否总是对应于`X`特定轴的长度?这个轴是什么? -1. 运行`A/A.sum(axis=1)`,看看会发生什么。你能分析原因吗? +1. 运行`A/A.sum(axis=1)`,看看会发生什么。请分析一下原因? 1. 考虑一个具有形状$(2,3,4)$的张量,在轴0、1、2上的求和输出是什么形状? 1. 为`linalg.norm`函数提供3个或更多轴的张量,并观察其输出。对于任意形状的张量这个函数计算得到什么? @@ -934,3 +1105,7 @@ tf.norm(tf.ones((4, 9))) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1753) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11682) +:end_tab: diff --git a/chapter_preliminaries/lookup-api.md b/chapter_preliminaries/lookup-api.md index 8129880c6..e79217a6f 100644 --- a/chapter_preliminaries/lookup-api.md +++ b/chapter_preliminaries/lookup-api.md @@ -1,53 +1,61 @@ # 查阅文档 :begin_tab:`mxnet` -由于本书篇幅限制,我们不可能介绍每一个MXNet函数和类(你可能也不希望我们这样做)。 +由于篇幅限制,本书不可能介绍每一个MXNet函数和类。 API文档、其他教程和示例提供了本书之外的大量文档。 -在本节中,我们为你提供了一些查看MXNet API的指导。 +本节提供了一些查看MXNet API的指导。 :end_tab: :begin_tab:`pytorch` -由于本书篇幅限制,我们不可能介绍每一个PyTorch函数和类(你可能也不希望我们这样做)。 +由于篇幅限制,本书不可能介绍每一个PyTorch函数和类。 API文档、其他教程和示例提供了本书之外的大量文档。 -在本节中,我们为你提供了一些查看PyTorch API的指导。 +本节提供了一些查看PyTorch API的指导。 :end_tab: :begin_tab:`tensorflow` -由于本书篇幅限制,我们不可能介绍每一个TensorFlow函数和类(你可能也不希望我们这样做)。 +由于篇幅限制,本书不可能介绍每一个TensorFlow函数和类。 API文档、其他教程和示例提供了本书之外的大量文档。 -在本节中,我们为你提供了一些查TensorFlow API的指导。 +本节提供了一些查TensorFlow API的指导。 :end_tab: ## 查找模块中的所有函数和类 -为了知道模块中可以调用哪些函数和类,我们调用`dir`函数。 +为了知道模块中可以调用哪些函数和类,可以调用`dir`函数。 例如,我们可以(**查询随机数生成模块中的所有属性:**) -```{.python .input n=1} +```{.python .input} from mxnet import np print(dir(np.random)) ``` -```{.python .input n=1} +```{.python .input} #@tab pytorch import torch print(dir(torch.distributions)) ``` -```{.python .input n=1} +```{.python .input} #@tab tensorflow import tensorflow as tf print(dir(tf.random)) ``` -通常,我们可以忽略以“`__`”(双下划线)开始和结束的函数(它们是Python中的特殊对象), -或以单个“`_`”(单下划线)开始的函数(它们通常是内部函数)。 +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +print(dir(paddle.distribution)) +``` + +通常可以忽略以“`__`”(双下划线)开始和结束的函数,它们是Python中的特殊对象, +或以单个“`_`”(单下划线)开始的函数,它们通常是内部函数。 根据剩余的函数名或属性名,我们可能会猜测这个模块提供了各种生成随机数的方法, 包括从均匀分布(`uniform`)、正态分布(`normal`)和多项分布(`multinomial`)中采样。 ## 查找特定函数和类的用法 -有关如何使用给定函数或类的更具体说明,我们可以调用`help`函数。 +有关如何使用给定函数或类的更具体说明,可以调用`help`函数。 例如,我们来[**查看张量`ones`函数的用法。**] ```{.python .input} @@ -64,8 +72,13 @@ help(torch.ones) help(tf.ones) ``` +```{.python .input} +#@tab paddle +help(paddle.ones) +``` + 从文档中,我们可以看到`ones`函数创建一个具有指定形状的新张量,并将所有元素值设置为1。 -让我们来[**运行一个快速测试**]来确认这一解释: +下面来[**运行一个快速测试**]来确认这一解释: ```{.python .input} np.ones(4) @@ -81,6 +94,11 @@ torch.ones(4) tf.ones(4) ``` +```{.python .input} +#@tab paddle +paddle.ones([4], dtype='float32') +``` + 在Jupyter记事本中,我们可以使用`?`指令在另一个浏览器窗口中显示文档。 例如,`list?`指令将创建与`help(list)`指令几乎相同的内容,并在新的浏览器窗口中显示它。 此外,如果我们使用两个问号,如`list??`,将显示实现该函数的Python代码。 @@ -88,11 +106,11 @@ tf.ones(4) ## 小结 * 官方文档提供了本书之外的大量描述和示例。 -* 我们可以通过调用`dir`和`help`函数或在Jupyter记事本中使用`?`和`??`查看API的用法文档。 +* 可以通过调用`dir`和`help`函数或在Jupyter记事本中使用`?`和`??`查看API的用法文档。 ## 练习 -1. 在深度学习框架中查找任何函数或类的文档。你能在这个框架的官方网站上找到文档吗? +1. 在深度学习框架中查找任何函数或类的文档。请尝试在这个框架的官方网站上找到文档。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1764) @@ -105,3 +123,7 @@ tf.ones(4) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1763) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11686) +:end_tab: diff --git a/chapter_preliminaries/ndarray.md b/chapter_preliminaries/ndarray.md index 186465383..bce0c7ff5 100644 --- a/chapter_preliminaries/ndarray.md +++ b/chapter_preliminaries/ndarray.md @@ -53,8 +53,14 @@ import torch import tensorflow as tf ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle +``` -[**张量表示由一个数值组成的数组,这个数组可能有多个维度**]。 +[**张量表示一个由数值组成的数组,这个数组可能有多个维度**]。 具有一个轴的张量对应数学上的*向量*(vector); 具有两个轴的张量对应数学上的*矩阵*(matrix); 具有两个轴以上的张量没有特殊的数学名称。 @@ -71,7 +77,6 @@ import tensorflow as tf 首先,我们可以使用 `range` 创建一个行向量 `x`。这个行向量包含以0开始的前12个整数,它们默认创建为整数。也可指定创建类型为浮点数。张量中的每个值都称为张量的 *元素*(element)。例如,张量 `x` 中有 12 个元素。除非额外指定,新的张量将存储在内存中,并采用基于CPU的计算。 :end_tab: - ```{.python .input} x = np.arange(12) x @@ -89,6 +94,12 @@ x = tf.range(12) x ``` +```{.python .input} +#@tab paddle +x = paddle.arange(12) +x +``` + [**可以通过张量的`shape`属性来访问张量(沿每个轴的长度)的*形状***] (~~和张量中元素的总数~~)。 @@ -114,6 +125,11 @@ x.numel() tf.size(x) ``` +```{.python .input} +#@tab paddle +x.numel() +``` + [**要想改变一个张量的形状而不改变元素数量和元素值,可以调用`reshape`函数。**] 例如,可以把张量`x`从形状为(12,)的行向量转换为形状为(3,4)的矩阵。 这个新的张量包含与转换前相同的值,但是它被看成一个3行4列的矩阵。 @@ -132,6 +148,12 @@ X = tf.reshape(x, (3, 4)) X ``` +```{.python .input} +#@tab paddle +X = paddle.reshape(x, (3, 4)) +X +``` + 我们不需要通过手动指定每个维度来改变形状。 也就是说,如果我们的目标形状是(高度,宽度), 那么在知道宽度后,高度会被自动计算得出,不必我们自己做除法。 @@ -156,6 +178,11 @@ torch.zeros((2, 3, 4)) tf.zeros((2, 3, 4)) ``` +```{.python .input} +#@tab paddle +paddle.zeros((2, 3, 4)) +``` + 同样,我们可以创建一个形状为`(2,3,4)`的张量,其中所有元素都设置为1。代码如下: ```{.python .input} @@ -172,6 +199,11 @@ torch.ones((2, 3, 4)) tf.ones((2, 3, 4)) ``` +```{.python .input} +#@tab paddle +paddle.ones((2, 3, 4)) +``` + 有时我们想通过从某个特定的概率分布中随机采样来得到张量中每个元素的值。 例如,当我们构造数组来作为神经网络中的参数时,我们通常会随机初始化参数的值。 以下代码创建一个形状为(3,4)的张量。 @@ -191,6 +223,11 @@ torch.randn(3, 4) tf.random.normal(shape=[3, 4]) ``` +```{.python .input} +#@tab paddle +paddle.randn((3, 4),'float32') +``` + 我们还可以[**通过提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值**]。 在这里,最外层的列表对应于轴0,内层的列表对应于轴1。 @@ -208,6 +245,11 @@ torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) tf.constant([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) ``` +```{.python .input} +#@tab paddle +paddle.to_tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) +``` + ## 运算符 我们的兴趣不仅限于读取数据和写入数据。 @@ -253,6 +295,13 @@ y = tf.constant([2.0, 2, 2, 2]) x + y, x - y, x * y, x / y, x ** y # **运算符是求幂运算 ``` +```{.python .input} +#@tab paddle +x = paddle.to_tensor([1.0, 2, 4, 8]) +y = paddle.to_tensor([2, 2, 2, 2]) +x + y, x - y, x * y, x / y, x**y # **运算符是求幂运算 +``` + (**“按元素”方式可以应用更多的计算**),包括像求幂这样的一元运算符。 ```{.python .input} @@ -269,6 +318,11 @@ torch.exp(x) tf.exp(x) ``` +```{.python .input} +#@tab paddle +paddle.exp(x) +``` + 除了按元素计算外,我们还可以执行线性代数运算,包括向量点积和矩阵乘法。 我们将在 :numref:`sec_linear-algebra`中解释线性代数的重点内容。 @@ -300,6 +354,13 @@ Y = tf.constant([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) tf.concat([X, Y], axis=0), tf.concat([X, Y], axis=1) ``` +```{.python .input} +#@tab paddle +X = paddle.arange(12, dtype='float32').reshape((3, 4)) +Y = paddle.to_tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) +paddle.concat((X, Y), axis=0), paddle.concat((X, Y), axis=1) +``` + 有时,我们想[**通过*逻辑运算符*构建二元张量**]。 以`X == Y`为例: 对于每个位置,如果`X`和`Y`在该位置相等,则新张量中相应项的值为1。 @@ -313,7 +374,7 @@ X == Y [**对张量中的所有元素进行求和,会产生一个单元素张量。**] ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle X.sum() ``` @@ -328,9 +389,10 @@ tf.reduce_sum(X) 在上面的部分中,我们看到了如何在相同形状的两个张量上执行按元素操作。 在某些情况下,[**即使形状不同,我们仍然可以通过调用 *广播机制*(broadcasting mechanism)来执行按元素操作**]。 -这种机制的工作方式如下:首先,通过适当复制元素来扩展一个或两个数组, -以便在转换之后,两个张量具有相同的形状。 -其次,对生成的数组执行按元素操作。 +这种机制的工作方式如下: + +1. 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状; +2. 对生成的数组执行按元素操作。 在大多数情况下,我们将沿着数组中长度为1的轴进行广播,如下例子: @@ -354,6 +416,13 @@ b = tf.reshape(tf.range(2), (1, 2)) a, b ``` +```{.python .input} +#@tab paddle +a = paddle.reshape(paddle.arange(3), (3, 1)) +b = paddle.reshape(paddle.arange(2), (1, 2)) +a, b +``` + 由于`a`和`b`分别是$3\times1$和$1\times2$矩阵,如果让它们相加,它们的形状不匹配。 我们将两个矩阵*广播*为一个更大的$3\times2$矩阵,如下所示:矩阵`a`将复制列, 矩阵`b`将复制行,然后再按元素相加。 @@ -389,7 +458,7 @@ TensorFlow中的`Variables`是支持赋值的可变容器。 :end_tab: ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle X[1, 2] = 9 X ``` @@ -406,7 +475,7 @@ X_var 虽然我们讨论的是矩阵的索引,但这也适用于向量和超过2个维度的张量。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle X[0:2, :] = 12 X ``` @@ -435,11 +504,10 @@ Y = Y + X id(Y) == before ``` -这可能是不可取的,原因有两个:首先,我们不想总是不必要地分配内存。 -在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。 -通常情况下,我们希望原地执行这些更新。 -其次,如果我们不原地更新,其他引用仍然会指向旧的内存位置, -这样我们的某些代码可能会无意中引用旧的参数。 +这可能是不可取的,原因有两个: + +1. 首先,我们不想总是不必要地分配内存。在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。通常情况下,我们希望原地执行这些更新; +2. 如果我们不原地更新,其他引用仍然会指向旧的内存位置,这样我们的某些代码可能会无意中引用旧的参数。 :begin_tab:`mxnet, pytorch` 幸运的是,(**执行原地操作**)非常简单。 @@ -478,6 +546,14 @@ Z.assign(X + Y) print('id(Z):', id(Z)) ``` +```{.python .input} +#@tab paddle +Z = paddle.zeros_like(Y) +print('id(Z):', id(Z)) +Z = X + Y +print('id(Z):', id(Z)) +``` + :begin_tab:`mxnet, pytorch` [**如果在后续计算中没有重复使用`X`, 我们也可以使用`X[:] = X + Y`或`X += Y`来减少操作的内存开销。**] @@ -497,7 +573,7 @@ print('id(Z):', id(Z)) :end_tab: ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle before = id(X) X += Y id(X) == before @@ -520,8 +596,8 @@ computation(X, Y) :begin_tab:`mxnet, tensorflow` 将深度学习框架定义的张量[**转换为NumPy张量(`ndarray`)**]很容易,反之也同样容易。 转换后的结果不共享内存。 -这个小的不便实际上是非常重要的:当你在CPU或GPU上执行操作的时候, -如果Python的NumPy包也希望使用相同的内存块执行其他操作,你不希望停下计算来等它。 +这个小的不便实际上是非常重要的:当在CPU或GPU上执行操作的时候, +如果Python的NumPy包也希望使用相同的内存块执行其他操作,人们不希望停下计算来等它。 :end_tab: :begin_tab:`pytorch` @@ -549,6 +625,13 @@ B = tf.constant(A) type(A), type(B) ``` +```{.python .input} +#@tab paddle +A = X.numpy() +B = paddle.to_tensor(A) +type(A), type(B) +``` + 要(**将大小为1的张量转换为Python标量**),我们可以调用`item`函数或Python的内置函数。 ```{.python .input} @@ -568,6 +651,12 @@ a = tf.constant([3.5]).numpy() a, a.item(), float(a), int(a) ``` +```{.python .input} +#@tab paddle +a = paddle.to_tensor([3.5]) +a, a.item(), float(a), int(a) +``` + ## 小结 * 深度学习存储和操作数据的主要接口是张量($n$维数组)。它提供了各种功能,包括基本数学运算、广播、索引、切片、内存节省和转换其他Python对象。 @@ -588,3 +677,7 @@ a, a.item(), float(a), int(a) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1746) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11680) +:end_tab: diff --git a/chapter_preliminaries/pandas.md b/chapter_preliminaries/pandas.md index 6bd4e0b7b..7729d6e93 100644 --- a/chapter_preliminaries/pandas.md +++ b/chapter_preliminaries/pandas.md @@ -6,7 +6,7 @@ 在Python中常用的数据分析工具中,我们通常使用`pandas`软件包。 像庞大的Python生态系统中的许多其他扩展包一样,`pandas`可以与张量兼容。 本节我们将简要介绍使用`pandas`预处理原始数据,并将原始数据转换为张量格式的步骤。 -我们将在后面的章节中介绍更多的数据预处理技术。 +后面的章节将介绍更多的数据预处理技术。 ## 读取数据集 @@ -99,6 +99,16 @@ X, y = tf.constant(inputs.values), tf.constant(outputs.values) X, y ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings(action='ignore') +import paddle + +X, y = paddle.to_tensor(inputs.values), paddle.to_tensor(outputs.values) +X, y +``` + ## 小结 * `pandas`软件包是Python中常用的数据分析工具中,`pandas`可以与张量兼容。 @@ -122,3 +132,7 @@ X, y :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1748) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11681) +:end_tab: diff --git a/chapter_preliminaries/probability.md b/chapter_preliminaries/probability.md index 65bef6be8..f5f9f49c1 100644 --- a/chapter_preliminaries/probability.md +++ b/chapter_preliminaries/probability.md @@ -11,9 +11,9 @@ 例如,假设我们为一家大型在线书店工作,我们可能希望估计某些用户购买特定图书的概率。 为此,我们需要使用概率学。 有完整的课程、专业、论文、职业、甚至院系,都致力于概率学的工作。 -所以很自然地,我们在这部分的目标不是教授你整个科目。 -相反,我们希望教给你在基础的概率知识,使你能够开始构建你的第一个深度学习模型, -以便你可以开始自己探索它。 +所以很自然地,我们在这部分的目标不是教授整个科目。 +相反,我们希望教给读者基础的概率知识,使读者能够开始构建第一个深度学习模型, +以便读者可以开始自己探索它。 现在让我们更认真地考虑第一个例子:根据照片区分猫和狗。 这听起来可能很简单,但对于机器却可能是一个艰巨的挑战。 @@ -82,8 +82,19 @@ import tensorflow_probability as tfp import numpy as np ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import random +import numpy as np +``` + 在统计学中,我们把从概率分布中抽取样本的过程称为*抽样*(sampling)。 -笼统来说,可以把*分布*(distribution)看作是对事件的概率分配, +笼统来说,可以把*分布*(distribution)看作对事件的概率分配, 稍后我们将给出的更正式定义。 将概率分配给一些离散选择的分布称为*多项分布*(multinomial distribution)。 @@ -107,6 +118,12 @@ fair_probs = tf.ones(6) / 6 tfp.distributions.Multinomial(1, fair_probs).sample() ``` +```{.python .input} +#@tab paddle +fair_probs = [1.0 / 6] * 6 +paddle.distribution.Multinomial(1, paddle.to_tensor(fair_probs)).sample() +``` + 在估计一个骰子的公平性时,我们希望从同一分布中生成多个样本。 如果用Python的for循环来完成这个任务,速度会慢得惊人。 因此我们使用深度学习框架的函数同时抽取多个样本,得到我们想要的任意形状的独立样本数组。 @@ -125,6 +142,11 @@ multinomial.Multinomial(10, fair_probs).sample() tfp.distributions.Multinomial(10, fair_probs).sample() ``` +```{.python .input} +#@tab paddle +paddle.distribution.Multinomial(10, paddle.to_tensor(fair_probs)).sample() +``` + 现在我们知道如何对骰子进行采样,我们可以模拟1000次投掷。 然后,我们可以统计1000次投掷后,每个数字被投中了多少次。 具体来说,我们计算相对频率,以作为真实概率的估计。 @@ -147,6 +169,12 @@ counts = tfp.distributions.Multinomial(1000, fair_probs).sample() counts / 1000 ``` +```{.python .input} +#@tab paddle +counts = paddle.distribution.Multinomial(1000, paddle.to_tensor(fair_probs)).sample() +counts / 1000 +``` + 因为我们是从一个公平的骰子中生成的数据,我们知道每个结果都有真实的概率$\frac{1}{6}$, 大约是$0.167$,所以上面输出的估计值看起来不错。 @@ -200,6 +228,23 @@ d2l.plt.gca().set_ylabel('Estimated probability') d2l.plt.legend(); ``` +```{.python .input} +#@tab paddle +counts = paddle.distribution.Multinomial(10, paddle.to_tensor(fair_probs)).sample((500,1)) +cum_counts = counts.cumsum(axis=0) +cum_counts = cum_counts.squeeze(axis=1) +estimates = cum_counts / cum_counts.sum(axis=1, keepdim=True) + +d2l.set_figsize((6, 4.5)) +for i in range(6): + d2l.plt.plot(estimates[:, i], + label=("P(die=" + str(i + 1) + ")")) +d2l.plt.axhline(y=0.167, color='black', linestyle='dashed') +d2l.plt.gca().set_xlabel('Groups of experiments') +d2l.plt.gca().set_ylabel('Estimated probability') +d2l.plt.legend() +``` + 每条实线对应于骰子的6个值中的一个,并给出骰子在每组实验后出现值的估计概率。 当我们通过更多的实验获得更多的数据时,这$6$条实体曲线向真实概率收敛。 @@ -248,13 +293,13 @@ d2l.plt.legend(); 请注意,*离散*(discrete)随机变量(如骰子的每一面) 和*连续*(continuous)随机变量(如人的体重和身高)之间存在微妙的区别。 现实生活中,测量两个人是否具有完全相同的身高没有太大意义。 -如果我们进行足够精确的测量,你会发现这个星球上没有两个人具有完全相同的身高。 +如果我们进行足够精确的测量,最终会发现这个星球上没有两个人具有完全相同的身高。 在这种情况下,询问某人的身高是否落入给定的区间,比如是否在1.79米和1.81米之间更有意义。 在这些情况下,我们将这个看到某个数值的可能性量化为*密度*(density)。 高度恰好为1.80米的概率为0,但密度不是0。 在任何两个不同高度之间的区间,我们都有非零的概率。 在本节的其余部分中,我们将考虑离散空间中的概率。 -对于连续随机变量的概率,你可以参考深度学习数学附录中[随机变量](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/random-variables.html) +连续随机变量的概率可以参考深度学习数学附录中[随机变量](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/random-variables.html) 的一节。 ## 处理多个随机变量 @@ -359,7 +404,6 @@ $$\begin{aligned} =& 0.011485. \end{aligned} $$ - 因此,我们得到 $$\begin{aligned} @@ -396,7 +440,6 @@ $$\begin{aligned} =& 0.98. \end{aligned} $$ - 现在我们可以应用边际化和乘法规则: $$\begin{aligned} @@ -451,7 +494,7 @@ $$\mathrm{Var}[f(x)] = E\left[\left(f(x) - E[f(x)]\right)^2\right].$$ 1. 进行$m=500$组实验,每组抽取$n=10$个样本。改变$m$和$n$,观察和分析实验结果。 2. 给定两个概率为$P(\mathcal{A})$和$P(\mathcal{B})$的事件,计算$P(\mathcal{A} \cup \mathcal{B})$和$P(\mathcal{A} \cap \mathcal{B})$的上限和下限。(提示:使用[友元图](https://en.wikipedia.org/wiki/Venn_diagram)来展示这些情况。) -3. 假设我们有一系列随机变量,例如$A$、$B$和$C$,其中$B$只依赖于$A$,而$C$只依赖于$B$,你能简化联合概率$P(A, B, C)$吗?(提示:这是一个[马尔可夫链](https://en.wikipedia.org/wiki/Markov_chain)。) +3. 假设我们有一系列随机变量,例如$A$、$B$和$C$,其中$B$只依赖于$A$,而$C$只依赖于$B$,能简化联合概率$P(A, B, C)$吗?(提示:这是一个[马尔可夫链](https://en.wikipedia.org/wiki/Markov_chain)。) 4. 在 :numref:`subsec_probability_hiv_app`中,第一个测试更准确。为什么不运行第一个测试两次,而是同时运行第一个和第二个测试? :begin_tab:`mxnet` @@ -465,3 +508,7 @@ $$\mathrm{Var}[f(x)] = E\left[\left(f(x) - E[f(x)]\right)^2\right].$$ :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1760) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11685) +:end_tab: diff --git a/chapter_recurrent-modern/beam-search.md b/chapter_recurrent-modern/beam-search.md index a6a461bba..1573634d9 100644 --- a/chapter_recurrent-modern/beam-search.md +++ b/chapter_recurrent-modern/beam-search.md @@ -3,7 +3,7 @@ 在 :numref:`sec_seq2seq`中,我们逐个预测输出序列, 直到预测序列中出现特定的序列结束词元“<eos>”。 -在本节中,我们将首先介绍*贪心搜索*(greedy search)策略, +本节将首先介绍*贪心搜索*(greedy search)策略, 并探讨其存在的问题,然后对比其他替代策略: *穷举搜索*(exhaustive search)和*束搜索*(beam search)。 @@ -36,12 +36,12 @@ $$y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} P(y \mid y_1, \ldots, y_{t :label:`fig_s2s-prob1` 如 :numref:`fig_s2s-prob1`中, -假设输出中有四个词元“A”、“B”、“C”和“<eos>”。 +假设输出中有四个词元“A”“B”“C”和“<eos>”。 每个时间步下的四个数字分别表示在该时间步 -生成“A”、“B”、“C”和“<eos>”的条件概率。 +生成“A”“B”“C”和“<eos>”的条件概率。 在每个时间步,贪心搜索选择具有最高条件概率的词元。 因此,将在 :numref:`fig_s2s-prob1`中 -预测输出序列“A”、“B”、“C”和“<eos>”。 +预测输出序列“A”“B”“C”和“<eos>”。 这个输出序列的条件概率是 $0.5\times0.4\times0.4\times0.6 = 0.048$。 @@ -63,16 +63,16 @@ $\prod_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$ :numref:`fig_s2s-prob2`中的“A”和“C”, 因此时间步$3$处的每个词元的条件概率也在 :numref:`fig_s2s-prob2`中改变。 假设我们在时间步$3$选择词元“B”, -于是当前的时间步$4$基于前三个时间步的输出子序列“A”、“C”和“B”为条件, -这与 :numref:`fig_s2s-prob1`中的“A”、“B”和“C”不同。 +于是当前的时间步$4$基于前三个时间步的输出子序列“A”“C”和“B”为条件, +这与 :numref:`fig_s2s-prob1`中的“A”“B”和“C”不同。 因此,在 :numref:`fig_s2s-prob2`中的时间步$4$生成 每个词元的条件概率也不同于 :numref:`fig_s2s-prob1`中的条件概率。 结果, :numref:`fig_s2s-prob2`中的输出序列 -“A”、“C”、“B”和“<eos>”的条件概率为 +“A”“C”“B”和“<eos>”的条件概率为 $0.5\times0.3 \times0.6\times0.6=0.054$, 这大于 :numref:`fig_s2s-prob1`中的贪心搜索的条件概率。 这个例子说明:贪心搜索获得的输出序列 -“A”、“B”、“C”和“<eos>” +“A”“B”“C”和“<eos>” 不一定是最佳序列。 ## 穷举搜索 @@ -148,7 +148,7 @@ $\alpha$通常设置为$0.75$。 束搜索的计算量为$\mathcal{O}(k\left|\mathcal{Y}\right|T')$, 这个结果介于贪心搜索和穷举搜索之间。 -实际上,贪心搜索可以看作是一种束宽为$1$的特殊类型的束搜索。 +实际上,贪心搜索可以看作一种束宽为$1$的特殊类型的束搜索。 通过灵活地选择束宽,束搜索可以在正确率和计算代价之间进行权衡。 ## 小结 @@ -164,6 +164,6 @@ $\alpha$通常设置为$0.75$。 1. 在 :numref:`sec_seq2seq`的机器翻译问题中应用束搜索。 束宽是如何影响预测的速度和结果的? 1. 在 :numref:`sec_rnn_scratch`中,我们基于用户提供的前缀, - 通过使用语言模型来生成文本。这个例子中使用了哪种搜索策略?你能改进吗? + 通过使用语言模型来生成文本。这个例子中使用了哪种搜索策略?可以改进吗? [Discussions](https://discuss.d2l.ai/t/5768) diff --git a/chapter_recurrent-modern/bi-rnn.md b/chapter_recurrent-modern/bi-rnn.md index da94a03bc..9584913dd 100644 --- a/chapter_recurrent-modern/bi-rnn.md +++ b/chapter_recurrent-modern/bi-rnn.md @@ -7,7 +7,7 @@ 对下一个输出进行建模。 虽然这是一个典型情景,但不是唯一的。 还可能发生什么其它的情况呢? -我们考虑以下三个在文本序列中填空的任务: +我们考虑以下三个在文本序列中填空的任务。 * 我`___`。 * 我`___`饿了。 @@ -75,7 +75,7 @@ $$\begin{aligned} =& \sum_{h_T} \pi_T(h_T) P(x_T \mid h_T). \end{aligned}$$ -通常,我们将“前向递归”(forward recursion)写为: +通常,我们将*前向递归*(forward recursion)写为: $$\pi_{t+1}(h_{t+1}) = \sum_{h_t} \pi_t(h_t) P(x_t \mid h_t) P(h_{t+1} \mid h_t).$$ @@ -98,7 +98,7 @@ $$\begin{aligned} =& \sum_{h_1} P(h_1) P(x_1 \mid h_1)\rho_{1}(h_{1}). \end{aligned}$$ -因此,我们可以将“后向递归”(backward recursion)写为: +因此,我们可以将*后向递归*(backward recursion)写为: $$\rho_{t-1}(h_{t-1})= \sum_{h_{t}} P(h_{t} \mid h_{t-1}) P(x_{t} \mid h_{t}) \rho_{t}(h_{t}),$$ @@ -149,7 +149,7 @@ $$P(x_j \mid x_{-j}) \propto \sum_{h_j} \pi_j(h_j) \rho_j(h_j) P(x_j \mid h_j).$ 对于任意时间步$t$,给定一个小批量的输入数据 $\mathbf{X}_t \in \mathbb{R}^{n \times d}$ -(样本数:$n$,每个示例中的输入数:$d$), +(样本数$n$,每个示例中的输入数$d$), 并且令隐藏层激活函数为$\phi$。 在双向架构中,我们设该时间步的前向和反向隐状态分别为 $\overrightarrow{\mathbf{H}}_t \in \mathbb{R}^{n \times h}$和 @@ -250,6 +250,27 @@ num_epochs, lr = 500, 1 d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn + +#加载数据 +batch_size, num_steps, device = 32, 35, d2l.try_gpu() +train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) +#通过设置“direction='bidirect'”来定义双向LSTM模型 +vocab_size, num_hiddens, num_layers = len(vocab), 256, 2 +num_inputs = vocab_size +lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers, direction='bidirect', time_major=True) +model = d2l.RNNModel(lstm_layer, len(vocab)) +#训练模型 +num_epochs, lr = 500, 1.0 +d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) +``` + 上述结果显然令人瞠目结舌。 关于如何更有效地使用双向循环神经网络的讨论, 请参阅 :numref:`sec_sentiment_rnn`中的情感分类应用。 @@ -274,3 +295,7 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2773) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11835) +:end_tab: diff --git a/chapter_recurrent-modern/deep-rnn.md b/chapter_recurrent-modern/deep-rnn.md index 702ee4074..681a295e6 100644 --- a/chapter_recurrent-modern/deep-rnn.md +++ b/chapter_recurrent-modern/deep-rnn.md @@ -5,7 +5,7 @@ 到目前为止,我们只讨论了具有一个单向隐藏层的循环神经网络。 其中,隐变量和观测值与具体的函数形式的交互方式是相当随意的。 只要交互类型建模具有足够的灵活性,这就不是一个大问题。 -然而,对于一个单层来说,这可能具有相当的挑战性。 +然而,对一个单层来说,这可能具有相当的挑战性。 之前在线性模型中,我们通过添加更多的层来解决这个问题。 而在循环神经网络中,我们首先需要确定如何添加更多的层, 以及在哪里添加额外的非线性,因此这个问题有点棘手。 @@ -91,6 +91,18 @@ batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn + +batch_size, num_steps = 32, 35 +train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) +``` + 像选择超参数这类架构决策也跟 :numref:`sec_lstm`中的决策非常相似。 因为我们有不同的词元,所以输入和输出都选择相同数量,即`vocab_size`。 隐藏单元的数量仍然是$256$。 @@ -113,6 +125,15 @@ model = d2l.RNNModel(lstm_layer, len(vocab)) model = model.to(device) ``` +```{.python .input} +#@tab paddle +vocab_size, num_hiddens, num_layers = len(vocab), 256, 2 +num_inputs = vocab_size +device = d2l.try_gpu() +lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers, time_major=True) +model = d2l.RNNModel(lstm_layer, len(vocab)) +``` + ## [**训练**]与预测 由于使用了长短期记忆网络模型来实例化两个层,因此训练速度被大大降低了。 @@ -120,7 +141,7 @@ model = model.to(device) ```{.python .input} #@tab all num_epochs, lr = 500, 2 -d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) +d2l.train_ch8(model, train_iter, vocab, lr*1.0, num_epochs, device) ``` ## 小结 @@ -137,7 +158,7 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) 1. 基于我们在 :numref:`sec_rnn_scratch`中讨论的单层实现, 尝试从零开始实现两层循环神经网络。 1. 在本节训练模型中,比较使用门控循环单元替换长短期记忆网络后模型的精确度和训练速度。 -1. 如果增加训练数据,你能够将困惑度降到多低? +1. 如果增加训练数据,能够将困惑度降到多低? 1. 在为文本建模时,是否可以将不同作者的源数据合并?有何优劣呢? :begin_tab:`mxnet` @@ -147,3 +168,7 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2770) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11834) +:end_tab: \ No newline at end of file diff --git a/chapter_recurrent-modern/encoder-decoder.md b/chapter_recurrent-modern/encoder-decoder.md index a9982ff68..e61732321 100644 --- a/chapter_recurrent-modern/encoder-decoder.md +++ b/chapter_recurrent-modern/encoder-decoder.md @@ -18,18 +18,18 @@ :label:`fig_encoder_decoder` 我们以英语到法语的机器翻译为例: -给定一个英文的输入序列:“They”、“are”、“watching”、“.”。 +给定一个英文的输入序列:“They”“are”“watching”“.”。 首先,这种“编码器-解码器”架构将长度可变的输入序列编码成一个“状态”, 然后对该状态进行解码, 一个词元接着一个词元地生成翻译后的序列作为输出: -“Ils”、“regordent”、“.”。 +“Ils”“regordent”“.”。 由于“编码器-解码器”架构是形成后续章节中不同序列转换模型的基础, 因此本节将把这个架构转换为接口方便后面的代码实现。 ## (**编码器**) 在编码器接口中,我们只指定长度可变的序列作为编码器的输入`X`。 -任何继承这个`Encoder` 基类的模型将完成代码实现。 +任何继承这个`Encoder`基类的模型将完成代码实现。 ```{.python .input} from mxnet.gluon import nn @@ -72,12 +72,28 @@ class Encoder(tf.keras.layers.Layer): raise NotImplementedError ``` +```{.python .input} +#@tab paddle +import warnings +warnings.filterwarnings("ignore") +from paddle import nn + +#@save +class Encoder(nn.Layer): + """编码器-解码器架构的基本编码器接口""" + def __init__(self, **kwargs): + super(Encoder, self).__init__(**kwargs) + + def forward(self, X, *args): + raise NotImplementedError +``` + ## [**解码器**] 在下面的解码器接口中,我们新增一个`init_state`函数, 用于将编码器的输出(`enc_outputs`)转换为编码后的状态。 注意,此步骤可能需要额外的输入,例如:输入序列的有效长度, -这在 :numref:`subsec_mt_data_loading` 中进行了解释。 +这在 :numref:`subsec_mt_data_loading`中进行了解释。 为了逐个地生成长度可变的词元序列, 解码器在每个时间步都会将输入 (例如:在前一时间步生成的词元)和编码后的状态 @@ -127,6 +143,21 @@ class Decoder(tf.keras.layers.Layer): raise NotImplementedError ``` +```{.python .input} +#@tab paddle +#@save +class Decoder(nn.Layer): + """编码器-解码器架构的基本解码器接口""" + def __init__(self, **kwargs): + super(Decoder, self).__init__(**kwargs) + + def init_state(self, enc_outputs, *args): + raise NotImplementedError + + def forward(self, X, state): + raise NotImplementedError +``` + ## [**合并编码器和解码器**] 总而言之,“编码器-解码器”架构包含了一个编码器和一个解码器, @@ -181,8 +212,24 @@ class EncoderDecoder(tf.keras.Model): return self.decoder(dec_X, dec_state, **kwargs) ``` -“编码器-解码器”体系架构中的术语“状态” -可能会启发你使用具有状态的神经网络来实现该架构。 +```{.python .input} +#@tab paddle +#@save +class EncoderDecoder(nn.Layer): + """编码器-解码器架构的基类""" + def __init__(self, encoder, decoder, **kwargs): + super(EncoderDecoder, self).__init__(**kwargs) + self.encoder = encoder + self.decoder = decoder + + def forward(self, enc_X, dec_X, *args): + enc_outputs = self.encoder(enc_X, *args) + dec_state = self.decoder.init_state(enc_outputs, *args) + return self.decoder(dec_X, dec_state) +``` + +“编码器-解码器”体系架构中的术语*状态* +会启发人们使用具有状态的神经网络来实现该架构。 在下一节中,我们将学习如何应用循环神经网络, 来设计基于“编码器-解码器”架构的序列转换模型。 @@ -195,7 +242,7 @@ class EncoderDecoder(tf.keras.Model): ## 练习 1. 假设我们使用神经网络来实现“编码器-解码器”架构,那么编码器和解码器必须是同一类型的神经网络吗? -1. 除了机器翻译,你能想到其它可以适用于”编码器-解码器“架构的应用吗? +1. 除了机器翻译,还有其它可以适用于”编码器-解码器“架构的应用吗? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/2780) @@ -204,3 +251,7 @@ class EncoderDecoder(tf.keras.Model): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2779) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11837) +:end_tab: \ No newline at end of file diff --git a/chapter_recurrent-modern/gru.md b/chapter_recurrent-modern/gru.md index 36959a60e..46ab4bd16 100644 --- a/chapter_recurrent-modern/gru.md +++ b/chapter_recurrent-modern/gru.md @@ -64,10 +64,10 @@ 我们来看一下门控循环单元的数学表达。 对于给定的时间步$t$,假设输入是一个小批量 $\mathbf{X}_t \in \mathbb{R}^{n \times d}$ -(样本个数:$n$,输入个数:$d$), +(样本个数$n$,输入个数$d$), 上一个时间步的隐状态是 $\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}$ -(隐藏单元个数:$h$)。 +(隐藏单元个数$h$)。 那么,重置门$\mathbf{R}_t \in \mathbb{R}^{n \times h}$和 更新门$\mathbf{Z}_t \in \mathbb{R}^{n \times h}$的计算如下所示: @@ -147,7 +147,7 @@ $$\mathbf{H}_t = \mathbf{Z}_t \odot \mathbf{H}_{t-1} + (1 - \mathbf{Z}_t) \odot 总之,门控循环单元具有以下两个显著特征: -* 重置门有助于捕获序列中的短期依赖关系。 +* 重置门有助于捕获序列中的短期依赖关系; * 更新门有助于捕获序列中的长期依赖关系。 ## 从零开始实现 @@ -184,6 +184,19 @@ batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import paddle.nn.functional as F +from paddle import nn + +batch_size, num_steps = 32, 35 +train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) +``` + ### [**初始化模型参数**] 下一步是初始化模型参数。 @@ -265,6 +278,32 @@ def get_params(vocab_size, num_hiddens): return params ``` +```{.python .input} +#@tab paddle +def get_params(vocab_size, num_hiddens): + num_inputs = num_outputs = vocab_size + + def normal(shape): + return paddle.randn(shape=shape)*0.01 + + def three(): + return (normal((num_inputs, num_hiddens)), + normal((num_hiddens, num_hiddens)), + paddle.zeros([num_hiddens])) + + W_xz, W_hz, b_z = three() # 更新门参数 + W_xr, W_hr, b_r = three() # 重置门参数 + W_xh, W_hh, b_h = three() # 候选隐状态参数 + # 输出层参数 + W_hq = normal((num_hiddens, num_outputs)) + b_q = paddle.zeros([num_outputs]) + # 附加梯度 + params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q] + for param in params: + param.stop_gradient = False + return params +``` + ### 定义模型 现在我们将[**定义隐状态的初始化函数**]`init_gru_state`。 @@ -288,6 +327,12 @@ def init_gru_state(batch_size, num_hiddens): return (d2l.zeros((batch_size, num_hiddens)), ) ``` +```{.python .input} +#@tab paddle +def init_gru_state(batch_size, num_hiddens): + return (paddle.zeros([batch_size, num_hiddens]), ) +``` + 现在我们准备[**定义门控循环单元模型**], 模型的架构与基本的循环神经网络单元是相同的, 只是权重更新公式更为复杂。 @@ -340,6 +385,22 @@ def gru(inputs, state, params): return tf.concat(outputs, axis=0), (H,) ``` +```{.python .input} +#@tab paddle +def gru(inputs, state, params): + W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params + H,*_ = state + outputs = [] + for X in inputs: + Z = F.sigmoid((X @ W_xz) + (H @ W_hz) + b_z) + R = F.sigmoid((X @ W_xr) + (H @ W_hr) + b_r) + H_tilda = paddle.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h) + H = Z * H + (1 - Z) * H_tilda + Y = H @ W_hq + b_q + outputs.append(Y) + return paddle.concat(outputs, axis=0), (H,*_) +``` + ### [**训练**]与预测 训练和预测的工作方式与 :numref:`sec_rnn_scratch`完全相同。 @@ -367,6 +428,15 @@ with strategy.scope(): d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) ``` +```{.python .input} +#@tab paddle +vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu() +num_epochs, lr = 500, 1.0 +model = d2l.RNNModelScratch(len(vocab), num_hiddens, get_params, + init_gru_state, gru) +d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) +``` + ## [**简洁实现**] 高级API包含了前文介绍的所有配置细节, @@ -404,6 +474,14 @@ with strategy.scope(): d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) ``` +```{.python .input} +#@tab paddle +num_inputs = vocab_size +gru_layer = nn.GRU(num_inputs, num_hiddens, time_major=True) +model = d2l.RNNModel(gru_layer, len(vocab)) +d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) +``` + ## 小结 * 门控循环神经网络可以更好地捕获时间步距离很长的序列上的依赖关系。 @@ -425,3 +503,7 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2763) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11812) +:end_tab: \ No newline at end of file diff --git a/chapter_recurrent-modern/index.md b/chapter_recurrent-modern/index.md index 487ddf446..7af2f2424 100644 --- a/chapter_recurrent-modern/index.md +++ b/chapter_recurrent-modern/index.md @@ -8,7 +8,7 @@ 例如,循环神经网络在实践中一个常见问题是数值不稳定性。 尽管我们已经应用了梯度裁剪等技巧来缓解这个问题, -但是仍需要通过设计更复杂的序列模型可以进一步处理它。 +但是仍需要通过设计更复杂的序列模型来进一步处理它。 具体来说,我们将引入两个广泛使用的网络, 即*门控循环单元*(gated recurrent units,GRU)和 *长短期记忆网络*(long short-term memory,LSTM)。 diff --git a/chapter_recurrent-modern/lstm.md b/chapter_recurrent-modern/lstm.md index f9ea667c0..6708c9b25 100644 --- a/chapter_recurrent-modern/lstm.md +++ b/chapter_recurrent-modern/lstm.md @@ -151,6 +151,19 @@ batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +import paddle.nn.functional as Function + +batch_size, num_steps = 32, 35 +train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) +``` + ### [**初始化模型参数**] 接下来,我们需要定义和初始化模型参数。 @@ -238,6 +251,34 @@ def get_lstm_params(vocab_size, num_hiddens): return params ``` +```{.python .input} +#@tab paddle +def get_lstm_params(vocab_size, num_hiddens): + num_inputs = num_outputs = vocab_size + + def normal(shape): + return paddle.randn(shape=shape)*0.01 + + def three(): + return (normal((num_inputs, num_hiddens)), + normal((num_hiddens, num_hiddens)), + d2l.zeros([num_hiddens])) + + W_xi, W_hi, b_i = three() # 输入门参数 + W_xf, W_hf, b_f = three() # 遗忘门参数 + W_xo, W_ho, b_o = three() # 输出门参数 + W_xc, W_hc, b_c = three() # 候选记忆元参数 + # 输出层参数 + W_hq = normal((num_hiddens, num_outputs)) + b_q = d2l.zeros([num_outputs]) + # 附加梯度 + params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, + b_c, W_hq, b_q] + for param in params: + param.stop_gradient = False + return params +``` + ### 定义模型 在[**初始化函数**]中, @@ -265,6 +306,13 @@ def init_lstm_state(batch_size, num_hiddens): tf.zeros(shape=(batch_size, num_hiddens))) ``` +```{.python .input} +#@tab paddle +def init_lstm_state(batch_size, num_hiddens): + return (paddle.zeros([batch_size, num_hiddens]), + paddle.zeros([batch_size, num_hiddens])) +``` + [**实际模型**]的定义与我们前面讨论的一样: 提供三个门和一个额外的记忆元。 请注意,只有隐状态才会传递到输出层, @@ -326,6 +374,25 @@ def lstm(inputs, state, params): return tf.concat(outputs, axis=0), (H,C) ``` +```{.python .input} +#@tab paddle +def lstm(inputs, state, params): + [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, + W_hq, b_q] = params + (H, C) = state + outputs = [] + for X in inputs: + I = Function.sigmoid((X @ W_xi) + (H @ W_hi) + b_i) + F = Function.sigmoid((X @ W_xf) + (H @ W_hf) + b_f) + O = Function.sigmoid((X @ W_xo) + (H @ W_ho) + b_o) + C_tilda = paddle.tanh((X @ W_xc) + (H @ W_hc) + b_c) + C = F * C + I * C_tilda + H = O * paddle.tanh(C) + Y = (H @ W_hq) + b_q + outputs.append(Y) + return paddle.concat(outputs, axis=0), (H, C) +``` + ### [**训练**]和预测 让我们通过实例化 :numref:`sec_rnn_scratch`中 @@ -351,6 +418,15 @@ with strategy.scope(): d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) ``` +```{.python .input} +#@tab paddle +vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu() +num_epochs, lr = 500, 1.0 +model = d2l.RNNModelScratch(len(vocab), num_hiddens, get_lstm_params, + init_lstm_state, lstm) +d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) +``` + ## [**简洁实现**] 使用高级API,我们可以直接实例化`LSTM`模型。 @@ -386,11 +462,19 @@ with strategy.scope(): d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) ``` +```{.python .input} +#@tab paddle +num_inputs = vocab_size +lstm_layer = nn.LSTM(num_inputs, num_hiddens, time_major=True) +model = d2l.RNNModel(lstm_layer, len(vocab)) +d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) +``` + 长短期记忆网络是典型的具有重要状态控制的隐变量自回归模型。 多年来已经提出了其许多变体,例如,多层、残差连接、不同类型的正则化。 然而,由于序列的长距离依赖性,训练长短期记忆网络 和其他序列模型(例如门控循环单元)的成本是相当高的。 -在后面的内容中,我们将讲述更高级的替代模型,如transformer。 +在后面的内容中,我们将讲述更高级的替代模型,如Transformer。 ## 小结 @@ -402,7 +486,7 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) ## 练习 1. 调整和分析超参数对运行时间、困惑度和输出顺序的影响。 -1. 你需要如何更改模型以生成适当的单词,而不是字符序列? +1. 如何更改模型以生成适当的单词,而不是字符序列? 1. 在给定隐藏层维度的情况下,比较门控循环单元、长短期记忆网络和常规循环神经网络的计算成本。要特别注意训练和推断成本。 1. 既然候选记忆元通过使用$\tanh$函数来确保值范围在$(-1,1)$之间,那么为什么隐状态需要再次使用$\tanh$函数来确保输出值范围在$(-1,1)$之间呢? 1. 实现一个能够基于时间序列进行预测而不是基于字符序列进行预测的长短期记忆网络模型。 @@ -414,3 +498,7 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2768) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11833) +:end_tab: \ No newline at end of file diff --git a/chapter_recurrent-modern/machine-translation-and-dataset.md b/chapter_recurrent-modern/machine-translation-and-dataset.md index 4c6b0646e..d872f820d 100644 --- a/chapter_recurrent-modern/machine-translation-and-dataset.md +++ b/chapter_recurrent-modern/machine-translation-and-dataset.md @@ -16,10 +16,10 @@ 几十年来,在使用神经网络进行端到端学习的兴起之前, 统计学方法在这一领域一直占据主导地位 :cite:`Brown.Cocke.Della-Pietra.ea.1988,Brown.Cocke.Della-Pietra.ea.1990`。 -因为*统计机器翻译*(statisticalmachine translation)涉及了 +因为*统计机器翻译*(statistical machine translation)涉及了 翻译模型和语言模型等组成部分的统计分析, 因此基于神经网络的方法通常被称为 -*神经机器翻译*(neuralmachine translation), +*神经机器翻译*(neural machine translation), 用于将两种翻译模型区分开来。 本书的关注点是神经网络机器翻译方法,强调的是端到端的学习。 @@ -51,6 +51,15 @@ import tensorflow as tf import os ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import os +``` + ## [**下载和预处理数据集**] 首先,下载一个由[Tatoeba项目的双语句子对](http://www.manythings.org/anki/) @@ -139,6 +148,7 @@ source[:6], target[:6] ```{.python .input} #@tab all +#@save def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist): """绘制列表长度对的直方图""" d2l.set_figsize() @@ -285,3 +295,7 @@ for X, X_valid_len, Y, Y_valid_len in train_iter: :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2776) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11836) +:end_tab: diff --git a/chapter_recurrent-modern/seq2seq.md b/chapter_recurrent-modern/seq2seq.md index 4e1ce1bb1..0b587cb8d 100644 --- a/chapter_recurrent-modern/seq2seq.md +++ b/chapter_recurrent-modern/seq2seq.md @@ -35,9 +35,9 @@ 编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 类似于 :numref:`sec_language_model`中语言模型的训练, 可以允许标签成为原始的输出序列, -从源序列词元“<bos>”、“Ils”、“regardent”、“.” +从源序列词元“<bos>”“Ils”“regardent”“.” 到新序列词元 -“Ils”、“regardent”、“.”、“<eos>”来移动预测的位置。 +“Ils”“regardent”“.”“<eos>”来移动预测的位置。 下面,我们动手构建 :numref:`fig_seq2seq`的设计, 并将基于 :numref:`sec_machine_translation`中 @@ -69,6 +69,17 @@ import math import tensorflow as tf ``` +```{.python .input} +#@tab paddle +import collections +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +from paddle import nn +``` + ## 编码器 从技术上讲,编码器将长度可变的输入序列转换成 @@ -186,6 +197,34 @@ class Seq2SeqEncoder(d2l.Encoder): return output[0], state ``` +```{.python .input} +#@tab paddle +#@save +class Seq2SeqEncoder(d2l.Encoder): + """用于序列到序列学习的循环神经网络编码器""" + def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, + dropout=0, **kwargs): + super(Seq2SeqEncoder, self).__init__(**kwargs) + weight_ih_attr = paddle.ParamAttr(initializer=nn.initializer.XavierUniform()) + weight_hh_attr = paddle.ParamAttr(initializer=nn.initializer.XavierUniform()) + # 嵌入层 + self.embedding = nn.Embedding(vocab_size, embed_size) + self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout, + time_major=True, weight_ih_attr=weight_ih_attr, weight_hh_attr=weight_hh_attr) + + def forward(self, X, *args): + # 输出'X'的形状:(batch_size,num_steps,embed_size) + X = self.embedding(X) + # 在循环神经网络模型中,第一个轴对应于时间步 + X = X.transpose([1, 0, 2]) + # 如果未提及状态,则默认为0 + output, state = self.rnn(X) + # PaddlePaddle的GRU层output的形状:(batch_size,time_steps,num_directions * num_hiddens), + # 需设定time_major=True,指定input的第一个维度为time_steps + # state[0]的形状:(num_layers,batch_size,num_hiddens) + return output, state +``` + 循环层返回变量的说明可以参考 :numref:`sec_rnn-concise`。 下面,我们实例化[**上述编码器的实现**]: @@ -222,6 +261,16 @@ output, state = encoder(X, training=False) output.shape ``` +```{.python .input} +#@tab paddle +encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, + num_layers=2) +encoder.eval() +X = d2l.zeros((4, 7), dtype=paddle.int64) +output, state = encoder(X) +output.shape +``` + 由于这里使用的是门控循环单元, 所以在最后一个时间步的多层隐状态的形状是 (隐藏层的数量,批量大小,隐藏单元的数量)。 @@ -232,7 +281,7 @@ len(state), state[0].shape ``` ```{.python .input} -#@tab pytorch +#@tab pytorch, paddle state.shape ``` @@ -365,6 +414,37 @@ class Seq2SeqDecoder(d2l.Decoder): return output, rnn_output[1:] ``` +```{.python .input} +#@tab paddle +class Seq2SeqDecoder(d2l.Decoder): + """用于序列到序列学习的循环神经网络解码器""" + def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, + dropout=0, **kwargs): + super(Seq2SeqDecoder, self).__init__(**kwargs) + self.embedding = nn.Embedding(vocab_size, embed_size) + weight_attr = paddle.ParamAttr(initializer=nn.initializer.XavierUniform()) + weight_ih_attr = paddle.ParamAttr(initializer=nn.initializer.XavierUniform()) + weight_hh_attr = paddle.ParamAttr(initializer=nn.initializer.XavierUniform()) + self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout, + time_major=True, weight_ih_attr=weight_ih_attr,weight_hh_attr=weight_hh_attr) + self.dense = nn.Linear(num_hiddens, vocab_size,weight_attr=weight_attr) + + def init_state(self, enc_outputs, *args): + return enc_outputs[1] + + def forward(self, X, state): + # 输出'X'的形状:(batch_size,num_steps,embed_size) + X = self.embedding(X).transpose([1, 0, 2]) + # 广播context,使其具有与X相同的num_steps + context = state[-1].tile([X.shape[0], 1, 1]) + X_and_context = d2l.concat((X, context), 2) + output, state = self.rnn(X_and_context, state) + output = self.dense(output).transpose([1, 0, 2]) + # output的形状:(batch_size,num_steps,vocab_size) + # state[0]的形状:(num_layers,batch_size,num_hiddens) + return output, state +``` + 下面,我们用与前面提到的编码器中相同的超参数来[**实例化解码器**]。 如我们所见,解码器的输出形状变为(批量大小,时间步数,词表大小), 其中张量的最后一个维度存储预测的词元分布。 @@ -396,6 +476,16 @@ output, state = decoder(X, state, training=False) output.shape, len(state), state[0].shape ``` +```{.python .input} +#@tab paddle +decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16, + num_layers=2) +decoder.eval() +state = decoder.init_state(encoder(X)) +output, state = decoder(X, state) +output.shape, state.shape +``` + 总之,上述循环神经网络“编码器-解码器”模型中的各层如 :numref:`fig_seq2seq_details`所示。 @@ -456,6 +546,22 @@ X = tf.constant([[1, 2, 3], [4, 5, 6]]) sequence_mask(X, tf.constant([1, 2])) ``` +```{.python .input} +#@tab paddle +#@save +def sequence_mask(X, valid_len, value=0): + """在序列中屏蔽不相关的项""" + maxlen = X.shape[1] + mask = paddle.arange((maxlen), dtype=paddle.float32)[None, :] < valid_len[:, None] + Xtype = X.dtype + X = X.astype(paddle.float32) + X[~mask] = float(value) + return X.astype(Xtype) + +X = paddle.to_tensor([[1, 2, 3], [4, 5, 6]]) +sequence_mask(X, paddle.to_tensor([1, 2])) +``` + (**我们还可以使用此函数屏蔽最后几个轴上的所有项。**)如果愿意,也可以使用指定的非零值来替换这些项。 ```{.python .input} @@ -475,6 +581,12 @@ X = tf.ones((2,3,4)) sequence_mask(X, tf.constant([1, 2]), value=-1) ``` +```{.python .input} +#@tab paddle +X = d2l.ones([2, 3, 4]) +sequence_mask(X, paddle.to_tensor([1, 2]), value=-1) +``` + 现在,我们可以[**通过扩展softmax交叉熵损失函数来遮蔽不相关的预测**]。 最初,所有预测词元的掩码都设置为1。 一旦给定了有效长度,与填充词元对应的掩码将被设置为0。 @@ -534,6 +646,24 @@ class MaskedSoftmaxCELoss(tf.keras.losses.Loss): return weighted_loss ``` +```{.python .input} +#@tab paddle +#@save +class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): + """带遮蔽的softmax交叉熵损失函数""" + # pred的形状:(batch_size,num_steps,vocab_size) + # label的形状:(batch_size,num_steps) + # valid_len的形状:(batch_size,) + def forward(self, pred, label, valid_len): + weights = paddle.ones_like(label) + weights = sequence_mask(weights, valid_len) + self.reduction='none' + unweighted_loss = super(MaskedSoftmaxCELoss, self).forward( + pred, label) + weighted_loss = (unweighted_loss * weights).mean(axis=1) + return weighted_loss +``` + 我们可以创建三个相同的序列来进行[**代码健全性检查**], 然后分别指定这些序列的有效长度为$4$、$2$和$0$。 结果就是,第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。 @@ -556,6 +686,13 @@ loss = MaskedSoftmaxCELoss(tf.constant([4, 2, 0])) loss(tf.ones((3,4), dtype = tf.int32), tf.ones((3, 4, 10))).numpy() ``` +```{.python .input} +#@tab paddle +loss = MaskedSoftmaxCELoss() +loss(d2l.ones([3, 4, 10]), d2l.ones((3, 4), dtype=paddle.int64), + paddle.to_tensor([4, 2, 0])) +``` + ## [**训练**] :label:`sec_seq2seq_training` @@ -673,6 +810,38 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): f'tokens/sec on {str(device)}') ``` +```{.python .input} +#@tab paddle +#@save +def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): + """训练序列到序列模型""" + optimizer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) + loss = MaskedSoftmaxCELoss() + net.train() + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[10, num_epochs]) + for epoch in range(num_epochs): + timer = d2l.Timer() + metric = d2l.Accumulator(2) # 训练损失总和,词元数量 + for batch in data_iter: + optimizer.clear_grad() + X, X_valid_len, Y, Y_valid_len = [paddle.to_tensor(x, place=device) for x in batch] + bos = paddle.to_tensor([tgt_vocab['']] * Y.shape[0]).reshape([-1, 1]) + dec_input = paddle.concat([bos, Y[:, :-1]], 1) # 强制教学 + Y_hat, _ = net(X, dec_input, X_valid_len.squeeze()) + l = loss(Y_hat, Y, Y_valid_len.squeeze()) + l.backward() # 损失函数的标量进行“反向传播” + d2l.grad_clipping(net, 1) + num_tokens = Y_valid_len.sum() + optimizer.step() + with paddle.no_grad(): + metric.add(l.sum(), num_tokens) + if (epoch + 1) % 10 == 0: + animator.add(epoch + 1, (metric[0] / metric[1],)) + print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' + f'tokens/sec on {str(device)}') +``` + 现在,在机器翻译数据集上,我们可以 [**创建和训练一个循环神经网络“编码器-解码器”模型**]用于序列到序列的学习。 @@ -806,6 +975,42 @@ def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, shape = -1).numpy().tolist())), attention_weight_seq ``` +```{.python .input} +#@tab paddle +#@save +def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, + device, save_attention_weights=False): + """序列到序列模型的预测""" + # 在预测时将net设置为评估模式 + net.eval() + src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ + src_vocab['']] + enc_valid_len = paddle.to_tensor([len(src_tokens)], place=device) + src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) + # 添加批量轴 + enc_X = paddle.unsqueeze( + paddle.to_tensor(src_tokens, dtype=paddle.int64, place=device), axis=0) + enc_outputs = net.encoder(enc_X, enc_valid_len) + dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) + # 添加批量轴 + dec_X = paddle.unsqueeze(paddle.to_tensor( + [tgt_vocab['']], dtype=paddle.int64, place=device), axis=0) + output_seq, attention_weight_seq = [], [] + for _ in range(num_steps): + Y, dec_state = net.decoder(dec_X, dec_state) + # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入 + dec_X = Y.argmax(axis=2) + pred = dec_X.squeeze(axis=0).astype(paddle.int32).item() + # 保存注意力权重(稍后讨论) + if save_attention_weights: + attention_weight_seq.append(net.decoder.attention_weights) + # 一旦序列结束词元被预测,输出序列的生成就完成了 + if pred == tgt_vocab['']: + break + output_seq.append(pred) + return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq +``` + ## 预测序列的评估 我们可以通过与真实的标签序列进行比较来评估预测序列。 @@ -868,7 +1073,7 @@ def bleu(pred_seq, label_seq, k): #@save [**将几个英语句子翻译成法语**],并计算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): @@ -898,8 +1103,8 @@ for eng, fra in zip(engs, fras): ## 练习 -1. 你能通过调整超参数来改善翻译效果吗? -1. 重新运行实验并在计算损失时不使用遮蔽。你观察到什么结果?为什么? +1. 试着通过调整超参数来改善翻译效果。 +1. 重新运行实验并在计算损失时不使用遮蔽,可以观察到什么结果?为什么会有这个结果? 1. 如果编码器和解码器的层数或者隐藏单元数不同,那么如何初始化解码器的隐状态? 1. 在训练中,如果用前一时间步的预测输入到解码器来代替强制教学,对性能有何影响? 1. 用长短期记忆网络替换门控循环单元重新运行实验。 @@ -912,3 +1117,7 @@ for eng, fra in zip(engs, fras): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/2782) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11838) +:end_tab: \ No newline at end of file diff --git a/chapter_recurrent-neural-networks/bptt.md b/chapter_recurrent-neural-networks/bptt.md index 1090e0832..e5c34494a 100644 --- a/chapter_recurrent-neural-networks/bptt.md +++ b/chapter_recurrent-neural-networks/bptt.md @@ -1,18 +1,18 @@ # 通过时间反向传播 :label:`sec_bptt` -到目前为止,我们已经反复提到像“梯度爆炸”、“梯度消失”, -以及需要对循环神经网络“分离梯度”。 +到目前为止,我们已经反复提到像*梯度爆炸*或*梯度消失*, +以及需要对循环神经网络*分离梯度*。 例如,在 :numref:`sec_rnn_scratch`中, 我们在序列上调用了`detach`函数。 为了能够快速构建模型并了解其工作原理, 上面所说的这些概念都没有得到充分的解释。 -在本节中,我们将更深入地探讨序列模型反向传播的细节, +本节将更深入地探讨序列模型反向传播的细节, 以及相关的数学原理。 当我们首次实现循环神经网络( :numref:`sec_rnn_scratch`)时, 遇到了梯度爆炸的问题。 -如果你做了练习题,就会发现梯度截断对于确保模型收敛至关重要。 +如果做了练习题,就会发现梯度截断对于确保模型收敛至关重要。 为了更好地理解此问题,本节将回顾序列模型梯度的计算方式, 它的工作原理没有什么新概念,毕竟我们使用的仍然是链式法则来计算梯度。 @@ -156,9 +156,9 @@ $$z_t= \frac{\partial f(x_{t},h_{t-1},w_h)}{\partial w_h} +\xi_t \frac{\partial 当基于循环神经网络使用通过时间反向传播 分析《时间机器》书中前几个字符的三种策略: -* 第一行采用随机截断,方法是将文本划分为不同长度的片断。 +* 第一行采用随机截断,方法是将文本划分为不同长度的片断; * 第二行采用常规截断,方法是将文本分解为相同长度的子序列。 - 这也是我们在循环神经网络实验中一直在做的。 + 这也是我们在循环神经网络实验中一直在做的; * 第三行采用通过时间的完全反向传播,结果是产生了在计算上不可行的表达式。 遗憾的是,虽然随机截断在理论上具有吸引力, @@ -249,7 +249,7 @@ $$\frac{\partial L}{\partial \mathbf{h}_T} = \text{prod}\left(\frac{\partial L}{ 当目标函数$L$通过$\mathbf{h}_{t+1}$和$\mathbf{o}_t$ 依赖$\mathbf{h}_t$时, -对于任意时间步$t < T$来说都变得更加棘手。 +对任意时间步$t < T$来说都变得更加棘手。 根据链式法则,隐状态的梯度 $\partial L/\partial \mathbf{h}_t \in \mathbb{R}^h$ 在任何时间步骤$t < T$时都可以递归地计算为: @@ -318,6 +318,6 @@ $\partial L / \partial \mathbf{W}_{hh}$时使用。 1. 证明$\mathbf{M}^k$拥有特征值$\lambda_i^k$。 1. 证明对于一个随机向量$\mathbf{x} \in \mathbb{R}^n$,$\mathbf{M}^k \mathbf{x}$将有较高概率与$\mathbf{M}$的特征向量$\mathbf{v}_1$在一条直线上。形式化这个证明过程。 1. 上述结果对于循环神经网络中的梯度意味着什么? -1. 除了梯度截断,你还能想到其他方法来应对循环神经网络中的梯度爆炸吗? +1. 除了梯度截断,还有其他方法来应对循环神经网络中的梯度爆炸吗? [Discussions](https://discuss.d2l.ai/t/2107) diff --git a/chapter_recurrent-neural-networks/language-models-and-dataset.md b/chapter_recurrent-neural-networks/language-models-and-dataset.md index e8aa04a3f..0b44c1ae0 100644 --- a/chapter_recurrent-neural-networks/language-models-and-dataset.md +++ b/chapter_recurrent-neural-networks/language-models-and-dataset.md @@ -118,7 +118,7 @@ P(x_1, x_2, x_3, x_4) &= P(x_1) P(x_2 \mid x_1) P(x_3 \mid x_1, x_2) P(x_4 $$ 通常,涉及一个、两个和三个变量的概率公式分别被称为 -“一元语法”(unigram)、“二元语法”(bigram)和“三元语法”(trigram)模型。 +*一元语法*(unigram)、*二元语法*(bigram)和*三元语法*(trigram)模型。 下面,我们将学习如何去设计更好的模型。 ## 自然语言统计 @@ -148,6 +148,15 @@ import tensorflow as tf import random ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +import random +``` + ```{.python .input} #@tab all tokens = d2l.tokenize(d2l.read_time_machine()) @@ -220,12 +229,13 @@ d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x', ``` 这张图非常令人振奋!原因有很多: -首先,除了一元语法词,单词序列似乎也遵循齐普夫定律, + +1. 除了一元语法词,单词序列似乎也遵循齐普夫定律, 尽管公式 :eqref:`eq_zipf_law`中的指数$\alpha$更小 -(指数的大小受序列长度的影响)。 -其次,词表中$n$元组的数量并没有那么大,这说明语言中存在相当多的结构, -这些结构给了我们应用模型的希望。 -第三,很多$n$元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。 +(指数的大小受序列长度的影响); +2. 词表中$n$元组的数量并没有那么大,这说明语言中存在相当多的结构, +这些结构给了我们应用模型的希望; +3. 很多$n$元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。 作为代替,我们将使用基于深度学习的模型。 ## 读取长序列数据 @@ -354,6 +364,23 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save yield X, Y ``` +```{.python .input} +#@tab paddle +def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save + """使用顺序分区生成一个小批量子序列""" + # 从随机偏移量开始划分序列 + offset = random.randint(0, num_steps) + num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size + Xs = d2l.tensor(corpus[offset: offset + num_tokens]) + Ys = d2l.tensor(corpus[offset + 1: offset + 1 + num_tokens]) + Xs, Ys = Xs.reshape((batch_size, -1)), Ys.reshape((batch_size, -1)) + num_batches = Xs.shape[1] // num_steps + for i in range(0, num_steps * num_batches, num_steps): + X = Xs[:, i: i + num_steps] + Y = Ys[:, i: i + num_steps] + yield X, Y +``` + 基于相同的设置,通过顺序分区[**读取每个小批量的子序列的特征`X`和标签`Y`**]。 通过将它们打印出来可以发现: 迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的。 @@ -412,12 +439,12 @@ def load_data_time_machine(batch_size, num_steps, #@save 1. 假设训练数据集中有$100,000$个单词。一个四元语法需要存储多少个词频和相邻多词频率? 1. 我们如何对一系列对话建模? -1. 一元语法、二元语法和三元语法的齐普夫定律的指数是不一样的,你能设法估计嘛? +1. 一元语法、二元语法和三元语法的齐普夫定律的指数是不一样的,能设法估计么? 1. 想一想读取长序列数据的其他方法? 1. 考虑一下我们用于读取长序列的随机偏移量。 1. 为什么随机偏移量是个好主意? 1. 它真的会在文档的序列上实现完美的均匀分布吗? - 1. 你要怎么做才能使分布更均匀? + 1. 要怎么做才能使分布更均匀? 1. 如果我们希望一个序列样本是一个完整的句子,那么这在小批量抽样中会带来怎样的问题?如何解决? :begin_tab:`mxnet` @@ -431,3 +458,7 @@ def load_data_time_machine(batch_size, num_steps, #@save :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2098) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11797) +:end_tab: diff --git a/chapter_recurrent-neural-networks/rnn-concise.md b/chapter_recurrent-neural-networks/rnn-concise.md index 5a065b9ea..a260ba634 100644 --- a/chapter_recurrent-neural-networks/rnn-concise.md +++ b/chapter_recurrent-neural-networks/rnn-concise.md @@ -35,12 +35,25 @@ batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) ``` +```{.python .input} +#@tab paddle +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +from paddle.nn import functional as F + +batch_size, num_steps = 32, 35 +train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) +``` + ## [**定义模型**] 高级API提供了循环神经网络的实现。 我们构造一个具有256个隐藏单元的单隐藏层的循环神经网络层`rnn_layer`。 事实上,我们还没有讨论多层循环神经网络的意义(这将在 :numref:`sec_deep_rnn`中介绍)。 -现在,你仅需要将多层理解为一层循环神经网络的输出被用作下一层循环神经网络的输入就足够了。 +现在仅需要将多层理解为一层循环神经网络的输出被用作下一层循环神经网络的输入就足够了。 ```{.python .input} num_hiddens = 256 @@ -63,6 +76,12 @@ rnn_layer = tf.keras.layers.RNN(rnn_cell, time_major=True, return_sequences=True, return_state=True) ``` +```{.python .input} +#@tab paddle +num_hiddens = 256 +rnn_layer = nn.SimpleRNN(len(vocab), num_hiddens, time_major=True) +``` + :begin_tab:`mxnet` 初始化隐状态是简单的,只需要调用成员函数`begin_state`即可。 函数将返回一个列表(`state`),列表中包含了初始隐状态用于小批量数据中的每个样本, @@ -91,6 +110,12 @@ state = rnn_cell.get_initial_state(batch_size=batch_size, dtype=tf.float32) state.shape ``` +```{.python .input} +#@tab paddle +state = paddle.zeros(shape=[1, batch_size, num_hiddens]) +state.shape +``` + [**通过一个隐状态和一个输入,我们就可以用更新后的隐状态计算输出。**] 需要强调的是,`rnn_layer`的“输出”(`Y`)不涉及输出层的计算: 它是指每个时间步的隐状态,这些隐状态可以用作后续输出层的输入。 @@ -123,6 +148,13 @@ Y, state_new = rnn_layer(X, state) Y.shape, len(state_new), state_new[0].shape ``` +```{.python .input} +#@tab paddle +X = paddle.rand(shape=[num_steps, batch_size, len(vocab)]) +Y, state_new = rnn_layer(X, state) +Y.shape, state_new.shape +``` + 与 :numref:`sec_rnn_scratch`类似, [**我们为一个完整的循环神经网络模型定义了一个`RNNModel`类**]。 注意,`rnn_layer`只包含隐藏的循环层,我们还需要创建一个单独的输出层。 @@ -213,6 +245,47 @@ class RNNModel(tf.keras.layers.Layer): return self.rnn.cell.get_initial_state(*args, **kwargs) ``` +```{.python .input} +#@tab paddle +#@save +class RNNModel(nn.Layer): + """循环神经网络模型""" + def __init__(self, rnn_layer, vocab_size, **kwargs): + super(RNNModel, self).__init__(**kwargs) + self.rnn = rnn_layer + self.vocab_size = vocab_size + self.num_hiddens = self.rnn.hidden_size + # 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1 + if self.rnn.num_directions==1: + self.num_directions = 1 + self.linear = nn.Linear(self.num_hiddens, self.vocab_size) + else: + self.num_directions = 2 + self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size) + + def forward(self, inputs, state): + X = F.one_hot(inputs.T, self.vocab_size) + Y, state = self.rnn(X, state) + # 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数) + # 它的输出形状是(时间步数*批量大小,词表大小)。 + output = self.linear(Y.reshape((-1, Y.shape[-1]))) + return output, state + + def begin_state(self, batch_size=1): + if not isinstance(self.rnn, nn.LSTM): + # nn.GRU以张量作为隐状态 + return paddle.zeros(shape=[self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens]) + else: + # nn.LSTM以元组作为隐状态 + return (paddle.zeros( + shape=[self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens]), + paddle.zeros( + shape=[self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens])) +``` + ## 训练与预测 在训练模型之前,让我们[**基于一个具有随机权重的模型进行预测**]。 @@ -242,6 +315,13 @@ with strategy.scope(): d2l.predict_ch8('time traveller', 10, net, vocab) ``` +```{.python .input} +#@tab paddle +device = d2l.try_gpu() +net = RNNModel(rnn_layer, vocab_size=len(vocab)) +d2l.predict_ch8('time traveller', 10, net, vocab, device) +``` + 很明显,这种模型根本不能输出好的结果。 接下来,我们使用 :numref:`sec_rnn_scratch`中 定义的超参数调用`train_ch8`,并且[**使用高级API训练模型**]。 @@ -263,6 +343,12 @@ num_epochs, lr = 500, 1 d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, strategy) ``` +```{.python .input} +#@tab paddle +num_epochs, lr = 500, 1.0 +d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device) +``` + 与上一节相比,由于深度学习框架的高级API对代码进行了更多的优化, 该模型在较短的时间内达到了较低的困惑度。 @@ -274,8 +360,8 @@ d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, strategy) ## 练习 -1. 尝试使用高级API,你能使循环神经网络模型过拟合吗? -1. 如果在循环神经网络模型中增加隐藏层的数量会发生什么?你能使模型正常工作吗? +1. 尝试使用高级API,能使循环神经网络模型过拟合吗? +1. 如果在循环神经网络模型中增加隐藏层的数量会发生什么?能使模型正常工作吗? 1. 尝试使用循环神经网络实现 :numref:`sec_sequence`的自回归模型。 :begin_tab:`mxnet` @@ -289,3 +375,7 @@ d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, strategy) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/5766) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11800) +:end_tab: diff --git a/chapter_recurrent-neural-networks/rnn-scratch.md b/chapter_recurrent-neural-networks/rnn-scratch.md index 42846fe37..5d6da152b 100644 --- a/chapter_recurrent-neural-networks/rnn-scratch.md +++ b/chapter_recurrent-neural-networks/rnn-scratch.md @@ -1,7 +1,7 @@ # 循环神经网络的从零开始实现 :label:`sec_rnn_scratch` -在本节中,我们将根据 :numref:`sec_rnn`中的描述, +本节将根据 :numref:`sec_rnn`中的描述, 从头开始基于循环神经网络实现字符级语言模型。 这样的模型将在H.G.Wells的时光机器数据集上训练。 和前面 :numref:`sec_language_model`中介绍过的一样, @@ -33,6 +33,18 @@ import math import tensorflow as tf ``` +```{.python .input} +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import math +import paddle +from paddle import nn +from paddle.nn import functional as F +``` + ```{.python .input} #@tab all batch_size, num_steps = 32, 35 @@ -76,6 +88,11 @@ F.one_hot(torch.tensor([0, 2]), len(vocab)) tf.one_hot(tf.constant([0, 2]), len(vocab)) ``` +```{.python .input} +#@tab paddle +F.one_hot(paddle.to_tensor([0, 2]), len(vocab)) +``` + 我们每次采样的(**小批量数据形状是二维张量: (批量大小,时间步数)。**) `one_hot`函数将这样一个小批量数据转换成三维张量, @@ -102,6 +119,12 @@ X = d2l.reshape(d2l.arange(10), (2, 5)) tf.one_hot(tf.transpose(X), 28).shape ``` +```{.python .input} +#@tab paddle +X = paddle.arange(10).reshape((2, 5)) +F.one_hot(X.T, 28).shape +``` + ## 初始化模型参数 接下来,我们[**初始化循环神经网络模型的模型参数**]。 @@ -171,6 +194,28 @@ def get_params(vocab_size, num_hiddens): return params ``` +```{.python .input} +#@tab paddle +def get_params(vocab_size, num_hiddens): + num_inputs = num_outputs = vocab_size + + def normal(shape): + return paddle.randn(shape=shape)* 0.01 + + # 隐藏层参数 + W_xh = normal([num_inputs, num_hiddens]) + W_hh = normal([num_hiddens, num_hiddens]) + b_h = d2l.zeros(shape=[num_hiddens]) + # 输出层参数 + W_hq = normal([num_hiddens, num_outputs]) + b_q = d2l.zeros(shape=[num_outputs]) + # 附加梯度 + params = [W_xh, W_hh, b_h, W_hq, b_q] + for param in params: + param.stop_gradient=False + return params +``` + ## 循环神经网络模型 为了定义循环神经网络模型, @@ -197,6 +242,12 @@ def init_rnn_state(batch_size, num_hiddens): return (d2l.zeros((batch_size, num_hiddens)), ) ``` +```{.python .input} +#@tab paddle +def init_rnn_state(batch_size, num_hiddens): + return (paddle.zeros(shape=[batch_size, num_hiddens]), ) +``` + [**下面的`rnn`函数定义了如何在一个时间步内计算隐状态和输出。**] 循环神经网络模型通过`inputs`最外层的维度实现循环, 以便逐时间步更新小批量数据的隐状态`H`。 @@ -249,6 +300,21 @@ def rnn(inputs, state, params): return d2l.concat(outputs, axis=0), (H,) ``` +```{.python .input} +#@tab paddle +def rnn(inputs, state, params): + # inputs的形状:(时间步数量,批量大小,词表大小) + W_xh, W_hh, b_h, W_hq, b_q = params + H, = state + outputs = [] + # X的形状:(批量大小,词表大小) + for X in inputs: + H = paddle.tanh(paddle.mm(X, W_xh) + paddle.mm(H, W_hh) + b_h) + Y = paddle.mm(H, W_hq) + b_q + outputs.append(Y) + return paddle.concat(x=outputs, axis=0), (H,) +``` + 定义了所有需要的函数之后,接下来我们[**创建一个类来包装这些函数**], 并存储从零开始实现的循环神经网络模型的参数。 @@ -306,6 +372,24 @@ class RNNModelScratch: #@save return self.init_state(batch_size, self.num_hiddens) ``` +```{.python .input} +#@tab paddle +class RNNModelScratch: #@save + """从零开始实现的循环神经网络模型""" + def __init__(self, vocab_size, num_hiddens, + get_params, init_state, forward_fn): + self.vocab_size, self.num_hiddens = vocab_size, num_hiddens + self.params = get_params(vocab_size, num_hiddens) + self.init_state, self.forward_fn = init_state, forward_fn + + def __call__(self, X, state): + X = F.one_hot(X.T, self.vocab_size) + return self.forward_fn(X, state, self.params) + + def begin_state(self, batch_size): + return self.init_state(batch_size, self.num_hiddens) +``` + 让我们[**检查输出是否具有正确的形状**]。 例如,隐状态的维数是否保持不变。 @@ -344,6 +428,16 @@ Y, new_state = net(X, state) Y.shape, len(new_state), new_state[0].shape ``` +```{.python .input} +#@tab paddle +num_hiddens = 512 +net = RNNModelScratch(len(vocab), num_hiddens, get_params, + init_rnn_state, rnn) +state = net.begin_state(X.shape[0]) +Y, new_state = net(X, state) +Y.shape, len(new_state), new_state[0].shape +``` + 我们可以看到输出形状是(时间步数$\times$批量大小,词表大小), 而隐状态形状保持不变,即(批量大小,隐藏单元数)。 @@ -409,13 +503,29 @@ def predict_ch8(prefix, num_preds, net, vocab): #@save return ''.join([vocab.idx_to_token[i] for i in outputs]) ``` +```{.python .input} +#@tab paddle +def predict_ch8(prefix, num_preds, net, vocab, device): #@save + """在prefix后面生成新字符""" + state = net.begin_state(batch_size=1) + outputs = [vocab[prefix[0]]] + get_input = lambda: d2l.reshape(d2l.tensor(outputs[-1], place=device), (1, 1)) + for y in prefix[1:]: # 预热期 + _, state = net(get_input(), state) + outputs.append(vocab[y]) + for _ in range(num_preds): # 预测num_preds步 + y, state = net(get_input(), state) + outputs.append(int(paddle.reshape(paddle.argmax(y,axis=1),shape=[1]))) + return ''.join([vocab.idx_to_token[i] for i in outputs]) +``` + 现在我们可以测试`predict_ch8`函数。 我们将前缀指定为`time traveller `, 并基于这个前缀生成10个后续字符。 鉴于我们还没有训练网络,它会生成荒谬的预测结果。 ```{.python .input} -#@tab mxnet,pytorch +#@tab mxnet,pytorch, paddle predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu()) ``` @@ -525,14 +635,29 @@ def grad_clipping(grads, theta): #@save return new_grad ``` +```{.python .input} +#@tab paddle +def grad_clipping(net, theta): #@save + """裁剪梯度""" + if isinstance(net, nn.Layer): + params = [p for p in net.parameters() if not p.stop_gradient] + else: + params = net.params + norm = paddle.sqrt(sum(paddle.sum((p.grad ** 2)) for p in params)) + if norm > theta: + with paddle.no_grad(): + for param in params: + param.grad.set_value(param.grad * theta / norm) +``` + ## 训练 在训练模型之前,让我们[**定义一个函数在一个迭代周期内训练模型**]。 -它与我们训练 :numref:`sec_softmax_scratch`模型的方式有三个不同之处: +它与我们训练 :numref:`sec_softmax_scratch`模型的方式有三个不同之处。 1. 序列数据的不同采样方法(随机采样和顺序分区)将导致隐状态初始化的差异。 1. 我们在更新模型参数之前裁剪梯度。 - 这样的操作的目的是:即使训练过程中某个点上发生了梯度爆炸,也能保证模型不会发散。 + 这样的操作的目的是,即使训练过程中某个点上发生了梯度爆炸,也能保证模型不会发散。 1. 我们用困惑度来评价模型。如 :numref:`subsec_perplexity`所述, 这样的度量确保了不同长度的序列具有可比性。 @@ -644,6 +769,45 @@ def train_epoch_ch8(net, train_iter, loss, updater, use_random_iter): return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() ``` +```{.python .input} +#@tab paddle +#@save +def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): + """训练网络一个迭代周期(定义见第8章)""" + state, timer = None, d2l.Timer() + metric = d2l.Accumulator(2) # 训练损失之和,词元数量 + for X, Y in train_iter: + if state is None or use_random_iter: + # 在第一次迭代或使用随机抽样时初始化state + state = net.begin_state(batch_size=X.shape[0]) + else: + if isinstance(net, nn.Layer) and not isinstance(state, tuple): + # state对于nn.GRU是个张量 + state.stop_gradient=True + else: + # state对于nn.LSTM或对于我们从零开始实现的模型是个张量 + for s in state: + s.stop_gradient=True + y = paddle.reshape(Y.T,shape=[-1]) + X = paddle.to_tensor(X, place=device) + y = paddle.to_tensor(y, place=device) + y_hat, state = net(X, state) + l = loss(y_hat, y).mean() + if isinstance(updater, paddle.optimizer.Optimizer): + updater.clear_grad() + l.backward() + grad_clipping(net, 1) + updater.step() + else: + l.backward() + grad_clipping(net, 1) + # 因为已经调用了mean函数 + updater(batch_size=1) + + metric.add(l * d2l.size(y), d2l.size(y)) + return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() +``` + [**循环神经网络模型的训练函数既支持从零开始实现, 也可以使用高级API来实现。**] @@ -728,12 +892,39 @@ def train_ch8(net, train_iter, vocab, lr, num_epochs, strategy, print(predict('traveller')) ``` +```{.python .input} +#@tab paddle +#@save +def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): + """训练模型(定义见第8章)""" + loss = nn.CrossEntropyLoss() + animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', + legend=['train'], xlim=[10, num_epochs]) + # 初始化 + if isinstance(net, nn.Layer): + updater = paddle.optimizer.SGD( + learning_rate=lr, parameters=net.parameters()) + else: + updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) + predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) + # 训练和预测 + for epoch in range(num_epochs): + ppl, speed = train_epoch_ch8( + net, train_iter, loss, updater, device, use_random_iter) + if (epoch + 1) % 10 == 0: + print(predict('time traveller')) + animator.add(epoch + 1, [ppl]) + print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}') + print(predict('time traveller')) + print(predict('traveller')) +``` + [**现在,我们训练循环神经网络模型。**] 因为我们在数据集中只使用了10000个词元, 所以模型需要更多的迭代周期来更好地收敛。 ```{.python .input} -#@tab mxnet,pytorch +#@tab mxnet,pytorch, paddle num_epochs, lr = 500, 1 train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu()) ``` @@ -763,6 +954,14 @@ train_ch8(net, train_iter, vocab_random_iter, lr, num_epochs, strategy, use_random_iter=True) ``` +```{.python .input} +#@tab paddle +net = RNNModelScratch(len(vocab), num_hiddens, get_params, + init_rnn_state, rnn) +train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(), + use_random_iter=True) +``` + 从零开始实现上述循环神经网络模型, 虽然有指导意义,但是并不方便。 在下一节中,我们将学习如何改进循环神经网络模型。 @@ -781,7 +980,7 @@ train_ch8(net, train_iter, vocab_random_iter, lr, num_epochs, strategy, 1. 尝试说明独热编码等价于为每个对象选择不同的嵌入表示。 1. 通过调整超参数(如迭代周期数、隐藏单元数、小批量数据的时间步数、学习率等)来改善困惑度。 - * 你能将困惑度降到多少? + * 困惑度可以降到多少? * 用可学习的嵌入表示替换独热编码,是否会带来更好的表现? * 如果用H.G.Wells的其他书作为数据集时效果如何, 例如[*世界大战*](http://www.gutenberg.org/ebooks/36)? @@ -803,3 +1002,7 @@ train_ch8(net, train_iter, vocab_random_iter, lr, num_epochs, strategy, :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2104) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11799) +:end_tab: diff --git a/chapter_recurrent-neural-networks/rnn.md b/chapter_recurrent-neural-networks/rnn.md index cbcef8523..94d43383e 100644 --- a/chapter_recurrent-neural-networks/rnn.md +++ b/chapter_recurrent-neural-networks/rnn.md @@ -166,7 +166,15 @@ import tensorflow as tf ``` ```{.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 X, W_xh = d2l.normal(0, 1, (3, 1)), d2l.normal(0, 1, (1, 4)) H, W_hh = d2l.normal(0, 1, (3, 4)), d2l.normal(0, 1, (4, 4)) d2l.matmul(X, W_xh) + d2l.matmul(H, W_hh) @@ -198,7 +206,7 @@ d2l.matmul(d2l.concat((X, H), 1), d2l.concat((W_xh, W_hh), 0)) Bengio等人首先提出使用神经网络进行语言建模 :cite:`Bengio.Ducharme.Vincent.ea.2003`。 接下来,我们看一下如何使用循环神经网络来构建语言模型。 -设小批量大小为1,批量中的那个文本序列为“machine”。 +设小批量大小为1,批量中的文本序列为“machine”。 为了简化后续部分的训练,我们考虑使用 *字符级语言模型*(character-level language model), 将文本词元化为字符而不是单词。 @@ -213,10 +221,10 @@ Bengio等人首先提出使用神经网络进行语言建模 然后利用交叉熵损失计算模型输出和标签之间的误差。 由于隐藏层中隐状态的循环计算, :numref:`fig_rnn_train`中的第$3$个时间步的输出$\mathbf{O}_3$ -由文本序列“m”、“a”和“c”确定。 +由文本序列“m”“a”和“c”确定。 由于训练数据中这个文本序列的下一个字符是“h”, 因此第$3$个时间步的损失将取决于下一个字符的概率分布, -而下一个字符是基于特征序列“m”、“a”、“c”和这个时间步的标签“h”生成的。 +而下一个字符是基于特征序列“m”“a”“c”和这个时间步的标签“h”生成的。 在实践中,我们使用的批量大小为$n>1$, 每个词元都由一个$d$维向量表示。 @@ -231,9 +239,9 @@ Bengio等人首先提出使用神经网络进行语言建模 一个好的语言模型能够用高度准确的词元来预测我们接下来会看到什么。 考虑一下由不同的语言模型给出的对“It is raining ...”(“...下雨了”)的续写: -1. "It is raining outside"(外面下雨了) -1. "It is raining banana tree"(香蕉树下雨了) -1. "It is raining piouw;kcj pwepoiut"(piouw;kcj pwepoiut下雨了) +1. "It is raining outside"(外面下雨了); +1. "It is raining banana tree"(香蕉树下雨了); +1. "It is raining piouw;kcj pwepoiut"(piouw;kcj pwepoiut下雨了)。 就质量而言,例$1$显然是最合乎情理、在逻辑上最连贯的。 虽然这个模型可能没有很准确地反映出后续词的语义, @@ -275,7 +283,7 @@ $x_t$是在时间步$t$从该序列中观察到的实际词元。 $$\exp\left(-\frac{1}{n} \sum_{t=1}^n \log P(x_t \mid x_{t-1}, \ldots, x_1)\right).$$ 困惑度的最好的理解是“下一个词元的实际选择数的调和平均数”。 -我们看看一些案例: +我们看看一些案例。 * 在最好的情况下,模型总是完美地估计标签词元的概率为1。 在这种情况下,模型的困惑度为1。 @@ -303,7 +311,7 @@ $$\exp\left(-\frac{1}{n} \sum_{t=1}^n \log P(x_t \mid x_{t-1}, \ldots, x_1)\righ 1. 如果我们使用循环神经网络来预测文本序列中的下一个字符,那么任意输出所需的维度是多少? 1. 为什么循环神经网络可以基于文本序列中所有先前的词元,在某个时间步表示当前词元的条件概率? -1. 如果你基于一个长序列进行反向传播,梯度会发生什么状况? +1. 如果基于一个长序列进行反向传播,梯度会发生什么状况? 1. 与本节中描述的语言模型相关的问题有哪些? :begin_tab:`mxnet` @@ -317,3 +325,7 @@ $$\exp\left(-\frac{1}{n} \sum_{t=1}^n \log P(x_t \mid x_{t-1}, \ldots, x_1)\righ :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2101) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11798) +:end_tab: diff --git a/chapter_recurrent-neural-networks/sequence.md b/chapter_recurrent-neural-networks/sequence.md index 64967fe25..5567f7677 100644 --- a/chapter_recurrent-neural-networks/sequence.md +++ b/chapter_recurrent-neural-networks/sequence.md @@ -1,8 +1,8 @@ # 序列模型 :label:`sec_sequence` -想象一下你正在看网飞(Netflix,一个国外的视频网站)上的电影。 -作为一名忠实的用户,你对每一部电影都给出评价, +想象一下有人正在看网飞(Netflix,一个国外的视频网站)上的电影。 +一名忠实的用户会对每一部电影都给出评价, 毕竟一部好电影需要更多的支持和认可。 然而事实证明,事情并不那么简单。 随着时间的推移,人们对电影的看法会发生很大的变化。 @@ -23,9 +23,9 @@ 简而言之,电影评分决不是固定不变的。 因此,使用时间动力学可以得到更准确的电影推荐 :cite:`Koren.2009`。 当然,序列数据不仅仅是关于电影评分的。 -下面给出了更多的场景: +下面给出了更多的场景。 -* 在使用应用程序时,许多用户都有很强的特定习惯。 +* 在使用程序时,许多用户都有很强的特定习惯。 例如,在学生放学后社交媒体应用更受欢迎。在市场开放时股市交易软件更常用。 * 预测明天的股价要比过去的股价更困难,尽管两者都只是估计一个数字。 毕竟,先见之明比事后诸葛亮难得多。 @@ -186,7 +186,17 @@ import tensorflow as tf ``` ```{.python .input} -#@tab mxnet, pytorch +#@tab paddle +%matplotlib inline +from d2l import paddle as d2l +import warnings +warnings.filterwarnings("ignore") +import paddle +from paddle import nn +``` + +```{.python .input} +#@tab mxnet, pytorch, paddle T = 1000 # 总共产生1000个点 time = d2l.arange(1, T + 1, dtype=d2l.float32) x = d2l.sin(0.01 * time) + d2l.normal(0, 0.2, (T,)) @@ -201,17 +211,17 @@ x = d2l.sin(0.01 * time) + d2l.normal([T], 0, 0.2) d2l.plot(time, [x], 'time', 'x', xlim=[1, 1000], figsize=(6, 3)) ``` -接下来,我们将这个序列转换为模型的“特征-标签”(feature-label)对。 +接下来,我们将这个序列转换为模型的*特征-标签*(feature-label)对。 基于嵌入维度$\tau$,我们[**将数据映射为数据对$y_t = x_t$ 和$\mathbf{x}_t = [x_{t-\tau}, \ldots, x_{t-1}]$。**] -你可能已经注意到,这比我们提供的数据样本少了$\tau$个, +这比我们提供的数据样本少了$\tau$个, 因为我们没有足够的历史记录来描述前$\tau$个数据样本。 一个简单的解决办法是:如果拥有足够长的序列就丢弃这几项; 另一个方法是用零填充序列。 在这里,我们仅使用前600个“特征-标签”对进行训练。 ```{.python .input} -#@tab mxnet, pytorch +#@tab mxnet, pytorch, paddle tau = 4 features = d2l.zeros((T - tau, tau)) for i in range(tau): @@ -283,6 +293,24 @@ def get_net(): loss = tf.keras.losses.MeanSquaredError() ``` +```{.python .input} +#@tab paddle +def init_weights(m): + if type(m) == nn.Linear: + nn.initializer.XavierUniform(m.weight) + +#一个简单的多层感知机 +def get_net(): + net = nn.Sequential(nn.Linear(4, 10), + nn.ReLU(), + nn.Linear(10, 1)) + net.apply(init_weights) + return net + +#平方损失。注意:MSELoss计算平方误差时不带系数1/2 +loss = nn.MSELoss(reduction='none') +``` + 现在,准备[**训练模型**]了。实现下面的训练代码的方式与前面几节(如 :numref:`sec_linear_concise`)中的循环训练基本相同。因此,我们不会深入探讨太多细节。 ```{.python .input} @@ -338,6 +366,23 @@ net = get_net() train(net, train_iter, loss, 5, 0.01) ``` +```{.python .input} +#@tab paddle +def train(net, train_iter, loss, epochs, lr): + trainer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) + for epoch in range(epochs): + for i,(X, y) in enumerate (train_iter()): + trainer.clear_grad() + l = loss(net(X), y) + l.sum().backward() + trainer.step() + print(f'epoch {epoch + 1}, ' + f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}') + +net = get_net() +train(net, train_iter, loss, 5, 0.01) +``` + ## 预测 由于训练损失很小,因此我们期望模型能有很好的工作效果。 @@ -392,6 +437,15 @@ for i in range(n_train + tau, T): d2l.reshape(multistep_preds[i - tau: i], (1, -1))), ())) ``` +```{.python .input} +#@tab paddle +multistep_preds = d2l.zeros([T]) +multistep_preds[: n_train + tau] = x[: n_train + tau] +for i in range(n_train + tau, T): + multistep_preds[i] = net( + d2l.reshape(multistep_preds[i - tau: i], (1, -1))) +``` + ```{.python .input} #@tab all d2l.plot([time, time[tau:], time[n_train + tau:]], @@ -445,6 +499,18 @@ for i in range(tau, tau + max_steps): features[:, i].assign(d2l.reshape(net((features[:, i - tau: i])), -1)) ``` +```{.python .input} +#@tab paddle +features = d2l.zeros((T - tau - max_steps + 1, tau + max_steps)) +# 列i(i=tau)是来自(i-tau+1)步的预测,其时间步从(i+1)到(i+T-tau-max_steps+1) +for i in range(tau, tau + max_steps): + features[:, i] = d2l.reshape(net(features[:, i - tau: i]), [-1]) +``` + ```{.python .input} #@tab all steps = (1, 4, 16, 64) @@ -459,7 +525,7 @@ d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps], ## 小结 -* 内插法(在现有观测值之间进行估计)和外推法(对超出已知观测范围进行预测)在实践的难度上差别很大。因此,对于你所拥有的序列数据,在训练时始终要尊重其时间顺序,即最好不要基于未来的数据进行训练。 +* 内插法(在现有观测值之间进行估计)和外推法(对超出已知观测范围进行预测)在实践的难度上差别很大。因此,对于所拥有的序列数据,在训练时始终要尊重其时间顺序,即最好不要基于未来的数据进行训练。 * 序列模型的估计需要专门的统计工具,两种较流行的选择是自回归模型和隐变量自回归模型。 * 对于时间是向前推进的因果模型,正向估计通常比反向估计更容易。 * 对于直到时间步$t$的观测序列,其在时间步$t+k$的预测输出是“$k$步预测”。随着我们对预测时间$k$值的增加,会造成误差的快速累积和预测质量的极速下降。 @@ -469,7 +535,7 @@ d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps], 1. 改进本节实验中的模型。 1. 是否包含了过去$4$个以上的观测结果?真实值需要是多少个? 1. 如果没有噪音,需要多少个过去的观测结果?提示:把$\sin$和$\cos$写成微分方程。 - 1. 你能在保持特征总数不变的情况下合并旧的观察结果吗?这能提高正确度吗?为什么? + 1. 可以在保持特征总数不变的情况下合并旧的观察结果吗?这能提高正确度吗?为什么? 1. 改变神经网络架构并评估其性能。 1. 一位投资者想要找到一种好的证券来购买。他查看过去的回报,以决定哪一种可能是表现良好的。这一策略可能会出什么问题呢? 1. 时间是向前推进的因果模型在多大程度上适用于文本呢? @@ -486,3 +552,7 @@ d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps], :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2092) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11795) +:end_tab: diff --git a/chapter_recurrent-neural-networks/text-preprocessing.md b/chapter_recurrent-neural-networks/text-preprocessing.md index 06e67ee7b..ad5266067 100644 --- a/chapter_recurrent-neural-networks/text-preprocessing.md +++ b/chapter_recurrent-neural-networks/text-preprocessing.md @@ -4,7 +4,7 @@ 对于序列数据处理问题,我们在 :numref:`sec_sequence`中 评估了所需的统计工具和预测时面临的挑战。 这样的数据存在许多种形式,文本是最常见例子之一。 -例如,一篇文章可以被简单地看作是一串单词序列,甚至是一串字符序列。 +例如,一篇文章可以被简单地看作一串单词序列,甚至是一串字符序列。 本节中,我们将解析文本的常见预处理步骤。 这些步骤通常包括: @@ -33,6 +33,13 @@ from d2l import tensorflow as d2l import re ``` +```{.python .input} +#@tab paddle +import collections +from d2l import paddle as d2l +import re +``` + ## 读取数据集 首先,我们从H.G.Well的[时光机器](https://www.gutenberg.org/ebooks/35)中加载文本。 @@ -216,3 +223,7 @@ len(corpus), len(vocab) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2095) :end_tab: + +:begin_tab:`paddle` +[Discussions](https://discuss.d2l.ai/t/11796) +:end_tab: diff --git a/config.ini b/config.ini index eb2e0c179..6838bd9d5 100644 --- a/config.ini +++ b/config.ini @@ -8,7 +8,7 @@ author = Aston Zhang, Zachary C. Lipton, Mu Li, and Alexander J. Smola copyright = 2022, All authors. Licensed under CC-BY-SA-4.0 and MIT-0. -release = 2.0.0-beta1 +release = 2.0.0 lang = zh @@ -33,7 +33,7 @@ exclusions = */*_origin.md README.md STYLE_GUIDE.md INFO.md CODE_OF_CONDUCT.md C # If True (default), then will evaluate the notebook to obtain outputs. eval_notebook = True -tabs = mxnet, pytorch, tensorflow +tabs = mxnet, pytorch, tensorflow, paddle sphinx_configs = numfig_format = {'figure': '图%%s', 'table': '表%%s', 'code-block': '列表%%s', 'section': '%%s节'} latex_elements = { @@ -44,14 +44,12 @@ sphinx_configs = numfig_format = {'figure': '图%%s', 'table': '表%%s', 'code-b \usepackage{ctex} \setmainfont{Source Serif Pro} \setsansfont{Source Sans Pro} - \setmonofont{Source Code Pro} + \setmonofont{Inconsolata} \setCJKmainfont[BoldFont=Source Han Serif SC SemiBold]{Source Han Serif SC} \setCJKsansfont[BoldFont=Source Han Sans SC Medium]{Source Han Sans SC Normal} \setCJKmonofont{Source Han Sans SC Normal} \addto\captionsenglish{\renewcommand{\chaptername}{}} \addto\captionsenglish{\renewcommand{\contentsname}{目录}} - \usepackage[draft]{minted} - \fvset{breaklines=true, breakanywhere=true} \setlength{\headheight}{13.6pt} \makeatletter \fancypagestyle{normal}{ @@ -64,7 +62,27 @@ sphinx_configs = numfig_format = {'figure': '图%%s', 'table': '表%%s', 'code-b \makeatother \CJKsetecglue{} \usepackage{zhnumber} + + \definecolor{d2lbookOutputCellBackgroundColor}{RGB}{255,255,255} + \definecolor{d2lbookOutputCellBorderColor}{rgb}{.85,.85,.85} + \def\diilbookstyleoutputcell + {\sphinxcolorlet{VerbatimColor}{d2lbookOutputCellBackgroundColor} + \sphinxcolorlet{VerbatimBorderColor}{d2lbookOutputCellBorderColor} + \sphinxsetup{verbatimwithframe,verbatimborder=0.5pt} + } + + \definecolor{d2lbookInputCellBackgroundColor}{rgb}{.95,.95,.95} + \def\diilbookstyleinputcell + {\sphinxcolorlet{VerbatimColor}{d2lbookInputCellBackgroundColor} + \sphinxsetup{verbatimwithframe=false,verbatimborder=0pt} + } ''', + + 'sphinxsetup': '''verbatimsep=2mm, + VerbatimColor={rgb}{.95,.95,.95}, + VerbatimBorderColor={rgb}{.95,.95,.95}, + pre_border-radius=3pt, + ''', # The font size ('10pt', '11pt' or '12pt'). 'pointsize': '10pt', # Latex figure (float) alignment @@ -170,7 +188,24 @@ reverse_alias = d2l.size\(([\w\_\d]+)\) -> tf.size(\1).numpy() d2l.nn_Module -> tf.keras.Model +[library-paddle] +lib_file = d2l/paddle.py +lib_name = paddle + +simple_alias = ones, zeros, tensor -> to_tensor, arange, meshgrid, sin, sinh, cos, cosh, + tanh, linspace, exp, log, normal, rand, randn, matmul, int32, float32, + concat, stack, abs, eye + +fluent_alias = numpy -> detach().numpy, size -> numel, reshape, to, + reduce_sum -> sum, argmax, astype, transpose -> t, + reduce_mean -> mean +alias = + nn_Module = nn.Layer + +reverse_alias = + d2l.nn_Module -> nn.Layer + [deploy] other_file_s3urls = s3://d2l-webdata/releases/d2l-zh/d2l-zh-1.0.zip @@ -184,6 +219,7 @@ google_analytics_tracking_id = UA-96378503-2 github_repo = mxnet, d2l-ai/d2l-zh-colab pytorch, d2l-ai/d2l-zh-pytorch-colab tensorflow, d2l-ai/d2l-zh-tensorflow-colab + paddle, d2l-ai/d2l-zh-paddle-colab replace_svg_url = img, http://d2l.ai/_images @@ -191,6 +227,7 @@ libs = mxnet, mxnet, -U mxnet-cu101==1.7.0 mxnet, d2l, git+https://github.com/d2l-ai/d2l-zh@release # installing d2l pytorch, d2l, git+https://github.com/d2l-ai/d2l-zh@release # installing d2l tensorflow, d2l, git+https://github.com/d2l-ai/d2l-zh@release # installing d2l + paddle, d2l, git+https://github.com/d2l-ai/d2l-zh@release # installing d2l [sagemaker] @@ -198,15 +235,18 @@ libs = mxnet, mxnet, -U mxnet-cu101==1.7.0 github_repo = mxnet, d2l-ai/d2l-zh-sagemaker pytorch, d2l-ai/d2l-zh-pytorch-sagemaker tensorflow, d2l-ai/d2l-zh-tensorflow-sagemaker + paddle, d2l-ai/d2l-zh-paddle-sagemaker kernel = mxnet, conda_mxnet_p36 pytorch, conda_pytorch_p36 tensorflow, conda_tensorflow_p36 + paddle, conda_paddle_p36 libs = mxnet, mxnet, -U mxnet-cu101==1.7.0 mxnet, d2l, .. # installing d2l pytorch, d2l, .. # installing d2l tensorflow, d2l, .. # installing d2l + paddle, d2l, .. # installing d2l [slides] diff --git a/contrib/chapter_recommender-systems/autorec.md b/contrib/chapter_recommender-systems/autorec.md index 5d32645e4..32b0e087c 100644 --- a/contrib/chapter_recommender-systems/autorec.md +++ b/contrib/chapter_recommender-systems/autorec.md @@ -113,6 +113,6 @@ d2l.train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs, * 修改自动编码器的隐含层维度,观察模型性能的变化。 * 尝试添加更多的隐含层。这对提高模型的性能有帮助吗? -* 你能找到更好的编码器激活函数和解码器激活函数吗? +* 可以找到更好的编码器激活函数和解码器激活函数吗? [讨论](https://discuss.d2l.ai/t/401) diff --git a/contrib/chapter_recommender-systems/mf.md b/contrib/chapter_recommender-systems/mf.md index b9fcdd9b7..0bcdd562a 100644 --- a/contrib/chapter_recommender-systems/mf.md +++ b/contrib/chapter_recommender-systems/mf.md @@ -1,6 +1,6 @@ # 矩阵分解 -矩阵分解(Matrix Factorization,MF)是推荐系统文献中公认的一种算法。最初版本的矩阵分解模型由Simon Funk发表在一篇非常知名的[博文](https://sifter.org/~simon/journal/20061211.html)中,他在这篇博文描述了分解矩阵的想法。随后,由于2006年举办的Netflix竞赛,该模型变得广为人知。那时,流媒体和视频租赁公司Netflix为了增进其推荐系统的性能而举办了一项比赛。如果最佳团队(例如Cinematch)能够将Netflix的基线提高10%,那么他们将赢得100万美元的奖励。这一比赛在推荐系统领域引起了广泛的关注。随后,BellKor's Pragmatic Chaos团队(一个由BellKor、Pragmatic Theory和BigChaos混合组成的团队)赢得了这一大奖。尽管他们的最终评分来自一个集成解决方案,矩阵分解算法仍在其中起到了关键作用。Netflix Grand Prize的技术报告:cite:`Toscher.Jahrer.Bell.2009`详细解释了该方案所采用的模型。在本节中,我们将深入研究矩阵分解模型的细节和实现过程。 +矩阵分解(Matrix Factorization,MF)是推荐系统文献中公认的一种算法。最初版本的矩阵分解模型由Simon Funk发表在一篇非常知名的[博文](https://sifter.org/~simon/journal/20061211.html)中,他在这篇博文描述了分解矩阵的想法。随后,由于2006年举办的Netflix竞赛,该模型变得广为人知。那时,流媒体和视频租赁公司Netflix为了增进其推荐系统的性能而举办了一项比赛。如果最佳团队(例如Cinematch)能够将Netflix的基线提高10%,那么他们将赢得100万美元的奖励。这一比赛在推荐系统领域引起了广泛的关注。随后,BellKor's Pragmatic Chaos团队(一个由BellKor、Pragmatic Theory和BigChaos混合组成的团队)赢得了这一大奖。尽管他们的最终评分来自一个集成解决方案,矩阵分解算法仍在其中起到了关键作用。Netflix Grand Prize的技术报告:cite:`Toscher.Jahrer.Bell.2009`详细解释了该方案所采用的模型。本节将深入研究矩阵分解模型的细节和实现过程。 ## 矩阵分解模型 diff --git a/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/machine-translation.md b/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/machine-translation.md index 2c6eb4f6f..bb811302f 100644 --- a/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/machine-translation.md +++ b/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/machine-translation.md @@ -88,7 +88,7 @@ class Encoder(nn.Block): return self.rnn.begin_state(*args, **kwargs) ``` -下面我们来创建一个批量大小为4、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对于门控循环单元来说,`state`列表中只含一个元素,即隐状态;如果使用长短期记忆,`state`列表中还将包含另一个元素,即记忆细胞。 +下面我们来创建一个批量大小为4、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对门控循环单元来说,`state`列表中只含一个元素,即隐状态;如果使用长短期记忆,`state`列表中还将包含另一个元素,即记忆细胞。 ```{.python .input n=166} encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2) diff --git a/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/sentiment-analysis-rnn.md b/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/sentiment-analysis-rnn.md index f409b77e3..fb5a3ffa8 100644 --- a/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/sentiment-analysis-rnn.md +++ b/contrib/to-rm-mx-contrib-text/chapter_natural-language-processing/sentiment-analysis-rnn.md @@ -2,7 +2,7 @@ 文本分类是自然语言处理的一个常见任务,它把一段不定长的文本序列变换为文本的类别。本节关注它的一个子问题:使用文本情感分类来分析文本作者的情绪。这个问题也叫情感分析(sentiment analysis),并有着广泛的应用。例如,我们可以分析用户对产品的评论并统计用户的满意度,或者分析用户对市场行情的情绪并用以预测接下来的行情。 -同求近义词和类比词一样,文本分类也属于词嵌入的下游应用。在本节中,我们将应用预训练的词向量和含多个隐藏层的双向循环神经网络,来判断一段不定长的文本序列中包含的是正面还是负面的情绪。 +同求近义词和类比词一样,文本分类也属于词嵌入的下游应用。本节将应用预训练的词向量和含多个隐藏层的双向循环神经网络,来判断一段不定长的文本序列中包含的是正面还是负面的情绪。 在实验开始前,导入所需的包或模块。 diff --git a/d2l/__init__.py b/d2l/__init__.py index 5af511a7c..9b6eba9b9 100644 --- a/d2l/__init__.py +++ b/d2l/__init__.py @@ -5,7 +5,8 @@ from d2l import mxnet as d2l # Use MXNet as the backend from d2l import torch as d2l # Use PyTorch as the backend from d2l import tensorflow as d2l # Use TensorFlow as the backend +from d2l import paddle as d2l # Use Paddle as the backend """ -__version__ = "2.0.0-beta1" +__version__ = "2.0.0" diff --git a/d2l/mxnet.py b/d2l/mxnet.py index 27275dd91..f29ef4203 100644 --- a/d2l/mxnet.py +++ b/d2l/mxnet.py @@ -1,3 +1,16 @@ +USE_MXNET = True +USE_PYTORCH = False +USE_TENSORFLOW = False + +DATA_HUB = dict() +DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/' + +from mxnet import autograd, context, gluon, image, init, np, npx +from mxnet.gluon import nn, rnn +from mxnet.gluon.data.vision import transforms + +nn_Module = nn.Block + ################# WARNING ################ # The below part is generated automatically through: # d2lbook build lib @@ -27,20 +40,20 @@ from mxnet.gluon import nn, rnn def use_svg_display(): - """使用svg格式在Jupyter中显示绘图 + """Use the svg format to display a plot in Jupyter. Defined in :numref:`sec_calculus`""" backend_inline.set_matplotlib_formats('svg') def set_figsize(figsize=(3.5, 2.5)): - """设置matplotlib的图表大小 + """Set the figure size for matplotlib. Defined in :numref:`sec_calculus`""" use_svg_display() d2l.plt.rcParams['figure.figsize'] = figsize def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): - """设置matplotlib的轴 + """Set the axes for matplotlib. Defined in :numref:`sec_calculus`""" axes.set_xlabel(xlabel) @@ -56,7 +69,7 @@ def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None): - """绘制数据点 + """Plot data points. Defined in :numref:`sec_calculus`""" if legend is None: @@ -65,7 +78,7 @@ def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # 如果X有一个轴,输出True + # Return True if `X` (tensor or list) has 1 axis def has_one_axis(X): return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) and not hasattr(X[0], "__len__")) @@ -87,35 +100,35 @@ def has_one_axis(X): set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend) class Timer: - """记录多次运行时间""" + """Record multiple running times.""" def __init__(self): """Defined in :numref:`subsec_linear_model`""" self.times = [] self.start() def start(self): - """启动计时器""" + """Start the timer.""" self.tik = time.time() def stop(self): - """停止计时器并将时间记录在列表中""" + """Stop the timer and record the time in a list.""" self.times.append(time.time() - self.tik) return self.times[-1] def avg(self): - """返回平均时间""" + """Return the average time.""" return sum(self.times) / len(self.times) def sum(self): - """返回时间总和""" + """Return the sum of time.""" return sum(self.times) def cumsum(self): - """返回累计时间""" + """Return the accumulated time.""" return np.array(self.times).cumsum().tolist() def synthetic_data(w, b, num_examples): - """生成y=Xw+b+噪声 + """Generate y = Xw + b + noise. Defined in :numref:`sec_linear_scratch`""" X = d2l.normal(0, 1, (num_examples, len(w))) @@ -124,33 +137,33 @@ def synthetic_data(w, b, num_examples): return X, d2l.reshape(y, (-1, 1)) def linreg(X, w, b): - """线性回归模型 + """The linear regression model. Defined in :numref:`sec_linear_scratch`""" return d2l.matmul(X, w) + b def squared_loss(y_hat, y): - """均方损失 + """Squared loss. Defined in :numref:`sec_linear_scratch`""" return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 def sgd(params, lr, batch_size): - """小批量随机梯度下降 + """Minibatch stochastic gradient descent. Defined in :numref:`sec_linear_scratch`""" for param in params: param[:] = param - lr * param.grad / batch_size def load_array(data_arrays, batch_size, is_train=True): - """构造一个Gluon数据迭代器 + """Construct a Gluon data iterator. Defined in :numref:`sec_linear_concise`""" dataset = gluon.data.ArrayDataset(*data_arrays) return gluon.data.DataLoader(dataset, batch_size, shuffle=is_train) def get_fashion_mnist_labels(labels): - """返回Fashion-MNIST数据集的文本标签 + """Return text labels for the Fashion-MNIST dataset. Defined in :numref:`sec_fashion_mnist`""" text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', @@ -158,7 +171,7 @@ def get_fashion_mnist_labels(labels): return [text_labels[int(i)] for i in labels] def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): - """绘制图像列表 + """Plot a list of images. Defined in :numref:`sec_fashion_mnist`""" figsize = (num_cols * scale, num_rows * scale) @@ -173,13 +186,13 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): return axes def get_dataloader_workers(): - """在非Windows的平台上,使用4个进程来读取数据 + """Use 4 processes to read the data except for Windows. Defined in :numref:`sec_fashion_mnist`""" return 0 if sys.platform.startswith('win') else 4 def load_data_fashion_mnist(batch_size, resize=None): - """下载Fashion-MNIST数据集,然后将其加载到内存中 + """Download the Fashion-MNIST dataset and then load it into memory. Defined in :numref:`sec_fashion_mnist`""" dataset = gluon.data.vision @@ -195,7 +208,7 @@ def load_data_fashion_mnist(batch_size, resize=None): num_workers=get_dataloader_workers())) def accuracy(y_hat, y): - """计算预测正确的数量 + """Compute the number of correct predictions. Defined in :numref:`sec_softmax_scratch`""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: @@ -204,16 +217,16 @@ def accuracy(y_hat, y): return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) def evaluate_accuracy(net, data_iter): - """计算在指定数据集上模型的精度 + """Compute the accuracy for a model on a dataset. Defined in :numref:`sec_softmax_scratch`""" - metric = Accumulator(2) # 正确预测数、预测总数 + metric = Accumulator(2) # No. of correct predictions, no. of predictions for X, y in data_iter: metric.add(accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] class Accumulator: - """在n个变量上累加""" + """For accumulating sums over `n` variables.""" def __init__(self, n): """Defined in :numref:`sec_softmax_scratch`""" self.data = [0.0] * n @@ -228,45 +241,45 @@ def __getitem__(self, idx): return self.data[idx] def train_epoch_ch3(net, train_iter, loss, updater): - """训练模型一个迭代周期(定义见第3章) + """Train a model within one epoch (defined in Chapter 3). Defined in :numref:`sec_softmax_scratch`""" - # 训练损失总和、训练准确度总和、样本数 + # Sum of training loss, sum of training accuracy, no. of examples metric = Accumulator(3) if isinstance(updater, gluon.Trainer): updater = updater.step for X, y in train_iter: - # 计算梯度并更新参数 + # Compute gradients and update parameters with autograd.record(): y_hat = net(X) l = loss(y_hat, y) l.backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.size) - # 返回训练损失和训练精度 + # Return training loss and training accuracy return metric[0] / metric[2], metric[1] / metric[2] class Animator: - """在动画中绘制数据""" + """For plotting data in animation.""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): """Defined in :numref:`sec_softmax_scratch`""" - # 增量地绘制多条线 + # Incrementally plot multiple lines if legend is None: legend = [] d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: self.axes = [self.axes, ] - # 使用lambda函数捕获参数 + # Use a lambda function to capture arguments self.config_axes = lambda: d2l.set_axes( self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): - # 向图表中添加多个数据点 + # Add multiple data points into the figure if not hasattr(y, "__len__"): y = [y] n = len(y) @@ -288,7 +301,7 @@ def add(self, x, y): display.clear_output(wait=True) def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): - """训练模型(定义见第3章) + """Train a model (defined in Chapter 3). Defined in :numref:`sec_softmax_scratch`""" animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], @@ -303,7 +316,7 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): assert test_acc <= 1 and test_acc > 0.7, test_acc def predict_ch3(net, test_iter, n=6): - """预测标签(定义见第3章) + """Predict labels (defined in Chapter 3). Defined in :numref:`sec_softmax_scratch`""" for X, y in test_iter: @@ -315,10 +328,10 @@ def predict_ch3(net, test_iter, n=6): d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n]) def evaluate_loss(net, data_iter, loss): - """评估给定数据集上模型的损失 + """Evaluate the loss of a model on the given dataset. Defined in :numref:`sec_model_selection`""" - metric = d2l.Accumulator(2) # 损失的总和,样本数量 + metric = d2l.Accumulator(2) # Sum of losses, no. of examples for X, y in data_iter: l = loss(net(X), y) metric.add(d2l.reduce_sum(l), d2l.size(l)) @@ -328,10 +341,10 @@ def evaluate_loss(net, data_iter, loss): DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/' def download(name, cache_dir=os.path.join('..', 'data')): - """下载一个DATA_HUB中的文件,返回本地文件名 + """Download a file inserted into DATA_HUB, return the local filename. Defined in :numref:`sec_kaggle_house`""" - assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}" + assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}." url, sha1_hash = DATA_HUB[name] os.makedirs(cache_dir, exist_ok=True) fname = os.path.join(cache_dir, url.split('/')[-1]) @@ -344,15 +357,15 @@ def download(name, cache_dir=os.path.join('..', 'data')): break sha1.update(data) if sha1.hexdigest() == sha1_hash: - return fname # 命中缓存 - print(f'正在从{url}下载{fname}...') + return fname # Hit cache + print(f'Downloading {fname} from {url}...') r = requests.get(url, stream=True, verify=True) with open(fname, 'wb') as f: f.write(r.content) return fname def download_extract(name, folder=None): - """下载并解压zip/tar文件 + """Download and extract a zip/tar file. Defined in :numref:`sec_kaggle_house`""" fname = download(name) @@ -363,12 +376,12 @@ def download_extract(name, folder=None): elif ext in ('.tar', '.gz'): fp = tarfile.open(fname, 'r') else: - assert False, '只有zip/tar文件可以被解压缩' + assert False, 'Only zip/tar files can be extracted.' fp.extractall(base_dir) return os.path.join(base_dir, folder) if folder else data_dir def download_all(): - """下载DATA_HUB中的所有文件 + """Download all files in the DATA_HUB. Defined in :numref:`sec_kaggle_house`""" for name in DATA_HUB: @@ -383,20 +396,20 @@ def download_all(): 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') def try_gpu(i=0): - """如果存在,则返回gpu(i),否则返回cpu() + """Return gpu(i) if exists, otherwise return cpu(). Defined in :numref:`sec_use_gpu`""" return npx.gpu(i) if npx.num_gpus() >= i + 1 else npx.cpu() def try_all_gpus(): - """返回所有可用的GPU,如果没有GPU,则返回[cpu()] + """Return all available GPUs, or [cpu()] if no GPU exists. Defined in :numref:`sec_use_gpu`""" devices = [npx.gpu(i) for i in range(npx.num_gpus())] return devices if devices else [npx.cpu()] def corr2d(X, K): - """计算二维互相关运算 + """Compute 2D cross-correlation. Defined in :numref:`sec_conv_layer`""" h, w = K.shape @@ -407,19 +420,20 @@ def corr2d(X, K): return Y def evaluate_accuracy_gpu(net, data_iter, device=None): - """使用GPU计算模型在数据集上的精度 + """Compute the accuracy for a model on a dataset using a GPU. Defined in :numref:`sec_lenet`""" - if not device: # 查询第一个参数所在的第一个设备 + if not device: # Query the first device where the first parameter is on device = list(net.collect_params().values())[0].list_ctx()[0] - metric = d2l.Accumulator(2) # 正确预测的数量,总预测的数量 + # No. of correct predictions, no. of predictions + metric = d2l.Accumulator(2) for X, y in data_iter: X, y = X.as_in_ctx(device), y.as_in_ctx(device) metric.add(d2l.accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): - """用GPU训练模型(在第六章定义) + """Train a model with a GPU (defined in Chapter 6). Defined in :numref:`sec_lenet`""" net.initialize(force_reinit=True, ctx=device, init=init.Xavier()) @@ -430,10 +444,11 @@ def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): 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) # 训练损失之和,训练准确率之和,样本数 + # Sum of training loss, sum of training accuracy, no. of examples + metric = d2l.Accumulator(3) for i, (X, y) in enumerate(train_iter): timer.start() - # 下面是与“d2l.train_epoch_ch3”的主要不同 + # Here is the major difference from `d2l.train_epoch_ch3` X, y = X.as_in_ctx(device), y.as_in_ctx(device) with autograd.record(): y_hat = net(X) @@ -455,6 +470,7 @@ def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): f'on {str(device)}') class Residual(nn.Block): + """The Residual block of ResNet.""" def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs): super().__init__(**kwargs) self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1, @@ -479,7 +495,7 @@ def forward(self, X): '090b5e7e70c295757f55df93cb0a180b9691891a') def read_time_machine(): - """将时间机器数据集加载到文本行的列表中 + """Load the time machine dataset into a list of text lines. Defined in :numref:`sec_text_preprocessing`""" with open(d2l.download('time_machine'), 'r') as f: @@ -487,7 +503,7 @@ def read_time_machine(): return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines] def tokenize(lines, token='word'): - """将文本行拆分为单词或字符词元 + """Split text lines into word or character tokens. Defined in :numref:`sec_text_preprocessing`""" if token == 'word': @@ -495,21 +511,21 @@ def tokenize(lines, token='word'): elif token == 'char': return [list(line) for line in lines] else: - print('错误:未知词元类型:' + token) + print('ERROR: unknown token type: ' + token) class Vocab: - """文本词表""" + """Vocabulary for text.""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): """Defined in :numref:`sec_text_preprocessing`""" if tokens is None: tokens = [] if reserved_tokens is None: reserved_tokens = [] - # 按出现频率排序 + # Sort according to frequencies counter = count_corpus(tokens) self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) - # 未知词元的索引为0 + # The index for the unknown token is 0 self.idx_to_token = [''] + reserved_tokens self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)} @@ -534,68 +550,71 @@ def to_tokens(self, indices): return [self.idx_to_token[index] for index in indices] @property - def unk(self): # 未知词元的索引为0 + def unk(self): # Index for the unknown token return 0 @property - def token_freqs(self): + def token_freqs(self): # Index for the unknown token return self._token_freqs def count_corpus(tokens): - """统计词元的频率 + """Count token frequencies. Defined in :numref:`sec_text_preprocessing`""" - # 这里的tokens是1D列表或2D列表 + # Here `tokens` is a 1D list or 2D list if len(tokens) == 0 or isinstance(tokens[0], list): - # 将词元列表展平成一个列表 + # Flatten a list of token lists into a list of tokens tokens = [token for line in tokens for token in line] return collections.Counter(tokens) def load_corpus_time_machine(max_tokens=-1): - """返回时光机器数据集的词元索引列表和词表 + """Return token indices and the vocabulary of the time machine dataset. Defined in :numref:`sec_text_preprocessing`""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落, - # 所以将所有文本行展平到一个列表中 + # Since each text line in the time machine dataset is not necessarily a + # sentence or a paragraph, flatten all the text lines into a single list corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] return corpus, vocab def seq_data_iter_random(corpus, batch_size, num_steps): - """使用随机抽样生成一个小批量子序列 + """Generate a minibatch of subsequences using random sampling. Defined in :numref:`sec_language_model`""" - # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1 + # Start with a random offset (inclusive of `num_steps - 1`) to partition a + # sequence corpus = corpus[random.randint(0, num_steps - 1):] - # 减去1,是因为我们需要考虑标签 + # Subtract 1 since we need to account for labels num_subseqs = (len(corpus) - 1) // num_steps - # 长度为num_steps的子序列的起始索引 + # The starting indices for subsequences of length `num_steps` initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # 在随机抽样的迭代过程中, - # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻 + # In random sampling, the subsequences from two adjacent random + # minibatches during iteration are not necessarily adjacent on the + # original sequence random.shuffle(initial_indices) def data(pos): - # 返回从pos位置开始的长度为num_steps的序列 + # Return a sequence of length `num_steps` starting from `pos` return corpus[pos: pos + num_steps] num_batches = num_subseqs // batch_size for i in range(0, batch_size * num_batches, batch_size): - # 在这里,initial_indices包含子序列的随机起始索引 + # Here, `initial_indices` contains randomized starting indices for + # subsequences initial_indices_per_batch = initial_indices[i: i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] yield d2l.tensor(X), d2l.tensor(Y) def seq_data_iter_sequential(corpus, batch_size, num_steps): - """使用顺序分区生成一个小批量子序列 + """Generate a minibatch of subsequences using sequential partitioning. Defined in :numref:`sec_language_model`""" - # 从随机偏移量开始划分序列 + # Start with a random offset to partition a sequence offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size Xs = d2l.tensor(corpus[offset: offset + num_tokens]) @@ -608,7 +627,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): yield X, Y class SeqDataLoader: - """加载序列数据的迭代器""" + """An iterator to load sequence data.""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): """Defined in :numref:`sec_language_model`""" if use_random_iter: @@ -623,7 +642,7 @@ def __iter__(self): def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): - """返回时光机器数据集的迭代器和词表 + """Return the iterator and the vocabulary of the time machine dataset. Defined in :numref:`sec_language_model`""" data_iter = SeqDataLoader( @@ -631,7 +650,7 @@ def load_data_time_machine(batch_size, num_steps, return data_iter, data_iter.vocab class RNNModelScratch: - """从零开始实现的循环神经网络模型""" + """An RNN Model implemented from scratch.""" def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn): """Defined in :numref:`sec_rnn_scratch`""" @@ -647,23 +666,23 @@ def begin_state(self, batch_size, ctx): return self.init_state(batch_size, self.num_hiddens, ctx) def predict_ch8(prefix, num_preds, net, vocab, device): - """在prefix后面生成新字符 + """Generate new characters following the `prefix`. Defined in :numref:`sec_rnn_scratch`""" state = net.begin_state(batch_size=1, ctx=device) outputs = [vocab[prefix[0]]] get_input = lambda: d2l.reshape( d2l.tensor([outputs[-1]], ctx=device), (1, 1)) - for y in prefix[1:]: # 预热期 + for y in prefix[1:]: # Warm-up period _, state = net(get_input(), state) outputs.append(vocab[y]) - for _ in range(num_preds): # 预测num_preds步 + for _ in range(num_preds): # Predict `num_preds` steps y, state = net(get_input(), state) outputs.append(int(y.argmax(axis=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) def grad_clipping(net, theta): - """裁剪梯度 + """Clip the gradient. Defined in :numref:`sec_rnn_scratch`""" if isinstance(net, gluon.Block): @@ -676,14 +695,15 @@ def grad_clipping(net, theta): param.grad[:] *= theta / norm def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): - """训练模型一个迭代周期(定义见第8章) + """Train a model within one epoch (defined in Chapter 8). Defined in :numref:`sec_rnn_scratch`""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失之和,词元数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for X, Y in train_iter: if state is None or use_random_iter: - # 在第一次迭代或使用随机抽样时初始化state + # Initialize `state` when either it is the first iteration or + # using random sampling state = net.begin_state(batch_size=X.shape[0], ctx=device) else: for s in state: @@ -695,19 +715,19 @@ def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): l = loss(y_hat, y).mean() l.backward() grad_clipping(net, 1) - updater(batch_size=1) # 因为已经调用了mean函数 + updater(batch_size=1) # Since the `mean` function has been invoked metric.add(l * d2l.size(y), d2l.size(y)) return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): - """训练模型(定义见第8章) + """Train a model (defined in Chapter 8). Defined in :numref:`sec_rnn_scratch`""" loss = gluon.loss.SoftmaxCrossEntropyLoss() animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) - # 初始化 + # Initialize if isinstance(net, gluon.Block): net.initialize(ctx=device, force_reinit=True, init=init.Normal(0.01)) @@ -717,18 +737,18 @@ def train_ch8(net, train_iter, vocab, lr, num_epochs, device, else: updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) - # 训练和预测 + # Train and predict for epoch in range(num_epochs): ppl, speed = train_epoch_ch8( net, train_iter, loss, updater, device, use_random_iter) if (epoch + 1) % 10 == 0: animator.add(epoch + 1, [ppl]) - print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}') + print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') print(predict('time traveller')) print(predict('traveller')) class RNNModel(nn.Block): - """循环神经网络模型 + """The RNN model. Defined in :numref:`sec_rnn-concise`""" def __init__(self, rnn_layer, vocab_size, **kwargs): @@ -740,8 +760,9 @@ def __init__(self, rnn_layer, vocab_size, **kwargs): def forward(self, inputs, state): X = npx.one_hot(inputs.T, self.vocab_size) Y, state = self.rnn(X, state) - # 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数) - # 它的输出形状是(时间步数*批量大小,词表大小) + # The fully-connected layer will first change the shape of `Y` to + # (`num_steps` * `batch_size`, `num_hiddens`). Its output shape is + # (`num_steps` * `batch_size`, `vocab_size`). output = self.dense(Y.reshape(-1, Y.shape[-1])) return output, state @@ -752,31 +773,30 @@ def begin_state(self, *args, **kwargs): '94646ad1522d915e7b0f9296181140edcf86a4f5') def read_data_nmt(): - """载入“英语-法语”数据集 + """Load the English-French dataset. Defined in :numref:`sec_machine_translation`""" data_dir = d2l.download_extract('fra-eng') - with open(os.path.join(data_dir, 'fra.txt'), 'r', - encoding='utf-8') as f: + with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: return f.read() def preprocess_nmt(text): - """预处理“英语-法语”数据集 + """Preprocess the English-French dataset. Defined in :numref:`sec_machine_translation`""" def no_space(char, prev_char): return char in set(',.!?') and prev_char != ' ' - # 使用空格替换不间断空格 - # 使用小写字母替换大写字母 + # Replace non-breaking space with space, and convert uppercase letters to + # lowercase ones text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() - # 在单词和标点符号之间插入空格 + # Insert space between words and punctuation marks out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char for i, char in enumerate(text)] return ''.join(out) def tokenize_nmt(text, num_examples=None): - """词元化“英语-法语”数据数据集 + """Tokenize the English-French dataset. Defined in :numref:`sec_machine_translation`""" source, target = [], [] @@ -789,16 +809,29 @@ def tokenize_nmt(text, num_examples=None): target.append(parts[1].split(' ')) return source, target +def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist): + """Plot the histogram for list length pairs. + + Defined in :numref:`sec_machine_translation`""" + d2l.set_figsize() + _, _, patches = d2l.plt.hist( + [[len(l) for l in xlist], [len(l) for l in ylist]]) + d2l.plt.xlabel(xlabel) + d2l.plt.ylabel(ylabel) + for patch in patches[1].patches: + patch.set_hatch('/') + d2l.plt.legend(legend) + def truncate_pad(line, num_steps, padding_token): - """截断或填充文本序列 + """Truncate or pad sequences. Defined in :numref:`sec_machine_translation`""" if len(line) > num_steps: - return line[:num_steps] # 截断 - return line + [padding_token] * (num_steps - len(line)) # 填充 + return line[:num_steps] # Truncate + return line + [padding_token] * (num_steps - len(line)) # Pad def build_array_nmt(lines, vocab, num_steps): - """将机器翻译的文本序列转换成小批量 + """Transform text sequences of machine translation into minibatches. Defined in :numref:`subsec_mt_data_loading`""" lines = [vocab[l] for l in lines] @@ -810,7 +843,7 @@ def build_array_nmt(lines, vocab, num_steps): return array, valid_len def load_data_nmt(batch_size, num_steps, num_examples=600): - """返回翻译数据集的迭代器和词表 + """Return the iterator and the vocabularies of the translation dataset. Defined in :numref:`subsec_mt_data_loading`""" text = preprocess_nmt(read_data_nmt()) @@ -826,7 +859,7 @@ def load_data_nmt(batch_size, num_steps, num_examples=600): return data_iter, src_vocab, tgt_vocab class Encoder(nn.Block): - """编码器-解码器架构的基本编码器接口""" + """The base encoder interface for the encoder-decoder architecture.""" def __init__(self, **kwargs): super(Encoder, self).__init__(**kwargs) @@ -834,7 +867,7 @@ def forward(self, X, *args): raise NotImplementedError class Decoder(nn.Block): - """编码器-解码器架构的基本解码器接口 + """The base decoder interface for the encoder-decoder architecture. Defined in :numref:`sec_encoder-decoder`""" def __init__(self, **kwargs): @@ -847,7 +880,7 @@ def forward(self, X, state): raise NotImplementedError class EncoderDecoder(nn.Block): - """编码器-解码器架构的基类 + """The base class for the encoder-decoder architecture. Defined in :numref:`sec_encoder-decoder`""" def __init__(self, encoder, decoder, **kwargs): @@ -861,42 +894,42 @@ def forward(self, enc_X, dec_X, *args): return self.decoder(dec_X, dec_state) class Seq2SeqEncoder(d2l.Encoder): - """用于序列到序列学习的循环神经网络编码器 + """The RNN encoder for sequence to sequence learning. Defined in :numref:`sec_seq2seq`""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) - # 嵌入层 + # Embedding layer self.embedding = nn.Embedding(vocab_size, embed_size) self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout) def forward(self, X, *args): - # 输出'X'的形状:(batch_size,num_steps,embed_size) + # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X) - # 在循环神经网络模型中,第一个轴对应于时间步 + # In RNN models, the first axis corresponds to time steps X = X.swapaxes(0, 1) state = self.rnn.begin_state(batch_size=X.shape[1], ctx=X.ctx) output, state = self.rnn(X, state) - # output的形状:(num_steps,batch_size,num_hiddens) - # state[0]的形状:(num_layers,batch_size,num_hiddens) + # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) + # `state[0]` shape: (`num_layers`, `batch_size`, `num_hiddens`) return output, state class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss): - """带遮蔽的softmax交叉熵损失函数 + """The softmax cross-entropy loss with masks. Defined in :numref:`sec_seq2seq_decoder`""" - # pred的形状:(batch_size,num_steps,vocab_size) - # label的形状:(batch_size,num_steps) - # valid_len的形状:(batch_size,) + # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) + # `label` shape: (`batch_size`, `num_steps`) + # `valid_len` shape: (`batch_size`,) def forward(self, pred, label, valid_len): - # weights的形状:(batch_size,num_steps,1) + # `weights` shape: (`batch_size`, `num_steps`, 1) weights = np.expand_dims(np.ones_like(label), axis=-1) weights = npx.sequence_mask(weights, valid_len, True, axis=1) return super(MaskedSoftmaxCELoss, self).forward(pred, label, weights) def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """训练序列到序列模型 + """Train a model for sequence to sequence. Defined in :numref:`sec_seq2seq_decoder`""" net.initialize(init.Xavier(), force_reinit=True, ctx=device) @@ -907,13 +940,13 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): xlim=[10, num_epochs]) for epoch in range(num_epochs): timer = d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失求和,词元数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for batch in data_iter: X, X_valid_len, Y, Y_valid_len = [ x.as_in_ctx(device) for x in batch] - bos = np.array([tgt_vocab['']] * Y.shape[0], - ctx=device).reshape(-1, 1) - dec_input = np.concatenate([bos, Y[:, :-1]], 1) # 强制教学 + bos = np.array( + [tgt_vocab['']] * Y.shape[0], ctx=device).reshape(-1, 1) + dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing with autograd.record(): Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) @@ -925,41 +958,42 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): if (epoch + 1) % 10 == 0: animator.add(epoch + 1, (metric[0] / metric[1],)) print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' - f'tokens/sec on {str(device)}') + f'tokens/sec on {str(device)}') def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=False): - """序列到序列模型的预测 + """Predict for sequence to sequence. Defined in :numref:`sec_seq2seq_training`""" src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ src_vocab['']] enc_valid_len = np.array([len(src_tokens)], ctx=device) src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) - # 添加批量轴 + # Add the batch axis enc_X = np.expand_dims(np.array(src_tokens, ctx=device), axis=0) enc_outputs = net.encoder(enc_X, enc_valid_len) dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) - # 添加批量轴 - dec_X = np.expand_dims(np.array([tgt_vocab['']], ctx=device), - axis=0) + # Add the batch axis + dec_X = np.expand_dims(np.array([tgt_vocab['']], ctx=device), axis=0) output_seq, attention_weight_seq = [], [] for _ in range(num_steps): Y, dec_state = net.decoder(dec_X, dec_state) - # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入 + # We use the token with the highest prediction likelihood as the input + # of the decoder at the next time step dec_X = Y.argmax(axis=2) pred = dec_X.squeeze(axis=0).astype('int32').item() - # 保存注意力权重(稍后讨论) + # Save attention weights (to be covered later) if save_attention_weights: attention_weight_seq.append(net.decoder.attention_weights) - # 一旦序列结束词元被预测,输出序列的生成就完成了 + # Once the end-of-sequence token is predicted, the generation of the + # output sequence is complete if pred == tgt_vocab['']: break output_seq.append(pred) return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq def bleu(pred_seq, label_seq, k): - """计算BLEU + """Compute the BLEU. Defined in :numref:`sec_seq2seq_training`""" pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') @@ -978,7 +1012,7 @@ def bleu(pred_seq, label_seq, k): def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), cmap='Reds'): - """显示矩阵热图 + """Show heatmaps of matrices. Defined in :numref:`sec_attention-cues`""" d2l.use_svg_display() @@ -997,10 +1031,10 @@ def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), fig.colorbar(pcm, ax=axes, shrink=0.6); def masked_softmax(X, valid_lens): - """通过在最后一个轴上掩蔽元素来执行softmax操作 + """Perform softmax operation by masking elements on the last axis. Defined in :numref:`sec_attention-scoring-functions`""" - # X:3D张量,valid_lens:1D或2D张量 + # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor if valid_lens is None: return npx.softmax(X) else: @@ -1009,18 +1043,20 @@ def masked_softmax(X, valid_lens): valid_lens = valid_lens.repeat(shape[1]) else: valid_lens = valid_lens.reshape(-1) - # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 + # On the last axis, replace masked elements with a very large negative + # value, whose exponentiation outputs 0 X = npx.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, True, value=-1e6, axis=1) return npx.softmax(X).reshape(shape) class AdditiveAttention(nn.Block): - """加性注意力 + """Additive attention. Defined in :numref:`sec_attention-scoring-functions`""" def __init__(self, num_hiddens, dropout, **kwargs): super(AdditiveAttention, self).__init__(**kwargs) - # 使用'flatten=False'只转换最后一个轴,以便其他轴的形状保持不变 + # Use `flatten=False` to only transform the last axis so that the + # shapes for the other axes are kept the same self.W_k = nn.Dense(num_hiddens, use_bias=False, flatten=False) self.W_q = nn.Dense(num_hiddens, use_bias=False, flatten=False) self.w_v = nn.Dense(1, use_bias=False, flatten=False) @@ -1028,41 +1064,44 @@ def __init__(self, num_hiddens, dropout, **kwargs): 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) - # 使用广播的方式进行求和 + # After dimension expansion, shape of `queries`: (`batch_size`, no. of + # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, + # no. of key-value pairs, `num_hiddens`). Sum them up with + # broadcasting features = np.expand_dims(queries, axis=2) + np.expand_dims( keys, axis=1) features = np.tanh(features) - # self.w_v仅有一个输出,因此从形状中移除最后那个维度。 - # scores的形状:(batch_size,查询的个数,“键-值”对的个数) + # There is only one output of `self.w_v`, so we remove the last + # one-dimensional entry from the shape. Shape of `scores`: + # (`batch_size`, no. of queries, no. of key-value pairs) scores = np.squeeze(self.w_v(features), axis=-1) self.attention_weights = masked_softmax(scores, valid_lens) - # values的形状:(batch_size,“键-值”对的个数,值的维度) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) return npx.batch_dot(self.dropout(self.attention_weights), values) class DotProductAttention(nn.Block): - """缩放点积注意力 + """Scaled dot product attention. Defined in :numref:`subsec_additive-attention`""" 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,查询的个数) + # Shape of `queries`: (`batch_size`, no. of queries, `d`) + # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) + # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] - # 设置transpose_b=True为了交换keys的最后两个维度 + # Set `transpose_b=True` to swap the last two dimensions of `keys` scores = npx.batch_dot(queries, keys, transpose_b=True) / math.sqrt(d) self.attention_weights = masked_softmax(scores, valid_lens) return npx.batch_dot(self.dropout(self.attention_weights), values) class AttentionDecoder(d2l.Decoder): - """带有注意力机制解码器的基本接口 + """The base attention-based decoder interface. Defined in :numref:`sec_seq2seq_attention`""" def __init__(self, **kwargs): @@ -1073,7 +1112,7 @@ def attention_weights(self): raise NotImplementedError class MultiHeadAttention(nn.Block): - """多头注意力 + """Multi-head attention. Defined in :numref:`sec_multihead-attention`""" def __init__(self, num_hiddens, num_heads, dropout, use_bias=False, @@ -1087,50 +1126,55 @@ def __init__(self, num_hiddens, num_heads, dropout, use_bias=False, self.W_o = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) 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) + # Shape of `queries`, `keys`, or `values`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`) + # Shape of `valid_lens`: + # (`batch_size`,) or (`batch_size`, no. of queries) + # After transposing, shape of output `queries`, `keys`, or `values`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `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次, - # 然后如此复制第二项,然后诸如此类。 + # On axis 0, copy the first item (scalar or vector) for + # `num_heads` times, then copy the next item, and so on valid_lens = valid_lens.repeat(self.num_heads, axis=0) - # output的形状:(batch_size*num_heads,查询的个数, - # num_hiddens/num_heads) + # Shape of `output`: (`batch_size` * `num_heads`, no. of queries, + # `num_hiddens` / `num_heads`) output = self.attention(queries, keys, values, valid_lens) - # output_concat的形状:(batch_size,查询的个数,num_hiddens) + # Shape of `output_concat`: + # (`batch_size`, no. of queries, `num_hiddens`) output_concat = transpose_output(output, self.num_heads) return self.W_o(output_concat) def transpose_qkv(X, num_heads): - """为了多注意力头的并行计算而变换形状 + """Transposition for parallel computation of multiple attention heads. Defined in :numref:`sec_multihead-attention`""" - # 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens) - # 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads, - # num_hiddens/num_heads) + # Shape of input `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`). + # Shape of output `X`: + # (`batch_size`, no. of queries or key-value pairs, `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) + # Shape of output `X`: + # (`batch_size`, `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) X = X.transpose(0, 2, 1, 3) - # 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数, - # num_hiddens/num_heads) + # Shape of `output`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) return X.reshape(-1, X.shape[2], X.shape[3]) def transpose_output(X, num_heads): - """逆转transpose_qkv函数的操作 + """Reverse the operation of `transpose_qkv`. Defined in :numref:`sec_multihead-attention`""" X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) @@ -1138,13 +1182,13 @@ def transpose_output(X, num_heads): return X.reshape(X.shape[0], X.shape[1], -1) class PositionalEncoding(nn.Block): - """位置编码 + """Positional encoding. Defined in :numref:`sec_self-attention-and-positional-encoding`""" def __init__(self, num_hiddens, dropout, max_len=1000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(dropout) - # 创建一个足够长的P + # Create a long enough `P` self.P = d2l.zeros((1, max_len, num_hiddens)) X = d2l.arange(max_len).reshape(-1, 1) / np.power( 10000, np.arange(0, num_hiddens, 2) / num_hiddens) @@ -1156,7 +1200,7 @@ def forward(self, X): return self.dropout(X) class PositionWiseFFN(nn.Block): - """基于位置的前馈网络 + """Positionwise feed-forward network. Defined in :numref:`sec_transformer`""" def __init__(self, ffn_num_hiddens, ffn_num_outputs, **kwargs): @@ -1169,7 +1213,7 @@ def forward(self, X): return self.dense2(self.dense1(X)) class AddNorm(nn.Block): - """残差连接后进行层规范化 + """Residual connection followed by layer normalization. Defined in :numref:`sec_transformer`""" def __init__(self, dropout, **kwargs): @@ -1181,7 +1225,7 @@ def forward(self, X, Y): return self.ln(self.dropout(Y) + X) class EncoderBlock(nn.Block): - """transformer编码器块 + """Transformer encoder block. Defined in :numref:`sec_transformer`""" def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, @@ -1198,7 +1242,7 @@ def forward(self, X, valid_lens): return self.addnorm2(Y, self.ffn(Y)) class TransformerEncoder(d2l.Encoder): - """transformer编码器 + """Transformer encoder. Defined in :numref:`sec_transformer`""" def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, @@ -1214,9 +1258,9 @@ def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, use_bias)) def forward(self, X, valid_lens, *args): - # 因为位置编码值在-1和1之间, - # 因此嵌入值乘以嵌入维度的平方根进行缩放, - # 然后再与位置编码相加。 + # Since positional encoding values are between -1 and 1, the embedding + # values are multiplied by the square root of the embedding dimension + # to rescale before they are summed up 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): @@ -1230,10 +1274,10 @@ def annotate(text, xy, xytext): arrowprops=dict(arrowstyle='->')) def train_2d(trainer, steps=20, f_grad=None): - """用定制的训练机优化2D目标函数 + """Optimize a 2D objective function with a customized trainer. Defined in :numref:`subsec_gd-learningrate`""" - # s1和s2是稍后将使用的内部状态变量 + # `s1` and `s2` are internal state variables that will be used later x1, x2, s1, s2 = -5, -2, 0, 0 results = [(x1, x2)] for i in range(steps): @@ -1246,7 +1290,7 @@ def train_2d(trainer, steps=20, f_grad=None): return results def show_trace_2d(f, results): - """显示优化过程中2D变量的轨迹 + """Show the trace of 2D variables during optimization. Defined in :numref:`subsec_gd-learningrate`""" d2l.set_figsize() @@ -1272,13 +1316,13 @@ def get_data_ch11(batch_size=10, n=1500): def train_ch11(trainer_fn, states, hyperparams, data_iter, feature_dim, num_epochs=2): """Defined in :numref:`sec_minibatches`""" - # 初始化模型 + # Initialization w = np.random.normal(scale=0.01, size=(feature_dim, 1)) b = np.zeros(1) w.attach_grad() b.attach_grad() net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss - # 训练模型 + # Train animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[0, num_epochs], ylim=[0.22, 0.35]) n, timer = 0, d2l.Timer() @@ -1299,7 +1343,7 @@ def train_ch11(trainer_fn, states, hyperparams, data_iter, def train_concise_ch11(tr_name, hyperparams, data_iter, num_epochs=2): """Defined in :numref:`sec_minibatches`""" - # 初始化模型 + # Initialization net = nn.Sequential() net.add(nn.Dense(1)) net.initialize(init.Normal(sigma=0.01)) @@ -1323,7 +1367,7 @@ def train_concise_ch11(tr_name, hyperparams, data_iter, num_epochs=2): print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') class Benchmark: - """用于测量运行时间""" + """For measuring running time.""" def __init__(self, description='Done'): """Defined in :numref:`sec_hybridize`""" self.description = description @@ -1336,7 +1380,7 @@ def __exit__(self, *args): print(f'{self.description}: {self.timer.stop():.4f} sec') def split_batch(X, y, devices): - """将X和y拆分到多个设备上 + """Split `X` and `y` into multiple devices. Defined in :numref:`sec_multi_gpu`""" assert X.shape[0] == y.shape[0] @@ -1344,7 +1388,7 @@ def split_batch(X, y, devices): gluon.utils.split_and_load(y, devices)) def resnet18(num_classes): - """稍加修改的ResNet-18模型 + """A slightly modified ResNet-18 model. Defined in :numref:`sec_multi_gpu_concise`""" def resnet_block(num_channels, num_residuals, first_block=False): @@ -1358,7 +1402,8 @@ def resnet_block(num_channels, num_residuals, first_block=False): return blk net = nn.Sequential() - # 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层 + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1), nn.BatchNorm(), nn.Activation('relu')) net.add(resnet_block(64, 2, first_block=True), @@ -1369,16 +1414,16 @@ def resnet_block(num_channels, num_residuals, first_block=False): return net def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): - """使用多个GPU计算数据集上模型的精度 + """Compute the accuracy for a model on a dataset using multiple GPUs. Defined in :numref:`sec_multi_gpu_concise`""" - # 查询设备列表 + # Query the list of devices devices = list(net.collect_params().values())[0].list_ctx() - # 正确预测的数量,预测的总数量 + # No. of correct predictions, no. of predictions metric = d2l.Accumulator(2) for features, labels in data_iter: X_shards, y_shards = split_f(features, labels, devices) - # 并行运行 + # Run in parallel pred_shards = [net(X_shard) for X_shard in X_shards] metric.add(sum(float(d2l.accuracy(pred_shard, y_shard)) for pred_shard, y_shard in zip( @@ -1387,7 +1432,7 @@ def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): def train_batch_ch13(net, features, labels, loss, trainer, devices, split_f=d2l.split_batch): - """用多GPU进行小批量训练 + """Train for a minibatch with mutiple GPUs (defined in Chapter 13). Defined in :numref:`sec_image_augmentation`""" X_shards, y_shards = split_f(features, labels, devices) @@ -1397,7 +1442,8 @@ def train_batch_ch13(net, features, labels, loss, trainer, devices, in zip(pred_shards, y_shards)] for l in ls: l.backward() - # True标志允许使用过时的梯度,这很有用(例如,在微调BERT中) + # The `True` flag allows parameters with stale gradients, which is useful + # later (e.g., in fine-tuning BERT) trainer.step(labels.shape[0], ignore_stale_grad=True) train_loss_sum = sum([float(l.sum()) for l in ls]) train_acc_sum = sum(d2l.accuracy(pred_shard, y_shard) @@ -1406,14 +1452,15 @@ def train_batch_ch13(net, features, labels, loss, trainer, devices, def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices=d2l.try_all_gpus(), split_f=d2l.split_batch): - """用多GPU进行模型训练 + """Train a model with mutiple GPUs (defined in Chapter 13). 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']) for epoch in range(num_epochs): - # 4个维度:储存训练损失,训练准确度,实例数,特点数 + # Sum of training loss, sum of training accuracy, no. of examples, + # no. of predictions metric = d2l.Accumulator(4) for i, (features, labels) in enumerate(train_iter): timer.start() @@ -1436,7 +1483,7 @@ def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, 'fba480ffa8aa7e0febbb511d181409f899b9baa5') def box_corner_to_center(boxes): - """从(左上,右下)转换到(中间,宽度,高度) + """Convert from (upper-left, lower-right) to (center, width, height). Defined in :numref:`sec_bbox`""" x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] @@ -1448,7 +1495,7 @@ def box_corner_to_center(boxes): return boxes def box_center_to_corner(boxes): - """从(中间,宽度,高度)转换到(左上,右下) + """Convert from (center, width, height) to (upper-left, lower-right). Defined in :numref:`sec_bbox`""" cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] @@ -1460,15 +1507,18 @@ def box_center_to_corner(boxes): return boxes def bbox_to_rect(bbox, color): - """Defined in :numref:`sec_bbox`""" - # 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式: - # ((左上x,左上y),宽,高) + """Convert bounding box to matplotlib format. + + Defined in :numref:`sec_bbox`""" + # Convert the bounding box (upper-left x, upper-left y, lower-right x, + # lower-right y) format to the matplotlib format: ((upper-left x, + # upper-left y), width, height) return d2l.plt.Rectangle( xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], fill=False, edgecolor=color, linewidth=2) def multibox_prior(data, sizes, ratios): - """生成以每个像素为中心具有不同形状的锚框 + """Generate anchor boxes with different shapes centered on each pixel. Defined in :numref:`sec_anchor`""" in_height, in_width = data.shape[-2:] @@ -1476,50 +1526,50 @@ def multibox_prior(data, sizes, ratios): boxes_per_pixel = (num_sizes + num_ratios - 1) size_tensor = d2l.tensor(sizes, ctx=device) ratio_tensor = d2l.tensor(ratios, ctx=device) - - # 为了将锚点移动到像素的中心,需要设置偏移量。 - # 因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5 + # Offsets are required to move the anchor to the center of a pixel. Since + # a pixel has height=1 and width=1, we choose to offset our centers by 0.5 offset_h, offset_w = 0.5, 0.5 - steps_h = 1.0 / in_height # 在y轴上缩放步长 - steps_w = 1.0 / in_width # 在x轴上缩放步长 + steps_h = 1.0 / in_height # Scaled steps in y-axis + steps_w = 1.0 / in_width # Scaled steps in x-axis - # 生成锚框的所有中心点 + # Generate all center points for the anchor boxes center_h = (d2l.arange(in_height, ctx=device) + offset_h) * steps_h center_w = (d2l.arange(in_width, ctx=device) + offset_w) * steps_w shift_x, shift_y = d2l.meshgrid(center_w, center_h) shift_x, shift_y = shift_x.reshape(-1), shift_y.reshape(-1) - # 生成“boxes_per_pixel”个高和宽, - # 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax) + # Generate `boxes_per_pixel` number of heights and widths that are later + # used to create anchor box corner coordinates (xmin, xmax, ymin, ymax) w = np.concatenate((size_tensor * np.sqrt(ratio_tensor[0]), sizes[0] * np.sqrt(ratio_tensor[1:]))) \ - * in_height / in_width # 处理矩形输入 + * in_height / in_width # Handle rectangular inputs h = np.concatenate((size_tensor / np.sqrt(ratio_tensor[0]), sizes[0] / np.sqrt(ratio_tensor[1:]))) - # 除以2来获得半高和半宽 + # Divide by 2 to get half height and half width anchor_manipulations = np.tile(np.stack((-w, -h, w, h)).T, (in_height * in_width, 1)) / 2 - # 每个中心点都将有“boxes_per_pixel”个锚框, - # 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次 + # Each center point will have `boxes_per_pixel` number of anchor boxes, so + # generate a grid of all anchor box centers with `boxes_per_pixel` repeats out_grid = d2l.stack([shift_x, shift_y, shift_x, shift_y], axis=1).repeat(boxes_per_pixel, axis=0) output = out_grid + anchor_manipulations return np.expand_dims(output, axis=0) def show_bboxes(axes, bboxes, labels=None, colors=None): - """显示所有边界框 + """Show bounding boxes. Defined in :numref:`sec_anchor`""" - def _make_list(obj, default_values=None): + + def make_list(obj, default_values=None): if obj is None: obj = default_values elif not isinstance(obj, (list, tuple)): obj = [obj] return obj - labels = _make_list(labels) - colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c']) + labels = make_list(labels) + colors = make_list(colors, ['b', 'g', 'r', 'm', 'c']) for i, bbox in enumerate(bboxes): color = colors[i % len(colors)] rect = d2l.bbox_to_rect(d2l.numpy(bbox), color) @@ -1531,39 +1581,37 @@ def _make_list(obj, default_values=None): bbox=dict(facecolor=color, lw=0)) def box_iou(boxes1, boxes2): - """计算两个锚框或边界框列表中成对的交并比 + """Compute pairwise IoU across two lists of anchor or bounding boxes. Defined in :numref:`sec_anchor`""" 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的数量,) + # Shape of `boxes1`, `boxes2`, `areas1`, `areas2`: (no. of boxes1, 4), + # (no. of boxes2, 4), (no. of boxes1,), (no. of boxes2,) areas1 = box_area(boxes1) areas2 = box_area(boxes2) - - # inter_upperlefts,inter_lowerrights,inters的形状: - # (boxes1的数量,boxes2的数量,2) + # Shape of `inter_upperlefts`, `inter_lowerrights`, `inters`: (no. of + # boxes1, no. of boxes2, 2) inter_upperlefts = np.maximum(boxes1[:, None, :2], boxes2[:, :2]) inter_lowerrights = np.minimum(boxes1[:, None, 2:], boxes2[:, 2:]) inters = (inter_lowerrights - inter_upperlefts).clip(min=0) - # inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量) + # Shape of `inter_areas` and `union_areas`: (no. of boxes1, no. of boxes2) inter_areas = inters[:, :, 0] * inters[:, :, 1] union_areas = areas1[:, None] + areas2 - inter_areas return inter_areas / union_areas def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): - """将最接近的真实边界框分配给锚框 + """Assign closest ground-truth bounding boxes to anchor boxes. Defined in :numref:`sec_anchor`""" num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0] - # 位于第i行和第j列的元素x_ij是锚框i和真实边界框j的IoU + # Element x_ij in the i-th row and j-th column is the IoU of the anchor + # box i and the ground-truth bounding box j jaccard = box_iou(anchors, ground_truth) - # 对于每个锚框,分配的真实边界框的张量 + # Initialize the tensor to hold the assigned ground-truth bounding box for + # each anchor anchors_bbox_map = np.full((num_anchors,), -1, dtype=np.int32, ctx=device) - # 根据阈值,决定是否分配真实边界框 + # Assign ground-truth bounding boxes according to the threshold max_ious, indices = np.max(jaccard, axis=1), np.argmax(jaccard, axis=1) anc_i = np.nonzero(max_ious >= 0.5)[0] box_j = indices[max_ious >= 0.5] @@ -1571,7 +1619,7 @@ def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): col_discard = np.full((num_anchors,), -1) row_discard = np.full((num_gt_boxes,), -1) for _ in range(num_gt_boxes): - max_idx = np.argmax(jaccard) + max_idx = np.argmax(jaccard) # Find the largest IoU box_idx = (max_idx % num_gt_boxes).astype('int32') anc_idx = (max_idx / num_gt_boxes).astype('int32') anchors_bbox_map[anc_idx] = box_idx @@ -1580,7 +1628,7 @@ def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): return anchors_bbox_map def offset_boxes(anchors, assigned_bb, eps=1e-6): - """对锚框偏移量的转换 + """Transform for anchor box offsets. Defined in :numref:`subsec_labeling-anchor-boxes`""" c_anc = d2l.box_corner_to_center(anchors) @@ -1591,7 +1639,7 @@ def offset_boxes(anchors, assigned_bb, eps=1e-6): return offset def multibox_target(anchors, labels): - """使用真实边界框标记锚框 + """Label anchor boxes using ground-truth bounding boxes. Defined in :numref:`subsec_labeling-anchor-boxes`""" batch_size, anchors = labels.shape[0], anchors.squeeze(0) @@ -1603,17 +1651,19 @@ def multibox_target(anchors, labels): label[:, 1:], anchors, device) bbox_mask = np.tile((np.expand_dims((anchors_bbox_map >= 0), axis=-1)), (1, 4)).astype('int32') - # 将类标签和分配的边界框坐标初始化为零 + # Initialize class labels and assigned bounding box coordinates with + # zeros class_labels = d2l.zeros(num_anchors, dtype=np.int32, ctx=device) assigned_bb = d2l.zeros((num_anchors, 4), dtype=np.float32, ctx=device) - # 使用真实边界框来标记锚框的类别。 - # 如果一个锚框没有被分配,我们标记其为背景(值为零) + # Label classes of anchor boxes using their assigned ground-truth + # bounding boxes. If an anchor box is not assigned any, we label its + # class as background (the value remains zero) 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 assigned_bb[indices_true] = label[bb_idx, 1:] - # 偏移量转换 + # Offset transformation offset = offset_boxes(anchors, assigned_bb) * bbox_mask batch_offset.append(offset.reshape(-1)) batch_mask.append(bbox_mask.reshape(-1)) @@ -1624,7 +1674,7 @@ def multibox_target(anchors, labels): return (bbox_offset, bbox_mask, class_labels) def offset_inverse(anchors, offset_preds): - """根据带有预测偏移量的锚框来预测边界框 + """Predict bounding boxes based on anchor boxes with predicted offsets. Defined in :numref:`subsec_labeling-anchor-boxes`""" anc = d2l.box_corner_to_center(anchors) @@ -1635,11 +1685,11 @@ def offset_inverse(anchors, offset_preds): return predicted_bbox def nms(boxes, scores, iou_threshold): - """对预测边界框的置信度进行排序 + """Sort confidence scores of predicted bounding boxes. Defined in :numref:`subsec_predicting-bounding-boxes-nms`""" B = scores.argsort()[::-1] - keep = [] # 保留预测边界框的指标 + keep = [] # Indices of predicted bounding boxes that will be kept while B.size > 0: i = B[0] keep.append(i) @@ -1652,7 +1702,7 @@ def nms(boxes, scores, iou_threshold): def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, pos_threshold=0.009999999): - """使用非极大值抑制来预测边界框 + """Predict bounding boxes using non-maximum suppression. Defined in :numref:`subsec_predicting-bounding-boxes-nms`""" device, batch_size = cls_probs.ctx, cls_probs.shape[0] @@ -1664,8 +1714,7 @@ def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, conf, class_id = np.max(cls_prob[1:], 0), np.argmax(cls_prob[1:], 0) predicted_bb = offset_inverse(anchors, offset_pred) keep = nms(predicted_bb, conf, nms_threshold) - - # 找到所有的non_keep索引,并将类设置为背景 + # Find all non-`keep` indices and set the class to background all_idx = np.arange(num_anchors, dtype=np.int32, ctx=device) combined = d2l.concat((keep, all_idx)) unique, counts = np.unique(combined, return_counts=True) @@ -1674,7 +1723,8 @@ def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, class_id[non_keep] = -1 class_id = class_id[all_id_sorted].astype('float32') conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted] - # pos_threshold是一个用于非背景预测的阈值 + # Here `pos_threshold` is a threshold for positive (non-background) + # predictions below_min_idx = (conf < pos_threshold) class_id[below_min_idx] = -1 conf[below_min_idx] = 1 - conf[below_min_idx] @@ -1689,7 +1739,7 @@ def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, '5de26c8fce5ccdea9f91267273464dc968d20d72') def read_data_bananas(is_train=True): - """读取香蕉检测数据集中的图像和标签 + """Read the banana detection dataset images and labels. Defined in :numref:`sec_object-detection-dataset`""" data_dir = d2l.download_extract('banana-detection') @@ -1702,13 +1752,14 @@ def read_data_bananas(is_train=True): images.append(image.imread( os.path.join(data_dir, 'bananas_train' if is_train else 'bananas_val', 'images', f'{img_name}'))) - # 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y), - # 其中所有图像都具有相同的香蕉类(索引为0) + # Here `target` contains (class, upper-left x, upper-left y, + # lower-right x, lower-right y), where all the images have the same + # banana class (index 0) targets.append(list(target)) return images, np.expand_dims(np.array(targets), 1) / 256 class BananasDataset(gluon.data.Dataset): - """一个用于加载香蕉检测数据集的自定义数据集 + """A customized dataset to load the banana detection dataset. Defined in :numref:`sec_object-detection-dataset`""" def __init__(self, is_train): @@ -1724,7 +1775,7 @@ def __len__(self): return len(self.features) def load_data_bananas(batch_size): - """加载香蕉检测数据集 + """Load the banana detection dataset. Defined in :numref:`sec_object-detection-dataset`""" train_iter = gluon.data.DataLoader(BananasDataset(is_train=True), @@ -1737,7 +1788,7 @@ def load_data_bananas(batch_size): '4e443f8a2eca6b1dac8a6c57641b67dd40621a49') def read_voc_images(voc_dir, is_train=True): - """读取所有VOC图像并标注 + """Read all VOC feature and label images. Defined in :numref:`sec_semantic_segmentation`""" txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation', @@ -1765,7 +1816,7 @@ def read_voc_images(voc_dir, is_train=True): 'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor'] def voc_colormap2label(): - """构建从RGB到VOC类别索引的映射 + """Build the mapping from RGB to class indices for VOC labels. Defined in :numref:`sec_semantic_segmentation`""" colormap2label = np.zeros(256 ** 3) @@ -1775,7 +1826,7 @@ def voc_colormap2label(): return colormap2label def voc_label_indices(colormap, colormap2label): - """将VOC标签中的RGB值映射到它们的类别索引 + """Map any RGB values in VOC labels to their class indices. Defined in :numref:`sec_semantic_segmentation`""" colormap = colormap.astype(np.int32) @@ -1784,7 +1835,7 @@ def voc_label_indices(colormap, colormap2label): return colormap2label[idx] def voc_rand_crop(feature, label, height, width): - """随机裁剪特征和标签图像 + """Randomly crop both feature and label images. Defined in :numref:`sec_semantic_segmentation`""" feature, rect = image.random_crop(feature, (width, height)) @@ -1792,7 +1843,7 @@ def voc_rand_crop(feature, label, height, width): return feature, label class VOCSegDataset(gluon.data.Dataset): - """一个用于加载VOC数据集的自定义数据集 + """A customized dataset to load the VOC dataset. Defined in :numref:`sec_semantic_segmentation`""" def __init__(self, is_train, crop_size, voc_dir): @@ -1824,7 +1875,7 @@ def __len__(self): return len(self.features) def load_data_voc(batch_size, crop_size): - """加载VOC语义分割数据集 + """Load the VOC semantic segmentation dataset. Defined in :numref:`sec_semantic_segmentation`""" voc_dir = d2l.download_extract('voc2012', os.path.join( @@ -1842,29 +1893,30 @@ def load_data_voc(batch_size, crop_size): '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') def read_csv_labels(fname): - """读取fname来给标签字典返回一个文件名 + """Read `fname` to return a filename to label dictionary. Defined in :numref:`sec_kaggle_cifar10`""" with open(fname, 'r') as f: - # 跳过文件头行(列名) + # Skip the file header line (column name) lines = f.readlines()[1:] tokens = [l.rstrip().split(',') for l in lines] return dict(((name, label) for name, label in tokens)) def copyfile(filename, target_dir): - """将文件复制到目标目录 + """Copy a file into a target directory. Defined in :numref:`sec_kaggle_cifar10`""" os.makedirs(target_dir, exist_ok=True) shutil.copy(filename, target_dir) def reorg_train_valid(data_dir, labels, valid_ratio): - """将验证集从原始的训练集中拆分出来 + """Split the validation set out of the original training set. Defined in :numref:`sec_kaggle_cifar10`""" - # 训练数据集中样本最少的类别中的样本数 + # The number of examples of the class that has the fewest examples in the + # training dataset n = collections.Counter(labels.values()).most_common()[-1][1] - # 验证集中每个类别的样本数 + # The number of examples per class for the validation set n_valid_per_label = max(1, math.floor(n * valid_ratio)) label_count = {} for train_file in os.listdir(os.path.join(data_dir, 'train')): @@ -1882,7 +1934,7 @@ def reorg_train_valid(data_dir, labels, valid_ratio): return n_valid_per_label def reorg_test(data_dir): - """在预测期间整理测试集,以方便读取 + """Organize the testing set for data loading during prediction. Defined in :numref:`sec_kaggle_cifar10`""" for test_file in os.listdir(os.path.join(data_dir, 'test')): @@ -1897,26 +1949,26 @@ def reorg_test(data_dir): '319d85e578af0cdc590547f26231e4e31cdf1e42') def read_ptb(): - """将PTB数据集加载到文本行的列表中 + """Load the PTB dataset into a list of text lines. Defined in :numref:`sec_word2vec_data`""" data_dir = d2l.download_extract('ptb') - # Readthetrainingset. + # Read the training set. with open(os.path.join(data_dir, 'ptb.train.txt')) as f: raw_text = f.read() return [line.split() for line in raw_text.split('\n')] def subsample(sentences, vocab): - """下采样高频词 + """Subsample high-frequency words. Defined in :numref:`sec_word2vec_data`""" - # 排除未知词元'' + # Exclude unknown tokens '' sentences = [[token for token in line if vocab[token] != vocab.unk] for line in sentences] counter = d2l.count_corpus(sentences) num_tokens = sum(counter.values()) - # 如果在下采样期间保留词元,则返回True + # Return True if `token` is kept during subsampling def keep(token): return(random.uniform(0, 1) < math.sqrt(1e-4 / counter[token] * num_tokens)) @@ -1925,26 +1977,27 @@ def keep(token): counter) def get_centers_and_contexts(corpus, max_window_size): - """返回跳元模型中的中心词和上下文词 + """Return center words and context words in skip-gram. Defined in :numref:`sec_word2vec_data`""" centers, contexts = [], [] for line in corpus: - # 要形成“中心词-上下文词”对,每个句子至少需要有2个词 + # To form a "center word--context word" pair, each sentence needs to + # have at least 2 words if len(line) < 2: continue centers += line - for i in range(len(line)): # 上下文窗口中间i + for i in range(len(line)): # Context window centered at `i` window_size = random.randint(1, max_window_size) indices = list(range(max(0, i - window_size), min(len(line), i + 1 + window_size))) - # 从上下文词中排除中心词 + # Exclude the center word from the context words indices.remove(i) contexts.append([line[idx] for idx in indices]) return centers, contexts class RandomGenerator: - """根据n个采样权重在{1,...,n}中随机抽取""" + """Randomly draw among {1, ..., n} according to n sampling weights.""" def __init__(self, sampling_weights): """Defined in :numref:`sec_word2vec_data`""" # Exclude @@ -1955,21 +2008,19 @@ def __init__(self, sampling_weights): def draw(self): if self.i == len(self.candidates): - # 缓存k个随机采样结果 + # Cache `k` random sampling results self.candidates = random.choices( self.population, self.sampling_weights, k=10000) self.i = 0 self.i += 1 return self.candidates[self.i - 1] -generator = RandomGenerator([2, 3, 4]) -[generator.draw() for _ in range(10)] - def get_negatives(all_contexts, vocab, counter, K): - """返回负采样中的噪声词 + """Return noise words in negative sampling. Defined in :numref:`sec_word2vec_data`""" - # 索引为1、2、...(索引0是词表中排除的未知标记) + # Sampling weights for words with indices 1, 2, ... (index 0 is the + # excluded unknown token) in the vocabulary sampling_weights = [counter[vocab.to_tokens(i)]**0.75 for i in range(1, len(vocab))] all_negatives, generator = [], RandomGenerator(sampling_weights) @@ -1977,14 +2028,14 @@ def get_negatives(all_contexts, vocab, counter, K): negatives = [] while len(negatives) < len(contexts) * K: neg = generator.draw() - # 噪声词不能是上下文词 + # Noise words cannot be context words if neg not in contexts: negatives.append(neg) all_negatives.append(negatives) return all_negatives def batchify(data): - """返回带有负采样的跳元模型的小批量样本 + """Return a minibatch of examples for skip-gram with negative sampling. Defined in :numref:`sec_word2vec_data`""" max_len = max(len(c) + len(n) for _, c, n in data) @@ -1992,15 +2043,14 @@ def batchify(data): for center, context, negative in data: cur_len = len(context) + len(negative) centers += [center] - contexts_negatives += \ - [context + negative + [0] * (max_len - cur_len)] + contexts_negatives += [context + negative + [0] * (max_len - cur_len)] masks += [[1] * cur_len + [0] * (max_len - cur_len)] labels += [[1] * len(context) + [0] * (max_len - len(context))] return (d2l.reshape(d2l.tensor(centers), (-1, 1)), d2l.tensor( contexts_negatives), d2l.tensor(masks), d2l.tensor(labels)) def load_data_ptb(batch_size, max_window_size, num_noise_words): - """下载PTB数据集,然后将其加载到内存中 + """Download the PTB dataset and then load it into memory. Defined in :numref:`subsec_word2vec-minibatch-loading`""" sentences = read_ptb() @@ -2031,7 +2081,7 @@ def load_data_ptb(batch_size, max_window_size, num_noise_words): 'c1816da3821ae9f43899be655002f6c723e91b88') class TokenEmbedding: - """GloVe嵌入""" + """Token Embedding.""" def __init__(self, embedding_name): """Defined in :numref:`sec_synonyms`""" self.idx_to_token, self.idx_to_vec = self._load_embedding( @@ -2043,13 +2093,13 @@ def __init__(self, embedding_name): def _load_embedding(self, embedding_name): idx_to_token, idx_to_vec = [''], [] data_dir = d2l.download_extract(embedding_name) - # GloVe网站:https://nlp.stanford.edu/projects/glove/ - # fastText网站:https://fasttext.cc/ + # GloVe website: https://nlp.stanford.edu/projects/glove/ + # fastText website: https://fasttext.cc/ with open(os.path.join(data_dir, 'vec.txt'), 'r') as f: for line in f: elems = line.rstrip().split(' ') token, elems = elems[0], [float(elem) for elem in elems[1:]] - # 跳过标题信息,例如fastText中的首行 + # Skip header information, such as the top row in fastText if len(elems) > 1: idx_to_token.append(token) idx_to_vec.append(elems) @@ -2066,11 +2116,11 @@ def __len__(self): return len(self.idx_to_token) def get_tokens_and_segments(tokens_a, tokens_b=None): - """获取输入序列的词元及其片段索引 + """Get tokens of the BERT input sequence and their segment IDs. Defined in :numref:`sec_bert`""" tokens = [''] + tokens_a + [''] - # 0和1分别标记片段A和B + # 0 and 1 are marking segment A and B, respectively segments = [0] * (len(tokens_a) + 2) if tokens_b is not None: tokens += tokens_b + [''] @@ -2078,7 +2128,7 @@ def get_tokens_and_segments(tokens_a, tokens_b=None): return tokens, segments class BERTEncoder(nn.Block): - """BERT编码器 + """BERT encoder. Defined in :numref:`subsec_bert_input_rep`""" def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, @@ -2090,12 +2140,14 @@ def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, for _ in range(num_layers): self.blks.add(d2l.EncoderBlock( num_hiddens, ffn_num_hiddens, num_heads, dropout, True)) - # 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数 + # In BERT, positional embeddings are learnable, thus we create a + # parameter of positional embeddings that are long enough self.pos_embedding = self.params.get('pos_embedding', shape=(1, max_len, num_hiddens)) def forward(self, tokens, segments, valid_lens): - # 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens) + # Shape of `X` remains unchanged in the following code snippet: + # (batch size, max sequence length, `num_hiddens`) X = self.token_embedding(tokens) + self.segment_embedding(segments) X = X + self.pos_embedding.data(ctx=X.ctx)[:, :X.shape[1], :] for blk in self.blks: @@ -2103,7 +2155,7 @@ def forward(self, tokens, segments, valid_lens): return X class MaskLM(nn.Block): - """BERT的掩蔽语言模型任务 + """The masked language model task of BERT. Defined in :numref:`subsec_bert_input_rep`""" def __init__(self, vocab_size, num_hiddens, **kwargs): @@ -2119,8 +2171,8 @@ def forward(self, X, pred_positions): pred_positions = pred_positions.reshape(-1) batch_size = X.shape[0] batch_idx = np.arange(0, batch_size) - # 假设batch_size=2,num_pred_positions=3 - # 那么batch_idx是np.array([0,0,0,1,1,1]) + # Suppose that `batch_size` = 2, `num_pred_positions` = 3, then + # `batch_idx` is `np.array([0, 0, 0, 1, 1, 1])` batch_idx = np.repeat(batch_idx, num_pred_positions) masked_X = X[batch_idx, pred_positions] masked_X = masked_X.reshape((batch_size, num_pred_positions, -1)) @@ -2128,7 +2180,7 @@ def forward(self, X, pred_positions): return mlm_Y_hat class NextSentencePred(nn.Block): - """BERT的下一句预测任务 + """The next sentence prediction task of BERT. Defined in :numref:`subsec_mlm`""" def __init__(self, **kwargs): @@ -2136,11 +2188,11 @@ def __init__(self, **kwargs): self.output = nn.Dense(2) def forward(self, X): - # X的形状:(batchsize,num_hiddens) + # `X` shape: (batch size, `num_hiddens`) return self.output(X) class BERTModel(nn.Block): - """BERT模型 + """The BERT model. Defined in :numref:`subsec_nsp`""" def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, @@ -2152,14 +2204,14 @@ def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, self.mlm = MaskLM(vocab_size, num_hiddens) self.nsp = NextSentencePred() - def forward(self, tokens, segments, valid_lens=None, - pred_positions=None): + def forward(self, tokens, segments, valid_lens=None, pred_positions=None): encoded_X = self.encoder(tokens, segments, valid_lens) if pred_positions is not None: mlm_Y_hat = self.mlm(encoded_X, pred_positions) else: mlm_Y_hat = None - # 用于下一句预测的多层感知机分类器的隐藏层,0是“”标记的索引 + # The hidden layer of the MLP classifier for next sentence prediction. + # 0 is the index of the '' token nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :])) return encoded_X, mlm_Y_hat, nsp_Y_hat @@ -2172,7 +2224,7 @@ def _read_wiki(data_dir): file_name = os.path.join(data_dir, 'wiki.train.tokens') with open(file_name, 'r') as f: lines = f.readlines() - # 大写字母转换为小写字母 + # Uppercase letters are converted to lowercase ones paragraphs = [line.strip().lower().split(' . ') for line in lines if len(line.split(' . ')) >= 2] random.shuffle(paragraphs) @@ -2183,7 +2235,7 @@ def _get_next_sentence(sentence, next_sentence, paragraphs): if random.random() < 0.5: is_next = True else: - # paragraphs是三重列表的嵌套 + # `paragraphs` is a list of lists of lists next_sentence = random.choice(random.choice(paragraphs)) is_next = False return sentence, next_sentence, is_next @@ -2194,7 +2246,7 @@ def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len): for i in range(len(paragraph) - 1): tokens_a, tokens_b, is_next = _get_next_sentence( paragraph[i], paragraph[i + 1], paragraphs) - # 考虑1个''词元和2个''词元 + # Consider 1 '' token and 2 '' tokens if len(tokens_a) + len(tokens_b) + 3 > max_len: continue tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b) @@ -2204,23 +2256,25 @@ def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len): def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds, vocab): """Defined in :numref:`sec_bert-dataset`""" - # 为遮蔽语言模型的输入创建新的词元副本,其中输入可能包含替换的“”或随机词元 + # Make a new copy of tokens for the input of a masked language model, + # where the input may contain replaced '' or random tokens mlm_input_tokens = [token for token in tokens] pred_positions_and_labels = [] - # 打乱后用于在遮蔽语言模型任务中获取15%的随机词元进行预测 + # Shuffle for getting 15% random tokens for prediction in the masked + # language modeling task random.shuffle(candidate_pred_positions) for mlm_pred_position in candidate_pred_positions: if len(pred_positions_and_labels) >= num_mlm_preds: break masked_token = None - # 80%的时间:将词替换为“”词元 + # 80% of the time: replace the word with the '' token if random.random() < 0.8: masked_token = '' else: - # 10%的时间:保持词不变 + # 10% of the time: keep the word unchanged if random.random() < 0.5: masked_token = tokens[mlm_pred_position] - # 10%的时间:用随机词替换该词 + # 10% of the time: replace the word with a random word else: masked_token = random.choice(vocab.idx_to_token) mlm_input_tokens[mlm_pred_position] = masked_token @@ -2231,13 +2285,14 @@ def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds, def _get_mlm_data_from_tokens(tokens, vocab): """Defined in :numref:`subsec_prepare_mlm_data`""" candidate_pred_positions = [] - # tokens是一个字符串列表 + # `tokens` is a list of strings for i, token in enumerate(tokens): - # 在遮蔽语言模型任务中不会预测特殊词元 + # Special tokens are not predicted in the masked language modeling + # task if token in ['', '']: continue candidate_pred_positions.append(i) - # 遮蔽语言模型任务中预测15%的随机词元 + # 15% of random tokens are predicted in the masked language modeling task num_mlm_preds = max(1, round(len(tokens) * 0.15)) mlm_input_tokens, pred_positions_and_labels = _replace_mlm_tokens( tokens, candidate_pred_positions, num_mlm_preds, vocab) @@ -2259,11 +2314,12 @@ def _pad_bert_inputs(examples, max_len, vocab): max_len - len(token_ids)), dtype='int32')) all_segments.append(np.array(segments + [0] * ( max_len - len(segments)), dtype='int32')) - # valid_lens不包括''的计数 + # `valid_lens` excludes count of '' tokens valid_lens.append(np.array(len(token_ids), dtype='float32')) all_pred_positions.append(np.array(pred_positions + [0] * ( max_num_mlm_preds - len(pred_positions)), dtype='int32')) - # 填充词元的预测将通过乘以0权重在损失中过滤掉 + # Predictions of padded tokens will be filtered out in the loss via + # multiplication of 0 weights all_mlm_weights.append( np.array([1.0] * len(mlm_pred_label_ids) + [0.0] * ( max_num_mlm_preds - len(pred_positions)), dtype='float32')) @@ -2276,24 +2332,25 @@ def _pad_bert_inputs(examples, max_len, vocab): class _WikiTextDataset(gluon.data.Dataset): """Defined in :numref:`subsec_prepare_mlm_data`""" def __init__(self, paragraphs, max_len): - # 输入paragraphs[i]是代表段落的句子字符串列表; - # 而输出paragraphs[i]是代表段落的句子列表,其中每个句子都是词元列表 + # Input `paragraphs[i]` is a list of sentence strings representing a + # paragraph; while output `paragraphs[i]` is a list of sentences + # representing a paragraph, where each sentence is a list of tokens paragraphs = [d2l.tokenize( paragraph, token='word') for paragraph in paragraphs] sentences = [sentence for paragraph in paragraphs for sentence in paragraph] self.vocab = d2l.Vocab(sentences, min_freq=5, reserved_tokens=[ '', '', '', '']) - # 获取下一句子预测任务的数据 + # Get data for the next sentence prediction task examples = [] for paragraph in paragraphs: examples.extend(_get_nsp_data_from_paragraph( paragraph, paragraphs, self.vocab, max_len)) - # 获取遮蔽语言模型任务的数据 + # Get data for the masked language model task examples = [(_get_mlm_data_from_tokens(tokens, self.vocab) + (segments, is_next)) for tokens, segments, is_next in examples] - # 填充输入 + # Pad inputs (self.all_token_ids, self.all_segments, self.valid_lens, self.all_pred_positions, self.all_mlm_weights, self.all_mlm_labels, self.nsp_labels) = _pad_bert_inputs( @@ -2309,7 +2366,7 @@ def __len__(self): return len(self.all_token_ids) def load_data_wiki(batch_size, max_len): - """加载WikiText-2数据集 + """Load the WikiText-2 dataset. Defined in :numref:`subsec_prepare_mlm_data`""" num_workers = d2l.get_dataloader_workers() @@ -2332,16 +2389,16 @@ def _get_batch_loss_bert(net, loss, vocab_size, tokens_X_shards, tokens_X_shards, segments_X_shards, valid_lens_x_shards, pred_positions_X_shards, mlm_weights_X_shards, mlm_Y_shards, nsp_y_shards): - # 前向传播 + # Forward pass _, mlm_Y_hat, nsp_Y_hat = net( tokens_X_shard, segments_X_shard, valid_lens_x_shard.reshape(-1), pred_positions_X_shard) - # 计算遮蔽语言模型损失 + # Compute masked language model loss mlm_l = loss( mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y_shard.reshape(-1), mlm_weights_X_shard.reshape((-1, 1))) mlm_l = mlm_l.sum() / (mlm_weights_X_shard.sum() + 1e-8) - # 计算下一句子预测任务的损失 + # Compute next sentence prediction loss nsp_l = loss(nsp_Y_hat, nsp_y_shard) nsp_l = nsp_l.mean() mlm_ls.append(mlm_l) @@ -2355,7 +2412,7 @@ def _get_batch_loss_bert(net, loss, vocab_size, tokens_X_shards, '01ada507287d82875905620988597833ad4e0903') def read_imdb(data_dir, is_train): - """读取IMDb评论数据集文本序列和标签 + """Read the IMDb review dataset text sequences and labels. Defined in :numref:`sec_sentiment`""" data, labels = [], [] @@ -2370,7 +2427,7 @@ def read_imdb(data_dir, is_train): return data, labels def load_data_imdb(batch_size, num_steps=500): - """返回数据迭代器和IMDb评论数据集的词表 + """Return data iterators and the vocabulary of the IMDb review dataset. Defined in :numref:`sec_sentiment`""" data_dir = d2l.download_extract('aclImdb', 'aclImdb') @@ -2389,7 +2446,7 @@ def load_data_imdb(batch_size, num_steps=500): return train_iter, test_iter, vocab def predict_sentiment(net, vocab, sequence): - """预测文本序列的情感 + """Predict the sentiment of a text sequence. Defined in :numref:`sec_sentiment_rnn`""" sequence = np.array(vocab[sequence.split()], ctx=d2l.try_gpu()) @@ -2401,14 +2458,14 @@ def predict_sentiment(net, vocab, sequence): '9fcde07509c7e87ec61c640c1b2753d9041758e4') def read_snli(data_dir, is_train): - """将SNLI数据集解析为前提、假设和标签 + """Read the SNLI dataset into premises, hypotheses, and labels. Defined in :numref:`sec_natural-language-inference-and-dataset`""" def extract_text(s): - # 删除我们不会使用的信息 + # Remove information that will not be used by us s = re.sub('\\(', '', s) s = re.sub('\\)', '', s) - # 用一个空格替换两个或多个连续的空格 + # Substitute two or more consecutive whitespace with space s = re.sub('\\s{2,}', ' ', s) return s.strip() label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2} @@ -2417,13 +2474,12 @@ def extract_text(s): with open(file_name, 'r') as f: rows = [row.split('\t') for row in f.readlines()[1:]] premises = [extract_text(row[1]) for row in rows if row[0] in label_set] - hypotheses = [extract_text(row[2]) for row in rows if row[0] \ - in label_set] + hypotheses = [extract_text(row[2]) for row in rows if row[0] in label_set] labels = [label_set[row[0]] for row in rows if row[0] in label_set] return premises, hypotheses, labels class SNLIDataset(gluon.data.Dataset): - """用于加载SNLI数据集的自定义数据集 + """A customized dataset to load the SNLI dataset. Defined in :numref:`sec_natural-language-inference-and-dataset`""" def __init__(self, dataset, num_steps, vocab=None): @@ -2431,8 +2487,8 @@ def __init__(self, dataset, num_steps, vocab=None): all_premise_tokens = d2l.tokenize(dataset[0]) all_hypothesis_tokens = d2l.tokenize(dataset[1]) if vocab is None: - self.vocab = d2l.Vocab(all_premise_tokens + \ - all_hypothesis_tokens, min_freq=5, reserved_tokens=['']) + self.vocab = d2l.Vocab(all_premise_tokens + all_hypothesis_tokens, + min_freq=5, reserved_tokens=['']) else: self.vocab = vocab self.premises = self._pad(all_premise_tokens) @@ -2452,7 +2508,7 @@ def __len__(self): return len(self.premises) def load_data_snli(batch_size, num_steps=50): - """下载SNLI数据集并返回数据迭代器和词表 + """Download the SNLI dataset and return data iterators and vocabulary. Defined in :numref:`sec_natural-language-inference-and-dataset`""" num_workers = d2l.get_dataloader_workers() @@ -2468,7 +2524,7 @@ def load_data_snli(batch_size, num_steps=50): return train_iter, test_iter, train_set.vocab def split_batch_multi_inputs(X, y, devices): - """将多输入'X'和'y'拆分到多个设备 + """Split multi-input `X` and `y` into multiple devices. Defined in :numref:`sec_natural-language-inference-attention`""" X = list(zip(*[gluon.utils.split_and_load( @@ -2476,7 +2532,7 @@ def split_batch_multi_inputs(X, y, devices): return (X, gluon.utils.split_and_load(y, devices, even_split=False)) def predict_snli(net, vocab, premise, hypothesis): - """预测前提和假设之间的逻辑关系 + """Predict the logical relationship between the premise and hypothesis. Defined in :numref:`sec_natural-language-inference-attention`""" premise = np.array(vocab[premise], ctx=d2l.try_gpu()) @@ -2484,10 +2540,286 @@ def predict_snli(net, vocab, premise, hypothesis): label = np.argmax(net([premise.reshape((1, -1)), hypothesis.reshape((1, -1))]), axis=1) return 'entailment' if label == 0 else 'contradiction' if label == 1 \ - else 'neutral'# Alias defined in config.ini + else 'neutral' + +d2l.DATA_HUB['ml-100k'] = ( + 'https://files.grouplens.org/datasets/movielens/ml-100k.zip', + 'cd4dcac4241c8a4ad7badc7ca635da8a69dddb83') + +def read_data_ml100k(): + data_dir = d2l.download_extract('ml-100k') + names = ['user_id', 'item_id', 'rating', 'timestamp'] + data = pd.read_csv(os.path.join(data_dir, 'u.data'), '\t', names=names, + engine='python') + num_users = data.user_id.unique().shape[0] + num_items = data.item_id.unique().shape[0] + return data, num_users, num_items + +def split_data_ml100k(data, num_users, num_items, + split_mode='random', test_ratio=0.1): + """Split the dataset in random mode or seq-aware mode.""" + if split_mode == 'seq-aware': + train_items, test_items, train_list = {}, {}, [] + for line in data.itertuples(): + u, i, rating, time = line[1], line[2], line[3], line[4] + train_items.setdefault(u, []).append((u, i, rating, time)) + if u not in test_items or test_items[u][-1] < time: + test_items[u] = (i, rating, time) + for u in range(1, num_users + 1): + train_list.extend(sorted(train_items[u], key=lambda k: k[3])) + test_data = [(key, *value) for key, value in test_items.items()] + train_data = [item for item in train_list if item not in test_data] + train_data = pd.DataFrame(train_data) + test_data = pd.DataFrame(test_data) + else: + mask = [True if x == 1 else False for x in np.random.uniform( + 0, 1, (len(data))) < 1 - test_ratio] + neg_mask = [not x for x in mask] + train_data, test_data = data[mask], data[neg_mask] + return train_data, test_data + +def load_data_ml100k(data, num_users, num_items, feedback='explicit'): + users, items, scores = [], [], [] + inter = np.zeros((num_items, num_users)) if feedback == 'explicit' else {} + for line in data.itertuples(): + user_index, item_index = int(line[1] - 1), int(line[2] - 1) + score = int(line[3]) if feedback == 'explicit' else 1 + users.append(user_index) + items.append(item_index) + scores.append(score) + if feedback == 'implicit': + inter.setdefault(user_index, []).append(item_index) + else: + inter[item_index, user_index] = score + return users, items, scores, inter + +def split_and_load_ml100k(split_mode='seq-aware', feedback='explicit', + test_ratio=0.1, batch_size=256): + data, num_users, num_items = read_data_ml100k() + train_data, test_data = split_data_ml100k( + data, num_users, num_items, split_mode, test_ratio) + train_u, train_i, train_r, _ = load_data_ml100k( + train_data, num_users, num_items, feedback) + test_u, test_i, test_r, _ = load_data_ml100k( + test_data, num_users, num_items, feedback) + train_set = gluon.data.ArrayDataset( + np.array(train_u), np.array(train_i), np.array(train_r)) + test_set = gluon.data.ArrayDataset( + np.array(test_u), np.array(test_i), np.array(test_r)) + train_iter = gluon.data.DataLoader( + train_set, shuffle=True, last_batch='rollover', + batch_size=batch_size) + test_iter = gluon.data.DataLoader( + test_set, batch_size=batch_size) + return num_users, num_items, train_iter, test_iter + +def train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus(), evaluator=None, + **kwargs): + timer = d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 2], + legend=['train loss', 'test RMSE']) + for epoch in range(num_epochs): + metric, l = d2l.Accumulator(3), 0. + for i, values in enumerate(train_iter): + timer.start() + input_data = [] + values = values if isinstance(values, list) else [values] + for v in values: + input_data.append(gluon.utils.split_and_load(v, devices)) + train_feat = input_data[0:-1] if len(values) > 1 else input_data + train_label = input_data[-1] + with autograd.record(): + preds = [net(*t) for t in zip(*train_feat)] + ls = [loss(p, s) for p, s in zip(preds, train_label)] + [l.backward() for l in ls] + l += sum([l.asnumpy() for l in ls]).mean() / len(devices) + trainer.step(values[0].shape[0]) + metric.add(l, values[0].shape[0], values[0].size) + timer.stop() + if len(kwargs) > 0: # It will be used in section AutoRec + test_rmse = evaluator(net, test_iter, kwargs['inter_mat'], + devices) + else: + test_rmse = evaluator(net, test_iter, devices) + train_l = l / (i + 1) + animator.add(epoch + 1, (train_l, test_rmse)) + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'test RMSE {test_rmse:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') + +class BPRLoss(gluon.loss.Loss): + def __init__(self, weight=None, batch_axis=0, **kwargs): + super(BPRLoss, self).__init__(weight=None, batch_axis=0, **kwargs) + + def forward(self, positive, negative): + distances = positive - negative + loss = - np.sum(np.log(npx.sigmoid(distances)), 0, keepdims=True) + return loss + +class HingeLossbRec(gluon.loss.Loss): + def __init__(self, weight=None, batch_axis=0, **kwargs): + super(HingeLossbRec, self).__init__(weight=None, batch_axis=0, + **kwargs) + + def forward(self, positive, negative, margin=1): + distances = positive - negative + loss = np.sum(np.maximum(- distances + margin, 0)) + return loss + +def hit_and_auc(rankedlist, test_matrix, k): + hits_k = [(idx, val) for idx, val in enumerate(rankedlist[:k]) + if val in set(test_matrix)] + hits_all = [(idx, val) for idx, val in enumerate(rankedlist) + if val in set(test_matrix)] + max = len(rankedlist) - 1 + auc = 1.0 * (max - hits_all[0][0]) / max if len(hits_all) > 0 else 0 + return len(hits_k), auc + +def evaluate_ranking(net, test_input, seq, candidates, num_users, num_items, + devices): + ranked_list, ranked_items, hit_rate, auc = {}, {}, [], [] + all_items = set([i for i in range(num_users)]) + for u in range(num_users): + neg_items = list(all_items - set(candidates[int(u)])) + user_ids, item_ids, x, scores = [], [], [], [] + [item_ids.append(i) for i in neg_items] + [user_ids.append(u) for _ in neg_items] + x.extend([np.array(user_ids)]) + if seq is not None: + x.append(seq[user_ids, :]) + x.extend([np.array(item_ids)]) + test_data_iter = gluon.data.DataLoader( + gluon.data.ArrayDataset(*x), shuffle=False, last_batch="keep", + batch_size=1024) + for index, values in enumerate(test_data_iter): + x = [gluon.utils.split_and_load(v, devices, even_split=False) + for v in values] + scores.extend([list(net(*t).asnumpy()) for t in zip(*x)]) + scores = [item for sublist in scores for item in sublist] + item_scores = list(zip(item_ids, scores)) + ranked_list[u] = sorted(item_scores, key=lambda t: t[1], reverse=True) + ranked_items[u] = [r[0] for r in ranked_list[u]] + temp = hit_and_auc(ranked_items[u], test_input[u], 50) + hit_rate.append(temp[0]) + auc.append(temp[1]) + return np.mean(np.array(hit_rate)), np.mean(np.array(auc)) + +def train_ranking(net, train_iter, test_iter, loss, trainer, test_seq_iter, + num_users, num_items, num_epochs, devices, evaluator, + candidates, eval_step=1): + timer, hit_rate, auc = d2l.Timer(), 0, 0 + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['test hit rate', 'test AUC']) + for epoch in range(num_epochs): + metric, l = d2l.Accumulator(3), 0. + for i, values in enumerate(train_iter): + input_data = [] + for v in values: + input_data.append(gluon.utils.split_and_load(v, devices)) + with autograd.record(): + p_pos = [net(*t) for t in zip(*input_data[0:-1])] + p_neg = [net(*t) for t in zip(*input_data[0:-2], + input_data[-1])] + ls = [loss(p, n) for p, n in zip(p_pos, p_neg)] + [l.backward(retain_graph=False) for l in ls] + l += sum([l.asnumpy() for l in ls]).mean()/len(devices) + trainer.step(values[0].shape[0]) + metric.add(l, values[0].shape[0], values[0].size) + timer.stop() + with autograd.predict_mode(): + if (epoch + 1) % eval_step == 0: + hit_rate, auc = evaluator(net, test_iter, test_seq_iter, + candidates, num_users, num_items, + devices) + animator.add(epoch + 1, (hit_rate, auc)) + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'test hit rate {float(hit_rate):.3f}, test AUC {float(auc):.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') + +d2l.DATA_HUB['ctr'] = (d2l.DATA_URL + 'ctr.zip', + 'e18327c48c8e8e5c23da714dd614e390d369843f') + +class CTRDataset(gluon.data.Dataset): + def __init__(self, data_path, feat_mapper=None, defaults=None, + min_threshold=4, num_feat=34): + self.NUM_FEATS, self.count, self.data = num_feat, 0, {} + feat_cnts = defaultdict(lambda: defaultdict(int)) + self.feat_mapper, self.defaults = feat_mapper, defaults + self.field_dims = np.zeros(self.NUM_FEATS, dtype=np.int64) + with open(data_path) as f: + for line in f: + instance = {} + values = line.rstrip('\n').split('\t') + if len(values) != self.NUM_FEATS + 1: + continue + label = np.float32([0, 0]) + label[int(values[0])] = 1 + instance['y'] = [np.float32(values[0])] + for i in range(1, self.NUM_FEATS + 1): + feat_cnts[i][values[i]] += 1 + instance.setdefault('x', []).append(values[i]) + self.data[self.count] = instance + self.count = self.count + 1 + if self.feat_mapper is None and self.defaults is None: + feat_mapper = {i: {feat for feat, c in cnt.items() if c >= + min_threshold} for i, cnt in feat_cnts.items()} + self.feat_mapper = {i: {feat_v: idx for idx, feat_v in enumerate(feat_values)} + for i, feat_values in feat_mapper.items()} + self.defaults = {i: len(feat_values) for i, feat_values in feat_mapper.items()} + for i, fm in self.feat_mapper.items(): + self.field_dims[i - 1] = len(fm) + 1 + self.offsets = np.array((0, *np.cumsum(self.field_dims).asnumpy() + [:-1])) + + def __len__(self): + return self.count + + def __getitem__(self, idx): + feat = np.array([self.feat_mapper[i + 1].get(v, self.defaults[i + 1]) + for i, v in enumerate(self.data[idx]['x'])]) + return feat + self.offsets, self.data[idx]['y'] + +def update_D(X, Z, net_D, net_G, loss, trainer_D): + """Update discriminator. + + Defined in :numref:`sec_basic_gan`""" + batch_size = X.shape[0] + ones = np.ones((batch_size,), ctx=X.ctx) + zeros = np.zeros((batch_size,), ctx=X.ctx) + with autograd.record(): + real_Y = net_D(X) + fake_X = net_G(Z) + # Do not need to compute gradient for `net_G`, detach it from + # computing gradients. + fake_Y = net_D(fake_X.detach()) + loss_D = (loss(real_Y, ones) + loss(fake_Y, zeros)) / 2 + loss_D.backward() + trainer_D.step(batch_size) + return float(loss_D.sum()) + +def update_G(Z, net_D, net_G, loss, trainer_G): + """Update generator. + + Defined in :numref:`sec_basic_gan`""" + batch_size = Z.shape[0] + ones = np.ones((batch_size,), ctx=Z.ctx) + with autograd.record(): + # We could reuse `fake_X` from `update_D` to save computation + fake_X = net_G(Z) + # Recomputing `fake_Y` is needed since `net_D` is changed + fake_Y = net_D(fake_X) + loss_G = loss(fake_Y, ones) + loss_G.backward() + trainer_G.step(batch_size) + return float(loss_G.sum()) + +d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip', + 'c065c0e2593b8b161a2d7873e42418bf6a21106c')# Alias defined in config.ini size = lambda a: a.size transpose = lambda a: a.T -nn_Module = nn.Block ones = np.ones zeros = np.zeros @@ -2503,7 +2835,6 @@ def predict_snli(net, vocab, premise, hypothesis): log = np.log tensor = np.array normal = np.random.normal -randn = np.random.randn rand = np.random.rand matmul = np.dot int32 = np.int32 @@ -2518,5 +2849,4 @@ def predict_snli(net, vocab, premise, hypothesis): reduce_sum = lambda x, *args, **kwargs: x.sum(*args, **kwargs) argmax = lambda x, *args, **kwargs: x.argmax(*args, **kwargs) astype = lambda x, *args, **kwargs: x.astype(*args, **kwargs) -reduce_mean = lambda x, *args, **kwargs: x.mean(*args, **kwargs) diff --git a/d2l/paddle.py b/d2l/paddle.py new file mode 100644 index 000000000..6c5813aeb --- /dev/null +++ b/d2l/paddle.py @@ -0,0 +1,2680 @@ +################# WARNING ################ +# The below part is generated automatically through: +# d2lbook build lib +# Don't edit it directly + +import collections +import hashlib +import math +import os +import random +import re +import shutil +import sys +import tarfile +import time +import zipfile +from collections import defaultdict +import pandas as pd +import requests +from IPython import display +from matplotlib import pyplot as plt +from matplotlib_inline import backend_inline + +d2l = sys.modules[__name__] + +import warnings +import numpy as np + +warnings.filterwarnings("ignore") +import paddle +import paddle.vision as paddlevision +from paddle import nn +from paddle.nn import functional as F +from paddle.vision import transforms +from PIL import Image + +paddle.disable_signal_handler() + +def use_svg_display(): + """使用svg格式在Jupyter中显示绘图 + + Defined in :numref:`sec_calculus`""" + backend_inline.set_matplotlib_formats('svg') + +def set_figsize(figsize=(3.5, 2.5)): + """设置matplotlib的图表大小 + + Defined in :numref:`sec_calculus`""" + use_svg_display() + d2l.plt.rcParams['figure.figsize'] = figsize + +def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): + """设置matplotlib的轴 + + Defined in :numref:`sec_calculus`""" + axes.set_xlabel(xlabel) + axes.set_ylabel(ylabel) + axes.set_xscale(xscale) + axes.set_yscale(yscale) + axes.set_xlim(xlim) + axes.set_ylim(ylim) + if legend: + axes.legend(legend) + axes.grid() + +def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, + ylim=None, xscale='linear', yscale='linear', + fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None): + """绘制数据点 + + Defined in :numref:`sec_calculus`""" + if legend is None: + legend = [] + + set_figsize(figsize) + axes = axes if axes else d2l.plt.gca() + + # 如果X有一个轴,输出True + def has_one_axis(X): + return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) + and not hasattr(X[0], "__len__")) + + if has_one_axis(X): + X = [X] + if Y is None: + X, Y = [[]] * len(X), X + elif has_one_axis(Y): + Y = [Y] + if len(X) != len(Y): + X = X * len(Y) + axes.cla() + for x, y, fmt in zip(X, Y, fmts): + if len(x): + axes.plot(x, y, fmt) + else: + axes.plot(y, fmt) + set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend) + +class Timer: + """记录多次运行时间""" + def __init__(self): + """Defined in :numref:`subsec_linear_model`""" + self.times = [] + self.start() + + def start(self): + """启动计时器""" + self.tik = time.time() + + def stop(self): + """停止计时器并将时间记录在列表中""" + self.times.append(time.time() - self.tik) + return self.times[-1] + + def avg(self): + """返回平均时间""" + return sum(self.times) / len(self.times) + + def sum(self): + """返回时间总和""" + return sum(self.times) + + def cumsum(self): + """返回累计时间""" + return np.array(self.times).cumsum().tolist() + +def synthetic_data(w, b, num_examples): + """生成y=Xw+b+噪声 + + Defined in :numref:`sec_linear_scratch`""" + X = d2l.normal(0, 1, (num_examples, len(w))) + y = d2l.matmul(X, w) + b + y += d2l.normal(0, 0.01, y.shape) + return X, d2l.reshape(y, (-1, 1)) + +def linreg(X, w, b): + """线性回归模型 + + Defined in :numref:`sec_linear_scratch`""" + return d2l.matmul(X, w) + b + +def squared_loss(y_hat, y): + """均方损失 + + Defined in :numref:`sec_linear_scratch`""" + return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 + +def sgd(params, lr, batch_size): + """小批量随机梯度下降 + + Defined in :numref:`sec_linear_scratch`""" + with paddle.no_grad(): + for i, param in enumerate(params): + param -= lr * params[i].grad / batch_size + params[i].set_value(param) + params[i].clear_gradient() + +def load_array(data_arrays, batch_size, is_train=True): + """构造一个Paddle数据迭代器 + + Defined in :numref:`sec_linear_concise`""" + dataset = paddle.io.TensorDataset(data_arrays) + return paddle.io.DataLoader(dataset, batch_size=batch_size, + shuffle=is_train, + return_list=True) + +def get_fashion_mnist_labels(labels): + """返回Fashion-MNIST数据集的文本标签 + + Defined in :numref:`sec_fashion_mnist`""" + text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', + 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot'] + return [text_labels[int(i)] for i in labels] + +def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): + """绘制图像列表 + + Defined in :numref:`sec_fashion_mnist`""" + figsize = (num_cols * scale, num_rows * scale) + _, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize) + axes = axes.flatten() + for i, (ax, img) in enumerate(zip(axes, imgs)): + if paddle.is_tensor(img): + # 图片张量 + ax.imshow(img.numpy()) + else: + # PIL图片 + ax.imshow(img) + ax.axes.get_xaxis().set_visible(False) + ax.axes.get_yaxis().set_visible(False) + if titles: + ax.set_title(titles[i]) + return axes + +def get_dataloader_workers(): + """使用4个进程来读取数据 + + Defined in :numref:`sec_fashion_mnist`""" + return 4 + +def load_data_fashion_mnist(batch_size, resize=None): + """下载Fashion-MNIST数据集,然后将其加载到内存中 + + Defined in :numref:`sec_fashion_mnist`""" + trans = [transforms.ToTensor()] + if resize: + trans.insert(0, transforms.Resize(resize)) + trans = transforms.Compose(trans) + mnist_train = paddle.vision.datasets.FashionMNIST(mode="train", + transform=trans) + mnist_test = paddle.vision.datasets.FashionMNIST(mode="test", + transform=trans) + return (paddle.io.DataLoader(dataset=mnist_train, + batch_size=batch_size, + shuffle=True, + return_list=True, + num_workers=get_dataloader_workers()), + paddle.io.DataLoader(dataset=mnist_test, + batch_size=batch_size, + return_list=True, + shuffle=True, + num_workers=get_dataloader_workers())) + +def accuracy(y_hat, y): + """计算预测正确的数量 + + Defined in :numref:`sec_softmax_scratch`""" + if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: + y_hat = d2l.argmax(y_hat, axis=1) + cmp = d2l.astype(y_hat, y.dtype) == y + return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) + +def accuracy(y_hat, y): + """计算预测正确的数量 + + Defined in :numref:`sec_softmax_scratch`""" + if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: + y_hat = y_hat.argmax(axis=1) + if len(y_hat.shape) < len(y.shape): + cmp = y_hat.astype(y.dtype) == y.squeeze() + else: + cmp = y_hat.astype(y.dtype) == y + return float(cmp.astype(y.dtype).sum()) + +def evaluate_accuracy(net, data_iter): + """计算在指定数据集上模型的精度 + + Defined in :numref:`sec_softmax_scratch`""" + if isinstance(net, paddle.nn.Layer): + net.eval() # 将模型设置为评估模式 + metric = Accumulator(2) # 正确预测数、预测总数 + with paddle.no_grad(): + for X, y in data_iter: + metric.add(accuracy(net(X), y), d2l.size(y)) + return metric[0] / metric[1] + +class Accumulator: + """在n个变量上累加""" + def __init__(self, n): + """Defined in :numref:`sec_softmax_scratch`""" + self.data = [0.0] * n + + def add(self, *args): + self.data = [a + float(b) for a, b in zip(self.data, args)] + + def reset(self): + self.data = [0.0] * len(self.data) + + def __getitem__(self, idx): + return self.data[idx] + +def train_epoch_ch3(net, train_iter, loss, updater): + """训练模型一个迭代周期(定义见第3章) + + Defined in :numref:`sec_softmax_scratch`""" + # 将模型设置为训练模式 + if isinstance(net, paddle.nn.Layer): + net.train() + # 训练损失总和、训练准确度总和、样本数 + metric = Accumulator(3) + + for X, y in train_iter: + # 计算梯度并更新参数 + y_hat = net(X) + l = loss(y_hat, y) + if isinstance(updater, paddle.optimizer.Optimizer): + # 使用PaddlePaddle内置的优化器和损失函数 + updater.clear_grad() + l.mean().backward() + updater.step() + else: + # 使用定制的优化器和损失函数 + l.sum().backward() + updater(X.shape[0]) + metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) + return metric[0] / metric[2], metric[1] / metric[2] + +class Animator: + """在动画中绘制数据""" + def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, + ylim=None, xscale='linear', yscale='linear', + fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, + figsize=(3.5, 2.5)): + """Defined in :numref:`sec_softmax_scratch`""" + # 增量地绘制多条线 + if legend is None: + legend = [] + d2l.use_svg_display() + self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) + if nrows * ncols == 1: + self.axes = [self.axes, ] + # 使用lambda函数捕获参数 + self.config_axes = lambda: d2l.set_axes( + self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) + self.X, self.Y, self.fmts = None, None, fmts + + def add(self, x, y): + # 向图表中添加多个数据点 + if not hasattr(y, "__len__"): + y = [y] + n = len(y) + if not hasattr(x, "__len__"): + x = [x] * n + if not self.X: + self.X = [[] for _ in range(n)] + if not self.Y: + self.Y = [[] for _ in range(n)] + for i, (a, b) in enumerate(zip(x, y)): + if a is not None and b is not None: + self.X[i].append(a) + self.Y[i].append(b) + self.axes[0].cla() + for x, y, fmt in zip(self.X, self.Y, self.fmts): + self.axes[0].plot(x, y, fmt) + self.config_axes() + display.display(self.fig) + display.clear_output(wait=True) + +def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): + """训练模型(定义见第3章) + + Defined in :numref:`sec_softmax_scratch`""" + animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], + legend=['train loss', 'train acc', 'test acc']) + for epoch in range(num_epochs): + train_metrics = train_epoch_ch3(net, train_iter, loss, updater) + test_acc = evaluate_accuracy(net, test_iter) + animator.add(epoch + 1, train_metrics + (test_acc,)) + train_loss, train_acc = train_metrics + assert train_loss < 0.5, train_loss + assert train_acc <= 1 and train_acc > 0.7, train_acc + assert test_acc <= 1 and test_acc > 0.7, test_acc + +def predict_ch3(net, test_iter, n=6): + """预测标签(定义见第3章) + + Defined in :numref:`sec_softmax_scratch`""" + for X, y in test_iter: + break + trues = d2l.get_fashion_mnist_labels(y) + preds = d2l.get_fashion_mnist_labels(d2l.argmax(net(X), axis=1)) + titles = [true +'\n' + pred for true, pred in zip(trues, preds)] + d2l.show_images( + d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n]) + +def evaluate_loss(net, data_iter, loss): + """评估给定数据集上模型的损失。 + + Defined in :numref:`sec_model_selection`""" + metric = d2l.Accumulator(2) # 损失的总和, 样本数量 + for X, y in data_iter: + out = net(X) + y = y.reshape(out.shape) + l = loss(out, y) + metric.add(l.sum(), l.numel()) + return metric[0] / metric[1] + +DATA_HUB = dict() +DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/' + +def download(name, cache_dir=os.path.join('..', 'data')): + """下载一个DATA_HUB中的文件,返回本地文件名 + + Defined in :numref:`sec_kaggle_house`""" + assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}" + url, sha1_hash = DATA_HUB[name] + os.makedirs(cache_dir, exist_ok=True) + fname = os.path.join(cache_dir, url.split('/')[-1]) + if os.path.exists(fname): + sha1 = hashlib.sha1() + with open(fname, 'rb') as f: + while True: + data = f.read(1048576) + if not data: + break + sha1.update(data) + if sha1.hexdigest() == sha1_hash: + return fname # 命中缓存 + print(f'正在从{url}下载{fname}...') + r = requests.get(url, stream=True, verify=True) + with open(fname, 'wb') as f: + f.write(r.content) + return fname + +def download_extract(name, folder=None): + """下载并解压zip/tar文件 + + Defined in :numref:`sec_kaggle_house`""" + fname = download(name) + base_dir = os.path.dirname(fname) + data_dir, ext = os.path.splitext(fname) + if ext == '.zip': + fp = zipfile.ZipFile(fname, 'r') + elif ext in ('.tar', '.gz'): + fp = tarfile.open(fname, 'r') + else: + assert False, '只有zip/tar文件可以被解压缩' + fp.extractall(base_dir) + return os.path.join(base_dir, folder) if folder else data_dir + +def download_all(): + """下载DATA_HUB中的所有文件 + + Defined in :numref:`sec_kaggle_house`""" + for name in DATA_HUB: + download(name) + +DATA_HUB['kaggle_house_train'] = ( + DATA_URL + 'kaggle_house_pred_train.csv', + '585e9cc93e70b39160e7921475f9bcd7d31219ce') + +DATA_HUB['kaggle_house_test'] = ( + DATA_URL + 'kaggle_house_pred_test.csv', + 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') + +def try_gpu(i=0): + """如果存在,则返回gpu(i),否则返回cpu()。 + + Defined in :numref:`sec_use_gpu`""" + if paddle.device.cuda.device_count() >= i + 1: + return paddle.CUDAPlace(i) + return paddle.CPUPlace() + +def try_all_gpus(): + """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]。 + + Defined in :numref:`sec_use_gpu`""" + devices = [paddle.CUDAPlace(i) + for i in range(paddle.device.cuda.device_count())] + return devices if devices else paddle.CPUPlace() + +def corr2d(X, K): + """计算二维互相关运算 + + Defined in :numref:`sec_conv_layer`""" + h, w = K.shape + Y = d2l.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) + for i in range(Y.shape[0]): + for j in range(Y.shape[1]): + Y[i, j] = d2l.reduce_sum((X[i: i + h, j: j + w] * K)) + return Y + +def evaluate_accuracy_gpu(net, data_iter, device=None): + """使用GPU计算模型在数据集上的精度 + + Defined in :numref:`sec_lenet`""" + 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] + +def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): + """用GPU训练模型(在第六章定义) + + Defined in :numref:`sec_lenet`""" + 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)}') + +class Residual(nn.Layer): + 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) + +d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt', + '090b5e7e70c295757f55df93cb0a180b9691891a') + +def read_time_machine(): + """将时间机器数据集加载到文本行的列表中 + + Defined in :numref:`sec_text_preprocessing`""" + with open(d2l.download('time_machine'), 'r') as f: + lines = f.readlines() + return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines] + +def tokenize(lines, token='word'): + """将文本行拆分为单词或字符词元 + + Defined in :numref:`sec_text_preprocessing`""" + if token == 'word': + return [line.split() for line in lines] + elif token == 'char': + return [list(line) for line in lines] + else: + print('错误:未知词元类型:' + token) + +class Vocab: + """文本词表""" + def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): + """Defined in :numref:`sec_text_preprocessing`""" + if tokens is None: + tokens = [] + if reserved_tokens is None: + reserved_tokens = [] + # 按出现频率排序 + counter = count_corpus(tokens) + self._token_freqs = sorted(counter.items(), key=lambda x: x[1], + reverse=True) + # 未知词元的索引为0 + self.idx_to_token = [''] + reserved_tokens + self.token_to_idx = {token: idx + for idx, token in enumerate(self.idx_to_token)} + for token, freq in self._token_freqs: + if freq < min_freq: + break + if token not in self.token_to_idx: + self.idx_to_token.append(token) + self.token_to_idx[token] = len(self.idx_to_token) - 1 + + def __len__(self): + return len(self.idx_to_token) + + def __getitem__(self, tokens): + if not isinstance(tokens, (list, tuple)): + return self.token_to_idx.get(tokens, self.unk) + return [self.__getitem__(token) for token in tokens] + + def to_tokens(self, indices): + if not isinstance(indices, (list, tuple)): + return self.idx_to_token[indices] + return [self.idx_to_token[index] for index in indices] + + @property + def unk(self): # 未知词元的索引为0 + return 0 + + @property + def token_freqs(self): + return self._token_freqs + +def count_corpus(tokens): + """统计词元的频率 + + Defined in :numref:`sec_text_preprocessing`""" + # 这里的tokens是1D列表或2D列表 + if len(tokens) == 0 or isinstance(tokens[0], list): + # 将词元列表展平成一个列表 + tokens = [token for line in tokens for token in line] + return collections.Counter(tokens) + +def load_corpus_time_machine(max_tokens=-1): + """返回时光机器数据集的词元索引列表和词表 + + Defined in :numref:`sec_text_preprocessing`""" + lines = read_time_machine() + tokens = tokenize(lines, 'char') + vocab = Vocab(tokens) + # 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落, + # 所以将所有文本行展平到一个列表中 + corpus = [vocab[token] for line in tokens for token in line] + if max_tokens > 0: + corpus = corpus[:max_tokens] + return corpus, vocab + +def seq_data_iter_random(corpus, batch_size, num_steps): + """使用随机抽样生成一个小批量子序列 + + Defined in :numref:`sec_language_model`""" + # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1 + corpus = corpus[random.randint(0, num_steps - 1):] + # 减去1,是因为我们需要考虑标签 + num_subseqs = (len(corpus) - 1) // num_steps + # 长度为num_steps的子序列的起始索引 + initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) + # 在随机抽样的迭代过程中, + # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻 + random.shuffle(initial_indices) + + def data(pos): + # 返回从pos位置开始的长度为num_steps的序列 + return corpus[pos: pos + num_steps] + + num_batches = num_subseqs // batch_size + for i in range(0, batch_size * num_batches, batch_size): + # 在这里,initial_indices包含子序列的随机起始索引 + initial_indices_per_batch = initial_indices[i: i + batch_size] + X = [data(j) for j in initial_indices_per_batch] + Y = [data(j + 1) for j in initial_indices_per_batch] + yield d2l.tensor(X), d2l.tensor(Y) + +def seq_data_iter_sequential(corpus, batch_size, num_steps): + """使用顺序分区生成一个小批量子序列 + + Defined in :numref:`sec_language_model`""" + # 从随机偏移量开始划分序列 + offset = random.randint(0, num_steps) + num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size + Xs = d2l.tensor(corpus[offset: offset + num_tokens]) + Ys = d2l.tensor(corpus[offset + 1: offset + 1 + num_tokens]) + Xs, Ys = Xs.reshape((batch_size, -1)), Ys.reshape((batch_size, -1)) + num_batches = Xs.shape[1] // num_steps + for i in range(0, num_steps * num_batches, num_steps): + X = Xs[:, i: i + num_steps] + Y = Ys[:, i: i + num_steps] + yield X, Y + +class SeqDataLoader: + """加载序列数据的迭代器""" + def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): + """Defined in :numref:`sec_language_model`""" + if use_random_iter: + self.data_iter_fn = d2l.seq_data_iter_random + else: + self.data_iter_fn = d2l.seq_data_iter_sequential + self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens) + self.batch_size, self.num_steps = batch_size, num_steps + + def __iter__(self): + return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps) + +def load_data_time_machine(batch_size, num_steps, + use_random_iter=False, max_tokens=10000): + """返回时光机器数据集的迭代器和词表 + + Defined in :numref:`sec_language_model`""" + data_iter = SeqDataLoader( + batch_size, num_steps, use_random_iter, max_tokens) + return data_iter, data_iter.vocab + +class RNNModelScratch: + """从零开始实现的循环神经网络模型""" + def __init__(self, vocab_size, num_hiddens, + get_params, init_state, forward_fn): + """Defined in :numref:`sec_rnn_scratch`""" + self.vocab_size, self.num_hiddens = vocab_size, num_hiddens + self.params = get_params(vocab_size, num_hiddens) + self.init_state, self.forward_fn = init_state, forward_fn + + def __call__(self, X, state): + X = F.one_hot(X.T, self.vocab_size) + return self.forward_fn(X, state, self.params) + + def begin_state(self, batch_size): + return self.init_state(batch_size, self.num_hiddens) + +def predict_ch8(prefix, num_preds, net, vocab, device): + """在prefix后面生成新字符 + + Defined in :numref:`sec_rnn_scratch`""" + state = net.begin_state(batch_size=1) + outputs = [vocab[prefix[0]]] + get_input = lambda: d2l.reshape(d2l.tensor(outputs[-1], place=device), (1, 1)) + for y in prefix[1:]: # 预热期 + _, state = net(get_input(), state) + outputs.append(vocab[y]) + for _ in range(num_preds): # 预测num_preds步 + y, state = net(get_input(), state) + outputs.append(int(paddle.reshape(paddle.argmax(y,axis=1),shape=[1]))) + return ''.join([vocab.idx_to_token[i] for i in outputs]) + +def grad_clipping(net, theta): + """裁剪梯度 + + Defined in :numref:`sec_rnn_scratch`""" + if isinstance(net, nn.Layer): + params = [p for p in net.parameters() if not p.stop_gradient] + else: + params = net.params + norm = paddle.sqrt(sum(paddle.sum((p.grad ** 2)) for p in params)) + if norm > theta: + with paddle.no_grad(): + for param in params: + param.grad.set_value(param.grad * theta / norm) + +def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): + """训练网络一个迭代周期(定义见第8章) + + Defined in :numref:`sec_rnn_scratch`""" + state, timer = None, d2l.Timer() + metric = d2l.Accumulator(2) # 训练损失之和,词元数量 + for X, Y in train_iter: + if state is None or use_random_iter: + # 在第一次迭代或使用随机抽样时初始化state + state = net.begin_state(batch_size=X.shape[0]) + else: + if isinstance(net, nn.Layer) and not isinstance(state, tuple): + # state对于nn.GRU是个张量 + state.stop_gradient=True + else: + # state对于nn.LSTM或对于我们从零开始实现的模型是个张量 + for s in state: + s.stop_gradient=True + y = paddle.reshape(Y.T,shape=[-1]) + X = paddle.to_tensor(X, place=device) + y = paddle.to_tensor(y, place=device) + y_hat, state = net(X, state) + l = loss(y_hat, y).mean() + if isinstance(updater, paddle.optimizer.Optimizer): + updater.clear_grad() + l.backward() + grad_clipping(net, 1) + updater.step() + else: + l.backward() + grad_clipping(net, 1) + # 因为已经调用了mean函数 + updater(batch_size=1) + + metric.add(l * d2l.size(y), d2l.size(y)) + return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() + +def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): + """训练模型(定义见第8章) + + Defined in :numref:`sec_rnn_scratch`""" + loss = nn.CrossEntropyLoss() + animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', + legend=['train'], xlim=[10, num_epochs]) + # 初始化 + if isinstance(net, nn.Layer): + updater = paddle.optimizer.SGD( + learning_rate=lr, parameters=net.parameters()) + else: + updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) + predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) + # 训练和预测 + for epoch in range(num_epochs): + ppl, speed = train_epoch_ch8( + net, train_iter, loss, updater, device, use_random_iter) + if (epoch + 1) % 10 == 0: + print(predict('time traveller')) + animator.add(epoch + 1, [ppl]) + print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}') + print(predict('time traveller')) + print(predict('traveller')) + +class RNNModel(nn.Layer): + """循环神经网络模型 + + Defined in :numref:`sec_rnn-concise`""" + def __init__(self, rnn_layer, vocab_size, **kwargs): + super(RNNModel, self).__init__(**kwargs) + self.rnn = rnn_layer + self.vocab_size = vocab_size + self.num_hiddens = self.rnn.hidden_size + # 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1 + if self.rnn.num_directions==1: + self.num_directions = 1 + self.linear = nn.Linear(self.num_hiddens, self.vocab_size) + else: + self.num_directions = 2 + self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size) + + def forward(self, inputs, state): + X = F.one_hot(inputs.T, self.vocab_size) + Y, state = self.rnn(X, state) + # 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数) + # 它的输出形状是(时间步数*批量大小,词表大小)。 + output = self.linear(Y.reshape((-1, Y.shape[-1]))) + return output, state + + def begin_state(self, batch_size=1): + if not isinstance(self.rnn, nn.LSTM): + # nn.GRU以张量作为隐状态 + return paddle.zeros(shape=[self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens]) + else: + # nn.LSTM以元组作为隐状态 + return (paddle.zeros( + shape=[self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens]), + paddle.zeros( + shape=[self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens])) + +d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', + '94646ad1522d915e7b0f9296181140edcf86a4f5') + +def read_data_nmt(): + """载入“英语-法语”数据集 + + Defined in :numref:`sec_machine_translation`""" + data_dir = d2l.download_extract('fra-eng') + with open(os.path.join(data_dir, 'fra.txt'), 'r', + encoding='utf-8') as f: + return f.read() + +def preprocess_nmt(text): + """预处理“英语-法语”数据集 + + Defined in :numref:`sec_machine_translation`""" + def no_space(char, prev_char): + return char in set(',.!?') and prev_char != ' ' + + # 使用空格替换不间断空格 + # 使用小写字母替换大写字母 + text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() + # 在单词和标点符号之间插入空格 + out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char + for i, char in enumerate(text)] + return ''.join(out) + +def tokenize_nmt(text, num_examples=None): + """词元化“英语-法语”数据数据集 + + Defined in :numref:`sec_machine_translation`""" + source, target = [], [] + for i, line in enumerate(text.split('\n')): + if num_examples and i > num_examples: + break + parts = line.split('\t') + if len(parts) == 2: + source.append(parts[0].split(' ')) + target.append(parts[1].split(' ')) + return source, target + +def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist): + """绘制列表长度对的直方图 + + Defined in :numref:`sec_machine_translation`""" + d2l.set_figsize() + _, _, patches = d2l.plt.hist( + [[len(l) for l in xlist], [len(l) for l in ylist]]) + d2l.plt.xlabel(xlabel) + d2l.plt.ylabel(ylabel) + for patch in patches[1].patches: + patch.set_hatch('/') + d2l.plt.legend(legend) + +def truncate_pad(line, num_steps, padding_token): + """截断或填充文本序列 + + Defined in :numref:`sec_machine_translation`""" + if len(line) > num_steps: + return line[:num_steps] # 截断 + return line + [padding_token] * (num_steps - len(line)) # 填充 + +def build_array_nmt(lines, vocab, num_steps): + """将机器翻译的文本序列转换成小批量 + + Defined in :numref:`subsec_mt_data_loading`""" + lines = [vocab[l] for l in lines] + lines = [l + [vocab['']] for l in lines] + array = d2l.tensor([truncate_pad( + l, num_steps, vocab['']) for l in lines]) + valid_len = d2l.reduce_sum( + d2l.astype(array != vocab[''], d2l.int32), 1) + return array, valid_len + +def load_data_nmt(batch_size, num_steps, num_examples=600): + """返回翻译数据集的迭代器和词表 + + Defined in :numref:`subsec_mt_data_loading`""" + text = preprocess_nmt(read_data_nmt()) + source, target = tokenize_nmt(text, num_examples) + src_vocab = d2l.Vocab(source, min_freq=2, + reserved_tokens=['', '', '']) + tgt_vocab = d2l.Vocab(target, min_freq=2, + reserved_tokens=['', '', '']) + src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps) + tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps) + data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len) + data_iter = d2l.load_array(data_arrays, batch_size) + return data_iter, src_vocab, tgt_vocab + +class Encoder(nn.Layer): + """编码器-解码器架构的基本编码器接口""" + def __init__(self, **kwargs): + super(Encoder, self).__init__(**kwargs) + + def forward(self, X, *args): + raise NotImplementedError + +class Decoder(nn.Layer): + """编码器-解码器架构的基本解码器接口 + + Defined in :numref:`sec_encoder-decoder`""" + def __init__(self, **kwargs): + super(Decoder, self).__init__(**kwargs) + + def init_state(self, enc_outputs, *args): + raise NotImplementedError + + def forward(self, X, state): + raise NotImplementedError + +class EncoderDecoder(nn.Layer): + """编码器-解码器架构的基类 + + Defined in :numref:`sec_encoder-decoder`""" + def __init__(self, encoder, decoder, **kwargs): + super(EncoderDecoder, self).__init__(**kwargs) + self.encoder = encoder + self.decoder = decoder + + def forward(self, enc_X, dec_X, *args): + enc_outputs = self.encoder(enc_X, *args) + dec_state = self.decoder.init_state(enc_outputs, *args) + return self.decoder(dec_X, dec_state) + +class Seq2SeqEncoder(d2l.Encoder): + """用于序列到序列学习的循环神经网络编码器 + + Defined in :numref:`sec_seq2seq`""" + def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, + dropout=0, **kwargs): + super(Seq2SeqEncoder, self).__init__(**kwargs) + weight_ih_attr = paddle.ParamAttr(initializer=nn.initializer.XavierUniform()) + weight_hh_attr = paddle.ParamAttr(initializer=nn.initializer.XavierUniform()) + # 嵌入层 + self.embedding = nn.Embedding(vocab_size, embed_size) + self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout, + time_major=True, weight_ih_attr=weight_ih_attr, weight_hh_attr=weight_hh_attr) + + def forward(self, X, *args): + # 输出'X'的形状:(batch_size,num_steps,embed_size) + X = self.embedding(X) + # 在循环神经网络模型中,第一个轴对应于时间步 + X = X.transpose([1, 0, 2]) + # 如果未提及状态,则默认为0 + output, state = self.rnn(X) + # PaddlePaddle的GRU层output的形状:(batch_size,time_steps,num_directions * num_hiddens), + # 需设定time_major=True,指定input的第一个维度为time_steps + # state[0]的形状:(num_layers,batch_size,num_hiddens) + return output, state + +def sequence_mask(X, valid_len, value=0): + """在序列中屏蔽不相关的项 + + Defined in :numref:`sec_seq2seq_decoder`""" + maxlen = X.shape[1] + mask = paddle.arange((maxlen), dtype=paddle.float32)[None, :] < valid_len[:, None] + Xtype = X.dtype + X = X.astype(paddle.float32) + X[~mask] = float(value) + return X.astype(Xtype) + +class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): + """带遮蔽的softmax交叉熵损失函数 + + Defined in :numref:`sec_seq2seq_decoder`""" + # pred的形状:(batch_size,num_steps,vocab_size) + # label的形状:(batch_size,num_steps) + # valid_len的形状:(batch_size,) + def forward(self, pred, label, valid_len): + weights = paddle.ones_like(label) + weights = sequence_mask(weights, valid_len) + self.reduction='none' + unweighted_loss = super(MaskedSoftmaxCELoss, self).forward( + pred, label) + weighted_loss = (unweighted_loss * weights).mean(axis=1) + return weighted_loss + +def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): + """训练序列到序列模型 + + Defined in :numref:`sec_seq2seq_decoder`""" + optimizer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters()) + loss = MaskedSoftmaxCELoss() + net.train() + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[10, num_epochs]) + for epoch in range(num_epochs): + timer = d2l.Timer() + metric = d2l.Accumulator(2) # 训练损失总和,词元数量 + for batch in data_iter: + optimizer.clear_grad() + X, X_valid_len, Y, Y_valid_len = [paddle.to_tensor(x, place=device) for x in batch] + bos = paddle.to_tensor([tgt_vocab['']] * Y.shape[0]).reshape([-1, 1]) + dec_input = paddle.concat([bos, Y[:, :-1]], 1) # 强制教学 + Y_hat, _ = net(X, dec_input, X_valid_len.squeeze()) + l = loss(Y_hat, Y, Y_valid_len.squeeze()) + l.backward() # 损失函数的标量进行“反向传播” + d2l.grad_clipping(net, 1) + num_tokens = Y_valid_len.sum() + optimizer.step() + with paddle.no_grad(): + metric.add(l.sum(), num_tokens) + if (epoch + 1) % 10 == 0: + animator.add(epoch + 1, (metric[0] / metric[1],)) + print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' + f'tokens/sec on {str(device)}') + +def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, + device, save_attention_weights=False): + """序列到序列模型的预测 + + Defined in :numref:`sec_seq2seq_training`""" + # 在预测时将net设置为评估模式 + net.eval() + src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ + src_vocab['']] + enc_valid_len = paddle.to_tensor([len(src_tokens)], place=device) + src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) + # 添加批量轴 + enc_X = paddle.unsqueeze( + paddle.to_tensor(src_tokens, dtype=paddle.int64, place=device), axis=0) + enc_outputs = net.encoder(enc_X, enc_valid_len) + dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) + # 添加批量轴 + dec_X = paddle.unsqueeze(paddle.to_tensor( + [tgt_vocab['']], dtype=paddle.int64, place=device), axis=0) + output_seq, attention_weight_seq = [], [] + for _ in range(num_steps): + Y, dec_state = net.decoder(dec_X, dec_state) + # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入 + dec_X = Y.argmax(axis=2) + pred = dec_X.squeeze(axis=0).astype(paddle.int32).item() + # 保存注意力权重(稍后讨论) + if save_attention_weights: + attention_weight_seq.append(net.decoder.attention_weights) + # 一旦序列结束词元被预测,输出序列的生成就完成了 + if pred == tgt_vocab['']: + break + output_seq.append(pred) + return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq + +def bleu(pred_seq, label_seq, k): + """计算BLEU + + Defined in :numref:`sec_seq2seq_training`""" + pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') + len_pred, len_label = len(pred_tokens), len(label_tokens) + score = math.exp(min(0, 1 - len_label / len_pred)) + for n in range(1, k + 1): + num_matches, label_subs = 0, collections.defaultdict(int) + for i in range(len_label - n + 1): + label_subs[' '.join(label_tokens[i: i + n])] += 1 + for i in range(len_pred - n + 1): + if label_subs[' '.join(pred_tokens[i: i + n])] > 0: + num_matches += 1 + label_subs[' '.join(pred_tokens[i: i + n])] -= 1 + score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n)) + return score + +def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), + cmap='Reds'): + """显示矩阵热图 + + Defined in :numref:`sec_attention-cues`""" + d2l.use_svg_display() + num_rows, num_cols = matrices.shape[0], matrices.shape[1] + fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, + sharex=True, sharey=True, squeeze=False) + for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)): + for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)): + pcm = ax.imshow(d2l.numpy(matrix), cmap=cmap) + if i == num_rows - 1: + ax.set_xlabel(xlabel) + if j == 0: + ax.set_ylabel(ylabel) + if titles: + ax.set_title(titles[j]) + fig.colorbar(pcm, ax=axes, shrink=0.6); + +def masked_softmax(X, valid_lens): + """通过在最后一个轴上掩蔽元素来执行softmax操作 + + Defined in :numref:`sec_attention-scoring-functions`""" + # 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) + +class AdditiveAttention(nn.Layer): + """加性注意力 + + Defined in :numref:`sec_attention-scoring-functions`""" + 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) + +class DotProductAttention(nn.Layer): + """缩放点积注意力 + + Defined in :numref:`subsec_additive-attention`""" + 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) + +class AttentionDecoder(d2l.Decoder): + """带有注意力机制解码器的基本接口 + + Defined in :numref:`sec_seq2seq_attention`""" + def __init__(self, **kwargs): + super(AttentionDecoder, self).__init__(**kwargs) + + @property + def attention_weights(self): + raise NotImplementedError + +class MultiHeadAttention(nn.Layer): + """Defined in :numref:`sec_multihead-attention`""" + 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) + +def transpose_qkv(X, num_heads): + """为了多注意力头的并行计算而变换形状 + + Defined in :numref:`sec_multihead-attention`""" + # 输入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])) + + +def transpose_output(X, num_heads): + """逆转transpose_qkv函数的操作 + + Defined in :numref:`sec_multihead-attention`""" + 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)) + +class PositionalEncoding(nn.Layer): + """位置编码 + + Defined in :numref:`sec_self-attention-and-positional-encoding`""" + 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) + +class PositionWiseFFN(nn.Layer): + """基于位置的前馈网络 + + Defined in :numref:`sec_transformer`""" + 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))) + +class AddNorm(nn.Layer): + """残差连接后进行层规范化 + + Defined in :numref:`sec_transformer`""" + 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) + +class EncoderBlock(nn.Layer): + """transformer编码器块 + + Defined in :numref:`sec_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)) + +class TransformerEncoder(d2l.Encoder): + """transformer编码器 + + Defined in :numref:`sec_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 + +def annotate(text, xy, xytext): + d2l.plt.gca().annotate(text, xy=xy, xytext=xytext, + arrowprops=dict(arrowstyle='->')) + +def train_2d(trainer, steps=20, f_grad=None): + """用定制的训练机优化2D目标函数 + + Defined in :numref:`subsec_gd-learningrate`""" + # s1和s2是稍后将使用的内部状态变量 + x1, x2, s1, s2 = -5, -2, 0, 0 + results = [(x1, x2)] + for i in range(steps): + if f_grad: + x1, x2, s1, s2 = trainer(x1, x2, s1, s2, f_grad) + else: + x1, x2, s1, s2 = trainer(x1, x2, s1, s2) + results.append((x1, x2)) + print(f'epoch {i + 1}, x1: {float(x1):f}, x2: {float(x2):f}') + return results + +def show_trace_2d(f, results): + """显示优化过程中2D变量的轨迹 + + Defined in :numref:`subsec_gd-learningrate`""" + d2l.set_figsize() + d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e') + x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1, dtype='float32'), + d2l.arange(-3.0, 1.0, 0.1, dtype='float32')) + d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4') + d2l.plt.xlabel('x1') + d2l.plt.ylabel('x2') + +d2l.DATA_HUB['airfoil'] = (d2l.DATA_URL + 'airfoil_self_noise.dat', + '76e5be1548fd8222e5074cf0faae75edff8cf93f') + +def get_data_ch11(batch_size=10, n=1500): + """Defined in :numref:`sec_minibatches`""" + data = np.genfromtxt(d2l.download('airfoil'), + dtype=np.float32, delimiter='\t') + data = d2l.tensor((data - data.mean(axis=0)) / data.std(axis=0)) + data_iter = d2l.load_array((data[:n, :-1], data[:n, -1]), + batch_size, is_train=True) + return data_iter, data.shape[1]-1 + +def train_ch11(trainer_fn, states, hyperparams, data_iter, + feature_dim, num_epochs=2): + """Defined in :numref:`sec_minibatches`""" + # 初始化模型 + w = d2l.tensor(d2l.normal(mean=0.0, std=0.01, shape=(feature_dim, 1)), stop_gradient=False) + b = d2l.tensor(d2l.zeros((1,)), stop_gradient=False) + net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss + # 训练模型 + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + l = loss(net(X), y).mean() + l.backward() + w, b = trainer_fn([w, b], states, hyperparams) + n += X.shape[0] + if n % 200 == 0: + timer.stop() + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss),)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + return timer.cumsum(), animator.Y[0] + +def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=4): + """Defined in :numref:`sec_minibatches`""" + # 初始化模型 + net = nn.Sequential(nn.Linear(5, 1)) + def init_weights(m): + if type(m) == nn.Linear: + paddle.nn.initializer.Normal(m.weight, std=0.01) + + net.apply(init_weights) + + optimizer = trainer_fn(parameters=net.parameters(), **hyperparams) + loss = nn.MSELoss(reduction='none') + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + optimizer.clear_grad() + out = net(X) + y = y.reshape(out.shape) + l = loss(out, y) + l.mean().backward() + optimizer.step() + n += X.shape[0] + if n % 200 == 0: + timer.stop() + # MSELoss计算平方误差时不带系数1/2 + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss) / 2,)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + +class Benchmark: + """用于测量运行时间""" + def __init__(self, description='Done'): + """Defined in :numref:`sec_hybridize`""" + self.description = description + + def __enter__(self): + self.timer = d2l.Timer() + return self + + def __exit__(self, *args): + print(f'{self.description}: {self.timer.stop():.4f} sec') + +def split_batch(X, y, devices): + """将X和y拆分到多个设备上 + + Defined in :numref:`sec_multi_gpu`""" + assert X.shape[0] == y.shape[0] + return (paddlescatter(X, devices), + paddlescatter(y, devices)) + +def resnet18(num_classes, in_channels=1): + """稍加修改的ResNet-18模型 + + Defined in :numref:`sec_multi_gpu_concise`""" + 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 + +def train_batch_ch13(net, X, y, loss, trainer, devices): + """Defined in :numref:`sec_image_augmentation`""" + """用多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 + +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus()): + """Defined in :numref:`sec_image_augmentation`""" + """用多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)}') + +d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip', + 'fba480ffa8aa7e0febbb511d181409f899b9baa5') + +def box_corner_to_center(boxes): + """从(左上,右下)转换到(中间,宽度,高度) + + Defined in :numref:`sec_bbox`""" + x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + cx = (x1 + x2) / 2 + cy = (y1 + y2) / 2 + w = x2 - x1 + h = y2 - y1 + boxes = d2l.stack((cx, cy, w, h), axis=-1) + return boxes + +def box_center_to_corner(boxes): + """从(中间,宽度,高度)转换到(左上,右下) + + Defined in :numref:`sec_bbox`""" + cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + x1 = cx - 0.5 * w + y1 = cy - 0.5 * h + x2 = cx + 0.5 * w + y2 = cy + 0.5 * h + boxes = d2l.stack((x1, y1, x2, y2), axis=-1) + return boxes + +def bbox_to_rect(bbox, color): + """Defined in :numref:`sec_bbox`""" + # 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式: + # ((左上x,左上y),宽,高) + return d2l.plt.Rectangle( + xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], + fill=False, edgecolor=color, linewidth=2) + +def multibox_prior(data, sizes, ratios): + """生成以每个像素为中心具有不同形状的锚框 + + Defined in :numref:`sec_anchor`""" + 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) + +def show_bboxes(axes, bboxes, labels=None, colors=None): + """显示所有边界框 + + Defined in :numref:`sec_anchor`""" + def _make_list(obj, default_values=None): + if obj is None: + obj = default_values + elif not isinstance(obj, (list, tuple)): + obj = [obj] + return obj + + labels = _make_list(labels) + colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c']) + for i, bbox in enumerate(bboxes): + color = colors[i % len(colors)] + rect = d2l.bbox_to_rect(d2l.numpy(bbox), color) + axes.add_patch(rect) + if labels and len(labels) > i: + text_color = 'k' if color == 'w' else 'w' + axes.text(rect.xy[0], rect.xy[1], labels[i], + va='center', ha='center', fontsize=9, color=text_color, + bbox=dict(facecolor=color, lw=0)) + +def box_iou(boxes1, boxes2): + """计算两个锚框或边界框列表中成对的交并比 + + Defined in :numref:`sec_anchor`""" + 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 + +def assign_anchor_to_bbox(ground_truth, anchors, place, iou_threshold=0.5): + """将最接近的真实边界框分配给锚框 + + Defined in :numref:`sec_anchor`""" + 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 + +def offset_boxes(anchors, assigned_bb, eps=1e-6): + """对锚框偏移量的转换 + + Defined in :numref:`subsec_labeling-anchor-boxes`""" + c_anc = d2l.box_corner_to_center(anchors) + c_assigned_bb = d2l.box_corner_to_center(assigned_bb) + offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:] + offset_wh = 5 * d2l.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:]) + offset = d2l.concat([offset_xy, offset_wh], axis=1) + return offset + +def multibox_target(anchors, labels): + """使用真实边界框标记锚框 + + Defined in :numref:`subsec_labeling-anchor-boxes`""" + 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) + +def offset_inverse(anchors, offset_preds): + """根据带有预测偏移量的锚框来预测边界框 + + Defined in :numref:`subsec_labeling-anchor-boxes`""" + anc = d2l.box_corner_to_center(anchors) + pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2] + pred_bbox_wh = d2l.exp(offset_preds[:, 2:] / 5) * anc[:, 2:] + pred_bbox = d2l.concat((pred_bbox_xy, pred_bbox_wh), axis=1) + predicted_bbox = d2l.box_center_to_corner(pred_bbox) + return predicted_bbox + +def nms(boxes, scores, iou_threshold): + """对预测边界框的置信度进行排序 + + Defined in :numref:`subsec_predicting-bounding-boxes-nms`""" + 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') + +def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, + pos_threshold=0.009999999): + """使用非极大值抑制来预测边界框 + + Defined in :numref:`subsec_predicting-bounding-boxes-nms`""" + 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) + +d2l.DATA_HUB['banana-detection'] = ( + d2l.DATA_URL + 'banana-detection.zip', + '5de26c8fce5ccdea9f91267273464dc968d20d72') + +def read_data_bananas(is_train=True): + """读取香蕉检测数据集中的图像和标签 + + Defined in :numref:`sec_object-detection-dataset`""" + 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 + +class BananasDataset(paddle.io.Dataset): + """一个用于加载香蕉检测数据集的自定义数据集 + + Defined in :numref:`sec_object-detection-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) + +def load_data_bananas(batch_size): + """加载香蕉检测数据集 + + Defined in :numref:`sec_object-detection-dataset`""" + 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 + +d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar', + '4e443f8a2eca6b1dac8a6c57641b67dd40621a49') + +def read_voc_images(voc_dir, is_train=True): + """Defined in :numref:`sec_semantic_segmentation` + + 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 + +VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], + [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128], + [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0], + [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128], + [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0], + [0, 64, 128]] + +VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat', + 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', + 'diningtable', 'dog', 'horse', 'motorbike', 'person', + 'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor'] + +def voc_colormap2label(): + """构建从RGB到VOC类别索引的映射 + + Defined in :numref:`sec_semantic_segmentation`""" + 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 + +def voc_label_indices(colormap, colormap2label): + """将VOC标签中的RGB值映射到它们的类别索引 + + Defined in :numref:`sec_semantic_segmentation`""" + colormap = colormap.transpose([1, 2, 0]).astype('int32') + idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + + colormap[:, :, 2]) + return colormap2label[idx] + +def voc_rand_crop(feature, label, height, width): + """随机裁剪特征和标签图像 + + Defined in :numref:`sec_semantic_segmentation`""" + 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 + +class VOCSegDataset(paddle.io.Dataset): + """Defined in :numref:`sec_semantic_segmentation`""" + """一个用于加载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) + +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')) + 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 + +d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip', + '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') + +def read_csv_labels(fname): + """读取fname来给标签字典返回一个文件名 + + Defined in :numref:`sec_kaggle_cifar10`""" + with open(fname, 'r') as f: + # 跳过文件头行(列名) + lines = f.readlines()[1:] + tokens = [l.rstrip().split(',') for l in lines] + return dict(((name, label) for name, label in tokens)) + +def copyfile(filename, target_dir): + """将文件复制到目标目录 + + Defined in :numref:`sec_kaggle_cifar10`""" + os.makedirs(target_dir, exist_ok=True) + shutil.copy(filename, target_dir) + +def reorg_train_valid(data_dir, labels, valid_ratio): + """将验证集从原始的训练集中拆分出来 + + Defined in :numref:`sec_kaggle_cifar10`""" + # 训练数据集中样本最少的类别中的样本数 + n = collections.Counter(labels.values()).most_common()[-1][1] + # 验证集中每个类别的样本数 + n_valid_per_label = max(1, math.floor(n * valid_ratio)) + label_count = {} + for train_file in os.listdir(os.path.join(data_dir, 'train')): + label = labels[train_file.split('.')[0]] + fname = os.path.join(data_dir, 'train', train_file) + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train_valid', label)) + if label not in label_count or label_count[label] < n_valid_per_label: + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'valid', label)) + label_count[label] = label_count.get(label, 0) + 1 + else: + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train', label)) + return n_valid_per_label + +def reorg_test(data_dir): + """在预测期间整理测试集,以方便读取 + + Defined in :numref:`sec_kaggle_cifar10`""" + for test_file in os.listdir(os.path.join(data_dir, 'test')): + copyfile(os.path.join(data_dir, 'test', test_file), + os.path.join(data_dir, 'train_valid_test', 'test', + 'unknown')) + +d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip', + '0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d') + +d2l.DATA_HUB['ptb'] = (d2l.DATA_URL + 'ptb.zip', + '319d85e578af0cdc590547f26231e4e31cdf1e42') + +def read_ptb(): + """将PTB数据集加载到文本行的列表中 + + Defined in :numref:`sec_word2vec_data`""" + data_dir = d2l.download_extract('ptb') + # Readthetrainingset. + with open(os.path.join(data_dir, 'ptb.train.txt')) as f: + raw_text = f.read() + return [line.split() for line in raw_text.split('\n')] + +def subsample(sentences, vocab): + """下采样高频词 + + Defined in :numref:`sec_word2vec_data`""" + # 排除未知词元'' + sentences = [[token for token in line if vocab[token] != vocab.unk] + for line in sentences] + counter = d2l.count_corpus(sentences) + num_tokens = sum(counter.values()) + + # 如果在下采样期间保留词元,则返回True + def keep(token): + return(random.uniform(0, 1) < + math.sqrt(1e-4 / counter[token] * num_tokens)) + + return ([[token for token in line if keep(token)] for line in sentences], + counter) + +def get_centers_and_contexts(corpus, max_window_size): + """返回跳元模型中的中心词和上下文词 + + Defined in :numref:`sec_word2vec_data`""" + centers, contexts = [], [] + for line in corpus: + # 要形成“中心词-上下文词”对,每个句子至少需要有2个词 + if len(line) < 2: + continue + centers += line + for i in range(len(line)): # 上下文窗口中间i + window_size = random.randint(1, max_window_size) + indices = list(range(max(0, i - window_size), + min(len(line), i + 1 + window_size))) + # 从上下文词中排除中心词 + indices.remove(i) + contexts.append([line[idx] for idx in indices]) + return centers, contexts + +class RandomGenerator: + """根据n个采样权重在{1,...,n}中随机抽取""" + def __init__(self, sampling_weights): + """Defined in :numref:`sec_word2vec_data`""" + # Exclude + self.population = list(range(1, len(sampling_weights) + 1)) + self.sampling_weights = sampling_weights + self.candidates = [] + self.i = 0 + + def draw(self): + if self.i == len(self.candidates): + # 缓存k个随机采样结果 + self.candidates = random.choices( + self.population, self.sampling_weights, k=10000) + self.i = 0 + self.i += 1 + return self.candidates[self.i - 1] + +generator = RandomGenerator([2, 3, 4]) +[generator.draw() for _ in range(10)] + +def get_negatives(all_contexts, vocab, counter, K): + """返回负采样中的噪声词 + + Defined in :numref:`sec_word2vec_data`""" + # 索引为1、2、...(索引0是词表中排除的未知标记) + sampling_weights = [counter[vocab.to_tokens(i)]**0.75 + for i in range(1, len(vocab))] + all_negatives, generator = [], RandomGenerator(sampling_weights) + for contexts in all_contexts: + negatives = [] + while len(negatives) < len(contexts) * K: + neg = generator.draw() + # 噪声词不能是上下文词 + if neg not in contexts: + negatives.append(neg) + all_negatives.append(negatives) + return all_negatives + +def batchify(data): + """返回带有负采样的跳元模型的小批量样本 + + Defined in :numref:`sec_word2vec_data`""" + max_len = max(len(c) + len(n) for _, c, n in data) + centers, contexts_negatives, masks, labels = [], [], [], [] + for center, context, negative in data: + cur_len = len(context) + len(negative) + centers += [center] + contexts_negatives += \ + [context + negative + [0] * (max_len - cur_len)] + masks += [[1] * cur_len + [0] * (max_len - cur_len)] + labels += [[1] * len(context) + [0] * (max_len - len(context))] + return (d2l.reshape(d2l.tensor(centers), (-1, 1)), d2l.tensor( + contexts_negatives), d2l.tensor(masks), d2l.tensor(labels)) + +def load_data_ptb(batch_size, max_window_size, num_noise_words): + """下载PTB数据集,然后将其加载到内存中 + + Defined in :numref:`subsec_word2vec-minibatch-loading`""" + num_workers = d2l.get_dataloader_workers() + sentences = read_ptb() + vocab = d2l.Vocab(sentences, min_freq=10) + subsampled, counter = subsample(sentences, vocab) + corpus = [vocab[line] for line in subsampled] + all_centers, all_contexts = get_centers_and_contexts( + corpus, max_window_size) + all_negatives = get_negatives( + all_contexts, vocab, counter, num_noise_words) + + class PTBDataset(paddle.io.Dataset): + def __init__(self, centers, contexts, negatives): + assert len(centers) == len(contexts) == len(negatives) + self.centers = centers + self.contexts = contexts + self.negatives = negatives + + def __getitem__(self, index): + return (self.centers[index], self.contexts[index], + self.negatives[index]) + + def __len__(self): + return len(self.centers) + + dataset = PTBDataset(all_centers, all_contexts, all_negatives) + + data_iter = paddle.io.DataLoader( + dataset, batch_size=batch_size, shuffle=True, return_list=True, + collate_fn=batchify, num_workers=num_workers) + return data_iter, vocab + +d2l.DATA_HUB['glove.6b.50d'] = (d2l.DATA_URL + 'glove.6B.50d.zip', + '0b8703943ccdb6eb788e6f091b8946e82231bc4d') + +d2l.DATA_HUB['glove.6b.100d'] = (d2l.DATA_URL + 'glove.6B.100d.zip', + 'cd43bfb07e44e6f27cbcc7bc9ae3d80284fdaf5a') + +d2l.DATA_HUB['glove.42b.300d'] = (d2l.DATA_URL + 'glove.42B.300d.zip', + 'b5116e234e9eb9076672cfeabf5469f3eec904fa') + +d2l.DATA_HUB['wiki.en'] = (d2l.DATA_URL + 'wiki.en.zip', + 'c1816da3821ae9f43899be655002f6c723e91b88') + +class TokenEmbedding: + """GloVe嵌入""" + def __init__(self, embedding_name): + """Defined in :numref:`sec_synonyms`""" + self.idx_to_token, self.idx_to_vec = self._load_embedding( + embedding_name) + self.unknown_idx = 0 + self.token_to_idx = {token: idx for idx, token in + enumerate(self.idx_to_token)} + + def _load_embedding(self, embedding_name): + idx_to_token, idx_to_vec = [''], [] + data_dir = d2l.download_extract(embedding_name) + # GloVe网站:https://nlp.stanford.edu/projects/glove/ + # fastText网站:https://fasttext.cc/ + with open(os.path.join(data_dir, 'vec.txt'), 'r') as f: + for line in f: + elems = line.rstrip().split(' ') + token, elems = elems[0], [float(elem) for elem in elems[1:]] + # 跳过标题信息,例如fastText中的首行 + if len(elems) > 1: + idx_to_token.append(token) + idx_to_vec.append(elems) + idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec + return idx_to_token, d2l.tensor(idx_to_vec) + + def __getitem__(self, tokens): + indices = [self.token_to_idx.get(token, self.unknown_idx) + for token in tokens] + vecs = self.idx_to_vec[d2l.tensor(indices)] + return vecs + + def __len__(self): + return len(self.idx_to_token) + +def get_tokens_and_segments(tokens_a, tokens_b=None): + """获取输入序列的词元及其片段索引 + + Defined in :numref:`sec_bert`""" + tokens = [''] + tokens_a + [''] + # 0和1分别标记片段A和B + segments = [0] * (len(tokens_a) + 2) + if tokens_b is not None: + tokens += tokens_b + [''] + segments += [1] * (len(tokens_b) + 1) + return tokens, segments + +class BERTEncoder(nn.Layer): + """BERT编码器 + + Defined in :numref:`subsec_bert_input_rep`""" + def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, + ffn_num_hiddens, num_heads, num_layers, dropout, + max_len=1000, key_size=768, query_size=768, value_size=768, + **kwargs): + super(BERTEncoder, self).__init__(**kwargs) + self.token_embedding = nn.Embedding(vocab_size, num_hiddens) + self.segment_embedding = nn.Embedding(2, num_hiddens) + self.blks = nn.Sequential() + for i in range(num_layers): + self.blks.add_sublayer(f"{i}", d2l.EncoderBlock( + key_size, query_size, value_size, num_hiddens, norm_shape, + ffn_num_input, ffn_num_hiddens, num_heads, dropout, True)) + # 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数 + x = paddle.randn([1, max_len, num_hiddens]) + self.pos_embedding = paddle.create_parameter(shape=x.shape, dtype=str(x.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign(x)) + + def forward(self, tokens, segments, valid_lens): + # 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens) + X = self.token_embedding(tokens) + self.segment_embedding(segments) + X = X + self.pos_embedding[:, :X.shape[1], :] + for blk in self.blks: + X = blk(X, valid_lens) + return X + +class MaskLM(nn.Layer): + """BERT的掩蔽语言模型任务 + + Defined in :numref:`subsec_bert_input_rep`""" + def __init__(self, vocab_size, num_hiddens, num_inputs=768, **kwargs): + super(MaskLM, self).__init__(**kwargs) + self.mlp = nn.Sequential(nn.Linear(num_inputs, num_hiddens), + nn.ReLU(), + nn.LayerNorm(num_hiddens), + nn.Linear(num_hiddens, vocab_size)) + + def forward(self, X, pred_positions): + num_pred_positions = pred_positions.shape[1] + pred_positions = pred_positions.reshape([-1]) + batch_size = X.shape[0] + batch_idx = paddle.arange(0, batch_size) + # 假设batch_size=2,num_pred_positions=3 + # 那么batch_idx是np.array([0,0,0,1,1]) + batch_idx = paddle.repeat_interleave(batch_idx, num_pred_positions) + masked_X = X[batch_idx, pred_positions] + masked_X = masked_X.reshape((batch_size, num_pred_positions, -1)) + mlm_Y_hat = self.mlp(masked_X) + return mlm_Y_hat + +class NextSentencePred(nn.Layer): + """BERT的下一句预测任务 + + Defined in :numref:`subsec_mlm`""" + def __init__(self, num_inputs, **kwargs): + super(NextSentencePred, self).__init__(**kwargs) + self.output = nn.Linear(num_inputs, 2) + + def forward(self, X): + # X的形状:(batchsize,num_hiddens) + return self.output(X) + +class BERTModel(nn.Layer): + """BERT模型 + + Defined in :numref:`subsec_nsp`""" + def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, + ffn_num_hiddens, num_heads, num_layers, dropout, + max_len=1000, key_size=768, query_size=768, value_size=768, + hid_in_features=768, mlm_in_features=768, + nsp_in_features=768): + super(BERTModel, self).__init__() + self.encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape, + ffn_num_input, ffn_num_hiddens, num_heads, num_layers, + dropout, max_len=max_len, key_size=key_size, + query_size=query_size, value_size=value_size) + self.hidden = nn.Sequential(nn.Linear(hid_in_features, num_hiddens), + nn.Tanh()) + self.mlm = MaskLM(vocab_size, num_hiddens, mlm_in_features) + self.nsp = NextSentencePred(nsp_in_features) + + def forward(self, tokens, segments, valid_lens=None, + pred_positions=None): + encoded_X = self.encoder(tokens, segments, valid_lens) + if pred_positions is not None: + mlm_Y_hat = self.mlm(encoded_X, pred_positions) + else: + mlm_Y_hat = None + # 用于下一句预测的多层感知机分类器的隐藏层,0是“”标记的索引 + nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :])) + return encoded_X, mlm_Y_hat, nsp_Y_hat + +d2l.DATA_HUB['wikitext-2'] = ( + 'https://s3.amazonaws.com/research.metamind.io/wikitext/' + 'wikitext-2-v1.zip', '3c914d17d80b1459be871a5039ac23e752a53cbe') + +def _read_wiki(data_dir): + """Defined in :numref:`sec_bert-dataset`""" + file_name = os.path.join(data_dir, 'wiki.train.tokens') + with open(file_name, 'r') as f: + lines = f.readlines() + # 大写字母转换为小写字母 + paragraphs = [line.strip().lower().split(' . ') + for line in lines if len(line.split(' . ')) >= 2] + random.shuffle(paragraphs) + return paragraphs + +def _get_next_sentence(sentence, next_sentence, paragraphs): + """Defined in :numref:`sec_bert-dataset`""" + if random.random() < 0.5: + is_next = True + else: + # paragraphs是三重列表的嵌套 + next_sentence = random.choice(random.choice(paragraphs)) + is_next = False + return sentence, next_sentence, is_next + +def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len): + """Defined in :numref:`sec_bert-dataset`""" + nsp_data_from_paragraph = [] + for i in range(len(paragraph) - 1): + tokens_a, tokens_b, is_next = _get_next_sentence( + paragraph[i], paragraph[i + 1], paragraphs) + # 考虑1个''词元和2个''词元 + if len(tokens_a) + len(tokens_b) + 3 > max_len: + continue + tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b) + nsp_data_from_paragraph.append((tokens, segments, is_next)) + return nsp_data_from_paragraph + +def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds, + vocab): + """Defined in :numref:`sec_bert-dataset`""" + # 为遮蔽语言模型的输入创建新的词元副本,其中输入可能包含替换的“”或随机词元 + mlm_input_tokens = [token for token in tokens] + pred_positions_and_labels = [] + # 打乱后用于在遮蔽语言模型任务中获取15%的随机词元进行预测 + random.shuffle(candidate_pred_positions) + for mlm_pred_position in candidate_pred_positions: + if len(pred_positions_and_labels) >= num_mlm_preds: + break + masked_token = None + # 80%的时间:将词替换为“”词元 + if random.random() < 0.8: + masked_token = '' + else: + # 10%的时间:保持词不变 + if random.random() < 0.5: + masked_token = tokens[mlm_pred_position] + # 10%的时间:用随机词替换该词 + else: + masked_token = random.choice(vocab.idx_to_token) + mlm_input_tokens[mlm_pred_position] = masked_token + pred_positions_and_labels.append( + (mlm_pred_position, tokens[mlm_pred_position])) + return mlm_input_tokens, pred_positions_and_labels + +def _get_mlm_data_from_tokens(tokens, vocab): + """Defined in :numref:`subsec_prepare_mlm_data`""" + candidate_pred_positions = [] + # tokens是一个字符串列表 + for i, token in enumerate(tokens): + # 在遮蔽语言模型任务中不会预测特殊词元 + if token in ['', '']: + continue + candidate_pred_positions.append(i) + # 遮蔽语言模型任务中预测15%的随机词元 + num_mlm_preds = max(1, round(len(tokens) * 0.15)) + mlm_input_tokens, pred_positions_and_labels = _replace_mlm_tokens( + tokens, candidate_pred_positions, num_mlm_preds, vocab) + pred_positions_and_labels = sorted(pred_positions_and_labels, + key=lambda x: x[0]) + pred_positions = [v[0] for v in pred_positions_and_labels] + mlm_pred_labels = [v[1] for v in pred_positions_and_labels] + return vocab[mlm_input_tokens], pred_positions, vocab[mlm_pred_labels] + +def _pad_bert_inputs(examples, max_len, vocab): + """Defined in :numref:`subsec_prepare_mlm_data`""" + max_num_mlm_preds = round(max_len * 0.15) + all_token_ids, all_segments, valid_lens, = [], [], [] + all_pred_positions, all_mlm_weights, all_mlm_labels = [], [], [] + nsp_labels = [] + for (token_ids, pred_positions, mlm_pred_label_ids, segments, + is_next) in examples: + all_token_ids.append(paddle.to_tensor(token_ids + [vocab['']] * ( + max_len - len(token_ids)), dtype=paddle.int64)) + all_segments.append(paddle.to_tensor(segments + [0] * ( + max_len - len(segments)), dtype=paddle.int64)) + # valid_lens不包括''的计数 + valid_lens.append(paddle.to_tensor(len(token_ids), dtype=paddle.float32)) + all_pred_positions.append(paddle.to_tensor(pred_positions + [0] * ( + max_num_mlm_preds - len(pred_positions)), dtype=paddle.int64)) + # 填充词元的预测将通过乘以0权重在损失中过滤掉 + all_mlm_weights.append( + paddle.to_tensor([1.0] * len(mlm_pred_label_ids) + [0.0] * ( + max_num_mlm_preds - len(pred_positions)), + dtype=paddle.float32)) + all_mlm_labels.append(paddle.to_tensor(mlm_pred_label_ids + [0] * ( + max_num_mlm_preds - len(mlm_pred_label_ids)), dtype=paddle.int64)) + nsp_labels.append(paddle.to_tensor(is_next, dtype=paddle.int64)) + return (all_token_ids, all_segments, valid_lens, all_pred_positions, + all_mlm_weights, all_mlm_labels, nsp_labels) + +class _WikiTextDataset(paddle.io.Dataset): + """Defined in :numref:`subsec_prepare_mlm_data`""" + def __init__(self, paragraphs, max_len): + # 输入paragraphs[i]是代表段落的句子字符串列表; + # 而输出paragraphs[i]是代表段落的句子列表,其中每个句子都是词元列表 + paragraphs = [d2l.tokenize( + paragraph, token='word') for paragraph in paragraphs] + sentences = [sentence for paragraph in paragraphs + for sentence in paragraph] + self.vocab = d2l.Vocab(sentences, min_freq=5, reserved_tokens=[ + '', '', '', '']) + # 获取下一句子预测任务的数据 + examples = [] + for paragraph in paragraphs: + examples.extend(_get_nsp_data_from_paragraph( + paragraph, paragraphs, self.vocab, max_len)) + # 获取遮蔽语言模型任务的数据 + examples = [(_get_mlm_data_from_tokens(tokens, self.vocab) + + (segments, is_next)) + for tokens, segments, is_next in examples] + # 填充输入 + (self.all_token_ids, self.all_segments, self.valid_lens, + self.all_pred_positions, self.all_mlm_weights, + self.all_mlm_labels, self.nsp_labels) = _pad_bert_inputs( + examples, max_len, self.vocab) + + def __getitem__(self, idx): + return (self.all_token_ids[idx], self.all_segments[idx], + self.valid_lens[idx], self.all_pred_positions[idx], + self.all_mlm_weights[idx], self.all_mlm_labels[idx], + self.nsp_labels[idx]) + + def __len__(self): + return len(self.all_token_ids) + +def load_data_wiki(batch_size, max_len): + """加载WikiText-2数据集 + + Defined in :numref:`subsec_prepare_mlm_data`""" + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('wikitext-2', 'wikitext-2') + paragraphs = _read_wiki(data_dir) + train_set = _WikiTextDataset(paragraphs, max_len) + train_iter = paddle.io.DataLoader(dataset=train_set, batch_size=batch_size, return_list=True, + shuffle=True, num_workers=num_workers) + return train_iter, train_set.vocab + +def _get_batch_loss_bert(net, loss, vocab_size, tokens_X, + segments_X, valid_lens_x, + pred_positions_X, mlm_weights_X, + mlm_Y, nsp_y): + """Defined in :numref:`sec_bert-pretraining`""" + # 前向传播 + _, mlm_Y_hat, nsp_Y_hat = net(tokens_X, segments_X, + valid_lens_x.reshape([-1]), + pred_positions_X) + # 计算遮蔽语言模型损失 + mlm_l = loss(mlm_Y_hat.reshape([-1, vocab_size]), mlm_Y.reshape([-1])) *\ + mlm_weights_X.reshape([-1, 1]) + mlm_l = mlm_l.sum() / (mlm_weights_X.sum() + 1e-8) + # 计算下一句子预测任务的损失 + nsp_l = loss(nsp_Y_hat, nsp_y) + l = mlm_l + nsp_l + return mlm_l, nsp_l, l + +d2l.DATA_HUB['aclImdb'] = ( + 'http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz', + '01ada507287d82875905620988597833ad4e0903') + +def read_imdb(data_dir, is_train): + """读取IMDb评论数据集文本序列和标签 + + Defined in :numref:`sec_sentiment`""" + data, labels = [], [] + for label in ('pos', 'neg'): + folder_name = os.path.join(data_dir, 'train' if is_train else 'test', + label) + for file in os.listdir(folder_name): + with open(os.path.join(folder_name, file), 'rb') as f: + review = f.read().decode('utf-8').replace('\n', '') + data.append(review) + labels.append(1 if label == 'pos' else 0) + return data, labels + +def load_data_imdb(batch_size, num_steps=500): + """返回数据迭代器和IMDb评论数据集的词表 + + Defined in :numref:`sec_sentiment`""" + data_dir = d2l.download_extract('aclImdb', 'aclImdb') + train_data = read_imdb(data_dir, True) + test_data = read_imdb(data_dir, False) + train_tokens = d2l.tokenize(train_data[0], token='word') + test_tokens = d2l.tokenize(test_data[0], token='word') + vocab = d2l.Vocab(train_tokens, min_freq=5) + train_features = d2l.tensor([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in train_tokens]) + test_features = d2l.tensor([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in test_tokens]) + train_iter = d2l.load_array((train_features, d2l.tensor(train_data[1])), + batch_size) + test_iter = d2l.load_array((test_features, d2l.tensor(test_data[1])), + batch_size, + is_train=False) + return train_iter, test_iter, vocab + +def predict_sentiment(net, vocab, sequence): + """预测文本序列的情感 + + Defined in :numref:`sec_sentiment_rnn`""" + sequence = paddle.to_tensor(vocab[sequence.split()], place=d2l.try_gpu()) + label = paddle.argmax(net(sequence.reshape((1, -1))), axis=1) + return 'positive' if label == 1 else 'negative' + +d2l.DATA_HUB['SNLI'] = ( + 'https://nlp.stanford.edu/projects/snli/snli_1.0.zip', + '9fcde07509c7e87ec61c640c1b2753d9041758e4') + +def read_snli(data_dir, is_train): + """将SNLI数据集解析为前提、假设和标签 + + Defined in :numref:`sec_natural-language-inference-and-dataset`""" + def extract_text(s): + # 删除我们不会使用的信息 + s = re.sub('\\(', '', s) + s = re.sub('\\)', '', s) + # 用一个空格替换两个或多个连续的空格 + s = re.sub('\\s{2,}', ' ', s) + return s.strip() + label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2} + file_name = os.path.join(data_dir, 'snli_1.0_train.txt' + if is_train else 'snli_1.0_test.txt') + with open(file_name, 'r') as f: + rows = [row.split('\t') for row in f.readlines()[1:]] + premises = [extract_text(row[1]) for row in rows if row[0] in label_set] + hypotheses = [extract_text(row[2]) for row in rows if row[0] \ + in label_set] + labels = [label_set[row[0]] for row in rows if row[0] in label_set] + return premises, hypotheses, labels + +class SNLIDataset(paddle.io.Dataset): + """用于加载SNLI数据集的自定义数据集 + + Defined in :numref:`sec_natural-language-inference-and-dataset`""" + def __init__(self, dataset, num_steps, vocab=None): + self.num_steps = num_steps + all_premise_tokens = d2l.tokenize(dataset[0]) + all_hypothesis_tokens = d2l.tokenize(dataset[1]) + if vocab is None: + self.vocab = d2l.Vocab(all_premise_tokens + \ + all_hypothesis_tokens, min_freq=5, reserved_tokens=['']) + else: + self.vocab = vocab + self.premises = self._pad(all_premise_tokens) + self.hypotheses = self._pad(all_hypothesis_tokens) + self.labels = paddle.to_tensor(dataset[2]) + print('read ' + str(len(self.premises)) + ' examples') + + def _pad(self, lines): + return paddle.to_tensor([d2l.truncate_pad( + self.vocab[line], self.num_steps, self.vocab['']) + for line in lines]) + + def __getitem__(self, idx): + return (self.premises[idx], self.hypotheses[idx]), self.labels[idx] + + def __len__(self): + return len(self.premises) + +def load_data_snli(batch_size, num_steps=50): + """下载SNLI数据集并返回数据迭代器和词表 + + Defined in :numref:`sec_natural-language-inference-and-dataset`""" + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('SNLI') + train_data = read_snli(data_dir, True) + test_data = read_snli(data_dir, False) + train_set = SNLIDataset(train_data, num_steps) + test_set = SNLIDataset(test_data, num_steps, train_set.vocab) + train_iter = paddle.io.DataLoader(train_set,batch_size=batch_size, + shuffle=True, + num_workers=num_workers, + return_list=True) + + test_iter = paddle.io.DataLoader(test_set, batch_size=batch_size, + shuffle=False, + num_workers=num_workers, + return_list=True) + return train_iter, test_iter, train_set.vocab + +def predict_snli(net, vocab, premise, hypothesis): + """预测前提和假设之间的逻辑关系 + + Defined in :numref:`sec_natural-language-inference-attention`""" + net.eval() + premise = paddle.to_tensor(vocab[premise], place=d2l.try_gpu()) + hypothesis = paddle.to_tensor(vocab[hypothesis], place=d2l.try_gpu()) + label = paddle.argmax(net([premise.reshape((1, -1)), + hypothesis.reshape((1, -1))]), axis=1) + + return 'entailment' if label == 0 else 'contradiction' if label == 1 \ + else 'neutral' + + +# Alias defined in config.ini +nn_Module = nn.Layer + +ones = paddle.ones +zeros = paddle.zeros +tensor = paddle.to_tensor +arange = paddle.arange +meshgrid = paddle.meshgrid +sin = paddle.sin +sinh = paddle.sinh +cos = paddle.cos +cosh = paddle.cosh +tanh = paddle.tanh +linspace = paddle.linspace +exp = paddle.exp +log = paddle.log +normal = paddle.normal +rand = paddle.rand +randn = paddle.randn +matmul = paddle.matmul +int32 = paddle.int32 +float32 = paddle.float32 +concat = paddle.concat +stack = paddle.stack +abs = paddle.abs +eye = paddle.eye +numpy = lambda x, *args, **kwargs: x.detach().numpy(*args, **kwargs) +size = lambda x, *args, **kwargs: x.numel(*args, **kwargs) +reshape = lambda x, *args, **kwargs: x.reshape(*args, **kwargs) +to = lambda x, *args, **kwargs: x.to(*args, **kwargs) +reduce_sum = lambda x, *args, **kwargs: x.sum(*args, **kwargs) +argmax = lambda x, *args, **kwargs: x.argmax(*args, **kwargs) +astype = lambda x, *args, **kwargs: x.astype(*args, **kwargs) +transpose = lambda x, *args, **kwargs: x.t(*args, **kwargs) +reduce_mean = lambda x, *args, **kwargs: x.mean(*args, **kwargs) + diff --git a/d2l/tensorflow.py b/d2l/tensorflow.py index 9735d43c0..9e40a2aa6 100644 --- a/d2l/tensorflow.py +++ b/d2l/tensorflow.py @@ -1,3 +1,11 @@ +DATA_HUB = dict() +DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/' + +import numpy as np +import tensorflow as tf + +nn_Module = tf.keras.Model + ################# WARNING ################ # The below part is generated automatically through: # d2lbook build lib @@ -27,20 +35,20 @@ import tensorflow as tf def use_svg_display(): - """使用svg格式在Jupyter中显示绘图 + """Use the svg format to display a plot in Jupyter. Defined in :numref:`sec_calculus`""" backend_inline.set_matplotlib_formats('svg') def set_figsize(figsize=(3.5, 2.5)): - """设置matplotlib的图表大小 + """Set the figure size for matplotlib. Defined in :numref:`sec_calculus`""" use_svg_display() d2l.plt.rcParams['figure.figsize'] = figsize def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): - """设置matplotlib的轴 + """Set the axes for matplotlib. Defined in :numref:`sec_calculus`""" axes.set_xlabel(xlabel) @@ -56,7 +64,7 @@ def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None): - """绘制数据点 + """Plot data points. Defined in :numref:`sec_calculus`""" if legend is None: @@ -65,7 +73,7 @@ def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # 如果X有一个轴,输出True + # Return True if `X` (tensor or list) has 1 axis def has_one_axis(X): return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) and not hasattr(X[0], "__len__")) @@ -87,35 +95,35 @@ def has_one_axis(X): set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend) class Timer: - """记录多次运行时间""" + """Record multiple running times.""" def __init__(self): """Defined in :numref:`subsec_linear_model`""" self.times = [] self.start() def start(self): - """启动计时器""" + """Start the timer.""" self.tik = time.time() def stop(self): - """停止计时器并将时间记录在列表中""" + """Stop the timer and record the time in a list.""" self.times.append(time.time() - self.tik) return self.times[-1] def avg(self): - """返回平均时间""" + """Return the average time.""" return sum(self.times) / len(self.times) def sum(self): - """返回时间总和""" + """Return the sum of time.""" return sum(self.times) def cumsum(self): - """返回累计时间""" + """Return the accumulated time.""" return np.array(self.times).cumsum().tolist() def synthetic_data(w, b, num_examples): - """生成y=Xw+b+噪声 + """Generate y = Xw + b + noise. Defined in :numref:`sec_linear_scratch`""" X = d2l.zeros((num_examples, w.shape[0])) @@ -126,26 +134,26 @@ def synthetic_data(w, b, num_examples): return X, y def linreg(X, w, b): - """线性回归模型 + """The linear regression model. Defined in :numref:`sec_linear_scratch`""" return d2l.matmul(X, w) + b def squared_loss(y_hat, y): - """均方损失 + """Squared loss. Defined in :numref:`sec_linear_scratch`""" return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 def sgd(params, grads, lr, batch_size): - """小批量随机梯度下降 + """Minibatch stochastic gradient descent. Defined in :numref:`sec_linear_scratch`""" for param, grad in zip(params, grads): param.assign_sub(lr*grad/batch_size) def load_array(data_arrays, batch_size, is_train=True): - """构造一个TensorFlow数据迭代器 + """Construct a TensorFlow data iterator. Defined in :numref:`sec_linear_concise`""" dataset = tf.data.Dataset.from_tensor_slices(data_arrays) @@ -155,7 +163,7 @@ def load_array(data_arrays, batch_size, is_train=True): return dataset def get_fashion_mnist_labels(labels): - """返回Fashion-MNIST数据集的文本标签 + """Return text labels for the Fashion-MNIST dataset. Defined in :numref:`sec_fashion_mnist`""" text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', @@ -163,7 +171,7 @@ def get_fashion_mnist_labels(labels): return [text_labels[int(i)] for i in labels] def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): - """绘制图像列表 + """Plot a list of images. Defined in :numref:`sec_fashion_mnist`""" figsize = (num_cols * scale, num_rows * scale) @@ -178,12 +186,12 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): return axes def load_data_fashion_mnist(batch_size, resize=None): - """下载Fashion-MNIST数据集,然后将其加载到内存中 + """Download the Fashion-MNIST dataset and then load it into memory. Defined in :numref:`sec_fashion_mnist`""" mnist_train, mnist_test = tf.keras.datasets.fashion_mnist.load_data() - # 将所有数字除以255,使所有像素值介于0和1之间,在最后添加一个批处理维度, - # 并将标签转换为int32。 + # Divide all numbers by 255 so that all pixel values are between + # 0 and 1, add a batch dimension at the last. And cast label to int32 process = lambda X, y: (tf.expand_dims(X, axis=3) / 255, tf.cast(y, dtype='int32')) resize_fn = lambda X, y: ( @@ -195,7 +203,7 @@ def load_data_fashion_mnist(batch_size, resize=None): batch_size).map(resize_fn)) def accuracy(y_hat, y): - """计算预测正确的数量 + """Compute the number of correct predictions. Defined in :numref:`sec_softmax_scratch`""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: @@ -204,16 +212,16 @@ def accuracy(y_hat, y): return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) def evaluate_accuracy(net, data_iter): - """计算在指定数据集上模型的精度 + """Compute the accuracy for a model on a dataset. Defined in :numref:`sec_softmax_scratch`""" - metric = Accumulator(2) # 正确预测数、预测总数 + metric = Accumulator(2) # No. of correct predictions, no. of predictions for X, y in data_iter: metric.add(accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] class Accumulator: - """在n个变量上累加""" + """For accumulating sums over `n` variables.""" def __init__(self, n): """Defined in :numref:`sec_softmax_scratch`""" self.data = [0.0] * n @@ -228,17 +236,18 @@ def __getitem__(self, idx): return self.data[idx] def train_epoch_ch3(net, train_iter, loss, updater): - """训练模型一个迭代周期(定义见第3章) + """The training loop defined in Chapter 3. Defined in :numref:`sec_softmax_scratch`""" - # 训练损失总和、训练准确度总和、样本数 + # Sum of training loss, sum of training accuracy, no. of examples metric = Accumulator(3) for X, y in train_iter: - # 计算梯度并更新参数 + # Compute gradients and update parameters with tf.GradientTape() as tape: y_hat = net(X) - # Keras内置的损失接受的是(标签,预测),这不同于用户在本书中的实现。 - # 本书的实现接受(预测,标签),例如我们上面实现的“交叉熵” + # Keras implementations for loss takes (labels, predictions) + # instead of (predictions, labels) that users might implement + # in this book, e.g. `cross_entropy` that we implemented above if isinstance(loss, tf.keras.losses.Loss): l = loss(y, y_hat) else: @@ -249,34 +258,34 @@ def train_epoch_ch3(net, train_iter, loss, updater): updater.apply_gradients(zip(grads, params)) else: updater(X.shape[0], tape.gradient(l, updater.params)) - # Keras的loss默认返回一个批量的平均损失 + # Keras loss by default returns the average loss in a batch l_sum = l * float(tf.size(y)) if isinstance( loss, tf.keras.losses.Loss) else tf.reduce_sum(l) metric.add(l_sum, accuracy(y_hat, y), tf.size(y)) - # 返回训练损失和训练精度 + # Return training loss and training accuracy return metric[0] / metric[2], metric[1] / metric[2] class Animator: - """在动画中绘制数据""" + """For plotting data in animation.""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): """Defined in :numref:`sec_softmax_scratch`""" - # 增量地绘制多条线 + # Incrementally plot multiple lines if legend is None: legend = [] d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: self.axes = [self.axes, ] - # 使用lambda函数捕获参数 + # Use a lambda function to capture arguments self.config_axes = lambda: d2l.set_axes( self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): - # 向图表中添加多个数据点 + # Add multiple data points into the figure if not hasattr(y, "__len__"): y = [y] n = len(y) @@ -298,7 +307,7 @@ def add(self, x, y): display.clear_output(wait=True) def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): - """训练模型(定义见第3章) + """Train a model (defined in Chapter 3). Defined in :numref:`sec_softmax_scratch`""" animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], @@ -313,7 +322,7 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): assert test_acc <= 1 and test_acc > 0.7, test_acc class Updater(): - """用小批量随机梯度下降法更新参数 + """For updating parameters using minibatch stochastic gradient descent. Defined in :numref:`sec_softmax_scratch`""" def __init__(self, params, lr): @@ -324,7 +333,7 @@ def __call__(self, batch_size, grads): d2l.sgd(self.params, grads, self.lr, batch_size) def predict_ch3(net, test_iter, n=6): - """预测标签(定义见第3章) + """Predict labels (defined in Chapter 3). Defined in :numref:`sec_softmax_scratch`""" for X, y in test_iter: @@ -336,10 +345,10 @@ def predict_ch3(net, test_iter, n=6): d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n]) def evaluate_loss(net, data_iter, loss): - """评估给定数据集上模型的损失 + """Evaluate the loss of a model on the given dataset. Defined in :numref:`sec_model_selection`""" - metric = d2l.Accumulator(2) # 损失的总和,样本数量 + metric = d2l.Accumulator(2) # Sum of losses, no. of examples for X, y in data_iter: l = loss(net(X), y) metric.add(d2l.reduce_sum(l), d2l.size(l)) @@ -349,10 +358,10 @@ def evaluate_loss(net, data_iter, loss): DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/' def download(name, cache_dir=os.path.join('..', 'data')): - """下载一个DATA_HUB中的文件,返回本地文件名 + """Download a file inserted into DATA_HUB, return the local filename. Defined in :numref:`sec_kaggle_house`""" - assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}" + assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}." url, sha1_hash = DATA_HUB[name] os.makedirs(cache_dir, exist_ok=True) fname = os.path.join(cache_dir, url.split('/')[-1]) @@ -365,15 +374,15 @@ def download(name, cache_dir=os.path.join('..', 'data')): break sha1.update(data) if sha1.hexdigest() == sha1_hash: - return fname # 命中缓存 - print(f'正在从{url}下载{fname}...') + return fname # Hit cache + print(f'Downloading {fname} from {url}...') r = requests.get(url, stream=True, verify=True) with open(fname, 'wb') as f: f.write(r.content) return fname def download_extract(name, folder=None): - """下载并解压zip/tar文件 + """Download and extract a zip/tar file. Defined in :numref:`sec_kaggle_house`""" fname = download(name) @@ -384,12 +393,12 @@ def download_extract(name, folder=None): elif ext in ('.tar', '.gz'): fp = tarfile.open(fname, 'r') else: - assert False, '只有zip/tar文件可以被解压缩' + assert False, 'Only zip/tar files can be extracted.' fp.extractall(base_dir) return os.path.join(base_dir, folder) if folder else data_dir def download_all(): - """下载DATA_HUB中的所有文件 + """Download all files in the DATA_HUB. Defined in :numref:`sec_kaggle_house`""" for name in DATA_HUB: @@ -404,7 +413,7 @@ def download_all(): 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') def try_gpu(i=0): - """如果存在,则返回gpu(i),否则返回cpu() + """Return gpu(i) if exists, otherwise return cpu(). Defined in :numref:`sec_use_gpu`""" if len(tf.config.experimental.list_physical_devices('GPU')) >= i + 1: @@ -412,7 +421,7 @@ def try_gpu(i=0): return tf.device('/CPU:0') def try_all_gpus(): - """返回所有可用的GPU,如果没有GPU,则返回[cpu(),] + """Return all available GPUs, or [cpu(),] if no GPU exists. Defined in :numref:`sec_use_gpu`""" num_gpus = len(tf.config.experimental.list_physical_devices('GPU')) @@ -420,7 +429,7 @@ def try_all_gpus(): return devices if devices else [tf.device('/CPU:0')] def corr2d(X, K): - """计算二维互相关运算""" + """Compute 2D cross-correlation.""" h, w = K.shape Y = tf.Variable(tf.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))) for i in range(Y.shape[0]): @@ -430,7 +439,7 @@ def corr2d(X, K): return Y class TrainCallback(tf.keras.callbacks.Callback): - """一个以可视化的训练进展的回调 + """A callback to visiualize the training progress. Defined in :numref:`sec_lenet`""" def __init__(self, net, train_iter, test_iter, num_epochs, device_name): @@ -443,10 +452,8 @@ def __init__(self, net, train_iter, test_iter, num_epochs, device_name): self.test_iter = test_iter self.num_epochs = num_epochs self.device_name = device_name - def on_epoch_begin(self, epoch, logs=None): self.timer.start() - def on_epoch_end(self, epoch, logs): self.timer.stop() test_acc = self.net.evaluate( @@ -463,7 +470,7 @@ def on_epoch_end(self, epoch, logs): f'{str(self.device_name)}') def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): - """用GPU训练模型(在第六章定义) + """Train a model with a GPU (defined in Chapter 6). Defined in :numref:`sec_lenet`""" device_name = device._device_name @@ -479,6 +486,7 @@ def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): return net class Residual(tf.keras.Model): + """The Residual block of ResNet.""" def __init__(self, num_channels, use_1x1conv=False, strides=1): super().__init__() self.conv1 = tf.keras.layers.Conv2D( @@ -504,7 +512,7 @@ def call(self, X): '090b5e7e70c295757f55df93cb0a180b9691891a') def read_time_machine(): - """将时间机器数据集加载到文本行的列表中 + """Load the time machine dataset into a list of text lines. Defined in :numref:`sec_text_preprocessing`""" with open(d2l.download('time_machine'), 'r') as f: @@ -512,7 +520,7 @@ def read_time_machine(): return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines] def tokenize(lines, token='word'): - """将文本行拆分为单词或字符词元 + """Split text lines into word or character tokens. Defined in :numref:`sec_text_preprocessing`""" if token == 'word': @@ -520,21 +528,21 @@ def tokenize(lines, token='word'): elif token == 'char': return [list(line) for line in lines] else: - print('错误:未知词元类型:' + token) + print('ERROR: unknown token type: ' + token) class Vocab: - """文本词表""" + """Vocabulary for text.""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): """Defined in :numref:`sec_text_preprocessing`""" if tokens is None: tokens = [] if reserved_tokens is None: reserved_tokens = [] - # 按出现频率排序 + # Sort according to frequencies counter = count_corpus(tokens) self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) - # 未知词元的索引为0 + # The index for the unknown token is 0 self.idx_to_token = [''] + reserved_tokens self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)} @@ -559,68 +567,71 @@ def to_tokens(self, indices): return [self.idx_to_token[index] for index in indices] @property - def unk(self): # 未知词元的索引为0 + def unk(self): # Index for the unknown token return 0 @property - def token_freqs(self): + def token_freqs(self): # Index for the unknown token return self._token_freqs def count_corpus(tokens): - """统计词元的频率 + """Count token frequencies. Defined in :numref:`sec_text_preprocessing`""" - # 这里的tokens是1D列表或2D列表 + # Here `tokens` is a 1D list or 2D list if len(tokens) == 0 or isinstance(tokens[0], list): - # 将词元列表展平成一个列表 + # Flatten a list of token lists into a list of tokens tokens = [token for line in tokens for token in line] return collections.Counter(tokens) def load_corpus_time_machine(max_tokens=-1): - """返回时光机器数据集的词元索引列表和词表 + """Return token indices and the vocabulary of the time machine dataset. Defined in :numref:`sec_text_preprocessing`""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落, - # 所以将所有文本行展平到一个列表中 + # Since each text line in the time machine dataset is not necessarily a + # sentence or a paragraph, flatten all the text lines into a single list corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] return corpus, vocab def seq_data_iter_random(corpus, batch_size, num_steps): - """使用随机抽样生成一个小批量子序列 + """Generate a minibatch of subsequences using random sampling. Defined in :numref:`sec_language_model`""" - # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1 + # Start with a random offset (inclusive of `num_steps - 1`) to partition a + # sequence corpus = corpus[random.randint(0, num_steps - 1):] - # 减去1,是因为我们需要考虑标签 + # Subtract 1 since we need to account for labels num_subseqs = (len(corpus) - 1) // num_steps - # 长度为num_steps的子序列的起始索引 + # The starting indices for subsequences of length `num_steps` initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # 在随机抽样的迭代过程中, - # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻 + # In random sampling, the subsequences from two adjacent random + # minibatches during iteration are not necessarily adjacent on the + # original sequence random.shuffle(initial_indices) def data(pos): - # 返回从pos位置开始的长度为num_steps的序列 + # Return a sequence of length `num_steps` starting from `pos` return corpus[pos: pos + num_steps] num_batches = num_subseqs // batch_size for i in range(0, batch_size * num_batches, batch_size): - # 在这里,initial_indices包含子序列的随机起始索引 + # Here, `initial_indices` contains randomized starting indices for + # subsequences initial_indices_per_batch = initial_indices[i: i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] yield d2l.tensor(X), d2l.tensor(Y) def seq_data_iter_sequential(corpus, batch_size, num_steps): - """使用顺序分区生成一个小批量子序列 + """Generate a minibatch of subsequences using sequential partitioning. Defined in :numref:`sec_language_model`""" - # 从随机偏移量开始划分序列 + # Start with a random offset to partition a sequence offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size Xs = d2l.tensor(corpus[offset: offset + num_tokens]) @@ -634,7 +645,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): yield X, Y class SeqDataLoader: - """加载序列数据的迭代器""" + """An iterator to load sequence data.""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): """Defined in :numref:`sec_language_model`""" if use_random_iter: @@ -649,7 +660,7 @@ def __iter__(self): def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): - """返回时光机器数据集的迭代器和词表 + """Return the iterator and the vocabulary of the time machine dataset. Defined in :numref:`sec_language_model`""" data_iter = SeqDataLoader( @@ -657,7 +668,7 @@ def load_data_time_machine(batch_size, num_steps, return data_iter, data_iter.vocab class RNNModelScratch: - """从零开始实现的循环神经网络模型""" + """A RNN Model implemented from scratch.""" def __init__(self, vocab_size, num_hiddens, init_state, forward_fn, get_params): """Defined in :numref:`sec_rnn_scratch`""" @@ -674,23 +685,22 @@ def begin_state(self, batch_size, *args, **kwargs): return self.init_state(batch_size, self.num_hiddens) def predict_ch8(prefix, num_preds, net, vocab): - """在prefix后面生成新字符 + """Generate new characters following the `prefix`. Defined in :numref:`sec_rnn_scratch`""" state = net.begin_state(batch_size=1, dtype=tf.float32) outputs = [vocab[prefix[0]]] - get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]]), - (1, 1)).numpy() - for y in prefix[1:]: # 预热期 + get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]]), (1, 1)).numpy() + for y in prefix[1:]: # Warm-up period _, state = net(get_input(), state) outputs.append(vocab[y]) - for _ in range(num_preds): # 预测num_preds步 + for _ in range(num_preds): # Predict `num_preds` steps y, state = net(get_input(), state) outputs.append(int(y.numpy().argmax(axis=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) def grad_clipping(grads, theta): - """裁剪梯度 + """Clip the gradient. Defined in :numref:`sec_rnn_scratch`""" theta = tf.constant(theta, dtype=tf.float32) @@ -711,14 +721,15 @@ def grad_clipping(grads, theta): return new_grad def train_epoch_ch8(net, train_iter, loss, updater, use_random_iter): - """训练模型一个迭代周期(定义见第8章) + """Train a model within one epoch (defined in Chapter 8). Defined in :numref:`sec_rnn_scratch`""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失之和,词元数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for X, Y in train_iter: if state is None or use_random_iter: - # 在第一次迭代或使用随机抽样时初始化state + # Initialize `state` when either it is the first iteration or + # using random sampling state = net.begin_state(batch_size=X.shape[0], dtype=tf.float32) with tf.GradientTape(persistent=True) as g: y_hat, state = net(X, state) @@ -728,23 +739,25 @@ def train_epoch_ch8(net, train_iter, loss, updater, use_random_iter): grads = g.gradient(l, params) grads = grad_clipping(grads, 1) updater.apply_gradients(zip(grads, params)) - # Keras默认返回一个批量中的平均损失 + + # Keras loss by default returns the average loss in a batch + # l_sum = l * float(d2l.size(y)) if isinstance( + # loss, tf.keras.losses.Loss) else tf.reduce_sum(l) metric.add(l * d2l.size(y), d2l.size(y)) return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() def train_ch8(net, train_iter, vocab, lr, num_epochs, strategy, use_random_iter=False): - """训练模型(定义见第8章) + """Train a model (defined in Chapter 8). Defined in :numref:`sec_rnn_scratch`""" with strategy.scope(): - loss = tf.keras.losses.SparseCategoricalCrossentropy( - from_logits=True) + loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) updater = tf.keras.optimizers.SGD(lr) animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) predict = lambda prefix: predict_ch8(prefix, 50, net, vocab) - # 训练和预测 + # Train and predict for epoch in range(num_epochs): ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, use_random_iter) @@ -752,7 +765,7 @@ def train_ch8(net, train_iter, vocab, lr, num_epochs, strategy, print(predict('time traveller')) animator.add(epoch + 1, [ppl]) device = d2l.try_gpu()._device_name - print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}') + print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') print(predict('time traveller')) print(predict('traveller')) @@ -766,7 +779,7 @@ def __init__(self, rnn_layer, vocab_size, **kwargs): def call(self, inputs, state): X = tf.one_hot(tf.transpose(inputs), self.vocab_size) - # rnn返回两个以上的值 + # Later RNN like `tf.keras.layers.LSTMCell` return more than two values Y, *state = self.rnn(X, state) output = self.dense(tf.reshape(Y, (-1, Y.shape[-1]))) return output, state @@ -778,31 +791,30 @@ def begin_state(self, *args, **kwargs): '94646ad1522d915e7b0f9296181140edcf86a4f5') def read_data_nmt(): - """载入“英语-法语”数据集 + """Load the English-French dataset. Defined in :numref:`sec_machine_translation`""" data_dir = d2l.download_extract('fra-eng') - with open(os.path.join(data_dir, 'fra.txt'), 'r', - encoding='utf-8') as f: + with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: return f.read() def preprocess_nmt(text): - """预处理“英语-法语”数据集 + """Preprocess the English-French dataset. Defined in :numref:`sec_machine_translation`""" def no_space(char, prev_char): return char in set(',.!?') and prev_char != ' ' - # 使用空格替换不间断空格 - # 使用小写字母替换大写字母 + # Replace non-breaking space with space, and convert uppercase letters to + # lowercase ones text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() - # 在单词和标点符号之间插入空格 + # Insert space between words and punctuation marks out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char for i, char in enumerate(text)] return ''.join(out) def tokenize_nmt(text, num_examples=None): - """词元化“英语-法语”数据数据集 + """Tokenize the English-French dataset. Defined in :numref:`sec_machine_translation`""" source, target = [], [] @@ -815,16 +827,29 @@ def tokenize_nmt(text, num_examples=None): target.append(parts[1].split(' ')) return source, target +def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist): + """Plot the histogram for list length pairs. + + Defined in :numref:`sec_machine_translation`""" + d2l.set_figsize() + _, _, patches = d2l.plt.hist( + [[len(l) for l in xlist], [len(l) for l in ylist]]) + d2l.plt.xlabel(xlabel) + d2l.plt.ylabel(ylabel) + for patch in patches[1].patches: + patch.set_hatch('/') + d2l.plt.legend(legend) + def truncate_pad(line, num_steps, padding_token): - """截断或填充文本序列 + """Truncate or pad sequences. Defined in :numref:`sec_machine_translation`""" if len(line) > num_steps: - return line[:num_steps] # 截断 - return line + [padding_token] * (num_steps - len(line)) # 填充 + return line[:num_steps] # Truncate + return line + [padding_token] * (num_steps - len(line)) # Pad def build_array_nmt(lines, vocab, num_steps): - """将机器翻译的文本序列转换成小批量 + """Transform text sequences of machine translation into minibatches. Defined in :numref:`subsec_mt_data_loading`""" lines = [vocab[l] for l in lines] @@ -836,7 +861,7 @@ def build_array_nmt(lines, vocab, num_steps): return array, valid_len def load_data_nmt(batch_size, num_steps, num_examples=600): - """返回翻译数据集的迭代器和词表 + """Return the iterator and the vocabularies of the translation dataset. Defined in :numref:`subsec_mt_data_loading`""" text = preprocess_nmt(read_data_nmt()) @@ -852,7 +877,7 @@ def load_data_nmt(batch_size, num_steps, num_examples=600): return data_iter, src_vocab, tgt_vocab class Encoder(tf.keras.layers.Layer): - """编码器-解码器架构的基本编码器接口""" + """The base encoder interface for the encoder-decoder architecture.""" def __init__(self, **kwargs): super(Encoder, self).__init__(**kwargs) @@ -860,7 +885,7 @@ def call(self, X, *args, **kwargs): raise NotImplementedError class Decoder(tf.keras.layers.Layer): - """编码器-解码器架构的基本解码器接口 + """The base decoder interface for the encoder-decoder architecture. Defined in :numref:`sec_encoder-decoder`""" def __init__(self, **kwargs): @@ -873,7 +898,7 @@ def call(self, X, state, **kwargs): raise NotImplementedError class EncoderDecoder(tf.keras.Model): - """编码器-解码器架构的基类 + """The base class for the encoder-decoder architecture. Defined in :numref:`sec_encoder-decoder`""" def __init__(self, encoder, decoder, **kwargs): @@ -887,12 +912,12 @@ def call(self, enc_X, dec_X, *args, **kwargs): return self.decoder(dec_X, dec_state, **kwargs) class Seq2SeqEncoder(d2l.Encoder): - """用于序列到序列学习的循环神经网络编码器 + """The RNN encoder for sequence to sequence learning. Defined in :numref:`sec_seq2seq`""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super().__init__(*kwargs) - # 嵌入层 + # Embedding layer self.embedding = tf.keras.layers.Embedding(vocab_size, embed_size) self.rnn = tf.keras.layers.RNN(tf.keras.layers.StackedRNNCells( [tf.keras.layers.GRUCell(num_hiddens, dropout=dropout) @@ -900,15 +925,15 @@ def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, * return_state=True) def call(self, X, *args, **kwargs): - # 输入'X'的形状:(batch_size,num_steps) - # 输出'X'的形状:(batch_size,num_steps,embed_size) + # The input `X` shape: (`batch_size`, `num_steps`) + # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X) output = self.rnn(X, **kwargs) state = output[1:] return output[0], state def sequence_mask(X, valid_len, value=0): - """在序列中屏蔽不相关的项 + """Mask irrelevant entries in sequences. Defined in :numref:`sec_seq2seq_decoder`""" maxlen = X.shape[1] @@ -921,16 +946,16 @@ def sequence_mask(X, valid_len, value=0): return tf.where(mask, X, value) class MaskedSoftmaxCELoss(tf.keras.losses.Loss): - """带遮蔽的softmax交叉熵损失函数 + """The softmax cross-entropy loss with masks. Defined in :numref:`sec_seq2seq_decoder`""" def __init__(self, valid_len): super().__init__(reduction='none') self.valid_len = valid_len - # pred的形状:(batch_size,num_steps,vocab_size) - # label的形状:(batch_size,num_steps) - # valid_len的形状:(batch_size,) + # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) + # `label` shape: (`batch_size`, `num_steps`) + # `valid_len` shape: (`batch_size`,) def call(self, label, pred): weights = tf.ones_like(label, dtype=tf.float32) weights = sequence_mask(weights, self.valid_len) @@ -941,7 +966,7 @@ def call(self, label, pred): return weighted_loss def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """训练序列到序列模型 + """Train a model for sequence to sequence. Defined in :numref:`sec_seq2seq_decoder`""" optimizer = tf.keras.optimizers.Adam(learning_rate=lr) @@ -949,12 +974,12 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): xlim=[10, num_epochs]) for epoch in range(num_epochs): timer = d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失总和,词元数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for batch in data_iter: X, X_valid_len, Y, Y_valid_len = [x for x in batch] bos = tf.reshape(tf.constant([tgt_vocab['']] * Y.shape[0]), shape=(-1, 1)) - dec_input = tf.concat([bos, Y[:, :-1]], 1) # 强制教学 + dec_input = tf.concat([bos, Y[:, :-1]], 1) # Teacher forcing with tf.GradientTape() as tape: Y_hat, _ = net(X, dec_input, X_valid_len, training=True) l = MaskedSoftmaxCELoss(Y_valid_len)(Y, Y_hat) @@ -970,37 +995,38 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, save_attention_weights=False): - """序列到序列模型的预测 + """Predict for sequence to sequence. Defined in :numref:`sec_seq2seq_training`""" src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ src_vocab['']] enc_valid_len = tf.constant([len(src_tokens)]) src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) - # 添加批量轴 + # Add the batch axis enc_X = tf.expand_dims(src_tokens, axis=0) enc_outputs = net.encoder(enc_X, enc_valid_len, training=False) dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) - # 添加批量轴 + # Add the batch axis dec_X = tf.expand_dims(tf.constant([tgt_vocab['']]), axis=0) output_seq, attention_weight_seq = [], [] for _ in range(num_steps): Y, dec_state = net.decoder(dec_X, dec_state, training=False) - # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入 + # We use the token with the highest prediction likelihood as the input + # of the decoder at the next time step dec_X = tf.argmax(Y, axis=2) pred = tf.squeeze(dec_X, axis=0) - # 保存注意力权重 + # Save attention weights if save_attention_weights: attention_weight_seq.append(net.decoder.attention_weights) - # 一旦序列结束词元被预测,输出序列的生成就完成了 + # Once the end-of-sequence token is predicted, the generation of the + # output sequence is complete if pred == tgt_vocab['']: break output_seq.append(pred.numpy()) - return ' '.join(tgt_vocab.to_tokens(tf.reshape(output_seq, - shape = -1).numpy().tolist())), attention_weight_seq + return ' '.join(tgt_vocab.to_tokens(tf.reshape(output_seq, shape = -1).numpy().tolist())), attention_weight_seq def bleu(pred_seq, label_seq, k): - """计算BLEU + """Compute the BLEU. Defined in :numref:`sec_seq2seq_training`""" pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') @@ -1019,7 +1045,7 @@ def bleu(pred_seq, label_seq, k): def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), cmap='Reds'): - """显示矩阵热图 + """Show heatmaps of matrices. Defined in :numref:`sec_attention-cues`""" d2l.use_svg_display() @@ -1038,10 +1064,10 @@ def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), fig.colorbar(pcm, ax=axes, shrink=0.6); def masked_softmax(X, valid_lens): - """通过在最后一个轴上掩蔽元素来执行softmax操作 + """Perform softmax operation by masking elements on the last axis. Defined in :numref:`sec_attention-scoring-functions`""" - # X:3D张量,valid_lens:1D或2D张量 + # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor if valid_lens is None: return tf.nn.softmax(X, axis=-1) else: @@ -1051,13 +1077,13 @@ def masked_softmax(X, valid_lens): else: valid_lens = tf.reshape(valid_lens, shape=-1) - # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 - X = d2l.sequence_mask(tf.reshape(X, shape=(-1, shape[-1])), - valid_lens, value=-1e6) + # On the last axis, replace masked elements with a very large negative + # value, whose exponentiation outputs 0 + X = d2l.sequence_mask(tf.reshape(X, shape=(-1, shape[-1])), valid_lens, value=-1e6) return tf.nn.softmax(tf.reshape(X, shape=shape), axis=-1) class AdditiveAttention(tf.keras.layers.Layer): - """Additiveattention. + """Additive attention. Defined in :numref:`sec_attention-scoring-functions`""" def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): @@ -1069,33 +1095,36 @@ def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): def call(self, queries, keys, values, valid_lens, **kwargs): queries, keys = self.W_q(queries), self.W_k(keys) - # 在维度扩展后, - # queries的形状:(batch_size,查询的个数,1,num_hidden) - # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens) - # 使用广播方式进行求和 + # After dimension expansion, shape of `queries`: (`batch_size`, no. of + # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, + # no. of key-value pairs, `num_hiddens`). Sum them up with + # broadcasting features = tf.expand_dims(queries, axis=2) + tf.expand_dims( keys, axis=1) features = tf.nn.tanh(features) - # self.w_v仅有一个输出,因此从形状中移除最后那个维度。 - # scores的形状:(batch_size,查询的个数,“键-值”对的个数) + # There is only one output of `self.w_v`, so we remove the last + # one-dimensional entry from the shape. Shape of `scores`: + # (`batch_size`, no. of queries, no. of key-value pairs) scores = tf.squeeze(self.w_v(features), axis=-1) self.attention_weights = masked_softmax(scores, valid_lens) - # values的形状:(batch_size,“键-值”对的个数,值的维度) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) return tf.matmul(self.dropout( self.attention_weights, **kwargs), values) class DotProductAttention(tf.keras.layers.Layer): - """Scaleddotproductattention. + """Scaled dot product attention. Defined in :numref:`subsec_additive-attention`""" def __init__(self, dropout, **kwargs): super().__init__(**kwargs) self.dropout = tf.keras.layers.Dropout(dropout) - # queries的形状:(batch_size,查询的个数,d) - # keys的形状:(batch_size,“键-值”对的个数,d) - # values的形状:(batch_size,“键-值”对的个数,值的维度) - # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数) + # Shape of `queries`: (`batch_size`, no. of queries, `d`) + # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) + # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) def call(self, queries, keys, values, valid_lens, **kwargs): d = queries.shape[-1] scores = tf.matmul(queries, keys, transpose_b=True)/tf.math.sqrt( @@ -1104,7 +1133,7 @@ def call(self, queries, keys, values, valid_lens, **kwargs): return tf.matmul(self.dropout(self.attention_weights, **kwargs), values) class AttentionDecoder(d2l.Decoder): - """带有注意力机制解码器的基本接口 + """The base attention-based decoder interface. Defined in :numref:`sec_seq2seq_attention`""" def __init__(self, **kwargs): @@ -1115,7 +1144,7 @@ def attention_weights(self): raise NotImplementedError class MultiHeadAttention(tf.keras.layers.Layer): - """多头注意力 + """Multi-head attention. Defined in :numref:`sec_multihead-attention`""" def __init__(self, key_size, query_size, value_size, num_hiddens, @@ -1129,50 +1158,53 @@ def __init__(self, key_size, query_size, value_size, num_hiddens, self.W_o = tf.keras.layers.Dense(num_hiddens, use_bias=bias) def call(self, queries, keys, values, valid_lens, **kwargs): - # queries,keys,values的形状: - # (batch_size,查询或者“键-值”对的个数,num_hiddens) - # valid_lens 的形状: - # (batch_size,)或(batch_size,查询的个数) - # 经过变换后,输出的queries,keys,values 的形状: - # (batch_size*num_heads,查询或者“键-值”对的个数, - # num_hiddens/num_heads) + # Shape of `queries`, `keys`, or `values`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`) + # Shape of `valid_lens`: + # (`batch_size`,) or (`batch_size`, no. of queries) + # After transposing, shape of output `queries`, `keys`, or `values`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `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次, - # 然后如此复制第二项,然后诸如此类。 + # On axis 0, copy the first item (scalar or vector) for + # `num_heads` times, then copy the next item, and so on valid_lens = tf.repeat(valid_lens, repeats=self.num_heads, axis=0) - # output的形状:(batch_size*num_heads,查询的个数, - # num_hiddens/num_heads) + # Shape of `output`: (`batch_size` * `num_heads`, no. of queries, `num_hiddens` / `num_heads`) output = self.attention(queries, keys, values, valid_lens, **kwargs) - # output_concat的形状:(batch_size,查询的个数,num_hiddens) + # Shape of `output_concat`: (`batch_size`, no. of queries, `num_hiddens`) output_concat = transpose_output(output, self.num_heads) return self.W_o(output_concat) def transpose_qkv(X, num_heads): - """为了多注意力头的并行计算而变换形状 + """Transposition for parallel computation of multiple attention heads. Defined in :numref:`sec_multihead-attention`""" - # 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens) - # 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads, - # num_hiddens/num_heads) + # Shape of input `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`). + # Shape of output `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_heads`, + # `num_hiddens` / `num_heads`) X = tf.reshape(X, shape=(X.shape[0], X.shape[1], num_heads, -1)) - # 输出X的形状:(batch_size,num_heads,查询或者“键-值”对的个数, - # num_hiddens/num_heads) + # Shape of output `X`: + # (`batch_size`, `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) X = tf.transpose(X, perm=(0, 2, 1, 3)) - # 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数, - # num_hiddens/num_heads) + # Shape of `output`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) return tf.reshape(X, shape=(-1, X.shape[2], X.shape[3])) def transpose_output(X, num_heads): - """逆转transpose_qkv函数的操作 + """Reverse the operation of `transpose_qkv`. Defined in :numref:`sec_multihead-attention`""" X = tf.reshape(X, shape=(-1, num_heads, X.shape[1], X.shape[2])) @@ -1180,13 +1212,13 @@ def transpose_output(X, num_heads): return tf.reshape(X, shape=(X.shape[0], X.shape[1], -1)) class PositionalEncoding(tf.keras.layers.Layer): - """位置编码 + """Positional encoding. Defined in :numref:`sec_self-attention-and-positional-encoding`""" def __init__(self, num_hiddens, dropout, max_len=1000): super().__init__() self.dropout = tf.keras.layers.Dropout(dropout) - # 创建一个足够长的P + # Create a long enough `P` self.P = np.zeros((1, max_len, num_hiddens)) X = np.arange(max_len, dtype=np.float32).reshape( -1,1)/np.power(10000, np.arange( @@ -1199,7 +1231,7 @@ def call(self, X, **kwargs): return self.dropout(X, **kwargs) class PositionWiseFFN(tf.keras.layers.Layer): - """基于位置的前馈网络 + """Positionwise feed-forward network. Defined in :numref:`sec_transformer`""" def __init__(self, ffn_num_hiddens, ffn_num_outputs, **kwargs): @@ -1212,7 +1244,7 @@ def call(self, X): return self.dense2(self.relu(self.dense1(X))) class AddNorm(tf.keras.layers.Layer): - """残差连接后进行层规范化 + """Residual connection followed by layer normalization. Defined in :numref:`sec_transformer`""" def __init__(self, normalized_shape, dropout, **kwargs): @@ -1224,7 +1256,7 @@ def call(self, X, Y, **kwargs): return self.ln(self.dropout(Y, **kwargs) + X) class EncoderBlock(tf.keras.layers.Layer): - """transformer编码器块 + """Transformer encoder block. Defined in :numref:`sec_transformer`""" def __init__(self, key_size, query_size, value_size, num_hiddens, @@ -1241,7 +1273,7 @@ def call(self, X, valid_lens, **kwargs): return self.addnorm2(Y, self.ffn(Y), **kwargs) class TransformerEncoder(d2l.Encoder): - """transformer编码器 + """Transformer encoder. Defined in :numref:`sec_transformer`""" def __init__(self, vocab_size, key_size, query_size, value_size, @@ -1257,9 +1289,9 @@ def __init__(self, vocab_size, key_size, query_size, value_size, num_layers)] def call(self, X, valid_lens, **kwargs): - # 因为位置编码值在-1和1之间, - # 因此嵌入值乘以嵌入维度的平方根进行缩放, - # 然后再与位置编码相加。 + # Since positional encoding values are between -1 and 1, the embedding + # values are multiplied by the square root of the embedding dimension + # to rescale before they are summed up X = self.pos_encoding(self.embedding(X) * tf.math.sqrt( tf.cast(self.num_hiddens, dtype=tf.float32)), **kwargs) self.attention_weights = [None] * len(self.blks) @@ -1274,10 +1306,10 @@ def annotate(text, xy, xytext): arrowprops=dict(arrowstyle='->')) def train_2d(trainer, steps=20, f_grad=None): - """用定制的训练机优化2D目标函数 + """Optimize a 2D objective function with a customized trainer. Defined in :numref:`subsec_gd-learningrate`""" - # s1和s2是稍后将使用的内部状态变量 + # `s1` and `s2` are internal state variables that will be used later x1, x2, s1, s2 = -5, -2, 0, 0 results = [(x1, x2)] for i in range(steps): @@ -1290,7 +1322,7 @@ def train_2d(trainer, steps=20, f_grad=None): return results def show_trace_2d(f, results): - """显示优化过程中2D变量的轨迹 + """Show the trace of 2D variables during optimization. Defined in :numref:`subsec_gd-learningrate`""" d2l.set_figsize() @@ -1316,12 +1348,12 @@ def get_data_ch11(batch_size=10, n=1500): def train_ch11(trainer_fn, states, hyperparams, data_iter, feature_dim, num_epochs=2): """Defined in :numref:`sec_minibatches`""" - # 初始化模型 + # Initialization w = tf.Variable(tf.random.normal(shape=(feature_dim, 1), mean=0, stddev=0.01),trainable=True) b = tf.Variable(tf.zeros(1), trainable=True) - # 训练模型 + # Train net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[0, num_epochs], ylim=[0.22, 0.35]) @@ -1347,7 +1379,7 @@ def train_ch11(trainer_fn, states, hyperparams, data_iter, def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=2): """Defined in :numref:`sec_minibatches`""" - # 初始化模型 + # Initialization net = tf.keras.Sequential() net.add(tf.keras.layers.Dense(1, kernel_initializer=tf.random_normal_initializer(stddev=0.01))) @@ -1369,14 +1401,15 @@ def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=2): timer.stop() p = n/X.shape[0] q = p/tf.data.experimental.cardinality(data_iter).numpy() - # MeanSquaredError计算平方误差时不带系数1/2 + # `MeanSquaredError` computes squared error without the 1/2 + # factor r = (d2l.evaluate_loss(net, data_iter, loss) / 2,) animator.add(q, r) timer.start() print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') class Benchmark: - """用于测量运行时间""" + """For measuring running time.""" def __init__(self, description='Done'): """Defined in :numref:`sec_hybridize`""" self.description = description @@ -1389,7 +1422,7 @@ def __exit__(self, *args): print(f'{self.description}: {self.timer.stop():.4f} sec') def box_corner_to_center(boxes): - """从(左上,右下)转换到(中间,宽度,高度) + """Convert from (upper-left, lower-right) to (center, width, height). Defined in :numref:`sec_bbox`""" x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] @@ -1401,7 +1434,7 @@ def box_corner_to_center(boxes): return boxes def box_center_to_corner(boxes): - """从(中间,宽度,高度)转换到(左上,右下) + """Convert from (center, width, height) to (upper-left, lower-right). Defined in :numref:`sec_bbox`""" cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] @@ -1413,12 +1446,54 @@ def box_center_to_corner(boxes): return boxes def bbox_to_rect(bbox, color): - """Defined in :numref:`sec_bbox`""" - # 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式: - # ((左上x,左上y),宽,高) + """Convert bounding box to matplotlib format. + + Defined in :numref:`sec_bbox`""" + # Convert the bounding box (upper-left x, upper-left y, lower-right x, + # lower-right y) format to the matplotlib format: ((upper-left x, + # upper-left y), width, height) return d2l.plt.Rectangle( xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], - fill=False, edgecolor=color, linewidth=2)# Alias defined in config.ini + fill=False, edgecolor=color, linewidth=2) + +def update_D(X, Z, net_D, net_G, loss, optimizer_D): + """Update discriminator. + + Defined in :numref:`sec_basic_gan`""" + batch_size = X.shape[0] + ones = tf.ones((batch_size,)) # Labels corresponding to real data + zeros = tf.zeros((batch_size,)) # Labels corresponding to fake data + # Do not need to compute gradient for `net_G`, so it's outside GradientTape + fake_X = net_G(Z) + with tf.GradientTape() as tape: + real_Y = net_D(X) + fake_Y = net_D(fake_X) + # We multiply the loss by batch_size to match PyTorch's BCEWithLogitsLoss + loss_D = (loss(ones, tf.squeeze(real_Y)) + loss( + zeros, tf.squeeze(fake_Y))) * batch_size / 2 + grads_D = tape.gradient(loss_D, net_D.trainable_variables) + optimizer_D.apply_gradients(zip(grads_D, net_D.trainable_variables)) + return loss_D + +def update_G(Z, net_D, net_G, loss, optimizer_G): + """Update generator. + + Defined in :numref:`sec_basic_gan`""" + batch_size = Z.shape[0] + ones = tf.ones((batch_size,)) + with tf.GradientTape() as tape: + # We could reuse `fake_X` from `update_D` to save computation + fake_X = net_G(Z) + # Recomputing `fake_Y` is needed since `net_D` is changed + fake_Y = net_D(fake_X) + # We multiply the loss by batch_size to match PyTorch's BCEWithLogits loss + loss_G = loss(ones, tf.squeeze(fake_Y)) * batch_size + grads_G = tape.gradient(loss_G, net_G.trainable_variables) + optimizer_G.apply_gradients(zip(grads_G, net_G.trainable_variables)) + return loss_G + +d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip', + 'c065c0e2593b8b161a2d7873e42418bf6a21106c')# Alias defined in config.ini size = lambda a: tf.size(a).numpy() reshape = tf.reshape @@ -1436,7 +1511,6 @@ def bbox_to_rect(bbox, color): rand = tf.random.uniform matmul = tf.matmul reduce_sum = tf.reduce_sum -reduce_mean = tf.reduce_mean argmax = tf.argmax tensor = tf.constant arange = tf.range @@ -1448,6 +1522,5 @@ def bbox_to_rect(bbox, color): stack = tf.stack abs = tf.abs eye = tf.eye -log = tf.math.log numpy = lambda x, *args, **kwargs: x.numpy(*args, **kwargs) diff --git a/d2l/torch.py b/d2l/torch.py index d7f7da3ad..58cecaeb6 100644 --- a/d2l/torch.py +++ b/d2l/torch.py @@ -1,3 +1,17 @@ +DATA_HUB = dict() +DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/' + +import numpy as np +import torch +import torchvision +from PIL import Image +from torch import nn +from torch.nn import functional as F +from torch.utils import data +from torchvision import transforms + +nn_Module = nn.Module + ################# WARNING ################ # The below part is generated automatically through: # d2lbook build lib @@ -33,20 +47,20 @@ from torchvision import transforms def use_svg_display(): - """使用svg格式在Jupyter中显示绘图 + """Use the svg format to display a plot in Jupyter. Defined in :numref:`sec_calculus`""" backend_inline.set_matplotlib_formats('svg') def set_figsize(figsize=(3.5, 2.5)): - """设置matplotlib的图表大小 + """Set the figure size for matplotlib. Defined in :numref:`sec_calculus`""" use_svg_display() d2l.plt.rcParams['figure.figsize'] = figsize def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): - """设置matplotlib的轴 + """Set the axes for matplotlib. Defined in :numref:`sec_calculus`""" axes.set_xlabel(xlabel) @@ -62,7 +76,7 @@ def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None): - """绘制数据点 + """Plot data points. Defined in :numref:`sec_calculus`""" if legend is None: @@ -71,7 +85,7 @@ def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # 如果X有一个轴,输出True + # Return True if `X` (tensor or list) has 1 axis def has_one_axis(X): return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) and not hasattr(X[0], "__len__")) @@ -93,35 +107,35 @@ def has_one_axis(X): set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend) class Timer: - """记录多次运行时间""" + """Record multiple running times.""" def __init__(self): """Defined in :numref:`subsec_linear_model`""" self.times = [] self.start() def start(self): - """启动计时器""" + """Start the timer.""" self.tik = time.time() def stop(self): - """停止计时器并将时间记录在列表中""" + """Stop the timer and record the time in a list.""" self.times.append(time.time() - self.tik) return self.times[-1] def avg(self): - """返回平均时间""" + """Return the average time.""" return sum(self.times) / len(self.times) def sum(self): - """返回时间总和""" + """Return the sum of time.""" return sum(self.times) def cumsum(self): - """返回累计时间""" + """Return the accumulated time.""" return np.array(self.times).cumsum().tolist() def synthetic_data(w, b, num_examples): - """生成y=Xw+b+噪声 + """Generate y = Xw + b + noise. Defined in :numref:`sec_linear_scratch`""" X = d2l.normal(0, 1, (num_examples, len(w))) @@ -130,19 +144,19 @@ def synthetic_data(w, b, num_examples): return X, d2l.reshape(y, (-1, 1)) def linreg(X, w, b): - """线性回归模型 + """The linear regression model. Defined in :numref:`sec_linear_scratch`""" return d2l.matmul(X, w) + b def squared_loss(y_hat, y): - """均方损失 + """Squared loss. Defined in :numref:`sec_linear_scratch`""" return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 def sgd(params, lr, batch_size): - """小批量随机梯度下降 + """Minibatch stochastic gradient descent. Defined in :numref:`sec_linear_scratch`""" with torch.no_grad(): @@ -151,14 +165,14 @@ def sgd(params, lr, batch_size): param.grad.zero_() def load_array(data_arrays, batch_size, is_train=True): - """构造一个PyTorch数据迭代器 + """Construct a PyTorch data iterator. Defined in :numref:`sec_linear_concise`""" dataset = data.TensorDataset(*data_arrays) return data.DataLoader(dataset, batch_size, shuffle=is_train) def get_fashion_mnist_labels(labels): - """返回Fashion-MNIST数据集的文本标签 + """Return text labels for the Fashion-MNIST dataset. Defined in :numref:`sec_fashion_mnist`""" text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', @@ -166,7 +180,7 @@ def get_fashion_mnist_labels(labels): return [text_labels[int(i)] for i in labels] def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): - """绘制图像列表 + """Plot a list of images. Defined in :numref:`sec_fashion_mnist`""" figsize = (num_cols * scale, num_rows * scale) @@ -174,10 +188,10 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): axes = axes.flatten() for i, (ax, img) in enumerate(zip(axes, imgs)): if torch.is_tensor(img): - # 图片张量 + # Tensor Image ax.imshow(img.numpy()) else: - # PIL图片 + # PIL Image ax.imshow(img) ax.axes.get_xaxis().set_visible(False) ax.axes.get_yaxis().set_visible(False) @@ -186,13 +200,13 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): return axes def get_dataloader_workers(): - """使用4个进程来读取数据 + """Use 4 processes to read the data. Defined in :numref:`sec_fashion_mnist`""" return 4 def load_data_fashion_mnist(batch_size, resize=None): - """下载Fashion-MNIST数据集,然后将其加载到内存中 + """Download the Fashion-MNIST dataset and then load it into memory. Defined in :numref:`sec_fashion_mnist`""" trans = [transforms.ToTensor()] @@ -209,7 +223,7 @@ def load_data_fashion_mnist(batch_size, resize=None): num_workers=get_dataloader_workers())) def accuracy(y_hat, y): - """计算预测正确的数量 + """Compute the number of correct predictions. Defined in :numref:`sec_softmax_scratch`""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: @@ -218,19 +232,20 @@ def accuracy(y_hat, y): return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) def evaluate_accuracy(net, data_iter): - """计算在指定数据集上模型的精度 + """Compute the accuracy for a model on a dataset. Defined in :numref:`sec_softmax_scratch`""" if isinstance(net, torch.nn.Module): - net.eval() # 将模型设置为评估模式 - metric = Accumulator(2) # 正确预测数、预测总数 + net.eval() # Set the model to evaluation mode + metric = Accumulator(2) # No. of correct predictions, no. of predictions + with torch.no_grad(): for X, y in data_iter: metric.add(accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] class Accumulator: - """在n个变量上累加""" + """For accumulating sums over `n` variables.""" def __init__(self, n): """Defined in :numref:`sec_softmax_scratch`""" self.data = [0.0] * n @@ -245,52 +260,52 @@ def __getitem__(self, idx): return self.data[idx] def train_epoch_ch3(net, train_iter, loss, updater): - """训练模型一个迭代周期(定义见第3章) + """The training loop defined in Chapter 3. Defined in :numref:`sec_softmax_scratch`""" - # 将模型设置为训练模式 + # Set the model to training mode if isinstance(net, torch.nn.Module): net.train() - # 训练损失总和、训练准确度总和、样本数 + # Sum of training loss, sum of training accuracy, no. of examples metric = Accumulator(3) for X, y in train_iter: - # 计算梯度并更新参数 + # Compute gradients and update parameters y_hat = net(X) l = loss(y_hat, y) if isinstance(updater, torch.optim.Optimizer): - # 使用PyTorch内置的优化器和损失函数 + # Using PyTorch in-built optimizer & loss criterion updater.zero_grad() l.mean().backward() updater.step() else: - # 使用定制的优化器和损失函数 + # Using custom built optimizer & loss criterion l.sum().backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) - # 返回训练损失和训练精度 + # Return training loss and training accuracy return metric[0] / metric[2], metric[1] / metric[2] class Animator: - """在动画中绘制数据""" + """For plotting data in animation.""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): """Defined in :numref:`sec_softmax_scratch`""" - # 增量地绘制多条线 + # Incrementally plot multiple lines if legend is None: legend = [] d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: self.axes = [self.axes, ] - # 使用lambda函数捕获参数 + # Use a lambda function to capture arguments self.config_axes = lambda: d2l.set_axes( self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): - # 向图表中添加多个数据点 + # Add multiple data points into the figure if not hasattr(y, "__len__"): y = [y] n = len(y) @@ -312,7 +327,7 @@ def add(self, x, y): display.clear_output(wait=True) def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): - """训练模型(定义见第3章) + """Train a model (defined in Chapter 3). Defined in :numref:`sec_softmax_scratch`""" animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], @@ -327,7 +342,7 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): assert test_acc <= 1 and test_acc > 0.7, test_acc def predict_ch3(net, test_iter, n=6): - """预测标签(定义见第3章) + """Predict labels (defined in Chapter 3). Defined in :numref:`sec_softmax_scratch`""" for X, y in test_iter: @@ -339,10 +354,10 @@ def predict_ch3(net, test_iter, n=6): d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n]) def evaluate_loss(net, data_iter, loss): - """评估给定数据集上模型的损失 + """Evaluate the loss of a model on the given dataset. Defined in :numref:`sec_model_selection`""" - metric = d2l.Accumulator(2) # 损失的总和,样本数量 + metric = d2l.Accumulator(2) # Sum of losses, no. of examples for X, y in data_iter: out = net(X) y = d2l.reshape(y, out.shape) @@ -354,10 +369,10 @@ def evaluate_loss(net, data_iter, loss): DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/' def download(name, cache_dir=os.path.join('..', 'data')): - """下载一个DATA_HUB中的文件,返回本地文件名 + """Download a file inserted into DATA_HUB, return the local filename. Defined in :numref:`sec_kaggle_house`""" - assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}" + assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}." url, sha1_hash = DATA_HUB[name] os.makedirs(cache_dir, exist_ok=True) fname = os.path.join(cache_dir, url.split('/')[-1]) @@ -370,15 +385,15 @@ def download(name, cache_dir=os.path.join('..', 'data')): break sha1.update(data) if sha1.hexdigest() == sha1_hash: - return fname # 命中缓存 - print(f'正在从{url}下载{fname}...') + return fname # Hit cache + print(f'Downloading {fname} from {url}...') r = requests.get(url, stream=True, verify=True) with open(fname, 'wb') as f: f.write(r.content) return fname def download_extract(name, folder=None): - """下载并解压zip/tar文件 + """Download and extract a zip/tar file. Defined in :numref:`sec_kaggle_house`""" fname = download(name) @@ -389,12 +404,12 @@ def download_extract(name, folder=None): elif ext in ('.tar', '.gz'): fp = tarfile.open(fname, 'r') else: - assert False, '只有zip/tar文件可以被解压缩' + assert False, 'Only zip/tar files can be extracted.' fp.extractall(base_dir) return os.path.join(base_dir, folder) if folder else data_dir def download_all(): - """下载DATA_HUB中的所有文件 + """Download all files in the DATA_HUB. Defined in :numref:`sec_kaggle_house`""" for name in DATA_HUB: @@ -409,7 +424,7 @@ def download_all(): 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') def try_gpu(i=0): - """如果存在,则返回gpu(i),否则返回cpu() + """Return gpu(i) if exists, otherwise return cpu(). Defined in :numref:`sec_use_gpu`""" if torch.cuda.device_count() >= i + 1: @@ -417,7 +432,7 @@ def try_gpu(i=0): return torch.device('cpu') def try_all_gpus(): - """返回所有可用的GPU,如果没有GPU,则返回[cpu(),] + """Return all available GPUs, or [cpu(),] if no GPU exists. Defined in :numref:`sec_use_gpu`""" devices = [torch.device(f'cuda:{i}') @@ -425,7 +440,7 @@ def try_all_gpus(): return devices if devices else [torch.device('cpu')] def corr2d(X, K): - """计算二维互相关运算 + """Compute 2D cross-correlation. Defined in :numref:`sec_conv_layer`""" h, w = K.shape @@ -436,19 +451,20 @@ def corr2d(X, K): return Y def evaluate_accuracy_gpu(net, data_iter, device=None): - """使用GPU计算模型在数据集上的精度 + """Compute the accuracy for a model on a dataset using a GPU. Defined in :numref:`sec_lenet`""" if isinstance(net, nn.Module): - net.eval() # 设置为评估模式 + net.eval() # Set the model to evaluation mode if not device: device = next(iter(net.parameters())).device - # 正确预测的数量,总预测的数量 + # No. of correct predictions, no. of predictions metric = d2l.Accumulator(2) + with torch.no_grad(): for X, y in data_iter: if isinstance(X, list): - # BERT微调所需的(之后将介绍) + # Required for BERT Fine-tuning (to be covered later) X = [x.to(device) for x in X] else: X = X.to(device) @@ -457,7 +473,7 @@ def evaluate_accuracy_gpu(net, data_iter, device=None): return metric[0] / metric[1] def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): - """用GPU训练模型(在第六章定义) + """Train a model with a GPU (defined in Chapter 6). Defined in :numref:`sec_lenet`""" def init_weights(m): @@ -472,7 +488,7 @@ def init_weights(m): legend=['train loss', 'train acc', 'test acc']) timer, num_batches = d2l.Timer(), len(train_iter) for epoch in range(num_epochs): - # 训练损失之和,训练准确率之和,样本数 + # Sum of training loss, sum of training accuracy, no. of examples metric = d2l.Accumulator(3) net.train() for i, (X, y) in enumerate(train_iter): @@ -499,6 +515,7 @@ def init_weights(m): f'on {str(device)}') class Residual(nn.Module): + """The Residual block of ResNet.""" def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1): super().__init__() @@ -526,7 +543,7 @@ def forward(self, X): '090b5e7e70c295757f55df93cb0a180b9691891a') def read_time_machine(): - """将时间机器数据集加载到文本行的列表中 + """Load the time machine dataset into a list of text lines. Defined in :numref:`sec_text_preprocessing`""" with open(d2l.download('time_machine'), 'r') as f: @@ -534,7 +551,7 @@ def read_time_machine(): return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines] def tokenize(lines, token='word'): - """将文本行拆分为单词或字符词元 + """Split text lines into word or character tokens. Defined in :numref:`sec_text_preprocessing`""" if token == 'word': @@ -542,21 +559,21 @@ def tokenize(lines, token='word'): elif token == 'char': return [list(line) for line in lines] else: - print('错误:未知词元类型:' + token) + print('ERROR: unknown token type: ' + token) class Vocab: - """文本词表""" + """Vocabulary for text.""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): """Defined in :numref:`sec_text_preprocessing`""" if tokens is None: tokens = [] if reserved_tokens is None: reserved_tokens = [] - # 按出现频率排序 + # Sort according to frequencies counter = count_corpus(tokens) self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) - # 未知词元的索引为0 + # The index for the unknown token is 0 self.idx_to_token = [''] + reserved_tokens self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)} @@ -581,68 +598,71 @@ def to_tokens(self, indices): return [self.idx_to_token[index] for index in indices] @property - def unk(self): # 未知词元的索引为0 + def unk(self): # Index for the unknown token return 0 @property - def token_freqs(self): + def token_freqs(self): # Index for the unknown token return self._token_freqs def count_corpus(tokens): - """统计词元的频率 + """Count token frequencies. Defined in :numref:`sec_text_preprocessing`""" - # 这里的tokens是1D列表或2D列表 + # Here `tokens` is a 1D list or 2D list if len(tokens) == 0 or isinstance(tokens[0], list): - # 将词元列表展平成一个列表 + # Flatten a list of token lists into a list of tokens tokens = [token for line in tokens for token in line] return collections.Counter(tokens) def load_corpus_time_machine(max_tokens=-1): - """返回时光机器数据集的词元索引列表和词表 + """Return token indices and the vocabulary of the time machine dataset. Defined in :numref:`sec_text_preprocessing`""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落, - # 所以将所有文本行展平到一个列表中 + # Since each text line in the time machine dataset is not necessarily a + # sentence or a paragraph, flatten all the text lines into a single list corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] return corpus, vocab def seq_data_iter_random(corpus, batch_size, num_steps): - """使用随机抽样生成一个小批量子序列 + """Generate a minibatch of subsequences using random sampling. Defined in :numref:`sec_language_model`""" - # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1 + # Start with a random offset (inclusive of `num_steps - 1`) to partition a + # sequence corpus = corpus[random.randint(0, num_steps - 1):] - # 减去1,是因为我们需要考虑标签 + # Subtract 1 since we need to account for labels num_subseqs = (len(corpus) - 1) // num_steps - # 长度为num_steps的子序列的起始索引 + # The starting indices for subsequences of length `num_steps` initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # 在随机抽样的迭代过程中, - # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻 + # In random sampling, the subsequences from two adjacent random + # minibatches during iteration are not necessarily adjacent on the + # original sequence random.shuffle(initial_indices) def data(pos): - # 返回从pos位置开始的长度为num_steps的序列 + # Return a sequence of length `num_steps` starting from `pos` return corpus[pos: pos + num_steps] num_batches = num_subseqs // batch_size for i in range(0, batch_size * num_batches, batch_size): - # 在这里,initial_indices包含子序列的随机起始索引 + # Here, `initial_indices` contains randomized starting indices for + # subsequences initial_indices_per_batch = initial_indices[i: i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] yield d2l.tensor(X), d2l.tensor(Y) def seq_data_iter_sequential(corpus, batch_size, num_steps): - """使用顺序分区生成一个小批量子序列 + """Generate a minibatch of subsequences using sequential partitioning. Defined in :numref:`sec_language_model`""" - # 从随机偏移量开始划分序列 + # Start with a random offset to partition a sequence offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size Xs = d2l.tensor(corpus[offset: offset + num_tokens]) @@ -655,7 +675,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): yield X, Y class SeqDataLoader: - """加载序列数据的迭代器""" + """An iterator to load sequence data.""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): """Defined in :numref:`sec_language_model`""" if use_random_iter: @@ -670,7 +690,7 @@ def __iter__(self): def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): - """返回时光机器数据集的迭代器和词表 + """Return the iterator and the vocabulary of the time machine dataset. Defined in :numref:`sec_language_model`""" data_iter = SeqDataLoader( @@ -678,7 +698,7 @@ def load_data_time_machine(batch_size, num_steps, return data_iter, data_iter.vocab class RNNModelScratch: - """从零开始实现的循环神经网络模型""" + """A RNN Model implemented from scratch.""" def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn): """Defined in :numref:`sec_rnn_scratch`""" @@ -694,23 +714,23 @@ def begin_state(self, batch_size, device): return self.init_state(batch_size, self.num_hiddens, device) def predict_ch8(prefix, num_preds, net, vocab, device): - """在prefix后面生成新字符 + """Generate new characters following the `prefix`. Defined in :numref:`sec_rnn_scratch`""" state = net.begin_state(batch_size=1, device=device) outputs = [vocab[prefix[0]]] get_input = lambda: d2l.reshape(d2l.tensor( [outputs[-1]], device=device), (1, 1)) - for y in prefix[1:]: # 预热期 + for y in prefix[1:]: # Warm-up period _, state = net(get_input(), state) outputs.append(vocab[y]) - for _ in range(num_preds): # 预测num_preds步 + for _ in range(num_preds): # Predict `num_preds` steps y, state = net(get_input(), state) outputs.append(int(y.argmax(dim=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) def grad_clipping(net, theta): - """裁剪梯度 + """Clip the gradient. Defined in :numref:`sec_rnn_scratch`""" if isinstance(net, nn.Module): @@ -723,21 +743,23 @@ def grad_clipping(net, theta): param.grad[:] *= theta / norm def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): - """训练网络一个迭代周期(定义见第8章) + """Train a net within one epoch (defined in Chapter 8). Defined in :numref:`sec_rnn_scratch`""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失之和,词元数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for X, Y in train_iter: if state is None or use_random_iter: - # 在第一次迭代或使用随机抽样时初始化state + # Initialize `state` when either it is the first iteration or + # using random sampling state = net.begin_state(batch_size=X.shape[0], device=device) else: if isinstance(net, nn.Module) and not isinstance(state, tuple): - # state对于nn.GRU是个张量 + # `state` is a tensor for `nn.GRU` state.detach_() else: - # state对于nn.LSTM或对于我们从零开始实现的模型是个张量 + # `state` is a tuple of tensors for `nn.LSTM` and + # for our custom scratch implementation for s in state: s.detach_() y = Y.T.reshape(-1) @@ -752,38 +774,38 @@ def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): else: l.backward() grad_clipping(net, 1) - # 因为已经调用了mean函数 + # Since the `mean` function has been invoked updater(batch_size=1) metric.add(l * d2l.size(y), d2l.size(y)) return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): - """训练模型(定义见第8章) + """Train a model (defined in Chapter 8). Defined in :numref:`sec_rnn_scratch`""" loss = nn.CrossEntropyLoss() animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) - # 初始化 + # Initialize if isinstance(net, nn.Module): updater = torch.optim.SGD(net.parameters(), lr) else: updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) - # 训练和预测 + # Train and predict for epoch in range(num_epochs): ppl, speed = train_epoch_ch8( net, train_iter, loss, updater, device, use_random_iter) if (epoch + 1) % 10 == 0: print(predict('time traveller')) animator.add(epoch + 1, [ppl]) - print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}') + print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') print(predict('time traveller')) print(predict('traveller')) class RNNModel(nn.Module): - """循环神经网络模型 + """The RNN model. Defined in :numref:`sec_rnn-concise`""" def __init__(self, rnn_layer, vocab_size, **kwargs): @@ -791,7 +813,8 @@ def __init__(self, rnn_layer, vocab_size, **kwargs): self.rnn = rnn_layer self.vocab_size = vocab_size self.num_hiddens = self.rnn.hidden_size - # 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1 + # If the RNN is bidirectional (to be introduced later), + # `num_directions` should be 2, else it should be 1. if not self.rnn.bidirectional: self.num_directions = 1 self.linear = nn.Linear(self.num_hiddens, self.vocab_size) @@ -803,19 +826,20 @@ def forward(self, inputs, state): X = F.one_hot(inputs.T.long(), self.vocab_size) X = X.to(torch.float32) Y, state = self.rnn(X, state) - # 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数) - # 它的输出形状是(时间步数*批量大小,词表大小)。 + # The fully connected layer will first change the shape of `Y` to + # (`num_steps` * `batch_size`, `num_hiddens`). Its output shape is + # (`num_steps` * `batch_size`, `vocab_size`). output = self.linear(Y.reshape((-1, Y.shape[-1]))) return output, state def begin_state(self, device, batch_size=1): if not isinstance(self.rnn, nn.LSTM): - # nn.GRU以张量作为隐状态 + # `nn.GRU` takes a tensor as hidden state return torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), device=device) else: - # nn.LSTM以元组作为隐状态 + # `nn.LSTM` takes a tuple of hidden states return (torch.zeros(( self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), device=device), @@ -827,31 +851,30 @@ def begin_state(self, device, batch_size=1): '94646ad1522d915e7b0f9296181140edcf86a4f5') def read_data_nmt(): - """载入“英语-法语”数据集 + """Load the English-French dataset. Defined in :numref:`sec_machine_translation`""" data_dir = d2l.download_extract('fra-eng') - with open(os.path.join(data_dir, 'fra.txt'), 'r', - encoding='utf-8') as f: + with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: return f.read() def preprocess_nmt(text): - """预处理“英语-法语”数据集 + """Preprocess the English-French dataset. Defined in :numref:`sec_machine_translation`""" def no_space(char, prev_char): return char in set(',.!?') and prev_char != ' ' - # 使用空格替换不间断空格 - # 使用小写字母替换大写字母 + # Replace non-breaking space with space, and convert uppercase letters to + # lowercase ones text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() - # 在单词和标点符号之间插入空格 + # Insert space between words and punctuation marks out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char for i, char in enumerate(text)] return ''.join(out) def tokenize_nmt(text, num_examples=None): - """词元化“英语-法语”数据数据集 + """Tokenize the English-French dataset. Defined in :numref:`sec_machine_translation`""" source, target = [], [] @@ -864,16 +887,29 @@ def tokenize_nmt(text, num_examples=None): target.append(parts[1].split(' ')) return source, target +def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist): + """Plot the histogram for list length pairs. + + Defined in :numref:`sec_machine_translation`""" + d2l.set_figsize() + _, _, patches = d2l.plt.hist( + [[len(l) for l in xlist], [len(l) for l in ylist]]) + d2l.plt.xlabel(xlabel) + d2l.plt.ylabel(ylabel) + for patch in patches[1].patches: + patch.set_hatch('/') + d2l.plt.legend(legend) + def truncate_pad(line, num_steps, padding_token): - """截断或填充文本序列 + """Truncate or pad sequences. Defined in :numref:`sec_machine_translation`""" if len(line) > num_steps: - return line[:num_steps] # 截断 - return line + [padding_token] * (num_steps - len(line)) # 填充 + return line[:num_steps] # Truncate + return line + [padding_token] * (num_steps - len(line)) # Pad def build_array_nmt(lines, vocab, num_steps): - """将机器翻译的文本序列转换成小批量 + """Transform text sequences of machine translation into minibatches. Defined in :numref:`subsec_mt_data_loading`""" lines = [vocab[l] for l in lines] @@ -885,7 +921,7 @@ def build_array_nmt(lines, vocab, num_steps): return array, valid_len def load_data_nmt(batch_size, num_steps, num_examples=600): - """返回翻译数据集的迭代器和词表 + """Return the iterator and the vocabularies of the translation dataset. Defined in :numref:`subsec_mt_data_loading`""" text = preprocess_nmt(read_data_nmt()) @@ -901,7 +937,7 @@ def load_data_nmt(batch_size, num_steps, num_examples=600): return data_iter, src_vocab, tgt_vocab class Encoder(nn.Module): - """编码器-解码器架构的基本编码器接口""" + """The base encoder interface for the encoder-decoder architecture.""" def __init__(self, **kwargs): super(Encoder, self).__init__(**kwargs) @@ -909,7 +945,7 @@ def forward(self, X, *args): raise NotImplementedError class Decoder(nn.Module): - """编码器-解码器架构的基本解码器接口 + """The base decoder interface for the encoder-decoder architecture. Defined in :numref:`sec_encoder-decoder`""" def __init__(self, **kwargs): @@ -922,7 +958,7 @@ def forward(self, X, state): raise NotImplementedError class EncoderDecoder(nn.Module): - """编码器-解码器架构的基类 + """The base class for the encoder-decoder architecture. Defined in :numref:`sec_encoder-decoder`""" def __init__(self, encoder, decoder, **kwargs): @@ -936,30 +972,30 @@ def forward(self, enc_X, dec_X, *args): return self.decoder(dec_X, dec_state) class Seq2SeqEncoder(d2l.Encoder): - """用于序列到序列学习的循环神经网络编码器 + """The RNN encoder for sequence to sequence learning. Defined in :numref:`sec_seq2seq`""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) - # 嵌入层 + # Embedding layer self.embedding = nn.Embedding(vocab_size, embed_size) self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout) def forward(self, X, *args): - # 输出'X'的形状:(batch_size,num_steps,embed_size) + # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X) - # 在循环神经网络模型中,第一个轴对应于时间步 + # In RNN models, the first axis corresponds to time steps X = X.permute(1, 0, 2) - # 如果未提及状态,则默认为0 + # When state is not mentioned, it defaults to zeros output, state = self.rnn(X) - # output的形状:(num_steps,batch_size,num_hiddens) - # state[0]的形状:(num_layers,batch_size,num_hiddens) + # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) + # `state` shape: (`num_layers`, `batch_size`, `num_hiddens`) return output, state def sequence_mask(X, valid_len, value=0): - """在序列中屏蔽不相关的项 + """Mask irrelevant entries in sequences. Defined in :numref:`sec_seq2seq_decoder`""" maxlen = X.size(1) @@ -969,12 +1005,12 @@ def sequence_mask(X, valid_len, value=0): return X class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): - """带遮蔽的softmax交叉熵损失函数 + """The softmax cross-entropy loss with masks. Defined in :numref:`sec_seq2seq_decoder`""" - # pred的形状:(batch_size,num_steps,vocab_size) - # label的形状:(batch_size,num_steps) - # valid_len的形状:(batch_size,) + # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) + # `label` shape: (`batch_size`, `num_steps`) + # `valid_len` shape: (`batch_size`,) def forward(self, pred, label, valid_len): weights = torch.ones_like(label) weights = sequence_mask(weights, valid_len) @@ -985,7 +1021,7 @@ def forward(self, pred, label, valid_len): return weighted_loss def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """训练序列到序列模型 + """Train a model for sequence to sequence. Defined in :numref:`sec_seq2seq_decoder`""" def xavier_init_weights(m): @@ -995,26 +1031,25 @@ def xavier_init_weights(m): for param in m._flat_weights_names: if "weight" in param: nn.init.xavier_uniform_(m._parameters[param]) - net.apply(xavier_init_weights) net.to(device) optimizer = torch.optim.Adam(net.parameters(), lr=lr) loss = MaskedSoftmaxCELoss() net.train() animator = d2l.Animator(xlabel='epoch', ylabel='loss', - xlim=[10, num_epochs]) + xlim=[10, num_epochs]) for epoch in range(num_epochs): timer = d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失总和,词元数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for batch in data_iter: optimizer.zero_grad() X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch] bos = torch.tensor([tgt_vocab['']] * Y.shape[0], - device=device).reshape(-1, 1) - dec_input = torch.cat([bos, Y[:, :-1]], 1) # 强制教学 + device=device).reshape(-1, 1) + dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) - l.sum().backward() # 损失函数的标量进行“反向传播” + l.sum().backward() # Make the loss scalar for `backward` d2l.grad_clipping(net, 1) num_tokens = Y_valid_len.sum() optimizer.step() @@ -1023,44 +1058,46 @@ def xavier_init_weights(m): if (epoch + 1) % 10 == 0: animator.add(epoch + 1, (metric[0] / metric[1],)) print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' - f'tokens/sec on {str(device)}') + f'tokens/sec on {str(device)}') def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=False): - """序列到序列模型的预测 + """Predict for sequence to sequence. Defined in :numref:`sec_seq2seq_training`""" - # 在预测时将net设置为评估模式 + # Set `net` to eval mode for inference net.eval() src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ src_vocab['']] enc_valid_len = torch.tensor([len(src_tokens)], device=device) src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) - # 添加批量轴 + # Add the batch axis enc_X = torch.unsqueeze( torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0) enc_outputs = net.encoder(enc_X, enc_valid_len) dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) - # 添加批量轴 + # Add the batch axis dec_X = torch.unsqueeze(torch.tensor( [tgt_vocab['']], dtype=torch.long, device=device), dim=0) output_seq, attention_weight_seq = [], [] for _ in range(num_steps): Y, dec_state = net.decoder(dec_X, dec_state) - # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入 + # We use the token with the highest prediction likelihood as the input + # of the decoder at the next time step dec_X = Y.argmax(dim=2) pred = dec_X.squeeze(dim=0).type(torch.int32).item() - # 保存注意力权重(稍后讨论) + # Save attention weights (to be covered later) if save_attention_weights: attention_weight_seq.append(net.decoder.attention_weights) - # 一旦序列结束词元被预测,输出序列的生成就完成了 + # Once the end-of-sequence token is predicted, the generation of the + # output sequence is complete if pred == tgt_vocab['']: break output_seq.append(pred) return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq def bleu(pred_seq, label_seq, k): - """计算BLEU + """Compute the BLEU. Defined in :numref:`sec_seq2seq_training`""" pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') @@ -1079,7 +1116,7 @@ def bleu(pred_seq, label_seq, k): def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), cmap='Reds'): - """显示矩阵热图 + """Show heatmaps of matrices. Defined in :numref:`sec_attention-cues`""" d2l.use_svg_display() @@ -1098,10 +1135,10 @@ def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), fig.colorbar(pcm, ax=axes, shrink=0.6); def masked_softmax(X, valid_lens): - """通过在最后一个轴上掩蔽元素来执行softmax操作 + """Perform softmax operation by masking elements on the last axis. Defined in :numref:`sec_attention-scoring-functions`""" - # X:3D张量,valid_lens:1D或2D张量 + # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor if valid_lens is None: return nn.functional.softmax(X, dim=-1) else: @@ -1110,13 +1147,14 @@ def masked_softmax(X, valid_lens): valid_lens = torch.repeat_interleave(valid_lens, shape[1]) else: valid_lens = valid_lens.reshape(-1) - # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 + # On the last axis, replace masked elements with a very large negative + # value, whose exponentiation outputs 0 X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, value=-1e6) return nn.functional.softmax(X.reshape(shape), dim=-1) class AdditiveAttention(nn.Module): - """加性注意力 + """Additive attention. Defined in :numref:`sec_attention-scoring-functions`""" def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): @@ -1128,40 +1166,43 @@ def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): 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) - # 使用广播方式进行求和 + # After dimension expansion, shape of `queries`: (`batch_size`, no. of + # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, + # no. of key-value pairs, `num_hiddens`). Sum them up with + # broadcasting features = queries.unsqueeze(2) + keys.unsqueeze(1) features = torch.tanh(features) - # self.w_v仅有一个输出,因此从形状中移除最后那个维度。 - # scores的形状:(batch_size,查询的个数,“键-值”对的个数) + # There is only one output of `self.w_v`, so we remove the last + # one-dimensional entry from the shape. Shape of `scores`: + # (`batch_size`, no. of queries, no. of key-value pairs) scores = self.w_v(features).squeeze(-1) self.attention_weights = masked_softmax(scores, valid_lens) - # values的形状:(batch_size,“键-值”对的个数,值的维度) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) return torch.bmm(self.dropout(self.attention_weights), values) class DotProductAttention(nn.Module): - """缩放点积注意力 + """Scaled dot product attention. Defined in :numref:`subsec_additive-attention`""" 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,查询的个数) + # Shape of `queries`: (`batch_size`, no. of queries, `d`) + # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) + # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] - # 设置transpose_b=True为了交换keys的最后两个维度 + # Set `transpose_b=True` to swap the last two dimensions of `keys` scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d) self.attention_weights = masked_softmax(scores, valid_lens) return torch.bmm(self.dropout(self.attention_weights), values) class AttentionDecoder(d2l.Decoder): - """带有注意力机制解码器的基本接口 + """The base attention-based decoder interface. Defined in :numref:`sec_seq2seq_attention`""" def __init__(self, **kwargs): @@ -1172,7 +1213,7 @@ def attention_weights(self): raise NotImplementedError class MultiHeadAttention(nn.Module): - """多头注意力 + """Multi-head attention. Defined in :numref:`sec_multihead-attention`""" def __init__(self, key_size, query_size, value_size, num_hiddens, @@ -1186,51 +1227,56 @@ def __init__(self, key_size, query_size, value_size, num_hiddens, self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=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) + # Shape of `queries`, `keys`, or `values`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`) + # Shape of `valid_lens`: + # (`batch_size`,) or (`batch_size`, no. of queries) + # After transposing, shape of output `queries`, `keys`, or `values`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `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次, - # 然后如此复制第二项,然后诸如此类。 + # On axis 0, copy the first item (scalar or vector) for + # `num_heads` times, then copy the next item, and so on valid_lens = torch.repeat_interleave( valid_lens, repeats=self.num_heads, dim=0) - # output的形状:(batch_size*num_heads,查询的个数, - # num_hiddens/num_heads) + # Shape of `output`: (`batch_size` * `num_heads`, no. of queries, + # `num_hiddens` / `num_heads`) output = self.attention(queries, keys, values, valid_lens) - # output_concat的形状:(batch_size,查询的个数,num_hiddens) + # Shape of `output_concat`: + # (`batch_size`, no. of queries, `num_hiddens`) output_concat = transpose_output(output, self.num_heads) return self.W_o(output_concat) def transpose_qkv(X, num_heads): - """为了多注意力头的并行计算而变换形状 + """Transposition for parallel computation of multiple attention heads. Defined in :numref:`sec_multihead-attention`""" - # 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens) - # 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads, - # num_hiddens/num_heads) + # Shape of input `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`). + # Shape of output `X`: + # (`batch_size`, no. of queries or key-value pairs, `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) + # Shape of output `X`: + # (`batch_size`, `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) X = X.permute(0, 2, 1, 3) - # 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数, - # num_hiddens/num_heads) + # Shape of `output`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) return X.reshape(-1, X.shape[2], X.shape[3]) def transpose_output(X, num_heads): - """逆转transpose_qkv函数的操作 + """Reverse the operation of `transpose_qkv`. Defined in :numref:`sec_multihead-attention`""" X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) @@ -1238,13 +1284,13 @@ def transpose_output(X, num_heads): return X.reshape(X.shape[0], X.shape[1], -1) class PositionalEncoding(nn.Module): - """位置编码 + """Positional encoding. Defined in :numref:`sec_self-attention-and-positional-encoding`""" def __init__(self, num_hiddens, dropout, max_len=1000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(dropout) - # 创建一个足够长的P + # Create a long enough `P` self.P = d2l.zeros((1, max_len, num_hiddens)) X = d2l.arange(max_len, dtype=torch.float32).reshape( -1, 1) / torch.pow(10000, torch.arange( @@ -1257,7 +1303,7 @@ def forward(self, X): return self.dropout(X) class PositionWiseFFN(nn.Module): - """基于位置的前馈网络 + """Positionwise feed-forward network. Defined in :numref:`sec_transformer`""" def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs, @@ -1271,7 +1317,7 @@ def forward(self, X): return self.dense2(self.relu(self.dense1(X))) class AddNorm(nn.Module): - """残差连接后进行层规范化 + """Residual connection followed by layer normalization. Defined in :numref:`sec_transformer`""" def __init__(self, normalized_shape, dropout, **kwargs): @@ -1283,7 +1329,7 @@ def forward(self, X, Y): return self.ln(self.dropout(Y) + X) class EncoderBlock(nn.Module): - """transformer编码器块 + """Transformer encoder block. Defined in :numref:`sec_transformer`""" def __init__(self, key_size, query_size, value_size, num_hiddens, @@ -1303,7 +1349,7 @@ def forward(self, X, valid_lens): return self.addnorm2(Y, self.ffn(Y)) class TransformerEncoder(d2l.Encoder): - """transformer编码器 + """Transformer encoder. Defined in :numref:`sec_transformer`""" def __init__(self, vocab_size, key_size, query_size, value_size, @@ -1321,9 +1367,9 @@ def __init__(self, vocab_size, key_size, query_size, value_size, num_heads, dropout, use_bias)) def forward(self, X, valid_lens, *args): - # 因为位置编码值在-1和1之间, - # 因此嵌入值乘以嵌入维度的平方根进行缩放, - # 然后再与位置编码相加。 + # Since positional encoding values are between -1 and 1, the embedding + # values are multiplied by the square root of the embedding dimension + # to rescale before they are summed up 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): @@ -1337,10 +1383,10 @@ def annotate(text, xy, xytext): arrowprops=dict(arrowstyle='->')) def train_2d(trainer, steps=20, f_grad=None): - """用定制的训练机优化2D目标函数 + """Optimize a 2D objective function with a customized trainer. Defined in :numref:`subsec_gd-learningrate`""" - # s1和s2是稍后将使用的内部状态变量 + # `s1` and `s2` are internal state variables that will be used later x1, x2, s1, s2 = -5, -2, 0, 0 results = [(x1, x2)] for i in range(steps): @@ -1353,7 +1399,7 @@ def train_2d(trainer, steps=20, f_grad=None): return results def show_trace_2d(f, results): - """显示优化过程中2D变量的轨迹 + """Show the trace of 2D variables during optimization. Defined in :numref:`subsec_gd-learningrate`""" d2l.set_figsize() @@ -1379,12 +1425,12 @@ def get_data_ch11(batch_size=10, n=1500): def train_ch11(trainer_fn, states, hyperparams, data_iter, feature_dim, num_epochs=2): """Defined in :numref:`sec_minibatches`""" - # 初始化模型 + # Initialization w = torch.normal(mean=0.0, std=0.01, size=(feature_dim, 1), requires_grad=True) b = torch.zeros((1), requires_grad=True) net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss - # 训练模型 + # Train animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[0, num_epochs], ylim=[0.22, 0.35]) n, timer = 0, d2l.Timer() @@ -1404,7 +1450,7 @@ def train_ch11(trainer_fn, states, hyperparams, data_iter, def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=4): """Defined in :numref:`sec_minibatches`""" - # 初始化模型 + # Initialization net = nn.Sequential(nn.Linear(5, 1)) def init_weights(m): if type(m) == nn.Linear: @@ -1427,14 +1473,14 @@ def init_weights(m): n += X.shape[0] if n % 200 == 0: timer.stop() - # MSELoss计算平方误差时不带系数1/2 + # `MSELoss` computes squared error without the 1/2 factor animator.add(n/X.shape[0]/len(data_iter), (d2l.evaluate_loss(net, data_iter, loss) / 2,)) timer.start() print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') class Benchmark: - """用于测量运行时间""" + """For measuring running time.""" def __init__(self, description='Done'): """Defined in :numref:`sec_hybridize`""" self.description = description @@ -1447,7 +1493,7 @@ def __exit__(self, *args): print(f'{self.description}: {self.timer.stop():.4f} sec') def split_batch(X, y, devices): - """将X和y拆分到多个设备上 + """Split `X` and `y` into multiple devices. Defined in :numref:`sec_multi_gpu`""" assert X.shape[0] == y.shape[0] @@ -1455,7 +1501,7 @@ def split_batch(X, y, devices): nn.parallel.scatter(y, devices)) def resnet18(num_classes, in_channels=1): - """稍加修改的ResNet-18模型 + """A slightly modified ResNet-18 model. Defined in :numref:`sec_multi_gpu_concise`""" def resnet_block(in_channels, out_channels, num_residuals, @@ -1469,13 +1515,13 @@ def resnet_block(in_channels, out_channels, num_residuals, blk.append(d2l.Residual(out_channels, out_channels)) return nn.Sequential(*blk) - # 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层 + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer net = nn.Sequential( nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(64), nn.ReLU()) - net.add_module("resnet_block1", resnet_block( - 64, 64, 2, first_block=True)) + net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True)) net.add_module("resnet_block2", resnet_block(64, 128, 2)) net.add_module("resnet_block3", resnet_block(128, 256, 2)) net.add_module("resnet_block4", resnet_block(256, 512, 2)) @@ -1485,11 +1531,11 @@ def resnet_block(in_channels, out_channels, num_residuals, return net def train_batch_ch13(net, X, y, loss, trainer, devices): - """用多GPU进行小批量训练 + """Train for a minibatch with mutiple GPUs (defined in Chapter 13). Defined in :numref:`sec_image_augmentation`""" if isinstance(X, list): - # 微调BERT中所需(稍后讨论) + # Required for BERT fine-tuning (to be covered later) X = [x.to(devices[0]) for x in X] else: X = X.to(devices[0]) @@ -1506,7 +1552,7 @@ def train_batch_ch13(net, X, y, loss, trainer, devices): def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices=d2l.try_all_gpus()): - """用多GPU进行模型训练 + """Train a model with mutiple GPUs (defined in Chapter 13). Defined in :numref:`sec_image_augmentation`""" timer, num_batches = d2l.Timer(), len(train_iter) @@ -1514,7 +1560,8 @@ def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, legend=['train loss', 'train acc', 'test acc']) net = nn.DataParallel(net, device_ids=devices).to(devices[0]) for epoch in range(num_epochs): - # 4个维度:储存训练损失,训练准确度,实例数,特点数 + # Sum of training loss, sum of training accuracy, no. of examples, + # no. of predictions metric = d2l.Accumulator(4) for i, (features, labels) in enumerate(train_iter): timer.start() @@ -1537,7 +1584,7 @@ def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, 'fba480ffa8aa7e0febbb511d181409f899b9baa5') def box_corner_to_center(boxes): - """从(左上,右下)转换到(中间,宽度,高度) + """Convert from (upper-left, lower-right) to (center, width, height). Defined in :numref:`sec_bbox`""" x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] @@ -1549,7 +1596,7 @@ def box_corner_to_center(boxes): return boxes def box_center_to_corner(boxes): - """从(中间,宽度,高度)转换到(左上,右下) + """Convert from (center, width, height) to (upper-left, lower-right). Defined in :numref:`sec_bbox`""" cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] @@ -1561,15 +1608,18 @@ def box_center_to_corner(boxes): return boxes def bbox_to_rect(bbox, color): - """Defined in :numref:`sec_bbox`""" - # 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式: - # ((左上x,左上y),宽,高) + """Convert bounding box to matplotlib format. + + Defined in :numref:`sec_bbox`""" + # Convert the bounding box (upper-left x, upper-left y, lower-right x, + # lower-right y) format to the matplotlib format: ((upper-left x, + # upper-left y), width, height) return d2l.plt.Rectangle( xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], fill=False, edgecolor=color, linewidth=2) def multibox_prior(data, sizes, ratios): - """生成以每个像素为中心具有不同形状的锚框 + """Generate anchor boxes with different shapes centered on each pixel. Defined in :numref:`sec_anchor`""" in_height, in_width = data.shape[-2:] @@ -1577,50 +1627,50 @@ def multibox_prior(data, sizes, ratios): boxes_per_pixel = (num_sizes + num_ratios - 1) size_tensor = d2l.tensor(sizes, device=device) ratio_tensor = d2l.tensor(ratios, device=device) - - # 为了将锚点移动到像素的中心,需要设置偏移量。 - # 因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5 + # Offsets are required to move the anchor to the center of a pixel. Since + # a pixel has height=1 and width=1, we choose to offset our centers by 0.5 offset_h, offset_w = 0.5, 0.5 - steps_h = 1.0 / in_height # 在y轴上缩放步长 - steps_w = 1.0 / in_width # 在x轴上缩放步长 + steps_h = 1.0 / in_height # Scaled steps in y axis + steps_w = 1.0 / in_width # Scaled steps in x axis - # 生成锚框的所有中心点 + # Generate all center points for the anchor boxes center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w shift_y, shift_x = torch.meshgrid(center_h, center_w) shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1) - # 生成“boxes_per_pixel”个高和宽, - # 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax) + # Generate `boxes_per_pixel` number of heights and widths that are later + # used to create anchor box corner coordinates (xmin, xmax, ymin, ymax) w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]), sizes[0] * torch.sqrt(ratio_tensor[1:])))\ - * in_height / in_width # 处理矩形输入 + * in_height / in_width # Handle rectangular inputs h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]), sizes[0] / torch.sqrt(ratio_tensor[1:]))) - # 除以2来获得半高和半宽 + # Divide by 2 to get half height and half width anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat( in_height * in_width, 1) / 2 - # 每个中心点都将有“boxes_per_pixel”个锚框, - # 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次 + # Each center point will have `boxes_per_pixel` number of anchor boxes, so + # generate a grid of all anchor box centers with `boxes_per_pixel` repeats out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1).repeat_interleave(boxes_per_pixel, dim=0) output = out_grid + anchor_manipulations return output.unsqueeze(0) def show_bboxes(axes, bboxes, labels=None, colors=None): - """显示所有边界框 + """Show bounding boxes. Defined in :numref:`sec_anchor`""" - def _make_list(obj, default_values=None): + + def make_list(obj, default_values=None): if obj is None: obj = default_values elif not isinstance(obj, (list, tuple)): obj = [obj] return obj - labels = _make_list(labels) - colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c']) + labels = make_list(labels) + colors = make_list(colors, ['b', 'g', 'r', 'm', 'c']) for i, bbox in enumerate(bboxes): color = colors[i % len(colors)] rect = d2l.bbox_to_rect(d2l.numpy(bbox), color) @@ -1632,39 +1682,38 @@ def _make_list(obj, default_values=None): bbox=dict(facecolor=color, lw=0)) def box_iou(boxes1, boxes2): - """计算两个锚框或边界框列表中成对的交并比 + """Compute pairwise IoU across two lists of anchor or bounding boxes. Defined in :numref:`sec_anchor`""" 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的数量,) + # Shape of `boxes1`, `boxes2`, `areas1`, `areas2`: (no. of boxes1, 4), + # (no. of boxes2, 4), (no. of boxes1,), (no. of boxes2,) areas1 = box_area(boxes1) areas2 = box_area(boxes2) - # inter_upperlefts,inter_lowerrights,inters的形状: - # (boxes1的数量,boxes2的数量,2) + # Shape of `inter_upperlefts`, `inter_lowerrights`, `inters`: (no. of + # boxes1, no. of boxes2, 2) inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2]) inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:]) inters = (inter_lowerrights - inter_upperlefts).clamp(min=0) - # inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量) + # Shape of `inter_areas` and `union_areas`: (no. of boxes1, no. of boxes2) inter_areas = inters[:, :, 0] * inters[:, :, 1] union_areas = areas1[:, None] + areas2 - inter_areas return inter_areas / union_areas def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): - """将最接近的真实边界框分配给锚框 + """Assign closest ground-truth bounding boxes to anchor boxes. Defined in :numref:`sec_anchor`""" num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0] - # 位于第i行和第j列的元素x_ij是锚框i和真实边界框j的IoU + # Element x_ij in the i-th row and j-th column is the IoU of the anchor + # box i and the ground-truth bounding box j jaccard = box_iou(anchors, ground_truth) - # 对于每个锚框,分配的真实边界框的张量 + # Initialize the tensor to hold the assigned ground-truth bounding box for + # each anchor anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long, device=device) - # 根据阈值,决定是否分配真实边界框 + # Assign ground-truth bounding boxes according to the threshold max_ious, indices = torch.max(jaccard, dim=1) anc_i = torch.nonzero(max_ious >= 0.5).reshape(-1) box_j = indices[max_ious >= 0.5] @@ -1672,7 +1721,7 @@ def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): col_discard = torch.full((num_anchors,), -1) row_discard = torch.full((num_gt_boxes,), -1) for _ in range(num_gt_boxes): - max_idx = torch.argmax(jaccard) + max_idx = torch.argmax(jaccard) # Find the largest IoU box_idx = (max_idx % num_gt_boxes).long() anc_idx = (max_idx / num_gt_boxes).long() anchors_bbox_map[anc_idx] = box_idx @@ -1681,7 +1730,7 @@ def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): return anchors_bbox_map def offset_boxes(anchors, assigned_bb, eps=1e-6): - """对锚框偏移量的转换 + """Transform for anchor box offsets. Defined in :numref:`subsec_labeling-anchor-boxes`""" c_anc = d2l.box_corner_to_center(anchors) @@ -1692,7 +1741,7 @@ def offset_boxes(anchors, assigned_bb, eps=1e-6): return offset def multibox_target(anchors, labels): - """使用真实边界框标记锚框 + """Label anchor boxes using ground-truth bounding boxes. Defined in :numref:`subsec_labeling-anchor-boxes`""" batch_size, anchors = labels.shape[0], anchors.squeeze(0) @@ -1704,18 +1753,20 @@ def multibox_target(anchors, labels): label[:, 1:], anchors, device) bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat( 1, 4) - # 将类标签和分配的边界框坐标初始化为零 + # Initialize class labels and assigned bounding box coordinates with + # zeros class_labels = torch.zeros(num_anchors, dtype=torch.long, device=device) assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32, device=device) - # 使用真实边界框来标记锚框的类别。 - # 如果一个锚框没有被分配,我们标记其为背景(值为零) + # Label classes of anchor boxes using their assigned ground-truth + # bounding boxes. If an anchor box is not assigned any, we label its + # class as background (the value remains zero) 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 assigned_bb[indices_true] = label[bb_idx, 1:] - # 偏移量转换 + # Offset transformation offset = offset_boxes(anchors, assigned_bb) * bbox_mask batch_offset.append(offset.reshape(-1)) batch_mask.append(bbox_mask.reshape(-1)) @@ -1726,7 +1777,7 @@ def multibox_target(anchors, labels): return (bbox_offset, bbox_mask, class_labels) def offset_inverse(anchors, offset_preds): - """根据带有预测偏移量的锚框来预测边界框 + """Predict bounding boxes based on anchor boxes with predicted offsets. Defined in :numref:`subsec_labeling-anchor-boxes`""" anc = d2l.box_corner_to_center(anchors) @@ -1737,11 +1788,11 @@ def offset_inverse(anchors, offset_preds): return predicted_bbox def nms(boxes, scores, iou_threshold): - """对预测边界框的置信度进行排序 + """Sort confidence scores of predicted bounding boxes. Defined in :numref:`subsec_predicting-bounding-boxes-nms`""" B = torch.argsort(scores, dim=-1, descending=True) - keep = [] # 保留预测边界框的指标 + keep = [] # Indices of predicted bounding boxes that will be kept while B.numel() > 0: i = B[0] keep.append(i) @@ -1754,7 +1805,7 @@ def nms(boxes, scores, iou_threshold): def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, pos_threshold=0.009999999): - """使用非极大值抑制来预测边界框 + """Predict bounding boxes using non-maximum suppression. Defined in :numref:`subsec_predicting-bounding-boxes-nms`""" device, batch_size = cls_probs.device, cls_probs.shape[0] @@ -1766,8 +1817,7 @@ def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, conf, class_id = torch.max(cls_prob[1:], 0) predicted_bb = offset_inverse(anchors, offset_pred) keep = nms(predicted_bb, conf, nms_threshold) - - # 找到所有的non_keep索引,并将类设置为背景 + # Find all non-`keep` indices and set the class to background all_idx = torch.arange(num_anchors, dtype=torch.long, device=device) combined = torch.cat((keep, all_idx)) uniques, counts = combined.unique(return_counts=True) @@ -1776,7 +1826,8 @@ def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, class_id[non_keep] = -1 class_id = class_id[all_id_sorted] conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted] - # pos_threshold是一个用于非背景预测的阈值 + # Here `pos_threshold` is a threshold for positive (non-background) + # predictions below_min_idx = (conf < pos_threshold) class_id[below_min_idx] = -1 conf[below_min_idx] = 1 - conf[below_min_idx] @@ -1791,7 +1842,7 @@ def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, '5de26c8fce5ccdea9f91267273464dc968d20d72') def read_data_bananas(is_train=True): - """读取香蕉检测数据集中的图像和标签 + """Read the banana detection dataset images and labels. Defined in :numref:`sec_object-detection-dataset`""" data_dir = d2l.download_extract('banana-detection') @@ -1804,13 +1855,14 @@ def read_data_bananas(is_train=True): images.append(torchvision.io.read_image( os.path.join(data_dir, 'bananas_train' if is_train else 'bananas_val', 'images', f'{img_name}'))) - # 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y), - # 其中所有图像都具有相同的香蕉类(索引为0) + # Here `target` contains (class, upper-left x, upper-left y, + # lower-right x, lower-right y), where all the images have the same + # banana class (index 0) targets.append(list(target)) return images, torch.tensor(targets).unsqueeze(1) / 256 class BananasDataset(torch.utils.data.Dataset): - """一个用于加载香蕉检测数据集的自定义数据集 + """A customized dataset to load the banana detection dataset. Defined in :numref:`sec_object-detection-dataset`""" def __init__(self, is_train): @@ -1825,7 +1877,7 @@ def __len__(self): return len(self.features) def load_data_bananas(batch_size): - """加载香蕉检测数据集 + """Load the banana detection dataset. Defined in :numref:`sec_object-detection-dataset`""" train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True), @@ -1838,7 +1890,7 @@ def load_data_bananas(batch_size): '4e443f8a2eca6b1dac8a6c57641b67dd40621a49') def read_voc_images(voc_dir, is_train=True): - """读取所有VOC图像并标注 + """Read all VOC feature and label images. Defined in :numref:`sec_semantic_segmentation`""" txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation', @@ -1867,7 +1919,7 @@ def read_voc_images(voc_dir, is_train=True): 'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor'] def voc_colormap2label(): - """构建从RGB到VOC类别索引的映射 + """Build the mapping from RGB to class indices for VOC labels. Defined in :numref:`sec_semantic_segmentation`""" colormap2label = torch.zeros(256 ** 3, dtype=torch.long) @@ -1877,7 +1929,7 @@ def voc_colormap2label(): return colormap2label def voc_label_indices(colormap, colormap2label): - """将VOC标签中的RGB值映射到它们的类别索引 + """Map any RGB values in VOC labels to their class indices. Defined in :numref:`sec_semantic_segmentation`""" colormap = colormap.permute(1, 2, 0).numpy().astype('int32') @@ -1886,7 +1938,7 @@ def voc_label_indices(colormap, colormap2label): return colormap2label[idx] def voc_rand_crop(feature, label, height, width): - """随机裁剪特征和标签图像 + """Randomly crop both feature and label images. Defined in :numref:`sec_semantic_segmentation`""" rect = torchvision.transforms.RandomCrop.get_params( @@ -1896,7 +1948,7 @@ def voc_rand_crop(feature, label, height, width): return feature, label class VOCSegDataset(torch.utils.data.Dataset): - """一个用于加载VOC数据集的自定义数据集 + """A customized dataset to load the VOC dataset. Defined in :numref:`sec_semantic_segmentation`""" @@ -1928,7 +1980,7 @@ def __len__(self): return len(self.features) def load_data_voc(batch_size, crop_size): - """加载VOC语义分割数据集 + """Load the VOC semantic segmentation dataset. Defined in :numref:`sec_semantic_segmentation`""" voc_dir = d2l.download_extract('voc2012', os.path.join( @@ -1946,29 +1998,30 @@ def load_data_voc(batch_size, crop_size): '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') def read_csv_labels(fname): - """读取fname来给标签字典返回一个文件名 + """Read `fname` to return a filename to label dictionary. Defined in :numref:`sec_kaggle_cifar10`""" with open(fname, 'r') as f: - # 跳过文件头行(列名) + # Skip the file header line (column name) lines = f.readlines()[1:] tokens = [l.rstrip().split(',') for l in lines] return dict(((name, label) for name, label in tokens)) def copyfile(filename, target_dir): - """将文件复制到目标目录 + """Copy a file into a target directory. Defined in :numref:`sec_kaggle_cifar10`""" os.makedirs(target_dir, exist_ok=True) shutil.copy(filename, target_dir) def reorg_train_valid(data_dir, labels, valid_ratio): - """将验证集从原始的训练集中拆分出来 + """Split the validation set out of the original training set. Defined in :numref:`sec_kaggle_cifar10`""" - # 训练数据集中样本最少的类别中的样本数 + # The number of examples of the class that has the fewest examples in the + # training dataset n = collections.Counter(labels.values()).most_common()[-1][1] - # 验证集中每个类别的样本数 + # The number of examples per class for the validation set n_valid_per_label = max(1, math.floor(n * valid_ratio)) label_count = {} for train_file in os.listdir(os.path.join(data_dir, 'train')): @@ -1986,7 +2039,7 @@ def reorg_train_valid(data_dir, labels, valid_ratio): return n_valid_per_label def reorg_test(data_dir): - """在预测期间整理测试集,以方便读取 + """Organize the testing set for data loading during prediction. Defined in :numref:`sec_kaggle_cifar10`""" for test_file in os.listdir(os.path.join(data_dir, 'test')): @@ -2001,26 +2054,26 @@ def reorg_test(data_dir): '319d85e578af0cdc590547f26231e4e31cdf1e42') def read_ptb(): - """将PTB数据集加载到文本行的列表中 + """Load the PTB dataset into a list of text lines. Defined in :numref:`sec_word2vec_data`""" data_dir = d2l.download_extract('ptb') - # Readthetrainingset. + # Read the training set. with open(os.path.join(data_dir, 'ptb.train.txt')) as f: raw_text = f.read() return [line.split() for line in raw_text.split('\n')] def subsample(sentences, vocab): - """下采样高频词 + """Subsample high-frequency words. Defined in :numref:`sec_word2vec_data`""" - # 排除未知词元'' + # Exclude unknown tokens '' sentences = [[token for token in line if vocab[token] != vocab.unk] for line in sentences] counter = d2l.count_corpus(sentences) num_tokens = sum(counter.values()) - # 如果在下采样期间保留词元,则返回True + # Return True if `token` is kept during subsampling def keep(token): return(random.uniform(0, 1) < math.sqrt(1e-4 / counter[token] * num_tokens)) @@ -2029,26 +2082,27 @@ def keep(token): counter) def get_centers_and_contexts(corpus, max_window_size): - """返回跳元模型中的中心词和上下文词 + """Return center words and context words in skip-gram. Defined in :numref:`sec_word2vec_data`""" centers, contexts = [], [] for line in corpus: - # 要形成“中心词-上下文词”对,每个句子至少需要有2个词 + # To form a "center word--context word" pair, each sentence needs to + # have at least 2 words if len(line) < 2: continue centers += line - for i in range(len(line)): # 上下文窗口中间i + for i in range(len(line)): # Context window centered at `i` window_size = random.randint(1, max_window_size) indices = list(range(max(0, i - window_size), min(len(line), i + 1 + window_size))) - # 从上下文词中排除中心词 + # Exclude the center word from the context words indices.remove(i) contexts.append([line[idx] for idx in indices]) return centers, contexts class RandomGenerator: - """根据n个采样权重在{1,...,n}中随机抽取""" + """Randomly draw among {1, ..., n} according to n sampling weights.""" def __init__(self, sampling_weights): """Defined in :numref:`sec_word2vec_data`""" # Exclude @@ -2059,21 +2113,19 @@ def __init__(self, sampling_weights): def draw(self): if self.i == len(self.candidates): - # 缓存k个随机采样结果 + # Cache `k` random sampling results self.candidates = random.choices( self.population, self.sampling_weights, k=10000) self.i = 0 self.i += 1 return self.candidates[self.i - 1] -generator = RandomGenerator([2, 3, 4]) -[generator.draw() for _ in range(10)] - def get_negatives(all_contexts, vocab, counter, K): - """返回负采样中的噪声词 + """Return noise words in negative sampling. Defined in :numref:`sec_word2vec_data`""" - # 索引为1、2、...(索引0是词表中排除的未知标记) + # Sampling weights for words with indices 1, 2, ... (index 0 is the + # excluded unknown token) in the vocabulary sampling_weights = [counter[vocab.to_tokens(i)]**0.75 for i in range(1, len(vocab))] all_negatives, generator = [], RandomGenerator(sampling_weights) @@ -2081,14 +2133,14 @@ def get_negatives(all_contexts, vocab, counter, K): negatives = [] while len(negatives) < len(contexts) * K: neg = generator.draw() - # 噪声词不能是上下文词 + # Noise words cannot be context words if neg not in contexts: negatives.append(neg) all_negatives.append(negatives) return all_negatives def batchify(data): - """返回带有负采样的跳元模型的小批量样本 + """Return a minibatch of examples for skip-gram with negative sampling. Defined in :numref:`sec_word2vec_data`""" max_len = max(len(c) + len(n) for _, c, n in data) @@ -2096,15 +2148,14 @@ def batchify(data): for center, context, negative in data: cur_len = len(context) + len(negative) centers += [center] - contexts_negatives += \ - [context + negative + [0] * (max_len - cur_len)] + contexts_negatives += [context + negative + [0] * (max_len - cur_len)] masks += [[1] * cur_len + [0] * (max_len - cur_len)] labels += [[1] * len(context) + [0] * (max_len - len(context))] return (d2l.reshape(d2l.tensor(centers), (-1, 1)), d2l.tensor( contexts_negatives), d2l.tensor(masks), d2l.tensor(labels)) def load_data_ptb(batch_size, max_window_size, num_noise_words): - """下载PTB数据集,然后将其加载到内存中 + """Download the PTB dataset and then load it into memory. Defined in :numref:`subsec_word2vec-minibatch-loading`""" num_workers = d2l.get_dataloader_workers() @@ -2133,9 +2184,9 @@ def __len__(self): dataset = PTBDataset(all_centers, all_contexts, all_negatives) - data_iter = torch.utils.data.DataLoader( - dataset, batch_size, shuffle=True, - collate_fn=batchify, num_workers=num_workers) + data_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True, + collate_fn=batchify, + num_workers=num_workers) return data_iter, vocab d2l.DATA_HUB['glove.6b.50d'] = (d2l.DATA_URL + 'glove.6B.50d.zip', @@ -2151,7 +2202,7 @@ def __len__(self): 'c1816da3821ae9f43899be655002f6c723e91b88') class TokenEmbedding: - """GloVe嵌入""" + """Token Embedding.""" def __init__(self, embedding_name): """Defined in :numref:`sec_synonyms`""" self.idx_to_token, self.idx_to_vec = self._load_embedding( @@ -2163,13 +2214,13 @@ def __init__(self, embedding_name): def _load_embedding(self, embedding_name): idx_to_token, idx_to_vec = [''], [] data_dir = d2l.download_extract(embedding_name) - # GloVe网站:https://nlp.stanford.edu/projects/glove/ - # fastText网站:https://fasttext.cc/ + # GloVe website: https://nlp.stanford.edu/projects/glove/ + # fastText website: https://fasttext.cc/ with open(os.path.join(data_dir, 'vec.txt'), 'r') as f: for line in f: elems = line.rstrip().split(' ') token, elems = elems[0], [float(elem) for elem in elems[1:]] - # 跳过标题信息,例如fastText中的首行 + # Skip header information, such as the top row in fastText if len(elems) > 1: idx_to_token.append(token) idx_to_vec.append(elems) @@ -2186,11 +2237,11 @@ def __len__(self): return len(self.idx_to_token) def get_tokens_and_segments(tokens_a, tokens_b=None): - """获取输入序列的词元及其片段索引 + """Get tokens of the BERT input sequence and their segment IDs. Defined in :numref:`sec_bert`""" tokens = [''] + tokens_a + [''] - # 0和1分别标记片段A和B + # 0 and 1 are marking segment A and B, respectively segments = [0] * (len(tokens_a) + 2) if tokens_b is not None: tokens += tokens_b + [''] @@ -2198,7 +2249,7 @@ def get_tokens_and_segments(tokens_a, tokens_b=None): return tokens, segments class BERTEncoder(nn.Module): - """BERT编码器 + """BERT encoder. Defined in :numref:`subsec_bert_input_rep`""" def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, @@ -2213,12 +2264,14 @@ def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, self.blks.add_module(f"{i}", d2l.EncoderBlock( key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, dropout, True)) - # 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数 + # In BERT, positional embeddings are learnable, thus we create a + # parameter of positional embeddings that are long enough self.pos_embedding = nn.Parameter(torch.randn(1, max_len, num_hiddens)) def forward(self, tokens, segments, valid_lens): - # 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens) + # Shape of `X` remains unchanged in the following code snippet: + # (batch size, max sequence length, `num_hiddens`) X = self.token_embedding(tokens) + self.segment_embedding(segments) X = X + self.pos_embedding.data[:, :X.shape[1], :] for blk in self.blks: @@ -2226,7 +2279,7 @@ def forward(self, tokens, segments, valid_lens): return X class MaskLM(nn.Module): - """BERT的掩蔽语言模型任务 + """The masked language model task of BERT. Defined in :numref:`subsec_bert_input_rep`""" def __init__(self, vocab_size, num_hiddens, num_inputs=768, **kwargs): @@ -2241,8 +2294,8 @@ def forward(self, X, pred_positions): pred_positions = pred_positions.reshape(-1) batch_size = X.shape[0] batch_idx = torch.arange(0, batch_size) - # 假设batch_size=2,num_pred_positions=3 - # 那么batch_idx是np.array([0,0,0,1,1]) + # Suppose that `batch_size` = 2, `num_pred_positions` = 3, then + # `batch_idx` is `torch.tensor([0, 0, 0, 1, 1, 1])` batch_idx = torch.repeat_interleave(batch_idx, num_pred_positions) masked_X = X[batch_idx, pred_positions] masked_X = masked_X.reshape((batch_size, num_pred_positions, -1)) @@ -2250,7 +2303,7 @@ def forward(self, X, pred_positions): return mlm_Y_hat class NextSentencePred(nn.Module): - """BERT的下一句预测任务 + """The next sentence prediction task of BERT. Defined in :numref:`subsec_mlm`""" def __init__(self, num_inputs, **kwargs): @@ -2258,11 +2311,11 @@ def __init__(self, num_inputs, **kwargs): self.output = nn.Linear(num_inputs, 2) def forward(self, X): - # X的形状:(batchsize,num_hiddens) + # `X` shape: (batch size, `num_hiddens`) return self.output(X) class BERTModel(nn.Module): - """BERT模型 + """The BERT model. Defined in :numref:`subsec_nsp`""" def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, @@ -2280,14 +2333,14 @@ def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, self.mlm = MaskLM(vocab_size, num_hiddens, mlm_in_features) self.nsp = NextSentencePred(nsp_in_features) - def forward(self, tokens, segments, valid_lens=None, - pred_positions=None): + def forward(self, tokens, segments, valid_lens=None, pred_positions=None): encoded_X = self.encoder(tokens, segments, valid_lens) if pred_positions is not None: mlm_Y_hat = self.mlm(encoded_X, pred_positions) else: mlm_Y_hat = None - # 用于下一句预测的多层感知机分类器的隐藏层,0是“”标记的索引 + # The hidden layer of the MLP classifier for next sentence prediction. + # 0 is the index of the '' token nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :])) return encoded_X, mlm_Y_hat, nsp_Y_hat @@ -2300,7 +2353,7 @@ def _read_wiki(data_dir): file_name = os.path.join(data_dir, 'wiki.train.tokens') with open(file_name, 'r') as f: lines = f.readlines() - # 大写字母转换为小写字母 + # Uppercase letters are converted to lowercase ones paragraphs = [line.strip().lower().split(' . ') for line in lines if len(line.split(' . ')) >= 2] random.shuffle(paragraphs) @@ -2311,7 +2364,7 @@ def _get_next_sentence(sentence, next_sentence, paragraphs): if random.random() < 0.5: is_next = True else: - # paragraphs是三重列表的嵌套 + # `paragraphs` is a list of lists of lists next_sentence = random.choice(random.choice(paragraphs)) is_next = False return sentence, next_sentence, is_next @@ -2322,7 +2375,7 @@ def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len): for i in range(len(paragraph) - 1): tokens_a, tokens_b, is_next = _get_next_sentence( paragraph[i], paragraph[i + 1], paragraphs) - # 考虑1个''词元和2个''词元 + # Consider 1 '' token and 2 '' tokens if len(tokens_a) + len(tokens_b) + 3 > max_len: continue tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b) @@ -2332,23 +2385,25 @@ def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len): def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds, vocab): """Defined in :numref:`sec_bert-dataset`""" - # 为遮蔽语言模型的输入创建新的词元副本,其中输入可能包含替换的“”或随机词元 + # Make a new copy of tokens for the input of a masked language model, + # where the input may contain replaced '' or random tokens mlm_input_tokens = [token for token in tokens] pred_positions_and_labels = [] - # 打乱后用于在遮蔽语言模型任务中获取15%的随机词元进行预测 + # Shuffle for getting 15% random tokens for prediction in the masked + # language modeling task random.shuffle(candidate_pred_positions) for mlm_pred_position in candidate_pred_positions: if len(pred_positions_and_labels) >= num_mlm_preds: break masked_token = None - # 80%的时间:将词替换为“”词元 + # 80% of the time: replace the word with the '' token if random.random() < 0.8: masked_token = '' else: - # 10%的时间:保持词不变 + # 10% of the time: keep the word unchanged if random.random() < 0.5: masked_token = tokens[mlm_pred_position] - # 10%的时间:用随机词替换该词 + # 10% of the time: replace the word with a random word else: masked_token = random.choice(vocab.idx_to_token) mlm_input_tokens[mlm_pred_position] = masked_token @@ -2359,13 +2414,14 @@ def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds, def _get_mlm_data_from_tokens(tokens, vocab): """Defined in :numref:`subsec_prepare_mlm_data`""" candidate_pred_positions = [] - # tokens是一个字符串列表 + # `tokens` is a list of strings for i, token in enumerate(tokens): - # 在遮蔽语言模型任务中不会预测特殊词元 + # Special tokens are not predicted in the masked language modeling + # task if token in ['', '']: continue candidate_pred_positions.append(i) - # 遮蔽语言模型任务中预测15%的随机词元 + # 15% of random tokens are predicted in the masked language modeling task num_mlm_preds = max(1, round(len(tokens) * 0.15)) mlm_input_tokens, pred_positions_and_labels = _replace_mlm_tokens( tokens, candidate_pred_positions, num_mlm_preds, vocab) @@ -2387,11 +2443,12 @@ def _pad_bert_inputs(examples, max_len, vocab): max_len - len(token_ids)), dtype=torch.long)) all_segments.append(torch.tensor(segments + [0] * ( max_len - len(segments)), dtype=torch.long)) - # valid_lens不包括''的计数 + # `valid_lens` excludes count of '' tokens valid_lens.append(torch.tensor(len(token_ids), dtype=torch.float32)) all_pred_positions.append(torch.tensor(pred_positions + [0] * ( max_num_mlm_preds - len(pred_positions)), dtype=torch.long)) - # 填充词元的预测将通过乘以0权重在损失中过滤掉 + # Predictions of padded tokens will be filtered out in the loss via + # multiplication of 0 weights all_mlm_weights.append( torch.tensor([1.0] * len(mlm_pred_label_ids) + [0.0] * ( max_num_mlm_preds - len(pred_positions)), @@ -2405,24 +2462,25 @@ def _pad_bert_inputs(examples, max_len, vocab): class _WikiTextDataset(torch.utils.data.Dataset): """Defined in :numref:`subsec_prepare_mlm_data`""" def __init__(self, paragraphs, max_len): - # 输入paragraphs[i]是代表段落的句子字符串列表; - # 而输出paragraphs[i]是代表段落的句子列表,其中每个句子都是词元列表 + # Input `paragraphs[i]` is a list of sentence strings representing a + # paragraph; while output `paragraphs[i]` is a list of sentences + # representing a paragraph, where each sentence is a list of tokens paragraphs = [d2l.tokenize( paragraph, token='word') for paragraph in paragraphs] sentences = [sentence for paragraph in paragraphs for sentence in paragraph] self.vocab = d2l.Vocab(sentences, min_freq=5, reserved_tokens=[ '', '', '', '']) - # 获取下一句子预测任务的数据 + # Get data for the next sentence prediction task examples = [] for paragraph in paragraphs: examples.extend(_get_nsp_data_from_paragraph( paragraph, paragraphs, self.vocab, max_len)) - # 获取遮蔽语言模型任务的数据 + # Get data for the masked language model task examples = [(_get_mlm_data_from_tokens(tokens, self.vocab) + (segments, is_next)) for tokens, segments, is_next in examples] - # 填充输入 + # Pad inputs (self.all_token_ids, self.all_segments, self.valid_lens, self.all_pred_positions, self.all_mlm_weights, self.all_mlm_labels, self.nsp_labels) = _pad_bert_inputs( @@ -2438,7 +2496,7 @@ def __len__(self): return len(self.all_token_ids) def load_data_wiki(batch_size, max_len): - """加载WikiText-2数据集 + """Load the WikiText-2 dataset. Defined in :numref:`subsec_prepare_mlm_data`""" num_workers = d2l.get_dataloader_workers() @@ -2454,15 +2512,15 @@ def _get_batch_loss_bert(net, loss, vocab_size, tokens_X, pred_positions_X, mlm_weights_X, mlm_Y, nsp_y): """Defined in :numref:`sec_bert-pretraining`""" - # 前向传播 + # Forward pass _, mlm_Y_hat, nsp_Y_hat = net(tokens_X, segments_X, valid_lens_x.reshape(-1), pred_positions_X) - # 计算遮蔽语言模型损失 + # Compute masked language model loss mlm_l = loss(mlm_Y_hat.reshape(-1, vocab_size), mlm_Y.reshape(-1)) *\ mlm_weights_X.reshape(-1, 1) mlm_l = mlm_l.sum() / (mlm_weights_X.sum() + 1e-8) - # 计算下一句子预测任务的损失 + # Compute next sentence prediction loss nsp_l = loss(nsp_Y_hat, nsp_y) l = mlm_l + nsp_l return mlm_l, nsp_l, l @@ -2472,7 +2530,7 @@ def _get_batch_loss_bert(net, loss, vocab_size, tokens_X, '01ada507287d82875905620988597833ad4e0903') def read_imdb(data_dir, is_train): - """读取IMDb评论数据集文本序列和标签 + """Read the IMDb review dataset text sequences and labels. Defined in :numref:`sec_sentiment`""" data, labels = [], [] @@ -2487,7 +2545,7 @@ def read_imdb(data_dir, is_train): return data, labels def load_data_imdb(batch_size, num_steps=500): - """返回数据迭代器和IMDb评论数据集的词表 + """Return data iterators and the vocabulary of the IMDb review dataset. Defined in :numref:`sec_sentiment`""" data_dir = d2l.download_extract('aclImdb', 'aclImdb') @@ -2508,7 +2566,7 @@ def load_data_imdb(batch_size, num_steps=500): return train_iter, test_iter, vocab def predict_sentiment(net, vocab, sequence): - """预测文本序列的情感 + """Predict the sentiment of a text sequence. Defined in :numref:`sec_sentiment_rnn`""" sequence = torch.tensor(vocab[sequence.split()], device=d2l.try_gpu()) @@ -2520,14 +2578,14 @@ def predict_sentiment(net, vocab, sequence): '9fcde07509c7e87ec61c640c1b2753d9041758e4') def read_snli(data_dir, is_train): - """将SNLI数据集解析为前提、假设和标签 + """Read the SNLI dataset into premises, hypotheses, and labels. Defined in :numref:`sec_natural-language-inference-and-dataset`""" def extract_text(s): - # 删除我们不会使用的信息 + # Remove information that will not be used by us s = re.sub('\\(', '', s) s = re.sub('\\)', '', s) - # 用一个空格替换两个或多个连续的空格 + # Substitute two or more consecutive whitespace with space s = re.sub('\\s{2,}', ' ', s) return s.strip() label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2} @@ -2536,13 +2594,12 @@ def extract_text(s): with open(file_name, 'r') as f: rows = [row.split('\t') for row in f.readlines()[1:]] premises = [extract_text(row[1]) for row in rows if row[0] in label_set] - hypotheses = [extract_text(row[2]) for row in rows if row[0] \ - in label_set] + hypotheses = [extract_text(row[2]) for row in rows if row[0] in label_set] labels = [label_set[row[0]] for row in rows if row[0] in label_set] return premises, hypotheses, labels class SNLIDataset(torch.utils.data.Dataset): - """用于加载SNLI数据集的自定义数据集 + """A customized dataset to load the SNLI dataset. Defined in :numref:`sec_natural-language-inference-and-dataset`""" def __init__(self, dataset, num_steps, vocab=None): @@ -2550,8 +2607,8 @@ def __init__(self, dataset, num_steps, vocab=None): all_premise_tokens = d2l.tokenize(dataset[0]) all_hypothesis_tokens = d2l.tokenize(dataset[1]) if vocab is None: - self.vocab = d2l.Vocab(all_premise_tokens + \ - all_hypothesis_tokens, min_freq=5, reserved_tokens=['']) + self.vocab = d2l.Vocab(all_premise_tokens + all_hypothesis_tokens, + min_freq=5, reserved_tokens=['']) else: self.vocab = vocab self.premises = self._pad(all_premise_tokens) @@ -2571,7 +2628,7 @@ def __len__(self): return len(self.premises) def load_data_snli(batch_size, num_steps=50): - """下载SNLI数据集并返回数据迭代器和词表 + """Download the SNLI dataset and return data iterators and vocabulary. Defined in :numref:`sec_natural-language-inference-and-dataset`""" num_workers = d2l.get_dataloader_workers() @@ -2589,7 +2646,7 @@ def load_data_snli(batch_size, num_steps=50): return train_iter, test_iter, train_set.vocab def predict_snli(net, vocab, premise, hypothesis): - """预测前提和假设之间的逻辑关系 + """Predict the logical relationship between the premise and hypothesis. Defined in :numref:`sec_natural-language-inference-attention`""" net.eval() @@ -2598,8 +2655,46 @@ def predict_snli(net, vocab, premise, hypothesis): label = torch.argmax(net([premise.reshape((1, -1)), hypothesis.reshape((1, -1))]), dim=1) return 'entailment' if label == 0 else 'contradiction' if label == 1 \ - else 'neutral'# Alias defined in config.ini -nn_Module = nn.Module + else 'neutral' + +def update_D(X, Z, net_D, net_G, loss, trainer_D): + """Update discriminator. + + Defined in :numref:`sec_basic_gan`""" + batch_size = X.shape[0] + ones = torch.ones((batch_size,), device=X.device) + zeros = torch.zeros((batch_size,), device=X.device) + trainer_D.zero_grad() + real_Y = net_D(X) + fake_X = net_G(Z) + # Do not need to compute gradient for `net_G`, detach it from + # computing gradients. + fake_Y = net_D(fake_X.detach()) + loss_D = (loss(real_Y, ones.reshape(real_Y.shape)) + + loss(fake_Y, zeros.reshape(fake_Y.shape))) / 2 + loss_D.backward() + trainer_D.step() + return loss_D + +def update_G(Z, net_D, net_G, loss, trainer_G): + """Update generator. + + Defined in :numref:`sec_basic_gan`""" + batch_size = Z.shape[0] + ones = torch.ones((batch_size,), device=Z.device) + trainer_G.zero_grad() + # We could reuse `fake_X` from `update_D` to save computation + fake_X = net_G(Z) + # Recomputing `fake_Y` is needed since `net_D` is changed + fake_Y = net_D(fake_X) + loss_G = loss(fake_Y, ones.reshape(fake_Y.shape)) + loss_G.backward() + trainer_G.step() + return loss_G + +d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip', + 'c065c0e2593b8b161a2d7873e42418bf6a21106c')# Alias defined in config.ini + ones = torch.ones zeros = torch.zeros @@ -2616,7 +2711,6 @@ def predict_snli(net, vocab, premise, hypothesis): log = torch.log normal = torch.normal rand = torch.rand -randn = torch.randn matmul = torch.matmul int32 = torch.int32 float32 = torch.float32 @@ -2632,5 +2726,4 @@ def predict_snli(net, vocab, premise, hypothesis): argmax = lambda x, *args, **kwargs: x.argmax(*args, **kwargs) astype = lambda x, *args, **kwargs: x.type(*args, **kwargs) transpose = lambda x, *args, **kwargs: x.t(*args, **kwargs) -reduce_mean = lambda x, *args, **kwargs: x.mean(*args, **kwargs) diff --git a/graffle/appendix/3dFunc.graffle b/graffle/appendix/3dFunc.graffle new file mode 100644 index 000000000..d815c780e Binary files /dev/null and b/graffle/appendix/3dFunc.graffle differ diff --git a/graffle/appendix/ChainNet1.graffle b/graffle/appendix/ChainNet1.graffle new file mode 100644 index 000000000..455f04965 Binary files /dev/null and b/graffle/appendix/ChainNet1.graffle differ diff --git a/graffle/appendix/ChainNet2.graffle b/graffle/appendix/ChainNet2.graffle new file mode 100644 index 000000000..7da983092 Binary files /dev/null and b/graffle/appendix/ChainNet2.graffle differ diff --git a/graffle/appendix/GridPoints.graffle b/graffle/appendix/GridPoints.graffle new file mode 100644 index 000000000..3b217d934 Binary files /dev/null and b/graffle/appendix/GridPoints.graffle differ diff --git a/graffle/appendix/GridTransform.graffle b/graffle/appendix/GridTransform.graffle new file mode 100644 index 000000000..7f0d72f4b Binary files /dev/null and b/graffle/appendix/GridTransform.graffle differ diff --git a/graffle/appendix/GridTransformFilled.graffle b/graffle/appendix/GridTransformFilled.graffle new file mode 100644 index 000000000..d26171e65 Binary files /dev/null and b/graffle/appendix/GridTransformFilled.graffle differ diff --git a/graffle/appendix/GridWithArrow.graffle b/graffle/appendix/GridWithArrow.graffle new file mode 100644 index 000000000..653d41d45 Binary files /dev/null and b/graffle/appendix/GridWithArrow.graffle differ diff --git a/graffle/appendix/Marginal.graffle b/graffle/appendix/Marginal.graffle new file mode 100644 index 000000000..d94e3cd4d Binary files /dev/null and b/graffle/appendix/Marginal.graffle differ diff --git a/graffle/appendix/ParVec.graffle b/graffle/appendix/ParVec.graffle new file mode 100644 index 000000000..3390ef27d Binary files /dev/null and b/graffle/appendix/ParVec.graffle differ diff --git a/graffle/appendix/ProjVec.graffle b/graffle/appendix/ProjVec.graffle new file mode 100644 index 000000000..c854099c1 Binary files /dev/null and b/graffle/appendix/ProjVec.graffle differ diff --git a/graffle/appendix/RectTrans.graffle b/graffle/appendix/RectTrans.graffle new file mode 100644 index 000000000..c375abc70 Binary files /dev/null and b/graffle/appendix/RectTrans.graffle differ diff --git a/graffle/appendix/SpaceDivision.graffle b/graffle/appendix/SpaceDivision.graffle new file mode 100644 index 000000000..bf1eccd5c Binary files /dev/null and b/graffle/appendix/SpaceDivision.graffle differ diff --git a/graffle/appendix/SpaceDivision3D.graffle b/graffle/appendix/SpaceDivision3D.graffle new file mode 100644 index 000000000..6f73c4f43 Binary files /dev/null and b/graffle/appendix/SpaceDivision3D.graffle differ diff --git a/graffle/appendix/SubArea.graffle b/graffle/appendix/SubArea.graffle new file mode 100644 index 000000000..a7f53987b Binary files /dev/null and b/graffle/appendix/SubArea.graffle differ diff --git a/graffle/appendix/SumOrder.graffle b/graffle/appendix/SumOrder.graffle new file mode 100644 index 000000000..f7a282a2d Binary files /dev/null and b/graffle/appendix/SumOrder.graffle differ diff --git a/graffle/appendix/VecAdd.graffle b/graffle/appendix/VecAdd.graffle new file mode 100644 index 000000000..06fafd632 Binary files /dev/null and b/graffle/appendix/VecAdd.graffle differ diff --git a/graffle/appendix/VecAngle.graffle b/graffle/appendix/VecAngle.graffle new file mode 100644 index 000000000..e9c8c21cd Binary files /dev/null and b/graffle/appendix/VecAngle.graffle differ diff --git a/graffle/appendix/comparing_estimators.graffle b/graffle/appendix/comparing_estimators.graffle new file mode 100644 index 000000000..9cff5f33e Binary files /dev/null and b/graffle/appendix/comparing_estimators.graffle differ diff --git a/graffle/appendix/mutual_information.graffle b/graffle/appendix/mutual_information.graffle new file mode 100644 index 000000000..17a3fe208 Binary files /dev/null and b/graffle/appendix/mutual_information.graffle differ diff --git a/graffle/appendix/negSecDer.graffle b/graffle/appendix/negSecDer.graffle new file mode 100644 index 000000000..60c4f7098 Binary files /dev/null and b/graffle/appendix/negSecDer.graffle differ diff --git a/graffle/appendix/posSecDer.graffle b/graffle/appendix/posSecDer.graffle new file mode 100644 index 000000000..f9ee6985a Binary files /dev/null and b/graffle/appendix/posSecDer.graffle differ diff --git a/graffle/appendix/statistical_power.graffle b/graffle/appendix/statistical_power.graffle new file mode 100644 index 000000000..65c139530 Binary files /dev/null and b/graffle/appendix/statistical_power.graffle differ diff --git a/graffle/appendix/statistical_significance.graffle b/graffle/appendix/statistical_significance.graffle new file mode 100644 index 000000000..b0aca28cf Binary files /dev/null and b/graffle/appendix/statistical_significance.graffle differ diff --git a/graffle/appendix/zeroSecDer.graffle b/graffle/appendix/zeroSecDer.graffle new file mode 100644 index 000000000..72446b0f0 Binary files /dev/null and b/graffle/appendix/zeroSecDer.graffle differ diff --git a/graffle/attention/add_norm.graffle b/graffle/attention/add_norm.graffle new file mode 100644 index 000000000..1a0c4a754 Binary files /dev/null and b/graffle/attention/add_norm.graffle differ diff --git a/graffle/attention/attention-output.graffle b/graffle/attention/attention-output.graffle new file mode 100644 index 000000000..f2e06a60a Binary files /dev/null and b/graffle/attention/attention-output.graffle differ diff --git a/graffle/attention/attention.graffle b/graffle/attention/attention.graffle new file mode 100644 index 000000000..61143ef35 Binary files /dev/null and b/graffle/attention/attention.graffle differ diff --git a/graffle/attention/cnn-rnn-self-attention.graffle b/graffle/attention/cnn-rnn-self-attention.graffle new file mode 100644 index 000000000..e37f6d784 Binary files /dev/null and b/graffle/attention/cnn-rnn-self-attention.graffle differ diff --git a/graffle/attention/encoder-decoder.graffle b/graffle/attention/encoder-decoder.graffle new file mode 100644 index 000000000..0cd77d323 Binary files /dev/null and b/graffle/attention/encoder-decoder.graffle differ diff --git a/graffle/attention/eye-book.graffle b/graffle/attention/eye-book.graffle new file mode 100644 index 000000000..747f7f255 Binary files /dev/null and b/graffle/attention/eye-book.graffle differ diff --git a/graffle/attention/eye-coffee.graffle b/graffle/attention/eye-coffee.graffle new file mode 100644 index 000000000..b33d51bc0 Binary files /dev/null and b/graffle/attention/eye-coffee.graffle differ diff --git a/graffle/attention/multi-head-attention.graffle b/graffle/attention/multi-head-attention.graffle new file mode 100644 index 000000000..56350911f Binary files /dev/null and b/graffle/attention/multi-head-attention.graffle differ diff --git a/graffle/attention/positional_encoding.graffle b/graffle/attention/positional_encoding.graffle new file mode 100644 index 000000000..f77de4627 Binary files /dev/null and b/graffle/attention/positional_encoding.graffle differ diff --git a/graffle/attention/qkv.graffle b/graffle/attention/qkv.graffle new file mode 100644 index 000000000..5b63806b8 Binary files /dev/null and b/graffle/attention/qkv.graffle differ diff --git a/graffle/attention/self-attention-predict.graffle b/graffle/attention/self-attention-predict.graffle new file mode 100644 index 000000000..d9192e7f1 Binary files /dev/null and b/graffle/attention/self-attention-predict.graffle differ diff --git a/graffle/attention/self-attention.graffle b/graffle/attention/self-attention.graffle new file mode 100644 index 000000000..9e5d6d939 Binary files /dev/null and b/graffle/attention/self-attention.graffle differ diff --git a/graffle/attention/seq2seq-attention-details.graffle b/graffle/attention/seq2seq-attention-details.graffle new file mode 100644 index 000000000..c120a7440 Binary files /dev/null and b/graffle/attention/seq2seq-attention-details.graffle differ diff --git a/graffle/attention/seq2seq_attention.graffle b/graffle/attention/seq2seq_attention.graffle new file mode 100644 index 000000000..f0f2273fa Binary files /dev/null and b/graffle/attention/seq2seq_attention.graffle differ diff --git a/graffle/attention/transformer.graffle b/graffle/attention/transformer.graffle new file mode 100644 index 000000000..8d6526c00 Binary files /dev/null and b/graffle/attention/transformer.graffle differ diff --git a/graffle/book-org.graffle b/graffle/book-org.graffle new file mode 100644 index 000000000..39e0a565a Binary files /dev/null and b/graffle/book-org.graffle differ diff --git a/graffle/cnn-basic/conv-1x1.graffle b/graffle/cnn-basic/conv-1x1.graffle new file mode 100644 index 000000000..a3f8992e6 Binary files /dev/null and b/graffle/cnn-basic/conv-1x1.graffle differ diff --git a/graffle/cnn-basic/conv-multi-in.graffle b/graffle/cnn-basic/conv-multi-in.graffle new file mode 100644 index 000000000..7d1477b7b Binary files /dev/null and b/graffle/cnn-basic/conv-multi-in.graffle differ diff --git a/graffle/cnn-basic/conv-pad.graffle b/graffle/cnn-basic/conv-pad.graffle new file mode 100644 index 000000000..b6465c134 Binary files /dev/null and b/graffle/cnn-basic/conv-pad.graffle differ diff --git a/graffle/cnn-basic/conv-stride.graffle b/graffle/cnn-basic/conv-stride.graffle new file mode 100644 index 000000000..dcf9122c6 Binary files /dev/null and b/graffle/cnn-basic/conv-stride.graffle differ diff --git a/graffle/cnn-basic/correlation.graffle b/graffle/cnn-basic/correlation.graffle new file mode 100644 index 000000000..58bad546b Binary files /dev/null and b/graffle/cnn-basic/correlation.graffle differ diff --git a/graffle/cnn-basic/lenet-vert.graffle b/graffle/cnn-basic/lenet-vert.graffle new file mode 100644 index 000000000..7a98dbf97 Binary files /dev/null and b/graffle/cnn-basic/lenet-vert.graffle differ diff --git a/graffle/cnn-basic/lenet.graffle b/graffle/cnn-basic/lenet.graffle new file mode 100644 index 000000000..995dcbf4f Binary files /dev/null and b/graffle/cnn-basic/lenet.graffle differ diff --git a/graffle/cnn-basic/pooling.graffle b/graffle/cnn-basic/pooling.graffle new file mode 100644 index 000000000..bdf589d40 Binary files /dev/null and b/graffle/cnn-basic/pooling.graffle differ diff --git a/graffle/cnn-basic/waldo-mask.graffle/data.plist b/graffle/cnn-basic/waldo-mask.graffle/data.plist new file mode 100644 index 000000000..397842fac Binary files /dev/null and b/graffle/cnn-basic/waldo-mask.graffle/data.plist differ diff --git a/graffle/cnn-basic/waldo-mask.graffle/image1.jpg b/graffle/cnn-basic/waldo-mask.graffle/image1.jpg new file mode 100644 index 000000000..06c3b87b1 Binary files /dev/null and b/graffle/cnn-basic/waldo-mask.graffle/image1.jpg differ diff --git a/graffle/cnn-modern/ResNetManyFlavor.graffle b/graffle/cnn-modern/ResNetManyFlavor.graffle new file mode 100644 index 000000000..4c6c85c82 Binary files /dev/null and b/graffle/cnn-modern/ResNetManyFlavor.graffle differ diff --git a/graffle/cnn-modern/alexnet.graffle b/graffle/cnn-modern/alexnet.graffle new file mode 100644 index 000000000..9987f86f2 Binary files /dev/null and b/graffle/cnn-modern/alexnet.graffle differ diff --git a/graffle/cnn-modern/densenet-block.graffle b/graffle/cnn-modern/densenet-block.graffle new file mode 100644 index 000000000..ade9b5a11 Binary files /dev/null and b/graffle/cnn-modern/densenet-block.graffle differ diff --git a/graffle/cnn-modern/densenet.graffle b/graffle/cnn-modern/densenet.graffle new file mode 100644 index 000000000..b1fd16330 Binary files /dev/null and b/graffle/cnn-modern/densenet.graffle differ diff --git a/graffle/cnn-modern/functionclasses.graffle b/graffle/cnn-modern/functionclasses.graffle new file mode 100644 index 000000000..c25e57af2 Binary files /dev/null and b/graffle/cnn-modern/functionclasses.graffle differ diff --git a/graffle/cnn-modern/inception-full.graffle b/graffle/cnn-modern/inception-full.graffle new file mode 100644 index 000000000..e8d708b5f Binary files /dev/null and b/graffle/cnn-modern/inception-full.graffle differ diff --git a/graffle/cnn-modern/inception.graffle b/graffle/cnn-modern/inception.graffle new file mode 100644 index 000000000..297df5736 Binary files /dev/null and b/graffle/cnn-modern/inception.graffle differ diff --git a/graffle/cnn-modern/nin-compare.graffle b/graffle/cnn-modern/nin-compare.graffle new file mode 100644 index 000000000..29a19a1a9 Binary files /dev/null and b/graffle/cnn-modern/nin-compare.graffle differ diff --git a/graffle/cnn-modern/nin.graffle b/graffle/cnn-modern/nin.graffle new file mode 100644 index 000000000..3bb7b8b0c Binary files /dev/null and b/graffle/cnn-modern/nin.graffle differ diff --git a/graffle/cnn-modern/residual-block.graffle b/graffle/cnn-modern/residual-block.graffle new file mode 100644 index 000000000..3ae06b5b1 Binary files /dev/null and b/graffle/cnn-modern/residual-block.graffle differ diff --git a/graffle/cnn-modern/resnet-block.graffle b/graffle/cnn-modern/resnet-block.graffle new file mode 100644 index 000000000..3d2b3b584 Binary files /dev/null and b/graffle/cnn-modern/resnet-block.graffle differ diff --git a/graffle/cnn-modern/resnet18.graffle b/graffle/cnn-modern/resnet18.graffle new file mode 100644 index 000000000..426ed46cf Binary files /dev/null and b/graffle/cnn-modern/resnet18.graffle differ diff --git a/graffle/cnn-modern/vgg.graffle b/graffle/cnn-modern/vgg.graffle new file mode 100644 index 000000000..81646d387 Binary files /dev/null and b/graffle/cnn-modern/vgg.graffle differ diff --git a/graffle/computation/asyncgraph.graffle b/graffle/computation/asyncgraph.graffle new file mode 100644 index 000000000..2aae3a638 Binary files /dev/null and b/graffle/computation/asyncgraph.graffle differ diff --git a/graffle/computation/blocks.graffle b/graffle/computation/blocks.graffle new file mode 100644 index 000000000..2e773ca2c Binary files /dev/null and b/graffle/computation/blocks.graffle differ diff --git a/graffle/computation/computegraph.graffle b/graffle/computation/computegraph.graffle new file mode 100644 index 000000000..331a7685e Binary files /dev/null and b/graffle/computation/computegraph.graffle differ diff --git a/graffle/computation/copyto.graffle b/graffle/computation/copyto.graffle new file mode 100644 index 000000000..335b9aecd Binary files /dev/null and b/graffle/computation/copyto.graffle differ diff --git a/graffle/computation/frontends.graffle b/graffle/computation/frontends.graffle new file mode 100644 index 000000000..60295f3cb Binary files /dev/null and b/graffle/computation/frontends.graffle differ diff --git a/graffle/computation/threading.graffle b/graffle/computation/threading.graffle new file mode 100644 index 000000000..d731a988d Binary files /dev/null and b/graffle/computation/threading.graffle differ diff --git a/graffle/computation/twogpu.graffle b/graffle/computation/twogpu.graffle new file mode 100644 index 000000000..be2e17783 Binary files /dev/null and b/graffle/computation/twogpu.graffle differ diff --git a/graffle/contribute.graffle b/graffle/contribute.graffle new file mode 100644 index 000000000..443066dd6 Binary files /dev/null and b/graffle/contribute.graffle differ diff --git a/graffle/convert.sh b/graffle/convert.sh new file mode 100644 index 000000000..c9e26eefb --- /dev/null +++ b/graffle/convert.sh @@ -0,0 +1 @@ +find . -iname '*.pdf' | while read f; do pdf2svg $f ${f%.pdf}.svg; done diff --git a/graffle/gan/gan.graffle b/graffle/gan/gan.graffle new file mode 100644 index 000000000..08d60e742 Binary files /dev/null and b/graffle/gan/gan.graffle differ diff --git a/graffle/intro/data-collection.graffle b/graffle/intro/data-collection.graffle new file mode 100644 index 000000000..450d4040e Binary files /dev/null and b/graffle/intro/data-collection.graffle differ diff --git a/graffle/intro/diveintodl.graffle b/graffle/intro/diveintodl.graffle new file mode 100644 index 000000000..fdca5b82d Binary files /dev/null and b/graffle/intro/diveintodl.graffle differ diff --git a/graffle/intro/ml-loop.graffle b/graffle/intro/ml-loop.graffle new file mode 100644 index 000000000..ec6af8427 Binary files /dev/null and b/graffle/intro/ml-loop.graffle differ diff --git a/graffle/intro/rl-environment.graffle b/graffle/intro/rl-environment.graffle new file mode 100644 index 000000000..d2b2faf64 Binary files /dev/null and b/graffle/intro/rl-environment.graffle differ diff --git a/graffle/intro/supervised-learning.graffle b/graffle/intro/supervised-learning.graffle new file mode 100644 index 000000000..22eec534f Binary files /dev/null and b/graffle/intro/supervised-learning.graffle differ diff --git a/graffle/intro/wake-word.graffle b/graffle/intro/wake-word.graffle new file mode 100644 index 000000000..3259d20a4 Binary files /dev/null and b/graffle/intro/wake-word.graffle differ diff --git a/graffle/linear/fit_linreg.graffle b/graffle/linear/fit_linreg.graffle new file mode 100644 index 000000000..2441f406a Binary files /dev/null and b/graffle/linear/fit_linreg.graffle differ diff --git a/graffle/linear/neuron.graffle b/graffle/linear/neuron.graffle new file mode 100644 index 000000000..7f9e649fb Binary files /dev/null and b/graffle/linear/neuron.graffle differ diff --git a/graffle/linear/singlelayer.graffle b/graffle/linear/singlelayer.graffle new file mode 100644 index 000000000..94167f6d2 Binary files /dev/null and b/graffle/linear/singlelayer.graffle differ diff --git a/graffle/linear/singleneuron.graffle b/graffle/linear/singleneuron.graffle new file mode 100644 index 000000000..50ade74c3 Binary files /dev/null and b/graffle/linear/singleneuron.graffle differ diff --git a/graffle/linear/softmaxreg.graffle b/graffle/linear/softmaxreg.graffle new file mode 100644 index 000000000..cabb533e9 Binary files /dev/null and b/graffle/linear/softmaxreg.graffle differ diff --git a/graffle/mlp/add_norm.graffle b/graffle/mlp/add_norm.graffle new file mode 100644 index 000000000..6ee265bce Binary files /dev/null and b/graffle/mlp/add_norm.graffle differ diff --git a/graffle/mlp/capacity_vs_error.graffle b/graffle/mlp/capacity_vs_error.graffle new file mode 100644 index 000000000..003730f6d Binary files /dev/null and b/graffle/mlp/capacity_vs_error.graffle differ diff --git a/graffle/mlp/dropout2.graffle b/graffle/mlp/dropout2.graffle new file mode 100644 index 000000000..f324c58ee Binary files /dev/null and b/graffle/mlp/dropout2.graffle differ diff --git a/graffle/mlp/forward.graffle b/graffle/mlp/forward.graffle new file mode 100644 index 000000000..0b93cf28e Binary files /dev/null and b/graffle/mlp/forward.graffle differ diff --git a/graffle/mlp/mlp.graffle b/graffle/mlp/mlp.graffle new file mode 100644 index 000000000..e85cc1e11 Binary files /dev/null and b/graffle/mlp/mlp.graffle differ diff --git a/graffle/nlp/bert-input.graffle b/graffle/nlp/bert-input.graffle new file mode 100644 index 000000000..5d4c77e29 Binary files /dev/null and b/graffle/nlp/bert-input.graffle differ diff --git a/graffle/nlp/bert-one-seq.graffle b/graffle/nlp/bert-one-seq.graffle new file mode 100644 index 000000000..3e16bf3b7 Binary files /dev/null and b/graffle/nlp/bert-one-seq.graffle differ diff --git a/graffle/nlp/bert-qa.graffle b/graffle/nlp/bert-qa.graffle new file mode 100644 index 000000000..1efe9f384 Binary files /dev/null and b/graffle/nlp/bert-qa.graffle differ diff --git a/graffle/nlp/bert-tagging.graffle b/graffle/nlp/bert-tagging.graffle new file mode 100644 index 000000000..af5cb679c Binary files /dev/null and b/graffle/nlp/bert-tagging.graffle differ diff --git a/graffle/nlp/bert-two-seqs.graffle b/graffle/nlp/bert-two-seqs.graffle new file mode 100644 index 000000000..8ba142f1f Binary files /dev/null and b/graffle/nlp/bert-two-seqs.graffle differ diff --git a/graffle/nlp/cbow.graffle b/graffle/nlp/cbow.graffle new file mode 100644 index 000000000..c12bb3a45 Binary files /dev/null and b/graffle/nlp/cbow.graffle differ diff --git a/graffle/nlp/conv1d-2d.graffle b/graffle/nlp/conv1d-2d.graffle new file mode 100644 index 000000000..6a9f23ad6 Binary files /dev/null and b/graffle/nlp/conv1d-2d.graffle differ diff --git a/graffle/nlp/conv1d-channel.graffle b/graffle/nlp/conv1d-channel.graffle new file mode 100644 index 000000000..753160ba4 Binary files /dev/null and b/graffle/nlp/conv1d-channel.graffle differ diff --git a/graffle/nlp/conv1d.graffle b/graffle/nlp/conv1d.graffle new file mode 100644 index 000000000..a871af693 Binary files /dev/null and b/graffle/nlp/conv1d.graffle differ diff --git a/graffle/nlp/elmo-gpt-bert.graffle b/graffle/nlp/elmo-gpt-bert.graffle new file mode 100644 index 000000000..42e5d8a10 Binary files /dev/null and b/graffle/nlp/elmo-gpt-bert.graffle differ diff --git a/graffle/nlp/hi-softmax.graffle b/graffle/nlp/hi-softmax.graffle new file mode 100644 index 000000000..8ced5df4e Binary files /dev/null and b/graffle/nlp/hi-softmax.graffle differ diff --git a/graffle/nlp/nli_attention.graffle b/graffle/nlp/nli_attention.graffle new file mode 100644 index 000000000..f428ece49 Binary files /dev/null and b/graffle/nlp/nli_attention.graffle differ diff --git a/graffle/nlp/nlp-map-app.graffle b/graffle/nlp/nlp-map-app.graffle new file mode 100644 index 000000000..e08987306 Binary files /dev/null and b/graffle/nlp/nlp-map-app.graffle differ diff --git a/graffle/nlp/nlp-map-nli-attention.graffle b/graffle/nlp/nlp-map-nli-attention.graffle new file mode 100644 index 000000000..25e180a69 Binary files /dev/null and b/graffle/nlp/nlp-map-nli-attention.graffle differ diff --git a/graffle/nlp/nlp-map-nli-bert.graffle b/graffle/nlp/nlp-map-nli-bert.graffle new file mode 100644 index 000000000..45f2260e5 Binary files /dev/null and b/graffle/nlp/nlp-map-nli-bert.graffle differ diff --git a/graffle/nlp/nlp-map-pretrain.graffle b/graffle/nlp/nlp-map-pretrain.graffle new file mode 100644 index 000000000..f2beb8a55 Binary files /dev/null and b/graffle/nlp/nlp-map-pretrain.graffle differ diff --git a/graffle/nlp/nlp-map-sa-cnn.graffle b/graffle/nlp/nlp-map-sa-cnn.graffle new file mode 100644 index 000000000..348ccfb35 Binary files /dev/null and b/graffle/nlp/nlp-map-sa-cnn.graffle differ diff --git a/graffle/nlp/nlp-map-sa-rnn.graffle b/graffle/nlp/nlp-map-sa-rnn.graffle new file mode 100644 index 000000000..3e757f693 Binary files /dev/null and b/graffle/nlp/nlp-map-sa-rnn.graffle differ diff --git a/graffle/nlp/sentiment-rnn.graffle b/graffle/nlp/sentiment-rnn.graffle new file mode 100644 index 000000000..66a2c4e24 Binary files /dev/null and b/graffle/nlp/sentiment-rnn.graffle differ diff --git a/graffle/nlp/skip-gram.graffle b/graffle/nlp/skip-gram.graffle new file mode 100644 index 000000000..c96af453c Binary files /dev/null and b/graffle/nlp/skip-gram.graffle differ diff --git a/graffle/nlp/textcnn.graffle b/graffle/nlp/textcnn.graffle new file mode 100644 index 000000000..6d769fb19 Binary files /dev/null and b/graffle/nlp/textcnn.graffle differ diff --git a/graffle/optimization/convex.graffle b/graffle/optimization/convex.graffle new file mode 100644 index 000000000..7c77105d3 Binary files /dev/null and b/graffle/optimization/convex.graffle differ diff --git a/graffle/performance/a77.graffle b/graffle/performance/a77.graffle new file mode 100644 index 000000000..bad585885 Binary files /dev/null and b/graffle/performance/a77.graffle differ diff --git a/graffle/performance/bw-hierarchy.graffle b/graffle/performance/bw-hierarchy.graffle new file mode 100644 index 000000000..3b4663fd0 Binary files /dev/null and b/graffle/performance/bw-hierarchy.graffle differ diff --git a/graffle/performance/bw-hierarchy.pdf b/graffle/performance/bw-hierarchy.pdf new file mode 100644 index 000000000..f9ea15d06 Binary files /dev/null and b/graffle/performance/bw-hierarchy.pdf differ diff --git a/graffle/performance/data-parallel.graffle b/graffle/performance/data-parallel.graffle new file mode 100644 index 000000000..2962b4d51 Binary files /dev/null and b/graffle/performance/data-parallel.graffle differ diff --git a/graffle/performance/falseshare.graffle b/graffle/performance/falseshare.graffle new file mode 100644 index 000000000..e90cdda6b Binary files /dev/null and b/graffle/performance/falseshare.graffle differ diff --git a/graffle/performance/mobo.graffle/data.plist b/graffle/performance/mobo.graffle/data.plist new file mode 100644 index 000000000..210794d15 Binary files /dev/null and b/graffle/performance/mobo.graffle/data.plist differ diff --git a/graffle/performance/mobo.graffle/image1.tiff b/graffle/performance/mobo.graffle/image1.tiff new file mode 100644 index 000000000..03660b695 Binary files /dev/null and b/graffle/performance/mobo.graffle/image1.tiff differ diff --git a/graffle/performance/mobo.graffle/preview.jpeg b/graffle/performance/mobo.graffle/preview.jpeg new file mode 100644 index 000000000..49f484875 Binary files /dev/null and b/graffle/performance/mobo.graffle/preview.jpeg differ diff --git a/graffle/performance/neon128.graffle b/graffle/performance/neon128.graffle new file mode 100644 index 000000000..56a2bdf38 Binary files /dev/null and b/graffle/performance/neon128.graffle differ diff --git a/graffle/performance/ps-distributed.graffle b/graffle/performance/ps-distributed.graffle new file mode 100644 index 000000000..fee44beeb Binary files /dev/null and b/graffle/performance/ps-distributed.graffle differ diff --git a/graffle/performance/ps-distributed.pdf b/graffle/performance/ps-distributed.pdf new file mode 100644 index 000000000..32c811aa2 Binary files /dev/null and b/graffle/performance/ps-distributed.pdf differ diff --git a/graffle/performance/ps-multimachine.graffle b/graffle/performance/ps-multimachine.graffle new file mode 100644 index 000000000..0a8a54625 Binary files /dev/null and b/graffle/performance/ps-multimachine.graffle differ diff --git a/graffle/performance/ps-multimachine.pdf b/graffle/performance/ps-multimachine.pdf new file mode 100644 index 000000000..0b5727881 Binary files /dev/null and b/graffle/performance/ps-multimachine.pdf differ diff --git a/graffle/performance/ps-multips.graffle b/graffle/performance/ps-multips.graffle new file mode 100644 index 000000000..1dc605784 Binary files /dev/null and b/graffle/performance/ps-multips.graffle differ diff --git a/graffle/performance/ps-multips.pdf b/graffle/performance/ps-multips.pdf new file mode 100644 index 000000000..781793476 Binary files /dev/null and b/graffle/performance/ps-multips.pdf differ diff --git a/graffle/performance/ps.graffle b/graffle/performance/ps.graffle new file mode 100644 index 000000000..05449a0da Binary files /dev/null and b/graffle/performance/ps.graffle differ diff --git a/graffle/performance/ps.pdf b/graffle/performance/ps.pdf new file mode 100644 index 000000000..7ea7680ee Binary files /dev/null and b/graffle/performance/ps.pdf differ diff --git a/graffle/performance/splitting.graffle b/graffle/performance/splitting.graffle new file mode 100644 index 000000000..825af8f0e Binary files /dev/null and b/graffle/performance/splitting.graffle differ diff --git a/graffle/preliminaries/polygon_circle.graffle b/graffle/preliminaries/polygon_circle.graffle new file mode 100644 index 000000000..ccb94181e Binary files /dev/null and b/graffle/preliminaries/polygon_circle.graffle differ diff --git a/graffle/recsys/rec-caser.graffle b/graffle/recsys/rec-caser.graffle new file mode 100644 index 000000000..101aca67f Binary files /dev/null and b/graffle/recsys/rec-caser.graffle differ diff --git a/graffle/recsys/rec-deepfm.graffle b/graffle/recsys/rec-deepfm.graffle new file mode 100644 index 000000000..e16182084 Binary files /dev/null and b/graffle/recsys/rec-deepfm.graffle differ diff --git a/graffle/recsys/rec-intro.graffle b/graffle/recsys/rec-intro.graffle new file mode 100644 index 000000000..1d6aaab71 Binary files /dev/null and b/graffle/recsys/rec-intro.graffle differ diff --git a/graffle/recsys/rec-mf.graffle b/graffle/recsys/rec-mf.graffle new file mode 100644 index 000000000..eb501ce01 Binary files /dev/null and b/graffle/recsys/rec-mf.graffle differ diff --git a/graffle/recsys/rec-neumf.graffle b/graffle/recsys/rec-neumf.graffle new file mode 100644 index 000000000..82716fbc7 Binary files /dev/null and b/graffle/recsys/rec-neumf.graffle differ diff --git a/graffle/recsys/rec-ranking.graffle b/graffle/recsys/rec-ranking.graffle new file mode 100644 index 000000000..4ce9194ab Binary files /dev/null and b/graffle/recsys/rec-ranking.graffle differ diff --git a/graffle/recsys/rec-seq-data.graffle b/graffle/recsys/rec-seq-data.graffle new file mode 100644 index 000000000..bdf857eba Binary files /dev/null and b/graffle/recsys/rec-seq-data.graffle differ diff --git a/graffle/rnn/beam-search.graffle b/graffle/rnn/beam-search.graffle new file mode 100644 index 000000000..43b942c0e Binary files /dev/null and b/graffle/rnn/beam-search.graffle differ diff --git a/graffle/rnn/birnn-ORIGINAL.graffle b/graffle/rnn/birnn-ORIGINAL.graffle new file mode 100644 index 000000000..c0a10ac6f Binary files /dev/null and b/graffle/rnn/birnn-ORIGINAL.graffle differ diff --git a/graffle/rnn/birnn.graffle b/graffle/rnn/birnn.graffle new file mode 100644 index 000000000..437cf5d19 Binary files /dev/null and b/graffle/rnn/birnn.graffle differ diff --git a/graffle/rnn/deep-rnn-ORIGINAL.graffle b/graffle/rnn/deep-rnn-ORIGINAL.graffle new file mode 100644 index 000000000..154db83e6 Binary files /dev/null and b/graffle/rnn/deep-rnn-ORIGINAL.graffle differ diff --git a/graffle/rnn/deep-rnn.graffle b/graffle/rnn/deep-rnn.graffle new file mode 100644 index 000000000..76af6ea13 Binary files /dev/null and b/graffle/rnn/deep-rnn.graffle differ diff --git a/graffle/rnn/hmm.graffle b/graffle/rnn/hmm.graffle new file mode 100644 index 000000000..e275bc398 Binary files /dev/null and b/graffle/rnn/hmm.graffle differ diff --git a/graffle/rnn/lang-model-data.graffle b/graffle/rnn/lang-model-data.graffle new file mode 100644 index 000000000..7afef13e0 Binary files /dev/null and b/graffle/rnn/lang-model-data.graffle differ diff --git a/graffle/rnn/rnn-bptt.graffle b/graffle/rnn/rnn-bptt.graffle new file mode 100644 index 000000000..e3e60e44f Binary files /dev/null and b/graffle/rnn/rnn-bptt.graffle differ diff --git a/graffle/rnn/rnn-train.graffle b/graffle/rnn/rnn-train.graffle new file mode 100644 index 000000000..958b9569e Binary files /dev/null and b/graffle/rnn/rnn-train.graffle differ diff --git a/graffle/rnn/rnn.graffle b/graffle/rnn/rnn.graffle new file mode 100644 index 000000000..bdcd4fc42 Binary files /dev/null and b/graffle/rnn/rnn.graffle differ diff --git a/graffle/rnn/s2s-prob1.graffle b/graffle/rnn/s2s-prob1.graffle new file mode 100644 index 000000000..a6e003905 Binary files /dev/null and b/graffle/rnn/s2s-prob1.graffle differ diff --git a/graffle/rnn/s2s-prob2.graffle b/graffle/rnn/s2s-prob2.graffle new file mode 100644 index 000000000..b1ac9cd84 Binary files /dev/null and b/graffle/rnn/s2s-prob2.graffle differ diff --git a/graffle/rnn/seq2seq-details.graffle b/graffle/rnn/seq2seq-details.graffle new file mode 100644 index 000000000..c4a49b9d6 Binary files /dev/null and b/graffle/rnn/seq2seq-details.graffle differ diff --git a/graffle/rnn/seq2seq.graffle b/graffle/rnn/seq2seq.graffle new file mode 100644 index 000000000..39381d5f5 Binary files /dev/null and b/graffle/rnn/seq2seq.graffle differ diff --git a/graffle/rnn/seq2seq_predict.graffle b/graffle/rnn/seq2seq_predict.graffle new file mode 100644 index 000000000..b2710ecc2 Binary files /dev/null and b/graffle/rnn/seq2seq_predict.graffle differ diff --git a/graffle/rnn/sequence-model.graffle b/graffle/rnn/sequence-model.graffle new file mode 100644 index 000000000..76a7e7419 Binary files /dev/null and b/graffle/rnn/sequence-model.graffle differ diff --git a/graffle/rnn/timemachine-5gram.graffle b/graffle/rnn/timemachine-5gram.graffle new file mode 100644 index 000000000..7d265044c Binary files /dev/null and b/graffle/rnn/timemachine-5gram.graffle differ diff --git a/graffle/rnn/truncated-bptt.graffle b/graffle/rnn/truncated-bptt.graffle new file mode 100644 index 000000000..cc262d9c2 Binary files /dev/null and b/graffle/rnn/truncated-bptt.graffle differ diff --git a/graffle/transformer.graffle b/graffle/transformer.graffle new file mode 100644 index 000000000..a3368732b Binary files /dev/null and b/graffle/transformer.graffle differ diff --git a/graffle/vision/anchor-label.graffle b/graffle/vision/anchor-label.graffle new file mode 100644 index 000000000..d2e3994b7 Binary files /dev/null and b/graffle/vision/anchor-label.graffle differ diff --git a/graffle/vision/fast-rcnn.graffle b/graffle/vision/fast-rcnn.graffle new file mode 100644 index 000000000..0ba46ba90 Binary files /dev/null and b/graffle/vision/fast-rcnn.graffle differ diff --git a/graffle/vision/faster-rcnn.graffle b/graffle/vision/faster-rcnn.graffle new file mode 100644 index 000000000..320052304 Binary files /dev/null and b/graffle/vision/faster-rcnn.graffle differ diff --git a/graffle/vision/fcn.graffle b/graffle/vision/fcn.graffle new file mode 100644 index 000000000..5cebda263 Binary files /dev/null and b/graffle/vision/fcn.graffle differ diff --git a/graffle/vision/finetune.graffle b/graffle/vision/finetune.graffle new file mode 100644 index 000000000..a620dbe07 Binary files /dev/null and b/graffle/vision/finetune.graffle differ diff --git a/graffle/vision/iou.graffle b/graffle/vision/iou.graffle new file mode 100644 index 000000000..b2ec17d69 Binary files /dev/null and b/graffle/vision/iou.graffle differ diff --git a/graffle/vision/mask-rcnn.graffle b/graffle/vision/mask-rcnn.graffle new file mode 100644 index 000000000..e80e880a0 Binary files /dev/null and b/graffle/vision/mask-rcnn.graffle differ diff --git a/graffle/vision/neural-style.graffle b/graffle/vision/neural-style.graffle new file mode 100644 index 000000000..1d7c12059 Binary files /dev/null and b/graffle/vision/neural-style.graffle differ diff --git a/graffle/vision/r-cnn.graffle b/graffle/vision/r-cnn.graffle new file mode 100644 index 000000000..66e784cb3 Binary files /dev/null and b/graffle/vision/r-cnn.graffle differ diff --git a/graffle/vision/roi.graffle b/graffle/vision/roi.graffle new file mode 100644 index 000000000..3dd30feff Binary files /dev/null and b/graffle/vision/roi.graffle differ diff --git a/graffle/vision/segmentation.graffle b/graffle/vision/segmentation.graffle new file mode 100644 index 000000000..f04a87a49 Binary files /dev/null and b/graffle/vision/segmentation.graffle differ diff --git a/graffle/vision/ssd.graffle b/graffle/vision/ssd.graffle new file mode 100644 index 000000000..48371fcc6 Binary files /dev/null and b/graffle/vision/ssd.graffle differ diff --git a/graffle/vision/style-transfer.graffle b/graffle/vision/style-transfer.graffle new file mode 100644 index 000000000..045a16c6c Binary files /dev/null and b/graffle/vision/style-transfer.graffle differ diff --git a/graffle/vision/trans_conv.graffle b/graffle/vision/trans_conv.graffle new file mode 100644 index 000000000..c3962e517 Binary files /dev/null and b/graffle/vision/trans_conv.graffle differ diff --git a/graffle/vision/trans_conv_2.graffle b/graffle/vision/trans_conv_2.graffle new file mode 100644 index 000000000..17f7e8bea Binary files /dev/null and b/graffle/vision/trans_conv_2.graffle differ diff --git a/graffle/vision/trans_conv_pad1_2.graffle b/graffle/vision/trans_conv_pad1_2.graffle new file mode 100644 index 000000000..158800398 Binary files /dev/null and b/graffle/vision/trans_conv_pad1_2.graffle differ diff --git a/graffle/vision/trans_conv_stride2.graffle b/graffle/vision/trans_conv_stride2.graffle new file mode 100644 index 000000000..2ed56bbc0 Binary files /dev/null and b/graffle/vision/trans_conv_stride2.graffle differ diff --git a/graffle/vision/trans_conv_stride2_2.graffle b/graffle/vision/trans_conv_stride2_2.graffle new file mode 100644 index 000000000..3ac3eddbb Binary files /dev/null and b/graffle/vision/trans_conv_stride2_2.graffle differ diff --git a/static/build.yml b/static/build.yml index 314d3328e..c3c8aa7e9 100644 --- a/static/build.yml +++ b/static/build.yml @@ -1,13 +1,16 @@ dependencies: - - python=3.8 + - python=3.9 - pip - pip: - - d2l==0.17.5 + - d2l==0.17.6 - git+https://github.com/d2l-ai/d2l-book - mxnet-cu102==1.7.0 - - torch==1.11.0+cu102 + - torch==1.12.0+cu102 - -f https://download.pytorch.org/whl/torch_stable.html - - torchvision==0.12.0+cu102 + - torchvision==0.13.0+cu102 - -f https://download.pytorch.org/whl/torch_stable.html - - tensorflow==2.8.0 - - tensorflow-probability==0.16.0 + - tensorflow==2.9.1 + - tensorflow-probability==0.17.0 + - paddlepaddle-gpu==2.3.2.post112 + - -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html + - opencv-python==4.6.0.66 diff --git a/static/frontpage/_images/huliujun.jpg b/static/frontpage/_images/huliujun.jpg new file mode 100644 index 000000000..32be7c431 Binary files /dev/null and b/static/frontpage/_images/huliujun.jpg differ diff --git a/static/frontpage/_images/logos/logoimg1-zh.png b/static/frontpage/_images/logos/logoimg1-zh.png new file mode 100644 index 000000000..1f18edc79 Binary files /dev/null and b/static/frontpage/_images/logos/logoimg1-zh.png differ diff --git a/static/frontpage/_images/logos/logoimg2-zh.png b/static/frontpage/_images/logos/logoimg2-zh.png new file mode 100644 index 000000000..1dd86983b Binary files /dev/null and b/static/frontpage/_images/logos/logoimg2-zh.png differ diff --git a/static/frontpage/_images/logos/logoimg3-zh.png b/static/frontpage/_images/logos/logoimg3-zh.png new file mode 100644 index 000000000..ab1bd2ecc Binary files /dev/null and b/static/frontpage/_images/logos/logoimg3-zh.png differ diff --git a/static/frontpage/_images/logos/logoimg4-zh.png b/static/frontpage/_images/logos/logoimg4-zh.png new file mode 100644 index 000000000..a914ded46 Binary files /dev/null and b/static/frontpage/_images/logos/logoimg4-zh.png differ diff --git a/static/frontpage/_images/logos/logoimg5-zh.png b/static/frontpage/_images/logos/logoimg5-zh.png new file mode 100644 index 000000000..43e1bc376 Binary files /dev/null and b/static/frontpage/_images/logos/logoimg5-zh.png differ diff --git a/static/frontpage/_images/logos/logoimg6-zh.png b/static/frontpage/_images/logos/logoimg6-zh.png new file mode 100644 index 000000000..fcd2554d7 Binary files /dev/null and b/static/frontpage/_images/logos/logoimg6-zh.png differ diff --git a/static/frontpage/_images/wugaosheng.jpg b/static/frontpage/_images/wugaosheng.jpg new file mode 100644 index 000000000..ab492b69f Binary files /dev/null and b/static/frontpage/_images/wugaosheng.jpg differ diff --git a/static/frontpage/_images/xiejiehang.jpg b/static/frontpage/_images/xiejiehang.jpg new file mode 100644 index 000000000..e4bd99e72 Binary files /dev/null and b/static/frontpage/_images/xiejiehang.jpg differ diff --git a/static/frontpage/_images/yuan.jpg b/static/frontpage/_images/yuan.jpg new file mode 100644 index 000000000..d29d84624 Binary files /dev/null and b/static/frontpage/_images/yuan.jpg differ diff --git a/static/frontpage/_images/zhangge.jpg b/static/frontpage/_images/zhangge.jpg new file mode 100644 index 000000000..81a21ca58 Binary files /dev/null and b/static/frontpage/_images/zhangge.jpg differ diff --git a/static/frontpage/frontpage.html b/static/frontpage/frontpage.html index c501145ef..c59eff02c 100644 --- a/static/frontpage/frontpage.html +++ b/static/frontpage/frontpage.html @@ -199,11 +199,11 @@

《动手学深度学习》

-

第二版预览版

+

第二版

跳转第一版

面向中文读者的能运行、可讨论的深度学习教科书

-

含 NumPy/MXNet、PyTorch 和 TensorFlow 实现

-

被全球 55 个国家 300 所大学用于教学

+

含 PyTorch、NumPy/MXNet、TensorFlow 和 PaddlePaddle 实现

+

被全球 60 多个国家 400 多所大学用于教学

Star

@@ -213,11 +213,9 @@

《动手学深度学习》

公告

    -
  • 【关注第二版更新】 - 英文版已翻译至中文版第二版,并含多种深度学习框架的实现。 - 英文版还新增了推荐系统深度学习的数学等。 - 如果想及时获取最新修订或增添的信息, - 请关注本书的中文开源项目英文开源项目。 +
  • 【重磅升级】 + 第二版纸质书——《动手学深度学习(PyTorch版)》预计于2023年元旦面世。第二版在线内容新增PaddlePaddle实现。 + 关注本书的中文开源项目英文开源项目以及时获取最新信息。
  • 【购买第一版纸质书(上架4周重印2次,累计3万+册)】 纸质书在内容上与在线版大致相同,但力求在样式、术语标注、语言表述、用词规范、标点以及图、表、章节的索引上符合出版标准和学术规范。 @@ -239,11 +237,12 @@

    公告

    课件、作业、教学视频等资源可参考伯克利“深度学习导论” 课程大纲 中的链接(中文版课件)。 - 基于本书较早草稿内容的中文教学视频在: - B站 + 基于本书PyTorch版的教学视频在: + B站; + 基于本书较早草稿内容的教学视频在: + B站 和 - Youtube。 - [关于资源] + Youtube
@@ -258,7 +257,7 @@

作者

阿斯顿·张

-

亚马逊高级科学家

+

亚马逊资深科学家

@@ -290,7 +289,7 @@

第二卷章节作者

布伦特 沃尼斯

-

亚马逊高级科学家
深度学习的数学

+

亚马逊资深科学家
深度学习的数学

@@ -330,11 +329,39 @@

阿尼如 达格

- +

唐源

蚂蚁集团高级工程师
TensorFlow改编

+
+
+ +

吴高升

+

百度飞桨高级工程师
飞桨改编

+
+
+
+
+ +

胡刘俊

+

飞桨开发者技术专家
飞桨改编

+
+
+
+
+ +

张戈

+

飞桨开发者技术专家
飞桨改编

+
+
+
+
+ +

谢杰航

+

飞桨开发者技术专家
飞桨改编

+
+

中文版译者

@@ -355,8 +382,8 @@

瑞潮儿·胡

-

感谢来自社区的 100+ 位贡献者

-

为本书贡献

+

感谢来自社区的 200 多位贡献者

+

为本书贡献

@@ -640,14 +667,12 @@

本书(中英文版)被用作教材或参考书

--> -
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -897,304 +922,395 @@

本书(中英文版)被用作教材或参考书

- Abasyn University, Islamabad Campus
- Alexandria University
- Amirkabir University of Technology
- Amity University
- Anna University
- Ateneo de Naga University
- Australian National University
- Bar-Ilan University
- Barnard College
- Birla Institute of Technology and Science, Hyderabad
- Birla Institute of Technology and Science, Pilani
- Boston University
- Brandeis University
- Brown University
- Brunel University London
- Cairo University
- California State University, Northridge
- Carnegie Mellon University
- Chalmers University of Technology
- Chennai Mathematical Institute
- Chouaib Doukkali University
- City College of New York
- College of Engineering Pune
- Columbia University
- Cyprus Institute
- Dresden University of Technology
- Duke University
- Durban University of Technology
- Eastern Mediterranean University
- Ecole Nationale Supérieure d'Informatique
- Eindhoven University of Technology
- Emory University
- Eötvös Loránd University
- Escuela Politécnica Nacional
- Escuela Superior Politecnica del Litoral
- Federal University Lokoja
- Fisk University
- Florida Atlantic University
- FPT University
- Fudan University
- Gayatri Vidya Parishad College of Engineering (Autonomous)
- Gazi Üniversitesi
- George Mason University
- Georgetown University
- Georgia Institute of Technology
- Gheorghe Asachi Technical University of Iaşi
- Golden Gate University
- Great Lakes Institute of Management
- Gwangju Institute of Science and Technology
- Habib University
- Hamad Bin Khalifa University
- Hangzhou Dianzi University
- Hangzhou Dianzi University
- Hankuk University of Foreign Studies
- Harbin Institute of Technology
- Harvard University
- Hasso-Plattner-Institut
- Heinrich-Heine-Universität Düsseldorf
- Henan Institute of Technology
- Hertie School
- Higher Institute of Applied Science and Technology of Sousse
- Hiroshima University
- Ho Chi Minh City University of Foreign Languages and Information Technology
- Hochschule Bremen
- Hochschule für Technik und Wirtschaft
- Hong Kong University of Science and Technology
- Huazhong University of Science and Technology
- Icahn School of Medicine at Mount Sinai
- Imperial College London
- IMT Mines Alès
- Indian Institute of Technology Bombay
- Indian Institute of Technology Hyderabad
- Indian Institute of Technology Jodhpur
- Indian Institute of Technology Kanpur
- Indian Institute of Technology Kharagpur
- Indian Institute of Technology Mandi
- Indian Institute of Technology Ropar
- Indian School of Business
- Indira Gandhi National Open University
- Indraprastha Institute of Information Technology, Delhi
- Institut catholique d'arts et métiers (ICAM)
- Institut de recherche en informatique de Toulouse
- Institut Supérieur d'Informatique et des Techniques de Communication
- Institut Supérieur De L'electronique Et Du Numérique
- Institut Teknologi Bandung
- Instituto Tecnológico Autónomo de México
- Instituto Tecnológico de Buenos Aires
- Islamic University of Medina
- İstanbul Teknik Üniversitesi
- IT-Universitetet i København
- Jeonbuk National Univerity
- Johns Hopkins University
- Keio University
- King Abdullah University of Science and Technology
- King Fahd University of Petroleum and Minerals
- Kongu Engineering College
- KPR Institute of Engineering and Technology
- Kyungpook National University
- Lancaster University
- Leading Unviersity
- Leibniz Universität Hannover
- Leuphana University of Lüneburg
- London School of Economics & Political Science
- M.S.Ramaiah University of Applied Sciences
- Make School
- Masaryk University
- Massachusetts Institute of Technology
- McGill University
- Menoufia University
- Milwaukee School of Engineering
- Minia University
- Mohammad Ali Jinnah University
- Mohammed V University in Rabat
- Monash University
- Multimedia University
- Murdoch University
- National Chung Hsing University
- National Institute of Technical Teachers Training & Research
- National Institute of Technology, Warangal
- National Sun Yat-sen University
- National Taiwan University
- National Technical University of Athens
- National Technical University of Ukraine
- National United University
- National University of Sciences and Technology
- National University of Singapore
- Nazarbayev University
- New Jersey Institute of Technology
- New Mexico Institute of Mining and Technology
- New York University
- Newman University
- North Ossetian State University
- Northeastern University
- Northwestern Polytechnical University
- Northwestern University
- Ohio University
- Pakuan University
- Peking University
- Pennsylvania State University
- Pohang University of Science and Technology
- Politecnico di Milano
- Politeknik Negeri Semarang
- Pomona College
- Pontificia Universidad Católica de Chile
- Pontificia Universidad Católica del Perú
- Portland State University
- Purdue University
- Quaid-e-Azam University
- Queen's University
- Radboud Universiteit
- Rowan University
- Rutgers, The State University of New Jersey
- RVS Institute of Management Studies and Research
- Santa Clara University
- Sapienza Università di Roma
- Seoul National University of Science and Technology
- Seoul National University
- Shanghai Jiao Tong University
- Shanghai University of Finance and Economics
- Shantilal Shah Engineering College
- Sharif University of Technology
- Shenzhen University
- Shivaji University, Kolhapur
- Simon Fraser University
- Singapore University of Technology and Design
- Sogang University
- Southern New Hampshire University
- St. Pölten University of Applied Sciences
- Stanford University
- State University of New York at Binghamton
- State University of New York at Fredonia
- Stevens Institute of Technology
- Sungkyunkwan University
- Technion - Israel Institute of Technology
- Technische Universität Berlin
- Technische Universiteit Delft
- Tekirdağ Namık Kemal Üniversitesi
- Texas A&M University
- Thapar Institute of Engineering and Technology
- Tsinghua University
- Tufts University
- Umeå University
- Universidad Carlos III de Madrid
- Universidad de Ibagué
- Universidad de Ingeniería y Tecnología - UTEC
- Universidad de Zaragoza
- Universidad Icesi
- Universidad Militar Nueva Granada
- Universidad Nacional Agraria La Molina
- Universidad Nacional Autónoma de México
- Universidad Nacional de Colombia Sede Manizales
- Universidad Nacional de Tierra del Fuego
- Universidad Politécnica de Chiapas
- Universidad San Francisco de Quito
- Universidad Tecnológica de Pereira
- Universidade Católica de Brasília
- Universidade Estadual de Campinas
- Universidade Federal de Goiás
- Universidade Federal de Minas Gerais
- Universidade Federal de Ouro Preto
- Universidade Federal de São Carlos
- Universidade Federal de Viçosa
- Universidade Federal do Rio Grande
- Universidade NOVA de Lisboa
- Universidade Presbiteriana Mackenzie
- Università Cattolica del Sacro Cuore
- Università degli Studi di Bari Aldo Moro
- Università degli Studi di Brescia
- Università degli Studi di Catania
- Università degli Studi di Padova
- Universitas Andalas, Padang
- Universitas Indonesia
- Universitas Negeri Yogyakarta
- Universitas Udayana
- Universität Bremen
- Universitat de Barcelona
- Universität Heidelberg
- Universität Leipzig
- Universitat Politècnica de Catalunya
- Universitatea Babeș-Bolyai
- Universitatea de Vest din Timișoara
- Université Clermont Auvergne
- Université Côte d'Azur
- Université de Caen Normandie
- Université de Rouen Normandie
- Université de technologie de Compiègne
- Université Paris-Saclay
- Université Toulouse 1 Capitole
- University of Arkansas
- University of Augsburg
- University of Bath
- University of British Columbia
- University of California, Berkeley
- University of California, Irvine
- University of California, Los Angeles
- University of California, San Diego
- University of California, Santa Barbara
- University of California, Santa Cruz
- University of Cambridge
- University of Canberra
- University of Cincinnati
- University of Connecticut
- University of Copenhagen
- University of Florida
- University of Groningen
- University of Hamburg
- University of Houston
- University of Hull
- University of Iceland
- University of Idaho
- University of Illinois at Urbana-Champaign
- University of International Business and Economics
- University of Klagenfurt
- University of Liège
- University of Maryland
- University of Massachusetts Lowell
- University of Michigan
- University of Milano-Bicocca
- University of Minnesota, Twin Cities
- University of Moratuwa
- University of New Hampshire
- University of Newcastle
- University of North Carolina at Chapel Hill
- University of North Texas
- University of Northern Philippines
- University of Nottingham
- University of Pennsylvania
- University of Pittsburgh
- University of São Paulo
- University of Science and Technology of China
- University of Southern Maine
- University of St Andrews
- University of Suffolk
- University of Szeged
- University of Technology Sydney
- University of Tehran
- University of Texas at Austin
- University of Texas at Dallas
- University of Texas Rio Grande Valley
- University of Warsaw
- University of Washington
- University of Waterloo
- University of Wisconsin Madison
- Univerzita Komenského v Bratislave
- Vardhaman College of Engineering
- Vardhman Mahaveer Open University
- Vietnamese-German University
- Vignana Jyothi Institute Of Management
- Wageningen University
- West Virginia University
- Western University
- Xavier University Bhubaneswar
- Xi'an Jiaotong Liverpool University
- Xiamen University
- Yeshiva University
- Yonsei University
- Yunnan University
- Zhejiang University + Abasyn University, Islamabad Campus
+ Alexandria University
+ Amirkabir University of Technology
+ Amity University
+ Amrita Vishwa Vidyapeetham University
+ Anna University
+ Anna University Regional Campus Madurai
+ Ateneo de Naga University
+ Australian National University
+ Bar-Ilan University
+ Barnard College
+ Beijing Foresty University
+ Birla Institute of Technology and Science, Hyderabad
+ Birla Institute of Technology and Science, Pilani
+ BML Munjal University
+ Boston College
+ Boston University
+ Brac University
+ Brandeis University
+ Brown University
+ Brunel University London
+ Cairo University
+ California State University, Northridge
+ Cankaya University
+ Carnegie Mellon University
+ Center for Research and Advanced Studies of the National Polytechnic Institute
+ Chalmers University of Technology
+ Chennai Mathematical Institute
+ Chouaib Doukkali University
+ Chulalongkorn University
+ City College of New York
+ City University of Hong Kong
+ City University of Science and Information Technology
+ College of Engineering Pune
+ Columbia University
+ Cornell University
+ Cyprus Institute
+ Deakin University
+ Diponegoro University
+ Dresden University of Technology
+ Duke University
+ Durban University of Technology
+ Eastern Mediterranean University
+ Ecole Nationale Supérieure d'Informatique
+ Ecole Nationale Supérieure de Cognitique
+ École Nationale Supérieure de Techniques Avancées
+ Eindhoven University of Technology
+ Emory University
+ Eötvös Loránd University
+ Escuela Politécnica Nacional
+ Escuela Superior Politecnica del Litoral
+ Federal University Lokoja
+ Feng Chia University
+ Fisk University
+ Florida Atlantic University
+ FPT University
+ Fudan University
+ Ganpat University
+ Gayatri Vidya Parishad College of Engineering (Autonomous)
+ Gazi Üniversitesi
+ Gdańsk University of Technology
+ George Mason University
+ Georgetown University
+ Georgia Institute of Technology
+ Gheorghe Asachi Technical University of Iaşi
+ Golden Gate University
+ Great Lakes Institute of Management
+ Gwangju Institute of Science and Technology
+ Habib University
+ Hamad Bin Khalifa University
+ Hangzhou Dianzi University
+ Hangzhou Dianzi University
+ Hankuk University of Foreign Studies
+ Harare Institute of Technology
+ Harbin Institute of Technology
+ Harvard University
+ Hasso-Plattner-Institut
+ Hebrew University of Jerusalem
+ Heinrich-Heine-Universität Düsseldorf
+ Henan Institute of Technology
+ Hertie School
+ Higher Institute of Applied Science and Technology of Sousse
+ Hiroshima University
+ Ho Chi Minh City University of Foreign Languages and Information Technology
+ Hochschule Bremen
+ Hochschule für Technik und Wirtschaft
+ Hochschule Hamm-Lippstadt
+ Hong Kong University of Science and Technology
+ Houston Community College
+ Huazhong University of Science and Technology
+ Humboldt-Universität zu Berlin
+ İbn Haldun Üniversitesi
+ Icahn School of Medicine at Mount Sinai
+ Imperial College London
+ IMT Mines Alès
+ Indian Institute of Technology Bombay
+ Indian Institute of Technology Hyderabad
+ Indian Institute of Technology Jodhpur
+ Indian Institute of Technology Kanpur
+ Indian Institute of Technology Kharagpur
+ Indian Institute of Technology Mandi
+ Indian Institute of Technology Ropar
+ Indian School of Business
+ Indira Gandhi National Open University
+ Indraprastha Institute of Information Technology, Delhi
+ Institut catholique d'arts et métiers (ICAM)
+ Institut de recherche en informatique de Toulouse
+ Institut Supérieur d'Informatique et des Techniques de Communication
+ Institut Supérieur De L'electronique Et Du Numérique
+ Institut Teknologi Bandung
+ Instituto Federal de Educação, Ciência e Tecnologia de São Paulo, Campus Salto
+ Instituto Politécnico Nacional
+ Instituto Tecnológico Autónomo de México
+ Instituto Tecnológico de Buenos Aires
+ Islamic University of Medina
+ İstanbul Teknik Üniversitesi
+ IT-Universitetet i København
+ Ivan Franko National University of Lviv
+ Jeonbuk National Univerity
+ Johns Hopkins University
+ Julius-Maximilians-Universität Würzburg
+ Keio University
+ King Abdullah University of Science and Technology
+ King Fahd University of Petroleum and Minerals
+ King Faisal University
+ Kongu Engineering College
+ Korea Aerospace University
+ KPR Institute of Engineering and Technology
+ Kyungpook National University
+ Lancaster University
+ Leading Unviersity
+ Leibniz Universität Hannover
+ Leuphana University of Lüneburg
+ London School of Economics & Political Science
+ M.S.Ramaiah University of Applied Sciences
+ Make School
+ Masaryk University
+ Massachusetts Institute of Technology
+ Maynooth University
+ McGill University
+ Menoufia University
+ Milwaukee School of Engineering
+ Minia University
+ Mississippi State University
+ Missouri University of Science and Technology
+ Mohammad Ali Jinnah University
+ Mohammed V University in Rabat
+ Monash University
+ Multimedia University
+ Murdoch University
+ Nanchang Hangkong University
+ Nanjing Medical University
+ Nanjing University
+ National Institute of Technology Trichy
+ National Technical University of Athens
+ National Technical University of Ukraine
+ National University of Sciences and Technology
+ National University of Singapore
+ Nazarbayev University
+ New Jersey Institute of Technology
+ New Mexico Institute of Mining and Technology
+ New Mexico State University
+ New York University
+ Newman University
+ North Ossetian State University
+ NorthCap University
+ Northeastern University
+ Northwestern Polytechnical University
+ Northwestern University
+ Ohio University
+ Pakuan University
+ Peking University
+ Pennsylvania State University
+ Pohang University of Science and Technology
+ Politechnika Białostocka
+ Politecnico di Milano
+ Politeknik Negeri Semarang
+ Pomona College
+ Pontificia Universidad Católica de Chile
+ Pontificia Universidad Católica del Perú
+ Portland State University
+ Punjabi University
+ Purdue University
+ Purdue University Northwest
+ Quaid-e-Azam University
+ Queen Mary University of London
+ Queen's University
+ Radboud Universiteit
+ Radboud University
+ Rajiv Gandhi Institute of Petroleum Technology
+ Rensselaer Polytechnic Institute
+ Rowan University
+ Rutgers, The State University of New Jersey
+ RVS Institute of Management Studies and Research
+ RWTH Aachen University
+ Sant Longowal Institute of Engineering Technology
+ Santa Clara University
+ Sapienza Università di Roma
+ Seoul National University
+ Seoul National University of Science and Technology
+ Shanghai Jiao Tong University
+ Shanghai University of Electric Power
+ Shanghai University of Finance and Economics
+ Shantilal Shah Engineering College
+ Sharif University of Technology
+ Shenzhen University
+ Shivaji University, Kolhapur
+ Simon Fraser University
+ Singapore University of Technology and Design
+ Sogang University
+ Sookmyung Women's University
+ Southern Connecticut State University
+ Southern New Hampshire University
+ St. Pölten University of Applied Sciences
+ Stanford University
+ State University of New York at Albany
+ State University of New York at Binghamton
+ State University of New York at Fredonia
+ Stellenbosch University
+ Stevens Institute of Technology
+ Sungkyunkwan University
+ Technion - Israel Institute of Technology
+ Technische Universität Berlin
+ Technische Universität München
+ Technische Universiteit Delft
+ Tecnológico de Monterrey, Campus Guadalajara
+ Tekirdağ Namık Kemal Üniversitesi
+ Télécom Paris
+ Telkom University
+ Texas A&M University
+ Thapar Institute of Engineering and Technology
+ Tsinghua University
+ Tufts University
+ Umeå University
+ Universidad Carlos III de Madrid
+ Universidad de Ibagué
+ Universidad de Ingeniería y Tecnología - UTEC
+ Universidad de Salamanca
+ Universidad de Zaragoza
+ Universidad del Norte, Colombia
+ Universidad Icesi
+ Universidad Militar Nueva Granada
+ Universidad Nacional Agraria La Molina
+ Universidad Nacional Autónoma de México
+ Universidad Nacional de Colombia Sede Manizales
+ Universidad Nacional de Tierra del Fuego
+ Universidad Politécnica de Chiapas
+ Universidad Politécnica de Valencia
+ Universidad Politécnica Salesiana, Cuenca
+ Universidad Rafael Landivar
+ Universidad Rey Juan Carlos
+ Universidad San Francisco de Quito
+ Universidad Tecnológica de Pereira
+ Universidad Tecnológica Nacional
+ Universidade Católica de Brasília
+ Universidade Estadual de Campinas
+ Universidade Federal de Goiás
+ Universidade Federal de Minas Gerais
+ Universidade Federal de Ouro Preto
+ Universidade Federal de Pernambuco
+ Universidade Federal de São Carlos
+ Universidade Federal de Viçosa
+ Universidade Federal do Pampa
+ Universidade Federal do Rio Grande
+ Universidade NOVA de Lisboa
+ Universidade Presbiteriana Mackenzie
+ Universidade Tecnológica Federal do Paraná
+ Università Cattolica del Sacro Cuore
+ Università degli Studi di Bari Aldo Moro
+ Università degli Studi di Brescia
+ Università degli Studi di Catania
+ Università degli Studi di Padova
+ Universitas Andalas, Padang
+ Universitas Indonesia
+ Universitas Negeri Yogyakarta
+ Universitas Udayana
+ Universität Bremen
+ Universitat de Barcelona
+ Universitat de València
+ Universität Heidelberg
+ Universität Leipzig
+ Universitat Politècnica de Catalunya
+ Universitatea Babeș-Bolyai
+ Universitatea de Vest din Timișoara
+ Université Abderrahmane Mira de Béjaïa
+ Université Clermont Auvergne
+ Université Côte d'Azur
+ Université de Caen Normandie
+ Université de Rouen Normandie
+ Université de technologie de Compiègne
+ Université Paris-Saclay
+ Université Toulouse 1 Capitole
+ University of Akron
+ University of Alabama in Huntsville
+ University of Allahabad
+ University of Applied Sciences Würzburg-Schweinfurt
+ University of Arkansas
+ University of Augsburg
+ University of Baghdad
+ University of Bath
+ University of Bordj Bou Arreridj
+ University of British Columbia
+ University of California, Berkeley
+ University of California, Irvine
+ University of California, Los Angeles
+ University of California, San Diego
+ University of California, Santa Barbara
+ University of California, Santa Cruz
+ University of Cambridge
+ University of Canberra
+ University of Catania
+ University of Cincinnati
+ University of Colorado Boulder
+ University of Connecticut
+ University of Copenhagen
+ University of Derby
+ University of Florida
+ University of Genoa
+ University of Ghana
+ University of Groningen
+ University of Hamburg
+ University of Houston
+ University of Hull
+ University of Iceland
+ University of Idaho
+ University of Illinois at Urbana-Champaign
+ University of International Business and Economics
+ University of Klagenfurt
+ University of Liège
+ University of Louisiana at Lafayette
+ University of Maryland
+ University of Maryland Baltimore County
+ University of Massachusetts Lowell
+ University of Michigan
+ University of Michigan Dearborn
+ University of Milano-Bicocca
+ University of Minnesota, Twin Cities
+ University of Moratuwa
+ University of Nebraska Omaha
+ University of New Hampshire
+ University of Newcastle
+ University of North Carolina at Chapel Hill
+ University of North Texas
+ University of Northern Philippines
+ University of Nottingham
+ University of Oslo
+ University of Pennsylvania
+ University of Pittsburgh
+ University of Rostock
+ University of São Paulo
+ University of Science and Technology of China
+ University of Southern California
+ University of Southern Maine
+ University of St Andrews
+ University of St. Thomas
+ University of Suffolk
+ University of Sydney
+ University of Szeged
+ University of Technology Sydney
+ University of Tehran
+ University of Texas at Austin
+ University of Texas at Dallas
+ University of Texas Rio Grande Valley
+ University of Udine
+ University of Warsaw
+ University of Washington
+ University of Waterloo
+ University of Wisconsin Madison
+ Univerzita Komenského v Bratislave
+ Uniwersytet Jagielloński
+ Vardhaman College of Engineering
+ Vardhman Mahaveer Open University
+ Vietnamese-German University
+ Vignana Jyothi Institute Of Management
+ Vilnius University
+ Wageningen University
+ West Virginia University
+ Western University
+ Wichita State University
+ Xavier University Bhubaneswar
+ Xi'an Jiaotong Liverpool University
+ Xiamen University
+ Xianning Vocational Technical College
+ Yale University
+ Yeshiva University
+ Yıldız Teknik Üniversitesi
+ Yonsei University
+ Yunnan University
+ Zhejiang University
diff --git a/static/post_latex/main.py b/static/post_latex/main.py index a873f207b..7dd96d63b 100644 --- a/static/post_latex/main.py +++ b/static/post_latex/main.py @@ -5,12 +5,10 @@ def _unnumber_chaps_and_secs(lines): def _startswith_unnumbered(l): - UNNUMBERED = {'\\section{Summary', - '\\section{Exercise', - '\\section{Exercises', - '\\subsection{Summary', - '\\subsection{Exercise', - '\\subsection{Exercises'} + UNNUMBERED = {'\\section{小结', + '\\section{练习', + '\\subsection{小结', + '\\subsection{练习'} for unnum in UNNUMBERED: if l.startswith(unnum): return True