李沐动手学深度学习:08 线性回归(代码逐行理解)

一、相关资料连接

1.1 李沐视频

1.2 代码、PPT

  1. 链接: 哔哩哔哩李沐课程
  2. 链接: 课程代码及PPT

二、代码及笔记(使用Jupyter Notebook)

2.1 线性回归从零开始实现

2.1.1 基本概念

  1. 线性回归是对n维输入的加权,外加偏差
  2. 使用平方损失来衡量预测值和真实值之间的差异
  3. 线性回归有显示解
  4. 线性回归可以看作单层神经网络

2.1.2 基础优化算法

  1. 梯度下降:
    梯度:使得这个函数增加最快的方向,负梯度:是使得这个函数下降最快的方向
  2. 小批量随机梯度下降:
    在整体训练集上计算梯度太贵了,可以随机采样b个样本来接近损失,b是批量大小是另外一个超参数
  3. 小结:
    (1)梯度下降通过不断沿着反梯度方向更新参数求解
    (2)小批量随机梯度下降是深度学习默认的求解算法
    (3)两个重要的超参数:批量大小和学习率(步长)

2.1.3 代码逐行理解

1. 我们将从零开始实现整个方法,包括数据流水线、模型、损失函数和小批量随机梯度下降优化器

01	!pip install -U d2l    # 首先安装d2l包
02	%matplotlib inline
	import random              #随机梯度下降和随机初始化我们的权重
	import torch
	from d2l import torch as d2l

(1) 根据带有噪声的线性模型构造一个人造数据集。 我们使用线性模型参数w=[2,−3.4]^⊤、b=4.2和噪声项ϵ生成数据集及其标签:y=Xw+b+ϵ

03	def synthetic_data(w,b,num_examples):    # num_examples:n个样本
	    '''生成 y=Xw+b+噪声'''
	    X = torch.normal(0,1,(num_examples,len(w)))  #生成 X,他是一个均值为0,方差为1的随机数,他的大小: 行为num_examples,列为w的长度表示多少个feature
	    y = torch.matmul(X,w) + b
	    y += torch.normal(0,0.01,y.shape)            #加入一些噪音,均值为0 ,方差为0.01,形状和y是一样
	    return X, y.reshape((-1,1))                  #把X和y作为一个列向量返回
	
	true_w = torch.tensor([2,-3.4])
	true_b = 4.2
	features,labels = synthetic_data(true_w,true_b,1000) #synthetic_data这个函数返回的是特征和标签,相当于分别吧真实的房屋‘关键因素’和对应的‘房价’列出来了


# 这里的X指房屋的关键因素集,长度len(w)即列数,表明有len(w)个关键因素,这里是2,比如‘卧室个数’和‘住房面积’两个关键因素,X的行数num_example=房屋的数量
# 以上相当于去市场调研收集真实的房屋数据

(2) 输出显示

04	print('features:', features[0], '\nlabel:', labels[0])


# 运行结果
features: tensor([-0.7228,  0.3353]) 
label: tensor([1.6056])

(3) 可视化显示

05	d2l.set_figsize()
	d2l.plt.scatter(features[:, 1].detach().numpy(),     #detach()分离出数值,不再含有梯度
	                labels.detach().numpy(), 1);         #scatter()函数的最后一个1是绘制点直径大小
	                
	#把feature的第一列和labels绘出来,是有线性相关的性质
	#显示结果如下图:

在这里插入图片描述

06	d2l.set_figsize()
	d2l.plt.scatter(features[:, 0].detach().numpy(),    
	                labels.detach().numpy(), 10); 
	                
	#修改为绘制第所有feature的第0列和label的关系,点的大小设置为10
	#第0列正相关,第1列负相关   w=[2,3.4]0列对应w的2,斜率为正的直线;1列对应w的-3.4,斜率为负的直线,所以一个正相关一个负相关
	#显示结果如下图:

在这里插入图片描述
2. 每次读取一个小批量:定义一个data_iter 函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量

07	def data_iter(batch_size, features, labels):    # data_iter函数接收批量大小、特征矩阵和标签向量作为输入
	    num_examples = len(features)                    
	    indices = list(range(num_examples))         # 生成每个样本的index,随机读取,没有特定顺序。range随机生成0 —(n-1,然后转化成python的list
	    random.shuffle(indices)                     # 将下标全都打乱,打乱之后就可以随机的顺序去访问一个样本
	    for i in range(0, num_examples, batch_size):   # 每次从0开始到num_examples,每次跳batch_size个大小
	        batch_indices = torch.tensor(              # 把batch_size的index找出来,因为可能会超出我们的样本个数,所以最后如果没有拿满的话,会取出最小值,所以使用min
	            indices[i:min(i +batch_size, num_examples)]) 
	        yield features[batch_indices], labels[batch_indices]
	
	        
	batch_size = 10
	
	for X, y in data_iter(batch_size, features, labels):          #调用data_iter这个函数返回iterator(迭代器),从中拿到X和y
	    print(X, '\n', y)                                         #给我一些样本标号每次随机的从里面选取一个样本返回出来参与计算
	    break
	    
	    
	# 只是indices这个List被打乱了,feature和labels都是顺序的,用循环才能随机的放进去 
	#(构造一个随机样本。把样本的顺序打乱,然后间隔相同访问,也能达到随机的目的)
	# yield是构造一个生成器,返回迭代器。yield就是return返回一个值,并且记住返回的位置,下次迭代就从这个开始
	

	# 运行结果如下:
	tensor([[-0.2374,  0.5196],
        [ 1.6525, -0.3879],
        [ 0.0729, -0.3433],
        [-0.1988, -1.0836],
        [ 0.9156, -0.2383],
        [ 2.5247,  0.5561],
        [ 1.5504, -0.8997],
        [-0.6272, -0.0649],
        [-0.6743,  0.0073],
        [-2.0423, -0.2847]]) 
	 tensor([[ 1.9519],
	        [ 8.8361],
	        [ 5.5263],
	        [ 7.4791],
	        [ 6.8320],
	        [ 7.3638],
	        [10.3666],
	        [ 3.1665],
	        [ 2.8238],
	        [ 1.0864]])

3. 定义 初始化模型参数

08	w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)       # w:size为21,随机初始化成均值为0,方差为0.01的正态分布,requires=true是指需要计算梯度
	b = torch.zeros(1, requires_grad=True) #对于偏差来说直接为01表示为一个标量,因为也需要进行更新所以为True
	
	
	#广播机制:当我们用一个向量加一个标量时,标量会被加到每一个分量上

4. 定义模型

09	def linreg(X, w, b):  
	    """线性回归模型。"""
	    return torch.matmul(X, w) + b    #矩阵乘以向量再加上偏差

5. 定义损失函数

10	def squared_loss(y_hat, y):         #y_hat是预测值,y是真实值
	    """均方损失。"""
	    return (y_hat - y.reshape(y_hat.shape))**2 / 2      #按元素做减法,按元素做平方,再除以2  (这里没有做均方)

6. 定义优化算法

11	def sgd(params, lr, batch_size):   # 优化算法是sgd,他的输入是:params给定所有的参数,这个是一个list包含了w和b,lr是学习率,和batch_size大小
	    """小批量随机梯度下降。"""
	    with torch.no_grad():      # 这里更新的时候不需要参与梯度计算所以是no_grad
	        for param in params:   # 对于参数中的每一个参数,可能是w可能是b
	            param -= lr * param.grad / batch_size   # 参数减去learning rate乘以他的梯度(梯度会存在.grad中)。上面的损失函数中没有求均值,所以这里除以了batch_size求均值,因为乘法对于梯度是一个线性的关系,所以除以在上面损失函数那里定义和这里是一样的效果
	            param.grad.zero_()     # 把梯度设置为0,因为pytorch不会自动的设置梯度为0,需要手动,下次计算梯度的时候就不会与这次相关了
	
	
	# 我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size)来归一化步长,这样步长大小就不会取决于我们对批量大小的选择
	# 更新数据时不需要求导
	# pytorch 会不断累加变量的梯度,所以每更新一次参数,就要让其对应的梯度清零

7. 训练过程

12	lr = 0.03                  # 首先指定一些超参数:学习率为0.03
	num_epochs = 3             # epoch为3表示把整个数据扫3遍 
	net = linreg               # network为linreg前面定义的线性回归模型
	loss = squared_loss        # loss为均方损失
	
	for epoch in range(num_epochs): # 训练的过程基本是两层for循环(loop),第一次for循环是对数据扫一遍
	    for X, y in data_iter(batch_size, features, labels):   # 对于每一次拿出一个批量大小的X和y
	        l = loss(net(X, w, b), y)     # 把X,w,b放进network中进行预测,把预测的y和真实的y来做损失,则损失就是一个长为批量大小的一个向量,是X和y的小批量损失
	                                      #l(loss)的形状是('batch_size',1,而不是一个标量
	        l.sum().backward()            #对loss求和然后算梯度。计算关于['w','b']的梯度
	        sgd([w, b], lr, batch_size)   #算完梯度之后就可以访问梯度了,使用sgd对w和b进行更新。使用参数的梯度对参数进行更新
	    #对数据扫完一遍之后来评价一下进度,这块是不需要计算梯度的,所以放在no_grad里面
	    with torch.no_grad():
	        train_l = loss(net(features, w, b), labels)      #把整个features,整个数据传进去计算他的预测和真实的labels做一下损失,然后print
	        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
	        
	   
	        
	# 求和本身是让l(即loss)以标量的形式表现出来(通过sum()转化标量之后在求梯度)(一般都是对一个标量进行求导,所以我们先对y进行求和再求导:见前面自动求导笔记)。
	# 不求和是向量,梯度算下来就是变成矩阵了,形状没有办法对应
	# 求梯度是对于l中每一个分量都是单独求的,l(loss)是一个向量每个元素表示一个样本的误差除以批量数
	# 如果没有no_grad,在评估模型的时候也进行梯度优化过程了

	#运行结果如下:
	epoch 1, loss 0.047467
	epoch 2, loss 0.000196
	epoch 3, loss 0.000047

8. 比较真实参数和通过训练学到的参数来评估训练的成功程度

13	print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
	print(f'b的估计误差: {true_b - b}')
	
	
	# 运行结果如下:
	w的估计误差: tensor([ 0.0003, -0.0010], grad_fn=<SubBackward0>)
	b的估计误差: tensor([0.0010], grad_fn=<RsubBackward1>)

9. 不同超参数的选择会有什么样的不同的效果(修改超参数)

14	lr = 0.001         # 学习率很小的时候,发现损失很大,即使把epoch调大,loss仍然很大
	num_epochs = 3
	net = linreg
	loss = squared_loss
	
	for epoch in range(num_epochs):
	    for X, y in data_iter(batch_size, features, labels):
	        l = loss(net(X, w, b), y)
	        l.sum().backward()
	        sgd([w, b], lr, batch_size)
	    with torch.no_grad():
	        train_l = loss(net(features, w, b), labels)
	        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
	
	# 重新运行的时候要把上面w和b初始化的代码重新运行一下,重新初始化w和b
	
	# 运行结果如下:
	epoch 1, loss 13.166480
	epoch 2, loss 10.856715
	epoch 3, loss 8.952998
15	lr = 10           # 学习率很大,发现loss是一个notnumber,learning rate太大跑飞了
	num_epochs = 3
	net = linreg
	loss = squared_loss
	
	for epoch in range(num_epochs):
	    for X, y in data_iter(batch_size, features, labels):
	        l = loss(net(X, w, b), y)
	        l.sum().backward()
	        sgd([w, b], lr, batch_size)
	    with torch.no_grad():
	        train_l = loss(net(features, w, b), labels)
	        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
	
	# 重新运行的时候要把上面w和b初始化的代码重新运行一下,重新初始化w和b
	
	# 运行结果如下:
	epoch 1, loss nan
	epoch 2, loss nan
	epoch 3, loss nan

2.2 线性回归的简洁实现

2.2.1 分解代码

1. 通过使用深度学习框架来简洁的实现 线性回归模型 生成数据集

	import numpy as np
	import torch
	from torch.utils import data
	from d2l import torch as d2l
	
	true_w = torch.tensor([2, -3.4])
	true_b = 4.2
	features, labels = d2l.synthetic_data(true_w, true_b, 1000)
	
	# 构造一个真实的w和b,然后通过人工数据合成函数生成我们需要的features和labels

2. 调用框架中现有的API来读取数据

	def load_array(data_arrays, batch_size, is_train=True):  
	    """构造一个PyTorch数据迭代器。"""
	    dataset = data.TensorDataset(*data_arrays)
	    return data.DataLoader(dataset, batch_size, shuffle=is_train)
	
	batch_size = 10
	data_iter = load_array((features, labels), batch_size)
	
	next(iter(data_iter))
	
	# 假设我们已经有features和labels了,我们把他做成一个List传到tensor的dataset里面,把我们的X和y传进去,得到pytorch的一个dataset,(也就是说dataset里面是由两部分组成,features和labels)
	# dataset里面拿到数据集之后我们可以调用dataloader函数每次从里面随机挑选batch_size个样本出来,shuffle是指是否需要随机去打乱顺序,如果是train则是需要的
	# 构造了一个data_iter(可迭代对象)之后,然后用iter()转成python的一个迭代器,再通过next()函数得到一个X和y
	
	# TensorDataset:把输入的两类数据进行一一对应,一个*表示解包(见python书的P175,一个*表示使用一个已经存在的列表作为可变参数)
	# DataLoader:构建可迭代的数据装载器
	# enumerate:返回值有两个,一个是序号,一个是数据(包含训练数据和标签)
	
	#运行结果如下:
	[tensor([[-1.6881,  0.5505],
         [-0.5636, -0.4053],
         [-0.5162, -1.8817],
         [-2.0505, -1.2795],
         [-0.3089, -0.7559],
         [-1.0222,  0.6298],
         [-1.4214, -1.4331],
         [-0.3923,  0.3843],
         [-0.5184,  0.5693],
         [-1.1609,  1.0831]]),
	 tensor([[-1.0346e+00],
	         [ 4.4640e+00],
	         [ 9.5674e+00],
	         [ 4.4545e+00],
	         [ 6.1507e+00],
	         [-4.5305e-04],
	         [ 6.2074e+00],
	         [ 2.1123e+00],
	         [ 1.2354e+00],
	         [-1.8010e+00]])]

3. 使用框架的预定义好的层

	from torch import nn     # nn是neural network的缩写,里面有大量定义好的层
	
	net = nn.Sequential(nn.Linear(2, 1))
	
	# 线性回归用的是nn里面的线性层(或者说是全连接层),它唯一要指定的是输入和输出的维度是多少,此处的输入维度是2,输出维度是1
	# 线性回归就是简单的单层神经网络,为了以后的方便,放进一个Sequential的容器里面,可以理解为一个list of layers把层按顺序一个一个放在一起
	
	# Sequential是一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数
	# Sequential 是一个容器,里面可以放置任何层,不一定是线性层

4. 初始化模型参数

	net[0].weight.data.normal_(0, 0.01)
	net[0].bias.data.fill_(0)
	
	# 上面定义的net就是只有一个layer,可以通过索引[0]访问到layer,然后用.weight访问到他的w,用.data访问真实data,normal_表示使用正态分布来替换到data的值,使用的是均值为0方差为0.01来替换
	# 偏差直接设置为0
	
	#运行结果如下:
	tensor([0.])

5. 计算均方误差使用的是MSELoss类,也称平方L2范数

	loss = nn.MSELoss()  #在nn中均方误差叫MSELoss

6. 实例化SGD实例

	trainer = torch.optim.SGD(net.parameters(), lr=0.03)
	
	# SGD至少传入两个参数。net.parameters里面就包括了所有的参数,w和b;指定学习率0.03
	# Optimize.Stochastic Gradient Descent (随机梯度下降法)    optim是指optimize优化,sgd是优化的一种方法
	# L1范数是算数差,L2范数是算平方差

7. 训练过程代码与我们从零开始实现时所做的非常相似

	num_epochs = 3         # 迭代3个周期      
	for epoch in range(num_epochs):    
	    for X, y in data_iter:     # 在data_iter里面一次一次的把minibatch(小批量)拿出来放进net里面
	        l = loss(net(X), y)    # net()这里本身带了模型参数,不需要把w和b放进去了,net(X)是预测值,y是真实值,拿到预测值和真实值做Loss
	        trainer.zero_grad()    # 梯度清零
	        l.backward()           # 计算反向传播,这里pytorch已经做了sum就不需要在做sum了(loss是一个张量,求sum之后是标量)
	        trainer.step()         # 有了梯度之后调用step()函数来进行一次模型的更新。调用step函数,从而分别更新权重和偏差
	    l = loss(net(features), labels)  # 当扫完一遍数据之后,把所有的feature放进network中,和所有的Label作一次Loss
	    print(f'epoch {epoch + 1}, loss {l:f}')   # {
    
    l:f} 是指打印l,格式是浮点型

	# 运行结果如下:
	epoch 1, loss 0.000288
	epoch 2, loss 0.000094
	epoch 3, loss 0.000093

2.2.2 整体代码

	import numpy as np
	import torch
	from torch import nn
	from torch.utils import data
	from d2l import torch as d2l
	
	true_w = torch.tensor([2, -3.4])
	true_b = 4.2
	features, labels = d2l.synthetic_data(true_w, true_b, 1000)
	
	
	# 读取数据
	def load_array(data_arrays, batch_size, is_train=True):  
	    """构造一个PyTorch数据迭代器。"""
	    dataset = data.TensorDataset(*data_arrays)
	    return data.DataLoader(dataset, batch_size, shuffle=is_train)
	
	batch_size = 10
	data_iter = load_array((features, labels), batch_size)
	
	next(iter(data_iter))
	
	# 选用模型及初始化
	net = nn.Sequential(nn.Linear(2, 1))
	net[0].weight.data.normal_(0, 0.01)
	net[0].bias.data.fill_(0)
	
	# 定义损失
	loss = nn.MSELoss()  
	
	#优化
	trainer = torch.optim.SGD(net.parameters(), lr=0.03)
	
	# 开始训练
	num_epochs = 3               
	for epoch in range(num_epochs):    
	    for X, y in data_iter:     
	        l = loss(net(X), y)    
	        trainer.zero_grad()    
	        l.backward()           
	        trainer.step()        
	    l = loss(net(features), labels)  
	    print(f'epoch {epoch + 1}, loss {l:f}') 


	# 运行结果如下:
	epoch 1, loss 0.000238
	epoch 2, loss 0.000091
	epoch 3, loss 0.000090

三、直播问题解答

  1. 问:损失为什么要求平均:
    答:求平均和不求平均本质上没有区别,数据是等价的。在一个样本上,不求了平均,梯度数值会比较大,在损失上除以n就等价于在梯度上除以了n,使用随机梯度下降的话,如果loss不除以n,则把学习率除以n就可以了。如下公式:假如L中没有除以n,则梯度会变成以前的n倍大小,若想得到和之前一样,则把学习率除以n即可。学习率是使得这块不要太大也不用太小,所以除以n的好处是指:不管样本多大,不管批量有多大,我的梯度值差不多,使得调学习率的时候比较好调
    W t = W t − 1 − η ∂ L ∂ W t − 1 W_t = {W_{t-1} } -\eta \frac{\partial L}{\partial W_{t-1}} Wt=Wt1ηWt1L

  2. 性回归的损失函数一般都是MSE(均方误差)

  3. batchsize越小越容易收敛,因为随机梯度下降理论上是带来了噪音,每次采样越小带来的噪音越多,但是噪音对神经网络是一件好事情,深度神经网络太复杂了,一定的噪音可以防止过拟合,模型的泛化性越好

  4. 梯度是线性的,损失函数是每个样本的损失函数相加,其等价于每个样本求梯度取均值

  5. 随机梯度下降法中的随机:批量大小是不变的(eg:128),每次在样本中随机采样128个元素

  6. 所有的运算子会自动加入计算图求梯度,detach可以把不需要求梯度的运算分离开

猜你喜欢

转载自blog.csdn.net/m0_45521766/article/details/126621549