PS:要转载请注明出处,本人版权所有。

PS: 这个只是基于《我自己》的理解,

如果和你的原则及想法相冲突,请谅解,勿喷。

前置说明

  本文作为本人csdn blog的主站的备份。(BlogID=105)

环境说明
  • Windows 10
  • VSCode
  • Python 3.8.10
  • Pytorch 1.8.1
  • Cuda 10.2

前言


  从我2017毕业到现在为止,我的工作一直都是AI在边缘端部署落地等相关内容。所以我的工作基本都集中在嵌入式+Linux+DL三者之内的范围,由于个人兴趣和一些工作安排,就会去做一些模型移植的工作,所以我会经常接触模型基本结构,前处理、后处理等等基本的知识,但是其实我很少去接触模型怎么来的这个问题。虽然以前也硬啃过Lenet5和BP算法,也按照别人弄好的脚本训练过一些简单的模型,但是从来没有认真仔细的看过这些脚本,这些脚本为什么这样写。

  在2019年下半年,随着我移植模型的工作深入,接触的各种硬件平台越来越多,经常遇见一些层在此平台无法移植,需要拆出来特殊处理。这让我产生了为啥这些层在这些特定平台不能够移植的疑问?为啥替换为别的层就能正常工作?为啥此平台不提供这个层?于是我去请教我们的算法小伙伴们,他们建议我如果要解决这个问题,建议我学习一下DL的基本知识,至少要简单了解从数据采集及处理、模型搭建及训练、模型部署等知识,其中模型部署可能是我最了解的内容了。利用一些闲暇时间和工作中的一些机会,我对以上需要了解的知识有了一个大概的认知。随着了解的深入,可能我也大概知道我比较缺一些基础知识。经过小伙伴的推荐和自己的搜索,我选择了《动⼿学深度学习.pdf》作为我的基础补全资料。

  本文是以‘补全资料’的Chapter3中线性规划为基础来编写的,主要是对‘补全资料’之前的基础知识的一个简单汇总,包含了深度学习中一些基本的知识点,同时会对这些基本知识点进行一些解释。

  由于我也是一个初学者,文中的解释是基于我的知识水平结构做的‘特色适配’,如果解释有误,请及时提出,我这里及时更正。写本文的原因也是记录我的学习历程,算是我的学习笔记。

回归概念


  回归是一种建模方法,得到的模型表示了自变量和因变量的关系。因此回归还可以解释为一种事与事之间的联系。对我们来说最常见的例子就是我们学习过的函数。例如函数Y=aX+b,这里的Y=aX+b就是我们模型, X代表自变量,Y代表因变量。

  这里顺便多说一句,深度学习是机器学习的子集。建模方法很多,回归只是其中的一种。

线性回归

  线性回归就是自变量和因变量是线性关系,感觉跟废话一样,换个方式表达就是自变量是一次。例如:Y=aX+b, Y=aX1+bX2+c, 这里的X、X1、X2都是一次的,不是二次或者更高的。

非线性回归

  非线性回归就是自变量和因变量不是是线性关系,同样感觉跟废话一样,换个方式表达就是自变量是二次及以上的。这里和线性回归对比一下就行。

基于y=aX1^2 + bX1^2 + cX2+dX2+e的回归Pytorch实例


  此实例是《动⼿学深度学习.pdf》中线性回归的从零实现的变种。从零实现,可以了解到许多的基本知识。

  此小节基本按照数据采集及处理、模型搭建及训练和模型部署来描述。

带噪声数据采集及处理

  我们知道,在我们准备数据时,肯定由于各种各样的原因,会有各种干扰,导致我们根据实际场景采集到的数据,其实不是精准的,但是这并不影响我们建模,因为我们的模型是尽量去拟合真实场景的情况。

  生成我们的实际模型的噪声数据,也就是我们常见的数据集的说法。如下是代码:

import numpy as np
def synthetic_data(w1, w2, b, num_examples): #@save
"""⽣成y = X1^2w1 + X2w2 + b + 噪声。"""
# 根据正太分布,随机生成我们的X1和X2
# X1和X2都是(1000, 2)的矩阵
X1 = np.random.normal(0, 1, (num_examples, len(w1)))
X2 = np.random.normal(0, 1, (num_examples, len(w2))) # 基于X1,X2,true_w1, true_w2, true_b, 通过向量内积、广播等方法计算模型的真实结果
y = np.dot(X1**2, w1) + np.dot(X2, w2) + b # 通过随机噪声加上真实结果,生成我们的数据集。
# y是(1000, 1)的矩阵
y += np.random.normal(0, 0.1, y.shape) return X1, X2, y.reshape((-1, 1)) true_w1 = np.array([5.7, -3.4])
true_w2 = np.array([4.8, -3.4])
true_b = 4.2 # 这里我们得到了我们的数据集,包含了特征和标签
features1, features2, labels = synthetic_data(true_w1, true_w2, true_b, 1000) # 因为我们知道我们的模型是y=aX1^2+bX1^2+cX2+dX2+e,于是我们知道a的数据分布是类似y=ax^2+b的这种形状。于是我们知道c的数据分布是类似y=ax+b的这种形状。
# 我们可以通过如下代码验证一下
plt.scatter(features1[:, 0], labels[:], 1, c='r')
plt.scatter(features2[:, 0], labels[:], 1, c='b')
plt.show()

其实从图中可以看到,红色的是类似y=ax^2+b的这种形状,蓝色是类似y=ax+b这种形状,但是他们都不是在一条线,说明我们的设置的噪声项是有效的。

  同时这里我们还要准备一个函数用来随机抽取特征和标签,一批一批的进行训练。

def data_iter(batch_size, features1, features2, labels):
num_examples = len(features1)
indices = list(range(num_examples))
np.random.shuffle(indices) # 样本的读取顺序是随机的 for i in range(0, num_examples, batch_size):
j = np.array(indices[i: min(i + batch_size, num_examples)])
# print(features1.take(j, axis=0).shape)
yield torch.tensor(features1.take(j, 0)), torch.tensor(features2.take(j, 0)), torch.tensor(labels.take(j)) # take函数根据索引返回对应元素
模型搭建及训练

  对于我们这个实例来说,模型就是一个二元二次函数,此外这里的模型也叫作目标函数。所以,下面我们用torch来实现它就行。

def our_func(X1, X2, W1, W2, B):
# print(X1.shape) (100, 2)
# print(W1.shape) (2, 1)
net_out = torch.matmul(X1**2, W1) + torch.matmul(X2, W2) + B
# print(net_out.shape) (100, 1)
return net_out

注意哟,这里的X1,X2,W1,W2,B都是torch的tensor格式。

  由数据采集及处理可知,在我们设定的真实true_w1,true_w2和true_b的情况下,我们得到了许许多多的X1,X2和y。我们要做的事情是求出W1、W2和b,这里看起是不是很矛盾?我们已知了true_w1,true_w2,true_b然后去求w1,w2,b。这里其实是一个错觉,由于在实际情况中,我们可能会得到许许多多的X1,X2和y,这些数据不是我们模拟生成的,而是某种关系实际产生的数据,只是这些数据被我们收集到了。在实际情况下,而且我们仅仅只能够得到X1,X2和y,我们通过观察X1,X2和y的关系,发现他们有一元二次和一元一次关系,所以我们建立的模型为y=aX12+bX12+cX2+dX2+e,这个过程称为建模。

  通过上面的说明,我们知道这个模型我们要求a,b,c,d,e这些参数的值,我们求这些参数的过程叫做训练。对于这个实例来说,这个过程也叫作求解方程,这里其实我们可以通过解方程的方法把这5个参数解出来,但是在实际情况中,我们建立的模型可能参数较多,可能手动解不出来,于是我们要通过训练的方式,去拟合这些参数。所以这里一个重要的问题就是怎么拟合这些参数?

  如果学习过《数值分析》这门课程的话,其实就比较好理解了,如果要拟合这些参数,我们有许多的方法可以使用,但是基本分为两类,一类是针对误差进行分析,一类是针对模型进行分析。那么在深度学习中,我们一般是对误差进行分析,也就是我们常说的梯度下降法,我们需要随机生成这些参数初始值(w1,w2,b),然后根据我们得到的X1,X2和y,通过梯度下降方法可以得到新的w1',w2',b',且w1',w2',b'更加接近true_w1,true_w2和true_b。这就是一种优化过程,经过多次优化,我们就有可能求出跟接近与true_w1,true_w2和true_b的值。

  上面啰嗦了一大堆之后,我这里正式引入损失函数这个概念,然后我们根据损失函数去优化我们的w1,w2,b。我们定义这个实例的损失函数如下:

def loss_func(y_train, y_label):
# print(y_train.shape)
# print(y_label.shape)
return ((y_train - y_label)**2)/2

  这里我们y_train就是我们得到的训练值,y_label就是我们采集到数值,这里的损失函数就是描述训练值和标签的差多少,那么我们只需要让这个损失函数的值越来越小就行,那么可能问题就变成了求损失函数的极小值问题,我们在这里还是用梯度下降的方法来求损失函数的极小值。

  这里要回忆起来一个概念,梯度是一个函数在这个方向增长最快的方向。我们求损失函数的极小值的话,就减去梯度就行了。

  这里还要说一句,损失函数有很多(L1,MSE,交叉熵等等),大家以后可以自己选一个合适的即可,这里的合适需要大家去学习每种损失函数的适用场景。这里的平方损失的合理性我个人认为有两种:

  • 1 直观法,平方损失函数描述的是在数据集上,训练值和真实值的误差趋势,我要想得到的参数最准确,就要求误差最小,误差最小就要求我去求解损失函数的极小值,这是我所了解的数学知识的直观反映。 (直觉大法)
  • 2 数学证明如下:我们生成数据或者采样数据的时候,他们的误差服从正态分布( $P(x) = 1/\sqrt{2\pi\sigma2}*exp((-1/2\sigma2)(x-\mu)^2)$ ),于是我们根据条件概率得到特定feature得到特定y的概率为: $P(y|X) = 1/\sqrt{2\pi\sigma2}*exp((-1/2\sigma2)(y-w1X12-w2X2-b)2)$,根据最大似然估计$P(y|X) = \prod\limits_{i=0}{n-1}P(y{i}|X^{i})$,此时最大似然估计最大,w1,w2,b就是最佳的参数,但是乘积函数的最大值不好求,我们用对数转换一下,变为各项求和,只需要保证含义一致就行。由于一般的优化一般是说最小化,所以我们要取负的对数和$-\log(P(y|X))$,此时我们把最大似然估计函数转换为对数形式:$-\log(P(y|X)) = \sum\limits_{i=0}{n-1}1/2\log(2\pi\sigma2)+1/2\sigma2(y-w1X12-w2X2-b)^2)$,我们可以看到要求此函数的最小值,其实就是后半部分的平方是最小值就行,因为其余的都是常数项,而后面部分恰好就是我们的平方损失函数。(Copy书上大法)

  这里的梯度下降法就是(w1,w2,b)-lr*(w1,w2,b).grad/batch_size,代码如下:

def sgd(params, lr, batch_size): #@save
"""⼩批量随机梯度下降。"""
for param in params:
with torch.no_grad():
param[:] = param - lr * param.grad / batch_size

  这里我画出我们的损失函数的三维图像,我们把损失函数简化为Z=aX^2+bY+c的形式。其图像如下图:

我们可以看到这个曲面有很多极小值,当我们随机初始化a,b,c的时候,我们会根据X和Y得到部分损失点,这时我们求出a,b,c的梯度,当我们对a,b,c减去偏导数时,就会更快的靠近损失函数的谷底,我们的当我们的损失函数到极小值时,我们就认为此时的a,b,c是我们要找的参数。这就是我们的梯度下降法的意义所在。

  下面就直接写训练代码就行。

w1 = torch.tensor(np.random.normal(0, 0.5, (2, 1)), requires_grad=True)
w2 = torch.tensor(np.random.normal(0, 0.5, (2, 1)), requires_grad=True)
b = torch.tensor(np.ones(1), requires_grad=True) # print(w1.grad)
# print(w1.grad_fn)
#
lr = 0.001
num_epochs = 10000
net = our_func
loss = loss_func batch_size = 200 for epoch in range(num_epochs):
for X1, X2, y in data_iter(batch_size, features1, features2, labels):
# print(X1.shape) (100, 2)
# print(y.shape) (100, 1)
l = loss(net(X1, X2, w1, w2, b), y.reshape(batch_size, 1)) # `X`和`y`的⼩批量损失
# print(f'epoch {epoch + 1}, before sdg loss {float(l.mean()):f}')
# 计算l关于[`w`, `b`]的梯度
# print(l.shape) # l.backward() default call, loss.backward(torch.Tensor(1.0))
w1.grad = None
w2.grad = None
b.grad = None l.backward(torch.ones_like(y.reshape(batch_size, 1))) sgd([w1, w2, b], lr, batch_size) # 使⽤参数的梯度更新参数 l1 = loss(net(X1, X2, w1, w2, b), y.reshape(batch_size, 1)) # `X`和`y`的⼩批量损失
# print(f'epoch {epoch + 1}, after sdg loss {float(l1.mean()):f}') train_l = loss(net( torch.from_numpy(features1), torch.from_numpy(features2), w1, w2, b), torch.from_numpy(labels))
# print(train_l.sum())
if (epoch % 1000 == 0):
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}') print('train_w1 ',w1)
print('train_w2 ',w2)
print('train_b ',b)

  这里做的事情就是首先设定训了次数,学习率,批次数量参数,然后随机生成了w1,w2,b,然后通过data_iter取一批数据,算出loss,清空w1,w2,b的梯度,对loss求w1,w2,b的偏导数,调用sdg求新的w1,w2,b。最后计算新参数在整个数据集上的损失。

  这里尤其需要注意的是在多次迭代过程中,一定要清空w1,w2,b的梯度,因为它的梯度是累加的,不会覆盖。这里用的Pytorch的自动求导模块,其实底层就是调用的bp算法,利用了链式法则,反向从loss开始,能够算出w1,w2,b的偏导数。

这里我们看到最终的平均误差到了一定值后就下降不了,这和我们的数据有关系,我们只需要关心这个误差我们能够接受吗?能接受,我们就训练得到了成功的模型,如果不能接受,我们就改参数重新来,这是一个多次试验尝试的过程。

  我们从上文可知,true_w1 = np.array([5.7, -3.4]),true_w2 = np.array([4.8, -3.4]),true_b = 4.2。我们训练出来的值是w1=(5.7012, -3.3955),w2=(4.8042, -3.4071),b=4.2000。可以看到其实训练得到的值还是非常接近的,但是这个任务其实太简单了。

后记


  这里主要用到了许多基本的概念,一个是数据集的收集和处理,模型搭建,模型训练,优化方法、损失函数等等。至少通过本文,我们知道了得到一个模型的基本过程,其实普遍性的深度学习就是使用新的损失函数、搭建新的模型、使用新的优化方法等等。

  从文中特例我们可以发现,我们只利用了Pytorch自带的自动求导模块,其他的一些常见的深度学习概念都是我们手动实现了,其实这些好多内容都是Pytorch这些框架封装好的,我们没有必要自己手写,但是如果是初次学习的话,建议手动来写,这样认知更加深刻。

参考文献


打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)

PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。

DL基础补全计划(一)---线性回归及示例(Pytorch,平方损失)的更多相关文章

  1. DL基础补全计划(二)---Softmax回归及示例(Pytorch,交叉熵损失)

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  2. DL基础补全计划(三)---模型选择、欠拟合、过拟合

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  3. DL基础补全计划(六)---卷积和池化

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  4. DL基础补全计划(五)---数值稳定性及参数初始化(梯度消失、梯度爆炸)

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  5. OSPF补全计划-0 preface

    哇靠,一看日历吓了我一跳,我这一个月都没写任何东西,好吧,事情的确多了点儿,同事离职,我需要处理很多untechnical的东西,弄得我很烦,中间学的一点小东西(关于Linux的)也没往这里记,但是我 ...

  6. 【hjmmm网络流24题补全计划】

    本文食用方式 按ABC--分层叙述思路 可以看完一步有思路后自行思考 飞行员配对问题 题目链接 这可能是24题里最水的一道吧... 很显然分成两个集合 左外籍飞行员 右皇家飞行员 跑二分图最大匹配 输 ...

  7. 2018.我的NOIP补全计划

    code: efzoi.tk @ shleodai noip2011 D1 选择客栈 这道题是一道大水题,冷静分析一会就会发现我们需要维护最后一个不合法点和前缀和. 维护最后一个不合法点只要边扫描边维 ...

  8. OSPF补全计划-2

    想起来几个面试题: 1. OSPF在什么情况下会stuck in Exstart /Exchange状态? 我知道的一个答案是两个端口的mtu不一致.当然整个也不是绝对,因为可以用ip ospf mt ...

  9. OSPF补全计划-1

    OSPF全称是啥我就不絮叨了,什么迪杰斯特拉,什么开放最短路径优先算法都是人尽皆知的事儿,尤其是一提算法还会被学数据结构的童鞋鄙视,干脆就不提了,直接开整怎么用吧.(不过好像真有人不知道OSPF里的F ...

随机推荐

  1. 29.Map,可变参数

    1.Map集合 1.1Map集合概述和特点[理解] 单列集合一次存一个元素 双列集合一次存两个元素 键:不能重复的        值:可以重复的 Map集合概述 interface Map<K, ...

  2. pytorch实现LeNet5分类CIFAR10

    关于LeNet-5 LeNet5的Pytorch实现在网络上已经有很多了,这里记录一下自己的实现方法. LeNet-5出自于Gradient-Based Learning Applied to Doc ...

  3. [Python] Virtualenv 使用

    参考 https://www.jianshu.com/p/b6e52b80653f

  4. 3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习

    3.1.5 LTP(Linux Test Project)学习(五)-LTP代码学习 Hello小崔 ​ 华为技术有限公司 Linux内核开发 2 人赞同了该文章 LTP代码学习方法主要介绍两个步骤, ...

  5. JDK版本升级

    背景:本来安装了一个1.6版本的JDK,因为版本过低需要升级成1.8 安装过程很简单一路next,主要是遇到几个问题需要备注一下解决方法. Error opening registry key'sof ...

  6. Https实践

    https实践 常用端口 ssh 22 telnet 23 ftp 21 rsync 873 http 80 mysql 3306 redis 6379 https 443 dns 53 php 90 ...

  7. 042.Python进程队列介绍

    进程队列介绍 1  基本语法及过程 先进先出,后进后出,q = Queue() 过程 (1)把数据放到q队列中 put (2)把书局从队列中拿出来 get from multiprocessing i ...

  8. Scala 字符串插值器

    Scala 提供了三种创新的字符串插值方法:s,f和raw,使用他们我们可以方便快捷的组合字符串. s 字符串插值器 在任何字符串前加上s,就可以直接在串中使用变量了,在生成字符串的时候会隐式调用其t ...

  9. shell基础之shell相关概念

    一.脚本介绍 1.脚本简单地说就是一条条的文字命令(一些指令的堆积),这些文字命令是可以看到的(如可以用记事本打开查看.编辑). 常见的脚本: JavaScript(JS,前端),VBScript, ...

  10. shell脚本编写习惯

    前言:在公众号看一篇比较不错的shell脚本文章,自己学习同时,加一些例子分享下,哪里做得不好,请多多指教哈一.在脚本写注释 1 #脚本的参数 2 #脚本的用途 3 #脚本的注意事项 4 #脚本的写作 ...