5.《动手学深度学习》深度学习计算

1 层和块

神经网络一般包含多层(layer)重复的特殊结构,即层组(groups of layers)

神经网络引入块(block)的概念,用于抽象地表示层、层组或整个模型

1.1 自定义块

从编程的角度来看,块由类(class)表示,类内需要包含前向传播函数和必需的参数,得益于自动微分的机制,后向传播函数是隐式的,一般无需单独定义

PyTortch版的自定义块示例如下:

import torch
from torch import nn
from torch.nn import functional as F

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) # 调用前向传播函数

Tensorflow版的自定义块示例如下:

import tensorflow as tf

class MLP(tf.keras.Model):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Model的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__()
        # Hiddenlayer
        self.hidden = tf.keras.layers.Dense(units=256, activation=tf.nn.relu)
        self.out = tf.keras.layers.Dense(units=10)  # Outputlayer

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def call(self, X):
        return self.out(self.hidden((X)))

net = MLP()
net(X) # 调用前向传播函数

1.2 顺序块

构建好不同的块后,还需要借助一个Sequential类(顺序块)来组建模型,这是一种特殊的类,输入参数用于指明块的执行顺序,从而实现块的组装与模型的搭建

PyTortch版的自定义顺序块示例如下:

# 对应内置类为nn.Sequential
class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
            # 变量_modules中。module的类型是OrderedDict
            self._modules[str(idx)] = module

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        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)

Tensorflow版的自定义顺序块示例如下:

# 对应内置类为keras.models.Sequential
class MySequential(tf.keras.Model):
    def __init__(self, *args):
        super().__init__()
        self.modules = []
        for block in args:
            # 这里,block是tf.keras.layers.Layer子类的一个实例
            self.modules.append(block)

    def call(self, X):
        for module in self.modules:
            X = module(X)
        return X

# 顺序块的使用示例
net = MySequential(
    tf.keras.layers.Dense(units=256, activation=tf.nn.relu),
    tf.keras.layers.Dense(10))
net(X)

常用技巧1:指定无需梯度更新的常数参数(constant parameter)

rand_weight = torch.rand((20, 20), requires_grad=False) # PyTorch
rand_weight = tf.constant(tf.random.uniform((20, 20))) # Tensorflow

常用技巧2:顺序块可以作为与其他块再组合,然后再套个顺序块(套娃~)

2 参数管理

2.1 参数访问

通过索引来访问模型的任意层,每一层都包含权重(weights)和偏置(bias)两个参数,每一个参数都表示为参数类的一个实例:

# PyTorch

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))
# torch.nn.parameter.Parameter 参数类的实例
print(net[2].bias)# 访问模型的第二层的偏置参数
print(net[2].bias.data)# 借助data属性访问数据
print(net[2].weight.grad)# 借助grad属性访问梯度

# 遍历地访问第一个全连接层的所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
# 遍历地访问所有层的所有参数
print(*[(name, param.shape) for name, param in net.named_parameters()])

# 另一种参数访问的方式
print(net.state_dict()['2.bias'].data)
# Tensorflow

import tensorflow as tf

# 初始化模型和参数
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4, activation=tf.nn.relu),
    tf.keras.layers.Dense(1),
])
X = tf.random.uniform((2, 4))
net(X)

# 访问模型的第二层
print(net.layers[2].weights)# 访问模型的第二层
print(type(net.layers[2].weights[1]))
# tensorflow.python.ops.resource_variable_ops.ResourceVariable
print(net.layers[2].weights[1])# 访问模型的第二层的偏置参数
print(tf.convert_to_tensor(net.layers[2].weights[1]))# 借助convert_to_tensor方法查看数据

# 访问第一个全连接层的所有参数
print(net.layers[1].weights)
# 访问所有层的所有参数
print(net.get_weights())

# 另一种参数访问的方式
net.get_weights()[1]

由上一节可知,模型内存在块与块之间的嵌套关系,可以通过嵌套索引的方式访问,

# PyTorch

# 初始化存在块嵌套的模型
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())
    return net

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

# 网络结构
print(rgnet)

# 访问第一个主要的块中、第二个子块的第一层的偏置项
rgnet[0][1][0].bias.data
# Tensorflow

# 初始化存在块嵌套的模型
def block1(name):
    return tf.keras.Sequential([
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(4, activation=tf.nn.relu)],
        name=name)

def block2():
    net = tf.keras.Sequential()
    for i in range(4):
        # 在这里嵌套
        net.add(block1(name=f'block-{i}'))
    return net

rgnet = tf.keras.Sequential()
rgnet.add(block2())
rgnet.add(tf.keras.layers.Dense(1))
rgnet(X)

# 网络结构
print(rgnet.summary())

# 访问第一个主要的块中、第二个子块的第一层的偏置项
rgnet.layers[0].layers[1].layers[1].weights[1]

2.2 参数初始化

良好的参数初始化有利于模型的训练和快速收敛,PyTorch和Keras都会根据输入输出的维度计算出一个合理的范围,用于均匀地初始化权重和偏置。PyTorch和Tensorflow分别在nn.init模块和keras.initializers模块中预置了多种初始化方法。

# PyTorch

# 正态分布初始化示例,均值0标准差0.01
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]

# Xavier初始化+常数初始化
def 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(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

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]
# Tensorflow

# 正态分布初始化示例,均值0标准差0.01
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4, activation=tf.nn.relu,
        kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.01),
        bias_initializer=tf.zeros_initializer()),
    tf.keras.layers.Dense(1)])

net(X)
net.weights[0], net.weights[1]

# Xavier初始化+常数初始化
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4,
        activation=tf.nn.relu,
        kernel_initializer=tf.keras.initializers.GlorotUniform()),
    tf.keras.layers.Dense(
        1, kernel_initializer=tf.keras.initializers.Constant(1)),
])

net(X)
print(net.layers[1].weights[0])
print(net.layers[2].weights[0])

# 自定义初始化方法
class MyInit(tf.keras.initializers.Initializer):
    def __call__(self, shape, dtype=None):
        data=tf.random.uniform(shape, -10, 10, dtype=dtype)
        factor=(tf.abs(data) >= 5)
        factor=tf.cast(factor, tf.float32)
        return data * factor

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4,
        activation=tf.nn.relu,
        kernel_initializer=MyInit()),
    tf.keras.layers.Dense(1),
])

net(X)
print(net.layers[1].weights[0])

# 手动直接设置参数
net.layers[1].weights[0][:].assign(net.layers[1].weights[0] + 1)
net.layers[1].weights[0][0, 0].assign(42)
net.layers[1].weights[0]

2.3 参数绑定

多个层间有时需要共享参数

当参数绑定时,梯度会叠加并用于共享参数的更新

# Pytorch:定义一个稠密层,然后使用它的参数来设置另一个层的参数

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])
# Tensorflow:Tensorfwlo会自动合并重复的层,所以不能直接套用PyTorch的方案。最常见的参数共享方案是通过制定命名空间的同一名字,并设置reuse=True实现参数的复用

# 在名字为foo的命名空间内创建名字为v的变量
with tf.variable_scope("foo"):
    v = tf.get_variable("v", [1], initializer=tf.constant_initializer(1.0))

# 因为在命名空间foo中已经存在名为v的变量,所以下面的代码将会报错
with tf.variable_scope("foo"):
    v = tf.get_variable("v",[1])

# 在生成上下文管理器时,将参数reuse设置为True。这样tf.get_vaiable函数将直接获取
# 已经声明的变量。
with tf.variable_scope("foo",reuse=True):
    v1 = tf.get_variable("v",[1])
    print v==v1 #输出为True,v和v1代表的是相同的变量

# 将参数reuse设置为True时,tf.variable_scope将只能获取已经创建过的变量。因为在命名
# 空间bar中还没有创建变量v,所以下面的代码将会报错
with tf.variable_scope("bar",reuse=True):
    v = tf.get_variable("v",[1])
# 参考自:https://ilewseu.github.io/2018/03/11/Tensorflow%E5%8F%98%E9%87%8F%E7%AE%A1%E7%90%86/

3 延后初始化

延后初始化(defers initialization):直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小

# PyTorch此功能还在开发中,API可能随时会变
# 目前是借助torch.nn.LazyLinear实现延后初始化

import torch
from torch import nn
net = nn.Sequential(nn.LazyLinear(256), nn.ReLU(),nn.Linear(256,10))
print(net) # 此时参数尺寸为空
[net[i].state_dict() for i in range(len(net))]
low = torch.finfo(torch.float32).min/10
high = torch.finfo(torch.float32).max/10
X = torch.zeros([2,20],dtype=torch.float32).uniform_(low, high)
net(X) # 根据输入维度自动完成初始化
print(net)
# Tensorflow

import tensorflow as tf

net = tf.keras.models.Sequential([
    tf.keras.layers.Dense(256, activation=tf.nn.relu),
    tf.keras.layers.Dense(10),
])
# 此时参数尚未初始化,权重为空
[net.layers[i].get_weights() for i in range(len(net.layers))]

X = tf.random.uniform((2, 20))
net(X) # 根据输入维度自动完成初始化
[w.shape for w in net.get_weights()]

4 自定义层

本节示例为自定义全连接层,输入参数包括in_units(输入维度)和unit(输出维度)

# PyTorch

import torch
import torch.nn.functional as F
from torch import nn

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
        return F.relu(linear)

linear = MyLinear(5, 3)
linear(torch.rand(2, 5)) # 测试前向传播计算

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64)) # 作为组件拼装到模型中
# Tensorflow

import tensorflow as tf

class MyDense(tf.keras.Model):
    def __init__(self, units):
        super().__init__()
        self.units = units

    def build(self, X_shape):
        self.weight = self.add_weight(name='weight',
            shape=[X_shape[-1], self.units],
            initializer=tf.random_normal_initializer())
        self.bias = self.add_weight(
            name='bias', shape=[self.units],
            initializer=tf.zeros_initializer())

    def call(self, X):
        linear = tf.matmul(X, self.weight) + self.bias
        return tf.nn.relu(linear)

dense = MyDense(3)
dense(tf.random.uniform((2, 5))) # 测试前向传播计算

net = tf.keras.models.Sequential([MyDense(8), MyDense(1)])
net(tf.random.uniform((2, 64))) # 作为组件拼装到模型中

5 读写文件

通过调用loadsave函数可以实现变量和模型参数的保存于读取

# PyTorch

import torch
import torch.nn.functional as F
from torch import nn

# 保存与读取单个张量
x = torch.arange(4)
torch.save(x, 'x-file')
x2 = torch.load('x-file')

# 保存与读取多个张量
y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')

# 保存与读取张量字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')

# 自定义模型
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)

# 保存与读取模型参数
torch.save(net.state_dict(), 'mlp.params')
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
# Tensorflow
import numpy as np
import tensorflow as tf

# 保存与读取单个张量
x = tf.range(4)
np.save('x-file.npy', x)
x2 = np.load('x-file.npy', allow_pickle=True)

# 保存与读取多个张量
y = tf.zeros(4)
np.save('xy-files.npy', [x, y])
x2, y2 = np.load('xy-files.npy', allow_pickle=True)

# 保存与读取张量字典
mydict = {'x': x, 'y': y}
np.save('mydict.npy', mydict)
mydict2 = np.load('mydict.npy', allow_pickle=True)

# 自定义模型
class MLP(tf.keras.Model):
    def __init__(self):
        super().__init__()
        self.flatten = tf.keras.layers.Flatten()
        self.hidden = tf.keras.layers.Dense(units=256, activation=tf.nn.relu)
        self.out = tf.keras.layers.Dense(units=10)

    def call(self, inputs):
        x = self.flatten(inputs)
        x = self.hidden(x)
        return self.out(x)

net = MLP()
X = tf.random.uniform((2, 20))
Y = net(X)

# 保存与读取模型参数
net.save_weights('mlp.params')
clone = MLP()
clone.load_weights('mlp.params')

6 GPU计算

6.1 查看GPU信息

首先确定你有GPU:

nvidia-smi # 查看显卡信息

通过设备device/上下文context可以实现计算的分配,常见设备相关操作:

# PyTorch

import torch
from torch import nn

torch.device('cpu') # 查看CPU
torch.device('cuda') # 查看GPU,等价于cuda:0
torch.device('cuda:1') # 查看第二块(index=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')]
# Tensorflow

import tensorflow as tf

tf.device('/CPU:0') # 查看CPU
tf.device('/GPU:0') # 查看GPU
tf.device('/GPU:1') # 查看第二块(index=1)GPU
len(tf.config.experimental.list_physical_devices('GPU')) # 查看可用GPU个数

def try_gpu(i=0):  #@save
    """如果存在,则返回gpu(i),否则返回cpu()"""
    if len(tf.config.experimental.list_physical_devices('GPU')) >= i + 1:
        return tf.device(f'/GPU:{i}')
    return tf.device('/CPU:0')

def try_all_gpus():  #@save
    """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
    num_gpus = len(tf.config.experimental.list_physical_devices('GPU'))
    devices = [tf.device(f'/GPU:{i}') for i in range(num_gpus)]
    return devices if devices else [tf.device('/CPU:0')]

6.2 指定与切换GPU

注意

  • 处于不同device中的两个张量,无法直接进行计算,因此需要切换device
  • device间传输数据效率偏低,因此要谨慎使用拷贝操作
  • 根据经验,一次性执行多个小操作比分散执行多个小操作要好很多
  • 由于存在多个device,打印张量或进行张量格式转换时可能存在额外的传输开销
# PyTorch

x = torch.tensor([1, 2, 3])
x.device # 查看张量绑定的device

X = torch.ones(2, 3, device=try_gpu()) # 绑定device(优先GPU)
Z = X.cuda(1) # device切换:从GPU:0切换到GPU:1

net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu()) # 对神经网络指定GPU
# Tensorflow

x = tf.constant([1, 2, 3])
x.device # 查看张量绑定的device

with try_gpu():
    X = tf.ones((2, 3)) # 绑定device(优先GPU)
with try_gpu(1):
    Z = X # device切换:从GPU:0切换到GPU:1

# 对神经网络指定GPU;MirroredStrategy为tf自带的多GPU并行策略
strategy = tf.distribute.MirroredStrategy()
with strategy.scope():
    net = tf.keras.models.Sequential([
        tf.keras.layers.Dense(1)])

一个典型的错误如下:计算GPU上每个小批量的损失,并在命令行中将其报告给用户(或将其记录在NumPy ndarray中)时,将触发全局解释器锁,从而使所有GPU阻塞。最好是为GPU内部的日志分配内存,并且只移动较大的日志。

往年同期文章