本文将从一个简单的线性回归问题出发,构建单层神经网络,并手动实现它的正向传播。同时,我们将介绍如何使用PyTorch中的核心模块torch.nn
来构建该线性回归的神经网络。最后,我们将问题拓展至二分类及多分类的情景,以及它们的代码实现。
线性回归下单层神经网络的正向传播
在深度学习中,线性回归方程通常被表达为:
- 表示线性回归的预测结果, 表示线性回归的真实标签;
- 此处使用 而非 来表示线性回归的标签,是因为在深度学习中, 通常用于表示模型结果标签,无论是分类问题( 是离散的整数),还是回归问题( 为连续数值)。线性回归的结果一般为深度学习模型的中间结果,因此用 来表示,以示区别。
写成矩阵的形式:
其中,
,
,
。
下面我们用一个简单的线性回归例子,来阐述最基础的神经网络的架构。它的数据如下:
【例1】线性回归
x0 | x1 | x2 | z |
---|---|---|---|
1 | 0 | 0 | -0.2 |
1 | 1 | 0 | -0.05 |
1 | 0 | 1 | -0.05 |
1 | 1 | 1 | 0.1 |
上述线性回归例子中,包含两个特征 ,,即该线性回归模型为:
我们可以用如下神经网络来描述它:
单层神经网络:线性回归问题以上是一个最简单的单层回归神经网络的表示图。在神经网络中,圆圈代表神经元,竖着排列在一起的神经元构成了一层神经网络。从图上看,线性回归模型似乎包含了两层神经网络(输入层和输出层),然而神经网络的输入层通常不计入神经网络的层数,因此我们称上图网络为单层神经网络。神经网络的层与层之间由带有参数的线条相连接,将左侧各神经元上的值(此例中为:,,)分别与其对应的参数(此例中为:,,)相乘,得到的相乘的结果(此例中为:,,)由相连的线条输送至下一层的神经元上,并进行加和(用符号表示),即可得到右侧神经元上的预测值,即。
本例中,左侧层为输入层,由承载数据用的神经元组成,数据从这里输入,并流入处理数据的神经元中。在所有神经网络中,输入层只有一层,且每个神经元上只能承载一个特征(一个)或者一个常量(通常为1)。常量仅被用来乘以偏差。对于没有偏差的线性回归,可以不设置常量1。右侧为输出层,由大于等于一个神经元组成,在这一层获得预测结果。输出层的每个神经元都承载着单个或多个功能,本例中,输出层神经元的功能为“加和”,可替换成其他功能,形成不同的神经网络。神经元之间相连的线表示了数据流动的方向,线上的参数(权重)也代表了信息传递的“强度”,权重越大,强度越强。
下面是使用Pytorch来实现线性回归的正向传播:
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], dtype=torch.float32)
# 定义参数(此处参数值为笔者任意设定,也可使用随机数生成)
w = torch.tensor([-0.2,0.15,0.15], dtype=torch.float32)
# 定义线性回归正向传播方法
def LinearR(X, w):
z_hat = torch.mv(X, w)
return z_hat
# 计算预测值
z_hat = LinearR(X,w)
z_hat
tensor([-0.2000, -0.0500, -0.0500, 0.1000])
z_hat == z
tensor([ True, False, False, False])
torch.allclose(z_hat, z)
True
注意事项
- 定义tensor时,最好每次都定义清楚tensor的数据类型,并且将tensor类型保持一致,因为在很多运算中要求tensor类型保持一致;
- 由于运算精度的原因,z_hat 与 z 并不完全相等;精度问题会在tensor维度非常高,数字很大时,更加突出;若对精度要求很高,可以使用float64;
- Pytorch中很多函数都不接受浮点型的分类标签,但也有很多函数要求真实标签的类型必须与预测标签的类型一致。通常将标签定义为float32,若在运算过程中报错,再使用.long()方法将其转化为整型;
- Pytorch中很多函数不接受一维张量,但也有很多函数不接受二维标签,因此在生成标签时,可以默认生成二维标签,若报错,再使用view()函数将其调整为一维。
使用 torch.nn.Linear 实现正向传播
torch.nn
是pytorch中核心模块之一,提供了构筑神经网络结构的基本元素,其中nn.Module
提供了神经网络的各种层;nn.functional
包含了各种神经网络的损失函数与激活函数。下面我们将使用该模块来实现线性回归的正向传播。
import torch
output = torch.nn.Linear(2, 1)
print(output.weight)
print(output.bias)
Parameter containing:tensor([[0.2015, 0.1453]], requires_grad=True)
Parameter containing:tensor([-0.4766], requires_grad=True)
说明
- output 是一个torch.nn.Linear的实例化;
- torch.nn.Linear两个参数分别为:上一层神经元个数、接收层神经元个数;本例中,上一层是输入层,因此神经元个数由特征的个数决定(2个,不包含常量),这一层是输出层,作为回归神经网络,输出层只有一个神经元。因此nn.Linear中输入的是(2,1);
- output将自动生成随机的权重及截距,用于神经网络的正向传播;
- 可以使用torch.random.manual_seed()来设置随机数种子。
X = torch.tensor([[0,0],[1,0],[0,1],[1,1]], dtype=torch.float32)
z_hat = output(X)
z_hat
tensor([[-0.4766],
[-0.2751],
[-0.3313],
[-0.1299]], grad_fn=<AddmmBackward>)
二分类神经网络的原理与实现
在实际应用中,只有很少的问题能满足线性模型。为了使模型能够更好的拟合曲线,统计学家们在线性方程的两边引入了联系函数(Link Function),变化后的方程被称为广义的线性回归。
Sigmoid函数
Sigmoid函数的曲线如下图:
# 绘制 sigmoid 函数曲线
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-10, 10, 100)
y = 1/(1+np.exp(-x))
plt.figure(figsize=(10,6))
plt.plot(x, y)
Sigmoid
Sigmoid 函数特点:
- 能将任意实数映射到区间;
- 在远离的区域,趋近于或者;
- 中心对称的S曲线;
- 平滑,处处可导。
经过sigmoid函数转换后得到的值都在区间内,通常我们可以将这个值作为分类为1的概率,比如:转换后的,则分类为1的概率为(即分类为的概率为)。
几率(odds)
几率:事件发生的概率与事件不发生的概率的比值。
对数几率(log odds)
实现二分类网络的正向传播
二分类神经网络的结构与线性回归类似(见下图),区别在于,二分类神经网络的输出层上,在加和函数的基础上,增加了一个连接函数(图中为Sigmoid函数)。
单层神经网络:二分类问题下面,我们用与门(And Gate)问题的例子,来介绍如何实现二分类神经网络的正向传播。
【例2】与门
x0 | x1 | x2 | andgate |
---|---|---|---|
1 | 0 | 0 | 0 |
1 | 1 | 0 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 1 | 1 |
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
y = torch.tensor([0, 0, 0, 1], dtype=torch.float32)
w = torch.tensor([-0.2,0.15,0.15], dtype=torch.float32)
def LogisticR(X, w):
z_hat = torch.mv(X, w)
sigma = torch.sigmoid(z_hat)
y_hat = torch.tensor([int(x) for x in sigma>=0.5], dtype=torch.float32)
return sigma, y_hat
sigma, y_hat = LogisticR(X, w)
print(sigma)
print(y_hat)
tensor([0.4502, 0.4875, 0.4875, 0.5250])
tensor([0., 0., 0., 1.])
常见连接函数:Sign、ReLU、Tanh
符号函数Sign
符号函数的曲线如下图:
import numpy as np
import matplotlib.pyplot as plt
x = np.array([-10, 0, 0, 0, 10])
y = np.array([-1, -1, 0, 1, 1])
plt.figure(figsize=(10,6))
plt.plot(x, y)
Sign
符号函数也被称为阶跃函数。此处,使用而非来表示输出结果,是因为输出结果直接为,就相当于类别标签了;而sigmoid函数输出的是一个0-1之间的数值,需要通过阈值将其转化为这样的标签。
符号函数也可将取值为0的结果直接合并:
等号被合并于上方或下方都可以。
由于,符号函数还可以被转化为以下式子:
或
此时,就是一个阈值,我们可以用任意字母代替它(比如)。
# 在二分类神经网络中使用符号函数
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
y = 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):
z_hat = torch.mv(X, w)
y_hat = torch.tensor([int(x) for x in z_hat>0], dtype=torch.float32)
return y_hat
y_hat = LinearRwithsign(X, w)
print(y_hat)
tensor([0., 0., 0., 1.])
ReLU (Rectified Linear Unit)
ReLU 函数在神经网络中很受欢迎。
ReLU函数的曲线如下图:
import numpy as np
import matplotlib.pyplot as plt
x = np.array([-10, 0, 10])
y = np.array([0, 0, 10])
plt.figure(figsize=(10,6))
plt.plot(x, y)
ReLU
值得注意的是:ReLU函数的导数为符号函数,即当输入时,导数为1,当输入时,导数为0,在处不可导。
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
y = torch.tensor([0, 0, 0, 1], dtype=torch.float32)
w = torch.tensor([-0.2,0.15,0.15], dtype=torch.float32)
def LinearRwithReLU(X, w):
z_hat = torch.mv(X, w)
sigma = torch.tensor([max(0,x) for x in z_hat], dtype=torch.float32)
y_hat = torch.tensor([int(x) for x in sigma>0.5], dtype=torch.float32)
return sigma, y_hat
sigma, y_hat = LinearRwithReLU(X,w)
print(sigma)
print(y_hat)
tensor([0.0000, 0.0000, 0.0000, 0.1000])
tensor([0., 0., 0., 0.])
Tanh (hyperbolic tangent)
Tanh函数的曲线如下图:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-10,10,100)
y = (np.exp(2*x)-1)/(np.exp(2*x)+1)
plt.figure(figsize=(10,6))
plt.plot(x, y)
Tanh
Tanh函数与Sigmoid函数形状相似,Tanh函数将任意实数映射至区间。
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
y = torch.tensor([0, 0, 0, 1], dtype=torch.float32)
w = torch.tensor([-0.2,0.15,0.15], dtype=torch.float32)
def LinearRwithtanh(X, w):
z_hat = torch.mv(X, w)
sigma = torch.tanh(z_hat)
y_hat = torch.tensor([int(x) for x in sigma>=0], dtype=torch.float32)
return sigma, y_hat
sigma, y_hat = LinearRwithtanh(X, w)
print(sigma)
print(y_hat)
tensor([-0.1974, -0.0500, -0.0500, 0.0997])
tensor([0., 0., 0., 1.])
torch.nn 实现单层二分类网络的正向传播
使用sigmoid函数:
import torch
from torch.nn import functional as F
torch.random.manual_seed(54)
dense = torch.nn.Linear(2, 1)
X = torch.tensor([[0,0],[1,0],[0,1],[1,1]], dtype=torch.float32)
y = torch.tensor([0, 0, 0, 1], dtype=torch.float32)
z_hat = dense(X)
sigma = torch.sigmoid(z_hat)
print(sigma)
y_hat = [int(x) for x in sigma>=0.5]
print(y_hat)
tensor([[0.6664],
[0.6871],
[0.7109],
[0.7299]], grad_fn=<SigmoidBackward>)[1, 1, 1, 1]
此处使用 dense 作为 torch.nn.Linear 输出结果的变量名。在神经网络中,dense 经常作为紧密连接层(上层大部分神经元都与这一层神经元相连)的变量名。
使用其他连接函数:
import torch
from torch.nn import functional as F
torch.random.manual_seed(54)
dense = torch.nn.Linear(2, 1)
X = torch.tensor([[0,0],[1,0],[0,1],[1,1]], dtype=torch.float32)
y = torch.tensor([0, 0, 0, 1], dtype=torch.float32)
z_hat = dense(X)
# sign
print(torch.sign(z_hat))
tensor([[1.], [1.], [1.], [1.]], grad_fn=<SignBackward>)
# ReLU
print(F.relu(z_hat))
tensor([[0.6920], [0.7865], [0.8998], [0.9943]], grad_fn=<ReluBackward0>)
# tanh
print(torch.tanh(z_hat))
tensor([[0.5993], [0.6564], [0.7162], [0.7592]], grad_fn=<TanhBackward>)
多分类神经网络
二分类神经网络分类标签通常为标签0,标签1;多分类神经网络中标签通常为。
在二分类神经网络中,输出层只有一个神经元(通常是分类标签为1的概率)。多分类神经网络中,神经元个数与标签类别个数相同,比如十分类,则输出层有10个神经元,每个神经元输出该分类的概率。
多分类神经网络的结构如下图:
单层神经网络:多分类问题Softmax函数
- 分子为多分类情况下某个标签类别的回归结果的指数函数;
- 分母为所有标签类别的回归结果的指数函数之和。
在pytorch中,我们通常不会手写softmax函数,因为指数函数容易因数值巨大而造成“溢出”。可直接调用 torch.softmax 来计算。
由于指数函数是一个单调函数,在使用softmax函数前后并不会改变各个类别输出值的相对大小,因此,无论是否使用softmax,我们都可以通过值大小来判断样本将会被归集为哪一个类别。在神经网络中,如果不需要了解具体类别的分类概率,只需知道最终分类结果,则可以省略softmax函数。
Pytorch 中 Softmax 函数的维度参数
Softmax函数只能对单一维度进行计算,它只能够识别单一维度上的不同类别,但我们输入softmax的张量却可能是一个很高维的张量。
import torch
s = torch.tensor([[[1,5],[3,4],[5,7]], [[0,1],[2,4],[3,7]]], dtype=torch.float32)
print(s.ndim)
print(s.shape)
3
torch.Size([2, 3, 2])
print(torch.softmax(s, dim=0))
# 在张量第0维度,有两个二维张量,
# 每个二维张量上对应位置的元素为一组分类
tensor([[[0.7311, 0.9820],
[0.7311, 0.5000],
[0.8808, 0.5000]],[[0.2689, 0.0180],
[0.2689, 0.5000],
[0.1192, 0.5000]]])
print(torch.softmax(s, dim=1))
# 在张量第1维度,有三个一维张量,
# 每个一维张量上对应位置的元素为一组分类
tensor([[[0.0159, 0.1142],
[0.1173, 0.0420],
[0.8668, 0.8438]],[[0.0351, 0.0024],
[0.2595, 0.0473],
[0.7054, 0.9503]]])
print(torch.softmax(s, dim=2))
# 在张量第2维度,有两个零维张量,
# 每个零维张量上对应位置的元素为一组分类
tensor([[[0.0180, 0.9820],
[0.2689, 0.7311],
[0.1192, 0.8808]],[[0.2689, 0.7311],
[0.1192, 0.8808],
[0.0180, 0.9820]]])
使用 nn.Linear 和 functional 实现多分类神经网络的正向传播
import torch
from torch.nn import functional as F
torch.random.manual_seed(54)
X = torch.tensor([[0,0], [1,0], [0,1], [1,1]], dtype=torch.float32)
dense = torch.nn.Linear(2,3)
z_hat = dense(X)
print(z_hat)
tensor([[ 0.2551, -0.0173, -0.4200],
[ 0.3496, 0.6747, -0.6627],
[ 0.4629, 0.1867, 0.0409],
[ 0.5574, 0.8787, -0.2018]], grad_fn=<AddmmBackward>)
sigma = torch.softmax(z_hat, dim=1)
print(sigma)
tensor([[0.4404, 0.3354, 0.2242],
[0.3640, 0.5038, 0.1323],
[0.4142, 0.3142, 0.2716],
[0.3513, 0.4843, 0.1644]], grad_fn=<SoftmaxBackward>)
网友评论