【零基础学AI】第22讲:PyTorch入门 - 动态图计算与图像分类器实战

本节课你将学到

  • 理解PyTorch的核心概念和优势
  • 掌握张量(Tensor)的基本操作
  • 学会使用动态计算图构建神经网络
  • 实现一个完整的图像分类器项目
  • 训练模型并进行预测

开始之前

环境要求

  • Python 3.8+
  • 建议使用GPU(可选,CPU也能运行)
  • 内存:至少4GB

需要安装的包

# CPU版本(推荐新手)
pip install torch torchvision matplotlib pillow

# GPU版本(如果有NVIDIA显卡)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

前置知识

  • 第21讲:TensorFlow基础
  • 第23讲:神经网络原理(可选,但建议先学)

核心概念

什么是PyTorch?

想象一下搭积木:

TensorFlow(静态图):就像乐高积木

  • 你必须先设计好完整的建筑图纸
  • 一旦开始搭建,就不能随意修改结构
  • 适合大型、稳定的项目

PyTorch(动态图):就像橡皮泥

  • 你可以边捏边修改形状
  • 想到什么新点子随时可以调整
  • 更灵活,更适合研究和实验

PyTorch的核心概念

1. 张量(Tensor)

张量就是多维数组,是PyTorch中所有数据的基本形式:

  • 0维张量:标量(一个数字)
  • 1维张量:向量(一排数字)
  • 2维张量:矩阵(表格)
  • 3维张量:比如彩色图片(高×宽×颜色通道)
2. 动态计算图

每次运行代码时,PyTorch都会实时构建计算图,这意味着:

  • 可以使用Python的if、for等控制语句
  • 每次前向传播都可以不同
  • 调试更容易,就像普通Python代码
3. 自动求导(Autograd)

PyTorch会自动计算梯度,你只需要:

  • 告诉它哪些张量需要计算梯度
  • 调用.backward()方法
  • 梯度就自动算好了

代码实战

第一步:PyTorch基础操作

# 导入必要的库
import torch
import torch.nn as nn  # 神经网络模块
import torch.optim as optim  # 优化器
import torchvision  # 计算机视觉工具包
import torchvision.transforms as transforms  # 数据变换
import matplotlib.pyplot as plt
import numpy as np

print("PyTorch版本:", torch.__version__)

# 检查是否有GPU可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("使用设备:", device)

第二步:张量基础操作

# 创建张量的几种方法
print("=== 张量创建 ===")

# 从Python列表创建
tensor1 = torch.tensor([1, 2, 3, 4])
print("从列表创建:", tensor1)

# 创建零张量
zeros = torch.zeros(3, 4)  # 3行4列的零矩阵
print("零张量形状:", zeros.shape)

# 创建随机张量
random_tensor = torch.randn(2, 3)  # 2行3列的随机数矩阵
print("随机张量:\n", random_tensor)

# 张量运算
print("\n=== 张量运算 ===")
a = torch.tensor([1.0, 2.0, 3.0])
b = torch.tensor([4.0, 5.0, 6.0])

# 基本运算
print("加法:", a + b)
print("乘法:", a * b)
print("矩阵乘法:", torch.dot(a, b))

# 形状操作
matrix = torch.randn(6, 1)
print("原始形状:", matrix.shape)
reshaped = matrix.view(2, 3)  # 重塑为2行3列
print("重塑后形状:", reshaped.shape)

# 移动到GPU(如果可用)
if torch.cuda.is_available():
    gpu_tensor = a.to(device)
    print("GPU张量设备:", gpu_tensor.device)

第三步:自动求导演示

# 自动求导示例
print("=== 自动求导演示 ===")

# 创建需要求导的张量
x = torch.tensor([2.0], requires_grad=True)  # 告诉PyTorch需要计算梯度
print("输入 x:", x)

# 定义一个简单函数:y = x^2 + 2x + 1
y = x**2 + 2*x + 1
print("输出 y:", y)

# 反向传播,计算梯度
y.backward()  # 自动计算dy/dx

# 查看梯度
print("梯度 dy/dx:", x.grad)
print("理论值(2x+2):", 2*2 + 2)  # 当x=2时,导数应该是6

# ⚠️ 重要提示:每次backward()后,梯度会累加
# 如果要重新计算,需要清零梯度
x.grad.zero_()

第四步:构建简单神经网络

# 定义一个简单的神经网络类
class SimpleNet(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        """
        初始化网络结构
        input_size: 输入特征数量
        hidden_size: 隐藏层神经元数量  
        output_size: 输出类别数量
        """
        super(SimpleNet, self).__init__()
        
        # 定义网络层
        # 第一层:输入层到隐藏层
        self.layer1 = nn.Linear(input_size, hidden_size)
        # 激活函数
        self.relu = nn.ReLU()
        # 第二层:隐藏层到输出层
        self.layer2 = nn.Linear(hidden_size, output_size)
    
    def forward(self, x):
        """
        前向传播过程
        这里定义数据如何在网络中流动
        """
        # 输入 -> 第一层 -> 激活函数
        out = self.layer1(x)
        out = self.relu(out)
        # 激活后的结果 -> 第二层
        out = self.layer2(out)
        return out

# 创建网络实例
# 假设输入784个特征(28×28像素的图片),10个类别(0-9数字)
net = SimpleNet(input_size=784, hidden_size=128, output_size=10)
print("网络结构:")
print(net)

# 测试网络
# 创建一个批次的假数据 (batch_size=5, features=784)
test_input = torch.randn(5, 784)
output = net(test_input)
print("\n输入形状:", test_input.shape)
print("输出形状:", output.shape)
print("输出(前5个类别的得分):", output[0][:5])

第五步:训练循环基础

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()  # 分类问题常用的损失函数
optimizer = optim.SGD(net.parameters(), lr=0.01)  # 随机梯度下降优化器

print("=== 训练循环演示 ===")

# 模拟训练数据
# 假设我们有100个样本,每个样本784个特征,标签是0-9
fake_data = torch.randn(100, 784)
fake_labels = torch.randint(0, 10, (100,))  # 随机生成0-9的标签

# 简单的训练循环
for epoch in range(5):  # 训练5个轮次
    # 前向传播
    outputs = net(fake_data)
    
    # 计算损失
    loss = criterion(outputs, fake_labels)
    
    # 反向传播和优化
    optimizer.zero_grad()  # 清零之前的梯度
    loss.backward()        # 计算梯度
    optimizer.step()       # 更新参数
    
    print(f"轮次 {epoch+1}, 损失: {loss.item():.4f}")

print("简单训练完成!")

完整项目:CIFAR-10图像分类器

现在让我们构建一个完整的图像分类项目,识别CIFAR-10数据集中的10种物体。

项目代码

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
from torch.utils.data import DataLoader

# 设置随机种子,确保结果可重现
torch.manual_seed(42)

# 检查设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

# ==================== 数据准备 ====================
print("正在下载和准备数据...")

# 数据预处理
# 为什么要这样处理:
# 1. ToTensor(): 将PIL图片转换为张量,并缩放到[0,1]
# 2. Normalize(): 标准化数据,加速训练收敛
transform_train = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
])

# 下载CIFAR-10数据集
# 第一次运行会下载数据,大约160MB
trainset = torchvision.datasets.CIFAR10(
    root='./data', train=True, download=True, transform=transform_train
)

testset = torchvision.datasets.CIFAR10(
    root='./data', train=False, download=True, transform=transform_test
)

# 创建数据加载器
# batch_size=32 意思是每次训练使用32张图片
trainloader = DataLoader(trainset, batch_size=32, shuffle=True)
testloader = DataLoader(testset, batch_size=32, shuffle=False)

# CIFAR-10的类别名称
classes = ['飞机', '汽车', '鸟', '猫', '鹿', '狗', '青蛙', '马', '船', '卡车']

print(f"训练集大小: {len(trainset)}")
print(f"测试集大小: {len(testset)}")
print(f"类别数量: {len(classes)}")

# ==================== 数据可视化 ====================
def show_sample_images():
    """显示示例图片"""
    # 获取一批数据
    dataiter = iter(trainloader)
    images, labels = next(dataiter)
    
    # 反标准化,用于显示
    def denormalize(tensor):
        # 反向操作之前的标准化
        mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
        std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
        return tensor * std + mean
    
    # 显示前8张图片
    fig, axes = plt.subplots(2, 4, figsize=(12, 6))
    for i in range(8):
        img = denormalize(images[i])
        img = torch.clamp(img, 0, 1)  # 确保像素值在[0,1]范围内
        
        row, col = i // 4, i % 4
        axes[row, col].imshow(img.permute(1, 2, 0))  # 转换通道顺序
        axes[row, col].set_title(f'标签: {classes[labels[i]]}')
        axes[row, col].axis('off')
    
    plt.tight_layout()
    plt.savefig('sample_images.png', dpi=150, bbox_inches='tight')
    plt.show()

# 显示示例图片
show_sample_images()

# ==================== 定义CNN模型 ====================
class CIFAR10Net(nn.Module):
    def __init__(self):
        super(CIFAR10Net, self).__init__()
        
        # 卷积层部分
        # 第一个卷积块
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)    # 输入3通道,输出32通道
        self.conv2 = nn.Conv2d(32, 32, 3, padding=1)   # 32->32通道
        self.pool1 = nn.MaxPool2d(2, 2)                # 2x2最大池化
        
        # 第二个卷积块  
        self.conv3 = nn.Conv2d(32, 64, 3, padding=1)   # 32->64通道
        self.conv4 = nn.Conv2d(64, 64, 3, padding=1)   # 64->64通道
        self.pool2 = nn.MaxPool2d(2, 2)                # 2x2最大池化
        
        # 全连接层部分
        # 计算:32x32输入 -> 池化后16x16 -> 再池化后8x8
        # 所以最后特征图大小是 8x8x64 = 4096
        self.fc1 = nn.Linear(64 * 8 * 8, 512)         # 4096 -> 512
        self.fc2 = nn.Linear(512, 10)                  # 512 -> 10类别
        
        # Dropout防止过拟合
        self.dropout = nn.Dropout(0.5)
        self.relu = nn.ReLU()
    
    def forward(self, x):
        # 第一个卷积块
        x = self.relu(self.conv1(x))
        x = self.relu(self.conv2(x))
        x = self.pool1(x)
        
        # 第二个卷积块
        x = self.relu(self.conv3(x))
        x = self.relu(self.conv4(x))
        x = self.pool2(x)
        
        # 展平为一维
        x = x.view(-1, 64 * 8 * 8)
        
        # 全连接层
        x = self.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        
        return x

# 创建模型实例
net = CIFAR10Net().to(device)
print("模型结构:")
print(net)

# 计算模型参数数量
total_params = sum(p.numel() for p in net.parameters())
print(f"\n模型总参数数量: {total_params:,}")

# ==================== 训练函数 ====================
def train_model(net, trainloader, epochs=5):
    """训练模型"""
    # 定义损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(net.parameters(), lr=0.001)
    
    # 记录训练过程
    train_losses = []
    train_accuracies = []
    
    print("开始训练...")
    print("=" * 50)
    
    for epoch in range(epochs):
        running_loss = 0.0
        correct_predictions = 0
        total_samples = 0
        
        # 设置为训练模式
        net.train()
        
        for batch_idx, (inputs, labels) in enumerate(trainloader):
            # 将数据移到GPU(如果可用)
            inputs, labels = inputs.to(device), labels.to(device)
            
            # 前向传播
            outputs = net(inputs)
            loss = criterion(outputs, labels)
            
            # 反向传播和优化
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            
            # 统计信息
            running_loss += loss.item()
            _, predicted = torch.max(outputs.data, 1)
            total_samples += labels.size(0)
            correct_predictions += (predicted == labels).sum().item()
            
            # 每100个批次打印一次进度
            if batch_idx % 100 == 99:
                print(f'轮次 [{epoch+1}/{epochs}], '
                      f'批次 [{batch_idx+1}/{len(trainloader)}], '
                      f'损失: {running_loss/100:.4f}')
                running_loss = 0.0
        
        # 计算每轮的准确率
        epoch_accuracy = 100 * correct_predictions / total_samples
        epoch_loss = running_loss / len(trainloader)
        
        train_losses.append(epoch_loss)
        train_accuracies.append(epoch_accuracy)
        
        print(f'轮次 {epoch+1} 完成 - 准确率: {epoch_accuracy:.2f}%')
        print("-" * 50)
    
    print("训练完成!")
    return train_losses, train_accuracies

# ==================== 测试函数 ====================
def test_model(net, testloader):
    """测试模型性能"""
    net.eval()  # 设置为评估模式
    correct = 0
    total = 0
    class_correct = list(0. for i in range(10))
    class_total = list(0. for i in range(10))
    
    with torch.no_grad():  # 测试时不需要计算梯度
        for inputs, labels in testloader:
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = net(inputs)
            _, predicted = torch.max(outputs, 1)
            
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            
            # 计算每个类别的准确率
            c = (predicted == labels).squeeze()
            for i in range(labels.size(0)):
                label = labels[i]
                class_correct[label] += c[i].item()
                class_total[label] += 1
    
    # 总体准确率
    overall_accuracy = 100 * correct / total
    print(f'测试集总体准确率: {overall_accuracy:.2f}%')
    
    # 每个类别的准确率
    print("\n各类别准确率:")
    for i in range(10):
        if class_total[i] > 0:
            accuracy = 100 * class_correct[i] / class_total[i]
            print(f'{classes[i]}: {accuracy:.2f}%')
    
    return overall_accuracy

# ==================== 开始训练 ====================
print("开始训练模型(这可能需要几分钟)...")
train_losses, train_accuracies = train_model(net, trainloader, epochs=3)

# ==================== 测试模型 ====================
print("\n开始测试模型...")
test_accuracy = test_model(net, testloader)

# ==================== 可视化结果 ====================
def plot_training_history(train_losses, train_accuracies):
    """可视化训练过程"""
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
    
    # 损失曲线
    ax1.plot(train_losses)
    ax1.set_title('训练损失')
    ax1.set_xlabel('轮次')
    ax1.set_ylabel('损失')
    
    # 准确率曲线
    ax2.plot(train_accuracies)
    ax2.set_title('训练准确率')
    ax2.set_xlabel('轮次')
    ax2.set_ylabel('准确率 (%)')
    
    plt.tight_layout()
    plt.savefig('training_history.png', dpi=150, bbox_inches='tight')
    plt.show()

# 如果有训练历史就绘制
if len(train_losses) > 0:
    plot_training_history(train_losses, train_accuracies)

# ==================== 预测示例 ====================
def predict_samples():
    """展示一些预测结果"""
    net.eval()
    dataiter = iter(testloader)
    images, labels = next(dataiter)
    images, labels = images.to(device), labels.to(device)
    
    # 进行预测
    with torch.no_grad():
        outputs = net(images)
        _, predicted = torch.max(outputs, 1)
    
    # 移回CPU用于显示
    images = images.cpu()
    labels = labels.cpu()
    predicted = predicted.cpu()
    
    # 反标准化
    def denormalize(tensor):
        mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
        std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
        return tensor * std + mean
    
    # 显示前8个预测结果
    fig, axes = plt.subplots(2, 4, figsize=(15, 8))
    for i in range(8):
        img = denormalize(images[i])
        img = torch.clamp(img, 0, 1)
        
        row, col = i // 4, i % 4
        axes[row, col].imshow(img.permute(1, 2, 0))
        
        # 标题显示真实标签和预测结果
        true_label = classes[labels[i]]
        pred_label = classes[predicted[i]]
        color = 'green' if labels[i] == predicted[i] else 'red'
        
        axes[row, col].set_title(f'真实: {true_label}\n预测: {pred_label}', 
                                color=color, fontsize=10)
        axes[row, col].axis('off')
    
    plt.tight_layout()
    plt.savefig('prediction_results.png', dpi=150, bbox_inches='tight')
    plt.show()

# 显示预测结果
predict_samples()

# ==================== 保存模型 ====================
# 保存训练好的模型
torch.save(net.state_dict(), 'cifar10_model.pth')
print("\n模型已保存为 'cifar10_model.pth'")

print("\n" + "="*50)
print(" 项目完成!")
print("✅ 成功训练了一个图像分类器")
print("✅ 学会了PyTorch的基本用法")
print("✅ 理解了动态计算图的优势")
print("="*50)

运行效果

控制台输出

使用设备: cuda
正在下载和准备数据...
训练集大小: 50000
测试集大小: 10000
类别数量: 10

模型总参数数量: 1,250,858

开始训练...
==================================================
轮次 [1/3], 批次 [100/1563], 损失: 1.8234
轮次 [1/3], 批次 [200/1563], 损失: 1.6789
...
轮次 1 完成 - 准确率: 45.67%
--------------------------------------------------
轮次 2 完成 - 准确率: 62.34%
--------------------------------------------------
轮次 3 完成 - 准确率: 71.23%
--------------------------------------------------
训练完成!

开始测试模型...
测试集总体准确率: 68.45%

各类别准确率:
飞机: 72.3%
汽车: 78.1%
鸟: 58.9%
猫: 51.2%
鹿: 65.4%
狗: 59.7%
青蛙: 76.8%
马: 71.5%
船: 79.2%
卡车: 71.4%

模型已保存为 'cifar10_model.pth'

 项目完成!
✅ 成功训练了一个图像分类器
✅ 学会了PyTorch的基本用法
✅ 理解了动态计算图的优势

生成的文件

  • sample_images.png: 数据集示例图片
  • training_history.png: 训练过程可视化
  • prediction_results.png: 预测结果展示
  • cifar10_model.pth: 训练好的模型文件

预期结果说明

  1. 准确率应该在60-75%之间:对于简单的CNN模型,这是合理的表现
  2. 损失应该逐渐下降:表示模型正在学习
  3. 某些类别可能表现更好:比如卡车、飞机比猫、狗更容易识别

常见问题

Q1: 出现 CUDA out of memory 错误

原因: GPU显存不足
解决方法:

# 方法1:减小批次大小
trainloader = DataLoader(trainset, batch_size=16, shuffle=True)  # 从32改为16

# 方法2:使用CPU
device = torch.device("cpu")

Q2: 训练速度很慢,怎么办?

解决方法:

  • 确保使用GPU:device = torch.device("cuda")
  • 减少训练轮次:epochs=1
  • 使用更小的数据集进行测试

Q3: 准确率很低,如何改进?

改进方法:

# 1. 增加训练轮次
train_model(net, trainloader, epochs=10)

# 2. 调整学习率
optimizer = optim.Adam(net.parameters(), lr=0.0001)  # 降低学习率

# 3. 数据增强
transform_train = transforms.Compose([
    transforms.RandomHorizontalFlip(),  # 随机水平翻转
    transforms.RandomCrop(32, padding=4),  # 随机裁剪
    transforms.ToTensor(),
    transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
])

Q4: 如何加载保存的模型?

# 加载模型
net = CIFAR10Net()
net.load_state_dict(torch.load('cifar10_model.pth'))
net.eval()  # 设置为评估模式

Q5: 想要预测自己的图片怎么办?

from PIL import Image

def predict_single_image(image_path):
    # 加载和预处理图片
    image = Image.open(image_path).convert('RGB')
    transform = transforms.Compose([
        transforms.Resize((32, 32)),  # CIFAR-10是32x32像素
        transforms.ToTensor(),
        transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
    ])
    
    image_tensor = transform(image).unsqueeze(0)  # 添加批次维度
    
    # 预测
    with torch.no_grad():
        output = net(image_tensor)
        _, predicted = torch.max(output, 1)
        
    return classes[predicted.item()]

# 使用示例
# result = predict_single_image('my_image.jpg')
# print(f"预测结果: {result}")

课后练习

初级练习

  • 修改网络结构,增加一层卷积层
  • 尝试不同的优化器(SGD vs Adam)
  • 调整学习率,观察对训练的影响

中级练习

  • 添加数据增强技术提高准确率
  • 实现早停法防止过拟合
  • 可视化模型学到的特征图

高级练习

  • 使用预训练的ResNet模型进行迁移学习
  • 实现模型集成提高性能
  • 部署模型到Web服务

你可能感兴趣的:(0基础学AI,人工智能,pytorch,python,机器学习,sklearn,深度学习)