步骤:
1.卷积
这个操作我简单理解为一片固定大小的区域,在一张图片数据上一点一点滑动,每次滑动都会把这片被区域圈住的数据作为一个新的小块数据,整个图片数据被扫过一遍以后,得到新的图片数据。卷积得到的图片数据叫特征图。
卷积核是一个多维矩阵(通常是2D或3D),其元素是可学习的权重参数。在图像处理中,常见的卷积核尺寸为3×3、5×5等。通过在输入数据上滑动(或扫描),卷积核与局部区域进行点乘运算,生成对应的输出特征图(Feature Map),从而提取特定模式(如边缘、纹理等)。
上图的7怎么得到的?在7这个值,是卷积核提取的第一个区域内一一对应的元素逐个相乘得到的结果。
为了多次卷积,提取更稳定的特征信息,需要填充(padding)0值在特征图周围,一般是能让新的特征图和卷积前特征图尺寸一样的份量。换句话说能进行无数次卷积特征图所需要的填充。
如下图,5*5的特征图,3*3的卷积核,如果不添加填充,就是无填充卷积(vaild padding),得到一个3*3的新特征图。
如果添加填充,而且得到的新特征图尺寸和原来的特征图尺寸一样,就是等尺寸填充(same padding),得到的新特征图的尺寸是5*5。
减少计算量,还会用到步长(stride),卷积核不会只移动一个像素距离,而是根据设定的步长一次移动多个像素的距离。
padding和stride一起调整网络的深度,可以根据需要自由调整。
2.池化
这一步可以简化卷积后得到的特征图的特征信息。
3.全连接
展平池化后的特征图变成一维向量。输入到全连接层中,对一个7*7*1024的特征图,展平以后得到的就是50176这么长的向量
进行y=w*x+b这种形式的运算。权重矩阵w和偏置项b对展平后的特征进行线性组合,x为展平后的输入向量,y为输出向量。
作用:将高维特征映射到低维空间,提取全局语义信息,消除特征的空间位置依赖性,增强模型对输入平移、旋转的鲁棒性。
输出向量到分类层。
4.分类
用专门的softmax()这个函数对输入的向量进行概率转换,例如对猫狗分类任务,输出一个2维向量 [3.2,−1.5][3.2,−1.5],分别表示“猫”和“狗”的原始得分。
softmax概率转换:
最终输出:模型认为输入图像有98%的概率是“猫”。
对李哥项目的理解:
一个对食物的图片进行分类的项目。
有11类食物。
其中带标签的数据:280 *11 不带标签的训练数据:6786 验证集30*11
测试集3347。
李哥说在模型训练中,有的时候得到一个不错的模型,需要保存这个随机种子。
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)
###############################################
处理图片数据
李哥创建一个food_Dataset的类,继承Dataset包。
class food_Dataset(Dataset):
在这个类中,李哥先定义了一个读取文件的函数read_file,
def read_file(self, path):
这个函数中有一个分支功能如下
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)
1.遍历类别目录
i = 5
path = "/%02d" % i # 输出 "/05"
循环处理11个类别(i从0到10),每个类别对应一个以两位数字命名的子目录(如/00、/01)。
2.加载图片数据
读取每个子目录下的所有图片文件。
Image.open加载图片,并统一缩放到指定尺寸(HW x HW)。
将图片转换为NumPy数组,存储为uint8类型(RGB通道的数据都是整数,所以不能是浮点类型)。
3.构建标签数据
为每个图片分配类别标签,标签值为当前子目录的编号i(0到10)。
4.合并数据集
将每个类别的图片数据(xi)和标签(yi)纵向堆叠,最终合并为完整的训练集X和标签Y。
首次迭代(i=0):直接初始化X = xi和Y = yi。
后续迭代(i≥1):通过np.concatenate将新的xi和yi沿样本轴(axis=0)合并到X和Y中。
合并顺序:严格保持类别顺序
5.输出结构
X的维度为(总样本数, HW, HW, 3),包含所有图片的像素数据。
Y的维度为(总样本数),包含所有图片对应的类别标签。
上图只是一个部分,最终大小会随着循环从280到560,最后到280*11。
图片和标签各自存放在一个数组里,位置顺序一一对应。
函数功能分支的另一个走向:
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
1. 模式检查
仅在 self.mode 为 "semi"(半监督模式)时执行读取操作。
2. 读取文件列表
使用 os.listdir(path) 获取指定路径下的所有文件名,生成 file_list。
3. 初始化存储数组
创建形状为 (len(file_list), HW, HW, 3) 的 numpy 数组 xi,用于存储缩放后的图像数据。
HW = 224
长宽为224*224,深度为3
4. 遍历处理每张图像
通过 Image.open(img_path) 加载图像。
使用 img.resize((HW, HW)) 将图像缩放至固定尺寸 HW x HW。
将缩放后的图像直接存入 xi 数组。
5. 返回结果
返回包含所有图像的数组 xi,并打印读取数量。
图片增广
为了让一张图片的不同角度,颜色,大小等情况都可以识别出来,要将所有这些情况的图片都收集起来。需要使用transforms包。
下面李哥首先编写一个数据变换的操作,对训练集的图片进行的变换。
train_transform = transforms.Compose(
[
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224 将图片转化为模型能够接受的格式
transforms.RandomResizedCrop(224),#将图片随机放大裁切以后的样子
transforms.RandomRotation(50),#图片在50度的范围以内随机旋转
transforms.ToTensor()#最后将图片转化为张量
]
)
变换好的数据可以使用了,在food_Dataset类的初始化函数__init__,取函数__getitem__,长度函数__len__里就可以使用转化好的数据。
def __init__(self, path, mode="train"):
self.mode = mode
if mode == "semi":
self.X = self.read_file(path)
else:
self.X, self.Y = self.read_file(path)#读取文件路径
self.Y = torch.LongTensor(self.Y) #标签转为长整形
if mode == "train":
self.transform = train_transform
else:
self.transform = val_transform
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-11_sample这个路径的图片,以学习交流为目的。实际上会使用的路径应该是food-11这个路径的图片,但是这样读取速度对大部分学生的设备来说会非常慢,所以选择food-11_sample。
# path = r"D:\P_projects\4\food_classification\food-11\training\labeled"
# train_path = r"D:\P_projects\4\food_classification\food-11\training\labeled"
# val_path = r"D:\P_projects\4\food_classification\food-11\validation"
train_path = r"D:\P_projects\4\food_classification\food-11_sample\training\labeled"
val_path = r"D:\P_projects\4\food_classification\food-11_sample\validation"
train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")
原本的图片集已经按照一类分在一起了,需要打乱,为了加快读取速度,以步长16抽取图片,然后打乱。
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)
之后李哥同理对验证集也进行处理,如上方代码展示。
李哥说在验证的时候,不用进行乱七八糟的变换,因为验证的图片相当于随机的形状,所以验证集的图片数据直接转为张量使用。
val_transform = transforms.Compose(
[
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224 将图片转化为模型能够接受的格式
transforms.ToTensor() #转为张量
]
)
所以在初始化函数__init__中根据模式选择不一样的分支操作——训练模式和验证模式的操作
if mode == "train":
self.transform = train_transform
else:
self.transform = val_transform
模型
一个模型类myModel,继承了nn这个包
class myModel(nn.Module):
随后李哥定义一个初始化函数__init__准备用于全连接分类,这里包含了卷积和池化操作。
def __init__(self, num_class):
初始卷积块:
conv1:3通道输入,64通道输出,3x3卷积核(stride=1,padding=1),保持特征图尺寸不变。
bn1:对64通道进行批归一化,加速训练并稳定学习过程。
ReLU:引入非线性激活。
pool1:2x2最大池化,特征图尺寸减半。
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
中间卷积块(layer1, layer2, layer3):
每层通过nn.Sequential堆叠卷积、批归一化、ReLU和池化。
layer1:64→128通道,3x3卷积,池化后尺寸再减半。
layer2:128→256通道,结构同前(需注意代码中可能缺少闭合括号的语法错误)。
layer3:256→512通道,同上。
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
)
额外池化层(pool2):
对layer3的输出进行2x2最大池化,进一步压缩特征图。
self.pool2 = nn.MaxPool2d(2) #512*7*7
全连接层:
fc1:将展平后的25088维特征映射到1000维,接ReLU激活。
fc2:1000维→num_class输出,得到分类结果。
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) # 展平为 [B, 512*7*7=25088]
x = self.fc1(x)
x = self.relu2(x)
x = self.fc2(x)
return x
输入各项参数
学习率
控制优化器在参数更新时的步长大小。值过大会导致模型不稳定或无法收敛;值过小会降低训练速度。0.001 是 Adam 系列优化器的常用初始值,适合大多数任务。
lr = 0.001
损失函数
结合Softmax和交叉熵,将模型输出的logits转换为概率分布后计算损失。
loss = nn.CrossEntropyLoss()
权重衰减
AdamW是Adam的改进版本,解耦权重衰减和梯度更新,避免传统 Adam 中权重衰减与动量冲突的问题。lr与全局学习率一致。weight_decay=1e-4正则化系数,通过惩罚大权重防止过拟合,提升泛化能力。
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
自动选择训练设备
GPU加速矩阵运算(如卷积),显著提升训练速度;CPU作为备用设备确保兼容性。
device = "cuda" if torch.cuda.is_available() else "cpu"
保存最佳模型权重
通常结合早停法(Early Stopping)或定期保存,避免训练中断或过拟合。
save_path = "model_save/best_model.pth"
训练的总轮数
15 轮适合中小型数据集或简单模型。
epochs = 15
导入验证集函数
从上一个项目复制过来的函数
def train_val(model, train_loader, val_loader, no_label_loader, device, 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()
直接调用这个函数
train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path)
在train_val函数中,额外记录模型的准确率
plt_train_acc = []
plt_val_acc = []
准确率超过了0就可以记录下来
max_acc = 0.0
在循环中加入的这些准确率结果
train_acc = 0.0
val_acc = 0.0
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
分离并转换预测张量:
pred.detach():从计算图分离,避免梯度计算。
cpu():将数据移至CPU(若原在GPU)。
numpy():转换为NumPy数组。
np.argmax(axis=1):获取每个样本的预测类别(最大概率对应的索引)。
处理真实标签:
target.cpu().numpy():同样移至CPU并转为NumPy数组。
计算正确预测数:
比较预测类别与真实标签,生成布尔数组。
np.sum():统计True的数量(正确样本数)。
累加准确率:
将当前批次的正确数累加到train_acc。
为李哥鼓掌
plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #记录准确率,
预测正确的次数/预测的总次数=准确率
同理验证集
val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
同理记录准确率
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
如果准确率超过以往准确率就会保存模型
if val_acc > max_acc:
torch.save(model, save_path)
max_acc = val_loss
要画训练集合验证集两个图像
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()
打印结果
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的浮点数, 后面可以对应。
使用大佬的模型
# model = myModel(11)
model, _ = initialize_model("vgg", 11, use_pretrained=True)
用他们模型的名字就可以用他们的模型了。
有很多优秀的模型,第一个MyModel是李哥的模型。
def initialize_model(model_name, num_classes, linear_prob=False, use_pretrained=True):
# 初始化将在此if语句中设置的这些变量。
# 每个变量都是模型特定的。
model_ft = None
input_size = 0
if model_name =="MyModel":
if use_pretrained == True:
model_ft = torch.load('model_save/MyModel')
else:
model_ft = MyModel(num_classes)
input_size = 224
elif model_name == "resnet18":
""" Resnet18
"""
model_ft = models.resnet18(pretrained=use_pretrained) # 从网络下载模型 pretrain true 使用参数和架构, false 仅使用架构。
set_parameter_requires_grad(model_ft, linear_prob) # 是否为线性探测,线性探测: 固定特征提取器不训练。
num_ftrs = model_ft.fc.in_features #分类头的输入维度
model_ft.fc = nn.Linear(num_ftrs, num_classes) # 删掉原来分类头, 更改最后一层为想要的分类数的分类头。
input_size = 224
elif model_name == "resnet50":
""" Resnet50
"""
model_ft = models.resnet50(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "googlenet":
""" googlenet
"""
model_ft = models.googlenet(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "alexnet":
""" Alexnet
"""
model_ft = models.alexnet(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier[6].in_features
model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
input_size = 224
elif model_name == "vgg":
""" VGG11_bn
"""
model_ft = models.vgg11_bn(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier[6].in_features
model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
input_size = 224
elif model_name == "squeezenet":
""" Squeezenet
"""
model_ft = models.squeezenet1_0(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
model_ft.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))
model_ft.num_classes = num_classes
input_size = 224
elif model_name == "densenet":
""" Densenet
"""
model_ft = models.densenet121(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier.in_features
model_ft.classifier = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "inception":
""" Inception v3
Be careful, expects (299,299) sized images and has auxiliary output
"""
model_ft = models.inception_v3(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
# 处理辅助网络
num_ftrs = model_ft.AuxLogits.fc.in_features
model_ft.AuxLogits.fc = nn.Linear(num_ftrs, num_classes)
# 处理主要网络
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs,num_classes)
input_size = 299
else:
print("Invalid model_utils name, exiting...")
exit()
return model_ft, input_size
李哥说在迁移学习中有线性探测和微调方式
线性探测(Linear Probing)
仅冻结预训练模型的特征提取器(如Transformer的编码层),仅训练顶层的线性分类头(全连接层)。
流程:
固定预训练模型的参数;
仅用新数据集训练分类头,不更新特征提取层;
适用于特征提取能力较强且新任务数据量较小的场景。
微调(Fine-Tuning)
解冻预训练模型的部分或全部参数,与分类头联合优化,使模型更适应新任务14。
流程:
初始化预训练模型的权重;
根据任务需求选择冻结/解冻的层(如仅解冻最后几层);
使用新数据对模型进行端到端训练。
半监督学习
半监督学习的特点是少量带标签数据和大量无标签数据,目的是为了提升模型的性能。
多数半监督方法需要迭代优化:
自训练(Self-training):先用有标签数据训练初始模型,预测无标签数据,选取高置信度预测作为伪标签,重新训练模型。
协同训练(Co-training):多个模型从不同视角对无标签数据预测,互相提供伪标签。
其核心价值在于以低成本利用无标签数据中的隐含规律,突破纯监督学习的数据瓶颈。
大模型的补充:
实现
首先在food_Dataset这个类的初始函数__init__中设置一个半监督的分支,如果mode="semi"就会进行半监督的数据读取。可以看到和否定分支相比少了Y,是无标签的数据。
def __init__(self, path, mode="train"):
self.mode = mode
if mode == "semi":
self.X = self.read_file(path)
else:
self.X, self.Y = self.read_file(path)
self.Y = torch.LongTensor(self.Y) #标签转为长整形\
设置文件路径
no_label_path = r"D:\P_projects\4\food_classification\food-11_sample\training\unlabeled\00"
no_label_set = food_Dataset(no_label_path, "semi")
对于半监督的无标签数据,它们的训练用途是在已经被有标签数据训练好的模型上进行验证,所以数据处理的方式和验证集一样,所以在__init__函数里的一个否分支执行的是验证集的数据处理方式。
if mode == "train":
self.transform = train_transform
else:
self.transform = val_transform #半监督的验证集数据处理方式
val_transform = transforms.Compose(
[
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224
transforms.ToTensor()
]
)
在__getitem__函数中半监督模式中直接返回transform处理的数据和原始的数据。这里transform的数据就像验证集里的数据一样,从模型出来以后得到预测值。
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]
原始数据存放在无标签集。 对无标签数据集的图片进行预测,不打乱顺序,因为需要和原始数据的位置一一对应。
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)
一个半监督的类
class semiDataset(Dataset):
初始化函数
def __init__(self, no_label_loder, model, device, thres=0.99):
no_label_loader: 未标注数据的加载器(DataLoader)
model: 已预训练(或部分训练)的分类模型,用于生成伪标签
device: 计算设备(CPU/GPU)
thres=0.99: 置信度阈值,仅保留模型预测置信度高于该值的样本
x, y = self.get_label(no_label_loder, model, device, thres)
遍历no_label_loader中的未标注图像,用model预测每张图像的类别概率分布,保留最高预测概率超过阈值thres的样本,将对应的图像数据存入x,伪标签(预测类别)存入y。
if x == []:
self.flag = False # 无有效伪标签数据
else:
self.flag = True # 存在有效伪标签数据
self.X = np.array(x) # 图像数据转NumPy数组
self.Y = torch.LongTensor(y) # 伪标签转长整型张量
self.transform = train_transform # 应用数据增强(如裁剪、翻转)
为什么要使用训练集的数据处理方式?因为半监督学习得到的数据最终也是用于训练模型的,所以和训练集的数据采取相同的处理。
get_label函数
def get_label(self, no_label_loder, model, device, thres):
model = model.to(device)
pred_prob = [] #预测值的一个列表
labels = [] #标签的一个列表
x = []
y = [] #最终的x和y
soft = nn.Softmax() #定义softmax函数
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())#同理
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
直观感受如下图
大模型补充
这里也就印证了之前取数据的时候为什么不对数据进行打乱,所以shuffle的值应为False 。
随后
def __getitem__(self, item):
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.X)
semi_data_loader函数
半监督学习的核心是利用模型对无标签数据的预测结果生成伪标签,而非直接使用固定标签数据。
训练集/验证集:数据标签是预先标注好的,直接读取即可。
半监督数据集:标签是模型动态预测的(伪标签),需实时生成并筛选。
def get_semi_loader(no_label_loder, model, device, thres):
semiset = semiDataset(no_label_loder, model, device, thres)
if semiset.flag == False:
return None
else:
semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
return semi_loader
在内部调用get_label生成伪标签,并根据阈值thres筛选高置信度样本。每次调用get_semi_loader,模型可能已更新,生成的伪标签会变化,因此需要重新构建数据集。
避免内存爆炸
直接读取无标签数据并一次性生成伪标签会导致内存占用过高,尤其当数据量极大时。通过 semiDataset的延迟加载(Lazy Loading):
按需生成:仅在训练时按批次生成伪标签,减少内存占用。
复用原始数据加载器:无需重复存储数据,直接复用no_label_loader的数据流。
若semiset中没有满足条件的样本,返回None以避免无效训练。
训练模型
在train_val函数中,有初始化loss和acc
semi_loss = 0.0
semi_acc = 0.0
如果semi_loader里有东西就拿来训练模型
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) #计算loss
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__())
如何让semi_loader里有东西?
# 条件触发:每隔3个epoch,且最新验证集准确率超过60%
if epoch % 3 == 0 and plt_val_acc[-1] > 0.6:
# 调用函数生成半监督数据加载器
semi_loader = get_semi_loader(
no_label_loader, # 无标签数据源
model, # 当前模型(用于生成伪标签)
device, # 计算设备(如GPU)
thres # 置信度阈值(过滤低置信度伪标签)
)
总结
虽然不能完全理解,但还是写了这些下来,想要加深理解仍然需要自己再照着写一个分类项目。