数字分类器
指标
像素相似度
:可以使用平均像素值。训练时将所有数字7的图片堆叠在一起,形成(dim,height,weight)
。
平均绝对差/L1范数
:先对差取绝对值,再求绝对值的平均值。
均方根误差(RMSE)/L2范数
:取差平方的均值,然后取其平方根。
F.l1_loss(a_3.float(),mean7), F.mse_loss(a_3,mean7).sqrt()
#l1范数(平均绝对值) mse均方差
广播机制计算指标
不是遍历数据集中的每一张图片,而是传递一个张量(tensor),广播机制不同张量可以匹配(相同阶数)。
def mnist_distance(a,b): return (a-b).abs().mean((-1,-2)) # dim*h*w,这里求的后两位
mnist_distance(a_3, mean3)
# tensor(0.1114)
valid_3_dist = mnist_distance(valid_3_tens, mean3)
valid_3_dist, valid_3_dist.shape
# (tensor([0.1115, 0.1365, 0.1111, ..., 0.1170, 0.1276, 0.1176]), torch.Size([1010]))
随机梯度下降法
梯度下降
初始化权重
对任意一张图像,使用这些权重进行预测,预测其类别是3还是7
基于上述预测结果,通过计算损失来表示模型的准确率
计算梯度,梯度表明了每个权重对于整个损失变化的影响程度
根据计算得到的梯度信息对权重进行迭代
回到步骤2,并重复整个操作
迭代完成,停止训练过程(模型达到足够的准确率或训练时间达到上限)
计算梯度
x = tensor(3.).requires_grad_() # 想得到x**2函数在3.处的梯度,标记变量
# 定义一个函数
y = x * x
# 计算反向传播
y.backward()
# 打印x的梯度
print(x.grad) # 输出:tensor(6.)
xt = tensor([3.,4.,10.]).requires_grad_() # 以一个向量作为参数
def f(x): return (x**2).sum() # 函数中求和,以便接收一个矢量(即一阶张量)并且能够返回一个标量(即一个0阶张量)
yt = f(xt)
yt.backward()
xt.grad
# tensor([ 6., 8., 20.])
梯度告诉了函数的斜率但是没有告诉确切的参数调整范围。如果斜率很大,则表明需要做更大的调整,而如果斜率很小,则表明已接近最佳值。
通过学习率迭代
基于梯度进行模型参数的修改,从将梯度乘以一些很小的值开始的,而这些很小的值被称为学习率(LR)
。学习率通常是一个取值范围在0.001到0.1之间的整数,也可以是其他形式。
w -= gradient(w) * lr
直观的随机梯度下降案例
模拟圆筒滚过土坡顶峰的速度
数据
time = torch.arange(0,20).float()
speed = speed = torch.randn(20)*3 + 0.75*(time-9.5)**2 + 1 # 二次函数上添加噪声
plt.scatter(time,speed)
def f(t, params): # 猜测为二次函数
a,b,c = params
return a*(t**2) + (b*t) + c
# 参数 a,b,c,
def mse(preds, targets): return ((preds-targets)**2).mean() # 损失函数 均方差
step 1 初始化参数
将参数初始化为随机值,并通过requires_grad_函数跟踪相关参数的梯度信息
params = torch.randn(3).requires_grad_()
step 2 计算预测值
计算出函数的结果
preds = f(time, params)
构建一个距离函数
def show_preds(preds, ax=None): if ax is None: ax=plt.subplots()[1] ax.scatter(time, speed) ax.scatter(time, to_np(preds), color='red') ax.set_ylim(-300,100) show_preds(preds)
这个函数的功能是将预测的速度和实际的速度在同一张图上绘制出来,预测的速度用红色表示,实际的速度用默认的颜色表示。这样,你就可以直观地看到预测的准确性了。
计算预测值step 3 计算损失
计算损失
loss = mse(preds, speed) # tensor(25823.8086, grad_fn=<MeanBackward0>)
step 4 计算梯度
对修改参数的一个估计
loss.backward() params.grad params.grad * 1e-5 # 用梯度优化参数
step 5 迭代权重
根据得到的梯度对参数更新
lr = 1e-5 params.data -= lr * params.grad.data params.grad = None #链式法则计算梯度
观察优化结果
preds = f(time,params) mse(preds, speed) show_preds(preds)
计算预测值step 6 重复以上流程
定义一个一次迭代的步骤
def apply_step(params, prn=True): preds = f(time, params) loss = mse(preds, speed) loss.backward() params.data -= lr * params.grad.data params.grad = None if prn: print(loss.item()) return preds for i in range(10): apply_step(params) _,axs = plt.subplots(1,4,figsize=(12,3)) for ax in axs: show_preds(apply_step(params, False), ax) plt.tight_layout()
损失的确下降了
损失下降step 7 停止
迭代了10个epoch停止
MNIST损失函数
batch @ weights + bias
def init_params(size, std=1.0):
return (torch.randn(size)*std).requires_grad_()
weights = init_params((28*28,1))
bias = init_params(1)
def mnist_loss(predictions, targets):
return torch.where(targets==1, 1-predictions, predictions).mean()
torch.where(condition, x, y)
是PyTorch中的一个函数,它的功能是根据一个条件来从两个张量中选择元素。具体来说,condition
是一个布尔类型的张量,x
和y
是两个形状相同的张量。torch.where(condition, x, y)
会返回一个新的张量,这个张量的元素来自x
和y
。如果condition
在某个位置的元素为True
,那么结果张量在这个位置的元素就是x
在这个位置的元素;否则,结果张量在这个位置的元素就是y
在这个位置的元素。
trgts = tensor([1,0,1])
prds = tensor([0.9, 0.4, 0.2])
# 假设三张图像,分别是3,7,3 对第一张图像为3具有较高的置信度0.9,第二张图像为7具有较低的置信度0.4,认为是3,第三张图像为3的置信度为0.2,认为是7
torch.where(trgts==1, 1-prds, prds)
# tensor([0.1000, 0.4000, 0.8000])
可以看到,当预测更准确,或者说准确的预测更可靠(绝对值更高),及不准确的预测更不可靠时,此函数返回的数值更小。总是假设损失函数的值越小越好。
因为需要一个标量作为最终损失,所以mnist_loss会对上一个张量取平均值。
mnist_loss(prds,trgts)
# tensor(0.4333)
如果将一个假目标的预测值从0.2改为0.8,损失会下降,表明这是一个更好的预测
mnist_loss(tensor([0.9, 0.4, 0.8]),trgts)
# tensor(0.2333)
预测的输出设置在0,1之间
Sigmoid
$\mathrm{S}(\mathrm{x})=\frac{1}{1+\mathrm{e}^{-\mathrm{x}}}$
总是输出一个介于0和1之间的数值。
def sigmoid(x): return 1/(1+torch.exp(-x))
plot_function(torch.sigmoid, title='Sigmoid', min=-4, max=4)
def mnist_loss(predictions, targets):
predictions = predictions.sigmoid()
return torch.where(targets==1, 1-predictions, predictions).mean()
损失必须是一个有意义的可导函数,不能有大的平坦部分和大的跳跃,而且必须相当平滑。会对置信水平的微小变化做出反应。
SGD和Mini-Batches
小批次
:一次计算几个数据项的平均损失。小批次中数据项的大小称为批次大小。
为获得更好的通用性,在创建小批次之前,在每个周期中随机地对数据集进行洗牌,而不是按顺序列举。
Pytorch和fastai提供了类DataLoader
,执行洗牌和小批次排序。
DataLoader可以获得任何的Python集合,并将其转换为多个批次的迭代器。
coll = range(15)
dl = DataLoader(coll, batch_size=5, shuffle=True)
list(dl)
# [tensor([ 3, 12, 8, 10, 2]),
# tensor([ 9, 4, 7, 14, 5]),
# tensor([ 1, 13, 0, 6, 11])]
还需要包含自变量和因变量(模型的输入和输出)的集合,称为Dataset
。
ds = L(enumerate(string.ascii_lowercase))
print(ds)
# (#26) [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd'),(4, 'e'),(5, 'f'),(6, 'g'),(7, 'h'),(8, 'i'),(9, 'j')...]
当将Dataset传递给DataLoader时,将得到许多批数据,这些数据本身就可以表示为自变量和因变量的张量元组。
dl = DataLoader(ds, batch_size=6, shuffle=True)
list(dl)
# 输出如下
[(tensor([17, 18, 10, 22, 8, 14]), ('r', 's', 'k', 'w', 'i', 'o')),
(tensor([20, 15, 9, 13, 21, 12]), ('u', 'p', 'j', 'n', 'v', 'm')),
(tensor([ 7, 25, 6, 5, 11, 23]), ('h', 'z', 'g', 'f', 'l', 'x')),
(tensor([ 1, 3, 0, 24, 19, 16]), ('b', 'd', 'a', 'y', 't', 'q')),
(tensor([2, 4]), ('c', 'e'))]
将它们集成在一起
# 初始化参数
weights = init_params((28*28,1))
bias = init_params(1)
# 从Dataset中创建DataLoader
dl = DataLoader(dset, batch_size=256)
xb,yb = first(dl)
xb.shape,yb.shape
# (torch.Size([256, 784]), torch.Size([256, 1]))
valid_dl = DataLoader(valid_dset, batch_size=256)
# 构建大小为4的批数据作为测试
batch = train_x[:4]
batch.shape
# torch.Size([4, 784])
preds = linear1(batch)
loss = mnist_loss(preds, train_y[:4])
loss.backward()
weights.grad.shape,weights.grad.mean(),bias.grad
放在一个函数里
def calc_grad(xb, yb, model):
preds = model(xb)
loss = mnist_loss(preds, yb)
loss.backward()
并对其测试:
calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad
# (tensor(-0.0098), tensor([-0.0653]))
# 再运行一次
# (tensor(-0.0148), tensor([-0.0979]))
loss.backward
函数会对现有损失计算得到的梯度与之前已经保存过的梯度进行相加。因此要重置现有梯度为0:
weights.grad.zero_()
bias.grad.zero_()
In-Place操作:Pytorch中下划线结尾的方法会直接对对象进行修改。
# 一个周期的基本训练函数
def train_epoch(model, lr, params):
for xb,yb in dl:
calc_grad(xb, yb, model)
for p in params:
p.data -= p.grad*lr
p.grad.zero_()
(preds>0.0).float() == train_y[:4]
# 计算验证集的识别准确率
def batch_accuracy(xb, yb):
preds = xb.sigmoid()
correct = (preds>0.5) == yb
return correct.float().mean()
batch_accuracy(linear1(batch), train_y[:4])
def validate_epoch(model):
accs = [batch_accuracy(model(xb), yb) for xb,yb in valid_dl]
return round(torch.stack(accs).mean().item(), 4)
validate_epoch(linear1)
# 训练一周期
lr = 1.
params = weights,bias
train_epoch(linear1, lr, params)
validate_epoch(linear1)
# 多训练几个周期
for i in range(20):
train_epoch(linear1, lr, params)
print(validate_epoch(linear1), end=' ')
# 0.9477 0.957 0.9599 0.9633 0.9672 0.9682 0.9682 0.9702 0.9706 0.9716 0.9721 0.9721 0.9726 0.9731 0.9726 0.9736 0.9736 0.974 0.9745 0.9745
创建一个优化器
优化器
:在深度学习中,优化器是用来更新和计算模型内部参数以减少模型误差的工具。优化算法决定了如何改变模型的权重以改善其在训练数据上的表现。优化器的目标是找到模型误差函数的最小值。
PyTorch提供了许多优化器,包括SGD(随机梯度下降)、Adam、RMSProp等。每种优化器都有其特点,适用于不同的情况和需求。
Pytorch中的nn.Linear
模块代替Linear
函数。与init_params
和linear
两个函数做的事情是相同的。它在单独的一个类中同时包含了权重和偏差。
linear_model = nn.Linear(28*28,1)
w,b = linear_model.parameters()
# 优化器
class BasicOptim:
def __init__(self,params,lr): self.params,self.lr = list(params),lr
def step(self, *args, **kwargs):
for p in self.params: p.data -= p.grad.data * self.lr
def zero_grad(self, *args, **kwargs):
for p in self.params: p.grad = None
opt = BasicOptim(linear_model.parameters(), lr)
# 训练循环
def train_epoch(model):
for xb,yb in dl:
calc_grad(xb, yb, model)
opt.step()
opt.zero_grad()
validate_epoch(linear_model) # 验证函数
def train_model(model, epochs):
for i in range(epochs):
train_epoch(model)
print(validate_epoch(model), end=' ')
train_model(linear_model, 20)
# 0.4932 0.5356 0.665 0.874 0.9189 0.937 0.9512 0.9575 0.9638 0.9663 0.9673 0.9702 0.9717 0.9736 0.9751 0.9761 0.9765 0.9775 0.978 0.978
fastai
提供的默认的SGD优化器类可以做到和我们的BasicOptim相同的事情。
linear_model = nn.Linear(28*28,1)
opt = SGD(linear_model.parameters(), lr)
train_model(linear_model, 20)
可以用fastai提供的Learner.fit
来代替train_model
。
dls = DataLoaders(dl, valid_dl)
learn = Learner(dls, nn.Linear(28*28,1), opt_func=SGD,
loss_func=mnist_loss, metrics=batch_accuracy)
learn.fit(10, lr=lr)
增加一个非线性特征
在线性分类器上进行了函数参数的优化,下面在两个线性分类器之间添加一些非线性的东西,成为神经网络。
def simple_net(xb):
res = xb@w1 + b1 # @操作符,矩阵乘
res = res.max(tensor(0.0))
res = res@w2 + b2
return res
# 初始化参数
w1 = init_params((28*28,30))#w1的激活值输出30,w2的激活值输入30,要匹配
b1 = init_params(30)
w2 = init_params((30,1))
b2 = init_params(1)
res.max(tensor(0.0))
被称为线性修正单元,也成ReLU
,把所有的负数替换成0。
plot_function(F.relu) #PyTorch中这样用
线性层堆叠相当于一个线性层,在其中加上非线性层才有意义。
数学上,两个线性函数的组合是另一个线性函数
如果能找到w1和w2的正确参数并且把这些矩阵做的足够大,那么这个小函数就可以在数学上证明它可以以任意高的准确率解决任何可计算的问题。对于任意的wiggly(波动)函数,可以把它近似为一组连接在一起的直线;为了使它更接近wiggly函数,只需要使用更短的线就好了,这就是近似逼近定理。
simple_net = nn.Sequential(
nn.Linear(28*28,30),
nn.ReLU(),
nn.Linear(30,1)
)
learn = Learner(dls, simple_net, opt_func=SGD,
loss_func=mnist_loss, metrics=batch_accuracy)
learn.fit(40, 0.1)
训练过程记录在learn.recorder
,输出的表格存储在values
属性中。
绘制训练的准确率:
learn.recorder
不是存储在本地文件系统中的一个文件,而是FastAILearner
对象的一个属性。它在内存中保存了训练过程中的一些信息,如每个epoch的损失和度量。
learn = cnn_learner(dls, resnet34, metrics=accuracy)
learn.fit(10, lr=0.01)
# 打印每个epoch的损失和度量
for epoch, values in enumerate(learn.recorder.values):
print(f"Epoch {epoch}: {values}")
# 把learn.recorder的内容保存到本地,可以使用Python的标准库pickle
import pickle
with open('recorder.pkl', 'wb') as f:
pickle.dump(learn.recorder, f)
Going Deeper
事实证明,使用更小的矩阵和更多的层,得到的结果比使用更大的矩阵和少量神经层的结果要好,所以要使用更深层的模型。
术语回顾
https://nbviewer.org/github/fastai/fastbook/blob/master/04_mnist_basics.ipynb