【Python】学习PyTorch

PyTorch从零开始:深度学习的全面探索与实践

第一部分:PyTorch 基础 - 构建深度学习的基石

第1章:PyTorch 导论与环境搭建

1.1 什么是 PyTorch?为什么选择 PyTorch?

PyTorch 是一个由 Facebook 人工智能研究院 (FAIR) 主导开发的开源机器学习库,广泛应用于计算机视觉、自然语言处理、强化学习等多个领域。它基于 Torch 库,但专为 Python 设计,提供了强大的张量计算(类似 NumPy 但支持 GPU 加速)和灵活的深度神经网络构建能力。

为什么选择 PyTorch?

  1. Pythonic 与易用性 (Pythonic and Ease of Use):

    • PyTorch 的 API 设计简洁直观,与 Python 的编程风格高度契合,使得熟悉 Python 的开发者能够快速上手。
    • 它的学习曲线相对平缓,尤其是对于初学者和研究人员而言,可以更专注于模型设计和实验。
  2. 动态计算图 (Dynamic Computational Graphs - Define-by-Run):

    • 这是 PyTorch 最显著的特点之一。与 TensorFlow 早期版本(TensorFlow 1.x)的静态计算图(Define-and-Run)不同,PyTorch 的计算图是在运行时动态构建的。
    • 优势:
      • 灵活性: 使得处理动态输入(如可变长度的序列)和实现复杂的、条件控制的或递归的神经网络结构(如 RNNs、Tree-LSTMs)变得非常自然和简单。
      • 易于调试: 由于图是动态构建的,你可以使用标准的 Python 调试工具(如 pdb 或 IDE 的调试器)在执行过程中逐行检查张量的值、图的结构等,这极大地简化了调试过程。
    • 例如,一个循环神经网络的展开长度可以根据每个批次中输入序列的最大长度动态确定,而不需要预先定义固定的图结构。
  3. 强大的 GPU 加速 (Powerful GPU Acceleration):

    • PyTorch 提供了无缝的 CUDA 支持,可以将计算密集型的张量运算和神经网络训练轻松地转移到 NVIDIA GPU 上执行,从而获得数十倍甚至数百倍的性能提升。
    • 管理 GPU 资源和在 CPU 与 GPU 之间移动数据非常方便。
  4. 丰富的生态系统和社区支持 (Rich Ecosystem and Community Support):

    • PyTorch 拥有一个庞大且活跃的开发者社区,贡献了大量的预训练模型、工具库、教程和研究论文实现。
    • 核心库: torchvision (计算机视觉), torchaudio (音频处理), torchtext (文本处理) 提供了丰富的数据集、模型架构和预处理工具。
    • 第三方库: Hugging Face Transformers (NLP 领域的SOTA模型库), PyTorch Lightning (高级封装,简化训练流程), Fastai (简化深度学习实践) 等极大地扩展了 PyTorch 的能力。
    • 大量的研究论文都优先使用 PyTorch 实现,使得复现最新研究成果更为便捷。
  5. 从研究到生产的平滑过渡 (Smooth Transition from Research to Production):

    • 虽然 PyTorch 最初以其在研究领域的灵活性著称,但它在生产部署方面也取得了长足进步。
    • TorchScript: 允许将 PyTorch 模型转换为一种可以在高性能 C++ 环境中运行的静态图表示,便于部署到不依赖 Python 的环境中。
    • TorchServe: PyTorch 官方的模型服务工具,用于大规模部署 PyTorch 模型。
    • ONNX (Open Neural Network Exchange) 支持: 可以将 PyTorch 模型导出为 ONNX 格式,从而在多种不同框架和硬件上运行。
    • 移动端支持 (PyTorch Mobile)。
  6. 命令式编程体验 (Imperative Programming Experience):

    • PyTorch 的操作方式更接近传统的命令式编程,代码的执行顺序与书写顺序一致,这使得理解和构建模型逻辑更加直观。

PyTorch 与其他框架的简要对比 (以 TensorFlow 为例):

  • 计算图:
    • PyTorch: 动态图 (Define-by-Run)。更灵活,易调试。
    • TensorFlow (2.x Eager Execution 默认为动态图, 1.x 为静态图): TensorFlow 2.x 默认采用 Eager Execution,体验上接近 PyTorch 的动态图。但仍可通过 tf.function 装饰器将其转换为静态图以优化性能和部署。TensorFlow 1.x 主要依赖静态图。
  • API 设计:
    • PyTorch: 被认为更 “Pythonic”,API 更加简洁和面向对象。
    • TensorFlow: API 较为全面,但有时可能显得更复杂。Keras 作为其高级 API 极大地简化了模型构建。
  • 调试:
    • PyTorch: 由于动态图特性,可以使用标准 Python 调试器。
    • TensorFlow (1.x): 静态图调试相对困难,通常需要 tf.print 或 TensorFlow Debugger (tfdbg)。TensorFlow 2.x Eager Execution 模式下调试体验改善。
  • 部署:
    • PyTorch: TorchScript, TorchServe, ONNX。
    • TensorFlow: TensorFlow Serving, TensorFlow Lite (移动和嵌入式), TensorFlow.js (浏览器), ONNX。TensorFlow 在部署方面历史更悠久,工具链更成熟一些,但 PyTorch 正在快速追赶。
  • 社区与生态:
    • 两者都有庞大且活跃的社区。PyTorch 在学术研究领域增长迅速,而 TensorFlow 在工业界有广泛应用。但这种界限正在逐渐模糊。

总而言之,PyTorch 因其灵活性、易用性和强大的社区支持,已成为深度学习研究和应用的重要工具。

1.2 PyTorch 的核心理念:张量 (Tensors) 与自动求导 (Autograd)

PyTorch 的核心可以概括为两个主要组件:

  1. 张量 (Tensors):

    • 张量是 PyTorch 中最基本的数据结构,可以理解为一个多维数组。它与 NumPy 的 ndarray 非常相似,但关键区别在于 PyTorch 张量可以在 GPU 上进行计算以加速运算。
    • 张量可以表示标量(0维张量)、向量(1维张量)、矩阵(2维张量)以及更高维的数据结构。
    • 深度学习模型的所有输入、输出以及模型的参数(权重和偏置)都是以张量的形式存在的。
    • PyTorch 提供了丰富的张量操作函数,涵盖了数学运算、逻辑运算、线性代数、傅里叶变换等。
  2. 自动求导 (Autograd - torch.autograd):

    • torch.autograd 是 PyTorch 实现神经网络自动微分的核心引擎。它为张量上的所有操作构建一个动态计算图 (DCG)。
    • 当张量的 requires_grad 属性设置为 True 时,PyTorch 会开始跟踪在该张量上执行的所有操作。
    • 完成前向传播计算后,通过在标量输出(通常是损失函数的值)上调用 .backward() 方法,Autograd 会自动计算图中所有 requires_grad=True 的叶子节点张量(通常是模型参数)相对于该标量输出的梯度。
    • 这些梯度存储在对应张量的 .grad 属性中。
    • 这个机制使得我们无需手动推导复杂的梯度公式,极大地简化了神经网络的训练过程。

我们将在后续章节中深入探讨这两个核心组件。

1.3 安装 PyTorch 与配置开发环境

安装 PyTorch 通常非常直接,官方网站 (pytorch.org) 提供了便捷的安装指令生成器。

步骤概览:

  1. 访问 PyTorch 官网: https://pytorch.org/get-started/locally/

  2. 选择你的配置:

    • PyTorch Build: 通常选择 Stable (稳定版) 或 LTS (长期支持版)。Preview (Nightly) 是最新的开发版,可能包含新特性但也可能不稳定。
    • Your OS: 选择你的操作系统 (Linux, Mac, Windows)。
    • Package:
      • Conda: 推荐使用 Conda 进行包管理和环境隔离,它可以更好地处理复杂的依赖关系,尤其是 CUDA 工具包。
      • Pip: 如果你习惯使用 pip,也可以选择它。
      • LibTorch: C++ 发行版,用于在 C++ 环境中运行 PyTorch 模型。
      • Source: 从源代码编译安装,适用于高级用户或需要特定编译选项的场景。
    • Language: 通常是 Python
    • Compute Platform (CUDA 版本或 CPU):
      • CPU: 如果你没有 NVIDIA GPU 或者只是想初步学习,可以选择 CPU
      • CUDA: 如果你有兼容的 NVIDIA GPU 并希望利用 GPU 加速,需要选择与你已安装的 NVIDIA驱动 和 CUDA Toolkit 版本相匹配的 CUDA 版本 (例如 CUDA 11.8, CUDA 12.1)。
        • 检查 NVIDIA 驱动版本: 在终端运行 nvidia-smi
        • 检查已安装的 CUDA Toolkit 版本 (如果已安装): 在终端运行 nvcc --version。如果未安装,PyTorch 的 Conda 安装通常会自动安装一个兼容的 CUDA Toolkit 运行时。
        • 注意: PyTorch 安装包中包含的 CUDA 运行时是其自身运行所需的,并不等同于系统级完整的 CUDA Toolkit (后者用于编译 CUDA 代码)。通常,你只需要保证 NVIDIA 驱动程序与 PyTorch 指定的 CUDA 版本兼容即可。
  3. 复制并运行安装命令: 官网会根据你的选择生成一条安装命令。

使用 Conda 安装 (推荐):

首先,确保你已经安装了 Anaconda 或 Miniconda。

  • 创建一个新的 Conda 环境 (推荐):

    conda create -n pytorch_env python=3.9  # 创建名为 pytorch_env 的环境,使用 Python 3.9
    conda activate pytorch_env             # 激活环境
    

    pytorch_env 替换为你想要的环境名称,python=3.9 可以替换为你需要的 Python 版本 (如 3.8, 3.10, 3.11)。

  • 根据官网生成的命令安装 PyTorch:
    例如,安装支持 CUDA 11.8 的稳定版 PyTorch (命令可能随官网更新而变化):

    # (在激活的 pytorch_env 环境中执行)
    # 示例命令,请务必从官网获取最新命令
    conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia
    
    • -c pytorch: 指定从 PyTorch 的 Conda 频道下载。
    • -c nvidia: 指定从 NVIDIA 的 Conda 频道下载 CUDA 相关依赖。
    • pytorch-cuda=11.8: 指定 PyTorch 使用的 CUDA 版本。
  • 安装 CPU 版本:
    如果选择 CPU,命令会更简单:

    # 示例命令,请务必从官网获取最新命令
    conda install pytorch torchvision torchaudio cpuonly -c pytorch
    

使用 Pip 安装:

  • 最好在虚拟环境中使用 pip:

    python -m venv pytorch_venv   # 创建名为 pytorch_venv 的虚拟环境
    source pytorch_venv/bin/activate # Linux/Mac 激活
    # .\pytorch_venv\Scripts\activate # Windows (cmd) 激活
    # pytorch_venv\Scripts\Activate.ps1 # Windows (PowerShell) 激活
    
  • 根据官网生成的命令安装 PyTorch:
    例如,安装支持 CUDA 11.8 的稳定版 PyTorch (命令可能随官网更新而变化):

    # (在激活的 pytorch_venv 环境中执行)
    # 示例命令,请务必从官网获取最新命令
    pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
    
    • --index-url https://download.pytorch.org/whl/cu118: 指定 PyTorch 的 wheel 文件下载地址,cu118 表示 CUDA 11.8。
  • 安装 CPU 版本:

    # 示例命令,请务必从官网获取最新命令
    pip3 install torch torchvision torchaudio
    

    或者指定 CPU 的 index URL:

    pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
    

验证安装:

安装完成后,可以在 Python 解释器中验证 PyTorch 是否安装成功,以及 CUDA 是否可用(如果安装了 GPU 版本)。

import torch # 导入torch库

# 1. 打印 PyTorch 版本
print(f"PyTorch Version: {
     
     torch.__version__}")
# 输出示例: PyTorch Version: 2.1.0+cu118 (版本号和CUDA版本会根据你的安装而不同)

# 2. 检查 CUDA 是否可用
cuda_available = torch.cuda.is_available() # 返回布尔值,True表示CUDA可用
print(f"CUDA Available: {
     
     cuda_available}")
# 如果安装了GPU版本且驱动和CUDA配置正确,应输出: CUDA Available: True
# 如果安装了CPU版本或GPU配置不正确,应输出: CUDA Available: False

if cuda_available:
    # 3. 获取 GPU 数量
    gpu_count = torch.cuda.device_count() # 返回可用的GPU数量
    print(f"Number of GPUs available: {
     
     gpu_count}")
    # 输出示例: Number of GPUs available: 1 (如果有一个GPU)

    # 4. 获取当前默认 GPU 的名称
    current_gpu_id = torch.cuda.current_device() # 获取当前GPU的ID (从0开始)
    current_gpu_name = torch.cuda.get_device_name(current_gpu_id) # 根据ID获取GPU名称
    print(f"Current GPU ID: {
     
     current_gpu_id}")
    print(f"Current GPU Name: {
     
     current_gpu_name}")
    # 输出示例:
    # Current GPU ID: 0
    # Current GPU Name: NVIDIA GeForce RTX 3090

    # 5. 创建一个张量并将其移动到 GPU
    # 创建一个在 CPU 上的张量
    tensor_cpu = torch.tensor([1.0, 2.0, 3.0], device='cpu') # 显式指定在CPU上
    print(f"Tensor on CPU: {
     
     tensor_cpu}, Device: {
     
     tensor_cpu.device}")
    # 输出示例: Tensor on CPU: tensor([1., 2., 3.]), Device: cpu

    # 将张量移动到默认 GPU (通常是 GPU 0)
    try:
        tensor_gpu = tensor_cpu.cuda() # .cuda() 是 .to('cuda') 的简写
        print(f"Tensor on GPU: {
     
     tensor_gpu}, Device: {
     
     tensor_gpu.device}")
        # 输出示例: Tensor on GPU: tensor([1., 2., 3.], device='cuda:0'), Device: cuda:0

        # 也可以使用 .to() 方法指定设备
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 动态选择设备
        tensor_on_device = torch.tensor([4.0, 5.0]).to(device)
        print(f"Tensor on selected device: {
     
     tensor_on_device}, Device: {
     
     tensor_on_device.device}")
        # 输出示例 (若CUDA可用): Tensor on selected device: tensor([4., 5.], device='cuda:0'), Device: cuda:0
    except Exception as e:
        print(f"Error moving tensor to GPU: {
     
     e}")
        # 如果在 .cuda() 或 .to('cuda') 时出错,会打印错误信息

else:
    print("CUDA is not available. PyTorch will run on CPU.")
    # 创建一个在 CPU 上的张量
    tensor_cpu_only = torch.tensor([1.0, 2.0, 3.0]) # 默认在CPU上创建
    print(f"Tensor on CPU: {
     
     tensor_cpu_only}, Device: {
     
     tensor_cpu_only.device}")
    # 输出示例: Tensor on CPU: tensor([1., 2., 3.]), Device: cpu

# 6. 检查 cuDNN 版本 (如果CUDA可用)
# cuDNN 是 NVIDIA 提供的用于深度神经网络的 GPU 加速库,PyTorch 会使用它。
if cuda_available and torch.backends.cudnn.is_available():
    cudnn_enabled = torch.backends.cudnn.enabled # 检查cuDNN是否启用 (默认为True)
    cudnn_version = torch.backends.cudnn.version() # 获取cuDNN版本号
    print(f"cuDNN Enabled: {
     
     cudnn_enabled}")
    print(f"cuDNN Version: {
     
     cudnn_version}")
    # 输出示例:
    # cuDNN Enabled: True
    # cuDNN Version: 8700 (或其他版本号, 例如 8.7.0)
    # torch.backends.cudnn.benchmark = True  # 可选: 启用基准测试模式,让cuDNN自动寻找最高效的算法,
                                            # 如果输入大小不变,可以加速训练,但初次迭代会稍慢。
else:
    if cuda_available:
        print("cuDNN is available but not found or not enabled by PyTorch.")
    else:
        print("cuDNN check skipped as CUDA is not available.")

代码解释:

  1. import torch: 导入 PyTorch 库,通常简称为 torch
  2. torch.__version__: 获取当前安装的 PyTorch 版本号。
  3. torch.cuda.is_available(): 这是检查 CUDA 环境是否被 PyTorch 正确识别并可用的关键函数。如果返回 False,即使你安装了 GPU 版本的 PyTorch,也意味着 PyTorch 无法使用你的 GPU,可能的原因包括:
    • NVIDIA 驱动版本过低或不兼容。
    • 安装的 PyTorch 版本与 CUDA Toolkit 版本不匹配 (例如,系统装了 CUDA 12.x,但 PyTorch 是为 CUDA 11.x 编译的)。
    • CUDA Toolkit 未正确安装或环境变量配置问题 (尽管 Conda 安装通常会处理好)。
    • 安装了 CPU 版本的 PyTorch。
  4. torch.cuda.device_count(): 返回 PyTorch 可以检测到的 CUDA GPU 数量。
  5. torch.cuda.current_device(): 返回当前 PyTorch 操作默认使用的 GPU 设备的索引 (从 0 开始)。
  6. torch.cuda.get_device_name(id): 根据 GPU 设备的索引返回其名称,例如 “NVIDIA GeForce RTX 4090”。
  7. torch.tensor([...], device='cpu'): 创建一个张量并显式指定它存储在 CPU 内存中。张量的 .device 属性会显示其所在的设备。
  8. tensor_cpu.cuda(): 这是一个便捷方法,将张量从 CPU 复制到默认的 GPU 设备 (通常是 cuda:0)。它返回一个新的位于 GPU 上的张量,原始 CPU 张量保持不变。
  9. tensor_cpu.to(device): 这是一个更通用的方法,可以将张量移动到指定的设备 (device 对象)。device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 是一种常用的写法,它会动态地选择 GPU (如果可用) 或 CPU。
  10. torch.backends.cudnn.is_available(): 检查 cuDNN 库是否对 PyTorch 可用。
  11. torch.backends.cudnn.enabled: 一个布尔标志,表示是否启用 cuDNN。默认为 True。可以设置为 False 来禁用 cuDNN,但这通常只在调试特定问题时使用。
  12. torch.backends.cudnn.version(): 返回 PyTorch 正在使用的 cuDNN 版本号。
  13. torch.backends.cudnn.benchmark = True: 这是一个优化选项。当设置为 True 时,cuDNN 会在每次遇到新的卷积层配置(输入大小、卷积核大小等不同)时,运行一些基准测试来选择针对当前硬件的最优卷积算法。这可能会导致初次迭代或模型结构变化后的迭代变慢,但后续具有相同配置的迭代会更快。如果模型的输入大小在训练过程中是固定的,设置它为 True 通常能带来性能提升。如果输入大小频繁变化,则可能会导致性能下降。

开发环境建议:

  • IDE:
    • VS Code: 强大的代码编辑器,具有优秀的 Python 和 Jupyter Notebook 支持,可以通过 Pylance/Pyright 提供良好的代码补全和类型检查。
    • PyCharm: JetBrains 出品的专业 Python IDE,功能全面,对科学计算和深度学习项目友好。
  • Jupyter Notebook / JupyterLab: 非常适合交互式数据探索、模型实验和结果可视化。

成功安装并验证 PyTorch 后,你就准备好开始探索张量的奇妙世界了。

第2章:深入理解 PyTorch 张量 (Tensors)

张量是 PyTorch 中数据操作的核心。可以将其视为 NumPy ndarray 的精神继承者,但增加了对 GPU 加速的无缝支持以及与自动求导系统的集成。理解张量的内部工作方式、各种操作及其细微差别,对于高效地使用 PyTorch 构建和训练复杂的深度学习模型至关重要。

2.1 张量的本质:数据容器与多维数组

从概念上讲,张量是一个多维数组,它可以容纳任何数值类型的数据(主要是浮点数和整数)。

  • 0阶张量 (Scalar): 单个数字,例如 torch.tensor(5.0)
  • 1阶张量 (Vector): 一维数组,例如 torch.tensor([1.0, 2.0, 3.0])
  • 2阶张量 (Matrix): 二维数组,例如 torch.tensor([[1, 2], [3, 4]])
  • n阶张量 (n-dimensional Tensor): 具有 n 个轴的多维数组。例如,在计算机视觉中,一批彩色图像通常表示为4阶张量:(batch_size, channels, height, width)。在自然语言处理中,一批词嵌入序列可能表示为3阶张量:(batch_size, sequence_length, embedding_dimension)
2.1.1 张量的创建:多样化的初始化方法

PyTorch 提供了多种创建张量的方法,以适应不同的需求。

1. 直接从数据创建 (torch.tensor() vs torch.Tensor())

  • torch.tensor(data, dtype=None, device=None, requires_grad=False): 这是推荐的从现有数据(如 Python 列表、NumPy 数组)创建张量的方法。它会推断数据类型 (dtype),除非显式指定。重要的是,它会复制输入数据。

    import torch
    import numpy as np
    
    # 从 Python 列表创建
    data_list = [[1, 2, 3], [4, 5, 6]]
    tensor_from_list = torch.tensor(data_list) # 自动推断 dtype 为 torch.int64
    print(f"Tensor from list:\n{
           
           tensor_from_list}")
    # 输出:
    # Tensor from list:
    # tensor([[1, 2, 3],
    #         [4, 5, 6]])
    print(f"Data type: {
           
           tensor_from_list.dtype}") # 数据类型
    # 输出: Data type: torch.int64
    
    # 指定数据类型为浮点数
    tensor_float = torch.tensor(data_list, dtype=torch.float32)
    print(f"\nTensor with float32 dtype:\n{
           
           tensor_float}")
    # 输出:
    # Tensor with float32 dtype:
    # tensor([[1., 2., 3.],
    #         [4., 5., 6.]])
    print(f"Data type: {
           
           tensor_float.dtype}") # 数据类型
    # 输出: Data type: torch.float32
    
    # 从 NumPy 数组创建 (数据会被复制)
    numpy_arr = np.array([[1.0, 2.5], [3.8, 4.2]])
    tensor_from_numpy_copy = torch.tensor(numpy_arr) # dtype 会从 numpy 数组继承 (float64)
    print(f"\nTensor from NumPy array (using torch.tensor, data copied):\n{
           
           tensor_from_numpy_copy}")
    # 输出:
    # Tensor from NumPy array (using torch.tensor, data copied):
    # tensor([[1.0000, 2.5000],
    #         [3.8000, 4.2000]], dtype=torch.float64)
    print(f"Data type: {
           
           tensor_from_numpy_copy.dtype}")
    # 输出: Data type: torch.float64
    
    # 修改原始 NumPy 数组,验证 torch.tensor 创建的是副本
    numpy_arr[0, 0] = 99.9
    print(f"\nOriginal NumPy array after modification:\n{
           
           numpy_arr}")
    # 输出:
    # Original NumPy array after modification:
    # [[99.9  2.5]
    #  [ 3.8  4.2]]
    print(f"Tensor created with torch.tensor (should be unchanged):\n{
           
           tensor_from_numpy_copy}")
    # 输出:
    # Tensor created with torch.tensor (should be unchanged):
    # tensor([[1.0000, 2.5000],
    #         [3.8000, 4.2000]], dtype=torch.float64)
    

    代码解释:

    • torch.tensor(data_list): 从 Python 列表 data_list 创建张量。PyTorch 会自动推断数据类型。对于整数列表,通常推断为 torch.int64
    • dtype=torch.float32: 显式指定创建的张量的数据类型为32位浮点数。这是深度学习中常用的数据类型。
    • 从 NumPy 数组创建时,torch.tensor() 同样会复制数据。这意味着修改原始 NumPy 数组不会影响已创建的 PyTorch 张量。
  • torch.Tensor(dims...)torch.Tensor(data) (不推荐直接用于数据初始化):

    • 这是一个历史悠久的构造函数,其行为有点令人困惑。
    • 如果传入的是维度参数 (如 torch.Tensor(2, 3)),它会创建一个具有指定形状的未初始化的张量,其值是内存中当前残留的任意值。这通常不是我们想要的。
    • 如果传入的是数据 (如 Python 列表 torch.Tensor(data_list)), 它会创建一个张量,但其默认数据类型是全局默认类型 (通常是 torch.float32),而不是从数据中推断。
    • 强烈建议使用 torch.tensor() 来从数据创建张量,使用 torch.empty() 来创建未初始化张量,或使用 torch.zeros(), torch.ones() 等来创建特定值的张量。

2. 从 NumPy 数组创建 (内存共享与复制的精细控制)

在数据预处理阶段,我们经常使用 NumPy 进行数值计算和数据清洗,然后需要将处理好的 NumPy 数组转换为 PyTorch 张量以供模型使用。PyTorch 提供了两种主要方式,它们在内存处理上有所不同:

  • torch.from_numpy(ndarray): 这是实现 NumPy 数组到 PyTorch 张量高效转换的关键方法。它创建的张量与原始 NumPy 数组共享相同的底层内存。这意味着:

    • 优点: 转换速度极快,因为不需要复制数据,对于大型数组尤其有利。
    • 潜在风险: 修改其中一个对象(PyTorch 张量或 NumPy 数组)会影响另一个。这在某些情况下可能导致难以察觉的 bug,需要特别小心。
    • 数据类型限制: torch.from_numpy 仅支持 NumPy 数组的特定数据类型,例如 np.bool_, np.uint8, np.int8, np.int16, np.int32, np.int64, np.float32, np.float64, np.complex64, np.complex128。如果 NumPy 数组的 dtype 不被支持,会抛出错误。
    import torch
    import numpy as np
    
    # 创建一个 NumPy 数组
    numpy_array_shared = np.array([[10.0, 20.0], [30.0, 40.0]], dtype=np.float32)
    print(f"Original NumPy array:\n{
           
           numpy_array_shared}")
    # 输出:
    # Original NumPy array:
    # [[10. 20.]
    #  [30. 40.]]
    
    # 使用 torch.from_numpy() 创建张量 (共享内存)
    tensor_shared_memory = torch.from_numpy(numpy_array_shared)
    print(f"\nTensor created with torch.from_numpy():\n{
           
           tensor_shared_memory}")
    # 输出:
    # Tensor created with torch.from_numpy():
    # tensor([[10., 20.],
    #         [30., 40.]])
    print(f"Tensor dtype: {
           
           tensor_shared_memory.dtype}") # dtype会与NumPy数组一致
    # 输出: Tensor dtype: torch.float32
    
    # 修改 NumPy 数组中的一个元素
    print("\nModifying the NumPy array (e.g., numpy_array_shared[0, 0] = 100.0)...")
    numpy_array_shared[0, 0] = 100.0
    print(f"NumPy array after modification:\n{
           
           numpy_array_shared}")
    # 输出:
    # NumPy array after modification:
    # [[100.  20.]
    #  [ 30.  40.]]
    print(f"Tensor (should reflect the change due to shared memory):\n{
           
           tensor_shared_memory}")
    # 输出 (张量的值也改变了):
    # Tensor (should reflect the change due to shared memory):
    # tensor([[100.,  20.],
    #         [ 30.,  40.]])
    
    # 修改 PyTorch 张量中的一个元素
    print("\nModifying the PyTorch tensor (e.g., tensor_shared_memory[1, 1] = 400.0)...")
    tensor_shared_memory[1, 1] = 400.0
    print(f"Tensor after modification:\n{
           
           tensor_shared_memory}")
    # 输出:
    # Tensor after modification:
    # tensor([[100.,  20.],
    #         [ 30., 400.]])
    print(f"NumPy array (should also reflect this change):\n{
           
           numpy_array_shared}")
    # 输出 (NumPy 数组的值也改变了):
    # NumPy array (should also reflect this change):
    # [[100.  20.]
    #  [ 30. 400.]]
    
    # 尝试使用不支持的 dtype (例如,NumPy 的 object 类型)
    try:
        numpy_object_array = np.array([{
         
         'a': 1}, {
         
         'b': 2}], dtype=object)
        torch.from_numpy(numpy_object_array)
    except TypeError as e:
        print(f"\nError when using torch.from_numpy with unsupported dtype: {
           
           e}")
        # 输出: Error when using torch.from_numpy with unsupported dtype: can't convert np.ndarray of type numpy.object_.
        # The only supported types are: bool, an integral type, or a floating point type.
    
    # **企业级场景下的注意事项**:
    # 在数据加载管道中,如果多个处理阶段都可能修改数据,并且这些阶段之间通过 NumPy 和 PyTorch 张量转换,
    # 那么必须非常清楚数据是否共享内存。否则,一个阶段的意外修改可能会污染后续阶段的数据,导致难以调试的错误。
    # 如果需要确保隔离性,应在转换后立即使用 .clone() 创建一个副本 (详见后续章节)。
    # 例如,在一个多进程数据加载器 (DataLoader) 中,如果工作进程返回的 NumPy 数组通过 from_numpy 转换为张量,
    # 并且该 NumPy 数组之后在工作进程中被复用或修改,那么主进程中得到的张量可能会在不知情的情况下被改变。
    # 解决方案:
    # 1. 工作进程返回 NumPy 数组的副本。
    # 2. 主进程在 from_numpy 之后立即 .clone().detach() (如果不需要梯度)。
    

    代码解释:

    • torch.from_numpy(numpy_array_shared): 此函数创建的 tensor_shared_memorynumpy_array_shared 指向同一块内存区域。
    • numpy_array_shared[0, 0] 被修改时,tensor_shared_memory[0, 0] 的值也随之改变,反之亦然。
    • 这种内存共享机制在处理大规模数据集时可以显著提高效率,避免了不必要的数据复制开销。
    • 对于 object 类型的 NumPy 数组,from_numpy 会抛出 TypeError,因为它主要设计用于数值型数据。
  • torch.tensor(ndarray) (回顾): 如前所述,torch.tensor() 构造函数在传入 NumPy 数组时,总是会复制数据来创建一个新的 PyTorch 张量。这意味着新张量和原始 NumPy 数组拥有各自独立的内存空间。

    numpy_array_for_copy = np.array([[1.1, 2.2], [3.3, 4.4]], dtype=np.float32)
    print(f"\nOriginal NumPy array for copy test:\n{
           
           numpy_array_for_copy}")
    # 输出:
    # Original NumPy array for copy test:
    # [[1.1 2.2]
    #  [3.3 4.4]]
    
    # 使用 torch.tensor() 创建张量 (复制数据)
    tensor_copied_data = torch.tensor(numpy_array_for_copy)
    print(f"\nTensor created with torch.tensor() (data copied):\n{
           
           tensor_copied_data}")
    # 输出:
    # Tensor created with torch.tensor() (data copied):
    # tensor([[1.1000, 2.2000],
    #         [3.3000, 4.4000]])
    print(f"Tensor dtype: {
           
           tensor_copied_data.dtype}") # dtype 继承自 numpy (float32)
    # 输出: Tensor dtype: torch.float32
    
    # 修改 NumPy 数组
    print("\nModifying the NumPy array (e.g., numpy_array_for_copy[0, 0] = 111.1)...")
    numpy_array_for_copy[0, 0] = 111.1
    print(f"NumPy array after modification:\n{
           
           numpy_array_for_copy}")
    # 输出:
    # NumPy array after modification:
    # [[111.1   2.2]
    #  [  3.3   4.4]]
    print(f"Tensor (should be unchanged as it's a copy):\n{
           
           tensor_copied_data}")
    # 输出 (张量未改变):
    # Tensor (should be unchanged as it's a copy):
    # tensor([[1.1000, 2.2000],
    #         [3.3000, 4.4000]])
    

    代码解释:

    • torch.tensor(numpy_array_for_copy) 创建的 tensor_copied_datanumpy_array_for_copy 的一个深拷贝。
    • 修改 numpy_array_for_copy 不会影响 tensor_copied_data

何时选择 from_numpy vs tensor?

  • 如果你需要最高的转换效率,并且确信在后续操作中共享内存不会引入问题(或者你有意利用这种共享),则使用 torch.from_numpy()
  • 如果你希望 PyTorch 张量与原始 NumPy 数组完全独立,以避免潜在的副作用,或者 NumPy 数组的 dtype 不被 from_numpy 支持但可以被 tensor 安全转换,则使用 torch.tensor()
  • 经验法则: 在不确定时,优先考虑数据安全性,使用 torch.tensor() (或 torch.from_numpy().clone()) 来避免意外的内存共享问题。性能优化可以在确认安全性的前提下进行。

3. 创建具有特定形状和值的张量

这些函数用于创建具有预定义结构和初始值的张量,常用于初始化模型参数、创建掩码或占位符。

  • torch.zeros(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False): 创建一个所有元素都为 0 的张量。

  • torch.ones(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False): 创建一个所有元素都为 1 的张量。

  • torch.empty(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False): 创建一个具有指定形状的张量,但其元素是未初始化的(即包含内存中当前位置的任意数据)。使用 emptyzerosones 稍快,因为它跳过了填充值的步骤,但后续必须确保在使用前为其赋值。

    # 创建全零张量
    zeros_tensor = torch.zeros(2, 3, 4) # 形状为 (2, 3, 4)
    print(f"\nZeros tensor (shape {
           
           zeros_tensor.shape}):\n{
           
           zeros_tensor}")
    # 输出:
    # Zeros tensor (shape torch.Size([2, 3, 4])):
    # tensor([[[0., 0., 0., 0.],
    #          [0., 0., 0., 0.],
    #          [0., 0., 0., 0.]],
    #
    #         [[0., 0., 0., 0.],
    #          [0., 0., 0., 0.],
    #          [0., 0., 0., 0.]]])
    print(f"Zeros tensor dtype: {
           
           zeros_tensor.dtype}") # 默认为 torch.float32
    # 输出: Zeros tensor dtype: torch.float32
    
    # 创建全一整数张量,并在 GPU 上 (如果可用)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    ones_tensor_int_gpu = torch.ones(3, 2, dtype=torch.int32, device=device)
    print(f"\nOnes tensor (shape {
           
           ones_tensor_int_gpu.shape}, dtype {
           
           ones_tensor_int_gpu.dtype}, device {
           
           ones_tensor_int_gpu.device}):\n{
           
           ones_tensor_int_gpu}")
    # 输出 (示例,若在CPU上):
    # Ones tensor (shape torch.Size([3, 2]), dtype torch.int32, device cpu):
    # tensor([[1, 1],
    #         [1, 1],
    #         [1, 1]], dtype=torch.int32)
    
    # 创建未初始化张量 (内容是随机的)
    empty_tensor = torch.empty(2, 2)
    print(f"\nEmpty tensor (shape {
           
           empty_tensor.shape}, values are uninitialized):\n{
           
           empty_tensor}")
    # 输出 (值是随机的,每次运行可能不同):
    # Empty tensor (shape torch.Size([2, 2]), values are uninitialized):
    # tensor([[3.0725e-41, 0.0000e+00],
    #         [1.1210e-43, 0.0000e+00]])
    
    # 企业级应用:大型模型权重初始化占位
    # 当加载一个巨大的预训练模型时,有时会先用 torch.empty 创建一个形状匹配的空张量,
    # 然后逐层或逐块地将实际权重加载到这个张量中,这可以更精细地控制内存分配过程,
    # 特别是在内存受限的环境下,或者当模型权重分布在多个文件/部分时。
    # 例如,为一个大型 Embedding 层预分配空间:
    vocab_size = 1000000 # 假设有一百万词汇
    embedding_dim = 768   # 嵌入维度
    # 直接 zeros 可能导致瞬间大量内存分配
    # large_embedding_weights_zeros = torch.zeros(vocab_size, embedding_dim)
    # 使用 empty,稍后填充
    large_embedding_weights_empty = torch.empty(vocab_size, embedding_dim, dtype=torch.float16, device=device) # 使用半精度节省内存
    print(f"\nShape of large empty embedding weights: {
           
           large_embedding_weights_empty.shape}, on device: {
           
           large_embedding_weights_empty.device}")
    # 假设权重从磁盘分块加载
    # for chunk_idx in range(num_chunks):
    #     weights_chunk = load_weights_chunk_from_disk(chunk_idx) # 自定义函数加载数据块
    #     start_row = chunk_idx * chunk_size
    #     end_row = start_row + chunk_size
    #     large_embedding_weights_empty[start_row:end_row, :] = torch.from_numpy(weights_chunk).to(device, dtype=torch.float16)
    # print("Large embedding weights would be filled here.")
    del large_embedding_weights_empty # 及时释放内存
    torch.cuda.empty_cache() if device.type == 'cuda' else None # 如果在GPU上,清空缓存
    

    代码解释:

    • torch.zeros(2, 3, 4): 创建一个形状为 (2, 3, 4),所有元素为0的张量。默认数据类型是 torch.float32
    • torch.ones(3, 2, dtype=torch.int32, device=device): 创建一个形状为 (3, 2),所有元素为1的张量,数据类型为 torch.int32,并将其放置在 device (GPU或CPU)上。
    • torch.empty(2, 2): 创建一个形状为 (2, 2) 的张量,其元素未被初始化,包含的是内存中的垃圾值。这比 zerosones 快,因为它省略了填充值的步骤。
    • 企业级应用示例展示了如何使用 torch.empty 为大型模型(如词嵌入层)预分配内存空间,特别是在内存敏感或分块加载权重的场景下。使用 torch.float16 (半精度浮点数) 可以进一步减少内存占用。
  • torch.zeros_like(input, ...), torch.ones_like(input, ...), torch.empty_like(input, ...), torch.full_like(input, fill_value, ...):
    这些函数会创建一个与 input 张量具有相同形状、相同设备和(默认情况下)相同数据类型的张量,但其值根据函数名确定(全0,全1,未初始化,或用 fill_value 填充)。

    base_tensor = torch.rand(2, 3, dtype=torch.double, device=device) # 创建一个随机的基准张量
    print(f"\nBase tensor:\n{
           
           base_tensor}")
    # 输出 (示例,若在CPU上,dtype为float64):
    # Base tensor:
    # tensor([[0.1327, 0.2648, 0.4759],
    #         [0.2835, 0.0397, 0.6970]], dtype=torch.float64)
    
    zeros_like_base = torch.zeros_like(base_tensor) # 与 base_tensor 形状、dtype、device 相同,但值为0
    print(f"\nZeros_like base tensor:\n{
           
           zeros_like_base}")
    # 输出:
    # Zeros_like base tensor:
    # tensor([[0., 0., 0.],
    #         [0., 0., 0.]], dtype=torch.float64)
    print(f"Shape: {
           
           zeros_like_base.shape}, Dtype: {
           
           zeros_like_base.dtype}, Device: {
           
           zeros_like_base.device}")
    # 输出: Shape: torch.Size([2, 3]), Dtype: torch.float64, Device: cpu (或cuda:0)
    
    # 可以覆盖 dtype 和 device
    ones_like_base_float32_cpu = torch.ones_like(base_tensor, dtype=torch.float32, device='cpu')
    print(f"\nOnes_like base tensor (float32, CPU):\n{
           
           ones_like_base_float32_cpu}")
    # 输出:
    # Ones_like base tensor (float32, CPU):
    # tensor([[1., 1., 1.],
    #         [1., 1., 1.]])
    print(f"Shape: {
           
           ones_like_base_float32_cpu.shape}, Dtype: {
           
           ones_like_base_float32_cpu.dtype}, Device: {
           
           ones_like_base_float32_cpu.device}")
    # 输出: Shape: torch.Size([2, 3]), Dtype: torch.float32, Device: cpu
    
    # 使用 full_like
    fill_value = 7.7
    full_like_base = torch.full_like(base_tensor, fill_value)
    print(f"\nFull_like base tensor (filled with {
           
           fill_value}):\n{
           
           full_like_base}")
    # 输出:
    # Full_like base tensor (filled with 7.7):
    # tensor([[7.7000, 7.7000, 7.7000],
    #         [7.7000, 7.7000, 7.7000]], dtype=torch.float64)
    
    # 企业级应用:创建与模型输出匹配的标签张量或掩码
    # 假设 model_output 是一个模型的输出 logits,形状为 (batch_size, num_classes)
    batch_size_eg = 4
    num_classes_eg = 10
    model_output_eg = torch.randn(batch_size_eg, num_classes_eg, device=device)
    
    # 需要创建一个与 model_output_eg 形状和设备都相同的全零张量,用于存储 one-hot 编码的标签
    # 但标签通常是整数类型
    target_labels_one_hot = torch.zeros_like(model_output_eg, dtype=torch.long) # 或者 dtype=torch.float32 如果损失函数需要
    print(f"\nShape of target_labels_one_hot: {
           
           target_labels_one_hot.shape}, dtype: {
           
           target_labels_one_hot.dtype}, device: {
           
           target_labels_one_hot.device}")
    # 输出: Shape of target_labels_one_hot: torch.Size([4, 10]), dtype: torch.long, device: cpu (或cuda:0)
    

    代码解释:

    • torch.zeros_like(base_tensor): 创建一个与 base_tensor 具有相同属性(形状、数据类型、设备)但所有元素为0的新张量。
    • torch.ones_like(base_tensor, dtype=torch.float32, device='cpu'): 同样基于 base_tensor 创建,但可以覆盖数据类型为 torch.float32 并强制放在 CPU 上。
    • torch.full_like(base_tensor, 7.7): 创建一个与 base_tensor 属性相同但所有元素填充为 7.7 的张量。
    • 在企业级应用中,*_like 函数非常方便。例如,在处理模型输出时,经常需要创建具有相同批次大小、设备等属性的目标张量或掩码,*_like 可以确保这些属性自动匹配,减少出错的可能。
  • torch.full(size, fill_value, ...): 创建一个指定形状 size 的张量,并用 fill_value 填充所有元素。

    custom_filled_tensor = torch.full((2, 4), fill_value=np.pi, dtype=torch.float64) # 形状(2,4),用pi填充
    print(f"\nTensor filled with pi (shape {
           
           custom_filled_tensor.shape}, dtype {
           
           custom_filled_tensor.dtype}):\n{
           
           custom_filled_tensor}")
    # 输出:
    # Tensor filled with pi (shape torch.Size([2, 4]), dtype torch.float64):
    # tensor([[3.1416, 3.1416, 3.1416, 3.1416],
    #         [3.1416, 3.1416, 3.1416, 3.1416]], dtype=torch.float64)
    

4. 创建序列和范围张量

  • torch.arange(start=0, end, step=1, ...): 创建一个一维张量,包含从 start (包含) 到 end (不包含),以 step 为步长的等差序列。功能类似于 Python 的 range() 和 NumPy 的 np.arange()

    range_tensor_1 = torch.arange(10) # 0 到 9
    print(f"\nArange tensor (0 to 9):\n{
           
           range_tensor_1}")
    # 输出: Arange tensor (0 to 9):
    # tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    
    range_tensor_2 = torch.arange(2, 10, step=2, dtype=torch.float32) # 2 到 8 (不含10),步长2,浮点数
    print(f"\nArange tensor (2 to 8, step 2, float32):\n{
           
           range_tensor_2}")
    # 输出: Arange tensor (2 to 8, step 2, float32):
    # tensor([2., 4., 6., 8.])
    
    # 企业级应用:生成时间步索引或位置编码的基向量
    # 在 Transformer 模型中,位置编码需要为序列中的每个位置生成一个索引
    sequence_length_eg = 128
    position_indices = torch.arange(sequence_length_eg, device=device, dtype=torch.long)
    print(f"\nPosition indices for a sequence of length {
           
           sequence_length_eg} (first 10):\n{
           
           position_indices[:10]}...")
    # 输出: Position indices for a sequence of length 128 (first 10):
    # tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], device='cpu' (或 cuda:0))...
    
  • torch.linspace(start, end, steps, ...): 创建一个一维张量,包含从 startend (两者都包含) 的 steps 个等间距点。

    linspace_tensor = torch.linspace(0, 1, steps=11) # 0到1之间,共11个点 (包含0和1)
    print(f"\nLinspace tensor (0 to 1, 11 steps):\n{
           
           linspace_tensor}")
    # 输出: Linspace tensor (0 to 1, 11 steps):
    # tensor([0.0000, 0.1000, 0.2000, 0.3000, 0.4000, 0.5000, 0.6000, 0.7000, 0.8000,
    #         0.9000, 1.0000])
    
    # 企业级应用:在可视化或参数扫描时生成均匀采样点
    # 例如,绘制函数 y = sin(x) 在 [0, 2*pi] 区间的图像
    x_coords = torch.linspace(0, 2 * np.pi, steps=100, device=device)
    y_coords = torch.sin(x_coords)
    # plt.plot(x_coords.cpu().numpy(), y_coords.cpu().numpy()) # 移回CPU并转为NumPy进行绘图
    # plt.title("sin(x) using linspace points")
    # plt.show()
    print(f"\nFirst 5 x_coords for plotting sin(x):\n{
           
           x_coords[:5]}")
    print(f"First 5 y_coords for plotting sin(x):\n{
           
           y_coords[:5]}")
    
  • torch.logspace(start, end, steps, base=10.0, ...): 创建一个一维张量,包含从 (base^{start}) 到 (base^{end}) (两者都包含) 的 steps 个按对数尺度等间距的点。

    logspace_tensor = torch.logspace(start=-2, end=2, steps=5, base=10.0) # 10^-2 到 10^2,5个点
    # 对应 0.01, 0.1, 1.0, 10.0, 100.0
    print(f"\nLogspace tensor (10^-2 to 10^2, 5 steps, base 10):\n{
           
           logspace_tensor}")
    # 输出: Logspace tensor (10^-2 to 10^2, 5 steps, base 10):
    # tensor([1.0000e-02, 1.0000e-01, 1.0000e+00, 1.0000e+01, 1.0000e+02])
    
    # 企业级应用:生成超参数的对数尺度搜索范围
    # 在超参数调优时,学习率 (learning rate) 或正则化系数 (regularization strength) 通常在对数尺度上搜索
    # 例如,搜索学习率范围从 1e-5 到 1e-1
    learning_rates_to_try = torch.logspace(-5, -1, steps=10, base=10.0)
    print(f"\nPotential learning rates for hyperparameter search (log scale):\n{
           
           learning_rates_to_try}")
    # 输出: Potential learning rates for hyperparameter search (log scale):
    # tensor([1.0000e-05, 2.7826e-05, 7.7426e-05, 2.1544e-04, 5.9948e-04,
    #         1.6681e-03, 4.6416e-03, 1.2915e-02, 3.5938e-02, 1.0000e-01])
    

5. 创建具有特定分布的随机张量

在初始化神经网络权重、数据增强、实现某些随机算法(如蒙特卡洛模拟)或生成模拟数据时,从特定概率分布中采样的随机张量非常重要。PyTorch 提供了丰富的随机数生成函数,并且可以控制随机数种子以保证实验的可复现性。

  • 设置随机数种子 (torch.manual_seed(seed)):
    为了使随机操作(如权重初始化、随机采样)的结果可复现,需要在程序开始时或关键随机操作前设置随机数种子。
    如果使用 GPU,还需要为所有 GPU 设置种子:torch.cuda.manual_seed_all(seed) (如果 torch.cuda.is_available())。
    更进一步,为了在 cuDNN 等底层库层面也获得确定性行为(可能会牺牲一些性能),可以设置:
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False (与 deterministic=True 通常一起使用)

    seed = 42
    torch.manual_seed(seed) # 设置CPU随机种子
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed) # 为所有GPU设置随机种子
        # 下面两行可以进一步增强确定性,但可能影响性能
        # torch.backends.cudnn.deterministic = True
        # torch.backends.cudnn.benchmark = False
    print(f"Set random seed to {
           
           seed}")
    
  • torch.rand(*size, ...): 创建一个张量,其元素从区间 [0, 1) 上的均匀分布中独立采样。

  • torch.randn(*size, ...): 创建一个张量,其元素从均值为 0、方差为 1 的标准正态分布 (高斯分布) 中独立采样。这是神经网络权重初始化的常用分布之一 (例如配合 Xavier/Glorot 或 He 初始化)。

    # 均匀分布 [0, 1)
    uniform_rand_tensor = torch.rand(2, 3)
    print(f"\nUniform random tensor (0 to 1):\n{
           
           uniform_rand_tensor}")
    # 输出 (设置种子后,结果是固定的):
    # Uniform random tensor (0 to 1):
    # tensor([[0.8823, 0.9150, 0.3829],
    #         [0.9593, 0.3904, 0.6009]])
    
    # 标准正态分布 (mean=0, std=1)
    normal_randn_tensor = torch.randn(2, 3)
    print(f"\nStandard normal random tensor (mean 0, std 1):\n
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值