图像分类项目-食物分类(监督学习和半监督学习)
文章目录
- 图像分类项目-食物分类(监督学习和半监督学习)
- 项目介绍
- 数据处理
- 设定随机种子
- 读取文件内容
- 图像增广
- 定义Dataset类
- 模型定义
- 迁移学习
- 定义超参
- Adam和AdamW
- 训练过程
- 半监督学习
- 定义Dataset类
- 模型定义
- 定义超参
- 训练过程
项目介绍
数据处理
设定随机种子
由于神经网络的训练具有随机性,为了保证之前得到的好的训练效果可以得到复现,设定随机种子,让训练过程中的随机行为每次训练都是相同。
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)
###############################################
读取文件内容
进行数据处理前,需要了解数据的形式训练集中,有标签的数据按照11类分别存放在11个文件夹中,因此要循环依次读取这11个文件夹的内容:
首先从文件夹中读出每张图片和对应标签(读取的是带标签的数据):
HW = 224
def read_file(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))
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
图像增广
模型对训练使用的图片数据有好的效果,但是如果对图片数据进行一定的变化,模型的效果就变差,因此在训练时,不仅使用原图片训练,还要对图片进行旋转,放大裁切等图像操作,将原图片和操作后的图片都作为训练数据,也就是图像增广,让模型的效果更好。
train_transform = transforms.Compose(#定义训练集增广方式
[
transforms.ToPILImage(), #224,224,3模型:3,224,244
transforms.RandomResizedCrop(224),#随机放大裁切
transforms.RandomRotation(50),#50度以内随机旋转
transforms.ToTensor()#模型运行的数据类型为张量
]
)
val_transform = transforms.Compose(#验证集不需要增广
[
transforms.ToPILImage(), # 224,224,3模型:3,224,244
transforms.ToTensor() # 模型运行的数据类型为张量
]
)
定义Dataset类
class food_Dataset(Dataset):#继承Dateset类
def __init__(self, path, mode="train"):
self.X, self.Y = 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):
return self.transform(self.X[item]), self.Y[item] #使用图片增广
def __len__(self):
return len(self.Y)
模型定义
在模型中设定一些卷积、归一化、池化、激活函数对数据进行特征提取。
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) #3厚度,64个卷积核,卷积核大小3,padding为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.size(0) 就是批量大小(batch_size),表示你有多少个样本输入到模型中。-1 是自动计算剩下的维度以便将数据展平。
x = self.fc1(x)
x = self.relu2(x)
x = self.fc2(x)
return x
迁移学习
良好的模型是需要大量的数据训练得到的,由于我们设备加上数据量的限制训练出来的模型效果不会特别好,甚至预测效果接近随机预测,因此我们要进行迁移学习。简单来说,**迁移学习就是使用大佬们用大量数据训练出来的现成模型,**由于大佬的模型经过训练后有很好的特征提取效果,因此我们只需要使用大佬的模型然后加上分类头作为训练的模型即可。
预训练是指在无关当前任务的模型训练,迁移学习使用的模型就是进行过预训练的模型,迁移学习时可以进行线性探测和微调,线性探测就是在训练中不进行参数的调整,完全信任迁移学习使用的模型,微调就是在训练过程中会进行参数调整。
迁移学习时可选择只使用架构和使用架构和参数,虽然迁移学习使用的架构很优秀但是参数是更加重要的部分,因此使用架构和参数的效果要更好,要想使用迁移学习的预训练参数要保持架构一致。
from torchvision.models import resnet18#导入模型
model = resnet18(pretrained=True)#使用架构和参数
in_fetures = model.fc.in_features#获取模型的特征提取后的输出维度
model.fc = nn.Linear(in_fetures, 11)#全连接分类头
定义超参
定义学习率、损失函数、优化器、训练轮次等超参数。
Adam和AdamW
Adam优化器不仅考虑当前点的梯度还考虑之前的梯度,并且会自动更改学习率,由于参数更改时要减去学习率×梯度,当这个值过大时,Adam会自动更改学习率,AdamW是在Adam的基础上增加了权重衰减使得模型曲线更加平滑。
训练过程
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, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
train_bat_loss = loss(pred, target) #获取一批数据的LOSS
train_bat_loss.backward() #梯度回传
optimizer.step()#更新模型
optimizer.zero_grad()
train_loss += train_bat_loss.cpu().item() #将gpu上的张量数据放到cpu上取出数据计算,累加记录本轮LOSS
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())#记录预测对的数量
plt_train_loss.append(train_loss / train_loader.__len__()) #除以轮次数,得到每个轮次的LOSS平均值
plt_train_acc.append(train_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 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("loss")
plt.legend(["train", "val"])
plt.show()
半监督学习
监督学习是指每个训练样本都有对应的标签,模型通过学习这些标注数据训练,目标是让模型能够根据新的输入数据预测正确的标签。
半监督学习是介于监督学习和无监督学习之间的一种方法。在半监督学习中,训练数据包含大量的未标注数据和少量的标注数据。模型利用少量的标注数据来进行学习,同时也借助未标注数据来进一步提高模型的性能。
- 模型首先使用标注数据进行训练。
- 模型的效果达到一定程度后,用训练得到的模型对未标注数据进行预测。
- 若预测结果结果的置信值(成功率)达到一定值后,将预测结果(伪标签)添加到训练数据集中。
为了加入半监督学习,对监督学习的代码进行修改。
定义Dataset类
class food_Dataset(Dataset):
def __init__(self, path, mode="train"):
self.mode = mode
if mode == "semi":#若为半监督模式,数据只有X,没有标签Y
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 read_file(self, path):#读取数据函数
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:
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]#前者为为了输入模型进行转换的X用于训练得到伪标签,后者为原始数据X用于加入半监督数据集
else:
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.X)
class semiDataset(Dataset):#半监督数据集Dataset类
def __init__(self, no_label_loder, model, device, 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):#给半监督数据打标签
model = model.to(device)
pred_prob = []#记录预测类型中最高概率
labels = []#记录最高概率对应的标签
x = []
y = []
soft = nn.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)#维度1为横向,取出最高概率和其对应的标签,由于loader中一个元素是一批数据,因此pred_max和pred_value的一个元素中包含对应批数个值
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
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):#获取半监督数据集
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
模型定义
加入半监督学习只需要复用监督学习的训练模型进行预测即可。
定义超参
加入半监督学习要额外定义包括置信度的超参。
训练过程
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#半监督数据集LOSS
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()