first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View File

@ -0,0 +1,85 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 如何高效入门PyTorch
你好我是方远欢迎你跟我一起学习PyTorch。
先做个自我介绍吧我曾先后供职于百度和腾讯两家公司任职高级算法研究员目前在一家国际知名互联网公司Line China担任数据科学家从事计算机视觉与自然语言处理相关的研发工作每天为千万级别的流量提供深度学习服务。
想一想我进入机器学习与深度学习的研究和应用领域已经有10年的时间了这是个很有意思的过程。在人工智能快速发展的背景下各种各样的深度学习框架层出不穷有当下的主流也有如今的新秀。
为什么我会这么说呢这其实可以追溯到我的研究生时期。最早我只是把PyTorch打上了一个新秀的标签记得那时候深度学习的浪潮才刚刚兴起传统的机器学习开始转到深度学习但是我们能选的框架却十分有限。
当时在学术界流行的一个深度学习框架是Theano可能有的同学都没有听说过它。这个框架就像是祖师爷般的存在从2008年诞生之后的很长一段时间中它都是深度学习开发和研究的行业标准。
为了复现论文中的算法我开始学习Theano。接触之后我发现它的声明式编程无论是风格还是逻辑都十分奇特。而且那时候的学习资料很匮乏只能啃官方的说明文档。如此一来我觉得Theano十分晦涩难学入门门槛非常高。
后来我去到了互联网大厂的核心部门工作。那时学术界已经涌现出了很多深度学习方面的研究而工业界才刚刚开始将深度学习技术落地。Google 的 TensorFlow 框架于2015年正式开源而我们的团队也开始着手把深度学习技术应用于文本处理等方向。
2017年Google发布了TensorFlow 1.0版本到了2019年又发布了2.0新版本。TensorFlow 1.x版本时期TensorFlow框架拥有大量的用户。不过问题也非常明显主要的弊端就是框架环境配置不兼容新老版本函数差异也很大且编程困难。
但凡涉及版本更新总会出现API变化前后版本不兼容的问题。并且当我阅读别人代码的时候TensorFlow 1.x的可读性也不是很高。这些问题都增加了我的学习成本。直到TensorFlow 2.x版本TensorFlow逐渐借鉴了PyTorch的优点进行了自我完善。
而与 TensorFlow 同一时期横空出世,也拥有众多用户的一个深度学习框架还有 Keras。Keras 的 API 对用户十分友好,使用起来很容易上手。如果有什么想法需要快速实验,看一看效果,那 Keras 绝对是不二的选择。
但是高度模块化的封装也同样会带来弊端看起来学习Keras似乎十分容易但我很快就遇到了瓶颈。高度封装就意味着不够灵活比如说如果需要修改一些网络底层的结构Keras 所提供的接口就没有支持。在使用Keras的大多数时间里我们主要都停留在调用接口这个阶段很难真正学习到深度学习的内容。
直到 PyTorch 出现随着使用它的人越来越多其技术迭代速度跟生态发展速度都很迅猛。如果你在GitHub找到了一个PyTorch项目相关的开源代码我们可以很容易移植到自己的项目中来直接站在巨人的肩膀上看世界。
而且相比前面那些主流框架PyTorch有着对用户友好的命令式编程风格。PyTorch设计得更科学无需像TensorFlow那样要在各种API之间切换操作更加便捷。
PyTorch 的环境配置也很方便各种开发版本都能向下兼容不存在老版本的代码在新版本上无法使用的困扰而且PyTorch跟NumPy的风格比较像能轻易和Python生态集成起来我们只需掌握NumPy和基本的深度学习概念即可上手在网络搭建方面也是快捷又灵活。
另外PyTorch 在debug代码的过程也十分方便可以随时输出中间向量结果。用PyTorch就像在Python中使用print一样简单只要把一个pdb断点扔进PyTorch模型里直接就能用。
因为它的优雅灵活和高效可用吸引了越来越多的人学习。如果还有人只把PyTorch当成一个新秀觉得PyTroch不过是个“挑战者”试图在TensorFlow主导的世界里划出一片自己的地盘。那么数据可以证明这种想法已经时过境迁。事实上PyTorch无论在学术界还是在工业界都已经霸占了半壁江山。
从学术界来看2019年之前TensorFlow还是各大顶会论文选择的主流框架而2019年之后顶会几乎成了PyTorch的天下此消彼长PyTorch只用了一年的时间。
要知道机器学习这个领域始终是依靠研究驱动的工业界自然也不能忽视科学研究的成果。就拿我所在的团队来说现在也已经逐步向PyTorch框架迁移新开展的项目都会首选用PyTorch框架进行实现。
不得不说PyTorch的应用范围已经逐渐扩大同时也促进了其生态建设的发展。由于现在越来越多的开发者都在使用PyTroch一旦我们的程序遇到了error或bug很容易就可以在开发论坛上寻找到解决方案。
总之一旦你掌握了PyTorch就相当于走上了深度学习、机器学习的快车道。以后学习其他深度学习框架也可以快速入门融会贯通。
如果你即将或者已经进入了深度学习和机器学习相关领域PyTorch能够帮你快速实现模型与算法的验证快速完成深度学习模型部署提供高并发服务还可以轻松实现图像生成、文本分析、情感分析等有趣的实验。另外有很多算法相关的岗位也同样会要求你熟练使用PyTorch等工具。
可以探索的方向还有很多这里就不一一列举了。那么问题来了既然PyTorch有这么多优点我们要怎样快速上手呢
只看原理好比空中楼阁,而直接实战对初学者来说又相对困难。因此我推荐的方法是,先理一个整体框架,有了整体认知之后,再通过实战练习巩固认知。
具体来说我们要先把框架的基本语法大致了解一下然后尽快融入到一个实际项目当中看一看在实际任务中我们是怎么基于框架去解决一个问题的。这个专栏也正是沿着这样一个思路设计的。我在专栏里给你提供了丰富的代码和实战案例可以帮助你快速上手PyTorch。
通过这个专栏,你将会熟练使用 PyTorch 工具,解决自己的问题,这是这个专栏要实现的最基础的目标。
除了掌握工具用法之上,我希望交付的终点是让你获得分析问题的能力和解决问题的方法,让你懂得如何优化你自己的算法与模型。在学习经验方面,我希望这个专栏为你打开一扇窗,让你知道走深度学习这条路,需要有怎样的知识储备。
为了让你由入门到精通,我把专栏分成了三个递进的部分。
基础篇
简要介绍PyTorch的发展趋势与框架安装方法以及 PyTorch的前菜——NumPy的常用操作。我们约定使用PyTorch 1.9.0 版本以及默认你已经掌握了Python编程与简单的机器学习基础不过你也不用太过担心遇到新知识的我基本都会从0开始讲起的。
模型训练篇
想要快速掌握一个框架就要从核心模块入手。在这个部分会结合深度学习模型训练的一系列流程为你详解自动求导机制、搭建网络、更新模型参数、保存与加载模型、训练过程可视化、分布式训练等等模块带你具体看看PyTorch 能给我们提供怎样的帮助。通过这个部分的学习你就能基于PyTorch搭建网络模型了。
实战篇
我们整个专栏都是围绕 PyTorch 框架在具体项目实践中的应用来讲的,所以最后我还会结合当下流行的图像与自然语言处理任务,串联前面两个模块的内容,为你深入讲解 PyTorch 如何解决实际问题。
总之除了交付给你一个系统的PyTorch技术学习框架我还希望给你传递我在深度学习这条路上的经验思考。
最后给你一点建议对于学习PyTorch来说边学边查、边练边查是个很好的方法。因为在我们实际做项目的时候肯定会遇到一些之前没有使用过的函数自己去查的话可以很好地加强记忆。
当然我也会尽心做好一个引路人带你一步步实现课程目标也期待你能以更加积极的状态投入到本次的学习之旅。现在就让我们一起探索PyTorch打开深度学习的大门吧

View File

@ -0,0 +1,232 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 PyTorch网红中的顶流明星
你好,我是方远。
从这节课开始我们正式进入PyTorch基础篇的学习。
在基础篇中我们带你了解PyTorch的发展趋势与框架安装方法然后重点为你讲解NumPy和 Tensor的常用知识点。
掌握这些基础知识与技巧,能够让你使用 PyTorch 框架的时候更高效,也是从头开始学习机器学习与深度学习迈出的第一步。磨刀不误砍柴工,所以通过这个模块,我们的目标是做好学习的准备工作。
今天这节课我们先从PyTorch的安装和常用编程工具说起先让你对PyTorch用到的语言、工具、技术做到心里有数以便更好地开启后面的学习之旅。
PyTorch登场
为什么选择 PyTorch 框架我在开篇词就已经说过了。从19年起无论是学术界还是工程界 PyTorch 已经霸占了半壁江山,可以说 PyTorch 已经是现阶段的主流框架了。
这里的Py我们不陌生它就是Python那Torch是什么从字面翻译过来是一个“火炬”。
什么是火炬呢其实这跟TensorFlow中的Tensor是一个意思我们可以把它看成是能在GPU中计算的矩阵。
那PyTorch框架具体是怎么用的呢说白了就是一个计算的工具。借助它我们就能用计算机完成复杂的计算流程。
但是我们都知道,机器跟人类的“语言”并不相通,想要让机器替我们完成对数据的复杂计算,就得先把数据翻译成机器能够理解的内容。无论是图像数据、文本数据还是数值数据,都要转换成矩阵才能进行后续的变化和运算。
搞定了读入数据这一步我们就要靠PyTorch搞定后面各种复杂的计算功能。这些所有的计算功能包括了从前向传播到反向传播甚至还会涉及其它非常复杂的计算而这些计算统统要交给 PyTorch 框架实现。
PyTorch会把我们需要计算的矩阵传入到GPU或CPU当中在GPU或CPU中实现各种我们所需的计算功能。因为GPU做矩阵运算比较快所以在神经网络中的计算一般都首选使用GPU但对于学习来说我们用CPU就可以了。
而我们要做的就是设计好整个任务的流程、整个网络架构这样PyTorch才能顺畅地完成后面的计算流程从而帮我们正确地计算。
安装PyTorch及其使用环境
在 PyTorch 安装之前,还有安装 Python3 以及 pip 这些最基础的操作,这些你在网上随便搜一下就能找到,相信你可以独立完成。
这里我直接从安装 PyTorch开始说PyTorch 安装起来非常非常简单,方法也有很多,这里我们先看看最简单的方法:使用 pip 安装。
使用pip安装PyTorch
CPU版本安装
# Linux
pip install torch==1.9.0+cpu torchvision==0.10.0+cpu torchaudio==0.9.0 -f https://download.pytorch.org/whl/torch_stable.html
# Mac & Windows
pip install torch torchvision torchaudio
GPU版本安装默认CUDA 11.1 版本)
# Linux & Windows
pip install torch==1.9.0+cu111 torchvision==0.10.0+cu111 torchaudio==0.9.0 -f https://download.pytorch.org/whl/torch_stable.html
我们只需要将上面的命令复制到计算机的命令行当中,即可实现快速安装。
这里有两个版本一个GPU版本一个CPU版本。建议你最好选择安装GPU版本也就是说我们的硬件设备中最好有英伟达独立显卡。用GPU训练深度学习模型是非常快速的所以在实际项目中都是使用GPU来训练模型。
但是如果说大家手里没有供开发使用的英伟达GPU显卡的话那么安装CPU版本也是可以的在学习过程中CPU也足够让我们的小实验运行起来。
另外安装GPU版本前需要安装对应版本的CUDA工具包。我们可以到英伟达官网选择相应操作系统的CUDA工具包进行下载与安装。硬件设备中无英伟达显卡的可以略过这部分。
目前 PyTorch 的稳定版本是 1.9.0,后续如果 PyTorch 的版本升级更新了,我们再将命令中的版本号稍作修改就可以了。
其它方法安装PyTorch
这里是PyTorch的官网在页面如下图所示的位置我们可以看到有一些配置选项和安装命令。
我们可以根据页面上的指引依次选择PyTorch的版本、你的操作系统、安装方式、编程语言以及计算平台然后根据最下方的执行命令进行安装即可。
值得注意的是Mac的操作系统只能安装CPU版本。我尝试下来最简单的方式还是使用pip来安装。
验证是否安装成功
你在终端中输入“python”就可以进入到Python交互模式。
首先输入如下代码如果没有报错就意味着PyTorch已经顺利安装了。
import torch
接下来输入下面的代码如果输出结果是“True”意味着你可以使用 GPU。 这行代码的意思是检测GPU是否可用。
torch.cuda.is_available()
这里你也许会有疑问,为什么我安装的明明是 GPU 版本但是代码却返回了“False”显示GPU不可用呢
对于这个问题,我们依次按照下面的步骤进行检查。
1.检查计算机上是否有支持CUDA的GPU。
首先查看电脑的显卡型号以及是否有独立显卡如果没有以“NVIDIAN”名称开头的独立显卡则不能支持CUDA因此GPU不可用。
然后你可以在这个页面查询GPU是否支持CUDA。如果你的GPU型号在页面的列表中则表示你的计算机搭载了能够利用 CUDA 加速应用的现代 GPU否则GPU也不可用。
若GPU支持CUDA你还需要确保已经完成了上面介绍过的CUDA工具包的安装。
2.检查显卡驱动版本。
在终端中输入“nvidia-smi”命令会显示出显卡驱动的版本和CUDA的版本如下图所示。
如果显卡驱动的版本过低与CUDA版本不匹配那么GPU也不可用需要根据显卡的型号更新显卡驱动。
我用表格的方式帮你梳理了CUDA版本与GPU驱动版本的对应关系你可以根据自己计算机驱动的情况对照查看。例如CUDA 11.1支持的 Linux驱动程序是450.80.02以上。-
我们可以在这里下载并安装显卡驱动程序。
3.检查PyTorch版本和CUDA版本是否匹配
PyTorch版本同样与CUDA版本有对应关系我们可以在这个页面查看它们之间的对应关系。如果两者版本不匹配可以重新安装对应版本的PyTorch或者升级CUDA工具包。
使用Docker
通过Docker使用PyTorch也非常简单连安装都不需要但是前提是你需要熟悉有关Docker的知识。
如果你会熟练地使用Docker我推荐后面这个网页链接供你参考这里有很多的PyTorch的Docker镜像你可以找到自己需要的镜像然后拉取一个镜像到你的服务器或者本地直接启动就可以了无需额外的环境配置。
常用编程工具
在使用PyTorch进行编程之前我们先来看看几个常用的编程工具但是并不要求你必须使用它们你可以根据自己的喜好自由选择。
Sublime Text
Sublime Text是一个非常轻量且强大的文本编辑工具内置了很多快捷的功能对于我们开发来说非常便捷。
例如它可以自动为项目中的类、方法和函数生成索引让我们可以跟踪代码。具体就是通过它的goto anything功能根据一些关键字查找到项目中的对应的代码行。另外它能支持的插件功能也很丰富。
PyCharm
PyCharm 作为一款针对 Python 的编辑器,配置简单、功能强大,使用起来省时省心,对初学者十分友好。它拥有一般 IDE 所具备的功能,比如:语法高亮、项目管理、代码跳转、代码补全、调试、单元测试、版本控制等等。
Vim
Vim是Linux系统中的文本编辑工具非常方便快捷并且很强大。我们在项目中经常用到它。
在我们的项目中经常是需要登录到服务器上进行开发的服务器一般都是Linux系统不会有Sublime Text与PyCharm所以我们用Vim打开代码直接去进行编辑就可以了。
对于没有接触过Linux或者一直习惯使用IDE来编程开发的同学初步接触的时候可能觉得Vim不是很方便但实际上Vim包含了丰富的快捷键对于Shell与Python的开发来说非常高效。
但是Vim的缺点正如刚才所说你需要去学习它的使用方法有一点点门槛但是只要你学会了我保证你将对它爱不释手这里也推荐有需要的同学去看看隔壁的《Vim 实用技巧必知必会》专栏)。
Jupyter Notebook&Lab
Jupyter Notebook 是一个开源的Web应用这也是我最想推荐给你的一个工具。它能够让你创建和分享包含可执行代码、可视化结构和文字说明的文档。
在后面的课程,如果涉及图片生成或结果展示,我们也会使用到 Jupyter Notebook这里推荐你先安装好。
简而言之Jupyter Notebook是以网页的形式打开可以在网页页面中直接编写代码和运行代码代码的运行结果也会直接在代码块下显示。比如在编程过程中需要编写说明文档可以在同一个页面中直接编写便于及时说明、解释。
而 Jupyter Lab 可以看做是 Jupyter Notebook 的终极进化版它不但包含了Jupyter Notebook所有功能并且集成了操作终端、打开交互模式、查看csv文件及图片等功能。
Jupyter Notebook在我们的深度学习领域非常活跃。在实验测试阶段相比用py文件来直接编程还是Jupyter Notebook方便一些。在项目结束之后如果要书写项目报告我觉得用Jupyter也比较合适。
使用pip安装Jupyter
通过pip安装 Jupyter Notebook的命令如下。
pip install jupyter
通过pip安装Jupyter Lab的命令如下。
pip install jupyterlab
启动Jupyter
完成安装,就可以启动了。我们直接在终端中,执行下面的命令,就可以启动 Jupyter Notebook。
jupyter notebook
启动 Jupyter Lab需要在终端执行如下命令。
jupyter lab
不管在macOS系统里还是在Windows系统通过以上任意一种方式启动成功之后浏览器都会自动打开Jupyter Notebook或者Jupyter Lab的开发环境你可以回顾下“Jupyter Notebook & Lab”那个例子里的界面
运行Jupyter Notebook
进入到 Jupyter Notebook 的界面我们尝试新建一个Python的Notebook。具体操作方法如下图所示。点击“New”下拉菜单然后点击“Python 3”选项来创建一个新的Python Notebook。
上面我们已经讲过了 PyTorch 的安装方法,我们可以执行下面这段代码,来看看 PyTorch 是否已经安装成功。
import torch
torch.__version__
点击运行按钮,我们可以看到代码执行的结果,输出了当前安装的 PyTorch 的版本即PyTorch 1.9.0 的GPU版本。这说明 PyTorch 框架已经安装成功。
小结
恭喜你完成了这节课的学习。
今天我们一起了解了PyTorch 框架的用途简单来说就是能利用GPU帮我们搞定深度学习中一系列复杂运算的框架。
想要用好这个工具我们就得设计好整个任务的流程、整个网络架构这样PyTorch才能实现各种各样复杂的计算功能。
之后我们学习了PyTorch 框架的安装方法,我还给你推荐了一些深度学习编程的常用工具。其中我最推荐的工具就是 Jupyter Notebook这个工具在深度学习领域里常常会用到后面课程里涉及图片生成或者结果展示的环节我们也会用到它。
课程的准备工作就是这些让我们一起动手配置好环境并选择一个你觉得顺手的开发工具正式开始PyTorch的探索之旅吧
千里之行,始于足下。我在下节课等你,如果你有什么问题,也可以通过留言区和我交流。
我是方远,我们下一讲见!

View File

@ -0,0 +1,424 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 NumPy核心数据结构详解
你好,我是方远。
通过前面两节课我们已经对PyTorch有了初步的了解你是不是迫不及待想要动手玩转PyTorch了先别着急我们有必要先品尝一道“前菜”它就是NumPy。
为什么我们要先拿下NumPy呢我相信无论你正在从事或打算入门机器学习不接触NumPy几乎不可能。现在的主流深度学习框架PyTorch与TensorFlow中最基本的计算单元Tensor都与NumPy数组有着类似的计算逻辑可以说掌握了NumPy对学习这两种框架都有很大帮助。
另外NumPy还被广泛用在PandasSciPy等其他数据科学与科学计算的Python模块中。而我们日常用得越来越多的人脸识别技术属于计算机视觉领域其原理本质上就是先把图片转换成NumPy的数组然后再进行一系列处理。
为了让你真正吃透NumPy我会用两节课的内容讲解NumPy。这节课我们先介绍NumPy的数组、数组的关键属性以及非常重要的轴的概念。
什么是NumPy
NumPy是用于Python中科学计算的一个基础包。它提供了一个多维度的数组对象稍后展开以及针对数组对象的各种快速操作例如排序、变换选择等。NumPy的安装方式非常简单可以使用Conda安装命令如下
conda install numpy
或使用pip进行安装命令如下
pip install numpy
NumPy数组
刚才所说的数组对象是NumPy中最核心的组成部分这个数组叫做ndarray是“N-dimensional array”的缩写。其中的N是一个数字指代维度例如你常常能听到的1-D数组、2-D数组或者更高维度的数组。
在NumPy中数组是由numpy.ndarray 类来实现的它是NumPy的核心数据结构。我们今天的内容就是围绕它进行展开的。
学习一个新知识我们常用的方法就是跟熟悉的东西做对比。NumPy数组从逻辑上来看与其他编程语言中的数组是一样的索引也是从0开始。而Python中的列表其实也可以达到与NumPy数组相同的功能但它们又有差异做个对比你就能体会到NumPy数组的特点了。
1.Python中的列表可以动态地改变而NumPy数组是不可以的它在创建时就有固定大小了。改变Numpy数组长度的话会新创建一个新的数组并且删除原数组。-
2.NumPy数组中的数据类型必须是一样的而列表中的元素可以是多样的。-
3.NumPy针对NumPy数组一系列的运算进行了优化使得其速度特别快并且相对于Python中的列表同等操作只需使用更少的内存。
创建数组
那就让我们来看看NumPy数组是怎么创建的吧
最简单的方法就是把一个列表传入到np.array()或np.asarray()中这个列表可以是任意维度的。np.array()属于深拷贝np.asarray()则是浅拷贝,它们的区别我们下节课再细讲,这里你有个印象就行。
我们可以先试着创建一个一维的数组,代码如下。
>>>import numpy as np
>>>#引入一次即可
>>>arr_1_d = np.asarray([1])
>>>print(arr_1_d)
[1]
再创建一个二维数组:
>>>arr_2_d = np.asarray([[1, 2], [3, 4]])
>>>print(arr_2_d)
[[1 2]
[3 4]]
你也可以试试自己创建更高维度的数组。
数组的属性
作为一个数组NumPy有一些固有的属性我们今天来介绍非常常用且关键的数组维度、形状、size与数据类型。
ndim
ndim表示数组维度或轴的个数。刚才创建的数组arr_1_d的轴的个数就是1arr_2_d的轴的个数就是2。
>>>arr_1_d.ndim
1
>>>arr_2_d.ndim
2
shape
shape表示数组的维度或形状 是一个整数的元组元组的长度等于ndim。
arr_1_d的形状就是1一个向量 arr_2_d的形状就是(2, 2)(一个矩阵)。
>>>arr_1_d.shape
(1,)
>>>arr_2_d.shape
(2, 2)
shape这个属性在实际中用途还是非常广的。比如说我们现在有这样的数据(B, W, H, C)熟悉深度学习的同学肯定会知道这代表一个batch size 为B的WHC数据。
现在我们需要根据WHC对数据进行变形或者其他处理这时我们可以直接使用input_data.shape[1:3]获取到数据的形状,而不需要直接在程序中硬编程、直接写好输入数据的宽高以及通道数。
在实际的工作当中我们经常需要对数组的形状进行变换就可以使用arr.reshape()函数,在不改变数组元素内容的情况下变换数组的形状。但是你需要注意的是,变换前与变换后数组的元素个数需要是一样的,请看下面的代码。
>>>arr_2_d.shape
(2, 2)
>>>arr_2_d
[[1 2]
[3 4]]
# 将arr_2_d reshape为(41)的数组
>>>arr_2_d.reshape((41))
array([[1],
[2],
[3],
[4]])
我们还可以使用np.reshape(a, newshape, order)对数组a进行reshape新的形状在newshape中指定。
这里需要注意的是reshape函数有个order参数它是指以什么样的顺序读写元素其中有这样几个参数。
C默认参数使用类似C-like语言行优先中的索引方式进行读写。
F使用类似Fortran-like语言列优先中的索引方式进行读写。
A原数组如果是按照C的方式存储数组则用C的索引对数组进行reshape否则使用F的索引方式。
reshape的过程你可以这样理解首先需要根据指定的方式(CF)将原数组展开,然后再根据指定的方式写入到新的数组中。
这是什么意思呢先看一个简单的2维数组的例子。
>>>a = np.arange(6).reshape(2,3)
array([[0, 1, 2],
[3, 4, 5]])
我们要将数组a按照C的方式reshape成(3,2)可以这样操作。首先将原数组展开对于C的方式来说是行优先最后一个维度最优先改变所以展开结果如下序号那一列代表展开顺序。-
所以reshape后的数组是按照012345这个序列进行写入数据的。reshape后的数组如下表所示序号代表写入顺序。
接下来再看看将数组a按照F的方式reshape成(3,2)要如何处理。
对于行优先的方式我们应该是比较熟悉的F方式是列优先的方式这一点对于没有使用过列优先的同学来说可能比较难理解一点。
首先是按列优先展开原数组,列优先意味着最先变化的是数组的第一个维度。下表是展开后的结果,序号是展开顺序,这里请注意下坐标的变换方式(第一个维度最先变化)。
所以reshape后的数组是按照031425这个序列进行写入数据的。reshape后的数组如下表所示序号代表写入顺序为了显示直观我将相同行以同样颜色显示了。
这里我给你留一个小练习你可以试试对多维数组的reshape吗
不过大部分时候还是使用C的方式比较多也就是行优先的形式。至少目前为止我还没有使用过FA的方式。
size
size也就是数组元素的总数它就等于shape属性中元素的乘积。
请看下面的代码arr_2_d的size是4。
>>>arr_2_d.size
4
dtype
最后要说的是dtype它是一个描述数组中元素类型的对象。使用dtype属性可以查看数组所属的数据类型。
NumPy中大部分常见的数据类型都是支持的例如int8、int16、int32、float32、float64等。dtype是一个常见的属性在创建数组数据类型转换时都可以看到它。
首先我们看看arr_2_d的数据类型
>>>arr_2_d.dtype
dtype('int64')
你可以回头看一下刚才创建arr_2_d的时候我们并没有指定数据类型如果没有指定数据类型NumPy会自动进行判断然后给一个默认的数据类型。
我们再看下面的代码我们在创建arr_2_d时对数据类型进行了指定。
>>>arr_2_d = np.asarray([[1, 2], [3, 4]], dtype='float')
>>>arr_2_d.dtype
dtype('float64')
数组的数据类型当然也可以改变我们可以使用astype()改变数组的数据类型,不过改变数据类型会创建一个新的数组,而不是改变原数组的数据类型。
请看后面的代码。
>>>arr_2_d.dtype
dtype('float64')
>>>arr_2_d.astype('int32')
array([[1, 2],
[3, 4]], dtype=int32)
>>>arr_2_d.dtype
dtype('float64')
# 原数组的数据类型并没有改变
>>>arr_2_d_int = arr_2_d.astype('int32')
>>>arr_2_d_int.dtype
dtype('int32')
但是,我想提醒你,不能通过直接修改数据类型来修改数组的数据类型,这样代码虽然不会报错,但是数据会发生改变,请看下面的代码:
>>>arr_2_d.dtype
dtype('float64')
>>>arr_2_d.size
4
>>>arr_2_d.dtype='int32'
>>>arr_2_d
array([[ 0, 1072693248, 0, 1073741824],
[ 0, 1074266112, 0, 1074790400]], dtype=int32)
1个float64相当于2个int32所以原有的4个float32会变为8个int32然后直接输出这个8个int32。
其他创建数组的方式
除了使用np.asarray或np.array来创建一个数组之外NumPy还提供了一些按照既定方式来创建数组的方法我们只需按照要求提供一些必要的参数即可。
np.ones() 与np.zeros()
np.ones()用来创建一个全1的数组必须参数是指定数组的形状可选参数是数组的数据类型你可以结合下面的代码进行理解。
>>>np.ones()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ones() takes at least 1 argument (0 given)
# 报错原因是没有给定形状的参数
>>>np.ones(shape=(2,3))
array([[1., 1., 1.],
[1., 1., 1.]])
>>>np.ones(shape=(2,3), dtype='int32')
array([[1, 1, 1],
[1, 1, 1]], dtype=int32)
创建全0的数组是np.zeros()用法与np.ones()类似,我们就不举例了。
那这两个函数一般什么时候用呢例如如果需要初始化一些权重的时候就可以用上比如说生成一个2x3维的数组每个数值都是0.5,可以这样做。
>>>np.ones((2, 3)) * 0.5
array([[0.5, 0.5, 0.5],
[0.5, 0.5, 0.5]]
np.arange()
我们还可以使用np.arange([start, ]stop, [step, ]dtype=None)创建一个在[start, stop)区间的数组元素之间的跨度是step。
start是可选参数默认为0。stop是必须参数区间的终点请注意刚才说的区间是一个左闭右开区间所以数组并不包含stop。step是可选参数默认是1。
# 创建从0到4的数组
>>>np.arange(5)
array([0, 1, 2, 3, 4])
# 从2开始到4的数组
>>>np.arange(2, 5)
array([2, 3, 4])
# 从2开始到8的数组跨度是3
>>>np.arange(2, 9, 3)
array([2, 5, 8])
np.linspace()
最后我们也可以用np.linspacestart, stop, num=50, endpoint=True, retstep=False, dtype=None创建一个数组具体就是创建一个从开始数值到结束数值的等差数列。
start必须参数序列的起始值。
stop必须参数序列的终点。
num序列中元素的个数默认是50。
endpoint默认为True如果为True则数组最后一个元素是stop。
retstep默认为False如果为True则返回数组与公差。
# 从2到10有3个元素的等差数列
>>>np.linspace(start=2, stop=10, num=3)
np.arange与np.linspace也是比较常见的函数比如你要作图的时候可以用它们生成x轴的坐标。例如我要生成一个\(y=x^{2}\)的图片x轴可以用np.linespace()来生成。
import numpy as np
import matplotlib.pyplot as plt
X = np.arange(-50, 51, 2)
Y = X ** 2
plt.plot(X, Y, color='blue')
plt.legend()
plt.show()
数组的轴
这是一个非常重要的概念也是NumPy数组中最不好理解的一个概念。它经常出现在np.sum()、np.max()这样关键的聚合函数中。
我们用这样一个问题引出,同一个函数如何根据轴的不同来获得不同的计算结果呢?比如现在有一个(4,3)的矩阵存放着4名同学关于3款游戏的评分数据。
>>>interest_score = np.random.randint(10, size=(4, 3))
>>>interest_score
array([[4, 7, 5],
[4, 2, 5],
[7, 2, 4],
[1, 2, 4]])
第一个需求是,计算每一款游戏的评分总和。这个问题如何解决呢,我们一起分析一下。-
数组的轴即数组的维度它是从0开始的。对于我们这个二维数组来说有两个轴分别是代表行的0轴与代表列的1轴。如下图所示。
我们的问题是要计算每一款游戏的评分总和也就是沿着0轴的方向进行求和。所以我们只需要在求和函数中指定沿着0轴的方向求和即可。
>>> np.sum(interest_score, axis=0)
array([16, 13, 18])
计算方向如绿色箭头所示:-
第二个问题是要计算每名同学的评分总和也就是要沿着1轴方向对二维数组进行操作。所以我们只需要将axis参数设定为1即可。
>>> np.sum(interest_score, axis=1)
array([16, 11, 13, 7])
计算方向如绿色箭头所示。-
二维数组还是比较好理解的那多维数据该怎么办呢你有没有发现其实当axis=i时就是按照第i个轴的方向进行计算的或者可以理解为第i个轴的数据将会被折叠或聚合到一起。
形状为(a, b, c)的数组沿着0轴聚合后形状变为(b, c)沿着1轴聚合后形状变为(a, c)
沿着2轴聚合后形状变为(a, b);更高维数组以此类推。
接下来我们再看一个多维数组的例子。对数组a求不同维度上的最大值。
>>> a = np.arange(18).reshape(3,2,3)
>>> a
array([[[ 0, 1, 2],
[ 3, 4, 5]],
[[ 6, 7, 8],
[ 9, 10, 11]],
[[12, 13, 14],
[15, 16, 17]]])
我们可以将同一个轴上的数据看做同一个单位,那聚合的时候,我们只需要在同级别的单位上进行聚合就可以了。-
如下图所示绿框代表沿着0轴方向的单位蓝框代表着沿着1轴方向的单位红框代表着2轴方向的单位。
当axis=0时就意味着将三个绿框的数据聚合在一起结果是一个23的数组数组内容为$\(\\begin{matrix}-
\[ \\ \[(max(a\_{000},a\_{100},a\_{200}), max(a\_{001},a\_{101},a\_{201}), max(a\_{002},a\_{102},a\_{202}))\], \\\\\\-
\[(max(a\_{010},a\_{110},a\_{210}), max(a\_{011},a\_{111},a\_{211}), max(a\_{012},a\_{112},a\_{212}))\] \\ \] \\-
\\end{matrix}\)$
代码如下:
>>> a.max(axis=0)
array([[12, 13, 14],
[15, 16, 17]])
当axis=1时就意味着每个绿框内的蓝框聚合在一起结果是一个33的数组数组内容为-
$\(\\begin{matrix}-
\[ \\ \[(max(a\_{000},a\_{010}), max(a\_{001},a\_{011}), max(a\_{002},a\_{012}))\], \\\\\\-
\[(max(a\_{100},a\_{110}), max(a\_{101},a\_{111}), max(a\_{102},a\_{112}))\], \\\\\\-
\[(max(a\_{200},a\_{210}), max(a\_{201},a\_{211}), max(a\_{202},a\_{212}))\], \\ \] \\-
\\end{matrix}-
\)$
代码如下:
>>> a.max(axis=1)
array([[ 3, 4, 5],
[ 9, 10, 11],
[15, 16, 17]])
当axis=2时就意味着每个蓝框中的红框聚合在一起结果是一个32的数组数组内容如下所示-
$\(\\begin{matrix}-
\[ \\ \[(max(a\_{000},a\_{001},a\_{002}), max(a\_{010},a\_{011},a\_{012}))\], \\\\\\-
\[(max(a\_{100},a\_{101},a\_{102}), max(a\_{110},a\_{111},a\_{112}))\], \\\\\\-
\[(max(a\_{200},a\_{201},a\_{202}), max(a\_{210},a\_{211},a\_{212}))\], \\ \] \\\\\\-
\\end{matrix}-
\)$
代码如下:
>>> a.max(axis=2)
array([[ 2, 5],
[ 8, 11],
[14, 17]])
axis参数非常常见不光光出现在刚才介绍的sum与max还有很多其他的聚合函数也会用到例如min、mean、argmin求最小值下标、argmax求最大值下标等。
小结
恭喜你完成了这节课的学习。其实你只要有一些其他语言的编程基础学Numpy还是非常容易的。这里我想再次强调一下为什么NumPy这道前菜必不可少。
其实Numpy的很多知识点是与PyTorch融会贯通的例如PyTorch中的Tensor。而且Numpy在机器学习中常常被用到很多模块都要基于NumPy展开尤其是在数据的预处理和膜后处理中。
NumPy是用于Python中科学计算的一个基础包。它提供了一个多维度的数组对象以及针对数组对象的各种快速操作。为了让你有更直观的体验我们学习了创建数组的四种方式。
其中你重点要掌握的方法就是如何使用np.asarray创建一个数组。这里涉及数组属性ndim、shape、dtype、size的灵活使用特别是数组的形状变化与数据类型转换。
最后我为你介绍了数组轴的概念我们需要在数组的聚合函数中灵活运用它。虽然这个概念十分常用但却不好理解建议你根据我课程里的例子仔细揣摩一下从2维数组一步步推理到多维数组根据轴的不同数组聚合的方向是如何变化的。
下一节课我们要继续学习NumPy中常用且重要的功能。
每课一练
在刚才用户对游戏评分的那个问题中,你能计算一下每位用户对三款游戏的打分的平均分吗?
欢迎你在留言区记录你的疑问或者收获,也推荐你把这节课分享给你的朋友。

View File

@ -0,0 +1,354 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 NumPy深度学习中的常用操作
你好,我是方远。
通过上节课的学习我们已经对NumPy数组有了一定的了解正所谓实践出真知今天我们就以一个图像分类的项目为例看看NumPy的在实际项目中都有哪些重要功能。
我们先从一个常见的工作场景出发互联网教育推荐平台每天都有千万量级的文字与图片的广告信息流入。为了给用户提供更加精准的推荐你的老板交代你设计一个模型让你把包含各个平台Logo比如包含极客时间Logo的图片自动找出来。
想要解决这个图片分类问题我们可以分解成数据加载、训练与模型评估三部分其实基本所有深度学习的项目都可以这样划分。其中数据加载跟模型评估中就经常会用到NumPy数组的相关操作。
那么我们先来看看数据的加载。
数据加载阶段
这个阶段我们要做的就是把训练数据读进来,然后给模型训练使用。训练数据不外乎这三种:图片、文本以及类似二维表那样的结构化数据。
不管使用PyTorch还是TensorFlow或者是传统机器学习的scikit-learn我们在读入数据这一块都会先把数据转换成NumPy的数组然后再进行后续的一系列操作。
对应到我们这个项目中需要做的就是把训练集中的图片读入进来。对于图片的处理我们一般会使用Pillow与OpenCV这两个模块。
虽然Pillow和OpenCV功能看上去都差不多但还是有区别的。在PyTorch中很多图片的操作都是基于Pillow的所以当使用PyTorch编程出现问题或者要思考、解决一些图片相关问题时要从Pillow的角度出发。
下面我们先以单张图片为例将极客时间的那张Logo图片分别用Pillow与OpenCV读入然后转换为NumPy的数组。
Pillow方式
首先我们需要使用Pillow中的下述代码读入上面的图片。
from PIL import Image
im = Image.open('jk.jpg')
im.size
输出: 318, 116
Pillow是以二进制形式读入保存的那怎么转为NumPy格式呢这个并不难我们只需要利用NumPy的asarray方法就可以将Pillow的数据转换为NumPy的数组格式。
import numpy as np
im_pillow = np.asarray(im)
im_pillow.shape
输出:(116, 318, 3)
OpenCV方式
OpenCV的话不再需要我们手动转格式它直接读入图片后就是以NumPy数组的形式来保存数据的如下面的代码所示。
import cv2
im_cv2 = cv2.imread('jk.jpg')
type(im_cv2)
输出numpy.ndarray
im_cv2.shape
输出:(116, 318, 3)
结合代码输出可以发现我们读入后的数组的最后一个维度是3这是因为图片的格式是RGB格式表示有R、G、B三个通道。对于计算视觉任务来说绝大多数处理的图片都是RGB格式如果不是RGB格式的话要记得事先转换成RGB格式。-
这里有个地方需要你关注Pillow读入后通道的顺序就是R、G、B而OpenCV读入后顺序是B、G、R。
模型训练时的通道顺序需与预测的通道顺序要保持一致。也就是说使用Pillow训练使用OpenCV读入图片直接进行预测的话不会报错但结果会不正确所以大家一定要注意。
接下来我们就验证一下Pillow与OpenCV读入数据通道的顺序是否如此借此引出有关Numpy数组索引与切片、合并等常见问题。
怎么验证这条结论呢只需要将R、G、B三个通道的数据单独提取出来然后令另外两个通道的数据全为0即可。
这里我给你说说为什么这样做。RGB色彩模式是工业界的一种颜色标准RGB分别代表红、绿、蓝三个通道的颜色将这三种颜色混合在一起就形成了我们眼睛所能看到的所有颜色。
RGB三个通道各有256个亮度分别用数字0到255表示数字越高代表亮度越强数字0则是代表最弱的亮度。在我们的例子中如果一个通道的数据再加另外两个全0的通道相当于关闭另外两个通道最终图像以红色格调可以先看一下后文中的最终输出结果呈现出来的话我们就可以认为该通道的数据是来源于R通道G与B通道的证明同样可以如此。
首先我们提取出RGB三个通道的数据这可以从数组的索引与切片说起。
索引与切片
如果你了解Python那么索引和切片的概念你应该不陌生。
就像图书目录里的索引我们可以根据索引标注的页码快速找到需要的内容而Python
里的索引也是同样的功能,它用来定位数组中的某一个值。而切片意思就相当于提取图书中从某一页到某一页的内容。
NumPy数组的索引方式与Python的列表的索引方式相同也同样支持切片索引。
这里需要你注意的是在NumPy数组中经常会出现用冒号来检索数据的形式如下所示
im_pillow[:, :, 0]
这是什么意思呢?我们一起来看看。“:”代表全部选中的意思。我们的图片读入后,会以下图的状态保存在数组中。
上述代码的含义就是取第三个维度索引为0的全部数据换句话说就是取图片第0个通道的所有数据。
这样的话,通过下面的代码,我们就可以获得每个通道的数据了。
im_pillow_c1 = im_pillow[:, :, 0]
im_pillow_c2 = im_pillow[:, :, 1]
im_pillow_c3 = im_pillow[:, :, 2]
获得了每个通道的数据接下来就需要生成一个全0数组该数组要与im_pillow具有相同的宽高。
全0数组你还记得怎么生成吗可以自己先思考一下生成的代码如下所示。
zeros = np.zeros((im_pillow.shape[0], im_pillow.shape[1], 1))
zeros.shape
输出:(116, 318, 1)
然后我们只需要将全0的数组与im_pillow_c1、im_pillow_c2、im_pillow_c3进行拼接就可以获得对应通道的图像数据了。
数组的拼接
刚才我们拿到了单独通道的数据接下来就需要把一个分离出来的数据跟一个全0数组拼接起来。如下图所示红色的可以看作单通道数据白色的为全0数据。
NumPy数组为我们提供了np.concatenate((a1, a2, …), axis=0)方法进行数组拼接。其中a1a2, …就是我们要合并的数组axis是我们要沿着哪一个维度进行合并默认是沿着0轴方向。
对于我们的问题是要沿着2轴的方向进行合并也是我们最终的目标是要获得下面的三幅图像。-
那么我们先将im_pillow_c1与全0数组进行合并生成上图中最左侧的数组有了图像的数组才能获得最终图像。合并的代码跟输出结果如下
im_pillow_c1_3ch = np.concatenate((im_pillow_c1, zeros, zeros),axis=2)
---------------------------------------------------------------------------
AxisError Traceback (most recent call last)
<ipython-input-21-e3d53c33c94d> in <module>
----> 1 im_pillow_c1_3ch = np.concatenate((im_pillow_c1, zeros, zeros),axis=2)
<__array_function__ internals> in concatenate(*args, **kwargs)
AxisError: axis 2 is out of bounds for array of dimension 2
看到这里你可能很惊讶竟然报错了错误的原因是在2维数组中axis如果等于2的话会越界。
我们看看im_pillow_c1与zeros的形状。
im_pillow_c1.shape
输出:(116, 318)
zeros.shape
输出:(116, 318, 1)
原来是我们要合并的两个数组维度不一样啊。那么如何统一维度呢将im_pillow_c1变成(116, 318, 1)即可。
方法一使用np.newaxis
我们可以使用np.newaxis让数组增加一个维度使用方式如下。
im_pillow_c1 = im_pillow_c1[:, :, np.newaxis]
im_pillow_c1.shape
输出:(116, 318, 1)
运行上面的代码就可以将2个维度的数组转换为3个维度的数组了。-
这个操作在你看深度学习相关代码的时候经常会看到只不过PyTorch中的函数名unsqueeze(), TensorFlow的话是与NumPy有相同的名字直接使用tf.newaxis就可以了。
然后我们再次将im_pillow_c1与zeros进行合并这时就不会报错了代码如下所示
im_pillow_c1_3ch = np.concatenate((im_pillow_c1, zeros, zeros),axis=2)
im_pillow_c1_3ch.shape
输出:(116, 318, 3)
方法二:直接赋值
增加维度的第二个方法就是直接赋值其实我们完全可以生成一个与im_pillow形状完全一样的全0数组然后将每个通道的数值赋值为im_pillow_c1、im_pillow_c2与im_pillow_c3就可以了。我们用这种方式生成上图中的中间与右边图像的数组。
im_pillow_c2_3ch = np.zeros(im_pillow.shape)
im_pillow_c2_3ch[:,:,1] = im_pillow_c2
im_pillow_c3_3ch = np.zeros(im_pillow.shape)
im_pillow_c3_3ch[:,:,2] = im_pillow_c3
这样的话我们就可以将三个通道的RGB图片打印出来了。-
关于绘图你可以使用matplotlib进行绘图它是NumPy的绘图库。如果你需要绘图可以在这个网站上找到各种各样的例子然后根据它提供的代码进行修改具体如何绘图我就不展开了。
说回我们的通道顺序验证问题完成前面的操作后你可以用下面的代码将原图、R通道、G通道与B通道的4幅图打印出来你看是不是RGB顺序的呢
from matplotlib import pyplot as plt
plt.subplot(2, 2, 1)
plt.title('Origin Image')
plt.imshow(im_pillow)
plt.axis('off')
plt.subplot(2, 2, 2)
plt.title('Red Channel')
plt.imshow(im_pillow_c1_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 3)
plt.title('Green Channel')
plt.imshow(im_pillow_c2_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 4)
plt.title('Blue Channel')
plt.imshow(im_pillow_c3_3ch.astype(np.uint8))
plt.axis('off')
plt.savefig('./rgb_pillow.png', dpi=150)
深拷贝(副本)与浅拷贝(视图)
刚才我们通过获取图片通道数据的练习,不过操作确实比较繁琐,介绍这些方法也主要是为了让你掌握切片索引和数组拼接的知识点。
其实我们还有一种更加简单的方式获得三个通道的BGR数据只需要将图片读入后直接将其中的两个通道赋值为0即可。代码如下所示
from PIL import Image
import numpy as np
im = Image.open('jk.jpg')
im_pillow = np.asarray(im)
im_pillow[:,:,1:]=0
输出:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-146-789bda58f667> in <module>
4 im = Image.open('jk.jpg')
5 im_pillow = np.asarray(im)
----> 6 im_pillow[:,:,1:-1]=0
ValueError: assignment destination is read-only
运行刚才的代码报错提示说数组是只读数组没办法进行修改。那怎么办呢我们可以使用copy来复制一个数组。-
说到copy()的话就要说到浅拷贝与深拷贝的概念上节课我们说到创建数组时就提过np.array()属于深拷贝np.asarray()则是浅拷贝。
简单来说浅拷贝或称视图指的是与原数组共享数据的数组请注意只是数据没有说共享形状。视图我们通常使用view()来创建。常见的切片操作也会返回对原数组的浅拷贝。
请看下面的代码数组a与b的数据是相同的形状确实不同但是修改b中的数据后a的数据同样会发生变化。
a = np.arange(6)
print(a.shape)
输出:(6,)
print(a)
输出:[0 1 2 3 4 5]
b = a.view()
print(b.shape)
输出:(6,)
b.shape = 2, 3
print(b)
输出:[[0 1 2]
[3 4 5]]
b[0,0] = 111
print(a)
输出:[111 1 2 3 4 5]
print(b)
输出:[[111 1 2]
[ 3 4 5]]
而深拷贝又称副本也就是完全复制原有数组创建一个新的数组修改新的数组不会影响原数组。深拷贝使用copy()方法。
所以,我们将刚才报错的程序修改成下面的形式就可以了。
im_pillow = np.array(im)
im_pillow[:,:,1:]=0
可别小看深拷贝和浅拷贝的区别。这里讲一个我以前遇到的坑吧,我曾经要开发一个部署在手机端的人像分割模型。
为了提高模型的分割效果,我考虑了新的实验方法——将前一帧的数据也作为当前帧的输入进行考虑,训练阶段没有发生问题,但是在调试阶段发现模型的效果非常差。
后来经过研究,我才发现了问题的原因。原因是我为了可视化分割效果,我将前一帧的数据进行变换打印出来。同时,我错误的采用了浅拷贝的方式,将前一帧的数据传入当前帧,所以说传入到当前帧的数据是经过变化的,而不是原始的输出。
这时再传入当前帧,自然无法得到正确结果。当时因为这个坑,差点产生要放弃这个实验的想法,后面改成深拷贝才解决了问题。
好了讲到这里你是否可以用上述的方法对OpenCV读取图片读入通道顺序进行一下验证呢
模型评估
在模型评估时,我们一般会将模型的输出转换为对应的标签。
假设现在我们的问题是将图片分为2个类别包含极客时间的图片与不包含的图片。模型会输出形状为(2, )的数组我们把它叫做probs它存储了两个概率我们假设索引为0的概率是包含极客时间图片的概率另一个是其它图片的概率它们两个概率的和为1。如果极客时间对应的概率大则可以推断该图片为包含极客时间的图片否则为其他图片。
简单的做法就是判断probs[0]是否大于0.5如果大于0.5,则可以认为图片是我们要寻找的。
这种方法固然可以,但是如果我们需要判断图片的类别有很多很多种呢?
例如有1000个类别的ImageNet。也许你会想到遍历这个数组求出最大值对应的索引。
那如果老板让你找出概率最大的前5个类别呢有没有更简单点的方法我们继续往下看。
Argmax Vs Argmin求最大/最小值对应的索引
NumPy的argmax(a, axis=None)方法可以为我们解决求最大值索引的问题。如果不指定axis则将数组默认为1维。
对于我们的问题,使用下述代码即可获得拥有最大概率值的图片。
np.argmax(probs)
Argmin的用法跟Argmax差不多不过它的作用是获得具有最小值的索引。
Argsort数组排序后返回原数组的索引
那现在我们再把问题升级一下比如需要你将图片分成10个类别要找到具有最大概率的前三个类别。
模型输出的概率如下:
probs = np.array([0.075, 0.15, 0.075, 0.15, 0.0, 0.05, 0.05, 0.2, 0.25])
这时我们就可以借助argsort(a, axis=-1, kind=None)函数来解决该问题。np.argsort的作用是对原数组进行从小到大的排序返回的是对应元素在原数组中的索引。-
np.argsort包括后面这几个关键参数
a是要进行排序的原数组
axis是要沿着哪一个轴进行排序默认是-1也就是最后一个轴
kind是采用什么算法进行排序默认是快速排序还有其他排序算法具体你可以看看数据结构的排序算法。
我们还是结合例子来理解你可以看看下面的代码它描述了我们使用argsort对probs进行排序然后返回对应坐标的全过程。
probs_idx_sort = np.argsort(-probs) #注意,加了负号,是按降序排序
probs_idx_sort
输出array([8, 7, 1, 3, 0, 2, 5, 6, 4])
#概率最大的前三个值的坐标
probs_idx_sort[:3]
输出array([8, 7, 1])
小结
恭喜你,完成了这一节课的学习。这一节介绍了一些常用且重要的功能。几乎在所有深度学习相关的项目中,你都会常常用到这些函数,当你阅读别人的代码的时候也会经常看到。
让我们一起来复习一下今天学到的这些函数,我画了一张表格,给你总结了它们各自的关键功能和使用要点。
我觉得NumPy最难懂的还是上节课的轴如果你把轴的概念理解清楚之后理解今天的内容会更加轻松。理解了原理之后关键还是动手练习。
每课一练
给定数组scores形状为2562562scores[: , :, 0] 与scores[:, :, 1]对应位置元素的和为1现在我们要根据scores生产数组mask要求scores通道0的值如果大于通道1的值则mask对应的位置为0否则为1。
scores如下你可以试试用代码实现
scores = np.random.rand(256, 256, 2)
scores[:,:,1] = 1 - scores[:,:,0]
欢迎你在留言区记录你的疑问或者收获,也推荐你把这节课分享给你的朋友。

View File

@ -0,0 +1,301 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 TensorPyTorch中最基础的计算单元
在上节课中我们一起学习了NumPy的主要使用方法和技巧有了NumPy我们可以很好地处理各种类型的数据。而在深度学习中数据的组织则更进一步从数据的组织到模型内部的参数都是通过一种叫做张量的数据结构进行表示和处理。
今天我们就来一块儿了解一下张量Tensor学习一下Tensor的常用操作。
什么是Tensor
Tensor是深度学习框架中极为基础的概念也是PyTroch、TensorFlow中最重要的知识点之一它是一种数据的存储和处理结构。
回忆一下我们目前知道的几种数据表示:
标量也称Scalar是一个只有大小没有方向的量比如1.8、e、10等。
向量也称Vector是一个有大小也有方向的量比如(1,2,3,4)等。
矩阵也称Matrix是多个向量合并在一起得到的量比如[(1,2,3),(4,5,6)]等。
为了帮助你更好理解标量、向量和矩阵,我特意准备了一张示意图,你可以结合图片理解。-
不难发现,几种数据表示其实都是有着联系的,标量可以组合成向量,向量可以组合成矩阵。那么,我们可否将它们看作是一种数据形式呢?
答案是可以的这种统一的数据形式在PyTorch中我们称之为张量(Tensor)。从标量、向量和矩阵的关系来看你可能会觉得它们就是不同“维度”的Tensor这个说法对也不全对。
说它不全对是因为在Tensor的概念中我们更愿意使用Rank来表示这种“维度”比如标量就是Rank为0阶的Tensor向量就是Rank为1阶的Tensor矩阵就是Rank为2阶的Tensor。也有Rank大于2的Tensor。当然啦你如果说维度其实也没什么错误平时很多人也都这么叫。
说完Tensor的含义我们一起看一下Tensor的类型以及如何创建Tensor。
Tensor的类型、创建及转换
在不同的深度学习框架下Tensor呈现的特点大同小异我们使用它的方法也差不多。这节课我们就以PyTorch中的使用方法为例进行学习。
Tensor的类型
在PyTorch中Tensor支持的数据类型有很多种这里列举较为常用的几种格式
一般来说torch.float32、torch.float64、torch.uint8和torch.int64用得相对较多一些但是也不是绝对还是要根据实际情况进行选择。这里你有个印象就行后面课程用到时我还会进一步讲解。
Tensor的创建
PyTorch对于Tensor的操作已经非常友好了你可以通过多种不同的方式创建一个任意形状的Tensor而且每种方式都很简便我们一起来看一下。
直接创建
首先来看直接创建的方法这也是最简单创建的方法。我们需要用到下面的torch.tensor函数直接创建。
torch.tensor(data, dtype=None, device=None,requires_grad=False)
结合代码,我们看看其中的参数是什么含义。-
我们从左往右依次来看首先是data也就是我们要传入模型的数据。PyTorch支持通过list、 tuple、numpy array、scalar等多种类型进行数据传入并转换为tensor。
接着是dtype它声明了你需要返回一个怎样类型的Tensor具体类型可以参考前面表格里列举的Tensor的8种类型。
然后是device这个参数指定了数据要返回到的设备目前暂时不需要关注缺省即可。
最后一个参数是requires_grad用于说明当前量是否需要在计算中保留对应的梯度信息。在PyTorch中只有当一个Tensor设置requires_grad为True的情况下才会对这个Tensor以及由这个Tensor计算出来的其他Tensor进行求导然后将导数值存在Tensor的grad属性中便于优化器来更新参数。
所以你需要注意的是把requires_grad设置成true或者false要灵活处理。如果是训练过程就要设置为true目的是方便求导、更新参数。而到了验证或者测试过程我们的目的是检查当前模型的泛化能力那就要把requires_grad设置成Fasle避免这个参数根据loss自动更新。
从NumPy中创建
还记得之前的课程中我们一同学习了NumPy的使用在实际应用中我们在处理数据的阶段多使用的是NumPy而数据处理好之后想要传入PyTorch的深度学习模型中则需要借助Tensor所以PyTorch提供了一个从NumPy转到Tensor的语句
torch.from_numpy(ndarry)
有时候我们在开发模型的过程中需要用到一些特定形式的矩阵Tensor比如全是0的或者全是1的。这时我们就可以用这个方法创建比如说先生成一个全是0的NumPy数组然后转换成Tensor。但是这样也挺麻烦的因为这意味着你要引入更多的包NumPy也会使用更多的代码这会增加出错的可能性。-
不过你别担心PyTorch内部已经提供了更为简便的方法我们接着往下看。
创建特殊形式的Tensor
我们一块来看一下后面的几个常用函数它们都是在PyTorch模型内部使用的。
创建零矩阵Tensor零矩阵顾名思义就是所有的元素都为0的矩阵。
torch.zeros(*size, dtype=None...)
其中我们用得比较多的就是size参数和dtype参数。size定义输出张量形状的整数序列。-
这里你可能注意到了在函数参数列表中我加入了省略号这意味着torch.zeros的参数有很多。不过。咱们现在是介绍零矩阵的概念形状相对来说更重要。其他的参数比如前面提到的requires_grad参数与此无关现阶段我们暂时不关注。
创建单位矩阵Tensor单位矩阵是指主对角线上的元素都为1的矩阵。
torch.eye(size, dtype=None...)
创建全一矩阵Tensor全一矩阵顾名思义就是所有的元素都为1的矩阵。
torch.ones(size, dtype=None...)
创建随机矩阵Tensor在PyTorch中有几种较为经常使用的随机矩阵创建方式分别如下。
torch.rand(size)
torch.randn(size)
torch.normal(mean, std, size)
torch.randint(low, high, size
这些方式各自有不同的用法,你可以根据自己的需要灵活使用。
torch.rand用于生成数据类型为浮点型且维度指定的随机Tensor随机生成的浮点数据在 0~1 区间均匀分布。
torch.randn用于生成数据类型为浮点型且维度指定的随机Tensor随机生成的浮点数的取值满足均值为 0、方差为 1 的标准正态分布。
torch.normal用于生成数据类型为浮点型且维度指定的随机Tensor可以指定均值和标准差。
torch.randint用于生成随机整数的Tensor其内部填充的是在[low,high)均匀生成的随机整数。
Tensor的转换
在实际项目中我们接触到的数据类型有很多比如Int、list、NumPy等。为了让数据在各个阶段畅通无阻不同数据类型与Tensor之间的转换就非常重要了。接下来我们一起来看看int、list、NumPy是如何与Tensor互相转换的。
Int与Tensor的转换
a = torch.tensor(1)
b = a.item()
我们通过torch.Tensor将一个数字或者标量转换为Tensor又通过item()函数将Tensor转换为数字标量item()函数的作用就是将Tensor转换为一个python number。
list与tensor的转换
a = [1, 2, 3]
b = torch.tensor(a)
c = b.numpy().tolist()
在这里对于一个list a我们仍旧直接使用torch.Tensor就可以将其转换为Tensor了。而还原回来的过程要多一步需要我们先将Tensor转为NumPy结构之后再使用tolist()函数得到list。
NumPy与Tensor的转换
有了前面两个例子你是否能想到NumPy怎么转换为Tensor么我们仍旧torch.Tensor即可是不是特别方便。
CPU与GPU的Tensor之间的转换
CPU->GPU: data.cuda()
GPU->CPU: data.cpu()
Tensor的常用操作
刚才我们一起了解了Tensor的类型如何创建Tensor以及如何实现Tensor和一些常见的数据类型之间的相互转换。其实Tensor还有一些比较常用的功能比如获取形状、维度转换、形状变换以及增减维度接下来我们一起来看看这些功能。
获取形状
在深度学习网络的设计中我们需要时刻对Tensor的情况做到了如指掌其中就包括获取Tensor的形式、形状等。
为了得到Tensor的形状我们可以使用shape或size来获取。两者的不同之处在于shape是torch.tensor的一个属性而size()则是一个torch.tensor拥有的方法。
>>> a=torch.zeros(2, 3, 5)
>>> a.shape
torch.Size([2, 3, 5])
>>> a.size()
torch.Size([2, 3, 5])
知道了Tensor的形状我们就能知道这个Tensor所包含的元素的数量了。具体的计算方法就是直接将所有维度的大小相乘比如上面的Tensor a所含有的元素的个数为2_3_5=30个。这样似乎有点麻烦我们在PyTorch中可以使用numel()函数直接统计元素数量。
>>> a.numel()
30
矩阵转秩(维度转换)
在PyTorch中有两个函数分别是permute()和transpose()可以用来实现矩阵的转秩或者说交换不同维度的数据。比如在调整卷积层的尺寸、修改channel的顺序、变换全连接层的大小的时候我们就要用到它们。
其中用permute函数可以对任意高维矩阵进行转置但只有 tensor.permute() 这个调用方式,我们先看一下代码:
>>> x = torch.rand(2,3,5)
>>> x.shape
torch.Size([2, 3, 5])
>>> x = x.permute(2,1,0)
>>> x.shape
torch.Size([5, 3, 2])
有没有发现原来的Tensor的形状是[2,3,5]我们在permute中分别写入原来索引位置的新位置x.permute(2,1,0)2表示原来第二个维度现在放在了第零个维度同理1表示原来第一个维度仍旧在第一个维度0表示原来第0个维度放在了现在的第2个维度形状就变成了[5,3,2]
而另外一个函数transpose不同于permute它每次只能转换两个维度或者说交换两个维度的数据。我们还是来看一下代码
>>> x.shape
torch.Size([2, 3, 4])
>>> x = x.transpose(1,0)
>>> x.shape
torch.Size([3, 2, 4])
需要注意的是经过了transpose或者permute处理之后的数据变得不再连续了什么意思呢
还是接着刚才的例子说我们使用torch.rand(2,3,4)得到的tensor在内存中是连续的但是经过transpose或者permute之后呢比如transpose(1,0)内存虽然没有变化但是我们得到的数据“看上去”是第0和第1维的数据发生了交换现在的第0维是原来的第1维所以Tensor都会变得不再连续。
那你可能会问了不连续就不连续呗好像也没啥影响吧这么想你就草率了我们继续来看看Tensor的形状变换学完以后你就知道Tensor不连续的后果了。
形状变换
在PyTorch中有两种常用的改变形状的函数分别是view和reshape。我们先来看一下view。
>>> x = torch.randn(4, 4)
>>> x.shape
torch.Size([4, 4])
>>> x = x.view(2,8)
>>> x.shape
torch.Size([2, 8])
我们先声明了一个[4, 4]大小的Tensor然后通过view函数将其修改为[2, 8]形状的Tensor。我们还是继续刚才的x再进行一步操作代码如下
>>> x = x.permute(1,0)
>>> x.shape
torch.Size([8, 2])
>>> x.view(4, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
结合代码可以看到利用permute我们将第0和第1维度的数据进行了变换得到了[8, 2]形状的Tensor在这个新Tensor上进行view操作忽然就报错了为什么呢其实就是因为view不能处理内存不连续Tensor的结构。-
那这时候要怎么办呢我们可以使用另一个函数reshape
>>> x = x.reshape(4, 4)
>>> x.shape
torch.Size([4, 4])
这样问题就迎刃而解了。其实reshape相当于进行了两步操作先把Tensor在内存中捋顺了然后再进行view操作。
增减维度
有时候我们需要对Tensor增加或者删除某些维度比如删除或者增加图片的几个通道。PyTorch提供了squeeze()和unsqueeze()函数解决这个问题。
我们先来看squeeze()。如果dim指定的维度的值为1则将该维度删除若指定的维度值不为1则返回原来的Tensor。为了方便你理解我还是结合例子来讲解。
>>> x = torch.rand(2,1,3)
>>> x.shape
torch.Size([2, 1, 3])
>>> y = x.squeeze(1)
>>> y.shape
torch.Size([2, 3])
>>> z = y.squeeze(1)
>>> z.shape
torch.Size([2, 3])
结合代码我们可以看到,我们新建了一个维度为[2, 1, 3]的Tensor然后将第1维度的数据删除得到ysqueeze执行成功是因为第1维度的大小为1。然而在y上我们打算进一步删除第1维度的时候就会发现删除失败了这是因为y此刻的第1维度的大小为3suqeeze不能删除。-
unsqueeze()这个函数主要是对数据维度进行扩充。给指定位置加上维数为1的维度我们同样结合代码例子来看看。
>>> x = torch.rand(2,1,3)
>>> y = x.unsqueeze(2)
>>> y.shape
torch.Size([2, 1, 1, 3])
这里我们新建了一个维度为[2, 1, 3]的Tensor然后在第2维度插入一个维度这样就得到了一个[2,1,1,3]大小的tensor。
小结
之前我们学习了NumPy相关的操作如果把NumPy和Tensor做对比就不难发现它们之间有很多共通的内容共性就是两者都是数据的表示形式都可以看作是科学计算的通用工具。但是NumPy和Tensor的用途是不一样的NumPy不能用于GPU加速Tensor则可以。
这节课我们一同学习了Tensor的创建、类型、转换、变换等常用功能通过这几个功能我们就可以对Tensor进行最基本也是最常用的操作这些都是必须要牢记的内容。
此外,在实际上,真正的项目实战中还有个非常多的操作种类,其中较为重要的是数学计算操作,比如加减乘除、合并、连接等。但是这些操作如果一个一个列举出来,数量极其繁多,你也会感觉很枯燥,所以在后续的课程中,咱们会在具体的实战环节来学习相关的数学操作。
下一节课的内容咱们会对Tensor的变形、切分等高级操作进行学习这是一个很好玩儿的内容敬请期待。
每课一练
在PyTorch中有torch.Tensor()和torch.tensor()两种函数,它们的区别是什么呢?
欢迎你在留言区和我交流,也推荐你把今天的内容分享给更多同事和朋友。

View File

@ -0,0 +1,427 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 Tensor变形记快速掌握Tensor切分、变形等方法
你好,我是方远。
上节课我们一起学习了Tensor的基础概念也熟悉了创建、转换、维度变换等操作掌握了这些基础知识你就可以做一些简单的Tensor相关的操作了。
不过要想在实际的应用中更灵活地用好TensorTensor的连接、切分等操作也是必不可少的。今天这节课咱们就通过一些例子和图片来一块学习下。虽然这几个操作比较有难度但只要你耐心听我讲解然后上手练习还是可以拿下的。
Tensor的连接操作
在项目开发中,深度学习某一层神经元的数据可能有多个不同的来源,那么就需要将数据进行组合,这个组合的操作,我们称之为连接。
cat
连接的操作函数如下。
torch.cat(tensors, dim = 0, out = None)
cat是concatnate的意思也就是拼接、联系的意思。该函数有两个重要的参数需要你掌握。
第一个参数是tensors它很好理解就是若干个我们准备进行拼接的Tensor。
第二个参数是dim我们回忆一下Tensor的定义Tensor的维度是有多种情况的。比如有两个3维的Tensor可以有几种不同的拼接方式如下图dim参数就可以对此作出约定。
看到这里你可能觉得上面画的图是三维的看起来比较晦涩所以咱们先从简单的二维的情况说起我们先声明两个3x3的矩阵代码如下
>>> A=torch.ones(3,3)
>>> B=2*torch.ones(3,3)
>>> A
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
>>> B
tensor([[2., 2., 2.],
[2., 2., 2.],
[2., 2., 2.]])
我们先看看dim=0的情况拼接的结果是怎样的
>>> C=torch.cat((A,B),0)
>>> C
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[2., 2., 2.],
[2., 2., 2.],
[2., 2., 2.]])
你会发现,两个矩阵是按照“行”的方向拼接的。
我们接下来再看看dim=1的情况是怎样的
>>> D=torch.cat((A,B),1)
>>> D
tensor([[1., 1., 1., 2., 2., 2.],
[1., 1., 1., 2., 2., 2.],
[1., 1., 1., 2., 2., 2.]])
显然两个矩阵是按照“列”的方向拼接的。那如果Tensor是三维甚至更高维度的呢其实道理也是一样的dim的数值是多少两个矩阵就会按照相应维度的方向链接两个Tensor。
看到这里你可能会问了cat实际上是将多个Tensor在已有的维度上进行连接那如果想增加新的维度进行连接又该怎么做呢这时候就需要stack函数登场了。
stack
为了让你加深理解我们还是结合具体例子来看看。假设我们有两个二维矩阵Tensor把它们“堆叠”放在一起构成一个三维的Tensor如下图
这相当于原来的维度是2现在变成了3变成了一个立体的结构增加了一个维度。你需要注意的是这跟前面的cat不同cat中示意图的例子原来就是3维的cat之后仍旧是3维的而现在咱们是从2维变成了3维。
在实际图像算法开发中咱们有时候需要将多个单通道Tensor2维合并得到多通道的结果3维。而实现这种增加维度拼接的方法我们把它叫做stack。
stack函数的定义如下
torch.stack(inputs, dim=0)
其中inputs表示需要拼接的Tensordim表示新建立维度的方向。
那stack如何使用呢我们一块来看一个例子
>>> A=torch.arange(0,4)
>>> A
tensor([0, 1, 2, 3])
>>> B=torch.arange(5,9)
>>> B
tensor([5, 6, 7, 8])
>>> C=torch.stack((A,B),0)
>>> C
tensor([[0, 1, 2, 3],
[5, 6, 7, 8]])
>>> D=torch.stack((A,B),1)
>>> D
tensor([[0, 5],
[1, 6],
[2, 7],
[3, 8]])
结合代码我们可以看到首先我们构建了两个4元素向量A和B它们的维度是1。然后我们在dim=0也就是“行”的方向上新建一个维度这样维度就成了2也就得到了C。而对于D我们则是在dim=1也就是“列”的方向上新建维度。
Tensor的切分操作
学完了连接操作之后,我们再来看看连接的逆操作:切分。
切分就是连接的逆过程有了刚才的经验你很容易就会想到切分的操作也应该有很多种比如切片、切块等。没错切分的操作主要分为三种类型chunk、split、unbind。
乍一看有不少,其实是因为它们各有特点,适用于不同的使用情景,让我们一起看一下。
chunk
chunk的作用就是将Tensor按照声明的dim进行尽可能平均的划分。
比如说我们有一个32channel的特征需要将其按照channel均匀分成4组每组8个channel这个切分就可以通过chunk函数来实现。具体函数如下
torch.chunk(input, chunks, dim=0)
我们挨个来看看函数中涉及到的三个参数:
首先是input它表示要做chunk操作的Tensor。
接着我们看下chunks它代表将要被划分的块的数量而不是每组的数量。请注意chunks必须是整型。
最后是dim想想这个参数是什么意思呢就是按照哪个维度来进行chunk。
还是跟之前一样,我们通过几个代码例子直观感受一下。我们从一个简单的一维向量开始:
>>> A=torch.tensor([1,2,3,4,5,6,7,8,9,10])
>>> B = torch.chunk(A, 2, 0)
>>> B
(tensor([1, 2, 3, 4, 5]), tensor([ 6, 7, 8, 9, 10]))
这里我们通过chunk函数将原来10位长度的Tensor A切分成了两个一样5位长度的向量。注意B是两个切分结果组成的tuple
那如果chunk参数不能够整除的话结果会是怎样的呢我们接着往下看
>>> B = torch.chunk(A, 3, 0)
>>> B
(tensor([1, 2, 3, 4]), tensor([5, 6, 7, 8]), tensor([ 9, 10]))
我们发现10位长度的Tensor A切分成了三个向量长度分别是442位。这是怎么分的呢不应该是334这样更为平均的方式么
想要解决问题就得找到规律。让我们再来看一个更大一点的例子将A改为17位长度。
>>> A=torch.tensor([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17])
>>> B = torch.chunk(A, 4, 0)
>>> B
(tensor([1, 2, 3, 4, 5]), tensor([ 6, 7, 8, 9, 10]), tensor([11, 12, 13, 14, 15]), tensor([16, 17]))
17位长度的Tensor A切分成了四个分别为5552位长度的向量。这时候你就会发现其实在计算每个结果元素个数的时候chunk函数是先做除法然后再向上取整得到每组的数量。
比如上面这个例子17/4=4.25向上取整就是5那就先逐个生成若干个长度为5的向量最后不够的就放在一块作为最后一个向量长度2
那如果chunk参数大于Tensor可以切分的长度又要怎么办呢我们实际操作一下代码如下
>>> A=torch.tensor([1,2,3])
>>> B = torch.chunk(A, 5, 0)
>>> B
(tensor([1]), tensor([2]), tensor([3]))
显然被切分的Tensor只能分成若干个长度为1的向量。
由此可以推论出二维的情况,我们再举一个例子, 看看二维矩阵Tensor的情况
>>> A=torch.ones(4,4)
>>> A
tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
>>> B = torch.chunk(A, 2, 0)
>>> B
(tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.]]),
tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.]]))
还是跟前面的cat一样这里的dim参数表示的是第dim维度方向上进行切分。
刚才介绍的chunk函数是按照“切分成确定的份数”来进行切分的那如果想按照“每份按照确定的大小”来进行切分该怎样做呢PyTorch也提供了相应的方法叫做split。
split
split的函数定义如下跟前面一样我们还是分别看看这里涉及的参数。
torch.split(tensor, split_size_or_sections, dim=0)
首先是tensor也就是待切分的Tensor。
然后是split_size_or_sections这个参数。当它为整数时表示将tensor按照每块大小为这个整数的数值来切割当这个参数为列表时则表示将此tensor切成和列表中元素一样大小的块。
最后同样是dim它定义了要按哪个维度切分。
同样的我们举几个例子来看一下split的具体操作。首先是split_size_or_sections是整数的情况。
>>> A=torch.rand(4,4)
>>> A
tensor([[0.6418, 0.4171, 0.7372, 0.0733],
[0.0935, 0.2372, 0.6912, 0.8677],
[0.5263, 0.4145, 0.9292, 0.5671],
[0.2284, 0.6938, 0.0956, 0.3823]])
>>> B=torch.split(A, 2, 0)
>>> B
(tensor([[0.6418, 0.4171, 0.7372, 0.0733],
[0.0935, 0.2372, 0.6912, 0.8677]]),
tensor([[0.5263, 0.4145, 0.9292, 0.5671],
[0.2284, 0.6938, 0.0956, 0.3823]]))
在这个例子里我们看到原来4x4大小的Tensor A沿着第0维度也就是沿“行”的方向按照每组2“行”的大小进行切分得到了两个2x4大小的Tensor。
那么问题来了如果split_size_or_sections不能整除对应方向的大小的话会有怎样的结果呢我们将代码稍作修改就好了
>>> C=torch.split(A, 3, 0)
>>> C
(tensor([[0.6418, 0.4171, 0.7372, 0.0733],
[0.0935, 0.2372, 0.6912, 0.8677],
[0.5263, 0.4145, 0.9292, 0.5671]]),
tensor([[0.2284, 0.6938, 0.0956, 0.3823]]))
根据刚才的代码我们就能发现原来PyTorch会尽可能凑够每一个结果使得其对应dim的数据大小等于split_size_or_sections。如果最后剩下的不够那就把剩下的内容放到一块作为最后一个结果。
接下来我们再看一下split_size_or_sections是列表时的情况。刚才提到了当split_size_or_sections为列表的时候表示将此tensor切成和列表中元素大小一样的大小的块我们来看一段对应的代码
>>> A=torch.rand(5,4)
>>> A
tensor([[0.1005, 0.9666, 0.5322, 0.6775],
[0.4990, 0.8725, 0.5627, 0.8360],
[0.3427, 0.9351, 0.7291, 0.7306],
[0.7939, 0.3007, 0.7258, 0.9482],
[0.7249, 0.7534, 0.0027, 0.7793]])
>>> B=torch.split(A,(2,3),0)
>>> B
(tensor([[0.1005, 0.9666, 0.5322, 0.6775],
[0.4990, 0.8725, 0.5627, 0.8360]]),
tensor([[0.3427, 0.9351, 0.7291, 0.7306],
[0.7939, 0.3007, 0.7258, 0.9482],
[0.7249, 0.7534, 0.0027, 0.7793]]))
这部分代码怎么解释呢其实也很好理解就是将Tensor A沿着第0维进行切分每一个结果对应维度上的尺寸或者说大小分别是23
unbind
通过学习前面的几个函数咱们知道了怎么按固定大小做切分或者按照索引index来进行选择。现在我们想象一个应用场景如果我们现在有一个3 channel图像的Tensor想要逐个获取每个channel的数据该怎么做呢
假如用chunk的话我们需要将chunks设为3如果用split的话需要将split_size_or_sections设为1。
虽然它们都可以实现相同的目的但是如果channel数量很大逐个去取也比较折腾。这时候就需要用到另一个函数unbind它的函数定义如下
torch.unbind(input, dim=0)
其中input表示待处理的Tensordim还是跟前面的函数一样表示切片的方向。
我们结合例子来理解:
>>> A=torch.arange(0,16).view(4,4)
>>> A
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
>>> b=torch.unbind(A, 0)
>>> b
(tensor([0, 1, 2, 3]),
tensor([4, 5, 6, 7]),
tensor([ 8, 9, 10, 11]),
tensor([12, 13, 14, 15]))
在这个例子中我们首先创建了一个4x4的二维矩阵Tensor随后我们从第0维也就是“行”的方向进行切分 因为矩阵有4行所以就会得到4个结果。
接下来我们看一下如果从第1维也就是“列”的方向进行切分会是怎样的结果呢
>>> b=torch.unbind(A, 1)
>>> b
(tensor([ 0, 4, 8, 12]),
tensor([ 1, 5, 9, 13]),
tensor([ 2, 6, 10, 14]),
tensor([ 3, 7, 11, 15]))
不难发现这里是按照“列”的方向进行拆解的。所以unbind是一种降维切分的方式相当于删除一个维度之后的结果。
Tensor的索引操作
你有没有发现刚才我们讲的chunk和split操作我们都是将数据整体进行切分并获得全部结果。但有的时候我们只需要其中的一部分这要怎么做呢一个很自然的想法就是直接告诉Tensor我想要哪些部分这种方法我们称为索引操作。
索引操作有很多方式有提供好现成API的也有用户自行定制的操作其中最常用的两个操作就是index_select和masked_select我们分别去看看用法。
index_select
这里就需要index_select这个函数了其定义如下
torch.index_select(tensor, dim, index)
这里的tensor、dim跟前面函数里的一样不再赘述。我们重点看一看index它表示从dim维度中的哪些位置选择数据这里需要注意index是torch.Tensor类型。
还是跟之前一样,我们来看几个示例代码:
>>> A=torch.arange(0,16).view(4,4)
>>> A
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
>>> B=torch.index_select(A,0,torch.tensor([1,3]))
>>> B
tensor([[ 4, 5, 6, 7],
[12, 13, 14, 15]])
>>> C=torch.index_select(A,1,torch.tensor([0,3]))
>>> C
tensor([[ 0, 3],
[ 4, 7],
[ 8, 11],
[12, 15]])
在这个例子中我们先创建了一个4x4大小的矩阵Tensor A。然后我们从第0维选择第1和3的数据并得到了最终的Tensor B其大小为2x4。随后我们从Tensor A中选择第0和3的数据得到了最终的Tensor C其大小为4x2。
怎么样,是不是非常简单?
masked_select
刚才介绍的indexed_select它是基于给定的索引来进行数据提取的。但有的时候我们还想通过一些判断条件来进行选择比如提取深度学习网络中某一层中数值大于0的参数。
这时候就需要用到PyTorch提供的masked_select函数了我们先来看它的定义
torch.masked_select(input, mask, out=None)
这里我们只需要关心前两个参数input和mask。
input表示待处理的Tensor。mask代表掩码张量也就是满足条件的特征掩码。这里你需要注意的是mask须跟input张量有相同数量的元素数目但形状或维度不需要相同。
你是不是还感觉有些云里雾里?让我来举一个例子,你看了之后,一下子就能明白。
你在平时的练习中有没有想过如果我们让Tensor和数字做比较会有什么样的结果比如后面这段代码我们随机生成一个5位长度的Tensor A
>>> A=torch.rand(5)
>>> A
tensor([0.3731, 0.4826, 0.3579, 0.4215, 0.2285])
>>> B=A>0.3
>>> B
tensor([ True, True, True, True, False])
在这段代码里我们让A跟0.3做比较得到了一个新的Tensor内部每一个数值表示的是A中对应数值是否大于0.3。
比如第一个数值原来是0.3731大于0.3所以是True最后一个数值0.2285小于0.3所以是False。
这个新的Tensor其实就是一个掩码张量它的每一位表示了一个判断条件是否成立的结果。
然后我们继续写一段代码看看基于掩码B的选择是怎样的结果
>>> C=torch.masked_select(A, B)
>>> C
tensor([0.3731, 0.4826, 0.3579, 0.4215])
你会发现C实际上得到的就是A中“满足B里面元素值为True的”对应位置的数据。
好了这下你应该知道了masked_select的作用了吧其实就是我们根据要筛选的条件得到一个掩码张量然后用这个张量去提取Tensor中的数据。
根据这个思路,上面的例子就可以简化为:
>>> A=torch.rand(5)
>>> A
tensor([0.3731, 0.4826, 0.3579, 0.4215, 0.2285])
>>> C=torch.masked_select(A, A>0.3)
>>> C
tensor([0.3731, 0.4826, 0.3579, 0.4215])
是不是非常简单呢?
小结
恭喜你完成了这节课的学习。这节课我们一同学习了Tensor里更加高级的操作包括Tensor之间的连接操作Tensor内部的切分操作以及基于索引或者筛选条件的数据选择操作。
当然了,在使用这些函数的时候,你最需要关注的就是边界的数值大小,具体来说就是维度和大小相关的参数,一定要提前仔细计算好,要不然就会产生错误的结果。
结合众多的例子,我相信你一定可以拿下这些操作。
这里我特意给你梳理了一张表格总结归纳了Tensor中的主要函数跟用法。不过这些参数咱们也不用死记硬背我们在使用的时候根据需要灵活查询相关的参数列表即可。-
-
通过这两节课我们搞懂了Tensor的一系列操作在以后的项目中你就可以游刃有余地对Tensor进行各种花式操作了加油!
每课一练
现在有个Tensor如下
>>> A=torch.tensor([[4,5,7], [3,9,8],[2,3,4]])
>>> A
tensor([[4, 5, 7],
[3, 9, 8],
[2, 3, 4]])
我们想提取出其中第一行的第一个,第二行的第一、第二个,第三行的最后一个,该怎么做呢?
欢迎你在留言区跟我交流互动,也推荐你把这节课分享给更多同事、朋友!

View File

@ -0,0 +1,269 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 Torchvision数据读取训练开始的第一步
你好,我是方远。
今天起我们进入模型训练篇的学习。如果将模型看作一辆汽车,那么它的开发过程就可以看作是一套完整的生产流程,环环相扣、缺一不可。这些环节包括数据的读取、网络的设计、优化方法与损失函数的选择以及一些辅助的工具等。未来你将尝试构建自己的豪华汽车,或者站在巨人的肩膀上对前人的作品进行优化。
试想一下如果你对这些基础环节所使用的方法都不清楚你还能很好地进行下去吗所以通过这个模块我们的目标是先把基础打好。通过这模块的学习对于PyTorch都为我们提供了哪些丰富的API你就会了然于胸了。
Torchvision 是一个和 PyTorch 配合使用的 Python 包包含很多图像处理的工具。我们先从数据处理入手开始PyTorch的学习的第一步。这节课我们会先介绍Torchvision的常用数据集及其读取方法在后面的两节课里我再带你了解常用的图像处理方法与Torchvision其它有趣的功能。
PyTorch中的数据读取
训练开始的第一步首先就是数据读取。PyTorch为我们提供了一种十分方便的数据读取机制即使用Dataset类与DataLoader类的组合来得到数据迭代器。在训练或预测时数据迭代器能够输出每一批次所需的数据并且对数据进行相应的预处理与数据增强操作。
下面我们分别来看下Dataset类与DataLoader类。
Dataset类
PyTorch中的Dataset类是一个抽象类它可以用来表示数据集。我们通过继承Dataset类来自定义数据集的格式、大小和其它属性后面就可以供DataLoader类直接使用。
其实这就表示无论使用自定义的数据集还是官方为我们封装好的数据集其本质都是继承了Dataset类。而在继承Dataset类时至少需要重写以下几个方法
__init__():构造函数,可自定义数据读取方法以及进行数据预处理;
__len__():返回数据集大小;
__getitem__():索引数据集中的某一个数据。
光看原理不容易理解下面我们来编写一个简单的例子看下如何使用Dataset类定义一个Tensor类型的数据集。
import torch
from torch.utils.data import Dataset
class MyDataset(Dataset):
# 构造函数
def __init__(self, data_tensor, target_tensor):
self.data_tensor = data_tensor
self.target_tensor = target_tensor
# 返回数据集大小
def __len__(self):
return self.data_tensor.size(0)
# 返回索引的数据与标签
def __getitem__(self, index):
return self.data_tensor[index], self.target_tensor[index]
结合代码可以看到我们定义了一个名字为MyDataset的数据集在构造函数中传入Tensor类型的数据与标签在__len__函数中直接返回Tensor的大小在__getitem__函数中返回索引的数据与标签。
下面我们来看一下如何调用刚才定义的数据集。首先随机生成一个10*3维的数据Tensor然后生成10维的标签Tensor与数据Tensor相对应。利用这两个Tensor生成一个MyDataset的对象。查看数据集的大小可以直接用len()函数,索引调用数据可以直接使用下标。
# 生成数据
data_tensor = torch.randn(10, 3)
target_tensor = torch.randint(2, (10,)) # 标签是0或1
# 将数据封装成Dataset
my_dataset = MyDataset(data_tensor, target_tensor)
# 查看数据集大小
print('Dataset size:', len(my_dataset))
'''
输出:
Dataset size: 10
'''
# 使用索引调用数据
print('tensor_data[0]: ', my_dataset[0])
'''
输出:
tensor_data[0]: (tensor([ 0.4931, -0.0697, 0.4171]), tensor(0))
'''
DataLoader类
在实际项目中如果数据量很大考虑到内存有限、I/O速度等问题在训练过程中不可能一次性的将所有数据全部加载到内存中也不能只用一个进程去加载所以就需要多进程、迭代加载而DataLoader就是基于这些需要被设计出来的。
DataLoader是一个迭代器最基本的使用方法就是传入一个Dataset对象它会根据参数 batch_size的值生成一个batch的数据节省内存的同时它还可以实现多进程、数据打乱等处理。
DataLoader类的调用方式如下
from torch.utils.data import DataLoader
tensor_dataloader = DataLoader(dataset=my_dataset, # 传入的数据集, 必须参数
batch_size=2, # 输出的batch大小
shuffle=True, # 数据是否打乱
num_workers=0) # 进程数, 0表示只有主进程
# 以循环形式输出
for data, target in tensor_dataloader:
print(data, target)
'''
输出:
tensor([[-0.1781, -1.1019, -0.1507],
[-0.6170, 0.2366, 0.1006]]) tensor([0, 0])
tensor([[ 0.9451, -0.4923, -1.8178],
[-0.4046, -0.5436, -1.7911]]) tensor([0, 0])
tensor([[-0.4561, -1.2480, -0.3051],
[-0.9738, 0.9465, 0.4812]]) tensor([1, 0])
tensor([[ 0.0260, 1.5276, 0.1687],
[ 1.3692, -0.0170, -1.6831]]) tensor([1, 0])
tensor([[ 0.0515, -0.8892, -0.1699],
[ 0.4931, -0.0697, 0.4171]]) tensor([1, 0])
'''
# 输出一个batch
print('One batch tensor data: ', iter(tensor_dataloader).next())
'''
输出:
One batch tensor data: [tensor([[ 0.9451, -0.4923, -1.8178],
[-0.4046, -0.5436, -1.7911]]), tensor([0, 0])]
'''
结合代码我们梳理一下DataLoader中的几个参数它们分别表示
datasetDataset类型输入的数据集必须参数
batch_sizeint类型每个batch有多少个样本
shufflebool类型在每个epoch开始的时候是否对数据进行重新打乱
num_workersint类型加载数据的进程数0意味着所有的数据都会被加载进主进程默认为0。
什么是Torchvision
PyTroch官方为我们提供了一些常用的图片数据集如果你需要读取这些数据集那么无需自己实现只需要利用Torchvision就可以搞定。
Torchvision 是一个和 PyTorch 配合使用的 Python 包。它不只提供了一些常用数据集还提供了几个已经搭建好的经典网络模型以及集成了一些图像数据处理方面的工具主要供数据预处理阶段使用。简单地说Torchvision 库就是常用数据集+常见网络模型+常用图像处理方法。
Torchvision的安装方式同样非常简单可以使用conda安装命令如下
conda install torchvision -c pytorch
或使用pip进行安装命令如下
pip install torchvision
Torchvision中默认使用的图像加载器是PIL因此为了确保Torchvision正常运行我们还需要安装一个Python的第三方图像处理库——Pillow库。Pillow提供了广泛的文件格式支持强大的图像处理能力主要包括图像储存、图像显示、格式转换以及基本的图像处理操作等。
使用conda安装Pillow的命令如下
conda install pillow
使用pip安装Pillow的命令如下
pip install pillow
利用Torchvision读取数据
安装好Torchvision之后我们再来接着看看。Torchvision库为我们读取数据提供了哪些支持。
Torchvision库中的torchvision.datasets包中提供了丰富的图像数据集的接口。常用的图像数据集例如MNIST、COCO等这个模块都为我们做了相应的封装。
下表中列出了torchvision.datasets包所有支持的数据集。各个数据集的说明与接口详见链接https://pytorch.org/vision/stable/datasets.html。
这里我想提醒你注意torchvision.datasets这个包本身并不包含数据集的文件本身它的工作方式是先从网络上把数据集下载到用户指定目录然后再用它的加载器把数据集加载到内存中。最后把这个加载后的数据集作为对象返回给用户。
为了让你进一步加深对知识的理解我们以MNIST数据集为例来说明一下这个模块具体的使用方法。
MNIST数据集简介
MNIST数据集是一个著名的手写数字数据集因为上手简单在深度学习领域手写数字识别是一个很经典的学习入门样例。
MNIST数据集是NIST数据集的一个子集MNIST 数据集你可以通过这里下载。它包含了四个部分,我用表格的方式为你做了梳理。
MNIST数据集是ubyte格式存储我们先将“训练集图片”解析成图片格式来直观地看一看数据集具体是什么样子的。具体怎么解析我在后面数据预览再展开。
数据读取
接下来我们看一下如何使用Torchvision来读取MNIST数据集。
对于torchvision.datasets所支持的所有数据集它都内置了相应的数据集接口。例如刚才介绍的MNIST数据集torchvision.datasets就有一个MNIST的接口接口内封装了从下载、解压缩、读取数据、解析数据等全部过程。
这些接口的工作方式差不多,都是先从网络上把数据集下载到指定目录,然后再用加载器把数据集加载到内存中,最后将加载后的数据集作为对象返回给用户。
以MNIST为例我们可以用如下方式调用
# 以MNIST为例
import torchvision
mnist_dataset = torchvision.datasets.MNIST(root='./data',
train=True,
transform=None,
target_transform=None,
download=True)
torchvision.datasets.MNIST是一个类对它进行实例化即可返回一个MNIST数据集对象。构造函数包括包含5个参数
root是一个字符串用于指定你想要保存MNIST数据集的位置。如果download是Flase则会从目标位置读取数据集
download是布尔类型表示是否下载数据集。如果为True则会自动从网上下载这个数据集存储到root指定的位置。如果指定位置已经存在数据集文件则不会重复下载
train是布尔类型表示是否加载训练集数据。如果为True则只加载训练数据。如果为False则只加载测试数据集。这里需要注意并不是所有的数据集都做了训练集和测试集的划分这个参数并不一定是有效参数具体需要参考官方接口说明文档
transform用于对图像进行预处理操作例如数据增强、归一化、旋转或缩放等。这些操作我们会在下节课展开讲解
target_transform用于对图像标签进行预处理操作。
运行上述的代码我们可以得到下图所示的效果。从图中我们可以看出程序首先去指定的网址下载了MNIST数据集然后进行了解压缩等操作。如果你再次运行相同的代码则不会再有下载的过程。
看到这你可能还有疑问好奇我们得到的mnist_dataset是什么呢
如果你用type函数查看一下mnist_dataset的类型就可以得到torchvision.datasets.mnist.MNIST 而这个类是之前我们介绍过的Dataset类的派生类。相当于torchvision.datasets 它已经帮我们写好了对Dataset类的继承完成了对数据集的封装我们直接使用即可。
这里我们主要以MNIST为例进行了说明。其它的数据集使用方法类似调用的时候你只要需要将类名“MNIST”换成其它数据集名字即可。
对于不同的数据集数据格式都不尽相同而torchvision.datasets则帮助我们完成了各种不同格式的数据的解析与读取可以说十分便捷。而对于那些没有官方接口的图像数据集我们也可以使用以torchvision.datasets.ImageFolder接口来自行定义在图像分类的实战篇中就是使用ImageFolder进行数据读取的你可以到那个时候再看一看。
数据预览
完成了数据读取工作我们得到的是对应的mnist_dataset刚才已经讲过了这是一个封装了的数据集。
如果想要查看mnist_dataset中的具体内容我们需要把它转化为列表。如果IOPub data rate超限可以只加载测试集数据令train=False
mnist_dataset_list = list(mnist_dataset)
print(mnist_dataset_list)
执行结果如下图所示。
从运行结果中可以看出,转换后的数据集对象变成了一个元组列表,每个元组有两个元素,第一个元素是图像数据,第二个元素是图像的标签。
这里图像数据是PIL.Image.Image类型的这种类型可以直接在Jupyter中显示出来。显示一条数据的代码如下。
display(mnist_dataset_list[0][0])
print("Image label is:", mnist_dataset_list[0][1])
运行结果如下图所示。可以看出数据集mnist_dataset中的第一条数据是图片手写数字“7”对应的标签是“7”。
好,如果你也得到了上面的运行结果,说明你的操作没问题,恭喜你成功完成了读取操作。
小结
恭喜你完成了这节课的学习。我们已经迈出了模型训练的第一步,学会了如何读取数据。
今天的重点就是掌握两种读取数据的方法,也就是自定义和读取常用图像数据集。
最通用的数据读取方法就是自己定义一个Dataset的派生类。而读取常用的图像数据集就可以利用PyTorch提供的视觉包Torchvision。
Torchvision库为我们读取数据提供了丰富的图像数据集的接口。我用手写数字识别这个经典例子给你示范了如何使用Torchvision来读取MNIST数据集。
torchvision.datasets继承了Dataset 类,它在预定义许多常用的数据集的同时,还预留了数据预处理与数据增强的接口。在下一节课中,我们就会接触到这些数据增强函数,并学习如何进行数据增强。
每课一练
在PyTorch中我们要定义一个数据集应该继承哪一个类呢
欢迎你在留言区和我交流互动,也推荐你把这节课内容分享给更多的朋友、同事,跟他一起学习进步。

View File

@ -0,0 +1,385 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 Torchvision数据增强让数据更加多样性
你好,我是方远。
上一节课我们一同迈出了训练开始的第一步——数据读取初步认识了Torchvision学习了如何利用Torchvision读取数据。不过仅仅将数据集中的图片读取出来是不够的在训练的过程中神经网络模型接收的数据类型是Tensor而不是PIL对象因此我们还需要对数据进行预处理操作比如图像格式的转换。
与此同时加载后的图像数据可能还需要进行一系列图像变换与增强操作例如裁切边框、调整图像比例和大小、标准化等以便模型能够更好地学习到数据的特征。这些操作都可以使用torchvision.transforms工具完成。
今天我们就来学习一下利用Torchvision如何进行数据预处理操作如何进行图像变换与增强。
图像处理工具之torchvision.transforms
Torchvision库中的torchvision.transforms包中提供了常用的图像操作包括对Tensor 及PIL Image对象的操作例如随机切割、旋转、数据类型转换等等。
按照torchvision.transforms 的功能大致分为以下几类数据类型转换、对PIL.Image 和 Tensor进行变化和变换的组合。下面我们依次来学习这些类别中的操作。
数据类型转换
在上一节课中我们学习了读取数据集中的图片读取到的数据是PIL.Image的对象。而在模型训练阶段需要传入Tensor类型的数据神经网络才能进行运算。
那么如何将PIL.Image或Numpy.ndarray格式的数据转化为Tensor格式呢这需要用到transforms.ToTensor() 类。
而反之将Tensor 或 Numpy.ndarray 格式的数据转化为PIL.Image格式则使用transforms.ToPILImage(mode=None) 类。它则是ToTensor的一个逆操作它能把Tensor或Numpy的数组转换成PIL.Image对象。
其中参数mode代表PIL.Image的模式如果mode为None默认值则根据输入数据的维度进行推断
输入为3通道mode为RGB
输入为4通道mode为RGBA
输入为2通道mode为LA;
输入为单通道mode根据输入数据的类型确定具体模式。
说完用法我们来看一个具体的例子加深理解。以极客时间的LOGO图片文件名为jk.jpg为例进行一下数据类型的相互转换。具体代码如下。
from PIL import Image
from torchvision import transforms
img = Image.open('jk.jpg')
display(img)
print(type(img)) # PIL.Image.Image是PIL.JpegImagePlugin.JpegImageFile的基类
'''
输出:
<class 'PIL.JpegImagePlugin.JpegImageFile'>
'''
# PIL.Image转换为Tensor
img1 = transforms.ToTensor()(img)
print(type(img1))
'''
输出:
<class 'torch.Tensor'>
'''
# Tensor转换为PIL.Image
img2 = transforms.ToPILImage()(img1) #PIL.Image.Image
print(type(img2))
'''
输出:
<class 'PIL.Image.Image'>
'''
首先用读取图片查看一下图片的类型为PIL.JpegImagePlugin.JpegImageFile这里需要注意PIL.JpegImagePlugin.JpegImageFile类是PIL.Image.Image类的子类。然后用transforms.ToTensor() 将PIL.Image转换为Tensor。最后再将Tensor转换回PIL.Image。
对 PIL.Image 和 Tensor 进行变换
torchvision.transforms 提供了丰富的图像变换方法例如改变尺寸、剪裁、翻转等。并且这些图像变换操作可以接收多种数据格式不仅可以直接对PIL格式的图像进行变换也可以对Tensor进行变换无需我们再去做额外的数据类型转换。
下面我们依次来看一看。
Resize
将输入的 PIL Image 或 Tensor 尺寸调整为给定的尺寸,具体定义为:
torchvision.transforms.Resize(size, interpolation=2)
我们依次看下相关的参数:
size期望输出的尺寸。如果 size 是一个像 (h, w) 这样的元组,则图像输出尺寸将与之匹配。如果 size 是一个 int 类型的整数,图像较小的边将被匹配到该整数,另一条边按比例缩放。
interpolation插值算法int类型默认为2表示 PIL.Image.BILINEAR。
有关Size中是tuple还是int这一点请你一定要注意。
让我说明一下在我们训练时通常要把图片resize到一定的大小比如说128x128256x256这样的。如果直接给定resize后的高与宽是没有问题的。但如果设定的是一个int型较长的边就会按比例缩放。
在resize之后呢一般会接一个crop操作crop到指定的大小。对于高与宽接近的图片来说这么做问题不大但是高与宽的差距较大时就会crop掉很多有用的信息。关于这一点我们在后续的图像分类部分还会遇到到时我在详细展开。
我们还是以极客时间的LOGO图片为例一起看一下Resize的效果。
from PIL import Image
from torchvision import transforms
# 定义一个Resize操作
resize_img_oper = transforms.Resize((200,200), interpolation=2)
# 原图
orig_img = Image.open('jk.jpg')
display(orig_img)
# Resize操作后的图
img = resize_img_oper(orig_img)
display(img)
首先定义一个Resize操作设置好变换后的尺寸为(200, 200)然后对极客时间LOGO图片进行Resize变换。-
原图以及Resize变换后的效果如下表所示。
剪裁
torchvision.transforms提供了多种剪裁方法例如中心剪裁、随机剪裁、四角和中心剪裁等。我们依次来看下它们的定义。
先说中心剪裁,顾名思义,在中心裁剪指定的 PIL Image 或 Tensor其定义如下
torchvision.transforms.CenterCrop(size)
其中size表示期望输出的剪裁尺寸。如果 size 是一个像 (h, w) 这样的元组,则剪裁后的图像尺寸将与之匹配。如果 size 是 int 类型的整数,剪裁出来的图像是 (size, size) 的正方形。
然后是随机剪裁,就是在一个随机位置剪裁指定的 PIL Image 或 Tensor定义如下
torchvision.transforms.RandomCrop(size, padding=None)
其中size代表期望输出的剪裁尺寸用法同上。而padding表示图像的每个边框上的可选填充。默认值是 None即没有填充。通常来说不会用padding这个参数至少对于我来说至今没用过。
最后要说的是FiveCrop我们将给定的 PIL Image 或 Tensor ,分别从四角和中心进行剪裁,共剪裁成五块,定义如下:
torchvision.transforms.FiveCrop(size)
size可以是int或tuple用法同上。-
掌握了各种剪裁的定义和参数用法以后,我们来看一下这些剪裁操作具体如何调用,代码如下。
from PIL import Image
from torchvision import transforms
# 定义剪裁操作
center_crop_oper = transforms.CenterCrop((60,70))
random_crop_oper = transforms.RandomCrop((80,80))
five_crop_oper = transforms.FiveCrop((60,70))
# 原图
orig_img = Image.open('jk.jpg')
display(orig_img)
# 中心剪裁
img1 = center_crop_oper(orig_img)
display(img1)
# 随机剪裁
img2 = random_crop_oper(orig_img)
display(img2)
# 四角和中心剪裁
imgs = five_crop_oper(orig_img)
for img in imgs:
display(img)
流程和Resize类似都是先定义剪裁操作然后对极客时间LOGO图片进行不同的剪裁。-
具体剪裁效果如下表所示。
翻转
接下来我们来看一看翻转操作。torchvision.transforms提供了两种翻转操作分别是以某一概率随机水平翻转图像和以某一概率随机垂直翻转图像。我们分别来看它们的定义。
以概率p随机水平翻转图像定义如下
torchvision.transforms.RandomHorizontalFlip(p=0.5)
以概率p随机垂直翻转图像定义如下
torchvision.transforms.RandomVerticalFlip(p=0.5)
其中p表示随机翻转的概率值默认为0.5。-
这里的随机翻转是为数据增强提供方便。如果想要必须执行翻转操作的话将p设置为1即可。
以极客时间的LOGO图片为例图片翻转的代码如下。
from PIL import Image
from torchvision import transforms
# 定义翻转操作
h_flip_oper = transforms.RandomHorizontalFlip(p=1)
v_flip_oper = transforms.RandomVerticalFlip(p=1)
# 原图
orig_img = Image.open('jk.jpg')
display(orig_img)
# 水平翻转
img1 = h_flip_oper(orig_img)
display(img1)
# 垂直翻转
img2 = v_flip_oper(orig_img)
display(img2)
翻转效果如下表所示。
只对Tensor进行变换
目前版本的Torchvisionv0.10.0)对各种图像变换操作已经基本同时支持 PIL Image 和 Tensor 类型了因此只针对Tensor的变换操作很少只有4个分别是LinearTransformation线性变换、Normalize标准化、RandomErasing随机擦除、ConvertImageDtype格式转换
这里我们重点来看最常用的一个操作标准化其他3个你可以查阅官方文档。
标准化
标准化是指每一个数据点减去所在通道的平均值,再除以所在通道的标准差,数学的计算公式如下:
\[output=(input-mean)/std\]而对图像进行标准化,就是对图像的每个通道利用均值和标准差进行正则化。这样做的目的,是为了保证数据集中所有的图像分布都相似,这样在训练的时候更容易收敛,既加快了训练速度,也提高了训练效果。
让我来解释一下:首先,标准化是一个常规做法,可以理解为无脑进行标准化后再训练的效果,大概率要好于不进行标准化。
我把极客时间的LOGO读入后所有像素都减去50获得下图。
对于我们人来说是可以分辨出这也是极客时间的LOGO。但是计算机也就是卷积神经网络就不一定能分辨出来了因为卷积神经网络是通过图像的像素进行提取特征的这两张图片像素的数值都不一样凭什么还让神经网络认为是一张图片
而标准化后的数据就会避免这一问题,标准化后会将数据映射到同一区间中,一个类别的图片虽说有的像素值可能有差异,但是它们分布都是类似的分布。
torchvision.transforms提供了对Tensor进行标准化的函数定义如下。
torchvision.transforms.Normalize(mean, std, inplace=False)
其中,每个参数的含义如下所示:
mean表示各通道的均值
std表示各通道的标准差
inplace表示是否原地操作默认为否。
以极客时间的LOGO图片为例我们来看看以(R, G, B)均值和标准差均为(0.5, 0.5, 0.5)来标准化图片后,是什么效果。
from PIL import Image
from torchvision import transforms
# 定义标准化操作
norm_oper = transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
# 原图
orig_img = Image.open('jk.jpg')
display(orig_img)
# 图像转化为Tensor
img_tensor = transforms.ToTensor()(orig_img)
# 标准化
tensor_norm = norm_oper(img_tensor)
# Tensor转化为图像
img_norm = transforms.ToPILImage()(tensor_norm)
display(img_norm)
上面代码的过程是,首先定义了均值和标准差均为(0.5, 0.5, 0.5)的标准化操作然后将原图转化为Tensor接着对Tensor进行标准化最后再将Tensor转化为图像输出。
标准化的效果如下表所示。
变换的组合
其实前面介绍过的所有操作都可以用 Compose 类组合起来,进行连续操作。
Compose类是将多个变换组合到一起它的定义如下。
torchvision.transforms.Compose(transforms)
其中transforms是一个Transform对象的列表表示要组合的变换列表。-
我们还是结合例子动手试试如果我们想要将图片变为200*200像素大小并且随机裁切成80像素的正方形。那么我们可以组合Resize和RandomCrop变换具体代码如下所示。
from PIL import Image
from torchvision import transforms
# 原图
orig_img = Image.open('jk.jpg')
display(orig_img)
# 定义组合操作
composed = transforms.Compose([transforms.Resize((200, 200)),
transforms.RandomCrop(80)])
# 组合操作后的图
img = composed(orig_img)
display(img)
运行的结果如下表所示,也推荐你动手试试看。
结合datasets使用
Compose类是未来我们在实际项目中经常要使用到的类结合torchvision.datasets包就可以在读取数据集的时候做图像变换与数据增强操作。下面让我们一起来看一看。
还记得上一节课中在利用torchvision.datasets 读取MNIST数据集时有一个参数“transform”吗它就是用于对图像进行预处理操作的例如数据增强、归一化、旋转或缩放等。这里的“transform”就可以接收一个torchvision.transforms操作或者由Compose类所定义的操作组合。
上节课中我们在读取MNIST数据集时直接读取出来的图像数据是PIL.Image.Image类型的。但是遇到要训练手写数字识别模型这类的情况模型接收的数据类型是Tensor而不是PIL对象。这时候我们就可以利用“transform”参数使数据在读取的同时做类型转换这样读取出的数据直接就可以是Tensor类型了。
不只是数据类型的转换我们还可以增加归一化等数据增强的操作只需要使用上面介绍过的Compose类进行组合即可。这样在读取数据的同时我们也就完成了数据预处理、数据增强等一系列操作。
我们还是以读取MNIST数据集为例看下如何在读取数据的同时完成数据预处理等操作。具体代码如下。
from torchvision import transforms
from torchvision import datasets
# 定义一个transform
my_transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5), (0.5))
])
# 读取MNIST数据集 同时做数据变换
mnist_dataset = datasets.MNIST(root='./data',
train=False,
transform=my_transform,
target_transform=None,
download=True)
# 查看变换后的数据类型
item = mnist_dataset.__getitem__(0)
print(type(item[0]))
'''
输出:
<class 'torch.Tensor'>
'''
当然MNIST数据集非常简单根本不进行任何处理直接读入的话效果也非常好但是它确实适合学习来使用你可以在利用它进行各种尝试。
我们下面先来看看在图像分类实战中使用的transform可以感受一下实际使用的transforms是什么样子
transform = transforms.Compose([
transforms.RandomResizedCrop(dest_image_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
这也是我在项目中使用的transform。数据增强的方法有很多不过根据我的经验来看并不是用的越多效果越好。
小结
恭喜你完成了这节课的学习,我来给你做个总结。
今天的重点内容就是torchvision.transforms工具的使用。包括常用的图像处理操作以及如何与torchvision.datasets结合使用。
常用的图像处理操作包括数据类型转换、图像尺寸变化、剪裁、翻转、标准化等等。Compose类还可以将多个变换操作组合成一个Transform对象的列表。
torchvision.transforms与torchvision.datasets结合使用可以在数据加载的同时进行一系列图像变换与数据增强操作不仅能够直接将数据送入模型训练还可以加快模型收敛速度让模型更好地学习到数据特征。
当然我们在实际的项目中会有自己的数据而不会使用torchvision.datasets中提供的公开数据集我们今天讲的torchvision.transforms 同样可以在我们自定义的数据集中使用,关于这一点,我会在图像分类的实战中继续讲解。
下节课中我们会介绍Torchvision中其他有趣的功能。包括经典网络模型的实例化与其他有用的函数。
每课一练
Torchvision中 transforms 模块的作用是什么?
欢迎你在留言区跟我交流讨论也欢迎你把这节课分享给自己的朋友和他一起尝试一下Torchvision的各种功能。

View File

@ -0,0 +1,222 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 Torchvision其他有趣的功能
你好,我是方远。
在前面的课程中我们已经学习了Torchvision的数据读取与常用的图像变换方法。其实Torchvision除了帮我们封装好了常用的数据集还为我们提供了深度学习中各种经典的网络结构以及训练好的模型只要直接将这些经典模型的类实例化出来就可以进行训练或使用了。
我们可以利用这些训练好的模型来实现图片分类、物体检测、视频分类等一系列应用。
今天我们就来学习一下经典网络模型的实例化与Torchvision中其他有趣的功能。
常见网络模型
Torchvision中的各种经典网络结构以及训练好的模型都放在了torchvision.models模块中下面我们来看一看torchvision.models 具体为我们提供了什么支持,以及这些功能如何使用。
torchvision.models模块
torchvision.models 模块中包含了常见网络模型结构的定义,这些网络模型可以解决以下四大类问题:图像分类、图像分割、物体检测和视频分类。图像分类、物体检测与图像分割的示意图如下图所示。
图像分类指的是单纯把一张图片判断为某一类例如将上图左侧第一张判断为cat。目标检测则是说首先检测出物体的位置还要识别出对应物体的类别。如上图中间的那张图不仅仅要找到猫、鸭子、狗的位置还有给出给定物体的类别信息。
我们看一下图里最右侧的例子,它表示的是分割。分割即是对图像中每一个像素点进行分类,确定每个点的类别,从而进行区域划分。
在早期的Torchvision版本中torchvision.models模块中只包含了图片分类中的一部分网络例如AlexNet、VGG系列、ResNet系列、Inception系列等。这里你先有个印象就行具体网络特点我后面会在图像分类中详细讲解。
到了现在随着深度学习技术的不断发展人工智能应用更为广泛torchvision.models模块中所封装的网络模型也在不断丰富。比如在当前版本v0.10.0的Torchvision中新增了图像语义分割、物体检测和视频分类的相关网络并且在图像分类中也新增了GoogLeNet、ShuffleNet以及可以使用于移动端的MobileNet系列。这些新模型都能让我们站在巨人的肩膀上看世界。
实例化一个GoogLeNet网络
如果我们直接把一个网络模型的类实例化就会得到一个网络模型。而这个网络模型的类可以是我们自己定义的结构也可以是按照经典模型的论文设计出来的结构。其实你自己按照经典模型的论文写一个类然后实例化一下这和从Torchvision中直接实例化一个网络效果是相同的。
下面我们就以 GoogLeNet 网络为例来说说如何使用torchvision.models模块实例化一个网络。
GoogLeNet是Google推出的基于Inception模块的深度神经网络模型。你可别小看这个模型GoogLeNet获得了2014年的ImageNet竞赛的冠军并且相比之前的AlexNet、VGG等结构能更高效地利用计算资源。
GoogLeNet 也被称为Inception V1在随后的两年中它一直在改进形成了Inception V2、Inception V3等多个版本。
我们可以使用随机初始化的权重创建一个GoogLeNet模型具体代码如下
import torchvision.models as models
googlenet = models.googlenet()
这时候的 GoogLeNet 模型,相当于只有一个实例化好的网络结构,里面的参数都是随机初始化的,需要经过训练之后才能使用,并不能直接用于预测。-
torchvision.models模块除了包含了定义好的网络结构还为我们提供了预训练好的模型我们可以直接导入训练好的模型来使用。导入预训练好的模型的代码如下
import torchvision.models as models
googlenet = models.googlenet(pretrained=True)
可以看出我们只是在实例化的时候引入了一个参数“pretrained=True”即可获得预训练好的模型因为所有的工作torchvision.models模块都已经帮我们封装好了用起来很方便。-
torchvision.models模块中所有预训练好的模型都是在ImageNet数据集上训练的它们都是由PyTorch 的torch.utils.model_zoo模块所提供的并且我们可以通过参数 pretrained=True 来构造这些预训练模型。
如果之前没有加载过带预训练参数的网络在实例化一个预训练好的模型时模型的参数会被下载至缓存目录中下载一次后不需要重复下载。这个缓存目录可以通过环境变量TORCH_MODEL_ZOO来指定。当然你也可以把自己下载好的模型然后复制到指定路径中。
下图是运行了上述实例化代码的结果可以看到GoogLeNet的模型参数被下载到了缓存目录/root/.cache/torch下面。
torchvision.models模块也包含了Inception V3和其他常见的网络结构在实例化时只需要修改网络的类名即可做到举一反三。torchvision.models模块中可实例化的全部模型详见这个网页。
模型微调
完成了刚才的工作你可能会疑惑实例化了带预训练参数的网络有什么用呢其实它除了可以直接用来做预测使用还可以基于它做网络模型的微调也就是“fine-tuning”。
那什么是“fine-tuning”呢
举个例子,假设你的老板给布置了一个有关于图片分类的任务,数据集是关于狗狗的图片,让你区分图片中狗的种类,例如金毛、柯基、边牧等等。
问题是数据集中狗的类别很多但数据却不多。你发现从零开始训练一个图片分类模型但这样模型效果很差并且很容易过拟合。这种问题该如何解决呢于是你想到了使用迁移学习可以用已经在ImageNet数据集上训练好的模型来达成你的目的。
例如上面我们已经实例化的GoogLeNet模型只需要使用我们自己的数据集重新训练网络最后的分类层即可得到区分狗种类的图片分类模型。这就是所谓的“fine-tuning”方法。
模型微调,简单来说就是先在一个比较通用、宽泛的数据集上进行大量训练得出了一套参数,然后再使用这套预训练好的网络和参数,在自己的任务和数据集上进行训练。使用经过预训练的模型,要比使用随机初始化的模型训练效果更好,更容易收敛,并且训练速度更快,在小数据集上也能取得比较理想的效果。
那新的问题又来了,为什么模型微调如此有效呢?因为我们相信同样是处理图片分类任务的两个模型,网络的参数也具有某种相似性。因此,把一个已经训练得很好的模型参数迁移到另一个模型上,同样有效。即使两个模型的工作不完全相同,我们也可以在这套预训练参数的基础上,经过微调性质的训练,同样能取得不错的效果。
ImageNet数据集共有1000个类别而狗的种类远远达不到1000类。因此加载了预训练好的模型之后还需要根据你的具体问题对模型或数据进行一些调整通常来说是调整输出类别的数量。
假设狗的种类一共为10类那么我们自然需要将GoogLeNet模型的输出分类数也调整为10。对预训练模型进行调整对代码如下
import torch
import torchvision.models as models
# 加载预训练模型
googlenet = models.googlenet(pretrained=True)
# 提取分类层的输入参数
fc_in_features = googlenet.fc.in_features
print("fc_in_features:", fc_in_features)
# 查看分类层的输出参数
fc_out_features = googlenet.fc.out_features
print("fc_out_features:", fc_out_features)
# 修改预训练模型的输出分类数(在图像分类原理中会具体介绍torch.nn.Linear)
googlenet.fc = torch.nn.Linear(fc_in_features, 10)
'''
输出:
fc_in_features: 1024
fc_out_features: 1000
'''
首先你需要加载预训练模型然后提取预训练模型的分类层固定参数最后修改预训练模型的输出分类数为10。根据输出结果我们可以看到预训练模型的原始输出分类数是1000。
其他常用函数
之前在torchvision.transforms中我们学习了很多有关于图像处理的函数Torchvision还提供了几个常用的函数make_grid和save_img让我们依次来看一看它们又能实现哪些有趣的功能。
make_grid
make_grid 的作用是将若干幅图像拼成在一个网格中,它的定义如下。
torchvision.utils.make_grid(tensor, nrow=8, padding=2)
定义中对应的几个参数含义如下:
tensor类型是Tensor或列表如果输入类型是Tensor其形状应是 (B x C x H x W);如果输入类型是列表,列表中元素应为相同大小的图片。
nrow表示一行放入的图片数量默认为8。
padding子图像与子图像之间的边框宽度默认为2像素。
make_grid函数主要用于展示数据集或模型输出的图像结果。我们以MNIST数据集为例整合之前学习过的读取数据集以及图像变换的内容来看一看make_grid函数的效果。
下面的程序利用make_grid函数展示了MNIST的测试集中的32张图片。
import torchvision
from torchvision import datasets
from torchvision import transforms
from torch.utils.data import DataLoader
# 加载MNIST数据集
mnist_dataset = datasets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor(),
target_transform=None,
download=True)
# 取32张图片的tensor
tensor_dataloader = DataLoader(dataset=mnist_dataset,
batch_size=32)
data_iter = iter(tensor_dataloader)
img_tensor, label_tensor = data_iter.next()
print(img_tensor.shape)
'''
输出torch.Size([32, 1, 28, 28])
'''
# 将32张图片拼接在一个网格中
grid_tensor = torchvision.utils.make_grid(img_tensor, nrow=8, padding=2)
grid_img = transforms.ToPILImage()(grid_tensor)
display(grid_img)
结合代码我们可以看到程序首先利用torchvision.datasets加载MNIST的测试集然后利用DataLoader类的迭代器一次获取到32张图片的Tensor最后利用make_grid函数将32张图片拼接在了一幅图片中。-
MNIST的测试集中的32张图片如下图所示这里我要特别说明一下因为MNIST的尺寸为28x28所以测试集里的手写数字图片像素都比较低但这并不影响咱们动手实践。你可以参照我给到的示范自己动手试试看。
save_img
一般来说在保存模型输出的图片时需要将Tensor类型的数据转化为图片类型才能进行保存过程比较繁琐。Torchvision提供了save_image函数能够直接将Tensor保存为图片即使Tensor数据在CUDA上也会自动移到CPU中进行保存。
save_image函数的定义如下。
torchvision.utils.save_image(tensor, fp, **kwargs)
这些参数也很好理解:
tensor类型是Tensor或列表如果输入类型是Tensor直接将Tensor保存如果输入类型是列表则先调用make_grid函数生成一张图片的Tensor然后再保存。
fp保存图片的文件名
**kwargsmake_grid函数中的参数前面已经讲过了。
我们接着上面的小例子将32张图片的拼接图直接保存代码如下。
# 输入为一张图片的tensor 直接保存
torchvision.utils.save_image(grid_tensor, 'grid.jpg')
# 输入为List 调用grid_img函数后保存
torchvision.utils.save_image(img_tensor, 'grid2.jpg', nrow=5, padding=2)
当输入为一张图片的Tensor时直接保存保存的图片如下所示。
当输入为List时则会先调用make_grid函数make_grid函数的参数直接加在后面即可代码中令nrow=5保存的图片如下所示。这时我们可以看到图片中每行中有5个数字最后一行不足的数字已经自动填充了空图像。
小结
恭喜你完成了这节课的学习。至此Torchvision的全部内容我们就学完了。
今天的重点内容是torchvision.models模块的使用包括如何实例化一个网络与如何进行模型的微调。
torchvision.models模块为我们提供了深度学习中各种经典的网络结构以及训练好的模型我们不仅可以实例化一个随机初始化的网络模型还可以实例化一个预训练好的网络模型。
模型微调可以让我们在自己的小数据集上快速训练模型,并取得比较理想的效果。但是我们需要根据具体问题对预训练模型或数据进行一些修改,你可以灵活调整输出类别的数量,或者调整输入图像的大小。
除了模型微调我还讲了两个Torchvision中有趣的函数make_grid和save_img我还结合之前我们学习过的读取数据集以及图像变换的内容为你做了演示。相信Torchvision工具配合PyTorch使用一定能够使你事半功倍。
每课一练
请你使用torchvision.models模块实例化一个VGG 16网络。
欢迎你在留言区跟我交流讨论,也推荐你把这节课分享给更多的同事、朋友。
我是方远,我们下节课见!

View File

@ -0,0 +1,296 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 卷积(上):如何用卷积为计算机“开天眼”?
你好,我是方远。
现在刷脸支付的场景越来越多,相信人脸识别你一定不陌生,你有没有想过,在计算机识别人脸之前,我们人类是如何判断一个人是谁的呢?
我们眼睛看到人脸的时候,会先将人脸的一些粗粒度特征提取出来,例如人脸的轮廓、头发的颜色、头发长短等。然后这些信息会一层一层地传入到某一些神经元当中,每经过一层神经元就相当于特征提取。我们大脑最终会将最后的特征进行汇总,类似汇总成一张具体的人脸,用这张人脸去大脑的某一个地方与存好的人名进行匹配。
那落实到我们计算机呢?其实这个过程是一样的,在计算机中进行特征提取的功能,就离不开我们今天要讲的卷积。
可以说,没有卷积的话,深度学习在图像领域不可能取得今天的成就。 那么就让我们来看看什么是卷积还有它在PyTorch中的实现吧。
卷积
在使用卷积之前,人们尝试了很多人工神经网络来处理图像问题,但是人工神经网络的参数量非常大,从而导致非常难训练,所以计算机视觉的研究一直停滞不前,难以突破。
直到卷积神经网络的出现,它的两个优秀特点:稀疏连接与平移不变性,这让计算机视觉的研究取得了长足的进步。什么是稀疏连接与平移不变性呢?简单来说,就是稀疏连接可以让学习的参数变得很少,而平移不变性则不关心物体出现在图像中什么位置。
稀疏连接与平移不变性是卷积的两个重要特点,如果你想从事计算机视觉相关的工作,这两个特点必须该清楚,但不是本专栏的重点,这里就不展开了,有兴趣你可以自己去了解。
下面我们直接来看看卷积是如何计算的。
最简单的情况
我们先看最简单的情况输入是一个4x4的特征图卷积核的大小为2x2。
卷积核是什么呢?其实就是我们卷积层要学习到的参数,就像下图中红色的示例,下图中的卷积核是最简单的情况,只有一个通道。
输入特征与卷积核计算时,计算方式是卷积核与输入特征按位做乘积运算然后再求和,其结果为输出特征图的一个元素,下图为计算输出特征图第一个元素的计算方式:
完成了第一个元素的计算,我们接着往下看,按以从左向右,从上至下的顺序进行滑动卷积核,分别与输入的特征图进行计算,请看下图,下图为上图计算完毕之后,向右侧滑动一个单元的计算方式:
第一行第三个单元的计算以此类推。说完了同一行的移动,我们再看看,第一行计算完毕,向下滑动的计算方式是什么样的。
第一行计算完毕之后,卷积核会回到行首,然后向下滑动一个单元,再重复以上从左至右的滑动计算。
这里我再给你补充一个知识点,什么是步长?
卷积上下左右滑动的长度我们称为步长用stride表示。上述例子中的步长就是1根据问题的不同会取不同的步长但通常来说步长为1或2。不管是刚才说的最简单的卷积计算还是我们后面要讲的标准卷积都要用到这个参数。
标准的卷积
好啦,前面只是最简单的情况,现在我们将最简单的卷积计算方式延伸到标准的卷积计算方式。
我们先将上面的例子描述为更加通用的形式输入的特征有m个通道宽为w高为h输出有n个特征图宽为\(w^{\\prime}\),高为\(h^{\\prime}\)卷积核的大小为kxk。
在刚才的例子中m、n、k、w、h、\(w^{\\prime}\)、\(h^{\\prime}\)的值分别为1、1、2、4、4、3、3。而现在我们需要把一个输入为(mhw)的输入特征图经过卷积计算,生成一个输出为(n, \(h^{\\prime}\), \(w^{\\prime}\))的特征图。
那我们来看看可以获得这个操作的卷积是什么样子的。输出特征图的通道数由卷积核的个数决定的所以说卷积核的个数为n。根据卷积计算的定义输入特征图有m个通道所以每个卷积核里要也要有m个通道。所以我们的需要n个卷积核每个卷积核的大小为(m, k, k)。
为了帮你更好地理解刚才所讲的内容,我画了示意图,你可以对照一下:
结合上面的图解可以看到卷积核1与全部输入特征进行卷积计算就获得了输出特征图中第1个通道的数据卷积核2与全部输入特征图进行计算获得输出特征图中第2个通道的数据。以此类推最终就能计算n个输出特征图。
在开篇的例子中输入只有1个通道现在有多个通道了那我们该如何计算呢其实计算方式类似输入特征的每一个通道与卷积核中对应通道的数据按我们之前讲过的方式进行卷积计算也就是输入特征图中第i个特征图与卷积核中的第i个通道的数据进行卷积。这样计算后会生成m个特征图然后将这m个特征图按对应位置求和即可求和后m个特征图合并为输出特征中一个通道的特征图。
我们可以用后面的公式表示当输入有多个通道时,每个卷积核是如何与输入进行计算的。
\(Output\_i\)表示计算第i个输出特征图i的取值为1到n
\(kernel\_k\)表示1个卷积核里的第k个通道的数据
\(input\_k\)表示输入特征图中的第k个通道的数据
\(bias\_k\)为偏移项,我们在训练时一般都会默认加上;
\(\\star\)为卷积计算;
\[Output\_i = \\sum\_{k=0}^{m}kernel\_k \\star input\_k + bias\_i, \\space \\space \\space \\space i=1,2,…,n\]我来解释一下为什么要加bias。就跟回归方程一样如果不加bias的话回归方程为y=wx不管w如何变化回归方程都必须经过原点。如果加上bias的话回归方程变为y=wx+b这样就不是必须经过原点可以变化的更加多样。
好啦,卷积计算方式的讲解到这里就告一段落了。下面我们看看在卷积层中有关卷积计算的另外一个重要参数。
Padding
让我们回到开头的例子可以发现输入的尺寸是4x4输出的尺寸是3x3。你有没有发现输出的特征图变小了没错在有多层卷积层的神经网络中特征图会越来越小。
但是,有的时候我们为了让特征图变得不是那么小,可以对特征图进行补零操作。这样做主要有两个目的:
1.有的时候需要输入与输出的特征图保持一样的大小;-
2.让输入的特征保留更多的信息。
这里我举个例子,带你看看,一般什么情况下会希望特征图变得不那么小。
通过刚才的讲解我们知道如果不补零且步长stride为1的情况下当有多层卷积层时特征图会一点点变小。如果我们希望有更多层卷积层来提取更加丰富的信息时就可以让特征图变小的速度稍微慢一些这个时候就可以考虑补零。
这个补零的操作就叫做paddingpadding等于1就是补一圈的零等于2就是补两圈的零如下图所示
在Pytorch中padding这个参数可以是字符串、int和tuple。
我们分别来看看不同参数类型怎么使用:当为字符串时只能取\(^{\\prime}valid^{\\prime}\)与\(^{\\prime}same^{\\prime}\)。当给定整型时则是说要在特征图外边补多少圈0。如果是tuple的时候则是表示在特征图的行与列分别指定补多少零。
我们重点看一下字符串的形式,相比于直接给定补多少零来说,我认为字符串更加常用。其中,\(^{\\prime}valid^{\\prime}\)就是没有padding操作就像开头的例子那样。\(^{\\prime}same^{\\prime}\)则是让输出的特征图与输入的特征图获得相同的大小。
那当padding为same时到底是怎么计算的呢我们继续用开篇的例子说明现在padding为\(^{\\prime}same^{\\prime}\)了。
当滑动到特征图最右侧时,发现输出的特征图的宽与输入的特征图的宽不一致,它会自动补零,直到输出特征图的宽与输入特征图的宽一致为止。如下图所示:
高的计算和宽的计算同理,当计算到特征图的底部时,发现输出特征图的高与输入特征图的高不一致时,它同样会自动补零,直到输入和输出一致为止,如下图所示。
完成上述操作我们就可以获得与输入特征图有相同高、宽的输出特征图了。理论讲完了我们还是要学以致用在实践中深入体会。在下面的练习中我们会实际考察一下当padding为same时是否像我们说的这样计算。
PyTorch中的卷积
卷积操作定义在torch.nn模块中torch.nn模块为我们提供了很多构建网络的基础层与方法。
在torch.nn模块中关于今天介绍的卷积操作有nn.Conv1d、nn.Conv2d与nn.Conv3d三个类。
请注意我们上述的例子都是按照nn.Conv2d来介绍的nn.Conv2d也是用的最多的而nn.Conv1d与nn.Conv3d只是输入特征图的维度有所不一样而已很少会被用到。
让我们先看看创建一个nn.Conv2d需要哪些必须的参数
# Conv2d类
class torch.nn.Conv2d(in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros',
device=None,
dtype=None)
我们挨个说说这些参数。首先是跟通道相关的两个参数in_channels是指输入特征图的通道数数据类型为int在标准卷积的讲解中in_channels为mout_channels是输出特征图的通道数数据类型为int在标准卷积的讲解中out_channels为n。
kernel_size是卷积核的大小数据类型为int或tuple需要注意的是只给定卷积核的高与宽即可在标准卷积的讲解中kernel_size为k。
stride为滑动的步长数据类型为int或tuple默认是1在前面的例子中步长都为1。
padding为补零的方式注意当padding为validsamestride必须为1。
对于kernel_size、stride、padding都可以是tuple类型当为tuple类型时第一个维度用于height的信息第二个维度时用于width的信息。
bias是否使用偏移项。
还有两个参数dilation与groups具体内容下节课我们继续展开讲解你先有个印象就行。
验证same方式
接下来我们做一个练习验证padding为same时计算方式是否像我们所说的那样。过程并不复杂一共三步分别是创建输入特征图、设置卷积以及输出结果。
先来看第一步我们创建好例子中的441大小的输入特征图代码如下
import torch
import torch.nn as nn
input_feat = torch.tensor([[4, 1, 7, 5], [4, 4, 2, 5], [7, 7, 2, 4], [1, 0, 2, 4]], dtype=torch.float32)
print(input_feat)
print(input_feat.shape)
# 输出:
tensor([[4., 1., 7., 5.],
[4., 4., 2., 5.],
[7., 7., 2., 4.],
[1., 0., 2., 4.]])
torch.Size([4, 4])
第二步创建一个2x2的卷积根据刚才的介绍输入的通道数为1输出的通道数为1padding为same所以卷积定义为
conv2d = nn.Conv2d(1, 1, (2, 2), stride=1, padding='same', bias=True)
# 默认情况随机初始化参数
print(conv2d.weight)
print(conv2d.bias)
# 输出:
Parameter containing:
tensor([[[[ 0.3235, -0.1593],
[ 0.2548, -0.1363]]]], requires_grad=True)
Parameter containing:
tensor([0.4890], requires_grad=True)
需要注意的是,默认情况下是随机初始化的。一般情况下,我们不会人工强行干预卷积核的初始化,但是为了验证今天的例子,我们对卷积核的参数进行干预。请注意下面代码中卷积核的注释,代码如下:
conv2d = nn.Conv2d(1, 1, (2, 2), stride=1, padding='same', bias=False)
# 卷积核要有四个维度(输入通道数,输出通道数,高,宽)
kernels = torch.tensor([[[[1, 0], [2, 1]]]], dtype=torch.float32)
conv2d.weight = nn.Parameter(kernels, requires_grad=False)
print(conv2d.weight)
print(conv2d.bias)
# 输出:
Parameter containing:
tensor([[[[1., 0.],
[2., 1.]]]])
None
完成之后就进入了第三步,现在我们已经准备好例子中的输入数据与卷积数据了,下面只需要计算一下,然后输出就可以了,代码如下:
output = conv2d(input_feat)
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
/var/folders/pz/z8t8232j1v17y01bkhyrl01w0000gn/T/ipykernel_29592/2273564149.py in <module>
----> 1 output = conv2d(input_feat)
~/Library/Python/3.8/lib/python/site-packages/torch/nn/modules/module.py in _call_impl(self, *input, **kwargs)
1049 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
1050 or _global_forward_hooks or _global_forward_pre_hooks):
-> 1051 return forward_call(*input, **kwargs)
1052 # Do not call functions when jit is used
1053 full_backward_hooks, non_full_backward_hooks = [], []
~/Library/Python/3.8/lib/python/site-packages/torch/nn/modules/conv.py in forward(self, input)
441
442 def forward(self, input: Tensor) -> Tensor:
--> 443 return self._conv_forward(input, self.weight, self.bias)
444
445 class Conv3d(_ConvNd):
~/Library/Python/3.8/lib/python/site-packages/torch/nn/modules/conv.py in _conv_forward(self, input, weight, bias)
437 weight, bias, self.stride,
438 _pair(0), self.dilation, self.groups)
--> 439 return F.conv2d(input, weight, bias, self.stride,
440 self.padding, self.dilation, self.groups)
441
RuntimeError: Expected 4-dimensional input for 4-dimensional weight[1, 1, 2, 2], but got 2-dimensional input of size [4, 4] instead
结合上面代码你会发现这里报错了提示信息是输入的特征图需要是一个4维的而我们的输入特征图是一个4x4的2维特征图。这是为什么呢-
请你记住Pytorch输入tensor的维度信息是(batch_size, 通道数,高,宽)但是在我们的例子中只给定了高与宽没有给定batch_size在训练时不会将所有数据一次性加载进来训练而是以多个批次进行读取的每次读取的量成为batch_size与通道数。所以我们要回到第一步将输入的tensor改为(1,1,4,4)的形式。
你还记得我在之前的讲解中提到过怎么对数组添加维度吗?
在Pytorch中unsqueeze()对tensor的维度进行修改。代码如下
input_feat = torch.tensor([[4, 1, 7, 5], [4, 4, 2, 5], [7, 7, 2, 4], [1, 0, 2, 4]], dtype=torch.float32).unsqueeze(0).unsqueeze(0)
print(input_feat)
print(input_feat.shape)
# 输出:
tensor([[[[4., 1., 7., 5.],
[4., 4., 2., 5.],
[7., 7., 2., 4.],
[1., 0., 2., 4.]]]])
torch.Size([1, 1, 4, 4])
这里unsqueeze()中的参数是指在哪个位置添加维度。-
好,做完了修改,我们再次执行代码。
output = conv2d(input_feat)
输出:
tensor([[[[16., 11., 16., 15.],
[25., 20., 10., 13.],
[ 9., 9., 10., 12.],
[ 1., 0., 2., 4.]]]])
你可以看看,跟我们在例子中推导的结果一不一样?
总结
恭喜你完成了今天的学习。今天所讲的卷积非常重要,它是各种计算机视觉应用的基础,例如图像分类、目标检测、图像分割等。
卷积的计算方式是你需要关注的重点。具体过程如下图所示输出特征图的通道数由卷积核的个数决定的下图中因为有n个卷积核所以输出特征图的通道数为n。输入特征图有m个通道所以每个卷积核里要也要有m个通道。
其实卷积背后的理论比较复杂但在PyTorch中实现却很简单。在卷积计算中涉及的几大要素输入通道数、输出通道数、步长、padding、卷积核的大小分别对应的就是PyTorch中nn.Conv2d的关键参数。所以就像前面讲的那样我们要熟练用好nn.Conv2d()。
之后我还带你做了一个验证same方式的练习动手跑跑代码会帮你形成直观印象快速掌握这部分内容。
当然对于卷积来说不光光有今天介绍的这种比较标准的卷积还有各种变形。例如今天没有讲到的dilation参数与groups参数基于这两个参数实现的卷积操作我会在下一节课中为展开敬请期待。
每课一练
请你想一想padding为samestride可以为1以外的数值吗
欢迎你在留言区记录你的疑问或收获,也推荐你把这节课分享给更多朋友、同事。
我是方远,我们下节课见!

View File

@ -0,0 +1,206 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 卷积(下):如何用卷积为计算机“开天眼”?
你好,我是方远。
经过上一节课的学习相信你已经对标准的卷积计算有所了解。虽然标准卷积基本上可以作为主力Carry全场但是人们还是基于标准卷积提出了一些其它的卷积方式这些卷积方式在应对不同问题时能够发挥不同的作用这里我为你列举了一些。
在上一节课中我们学习了conv2d的in_channels、out_channels、kernel_size、stride、padding与bias参数。
其中PyTorch中conv2d中剩余的两个参数它们分别对应着两种不同的卷积分别是深度可分离卷积和空洞卷积让我们一起来看看。
深度可分离卷积Depthwise Separable Convolution
我们首先看看依托groups参数实现的深度可分离卷积。
随着深度学习技术的不断发展许多很深、很宽的网络模型被提出例如VGG、ResNet、SENet、DenseNet等这些网络利用其复杂的结构可以更加精确地提取出有用的信息。同时也伴随着硬件算力的不断增强可以将这些复杂的模型直接部署在服务器端在工业中可以落地的项目中都取得了非常优秀的效果。
但这些模型具有一个通病,就是速度较慢、参数量大,这两个问题使得这些模型无法被直接部署到移动终端上。而移动端的各种应用无疑是当今最火热的一个市场,这种情况下这些深而宽的复杂网络模型就不适用了。
因此,很多研究将目光投入到寻求更加轻量化的模型当中,这些轻量化模型的要求是速度快、体积小,精度上允许比服务器端的模型稍微降低一些。
深度可分离卷积就是谷歌在MobileNet v1中提出的一种轻量化卷积。简单来说深度可分离卷积就是我们刚才所说的在效果近似相同的情况下需要的计算量更少。接下来我们先来看看深度可分离卷积是如何计算的然后再对比一下计算量到底减少了多少。
深度可分离卷积Depthwise Separable Convolution由 DepthwiseDW和 PointwisePW这两部分卷积组合而成的。
我们先来复习一下标准卷积,然后再来讲解一下获得同样输出特征图的深度可分离卷积是如何工作的。
你还记得下面这张图吗这是我们上节课讲到的标准卷积计算方式它描述的是输入m个尺寸为h, w的特征图通过卷积计算获得n个通道尺寸为\(h^{\\prime}\)与\(w^{\\prime}\)的特征图的计算过程。
我们将特征图与一个卷积核计算的过程展开一下请看下图。一个卷积核中的m个卷积分别与输入特征图的m个通道数据进行卷积计算生成一个中间结果然后m个中间结果按位求和最终就能获得n个输出特征图中的一个特征图。-
DepthwiseDW卷积
那什么是DW卷积呢DW卷积就是有m个卷积核的卷积每个卷积核中的通道数为1这m个卷积核分别与输入特征图对应的通道数据做卷积运算所以DW卷积的输出是有m个通道的特征图。通常来说DW卷积核的大小是3x3的。
DW卷积的过程如下图所示
PointwisePW卷积
通常来说,深度可分离卷积的目标是轻量化标准卷积计算的,所以它是可以来替换标准卷积的,这也意味着原卷积计算的输出尺寸是什么样,替换后的输出尺寸也要保持一致。
所以在深度可分离卷积中我们最终要获得一个具有n个通道的输出特征图而刚才介绍的DW卷积显然没有达到并且DW卷积也忽略了输入特征图通道之间的信息。
所以在DW之后我们还要加一个PW卷积。PW 卷积也叫做逐点卷积。PW卷积的主要作用就是将DW输出的m个特征图结合在一起考虑再输出一个具有n个通道的特征图。
在卷积神经网络中我们经常可以看到使用1x1的卷积1x1的卷积主要作用就是升维与降维。所以在DW的输出之后的PW卷积就是n个卷积核的1x1的卷积每个卷积核中有m个通道的卷积数据。
为了帮你理解刚才我描述的这个过程,我还是用图解的方式为你描述一下,你可以对照下图看一看:
经过这样的DW与PW的组合我们就可以获得一个与标准卷积有同样输出尺寸的轻量化卷积啦。既然是轻量化那么我们下面就来看看深度可分离卷积的计算量相对于标准卷积减少了多少呢
计算量
我们的原问题是有m个通道的输入特征图卷积核尺寸为kxk输出特征图的尺寸为\((n, h^{\\prime}, w^{\\prime})\),那么标准的卷积的计算量为:
\[k \\times k \\times m \\times n \\times h^{\\prime} \\times w^{\\prime}\]我们是怎么得出这个结果的呢?你可以从输出特征图往回思考。
上图输出特征图中每个点的数值是由n个卷积核与输入特征图计算出来的吧这个计算量是\(k \\times k \\times m \\times n\),那输出特征图有多少个点?没错,一共有\(h^{\\prime} \\times w^{\\prime}\)个。所以,我们自然就得出上面的计算方式了。
如果采用深度可分离卷积DW的计算量为\(k \\times k \\times m \\times h^{\\prime} \\times w^{\\prime}\)而PW的计算量为\(1 \\times 1 \\times m \\times n \\times h^{\\prime} \\times w^{\\prime}\)。
我们不难得出标准卷积与深度可分离卷积计算量的比值为:
\[\\frac {k \\times k \\times m \\times h^{\\prime} \\times w^{\\prime} + 1 \\times 1 \\times m \\times n \\times h^{\\prime} \\times w^{'}}{k \\times k \\times m \\times n \\times h^{\\prime} \\times w^{\\prime}}\]\[= \\frac {1}{n} + \\frac {1}{k \\times k}\]所以,深度可分离卷积的计算量大约为普通卷积计算量的\(\\frac {1}{k^2}\)。
那深度可分离卷积落实到PyTorch中是怎么实现的呢
PyTorch中的实现
在PyTorch中实现深度可分离卷积的话我们需要分别实现DW与PW两个卷积。我们先看看DW卷积实现DW卷积的话就会用到nn.Conv2d中的groups参数。groups参数的作用就是控制输入特征图与输出特征图的分组情况。
当groups等于1的时候就是我们上一节课讲的标准卷积而groups=1也是nn.Conv2d的默认值。
当groups不等于1的时候会将输入特征图分成groups个组每个组都有自己对应的卷积核然后分组卷积获得的输出特征图也是有groups个分组的。需要注意的是groups不为1的时候groups必须能整除in_channels和out_channels。
当groups等于in_channels时就是我们的DW卷积啦。
下面我们一起动手操作一下看看如何实现一个DW卷积。首先我们来生成一个三通道的5x5输入特征图然后经过深度可分离卷积输出一个4通道的特征图。
DW卷积的实现代码如下
import torch
import torch.nn as nn
# 生成一个三通道的5x5特征图
x = torch.rand((3, 5, 5)).unsqueeze(0)
print(x.shape)
# 输出:
torch.Size([1, 3, 5, 5])
# 请注意DW中输入特征通道数与输出通道数是一样的
in_channels_dw = x.shape[1]
out_channels_dw = x.shape[1]
# 一般来讲DW卷积的kernel size为3
kernel_size = 3
stride = 1
# DW卷积groups参数与输入通道数一样
dw = nn.Conv2d(in_channels_dw, out_channels_dw, kernel_size, stride, groups=in_channels_dw)
你需要注意以下几点内容:
1.DW中输入特征通道数与输出通道数是一样的-
2.一般来讲DW的卷积核为3x3-
3.DW卷积的groups参数与输出通道数是一样的。
好啦DW如何实现我们已经写好了接下来就是PW卷积的实现。其实PW卷积的实现就是我们上一节课介绍的标准卷积只不过卷积核为1x1。需要注意的是PW卷积的groups就是默认值了。
具体代码如下所示:
in_channels_pw = out_channels_dw
out_channels_pw = 4
kernel_size_pw = 1
pw = nn.Conv2d(in_channels_pw, out_channels_pw, kernel_size_pw, stride)
out = pw(dw(x))
print(out.shape)
好了groups以及深度可分离卷积就讲完了接下来我们看看最后一个dilation参数它是用来实现空洞卷积的。
空洞卷积
空洞卷积经常用于图像分割任务当中。图像分割任务的目的是要做到pixel-wise的输出也就是说对于图片中的每一个像素点模型都要进行预测。
对于一个图像分割模型,通常会采用多层卷积来提取特征的,随着层数的不断加深,感受野也越来越大。这里有个新名词——“感受野”,这个我稍后再解释。我们先把空洞卷积的作用说完。
但是对于图像分割模型有个问题经过多层的卷积与pooling操作之后特征图会变小。为了做到每个像素点都有预测输出我们需要对较小的特征图进行上采样或反卷积将特征图扩大到一定尺度然后再进行预测。
要知道,从一个较小的特征图恢复到一个较大的特征图,这显然会带来一定的信息损失,特别是较小的物体,这种损失是很难恢复的。那问题来了,能不能既保证有比较大的感受野,同时又不用缩小特征图呢?
估计你已经猜到了,空洞卷积就是解决这个问题的杀手锏,它最大的优点就是不需要缩小特征图,也可以获得更大的感受野。
感受野
现在让我来解释一下什么是感受野。感受野是计算机视觉领域中经常会看到的一个概念。
因为伴随着不断的pooling这是卷积神经网络中的一种操作通常是在一定区域的特征图内取最大值或平均值用最大值或平均值代替这个区域的所有数据pooling操作会使特征图变小或者卷积操作在卷积神经网络中不同层的特征图是越来越小的。
这就意味着在卷积神经网络中,相对于原图来说,不同层的特征图,其计算区域是不一样的,这个区域就是感受野。感受野越大,代表着包含的信息更加全面、语义信息更加抽象,而感受野越小,则代表着包含更加细节的语义信息。
光说理论不容易理解我们还是结合例子看一看。请看下图原图是6x6的图像第一层卷积层为3x3这时它输出的感受野就是3因为输出的特征图中每个值都是由原图中3x3个区域计算而来的。
再看下图卷积层2也为3x3的卷积输出为2x2的特征图。这时卷积层2的感受野就会变为5输入特征图中蓝色加橘黄色部分
配合图解,我相信你很容易就能明白感受野的含义了。
计算方式
好,那么我们再来看看空洞卷积具体是如何计算的。
用语言来描述空洞卷积的计算方式比较抽象我们不妨看一下它的动态示意图这个GitHub中有各种卷积计算的动态图非常直观我们借助它来学习一下空洞卷积
首先,我们先来看看上节课讲的标准卷积是如何计算的。
对照上图,下面的蓝图为输入特征图,滑动的阴影为卷积核,绿色的为输出特征图。
然后我们再对照一下的空洞卷积示意图。
结合示意图我们会发现计算方式与普通卷积一样只不过是将卷积核以一定比例拆分开来。实现起来呢就是用0来充填卷积核。
这个分开的比例我们一般称之为扩张率就是Conv2d中的dilation参数。
dilation参数默认为1同样也是可以为int或者tuple。当为tuple时第一位代表行的信息第二位代表列的信息。
总结
恭喜你完成了今天的学习。今天我们在实现PyTorch卷积操作的同时学习了两个特殊的卷积深度可分离卷积与空洞卷积。
对于空洞卷积,你最需要掌握的是感受野这个概念,以及空洞卷积的计算方式。感受野就是能在原始图像中反应的区域。
深度可分离卷积主要用于轻量化的模型,而空洞卷积主要用于图像分割任务中。
这里分享一下我的经验:如果说你需要轻量化你的模型,让你的模型变得更小、更快,你可以考虑将卷积层替换为深度可分离卷积。如果你在做图像分割项目的话,可以考虑将网络靠后的层替换为空洞卷积,看看效果是否能有所提高。
最后我们再来总结一下PyTorch中卷积操作的各个重要参数我用表格的方式帮你做了总结归纳你可以把它作为自己的工具包时常翻看。
每课一练
随机生成一个3通道的128x128的特征图然后创建一个有10个卷积核且卷积核尺寸为3x3DW卷积的深度可分离卷积对输入数据进行卷积计算。
欢迎你在留言区跟我交流互动,也推荐你把今天的内容分享给更多同事、朋友。

View File

@ -0,0 +1,186 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 损失函数:如何帮助模型学会“自省”?
你好,我是方远。
在前面的课程中我们一同拿下了深度学习实战所需的预备基础知识包括PyTorch的基础操作、NumPy、Tensor的特性跟使用方法等还一起学习了基于Torchvision的数据相关操作与特性。恭喜你走到这里基础打好以后我们距离实战关卡又进了一步。
有了基础预备知识,我们就要开始学习深度学习的几个重要的概念了。
一个深度学习项目包括了模型的设计、损失函数的设计、梯度更新的方法、模型的保存与加载、模型的训练过程等几个主要模块。每个模块都在整个深度学习项目搭建中意义重大,我特意为你画了一个示意图,方便你整体把握它们的功能。
这节课咱们先从损失函数开始说起。损失函数是一把衡量模型学习效果的尺子,甚至可以说,训练模型的过程,实际就是优化损失函数的过程。如果你去面试机器学习岗位,常常会被问到前向传播、神经网络等内容,其实这些知识的考察都不可避免地会涉及到损失函数的相关概念。
今天,我就从识别劳斯莱斯这个例子,带你了解损失函数的工作原理和常见类型。
一个简单的例子
回想一下我们学习新知识的大致过程,比如现在让你背一个单词,我举一个夸张的例子:
Pneumonoultramicroscopicsilicovolcanoconiosis矽肺病
为了背会这个单词,你要反复地去看去记,第一次可能记住了开头的几个字母,第二次又记住了中间的几个字母,第三次又记住了结尾的几个字母,然后不断地反复学习,才能掌握这个单词的准确组成。为了检验你的学习成果,老师还会让你默写单词,跟标准拼写进行对照。
刚才的例子用的是自然语言,那么如果视觉问题呢?比如我现在给你一个劳斯莱斯汽车的照片,让你记住,这就是这辈子都买不起的劳斯莱斯。
你会怎么去记住它呢?对,你会下意识去寻找最具有代表性的内容,比如车前脸的方形格栅、车前面的立起来的小金人,方方正正的车体等。
等你以后见到了有了具有以上特征的汽车,你就知道,它是你要躲远点的劳斯莱斯了。不过呢,如果这些特征发生了变化,你又要犹豫或者怀疑它是不是别的品牌的汽车了。
其实,模型的学习也是一样的,模型最开始的时候就是一张白纸,它什么都不知道。我们作为研发人员,就要不断地给模型提供要学习的数据。
模型拿到数据之后就要有一个非常重要的环节:把模型自己的判断结果和数据真实的情况做比较。如果偏差或者差异特别大,那么模型就要去纠正自己的判断,用某种方式去减少这种偏差,然后反复这个过程,直到最后模型能够对数据进行正确的判断。
衡量这种偏差的方式很重要,也是模型学习进步的关键所在。这种减少偏差的过程,我们称之为拟合。接下来我们一同看看拟合的几种情况。
过拟合与欠拟合
我们先来学习第一组概念,也就是过拟合和欠拟合。为了方便你理解,我们结合函数曲线的例子来看看。
首先假设在一个二维坐标系中有若干个点,我们需要让一个函数(模型)通过学习去尽可能地拟合这些点。那么拟合的结果都有哪几种可能呢?我们看看下面的图片:
在第一张图中蓝色的曲线是我们学习到的第一个模型函数H1。我们发现H1好像没有很好地学习到这些点的拟合或者说函数跟样本点的拟合效果较差只有一个大致符合的趋势。这种情况我们称之为“欠拟合”。
既然有“欠”就有“过”,我们继续看第二张图。
在这张图中红色的曲线是我们学习到的第二个模型函数H2在这个结果上我们看到函数曲线可以很好地拟合所有的点。
但是这里存在两个问题第一曲线对应的函数有点太过复杂了不像H1那样简单明了第二如果我们在H2的曲线附近再增加一个点这条H2对应的曲线就很难去拟合好。这种情况就叫做“过拟合”实在是太过了。
那么我们再来看第三张图,这张图的曲线就比较靠谱了,这个函数不是太复杂,同时也能较好拟合绝大部分的点。
看到这里,你可能会有疑惑,为什么我们会如此的在意“复杂”这个问题呢?其实你可以这样想,有这样两个函数:\(y1=3x^2 + 2\)\(y2=3x^7 + 7x^6 + 6x^2 + 4x+18\)。y1无论是从可解释性上还是在简洁程度、计算量方面都要比y2好得多。
越复杂的函数,在实际工作中就需要越多的计算资源和时间消耗。当然了,我们也不能一味的追求简单,否则就会欠拟合。
损失函数与代价函数
过拟合和欠拟合的概念实际上就是模型的表现效果。接下来,我们再来看看损失函数和代价函数,这组概念就是我们刚才说用来衡量“偏差”、“效果”的方法。
我们还是延续之前的思路用函数举例子。假设刚才的二维空间中任意一个点对应的真实函数为F(x)。我们通过模型的学习拟合出来的函数为f(x)。根据刚才提到的学习过程我们会知道F(x)和f(x)之间存在一个误差我们定义为L(x),于是有:
\[-
L(x)=(F(x)-f(x))^{2}-
\]这里F(x)和f(x)的差距我们做了一个平方和,是为了保证两者的误差是一个正值,方便后续的计算。当然,你也可以做成绝对值的形式,后面课程里我们还会讲到梯度更新,那时你就会发现,平方和要比绝对值更为方便。这里你先有个印象就好,让我们言归正传。
有了L(x)我们就有了一个评价拟合函数表现效果“好坏”的度量指标这个指标函数我们称作损失函数loss fuction)。根据公式可知损失函数越小拟合函数对于真实情况的拟合效果就越好。这里有一点需要你注意损失函数的种类有很多种L(x)只是我们学习到的第一个损失函数。
接下来,我们将数据从刚才的任意一个点,扩大到所有的点,那么这些点实际上就是一个训练集合。把集合所有的点对应的拟合误差做平均,就会得到如下公式:
\[-
\\frac{1}{N} \\sum\_{i=0}^{N}(F(x)-f(x))^{2}-
\]这个函数叫做代价函数cost function即在训练样本集合上所有样本的拟合误差的平均值。代价函数我们也称作经验风险。
其实,在实际的应用中,我们并不会严格区分损失函数和代价函数。你只需要知道,损失函数是单个样本点的误差,代价函数是所有样本点的误差。明白了这些,你哪怕混着叫,也没什么问题。
常见损失函数
在了解了损失函数的定义之后,我们来看一下常用的损失函数都有哪些。
其实,严格来说,损失函数的种类是无穷多的。这是因为损失函数是用来度量模型拟合效果和真实值之间的差距,而度量方式要根据问题的特点或者需要优化的方面具体定制,所以损失函数的种类是无穷无尽的。
作为初学者我推荐你从一些常用的损失函数做开始学习。今天我们一块来看看5种最基本的损失函数。
0-1损失函数
假定我们要一个判断类型的问题,比如让模型判断用户输入的文字是不是数字。那么模型判断的结果只有两种:是和不是。
于是我们很容易就会想到一个最为简单的评估方式如果模型预测对了损失函数的值就为0因为没有误差如果模型预测错了那么损失函数的值就为1。这就是最简单的0-1损失函数这个函数的公式表示如下
\[-
L(F(x), f(x)) = \\left\\{\\begin{matrix}-
0 & if F(x) \\ne f(x)\\\\\\-
1 & if F(x) = f(x)-
\\end{matrix}\\right.-
\]
其中F(x)是输入数据的真实类别f(x)是模型预测的类别。是不是很简单?
但是0-1损失函数的使用频率是非常少的这是为什么呢因为模型训练中经常用到的梯度更新和反向传播都需要能够求导的损失函数可是0-1损失函数的导数值是0常数的导数为0所以它应用不多。
尽管如此我们也一定要了解0-1损失函数因为它是最简单的损失函数有着很重要的意义。
平方损失函数
前面讲损失函数的定义时,我们曾举了一个例子\(L(x)=(F(x)-f(x))^{2}\)这个函数的正式名称叫做平方损失函数。有时候我们会在损失函数中加入一个1/2的系数这是为了求导的时候能够跟平方项的系数约掉。
平方损失函数是可求导的损失函数中最简单的一种,它直接度量了模型拟合结果和真实结果之间的距离。在实际项目中,很多简单的问题,比如手写分类、花卉识别等,都可以使用这种简单的损失函数。
均方差损失函数和平均绝对误差损失函数
在正式讲解均方差损失函数之前,我们先补充一个重要的背景知识:机器学习分为有监督学习和无监督学习两大类。
其中有监督学习是从标签化训练数据集中,推断出函数的机器学习任务,也就是说:模型通过标注好的数据,就像一个学生(模型)一样,被老师(数据)“指导”和“监督”着去学习。有监督学习问题主要可以划分为两类,分类和回归。其中回归问题是根据数据预测一个数值。
而均方误差Mean Squared ErrorMSE是回归问题损失函数中最常用的一个也称作L2损失函数。它是预测值与目标值之间差值的平方和。它的定义如下
\[-
M S E=\\frac{\\sum\_{i=1}^{n}\\left(s\_{i}-y\_{i}^{p}\\right)^{2}}{n}-
\]其中s为目标值的向量表示y为预测值的向量表示。
细心的你会发现平方损失函数好像也是差不多一个样子呀没错这两种形式本质上是等价的。只是MSE计算得到的值是把整个样本的误差做了平均也就是加起来之后除了一个n。误差平方和以及均方差的公式中有系数1/2这是为了求导后系数被约去。
而平均绝对误差损失函数Mean Absolute Error, MAE是另一种常用于回归问题的损失函数它的目标是度量真实值和预测值差异的绝对值之和定义如下
\[-
M A E=\\frac{\\sum\_{i=1}^{n}\\left|y\_{i}-y\_{i}^{p}\\right|}{n}-
\]
交叉熵损失函数
接下来,我们再了解一下交叉熵损失函数。
熵这个概念有的小伙伴可能有些陌生,跟刚才一样,让我们先来简单了解一下什么是熵。熵最开始是物理学中的一个术语,它表示了一个系统的混乱程度或者说无序程度。如果一个系统越混乱,那么它的熵越大。
后来,信息论创始人香农把这个概念引申到信道通信的过程中,开创了信息论,所以这里的熵又称为信息熵。信息熵的公式化可以表示为:
\[-
H§=-\\sum\_{i} p\\left(x\_{i}\\right) \\log p\\left(x\_{i}\\right)-
\]其中x表示随机变量与之相对应的是所有可能输出的集合。P(x)表示输出概率函数。变量的不确定性越大,熵也就越大,把变量搞清楚所需要的信息量也就越大。
当我们将函数变为如下格式将log p改为log q
\[-
\-\\sum\_{i=1}^{n} p\\left(x\_{i}\\right) \\log \\left(q\\left(x\_{i}\\right)\\right)-
\]其中,𝑝(𝑥)表示真实概率分布,𝑞(𝑥)表示预测概率分布。这个函数就是交叉熵损失函数Cross entropy loss。也就意味着这个公式同时衡量了真实概率分布和预测概率分布两方面。所以这个函数实际上就是通过衡量并不断去尝试缩小两个概率分布的误差使预测的概率分布尽可能达到真实概率分布。
softmax损失函数
softmax是深度学习中使用非常频繁的一个函数。在某些场景下一些数值大小范围分布非常广而为了方便计算或者使梯度更好的更新后续我们还会学习梯度更新我们需要把输入的这些数值映射为0-1之间的实数并且归一化后能够保证几个数的和为1。
它的公式化表示为:-
$\(-
S\_{j}=\\frac{e^{a\_{j}}}{\\sum\_{k=1}^{T} e^{a\_{k}}}-
\)$
回到刚才的交叉熵损失函数公式中的q(xi)也就是预测的概率分布如果我们换成softmax方式的表示
\[\\sum\_{i=1}^{n}p(x\_i)log(S\_i)\]之后我们就得到了一个成为softmax损失函数softmax loss的新函数也称为softmax with cross-entropy loss它是交叉熵损失函数的一个特例。
损失函数的种类非常多,这里我选择了最常用的几种。咱们在后续的实战环节,将会遇到更多的损失函数,到时候我再为你详细展开。
小结
这节课我们一同学习了损失函数的原理。对于模型来说,损失函数就是一个衡量其效果表现的尺子,有了这把尺子,模型就知道了自己在学习过程中是否有偏差,以及偏差到底有多大,从而做到“三省吾身”。
今天所讲的公式虽然数量不少,但并不需要你背下来。我想提醒你的是,这些公式有必要先过一遍,有了基本的理解,才能知道原理。否则,没有这些公式做基础,后面你根本无法区分不同的损失函数。
在实际的研发中,损失函数的设定是非常重要的,其地位甚至比得上模型网络设计。因为如果没有好的损失函数做指导的话,一切的功夫都白做了。就比如我们做最简单的手写体识别,损失函数每次计算模型和真实值的区别,通过这个损失函数,我们的模型才能知道自己学对了还是学错了,才能真正的有效学习。
后面咱们就要开始学习如何通过损失函数来更新模型参数的方法了,这也是非常有意思的一个话题,敬请期待。
每课一练
损失函数的值越小越好么?
欢迎你在留言区跟我交流互动,也推荐你把今天的内容分享给更多同事,朋友。
我是方远,我们下节课见!

View File

@ -0,0 +1,165 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 计算梯度:网络的前向与反向传播
你好,我是方远。
在上节课,我们一同学习了损失函数的概念以及一些常用的损失函数。你还记得我们当时说的么:模型有了损失函数,才能够进行学习。那么问题来了,模型是如何通过损失函数进行学习的呢?
在接下来的两节课中,我们将会学习前馈网络、导数与链式法则、反向传播、优化方法等内容,掌握了这些内容,我们就可以将模型学习的过程串起来作为一个整体,彻底搞清楚怎样通过损失函数训练模型。
下面我们先来看看最简单的前馈网络。
前馈网络
前馈网络,也称为前馈神经网络。顾名思义,是一种“往前走”的神经网络。它是最简单的神经网络,其典型特征是一个单向的多层结构。简化的结构如下图:
结合上面的示意图我带你具体看看前馈网络的结构。这个图中你会看到最左侧的绿色的一个个神经元它们相当于第0层一般适用于接收输入数据的层所以我们把它们叫做输入层。
比如我们要训练一个y=f(x)函数的神经网络x作为一个向量就需要通过这个绿色的输入层进入模型。那么在这个网络中输入层有5个神经元这意味着它可以接收一个5维长度的向量。
结合图解,我们继续往下看,网络的中间有一层红色的神经元,它们相当于模型的“内部”,一般来说对外不可见,或者使用者并不关心的非结果部分,我们称之为隐藏层。在实际的网络模型中,隐藏层会有非常多的层数,它们是网络最为关键的内部核心,也是模型能够学习知识的关键部分。
在图的右侧,蓝色的神经元是网络的最后一层。模型内部计算完成之后,就需要通过这一层输出到外部,所以也叫做输出层。
需要说明的是,神经元之间的连线,表示神经元之间连接的权重,通过权重就会知道网络中每个节点的重要程度。
那么现在我们回头再来看看前馈神经网络这个名字,是不是就很好理解了。在前馈网络中,数据从输入层进入到隐藏层的第一层,然后传播到第二层,第三层……一直到最后通过输出层输出。数据的传播是单向的,无法后退,只能前行。
导数、梯度与链式法则
既然有了前向的数据传播,自然也会有反向的数据传播过程。
说到反向传播,我们常常还会把梯度下降、链式法则这些词挂在嘴边。不过初次接触的话,这些生词你直接搜定义,常常还是一头雾水。其实并不是这些概念很复杂,而是你的学习路径有问题。
所以,接下来我会带你重温高数学过的导数、偏导数,搞懂这些前置知识,你就能对反向传播所需的知识做一个回顾,也能更好地理解反向传播的原理。
导数
导数,也叫做导函数值。
还记得高中数学我们曾学习过的斜率么?例如一个函数\(F=2x^2\)它的导数F=4x。其实斜率就是一种特殊情况下的导数。
更普遍的情况也很容易推导我们以F=3x为例在x=3的时候函数值为3x=3*3=9。现在我们给x一个非常小的增量Δx那么就有了F(x+Δx)=3(x+Δx)也就是说函数值也有了一个非常小的增量我们记为Δy。
当函数值增量Δy与变量x的增量Δx的比值在Δx趋近于0时如果极限a存在我们就称a为函数F(x)在x处的导数。
需要注意的是Δx一定要趋近于0而且极限a是要存在的。不过在这节课里极限的定义以及如何去判断极限并非是核心内容感兴趣的小伙伴有空可以自己查阅相关的内容。
对照下面的公式,你会对导数的理解更加清晰,高中数学的斜率其实就是一种特殊的导数。导数我们一般采用如下的方式做描述:
\[-
f^{\\prime}\\left(x\_{0}\\right)=\\lim \_{\\Delta x \\rightarrow 0} \\frac{\\Delta y}{\\Delta x}=\\lim \_{\\Delta x \\rightarrow 0} \\frac{f\\left(x\_{0}+\\Delta x\\right)-f\\left(x\_{0}\\right)}{\\Delta x}-
\]这里面lim就是极限的意思。另外函数y关于x的导数也可以记为\(\\frac{\\partial y}{\\partial x}\)。
偏导数
细心的小伙伴看到这里可能就会有疑问了有的函数不止一个变量呀比如z=3x+2y这个函数中就同时存在了x和y两种变量那该怎么求它们的导数呢
别着急,这时我们就要让偏导数登场了。偏导数其实就是保持一个变量变化,而所有其他变量恒定不变的求导过程。
还是刚才的原理假设有个函数z=f(x,y)当我们要求x方向的导数的时候就可以给x一个非常小的增量Δx同时保持y不变。反之如果要求y方向的导数则需要给y一个非常小的增量Δy而x保持不变。于是就能得出如下的偏导数描述公式
\[-
\\frac{\\partial}{\\partial x\_{j}} f\\left(x\_{0}, x\_{1}, \\ldots, x\_{n}\\right)=\\lim \_{\\Delta x \\rightarrow 0} \\frac{\\Delta y}{\\Delta x}=\\lim \_{\\Delta x \\rightarrow 0} \\frac{f\\left(x\_{0}, \\ldots, x\_{j}+\\Delta x, \\ldots, x\_{n}\\right)-f\\left(x\_{0}, \\ldots, x\_{j}, \\ldots, x\_{n}\\right)}{\\Delta x}-
\]上面的公式,看上去很复杂,其实仔细看,你就会发现只有\(x\_{j}\)这个变量有一个小小的Δx也就是说在x的某一个维度(j)增加了一个小的增量。
我们举个具体的例子来加深理解。比如对于函数\(z=x^{2}+y^{2}\)\(\\frac{\\partial z}{\\partial x}=2 x\)表示函数z在x上的导数\(\\frac{\\partial z}{\\partial y}=2 y\)表示函数z在y上的导数。
梯度
当我们了解了导数和偏导数的概念之后,那么梯度的概念就会非常容易理解了。函数所有偏导数构成的向量就叫做梯度。是不是非常简单呢?
我们一般使用\(\\nabla f\)来表述函数的梯度。它的描述公式为:
\[-
\\nabla f(x)=\\left\[\\frac{\\partial f}{\\partial x\_{1}}, \\frac{\\partial f}{\\partial x\_{2}}, \\ldots, \\frac{\\partial f}{\\partial x\_{i}}\\right\]-
\]关于梯度,后面这个结论你一定要牢记:梯度向量的方向即为函数值增长最快的方向。
这是一个非常重要的结论,它贯穿了整个深度学习的全过程。模型要学习知识,就要用最快最好的方式来完成,其实就是需要借助梯度来进行。不过,这个结论涉及的证明过程以及数学知识点非常多,这里你只需要记住结论就够了。
链式法则
深度学习的整个学习过程其实就是一个更新网络节点之前权重的过程。这个权重就是刚才咱们在前馈网络中示意图中看到的节点之间的连线权重我们一般使用w来进行表示。
回忆一下上节课我们提到的损失函数,模型就是通过不断地减小损失函数值的方式来进行学习的。让损失函数最小化,通常就要采用梯度下降的方式,即:每一次给模型的权重进行更新的时候,都要按照梯度的反方向进行。
为什么呢?因为梯度向量的方向即为函数值增长最快的方向,反方向则是减小最快的方向。
上面这个自然段的内容非常非常核心,为了确保你学会,我们换个方式再说一次:模型通过梯度下降的方式,在梯度方向的反方向上不断减小损失函数值,从而进行学习。
好,我们具体来看一个公式加深理解,假设我们把损失函数表示为:-
$\(-
H\\left(W\_{11}, W\_{12}, \\cdots, W\_{i j}, \\cdots, W\_{m n}\\right)-
\)$
其中Wij表示第i层的第j个节点对应的权重值。则其梯度向量▽H为
\[-
\\left\[\\frac{\\partial H}{\\partial w\_{11}}, \\quad \\frac{\\partial H}{\\partial w\_{12}}, \\ldots, \\quad \\frac{\\partial H}{\\partial w\_{i j}}, \\ldots, \\quad \\frac{\\partial H}{\\partial w\_{m n}}\\right\]-
\]看到这里你发现了什么问题感觉这个公式好复杂啊令人头秃。就比如第一项w11跟H的关系我哪知道呀中间隔了那么多层。
这时候,就需要链式法则隆重登场了:“两个函数组合起来的复合函数,导数等于里面函数代入外函数值的导数,乘以里面函数之导数。”这个法则包括了两种形式:
\[-
\\text { 1. } \\frac{\\mathrm{d} y}{\\mathrm{~d} x}=f^{\\prime}(g(x)) g^{\\prime}(x)-
\]-
$\(-
\\text { 2. } \\frac{\\mathrm{d} y}{\\mathrm{~d} x}=\\frac{\\mathrm{d} y}{\\mathrm{~d} u} \\cdot \\frac{\\mathrm{d} u}{\\mathrm{~d} x}-
\)$
可能这时候的你仍旧还很懵,不过没关系,我们通过一个更具体的例子再解释一下,你就知道该如何去计算了。
假设我们手中有函数\(f(x)=\\cos \\left(x^{2}-1\\right)\)。我们可以把函数分解为:
\[-
\\text { 1. } f(x)=\\cos (x)-
\]\[-
\\text { 2. } g(x)=x^{2}-1-
\]\(\\mathrm{g}(\\mathrm{x})\)的导数\(g^{\\prime}(x)=2 x\)\(\\mathrm{f}(\\mathrm{x})\)的导数\(f^{\\prime}(x)=-\\sin (x)\),则\(f^{\\prime}(x)=f^{\\prime}(g(x)) g^{\\prime}(x)=-\\sin \\left(x^{\\wedge} 2-1\\right) 2 x\),相当于各自求导后再相乘。
说到这,你是不是有点感觉了?这个部分需要你结合公式和我提供的例子仔细看一看,相信你一定可以搞定它。
反向传播
了解了前面的导数、偏导数、梯度、链式法则,反向传播必备的前置知识我们就搞定了。接下来正式进入反向传播的学习,你会发现前面咱们花的这些功夫都没有白费。
反向传播算法Backpropagation是目前训练神经网络最常用且最有效的算法。模型就是通过反向传播的方式来不断更新自身的参数从而实现了“学习”知识的过程。
反向传播的主要原理是:
前向传播:数据从输入层经过隐藏层最后输出,其过程和之前讲过的前馈网络基本一致。
计算误差并传播:计算模型输出结果和真实结果之间的误差,并将这种误差通过某种方式反向传播,即从输出层向隐藏层传递并最后到达输入层。
迭代:在反向传播的过程中,根据误差不断地调整模型的参数值,并不断地迭代前面两个步骤,直到达到模型结束训练的条件。
其中最重要的环节有两个:一是通过某种方式反向传播;二是根据误差不断地调整模型的参数值。
这两个环节,我们统称为优化方法,一般而言,多采用梯度下降的方法。这里就要使用到导数、梯度和链式法则相关的知识点,梯度下降我们将在下节课详细展开。
反向传播的数学推导以及证明过程是非常复杂的在实际的研发过程中反向传播的过程已经被PyTorch、TensorFlow等深度学习框架进行了完善的封装所以我们不需要手动去写这个过程。不过作为深度学习的研发人员你还是需要深入了解这个过程的运转方式这样才能搞清楚深度学习中模型具体是如何学习的。
小结
这节课我们一块学习了前馈网络这种最简单的神经网络。
虽然前馈网络很简单,但是它的思想贯穿了整个深度学习的过程,是非常重要的概念。同时我们又学习了导数、梯度和链式法则,这几个内容是模型做反向传播从而学习知识的最重要知识点,也是深度学习的内在核心内容,你一定要牢牢掌握。
最后,我们初步了解了反向传播的大致过程和概念,这为我们后面正式学习如何计算反向传播奠定了基础。
今天的内容里,我尽可能将相关数学知识点进行了简化,保留了最核心的内容。但实际上在深度学习的研究中,涉及的数学知识点非常多,如果感兴趣,你可以在课后查阅更多的相关资料,不断进步。
下节课,我会带你学习优化函数,学会了优化函数之后,我们就可以正式开始计算反向传播的过程了。
每课一练
深度学习都是基于反向传播的么?
欢迎你在留言区跟我交流互动,也推荐你把今天的内容分享给更多同事、朋友。
我是方远,我们下节课见!

View File

@ -0,0 +1,175 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 优化方法:更新模型参数的方法
你好,我是方远。
在上节课中,我们共同了解了前馈网络、导数、梯度、反向传播等概念。但是距离真正完全了解神经网络的学习过程,我们还差一个重要的环节,那就是优化方法。只有搞懂了优化方法,才能做到真的明白反向传播的具体过程。
今天我们就来学习一下优化方法,为了让你建立更深入的理解,后面我还特意为你准备了一个例子,把这三节课的所有内容串联起来。
用下山路线规划理解优化方法
深度学习,其实包括了三个最重要的核心过程:模型表示、方法评估、优化方法。我们上节课学习的内容,都是为了优化方法做铺垫。
优化方法指的是一个过程这个过程的目的就是寻找模型在所有可能性中达到评估效果指标最好的那一个。我们举个例子对于函数f(x),它包含了一组参数。
这个例子中优化方法的目的就是找到能够使得f(x)的值达到最小值对应的权重。换句话说,优化过程就是找到一个状态,这个状态能够让模型的损失函数最小,而这个状态就是模型的权重。
常见的优化方法种类非常多常见的有梯度下降法、牛顿法、拟牛顿法等涉及的数学知识也更是不可胜数。同样的PyTorch也将优化方法进行了封装我们在实际开发中直接使用即可节省了大量的时间和劳动。
不过,为了更好地理解深度学习特别是反向传播的过程,我们还是有必要对一些重要的优化方法进行了解。我们这节课要学习的梯度下降法,也是深度学习中使用最为广泛的优化方法。
梯度下降其实很好理解,我给你举一个生活化的例子。假期你跟朋友去爬山,到了山顶之后忽然想上厕所,需要尽快到达半山腰的卫生间,这时候你就需要规划路线,该怎么规划呢?
在不考虑生命危险的情况下,那自然是怎么快怎么走了,能跳崖我们绝不走平路,也就是说:越陡峭的地方,就越有可能快速到达目的地。
所以,我们就有了一个送命方案:每走几步,就改变方向,这个方向就是朝着当前最陡峭的方向,即坡度下降最快的方向行走,并不断重复这个过程。这就是梯度下降的最直观的表示了。
在上节课中我们曾说过:梯度向量的方向即为函数值增长最快的方向,梯度的反方向则是函数减小最快的方向。
梯度下降,就是梯度在深度学习中最重要的用途了。下面我们用相对严谨的方式来表述梯度下降。
在一个多维空间中,对于任何一个曲面,我们都能够找到一个跟它相切的超平面。这个超平面上会有无数个方向(想想这是为什么?),但是这所有的方向中,肯定有一个方向是能够使函数下降最快的方向,这个方向就是梯度的反方向。每次优化的目标就是沿着这个最快下降的方向进行,就叫做梯度下降。
具体来说,在一个三维空间曲线中,任何一点我们都能找到一个与之相切的平面(更高维则是超平面),这个平面上就会有无穷多个方向,但是只有一个使曲线函数下降最快的梯度。再次叨叨一遍:每次优化就沿着梯度的反方向进行,就叫做梯度下降。使什么函数下降最快呢?答案就是损失函数。
这下你应该将几个知识点串联起来了吧:为了得到最小的损失函数,我们要用梯度下降的方法使其达到最小值。这两节课的最终目的,就是让你牢牢记住这句话。
我们继续回到刚才的例子。
图中红色的线路,是一个看上去还不错的上厕所的路线。但是我们发现,还有别的路线可选。不过,下山就算是不要命地跑,也得讲究方法。
就比如,步子大小很重要,太大的话你可能就按照上图中的黄色路线跑了,最后跑到了别的山谷中(函数的局部极小值而非整体最小值)或者在接近和远离卫生间的来回震荡过程中,结果可想而知。但是如果步伐太小了,则需要的时间就很久,可能你还没走到目的地,就坚持不住了(蓝色路线)。
在算法中这个步子的大小叫做学习率learning rate。因为步长的原因理论上我们是不可能精确地走到目的地的而是最后在最小值的某一个范围内不断地震荡也会存在一定的误差不过这个误差是我们可以接受的。
在实际的开发中,如果损失函数在一段时间内没有什么变化,我们就认为是到达了需要的“最低点”,就可以认为模型已经训练收敛了,从而结束训练。
常见的梯度下降方法
我们搞清楚了梯度下降的原理之后,下面具体来看几种最常用的梯度下降优化方法。
1.批量梯度下降法Batch Gradient DescentBGD
线性回归模型是我们最常用的函数模型之一。假设对于一个线性回归模型y是真实的数据分布函数\(h\_\\theta(x) = \\theta\_1x\_1 + \\theta\_2x\_2 + … + \\theta\_nx\_n\)是我们通过模型训练得到的函数其中θ是h的参数也是我们要求的权值。
损失函数J(θ)可以表述为如下公式:
\[-
\\operatorname{cost}=J(\\theta)=\\frac{1}{2 m} \\sum\_{i=1}^{m}\\left(h\_{\\theta}\\left(x^{i}\\right)-y^{i}\\right)^{2}-
\]在这里m表示样本数量。既然要想损失函数的值最小我们就要使用到梯度还记得我们反复说的“梯度向量的方向即为函数值增长最快的方向”么让损失函数以最快的速度减小就得用梯度的反方向。
首先我们对J(θ)中的θ求偏导数,这样就可以得到每个θ对应的梯度:
\[-
\\frac{\\partial J(\\theta)}{\\partial \\theta\_{j}}=-\\frac{1}{m} \\sum\_{i=1}^{m}\\left(h\_{\\theta}\\left(x^{i}\\right)-y^{i}\\right) x\_{j}^{i}-
\]-
得到了每个θ的梯度之后,我们就可以按照下降的方向去更新每个θ,即:
\[-
\\theta\_{j}^{\\prime}=\\theta\_{j}-\\alpha \\frac{1}{m} \\sum\_{i=1}^{m}\\left(h\_{\\theta}\\left(x^{i}\\right)-y^{i}\\right) x\_{j}^{i}-
\]其中α就是我们刚才提到的学习率。更新θ之后,我们就得到了一个更新之后的损失函数,它的值肯定就会更小,那么我们的模型就更加接近于真实的数据分布了。
在上面的公式中你注意到了m这个数了吗没错这个方法是当所有的数据都经过了计算之后再整体除以它即把所有样本的误差做平均。这里我想提醒你在实际的开发中往往有百万甚至千万数量级的样本那这个更新的量就很恐怖了。所以就需要另一个办法随机梯度下降法。
2.随机梯度下降Stochastic Gradient DescentSGD
随机梯度下降法的特点是,每计算一个样本之后就要更新一次参数,这样参数更新的频率就变高了。其公式如下:
\[-
\\theta\_{j}^{\\prime}=\\theta\_{j}-\\alpha\\left(h\_{\\theta}\\left(x^{i}\\right)-y^{i}\\right) x\_{j}^{i}-
\]想想看,每训练一条数据就更新一条参数,会有什么好处呢?对,有的时候,我们只需要训练集中的一部分数据,就可以实现接近于使用全部数据训练的效果,训练速度也大大提升。
然而鱼和熊掌不可兼得SGD虽然快也会存在一些问题。就比如训练数据中肯定会存在一些错误样本或者噪声数据那么在一次用到该数据的迭代中优化的方向肯定不是朝着最理想的方向前进的也就会导致训练效果比如准确率的下降。最极端的情况下就会导致模型无法得到全局最优而是陷入到局部最优。
世间安得两全法,有的时候舍弃一些东西,我们才能获得想要的。随机梯度下降方法选择了用损失很小的一部分精确度和增加一定数量的迭代次数为代价,换取了最终总体的优化效率的提高。
当然这个过程中增加的迭代次数,还是要远远小于样本的数量的。
那如果想尽可能折衷地去协调速度和效果,该怎么办呢?我们很自然就会想到,每次不用全部的数据,也不只用一条数据,而是用“一些”数据,这就是接下来我们要说的小批量梯度下降。
3.小批量梯度下降Mini-Batch Gradient Descent, MBGD
Mini-batch的方法是目前主流使用最多的一种方式它每次使用一个固定数量的数据进行优化。
这个固定数量我们称它为batch size。batch size较为常见的数量一般是2的n次方比如32、128、512等越小的batch size对应的更新速度就越快反之则越慢但是更新速度慢就不容易陷入局部最优。
其实具体的数值设成为多少也需要根据项目的不同特点采用经验或不断尝试的方法去进行设置比如图像任务batch size我们倾向于设置得稍微小一点NLP任务则可以适当的大一些。
基于随机梯度下降法人们又提出了包括momentum、nesterov momentum等方法这部分知识同学们有兴趣点击这里可以自行查阅。
一个简单的抽象例子
我们通过三节课第11到13节课分别学习了损失函数、反向传播和优化方法梯度下降的概念。这三个概念也是深度学习中最为重要的内容其核心意义在于能够让模型真正做到不断学习和完善自己的表现。
那么接下来我们将通过一个简单的抽样例子把三节课的内容汇总起来。需要注意的是下面的例子不是一个能够运行的例子而是旨在让我们更加明确一个最基本的PyTorch训练过程都需要哪些步骤你可以当这是一次军训。有了这个演示例子以后我们上战场也就是实现真正可用的例子也会事半功倍。
在一个模型中,我们要设置如下几个内容:
模型定义。
损失函数定义。
优化器定义。
通过下面的代码,我们来一块了解一下,上面三个内容在实际开发中应该怎么组合。当然,这个代码是一个抽象版本,目的是帮你快速领会思路。具体的代码填充,还是要根据实际项目来修改。
import LeNet #假定我们使用的模型叫做LeNet,首先导入模型的定义类
import torch.optim as optim #引入PyTorch自带的可选优化函数
...
net = LeNet() #声明一个LeNet的实例
criterion = nn.CrossEntropyLoss() #声明模型的损失函数,使用的是交叉熵损失函数
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# 声明优化函数我们使用的就是之前提到的SGD优化的参数就是LeNet内部的参数lr即为之前提到的学习率
#下面开始训练
for epoch in range(30): #设置要在全部数据上训练的次数
for i, data in enumerate(traindata):
#data就是我们获取的一个batch size大小的数据
inputs, labels = data #分别得到输入的数据及其对应的类别结果
# 首先要通过zero_grad()函数把梯度清零不然PyTorch每次计算梯度会累加不清零的话第二次算的梯度等于第一次加第二次的
optimizer.zero_grad()
# 获得模型的输出结果,也即是当前模型学到的效果
outputs = net(inputs)
# 获得输出结果和数据真正类别的损失函数
loss = criterion(outputs, labels)
# 算完loss之后进行反向梯度传播这个过程之后梯度会记录在变量中
loss.backward()
# 用计算的梯度去做优化
optimizer.step()
...
这个抽象框架是不是非常清晰我们先设置好模型、损失函数和优化函数。然后针对每一批batch数据求得输出结果接着计算损失函数值再把这个值进行反向传播并利用优化函数进行优化。-
别看这个过程非常简单,但它是深度学习最根本、最关键的过程了,也是我们通过三节课学习到的最核心内容了。
总结
这节课,我们学习了优化方法以及梯度下降法,并通过一个例子将损失函数、反向传播、梯度下降做了串联。至此,我们就能够在给定一个模型的情况下,训练属于我们自己的深度学习模型了,恭喜你耐心看完。
当你想不起来梯度下降原理的时候,不妨回顾一下我们下山路线规划的例子。我们的目标就是设置合理的学习率(步伐),尽可能接近咱们的目的地(达到较理想的拟合效果)。用严谨点的表达说,就是正文里咱们反复强调的:为了得到最小的损失函数,我们要用梯度下降的方法使其达到最小值。
这里我再带你回顾一下这节课的要点:
模型之所以使用梯度下降,其实是通过优化方法不断的去修正模型和真实数据的拟合差距。
常用的三种梯度方法包括批量、随机和小批量,一般来说我们更多采用小批量梯度下降。
最后我们通过一个抽象的框架,汇总了训练一个模型所需要的几个关键内容,如损失函数、优化函数等,这部分内容是深度学习最关键的过程,建议你重点关注。
每课一练
batch size越大越好吗
欢迎你在留言区记录你的疑问或收获,也推荐你把这节课分享给更多的同事、朋友。
我是方远,我们下节课见!

View File

@ -0,0 +1,504 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 构建网络:一站式实现模型搭建与训练
你好,我是方远。
前面我们花了不少时间,既学习了数据部分的知识,还研究了模型的优化方法、损失函数以及卷积计算。你可能感觉这些知识还有些零零散散,但其实我们不知不觉中,已经拿下了模型训练的必学内容。
今天这节课也是一个中期小练习是我们检验自己学习效果的好时机。我会带你使用PyTorch构建和训练一个自己的模型。
具体我是这么安排的首先讲解搭建网络必备的基础模块——nn.Module模块也就是如何自己构建一个网络并且训练它换句话说就是搞清楚VGG、Inception那些网络是怎么训练出来的。然后我们再看看如何借助Torchvision的模型作为预训练模型来训练我们自己的模型。
构建自己的模型
让我们直接切入主题使用PyTorch自己构建并训练一个线性回归模型来拟合出训练集中的走势分布。
我们先随机生成训练集X与对应的标签Y具体代码如下
import numpy as np
import random
from matplotlib import pyplot as plt
w = 2
b = 3
xlim = [-10, 10]
x_train = np.random.randint(low=xlim[0], high=xlim[1], size=30)
y_train = [w * x + b + random.randint(0,2) for x in x_train]
plt.plot(x_train, y_train, 'bo')
上述代码中生成的数据,整理成散点图以后,如下图所示:-
熟悉回归的同学应该知道,我们的回归模型为:\(y = wx+b\)。这里的x与y其实就对应上述代码中的x_train与y_train而w与b正是我们要学习的参数。
好,那么我们看看如何构建这个模型。我们还是先看代码,再具体讲解。
import torch
from torch import nn
class LinearModel(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.randn(1))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, input):
return (input * self.weight) + self.bias
通过上面这个线性回归模型的例子,我们可以引出构建网络时的重要几个知识点。
1.必须继承nn.Module类。
2.重写__init__()方法。通常来说要把有需要学习的参数的层放到构造函数中例如例子中的weight与bias还有我们之前学习的卷积层。我们在上述的__init__()中使用了nn.Parameter()它主要的作用就是作为nn.Module中可训练的参数使用。
3.forward()是必须重写的方法。看函数名也可以知道它是用来定义这个模型是如何计算输出的也就是前向传播。对应到我们的例子就是获得最终输出y=weight * x+bias的计算结果。对于一些不需要学习参数的层一般来说可以放在这里。例如BN层激活函数还有Dropout。
nn.Module模块
nn.Module是所有神经网络模块的基类。当我们自己要设计一个网络结构的时候就要继承该类。也就说其实Torchvison中的那些模型也都是通过继承nn.Module模块来构建网络模型的。
需要注意的是模块本身是callable的当调用它的时候就是执行forward函数也就是前向传播。
我们还是结合代码例子直观感受一下。请看下面的代码先创建一个LinearModel的实例model然后model(x)就相当于调用LinearModel中的forward方法。
model = LinearModel()
x = torch.tensor(3)
y = model(x)
在我们之前的课程里已经讲过,模型是通过前向传播与反向传播来计算梯度,然后更新参数的。我想学到这里,应该没有几个人会愿意去写反向传播和梯度更新之类的代码吧。
这个时候PyTorch的优点就体现出来了当你训练时PyTorch的求导机制会帮你自动完成这些令人头大的计算。
除了刚才讲过的内容关于初始化方法__init__你还需要关注的是必须调用父类的构造方法才可以也就是这行代码
super().__init__()
因为在nn.Module的__init__()中会初始化一些有序的字典与集合。这些集合用来存储一些模型训练过程的中间变量如果不初始化nn.Module中的这些参数的话模型就会报下面的错误。
AttributeError: cannot assign parameters before Module.__init__() call
模型的训练
我们的模型定义好之后还没有被训练。要想训练我们的模型就需要用到损失函数与优化方法这一部分前面课里如果你感觉陌生的话可以回顾1113节课已经学过了所以现在我们直接看代码就可以了。
这里选择的是MSE损失与SGD优化方法。
model = LinearModel()
# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)
y_train = torch.tensor(y_train, dtype=torch.float32)
for _ in range(1000):
input = torch.from_numpy(x_train)
output = model(input)
loss = nn.MSELoss()(output, y_train)
model.zero_grad()
loss.backward()
optimizer.step()
经过1000个Epoch的训练以后我们可以打印出模型的weight与bias看看是多少。
对于一个模型的可训练的参数我们可以通过named_parameters()来查看,请看下面代码。
for parameter in model.named_parameters():
print(parameter)
# 输出:
('weight', Parameter containing:
tensor([2.0071], requires_grad=True))
('bias', Parameter containing:
tensor([3.1690], requires_grad=True))
可以看到weight是2.0071bias是3.1690你再回头对一下我们创建训练数据的w与b它们是不是一样呢
我们刚才说过继承一个nn.Module之后可以定义自己的网络模型。Module同样可以作为另外一个Module的一部分被包含在网络中。比如我们要设计下面这样的一个网络
观察图片很容易就会发现在这个网络中有大量重复的结构。上图中的3x3与2x2的卷积组合按照我们开篇的讲解的话我们需要把每一层卷积都定义到__init__()然后再在forward中定义好执行方法就可以了例如下面的伪代码
class CustomModel(nn.Module):
def __init__(self):
super().__init__()
self.conv1_1 = nn.Conv2d(in_channels=1, out_channels=3, kernel_size=3, padding='same')
self.conv1_2 = nn.Conv2d(in_channels=3, out_channels=1, kernel_size=2, padding='same')
...
self.conv_m_1 = nn.Conv2d(in_channels=1, out_channels=3, kernel_size=3, padding='same')
self.conv_m_2 = nn.Conv2d(in_channels=3, out_channels=1, kernel_size=2, padding='same')
...
self.conv_n_1 = nn.Conv2d(in_channels=1, out_channels=3, kernel_size=3, padding='same')
self.conv_n_2 = nn.Conv2d(in_channels=3, out_channels=1, kernel_size=2, padding='same')
def forward(self, input):
x = self.conv1_1(input)
x = self.conv1_2(x)
...
x = self.conv_m_1(x)
x = self.conv_m_2(x)
...
x = self.conv_n_1(x)
x = self.conv_n_2(x)
...
return x
其实这部分重复的结构完全可以放在一个单独的module中然后在我们模型中直接调用这部分即可具体实现你可以参考下面的代码
class CustomLayer(nn.Module):
def __init__(self, input_channels, output_channels):
super().__init__()
self.conv1_1 = nn.Conv2d(in_channels=input_channels, out_channels=3, kernel_size=3, padding='same')
self.conv1_2 = nn.Conv2d(in_channels=3, out_channels=output_channels, kernel_size=2, padding='same')
def forward(self, input):
x = self.conv1_1(input)
x = self.conv1_2(x)
return x
然后呢CustomModel就变成下面这样了
class CustomModel(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = CustomLayer(11)
...
self.layerm = CustomLayer(11)
...
self.layern = CustomLayer(11)
def forward(self, input):
x = self.layer1(input)
...
x = self.layerm(x)
...
x = self.layern(x)
...
return x
熟悉深度学习的同学一定听过残差块、Inception块这样的多层的一个组合。你没听过也没关系在图像分类中我还会讲到。这里你只需要知道这种多层组合的结构是类似的对于这种组合我们就可以用上面的代码的方式实现。
模型保存与加载
我们训练好的模型最终的目的,就是要为其他应用提供服务的,这就涉及到了模型的保存与加载。
模型保存与加载的话有两种方式。PyTorch模型的后缀名一般是pt或pth这都没有关系只是一个后缀名而已。我们接着上面的回归模型继续讲模型的保存与加载。
方式一:只保存训练好的参数
第一种方式就是只保存训练好的参数。然后加载模型的时候,你需要通过代码加载网络结构,然后再将参数赋予网络。
只保存参数的代码如下所示:
torch.save(model.state_dict(), './linear_model.pth')
第一个参数是模型的state_dict而第二个参数要保存的位置。
代码中的state_dict是一个字典在模型被定义之后会自动生成存储的是模型可训练的参数。我们可以打印出线性回归模型的state_dict如下所示
model.state_dict()
输出OrderedDict([('weight', tensor([[2.0071]])), ('bias', tensor([3.1690]))])
加载模型的方式如下所示:
# 先定义网络结构
linear_model = LinearModel()
# 加载保存的参数
linear_model.load_state_dict(torch.load('./linear_model.pth'))
linear_model.eval()
for parameter in linear_model.named_parameters():
print(parameter)
输出:
('weight', Parameter containing:
tensor([[2.0071]], requires_grad=True))
('bias', Parameter containing:
tensor([3.1690], requires_grad=True))
这里有个model.eval()需要你注意一下因为有些层例如Dropout与BN在训练时与评估时的状态是不一样的当进入评估时要执行model.eval(),模型才能进入评估状态。这里说的评估不光光指代评估模型,也包括模型上线时候时的状态。
方式二:保存网络结构与参数
相比第一种方式,这种方式在加载模型的时候,不需要加载网络结构了。具体代码如下所示:
# 保存整个模型
torch.save(model, './linear_model_with_arc.pth')
# 加载模型,不需要创建网络了
linear_model_2 = torch.load('./linear_model_with_arc.pth')
linear_model_2.eval()
for parameter in linear_model_2.named_parameters():
print(parameter)
# 输出:
('weight', Parameter containing:
tensor([[2.0071]], requires_grad=True))
('bias', Parameter containing:
tensor([3.1690], requires_grad=True))
这样操作以后,如果你成功输出了相应数值,而且跟之前保存的模型的参数一致,就说明加载对了。
使用Torchvison中的模型进行训练
我们前面说过Torchvision提供了一些封装好的网络结构我们可以直接拿过来使用。但是并没有细说如何使用它们在我们的数据集上进行训练。今天我们就来看看如何使用这些网络结构在我们自己的数据上训练我们自己的模型。
再说微调
其实Torchvision提供的模型最大的作用就是当作我们训练时的预训练模型用来加速我们模型收敛的速度这就是所谓的微调。
对于微调最关键的一步就是之前讲的调整最后全连接层输出的数目。Torchvision中只是对各大网络结构的复现而不是对它们进行了统一的封装所以在修改全连接层时不同的网络有不同的修改方法。
不过你也别担心这个修改并不复杂你只需要打印出网络结构就可以知道如何修改了。我们接下来以AlexNet为例带你尝试一下如何微调。
前面讲Torchvision的时候其实提到过一次微调那个时候说的是固定整个网络的参数只训练最后的全连接层。今天我再给你介绍另外一种微调的方式那就是修改全连接层之后整个网络都重新开始训练。只不过这时候要使用预训练模型的参数作为初始化的参数这种方式更为常用。
接下来我们就看看如何使用Torchvision中模型进行微调。
首先,导入模型。代码如下:
import torchvision.models as models
alexnet = models.alexnet(pretrained=True)
这一步如果你不能“科学上网”的话可能会比较慢。你可以先根据命令中提示的url手动下载然后使用今天讲的模型加载的方式加载预训练模型代码如下所示
import torchvision.models as models
alexnet = models.alexnet()
alexnet.load_state_dict(torch.load('./model/alexnet-owt-4df8aa71.pth'))
为了验证加载是否成功,我们让它对下图进行预测:-
代码如下:
from PIL import Image
import torchvision
import torchvision.transforms as transforms
im = Image.open('dog.jpg')
transform = transforms.Compose([
transforms.RandomResizedCrop((224,224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
input_tensor = transform(im).unsqueeze(0)
alexnet(input_tensor).argmax()
输出263
运行了前面的代码之后对应到ImageNet的类别标签中可以找到263对应的是Pembroke柯基狗这就证明模型已经加载成功了。-
这个过程中有两个重点你要留意。
首先因为Torchvision中所有图像分类的预训练模型它们都是在ImageNet上训练的。所以输入数据需要是3通道的数据也就是shape为(B, 3, H, W)的TensorB为batchsize。我们需要使用均值为[0.485, 0.456, 0.406],标准差为[0.229, 0.224, 0.225]对数据进行正规化。
另外从理论上说大部分的经典卷积神经最后采用全连接层也就是机器学习中的感知机进行分类这也导致了网络的输入尺寸是固定的。但是在Torchvision的模型可以接受任意尺寸的输入的。
这是因为Torchvision对模型做了优化有的网络是在最后的卷积层采用了全局平均或者采用的是全卷积网络。这两种方式都可以让网络接受在最小输入尺寸基础之上任意尺度的输入。这一点你现在可能认识得还不够清楚不过别担心以后我们学习完图像分类理论之后你会理解得更加透彻。
我们回到微调这个主题。正如刚才所说训练一个AlexNet需要的数据必须是三通道数据。所以在这里我使用了CIFAR-10公开数据集举例。
CIFAR-10数据集一共有60000张图片构成共10个类别每一类包含6000图片。每张图片为32x32的RGB图片。其中50000张图片作为训练集10000张图片作为测试集。
可以说CIFAR-10是非常接近真实项目数据的数据集了因为真实项目中的数据通常是RGB三通道数据而CIFAR-10同样是三通道数据。
我们用之前讲的make_grid方法将CIFAR-10的数据打印出来代码如下
cifar10_dataset = torchvision.datasets.CIFAR10(root='./data',
train=False,
transform=transforms.ToTensor(),
target_transform=None,
download=True)
# 取32张图片的tensor
tensor_dataloader = DataLoader(dataset=cifar10_dataset,
batch_size=32)
data_iter = iter(tensor_dataloader)
img_tensor, label_tensor = data_iter.next()
print(img_tensor.shape)
grid_tensor = torchvision.utils.make_grid(img_tensor, nrow=16, padding=2)
grid_img = transforms.ToPILImage()(grid_tensor)
display(grid_img)
请注意上述代码中的transform我为了打印图片只使用了transform.ToTensor()输出图片,结果如下所示:-
这里我特别说明一下因为这个训练集的数据都是32x32的所以你现在看到的就是原图效果图片大小并不影响咱们的学习。
下面我们要做的是修改全连接层直接print就可以打印出网络结构代码如下
print(alexnet)
输出:
AlexNet(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
(1): ReLU(inplace=True)
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU(inplace=True)
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU(inplace=True)
(8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU(inplace=True)
(10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(6, 6))
(classifier): Sequential(
(0): Dropout(p=0.5, inplace=False)
(1): Linear(in_features=9216, out_features=4096, bias=True)
(2): ReLU(inplace=True)
(3): Dropout(p=0.5, inplace=False)
(4): Linear(in_features=4096, out_features=4096, bias=True)
(5): ReLU(inplace=True)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
可以看到最后全连接层输入是4096个单元输出是1000个单元我们要把它修改为输出是10个单元的全连接层CIFR10有10类。代码如下
# 提取分类层的输入参数
fc_in_features = alexnet.classifier[6].in_features
# 修改预训练模型的输出分类数
alexnet.classifier[6] = torch.nn.Linear(fc_in_features, 10)
print(alexnet)
输出:
AlexNet(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
(1): ReLU(inplace=True)
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU(inplace=True)
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU(inplace=True)
(8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU(inplace=True)
(10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(6, 6))
(classifier): Sequential(
(0): Dropout(p=0.5, inplace=False)
(1): Linear(in_features=9216, out_features=4096, bias=True)
(2): ReLU(inplace=True)
(3): Dropout(p=0.5, inplace=False)
(4): Linear(in_features=4096, out_features=4096, bias=True)
(5): ReLU(inplace=True)
(6): Linear(in_features=4096, out_features=10, bias=True)
)
)
这时你可以发现输出就变为10个单元了。
接下来就是在CIFAR-10上使用AlexNet作为预训练模型训练我们自己的模型了。首先是数据读入代码如下
transform = transforms.Compose([
transforms.RandomResizedCrop((224,224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
cifar10_dataset = torchvision.datasets.CIFAR10(root='./data',
train=False,
transform=transform,
target_transform=None,
download=True)
dataloader = DataLoader(dataset=cifar10_dataset, # 传入的数据集, 必须参数
batch_size=32, # 输出的batch大小
shuffle=True, # 数据是否打乱
num_workers=2) # 进程数, 0表示只有主进程
这里需要注意的是我更改了transform并且将图片resize到224x224大小。这个尺寸是Torchvision中推荐的一个最小训练尺寸。模型就是我们修改后的AlexNet之后的训练跟我们之前讲的是一样的。-
先定义优化器,代码如下:
optimizer = torch.optim.SGD(alexnet.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)
然后开始模型训练,是不是感觉后面的代码很眼熟,没错,它跟我们之前讲的一样:
# 训练3个Epoch
for epoch in range(3):
for item in dataloader:
output = alexnet(item[0])
target = item[1]
# 使用交叉熵损失函数
loss = nn.CrossEntropyLoss()(output, target)
print('Epoch {}, Loss {}'.format(epoch + 1 , loss))
#以下代码的含义,我们在之前的文章中已经介绍过了
alexnet.zero_grad()
loss.backward()
optimizer.step()
这里用到的微调方式,就是所有参数都需要进行重新训练。
而第一种方式(固定整个网络的参数,只训练最后的全连接层),只需要在读取完预训练模型之后,将全连接层之前的参数全部锁死即可,也就是让他们无法训练,我们模型训练时,只训练全连接层就行了,其余一切都不变。代码如下所示:
alexnet = models.alexnet()
alexnet.load_state_dict(torch.load('./model/alexnet-owt-4df8aa71.pth'))
for param in alexnet.parameters():
param.requires_grad = False
说到这里,我们的模型微调就讲完了,你可以自己动手试试看。
总结
今天的内容主要是围绕如何自己搭建一个网络模型我们介绍了nn.Module模块以及围绕它的一些方法。
根据这讲我分享给你的思路,之后如果你有什么想法时,就可以快速搭建一个模型进行训练和验证。
其实实际的开发中我们很少会自己去构建一个网络绝大多数都是直接使用前人已经构建好的一些经典网络例如Torchvision中那些模型。当你去看一些还没有被封装到PyTorch的模型的时候今天所学的内容就能够帮你直接借鉴前人的工作结果训练属于自己的模型。
最后我再结合自己的学习研究经验给有兴趣了解更多深度学习知识的同学提供一些学习线索。目前我们只讲了卷积层对于一个网络还有很多其余层比如Dropout、Pooling层、BN层、激活函数等。Dropout函数、Pooling层、激活函数相对比较好理解BN层可能稍微复杂一些。
另外细心的小伙伴应该发现了我们在打印AlexNet网络结构中的时候它的一部分是使用nn.Sequential构建的。nn.Sequential是一种快速构建网络的方式有了这节课的知识作储备弄懂这个方式你会觉得非常简单也推荐你去看看。
每课一练
请你自己构建一个卷积神经网络基于CIFAR-10训练一个图像分类模型。因为还没有学习图像分类原理所以我先帮你写好了网络的结构需要你补全数据读取、损失函数(交叉熵损失)与优化方法SGD等部分。
class MyCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=3)
# conv1输出的特征图为222x222大小
self.fc = nn.Linear(16 * 222 * 222, 10)
def forward(self, input):
x = self.conv1(input)
# 进去全连接层之前,先将特征图铺平
x = x.view(x.shape[0], -1)
x = self.fc(x)
return x
欢迎你在留言区和我交流讨论。如果这节课对你有帮助,也推荐你顺手分享给更多的同事、朋友,跟他一起学习进步。

View File

@ -0,0 +1,313 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 可视化工具:如何实现训练的可视化监控?
你好我是方远。欢迎来到第15节课的学习。
上节课中,我们以线性回归模型为例,学习了模型从搭建到训练的全部过程。在深度学习领域,模型训练是一个必须的环节,而在训练过程中,我们常常需要对模型的参数、评价指标等信息进行可视化监控。
今天我们主要会学习两种可视化工具,并利用它们实现训练过程的可视化监控。
在TensorFlow中最常使用的可视化工具非Tensorboard莫属而TensorboardX工具使得PyTorch也享受到Tensorboard的便捷功能。另外FaceBook也为PyTorch开发了一款交互式可视化工具Visdom它可以对实时数据进行丰富的可视化帮助我们实时监控实验过程。
让我们先从TensorboardX说起。
TensorboardX
Tensorboard是TensorFlow的一个附加工具用于记录训练过程的模型的参数、评价指标与图像等细节内容并通过Web页面提供查看细节与过程的功能用浏览器可视化的形式展现帮助我们在实验时观察神经网络的训练过程把握训练趋势。
既然Tensorboard工具这么方便TensorFlow外的其它深度学习框架自然也想获取Tensorboard的便捷功能于是TensorboardX应运而生。
安装
安装 Tensorboard很容易我们可以使用pip进行安装命令如下
pip install tensorboard
如果你已经安装过TensorFlow那么就无需额外安装Tensorboard了。
接下来,我们需要安装 TensorboardX。这里需要注意的是PyTorch 1.8之后的版本自带TensorboardX它被放在torch.utils.tensorboard中因此无需多余配置。
如果你用的是PyTorch 1.8之前的版本TensorboardX安装起来也非常简单。我们依然使用pip命令安装
pip install tensorboardX
使用与启动
为了使用TensorboardX我们首先需要创建一个SummaryWriter的实例然后再使用add_scalar方法或add_image方法将数字或图片记录到SummaryWriter实例中。
SummaryWriter类的定义如下
torch.utils.tensorboard.writer.SummaryWriter(log_dir=None)
其中的log_dir表示保存日志的路径默认会保存在“runs/当前时间_主机名”文件夹中。
实例创建好之后我们来看add_scalar方法这个方法用来记录数字常量它的定义如下
add_scalar(tag, scalar_value, global_step=None, walltime=None)
根据定义,我们依次说说其中的参数:
tag字符串类型表示数据的名称不同名称的数据会使用不同曲线展示
scalar_value浮点型表示要保存的数值
global_step整型表示训练的step数
walltime浮点型表示记录发生的时间默认为time.time()。
我们一般会使用add_scalar方法来记录训练过程的loss、accuracy、learning rate等数值的变化这样就能直观地监控训练过程。
add_image方法用来记录单个图像数据需要Pillow库的支持它的定义如下
add_image(tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')
tag、global_step和walltime的含义跟add_scalar方法里一样所以不再赘述我们看看其他新增的参数都是什么含义。
img_tensorPyTorch的Tensor类型或NumPy的array类型表示图像数据
dataformats字符串类型表示图像数据的格式默认为“CHW”即Channel x Height x Width还可以是“CHW”、“HWC”或“HW”等。
我们来看一个例子加深理解,具体代码如下。
from torch.utils.tensorboard import SummaryWriter
# PyTorch 1.8之前的版本请使用:
# from tensorboardX import SummaryWriter
import numpy as np
# 创建一个SummaryWriter的实例
writer = SummaryWriter()
for n_iter in range(100):
writer.add_scalar('Loss/train', np.random.random(), n_iter)
writer.add_scalar('Loss/test', np.random.random(), n_iter)
writer.add_scalar('Accuracy/train', np.random.random(), n_iter)
writer.add_scalar('Accuracy/test', np.random.random(), n_iter)
img = np.zeros((3, 100, 100))
img[0] = np.arange(0, 10000).reshape(100, 100) / 10000
img[1] = 1 - np.arange(0, 10000).reshape(100, 100) / 10000
writer.add_image('my_image', img, 0)
writer.close()
我给你梳理一下这段代码都做了什么。
首先创建一个SummaryWriter的实例这里注意PyTorch 1.8之前的版本请使用“from tensorboardX import SummaryWriter”PyTorch 1.8之后的版本请使用“from torch.utils.tensorboard import SummaryWriter”。
然后我们随机生成一些随机数用来模拟训练与预测过程中的Loss和Accuracy并且用add_scalar方法进行记录。最后生成了一个图像用add_image方法来记录。
上述代码运行后会在当前目录下生成一个“runs”文件夹里面存储了我们需要记录的数据。
然后我们在当前目录下执行下面的命令即可启动Tensoboard。
tensorboard --logdir=runs
启动后在浏览器中输入“http://127.0.0.1:6006/”Tensorboard的默认端口为6006即可对刚才我们记录的数据进行可视化。
Tensorboard的界面如下图所示。图片中右侧部分就是刚刚用add_scalar方法记录的Loss和Accuracy。你看Tensorboard已经帮我们按照迭代step绘制成了曲线图可以非常直观地监控训练过程。
在“IMAGES”的标签页中可以显示刚刚用add_image方法记录的图像数据如下图所示。
训练过程可视化
进行到这里我们已经装好了TensorboardX并启动还演示了这个工具如何使用。
那么如何在我们实际的训练过程中来进行可视化监控呢?我们用上节课构建并训练的线性回归模型为例,来进行实践。
下面的代码上节课讲过作用是定义一个线性回归模型并随机生成训练集X与对应的标签Y。
import random
import numpy as np
import torch
from torch import nn
# 模型定义
class LinearModel(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.randn(1))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, input):
return (input * self.weight) + self.bias
# 数据
w = 2
b = 3
xlim = [-10, 10]
x_train = np.random.randint(low=xlim[0], high=xlim[1], size=30)
y_train = [w * x + b + random.randint(0,2) for x in x_train]
然后我们在训练的过程中加入刚才讲过的SummaryWriter实例与add_scalar方法具体的代码如下。
# Tensorboard
from torch.utils.tensorboard import SummaryWriter
# 训练
model = LinearModel()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)
y_train = torch.tensor(y_train, dtype=torch.float32)
writer = SummaryWriter()
for n_iter in range(500):
input = torch.from_numpy(x_train)
output = model(input)
loss = nn.MSELoss()(output, y_train)
model.zero_grad()
loss.backward()
optimizer.step()
writer.add_scalar('Loss/train', loss, n_iter)
通过上面这段代码我们记录了训练过程中的Loss的变换过程。具体的趋势如下图所示。
可以看到Loss是一个下降的趋势说明随着训练过程模型越来越拟合我们的训练数据了。进行到这里我们已经走完了利用TensorboardX工具实现训练可视化监控的整个过程。
TensorboardX除了包括上述的常用方法之外还有许多其他方法如add_histogram、add_graph、add_embedding、add_audio 等,感兴趣的同学可以参考[官方文档]。相信参考已经学习过的两个add方法你一定能够举一反三很快熟练调用其它的方法。
Visdom
Visdom是Facebook开源的一个专门用于PyTorch的交互式可视化工具。它为实时数据提供了丰富的可视化种类可以在浏览器中进行查看并且可以很容易地与其他人共享可视化结果帮助我们实时监控在远程服务器上进行的科学实验。
安装与启动
Visdom的安装非常简单可直接使用pip进行安装具体的命令如下
pip install visdom
执行安装命令后我们可以执行以下命令启动Visdom
python -m visdom.server
Visdom的默认端口是8097如果需要修改可以使用-p选项。
启动成功后在浏览器中输入“http://127.0.0.1:8097/”进入Visdom的主界面。
Visdom的主界面如下图所示。
请你注意Visdom的使用与Tensorboard稍有不同。Tensorboard是在生成记录文件后启动可视化界面。而Visdom是先启动可视化界面当有数据进入Visdom的窗口时会实时动态地更新并绘制数据。
快速上手
下面我们就来动手试一下看看Visdom如何绘制数据。
具体过程分四步走首先我们需要将窗口类Visdom实例化然后利用line()方法创建一个线图窗口并初始化接着利用生成的一组随机数数据来更新线图窗口。最后通过image()方法来绘制一张图片。
上述过程的具体代码如下。
from visdom import Visdom
import numpy as np
import time
# 将窗口类实例化
viz = Visdom()
# 创建窗口并初始化
viz.line([0.], [0], win='train_loss', opts=dict(title='train_loss'))
for n_iter in range(10):
# 随机获取loss值
loss = 0.2 * np.random.randn() + 1
# 更新窗口图像
viz.line([loss], [n_iter], win='train_loss', update='append')
time.sleep(0.5)
img = np.zeros((3, 100, 100))
img[0] = np.arange(0, 10000).reshape(100, 100) / 10000
img[1] = 1 - np.arange(0, 10000).reshape(100, 100) / 10000
# 可视化图像
viz.image(img)
可以看出使用过程与Tensorboard基本一致只是函数调用上的不同。-
绘制线图的结果如下图所示。
对应的绘制图片结果如下。可以看出Visodm绘制数据时是动态更新的。
训练可视化监控
同样地,我们学习可视化工具的使用主要是为了监控我们的训练过程。我们还是以构建并训练的线性回归模型为例,来进行实践。
Visdom监控训练过程大致分为三步
实例化一个窗口;
初始化窗口的信息;
更新监听的信息。
定义模型与生成训练数据的过程跟前面一样我就不再重复了。在训练过程中实例化并初始化Visdom窗口、实时记录Loss的代码如下。
# Visdom
from visdom import Visdom
import numpy as np
# 训练
model = LinearModel()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)
y_train = torch.tensor(y_train, dtype=torch.float32)
# 实例化一个窗口
viz = Visdom(port=8097)
# 初始化窗口的信息
viz.line([0.], [0.], win='train_loss', opts=dict(title='train loss'))
for n_iter in range(500):
input = torch.from_numpy(x_train)
output = model(input)
loss = nn.MSELoss()(output, y_train)
model.zero_grad()
loss.backward()
optimizer.step()
# 更新监听的信息
viz.line([loss.item()], [n_iter], win='train_loss', update='append')
在Visdom的界面中我们可以看到Loss的变化趋势如下图所示。Visdom不会像Tensorboard自动对曲线进行缩放或平滑因此可以看到50轮之后由于Loss值变化范围比较小图像的抖动趋势被压缩得非常不明显。
小结
这节课我带你学习了两种可视化工具TensorboardX和Visdom。
相信通过一节课的讲解和练习,这两种可视化工具如何安装、启动,还有如何用它们绘制线图和图片这些基本的操作,相信你都已经掌握了。
学习使用可视化工具的主要目的,是为了帮助我们在深度学习模型的训练过程中,实时监控一些数据,例如损失值、评价指标等等。对这些数据进行可视化监控,可以帮助我们感知各个参数与指标的变化,实时把握训练趋势。因此,如何将可视化工具应用于模型训练过程中,是我们学习的重点。
TensorboardX和Visdom还有其它诸如绘制散点图、柱状图、热力图等等多种多样的功能如果你感兴趣可以参考官方文档类比我们今天学习的方法动手试一试经过练习一定可以熟练使用它们。
每课一练
参考Visdom快速上手中的例子现在需要生成两组随机数分别表示Loss和Accuracy。在迭代的过程中如何用代码同时绘制出Loss和Accuracy两组数据呢
欢迎记录你的思考或疑惑,也推荐你把今天学到的可视化工具分享给更多同事、朋友。

View File

@ -0,0 +1,343 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 分布式训练:如何加速你的模型训练?
你好,我是方远。
在之前的课程里,我们一起学习了深度学习必备的内容,包括构建网络、损失函数、优化方法等,这些环节掌握好了,我们就可以训练很多场景下的模型了。
但是有的时候,我们的模型比较大,或者训练数据比较多,训练起来就会比较慢,该怎么办呢?这时候牛气闪闪的分布式训练登场了,有了它,我们就可以极大地加速我们的训练过程。
这节课我就带你入门分布式训练,让你吃透分布式训练的工作原理,最后我还会结合一个实战项目,带你小试牛刀,让你在动手过程中加深对这部分内容的理解。
分布式训练原理
在具体介绍分布式训练之前我们需要先简要了解一下为什么深度学习要使用GPU。
在我们平时使用计算机的时候程序都是将进程或者线程的数据资源放在内存中然后在CPU进行计算。通常的程序中涉及到了大量的if else等分支逻辑操作这也是CPU所擅长的计算方式。
而在深度学习中模型的训练与计算过程则没有太多的分支基本上都是矩阵或者向量的计算而这种暴力又单纯的计算形式非常适合用GPU处理GPU 的整个处理过程就是一个流式处理的过程。
但是再好的车子一个缸的发动机也肯定比不过12个缸的同理单单靠一个GPU速度肯定还是不够快于是就有了多个GPU协同工作的办法即分布式训练。分布式训练顾名思义就是训练的过程是分布式的重点就在于后面这两个问题
1.谁分布了?答案有两个:数据与模型。-
2.怎么分布?答案也有两个:单机多卡与多机多卡。
也就是说为了实现深度学习的分布式训练我们需要采用单机多卡或者多机多卡的方式让分布在不同GPU上的数据和模型协同训练。那么接下来我们先从简单的单机单卡入手了解一下GPU的训练过程。
单机单卡
想象一下如果让你把数据或者模型推送到GPU上需要做哪几步操作呢让我们先从单GPU的情况出发。
第一步我们需要知道手头有多少GPU。PyTorch中使用torch.cuda.is_available()函数来判断当前的机器是否有可用的GPU而函数torch.cuda.device_count()则可以得到目前可用的GPU的数量。
第二步获得GPU的一个实例。例如下面的语句
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
这里torch.device代表将torch.Tensor分配到的设备是一个设备对象实例也就是GPU。其中cuda: 0表示我们使用的是第一块GPU。当然你也可以不用声明“:0”默认就从第一块开始。如果没有GPUtorch.cuda.is_available()那就只能使用CPU了。
第三步将数据或者模型推到GPU上去这个过程我们称为迁移。
在PyTorch中这个过程的封装程度非常高换句话说我们只需要保证即将被推到GPU的内容是张量Tensor或者模型Module就可以用to()函数快速进行实现。例如:
data = torch.ones((3, 3))
print(data.device)
# Get: cpu
# 获得device
device = torch.device("cuda: 0")
# 将data推到gpu上
data_gpu = data.to(device)
print(data_gpu.device)
# Get: cuda:0
在上面这段代码中我们首先创建了一个常规的张量data通过device属性可以看到data现在是在CPU上的。随后我们通过to()函数将data迁移到GPU上同样也能通过device属性看到data确实已经存在于GPU上了。
那么对于模型,是否也是一样的操作呢?答案是肯定的,我们接下来看一个例子:
net = nn.Sequential(nn.Linear(3, 3))
net.to(device)
这里仍旧使用to()函数即可。
单机单卡的模式,相当于有一批要处理加工的产品,只分给了一个工人和一台机器来完成,这种情况下数量少了还可以,但是一旦产品太多了,就得加人、加机器才能快速交工了。
深度学习也是一样,在很多场景中,比如推荐算法模型、语言模型等,往往都有着百万、千万甚至上亿的训练数据,这样如果只用一张卡的话肯定是搞不定了。于是就有了单机多卡和多机多卡的解决方案。
单机多卡
那么在PyTorch中单机多卡的训练是如何进行的呢其实PyTorch提供了好几种解决方案咱们先看一个最简单也是最常用的办法nn.DataParallel()。其具体定义如下:
torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
在这里module就是你定义的模型device_ids即为训练模型时用到的GPU设备号output_device表示输出结果的device默认为0也就是第一块卡。
我们可以使用nvidia-smi命令查看GPU使用情况。如果你足够细心就会发现使用多个卡做训练的时候output_device的卡所占的显存明显大一些。
继续观察你还会发现使用DataParallel时数据的使用是并行的每张卡获得的数据都一样多但是输出的loss则是所有的卡的loss都会在第output_device块GPU进行计算这导致了output_device卡的负载进一步增加。
就这么简单就这么简单只需要一个DataParallel函数就可以将模型分发到多个GPU上。但是我们还是需要了解这内部的运行逻辑因为只有了解了这个逻辑在以后的开发中遇到了诸如时间计算、资源预估、优化调试问题的时候你才可以更好地运用GPU让多GPU的优势真正发挥出来。
在模型的前向计算过程中数据会被划分为多个块被推送到不同的GPU进行计算。但是不同的是模型在每个GPU中都会复制一份。我们看一下后面的代码
class ASimpleNet(nn.Module):
def __init__(self, layers=3):
super(ASimpleNet, self).__init__()
self.linears = nn.ModuleList([nn.Linear(3, 3, bias=False) for i in range(layers)])
def forward(self, x):
print("forward batchsize is: {}".format(x.size()[0]))
x = self.linears(x)
x = torch.relu(x)
return x
batch_size = 16
inputs = torch.randn(batch_size, 3)
labels = torch.randn(batch_size, 3)
inputs, labels = inputs.to(device), labels.to(device)
net = ASimpleNet()
net = nn.DataParallel(net)
net.to(device)
print("CUDA_VISIBLE_DEVICES :{}".format(os.environ["CUDA_VISIBLE_DEVICES"]))
for epoch in range(1):
outputs = net(inputs)
# Get:
# CUDA_VISIBLE_DEVICES : 3, 2, 1, 0
# forward batchsize is: 4
# forward batchsize is: 4
# forward batchsize is: 4
# forward batchsize is: 4
在上面的程序中我们通过CUDA_VISIBLE_DEVICES得知了当前程序可见的GPU数量为4而我们的batch size为16输出每个GPU上模型forward函数内部的print内容验证了每个GPU获得的数据量都是4个。这表示DataParallel 会自动帮我们将数据切分、加载到相应 GPU将模型复制到相应 GPU进行正向传播计算梯度并汇总。
多机多卡
多机多卡一般都是基于集群的方式进行大规模的训练,需要涉及非常多的方面,咱们这节课只讨论最基本的原理和方法。在具体实践中,你可能还会遇到其它网络或环境等问题,届时需要具体问题具体解决。
DP与DDP
刚才我们已经提到对于单机多卡训练有一个最简单的办法DataParallel。其实PyTorch的数据并行还有一个主要的API那就是DistributedDataParallel。而DistributedDataParallel也是我们实现多机多卡的关键API。
DataParallel简称为DP而DistributedDataParallel简称为DDP。我们来详细看看DP与DDP的区别。
先看DPDP是单进程控制多GPU。从之前的程序中我们也可以看出DP将输入的一个batch数据分成了n份n为实际使用的GPU数量分别送到对应的GPU进行计算。
在网络前向传播时模型会从主GPU复制到其它GPU上在反向传播时每个GPU上的梯度汇总到主GPU上求得梯度均值更新模型参数后再复制到其它GPU以此来实现并行。
由于主GPU要进行梯度汇总和模型更新并将计算任务下发给其它GPU所以主GPU的负载与使用率会比其它GPU高这就导致了GPU负载不均衡的现象。
再说说DDPDDP多进程控制多GPU。系统会为每个GPU创建一个进程不再有主GPU每个GPU执行相同的任务。DDP使用分布式数据采样器DistributedSampler加载数据确保数据在各个进程之间没有重叠。
在反向传播时各GPU梯度计算完成后各进程以广播的方式将梯度进行汇总平均然后每个进程在各自的GPU上进行梯度更新从而确保每个GPU上的模型参数始终保持一致。由于无需在不同GPU之间复制模型DPP的传输数据量更少因此速度更快。
DistributedDataParallel既可用于单机多卡也可用于多机多卡它能够解决DataParallel速度慢、GPU负载不均衡等问题。因此官方更推荐使用DistributedDataParallel来进行分布式训练也就是接下来要说的DDP训练。
DDP训练
DistributedDataParallel主要是为多机多卡而设计的不过单机上也同样可以使用。
想要弄明白DPP的训练机制我们先要弄明白这几个分布式中的概念
group即进程组。默认情况下只有一个组即一个world。
world_size :表示全局进程个数。
rank表示进程序号用于进程间通讯表示进程优先级。rank=0的主机为主节点。
使用DDP进行分布式训练的具体流程如下。接下来我们就按步骤分别去实现。
第一步初始化进程组。我们使用init_process_group函数来进行分布式初始化其定义如下
torch.distributed.init_process_group(backend, init_method=None,, world_size=-1, rank=-1, group_name='')
我们分别看看定义里的相关参数:
backend是通信所用的后端可以是“nccl”或“gloo”。一般来说nccl用于GPU分布式训练gloo用于CPU进行分布式训练。
init_method字符串类型是一个url用于指定进程初始化方式默认是 “env://”表示从环境变量初始化还可以使用TCP的方式或共享文件系统 。
world_size执行训练的所有的进程数表示一共有多少个节点机器
rank进程的编号也是其优先级表示当前节点机器的编号。
group_name进程组的名字。
使用nccl后端的代码如下。
torch.distributed.init_process_group(backend="nccl")
完成初始化以后第二步就是模型并行化。正如前面讲过的我们可以使用DistributedDataParallel将模型分发至多GPU上其定义如下
torch.nn.parallel.DistributedDataParallel(module, device_ids=None, output_device=None, dim=0
DistributedDataParallel的参数与DataParallel基本相同因此模型并行化的用法只需将DataParallel函数替换成DistributedDataParallel即可具体代码如下。
net = torch.nn.parallel.DistributedDataParallel(net)
最后就是创建分布式数据采样器。在多机多卡情况下,分布式训练数据的读取也是一个问题,不同的卡读取到的数据应该是不同的。
DP是直接将一个batch的数据划分到不同的卡但是多机多卡之间进行频繁的数据传输会严重影响效率这时就要用到分布式数据采样器DistributedSampler它会为每个子进程划分出一部分数据集从而使DataLoader只会加载特定的一个子数据集以避免不同进程之间有数据重复。
创建与使用分布式数据采样器的代码如下。
train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)
data_loader = DataLoader(train_dataset, batch_size=batch_size, sampler=train_sampler)
结合代码我给你解读一下。-
首先我们将train_dataset送到了DistributedSampler中并创建了一个分布式数据采样器train_sampler。
然后在构造DataLoader的时候, 参数中传入了一个sampler=train_sampler即可让不同的进程节点加载属于自己的那份子数据集。也就是说使用DDP时不再是从主GPU分发数据到其他GPU上而是各GPU从自己的硬盘上读取属于自己的那份数据。
为什么要使用分布式训练以及分布式训练的原理我们就讲到这里。相信你已经对数据并行与模型并行都有了一个初步的认识。
小试牛刀
下面我们将会讲解一个官方的ImageNet的示例以后你可以把这个小项目当做分布式训练的一个模板来使用。
这个示例可对使用DP或DDP进行选配下面我们就一起来看核心代码。
if args.distributed:
if args.dist_url == "env://" and args.rank == -1:
args.rank = int(os.environ["RANK"])
if args.multiprocessing_distributed:
# For multiprocessing distributed training, rank needs to be the
# global rank among all the processes
args.rank = args.rank * ngpus_per_node + gpu
dist.init_process_group(backend=args.dist_backend, init_method=args.dist_url,
world_size=args.world_size, rank=args.rank)
这里你可以重点关注示例代码中的“args.distributed”参数args.distributed为True表示使用DDP反之表示使用DP。
我们来看main_worker函数中这段针对DDP的初始化代码如果使用DDP那么使用init_process_group函数初始化进程组。ngpus_per_node表示每个节点的GPU数量。
我们再来看main_worker函数中的这段逻辑代码。
if not torch.cuda.is_available():
print('using CPU, this will be slow')
elif args.distributed:
# For multiprocessing distributed, DistributedDataParallel constructor
# should always set the single device scope, otherwise,
# DistributedDataParallel will use all available devices.
if args.gpu is not None:
torch.cuda.set_device(args.gpu)
model.cuda(args.gpu)
# When using a single GPU per process and per
# DistributedDataParallel, we need to divide the batch size
# ourselves based on the total number of GPUs we have
args.batch_size = int(args.batch_size / ngpus_per_node)
args.workers = int((args.workers + ngpus_per_node - 1) / ngpus_per_node)
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu])
else:
model.cuda()
# DistributedDataParallel will divide and allocate batch_size to all
# available GPUs if device_ids are not set
model = torch.nn.parallel.DistributedDataParallel(model)
elif args.gpu is not None:
torch.cuda.set_device(args.gpu)
model = model.cuda(args.gpu)
else:
# DataParallel will divide and allocate batch_size to all available GPUs
if args.arch.startswith('alexnet') or args.arch.startswith('vgg'):
model.features = torch.nn.DataParallel(model.features)
model.cuda()
else:
model = torch.nn.DataParallel(model).cuda()
这段代码是对使用CPU还是使用GPU、如果使用GPU是使用DP还是DDP进行了逻辑选择。我们可以看到这里用到了DistributedDataParallel函数或DataParallel函数对模型进行并行化。-
并行化之后就是创建分布式数据采样器,具体代码如下。
if args.distributed:
train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)
else:
train_sampler = None
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=args.batch_size, shuffle=(train_sampler is None),
num_workers=args.workers, pin_memory=True, sampler=train_sample
这里需要注意的是在建立Dataloader的过程中如果sampler参数不为None那么shuffle参数不应该被设置。
最后我们需要为每个机器节点上的每个GPU启动一个进程。PyTorch提供了torch.multiprocessing.spawn函数来在一个节点启动该节点所有进程具体的代码如下。
ngpus_per_node = torch.cuda.device_count()
if args.multiprocessing_distributed:
# Since we have ngpus_per_node processes per node, the total world_size
# needs to be adjusted accordingly
args.world_size = ngpus_per_node * args.world_size
# Use torch.multiprocessing.spawn to launch distributed processes: the
# main_worker process function
mp.spawn(main_worker, nprocs=ngpus_per_node, args=(ngpus_per_node, args))
else:
# Simply call main_worker function
main_worker(args.gpu, ngpus_per_node, args)
对照代码我们梳理一下其中的要点。之前我们提到的main_worker函数就是每个进程中需要执行的操作。ngpus_per_node是每个节点的GPU数量每个节点GPU数量相同如果是多进程ngpus_per_node * args.world_size则表示所有的节点中一共有多少个GPU即总进程数。-
一般情况下进程0是主进程比如我们会在主进程中保存模型或打印log信息。
当节点数为1时实际上就是单机多卡所以说DDP既可以支持多机多卡也可以支持单机多卡。
main_worker函数的调用方法如下。
main_worker(args.gpu, ngpus_per_node, args)
其中args.gpu表示当前所使用GPU的id。而通过mp.spawn调用之后会为每个节点上的每个GPU都启动一个进程每个进程运行main_worker(i, ngpus_per_node, args)其中i是从0到ngpus_per_node-1。-
模型保存的代码如下。
if not args.multiprocessing_distributed or (args.multiprocessing_distributed
and args.rank % ngpus_per_node == 0):
save_checkpoint({
'epoch': epoch + 1,
'arch': args.arch,
'state_dict': model.state_dict(),
'best_acc1': best_acc1,
'optimizer' : optimizer.state_dict(),
}, is_best)
这里需要注意的是使用DDP意味着使用多进程如果直接保存模型每个进程都会执行一次保存操作此时只使用主进程中的一个GPU来保存即可。
好,说到这,这个示例中有关分布式训练的重点内容我们就讲完了。
小结
恭喜你走到这里,这节课我们一起完成了分布式训练的学习,最后咱们一起做个总结。
今天我们不但学习了为什么要使用分布式训练以及分布式训练的原理,还一起学习了一个分布式训练的实战项目。
在分布式训练中主要有DP与DDP两种模式。其中DP并不是完整的分布式计算只是将一部分计算放到了多张GPU卡上在计算梯度的时候仍然是“一卡有难八方围观”因此DP会有负载不平衡、效率低等问题。而DDP刚好能够解决DP的上述问题并且既可以用于单机多卡也可以用于多机多卡因此它是更好的分布式训练解决方案。
你可以将今天讲解的示例当做分布式训练的一个模板来使用。它包括了DP与DPP的完整使用过程并且包含了如何在使用DDP时保存模型。不过这个示例中的代码里其实还有更多的细节建议你留用课后空余时间通过精读代码、查阅资料多动手、多思考来巩固今天的学习成果。
每课一练
在torch.distributed.init_process_group(backend=“nccl”)函数中backend参数可选哪些后端它们分别有什么区别
推荐你好好研读今天的分布式训练demo也欢迎你记录自己的学习感悟或疑问我在留言区等你。

View File

@ -0,0 +1,241 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 图像分类(上):图像分类原理与图像分类模型
你好,我是方远,欢迎来到图像分类的学习。
通过前面的学习我们已经掌握了PyTorch有关深度学习的不少知识。为了避免纸上谈兵我们正式进入实战环节分别从计算机视觉与自然语言处理这两个落地项目最多的深度学习应用展开看看业界那些常见深度学习应用都是如何实现的。
完成这个模块的学习以后,我想你不仅仅会巩固之前学习的内容,还会进一步地落实到细分的领域去看待问题、解决问题。
说到计算机视觉,很常见的一种应用方向就是图像分类。关于图像分类,其实离我们并不遥远。你有没有发现,现在很多智能手机,照相的时候都会自动给照片内容打上标签。
举个例子,你看后面的截图,就是我用手机拍照的时候,手机自动对摄像头的内容进行了识别,打上了“多云”这个标签。
然后你会发现,手机还能根据识别到的内容,为你推荐一些美化的方案。那这是怎么做到的呢?其实这就是卷积神经网络最常用、最广泛且最基本的一个应用:图像分类。
今天咱们就来一探究竟,看看图像分类到底是怎么一回事。我会用两节课的篇幅,带你学习图像分类。这节课我们先学习理论知识,掌握图像分类原理和常见的卷积神经网络。下节课,我们再基于今天学到的原理,一块完成一个完整的图像分类项目实践。
图像分类原理
我们还是“书接上文”沿用第3节课NumPy的那个例子。现在线上每天都有大量的图片被上传老板交代你设计一个模型把有关极客时间Logo的图片自动找出来。
把这个需求翻译一下就是建立一个图像分类模型提供自动识别有极客时间Logo图片的功能。
我们来梳理一下这个模型的功能我们这个模型会接收一张图片然后会输出一组概率分别是该图片为Logo的概率与该图片为其他图片的概率从而通过概率来判断这张图片是Logo类还是Other类如下图所示
感知机
我们将上面的模型进一步拆分,看看如何才能获得这样的一组输出。
其中输入的图片就是输入X将其展开后可以获得输入X为\(X={x\_1, x\_2, … , x\_n}\)而模型可以看做有两个节点每个节点都会有一个输出分别代表着对输入为Logo和Other的判断但这里的输出暂时还不是概率只是模型输出的一组数值。这一部分内容如下图所示
上图这个结构其实就是感知机了,中间绿色的节点叫做神经元,是感知机的最基本组成单元。上图中的感知机只有中间一层(绿色的神经元),如果有多层神经元的话,我们就称之为多层感知机。
那什么是神经元呢神经元是关于输入的一个线性变换每一个输入x都会有一个对应的权值上图中的y的计算方式为
\[y\_i=\\delta(w\_{i1}x\_{1} + w\_{i2}x\_{2} + … + w\_{i\_n}x\_{n} + b\_i), \\space \\space \\space i=1,2\]其中,\(w\_{i1}, w\_{i2}, …, w\_{in}\)是神经元的权重,\(b\_i\)为神经元的偏移项。权重与偏移项都是通过模型学习到的参数。\(\\delta\)为激活函数,激活函数是一个可选参数。
那如何将一组数值,也就是\(y\_{1}\)与\(y\_{2}\)转换为一组对应的概率呢这个时候Softmax函数就要登场了。它的作用就是将一组数值转换为对应的概率概率和为1。
Softmax的计算公式如下
\[\\delta(x\_j) = \\frac{e^{x\_j}}{\\sum\_{j=1}^{m}e^{x\_j}}\]请看下面的代码我们用Softmax函数对原始的输入y做个转化将y中的数值转化为一组对应的概率
import torch
import torch.nn as nn
# 2个神经元的输出y的数值为
y = torch.randn(2)
print(y)
输出tensor([0.2370, 1.7276])
m = nn.Softmax(dim=0)
out = m(y)
print(out)
输出tensor([0.1838, 0.8162])
你看经过Softmax之后原始的输出y是不是转换成一组概率并且概率的和为1呢。原始y中最大的y具有最大的概率。
当然Softmax也不是每一个问题都会使用。我们根据问题的不同可以采用不同的函数例如有的时候也会使用sigmoid激活函数sigmoid激活函数是将1个数值转换为0到1之间的概率。
现在,我们将上述的过程补充到前面的模型里,如下图所示。
全连接层
其实上面那张示意图就是图像的分类原理了。其中绿色那一层。在卷积神经网络中称为全连接层Full Connection Layer简称fc层。一般都是放在网络的最后端用来获得最终的输出也就是各个类别的概率。
因为全连接层中的神经元的个数是固定的所以说在有全连接层的网络中输入图片是必须固定尺寸的。而现实里我们线上收集到的图片会有不同的尺寸所以需要先把图片尺寸统一起来PyTorch才能进一步处理。
我们假设将前面的输入图片resize到128x128然后看看全连接层推断的过程在PyTorch中是如何实现的。
x = torch.randint(0, 255, (1, 128*128), dtype=torch.float32)
fc = nn.Linear(128*128, 2)
y = fc(x)
print(y)
输出tensor([[ 72.1361, -120.3565]], grad_fn=<AddmmBackward>)
# 注意y的shape是(1, 2)
output = nn.Softmax(dim=1)(y)
print(output)
输出tensor([[1., 0.]], grad_fn=<SoftmaxBackward>)
结合代码不难看出PyTorch中全连接层用nn.Linear来实现。我们分别看看里面的重要参数有哪些
in_features输入特征的个数在本例中为128x128
out_features输出的特征数在本例中为2
bias是否需要偏移项默认为True。
全连接层的输入,也不是原始图片数据,而是经过多层卷积提取的特征。
前面我们曾说过有的网络是可以接收任意尺度的输入的。在上文中的设计中全连接层的输入x1到xn是固定的数目等于最后一层特征图所有元素的数目。如下图所示
我们将上述结构稍作调整,就可以接收任意尺度的输入了。只需要在最后的特征图后面加一个全局平均即可,也就是将每个特征图进行求平均,用平均值代替特征图,这样无论输入的尺度是多少,进入全连接层的数据量都是固定的。
如下图所示,黄色的圈就是全局平均的结果。
我们下一节课介绍的EfficientNet就是采用这种方式使得网络可以使用任意尺度的图片进行训练。
卷积神经网络
其实刚才说的多层感知机就是卷积神经网络的前身,由于自身的缺陷(参数量大、难以训练),使其在历史上有段时间一直是停滞不前,直到卷积神经网络的出现,打破了僵局。
卷积神经网络的最大作用就是提取出输入图片的丰富信息,然后再对接上层的一些应用,比如前面提到的图片分类。把卷积神经网络应用到图像分类原理中,得到的模型如下图所示:
你需要注意的是示意图中各个层的定义,不同层有不同的名称。
在上图中,整个模型或者网络的重点全都在卷积神经网络那块,所以这也是我们的工作重点。
那如何找到一个合适的卷积神经网络呢?在实际工作中,我们几乎不会自己去设计一个神经网络网的(因为不可控的变量太多),而是直接选择一些大神设计好的网络直接使用。那网络模型那么多,我们如何验证大神们提出的网络确实是可靠、可用的呢?
ImageNet
在业界中有个标杆——ImageNet大家都用它来评价提出模型的好与坏。
ImageNet本身包含了一个非常大的数据集并且从2010年开始每年都会举办一次著名的ImageNet 大规模视觉识别挑战赛The ImageNet Large Scale Visual Recognition Challenge ILSVRC比赛包含了图像分类、目标检测与图像分割等任务。
其中图像分类比赛使用的数据集是一份有1000个类别的庞大数据集只要能在这个比赛中脱颖而出的模型都是我们所说的经典网络结构这些网络在实际项目中基本都是我们的首选。
从2012年开始伴随着深度学习的发展几乎每一年都有非常经典的网络结构诞生下表为历年来ImageNet上Top-5的错误率。
你可能会有疑问,了解这么多网络模型真的有必要么?
我想说的是磨刀不误砍柴工机器学习这个领域始终是依靠研究驱动的。工作当中我们很少从0到1自创一个网络模型常常是在经典设计基础上做一些自定义配置所以你最好对这些经典网络都有所了解。
接下来,我们就挑选几个经典的神经网络来看看。
VGG
VGG取得了ILSVRC 2014比赛分类项目的第2名和定位项目的第1名的优异成绩。
当年的VGG一共提供了A到E6种不同的VGG网络字母不同只是表示层数不一样。VGG19的效果虽说最好但是综合模型大小等指标在实际项目中VGG16用得更加多一点。具体的网络结构你可以看看论文。
我们来看看VGG突破的一些重点
证明了随着模型深度的增加,模型效果也会越来越好。
使用较小的3x3的卷积代替了AlexNet中的11x11、7x7以及5x5的大卷积核。
关于第二点VGG中将5x5的卷积用2层3x3的卷积替换将7x7的卷积用3层3x3的卷积替换。这样做首先可以减少网络的参数其次是可以在相同感受野的前提下加深网络的层数从而提取出更加多样的非线性信息。
GoogLeNet
2014年分类比赛的冠军是GoogLeNetVGG同年。GoogLeNet的核心是Inception模块。这个时期的Inception模块是v1版本后续还有v2、v3以及v4版本。
我们先来看看GoogLeNet解决了什么样的问题。研究人员发现对于同一个类别的图片主要物体在不同图片中所占的区域大小均有不同例如下图所示。
如果使用AlexNet或者VGG中标准的卷积的话每一层只能以相同的尺寸的卷积核来提取图片中的特征。
但是正如上图所示很可能物体以不同的尺寸出现在图片中那么能否以不同尺度的卷积来提取不同的特征呢沿着这个想法Inception模块应运而生如下图示
结合图示我们发现这里是将原来的相同尺寸卷积提取特征的方式拆分为使用1x1、3x3、5x5以及3x3的max pooling同时进行特征提取然后再合并到一起。这样就做到了以多尺度的方式提取图片中的特征。
作者为了降低网络的计算成本将上述的Inception模块做了一步改进在3x3、5x5之前与pooling之后添加了1x1卷积用来降维从而获得了Inception模块的最终形态。
这里有个额外的小知识点如果是面试经常会被问到为什么采用1x1的卷积或者1x1卷积的作用。1x1卷积的作用就是用来升维或者降维的。
GooLeNet就是由以上的Inception模块构成的一个22层网络。别看网络层数有22层但是它参数量却比AlexNet与VGG都要少这带来的优势就是搭建起来的模型就很小占的存储空间也小。具体的网络结构你可以参考它的论文。
ResNet
ResNet中文意思是残差神经网络。在2015年的ImageNet比赛中模型的分类能力首次超越人眼1000类图片top-5的错误率降低到3.57%。
在论文中作者给出了18层、34层、50层、101层与152层的ResNet。101层的与152层的残差神经网络效果最好但是受硬件设备以及推断时间的限制50层的残差神经网络在实际项目中更为常用。
具体的网络结构你感兴趣的话可以自己看看论文全文,这里我着重带你看看这个网络的主要突破点。
网络退化问题
虽说研究已经证明,随着网络深度的不断增加,网络的整体性能也会提升。如果只是单纯的增加网络,就会引起以下两个问题:第一,模型容易过拟合;第二,产生梯度消失、梯度爆炸的问题。
虽然随着研究的不断发展以上两个问题都可以被解决掉但是ResNet网络的作者发现以上两个问题被规避之后简单的堆叠卷积层依然不能获得很好的效果。
为了验证刚才的观点作者做了这样的一个实验。通过搭建一个普通的20层卷积神经网络与一个56层的卷积神经网络在CIFAR-10数据集上进行了验证。无论训练集误差还是测试集误差56层的网络均高于20层的网络。下图来源于论文。
出现这样的情况,作者认为这是网络退化造成的。
网络退化是指当一个网络可以开始收敛时随着网络层数的增加网络的精度逐渐达到饱和并且会迅速降低。这里精度降低的原因并不是过拟合造成的因为如果是过拟合上图中56层的在训练集上的精度应该高于20层的精度。
作者认为这一现象并不合理假设20层是一个最优的网络通过加深到56层之后理论上后面的36层是可以通过学习到一个恒等映射的也就是说理论上不会学习到一个比26层还差的网络。所以作者猜测网络不能很容易地学习到恒等映射(恒等映射就是f(x)=x)。
残差学习
正如刚才所说,从网络退化问题中可以发现,通过简单堆叠卷积层似乎很难学会到恒等映射。为了改善网络退化问题,论文作者何凯明提出了一种深度残差学习的框架。
因为网络不容易学习到恒等映射,所以就让它强制添加一个恒等映射,如下图所示(下图来源于论文)。
具体实现是通过一种叫做shortcut connection的机制来完成的。在残差神经网络中shortcut connection就是恒等变换就是上图中带有x identity的那条曲线包含shortcut connection的几层网络我们称之为残差块。
残差块被定义为如下形式:
\[y = F(x, W\_i) + x\]F可以是2层的卷积层。也可以是3层的卷积层。最后作者发现通过残差块就可以训练出更深、更加优秀的卷积神经网络了。
小结
恭喜你完成了这节课的学习,让我们回顾一下这节课的主要内容。
首先我们从多层感知机说起,带你认识了这个卷积神经网络的前身。之后我们一起推导出了图像分类原理的基础模型。你需要注意的是,整个模型或者网络的重点全都在卷积神经网络那块,所以这也是我们的工作重点。
之后我们结合业界标杆ImageNet的评选情况一起学习了一些经典的网络结构VGG、GoogLeNet、ResNet。这里为了让你快速抓住重点我是从每个网络解决了什么问题各自有什么突破点展开的。也建议你课余时间多读读相关论文做更为详细深入的了解。
纵观网络结构的发展,我们不难发现,一直都是长江后浪推前浪,一代更比一代强。掌握了这些网络结构,你就是深度学习未来的弄潮儿。下节课我们再一起实践一个图像分类项目,加深你对图像分类的理解,敬请期待。
思考题
欢迎推荐一下近几年来,你自己觉得比较不错的神经网络模型。
欢迎你在留言区跟我交流互动,也推荐你把这节课分享给更多的同事、朋友。

View File

@ -0,0 +1,343 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 图像分类如何构建一个图像分类模型_
你好我是方远。欢迎来到第18节课的学习。
我相信经过上节课的学习,你已经了解了图像分类的原理,还初步认识了一些经典的卷积神经网络。
正所谓“纸上得来终觉浅,绝知此事要躬行”,今天就让我们把上节课的理论知识应用起来,一起从数据的准备、模型训练以及模型评估,从头至尾一起来完成一个完整的图像分类项目实践。
课程代码你可以从这里下载。
问题回顾
我们先来回顾一下问题背景我们要解决的问题是在众多图片中自动识别出极客时间Logo的图片。想要实现自动识别首先需要分析数据集里的图片是啥样子的。
那我们先来看一张包含极客时间Logo的图片如下所示。
你可以看到Logo占整张图片的比例还是比较小的所以说如果这个项目是真实存在的目标检测其实更加合适。不过我们可以将问题稍微修改一下修改成自动识别极客时间宣传海报这其实就很适合图像分类任务了。
数据准备
相比目标检测与图像分割来说,图像分类的数据准备还是比较简单的。在图像分类中,我们只需要将每个类别的图片放到指定的文件夹里就行了。
下图是我的图片组织方式,文件夹就是图片所属的类别。
logo文件夹中存放的是10张极客时间海报的图片。
而others中理论上应该是各种其它类型的图片但这里为了简化问题我这个文件夹中存放的都是小猫的图片。
模型训练
好啦,数据准备就绪,我们现在进入模型训练阶段。
今天我想向你介绍一个在最近2年非常受欢迎的一个网络——EfficientNet。它为我们提供了B0B7一共8个不同版本的模型这8个版本有着不同的参数量在同等参数量的模型中它的精度都是首屈一指的。因此这8个版本的模型可以解决你的大多数问题。
EfficientNet
我先给你解读一下EfficientNet的这篇论文这里我着重分享论文的核心思路还有我的理解学有余力的同学可以在课后自行阅读原文。
EfficientNet一共有B0到B78个模型参数量由少到多精度也越来越高具体你可以看看后面的评价指标。
在之前的那些网络要么从网络的深度出发要么从网络的宽度出发来优化网络的性能但从来没有人将这些方向结合在一起考虑。而EfficientNet就做了这样的尝试它探索了网络深度、网络宽度、图像分辨率之间的最优组合。
EfficientNet利用一种复合的缩放手段对网络的深度depth、宽度width和分辨率resolution同时进行缩放按照一定的缩放规律来达到精度和运算复杂度FLOPS的权衡。
但即使只探索这三个维度搜索空间仍然很大所以作者规定只在B0作者提出的EfficientNet的一个Baseline上进行放大。
首先,作者比较了单独放大这三个维度中的任意一个维度效果如何。得出结论是放大网络深度或网络宽度或图像分辨率,均可提升模型精度,但是越放大,精度增加越缓慢,如下图所示:
然后作者做了第二个实验尝试在不同的r分辨率d深度组合下变动w宽度得到下图
结论是,得到更高的精度以及效率的关键是平衡网络宽度,网络深度,图像分辨率三个维度的缩放倍率(d, r, w)。
因此,作者提出了混合维度放大法,该方法使用一个\(\\phi\)(混合稀疏)来决定三个维度的放大倍率。
深度depth\(d = \\alpha ^{\\phi}\)
宽度width\(w = \\beta ^{\\phi}\)
分辨率resolution: \(r = \\gamma ^{\\phi}\)
\[s.t. \\space \\alpha\\cdot\\beta^2\\cdot\\gamma^2 \\approx2 \\space \\space \\alpha \\geq1,\\beta \\geq1,\\gamma \\geq1\]第一步,固定\(\\phi\)为1也就是计算量为2倍使用网格搜索得到了最佳的组合也就是\(\\alpha=1.2, \\beta = 1.1, \\gamma = 1.15\)。
第二步,固定\(\\alpha=1.2, \\beta = 1.1, \\gamma = 1.15\),使用不同的混合稀疏\(\\phi\)得到了B1~B7。
整体评估效果如下图所示:
从评估结果上可以看到EfficientNet的各个版本均超过了之前的一些经典卷积神经网络。
EfficientNet v2也已经被提出来了有时间的话你可以自己去看看。
我们不妨借助一下EfficientNet的GitHub它里面有训练ImageNet的demo(demo/imagenet/main.py),接下来我们一起看看它的核心代码,然后精简一下代码,把它运行起来(Torchvision也提供了EfficientNet的模型课后你也可以自己试一试)。
这里我们再回顾一下之前说的机器学习3件套
1.数据处理-
2.模型训练(构建模型、损失函数与优化方法)-
3.模型评估
接下来我们就挨个看看这些步骤。你需要先把https://github.com/lukemelas/EfficientNet-PyTorch给克隆下来我们只使用efficientnet_pytorch中的内容它包含着模型的网络结构。
之后我们来创建一个叫做geektime的项目文件夹然后把efficientnet_pytorch放进去。
在开始之前我先把程序需要的参数给你列一下在下面的讲解中我们就直接使用这些参数了。当你在实现今天代码的时候需要将这些参数补充到代码中可以使用argparsem模块
好,下面让我们正式开始动手。
加载数据
首先是数据加载的环节我们创建一个dataset.py文件用来存储与数据有关的内容。dataset.py如下我省略了模块的引入
# 作者给出的标准化方法
def _norm_advprop(img):
return img * 2.0 - 1.0
def build_transform(dest_image_size):
normalize = transforms.Lambda(_norm_advprop)
if not isinstance(dest_image_size, tuple):
dest_image_size = (dest_image_size, dest_image_size)
else:
dest_image_size = dest_image_size
transform = transforms.Compose([
transforms.RandomResizedCrop(dest_image_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize
])
return transform
def build_data_set(dest_image_size, data):
transform = build_transform(dest_image_size)
dataset=datasets.ImageFolder(data, transform=transform, target_transform=None)
return dataset
这部分代码完成的工作是通过build_data_set构建数据集。这里我们使用了torchvision.datasets.ImageFolder来创建Dataset。ImageFolder能将按文件夹形式的组织数据生成到一个Dataset。
在这个例子中,我传入的训练集路径为’./data/train你可以看看开篇的截图。
ImageFolder会自动的将同一文件夹内的数据打上一个标签也就是说logo文件夹的数据ImageFolder会认为是来自同一类别others文件夹的数据ImageFolder会认为是来自另外一个类别。
我们这个精简版只构建了训练集的Dataset当你看Efficient官方代码的时候在验证集的构建过程中你需要留意一下验证集的transforms。
我认为这里这么做是有点问题的原因是Resize中size参数如果是个tuple类型则直接按照size的尺寸进行resize。如果是一个int的时候如果图片的height大于width则按照(size * height/width, size)进行resize。
在作者的原始程序中imag_size是个int而不是tuple。所以按照这种先resize再crop的方式处理一下对长宽比比较大的图片来说效果不是很好。
让我们实际验证一下这个想法我将开篇的例子也就是那张海报图的image_size设定为224后用上述的方式进行处理后获得下图。
你看,是不是缺少了很多信息?
所以如果在我们的例子中使用作者的程序就需要做一下修改。把这里的代码逻辑修改为如果image_size不是tuple先将image_size转换为tuple并且也不需要crop了。代码如下所示
if not isinstance(image_size, tuple):
image_size = (image_size, image_size)
else:
image_size = image_size
transform = transforms.Compose([
transforms.Resize(image_size, interpolation=Image.BICUBIC),
transforms.ToTensor(),
normalize,
])
训练的主程序我们定义在main.py中在main.py中的main()中,进行数据的加载,如下所示。
然后我们通过for循环一个一个Epoch的调用train方法进行训练就可以了。
# 省略了一些模块的引入
from efficientnet import EfficientNet
from dataset import build_data_set
def main():
# part1: 模型加载 (稍后补充)
# part2: 损失函数、优化方法(稍后补充)
train_dataset = build_data_set(args.image_size, args.train_data)
train_loader = torch.utils.data.DataLoader(
train_dataset,
batch_size=args.batch_size,
shuffle=True,
num_workers=args.workers,
)
for epoch in range(args.epochs):
# 调用train函数进行训练稍后补充
train(train_loader, model, criterion, optimizer, epoch, args)
# 模型保存
if epoch % args.save_interval == 0:
if not os.path.exists(args.checkpoint_dir):
os.mkdir(args.checkpoint_dir)
torch.save(model.state_dict(), os.path.join(args.checkpoint_dir,
'checkpoint.pth.tar.epoch_%s' % epoch))
创建模型
接下来我们来看看如何创建模型这一步我们直接使用作者给出的Efficient模型。在上面代码注释中的part1部分用下述代码即可加载EfficientNet模型。
args.classes_num = 2
if args.pretrained:
model = EfficientNet.from_pretrained(args.arch, num_classes=args.classes_num,
advprop=args.advprop)
print("=> using pre-trained model '{}'".format(args.arch))
else:
print("=> creating model '{}'".format(args.arch))
model = EfficientNet.from_name(args.arch, override_params={'num_classes': args.classes_num})
# 有GPU的话加上cuda()
#mode.cuda()
这段代码是说如果pretrained model参数为True则自动下载并加载pretrained model后进行训练否则是使用随机数初始化网络。-
from_pretrained与from_name中都需要修改一下num_classes将EfficientNet的全连接层修改我们项目对应的类别数这里的args.classes_num为2logo类与others类
模型微调
模型微调在第8节课和第14节课时说过这个概念比较重要我们一起再复习一下。
Pretrained model一般是在ImageNet也有可能是COCO或VOC都是公开数据集上训练过的模型我们可以直接把它在ImageNet上训练好的模型参数直接拿过来在其基础上训练我们自己的模型这就是模型微调。
所以说如果有Pretrained model我们一定会使用Pretrained model进行训练收敛速度会快。
使用Pretrained model的时候要注意一点在ImageNet上训练后的全连接层一共有1000个节点所以使用Pretrained model的时候只使用全连接层以外的参数。
在上述代码的EfficientNet.from_pretrained中会通调用load_pretrained_weights函数调用之前num_classes已经被修改为2logo与others所以说传入load_pretrained_weights的load_fc参数为False也就是说不会加载全连接层的参数。load_pretrained_weights的调用如下所示
load_pretrained_weights(model, model_name, load_fc=(num_classes == 1000), advprop=advprop)
load_pretrained_weights函数中包含下面这段代码就像刚才所说如果不加载全连接层则删除_fc的weight与bias
if load_fc:
ret = model.load_state_dict(state_dict, strict=False)
assert not ret.missing_keys, 'Missing keys when loading pretrained weights: {}'.format(ret.missing_keys)
else:
state_dict.pop('_fc.weight')
state_dict.pop('_fc.bias')
ret = model.load_state_dict(state_dict, strict=False)
设定损失函数与优化方法
最后要做的就是设定损失函数与优化方法了我们将下面的代码补充到part2部分
criterion = nn.CrossEntropyLoss() # 有GPU的话加上.cuda()
optimizer = torch.optim.SGD(model.parameters(), args.lr,
momentum=args.momentum,
weight_decay=args.weight_decay)
到这里我们就完成训练的所有准备了只要再补充好train函数就可以了代码如下。下面的代码的原理我们在第13节课中已经讲过了记不清的可以去回顾一下。
def train(train_loader, model, criterion, optimizer, epoch, args):
# switch to train mode
model.train()
for i, (images, target) in enumerate(train_loader):
# compute output
output = model(images)
loss = criterion(output, target)
print('Epoch ', epoch, loss)
# compute gradient and do SGD step
optimizer.zero_grad()
loss.backward()
optimizer.step()
不过在我的程序里保存了若干个Epoch的模型我们应该怎么选择呢这就要说到模型的评估环节。
模型评估
对于分类模型的评估来说有很多评价指标例如准确率、精确率、召回率、F1-Score等。其中我认为最直观、最有说服力的就是精确率与召回率这也是我在项目中观察的主要是指标。下面我们依次来看看。
混淆矩阵
在讲解精确率与召回率之前我们先看看混淆矩阵这个概念。其实精确率与召回率就是通过它计算出来的。下表就是一个混淆矩阵正例就是logo类负例就是others类。
根据预测结果和真实类别的组合,一共有四种情况:
1.TP是说真实类别为Logo模型也预测为Logo-
2.FP是说真实类别为Others但模型预测为Logo-
3.FN是说真实类别为Logo但模型预测为Others-
4.TN是说真实类别为Others模型也预测为Others
精确率的计算方法为:
\[precision = \\frac{TP}{ (TP + FP)}\]召回率的计算方式为:
\[recall = \\frac{TP}{(TP + FN)}\]精确率与召回率分别衡量了模型的不同表现精确率说的是如果模型认为一张图片是Logo类那有多大概率是Logo类。而召回率衡量的是在整个验证集中模型能找到多少Logo图片。
那问题来了,怎样根据这两个指标来选择模型呢?业务需求不同,我们侧重的指标就不一样。
比如在我们的这个项目中如果老板允许一部分Logo图片没有被识别但是模型必须非常准模型说一张图片是Logo类那图片真实类别就有非常大的概率是Logo类图片那应该侧重的就是精确率如果老板希望把线上Logo类尽可能地识别出来允许一部分图片被误识别那应该侧重的就是召回率。
在计算精确率与召回率的时候给你分享一下我的经验。在实际项目中我习惯把模型对每张图片的预测结果保存到一个txt中这样可以比较直观地筛选一些模型的badcase并且验证集如果非常大又需要调整的时候直接更改txt就可以了不需要再次让模型预测整个验证集。
下面是txt文件的一部分分别记录了logo类的概率、others类的概率、真实类别是否为logo、真实类别是否为others、预测类别是否为logo、预测类别是否为ohters、图片名。
14.jpeg是开篇例子的那张图片模型认为它是Logo的概率是0.58476others类的概率是0.41524。
...
0.64460 0.35540 1 0 1 0 ./data/val/logo/13.jpeg
0.58476 0.41524 1 0 1 0 ./data/val/logo/14.jpeg
...
下图是我训练了10个Epoch的B0模型在验证集(这里我用训练集充当了一下验证集)上的评价效果。-
通过混淆矩阵可以看到整个验证集一共有8+0张图片被预测为logo类所以logo类的精确率为8 / (8 + 0 ) = 1logo类一共有8+2张图片有两张预测错了所以召回率为8 / (8 +2) = 0.8。
others类别的计算类似你可以自己算算看。
小结
恭喜你,完成了今天的学习任务。今天我们一起完成了一个图像分类项目的实践。虽然项目规模较小,但是在真实项目中的每一个环节都包含在内了,可以说是麻雀虽小,五脏俱全。
下面我们回顾一下每个环节上的关键要点和实操经验。
数据准备其实是最关键的一步,数据的质量直接决定了模型好坏。所以,在开始训练之前你应该对你的数据集有十足的了解才可以。例如,验证集还是否可以反映出训练集、数据中有没有脏数据、数据分布有没有偏等等。
完成数据准备之后就到了模型训练,图像分类任务其实基本上都是采用主流的卷积神经网络了,很少对模型结构做一些更改。
最后的模型评估环节要侧重业务场景,看业务上需要高精确还是高召回,然后再对你的模型做调整。
思考题
老板希望你的模型能尽可能的把线上所有极客时间的海报都找到,允许一些误召回。训练模型的时候你应该侧重精确率还是召回率?
推荐你动手实现一下今天的Demo也欢迎你把这节课分享给更多的同事、朋友跟他一起学习进步。

View File

@ -0,0 +1,244 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 图像分割(上):详解图像分割原理与图像分割模型
你好,我是方远。
在前两节课我们完成了有关图像分类的学习与实践。今天,让我们进入到计算机视觉另外一个非常重要的应用场景——图像分割。
你一定用过或听过腾讯会议或者Zoom之类的产品吧?在进行会议的时候,我们可以选择对背景进行替换,如下图所示。
在华为手机中也曾经有过人像留色的功能。
这些应用背后的实现都离不开今天要讲的图像分割。
我们同样用两节课的篇幅进行学习,这节课主攻分割原理,下节课再把这些技能点活用到实战上,从头开始搭建一个图像分割模型。
图像分割
我们不妨用对比的视角,先从概念理解一下图像分割是什么。图像分类是将一张图片自动分成某一类别,而图像分割是需要将图片中的每一个像素进行分类。
图像分割可以分为语义分割与实例分割,两者的区别是语义分割中只需要把每个像素点进行分类就可以了,不需要区分是否来自同一个实例,而实例分割不仅仅需要对像素点进行分类,还需要判断来自哪个实例。
如下图所示,左侧为语义分割,右侧为实例分割。我们这两节课都会以语义分割来展开讲解。
语义分割原理
语义分割原理其实与图像分类大致类似,主要有两点区别。首先是分类端(这是我自己起的名字,就是经过卷积提取特征后分类的那一块)不同,其次是网络结构有所不同。先看第一点,也就是分类端的不同。
分类端
我们先回想一下图像分类的原理。你可以结合下面的示意图做理解。
输入图片经过卷积层提取特征后最终会生成若干特征图然后在这些特征图之后会接一个全连接层上图中红色的圆圈全连接层中的节点数就对应着要将图片分为几类。我们将全连接层的输出送入到softmax中就可以获得每个类别的概率然后通过概率就可以判断输入图片属于哪一个类别了。
在图像分割中,同样是利用卷积层来提取特征,最终生成若干特征图。只不过最后生成的特征图
的数目对应着要分割成的类别数。举一个例子,假设我们想要将输入的小猫分割出来,也就是说,这个图像分割模型有两个类别,分别是小猫与背景,如下图所示。
最终的两个特征图中通道1代表的小猫的信息通道2对应着背景的信息。
这里我给你再举一个例子来说明一下如何判断每个像素的类别。假设通道1中0,0这个位置的输出是2通道2中0,0这个位置的输出是30。
经过softmax转为概率后通道10, 0这个位置的概率为0而对应通道2中(0,0)这个位置的概率为1我们通过概率可以判断出00这个位置是背景而不是小猫。
网络结构
在分割网络中最终输出的特征图的大小要么是与输入的原图相同,要么就是接近输入。
这么做的原因是我们要对原图中的每个像素进行判断。当输出特征图与原图尺寸相同时可以直接进行分割判断。当输出特征图与原图尺寸不相同时需要将输出的特征图resize到原图大小。
如果是从一个比较小的特征图resize到一个比较大的尺寸的时候必定会丢失掉一部分信息的。所以输出特征图的大小不能太小。
这也是图像分割网络与图像分类网络的第二个不同点,在图像分类中,经过多层的特征提取,最后生成的特征图都是很小的。而在图像分割中,最后生成的特征图通常来说是接近原图的。
前文也说过图像分割网络也是通过卷积进行提取特征的按照之前的理论特征提取后特征图尺寸是减小的。如果说把特征提取看做Encoder的话那在图像分割中还有一步是Decoder。
Decoder的作用就是对特征图尺寸进行还原的过程将尺寸还原到一个比较大的尺寸。这个还原的操作对应的就是上采样。而在上采样中我们通常使用的是转置卷积。
转置卷积
接下来我就带你研究一下转置卷积的计算原理,这也是这节课的重点内容。
我们看下面图这个卷积计算padding为0stride为1。
从之前的学习我们可以知道卷积操作是一个多对一的运算输出中的每一个y都与输入中的4个x有关。其实转置卷积从逻辑上是卷积的一个逆过程而不是卷积的逆运算。
也就是说转置卷积并不是使用上图中的输出Y与卷积核Kernel来获得上图中的输入X转置卷积只能还原出一个与输入特征图尺寸相同的特征图。
我们将转置卷积中的卷积核用k表示那么一个y会与四个k进行还原如下所示
还原尺寸的过程如下所示下图中每个还原后的结果都对应着原始3x3的输入。
通过观察你可以发现,有些部分是重合的,对于重合部分把它们加起来就可以了,最终还原后的特征图如下:
将上图的结果稍作整理,整理为下面的结果,也没有做什么特殊处理,只是补了一些零:
上面的结果,我们又可以通过下面的卷积获得:-
你有没有发现一件很神奇的事情,转置卷积计算又变回了卷积计算。
所以,我们一起梳理一下,转置卷积的计算过程如下:
1.对输入特征图进行补零操作。-
2.将转置卷积的卷积核上下、左右变换作为新的卷积核。-
3.利用新的卷积核在1的基础上进行步长为1padding为0的卷积操作。
我们先来看一下PyTorch中转置卷积以及它的主要参数再根据参数解释一下第一步1是如何补零的。
class torch.nn.ConvTranspose2d(in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
groups=1,
bias=True,
dilation=1)
其中in_channels、out_channels、kernel_size、groups、bias以及dilation与我们之前讲卷积时的参数含义是一样的你可以回顾卷积的第9、10两节课这里我们就不赘述了。
首先我们看一下stride。因为转置卷积是卷积的一个逆向过程所以这里的stride指的是在原图上的stride。
在我们刚才的例子里stride是等于1的如果等于2时按照同样的套路可以转换为如下的卷积变换。 同时我们也可以得到结论上文中第一步补零的操作是在输入的特征图的行与列之间补stride-1个行与列的零。
再来看padding操作padding是指要在输入特征图的周围补dilation * (kernel_size - 1) - padding圈零。这里用到了dliation参数但是通常在转置卷积中dilation、groups参数使用的比较少。
以上就是转置卷积的补零操作了,图片和文字双管齐下,我相信你一定能够理解它。
通过上述的讲解,我们可以推导出输出特征图尺寸与输入特征图尺寸的关系:
\[h\_{out} = (h\_{in} - 1) \* stride\[0\] - padding\[0\] + kernel\\\_size\[0\]\]\[w\_{out} = (w\_{in} - 1) \* stride\[1\] - padding\[1\] + kernel\\\_size\[1\]\]下面,我们借助代码来验证一下,我们讲的转置卷积是否是向我们所说的那样计算。
现在有特征图input_feat:
import torch
import torch.nn as nn
import numpy as np
input_feat = torch.tensor([[[[1, 2], [3, 4]]]], dtype=torch.float32)
input_feat
输出:
tensor([[[[1., 2.],
[3., 4.]]]])
卷积核k
kernels = torch.tensor([[[[1, 0], [1, 1]]]], dtype=torch.float32)
kernels
输出:
tensor([[[[1., 0.],
[1., 1.]]]])
stride为1padding为0的转置卷积
convTrans = nn.ConvTranspose2d(1, 1, kernel_size=2, stride=1, padding=0, bias = False)
convTrans.weight=nn.Parameter(kernels)
按照我们刚才讲的,第一步是补零操作,输入的特征图补零后为:
\[input\\\_feat = \\begin{bmatrix}-
0 & 0 & 0 & 0 \\\\\\-
0 & 1 & 2 & 0 \\\\\\-
0 & 3 & 4 & 0 \\\\\\-
0 & 0 & 0 & 0\\\\\\-
\\end{bmatrix} \]然后再与变换后的卷积核:-
$\(\\begin{bmatrix}-
1 & 1 \\\\\\-
0 & 1-
\\end{bmatrix}\)\(-
做卷积运算后,获得输出:-
\)\(output = \\begin{bmatrix}-
1 & 2 & 0 \\\\\\-
4 & 7 & 2 \\\\\\-
3 & 7 & 4-
\\end{bmatrix} \)$
我们再看看代码的输出,如下所示:
convTrans(input_feat)
输出:
tensor([[[[1., 2., 0.],
[4., 7., 2.],
[3., 7., 4.]]]], grad_fn=<SlowConvTranspose2DBackward>)
你看看是不是一样呢?
损失函数
说完网络结构,我们再开启图像分割里的另一个话题:损失函数。
在图像分割中依然可以使用在图像分类中经常使用的交叉熵损失。在图像分类中一张图片有一个预测结果预测结果与真实值就可以计算出一个Loss。而在图像分割中真实的标签是一张二维特征图这张特征图记录着每个像素的真实分类结果。在分割中含有像素类别的特征图我们一般称为Mask。
我们结合一张小猫图片的例子解释一下。对于下图中的小猫进行标记标记后会生成它的GT这个GT就是一个Mask。
GT是Ground Truth的缩写在图像分割中我们经常使用这个词。在图像分类中与之对应的就是数据的真实标签在图像分割中则GT是每个像素的真实分类如下面的例子所示。
GT如下所示
那在我们模型预测的Mask中每个位置都会有一个预测结果这个预测结果与GT中的Mask做比较然后会生成一个Loss。
当然在图像分割中不光有交叉熵损失可以用还可以用更加有针对性的Dice Loss下节课我再继续展开。
公开数据集
刚才我们也看到了,图像分割的数据标注还是比较耗时的,具体如何标注一张语义分割所需要的图片,下节课我们再一起通过实践探索。
除此之外业界也有很多比较有权威性且质量很高的公开数据集。最著名的就是COCO了链接如下https://cocodataset.org/#detection-2016。一共有80个类别超过2万张图片。感兴趣的话课后你可以尝试着使用它训练来看看。
小结
恭喜你完成了今天的学习。
今天我们首先明确了语义分割要解决的问题是什么,它可以对图像中的每个像素都进行分类。
然后我们对比图像分类原理,说明了语义分割的原理。它与图像分类主要有两个不同点:
1.在分类端有所不同,在图像分类中,经过卷积的特征提取后,最后会以若干个神经元的形式作为输出,每个神经元代表着对一个类别的判断情况。而语义分割,则是会输出若干的特征图,每个特征图代表着对应类别判断。
2.在图像分类的网络中特征图是不断减小的。但是在语义分割的网络中特征图还会有decoder这一步它是将特征图进行放大的过程。实现decoder的方式称为上采样在上采样中我们最常使用的就是转置卷积。
对于转置卷积,我们除了要知道它是怎么计算的之外,最重要的是要记住它不是卷积的逆运算,只是能将特征图的大小进行放大的一种卷积运算。
每课一练
对于本文的小猫分割问题最终只输出1个特征图是否可以
欢迎你在留言区跟我交流互动,也推荐你把这节课分享给更多同事、朋友。

View File

@ -0,0 +1,495 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 图像分割(下):如何构建一个图像分割模型?
你好,我是方远。
在上一节课中,我们掌握了图像分割的理论知识,你是不是已经迫不及待要上手体验一下,找找手感了呢?
今天我们就从头开始来完成一个图像分割项目。项目的内容是对图片中的小猫进行语义分割。为了实现这个项目我会引入一个简单但实用的网络结构UNet。通过这节课的学习你不但能再次体验一下完整机器学习的模型实现过程还能实际训练一个语义分割模型。
课程代码你可以从这里下载。
数据部分
我们还是从机器学习开发三件套:数据、训练、评估说起。首先是数据准备部分,我们先对训练数据进行标记,然后完成数据读取工作。
分割图像的标记
之前也提到过图像分割的准备相比图像分类的准备更加复杂。那我们如何标记语义分割所需要的图片呢在图像分割中我们使用的每张图片都要有一张与之对应的Mask如下所示
-
上节课我们说过Mask就是含有像素类别的特征图。结合这里的示例图片我们可以看到Mask就是原图所对应的一张图片它的每个位置都记录着原图每个位置对应的像素类别。对于Mask的标记我们需要使用到Labelme工具。
标记的方法一共包括七步,我们挨个看一下。
第一步下载安装Labelme。我们按照Github中的安装方式进行安装即可。如果安装比较慢的话你可以使用国内的镜像例如清华的进行安装。
第二步我们要将需要标记的图⽚放到⼀个⽂件夹中。这里我是将所有猫的图片放入到cats文件夹中了。
第三步我们事先准备好⼀个label.txt的⽂件⾥⾯每⼀⾏写好的需要标记的类别。我的label.txt如下
__ignore__
_background_
cat
这里我要提醒你的是前两行最好这么写。不这样写的话使用label2voc.py转换就会报错但label2voc.py不是唯一的数据转换方式还可以使用labelme_json_to_dataset但推荐你使用label2voc.py。从第三行开始表示要标记的类别。
第四步执行后面的这条命令就会自动启动Labelme。
labelme --labels labels.txt --nodata
第五步点我们击左侧的Open Dir选择第二步中的文件夹就会自动导入需要标记的图片。在右下角选择需要标记的文件后会自动显示出来如下图所示。
第六步点击左侧的Create Polygons。就可以开始标注了。标记的方式就是将小猫沿着它的边界给圈出来当形成一个闭环的时候Labelme会自动提示你输入类别我们选择cat类即可。
标记成功后,结果如下图所示。
当标记完成后我们需要保存一下保存之后会生成标记好的json文件。如下所示
fangyuan@geektime data $ ls cats
1.jpeg 1.json 10.jpeg 10.json 2.jpeg 3.jpeg 4.jpeg 4.json
第七步执行下面的代码将标记好的数据转换成Mask。
python label2voc.py cats cats_output --label label.txt
上面代码里用到的label2voc.py你可以通过后面这个链接获取它https://github.com/wkentaro/labelme/blob/main/examples/semantic_segmentation/labelme2voc.py。
其中cats为标记好的数据cats_output为输出文件夹。在cats_output下会自动生成4个文件夹我们只需要两个文件夹分别是JPEGImages训练原图与SegmentationClassPNG转换后的Mask
到此为止我们的数据就准备好了。我一共标记了8张图片如下所示。当然了在实际的项目中需要大量标记好的图片这里主要是为了方便演示。
到此为止,标记工作宣告完成。
数据读取
完成了标记工作之后我们就要用PyTorch把这些数据给读入进来了我们把数据相关的写在dataset.py中。具体还是和之前讲的一样要继承Dataset类然后实现__init__、__len__和__getitem__方法。
dataset.py的代码如下所示我已经在代码中写好注释了相信结合注释你很容易就能领会意思。
import os
import torch
import numpy as np
from torch.utils.data import Dataset
from PIL import Image
class CatSegmentationDataset(Dataset):
# 模型输入是3通道数据
in_channels = 3
# 模型输出是1通道数据
out_channels = 1
def __init__(
self,
images_dir,
image_size=256,
):
print("Reading images...")
# 原图所在的位置
image_root_path = images_dir + os.sep + 'JPEGImages'
# Mask所在的位置
mask_root_path = images_dir + os.sep + 'SegmentationClassPNG'
# 将图片与Mask读入后分别存在image_slices与mask_slices中
self.image_slices = []
self.mask_slices = []
for im_name in os.listdir(image_root_path):
# 原图与mask的名字是相同的只不过是后缀不一样
mask_name = im_name.split('.')[0] + '.png'
image_path = image_root_path + os.sep + im_name
mask_path = mask_root_path + os.sep + mask_name
im = np.asarray(Image.open(image_path).resize((image_size, image_size)))
mask = np.asarray(Image.open(mask_path).resize((image_size, image_size)))
self.image_slices.append(im / 255.)
self.mask_slices.append(mask)
def __len__(self):
return len(self.image_slices)
def __getitem__(self, idx):
image = self.image_slices[idx]
mask = self.mask_slices[idx]
# tensor的顺序是Batch_size, 通道而numpy读入后的顺序是(高,宽,通道)
image = image.transpose(2, 0, 1)
# Mask是单通道数据所以要再加一个维度
mask = mask[np.newaxis, :, :]
image = image.astype(np.float32)
mask = mask.astype(np.float32)
return image, mask
然后我们的训练代码写在train.py中train.py中的main函数为主函数在main中我们会调用data_loaders来加载数据。代码如下所示
import torch
from torch.utils.data import DataLoader
from torch.utils.data import DataLoader
from dataset import CatSegmentationDataset as Dataset
def data_loaders(args):
dataset_train = Dataset(
images_dir=args.images,
image_size=args.image_size,
)
loader_train = DataLoader(
dataset_train,
batch_size=args.batch_size,
shuffle=True,
num_workers=args.workers,
)
return loader_train
# args是传入的参数
def main(args):
loader_train = data_loaders(args)
以上就是数据处理的全部内容了。接下来,我们再来看看模型训练部分的内容。
模型训练
我们先来回忆一下,模型训练的老三样,分别是网络结构、损失函数和优化方法。
先从网络结构说起今天我要为你介绍一个叫做UNet的语义分割网络。
网络结构UNet
UNet是一个非常实用的网络。它是一个典型的Encoder-Decoder类型的分割网络网络结构非常简单如下图所示。
它的网络结构虽然简单但是效果并不“简单”我在很多项目中都用它与一些主流的语义分割做对比而UNet都取得了非常好的效果。
整体网络结构跟论文中给出的示意图一样,我们重点去关注几个实现细节。
第一点图中横向蓝色的箭头它们都是重复的相同结构都是由两个3x3的卷积层组合而成的在每层卷积之后会跟随一个BN层与ReLU的激活层。按照第14节课讲的这一部分重复的组织是可以单独提取出来的。我们先来创建一个unet.py文件用来定义网络结构。
现在unet.py中创建Block类它是用来定义刚才所说的重复的卷积块
class Block(nn.Module):
def __init__(self, in_channels, features):
super(Block, self).__init__()
self.features = features
self.conv1 = nn.Conv2d(
in_channels=in_channels,
out_channels=features,
kernel_size=3,
padding='same',
)
self.conv2 = nn.Conv2d(
in_channels=features,
out_channels=features,
kernel_size=3,
padding='same',
)
def forward(self, input):
x = self.conv1(input)
x = nn.BatchNorm2d(num_features=self.features)(x)
x = nn.ReLU(inplace=True)(x)
x = self.conv2(x)
x = nn.BatchNorm2d(num_features=self.features)(x)
x = nn.ReLU(inplace=True)(x)
return x
这里需要注意的是同一个块内特征图的尺寸是不变的所以padding为same。
第二点,就是绿色向上的箭头,也就是上采样的过程。这块的实现就是采用上一节课所讲的转置卷积来实现的。
最后一点我们现在是要对小猫进行分割也就是说一共有两个类别——猫与背景。对于二分类的问题我们可以直接输出一张特征图然后通过概率来进行判断是正例还是负例背景也就是下面代码中的第71行。同时下述代码也补全了unet.py中的所有代码。
import torch
import torch.nn as nn
class Block(nn.Module):
...
class UNet(nn.Module):
def __init__(self, in_channels=3, out_channels=1, init_features=32):
super(UNet, self).__init__()
features = init_features
self.conv_encoder_1 = Block(in_channels, features)
self.conv_encoder_2 = Block(features, features * 2)
self.conv_encoder_3 = Block(features * 2, features * 4)
self.conv_encoder_4 = Block(features * 4, features * 8)
self.bottleneck = Block(features * 8, features * 16)
self.upconv4 = nn.ConvTranspose2d(
features * 16, features * 8, kernel_size=2, stride=2
)
self.conv_decoder_4 = Block((features * 8) * 2, features * 8)
self.upconv3 = nn.ConvTranspose2d(
features * 8, features * 4, kernel_size=2, stride=2
)
self.conv_decoder_3 = Block((features * 4) * 2, features * 4)
self.upconv2 = nn.ConvTranspose2d(
features * 4, features * 2, kernel_size=2, stride=2
)
self.conv_decoder_2 = Block((features * 2) * 2, features * 2)
self.upconv1 = nn.ConvTranspose2d(
features * 2, features, kernel_size=2, stride=2
)
self.decoder1 = Block(features * 2, features)
self.conv = nn.Conv2d(
in_channels=features, out_channels=out_channels, kernel_size=1
)
def forward(self, x):
conv_encoder_1_1 = self.conv_encoder_1(x)
conv_encoder_1_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_1_1)
conv_encoder_2_1 = self.conv_encoder_2(conv_encoder_1_2)
conv_encoder_2_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_2_1)
conv_encoder_3_1 = self.conv_encoder_3(conv_encoder_2_2)
conv_encoder_3_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_3_1)
conv_encoder_4_1 = self.conv_encoder_4(conv_encoder_3_2)
conv_encoder_4_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_4_1)
bottleneck = self.bottleneck(conv_encoder_4_2)
conv_decoder_4_1 = self.upconv4(bottleneck)
conv_decoder_4_2 = torch.cat((conv_decoder_4_1, conv_encoder_4_1), dim=1)
conv_decoder_4_3 = self.conv_decoder_4(conv_decoder_4_2)
conv_decoder_3_1 = self.upconv3(conv_decoder_4_3)
conv_decoder_3_2 = torch.cat((conv_decoder_3_1, conv_encoder_3_1), dim=1)
conv_decoder_3_3 = self.conv_decoder_3(conv_decoder_3_2)
conv_decoder_2_1 = self.upconv2(conv_decoder_3_3)
conv_decoder_2_2 = torch.cat((conv_decoder_2_1, conv_encoder_2_1), dim=1)
conv_decoder_2_3 = self.conv_decoder_2(conv_decoder_2_2)
conv_decoder_1_1 = self.upconv1(conv_decoder_2_3)
conv_decoder_1_2 = torch.cat((conv_decoder_1_1, conv_encoder_1_1), dim=1)
conv_decoder_1_3 = self.decoder1(conv_decoder_1_2)
return torch.sigmoid(self.conv(conv_decoder_1_3))
到这里,网络结构我们就搭建好了,然后我们来我看看损失函数。
损失函数Dice Loss
这里我们来看一下语义分割中常用的损失函数Dice Loss。
想要知道这个损失函数如何生成你需要先了解一个语义分割的评价指标但更常用的还是后面要讲的的mIoU它就是Dice系数常用于计算两个集合的相似度取值范围在0-1之间。
Dice系数的公式如下。
\[Dice=\\frac{2|P\\cap G|}{|P|+|G|}\]其中,\(|P\\cap G|\)是集合P与集合G之间交集元素的个数\(|P|\)和\(|G|\)分别表示集合P和G的元素个数。分子的系数2这是为了抵消分母中P和G之间的共同元素。对语义分割任务而言集合P就是预测值的Mask集合G就是真实值的Mask。
根据Dice系数我们就能设计出一种损失函数也就是Dice Loss。它的计算公式非常简单如下所示。
\[Dice Loss=1-\\frac{2|P\\cap G|}{|P|+|G|}\]从公式中可以看出当预测值的Mask与GT越相似损失就越小当预测值的Mask与GT差异度越大损失就越大。
对于二分类问题GT只有0和1两个值。当我们直接使用模型输出的预测概率而不是使用阈值将它们转换为二值Mask时这种损失函数就被称为Soft Dice Loss。此时\(|P\\cap G|\)的值近似为GT与预测概率矩阵的点乘。
定义损失函数的代码如下。
import torch.nn as nn
class DiceLoss(nn.Module):
def __init__(self):
super(DiceLoss, self).__init__()
self.smooth = 1.0
def forward(self, y_pred, y_true):
assert y_pred.size() == y_true.size()
y_pred = y_pred[:, 0].contiguous().view(-1)
y_true = y_true[:, 0].contiguous().view(-1)
intersection = (y_pred * y_true).sum()
dsc = (2. * intersection + self.smooth) / (
y_pred.sum() + y_true.sum() + self.smooth
)
return 1. - dsc
其中self.smooth是一个平滑值这是为了防止分子和分母为0的情况。
训练流程
最后,我们将模型、损失函数和优化方法串起来,看下整体的训练流程,训练的代码如下。
def main(args):
makedirs(args)
# 根据cuda可用情况选择使用cpu或gpu
device = torch.device("cpu" if not torch.cuda.is_available() else args.device)
# 加载训练数据
loader_train = data_loaders(args)
# 实例化UNet网络模型
unet = UNet(in_channels=Dataset.in_channels, out_channels=Dataset.out_channels)
# 将模型送入gpu或cpu中
unet.to(device)
# 损失函数
dsc_loss = DiceLoss()
# 优化方法
optimizer = optim.Adam(unet.parameters(), lr=args.lr)
loss_train = []
step = 0
# 训练n个Epoch
for epoch in tqdm(range(args.epochs), total=args.epochs):
unet.train()
for i, data in enumerate(loader_train):
step += 1
x, y_true = data
x, y_true = x.to(device), y_true.to(device)
y_pred = unet(x)
optimizer.zero_grad()
loss = dsc_loss(y_pred, y_true)
loss_train.append(loss.item())
loss.backward()
optimizer.step()
if (step + 1) % 10 == 0:
print('Step ', step, 'Loss', np.mean(loss_train))
loss_train = []
torch.save(unet, args.weights + '/unet_epoch_{}.pth'.format(epoch))
需要注意的点,我都在注释中进行了说明,你可以自己看一看。其实就是我们一直说的模型训练的那几件事情:数据加载、构建网络以及迭代更新网络参数。
我用训练数据训练了若干个Epoch同时也保存了若干个模型保存为pth格式。到这里就完成了模型训练的整个环节我们可以使用保存好的模型进行预测来看看分割效果如何。
模型预测
现在我们要用训练生成的模型来进行语义分割,看看结果是什么样子的。
模型预测的代码如下。
import torch
import numpy as np
from PIL import Image
img_size = (256, 256)
# 加载模型
unet = torch.load('./weights/unet_epoch_51.pth')
unet.eval()
# 加载并处理输入图片
ori_image = Image.open('data/JPEGImages/6.jpg')
im = np.asarray(ori_image.resize(img_size))
im = im / 255.
im = im.transpose(2, 0, 1)
im = im[np.newaxis, :, :]
im = im.astype('float32')
# 模型预测
output = unet(torch.from_numpy(im)).detach().numpy()
# 模型输出转化为Mask图片
output = np.squeeze(output)
output = np.where(output>0.5, 1, 0).astype(np.uint8)
mask = Image.fromarray(output, mode='P')
mask.putpalette([0,0,0, 0,128,0])
mask = mask.resize(ori_image.size)
mask.save('output.png')
这段代码也很好理解。首先用torch.load函数加载模型。接着加载一张待分割的图片并进行数据预处理。然后将处理好的数据送入模型中得到预测值output。最后将预测值转化为可视化的Mask图片进行保存。
输入图片也就是待分割的图片如下左图所示。最终的输出即可视化的Mask图片如下右图所示。
在将预测值转化为Mask图片的过程中最终预测值的概率卡了0.5的阈值超过阈值的像素点在output矩阵中的值为1表示猫的区域没有超过阈值的像素点在output矩阵中的值为0表示背景区域。
为了将output矩阵输出为可视化的图像我们使用Image.fromarray函数将Numpy的array转化为Image格式并将模式设置为“P”即调色板模式。然后用putpalette函数来给Image对象上色。
其中putpalette函数的参数是一个列表[0, 0, 0, 0, 128, 0]列表前三个数表示值为0的像素的RGB[0, 0, 0]表示黑色列表后三个数表示值为1的像素的RGB[0, 128, 0]表示绿色。这样我们保存的Mask图片黑色部分即为背景区域绿色部分即为猫的区域。
不过这样分开的轮廓图可能无法让我们很直观地看出语义分割的效果。所以我们将原图和Mask合成一张图片来看看效果。具体的代码如下。
image = ori_image.convert('RGBA')
mask = mask.convert('RGBA')
# 合成
image_mask = Image.blend(image, mask, 0.3)
image_mask.save("output_mask.png")
首先我们将原图image和Mask图片都转换为RGBA带透明度的模式。然后使用Image.blend函数将两张图片合成一张图片最后一个参数0.3表示Mask图片透明度为30%原图的透明度为70%。-
最终的结果如下图所示。
这样我们就可以直观地看出哪些地方预测得不准确了。
模型评估
在语义分割中常用的评价指标是mIoU。mIoU全称为mean Intersection over Union即平均交并比。交并比是真实值和预测值的交集和并集之比。
真实值就是我们刚刚用labelme标注的Mask也是Ground TruthGT。如下左图所示。
预测值就是模型预测出的Mask用Prediction表示。如后面右图所示。
交集是指真实值与预测值的交集,如下图黄色区域所示。并集是指真实值与预测值的并集,如下图蓝色区域所示。
通过上面几个图我们很容易就能理解mIoU了。mIoU的公式如下所示。
\[mIoU=\\frac{1}{k}\\sum\_{i=1}^{k}{\\frac{P\\cap G}{P\\cup G}}\]其中k为所有类别数在我们的例子中只有“cat”一类因此k为1我们通常不将背景计算到mIoU中P为预测值G是真实值。
小结
恭喜你,完成了今天的学习任务。这节课我们一起完成了一个图像分割项目的实践。
首先我带你了解了图像分割的数据准备需要使用Labelme工具为图像做标记。数据质量的好坏决定了最终模型的质量所以你要对数据的标注好好把握。在使用Labelme标记完成之后我们可以使用label2voc.py将json转换为Mask。
之后我们学习了一种非常高效且实用的模型UNet并使用PyTorch实现了其网络结构。
然后我为你讲解了图像分割的评估指标mIoU和损失函数Dice Loss。
mIoU的公式如下
\[mIoU=\\frac{1}{k}\\sum\_{i=1}^{k}{\\frac{P\\cap G}{P\\cup G}}\]mIoU主要是从预测结果与GT的重合度这一角度来衡量分割模型的好与坏的它是图像分割中经常使用的评价指标。
最后,我们使用训练好的模型进行预测,并对分割结果进行了可视化绘制。相信通过之前学习的图像分类项目与今天学习的图像分割项目,对于图像处理,你会获得更深层次的理解。
每课一练
你可以根据今天的内容,自己动手试试建立一个图像分割模型,然后用一张图片来测一下效果如何。
欢迎你在留言区跟我交流讨论,也推荐你把今天的内容分享给更多同事、朋友,我们下节课见。

View File

@ -0,0 +1,250 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 NLP基础详解自然语言处理原理与常用算法
你好,我是方远。
在之前的课程中,我们一同学习了图像分类、图像分割的相关方法,还通过实战项目小试牛刀,学完这部分内容,相信你已经对深度学习图像算法有了一个较为深入的理解。
然而在实际的项目中除了图像算法还有一个大的问题类型就是文字或者说语言相关的算法。这一类让程序理解人类语言表达的算法或处理方法我们统称为自然语言处理Natural Language Processing, NLP
这节课我们先来学习自然语言处理的原理和常用算法通过这一部分的学习以后你遇到一些常见的NLP问题很容易就能想出自己的解决办法。不必担心原理、算法的内容太过理论化我会结合自己的经验从实际应用的角度为你建立对NLP的整体认知。
NLP的应用无处不在
NLP研究的领域非常广泛凡是跟语言学有关的内容都属于NLP的范畴。一般来说较为多见的语言学的方向包括词干提取、词形还原、分词、词性标注、命名实体识别、语义消歧、句法分析、指代消解、篇章分析等方面。
看到这里你可能感觉这些似乎有点太学术、太专业了涉及语言的结构、构成甚至是性质方面的研究了。没错这些都是NLP研究在语言学中的应用方面就会给人一种比较偏研究的感觉。
实际上NLP还有很多的研究内容是侧重“处理”和“应用”方面的比如我们常见的就有机器翻译、文本分类、问答系统、知识图谱、信息检索等等。
我举一个例子,你就知道自然语言处理有多么重要了。平时我们经常会用搜索引擎,当你打开网页、在搜索框中输入自己想要了解的关键词之后,搜索引擎的后台算法逻辑就要开始一整套非常复杂的算法逻辑,这其中包括几个比较重要的方面,我们不妨结合例子来看看。
在搜索引擎的输入框中输入“亚洲的动wu”文本显示的内容如下图所示。别看只是一次简单的检索动作搜索系统要完成的工作可不少。
首先搜索引擎要对你输入的内容query进行解析这就涉及到了之前提到的分词、命名实体识别、语义消歧等内容当然还涉及到了query纠错因为你错误地输入了拼音而非汉字需要改写成正确的形式。
通过一系列的算法之后,系统识别出你的需求是:寻找动物相关的搜索结果,这些结果的限定条件是它们要生活在亚洲。
接着,系统就开始在数据库(或者是存储的集群中)搜索相关的实体,这些实体的查询和限制条件的过滤,就涉及信息检索、知识图谱等内容。
最后,细心的同学对照搜索结果会发现,有的时候搜索引擎除了提供严格匹配的检索结果之外,还会提供一些相关内容的扩展结果,比如广告、新闻、视频等。而且很多搜索引擎的扩展搜索结果页都是个性化的,也就是根据用户的特点行为提供推荐,这些让我们的搜索结果更加丰富,体验更好。
仅仅只有这些了么?不,远远没有,因为刚才的这个过程,只是针对你这一个用户的一次检索所需要完成的一部分工作而已。更多的工作,实际是用户开始使用搜索引擎之前的构建准备阶段。
为了构建搜索引擎,就需要对存储的内容进行解析,这就包括了篇章理解、文本处理、图片识别、音视频算法等环节,对每一个网页(内容)进行特征的提取,构建检索库、知识库等,这个工作量就会非常的大,涉及的面也非常广泛。
由此可见NLP的应用真的深入到了互联网业务的方方面面掌握了NLP的相关算法将会使我们的竞争力变得更强。接下来针对自然语言处理的“应用”方面我们一起聊聊NLP中文场景下的一些重要内容。
NLP的几个重要内容
想要让程序对文本内容进行理解,我们需要解决几个非常基础和重要的内容,分别是分词、文本表示以及关键词提取。
分词
中文跟英文最大的不同在于,英文是由一个个单词构成的,单词与单词之间有空格隔断。但是中文不一样,中文单词和单词之间除了标点符号没有别的隔断。这就给程序理解文本带来了一定的难度,分词的需求也应运而生。
尽管现在的深度学习已经对分词的依赖越来越小可以通过Word Embedding等方式对字符token级的文字进行表示但是分词的地位不会降低单词、词组级别的文本表示仍旧有非常多的应用场景。
因为我们的学习重在快速上手和实战应用,所以为了降低你的学习成本,这个专栏里我不会专门深入讲解各种分词算法细节,而是侧重于带你理解其特点,并教你学习如何用相应的工具包实现分词过程。
目前网络上已经有了很多的开源或者免费的NLP分词工具比如jieba、HanLP、THULAC等包括腾讯、百度、阿里等公司也有相应的商业付费工具。
贫穷使人理智我们今天使用免费的jieba分词来做一个分词的例子链接你可以从这里获取。安装这个工具非常简单只需要使用pip即可。
pip install jieba
jieba的使用也很方便我来演示一下
import jieba
text = "极客时间棒呆啦"
# jieba.cut得到的是generator形式的结果
seg = jieba.cut(text)
print(' '.join(seg))
# Get 极客 时间 棒呆 啦
其实除了分词jieba还提供了词性标注的结果pos
import jieba.posseg as posseg
text = "一天不看极客时间我就浑身难受"
# 形如pair('word, 'pos')的结果
seg = posseg.cut(text)
print([se for se in seg])
# Get [pair('一天', 'm'), pair('不', 'd'), pair('看', 'v'), pair('极客', 'n'), pair('时间', 'n'), pair('我', 'r'), pair('就', 'd'), pair('浑身', 'n'), pair('难受', 'v')]
是不是非常简单?搞定了分词,我们接下来就要开始对文本进行表示了。
文本表示的方法
在深度学习被广泛采用之前,很多传统机器学习算法结合自身的特点,使用了各种各样的文本表示。
最经典的就是独热One-hot表示法了。在这种方法中假定所有的文字一共有N个单词也可以是字符我们可以将每个单词赋予一个单独的序号id那么对于任意一个单词我们都可以采用一个N位的列表向量对其进行表示。在这个表示中只需要将这个单词对应序号id的位置为1其他位置为0即可。
我还是举个例子来帮你加深理解。比方说我们词典大小为10000“极客”这个单词的序号id为666那么我们就需要建立一个10000长度的向量并将其中的第666位置为1其余位为0。如下
这时候你就会发现在UTF-8编码中中文字符有两万多个词语数量更是天文数字那么我们仅用字符的方式每个字就需要两万多维度的数据来表示。推算一下如果有一篇一万字的文章这个数据量就很可怕了。
为了进一步的压缩数据的体积可以只使用一个向量表示文章中所有的单词例如前面的例子我们仍旧建立一个10000维的向量把文章中出现过的所有单词的对应位置置为1其余为0。
这样看上去数据体积就少了很多还有没有其他办法进一步缩减空间的占用呢有的例如count-based表示方法。
在这种方法中我们采用v={index1: count1, index2: count2,…, index n: count n}的形式对每一个出现的单词的序号id以及出现过的次数进行统计这样一来“极客时间”我们只需要两个k-v对的dict即可表示: {3:1, 665:1}。
这种表示方法在SVM、树模型等多个算法包中被广泛采用因为客观来说它确实能够大幅度地压缩空间的占用生成起来也非常方便。但是你会发现前面这几种方法不能表述单词的语序信息。
举个例子,“我/喜欢/你”和“你/喜欢/我”两个截然不同的意思,用前面的方法做分词的话,却会得到相同的表示结果。这时候如果搞错了,其实却是单相思的话,那岂不是很苦涩?
好在现在深度学习的使用推动了Word Embedding的发展基本上我们都会采用该方法进行文本表示。但是还是刚才的话这并不意味着传统的文本表示方法就过时了。在一些小规模、轻量级的文本处理场景中它们的作用仍旧非常大。
关于文本表示中Word Embedding的部分咱们在后续课程再展开讲解这也是NLP深度学习的核心内容之一。
让我们回到刚才的传统文本表示方法为了实现对单词顺序信息的记录该怎么办呢这时我们要解决NLP中的一个重要问题关键词的提取。
关键词的提取
关键词,顾名思义,就是能够表达文本中心内容的词语。关键词提取在检索系统、推荐系统等应用中有着极重要的地位。它是文本数据挖掘领域的一个分支,所以在摘要生成、文本分类/聚类等领域中也是非常基础的环节。
关键词提取,主要分为有监督和无监督的方法,一般来说,我们采用无监督的方法较多一些,这是因为它不需要人工标注的语料,只需要对文本中的单词按照位置、频率、依存关系等信息进行判断,就可以实现关键词的识别和提取。
无监督方法一般有三种类型,基于统计特征的方法、基于词图模型的方法,以及基于主题模型的方法,我们分别来看看。
基于统计特征的方法
这种类型的方法最为经典的就是TF-IDFterm frequencyinverse document frequency词频-逆向文件频率)。该方法最核心的思想非常简单:一个单词在文件中出现的次数越多,它的重要性越高;但它在语料库中出现的频率越高,它的重要性反而越小。
什么意思呢就比如说我们有10篇文章其中有2篇财经文章、5篇科技、3篇娱乐对于单词“股票”它在财经文章中的次数肯定非常多但是在娱乐和科技中就非常少这就意味着“股票”这个词就能够更好的“区分”文章的类别那它的重要性自然也就非常高了。
在TF-IDF中词频TF表示关键字在文本中出现的频率。而逆向文件频率 (IDF) 是由包含该词语的文件的数目除以总文件数目得到的,一般情况下还会取对数对结果进行缩放。
-
你可以自己先想想这里为什么分母要加1呢这是为了避免分母为0的情况。得到了TF和IDF之后我们将两者相乘就得到了TF-IDF了。
通过TF-IDF不难看出基于统计的方法的特点在于对单词出现的次数以及分布进行数学上的统计从而发现相应的规律和重要性权重并以此作为关键词提取的依据。
跟分词一样关键词的提取目前也有很多集成工具包比如NLTKNatural Language Toolkit它是一个非常著名的自然语言处理工具包是NLP研究领域常用的Python库。我们仍旧可以使用pip install nltk命令来进行安装。
使用NLTK来计算TF-IDF非常简单代码如下
from nltk import word_tokenize
from nltk import TextCollection
sents=['i like jike','i want to eat apple','i like lady gaga']
# 首先进行分词
sents=[word_tokenize(sent) for sent in sents]
# 构建语料库
corpus=TextCollection(sents)
# 计算TF
tf=corpus.tf('one',corpus)
# 计算IDF
idf=corpus.idf('one')
# 计算任意一个单词的TF-IDF
tf_idf=corpus.tf_idf('one',corpus)
你可以执行前面这段代码看看tf_idf等于多少
基于词图模型的关键词提取
前面基于统计的方法采用的是对词语的频率计算的方式,但我们还可以有其他的提取思路,那就是基于词图模型的关键词提取。
在这种方法中,我们首先要构建文本一个图结构,用来表示语言的词语网络。然后对语言进行网络图分析,在这个图上寻找具有重要作用的词或者短语,即关键词。
该类方法中最经典的就是TextRank算法了它脱胎于更为经典的网页排序算法PageRank。关于PageRank算法你可以参考这个wiki戳我。戳完PageRank之后你就会知道PageRank算法的核心内容有两点
如果一个网页被很多其他网页链接到的话就说明这个网页比较重要也就是PageRank值会相对较高。
如果一个PageRank值很高的网页链接到一个其他的网页那么被链接到的网页的PageRank值会相应地因此而提高。-
而TextRank就非常好理解了。它跟PageRank的区别在于
用句子代替网页
任意两个句子的相似性可以采用类似网页转换概率的概念计算但是也稍有不同TextRank用归一化的句子相似度代替了PageRank中相等的转移概率所以在TextRank中所有节点的转移概率不会完全相等。
利用矩阵存储相似性的得分类似于PageRank的矩阵M。
TextRank的基本流程如下图所示。
看上去蛮复杂的不过没有关系刚才提到的jieba也有了相应的集成算法。在jieba中我们可以使用如下的函数进行提取
jieba.analyse.textrank(sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False)
其中sentence是待处理的文本topK是选择最重要的K个关键词基本上你用好这两个参数就足够了。
基于主题模型的关键词提取
最后一种关键词提取方法就是基于主题模型的关键词提取。
主题模型,这个名字看起来就高端了很多,实际上它也是一种基于统计的模型,只不过它会“发现”文档集合中出现的抽象的“主题”,并用于挖掘文本中隐藏的语义结构。
LDALatent Dirichlet Allocation文档主题生成模型是最典型的基于主题模型的算法。有关LDA的算法的介绍你随便搜索一下网络资料就能找到我就不展开说了。而咱们在这节课中将会利用已经集成好的工具包gensim来实现使用这个模型代码也非常简单我们一起来看一下
from gensim import corpora, models
import jieba.posseg as jp
import jieba
input_content = [line.strip() for line in open ('input.txt', 'r')]
# 老规矩,先分词
words_list = []
for text in input_content:
words = [w.word for w in jp.cut(text)]
words_list.append(words)
# 构建文本统计信息, 遍历所有的文本为每个不重复的单词分配序列id同时收集该单词出现的次数
dictionary = corpora.Dictionary(words_list)
# 构建语料将dictionary转化为一个词袋。
# corpus是一个向量的列表向量的个数就是文档数。你可以输出看一下它内部的结构是怎样的。
corpus = [dictionary.doc2bow(words) for words in words_list]
# 开始训练LDA模型
lda_model = models.ldamodel.LdaModel(corpus=corpus, num_topics=8, id2word=dictionary, passes=10)
在训练环节中num_topics代表生成的主题的个数。id2word即为dictionary它把id都映射成为字符串。passes相当于深度学习中的epoch表示模型遍历语料库的次数。
小结
在这节课中我带你一同了解了自然语言处理的应用场景以及三个经典的NLP基础问题。
NLP的三大经典问题包括分词、文本表示、关键词提取正是因为这三个问题太过经典和基础所以现在已经有了大量的集成工具供我们直接使用。
但我还是那句话,有了工具,并不意味着我们不需要理解它内部的原理,学习要知其然,更需要知其所以然,这样在实际的工作中遇到问题的时候,我们才能游刃有余地解决。
细心的你可能已经发现了今天的课程里我们针对不同的问题使用了不同的工具包分词我们使用了jieba关键词的提取我们使用了gensim和NLTK所以我希望你在课后有空的时候也去了解一下这三个工具的具体使用和更多功能因为它们真的很强大。
在文本表示方法中我们留了一个小尾巴也就是Word Embedding。随着深度学习的越来越广泛使用词嵌入Word Embedding的方法也有了越来越多的算法和工具来实现。在后续的课程中我会通过BERT的实战开发来向你介绍Word Embedding的训练生成和使用。
每课一练
TF-IDF有哪些缺点呢你不妨结合它的计算过程做个梳理。
期待你在留言区跟我交流互动也推荐你把这节课分享给身边对NLP感兴趣的同事、朋友跟他一起学习进步。

View File

@ -0,0 +1,128 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 NLP基础详解语言模型与注意力机制
你好,我是方远。
在上节课中我们一同了解了NLP任务中的几个经典问题这些方法各有千秋但是我们也发现有的方法并不能很好地将文本中单词、词组的顺序关系或者语义关系记录下来也就是说不能很好地量化表示也不能对语言内容不同部分的重要程度加以区分。
那么,有没有一种方法,可以把语言变成一种数学计算过程,比如采用概率、向量等方式对语言的生成和分析加以表示呢?答案当然是肯定的,这就是这节课我们要讲到的语言模型。
那如何区分语言不同部分的重要程度呢?我会从深度学习中最火热的注意力机制这一角度为你讲解。
语言模型
语言模型是根据语言客观事实而进行的语言抽象数学建模是一种对应关系。很多NLP任务中都涉及到一个问题对于一个确定的概念或者表达判断哪种表示结果是最有可能的。
我们结合两个例子体会一下。
先看第一个例子,翻译文字是:今天天气很好。可能的结果是: res1 = Today is a fine day. res2 = Today is a good day.那么我们最后要的结果就是看概率P(res1)和P(res2)哪个更大。
再比如说问答系统提问我什么时候才能成为亿万富翁。可能的结果有ans1 = 白日做梦去吧。ans2 = 红烧肉得加点冰糖。那么,最后返回的答案就要选择最贴近问题内容本身的结果,这里就是前一个答案。
对于上面例子中提到的问题,我们很自然就会联想到,可以使用概率统计的方法来建立一个语言模型,这种模型我们称之为统计语言模型。
统计语言模型
统计语言模型的原理简单来说就是计算一句话是自然语言也就是一个正常句子的概率。多年以来专家学者构建出了非常多的语言模型其中最为经典的就是基于马尔可夫假设n-gram语言模型它也是被广泛采用的模型之一。
接下来我们先从一个简单的抽象例子出发,来了解一下什么是统计语言模型。
给定一个句子S=w1,w2,w3,…,wn则生成该句子的概率为p(S)=p(w1,w2,w3,w4,w5,…,wn)再由链式法则我们可以继续得到p(S)=p(w1)p(w2|w1)p(w3|w1,w2)…p(wn|w1,w2,…,wn-1)。那么这个p(S)就是我们所要的统计语言模型。
那么问题来了你会发现从p(w1,w2,w3,w4,w5,…,wn)到p(w1)p(w2|w1)p(w3|w1,w2)…p(wn|w1,w2,…,wn-1)无非是一个概率传递的过程有一个非常本质的问题并没有被解决那就是语料中数据必定存在稀疏的问题公式中的很多部分是没有统计值的那就成了0了而且参数量真的实在是太大了。
怎么办呢?我们观察一下后面这句话:“我们本节课将会介绍统计语言模型及其定义”。其中“定义”这个词,是谁的定义呢?是“其”的。那“其”又是谁呢?是前面的“语言模型”的。于是我们发现,对于文本中的一个词,它出现的概率,很大程度上是由这个单词前面的一个或者几个单词决定的,这就是马尔可夫假设。
有了马尔可夫假设我们就可以把前面的公式中的p(wn|w1,w2,…,wn-1)进一步简化简化的程度取决于你认为一个单词是由前面的几个单词所决定的如果只由前面的一个单词决定那它就是p(wn|wn-1)我们称之为bigram。如果由前面两个单词决定则变为p(wn|wn-2wn-1)我们称之为trigram。
当然了如果你认为单词的出现仅由其本身决定的与其他单词无关就变成了最简单的形式p(wn)我们称之为unigram一元模型
那么现在我们知道了基于马尔可夫链的统计语言模型其核心就在于基于统计的条件概率。为了计算一个句子的生成概率我们只需要统计每个词及其前面n个词的共现条件概率再经过简单的乘法计算就可以得到最终结果了。
神经网络语言模型
ngram模型一定程度上减少了参数的数量但是如果n比较大或者相关语料比较少的时候数据稀疏问题仍然不能得到很好地解决。
这就好比我们把水浒传的文本放入模型中进行统计训练,最后却问模型林冲和潘金莲的关系,这就很难回答了。
因为基于ngram的统计模型实在是收集不到两者共现的文本。这种稀疏问题靠统计肯定不行了。那么怎么办呢这时候就轮到神经网络语言模型闪亮登场了。
其实从本质上说神经网络语言模型也是通过ngram来进行语言的建模但是神经网络的学习不是通过计数统计的方法而是通过神经网络内部神经元针对数据不断更新。
具体是怎么做的呢首先我们要定义一个向量空间假定这个空间是一百维的这就意味着对于每个单词我们可以用一个一百维的向量对其进行表示比如V(中国)=[0.2821289, 0.171265, 0.12378123,…,0.172364]。
这样对于任意两个单词我们可以用距离计算的方式来评价它们之间的联系。比如我们使用cosin距离计算“中国”和“北京”两个单词的距离就大概率要比“中国”和“西瓜”的距离要近得多。
这样做有什么好处呢首先词与词之间的距离可以作为两个词之间相似性的度量。其次向量空间隐含了很多的数学计算比如经典的V(国王)- V(皇后) = V(男人) - V(女人) ,这让词语之间有了更多的语义上的关联。
除了维度,为了确定向量空间,我们还需要确定这个空间有多少个“点”,也就是词语的数量有多少。一般来说,我们是将语料库中出现超过一定阈值次数的单词保留,把这些留下来的单词的数量,作为空间点的量了。
我们具体看看实际的操作过程中是怎么做的。我们只需要建立一个M*N大小的矩阵并随机初始化里面的每一个数值其中M表示的是词语的数量N表示词语的维度。我们把这样矩阵叫做词向量矩阵。
既然是随机初始化的,那么就意味着这个向量空间不能作为我们的语言模型使用。下面我们就要想办法让这个矩阵学到内容。如下图:
刚才我们说过神经网络语言模型也是通过ngram来进行语言建模的。假定我们的ngram长度为n那么我们就从词向量矩阵中找到对应的前n-1个词的向量经过若干层神经网络包括激活函数将这n-1个词的向量映射到对应的条件概率分布空间中。最后模型就可以通过参数更新的方式学习词向量的映射关系参数以及上下文单词出现的条件概率参数了。
简单来说就是我们使用n-1个词预测第n个词并利用预测出来的词向量跟真实的词向量做损失函数并更新就可以不断更新词向量矩阵从而获得一个语言模型。这种类型的神经网络语言模型我们称之为前馈网络语言模型。
除了前馈网络语言模型还有一种叫做基于LSTM的语言模型。下节课我们将会通过LSTM完成情感分析任务的项目进一步细化LSTM神经网络语言模型的训练过程同时也会用到前面提到的词向量矩阵。
现在,我们回过头来比较一下统计语言模型和神经网络语言模型的区别。统计语言模型的本质是基于词与词共现频次的统计,而神经网络语言模型则是给每个词分别赋予了向量空间的位置作为表征,从而计算它们在高维连续空间中的依赖关系。相对来说,神经网络的表示以及非线性映射,更加适合对自然语言进行建模。
注意力机制
如果你足够细心就会发现在前面介绍的神经网络语言模型中我们似乎漏掉了一个点那就是对于一个由n个单词组成的句子来说不同位置的单词重要性是不一样的。因此我们需要让模型“注意”到那些相对更加重要的单词这种方式我们称之为注意力机制也称作Attention机制。
既然是机制它就不是一个算法准确来说是一个构建网络的思路。关于注意力机制最经典的论文就是大名鼎鼎的《Attention Is All You Need》如果你有兴趣的话可以自行阅读这篇论文。
因为我们的专栏是以动手实践为主,重在实际应用各种机器学习的理论,所以不会对其内部的数学原理进行过多剖析,但是我们还是要知道它是怎么运作的。
我们从一个例子入手,比如“我今天中午跑到了肯德基吃了仨汉堡”。这句话中,你一定对“我”、“肯德基”、“仨”、“汉堡”这几个词比较在意,不过,你是不是没注意到“跑”字?
其实Attention机制要做的就是这件事找到最重要的关键内容。它对网络中的输入或者中间层的不同位置给予了不同的注意力或者权重然后再通过学习网络就可以逐渐知道哪些是重点哪些是可以舍弃的内容了。
在前面的神经网络语言模型中对于一个确定的单词它的向量是固定的但是现在不一样了因为Attention机制对于同一个单词在不同语境下它的向量表达是不一样的。
下面这张图是Attention机制和RNN结合的例子。其中红色框中的是RNN的展开模式我们可以看到我/爱/极/客四个字的向量沿着绿色箭头的方向传递每个字从RNN节点出来之后都会有一个隐藏状态h也就是输入节点上面的蓝色方框在这个过程中每个状态的权重是一样的不分大小。
而蓝色框就是Attention机制所加入的部分其中的每个α就是每个状态h的权重有了这个权重就可以将所有的状态h加权汇总到softmax中然后求和得到最终输出C。这个C就可以为后续的RNN判断权重提供更多的计算依据。
你看这个注意力机制的原理其实很简单但是也很巧妙。只需要增加很少的参数就可以让模型自己弄清楚谁重要谁次要。那么下面我们来看一下抽象化之后的Attention如下图这张图想必你应该在很多attention的相关介绍中见过了。
在这里输入是query(Q), key(K), value(V)输出是attention value。跟刚才Attention与RNN结合的图类比query就是上一个时间节点传递进来的状态Zt-1而这个Zt-1就是上一个时间节点输出的编码。key就是各个隐藏状态hvalue也是隐藏状态hh1, h2…hn。模型通过Q和K的匹配公式计算出权重再同V结合就可以得到输出这就相当于计算得到了当前的输出和所有输入的匹配度公式如下
\[\\operatorname{Attention}(Q, K, V)=\\operatorname{softmax}(\\operatorname{sim}(Q, K)) V\]Attention目前主要有两种一种是soft attention一种是hard attention。hard attention关注的是当前词附近很小的一个区域而soft attention则是关注了更大更广的范围也更为常用。
作为应用者你了解到Attention的基本原理就足够使用了因为现在已经有了很多基于Attention的预训练模型可以直接使用。如果你想了解更深入可以跟我在留言区和交流群中进一步讨论。
小结
恭喜你完成了今天的学习任务。
今天我们一起学习了语言模型的基本原理,了解了注意力机制如何给模型赋能,让模型更加“善解人意”,提高它抓取文本重点内容的能力。
通过语言模型我们可以将语言文字变成可以计算的形式让文字之间有了更为直接的关联。有了注意力机制我们可以让模型了解到哪些是应该被更加关注的内容从而提高语言模型的效果。如果你对注意力机制的数学原理感兴趣并需要更深层次的专项学习推荐你阅读《Attention Is All You Need》这篇论文。
至此我们通过两节课了解了NLP任务中的基础问题和重要内容接下来的课程中我们即将迎来动手操作环节。
首先是基于LSTM的情感分析项目通过这个项目你可以了解语言模型的构建方法并可以实现一个由情感感知能力构成的模型。之后我们还会使用目前火热的Bert模型来构建一个效果非常给力的文本分类模型敬请期待。
每课一练
词向量的长度多少比较合适呢?越长越好吗?
欢迎你在留言区跟我交流互动,也推荐你把这节课分享给更多同事、朋友。

View File

@ -0,0 +1,360 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 情感分析如何使用LSTM进行情感分析
你好,我是方远。
欢迎来跟我一起学习情感分析,今天我们要讲的就是机器学习里的文本情感分析。文本情感分析又叫做观点提取、主题分析、倾向性分析等。光说概念,你可能会觉得有些抽象,我们一起来看一个生活中的应用,你一看就能明白了。
比方说我们在购物网站上选购一款商品时,首先会翻阅一下商品评价,看看是否有中差评。这些评论信息表达了人们的各种情感色彩和情感倾向性,如喜、怒、哀、乐和批评、赞扬等。像这样根据评价文本,由计算机自动区分评价属于好评、中评或者说差评,背后用到的技术就是情感分析。
如果你进一步观察,还会发现,在好评差评的上方还有一些标签,比如“声音大小合适”、“连接速度快”、“售后态度很好”等。这些标签其实也是计算机根据文本,自动提取的主题或者观点。
情感分析的快速发展得益于社交媒体的兴起自2000年初以来情感分析已经成长为自然语言处理NLP中最活跃的研究领域之一它也被广泛应用在个性化推荐、商业决策、舆情监控等方面。
今天这节课,我们将完成一个情感分析项目,一起来对影评文本做分析。
数据准备
现在我们手中有一批影评数据IMDB数据集影评被分为两类正面评价与负面评价。我们需要训练一个情感分析模型对影评文本进行分类。
这个问题本质上还是一个文本分类问题,研究对象是电影评论类的文本,我们需要对文本进行二分类。下面我们来看一看训练数据。
IMDBInternet Movie Database是一个来自互联网电影数据库其中包含了50000条严重两极分化的电影评论。数据集被划分为训练集和测试集其中训练集和测试集中各有25000条评论并且训练集和测试集都包含50%的正面评论和50%的消极评论。
如何用Torchtext读取数据集
我们可以利用Torchtext工具包来读取数据集。
Torchtext是一个包含常用的文本处理工具和常见自然语言数据集的工具包。我们可以类比之前学习过的Torchvision包来理解它只不过Torchvision包是用来处理图像的而Torchtext则是用来处理文本的。
安装Torchtext同样很简单我们可以使用pip进行安装命令如下
pip install torchtext
Torchtext中包含了上面我们要使用的IMDB数据集并且还有读取语料库、词转词向量、词转下标、建立相应迭代器等功能可以满足我们对文本的处理需求。
更为方便的是Torchtext已经把一些常见对文本处理的数据集囊括在了torchtext.datasets中与Torchvision类似使用时会自动下载、解压并解析数据。
以IMDB为例我们可以用后面的代码来读取数据集
# 读取IMDB数据集
import torchtext
train_iter = torchtext.datasets.IMDB(root='./data', split='train')
next(train_iter)
torchtext.datasets.IMDB函数有两个参数其中
root是一个字符串用于指定你想要读取目标数据集的位置如果数据集不存在则会自动下载
split是一个字符串或者元组表示返回的数据集类型是训练集、测试集或验证集默认是 (train, test)。-
torchtext.datasets.IMDB函数的返回值是一个迭代器这里我们读取了IMDB数据集中的训练集共25000条数据存入了变量train_iter中。
程序运行的结果如下图所示。我们可以看到利用next()函数读取出迭代器train_iter中的一条数据每一行是情绪分类以及后面的评论文本。“neg”表示负面评价“pos”表示正面评价。
数据处理pipelines
读取出了数据集中的评论文本和情绪分类我们还需要将文本和分类标签处理成向量才能被计算机读取。处理文本的一般过程是先分词然后根据词汇表将词语转换为id。
Torchtext为我们提供了基本的文本处理工具包括分词器“tokenizer”和词汇表“vocab”。我们可以用下面两个函数来创建分词器和词汇表。
get_tokenizer函数的作用是创建一个分词器。将文本喂给相应的分词器分词器就可以根据不同分词函数的规则完成分词。例如英文的分词器就是简单按照空格和标点符号进行分词。
build_vocab_from_iterator函数可以帮助我们使用训练数据集的迭代器构建词汇表构建好词汇表后输入分词后的结果即可返回每个词语的id。
创建分词器和构建词汇表的代码如下。首先我们要建立一个可以处理英文的分词器tokenizer然后再根据IMDB数据集的训练集迭代器train_iter建立词汇表vocab。
# 创建分词器
tokenizer = torchtext.data.utils.get_tokenizer('basic_english')
print(tokenizer('here is the an example!'))
'''
输出:['here', 'is', 'the', 'an', 'example', '!']
'''
# 构建词汇表
def yield_tokens(data_iter):
for _, text in data_iter:
yield tokenizer(text)
vocab = torchtext.vocab.build_vocab_from_iterator(yield_tokens(train_iter), specials=["<pad>", "<unk>"])
vocab.set_default_index(vocab["<unk>"])
print(vocab(tokenizer('here is the an example <pad> <pad>')))
'''
输出:[131, 9, 40, 464, 0, 0]
'''
在构建词汇表的过程中yield_tokens函数的作用就是依次将训练数据集中的每一条数据都进行分词处理。另外在构建词汇表时用户还可以利用specials参数自定义词表。
上述代码中我们自定义了两个词语:“”和“”,分别表示占位符和未登录词。顾名思义,未登录词是指没有被收录在分词词表中的词。由于每条影评文本的长度不同,不能直接批量合成矩阵,因此需通过截断或填补占位符来固定长度。
为了方便后续调用我们使用分词器和词汇表来建立数据处理的pipelines。文本pipeline用于给定一段文本返回分词后的id。标签pipeline用于将情绪分类转化为数字即“neg”转化为0“pos”转化为1。
具体代码如下所示。
# 数据处理pipelines
text_pipeline = lambda x: vocab(tokenizer(x))
label_pipeline = lambda x: 1 if x == 'pos' else 0
print(text_pipeline('here is the an example'))
'''
输出:[131, 9, 40, 464, 0, 0 , ... , 0]
'''
print(label_pipeline('neg'))
'''
输出0
'''
通过示例的输出结果相信你很容易就能理解文本pipeline和标签pipeline的用法了。
生成训练数据
有了数据处理的pipelines接下来就是生成训练数据也就是生成DataLoader。
这里还涉及到一个变长数据处理的问题。我们在将文本pipeline所生成的id列表转化为模型能够识别的tensor时由于文本的句子是变长的因此生成的tensor长度不一无法组成矩阵。
这时我们需要限定一个句子的最大长度。例如句子的最大长度为256个单词那么超过256个单词的句子需要做截断处理不足256个单词的句子需要统一补位这里用“/ ”来填补。
上面所说的这些操作我们都可以放到collate_batch函数中来处理。
collate_batch函数有什么用呢它负责在DataLoad提取一个batch的样本时完成一系列预处理工作包括生成文本的tensor、生成标签的tensor、生成句子长度的tensor以及上面所说的对文本进行截断、补位操作。所以我们将collate_batch函数通过参数collate_fn传入DataLoader即可实现对变长数据的处理。
collate_batch函数的定义以及生成训练与验证DataLoader的代码如下。
# 生成训练数据
import torch
import torchtext
from torch.utils.data import DataLoader
from torch.utils.data.dataset import random_split
from torchtext.data.functional import to_map_style_dataset
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def collate_batch(batch):
max_length = 256
pad = text_pipeline('<pad>')
label_list, text_list, length_list = [], [], []
for (_label, _text) in batch:
label_list.append(label_pipeline(_label))
processed_text = text_pipeline(_text)[:max_length]
length_list.append(len(processed_text))
text_list.append((processed_text+pad*max_length)[:max_length])
label_list = torch.tensor(label_list, dtype=torch.int64)
text_list = torch.tensor(text_list, dtype=torch.int64)
length_list = torch.tensor(length_list, dtype=torch.int64)
return label_list.to(device), text_list.to(device), length_list.to(device)
train_iter = torchtext.datasets.IMDB(root='./data', split='train')
train_dataset = to_map_style_dataset(train_iter)
num_train = int(len(train_dataset) * 0.95)
split_train_, split_valid_ = random_split(train_dataset,
[num_train, len(train_dataset) - num_train])
train_dataloader = DataLoader(split_train_, batch_size=8, shuffle=True, collate_fn=collate_batch)
valid_dataloader = DataLoader(split_valid_, batch_size=8, shuffle=False, collate_fn=collate_batch)
我们一起梳理一下这段代码的流程,一共是五个步骤。
1.利用torchtext读取IMDB的训练数据集得到训练数据迭代器-
2.使用to_map_style_dataset函数将迭代器转化为Dataset类型-
3.使用random_split函数对Dataset进行划分其中95%作为训练集5%作为验证集;-
4.生成训练集的DataLoader-
5.生成验证集的DataLoader。
到此为止,数据部分已经全部准备完毕了,接下来我们来进行网络模型的构建。
模型构建
之前我们已经学过卷积神经网络的相关知识。卷积神经网络使用固定的大小矩阵作为输入(例如一张图片),然后输出一个固定大小的向量(例如不同类别的概率),因此适用于图像分类、目标检测、图像分割等等。
但是除了图像之外还有很多信息其大小或长度并不是固定的例如音频、视频、文本等。我们想要处理这些序列相关的数据就要用到时序模型。比如我们今天要处理的文本数据这就涉及一种常见的时间序列模型循环神经网络Recurrent Neural NetworkRNN
不过由于RNN自身的结构问题在进行反向传播时容易出现梯度消失或梯度爆炸。LSTM网络在RNN结构的基础上进行了改进通过精妙的门控制将短时记忆与长时记忆结合起来一定程度上解决了梯度消失与梯度爆炸的问题。
我们使用LSTM网络来进行情绪分类的预测。模型的定义如下。
# 定义模型
class LSTM(torch.nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional,
dropout_rate, pad_index=0):
super().__init__()
self.embedding = torch.nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_index)
self.lstm = torch.nn.LSTM(embedding_dim, hidden_dim, n_layers, bidirectional=bidirectional,
dropout=dropout_rate, batch_first=True)
self.fc = torch.nn.Linear(hidden_dim * 2 if bidirectional else hidden_dim, output_dim)
self.dropout = torch.nn.Dropout(dropout_rate)
def forward(self, ids, length):
embedded = self.dropout(self.embedding(ids))
packed_embedded = torch.nn.utils.rnn.pack_padded_sequence(embedded, length, batch_first=True,
enforce_sorted=False)
packed_output, (hidden, cell) = self.lstm(packed_embedded)
output, output_length = torch.nn.utils.rnn.pad_packed_sequence(packed_output)
if self.lstm.bidirectional:
hidden = self.dropout(torch.cat([hidden[-1], hidden[-2]], dim=-1))
else:
hidden = self.dropout(hidden[-1])
prediction = self.fc(hidden)
return prediction
网络模型的具体结构首先是一个Embedding层用来接收文本id的tensor然后是LSTM层最后是一个全连接分类层。其中bidirectional为True表示网络为双向LSTMbidirectional为False表示网络为单向LSTM。
网络模型的结构图如下所示。
模型训练与评估
定义好网络模型的结构,我们就可以进行模型训练了。首先是实例化网络模型,参数以及具体的代码如下。
# 实例化模型
vocab_size = len(vocab)
embedding_dim = 300
hidden_dim = 300
output_dim = 2
n_layers = 2
bidirectional = True
dropout_rate = 0.5
model = LSTM(vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout_rate)
model = model.to(device)
由于数据的情感极性共分为两类因此这里我们要把output_dim的值设置为2。-
接下来是定义损失函数与优化方法,代码如下。在之前的课程里也多次讲过了,所以这里不再重复。
# 损失函数与优化方法
lr = 5e-4
criterion = torch.nn.CrossEntropyLoss()
criterion = criterion.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
计算loss的代码如下。
import tqdm
import sys
import numpy as np
def train(dataloader, model, criterion, optimizer, device):
model.train()
epoch_losses = []
epoch_accs = []
for batch in tqdm.tqdm(dataloader, desc='training...', file=sys.stdout):
(label, ids, length) = batch
label = label.to(device)
ids = ids.to(device)
length = length.to(device)
prediction = model(ids, length)
loss = criterion(prediction, label) # loss计算
accuracy = get_accuracy(prediction, label)
# 梯度更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_losses.append(loss.item())
epoch_accs.append(accuracy.item())
return epoch_losses, epoch_accs
def evaluate(dataloader, model, criterion, device):
model.eval()
epoch_losses = []
epoch_accs = []
with torch.no_grad():
for batch in tqdm.tqdm(dataloader, desc='evaluating...', file=sys.stdout):
(label, ids, length) = batch
label = label.to(device)
ids = ids.to(device)
length = length.to(device)
prediction = model(ids, length)
loss = criterion(prediction, label) # loss计算
accuracy = get_accuracy(prediction, label)
epoch_losses.append(loss.item())
epoch_accs.append(accuracy.item())
return epoch_losses, epoch_accs
可以看到这里训练过程与验证过程的loss计算分别定义在了train函数和evaluate函数中。主要区别是训练过程有梯度的更新而验证过程中不涉及梯度的更新只计算loss即可。-
模型的评估我们使用ACC也就是准确率作为评估指标计算ACC的代码如下。
def get_accuracy(prediction, label):
batch_size, _ = prediction.shape
predicted_classes = prediction.argmax(dim=-1)
correct_predictions = predicted_classes.eq(label).sum()
accuracy = correct_predictions / batch_size
return accuracy
最后训练过程的具体代码如下。包括计算loss和ACC、保存losses列表和保存最优模型。
n_epochs = 10
best_valid_loss = float('inf')
train_losses = []
train_accs = []
valid_losses = []
valid_accs = []
for epoch in range(n_epochs):
train_loss, train_acc = train(train_dataloader, model, criterion, optimizer, device)
valid_loss, valid_acc = evaluate(valid_dataloader, model, criterion, device)
train_losses.extend(train_loss)
train_accs.extend(train_acc)
valid_losses.extend(valid_loss)
valid_accs.extend(valid_acc)
epoch_train_loss = np.mean(train_loss)
epoch_train_acc = np.mean(train_acc)
epoch_valid_loss = np.mean(valid_loss)
epoch_valid_acc = np.mean(valid_acc)
if epoch_valid_loss < best_valid_loss:
best_valid_loss = epoch_valid_loss
torch.save(model.state_dict(), 'lstm.pt')
print(f'epoch: {epoch+1}')
print(f'train_loss: {epoch_train_loss:.3f}, train_acc: {epoch_train_acc:.3f}')
print(f'valid_loss: {epoch_valid_loss:.3f}, valid_acc: {epoch_valid_acc:.3f}')
我们还可以利用保存下来train_losses列表绘制训练过程中的loss曲线或使用第15课讲过的可视化工具来监控训练过程-
至此一个完整的情感分析项目已经完成了从数据读取到模型构建与训练的方方面面我都手把手教给了你希望你能以此为模板独立解决自己的问题
小结
恭喜你完成了今天的学习任务今天我们一起完成了一个情感分析项目的实践相当于是对自然语言处理任务的一个初探我带你回顾一下今天学习的要点
在数据准备阶段我们可以使用PyTorch提供的文本处理工具包Torchtext想要掌握Torchtext也不难我们可以类比之前详细介绍过的Torchvision不懂的地方再对应去查阅文档相信你一定可以做到举一反三
模型构建时要根据具体的问题选择适合的神经网络卷积神经网络常被用于处理图像作为输入的预测问题循环神经网络常被用于处理变长的序列相关的数据而LSTM相较于RNN能更好地解决梯度消失与梯度爆炸的问题
在后续的课程中我们还会讲解两大自然语言处理任务文本分类和摘要生成它们分别包括了判别模型和生成模型相信那时你一定会在文本处理方面有更深层次的理解
每课一练
利用今天训练的模型编写一个函数predict_sentiment实现输入一句话输出这句话的情绪类别与概率
例如
text = "This film is terrible!"
predict_sentiment(text, model, tokenizer, vocab, device)
'''
输出('neg', 0.8874172568321228)
'''
欢迎你在留言区跟我交流互动也推荐你把今天学到的内容分享给更多朋友跟他一起学习进步

View File

@ -0,0 +1,293 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 文本分类如何使用BERT构建文本分类模型
你好,我是方远。
在第22节课我们一起学习了不少文本处理方面的理论其实文本分类在机器学习领域的应用也非常广泛。
比如说你现在是一个NLP研发工程师老板啪地一下甩给你一大堆新闻文本数据它们可能来源于不同的领域比如体育、政治、经济、社会等类型。这时我们就需要对文本分类处理方便用户快速查询自己感兴趣的内容甚至按用户的需要定向推荐某类内容。
这样的需求就非常适合用PyTorch + BERT处理。为什么会选择BERT呢因为BERT是比较典型的深度学习NLP算法模型也是业界使用最广泛的模型之一。接下来我们就一起来搭建这个文本分类模型相信我它的效果表现非常强悍。
问题背景与分析
正式动手之前,我们不妨回顾一下历史。文本分类问题有很多经典解决办法。
开始时就是最简单粗暴的关键词统计方法。之后又有了基于贝叶斯概率的分类方法,通过某些条件发生的概率推断某个类别的概率大小,并作为最终分类的决策依据。尽管这个思想很简单,但是意义重大,时至今日,贝叶斯方法仍旧是非常多应用场景下的好选择。
之后还有支持向量机SVM很长一段时间其变体和应用都在NLP算法应用的问题场景下占据统治地位。
随着计算设备性能的提升、新的算法理论的产生等进步一大批的诸如随机森林、LDA主题模型、神经网络等方法纷纷涌现可谓百家争鸣。
既然有这么多方法为什么这里我们这里推荐选用BERT呢
因为在很多情况下尤其是一些复杂场景下的文本像BERT这样具有强大处理能力的工具才能应对。比如说新闻文本就不好分类因为它存在后面这些问题。
1.类别多。在新闻资讯App中新闻的种类是非常多的需要产品经理按照统计、实用的原则进行文章分类体系的设计使其类别能够覆盖所有的文本一般来说都有50种甚至以上。不过为了让你把握重点咱们先简化问题假定文本的分类体系已经确定。
2.数据不平衡。不难理解,在新闻中,社会、经济、体育、娱乐等类别的文章数量相对来说是比较多的,占据了很大的比例;而少儿、医疗等类别则相对较少,有的时候一天也没有几篇对应的文章。
3.多语言。一般来说,咱们主要的语言除了中文,应该是大多数人只会英语了,不过为了考虑到新闻来源的广泛性,咱们也假定这批文本是多语言的。
刚才提到了因为Bert是比较典型的深度学习NLP算法模型也是业界使用最广泛的模型之一。如果拿下这么有代表性的模型以后你学习和使用基于Attention的模型你也能举一反三比如GPT等。
想要用好BERT我们需要先了解它有哪些特点。
BERT原理与特点分析
BERT的全称是Bidirectional Encoder Representation from Transformers即双向Transformer的Encoder。作为一种基于Attention方法的模型它最开始出现的时候可以说是抢尽了风头在文本分类、自动对话、语义理解等十几项NLP任务上拿到了历史最好成绩。
在第22节课如果不熟悉可以回看我们已经了解了Attention的基本原理有了这个知识做基础我们很容易就能快速掌握BERT的原理。
这里我再快速给你回顾一下BERT的理论框架主要是基于论文《Attention is all you need》中提出的Transformer而后者的原理则是刚才提到的Attention。其最为明显的特点就是摒弃了传统的RNN和CNN逻辑有效解决了NLP中的长期依赖问题。
在BERT中它的输入部分也就是图片的左边其实是由N个多头Attention组合而成。多头Attention是将模型分为多个头形成多个子空间可以让模型去关注不同方面的信息这有助于网络捕捉到更丰富的特征或者信息。具体原理一定要查阅《Attention is all you need》哦
结合上图我们要注意的是BERT采用了基于MLM的模型训练方式即Mask Language Model。因为BERT是Transformer的一部分即encoder环节所以没有decoder的部分其实就是GPT
为了解决这个问题MLM方式应运而生。它的思想也非常简单就是在训练之前随机将文本中一部分的词语token进行屏蔽mask然后在训练的过程中使用其他没有被屏蔽的token对被屏蔽的token进行预测。
用过Word2Vec的小伙伴应该比较清楚在Word2Vec中对于同一个词语它的向量表示是固定的这也就是为什么会有那个经典的“_国王-男人+女人=皇后_”计算式了。
但是有一个问题“苹果”这个词有可能是水果的苹果也可能是电子产品的品牌如果还是用同一个向量表示这样就有可能产生偏差。而在BERT中则不一样根据上下文的不同对于同一个token给出的词向量是动态变化的更加灵活。
此外BERT还有多语言的优势。在以前的算法中比如SVM如果要做多语言的模型就要涉及分词、提取关键词等操作而这些操作要求你对该语言有所了解。像阿拉伯文、日语等语言咱们大概率是看不懂的这会对我们最后的模型效果产生极大影响。
BERT则不需要担心这个问题通过基于字符、字符片段、单词等不同粒度的token覆盖并作WordPiece能够覆盖上百种语言甚至可以说只要你能够发明出一种逻辑上自洽的语言BERT就能够处理。有关WordPiece的介绍你可以通过这里做拓展阅读。
说了这么多集高效、准确、灵活再加上用途广泛于一体的BERT自然而然就成为了咱们的首选下面咱们开始正式构建一个文本分类模型。
安装与准备
工欲善其事,必先利其器,在开始构建模型之前,我们要安装相应的工具,然后下载对应的预先训练好的模型,同时还要了解数据的格式。
环境准备
因为咱们要做的是一个基于PyTorch 的BERT模型那么就要安装对应的python包这里我选择的是hugging face的PyTorch版本的Transformers包。你可以通过pip命令直接安装。
pip install Transformers
模型准备
安装之后我们打开Transformers的git页面并找到如下的文件夹。
src/Transformers/models/BERT
从这个文件夹里我们需要找到两个很重要的文件分别是convert_BERT_original_tf2_checkpoint_to_PyTorch.py和modeling_BERT.py文件。
先来看第一个文件你看看名字是不是就能猜出来它大概是用来做什么的了没错就是用来将原来通过TensorfFlow预训练的模型转换为PyTorch的模型。
然后是modeling_BERT.py文件这个文件实际上是给了你一个使用BERT的范例。
下面,咱们开始准备模型,打开这个地址,你会发现在这个页面中,有几个预训练好的模型。
对照这节课的任务我们选择的是“BERT-Base, Multilingual Cased”的版本。从GitHub的介绍可以看出这个版本的checkpoint支持104种语言是不是很厉害当然如果你没有多语言的需求也可以选择其他版本的它们的区别主要是网络的体积不同。
转换完模型之后你会发现你的本地多了三个文件分别是config.json、pytorch_model.bin和vocab.txt。我来分别给你说一说。
1.config.json顾名思义该文件就是BERT模型的配置文件里面记录了所有用于训练的参数设置。
2.PyTorch_model.bin模型文件本身。
3.vocab.txt词表文件。尽管BERT可以处理一百多种语言但是它仍旧需要词表文件用于识别所支持语言的字符、字符串或者单词。
格式准备
现在模型准备好了我们还要看看跟模型匹配的格式。BERT的输入不算复杂但是也需要了解其形式。在训练的时候我们输入的数据不能是直接把词塞到模型里而是要转化成后面这三种向量。
1.Token embeddings词向量。这里需要注意的是Token embeddings的第一个开头的token一定得是“[CLS]”。[CLS]作为整篇文本的语义表示,用于文本分类等任务。
2.Segment embeddings。这个向量主要是用来将两句话进行区分比如问答任务会有问句和答句同时输入这就需要一个能够区分两句话的操作。不过在咱们此次的分类任务中只有一个句子。
3.Position embeddings。记录了单词的位置信息。
模型构建
准备工作已经一切就绪我们这就来搭建一个基于BERT的文本分类网络模型。这包括了网络的设计、配置、以及数据准备这个过程也是咱们的核心过程。
网络设计
从上面提到的modeling_BERT.py文件中我们可以看到作者实际上已经给我们提供了很多种类的NLP任务的示例代码咱们找到其中的“BERTForSequenceClassification”这个分类网络我们可以直接使用它也是最最基础的BERT文本分类的流程。
这个过程包括了利用BERT得到文本的embedding表示、将embedding放入全连接层得到分类结果两部分。我们具体看一下代码。
class BERTForSequenceClassification(BERTPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels//类别标签数量
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)//还记得Dropout是用来做什么的吗可以一定程度防止过拟合。
self.classifier = nn.Linear(config.hidden_size, config.num_labels)//BERT输出的embedding传入一个MLP层做分类。
self.init_weights()
def forward(
self,
input_ids=None,
attention_mask=None,
token_type_ids=None,
position_ids=None,
head_mask=None,
inputs_embeds=None,
labels=None,
output_attentions=None,
output_hidden_states=None,
return_dict=None,
):
outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
pooled_output = outputs[1]//这个就是经过BERT得到的中间输出。
pooled_output = self.dropout(pooled_output)//对,就是为了减少过拟合和增加网络的健壮性。
logits = self.classifier(pooled_output)//多层MLP输出最后的分类结果。
对照前面的代码可以发现接收到输入信息之后BERT返回了一个outputsoutputs包括了模型计算之后的全部结果不仅有每个token的信息也有整个文本的信息这个输出具体包括以下信息。
last_hidden_state是模型最后一层输出的隐藏层状态序列。shape是(batch_size, sequence_length, hidden_size)。其中hidden_size=768这个部分的状态就相当于利用sequence_length * 768维度的矩阵记录了整个文本的计算之后的每一个token的结果信息。
pooled_output代表序列的第一个token的最后一个隐藏层的状态。shape是(batch_size, hidden_size)。所谓的第一个token就是咱们刚才提到的[CLS]标签。
除了上面两个信息还有hidden_states、attentions、cross attentions。有兴趣的小伙伴可以去查一下它们有何用途。
通常的任务中我们用得比较多的是last_hidden_state对应的信息我们可以用pooled_output = outputs[1]来进行获取。
至此我们已经有了经过BERT计算的文本向量表示然后我们将其输入到一个linear层中进行分类就可以得到最后的分类结果了。为了提高模型的表现我们往往会在linear层之前加入一个dropout层这样可以减少网络的过拟合的可能性同时增强神经元的独立性。
模型配置
设计好网络我们还要对模型进行配置。还记得刚才提到的config.json文件么这里面就记录了BERT模型所需的所有配置信息我们需要对其中的几个内容进行调整这样模型就能知道我们到底是要做什么事情了。
后面这几个字段我专门说一下。
id2label这个字段记录了类别标签和类别名称的映射关系。
label2id这个字段记录了类别名称和类别标签的映射关系。
num_labels_cate类别的数量。
数据准备
模型网络设计好了配置文件也搞定了下面我们就要开始数据准备这一步了。这里的数据准备是指将文本转换为BERT能够识别的形式即前面提到的三种向量在代码中对应的就是input_ids、token_type_ids、attention_mask。
为了生成这些数据我们需要在git中找到“src/Transformers/data/processors/utils.py”文件在这个文件中我们要用到以下几个内容。
1.InputExample它用于记录单个训练数据的文本内容的结构。
2.DataProcessor通过这个类中的函数我们可以将训练数据集的文本表示为多个InputExample组成的数据集合。
3.get_features用于把InputExample数据转换成BERT能够理解的数据结构的关键函数。我们具体来看一下各个数据都怎么生成的。
input_ids记录了输入token对应在vocab.txt的id序号它是通过如下的代码得到的。
input_ids = tokenizer.encode(
example.text_a,
add_special_tokens=True,
max_length=min(max_length, tokenizer.max_len),
)
而attention_mask记录了属于第一个句子的token信息通过如下代码得到。
attention_mask = [1 if mask_padding_with_zero else 0] * len(input_ids)
attention_mask = attention_mask + ([0 if mask_padding_with_zero else 1] * padding_length)
另外不要忘记记录文本类别的信息label。你可以自己想想看能否按照utils.py文件中的声明方式构建出对应的label信息呢
模型训练
到目前为止我们有了网络结构定义BERTForSequenceClassification、数据集合get_features现在就可以开始编写实现训练过程的代码了。
选择优化器
首先我们来选择优化器代码如下。我们要对网络中的所有权重参数进行设置这样优化器就可以知道哪些参数是要进行优化的。然后我们将参数list放到优化器中BERT使用的是AdamW优化器。
param_optimizer = list(model.named_parameters())
no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
{'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
{'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]
optimizer = AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)
这部分的代码,主要是为了选择一个合适咱们模型任务的优化器,并将网络中的参数设定好学习率。
构建训练过程逻辑
训练的过程逻辑是非常简单的只需要两个for循环分别代表epoch和batch然后在最内部增加一个训练核心语句以及一个梯度更新语句这就足够了。可以看到PyTorch在工程代码的实现上封装得非常完善和简练。
for epoch in trange(0, args.num_train_epochs):
model.train()//一定别忘了要把模型设置为训练状态。
for step, batch in enumerate(tqdm(train_dataLoader, desc='Iteration')):
step_loss = training_step(batch)//训练的核心环节
tr_loss += step_loss[0]
optimizer.step()
optimizer.zero_grad()
训练的核心环节
训练的核心环节你需要关注两个部分分别是通过网络得到预测输出也就是logits以及基于logits计算得到的lossloss是整个模型使用梯度更新需要用到的数据。
def training_step(batch):
input_ids, token_type_ids, attention_mask, labels = batch
input_ids = input_ids.to(device)//将数据发送到GPU
token_type_ids = token_type_ids.to(device)
attention_mask = attention_mask.to(device)
labels = labels_voc.to(device)
logits = model(input_ids,
token_type_ids=token_type_ids,
attention_mask=attention_mask,
labels=labels)
loss_fct = BCEWithLogitsLoss()
loss = loss_fct(logits.view(-1, num_labels_cate), labels.view(-1, num_labels_cate).float())
loss.backward()
至此咱们已经快速构建出了一个BERT分类器所需的所有关键代码。但是仍旧有一些小小的环节需要你来完善比如training_step代码块中的device是怎么得到的呢回顾一下咱们之前学习的内容相信你一定可以做得到。
小结
恭喜你完成了这节课的学习尽管现在GitHub上已经有了很多已经封装得非常完善的BERT代码你也可以很快实现一个最基本的NLP算法流程但是我仍希望你能够抽出时间好好看一下Transformer中的模型代码这会对你的技术提升有非常大的助益。
这节课我们学习了如何用PyTorch快速构建一个基本的文本分类模型想要实现这个过程你需要了解BERT的预训练模型的获取以及转化、分类网络的设计方法、训练过程的编写。整个过程不难但是却可以让你快速上手了解PyTorch在NLP方面如何应用。
除了技术本身,业务方面的考虑我们也要注意。比如新闻文本的多语言、数据不平衡等问题,模型有时不能解决所有的问题,因此你还需要学习一些数据预处理的技巧,这包括很多技术和算法方面的内容。
即使我列出一份长长的学习清单也可能会挂一漏万所以数据预处理方面的知识我建议你重点关注以下内容建议你需要花一些时间去学习NumPy和Pandas的使用这样才能更加得心应手地处理数据你还可以多学习一些常见的数据挖掘算法比如决策树、KNN、支持向量机等另外深度学习的广泛使用其实仍旧非常需要传统机器学习算法的背后支撑也建议你多多了解。
思考题
BERT处理文本是有最大长度要求的512那么遇到长文本该怎么办呢
也欢迎你在留言区记录你的疑问或者收获,也推荐你把这节课分享给你的朋友。

View File

@ -0,0 +1,307 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 摘要:如何快速实现自动文摘生成?
你好,我是方远。
当我们打开某个新闻APP或者某个网站时常常被这样的标题所吸引“震惊了十亿人”、“一定要读完跟你的生命有关”等。但是当我们点进去却发现都是标题党实际内容大相径庭这时候你可能会想如果有一种工具能帮助我们提炼文章的关键内容那我们就不会再受到标题党的影响了。其实想要实现这个工具并不复杂用自动文摘技术就能解决。
自动文摘充斥着我们生活的方方面面它可用于热点新闻聚合、新闻推荐、语音播报、APP消息Push、智能写作等场景。今天我们要讲的这个自然语言处理任务就是自动文摘生成。
问题背景
自动文摘技术,就是自动提炼出一些句子来概括整篇文章的大意,用户通过读摘要就可以了解到原文要表达的意思。
抽取与生成
自动文摘有两种解决方案一种是抽取式Extractive就是从原文中提取一些关键的句子组合成一篇摘要另外一种是生成式Abstractive也是这节课我们重点要讲的内容这种方式需要计算机通读原文后在理解整篇文章内容的基础上使用简短连贯的语言将原文的主要内容表达出来即会产生原文中没有出现的词和句子。
现阶段,抽取式的摘要目前已经相对成熟,但是抽取质量及内容流畅度都不够理想。随着深度学习的研究,生成式摘要的质量和流畅度都有很大提升,但目前也受到原文本长度过长、抽取内容不佳等限制,生成的摘要与人工摘要相比,还有相当的差距。
语言的表达方式多种多样,机器生成的摘要可能和人工摘要并不相同,那么如何衡量自动摘要的好坏呢?这就涉及到摘要的评价指标。
评价指标
评价自动摘要的效果通常使用 ROUGERecall Oriented Understudy for Gisting Evaluation评价。
ROUGE评价法参考了机器翻译自动评价方法并且考虑了N-gram共同出现的程度。这个方法具体是这样设计的首先由多个专家分别生成人工摘要构成标准摘要集然后对比系统生成的自动摘要与人工生成的标准摘要通过统计二者之间重叠的基本单元n元语法、词序或词对的数目来评价摘要的质量。通过与多专家人工摘要的对比提高评价系统的稳定性和健壮性。
ROUGE主要包括以下4种评价指标
1.ROUGE-N基于n-gram的共现统计-
2.ROUGE-L基于最长公共子串-
3.ROUGE-S基于顺序词对统计-
4.ROUGE-W在ROUGE-L的基础上考虑串的连续匹配。-
了解了自动文摘的种类与评价指标下面我们再来认识一个用于自动文摘生成的模型——BART。它的名字和上节课讲过的BERT非常像我们先来看看它有哪些特点。
BART原理与特点分析
BART的全称是Bidirectional and Auto-Regressive Transformers双向自回归变压器。它是由 Facebook AI 在2019年提出的一个新的预训练模型结合了双向Transformer和自回归Transformer在文本生成相关任务中达到了SOTA的结果。你可以通过这个链接查看相关论文。
我们已经熟知了论文《Attention is all you need》中提出的Transformer。Transformer左半边为Encoder右半边为Decoder。Encoder和Decoder的结构分别如下图ab所示。
Encoder负责将原始文本进行self-attention并获得句子中每个词的词向量最经典的 Encoder架构就是上节课所学习的BERT但是单独Encoder结构不适用于文本生成任务。
Decoder的输入与输出之间错开一个位置这是为了模拟文本生成时不能让模型看到未来的词这种方式称为Auto-Regressive自回归。例如GPT等基于Decoder结构的模型通常适用于做文本生成任务但是无法学习双向的上下文语境信息。
BART模型就是将Encoder和Decoder结合在一起的一种sequence-to-sequence结构它的主要结构如下图所示。
BART模型的结构看似与Transformer没什么不同主要区别在于BART的预训练阶段。首先在Encoder端使用多种噪声对原始文本进行破坏然后再使用Decoder 重建原始文本。
由于BART本身就是在sequence-to-sequence的基础上构建并且进行预训练它天然就适合做序列生成的任务例如问答、文本摘要、机器翻译等。在生成任务上获得进步的同时在一些文本理解类任务上它也可以取得很好的效果。
下面我们进入实战阶段利用BART来实现自动文摘生成。
快速文摘生成
这里我们还是使用hugging face的Transformers工具包。具体的安装过程上一节课已经介绍过了。
Transformers工具包为快速使用自动文摘生成模型提供了pipeline API。pipeline聚合了文本预处理步骤与训练好的自动文摘生成模型。利用Transformers的pipeline我们只需短短几行代码就可以快速生成文本摘要。
下面是一个使用pipeline生成文摘的例子代码如下。
from transformers import pipeline
summarizer = pipeline("summarization")
ARTICLE = """ New York (CNN)When Liana Barrientos was 23 years old, she got married in Westchester County, New York.
A year later, she got married again in Westchester County, but to a different man and without divorcing her first husband.
Only 18 days after that marriage, she got hitched yet again. Then, Barrientos declared "I do" five more times, sometimes only within two weeks of each other.
In 2010, she married once more, this time in the Bronx. In an application for a marriage license, she stated it was her "first and only" marriage.
Barrientos, now 39, is facing two criminal counts of "offering a false instrument for filing in the first degree," referring to her false statements on the
2010 marriage license application, according to court documents.
Prosecutors said the marriages were part of an immigration scam.
On Friday, she pleaded not guilty at State Supreme Court in the Bronx, according to her attorney, Christopher Wright, who declined to comment further.
After leaving court, Barrientos was arrested and charged with theft of service and criminal trespass for allegedly sneaking into the New York subway through an emergency exit, said Detective
Annette Markowski, a police spokeswoman. In total, Barrientos has been married 10 times, with nine of her marriages occurring between 1999 and 2002.
All occurred either in Westchester County, Long Island, New Jersey or the Bronx. She is believed to still be married to four men, and at one time, she was married to eight men at once, prosecutors say.
Prosecutors said the immigration scam involved some of her husbands, who filed for permanent residence status shortly after the marriages.
Any divorces happened only after such filings were approved. It was unclear whether any of the men will be prosecuted.
The case was referred to the Bronx District Attorney\'s Office by Immigration and Customs Enforcement and the Department of Homeland Security\'s
Investigation Division. Seven of the men are from so-called "red-flagged" countries, including Egypt, Turkey, Georgia, Pakistan and Mali.
Her eighth husband, Rashid Rajput, was deported in 2006 to his native Pakistan after an investigation by the Joint Terrorism Task Force.
If convicted, Barrientos faces up to four years in prison. Her next court appearance is scheduled for May 18.
"""
print(summarizer(ARTICLE, max_length=130, min_length=30))
'''
输出:
[{'summary_text': ' Liana Barrientos, 39, is charged with two counts of "offering a false instrument for filing in
the first degree" In total, she has been married 10 times, with nine of her marriages occurring between 1999 and
2002 . At one time, she was married to eight men at once, prosecutors say .'}]
'''
第3行代码的作用是构建一个自动文摘的pipelinepipeline会自动下载并缓存训练好的自动文摘生成模型。这个自动文摘生成模型是BART模型在CNN/Daily Mail数据集上训练得到的。
第5~22行代码是待生成摘要的文章原文。第24行代码是针对文摘原文自动生成文摘其中参数max_length和min_length限制了文摘的最大和最小长度输出的结果如上面代码注释所示。
如果你不想使用Transformers提供的预训练模型而是想使用自己的模型或其它任意模型也很简单。具体代码如下。
from transformers import BartTokenizer, BartForConditionalGeneration
model = BartForConditionalGeneration.from_pretrained('facebook/bart-large-cnn')
tokenizer = BartTokenizer.from_pretrained('facebook/bart-large-cnn')
inputs = tokenizer([ARTICLE], max_length=1024, return_tensors='pt')
# 生成文摘
summary_ids = model.generate(inputs['input_ids'], max_length=130, early_stopping=True)
summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
print(summary)
流程是一共包括四步,我们分别看一下。-
第一步是实例化一个BART的模型和分词器对象。BartForConditionalGeneration类是BART模型用于摘要生成的类BartTokenizer是BART的分词器它们都有from_pretrained()方法,可以加载预训练模型。
from_pretrained()函数需要传入一个字符串作为参数这个字符串可以是本地模型的路径也可以是上传到Hugging Face模型库中的模型名字。
这里“facebook/bart-large-cnn”是Facebook利用CNN/Daily Mail数据集训练的BART模型模型具体细节你可以参考这里。
接下来是第二步对原始文本进行分词。我们可以利用分词器对象tokenizer对原始文本ARTICLE进行分词并得到词语id的Tensor。return_tensors=pt表示返回值是PyTorch的Tensor。
第三步使用generate()方法生成摘要。其中参数max_length限制了生成摘要的最大长度early_stopping表示生成过程是否可提前停止。generate()方法的输出是摘要词语的id。
最后一步利用分词器解码得到最终的摘要文本。利用tokenizer.decode()函数将词语id转换为词语文本。其中参数skip_special_tokens表示是否去掉“ ”、”<\s>“等一些特殊token。
Fine-tuning BART
下面我们来看一看如何用自己的数据集来训练BART模型。
模型加载
模型加载部分和之前讲的一样不再过多重复。这里我们要利用BartForConditionalGeneration类的from_pretrained()函数加载一个BART模型。
模型加载的代码如下。这里我们会在Facebook训练好的摘要模型上继续Fine-tuning。
from transformers import BartTokenizer, BartForConditionalGeneration
tokenizer = BartTokenizer.from_pretrained('facebook/bart-large-cnn')
model = BartForConditionalGeneration.from_pretrained('facebook/facebook/bart-large-cnn')
数据准备
接下来是数据准备。我们先来回顾一下之前学习过的读取文本数据集的方式。在第6课中我们学习过使用PyTorch原生的的Dataset类读取数据集在第23课中我们学习了使用Torchtext工具torchtext.datasets来读取数据集。今天我们还要学习一种新的数据读取工具Datasets库。
Datasets库也是由hugging face团队开发的旨在轻松访问与共享数据集。官方的文档在这里有兴趣了解更多的同学可以去看看。
Datasets库的安装同样非常简单。可以使用pip安装
pip install datasets
或使用conda进行安装
conda install -c huggingface -c conda-forge datasets
Datasets库中同样包括常见数据集而且帮我们封装好了读取数据集的操作。我们来看一个读取IMDB数据集第23课讲过的训练数据的示例
import datasets
train_dataset = datasets.load_dataset("imdb", split="train")
print(train_dataset.column_names)
'''
输出:
['label', 'text']
'''
用load_dataset()函数来加载数据集它的参数是数据集的名字或本地文件的路径split参数用于指定加载训练集、测试集或验证集。
我们还可以从不止一个csv文件中加载数据
data_files = {"train": "train.csv", "test": "test.csv"}
dataset = load_dataset("namespace/your_dataset_name", data_files=data_files)
print(datasets)
'''
示例输出:(实际输出与此不同)
{train: Dataset({
features: ['idx', 'text', 'summary'],
num_rows: 3668
})
test: Dataset({
features: ['idx', 'text', 'summary'],
num_rows: 1725
})
}
'''
通过参数data_files指定训练集、测试集或验证集所需加载的文件路径即可。-
我们可以使用map()函数来对数据集进行一些预处理操作,示例如下:
def add_prefix(example):
example['text'] = 'My sentence: ' + example['text']
return example
updated_dataset = dataset.map(add_prefix)
updated_dataset['train']['text'][:5]
'''
示例输出:
['My sentence: Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
"My sentence: Yucaipa owned Dominick 's before selling the chain to Safeway in 1998 for $ 2.5 billion .",
'My sentence: They had published an advertisement on the Internet on June 10 , offering the cargo for sale , he added .',
'My sentence: Around 0335 GMT , Tab shares were up 19 cents , or 4.4 % , at A $ 4.56 , having earlier set a record high of A $ 4.57 .',
]
'''
我们首先定义了一个add_prefix()函数其作用是为数据集的“text”字段加上一个前缀“My sentence: ”。然后调用数据集dataset的map方法可以看到输出中“text”字段的内容前面都增加了指定前缀。
下面我们来看一看使用自定义的数据集fine-tuning BART模型应该怎么做。具体的代码如下
from transformers.modeling_bart import shift_tokens_right
dataset = ... # Datasets的对象数据集需有'text'和'summary'字段,并包含训练集和验证集
def convert_to_features(example_batch):
input_encodings = tokenizer.batch_encode_plus(example_batch['text'], pad_to_max_length=True, max_length=1024, truncation=True))
target_encodings = tokenizer.batch_encode_plus(example_batch['summary'], pad_to_max_length=True, max_length=1024, truncation=True))
labels = target_encodings['input_ids']
decoder_input_ids = shift_tokens_right(labels, model.config.pad_token_id)
labels[labels[:, :] == model.config.pad_token_id] = -100
encodings = {
'input_ids': input_encodings['input_ids'],
'attention_mask': input_encodings['attention_mask'],
'decoder_input_ids': decoder_input_ids,
'labels': labels,
}
return encodings
dataset = dataset.map(convert_to_features, batched=True)
columns = ['input_ids', 'labels', 'decoder_input_ids','attention_mask',]
dataset.set_format(type='torch', columns=columns)
首先需要加载自定义的数据集你要注意的是这个数据集需要包含原文和摘要两个字段并且包含训练集和验证集。加载数据集的方法可以用我们刚刚讲过的load_dataset()函数。
由于加载的数据需要经过一系列预处理操作比如通过分词器进行分词等等的处理后才能送入到模型中因此我们需要定义一个函数convert_to_features()来处理原文和摘要文本。
convert_to_features()函数中的主要操作就是调用tokenizer来将文本转化为词语id。需要注意的是代码第10行中有一个shift_tokens_right()函数它的作用就是我们在原理中介绍过的Auto-Regressive目的是将Decoder的输入向后移一个位置。
然后我们需要调用dataset.map()函数来对数据集进行预处理操作参数batched=True表示支持在batch数据上操作。
最后再利用set_format()函数生成选择训练所需的数据字段并生成PyTroch的Tensor。到这里数据准备的工作就告一段落了。
模型训练
做好了前面的准备工作最后我们来看模型训练部分。Transformers工具已经帮我们封装了用于训练文本生成模型的Seq2SeqTrainer类无需我们自己再去定义损失函数与优化方法了。
具体的训练代码如下。
from transformers import Seq2SeqTrainingArguments, Seq2SeqTrainer
training_args = Seq2SeqTrainingArguments(
output_dir='./models/bart-summarizer',# 模型输出目录
num_train_epochs=1, # 训练轮数
per_device_train_batch_size=1, # 训练过程bach_size
per_device_eval_batch_size=1, # 评估过程bach_size
warmup_steps=500, # 学习率相关参数
weight_decay=0.01, # 学习率相关参数
logging_dir='./logs', # 日志目录
)
trainer = Seq2SeqTrainer(
model=model,
args=training_args,
train_dataset=dataset['train'],
eval_dataset=dataset['validation']
)
trainer.train()
首先我们要定义一个训练参数的对象关于训练的相关参数都通过Seq2SeqTrainingArguments类进行定义。然后再实例化一个Seq2SeqTrainer类的对象将模型和训练数据作为参数传入其中。最后调用train()方法,即可一键开始训练。
小结
恭喜你完成了今天的学习任务同时也完成了PyTorch的全部学习内容。
这节课我们先一起了解了BART模型的原理与特点这个模型是一个非常实用的预训练模型能够帮助我们实现文本摘要生成。然后我们结合实例学习了如何用PyTorch快速构建一个自动文摘生成项目包括利用Transformers的pipeline快速生成文本摘要和Fine-tuning BART模型。
因为BART模型具有自回归Transformer的结构所以它不只可以用于摘要生成还适用于其它文本生成类的项目例如机器翻译、对话生成等。相信理解了它的基本原理与模型Fine-tuning的基本流程你可以很容易地利用BART完成文本生成类的任务期待你举一反三亲手做更多的实验。
通过实战篇的学习我们一共探讨、实现了2个图像项目和3个自然语言处理项目。如何基于 PyTorch 搭建自己的深度学习网络相信你已经了然于胸了。当我们解决实际的问题时首先要从原理出发选择适合的模型PyTorch只是一个工具辅助我们实现自己需要的网络。
除了自动摘要外其他四个项目的共通思路都是把问题转化为分类问题。图像、文本分类不必细说图像分割其实是判别一个像素是属于哪一个类别情感分析则是判别文本是积极类还是消极类。而自动摘要则是生成模型通常是基于sequence-to-sequence的结构来实现。
这些是我通过一系列的实战训练,最终希望你领会到的模型搭建思路。
思考题
自从2018年BERT被提出以来获得了很大的成功学术界陆续提出了各类相关模型例如我们今天学习的BART。请你查一查还有哪些BERT系列的模型并阅读相关论文自行学习一下它们的原理与特点。
欢迎你在留言区和我交流互动,也推荐你把这节课转发给更多同事、朋友,跟他一起学习进步。

View File

@ -0,0 +1,126 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
加餐 机器学习其实就那么几件事
你好,我是方远。
通过前面的学习我们知道PyTorch是作为一种机器学习或深度学习的实现工具出现的因此学习PyTorch的时候免不了会碰到一些机器学习中的相关概念和名词。
在专栏前期调研和上线之后,我收到了不少反馈、留言,希望可以在专栏里介绍一下机器学习的基本知识。
今天这次加餐,我们就一起来看看什么是机器学习,它是怎么分类的,都有哪些常见名词。在补充了这些基础知识之后,我还会和你聊聊模型训练的本质是什么,你可以把它当作专栏更新过半的期中总结。
好,让我们正式开始今天的学习。
人工智能、机器学习与深度学习
说到人工智能、机器学习还有深度学习这三个词,我们虽然很眼熟,但三者的关系总是理不清。
其实,这三者的关系是一种包含的关系。人工智能包含机器学习,而机器学习又包含深度学习。
人工智能的概念其实很早就有了,不过受到技术能力的限制,很少进入到人们的视线当中。当你在网络上搜索人工智能的概念时,可能每一条搜索结果都是用大段文字来解释。归根结底,人工智能的本质就是人们想让计算机像人一样思考,来帮助人们解决一些重复、繁重的工作。
人工智能的应用主要包括以下这几项:
专家系统
自然语言处理
计算机视觉
语音系统
机器人
其他
其中自然语言处理、计算机视觉与语音系统是现在大热的几个方向,从招聘信息中就可以看出来,例如去检索大厂的计算机视觉工程师、自然语言处理工程师等。这些领域中的问题,本质上都可以用传统的机器学习来解决,但依然是受到技术能力的限制,一直处于瓶颈。
近十年随着深度学习的发展人们在这三个领域的研究中取得了长足的进步。越来越多的人工智能产品得以落地让我们的生活变得更加便利、快捷。我们专栏所讲的PyTorch也活跃于这些领域当中。
机器学习(深度学习)
深度学习起源于机器学习中的人工神经网络,所以从工作机制上讲机器学习与深度学习是完全一致的,接下来我们就看看什么是机器学习与深度学习的分类与工作流程。(下文简称机器学习,省略深度学习)。
正如前文所说,机器学习的目的是让机器能够像人一样思考。那么我们可以先想想看,人类是根据什么来思考问题的呢?很显然,我们思考问题时,通常会根据以往的一些经验对当前的问题作出判断。
与人类归纳总结经验的过程类似,机器学习的主要目的是把人类归纳经验的过程,转化为由计算机自己来归纳总结的过程。
其中,人类积累的历史经验,在机器学习中体现为大量的数据。
比如在图像分类的过程中我们给计算机提供了大量的图片,总结归纳这个过程,就是机器学习的训练过程,即计算机处理图片并“学习”其中潜在特征的过程。最终的“规律”,则体现为机器学习中的模型,模型也是我们机器学习任务中最终的一个产出。
所以说,机器学习是一种通过利用数据,训练出模型,然后使用模型预测的一种方法。
有监督学习 Vs 无监督学习
刚才我们说到,机器学习需要训练出模型。机器学习中的模型基本上可以分为有监督学习与无监督学习两大类,当然,基于这两大分类,下面还有很多小的细分类别,我们先不做讨论。
这里我们先弄清楚,什么是有监督学习与无监督学习呢?
有监督学习与无监督学习最明显的区别就是,在训练的时候是否会使用数据真实的标签。为了让你快速理解,这里我结合一个人脸识别的例子来解释一下。
首先来看有监督学习我们现在要训练一个人脸识别模型来自动识别人脸是A还是B。那么在训练的时候就要给模型看大量标记为A的A照片以及标记为B的B照片让模型学习谁是A谁是B。只有经过这样的训练之后当我们进行预测的时候模型才能正确判断出这张人脸图片是A还是B。
再来说说无监督学习,我们手机的相册中有这样的功能,它能自动把某一个人的照片汇聚在一起,但其实手机并不知道汇集到一起的照片是谁。这背后的模型训练原理是怎样的呢?其实训练的时候是把一堆图片给模型看,但是模型并不知道这些图片真实对应的标签,而是模型自己探索这些图片中的潜在特征。
大多数我们可以体验到的深度学习应用都属于有监督学习例如人脸识别、图像分类、手势识别、人像分割、情感分析等。而最近几年特别流行的GAN就属于无监督学习。
常见名词讲解
我们在专栏中出现了很多专业的术语,在这里我们就一起汇总一下,解释一下都是什么意思。为了不让你觉得这部分像教科书那样照本宣科,所以我决定用一个例子把这些名词给串联起来。
我们就像开篇所说的那样,机器学习的本质就是让机器像人一样的思考,所以,我就用学习这个专栏的过程来解释机器学习中的一些术语。
训练集与验证集
在训练时使用的数据我们称之为训练集。评估模型时使用的数据称之为评估集、验证集或测试集。
通过这个专栏的学习会让你从无到有地掌握有关PyTorch的知识在专栏结束的时候我们还设置了期末测试题用来帮助你衡量一下自己的学习成果。
那么,这个专栏的内容就相当于训练集,测试题就是验证集(或称测试集)。训练集是用来训练模型的,而验证集是用来评估模型的。
在模型训练的时候要注意训练集与验证集一定是来自同一问题的不同数据。就像专栏学习的是PyTorch但是后面是Python的测试题那显然不能反映出你真实的学习成果。
Epoch与Step
用所有数据训练一遍就是一个Epoch也就是把专栏学习一遍就叫做一个Epoch。
但受到硬件设备的限制训练时不会一次性的读入所有数据而是一次读入一部分进行训练就像我们每周一、三、五更新一篇内容然后你相应的去学习一部分内容一样。这里的“每次”就是对应的Step这个概念。那每次读入的数据量就是batch_size。
模型训练本质
刚才我们通过一个例子理顺了不少机器学习的关键名词。其实专栏更新到现在我们已经讲完了使用PyTorch做模型训练的大部分内容了恭喜你坚持到这里。
其实我刚开始接触机器学习的时候,总是被它的那些算法弄得晕头转向,有一个阶段一直是摸不清头脑的迷茫状态。有的算法即使看明白了,我也不知道该如何使用。
所幸坚持学习了一段时间后,我慢慢发现,机器学习其实就那么几件事,可谓万变不离其宗。接下来让我们一起回顾一下机器学习乃至深度学习开发的几个重要环节。
首先看看机器学习开发的几个步骤,这我在之前的专栏也有提及,记不清的部分你可以温习回顾。
1.数据处理:主要包括数据清理、数据预处理、数据增强等。总之,就是构建让模型使用的训练集与验证集。-
2.模型训练:确定网络结构,确定损失函数与设置优化方法。-
3.模型评估:使用各种评估指标来评估模型的好坏。
你现在可以想想,基本没有项目的开发能离开这三步吧。无论是深度学习中的深度模型还是机器学习中的浅层模型,它们的开发基本都离不开这三步。
然后,我们再来看看其中的模型训练部分。各种模型纵有千万种变化,但是依然离不开以下几步:
1.模型结构设计例如机器学习中回归算法、SVM等深度学习中的VGG、ResNet、SENet等各种网络结构再或者你自己设计的自定义模型结构。-
2.给定损失函数:损失函数衡量的是当前模型预测结果与真实标签之间的差距。-
3.给定优化方法:与损失函数搭配,更新模型中的参数。
你现在再想想是不是基本所有模型的训练都离不开这三步呢其实上面讲的这6点都来源于咱们前面讲过的内容。学习前面的内容就好比学会如何制造汽车的零部件将这些零件组装起来就是完成了一辆汽车的完整生产而这一步是我们后面要继续研究的。
这里面变化最多的就是模型结构了,这一点除了多读读论文,看看相关博客来扩充知识面之外,没有什么捷径可走。然后呢,我们也不要小瞧了损失函数,不同的损失函数有不同的侧重点,当你模型训练处于瓶颈很难提升,或者解决不了现有问题的话,可以考虑考虑调整一下损失函数。
在我看来,模型训练的本质就是确定网络结构、设定损失函数与优化方法。接下来,我们将一起学习如何将前面学习的各个环节组装起来,完成一个完整的模型训练。
欢迎你在留言区跟我交流讨论,咱们一起继续加油。

View File

@ -0,0 +1,111 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
用户故事 Tango师傅领进门修行在个人
你好我是Tango。
很高兴能有机会来分享我对这个专栏的学习体验。先做个自我介绍我是一个工作了11年的非科班出身程序员大专日语专业。目前在NTTDATA中国数据信息技术有限公司工作出于对编程的兴趣便加入了咱们开发者大军。
如果你平时关注部落或者InfoQ写作平台的话可能对我的头像有点印象。从2017年购买的第一门课到现在我已经累计学习了153门课程其中学完的有130门课。作为一个文科生在没有遇到极客时间之前我都是在某宝上找资源自学或者买相关的图书但是那种学习效果并不是很理想。
随着近些年机器学习的大热,我也开始接触这一块的内容。说实话,想入门机器学习这个领域还是很辛苦的一件事,不单要完成逻辑思维层面的转换,更需要补充很多基础知识。
之前我买过很多书,但是看起来总是很费劲。而网上能找到的资料,要么通篇数学公式,让我这种数学知识都还给老师的同学扼腕叹息,要么一笔带过原理,一直堆砌代码片段。总之,学习下来极其痛苦,也很难抓到重点是什么。所以当看到咱们这个专栏上线,就第一时间入手了。
相比之前阅读的纸质图书,我觉得通过专栏学习还是有不少优点的。
首先,老师沉淀的经验都来自于实际项目,这样我们接触到的知识便是最有用的部分。
其次,因为专栏形式是音频配合图文,可以很好地增加记忆。比方说,通勤路上或者其他零散时间我会听听音频,而有了整块儿时间还会回看图文内容,复习之前所学。
最后还有一点我尤其看重,就是专栏提供的互动功能,可以在专栏课程下面还有社群(主要是微信群)跟老师、同学互动。三人行,必有我师,很多时候,技术学习需要良好的交流、讨论氛围。
在业余时间我也参加过开源社区的活动目前在OpenVINO中文社区做志愿者而OpenVINO就是做机器学习推理的这让我对如何利用PyTorch来训练模型更加感兴趣。
这个专栏从基础理论到实战篇,每一篇都是干货满满,这要比我在网上看的视频,买的书要好很多,但伴随着知识的密集和难度的增加,如何做到能更好地掌握专栏的内容,让学习效果达到最好,也成为了一个亟待解决的问题。
我的学习方法
我梳理了一下自己的学习方法,主要是这样五步:学习、复习、归纳总结、复盘和进一步持续学习。
先说说初步学习,我用的日常听音频+周末整体理解的方式。每周3篇的更新频率要学习的内容还是很多的如果只是听音频基本上收获是很少的所以我习惯用周末的时间将专栏中的代码写一遍重新理解一下文章中的内容尤其是文章中的代码更值得仔细研读。
好多小伙伴看到专栏不是视频课程就不想加入,其实我整个学习下来,感觉图文专栏的学习效率会更高一些。
之后就是复习,和软技能类的专栏不同,我们如果只在通勤路上或者做家务的时候听一听,那学习效果就会大打折扣。这类需要大量动手实践类的专栏,是需要反复学习、动手实践、消化理解后进行归纳总结的。
那怎样归纳总结呢将专栏中的知识点归纳成文章发布在InfoQ写作平台这是我比较推荐的一个方法。我之前的笔记有一部分写在了本地MarkDown文档里后来发现有的时候需要查找时还是很不方便所以慢慢就转到了InfoQ写作平台上面。这样不但可以随时可以查看自己的笔记还可以分享给他人。
说完方法我想还想聊聊有什么内容值得归纳总结。在我看来除了专栏中的知识点微信群和专栏的留言区老师的答疑也是很大的宝藏很值得整理出来。上面提到的总结内容等到我把专栏讲解内容消化之后我会一起公布在InfoQ写作平台上也欢迎小伙伴围观。
除了文字的输出为了实践“费曼学习法”检验自己的学习效果并将自己掌握的内容和其他人分享我有时候还会到B站直个播。直播过程中有问题或者细节想不起来了还会重新去看专栏或者去网上搜索一下。这也就是为啥我的直播总是“翻车”。
编程的课程在学习时,很容易出现一种错觉,眼睛觉得学会了,可实际动手写的时候,又好像感觉之前的内容没学到位。有了这个直播写代码的过程,我觉得会让学习变得轻松有意思一些,也能够查漏补缺。
最后还有一个持续学习的问题,很多专栏虽然完结了,但是评论区的内容还是不断出现新内容以及新的知识点,那么如何实时跟踪专栏的评论区内容的更新呢?我采用了自动化的方式,自己写一个工具去定时跟踪。比如一个星期去把专栏留言以及老师回复的内容抓取一下,然后利用下一周的时间整理一下。
另外,专栏毕竟篇幅有限,很多内容没有办法在专栏中事无巨细的交代。如果在工作或者项目中遇到了,则需要自己动手去查找,这也是一个持续学习的过程。
学习收获与建议
在我看来学习这门课程绝对是一个正确的选择。因为通过学习方远老师的这个专栏我不但很好地掌握了PyTorch的不少重要知识还了解一些常用数学公式的定义也算意外之喜。
老师会用Python代码来解释公式的代码逻辑我们都知道Python的代码相对容易理解对入门同学来说这大大降低了学习成本。
通过整个专栏的学习我基本已经掌握了PyTorch的基本运用。在整个学习过程中还结交了很多一起学习的小伙伴。老师关于VGGGoogLeNet以及ResNet的讲解简洁明了这对想了解机器视觉领域算法的新手有很大的帮助。
在整个专栏的学习过程中印象最深的地方就是老师在讲完理论知识点后便会用实际生活中的例子来做联系就拿基础部分的NumPy相关的内容来说老师用一个章节讲了需要掌握的知识点后在下一个章节中就利用了上个章节中的内容用极客时间的Logo做了一个实际可操作的Demo。这对新手来说非常友好可以很快地将所学的内容运用起来学起来很过瘾。
在后面的学习中我了解到原来NumPy是不可以用来GPU加速的而Tensor却是可以的。这个知识点我之前却从来未了解过学到这个对我后面的训练起到了很大的帮助我也重构了部分之前的代码效果显著。
在学习卷积相关的章节时老师很贴心地整理一份文档:
这个文档在我的项目中也帮我节省了一些查找资料的时间。
在接触咱们这门课之前,参加大会的时候,经常听到别人提到多机多卡的方式进行模型训练,可是怎么操作并不太清楚。在学完咱们这个专栏,我已经掌握了如何搭建分布式的训练环境,等我的显卡到了,我就要开始动手试试了。
现在暂时先将专栏的第16课收藏了起来以下是老师给的关键代码块
if args.distributed:
if args.dist_url == "env://" and args.rank == -1:
args.rank = int(os.environ["RANK"])
if args.multiprocessing_distributed:
# For multiprocessing distributed training, rank needs to be the
# global rank among all the processes
args.rank = args.rank * ngpus_per_node + gpu
dist.init_process_group(backend=args.dist_backend, init_method=args.dist_url,
world_size=args.world_size, rank=args.rank)
另外我在学完第一遍后尝试用PyTorch做了一个日文识别的项目目前还在编写中以下代码片段是手写体数字识别MNIST中的部分内容:
整个训练次数设置为20回
在学完方老师的专栏后想利用PyTorch来实现一下看看OpenVINO结合PyTorch的效果如何。目前还在学习阶段等后面可以展示的时候我会将GitHub的地址放在评论区或者社群中。
从代码上看感觉要比TF更加容易API的变动也没有TF的那么大很适合用来做学习。而且后期转换成OpenVINO所支持的形式也是很方便的只需使用OpenVINO的model optimizer将ONNX转换为IR形式即可。
除了前面的启发好用的工具也会大大提升工作效率。专栏里介绍的TensorboardX 和 Visdom工具15讲就很不错可以更好地在可视化深度学习模型的训练过程中实时监控一些数据例如损失值、评价指标等等我之前一直是自己在notebook中查看的现在用到了课程里讲到的工具可视化方便了很多也节省了很多时间。
最后实战篇的内容也值得反复琢磨。比方说在学习本专栏之前一直比较困惑面对不同风格的PDF文件如何才能准确提取出我需要的内容。在学完课程后17 ~ 18讲给了我很大启发我可以先将PDF不可编辑的版本转换成图片然后按照规则先训练好目标分类再按照不同的分类进行图像识别从而提取出我所需要的内容19 ~ 20讲
说了这么多还是很希望你也和我一样一起深入学习专栏一起动手尝试实验。如果你和我一样已经加入到了学习队伍中希望你在学习一遍后能有所收获欢迎和我一起来N刷这门课程一定会有不同的收获如果你在实际项目中有什么问题可以一起在评论区或者社群中积极讨论。
以上便是我分享的内容了,感谢你的阅读,如果能对你有所帮助,那是我最大的荣幸,如果有不足的地方,也欢迎留言提出,我们一起进步。极客时间,让学习成为习惯。

View File

@ -0,0 +1,334 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
答疑篇 思考题答案集锦
你好,我是编辑宇新。春节将至,给你拜个早年。
距离我们的专栏更新结束,已经过去不少时间啦。方远老师仍然会在工作之余,回到专栏里转一转,看看同学最新的学习动态。大部分的疑问,老师都在留言区里做了回复。
除了紧跟更新的第一批同学,也很开心看到有更多新朋友加入到这个专栏的学习中。课程的思考题,为了给你留足思考和研究的时间,我们选择用加餐的方式,把所有参考答案一次性发布出来。
这里要提醒一下,建议你先自己思考和练习后,再来对答案。每节课都有超链接,方便你跳转回顾。
第2节课
题目:在刚才用户对游戏评分的那个问题中,你能计算一下每位用户对三款游戏打分的平均分吗?
答案:
>>>interest_score.mean(axis=1)
第3节课
题目给定数组scores形状为2562562scores[: , :, 0] 与scores[:, :, 1]对应位置元素的和为1现在我们要根据scores生产数组mask要求scores通道0的值如果大于通道1的值则mask对应的位置为0否则为1。
scores如下你可以试试用代码实现
scores = np.random.rand(256, 256, 2)
scores[:,:,1] = 1 - scores[:,:,0]
答案:
mask = np.argmax(scores, axis=2)
第4节课
题目在PyTorch中有torch.Tensor()和torch.tensor()两种函数,它们的区别是什么呢?
答案torch.Tensor()是Pytorch中的类其实它是torch.FloatTensor()的别名使用torch.Tensor()会调用Tensor类的构造函数生成float类型的张量
而torch.tensor()是Pytorch的函数函数原型是torch.tensor(data, dtype…)其中data可以是scalarlisttuple等不同的数据结构形式。
第5节课
题目现在有个Tensor如下。
>>> A=torch.tensor([[4,5,7], [3,9,8],[2,3,4]])
>>> A
tensor([[4, 5, 7],
[3, 9, 8],
[2, 3, 4]])
我们想提取出其中第一行的第一个,第二行的第一第二个,第三行的最后一个,该怎么做呢?
答案:
>>> B=torch.Tensor([[1,0,0], [1,1,0],[0,0,1]]).type(torch.ByteTensor)
>>> B
tensor([[1, 0, 0],
[1, 1, 0],
[0, 0, 1]], dtype=torch.uint8)
>>> C=torch.masked_select(A,B)
>>> C
tensor([4, 3, 9, 4])
我们只需要创建一个形状跟A一样的Tensor然后将对应位置的数值置为1然后再把Tensor转换成torch.ByteTensor类型得到B最后跟之前masked_select一样的操作就OK啦。
第6节课
题目在PyTorch中我们要定义一个数据集应该继承哪一个类呢
答案torch.utils.data.Dataset
第7节课
题目Torchvision中 transforms 模块的作用是什么?
答案常用的图像操作例如随机切割、旋转、Tensor 与 Numpy 和 PIL Image 的数据类型转换等。
第8节课
题目请你使用torchvision.models模块实例化一个VGG 16网络。
答案:
import torchvision.models as models
vgg16 = models.vgg16(pretrained=True)
第9节课
题目请你想一想padding为samestride可以为1以外的数值吗
答案:不可以。
第10节课
题目随机生成一个3通道的128x128的特征图然后创建一个有10个卷积核且卷积核尺寸为3x3DW卷积的深度可分离卷积对输入数据进行卷积计算。
答案:
import torch
import torch.nn as nn
# 生成一个三通道的128x128特征图
x = torch.rand((3, 128, 128)).unsqueeze(0)
# DW卷积groups参数与输入通道数一样
dw = nn.Conv2d(x.shape[1], x.shape[1], 3, 1, groups=x.shape[1])
pw = nn.Conv2d(x.shape[1], 10, 1, 1)
out = pw(dw(x))
print(out.shape)
第11节课
题目:损失函数的值越小越好么?
答案:不是的,咱们在这节课中学习的损失函数,实际上是模型在训练数据上的平均损失,这种损失函数我们称作为经验风险。实际上,还有一个方面也是我们在实际工作中需要考虑的,那就是模型的复杂度:一味追求经验风险的最小化,很容易使得模型过拟合(可回顾一下前文内容)。
所以,还需要对模型的复杂度进行约束,我们称之为结构风险。实际研发场景中,最终的损失函数是由经验风险和结构风险共同组成的,我们要求的是两者之和的最小化。
第12节课
题目:深度学习都是基于反向传播的么?
答案:不是的,主流的深度学习模型是基于反向传播和梯度下降的,但是一些非梯度下降的二阶优化算法也是存在的,比如拟牛顿法等。不过计算代价非常大,用的就比较少了。而且一般而言,工业界基本都采用基于反向传播和梯度下降的方式。
第13节课
题目batch size越大越好吗
答案不是的。较大的batch_size容易使模型收敛在局部最优点特别小则容易受噪声影响。
第14节课
题目请你自己构建一个卷积神经网络基于CIFAR-10训练一个图像分类模型。因为还没有学习图像分类原理所以我先帮你写好了网络的结构需要你补全数据读取、损失函数(交叉熵损失)与优化方法SGD等部分。
class MyCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=3)
# conv1输出的特征图为222x222大小
self.fc = nn.Linear(16 * 222 * 222, 10)
def forward(self, input):
x = self.conv1(input)
# 进去全连接层之前,先将特征图铺平
x = x.view(x.shape[0], -1)
x = self.fc(x)
return x
答案:
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
transform = transforms.Compose([
transforms.RandomResizedCrop((224,224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
cifar10_dataset = torchvision.datasets.CIFAR10(root='./data',
train=False,
transform=transform,
target_transform=None,
download=True)
dataloader = DataLoader(dataset=cifar10_dataset, # 传入的数据集, 必须参数
batch_size=32, # 输出的batch大小
shuffle=True, # 数据是否打乱
num_workers=2) # 进程数, 0表示只有主进程
class MyCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=3)
# conv1输出的特征图为222x222大小
self.fc = nn.Linear(16 * 222 * 222, 10)
def forward(self, input):
x = self.conv1(input)
# 进去全连接层之前,先将特征图铺平
x = x.view(x.shape[0], -1)
x = self.fc(x)
return x
cnn = MyCNN()
optimizer = torch.optim.SGD(cnn.parameters(), lr=1e-5, weight_decay=1e-2, momentum=0.9)
# 训练3个Epoch
for epoch in range(3):
for step, (images, target) in enumerate(dataloader):
output = cnn(images)
loss = nn.CrossEntropyLoss()(output, target)
print('Epoch: {} Step: {} Loss: {}'.format(epoch + 1 , step, loss))
cnn.zero_grad()
loss.backward()
optimizer.step()
第15节课
题目参考Visdom快速上手中的例子现在需要生成两组随机数分别表示Loss和Accuracy。在迭代的过程中如何用代码同时绘制出Loss和Accuracy两组数据呢
答案:
from visdom import Visdom
import numpy as np
import time
# 实例化窗口
viz = Visdom(port=6006)
# 初始化窗口参数
viz.line([[0.,0.]], [0.],
win='train',
opts=dict(title='loss&acc', legend=['loss','acc'])
)
for step in range(10):
loss = 0.2 * np.random.randn() + 1
acc = 0.1 * np.random.randn() + 0.5
# 更新窗口数据
viz.line([[loss, acc]], [step], win='train', update='append')
time.sleep(0.5)
运行结果如图所示:
第16节课
题目在torch.distributed.init_process_group(backend=“nccl”)函数中backend参数可选哪些后端它们分别有什么区别
答案backend参数指定的通信后端包括NCCL、MPI、gloo。NCCL是Nvidia提供的官方多卡通信框架相对比较高效MPI也是高性能计算常用的通信协议但是需要自己安装MPI实现框架例如OpenMPIgloo是内置通信后端但不够高效。
第18节课
题目:老板希望你的模型能尽可能的把线上所有极客时间的海报都找到,允许一些误召回。训练模型的时候你应该侧重精确率还是召回率?
答案:侧重召回率。
第19节课
题目对于这节课里讲的小猫分割问题最终只输出1个特征图是否可以
答案可以的因为小猫分割是一个二分类问题可以将输出的特征图使用sigmoid函数将输出的数值转换为一个概率从而进行判断。
第20节课
题目:图像分割的评价指标都有什么?
答案mIoU和Dice系数。
第21节课
题目TF-IDF有哪些缺点呢你不妨结合它的计算过程做个梳理。
答案TF-IDF认为文本频率小的单词就越重要也就是区分性越强但是实际上很多情况下这并不正确。比如一篇财经类文章有一句“股价就跟火箭一样上了天”这里的“火箭”就会变得非常重要显然是错误的。怎么办呢一般我们会对词频做一个条件过滤比如超过多少次。也会对TF-IDF的公式进行改进具体改进方法如果有兴趣的话你可以借助网络查找相应的文章。
第22节课
题目:词向量的长度多少比较合适呢?越长越好吗?
答案不是的越长的词向量尽管可以更加精细的表示词语的空间位置但是也会带来计算量的暴涨、数据稀疏等问题一般来说我们较多的选择64、128、256这样的长度具体是多少要靠实验来不断的确定。有的论文给出的建议是n>8.33logN,具体是否可行,还是要结合实际情况来敲定。
第23节课
题目利用今天训练的模型编写一个函数predict_sentiment实现输入一句话输出这句话的情绪类别与概率。
例如:
text = "This film is terrible!"
predict_sentiment(text, model, tokenizer, vocab, device)
'''
输出:('neg', 0.8874172568321228)
'''
答案:参考代码如下。
# 预测过程
def predict_sentiment(text, model, tokenizer, vocab, device):
tokens = tokenizer(text)
ids = [vocab[t] for t in tokens]
length = torch.LongTensor([len(ids)])
tensor = torch.LongTensor(ids).unsqueeze(dim=0).to(device)
prediction = model(tensor, length).squeeze(dim=0)
probability = torch.softmax(prediction, dim=-1)
predicted_class = prediction.argmax(dim=-1).item()
predicted_probability = probability[predicted_class].item()
predicted_class = 'neg' if predicted_class == 0 else 'pos'
return predicted_class, predicted_probability
# 加载模型
model.load_state_dict(torch.load('lstm.pt'))
text = "This film is terrible!"
predict_sentiment(text, model, tokenizer, vocab, device)
第24节课
题目Bert处理文本是有最大长度要求的512那么遇到长文本该怎么办呢
答案这是一个非常开放的问题设置为最大512主要还是兼顾了效率问题但还是有非常多的解决办法比如我们之前提到过的关键词提取。或者分别从开头、中间、结尾选择一定长度的内容做运算。不过这些都是比较简单的办法。你还有更好的办法吗欢迎留言给我。
第25节课
题目自2018年BERT被提出以来获得了很大的成功学术界陆续提出了各类相关模型例如我们今天学习的BART。请你查一查还有哪些BERT系列的模型并阅读相关论文自行学习一下它们的原理与特点。
答案:
XLNet: Generalized Autoregressive Pretraining for Language Understanding
RoBERTa: A Robustly Optimized BERT Pretraining Approach
ALBERT: A Lite BERT for Self-supervised Learning of Language Representations
最后,再次祝愿你虎年快乐,学习进步,工作顺利!

View File

@ -0,0 +1,97 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 人生充满选择,选择与努力同样重要
你好,我是方远。
固定的打招呼开头,但是这节课却是咱们这个专栏的最后一节课了。感谢你一路坚持学习到这里,现在有没有感觉要解放了?哈哈。
不过解放之前,让我们一起快速回顾一下这门课程咱们学了什么,我也会像电影点映后的发布会那样,顺便揭秘一下,我为什么要这样讲。
咱们的课程核心是PyTorch实战但是实战就跟习武一样得先挑几件趁手的兵器要不只能秒送人头。
正所谓“工欲善其事必先利其器”所以在最开始的时候我们并没有上来就聊PyTorch本身而是讲了如何使用NumPy工具还一起研究了Tensor的数据结构。
如果你是一个算法工程师很容易就会发现在非常多的场景下都有NumPy的出现因为无论从数据操作的便捷性、友好性还是通用性NumPy都是强大到没朋友。就算你以后不用PyTorch开发甚至不做深度学习开发NumPy也是你躲不开的宿命。所以我希望你在以后的学习中对于NumPy以及Pandas的内容还是要多多关注。
而我们之所以要花不少篇幅来学习Tensor也是因为它的通用性。不过咱们以前都习惯了dict、list、set这样的数据结构以及NumPy中ndarray这样的通用数据处理格式忽然转变操作数据的方法肯定是有一个适应的过程特别是像数据切片、数据变形、维度变化这样看不见、摸不着的操作你感觉一个脑袋两个大也是很正常的。
我想和你强调的是Tensor玩转了之后在以后的深度学习开发中无论你使用PyTorch还是TensorFlow亦或是未来新出来的框架都可以让你快速上手从青铜迅速变王者。
掌握基础工具的使用,就如同选好了一把大宝剑,接下来就得学习训练模型所需要的招数和心法了。深度学习常用的内容比如卷积、损失函数、优化函数、梯度更新都是重中之重,甚至可以说是缺一不可了,咱们花了不少篇幅来细致地讲解其中的知识点,相信你现在应该也到了张口就来的地步了。
为了聚焦重点其他类似全连接、池化等简单的结构咱们则是简要带过你有兴趣可以课后了解毕竟咱们要快速上手的。既然是实战咱们就要从实际、客观、高效的角度去借鉴前人的工作成果于是例如Torchvision、可视化工具、分布式训练方法等内容可以让我们少绕弯路直达目标。
兵器也有了武功也会了就得出门下山历练了否则光说不做就全成了嘴上功夫。每一个成就的解锁都需要我们挑战一个个艰难的任务所以我特别为你安排了图像和文本算法任务这两个大Boss。
细心的同学不难发现在每个任务开始之前我都会向你介绍这个大Boss的背景比如在NLP部分我先向你介绍了NLP领域的几大内容、常用算法等这样你才能更好地理解任务的目的和解决思路。
当你完成了整个课程就相当于完成了习武历练的过程从此以后你就可以独立完成基于PyTorch的深度学习Pipeline了是不是非常棒
但是这就是全部吗?不是,还差得远。
通过这样一个PyTorch的专栏最根本的目的不是让你知道如何使用它而是希望你借助它高效便捷地了解深度学习。这个链接是PyTorch的官方文档。你会发现这里面的内容浩如烟海但是我们没有必要把它所有的函数功能都学会它是一个工具仅此而已。
所以临别之际对于即将踏上AI之途的小伙伴我有几句话想对你说简单概括是“五个保持”。
第一保持好奇。人工智能是一个更新迭代非常快的领域以前很火的内容可能没过多久就过时或者很少使用了比如之前的RNN之于现在的Attention。所以你一定要多看论文每年的顶会论文都是最好的学习资料。
后面我列出了CV和NLP领域的顶会供你参考。
看完这份清单,有心人自然知道去搜搜这些会议的时间。会议结束后,你就可以自行查找各种论文的分析介绍了,当然还是建议你尽可能看原版。不过哪怕你看不懂长篇大论的英文论文,没有关系,中文版本的论文分析介绍也不少,这样也可以提高。
除了这些会议还有不少是比较综合性的人工智能会议比如IEEE、ICLR等等你同样可以按需关注。
第二,保持平和。作为过来人,我想说,在以后的深度学习开发过程中,你会见识到各种各样奇葩的结果。
比如明明训练过程中各项指标都好好的一到预测环节就崩盘的情况。这都算常见的。再比如两张差不多的图片只因为其中一张多了几个色块或者形状导致最后的结果大相径庭这种case查起来就非常的“蓝瘦香菇”了。又或者你会因为业务提供的数据资源有限而苦恼要么量太少、要么太不平衡这些都是很难用技术的思路去解决的。
所以你要多多参与项目,多接触不同的场景,慢慢的……你就习惯了哈哈。当然,这不意味着破罐子破摔,而是随着历练的增多,你终会找到或者学到解决这些困难的办法。其实每一个深度学习算法工程师的成长,都是靠着一个又一个的狗血问题一路走来的。
第三保持谦逊。诚然算法工程师特别是深度学习算法工程师目前是IT领域第一梯队的存在也是好多IT人羡慕的工作但是一定要记住山外有山要多向别人取经多向前人借鉴才能让自己一直有足够的竞争力。
第四保持童心。实不相瞒我打小就想当超级英雄拯救世界一直到今天也这样。这并不可笑反而这是很真实的自我。保持童心可以让你时时刻刻充满天马行空式的想象而AI领域从业者最大的限制恰恰不是技术而是想象力。
有了想象力你可以开发仿生的阿尔法狗你可以开发堪比艺术家的创作AI你可以心血来潮搞一个大变脸AI把自己无缝放入任何好莱坞大片中。在AI的世界你可以改变世界就可以成为真正的超级英雄。
还有一条,保持活力。这与技术无关,码农的秃顶、肥胖、腰间盘突出、前列腺炎、啤酒肚、油腻、格子衫……这些已经在网络上被黑了无数次了。其实这一方面是他人的刻板印象,一方面可能真的是咱们的现状。
我建议你跟我一样,能够经常锻炼,参加体育活动,工作之余来几个俯卧撑,下班之后撸撸铁,周六、日的时候约上三五好友,骑骑车、打打球。因为你的人生中,工作只占了非常非常非常小的一个比例,多出去走走,发现更大更好的世界。
五条“保持”我就说完了,还记得开篇词,我是如何高大上地介绍我自己的吗?其实还有一部分我没有说,现在我也愿意跟你分享出来。
作为一名80后其实小的时候电脑这玩意儿并没有那么普及。一般都是家里条件非常好的同学才会有一台大头机。所以那时候每周一节的微机课就成为了我们最期待的、事实上的“游戏课”。电脑对于我来说就是打打游戏查查资料看看电影仅此而已。
面临高考报志愿这个人生抉择的时候,我听了家长和老师的话,选了制造、自动化、土木相关的超热门专业志愿,分数差了几分没上去,调剂到了计算机,也算是阴差阳错。但之后随着对编程的深入了解,我越来越觉得自己喜欢上了编程,喜欢上了算法。或许是命中注定,我就应该走这条路。
再后来我发现这个时代是永远在快马加鞭发展着的人工智能将影响所有人的生活。所以我选择了AI方向。每个方向都有它独到的魅力而AI的魅力在于它是没有边界的就像绿灯侠一样。就像我前面说的那样AI很多时候没有技术限制唯一能限制你的只有你的想象力。
当然,学习和提升自己是痛苦且枯燥的,这个过程充满了复杂的公式,难缠的优化方法,还有玄学一般的调参经历。但是这种自我蜕变是极具成就感的:我参与完成的搜索引擎推荐算法项目,时至今日仍然每天在为数亿用户服务;我主导的资讯产品文本算法项目,同样为数以千万计的用户更高效地获取信息而默默运转着;我参加的多模态算法项目,为无数的儿童与青少年提供了纯净的网络环境……
好了,最后一课的鸡汤终于要登场了,请你记住:
阴差阳错是一种选择
命中注定也是一种选择
自我蜕变更是一种选择
人生充满了选择,选择与努力同样重要
时间过得真快,快到让我觉得第一节课也就是几天前的事情。时间过得好慢,慢到使我迫不及待地希望在更多的课程中与你一同进步。
下课,再会。
最后,我希望你可以填一下后面这张毕业问卷,说说你学习这个专栏的感受。