pytorch
pytorch基础使用和底层原理
基础使用
pytorch是一个开源的深度学习框架,提供了灵活的张量操作和自动求导机制,广泛应用于计算机视觉、自然语言处理等领域。重点是针对张量进行操作。在pytorch语法中,会发现许多函数有类似的名字,只是差了一个下划线,比如add和add_,前者是返回一个新的张量,而后者是原地操作,会修改原始张量的值。
张量操作
- 张量创建:可以使用
torch.tensor()、torch.zeros()、torch.ones()等函数创建张量。1 2 3 4
zeros = torch.zeros(3, 4) # 3x4全零张量 ones = torch.ones(2,3) # 2x3全一张量 range_tensor = torch.range(0,9) # [0,1,2,...,9] identity = torch.eye(3) # 3x3单位矩阵
- 内存布局:PyTorch中的张量可以是连续的(contiguous)或非连续的(non-contiguous)。连续张量在内存中以行优先(row-major)或列优先(column-major)的方式存储,而非连续张量则可能由于转置、切片等操作而导致内存布局不连续。 Stride 定义了:为了在某个维度上移动到下一个元素,需要在内存中跳过多少个逻辑位置。 shape 定义了:每个维度的大小。
1 2 3
x = torch.tensor([[1, 2, 3], [4, 5, 6]]) print(x.shape) # 输出: torch.Size([2, 3]) print(x.stride()) # 输出: (3, 1)
contiguous()函数:将张量转换为内存中连续存储的格式,返回一个新的张量。
1 2 3 4 5
x = torch.tensor([[1, 2, 3], [4, 5, 6]]) y = x.t() # 转置操作 print(y.is_contiguous()) # 输出: False z = y.contiguous() # 转换为连续存储格式 print(z.is_contiguous()) # 输出: True
为什么转置后就不连续了:因为转置操作并不会挪动数据的位置,而是改变了张量的视图,比如上面的例子,他会将stride从(3,1)变成(1,3),这一位这在最低维度移动一次,在内存中要移动3个位置。
- 广播机制:当进行张量操作时,如果两个张量的形状不完全相同,但满足某些条件,PyTorch会自动进行广播,使得它们具有相同的形状。
1 2 3 4
a = torch.tensor([1, 2, 3]) # 形状为 (3,) b = torch.tensor([[4], [5], [6]]) # 形状为 (3, 1) c = a + b # 广播机制使得a和b具有相同的形状 (3, 3) print(c) # 输出: tensor([[5, 6, 7], [6, 7, 8], [7, 8, 9]])
- 高级索引:PyTorch支持多种高级索引方式,包括布尔索引、整数数组索引等。
1 2 3 4 5 6 7 8 9 10 11 12 13
x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 布尔索引 mask = x > 5 print(x[mask]) # 输出: tensor([6, 7, 8, 9]) # 整数数组索引 indices = torch.tensor([0, 2]) print(x[indices]) # 输出: tensor([[1, 2, 3], [7, 8, 9]]) # 任务3: 使用where,小于等于5的变成0,大于5的保持原值 # TODO: 你的代码 conditional = torch.where(x>5,x,0) # 提示: torch.where(condition, x, 0) print(conditional) # 输出: tensor([[0, 0, 0], [0, 0, 6], [7, 8, 9]])
- view and reshape:用于改变张量的形状,view要求张量是连续的,而reshape则不要求。 ```python “””
- view: 要求张量必须是contiguous的,否则报错
- reshape: 会自动处理non-contiguous的情况(可能复制数据)
- contiguous: 返回一个连续内存的张量副本
任务:
- 创建一个张量并转置(变成non-contiguous)
- 尝试用view重塑(会失败)
- 用reshape重塑(会成功)
- 先contiguous再view(会成功) “”” x = torch.arange(6).reshape(2, 3) x_t = x.t() # 转置后变成 non-contiguous
# 任务1: 检查x_t是否contiguous # TODO: 你的代码 is_contig = x_t.is_contiguous() # 应该是 False
# 任务2: 尝试用view,应该会失败 view_success = False try: # TODO: 尝试 x_t.view(6) x_t.view(6) # YOUR CODE HERE view_success = True except RuntimeError: view_success = False
# 任务3: 用reshape(会成功) # TODO: 你的代码 reshaped = x_t.reshape(6) # x_t.reshape(6)
# 任务4: 先contiguous再view # TODO: 你的代码 viewed = x_t.contiguous().view(6) # x_t.contiguous().view(6) ```
自动求导
PyTorch提供了自动求导机制,通过torch.autograd模块实现。
- 基本梯度计算: 通过设置
requires_grad=True来跟踪张量的计算历史,调用backward()方法计算梯度。1 2 3 4 5
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) y = x * 2 + 1 z = y.sum() z.backward() print(x.grad) # 输出: tensor([2., 2., 2.])
在设置requires_grad=True后,PyTorch会自动跟踪对该张量的所有操作,在调用backward()时,PyTorch会自动根据因变量进行链式法则求导,计算出自变量的梯度,并存储在自变量.grad属性中。
- 梯度累积:默认情况下,每次调用backward()时,计算的梯度会累积到.grad属性中,而不是覆盖之前的值。每次backward会将新梯度加到已有梯度上。 这就是为什么训练循环中需要 optimizer.zero_grad()
1 2 3 4 5 6 7 8
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) y1 = x * 2 y2 = x * 3 y1.backward(torch.ones_like(x)) # 计算y1的梯度 print(x.grad) # 输出: tensor([2., 2., 2.]) y2.backward(torch.ones_like(x)) # 计算y2的梯度,累积到x.grad中 print(x.grad) # 输出: tensor([5., 5., 5.]) (2 + 3)
为了保留在某一时刻的梯度,可以使用x.grad.clone(),为了清零梯度,可以使用x.grad.zero_()或者optimizer.zero_grad()。
- detach 和 no_grad: detach()方法可以创建一个新的张量,该张量与原始张量共享数据但不跟踪计算历史。no_grad上下文管理器可以在其作用范围内禁止梯度计算。
1 2 3 4 5 6 7 8 9 10 11 12
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) y = x * 2 z = y.sum() # 使用detach创建一个新的张量,不跟踪计算历史 y_detached = y.detach() print(y_detached.requires_grad) # 输出: False # 在no_grad上下文中进行操作,不计算梯度 with torch.no_grad(): y_no_grad = x * 3 print(y_no_grad.requires_grad) # 输出: False
- 计算图和反向传播:PyTorch会动态构建计算图,只有在调用backward()时才会进行反向传播计算梯度。
1 2 3 4 5 6 7 8 9 10 11 12
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) y = x * 2 + 1 z = y.sum() # 计算图在这里被构建 print(z.grad_fn) # 输出: <SumBackward0 object at ...> z.backward(retain_graph=True) # 反向传播计算梯度 print(x.grad) # 输出: tensor([2., 2., 2.]) z.grad.zero_() # 清零z的梯度 z.backward() # 再次反向传播计算梯度 print(x.grad) # 输出: tensor([2., 2., 2.]) (因为z的梯度被清零了,所以x.grad没有累积)
如果不添加retain_graph=True,第一次调用backward()后计算图会被释放,第二次调用backward()时会报错,因为计算图已经不存在了。
- 梯度钩子:可以使用register_hook()方法在张量上注册一个钩子函数,在每次计算梯度时被调用。
1 2 3 4 5 6 7 8 9 10 11 12
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) # 定义一个钩子函数,打印梯度 def print_grad(grad): print("Gradient:", grad) # 在x上注册钩子函数 x.register_hook(print_grad) y = x * 2 + 1 z = y.sum() z.backward() # 每次计算x的梯度时,都会调用print_grad函数
模型构建
PyTorch提供了torch.nn模块用于构建神经网络模型。
- 定义模型:可以通过继承
nn.Module类来定义一个神经网络模型,并在__init__方法中定义层,在forward方法中定义前向传播逻辑。1 2 3 4 5 6 7 8
import torch.nn as nn class SimpleNN(nn.Module): def __init__(self,input_size, output_size): super.__init__() self.weights=nn.Parameter(torch.randn(input_size, output_size)) self.bias=nn.Parameter(torch.zeros(output_size)) def forward(self, x): return x @ self.weights + self.bias
- Parameter vs Buffer paramerter是 模型中需要学习的参数,通常是权重和偏置等,可以通过优化器进行更新。buffer是模型中不需要学习的参数,通常是一些统计量或者状态信息,不会被优化器更新。
1 2 3 4 5
class MyModel(nn.Module): def __init__(self): super().__init__() self.weight = nn.Parameter(torch.randn(10, 10)) # 这是一个可学习的参数 self.register_buffer('running_mean', torch.zeros(10)) # 这是一个缓冲区,不可学习
- 训练与评估模式 PyTorch中的模型有两种模式:训练模式(train())和评估模式(eval())。在训练模式下,模型会启用一些特定于训练的行为,如dropout和batch normalization等。而在评估模式下,这些行为会被禁用,以确保模型在推理时的稳定性。
1 2 3 4 5 6
model = SimpleNN(10, 5) model.train() # 切换到训练模式 output_train = model(torch.randn(1, 10)) # 在训练模式下的输出 model.eval() # 切换到评估模式 output_eval = model(torch.randn(1, 10)) # 在评估模式下的输出
- 模型保存与加载 PyTorch提供了
torch.save()和torch.load()函数用于保存和加载模型。有完整保存模型与只保存state_dict两种方式,推荐使用state_dict方式,因为它更灵活且不依赖于模型的定义。1 2 3 4 5 6 7 8
model = SimpleNN(10, 5) # 保存模型的state_dict torch.save(model.state_dict(), 'model_state.pth') # 加载模型的state_dict loaded_model = SimpleNN(10, 5) loaded_model.load_state_dict(torch.load('model_state.pth'))
state_dict是一个字典对象,包含了模型的所有参数和缓冲区的名称和对应的张量值。通过保存和加载state_dict,可以在不同的环境中重现模型的训练结果,而不需要依赖于模型的定义。
数据加载
PyTorch提供了torch.utils.data模块用于数据加载和预处理。
- Dataset和DataLoader:可以通过继承
torch.utils.data.Dataset类来定义一个数据集,并使用torch.utils.data.DataLoader来加载数据。1 2 3 4 5 6 7 8 9 10 11 12
from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, data): self.data = data def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx] dataset = MyDataset([1, 2, 3, 4, 5]) dataloader = DataLoader(dataset, batch_size=2, shuffle=True) for batch in dataloader: print(batch) # 输出: tensor([2, 5]) (每次输出可能不同,因为shuffle=True)
其中,dataset是一个自定义的数据集类,必须实现
__len__和__getitem__方法。DataLoader则负责将数据集分成小批量,并提供迭代器接口来加载数据。 - collate_fn 用于定义如何将一个batch的数据样本合并成一个mini-batch的函数。默认情况下,DataLoader会将一个batch的数据样本合并成一个张量,但有时候我们需要自定义合并方式,比如处理变长序列或者不同类型的数据。
1 2 3 4 5 6 7
def my_collate_fn(batch): # 假设每个样本是一个字典,包含'input'和'label'两个键 inputs = torch.stack([item['input'] for item in batch]) labels = torch.tensor([item['label'] for item in batch]) return inputs, labels dataloader = DataLoader(dataset, batch_size=2, shuffle=True, collate_fn=my_collate_fn)
- sampler 用于定义如何从数据集中采样数据的策略。默认情况下,DataLoader会使用一个随机采样器(RandomSampler)来随机打乱数据,但有时候我们需要自定义采样策略,比如按照类别平衡采样或者按照权重采样。
1 2 3 4 5
from torch.utils.data import WeightedRandomSampler # 假设我们有一个数据集,其中每个样本的类别分布不平衡,我们可以为每个样本分配一个权重,来实现按照权 重采样 weights = [0.1, 0.2, 0.3, 0.4] # 每个样本的权重 sampler = WeightedRandomSampler(weights, num_samples=4, replacement=True) dataloader = DataLoader(dataset, batch_size=2, sampler=sampler)
在这个例子中,我们使用了WeightedRandomSampler来根据指定的权重进行采样,num_samples参数指定了每个epoch中采样的样本数量,replacement=True表示允许重复采样。