-
只训练模型的一部分参数例如,只想训练上面的model中的layer参数,而保持layer2的参数不动。可以如下设置Optimizer:model = Net() optimizer_Adam = torch.optim.Adam(model.layer.parameters(), lr=0.1) # 只传入layer层的参数,就可以只更新layer层的参数而不影响其他参数。不同部分的参数设置不同的学习率(以及其他属性)例如,要想使model的layer参数学习率为0.1,layer2的参数学习率为0.2,可以如下设置Optimizer:model = Net() params_dict = [{'params': model.layer.parameters(), 'lr': 0.1}, {'params': model.layer2.parameters(), 'lr': 0.2}] optimizer_Adam = torch.optim.Adam(params_dict)这种方法更为灵活,手动构造一个params_dict列表来初始化Optimizer。注意,字典中的参数部分的 key 必须为 ‘params’。这样就可以灵活的设置Optimizer啦。动态更新learning rate了解了Optimizer的基本结构和使用方法,接下来就可以看一下,如何在训练过程中动态更新learning rate。手动修改lr上面我们了解到,Optimizer的每一组参数维护一个lr,因此,最直接的方法就是我们在训练过程中手动修改Optimizer中对应的lr的值:model = Net() # 生成网络 optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 生成优化器 lr_list = [] for epoch in range(100): # 假设迭代100次 if epoch % 5 == 0: # 每迭代5次,更新一次学习率 for params in optimizer_Adam.param_groups: # 遍历Optimizer中的每一组参数 params['lr'] *= 0.9 # 将该组参数的学习率 * 0.9 # params['weight_decay'] = 0.5 # 当然也可以修改其他属性 lr_list.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list, color='r') plt.show()orch.optim.lr_schedulertorch.optim.lr_scheduler包中提供了一些类,用于动态修改lr。torch.optim.lr_scheduler.LambdaLrtorch.optim.lr_scheduler.StepLRtorch.optim.lr_scheduler.MultiStepLRtorch.optim.lr_scheduler.ExponentialLRtorch.optim.lr_sheduler.CosineAnneaingLRtorch.optim.lr_scheduler.ReduceLROnPlateau注意: pytorch 1.1.0版本之后,在创建了lr_scheduler对象之后,会自动执行第一次lr更新(可以理解为执行一次scheduler.step())。因此,在使用的时候,需要先调用optimizer.step(),再调用scheduler.step()。如果创建了lr_scheduler对象之后,先调用scheduler.step(),再调用optimizer.step(),则会跳过了第一个学习率的值。# 调用顺序 loss.backward() optimizer.step() scheduler.step() ...注意: 创建scheduler时,所传入的Optimizer的param_groups必须有一个initial_lr键作为初始学习率。如果last_epoch=-1,则用于初始化的Optimizer可以没有initial_lr键,以 lr 键初始化为initial_lr。torch.optim.lr_scheduler.LambdaLrtorch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1)Optimizer:优化器实例lr_lambda:是一个函数(常用lambda表达式)或函数列表,该函数接收一个int参数(epoch),然后计算出一个系数α,最后学习率更新为。其中lr_lambda如果传入多个函数的list的话,则对应每组param_groups的学习率调整策略。last_epoch:(int)上一个epoch数。默认为-1,且当last_epoch=-1时,将lr设置为initial_lr。第一次更新lr时,就按照epoch = last_epoch + 1更新。(比如last_epoch = 1,则第一次lr更新时,就将epoch=2传入上面的lr_lambda函数,得出系数α需要注意的是,该scheduler每次lr更新,是用initial_lr 乘以系数 α,而不是用上一次的lr 乘以系数 α,即orch.optim.lr_scheduler.StepLRtorch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)每迭代step_size次,学习率乘以gamma。model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 创建scheduler,每迭代5次,学习率衰减一半 scheduler = torch.optim.lr_scheduler.StepLR(optimizer_Adam, step_size=5, gamma=0.5, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()orch.optim.lr_scheduler.MultiStepLRtorch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)多段衰减法。milestones传入一个list,指定多个epoch数,每迭代到指定的epoch次数时,lr乘以gamma。例子:model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 初始 lr=0.1 # lr 变化 # 0 - 20 epoch: 0.1 # 21 - 40 epoch: 0.05 # 41 - 60 epoch: 0.025 # 60 - 80 epoch: 0.0125 # 80 - end epoch: 0.01125 scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer_Adam, milestones=[20, 40, 60, 80], gamma=0.5, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()torch.optim.lr_scheduler.ExponentialLRtorch.optim.lr_scheduler.ExponentialLR(optimizer, gamma, last_epoch=-1)每个epoch按指数衰减 lr。model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 指数衰减学习率,衰减率为 gamma=0.9 scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer_Adam, gamma=0.9, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()orch.optim.lr_scheduler.CosineAnnealingLRtorch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max, eta_min=0, last_epoch=-1)按照三角函数规则来更新学习率。 表示最小学习率, 即正弦函数最低点 表示最大学习率, 设置为initial_Ir (在last_epoch=-1 时, 即为Ir)。 表示当前epoch数 表示 个 周期所对应的epoch数值model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1) # 周期为50epoch,lr最小值为0(默认) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer_Adam, eta_min=0, T_max=25, last_epoch=-1) lr_list_1 = [] for epoch in range(100): scheduler.step() lr_list_1.append(optimizer_Adam.state_dict()['param_groups'][0]['lr']) plt.plot(range(100), lr_list_1, color='r', label='lr') plt.legend() plt.show()orch.optim.lr_scheduler.ReduceLROnPlateautorch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10, verbose=False, threshold=0.0001, threshold_mode='rel', cooldown=0, min_lr=0, eps=1e-08)根据指定的指标量来调整学习率。如果指标量停止变化时,就减小学习率。参数:mode:(str),从(min, max)中选择。min:如果指定量不再下降,就减小lrmax:如果指定量不再上升,就减小lrfactor:(float),衰减因子,每次更新lr = lr * factorpatience:(int),容忍度,如果经过patience次迭代后,指标没有变化(上升或下降),就更新lr。verbose:(bool),每次更新lr,是否向std输出。threshold:(float),阈值,对于制定的指标只有超过阈值才算有变化threshold_mode:(str),从(rel,abs)总选择。性能衡量方式。max模式下:dynamic_threshold = best + thresholdmin模式下:dynamic_threshold = best - thresholdmax模式下:dynamic_threshold = best * ( 1 + threshold )min模式下:dynamic_threshold = best * ( 1 - threshold )rel:abs:cooldown:(int),每次调整lr之后,冷却cooldown个epoch,避免lr下降过快min_lr:(float or list),学习率最小值。如果给定一个标量值,就param_groups中所有组都设置该最小值;也可以用一个list为每组指定一个最小值。eps:(float),lr变化最小值,如果lr的两次变化差距小于eps,则忽略这次变化。optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9) scheduler = ReduceLROnPlateau(optimizer, 'min') for epoch in range(10): train(...) val_loss = validate(...) # Note that step should be called after validate() scheduler.step(val_loss)
-
导读 在很多学习过程中,都会采用动态调整学习率的方法。刚开始训练的时候,学习率设置大一点,以加快学习速度;之后逐渐减小学习率,来寻找最优解。那么在Pytorch中,如在训练过程中动态地调整学习率呢? 学习率设置对于学习过程来说相当重要。学习率过低会导致学习速度太慢,学习率过高又容易导致难以收敛。在很多学习过程中,都会采用动态调整学习率的方法。刚开始训练的时候,学习率设置大一点,以加快学习速度;之后逐渐减小学习率,来寻找最优解。那么在Pytorch中,如在训练过程中动态地调整学习率呢?目录优化器Optimizer只训练模型的一部分参数不同部分的参数设置不同的学习率(以及其他属性)Optimizer基本属性optimizer基本方法动态更新learning ratetorch.optim.lr_schedulertorch.optim.lr_scheduler.LambdaLrtorch.optim.lr_scheduler.StepLRtorch.optim.lr_scheduler.MultiStepLRtorch.optim.lr_scheduler.ExponentialLRtorch.optim.lr_scheduler.CosineAnnealingLRtorch.optim.lr_scheduler.ReduceLROnPlateau手动修改lr优化器Optimizer在说学习率调整方法之前,先来了解一下Pytorch中的优化器Optimizer机制。用过Pytorch的都知道,模型训练时的固定搭配。loss.backward() optimizer.step() optimizer.zero_grad() ...简单来说,loss.backward()就是反向计算出各参数的梯度,然后optimizer.step()就是更新网络中的参数,optimizer.zero_grad()将这一轮的梯度清零,防止这一轮的梯度影响下一轮的更新。常用优化器都在torch.optim包中,因此需要先导入包:import torch.optim.Adam import torch.optim.SGD这里以常用的Adam优化器和SGD优化器为例,介绍一下Pytorch中的优化器使用方法。假设我们有一个网络如下,下面的例子都以此网络作为例子:class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.layer = nn.Linear(10, 2) self.layer2 = nn.Linear(2, 10) def forward(self, input): return self.layer(input) # Optimizer基本属性所有Optimizer公有的一些基本属性:lr: learning rate,学习率eps: 学习率最小值,在动态更新学习率时,学习率最小不会小于该值。weight_decay: 权值衰减。相当于对参数进行L2正则化(使模型复杂度尽可能低,防止过拟合),该值可以理解为正则化项的系数。betas: (待研究)amsgrad: (bool)(待研究)每个Optimizer都维护一个param_groups的list。该list中维护需要优化的参数以及对应的属性设置。optimizer基本方法add_param_group(param_group): 为optimizer的param_groups增加一个参数组。这在微调预先训练的网络时非常有用,因为冻结层可以训练并随着训练的进行添加到优化器中。load_state_dict(state_dict): 加载optimizer state。参数必须是optimizer.state_dict()返回的对象。state_dict(): 返回一个dict,包含optimizer的状态:state和param_groups。step(closure): 执行一次参数更新过程。zero_grad(): 清除所有已经更新的参数的梯度。我们在构造优化器时,最简单的方法通常如下:model = Net() optimizer_Adam = torch.optim.Adam(model.parameters(), lr=0.1)model.parameters()返回网络model的全部参数。将model的全部参数传入Adam中构造出一个Adam优化器,并设置 learning rate=0.1。因此该 Adam 优化器的 param_groups 维护的就是模型 model 的全部参数,并且学习率为0.1。这样在调用optimizer_Adam.step()时,就会对model的全部参数进行更新。Optimizer的param_groups是一个list,其中的每个元素都是一组独立的参数,以dict的方式存储。结构如下:-param_groups -0(dict) # 第一组参数 params: # 维护要更新的参数 lr: # 该组参数的学习率 betas: eps: # 该组参数的学习率最小值 weight_decay: # 该组参数的权重衰减系数 amsgrad: -1(dict) # 第二组参数 -2(dict) # 第三组参数 ... ...这样可以实现很多灵活的操作,比如以下。
-
训练框架代码讲解训练框架相关代码实现在 ocr_by_transformer.py 文件中下面开始逐步讲解代码,主要有以下几个部分:构建dataset → 图像预处理、label处理等;模型构建 → backbone + transformer;模型训练推理 → 贪心解码下面一步步来看4.1 准备工作首先导入后面需要用到的库import os import time import copy from PIL import Image # 在线数据集相关包 from tensorbay import GAS from tensorbay.dataset import Dataset # torch相关包 import torch import torch.nn as nn from torch.autograd import Variable import torchvision.models as models import torchvision.transforms as transforms # 导入工具类包 from analysis_recognition_dataset import load_lbl2id_map, statistics_max_len_label from transformer import * from train_utils import *然后设置一些基础的参数device = torch.device('cuda') # 'cpu'或者'cuda' nrof_epochs = 1500 # 迭代次数,1500,根据需求进行修正 batch_size = 64 # 批量大小,64,根据需求进行修正 model_save_path = './log/ex1_ocr_model.pth'在线获取图像数据,并读取图像label中字符与其id的映射字典,后续Dataset创建需要使用。# GAS凭证 KEY = 'Accesskey-fd26cc098604c68a99d3bf7f87cd480a' gas = GAS(KEY) # 在线获取数据集 dataset_online = Dataset("ICDAR2015", gas) dataset_online.enable_cache('./data') # 开启本地缓存 # 获取训练集和验证集 train_data_online = dataset_online["train"] valid_data_online = dataset_online['valid'] # 读取label-id映射关系记录文件 lbl2id_map_path = os.path.join('./', 'lbl2id_map.txt') lbl2id_map, id2lbl_map = load_lbl2id_map(lbl2id_map_path) # 统计数据集中出现的所有的label中包含字符最多的有多少字符,数据集构造gt(ground truth)信息需要用到 train_max_label_len = statistics_max_len_label(train_data_online) valid_max_label_len = statistics_max_len_label(valid_data_online) # 数据集中字符数最多的一个case作为制作的gt的sequence_len sequence_len = max(train_max_label_len, valid_max_label_len) 4.2 Dataset构建下面来介绍Dataset构建相关内容,首先思考下如何进行图片预处理比较合理。图片预处理方案假设图片尺寸为 经过网络后的特征图尺寸为 基于之前对于数据集的分析,图片基本都是水平长条状的,图像内容是水平排列的字符组成的单词。那么图片空间上同一纵向切片的位置,基本只有一个字符,因此纵向分辨率不需要很大,那么取 即可;而横向的分辨率需要大一些,我们需要有不同的embedding来编码水平方向上不同字符的特征。这里,我们就用最经典的resnet18网络作为backbone,由于其下采样倍数为32,最后一层特征图channel数为512,那么:那么输入图片的宽度如何确定呢?这里给出两种方案,如下图所示:方法一:设定一个固定尺寸,将图像保持其宽高比进行resize,右侧空余区域进行padding;方法二:直接将原始图像强制resize到一个预设的固定尺寸。注:这里不妨先思考下,你觉得哪种方案比较好呢?作者选择了方法一,因为图片的宽高比和图片中单词的字符数量是大致呈正比的,如果预处理时保持住原图片的宽高比,那么特征图上每一个像素对应原图上字符区域的范围就是基本稳定的,这样或许有更好的预测效果。这里还有个细节,观察上图你会发现,每个宽:高=1:1的区域内,基本都分布着2-3个字符,因此我们实际操作时也没有严格的保持宽高比不变,而是将宽高比提升了3倍,即先将原始图片宽度拉长到原来的3倍,再保持宽高比,将高resize到32。注:这里建议再次停下来思考下,刚刚这个细节又是为什么?这样做的目的是让图片上每一个字符,都有至少一个特征图上的像素与之对应,而不是特征图宽维度上一个像素,同时编码了原图中的多个字符的信息,这样我认为会对transformer的预测带来不必要的困难(仅是个人观点,欢迎讨论)。确定了resize方案, 具体设置为多少呢?结合前面我们对数据集做分析时的两个重要指标,数据集label中最长字符数为21,最长的宽高比8.6,我们将最终的宽高比设置为 24:1,因此汇总一下各个参数的设置:相关代码实现:# ---------------- # 图片预处理 # ---------------- # load image with img_data.open() as fp: img = Image.open(fp).convert('RGB') # 对图片进行大致等比例的缩放 # 将高缩放到32,宽大致等比例缩放,但要被32整除 w, h = img.size ratio = round((w / h) * 3) # 将宽拉长3倍,然后四舍五入 if ratio == 0: ratio = 1 if ratio > self.max_ratio: ratio = self.max_ratio h_new = 32 w_new = h_new * ratio img_resize = img.resize((w_new, h_new), Image.BILINEAR) # 对图片右半边进行padding,使得宽/高比例固定=self.max_ratio img_padd = Image.new('RGB', (32*self.max_ratio, 32), (0,0,0)) img_padd.paste(img_resize, (0, 0)) 图像增广图像增广并不是重点,这里我们除了上述的resize方案外,仅对图像进行常规的随机颜色变换和归一化操作。完整代码构建Dataset的完整代码如下:class Recognition_Dataset(object): def __init__(self, segment, lbl2id_map, sequence_len, max_ratio, pad=0): self.data = segment self.lbl2id_map = lbl2id_map self.pad = pad # padding标识符的id,默认0 self.sequence_len = sequence_len # 序列长度 self.max_ratio = max_ratio * 3 # 将宽拉长3倍 # 定义随机颜色变换 self.color_trans = transforms.ColorJitter(0.1, 0.1, 0.1) # 定义 Normalize self.trans_Normalize = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]), ]) def __getitem__(self, index): """ 获取对应index的图像和ground truth label,并视情况进行数据增强 """ img_data = self.data[index] lbl_str = img_data.label.classification.category # 图像标签 # ---------------- # 图片预处理 # ---------------- # load image with img_data.open() as fp: img = Image.open(fp).convert('RGB') # 对图片进行大致等比例的缩放 # 将高缩放到32,宽大致等比例缩放,但要被32整除 w, h = img.size ratio = round((w / h) * 3) # 将宽拉长3倍,然后四舍五入 if ratio == 0: ratio = 1 if ratio > self.max_ratio: ratio = self.max_ratio h_new = 32 w_new = h_new * ratio img_resize = img.resize((w_new, h_new), Image.BILINEAR) # 对图片右半边进行padding,使得宽/高比例固定=self.max_ratio img_padd = Image.new('RGB', (32*self.max_ratio, 32), (0,0,0)) img_padd.paste(img_resize, (0, 0)) # 随机颜色变换 img_input = self.color_trans(img_padd) # Normalize img_input = self.trans_Normalize(img_input) # ---------------- # label处理 # ---------------- # 构造encoder的mask encode_mask = [1] * ratio + [0] * (self.max_ratio - ratio) encode_mask = torch.tensor(encode_mask) encode_mask = (encode_mask != 0).unsqueeze(0) # 构造ground truth label gt = [] gt.append(1) # 先添加句子起始符 for lbl in lbl_str: gt.append(self.lbl2id_map[lbl]) gt.append(2) for i in range(len(lbl_str), self.sequence_len): # 除去起始符终止符,lbl长度为sequence_len,剩下的padding gt.append(0) # 截断为预设的最大序列长度 gt = gt[:self.sequence_len] # decoder的输入 decode_in = gt[:-1] decode_in = torch.tensor(decode_in) # decoder的输出 decode_out = gt[1:] decode_out = torch.tensor(decode_out) # decoder的mask decode_mask = self.make_std_mask(decode_in, self.pad) # 有效tokens数 ntokens = (decode_out != self.pad).data.sum() return img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens @staticmethod def make_std_mask(tgt, pad): """ Create a mask to hide padding and future words. padd 和 future words 均在mask中用0表示 """ tgt_mask = (tgt != pad) tgt_mask = tgt_mask & Variable(subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data)) tgt_mask = tgt_mask.squeeze(0) # subsequent返回值的shape是(1, N, N) return tgt_mask def __len__(self): return len(self.data)上面的代码中还涉及到几个和label处理相关的细节,属于Transformer训练相关的逻辑,这里再简单提一下:encode_mask由于我们对图像进行了尺寸调整,并根据需求对图像进行了padding,而padding的位置是没有包含有效信息的,为此需要根据padding比例构造相应encode_mask,让transformer在计算时忽略这部分无意义的区域。label处理本实验使用的预测标签与机器翻译模型训练时的标签基本一致,因此在处理方式中差异较小。标签处理中,将label中字符转换成其对应id,并在句子开始添加起始符,句子最后添加终止符,并在不满足sequence_len长度时在剩余位置进行padding(0补位)。decode_mask一般的在decoder中我们会根据label的sequence_len生成一个上三角阵形式的mask,mask的每一行便可以控制当前time_step时,只允许decoder获取当前步时之前的字符信息,而禁止获取未来时刻的字符信息,这防止了模型训练时的作弊行为。decode_mask经过一个特殊的函数 make_std_mask() 进行生成。同时,decoder的label制作同样要考虑上对padding的部分进行mask,所以decode_mask在label被padding对应的位置处也应该进行写成False。生成的decode_mask如下图所示:以上是构建Dataset的所有细节,进而我们可以构建出DataLoader供训练使用# 构造 dataloader max_ratio = 8 # 图片预处理时 宽/高的最大值,不超过就保比例resize,超过会强行压缩 train_dataset = Recognition_Dataset(train_data_online, lbl2id_map, sequence_len, max_ratio, pad=0) valid_dataset = Recognition_Dataset(valid_data_online, lbl2id_map, sequence_len, max_ratio, pad=0) # loader size info: # --> img_input: [batch_size, c, h, w] --> [64, 3, 32, 32*8*3] # --> encode_mask: [batch_size, h/32, w/32] --> [64, 1, 24] 本文backbone采用的32倍下采样,所以除以32 # --> decode_in: [bs, sequence_len-1] --> [64, 20] # --> decode_out: [bs, sequence_len-1] --> [64, 20] # --> decode_mask: [bs, sequence_len-1, sequence_len-1] --> [64, 20, 20] # --> ntokens: [bs] --> [64] train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4) valid_loader = torch.utils.data.DataLoader(valid_dataset, batch_size=batch_size, shuffle=False, num_workers=4)4.3 模型构建代码通过 make_ocr_model 和 OCR_EncoderDecoder 类完成模型结构搭建。可以从 make_ocr_model 这个函数看起,该函数首先调用了pytorch中预训练的Resnet-18作为backbone以提取图像特征,此处也可以根据自己需要调整为其他的网络,但需要重点关注的是网络的下采样倍数,以及最后一层特征图的channel_num,相关模块的参数需要同步调整。之后调用了 OCR_EncoderDecoder 类完成transformer的搭建。最后对模型参数进行初始化。在 OCR_EncoderDecoder 类中,该类相当于是一个transformer各基础组件的拼装线,包括 encoder 和 decoder 等,其初始参数是已存在的基本组件,其基本组件代码都在transformer.py文件中,本文将不在过多叙述。这里再来回顾一下,图片经过backbone后,如何构造为Transformer的输入:图片经过backbone后将输出一个维度为 [batch_size, 512, 1, 24] 的特征图,在不关注batch_size的前提下,每一张图像都会得到如下所示具有512个通道的1×24的特征图,如图中红色框标注所示,将不同通道相同位置的特征值拼接组成一个新的向量,并作为一个时间步的输入,此时变构造出了维度为 [batch_size, 24, 512] 的输入,满足Transformer的输入要求。下面来看下完整的构造模型部分的代码:# 模型结构 class OCR_EncoderDecoder(nn.Module): """ A standard Encoder-Decoder architecture. Base for this and many other models. """ def __init__(self, encoder, decoder, src_embed, src_position, tgt_embed, generator): super(OCR_EncoderDecoder, self).__init__() self.encoder = encoder self.decoder = decoder self.src_embed = src_embed # input embedding module self.src_position = src_position self.tgt_embed = tgt_embed # ouput embedding module self.generator = generator # output generation module def forward(self, src, tgt, src_mask, tgt_mask): "Take in and process masked src and target sequences." # src --> [bs, 3, 32, 768] [bs, c, h, w] # src_mask --> [bs, 1, 24] [bs, h/32, w/32] memory = self.encode(src, src_mask) # memory --> [bs, 24, 512] # tgt --> decode_in [bs, 20] [bs, sequence_len-1] # tgt_mask --> decode_mask [bs, 20] [bs, sequence_len-1] res = self.decode(memory, src_mask, tgt, tgt_mask) # [bs, 20, 512] return res def encode(self, src, src_mask): # feature extract # src --> [bs, 3, 32, 768] src_embedds = self.src_embed(src) # 此处使用的resnet18作为backbone 输出-->[batchsize, c, h, w] --> [bs, 512, 1, 24] # 将src_embedds由shape(bs, model_dim, 1, max_ratio) 处理为transformer期望的输入shape(bs, 时间步, model_dim) # [bs, 512, 1, 24] --> [bs, 24, 512] src_embedds = src_embedds.squeeze(-2) src_embedds = src_embedds.permute(0, 2, 1) # position encode src_embedds = self.src_position(src_embedds) # [bs, 24, 512] return self.encoder(src_embedds, src_mask) # [bs, 24, 512] def decode(self, memory, src_mask, tgt, tgt_mask): target_embedds = self.tgt_embed(tgt) # [bs, 20, 512] return self.decoder(target_embedds, memory, src_mask, tgt_mask) def make_ocr_model(tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1): """ 构建模型 params: tgt_vocab: 输出的词典大小 N: 编码器和解码器堆叠基础模块的个数 d_model: 模型中embedding的size,默认512 d_ff: FeedForward Layer层中embedding的size,默认2048 h: MultiHeadAttention中多头的个数,必须被d_model整除 dropout: dropout的比率 """ c = copy.deepcopy # torch中预训练的resnet18作为特征提取网络, backbone backbone = models.resnet18(pretrained=True) backbone = nn.Sequential(*list(backbone.children())[:-2]) # 去掉最后两个层 (global average pooling and fc layer) attn = MultiHeadedAttention(h, d_model) ff = PositionwiseFeedForward(d_model, d_ff, dropout) position = PositionalEncoding(d_model, dropout) # 构建模型 model = OCR_EncoderDecoder( Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N), Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N), backbone, c(position), nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)), Generator(d_model, tgt_vocab)) # 此处的generator并没有在类内调用 # Initialize parameters with Glorot / fan_avg. for child in model.children(): if child is backbone: # 将backbone的权重设为不计算梯度 for param in child.parameters(): param.requires_grad = False # 预训练好的backbone不进行随机初始化,其余模块进行随机初始化 continue for p in child.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) return model通过上述的两个类,可以方便构建transformer模型:# build model # use transformer as ocr recognize model # 此处构建的ocr_model不含有Generator tgt_vocab = len(lbl2id_map.keys()) d_model = 512 ocr_model = make_ocr_model(tgt_vocab, N=5, d_model=d_model, d_ff=2048, h=8, dropout=0.1) ocr_model.to(device)4.4 模型训练模型训练之前,还需要定义模型评判准则、迭代优化器等。本实验在训练时,使用了标签平滑(label smoothing)、网络训练热身(warmup)等策略,以上策略的调用函数均在train_utils.py文件中,此处不涉及以上两种方法的原理及代码实现。label smoothing可以将原始的硬标签转化为软标签,从而增加模型的容错率,提升模型泛化能力。代码中 LabelSmoothing() 函数实现了label smoothing,同时内部使用了相对熵函数计算了预测值与真实值之间的损失。warmup策略能够有效控制模型训练过程中的优化器学习率,自动化的实现模型学习率由小增大再逐渐下降的控制,帮助模型在训练时更加稳定,实现损失的快速收敛。代码中 NoamOpt() 函数实现了warmup控制,采用的Adam优化器,实现学习率随迭代次数的自动调整。# train prepare criterion = LabelSmoothing(size=tgt_vocab, padding_idx=0, smoothing=0.0) # label smoothing optimizer = torch.optim.Adam(ocr_model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9) model_opt = NoamOpt(d_model, 1, 400, optimizer) # warmup模型训练过程的代码如下所示,每训练10个epoch便进行一次验证,单个epoch的计算过程封装在 run_epoch() 函数中。# train & valid ... for epoch in range(nrof_epochs): print(f"\nepoch {epoch}") print("train...") # 训练 ocr_model.train() loss_compute = SimpleLossCompute(ocr_model.generator, criterion, model_opt) train_mean_loss = run_epoch(train_loader, ocr_model, loss_compute, device) if epoch % 10 == 0: print("valid...") # 验证 ocr_model.eval() valid_loss_compute = SimpleLossCompute(ocr_model.generator, criterion, None) valid_mean_loss = run_epoch(valid_loader, ocr_model, valid_loss_compute, device) print(f"valid loss: {valid_mean_loss}") # save model torch.save(ocr_model.state_dict(), './trained_model/ocr_model.pt')SimpleLossCompute() 类实现了transformer输出结果的loss计算。在使用该类直接计算时,类需要接收(x, y, norm)三个参数,x为decoder输出的结果,y为标签数据,norm为loss的归一化系数,用batch中所有有效token数即可。由此可见,此处才正完成transformer所有网络的构建,实现数据计算流的流通。run_epoch() 函数内部完成了一个epoch训练的所有工作,包括数据加载、模型推理、损失计算与方向传播,同时将训练过程信息进行打印。def run_epoch(data_loader, model, loss_compute, device=None): "Standard Training and Logging Function" start = time.time() total_tokens = 0 total_loss = 0 tokens = 0 for i, batch in enumerate(data_loader): img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens = batch img_input = img_input.to(device) encode_mask = encode_mask.to(device) decode_in = decode_in.to(device) decode_out = decode_out.to(device) decode_mask = decode_mask.to(device) ntokens = torch.sum(ntokens).to(device) out = model.forward(img_input, decode_in, encode_mask, decode_mask) # out --> [bs, 20, 512] 预测结果 # decode_out --> [bs, 20] 实际结果 # ntokens --> 标签中实际有效字符 loss = loss_compute(out, decode_out, ntokens) # 损失计算 total_loss += loss total_tokens += ntokens tokens += ntokens if i % 50 == 1: elapsed = time.time() - start print("Epoch Step: %d Loss: %f Tokens per Sec: %f" % (i, loss / ntokens, tokens / elapsed)) start = time.time() tokens = 0 return total_loss / total_tokens class SimpleLossCompute: "A simple loss compute and train function." def __init__(self, generator, criterion, opt=None): self.generator = generator self.criterion = criterion self.opt = opt def __call__(self, x, y, norm): """ norm: loss的归一化系数,用batch中所有有效token数即可 """ # x --> out --> [bs, 20, 512] 预测结果 # y --> decode_out --> [bs, 20] 实际结果 # norm --> ntokens --> 标签中实际有效字符 x = self.generator(x) # label smoothing需要对应维度变化 x_ = x.contiguous().view(-1, x.size(-1)) # [20bs, 512] y_ = y.contiguous().view(-1) # [20bs] loss = self.criterion(x_, y_) loss /= norm loss.backward() if self.opt is not None: self.opt.step() self.opt.optimizer.zero_grad() #return loss.data[0] * norm return loss.item() * norm4.5 贪心解码方便起见,我们使用最简单的贪心解码直接进行OCR结果预测。因为模型每一次只会产生一个输出,我们选择输出的概率分布中的最高概率对应的字符为本次预测的结果,然后预测下一个字符,这就是所谓的贪心解码,见代码中 greedy_decode() 函数。实验中分别将每一张图像作为模型的输入,逐张进行贪心解码统计正确率,并最终给出了训练集和验证集各自的预测准确率。# 训练结束,使用贪心的解码方式推理训练集和验证集,统计正确率 ocr_model.eval() print("\n------------------------------------------------") print("greedy decode trainset") total_img_num = 0 total_correct_num = 0 for batch_idx, batch in enumerate(train_loader): img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens = batch img_input = img_input.to(device) encode_mask = encode_mask.to(device) # 获取单张图像信息 bs = img_input.shape[0] for i in range(bs): cur_img_input = img_input[i].unsqueeze(0) cur_encode_mask = encode_mask[i].unsqueeze(0) cur_decode_out = decode_out[i] # 贪心解码 pred_result = greedy_decode(ocr_model, cur_img_input, cur_encode_mask, max_len=sequence_len, start_symbol=1, end_symbol=2) pred_result = pred_result.cpu() # 判断预测是否正确 is_correct = judge_is_correct(pred_result, cur_decode_out) total_correct_num += is_correct total_img_num += 1 if not is_correct: # 预测错误的case进行打印 print("----") print(cur_decode_out) print(pred_result) total_correct_rate = total_correct_num / total_img_num * 100 print(f"total correct rate of trainset: {total_correct_rate}%") # 与训练集解码代码相同 print("\n------------------------------------------------") print("greedy decode validset") total_img_num = 0 total_correct_num = 0 for batch_idx, batch in enumerate(valid_loader): img_input, encode_mask, decode_in, decode_out, decode_mask, ntokens = batch img_input = img_input.to(device) encode_mask = encode_mask.to(device) bs = img_input.shape[0] for i in range(bs): cur_img_input = img_input[i].unsqueeze(0) cur_encode_mask = encode_mask[i].unsqueeze(0) cur_decode_out = decode_out[i] pred_result = greedy_decode(ocr_model, cur_img_input, cur_encode_mask, max_len=sequence_len, start_symbol=1, end_symbol=2) pred_result = pred_result.cpu() is_correct = judge_is_correct(pred_result, cur_decode_out) total_correct_num += is_correct total_img_num += 1 if not is_correct: # 预测错误的case进行打印 print("----") print(cur_decode_out) print(pred_result) total_correct_rate = total_correct_num / total_img_num * 100 print(f"total correct rate of validset: {total_correct_rate}%")greedy_decode() 函数实现。# greedy decode def greedy_decode(model, src, src_mask, max_len, start_symbol, end_symbol): memory = model.encode(src, src_mask) # ys代表目前已生成的序列,最初为仅包含一个起始符的序列,不断将预测结果追加到序列最后 ys = torch.ones(1, 1).fill_(start_symbol).type_as(src.data).long() for i in range(max_len-1): out = model.decode(memory, src_mask, Variable(ys), Variable(subsequent_mask(ys.size(1)).type_as(src.data))) prob = model.generator(out[:, -1]) _, next_word = torch.max(prob, dim = 1) next_word = next_word.data[0] next_word = torch.ones(1, 1).type_as(src.data).fill_(next_word).long() ys = torch.cat([ys, next_word], dim=1) next_word = int(next_word) if next_word == end_symbol: break #ys = torch.cat([ys, torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=1) ys = ys[0, 1:] return ys def judge_is_correct(pred, label): # 判断模型预测结果和label是否一致 pred_len = pred.shape[0] label = label[:pred_len] is_correct = 1 if label.equal(pred) else 0 return is_correct运行下面的命令即可一键开启训练:python ocr_by_transformer.py训练日志如下所示:epoch 0 train... Epoch Step: 1 Loss: 5.142612 Tokens per Sec: 852.649109 Epoch Step: 51 Loss: 3.064528 Tokens per Sec: 2709.471436 valid... Epoch Step: 1 Loss: 3.018526 Tokens per Sec: 1413.900391 valid loss: 2.7769546508789062 epoch 1 train... Epoch Step: 1 Loss: 3.440590 Tokens per Sec: 1303.567993 Epoch Step: 51 Loss: 2.711708 Tokens per Sec: 2743.414307 ... epoch 1499 train... Epoch Step: 1 Loss: 0.005739 Tokens per Sec: 1232.602783 Epoch Step: 51 Loss: 0.013249 Tokens per Sec: 2765.866211 ------------------------------------------------ greedy decode trainset ---- tensor([17, 32, 18, 19, 31, 50, 30, 10, 30, 10, 17, 32, 41, 55, 55, 55, 2, 0, 0, 0]) tensor([17, 32, 18, 19, 31, 50, 30, 10, 30, 10, 17, 32, 41, 55, 55, 55, 55, 55, 55, 55]) ---- tensor([17, 32, 18, 19, 31, 50, 30, 10, 17, 32, 41, 55, 55, 2, 0, 0, 0, 0, 0, 0]) tensor([17, 32, 18, 19, 31, 50, 30, 10, 17, 32, 41, 55, 55, 55, 55, 2]) total correct rate of trainset: 99.95376791493297% ------------------------------------------------ greedy decode validset ---- tensor([10, 11, 28, 27, 25, 11, 47, 45, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) tensor([10, 11, 28, 27, 25, 11, 62, 2]) ... tensor([20, 12, 24, 35, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) tensor([20, 12, 21, 12, 22, 23, 34, 2]) total correct rate of validset: 92.72088353413655%
-
如何将transformer引入OCR很多算法本身并不难,难的是如何思考和定义问题,把它转化到已知的解决方案上去。因此在看代码之前,我们先要聊聊,为什么transformer可以解决OCR问题,动机是什么?首先,我们知道,transformer被广泛应用在NLP领域中,可以解决类似机器翻译这样的sequence to sequence类的问题,如下图所示:而OCR识别任务,如下图所示,我们希望将下图识别为"Share",本质上也可以看作是一个sequence to sequence任务,只不过输入的序列信息是由图片形式表示的。因此,如果从把OCR问题看作是一个sequence to sequence预测问题这个角度,使用transformer解决OCR问题貌似是一个非常自然和顺畅的想法,剩下的问题只是如何将图片的信息构造成transformer想要的,类似于 word embedding 形式的输入。回到我们的任务,既然待预测的图片都是长条状的,文字基本都是水平排列,那么我们将特征图沿水平方向进行整合,得到的每一个embedding可以认为是图片纵向的某个切片的特征,将这样的特征序列交给transformer,利用其强大的attention能力来完成预测。因此,基于以上分析,我们将模型框架的pipeline定义为下图所示的形式:通过观察上图可以发现,整个pipeline和利用transformer训练机器翻译的流程是基本一致的,之间的差异主要是多了借助一个CNN网络作为backbone提取图像特征得到input embedding的过程。关于构造transformer的输入embedding这部分的设计,是本文的重点,也是整个算法能够work的关键。后文会结合代码,对上面示意图中展示的相关细节进行展开讲解。
-
数据集图像尺寸分析在进行图像分类检测等任务时,经常会查看图像的尺寸分布,进而确定合适的图像的预处理方式,例如在进行目标检测时会对图像尺寸和bounding box的尺寸进行统计,分析长宽比进而选择合适的图像裁剪策略和适当的初始anchor策略等。因此这里通过分析图像宽度、高度和宽高比等信息来了解数据的特点,为后续实验策略制定提供参考。def read_gas_image(data): with data.open() as fp: image = Image.open(fp) return image # 分析数据集图片尺寸 print("分析数据集图片尺寸:") # 初始化参数 min_h = 1e10 min_w = 1e10 max_h = -1 max_w = -1 min_ratio = 1e10 max_ratio = 0 # 遍历数据集计算尺寸信息 for data in tqdm.tqdm(train_segment): img = read_gas_image(data) # 读取图片 w, h = img.size # 提取图像宽高信息 ratio = w / h # 宽高比 min_h = min_h if min_h <= h else h # 最小图片高度 max_h = max_h if max_h >= h else h # 最大图片高度 min_w = min_w if min_w <= w else w # 最小图片宽度 max_w = max_w if max_w >= w else w # 最大图片宽度 min_ratio = min_ratio if min_ratio <= ratio else ratio # 最小宽高比 max_ratio = max_ratio if max_ratio >= ratio else ratio # 最大宽高比 # 输出信息 print('min_h:', min_h) print('max_h:', max_h) print('min_w:', min_w) print('max_w:', max_w) print('min_ratio:', min_ratio) print('max_ratio:', max_ratio)数据集图片尺寸相关情况统计结果如下:min_h: 9 max_h: 295 min_w: 16 max_w: 628 min_ratio: 0.6666666666666666 max_ratio: 8.619047619047619通过以上的结果,可看出图片多为卧倒的长条形,最大宽高比 > 8 可见存在极细长的图片。以上便是对于数据集的若干简单分析,并且准备出了训练要用的char2id映射文件,下面就是重头戏了,来看看我们如何将transfomer引入,来完成OCR单词识别这样的CV任务。
-
char和id的映射字典构建在本文OCR任务中,需要对图片中的每个字符进行预测,为了达到这个目的,首先就需要建立一个字符与其id的映射关系,将文本信息转化为可供模型读取的数字信息,这一步类似NLP中建立语料库。在构建映射关系时,除了记录所有标签文件中出现的字符外,还需要初始化三个特殊字符,分别用来代表一个句子起始符、句子终止符和填充(Padding)标识符(相关介绍戳这里)。后面dataset构建部分的讲解也还会再次提到。脚本运行后,所有字符的映射关系将会保存在 lbl2id_map.txt文件中。# 构造label中 字符--id 之间的映射 print("构造label中 字符--id 之间的映射:") lbl2id_map = dict() # 初始化三个特殊字符 lbl2id_map['☯'] = 0 # padding标识符 lbl2id_map['■'] = 1 # 句子起始符 lbl2id_map['□'] = 2 # 句子结束符 # 生成其余字符的id映射关系 cur_id = 3 for lbl in lbl_cnt_map.keys(): lbl2id_map[lbl] = cur_id cur_id += 1 # 保存 字符--id 之间的映射 到txt文件 with open(lbl2id_map_path, 'w', encoding='utf-8') as writer: # 参数encoding是可选项,部分设备并未默认为utf-8 for lbl in lbl2id_map.keys(): cur_id = lbl2id_map[lbl] print(lbl, cur_id) line = lbl + '\t' + str(cur_id) + '\n' writer.write(line)构造出的 字符-id 之间的映射:☯ 0 ■ 1 □ 2 C 3 A 4 ... = 85 ( 86 ) 87 + 88 é 89此外,analysis_recognition_dataset.py 文件中还包含一个建立关系映射字典的函数,可以通过读取含有映射关系txt的文件,构建出字符到id和id到字符的映射字典。这服务于后续transformer训练过程,以方便字符关系快速实现转换。def load_lbl2id_map(lbl2id_map_path): """ 读取 字符-id 映射关系记录的txt文件,并返回 lbl->id 和 id->lbl 映射字典 lbl2id_map_path : 字符-id 映射关系记录的txt文件路径 """ lbl2id_map = dict() id2lbl_map = dict() with open(lbl2id_map_path, 'r') as reader: for line in reader: items = line.rstrip().split('\t') label = items[0] cur_id = int(items[1]) lbl2id_map[label] = cur_id id2lbl_map[cur_id] = label return lbl2id_map, id2lbl_map
-
下面简单介绍数据集的快速使用:本地下载安装 tensorbaypip3 install tensorbay打开本文数据集链接:https://gas.graviti.cn/dataset/datawhale/ICDAR2015将数据集fork到自己账户下点击网页上方开发者工具 --> AccessKey --> 新建一个AccessKey --> 复制这个Key from tensorbay import GAS from tensorbay.dataset import Dataset # GAS凭证 KEY = 'Accesskey-***************80a' # 添加自己的AccessKey gas = GAS(KEY) # 获取数据集 dataset = Dataset("ICDAR2015", gas) # dataset.enable_cache('./data') # 开启本语句,选择将数据建立本地缓存 # 训练集和验证集 train_segment = dataset["train"]valid_segment = dataset['valid'] # 数据及标签 for data in train_segment: # 图像数据 img = data.open() # 图像标签 label = data.label.classification.category break通过以上代码获取的图像及标签形式如下:imglabel['C', 'A', 'U', 'T', 'I', 'O', 'N']通过以上简单代码便可以快速获取图像数据及标签,但程序每次运行都会自动去平台下载数据,因此耗时较长,建议开启本地缓存,一次下载多次使用,当不再使用数据时便可以将数据删除。二、数据分析与关系构建开始实验前,我们先对数据进行简单分析,只有对数据的特性足够了解,才能够更好的搭建出baseline,在训练中少走弯路。运行下面代码,即可一键完成对于数据集的简单分析:python analysis_recognition_dataset.py具体地,这个脚本所做的工作包括:对数据进行标签字符统计(有哪些字符、每个字符出现次数多少)、最长标签长度统计,图像尺寸分析等,并且构建字符标签的映射关系文件 lbl2id_map.txt。下面我们来一点点看代码:注:本文代码开源地址:https://github.com/datawhalechina/dive-into-cv-pytorch/tree/master/code/chapter06_transformer/6.2_recognition_by_transformer(online_dataset)首先完成准备工作,导入需要的库,并设置好相关目录或文件的路径import os from PIL import Image import tqdm from tensorbay import GAS from tensorbay.dataset import Dataset # GAS凭证 KEY = 'Accesskey-************************480a' # 添加自己的AccessKey gas = GAS(KEY) # 数据集获取并本地缓存 dataset = Dataset("ICDAR2015", gas) dataset.enable_cache('./data') # 数据缓存地址 # 获取训练集和验证集 train_segment = dataset["train"] valid_segment = dataset['valid'] # 中间文件存储路径,存储标签字符与其id的映射关系 base_data_dir = './' lbl2id_map_path = os.path.join(base_data_dir, 'lbl2id_map.txt')2.1 标签最长字符个数统计首先统计数据集最长label中包含的字符数量,此处要将训练集和验证集中的最长标签都进行统计,进而得到最长标签所含字符。def statistics_max_len_label(segment): """ 统计标签中最长的label所包含的字符数 """ max_len = -1 for data in segment: lbl_str = data.label.classification.category # 获取标签 lbl_len = len(lbl_str) max_len = max_len if max_len > lbl_len else lbl_len return max_len train_max_label_len = statistics_max_len_label(train_segment) # 训练集最长label valid_max_label_len = statistics_max_len_label(valid_segment) # 验证集最长label max_label_len = max(train_max_label_len, valid_max_label_len) # 全数据集最长label print(f"数据集中包含字符最多的label长度为{max_label_len}")数据集中最长label含有21个字符,这将为后面transformer模型搭建时的时间步长度的设置提供参考。2.2 标签所含字符统计下面代码查看数据集中出现过的所有字符:def statistics_label_cnt(segment, lbl_cnt_map): """ 统计标签文件中label都包含哪些字符以及各自出现的次数 lbl_cnt_map : 记录标签中字符出现次数的字典 """ for data in segment: lbl_str = data.label.classification.category # 获取标签 for lbl in lbl_str: if lbl not in lbl_cnt_map.keys(): lbl_cnt_map[lbl] = 1 else: lbl_cnt_map[lbl] += 1 lbl_cnt_map = dict() # 用于存储字符出现次数的字典 statistics_label_cnt(train_segment, lbl_cnt_map) # 训练集中字符出现次数统计 print("训练集中label中出现的字符:") print(lbl_cnt_map) statistics_label_cnt(valid_segment, lbl_cnt_map) # 训练集和验证集中字符出现次数统计 print("训练集+验证集label中出现的字符:") print(lbl_cnt_map)输出结果为:训练集中label中出现的字符: {'C': 593, 'A': 1189, 'U': 319, 'T': 896, 'I': 861, 'O': 965, 'N': 785, 'D': 383, 'W': 179, 'M': 367, 'E': 1423, 'X': 110, '$': 46, '2': 121, '4': 44, 'L': 745, 'F': 259, 'P': 389, 'R': 836, 'S': 1164, 'a': 843, 'v': 123, 'e': 1057, 'G': 345, "'": 51, 'r': 655, 'k': 96, 's': 557, 'i': 651, 'c': 318, 'V': 158, 'H': 391, '3': 50, '.': 95, '"': 8, '-': 68, ',': 19, 'Y': 229, 't': 563, 'y': 161, 'B': 332, 'u': 293, 'x': 27, 'n': 605, 'g': 171, 'o': 659, 'l': 408, 'd': 258, 'b': 88, 'p': 197, 'K': 163, 'J': 72, '5': 80, '0': 203, '1': 186, 'h': 299, '!': 51, ':': 19, 'f': 133, 'm': 202, '9': 66, '7': 45, 'j': 15, 'z': 12, '´': 3, 'Q': 19, 'Z': 29, '&': 9, ' ': 50, '8': 47, '/': 24, '#': 16, 'w': 97, '?': 5, '6': 40, '[': 2, ']': 2, 'É': 1, 'q': 3, ';': 3, '@': 4, '%': 28, '=': 1, '(': 6, ')': 5, '+': 1} 训练集+验证集label中出现的字符: {'C': 893, 'A': 1827, 'U': 467, 'T': 1315, 'I': 1241, 'O': 1440, 'N': 1158, 'D': 548, 'W': 288, 'M': 536, 'E': 2215, 'X': 181, '$': 57, '2': 141, '4': 53, 'L': 1120, 'F': 402, 'P': 582, 'R': 1262, 'S': 1752, 'a': 1200, 'v': 169, 'e': 1536, 'G': 521, "'": 70, 'r': 935, 'k': 137, 's': 793, 'i': 924, 'c': 442, 'V': 224, 'H': 593, '3': 69, '.': 132, '"': 8, '-': 87, ',': 25, 'Y': 341, 't': 829, 'y': 231, 'B': 469, 'u': 415, 'x': 38, 'n': 880, 'g': 260, 'o': 955, 'l': 555, 'd': 368, 'b': 129, 'p': 317, 'K': 253, 'J': 100, '5': 105, '0': 258, '1': 231, 'h': 417, '!': 65, ':': 24, 'f': 203, 'm': 278, '9': 76, '7': 62, 'j': 19, 'z': 14, '´': 3, 'Q': 28, 'Z': 36, '&': 15, ' ': 82, '8': 58, '/': 29, '#': 24, 'w': 136, '?': 7, '6': 46, '[': 2, ']': 2, 'É': 2, 'q': 3, ';': 3, '@': 9, '%': 42, '=': 1, '(': 7, ')': 5, '+': 2, 'é': 1}上方代码中,lbl_cnt_map 为字符出现次数的统计字典,后面还会用于建立字符及其id映射关系。从数据集统计结果来看,测试集含有训练集没有出现过的字符,例如测试集中包含1个'é'未曾在训练集出现。这种情况数量不多,应该问题不大,所以此处未对数据集进行额外处理(但是有意识的进行这种训练集和测试集是否存在diff的检查是必要的)。
-
文章来源于Datawhale ,作者安晟、袁明坤在CV领域中,transformer除了分类还能做什么?本文将采用一个单词识别任务数据集,讲解如何使用transformer实现一个简单的OCR文字识别任务,并从中体会transformer是如何应用到除分类以外更复杂的CV任务中的。全文分为四部分:一、数据集简介与获取二、数据分析与关系构建三、如何将transformer引入OCR四、训练框架代码讲解注:本文围绕如何设计模型和训练架构来解决OCR任务,文章含完整实践,代码很长建议收藏。不熟悉transformer的小伙伴可以点击这里回顾。整个文字识别任务中,主要包括以下几个文件:- analysis_recognition_dataset.py (数据集分析脚本)- ocr_by_transformer.py (OCR任务训练脚本)- transformer.py (transformer模型文件)- train_utils.py (训练相关辅助函数,loss、optimizer等)其中 ocr_by_transformer.py 为主要的训练脚本,其依托 train_utils.py 和 transformer.py 两个文件构建 transformer 来完成字符识别模型的训练一、数据集简介与获取本文使用的数据集基于ICDAR2015 Incidental Scene Text 中的 Task 4.3: Word Recognition,这是一个著名的自然场景下文本识别数据集,本次用来进行单词识别任务,我们去掉了其中一些图片,来简化这个实验的难度,因此本文的数据集与原始数据集略有差别。为了能够更好的进行数据共享和版本管控,我们选择在线调用数据集,将简化后的数据集存放在专门的数据共享平台,数据开源地址: https://gas.graviti.cn/dataset/datawhale/ICDAR2015 ,有相关问题可以直接在数据集讨论区交流。该数据集包含了众多自然场景图像中出现的文字区域,数据中训练集含有4326张图像,测试集含有1992张图像,他们都是从原始大图中依据文字区域的bounding box裁剪出来的,图像中的文字基本处于图片中心位置。数据集中图像类似如下样式:数据本身以图像展示,对应的标签存储在CLASSIFICATION中,后文代码中标签获取,将直接得到一个包含所有字符的列表,这也是为了方便标签易用性进行的存储选择。
-
我们知道BN在每一层计算的 与 都是基于当前batch中的训练数据, 但是这就带来了一个问 题: 我们在预测阶段, 有可能只需要预测一个样本或很少的样本, 没有像训练样本中那么多的数 据, 此时 与 的计算一定是有偏估计, 这个时候我们该如何进行计算呢?利用BN训练好模型后, 我们保留了每组mini-batch训练数据在网络中每一层的 与 。此时我们使用整个样本的统计量来对Test数据进行归一化,具体来说使用均值与方差的无偏估计:得到每个特征的均值与方差的无偏估计后, 我们对test数据采用同样的normalization方法:另外,除了采用整体样本的无偏估计外。吴恩达在Coursera上的Deep Learning课程指出可以对train阶段每个batch计算的mean/variance采用指数加权平均来得到test阶段mean/variance的估计。4 Batch Normalization的优势Batch Normalization在实际工程中被证明了能够缓解神经网络难以训练的问题,BN具有的有事可以总结为以下三点:(1)BN使得网络中每层输入数据的分布相对稳定,加速模型学习速度BN通过规范化与线性变换使得每一层网络的输入数据的均值与方差都在一定范围内,使得后一层网络不必不断去适应底层网络中输入的变化,从而实现了网络中层与层之间的解耦,允许每一层进行独立学习,有利于提高整个神经网络的学习速度。(2)BN使得模型对网络中的参数不那么敏感,简化调参过程,使得网络学习更加稳定在神经网络中,我们经常会谨慎地采用一些权重初始化方法(例如Xavier)或者合适的学习率来保证网络稳定训练。当学习率设置太高时, 会使得参数更新步伐过大, 容易出现震荡和不收敛。但是使用BN的网络将 不会受到参数数值大小的影响。例如, 我们对参数 进行缩放得到 。对于缩放前的值 , 我们设其均值为 , 方差为 ; 对于缩放值 , 设其均值为 , 方差为 , 则我们有:注:公式中的 是当前层的输入,也是前一层的输出;不是下标啊旁友们!我们可以看到, 经过BN操作以后, 权重的缩放值会被“抺去”, 因此保证了输入数据分布稳定在一定范围内。另外, 权重的缩放并不会影响到对 的梯度计算; 并且当权重越大时, 即 越大, 越小,意味着权重 的梯度反而越小,这样BN就保证了梯度不会依赖于参数的scale, 使得参数的更新处在更加稳定的状态。因此,在使用Batch Normalization之后,抑制了参数微小变化随着网络层数加深被放大的问题,使得网络对参数大小的适应能力更强,此时我们可以设置较大的学习率而不用过于担心模型divergence的风险。(3)BN允许网络使用饱和性激活函数(例如sigmoid,tanh等),缓解梯度消失问题在不使用BN层的时候, 由于网络的深度与复杂性, 很容易使得底层网络变化累积到上层网络中, 导致模型的训练很容易进入到激活函数的梯度饱和区; 通过normalize操作可以让激活函数的输入数据落在梯度非饱和区, 缓解梯度消失的问题; 另外通过自适应学习 与 又让数据保留更多的原始信息。(4)BN具有一定的正则化效果在Batch Normalization中,由于我们使用mini-batch的均值与方差作为对整体训练样本均值与方差的估计,尽管每一个batch中的数据都是从总体样本中抽样得到,但不同mini-batch的均值与方差会有所不同,这就为网络的学习过程中增加了随机噪音,与Dropout通过关闭神经元给网络训练带来噪音类似,在一定程度上对模型起到了正则化的效果。另外,原作者通过也证明了网络加入BN后,可以丢弃Dropout,模型也同样具有很好的泛化效果。(二)实战板块经过了上面了理论学习,我们对BN有了理论上的认知。“Talk is cheap, show me the code”。接下来我们就通过实际的代码来对比加入BN前后的模型效果。实战部分使用MNIST数据集作为数据基础,并使用TensorFlow中的Batch Normalization结构来进行BN的实现。数据准备:MNIST手写数据集代码地址:我的GitHub (https://github.com/NELSONZHAO/zhihu/tree/master/batch_normalization_discussion)注:TensorFlow版本为1.6.0实战板块主要分为两部分:网络构建与辅助函数BN测试1 网络构建与辅助函数首先我们先定义一下神经网络的类,这个类里面主要包括了以下方法:build_network:前向计算fully_connected:全连接计算train:训练模型test:测试模型1.1 build_network我们首先通过构造函数,把权重、激活函数以及是否使用BN这些变量传入,并生成一个training_accuracies来记录训练过程中的模型准确率变化。这里的initial_weights是一个list,list中每一个元素是一个矩阵(二维tuple),存储了每一层的权重矩阵。build_network实现了网络的构建,并调用了fully_connected函数(下面会提)进行计算。要注意的是,由于MNIST是多分类,在这里我们不需要对最后一层进行激活,保留计算的logits就好。
-
2.2.1 参数定义我们依旧以下图这个神经网络为例。我们定义网络总共有 LL 层(不包含输入层)并定义如下符号:参数相关: : 网络中的层标号 : 网络中的最后一层或总层数 : 第 层的维度, 即神经元结点数 : 第 层的权重矩阵, 第 层的偏置向量, : 第 层的线性计算结果, input : 第 层的激活函数 : 第 层的非线性激活结果,样本相关: : 训练样本的数量 : 训练样本的特征数 : 训练样本集, (注意这里 的一列是一个 样本) : batch size, 即每个batch中样本的数量 : 第 个mini-batch的训练数据, , 其中2.2.2 算法步骤介绍算法思路沿袭前面BN提出的思路来讲。第一点, 对每个特征进行独立的normalization。我们考虑一个batch的训练, 传入 个训练样本, 并关注网络中的某一层, 忽略上标 。我们关注当前层的第 个维度, 也就是第 个神经元结点, 则有 。我们当前维度进行规范化:下面我们再来结合个具体的例子来进行计算。下图我们只关注第 层的计算结果, 左边的矩阵是 线性计算结果, 还末进行激活函数的非线性变换。此时每一列是一个样本, 图中可以看到共有8列, 代表当前训练样本的batch中zo共有8个样本, 每一行代表当前 层神经元的一个节点, 可以看到当前 层共有4个神经元结点, 即第 层维度为4。我们可以看到, 每行的数据分布都不同。对于第一个神经元, 我们求得 (其中 ), 此时我们利用 对第一行数据(第一个维度)进行normalization得到新的值 。同理我们可以计算出其他输入维度归一化后的值。如下图:通过上面的变换,我们解决了第一个问题,即用更加简化的方式来对数据进行规范化,使得第 ll 层的输入每个特征的分布均值为0,方差为1。如同上面提到的,Normalization操作我们虽然缓解了ICS问题,让每一层网络的输入数据分布都变得稳定,但却导致了数据表达能力的缺失。也就是我们通过变换操作改变了原有数据的信息表达(representation ability of the network),使得底层网络学习到的参数信息丢失。另一方面,通过让每一层的输入分布均值为0,方差为1,会使得输入在经过sigmoid或tanh激活函数时,容易陷入非线性激活函数的线性区域。因此, BN又引入了两个可学习 (learnable) 的参数 与 。这两个参数的引入是为了恢复数据本 身的表达能力, 对规范化后的数据进行线性变换, 即 。特别地, 当 时, 可以实现等价变换(identity transform)并且保留了原始输入特征的分布信 息。通过上面的步骤,我们就在一定程度上保证了输入数据的表达能力。以上就是整个Batch Normalization在模型训练中的算法和思路。补充:在进行normalization的过程中, 由于我们的规范化操作会对减去均值, 因此, 偏置项 可以被忽略掉或可以被置为0, 即
-
作者丨天雨粟@知乎链接丨https://zhuanlan.zhihu.com/p/34879333前言本文主要分为两大部分。第一部分是理论板块,主要从背景、算法、效果等角度对Batch Normalization进行详解;第二部分是实战板块,主要以MNIST数据集作为整个代码测试的数据,通过比较加入Batch Normalization前后网络的性能来让大家对Batch Normalization的作用与效果有更加直观的感知。(一)理论板块理论板块将从以下四个方面对Batch Normalization进行详解:提出背景BN算法思想测试阶段如何使用BNBN的优势理论部分主要参考2015年Google的Sergey Ioffe与Christian Szegedy的论文内容,并辅以吴恩达Coursera课程与其它博主的资料。所有参考内容链接均见于文章最后参考链接部分。1 提出背景1.1 炼丹的困扰在深度学习中,由于问题的复杂性,我们往往会使用较深层数的网络进行训练,相信很多炼丹的朋友都对调参的困难有所体会,尤其是对深层神经网络的训练调参更是困难且复杂。在这个过程中,我们需要去尝试不同的学习率、初始化参数方法(例如Xavier初始化)等方式来帮助我们的模型加速收敛。深度神经网络之所以如此难训练,其中一个重要原因就是网络中层与层之间存在高度的关联性与耦合性。下图是一个多层的神经网络,层与层之间采用全连接的方式进行连接。我们规定左侧为神经网络的底层,右侧为神经网络的上层。那么网络中层与层之间的关联性会导致如下的状况:随着训练的进行,网络中的参数也随着梯度下降在不停更新。一方面,当底层网络中参数发生微弱变化时,由于每一层中的线性变换与非线性激活映射,这些微弱变化随着网络层数的加深而被放大(类似蝴蝶效应);另一方面,参数的变化导致每一层的输入分布会发生改变,进而上层的网络需要不停地去适应这些分布变化,使得我们的模型训练变得困难。上述这一现象叫做Internal Covariate Shift。1.2 什么是Internal Covariate ShiftBatch Normalization的原论文作者给了Internal Covariate Shift一个较规范的定义:在深层网络训练的过程中,由于网络中参数变化而引起内部结点数据分布发生变化的这一过程被称作Internal Covariate Shift。这句话该怎么理解呢? 我们同样以1.1中的图为例, 我们定义每一层的线性变换为 input , 其中 代表层数; 非线性变换为 , 其中 为 第 层的激活函数。随着梯度下降的进行, 每一层的参数 与 都会被更新, 那么 的分布也就发生了改变, 进而 也同样出现分布的改变。而 作为第 层的输入, 意味着 层就需要去不停 适应这种数据分布的变化, 这一过程就被叫做Internal Covariate Shift。1.3 Internal Covariate Shift会带来什么问题?(1)上层网络需要不停调整来适应输入数据分布的变化,导致网络学习速度的降低我们在上面提到了梯度下降的过程会让每一层的参数 和 发生变化,进而使得每一层的线性与非线性计算结果分布产生变化。后层网络就要不停地去适应这种分布变化,这个时候就会使得整个网络的学习速率过慢。(2)网络的训练过程容易陷入梯度饱和区,减缓网络收敛速度当我们在神经网络中采用饱和激活函数 (saturated activation function) 时, 例如sigmoid, tanh 激活函数, 很容易使得模型训练陷入梯度饱和区 (saturated regime)。随着模型训练的进行, 我 们的参数 会逐渐更新并变大, 此时 就会随之变大, 并且 还受 到更底层网络参数 的影响, 随着网络层数的加深, 很容易陷入梯度 饱和区, 此时梯度会变得很小甚至接近于 0 , 参数的更新速度就会减慢, 进而就会放慢网络的收玫 速度。对于激活函数梯度饱和问题,有两种解决思路。第一种就是更为非饱和性激活函数,例如线性整流函数ReLU可以在一定程度上解决训练进入梯度饱和区的问题。另一种思路是,我们可以让激活函数的输入分布保持在一个稳定状态来尽可能避免它们陷入梯度饱和区,这也就是Normalization的思路。1.4 我们如何减缓Internal Covariate Shift?要缓解ICS的问题,就要明白它产生的原因。ICS产生的原因是由于参数更新带来的网络中每一层输入值分布的改变,并且随着网络层数的加深而变得更加严重,因此我们可以通过固定每一层网络输入值的分布来对减缓ICS问题。(1)白化(Whitening)白化(Whitening)是机器学习里面常用的一种规范化数据分布的方法,主要是PCA白化与ZCA白化。白化是对输入数据分布进行变换,进而达到以下两个目的:使得输入特征分布具有相同的均值与方差。 其中PCA白化保证了所有特征分布均值为0,方差为1;而ZCA白化则保证了所有特征分布均值为0,方差相同;去除特征之间的相关性。通过白化操作,我们可以减缓ICS的问题,进而固定了每一层网络输入分布,加速网络训练过程的收敛(LeCun et al.,1998b;Wiesler&Ney,2011)。(2)Batch Normalization提出既然白化可以解决这个问题,为什么我们还要提出别的解决办法?当然是现有的方法具有一定的缺陷,白化主要有以下两个问题:白化过程计算成本太高, 并且在每一轮训练中的每一层我们都需要做如此高成本计算的白化操作;白化过程由于改变了网络每一层的分布,因而改变了网络层中本身数据的表达能力。底层网络学习到的参数信息会被白化操作丢失掉。既然有了上面两个问题,那我们的解决思路就很简单,一方面,我们提出的normalization方法要能够简化计算过程;另一方面又需要经过规范化处理后让数据尽可能保留原始的表达能力。于是就有了简化+改进版的白化——Batch Normalization。2 Batch Normalization2.1 思路既然白化计算过程比较复杂,那我们就简化一点,比如我们可以尝试单独对每个特征进行normalizaiton就可以了,让每个特征都有均值为0,方差为1的分布就OK。另一个问题,既然白化操作减弱了网络中每一层输入数据表达能力,那我就再加个线性变换操作,让这些数据再能够尽可能恢复本身的表达能力就好了。因此,基于上面两个解决问题的思路,作者提出了Batch Normalization,下一部分来具体讲解这个算法步骤。2.2 算法在深度学习中,由于采用full batch的训练方式对内存要求较大,且每一轮训练时间过长;我们一般都会采用对数据做划分,用mini-batch对网络进行训练。因此,Batch Normalization也就在mini-batch的基础上进行计算。
-
最近一年,以 ChatGPT 为代表的大模型取得突破性进展,不仅在各项基准数据集中成绩瞩目,也获得了大量用户的青睐。GPT 系列模型的发展似乎表明,随着模型参数和训练数据不断增多,模型会变得越来越可信。然而,随着研究者的深入调查,人们发现近期的大模型在几个重要方面仍然存在着可信问题。其一,大模型在专业性较强,数据较少的领域鲁棒性不足,性能严重下降或者不稳定。其二,大模型的预测常对一些社会群体带有偏见,缺乏公平性。其三,大模型经常产生 (hallucination),输出一些似是而非的答案。大量证据表明,这些问题并没有随着规模增大得到令人满意的解决方案。RLHF 这一类技术虽然可以使模型产生更符合标注者偏好的输出,但从实际效果上并不能显著提升模型的鲁棒性和公平性。前文介绍的可信机器学习方法具有坚实的理论基础,也在多种场景中验证有效,如果用于训练大模型,将有望提高模型的可信性。然而大模型的训练需要极大的成本,为了改进现有的大模型,可以借助高效使用大模型的技术,如微调 (finetuning),参数高效微调 (parameter-efficient fine-tuning),提示学习 (prompting),以及从人类反馈进行强化学习 (RLHF)。作者对近年来这些技术的发展逐一介绍,并表明前三者采用不同参数化的经验风险最小化 (ERM) 目标,而 RLHF 以学习到的人类奖励作为目标。它们都可以与综述中的主题公式结合,进一步提高模型的可信性。而这种结合的可能性,也是这篇综述在整理现有方法的同时,具备了一定预测未来可能诞生的方法的能力。提高机器学习模型的可信性,在大模型时代尤为重要,这需要研究者和实践者的共同努力。最后,作者探讨了可信机器学习可能的未来方向,如:1. 将不同的可信话题结合起来,训练同时具有多种可信属性的模型;2. 利用因果推理的最新研究进展,推动更有效的可信机器学习方法研究;3. 在更复杂的场景,如多模型的协作中,开发和检验可信机器学习方法。
-
图灵奖得主 Judea Pearl 提出的因果推理框架关注如何从关联数据中得到稳健的因果关系,消除数据中混淆因子的影响,这正是可信机器学习的核心问题。从因果的角度,可以在数据生成过程中找到混淆因子的产生机制,从而使用因果推断的方法消除其影响。可信机器学习领域的诸多方法,与因果推理的原理不谋而合。不同机器学习任务的数据生成过程多种多样,作者经过调研,选择最普遍的一种展开讨论。假设数据是由两种潜在变量生成的,称其为 “因果变量” 和 “非因果变量”。这两种变量之间存在着非因果的关联;标签变量只由因果变量生成。下图左为数据生成过程的因果图。图 5:左图为数据生成过程的因果图,右图显示对因果特征施加干预后的变化。由于外部的机制取代了生成 C 的机制,图中进入节点 C 的边被取消。按照 Pearl 的因果层次论,因果有三个层级:关联,干预,反事实。作者沿着因果层次介绍重要的因果推理概念及技术,如随机受控实验,工具变量,逆概率加权,后门调整等。作者通过推导说明,如果在数据生成过程中对因果特征进行干预(如上图右所示),可以消除数据中混淆因子的影响,而数据增强和样本加权等方法可以间接实现这种干预。作者从因果的角度重新阐述了上文提到的可信机器学习方法。此外,近年来一些工作提出了明确基于因果推理的机器学习方法,在综述中也进行了介绍。图 6:用以 GAN (左图) 和 VAE (右图) 为代表的生成模型进行数据增强,与反事实推理的相似之处。从因果的角度理解可信机器学习,可以得知现有方法背后的因果假设,判断它们的适用范围。也可以从数据生成过程出发,选择能消除混淆效应的因果技术,从而启发更有效的可信机器学习方法的设计。目前,因果推理在机器学习中的应用还面临诸多挑战,如无法测量的内生变量,以及特征的耦合等等,这些问题仍需要进一步研究和解决。
-
在过去的十年里,学术界为了提高机器学习模型的可信性,在鲁棒性、安全性、公平性、可解释性等方面提出了许多方法。尽管这些方法千变万化,但他们之间具有内在的联系。鲁棒性 领域适应 (Domain Adaptation) 可能是最早关注机器学习模型对分布迁移的鲁棒性的方向之一。模型在一个分布的数据上训练,用遵从另一个分布的一些未标注数据适应后,希望模型将在训练集上学到的知识迁移到这个新的分布中,做出准确预测。自领域对抗网络方法问世以来,学习领域不变表示这一思路在鲁棒性研究中迅速普及。另外一大类方法使用生成模型进行数据增强,从训练样本产生符合新的分布风格的样本。与领域适应密切相关的是领域泛化,这个方向关注更有挑战性的场景,即希望模型在来自一个或多个分布的数据上训练后,不需适应能直接在一个未曾见过的新分布上做出准确预测。安全性 作者围绕 “对抗鲁棒性” 的话题展开讨论。一些早期工作发现,可以对图像施加人眼难以察觉的扰动,让图像分类模型做出误判,准确率降到接近零。这种被称为 “对抗攻击” 的方法,和神经网络易受攻击的特性,给包括无人驾驶在内的 AI 应用带来很大的安全隐患。人们对这一现象的本质提出了多种角度的理解,比如认为对抗噪音也是一种特征。为了解决这一问题,最常使用的方法是对抗训练,即用生成的对抗样本和原样本一起训练模型。很多研究也借鉴了领域不变表示的方法。公平性 公平性主要指两个方面的问题:结果歧视、质量差异。前者是指模型根据人的社会群体信息做出带有偏见的预测,后者是指模型对少数群体的预测质量降低。结果歧视问题与领域泛化有类似的结构,可以采用领域不变表示学习,或者数据增强的方法。质量差异往往是某些群体的数据稀少导致的,往往可以通过某种算法对训练样本进行加权来解决。可解释性 打开模型的黑盒子,向用户解释模型预测的理由,对一些涉及重要后果的应用场景十分有必要。可解释性方法可以分为全局解释和局部解释,包括多种形式。作者关注的一类方法旨在揭示模型主要用了输入样本的哪些特征做出预测。实现方法往往涉及对样本进行扰动,或计算输出对特征的梯度,与对抗攻击原理类似。图 3:可信机器学习相同主题的方法总结。(a) 标准 ERM 损失;(b)领域对抗网络方法 ;(c) 最坏情况数据增强策略;(d)样本加权方法。在这份综述中,作者围绕这些可信话题,梳理了近年来各种方法的发展脉络。他们发现,这些为不同话题提出的方法可以用几个主题公式统一起来,如图 3 所示。他们还探讨了这些公式与因果推理框架之间的联系,将在下文介绍。图 4:综述中讨论的几大话题总结。蓝框表示可信话题,红框表示主题公式,绿框表示因果层次。
-
一、背景随着机器学习模型性能不断强大,它们更加广泛地进入人们的生活,模型的可信性变得尤为重要。人们对模型 “可信” 的要求涵盖很多方面:一个训练好的模型部署到实际中,需要在未知的分布迁移下保持准确预测;为了使用者理解、验证和采信模型做出的高风险预测,模型需要向用户解释其推理过程;模型应该不带偏见地预测,保证对社会各个群体的公平性,等等。研究者们认为,现有的模型之所以不够可信,根源往往在于数据的构成:数据集中广泛存在着混淆因子、虚假特征等结构,即使标注质量再高也无法避免。模型在经验风险最小化 (ERM) 的原则下训练,容易学到这些虚假特征和标签之间的统计关联。这些关联在独立同分布假设下存在,但其本质很脆弱,在真实世界随时可能发生变化,使得模型的性能骤降。此外出于伦理考虑,我们也不希望模型学到某些统计关联,比如在互联网数据中广泛存在的对社会弱势群体的歧视。
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签