送书 | 从零开始学习 PyTorch:多层全连接神经网络

百家 作者:AI100 2017-12-01 06:51:49



本文引自博文视点新书《深度学习入门之PyTorch》3 章——多层全连接神经网络


内容提要:深度学习如今已经成为科技领域最炙手可热的技术,在《深度学习入门之PyTorch》中,我们将帮助你入门深度学习。《深度学习入门之PyTorch》将从机器学习和深度学习的基础理论入手,从零开始学习 PyTorch,了解 PyTorch 基础,以及如何用 PyTorch 框架搭建模型。通过阅读《深度学习入门之PyTorch》,你将学到机器学习中的线性回归和 Logistic 回归、深度学习的优化方法、多层全连接神经网络、卷积神经网络、循环神经网络,以及生成对抗网络,最后通过实战了解深度学习前沿的研究成果,以及 PyTorch 在实际项目中的应用。《深度学习入门之PyTorch》将理论和代码相结合,帮助读者更好地入门深度学习,适合任何对深度学习感兴趣的人阅读。


参与方式:喜欢这本书,请在评论区留言,和大家分享你对PyTorch的使用经验和心得,根据评论质量和评论点赞数,前五名同学可获得本书。活动截止时间为12月4日(下周一22点



深度学习的前身便是多层全连接神经网络,神经网络领域最开始主要是用来模拟人脑神经元系统,但是随后逐渐发展成了一项机器学习技术。多层全连接神经网络是现在深度学习中各种复杂网络的基础,了解它能够帮助我们更好地学习之后的内容。这一章我们将先从 PyTorch 基础入手,介绍 PyTorch 的处理对象、运算操作、自动求导,以及数据处理方法,接着从线性模型开始进入机器学习的内容,然后由 Logistic回归引入分类问题,接着介绍多层全连接神经网络、反向传播算法、各种基于梯度的优化算法、数据预处理和训练技巧,最后用 PyTorch 实现多层全连接神经网络。



3.1 热身:PyTorch 基础


首先我们会介绍一下 PyTorch 里面的一些基础知识,有了这些基础知识之后我们才能做出更多复杂的变形。


3.1.1 Tensor(张量)


PyTorch 里面处理的最基本的操作对象就是 Tensor,Tensor 是张量的英文,表示的是一个多维的矩阵,比如零维就是一个点,一维就是向量,二维就是一般的矩阵,多维就相当于一个多维的数组,这和 numpy 是对应的,而且 PyTorch 的 Tensor 可以和 numpy的 ndarray 相互转换,唯一不同的是 PyTorch 可以在 GPU 上运行,而 numpy 的 ndarray只能在 CPU 上运行。

 

我们先介绍一下一些常用的不同数据类型的 Tensor,有 32 位浮点型  torch.FloatTensor、64 位浮点型  torch.DoubleTensor、16 位整型  torch.Shor  tTensor、32 位整型  torch.IntTensor 和 64 位整型  torch.LongTensor。我们可以通过下面这样的方式来定义一个三行两列给定元素的矩阵,并且显示出矩阵的元素和大小:


a  =  torch.Tensor([[2,  3],  [4,  8],  [7,  9]])

print('a  is:  {}'.format(a))

print('a  size  is  {}'.format(a.size()))  #  a.size()  =  3,  2

 

需要注意的是  torch.Tensor 默认的是  torch.FloatTensor 数据类型,也可以

定义我们想要的数据类型,就像下面这样:

 

b  =  torch.LongTensor([[2,  3],  [4,  8],  [7,  9]])

print('b  is  :  {}'.format(b))


当然也可以创建一个全是 0 的空 Tensor 或者取一个正态分布作为随机初始值:


c  =  torch.zeros((3,  2))

print('zero  tensor:  {}'.format(c))


d  =  torch.randn((3,  2))

print('normal  randon  is  :  {}'.format(d))

 

我们也可以像 numpy 一样通过索引的方式取得其中的元素,同时也可以改变它的值,比如将 a 的第一行第二列改变为 100。


a[0,  1]  =  100

print('changed  a  is:  {}'.format(a))


除此之外,还可以在 Tensor 与 numpy.ndarray 之间相互转换:


numpy_b  =  b.numpy()

print('conver  to  numpy  is  n  {}'.format(numpy_b))


e  =  np.array([[2,  3],  [4,  5]])

torch_e  =  torch.from_numpy(e)

print('from  numpy  to  torch.Tensor  is  {}'.format(torch_e))

f_torch_e  =  torch_e.float()

print('change  data  type  to  float  tensor:  {}'.format(f_torch_e))

 

通过简单的  b.numpy(),就能将 b 转换为 numpy 数据类型,同时使用  torch.from_num  py() 就能将 numpy 转换为 tensor,如果需要更改 tensor 的数据类型,只需要在转换后的 tensor 后面加上你需要的类型,比如想将 a 的类型转换成 float,只需  a.float()就可以了。

如果你的电脑支持 GPU 加速,还可以将 Tensor 放到 GPU 上。首先通过  torch.cuda.is_available() 判断一下是否支持 GPU,如果想把 tensora 放到 GPU 上,只需  a.cuda() 就能够将 tensor a 放到 GPU 上了。


if  torch.cuda.is_available():

a_cuda  =  a.cuda()

print(a_cuda)

 

3.1.2 Variable(变量)


接着要讲的一个概念就是 Variable,也就是变量,这个在 numpy 里面就没有了,是神经网络计算图里特有的一个概念,就是 Variable 提供了自动求导的功能,之前如果了解过 Tensorflow 的读者应该清楚神经网络在做运算的时候需要先构造一个计算图谱,然后在里面进行前向传播和反向传播。


Variable 和 Tensor 本质上没有区别,不过 Variable 会被放入一个计算图中,然后进行前向传播,反向传播,自动求导。首先 Variable 是在  torch.autograd.Variable 中,要将一个 tensor 变成 Variable也非常简单,比如想让一个 tensor a 变成 Variable,只需要  Variable(a) 就可以了。我们可以通过图 3.1了解到 Variable 的属性。


图 3.1   结构图


从图3.1中可以看出 Variable 有三个比较重要的组成属性:data,grad 和 grad_fn。通过 data 可以取出 Variable 里面的 tensor 数值,grad_fn 表示的是得到这个 Variable 的操作,比如通过加减还是乘除来得到的,最后 grad 是这个 Variabel 的反向传播梯度,下面通过例子来具体说明一下:

#  Create  Variable

x  =  Variable(torch.Tensor([1]),  requires_grad=True)

w  =  Variable(torch.Tensor([2]),  requires_grad=True)

b  =  Variable(torch.Tensor([3]),  requires_grad=True)


#  Build  a  computational  graph.

y  =  w  *  x  +  b     #  y  =  2  *  x  +  3


#  Compute  gradients

y.backward()  #  same  as  y.backward(torch.FloatTensor([1]))

#  Print  out  the  gradients.

print(x.grad)     #  x.grad  =  2

print(w.grad)     #  w.grad  =  1

print(b.grad)     #  b.grad  =  1

 

构建 Variable,要注意得传入一个参数  requires_grad=True,这个参数表示是否对这个变量求梯度,默认的是 False,也就是不对这个变量求梯度,这里我们希望得到这些变量的梯度,所以需要传入这个参数。从上面的代码中,我们注意到了一行 y.backward(),这一行代码就是所谓的自动求导,这其实等价于  y.backward(torch.FloatTensor([1])),只不过对于标量求导里面的参数就可以不写了,自动求导不需要你再去明确地写明哪个函数对哪个函数求导,直接通过这行代码就能对所有的需要梯度的变量进行求导,得到它们的梯度,然后通过 x.grad 可以得到 x 的梯度。


上面是标量的求导,同时也可以做矩阵求导,比如:


x  =  torch.randn(3)

x  =  Variable(x,  requires_grad=True)


y  =  x  *  2

print(y)


y.backward(torch.FloatTensor([1,  0.1,  0.01]))

print(x.grad)

 

相当于给出了一个三维向量去做运算,这时候得到的结果 y 就是一个向量,这里对这个向量求导就不能直接写成  y.backward(),这样程序是会报错的。这个时候需要传入参数声明,比如  y.backward(torch.FloatTensor([1,  1,  1])),这样得到的结果就是它们每个分量的梯度,或者可以传入  y.backward(torch.FloatTensor([1,0.1,  0.01])),这样得到的梯度就是它们原本的梯度分别乘上 1,0.1 和 0.01。


3.1.3 Dataset(数据集)


在处理任何机器学习问题之前都需要数据读取,并进行预处理。PyTorch 提供了很多工具使得数据的读取和预处理变得很容易。


torch.utils.data.Dataset 是代表这一数据的抽象类,你可以自己定义你的数

据类继承和重写这个抽象类,非常简单,只需要定义__len__和__getitem__这两个函数:


class  myDataset(Dataset):

def  __init__(self,  csv_file,  txt_file,  root_dir,  other_file):

self.csv_data  =  pd.read_csv(csv_file)

with  open(txt_file,  'r')  as  f:

data_list  =  f.readlines()

self.txt_data  =  data_list

self.root_dir  =  root_dir


def  __len__(self):

return  len(self.csv_data)


def  __getitem__(self,  idx):

data  =  (self.csv_data[idx],  self.txt_data[idx])

return  data


通过上面的方式,可以定义我们需要的数据类,可以通过迭代的方式来取得每一个数据,但是这样很难实现取 batch,shuffle 或者是多线程去读取数据,所以 PyTorch中提供了一个简单的办法来做这个事情,通过  torch.utils.data.DataLoader 来定义一个新的迭代器,如下:


dataiter  =  DataLoader(myDataset,  batch_size=32,  shuffle=True,

collate_fn=default_collate)

 

里面的参数都特别清楚,只有  collate_fn 是表示如何取样本的,我们可以定义

自己的函数来准确地实现想要的功能,默认的函数在一般情况下都是可以使用的。


另外在  torchvision  这个包中还有一个更高级的有关于计算机视觉的数据读取类:ImageFolder,主要功能是处理图片,且要求图片是下面这种存放形式:


root/dog/xxx.png

root/dog/xxy.png

root/dog/xxz.png


root/cat/123.png

root/cat/asd.png

root/cat/zxc.png


之后这样来调用这个类:


dset  =  ImageFolder(root='root_path',  transform=None,loader=default_loader)

 


其中的 root 需要是根目录,在这个目录下有几个文件夹,每个文件夹表示一个类别:transform 和  target_transform 是图片增强,之后我们会详细讲;loader 是图片读取的办法,因为我们读取的是图片的名字,然后通过 loader 将图片转换成我们需要的图片类型进入神经网络。


3.1.4   nn.Module(模组)


在 PyTorch 里面编写神经网络,所有的层结构和损失函数都来自于  torch.nn,所有的模型构建都是从这个基类  nn.Module 继承的,于是有了下面这个模板。


class  net_name(nn.Module):

def  __init__(self,  other_arguments):

super(net_name,  self).__init__()

self.conv1  =  nn.Conv2d(in_channels,  out_channels,  kernel_size)

#  other  network  layer


def  forward(self,  x):

x  =  self.conv1(x)

return  x

 

这样就建立了一个计算图,并且这个结构可以复用多次,每次调用就相当于用该计算图定义的相同参数做一次前向传播,这得益于 PyTorch 的自动求导功能,所以我们不需要自己编写反向传播,而所有的网络层都是由 nn 这个包得到的,比如线性层nn.Linear,等之后使用的时候我们可以详细地介绍每一种网络对应的结构,以及如何调用。

定义完模型之后,我们需要通过 nn 这个包来定义损失函数。常见的损失函数都已经定义在了 nn 中,比如均方误差、多分类的交叉熵,以及二分类的交叉熵,等等,调用这些已经定义好的损失函数也很简单:


criterion  =  nn.CrossEntropyLoss()

loss  =  criterion(output,  target)

 

这样就能求得我们的输出和真实目标之间的损失函数了。


3.1.5 torch.optim(优化)


在机器学习或者深度学习中,我们需要通过修改参数使得损失函数最小化(或最大化),优化算法就是一种调整模型参数更新的策略。


优化算法分为两大类。


1. 一阶优化算法

这种算法使用各个参数的梯度值来更新参数,最常用的一阶优化算法是梯度下降。所谓的梯度就是导数的多变量表达式,函数的梯度形成了一个向量场,同时也是一个方向,这个方向上方向导数最大,且等于梯度。梯度下降的功能是通过寻找最小值,控制方差,更新模型参数,最终使模型收敛,网络的参数更新公式是:



其中 η 是学习率,是函数的梯度,我们可以通过图 3.2形象地说明一下该方法。这是深度学习里面最常用的优化方法,我们之后会详细讲解它的各种变式。


2. 二阶优化算法


二阶优化算法使用了二阶导数(也叫做 Hessian 方法)来最小化或最大化损失函数,主要基于牛顿法,但是由于二阶导数的计算成本很高,所以这种方法并没有广泛使用。torch.optim 是一个实现各种优化算法的包,大多数常见的算法都能够直接通过这个包来调用,比如随机梯度下降,以及添加动量的随机梯度下降,自适应学习率等。


在调用的时候将需要优化的参数传入,这些参数都必须是 Variable,然后传入一些基本的设定,比如学习率和动量等。


图 3.2 一阶优化算法


下面举一个例子:

 

optimizer  =  torch.optim.SGD(model.parameters(),  lr=0.01,  momentum=0.9)

 

这样我们就将模型的参数作为需要更新的参数传入优化器,设定学习率是 0.01,动量是 0.9 的随机梯度下降,在优化之前需要先将梯度归零,即  optimizer.zeros(),然后通过  loss.backward() 反向传播,自动求导得到每个参数的梯度,最后只需要optimizer.step() 就可以通过梯度做一步参数更新。


3.1.6   模型的保存和加载


在 PyTorch 里面使用 torch.save 来保存模型的结构和参数,有两种保存方式:


(1)保存整个模型的结构信息和参数信息,保存的对象是模型 model;

(2)保存模型的参数,保存的对象是模型的状态  model.state_dict()。


可以这样保存,save 的第一个参数是保存对象,第二个参数是保存路径及名称:


torch.save(model,  './model.pth')

torch.save(model.state_dict(),  './model_state.pth')


加载模型有两种方式对应于保存模型的方式:


(1)加载完整的模型结构和参数信息,使用  load_model  =  torch.load('model.pth'),在网络较大的时候加载的时间比较长,同时存储空间也比较大;

(2)加载模型参数信息,需要先导入模型的结构,然后通过  model.load_state_dic  (torch.load('model_state.pth')) 来导入。



3.2 线性模型


这一节将从机器学习最简单的线性模型入手,看看 PyTorch 如何解决这个问题。


3.2.1 问题介绍


说起线性模型,大家对它都很熟悉了,通俗来讲就是给定很多个数据点,希望能够找到一个函数来拟合这些数据点使其误差最小,比如最简单的一元线性模型就可以用图3.3来表示。


图 3.3   一元线性模型


图3.3给出了一系列的点,找一条直线,使得直线尽可能与这些点接近,也就是这些点到直线的距离之和尽可能小。用数学语言来严格表达,即给定由 d 个属性描述的示例 x = (x1, x2, x3, • • • , xd),其中 xi 表示 x 在第 i 个属性上面的取值,线性模型就是试图学习一个通过属性的线性组合来进行预测的函数,即


f (x) = w1x1 + w2x2 + • • • + wdxd + b (3.2)


一般可以用向量的形式来表达:


f (x) = wT x + b (3.3)


其中 w = (w1, w2, • • • , wd),w 和 b 都是需要学习的参数,模型通过不断地调整 w 和 b,最后就能够得到一个最优的模型。


线性模型形式简单、易于建模,却孕育着机器学习领域中重要的基本思想,同时线性模型还具有特别好的解释性,因为权重的大小就直接可以表示这个属性的重要程度。首先我们从最简单的一维线性回归入手。


3.2.2   一维线性回归


给定数据集 D  =  {(x1, y1), (x2, y2), (x3, y3), • • • , (xm, ym)},线性回归希望能够优化出一个好的函数 f (x),使得 f (xi) = wxi + b 能够与 yi 尽可能接近。


如何才能学习到参数 w 和 b 呢?很简单,只需要确定如何衡量 f (x) 与 y 之间的差别,就像之前讲的一样,可以用它们之间的距离差异



来衡量误差,取平方是因为距离有正有负,我们希望能够将它们全部变成正的。这也就是著名的均方误差,要做的事情就是希望能够找到 w∗ 和 b∗,使得



均方误差非常直观,也有着很好的几何意义,对应了常用的欧几里得距离,基于均方误差最小化来进行模型求解的办法也称为“最小二乘法”。


求解办法其实非常简单,如果求这个连续函数的最小值,那么只需要求它的偏导数,让它的偏导数等于 0 来估计它的参数,即:

 

 

通过求解式 (3.6) 和式 (3.7),我们就可以得到 w 和 b 的最优解:

 



其中 

 


即为 x 的均值。

 

3.2.3 多维线性回归


更一般的情况是多维线性回归,比如像前文描述的,我们有 d 个属性,试图学得最优的函数 f (x):


 

最小,这也称为“多元线性回归”,同样可以用最小二乘法对 w和 b 进行估计,为了方便计算,可以将 w 和 d 写进同一个矩阵,将数据集 D 表示成一个 m × (d + 1) 的矩阵 X,每行前面 d 个元素表示 d 个属性值,最后一个元素设为 1,即

 

将目标 y 也写乘向量的形式 y = (y1, y2, • • • , ym),那么我们就能够得到:


同样对其求导,令它等于 0。

 


因为上面涉及了矩阵的逆运算,所以需要 XT X 是一个满秩矩阵或者正定矩阵,那么我们可以得到:



所以线性回归模型可以写成:



然而在现实任务中,XT X 往往不是满秩矩阵,就算是满秩矩阵,求解逆的过程也比较慢,所以我们一般可以使用梯度下降法去求解这个最小二乘问题。


3.2.4   一维线性回归的代码实现


讲了这么多原理,下面用 PyTorch 来求解一下一维线性回归问题。

首先我们随便给出一些点:


x_train  =  np.array([[3.3],  [4.4],  [5.5],  [6.71],  [6.93],  [4.168],

[9.779],  [6.182],  [7.59],  [2.167],  [7.042],

[10.791],  [5.313],  [7.997],  [3.1]],  dtype=np.float32)


y_train  =  np.array([[1.7],  [2.76],  [2.09],  [3.19],  [1.694],  [1.573],

[3.366],  [2.596],  [2.53],  [1.221],  [2.827],

[3.465],  [1.65],  [2.904],  [1.3]],  dtype=np.float32)

 

通过 matplotlib 画出来就是这个样子,如图3.4所示。


我们想要做的事情就是找一条直线去逼近这些点,也就是希望这条直线离这些点的距离之和最小,先将  numpy.array 转换成  Tensor,因为  PyTorch 里面的处理单元是Tensor,按上一章讲的方法,这就特别简单了:


x_train  =  torch.from_numpy(x_train)

y_train  =  torch.from_numpy(y_train)


接着需要建立模型,根据上一节 PyTorch 的基础知识,这样来定义一个简单的模型:


class  LinearRegression(nn.Module):

def  __init__(self):

super(LinearRegression,  self).__init__()

self.linear  =  nn.Linear(1,  1)  #  input  and  output  is  1  dimension


def  forward(self,  x):

out  =  self.linear(x)

return  out


if  torch.cuda.is_available():

model  =  LinearRegression().cuda()

else:

model  =  LinearRegression()

 

图 3.4 matplotlib 画出的图


这里我们就定义了一个超级简单的模型 y  = wx + b,输入参数是一维,输出参数也是一维,这就是一条直线,当然这里可以根据你想要的输入输出维度进行更改,我们希望去优化参数 w 和 b 能够使得这条直线尽可能接近这些点,如果支持 GPU 加速,可以通过  model.cuda() 将模型放到 GPU 上。然后定义损失函数和优化函数,这里使用均方误差作为优化函数,使用梯度下降进行优化:


criterion  =  nn.MSELoss()

optimizer  =  optim.SGD(model.parameters(),  lr=1e-3)


接着就可以开始训练我们的模型了:


num_epochs  =  1000

for  epoch  in  range(num_epochs):

if  torch.cuda.is_available():

inputs  =  Variable(x_train).cuda()

target  =  Variable(y_train).cuda()

else:

inputs  =  Variable(x_train)

target  =  Variable(y_train)

#  forward

out  =  model(inputs)

loss  =  criterion(out,  target)

#  backward

optimizer.zero_grad()

loss.backward()

optimizer.step()


if  (epoch+1)  %  20  ==  0:

print('Epoch[{}/{}],  loss:  {:.6f}'

.format(epoch+1,  num_epochs,  loss.data[0]))

 

定义好我们要跑的  epoch  个数,然后将数据变成  Variable  放入计算图,然后通过  out=model(inputs) 得到网络前向传播得到的结果,通过  loss=criterion(out,target) 得到损失函数,然后归零梯度,做反向传播和更新参数,特别要注意的是,每次做反向传播之前都要归零梯度,optimizer.zero_grad()。不然梯度会累加在一起,造成结果不收敛。在训练的过程中隔一段时间就将损失函数的值打印出来看看,确保我们的模型误差越来越小。注意  loss.data[0],首先 loss 是一个 Variable,所以通过loss.data 可以取出一个 Tensor,再通过  loss.data[0] 得到一个 int 或者 float 类型的数据,这样我们才能够打印出相应的数据。


做完训练之后可以预测一下结果。


model.eval()

predict  =  model(Variable(x_train))

predict  =  predict.data.numpy()

plt.plot(x_train.numpy(),  y_train.numpy(),  'ro',  label='Original  data')

plt.plot(x_train.numpy(),  predict,  label='Fitting  Line')

plt.show()

 

首先需要通过  model.eval() 将模型变成测试模式,这是因为有一些层操作,比

如 Dropout 和 BatchNormalization 在训练和测试的时候是不一样的,所以我们需要通过这样一个操作来转换这些不一样的层操作。然后将测试数据放入网络做前向传播得到结果,最后画出的结果如图 3.5所示。


图 3.5 一元回归


这样我们就通过 PyTorch 解决了一个简单的一元回归问题,得到了一条直线去尽可能逼近这些离散的点。


3.2.5 多项式回归


对于一般的线性回归,由于该函数拟合出来的是一条直线,所以精度欠佳,我们可以考虑多项式回归,也就是提高每个属性的次数,而不再是只使用一次去回归目标函数。原理和之前的线性回归是一样的,只不过这里用的是高次多项式而不是简单的一次线性多项式。首先给出我们想要拟合的方程:


y = 0.9 + 0.5 × x + 3 × x2 + 2.4 × x3


然后可以设置参数方程:


y = b + w1 × x + w2 × x2 + w3 × x3


我们希望每一个参数都能够学习到和真实参数很接近的结果。下面来看看如何用 PyTorch 实现这个简单的任务。


首先需要预处理数据,也就是需要将数据变成一个矩阵的形式:



在 PyTorch 里面使用  torch.cat() 函数来实现 Tensor 的拼接:


def  make_features(x):

"""Builds  features  i.e.  a  matrix  with  columns  [x,  x^2,  x^3]."""

x  =  x.unsqueeze(1)

return  torch.cat([x  **  i  for  i  in  range(1,  4)],  1)


对于输入的 n 个数据,我们将其扩展成上面矩阵所示的样子。

然后定义好真实的函数:


W_target  =  torch.FloatTensor([0.5,  3,  2.4]).unsqueeze(1)

b_target  =  torch.FloatTensor([0.9])


def  f(x):

"""Approximated  function."""

return  x.mm(W_target)  +  b_target[0]

 

这里的权重已经定义好了,unsqueeze(1) 是将原来的 tensor 大小由 3 变成 (3, 1),x.mm(W_target) 表示做矩阵乘法,f (x) 就是每次输入一个 x 得到一个 y 的真实函数。在进行训练的时候我们需要采样一些点,可以随机生成一些数来得到每次的训练集:


def  get_batch(batch_size=32):

"""Builds  a  batch  i.e.  (x,  f(x))  pair."""

random  =  torch.randn(batch_size)

x  =  make_features(random)

y  =  f(x)

if  torch.cuda.is_available():

return  Variable(x).cuda(),  Variable(y).cuda()

else:

return  Variable(x),  Variable(y)

 

通过上面这个函数我们每次取  batch_size 这么多个数据点,然后将其转换成矩阵的形式,再把这个值通过函数之后的结果也返回作为真实的目标。然后可以定义多项式模型:

 

#  Define  model

class  poly_model(nn.Module):

def  __init__(self):

super(poly_model,  self).__init__()

self.poly  =  nn.Linear(3,  1)


def  forward(self,  x):

out  =  self.poly(x)

return  out


if  torch.cuda.is_available():

model  =  poly_model().cuda()

else:

model  =  poly_model()


这里的模型输入是 3 维,输出是 1 维,跟之前定义的线性模型只有很小的差别。然后我们定义损失函数和优化器:


criterion  =  nn.MSELoss()

optimizer  =  optim.SGD(model.parameters(),  lr=1e-3)

 

同样使用均方误差来衡量模型的好坏,使用随机梯度下降来优化模型,然后开始训练模型:


epoch  =  0

while  True:

#  Get  data

batch_x,  batch_y  =  get_batch()

#  Forward  pass

output  =  model(batch_x)

loss  =  criterion(output,  batch_y)

print_loss  =  loss.data[0]

#  Reset  gradients

optimizer.zero_grad()

#  Backward  pass

loss.backward()

#  update  parameters

optimizer.step()

epoch  +=  1

if  print_loss  <   1e-3:

break

 

这里我们希望模型能够不断地优化,直到实现我们设立的条件,取出的 32 个点的均方误差能够小于 0.001。运行程序可以得到如图3.6所示的结果。


图 3.6 程序运行结果


将真实函数的数据点和拟合的多项式画在同一张图上,我们可以得到如图3.7所示的结果。


图 3.7   多项式回归


从两个结果来看,我们已经很接近真实的函数了。现实世界中很多问题都不是简单的线性回归,涉及很多复杂的非线性模型,而线性模型是机器学习中最重要的模型之一,它的统计思想及其非常直观的解释性仍然可以给我们一些启发。



3.3 分类问题


下面这节我们将开始介绍机器学习领域里面的另一个问题:分类问题。


3.3.1 问题介绍


机器学习中的监督学习主要分为回归问题和分类问题,我们之前已经讲过回归问题了,它希望预测的结果是连续的,那么分类问题所预测的结果就是离散的类别。这时输入变量可以是离散的,也可以是连续的,而监督学习从数据中学习一个分类模型或者分类决策函数,它被称为分类器(classifier)。分类器对新的输入进行输出预测,这个过程即称为分类(classification)。例如,判断邮件是否为垃圾邮件,医生判断病人是否生病,或者预测明天天气是否下雨等。同时分类问题中包括有二分类和多分类问题,我们下面先讲一下最著名的二分类算法——Logistic 回归。首先从 Logistic 回归的起源说起。


3.3.2 Logistic 起源


Logistic 起源于对人口数量增长情况的研究,后来又被应用到了对于微生物生长情况的研究,以及解决经济学相关的问题,现在作为回归分析的一个分支来处理分类问题,先从 Logistic 分布入手,再由 Logistic 分布推出 Logistic 回归。


3.3.3 Logistic 分布


设 X  是连续的随机变量,服从 Logistic 分布是指 X  的积累分布函数和密度函数如下:



其中 µ 影响中心对称点的位置,γ 越小中心点附近的增长速度越快。下一节会讲到在深度学习中常用的一个非线性变换 Sigmoid 函数是 Logistic 分布函数中 γ = 1, µ = 0 的特殊形式。


(sigmoid)Logistic 函数的表达形式如下:



其函数图像如图 3.8所示,由于函数很像“S”形,所以该函数又叫 Sigmoid 函数。


图 3.8   Sigmoid 函数图像


3.3.4   二分类的 Logistic 回归


Logistic 回归不仅可以解决二分类问题,也可以解决多分类问题,但是二分类问题最为常见同时也具有良好的解释性。对于二分类问题,Logistic 回归的目标是希望找到一个区分度足够好的决策边界,能够将两类很好地分开。


假设输入的数据的特征向量 x ∈ Rn,那么决策边界可以表示为 ∑ni=1 wixi + b = 0;假设存在一个样本点使得 hw(x) = ∑ni=1 wixi + b > 0,那么可以判定它的类别是 1;如果 hw(x) = ∑mi=1 wixi + b < 0,那么可以判定其类别是 0。这个过程其实是一个感知机的过程,通过决策函数的符号来判断其属于哪一类。而 Logistic 回归要更进一步,通过找到分类概率 P (Y   = 1) 与输入变量 x 的直接关系,然后通过比较概率值来判断类别,简单来说就是通过计算下面两个概率分布:



其中 w 是权重,b 是偏置。现在介绍 Logistic 模型的特点,先引入一个概念:一个事件发生的几率(odds)是指该事件发生的概率与不发生的概率的比值,比如一个事件发生的概率是 p,那么该事件发生的几率是

 



对于 Logistic 回归而言,我们由式(3.16)和式(3.17)可以得到:

 


这也就是说在 Logistic 回归模型中,输出 Y   = 1 的对数几率是输入 x 的线性函数,这也就是 Logistic 回归名称的原因。如果观察式(3.17),则可以得到另外一种 Logistic 回归的定义,即线性函数的值越接近正无穷,概率值就越接近 1;线性函数的值越接近负无穷,概率值越接近 0。因此 Logistic 回归的思路是先拟合决策边界(这里的决策边界不局限于线性,还可以是多项式),在建立这个边界和分类概率的关系,从而得到二分类情况下的概率。


3.3.5 模型的参数估计


上面我们简单地介绍了 Logistic 回归模型的建立,下面通过极大似然估计来求出模型中的参数。对于给定的训练集数据 T   =  {(x1, y1), (x2, y2), • • • (xn, yn)},其中 xi  ∈  Rn, yi  ∈{0, 1},假设 P (Y  = 1|x) = π(x),那么 P (Y  = 0|x) = 1 − π(x),所以似然函数可以有如下的表达:


对 L(w) 求极大值就能够得到 w 的估计值,可以使用最简单的梯度下降法来求得。用 L(w) 对 W  求导,可以得到:

 

求出梯度之后就可以使用迭代的梯度下降来求解。


3.3.6   Logistic 回归的代码实现


首先我们打开 txt 文件,可以看到数据存放的方式,如图3.9所示。


图 3.9   数据存放


每个数据点是一行,每一行中前面两个数据表示 x 坐标和 y 坐标,最后一个数据表示其类别。我们先从 data.txt 文件中读取数据,使用非常简单的 python 读取 txt 的方法就能够实现。


with  open('data.txt',  'r')  as  f:

data_list  =  f.readlines()

data_list  =  [i.split('n')[0]  for  i  in  data_list]

data_list  =  [i.split(',')  for  i  in  data_list]

data  =  [(float(i[0]),  float(i[1]),  float(i[2]))  for  i  in  data_list]


然后通过 matplotlib 能够简单地将数据画出来。


x0  =  list(filter(lambda  x:  x[-1]  ==  0.0,  data))

x1  =  list(filter(lambda  x:  x[-1]  ==  1.0,  data))

plot_x0_0  =  [i[0]  for  i  in  x0]

plot_x0_1  =  [i[1]  for  i  in  x0]

plot_x1_0  =  [i[0]  for  i  in  x1]

plot_x1_1  =  [i[1]  for  i  in  x1]


plt.plot(plot_x0_0,  plot_x0_1,  'ro',  label='x_0')

plt.plot(plot_x1_0,  plot_x1_1,  'bo',  label='x_1')

plt.legend(loc='best')

 

首先将两个类别分开,然后将所有的数据点画出就能够得到图3.10。


图 3.10 数据点


从图3.10中我们可以明显看出这些数据点被分为两个类:一类用红色的点,一类用蓝色的点,我们希望通过 Logistic 回归将它们分开。接下来定义 Logistic 回归的模型,以及二分类问题的损失函数和优化方法。


class  LogisticRegression(nn.Module):

def  __init__(self):

super(LogisticRegression,  self).__init__()

self.lr  =  nn.Linear(2,  1)

self.sm  =  nn.Sigmoid()


def  forward(self,  x):

x  =  self.lr(x)

x  =  self.sm(x)

return  x


logistic_model  =  LogisticRegression()

if  torch.cuda.is_available():

logistic_model.cuda()


criterion  =  nn.BCELoss()

optimizer  =  torch.optim.SGD(logistic_model.parameters(),  lr=1e-3,

momentum=0.9)

 

这里  nn.BCELoss 是二分类的损失函数,torch.optim.SGD 是随机梯度下降优化函数。然后训练模型,并且间隔一定的迭代次数输出结果。


for  epoch  in  range(50000):

if  torch.cuda.is_available():

x  =  Variable(x_data).cuda()

y  =  Variable(y_data).cuda()

else:

x  =  Variable(x_data)

y  =  Variable(y_data)

#  ==================forward==================

out  =  logistic_model(x)

loss  =  criterion(out,  y)

print_loss  =  loss.data[0]

mask  =  out.ge(0.5).float()

correct  =  (mask  ==  y).sum()

acc  =  correct.data[0]  /  x.size(0)

#  ===================backward=================

optimizer.zero_grad()

loss.backward()

optimizer.step()

if  (epoch+1)  %  1000  ==  0:

print('*'*10)

print('epoch  {}'.format(epoch+1))

print('loss  is  {:.4f}'.format(print_loss))

print('acc  is  {:.4f}'.format(acc))

 

其中  mask=out.ge(0.5).float() 是判断输出结果如果大于 0.5 就等于 1,小于

0.5 就等于 0,通过这个来计算模型分类的准确率。


训练完成我们可以得到图 3.11所示的 loss 和准确率。因为数据相对简单,同时我们使用的是也是简单的线性 Logistic 回归,loss 已经降得相对较低,同时也有 91% 的准确率。


图 3.11 loss 和准确率


我们可以将这条直线画出来,因为模型中学习的参数 w1,w2 和 b 其实构成了一条直线 w1x + w2y + b = 0,在直线上方是一类,在直线下方又是一类。我们可以通过下面的方式将模型的参数取出来,并将直线画出来,如图 3.12所示。

 

w0,  w1  =  logistic_model.lr.weight[0]

w0  =  w0.data[0]

w1  =  w1.data[0]

b  =  logistic_model.lr.bias.data[0]

plot_x  =  np.arange(30,  100,  0.1)


plot_y  =  (-w0  *  plot_x  -  b)  /  w1

plt.plot(plot_x,  plot_y)

plt.show()


通过图 3.12 我们可以看出这条直线基本上将这两类数据都分开了。


以上我们介绍了分类问题中的二分类问题和 Logistic 回归算法,一般来说,Logistic回归也可以处理多分类问题,但最常见的还是应用在处理二分类问题上,下面我们将介绍一下使用神经网络算法来处理多分类问题。


感谢电子工业出版社博文视点对本次活动的支持


关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接