一、线性回归
线性回归模型是输入一个特征的张量,做线性变换,输出一个预测张量
为了构造线性变换,需要知道输入特征维度大小,并且知道线性回归的权重和偏置,在forward方法中,输入一个特征张量x(大小为迷你批次×特征维度的大小),做线性变换(使用mm方法做矩阵乘法进行线性变换),加偏置的值,最后输出一个预测的值,并且一开始被初始化,使得每个分量为标准正态分布
另外,需要使用nn.Parameter来包装这些参数,使之成为子模块(仅仅由参数构成的子模块),这是因为在后续训练的时候需要对参数进行优化,只有将张量转化为参数之后才能在后续的优化过程中被优化器访问到
在使用线性回归模型之前,首先要做的就是模型的初始化,初始化的任务由初始化模型的类的实例开始
类的实例化和方法调用
对pytorch的模块,有一些常用的方法可以在训练和预测的时候用
1、调用 named_parameters 方法和 parameter 方法获取模型的参数
通过调用named_parameters 方法和 parameter 方法,返回的是python里面的一个生成器,通过访问生成器的对象得到的是该模型所有参数的名称和对应的张量值,通过调用 parameter 方法,返回的也是一个生成器,访问生成器的结果是该模型的所有参数对应的张量的值。pytorch的优化器直接接受模型生成参数作为函数的参数,并且会根据梯度来优化生成器里的所有张量(需要调用反向传播函数)
2、使用 train 和 eval 方法进行模型训练和测试状态转换
在模型的使用过程中,有些子模块(如丢弃层和批次归一化层等)有两种状态,即训练状态和预测状态,pytorch 的模型经常需要在两种状态中相互转换
通过调用 train 方法会把模块(包含所有的子模块)转换到训练状态,调用 eval 方法则会切换到预测状态,不同的模型预测结果也会有差异,在训练模型的时候要转化为训练状态,预测的时候要转化为预测状态,否则最后的预测模型的准确率可能会降低,甚至得到错误的结果
3、使用 named_buffers 方法或 buffers 方法获取张量的缓存
除通过反向传播得到梯度来进行训练的参数外,还有一些参数并不参与梯度传播,但是会在训练的时候得到更新,这种参数称为缓存,其中具体的例子包括批次归一化的平均值(Mean)和方差(Variance),通过在模块中调用 register_buffers 可以获取缓存的名字和缓存张量的值组成的生成器,通过buffers方法可以获取缓存张量值组成的生成器
4、使用 named_children 方法和 children 方法获取模型的子模块
需要对子模块进行迭代的时候,就需要使用 named_children 方法和 children 方法来获取子模块的名字、子模块的生成器,以及只有子模块的生成器
由于pytorch的模块的构造可以嵌套,所以子模块还有可能有自身的子模块,如果要获取模块中的所有模块的信息,可以使用 named_modules 和 modules 来递归的得到相关的信息
5、使用 apply 方法递归的对子模块进行函数应用
如果需要对pytorch所有的模块应用一个函数,可以使用 apply 方法,通过传入一个函数或者匿名函数(通过 python 的 lambda 关键字定义)来递归地应用这些函数,传入的函数以模块作为参考,在函数的内部对模块进行修改
6、改变模块参数数据类型和存储位置
除对模块进行修改之外,在深度学习模型的构建中还可对参数进行修改,和张量的运算一样,可以改变模块的参数所在的设备(CPU 或 GPU),具体可以通过调用模块自带的cpu方法和cuda方法来实现
另外,如果需要改变参数的数据类型,可以通过调用to方法加上需要转变的目标数据类型来实现(也可使用具体一些的方法:如 float 方法会转换所有的参数为双精度浮点数)
python
import torch
import torch.nn as nn
# 定义一个简单的模型
class SimpleModel(nn.Module):
def __init__(self):
super(SimpleModel, self).__init__()
self.linear = nn.Linear(10, 5)
def forward(self, x):
return self.linear(x)
# 实例化模型
model = SimpleModel()
# 检查模型当前所在的设备
print(next(model.parameters()).device) # 默认情况下,模型参数在CPU上
# 如果有可用的GPU,则将模型移到GPU上
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
# 再次检查模型参数所在的设备
print(next(model.parameters()).device) # 现在模型参数应该在GPU上(如果有的话)
二、计算图和自动求导机制
pytorch会根据计算过程来自动生成动态图,然后可以根据动态图的创建过程进行反向传播,计算得到每个节点的梯度值,为了能够记录张量的梯度,首先需要在创建张量的时候设置一个参数 requires_grad = True,意味着这个张量将会加入计算图,作为计算图的叶子结点参与运算,通过一系列的计算,最后输出结果张量,也就是根节点
几乎所有的张量的创建方式都可以使用参数 requires_grad = True,一但指定了这个参数,在后续的计算中得到的中间结果的张量都会被设置为 requires_grad = True,对pytorch来说,每个张量都有一个 grad_fn 方法,这个方法包含着创建盖章量的运算的导数信息,在反向传播的过程中,通过传入后一层的神经网络的梯度,该函数会计算出参与运算的所有张量的梯度,grad_fn 方法本身也携带有一个 next_function 属性,包含链接盖章量的其他张量的 grad_fn ,通过不断反向传播回溯中间张量的计算节点,可以得到所有张量的梯度,一个张量的梯度张量的信息保存在该张量的 grad 属性中
除了pytorch张量本身外,pytorch还提供了一个专门用于自动求导的包,即 torch.autograd ,它包含两个重要的参数,即 torch.autograd.backward 函数和 torch.autograd.grad 函数:torch.autograd.backward 函数通过传入根节点张量,以及初始梯度张量(形状和当前的张量相同),可以计算产生该节点所有对应的叶子结点的梯度
当张量为标量张量时(Scalar,即只有一个元素的张量),可以不传入初始梯度张量,默认会设置梯度张量为1,当计算梯度张量的时候,原先建立起来的计算图就会被自动释放,如需要再次做自动求导,因为计算图已经不存在,就会报错,如果要在反向传播的时候,保留计算图(反向传播也是一个计算过程,可以动态创建计算图)
需要反向传播的同时建立和梯度张量相关的计算图(在某些情况下,如需计算高阶导数)可以设置 create_graph = True ,对于一个可求导的张量,也可直接调用该张量内部的 backward 方法来进行自动求导
自动求导机制
python
import torch
import torch.nn as nn
# 定义一个3x3的张量
tensor = torch.randn(3, 3, requires_grad=True)
print("原始张量:")
print(tensor)
# 计算张量所有分量的平方和
sum_of_squares = torch.sum(tensor ** 2)
print("第一次计算所有分量的平方和:")
print(sum_of_squares)
# 反向传播,使梯度是原始张量的2倍
# 首先需要手动设置梯度为原始张量的2倍
sum_of_squares.backward(gradient=torch.ones_like(sum_of_squares) * 2)
print("梯度(原始张量的2倍):")
print(tensor.grad)
# 清零梯度,为下一步做准备
tensor.grad.zero_()
# 再次计算所有分量的平方和
# 因为tensor的值没有改变,所以结果应该和之前一样
sum_of_squares_again = torch.sum(tensor ** 2)
print("第二次计算所有分量的平方和:")
print(sum_of_squares_again)
# 步骤5: 再次反向传播,梯度累积
# 直接调用backward()即可,梯度会自动累积到tensor.grad中
sum_of_squares_again.backward()
print("梯度累积后的结果:")
print(tensor.grad)
需要注意的是:张量绑定的梯度张量在不清空的情况下会逐渐累积 ,这种特性在某些特殊的情况下是有用的,如需要一次性求很多迷你批次的累计梯度,但在一般的情况下,不需要用到这个特性,所以最后应当注意将梯度清零:单个张量的梯度清零的方法------tensor.grad.zero_() (模块和优化器都有清零参数张量梯度的函数)
梯度函数的使用
某些情况下,不需要求出当前的张量对所有产生该张量的叶子结点的梯度,这时可以使用 tensor.autograd.grad 函数,这个函数的参数是两个张量:第一个张量时计算图的数据结果张量(或张量列表),第二个是需要对计算图求导的张量(或张量列表),最后输出的结果是第一个张量随第二个张量求导的结果(注意最后输出的梯度会累积,和 tensor.grad.backward 函数的行为一样)
需要注意的是,这个函数不会改变叶子结点的 grad 属性,而不像 tensor.grad.backward 函数会在反向传播求导的时候释放计算图,如果需要保留计算图,同样可以设置 retain_graph = True,如果需要反向传播的计算图,可以设置 create_graph = True
另外,如果求导的两个张量在计算图上没有关联,函数会报错,不希望报错的话,可以设置 allow_unused = True 这个参数,结果会返回分量全为0的梯度张量(因为两个张量没有关联,所以求导的梯度为0)
python
import torch
import torch.nn as nn
import torch.autograd.function as Function
# 自定义一个PyTorch Function,用于实现前向传播和反向传播
class MyCustomFunction(Function):
@staticmethod
def forward(ctx, input):
# 前向传播:对输入执行一些操作,例如取平方
ctx.save_for_backward(input) # 保存输入,以便在反向传播中使用
output = input ** 2
return output
@staticmethod
def backward(ctx, grad_output):
# 反向传播:计算梯度
input, = ctx.saved_tensors # 取出之前保存的输入
grad_input = 2 * input * grad_output # 根据链式法则计算输入的梯度
return grad_input
# 使用自定义Function
def my_custom_op(input):
return MyCustomFunction.apply(input)
# 创建一个需要计算梯度的张量
x = torch.tensor([2.0, 3.0], requires_grad=True)
# 执行前向传播
y = my_custom_op(x)
# 计算y关于x的梯度
y.backward()
# 输出结果和梯度
print("y:", y)
print("x.grad:", x.grad) # 这将输出x的梯度,它是2 * x,因为我们对x取了平方
计算图的构建和使用
由于计算图的构建需要消耗内存和计算资源,在一些情况下,计算图并不是必要的,比如神经网络的推导,此时可以使用 torch.no_grad 上下文管理器,在这个上下文管理器的作用于进行的神经网络计算不会构建任何的计算图
另外,再对一个张量进行反向传播的时候可能不需要让梯度通过这个张量的节点,也就是新建的计算图要和原来的计算图分理,这种情况可以使用张量的 detach 方法,通过调用这个函数方法,可以返回一个新的张量,该张量会成为一个新的计算图的叶子结点,新的计算图和老的计算图互相分离,互不影响
python
import torch
import torch.nn as nn
import torch.optim as optim
# 定义一个简单的神经网络
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
self.fc = nn.Linear(10, 1)
def forward(self, x):
return self.fc(x)
# 初始化网络和优化器
model = SimpleNet()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 创建一些虚拟数据
inputs = torch.randn(16, 10)
targets = torch.randn(16, 1)
# 训练阶段:需要计算梯度
model.train()
optimizer.zero_grad() # 清零梯度缓存
outputs = model(inputs) # 前向传播
loss = nn.MSELoss()(outputs, targets) # 计算损失
loss.backward() # 反向传播,计算梯度
optimizer.step() # 根据梯度更新权重
# 评估阶段:不需要计算梯度
model.eval()
with torch.no_grad():
# 使用相同的输入数据进行评估
outputs_eval = model(inputs)
# 注意:这里不会计算梯度,所以不会更新模型权重
# 可以使用outputs_eval进行预测或计算评估指标
print("Training loss:", loss.item())
print("Evaluation outputs:", outputs_eval)
三、损失函数和优化器
损失函数
pytorch的损失函数有两种形式:函数形式和模块形式,前者调用的是 torch.nn.functional 库中的函数,通过传入神经网络预测值和目标值来计算损失函数,后者是 torch.nn 库里面的模块,通过新建一个模块的示例,然后调用模块来计算最终的损失函数
由于训练数据一般以迷你批次的形式输入神经网络,最后预测的值也是以迷你批次的形式输出的,而损失函数最后的输出结果应当是一个标量张量,因此对于迷你批次的归约一般有两种方法:一是对迷你批次的损失函数求和,二是对迷你批次的损失函数求平均,一般来说,最后输出的损失函数是迷你批次损失函数的平均
神经网络处理的预测问题分为回归问题和分类问题两种,对于回归问题,一般情况是使用的 torch.nn.MSELoss 模块,即前面介绍的平方损失函数函数,通过创建这个模块的示例(一般使用默认参数,即在累的构造函数中不传入任何的参数,这样会输出损失函数对迷你批次的平均,如果要输出迷你批次的每个损失函数,可以指定参数 reduction='none',如果要输出迷你批次的损失函数的和,可以指定参数 reduction='sum' )在实例中传入神经网络预测的值和目标值,能够计算最后的损失函数
python
import torch
import torch.nn as nn
import torch.nn.functional as F
# 假设我们有一个简单的神经网络模型
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
self.fc = nn.Linear(10, 1) # 假设输入特征为10,输出为1的线性层
def forward(self, x):
return self.fc(x)
# 实例化模型
model = SimpleNet()
# 假设我们有一个迷你批次的数据
batch_size = 16
num_features = 10
# 随机生成预测值和目标值
predictions = torch.randn(batch_size, 1) # 神经网络输出的预测值
targets = torch.randn(batch_size, 1) # 真实的目标值
# 使用函数形式计算损失
mse_loss_func = F.mse_loss
loss_func = mse_loss_func(predictions, targets, reduction='mean') # 默认是求平均
print(f"Loss using functional form (mean): {loss_func.item()}")
# 使用模块形式计算损失
mse_loss_module = nn.MSELoss(reduction='mean') # 创建一个MSELoss模块的实例,默认是求平均
loss_module = mse_loss_module(predictions, targets)
print(f"Loss using module form (mean): {loss_module.item()}")
# 如果你想输出迷你批次的每个损失函数值,而不是平均值或总和
mse_loss_module_none = nn.MSELoss(reduction='none')
loss_per_element = mse_loss_module_none(predictions, targets)
print(f"Loss per element: {loss_per_element}")
# 如果你想输出迷你批次的损失函数的总和
mse_loss_module_sum = nn.MSELoss(reduction='sum')
loss_sum = mse_loss_module_sum(predictions, targets)
print(f"Loss sum: {loss_sum.item()}")
除回归问题之外,关于分类问题,pytorch也有和回归问题使用方法类似损失函数,如果是二分类问题用到的交叉熵损失函数,可以使用 torch.nn.BCELoss 模块实现,同样,在初始化这个模块的时候可以使用默认参数,输出所有损失函数的平均,该模块一般接受的是Sigmoid函数的输出
另一个经常用到的函数是对数(Logits)交叉熵损失函数 torch.nn.BCEWithLogitsLoss,这个函数和前面的函数的区别在于,可以直接省略Sigmoid函数的计算部分,不用计算概率,该损失函数会自动在损失函数内部的实现部分添加Sigmoid激活函数,当训练的时候使用这个损失函数可以增加计算的数值的稳定性,因为当概率接近0或者概率接近1的时候,二分交叉熵函数的对数部分会很容易接近无情打,这样会造成树脂的不稳定,通过在损失函数中加入Sigmoid函数并针对此函数简化计算损失函数,能够有效的避免这两种情况下的数值不稳定
和二分类问题类似,在多分类的情况下,也可以使用两个模块,第一个模块是 torch.nn.NLLLoss,即负对数似然函数,这个损失函数的运算过程是根据预测值(经过Softmax的计算和对数计算)和目标值(使用独热编码)计算这两个值按照元素一一对应的成绩,然后成绩求和,并取负值
因此在使用这个损失函数之前必须先计算Softmax函数取对数的结果,pytorch有一个函数 torch.nn.function.log_softmax可以实现这个目的,第二个模块是 torch.nn.CrossEntryLoss,用于构建目标损失函数,这个损失函数可以避免 LogSoftmax的计算,在损失函数里整合Softmax输出概率,以及对概率取对数输出损失函数