系列文章目录

动手学深度学习笔记系列:
动手学深度学习笔记3.1+3.2+3.3

文章目录

  • 系列文章目录
  • 前言
  • 一、softmax回归
    • 1.1 分类问题
    • 1.2 网络架构
    • 1.3 全连接层的参数开销
    • 1.4 softmax运算
    • 1.5 小批量样本的矢量化
    • 1.6 损失函数
    • 1.7 softmax函数及其导数
    • 1.8 信息论基础
      • 1.8.1 熵
      • 1.8.2 信息量
  • 二、代码实现
    • 2.1 图像分类数据集
      • 2.1.1 导入库
      • 2.1.2 读取数据集
      • 2.1.3 读取小批量
      • 2.1.4 整合所有组件
    • 2.2 从零实现softmax
      • 2.2.1 导入库
      • 2.2.2 读取数据集
      • 2.2.3 创建迭代器
      • 2.2.4 初始化模型参数
      • 2.2.5 定义softmax运算
      • 2.2.6 定义模型
      • 2.2.7 定义损失函数
      • 2.2.8 测试softmax和损失函数
      • 2.2.9 分类精度
      • 2.2.10 对变量累加
      • 2.2.11 实现一个周期的训练
      • 2.2.12 绘图类
      • 2.2.13 模型训练
      • 2.2.14 梯度下降
      • 2.2.15 训练模型并绘图
      • 2.2.16 模型预测
    • 2.3 softmax简洁实现
  • 总结

前言

上一篇笔记中,我详细记录了线性回归和线性回归的实现。线性回归一般用于回归问题的预测。但是在机器学习里面还有很多类问题,比如分类问题,今天这篇笔记就是记录如何用一个简单的模型softmax回归解决简单的分类问题。(为什么说简单呢,因为如果是按照现在这种像素级别的图片输入softmax的话,可能就会参数爆炸了,至于啥是参数爆炸,在这篇笔记中也会提到,当然也会提到之前挖的坑前向传播(正向传播)和反向传播。)
——————————————不正经的分割线————————————————

一、softmax回归

1.1 分类问题

假设每次输入是一个2×2的灰度图像。我们可以用一个标量表示每个像素值,每个图像对应四个特征x1,x2,x3,x4。此外,假设每个图象属于类别“猫”,“鸡”,“狗”中的一个。

这几句话的意思就是说,现在输入一个2×2的图片,2×2的图片有四个像素,每个像素起一个名字,分别叫x1、x2、x3、x4。这个图片可以分为三类:猫、鸡、狗。

接下来,我们要选择如何表示标签。我们有两个明显的选择:最直接的想法是选择y∈{1,2,3},
其中整数分别代表{狗,猫,鸡}。这是在计算机上存储此类信息的有效方法。如果类别间有一些自然顺序,比如说我们试图预测{婴儿,儿童,青少年,中年人,老年人},那么这个问题转变为回归问题,并且保留这种格式是有意义的。

上面这一堆话的意思是,可以用回归来解决分类问题,特别是在分类类别有一定的顺序性,比如年龄、身高等。这样回归输出一个值的时候,这个值的大小刚好可以和类别的顺序性有一定的关联。但是这一方法对没有顺序性的分类就没有太大效果,比如水果分类。

但是一般的分类问题并不与类别之间的自然顺序有关。幸运的是,统计学家很早以前就发明了一种表示分类数据的简单方法:独热编码(one-hot encoding)。独热编码是一个向量,它的分类和类别一样多。类别对应的分量设置为1,其他所有分量设置为0.在我们的例子中,标签y将是一个三维向量,其中(1,0,0)对应于“猫”、(0,1,0)对应于“鸡”,(0,0,1)对应于狗:
y∈{(1,0,0),(0,1,0),(0,0,1)}

这里就很有意思,也就是将类别转化为了向量。其实在这里我感觉和四元数是有一点异曲同工的(当然完全可能是我感觉错了)。这样类别转化为了向量后,就相当于将一个个离散的实数值转化为了空间中的一个个点,这样可以应用各种公式,比如上篇文章说的距离公式等。
而且这里我自己还有一种感觉,那就是当我看到那个向量集合{(1,0,0),(0,1,0),(0,0,1)}时,我虎躯一震,这不是个三维空间的基吗,而且还是规范正交基,也就是无论怎么扩大类别,每一个类别对应的向量组合在一起形成的集合也是规范正交基!当然分类的向量集合是规范正交基似乎并没有什么太大的作用(我目前看来),我只是记录一下自己看到这个东西的想法。
其实继续向下引申,那么独热编码可以用于分类的类别表示,独热编码是不是也可以表示别的东西?举一个小栗子:假设给了一组人的信息,这些人的信息具有的特征是性别、最近十四天去过的地方的疫情风险级别、最近七天核酸检测日期(随便举例子玩的,别当真啊)。你会发现,性别也可以分类为男、女,风险级别可以分类为高、中、低,最近七天核酸检测日期可以分类为1、2、3、4、5、6、7。那么既然每一个特征可以分类,这就来了一个更有意思的地方,独热编码可以表示特征。可以给每个特征形成一个独热编码,也就是:
性别:{男,女} → {(1,0),(0,1)}
风险等级:{高,中,低} → {(1,0,0),(0,1,0),(0,0,1)}
检测日期:{1,2,3,4,5,6,7} → {(1,0,0,0,0,0,0),(0,1,0,0,0,0,0),(0,0,1,0,0,0,0),(0,0,0,1,0,0,0),(0,0,0,0,1,0,0),(0,0,0,0,0,1,0),(0,0,0,0,0,0,1)}
那么我们将每个人的所有特征对应的向量加在一起形成一个新的向量:假设一个男人最近去过低风险等级的地方,并且一天前做的核酸检测,那么这个人的特征形成的向量为
(1,0,0,0,1,1,0,0,0,0,0,0).
是不是有点糊涂了,咋把特征变成了一个向量?其实很简单,就是三个向量直接拼接而成的。上面的这个向量前两位是性别对应的向量,第3位、第4位、第5位是风险对应的向量,其余的位是核酸检测日期对应的向量。如下图所示:

在我看来,给样本集每一个特征形成一个独热编码之后,再将一个样本的特征对应的向量拼接而成后形成的向量的示意图画起来十分类似于寄存器的每一位的表示(滑稽)
而且你会发现,,这样形成的一个个向量是不是可以比较两个向量之间的相似性呢,这样又可以进一步引申到比较特征的相似性,特征形成向量后,我们可以用距离公式计算向量之间的距离,然后比较距离大小来判断向量的相似度,那这可以用在好多地方了,比如聚类等等
而且其实这里已经暴露了独热编码的一个缺点:如果一个特征可以被分成好多类别,那么这个特征形成的向量拼接成的矩阵是个稀疏矩阵
当然独热向量还有各种有意思的东西,详情参见下面这篇文章:
https://blog.csdn.net/qq_38651469/article/details/121100275
独热编码(One-Hot)及其代码
不能再引申下去了,否则这篇文章就变成讲独热编码的文章了
————————————————分割线————————————————————
刚刚出去冷静了一下,虽然我觉得我写下这一段想法可能不会变成现实,但是我觉得在这里有点东西,那就是可能独热编码并不是一个很好的编码,但是不是能够有一种编码或者模式能够记录人的大部分有用的特征(比如基因等等),并且通过一个模型训练将人群进行聚类或者预测这个人的行为呢?再想到我今年上的一门mooc叫《人工智能与信息社会》里面的一堂课,叫作“下棋高手”,一开始我也没认真听,毕竟AlphaGo我上大学期间在人工智能专业课上就被不少老师拿来当例子讲了,但是这节课还讲了一个东西却引起了我的注意,那就是人工智能在MOBA游戏上的快速发展。据这节课所讲,在MOBA游戏领域,我就直接贴图了,内容有点多:


但是人工智能似乎好像还没在星际争霸(非MOBA)中打败人类,这种策略类游戏还是人工智能发展的地方?当然我还玩过一个叫庙算的游戏,十分有意思的军旗类游戏,可以和ai对战,而且好像对战过程也是训练ai的过程。那么未来战场收集的数据是否也可以通过信息链传回后方进行分析呢?(我知道有这方面的研究在进行了,但这样的东西还是使我遐想。)
————————————————分割线————————————————————

1.2 网络架构

为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数。

首先是有多个输出,每个类别对应一个输出。这句话的意思是,在这个模型里面,我们会判断这幅图像是每一个类别的可能性。所以我们会基于这幅图像的四个像素计算每一个类别的可能性。也就是一个输出对应这幅图是某个类别的可能性。所以有多少个类别,就有多少个输出。

每个输出对应于它自己的仿射函数。

第二句话,每个输出对应于它自己的仿射函数。这句话的意思是因为要根据四个像素(我们这个问题输入的图片只有四个像素)计算每一类别的可能性,所以一个类别的可能性(输出)和四个像素x1、x2、x3、x4(输入)构成了一个函数关系:可能性 = f(i)(x1,x2,x3,x4)。这个f(i)就是仿射函数,i∈(0,4)且i只能取正整数。
别看这么抽象,也就是说:
计算猫的可能性的函数是:猫的可能性 = f(1)(x1,x2,x3,x4)
计算鸡的可能性的函数是:鸡的可能性 = f(2)(x1,x2,x3,x4)
计算狗的可能性的函数是:狗的可能性 = f(3)(x1,x2,x3,x4)

在我们的例子中,由于我们有4个特征和3个可能的输出类别,我们将需要12个标量来表示权重(带下标的w),3个偏量来表示偏置(带下标的b)。

这句话的意思有点东西,我们先来看这句话对应的公式(下图所示):

这里可以看出,其实所谓的仿射函数,也就是y = XW +b,是不是和线性回归十分相似呢?我的理解是,在预测每一个类别的可能性的时候,其实还是使用了线性回归的思想。这样就可以理解了,一共三类,每一类一个仿射函数,每一个仿射函数是一个线性回归,那么每一个仿射函数就有四个带下标的w,一个b。那么三个仿射函数就是12个w(权重),3个b(偏置)。

与线性回归一样,softmax回归也是一个单层神经网络。由于计算每个输出o1、o2和o3取决于所有输入x1、x2、x3和x4,所以softmax回归的输出层也是全连接层。

这个没啥好说的,直接看下图就明白了。

通过向量形式表达为o = Wx + b,这是一种更适合数学和编写代码的形式。由此,我们已经将所有权重放到一个3×4矩阵中。对于给定数据样本的特征x,我们的输出是由权重与输入特征进行矩阵-向量乘法再加上偏置b得到的。

其实在看到一个方程组的时候,我就有预感要用系数矩阵了。其实上面这段引用就是把方程组变成了系数矩阵W×自变量矩阵x + 偏置矩阵b = 输出矩阵o,有点抽象,看一下下图就明白了:

1.3 全连接层的参数开销

具体来说,对于任何具有d个输入和q个输出的全连接层,参数开销为O(dq),这个数字在实践中可能高得令人望而却步。幸运的是,将d个输入转换为q个输出的成本可以减少到O(dq/n),其中超参数n可以由我们灵活指定,以在实际应用中平衡参数节约和模型有效性。

这几句话的意思是,如果按照本篇文章1.2的方法去解决现实中的一些问题,可能会遭遇参数爆炸。这个在后面多层感知机中提到。

1.4 softmax运算

现在我们将优化参数以最大化观测数据的概率。为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。
我们希望模型的输出y*j可以视为类j的概率,然后选择具有最大输出值的类别argmaxj(yj)作为我们的预测。例如,如果y*1、y*2、y*3分别是0.1、0.8、0.1,那么我们预测的类别是2。

这个其实也好理解,将模型参数优化,之后让观测数据对应的预测概率最大化。之后设置一个输出条件(阈值),比如选择具有最大概率的标签。也就是说,输出的是具有最大概率的标签。

然而我们能否将未规范化的预测o直接视作我们感兴趣的输出呢?答案是否定的。因为将线性层的输出直接视为概率时存在一些问题:一方面,我们没有限制这些输出数字的总和为一。另一方面,根据输入的不同,它们可以为负值。这些违反了2.6节所说的概率基本公理。

啊,这里提到了一句书中2.6节的概率基本公理,看一下是什么:

概率(probability)可以被认为是将集合映射到真实值的函数。在给定的样本空间s中,事件A的概率,表示为P(A),满足以下属性:
·对于任意事件A,其概率从不会是负数,即P(A)≥0;
·整个样本空间的概率为1,即P(S) = 1;
·对于互斥(mutually exclusive)事件(对于所有i ≠ j都有Ai∩Aj=∅)的任意一个可数序列A1,A2……,序列中任意一个事件发生的概率等于它们各自发生的概率之和,即:

这是本节涉及到的第一个概率论知识,这里是概率的公理化定义,看看当时我学概率论时候记得另外两种定义:
(1)描述性定义:通常将随机事件A发生的可能性大小的度量(非负值)称为事件A发生的概率,记为P(A)。
(2)统计性定义:在相同条件下做重复试验,事件A出现的次数k和总的试验次数n之比k/n,称为事件A在这n次试验中出现的频率,当试验次数n充分大时,频率将’稳定"于某常数p“附近”,n越大,频率偏离该常数的可能性越小,这个常数P称为事件A的概率。
好家伙一堆定义,我现在看都头疼。其实意思很简单,概率必须满足公理化定义的三个条件。举一个小栗子:假设今天去打靶,这一枪打i环的概率是pi,假设必打中靶子。
(1)pi必须≥0。假设你打了十环,那么打九环的概率最低也是0,不可能出现概率为负数。
(2)pi相加必定总和为一。因为看一下统计性定义就很清楚,打中i环的概率×总次数 = 打中i环的次数。所有的打中i环的次数累加起来必定等于总次数,也就是所有打中i环的次数的和/总次数 = 1。
(3)打中9环和打中10环是互斥事件,不能说你这一枪既算9环也算10环不然怎么积分呢,那么打中9环的概率+打中10环的概率 = 打中9环或10环的概率。

经过一大长串的描述,可以看出直接把线性层输出作为概率是不合适的。因为线性层的输出可能为负数(没有限制线性层输出必定大于零,线性层的输出可能是整个实数集),而且三个输出加起来不一定为1(理由同上)。
所以需要对线性层的输出进行一个规范化。

要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。此外,我们需要一个训练目标,来鼓励模型精准地估计概率。在分类器输出0.5的所有样本中,我们希望这些样本有一半实际上属于预测的类。这个属性叫作校准。

这几句话其实就是描述规范化需要做什么事,首先要将输出限制在非负数,也就是出现负数的话要转换为正数。然后是输出要满足公式概率 = (事件出现的次数/总次数)。

softmax函数将未规范化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未规范化的预测求幂,这样可以确保输出非负。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。如下式:

这里是一个非常天才的想法。
首先我们知道线性层的输出是属于实数集的,所以需要对属于实数集的数进行转化为非负数集的数,那么需要找一个定义域是实数集、值域是非负数集的函数,同时这个函数还必须可导(为了反向传播(如果你还记得反向传播要计算导数),无法求导就无法反向传播优化参数了),那么指数函数是一个很好的选择。以e为底的指数函数满足条件“定义域是实数集、值域是非负数集”,exp(线性层的输出)这样就把线性层输出转化为非负数,满足了概率定义的第一条。
第二步可以理解为“归一化”,也就是经过上步转化为非负数之后,我们将三个线性层输出转化成的非负数叫a、b、c,之后让第一个类别的概率为p1=a/(a+b+c)、让第二个类别的概率为p2=b/(a+b+c)、让第三个类别的概率为p3=c/(a+b+c),之后可以发现p1+p2+p3 = a/(a+b+c)+b/(a+b+c)+c/(a+b+c) = (a+b+c)/(a+b+c) = 1,满足了概率定义的第二条和第三条。

softmax运算不会改变未规范化的预测o之间的顺序,只会确定分配给每个类别的概率。因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别:

这个也比较好理解,复合函数的单调性。以e为底的函数单调性是增函数,exp(oi)/Σkexp(ok)也是增函数,两个增函数复合在一起也是增函数。那么y*j = softmax(oj)也是增函数。也就是不会改变未规范化的预测o的顺序,只会确定分配给每个类别的概率。(和求导取指数函数简化求导求极值有相似的地方吧)

尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。因此,softmax回归是一个线性模型。

其实这里我的理解是,机器学习中的线性模型和非线性模型并不是取决于仿射函数是线性模型还是非线性模型,只要输出是由输入特征的仿射变换决定的,就是线性模型。(可能我的理解有误。)

1.5 小批量样本的矢量化

为了提高计算效率并且充分利用GPU,我们通常会针对小批量数据执行矢量计算。假设我们读取了一个批量的样本X,其中特征维度(输入数量)为d,批量大小为n。此外,假设我们在输出中有q个类别。那么小批量特征为X∈Rn×d,权重为W∈Rd×q,偏置为b∈R1×q。softmax回归的矢量计算表达式为:

其实没啥好讲的,参数矩阵和输入矩阵都在本篇文章1.2节中描述了。只需要知道为了提高计算效率,其实执行的是矢量(矩阵)计算。

1.6 损失函数

softmax函数给出了一个向量y,我们可以将其视为“对给定任意输入x的每个类的条件概率”。假设整个数据集{X,Y}有n个样本,其中索引i的样本由特征向量x(i)和独热标签向量y(i)组成。我们可以将估计值与实际值进行比较:

根据最大似然估计,我们最大化P(Y|X),相当于最小化负对数似然:

其中,对于任何标签y和模型预测y^,损失函数为:

来了来了,之前提到的交叉熵损失。交叉熵损失推导有两种思路,第一种是最大似然估计,这也是比较简单的思路。还有一种是通过信息论的角度推导,这个需要一定的信息论知识基础,待会再讲(感觉这篇文章要写两万字了)。先来看看如何从最大似然估计推导。
什么是最大似然估计?
假设有一个样本集X{x1,x2,x3,……,xn},样本集中的样本符合独立同分布,其概率密度为p(x|θ),其中x对应的是样本点横坐标,θ代表模型中的参数。
那么在这个参数θ下,形成这个样本集的概率是P(x1,x2,x3,……,xn|θ) = p(x1|θ)×p(x2|θ)×p(x3|θ)×……×p(xn|θ)=L(θ),这个L(θ)就是参数θ相对于样本集X的似然函数。
这里的θ是个变量,θ是影响模型概率密度的参数(注意区分不是神经网络模型的参数!)。那么我们可以计算出在θ等于多少时,这个样本集出现的概率最大,也就是L(θ)最大。既然是求L(θ)最大,这就变成了求函数极大值,先对L(θ)取对数(相乘项多,取对数化简求导过程),得到:

然后对log(L(θ))求导,得:

然后令(lnL(θ))’ = 0,解出最大值对应的θ*。θ*就是θ的最大似然估计。
(写了一遍推导我对极大似然估计的公式记得更深了)
那么,从上面可以看出来,我们其实是根据已知的数据(样本集X),反推出最有可能导致已知数据的概率密度的参数(θ)。
那么极大似然估计和交叉熵损失有什么关系呢?(脑中划过闪电)
首先我们知道我们的模型不是连续分布,而是离散分布。也就是说,概率分布将替代概率密度(两者其实一回事,只不过概率分布是函数值是离散的,概率密度是函数值连续的)。
之后我们的样本集(X,Y),X是输入特征向量(x1,x2,x3,x4),Y是独热编码向量。类别对应的独热编码是 y∈{y1,y2,y3,……,yn}。输入经过softmax函数后,得到。这个对应的是x(i) = {x1,x2,x3,x4}(注意不止一张图片,所以x1、x2、x3、x4也是会不一样的),对应的y(i)是独热编码中的一个。
那么概率分布就是:P(这个x(i)所属的类别)~{P(y(1)|x(1)),P(y(2)|x(2)),P(y(3)|x(3)),……,P(y(n)|x(n))},也就是P服从于{……}

那么,先构建一个似然函数,就变成了下式:

这里还有一点要说明,yi是独热向量中的对应的位。其实这里可以理解为:一个独热编码其实也是一个样本集,这个样本集用1来表示这个类别被选中的事件,而每个类别被选中的概率是……
既然似然函数已经求出来了,接下来就是对似然函数取对数,理论上log的底数随便取数,但是为了简便,这里log的底数取e:

接下来就是要最大似然估计了,也就是要让输入这幅图片对应的这个类别的概率最大,那么也就是找-ln(θ)的最小值(ln(x)是增函数,加负号就是减函数,求ln(x)的最大值就是求-ln(x)的最小值)。

之后得到交叉熵损失函数:

然后会发现独热编码中只有一位是1,也就是yi中只有一个是1,上式也就变成下式:

1.7 softmax函数及其导数

这里有一个很有意思的东西,印刷的书中并没提及,而在d2l网站的pdf中提到了,等我折腾明白再回来填这个坑。

1.8 信息论基础

信息论涉及编码、解码、发送以及尽可能简洁地处理信息或数据。

1.8.1 熵

信息论的核心思想是量化数据中的信息内容。在信息论中,该数值被称为分布P的熵。可以通过以下方程得到:

信息论的基本定理指出,为了对从分布p中随机抽取的数据进行编码,我们至少需要H[P]“纳特”对其进行编码。“纳特”相当于比特,但是对数底为e而不是2。因此,一个纳特是1/log(2)≈1.44比特。

1.8.2 信息量

但是,如果我们不能完全预测每一个事件,那么我们有时可能会感到“惊异”。克劳德·香农决定用信息量log(1/P(j)) = -logP(j)来量化这种惊异程度。在观察一个事件j时,并赋予它(主观)概率P(j)。当我们赋予一个事件较低的概率时,我们的惊异会更大,该事件的信息量也就更大。上一小节定义的熵,是当分配的概率真正匹配数据生成过程时的信息量的期望。

二、代码实现

2.1 图像分类数据集

MNIST数据集是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。我们将使用类似但更复杂的Fashion-MNIST数据集。

这没啥好介绍的了,图像分类的数据集有很多。

2.1.1 导入库

%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2ld2l.use_svg_display()

2.1.2 读取数据集

我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。

trans = transforms.ToTensor()         #transforms.ToTensor()可以将PIL格式转为float32的Tensor
mnist_train = torchvision.datasets.FashionMNIST(root="D:/data",train=True,transform=trans,download=True)     #训练集下载存储到D盘data文件夹
mnist_test = torchvision.datasets.FashionMNIST(root="D:/data",train=True,transform=trans,download=True)     #测试集下载存储到D盘data文件夹


数据集下载太慢了,明天再来放图
————————————————分割线——————————————————————

Fashion-MNIST由10个类别的图像组成,每个类别由训练数据集中的6000张图像和测试数据集中的1000张图像组成。因此训练集和测试集分别包含60000和10000张图像。测试数据集不会用于训练,只用于评估模型性能。

len(mnist_train),len(mnist_test)

每个输入图像的高度和宽度均为28像素。数据集由灰度图像组成,其通道数为1.为了简洁起见,本书将高度h像素、宽度w像素图像的形状记为h×w或(h,w)

mnist_train[0][0].shape

Fashion-MNIST中包含10个类别,分别是t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。以下函数用于在数字标签索引及其文本名称之间进行转换。

def get_fashion_mnist_labels(labels):text_labels = ['t-shirt','trouser','puulover','dress','coat','sandal','shirt','sneaker','bag','ankle boot']return [text_labels[int(i)] for i in labels]

现在创建一个函数来可视化这些样本。

def show_images(imgs,num_rows,num_cols,titles=None,scale=1.5):figsize = (num_cols * scale,num_rows * scale)_,axes = d2l.plt.subplots(num_rows,num_cols,figsize=figsize)axes = axes.flatten()for i,(ax,img) in enumerate(zip(axes,imgs)):if torch.is_tensor(img):ax.imshow(img.numpy())else:ax.imshow(img)ax.axes.get_xaxis().set_visible(False)ax.axes.get_xaxis().set_visible(False)if titles:ax.set_title(titles[i])return axes

现在看看训练集前几个样本的图像和对应的标签。

X,y = next(iter(data.DataLoader(mnist_train,batch_size=18)))
show_images(X.reshape(18,28,28),2,9,titles=get_fashion_mnist_labels(y))

2.1.3 读取小批量

为了使我们在读取训练集和测试集时更容易,我们使用内置的迭代器,而不是从零开始创建。在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size。通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。

batch_size = 256def get_dataloader_workers():      return 4                       #使用4个进程来读取数据train_iter = data.DataLoader(mnist_train,batch_size,shuffle=True,num_workers=get_dataloader_workers())
#mnist_train是数据集,batch_size是小批量的大小,shuffle是每次读取时是否打乱顺序,num_workers是使用多少进程读取数据timer = d2l.Timer()     #测试读取时间
for X,y in train_iter:continue
f'(timer.stop:.2f) sec'

注:这里可能会报一个DataLoader worker (pid(s) 5344, 5644, 10908) exited unexpectedly的错,这里可以将4改为0,禁止多线程。同时,这里修改了batch_size,是因为电脑内存不够,所以修改batch_size,不修改的话后面可能会报Unable to allocate 44.9 MiB for an array with shape (47040000,) and data type uint8。

batch_size = 128def get_dataloader_workers():  #@save"""使用4个进程来读取数据"""return 0train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers())


改动上述几个地方后,可以发现读取数据集速度明显下降。

2.1.4 整合所有组件

现在我们定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。这个函数返回训练集和验证集的数据迭代器。此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一个形状。

def load_data_fashion_mnist(batch_size,resize=None):if resize:trans.insert(0,transforms.Resize(resize))trans = torchvision.transforms.ToTensor()         #transforms.ToTensor()可以将PIL格式转为float32的Tensormnist_train = torchvision.datasets.FashionMNIST(root="D:/data",train=True,transform=trans,download=True)     #训练集下载存储到D盘data文件夹mnist_test = torchvision.datasets.FashionMNIST(root="D:/data",train=True,transform=trans,download=True)     #测试集下载存储到D盘data文件夹return (data.DataLoader(mnist_train,batch_size,shuffle=True,num_workers=get_dataloader_workers()),data.DataLoader(mnist_test,batch_size,shuffle=True,num_workers=get_dataloader_workers()))

下面通过指定resize参数来测试load_data_fashion_mnist函数的图像大小调整功能。

train_iter,test_iter = load_data_fashion_mnist(32,resize=64)
for X,y in train_iter:print(X.shape,X.dtype,y.shape,y.dtype)break

2.2 从零实现softmax

欲知后事如何,且听明天分解。
——————————————分割线————————————————
昨天睡得不是很好,而且背了600多个单词耗得精神有点多,先把代码一贴,之后就去休息了。
代码我和书中有稍微一点不一样(一点.jpg),因为主要还是昨天读取数据集的问题,我只对一个函数进行了一些修改。
——————————————分割线————————————————
争取今天写完吧,再拖下去来不及写多层感知机和slam的笔记了。

2.2.1 导入库

import torch
from IPython import display
from d2l import torch as d2limport torchvision
from torch.utils import data
from torchvision import transforms

2.2.2 读取数据集

def load_data_fashion_mnist(batch_size, resize=None):  #@save"""下载Fashion-MNIST数据集,然后将其加载到内存中"""trans = [transforms.ToTensor()]if resize:trans.insert(0, transforms.Resize(resize))trans = transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST(root="D:/data", train=True, transform=trans, download=True)mnist_test = torchvision.datasets.FashionMNIST(root="D:/data", train=False, transform=trans, download=True)return (data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers()),data.DataLoader(mnist_test, batch_size, shuffle=False,num_workers=get_dataloader_workers()))

2.2.3 创建迭代器

batch_size = 128def get_dataloader_workers():return 0        #return之后的数字代表使用多少进程来读取数据,0代表不使用多进程train_iter,test_iter = load_data_fashion_mnist(batch_size)  #读取数据集创建训练集迭代器和测试集迭代器

这里的batch_size书中给的是256,我电脑内存不够就缩减成128。

2.2.4 初始化模型参数

num_inputs = 784             #Fashion-MNIST一张图片是28*28,也就是784个像素,一个仿射函数就有784个权重
num_outputs = 10             #Fashion-MNIST有十种图片,所以有十个输出W = torch.normal(0,0.01,size = (num_inputs,num_outputs),requires_grad=True)     #正态分布初始化权重
b = torch.zeros(num_outputs,requires_grad=True)                                 #偏置b置0

这里要说明的是,因为十个种类,所以有十个输出,而且会有十个仿射函数。每一个仿射函数都有784个权重wi和1个偏置b。那么参数矩阵的形状就是(784,10),偏置b的形状是(10,1)。
而且注意这里,其实我们是把28*28的图像展成按行拼接一个784长度的一维向量。再往后讨论卷积神经网络的时候会利用图像空间结构的特征。在这里我们把图片展成一个向量。

2.2.5 定义softmax运算

回想一下,实现softmax由三个步骤组成:
1.对每个项求幂(使用exp);
2.对每一行求和(小批量每个样本是一行),得到每个样本的规范化参数;
3.将每一行除以其规范化常数,确保结果的和为1。

def softmax(X):X_exp = torch.exp(X)                      #exp()实现指数运算partition = X_exp.sum(1,keepdim=True)    #然后按照输出矩阵的行相加return X_exp / partition                 #最后将每一项除以求和

2.2.6 定义模型

注意,将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。

def net(X):return softmax(torch.matmul(X.reshape((-1,W.shape[0])),W)+b)

X与W矩阵相乘后然后再加b,最后将计算结果传入softmax()函数。

2.2.7 定义损失函数

def cross_entropy(y_hat,y):return - torch.log(y_hat[range(len(y_hat)),y])

这里稍微有点复杂,y_hat矩阵存储着每张图片对每个种类的预测的正确率,y矩阵存储着每个图片对应的正确种类。y_hat[range(len(y_hat)),y]就可以取出对每一张图片预测概率中正确种类对应的概率,然后对这个概率取负对数,之后就可以变为损失函数了。

2.2.8 测试softmax和损失函数

y = torch.tensor([0,2])
y_hat = torch.tensor([[0.1,0.3,0.6],[0.3,0.2,0.5]])
y_hat[[0,1],y]

X = torch.normal(0,1,(2,5))       #从均值为0,标准差为1的正态分布中取一个2*5的矩阵
X_prob = softmax(X)               #softmax运算
X_prob,X_prob.sum(1)

cross_entropy(y_hat,y)

2.2.9 分类精度

def accuracy(y_hat,y):if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:y_hat = y_hat.argmax(axis=1)cmp = y_hat.type(y.dtype)  == yreturn float(cmp.type(y.dtype).sum())
def evaluate_accuracy(net, data_iter): if isinstance(net, torch.nn.Module):net.eval() metric = Accumulator(2) with torch.no_grad():for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())return metric[0] / metric[1]

2.2.10 对变量累加

class Accumulator:  #@save"""在n个变量上累加"""def __init__(self, n):self.data = [0.0] * ndef add(self, *args):self.data = [a + float(b) for a, b in zip(self.data, args)]def reset(self):self.data = [0.0] * len(self.data)def __getitem__(self, idx):return self.data[idx]

2.2.11 实现一个周期的训练

def train_epoch_ch3(net, train_iter, loss, updater):  if isinstance(net, torch.nn.Module):net.train()metric = Accumulator(3)for X, y in train_iter:y_hat = net(X)l = loss(y_hat, y)if isinstance(updater, torch.optim.Optimizer):updater.zero_grad()l.mean().backward()updater.step()else:l.sum().backward()updater(X.shape[0])metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())return metric[0] / metric[2], metric[1] / metric[2]

2.2.12 绘图类

class Animator:  #@save"""在动画中绘制数据"""def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,ylim=None, xscale='linear', yscale='linear',fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,figsize=(3.5, 2.5)):# 增量地绘制多条线if legend is None:legend = []d2l.use_svg_display()self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)if nrows * ncols == 1:self.axes = [self.axes, ]# 使用lambda函数捕获参数self.config_axes = lambda: d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)self.X, self.Y, self.fmts = None, None, fmtsdef add(self, x, y):# 向图表中添加多个数据点if not hasattr(y, "__len__"):y = [y]n = len(y)if not hasattr(x, "__len__"):x = [x] * nif not self.X:self.X = [[] for _ in range(n)]if not self.Y:self.Y = [[] for _ in range(n)]for i, (a, b) in enumerate(zip(x, y)):if a is not None and b is not None:self.X[i].append(a)self.Y[i].append(b)self.axes[0].cla()for x, y, fmt in zip(self.X, self.Y, self.fmts):self.axes[0].plot(x, y, fmt)self.config_axes()display.display(self.fig)display.clear_output(wait=True)

2.2.13 模型训练

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):train_metrics = train_epoch_ch3(net, train_iter, loss, updater)test_acc = evaluate_accuracy(net, test_iter)animator.add(epoch + 1, train_metrics + (test_acc,))train_loss, train_acc = train_metricsassert train_loss < 0.5, train_lossassert train_acc <= 1 and train_acc > 0.7, train_accassert test_acc <= 1 and test_acc > 0.7, test_acc

2.2.14 梯度下降

lr = 0.1
def updater(batch_size):return d2l.sgd([W,b],lr,batch_size)

2.2.15 训练模型并绘图

这里特别好玩,因为之前实现过一个画图类,这地方的画图是动态的,如果电脑内存不够的话谨慎运行程序(我看直播跑这个程序之后电脑死机了)

num_epochs = 10
train_ch3(net,train_iter,test_iter,cross_entropy,num_epochs,updater)

2.2.16 模型预测

def predict_ch3(net, test_iter, n=6):  for X, y in test_iter:breaktrues = d2l.get_fashion_mnist_labels(y)preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))titles = [true +'\n' + pred for true, pred in zip(trues, preds)]d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])predict_ch3(net, test_iter)

2.3 softmax简洁实现

欲知后事如何,且听明天分解。

总结

还没写完,今天写的有点累了,主要是推交叉熵公式花了点时间,明天再写。
放一下引用或参考文章:
最大似然估计:
https://blog.csdn.net/weixin_37697191/article/details/88661897
https://blog.csdn.net/zouxy09/article/details/8537620
https://blog.csdn.net/ftfy123/article/details/104789672
交叉熵损失推导:
https://blog.csdn.net/qq_38147421/article/details/117216513
https://zhuanlan.zhihu.com/p/349435290
transforms.ToTensor():https://www.csdn.net/tags/OtDaIg0sNzY3NjItYmxvZwO0O0OO0O0O.html
Unable to allocate 44.9 MiB for an array with shape (47040000,) and data typ:
https://blog.csdn.net/u014427391/article/details/123982123
https://blog.csdn.net/weixin_44152421/article/details/106543679
DataLoader worker (pid(s) 5344, 5644, 10908) exited unexpectedly:
https://blog.csdn.net/SHERLOCKSALVATORE/article/details/124511501
https://blog.csdn.net/m0_55868614/article/details/123651223

动手学深度学习笔记3.4+3.5+3.6+3.7相关推荐

  1. 动手学深度学习笔记(1)

    动手学深度学习 深度学习简介 深度学习简介 举一个小的例子,如何编写一个程序,让机器识别我输入的图片是否有一只猫?我们需要哪些值来帮助我们确定?事实上,要想解读图像中的内容,需要寻找仅仅在结合成千上万 ...

  2. [深度学习]动手学深度学习笔记-3

    Task-2 文本预处理:语言模型:循环神经网络基础 3.1 文本预处理 文本是一类序列数据,一篇文章可以看作是字符或单词的序列,本节将介绍文本数据的常见预处理步骤,预处理通常包括四个步骤: 读入文本 ...

  3. [深度学习]动手学深度学习笔记-5

    Task2--梯度消失.梯度爆炸 5.1 梯度消失与梯度爆炸的概念 深度神经网络训练的时候,采用的是反向传播方式,该方式使用链式求导,计算每层梯度的时候会涉及一些连乘操作,因此如果网络过深. 那么如果 ...

  4. [深度学习]动手学深度学习笔记-14

    Task9--目标检测基础 14.1 目标检测和边界框 在前面的一些章节中,我们介绍了诸多用于图像分类的模型.在图像分类任务里,我们假设图像里只有一个主体目标,并关注如何识别该目标的类别.然而,很多时 ...

  5. 动手学深度学习笔记4——微积分自动微分

    目录 1.微积分 1.1导数和微分 1.2偏导数 1.3梯度 1.4链式法则 1.5小结 1.6练习 2.自动微分 2.1一个简单的例子 2.2非标量变量的反向传播 2.3分离计算 2.4Python ...

  6. [深度学习]动手学深度学习笔记-15

    Task6--批量归一化和残差网络 15.1 批量归一化 本节我们介绍批量归一化(batch normalization)层,它能让较深的神经网络的训练变得更加容易.在(实战Kaggle比赛:预测房价 ...

  7. 动手学深度学习笔记(一)——权重衰退

            一直在csdn上学习别人发的博客,第一次尝试自己写博客,今天周日,在实验室小学一会,发现自己有些不明白的点突然想明白了,想记录一下在学习过程中遇到的难点或者有些迷惑的地方,希望自己以后 ...

  8. [深度学习]动手学深度学习笔记-12

    Task10--GAN:DCGAN 12.1 GAN原理 GAN的基本原理其实非常简单,这里以生成图片为例进行说明.假设我们有两个网络,G(Generator)和D(Discriminator).正如 ...

  9. [深度学习]动手学深度学习笔记-11

    Task06--梯度下降 11.1 梯度下降法 梯度 导数我们都非常熟悉,既可以表示某点的切线斜率,也可以表示某点变化率,公式如下表示: f′(x)=lim⁡Δx→0ΔyΔx=lim⁡Δx→0f(x+ ...

最新文章

  1. UVA1025 城市里的间谍 A Spy in the Metro(2003 ICPC world final)(DAG上DP)
  2. GTX1080 LetNet-5 CPU GPU cuDNN5.1 时间对比
  3. Shell---判断(if)和分支(case)
  4. java搭配oracle,Java联接Oracle(高级篇)
  5. 我怀疑对象做了什么对不起我的事......
  6. C语言中的“悬空指针”和“野指针”是什么意思?
  7. docker添加新的环境变量_Docker环境变量
  8. L2-008 最长对称字串 以下标i展开
  9. 推荐!可视化垃圾回收算法(转)
  10. android 蓝牙通信编程
  11. Unity3D之UGUI基础2:Text文本
  12. ubuntu 16.04 + zabbix 3.4 + postgresql shell
  13. 谷歌浏览器网页翻译失效,无法翻译成中文,且谷歌翻译api报404问题
  14. 小程序与MySQL数据库的交互_微信小程序与服务器的交互原理
  15. 诚之和:太平鸟难渡“抄袭劫”?
  16. halcon学习-算子学习
  17. 查看linux服务器存储空间状况
  18. C++ 内联函数/宏/outo/for/nullptr
  19. OpenCV图形处理
  20. 如何制作新年公众号封面图?手把手教你在线编辑图片

热门文章

  1. 北大计算机所孙俊教授,教育部人工智能咨询专家孙俊:科技怎样赋能智慧教育?...
  2. CSS中英文换行问题
  3. 「全网最细」接口测试怎么测?接口测试的流程和步骤
  4. Android连接多个usb,Android实现USB连接
  5. Aimersoft iMusic for mac(音乐下载传输工具)
  6. getdate、dateadd、datediff、datename、datepart关于日期函数的用法及理解
  7. java 并发框架源码_Java并发编程高阶技术-高性能并发框架源码解析与实战
  8. vue中实现点击复制文本内容之clipboard
  9. Linux(Ubuntu16.04)自学笔记,资源整理
  10. 工作中常见的开会问题