【Scala PyTorch深度学习】PyTorch On Scala 系列课程 第二章 05 :张量广播机制【Ai Infra 3.0】[PyTorch Scala 硕士研一课程]

PyTorch Scala 高校计算机硕士研一课程
理解广播机制
当对张量执行逐元素操作(如加法、减法或乘法)时,它们的形状通常需要对齐。但是,手动调整或重复张量以匹配形状可能会很繁琐且效率低下,尤其是在处理大型数据集时。PyTorch 通过一种称为**广播(broadcasting)**的机制解决了这个问题。
广播提供了一套规则,允许 PyTorch 在执行操作时自动扩展张量维度,前提是它们的形状满足特定的兼容标准。这在许多常见情况下省去了显式维度扩展的需要,使得代码更简洁,内存使用更优化,因为实际数据并未重复;只有计算行为像数据重复了一样。
广播规则
PyTorch 通过逐元素比较两个张量的形状来判断它们是否“可广播”,比较从末尾(最右侧)维度开始。如果满足以下条件,则两个张量可兼容进行广播(从右到左比较每个维度对):
- 维度相等: 维度大小相等。
- 其中一个维度为 1: 两个维度中的一个为 1。
- 缺少维度: 一个张量不具备该维度(在此比较中,其大小被视为 1)。
如果所有维度对都满足这些条件,则张量是可广播的。结果张量的形状将沿每个维度对取最大尺寸。如果任何维度对不满足条件(即,维度不同且都不为 1),则会引发 RuntimeError。
我们来分析一下这个过程:
- 对齐形状: 张量根据它们的末尾维度进行对齐。如果一个张量的维度少于另一个,那么为了对齐,会在其形状前面添加大小为 1 的维度。
- 检查兼容性并确定结果形状: 从最右侧维度开始,比较尺寸:
- 如果维度相等,则结果维度大小就是该尺寸。
- 如果一个维度为 1,则结果维度大小是另一个(较大)维度的大小。
- 如果一个张量缺少某个维度(由于对齐),则结果维度大小是另一个张量中该维度的大小。
- 执行操作: 操作的执行方式,就像是沿着给定维度大小为 1 的张量,其值被复制以匹配另一个张量中对应维度的大小一样。
广播示例
我们用代码示例来说明。
标量与张量
将标量(一个 0 维张量)添加到任何张量时,总是通过广播机制生效。标量会有效地扩展以匹配张量的形状。
import torch.*
// 张量 A: 形状 [2, 3]
val a = torch.tensor([[1, 2, 3], [4, 5, 6]])
// 标量 B: 形状 [] (0 维度)
val b = torch.tensor(10)
// 将标量添加到张量
val c = a + b
println(f"Shape of a: {a.shape}")
// 张量 a 的形状: torch.Size([2, 3])
println(f"Shape of b: {b.shape}")
// 标量 b 的形状: torch.Size([])
println(f"Shape of c: {c.shape}")
// 张量 c 的形状: torch.Size([2, 3])
println(f"Result c:\n{c}")
// 结果 c:
// tensor([[11, 12, 13],
// [14, 15, 16]])
这里,b(形状 [])被广播到形状 [2, 3] 以匹配 a。
行向量与矩阵
考虑将一个行向量(形状 [3])添加到一个矩阵(形状 [2, 3])中。
import torch.*
// 张量 A: 形状 [2, 3]
val a = torch.tensor([[1, 2, 3],
[4, 5, 6]])
// 张量 B: 形状 [3] (为了广播,可以视为 [1, 3])
val b = torch.tensor([10, 20, 30])
// 将行向量添加到矩阵
val c = a + b
println(f"Shape of a: {a.shape}") // torch.Size([2, 3])
println(f"Shape of b: {b.shape}") // torch.Size([3])
println(f"Shape of c: {c.shape}") // torch.Size([2, 3])
println(f"Result c:\n{c}")
// 结果 c:
// tensor([[11, 22, 33],
// [14, 25, 36]])
-
对齐:
a的形状为[2, 3]。b的形状为[3]。右侧对齐结果如下:张量 A: 2 x 3 张量 B: 3 -
兼容性检查:
- 末尾维度:
3等于3。兼容。结果维度大小为3。 - 下一个维度:
a为2,b在此处没有维度(隐式大小为1)。兼容。结果维度大小为2。
- 末尾维度:
-
结果形状:
[2, 3]。 -
扩展: 张量
b被视为形状[1, 3],并且其单行沿第一个维度复制以匹配a的形状[2, 3]。
列向量与矩阵
现在,我们来将一个列向量(形状 [2, 1])添加至同一矩阵(形状 [2, 3])。
import torch.*
// 张量 A: 形状 [2, 3]
val a = torch.tensor(Seq(Seq(1, 2, 3),
Seq(4, 5, 6)))
// 张量 B: 形状 [2, 1]
val b = torch.tensor(Seq(Seq(10), Seq(20)))
// 将列向量添加到矩阵
val c = a + b
println(f"Shape of a: {a.shape}") // torch.Size([2, 3])
println(f"Shape of b: {b.shape}") // torch.Size([2, 1])
println(f"Shape of c: {c.shape}") // torch.Size([2, 3])
println(f"Result c:\n{c}")
// 结果 c:
// tensor([[11, 12, 13],
// [24, 25, 26]])
-
对齐:
张量 A: 2 x 3 张量 B: 2 x 1 -
兼容性检查:
- 末尾维度:
a为3,b为1。兼容(其中一个为 1)。结果维度大小为3。 - 下一个维度:
a为2,b为2。兼容(相等)。结果维度大小为2。
- 末尾维度:
-
结果形状:
[2, 3]。 -
扩展: 张量
b中大小为1的维度(列维度)通过跨列复制值来扩展,以匹配a的形状[2, 3]。
可视化示例
我们来可视化张量 A(形状 [3, 1])和 B(形状 [4])的广播过程。
张量 A (形状: [3, 1])张量 B (形状: [4])广播 A + B -> 结果 (形状: [3, 4])A1A2A3B1B2B3B4A1, A1, A1, A1A2, A2, A2, A2A3, A3, A3, A3扩展维度 1(大小 1 -> 4)B1, B2, B3, B4B1, B2, B3, B4B1, B2, B3, B4添加维度 0 并扩展(大小 [4] -> [1,4] -> [3,4])A1+B1, A1+B2, A1+B3, A1+B4A2+B1, A2+B2, A2+B3, A2+B4A3+B1, A3+B2, A3+B3, A3+B4++
张量 A (形状 [3, 1]) 和张量 B (形状 [4]) 进行广播加法的示意图。张量 A 的第二个维度(大小 1)扩展到 4。张量 B 获得一个大小为 1 的前置维度(变为形状 [1, 4]),然后扩展到大小 3。两者都有效地变为形状 [3, 4] 以进行逐元素加法。
不兼容的形状
如果非匹配维度不为 1,则广播会失败。
import torch.*
// 张量 A: 形状 [2, 3]
val a = torch.tensor(Seq(Seq(1, 2, 3),
Seq(4, 5, 6)))
// 张量 B: 形状 [2]
val b = torch.tensor(Seq(10, 20))
try:
val c = a + b
except Exception as e:
println(f"Error: {e}")
// 错误: 张量 a (3) 的大小必须与张量 b (2) 在非单例维度 1 处匹配
-
对齐:
张量 A: 2 x 3 张量 B: 2 -
兼容性检查:
- 末尾维度:
a为3,b为2。两者都不为 1。不兼容。 操作失败。
- 末尾维度:
常见用途
广播在神经网络中经常使用:
- 添加偏置: 将偏置向量(形状
[output_features])添加到线性层的输出(形状[batch_size, output_features])。 - 归一化: 从一批数据中减去均值(标量或按特征向量)并除以标准差(标量或按特征向量)。
- 应用掩码: 将数据与可能具有较少维度的布尔掩码进行逐元素乘法。
理解广播对于编写简洁高效的 PyTorch 代码非常重要。它允许你自然地对不同形状的张量执行操作,只要它们遵守兼容性规则,从而简化了许多常见的数据处理和建模任务。
张量数据类型
张量具有数据类型,通常称为 dtype。数据类型决定了张量可以存储的数值种类(例如整数或浮点数)以及每个元素占用多少内存。选择合适的数据类型对于管理计算资源和确保深度学习模型中的数值精度非常重要。
PyTorch 支持多种数值数据类型,与 NumPy 中的类似。每种类型都有不同的用途,平衡了内存使用、计算速度以及可表示数字的范围或精度。
理解 dtype
每个张量都有一个 dtype 属性,用于指定其元素的类型。默认情况下,PyTorch 创建浮点张量时使用 torch.float32,整数张量时使用 torch.int64。你可以这样检查张量的数据类型:
import torch.*
// 默认浮点张量
val a = torch.tensor(Seq(1.0, 2.0, 3.0))
println(f"Tensor a: {a}")
println(f"dtype of a: {a.dtype}")
// 默认整数张量
val b = torch.tensor(Seq(1, 2, 3))
println(f"\nTensor b: {b}")
println(f"dtype of b: {b.dtype}")
输出:
Tensor a: tensor([1., 2., 3.])
dtype of a: torch.float32
Tensor b: tensor([1, 2, 3])
dtype of b: torch.int64
在创建张量时,你也可以明确指定 dtype:
// 创建一个64位浮点数张量
val c = torch.tensor(Seq(1.0, 2.0), dtype=torch.float64)
println(f"\nTensor c: {c}")
println(f"dtype of c: {c.dtype}")
// 创建一个32位整数张量
val d = torch.ones(2, 2, dtype=torch.int32)
println(f"\nTensor d:\n{d}")
println(f"dtype of d: {d.dtype}")
输出:
Tensor c: tensor([1., 2.], dtype=torch.float64)
dtype of c: torch.float64
Tensor d:
tensor([[1, 1],
[1, 1]], dtype=torch.int32)
dtype of d: torch.int32
你可以使用 torch.get_default_dtype() 查看 PyTorch 默认使用的浮点类型。
常用数据类型
以下是 PyTorch 中一些最常用的数据类型:
- 浮点类型:
torch.float32(或torch.float):标准的32位单精度浮点数。由于它在CPU和GPU上兼顾了精度和性能,因此是模型参数和一般计算中最常见的类型。torch.float64(或torch.double):64位双精度浮点数。提供更高的精度,但占用两倍内存,并且速度可能明显较慢,尤其是在未针对双精度进行优化的GPU上。当绝对需要高数值精度时使用它。torch.float16(或torch.half):16位半精度浮点数。占用更少内存,并可在现代GPU(如NVIDIA Tensor Cores)上显著加快计算速度。然而,其有限的范围和精度有时可能导致数值不稳定(溢出或下溢)。常用于混合精度训练。torch.bfloat16:一种替代的16位格式(脑浮点)。它与float32具有相似的范围,但精度较低。它正变得越来越受兼容硬件(例如,较新的NVIDIA GPU、Google TPU)上深度学习训练的欢迎,因为它提供了内存节省和速度提升,同时通常比float16保持更好的稳定性。
- 整数类型:
torch.int64(或torch.long):64位有符号整数。默认整数类型。常用于张量索引和分类任务中表示类别标签。torch.int32(或torch.int):32位有符号整数。torch.int16:16位有符号整数。torch.int8:8位有符号整数。较小的整数类型可以节省内存,并对某些操作更快,常用于模型量化。- 相应的无符号整数类型也存在 (
torch.uint8)。
- 布尔类型:
torch.bool:表示布尔值True或False。对于逻辑操作、使用掩码进行索引以及条件逻辑非常重要。
类型转换
你经常需要将张量从一种数据类型转换为另一种。这就是所谓的类型转换。转换张量的主要方式是使用 .to() 方法,该方法我们在张量在设备(CPU/GPU)之间移动的背景下也见过。
import torch.*
// 原始浮点张量
val float_tensor = torch.tensor(Seq(1.1, 2.2, 3.3), dtype=torch.float32)
println(f"Original tensor: {float_tensor}, dtype: {float_tensor.dtype}")
// 使用 .to() 转换为 int64
val int_tensor = float_tensor.to(torch.int64)
println(f"Casted to int64: {int_tensor}, dtype: {int_tensor.dtype}") // 注意截断
// 使用 .to() 转换回 float16
val half_tensor = int_tensor.to(dtype=torch.float16) // 可以只指定 dtype
println(f"Casted to float16: {half_tensor}, dtype: {half_tensor.dtype}")
输出:
Original tensor: tensor([1.1000, 2.2000, 3.3000]), dtype: torch.float32
Casted to int64: tensor([1, 2, 3]), dtype: torch.int64
Casted to float16: tensor([1., 2., 3.], dtype=torch.float16), dtype: torch.float16
注意,从浮点数转换为整数会截断小数部分。
PyTorch 还提供了便捷方法来进行常见的类型转换:
import torch.*
// 原始整数张量
val tensor_a = torch.tensor(Seq(0, 1, 0, 1))
println(f"Original tensor: {tensor_a}, dtype: {tensor_a.dtype}")
// 使用 .float() 转换为浮点数
val tensor_b = tensor_a.float() // 等同于 .to(torch.float32)
println(f".float(): {tensor_b}, dtype: {tensor_b.dtype}")
// 使用 .long() 转换为长整型
val tensor_c = tensor_b.long() // 等同于 .to(torch.int64)
println(f".long(): {tensor_c}, dtype: {tensor_c.dtype}")
// 使用 .bool() 转换为布尔型
val tensor_d = tensor_a.bool() // 等同于 .to(torch.bool)
println(f".bool(): {tensor_d}, dtype: {tensor_d.dtype}")
输出:
.float(): tensor([0., 1., 0., 1.]), dtype: torch.float32
.long(): tensor([0, 1, 0, 1]), dtype: torch.int64
.bool(): tensor([False, True, False, True]), dtype: torch.bool
请记住,类型转换通常会在内存中创建一个具有指定数据类型的新张量,而不是就地修改原始张量。
操作中的类型提升
当你对不同数据类型的张量执行操作时,PyTorch 通常会自动提升类型以确保兼容性。一般规则是,整数类型与浮点类型进行操作时,结果将是浮点类型。不同浮点类型之间的操作通常会得到更高精度的类型。
val int_t = torch.tensor(Seq(1, 2), dtype=torch.int32)
val float_t = torch.tensor(Seq(0.5, 0.5), dtype=torch.float32)
val double_t = torch.tensor(Seq(0.1, 0.1), dtype=torch.float64)
// int32 + float32 -> float32
val result1 = int_t + float_t
println(f"\nint32 + float32 = {result1}, dtype: {result1.dtype}")
// float32 + float64 -> float64
val result2 = float_t + double_t
println(f"float32 + float64 = {result2}, dtype: {result2.dtype}")
输出:
int32 + float32 = tensor([1.5000, 2.5000]), dtype: torch.float32
float32 + float64 = tensor([0.6000, 0.6000], dtype=torch.float64), dtype: torch.float64
虽然方便,但要注意自动类型提升,因为它如果未被预料到,可能会导致意外结果或性能影响。使用 .to() 进行显式类型转换可以让你对计算中所需的数据类型有更清晰的控制。
理解和管理张量数据类型是高效 PyTorch 编程的重要组成部分。它能让你控制内存占用,运用硬件加速(如GPU上的FP16),并为你的特定深度学习任务保持必要的数值精度。
CPU 与 GPU 张量
深度学习计算,特别是涉及大型张量和复杂模型的计算,需要强大的计算能力。中央处理器(CPU)用途广泛,而图形处理器(GPU)提供大规模并行能力,可以大幅加速构成神经网络核心的矩阵和向量运算。PyTorch 提供简单直接的机制来管理张量的存放位置和计算发生地。了解如何在 CPU 和 GPU 之间移动张量是高效模型训练和推理的必备技能。
CPU:默认计算中心
默认情况下,当您创建 PyTorch 张量时未指定设备,它会分配在 CPU 上。
import torch.*
// 默认在 CPU 上创建张量
val cpu_tensor = torch.tensor(Seq(1.0, 2.0, 3.0))
println(f"默认张量设备:{cpu_tensor.device}")
CPU 完全适用于许多任务,包括预处理步骤、较小规模的计算,或在没有兼容 GPU 时运行模型。然而,对于训练大型深度学习模型,由于 CPU 的顺序处理特性与 GPU 的并行架构相比,仅依赖 CPU 常常会导致训练时间过长,难以承受。
GPU:使用并行加速深度学习
GPU 包含数百或数千个核心,旨在同时执行大量计算。这种架构非常适合深度学习中常见的运算类型,如大型矩阵乘法和卷积。PyTorch 通过 CUDA(统一计算设备架构)平台使用 NVIDIA GPU。
要使用 GPU,您需要:
- 一个兼容 CUDA 的 NVIDIA GPU。
- 已安装适当的 CUDA 工具包。
- 安装了 CUDA 支持的 PyTorch 版本。
检查 GPU 可用性并设置设备
在尝试使用 GPU 之前,最好检查它是否可用且已为 PyTorch 正确配置。torch.cuda.is_available() 函数在 PyTorch 可以访问支持 CUDA 的 GPU 时返回 True。
之后我们可以创建一个 torch.device 对象来表示我们的目标计算设备(CPU 或 GPU)。这使得代码具有适应性,如果 GPU 可用则自动使用 GPU,否则回退到 CPU。
import torch.*
// 检查 CUDA 可用性并相应设置设备
if torch.cuda.is_available() then
val device = torch.device("cuda") // 使用第一个可用的 CUDA 设备
println(f"CUDA (GPU) 可用。使用设备:{device}")
// 您也可以指定特定 GPU,例如 torch.device("cuda:0")
else:
val device = torch.device("cpu")
println(f"CUDA (GPU) 不可用。使用设备:{device}")
// device 现在包含 torch.device('cuda') 或 torch.device('cpu')
直接在设备上创建张量
您可以在张量创建期间使用 device 参数直接指定目标设备。这通常比在 CPU 上创建然后再移动更高效。
// 直接在选定设备上创建张量
try:
// 如果 device='cpu',此张量将在 CPU 上;如果 device='cuda',则在 GPU 上
val device_tensor = torch.randn(3, 4, device=device)
println(f"张量创建于:{device_tensor.device}")
catch RuntimeError as e:
println(f"无法直接在 {device} 上创建张量:{e}") // 处理未找到 GPU 等情况
在 CPU 和 GPU 之间移动张量
通常,您需要在设备之间传输现有张量。例如,从磁盘加载的数据通常位于 CPU 上,但您的模型可能在 GPU 上以加快计算。移动张量的主要方法是 .to() 方法。
.to() 方法接受 torch.device 对象、设备字符串(例如 'cuda'、'cpu'),甚至另一个张量(在这种情况下,张量会移动到与参数张量相同的设备上)作为输入。它在指定设备上返回一个 新 张量。原始张量在其原始设备上保持不变。
// 从 CPU 张量开始
val cpu_tensor = torch.ones(2, 2)
println(f"原始张量:{cpu_tensor.device}")
// 将张量移动到选定设备(如果 GPU 可用,则为 GPU;否则为 CPU)
// 请记住,'device' 是根据可用性预先设置的
val moved_tensor = cpu_tensor.to(device)
println(f"移动后的张量:{moved_tensor.device}")
// 如果张量在 GPU 上,则显式移回 CPU
if moved_tensor.is_cuda then // 检查张量是否在 CUDA 设备上
val back_to_cpu = moved_tensor.to("cpu")
println(f"张量移回至:{back_to_cpu.device}")
PyTorch 还提供便利方法:.cpu() 和 .cuda()。它们分别是 .to('cpu') 和 .to('cuda:0')(或当前默认的 CUDA 设备)的简写。
// 使用便利方法(假设 GPU 可用且 'device' 为 'cuda')
if device.type == 'cuda' then
// 将 cpu_tensor 移动到 GPU
val gpu_tensor_alt = cpu_tensor.cuda()
println(f"使用 .cuda():{gpu_tensor_alt.device}")
// 将 gpu_tensor_alt 移回 CPU
val cpu_tensor_alt = gpu_tensor_alt.cpu()
println(f"使用 .cpu():{cpu_tensor_alt.device}")
设备管理的重要考量
-
设备一致性: 涉及多个张量的操作(例如,加法、矩阵乘法)通常要求所有参与的张量都在 同一 设备上。尝试在 CPU 张量和 GPU 张量之间进行操作将导致
RuntimeError。在执行操作前,请确保您的数据和模型位于同一设备上。// 错误示例(假设 device='cuda') val cpu_a = torch.randn(2, 2) val gpu_b = torch.randn(2, 2, device=device) try: // 如果 device 是 'cuda',这很可能会失败 val c = cpu_a + gpu_b catch RuntimeError as e: println(f"在不同设备上执行操作时出错:{e}") -
数据传输开销: 在 CPU 和 GPU 内存之间移动数据并非瞬间完成。虽然 GPU 计算速度快,但如果管理不当,数据的来回传输可能成为瓶颈。为了最佳性能,请尝试在 GPU 上尽可能多地执行操作,然后在需要时(例如,保存到磁盘或转换为 NumPy)再将最终结果移回 CPU。
-
模型放置: 与张量一样,使用
torch.nn.Module定义的神经网络模型也需要使用.to(device)方法移动到适当的设备。这确保模型的参数(本身也是张量)位于目标设备上进行计算。在讨论模型构建时,会更详细地介绍这一点。
掌握使用 device 和 .to() 方法进行张量放置是发挥 GPU 计算能力和编写高效、硬件感知型 PyTorch 代码不可或缺的要素。请记住始终检查设备一致性并注意数据传输成本。
练习:张量操作技巧
练习张量操作技术,包括索引、形状改变、组合、广播、数据类型以及张量在不同设备间的移动。通过这些示例进行练习是巩固理解的最佳途径。请确保已导入PyTorch。
import torch.*
import numpy as np
// 检查CUDA是否可用并设置设备
val device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
println(f"正在使用设备: {device}")
索引和切片练习
索引和切片是访问和修改张量部分内容的基本操作。让我们尝试选择特定的数据点。
任务1: 创建一个二维张量,并选择第二行第三列的元素。
// 创建一个示例二维张量(3行,4列)
val data = Seq(Seq(1, 2, 3, 4), Seq(5, 6, 7, 8), Seq(9, 10, 11, 12))
val tensor_2d = torch.tensor(data)
println("原始张量:\n", tensor_2d)
// 选择行索引为1、列索引为2的元素
val element = tensor_2d(1, 2)
println("\n在 [1, 2] 的元素:", element)
println("值:", element.item()) // 使用 .item() 获取Scala数值
任务2: 选择 tensor_2d 的整个第二行。
// 选择索引为1的行
val row_1 = tensor_2d(1)
println("\n第二行(索引1):\n", row_1)
// 使用切片的替代方法(选择第1行,所有列)
val row_1_alt = tensor_2d(1, ::)
println("\n第二行(替代方法):\n", row_1_alt)
任务3: 选择 tensor_2d 的第三列。
// 选择所有行,列索引为2
val col_2 = tensor_2d(::, 2)
println("\n第三列(索引2):\n", col_2)
任务4: 创建一个布尔掩码以选择 tensor_2d 中所有大于7的元素,然后使用该掩码提取这些元素。
// 创建布尔掩码
val mask = tensor_2d > 7
println("\n布尔掩码(张量 > 7):\n", mask)
// 应用掩码
val selected_elements = tensor_2d(mask)
println("\n大于7的元素:\n", selected_elements)
这些练习说明了标准Python索引如何结合类似NumPy的切片和布尔掩码,以提供灵活的数据访问方式。
形状改变与重排练习
在不改变张量数据的情况下改变其形状是很常见的,尤其是在为不同神经网络层准备输入时。
任务1: 创建一个包含12个元素的一维张量,并将其形状改为3x4的张量。
// 创建一个值从0到11的张量
val tensor_1d = torch.arange(12)
println("\n原始一维张量:", tensor_1d)
// 使用 reshape() 改变形状
val reshaped_tensor = tensor_1d.reshape(3, 4)
println("\n改变形状为3x4:\n", reshaped_tensor)
// 使用 view() 改变形状 - 注意 view 适用于内存连续的张量
// arange 创建的是连续张量,因此 view 在这里适用。
val view_tensor = tensor_1d.view(3, 4)
println("\n视为3x4:\n", view_tensor)
请记住,view 要求张量在内存中是连续的,并共享底层数据。reshape 可能会返回一个副本或视图,具体取决于连续性。
任务2: 给定 reshaped_tensor(3x4),使用 permute 交换其维度,得到一个4x3的张量。
// 原始3x4张量
println("\n原始3x4张量:\n", reshaped_tensor)
// 交换维度0和1
val permuted_tensor = reshaped_tensor.permute(1, 0)
println("\n置换为4x3:\n", permuted_tensor)
println("原始形状:", reshaped_tensor.shape)
println("置换后形状:", permuted_tensor.shape)
permute 在改变图像维度顺序等任务中有用(例如,从通道数 x 高度 x 宽度 变为 高度 x 宽度 x 通道数)。
连接与拆分练习
合并或拆分张量通常是必要的,尤其是在处理批次或不同特征集时。
任务1: 创建两个2x3张量,并沿维度0(行)连接它们。
// 创建两个2x3张量
val tensor_a = torch.tensor(Seq(Seq(1, 2, 3), Seq(4, 5, 6)))
val tensor_b = torch.tensor(Seq(Seq(7, 8, 9), Seq(10, 11, 12)))
println("\n张量A:\n", tensor_a)
println("张量B:\n", tensor_b)
// 沿维度0连接(堆叠行)
val concatenated_rows = torch.cat((tensor_a, tensor_b), dim=0)
println("\n沿行连接(dim=0):\n", concatenated_rows)
println("形状:", concatenated_rows.shape) // 应该是 4x3
任务2: 沿维度1(列)连接 tensor_a 和 tensor_b。
// 沿维度1连接(连接列)
val concatenated_cols = torch.cat((tensor_a, tensor_b), dim=1)
println("\n沿列连接(dim=1):\n", concatenated_cols)
println("形状:", concatenated_cols.shape) // 应该是 2x6
任务3: 使用 stack 组合 tensor_a 和 tensor_b,形成一个形状为2x2x3的新张量。
// 堆叠张量 - 创建一个新维度(默认 dim=0)
val stacked_tensor = torch.stack((tensor_a, tensor_b), dim=0)
println("\n堆叠的张量(dim=0):\n", stacked_tensor)
println("形状:", stacked_tensor.shape) // 应该是 2x2x3
// 沿维度1堆叠
val stacked_tensor_dim1 = torch.stack((tensor_a, tensor_b), dim=1)
println("\n堆叠的张量(dim=1):\n", stacked_tensor_dim1)
println("形状:", stacked_tensor_dim1.shape) // 应该是 2x2x3
请注意 stack 如何添加一个新维度,而 cat 沿着现有维度连接。
任务4: 创建一个6x4张量,并沿维度0将其拆分为三个等大小的部分。
// 创建一个值从0到23的张量
val tensor_to_split = torch.arange(24).reshape(6, 4)
println("\n待拆分张量(6x4):\n", tensor_to_split)
// 沿维度0拆分为3个部分
val chunks = torch.chunk(tensor_to_split, chunks=3, dim=0)
println("\n拆分为3个部分:")
for (i, chunk) <- chunks.zipWithIndex {
println(f"部分 {i}(形状 ${chunk.shape}):\n", chunk)
}
广播练习
广播简化了不同形状张量之间的操作。
任务1: 创建一个3x3张量和一个1x3张量(行向量)。将它们相加。
// 创建一个3x3张量和一个1x3张量(行向量)
val matrix = torch.tensor(Seq(Seq(1, 2, 3), Seq(4, 5, 6), Seq(7, 8, 9)))
val row_vector = torch.tensor(Seq(Seq(10, 20, 30))) // 形状 1x3
println("\n矩阵(3x3):\n", matrix)
println("行向量(1x3):\n", row_vector)
// 广播加法: row_vector 被扩展以匹配 matrix 的形状
val result = matrix + row_vector
println("\n矩阵 + 行向量(广播):\n", result)
PyTorch 自动扩展了 row_vector(形状1x3)的行,使其形状变为3x3,从而允许与 matrix 进行逐元素相加。
任务2: 创建一个3x3张量和一个3x1张量(列向量)。将它们相加。
// 创建一个3x3张量和一个3x1张量(列向量)
val matrix = torch.tensor(Seq(Seq(1, 2, 3), Seq(4, 5, 6), Seq(7, 8, 9)))
val col_vector = torch.tensor(Seq(Seq(100), Seq(200), Seq(300))) // 形状 3x1
println("\n矩阵(3x3):\n", matrix)
println("列向量(3x1):\n", col_vector)
// 广播加法: col_vector 被扩展以匹配 matrix 的形状
val result_col = matrix + col_vector
println("\n矩阵 + 列向量(广播):\n", result_col)
这里,col_vector(形状3x1)被广播到各列,以匹配 matrix 的3x3形状。
数据类型练习
管理数据类型对于内存效率和数值稳定性很重要。
任务1: 创建一个整数张量并检查其 dtype。然后将其转换为浮点张量。
// 创建一个整数张量
val int_tensor = torch.tensor(Seq(1, 2, 3, 4))
println("\n整数张量:", int_tensor)
println("数据类型:", int_tensor.dtype)
// 转换为 float32
val float_tensor = int_tensor.to(torch.float32)
// 替代方法: float_tensor = int_tensor.float()
println("\n转换为浮点张量:", float_tensor)
println("数据类型:", float_tensor.dtype)
任务2: 创建一个浮点张量并将其转换为整数张量。观察任何变化。
// 创建一个浮点张量
val float_tensor_orig = torch.tensor(Seq(1.1, 2.7, 3.5, 4.9))
println("\n原始浮点张量:", float_tensor_orig)
println("数据类型:", float_tensor_orig.dtype)
// 转换为 int32
val int_tensor_cast = float_tensor_orig.to(torch.int32)
// 替代方法: int_tensor_cast = float_tensor_orig.int()
println("\n转换为整数张量:", int_tensor_cast)
println("数据类型:", int_tensor_cast.dtype)
请注意,从浮点数转换为整数会截断小数部分。请注意可能的数据精度损失。
CPU 与 GPU 练习
将张量移动到合适的设备(CPU或GPU)是必要的,以发挥硬件加速的优势。
任务1: 创建一个张量并检查其默认设备。然后,将其移动到GPU(如果可用),再移回CPU。
// 创建张量(除非另有说明,否则默认为CPU)
val cpu_tensor = torch.randn(2, 2)
println(f"\n张量在CPU上: {cpu_tensor.device}\n", cpu_tensor)
// 移动到配置的设备(如果可用则为GPU,否则为CPU)
val device_tensor = cpu_tensor.to(device)
println(f"\n张量已移动到 {device_tensor.device}:\n", device_tensor)
// 明确移回CPU
val cpu_tensor_again = device_tensor.to("cpu")
println(f"\n张量已移回CPU: {cpu_tensor_again.device}\n", cpu_tensor_again)
// 执行操作 - 需要张量在同一设备上
if device_tensor.device != cpu_tensor.device then
println("\n在不同设备上的张量相加会导致错误。")
// 这会失败: cpu_tensor + device_tensor
// 正确方法:
val result_on_device = device_tensor + device_tensor
println(f"在 {result_on_device.device} 上的操作结果:\n", result_on_device)
else
println("\n两个张量都在CPU上,相加没问题。")
val result_on_cpu = cpu_tensor + cpu_tensor_again
println(f"在 {result_on_cpu.device} 上的操作结果:\n", result_on_cpu)
请记住,张量之间的操作通常要求它们在同一设备上进行。使用 .to(device) 显式移动张量是 PyTorch 代码中常见的做法,尤其是在为GPU训练准备数据和模型时。
本次实践涵盖了操作张量的主要技巧。随着您构建更复杂的模型,熟练掌握索引、形状改变、组合张量、理解广播、管理数据类型以及控制设备放置将变得愈发重要。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)