PyTorch深度学习笔记(2):单层神经网络的正向传播

本文将从一个简单的线性回归问题出发,构建单层神经网络,并手动实现它的正向传播。同时,我们将介绍如何使用PyTorch中的核心模块torch.nn来构建该线性回归的神经网络。最后,我们将问题拓展至二分类多分类的情景,以及它们的代码实现。

线性回归下单层神经网络的正向传播

在深度学习中,线性回归方程通常被表达为:
\hat{z}_{i} = b + w_1 x_{i1} + ... + w_n x_{in}

  • \hat{z} 表示线性回归的预测结果,z 表示线性回归的真实标签;
  • 此处使用 z 而非 y 来表示线性回归的标签,是因为在深度学习中,y 通常用于表示模型结果标签,无论是分类问题(y 是离散的整数),还是回归问题(y 为连续数值)。线性回归的结果一般为深度学习模型的中间结果,因此用 z 来表示,以示区别。

写成矩阵的形式:
\hat{\boldsymbol{z}} = \boldsymbol{Xw}
其中,
\boldsymbol{X} = \left[ \begin{matrix} 1 & x_{11} & x_{12} & ... & x_{1n} \\ 1 & x_{21} & x_{22} & ... & x_{2n} \\ \vdots & \vdots & \vdots & ... & \vdots \\ 1 & x_{m1} & x_{m2} & ... & x_{mn} \end{matrix} \right]
\hat{\boldsymbol{z}} = \left[ \begin{matrix} \hat{z}_{1} \\ \hat{z}_{2} \\ \vdots \\ \hat{z}_{m} \end{matrix} \right]
\boldsymbol{w} = \left[ \begin{matrix} b \\ w_{1} \\ \vdots \\ w_{n} \end{matrix} \right]

下面我们用一个简单的线性回归例子,来阐述最基础的神经网络的架构。它的数据如下:

【例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

上述线性回归例子中,包含两个特征 x_1x_2,即该线性回归模型为:
\hat{z} = b + x_{1}w_{1} + x_{2}w_{2}

我们可以用如下神经网络来描述它:

单层神经网络:线性回归问题

以上是一个最简单的单层回归神经网络的表示图。在神经网络中,圆圈代表神经元,竖着排列在一起的神经元构成了一层神经网络。从图上看,线性回归模型似乎包含了两层神经网络(输入层和输出层),然而神经网络的输入层通常不计入神经网络的层数,因此我们称上图网络为单层神经网络。神经网络的层与层之间由带有参数的线条相连接,将左侧各神经元上的值(此例中为:1x_1x_2)分别与其对应的参数(此例中为:bw_1w_2相乘,得到的相乘的结果(此例中为:bx_1 w_1x_2 w_2)由相连的线条输送至下一层的神经元上,并进行加和(用符号\Sigma表示),即可得到右侧神经元上的预测值\hat{z},即\hat{z} = b + x_{1}w_{1} + x_{2}w_{2}

本例中,左侧层为输入层,由承载数据用的神经元组成,数据从这里输入,并流入处理数据的神经元中。在所有神经网络中,输入层只有一层,且每个神经元上只能承载一个特征(一个x)或者一个常量(通常为1)。常量仅被用来乘以偏差b。对于没有偏差的线性回归,可以不设置常量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函数

\sigma = Sigmoid(z) = \frac{1}{1+e^{-z}}

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 函数特点:

  • 能将任意实数映射到(0, 1)区间;
  • z远离0的区域,Sigmoid(z)趋近于0或者1
  • 中心对称的S曲线;
  • 平滑,处处可导。
    经过sigmoid函数转换后得到的值都在(0, 1)区间内,通常我们可以将这个值作为分类为1的概率,比如:转换后的\sigma = 0.6,则分类为1的概率为0.6(即分类为0的概率为1-0.6=0.4)。

几率(odds)

几率\frac{\sigma}{1-\sigma}:事件发生的概率与事件不发生的概率的比值。

对数几率(log odds)

\log(\frac{\sigma}{1-\sigma}) = \boldsymbol{Xw}

实现二分类网络的正向传播

二分类神经网络的结构与线性回归类似(见下图),区别在于,二分类神经网络的输出层上,在加和函数\Sigma的基础上,增加了一个连接函数(图中为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

y = \left\{ \begin{aligned} 1 & &\text{if } z \gt 0 \\ 0 & &\text{if } z = 0 \\ -1 & &\text{if } z \lt 0 \end{aligned} \right.

符号函数的曲线如下图:

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

符号函数也被称为阶跃函数。此处,使用y而非\sigma来表示输出结果,是因为输出结果直接为-1, 0, 1,就相当于类别标签了;而sigmoid函数输出的\sigma是一个0-1之间的数值,需要通过阈值将其转化为0, 1这样的标签。
符号函数也可将取值为0的结果直接合并:
y = \left\{ \begin{aligned} 1 & &\text{if } z \gt 0 \\ 0 & &\text{if } z \le 0 \end{aligned} \right.
等号被合并于上方或下方都可以。

由于z = w_1 x_1 + w_2 x_2 + b,符号函数还可以被转化为以下式子:

y = \left\{ \begin{aligned} 1 & &\text{if } w_1 x_1 + w_2 x_2 + b \gt 0 \\ 0 & &\text{if } w_1 x_1 + w_2 x_2 + b \le 0 \end{aligned} \right.

y = \left\{ \begin{aligned} 1 & &\text{if } &w_1 x_1 + w_2 x_2 \gt -b \\ 0 & &\text{if } &w_1 x_1 + w_2 x_2 \le -b \end{aligned} \right.
此时,-b就是一个阈值,我们可以用任意字母代替它(比如\theta)。

# 在二分类神经网络中使用符号函数
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 函数在神经网络中很受欢迎。

\sigma = \max(0, z)

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函数的导数为符号函数,即当输入z>0时,导数为1,当输入z<0时,导数为0,在z=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)

\sigma = \frac{e^{2z}-1}{e^{2z}+1}

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函数将任意实数映射至(-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 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, +\infty)

在二分类神经网络中,输出层只有一个神经元(通常是分类标签为1的概率)。多分类神经网络中,神经元个数与标签类别个数相同,比如十分类,则输出层有10个神经元,每个神经元输出该分类的概率\sigma_{1}, \sigma_{2}, ..., \sigma_{10}

多分类神经网络的结构如下图:

单层神经网络:多分类问题

Softmax函数

\sigma_{k} = \frac{e^{z_k}}{\sum_{j=1}^{K}e^{z_j}}

  • 分子为多分类情况下某个标签类别的回归结果的指数函数;
  • 分母为所有标签类别的回归结果的指数函数之和。
    在pytorch中,我们通常不会手写softmax函数,因为指数函数容易因数值巨大而造成“溢出”。可直接调用 torch.softmax 来计算。

由于指数函数是一个单调函数,在使用softmax函数前后并不会改变各个类别输出值的相对大小,因此,无论是否使用softmax,我们都可以通过z值大小来判断样本将会被归集为哪一个类别。在神经网络中,如果不需要了解具体类别的分类概率,只需知道最终分类结果,则可以省略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>)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容