PyTorch,从零基础到实战教程

2024-11-16

PyTorch,一个好用的深度学习框架。

1. 深度学习与 PyTorch 简介

1.1 什么是深度学习?

在我们日常生活中,我们的大脑会通过大量的学习和训练来识别事物,比如区分猫和狗。这种能力依赖于 神经网络,它由大量的神经元组成,能够处理复杂的信息。

深度学习(Deep Learning) 是一种受生物神经网络启发的人工智能方法,它能够通过 神经网络(Neural Network) 自动学习数据中的特征,并用于分类、预测等任务。

1.2 PyTorch 的核心优势

PyTorch 是一个广泛使用的深度学习框架,它的特点包括:

  • 动态图计算 vs 静态计算图:与 TensorFlow(早期版本)不同,PyTorch 采用动态图计算,代码更直观,调试更方便。
    • 动态计算图(Dynamic Computation Graph)静态计算图(Static Computation Graph) 是深度学习框架中两种不同的计算图构建方式,它们的区别可以用 “实时绘图” vs “蓝图施工” 来形象理解。

      特性 动态图(PyTorch) 静态图(TensorFlow 早期版本)
      构建时机 代码执行时实时构建(边运行边画图) 运行前先定义完整计算图(先画蓝图再施工)
      灵活性 支持条件分支、循环等复杂逻辑(自由调整) 需提前定义所有路径(固定结构)
      调试难度 可直接打印中间变量(透明可见) 需通过会话(Session)调试(黑箱操作)
      性能优化 运行时优化受限(灵活性优先) 可全局优化(适合部署)
      典型应用场景 研究、快速实验、可变长度输入(如 NLP) 工业部署、固定计算流程(如推荐系统)
    • 动态图的直观示例

      想象你在搭建乐高模型:

        # PyTorch 动态图示例:可以实时修改结构
        for data in dataset:
            x = torch.randn(3)  # 输入数据
            if x.sum() > 0:     # 动态分支
                y = x * 2
            else:
                y = x + 1
            loss = y.mean()     # 实时构建计算图
            loss.backward()     # 反向传播
      
    • 静态图的局限与突破

      早期 TensorFlow 的静态图需要预先定义完整流程:

        # TensorFlow 静态图示例(旧版本)
        import tensorflow as tf
              
        # 必须预先定义所有可能的分支
        x = tf.placeholder(tf.float32, shape=[None, 3])
        condition = tf.reduce_sum(x) > 0
        y = tf.cond(condition,
                    lambda: x * 2,
                    lambda: x + 1)  # 所有分支需提前声明
        loss = tf.reduce_mean(y)
              
        with tf.Session() as sess:
            result = sess.run(loss, feed_dict={x: data})  # 运行时无法修改结构
      
    • 为什么 PyTorch 选择动态图?
      1. 符合直觉的编程方式

        动态图的代码就像普通 Python 代码一样逐行执行,可插入 print 语句调试中间结果

         x = torch.tensor([1.0], requires_grad=True)
         y = x * 2
         print(y)  # 输出 tensor([2.], grad_fn=<MulBackward0>)
         y.backward()  # 随时查看梯度
        
      2. 处理可变长度数据

        在自然语言处理(NLP)中,每个句子的长度不同,动态图可以为每个样本单独构建计算图

         # 处理不同长度的句子
         for sentence in text_data:
             words = sentence.split()
             embeddings = lookup_table(words)  # 动态调整嵌入层维度
             output = model(embeddings)        # 无需填充(padding)
                    
        
      3. 快速原型开发

        研究者可以自由修改网络结构,例如在循环中动态添加层:

         class DynamicNet(nn.Module):
             def __init__(self):
                 super().__init__()
                 self.layers = nn.ModuleList()
                    
             def forward(self, x):
                 for i in range(random.randint(1,5)):  # 动态深度!
                     x = nn.Linear(10,10)(x)
                     x = torch.relu(x)
                 return x
                    
        
    • 动态图的代价

      尽管动态图灵活,但也有一些缺点:

      • 性能损失:每次前向传播都重新构建计算图,无法进行全局优化
      • 部署难度:动态逻辑难以导出为静态模型(可通过 TorchScript 转换解决)
  • GPU 加速:PyTorch 允许代码在 CPU 和 GPU 之间无缝切换,加快计算速度。
  • 生态丰富:PyTorch 提供了 TorchVision(图像处理)、TorchText(自然语言处理)、TorchAudio(语音处理)等工具,适用于多个领域。
    • 社区生态:Hugging Face Transformers(最流行的 NLP 库)、PyTorch Lightning(简化训练流程)、TorchServe(生产环境部署工具)

2. 虚拟环境与 PyTorch 安装

2.1 为什么需要虚拟环境?

在机器学习和深度学习项目中,我们通常会安装不同的 Python 依赖库,而这些库可能存在 版本冲突。虚拟环境(Virtual Environment)允许我们在不同的项目中隔离 Python 依赖,保证项目的稳定性。

想象你是一个化学家,你的实验室里有很多不同的试剂。如果你把所有试剂混在一个瓶子里,可能会发生危险的化学反应。同样,在计算机环境中,不同的 Python 依赖可能会相互影响,导致项目无法运行。


2.2 详细安装步骤(适用于 Windows/macOS/Linux)

1. 创建虚拟环境

# 创建虚拟环境
python -m venv pytorch_env

2. 激活虚拟环境

# Windows
pytorch_env\Scripts\activate

# macOS/Linux
source pytorch_env/bin/activate

3. 安装 PyTorch

我们可以使用 PyTorch 官方网站的安装命令生成器(https://pytorch.org/get-started/locally/)。

安装 PyTorch(根据硬件环境选择适合的版本):

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

4. 验证安装是否成功

import torch
print(torch.__version__)  # 打印 PyTorch 版本
print(torch.cuda.is_available())  # 检查 GPU 支持
print(torch.rand(3,3).to('cuda'))  # 测试 GPU 计算

如果输出 True,说明 PyTorch 已成功检测到 GPU。


3. PyTorch 核心概念和基础操作

3.1 什么是 Tensor?

在 PyTorch 中,Tensor(张量) 是数据的基本存储单位,类似于 NumPy 数组,但具有更强的计算能力。Tensor 主要用于存储数据,并可以在 CPUGPU 上进行计算。

3.2 Tensor 和 NumPy 的区别:

  • NumPy:只支持 CPU 计算,不支持 GPU 加速。
  • PyTorch Tensor:可以在 GPU 上加速计算,并支持自动梯度计算。

3.3 创建 Tensor

import torch

# 创建一个 2x3 的矩阵(手动指定数据)
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
print("张量 x:", x)

# 创建全 0 和全 1 的张量
zeros = torch.zeros(3, 3)  # 3x3 的全零矩阵
ones = torch.ones(2, 2)    # 2x2 的全 1 矩阵
print("全 0 张量:", zeros)
print("全 1 张量:", ones)

# 创建随机数张量
rand_tensor = torch.rand(4, 4)  # 4x4 的随机数矩阵
print("随机张量:", rand_tensor)

说明:

  • torch.tensor(data):直接使用 Python 列表或 NumPy 数组创建 Tensor。
  • torch.zeros(shape):创建指定形状的全零张量。
  • torch.ones(shape):创建指定形状的全 1 张量。
  • torch.rand(shape):创建指定形状的随机数张量,范围在 [0,1] 之间。

3.4 Tensor 的基本运算

x = torch.tensor([[1, 2], [3, 4]])
y = torch.tensor([[5, 6], [7, 8]])

# 张量加法(两种方式)
print("x + y:", x + y)
print("torch.add(x, y):", torch.add(x, y))

# 逐元素乘法(Hadamard 乘积)
print("x * y:", x * y)

# 矩阵乘法(点积)
print("矩阵乘法 torch.matmul(x, y):", torch.matmul(x, y))

# 求均值和求和
print("x 的均值:", x.float().mean())
print("x 的总和:", x.sum())

说明:

  • x + ytorch.add(x, y):计算逐元素加法。
  • x * y:计算逐元素乘法。
  • torch.matmul(x, y):计算矩阵乘法(点积)。
  • x.mean():计算所有元素的均值。
  • x.sum():计算所有元素的总和。

3.5 Tensor 的变形、切片和转换

# 变形(Reshape)
tensor = torch.arange(12)  # 创建一个包含 0~11 的一维张量
print("原始张量:", tensor)
reshaped_tensor = tensor.view(3, 4)  # 变形成 3x4 矩阵
print("变形后的张量:", reshaped_tensor)

# 维度变换(Transpose)
x = torch.rand(2, 3)
y = x.t()  # 转置操作
print("原始张量:", x)
print("转置后的张量:", y)

# 切片(Slicing)
tensor = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("原始张量:", tensor)
print("第一行:", tensor[0])  # 选择第一行
print("第一列:", tensor[:, 0])  # 选择第一列
print("子矩阵:", tensor[0:2, 1:])  # 选择子矩阵

# 数据类型转换(dtype Conversion)
tensor = torch.tensor([1.2, 3.4, 5.6])
print("原始数据类型:", tensor.dtype)
int_tensor = tensor.int()  # 转换为整数类型
print("转换后的数据类型:", int_tensor.dtype)

这些操作对于处理图像数据、神经网络输入、模型优化等非常重要。

3.6 Tensor 的拼接、广播、克隆

# 拼接(Concatenation)
a = torch.rand(2, 3)
b = torch.rand(2, 3)
concat_dim0 = torch.cat((a, b), dim=0)  # 在行方向拼接
concat_dim1 = torch.cat((a, b), dim=1)  # 在列方向拼接
print("拼接后(按行):", concat_dim0)
print("拼接后(按列):", concat_dim1)

# 广播机制(Broadcasting)
x = torch.rand(3, 1)
y = torch.rand(1, 4)
result = x + y  # PyTorch 自动扩展形状以匹配
print("广播结果:", result)

# 克隆(Cloning)
tensor = torch.rand(3, 3)
clone_tensor = tensor.clone()  # 深拷贝,不共享内存
print("原始张量:", tensor)
print("克隆张量:", clone_tensor)

3.7 Tensor 的索引、掩码、填充

# 张量索引
x = torch.tensor([[10, 20, 30], [40, 50, 60], [70, 80, 90]])
print("第一行:", x[0])  # 选择第一行
print("第一列:", x[:, 0])  # 选择第一列
print("子矩阵:", x[0:2, 1:])  # 选择子矩阵

# 掩码操作(提取符合条件的元素)
x = torch.tensor([1, 2, 3, 4, 5])
mask = x > 3  # 生成布尔掩码
filtered_x = x[mask]  # 仅保留大于 3 的元素
print("大于 3 的元素:", filtered_x)

# 填充(Padding)
import torch.nn.functional as F
x = torch.tensor([[1, 2], [3, 4]])
padded_x = F.pad(x, (1, 1, 1, 1), mode='constant', value=0)  # 在四周填充 0
print("填充后的张量:", padded_x)

这些操作在 数据预处理、特征工程、深度学习计算 中非常重要,能够提高计算效率,减少不必要的数据复制。


4. 自动求导(Autograd)

4.1 Autograd 介绍

在深度学习中,模型的训练依赖于反向传播(Backpropagation) 计算参数的梯度,从而更新模型的权重。PyTorch 提供了 autograd 模块,它可以自动计算梯度,使得训练神经网络更加高效。autograd深度学习模型训练的关键,确保梯度计算正确无误。

主要概念:

  • requires_grad=True:指定张量是否需要计算梯度。
  • backward():执行反向传播计算梯度。
  • .grad:存储梯度信息。
  • detach():阻止张量跟踪梯度。

4.2 计算梯度示例

import torch

# 创建需要计算梯度的张量
x = torch.randn(3, requires_grad=True)
print("x:", x)

# 定义计算公式 y = x^2
y = x ** 2
print("y:", y)

# 计算 z = y 的和
z = y.sum()
print("z:", z)

# 反向传播计算梯度
z.backward()

# 输出梯度值,梯度是 dy/dx = 2x
print("x 的梯度:", x.grad)

说明:

  1. x 是一个 需要计算梯度的张量
  2. y = x^2 计算了每个元素的平方。
  3. z = y.sum() 计算 y 的总和,这是为了计算 zx 的梯度。
  4. z.backward() 计算 dz/dx,并将梯度存储在 x.grad 中。
  5. 根据微积分公式 dy/dx = 2x,因此 x.grad 的值应该是 2*x

4.3 阻止梯度计算

有时候,我们不希望计算某些张量的梯度(例如,在预测或冻结某些网络层时)。可以使用 detach()with torch.no_grad()

x = torch.randn(3, requires_grad=True)
y = x ** 2

# 方法 1:使用 detach()
y_detached = y.detach()
print("y_detached:", y_detached)

# 方法 2:使用 with torch.no_grad()
with torch.no_grad():
    y_no_grad = x ** 2
    print("y_no_grad:", y_no_grad)

说明:

  • detach() 生成一个新的张量,但不会计算梯度。
  • torch.no_grad() 作用域内的计算不会影响梯度。

4.4 清除梯度

默认情况下,PyTorch 的 grad 会累积梯度,因此每次计算前需要清除梯度,以防止梯度计算错误。

x = torch.randn(3, requires_grad=True)
y = x ** 2
z = y.sum()
z.backward()

print("第一次计算的梯度:", x.grad)

# 清除梯度
x.grad.zero_()
z = (x ** 3).sum()
z.backward()
print("清除梯度后的新计算:", x.grad)

为什么要清除梯度?

  • 如果不清除,梯度会累加,影响训练结果。
  • x.grad.zero_()原地操作,可以避免不必要的计算开销。

4.5 在深度学习模型中应用 Autograd

在训练神经网络时,我们通常会执行以下步骤:

  1. 前向传播(Forward Propagation):计算模型的输出。
  2. 计算损失(Loss Computation):比较预测值和真实标签。
  3. 反向传播(Backward Propagation):计算梯度。
  4. 更新参数(Optimization):使用优化器更新模型参数。
import torch
import torch.nn as nn
import torch.optim as optim

# 创建一个简单的线性模型
class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.linear = nn.Linear(1, 1)  # 输入 1 维,输出 1 维

    def forward(self, x):
        return self.linear(x)

# 初始化模型、损失函数和优化器
model = SimpleNet()
criterion = nn.MSELoss()  # 均方误差损失函数
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 生成数据
torch.manual_seed(0)
x_train = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
y_train = torch.tensor([[2.0], [4.0], [6.0], [8.0]])

# 训练模型
for epoch in range(100):
    optimizer.zero_grad()  # 清空梯度
    y_pred = model(x_train)  # 前向传播
    loss = criterion(y_pred, y_train)  # 计算损失
    loss.backward()  # 反向传播(计算梯度)
    optimizer.step()  # 更新参数

    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/100], Loss: {loss.item():.4f}')

解释:

  • model(x_train):执行前向传播计算预测值。
  • criterion(y_pred, y_train):计算损失值。
  • loss.backward():计算所有参数的梯度。
  • optimizer.step():根据梯度更新参数。
  • optimizer.zero_grad():清空之前计算的梯度,避免梯度累积。

深度学习训练 过程中,autograd 负责自动计算梯度并应用优化,使得训练流程更加高效和灵活。


5. 构建神经网络

5.1 神经网络基础

  1. 神经网络的基本概念

    神经网络由 输入层(Input Layer)、隐藏层(Hidden Layers)和输出层(Output Layer) 组成。

    • 输入层:接收原始数据(如图像像素)。
    • 隐藏层:由多个神经元组成,进行数据特征提取。
    • 输出层:生成最终的分类或回归结果。
  2. 数学表达

    在神经网络中,每个神经元的计算可以表示为:

    \[y=f(Wx+b)\]

    其中:

    • $x$ 是输入数据,$W$ 是权重矩阵, $b$ 是偏置项。
    • $f(\cdot)$ 是 激活函数,如 ReLU、Sigmoid、Softmax 等。
  3. 激活函数的作用

    激活函数用于引入非线性,使神经网络能够学习复杂的特征。

    • ReLU(修正线性单元)

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

      计算简单,适用于深度神经网络。

    • Softmax 函数(用于多分类任务):

      \[f_i(x) = \frac{e^{x_i}}{\sum_{j} e^{x_j}}\]

5.2 载入数据集(MNIST 手写数字集)

from torchvision import datasets, transforms

# 定义数据预处理流程
transform = transforms.Compose([
    # 将 PIL 图片转换为 PyTorch 张量(范围从 0-255 变为 0-1)
        transforms.ToTensor(),
    # 归一化:使数据均值为 0.1307,标准差为 0.3081,以提高训练稳定性
        transforms.Normalize((0.1307,), (0.3081,))  # 归一化
])

# 下载并加载 MNIST 数据集(如果数据集未下载,会自动下载)
train_set = datasets.MNIST('./data', train=True, download=True, transform=transform)
# 使用 DataLoader 进行批量加载数据
# batch_size=64:每个批次包含 64 张图片
# shuffle=True:在每个 epoch 之前打乱数据,以提高模型泛化能力
train_loader = torch.utils.data.DataLoader(train_set, batch_size=64, shuffle=True)

说明

  • transforms.ToTensor():将图像转换为 PyTorch 张量。
  • transforms.Normalize((0.1307,), (0.3081,)):对数据进行归一化,提高训练稳定性。
  • batch_size=64:每次训练使用 64 张图片。

5.3 定义神经网络

iimport torch.nn as nn

# 定义一个神经网络模型
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        # 将输入的 28x28 图像展平为 1D 向量 (784 维)
        self.flatten = nn.Flatten()
        
        # 第一个全连接层:输入 784 维(28x28),输出 128 维
        self.linear1 = nn.Linear(28*28, 128)
        
        # 激活函数 ReLU:增加网络的非线性表达能力
        self.relu = nn.ReLU()
        
        # 第二个全连接层:输入 128 维,输出 10 维(对应 10 个类别)
        self.linear2 = nn.Linear(128, 10)

    def forward(self, x):
        # 展平输入数据,使其适应全连接层
        x = self.flatten(x)
        
        # 通过第一个全连接层
        x = self.linear1(x)
        
        # 应用 ReLU 激活函数
        x = self.relu(x)
        
        # 通过第二个全连接层,输出最终分类结果
        return self.linear2(x)

# 将模型移到 GPU 上(如果可用)
model = Net().to('cuda')

说明

  • nn.Flatten():将 28×28 的图像展平为 784 维输入。
  • nn.Linear(28*28, 128):第一个全连接层,输入 784 维,输出 128 维。
  • nn.ReLU():激活函数,提供非线性能力。
  • nn.Linear(128, 10):最终输出 10 维,对应 10 个类别(数字 0-9)。

5.4 训练神经网络

import torch.optim as optim

# 定义损失函数(交叉熵损失)
# 适用于多分类问题,计算预测值与真实标签之间的误差
criterion = nn.CrossEntropyLoss()

# 选择优化器(Adam)
# 作用:更新模型参数以最小化损失
# lr=0.001 代表学习率,影响模型收敛速度
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 训练模型 10 轮(Epochs)
for epoch in range(10):
    # 遍历训练数据集
    for images, labels in train_loader:
        # 将数据移动到 GPU(如果可用)
        images, labels = images.to('cuda'), labels.to('cuda')

        # 清空之前计算的梯度,防止梯度累积影响优化
        optimizer.zero_grad()

        # 前向传播:计算模型预测值
        outputs = model(images)

        # 计算损失值(预测值与真实值的误差)
        loss = criterion(outputs, labels)

        # 反向传播:计算损失对模型参数的梯度
        loss.backward()

        # 更新模型参数(基于计算出的梯度)
        optimizer.step()

    # 打印当前轮次的损失值
    print(f'Epoch [{epoch+1}/10], Loss: {loss.item():.4f}')

详细步骤

  1. 前向传播(Forward Propagation):
    • outputs = model(images) 计算模型预测值。
  2. 计算损失(Loss Computation):
    • criterion(outputs, labels) 计算模型预测值与真实值之间的误差。
  3. 反向传播(Backward Propagation):
    • loss.backward() 计算每个参数的梯度。
  4. 优化(参数更新)
    • optimizer.step() 更新模型参数。
  5. 清除梯度
    • optimizer.zero_grad() 避免梯度累积。

5.5 评估神经网络

correct = 0
total = 0

# 关闭梯度计算,提高推理速度
with torch.no_grad():
    for images, labels in train_loader:
        images, labels = images.to('cuda'), labels.to('cuda')
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)  # 选取最大概率对应的类别
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

accuracy = 100 * correct / total
print(f'模型在训练集上的准确率: {accuracy:.2f}%')

说明

  • torch.no_grad():关闭梯度计算,节省内存并加快推理。
  • torch.max(outputs, 1):找到模型预测的类别。
  • 计算模型的准确率。

总结

通过本次讲座,我们从 PyTorch 的基础知识与安装入手,学习了张量的运算,了解了神经网络以及如何搭建神经网络、进行数据预处理,并进行了实战 — 训练了一个用于 MNIST 手写数字识别的模型。这是深度学习的第一步,也是通往更高级任务的起点。

未来,我们可以探索更多高级内容,例如:

  • 卷积神经网络(CNN):用于图像分类任务,如人脸识别。
  • 循环神经网络(RNN)与 Transformer:用于自然语言处理,如机器翻译。
  • 强化学习(RL):用于机器人控制或游戏 AI。
  • 模型优化:使用更复杂的优化器、数据增强和正则化技巧提升模型表现。

希望这些内容能够帮助大家理解 PyTorch 的核心概念,并激发大家在深度学习领域的探索兴趣!如有任何问题,欢迎交流和讨论。祝大家在 AI 之路上不断进步!