def seed_everything(seed):
# torch.manual_seed 是 PyTorch 提供的一个函数,用于设置 CPU 生成随机数的种子。这样在使用 PyTorch 进行张量初始化、随机采样等涉及随机数的操作时,只要种子相同,每次生成的随机数序列都是一样的。
torch.manual_seed(seed)
# torch.cuda.manual_seed 用于设置当前 GPU 生成随机数的种子。如果代码在 GPU 上运行,这个函数可以确保在当前 GPU 上的随机操作是可重复的。
torch.cuda.manual_seed(seed)
# torch.cuda.manual_seed_all 用于设置所有可用 GPU 生成随机数的种子。当使用多个 GPU 进行训练时,这个函数可以保证所有 GPU 上的随机操作都是可重复的。
torch.cuda.manual_seed_all(seed)
# torch.backends.cudnn.benchmark 是 PyTorch 中用于 CuDNN(NVIDIA 提供的深度神经网络库)的一个设置选项。
# 当设置为 True 时,CuDNN 会在每次运行时自动寻找最优的卷积算法,以提高计算效率,但这会引入一些随机性。
# 将其设置为 False 可以确保每次运行时使用相同的卷积算法,从而保证结果的可重复性。
torch.backends.cudnn.benchmark = False
# torch.backends.cudnn.deterministic 也是用于 CuDNN 的设置选项。当设置为 True 时,CuDNN 会使用确定性的算法,
# 避免一些可能引入随机性的操作,进一步保证实验结果的可重复性。
torch.backends.cudnn.deterministic = True
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
#################################################################
seed_everything(0)
###############################################
这段代码的主要目的是设置随机种子,以确保深度学习实验的可重复性。在深度学习中,很多操作都涉及到随机数的生成,例如模型参数的初始化、数据的随机打乱等。如果不固定随机种子,每次运行代码时这些随机操作的结果都会不同,从而导致模型的训练结果和性能也会有所差异。通过设置随机种子,可以保证每次运行代码时,随机数的生成序列都是相同的,进而使得实验结果可以复现。
class food_Dataset(Dataset):
def __init__(self, path, mode="train"):
self.mode = mode
# 如果 mode 为 "semi"(半监督模式),调用 read_file 方法读取数据,并将结果保存到 self.X 中。
# 否则,调用 read_file 方法读取数据和对应的标签,分别保存到 self.X 和 self.Y 中,并将标签转换为 torch.LongTensor 类型。
if mode == "semi":
self.X = self.read_file(path)
else:
self.X, self.Y = self.read_file(path)
self.Y = torch.LongTensor(self.Y) #标签转为长整形\
# 如果 mode 为 "train",使用 train_transform 作为图像预处理转换;否则,使用 val_transform 作为图像预处理转换。
if mode == "train":
self.transform = train_transform
else:
self.transform = val_transform
def read_file(self, path):
# 如果 mode 为 "semi",获取指定路径下的所有文件列表,并创建一个形状为 (文件数量, HW, HW, 3) 的零数组 xi,用于存储图像数据。
if self.mode == "semi":
file_list = os.listdir(path)
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
img_path = os.path.join(path, img_name)
img = Image.open(img_path)
img = img.resize((HW, HW))
xi[j, ...] = img
print("读到了%d个数据" % len(xi))
return xi
else:
# 如果 mode 不是 "semi",遍历 11 个类别文件夹,对于每个类别文件夹,获取该文件夹下的所有文件列表,并创建两个零数组 xi 和 yi,分别用于存储图像数据和标签。
for i in tqdm(range(11)):
file_dir = path + "/%02d" % i
file_list = os.listdir(file_dir)
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
yi = np.zeros(len(file_list), dtype=np.uint8)
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
img_path = os.path.join(file_dir, img_name)
img = Image.open(img_path)
img = img.resize((HW, HW))
xi[j, ...] = img
yi[j] = i
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
def __getitem__(self, item):
if self.mode == "semi":
return self.transform(self.X[item]), self.X[item]
else:
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.X)
这段代码定义了一个名为 food_Dataset 的自定义数据集类,继承自 torch.utils.data.Dataset。该类的主要作用是读取食品图像数据,并根据不同的模式(训练、验证、半监督)对数据进行处理,方便后续使用 DataLoader 进行批量加载。
class semiDataset(Dataset):
def __init__(self, no_label_loder, model, device, thres=0.99):
# __init__ 是类的初始化方法,接受四个参数:
# no_label_loder:无标签数据的 DataLoader。
# model:用于预测伪标签的模型。
# device:模型所在的设备(如 'cuda' 或 'cpu')。
# thres=0.99:置信度阈值,只有预测概率大于该阈值的样本才会被选中作为伪标签数据。
x, y = self.get_label(no_label_loder, model, device, thres)
if x == []:
self.flag = False
else:
self.flag = True
self.X = np.array(x)
self.Y = torch.LongTensor(y)
self.transform = train_transform
def get_label(self, no_label_loder, model, device, thres):
# pred_prob:存储每个样本的预测概率最大值。
# labels:存储每个样本的预测标签。
# x:存储符合条件的图像数据。
# y:存储符合条件的伪标签。
model = model.to(device)
pred_prob = []
labels = []
x = []
y = []
soft = nn.Softmax()
# 使用 with torch.no_grad() 上下文管理器禁用梯度计算,以节省内存和计算资源。
# 遍历无标签数据加载器 no_label_loder,对每个批次的图像数据进行预测:
# 将批次数据 bat_x 移动到设备上。
# 使用模型 model 进行预测,得到原始输出 pred。
# - 通过 soft(Softmax 层)将输出转换为概率分布 pred_soft。
# - 找到每个样本的最大概率值 pred_max 和对应的预测标签 pred_value。
# - 将 pred_max 和 pred_value 转换为 numpy 数组,并添加到 pred_prob 和 labels 列表中。
with torch.no_grad():
for bat_x, _ in no_label_loder:
bat_x = bat_x.to(device)
pred = model(bat_x)
pred_soft = soft(pred)
pred_max, pred_value = pred_soft.max(1)
pred_prob.extend(pred_max.cpu().numpy().tolist())
labels.extend(pred_value.cpu().numpy().tolist())
# 遍历 pred_prob 列表中的每个概率值:
# - 如果概率值 prob 大于阈值 thres,则将对应的图像数据和伪标签添加到 x 和 y 列表中。
# no_label_loder.dataset[index][1] 用于获取原始无标签数据集中第 index 个样本的图像数据(假设原始数据集的 __getitem__ 返回 (图像, 无标签),这里通过索引 1 获取图像数据)。
for index, prob in enumerate(pred_prob):
if prob > thres:
x.append(no_label_loder.dataset[index][1]) #调用到原始的getitem
y.append(labels[index])
return x, y
def __getitem__(self, item):
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.X)
def get_semi_loader(no_label_loder, model, device, thres):
# no_label_loder:无标签数据的 DataLoader 对象,用于加载无标签的图像数据。
# model:用于预测无标签数据伪标签的模型。
# device:模型运行的设备,例如 'cuda' 表示使用 GPU,'cpu' 表示使用 CPU。
# thres:置信度阈值,用于筛选出模型预测置信度较高的样本作为伪标签数据。
semiset = semiDataset(no_label_loder, model, device, thres)
# semiset.flag 是 semiDataset 类中的一个标志属性。如果 flag 为 False,说明在生成伪标签数据的过程中,没有找到预测概率大于阈值的样本,即没有可用的伪标签数据。此时,函数直接返回 None。
if semiset.flag == False:
return None
# 如果 semiset.flag 为 True,说明有可用的伪标签数据。使用 torch.utils.data.DataLoader 创建一个半监督数据加载器 semi_loader。
# semiset:传入前面创建的半监督数据集对象。
# batch_size=16:设置每个批次加载的数据样本数量为 16。
# shuffle=False:设置不打乱数据的顺序。在半监督学习中,通常不需要打乱伪标签数据的顺序。
# 最后,函数返回创建好的半监督数据加载器 semi_loader,以便后续在训练过程中使用。
else:
semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
return semi_loader
class semiDataset 的作用是:
生成伪标签:利用已有的模型对无标签数据进行预测,筛选出预测置信度较高的样本作为伪标签数据。
数据加载:将伪标签数据包装成 Dataset 格式,方便与 DataLoader 配合使用,用于半监督训练。
数据增强:对伪标签数据应用与训练集相同的数据增强操作(如随机裁剪、旋转),以提高模型的泛化能力
该类通过动态生成伪标签,将无标签数据转化为有监督学习的训练数据,从而提升模型在半监督学习任务中的性能。
class myModel(nn.Module):
def __init__(self, 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)
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)
x = self.fc1(x)
x = self.relu2(x)
x = self.fc2(x)
return x
myModel 是一个简单的卷积神经网络模型,通过卷积层提取图像特征,再通过全连接层进行分类。它的输入是 3x224x224 的 RGB 图像,输出是 num_class 个类别的预测分类类别。
def train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path):
# model:待训练的深度学习模型。
# train_loader:训练集的数据加载器,用于批量加载有标签的训练数据。
# val_loader:验证集的数据加载器,用于批量加载验证数据。
# no_label_loader:无标签数据的数据加载器,用于半监督学习。
# device:模型运行的设备,如 'cuda' 或 'cpu'。
# epochs:训练的轮数。
# optimizer:优化器,用于更新模型的参数。
# loss:损失函数,用于计算模型预测结果与真实标签之间的差异。
# thres:半监督学习中生成伪标签的置信度阈值。
# save_path:保存最佳模型的文件路径。
model = model.to(device)
semi_loader = None
plt_train_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
semi_loss = 0.0
semi_acc = 0.0
start_time = time.time()
model.train()
for batch_x, batch_y in train_loader:
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
train_bat_loss = loss(pred, target)
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) == target.cpu().numpy())
plt_train_loss.append(train_loss / train_loader.__len__())
plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #记录准确率,
if semi_loader!= None:
for batch_x, batch_y in semi_loader:
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
semi_bat_loss = loss(pred, target)
semi_bat_loss.backward()
optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度
optimizer.zero_grad()
semi_loss += train_bat_loss.cpu().item()
semi_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
print("半监督数据集的训练准确率为", semi_acc/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.dataset.__len__())
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
if epoch%3 == 0 and plt_val_acc[-1] > 0.6:
semi_loader = get_semi_loader(no_label_loader, model, device, thres)
if val_acc > max_acc:
torch.save(model, save_path)
max_acc = val_loss
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()
HW = 224
# transforms.ToPILImage():将输入数据转换为 PIL(Python Imaging Library)图像对象。在深度学习中,有时候数据可能是以其他格式(如 numpy 数组)存在的,这个操作可以将其转换为 PIL 图像,方便后续的图像处理操作。
# transforms.RandomResizedCrop(224):这是一个数据增强操作。它会随机裁剪输入图像,并将裁剪后的图像调整为 224x224 的大小。随机裁剪的区域和比例是随机选择的,这样可以增加训练数据的多样性,使模型能够学习到不同位置和尺度的特征,从而提高模型的泛化能力。
# transforms.RandomRotation(50):也是一个数据增强操作。它会随机将图像旋转一定的角度,旋转角度的范围是 [-50, 50] 度。通过随机旋转图像,可以让模型学习到不同角度下的图像特征,进一步增强模型对图像旋转的鲁棒性。
# transforms.ToTensor():将 PIL 图像或 numpy 数组转换为 torch.Tensor 类型,同时将图像的像素值归一化到 [0, 1] 范围内。
train_transform = transforms.Compose(
[
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224
transforms.RandomResizedCrop(224),
transforms.RandomRotation(50),
transforms.ToTensor()
]
)
# transforms.ToPILImage():同样是将输入数据转换为 PIL 图像对象。
# transforms.ToTensor():将 PIL 图像或 numpy 数组转换为 torch.Tensor 类型,并将像素值归一化到 [0, 1] 范围内。
val_transform = transforms.Compose(
[
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224
transforms.ToTensor()
]
)
# 数据增强方面:train_transform 包含了 RandomResizedCrop 和 RandomRotation 两个数据增强操作,而 val_transform 没有这些操作。数据增强主要用于训练阶段,通过对训练数据进行随机变换,可以增加训练数据的多样性,使模型学习到更丰富的特征,从而提高模型的泛化能力。
# 稳定性需求方面:验证集(或测试集)的目的是评估模型的真实性能,因此不需要对数据进行随机变换,以保证评估结果的稳定性和准确性。如果在验证集上使用数据增强,每次评估时输入的数据都不同,会导致评估结果不稳定,无法准确反映模型的真实性能。
train_path = r"E:\深度学习\food_classification\food-11\training\labeled"
val_path = r"E:\深度学习\food_classification\food-11\validation"
no_label_path = r"E:\深度学习\food_classification\food-11\training\unlabeled\00"
train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")
no_label_set = food_Dataset(no_label_path, "semi")
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)
model, _ = initialize_model("vgg", 11, use_pretrained=True)
lr = 0.001
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
device = "cuda" if torch.cuda.is_available() else "cpu"
save_path = "model_save/best_model.pth"
epochs = 15
thres = 0.99
train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path)