神经网络是模仿人类大脑结构所构建的算法,在人脑里,我们有轴突连接神经元,在算法中,我们用圆表示神经元,用线表示神经元之间的连接,数据从神经网络的左侧输入,让神经元处理之后,从右侧输出结果。
下图是一个最简单的神经元的结构。从这里开始,我们正式开始认识神经网络。
28 单层回归网络:线性回归
28.1 单层回归网络的理论基础
深度学习中的计算是“简单大量”,而不是“复杂的单一问题”。神经网络的原理很多时候都比经典机器学习算法简单。了解神经网络,可以从 线性回归 算法开始。
线性回归算法是机器学习中最简单的回归类算法,多元线性回归指的就是一个样本对应多个特征的线性回归问题。假设我们的数据现在就是二维表,对于一个有
n
n
n个特征的样本而言,它的预测结果可以写作一个几乎人人熟悉的方程:
z
^
i
=
b
+
w
1
x
i
1
+
w
2
x
i
2
+
…
+
w
n
x
i
n
\hat{z}_i = b + w_1 x_{i1} + w_2 x_{i2} + \ldots + w_n x_{in}
z^i=b+w1xi1+w2xi2+…+wnxin
w
w
w和
b
b
b被统称为模型的权重,其中
b
b
b被称为截距(intercept
),也叫做偏差(bias
),
w
1
w_1
w1~
w
n
w_n
wn被称为回归系数(regression coefficient
),也叫作权重(weights
),
x
i
1
x_{i1}
xi1~
x
i
n
x_{in}
xin是样本
i
i
i上的不同特征。这个表达式,其实就和我们小学时就无比熟悉的
y
=
a
x
+
b
y = ax + b
y=ax+b 是同样的性质。其中
y
y
y被我们称为因变量,在线性回归中表示为
z
z
z,在机器学习中也就表现为我们的标签。如果写作
z
z
z,则代表真实标签。如果写作
z
^
\hat{z}
z^(读作z帽或者zhat
),则代表预测出的标签。模型得出的结果,一定是预测的标签
。
符号规范 |
---|
在我们学习autograd的时候,我们说线性回归的方程是
y
^
i
=
b
+
w
1
x
i
1
+
w
2
x
i
2
+
…
+
w
n
x
i
n
\hat{y}_i = b + w_1 x_{i1} + w_2 x_{i2} + \ldots + w_n x_{in}
y^i=b+w1xi1+w2xi2+…+wnxin。但在这里,为什么写做
z
z
z呢?首先,无论是回归问题还是分类问题,y永远表示标签(labels)。在回归问题中,y 是连续型数字,在分类问题中,y 是离散型的整数。对于线性回归来说,线性方程的输出结果就是最终的标签。但对于整个深度学习体系而言,复杂神经网络的输出才是最后的标签。在我们单独对线性回归进行说明的时候,行业惯例就是使用
z
z
z来表示线性回归的结果。 |
如果考虑我们有m个样本,则回归结果可以被写作:
z
^
i
=
b
+
w
1
x
i
1
+
w
2
x
i
2
+
…
+
w
n
x
i
n
\hat{z}_i = b + w_1 x_{i1} + w_2 x_{i2} + \ldots + w_n x_{in}
z^i=b+w1xi1+w2xi2+…+wnxin
其中
z
^
i
\hat{z}_i
z^i是包含了m
个全部的样本的预测结果的列向量。注意,我们通常使用粗体的小写字母来表示列向量,粗体的大写字母表示矩阵或者行列式。 并且在机器学习中,我们默认所有的一维向量都是列向量。
我们可以使用矩阵来表示上面多个样本的回归结果的方程,其中
w
w
w可以被看做是一个结构为(n+1,1)
的列矩阵(这里的n
加上的1
是我们的截距b
), 是一个结构为(m,n+1)
的特征矩阵(这里的n
加上的1
是为了与截距b
相乘而留下的一列1
,这列1
有时也被称作
x
0
x_0
x0,则有:
[ z ^ 1 z ^ 2 z ^ 3 … z ^ m ] = [ 1 x 11 x 12 x 13 … x 1 n 1 x 21 x 22 x 23 … x 2 n 1 x 31 x 32 x 33 … x 3 n … … … … … 1 x m 1 x m 2 x m 3 … x m n ] ∗ [ b w 1 w 2 … w n ] \begin{bmatrix}\hat{z}_1 \\\hat{z}_2 \\\hat{z}_3 \\\ldots \\\hat{z}_m\end{bmatrix}= \begin{bmatrix} 1 & x_{11} & x_{12} & x_{13} & \ldots & x_{1n} \\ 1 & x_{21} & x_{22} & x_{23} & \ldots & x_{2n} \\ 1 & x_{31} & x_{32} & x_{33} & \ldots & x_{3n} \\ \ldots & \ldots & \ldots & \ldots & & \ldots \\ 1 & x_{m1} & x_{m2} & x_{m3} & \ldots & x_{mn} \end{bmatrix} * \begin{bmatrix} b \\ w_1 \\ w_2 \\ \ldots \\ w_n \end{bmatrix} z^1z^2z^3…z^m = 111…1x11x21x31…xm1x12x22x32…xm2x13x23x33…xm3…………x1nx2nx3n…xmn ∗ bw1w2…wn
z ^ = X w \hat{z} = Xw z^=Xw
如果在我们的方程里没有常量b
,我们则可以不写X中的第一列以及w中的第一行。
线性回归的任务,就是构造一个预测函数来映射输入的特征矩阵 和标签值 的线性关系。这个预测函数的图像是一条直线,所以线性回归的求解就是对直线的拟合过程。
预测函数的本质就是我们需要构建的模型,而构造预测函数的核心就是找出模型的权重向量 ,也就是求解线性方程组的参数(相当于求解 y = a x + b y=ax+b y=ax+b里的 a a a与 b b b)。
现在假设,我们的数据只有2
个特征,则线性回归方程可以写作如下结构:
z
^
=
b
+
x
1
w
1
+
x
2
w
2
\hat{z}=b+x_1w_1+x_2w_2
z^=b+x1w1+x2w2
此时,我们只要对模型输入特征
x
1
x_1
x1,
x
2
x_2
x2的取值,就可以得出对应的预测值
z
^
\hat{z}
z^。神经网络的预测过程是从神经元左侧输入特征,让神经元处理数据,并从右侧输出预测结果。这个过程和我们刚才说到的线性回归输出预测值的过程是一致的。如果我们使用一个神经网络来表达线性回归上的过程,则可以有:
这就是一个最简单的单层回归神经网络的表示图。
在神经网络中,竖着排列在一起的一组神经元叫做“一层网络”,所以线性回归的网络直观看起来有两层,两层神经网络通过写有参数的线条相连。我们从左侧输入常数1和特征取值 x 1 x_1 x1, x 2 x_2 x2,再让它们与相对应的参数相乘,就可以得到 b b b, x 1 w 1 x_1w_1 x1w1, x 2 w 2 x_2w_2 x2w2三个结果。这三个结果通过连接到下一层神经元的直线,被输入下一层神经元。我们在第二层的神经元中将三个乘积进行加和(使用符号 ∑ \sum ∑表示),就可以得到加和结果 z ^ \hat{z} z^,即 b + x 1 w 1 + x 2 w 2 b+x_1w_1+x_2w_2 b+x1w1+x2w2,这个值正是我们的预测值。可见,线性回归方程与上面的神经网络图达到的效果是一模一样的。
在上述过程中,左侧的是神经网络的输入层(input layer
)。输入层由众多承载数据用的神经元组成,数据从这里输入,并流入处理数据的神经元中。在所有神经网络中,输入层永远只有一层,且每个神经元上只能承载一个特征(一个
x
x
x)或一个常量(通常都是1
)。现在的二元线性回归只有两个特征,所以输入层上只需要三个神经元,包括两个特征和一个常量,其中这里的常量仅仅是被用来乘以偏差
b
b
b用的。对于没有偏差的线性回归来说,我们可以不设置常量1
。
右侧的是输出层(output layer
)。输出层由大于等于一个神经元组成,我们总是从这一层来获取预测结果。输出层的每个神经元上都承载着单个或多个功能,可以处理被输入神经元的数据。在线性回归中,这个功能就是“加和”,当我们把加和替换成其他的功能,就能够形成各种不同的神经网络。
在神经元之间相互连接的线表示了数据流动的方向,就像人脑神经细胞之间相互联系的“轴突”。在人脑神经细胞中,轴突控制电子信号流过的强度,在人工神经网络中,神经元之间的连接线上的权重也代表了信息可通过的强度。最简单的例子是,当
w
w
w为0.5
时,在特征
x
1
x_1
x1上的信息就只有0.5
倍能够传递到下一层神经元中,因为被输入到下层神经元中去进行计算的实际值是
0.5
x
1
0.5x_1
0.5x1。相对的,如果
w
1
w_1
w1是2.5
,则会传递2.5
倍的 上的信息。因此,有的深度学习课程会将权重
w
w
w比喻成是电路中的”电压“,电压越大,则电信号越强烈,电压越小,信号也越弱,这都是在描述权重
w
w
w会如何影响传入下一层神经元的信息/数据量的大小。
到此,我们已经了解了线性回归的网络是怎么一回事,它是最简单的回归神经网络,同时也是最简单的神经网络。类似于线性回归这样的神经网络,被称为单层神经网络。
单层神经网络 |
---|
从直观来看,线性回归的网络结构明明有两层,为什么线性回归被叫做“单层神经网络”呢?业内通识是,在描述神经网络的层数的时候,我们不考虑输入层。输入层是每个神经网络都必须存在的一层,当使用相同的输入数据时,任意两个神经网络之间的不同之处就在输入层之后的所有层。所以,我们把输入层之后只有一层的神经网络称为单层神经网络。在非常非常少见的情况下,有的深度学习课程或教材中也会直接将所有层都算入其中,将上述网络称为“两层神经网络”,这种做法虽然不太规范,但也不能称之为“错误的”。因此,当出现“N层神经网络”的描述时,一定要注意原作者是否将输入层考虑进去了。 |
28.2 tensor实现单层神经网络的正向传播
让我们使用一组非常简单的代码来实现一下回归神经网络求解
z
^
\hat{z}
z^的过程,在神经网络中,这个过程是从左向右进行的,被称为神经网络的正向传播(forward spread
)。来看下面这组数据:
x0 | x1 | x2 | z |
---|---|---|---|
1 | 0 | 0 | -0.2 |
1 | 1 | 0 | -0.05 |
1 | 0 | 1 | -0.05 |
1 | 1 | 1 | 0.1 |
我们将构造能够拟合出以上数据的单层回归神经网络
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype = torch.float32)
z = torch.tensor([[-0.2],[-0.05], [-0.05],[0.1]])
w = torch.tensor([-0.2,0.15,0.15])
def LinearR(X,w):
zhat = torch.mv(X,w)
return zhat
zhat = LinearR(X,w)
28.3 tensor计算中的新手陷阱
接下来,我们对这段代码进行详细的说明
# 导入库
import torch
# 首先生成特征张量
X = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]])
# 我们输入的是整数,默认生成的是int64的类型
# 生成标签
z = torch.tensor([-0.2, -0.05, -0.05, 0.1])
# 我们输入的是浮点数,默认生成的是float32的类型
# 定义常量b和权重w
w = torch.tensor([-0.2, 0.15, 0.15])
# 注意,常量b所在的位置必须与特征张量中X中全为1的那一列所在位置相对应
tensor计算中的第一大坑:PyTorch的静态性
在前几节有提到过:静态性指的是对输入的张量类型有明确的要求,例如部分函数只能输入浮点型张量,而不能输入整型张量。
# 定义线性回归计算的函数
def LinearR(X, w):
# 矩阵与向量相乘时,向量必须作为mv的第二个参数
zhat = torch.mv(X, w)
return zhat
LnearR(X,w)
# output :
RuntimeError : expected scalar type Long but found Float
ps:Long = int64
PyTorch
中的许多函数都不接受浮点型的分类标签,但也有许多函数要求真实标签的类型必须与预测值的类型一致,因此标签的类型定义总是一个容易踩坑的地方。通常来说,我们还是回将标签定义为float32
,如果在函数运行时报错,要求整形,我们再使用.long()
方法将其转换为整型。
另一个非常容易踩坑的地方是,PyTorch
中许多函数不接受一维张量但同时也有许多函数不接受二维标签( ̄_ ̄|||)
。因此我们在生成标签时,可以默认生成二维标签,若函数报错说不能接受二维标签,我们再使用view()
函数将其调整为一维。
# 因此之后需要改成:
def LinearR(X, w):
zhat = torch.mv(X.float(), w)
return zhat
# 还可以使用大写的Tensor来解决这个问题,但这个方法并不推荐
X = torch.Tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]])
# 或者直接养成好习惯
X = torch.Tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]], dtype = torch.float32)
LinearR(X,w)
# output :
tensor([-0.2000, -0.0500, -0.0500, 0.1000])
torch.tensor——判断你的输入类型是什么类型,然后根据你输入的数据类型来确定结果的数据类型
torch.Tensor——无论你输入什么数据,都无脑使用float32
tensor计算中的第二大坑:精度问题
# 预测值
zhat = LinearR(X, w)
# 真实值
z = torch.tensor([[-0.2],[-0.05], [-0.05],[0.1]], dtype = torch.float32)
zhat == z
# output :
tensor([ True, False, False, False])
False
一定不是因为数据类型发生错误得出的,因为我们已经把z
的数据类型改为了浮点型。
在多元线性回归中,我们使用SSE
(误差平方和)来衡量回归的结果优劣:
S
S
E
=
∑
i
=
1
m
(
z
i
−
z
^
i
)
2
SSE = \sum_{i=1}^{m}(z_i - \hat{z}_i)^2
SSE=i=1∑m(zi−z^i)2
如果预测值和真实值完全相等,那SSE
的结果应该为0
。在这里,SSE
虽然非常接近0
,但的确是不为0
的。
SSE = sum((zhat - z) ** 2)
SSE
# output :
tensor(8.3267e-17)
#设置显示精度,再来看yhat与y_reg
torch.set_printoptions(precision=30) #看小数点后面30位的情况
zhat
# output :
tensor([-0.200000002980232238769531250000, -0.049999997019767761230468750000, -0.049999997019767761230468750000, 0.100000008940696716308593750000])
z
# output :
tensor([-0.200000002980232238769531250000, -0.050000000745058059692382812500, -0.050000000745058059692382812500, 0.100000001490116119384765625000])
zhat
和z
的差异有两个原因:
float32
由于只保留32
位,所以精确性会有一些问题。torch.mv
这个函数在进行计算时,内部计算时会出现一些很微小的精度问题。
精度问题在tensor
维度很高,数字很大时,也会变得更大
preds = torch.ones(300,68,64,64) * 0.1
preds.sum() * 10
# output :
tensor(83558352.)
preds = torch.ones(300,68,64,64)
preds.sum()
# output :
tensor(83558400.)
怎么解决这个问题呢?
与python
中存在decimal
库不同,pytorch
设置了64
位浮点数来 尽量 减轻精度问题
preds = torch.ones(300,68,64,64,dtype = torch.float64) *0.1
preds.sum() * 10
# output :
tensor(83558400.000000059604644775390625000000, dtype=torch.float64)
但即便如此,也不能完全消除精度问题带来的区别
如果你希望能够无视掉非常小的区别,而让两个张量的比较结果展示为True,可以使用下面的代码
torch.allclose(zhat, z)
28.4 torch.nn.Linear实现单层回归神经网络的正向传播
上面为pytorch
的架构图,从图中我们可以看到,torch.nn
是包含了构筑神经网络结构基本元素的包,在这个包中可以找到任意的神经网络层。这些神经网络层都是nn.Module
这个大类的子类。
我们的torch.nn.Linear
就是神经网络中的“线性层”,它可以实现形如
z
^
=
X
w
\hat{z}=Xw
z^=Xw的加和功能。
在单层回归神经网络结构图中,torch.nn.Linear
类表示了我们的输出层。现在我们就来看看它是如何使用的。
回顾一下我们的数据:
x0 | x1 | x2 | z |
---|---|---|---|
1 | 0 | 0 | -0.2 |
1 | 1 | 0 | -0.05 |
1 | 0 | 1 | -0.05 |
1 | 1 | 1 | 0.1 |
接下来,使用nn.Linear
来实现单层回归神经网络:
import torch
X = torch.tensor([[0,0],[1,0],[0,1],[1,1]],dtype = torch.float32)
output = torch.nn.Linear(2, 1)
zhat = output(X)
nn.Linear
是一个类,在这里代表了输出层,所以使用了output
作为变量名,output =
这一行相当于是类的实例化过程。- 实例化的时候,
nn.Linear
需要输入两个参数,分别是(上一层的神经元个数,这一层的神经元个数)。上一层是输出层,因此神经元个数由特征的个数决定(2
个)。这一层是输出层,作为回归神经网络,输出层只有一个神经元。因此nn.Linear
中输入的是(2,1
)。 - 上面只定义了
X
,没有定义w
和b
。所有nn.Module
的子类,形如nn.XXX
的层,都会在实例化的同时随机生成w
和b
的初始值。所以实例化之后,我们就可以调用以下属性来查看生成的 w w w和 b b b:
# 查看生成的w
output.weight
# output :
Parameter containing:tensor([[ 0.683788955211639404296875000000, -0.588803172111511230468750000000]],requires_grad=True)
# 查看生成的b
output.bias
# output :
Parameter containing:tensor([0.426940977573394775390625000000], requires_grad=True)
- 其中,
w
是必然会生成的,b
是我们可以控制是否要生成的。
output = torch.nn.Linear(2, 1, bias = False)
- 由于
w
和b
是随机生成的,所以同样的代码运行多次后的结果是不一致的。如果我们希望控制随机性,则可以使用torch
中的random
类。如下所示:
torch.random.manual_seed(420) # 人为设置随机数种子
- 由于不需要定义常量
b
,因此在特征张量中,也不需要留出与常数项相乘的x0那一列,在输入数据时,我们只输入了两个特征x1和x2。 - 输入层只有一层,且输入层的结构(神经元的个数)由输入的特征张量 X 决定,因此在
pytorch
中构筑神经网络时,不需要定义输入层。 - 实例化之后,将特征张量输入到实例化后的类中,即可得到输出层的输出结果。
由于我们没有自己定义w
和b
,所以无法让nn.Linear
输出的zhat
与我们真实的z
接近——让真实值与预测值差异更小的部分,我们会在之后进行讲解。
29 二分类神经网络:逻辑回归
29.1 二分类神经网络的理论基础
线性回归是统计学经典算法,它能够拟合出一条直线来描述变量之间的 线性关系 。但 在实际中,变量之间的关系通常都不是一条直线,而是呈现出某种曲线关系 。在统计学的历史中,为了让统计学模型能够更好地拟合曲线,统计学家们在线性回归的方程两边引入了联系函数(link function
),对线性回归的方程做出了各种各样的变化,并将这些变化后的方程称为“广义线性回归”。其中比较著名的有等式两边同时取对数的对数函数回归、同时取指数的S
形函数回归等。
y = a x + b → ln y = ln ( a x + b ) y = a x + b → e y = e a x + b \begin{align*} y &= ax + b \quad \rightarrow \quad \ln y = \ln(ax + b) \\ y &= ax + b \quad \rightarrow \quad e^y = e^{ax + b} \end{align*} yy=ax+b→lny=ln(ax+b)=ax+b→ey=eax+b
在探索的过程中,一种奇特的变化吸引了统计学家们的注意,这个变化就是sigmoid
函数带来的变化。
Sigmoid
函数的公式如下:
σ
=
S
i
g
m
o
i
d
(
z
)
=
1
1
+
e
−
z
\sigma = Sigmoid(z) = \frac{1}{1 + e^{-z}}
σ=Sigmoid(z)=1+e−z1
其中
e
e
e为自然常数(约为2.71828
),其中
z
z
z是它的自变量,
σ
\sigma
σ是因变量,
z
z
z的值常常是线性模型的取值(比如,线性回归的结果
z
z
z)。Sigmoid
函数是一个S
型的函数,它的图像如下:
从图像上就可以看出,这个函数的性质相当特别。当自变量
z
z
z趋近正无穷时,因变量
σ
\sigma
σ趋近于1
,而当
z
z
z趋近负无穷时,
σ
\sigma
σ趋近于0
,这使得sigmoid
函数能够将任何实数映射到(0,1)
区间。同时,Sigmoid
的导数在$ z=0
点时最大(这一点的斜率最大),所以它可以快速将数据从
点时最大(这一点的斜率最大),所以它可以快速将数据从
点时最大(这一点的斜率最大),所以它可以快速将数据从z=0$的附近排开,让数据点到远离自变量取0
的地方去。这样的性质,让sigmoid
函数拥有将连续性变量 转化为离散型变量 的力量,这也就是化回归算法为分类算法的力量。
具体怎么操作呢?只要将线性回归方程的结果作为自变量带入sigmoid
函数,得出的数据就一定是(0,1)
之间的值。此时,只要我们设定一个阈值(比如0.5
),规定 大于0.5
时,预测结果为1
类, 小于0.5
时,预测结果为0
类,则可以顺利将回归算法转化为分类算法。此时,我们的标签就是类别0和1
了。这个阈值可以自己调整,在没有调整之前,一般默认0.5
。
σ
=
1
1
+
e
−
z
=
1
1
+
e
−
X
w
\sigma = \frac{1}{1 + e^{-z}} = \frac{1}{1 + e^{-Xw}}
σ=1+e−z1=1+e−Xw1
更神奇的是,当我们对线性回归的结果取sigmoid
函数之后,只要再进行以下操作:
1)将结果 σ \sigma σ以几率 ( σ 1 − σ ) \left(\frac{\sigma}{1-\sigma}\right) (1−σσ)的形式展现
2)在几率上求以e
为底的对数
就很容易得到:
ln
σ
1
−
σ
=
ln
(
1
1
+
e
−
X
w
1
−
1
1
+
e
−
X
w
)
=
ln
(
1
1
+
e
−
X
w
e
−
X
w
1
+
e
−
X
w
)
=
ln
(
1
e
−
X
w
)
=
ln
(
e
X
w
)
=
X
w
\begin{align*} \ln \frac{\sigma}{1-\sigma} &= \ln \left( \frac{\frac{1}{1+e^{-Xw}}}{1 - \frac{1}{1+e^{-Xw}}} \right) \\ &= \ln \left( \frac{\frac{1}{1+e^{-Xw}}}{\frac{e^{-Xw}}{1+e^{-Xw}}} \right) \\ &= \ln \left( \frac{1}{e^{-Xw}} \right) \\ &= \ln (e^{Xw}) \\ &= Xw \end{align*}
ln1−σσ=ln(1−1+e−Xw11+e−Xw1)=ln(1+e−Xwe−Xw1+e−Xw1)=ln(e−Xw1)=ln(eXw)=Xw
不难发现,让
σ
\sigma
σ取对数几率后所得到的值就是我们线性回归的
z
z
z!因为这个性质,在等号两边加sigmoid
的算法被称为“对数几率回归”,在英文中就是Logistic Regression
,就是逻辑回归。逻辑回归可能是广义线性回归中最广为人知的算法,它是一个叫做“回归“实际上却总是被用来做分类的算法,对机器学习和深度学习都有重大的意义。在面试中,如果我们希望了解一个人对机器学习的理解程度,第一个问题可能就会从sigmoid
函数以及逻辑回归是如何来的开始。
σ \sigma σ值代表了样本为某一类标签的概率 |
---|
ln
σ
1
−
σ
\ln \frac{\sigma}{1 - \sigma}
ln1−σσ是形似对数几率的一种变化。而几率odds 的本质其实是
p
1
−
p
\frac{p}{1-p}
1−pp,其中p是事件A发生的概率,而1-p 是事件A 不会发生的概率,并且p+(1-p)=1 。因此,很多人在理解逻辑回归时,都对
σ
\sigma
σ做出如下的解释:我们让线性回归结果逼近0 和1 ,此时
σ
\sigma
σ和
1
−
σ
1-\sigma
1−σ之和为1,因此它们可以被我们看作是一对正反例发生的概率,即
σ
\sigma
σ是某样本i 的标签被预测为1 的概率,而
1
−
σ
1-\sigma
1−σ是i 的标签被预测为0 的概率,
σ
1
−
σ
\frac{\sigma}{1-\sigma}
1−σσ就是样本i的标签被预测为1的相对概率。基于这种理解,逻辑回归、即单层二分类神经网络返回的结果被当成是概率来看待和使用(如果直接说它就是概率,或许不太严谨)。每当我们希望求解“样本i 的标签是1 或是0 的概率”时,我们就使用逻辑回归。因此,当一个样本对应的
σ
i
\sigma_i
σi越接近1 或0 ,我们就认为逻辑回归对这个样本的预测结果越肯定,样本被分类正确的可能性也越高。如果
σ
i
\sigma_i
σi非常接近阈值(比如0.5),就说明逻辑回归其实对这个样本究竟应该是哪一类别,不是非常肯定。 |
29.2 tensor实现二分类神经网络的正向传播
我们可以在PyTorch
中非常简单地实现逻辑回归的预测过程,让我们来看下面这一组数据。很容易注意到,这组数据和上面的回归数据的特征(
x
1
,
x
2
x_1,x_2
x1,x2)是完全一致的,只不过标签y
由连续型结果转变为了分类型的0
和1
。这一组分类的规律是这样的:当两个特征都为1
的时候标签就为1
,否则标签就为0
。这一组特殊的数据被我们称之为 “与门”(AND GATE) ,这里的“与”正是表示“特征一与特征二都是1
”的含义。
x0 | x1 | x2 | andgate |
---|---|---|---|
1 | 0 | 0 | 0 |
1 | 1 | 0 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 1 | 1 |
要拟合这组数据,只需要在刚才我们写好的代码后面加上sigmoid
函数以及阈值处理后的变化。
import torch
X = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 0, 1], [1, 1, 1]], dtype = torch.float32)
andgate = torch.tensor([-0.2, 0.15, 0.15], dtype = torch.float32)
# 保险起见,生成二维的、float32类型的标签
w = torch.tensor([-0.2,0.15,0.15], dtype = torch.float32)
def LogisticR(X,w):
zhat = torch.mv(X,w)
sigma = 1/(1+torch.exp(-zhat))
#sigma = torch.sigmoid(zhat)
andhat = torch.tensor([int(x) for x in sigma >= 0.5], dtype = torch.float32)
return sigma, andhat
接下来,我们对这段代码进行详细的说明:
# 导入torch库
import torch
# 特征张量,养成良好习惯,上来就定义数据类型
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype = torch.float32)
#标签,分类问题的标签是整型
andgate = torch.tensor([0,0,0,1], dtype = torch.float32)
#定义w,注意这一组w与之前在回归中使用的完全一样
w = torch.tensor([-0.2,0.15,0.15], dtype = torch.float32)
def LogisticR(X,w):
#首先执行线性回归的过程,依然是mv函数,让矩阵与向量相乘得到z
zhat = torch.mv(X,w)
#执行sigmoid函数,你可以调用torch中的sigmoid函数,也可以自己用torch.exp来写
sigma = torch.sigmoid(zhat)
#sigma = 1/(1+torch.exp(-zhat))
#设置阈值为0.5, 使用列表推导式将值转化为0和1
andhat = torch.tensor([int(x) for x in sigma >= 0.5], dtype = torch.float32)
return sigma, andhat
sigma, andhat = LogisticR(X,w)
sigma
# output :
tensor([0.450166016817092895507812500000, 0.487502634525299072265625000000,0.487502634525299072265625000000, 0.524979174137115478515625000000])
andhat
# output :
tensor([0., 0., 0., 1.])
andgate == andhat
#最后得到的都是0和1,虽然andhat数据格式是float32,但本质上数还是整数,不存在精度问题
可见,这里得到了与我们期待的结果一致的结果,这就将回归算法转变为了二分类。这个过程在神经网络中的表示图如下:
可以看出,这个结构与线性回归的神经网络唯一不同的就是输出层中多出了一个Sigmoid(z) 。当有了Sigmoid
函数的结果
σ
\sigma
σ之后,只要了解阈值是0.5
(或者任意我们自己设定的数值),就可以轻松地判断任意样本的预测标签
y
^
\hat{y}
y^。在二分类神经网络中,Sigmoid
实现了将连续型数值转换为分类型数值的作用,在现代神经网络架构中,除了Sigmoid
函数之外,还有许多其他的函数可以被用来将连续型数据分割为离散型数据,接下来,我们就介绍一下这些函数。
29.3 符号函数sign,ReLU,Tanh
符号函数sign
我们可以使用以下表达式来表示它:
y
=
{
1
if
z
>
0
0
if
z
=
0
−
1
if
z
<
0
y = \begin{cases} 1 & \text{if } z > 0 \\ 0 & \text{if } z = 0 \\ -1 & \text{if } z < 0 \end{cases}
y=⎩
⎨
⎧10−1if z>0if z=0if z<0
由于函数的取值是间断的,符号函数也被称为“阶跃函数”,表示在0
的两端,函数的结果y
是从-1
直接阶跃到了1
。在这里,我们使用y
而不是
σ
\sigma
σ来表示输出的结果,是因为输出结果直接是0
、1
、-1
这样的类别,就相当于标签了。对于sigmoid
函数而言, 返回的是0~1
之间的概率值,如果我们希望获取最终预测出的类别,还需要将概率转变成0
或1
这样的数字才可以。但符号函数可以直接返回类别,因此我们可以认为符号函数输出的结果就是最终的预测结果y
。在二分类中,符号函数也可以忽略中间的时候,直接分为0
和1
两类,用如下式子表示:
y
=
{
1
if
z
>
0
0
if
z
≤
0
y = \begin{cases} 1 & \text{if } z > 0 \\ 0 & \text{if } z \leq 0 \end{cases}
y={10if z>0if z≤0
等号被并在上方或下方都可以。这个式子可以很容易被转化为下面的式子:
∵
z
=
w
1
x
1
+
w
2
x
2
+
b
∴
y
=
{
1
if
w
1
x
1
+
w
2
x
2
+
b
>
0
0
if
w
1
x
1
+
w
2
x
2
+
b
≤
0
∴
y
=
{
1
if
w
1
x
1
+
w
2
x
2
>
−
b
0
if
w
1
x
1
+
w
2
x
2
≤
−
b
\because z = w_1 x_1 + w_2 x_2 + b \\ \therefore y = \begin{cases} 1 & \text{if } w_1 x_1 + w_2 x_2 + b > 0 \\ 0 & \text{if } w_1 x_1 + w_2 x_2 + b \leq 0 \end{cases} \\ \therefore y = \begin{cases} 1 & \text{if } w_1 x_1 + w_2 x_2 > -b \\ 0 & \text{if } w_1 x_1 + w_2 x_2 \leq -b \end{cases}
∵z=w1x1+w2x2+b∴y={10if w1x1+w2x2+b>0if w1x1+w2x2+b≤0∴y={10if w1x1+w2x2>−bif w1x1+w2x2≤−b
此时,
−
b
-b
−b就是一个阈值,我们可以使用任意字母来替代它,比较常见的是字母
θ
\theta
θ 。当然,不把它当做阈值,依然保留
w
1
x
1
+
w
2
x
2
+
b
w_1x_1+w_2x_2+b
w1x1+w2x2+b与0
进行比较的关系也没有任何问题。和sigmoid
一样,我们也可以使用阶跃函数来处理”与门“的数据:
import torch
X = torch.tensor([[0,0],[1,0],[0,1],[1,1]],dtype=torch.float32)
andgate = torch.tensor([[0],[0],[0],[1]], dtype = torch.float32)
w = torch.tensor([-0.2,0.15, 0.15], dtype = torch.float32)
def LinearRwithsign(X,w):
zhat = torch.mv(X,w)
andhat = torch.tensor([int(x) for x in zhat >= 0], dtype = torch.float32)
return zhat, andhat
阶跃函数和sigmoid
都可以完成二分类的任务。在神经网络的二分类中, 的默认取值一般都是sigmoid
函数,少用阶跃函数,这是由神经网络的解法决定的。
ReLU
ReLU(Rectified Linear Unit)
函数又名整流线型单元函数,应用甚至比sigmoid
更广泛。ReLU
提供了一个很简单的非线性变换:当输入的自变量大于0
时,直接输出该值,当输入的自变量小于等于0
时,输出0
。这个过程可以用以下公式表示出来:
R
e
L
U
:
σ
=
{
z
(
z
>
0
)
0
(
z
≤
0
)
ReLU: \sigma = \begin{cases} z & (z > 0) \\ 0 & (z \leq 0) \end{cases}
ReLU:σ={z0(z>0)(z≤0)
ReLU
函数是一个非常简单的函数,本质就是max(0,z)
。max
函数会从输入的数值中选择较大的那个值进行输出,以达到保留正数元素,将负元素清零的作用。ReLU
的图像如下所示:
相对的,ReLU
函数导数的图像如下:
当输入
z
z
z为正数时,ReLU
函数的导数为1
,当
z
z
z为负数时,ReLU
函数的导数为0
,当输入为0
时,ReLU
函数不可导。因此,ReLU
函数的导数图像看起来就是阶跃函数,这是一个美好的巧合。
tanh
tanh(hyperbolic tangent)
是双曲正切函数,双曲正切函数的性质与sigmoid
相似,它能够将数值压缩到(-1,1)
区间内。
t
a
n
h
:
σ
=
e
2
z
−
1
e
2
z
+
1
tanh: \sigma = \frac{e^{2z} - 1}{e^{2z} + 1}
tanh:σ=e2z+1e2z−1
而双曲正切函数的图像如下:
可以看出,tanh
的图像和sigmoid
函数很像,不过sigmoid
函数的范围是在(0,1)
之间,tanh
却是在坐标系的原点(0,0)
点上中心对称。
对tanh
求导后可以得到如下公式和导数图像:
tanh
′
(
z
)
=
1
−
tanh
2
(
z
)
\tanh'(z) = 1 - \tanh^2(z)
tanh′(z)=1−tanh2(z)
可以看出,当输入的 约接近于0,tanh函数导数也越接近最大值1,当输入越偏离0时,tanh函数的导数越接近于0。**这些函数是最常见的二分类转化函数,他们在神经网络的结构中有着不可替代的作用。**在单层神经网络中,这种作用是无法被体现的,因此关于这一点,我们可以之后再进行说明。到这里,我们只需要知道这些函数都可以将连续型数据转化为二分类就足够了。
29.4 torch.functional实现二分类神经网络的正向传播
之前我们使用torch.nn.Linear类实现了单层回归神经网络,现在我们试着来实现单层二分类神经网络,也就是逻辑回归。逻辑回归与线性回归的唯一区别,就是在线性回归的结果之后套上了sigmoid函数。
不难想象,只要让nn.Linear的输出结果再经过sigmoid函数,就可以实现逻辑回归的正向传播了。
在PyTorch
中,我们几乎总是从nn.functional
中调用相关函数。
回顾一下我们的数据和网络架构:
x0 | x1 | x2 | andgate |
---|---|---|---|
1 | 0 | 0 | 0 |
1 | 1 | 0 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 1 | 1 |
接下来,我们在之前线性回归代码的基础上,加上nn.functional
来实现单层二分类神经网络:
import torch
from torch.nn import functional as F
X = torch.tensor([[0,0],[1,0],[0,1],[1,1]], dtype = torch.float32)
torch.random.manual_seed(420) #人为设置随机数种子
dense = torch.nn.Linear(2,1)
zhat = dense(X)
sigma = F.sigmoid(zhat)
y = [int(x) for x in sigma > 0.5]
在这里,nn.Linear
虽然依然是输出层,但却没有担任最终输出值的角色,因此这里我们使用dense
作为变量名。dense
表示紧密链接的层,即上一层的大部分神经元都与这一层的大部分神经元相连,在许多神经网络中我们都会用到密集链接的层,因此dense
是我们经常会用到的一个变量名。我们将数据从nn.Linear
传入,得到zhat
,然后再将zhat
的结果传入sigmoid
函数,得到sigma
,之后再设置阈值为0.5
,得到最后的y
。
在PyTorch
中,我们可以从functional
模块里找出大部分之前我们提到的函数
#符号函数sign
torch.sign(zhat)
#ReLU
F.relu(zhat)
#tanh
torch.tanh(zhat)
在PyTorch
的安排中,符号函数sign
与双曲正切函数tanh
更多时候只是被用作数学计算工具,而ReLU
和Sigmoid
却作为神经网络的组成部分被放在库functiona
l中,这其实反映出实际使用时大部分人的选择。
ReLU
与Sigmoid
还是主流的、位于nn.Linear
后的函数。
30 多分类神经网络:Softmax回归
30.1 认识softmax函数
之前介绍分类神经网络时,我们只说明了二分类问题,即标签只有两种类别的问题(0和1,猫和狗)。虽然在实际应用中,许多分类问题都可以用二分类的思维解决,但依然存在很多多分类的情况,最典型的就是手写数字的识别问题。计算机在识别手写数字时,需要对每一位数字进行判断,而个位数字总共有10
个(0~9),所以手写数字的分类是十分类问题,一般分别用0~9表示。
Softmax
函数是深度学习基础中的基础,它是神经网络进行多分类时,默认放在输出层中处理数据的函数。假设现在神经网络是用于三分类数据,且三个分类分别是苹果,柠檬和百香果,序号则分别是分类1
、分类2
和分类3
。则使用softmax
函数的神经网络的模型会如下所示:
与二分类一样,我们从网络左侧输入特征,从右侧输出概率,且概率是通过线性回归的结果
z
z
z外嵌套softmax
函数来进行计算。在二分类时,输出层只有一个神经元,只输出样本对于正类别的概率(通常是标签为1
的概率),而softmax
的输出层有三个神经元,分别输出该样本的真实标签是苹果、柠檬或百香果的概率
σ
1
,
σ
2
,
σ
3
\sigma_1,\sigma_2,\sigma_3
σ1,σ2,σ3。在多分类中,神经元的个数与标签类别的个数是一致的,如果是十分类,在输出层上就会存在十个神经元,分别输出十个不同的概率。此时,样本的预测标签就是所有输出的概率
σ
1
,
σ
2
,
σ
3
\sigma_1,\sigma_2,\sigma_3
σ1,σ2,σ3中最大的概率对应的标签类别。
那每个概率是如何计算出来的呢?来看Softmax函数的公式:
σ
k
=
Softmax
(
z
k
)
=
e
z
k
∑
K
e
z
\sigma_k = \text{Softmax}(z_k) = \frac{e^{z_k}}{\sum^{K} e^z}
σk=Softmax(zk)=∑Kezezk
其中
e
e
e为自然常数(约为2.71828
), 与sigmoid
函数中的
z
z
z一样,表示回归类算法(如线性回归)的结果。 表示该数据的标签中总共有
K
K
K个标签类别,如三分类时
K
=
3
K=3
K=3,四分类时
K
=
4
K=4
K=4。
k
k
k表示标签类别
k
k
k类。很容易可以看出,Softmax
函数的分子是多分类状况下某一个标签类别的回归结果的指数函数,分母是多分类状况下所有标签类别的回归结果的指数函数之和,因此Softmax****函数的结果代表了样本的结果为类别
k
k
k的概率**。
30.2 Pytorch中的softmax函数
我们曾经提到过,神经网络是模型效果很好,但运算速度非常缓慢的算法。softmax
函数也存在相同的问题——它可以将多分类的结果转变为概率(这是一个极大的优势),但它需要的计算量非常巨大。由于softmax
的分子和分母中都带有
e
e
e为底的指数函数,所以在计算中非常容易出现极大的数值。
如上图所示, e10就已经等于20000
了,而回归结果
z
z
z完全可能是成千上万的数字。事实上e100会变成一个后面有40多个0的超大值,e1000则会直接返回无限大inf
,这意味着这些数字已经超出了计算机处理数时要求的有限数据宽度,超大数值无法被计算机运算和表示。这种现象叫做“溢出“,当计算机返回”内存不足”或Python
服务器直接断开连接的时候,可能就是发生了这个问题。来看看这个问题实际发生时的状况:
#对于单一样本,假定一组巨大的z
z = torch.tensor([1010,1000,990], dtype=torch.float32)
torch.exp(z) / torch.sum(torch.exp(z)) # softmax函数的运算
# output :
tensor([nan, nan, nan])
因此,我们一般不会亲自使用tensor
来手写softmax
函数。在PyTorch
中,我们往往使用内置好的softmax
函数来计算softmax
的结果,我们可以使用torch.softmax
来轻松的调用它,具体代码如下:
z = torch.tensor([1010,1000,990], dtype=torch.float32)
torch.softmax(z,0)
#你也可以使用F.softmax, 它返回的结果与torch.softmax是完全一致的
#假设三个输出层神经元得到的z分别是10,9,5
z = torch.tensor([10,9,5], dtype=torch.float32)
torch.exp(z) / torch.sum(torch.exp(z)) # softmax函数的运算
z = torch.tensor([10,9,5], dtype=torch.float32)
torch.softmax(z,0) # 第二个参数表示计算的维度索引
# output :
tensor([0.7275, 0.2676, 0.0049])
从上面的结果可以看出,softmax
函数输出的是从0
到1.0
之间的实数,而且多个输出值的总和是1
。因为有了这个性质,我们可以把softmax
函数的输出解释为“概率”,这和我们使用sigmoid
函数之后认为函数返回的结果是概率异曲同工。从结果来看,我们可以认为返回了我们设定的 ([10,9,5])
的这个样本的结果应该是第一个类别(也就是z=10
的类别),因为类别1的概率是最大的
需要注意的是,使用了softmax
函数之后,各个 之间的大小关系并不会随之改变,这是因为指数函数ez是单调递增函数,也就是说,使用softmax
之前的 如果比较大,那使用softmax
之后返回的概率也依然比较大。这是说,无论是否使用softmax,我们都可以判断出样本被预测为哪一类,我们只需要看最大的那一类就可以了。所以,在神经网络进行分类的时候,如果不需要了解具体分类问题每一类的概率是多少,而只需要知道最终的分类结果,我们可以省略输出层上的softmax
函数。
30.3 使用nn.Linear与functional实现多分类神经网络的正向传播
import torch
from torch.nn import functional as F
X = torch.tensor([[0,0],[1,0],[0,1],[1,1]], dtype = torch.float32)
torch.random.manual_seed(420)
dense = torch.nn.Linear(2,3) #此时,输出层上的神经元个数是3个,因此应该是(2,3)
zhat = dense(X)
sigma = F.softmax(zhat,dim=1) #此时需要进行加和的维度是1
31 回归vs二分类vs多分类
到这里,我们已经见过了三个不同的神经网络:
注意到有什么相似和不同了吗?
首先可能会注意到的是,这三个神经网络都是单层神经网络,除了输入层,他们都有且只有一层网络。实际上,现实中使用的神经网络几乎99%
都是多层的,但我们的网络也能够顺利进行预测,这说明单层神经网络其实已经能够实现基本的预测功能。同时,这也说明了一个问题,无论处理的是回归还是分类,神经网络的处理原理是一致的。实际上,就连算法的限制、优化方法和求解方法也都是一致的。回归和分类神经网络唯一的不同只有输出层上的
σ
\sigma
σ。
虽然线性回归看起来并没有
σ
\sigma
σ的存在,但实际上我们可以认为线性回归中的
σ
\sigma
σ是一个恒等函数(identityfunction
),即是说
σ
(
z
)
=
z
\sigma(z)=z
σ(z)=z(相当于
y
=
x
y=x
y=x,或
f
(
x
)
=
x
f(x)=x
f(x)=x)。而多分类的时候也可以不采用任何函数,只观察
z
z
z的大小,所以多分类也可以被认为是利用了恒等函数作为
σ
\sigma
σ。总结来说,回归和分类对应的
σ
\sigma
σ分别如下:
输出类型 | σ \sigma σ |
---|---|
回归 | 恒等函数 |
二分类 | sigmoid 或任意可以实现二分类的函数(通常都是sigmoid ) |
多分类 | softmax 或恒等函数 |
第二个很容易发现的现象是,只有多分类的情况在输出层出现了超过一个神经元。实际上,当处理单标签问题时(即只有一个y
的问题),回归神经网络和二分类神经网络的输出层永远只有一个神经元,而只有多分类的情况才会让输出层上超过一个神经元。