pytorch-动手学深度学习

目录

2.预备知识

2.1数据操作

torch.arange()

x.shape

x.numel()

x.reshape(m, n)

torch.zeros((2, 3, 4))  torch.ones((2, 3, 4))

torch.randn(3, 4)

torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

x + y, x - y, x * y, x / y, x ** y, x == y

torch.exp(x)

torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)

X.sum()

torch.zeros_like(Y)

id()

X[:] = X + Y 或X += Y

A = X.numpy() ,  B = torch.tensor(A)

a.item(), float(a), int(a)

2.2. 数据预处理

os.makedirs(name, mode=0o777, exist_ok=False)

os.path.join()

with open() as f

pd.read_csv()

.iloc[ , ]

.fillna()

pd.get_dummies()

.values

2.3. 线性代数

len(x)

A.T

.clone()

.sum()

.mean()

.cumsum(axis=0)

torch.dot(x, y) 与 torch.sum(x * y)相同(点积)

torch.mv(A, x) (矩阵-向量积)

torch.mm(A, B)(矩阵-矩阵乘法)

torch.norm()

torch.abs()

2.4. 微积分


2.预备知识

2.1数据操作

torch.arange()

arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor
  • 返回一维张量,其值介于区间 [ start , end )(前闭后开),以 step 为步长等间隔取值。
  • 当requires_grad=True时,这个变量的梯度将会被累加(默认是不累加)。这意味着在反向传播时,这个变量的梯度将会被计算并累加到其它变量上。
    >>> torch.arange(5)  # 默认以 0 为起点
    tensor([ 0,  1,  2,  3,  4])
    >>> torch.arange(1, 4)  # 默认间隔为 1
    tensor([ 1,  2,  3])
    >>> torch.arange(1, 2.5, 0.5)  # 指定间隔 0.5
    tensor([ 1.0000,  1.5000,  2.0000])

x.shape

       可以通过张量的shape属性来访问张量(沿每个轴的长度)的形状 。

x.numel()

       如果只想知道张量中元素的总数(标量),即形状的所有元素乘积,可以检查它的大小(size)。

x.reshape(m, n)

       改变一个张量的形状而不改变元素数量和元素值。不需要通过手动指定每个维度来改变形状。 也就是说,如果我们的目标形状是(高度,宽度), 那么在知道宽度后,高度会被自动计算得出,不必我们自己做除法。 为了获得一个3行的矩阵,我们手动指定了它有3行和4列。 幸运的是,我们可以通过-1来调用此自动计算出维度的功能。 即我们可以用x.reshape(-1, 4)或x.reshape(3 , -1)来取代x.reshape(3 ,4)。

torch.zeros((2, 3, 4))  torch.ones((2, 3, 4))

     可以创建一个形状为(2,3,4)的张量,其中所有元素都设置为0/1。

torch.randn(3, 4)

      创建一个形状为(3,4)的张量。 其中的每个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样。

torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

      提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值。 在这里,最外层的列表对应于轴0,内层的列表对应于轴1。

x + y, x - y, x * y, x / y, x ** y, x == y

     x = torch.tensor([1.0, 2, 4, 8])           # **运算符是求幂运算
     y = torch.tensor([2, 2, 2, 2]) 

     对于任意具有相同形状的张量, 常见的标准算术运算符(+-*/**)都可以被升级为按元素运算。

     x == y: 对于每个位置,如果x和y在该位置相等,则新张量中相应项的值为1。意味着该位置处为真(True),否则该位置为0(False)。

torch.exp(x)

     对张量作指数运算

torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)

     沿行(轴-0,形状的第一个元素) 和按列(轴-1,形状的第二个元素)连结两个矩阵时,dim=0,增加行数;dim=1,增加列数。

X.sum()

     对张量中的所有元素进行求和,会产生一个单元素张量。

torch.zeros_like(Y)

     继承了Y的shape,将里面的值全部替换成0。

id()

   id()它给我们提供了内存中引用对象的确切地址。

X[:] = X + Y 或X += Y

      运行一些操作可能会导致为新结果分配内存。 例如,如果我们用Y = X + Y,我们将取消引用Y指向的张量,而是指向新分配的内存处的张量。可以使用X[:] = X + Y 或X += Y来减少操作的内存开销。

A = X.numpy() ,  B = torch.tensor(A)

      将深度学习框架定义的张量转换为NumPy张量(numpy)很容易,反之也同样容易。 torch张量和numpy数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。

a.item(), float(a), int(a)

     要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数。

2.2. 数据预处理

      padans软件包是Python中常用的数据分析工具中,padans可以与张量兼容。

import os

os.makedirs(os.path.join('..', 'data'), exist_ok=True)
data_file = os.path.join('..', 'data', 'house_tiny.csv')
with open(data_file, 'w') as f:
    f.write('NumRooms,Alley,Price\n')  # 列名
    f.write('NA,Pave,127500\n')  # 每行表示一个数据样本
    f.write('2,NA,106000\n')
    f.write('4,NA,178100\n')
    f.write('NA,NA,140000\n')

os.makedirs(name, mode=0o777, exist_ok=False)

  参数说明:

      name:要创建的目录路径。

      mode:目录权限,默认为0o777,表示该目录所有用户都有读写执行权限。

      exist_ok:如果设置为True,则在目录已经存在的情况下不会抛出异常,否则会抛出FileExistsError异常。

      总之,os.mkdir()和os.makedirs()都是Python中用于创建目录的函数(创建文件夹),其中os.makedirs()具有递归创建目录的功能,使用时需要注意它们的区别和语法。

os.path.join()

     Python中的os.path.join是用来合并路径的函数。它可以把多个路径组合在一起,并正确处理斜杠和反斜杠。

print(os.path.join('C:\\', 'Users', 'username', 'Desktop'))
# 输出: 'C:\\Users\\username\\Desktop'

with open() as f

文件的读写操作:

  •    with open('filename.txt', 'r') as f:

          content = f.read(f)  #文件的读操作

  •    with open('data.txt', 'w') as f:

         f.write('hello world')  #文件的写操作

pd.read_csv()

       pd.read_csv是pandas库中的一个函数,用于读取CSV文件并将其转换为DataFrame对象。加载原始数据集。

.iloc[ , ]

       (index location),iloc[a:b,c:d]:取行索引从a到b-1,列索引从c到d-1的数据。

.fillna()

       fillna() 是 DataFrame 中的一个方法,用于将数据中缺失的值填充为指定的值或方法所返回的值。填充的数值可以是标量,Series,DataFrame 等。该方法有多个参数,包括 value,method,axis 等。其中,value 参数用于填充缺失值的常数值;method 参数可以采用 ffill 或 bfill 方法进行前向或后向填充;axis 参数表示在哪个维度上填充缺失值,默认为 0,即在行上填充。

pd.get_dummies()

     用独热编码来替换离散值特征。

     函数中还有一个参数是dummy_na,其作用是:是否将“na”(缺失值)视为有效的特征值,并为其创建指示符特征。dummy_na = false,就是不将“na”视为有效特征值,等于true就是将“na”视为一个特征值,并为其建立指示符特征。

.values

     获取当前字典中所有键值对中的值( value )。

2.3. 线性代数

len(x)

     返回对象(字符、列表、元组等)长度或者项目个数。我们也可以通过.shape属性访问向量的长度。(返回轴0长度)

A.T

     矩阵的转置。

.clone()

B = A.clone()  # 通过分配新内存,将A的一个副本分配给B

.sum()

    调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。

    以矩阵为例,为了通过求和所有行的元素来降维(轴0),可以在调用函数时指定axis=0。 由于输入矩阵沿0轴降维以生成输出向量,因此输入轴0的维数在输出形状中消失

A_sum_axis0 = A.sum(axis=0)
A.sum(axis=[0, 1])  # 结果和A.sum()相同

sum_A = A.sum(axis=1, keepdims=True)  #保持轴数不变

.mean()

      mean() 是计算每组平均值的方法。同样,计算平均值的函数也可以沿指定轴降低张量的维度。

A.mean(axis=0), A.sum(axis=0) / A.shape[0] #两个结果相同

.cumsum(axis=0)

       沿某个轴计算A元素的累积总和, 比如axis=0(按行计算),可以调用cumsum函数。 此函数不会沿任何轴降低输入张量的维度。

torch.dot(x, y) 与 torch.sum(x * y)相同(点积)

       点积:两向量相同位置的按元素乘积的和。

torch.mv(A, x) (矩阵-向量积)

  A的列维数(沿轴1的长度)必须与x的维数(其长度)相同。其结果是一个向量。

torch.mm(A, B)(矩阵-矩阵乘法)

      A的列数要等于B的行数。

torch.norm()

      是对输入的tensor求对应的范数

      默认为p=‘fro’,计算矩阵的Frobenius norm (Frobenius 范数),就是矩阵各项元素的绝对值平方的总和的平方根。

torch.abs()

      返回输入参数的绝对值作为输出,输入参数必须是一个Tensor 数据类型的变量。

torch.abs(u).sum()  #求L1范数

2.4. 微积分

for in range

     用于循环遍历一个指定范围内的数字。(前闭后开)

for i in range(start, end, step):
    # Do something

2.5. 自动微分

y.backward()

     自动求导,如果y值是个标量,对向量x求导,那么直接使用;否则y不是标量,直接使用会报错。

y.backward()
x.grad

# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()

3.线性神经网络

3.2. 线性回归的从零开始实现

# 线性回归从零开始实现

%matplotlib inline
import random
import torch
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.shape)
    return X, y.reshape((-1, 1))

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

print('feature:', features[0], 'labelss:', labels[0])

d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1);  #detach:pytoch里一些计算图需要detach出来,然后才能转到numpy里去

# 读取数据集

def data_iter(batch_size, features, labels):
    num_examples = len(features)
    indices = list(range(num_examples))   #range()表示从0 - n-1,list()表示转成list格式
    # # 这些样本是随机读取的,没有特定的顺序,把下标随机打乱
    random.shuffle(indices)
    for i in range(0, num_examples, batch_size):
        batch_indices = torch.tensor(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):
    print(X, '\n', y)
    break

# 定义初始化模型参数

w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

# 定义模型

def linreg(X, w, b):
    #线性回归模型
    return torch.matmul(X, w) + b

# 定义损失函数

def squared_loss(y_hat, y):
    #均方损失
    return (y_hat-y.reshape(y_hat.shape))**2/2

# 定义优化算法

def sgd(params, lr, batch_size):                    #params是list,里面包含了w和b
    #小批量随机梯度下降
    with torch.no_grad():                           #更新的时候不需要参与梯度计算
        for param  in params:
            param -= lr * param.grad / batch_size   #梯度存在.grad里面
            param.grad.zero_()                      #把梯度设成0,不影响下一次梯度计算

# 训练过程

lr = 0.03  #学习率不能太小,也不能太大
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}')

print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')

3.3. 线性回归的简洁实现

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)        #生成数据集

#调用现有的API来读取数据
def load_array(data_arrays, batch_size, is_train=True):  
    #构造一个pytorch数据迭代器
    dataset = data.TensorDataset(*data_arrays)          #已经有features, labels,把它做成list,传到dataset里面
    return data.DataLoader(dataset, batch_size, shuffle=is_train)     #得到打乱的批量数据
 
batch_size = 10
data_iter = load_array((features, labels), batch_size)

next(iter(data_iter))   #把data_iter转成iterator,通过next获得第一批数据

#使用框架预定义好的层
# 'nn'是神经网络的缩写
from torch import nn

net = nn.Sequential(nn.Linear(2,1))         #线性层,即全连接层,唯一要指定输入维度和输出维度,Sequential是list of layers

#初始化模型参数
net[0].weight.data.normal_(0, 0.01)   #net可以通过0访问到w,再通过data访问到它的真实值
net[0].bias.data.fill_(0)

#计算均方误差
loss = nn.MSELoss()    

#实例化SGD
trainer = torch.optim.SGD(net.parameters(), lr=0.03)    #传入net所有参数,包括w和b

#训练
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()           #pytorch已经做了sum了
        trainer.step()         #进行模型更新
    l = loss(net(features), labels)
    print(f'epoch{epoch + 1}, loss{l:f}')

5.深度学习计算

5.1层和块

# 多层感知机
import torch
from torch import nn
from torch.nn import functional as F

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

X = torch.rand(2, 20)
net(X)

# 自定义块
class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Module的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__()
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))
    
net = MLP()
net(X)

# 顺序块
class MySequential(nn.Module):
    def __init__(self, *args):        # *args是收集参数,相当于把若干个参数打包成一个来传入
        super().__init__()
        for block in args:
            self._modules[block] = block
    
    def forward(self, X):
        for block in self._modules.values():
            X = block(X)
        return X
    
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)

# 在前向传播函数中执行代码
class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 不计算梯度的随机权重参数。因此其在训练期间保持不变
        self.rand_weight = torch.rand((20, 20), requires_grad=False)
        self.linear = nn.Linear(20, 20)

    def forward(self, X):
        X = self.linear(X)
        # 使用创建的常量参数以及relu和mm函数
        X = F.relu(torch.mm(X, self.rand_weight) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数
        X = self.linear(X)
        # 控制流
        while X.abs().sum() > 1:       # 绝对值求和
            X /= 2
        return X.sum()

net = FixedHiddenMLP()
net(X)

# 混合搭配各种组合块的方法
class NestMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU())
        self.linear = nn.Linear(32, 16)
        
    def forward(self, X):
        return self.linear(self.net(X))

chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)

5.2. 参数管理

import torch
from torch import nn

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)

# 参数访问
print(net[2].state_dict())

# 目标参数
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)

net[2].weight.grad == None  # 这里还没有做反向计算

#  一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])   # 整个网络

net.state_dict()['2.bias'].data

# 从嵌套块收集参数
def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                         nn.Linear(8, 4), nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        # 在这里嵌套
        net.add_module(f'block {i}', block1())    # 插入四个block1
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)

print(rgnet)

# 内置初始化
def init_normal(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
        
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]

# 将所有参数初始化为给定的常数,比如初始化为1(不可以这样做)
def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)
        nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]

# 对某些块应用不同的初始化方法
def init_xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)

net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

# 自定义初始化
def my_init(m):
    if type(m) == nn.Linear:
        print("Init", *[(name, param.shape)
                        for name, param in m.named_parameters()][0])
        nn.init.uniform_(m.weight, -10, 10)   # 均匀初始化
        m.weight.data *= m.weight.data.abs() >= 5    # 保留绝对值大于5的权重,不是的话就设成0

net.apply(my_init)
net[0].weight[:2]

# 可以直接设置参数
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]

# 参数绑定,希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

5.4. 自定义层

#构造一个没有任何参数的自定义层
import torch
import torch.nn.functional as F
from torch import nn

class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()
        
    def forward(self, X):
        return X - X.mean()
    
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))

# 将层作为组件合并到更复杂的模型中
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

Y = net(torch.rand(4, 8))
Y.mean()

# 带参数的层
class MyLinear(nn.Module):
    def __init__(self, in_units, units):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(in_units, units))
        self.bias = nn.Parameter(torch.randn(units,))
        
    def forward(self, X):
        linear = torch.matmul(X, self.weight.data) + self.bias.data    # 通过.data访问对应的参数
        return F.relu(linear)
    
linear = MyLinear(5, 3)
linear.weight

# 使用自定义层直接执行前向传播计算
linear(torch.rand(2, 5))

# 使用自定义层构建模型
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

5.5. 读写文件

# 加载和保存张量
import torch
from torch import nn
from torch.nn import functional as F

x = torch.arange(4)
torch.save(x, 'x-file')

x2 = torch.load("x-file")
x2

# 存储一个张量列表,然后把它们读回内存
y = torch.zeros(4)
torch.save([x, y], 'x-file')

x2, y2 = torch.load("x-file")
(x2, y2)

# 写入或读取从字符串映射到张量的字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load("mydict")
mydict2

# 加载和保存模型参数
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(20, 256)
        self.output = nn.Linear(256, 10)
        
    def forward(self, x):
        return self.output(F.relu(self.hidden(x)))
    
net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)

# 将模型的参数存储在一个叫做“mlp.params”的文件中
torch.save(net.state_dict(), 'mlp.params')   # state_dict()得到所有的parameter

# 实例化了原始多层感知机模型的一个备份,直接读取文件中存储的参数。
clone = MLP()   # 先声明一下,把网络生成出来
clone.load_state_dict(torch.load('mlp.params'))    # 从字典读出,overwrite初始化的参数
clone.eval()

# 由于两个实例具有相同的模型参数,在输入相同的X时, 两个实例的计算结果应该相同。 让我们来验证一下
Y_clone = clone(X)
Y_clone == Y

5.6. GPU

import torch
from torch import nn

torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')

# 查询可用gpu的数量
torch.cuda.device_count()

# 定义了两个方便的函数, 这两个函数允许我们在不存在所需所有GPU的情况下运行代码
def try_gpu(i=0):  #@save
    """如果存在,则返回gpu(i),否则返回cpu()"""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

def try_all_gpus():  #@save
    """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
    devices = [torch.device(f'cuda:{i}')
             for i in range(torch.cuda.device_count())]
    return devices if devices else [torch.device('cpu')]

try_gpu(), try_gpu(10), try_all_gpus()

# 查询张量所在的设备
x = torch.tensor([1, 2, 3])
x.device

# 存储在GPU上
X = torch.ones(2, 3, device=try_gpu())
X

Y = torch.rand(2, 3, device=try_gpu(1))
Y

# 它在同一设备上找不到数据会导致失败。 由于Y位于第二个GPU上,所以我们需要将X移到那里, 然后才能执行相加运算。
Z = X.cuda(1)
print(X)
print(Z)

# 现在数据在同一个GPU上(Z和Y都在),我们可以将它们相加
Y + Z  

# 假设变量Z已经存在于第二个GPU上。 如果我们还是调用Z.cuda(1)会发生什么? 它将返回Z,而不会复制并分配新内存。
Z.cuda(1) is Z

# 神经网络与GPU
# 神经网络模型可以指定设备。 下面的代码将模型参数放在GPU上
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())    # 把它挪到GPU上

# 当输入为GPU上的张量时,模型将在同一GPU上计算结果
net(X)

# 确认模型参数存储在同一个GPU上
net[0].weight.data.device

6. 卷积神经网络

6.2. 图像卷积

# 互相关运算
import torch
from torch import nn
from d2l import torch as d2l

def corr2d(X, K):   # K是核矩阵
    """计算二维互相关运算"""
    h, w = K.shape     # 行数和列数
    Y = torch.zeros(X.shape[0] - h + 1, X.shape[1] - w + 1)   # Y输出的大小
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
    return Y

# 验证上述二维互相关运算的输出
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)

# 实现二维卷积层
class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))    # 可学
        self.bias =  nn.Parameter(torch.zeros(1))
        
    def forward(self, x):
        return corr2d(x, self.weight) + self.bias
    
# 图像中目标的边缘检测
X = torch.ones(6, 8)
X[:, 2:6] = 0
X

K = torch.tensor([[1.0, -1.0]])

Y = corr2d(X, K)
Y

corr2d(X.t(), K)   # 这个卷积核K只可以检测垂直边缘,无法检测水平边缘

# 学习由X生成Y的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)     # 输入输出通道都为1

X = X.reshape((1, 1, 6, 8))     # 批量大小数,通道数
Y = Y.reshape((1, 1, 6, 7))

for i in range(10):    # 迭代十次
    Y_hat = conv2d(X)
    l = (Y_hat - Y)**2
    conv2d.zero_grad()
    l.sum().backward()
    conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad    # 学习率3e-2
    if (i + 1) % 2 == 0:
        print(f'batch {i + 1}, loss {l.sum():.3f}')

# 所学的卷积核的权重张量
conv2d.weight.data.reshape((1, 2))

6.3. 填充和步幅

# 在所有侧边填充1个像素
import torch
from torch import nn

def comp_conv2d(conv2d, X):
    X = X.reshape((1, 1) + X.shape)   # (1, 1)是批量大小数和通道数
    Y = conv2d(X)
    return Y.reshape(Y.shape[2:])

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)      # 先填充再卷积
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape

# 填充不同的高度和宽度
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

# 将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape

# 一个稍微复杂的例子
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape

6.4. 多输入多输出通道

# 实现一下多输入通道互相关运算
import torch
from d2l import torch as d2l

def corr2d_multi_in(X, K):     # X,K都是3d
    return sum(d2l.corr2d(x, k) for x, k in zip(X, K))    # 对X,K zip起来,每次zip起来都会对最外面的那个通道做遍历,即输入通道

# 验证互相关运算的输出
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)

# 计算多个通道的输出的互相关函数
def corr2d_multi_in_out(X, K):      # X是3d的,K是4d的,其最外维是输出通道
    return torch.stack([corr2d_multi_in(X, k) for k in K], 0)    # 把每个输出通道做多输入通道互相关运算

K = torch.stack((K, K + 1, K + 2), 0)
K.shape

corr2d_multi_in_out(X, K)

#  1x1卷积层, 等价于一个全连接
def corr2d_multi_in_out_1x1(X, K):
    c_i, h, w = X.shape
    c_o = K.shape[0]
    X = X.reshape((c_i, h * w))   # h * w把高宽拉成一条向量
    K = K.reshape((c_o, c_i))
    Y = torch.matmul(K, X)
    return Y.reshape((c_o, h, w))

X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))

Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
# 当执行1x1卷积运算时,上述函数相当于先前实现的互相关函数corr2d_multi_in_out
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6   

6.5. 汇聚层

# 实现汇聚层的前向传播
import torch
from torch import nn
from d2l import torch as d2l

def pool2d(X, pool_size, mode='max'):
    p_h, p_w = pool_size
    Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))   # 单通道
    for i in range(Y.shape[0]):
         for j in range(Y.shape[1]):
                if mode == 'max':
                    Y[i, j] = X[i:i + p_h, j:j + p_w].max()
                elif mode == 'avg':
                    Y[i, j] = X[i:i + p_h, j:j + p_w].mean()
    return Y

# 验证二维最大汇聚层的输出
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))

# 验证平均汇聚层
pool2d(X, (2, 2), 'avg')

# 填充和步幅
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X

# 深度学习框架中的步幅与汇聚窗口的大小相同,窗口没有重叠
pool2d = nn.MaxPool2d(3)   # 3x3窗口
pool2d(X)

# 手动设定填充和步幅
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)

# 设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)

# 池化层在每个输入通道上单独运算
X = torch.cat((X, X + 1), 1)    # 双通道
X

pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)

6.6. 卷积神经网络(LeNet)

# LeNet(LeNet-5)由两个部分组成:卷积编码器和全连接层密集块
import torch
from torch import nn
from d2l import torch as d2l

class Reshape(torch.nn.Module):      # 可以放在nn.Sequential()中
    def forward(self, x):
        return x.view(-1, 1, 28, 28)
    
net = torch.nn.Sequential(Reshape(),
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),    # 批量保持,后面全拉成一个向量
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10))

# 检查模型
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__, 'output shape: \t', X.shape)

# 模型训练,LeNet在Fashion-MNIST数据集上的表现
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

# 对evaluate_accuracy函数进行轻微的修改
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
    """使用GPU计算模型在数据集上的精度"""
    if isinstance(net, nn.Module):
        net.eval()  # 设置为评估模式
        if not device:
            device = next(iter(net.parameters())).device
    # 正确预测的数量,总预测的数量
    metric = d2l.Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(X, list):
                # BERT微调所需的(之后将介绍)
                X = [x.to(device) for x in X]
            else:
                X = X.to(device)
            y = y.to(device)
            metric.add(d2l.accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

# 为了使用GPU,我们还需要一点小改动
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    """用GPU训练模型(在第六章定义)"""
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    print('training on', device)
    net.to(device)
    optimizer = torch.optim.SGD(net.parameters(), lr=lr)
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
    timer, num_batches = d2l.Timer(), len(train_iter)
    for epoch in range(num_epochs):
        # 训练损失之和,训练准确率之和,样本数
        metric = d2l.Accumulator(3)
        net.train()
        for i, (X, y) in enumerate(train_iter):
            timer.start()
            optimizer.zero_grad()
            X, y = X.to(device), y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
            timer.stop()
            train_l = metric[0] / metric[2]
            train_acc = metric[1] / metric[2]
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i + 1) / num_batches,
                             (train_l, train_acc, None))
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
    print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
          f'test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(device)}')

# 训练和评估LeNet-5模型
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

7. 现代卷积神经网络

7.1. 深度卷积神经网络(AlexNet)

import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(
    # 这里使用一个11*11的更大窗口来捕捉对象。
    # 同时,步幅为4,以减少输出的高度和宽度。
    # 另外,输出通道的数目远大于LeNet
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 使用三个连续的卷积层和较小的卷积窗口。
    # 除了最后的卷积层,输出通道的数量进一步增加。
    # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Flatten(),
    # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
    nn.Linear(6400, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    # 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
    nn.Linear(4096, 10))

X = torch.randn(1, 1, 224, 224)
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape)

# Fashion-MNIST图像的分辨率(28x28像素)低于ImageNet图像。我们将它们增加到224x224(通常来讲这不是一个明智的做法,但在这里这样做是为了有效使用AlexNet架构)
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

# 训练
lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

7.2. 使用块的网络(VGG)

import torch
from torch import nn
from d2l import torch as d2l

# 定义了一个名为vgg_block的函数来实现一个VGG块
def vgg_block(num_convs, in_channels, out_channels):   # num_convs卷积层的个数
    layers = []
    for _ in range(num_convs):
        layers.append(nn.Conv2d(in_channels, out_channels,
                                kernel_size=3, padding=1))
        layers.append(nn.ReLU())
        in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
    return nn.Sequential(*layers)

# VGG网络
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))   # 一共五块,(卷积层数,输出通道数),块数不能变,但可以设置每块多少层卷积和通道数

# VGG-11
def vgg(conv_arch):
    conv_blks = []
    in_channels = 1
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
        in_channels = out_channels

    return nn.Sequential(
        *conv_blks, nn.Flatten(),
        # 全连接层部分
        nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 10))

net = vgg(conv_arch)

# 观察每个层输出的形状
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
    X = blk(X)
    print(blk.__class__.__name__,'output shape:\t',X.shape)

# 由于VGG-11比AlexNet计算量更大,因此我们构建了一个通道数较少的网络
ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]    # 所有通道数除以4
net = vgg(small_conv_arch)

# 模型训练
lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

7.3. 网络中的网络(NiN)

import torch
from torch import nn
from d2l import torch as d2l

# NiN块
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
    return nn.Sequential(
        nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),     # 一个普通卷积层开始,后面是两个1x1的卷积层
        nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())

# NiN模型
net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, strides=4, padding=0),
    nn.MaxPool2d(3, stride=2),
    nin_block(96, 256, kernel_size=5, strides=1, padding=2),
    nn.MaxPool2d(3, stride=2),
    nin_block(256, 384, kernel_size=3, strides=1, padding=1),
    nn.MaxPool2d(3, stride=2),
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, strides=1, padding=1),
    nn.AdaptiveAvgPool2d((1, 1)),   # 全局平均池化层,高宽都变成1
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten())

# 查看每个块的输出形状
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

# 训练模型
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

7.4. 含并行连结的网络(GoogLeNet)

# Inception块
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Inception(nn.Module):
    # c1--c4是每条路径的输出通道数
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        super(Inception, self).__init__(**kwargs)
        # 线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1x1卷积层后接5x5卷积层
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3x3最大汇聚层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return torch.cat((p1, p2, p3, p4), dim=1)       # dim=0是批量数

b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.ReLU(),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   nn.AdaptiveAvgPool2d((1,1)),
                   nn.Flatten())

net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))

X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

7.5. 批量规范化

import torch
from torch import nn
from d2l import torch as d2l

# 从零实现
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):     # gamma, beta可学的
    # 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
    if not torch.is_grad_enabled():
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)   
        if len(X.shape) == 2:
            # 使用全连接层的情况,计算特征维上的均值和方差
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
            # 这里我们需要保持X的形状以便后面可以做广播运算
            mean = X.mean(dim=(0, 2, 3), keepdim=True)
            var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
        # 训练模式下,用当前的均值和方差做标准化
        X_hat = (X - mean) / torch.sqrt(var + eps)
        # 更新移动平均的均值和方差,全局的
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
    Y = gamma * X_hat + beta  # 缩放和移位
    return Y, moving_mean.data, moving_var.data

class BatchNorm(nn.Module):
    # num_features:完全连接层的输出数量或卷积层的输出通道数。
    # num_dims:2表示完全连接层,4表示卷积层
    def __init__(self, num_features, num_dims):
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 非模型参数的变量初始化为0和1
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(shape)

    def forward(self, X):
        # 如果X不在内存上,将moving_mean和moving_var
        # 复制到X所在显存上
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
        # 保存更新过的moving_mean和moving_var
        Y, self.moving_mean, self.moving_var = batch_norm(
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9)
        return Y

# 使用批量规范化层的 LeNet
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(16 * 4 * 4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
    nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
    nn.Linear(84, 10))

# 训练
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,))

# 简明实现
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
    nn.Linear(84, 10))

d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

7.6. 残差网络(ResNet)

10.注意力机制

10.2.注意力汇聚:Nadaraya-Watson 核回归

import torch
from torch import nn
from d2l import torch as d2l
from matplotlib import pyplot as plt

# 注意力汇聚:Nadaraya-Watson 核回归
n_train = 50  # 训练样本数
x_train, _ = torch.sort(torch.rand(n_train) * 5)   # 排序后的训练样本,0-5之间随机生成50个数,然后排序

def f(x):
    return 2 * torch.sin(x) + x**0.8

y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,))  # 训练样本的输出,label
x_test = torch.arange(0, 5, 0.1)  # 测试样本
y_truth = f(x_test)  # 测试样本的真实输出
n_test = len(x_test)  # 测试样本数
n_test

def plot_kernel_reg(y_hat):
    d2l.plot(x_test, [y_truth, y_hat], 'x', 'y', legend=['Truth', 'Pred'],
             xlim=[0, 5], ylim=[-1, 5])
    d2l.plt.plot(x_train, y_train, 'o', alpha=0.5);
    
# 平均汇聚
y_hat = torch.repeat_interleave(y_train.mean(), n_test)
plot_kernel_reg(y_hat)
# 黄色的点是训练的点

# 非参数注意力汇聚

# x_test是query,x_train包含着键

# X_repeat的形状:(n_test,n_train),
# 每一行都包含着相同的测试输入(例如:同样的查询)
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))    # repeat_interleave控制重复多少次
# x_train包含着键。attention_weights的形状:(n_test,n_train),
# 每一行都包含着要在给定的每个查询的值(y_train)之间分配的注意力权重
attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1)
# y_hat的每个元素都是值的加权平均值,其中的权重是注意力权重
y_hat = torch.matmul(attention_weights, y_train)
plot_kernel_reg(y_hat)

# 注意力的权重
 
# 以后调用该函数来显示att权重
def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5),
                 cmap='Reds'):
    d2l.use_svg_display()
    num_rows, num_cols = matrices.shape[0], matrices.shape[1]
    fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize,
                                sharex=True, sharey=True, squeeze=False)
    for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)):
        for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)):
            pcm = ax.imshow(matrix.detach().numpy(), cmap=cmap)
            if i == num_rows - 1:
                ax.set_xlabel(xlabel)
            if j == 0:
                ax.set_ylabel(ylabel)
            if titles:
                 ax.set_title(titles[j])
    fig.colorbar(pcm, ax=axes, shrink=0.6);


show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0),
                  xlabel='Sorted training inputs',
                  ylabel='Sorted testing inputs')
plt.show()

# 批量矩阵乘法
X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
torch.bmm(X, Y).shape

# 带参数注意力汇聚
class NWKernelRegression(nn.Module):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.w = nn.Parameter(torch.rand((1,), requires_grad=True))

    def forward(self, queries, keys, values):
        # queries和attention_weights的形状为(查询个数,“键-值”对个数)
        queries = queries.repeat_interleave(keys.shape[1]).reshape((-1, keys.shape[1]))
        self.attention_weights = nn.functional.softmax(
            -((queries - keys) * self.w)**2 / 2, dim=1)
        # values的形状为(查询个数,“键-值”对个数)
        return torch.bmm(self.attention_weights.unsqueeze(1),
                         values.unsqueeze(-1)).reshape(-1)

# 训练
# X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入
X_tile = x_train.repeat((n_train, 1))
# Y_tile的形状:(n_train,n_train),每一行都包含着相同的训练输出
Y_tile = y_train.repeat((n_train, 1))
# keys的形状:('n_train','n_train'-1)
keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
# values的形状:('n_train','n_train'-1)
values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))

# 使用平方损失函数
net = NWKernelRegression()
loss = nn.MSELoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.5)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])

for epoch in range(5):
    trainer.zero_grad()
    l = loss(net(x_train, keys, values), y_train)
    l.sum().backward()
    trainer.step()
    print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
    animator.add(epoch + 1, float(l.sum()))

# keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键)
keys = x_train.repeat((n_test, 1))
# value的形状:(n_test,n_train)
values = y_train.repeat((n_test, 1))
y_hat = net(x_test, keys, values).unsqueeze(1).detach()
plot_kernel_reg(y_hat)

show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0),
                  xlabel='Sorted training inputs',
                  ylabel='Sorted testing inputs')
# w使窗口变窄,每一个测试样本分的权重相对集中

10.3. 注意力评分函数

# 注意力打分函数
import math
import torch
from torch import nn
from d2l import torch as d2l
from matplotlib import pyplot as plt

# 遮蔽softmax操作
#@save
def masked_softmax(X, valid_lens):
    """通过在最后一个轴上掩蔽元素来执行softmax操作"""
    # X:3D张量,valid_lens:1D或2D张量
    if valid_lens is None:
        return nn.functional.softmax(X, dim=-1)    # 没给valid_lens,就是正常的softmax
    else:
        shape = X.shape
        if valid_lens.dim() == 1:
            valid_lens = torch.repeat_interleave(valid_lens, shape[1])
        else:
            valid_lens = valid_lens.reshape(-1)
        # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
        X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens,
                              value=-1e6)
        return nn.functional.softmax(X.reshape(shape), dim=-1)

# 演示此函数是如何工作
# 假设给了(2, 2, 4),第一个2是batch_size,[2, 3]表示第一个样本前面两列是有效的,第二个前三列是有效的
masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3]))   

masked_softmax(torch.rand(2, 2, 4), torch.tensor([[1, 3], [2, 4]]))

#@save
class AdditiveAttention(nn.Module):
    """加性注意力"""
    def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
        super(AdditiveAttention, self).__init__(**kwargs)
        self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
        self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
        self.w_v = nn.Linear(num_hiddens, 1, bias=False)
        self.dropout = nn.Dropout(dropout)

    def forward(self, queries, keys, values, valid_lens):    # valid_lens表示有多少对k-v是需要的
        queries, keys = self.W_q(queries), self.W_k(keys)
        # 在维度扩展后,
        # queries的形状:(batch_size,查询的个数,1,num_hidden)
        # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
        # 使用广播方式进行求和
        features = queries.unsqueeze(2) + keys.unsqueeze(1)
        features = torch.tanh(features)
        # self.w_v仅有一个输出,因此从形状中移除最后那个维度。
        # scores的形状:(batch_size,查询的个数,“键-值”对的个数)
        scores = self.w_v(features).squeeze(-1)
        self.attention_weights = masked_softmax(scores, valid_lens)
        # values的形状:(batch_size,“键-值”对的个数,值的维度)
        return torch.bmm(self.dropout(self.attention_weights), values)
    
# 演示上面的AdditiveAttention类
queries, keys = torch.normal(0, 1, (2, 1, 20)), torch.ones((2, 10, 2))   # (2, 1, 20) batch_size为2,1个query,长度为20
# values的小批量,两个值矩阵是相同的,是(2, 10, 4)
values = torch.arange(40, dtype=torch.float32).reshape(1, 10, 4).repeat( 2, 1, 1)
valid_lens = torch.tensor([2, 6])    # 查看键值对

attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8,
                              dropout=0.1)
attention.eval()
attention(queries, keys, values, valid_lens)

# 注意力权重
def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5),
                 cmap='Reds'):
    d2l.use_svg_display()
    num_rows, num_cols = matrices.shape[0], matrices.shape[1]
    fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize,
                                sharex=True, sharey=True, squeeze=False)
    for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)):
        for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)):
            pcm = ax.imshow(matrix.detach().numpy(), cmap=cmap)
            if i == num_rows - 1:
                ax.set_xlabel(xlabel)
            if j == 0:
                ax.set_ylabel(ylabel)
            if titles:
                 ax.set_title(titles[j])
    fig.colorbar(pcm, ax=axes, shrink=0.6);
    
show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)),
                  xlabel='Keys', ylabel='Queries')
plt.show()

# 缩放点积注意力
#@save
class DotProductAttention(nn.Module):
    """缩放点积注意力"""
    def __init__(self, dropout, **kwargs):
        super(DotProductAttention, self).__init__(**kwargs)
        self.dropout = nn.Dropout(dropout)

    # queries的形状:(batch_size,查询的个数,d)
    # keys的形状:(batch_size,“键-值”对的个数,d)
    # values的形状:(batch_size,“键-值”对的个数,值的维度)
    # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
    def forward(self, queries, keys, values, valid_lens=None):
        d = queries.shape[-1]
        # 设置transpose_b=True为了交换keys的最后两个维度
        scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)   # key要做转置
        self.attention_weights = masked_softmax(scores, valid_lens)
        return torch.bmm(self.dropout(self.attention_weights), values)

# 演示上述的DotProductAttention类
queries = torch.normal(0, 1, (2, 1, 2))
attention = DotProductAttention(dropout=0.5)
attention.eval()
attention(queries, keys, values, valid_lens)

show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)),
                  xlabel='Keys', ylabel='Queries')

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值