前言
书接上回,没看过(一)的记得看看哦!
求导
链式法则
如果某个函数由复合函数表示,则该复合函数的导数可以用构成复 合函数的各个函数的导数的乘积表示。
比如:z =t**2 t =x + y
可以得到
计算图与反向传播
计算图可以帮助我们理解局部的计算,以及写下中间得到的结果。
其中,每一步运算我们称之为一个节点。
可以看到,每一步都可以算单独的导数,将它们从后往前相乘,就得到了整体的导数
pytorch里面有很方便的自动求导,所以实现部分跳过。
线性回归
假设自变量x和因变量y之间的关系是线性的,即y可以表示为x中元素的加权和,这里通常允许包含观测值的一些噪声;其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。
为什么假设是正态分布?问了问ChatGPT。
线性回归假设误差项服从正态分布是为了确保模型参数的估计具有最佳线性无偏性(BLUE),并且在进行假设检验时能够应用传统的统计方法,如t检验和F检验。
可以发现,当误差项服从正态分布时,估计出的回归系数具有更好的置信区间和显著性测试结果。如果误差项不正态,虽然估计仍然是无偏的,但可能导致假设检验结果的不准确。
(不是很懂)
容易知道可以表示为:
自然就是预测值了
用向量和点积表示:
所以我们的任务是找到w和b使得预测误差尽可能的小。
在那之前还需要:(1)一种模型质量的度量方式(合适的损失函数); (2)一种能够更新模型以提高模型预测质量的方法。
损失函数
回归问题常用平方误差函数。
1/2是求导后更方便,没有本质区别
有n个样本的话,就对它们求平均数。
解析解
线性回归的解可以用一个公式简单地表达出来, 这类解叫作解析解。
然而不是所有的问题都有解析解。解析解对问题的限制很严格,导致它无法广泛应用在深度学习里。
极大似然估计法(MLE)
似然是什么?
似然是指在给定模型参数的情况下,观察到的数据出现的概率。在统计推断中,似然函数用于评估不同参数值对观测数据的适合程度,最终帮助我们估计模型参数。
过程
我们假设了观测中包含噪声,其中噪声服从正态分布。
可以写出通过给定的x观测到特定y的似然 :
注意,这里是条件概率的意思
根据极大似然估计法,参数w和b的最优值是使整个数据集的似然最大的值:
作为概率,P这样相乘处理很有可能会让乘积越来越小,最终被当作0。所以我们取对数,相乘变为相加,这样也不会越来越小了。
因为历史原因,优化通常是说最小化而不是最大化。 我们可以改为最小化负对数似然。
线性回归实现
import random
import torch
def synthetic_data(w,b,num_examples):
#最后一个参数是样本数
X=torch.normal(0,1,(num_examples,len(w)))
#定义了形状
y=torch.matmul(X,w)+b
y+=torch.normal(0,0.01,y.size())
#噪声
return X,y.reshape(-1,1)
true_w=torch.tensor([2,-3.4])
true_b=4.2
f,l=synthetic_data(true_w,true_b,1000)
f,l
'''
(tensor([[ 1.4515, -0.2315],
[-0.3087, -0.7018],
[ 0.0367, -0.2297],
...,
[-0.6344, 0.6715],
[ 0.4404, 0.0044],
[ 0.3537, -0.2044]]),
tensor([[ 7.8885e+00],
[ 5.9413e+00],
[ 5.0644e+00],
[ 1.5446e+00],
[ 5.8194e+00],
[ 7.8561e+00],
[ 6.3526e+00],
[-2.9164e+00],
[-2.0117e+00],
[-1.8629e+00],
[ 8.3480e+00],
[-1.4740e+00],
[ 2.9921e+00],
[ 4.1162e+00],
[ 7.3081e-01],
[ 1.1279e+00],
[ 6.4438e+00],
[-1.7765e-01],
...
[ 2.5506e+00],
[ 9.4781e+00],
[ 6.3630e-01],
[ 5.0628e+00],
[ 5.6056e+00]]))
'''
这里特征(f)的形状是(num_examples,len(w))
标签(l)的形状是(num_examples,1),列向量。
注意,这里的f中的两个元素和对应的l都有线性关系
def data_iter(batch_size,features,labels):
examples=len(features)
indices=list(range(examples))
random.shuffle(indices)
for i in range(0,examples,batch_size):
batch_indices=torch.tensor(indices[i:min(i+batch_size,examples)])
yield features[batch_indices],labels[batch_indices]
yield象征着迭代器,也就是说不会全部返回,而是一次一次返回,每次都会保留状态,也可以节省内存,用在循环中会逐步生成数据
这样写,也就是说每次返回一个小批量数据。
batch_indices赋值时使用min,防止索引超出范围
其实深度学习框架有自动迭代器,会比这样写效率更高。
w=torch.normal(0,0.01,size=(2,1),requires_grad=True)
b=torch.zeros(1,requires_grad=True)
#偏置默认为0
def linreg(X,w,b):
return torch.mm(X,w)+b
def loss(y_hat,y):
return(y_hat-y.reshape(y_hat.shape))**2/2
def sgd(params,lr,batch_size):#params传递的是引用
with torch.no_grad():
for param in params:
param-=lr*param.grad/batch_size
param.grad.zero_()
“/batch_size”是为什么呢?
因为backward()对标量使用,损失函数最后进行了求和(sum()),这样除就可以规范步长,不会受到batch_size不同的影响。
“with torch.no_grad():”是为什么呢?
在sgd函数中,使用torch.no_grad(),确保在更新参数时,不会跟踪梯度,从而避免不必要的计算,提升效率。
linreg函数是模型的计算部分,loss计算平方损失,sgd是梯度下降法。注意params传递的是引用,所以更新params里面的param会直接修改参数。
训练
以下为流程:
-
初始化参数
-
重复以下训练,直到完成
- 计算梯度
- 更新参数
注意,这里每个迭代周期(epoch)是将数据集全部遍历一次
lr=0.05#学习率,或者叫做步长
num_epoches=10#将数据集遍历3遍
batch_size=10
for epoch in range(num_epoches):
for X,y in data_iter(batch_size,f,l):#提取小批量数据
lo=loss(linreg(X,w,b),y)#计算损失
lo.sum().backward()
sgd([w,b],lr,batch_size)
with torch.no_grad():
train_l=loss(linreg(f,w,b),l)#计算整体的损失函数
print(f'epoch {epoch+1},loss {float(train_l.mean()):f}')
#.mean()计算了平均数,:f格式化输出至
#小数点后六位
w,b
'''
epoch 1,loss 0.000183
epoch 2,loss 0.000169
epoch 3,loss 0.000161
epoch 4,loss 0.000145
epoch 5,loss 0.000137
epoch 6,loss 0.000132
epoch 7,loss 0.000128
epoch 8,loss 0.000119
epoch 9,loss 0.000115
epoch 10,loss 0.000113
(tensor([[ 1.9894],
[-3.3982]], requires_grad=True),
tensor([4.2013], requires_grad=True))
'''
true_w=torch.tensor([2,-3.4])和true_b=4.2是我们最早的参数,学习结果非常接近!
题外话
不应该想当然地认为我们能够完美地求解参数。 在机器学习中,我们通常不太关心恢复真正的参数,而更关心如何高度准确预测参数。 幸运的是,即使是在复杂的优化问题上,随机梯度下降通常也能找到非常好的解。 其中一个原因是,在深度网络中存在许多参数组合能够实现高度精确的预测。
更简洁的实现
%matplotlib inline
import random
import torch
import numpy as np
from torch.utils import data
from d2l import torch as d2l
def synthetic_data(w,b,num_examples):#最后一个参数是样本数
X=torch.normal(0,1,(num_examples,len(w)))#定义了形状
y=torch.matmul(X,w)+b
y+=torch.normal(0,0.01,y.size())
return X,y.reshape(-1,1)
true_w=torch.tensor([2,-3.4])
true_b=4.2
f,l=synthetic_data(true_w,true_b,1000)
#同上
torch.utils是什么(GPT回答)
torch.utils 是 PyTorch 中的一个模块,提供了一些实用工具和功能,以帮助用户更方便地进行深度学习和数据处理。以下是 torch.utils 中常见的一些子模块和功能:
-
torch.utils.data:用于处理数据加载和预处理,包括:Dataset:自定义数据集的基类,可以通过继承它来创建自己的数据集。DataLoader:用于批量加载数据,可以自动打乱数据、并行加载等,便于训练过程中的小批量数据处理。
-
torch.utils.tensorboard:提供与 TensorBoard 的集成,允许用户记录训练过程中的损失、准确率等指标,以便后续可视化分析。 -
torch.utils.checkpoint:用于内存优化,允许在训练过程中通过重计算来减少内存使用,特别是在处理大型模型时。 -
torch.utils.mobile:提供将 PyTorch 模型导出为适合移动设备的格式的工具。
这些工具使得用户在构建和训练模型时更加高效和灵活。
框架内的数据迭代器
def load_array(data_arrays,batch_size,is_train=True):
dataset=data.TensorDataset(*data_arrays)
return data.DataLoader(dataset,batch_size,shuffle=is_train)
batch_size=10
data_iter=load_array((f,l),batch_size)
我们先看下面的参数,(f,l)是将训练特征和标签组成了一个元组作为参数,然后创建一个TensorDataset类,名字叫做dataset。
*是什么?
* 是 Python 中的解包操作符,允许你将一个可迭代对象(如列表或元组)中的元素拆分出来,传递给函数或用于其他操作。
也就是说,这里也可以是[f,l](列表)。
这类似于 C 语言中的指针访问,但实际上是 Python 中的一种语法特性,用于处理可迭代对象。通过解包,可以简化函数调用,使其更灵活和易于阅读。
也就是说,TensorDataset类的构造函数有f和l两个参数。TensorDataset将把它们配对,使得每个样本的特征和对应的标签可以一起返回。
最终调用了数据集加载器DataLoader(),dataset作为参数,shuffle根据is_train决定是否在每个epoch之前随机打乱数据。返回的是一个可迭代对象。
可迭代对象和迭代器是不一样的!
next(iter(data_iter))
iter()返回了迭代器,这样就可以用next()访问每一下批数据了!
模型的简化
我们使用torch.nn。nn是neural networks的缩写,是专门为构建和训练神经网络提供的模块,它包含了常见的神经网络层、损失函数、激活函数等组件。
from torch import nn
net=nn.Sequential(nn.Linear(2,1))
Sequential是一个类,将多个神经网络层组合在一起,按照顺序依次执行。后面还可以放激活函数和不同的层,之间用逗号分隔,用数组的下标可访问里面的元素(net[n])。
Linear是一个全连接层,进行线性变换,也就是计算
。
传入了2和1,就是特征X和标签y的形状。
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
容易理解,这是初始化w和b,0.01是标准差。
为什么用“data”?
tensor.data主要用于直接访问张量的存储数据,而不记录计算图中的操作。这种直接操作不会影响梯度的计算,也就是说不会记录在自动微分的计算图中。
但是GPT说,pytorch建议直接操作张量(去掉data),结合前面所学,可以使用with torch.no_grad():,这样效果上是一样的!
loss = nn.MSELoss()
#这是损失函数
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
#看名字猜得出来是用于小批量梯度下降的函数
MSELoss是Mean Squared Error Loss(均方误差损失)的缩写,它是框架内的损失函数之一。loss对象并不直接和前面的net对象相关联,所以之后要自己把输出和标签传入计算。
net.parameters()直接传入了net的参数,再传个学习率就行了。
训练
复习一下流程:
- 正向传播(计算模型)
- 计算损失函数
- 反向传播(求梯度)(别忘记要先清零上次的梯度!)
- 更新参数(梯度下降法)
num_epochs=10
for i in range(num_epochs):
for X,y in data_iter:
lo=loss(net(X),y)
trainer.zero_grad()
lo.backward()
trainer.step()
lo=loss(net(f),l)#算完一次数据集看看效果
print(f'epoch:{i+1},loss:{lo:f}')
net[0].weight,net[0].bias
'''
epoch:1,loss:0.000283
epoch:2,loss:0.000101
epoch:3,loss:0.000102
epoch:4,loss:0.000101
epoch:5,loss:0.000101
epoch:6,loss:0.000102
epoch:7,loss:0.000102
epoch:8,loss:0.000101
epoch:9,loss:0.000102
epoch:10,loss:0.000102
(Parameter containing:
tensor([[ 1.9995, -3.3996]], requires_grad=True),
Parameter containing:
tensor([4.1997], requires_grad=True))
'''
损失成功下降了,w和b也非常接近true_w和true_b!
最后有一些问题!
-
Q:前面区分过了,data_iter是可迭代对象对吧,为什么可以和迭代器一样呢,不用iter()吗?
A:GPT:在for循环中,Python 自动调用了iter() 函数来获取迭代器。
-
Q:这里反向累积的过程,为什么没有求和呢?
A:loss = nn.MSELoss()会自动求平均,这样就不需要再求和再除以batch_size这样的过程了!
-
Q:batch_size大还是小好?
A:李沐:小点更好,batch_size越小,对噪音更敏感,其实是好事情
总结
其实人工智能从头开始学习会比调用API更有挑战性,但如果是想要深入理解学习,硬看数学公式和学习各种原理都是必不可少的过程!