资讯详情

资讯详情

食物分类任务实战

一、分类任务的输入和输出输入单张图形状 (H, W, 3)一个 batch(B, H, W, 3)其中B: 一个批次的数量 H: 图片的高度 W: 图片的高度 3RGB3通道。输出Linear( 特征num_class)标签Y采用独热编码表示类别如图所示。输出是一个长度为类别数的向量预测值Y最大值所在下标为预测类如图所示。二、分类的卷积神经网络求解步骤1、让输入图片经过多层卷积层和池化层ConvBatchNormReluMaxPool)提取图片局部特征。2、将提取到的特征展平经过全连接映射到类别空间再经过Softmax()输出各个类别概率的向量。3、将该向量与真实的标签通过交叉熵求Loss然后梯度回传更新参数。三、食物分类的任务将食物图片分为11类。其中带标签的数据280 *11不带标签的训练数据 6786验证集数据30*11测试集数据3347个。四、代码实现1、导入的包import random, torch, torch.nn as nn, numpy as np, os from PIL import Image from torch.utils.data import Dataset, DataLoader from tqdm import tqdm from torchvision import transforms import time, matplotlib.pyplot as plt from model_utils.model import initialize_model2、随机种子固定所有随机源保证每次运行结果可复现。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)3、数据部分首先读出文件夹里的所有数据然后根据模式不同对数据进行不同的增广方式训练数据要经过放大裁剪、旋转等方式增强训练集而其他数据则不用。如果是有标签数据需要读取增广后的数据图片、标签。没标签数据需要读取增广后的数据图片和原始图片。class food_Dataset(Dataset): def __init__(self, path, modetrain): 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 read_file(self, path): if self.mode semi: file_list os.listdir(path) # 列出文件夹下所有文件的名字 # 预分配数组空间 xi np.zeros((len(file_list), HW, HW, 3), dtypenp.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 # ...表示维度和img一样 print(读到了%d个数据 % len(xi)) return xi else: for i in tqdm(range(11)): #tqdm是进度条库实时显示循环的进度。 file_dir path \%02d %i file_list os.listdir(file_dir) # 列出文件夹下所有文件的名字 # 预分配数组空间 xi np.zeros((len(file_list), HW, HW, 3), dtypenp.uint8) # 每一类图片 yi np.zeros(len(file_list), dtypenp.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 # ...表示维度和img一样 yi[j] i if i 0: X xi Y yi else: X np.concatenate((X, xi), axis0) Y np.concatenate((Y, yi), axis0) 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]# 训练集数据增强 train_transform transforms.Compose( [ transforms.ToPILImage(), #将2242243改成模型的3 224224 transforms.RandomResizedCrop(224), #图片放大裁切 transforms.RandomRotation(50), #将图片旋转50度以内 transforms.ToTensor() # 转Tensor并归一化到[0,1] ] ) # 验证集仅做基本变换 val_transform transforms.Compose( [ transforms.ToPILImage(), #将2242243改成模型的3 224224 transforms.ToTensor() ] )由于有没有标签的数据为了利用这些无标签数据增加数据量采用半监督学习。semiDataset(半监督训练集)是当前模型预测的准确度大于0.7就用当前模型预测无标签数据挑出高置信度样本赋予伪标签将其放入训练集进行训练大大提高了模型的训练数据。class semiDataset(Dataset): def __init__(self, no_label_loader, model, device, thres0.99): # thres是置信度 x, y self.get_label(no_label_loader, 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_loader, model, device, thres): model model.to(device) pred_prob [] # 存概率值 labels [] # 存标签 x [] y [] soft nn.Softmax() # Softmax转换成概率总和为1 with torch.no_grad(): for bat_x, _ in no_label_loader: # bat_x用来经过transform的x, _指得是原始的x bat_x bat_x.to(device) pred model(bat_x) # 1. 对无标签数据做预测 pred_soft soft(pred) # 转成概率 # 2. 获取最高概率和对应的类别 pred_max, pred_index pred_soft.max(1) # (1)横向维度上 pred_prob.extend(pred_max.cpu().numpy().tolist()) labels.extend(pred_index.cpu().numpy().tolist()) for index, prob in enumerate(pred_prob): if prob thres: # 3. 筛选概率阈值的样本 x.append(no_label_loader.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) # 返回筛选后的样本数量构建半监督 loader筛不出高置信度的样本就返回 None训练时跳过。​ def get_semi_loader(no_label_loader, model, device, thres): semi_set semiDataset(no_label_loader, model, device, thres) if semi_set.flag False: return None else: semi_loader DataLoader(semi_set, batch_size16, shuffleFalse) return semi_loader ​4、模型部分首先自定义了一个模型myModel采用4 个卷积层一次全局的平均池化 将图片从 3×224×224—512×7×7拉直后采用2 层全连接。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) self.bn1 nn.BatchNorm2d(64) self.relu1 nn.ReLU() self.pool1 nn.MaxPool2d(2) # 64*112*112 self.layer1 nn.Sequential( nn.Conv2d(64, 128, 3, 1, 1), 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) self.relu2 nn.ReLU() self.fc2 nn.Linear(1000, num_class) def forward(self, x): x self.conv1(x) x self.bn1(x) x self.relu1(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.relu1(x) x self.fc2(x) return x但是myModel模型的训练效果太差因此使用迁移学习采用ResNet18的网络结构和参数。model, _ initialize_model(resnet18, 11, use_pretrainedTrue)5、超参设置设置训练轮数、学习率、损失函数、优化器、thres置信度epochs 10 lr 0.001 loss nn.CrossEntropyLoss() # 使用交叉熵求loss optimizer torch.optim.AdamW(model.parameters(), lrlr, weight_decay1e-4) #使用AdamW优化器 device cuda if torch.cuda.is_available() else cpu save_path model_save/best_model.pth thres 0.99 # thres应该设为0.996、训练流程分类任务的训练流程和回归任务的训练流程大致相同只不过增加了训练准确率和验证准确率其中train_acc表示的是预测正确的个数因此训练准确率plt_train_acctrain_acc/总样本的数量。而train_loss表示的是一个训练轮次中所有batch的loss因此平均训练losstrain_loss/一个batch的大小。还有半监督训练集如果有数据的话也让其加入训练集进行训练。# 训练 def train_val(model, train_loader, val_loader, no_label_loader, lr, optimizer, device, epochs, thres, save_path): model model.to(device) #将模型放到设备上防止意外 semi_loader None plt_train_loss [] # 总训练loss plt_val_loss [] # 总验证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() #将模式设置成train模式 for x, y in train_loader: x, y x.to(device), y.to(device) y_pred model(x) bat_loss loss(y_pred, y) bat_loss.backward() optimizer.step() # 更新参数 optimizer.zero_grad() #梯度清零 train_loss bat_loss.cpu().item() #bat_loss.cpu()是张量bat_loss.cpu().item()取出张量的值 train_acc np.sum(np.argmax(y_pred.detach().cpu().numpy(), axis1) y.cpu().numpy()) #计算预测正确的数量 argmax()取出最大值的下标 plt_train_loss.append(train_loss / train_loader.__len__()) # 计算平均训练损失。train_loader.__len__()是本轮有多少个 batch plt_train_acc.append(train_acc / train_loader.dataset.__len__()) # 记录准确率。train_loader.dataset.__len__()是整个训练集有多少个样本 if semi_loader ! None: for x, y in semi_loader: x, y x.to(device), y.to(device) y_pred model(x) bat_loss loss(y_pred, y) bat_loss.backward() optimizer.step() # 更新参数 optimizer.zero_grad() # 梯度清零 semi_loss bat_loss.cpu().item() # bat_loss.cpu()是张量bat_loss.cpu().item()取出张量的值 semi_acc np.sum(np.argmax(y_pred.detach().cpu().numpy(), axis1) y.cpu().numpy()) print(半监督数据集的训练准确率为, semi_acc/train_loader.dataset.__len__()) model.eval() with torch.no_grad(): # 不计算梯度 for val_x, val_y in val_loader: val_x, val_y val_x.to(device), val_y.to(device) val_pred_y model(val_x) val_bat_loss loss(val_pred_y, val_y) val_loss val_bat_loss.cpu().item() val_acc np.sum(np.argmax(val_pred_y.detach().cpu().numpy(), axis1) val_y.cpu().numpy()) plt_val_loss.append(val_loss / val_loader.__len__()) #train_loader.__len__() 本轮 batch 数量train_loss / batch 数 得到平均每 batch 的 loss plt_val_acc.append(val_acc / val_loader.dataset.__len__()) # 记录准确率 if epoch % 5 0 and plt_val_acc[-1] 0.7: semi_loader get_semi_loader(no_label_loader, model, device, thres) if val_acc max_acc: max_acc val_acc torch.save(model, save_path) #save_path是最优模型的保存路径 print([%03d/%03d] %2.2f sec(s) train_loss: %.6f val_loss: %.6f train_acc: %.6f val_acc: %.6f% \ (epoch, epochs, time.time()-start_time, plt_train_loss[-1], plt_val_loss[-1], plt_train_acc[-1], plt_val_acc[-1]) ) 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()
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →