李哥深度学习班 图片分类实战
其中不带标签的数据只有x没有y(标签即分类)

其中不带标签的数据只有x没有y(标签即分类)
随机种子
def seed_everything(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
#################################################################
seed_everything(0) #用于复现结果, 随机种子:把结果固定下来 每次都一样
###############################################
1、torch.manual_seed(seed):
为CPU设置随机种子,使得PyTorch中涉及CPU的随机操作(如初始化权重、数据洗牌)可重现。
2、torch.cuda.manual_seed(seed):
为当前GPU设置随机种子。如果使用多个GPU,只设置当前GPU(通常是第一个)的种子。
3、torch.cuda.manual_seed_all(seed):
为所有GPU设置随机种子。这在多GPU环境下使用,确保所有GPU的随机初始化一致。
4、torch.backends.cudnn.benchmark = False:
禁用cuDNN的自动寻找最优化卷积算法的功能。因为如果启用,cuDNN会根据硬件和输入大小自动选择算法,这可能导致每次运行的结果不一致。
5、torch.backends.cudnn.deterministic = True:
启用cuDNN的确定性模式。这可能会降低性能,但可以保证每次运行的结果相同。注意,这个设置只影响cuDNN,但结合benchmark=False可以确保确定性。
6、random.seed(seed):
设置Python内置随机模块的种子,影响所有使用该模块的随机操作。
7、np.random.seed(seed):
设置NumPy随机生成器的种子,确保使用NumPy的随机操作可重现。
8、os.environ['PYTHONHASHSEED'] = str(seed):
设置环境变量PYTHONHASHSEED,这会影响Python的哈希函数(例如,在字典和集合中)。设置相同的种子可以使得哈希值可预测,从而在某些情况下确保可重复性(例如,数据集的划分)。
最后调用seed_everything(0),将随机种子设置为0。这意味着每次运行代码时,只要使用相同的种子,就应该得到相同的结果。只要同一次实验用同一个 seed,结果就能复现!
数据读取(未加半监督情况下)
def read_file(path): #数据集结构 共 11 个文件夹
for i in range(11):
file_dir = path + "/%02d"% i #构造当前类别的文件夹路径
file_list = os.listdir(file_dir) #获取该文件夹下所有文件名列表(如 ["img1.jpg", "img2.png", ...])
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8) #np.zeros创建一个全为0的数组 后续我们会用真实的图像数据覆盖这些 0
yi = np.zeros(len(file_list), dtype=np.uint8)
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list): ## 遍历该文件夹,获得索引j和文件名(图片名)
img_path = os.path.join(file_dir, img_name) ## 把图片名添加到路径形成图片的路径
img = Image.open(img_path) # 根据图片路径打开图片
img = img.resize((HW, HW)) #调整图片大小
xi[j, ...] = img # 第j张完整的图像,形状为 (224, 224, 3),省略号表示对后三维“取全部”等价于 xi[j, :, :, :]
yi[j] = i # i = 第i个文件夹 = 这一堆图片属于第i类 = 标签
#至此已分类11个类别 接下来将所有类别的数据合并为一个统一的训练集。
#为什么合并? 如果不合并,到i=10时,前面的数据和标签都已经被覆盖了,最终读的数据只是最后一个类别的数据
# 注意:若不在循环中累积保存,每次迭代的 xi/yi 会被覆盖,
# 导致最终仅保留最后一类(i=10)的数据,造成训练数据严重缺失。
if i == 0:
X = xi
Y = yi
else:
X = np.concatenate((X, xi), axis=0)
Y = np.concatenate((Y, yi), axis=0)
print("读到了%d个数据" % len(Y))
return X, Y
图片存储
xi 是 4 维的,因为它要同时描述:
“哪张图” + “图中哪个位置” + “哪个颜色通道” —— 共 4 个信息,所以需要 4 个维度。
一张 224×224 的 RGB 图像 = (224, 224, 3) 的 3D 数组。
4 维 = 一摞相册(多个3D图像堆在一起)
y_i 就是一个一维数组,存储各个图片对应的类别(label)
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8) 初始化数组
- 这是 NumPy 的函数,用于创建一个全为 0 的数组;
- 后续我们会用真实的图像数据覆盖这些 0。
| 维度 | 含义 | 举例 |
|---|---|---|
第 0 维:len(file_list) |
当前类别有多少张图片 | 如果文件夹里有 50 张图 → 这个值就是 50 |
第 1 维:HW |
每张图的高度(Height) | HW = 224 → 高 224 像素 |
第 2 维:HW |
每张图的宽度(Width) | 宽也是 224 像素(正方形) |
第 3 维:3 |
颜色通道数(RGB) | 红、绿、蓝三通道 |
dtype=np.uint8 标准数字图像的像素值就是 0~255 的整数 (RGB)没有小数点
X = np.concatenate((X, xi), axis=0) 拼接数组
把两个数组拼接起来,拼接的维度是第 0 维,也就是“增加数组中的图片数”,X仍然是四维数组,只是图片数增加了。
同理,对 Y 进行拼接后,它也仍然是一个一维数组,只是其中的标签数更多了
enumerate()
enumerate() 是 Python 的内置函数,在遍历可迭代对象(如列表、元组等)时,同时获取“索引(index)”和“元素(value)”。
enumerate(iterable, start=0)
iterable:任何可遍历的对象(如list,tuple,string等)- 默认从
0开始计数,也可以指定起始值
fruits = ['apple', 'banana', 'cherry']
for i, fruit in enumerate(fruits):
print(i, fruit)
#0 apple
#1 banana
#2 cherry
数据增强(Data Augmentation)
train_transform = transforms.Compose(
[
transforms.ToPILImage(), #将 PyTorch 张量(Tensor)或 NumPy 数组转换为 PIL Image 对象。方便显示
transforms.RandomResizedCrop(224), #增加数据多样性
transforms.RandomRotation(50), #增加数据多样性
transforms.ToTensor() #把 PIL 或 NumPy 图像转成 PyTorch 能用的标准输入格式
]
)
val_transform = transforms.Compose(
[
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224
transforms.ToTensor()
]
)
transforms.RandomResizedCrop(224), 随机裁剪 + 缩放到固定尺寸(224×224)
为什么用它?
- 模拟“物体在图像中位置和大小不固定”的情况;
- 即使原图很大(如 512×512),也能生成多样化的 224×224 输入;
- 是 ImageNet 等标准数据集训练时的标配增强。
transforms.RandomRotation(50),# 随机旋转 ±50 度
⚠️ 重要原则:训练用增强,验证不用!
- 如果你对这张图做
RandomRotation(50),可能转成 30°、-20°、45°……- 每次预测结果可能不同(有时对,有时错);
- 你无法知道模型到底行不行!
🎯 验证必须是确定性的(deterministic):同一张图,永远得到同一个结果。
⚠️读图用 PIL,进模型用 Tensor
transforms.ToTensor()
数值范围转换(归一化)
# 将像素值从 [0, 255] 的整数范围转换为 [0.0, 1.0] 的浮点数范围
uint8 (0-255) → float32 (0.0-1.0)
数据集类
class food_Dataset(Dataset):
def __init__(self, path, mode="train"):
self.X, self.Y = read_file(path) #一次性将所有图像和标签加载到内存(self.X 是 NumPy 数组,self.Y 是标签数组)
self.Y = torch.LongTensor(self.Y) # 标签转化为长整形
if mode == "train":
self.transform = train_transform # 数据增广
else:
self.transform = val_transform
def __getitem__(self, item): #获取单个样本
return self.transform(self.X[item]), self.Y[item]
def __len__(self): #返回数据长度
return len(self.X)
- 将标签转为
torch.LongTensor(64 位整数); - 为什么?
PyTorch 的分类损失函数(CrossEntropyLoss)要求标签是LongTensor,不能是 float 或 int32。
模型
class myModel(nn.Module):
def __init__(self, num_class): #num_class 分类的数量
super(myModel, self).__init__()
#3 *224 *224 -> 512*7*7 -> 拉直 -》全连接分类
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 64*224*224
self.bn1 = nn.BatchNorm2d(64) #BatchNorm2d与输出通道数保持一致
self.relu = nn.ReLU()
self.pool1 = nn.MaxPool2d(2) #64*112*112
self.layer1 = nn.Sequential(
nn.Conv2d(64, 128, 3, 1, 1), # 128*112*112
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2) #128*56*56
)
self.layer2 = nn.Sequential(
nn.Conv2d(128, 256, 3, 1, 1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2) #256*28*28
)
self.layer3 = nn.Sequential(
nn.Conv2d(256, 512, 3, 1, 1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2) #512*14*14
)
self.pool2 = nn.MaxPool2d(2) #512*7*7
self.fc1 = nn.Linear(25088, 1000) #25088->1000
self.relu2 = nn.ReLU()
self.fc2 = nn.Linear(1000, num_class) #1000-11
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.pool1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.pool2(x)
#拉直
x = x.view(x.size()[0], -1)
# 保持第一维(图片个数,这里等于batchsize)不变,其余全部放入最后一维 用于全连接参数计算
x = self.fc1(x)
x = self.relu2(x)
x = self.fc2(x)
return x
以下两种写法等价
# self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 3*224*224 -> 64*224*224
# self.bn1 = nn.BatchNorm2d(64)
# self.relu1 = nn.ReLU()
# self.pool1 = nn.MaxPool2d(2) # 池化 64*224*224 -> 64*112*112
self.layer1 = nn.Sequential( # 与上面的写法等价,但是更方便
# 3*224*224 -> 64*112*112
nn.Conv2d(3, 64, 3, 1, 1), # 3*224*224 -> 64*224*224
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2), # 池化 64*224*224 -> 64*112*112
# 输出64*112*112的特征图
)
nn.Sequential
nn.Sequential是一个容器,它按照顺序包装多个层,使得这些层可以依次执行。
使用nn.Sequential可以让代码更简洁,特别是在构建连续的层时。它允许我们将整个块视为一个整体,这样在正向传播时只需调用一次。
逐层定义:
在正向传播时,你需要这样写:
x = self.conv1(x)
x = self.bn1(x)
x = self.relu1(x)
x = self.pool1(x)
使用nn.Sequential
在正向传播时,你只需要写:
x = self.layer1(x)
nn.Conv2d--提取局部特征,并输出多通道特征图
torch.nn.Conv2d(
in_channels, # 输入通道数(如 RGB 图像为 3)
out_channels, # 输出通道数(即卷积核/滤波器的数量)
kernel_size, # 卷积核大小(如 3, 5, (3,5))
stride=1, # 步长(默认为 1)
padding=0, # 填充(默认为 0)
dilation=1, # 空洞卷积(默认为 1)
groups=1, # 分组卷积(默认为 1)
bias=True, # 是否使用偏置(默认为 True)
padding_mode='zeros' # 填充模式(默认为 'zeros')
)
nn.BatchNorm2d - 批量归一化
# 函数签名
torch.nn.BatchNorm2d(
num_features, # 需要归一化的通道数 (64)
eps=1e-05, # 数值稳定性小常数,防止除以0
momentum=0.1, # 运行统计量的动量
affine=True, # 是否学习缩放和平移参数
track_running_stats=True # 是否跟踪运行统计量
)
训练流程
def train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path):
model = model.to(device)
plt_train_loss = [] #总训练loss
plt_val_loss = []
plt_train_acc = [] #总训练准确率
plt_val_acc = []
max_acc = 0.0 #准确率最大值
for epoch in range(epochs):
train_loss = 0.0
val_loss = 0.0
train_acc = 0.0
val_acc = 0.0
start_time = time.time()
model.train()
for batch_x, batch_y in train_loader:
x, y= batch_x.to(device), batch_y.to(device)
pred = model(x)
train_bat_loss = loss(pred, y)
train_bat_loss.backward()
optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度
optimizer.zero_grad()
train_loss += train_bat_loss.cpu().item()
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == y.cpu().numpy())
plt_train_loss.append(train_loss / train_loader.__len__()) #每个 batch 平均 loss 的平均值
#train_loss 是所有 batch(批次数) 的 loss 值的累加(求和)
#train_loader.__len__()返回的是批次数(iterations),不是batch_size
plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #记录准确率,正确样本数 / 总样本数 = 准确率(Accuracy)
#train_acc预测正确的样本总数(不是按 batch 平均!)
#train_loader.dataset.__len__()训练集总样本数
model.eval() #验证模式
with torch.no_grad():
for batch_x, batch_y in val_loader:
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
val_bat_loss = loss(pred, target)
val_loss += val_bat_loss.cpu().item()
val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
plt_val_loss.append(val_loss / val_loader.__len__())
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
if val_acc > max_acc:
torch.save(model, save_path)
max_acc = val_acc
print('[%03d/%03d] %2.2f sec(s) TrainLoss : %.6f | valLoss: %.6f Trainacc : %.6f | valacc: %.6f' % \
(epoch, epochs, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1], plt_train_acc[-1], plt_val_acc[-1])
) # 打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title("loss")
plt.legend(["train", "val"])
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title("acc")
plt.legend(["train", "val"])
plt.show()
| 属性 | 含义 | 示例 |
|---|---|---|
len(train_loader) |
batch 的数量 | ceil(总样本数 / batch_size) |
len(train_loader.dataset) |
总样本数 | 50 |
for x, y in train_loader |
每次返回一个 batch | x: (B, C, H, W), y: (B,) |
train_loader 是一个「可迭代的数据批生成器」,它每次返回一个 batch 的数据(图像 + 标签),供模型训练使用。
交叉熵损失函数
分类任务中,交叉熵损失值本身没有直观的类别错误信息,我们需要将其转化为准确率、精确率等指标才能理解模型性能。
准确率计算
train_acc += np.sum(np.argmax(y_pred.detach().cpu().numpy(), axis=1) == y.cpu().numpy())
y_pred 是模型对一个 batch 的输出,假设 batch_size = 4,类别数 = 11,那么 y_pred 的形状是:torch.Tensor of shape (4, 11),每一行是一个样本的 logits(未归一化分数,)。
y_pred.detach()
因为y_ pred 是模型计算图的一部分(有梯度信息),但我们现在只是想“看”它的值,不需要反向传播。
.cpu()
- 如果你的模型在 GPU 上运行,
pred就在 GPU 内存里。
- 但
numpy()只能处理 CPU 上的数据。 - 所以必须先用
.cpu()把数据从 GPU 移到 CPU。
.numpy()
为什么?
np.argmax() 是 NumPy 函数,只能操作 NumPy 数组,不能直接操作 PyTorch Tensor!
- 把 PyTorch Tensor 转成 NumPy 数组。
- 现在
pred.detach().cpu().numpy()是一个 NumPy 数组,形状(4, 11),类型float32。
np.argmax(..., axis=1)
np.argmax(array, axis=1):在第 1 维(即“类别”维度)找最大值的索引。- 对每一行(每个样本)找出 logits 最大的那个类别的编号。
==
- 结果是一个 布尔数组(boolean array),表示每张图是否预测正确。
np.sum(...)
- 在 NumPy 中,
True = 1,False = 0 - 所以
np.sum([True, False, True, True]) = 1 + 0 + 1 + 1 = 3
axis=1
沿着“类别”维度(第1维)找最大值
找(4,11)中的11中最大的数
总结
它统计了当前 batch 中模型猜对了多少张图片,并把这个数字加到总正确数 train_acc 上。
示例
# 假设 batch_size=4, num_classes=3
train_acc = 0
y_pred = torch.tensor([[0.1, 0.8, 0.1], # 预测类别1
[0.7, 0.2, 0.1], # 预测类别0
[0.2, 0.3, 0.5], # 预测类别2
[0.4, 0.5, 0.1]]) # 预测类别1
y = torch.tensor([1, 0, 2, 0]) # 真实标签
# 执行过程:
pred_labels = np.argmax(y_pred.numpy(), axis=1) # [1, 0, 2, 1] 与概率最大的那个数是否对应
correct = (pred_labels == y.numpy()) # [True, True, True, False]
num_correct = np.sum(correct) # 3
train_acc += 3 # 累加
更多推荐
所有评论(0)