1.背景
- 任务输入:一系列手写数字图片,其中每张图片都是28x28的像素矩阵。
- 任务输出:经过了大小归一化和居中处理,输出对应的0~9数字标签。
参考项目代码:https://github.com/wuya11/easy-classification
1.1 说明
- 深度学习理论知识:参考黄海广博士组织翻译的吴恩达深度学习课程笔记,链接:《深度学习笔记》
- Pytorch常用函数,nn.Module,加载数据等功能,参考链接:《Pytorch中文文档》
- Mobilenetv3分类,了解卷积网络构建参数,开始输入和最终网络模型输出size,不做过多底层的了解。参考链接:《Mobilenetv3解析》
1.2 数据集来源
- 标签生成为目录,每个目录里面为具体的数字图片。比如0目录的图片均是手写数字为0的图片。
- 每个图像解析后,size为28*28。(若后续模型的入参需求为224*224,可以在此处调整图像大小。但不建议在一开始就修改,在图像转为张量处调整更合理,不同的模型入参不一定相同)
- 训练集、验证集和测试集各自的作用,参考说明:《数据集说明》
1.3 构建神经网络模型
2.数据处理
2.1 加载配置文件
cfg = { ### Global Set "model_name": "mobilenetv3", #shufflenetv2 adv-efficientnet-b2 se_resnext50_32x4d xception "class_number": 10, "random_seed":42, "cfg_verbose":True, "num_workers":4, ### Train Setting 'train_path':"./data/train", 'val_path':"./data/val", ### Test 'model_path':'output/mobilenetv3_e50_0.77000.pth',#test model 'eval_path':"./data/test",#test with label,get test acc 'test_path':"./data/test",#test without label, just show img result ### 更多参考项目中的config.py文件 }
from config import cfg path=cfg["train_path"] #获取config文件中的train_path变量
2.2 加载训练集图片信息
2.2.1 获取原始图像
train_data = getFileNames(self.cfg['train_path']) val_data = getFileNames(self.cfg['val_path']) def getFileNames(file_dir, tail_list=['.png','.jpg','.JPG','.PNG']): L=[] for root, dirs, files in os.walk(file_dir): for file in files: if os.path.splitext(file)[1] in tail_list: L.append(os.path.join(root, file)) return L
2.2.2 原始图像调整
# 随机处理训练集 train_data.sort(key = lambda x:os.path.basename(x)) train_data = np.array(train_data) random.shuffle(train_data) # 调整训练时的数据量 if self.cfg['try_to_train_items'] > 0: train_data = train_data[:self.cfg['try_to_train_items']] val_data = val_data[:self.cfg['try_to_train_items']]
2.2.3 图像调整
class TrainDataAug: def __init__(self, img_size): self.h = img_size[0] self.w = img_size[1] def __call__(self, img): # opencv img, BGR img = cv2.resize(img, (self.h,self.w)) img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB) img = Image.fromarray(img) return img
2.2.4 图像生成张量
my_normalize = getNormorlize(cfg['model_name']) data_aug_train = TrainDataAug(cfg['img_size']) transforms.Compose([ # 调整图像大小 data_aug_train, # 图像转换为张量 transforms.ToTensor(), # 归一化处理 my_normalize, ])
def getNormorlize(model_name): if model_name in ['mobilenetv2','mobilenetv3']: my_normalize = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) elif model_name == 'xception': my_normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) elif "adv-eff" in model_name: my_normalize = transforms.Lambda(lambda img: img * 2.0 - 1.0) elif "resnex" in model_name or 'eff' in model_name or 'RegNet' in model_name: my_normalize = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) #my_normalize = transforms.Normalize([0.4783, 0.4559, 0.4570], [0.2566, 0.2544, 0.2522]) elif "EN-B" in model_name: my_normalize = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) else: print("[Info] Not set normalize type! Use defalut imagenet normalization.") my_normalize = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) return my_normalize
class TensorDatasetTestClassify(Dataset): def __init__(self, train_jpg, transform=None): self.train_jpg = train_jpg if transform is not None: self.transform = transform else: self.transform = None def __getitem__(self, index): img = cv2.imread(self.train_jpg[index]) if self.transform is not None: img = self.transform(img) return img, self.train_jpg[index] def __len__(self): return len(self.train_jpg)
my_dataloader = TensorDatasetTestClassify train_data = getFileNames(self.cfg['train_path']) train_loader = torch.utils.data.DataLoader( my_dataloader(train_data, transforms.Compose([ data_aug_train, transforms.ToTensor(), my_normalize, ])), batch_size=cfg['batch_size'], shuffle=True, num_workers=cfg['num_workers'], pin_memory=True)
2.2.5 小节总结
- 获取到原始图像信息时,需要随机打乱图像,避免训练集精度问题。
- 基于Pytorch 框架,自定义DataSet时,需定义item返回的对象信息(返回图片信息,图片张量信息,标签信息等可自定义)。
- 图像转换为张量时,引入归一化,对生成的张量信息做处理。
- DataLoade数据加载器,分组跑数据,提升效率,也可以自行编写for实现,但框架已有,调用框架的方便。
2.3 加载其他图片信息
3.模型设计
3.1 Pytorch 构建网络
- 经典的网络模型,目前均可在网上找到开源的网络模型构建代码。Pytorch也封装了部分网络模型的代码,详情参考:《torchvision.models》
- 自定义网络模型,主要是构建网络骨干,基于卷积层,激活函数等组合使用。最后根据任务分类,构建对应的全连接层。详情参考:《PyTorch-OpCounter》,《构建神经网络常用实现函数》
- 构建Mobilenetv3网络,可直接调用torchvision.models中已经封装好的模型。自定义实现也可参考《Pytorch:图像分类经典网络_MobileNet(V1、V2、V3)》
3.2 预训练模型
self.pretrain_model = MobileNetV3() # 预训练模型权重路径 if self.cfg['pretrained']: state_dict = torch.load(self.cfg['pretrained']) # 模型与预训练不一致时,逻辑处理 state_dict = {k.replace('pretrain_', ''):v for k, v in state_dict.items()} state_dict = {k.replace('model.', ''): v for k, v in state_dict.items()} # 跳过不一致的地方 self.pretrain_model.load_state_dict(state_dict,False)
3.3 构建全连接层
def forward(self, x): x = self.features(x) x = x.mean(3).mean(2) #张量维度换 4转2 last_channel=1280 # mobilenetv3 large最终输出为1280 # 构建一个全连接层 self.classifier = nn.Sequential( nn.Dropout(p=dropout), # refer to paper section 6 nn.Linear(last_channel, 10), #数字0-9共10个分类 x = self.classifier(x) return x
3.4 小节总结
- 经典网络的实现,可当作黑盒对待。不做过多深入的研究。参考网络模型实现代码时,注意开始的输入张量和最终骨干网络的输出张量信息。
- 网上部分博客,关于Mobilenetv3-small,最终输出有写 1280,也有写1024的,参考论文应该是1024,最终层这种也可自定义,但建议还是以论文为准。
- 选用一个经典的网络模型,主要是使用其骨干层训练的结果,在根据自身任务的分类特性,做全连接层处理。
- 训练一个新任务时,根据使用的分类模型,一般不建议从零开始训练,可基于已经存在的模型权重,做预训练微调处理。
4.训练配置
class ModelRunner(): def __init__(self, cfg, model): # 定义加载配置文件 self.cfg = cfg # 定义设备信息 if self.cfg['GPU_ID'] != '' : self.device = torch.device("cuda") else: self.device = torch.device("cpu") self.model = model.to(self.device) # gpu加速,cpu模式无效 self.scaler = torch.cuda.amp.GradScaler() # loss 定义损失函数 self.loss_func = getLossFunc(self.device, cfg) # 定义优化器 self.optimizer = getOptimizer(self.cfg['optimizer'], self.model, self.cfg['learning_rate'], self.cfg['weight_decay']) # 定义调整学习率的策略 self.scheduler = getSchedu(self.cfg['scheduler'], self.optimizer)
4.1 设备硬件资源
4.2 损失函数
class CrossEntropyLoss(nn.Module): def __init__(self, label_smooth=0, weight=None): super().__init__() self.weight = weight self.label_smooth = label_smooth self.epsilon = 1e-7 def forward(self, x, y, sample_weights=0, sample_weight_img_names=None): one_hot_label = F.one_hot(y, x.shape[1]) if self.label_smooth: one_hot_label = labelSmooth(one_hot_label, self.label_smooth) #y_pred = F.log_softmax(x, dim=1) # equal below two lines y_softmax = F.softmax(x, 1) #print(y_softmax) y_softmax = torch.clamp(y_softmax, self.epsilon, 1.0-self.epsilon)# avoid nan y_softmaxlog = torch.log(y_softmax) # original CE loss loss = -one_hot_label * y_softmaxlog loss = torch.mean(torch.sum(loss, -1)) return loss
4.3 优化器
- 内层循环:负责整个数据集的一次遍历,遍历数据集采用分批次(batch)方式。
- 外层循环:定义遍历数据集的次数,如训练中外层循环100次,训练次数可通过配置参数设置。
def getOptimizer(optims, model, learning_rate, weight_decay): if optims=='Adam': optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=weight_decay) elif optims=='AdamW': optimizer = optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=weight_decay) elif optims=='SGD': optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9, weight_decay=weight_decay) elif optims=='AdaBelief': optimizer = AdaBelief(model.parameters(), lr=learning_rate, eps=1e-12, betas=(0.9,0.999)) elif optims=='Ranger': optimizer = Ranger(model.parameters(), lr=learning_rate, weight_decay=weight_decay) else: raise Exception("Unkown getSchedu: ", optims) return optimizer
4.4 学习率调整
def getSchedu(schedu, optimizer): if 'default' in schedu: factor = float(schedu.strip().split('-')[1]) patience = int(schedu.strip().split('-')[2]) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=factor, patience=patience,min_lr=0.000001) elif 'step' in schedu: step_size = int(schedu.strip().split('-')[1]) gamma = int(schedu.strip().split('-')[2]) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=step_size, gamma=gamma, last_epoch=-1) elif 'SGDR' in schedu: T_0 = int(schedu.strip().split('-')[1]) T_mult = int(schedu.strip().split('-')[2]) scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=T_0, T_mult=T_mult) elif 'multi' in schedu: milestones = [int(x) for x in schedu.strip().split('-')[1].split(',')] gamma = float(schedu.strip().split('-')[2]) scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=gamma, last_epoch=-1) else: raise Exception("Unkown getSchedu: ", schedu) return scheduler
4.5 小节总结
- 在训练之前,首先确定硬件环境,cpu跑的慢,能用GPU的,优先使用GPU。Pytorch版本安装时,也需根据硬件配置选择,安装地址:《Pytorch Get Start》。
- 损失函数可用Pytorch封装了常用损失函数,或参考数学公式自定义编写。损失函数实现逻辑可不过分深究,当作黑盒模式对待。正确传入损失函数入参的参数,在不同任务场景下,测试或参考论文,选择或编写合适的损失函数。
- 优化器,学习率调整均可当作黑盒模式对待。会传参和调用即可。优化器,学习率存在多种解决方案,在不同任务场景下,测试或参考论文,选择合适该任务的方案。
5.训练过程
5.1 训练流程概述
def train(self, train_loader, val_loader): # step 1:定义训练开始时一些全局的变量,如是否过早停止表示,执行时间等 self.onTrainStart() # step 2: 外层大轮询次数,每次轮询 全量 train_loader,val_loader for epoch in range(self.cfg['epochs']): # step 3: 非必须,过滤处理部分次数,做冻结训练处理 self.freezeBeforeLinear(epoch, self.cfg['freeze_nonlinear_epoch']) # step 4: 训练集数据处理 self.onTrainStep(train_loader, epoch) # step 5: 验证集数据处理,最好训练模型权重保存,过早结束逻辑处理 self.onValidation(val_loader, epoch) # step 6: 满足过早结束条件时,退出循环,结束训练 if self.earlystop: break # step 7:训练过程结束,释放资源 self.onTrainEnd()
5.2 冻结训练
# freeze_epochs :设置冻结的标识,小于该值时冻结 # epoch: 轮询次数值,从0开始 def freezeBeforeLinear(self, epoch, freeze_epochs=2): if epoch < freeze_epochs: for child in list(self.model.children())[:-1]: for param in child.parameters(): param.requires_grad = False # 等于标识值后,解冻 elif epoch == freeze_epochs: for child in list(self.model.children())[:-1]: for param in child.parameters(): param.requires_grad = True
5.3 训练数据
# 定义模型为训练 self.model.train() # 轮询处理批次数据 for batch_idx, (data, target, img_names) in enumerate(train_loader): one_batch_time_start = time.time() # 来源于dataset对象,item中定义的对象 target = target.to(self.device) # 张量复制到硬件资源上 data = data.to(self.device) # gpu模式下,加快训练,混合精度 with torch.cuda.amp.autocast(): # 模型训练输出张量,参考模型定义的forward返回方法 output = self.model(data).double() # 计算损失函数,可自定义或调用PyTorch常用的 loss = self.loss_func(output, target, self.cfg['sample_weights'], sample_weight_img_names=img_names) # 一个batchSize 求和 total_loss += loss.item() # 把梯度置零 self.optimizer.zero_grad() # loss.backward() #计算梯度 # self.optimizer.step() #更新参数 # 基于GPU scaler 加速 self.scaler.scale(loss).backward() self.scaler.step(self.optimizer) self.scaler.update() ### 返回 batchSize个最大张量值对应的数组下标值 pred = output.max(1, keepdim=True)[1] # 训练图像对应的 分类标签 if len(target.shape) > 1: target = target.max(1, keepdim=True)[1] # 统计一组数据batchSize 中训练出来的分类值与 实际图像分类标签一样的数据条数 correct += pred.eq(target.view_as(pred)).sum().item() # 统计总训练数据条数 count += len(data) # 计算准确率 train_acc = correct / count train_loss = total_loss / count
5.4 验证数据
# 定义模型为验证 self.model.eval() # 重点,验证流程定义不求导 with torch.no_grad(): pres = [] labels = [] # 基于批次迭代验证数据 for (data, target, img_names) in val_loader: data, target = data.to(self.device), target.to(self.device) # GPU下加速处理 with torch.cuda.amp.autocast(): # 模型输出张量,参考模型定义的forward返回方法 output = self.model(data).double() # 定义交叉损失函数 self.val_loss += self.loss_func(output, target).item() # sum up batch loss pred_score = nn.Softmax(dim=1)(output) # print(pred_score.shape) pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability if self.cfg['use_distill']: target = target.max(1, keepdim=True)[1] # 真实值与验证值一致求和数量 self.correct += pred.eq(target.view_as(pred)).sum().item() batch_pred_score = pred_score.data.cpu().numpy().tolist() batch_label_score = target.data.cpu().numpy().tolist() pres.extend(batch_pred_score) labels.extend(batch_label_score) # print('\n',output[0],img_names[0]) pres = np.array(pres) labels = np.array(labels) # print(pres.shape, labels.shape) self.val_loss /= len(val_loader.dataset) # 计算准确率 self.val_acc = self.correct / len(val_loader.dataset) # 当次值记录为最优,后续应用和历史最优值做比较 self.best_score = self.val_acc
5.5 保存最优的模型权重
def checkpoint(self, epoch): # 当前值小于历史记录的值时 if self.val_acc <= self.early_stop_value: if self.best_score <= self.early_stop_value: if self.cfg['save_best_only']: pass else: save_name = '%s_e%d_%.5f.pth' % (self.cfg['model_name'], epoch + 1, self.best_score) self.last_save_path = os.path.join(self.cfg['save_dir'], save_name) self.modelSave(self.last_save_path) else: # 保存最优权重信息 if self.cfg['save_one_only']: if self.last_save_path is not None and os.path.exists(self.last_save_path): os.remove(self.last_save_path) save_name = '%s_e%d_%.5f.pth' % (self.cfg['model_name'], epoch + 1, self.best_score) self.last_save_path = os.path.join(self.cfg['save_dir'], save_name) torch.save(self.model.state_dict(), save_name)
5.6 提前终止
def earlyStop(self, epoch): ### earlystop 配置下降次数,如当前值小于历史值出现7次,就提前终止 if self.val_acc > self.early_stop_value: self.early_stop_value = self.val_acc if self.best_score > self.early_stop_value: self.early_stop_value = self.best_score self.early_stop_dist = 0 self.early_stop_dist += 1 if self.early_stop_dist > self.cfg['early_stop_patient']: self.best_epoch = epoch - self.cfg['early_stop_patient'] + 1 print("[INFO] Early Stop with patient %d , best is Epoch - %d :%f" % ( self.cfg['early_stop_patient'], self.best_epoch, self.early_stop_value)) self.earlystop = True if epoch + 1 == self.cfg['epochs']: self.best_epoch = epoch - self.early_stop_dist + 2 print("[INFO] Finish trainging , best is Epoch - %d :%f" % (self.best_epoch, self.early_stop_value)) self.earlystop = True
5.7 释放资源
def onTrainEnd(self): # 删除模型实例 del self.model # 垃圾回收 gc.collect() # 清空gpu上面的缓存 torch.cuda.empty_cache()
5.8 小节总结
- 训练过程综合使用了损失函数,优化器,学习率,梯度下降等知识,一般基于内外两个大循环训练数据,最终产生模型的权重参数,并保存下来。
- 保存的模型权重可用于评估和实际使用。也可以当作其他任务的预加载模型权重。
- 训练过程是训练集数据,验证集数据交替进行的,单独的只进行训练集数据的处理无明细意义。
- 训练集数据需要求导(冻结训练层除外),做前向计算和反向传播处理。验证集不需要,在训练之后,只做验证结果的评分处理。
6.评估与应用
6.1 评估与应用
# 加载训练的模型权重 runner.modelLoad(cfg['model_path']) # 评估跑数据 runner.evaluate(train_loader) # 评估函数 def evaluate(self, data_loader): self.model.eval() correct = 0 # 验证不求导 with torch.no_grad(): pres = [] labels = [] for (data, target, img_names) in data_loader: data, target = data.to(self.device), target.to(self.device) with torch.cuda.amp.autocast(): output = self.model(data).double() pred_score = nn.Softmax(dim=1)(output) pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability if self.cfg['use_distill']: target = target.max(1, keepdim=True)[1] correct += pred.eq(target.view_as(pred)).sum().item() batch_pred_score = pred_score.data.cpu().numpy().tolist() batch_label_score = target.data.cpu().numpy().tolist() pres.extend(batch_pred_score) labels.extend(batch_label_score) pres = np.array(pres) labels = np.array(labels) # acc评分 acc = correct / len(data_loader.dataset) print('[Info] acc: {:.3f}% \n'.format(100. * acc)) # f1评分 if 'F1' in self.cfg['metrics']: precision, recall, f1_score = getF1(pres, labels) print(' precision: {:.5f}, recall: {:.5f}, f1_score: {:.5f}\n'.format( precision, recall, f1_score))
# 加载权重 runner.modelLoad(cfg['model_path']) # 开始预测 res_dict = runner.predict(test_loader) # 预测函数 def predict(self, data_loader): self.model.eval() correct = 0 res_dict = {} with torch.no_grad(): pres = [] labels = [] for (data, img_names) in data_loader: data = data.to(self.device) output = self.model(data).double() pred_score = nn.Softmax(dim=1)(output) pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability batch_pred_score = pred_score.data.cpu().numpy().tolist() for i in range(len(batch_pred_score)): res_dict[os.path.basename(img_names[i])] = pred[i].item() # 保存图像与预测结果 res_df = pd.DataFrame.from_dict(res_dict, orient='index', columns=['label']) res_df = res_df.reset_index().rename(columns={'index':'image_id'}) res_df.to_csv(os.path.join(cfg['save_dir'], 'pre.csv'), index=False,header=True)
6.2 分类结果展示
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:深度学习-网络训练流程说明 - Python技术站