如何在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__:自动遍历cat和dog子文件夹,收集所有有效图像路径和对应标签,不用手动整理标签文件__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的逻辑回归代码无缝迁移过来,而且代码结构非常整洁,后续要调整预处理步骤(比如加数据增强)也很方便。如果还有问题,比如模型评估、测试集推理,随时问我! 😊




