其中不带标签的数据只有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:任何可遍历的对象(如 listtuplestring 等)
  • 默认从 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 = 1False = 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  # 累加

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐