深度学习(一):基础知识

深度学习(一):基础知识

参考书为:https://zh.d2l.ai/chapter_preface/index.html

基础

Python

  1. list前加*号的作用:将该list转化为一个个分散的值
  2. enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。

pytorch工具函数

  1. softmax运算
nn.functional.softmax(input_tensor, dim=xxx)

沿着dim做softmax函数,输出的张量形状和输入相同。

  1. repeat_interleave函数
torch.repeat_interleave(input, repeats, dim=None) → Tensor

将input重复,其中repeats指定了每个元素的重复次数。默认情况下,先将input展平为一维张量,然后重复。

  1. torch.bmm函数
torch.bmm(input, mat2, *, out=None) → Tensor

输入张量input和mat2必须是3维张量,如果inputs的形状为\(b\times n\times m\),mat2的形状为\(b\times m\times p\),其中b是batch_size维,真正会相乘的矩阵是后面那两维,即输出结果是\(b\times n\times p\)。该函数实现小批量样本的乘法。

  1. unsqueeze函数
torch.unsqueeze(input, dim) → Tensor

将一个大小为1的维度插入到input的特定位置上。其中dim参数表示插入维度的位置。

  1. transpose函数
torch.transpose(input, dim0, dim1) → Tensor

该函数是一个将张量转置的函数,要转置的维度分别为dim0和dim1。

  1. one_hot函数
torch.nn.functional.one_hot(tensor, num_classes=- 1) → LongTensor

将输入形状为(*)的张量变为(*, num_classes)形状的张量并输出。输出的张量除了位置为元素的值所在处为1,其余都是0.

张量

  • 张量的创建
import torch
x = torch.arange(12)
X = x.reshape(3, 4) #等价于x.reshape(-1,4)
torch.zeros((2, 3, 4))
torch.ones((2, 3, 4))
torch.randn(3, 4)# 均值为0,标准差为1
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
  • 张量的连接
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0) #按行连接,即最后行数会翻倍
torch.cat((X, Y), dim=1) #按列连接,即将两个tensor横着拼在一起,列数会翻倍
  • 广播机制:复制某些元素,满足相应的操作

  • 索引和切片

    X[0:2, :] = 12 #第0到第1行,:表示按列的所有元素
    
  • 避免内存泄漏

    Z[:] = X + Y和 X+=Y

数据处理

读取数据使用pandas。

import pandas as pd

data = pd.read_csv(data_file)
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2] #分别表示data的前两列和最后一列
inputs = inputs.fillna(inputs.mean())

#将类别行转化为one-hot编码
inputs = pd.get_dummies(inputs, dummy_na=True)
#将其转化为张量
X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)

线性代数

单个向量的默认方向是列向量

axis=0表示沿着轴0

沿某个轴计算A元素的累积总和:

A.cumsum(axis=0)

矩阵按元素乘:A*A

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

  • 范数:描述张量的大小

L1范数:向量元素的绝对值之和:torch.abs(u).sum()

L2范数:向量元素的平方的和的平方根:torch.norm(u)。\(||x||\)一般表示L2范数

微积分

  • 梯度

image-20230224165816932

  • 链式法则

image-20230224170204354

自动微分

根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。

  • 单独计算批量中每个样本的偏导数之和:
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
  • 分离计算

有时,我们希望将某些计算移动到记录的计算图之外。 例如,假设y是作为x的函数计算的,而z则是作为yx的函数计算的。 想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数, 并且只考虑到xy被计算后发挥的作用。

这里可以分离y来返回一个新变量u,该变量与y具有相同的值, 但丢弃计算图中如何计算y的任何信息。 换句话说,梯度不会向后流经ux。 因此,下面的反向传播函数计算z=u*x关于x的偏导数,同时将u作为常数处理, 而不是z=x*x*x关于x的偏导数。

x.grad.zero_()
y = x * x
u = y.detach()
z = u * x

z.sum().backward()
x.grad == u

总结:深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。

线性神经网络

线性回归

我们要求一组权重和偏置使得损失函数最小。

损失函数

i表示样本i

image-20230224185720422 image-20230224185440355

梯度下降

通过不断地在损失函数递减的方向上更新参数来降低误差。

  • 小批量随机梯度下降:

步骤:(1)初始化模型参数的值,如随机初始化; (2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。

image-20230224190035604

\(|B|\)表示每个小批量中的样本数,这也称为批量大小(batch size)。\(\eta\)表示学习率(learning rate)。这些可以调整但不在训练过程中更新的参数称为超参数(hyperparameter)。 调参(hyperparameter tuning)是选择超参数的过程。 超参数通常是我们根据训练迭代结果来调整的, 而训练迭代结果是在独立的验证数据集(validation dataset)上评估得到的。

def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

训练过程

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)  # X和y的小批量损失
        # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
        # 并以此计算关于[w,b]的梯度
        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}')

使用Pytorch

  • 读取数据集:

    def load_array(data_arrays, batch_size, is_train=True):  #@save
        """构造一个PyTorch数据迭代器"""
        dataset = data.TensorDataset(*data_arrays)
        return data.DataLoader(dataset, batch_size, shuffle=is_train)
    
    batch_size = 10
    data_iter = load_array((features, labels), batch_size)#features为特征,即X;labels即Y
    
  • 模型定义:

    # nn是神经网络的缩写
    from torch import nn
    
    net = nn.Sequential(nn.Linear(2, 1))
    

    nn.Linear为全连接层,2表示输入的个数,1表示输出的个数。

  • 初始化模型参数

    net[0].weight.data.normal_(0, 0.01)
    net[0].bias.data.fill_(0)
    
  • 损失函数

    loss = nn.MSELoss()#均方误差,即平方L2范数
    
  • 定义优化算法

    trainer = torch.optim.SGD(net.parameters(), lr=0.03)
    
  • 训练

    num_epochs = 3
    for epoch in range(num_epochs):
        for X, y in data_iter:
            l = loss(net(X) ,y) #计算损失
            trainer.zero_grad() #清除前一次batch的梯度
            l.backward()		#对损失函数求梯度
            trainer.step()		#调用优化器来更新模型参数
        l = loss(net(features), labels)
        print(f'epoch {epoch + 1}, loss {l:f}')
    

softmax回归

解决分类问题。

输出为:

image-20230224223251695

损失函数为,也称为交叉熵损失:

image-20230224223418300

Softmax导数的计算:

image-20230224223607619

使用Pytorch

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

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
  • 初始化模型参数:
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);
  • 交叉熵损失函数
loss = nn.CrossEntropyLoss(reduction='none')
  • 优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
  • 训练:
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

多层感知机

在网络中加入隐藏层可以克服线性模型的限制, 使其能处理更普遍的函数关系类型。要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。这种架构称为多层感知机(MLP)

image-20230225081625680

image-20230225081923901

多层感知机在输出层和输入层之间增加一个或多个全连接隐藏层,并通过激活函数转换隐藏层的输出。

激活函数

ReLu

最受欢迎的激活函数是修正线性单元,给定元素x,有:

\[ReLU(x) = max(0, x) \]

常用于隐藏层

Sigmoid函数

Sigmoid常称为挤压函数,将定义域为R的输入映射到(0,1)。

\[sigmoid(x) = \frac{1}{1+e^{-x}} \]

sigmoid在隐藏层中已经较少使用。

image-20230225082750579

tanh

tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。

\[tanh(x)=\frac{1-e^{-2x}}{1+e^{-2x}} \]

image-20230225082955969

从零开始实现

  • 初始化模型参数
num_inputs, num_outputs, num_hiddens = 784, 10, 256#隐藏单元常设为2的幂次

W1 = nn.Parameter(torch.randn(
    num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
    num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]
  • 激活函数
def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)
  • 模型
def net(X):
    X = X.reshape((-1, num_inputs))
    H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法
    return (H@W2 + b2)
  • 损失函数
loss = nn.CrossEntropyLoss(reduction='none')
  • 训练
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
  • 评估
d2l.predict_ch3(net, test_iter)

Pytorch实现

  • 模型定义
net = nn.Sequential(nn.Flatten(),		#展平,从第1维(第0维是样本数那一维)到最后一维展平张量
                    nn.Linear(784, 256),#隐藏层
                    nn.ReLU(),
                    nn.Linear(256, 10)) #输出层

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

权重衰减

为了缓解过拟合的情况,即模型过于复杂,可以采用正则化的方式,其中最广泛使用的方法是权重衰减,也被称为L2正则化。即将权重\(w\)的范数\(||w||\)作为惩罚项加到最小化损失的问题中,可以得到损失函数变为:

\[L(w,b)+\frac{\lambda}{2}||w||^2 \]

则小批量随机梯度下降算法对应的更新步骤为:

image-20230225093001725

通常,网络输出层的偏置项不会被正则化。

  • Pytorch框架版

为了实现权重衰减,可以在优化器中增加相关参数,下面的例子只为权重赋予了正则化:

trainer = torch.optim.SGD([
        {"params":net[0].weight,'weight_decay': wd},#wd表示lambda
        {"params":net[0].bias}], lr=lr)

暂退法

DropOut方法是为了提高模型的稳健性,使其不过分依赖任何一个参数。它的做法是随机将几个隐藏层的单元省去,并且不计算它们的梯度。但测试时一般不删去,只在训练时删去。

image-20230225094117768

暂退法对每个h施加作用,使其变为h':

image-20230225094400749

暂退法的代码实现:

def dropout_layer(X, dropout): #dropout为概率p
    assert 0 <= dropout <= 1
    # 在本情况中,所有元素都被丢弃
    if dropout == 1:
        return torch.zeros_like(X)
    # 在本情况中,所有元素都被保留
    if dropout == 0:
        return X
    mask = (torch.rand(X.shape) > dropout).float()
    return mask * X / (1.0 - dropout)
  • 模型定义,一遍靠近输入层的地方设置较低的暂退率:
dropout1, dropout2 = 0.2, 0.5

class Net(nn.Module):
    def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
                 is_training = True):
        super(Net, self).__init__()
        self.num_inputs = num_inputs
        self.training = is_training
        self.lin1 = nn.Linear(num_inputs, num_hiddens1)
        self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
        self.lin3 = nn.Linear(num_hiddens2, num_outputs)
        self.relu = nn.ReLU()

    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
        # 只有在训练模型时才使用dropout
        if self.training == True:
            # 在第一个全连接层之后添加一个dropout层
            H1 = dropout_layer(H1, dropout1)
        H2 = self.relu(self.lin2(H1))
        if self.training == True:
            # 在第二个全连接层之后添加一个dropout层
            H2 = dropout_layer(H2, dropout2)
        out = self.lin3(H2)
        return out


net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
  • Pytorch简洁实现:
net = nn.Sequential(nn.Flatten(),
        nn.Linear(784, 256),
        nn.ReLU(),
        # 在第一个全连接层之后添加一个dropout层
        nn.Dropout(dropout1),
        nn.Linear(256, 256),
        nn.ReLU(),
        # 在第二个全连接层之后添加一个dropout层
        nn.Dropout(dropout2),
        nn.Linear(256, 10))

nn.Dropout层在训练时,Dropout层将根据指定的暂退概率随机丢弃上一层的输出(相当于下一层的输入)。 在测试时,Dropout层仅传递数据。

前向传播、后向传播、计算图

前向传播(forward propagation或forward pass) 指的是:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。

反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法。 简言之,该方法根据微积分中的链式规则(链式法则就是按相反的顺序从因变量到自变量求导数),按相反的顺序从输出层到输入层遍历网络。 该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。

深度学习计算

块是层的集合,可以表示模型的一部分或者整个模型。通过一个类来表示块,例如我们可以定义一个MLP的块:

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)

注意,除非我们实现一个新的运算符, 否则我们不必担心反向传播函数或参数初始化, 系统将自动生成这些。

参数管理

  • 访问参数

    net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
    print(net[2].state_dict())
    

    每个参数都表示为参数类的一个实例,包含梯度、值和额外信息:

    print(type(net[2].bias))
    print(net[2].bias)
    print(net[2].bias.data)
    

    结果:

    <class 'torch.nn.parameter.Parameter'>
    Parameter containing:
    tensor([-0.0291], requires_grad=True)
    tensor([-0.0291])
    

    目前还没有调用反向传播,所以参数梯度处于初始状态:net[2].weight.grad == None

  • 参数初始化

默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。

  1. 高斯正态分布初始化:
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)
  1. 初始化为常数
nn.init.constant_(m.weight, 1)
  1. 也可以直接设置参数
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42

延后初始化

有时输入特征数未知时,可以采用延后初始化:

net = nn.Sequential(nn.LazyLinear(256), nn.ReLU(), nn.LazyLinear(10))#只定义了linear层的out_features的大小
X = torch.rand(2, 20)
net(X)#当执行前向传播时,参数就被初始化了
print(net)
# 输出结果
Sequential(
  (0): Linear(in_features=20, out_features=256, bias=True)
  (1): ReLU()
  (2): Linear(in_features=256, out_features=10, bias=True)
)

延迟初始化非常方便,允许框架自动推断参数形状,使修改体系结构变得容易,并消除了一个常见的错误来源。我们可以通过模型传递数据,使框架最终初始化参数。

自定义层

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)

读写文件

  1. 加载和保存张量
x = torch.arange(4)
torch.save(x, 'x-file') #将x保存在当前文件夹的x-file中
x2 = torch.load('x-file')
# 用列表保存
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')
  1. 加载和保存模型参数

将net的参数保存:

net = MLP()
torch.save(net.state_dict(), 'mlp.params')

将net的参数传入到模型中:

clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()

GPU

这两个函数允许我们在不存在所需所有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')]

默认情况下,张量是在CPU上创建的。下面将张量创建在GPU上:

X = torch.ones(2, 3, device=try_gpu()) # X在GPU0 上

如果我们要将不同GPU上的数据相加时,需要先复制到同一个GPU上,再相加:

Z = X.cuda(1) #Z是在GPU 1上的,值和X相同

神经网络和GPU

神经网络模型可以指定设备。 下面的代码将模型参数放在GPU上。

net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())

只要所有的数据和参数都在同一个设备上, 我们就可以有效地学习模型。

posted @ 2023-02-25 15:56  KouweiLee  阅读(46)  评论(0编辑  收藏  举报