这样拟合正弦函数你会吗

为了加深大家对深度学习这一概念的理解,尤其是对深度(多层神经网络) 两个字的认识,笔者在本篇文章中将会通过一个拟合正弦函数例子再次介绍“深度”这一概念。但巧妇难为无米之炊,所以接下来笔者首先会以线性回归的实现为例,来简单介绍一下Pytorch;然后再来实现对正弦函数的拟合。

1 动手实现线性回归

1.1 深度学习框架

在前面介绍《跟我一起深度学习》这个专栏时我们就说到后面会使用Pytorch这个框架来进行相应模型的实现,但并未解释到它是用来干什么的。并且如果是接触过深度学习的同学肯定知道深度学习的相关框架不止Pytorch一种,还有Tensoflow、MXNet、PaddlePaddle等等,那我们为什么我们需要这些框架?根据笔者的认知来看,需要这些框架最重要的一个目的就是实现自动求导(Auto grad)

图 1. 常见深度学习框架

在机器学习中,对于简单的线性回归或者是逻辑回归我们还能自己手动求得目标函数关于参数的梯度,但是在深度学习中这一想法几乎是不现实的。因为在深度学习中普遍的网络结构相较于线性回归都要复杂很多,所以如何快速实现对参数梯度求解便是深度学习框架的核心功能之一。

下面就以线性回归为例,来向大家展示一下如何使用Pytorch进行建模。同时需要提醒的是,如果有时间可以自己先行去了解一下Pytorch中的基本操作[1];如果没有时间也没关系,跟着后面的文章学习就是,每遇到一个新的知识点笔者都会进行介绍。

1.2 安装Pytorch

对于Pytorch的安装,官方网站的引导可谓十分友好,相比起Tensorflow简直不能太好。打开Pytorch官网[2],然后点击Get started就能看到如下一张选项图,选择符合自己情况的选项记得得到相应的安装命令:

图 2. Pytorch安装命令索引图

在得到安装命令后,激活之前主机上安装好的虚拟环境,然后运行即可:

图 3. Pytorch安装进程图

如果出现如上提示,则表示Pytorch极其相关依赖库正在安装,如果长时间没有反映可以考虑切换一下安装源。安装成后则可以看到如下提示:

图 4. Pytorch安装成功示例图

1.3 线性回归的实现

  • 导入相关包

    这里首先导入我们需要用到的相关包,在本示例中一共有4个:

    from sklearn.datasets import load_boston
    from sklearn.preprocessing import StandardScaler
    import numpy as np
    import torch
    

    前三个我们之前都用过就不再做介绍,第4个就是我们安装好的Pytorch框架。

  • 载入数据

    def load_data():
        data = load_boston()
        x, y = data.data, data.target
        ss = StandardScaler()
        x = ss.fit_transform(x)# 特征标准化
        x = torch.tensor(x, dtype=torch.float32)
        y = torch.tensor(y, dtype=torch.float32)
        return x, y
    

    上述代码中,前面5行我们之前也都用过;而第6,7行的作用就是将numpy类型的变量转化为Pytorch框架所接受的tensor张量。同时需要注意的是,在使用所有有关torch方法时,其所接收的变量都必须是<class 'torch.Tensor'>类型的,如果不是则需要通过torch.tensor()进行转换。

  • 预测

    def forward(x, weights, bias):  # 预测
        y = torch.matmul(x, weights) + bias
        return y
    

    这几行代码与之前在机器学习中用numpy实现的并无太大差异,只是将np.matul变成了torch.matul。同时,在定义这个函数时我们并没有使用prediction这个名字,而是使用了forward这个词,具体含义我们后续再解释。

  • 定义损失

    def loss(y, y_hat):
        return 0.5 * torch.mean((y - y_hat.reshape(y.shape)) ** 2) 
    # 一定要注意将两者reshape一样
    

    这两句代码同样很简单,但是需要注意的是在做差的时候,要将两者转换成同一个shape不然可能触发广播机制,造成不易察觉的严重错误。例如形状为[3,1]张量减去形状为[1,3]的张量,得到的结果将是一个[3,3]形状的结果(反过来也是一样)。

  • 定义评估标准

    def RMSE(y, y_hat):
        return torch.sqrt(loss(y, y_hat))
    

    在这里,我们依旧选择RMSE作为评估标准,即损失开方即可。

  • 定义梯度下降

    def gradientDescent(params, lr):
        for param in params:
            param.data -= lr * param.grad
            param.grad.zero_()
    

    其中前三行代码是执行参数的梯度下降步骤,需要注意的是由于在定义损失的时候计算的是MSE,也就是已经除以了样本总数,所有在第3行代码中param.grad就是每个样本的平局梯度不需要再除以样本数。同时,最后一行代码是将先前的梯度值清除。因为在Pytorch中,每次计算出来的梯度都会累积(accumulate)到一起,所有每一次梯度下降后都需要置为0。

  • 训练模型

    def train(x, y):
        epoches = 100
        lr = 0.3
        w = torch.tensor(np.random.normal(0, 0.1, [x.shape[1], 1]), 			                              dtype=torch.float32, requires_grad=True)
        b = torch.tensor(np.random.randn(1), dtype=torch.float32, requires_grad=True)
        for i in range(epoches):
            logits = forward(x, w, b)
            l = loss(y, logits)
            l.backward()
            gradientDescent([w, b], lr)
            if i % 5 == 0:
                print("Epoch: {}, loss: {}".format(i, l))
        logits = forward(x, w, b)
        print("RMSE: {}".format(RMSE(y, logits)))
    

    如上代码所示,其中第4、5行为初始化权重和截距,其原理就是将一个numpy类似的值转换为tensor,同时需要指定类型;最后requires_grad=True表示将该张量设置为一个可求导的状态,也就是说后面如果我们需要对某个变量求导,那么这个变量在定义的时候该属性就需要设置为True。从第6行代码开始就是执行训练的步骤,l=loss(y,logits)是用来计算整个目标函数的损失,而第7行代码l.backward()就是用来求解损失l关于所有变量的梯度。可以看到一行代码就能求解得到所有参数的梯度,这就是框架的作用。

    同时,第10行代码就开始调用梯度下降算法。值得注意的是在函数gradientDescent()中,param其实就是wb,而param.data访问的就是param对应的值所进行的原地(in-place)操作,暂时不理解也无所谓,知道更新梯度是这么写的即可。

  • 运行结果

    Epoch: 0, loss: 307.88818359375
    Epoch: 10, loss: 12.249261856079102
    Epoch: 20, loss: 11.236662864685059
    Epoch: 30, loss: 11.09315013885498
    Epoch: 40, loss: 11.032454490661621
    Epoch: 50, loss: 11.000597953796387
    Epoch: 60, loss: 10.982012748718262
    Epoch: 70, loss: 10.970415115356445
    Epoch: 80, loss: 10.96288013458252
    Epoch: 90, loss: 10.957874298095703
    RMSE: 3.309760093688965
    

    可以发现,随着迭代次数的增加,大约在100轮之后损失函数就开始收敛了。最终得到的RMSE指标为3.30。

经过上面的介绍,我们算是对如何通过Pytorch框架来实现一个简单的线性回归有了一定的了解。接下来我们就开始进入本篇文章主要内容。

2 动手实现正弦函数拟合

在接下来的这部分内容中,我们将通过一个两层的神经网络来拟合余弦函数,并且同时还会将拟合的整个过程进行可视化,与原始图像进行比较。

2.1 网络结构图

在上一篇文章中我们说到一个重要观点,所谓深度学习就是指将原始特征通过多层神经网络进行抽象特征提取,然后再将提取得到的特征输入到最后一层进行回归或者分类的处理过程。同时,对于输出层之前的所有层,我们都可以将其看成是一个特征提取的过程,而且越靠后的隐含层也就意味着提取得到的特征越抽象。这就意味着,如果我们想要最终预测得到的结果越准确,那么就需要提取得到更多抽象的特征。因此对于拟合正弦函数这么一个任务,我们先根据如下的一个网络结构进行建模:

图 5. 拟合正弦函数网络结构图(偏置未画出)

如图5所示便为我们需要实现用来拟合正弦函数的网络结构图。从图中可以看出其一共包含有两个全连接层,其中第一个全连接层用来对原始输入的一个特征进行特征提取;而第二个全连接层则是用来进行回归。同时,网络结构中并没有显式的画出隐藏层有多少个神经元,这也就意味着其实事先我们并不明确需要将原始的一维特征提取到多少维特征后才更有利于最后的回归任务。因此它是一个超参数,但一定程度上来说其数量越多,越有利于后续的回归任务。

2.2 拟合正弦函数

  • 导入相关包

    import torch
    import numpy as np
    import matplotlib.pyplot as plt
    

    由于后期需要可视化,所以导入了matplotlib库。

  • 构造数据集

    def make_data():
        num_samples = 200
        x = torch.linspace(-np.pi, np.pi, num_samples, 
                           dtype=torch.float32).reshape(-1, 1)
        y = torch.sin(x)
        return x, y
    

    这里我们用torch.linspace()来构造区间 [ − π , π ] [-\pi,\pi] [π,π]之间的200个样本点。同时,损失函数和梯度下降部分同上面一样就不再赘述。

  • 预测

    def forward(x, w1, b1, w2, b2):
        out1 = torch.matmul(x, w1) + b1
        out1 = torch.sigmoid(out1)
        out2 = torch.matmul(out1, w2) + b2
        return out2
    

    根据图5中的结构,我们能写出如上5行代码,其中第3行代码是对提取得到的特征进行非线性变换,第4行代码为进行回归预测。

  • 训练

    def train(x, y):
        input_nodes = x.shape[1]# 1
        hidden_nodes = 50
        output_nodes = 1
        epoches = 3000
        lr = 0.2
        w1 = torch.tensor(np.random.normal(0, 0.5, [input_nodes, hidden_nodes]), dtype=torch.float32, requires_grad=True)
        b1 = torch.tensor(0, dtype=torch.float32, requires_grad=True)
        w2 = torch.tensor(np.random.normal(0, 0.5, [hidden_nodes, output_nodes]), dtype=torch.float32, requires_grad=True)
        b2 = torch.tensor(0, dtype=torch.float32, requires_grad=True)
        #.....
        #.....
    

    由于篇幅有限如上所示仅为部分的代码,完整部分请直接参见示例代码[3]。从第3行代码可以看到我们将隐藏层神经元的个数设置为了50,后面也会对不同值的结果进行比较。

  • 结果

图 6. 拟合结果示意图

如图所示为正弦函数在拟合过程中的变化图,其中蓝色圆点为根据 s i n ( x ) sin(x) sin(x)生成的训练集,变化的曲线为拟合过程中的情况。

2.3 对比

图6中的所示为当隐藏层个数设置为50时的结果,接下来我们再来看看不同神经元个数下的结果对比:

表 1. 不同数量神经元下的RMSE结果表
从表1中可以看出,在一定程度上该隐藏层神经元的个数越多,模型最终的RMSE越低;但这也并不意味着神经元越多越好。其原因在于,尽管从纵向的角度来增加神经元个数可以丰富原始单一特征的表达能力,但是这些神经元所蕴含的信息仍旧比较浅显;因此更多的做法是从横向的角度来加深网络的宽度,通过多层叠加的非线性变换达到深层特征提取的目的,而这才是深度学习的核心。例如采用一个5层,每层50个神经元的神经网络。这个角度的比较我们将在后续实验中进行比较。

3 总结

在这篇文章中,笔者首先介绍了深度学习中常用的几种深度学习框架,并且还提到了这些深度学习框架的核心功能之一就是自动微分;接着介绍了如何通过Pytorch来实现一个简单的线性回归模型;最后介绍了如何通过Pytorch来实现一个简单的两层神经网络对正弦函数进行拟合,以及对比了不同神经元个数下的回归效果。同时,我们需要明白的是深度学习的“深”主要体现在的是网络层数(而不是其中一层神经元的个数)的多少,网络越深也就表示最终提取得到的特征表达越复杂、越抽象,更有利于后续的任务。 本次内容就到此结束,感谢您的阅读!

本次内容就到此结束,感谢您的阅读!如果你觉得上述内容对你有所帮助,欢迎关注并传播本公众号!若有任何疑问与建议,请添加笔者微信’nulls8’加群进行交流。青山不改,绿水长流,我们月来客栈见!

引用

[1]深度学习与PyTorch入门实战教程 https://www.bilibili.com/video/BV11z4y1R748?p=39

[2]Pytorch官网 https://pytorch.org/

[3]示例代码:https://github.com/moon-hotel/DeepLearningWithMe

推荐阅读

[1]你告诉我什么是深度学习

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页