You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在PyTorch中基于猫狗图像实现逻辑回归二分类器的预处理与训练?

如何在PyTorch中基于猫狗图像实现逻辑回归二分类器的预处理与训练?

兄弟,我太懂你这种从MNIST那种规整到离谱的数据集,突然跳到真实世界猫狗图像的迷茫了!MNIST的图都是统一大小的灰度图,直接就能喂模型,但猫狗图不仅是RGB的,尺寸还乱七八糟,文件夹结构也得自己处理。我刚学PyTorch的时候也卡过这一步,给你捋一套清晰、可复用的流程,保证你能跑通!


一、核心需求拆解

先把你要做的事拆成可落地的步骤,避免代码混乱:

  • 遍历猫狗数据集的文件夹,自动给猫/狗打标签(比如猫=0,狗=1)
  • 把每张RGB图转成灰度图,统一缩放到固定尺寸(比如64×64)
  • 把预处理后的图像转成PyTorch张量,喂给逻辑回归模型
  • 用自定义Dataset类把这些步骤优雅整合,再用DataLoader批量加载

二、自定义Dataset类的正确姿势(核心)

PyTorch的Dataset是连接原始数据和模型的核心桥梁,必须重写__len__(返回数据总量)和__getitem__(返回单条数据的图像+标签)。下面是针对猫狗数据集的定制实现:

import os
from PIL import Image
from torch.utils.data import Dataset

class CatDogDataset(Dataset):
    def __init__(self, root_dir, transform=None):
        self.root_dir = root_dir  # 数据集根目录,比如"./train"
        self.transform = transform  # 预处理流水线
        self.image_paths = []
        self.labels = []

        # 遍历文件夹,自动收集所有图像路径和标签
        # 假设你的数据集结构是:root_dir/cat/xxx.jpg、root_dir/dog/xxx.jpg
        for label, class_name in enumerate(["cat", "dog"]):
            class_folder = os.path.join(self.root_dir, class_name)
            # 遍历当前类下的所有图像文件
            for img_filename in os.listdir(class_folder):
                img_path = os.path.join(class_folder, img_filename)
                # 跳过非图像文件(比如隐藏文件)
                if img_filename.lower().endswith((".jpg", ".jpeg", ".png")):
                    self.image_paths.append(img_path)
                    self.labels.append(label)  # cat对应0,dog对应1

    def __len__(self):
        # 返回数据集总样本数
        return len(self.image_paths)

    def __getitem__(self, idx):
        # 加载单张图像并预处理
        try:
            img_path = self.image_paths[idx]
            # 加载图像并转成灰度图('L'表示8位灰度图)
            image = Image.open(img_path).convert("L")
            label = self.labels[idx]

            # 应用预处理流水线(比如resize、转张量)
            if self.transform:
                image = self.transform(image)

            return image, label
        except Exception as e:
            # 跳过损坏的图像(数据集里偶尔会有坏图)
            print(f"跳过损坏图像:{self.image_paths[idx]},错误:{str(e)}")
            return self.__getitem__((idx + 1) % len(self))

代码解释:

  • __init__:自动遍历catdog子文件夹,收集所有有效图像路径和对应标签,不用手动整理标签文件
  • __getitem__:负责加载单张图像,转灰度,应用预处理,还加了异常处理跳过坏图(我之前踩过坏图导致训练崩溃的坑!)
  • 支持传入自定义预处理流水线,灵活性拉满

三、预处理流水线的最佳实践

torchvision.transforms.Compose把所有预处理步骤组合成一个流水线,代码更整洁,还能灵活调整:

from torchvision import transforms

# 组合预处理步骤
preprocess = transforms.Compose([
    transforms.Resize((64, 64)),  # 统一缩放到64×64,可根据需求改(比如32×32)
    transforms.ToTensor(),  # 把PIL图像转成PyTorch张量,形状变为(1, 64, 64)(灰度图单通道)
    transforms.Normalize(mean=[0.5], std=[0.5])  # 归一化到[-1,1],帮助模型更快收敛(灰度图只有1个通道,所以均值/标准差是单值)
])

可选调整:

  • 如果不想手动转灰度,也可以用transforms.Grayscale(num_output_channels=1)替代Image.convert("L"),更符合PyTorch规范
  • 若想加快训练速度,可以把Resize的尺寸改小(比如32×32),代价是轻微的精度损失

四、用DataLoader批量加载数据

DataLoader负责把Dataset里的数据打包成批量、打乱顺序、多线程加载,是训练的必备组件:

from torch.utils.data import DataLoader

# 创建训练集Dataset和DataLoader
train_dataset = CatDogDataset(root_dir="./train", transform=preprocess)
train_loader = DataLoader(
    train_dataset,
    batch_size=32,  # 批量大小,根据你的显存调整(显存小就设16)
    shuffle=True,  # 训练时必须打乱数据,避免模型学到顺序信息
    num_workers=2,  # 多线程加载数据,加快速度(一般设为CPU核心数的一半)
    drop_last=True  # 丢弃最后一个不满批量的样本,避免训练时维度不匹配
)

# 验证集同理,只需要把root_dir改成验证集路径,shuffle设为False
val_dataset = CatDogDataset(root_dir="./val", transform=preprocess)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

五、逻辑回归模型与训练闭环

因为是二分类,逻辑回归本质就是一个线性层(输入是展平的灰度图向量,输出是1个logit值)。这里结合你之前做MNIST的经验,补全训练流程:

import torch
import torch.nn as nn
import torch.optim as optim

# 定义逻辑回归模型
class LogisticRegression(nn.Module):
    def __init__(self, input_size, num_classes=1):
        super().__init__()
        self.flatten = nn.Flatten()  # 把(1,64,64)的张量展平成(4096,)的向量
        self.linear = nn.Linear(input_size, num_classes)  # 线性层,输出1个logit值

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear(x)
        return logits

# 初始化模型、损失函数、优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
input_size = 64 * 64  # 64×64灰度图展平后的维度
model = LogisticRegression(input_size=input_size).to(device)

# 二分类用BCEWithLogitsLoss(自带sigmoid,数值稳定性更好)
criterion = nn.BCEWithLogitsLoss()
# 用SGD优化器,学习率可以根据训练情况调整
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# 训练循环
num_epochs = 15
for epoch in range(num_epochs):
    model.train()  # 切换到训练模式
    train_loss = 0.0

    for images, labels in train_loader:
        # 把数据移到GPU(如果有的话)
        images = images.to(device)
        # 把标签转成float类型,并且扩展维度(BCE要求标签形状和输出一致)
        labels = labels.float().unsqueeze(1).to(device)

        # 前向传播
        logits = model(images)
        loss = criterion(logits, labels)

        # 反向传播+更新参数
        optimizer.zero_grad()  # 清空梯度
        loss.backward()  # 反向传播计算梯度
        optimizer.step()  # 更新参数

        train_loss += loss.item() * images.size(0)

    # 计算当前epoch的平均损失
    avg_train_loss = train_loss / len(train_loader.dataset)
    print(f"Epoch [{epoch+1}/{num_epochs}],训练损失:{avg_train_loss:.4f}")

    # 每轮训练后验证一下(可选)
    model.eval()  # 切换到评估模式
    val_loss = 0.0
    with torch.no_grad():  # 验证时不需要计算梯度
        for images, labels in val_loader:
            images = images.to(device)
            labels = labels.float().unsqueeze(1).to(device)
            logits = model(images)
            loss = criterion(logits, labels)
            val_loss += loss.item() * images.size(0)
    avg_val_loss = val_loss / len(val_loader.dataset)
    print(f"验证损失:{avg_val_loss:.4f}\n")

关键注意点:

  • 二分类用BCEWithLogitsLoss比单独用sigmoid+BCELoss数值稳定性更好,避免梯度消失
  • 标签必须转成float类型,并且用unsqueeze(1)把形状从(batch_size,)改成(batch_size,1),和模型输出维度匹配
  • 训练时要切换到model.train(),验证时切换到model.eval(),避免影响批量归一化、 dropout等层的行为

六、我之前踩过的坑

  • 坏图像问题:有些公开数据集里会有损坏的图像,一定要加异常处理跳过,否则训练到一半会崩溃
  • 灰度图通道数:归一化时要注意用单通道的均值/标准差,不能直接抄RGB的三个值
  • 学习率调整:逻辑回归的学习率不能太小,否则收敛极慢;如果损失不下降,可以试试把学习率调到0.01或0.1
  • 批量大小:如果显存不足,把batch_size改小,或者用num_workers=0(单线程加载)

按照这个流程,你就能把之前MNIST的逻辑回归代码无缝迁移过来,而且代码结构非常整洁,后续要调整预处理步骤(比如加数据增强)也很方便。如果还有问题,比如模型评估、测试集推理,随时问我! 😊

火山引擎 最新活动