引言

- ## 日常生活中的机器学习
- ## 机器学习中的关键组件
  collapsed:: true
	- ### 数据
	- ### 模型
	- ### 目标函数
	- ### 优化算法
- ## 各种机器学习问题
  collapsed:: true
	- ### 监督学习
		- #### 回归
		- #### 分类
		- #### 标记问题
		- #### 搜索
		- #### 推荐系统
		- #### 序列学习
	- ### 无监督学习
	  collapsed:: true
		- [[监督学习]]模型像一个打工仔,有一份极其专业的工作和一位极其平庸的老板。 老板站在身后,准确地告诉模型在每种情况下应该做什么,直到模型学会从情况到行动的映射。相反,如果工作没有十分具体的目标,就需要“自发”地去学习了。 比如,老板可能会给我们一大堆数据,然后要求用它做一些数据科学研究,却没有对结果有要求。 这类数据中不含有“目标”的机器学习问题通常被为[[无监督学习]]
		- 那么无监督学习可以回答什么样的问题呢?来看看下面的例子。
			- *聚类*(clustering)问题:没有标签的情况下,我们是否能给数据分类呢?比如,给定一组照片,我们能把它们分成风景照片、狗、婴儿、猫和山峰的照片吗?同样,给定一组用户的网页浏览记录,我们能否将具有相似行为的用户聚类呢?
			- *主成分分析*(principal component analysis)问题:我们能否找到少量的参数来准确地捕捉数据的线性相关属性?比如,一个球的运动轨迹可以用球的速度、直径和质量来描述。再比如,裁缝们已经开发出了一小部分参数,这些参数相当准确地描述了人体的形状,以适应衣服的需要。另一个例子:在欧几里得空间中是否存在一种(任意结构的)对象的表示,使其符号属性能够很好地匹配?这可以用来描述实体及其关系,例如“罗马” \(-\) “意大利” \(+\) “法国” \(=\) “巴黎”。
			- *因果关系*(causality)和*概率图模型*(probabilistic graphical models)问题:我们能否描述观察到的许多数据的根本原因?例如,如果我们有关于房价、污染、犯罪、地理位置、教育和工资的人口统计数据,我们能否简单地根据经验数据发现它们之间的关系?
			- *生成对抗性网络*(generative adversarial networks):为我们提供一种合成数据的方法,甚至像图像和音频这样复杂的非结构化数据。潜在的统计机制是检查真实和虚假数据是否相同的测试,它是无监督学习的另一个重要而令人兴奋的领域。
	- ### 与环境互动
	- ### 强化学习
		- [[深度强化学习]]将深度学习应用于强化学习的问题,是非常热门的研究领域。
		- 在强化学习问题中,智能体(agent)在一系列的时间步骤上与环境交互。 在每个特定时间点,智能体从环境接收一些*观察*,并且必须选择一个*动作*,然后通过某种机制(有时称为执行器)将其传输回环境,最后智能体从环境中获得*奖励*。 此后新一轮循环开始,智能体接收后续观察,并选择后续操作,依此类推。
		- ![image.png](../assets/image_1688437004296_0.png)
		- 请注意,强化学习的目标是产生一个好的*策略*。 强化学习智能体选择的“动作”受策略控制,即一个从环境观察映射到行动的功能。
- ## 起源
  collapsed:: true
	- 为了解决各种各样的机器学习问题,[[深度学习]]提供了强大的工具。 虽然许多深度学习方法都是最近才有重大突破,但使用数据和神经网络编程的核心思想已经研究了几个世纪。
	- 事实上,人类长期以来就有分析数据和预测未来结果的愿望,而自然科学大部分都植根于此。 例如,伯努利分布是以[雅各布•伯努利(1654-1705)](https://en.wikipedia.org/wiki/JacobuBernoulli)命名的。 而高斯分布是由[卡尔•弗里德里希•高斯(1777-1855)](https://en.wikipedia.org/wiki/Carl_Friedrich_Gauss)发现的, 他发明了最小均方算法,至今仍用于解决从保险计算到医疗诊断的许多问题。
	- 随着数据的收集和可获得性,统计数据真正实现了腾飞。 [罗纳德·费舍尔(1890-1962)](https://en.wikipedia.org/wiki/Ronald_-Fisher)对统计理论和在遗传学中的应用做出了重大贡献。 他的许多算法(如线性判别分析)和公式(如费舍尔信息矩阵)至今仍被频繁使用。
	- [[机器学习]]的第二个影响来自[克劳德·香农(1916–2001)](https://en.wikipedia.org/wiki/Claude_Shannon)的信息论和[艾伦·图灵(1912-1954)](https://en.wikipedia.org/wiki/Alan_Turing)的计算理论。
	- 另一个影响可以在[[神经科学]]和[[心理学]]中找到。 其中,最古老的算法之一是[唐纳德·赫布 (1904–1985)](https://en.wikipedia.org/wiki/Donald_O._Hebb)开创性的著作《行为的组织》 ([Hebb and Hebb, 1949](https://zh-v2.d2l.ai/chapter_references/zreferences.html#id62)) 。 他提出神经元通过积极强化学习,是Rosenblatt感知器学习算法的原型,被称为“赫布学习”。 这个算法也为当今深度学习的许多随机梯度下降算法奠定了基础:强化期望行为和减少不良行为,从而在神经网络中获得良好的参数设置。
- ## [[深度学习]]的发展
  collapsed:: true
	- 大约2010年开始,那些在计算上看起来不可行的神经网络算法变得热门起来,实际上是以下两点导致的: 其一,随着互联网的公司的出现,为数亿在线用户提供服务,大规模数据集变得触手可及; 另外,廉价又高质量的传感器、廉价的数据存储(克莱德定律)以及廉价计算(摩尔定律)的普及,特别是GPU的普及,使大规模算力唾手可得。
- ## [[深度学习]]的成功案例
  collapsed:: true
	- 智能助理、数字助理、物体识别、游戏、自动驾驶等等。
- ## 特点
  collapsed:: true
	- 如前所述,[[机器学习]]可以使用数据来学习输入和输出之间的转换,例如在语音识别中将音频转换为文本。 在这样做时,通常需要以适合算法的方式表示数据,以便将这种表示转换为输出。[[深度学习]]是“深度”的,模型学习了许多“层”的转换,每一层提供一个层次的表示。 例如,靠近输入的层可以表示数据的低级细节,而接近分类输出的层可以表示用于区分的更抽象的概念。 由于*表示学习*(representation learning)目的是寻找表示本身,因此深度学习可以称为“多级表示学习”。
- ## 小结
  collapsed:: true
	- 机器学习研究计算机系统如何利用经验(通常是数据)来提高特定任务的性能。它结合了统计学、数据挖掘和优化的思想。通常,它是被用作实现人工智能解决方案的一种手段。
	  logseq.order-list-type:: number
	- 表示学习作为机器学习的一类,其研究的重点是如何自动找到合适的数据表示方式。深度学习是通过学习多层次的转换来进行的多层次的表示学习。
	  logseq.order-list-type:: number
	- 深度学习不仅取代了传统机器学习的浅层模型,而且取代了劳动密集型的特征工程。
	  logseq.order-list-type:: number
	- 最近在深度学习方面取得的许多进展,大都是由廉价传感器和互联网规模应用所产生的大量数据,以及(通过GPU)算力的突破来触发的。
	  logseq.order-list-type:: number
	- 整个系统优化是获得高性能的关键环节。有效的深度学习框架的开源使得这一点的设计和实现变得非常容易。
	  logseq.order-list-type:: number
  • 预备知识

    • 2.1. 数据操作

      collapsed:: true
      • 为了能够完成各种数据操作,我们需要某种方法来存储和操作数据。 通常,我们需要做两件重要的事:(1)获取数据;(2)将数据读入计算机后对其进行处理。 如果没有某种方法来存储数据,那么获取数据是没有意义的。
      • 2.1.1. 入门

        • # 导入是torch,虽然它被称为PyTorch。
          import torch
          
        • 张量表示一个由数值组成的数组,这个数组可能有多个维度。 具有一个轴的张量对应数学上的向量; 具有两个轴的张量对应数学上的矩阵; 具有两个轴以上的张量没有特殊的数学名称。
        • # 使用arange创建一个行向量x,x包含以0开始的前12个整数(默认创建为整数)。
          x = torch.arange(12)
          # 通过张量的shape属性来访问张量(沿每个轴的长度)的形状 。
          x.shape
          # 如果只想知道张量中元素的总数,即形状的所有元素乘积,可以检查它的大小(size)。
          x.numel()
          # 要想改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数。要重点说明一下,虽然张量的形状发生了改变,但其元素值并没有变。注意,通过改变张量的形状,张量的大小不会改变。
          X = x.reshape(3, 4)
          # 有时我们想通过从某个特定的概率分布中随机采样来得到张量中每个元素的值。以下代码创建一个形状为(3,4)的张量。 其中的每个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样。
          torch.randn(3, 4)
          # 我们还可以通过提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值。在这里,最外层的列表对应于轴0,内层的列表对应于轴1。
          torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
          
      • 2.1.2. 运算符

        • ((64a3ab89-7e94-4d23-83c2-e4437b726fa8))
        • 对于任意具有相同形状的张量,常⻅的标准算术运算符(+、-、*、/和**)都可以被升级为按元素运算。
        • x = torch.tensor([1.0, 2, 4, 8])
          y = torch.tensor([2, 2, 2, 2])
          x + y, x - y, x * y, x / y, x ** y  # **运算符是求幂运算
          
        • 除了按元素计算外,我们还可以执行线性代数运算,包括向量点积和矩阵乘法。
        • 我们也可以把多个张量连结(concatenate)在一起,把它们端对端地叠起来形成一个更大的张量。我们只需要提供张量列表,并给出沿哪个轴连结。
      • 2.1.3. 广播机制

        • 在某些情况下,即使形状不同,我们仍然可以通过调用广播机制来执行按元素操作。
      • 2.1.4. 索引和切片

        • 就像在任何其他Python数组中一样,张量中的元素可以通过索引访问。与任何Python数组一样:第一个元素的索引是0,最后一个元素索引是-1;可以指定范围以包含第一个元素和最后一个之前的元素。
        • X = torch.arange(12, dtype=torch.float32).reshape((3,4))
          X[-1], X[1:3]
          
        • 除读取外,我们还可以通过指定索引来将元素写入矩阵。
        • X[1, 2] = 9
          # tensor([[ 0.,  1.,  2.,  3.],
                    [ 4.,  5.,  9.,  7.],
                    [ 8.,  9., 10., 11.]])
          
        • 如果我们想为多个元素赋值相同的值,我们只需要索引所有元素,然后为它们赋值。 例如,[0:2, :]访问第1行和第2行,其中“:”代表沿轴1(列)的所有元素。 虽然我们讨论的是矩阵的索引,但这也适用于向量和超过2个维度的张量。
        • X[0:2, :] = 12
          # tensor([[12., 12., 12., 12.],
                	  [12., 12., 12., 12.],
                 	  [ 8.,  9., 10., 11.]])
          
      • 2.1.5. 节省内存

        • 运行一些操作可能会导致为新结果分配内存。 例如,如果我们用Y = X + Y,我们将取消引用Y指向的张量,而是指向新分配的内存处的张量。
        • 这可能是不可取的,原因有两个:
          • 首先,我们不想总是不必要地分配内存。在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。通常情况下,我们希望原地执行这些更新; logseq.order-list-type:: number
          • 如果我们不原地更新,其他引用仍然会指向旧的内存位置,这样我们的某些代码可能会无意中引用旧的参数。 logseq.order-list-type:: number
        • 幸运的是,执行原地操作非常简单。 我们可以使用切片表示法将操作的结果分配给先前分配的数组,例如X[:] = X + Y。
      • 2.1.6. 转换为其他 Python 对象

        • 将深度学习框架定义的张量转换为NumPy张量(ndarray)很容易,反之也同样容易。torch张量和numpy数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。
        • A = X.numpy()
          B = torch.tensor(A)
          type(A), type(B)
          
        • 要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数。
        • a = torch.tensor([3.5])
          a, a.item(), float(a), int(a)
          
      • 2.1.7. 小结

        • 深度学习存储和操作数据的主要接口是张量(n维数组)。它提供了各种功能,包括基本数学运算、广播、索引、切片、内存节省和转换其他 Python 对象。
      • 2.1.8. 练习

    • 2.2. 数据预处理

      collapsed:: true
      • 2.2.1. 读取数据集

        • 举一个例子,我们首先创建一个人工数据集,并存储在CSV文件 ../data/house_tiny.csv中。下面我们将数据集按行写入CSV文件中。
        • import os
          os.makedirs(os.path.join('..', 'data'), exist_ok=True)
          data_file = os.path.join('..', 'data', 'house_tiny.csv')
          with open(data_file, 'w') as f:
            f.write('NumRooms,Alley,Price\n')  # 列名
            f.write('NA,Pave,127500**\n**')  # 每行表示一个数据样本
            f.write('2,NA,106000**\n**')
            f.write('4,NA,178100**\n**')
            f.write('NA,NA,140000**\n**')
          
        • 要从创建的CSV文件中加载原始数据集,我们导入pandas包并调用read_csv函数。该数据集有四行三列。其中每行描述了房间数量(“NumRooms”)、巷子类型(“Alley”)和房屋价格(“Price”)。
        • import pandas as pd
          data = pd.read_csv(data_file)
          print(data)
          
      • 2.2.2. 处理缺失值

        • 注意,“NaN”项代表缺失值。 为了处理缺失的数据,典型的方法包括插值法删除法, 其中插值法用一个替代值弥补缺失值,而删除法则直接忽略缺失值。
      • 2.2.3. 转换为张量格式

      • 2.2.4. 小结

        • pandas软件包是Python中常用的数据分析工具中,pandas可以与张量兼容。 logseq.order-list-type:: number
        • 用pandas处理缺失的数据时,我们可根据情况选择用插值法和删除法。 logseq.order-list-type:: number
      • 2.2.5. 练习

    • 2.3. 线性代数

      collapsed:: true
      • 2.3.1. 标量

        • 本书采用了数学表示法,其中标量变量由普通小写字母表示(例如,x、y和z)。本书用R表示所有(连续)实数标量的空间,之后将严格定义空间(space)是什么,但现在只要记住表达式x ∈ R是表示x是一个实值标量的正式形式。符号∈称为“属于”,它表示“是集合中的成员”。例如x, y ∈ {0, 1}可以用来表明x和y是值只能为0或1的数字。
      • 2.3.2. 向量

        • 向量可以被视为标量值组成的列表。这些标量值被称为向量的元素(element)或分量(component)。当向量表示数据集中的样本时,它们的值具有一定的现实意义。在数学表示法中,向量通常记为粗体、小写的符号(例如,xyz))。
        • 我们可以使用下标来引用向量的任一元素,例如可以通过xi来引用第i个元素。注意,元素xi是一个标量,所以我们在引用它时不会加粗。**大量文献认为列向量是向量的默认方向,在本书中也是如此。**在数学中,向量x可以写为 image.png 其中x1, … , xn是向量的元素。
        • 2.3.2.1.长度、维度和形状

          • ((64a4cae6-de88-4086-a9d1-ead95561e9cf))
          • 当用张量表示一个向量(只有一个轴)时,我们也可以通过.shape属性访问向量的⻓度。形状(shape)是一个元素组,列出了张量沿每个轴的⻓度(维数)。对于只有一个轴的张量,形状只有一个元素。
          • 请注意,维度(dimension)这个词在不同上下文时往往会有不同的含义,这经常会使人感到困惑。为了清楚起⻅,我们在此明确一下:向量或轴的维度被用来表示向量或轴的⻓度,即向量或轴的元素数量。然而,张量的维度用来表示张量具有的轴数。在这个意义上,张量的某个轴的维数就是这个轴的⻓度。
      • 2.3.3. 矩阵

        • 矩阵,我们通常用粗体、大写字母来表示(例如,XYZ),在代码中表示为具有两个轴的张量
        • 数学表示法使用A ∈ R 来表示矩阵A,其由m行和n列的实值标量组成。我们可以将任意矩阵A ∈ R 视为一个表格,其中每个元素属于第i行第j列: image.png 特别地,当矩阵具有相同的数量的行和列时,其形状将变为正方形,此时称其为方阵。
        • 当我们交换矩阵的行和列时,结果称为矩阵的转置。通常用来表示矩阵的转置,如果,则对于任意i和j,都有bij = aji。
        • # 当调用函数来实例化张量时,我们可以通过指定两个分量m和n来创建一个形状为m × n的矩阵。
          A = torch.arange(20).reshape(5, 4)
          # 我们可以通过行索引(i)和列索引(j)来访问矩阵中的标量元素aij ,例如[A]ij 。
          # 在代码中访问矩阵的转置
          A.T
          
        • ((64a4ce23-ce2d-4c8d-b8f2-07fe114ca49e))
      • 2.3.4. 张量

        • 张量(本小节中的“张量”指代数对象)是描述具有任意数量轴的n维数组的通用方法。例如,向量是一阶张量,矩阵是二阶张量。张量用特殊字体的大写字母表示(例如,X、Y和Z),它们的索引机制与矩阵类似。
        • X = torch.arange(24).reshape(2, 3, 4)
          # 上述代码创建了一个大小为2x3x4的张量X。根据代码中的reshape操作,我们可以知道该张量有三个维度。
          第一个维度的大小是2,表示张量X中有2个元素(2个子数组或2个“面”)。每个子数组是一个大小为3x4的矩阵。
          第二个维度的大小是3,表示每个子数组中有3行。
          第三个维度的大小是4,表示每行有4个元素。
          
      • 2.3.5. 张量算法的基本性质

        • 标量、向量、矩阵和任意数量轴的张量(本小节中的“张量”指代数对象)有一些实用的属性。 给定具有相同形状的任意两个张量,任何按元素二元运算的结果都将是相同形状的张量。 例如,将两个相同形状的矩阵相加,会在这两个矩阵上执行元素加法。
        • 具体而言,两个矩阵的按元素乘法称为Hadamard积(数学符号⊙)。 对于矩阵AB的Hadamard积为: image.png
          A * B
          
        • 将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘。
          a = 2
          X = torch.arange(24).reshape(2, 3, 4)
          a + X, (a * X).shape
          
      • 2.3.6. 降维

        • 我们可以对任意张量进行的一个有用的操作是计算其元素的和。 数学表示法使用∑符号表示求和。 为了表示长度为d的向量中元素的总和,可以记为。 在代码中可以调用计算求和的函数:
          x = torch.arange(4, dtype=torch.float32)
          x.sum()
          
        • 我们可以表示任意形状张量的元素和。例如,矩阵A中元素的和可以记为
          A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
          A.shape, A.sum()
          
        • 默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。 我们还可以指定张量沿哪一个轴来通过求和降低维度。
        • 以矩阵为例,为了通过求和所有行的元素来降维(轴0),可以在调用函数时指定axis=0。 由于输入矩阵沿0轴降维以生成输出向量,因此输入轴0的维数在输出形状中消失。
          A_sum_axis0 = A.sum(axis=0)
          A_sum_axis0, A_sum_axis0.shape
          
        • 同样的,指定axis=1将通过汇总所有列的元素降维(轴1)。因此,输入轴1的维数在输出形状中消失。
        • 沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和。
        • 一个与求和相关的量是平均值(mean或average)。 我们通过将总和除以元素总数来计算平均值。 在代码中,我们可以调用函数来计算任意形状张量的平均值。
          A.mean(), A.sum() / A.numel()
          
        • 同样,计算平均值的函数也可以沿指定轴降低张量的维度。
          A.mean(axis=0), A.sum(axis=0) / A.shape[0]
          
        • 非降维求和

          • 但是,有时在调用函数来计算总和或均值时保持轴数不变会很有用。
          • sum_A = A.sum(axis=1, keepdims=**True**)
            sum_A
            
          • 由于sum_A在对每行进行求和后仍保持两个轴,我们可以通过广播将A除以sum_A。
          • id:: 64a4dbf1-3353-47d5-907f-015f9693b52d
            A / sum_A
            
          • 如果我们想沿某个轴计算A元素的累积总和,比如axis=0(按行计算),可以调用cumsum函数。此函数不会沿任何轴降低输入张量的维度。
          • A.cumsum(axis=0)
            
      • 2.3.7. 点积(Dot Product)

        • 我们已经学习了按元素操作、求和及平均值。另一个最基本的操作之一是点积。给定两个向量x, y ∈ R,它们的点积(dot product)是相同位置的按元素乘积的和:
        • x = torch.arange(4, dtype=torch.float32)
          y = torch.ones(4, dtype = torch.float32)
          torch.dot(x, y)
          # 我们也可以通过执行按元素乘法,然后进行求和来表示两个向量的点积
          torch.sum(x * y)
          
        • 点积在很多场合都很有用。例如,给定一组由向量x ∈ R表示的值,和一组由w ∈ R表示的权重。x中的值根据权重w的加权和,可以表示为点积。当权重为非负数且和为1时,点积表示加权平均。将两个向量规范化得到单位⻓度后,点积表示它们夹⻆的余弦等等。
      • 2.3.8. 矩阵-向量积

        • 现在我们知道如何计算点积,可以开始理解矩阵-向量积。回顾定义的矩阵A和向量x。让我们将矩阵A用它的行向量表示: image.png
        • 其中每个都是行向量,表示矩阵的第i行。矩阵-向量积Ax是一个⻓度为m的列向量,其第i个元素是点积image.png
        • 在代码中使用张量表示矩阵-向量积,我们使用mv函数。当我们为矩阵A和向量x调用torch.mv(A, x)时,会执行矩阵-向量积。注意,A的列维数(沿轴1的⻓度)必须与x的维数(其⻓度)相同。
          A.shape, x.shape, torch.mv(A, x)
          
      • 2.3.9. 矩阵-矩阵乘法

        • 在掌握点积和矩阵-向量积的知识后,那么矩阵-矩阵乘法应该很简单。我们可以将矩阵-矩阵乘法AB看作简单地执行m次矩阵-向量积,并将结果拼接在一起,形成一个n × m矩阵。
        • 在下面的代码中,我们在A和B上执行矩阵乘法。这里的A是一个5行4列的矩阵,B是一个4行3列的矩阵。两者相乘后,我们得到了一个5行3列的矩阵。
          B = torch.ones(4, 3)
          torch.mm(A, B)
          
        • 矩阵-矩阵乘法可以简单地称为矩阵乘法,不应与”Hadamard积”混淆。
      • 2.3.10. 范数

        • 范数是一种用于度量向量大小的数学概念。简单来说,范数可以看作是对向量进行求长度或者求距离的操作。
        • 在线性代数中,向量范数是将向量映射到标量的函数f 。给定任意向量x,向量范数要满足一些性质:1. 如果我们按常数因子α缩放向量的所有元素, 其范数也会按相同常数因子的绝对值缩放;2. 三角不等式,即;3. 范数必须是非负的。
        • 范数有多种不同的定义和计算方法,常见的有欧几里得范数(L2范数)、曼哈顿范数(L1范数)和无穷范数(L∞范数)等。以欧几里得范数为例,它是向量元素平方和的平方根。 image.png
          # L2范数代码表示如下
          u = torch.tensor([3.0, -4.0])
          torch.norm(u)
          
        • 类似于向量的L2范数,矩阵X的Frobenius范数是矩阵元素平方和的平方根: image.png
          torch.norm(torch.ones((4, 9)))
          
      • 2.3.11. 关于线性代数的更多信息

        • 如果渴望了解有关线性代数的更多信息,可以参考线性代数运算的在线附录或其他优秀资源 (Kolter, 2008, Petersen et al., 2008, Strang, 1993)。
      • 2.3.12. 小结

        • 标量、向量、矩阵和张量是线性代数中的基本数学对象。 logseq.order-list-type:: number
        • 向量泛化自标量,矩阵泛化自向量。 logseq.order-list-type:: number
        • 标量、向量、矩阵和张量分别具有零、一、二和任意数量的轴。 logseq.order-list-type:: number
        • 一个张量可以通过sum和mean沿指定的轴降低维度。 logseq.order-list-type:: number
        • 两个矩阵的按元素乘法被称为他们的Hadamard积。它与矩阵乘法不同。 logseq.order-list-type:: number
        • 在深度学习中,我们经常使用范数,如L1范数、L2范数和Frobenius范数。 logseq.order-list-type:: number
        • 我们可以对标量、向量、矩阵和张量执行各种操作。 logseq.order-list-type:: number
      • 2.3.13. 练习

    • 2.4. 微积分

      collapsed:: true
      • 2.4.1. 导数和微分

      • 2.4.2. 偏导数

      • 2.4.3. 梯度

        • ((64a52720-bc2d-4e17-9871-9969a4988d1e))
        • ((64a52736-b37b-4b15-bde5-543aeb1156dd))
      • 2.4.4. 链式法则

        • 然而,上面方法可能很难找到梯度。 这是因为在深度学习中,多元函数通常是复合的,难以应用上述任何规则来微分这些函数。幸运的是,链式法则可以被用来微分复合函数。
        • 让我们先考虑单变量函数。假设函数y=f(u)和u=g(x)都是可微的,根据链式法则:
        • 现在考虑一个更一般的场景,即函数具有任意数量的变量的情况。假设可微分函数y有变量u1, u2, … , um,其中每个可微分函数ui都有变量x1, x2, … , xn。注意,y是x1, x2 … , xn的函数。对于任意i = 1, 2, … , n,链式法则给出: image.png
      • 2.4.5. 小结

        • 微分和积分是微积分的两个分支,前者可以应用于深度学习中的优化问题。 logseq.order-list-type:: number
        • 导数可以被解释为函数相对于其变量的瞬时变化率,它也是函数曲线的切线的斜率。 logseq.order-list-type:: number
        • 梯度是一个向量,其分量是多变量函数相对于其所有变量的偏导数。 logseq.order-list-type:: number
        • 链式法则可以用来微分复合函数。 logseq.order-list-type:: number
      • 2.4.6. 练习

    • 2.5. 自动微分

      collapsed:: true
      • 求导是几乎所有深度学习优化算法的关键步骤。虽然求导的计算很简单,但对于复杂的模型,手工进行更新是一件很痛苦的事情。深度学习框架通过自动计算导数,即自动微分来加快求导。实际中,根据设计好的模型,系统会构建一个计算图,来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。这里,反向传播意味着跟踪整个计算图,填充关于每个参数的偏导数。
      • 2.5.1. 一个简单的例子

        • import torch
          
          x = torch.arange(4.0) #创建变量x并为其分配一个初始值。
          x.requires_grad_(True) # 在我们计算y关于x的梯度之前,需要一个地方来存储梯度。
          y = 2 * torch.dot(x, x) # 计算y
          y.backward() # 通过调用反向传播函数来自动计算y关于x每个分量的梯度。
          print(x.grad) # 打印梯度
          
        • 按照我的理解,调用 y.backward() 时,PyTorch会自动计算 y 相对于每个相关参数的导数(或偏导数)并将结果存储在各个参数的 grad 属性中。
      • 2.5.2. 非标量变量的反向传播

        • 当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
      • 2.5.3. 分离计算

        • 有时,我们希望将某些计算移动到记录的计算图之外。例如,假设y是作为x的函数计算的,而z则是作为y和x的函数计算的。想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数,并且只考虑到x在y被计算后发挥的作用。
        • 这里可以分离y来返回一个新变量u,该变量与y具有相同的值,但丢弃计算图中如何计算y的任何信息。换句话说,梯度不会向后流经u到x。因此,下面的反向传播函数计算z=ux关于x的偏导数,同时将u作为常数处理,而不是z=xx*x关于x的偏导数。
        • x.grad.zero_()
          y = x * x
          u = y.detach()
          z = u * x
          - z.sum().backward()
          x.grad == u
          
        • 由于记录了y的计算结果,我们可以随后在y上调用反向传播, 得到y=x*x关于的x的导数,即2x。
        • x.grad.zero_()
          y.sum().backward()
          x.grad == 2 * x
          
      • 2.5.4. Python控制流的梯度计算

        • 使用自动微分的一个好处是: 即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。
        • import torch
          
          def f(a):
              b = a * 2
              while b.norm() < 1000:
                  b = b * 2
              if b.sum() > 0:
                  c = b
              else:
                  c = 100 * b
              return c
          
          a = torch.randn(size=(), requires_grad=True)
          d = f(a)
          d.backward()
          
          print(a.grad == d / a)
          
      • 2.5.5. 小结

        • 深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。
      • 2.5.6. 练习

    • 2.6. 概率

      collapsed:: true
      • 简单地说,机器学习就是做出预测。
      • 概率是一种灵活的语言,用于说明我们的确定程度,并且它可以有效地应用于广泛的领域中。
      • 2.6.1. 基本概率论

        • 假设我们掷骰子,想知道看到1的几率有多大,而不是看到另一个数字。如果骰子是公平的,那么所有六个结果{1,…,6}都有相同的可能发生, 因此我们可以说1发生的概率为1/6。
        • 然而现实生活中,对于我们从工厂收到的真实骰子,我们需要检查它是否有瑕疵。检查骰子的唯一方法是多次投掷并记录结果。对于每个值,一种自然的方法是将它出现的次数除以投掷的总次数, 即此事件概率的估计值大数定律告诉我们: 随着投掷次数的增加,这个估计值会越来越接近真实的潜在概率。 让我们用代码试一试!
          import torch
          from torch.distributions import multinomial
          from d2l import torch as d2l
          
          fair_probs = torch.ones([6]) / 6
          # 将结果存储为32位浮点数以进行除法
          counts = multinomial.Multinomial(1000, fair_probs).sample()
          a = counts / 1000  # 相对频率作为估计值
          print(a)
          
        • 概率论公理

          • 在处理骰子掷出时,我们将集合S={1,2,3,4,5,6} 称为样本空间结果空间, 其中每个元素都是结果事件是一组给定样本空间的随机结果。例如,“看到5”({5})和“看到奇数”({1,3,5})都是掷出骰子的有效事件。注意,如果一个随机实验的结果在A中,则事件A已经发生。也就是说,如果投掷出3点,因为3∈{1,3,5},我们可以说,“看到奇数”的事件发生了。
          • 概率可以被认为是将集合映射到真实值的函数。在给定的样本空间S中,事件A的概率, 表示为P(A),满足以下属性:
            • 对于整个事件A,其概率从不会是负数;
            • 整个样本空间的概率为1;
            • 对于互斥事件的任意一个可数序列A1,A2,…,序列中任意一个事件发生的概率等于它们各自发生概率之和。
        • 随机变量

          • 在我们掷骰子的随机实验中,我们引入了随机变量的概念。随机变量几乎可以是任何数量,并且它可以在随机实验的一组可能性中取一个值。考虑一个随机变量X,其值在掷骰子的样本空间S = {1, 2, 3, 4, 5, 6}中。我们可以将事件“看到一个5”表示为{X = 5}或X = 5,其概率表示为P ({X =5})或P (X = 5)。通过P (X = a),我们区分了随机变量X和X可以采取的值(例如a)。然而,这可能会导致 繁琐的表示。为了简化符号,一方面,我们可以将P (X)表示为随机变量X上的分布(distribution):分布告诉我们X获得某一值的概率。另一方面,我们可以简单用P (a)表示随机变量取值a的概率。由于概率论中的事件是来自样本空间的一组结果,因此我们可以为随机变量指定值的可取范围。例如,P (1 ≤ X ≤ 3)表示事件{1 ≤ X ≤ 3},即{X = 1, 2, or, 3}的概率。等价地,P (1 ≤ X ≤ 3)表示随机变量X从{1, 2, 3}中取值的概率。
      • 2.6.2. 处理多个随机变量

        • 很多时候,我们会考虑多个随机变量。比如,我们可能需要对疾病和症状之间的关系进行建模。给定一个疾病和一个症状,比如“流感”和“咳嗽”,以某个概率存在或不存在于某个患者身上。我们需要估计这些概率以及概率之间的关系,以便我们可以运用我们的推断来实现更好的医疗服务。
          • 联合概率

            • 第一个被称为联合概率P (A = a, B = b)。给定任意值a和b,联合概率可以回答:A = a和B = b同时满足的概率是多少?请注意,对于任何a和b的取值,P (A = a, B = b) ≤ P (A = a)。这点是确定的,因为要同时发生A = a和B = b,A = a就必须发生,B = b也必须发生(反之亦然)。因此,A = a和B = b同时发生的可能性不大于A = a或是B = b单独发生的可能性。
          • 条件概率

            • ((64a65324-b20e-409c-a190-ff9c17285601))
          • 贝叶斯定理

            • ((64a65421-d323-4717-bbed-22bfb10589a6))
          • 边际化

            • 为了能进行事件概率求和,我们需要求和法则,即B的概率相当于计算A的所有可能选择,并将所有选择的联合概率聚合在一起: 这也称为边际化。边际化结果的概率或分布称为边际概率或边 际分布。
          • 独立性

            • 另一个有用属性是依赖(dependence)与独立。 如果两个随机变量A和B是独立的,意味着事件A的发生跟B事件的发生无关。在这种情况下,统计学家通常将这一点表述为A⊥B。根据贝叶斯定理,马上就能同样得到P(A∣B)=P(A)。在所有其他情况下,我们称A和B依赖。比如,两次连续抛出一个骰子的事件是相互独立的。
      • 2.6.3. 期望和方差

        • 为了概括概率分布的关键特征,我们需要一些测量方法。一个随机变量X的期望表示为
        • 当函数f (x)的输入是从分布P 中抽取的随机变量时,f (x)的期望值为
        • 在许多情况下,我们希望衡量随机变量X与其期望值的偏置。这可以通过方差来量化
        • 方差的平方根被称为标准差。随机变量函数的方差衡量的是:当从该随机变量分布中采样不同值x时,函数值偏离该函数的期望的程度:
      • 2.6.4. 小结

        • 我们可以从概率分布中采样。 logseq.order-list-type:: number :LOGBOOK: CLOCK: [2023-07-06 Thu 14:28:44] :END:
        • 我们可以使用联合分布、条件分布、Bayes定理、边缘化和独立性假设来分析多个随机变量。 logseq.order-list-type:: number
        • 期望和方差为概率分布的关键特征的概括提供了实用的度量形式。 logseq.order-list-type:: number
      • 2.6.5. 练习

    • 2.7. 查阅文档

      collapsed:: true
      • 2.7.1. 查找模块中的所有函数和类

        • 为了知道模块中可以调用哪些函数和类,可以调用dir函数。 例如,我们可以查询随机数生成模块中的所有属性:
          import torch
          print(dir(torch.distributions))
          
        • 通常可以忽略以“__”(双下划线)开始和结束的函数,它们是Python中的特殊对象,或以单个“_”(单下划线)开始的函数,它们通常是内部函数。根据剩余的函数名或属性名,我们可能会猜测这个模块提供了各种 生成随机数的方法,包括从均匀分布(uniform)、正态分布(normal)和多项分布(multinomial)中采样。
      • 2.7.2. 查找特定函数和类的用法

        • 有关如何使用给定函数或类的更具体说明,可以调用help函数。例如,我们来查看张量ones函数的用法。
          help(torch.ones)
          
          从文档中,我们可以看到ones函数创建一个具有指定形状的新张量,并将所有元素值设置为1。
      • 2.7.3. 小结

        • 官方文档提供了本书之外的大量描述和示例。 logseq.order-list-type:: number
        • 可以通过调用dir和help函数或在Jupyter记事本中使用?和??查看API的用法文档。 logseq.order-list-type:: number
      • 2.7.4. 练习

  • 线性神经网络

    • 在介绍深度神经网络之前,我们需要了解神经网络训练的基础知识。 本章我们将介绍神经网络的整个训练过程, 包括:定义简单的神经网络架构、数据处理、指定损失函数和如何训练模型。 为了更容易学习,我们将从经典算法——线性神经网络开始,介绍神经网络的基础知识。 经典统计学习技术中的线性回归和softmax回归可以视为线性神经网络, 这些知识将为本书其他部分中更复杂的技术奠定基础。
    • 3.1. 线性回归

      collapsed:: true
      • 回归是能为一个或多个自变量与因变量之间关系建模的一类方法。在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。在机器学习领域中的大多数任务通常都与预测有关。当我们想预测一个数值时,就会涉及到回归问题。常⻅的例子包括:预测价格(房屋、股票等)、预测需求(零售销量等)。但不是所有的预测都是回归问题。
      • 3.1.1. 线性回归的基本元素

        • 为了解释线性回归,我们举一个例子:我们希望根据房屋的面积和房龄来估算房屋价格。为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。这个数据集包括了房屋的销售价格、面积和房龄。在机器学习的术语中,该数据集称为训练数据集或训练集。每行数据(比如一次房屋交易相对应的数据)称为样本,也可以称为数据点或数据样本。我们把试图预测的目标(比如预测房屋价格)称为标签或目标。预测所依据的自变量称为特征或协变量。
        • 通常,我们使用n来表示数据集中的样本数。对索引为i的样本,其输入表示为,其对应的标签是
        • 线性模型

          • 线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和,如下面的式子: 上中的称为权重,权重决定了每个特征对我们预测值的影响。b称为偏置、偏移量或截距。偏置是指当所有特征都取值为0时,预测值应该为多少。即使现实中不会有任何房子的面积是0或房龄正好是0年,我们仍然需要偏置项。如果没有偏置项,我们模型的表达能力将受到限制。
          • 严格来说,该式是输入特征的一个 仿射变换。仿射变换的特点是通过加权和对特征进行线性变换,并通过偏置项来进行平移。
          • 而在机器学习领域,我们通常使用的是高维数据集,建模时采用线性代数表示法会比较方便。当我们的输入包含d个特征时,我们将预测结果(通常使用“尖⻆”符号表示y的估计值)表示为:
          • 将所有特征放到向量x ∈ R中,并将所有权重放到向量w ∈ R中,我们可以用点积形式来简洁地表达模型:
          • 向量x对应于单个数据样本的特征。用符号表示的矩阵X ∈ R 可以很方便地引用我们整个数据集的n个样本。其中,X的每一行是一个样本,每一列是一种特征。对于特征集合X,预测值 ∈ R 可以通过矩阵-向量乘法表示为:
          • 这个过程中的求和将使用广播机制。给定训练数据特征X和对应的已知标签y线性回归的目标是找到一组权重向量w和偏置b:当给定从X的同分布中取样的新样本特征时,这组权重向量和偏置能够使得新样本预测标签的误差尽可能小。
          • 无论我们使用什么手段来观察特征X和标签y,都可能会出现少量的观测误差。因此,即使确信特征与标签的潜在关系是线性的,我们也会加入一个噪声项来考虑观测误差带来的影响。
          • 在开始寻找最好的模型参数w和b之前,我们还需要两个东西:(1)一种模型质量的度量方式;(2)一种能够更新模型以提高模型预测质量的方法。
        • 损失函数

          • 在我们开始考虑如何用模型拟合数据之前,我们需要确定一个拟合程度的度量。损失函数能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。
          • 回归问题中最常用的损失函数是平方误差函数。当样本i的预测值为,其相应的真实标签为时,平方误差可以定义为以下公式: 常数1/2不会带来本质的差别,但这样在形式上稍微简单一些(因为当我们对损失函数求导后常数系数为1)。由于训练数据集并不受我们控制,所以经验误差只是关于模型参数的函数。
          • 由于平方误差函数中的二次方项,估计值和观测值之间较大的差异将导致更大的损失。为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。
          • 在训练模型时,我们希望寻找一组参数(w∗, b∗),这组参数能最小化在所有训练样本上的总损失。如下式:
        • 解析解

          • 线性回归的解可以用一个公式简单地表达出来,这类解叫作解析解。
          • 像线性回归这样的简单问题存在解析解,但并不是所有的问题都存在解析解。
        • 随机梯度下降

          • 即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。在许多任务上,那些难以优化的模型效果要更好。因此,弄清楚如何训练这些难以优化的模型是非常重要的。
          • 本书中我们用到一种名为梯度下降的方法,这种方法几乎可以优化所有深度学习模型。它通过不断地在损失函数递减的方向上更新参数来降低误差。
          • ((64a67693-e171-4e55-b27c-09004cadbd85))
          • ((64a676ab-d9a0-41b4-a5c4-c82934214e28))
        • 用模型进行预测

          • 给定“已学习”的线性回归模型,现在我们可以通过房屋面积x1和房龄x2来估计一个(未包含在训练数据中的)新房屋价格。给定特征估计目标的过程通常称为预测或推断。
      • 3.1.2. 矢量化加速

        • 在训练我们的模型时,我们经常希望能够同时处理整个小批量的样本。为了实现这一点,需要我们对计算进行矢量化,从而利用线性代数库,而不是在Python中编写开销高昂的for循环。
        • 矢量化代码通常会带来数量级的加速。另外,我们将更多的数学运算放到库中,而无须自己编写那么多的计算,从而减少了出错的可能性。
      • 3.1.3. 正态分布与平方损失

        • 接下来,我们通过对噪声分布的假设来解读平方损失目标函数
        • 正态分布线性回归之间的关系很密切,具体看定义。
        • 下面我们定义一个Python函数来计算正态分布并进行可视化:
          import math
          import numpy as np
          from d2l import torch as d2l
          
          def normal(x, mu, sigma):
              p = 1 / math.sqrt(2 * math.pi * sigma**2)
              return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)
          
          # 再次使用numpy进行可视化
          x = np.arange(-7, 7, 0.01)
          
          # 均值和标准差对
          params = [(0, 1), (0, 2), (3, 1)]
          d2l.plot(x, [normal(x, mu, sigma) for mu, sigma in params], xlabel='x',
                   ylabel='p(x)', figsize=(4.5, 2.5),
                   legend=[f'mean {mu}, std {sigma}' for mu, sigma in params])
          
          d2l.plt.show()
          
        • Figure_1.png
        • 就像我们所看到的,改变均值会产生沿x轴的偏移,增加方差将会分散分布、降低其峰值。
        • ((64a68607-b889-4928-9738-04c4dec1c39e))
      • 3.1.4. 从线性回归到深度网络

        • 到目前为止,我们只谈论了线性模型。尽管神经网络涵盖了更多更为丰富的模型,我们依然可以用描述神经网络的方式来描述线性模型,从而把线性模型看作一个神经网络。首先,我们用“层”符号来重写这个模型。
        • 神经网络图

          • 在下图中,我们将线性回归模型描述为一个神经网络。需要注意的是,该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。
          • image.png
          • 在上图所示的神经网络中,输入为x1, … , xd,因此输入层中的输入数(或称为特征维度)为d。网络的输出为o1,因此输出层中的输出数是1。需要注意的是,输入值都是已经给定的,并且只有一个计算神经元。由于模型重点在发生计算的地方,所以通常我们在计算层数时不考虑输入层。也就是说,上图中神经网络的层数为1。我们可以将线性回归模型视为仅由单个人工神经元组成的神经网络,或称为单层神经网络
          • 对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连,我们将这种变换称为全连接层或称为稠密层。下一章将详细讨论由这些层组成的网络。
        • 生物学

          • image.png
          • 这是一张由树突(dendrites,输入终端)、细胞核(nucleus,CPU)组成的生物神经元图片。轴突(axon,输出线)和轴突端子(axon terminal,输出端子)通过突触(synapse)与其他神经元连接。
          • 树突中接收到来自其他神经元(或视网膜等环境传感器)的信息。该信息通过突触权重来加权,以确定输入的影响(即,通过相乘来激活或抑制)。来自多个源的加权输入以加权和的形式汇聚在细胞核中,然后将这些信息发送到轴突y中进一步处理,通常会通过σ(y)进行一些非线性处理。之后,它要么到达目的地(例如肌肉),要么通过树突进入另一个神经元。
          • 当然,许多这样的单元可以通过正确连接和正确的学习算法拼凑在一起,从而产生的行为会比单独一个神经元所产生的行为更有趣、更复杂,这种想法归功于我们对真实生物神经系统的研究。
      • 3.1.5. 小结

        • 机器学习模型中的关键要素是训练数据、损失函数、优化算法,还有模型本身。 logseq.order-list-type:: number
        • 矢量化使数学表达上更简洁,同时运行的更快。 logseq.order-list-type:: number
        • 最小化目标函数和执行极大似然估计等价。 logseq.order-list-type:: number
        • 线性回归模型也是一个简单的神经网络。 logseq.order-list-type:: number
      • 3.1.6. 练习

    • 3.2. 线性回归的从零开始实现

      collapsed:: true
      • 在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。 在这一节中,我们将从零开始实现整个方法, 包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。 虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道自己在做什么。 同时,了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。 在这一节中,我们将只使用张量和自动求导。 在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。
      • import random
        import torch
        from d2l import torch as d2l
        
      • 3.2.1. 生成数据集

        • def synthetic_data(w, b, num_examples):  #@save
            """生成y=Xw+b+噪声"""
            	X = torch.normal(0, 1, (num_examples, len(w)))
            	y = torch.matmul(X, w) + b
            	y += torch.normal(0, 0.01, y.shape)
            	return X, y.reshape((-1, 1))
          true_w = torch.tensor([2, -3.4])
          true_b = 4.2
          features, labels = synthetic_data(true_w, true_b, 1000)
          
        • 这段代码定义了一个函数 synthetic_data(),用于生成合成数据集。它接受三个参数,分别是权重向量 w、偏置 b 和样本数目sum_examples。函数的目标是生成一个具有一定噪声的合成数据集。
        • 在函数内部,首先使用 torch.normal() 函数生成一个均值为0、标准差为1、形状为 (num_examples, len(w)) 的矩阵 X。这里使用了 PyTorch 库的张量操作来生成服从正态分布的随机数。这个矩阵 X 代表了输入特征。
        • 接着,利用 torch.matmul() 函数对矩阵 X 和权重向量 w 进行矩阵乘法操作,得到一个形状为 (num_examples,) 的向量 y。这一步实现了线性关系的建模,其中 X 的每一行与权重向量 w 的对应元素相乘,并求和得到一个标量作为 y 中的一个元素。
        • 最后,通过使用 torch.normal() 函数为向量 y 添加服从均值为 0,标准差为 0.01 的正态分布噪声。这个步骤用于给生成的数据添加一些随机性,以模拟真实世界中的数据。
        • 函数返回生成的特征矩阵 X 和标签向量 labels。其中,特征矩阵 X 的形状是 (num_examples, len(w)),而标签向量 labels 的形状是 (num_examples, 1)。
        • 最后一行代码利用真实的权重向量 true_w 和偏置 true_b 调用了 synthetic_data() 函数来生成一个包含 1000 个样本的合成数据集。结果存储在 features 和 labels 这两个变量中,供后续使用。
        • d2l.set_figsize()
          d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1);
          d2l.plt.show()
          
        • 通过生成第二个特征features[:, 1]和labels的散点图, 可以直观观察到两者之间的线性关系。
        • image.png
      • 3.2.2. 读取数据集

        • 回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数,该函数能打乱数据集中的样本并以小批量方式获取数据。
        • 在下面的代码中,我们定义一个data_iter函数,该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签。
          def data_iter(batch_size, features, labels):
              num_examples = len(features)
              indices = list(range(num_examples))
          	# 这些样本是随机读取的,没有特定的顺序
            	random.shuffle(indices)
            	for i in range(0, num_examples, batch_size):
                	batch_indices = torch.tensor(
                    	indices[i: min(i + batch_size, num_examples)])
               	yield features[batch_indices], labels[batch_indices]
          
        • 通常,我们利用GPU并行运算的优势,处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算,且每个样本损失函数的梯度也可以被并行计算。 GPU可以在处理几百个样本时,所花费的时间不比处理一个样本时多太多。
        • 当我们运行迭代时,我们会连续地获得不同的小批量,直至遍历完整个数据集。 上面实现的迭代对教学来说很好,但它的执行效率很低,可能会在实际问题上陷入麻烦。 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多, 它可以处理存储在文件中的数据和数据流提供的数据。
      • 3.2.3. 初始化模型参数

        • 在我们开始用小批量随机梯度下降优化我们的模型参数之前, 我们需要先有一些参数。 在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。
          w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
          b = torch.zeros(1, requires_grad=True)
          
        • 在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。每次更新都需要计算损失函数关于模型参数的梯度。有了这个梯度,我们就可以向减小损失的方向更新每个参数。因为手动计算梯度很枯燥而且容易出错,所以没有人会手动计算梯度。我们使用 2.5节中引入的自动微分来计算梯度。
      • 3.2.4. 定义模型

        • 接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。回想一下,要计算线性模型的输出,我们只需计算输入特征X和模型权重w的矩阵-向量乘法后加上偏置b。
          def linreg(X, w, b):  #@save
              """线性回归模型"""*
              return torch.matmul(X, w) + b
          
      • 3.2.5. 定义损失函数

        • 因为需要计算损失函数的梯度,所以我们应该先定义损失函数。 这里我们使用3.1节中描述的平方损失函数。 在实现中,我们需要将真实值y的形状转换为和预测值y_hat的形状相同。
          def squared_loss(y_hat, y):  #@save
              """均方损失"""
              return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
          
      • 3.2.6. 定义优化算法

        • 这里我们介绍小批量随机梯度下降。在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。
        • 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每一步更新的大小由学习速率lr决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size)来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
        • def sgd(params, lr, batch_size):  #@save
              """小批量随机梯度下降"""
              with torch.no_grad():
                  for param in params:
                      param -= lr * param.grad / batch_size
                      param.grad.zero_()
          
      • 3.2.7. 训练

        • 现在我们已经准备好了模型训练所有需要的要素,可以实现主要的训练过程部分了。 理解这段代码至关重要,因为从事深度学习后, 相同的训练过程几乎一遍又一遍地出现。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。 最后,我们调用优化算法sgd来更新模型参数。
        • 概括一下,我们将执行以下循环:
          • 初始化参数
          • 重复以下训练,直到完成
            • 计算梯度
            • 更新参数
        • 在每个迭代周期(epoch)中,我们使用data_iter函数遍历整个数据集,并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。这里的迭代周期个数num_epochs和学习率lr都是超参数,分别设 为3和0.03。设置超参数很棘手,需要通过反复试验进行调整,我们现在忽略这些细节。
        • lr = 0.03
          num_epochs = 3
          net = linreg
          loss = squared_loss
          
          for epoch in range(num_epochs):
              for X, y in data_iter(batch_size, features, labels):
                  l = loss(net(X, w, b), y)  # X和y的小批量损失
                  # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
                  # 并以此计算关于[w,b]的梯度
                  l.sum().backward()
                  sgd([w, b], lr, batch_size)  # 使用参数的梯度更新参数
              with torch.no_grad():
                  train_l = loss(net(features, w, b), labels)
                  print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
          
        • 因为我们使用的是自己合成的数据集,所以我们知道真正的参数是什么。 因此,我们可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度。 事实上,真实参数和通过训练学到的参数确实非常接近。
        • print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
          print(f'b的估计误差: {true_b - b}')
          
        • 注意,我们不应该想当然地认为我们能够完美地求解参数。 在机器学习中,我们通常不太关心恢复真正的参数,而更关心如何高度准确预测参数。 幸运的是,即使是在复杂的优化问题上,随机梯度下降通常也能找到非常好的解。 其中一个原因是,在深度网络中存在许多参数组合能够实现高度精确的预测。
      • 3.2.8. 小结

        • 我们学习了深度网络是如何实现和优化的。在这一过程中只使用张量和自动微分,不需要定义层或复杂的优化器。 logseq.order-list-type:: number
        • 这一节只触及到了表面知识。在下面的部分中,我们将基于刚刚介绍的概念描述其他模型,并学习如何更简洁地实现其他模型 logseq.order-list-type:: number
      • 3.2.9. 练习

    • 3.3. 线性回归的简洁实现

      collapsed:: true
      • 本节将介绍如何通过使用深度学习框架来简洁地实现3.2节中的线性回归模型。
      • 3.3.1. 生成数据集

        • import numpy as np
          import torch
          from torch.utils import data
          from d2l import torch as d2l
          
          ture_w = torch.tensor([2,-3.4])
          ture_b = 4.2
          features, labels = d2l.synthetic_data(ture_w, ture_b, 1000)
          
      • 3.3.2. 读取数据集

        • 我们可以调用框架中现有的API来读取数据。 我们将features和labels作为API的参数传递,并通过数据迭代器指定batch_size。 此外,布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。
        • id:: 64a7d3b9-e334-40cb-9d30-37245c7b4a0c
          def load_array(data_arrays, batch_size, is_train=True):  #@save
              """构造一个PyTorch数据迭代器"""
              dataset = data.TensorDataset(*data_arrays)
              return data.DataLoader(dataset, batch_size, shuffle=is_train)
          
          batch_size = 10
          data_iter = load_array((features, labels), batch_size)
          
      • 3.3.3. 定义模型

        • 对于标准深度学习模型,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。 我们首先定义一个模型变量net,它是一个Sequential类的实例。 Sequential类将多个层串联在一起。 当给定输入数据时,Sequential实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。 在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。 但是由于以后几乎所有的模型都是多层的,在这里使用Sequential会让你熟悉“标准的流水线”。
        • # nn是神经网络的缩写
          from torch import nn
          
          #在PyTorch中,全连接层在Linear类中定义。 值得注意的是,我们将两个参数传递到nn.Linear中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。
          net = nn.Sequential(nn.Linear(2, 1))
          
      • 3.3.4. 初始化模型参数

        • 在使用net之前,我们需要初始化模型参数。如在线性回归模型中的权重和偏置。深度学习框架通常有预定义的方法来初始化参数。在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采 样,偏置参数将初始化为零。
        • 正如我们在构造nn.Linear时指定输入和输出尺寸一样,现在我们能直接访问参数以设定它们的初始值。我们通过net[0]选择网络中的第一个图层,然后使用weight.data和bias.data方法访问参数。我们还可以使用 替换方法normal_和fill_来重写参数值。
        • net[0].weight.data.normal_(0, 0.01)
          net[0].bias.data.fill_(0)
          
      • 3.3.5. 定义损失函数

        • 计算均方误差使用的是MSELoss类,也称为平方L2范数。 默认情况下,它返回所有样本损失的平均值。
        • loss = nn.MSELoss()
          
      • 3.3.6. 定义优化算法

        • 当我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr值,这里设置为0.03。
        • trainer = torch.optim.SGD(net.parameters(), lr=0.03)
          
      • 3.3.7. 训练

        • 当我们有了所有的基本组件,训练过程代码与我们从零开始实现时所做的非常相似。
        • num_epochs = 3
          for epoch in range(num_epochs):
              for X, y in data_iter:
                  l = loss(net(X) ,y)
                  trainer.zero_grad()
                  l.backward()
                  trainer.step()
              l = loss(net(features), labels)
              print(f'epoch {epoch + 1}, loss {l:f}')
          
        • 下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。 要访问参数,我们首先从net访问所需的层,然后读取该层的权重和偏置。 正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。
        • w = net[0].weight.data
          print('w的估计误差:', true_w - w.reshape(true_w.shape))
          b = net[0].bias.data
          print('b的估计误差:', true_b - b)
          
      • 3.3.8. 小结

        • 我们可以使用PyTorch的高级API更简洁地实现模型。 logseq.order-list-type:: number
        • 在PyTorch中,data模块提供了数据处理工具,nn模块定义了大量的神经网络层和常见损失函数。 logseq.order-list-type:: number
        • 我们可以通过_结尾的方法将参数替换,从而初始化参数。 logseq.order-list-type:: number
      • 3.3.9. 练习

    • 3.4. softmax回归

      collapsed:: true
      • 3.4.1. 分类问题

        • 我们从一个图像分类问题开始。 假设每次输入是一个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}对应于“狗”。
      • 3.4.2. 网络架构

        • 为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。。为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数。在我们的例子中,由于我们有4个特征和3个可能的输出类别,我们将需要12个标量来表示权重(带下标的w),3个标量来表示偏置(带下标的b)。下面我们为每个输入计算三个未规范化的预测:o1o2o3
        • \begin{split}\begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{12} + x_3 w_{13} + x_4 w_{14} + b_1,\ o_2 &= x_1 w_{21} + x_2 w_{22} + x_3 w_{23} + x_4 w_{24} + b_2,\ o_3 &= x_1 w_{31} + x_2 w_{32} + x_3 w_{33} + x_4 w_{34} + b_3. \end{aligned}\end{split}
        • 我们可以用神经网络图来描述这个计算过程。与线性回归一样,softmax回归也是一个单层神经网络。 由于计算每个输出o1、o2和o3取决于所有输入x1、x2、x3和x4, 所以softmax回归的输出层也是全连接层
        • image.png id:: 64b4d4ed-ce90-48f8-af04-a6803798e2c8
        • 为了更简洁地表达模型,我们仍然使用线性代数符号。 通过向量形式表达为, 这是一种更适合数学和编写代码的形式。
      • 3.4.3. 全连接层的参数开销

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

        • 现在我们将优化参数以最大化观测数据的概率。为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。我们希望模型的输出视为属于类的概率,然后选择具有最大输出值的类别作为我们的预测。例如,如果ˆy1、ˆy2和ˆy3分别为0.1、0.8和0.1,那么我们预测的类别是2,在我们的例子中代表“鸡”。
        • 要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。此外,我们需要一个训练的目标函数,来激励模型精准地估计概率。例如,在分类器输出0.5的所有样本中,我们希望这些样本是刚好有一半实际上属于预测的类别。这个属性叫做校准。 id:: 64b4dbd5-c1bf-4e6a-b77d-23a207253039
        • 社会科学家邓肯·卢斯于1959年在选择模型的理论基础上发明的softmax函数正是这样做的:softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持可导的性质。为了完成这一目标,我们首先对每个未规范化的预测求幂,这样可以确保输出非负。为了确保最终输出的概率值总和为1,我们再让每个求幂后的结果除以它们的总和。如下式:
        • 这里,对于所有的j总有0≤≤1。 因此,可以视为一个正确的概率分布。softmax运算不会改变未规范化的预测之间的大小次序,只会确定分配给每个类别的概率。 因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别。
        • 尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。因此,softmax回归是一个线性模型。
      • 3.4.5. 小批量样本的矢量化

        • 为了提高计算效率并且充分利用GPU,我们通常会对小批量样本的数据执行矢量计算。假设我们读取了一个批量的样本X,其中特征维度(输入数量)为d,批量大小为n。此外,假设我们在输出中有q个类别。那么小批量样本的特征为X ∈ R,权重为W ∈ R,偏置为b ∈ Rsoftmax回归的矢量计算表达式为: \begin{split}\begin{aligned} \mathbf{O} &= \mathbf{X} \mathbf{W} + \mathbf{b}, \\ \hat{\mathbf{Y}} & = \mathrm{softmax}(\mathbf{O}). \end{aligned}\end{split}
        • 相对于一次处理一个样本,小批量样本的矢量化加快了XW的矩阵-向量乘法。由于X中的每一行代表一个数据样本,那么softmax运算可以按行执行:对于O的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。在上式中,XW + b的求和会使用广播机制,小批量的未规范化预测O和输出概率都是形状为n × q的矩阵。
      • 3.4.6. 损失函数

        • 对数似然

          • ((64b4f6e2-8e12-459c-bebf-e52ddc6c583b))
          • (3.4.8)中的损失函数通常被称为交叉熵损失。由于y是一个⻓度为q的独热编码向量,所以除了一个项以外的所有项j都消失了。由于所有ˆyj 都是预测的概率,所以它们的对数永远不会大于0。因此,如果正确地预测实际标签,即如果实际标签P (y | x) = 1,则损失函数不能进一步最小化。注意,这往往是不可能的。例如,数据集中可能存在标签噪声(比如某些样本可能被误标),或输入特征没有足够的信息来完美地对每一个样本分类。
        • softmax及其导数

          • 由于softmax和相关的损失函数很常⻅,因此我们需要更好地理解它的计算方式。将 (3.4.3)代入损失 (3.4.8)中。利用softmax的定义,我们得到: l(\mathbf{y}, \hat{\mathbf{y}}) &= - \sum_{j=1}^q y_j \log \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} \\ &= \sum_{j=1}^q y_j \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j\\ &= \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j \end{aligned}\end{split}$$
          • 考虑相对于任何未规范化的预测oj 的导数,我们得到:
          • 换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。 从这个意义上讲,这与我们在回归中看到的非常相似, 其中梯度是观测值和估计值之间的差异。 这不是巧合,在任何指数族分布模型中, 对数似然的梯度正是由此得出的。 这使梯度计算在实践中变得容易很多。
        • 交叉熵损失

          • 现在让我们考虑整个结果分布的情况,即观察到的不仅仅是一个结果。对于标签y,我们可以使用与以前相同的表示形式。唯一的区别是,我们现在用一个概率向量表示,如(0.1, 0.2, 0.7),而不是仅包含二元项的向量(0, 0, 1)。我们使用 (3.4.8)来定义损失l,它是所有标签分布的预期损失值。此损失称为交叉熵损失,它是分类问题最常用的损失之一。
      • 3.4.7. 信息论基础

        • 信息论涉及编码、解码、发送以及尽可能简洁地处理信息或数据。
          • 信息论的核心思想是量化数据中的信息内容。在信息论中,该数值被称为分布P的熵。可以通过以下方程得到:
          • 信息论的基本定理之一指出,为了对从分布p中随机抽取的数据进行编码,我们至少需要H[P]“纳特”对其进行编码。“纳特”相当于比特,但是对数底为e而不是2。
        • 信息量

          • 压缩与预测有什么关系呢?想象一下,我们有一个要压缩的数据流。如果我们很容易预测下一个数据,那么这个数据就很容易压缩。但是,如果我们不能完全预测每一个事件,那么我们有时可能会感到”惊异”。克劳德·香农决定用信息量来量化这种惊异程度。在观察一个事件j时,并赋予它(主观)概率P (j)。当我们赋予一个事件较低的概率时,我们的惊异会更大,该事件的信息量也就更大。在 (3.4.11)中定义的熵,是当分配的概率真正匹配数据生成过程时的信息量的期望。
        • 重新审视交叉熵

          • 如果把熵H(P)想象为“知道真实概率的人所经历的惊异程度”,那么什么是交叉熵?交叉熵从PQ,记为H(P,Q)。我们可以把交叉熵想象为“主观概率为Q的观察者在看到根据概率P生成的数据时的预期惊异”。当P = Q时,交叉熵达到最低。在这种情况下,从PQ的交叉熵是H(P, P) = H(P )
          • 简而言之,我们可以从两方面来考虑交叉熵分类目标:(i)最大化观测数据的似然;(ii)最小化传达标签所需的惊异。
      • 3.4.8. 模型预测和评估

        • 在训练softmax回归模型后,给出任何样本特征,我们可以预测每个输出类别的概率。通常我们使用预测概率最高的类别作为输出类别。如果预测与实际类别(标签)一致,则预测是正确的。在接下来的实验中,我们将使用精度来评估模型的性能。精度等于正确预测数与预测总数之间的比率。
      • 3.4.9. 小结

        • softmax运算获取一个向量并将其映射为概率。 logseq.order-list-type:: number
        • softmax回归适用于分类问题,它使用了softmax运算中输出类别的概率分布。 logseq.order-list-type:: number
        • 交叉熵是一个衡量两个概率分布之间差异的很好的度量,它测量给定模型编码数据所需的比特数。 logseq.order-list-type:: number
      • 3.4.10. 练习

    • 3.5. 图像分类数据集

      collapsed:: true
      • 3.5.1. 读取数据集

        • 我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。
        • # 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式
          # 并除以255使得所有像素的数值均在0~1之间
          trans = transforms.ToTensor()
          mnist_train = torchvision.datasets.FashionMNIST(
            root="../data", train=True, transform=trans, download=True)
          mnist_test = torchvision.datasets.FashionMNIST(
            root="../data", train=False, transform=trans, download=True)
          
        • Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集中的6000张图像 和测试数据集中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练,只用于评估模型性能。
        • 每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 为了简洁起见,本书将高度(h)像素、宽度(w)像素图像的形状记为(h \times w)或((h),(w))。
          mnist_train[0][0].shape
          # torch.Size([1, 28, 28])
          
        • Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。 以下函数用于在数字标签索引及其文本名称之间进行转换。
          def get_fashion_mnist_labels(labels):  #@save
              """返回Fashion-MNIST数据集的文本标签"""
              text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                             'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
              return [text_labels[int(i)] for i in labels]
          
        • 我们现在可以创建一个函数来可视化这些样本。
        • def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):  #@save
              """绘制图像列表"""
              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:
                      # PIL图片
                      ax.imshow(img)
                  ax.axes.get_xaxis().set_visible(False)
                  ax.axes.get_yaxis().set_visible(False)
                  if titles:
                      ax.set_title(titles[i])
              return axes
          
        • 以下是训练数据集中前几个样本的图像及其相应的标签。
          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));
          
      • 3.5.2. 读取小批量

        • 为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器。回顾一下,在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size。通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏⻅地读取小批量。
        • batch_size = 256
          
          def get_dataloader_workers():  #@save
              """使用4个进程来读取数据"""
              return 4
          
          # data.DataLoader 是 PyTorch 中用于构建数据加载器的类,它可以方便地对数据集进行批次读取和多进程加速。
          train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
                                       num_workers=get_dataloader_workers())
          
      • 3.5.3. 整合所有组件

        • 现在我们定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。这个函数返回训练集和验证集的数据迭代器。此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状。
        • 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="../data", train=True, transform=trans, download=True)
              mnist_test = torchvision.datasets.FashionMNIST(
                  root="../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()))
          
        • 我们现在已经准备好使用Fashion-MNIST数据集,便于下面的章节调用来评估各种分类算法。
      • 3.5.4. 小结

        • Fashion-MNIST是一个服装分类数据集,由10个类别的图像组成。我们将在后续章节中使用此数据集来评估各种分类算法。 logseq.order-list-type:: number
        • 我们将高度(h)像素,宽度(w)像素图像的形状记为(h \times w)或((h),(w))。 logseq.order-list-type:: number
        • 数据迭代器是获得更高性能的关键组件。依靠实现良好的数据迭代器,利用高性能计算来避免减慢训练过程。 logseq.order-list-type:: number
      • 3.5.5. 练习

    • 3.6. softmax回归的从零开始实现

      collapsed:: true
      • 本节我们将使用刚刚在 3.5节中引入的Fashion-MNIST数据集, 并设置数据迭代器的批量大小为256。
      • import torch
        from IPython import display
        from d2l import torch as d2l
        
        batch_size = 256
        train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
        
      • 3.6.1. 初始化模型参数

        • 和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是28×28的图像。 本节将展平每个图像,把它们看作长度为784的向量。
        • softmax回归中,我们的输出与类别一样多。因为我们的数据集有10个类别,所以网络输出维度为10。因此,权重将构成一个784 × 10的矩阵,偏置将构成一个1 × 10的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重W,偏置初始化为0。
        • import torch
          from IPython import display
          from d2l import torch as d2l
          
          batch_size = 256
          train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
          
      • 3.6.2. 定义softmax操作

        • 先简要回顾一下sum运算符如何沿着张量中的特定维度工作。如 2.3.6节和 2.3.6.1节所述,给定一个矩阵X,我们可以对所有元素求和(默认情况下)。也可以只求同一个轴上的元素,即同一列(轴0)或同一行(轴1)。如果X是一个形状为(2, 3)的张量,我们对列进行求和, 则结果将是一个具有形状(3,)的向量。当调用sum运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。这将产生一个具有形状(1, 3)的二维张量。
        • 回想一下,实现softmax由三个步骤组成:
          • 对每个项求幂(使用exp);
          • 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
          • 将每一行除以其规范化常数,确保结果的和为1。
        • 分母或规范化常数,有时也称为配分函数(其对数称为对数-配分函数)。
          def softmax(X):
              X_exp = torch.exp(X)
              partition = X_exp.sum(1, keepdim=True)  # 对张量X_exp的第一个维度(行)进行求和,并保持结果的维度不变。
              return X_exp / partition  # 这里应用了广播机制
          
        • 正如上述代码,对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1。
      • 3.6.3. 定义模型

        • 定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。注意,将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。
        • def net(X):
              return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
          
      • 3.6.4. 定义损失函数

        • 接下来,我们实现 3.4节中引入的 交叉熵损失函数。
        • ((64c89e14-b51c-460e-a6f4-c11117946197))
        • 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]
          
        • 现在我们只需一行代码就可以实现交叉熵损失函数。
      • 3.6.5. 分类精度

        • 当预测与标签分类y一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。 虽然直接优化精度可能很困难(因为精度的计算不可导), 但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。
        • 为了计算精度,我们执行以下操作。 首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y元素进行比较。 由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。
        • def accuracy(y_hat, y):  #@save
              """计算预测正确的数量"""
              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) == y
              return float(cmp.type(y.dtype).sum())
          
        • 我们将继续使用之前定义的变量y_hat和y分别作为预测的概率分布和标签。 可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。 第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。 因此,这两个样本的分类精度率为0.5。
        • accuracy(y_hat, y) / len(y)
          
        • 同样,对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度。
        • def evaluate_accuracy(net, data_iter):  #@save
              """计算在指定数据集上模型的精度"""
              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]
          
        • 这里定义一个实用程序类Accumulator,用于对多个变量进行累加。 在上面的evaluate_accuracy函数中, 我们在Accumulator实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时,两者都将随着时间的推移而累加。
        • class Accumulator:  #@save
              """在n个变量上累加"""
              def __init__(self, n):
                  self.data = [0.0] * n
          
              def add(self, *args):
                  self.data = [a + float(b) for a, b in zip(self.data, args)]
          
              def reset(self):
                  self.data = [0.0] * len(self.data)
          
              def __getitem__(self, idx):
                  return self.data[idx]
          
      • 3.6.6. 训练

        • 在我们看过 3.2节中的线性回归实现,softmax回归的训练过程代码应该看起来非常眼熟。在这里,我们重构训练过程的实现以使其可重复使用。首先,我们定义一个函数来训练一个迭代周期。请注意,updater是更新模型参数的常用函数,它接受批量大小作为参数。
        • def train_epoch_ch3(net, train_iter, loss, updater):  #@save
              """训练模型一个迭代周期(定义见第3章)"""
              # 将模型设置为训练模式
              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):
                      # 使用PyTorch内置的优化器和损失函数
                      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]
          
        • 接下来我们实现一个训练函数,它会在train_iter访问到的训练数据集上训练一个模型net。 该训练函数将会运行多个迭代周期(由num_epochs指定)。在每个迭代周期结束时,利用test_iter访问到的测试数据集对模型进行评估。我们将利用Animator类来可视化训练进度。
        • def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
              """训练模型(定义见第3章)"""
              animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                                  legend=['train loss', 'train acc', 'test acc'])
              for epoch in range(num_epochs):
                  train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
                  test_acc = evaluate_accuracy(net, test_iter)
                  animator.add(epoch + 1, train_metrics + (test_acc,))
              train_loss, train_acc = train_metrics
              assert train_loss < 0.5, train_loss
              assert train_acc <= 1 and train_acc > 0.7, train_acc
              assert test_acc <= 1 and test_acc > 0.7, test_acc
          
        • 作为一个从零开始的实现,我们使用 3.2节中定义的小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。
        • lr = 0.1
          def updater(batch_size):
              return d2l.sgd([W, b], lr, batch_size)
          
        • 现在,我们训练模型10个迭代周期。 请注意,迭代周期(num_epochs)和学习率(lr)都是可调节的超参数。通过更改它们的值,我们可以提高模型的分类精度。
        • num_epochs = 10
          train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
          
      • 3.6.7. 预测

        • 现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。
        • def predict_ch3(net, test_iter, n=6):  #@save
              """预测标签(定义见第3章)"""
              for X, y in test_iter:
                  break
              trues = 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)
          
      • 3.6.8. 小结

        • 借助softmax回归,我们可以训练多分类的模型。 logseq.order-list-type:: number
        • 训练softmax回归循环模型与训练线性回归模型非常相似:先读取数据,再定义模型和损失函数,然后使用优化算法训练模型。大多数常⻅的深度学习模型都有类似的训练过程。 logseq.order-list-type:: number
      • 3.6.9. 练习

    • 3.7. softmax回归的简洁实现

      collapsed:: true
      • 通过深度学习框架的高级API能更方便地实现softmax回归模型。本节如在 3.6节中一样, 继续使用Fashion-MNIST数据集,并保持批量大小为256。
      • import torch
        from d2l import torch as d2l
        
        batch_size = 256
        train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
        
      • 3.7.1. 初始化模型参数

        • softmax回归的输出层是一个全连接层。因此,为了实现我们的模型,我们只需在Sequential中添加一个带有10个输出的全连接层。同样,在这里Sequential并不是必要的,但它是实现深度模型的基础。我们仍然以均值0和标准差0.01随机初始化权重。
        • # PyTorch不会隐式地调整输入的形状。因此,我们在线性层前定义了展平层(将输入的多维张量转换为一维张量,以便能够传递给线性层进行处理),来调整网络输入的形状
          net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
          
          def init_weights(m):
              if type(m) == nn.Linear:
                  nn.init.normal_(m.weight, std=0.01)
          
          net.apply(init_weights);
          
      • 3.7.2. 重新审视Softmax的实现

        • 在前面 3.6节的例子中, 我们计算了模型的输出,然后将此输出送入交叉熵损失。从数学上讲,这是一件完全合理的事情。然而,从计算角度来看,指数可能会造成数值稳定性问题。
        • softmax函数()中中的一些数值非常大, 那么可能大于数据类型容许的最大数字,即上溢。这将使分母或分子变为inf(无穷大),最后得到的是0、inf或nan(不是数字)的。在这些情况下,我们无法得到一个明确定义的交叉熵值。
        • 解决这个问题的一个技巧是:在继续softmax计算之前,先从所有中减去\hat y_j & = \frac{\exp(o_j - \max(o_k))\exp(\max(o_k))}{\sum_k \exp(o_k - \max(o_k))\exp(\max(o_k))} \\ & = \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}. \end{aligned}\end{split}$$
        • 在减法和规范化步骤之后,可能有些具有较大的负值。由于精度受限,将有接近零的值,即下溢。 反向传播几步后,我们可能会发现自己面对一屏幕可怕的nan结果。
        • 尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题,如下面的等式所示。 \log{(\hat y_j)} & = \log\left( \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}\right) \\ & = \log{(\exp(o_j - \max(o_k)))}-\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)} \\ & = o_j - \max(o_k) -\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)}. \end{aligned}\end{split}$$
        • 我们也希望保留传统的softmax函数,以备我们需要评估通过模型输出的概率。但是,我们没有将softmax概率传递到损失函数中,而是在交叉熵损失函数中传递未规范化的预测,并同时计算softmax及其对数。
        • loss = nn.CrossEntropyLoss(reduction='none')
          
      • 3.7.3. 优化算法

        • 在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。
        • trainer = torch.optim.SGD(net.parameters(), lr=0.1)
          
      • 3.7.4. 训练

        • 接下来调用 3.6节中 定义的训练函数来训练模型。
        • num_epochs = 10
          d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
          
      • 3.7.5. 小结

        • 使用深度学习框架的高级API,我们可以更简洁地实现softmax回归。 logseq.order-list-type:: number
        • 从计算的角度来看,实现softmax回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。 logseq.order-list-type:: number
      • 3.7.6. 练习

  • 4 多层感知机

    • 在本章中,我们将第一次介绍真正的**深度网络。 最简单的深度网络称为多层感知机多层感知机由多层神经元组成,每一层与它的上一层相连,从中接收输入;同时每一层也与它的下一层相连,影响当前层的神经元。当我们训练容量较大的模型时,我们面临着过拟合的风险。因此,本章将从基本的概念介绍开始讲起,包括过拟合欠拟合和模型选择。为了解决这些问题,本章将介绍权重衰减暂退法正则化**技术。我们还将讨论数值稳定性和参数初始化相关的问题,这些问题是成功训练深度网络的关键。在本章的最后,我们将把所介绍的内容应用到一个真实的案例:房价预测。关于模型计算性能、可伸缩性和效率相关的问题,我们将放在后面的章节中讨论。
    • 4.1. 多层感知机

      collapsed:: true
      • 4.1.1. 隐藏层

        • 回想一下如 图3.4.1中所示的softmax回归的模型架构。 该模型通过单个仿射变换将我们的输入直接映射到输出,然后进行softmax操作。 如果我们的标签通过仿射变换后确实与我们的输入数据相关,那么这种方法确实足够了。 但是,仿射变换中的线性是一个很强的假设。
        • 线性模型可能会出错

          • 线性意味着单调假设:任何特征的增大都会导致模型输出的增大(如果对应的权重为正),或者导致模型输出的减小(如果对应的权重为负)。有时这是有道理的。例如,如果我们试图预测一个人是否会偿还贷款。我们可以认为,在其他条件不变的情况下,收入较高的申请人比收入较低的申请人更有可能偿还贷款。但是,虽然收入与还款概率存在单调性,但它们不是线性相关的。收入从0增加到5万,可能比从100万增加到105万带来更大的还款可能性。处理这一问题的一种方法是对我们的数据进行预处理,使线性变得更合理,如使用收入的对数作为我们的特征。
          • 然而我们可以很容易找出违反单调性的例子,如对猫和狗的图像进行分类。这里的线性很荒谬,而且我们难以通过简单的预处理来解决这个问题。这是因为任何像素的重要性都以复杂的方式取决于该像素的上下文(周围像素的值)。我们的数据可能会有一种表示,这种表示会考虑到我们在特征之间的相关交互作用。在此表示的基础上建立一个线性模型可能会是合适的, 但我们不知道如何手动计算这么一种表示。对于深度神经网络,我们使用观测数据来联合学习隐藏层表示和应用于该表示的线性预测器
        • 在网络中加入隐藏层

          • 我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制,使其能处理更普遍的函数关系类型。要做到这一点,最简单的方法是将许多全连接层堆叠在一起。每一层都输出到上面的层,直到生成最后的输出。我们可以把前L−1层看作表示,把最后一层看作线性预测器。 这种架构通常称为**多层感知机,通常缩写为 MLP **。 下面,我们以图的方式描述了多层感知机
          • image.png
          • 这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2。注意,这两个层都是全连接的。每个输入都会影响隐藏层中的每个神经元,而隐藏层中的每个神经元又会影响输出层中的每个神经元。
        • 从线性到非线性

          • ((64c9b4e9-6396-4d21-85a0-51edc4e76201))
          • 注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。可我们能从中得到什么好处呢?在上面定义的模型里,我们没有好处!原因很简单:上面的隐藏单元由输入的仿射函数给出,而输出(softmax操作前)只是隐藏单元的仿射函数。仿射函数的仿射函数本身就是仿射函数,但是我们之前的线性模型已经能够表示任何仿射函数。
          • 为了发挥多层架构的潜力, 我们还需要一个额外的关键要素:在仿射变换之后对每个隐藏单元应用非线性的**激活函数σ。激活函数的输出(例如,σ(·))被称为活性值**。一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型: \mathbf{H} & = \sigma(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}), \\ \mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}.\\ \end{aligned}\end{split}$$
          • 由于X中的每一行对应于小批量中的一个样本, 出于记号习惯的考量, 我们定义非线性函数\sigma也以按行的方式作用于其输入,即一次计算一个样本。 我们在 3.4.5节中以相同的方式使用了softmax符号来表示按行操作。但是本节应用于隐藏层激活函数通常不仅按行操作,也按元素操作。这意味着在计算每一层的线性部分之后,我们可以计算每个活性值,而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。
          • 为了构建更通用的多层感知机,我们可以继续堆叠这样的隐藏层,从而产生更有表达能力的模型。
        • 通用近似定理

          • 多层感知机可以通过隐藏神经元,捕捉到输入之间复杂的相互作用,这些神经元依赖于每个输入的值。我们可以很容易地设计隐藏节点来执行任意计算。例如,在一对输入上进行基本逻辑操作,多层感知机是通用近似器。即使是网络只有一个隐藏层,给定足够的神经元和正确的权重,我们可以对任意函数建模,尽管实际中学习该函数是很困难的。神经网络有点像C语言。C语言和任何其他现代编程语言一样,能够表达任何可计算的程序。但实际上,想出一个符合规范的程序才是最困难的部分。
          • 而且,虽然一个单隐层网络能学习任何函数,但并不意味着我们应该尝试使用单隐藏层网络来解决所有问题。事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。
      • 4.1.2. 激活函数

        • 激活函数通过计算加权和并加上偏置来确定神经元是否应该被激活,它们将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。由于激活函数是深度学习的基础,下面简要介绍一些常见的激活函数
        • ReLU函数

          • 最受欢迎的激活函数是**修正线性单元ReLU )**,因为它实现简单,同时在各种预测任务中表现良好。 ReLU提供了一种非常简单的非线性变换。 给定元素x,ReLU函数被定义为该元素与0的最大值:
          • 通俗地说,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。为了直观感受一下,我们可以画出函数的曲线图。
          • x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
            y = torch.relu(x)
            d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))
            # x.detach()和y.detach()是为了脱离计算图,以便在绘制图像时不进行梯度计算。
            
          • 当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。在此时,我们默认使用左侧的导数,即当输入为0时导数为0。下面我们绘制ReLU函数的导数。
          • y.backward(torch.ones_like(x), retain_graph=True)
            d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
            
          • 使用 ReLU 的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。这使得优化表现得更好,并且 ReLU 减轻了困扰以往神经网络梯度消失问题。
          • 注意,ReLU函数有许多变体,包括参数化ReLU函数。该变体为 ReLU 添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过:
        • sigmoid函数

          • 对于一个定义域在中的输入, sigmoid函数 将输入变换为区间(0, 1)上的输出。因此, sigmoid 通常称为**挤压函数**:它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:
          • 在最早的神经网络中,科学家们感兴趣的是对“激发”或“不激发”的生物神经元进行建模。因此,这一领域的先驱专注于阈值单元。阈值单元在其输入低于某个阈值时取值0,当输入超过阈值时取值1。
          • 当人们逐渐关注到到基于梯度的学习时,sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似。当我们想要将输出视作二元分类问题的概率时, sigmoid 仍然被广泛用作输出单元上的激活函数 (sigmoid可以视为softmax的特例)。然而, sigmoid隐藏层中已经较少使用,它在大部分时候被更简单、更容易训练的 ReLU 所取代。
          • 下面,我们绘制sigmoid函数。注意,当输入接近0时,sigmoid函数接近线性变换。
          • y = torch.sigmoid(x)
            d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
            
          • sigmoid函数的导数为下面的公式:
          • sigmoid函数的导数图像如下所示。注意,当输入为0时,sigmoid函数的导数达到最大值0.25;而输入在任一方向上越远离0点时,导数越接近0。
          • # 清除以前的梯度
            x.grad.data.zero_()
            y.backward(torch.ones_like(x),retain_graph=True)
            d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))
            
        • tanh函数

          • sigmoid函数类似, tanh函数 (双曲正切)也能将其输入压缩转换到区间(-1, 1)上。tanh函数的公式如下:
          • 下面我们绘制tanh函数。注意,当输入在0附近时,tanh函数接近线性变换。函数的形状类似于sigmoid函数,不同的是tanh函数关于坐标系原点中心对称。
          • y = torch.tanh(x)
            d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))
            
          • tanh函数的导数是:
          • tanh函数的导数图像如下所示。当输入接近0时,tanh函数的导数接近最大值1。与我们在sigmoid函数图像中看到的类似,输入在任一方向上越远离0点,导数越接近0。
          • # 清除以前的梯度
            x.grad.data.zero_()
            y.backward(torch.ones_like(x),retain_graph=True)
            d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))
            
      • 4.1.3. 小结

      • 4.1.4. 练习

    • 4.2. 多层感知机的从零开始实现

      collapsed:: true
      • 现在让我们尝试自己实现一个多层感知机。为了与之前softmax回归获得的结果进行比较, 我们将继续使用Fashion-MNIST图像分类数据集。
      • 4.2.1. 初始化模型参数

        • ((64c9eb0c-919a-4826-9fe7-f003a68b48fe))
        • num_inputs, num_outputs, num_hiddens = 784, 10, 256
          
          W1 = nn.Parameter(torch.randn(
              num_inputs, num_hiddens, requires_grad=True) * 0.01)
          b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
          W2 = nn.Parameter(torch.randn(
              num_hiddens, num_outputs, requires_grad=True) * 0.01)
          b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))
          
          params = [W1, b1, W2, b2]
          
      • 4.2.2. 激活函数

        • 为了确保我们对模型的细节了如指掌,我们将实现ReLU函数, 而不是直接调用。
        • def relu(X):
              a = torch.zeros_like(X)
              return torch.max(X, a)
          
      • 4.2.3. 模型

        • 因为我们忽略了空间结构,所以我们使用reshape将每个二维图像转换为一个长度为num_inputs的向量。
        • def net(X):
              X = X.reshape((-1, num_inputs))
              H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法
              return (H@W2 + b2)
          
      • 4.2.4. 损失函数

        • 由于我们已经从零实现过softmax函数,因此在这里我们直接使用高级API中的内置函数来计算softmax和交叉熵损失。回想一下我们之前在 3.7.2节中对这些复杂问题的讨论。我们鼓励感兴趣的读者查看损失函数的源代码,以加深对实现细节的了解。
        • loss = nn.CrossEntropyLoss(reduction='none')
          
      • 4.2.5. 训练

        • 多层感知机的训练过程与softmax回归的训练过程完全相同。可以直接调用d2l包的train_ch3函数(参见 3.6节 ),将迭代周期数设置为10,并将学习率设置为0.1。
        • num_epochs, lr = 10, 0.1
          updater = torch.optim.SGD(params, lr=lr)
          d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
          
        • image.png
        • 为了对学习到的模型进行评估,我们将在一些测试数据上应用这个模型。
        • d2l.predict_ch3(net, test_iter)
          
      • 4.2.6. 小结

        • 手动实现一个简单的多层感知机是很容易的。然而如果有大量的层,从零开始实现多层感知机会变得很麻烦(例如,要命名和记录模型的参数)。
      • 4.2.7. 练习

    • 4.3. 多层感知机的简洁实现

      collapsed:: true
      • 本节将介绍通过高级API更简洁地实现多层感知机
      • import torch
        from torch import nn
        from d2l import torch as d2l
        
      • 4.3.1. 模型

        • softmax回归的简洁实现相比,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。第一层是隐藏层,它包含256个隐藏单元,并使用了ReLU激活函数。第二层是输出层。
        • net = nn.Sequential(nn.Flatten(),
                              nn.Linear(784, 256),
                              nn.ReLU(),
                              nn.Linear(256, 10))
          
          def init_weights(m):
              if type(m) == nn.Linear:
                  nn.init.normal_(m.weight, std=0.01)
          
          net.apply(init_weights);
          
        • 训练过程的实现与我们实现softmax回归时完全相同, 这种模块化设计使我们能够将与模型架构有关的内容独立出来。
        • batch_size, lr, num_epochs = 256, 0.1, 10
          loss = nn.CrossEntropyLoss(reduction='none')
          trainer = torch.optim.SGD(net.parameters(), lr=lr)
          
          train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
          d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
          
        • image.png
      • 4.3.2. 小结

      • 4.3.3. 练习

    • 4.4. 模型选择、欠拟合过拟合

      collapsed:: true
      • 作为机器学习科学家,我们的目标是发现模式, 这些模式捕捉到了我们训练集潜在总体的规律。如果成功做到了这点,即使是对以前从未遇到过的个体,模型也可以成功地评估风险。如何发现可以**泛化**的模式是机器学习的根本问题。
      • 困难在于,当我们训练模型时,我们只能访问数据中的小部分样本。最大的公开图像数据集包含大约一百万张图像。而在大部分时候,我们只能从数千或数万个数据样本中学习。在大型医院系统中,我们可能会访问数十万份医疗记录。当我们使用有限的样本时,可能会遇到这样的问题:当收集到更多的数据时,会发现之前找到的明显关系并不成立。
      • 将模型在训练数据上拟合的比在潜在分布中更接近的现象称为**过拟合, 用于对抗过拟合的技术称为正则化**。在前面的章节中,有些读者可能在用Fashion-MNIST数据集做实验时已经观察到了这种过拟合现象。在实验中调整模型架构或超参数时会发现:如果有足够多的神经元、层数和训练迭代周期,模型最终可以在训练集上达到完美的精度,此时测试集的准确性却下降了
      • 4.4.1. 训练误差泛化误差

        • 为了进一步讨论这一现象,我们需要了解训练误差泛化误差。**训练误差是指, 模型在训练数据集上计算得到的误差。泛化误差**是指, 模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。
        • 问题是,我们永远不能准确地计算出泛化误差。 这是因为无限多的数据样本是一个虚构的对象。 在实际中,我们只能通过将模型应用于一个独立的测试集来估计泛化误差, 该测试集由随机选取的、未曾在训练集中出现的数据样本构成。
        • 统计学习理论

          • 泛化是机器学习中的基本问题,许多数学家和理论家致力于研究描述泛化的形式理论。格里文科和坎特利在同名定理中推导出了训练误差收敛到泛化误差的速率,而Vapnik和Chervonenkis将这一理论扩展到更一般的函数。这为统计学习理论奠定了基础。
          • 目前在监督学习中,假设训练数据和测试数据都是从相同的分布中独立提取的,即独立同分布假设。然而,实际情况中可能存在违背独立同分布假设的因素,如不同数据集的分布差异或抽样过程与时间相关。虽然轻微违背独立同分布假设时模型仍可能良好运行,但某些违背行为会导致问题。因此,理解泛化性和解释深度神经网络泛化性能的理论基础仍是学习理论领域的难题。
          • 当我们训练模型时,过拟合是一个需要避免或控制的问题,即模型过于拟合训练数据而无法良好泛化。深度学习中有多种启发式技术可用于防止过拟合
        • 模型复杂性

          • ((64ca0204-f9bf-491f-b562-1dcd960c6779))
          • 就目前而言,一条简单的经验法则相当有用:统计学家认为,能够轻松解释任意事实的模型是复杂的,而表达能力有限但仍能很好地解释数据的模型可能更有现实用途。
          • ((64ca025e-6545-4ef2-ae92-cd212dd7adab))
      • 4.4.2. 模型选择

        • 在机器学习中,我们通常在评估几个候选模型后选择最终的模型。这个过程叫做模型选择。有时,需要进行比较的模型在本质上是完全不同的(比如,决策树与线性模型)。又有时,我们需要比较不同的超参数设置下的同一类模型。例如,训练多层感知机模型时,我们可能希望比较具有不同数量的隐藏层、不同数量的隐藏单元以及不同的激活函数组合的模型。为了确定候选模型中的最佳模型,我们通常会使用验证集。
        • 验证集

          • 为了避免训练过程中的过拟合,我们通常会将训练数据划分为训练集和验证集,而不直接使用测试集进行模型的选择和调优。验证集的目的是提供一个与训练集相独立的数据集,以评估模型在泛化数据上的性能。
          • 一般的做法是将训练数据划分为训练集和验证集,例如将训练数据的70-80%用作训练集,剩余的20-30%用作验证集。然后使用训练集训练模型,并使用验证集评估模型的性能。根据验证集的结果,我们可以进行超参数的调整和模型的改进,以获得更好的性能。
          • 需要注意的是,在模型调优完成后,我们需要最终评估模型的性能,这时需要使用独立于训练过程的测试集。测试集是一组模型从未见过的样本,用于评估模型在真实场景下的性能表现。
          • 形象上来说训练集就像是学生的课本,学生根据课本里的内容来掌握知识,验证集就像是作业,可以知道不同学生学习情况、进步的速度快慢,而最终的测试集就像是考试,考的题是平常都没有见过,考察学生举一反三的能力。此外,仅凭一次考试就对模型的好坏进行评判显然是不合理的,所以接下来就要介绍K折交叉验证法
        • K折交叉验证

          • 这里,原始训练数据被分成K个不重叠的子集。然后执行K次模型训练和验证,每次在K−1个子集上进行训练,并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。最后,通过对K次实验的结果取平均来估计训练和验证误差。
      • 4.4.3. 欠拟合还是过拟合

        • 当我们比较训练和验证误差时,我们要注意两种常见的情况。首先,我们要注意这样的情况:训练误差和验证误差都很严重,但它们之间仅有一点差距。如果模型不能降低训练误差,这可能意味着模型过于简单(即表达能力不足),无法捕获试图学习的模式。此外,由于我们的训练和验证误差之间的泛化误差很小, 我们有理由相信可以用一个更复杂的模型降低训练误差。 这种现象被称为**欠拟合**。
        • 另一方面,当我们的训练误差明显低于验证误差时要小心, 这表明严重的**过拟合**。 注意,过拟合并不总是一件坏事。通常我们更关心验证误差,而不是训练误差和验证误差之间的差距。
        • 是否过拟合欠拟合可能取决于模型复杂性和可用训练数据集的大小, 这两个点将在下面进行讨论。
        • 模型复杂度

          • 模型复杂性与过拟合欠拟合之间有密切的关系。理解这种关系可以通过一个简单的例子来说明。
          • 假设我们有一个二维的数据集,其中的每个样本都有一个输入特征x和一个目标标签y。我们想要通过训练一个模型来学习输入特征x与目标标签y之间的关系。我们可以选择不同复杂度的模型来解决这个问题。例如,我们可以选择一个非常简单的模型,如线性模型,它只有一个参数w和一个偏置b,模型的输出为y_pred = w * x + b。另一方面,我们可以选择一个更复杂的模型,如多项式模型,它可以通过引入更多的参数来适应更复杂的数据模式。例如,我们可以使用多项式模型 y_pred = w1 * x + w2 * x^2 + w3 * x^3 + b。
          • 如果我们选择一个非常复杂的模型来拟合相对较小的数据集,模型可能会过度拟合训练数据,即将噪声也学习进去。相反,如果我们选择一个非常简单的模型来拟合复杂的数据模式,模型可能无法捕捉到数据中的重要特征和模式,导致在训练数据和测试数据上都表现不佳。
          • 因此,模型复杂性是在解决机器学习问题时需要权衡的一个关键因素。我们希望选择一个适度复杂的模型,能够恰好捕捉到数据中的重要特征和模式,以实现较好的泛化性能。
        • 数据集大小

          • 训练数据集中的样本越少,我们就越有可能(且更严重地)过拟合。随着训练数据量的增加,泛化误差通常会减小。此外,一般来说,更多的数据不会有什么坏处。
      • 4.4.4. 多项式回归

        • 生成数据集

          • 我们现在可以通过多项式拟合来探索这些概念。
          • 给定x,我们将使用以下三阶多项式来生成训练和测试数据的标签: \epsilon \sim \mathcal{N}(0, 0.1^2).$$
          • 噪声项\epsilon服从均值为0且标准差为0.1的正态分布。在优化的过程中,我们通常希望避免非常大的梯度值或损失值。这就是我们将特征从调整为的原因, 这样可以避免很大的i带来的特别大的指数值。我们将为训练集和测试集各生成100个样本。
          • max_degree = 20  # 多项式的最大阶数
            n_train, n_test = 100, 100  # 训练和测试数据集大小
            true_w = np.zeros(max_degree)  # 分配大量的空间
            true_w[0:4] = np.array([5, 1.2, -3.4, 5.6])
            
            features = np.random.normal(size=(n_train + n_test, 1))
            np.random.shuffle(features)
            poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
            for i in range(max_degree):
                poly_features[:, i] /= math.gamma(i + 1)  # gamma(n)=(n-1)!
            # labels的维度:(n_train+n_test,)
            labels = np.dot(poly_features, true_w)
            labels += np.random.normal(scale=0.1, size=labels.shape)
            
          • 同样,存储在poly_features中的单项式由gamma函数重新缩放, 其中。从生成的数据集中查看一下前2个样本,第一个值是与偏置相对应的常量特征。
          • # NumPy ndarray转换为tensor
            true_w, features, poly_features, labels = [torch.tensor(x, dtype=
                torch.float32) for x in [true_w, features, poly_features, labels]]
            
        • 对模型进行训练和测试

          • 首先让我们实现一个函数来评估模型在给定数据集上的损失。
          • def evaluate_loss(net, data_iter, loss):  #@save
                """评估给定数据集上模型的损失"""
                metric = d2l.Accumulator(2)  # 损失的总和,样本数量
                for X, y in data_iter:
                    out = net(X)
                    y = y.reshape(out.shape)
                    l = loss(out, y)
                    metric.add(l.sum(), l.numel())
                return metric[0] / metric[1]
            
          • 现在定义训练函数。
          • def train(train_features, test_features, train_labels, test_labels,
                      num_epochs=400):
                loss = nn.MSELoss(reduction='none')
                input_shape = train_features.shape[-1]
                # 不设置偏置,因为我们已经在多项式中实现了它
                net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
                batch_size = min(10, train_labels.shape[0])
                train_iter = d2l.load_array((train_features, train_labels.reshape(-1,1)),
                                            batch_size)
                test_iter = d2l.load_array((test_features, test_labels.reshape(-1,1)),
                                           batch_size, is_train=False)
                trainer = torch.optim.SGD(net.parameters(), lr=0.01)
                animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log',
                                        xlim=[1, num_epochs], ylim=[1e-3, 1e2],
                                        legend=['train', 'test'])
                for epoch in range(num_epochs):
                    d2l.train_epoch_ch3(net, train_iter, loss, trainer)
                    if epoch == 0 or (epoch + 1) % 20 == 0:
                        animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),
                                                 evaluate_loss(net, test_iter, loss)))
                print('weight:', net[0].weight.data.numpy())
            
        • 三阶多项式函数拟合(正常)

          • 我们将首先使用三阶多项式函数,它与数据生成函数的阶数相同。结果表明,该模型能有效降低训练损失和测试损失。学习到的模型参数也接近真实值w = [5, 1.2, −3.4, 5.6]。
          • # 从多项式特征中选择前4个维度,即1,x,x^2/2!,x^3/3!
            train(poly_features[:n_train, :4], poly_features[n_train:, :4],
                  labels[:n_train], labels[n_train:])
            
        • 线性函数拟合(欠拟合

          • 让我们再看看线性函数拟合,减少该模型的训练损失相对困难。在最后一个迭代周期完成后,训练损失仍然很高。当用来拟合非线性模式(如这里的三阶多项式函数)时,线性模型容易欠拟合
          • # 从多项式特征中选择前2个维度,即1和x
            train(poly_features[:n_train, :2], poly_features[n_train:, :2],
                  labels[:n_train], labels[n_train:])
            
        • 高阶多项式函数拟合(过拟合

          • 现在,让我们尝试使用一个阶数过高的多项式来训练模型。在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。虽然训练损失可以有效地降低,但测试损失仍然很高。结果表明,复杂模型对数据造成了过拟合
          • # 从多项式特征中选取所有维度
            train(poly_features[:n_train, :], poly_features[n_train:, :],
                  labels[:n_train], labels[n_train:], num_epochs=1500)
            
      • 4.4.5. 小结

        • 欠拟合是指模型无法继续减少训练误差。过拟合是指训练误差远小于验证误差。 logseq.order-list-type:: number
        • 由于不能基于训练误差来估计泛化误差,因此简单地最小化训练误差并不一定意味着泛化误差的减小。机器学习模型需要注意防止过拟合,即防止泛化误差过大。 logseq.order-list-type:: number
        • 验证集可以用于模型选择,但不能过于随意地使用它。 logseq.order-list-type:: number
        • 我们应该选择一个复杂度适当的模型,避免使用数量不足的训练样本。 logseq.order-list-type:: number
      • 4.4.6. 练习

    • 4.5. 权重衰减

      collapsed:: true
      • 我们总是可以通过去收集更多的训练数据来缓解过拟合。但这可能成本很高,耗时颇多,或者完全超出我们的控制,因而在短期内不可能做到。假设我们已经拥有尽可能多的高质量数据,我们便可以将重点放在正则化技术上。
      • 在训练参数化机器学习模型时,权重衰减是最广泛使用的正则化的技术之一,它通常也被称为L2正则化。这项技术通过函数与零的距离来衡量函数的复杂度,因为在所有函数f中,函数f = 0(所有输入都得到值0)在某种意义上是最简单的。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。
      • 一种简单的方法是通过线性函数 (f(\mathbf{x}) = \mathbf{w}^\top \mathbf{x}) 中的权重向量的某个范数来度量其复杂性, 例如(| \mathbf{w} |^2)。 要保证权重向量比较小, 最常用方法是将其范数作为惩罚项加到最小化损失的问题中。 将原来的训练目标最小化训练标签上的预测损失, 调整为最小化预测损失和惩罚项之和。 现在,如果我们的权重向量增长的太大, 我们的学习算法可能会更集中于最小化权重范数(| \mathbf{w} |^2)。 这正是我们想要的。 让我们回顾一下 3.1节中的线性回归例子。 我们的损失由下式给出:
      • 回想一下,(\mathbf{x}^{(i)})是样本(i)的特征, (y^{(i)})是样本(i)的标签, ((\mathbf{w}, b))是权重和偏置参数。为了惩罚权重向量的大小,我们必须以某种方式在损失函数中添加(| \mathbf{w} |^2),但是模型应该如何平衡这个新的额外惩罚的损失?实际上,我们通过**正则化常数(\lambda)来描述这种权衡, 这是一个非负超参数**,我们使用验证数据拟合:
      • 对于(\lambda = 0),我们恢复了原来的损失函数。对于(\lambda > 0),我们限制(| \mathbf{w} |)的大小。这里我们仍然除以(2):当我们取一个二次函数的导数时,(2)和(1/2)会抵消,以确保更新表达式看起来既漂亮又简单。为什么在这里我们使用平方范数而不是标准范数(即欧几里得距离)?我们这样做是为了便于计算。通过平方(L_2)范数,我们去掉平方根,留下权重向量每个分量的平方和。这使得惩罚的导数很容易计算:导数的和等于和的导数。
      • 此外,为什么我们首先使用(L_2)范数,而不是(L_1)范数。事实上,这个选择在整个统计领域中都是有效的和受欢迎的。L2正则化线性模型构成经典的岭回归算法,L1正则化线性回归是统计学中类似的基本模型, 通常被称为套索回归。使用(L_2)范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。在实践中,这可能使它们对单个变量中的观测误差更为稳定。相比之下,(L_1)惩罚会导致模型将权重集中在一小部分特征上,而将其他权重清除为零。这称为特征选择,这可能是其他场景下需要的。
      • 使用与 (3.1.10)中的相同符号,L2正则化回归的小批量随机梯度下降更新如下式:
      • 根据之前章节所讲的,我们根据估计值与观测值之间的差异来更新(\mathbf{w})。 然而,我们同时也在试图将(\mathbf{w})的大小缩小到零。这就是为什么这种方法有时被称为**权重衰减。我们仅考虑惩罚项,优化算法在训练的每一步衰减**权重。与特征选择相比,权重衰减为我们提供了一种连续的机制来调整函数的复杂度。较小的(\lambda)值对应较少约束的(\mathbf{w}),而较大的(\lambda)值对(\mathbf{w})的约束更大。
      • 是否对相应的偏置(b^2)进行惩罚在不同的实践中会有所不同,在神经网络的不同层中也会有所不同。通常,网络输出层的偏置项不会被正则化
      • 4.5.1. 高维线性回归

        • 我们通过一个简单的例子来演示权重衰减。
        • import torch
          from torch import nn
          from d2l import torch as d2l
          
        • 首先,我们像以前一样生成一些数据,生成公式如下:
        • 我们选择标签是关于输入的线性函数。标签同时被均值为0,标准差为0.01高斯噪声破坏。为了使过拟合的效果更加明显,我们可以将问题的维数增加到(d = 200), 并使用一个只包含20个样本的小训练集。
        • n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
          true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
          train_data = d2l.synthetic_data(true_w, true_b, n_train)
          train_iter = d2l.load_array(train_data, batch_size)
          test_data = d2l.synthetic_data(true_w, true_b, n_test)
          test_iter = d2l.load_array(test_data, batch_size, is_train=False)
          
      • 4.5.2. 从零开始实现

        • 下面我们将从头开始实现权重衰减,只需将L2的平方惩罚添加到原始目标函数中。
        • 初始化模型参数

          • 首先,定义一个函数来随机初始化模型参数。
          • def init_params():
                w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
                b = torch.zeros(1, requires_grad=True)
                return [w, b]
            
        • 定义(L_2)范数惩罚

          • 实现这一惩罚。
          • def l2_penalty(w):
                return torch.sum(w.pow(2)) / 2
            
        • 定义训练代码实现

          • 下面的代码将模型拟合训练数据集,并在测试数据集上进行评估。
          • def train(lambd):
                w, b = init_params()
                net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
                num_epochs, lr = 100, 0.003
                animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
                                        xlim=[5, num_epochs], legend=['train', 'test'])
                for epoch in range(num_epochs):
                    for X, y in train_iter:
                        # 增加了L2范数惩罚项,
                        # 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
                        l = loss(net(X), y) + lambd * l2_penalty(w)
                        l.sum().backward()
                        d2l.sgd([w, b], lr, batch_size)
                    if (epoch + 1) % 5 == 0:
                        animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
                                                 d2l.evaluate_loss(net, test_iter, loss)))
                print('w的L2范数是:', torch.norm(w).item())
            
        • 忽略正则化直接训练

          • 我们现在用lambd = 0禁用权重衰减后运行这个代码。注意,这里训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合
          • train(lambd=0)
            
          • image.png
        • 使用权重衰减

          • 下面,我们使用权重衰减来运行代码。注意,在这里训练误差增大,但测试误差减小。这正是我们期望从正则化中得到的效果。
          • train(lambd=3)
            
          • image.png
      • 4.5.3. 简洁实现

        • 由于权重衰减神经网络优化中很常用,深度学习 框架为了便于我们使用权重衰减, 将其集成到优化算法中,以便与任何损失函数结合使用。此外,这种集成还有计算上的好处,允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。由于更新的权重衰减部分仅依赖于每个参数的当前值, 因此优化器必须至少接触每个参数一次。
        • def train_concise(wd):
              net = nn.Sequential(nn.Linear(num_inputs, 1))
              for param in net.parameters():
                  param.data.normal_()
              loss = nn.MSELoss(reduction='none')
              num_epochs, lr = 100, 0.003
              # 偏置参数没有衰减
              trainer = torch.optim.SGD([
                  {"params":net[0].weight,'weight_decay': wd},
                  {"params":net[0].bias}], lr=lr)
              animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
                                      xlim=[5, num_epochs], legend=['train', 'test'])
              for epoch in range(num_epochs):
                  for X, y in train_iter:
                      trainer.zero_grad()
                      l = loss(net(X), y)
                      l.mean().backward()
                      trainer.step()
                  if (epoch + 1) % 5 == 0:
                      animator.add(epoch + 1,
                                   (d2l.evaluate_loss(net, train_iter, loss),
                                    d2l.evaluate_loss(net, test_iter, loss)))
              print('w的L2范数:', net[0].weight.norm().item())
          
        • 这些图看起来和我们从零开始实现权重衰减时的图相同。然而,它们运行得更快,更容易实现。对于更复杂的问题,这一好处将变得更加明显。
      • 4.5.4. 小结

        • 正则化是处理过拟合的常用方法:在训练集的损失函数中加入惩罚项,以降低学习到的模型的复杂度。 logseq.order-list-type:: number
        • 保持模型简单的一个特别的选择是使用L2惩罚的权重衰减。这会导致学习算法更新步骤中的权重衰减。 logseq.order-list-type:: number
        • 权重衰减功能在深度学习框架的优化器中提供。 logseq.order-list-type:: number
        • 在同一训练代码实现中,不同的参数集可以有不同的更新行为。 logseq.order-list-type:: number
      • 4.5.5. 练习

    • 4.6. 暂退法(Dropout)

      collapsed:: true
      • 在上节中,我们介绍了通过惩罚权重的L2范数来正则化统计模型的经典方法。从概率⻆度看,我们可以通过以下论证来证明这一技术的合理性:我们已经假设了一个先验,即权重的值取自均值为0的高斯分布。更直观的是,我们希望模型深度挖掘特征,即将其权重分散到许多特征中,而不是过于依赖少数潜在的虚假关联。
      • 4.6.1. 重新审视过拟合

        • 当面对更多的特征而样本不足时,线性模型往往会过拟合。相反,当给出更多样本而不是特征,通常线性模型不会过拟合。不幸的是,线性模型泛化的可靠性是有代价的。简单地说,线性模型没有考虑到特征之间的交互作用。对于每个特征,线性模型必须指定正的或负的权重,而忽略其他特征。 泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡(bias-variance tradeoff)。线性模型有很高的偏差:它们只能表示一小类函数。然而,这些模型的方差很低:它们在不同的随机数据样本上可以得出相似的结果。
        • 深度神经网络位于偏差-方差谱的另一端。与线性模型不同,神经网络并不局限于单独查看每个特征,而是学习特征之间的交互。例如,神经网络可能推断“尼日利亚”和“西联汇款”一起出现在电子邮件中表示垃圾邮件,但单独出现则不表示垃圾邮件。
        • 即使我们有比特征多得多的样本,深度神经网络也有可能过拟合。2017年,一组研究人员通过在随机标记的图像上训练深度网络。这展示了神经网络的极大灵活性,因为人类很难将输入和随机标记的输出联系起来,但通过随机梯度下降优化的神经网络可以完美地标记训练集中的每一幅图像。想一想这意味着什么?假设标签是随机均匀分配的,并且有10个类别,那么分类器在测试数据上很难取得高于10%的精度,那么这里的泛化差距就高达90%,如此严重的过拟合。
        • 深度网络的泛化性质令人费解,而这种泛化性质的数学基础仍然是悬而未决的研究问题。
      • 4.6.2. 扰动的稳健性

        • 经典泛化理论认为,为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。简单性以较小维度的形式展现。
        • 简单性的另一个角度是平滑性,即函数不应该对其输入的微小变化敏感。 例如,当我们对图像进行分类时,我们预计向像素添加一些随机噪声应该是基本无影响的。
        • 然后在2014年,斯里瓦斯塔瓦等人 (Srivastava et al., 2014) 提出了一个想法:在训练过程中,建议在计算后续层之前向网络的每一层注入噪声。因为当训练一个有多层的深层网络时,注入噪声只会在输入-输出映射上增强平滑性。
        • 这个想法被称为**暂退法**。 暂退法在前向传播过程中,计算每一内部层的同时注入噪声。 这种方法之所以被称为暂退法,因为我们从表面上看是在训练过程中丢弃一些神经元。在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些节点置零。
        • 需要说明的是,暂退法会破坏共适应性,就像有性生殖会破坏共适应的基因一样。
        • 那么关键的挑战就是如何注入这种噪声。 一种想法是以一种无偏向的方式注入噪声。这样在固定住其他层时,每一层的期望值等于没有噪音时的值。
        • 在标准暂退法正则化中,通过按保留(未丢弃)的节点的分数进行规范化来消除每一层的偏差。换言之,每个中间活性值[h]以暂退概率[p]由随机变量[h^{’}]替换,如下所示: h' = \begin{cases} 0 & \text{ 概率为 } p \\ \frac{h}{1-p} & \text{ 其他情况} \end{cases} \end{aligned}\end{split}$$
        • 根据此模型的设计,其期望值保持不变,即
      • 4.6.3. 实践中的暂退法

        • 回想一下 图4.1.1中带有1个隐藏层和5个隐藏单元的多层感知机。当我们将暂退法应用到隐藏层,以p的概率将隐藏单元置为零时, 结果可以看作一个只包含原始神经元子集的网络。比如在 图4.6.1中,删除了ℎ2和ℎ5,因此输出的计算不再依赖于ℎ2或ℎ5,并且它们各自的梯度在执行反向传播时也会消失。这样,输出层的计算不能过度依赖于ℎ1,…,ℎ5的任何一个元素。
        • image.png
        • 通常,我们在测试时不用暂退法。给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。然而也有一些例外:一些研究人员在测试时使用暂退法, 用于估计神经网络预测的“不确定性”:如果通过许多不同的暂退法遮盖后得到的预测结果都是一致的,那么我们可以说网络发挥更稳定。
      • 4.6.4. 从零开始实现

        • 在下面的代码中,我们实现dropout_layer函数,该函数以dropout的概率丢弃张量输入X中的元素,并重新缩放剩余部分:将剩余部分除以1.0-dropout。
        • import torch
          from torch import nn
          from d2l import torch as d2l
          
          
          def dropout_layer(X, dropout):
              assert 0 <= dropout <= 1
              # 在本情况中,所有元素都被丢弃
              if dropout == 1:
                  return torch.zeros_like(X)
              # 在本情况中,所有元素都被保留
              if dropout == 0:
                  return X
              mask = (torch.rand(X.shape) > dropout).float()
              return mask * X / (1.0 - dropout)
          
        • 定义模型参数

          • 同样,我们使用 3.5节中引入的Fashion-MNIST数据集。我们定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元。
          • num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
            
        • 定义模型

          • 我们可以将暂退法应用于每个隐藏层的输出(在激活函数之后),并且可以为每一层分别设置暂退概率:常⻅的技巧是在靠近输入层的地方设置较低的暂退概率。下面的模型将第一个和第二个隐藏层的暂退概率分别 设置为0.2和0.5,并且暂退法只在训练期间有效。
          • dropout1, dropout2 = 0.2, 0.5
            
            class Net(nn.Module):
                def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
                             is_training = True):
                    super(Net, self).__init__()
                    self.num_inputs = num_inputs
                    self.training = is_training
                    self.lin1 = nn.Linear(num_inputs, num_hiddens1)
                    self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
                    self.lin3 = nn.Linear(num_hiddens2, num_outputs)
                    self.relu = nn.ReLU()
            
                def forward(self, X):
                    H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
                    # 只有在训练模型时才使用dropout
                    if self.training == True:
                        # 在第一个全连接层之后添加一个dropout层
                        H1 = dropout_layer(H1, dropout1)
                    H2 = self.relu(self.lin2(H1))
                    if self.training == True:
                        # 在第二个全连接层之后添加一个dropout层
                        H2 = dropout_layer(H2, dropout2)
                    out = self.lin3(H2)
                    return out
            
            
            net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
            
        • 训练和测试

          • num_epochs, lr, batch_size = 10, 0.5, 256
            loss = nn.CrossEntropyLoss(reduction='none')
            train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
            trainer = torch.optim.SGD(net.parameters(), lr=lr)
            d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
            
          • image.png
      • 4.6.5. 简洁实现

        • net = nn.Sequential(nn.Flatten(),
                  nn.Linear(784, 256),
                  nn.ReLU(),
                  # 在第一个全连接层之后添加一个dropout层
                  nn.Dropout(dropout1),
                  nn.Linear(256, 256),
                  nn.ReLU(),
                  # 在第二个全连接层之后添加一个dropout层
                  nn.Dropout(dropout2),
                  nn.Linear(256, 10))
          
          def init_weights(m):
              if type(m) == nn.Linear:
                  nn.init.normal_(m.weight, std=0.01)
          
          net.apply(init_weights)
          
          trainer = torch.optim.SGD(net.parameters(), lr=lr)
          d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
          d2l.plt.show()
          
        • image.png
      • 4.6.6. 小结

        • 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。 logseq.order-list-type:: number
        • 暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。 logseq.order-list-type:: number
        • 暂退法将活性值ℎ替换为具有期望值ℎ的随机变量。 logseq.order-list-type:: number
        • 暂退法仅在训练期间使用。 logseq.order-list-type:: number
      • 4.6.7. 练习

    • 4.7. 前向传播反向传播和计算图

      collapsed:: true
      • 我们已经学习了如何用小批量随机梯度下降训练模型。然而当实现该算法时,我们只考虑了通过**前向传播**所涉及的计算。在计算梯度时,我们只调用了深度学习框架提供的反向传播函数,而不知其所以然。
      • 梯度的自动计算(自动微分)大大简化了深度学习算法的实现。本节将通过一些基本的数学和计算图, 深入探讨**反向传播**的细节。 首先,我们将重点放在带权重衰减L2正则化)的单隐藏层多层感知机上。
      • 4.7.1. 前向传播

        • **前向传播**指的是:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。
        • 我们将一步步研究单隐藏层神经网络的机制,为了简单起见,我们假设输入样本是,并且我们的隐藏层不包括偏置项。这里的中间变量是:
        • 其中隐藏层的权重参数。将中间变量通过激活函数\phi后,我们得到长度为ℎ的隐藏激活向量:
        • 隐藏变量ℎ也是一个中间变量。假设输出层的参数只有权重,我们可以得到输出层变量,它是一个长度为q的向量:
        • 假设损失函数为l,样本标签为y,我们可以计算单个数据样本的损失项,
        • 根据L2正则化的定义,给定超参数\lambda,正则化项为
        • 其中矩阵的Frobenius范数是将矩阵展平为向量后应用的L2范数。最后,模型在给定数据样本上的正则化损失为:
        • 在下面的讨论中,我们将J称为目标函数
      • 4.7.2. 前向传播计算图

        • 绘制计算图有助于我们可视化计算中操作符和变量的依赖关系。下图是与上述简单网络相对应的计算图,其中正方形表示变量,圆圈表示操作符。左下角表示输入,右上角表示输出。注意显示数据流的箭头方向主要是向右和向上的。
        • image.png
      • 4.7.3. 反向传播

        • 反向传播指的是计算神经网络参数梯度的方法。简言之,该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。 假设我们有函数,其中输入和输出X,Y,Z是任意形状的张量。利用链式法则,我们可以计算Z关于X的导数
        • 在这里,我们使用prod运算符在执行必要的操作(如换位和交换输入位置)后将其参数相乘。对于向量,这很简单,它只是矩阵-矩阵乘法。对于高维张量,我们使用适当的对应项。运算符prod指代了所有的这些符号。
        • 回想一下,在计算图 图4.7.1中的单隐藏层简单网络的参数是 反向传播的目的是计算梯度。为此,我们应用链式法则,依次计算每个中间变量和参数的梯度。计算的顺序与前向传播中执行的顺序相反,因为我们需要从计算图的结果开始,并朝着参数的方向努力。第一步是计算目标函数相对于损失项L和正则项s的梯度。
        • 接下来,我们根据链式法则计算目标函数关于输出层变量o的梯度: = \text{prod}\left(\frac{\partial J}{\partial L}, \frac{\partial L}{\partial \mathbf{o}}\right) = \frac{\partial L}{\partial \mathbf{o}} \in \mathbb{R}^q.$$
        • 接下来,我们计算正则化项相对于两个参数的梯度: \; \text{and} \; \frac{\partial s}{\partial \mathbf{W}^{(2)}} = \lambda \mathbf{W}^{(2)}.$$
        • 现在我们可以计算最接近输出层的模型参数的梯度。使用链式法则得出:
        • 为了获得关于的梯度,我们需要继续沿着输出层到隐藏层反向传播。关于隐藏层输出的梯度由下式给出: = \text{prod}\left(\frac{\partial J}{\partial \mathbf{o}}, \frac{\partial \mathbf{o}}{\partial \mathbf{h}}\right) = {\mathbf{W}^{(2)}}^\top \frac{\partial J}{\partial \mathbf{o}}.$$
        • 由于激活函数\phi是按元素计算的,计算中间变量z的梯度需要使用按元素乘法运算符,我们用⊙表示: = \text{prod}\left(\frac{\partial J}{\partial \mathbf{h}}, \frac{\partial \mathbf{h}}{\partial \mathbf{z}}\right) = \frac{\partial J}{\partial \mathbf{h}} \odot \phi'\left(\mathbf{z}\right).$$
        • 最后,我们可以得到最接近输入层的模型参数的梯度。根据链式法则,我们得到: = \text{prod}\left(\frac{\partial J}{\partial \mathbf{z}}, \frac{\partial \mathbf{z}}{\partial \mathbf{W}^{(1)}}\right) + \text{prod}\left(\frac{\partial J}{\partial s}, \frac{\partial s}{\partial \mathbf{W}^{(1)}}\right) = \frac{\partial J}{\partial \mathbf{z}} \mathbf{x}^\top + \lambda \mathbf{W}^{(1)}.$$
      • 4.7.4. 训练神经网络

        • 在训练神经网络时,前向传播反向传播相互依赖。 对于前向传播,我们沿着依赖的方向遍历计算图并计算其路径上的所有变量。 然后将这些用于反向传播,其中计算顺序与计算图的相反。
        • 以上述简单网络为例:一方面,在前向传播期间计算正则项 (4.7.5)取决于模型参数的当前值。它们是由优化算法根据最近迭代的反向传播给出的。 另一方面,反向传播期间参数 (4.7.11)的梯度计算,取决于由前向传播给出的隐藏变量ℎ的当前值。
        • 因此,在训练神经网络时,在初始化模型参数后,我们交替使用前向传播反向传播,利用反向传播给出的梯度来更新模型参数。注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算。带来的影响之一是我们需要保留中间值,直到反向传播完成。这也是训练比单纯的预测需要更多的内存(显存)的原因之一。此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。因此,使用更大的批量来训练更深层次的网络更容易导致内存不足错误。
      • 4.7.5. 小结

        • 前向传播神经网络定义的计算图中按顺序计算和存储中间变量,它的顺序是从输入层到输出层。 logseq.order-list-type:: number
        • 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。 logseq.order-list-type:: number
        • 在训练深度学习模型时,前向传播反向传播是相互依赖的。 logseq.order-list-type:: number
        • 训练比预测需要更多的内存。 logseq.order-list-type:: number
      • 4.7.6. 练习

    • 4.8. 数值稳定性和模型初始化

      collapsed:: true
      • 初始化方案的选择在神经网络学习中起着举足轻重的作用,它对保持数值稳定性至关重要。此外,这些初始化方案的选择可以与非线性激活函数的选择有趣的结合在一起。我们选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度有多快。糟糕选择可能会导致我们在训练时遇到梯度爆炸梯度消失。本节将更详细地探讨这些主题,并讨论一些有用的启发式方法。这些启发式方法在整个深度学习生涯中都很有用。
      • 4.8.1. 梯度消失梯度爆炸

        • ((64cca18e-5126-43c0-9141-206806e088e8))
        • 因此,我们容易受到数值下溢问题的影响。当将太多的概率乘在一起时,这些问题经常会出现。在处理概率时,一个常⻅的技巧是切换到对数空间,即将数值表示的压力从尾数转移到指数。不幸的是,上面的问题更为严重:最初,矩阵 M(l) 可能具有各种各样的特征值。他们可能很小,也可能很大;他们的乘积可能非常大,也可能非常小。不稳定梯度带来的风险不止在于数值表示;不稳定梯度也威胁到我们优化算法的稳定性。我们可能面临一些问题。要么是梯度爆炸问题:参数更新过大,破坏了模型的稳定收敛;要么是梯度消失问题:参数更新过小,在每次更新时几乎不会移动,导致模型无法学习。
        • 梯度消失

          • 曾经sigmoid函数很流行,因为它类似于阈值函数。由于早期的人工神经网络受到生物神经网络的启发,神经元要么完全激活要么完全不激活(就像生物神经元)的想法很有吸引力。然而,它却是导致梯度消失问题的一个常见的原因,让我们仔细看看sigmoid函数为什么会导致梯度消失
          • import torch
            from d2l import torch as d2l
            
            x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
            y = torch.sigmoid(x)
            y.backward(torch.ones_like(x))
            
            d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
                     legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
            
          • image.png
          • 正如上图,当sigmoid函数的输入很大或是很小时,它的梯度都会消失。此外,当反向传播通过许多层时,除非我们在刚刚好的地方,这些地方sigmoid函数的输入接近于零,否则整个乘积的梯度可能会消失。当我们的网络有很多层时,除非我们很小心,否则在某一层可能会切断梯度。事实上,这个问题曾经困扰着深度网络的训练。因此,更稳定的ReLU函数已经成为从业者的默认选择(虽然在神经科学的角度看起来不太合理)。
        • 梯度爆炸

          • 相反,梯度爆炸可能同样令烦恼。为了更好地说明这一点,我们生成100个高斯随机矩阵,并将它们与某个初始矩阵相乘。对于我们选择的尺度(方差),矩阵乘积发生爆炸。 当这种情况是由于深度网络的初始化所导致时,我们没有机会让梯度下降优化器收敛。
          • M = torch.normal(0, 1, size=(4,4))
            print('一个矩阵 \n',M)
            for i in range(100):
                M = torch.mm(M,torch.normal(0, 1, size=(4, 4)))
            
            print('乘以100个矩阵后\n', M)
            
        • 对破对称性

          • 神经网络设计中的另一个问题是其参数化所固有的对称性。假设我们有一个简单的多层感知机,它有一个隐藏层和两个隐藏单元。在这种情况下,我们可以对第一层的权重进行重排列,并且同样对输出层的权重进行重排列,可以获得相同的函数。第一个隐藏单元与第二个隐藏单元没有什么特别的区别。换句话说,我们在每一层的隐藏单元之间具有排列对称性。就好像我们把一道菜的盘子和碗的位置互换,它的味道和口感不会有任何变化一样。
          • 假设输出层将上述两个隐藏单元的多层感知机转换为仅一个输出单元。想象一下,如果我们将隐藏层的所有参数初始化为,c为常量,会发生什么?在这种情况下,在前向传播期间,两个隐藏单元采用相同的输入和参数,产生相同的激活,该激活被送到输出单元。在反向传播期间,根据参数对输出单元进行微分,得到一个梯度,其元素都取相同的值。因此,在基于梯度的迭代(例如,小批量随机梯度下降)之后,的所有元素仍然采用相同的值。这样的迭代永远不会打破对称性,我们可能永远也无法实现网络的表达能力。隐藏层的行为就好像只有一个单元。请注意,虽然小批量随机梯度下降不会打破这种对称性,但暂退法正则化可以。通过这些方法,我们可以打破参数之间的对称性,使神经网络更容易训练,找到更好的参数设置。就像我们在烹饪中会用不同的盘子和碗来装菜一样,让神经网络中的参数有所差别,可以让它更有效地学习和表现。
      • 4.8.2. 参数初始化

        • 解决(或至少减轻)上述问题的一种方法是进行参数初始化,优化期间的注意和适当的正则化也可以进一步提高稳定性。
        • 默认初始化

          • 在前面的部分中,例如在 3.3节中,我们使用正态分布来初始化权重值。如果我们不指定初始化方法,框架将使用默认的随机初始化方法,对于中等难度的问题,这种方法通常很有效。
        • Xavier初始化

          • 让我们看看某些没有非线性全连接层输出(例如,隐藏变量)的尺度分布。对于该层输入及其相关权重,输出由下式给出
          • 权重都是从同一分布中独立抽取的。此外,让我们假设该分布具有零均值和方差。现在,让我们假设层的输入也具有零均值和方差, 并且它们独立于并且彼此独立。在这种情况下,我们可以按如下方式计算的平均值和方差: E[o_i] & = \sum_{j=1}^{n_\mathrm{in}} E[w_{ij} x_j] \\&= \sum_{j=1}^{n_\mathrm{in}} E[w_{ij}] E[x_j] \\&= 0, \\ \mathrm{Var}[o_i] & = E[o_i^2] - (E[o_i])^2 \\ & = \sum_{j=1}^{n_\mathrm{in}} E[w^2_{ij} x^2_j] - 0 \\ & = \sum_{j=1}^{n_\mathrm{in}} E[w^2_{ij}] E[x^2_j] \\ & = n_\mathrm{in} \sigma^2 \gamma^2. \end{aligned}\end{split}$$
          • 保持方差不变的一种方法是设置。现在考虑反向传播过程,我们面临着类似的问题,尽管梯度是从更靠近输出的层传播的。使用与前向传播相同的推断,我们可以看到,除非,否则梯度的方差可能会增大,其中是该层的输出的数量。这使得我们进退两难:我们不可能同时满足这两个条件。相反,我们只需满足: \frac{1}{2} (n_\mathrm{in} + n_\mathrm{out}) \sigma^2 = 1 \text{ 或等价于 } \sigma = \sqrt{\frac{2}{n_\mathrm{in} + n_\mathrm{out}}}. \end{aligned}$$
          • 这就是现在标准且实用的**Xavier初始化**的基础。通常,Xavier初始化从均值为零,方差的高斯分布中采样权重。我们也可以将其改为选择从均匀分布中抽取权重时的方差。注意均匀分布的方差为。 将代入到的条件中,将得到初始化值域:
          • 尽管在上述数学推理中,“不存在非线性”的假设在神经网络中很容易被违反, 但Xavier初始化方法在实践中被证明是有效的。
        • 额外阅读

          • 上面的推理仅仅触及了现代参数初始化方法的皮毛。 深度学习框架通常实现十几种不同的启发式方法。此外,参数初始化一直是深度学习基础研究的热点领域。其中包括专门用于参数绑定(共享)、超分辨率、序列模型和其他情况的启发式算法。
      • 4.8.3. 小结

        • 梯度消失梯度爆炸深度网络中常见的问题。在参数初始化时需要非常小心,以确保梯度和参数可以得到很好的控制。 logseq.order-list-type:: number
        • 需要用启发式的初始化方法来确保初始梯度既不太大也不太小。 logseq.order-list-type:: number
        • ReLU激活函数缓解了梯度消失问题,这样可以加速收敛。 logseq.order-list-type:: number
        • 随机初始化是保证在进行优化前打破对称性的关键。 logseq.order-list-type:: number
        • Xavier初始化表明,对于每一层,输出的方差不受输入数量的影响,任何梯度的方差不受输出数量的影响。 logseq.order-list-type:: number
      • 4.8.4. 练习

    • 4.9. 环境和分布偏移

      • 前面我们学习了许多机器学习的实际应用,将模型拟合各种数据集。然而,我们从来没有想过数据最初从哪里来?以及我们计划最终如何处理模型的输出?通常情况下,开发人员会拥有一些数据且急于开发模型,而不关注这些基本问题。
      • 许多失败的机器学习部署(即实际应用)都可以追究到这种方式。有时,根据测试集的精度衡量,模型表现得非常出色。但是当数据分布突然改变时,模型在部署中会出现灾难性的失败。更隐蔽的是,有时模型的部署本身就是扰乱数据分布的催化剂。
      • 虽然我们不可能在一节中讨论全部的问题,但我们希望揭示一些常⻅的问题,并激发批判性思考,以便及早发现这些情况,减轻灾难性的损害。有些解决方案很简单(要求“正确”的数据),有些在技术上很困难(实施强化学习系统),还有一些解决方案要求我们完全跳出统计预测,解决一些棘手的、与算法伦理应用有关的哲学问题。
      • 4.9.1. 分布偏移的类型

        • 首先,我们考虑数据分布可能发生变化的各种方式,以及为挽救模型性能可能采取的措施。 在一个经典的情景中,假设训练数据是从某个分布中采样的, 但是测试数据将包含从不同分布中抽取的未标记样本。 一个清醒的现实是:如果没有任何关于之间相互关系的假设, 学习到一个分类器是不可能的。
        • 考虑一个二元分类问题:区分狗和猫。 如果分布可以以任意方式偏移,那么我们的情景允许病态的情况, 即输入的分布保持不变:��(�)=��(�), 但标签全部翻转:��(�|�)=1−��(�|�)。 换言之,如果将来所有的“猫”现在都是狗,而我们以前所说的“狗”现在是猫。 而此时输入�(�)的分布没有任何改变, 那么我们就不可能将这种情景与分布完全没有变化的情景区分开。
        • 幸运的是,在对未来我们的数据可能发生变化的一些限制性假设下, 有些算法可以检测这种偏移,甚至可以动态调整,提高原始分类器的精度。
      • 4.9.2. 分布偏移示例

      • 4.9.3. 分布偏移纠正

      • 4.9.4. 学习问题的分类法

      • 4.9.5. 机器学习中的公平、责任和透明度

      • 4.9.6. 小结

      • 4.9.7. 练习

    • 4.10. 实战Kaggle比赛:预测房价

      • 4.10.1. 下载和缓存数据集

      • 4.10.2. Kaggle

      • 4.10.3. 访问和读取数据集

      • 4.10.4. 数据预处理

      • 4.10.5. 训练

      • 4.10.6. (K)折交叉验证

      • 4.10.7. 模型选择

      • 4.10.8. 提交Kaggle预测

      • 4.10.9. 小结

      • 4.10.10. 练习