其他分享
首页 > 其他分享> > fastai 2019 lesson8 notes 笔记

fastai 2019 lesson8 notes 笔记

作者:互联网

lesson8

前言

这次的第 2 部分与 2018 年的版本有很大不同。课程名称是“从基础开始的深度学习”。我们将学习实现 Fastai 和 PyTorch 中的很多东西。基本上,我们将学习可以用来构建我们自己的深度学习库的东西。在此过程中,我们将学习如何实现论文,这是制作最先进模型时需要掌握的一项重要技能。

基础,但它基本上意味着从头开始,所以我们将研究基本的矩阵演算,从头开始创建一个训练循环,从头开始创建一个优化器以及许多不同的层和架构等等orth,而不仅仅是创建某种对任何事情都没有用的愚蠢的库,而是从头开始实际构建一些可以训练尖端世界级模型的东西,所以这是我们在我之前从未做过的目标,我们认为以前没有人做过这件事,所以我不确切知道我们会走多远,但这是你知道的,这是我们正在进行的旅程,我们将看到我们将如何前进。

因此在此过程中我们将不得不阅读和正确实现论文,因为 Fastai library充满了实施的论文,所以如果你不阅读和实现论文,你将无法做到这一点,我们也将实现大部分 pytorch。正如您将看到的,我们还将更深入地解决一些尚未完全融入 fastai库的应用程序,因此将需要大量自定义工作,因此诸如对象检测、序列seq2seq之类的事情、要使用注意力转换器进行排序、transformer、 excel 循环增益音频之类的东西我会所以要更深入地研究一些性能方面的考虑,比如使用我们编写的新的即时编译器进行分布式多 GPU 训练,从现在开始称为 JIT 和 C++ 之类的东西,所以这是前五节课。

最后两节课是用Swift实现其中的一些应用,是Impractical的深度学习。这部分内容与part1相反。

我们今年改变了,有几个原因:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-81HKy12m-1632878474338)(Snipaste_2021-09-27_17-34-01.png)]
请添加图片描述

我们要做什么?

why swift?

Swift和Pytorch的优缺点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hDUs3nxO-1632878474340)(Snipaste_2021-09-28_10-45-08.png)]
请添加图片描述

what do we mean by from the foundations

重写fastai和很多pytorch的功能:矩阵乘法、torch.nn, torch.optim,还有顶级的数据集加载data loader

我们可以使用python和一些python标准库

规则

why?

请添加图片描述

本门课程有很多机会

在这里插入图片描述

part1 回顾

所以我假设你记得第一部分的内容,这里是第一部分的内容 在实践中 你不太可能记住所有这些东西 因为没有人是完美的 所以我实际上希望你做的是我正在做的事情。你在想我不知道他在说什么 你会回去观看关于那件事的视频 不要只是继续向前推进 因为我假设你已经知道第一部分的内容。特别是如果你对第一部分的后半部分不太自信,在那里我们更深入地了解什么是真正的激活,什么是真正的参数,它与 SGD 的工作完全一样,特别是在今天的课程中,我假设你真的懂得那些东西,所以如果你没有懂,回去重新看那些视频,从头开始回到像SGD那样的时间,并花点时间。

我设计这门课程是为了让大部分人忙起来,直接忙到下一门课程,所以请随意花时间深入挖掘。

① 过拟合》② 减少过拟合》没有第三步

  1. 首先,我们尝试创造比我们需要的容量更大的东西(尝试创建一个复杂模型)

    • 无正则化
    • 过拟合
    • 过拟合意味着你的训练损失低于验证损失 ✘
  2. 过拟合并不意味着训练损失低于验证损失

    • 拟合良好的模型,train loss总是低于valid loss
    • 过拟合的标志是当您实际看到验证损失valid loss变得更糟时
  3. 可视化输入和输出:

    • 看看到底发生了什么

避免过拟合的五个步骤

五件事情可以避免过拟合!

  1. 更多数据
  2. 数据增强
  3. 通用架构
  4. 正则化
  5. 降低架构复杂性

在这里插入图片描述

是时候开始阅读论文了

所以我们将阅读part1中我们没有阅读的论文。阅读论文可能会非常令人生畏。excel 上最简单的计算可能是论文中的一大堆符号。

课程大纲

基本现代 basic modern CNN model模型的步骤

对于接下来的几节课。我们将制作一个合格的 CNN 模型。

今天上课的目标

因为我们做到了在上一门课程中,我们已经有了用于创建 ResNet 的层 我们实际上得到了很好的结果 所以我们只需要做好所有这些事情就可以让我们从这里到这里 这只是接下来的几节课我们将走得更远,

我在这里向您展示的是我将如何在 Jupiter 笔记本中构建我们的库,很多非常聪明的人向我保证,不可能在 Jupiter notebooks 中进行有效的library开发,这是一种耻辱,因为我已经建立了一个library。但是我们的 notebooks 所以无论如何人们会经常告诉你事情是不可能的,但是t我会告诉你我的观点,即我已经编程了 30 多年,在我一直使用的时间里,但是我的开发是一个愚蠢的笔记本,**我猜我的生产力大约提高了两到三倍。**是的,在过去的两三年里,我构建了比之前做的更多有用的东西,所以我会告诉你我们需要如何做一些事情。

我们不能仅仅用我们的整个library创建一个巨大的笔记本 我们必须能够以某种方式提取那些小宝石 我们认为哦这是的那些代码好的,让我们保留它,我们必须将它提取到一个我们可以重用的包中,以便告诉我们的系统这是一个我希望您保留和重用的单元格。

我在单元格的顶部使用这个特殊的评论缓存导出和然后我有一个名为 notebook2script 的程序,它遍历 notebook 并找到那些单元格和将它们放入 Python 模块中,把ipynb转换为py文件。

00_exports.ipynb 开始,

第00课.ipynb

如何从 jupyter 中提取一些代码到一个包中

Notebook 01 矩阵乘法(文件01_matmul.ipynb

  %load_ext autoreload
  %autoreload 2
  
  %matplotlib inline
  mpl.rcParams['image.cmap'] = 'gray'
  #export
  
  # standard libraries
  from pathlib import Path
  from IPython.core.debugger import set_trace
  import pickle, gzip, math, torch, matplotlib as mpl
  import matplotlib.pyplot as plt
  
  # datasets
  from fastai import datasets
  
  # basic pytorch
  from torch import tensor
  
  MNIST_URL='http://deeplearning.net/data/mnist/mnist.pkl'

下载 mnist 数据集并加载

path = datasets.download_data(MNIST_URL, ext='.gz'); path

# unzips the download
with gzip.open(path, 'rb') as f:
    ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding='latin-1')

numpy不允许使用,所以使用python的map,映射为tensor格式,pytroch的tensor是个不错的选择。

# maps the pytorch tensor function against each
# of the loaded arrays to make pytorch versions of them
x_train,y_train,x_valid,y_valid = map(tensor, (x_train,y_train,x_valid,y_valid))

# store the number of 
# n = rows
# c = columns
n,c = x_train.shape

# take a look at the values and the shapes
x_train, x_train.shape, y_train, y_train.shape, y_train.min(), y_train.max()
(tensor([[0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         ...,
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.]]),
 torch.Size([50000, 784]),
 tensor([5, 0, 4,  ..., 8, 4, 8]),
 torch.Size([50000]),
 tensor(0),
 tensor(9))

让我们测试我们的输入数据

  1. 行检查:检查行数x_train是否与形状相同,y_train并且该数字应为 50,000
  2. 列检查:检查列数是否为**28*28,**因为这是展开图像的总像素数
  3. 类检查:测试是否在y_train0 - 9 中找到了 10 个不同的类
assert n==y_train.shape[0]==50000
test_eq(c,28*28)
test_eq(y_train.min(),0)
test_eq(y_train.max(),9)

偷看其中一张图片

img = x_train[0]
img.view(28,28).type()
'torch.FloatTensor'
# note that there is a single vector that is reshaped into the square format
plt.imshow(img.view((28,28)));

初始模型

我们将首先尝试线性模型:

Y=W^T X+b 将是我们将尝试的第一个模型。我们将需要以下内容:

weights = torch.randn(784,10)
bias = torch.zeros(10)

矩阵乘法

我们将经常这样做,所以熟悉这一点很好。有一个很棒的网站matrixmultiplication.xyz,它会说明矩阵乘法的工作原理。

矩阵乘法函数: 以下函数将两个数组逐个相乘

def matmul(a,b):
    
    # gets the shapes of the input arrays
    ar,ac = a.shape # n_rows * n_cols
    br,bc = b.shape
    
    # checks to make sure that the
    # inner dimensions are the same
    assert ac==br
    
    # initializes the new array
    c = torch.zeros(ar, bc)
    
    # loops by row in A
    for i in range(ar):
        
        # loops by col in B
        for j in range(bc):
            
            # for each value
            for k in range(ac): # or br
                c[i,j] += a[i,k] * b[k,j]
    return c

让我们做一个快速示例

将使用验证数据中的前 5 张图像并将它们乘以矩阵的权重

m1 = x_valid[:5]
m2 = weights
m1.shape, m2.shape
(torch.Size([5, 784]), torch.Size([784, 10]))

将计时操作

%time t1=matmul(m1, m2)
CPU times: user 605 ms, sys: 2.21 ms, total: 607 ms 
Wall time: 606 ms
t1.shape
torch.Size([5, 10])

我们怎样才能更快地做到这一点?

我们可以通过逐元素操作来做到这一点。我们将使用 pytorch 的张量来说明这一点。使用 pytorch 对象时,操作符 (+,-,*,/,>,<,==) 通常是逐元素的。按元素操作的示例:

a = tensor([10., 6, -4])
b = tensor([2., 8, 7])
m = tensor([[1., 2, 3], [4,5,6], [7,8,9]]); 
a, b, m
(tensor([10.,  6., -4.]), tensor([2., 8., 7.]), tensor([[1., 2., 3.],
         [4., 5., 6.],
         [7., 8., 9.]]))
# Addition
print(a + b)
# comparisons
print(a < b)
# can summarize
print((a < b).float().mean())
# frobenius norm 范数计算
print((m*m).sum().sqrt())
tensor([12., 14.,  3.])
tensor([0, 1, 1], dtype=torch.uint8)
tensor(0.6667)
tensor(16.8819)

如果我们调整 matmul

for k in range(ac): # or br
    c[i,j] += a[i,k] * b[k,j]

将被替换

c[i,j] = (a[i,:] * b[:,j]).sum()
def matmul(a,b):   
    # gets the shapes of the input arrays
    ar,ac = a.shape # n_rows * n_cols
    br,bc = b.shape 
    # checks to make sure that the
    # inner dimensions are the same
    assert ac==br
    # initializes the new array
    c = torch.zeros(ar, bc) 
    # loops by row in A
    for i in range(ar):  
        # loops by col in B
        for j in range(bc): 
            c[i,j] = (a[i,:] * b[:,j]).sum()
    return c

性能改变后,乘法要快得多

%time t1=matmul(m1, m2)
CPU times: user 1.57 ms, sys: 864 µs, total: 2.44 ms
Wall time: 1.57 ms

为了测试它,我们将编写另一个函数来比较矩阵。原因是由于数学运算的舍入误差,矩阵可能不完全相同。因此,我们希望有一个功能,将“是等于B一定误差内的

#export
def near(a,b): 
    return torch.allclose(a, b, rtol=1e-3, atol=1e-5)

def test_near(a,b): 
    test(a,b,near)
test_near(t1, matmul(m1, m2))

广播

广播描述了在算术运算期间如何处理具有不同形状的数组。术语广播首先由 Numpy 使用。

在这里插入图片描述

我们如何才能做到 a > 0?0 正在广播以具有与 a 相同的维度。

例如,您可以使用广播通过从整个数据集(矩阵)中减去均值(一个标量)并除以标准差(另一个标量)来标准化我们的数据集。

示例:针对矩阵广播向量

您可以使用特殊值进行索引[None]或用于unsqueeze()将一维数组转换为二维数组(尽管其中一个维度的值为 1)。这在稍后在建模中使用矩阵乘法时很重要

我们并没有真正复制,看起来我们复制了,但实际上step=0。

回到我们的函数

让我们利用广播并减少matmul函数中的循环:

a[i,:] 查看 1 级张量

**.unsqueeze(-1)**使它成为 2d,这-1意味着最后一个维度

\* b 广播结束 b

.sum(dim=0) 沿第一个轴求和

def matmul(a,b):
    # gets the shapes of the input arrays
    ar,ac = a.shape # n_rows * n_cols
    br,bc = b.shape
    # checks to make sure that the
    # inner dimensions are the same
    assert ac==br  
    # initializes the new array
    c = torch.zeros(ar, bc) 
    # loops by row in A
    for i in range(ar):
        c[i] = (a[i].unsqueeze(-1) * b).sum(dim=0)
    return c
%time t1=matmul(m1, m2)
CPU times: user 440 µs, sys: 283 µs, total: 723 µs
Wall time: 421 µs
test_near(t1, matmul(m1, m2))

广播规则

由于多维广播可能很复杂,因此请务必遵循一些规则

在对两个数组/张量进行操作时,Numpy/PyTorch 会按元素比较它们的形状。它从尾随的维度开始,然后向前推进。当两个维度兼容

数组不需要具有相同的维数。例如,如果您有一个 256 256 3 个 RGB 值数组,并且您想将图像中的每种颜色缩放不同的值,则可以将图像乘以具有 3 个值的一维数组。根据广播规则排列这些数组的尾随轴的大小,表明它们是兼容的:

爱因斯坦求和

爱因斯坦求和 ( einsum) 是以一般方式组合乘积和总和的紧凑表示。从 numpy 文档:

下标字符串是一个逗号分隔的下标标签列表,其中每个标签指的是对应操作数的一个维度。每当一个标签被重复时,它就会被求和,所以np.einsum('i,i', a, b)等价于np.inner(a,b)。如果一个标签只出现一次,它不会被求和,所以np.einsum('i', a)会产生一个没有变化的视图。”

c[i,j] += a[i,k] * b[k,j]
c[i,j] = (a[i,:] * b[:,j]).sum()

考虑一些重新排列,将目标向右移动,并去掉名称

a[i,k] * b[k,j] -> c[i,j]
[i,k] * [k,j] -> [i,j]
ik,kj -> [ij]
# c[i,j] += a[i,k] * b[k,j]
# c[i,j] = (a[i,:] * b[:,j]).sum()
def matmul(a,b): return torch.einsum('ik,kj->ij', a, b)
%timeit -n 10 _=matmul(m1, m2)
47.7 µs ± 4.04 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

性能注意事项

不幸的是,在 einsum 中隐藏了另一种非常高性能的语言。目前,人们对高性能语言有很多兴趣和发展。这是为称为“halide”的语言所做的一些工作的链接

http://halide-lang.org/ 9

pytorch opUnfo

我们加快了速度,但而且 pytorch 操作得到了更多优化。即使使用矢量化操作,也有处理内存的缓慢和快速方法。不幸的是,大多数程序员无法访问这个,缺少使用BLAS库中提供的函数(基本线性代数子程序)

要查找的主题:张量理解

%timeit -n 10 t2 = m1.matmul(m2)
14 µs ± 4.44 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
t2 = m1@m2
!python notebook2script.py lesson82.ipynb
Converted lesson82.ipynb to nb_lesson82.py
CPU矩阵乘法时间消耗
三个循环330ms
pytorch矩阵点乘709us
pytorch 广播乘法289us
爱因斯坦求和16.6us

第8课制作Relu/初始化

%load_ext autoreload
%autoreload 2

%matplotlib inline
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
  %reload_ext autoreload
#export
from exp.nb_lesson81 import *
from exp.nb_lesson82 import *

def test(a,b,cmp,cname=None):
    if cname is None: cname=cmp.__name__
    assert cmp(a,b),f"{cname}:\n{a}\n{b}"

def near(a,b): 
    return torch.allclose(a, b, rtol=1e-3, atol=1e-5)

def test_near(a,b): 
    test(a,b,near)
    

def get_data():
    """
    Loads the MNIST data from before
    """
    path = datasets.download_data(MNIST_URL, ext='.gz')
    with gzip.open(path, 'rb') as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding='latin-1')
    return map(tensor, (x_train,y_train,x_valid,y_valid))

def normalize(x, m, s): 
    """
    Normalizes an input array
    Subtract the mean and divide by standard dev
    result should be mean 0, std 1
    """
    return (x-m)/s

def test_near_zero(a,tol=1e-3): 
    assert a.abs()<tol, f"Near zero: {a}"

加载 MNIST 数据并标准化

向前和向后传球

# load the data
x_train, y_train, x_valid, y_valid = get_data()

# calculate the mean and standard deviation
train_mean,train_std = x_train.mean(),x_train.std()
print("original mean and std:", train_mean,train_std)

# normalize the values
x_train = normalize(x_train, train_mean, train_std)
x_valid = normalize(x_valid, train_mean, train_std)

# check the updated values
train_mean,train_std = x_train.mean(),x_train.std()
print("normalized mean and std:", train_mean, train_std)
original mean and std: tensor(0.1304) tensor(0.3073)
normalized mean and std: tensor(0.0001) tensor(1.)
# check to ensure that mean is near zero
test_near_zero(x_train.mean())

# check to ensure that std is near zero
test_near_zero(1-x_train.std())

看一下训练数据

注意训练集的大小

n,m = x_train.shape
c = y_train.max()+1
n,m,c
(50000, 784, tensor(10))

我们的第一个模型

我们的第一个模型将有 50 个隐藏单元。它还将有两个隐藏层:

  1. 第一层 ( w1): 将是input_shapex 的大小hidden units
  2. 第二层 ( w2): 将是大小hidden units
# our linear layer definition

def lin(x, w, b):
    return x@w + b

# number of hidden units
nh = 50

# initialize our weights and bias
# simplified kaiming init / he init
w1 = torch.randn(m,nh)/math.sqrt(m)
b1 = torch.zeros(nh)

w2 = torch.randn(nh,1)/math.sqrt(nh)
b2 = torch.zeros(1)

定义模型

获得标准化权重

如果我们希望我们的权重同样在 0 和 1 之间。我们将除以这些不同的因素,以便输出也应该有一个均值 0 和标准差 1。这通常用 kaiming normal 来完成,但我们通过除法来近似它由 sqrt

t = lin(x_valid, w1, b1)
print(t.mean(), t.std())
tensor(-0.0155) tensor(1.0006)

初始化权重很重要。示例:使用非常具体的权重初始化训练大型网络https://arxiv.org/abs/1901.09321 5. 事实证明,即使在单周期训练中,那些第一次迭代也非常重要。我们会回到这个

**这可能看起来是一个非常小的问题,但是作为我们将在接下来的几节课中看到它就像训练神经网络时重要的事情,**实际上在过去的几个月里人们真的注意到这有多么重要,有修复初始化之类的事情。这些人实际上训练了一个没有归一化层的 10,000 层深度神经网络,基本上只是通过仔细的初始化,所以现在人们真的花了很多时间思考,我们如何初始化真的很重要,你知道我们已经有了很多诸如单循环训练和超级收敛之类的成功,这与前几次迭代中发生的事情有关,事实证明这完全与初始化有关,因此我们将花费大量时间深入研究这一点,

我们的 ReLu(整流线性单元)

def relu(x):
    """
    Will return itself, unless its below 0
    then will return 0
    """
    return x.clamp_min(0.)

检查均值 0 std 1

这不会是真的,因为所有负值都将更改为 0,因此均值不会为零,并且 std 也会发生变化

ReLU改变了隐藏层激活元的均值和方差,因为非线性截断了。

我可以通过多种方式编写此代码,但是如果您可以使用类似于 pytorch 中的单个函数的东西来实现它,它几乎总是更快,因为通常用 C 编写的东西

t = relu(lin(x_valid, w1, b1))
print(t.mean(), t.std())
tensor(0.3896) tensor(0.5860)

如何处理Relu --> (0,1)

在这里插入图片描述

您可能会看到Glorot初始化 (2010)。论文很好,影响很大。在接下来的几节课中,我们实际上将重新实现本文中的大部分内容,他们描述了一个关于如何初始化神经网络的建议

在这里插入图片描述

但是网络变深了以后,梯度会消失的,分母的值太大了。所以ImageNet的人做了一些改变,将6改为了2

# kaiming init / he init for relu
w1 = torch.randn(m,nh)*math.sqrt(2/m)

s

w1.mean(),w1.std()
(tensor(0.0003), tensor(0.0506))
t = relu(lin(x_valid, w1, b1))
t.mean(),t.std()

现在结果更接近于均值 0,标准 1

(tensor(0.5896), tensor(0.8658))

这篇论文值得深入研究。他们解决的另一个有趣的话题是 conv 层非常类似于矩阵乘法

在这里插入图片描述

b可能不怎么重要,

然后他们会带您逐步了解整个网络中的方差如何变化。

fan_in 在前向传播中保留权重方差的幅值

fan_out在反向传播中保留幅值

#export
from torch.nn import init

w1 = torch.zeros(m,nh)
init.kaiming_normal_(w1, mode='fan_out')
t = relu(lin(x_valid, w1, b1))
import torch.nn
torch.nn.Linear(m,nh).weight.shape
-----
  torch.Size([50, 784])

doc(torch.nn.Linear(.forward) ,在pytorch中**F总是指torch.nn.functional**,

...
# Source:   
    @weak_script_method
    def forward(self, input):
        return F.linear(input, self.weight, self.bias)
...

torch.nn.functional.linear??,我们在文档字符串中看到我们使用以下短语进行转置,**weight.t()**这就是维度翻转的原因

@torch._jit_internal.weak_script
def linear(input, weight, bias=None):
    # type: (Tensor, Tensor, Optional[Tensor]) -> Tensor
    r"""
    Applies a linear transformation to the incoming data: :math:`y = xA^T + b`.

    Shape:

        - Input: :math:`(N, *, in\_features)` where `*` means any number of
          additional dimensions
        - Weight: :math:`(out\_features, in\_features)`
        - Bias: :math:`(out\_features)`
        - Output: :math:`(N, *, out\_features)`
    """
    if input.dim() == 2 and bias is not None:
        # fused op is marginally faster
        ret = torch.addmm(torch.jit._unwrap_optional(bias), input, weight.t())
    else:
        output = input.matmul(weight.t())	# 所以是转置了的,pytorch中的权重矩阵是反的 
        if bias is not None:
            output += torch.jit._unwrap_optional(bias)
        ret = output
    return ret

pytorch如何初始化线性层和卷积层的呢?

torch.nn.Conv2d??
torch.nn.modules.conv._ConvNd.reset_parameters??

所以卷积层的初始化操作:请注意,它除以math.sqrt(5),结果不是很好。

用的是kaiming_uniform这是普通的kaiming_norm基本相同,但是 5 \sqrt{5} 5 ​,这个 5 \sqrt{5} 5 ​貌似没有文献记录的有,这个 5 \sqrt{5} 5 ​ seems to work pretty badly,所以看源代码是非常有用的。

# Source:
    def reset_parameters(self):
        n = self.in_channels
        # 请注意,它除以math.sqrt(5),结果不是很好。
        init.kaiming_uniform_(self.weight, a=math.sqrt(5)) # 初始化用的是kaiming_uniform
        if self.bias is not None:
            fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
            bound = 1 / math.sqrt(fan_in)
            init.uniform_(self.bias, -bound, bound)

回到激活函数

现在我们看到均值为零,标准差接近 1

所以我们要尝试这个东西并从我们的 ReLu 中减去 0.5 所以这样很酷对我们已经设计了我们自己的新激活函数,是不是很糟糕我不知道,但就像这样当人们写论文时,你知道这是一种调整水平 tweak,总水平就像对一行代码的微小更改可能会很有趣,看看它有多大帮助。

def relu(x): 
    return x.clamp_min(0.) - 0.5 # 重新定义了我们新的激活函数


for i in range(10):
    # kaiming init / he init for relu
    w1 = torch.randn(m,nh)*math.sqrt(2./m )
    t1 = relu(lin(x_valid, w1, b1))
    print(t1.mean(), t1.std(), '| ')
tensor(0.0482) tensor(0.7982) | 
tensor(0.0316) tensor(0.8060) | 
tensor(0.1588) tensor(0.9367) | 
tensor(0.0863) tensor(0.8403) | 
tensor(-0.0310) tensor(0.7310) | 
tensor(0.0467) tensor(0.7965) | 
tensor(0.1252) tensor(0.8700) | 
tensor(-0.0610) tensor(0.7189) | 
tensor(0.0264) tensor(0.7755) | 
tensor(0.1081) tensor(0.8605) | 

我们的第一个模型

在Pytorch中,model也可以是一个function函数,

def relu(x): 
    return x.clamp_min(0.) - 0.5

def lin(x, w, b):
    return x@w + b

def model(xb):
    l1 = lin(xb, w1, b1)
    l2 = relu(l1)
    l3 = lin(l2, w2, b2)
    return l3

所以这是我们的模型,它只是一个执行一个线性层的函数,一个ReLU层,和一个linear层,让我们尝试运行它,好吧,在验证集上运行模型需要 8 毫秒,因此它足够快,可以训练

# timing it on the validation set
%timeit -n 10 _=model(x_valid)
-------
6.71 ms ± 456 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
assert model(x_valid).shape==torch.Size([x_valid.shape[0],1])

损失函数:MSE

一件事情是损失函数,正如我所说,我们现在将通过使用主平方误差来简化事情,尽管这显然是一个愚蠢的想法。我们的模型正在返回大小为 10,000 的东西,但是

我们需要**squeeze()去掉尾随(,1)**, 以便使用 mse。(当然,mse 不是多类分类的合适损失函数;我们很快就会使用更好的损失函数。为了简单起见,我们现在将使用 mse。)

model(x_valid).shape
----------
torch.Size([10000, 1])

这里偷懒了,对output进行了squeeze(),很多时候fastai论坛上同学们report code breaks,通常都是因为他们batch size =1,然后调用了squeeze(),就变成了标量,然后就崩溃了。所以在使用squeeze时最好指明维度。

#export
def mse(output, targ): 
    # we want to drop the last dimension
    return (output.squeeze(-1) - targ).pow(2).mean()
# converting to float (from tensors),把这些数值转为float
y_train, y_valid = y_train.float(), y_valid.float()

# make our predictions  计算预测
preds = model(x_train)		# 前向传播
print(preds.shape)			# 计算loss
# check our mse
print(mse(preds, y_train))
-----------
torch.Size([50000, 1])
tensor(22.1963)

梯度与反向传播

关于矩阵微积分你应该知道多少?这取决于你,但有一篇很好的参考文章:The Matrix Calculus You need for deep learning

MSE

如果我们对求导,我们会得到,将其应用到代码中$ (x^2)‘= 2x$

#MSE
(output.squeeze(-1) - targ).pow(2).mean()

#MSE grad
inp.g = 2. * (inp.squeeze() - targ).unsqueeze(-1) / inp.shape[0]
# 存储上一层的梯度,因为链式法则中,梯度是要连乘的。

ReLU的梯度

  1. 如果输入 >0,则为 True(或 1)
  2. 如果输入 <=0,则为 False(或 0)
  3. 乘以超时.g(这是梯度

这是反向传播

我们保存中间计算,所以我们不必计算两次。请注意,在计算正向和反向传播时通常不需要损失

def mse_grad(inp, targ): 
    # grad of loss with respect to output of previous layer loss对最后一层的导数
    # the derivative of squared output x^2 => 2x
    inp.g = 2. * (inp.squeeze() - targ).unsqueeze(-1) / inp.shape[0]
    
def relu_grad(inp, out):
    # grad of relu with respect to input activations
    inp.g = (inp>0).float() * out.g

# 线性层的梯度求法:
def lin_grad(inp, out, w, b):
    # grad of matmul with respect to input
    inp.g = out.g @ w.t()
    w.g = (inp.unsqueeze(-1) * out.g.unsqueeze(1)).sum(0)
    b.g = out.g.sum(0)
    # 前向传播
def forward_and_backward(inp, targ):
    # forward pass:
    l1 = inp @ w1 + b1
    l2 = relu(l1)
    out = l2 @ w2 + b2
    # we don't actually need the loss in backward!
    loss = mse(out, targ)
    
        # backward pass:
    mse_grad(out, targ)
    lin_grad(l2, out, w2, b2)
    relu_grad(l1, l2)
    lin_grad(inp, l1, w1, b1)

反向传播就是链式法则,只不过节省了中间计算,不必要每次都计算他们。

loss基本上在梯度中没有出现,在反向传播中没有用到。

测试和比较与pytorch版本

orward_and_backward(x_train, y_train)

# Save for testing against later
w1g = w1.g.clone()
w2g = w2.g.clone()
b1g = b1.g.clone()
b2g = b2.g.clone()
ig  = x_train.g.clone()

# =========================================
# PYTORCH version for checking
# =========================================

# check against pytorch's version
xt2 = x_train.clone().requires_grad_(True)
w12 = w1.clone().requires_grad_(True)
w22 = w2.clone().requires_grad_(True)
b12 = b1.clone().requires_grad_(True)
b22 = b2.clone().requires_grad_(True)

def forward(inp, targ):
    # forward pass:
    l1 = inp @ w12 + b12
    l2 = relu(l1)
    out = l2 @ w22 + b22
    # we don't actually need the loss in backward!
    return mse(out, targ)

将我们写的与Pytorch的结果进行对比,发现差不多。

loss = forward(xt2, y_train)
loss.backward()
test_near(w22.grad, w2g)
test_near(b22.grad, b2g)
test_near(w12.grad, w1g)
test_near(b12.grad, b1g)
test_near(xt2.grad, ig )

重构 Refactoring

让我们做一些有意思的重构:

这与pytorchAPI非常相似。对于这些函数中的每一个,我们将前向和后向函数组合在一个类中。Relu 会有自己的前进后退功能

__call__ 将类视为函数

将所有的层作为一个class,并实现前向传播和反向传播。有了__call__意味着可以将这个类class视为函数。

class Relu():
    def __call__(self, inp):
        self.inp = inp
        self.out = inp.clamp_min(0.)-0.5
        return self.out
    
    def backward(self):
        self.inp.g = (self.inp>0).float() * self.out.g

提醒一下,在线性层中,Lin我们需要输出相对于权重的梯度和相对于偏差的输出

class Lin():
    def __init__(self, w, b): 
        self.w,self.b = w,b
        
    def __call__(self, inp):
        self.inp = inp
        self.out = inp@self.w + self.b
        return self.out
    
    def backward(self):
        self.inp.g = self.out.g @ self.w.t()
        # Creating a giant outer product, just to sum it, is inefficient!
        self.w.g = (self.inp.unsqueeze(-1) * self.out.g.unsqueeze(1)).sum(0)
        self.b.g = self.out.g.sum(0)
        
class Mse():
    def __call__(self, inp, targ):
        self.inp = inp
        self.targ = targ
        self.out = (inp.squeeze() - targ).pow(2).mean()
        return self.out
    
    def backward(self):
        self.inp.g = 2. * (self.inp.squeeze() - self.targ).unsqueeze(-1) / self.targ.shape[0]

让我们也让我们的模型成为一个类。此类中没有使用 pytorch 函数或实用程序

class Model():
    def __init__(self, w1, b1, w2, b2):
        self.layers = [Lin(w1,b1), Relu(), Lin(w2,b2)]
        self.loss = Mse()
        
    def __call__(self, x, targ):
        for l in self.layers: x = l(x)
        return self.loss(x, targ)
    
    def backward(self):
        self.loss.backward()
        
        # iterates through layers
        for l in reversed(self.layers): 
            l.backward()

让我们训练

# initialize the gradients
w1.g, b1.g, w2.g, b2.g = [None]*4

# create the model
model = Model(w1, b1, w2, b2)

时间消耗还是比较长的

%time loss = model(x_train, y_train)
--------
CPU times: user 274 ms, sys: 44.9 ms, total: 319 ms
Wall time: 59.6 ms

围绕具有通用功能的通用类进行设计

让我们尝试减少重复代码的数量。这将在通用module类中设计。然后对于每个函数,我们将为每个函数扩展基本模块。

# ============================================
# Base class
# ============================================

class Module():
    def __call__(self, *args):
        self.args = args
        self.out = self.forward(*args)
        return self.out
    
    def forward(self): 
        """ will be implemented when extended"""
        raise Exception('not implemented')
        
    def backward(self): 
        self.bwd(self.out, *self.args)
        
# ============================================
# Relu extended from module class
# ============================================     

class Relu(Module):
    def forward(self, inp): 
        return inp.clamp_min(0.)-0.5
    
    def bwd(self, out, inp): 
        inp.g = (inp>0).float() * out.g
        
# ============================================
# linear layer extended from module class
# ============================================
class Lin(Module):
    def __init__(self, w, b): 
        self.w,self.b = w,b
        
    def forward(self, inp): 
        return inp@self.w + self.b
    
    def bwd(self, out, inp):
        inp.g = out.g @ self.w.t()
        
        # implementing einsum 使用了爱因斯坦求和
        self.w.g = torch.einsum("bi,bj->ij", inp, out.g)
        self.b.g = out.g.sum(0)
        
# ============================================
# MSE extended from module
# ============================================
class Mse(Module):
    def forward (self, inp, targ):
        return (inp.squeeze() - targ).pow(2).mean()
    
    def bwd(self, out, inp, targ): 
        inp.g = 2*(inp.squeeze()-targ).unsqueeze(-1) / targ.shape[0]
        
# ============================================
# Remake the model
# ============================================
class Model():
    def __init__(self):
        self.layers = [Lin(w1,b1), Relu(), Lin(w2,b2)]
        self.loss = Mse()
        
    def __call__(self, x, targ):
        for l in self.layers: x = l(x)
        return self.loss(x, targ)
    
    def backward(self):
        self.loss.backward()
        for l in reversed(self.layers): l.backward()

让我们重新计时

w1.g,b1.g,w2.g,b2.g = [None]*4
model = Model()

s

%time loss = model(x_train, y_train)
-----------
CPU times: user 294 ms, sys: 11.2 ms, total: 306 ms
Wall time: 44.3 ms
%time model.backward()
-------
CPU times: user 454 ms, sys: 92.4 ms, total: 547 ms
Wall time: 174 ms

没有 Einsum

class Lin(Module):
    def __init__(self, w, b): self.w,self.b = w,b
        
    def forward(self, inp): return inp@self.w + self.b
    
    def bwd(self, out, inp):
        inp.g = out.g @ self.w.t()
        self.w.g = inp.t() @ out.g
        self.b.g = out.g.sum(0)

s

w1.g,b1.g,w2.g,b2.g = [None]*4
model = Model()

s

%time loss = model(x_train, y_train)

-----------
CPU times: user 280 ms, sys: 33.7 ms, total: 314 ms
Wall time: 45.8 ms
    
------
%time model.backward()
-----
CPU times: user 442 ms, sys: 70.9 ms, total: 513 ms
Wall time: 158 ms

带有nn.Linear和的Pytorch 版本nn.Module

from torch import nn

class Model(nn.Module):
    def __init__(self, n_in, nh, n_out):
        super()._0_init__()
        self.layers = [nn.Linear(n_in,nh), nn.ReLU(), nn.Linear(nh,n_out)]
        self.loss = mse
        
    def __call__(self, x, targ):
        for l in self.layers: x = l(x)
        return self.loss(x.squeeze(), targ)

s

model = Model(m, nh, 1)
%time loss = model(x_train, y_train)

--------
CPU times: user 280 ms, sys: 36.7 ms, total: 316 ms
Wall time: 40.5 ms

s

%time loss.backward()
--------
CPU times: user 183 ms, sys: 6.87 ms, total: 190 ms
Wall time: 33.8 ms

odel.backward()

CPU times: user 454 ms, sys: 92.4 ms, total: 547 ms
Wall time: 174 ms

### 没有 Einsum

```python
class Lin(Module):
    def __init__(self, w, b): self.w,self.b = w,b
        
    def forward(self, inp): return inp@self.w + self.b
    
    def bwd(self, out, inp):
        inp.g = out.g @ self.w.t()
        self.w.g = inp.t() @ out.g
        self.b.g = out.g.sum(0)

s

w1.g,b1.g,w2.g,b2.g = [None]*4
model = Model()

s

%time loss = model(x_train, y_train)

-----------
CPU times: user 280 ms, sys: 33.7 ms, total: 314 ms
Wall time: 45.8 ms
    
------
%time model.backward()
-----
CPU times: user 442 ms, sys: 70.9 ms, total: 513 ms
Wall time: 158 ms

带有nn.Linear和的Pytorch 版本nn.Module

from torch import nn

class Model(nn.Module):
    def __init__(self, n_in, nh, n_out):
        super()._0_init__()
        self.layers = [nn.Linear(n_in,nh), nn.ReLU(), nn.Linear(nh,n_out)]
        self.loss = mse
        
    def __call__(self, x, targ):
        for l in self.layers: x = l(x)
        return self.loss(x.squeeze(), targ)

s

model = Model(m, nh, 1)
%time loss = model(x_train, y_train)

--------
CPU times: user 280 ms, sys: 36.7 ms, total: 316 ms
Wall time: 40.5 ms

s

%time loss.backward()
--------
CPU times: user 183 ms, sys: 6.87 ms, total: 190 ms
Wall time: 33.8 ms

标签:tensor,self,lesson8,inp,fastai,train,2019,ms,def
来源: https://blog.csdn.net/haronchou/article/details/120541922