深度学习
- 1.神经网络原理
- 1.1神经元模型
- 1.2神经网络结构
- 1.3隐藏层
- 1.3.1激活函数层
- 1.4输出层
- 1.4.1softmax层
- 1.5损失函数
- 1.6反向传播
- 2.多层感知机
- 2.1线性网络的局限性
- 2.2引入非线性
- 2.3多层感知机(Multi-Layer Perceptron,MLP)
- 2.4激活函数(Activation Function)
- 2.4.1Sigmoid函数
- 2.4.2Tanh函数
- 2.4.3ReLU函数
- 2.4.4Softmax函数
- 3.前向传播和反向传播
- 3.1前向传播
- 3.2损失函数
- 3.3反向传播
- 3.3.1反向传播原理
- 3.3.2最小化损失函数
- 4.多层感知机代码实现
- 4.1MNIST数据集
- 4.2Pytorch搭建神经网络
- 4.2.1导入数据
- 4.2.2创建网络结构
- 4.2.3定义损失函数
- 4.2.4创建优化器
- 4.2.5训练模型
- 4.2.6测试模型
- 4.2.7保存模型参数
- 5.回归问题
- 5.1一元线性回归
- 5.2多元线性回归
- 5.3多项式回归
- 5.4线性回归代码实现
- 5.4.1数据生成
- 5.4.2设置超参数
- 5.4.3初始化参数
- 5.4.3开始训练
- 5.4.4可视化
- 5.4.5利用Pytorch实现线性回归
- 6.分类问题
- 6.1分类任务的定义
- 6.2机器学习算法流程
- 6.3多分类问题的数学表示
- 6.4Softmax回归
- 6.4损失函数
- 6.4.1对数损失函数
- 6.4.2交叉熵损失函数
- 7.多分类问题代码实现
- 7.1加载MNIST数据集
- 7.2数据加载器
- 7.3构建网络
- 7.4定义损失函数和优化器
- 7.5模型评估
- 7.6模型训练
1.神经网络原理
1.1神经元模型
神经网络是一种数学模型,下图是一个最简单的神经元,通常用带有输入输出的圆圈来表示,虽然其名为神经元,但是其内在是线性模型。
神经元模型用数学公式表示为
y
=
f
(
∑
i
=
1
n
w
i
x
i
+
b
)
y=f(\sum_{i=1}^{n}w_ix_i+b)
y=f(∑i=1nwixi+b),其中
w
i
x
i
+
b
w_ix_i+b
wixi+b是直线方程,
w
w
w为斜率,也被称为神经元连线上的权重,整个式子描述的是从输入的
x
1
x_1
x1至
x
n
x_n
xn到输出
y
y
y之间的映射关系。其中
b
b
b截距,是bias的首字母;
n
n
n是输入的个数,对应神经元的个数;
f
f
f是激活函数,是一个非线性的变换。式子也通常被写为矩阵的形式
y
=
f
(
wx
+
b
)
y=f(\textbf{wx}+b)
y=f(wx+b)。
神经元模型也被称作感知器模型或感知机模型(Perception)。
1.2神经网络结构
神经网络,顾名思义,就是由很多个神经元节点前后相连组成的。虽然长相像是网络,其实就是很多个线性模型的模块化组合,一般来说有三层,最左边是输入层,最右边是输出层,中间是隐藏层。
虽然输入层也用圆圈表示,但是经常不算,因为它本身就是样本本身,其维度就是样本的维度;输出层是样本的类别标签;隐藏层至少一个,也可以多层,它才是神经网络中最重要的一层。神经元的个数和层数越多或者说模型结构越复杂,就变成了我们经常听到的深度学习。多少层算深呢,其实几层也叫深层网络,但是多数的情况下会有几十、几百乃至上千层,层数和神经元的个数并不是越多越好,可能会发生过拟合问题,泛化能力不好。至于需要多少神经元和多少层,需要具体问题具体分析。
单个神经元的别名叫做感知器(Perception),神经网络在发展的早期,一度被称为多层感知机(Multilayer Perception),简称MLP。
1.3隐藏层
如果输入的是一张32×32像素的灰度图像,那么输入层的维度就是32×32,有的时候为了方便处理,会将其reshape成列向量来表示。为了演示方便,先假设它是一个1×2的向量,连接输入层和隐藏层的是
w
1
w_1
w1和
b
1
b_1
b1,那么隐藏层
H
=
w
1
x
+
b
1
H=w_1x+b_1
H=w1x+b1。假定隐藏层是50维,也可以理解成50个神经元,那么矩阵
H
\textbf{H}
H的大小就是1×50。连接隐藏层和输出层的是权重
w
2
w_2
w2和
b
2
b_2
b2,同样通过矩阵运算可以得到
Y
=
w
2
H
+
b
2
\textbf {Y}=\textbf{w}_2\textbf{H}+b2
Y=w2H+b2,维度假定是1×4。通过两个线性方程的计算,最终得到输出
Y
\textbf{Y}
Y,并且可以将两个式子用一个式子来表示。
对于两层的神经网络如此,深度加到100层依旧是如此,如此一来神经网络就失去了意义,在神经网络理论中,管这种现象叫做退化,也就是说,随着网络层数的增加,网络的能力或者进行分类的准确率并没有发生明显的变化,甚至可能下降,这就像不思进取,所以叫做退化。因此,这里要对网络注入灵魂,就是所谓的激活层。
1.3.1激活函数层
激活函数的英文是activation,作用是将线性变成非线性,比如下面的阶跃函数、Sigmoid函数、Relu函数。Relu函数在神经网络里较为常用的激活函数。对激活函数的要求是其可导。
假如经过
H
=
w
i
x
+
b
\textbf{H}=w_ix+b
H=wix+b计算得到
H
\textbf{H}
H的值为{1,-8,5,-4,7…},经过一个Relu激活函数就会变成{1,0,5,0,7…}负的值将变成0。每个隐藏层计算(矩阵线性计算)后都需要加一个激活层,否则该层线性计算是没有意义的。没有将隐藏层和激活函数合并的意义是,极大地简化学习步骤。
1.4输出层
如下所示神经网络多了子层——激活函数,下图已经构成了神经网络的整个过程,求得的
Y
\textbf{Y}
Y中最大值就是当前的分类,但是当前的输出是随机值,不够直观,需要归一化。
1.4.1softmax层
将结果
Y
\textbf{Y}
Y输出为每个分类的概率值,可以用
y
i
=
e
i
∑
j
e
j
y_i=\frac{e^i}{\sum_{j}e^j}
yi=∑jejei来实现。对所有的元素求指数次幂求和作分母,每个元素的指数幂做分子,那么
Y
\textbf{Y}
Y中所有的元素和为1,每一个元素将代表概率值。经过这样运算的层就是softmax层。例如得到的结果是[0.9,0.06,0.02,0.02],真实分类结果应该是[1,0,0,0]只有第一个为1,其余全为0。
1.5损失函数
虽然结果可以正确分类,但是和真实结果有一定差距,需要对输出结果好坏进行量化。一种简单直接的方法就是用1减去softmax输出的概率;更为巧妙地一种方法是求对数的负数,结果越接近于0,结果越准确,这就是交叉熵损失函数(cross entropy error)。训练神经网络的目的是为了使损失尽可能地小。
不同神经网络之间地差异主要体现在:
1.网络模型结构不同;
2.损失函数不同;
3.动态求解损失函数过程不同;
4.对过程中,出现的过拟合等问题解决方式不同。
1.6反向传播
反向传播就是利用梯度下降法寻找最优参数(权重w和偏置b),训练样本有很多过程,就好比下山练习、一遍遍总结经验,最后找到最优路径。
2.多层感知机
2.1线性网络的局限性
线性组合无法表示复杂的非线性数据。如下图左图所示,蓝色以上是一类,绿色以下是另一类,如果分界线用线性的最好的结果只能如右图所示,显然此边界并不能很好地对这两类问题进行分类。
神经元线性模型公式:
h
1
=
W
1
x
+
b
1
h
2
=
W
2
h
1
+
b
2
=
W
2
(
W
1
x
+
b
1
)
+
b
2
=
W
2
W
1
x
+
W
2
b
1
+
b
2
=
W
′
x
+
b
′
\begin{align*} h_1 &=W_1x+b_1\\ h_2&=W_2h_1+b_2\\ &=W_2(W_1x+b_1)+b_2\\ &=W_2W_1x+W_2b_1+b_2\\ &=W'x+b' \end{align*}
h1h2=W1x+b1=W2h1+b2=W2(W1x+b1)+b2=W2W1x+W2b1+b2=W′x+b′
从上式可以看出,线性模型地组合依然是线性地模型,无法表示复杂地非线性数据。在实际情况中,大多数问题都是非线性地,因此机器学习要解决的核心问题就是如何用线性的模型去逼近或者解决非线性的问题。
2.2引入非线性
引入非线性就用到了激活函数(activation function),激活的意思就是产生非线性的变换。引入非线性因素,就是为了提高神经网络的表示能力,从而更好地模拟真实世界地复杂数据。非线性地空间变换将原始空间中的数据变换到高维中去,具备更好的信息可分性,比如说SVM知识向量机,它就是一个典型的例子,只不过它是使用核技巧来实现的非线性空间变换,而神经网络是通过增加隐藏层和隐藏层的神经单元来实现。
h
=
f
(
W
h
h
p
r
e
v
+
b
h
)
\begin{align*} h=f(W_hh_{prev}+b_h) \end{align*}
h=f(Whhprev+bh)
下面的例子,最左边是线性空间,经过特征空间的扭曲变换,到达第三层后非线性不可分的数据变得线性可分了,使得神经网络在高维空间中能够更好地捕捉非线性数据的特征,从而实现更好的预测结果。
2.3多层感知机(Multi-Layer Perceptron,MLP)
多层感知机是一种前馈的、向前传播的神经网络。下图是简单的三层神经网络,输入层
x
∈
R
n
x \in R^n
x∈Rn,隐藏层
W
1
∈
R
m
×
n
,
b
1
∈
R
n
W_1 \in R^{m×n},b_1 \in R^n
W1∈Rm×n,b1∈Rn,输出层
W
2
∈
R
m
×
k
,
b
2
∈
R
k
W_2 \in R^{m×k},b_2 \in R^k
W2∈Rm×k,b2∈Rk
h
=
f
(
W
1
x
+
b
1
)
y
=
f
y
(
W
2
T
h
+
b
2
)
\begin{align*} h&=f(W_1x+b_1)\\ y&=f_y(W_2^Th+b_2) \end{align*}
hy=f(W1x+b1)=fy(W2Th+b2)
隐藏层是多层感知机中最重要的一层,不能直接与输入输出打交道,主要的作用是学习数据的高级表示,提高模型的准确度。
多层感知机的隐藏层可以不止一层,以下是三层隐藏层的多层感知机。多分类问题输出层的
f
y
f_y
fy多用softmax。
h
1
=
f
1
(
W
1
x
+
b
1
)
h
2
=
f
2
(
W
2
x
+
b
2
)
h
3
=
f
3
(
W
3
x
+
b
3
)
y
=
f
y
(
W
4
h
3
+
b
4
)
\begin{align*} h_1&=f_1(W_1x+b_1)\\ h_2&=f_2(W_2x+b_2)\\ h_3&=f_3(W_3x+b_3)\\ y&=f_y(W_4h_3+b_4) \end{align*}
h1h2h3y=f1(W1x+b1)=f2(W2x+b2)=f3(W3x+b3)=fy(W4h3+b4)
隐藏层的数量和大小会影响整个神经网络的性能,如果层数和每层神经元的个数都比较少,整个模型的表现能力都比较弱,可能没办法准确拟合训练数据;而如果层数和数量比较多,表示能力虽然强,但是容易出现过拟合(在训练数据表现良好,在测试数据表现不佳),这就是训练过度的情况。因此在设计多层感知机的时候,需要进行综合考虑隐藏层的数量和大小,以便达到较好的模型性能。
2.4激活函数(Activation Function)
如果没有激活函数,不管神经网络有多深,它依然是线性的,可以直接替换成反射变换(Affine Function),只能做空间的方向性的选中、伸缩、平移等等。有了激活函数就可以对特征空间进行扭曲、翻转,在扭曲了、反转了的高维空间中,原来线性的数据反倒变成线性可分了,这就是神经网络的灵魂、本质。
2.4.1Sigmoid函数
Sigmoid函数是“S”型曲线,将输入值压缩到0~1,很大的负数变为0,很大的正数变为1。在输出值不是0或者1的情况下,Sigmoid函数具有很好的非线性,适用于二分类问题,是逻辑回归中特别常用的激活函数。缺点就是输出值处于0或者1的时候,两部分梯度几乎为零,容易导致梯度消失,也就是在深度网络的传递过程中,相乘的结果解决于0 ;另一个缺点是它的输出不是以0为中心,而是以0.5为中心<,因此除了用于二分类问题外,已经比较少使用Sigmoid函数。
f
(
x
)
=
1
1
+
e
−
x
\begin{align*} f(x)=\frac{1}{1+e^{-x}} \end{align*}
f(x)=1+e−x1
下图左侧非线性数据使用Sigmoid激活函数产生了空间扭曲,再投影回线性空间,就找到了边界。所以经过激活函数进行扭曲变换之后,在高维的非线性空间中,变成了线性可分。
2.4.2Tanh函数
Tanh函数是一种双曲正切函数,可以看作是经过简单放法加平移后的Sigmoid函数的改进版,输出值域变为(1-,1),更适合神经网络的输入层和隐藏层,因为这两成需要在更广的范围内进行变化;其次是Tanh函数是以0为中心,当输入为0时,输出也为0,这种特性对于梯度下降法这样的优化算法非常重要,因为它有助于减少训练的时间和提高模型的性能;第三,和Sigmoid函数相比具备更快的收敛速度。
Tanh函数仍然是非常常用的激活函数之一,特别是在循环神经网络RNN和长短时机翼LSTM中。缺点是当输入值较大时,容易导致梯度消失,在实际使用中,需要根据具体的问题进行选择。
f
(
x
)
=
e
x
−
e
−
x
e
x
+
e
−
x
\begin{align*} f(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}} \end{align*}
f(x)=ex+e−xex−e−x
下面是将输入数据经过Tanh函数映射到高维空间,将原本不能线性可分数据变得线性可分。对比Sigmoid函数,基本上很类似,但是边界上略微有所不同。
2.4.3ReLU函数
ReLU(Rectified Linear Unit的简称)函数,是一种最大值函数,当输入值大于零时,当输入值x大于0时,为x本身,当x小于0时,就直接等于0。
ReLU函数和Tanh函数、Sigmoid函数相比具有的优点为:一是其导数始终为0或1,不会出现梯度消失的我呢提,这就解决了Sigmoid函数和Tanh函数在深层网络中梯度消失的问题;其次,在计算上比Sigmoid和Tanh函数更快,因为它不需要指数的运算,指数运算非常占计算资源;第三是相比于其他的激活函数,Tanh函数更加文档,默认是第一选择。 缺点是当输入值为负数时,函数值为0,这将导致网络中某些权重不能更新,称之为Dying ReLU问题,从而导致网络难以训练。为了解决这个问题,出现了很多改进版的ReLU函数,比如说LeakyReLU增加了一个小小的复数斜率,。
f
(
x
)
=
m
a
x
(
0
,
x
)
\begin{align*} f(x)=max(0,x) \end{align*}
f(x)=max(0,x)
下图是ReLU函数对非线性数据,在高维空间进行非线性激活变换,实现线性可分。虽然和Sigmoid、Tanh函数不太一样,没有很光滑,但是可对两类数据进行分类。
2.4.4Softmax函数
Softmax函数是将输入值映射到一个概率分布上,输入是一个向量,输出也是一个向量,每个输出值都是一个概率值,累加和为1。因为输入输出都是向量,不是标量,所以无法用单个坐标轴来画出Softmax函数。
Softmax函数主要用在多分类问题中,在神经网络的最后一层把输出值转化为概率值,使得输出具有很好的解释性,便于解释预测的结果。
3.前向传播和反向传播
3.1前向传播
正常情况下,神经网络的信息流是从前往后前向传播。输入的数据,经过线性的加权组合、激活函数,实现非线性的变换,在不同的层级之间反复迭代,直到最后的输出。这样的网络常常被称为深度前馈网络(Deep Feedforward Network) 或者前馈神经网络(Feedforward Neural Network)。前面提到的多层感知机就是最典型的模型,需要注意的是神经网络允许跳层,但是不允许横向、倒向跳,是一个前向有向图。
某一层隐藏层非线性变换如下所示:
h
=
f
(
W
h
h
p
r
e
v
+
b
h
)
y
=
f
y
(
W
y
h
m
+
b
y
)
\begin{align*} h&=f(W_hh_{prev}+b_h)\\ y&=f_y(W_yh_m+b_y) \end{align*}
hy=f(Whhprev+bh)=fy(Wyhm+by)
其中,第一个式子中的KaTeX parse error: Expected 'EOF', got '}' at position 4: W_h}̲为权重矩阵,
h
p
r
e
v
h_{prev}
hprev是前一时刻的输出,
b
h
b_{h}
bh为偏置,
f
f
f是激活函数,
h
h
h是这一层的输出。第二个式子表达了最后的数据,其中
f
y
f_y
fy是输出的激活函数,如果是分类问题,可以用Softmax函数。
3.2损失函数
神经网络的训练往往是以随机的参数开始,初始的输出结果往往都不如人意。神经网络训练过程中,经常会用损失函数来衡量预测结果和真实结果之间的差距。比如说常用的均方误差(Mean Squared Error,简称MSE)。
M
S
E
=
1
n
∑
i
=
1
n
(
y
i
−
y
i
^
)
2
\begin{align*} MSE=\frac{1}{n} \sum_{i=1}^{n}(y_i-\hat{y_i})^2 \end{align*}
MSE=n1i=1∑n(yi−yi^)2
其中,
y
i
y_i
yi是第i个样本的真实值,
y
i
^
\hat{y_i}
yi^是模型预测的输出值。以下图线性回归为例,紫色的点是真实数据,蓝色的是预测的值,它们之间y方向的垂直距离就是误差,误差的平方和再求一个平均,就是MSE。
通常,我们希望通过训练使得损失函数的值尽可能地小,因此需要找到一种方法来有效地更新网络地权重和偏置(也就是模型参数),使得损失函数不断减小。
3.3反向传播
反向传播(Back Propagation),本质上是计算神经网络每一层梯度地方法,梯度也就是偏导数(gradient)。利用链式法则,从最后一层地损失函数开始,逐层往前,计算各个神经元权重和偏置模型参数的偏导数构成损失函数,将权重和偏置向量的梯度作为修改模型参数的依据。
从理论上讲是,一个损失的倒查分解,从输出开始,逐渐往前找产生错误的原因,看哪个神经元担的责任大,然后把误差进行倒查分解。
3.3.1反向传播原理
假设
i
i
i和
j
j
j是前后连接的两个神经元,是两个隐藏层。用
x
x
x和
y
y
y表示输入和输出。从下图可以看到,
i
i
i的输出
y
i
y_i
yi乘上权重
w
j
i
w_{ji}
wji得到了
x
j
x_{j}
xj,
x
j
x_j
xj其实是所有链接的加权和,这里仅列出了一条线,有的时候也会加上偏置
b
b
b,偏置起到的作用其实就是个阈值,对于求导而言,常数项不起作用。经过激活函数
f
f
f得到
y
j
y_j
yj,这里以Sigmoid函数为例。每个神经元其实可以分成两部分,前面是线性组合,后面是激活函数。神经网络的目标就是找到一组权重参数能够确保任何一个输入向量
x
x
x产生的输出向量
y
y
y都能和想要的目标输出向量
d
j
d_j
dj完全一样或者足够地接近。
x
j
=
∑
i
y
i
w
j
i
y
i
=
f
(
∑
i
y
i
w
j
i
+
b
)
f
(
x
)
=
1
1
+
e
−
x
\begin{align*} x_j&=\sum_{i}y_iw_{ji}\\ y_i&=f(\sum_iy_iw_{ji}+b)\\ f(x)&=\frac{1}{1+e^{-x}} \end{align*}
xjyif(x)=i∑yiwji=f(i∑yiwji+b)=1+e−x1
3.3.2最小化损失函数
在3.3.1小节例子中,以均方误差MSE为例,下式的
y
j
,
c
y_{j,c}
yj,c表示网络预测输出,
d
j
,
c
d_{j,c}
dj,c表示样本类别的真实值,二者做差求平方,在求和。两个求和符号,其中
c
c
c是所有训练样本的索引,
j
j
j是输出神经元的索引,按道理来讲,前面还有一个
1
c
\frac{1}{c}
c1, 对于求导而言,不是很重要,所以将其省略了,而是替换了一个常数
1
2
\frac{1}{2}
21,主要是为例求导以后,平方正好和
1
2
\frac{1}{2}
21消除掉。
E
=
1
2
∑
c
∑
j
(
y
j
,
c
−
d
j
,
c
)
2
\begin{align*} E=\frac{1}{2}\sum_c \sum_j(y_{j,c}-d_{j,c})^2 \end{align*}
E=21c∑j∑(yj,c−dj,c)2
E
E
E是对所有的训练样本损失求和,也就是总损失和,这个函数方程一般是超越方程,无法直接求解,没有解析解。数值计算中的最优化方法,比如说梯度下降方法,通过求权重的偏导数,逐步地逼近求解,通过这种方式就把一个机器学习地问题,转化成立一个最优化地问题。
a
r
g
m
i
n
E
w
=
1
2
∑
c
∑
j
(
y
j
,
c
−
d
j
,
c
)
2
\begin{align*} \mathop{argminE}\limits_{w}=\frac{1}{2}\sum_c \sum_j(y_{j,c}-d_{j,c})^2 \end{align*}
wargminE=21c∑j∑(yj,c−dj,c)2
将问题写成最优化地式子就是上式,找到参数
w
w
w,去最小化总损失
E
E
E,用的是均方差MSE损失函数。如果不熟悉,需要去补充机器学习或者数值计算最优化的那部分知识。下图神经元
j
j
j是输出层,
i
i
i是隐藏层,从输出
j
j
j倒着反向求偏导数。
通过链式法则一层一层往前去,求各个变量的导数。
∂
E
∂
y
j
=
y
j
−
d
j
∂
E
∂
x
i
=
∂
E
∂
y
j
⋅
d
y
j
d
x
j
f
(
x
)
=
1
1
+
e
−
x
j
⇒
d
y
i
d
x
j
=
y
j
(
1
−
y
j
)
∂
E
∂
w
j
i
=
∂
E
∂
x
j
⋅
∂
x
j
∂
w
j
i
x
j
=
∑
i
y
i
w
j
i
⇒
=
∂
E
∂
x
j
⋅
y
i
∂
E
∂
y
i
=
∑
j
∂
E
∂
x
j
⋅
∂
x
j
∂
y
i
=
∑
j
∂
E
∂
x
j
⋅
w
j
i
\begin{align*} \frac{\partial E}{\partial y_j}&=y_j-d_j\\ \frac{\partial E}{\partial x_i}&=\frac{\partial E}{\partial y_j}·\frac{dy_j}{dx_j}\\ f(x)=\frac{1}{1+e^{-x_j}} \Rightarrow \frac{dy_i}{dx_j}&=y_j(1-y_j)\\ \frac{\partial E}{\partial w_{ji}}&=\frac{\partial E}{\partial x_j}·\frac{\partial x_j}{\partial w_{ji}}\\ x_j=\sum_iy_iw_{ji} \Rightarrow &=\frac{\partial E}{\partial x_j}·y_i\\ \frac{\partial E}{\partial y_{i}}&=\sum_j \frac{\partial E}{\partial x_j}·\frac{\partial x_j}{\partial y_i}\\ &=\sum_j \frac{\partial E}{\partial x_j}·w_{ji} \end{align*}
∂yj∂E∂xi∂Ef(x)=1+e−xj1⇒dxjdyi∂wji∂Exj=i∑yiwji⇒∂yi∂E=yj−dj=∂yj∂E⋅dxjdyj=yj(1−yj)=∂xj∂E⋅∂wji∂xj=∂xj∂E⋅yi=j∑∂xj∂E⋅∂yi∂xj=j∑∂xj∂E⋅wji
4.多层感知机代码实现
4.1MNIST数据集
手写数字识别任务,给定一张手写数字的图片,然后预测图片中是数字几。这里使用的是机器学习中比较有名的MNIST数据集,里面包含60000张训练图片和10000张测试图片,每张图片都是灰度图像,大小是28×28。
4.2Pytorch搭建神经网络
Pytorch搭建神经网络的常规步骤:首先加载数据,接着定义网络模型,然后根据分类问题还是回归问题确定损失函数,再然后是确定优化器,紧接着就可以进行模型训练了,训练好后进行测试,然后进行保存数据(有可视化操作在这一步进行)。
代码在Jupyter中实现。
4.2.1导入数据
MNIST数据集集成到了torchvision包中,torchvision包中还定义了其他公开数据集的相关操作,导入transformer完成数据处理。
# 导入包
import torch
from torchvision import datasets
#导入transformer将数据转成tensor
from torchvision import transforms
#导入神经网络
import torch.nn as nn
#定义优化器
import torch.optim as optim
root
代表数据集存放的位置,这里设置为data下面的mnist文件夹下,train
为True代表加载的是训练集数据,transform
代表对数据集的处理直接转换为tensor,download
设置为True表示当前路径下没有数据,需要先进行下载,如果数据集已经下载,就不会再进行下载了;测试数据集,需要将train
改为False。
train_data = datasets.MNIST(root="data/mnist",train=True,transform=transforms.ToTensor(),download=True)
test_data = datasets.MNIST(root="data/mnist",train=Fasle,transform=transforms.ToTensor(),download=True)
前面提到MNIST数据集训练图片有六万张,我们不能一次性喂给模型进行训练,内存显存吃不消,为了解决这个问题,Pytorch采用了batch_size的方法,从数据集中每次选取一小部分数据进行训练,这里定义batch_size
的大小为100,为了使用batch_size
需要使用torch.utils.data.DataLoader
方法,dataset
为加载的训练集,batch_size
的值为每次加载的大小,shuffle
为Ture表示打乱数据,这样就能去掉因为数据排列可能带来的问题,提高泛化能力;测试数据集做同样处理,数据集改为测试数据集,shuffle
改为False,测试数据集不用打乱顺序。 到此就构造好l数据加载对象。
batch_size = 100
train_loader = torch.utils.data.DataLoader(dataset=train_data,batch_size=batch_size,shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_data,batch_size=batch_size,shuffle=False)
4.2.2创建网络结构
定义一个包含两个隐藏层的MLP网络,class类MLC继承nn.Moudle
,并初始化类。
# 定义MLP网络 继承nn.Module
class MLP(nn.Module):
# 初始化方法
# input_size输入数据的维度
# hidden_size隐藏层的大小
# num_classes输出分类的数量
def __init__(self, input_size, hidden_size, num_classes):
# 调用父类的初始化方法
super(MLP, self).__init__()
# 定义第1个全连接层
self.fc1 = nn.Linear(input_size, hidden_size)
# 定义激活函数
self.relu = nn.ReLU()
# 定义第2个全连接层
self.fc2 = nn.Linear(hidden_size,hidden_size)
# 定义第3个全连接层
self.fc3 = nn.Linear(hidden_size, num_classes)
# 定义forward函数
# x 输入的数据
def forward(self, x):
# 第一层运算
out = self.fc1(x)
# 将上一步的结果发送给激活函数
out = self.relu(out)
# 将上一步结果送给fc2
out = self.fc2(out)
# 同样将结果送给激活函数
out = self.relu(out)
# 将上一步结果传递给fc3
out = self.fc3(out)
# 返回结果
return out
# 定义参数
input_size = 28*28 #输入大小
hidden_size =512 #隐藏层大小
num_classes = 10 #输出大小(类别数)
# 初始化MLP
model = MLP(input_size, hidden_size, num_classes)
4.2.3定义损失函数
定义损失函数:
criterion = nn.CrossEntropyLoss()
4.2.4创建优化器
定义优化器需要定义学习率,优化器使用Adam,传入模型参数和学习率。
learning_rate = 0.001 #学习率
optimizer = optim.Adam(model.parameters(),lr=learning_rate)
在图像处理过程中,一般采用min_batch小批量的决策对数据进行读取和训练。num_epochs
设置为10,为训练10次,里面的for
循环循环读取train_loader
中的数据,每次都是batch_size
大小。将图片大小 转换为向量,传入模型,计算loss损失,然后再进行反向传播,使用step
方法进行更新参数。
4.2.5训练模型
# 训练网络
num_epochs = 10 #训练次数
for epoch in range(num_epochs):
for i,(images, labels) in enumerate(train_loader):
# 将images转成向量
images = images.reshape(-1,28*28)
# 将数据送到网络中
outputs = model(images)
# 计算损失
loss = criterion(outputs, labels)
# 首先将梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
if (i+1)%100==0:
print(f'Epoch[{epoch+1}/{num_epochs}],Step[{i+1}/{len(train_loader)}], Loss:{loss.item():.4f}')
4.2.6测试模型
测试网络中,将测试数据传入网络,输出的是一个概率的结果,在这个维度上取出最大值对应的索引值。
# 测试网络
with torch.no_grad():
correct = 0
total = 0
# 从test_loader中循环读取测试数据
for images, labels in test_loader:
# 将image转成向量
images = images.reshape(-1,28*28);
# 将数据送给网络
outputs = model(images)
# 取出最大值对应的索引,即预测值
_,predicted = torch.max(outputs.data,1)
# 累加label数
total += labels.size(0)
# 预测值与labels值比对,获取预测正确的数量
correct += (predicted == labels).sum().item()
# 打印最终的准确率
print(f'Accuracy of the network on the 10000 test image:{100 * correct / total}%')
4.2.7保存模型参数
最后就是保存模型,利用torch.save
方法传入model以及文件名,保存整个模型。
torch.save(model,"mnist_mlp_model.pkl")
5.回归问题
回归regresssion的真实含义是回归到线,假定一条带有参数的线,无论直线还是曲线,确定一个标准(训练数据点和候选直线或曲线直接的距离),找到差的平方求和就是损失函数(loss function)。用最优化的方法去求解,逼近迭代,得到的参数就是模型参数。
5.1一元线性回归
一元线性回归中的“一元”的意思是数据只有一个特征值当自变量,有很多个特征值时,就叫做多元线性回归。在下图中,有一堆样本点,我们希望找到一条直线
y
=
k
x
+
b
y=kx+b
y=kx+b,问题的核心是找到最好的一条直线,这个问题延申出了数学分支,叫做最优化问题。基本思想是民主投票。衡量的标准是点到线的距离。
点位于直线上下两侧,距离有正负,如果给距离加上绝对值
∣
y
−
y
^
∣
|y-\hat{y}|
∣y−y^∣就是曼哈顿距离,当然也可以用距离的平方(欧氏距离)来计算。
∑
i
=
1
m
(
y
i
−
y
^
i
)
2
\sum_{i=1}^{m}(y_i-\hat{y}_i)^2
∑i=1m(yi−y^i)2,将直线方程代入
∑
i
=
1
m
(
y
i
−
k
x
i
−
b
)
2
\sum_{i=1}^{m}(y_i-kx_i-b)^2
∑i=1m(yi−kxi−b)2,找最优直线就变成了找最优参数
k
k
k和
b
b
b,使得该式值尽可能小,用数学语言表达如下所示,整个式子叫做目标函数,因在很多时候,它衡量了误差的大小, 所以我们也管它叫做损失函数(loss function)。
a
r
g
m
i
n
k
,
b
∑
i
=
1
m
(
y
i
−
k
x
i
−
b
)
2
\begin{align*} \mathop{argmin }\limits_{k,b}\sum_{i=1}^m(y_i-kx_i-b)^2 \end{align*}
k,bargmini=1∑m(yi−kxi−b)2
找到目标函数,利用训练数据反复迭代,求解其中的参数,这就是最优化的核心思想。更具体地来说,现在是凸优化,“凸”主要强调函数连续可导,便于求导梯度。这个过程常常被称为参数学习的过程。
k
k
k和
b
b
b的计算公式如下:
最小二乘法:
k
=
∑
i
=
1
m
(
x
i
−
x
‾
)
(
y
i
−
y
‾
)
∑
i
=
1
m
(
x
u
−
x
‾
)
2
b
=
y
‾
−
k
x
‾
\begin{align*} 最小二乘法:k=\frac{\sum_{i=1}^{m}(x_i-\overline{x})(y_i-\overline{y})}{\sum_{i=1}^{m}(x_u-\overline{x})^2} \quad b=\overline{y}-k\overline{x} \end{align*}
最小二乘法:k=∑i=1m(xu−x)2∑i=1m(xi−x)(yi−y)b=y−kx
其中
x
‾
\overline{x}
x和
y
‾
\overline{y}
y分别是x和y的平均值,上式就是一元回归的解,是最著名的最小二乘法。
5.2多元线性回归
在实际问题中,特征值常常有多个,那么就用到了多元线性回归。处理思,思想非常简单和暴力,将所有特征值赛道一个矩阵中,依然可用二维坐标系来进行可视化,只不过横轴x变为了向量,y变为了如下所示:
y
=
w
0
+
w
1
x
1
+
w
2
x
2
+
.
.
.
+
w
n
x
n
=
w
0
+
x
⋅
w
T
\begin{align*} y&=w_0+w_1x_1+w_2x_2+...+w_nx_n\\ &=w_0+\textbf{x}·\textbf{w}^T \end{align*}
y=w0+w1x1+w2x2+...+wnxn=w0+x⋅wT
其中n是特征的维度,点乘又叫做内积。再进哟不假设一个恒等于1的
x
0
x_0
x0,式子写成如下所示:
y
=
x
⋅
w
T
\begin{align*} y&=\textbf{x}·\textbf{w}^T \end{align*}
y=x⋅wT
将
x
0
x_0
x0塞到了矩阵
x
\textbf{x}
x中,
w
0
w_0
w0塞到了矩阵
w
\textbf{w}
w中。
目标是使距离最小,参数最小化目标函数:
∑
i
=
1
m
(
y
i
−
y
i
^
)
2
\sum_{i=1}{m}(y_i-\hat{y_i})^2
∑i=1m(yi−yi^)2,代入方程如下所示:
a
r
g
m
i
n
w
(
y
−
X
⋅
w
)
T
(
y
−
X
⋅
w
)
\begin{align*} \mathop{argmin }\limits_{w}(\textbf{y}-\textbf{X}·\textbf{w})^T(\textbf{y}-\textbf{X}·\textbf{w}) \end{align*}
wargmin(y−X⋅w)T(y−X⋅w)
在线性代数中,矩阵相乘就可转换成上式,其中
y
\textbf{y}
y和
w
\textbf{w}
w是向量,
X
\textbf{X}
X是一个矩阵:
y
m
×
1
=
[
y
1
⋮
y
m
]
X
m
×
(
n
+
1
)
=
[
1
x
1
,
1
…
x
1
,
n
⋮
1
x
m
,
1
…
x
m
,
n
]
w
(
n
+
1
)
×
1
=
[
w
0
⋮
w
n
]
\begin{align*} \textbf{y}^{m×1}= \begin{bmatrix} y_1 \\ \vdots \\ y_m \end{bmatrix} \textbf{X}^{m×(n+1)}= \begin{bmatrix} 1 & x_{1,1} & \dots&x_{1,n} \\ &&\vdots \\ 1 & x_{m,1} & \dots&x_{m,n} \end{bmatrix} \textbf{w}^{(n+1)×1}= \begin{bmatrix} w_0 \\ \vdots \\ w_n \end{bmatrix} \end{align*}
ym×1=
y1⋮ym
Xm×(n+1)=
11x1,1xm,1…⋮…x1,nxm,n
w(n+1)×1=
w0⋮wn
其中,m是样本的个数,n是特征的维度。多元线性回归正规方程解如下:
w
=
(
X
T
X
)
−
1
X
T
y
\begin{align*} \textbf{w}=(\textbf{X}^T\textbf{X})^{-1}\textbf{X}^T\textbf{y} \end{align*}
w=(XTX)−1XTy
在真正算的时候不会直接使用上面的公式,而是使用梯度下降法这种更加简单的搜素形式。
5.3多项式回归
无论是一元线性回归还是多元线性回归,都是假定样本数据是线性分布的,如果不是线性分布的,就是多项式回归。基本思路也是找一条线,不再是之前的直线,而是下图所示的曲线。
上图是一个二次曲线,写成方程的形式如下所示,可以经过变量替换变成线性回归。
5.4线性回归代码实现
假设输出与输入之间具有如下线性关系,其中
x
x
x为输入,
y
y
y为输出,
w
w
w为权重,
b
b
b为偏置。目标是通过训练样本找到最优的参数
w
w
w和
b
b
b使得模型能够准确的预测新的样本。
y
=
w
x
+
b
\begin{align*} y=wx+b \end{align*}
y=wx+b
为了找到最优参数,定义损失函数如下,其中
y
i
y_i
yi为第i个样本的真实输出值,
y
^
i
\hat{y}_i
y^i是预测值,n是样本的数量。
M
S
E
=
1
n
∑
i
=
1
n
(
y
i
−
y
i
^
)
2
\begin{align*} MSE=\frac{1}{n}\sum_{i=1}^n(y_i-\hat{y_i})^2 \end{align*}
MSE=n1i=1∑n(yi−yi^)2
使用梯度下降算法来最小化损失函数,因为上式是超越方程,没有解析解。计算损失函数对参数的梯度,按以下公式更新,其中
α
\alpha
α是学习率,
L
L
L是损失函数,重复以下过程,直到损失函数的值达到最小或者设定的值。
w
=
w
−
α
∂
L
∂
w
b
=
b
−
α
∂
L
∂
b
\begin{align*} w&=w-\alpha \frac{\partial L}{\partial w}\\ b&=b-\alpha \frac{\partial L}{\partial b} \end{align*}
wb=w−α∂w∂L=b−α∂b∂L
5.4.1数据生成
需要注意的是,这里tensor是张量的意思,是一种多维数组。在机器学习领域,tensor通常用来表示训练数据、模型参数、输入数据等。tensor是机器学习框架Pytorch的基本数据结构,具备良好的计算性能,可以使GPU加速计算,大大提高计算效率。
# 导入包
import numpy as np
import torch
# 设置随机数种子,使得每次运行代码生成的数据相同
np.random.seed(42)
# 生成随机数据
x = np.random.rand(100,1)
y = 1 + 2*x + 0.1 * np.random.randn(100,1)
# 将数据转换为 pytorch tensor
x_tensor = torch.from_numpy(x).float()
y_tensor = torch.form_numpy(y).float()
5.4.2设置超参数
设置超参数 α \alpha α和最大迭代次数:
# 设置超参数
learing_rate = 0.1
num_epochs = 1000
5.4.3初始化参数
初始化参数
w
w
w和
b
b
b。我们使用pytorch的randn
函数来初始化
w
w
w和zeros
函数来初始化参数
b
b
b。randn
函数会生成一个均值为0,标准差为1的随机张量,而zeros
函数会生成一个全部元素都是0的张量,设置requires_grad
为True是希望在方向传播时,计算参数的梯度。通常来说初始化参数对模型训练会产生较大影响,一般用下面几种方法来初始化参数:1是使用常数值,比如所有参数都是0;2是使用随机数,比如使用torch的randn
函数,生成均值为0,标准差为1的随机数;3是已经提前训练好的参数。
具体使用哪种方法,取决于具体的任务和数据。有时候,使用较好的初始化方法可以加速模型的训练,提高模型的准确率。
# 初始化参数
w = torch.randn(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
5.4.3开始训练
使用for
循环迭代了最大迭代次数。在每一次迭代中,我们首先计算了预测值
y
^
\hat{y}
y^,然后计算随时函数
L
L
L。接着使用loss.backforwar()
来反向传播梯度,然后在with torch.no_grad()
块内借助框架自动计算梯度更新参数
w
w
w和
b
b
b。更新完参数后,使用grad.zero_()
方法来清除梯度。
# 开始训练
for epoch in range(num_epochs):
# 计算预测值
y_pred = x_tensor * w + b
# 计算损失
loss = ((y_pred - y_tensor) ** 2).mean()
# 反向传播
loss.backward()
# 更新参数
with torch.no_grad():
w -= learning_rate * w.grad
b -= learning_rate * b.grad
# 清空梯度
w.grad.zero_()
b.grad.zero_()
# 输出训练后的参数
print('w:',w)
print('b:',b)
5.4.4可视化
import matplotlib.pyplot as plt
plt.plot(x, y, 'o')
plt.plot(x_tensor.numpy(), y_pred.detach().numpy())
plt.show()
5.4.5利用Pytorch实现线性回归
# 导入包
import numpy as np
import torch
import torch.nn as nn
# 设置随机数种子,使得每次运行代码生成的数据相同
np.random.seed(42)
# 生成随机数据
x = np.random.rand(100,1)
y = 1 + 2*x + 0.1 * np.random.randn(100,1)
# 将数据转换为 pytorch tensor
x_tensor = torch.from_numpy(x).float()
y_tensor = torch.from_numpy(y).float()
# 设置超参数
learning_rate = 0.1
num_epochs = 1000
# 定义输入数据的维度和输出数据的维度
input_dim = 1
output_dim = 1
# 定义模型
model = nn.Linear(input_dim, output_dim)
# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = learning_rate)
# 开始训练
for epoch in range(num_epochs):
# 将输入数据喂给模型
y_pred = model(x_tensor)
# 计算损失
loss = criterion(y_pred, y_tensor)
# 清空损失
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 输出训练后的数据
print('w:',model.weight.data)
print('b:',model.bias.data)
6.分类问题
人类最基本的智能型号,一是回归,而是分类。和习惯或者是顺序相关的,是回归任务,它研究的是数据内在的规律,是一种惯性规律,一旦找到,就可以用来做预测。
另一种是不断地学习,这种智能的形式就是分类任务。
6.1分类任务的定义
机器学习,把人类的能力(分类任务)进行了抽象分类,一类是常见的监督学习问题,涉及将输入的数据集划分到一个或多个类别中的过程,通常情况下,这些类别是事先确定的,并且类别标签已知。例如垃圾邮件过滤器分类问题,输入数据是邮件,输出类别可能是垃圾邮件或者非垃圾文件。在这种情况下,使用机器学习的算法训练,使得模型可以根据邮件的内容将其分类为垃圾邮件或者非垃圾邮件。分类问题的模型输出是一个离散的类别标签,不是连续的值。
6.2机器学习算法流程
机器学习的套路是用一个数学模型来描述问题,找到目标函数,再用最优化方法,逼近求解得到模型的参数,这个过程就是机器学习训练的过程。
6.3多分类问题的数学表示
在数学中,我们通常使用一个向量来表示输入数据,这个向量被称为“特征向量”,是由输入数据的多个特征组成。比如我们相对图像进行分类,可能会使用像素值作为特征。一种常见的表示离散类别标签的方法是“One-hot编码”(One-hoot encoding),这种方法将每个可能的类别映射到一个独立的维度上,并且在此维度上用1来表示类别。比如有三个可能的类别,分布是猫、狗、鸟,One-hot编码使用一个三维向量,第一维表示猫,第二维表示狗,第三维表示鸟。 这种表示方法的优点是容易区分不同类别,并且可以使用线性模型进行分类。缺点是,当类别数量非常大时,导致特征维度的数量非常大。
另一种常用的方法是使用概率表示,使用每个类别的概率表示输入数据属于该类别的可能性。比如我们想分类一张图像,假设用x表示它,使用一个三维的概率
A
X
=
[
0.7
0.2
0.1
]
AX= \begin{bmatrix} 0.7 \\ 0.2 \\ 0.1\end{bmatrix}
AX=
0.70.20.1
,表示属于不同类别的概率,那么图像会被判定是猫。优点是很容易表示输入数据属于不同类别的可能性,因为这种方法表达了一种概率的可能性,因此可以使用贝叶斯公式进行分类。
6.4Softmax回归
Softmax回归,也被称为多项式逻辑回归,可以输出多个类别的概率,用数学公式表达如下:
y
^
=
s
o
f
t
m
a
x
(
Wx+b
)
s
o
f
t
m
a
x
(
z
)
i
=
e
x
p
(
z
i
)
∑
j
=
1
K
e
x
p
(
z
j
)
\begin{align*} \hat{y}&=softmax(\textbf{Wx+b})\\ softmax(z)_i&=\frac{exp(z_i)}{\sum_{j=1}^Kexp(z_j)} \end{align*}
y^softmax(z)i=softmax(Wx+b)=∑j=1Kexp(zj)exp(zi)
其中,
x
\textbf{x}
x为输入,
W
\textbf{W}
W和
b
\textbf{b}
b是模型的参数,
W
\textbf{W}
W是权重矩阵,
b
\textbf{b}
b是偏置向量,线性组合可以表达成
z=Wx+b
\textbf{z=Wx+b}
z=Wx+b。softmax
是一个函数。可以把线性组合中的每一个元素,转换成概率值,输出
y
^
\hat{y}
y^向量,其中的每一个元素表示输入x属于第i个类别的概率。结果可以使得所有类别的概率和等于1。使用softmax
的好处是可以将输入的特征向量转换成概率值,概率值符合我们的直觉,从而方便进行决策。
数学模型有了,下一步就是确定一个目标函数,让数据进行投票,在多项式逻辑回归中,就是交叉熵损失函数。
6.4损失函数
可能会有疑问,为什么不继续使用线性回归中的均方误差做损失函数?在多项式逻辑回归中,输出值是一个概率值,而不是连续的实数,因此使用均方误差作为损失函数就不太合适了,它虽然能够衡量回归预测结果与真实值之间的差距,但是却不能很好地衡量分类结果与真实值之间的差。因此,在多项式逻辑回归中不再使用均方误差作为损失函数,而是会选择使用对数似然函数胡总和交叉熵作为损失函数。
6.4.1对数损失函数
对数损失函数或者叫对数似然函数,是深度学习中一种常用的损失函数。它衡量在给定的观测数据情况下,模型参数的最优取值,当模型参数最优的时候,对数损失函数也取最小值。公式定义如下:
二分类:
L
(
y
,
f
(
x
)
)
=
−
[
y
l
o
g
(
f
(
x
)
)
+
(
1
−
y
)
l
o
g
(
1
−
f
(
x
)
)
]
\begin{align*} L(y,f(x))=-[ylog(f(x))+(1-y)log(1-f(x))] \end{align*}
L(y,f(x))=−[ylog(f(x))+(1−y)log(1−f(x))]
上式是将分段函数的两种情况写在了一起,加号“+”后面的
(
1
−
y
)
l
o
g
(
1
−
f
(
x
)
)
(1-y)log(1-f(x))
(1−y)log(1−f(x))当y=1时,就不存在了,此时是正类。当y=0时前面项
y
l
o
g
(
f
(
x
)
)
ylog(f(x))
ylog(f(x))就没了,此时是负类。
多分类:
L
(
y
,
f
(
x
)
)
=
−
∑
i
(
y
i
l
o
g
(
f
(
x
i
)
)
)
\begin{align*} L(y,f(x))=-\sum_{i}(y_ilog(f(x_i))) \end{align*}
L(y,f(x))=−i∑(yilog(f(xi)))
上式中的i是类别编号,假设有n个类别,上式就是将n个对数损失求和。为什么要用对数运算呢,对数运算具有非常多的优秀的性质,例如它是单调的,当一个数越大时,其对数值就越大;对数运算具有结合性,可以将多个数的乘积转换为多个数和的形式;对数运算具有放缩性,可以将一个大范围的数值压缩到一个小范围的数值中。
当模型参数最优时,对数损失函数也取得最小值,这使得我们可以直接使用梯度下降等优化算法来最小化损失函数。
6.4.2交叉熵损失函数
交叉熵损失函数(Cross-entropy loss function)本质上与对数损失函数是等价的,在二元分类问题上,二者没有任何区别,对于多元分类问题,在数学表示上略有区别。数学公式如下:
L
(
y
,
f
(
x
)
)
=
1
m
∑
i
=
1
m
∑
j
=
1
n
y
i
j
l
o
g
p
(
x
i
j
)
\begin{align*} L(y,f(x))=\frac{1}{m}\sum_{i=1}^m\sum_{j=1}^ny_{ij}logp(x_{ij}) \end{align*}
L(y,f(x))=m1i=1∑mj=1∑nyijlogp(xij)
其中,
m
m
m表示样本数,
n
n
n表示所属不同类别个数,
y
i
j
y_{ij}
yij表示样本
i
i
i所属类别、
j
j
j为标号,p(x_{ij})表示预测样本
i
i
i属于类别
j
j
j的概率。
7.多分类问题代码实现
在二分类问题中,我们可以使用sigmoid函数。而在多分类问题中,我们希望将输入数据划分到超过两个类别中的一个。这种情况下,我们通常使用一种叫做"softmax"的函数。我们将神经网络的最后一层称为“softmax”层,这一层的输出是一个概率分布,表示输入数据属于每个类别的概率,为了计算这些概率,我们使用softmax函数。
假设有K个类别,第K个类别的概率为
p
k
p_k
pk,将最后一层的输出(即
l
o
g
i
t
s
logits
logits)记为
z
1
,
z
2
,
.
.
.
,
z
k
z_1,z_2,...,z_k
z1,z2,...,zk,根据softmax函数定义,每个类别的概率可以计算为:
p
k
=
e
x
p
(
z
k
)
∑
i
=
1
k
e
x
p
(
z
i
)
\begin{align*} p_k=\frac{exp(z_k)}{\sum_{i=1}^kexp(z_i)} \end{align*}
pk=∑i=1kexp(zi)exp(zk)
以上过程是对K个
l
o
g
i
t
s
logits
logits应用指数函数,然后将它们归一化为概率分布。在训练神经网络时,通常使用交叉熵损失函数来度量预测值和真实值之间的差距,对于多分类问题,交叉熵损失函数可以计算为:
L
=
−
∑
i
=
1
k
y
i
∗
l
o
g
(
p
i
)
\begin{align*} L=-\sum_{i=1}^ky_i*log(p_i) \end{align*}
L=−i=1∑kyi∗log(pi)
其中,
y
i
y_i
yi是真实值,
p
i
p_i
pi是预测值。通过最小化交叉熵损失函数,可以训练出模型数据。训练完神经网络后,即可使用它来进行多分类。为了做出预测,需要将输入数据输入到神经网络中,并根据输出的概率分布决定它属于哪个类别。
使用softmax函数+交叉熵损失函数是一种多分类问题的常用方法。
7.1加载MNIST数据集
选择MNIST手写数据集,在Pytorch中,可以使用torchvision库来加载器来访问MNIST数据集。
import torch
import torchvision
transformation = torchvision.transforms.ToTensor()
train_dataset = torchvision.datasets.MNIST(root='./data/mnist',train=True,download=True,transform=transformation)
test_dataset = torchvision.datasets.MNIST(root='./data/mnist',train=False,download=True,transform=transformation)
7.2数据加载器
数据加载器是Pytorch用于加载和预处理数据的一个工具,具体来说它是一个Python的类。在训练时,可以将数据分成若干批次,每次输入一个批次的数据到神经网络,可以减少内存的使用,并且使得训练更快。
batch_size = 64
train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
可以用以下代码显示图片和标签大小,显示灰度图片并显示标签
import matplotlib.pyplot as plt
for images, labels in train_dataloader:
print(images.shape, labels.shape)
plt.imshow(images[0][0], cmap='gray')
plt.show()
print(labels[0])
如Jupyter显示“Kernel Restarting The kernel for xxx.ipynb appears to have died. It will restart automatically.”报错,则加入下面代码:
import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'
7.3构建网络
下面使用Pytorch来构建一个使用softmax和交叉熵损失函数的网络,使用它来进行MNIST数据集的数字识别。首先引导torch.nn
模块,定义一个类Model
继承nn.Module
类。Model
类中首先定义__init__
初始化方法,传入输入大小input_size
和输出大小outpu_size
。super().__init__()
是继承父类的方法, 然后使用nn.Linear()
定义一层线性网络。
再定义forward
方法进行前向传播,这里使用最简单的一层线性网络。
模型定义好后需要实例化一个模型,
import torch.nn as nn
class Model(nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.linear = nn.Linear(input_size,output_size)
def forward(self,x):
logits = self.linear(x)
return logits
input_size = 28*28
output_size = 10
model = Model(input_size, output_size)
7.4定义损失函数和优化器
下面是定义损失函数和优化器,多分类任务的损失函数使用交叉熵(cross entropy loss)。优化器选用一个普通的SGD(随机梯度下降算法),传入模型参数,并指定学习率。
criterion = nn.CrossEntropyLoss()
optimizer = torch.optm.SGD(model.parameters(), lr=0.01)
7.5模型评估
下面函数需要传入两个参数,模型model
和数据加载器data_loader
。首先将模型使用model.eval()
设置为评估模式,这个模式下的模型不会更新权重。变量correct
和total
分别用于记录正确的样本数量和测试集中的总样本数量。然后使用Pytorch中的torch.no_grad()
上下文管理器,适用于在不需要求梯度的情况。因为在评估模型的准确率时,并不需要计算出梯度,
在for循环中,逐个去除样本输入到模型中,得到输出结果logits
,然后使用Pytorch的torch.max
方法,在函数输出结果中周到logits
的最大值,也就是softmax层输出的概率最大值,并将最大值对应的类别作为模型的预测结果放入变量predict
中。其中x.view()
方法时将x的大小转化为input_size
列,-1是自动计算数据维度,每一行对应一个数据样本,每一列对应一个特征,这样做的目的是为了让输入数据的形状满足模型的要求。
def evaluate(model, dataloader):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for x, y in data_loader:
x = x.view(-1, input_size)
logits = model(x)
_, predicted = torch.max(logits.data, 1)
total += y.size(0)
correct += (predicted == y).sum().item()
return correct / total
7.6模型训练
模型训练了10个epoch,每一轮都评估模型的准确率,可以看到随着训练次数的增加,准确率有所提升。
for epoch in range(10):
for images,labels in train_dataloader:
# 将图像和标签转化成标量
images = images.view(-1,28*28)
labels = labels.long()
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
accuracy = evaluate(model, test_dataloader)
print(f'Epoch {epoch+1}: test accuracy= {accuracy:.2f}')