引言

本笔记参考了大量资料如下

李沐——动手学深度学习pytorch版

土堆——pytorch深度学习

吴恩达——机器学习2020

吴恩达——深度学习2020

李宏毅——深度学习2020

宋浩——概率论、高等数学

李航——统计学习方法


前言

基本上这个笔记很多部分都是用李沐博士的书来作为基础的,但是实话说,一堆术语对小白实在太不友好了。是的,我是那种白到不能再白的小白了,所以,别打我谢谢。


怎么使用这份笔记

如果你是想动手做一个项目的话,那么我建议你必须有很好的python基础。否则你里面的代码都会看不懂。在有良好的python基础上,我的笔记是对李沐博士书的剖析,并且加入一些我认为能够改良的知识。为了能够改良我也看了很多的视频和书,所以如果里面某个知识出了问题,这完全是能够理解的,因为我菜。所以要做项目的话你就必须从最简单的代码,层层深入,多查询torch的API。

那如果你是要用来应付大学的考试的话,那么我可以告诉你看这个笔记就够了,因为在原理部分我可以说是讲的非常多了,甚至为了让你记住一些东西我写了很多的废话。所以想要应付考试的话,你要在神经网络的计算图方面多动手去算,在优化方面和原理方面要去理解。


目录


第一章 介绍

1.1 欢迎学习

深度学习改变了传统互联网业务。第一次听到这个名词时可能大家都会对这方面的知识感到一头雾水,到底什么是深度学习?实际上,深度学习已经应用到生活中的点点滴滴了,比如我们熟知的自动无人驾驶,小爱同学音箱和其他的一些人工智能产品。在这个笔记中,你可以无需任何视频直接从头看到尾,也可以搭配任何一个深度学习的课程视频进行观看,当然,除了里面的代码部分,其他的对于所有的深度学习框架是通用的,代码部分主要用的是pytorch框架。


1.2 什么是神经网络

深度学习,实际上就是指训练神经网络的过程。有时它指的是特别大规模的神经网络训练。

那么,什么是神经网络呢?


第二章 预备知识

概述

很多人学不好深度学习,就是因为预备知识不够,深度学习对许多知识的整合能力要求是非常高的。

2.1 查阅文档

概述

image-20211223100707864

我们可以把torch这个包看成是一个工具箱,里面有1、2、3这么多分区,每个分区又能分成a、b、c这么多小分区。


重要函数

dir():查找模块中的所有函数和类

help():说明书

例如打开工具箱,就是dir(torch),如果要查看工具箱里面某些工具的说明书,即可以用help()。


dir()示例

import torch
print(dir(torch.distributions))

【注:通常,我们可以忽略以"__"(双下划线)开始和结束的函数(它们是Python中的特殊对象),或者以单个" _ "(单下划线)开始的函数(它们通常是内部函数)。】


help()示例

help(torch.ones)

小结

  • 官方文档提供了本书之外的大量描述和示例
  • 我们可以通过调用dirhelp函数或在Jupyter notebook中使用???查看API的用法文档

image-20211223110501250

【注:???的作用可以使一些不喜欢看一段一段解释的人可以看一大段的解释(笑)。】


2.2 数据操作

2.2.1 入门

为了在python内完成各种数据的操作,我们需要某种方法来存储和操作数据。通常,我们需要做的事情有两种:获取数据和处理数据。


导入工具包

我们虽然老是说学pytorch,但是实际上我们用的是torch包,只是由于在python里面,所以我们叫它pytorch。


张量

这个名词实际上很蛋疼,说白了就是我们平常说的n维数组,这里之所以要引入张量,是因为他的英文在torch里面可以和numpy中的多维数组的英文区分开。

也就是说,torch里面的张量叫tensor,而Numpy里面的张量叫ndarray,如果这两个英文一样,那在python中的调用就不好调用了是吧。

在深度学习框架中的张量一般都可以很好地支持GPU加速运算,而Numpy仅仅支持CPU计算。

基于前面我们说的那些,那么和以前高中相似,一个轴的张量叫做向量;具有两个轴的张量对应数学上的矩阵


张量相关的方法

arange

类似于Numpy中的arange,在torch中也有类似的方法可以创建。

x = torch.arange(12)
x

image-20211223153511895


shape

其中我们可以用shape来访问张量的形状。

x.shape

image-20211223153631605


numel

如果想知道张量中元素的总数,可以用numel

x.numel

image-20211223153856288


reshape

reshape顾名思义,就是重新改变形状。

X = x.reshape(3,4)
X

image-20211223154052369


zeros

顾名思义,就是创建全0的张量。

torch.zeros((2,3,4))

image-20211223154320509


ones

同上zeros,这里不做演示,就是创建全1的张量。


randn

定义的张量中所有的元素均从正态分布中随机采样。

torch.randn(3, 4)

image-20211223154531819


自定义

张量我们还可以自定义,但是要用嵌套列表来表示。

torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

image-20211223154649791


2.2.2 运算符

我们前面学习了怎么定义张量并且查看其大小以及一些其他的方法,但是我们希望的是进行线性代数中的运算,这就不得不提到一些运算符。


2.3 数据预处理

在python中,通过以前的学习我们得知,如果要进行数据分析的话,最常用的包就是pandas软件包。


2.3.1 读取数据集

这里随便创建一个人工数据集,并用CSV(逗号分隔符)文件来存储。

import os

os.makedirs(os.path.join('C:', 'data'), exist_ok=True)
data_file = os.path.join('C:', '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')
import pandas as pd

data = pd.read_csv(data_file)
print(data)

image-20211223160847036


复习

这里要用到数据分析的相关知识,在这里补充一下

很显然,每次通过手工构造数据框根本不现实。
如果要用python读取txt或者csv格式的数据,可以用Pandas模块中的read_table函数或者read_csv函数。

Pd.read_table参数详解:
Filepath_or_buffer:指定txt文件或csv文件所在的具体路径
Sep:指定原数据集中各字段之间的分隔符,默认为Tab制表符
Header:是否需要将原数据集中的第一行作为表头,默认将第一行用作字段名称
Names:如果原数据集中没有字段,可以通过该参数在数据读取时给数据框添加具体的表头
Index_col:指定原数据集中的某些列作为数据框的行索引
Usecols:指定需要读取原数据集中的哪些变量名
Dtype:读取数据时,可以为原数据集的每个字段设置不同的数据类型
Converters:通过字典格式,为数据集中的某些字段设置转换函数
Skiprows:数据读取时,指定需要跳过原数据集开头的行数
Skipfooter:数据读取时,指定需要跳过原数据集末尾的行数
Nrows:指定读取数据的行数
Na_values:指定原数据集中哪些特征的值作为缺失值
Skip_blank_lines:读取数据时是否需要跳过原数据集中的空白行,默认为Ture
Parse_dates:如果参数为Ture,则尝试解析数据框的行索引,如果参数为列表,则尝试解析对应的日期列
Thousands:指定原始数据集中的千分位符
Comment:指定注释符,在读取数据时,如果碰到行首指定的注释符,则跳过该行
Encoding:如果文件中含有中文,有时需要指定字符编码,utf-8或者gbk

常见问题的处理方法:
数据集并不是从第一行开始,前面几行实际上是数据集的来源说明,读取数据时需要注意什么问题
Skiprows:数据读取时,指定需要跳过原数据集开头的行数

数据集的末尾三行仍然不是需要读入的数据,如何避免后三行数据的读入
Skipfooter:数据读取时,指定需要跳过原数据集末尾的行数

中间部分的数据,第四行前加了#号,表示不需要读取该行,该如何处理
Comment:指定注释符,在读取数据时,如果碰到行首指定的注释符,则跳过该行

数据集中的收入一列,千分位符是&,如何将该字段读入为正常的数值型数据
Thousands:指定原始数据集中的千分位符

如果需要将year,month,和day三个字段解析为新的birthday字段,该如何做到
Parse_dates

数据集中含有中文,一般在读取含中文的文本文件时都会出现编码错误,该如何解决。
Encoding:如果文件中含有中文,有时需要指定字符编码

2.3.2 处理缺失值

处理缺失值一般有两种办法,一种是插值法,一种是删除法

插值法:用一个替代之弥补缺失值

删除法:忽略缺失值

通过位置索引iloc,我们可以将前面小节的数据集分成inputs和outputs,前者为前两列,后者为最后一列。对于inputs中的缺失值,我们用其同一列的均值来替换。

inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean())
print(inputs)
image-20211223161921825

可以看到,此刻缺失值已经被替换成了均值3.0了。

对于inputs中的类别值或离散值,我们将“NaN”视为一个类别。 由于“巷子类型”(“Alley”)列只接受两种类型的类别值“Pave”和“NaN”, pandas可以自动将此列转换为两列“Alley_Pave”和“Alley_nan”。 巷子类型为“Pave”的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。 缺少巷子类型的行会将“Alley_Pave”和“Alley_nan”分别设置为0和1。

inputs = pd.get_dummies(inputs, dummy_na=True)
print(inputs)
image-20211223162116955

2.3.3 转换成张量格式

对于深度学习来说,计算效率是非常重要的,小数据可能体现不出速度,但是在数据非常庞大的时候,速度尤为重要;所以把数值变成张量,进而利用线性代数的方法去求解,速度能变快好几倍。

import torch

X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
X, y

image-20211223162617930


复习

选取行和列
loc方法

import pandas as pd
import numpy as np
a = pd.DataFrame(np.arange(10).reshape(5,2),
                 index = ['a','b','c','d','e'],
                 columns = ['A','B'])
reindex_a = a.reindex(index = ['b','a','d','c','e','f'],fill_value = 100)
loc_b = reindex_a.loc['b']
print(loc_b)



iloc方法

import pandas as pd
import numpy as np
a = pd.DataFrame(np.arange(10).reshape(5,2),
                 index = ['a','b','c','d','e'],
                 columns = ['A','B'])
reindex_a = a.reindex(index = ['b','a','d','c','e','f'],fill_value = 100)
loc_b = reindex_a.iloc[[1,3],[0,1]]
print(loc_b)



ix方法

import pandas as pd
import numpy as np
a = pd.DataFrame(np.arange(10).reshape(5,2),
                 index = ['a','b','c','d','e'],
                 columns = ['A','B'])
reindex_a = a.reindex(index = ['b','a','d','c','e','f'],fill_value = 100)
loc_b = reindex_a.ix[[1,3],['A']]
print(loc_b)

2.3.4 小结

  • pandas软件包是Python中常用的数据分析工具,pandas可以与张量兼容。
  • 用pandas处理确实的数据时,我们可根据情况选择用插值法和删除法。

2.3.5 练习

创建包含更多行和列的原始数据集

  1. 删除缺失值最多的列
  2. 将预处理后的数据集转换为张量格式

!作业未做!后面记得做!


2.4 线性代数

概述

线性代数一定要看一定要看!如果后面学不会了就是这里出问题了。


2.4.1 标量

标量不过多解释,实际上就是相对于线性代数来说的一个名词,因为有了张量的概念,那么不是张量的数值就是标量了。

比如:tensor(1)和1,一个是张量一个是标量,嗯,就是这么简单。


加减乘除幂

import torch

x = torch.tensor(3.0)
y = torch.tensor(2.0)

x + y, x * y, x / y, x**y
结果:
(tensor(5.), tensor(6.), tensor(1.5000), tensor(9.))

2.4.2 向量

前面我们说过了,一条轴的张量(一维张量)就是向量,和我们高中理解的向量一模一样的,结合上一小节所说,我们可以理解为向量里面的元素全是标量

x  = torch.arange(4)
x
结果:
tensor([0, 1, 2, 3])

访问元素

和数组一样,我们可以用索引访问向量的元素。

x[3]
结果:
tensor(3)

长度和形状

如果想知道一条向量多长,我们可以用python内置函数len()去查看。

len(x)
结果:
4

如果要看一个向量是什么形状,我们可以用shape去查看,shape返回的数值告诉你这个张量是几行几列,如果这个张量是一维,那么返回的数值就是0行几列,也可以理解为是向量的长度。

x.shape
结果:
torch.Size([4])

维度

dimension这个词在不同的地方有不同的含义。

张量的维度:张量所具有的轴数

轴的维度:轴的长度


2.4.3 矩阵

由点到线,由线到面。正如我们由标量到向量,向量到矩阵。


创建矩阵

可以用reshape自己捏一个矩阵出来

A = torch.arange(20).reshape(5, 4)
A
结果:
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11],
        [12, 13, 14, 15],
        [16, 17, 18, 19]])

同样的,如果你想利用索引查找某个元素。

print(A[1][1])
结果:
tensor(5)

矩阵的转置

Tensor的矩阵转置利用.T即可实现,目前来看Tensor好像无法用.I求逆矩阵,后面再补全这里的笔记。

A.T
结果:
tensor([[ 0,  4,  8, 12, 16],
        [ 1,  5,  9, 13, 17],
        [ 2,  6, 10, 14, 18],
        [ 3,  7, 11, 15, 19]])

2.4.4 张量

往更高的维度走,即为张量。

故技重施,我们可以捏一个张量。

X = torch.arange(24).reshape(2, 3, 4)
X
结果:
tensor([[[ 0,  1,  2,  3],
         [ 4,  5,  6,  7],
         [ 8,  9, 10, 11]],

        [[12, 13, 14, 15],
         [16, 17, 18, 19],
         [20, 21, 22, 23]]])

2.4.5 张量的算法

这里我的理解是,最好把张量看成是多维的矩阵,那么我们就可以利用线性代数的相关知识来理解他了。


加减法

我们知道矩阵相加减是矩阵内部元素对应相加减。

A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone()  # 通过分配新内存,将A的一个副本分配给B
A, A + B
结果:
(tensor([[ 0.,  1.,  2.,  3.],
         [ 4.,  5.,  6.,  7.],
         [ 8.,  9., 10., 11.],
         [12., 13., 14., 15.],
         [16., 17., 18., 19.]]),
 tensor([[ 0.,  2.,  4.,  6.],
         [ 8., 10., 12., 14.],
         [16., 18., 20., 22.],
         [24., 26., 28., 30.],
         [32., 34., 36., 38.]]))

复习:矩阵的乘法

image-20211223170502978


哈达玛积

这个知识在线性代数没见过,本质就是和矩阵的乘法区分开,哈达玛积是两个矩阵对应元素相乘,在python中用*来表示。

A*B
结果:
tensor([[  0.,   1.,   4.,   9.],
        [ 16.,  25.,  36.,  49.],
        [ 64.,  81., 100., 121.],
        [144., 169., 196., 225.],
        [256., 289., 324., 361.]])

当然,我们根据矩阵的性质可知,矩阵乘以一个k,相当于矩阵每个元素都乘k。

对应到张量,将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘。

a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
结果:
(tensor([[[ 2,  3,  4,  5],
          [ 6,  7,  8,  9],
          [10, 11, 12, 13]],

         [[14, 15, 16, 17],
          [18, 19, 20, 21],
          [22, 23, 24, 25]]]),
 torch.Size([2, 3, 4]))

2.4.6 降维

降维这个名词在机器学习也曾经出现过。这里复习一下。

降维实际上就是为了加速算法,减少内存的消耗,常见的降维方法有:数据压缩,主成分分析,PCA降维。当然,这些内容属于机器学习,我们这里不讲,因为对我们的学习没有任何帮助。


求和

python的内置函数sum()允许我们对张量内的元素求和。当前我们可以发现,通过求和,本来是一维张量,变成了0维的张量。

x = torch.arange(4, dtype=torch.float32)
x, x.sum()
结果:
(tensor([0., 1., 2., 3.]), tensor(6.))

明显地,这实际上就是一种降维的方法,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量;一维可以变0维,那么说明二维也能变一维,我们可以指定沿着哪个轴来通过求和降低维度。

A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
结果:
(tensor([40., 45., 50., 55.]), torch.Size([4]))

对比矩阵A,我们可以发现如果沿着0轴(行)求和,那么实际上就是把该列所有的元素全部相加,加到该列的第0个元素上去。


同样的,如果指定axis = 1,那么将沿着列求和,那么实际上就是把每行的所有元素相加,加到该行的第0个元素上去。

A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
结果:
(tensor([ 6., 22., 38., 54., 70.]), torch.Size([5]))

axis = 0按照行,可以理解为把“行”给抹去只剩1行,也就是上下压扁。
axis = 1按照列,可以理解为把“列”给抹去只剩1列,也就是左右压扁。


当然,如果你又对行又对列求和,那么实际上就是对矩阵的所有元素求和。

A.sum(axis=[0, 1])  # SameasA.sum()
结果:
tensor(190.)

也许有人说不喜欢通过求和加到某条轴上这种方式去降维,那你可以选择通过求平均值然后把平均值写在某条轴上来降维。求平均值有两种方法,一种是调用python内置函数mean(),另外一种就是蛋疼用sum()/numel,这实际上也是一种求平均值的方法,但是,我相信你不会那么蠢选择后者是吧。


2.4.7 非降维求和


2.4.8 点积

这里说的点积,就是我们平常说的矩阵的乘法了,只是点积是针对于两个向量来说的,但是会矩阵的乘法就会点积了,具体的复习在哈达玛积那里已经说过了。

y = torch.ones(4, dtype = torch.float32)
x, y, torch.dot(x, y)
结果:
(tensor([0., 1., 2., 3.]), tensor([1., 1., 1., 1.]), tensor(6.))

2.4.9 矩阵向量积

没什么神秘的,你可以理解为是一个矩阵和一个只有一维的矩阵相乘。

A.shape, x.shape, torch.mv(A, x)
结果:
(torch.Size([5, 4]), torch.Size([4]), tensor([ 14.,  38.,  62.,  86., 110.]))

2.4.10 矩阵乘法

这里的不多说了,过于简单。

B = torch.ones(4, 3)
torch.mm(A, B)
结果:
tensor([[ 6.,  6.,  6.],
        [22., 22., 22.],
        [38., 38., 38.],
        [54., 54., 54.],
        [70., 70., 70.]])

2.4.11 范数

范数英文norm,他实际上刻画了一个向量的大小,对于一维的向量来说,它的范数指的是\(L_2范数\),你可以理解为求向量的模

u = torch.tensor([3.0, -4.0])
torch.norm(u)
结果:
tensor(5.)

如果是\(L_1\)范数,他表示为向量元素的绝对值之和。这么说,我们求\(L_1\)范数可以这么求:先求每个元素的绝对值,然后再求和。

torch.abs(u).sum()
结果:
tensor(7.)

2.4.12 小结

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

2.4.13 练习

  1. 证明一个矩阵A的转置的转置是A,即(A⊤)⊤=A
  2. 给出两个矩阵AB,证明“它们转置的和”等于“它们和的转置”,即A⊤+B⊤=(A+B)⊤。
  3. 给定任意方阵AA+A⊤总是对称的吗?为什么?
  4. 我们在本节中定义了形状(2,3,4)的张量Xlen(X)的输出结果是什么?
  5. 对于任意形状的张量X,len(X)是否总是对应于X特定轴的长度?这个轴是什么?
  6. 运行A/A.sum(axis=1),看看会发生什么。你能分析原因吗?
  7. 考虑一个具有形状(2,3,4)的张量,在轴0、1、2上的求和输出是什么形状?
  8. linalg.norm函数提供3个或更多轴的张量,并观察其输出。对于任意形状的张量这个函数计算得到什么?

2.5 微积分

这里需要先知道两个概念:

  • 优化(optimization):用模型拟合观测数据的过程;
  • 泛化(generalization):数学原理和实践者的智慧,能够指导我们生成出有效性超出用于训练的数据集本身的模型。

emm,好吧,可能说了这两个概念说了和没说一样,如果你看不懂,你可以继续看下去,后面就看得懂了。。。


2.5.1 导数和微分

导数,说简单不简单,说难不难,可能有些人自从不学高数后就再也没有碰过数学,导致对导数、微分这些概念已经忘得差不多了,所以,这里我们简单回顾下。

假设我们现在有一个函数f(a) = 3a,它是一条直线。

image-20211227170931280

如果a = 2,那么理所当然f(a) = 6,如果我对a做一个小小的增加,比如说增加0.001,小到我自己都看不出来,那么f(a)就会变为6.003,我们会发现,因变量受自变量3倍的影响,这就是导数。导数实际上就是函数的斜率。但是我们这里的函数是一条直线。在讨论曲线的导数时,我们一般是讨论某个点的导数。一个等价的导数表达式可以这样写\(f'(a) = \frac{d}{da}f(a)\)

不管你是否将𝑓(𝑎)放在上面或者放在右边都没有关系。

所以上面吧啦吧啦这么多,只是想告诉你两点:

  1. 导数就是斜率,而曲线函数的斜率,在不同的点是不同的。
  2. 如果想知道一个函数的导数,你可以参考下面的导数公式。

导数公式

  1. y=c(c为常数) y'=0
  2. \(y=x^n\) \(y'=nx^{n-1}\)
  3. \(y=a^x\) \(y'=a^xlna\)
  4. \(y=e^x\) \(y'=e^x\)
  5. y=logax \(y'=logae/x\)
  6. y=lnx \(y'=1/x\)
  7. y=sinx \(y'=cosx\)
  8. y=cosx \(y'=-sinx\)
  9. y=tanx \(y'=1/cos^2x\)
  10. y=cotx \(y'=-1/sin^2x\)

运算法则

减法法则:

\((f(x)-g(x))'=f'(x)-g'(x)\)

加法法则:

\((f(x)+g(x))'=f'(x)+g'(x)\)

乘法法则:\((f(x)g(x))'=f'(x)g(x)+f(x)g'(x)\)

除法法则:\((g(x)/f(x))'=(g'(x)f(x)-f'(x)g(x))/(f(x))^2\)


下面我们看一下用python如何表达导数



2.5.2 偏导数

如果只是对一个变量求导,形如\(y = 2x+1\),那么对x求导的话,实际上就是对一个变量求导,而在多元函数微分学中,我们常常会遇到多变量的情况,此时我们就要用到偏微分,也就是偏导数。

在深度学习中,函数通常依赖许多变量,所以偏导数也及其重要。


复习:偏导数

image-20211223205749746

求偏导的时候,把要求导的变量像平时那样求导,把不求导的变量看成常数即可。


2.5.3 梯度

我们把某一个多元函数对其所有变量的偏导数用向量框起来,这就是我们所说的关于该函数的梯度向量。

如果说一个函数时y = \(ax_1+bx_2+...\)

那么该函数的梯度即为:\(∇_xf(x)=[\frac{∂f(x)}{∂x1},\frac{∂f(x)}{∂x2},…,\frac{∂f(x)}{∂xn}]^T\)

其中\(∇_xf(x)\)通常在没有歧义时被\(∇f(x)\)取代

假设x为n维向量,在微分多元函数时经常使用以下规则:

  • 对于所有\(A∈R^{m×n}\),都有\(∇_xA_x=A^⊤\)
  • 对于所有\(A∈R^{n×m}\),都有\(∇_xx^⊤A=A\)
  • 对于所有\(A∈R^{n×m}\),都有\(∇_xx^⊤Ax=(A+A^⊤)x\)
  • \(∇_x∥x∥^2=∇_xx^⊤x=2x\)

2.5.4 链式法则

多元函数通常是复合的,所以我们可能没法应用上述任何规则来微分这些函数。所以,我们需要链式法则使我们能够微分复合函数。


复习:链式求导


2.5.5 小结

  • 微分和积分是微积分的两个分支,前者可以应用于深度学习中的优化问题。
  • 导数可以被解释为函数相对于其变量的瞬时变化率,它也是函数曲线的切线的斜率。
  • 梯度是一个向量,其分量是多变量函数相对于其所有变量的偏导数。
  • 链式法则使我们能够微分复合函数。

2.5.6 练习

  1. 绘制函数y=f(x)=x3−1xy=f(x)=x3−1x和其在x=1x=1处切线的图像。
  2. 求函数f(x)=3x21+5ex2f(x)=3x12+5ex2的梯度。
  3. 函数f(x)=∥x∥2f(x)=‖x‖2的梯度是什么?
  4. 你可以写出函数u=f(x,y,z)u=f(x,y,z),其中x=x(a,b)x=x(a,b),y=y(a,b)y=y(a,b),z=z(a,b)z=z(a,b)的链式法则吗?

2.6 自动微分

求导计算在高数里面看似很简单,实际上对于复杂的模型来说,如果要自己去求导可能把自己搞到吐血(笑)。

在深度学习框架中,我们都是由自动计算导数的,即自动微分,他可以帮我们加快求导这部分繁杂的操作。

虽然我们用了很多篇幅来讲这些东西,但是我们并不知道这些知识是用来干嘛的,实际上这些知识是用来解决反向传播计算的,反向传播这个名词后面会解释是什么意思,别急。


2.6.1 自动微分的例子

假设我们想对函数\(y=2x^Tx\)关于列向量x求导。 首先,我们创建变量x并为其分配一个初始值。

import torch

x = torch.arange(4.0)
x
结果:
tensor([0., 1., 2., 3.])

我们可以根据x.grad来求取y关于x的梯度,但是在此之前,我们需要一个地方来存储梯度。

x.requires_grad_(True)  # 等价于x=torch.arange(4.0,requires_grad=True)
x.grad  # 默认值是None

现在我们来计算y的值

y = 2 * torch.dot(x, x)
y
结果:
tensor(28., grad_fn=<MulBackward0>)

根据前面我们定义的x可知,x是一个长度为4的向量,计算x和x的点积乘以二就是y,由计算可知y实际上是一个标量。下面我们通过调用反向传播函数来自动计算y关于x每个分量的梯度,并打印这些梯度。

y.backward() #这个意思是对y进行反向传播
x.grad
结果:
tensor([ 0.,  4.,  8., 12.])

这个结果是我们所要的x关于y的梯度,那么如果要验证这个结果是否正确,由我们前面梯度所学的性质:\(∇_x∥x∥^2=∇_xx^⊤x=2x\) , 我们可以知道我们所求梯度应该是4x,下面我们验证这个梯度是否正确。

x.grad == 4*x
结果:
tensor([True, True, True, True])

现在让我们计算x的另一个函数

# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
结果:
tensor([1., 1., 1., 1.])


2.7 概率论

我们现在说的人工智能,并不是真正意义上的人工智能,我们是把根据以往所统计的经验,分析出后面可能发生某事的概率,进而来预测未来发生的事,所以,学好概率论十分重要。

现在引入一个例子,我们现在要做一个二分类相关的问题:根据照片区分猫和狗。这听起来可能很简单,但对于机器来说确实一个非常难得问题,因为,这和分辨率有关,一旦分辨率不清晰,机器可能识别错误。

image-20211225222606556

用概率来识别两张图片,概率只能告诉我们是猫的概率是1还是0或是其中间值,但概率不会告诉我们说概率P = "猫"或P = "狗".


2.7.1 基本概率论

引入

假设我们扔骰子,想知道能看到1点的几率有多大,按照常识,理应是\(\frac1 6\),然而在现实生活中,由于骰子的质量等各方面问题,概率并不会是我们上面说的,但是根据大数定律,随着投掷次数的增加,这个概率会显得越来越准确,这就是我们概率论中所说的用频率来充当概率。

好的,我们用代码来试试。

%matplotlib inline
import torch
from torch.distributions import multinomial
from d2l import torch as d2l

在统计学中,我们把概率分布中抽取样本的过程叫做抽样。小部分样本叫做分布,其可以看作是事件的概率分配。将概率分配给一些零散的分布我们叫做多项分布

为了抽取一个样本,也就是扔骰子,我们只需传入一个概率向量,这个向量包含六个值,也就是每个点数出现的可能性。输出的是另一个相同长度的向量:它在索引i处的值是采样结果i中出现的次数。

fair_probs = torch.ones([6]) / 6
multinomial.Multinomial(1, fair_probs).sample()
结果:
tensor([0., 0., 0., 1., 0., 0.])

上面的代码显然只从中抽出一个样本来查看结果,结果是点数4出现了1次,而在估计一个骰子的质量(也就是它公不公平)时,我们希望从同一个分布中生成多个样本。如果用python在分布中去遍历多个样本,显然作为顺序扫描的速度会慢很多。因此我们采用深度学习框架的函数同时抽取多个样本,得到我们想要的任意形状的独立样本数组。

multinomial.Multinomial(10, fair_probs).sample()

比如上面这行代码,他的意思就是从骰子投掷出来的许多结果中抽取10个来作为样本。

结果:
tensor([1., 4., 0., 3., 0., 2.])

这个结果表明了点数1出现了1次,点数2出现了4次,点数3...

有了上面的代码基础,我们可以把10改成1000,这样就能模拟1000次投掷,然后根据向量来查看1000次投掷后,每个数字被投中了多少次,如果投掷数接近无穷,按照理论,概率可以近似替代频率。

# 将结果存储为32位浮点数以进行除法
counts = multinomial.Multinomial(1000, fair_probs).sample()
counts / 1000  # 相对频率作为估计值
结果:
tensor([0.1640, 0.1580, 0.1590, 0.1960, 0.1600, 0.1630])

从结果上看,每个点数的概率都接近\(\frac16\),大约是0.167,所以这组数据还不错,验证了我们的猜想。


概念和基本定理

随机试验特点

  1. 在相同条件下可重复
  2. 结果不止一个
  3. 无法预测

事件:每种结果

样本空间(结果空间):所有基本事件的集合Ω,例如投掷一次硬币的样本空间是:{正,反};投掷一次骰子的样本空间是:{1,2,3,4,5,6}

样本点:样本空间元素(基本事件w)


概率的统计定义

概率的古典定义

概率的几何定义


大数定律

在讲大数定律之前,我们提一下小数定律。

小数定律举个例子就是,总有些人掌握了少部分的数据,而认为自己找到了规律;比如双色球,连续三期开某个数,就有人误以为下一期还是开那个数。事实上,如果数据少,随机现象可以看起来很不随机。甚至非常整齐,好像真的有规律一样。

所以所谓的小数定律就是:如果统计数据很少,那么事件就表现为各种极端情况,而这些情况都是偶然事件,跟他的期望值一点关系都没有。

那么什么是大数定律呢?大数定律说:如果统计数据足够大,那么事件出现的概率就能无限接近它的期望值。


随机变量

如果把概率论的随机变量定义直白地讲出来肯定很多人不理解,但是为了给有些能看明白的人看,这里先给出概念:

设随机试验的样本空间为\(Ω\),如果对\(Ω\)中每一个元素e,有一个实数x(e)与之对应,这样就得到一个定义在\(Ω\)上的实值单值函数x = x(e),我们称为随机变量。

我们学了那么多编程,我们知道int a = 10是代表整型变量a存放10,它代表10;同理,这个随机变量就是代表某一个随机的可能性。例如我投掷骰子出5点数的概率用随机变量来代表我可以写成P({X = 5})或P(X = 5)。当然还能更懒写成P(5)。这样的话,我们可以用P(X)表示为随机变量X上的分布:分布告诉我们X获得某一值的概率。当然你不想表示某一个确定的值得概率也不是不行,你可以写成P(1<X<3)这样的来表示事件{1<X<3}。

当然,随机变量有些是离散,有些是连续。这里后面再详说。


2.7.2 处理多个随机变量

在深度学习中,我们常常会考虑许多随机变量,这些随机变量可能是某件事和某件事,比如说得某个疾病和某个症状,这里的“某个”就是指概率,这里的“和”就是指概率间的联系。大到更复杂的例子,如果我们要处理图像,图像对应的某个特征就是某一个随机变量,那么拿上面分类猫和狗的二分类问题来说的话,我们实际上就是结合图片中的多个特征对应的随机变量来判断这个图片属于哪个动物。


联合概率

联合概率也是我们说的and事件,即A = a和B = b同时发生的概率是多少。


条件概率

条件概率的定义是:设AB两个事件,且P(B)>0,则称\(\frac {P(AB)}{P(B)}\)为事件B已发生的条件下事件A发生的条件概率记为P(A|B)。

公理

  1. 对于任一事件A,由P(A|B)>=0
  2. P(Ω|B) = 1

乘法定理

P(A|B) = \(\frac {P(AB)}{P(B)}\)


贝叶斯定理

贝叶斯定理也叫逆概公式。由于P(AB)可以以乘法定理为桥梁,拓展出\(P(AB) = P(B|A)P(A) 或者P(AB) = P(A|B)P(B)\) , 所以当以P(AB)为桥梁时,贝叶斯定理就产生了

\(P(A|B) = \frac {P(B|A)P(A)}{P(B)}\)


边际化

为了能进行事件概率求和,


独立性

我们常说的独立性说成大白话就是互不影响,比如连续抛出两次骰子,第二次抛出不会受第一次影响;还有投篮啊,打靶啊都是独立的;而守株待兔,就是依赖关系,因为第一次抓到了兔子,第二次抓到兔子的概率会因为农夫老是在同一个地点蹲守而导致变高(这个例子有点扯淡,如果看不懂就算了,笑。。。。)

也就是说,设有A、B是两个任意事件,如果P(AB) = P(A)P(B),则称事件A和B相互独立,简称A和B独立。


2.7.3 期望和方差

离散型期望

为了概括概率分布的关键特征,我们需要一些测量方法,也就是我们说的平均数。但是我们在概率论里面说的平均数通常指的是加权平均数,也就是数学期望,离散型随机变量的数学期望为:\(E[X]=\sum^\infty_{k = 1}x_kp_k.\)


连续型期望


方差

方差的定义为:设X是一个随机变量,若\(E[X-E(X)]^2\)存在,则称\(E[X-E(X)]^2\)为X的方差,记为D(X)。方差刻画了随机变量和期望值相差了多少,即所谓的偏置


标准差

实际上我本人的理解是:标准差才是刻画偏置的,因为平方差是标准差后平方。所以从定义上来看:标准差为\(\sqrt{D(X)}\),但是通过化简,可以从离散和连续的角度得到另外两条公式:

离散:\(DX = \sum_k(X_k - EX)^2P_k\)

连续:


2.7.4 小结

  • 我们可以从概率分布中采样。
  • 我们可以使用联合分布、条件分布、Bayes定理、边缘化和独立性假设来分析多个随机变量。
  • 期望和方差为概率分布的关键特征的概括提供了实用的度量形式。

2.7.5 练习

  1. 进行m=500m=500组实验,每组抽取n=10n=10个样本。改变mm和nn,观察和分析实验结果。
  2. 给定两个概率为P(A)P(A)和P(B)P(B)的事件,计算P(A∪B)P(A∪B)和P(A∩B)P(A∩B)的上限和下限。(提示:使用友元图来展示这些情况。)
  3. 假设我们有一系列随机变量,例如AA、BB和CC,其中BB只依赖于AA,而CC只依赖于BB,你能简化联合概率P(A,B,C)P(A,B,C)吗?(提示:这是一个马尔可夫链。)

2.8 机器学习

机器学习实际上是深度学习网络的先修课,当然,如果你是直接学习深度学习的话,那么一些机器学习的概念你也是需要知道的。


2.8.1 监督学习

监督学习是指从标注数据中学习预测模型的机器学习问题,标注数据表示输入输出的对应关系,预测模型对给定的输出产生相应的输出。监督学习的本质是学习输入到输出的映射的统计规律

其实监督学习实际上就是有模型可以对应,比如说有对应的标准给你去参照。

而与有监督学习相对于的就是无监督学习,无监督学习实际上就是没有模型给我们参照,而我们要从杂乱无章的数据中自己寻找某个规律来学习这些数据。


2.8.2 输入空间、特征空间和输出空间

输入空间和输出空间

在监督学习中,我们将输入与输出的所有可能取值所构成的集合分别称为输入空间和输出空间,输入输出空间可以是有限元素的集合,也可以是整个欧式空间(欧式空间通常指的是欧几里得空间,如果不知道概念可以不用管这句话)。输入空间和输出空间可以是同一个空间,也可以是不同的空间,但通常输入空间远远小于输出空间。


特征空间

每个具体的输入是一个实例(如果你学过数据库原理,那你就把这个词理解为元组),通常用特征向量表示。你想嘛,任意取一个样本,那么他有很多个特征,如果用x表示特征,那么所有的特征可以放进一个向量,这就是我们说的特征向量。当然我们做机器学习和深度学习的时候肯定不止用到一个样本,一个样本就一条特征向量,那许多的样本对应的就是一个特征空间了,也就是说,所有特征向量存在的空间就叫做特征空间。


2.8.3 回归和分类

输入变量X和输出变量Y有不同的类型,可以是连续的,也可以是离散的。人们根据输入输出的不同类型,对预测任务给予不同的名称:输入变量和输出变量均为连续变量的预测问题成为回归问题输出变量为有限个离散变量的预测问题称为分类问题,输入变量和输出变量均为变量序列的预测问题称为标注问题。


2.8.4 梯度下降

梯度下降是一个用来求函数最小值的算法,我们将使用梯度下降算法来求出代价函数J(θ1,θ2)的最小值。当然了,不同的深度学习主讲人,用的代价函数符号不同,李沐用的是L,吴恩达用的是J。

梯度下降背后的思想是:

  1. 开始时我们随机选择一个参数的组合(θ1,θ2,θ3……θn),计算代价函数,
  2. 然后我们寻找下一个能让代价函数值下降最多的参数组合。我们持续这么做直到到一个局部最小值,因为我们并没有尝试完所有的参数组合,所以不能确定我们得到的局部最小值是否便是全局最小值。
  3. 选择不同的初始参数组合,可能会找到不同的局部最小值。

想象一下我们正站在山的这一点上,也就是上文说的“开始时我们随机选择一个参数的组合(θ1,θ2,θ3……θn”,在梯度下降算法中,我们要做的就是旋转360度,看着我们的周围,并问自己要在某个方向上,用小碎步尽快下山。这些小碎步要朝什么方向?也就是上文说的“然后我们寻找下一个能让代价函数值下降最多的参数组合。我们持续这么做直到到一个局部最小值”,如果我们站在山坡上的这一点,然后看一下周围,我们会发现最佳的下山方向,我们再看看周围,然后再一次想想,我应该从什么方向迈着小碎步下山?然后我们按照自己的判断又迈出一步,重复上面的步骤,从这个新的点,我环顾四周,并确定从什么方向将会最快下山,然后又迈进了一小步,并以此类推,直到我接近局部最低点的位置。

image-20211227164927750

如果换一个初始点,那么得到的局部最低点的位置可能完全不一样。

image-20211227164959199

梯度下降算法的公式为:

\(\theta_j := \theta_j - a\frac{\part}{\part\theta_j}j(\theta_0,\theta_1)\)

其中\(a\)​是学习率,它决定了我们沿着能让代价函数下降程度最大的方向迈出步子的步伐距离,如果α的数值很大,那么我们会跨大步下山,如果α的数值很小,我们会迈着小碎步下山。在梯度下降中,我们每一次都同时让所有的参数减去学习速度乘以代价函数的导数。:=实际上等价于代码中的赋值操作,这个东西出自于python3.8中的海象运算符。

其中\(\part\)这个符号实际上是偏导符号,读作round。(好笑的是,吴恩达博士开玩笑说这个符号是花里胡哨符号,因为数学家想分辨求导和求偏导才开发这样的符号,但是这个符号对人工智能科学家不友好)求导用的是\(d\)这个符号。

在这里,如果α太小了,也就是我们的学习速率太小了,结果只能是像个瘸子一样一小步一小步移动,去努力努力接近最低点,这样就需要很多步才能到达最低点,所以如果α太小的话,可能会很慢,因为它会一点点挪动,它会需要很多步才能到达全局最低点。

如果α太大,那么梯度下降法可能会越过最低点,甚至可能无法收敛,下一次迭代又移动了一大步,越过一次,又越过一次,一次次越过最低点,直到你发现实际上离最低点越来越远,所以如果α过大,它会导致无法收敛,甚至发散。

image-20211227165907088

再回过来看θ这个参数,如果一开始我们就把θ1初始化在局部最优点,那么我们下一步梯度下降法会怎么样工作?由于已经处于局部最优点,导致他导数为0,也就是该点的最小值为0,也就是导致公式的后半部分皆为0,那么就算你学习率再大再小也不会影响参数的结果,因此,如果你的参数已经处于局部最低点,那么梯度下降法更新其实什么都没做,他不会改变参数的值。这也解释了为什么即使学习速率α保持不变时,梯度下降也可以收敛到局部最低点。


2.8.5 逻辑斯蒂回归

LR回归,虽然这个算法从名字上来看,是回归算法,但其实际上是一个分类算法,学术界也叫它logit regression, maximum-entropy classification (MaxEnt)或者是the log-linear classifier。在机器学习算法中,有几十种分类器,LR回归是其中最常用的一个。

LR回归是在线性回归模型的基础上,使用sigmoid函数,将线性模型\(w^Tx\)的结果压缩到[0,1]之间,使其拥有概率意义。 其本质仍然是一个线性模型,实现相对简单。在广告计算和推荐系统中使用频率极高,是CTR预估模型的基本算法。同时,LR模型也是深度学习的基本组成单元

逻辑斯蒂回归是统计学习中的经典分类方法。最大熵是概率模型学习的一个准则,将其推广到分类问题得到最大熵模型。逻辑斯蒂回归和最大熵模型都属于对数线性模型。

在分类问题中,你要预测的变量 是离散的值,我们将学习一种叫做逻辑回归 (Logistic Regression) 的算法,这是目前最流行使用最广泛的一种学习算法。

在分类问题中,我们尝试预测的是结果是否属于某一个类(例如正确或错误)。分类问题的例子有:判断一封电子邮件是否是垃圾邮件;判断一次金融交易是否是欺诈;之前我们也谈到了肿瘤分类问题的例子,区别一个肿瘤是恶性的还是良性的。


第三章 线性神经网络

3.1 线性回归

回归,英文名regression。在先修课机器学习中,我们经常能够遇见两个名词:回归分类。这两者的区别实际上就是:回归是根据以往的经验来预测未来的趋势或走向,比如说经典的房价预测问题;而分类是根据根据以往的经验来预测下一个东西是属于什么,经典的就是二分类问题;从宏观上来看,回归是相对于连续的,而分类是相对于离散的。


3.1.1 线性回归的基本元素

线性回归基于几个简单的假设:首先,假设自变量x和因变量y之间的关系是线性的,即y可以表示为x中元素的加权和,这里通常允许包含观测值的一些噪声;其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。

为了解释线性回归,我们举一个实际的例子,我们希望根据房屋的面积和房龄来估算房屋的价格。接下来这里涉及到许多机器学习的术语;为了开发一个能够预测房价的模型,我们需要收集一个真实的数据集,这个数据集包括了以往房屋的预售价格、面积和房龄,当然,如果你要收集更多的特征也不是不行,但是我们目前从最简单的开始讲起。

在机器学习的术语中,我们把这部分收集来用作开发预测房价模型的数据集叫做训练数据集(training data set)训练集(training set)。每行数据或者用数据库的术语来说叫元组在这里称为样本,也可以称为数据样本(training instance)或者数据点(data point)。我们把试图预测的目标(在这个例子中指的是房屋价格)称为标签(label)或者目标(target)。预测所依据的自变量(在这个例子中指的是面积和房龄)称为特征(feature)或者协变量(covariate)

通常,我们使用n来表示数据集中的样本数。对索引为i的样本,其输入表示为:\(x^{(i)} = [x_1^{(i)},x_2^{(i)}]^T\),其对应的标签是\(y^{(i)}\)


3.1.1.1 线性模型

线性假设是指目标可以表示为特征的加权和,也就是我们高中所熟悉的一次函数y = kx+b,只是在深度学习中,我们换成了\(y = w_1x_1 + w_2x_2 +....+b\)。其中\(w\)叫做权重(weight),b叫做截距(intercept),b在高中数学中叫截距比较多一点,但是在深度学习中它通常被叫做偏置(bias)。偏置是指当前所有特征都取值为0时,预测值应该为多少。虽然特征取值为0可能在我们说的预测房价的例子中并不存在,但是我们仍然需要偏置,因为如果没有偏置那我们的模型会受到限制。

严格来说,如果应用到房价预测的例子上的话,我们可以写出这样的式子:

\(price = w_{area}·area+w_{age}·age+b\)

这实际上是一个仿射变换(仿射变换,又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。)。

如果是单纯地一个特征就写一个x,那个式子就会变为:\(y = w_1x_1+w_2x_2+....w_nx_n+b\),这样的话实际上不利于我们计算,而且不简洁。根据我们线性代数学过的知识,我们知道可以用向量存放特征,即x = {x1,x2,x3...xn},当然,这仅仅是一个样本,如果是多样本的话,我们可以用矩阵来放。X的每一行是一个样本,每一列是一种特征

\[\begin{bmatrix} x_1^1 & x_2^1 & x_3^1 \\ x_1^2 & x_2^2 & x_3^2 \\ x_1^3 & x_2^3 & x_3^3 \end{bmatrix} \ \]

如同我们前面所说,下标表示第几个特征,上标表示第几个样本。

同样地我们也把权重w放进矩阵,那么模型简化为:\(\hat{y} = w^Tx + b\)

其中w之所以加转置是因为矩阵乘法就是\(A^TB\)。而w和x两个矩阵相乘后,由于b是标量,这个时候就会用到python的广播机制去相加。

在我们给定训练数据X和对应的已知标签y后,线性回归的目标就是找到一组权重向量w和编制b,找到后这个模型就确定下来了;当有新的x进来后,这个模型预测的y能够和真实的y尽可能的接近。

虽然我们相信给定x预测y的最佳模型会是线性的,但我们很难找到一个有n个样本的真实数据集,然后算出来的结果真的是线性的,这根线一点弯曲都没有,这是不可能的。因此,即使我们确信他们的潜在关系是线性,我们也需要加入一个噪声项来考虑误差所带来的影响。


3.1.1.2 损失函数

我们一般做机器学习的话,一般遵循三步准则:

  1. 找模型
  2. 找更好的函数
  3. 梯度下降

找模型在我们这里例子中已经确定是线性模型了,找更好的函数实际上就是寻找好的w和x,这两个参数确定了,y = wx+b这个函数也就随之确定了;而梯度下降就是用来寻找这个更好的w和b的,这个w和b是根据损失函数求得损失最小的那一刻,对应的w和b来确定最终选取。

大饼画完了,该细致来谈谈细节了。

在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量,这个度量实际上就是我前面提到的损失函数(loss function)。一般来说,我们最开始用的损失函数都是用平方损失函数,但是后面我们就不会用它了(笑。。。用它纯粹是为了先让你入门罢了)。平方误差的定义是:

\(l^{(i)}(w,b)=(\hat{y}^{(i)}−y^{(i)})^2\)

但是后面拿这个函数计算时,经常会对其求导,这样的话平方项拉下来会多一个2,所以为了计算简便,我们习惯把平方损失函数写成:

\(l^{(i)}(w,b)=\frac12(\hat{y}^{(i)}−y^{(i)})^2\)

常数\(\frac 12\)并不能改变啥,纯粹是为了简化计算罢了。

当然如果我们只是度量一个样本肯定不行,万一恰好这个样本点刚好离真实值远的离谱怎么办。所以我们需要计算多个样本的损失均值,假设我们训练集有n个样本,那么即:

\(L(w,b)=\frac1 n\sum_{i = 1}^n l^{(i)}(w,b) = \frac 1 n\sum^n_{i = 1} \frac 1 2(w^⊤x^{(i)}+b−y^{(i)})^2\)

也就是说,在训练模型时,我们希望寻找一组参数(w,b),这组参数能最小化在所有训练样本上的总损失:\(w*,b* = argmin_{w,b} L(w,b)\)


3.1.1.3 解析解

这个名字听起来挺玄乎,但是你是否记得我们高中的时候曾经学过一个最优解的方法?就是线性规划。如果你不记得了,那你要好好反思一下了。这里给出一道题帮你唤醒记忆。

做法:

  1. 列出约束条件及目标函数
  2. 画出约束条件所表示的可行域
  3. 在可行域内求目标函数的最优解及最优值

求出来的最优解实际上就是我们这里说的解析解。但是你也看到了,这个解在高中的时候我们就被告知不是什么方程都能求出最优解的,所以这种线性规划求最优解的方式只在某些场合适用。


3.1.1.4 随机梯度下降

即使我们无法得到最优解的情况下,我们仍然可以有效地训练模型。

我们一般来优化损失函数的方法是用梯度下降(gradient descent)的方法,这种方法几乎可以用来优化所有深度学习模型。他通过不断地在损失函数递减的方向上更新参数来降低误差。

梯度下降最简单的用法是计算损失函数关于模型参数的导数。但实际上执行可能会非常慢,因为每一次更新参数,我们基本遍历每个样本。一次,我们通常会在每次更新的时候抽取一小批样本,这种方法的变体我们叫做小批量随机梯度下降

好的,我相信我上面说了个寂寞,你可能听不太懂,我下面用一种理科生比较能接受的思维再说一次吧。

在数学公式中,我们经常用->或者:=来表示更新。我们把小批量随机梯度下降的更新过程用公式表示一下:

\((w,b)<-(w,b)-\frac{η}{|B|} \sum _{i∈B}∂_{(w,b)}l^{(i)}(w,b)\)

我们现在从所有的样本中抽取了\(B\)个样本,也就是小批量\(B\)。参数η我们叫做学习率。


线性回归比较简单,一般来说像我们上面线性规划提到的,他通常只会有一个最小值。但是实际上我们很少会去根据这个最小值去找对应的w和b,因为这样的话也许在训练集上的损失能达到最小,但是当训练出来的模型用于应用时,对于未见过的数据,可能损失就会比较大了;所以我们更难做到的是去找一组参数,使得我们的模型能够在没见过的数据上依然能够保持较低的损失,这种让模型能够适应未知数据的挑战我们叫做泛化


3.1.1.5 用模型进行预测

在模型(上面步骤提到的“更好的函数”)做出来后,我们可以通过输入房屋面积和房龄来估计新房屋的价格了。此时给定特征估计目标的过程通常叫做预测(prediction)或者推断(inference),在深度学习研究领域中推断的术语用的比较多,但是我们平时交谈的话最好用预测这个术语,否则你和统计学家交谈时统计学家会懵逼。


3.1.2 矢量化加速

这个名词听着也挺玄乎,但是实际上就是我们前面说的,把特征装进向量计算,因为如果你不用线性代数的知识去计算而采用for循环遍历计算,那么对于内存的开销和执行的时间开销无疑是巨大的。

矢量化在某一些书上也被叫做向量化。实际上在深度学习兴起之前,向量化是很棒的,因为它可以加速我们的运算,尽管有时候也会用不上。但是在深度学习时代向量化,摆脱for循环已经变得非常重要,因为我们越来越多地训练非常大的数据集,如果你摆烂:诶,我就是用for循环,我不干了。那你在训练的时候就要摆黑脸了,因为情况就会像百度网盘限速一样让你哭泣,因此我们很需要让我们的代码变得非常高效。


3.1.3 正态分布和平方损失

接下来,我们通过对噪声分布的假设来解读平方损失目标函数。

正态分布由于是数学家高斯发现的所以也叫高斯分布(Gaussian distribution)


3.1.4 从线性回归到深度网络

到目前未知,我们谈论了线性模型。接下来我们用前面的知识来开辟后面的知识。


3.1.4.1 神经网络图

如果我们把线性回归描述成神经网络。那么我们的输入层就是所有的特征,而输出层就是预测值。

在如图所示的神经网络中,输入为\(x_1,x_2...x_d\),因此输入层中的输入数(由于我们常常把输入的特征放入向量,实际上向量的长度就是维度,故我们把输入数也叫特征维度)为d。

由于我们通常计算时发生在输出层里,输入层只是负责传入数据,所以一般输入层不算入层数,这么说下来,我们可以得出结论:这是一个单层神经网络。

再啰嗦几句,上面的输入层把数据输到输出层,所以给人感觉就好像输出层一下子要处理很多的输入(笑。。。不知道你能不能get到那种感觉),所以这大概率为什么这种输入到输出的变化被叫做全连接层(fully_connected layer)或称为稠密层(dense layer)的原因了。


3.1.4.2 生物学

为了构建神经网络模型,我们需要首先思考大脑中的神经网络是怎样的?每一个神经元都可以被认为是一个处理单元/神经核(processing unit/Nucleus),它含有许多输入/树突(input/Dendrite),并且有一个输出/轴突(output/Axon)。神经网络是大量神经元相互链接并通过电脉冲来交流的一个网络。

image-20211226173823664

下面是一组神经元的示意图,神经元利用微弱的电流进行沟通。这些弱电流也称作动作电位,其实就是一些微弱的电流。所以如果神经元想要传递一个消息,它就会就通过它的轴突,发送一段微弱电流给其他神经元,这就是轴突。

这里是一条连接到输入神经,或者连接另一个神经元树突的神经,接下来这个神经元接收这条消息,做一些计算,它有可能会反过来将在轴突上的自己的消息传给其他神经元。这就是所有人类思考的模型:我们的神经元把自己的收到的消息进行计算,并向其他神经元传递消息。这也是我们的感觉和肌肉运转的原理。如果你想活动一块肌肉,就会触发一个神经元给你的肌肉发送脉冲,并引起你的肌肉收缩。如果一些感官:比如说眼睛想要给大脑传递一个消息,那么它就像这样发送电脉冲给大脑。

image-20211226173849182

神经网络模型建立在很多神经元之上,每一个神经元又是一个个学习模型。这些神经元(也叫激活单元,activation unit)采纳一些特征作为输出,并且根据本身的模型提供一个输出。下图是一个以逻辑回归模型作为自身学习模型的神经元示例,在神经网络中,参数又可被成为权重(weight)image-20211226173918842

我们设计出了类似于神经元的神经网络,效果如下:

image-20211226173944020

其中x1,x2,x3是输入单元,我们将原始数据输入给他们,a1,a2,a3是中间 单元,他们负责将数据进行处理,然后传递到下一层。最后是输出单元,他们负责计算h0(x)。

神经网络模型是许多逻辑单元按照不同层级组织起来的网络,每一层的输出变量都是下一层的输入变量。下图为一个3层的神经网络,第一层成为输入层(Input Layer),最后一层称为输出层(Output Layer),中间一层成为隐藏层(Hidden Layers)。我们为每一层都增加一个偏差单位(bias unit)。

详情我们后面会再继续叙述,但现在点到为止即可。


3.1.5 小结

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

3.1.6 练习

  1. 假设我们有一些数据x1,…,xn∈Rx1,…,xn∈R。我们的目标是找到一个常数bb,使得最小化∑i(xi−b)2∑i(xi−b)2。
    1. 找到最优值bb的解析解。
    2. 这个问题及其解与正态分布有什么关系?
  2. 推导出使用平方误差的线性回归优化问题的解析解。为了简化问题,可以忽略偏置bb(我们可以通过向XX添加所有值为1的一列来做到这一点)。
    1. 用矩阵和向量表示法写出优化问题(将所有数据视为单个矩阵,将所有目标值视为单个向量)。
    2. 计算损失对ww的梯度。
    3. 通过将梯度设为0、求解矩阵方程来找到解析解。
    4. 什么时候可能比使用随机梯度下降更好?这种方法何时会失效?
  3. 假定控制附加噪声ϵϵ的噪声模型是指数分布。也就是说,p(ϵ)=12exp(−|ϵ|)p(ϵ)=12exp⁡(−|ϵ|)
    1. 写出模型−logP(y∣X)−log⁡P(y∣X)下数据的负对数似然。
    2. 你能写出解析解吗?
    3. 提出一种随机梯度下降算法来解决这个问题。哪里可能出错?(提示:当我们不断更新参数时,在驻点附近会发生什么情况)你能解决这个问题吗?

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

在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。emm,其实这一部分你并不需要看懂,因为我们根本不需要在这种没有框架的帮助下从零开始搭建一个线性回归。而且你看不懂很可能对你后续的学习可能打击很大。

对了再说一个事,你如果真的愿意看下面的代码,那么你肯定会很奇怪有个d2l的包,这个包是李沐博士的团队做的一个包,包里包含了我们做数据分析常用的一些库,比如pandas,numpy,matplotlib。虽然不知道做这个包的意义是啥(笑,别骂了别骂了水平有限),但是看到李沐学习网站上有条评论关于这个包的解释是:不用写那么多行代码。


导包

%matplotlib inline
import random #随机数包
import torch
from d2l import torch as d2l #d2l是个工具包

3.2.1 生成数据集

为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集。我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。我们也不搞多少花里胡哨的特征,一开始先来二维的,这样就可以很容易地将其可视化,不然看不到效果。样本的话我们生成一个包含1000个样本的数据集即可,每个样本包含标准正态分布中采样的两个特征。

我们使用线性模型参数\(w = [2,-3.4]^T、 b = 4.2\)和噪声项ϵ生成数据集及其标签。其线性模型表示如下:$ y = Xw +b+ϵ$

ϵ参数也不要管太多,就认为是误差即可。这个误差为了太过单调,我们假设该参数符合正态分布。

这里为了简化问题,标准差设为0.01。

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)

其中features中的每一行都包含一个二维数据样本,labels中的每一行都包含一维标签值(一个标量)。

这里用到个API,为了防止有人看不懂我注明一下哈。

torch.normal(means, std, out=None)
means (Tensor) – 均值
std (Tensor) – 标准差
out (Tensor) – 可选的输出张量

接着输出一下特征和样本来看下效果。

print('features:', features[0],'\nlabel:', labels[0])

通过生成第二个特征features[:,1]和labels的散点图, 可以直观观察到两者之间的线性关系。

d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);

image-20211228182658853


3.2.2 读取数据集

回想一下,训练模型时要对数据集进行遍历,但是由于我们为了提高效率,所以我们每次都不是一个一个样本抽取的,而是一批一批取的,并使用他们来更新我们的模型。由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数,该函数能打乱数据集中的样本并以小批量方式获取数据。

在下面的代码中,我们定义一个data_iter函数,该函数接受批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签。

#输入:每次批量生成的样本数,特征向量,标签
def data_iter(batch_size, features, labels):
	#这里输入的特征由于有多个样本,所以实际上并不是向量,而是矩阵。而len一般对于规则的张量来说是求其最高维度的长度,如三维就求层数,二维就求行数,一维就求长度。
    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]

这里有个随机数API,解释一下:

random.shuffle(a):根据数组a的第一轴进行随机排列,改变数组x。

通常,我们利用GPU并行计算的优势,处理合理大小的“小批量”。也就是说,GPU你可以想象成海绵宝宝里面的章鱼哥,如果规定一条触手只能干一件活,那么在一条触手干一件活的时候,其他触手闲置,而多条触手干多件活(这些活都一样难),那么干一件活和干多件活的时间其实相差不大,只是章鱼哥可能由于触手不协调会稍微慢那么一点点(笑。。听不懂就算了哈,这个比喻有点难get到)。

我们现在调动一下小批量运算:读取第一个小批量数据样本并打印,一次批量读取的样本数为10。每个批量的特征维度显示批量大小和输入特征数。同样的,批量的标签形状与batch_size相等。

batch_size = 10

for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break
结果:
tensor([[ 0.6308,  1.2033],
        [-0.4742,  0.3343],
        [ 0.4508,  1.4873],
        [-0.8927, -1.5153],
        [-0.6711, -0.9142],
        [-0.5845,  0.9920],
        [ 0.0399, -1.4413],
        [ 0.5356,  0.5376],
        [-0.4455, -0.8835],
        [ 0.5320,  0.3199]]) 
 tensor([[ 1.3658],
        [ 2.1098],
        [ 0.0544],
        [ 7.5658],
        [ 5.9680],
        [-0.3368],
        [ 9.1913],
        [ 3.4516],
        [ 6.3184],
        [ 4.1755]])

第一个小批量能读出来,emm,没什么问题。如果继续调第二个、第三个,直至遍历整个数据集,但是实际上执行效率还是很低,还容易在实际问题上出现问题:因为你会发现,它数据集小批量小批量读进来了,但是前面我们用了打乱数据集随机抽取批量,这个随机实际上就导致了每个数据都可能被抽到,所以所有数据集一开始就得全部加载在内存中,时刻面临被抽取的“危险”。而如果用深度学习框架中实现的内置迭代器效率就要高得多,它可以处理存储在文件中的数据和数据流提供的数据。


3.2.3 初始化模型参数

在我们开始用小批量随机梯度下降优化我们的模型参数之前,肯定要有w和b呀。但是b可以只用一个,根据我们前面说的,特征有多少个w就要有多少个,所以w同样需要生成。在这里的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0。

w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

在初始化这些参数之后,我们的任务是通过随机梯度下降来更新这些参数。当然了,随机梯度下降嘛,就是每次更新都会对梯度求导,一次求导还行,但是在这里可不是求一次导就能解决问题的,所以为了解决这个问题,我们要用自动微分来计算梯度。


3.2.4 定义模型

我们必须定义我们要用的模型吧?我们这里用的是线性模型,所以我们这么定义:

def linreg(X, w, b):  #@save
    """线性回归模型"""
    return torch.matmul(X, w) + b

3.2.5 定义损失函数

定义好模型了,有初始化参数了,有数据集了,现在就差个损失函数了,定义损失函数后,用梯度下降进行优化参数即可,那现在先定义一个损失函数吧,我们这里用的是前面说的平方损失函数。

def squared_loss(y_hat, y):  #@save
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

注意这里为啥要把y的形状reshape成y_hat的样子,因为这样才能方便后续进行矩阵的计算。


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来更新模型参数。

概括一下,我们将会执行以下循环:

  1. 初始化参数

  2. 重复以下训练,直到完成

    • 计算梯度

    • 更新参数

在每个迭代周期中,我们使用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}')

结果:
epoch 1, loss 0.045103
epoch 2, loss 0.000178
epoch 3, loss 0.000053

因为使用的不是真实的数据集,而是使用我们自己合成的数据集,所有我们知道真正的参数是什么。因此,我们可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度。事实上,真实参数和通过训练学到的参数确实非常接近。

print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
结果:
w的估计误差: tensor([-0.0004, -0.0002], grad_fn=<SubBackward0>)
b的估计误差: tensor([3.1948e-05], grad_fn=<RsubBackward1>)

当然了,很前面说的一样,我们要做的并不是为了找到一个非常好的参数用来做出一个真实模型,而是为了做出一个可以预测未来的模型,所以我们并不关心恢复真正的参数,而且通常做完随机梯度下降后,求出来的参数一般效果已经够好了。


3.2.8 小结

好了好了,别骂了,知道你还是看不懂。。我画几个图解释一下吧。

image-20211229165321829

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

3.2.9 练习

  1. 如果我们将权重初始化为零,会发生什么。算法仍然有效吗?
  2. 假设你是乔治·西蒙·欧姆,试图为电压和电流的关系建立一个模型。你能使用自动微分来学习模型的参数吗?
  3. 您能基于普朗克定律使用光谱能量密度来确定物体的温度吗?
  4. 如果你想计算二阶导数可能会遇到什么问题?你会如何解决这些问题?
  5. 为什么在squared_loss函数中需要使用reshape函数?
  6. 尝试使用不同的学习率,观察损失函数值下降的快慢。
  7. 如果样本个数不能被批量大小整除,data_iter函数的行为会有什么变化?

3.3 线性回归的简洁实现

在过去的几年里,出于对深度学习的强烈兴趣,许多公司、学者和业余爱好者开发了各种成熟的开源框架,这些框架可以自动化基于梯度的学习算法中重复性的工作。

接下来我们用深度学习框架来实现线性回归模型。


3.3.1 生成数据集

与3.2类似,我们首先生成数据集。

import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l

true_w = torch.tensor([2, -3.4]) #这里的w使我们自己给定的
true_b = 4.2 #这里的b也是我们自己给定的
features, labels = d2l.synthetic_data(true_w, true_b, 1000)

3.3.2 读取数据集

我们可以调用框架中现有的API来读取数据。我们将features和labels作为API的参数传递,并通过数据迭代器指定batch_size。



3.4 softmax回归

前面我们已经介绍过线性回归。回归可以用于预测多少的问题。比如预测房屋被售出价格,或者棒球队可能获得的胜场数,又或者患者住院的天数。

而相对于回归,前面我们还提到过分类,分类和回归的区别我们已经介绍过了。分类实际上我们就是问:是不是?

这个电子邮件是不是垃圾邮件(垃圾邮件分类)

这个图片是猫还是狗(图像识别)

这个人说的是什么(语音识别)


3.4.1 分类问题

如果只是二分分类,这种分类我们大可只用0或者1来表示,那么我们只需要采用sigmoid函数来修正为0或1即可(如果不懂后面会重新讲sigmoid函数),那如果是多种可能呢?

假设每次输入是一个2×2的灰度图像。我们可以用一个标量表示每个像素值,每个图像对应四个特征\(x_1,x_2,x_3,x_4\),此外,假设每个图像属于类别"猫","鸡","狗"中的一个。

接下来,我们要选择如何表示标签,如同我们前面所说,我们肯定不可能说P = "猫",我们有两个明显地选择:最直接的想法是选择y∈{1,2,3},其中正数分别代表{"狗","猫","鸡"}。

但是统计学家很早之前就发明了一种表示分类数据的简单方法:独热编码。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。比如说应用到我们这个例子上的话,我们就可以用独热编码来表示我们的标签y,其中猫对应(1,0,0),鸡对应(0,1,0),狗对应(0,0,1)。


3.4.2 网络架构

为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数。emm,前面的术语可能听着挺懵逼,这样吧,如果说用前面的线性模型表示的话我们应该是下图:

但是很明显,我们要判断图片是属于什么类别,那么输出层肯定不止一个结点,所以用softmax回归的输出层的话表示就是下图了:

image-20211227152914400

这个图一下子就解释了前面一堆听不懂的话了。

为了简洁地表达模型,我们仍然使用线性代数符号,你总不能写一堆的输出把?例如\(o_1,o_2...o_n\)啥的。所以我们把o统一放在向量里,写成o = Wx+b。其中W也是向量,x也是向量,b是标量。


3.4.3 全连接层的参数开销

全连接层这个名词已经开始慢慢频繁出现了。至于开销问题,我觉得如果是入门级的话,现在说太多肯定也看不懂,所以干脆后面再说这个话题了。但是你要知道的是,如果频繁地使用全连接层的话,开销会很大,所以应该尽量减少参数的过多使用。


3.4.4 softmax运算

现在我们将优化参数以最大化观测数据的概率,为了的到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。是的,这句话好别扭,这句话说成听得懂的话就是:实际上我们就是在算这个图片是不是某一类的概率,那这个概率是要多大才能判定这个图片属于这个类?这个概率要达到什么标准?这个标准是我们自己定的,也就是所谓的阈值。

我们希望把我们通过运算算出来的\(\hat{y}_j\)可以直接看做是j的概率,但是这明显不可能,因为我们知道概率是处于0到1的,我们又没对这个模型做什么处理,他凭什么输出的值刚好就在0和1之间,是吧。

那么既然没有处理,那接下来就要让输出结果某种处理后,变成0-1之间,这个处理我们就叫做校准

那我们要用什么东西来校准呢?这就进入了我们这一小节的标题了:softmax函数。

softmax函数可以将未规范化的预测变换为非负并且总和为1,同时要求模型保持可导。当然了在一些其他的深度学习专家比如吴恩达,他们更喜欢用逻辑斯蒂回归来做这种工作,emm,这种东西看个人吧,你用着怎么舒服怎么来。

\(\hat{y} = softmax(o),其中\hat{y}_i = \frac{exp(o_j)}{\sum_kexp(o_k)}\)

这里,对于所有的j总有0<=\(\hat{y_j}\)<=1。因此,\(\hat{y}\)可以视为一个正确的概率发布。softmax运算不会改变未规范化的预测o之间的顺序,只会确定分配给每个类别的概率。

从上面的公式我们可以看出,这个公式明显不是线性函数,但是由于我们是先通过输入特征的仿射变换做出o,然后再把o出入上面的softmax函数中的,所以,softmax回归是一个线性模型。


3.4.5 小批量样本的矢量化

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

softmax回归的矢量计算表达式为:

\[O = XW+b\\ \hat{Y} = softmax(O) \]

相对于一次输出一个样本,小批量样本一次性用了多个样本,这样的话就会导致使用了线性代数矩阵乘法运算,而避免先循环做一个样本做完继续做下一个浪费时间。在X中,每一行代表一个数据样本,对于O的每一行,我们都先对所有项代入函数进行运算,然后通过求和对他们进行标准化。


3.4.6 损失函数

接下里,我们需要使用一个损失函数来度量预测的效果。我们将使用最大似然估计,这与在线性回归中的方法相同。


3.4.6.1 对数似然

这里听到这个名词瞬间呆住,是吧。如果你概率论学的很好当我没说,但是如果你学得不好,我建议你直接知道一件事,我们不会再用线性回归里面的那个平方损失函数了。

在统计学习中,我们常用的损失函数有以下几种:

  1. 0-1损失函数
  2. 平方损失函数
  3. 绝对损失函数
  4. 对数损失函数(也叫对数似然损失函数)

这里要讲的实际上就是对数损失函数。这个函数还有一个名字叫做交叉熵损失

这个损失函数的表达形式是:\(L(\hat{y},y) = -(ylog\hat{y}+(1-y)log(1-\hat{y}))\)

机器学习里面,对模型的训练都是对Loss function进行优化,在分类问题中,我们一般使用最大似然估计(Maximum likelihood estimation)来构造损失函数。对于输入的x,其对应的类标签为t,我们的目的是找到使p(t|x)最大的模型f(x),y=f(x)为模型的预测值。


!!!!!!这里要给计算过程 2021/12/29



3.4.6.2 softmax及其导数

3.4.6.3 交叉熵损失

3.4.7 信息论基础

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


3.4.7.1 熵

熵,老朋友了。大学如果没有学过信息学的第一次见到他应该是在高中化学和物理里面。熵是一个物理学概念。但是如果说到在大学,如果是学计算机专业的话,第一次见到熵可能就是在机器学习中的决策树了。

我们来看下面这三张图,是关于液态水的结构;由于冰中的粒子活动空间较小,其结构比较坚固,粒子大多数都固定不动,液态水的结构坚固程度次之,粒子有一定的活动空间,而水蒸气的结构则处于另外一个极端,一个粒子的去向有多种可能性,可经常移动。

说白了熵其实就是描述粒子的无序性,冰很有序很稳定,所以熵最小。

image-20211229172153119

当然,熵也可以用概率来解释

如果我们把球排成一条线,熵就是这些球移动的自由程度。第一个桶的结构较为固定,无论我们如何摆放这些球,他们总是处于同一状态,因此第一个桶中的球的熵最低,其实这并不是熵的概念,但是他却给了我们一种感觉,集合越稳固或越具有同类性,其熵越低,反之也是这样。

因此我们总结,第一个桶熵最低,第二个居中,第三个最高。

当我们把球作放回抽取,则每次抽取都是独立重复事件,也就是说,我们抽到在第一个桶抽到四个红球的概率是1,当我们在第二个桶抽三红一蓝的概率,这时候是大约百分之十,在第三个桶抽二红二蓝的概率大约是百分之六。如下图

image-20211229172407410

然而,这些结果可能会因为我们的两个问题而感到费解:第一,假如我们有一千个球,那么我们得到一千个球的积,这个积总位于零和一之间,有可能非常小,第二就是,稍微如果改变其中一个因素,可能就会对最后的积产生极大的影响,我们此时需要更可控的因素。还有什么比积更好呢?答案是和。因为积的对数等于各自对数之和。

基于信息理论与概率统计,我们采用底数为2或用底数为e(自然对数)的对数函数。

做个总结,我们有三种红篮球搭配方式.

image-20211229172640890

由于我们的真数小于1,所以其对数函数值为负数,所以我们取整个函数的负数,将他转为正数。

由此我们给出熵的定义:

在信息论与概率统计中,熵是表示随机变量不确定性的度量。设X是一个取有限个值离散型随机变量,其概率分布为:\(P(X = \vec{x_i}) = p_i,其中i = 1,2,...,n\)

那么随机变量X的熵为:

\[H(X) = -\sum^{n}_{i=1}p_ilogp_i \]


3.4.7.2 惊异

压缩和预测有什么关系呢?这里没必要听到压缩这个名词就心里被吓到了,就是我们平常说的那个压缩包的压缩。想象一下,我们有一个要压缩的数据流。如果我们很容易预测下一个数据,那么这个数据很容易压缩。为什么这么说?很容易预测,说明这个数据很有规律,很有规律,说明我们都知道下一个是什么了,那就没有必要“打包压缩了”。为了传递数据流的内容,我们没必要把知道的东西也传输来,所以,当数据易于预测,也就是易于压缩。


3.4.8 模型预测和评估

在训练softmax回归模型后,我们怎么判断我们的模型好不好呢?这个问题交给小学生,小学生都能回答,当然是看看你的模型能不能准确预测出想要的东西呗。如果预测与实际类别(标签)一致,则预测是正确的。我们在后面的学习中,会用精度(accuracy)这个名词来评估模型的性能。精度等于正确预测数和预测总数之间的比率。


3.4.9 小结

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

3.4.10 练习

  1. 我们可以更深入地探讨指数族与softmax之间的联系。
    1. 计算softmax交叉熵损失l(y,y)l(y,y)的二阶导数。
    2. 计算softmax(o)softmax(o)给出的分布方差,并与上面计算的二阶导数匹配。
  2. 假设我们有三个类发生的概率相等,即概率向量是(13,13,13)(13,13,13)。
    1. 如果我们尝试为它设计二进制代码,有什么问题?
    2. 你能设计一个更好的代码吗?提示:如果我们尝试编码两个独立的观察结果会发生什么?如果我们联合编码nn个观测值怎么办?

3.5 图像分类数据集

前面不是学了softmax回归吗,学了就要用了对吧。前面我们说过他是用来解决多分类问题的。所以为了解决多分类问题,我们用一些数据集来做图像分类。

MNIST数据集是图像分类中广泛使用的数据集之一,但是由于基准数据集过于简单,不太好体现效果,所以我们将使用类似但更复杂的Fashion-MNIST数据集。

首先我们先把必要的包导入到jupyter notebook中。

%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l

d2l.use_svg_display()

3.5.1 读取数据集

在讲解这一部分知识之前,我建议你看一下有关于图片的一些知识:

数字图像的基本概念
对于一幅的数字图像,我们看到的是 肉眼可见的一幅真正的图片,但是计算机看来,这副图像只是一堆亮度各异的点。一副尺寸为 M × N 的图像可以用一个 M × N 的矩阵来表示,矩阵元素的值表示这个位置上的像素的亮度,一般来说像素值越大表示该点越亮。

一般来说,灰度图用 2 维矩阵表示,彩色(多通道)图像用 3 维矩阵(M× N × 3)表示。

下面说说什么是通道数

通道数问题
描述一个像素点,如果是灰度,那么只需要一个数值来
描述它,就是单通道。

如果一个像素点,有RGB三种颜色来描述它,就是三通道。
而四通道图像,就是R、G、B加上一个A通道,表示透明度。一般叫做alpha通道,表示透明度的。
2通道图像不常见,通常在程序处理中会用到,如傅里叶变换,可能会用到,一个通道为实数,一个通道为虚数,主要是编程方便。

通过通道可以改变图像的色相和颜色,例如如果你保存红色通道,那么图像本身就只保留红色的元素和信息。
如果察看单个通道,发现每个通道都显示为一幅灰度图像(不能说是黑白图像)。某个通道的灰度图像中的明暗对应该通道色的明暗,从而表达出该色 光在整体图像上的分布情况。由于通道共有3个,所以也就有了3幅灰度图像。
通道中的纯白,代表了该色光在此处为最高亮度,亮度级别是255。
 通道中的纯黑,代表了该色光在此处完全不发光,亮度级别是0。
 介于纯黑纯白之间的灰度,代表了不同的发光程度,亮度级别介于1至254之间。
 灰度中越偏白的部分,表示色光亮度值越高,越偏黑的部分则表示亮度值越低。
现在可以明白为何通道用灰度表示了吧?因为通道中色光亮度从最低到最高的特性,正符合灰度模式那种从黑到白过渡的表示。正是因为灰度的这种特性,使得它在以后还被应用到其它地方。通道中的灰度,与颜色调板的灰度滑块是对应的

可以通过框架中的内置函数将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张图像。测试数据集不会用于训练,只用于评估模型性能。

我们查看一下我们上面说明的是否正确哈。

len(mnist_train), len(mnist_test)
结果:
(60000, 10000)

第四章 多层感知机

4.1 神经网络的表示

在本章中,我们将介绍最简单的神经网络,也就是多层感知机。它们由多层神经元组成,每一层和它的上一层相连,从中接收输入;同时每一层也与它的下一层相连,影响当前层的神经元。

实际上,多层感知机这个名词不是很有用,你只需要知道他是比较复杂的神经网络即可,记不记这个名词作用不大。

也许听我在这画大饼你并不能听懂什么,接下来让我们画个小饼(笑)。

我们首先关注一个例子,本例中的神经网络只包含一个隐藏层。

image-20211227173204553

这个图是一个神经网络的图片,让我们给这个图片的不同部分取些名字。

我们由输入特征\(x_1,x_2...x_n\),它们被竖直地堆叠起来,这叫做神经网络的输入层。但是前面我们也说过,这一层只提供输入特征,计算不在这一层,所以一般输入层是不算入神经网络的层数的。然后这里还有另外一层我们称之为隐藏层,等下后面我们再回来讲隐藏的含义;而在最后一层是输出层,他负责产生预测值。


4.1.1 隐藏层

在训练集中,当通过神经网络的时候,隐藏层计算的值我们是不知道的,也就是说我们看不见它们在训练集中具有的值。当然你要是真想知道里面的值是啥,你可以从输入层开始根据隐藏层对应的算法一步一步算进去。


4.1.1.1 线性模型可能会出错

虽然我们前面一直在用线性回归模型,但是并不代表我们能一直用下去。因为线性意味着自变量和因变量呈单调趋势。也就是说:特征变大模型输出就变大,特征变小输出就变小。要是能够一直用下去那就皆大欢喜了,毕竟线性它简单呀。


4.1.1.2 在网络中加入隐藏层

我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。 要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。 我们可以把前L−1层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。 下面,我们以图的方式描述了多层感知机。

image-20211227174900706

这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2。 注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。

当然,隐藏层的里面的隐藏单元不是越多越好,正如我们前面在3.4.3说的一样,具有全连接层的多层感知机的参数开销可能会高得让你自闭。所以平时选参数的时候,最好是选有用的,然后精简点,能一个参数搞定就不要搞两个参数。


4.1.1.3 从线性到非线性

为什么需要非线性激活函数?

从矩阵方面的知识我们可以知道,加入我们由n个样本的小批量,每个样本由d个特征,那么这个输入的张量就是\(X∈R^{n×d}\)。诶,你别和我说不知道,这是矩阵乘法的性质啊。n×1的向量和1×d的矩阵相乘为n×d。

对于具有h个隐藏单元的单隐藏层多层感知机,用\(H∈R^{n×h}\)表示隐藏层的输出,称为隐藏表示(hidden representations)。在数学或代码中,H也被叫做隐藏层变量(hidden-layer variable)隐藏变量(hidden variable)。因为隐藏层和输出层都是全连接的,所有我们由隐藏层权重\(W^{(1)}∈R^{d×h}\)和隐藏层偏置\(b^{(1)}∈R^{1×h}\)以及输出层权重\(W^{(2)}∈R^{h×q}\)和输出层偏置\(b^{(2)}∈R^{1×q}\)。也就是说,如果用表达式来表达计算单隐藏层多层感知机的输出O的话,我们是这么算的:

\[H = XW^{(1)}+b^{(1)}\\ O = HW^{(2)}+b^{(2)} \]

emm,也就是说,其实我们完全不需要添加这个隐藏层,有点多余了。为什么这么说呢,因为上面的式子可以合并成一步:

\[O = (XW^{(1)}+b^{(1)})W^{(2)}+b{(2)} \]

也就是说,不需要隐藏层,只需要一层输出层即可解决上面的问题。事实证明,如果你使用线性激活函数(也叫恒等激励函数)或者没有用一个激活函数,那么无论你的神经网络有多少层一直在做的都只是计算线性函数。

所以为了发挥多层的用处,我们需要添加一个额外的关键元素:也就是在隐藏层不仅计算好\(XW^{(1)}+b^{(1)}\),还要把它用非线性激活函数\(σ\)激活一下,经过非线性激活函数的激活后,线性会变成非线性,这里别急哈,我知道你不懂什么叫激活函数,下一小节就来说了。

\[H = σ(XW^{(1)}+b^{(1)})\\ O = HW^{(2)}+b^{(2)} \]

为了构建更通用的多层感知机,我们可以继续堆叠这样的隐藏层,从而产生更有表达能力的模型。


4.1.1.4 通用近似定理

多层感知机可以通过隐藏神经元,捕捉到输入之间复杂的相互作用,这些神经元依赖于每个输入的值。比如两个输入端一个1一个2,我就可以设计隐藏层来个1+2,然后再用3和隐藏层另外一个结点算出来的4再来个相乘计算得出输出端12。从这个简单的意思来看我们实际上可以很容易地设计隐藏节点来执行任意计算。例如,在一对输入上进行基本逻辑操作,多层感知机是通用近似器。

虽然啊,实际上要算一个值,你可以通过增加输入端和调整权重并且更改输出端结点包含的逻辑来算任意一个值,建立任意函数的模型。但是最好不要把神经网络按上述说的做成广度,而应该做成有深度的神经网络。至于为什么,后面说了你就懂了,总之就是,往深度神经网络来建立,而不是往广度神经网络来建立。


4.1.2 激活函数

激活函数,英文为Activation functions。使用一个神经网络时,需要决定使用那种激活函数用于隐藏层上,哪种用在输出节点上,这是我们前面讲过的,但是我们还不知道啥叫激活函数,所以这里就来讲了。


4.1.2.1 ReLU函数

这个函数可以说是在机器学习里面最受欢迎的函数了,它的中文叫做修正线性单元(Rectified linear unit,ReLU),因为它实现简单,同时效果良好。其公式的作用是:给定元素x,ReLU函数被定义为该元素和0哪个大取哪个值,即:

\(ReLU(x) = max(x,0)\)

通俗地说,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。 为了直观感受一下,我们可以画出函数的曲线图。 正如从图中所看到,激活函数是分段线性的。

image-20211227181941097

当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。 我们可以忽略这种情况,因为输入可能永远都不会是0。

使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。 这使得优化表现的更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题,这个问题稍后将详细介绍。

刚刚前面我们说了,如果是ReLU函数的话,基本上是一段掐,只有正元素才有活性,才可以通过该隐藏层,否则的话就会"失活"。但是我们不能保证,所有负元素都是无用的信息,因此,ReLU函数有许多变体,其中的一个变体就是参数化修正线性单元。该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过。

\(pReLU(x) = max(0,x)+amin(0,x)\)


4.1.2.2 sigmoid函数

对于一个定义域在R中的输入,sigmoid函数将输入变化为区间(0,1)上的输出,因此,sigmoid通常被称为挤压函数:它将任意范围的参数投射到区间(0,1)中的某个值。该函数表示为:

\(sigmoid(x) =\frac{1}{1+exp(-x)}\)

当人们的注意力逐渐转移到基于梯度的学习时,sigmoid函数时一个自然的选择,因为它是一个平滑的、可微的阈值单元近似。当我们要做二元分类时,sigmoid可以作为输出层的激活函数。但是吧,实际上下面要讲的tanh函数用起来效果比sigmoid函数效果更好。而在隐藏层中,ReLU函数效果会更好,所以sigmoid函数现在处于比较尴尬的位置了。

image-20211228124351239

sigmoid函数的导数为下面的公式:

\(\frac{d}{dx}sigmoid(x)=\frac{exp(−x)}{(1+exp(−x))^2} = sigmoid(x)(1−sigmoid(x))\)

其导数图像为:

image-20211228170713665


4.1.2.3 tanh函数

与sigmoid函数类似,tanh(双曲正切)函数也能将其输入压缩转换到区间(-1.1)上。tanh函数的公式如下:

\(tanh(x)=\frac{1−exp(−2x)}{1+exp(−2x)}.\)

image-20211228124627371

tanh函数的导数是:

\(\frac{d}{dx}tanh(x)=1−tanh^2(x)\)

其导数图像为:

image-20211228170653353

事实上,tanh函数时sigmoid的向下平移和伸缩后的结果,对它进行变形后,穿过了(0,0)点,并且值域介于+1和-1之间。


4.1.3 激活函数的导数

在神经网络中使用反向传播的时候,我们需要计算激活函数的斜率或者导数,针对上面说的三种激活函数,其中ReLU函数我就不求了,比较简单,我们来亲自求一下其余两个导数:

image-20211228181945920


4.1.4 小结

在讨论优化算法时,吴恩达博士曾经说过,在所有场合上,tanh函数都优于sigmoid函数,但是有一个例外,在二分类的问题中,对于输出层,因为y的值是0或1,所以想让\(\hat{y}\)的数值介于0和1之间,而不是在-1和+1之间。所以需要使用sigmoid激活函数。

在不同的神经网络层中,是可以使用不同的激活函数的,也许我在隐藏层1中用了ReLU,然后我在隐藏层2中又用了tanh,诶,这不是不行哦,的确可以。

sigmoid函数和tanh函数两者共同的缺点就是,在自变量特别大的情况下,导数的梯度或者函数的斜率会变得特别小,最后就会接近于0,不信你看他们的导数图像。那导数的梯度为0可不行,这会影响梯度下降的速度的。

所以综上所述,我们总结出了一些选择激活函数的经验法则:

如果输出是0、1值(二分类问题),则输入层选择sigmoid函数,然后其它的所有单元都选择ReLU函数。这是很多激活函数的默认选择,如果在隐藏层上不确定用哪个激活函数,那么通常会使用ReLU激活函数。

  • 多层感知机在输出层和输入层之间增加一个或多个全连接隐藏层,并通过激活函数转换隐藏层的输出。
  • 常用的激活函数包括ReLU函数、sigmoid函数和tanh函数。

4.1.5 练习

  1. 计算pReLU激活函数的导数。
  2. 证明一个仅使用ReLU(或pReLU)的多层感知机构造了一个连续的分段线性函数。
  3. 证明tanh(x)+1=2sigmoid(2x)tanh⁡(x)+1=2sigmoid⁡(2x)。
  4. 假设我们有一个非线性单元,将它一次应用于一个小批量的数据。你认为这会导致什么样的问题?

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



4.3 多层感知机的简洁实现



4.4 模型选择、欠拟合和过拟合

过拟合的问题

当假设空间含有不同复杂度的模型时,就要面临模型选择的问题。我们希望选择或学习一个合适的模型。如果在假设空间中存在真模型,那么所选择的模型应该逼近真模型。具体地,所选择的模型要与真模型的参数个数相同,所选择的模型的参数向量与真模型的参数向量相近。

如果一味追求提高对训练数据的预测能力,所选模型的复杂度则往往会比真模型更高。这种现象称为过拟合。过拟合是指学习时选择的模型所包含的参数过多,以至于出现这一模型对已知数据预测的很好,但对未知数据预测得很差的现象。可以说模型选择旨在避免过拟合并提高模型的预测能力。


过拟合和欠拟合

如下图,我们先来理解第一段话的意思。

现在我们有以下的数据集,我们要选择一个模型去拟合他的真模型,也就是M=0时图中画的曲线,那条曲线即为真模型。这样就可以理解这句话了。“当假设空间含有不同复杂度的模型时,就要面临模型选择的问题。我们希望选择或学习一个合适的模型。如果在假设空间中存在真模型,那么所选择的模型应该逼近真模型。

当我们M=1,选择的是一条直线,这种模型其实是罔顾事实的做法,我们完全不考虑拟合的效果,一上来就乱套模型,这样会导致拟合数据的效果贼差。这种在古老的文献中称为“欠拟合”现象。

当M=3时,我们选择的模型已经接近他数据所对应的真模型了,已经几乎拟合了,这时候的模型符合测试误差最小的学习目的了。这样我们就可以理解“具体地,所选择的模型要与真模型的参数个数相同,所选择的模型的参数向量与真模型的参数向量相近。”这句话的意思了。

当M=9时,这时候就是所谓的过拟合现象了,由于参数设置过多,导致这条曲线几乎穿过了我们已知的所有的数据点。的确,他对已知数据预测很好(穿过了嘛),但是他对未知数据却预测很差(说不定下一个点不在这条线上,这就导致前面预测很准,后面误差越来越大)。这也就是上面“过拟合是指学习时选择的模型所包含的参数过多,以至于出现这一模型对已知数据预测的很好,但对未知数据预测得很差的现象。”的意思。

image-20211227183615429

在M=9时,这个模型具有高方差,高方差是一个历史遗留的说法。如果我们拟合一个高阶多项式,那么这个假设函数可以拟合几乎一切的数据,这就会导致我们采用的模型太过庞大,变量太多的问题,那么当我们后面想用约束条件去控制它的时候会控制不住。

再者,在M=9时,我们的损失函数可能会接近0,或者甚至就等于0,这就会导致这个模型在现阶段样本看起来挺牛,但是换了一个新样本就不行了,也就是我们所说的泛化能力差。

解决过拟合的方法

简单说法:如果简单来说,想解决过拟合,实际上无非就是选择复杂度适当的模型,以达到使测试误差最小的学习目的。我们常用的模型选择方法:正则化和交叉验证。

1、过拟合无非是选取过多的变量,那我们可以采用减少选取变量的数量的做法,具体而言,我们可以人工检查变量清单。人工检查我们可以自己选择保留在众多变量中较为有用的变量,而舍弃无用的变量。我们也可以用模型选择算法,这种算法可以自动选择,哪些特征变量可以保留,哪些特征变量可以舍弃,这种做法可以有效减少过拟合的发生,但是这也有缺点,当你舍弃了特征变量的时候,实际上你也舍弃了获取关于问题的一些信息信息。

2、我们也可以选用正则化,我们在保留所有特征变量的要求下,减少量级,或者控制参数θ的范围大小,这种方法非常有效,当我们有很多特征变量时,其实里面的变量都或多或少的会对预测值y造成影响。这就是正则化的思想。


4.4.1 训练误差和泛化误差

训练误差

训练误差是指模型在训练数据集上计算得到的误差。


泛化误差

泛化误差是指模型应用在同样从原始样本的分布中抽取的无限多的数据样本时,模型误差的期望。


运用三个思维实验将有助于更好地说明这种情况。假设现在有两个大学生正在努力准备期末考试。其中一个学生拼命死记硬背往年的考题,背到滴水不漏,过目不忘;而另外一个学生它虽然会背一些往年的考题,但他更多地是去理解,以便为后来可能出现的新题做准备。前者就是我们说的过拟合,其训练误差已经几乎为零,但是泛化误差很大,而后者就是我们想要的,训练误差和泛化误差相匹配。


4.4.1.1 统计学习理论

以前我们都是直接在一个分布中(同一堆样本中)取出所有的样本作为训练集的,没有测试集来测试,会造成训练的效果实际上没有那么好。因此,如果我们假设训练集和测试集都是来自同一个分布,那么这个我们叫做独立同分布假设。也就是说,你假设彼此的样本是不会影响的。

可能你听不懂,比如说我投掷骰子是用某一颗骰子投出来的100种可能,那么这100种可能就是100个样本,他们或多或少受到骰子质量的影响,假设我现在要得不止100个样本,要1000个,那么换个骰子继续扔100次,以此类推。那么第一个骰子投的第1次。和第n个骰子投的第n次,可能就没什么关系;但是第一个骰子投的第1次和第一个骰子投的第2次可能受骰子质量影响。

但是结果我们刚刚的独立同分布假设,这意味着我们对数据进行采样的过程没有进行“记忆”。换句话说,抽取的第2个样本和第3个样本的相关性,并不比抽取的第2个样本和第200万个样本的相关性更强。

如果要称为一个优秀的机器学习科学家需要具备批判性思维。你应该已经从这个假设中找出漏洞,即容易找出假设失效的情况。如果我们根据从加州大学旧金山分校医学中心的患者数据训练死亡风险预测模型,可能应用于本地效果比较好,但是换了一个地方就不一样了。比如说用于马塞诸塞州中和医院的患者数据,这两个数据的分布可能不完全医院。此外,抽样过程可能与时间有关,从而违反独立性假设。

当然了,你别说违背了独立性假设了还硬要乱搞。你用训练衣服分类的模型来测试人脸,就滑天下之大稽了是吧。


4.4.1.2 模型复杂性

当我们有简单的模型和大量的数据时,我们期望泛化误差和训练误差相接近,这个我们前面提过的。当我们有更复杂的模型和更好的样本时,我们预计训练误差会下降,但泛化误差会增大


4.5 权重衰减

前一节我们描述了过拟合的问题,这一部分我们要来应对这个问题。通常应对过拟合采用的是正则化交叉验证。下面让我们来分别一一讲解。

在4.4中,我们说到了在多项式回归中,如果项数越多,就会找出控制要素过多,就会导致过拟合现象,所以一般来说只要达到泛化误差足够小的目的了,就足够了,无需添加过多的参数。

emm,也就是\(y = w_1x_1+w_2x_2+...+w_nx_n\)中的n要尽量小,懂吧。

但是我们会发现,如果不添加过多的参数,那有些参数就会被丢掉,这就导致了有些数据没用上,这是非常可惜的。所以为了解决这个问题,我们重新来审视我们的要求:我们要用上尽可能多的特征(原数据),还要保证不过拟合。所以在此基础上,我们可以在多项式的损失函数最后面添加一项工具来帮我们平衡参数。


4.5.1 范数与权重衰减

在第二章的2.4.11中我们讲过范数。

在训练参数化机器学习模型时,权重衰减(weight decay)是最广泛使用的正则化的技术之一,它通常也被叫做L2正则化。这项技术通过函数与零的距离来衡量函数的复杂度。

一种简单的方法是通过线性函数\(f(x) = w^Tx\)中的权重向量的某个范数来度量其复杂性,例如用L2范数来表示\(||w||^2\)。要保证权重向量比较小,最常用方法是将其范数作为惩罚项加到最小化损失的问题中。将原来的训练目标是去最小化训练标签上的预测损失,调整为最小化预测损失和惩罚项之和。现在,如果我们的权重向量增长的太大,我们的学习算法可能会更集中于最小化权重范数\(||w||^2\)。打个比方来说,我们的确是用了相当多的特征(因为觉得不用可惜),但是用了之后为了防止过拟合,我们会把装有权重w的向量进行衰减,使他“变短”。怎么衰减呢?就是用正则化罚项。

在线性回归中,我们的损失函数时这样的:

\(L(w,b)=\frac{1}{n}∑^n_i=\frac{1}{2}(w^⊤x^{(i)}+b−y^{(i)})^2\)

在上面的式子中,\(x^{(i)}\)是样本i的特征,\(y^{(i)}\)是样本i的标签,(w,b)是权重和偏置参数。为了惩罚权重向量的大小,我们必须以某种方式在损失函数中添加\(||w||^{2}\),但是以什么样的方式来添加呢?添加方式如下:

\[L(w,b)+\frac{λ}{2}||w||^{2} \]

对于λ = 0,我们恢复了原来的损失喊叔叔,对于λ>0,我们限制\(||w||^{2}\)的大小。当我们对它求导时,又可以用1/2来抵消。

至于为什么使用L2范数而不是L1范数。这个问题现在说了你也不懂,是的,除非你是机器学习大佬那当我没说。

加入了L2正则化回归的小批量随机梯度更新如下式:

\[w←(1−ηλ)w−\frac{η}{|B|}\sum_{i∈B}x^{(i)}(w^⊤x^{(i)}+b−y^{(i)}) \]

随机梯度每更新一次,w和b更新一次,加入正则化罚项后,w在更新的过程中还被平衡了。这就是为什么这种方法被叫做权重衰减。


4.5.2 高维线性回归

4.6 暂退法

在4.5权重衰减中,我们介绍了惩罚权重的L2范数来正则化统计模型的经典方法。在概率角度看,我们可以通过以下论证来证明这一技术的合理性。

4.6.1 重新审视过拟合

当面对更更多的特征而样本不足时,线性模型往往会过拟合。相反,当给出更多的样本而不是特征,通常线性模型不会过拟合。不幸的是,线性模型泛化的可靠性是由代价的。简单地说,线性模型没有考虑到特征之间的交互作用。对于每个特征,线性模型都必须指定正的或负的权重。

繁华小和灵活性之间的这种基本权衡被描述为偏差——方差权衡。线性模型有很高的偏差:它们只能表示一小类函数。然而,这些模型的方差很低:它们在不同的随机数据样本上可以得出了相似的结果。

如果把偏差——方差看成一个色谱,那么与之相反的一端的是深度神经网络。神经网络并不局限与单独查看每个特征,而是学习特征之间的交互。例如:神经网络可能推断“尼日利亚”和“西联汇款”一起出现在电子邮件中表示垃圾邮件,但单独出现则不表示垃圾邮件。

即使我们有比特征多得多的样本,深度神经网络也有可能过拟合。


4.6.2 扰动的稳健性

在探究泛化性之前,我们先来定义一个什么是一个“好”的预测模型。前面我们说过,我们希望训练误差和泛化误差之间折中,做到一个模型能够很好地拟合训练数据,也能够很好地预测未知数据。所以根据经典泛化理论 :为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。简单些以较小维度的形式展现。这也是为什么开头我们用了最简单的线性模型来讲解原理的缘故。而正则化也是一样,为了说明我们是要减少特征向量的大小,我们直接对特征向量大小对应的概念——范数来下手。

简单性的另一个角度是平滑性。即函数不应该对其输入的微小变化敏感(这个也叫鲁棒性)。这也是为什么我们第一章线性模型所需的数据中我们往里添加了噪声点。因为一个函数不应该因为某些微小的噪声就失效。

所以根据上面所说的原理,科学家提出了一个想法:在训练过程中,在进行后续层的计算(我们的计算比如第一层输入层是不计算的,主要的计算在第二层)之前,我们先对网络添加小部分噪声。因为当训练一个有多层的深层网络时,注入噪声只会在输入——输出映射上增强平滑性。

这个想法被称为暂退法也叫丢弃法。暂退法在前向传播的过程中,计算每一内部层的同时注入噪声,这已经称为训练神经网络的常用技术。这种方法之所以被称为暂退法,是因为我们从表面上看是在训练的过程中丢弃一些神经元。在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些结点置零。


4.6.3 实践中的暂退法

回想之前做的多层感知机。如果我们将暂退法应用到隐藏层,以p的概率将隐藏单元置为零时,结果可以看作是只包含原始神经元子集的网络。比如在下图中,删除了h2和h5,因此输出的计算不再依赖于h2和h5,并且它们各自的梯度在执行反向传播时也会消失。这样,输出层的计算不能过度依赖于h1...h5的任何一个元素。

image-20220102145259596

通常,我们在测试的时候不会用到暂退法。给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。但是研究人员就需要去用暂退法去估计神经网络预测的不确定性了;如果通过应用不同暂退法(一种暂退法包含抹去某些节点)得到的预测结果都是一致的,那么我们可以说网络发挥更稳定。


4.7 前向传播、反向传播和计算图

就我个人而言,我实际上认为在开始多层感知机之前这个知识点就应该写在最前面了,因为很多人如果没学好机器学习或者没学过的话,根本不知道前向传播和反向传播是什么。但是依照李沐博士的书的话,既然是放到这里来讲的话,那笔记我也放到这里吧(狗头保命)。

4.7.1 前向传播

前向传播英文是forward propagation 或 forward pass,它指的是:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。

4.7.2 前向传播计算图

为了解释前向传播,我们比起复杂的逻辑斯蒂回归或是其他模型,我们举一个比较简单的例子。

image-20220102182948332

实际上,前向传播就是算每一层的值。

4.7.3 反向传播

反向传播(backward propagation 或者 backpropagation)指的是计算神经网络参数梯度的方法。简而言之就是,该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。同理,反向传播计算图如图所示。

image-20220102183052989

反向传播实际上就是根据求导,来算出某一节点对于另外的某一节点所给的“回馈”。即导数。

4.7.4 训练神经网络

在训练神经网络时,前向传播和反向传播相互依赖。对于前向传播,我们沿着依赖的方向去遍历计算图并计算其路径上的所有变量。然后将这些用于反向传播,其中计算顺序与计算图的相反。


4.7.5 小结

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

4.7.6 练习

  1. 假设一些标量函数XX的输入XX是n×mn×m矩阵。ff相对于XX的梯度维数是多少?
  2. 向本节中描述的模型的隐藏层添加偏置项(不需要在正则化项中包含偏置项)。
    1. 画出相应的计算图。
    2. 推导正向和反向传播方程。
  3. 计算本节所描述的模型,用于训练和预测的内存占用。
  4. 假设你想计算二阶导数。计算图发生了什么?你预计计算需要多长时间?
  5. 假设计算图对于你的GPU来说太大了。
    1. 你能把它划分到多个GPU上吗?
    2. 与小批量训练相比,有哪些优点和缺点?

第五章 深度学习计算

除了庞大的数据集和强大的硬件,优秀的软件工具在深度学习的快速发展中发挥了不可或缺的作用。就像我们在写一些游戏框架的时候,我们如果用java写框架都是先搭抽象类,而设计神经网络架构时考虑的就是粗糙的块(block)。

之前我们已经介绍了一些基本的机器学习概念,并慢慢介绍了功能齐全的深度学习模型。在上一章中,我们从零开始实现了多层感知机的每个组件,然后展示如何应用高级API轻松地实现相同的模型。为了易于学习,我们调用了深度学习库,但是跳过了它们工作的细节。在本章中,我们将深入探索深度学习计算的关键组件,即模型构建、参数访问与初始化、设计自定义层和块、将模型读写到磁盘,以及利用GPU实现显著的加速。

5.1 层和块

之前首次介绍神经网络时,我们关注的是具有单一输出的线性模型。在这里,整个模型只有一个输出。

单个的神经网络具有一些特征:

  1. 接受一些输入
  2. 生成相应的标量输出
  3. 具有一组相关参数,更新这些参数可以优化某目标函数

然后,当考虑具有多个输出的网络时,我们利用矢量化算法来描述整层神经元。这样的话可以加速我们的神经网络计算,像单个神经元一样,其也具有以上的特征。在这些基础上,我们引入了多层感知机,我们仍然可以说该模型保留了上面说的以上特征。

对于多层感知机而言,整个模型及其组成层都是这种架构。整个模型接受原始输入(特征),生成输出(预测),并包含一些参数(所有组成层的参数集合)。同样,每个单独的层接受输出(由前一层提供),生成输出(到下一层的输入),并且具有一组可调参数,这些参数根据从下一层反向传播的信号进行更新。

事实证明,比单个层大单比模型小的组件更有价值。所以为了实现复杂的网络,我们引入了神经网络的这个概念。块可以描述单个层、由多个层组成的组件或整个模型本身。使用块进行抽象的好处就是可以将一些块组合成更大的组件,这一过程通常是递归的。通过定义代码来按需生成任意复杂度的块,我们可以通过简洁地代码实现复杂的神经网络。

image-20220101161200498

从编程的角度来看,块由表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函数,并且必须存储任何必须的参数注意,有些块不需要任何参数。最后,为了计算梯度,块必须具有反向传播函数。在定义我们自己的块时,由于自动微分提供了一些后端实现,我们只需要考虑前向传播函数和必须的参数。

在构造自定义块之前,我们先回顾一些多层感知机的代码,下面的代码可以生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层,然后是一个具有10个单元且不带激活函数的全连接输出层。

import torch
from torch import nn
from torch.nn import functional as F

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

X = torch.rand(2, 20)
net(X)
结果:
tensor([[-0.2152, -0.0364,  0.0181, -0.2068,  0.0984,  0.0724, -0.1967,  0.0351,
          0.2090,  0.0342],
        [-0.1807,  0.0948, -0.0694, -0.1129,  0.0847,  0.0484, -0.1540, -0.0715,
          0.0780,  0.0344]], grad_fn=<AddmmBackward>)

上面的代码设计到某些API,在这里给出来先。

Linear layers

class torch.nn.Linear(in_features, out_features, bias=True)
对输入数据做线性变换:y=Ax+b
参数:
in_features - 每个输入样本的大小
out_features - 每个输出样本的大小
bias - 若设置为False,这层不会学习偏置。默认值:True
形状:

输入: (N,in_features)
输出: (N,out_features)
变量:

weight -形状为(out_features x in_features)的模块中可学习的权值
bias -形状为(out_features)的模块中可学习的偏置

例子:

>>> m = nn.Linear(20, 30)
>>> input = autograd.Variable(torch.randn(128, 20))
>>> output = m(input)
>>> print(output.size())

在这个例子中,我们通过实例化nn.Sequential来构建我们的模型, 层的执行顺序是作为参数传递的。 简而言之,nn.Sequential定义了一种特殊的Module, 即在PyTorch中表示一个块的类, 它维护了一个由Module组成的有序列表。 注意,两个全连接层都是Linear类的实例, Linear类本身就是Module的子类。 另外,到目前为止,我们一直在通过net(X)调用我们的模型来获得模型的输出。 这实际上是net.__call__(X)的简写。 这个前向传播函数非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。


5.1.1 自定义块

想要直观地了解块时如何工作的,最简单的方法就是自己做一个。

块的功能

  1. 将输入数据作为其前向传播函数的参数。
  2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。
  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
  4. 存储和访问前向传播计算所需的参数。
  5. 根据需要初始化模型参数。

在下面的代码片段中,我们从零开始编写一个块。它包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10个单元的输出层。

class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Module的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__()
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))

我们首先看一下前向传播函数,它以x作为输入,计算带有激活函数的隐藏表示,并输出其未规范化的输出值。

接着我们实例化多层感知机的层,然后在每次调用前向传播函数时调用这些层。注意一些关键细节:


5.6 GPU

GPU的发展非常迅速,这也为深度学习提供了足够的算力。本节,我们将讨论如何利用这种计算性能进行研究。首先是如何使用单个CPU,然后是如何使用多个GPU和多个服务器。

在window10的系统下,你可以打开cmd黑窗口,然后在里面输入以下内容来查看显卡信息。

nvidia-smi
结果:
(base) C:\Users\13966>nvidia-smi
Sun Jan  2 15:54:57 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 462.59       Driver Version: 462.59       CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name            TCC/WDDM | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  GeForce MX150      WDDM  | 00000000:02:00.0 Off |                  N/A |
| N/A   56C    P0    N/A /  N/A |     64MiB /  2048MiB |      2%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

别骂了别骂了,我是学生!电脑显卡太差啦!如果你愿意赞助我学习不是不行哈。

在pytorch中,每个数组都有一个设备,我们通常将其称为上下文。默认情况下,所有变量和相关的计算都分配给CPU。有时上下文可能是GPU。当我们跨多个服务器部署作业时,事情会变得更加棘手。通常智能地将数组分配给上下文,我们可以最大限度地减少在设备之间传输数据的时间。例如:当在带有GPU的服务器上训练神经网络时,我们通常希望模型的参数在GPU上。

5.6.1 计算设备

我们可以指定用于存储和计算的设备,如CPU和GPU。默认情况下,张量是在内存中创建的,然后使用CPU计算它。

在PyTorch中,CPU和GPU可以用torch.device('cpu')torch.device('cuda')表示。 应该注意的是,cpu设备意味着所有物理CPU和内存, 这意味着PyTorch的计算将尝试使用所有CPU核心。 然而,gpu设备只代表一个卡和相应的显存。 如果有多个GPU,我们使用torch.device(f'cuda:{i}') 来表示第ii块GPU(ii从0开始)。 另外,cuda:0cuda是等价的。

import torch
from torch import nn

torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')
结果:
(device(type='cpu'), device(type='cuda'), device(type='cuda', index=1))

我们可以查询可用GPU的数量

torch.cuda.device_count()
结果:
0

有点尴尬,xdm。我这个破笔记本电脑没有GPU。


第六章 卷积神经网络

在前面的章节中,我们曾经在3.5的图像分类数据集中遇到过图像数据。在当时,我们数据集中的每个样本都由一个二维像素网格组成,每个像素可能是一个或者多个数值,取决于黑白还是彩色图像。到目前为止,我们处理这类架构丰富的数据的方式还不够有效。为啥这么说呢?记得我在图像分类数据集那一小节中,我给你补充了关于数字图像的事,那里面说到一个通道数的问题。

如果你只是一个二值图或是灰度图,那么他的通道数是1,也就是说,每一张图实际上都是一个矩阵(二维张量);但是遇见彩色的RGB三通道图你怎么处理呢?当我们仅仅通过将图像数据展成一维向量而忽略了每个图像的空间结构信息,将数据送入一个全连接的多层感知机中。因为这些网络特征元素的顺序是不变的,因此最优的结果是利用先验知识,即利用相近像素之间的相互关联性,从 图像数据中学习得到有效地模型。

本章介绍的卷积神经网络是一类强大的、为处理图像数据而设计的神经网络。基于卷积神经网络架构的模型在计算机视觉领域中已经占主导地位,当今几乎所有的图像识别、目标检测或语义分割相关的学术竞赛和商业应用都以这种方法为基础。

卷积神经网络需要的参数少于我们前面学的那种全连接架构的网络例如多层感知机那样的,而且卷积也很容易用GPU并行运算。因此卷积神经网络除了能够高效地采样从而获得精确的模型,还能够高效地计算。所以不管是语音识别还是自然语言处理还是图像识别,从事相关工作人员都喜欢用CNN去处理。

在本章的开始,我们将介绍构成所有卷积网络主干的基本元素。这包括卷积层本身、填充(padding)步幅(stride)的基本细节,还有用于在相邻区域集聚信息的汇聚层(pooling),在每一层中多通道(channel)的使用,以及有关现代卷积网络架构的仔细讨论。

6.1 从全连接到卷积

我们之前讨论的多层感知机适合用来处理表格之类的问题,行对应样本,列对应特征,对于表格来说,我们找出不同样本中每个特征之间的交互。因此,多层感知机可能是最好的选择。但是对于高维感知数据(就是说特征过多,参数过多的情况),这种缺少结构的网络可能会不实用(为什么?全连接,就代表你要计算所有的参数,如果算力不够,可能算个三四天都出不来结果。)

例如,在之前猫狗分类的例子中:假设我们由一个足够充分的照片数据集,数据集中是拥有标注的照片,每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。即使将隐藏层维度降低到1000,这个全连接层也将有一堆的参数。所以模型挺美,就是实现不仅人要有耐心,设备还要牛逼,可以把你气死罢了。

诶,有些人说,我不需要那么好的分辨率,我像素调低点就可以了。但是你要想,你用比较模糊的照片去训练,训练效果大打折扣不说,而且依旧远远看起来还是很庞大的,再说了,不说图片本身的问题,就说你要训练出这种可以分辨图片的模型,不是一张图片或者两张就能做到的,至少也要个五千以上吧?这么多样本,你神经网络还不改良,这不算到你自闭?balabala这么多,实际上就是想告诉你,卷积神经网络)(convolutional neural networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。


6.1.1 不变性

如果是你自己,抛开这个学科不讲,就你自己,你如果想在一张图片中找到某个物体,拿到一张图片,我们可以知道的绝对是:物体和所在位置无关。比如说猪会飞,飞机会下海,哥斯拉会出现。这些反人类操作的事情出现,你也不能觉得奇怪,因为图片又不一定是反映真实世界的,也可能反映二次元世界的嘛。要不然p图用来干嘛。

image-20220102163506663

嗯,如果给你这张图,要你在里面找一个人,你能找到吗?你要怎么找?我敢保证即使它cosplay了你也要找一阵子。所以,如果你想找到这个人,你可以把这种图片切割成n份,然后给每份的区域打个分(表示这个人可能在这部分被切割区域的可能性)。

所以巴拉这么多,我们总结:

  1. 平移不变性:不管检测对象出现在图片的哪个位置,神经网络的前面几层应该对相同的图像区域反应都一样,你不能说猪就不能上天了是吧。
  2. 局部性:神经网络的前面几层应该只探索输出图像的局部区域(也就是图片被分割的那一小块),而不是过度去在意图像中相隔较远区域的关系。也就是说,我们如果是想找到一个人,那么我们只需要在分割的区域中找到人就行,而不是直接在图像的整体找到人。

那下面就要进入大家最害怕的数学环节了哈。


6.1.2 限制多层感知机

首先,多层感知机的输入是二维图像X,其隐藏表示H在数学上是一个矩阵,在代码中表示为二维张量。其中X和H具有相同的形状。为了方便理解,我们可以认为,无论是输入还是隐藏表示都拥有空间结构。

下面我们可能会看到非常可怕的式子,但是你别被吓到,首先你要理解每个参数的意思,你就会发出一句:paper tiger!

使用\([X]_{i,j}\)\([H]_{i,j}\)分别表示输入图像和隐藏表示中位置(i,j)处的像素。为了使每个隐藏神经元都能接收到每个输入像素的信息,我们将参数从权重矩阵替换为四阶权重张量W。假设U包含偏置参数,我们可以将全连接层形式化表示为:

\([H]i,j=[U]_{i,j}+\sum_k\sum_l[W]_{i,j,k,l}[X]_{k,l}=[U]_{i,j}+∑_a∑_b[V]_{i,j,a,b}[X]_{i+a,j+b}\)

上面的公式看似挺可怕,实际上阐述了个什么意思呢?

把一张大图片分割成很多张同等大小的图片,每张图片上的某个位置叫做i和j,当我处理完一张图片的所有处于i和j位置的像素点后,如果想换一张小图片,那么我只需要在i和j的基础上加上一个a和b,这样就会跳到另外一张图片上,这张图片由于是和第一张图片等大的,就导致像素点i,j的位置变成了i+a,j+b。每个像素点对应着一个特征,一个特征对应一个w和一个b,对应输出的一个y。


6.1.2.1 平移不变性

现在引用上面说的两个特性的其中一个:平移不变性。这条公式如果直接理解起来可能很难受,但是如果你理解了卷积的含义的话在来理解就比较好理解了,这里我先给出公式:

\([H]_{i,j}=u+∑_a∑_b[V]_{a,b}[X]_{i+a,j+b}\)


6.1.2.2 局部性

饮用上述的第二个原则:局部性。如上所述,为了收集用来训练参数

简而言之,上述的公式的所有输出构成卷积层,而卷积神经网络时包含卷积层的一类特殊的神经网络。在深度学习研究社区中,V被称为卷积核或者滤波器,它仅仅是可学习一个层的权重。当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的;以前,多层感知机可能需要很多的参数来表示网络中的一层,而现在的卷积神经网络经过卷积后,参数大大减少

6.1.3 卷积

卷积

卷积是一个数学概念。首先我们要知道卷积卷了啥。我们这里提几个例子。

我们点一根蜡烛,蜡烛十分钟后熄灭。我们同时点燃十根蜡烛,蜡烛也是十分钟后熄灭。那我们现在做一件这样的事情:第0分钟点燃一根,第4分钟点燃4根,第五分钟点燃三根,第9分钟点燃两根。

image-20220105103530588

现在我们提出一个这样的问题,到第七分钟,蜡烛的总质量还有多少,我只需要在七分钟的时候拍照看一下即可;并且,这种对应关系是由我们点燃的方法和蜡烛本身来决定的,这就是卷积。这是不是猝不及防?且听我细细道来。

我们在前面说到的例子中,如果是第七分钟拍张照观察,我们会发现第七分钟时,第一根蜡烛烧了7分钟,第四分钟点燃的四根烧了3分钟,第五分钟点燃的三根烧了2分钟,而由于第九分钟点燃的两根未被点燃,我们可以理解为点燃了-2分钟。

那么从中总结规律的话,我们可以发现,t分点燃的蜡烛,实际上少了7-t分钟。

image-20220105104405389

我们把要做的事情的图像叫做f,把使用的蜡烛的图像叫做g。如果对应到图像上的话,t分点燃的蜡烛数量,实际上就是右边的那张图,也就是由f(t)根;每根蜡烛的剩余质量,也就是我们的左图,也就是g(t)。如果到了第n分钟,这f(t)根蜡烛剩下的总质量是多少呢?实际上就是f(t)g(n-t)。现在我们说的只是某一时刻点燃的某些蜡烛,桌子上还有其他时刻点燃的某些蜡烛呢。所以这个式子应该写成\(\sum_t f(t)g(n-t)\)。如果右图不是离散的是连续的,我们还可以写成\(\int^{\infty}_{-\infty}f(t)g(n-t)dt\).这就是卷积公式。

是不是感觉缺了点啥,卷积的“卷”,是卷在哪了?


卡农与卷积

卡农这首曲子是根据把一段同样的旋律,然后复制,然后把另外的旋律延迟,升降调,叠加。如图所示。

image-20220105110507075

而这正是卷积的核心:延迟,倍率,叠加。


总结

从上面两个例子我们可以看到,不管是卡农还是蜡烛,都说明了一件事:卷积实际上就是卷集了过去、现在和未来对最终结果的影响。


回到卷积

也就是说,卷积实际上是测量f和g之间(把其中一个函数“翻转”并移位x时)的重叠。当我们是离散的时候,就是求和;当我们是连续的时候,就是积分。这实际上和概率学里面的连续型变量和离散型变量有些类似。


6.1.4 游戏回顾

当我们现在回到这个游戏。卷积层根据滤波器V选取给定大小的窗口,并加权处理图片。我们的目标实际上是学习一个模型,一遍探测要找的人最可能出现的地方。

image-20220102163506663


6.1.4.1 通道

然而这种方法有一个问题,我们在上面一直在意的是像素点的位置,而忽略了如果是彩色的图片的话,我们要考虑的还有通道数。所以按照前面补充的知识,我们可以知道,图片不是一个二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如包含1024×1024×3个像素。前两个轴和像素的空间位置有关,而第三个轴可以看作是每个像素的多维表示。因此,我们将X索引调整为[X]i,j,k。对应的卷积调整为\([V]_{a,b,c}\),而不是\([V]_{a,b}\)

由于输入图像是三维的,所以根据矩阵计算的原理,我们可以知道我们的隐藏表示H也是一个三维张量。换句话出,对于每一个空间位置,我们需要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一个互相堆叠的二维网格。

image-20220105112637022

由上述可知,我们可以把隐藏表示想象成一系列具有二维张量的通道。这些通道有时也被称为特征映射。直观上你可以想象在靠近输入的底层,一些通道专门识别边


6.1.5 小结

  • 图像的平移不变性使我们以相同的方式处理局部图像,而不在乎它的位置。
  • 局部性意味着计算相应的隐藏表示只需一小部分局部图像像素。
  • 在图像处理中,卷积层通常比全连接层需要更少的参数,但依旧获得高效用的模型。
  • 卷积神经网络(CNN)是一类特殊的神经网络,它可以包含多个卷积层。
  • 多个输入和输出通道使模型在每个空间位置可以获取图像的多方面特征。

6.1.6 练习

  1. 假设卷积层 (6.1.3)覆盖的局部区域Δ=0。在这种情况下,证明卷积内核为每组通道独立地实现一个全连接层。
  2. 为什么平移不变性可能也不是好主意呢?
  3. 当从图像边界像素获取隐藏表示时,我们需要思考哪些问题?
  4. 描述一个类似的音频卷积层的架构。
  5. 卷积层也适合于文本数据吗?为什么?
  6. 证明在 (6.1.6)中,f∗g=g∗f。

6.2 图像卷积

CNN一般用于处理图像,这里我们用图像来举例。

6.2.1 互相关运算

严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算,而不是卷积运算,在卷积层中,输入张量卷积核张量通过互相关运算产生输出张量

首先我们暂且忽略通道这一要素,看看如何处理二维图像数据和隐藏表示。假如现在输入是高度为3,、宽度为3的二维张量。卷积核高度和宽度都是2,而卷积核窗口(或者我们暂且认为它是卷积层)的形状由内核的高度和宽度决定。

image-20220105130429041

上图说明了,卷积层上的19是根据卷积核去扫描输入图像然后做计算得出的结果。

根据上面的原理,我们可以知道,输入大小等于输出\(n_h×n_w\)减去卷积核大小\(k_h×k_w\),(注:h为height长度,w为weight宽度)即:

\((n_h-k_h+1)×(n_w-k_w+1)\)

这是因为我们需要足够的空间在图像上“移动”卷积核。稍后,我们将看到如何通过在图像边界周围填充零(零填充)来保证有足够的空间移动内核,从而保持输出大小不变。


6.2.2 卷积层

卷积层对于输入和卷积核进行互相关运算,并在添加标量偏置后产生输出。所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。就像我们之前随机初始化全连接一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。

高度和宽度分别为h和w的卷积核可以被称为h×w卷积或h×w卷积核。 我们也将带有h×w卷积核的卷积层称为h×w卷积层。


6.2.3 图像中目标的边缘检测

如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。

首先在没有图像的情况下,我们先来构造一个6×8像素的黑白图像。

X = torch.ones((6, 8))
X[:, 2:6] = 0
X
结果:
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.]])

接下来,我们构造一个高度为1、宽度为2的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出非0。

K = torch.tensor([[1.0, -1.0]])

现在,我们对参数X(输入)和K(卷积核)执行互相关运算。 如下所示,输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为0。

Y = corr2d(X, K)
Y
结果:
tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])

现在我们将输入的二维图像转置,再进行如上的互相关运算。 其输出如下,之前检测到的垂直边缘消失了。 不出所料,这个卷积核K只可以检测垂直边缘,无法检测水平边缘。

corr2d(X.t(), K)
结果:
tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])

6.2.4 学习卷积核

6.2.6 特征映射和感受野

感受野

在卷积神经网络中,感受野(Receptive Field)的定义是卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小。再通俗点的解释是,特征图上的一个点对应输入图上的区域,如图1所示。

image-20220105125509616

6.2.7. 小结

  • 二维卷积层的核心计算是二维互相关运算。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。
  • 我们可以设计一个卷积核来检测图像的边缘。
  • 我们可以从数据中学习卷积核的参数。
  • 学习卷积核时,无论用严格卷积运算或互相关运算,卷积层的输出不会受太大影响。
  • 当需要检测输入特征中更广区域时,我们可以构建一个更深的卷积网络。

6.2.8. 练习

  1. 构建一个具有对角线边缘的图像X
    1. 如果将本节中举例的卷积核K应用于X,会发生什么情况?
    2. 如果转置X会发生什么?
    3. 如果转置K会发生什么?
  2. 在我们创建的Conv2D自动求导时,有什么错误消息?
  3. 如何通过改变输入张量和卷积核张量,将互相关运算表示为矩阵乘法?
  4. 手工设计一些卷积核:
    1. 二阶导数的核形式是什么?
    2. 积分的核形式是什么?
    3. 得到dd次导数的最小核大小是多少?

6.3 填充和步幅

在前面的例子中,输入的高度和宽度都为3,卷积核的高度和宽度都为2,生成的输出维数都是2×2。

那还有什么因素会影响输出的大小呢。答案是填充和步幅。有时,在应用了连续的卷积时,我们最终得到的输出远远小于输入大小(因为卷积核4个元素扫输入输出的是1个元素)。所以导致越卷积输出越来越小。所以一个图像如果越卷积越小,那么很多有用的原图特征信息就会丢失。而填充就是解决此问题的有效方法而如果我们希望大幅降低图像的宽度和高度,那么我们可以用到步幅


6.3.1 填充

现在给出一幅图,告诉你会什么会丢失边缘像素。

image-20220105175842567

假设图像的大小为nxn,过滤器是fxf,那么卷积运算的结果为(n-f+1)x(n-f+1)

  • 卷积运算后图像会缩小,经过若干次卷积运d算图像和图像的特征可能会缩小
  • 卷积运算中覆盖边缘和角落的像素点比中间像素点少,导致丢失图像边缘信息

为了解决这一问题,引入填充Padding

填充在一般来说都是在边缘填充0,我们也叫零填充。当然,也可以不是0。在卷积神经网络中,卷积核的高度和宽度通常为奇数,例如1、3、5、7。选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。

当卷积内核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。

6.3.2 步幅

在计算互相关时,卷积窗口从输出张量的左上角开始,向下和向右滑动。在前面的例子中,我们默认每次滑动卷积核都是滑动一步。但是有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。

我们将每次滑动元素的数量称为步幅(stride)。到目前为止,我们只是用过高度或宽度为1的步幅,那么如何使用较大的步幅呢?

image-20220105180851555

可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。

6.3.3. 小结

  • 填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
  • 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的1/n1/n(nn是一个大于11的整数)。
  • 填充和步幅可用于有效地调整数据的维度。

6.3.4. 练习

  1. 对于本节中的最后一个示例,计算其输出形状,以查看它是否与实验结果一致。

  2. 在本节中的实验中,试一试其他填充和步幅组合。

  3. 对于音频信号,步幅2说明什么?

  4. 步幅大于1的计算优势是什么?

    解:步幅大于1的优势在于可以快速的降低输出的维数


6.4 多输入多输出通道

虽然我们在前面说到了构成每个图像的多个通道和多层卷积层,但是还没细致的讲过,现在我们来讲讲。

彩色图像具有标准的RGB通道来指示红、绿和蓝。当我们添加通道时,我们的输入和隐藏的表示都表示成了三维张量。例如,每个RGB输入图像具有3×h×w的形状。我们将大小为3的轴称为通道维度。

6.4.1 多输入通道


6.5 汇聚层

通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野输入就增大。

上面这句话可能有点突兀。还是拿感受野里说过的例子来解释:

img

在这张图片里,用了两次卷积核扫描。

两层3×3的卷积核卷积操作之后的感受野是5×5,其中卷积核(filter)的步长(stride)为1、padding为0。这就意味着越往后的神经元,其感受野越大。

而我们的机器学习任务通常会跟全局图像的问题有关,例如图像中是否包含一样东西呢?所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。

此外,当检测较底层的特征时,我们通常希望这些特征保持某种程度上的平移不变性。

emm。什么意思呢,到底要做什么呢?我来举一个例子把。

image-20220105221745542

看到上面这幅图了吗?如果问两张图里面有没有叉,你肯定会说有,可是机器就懵了呀,转了一下角度他就方便不出来了。可是,这两张图还是有相似的地方,标出来的位置明显相同!所以我们可以给这张图片的局部来打分,如果加起来的分数超过了自己设定的图片阈值,即确定该图片是叉。那如何提取这张图的局部信息呢?这就需要用到汇聚层(pooling),也就是我们说的池化层,它的中作用是为了降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

6.5.1 最大汇聚层和平均汇聚层

与卷积层类似,汇聚层运算符有一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。然而,不同于卷积层中的输入和卷积核之间的互相关计算,汇聚层不包含参数,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层平均汇聚层

在这两种情况下,与互相关运算符一样,汇聚窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。

image-20220105222823891

举个例子,用2×2的汇聚窗口去扫描,如果窗口内的值是

max(0,1,3,4)=4,

max(1,2,4,5)=5,

max(3,4,6,7)=7,

max(4,5,7,8)=8。

汇聚窗口形状为p×q的汇聚层称为p×q汇聚层,汇聚操作称为p×q汇聚

致谢

大白话讲解卷积神经网络工作原理

https://www.bilibili.com/video/BV1sb411P7pQ/?spm_id_from=333.788.recommend_more_video.-1

【卷积神经网络可视化】 从卷积层到池化层的可视化演示(中英双语字幕)

https://www.bilibili.com/video/BV1nU4y187sX?from=search&seid=3594538571584519906&spm_id_from=333.337.0.0

posted on 2021-12-29 23:28  尘鱼好美  阅读(451)  评论(0编辑  收藏  举报