PyTorch学习笔记
数据读取
使用Dataset进行数据读取
在PyTorch中,Dataset是一个抽象基类,其规定了所有数据集必须具备两个功能:
-
__len__:告诉模型这里一共有多少张图 -
__getitem__:告诉模型一个编号idx,把对应的图片和标签给你
__getitem__ 的按需读取并没有在init中把所有图片都加载进内存,只有当代码运行到dataset[idx]时,Image.open才会去磁盘读取这一张图。
python
import os
from PIL import Image
from torch.utils.data import Dataset # 导入Dataset基类
"""
定义一个名为MyData的类,继承PyTorch提供的Dataset类
"""
class MyDataSet(Dataset):
# 初始化方法:用于获取数据的信息
def __init__(self, root_dirt, label_dirt):
"""
:param root_dirt: 数据集的根目录路径
:param label_dirt: 标签的文件夹名称
"""
self.root_dirt = root_dirt
self.label_dirt = label_dirt
# 使用os.path.join拼接路径
self.path = os.path.join(self.root_dirt, self.label_dirt)
# 获取该目录下所有文件的文件名,以列表的形式存储
self.img_path = os.listdir(self.path)
# 获取单个数据样本的方法
def __getitem__(self, idx):
"""
:param idx: 数据的编号,如第0张图,第1张图
:return: 返回处理后的图像和对应的标签
"""
# 1. 根据索引idx从文件夹列表中获取对应的文件名
img_name = self.img_path[idx]
# 2. 拼接出完整路径
img_item_path = os.path.join(self.root_dirt, self.label_dirt, img_name)
# 3. 使用PIL库的open打开图片文件
img = Image.open(img_item_path)
# 4. 获取标签,使用文件夹名称作为标签名称
label = self.label_dirt
# 5. 返回一个元组:(图片,标签)
return img, label
# 获取数据集样本的长度
def __len__(self):
return len(self.img_path)
"""
代码测试
"""
# 1. 设置数据集所在的绝对路径
root_dirt = r"D:\RecommendationSystem\PyTorch学习\hymenoptera_data\train"
ants_label_dir = "ants" # 蚂蚁文件夹
bees_label_dir = "bees" # 蜜蜂文件夹
# 2. 创建蚂蚁数据集的实例对象
ants_dataset = MyDataSet(root_dirt, ants_label_dir)
# 3. 访问数据集,通过索引触发__getitem__方法
img, label = ants_dataset[0]
img.show()
使用DataLoader进行批量读取
DataLoader用于批量读取,在神经网络中常常输入一个batch进行训练。常见的参数有Batch Size,表示批次大小;Shuffle,表示输入前对样本顺序进行洗牌。
python
from torch.utils.data import DataLoader
from torch.utils.data import Dataset
from torchvision import transforms
from PIL import Image
import os
class MyData(Dataset):
def __init__(self, root_dir, label_dir, transforms=None):
self.root_dir = root_dir
self.label_dir = label_dir
self.path = os.path.join(self.root_dir, self.label_dir)
self.img_path = os.listdir(self.path) # 用os的listdir获取数据集的图片列表
self.transform = transforms # 输入图像转换操作
def __getitem__(self, idx):
img_name = self.img_path[idx] # 根据idx获取单张图片
img_item_path = os.path.join(self.root_dir, self.label_dir, img_name)
img = Image.open(img_item_path).convert("RGB") # 强制转换成三通道
if self.transform:
img = self.transform(img)
label = self.label_dir
return img, label
def __len__(self):
return len(self.img_path)
# 定义一个变换,因为DataLoader的输入是张量
img_transforms = transforms.Compose(
[
transforms.Resize((256, 256)), # 进行缩放
transforms.RandomHorizontalFlip(), # 随机水平翻转,数据增强,
transforms.ToTensor(), # 转换为张量
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.1, 0.1, 0.1]), # 标准化
]
)
# 创建蚂蚁数据集实例
root_dir = r"D:\RecommendationSystem\PyTorch学习\hymenoptera_data\train" # 根目录,存储数据级的目录
ants_label_dir = "ants" # 蚂蚁类别的数据集目录
bees_label_dir = "bees" # 蜜蜂类别的数据集目录
ants_dataset = MyData(root_dir, ants_label_dir, transforms=img_transforms)
MyDataLoader = DataLoader(
dataset=ants_dataset, batch_size=32, shuffle=True, drop_last=False
)
for data in MyDataLoader:
imgs, labels = data
print(f"当前批次的图片形状:{imgs.shape}")
使用Transforms进行数据转换
py
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
# 1. 加载图片
img = Image.open(r"C:\Users\13680\Pictures\猫咪头像.jpg")
# 2. 从图片到张量 (ToTensor)
# 目的:将 PIL 图片转为模型能处理的浮点张量,并将像素范围从 [0, 255] 归一化到 [0, 1]
# 还会自动把维度顺序从 [H, W, C] 调整为 [C, H, W]
to_tensor = transforms.ToTensor()
img_tensor = to_tensor(img)
print(f"张量形状 [通道, 高, 宽]: {img_tensor.shape}")
# 3. 尺寸缩放 (Resize)
# 目的:统一输入尺寸。模型网络层的参数是固定的,必须接受相同大小的图片
resize = transforms.Resize((512, 512))
img_resized = resize(img)
print(f"缩放后形状: {to_tensor(img_resized).shape}")
# 4. 中心裁剪 (CenterCrop)
# 目的:移除背景干扰,只关注图片最中心的核心特征,比如猫脸
centercrop = transforms.CenterCrop(300)
img_croped = centercrop(img)
# 5. 随机裁剪 (RandomCrop)
randomcrop = transforms.RandomCrop(size=(100, 200))
img_randomcroped = randomcrop(img)
# 6. 随机水平翻转 (RandomHorizontalFlip)
# 目的:模拟物体从不同方向出现的情况。p=0.5 表示一半的概率翻转,一半不翻
random_horizontal_flip = transforms.RandomHorizontalFlip(p=0.5)
flipped_image = random_horizontal_flip(img)
# 绘图展示对比
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.title("Original Image")
plt.imshow(img)
plt.axis("off") # 关闭坐标轴
plt.subplot(1, 2, 2)
plt.title("Flipped Image")
plt.imshow(flipped_image)
plt.axis("off")
# plt.show()
# 7. 填充 (Pad)
# 目的:给图片加边框。在某些检测任务中,为了保持比例会先填充再缩放
pad = transforms.Pad(padding=10, fill=0) # 上下左右各填充 10 像素,黑色填充
img_padded = pad(img)
print(f"填充后形状: {to_tensor(img_padded).shape}")
# 8. 标准化 (Normalize)
# 公式:output = (input - mean) / std
# 目的:使数据分布在 0 附近。均值 0.5,标准差 0.1 是为了让大多数像素落在 [-5, 5] 之间
mean = [0.5, 0.5, 0.5]
std = [0.1, 0.1, 0.1]
normalize = transforms.Normalize(mean=mean, std=std)
img_normalized = normalize(to_tensor(img))
print(f"标准化后的像素均值: {img_normalized.mean()}")
# 9. 操作组合 (Compose)
# 目的:将上述所有步骤串联成一个流水线,以后只需调用 img_transforms(img)
img_transforms = transforms.Compose(
[
transforms.Resize((256, 256)), # 1. 缩放
transforms.RandomHorizontalFlip(), # 2. 增强
transforms.ToTensor(), # 3. 转张量
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.1, 0.1, 0.1]), # 4. 标准化
]
)
img_transformed = img_transforms(img)
神经网络
Module的使用
输出通道 (Out Channels)其实就是卷积核(Filter)的数量。每一个卷积核负责寻找一种特征。设置64个输出通道,就代表想让模型在这一层提取64种不同的高级特征。
当kernel_size=3,padding=1,stride=1时,卷积后的图片长宽不变,公式如下:
Output=Input+2×Padding−KernelSize+1Stride Output = \frac{Input + 2 \times Padding - KernelSize + 1}{Stride} Output=StrideInput+2×Padding−KernelSize+1
这让我们在设计网络时,只需要担心"池化"带来的减半,而不需要担心卷积层让图片变小。
如果没有激活函数F.relu,无论你堆叠多少层卷积,结果依然只是一个复杂的线性公式。
py
import torch
import torch.nn as nn
import torch.nn.functional as F
class Mymodel(nn.Module):
def __init__(self, num_classes):
super().__init__()
# 1. 卷积层
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
"""
输入通道数,输出通道数,填充大小
后一层的输入通道数要等于前一层的输出通道数
padding = (kernel_size - 1) / 2
"""
# 2. 池化层
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
"""
kernel_size是窗口大小,stride是窗口每次滑动的距离,二者相等时,池化后的图像大小减半
"""
# 3. 全连接层
self.fc1 = nn.Linear(64 * 64 * 64, 256) # 假设输入图像的形状:(3, 512, 512)
self.fc2 = nn.Linear(256, num_classes)
def forward(self, x):
# 卷积 -> 激活 -> 池化
x = self.pool(
F.relu(self.conv1(x))
) # 卷积后:(16, 512, 512);池化后:(16, 256, 256)
x = self.pool(
F.relu(self.conv2(x))
) # 卷积后:(32, 256, 256);池化后:(32, 128, 128)
x = self.pool(
F.relu(self.conv3(x))
) # 卷积后:(64, 128, 128);池化后:(64, 64, 64)
# 展平,进入全连接层前,需要把多维张量拉成一维向量
x = torch.flatten(x, 1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
model = Mymodel(num_classes=2)
print(model)
池化的使用
最大池化只看最强的信号,它能很好地提取边缘、纹理等剧烈变化的特征。平均池化 考虑所有像素,常用于网络的最后一层,用来整合全局信息。
一个非常普遍的工程习惯是kernel_size == stride。如果kernel_size=2,stride=2,图片尺寸直接除以2。如果stride小于kernel_size,池化窗口会产生重叠,这在如AlexNet等某些老模型中被认为能抑制过拟合。
python
import torch
import torch.nn as nn
# 模拟一个 4x4 的输入特征图 (Batch=1, Channel=1, H=4, W=4)
input_data = torch.tensor(
[[[[10, 20, 5, 0],
[30, 40, 0, 5],
[1, 2, 100, 80],
[0, 1, 90, 70]]]],
dtype=torch.float32,
)
# 1. 最大池化 (Max Pooling)
# 目的:保留窗口内最显著的特征(数值最大的那个)
# 窗口 2x2,步长 2:意味着图片长宽都会减半
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
# 2. 平均池化 (Average Pooling)
# 目的:保留窗口内的整体背景信息(计算平均值)
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
# 执行池化
output_max = max_pool(input_data)
output_avg = avg_pool(input_data)
print("--- 原始输入 (4x4) ---")
print(input_data[0,0])
print("\n--- 最大池化结果 (2x2) ---")
print(output_max[0,0])
print("\n--- 平均池化结果 (2x2) ---")
print(output_avg[0,0])
"""
--- 原始输入 (4x4) ---
tensor([[ 10., 20., 5., 0.],
[ 30., 40., 0., 5.],
[ 1., 2., 100., 80.],
[ 0., 1., 90., 70.]])
--- 最大池化结果 (2x2) ---
tensor([[ 40., 5.],
[ 2., 100.]])
--- 平均池化结果 (2x2) ---
tensor([[25.0000, 2.5000],
[ 1.0000, 85.0000]])
"""
线性层的使用
py
import torch
import torch.nn as nn
# 模拟一个卷积层输出的特征图 (Batch=1, Channel=64, H=4, W=4)
input_feature_map = torch.randn(1, 64, 4, 4)
# --- 1. Flatten (展平层) ---
# 将三维特征 (C, H, W) 转化为一维特征向量。默认从 dim=1 开始展平,保留 dim=0 的 Batch 维度。
flatten = nn.Flatten()
output_flatten = flatten(input_feature_map)
# --- 2. Linear (线性层/全连接层) ---
# 每一个输出节点都连接了输入的所有节点,用于学习全局组合逻辑。
fc = nn.Linear(in_features=1024, out_features=256)
output_fc = fc(output_flatten)
# --- 3. Dropout (随机失活层) ---
# 强制让网络在训练时不要依赖某些特定的神经元。
# p=0.5 表示每次训练时,随机把一半的神经元"关掉"(置为0)。
dropout = nn.Dropout(p=0.5)
output_dropout = dropout(output_fc)
print(f"输入特征图形状: {input_feature_map.shape}") # [1, 64, 4, 4]
print(f"展平后形状: {output_flatten.shape}") # [1, 1024]
print(f"线性层输出形状: {output_fc.shape}") # [1, 256]
非线性激活的使用
py
import torch
import torch.nn as nn
# 模拟一个包含正数、负数和零的输入张量
input_data = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
# 1. ReLU 激活函数:简单高效
# 逻辑:max(0, x)
relu = nn.ReLU()
# 2. Sigmoid 激活函数:压缩映射
sigmoid = nn.Sigmoid()
# 执行激活操作
output_relu = relu(input_data)
output_sigmoid = sigmoid(input_data)
print(f"输入数据: {input_data}")
print(f"ReLU 输出: {output_relu}")
print(f"Sigmoid 输出: {output_sigmoid}")
模型训练
反向传播手动实现
py
import torch
# 1. 初始化
x = torch.tensor([0.5])
y = torch.tensor([1.0])
w1 = torch.tensor([0.2])
w2 = torch.tensor([0.5])
learning_rate = 0.1
for epoch in range(3):
# --- A. 前向传播 ---
z1 = w1 * x
a1 = torch.sigmoid(z1)
z2 = w2 * a1
a2 = torch.sigmoid(z2)
C = 0.5 * (a2 - y) ** 2
# --- B. 反向传播 ---
# --- 第一步:计算 w2 的梯度 (输出层) ---
# 公式:dC/dw2 = (dC/da2) * (da2/dz2) * (dz2/dw2)
dC_da2 = a2 - y # Loss 对预测值的导数
da2_dz2 = a2 * (1 - a2) # Sigmoid 导数
dz2_dw2 = a1 # 因为 z2 = w2 * a1,对 w2 求导剩下 a1
grad_w2 = dC_da2 * da2_dz2 * dz2_dw2
# --- 第二步:计算 w1 的梯度 (隐含层) ---
# 公式:dC/dw1 = [(dC/da2) * (da2/dz2) * (dz2/da1)] * (dz1/dw1) * (da1/dz1)
# 方括号的内容表示半成品a1的改变对总损失C的影响
dz2_da1 = w2
dz1_dw1 = x
da1_dz1 = a1 * (1 - a1)
# 链式相乘:(dC/da2 * da2/dz2) 即是上一层的误差传递,再乘以 (dz2/da1 * da1/dz1 * dz1/dw1)
grad_w1 = dC_da2 * da2_dz2 * dz2_da1 * dz1_dw1 * da1_dz1
# --- C. 权重更新 ---
w1 = w1 - learning_rate * grad_w1
w2 = w2 - learning_rate * grad_w2
# 打印查看每轮的变化
print(f"Epoch {epoch + 1}: Loss = {C.item():.4f}, w1 = {w1.item():.4f}, w2 = {w2.item():.4f}")
完整训练示例
py
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, ConcatDataset
from torchvision import transforms
from PIL import Image
import os
# --- 1. 数据集定义 ---
class MyData(torch.utils.data.Dataset):
def __init__(self, root_dir, label_dir, transform=None):
self.root_dir = root_dir
self.label_dir = label_dir
self.path = os.path.join(self.root_dir, self.label_dir)
self.img_path = os.listdir(self.path)
self.transform = transform
# ants -> 0, bees -> 1
self.label = 0 if label_dir == "ants" else 1
def __getitem__(self, idx):
img_name = self.img_path[idx]
img_item_path = os.path.join(self.path, img_name)
img = Image.open(img_item_path).convert("RGB") # 保证三通道
if self.transform:
img = self.transform(img)
return img, self.label
def __len__(self):
return len(self.img_path)
# --- 2. 配置变换 ---
data_transforms = transforms.Compose(
[
transforms.Resize((64, 64)),
transforms.ToTensor(),
transforms.Normalize([0.5, 0.5, 0.5], [0.2, 0.2, 0.2]),
]
)
# --- 3. 准备加载器 ---
train_root = r"D:\RecommendationSystem\PyTorch学习\hymenoptera_data\train"
val_root = r"D:\RecommendationSystem\PyTorch学习\hymenoptera_data\val"
# 训练集
train_ants = MyData(train_root, "ants", transform=data_transforms)
train_bees = MyData(train_root, "bees", transform=data_transforms)
train_loader = DataLoader(
ConcatDataset([train_ants, train_bees]), batch_size=8, shuffle=True
)
# 验证集
val_ants = MyData(val_root, "ants", transform=data_transforms)
val_bees = MyData(val_root, "bees", transform=data_transforms)
val_loader = DataLoader(
ConcatDataset([val_ants, val_bees]), batch_size=8, shuffle=False
)
# --- 4. 模型定义 ---
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 16, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(16 * 32 * 32, 2),
)
def forward(self, x):
return self.model(x)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNN().to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# --- 5. 训练与评估循环 ---
for epoch in range(10):
# 训练阶段
model.train() # 切换到训练模式
train_loss = 0
for imgs, labels in train_loader:
imgs, labels = imgs.to(device), labels.to(device)
outputs = model(imgs)
loss = loss_fn(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
# 评估阶段
model.eval() # 切换到评估模式
val_loss = 0
total_accuracy = 0
with torch.no_grad(): # 评估时不计算梯度,节省内存和计算量
for imgs, labels in val_loader:
imgs, labels = imgs.to(device), labels.to(device)
outputs = model(imgs)
loss = loss_fn(outputs, labels)
val_loss += loss.item()
# 计算准确率
predictions = outputs.argmax(1) # 获取概率最大的类别索引
total_accuracy += (predictions == labels).sum().item()
avg_acc = total_accuracy / len(val_loader.dataset)
print(f"Epoch {epoch}: Train Loss: {train_loss:.3f}, Val Acc: {avg_acc:.2%}")
# --- 6. 保存模型 ---
os.makedirs("models", exist_ok=True)
torch.save(model.state_dict(), "models/best_model.pth")
损失函数
py
import torch
import torch.nn as nn
# 设置随机种子保证结果可复现
torch.manual_seed(42)
# ==========================================
# 场景 A:回归问题 (Regression)
# 常用 L1Loss 或 MSELoss
# ==========================================
# 预测值与真实值
predict = torch.tensor([1.0, 2.0, 3.0])
target = torch.tensor([1.0, 2.0, 5.0])
# 1. L1Loss: 计算平均绝对误差 (Mean Absolute Error, MAE)
# 公式: sum(|predict - target|) / n = (0 + 0 + 2) / 3 = 0.6667
loss_l1 = nn.L1Loss()
res_l1 = loss_l1(predict, target)
# 2. MSELoss: 计算均方误差 (Mean Squared Error)
# 公式: sum((predict - target)^2) / n = (0^2 + 0^2 + 2^2) / 3 = 4 / 3 = 1.3333
loss_mse = nn.MSELoss()
res_mse = loss_mse(predict, target)
print("--- 回归损失 (Regression Losses) ---")
print(f"L1 Loss (MAE): {res_l1.item():.4f}")
print(f"MSE Loss: {res_mse.item():.4f}\n")
# ==========================================
# 场景 B:分类问题 (Classification)
# 常用 CrossEntropyLoss
# ==========================================
# 假设我们在做一个三分类任务(如:猫、狗、猪)
# 输入 x: 模型给出的每个类别的得分 (Logits),形状为 (样本数, 类别数)
# 目标 y: 真实类别的索引 (Index),形状为 (样本数)
x = torch.tensor([[0.1, 0.2, 0.3]]) # 模型预测各类的"信心"
y = torch.tensor([1]) # 真实标签是索引 1 (比如"狗")
# CrossEntropyLoss 会自动帮我们做 Softmax + Log + NLLLoss
loss_ce = nn.CrossEntropyLoss()
res_ce = loss_ce(x, y)
"""
对网络输出的样本分数使用softmax转换成概率
对真实标签下的预测样本取负对数似然,即为损失
"""
print("--- 分类损失 (Classification Loss) ---")
print(f"CrossEntropy Loss: {res_ce.item():.4f}")