前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始学习 PyTorch:多层全连接神经网络

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

作者头像
AI科技大本营
发布2018-04-27 16:05:29
5.5K0
发布2018-04-27 16:05:29
举报
文章被收录于专栏:AI科技大本营的专栏

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

深度学习的前身便是多层全连接神经网络,神经网络领域最开始主要是用来模拟人脑神经元系统,但是随后逐渐发展成了一项机器学习技术。多层全连接神经网络是现在深度学习中各种复杂网络的基础,了解它能够帮助我们更好地学习之后的内容。这一章我们将先从 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回归也可以处理多分类问题,但最常见的还是应用在处理二分类问题上,下面我们将介绍一下使用神经网络算法来处理多分类问题。

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-12-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AI科技大本营 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档