当前位置:   article > 正文

借京东图文识别baseline 来看clip训练过程。 clip是怎样练成的 。_clip 自己训练

clip 自己训练

目录

bert

embedding层

encoder

pool层

text 后处理

回到clip 、

测试 

get_metrics

二 : 用预训练的模型去预测。 

读数据和写数据 


这次轮到clip模型啦 。记笔记记笔记。

背景 是 京东已经给了 图片的feature 也就是不需要我们再去抽特征 。 然后给了图片对应的标题。

我们直接从clip训练开始。

  1. dataloader, sampler = data['train'].dataloader, data['train'].sampler
  2. loss_img = nn.CrossEntropyLoss()
  3. loss_txt = nn.CrossEntropyLoss()
  4. if args.gpu is not None:
  5. loss_img = loss_img.cuda(args.gpu)
  6. loss_txt = loss_txt.cuda(args.gpu)

定义数据loader 和两个loss 都是普通的交叉熵loss。

  1. for i, batch in enumerate(dataloader):
  2. step = num_batches_per_epoch * epoch + i
  3. scheduler(step)
  4. optimizer.zero_grad()
  5. images, texts = batch

设置学习率 然后 读入一个batch的数据 我们看看数据长 啥样。 

在我的这批数据里 图像和文字的编码长度都是2048  所以上面就是images了 一批256张,每张2048维。 而text 就是一批文字数据。256句文字 

tokens = tokenize(texts)

这句代码 很长 就是用bert的tokenize对文字进行编码 

 而bert的编码 处理文字后一般有三个部分 

1 input_ids :就是输入的文字的id  注意只是id编号 而不是编码  101表示cls 的token 6144也代表着一个字 

2  token_types_id 这个属性表示的是 这个字在哪个句子里(一般seq把两个句子分开。) 因为我们只有一个句子  所有全部都是0 他的大小是 128*77  表示128个样本里  每个字的所在句子位置。

3 attention_mask  表示我们要关注哪些字 一般填充的pad对应0 其他的对应1 

image_features, text_features, logit_scale = model(images, texts)

把图像特征 和文字编码输入进模型 

clip的编码就这两句  第一句编图像 第二句编文字 但这里图像编码好了 所以去编码文字 。 

  1. image_features = self.encode_image(image)
  2. text_features = self.encode_text(text)
  1. class TextModel(torch.nn.Module):
  2. def __init__(self, model_name='M-CLIP/M-BERT-Base-69', out_features=2048):
  3. super().__init__()
  4. self.model_name = model_name
  5. self.transformer = transformers.AutoModel.from_pretrained(model_name, cache_dir='~/.cache')
  6. in_features = self.transformer.pooler.dense.out_features
  7. self.clip_head = torch.nn.Linear(in_features=in_features, out_features=out_features)
  8. def forward(self, txt_tok):
  9. embs = self.transformer(**txt_tok)[0]
  10. att = txt_tok['attention_mask']
  11. embs = (embs * att.unsqueeze(2)).sum(dim=1) / att.sum(dim=1)[:, None]
  12. return self.clip_head(embs)

这个class就是用来编码文字的 forward里有四句 我们一句一句来看 。

bert

第一句 transoforms 就是指 bert模型  关于bert模型 可以看 这篇介绍 

HuggingFace BERT源码详解:基本模型组件实现_PaperWeekly的博客-CSDN博客

bert分三块 第一部分是 编码 embedding 也就是把上面的字的id 对应成768维的向量。

第二步是encoder 就是子注意力模块。 而第三部分就是pooler层。 

bert模型的输入一半要求三个东西 就是上面的三个。

embedding层

我们进入embedding层  这里的输入是   128*77 

其中 128是batch  因为我用了两个gpu 所以减半了 。 77是text的长度 其中位置0上是cls 后面跟着句子中字的id  后面是seq  再后面都是pad。  

  1. seq_length = input_shape[1]
  2. if position_ids is None:
  3. position_ids = self.position_ids[:, past_key_values_length : seq_length + past_key_values_length]

获得位置的id  这里的positionids 就是0到76

  1. inputs_embeds = self.word_embeddings(input_ids)
  2. token_type_embeddings = self.token_type_embeddings(token_type_ids)
  3. position_embeddings = self.position_embeddings(position_ids)

把字和 句子位置都编码  data变成了 128*77*768   位置也编码  变成1*77*768 然后把他们三个直接加起来 。 位置编码在加的时候会自动扩充。 

  1. embeddings = self.LayerNorm(embeddings)
  2. embeddings = self.dropout(embeddings)
  3. return embeddings

经过 LN和DP 后得到带位置信息和句子位置信息 的编码 。输出是 128*77*768 

encoder

   encoder 就是自 注意力层  bertchinese 是有12层 每层注意力头有12个 

下面是其中一层。 

  1. class BertLayer(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.chunk_size_feed_forward = config.chunk_size_feed_forward
  5. self.seq_len_dim = 1
  6. self.attention = BertAttention(config)
  7. self.is_decoder = config.is_decoder
  8. self.add_cross_attention = config.add_cross_attention
  9. if self.add_cross_attention:
  10. if not self.is_decoder:
  11. raise ValueError(f"{self} should be used as a decoder model if cross attention is added")
  12. self.crossattention = BertAttention(config, position_embedding_type="absolute")
  13. self.intermediate = BertIntermediate(config)
  14. self.output = BertOutput(config)
  15. class BertAttention(nn.Module):
  16. def __init__(self, config, position_embedding_type=None):
  17. super().__init__()
  18. self.self = BertSelfAttention(config, position_embedding_type=position_embedding_type)
  19. self.output = BertSelfOutput(config)
  20. self.pruned_heads = set()
  21. self.num_attention_heads = config.num_attention_heads
  22. self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
  23. self.all_head_size = self.num_attention_heads * self.attention_head_size
  24. self.query = nn.Linear(config.hidden_size, self.all_head_size)
  25. self.key = nn.Linear(config.hidden_size, self.all_head_size)
  26. self.value = nn.Linear(config.hidden_size, self.all_head_size)
  27. self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
  28. self.position_embedding_type = position_embedding_type or getattr(
  29. config, "position_embedding_type", "absolute"
  30. )
  31. if self.position_embedding_type == "relative_key" or self.position_embedding_type == "relative_key_query":
  32. self.max_position_embeddings = config.max_position_embeddings
  33. self.distance_embedding = nn.Embedding(2 * config.max_position_embeddings - 1, self.attention_head_size)
  34. self.is_decoder = config.is_decoder

上面三层 扣起来的 我也是醉了 。  下面的hidden_states就是输入。 他依然是 128*77*768 

  1. mixed_query_layer = self.query(hidden_states)
  2. query_layer = self.transpose_for_scores(mixed_query_layer)
  3. key_layer = self.transpose_for_scores(self.key(hidden_states))
  4. value_layer = self.transpose_for_scores(self.value(hidden_states))

q,k,v 就是三个linear得到的 之后要 经过一个reshape  他们三个都是  128 *12 * 77 *64  12是注意力的头数  64是每个头得到的数据维数 所以这样子我们就可以12个头同时计算了 。 

  1. attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
  2. attention_scores = attention_scores / math.sqrt(self.attention_head_size)
  3. attention_scores = attention_scores + attention_mask #注意这一句 attention 前面做了反向 也就是说 pad的位置上 都是-10000 很小 然后加起来 pad位置的注意力都很小 这样softmax后 几乎为 0 就不会注意到pad
  4. context_layer = torch.matmul(attention_probs, value_layer)
  5. context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
  6. new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,)
  7. context_layer = context_layer.view(new_context_layer_shape)

 上面代码就完成了attention is all your need 的这一数学公式 

上面的操作 做12次 就得到了encoder后的特征  大小是 128*77*768 

pool层

  1. sequence_output = encoder_outputs[0]
  2. pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
  1. class BertPooler(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.hidden_size, config.hidden_size)
  5. self.activation = nn.Tanh()
  6. def forward(self, hidden_states):
  7. # We "pool" the model by simply taking the hidden state corresponding
  8. # to the first token.
  9. first_token_tensor = hidden_states[:, 0]
  10. pooled_output = self.dense(first_token_tensor)
  11. pooled_output = self.activation(pooled_output)
  12. return pooled_output

 这个pool层的意思很简单 就是取cls 对应的特征 相当于 只取第一个字  因为cls 考虑了全局   他的大小是 128*768   然后经过一个mlp  就完成了 bert对text的编码 。 

  1. return BaseModelOutputWithPoolingAndCrossAttentions(
  2. last_hidden_state=sequence_output,
  3. pooler_output=pooled_output,
  4. past_key_values=encoder_outputs.past_key_values,
  5. hidden_states=encoder_outputs.hidden_states,
  6. attentions=encoder_outputs.attentions,
  7. cross_attentions=encoder_outputs.cross_attentions,
  8. )

看着很多但其实就返回了两个值 一个 是 pool前的 一个pool后的结果 

text 后处理

att = txt_tok['attention_mask']

取出att  就是那个 有字1 没字0的 

embs = (embs * att.unsqueeze(2)).sum(dim=1) / att.sum(dim=1)[:, None]

att 我们知道 是 128 * 77 

att.unsqueeze(2)    这个是指在第三维扩充一维  变成 128*77 *1  

embs 是pool前的结果 所以 他的大小是128 * 77*768  而*乘是指两个相同大小矩阵对应位置的数相乘。 

不相同怎么*乘呢 ? 其实有一个扩充  比如下面代码 。 a 大小是2*2  b是2*1  会自动把b的1维扩充为2 变成2*2  之后*乘 

a =torch.tensor([[1,1],[1,1]])
print(a)
b = torch.tensor([[2],[3]])
print(a*b)

*******************************

tensor([[1, 1],
        [1, 1]])
tensor([[2, 2],
        [3, 3]])

然后在第一维相加  也就是在77这里加  就是说不用pool了 现在我要取全部字的特征 加起来 然后 取平均值 。 (我觉得 这样不是很好 说实话)    那个

att.sum(dim=1)[:, None]  这个的意思是 看这句有多少字  然后[:,None]可以起到扩充维度的作用
a =torch.tensor([[1,1],[1,1]])
print(a)
print(a[:,None].shape)

torch.Size([2, 1, 2])

 return self.clip_head(embs)

clip_head 就是一个fc  把特征从768 到2048  最后结果是 128*2048 

回到clip 、

text_features = text_features / text_features.norm(dim=-1, keepdim=True)

return image_features, text_features, self.logit_scale.exp()

上面我们得到了文字和图像的编码 。  .norm表示二范数 用二范数归一化 。 但我真的不知道那个logit_scale_有啥用   可能后面有大用吧 

回到train函数    注意 我们从两个gpu回收了data 所以现在数据是 256 *2048 

  1. logits_per_image = logit_scale * image_features @ text_features.t()
  2. logits_per_text = logit_scale * text_features @ image_features.t()

logit_scale 在这里当一个倍数?   

logits_per_image 大小是256 *256 表示的意思是 256张 照片 每张图片 有256个分类结果 相当于一个分类256的网络结果。 其中一个是正确的txt 。   对于per——text也是一样。 
ground_truth = torch.arange(len(logits_per_image)).long()

        gt 是0到256 其实很简单 我们当作一个普通的分类任务结果。 第一个图片 有256个预测结果 ,他的真实结果是哪个呢 ? 显然是第一个文字 。 所以他的标签是0  . 着256张照片对应的txt下标就是 0,1,2,3,.。。。255   对于txt也是一样。 这里的loss是交叉熵损失。 

  1. scaler.scale(total_loss).backward()
  2. scaler.step(optimizer)
  3. scaler.update()

更新参数和loss    所以logit_scale有什么用 ? 

        m.logit_scale.data = torch.clamp(m.logit_scale.data, 0, 4.6052)

torch.clamp  表示让一个数落在一个区间  超过就 裁剪 。  所以logit_scale有什么用 ? 

我们可以看到  这里clip并没有像论文里那样用全部的数据进行对比学习 而只是用一个bat的256个数据进行对比学习  。也就是说  是一个阉割版的 。主要是官方很贴心 知道大家都没时间(没钱)所以写了个判断 如果在分布式系统上进行训练 ,才会 进行全部数据的对比学习。 

  1. if args.distributed and args.aggregate:
  2. world_size = dist.get_world_size()
  3. rank = dist.get_rank()
  4. # We gather tensors from all gpus to get more negatives to contrast with.
  5. gathered_image_features = [
  6. torch.zeros_like(image_features) for _ in range(world_size)
  7. ]
  8. gathered_text_features = [
  9. torch.zeros_like(text_features) for _ in range(world_size)
  10. ]
  11. dist.all_gather(gathered_image_features, image_features)
  12. dist.all_gather(gathered_text_features, text_features)
  13. all_image_features = torch.cat(
  14. [image_features]
  15. + gathered_image_features[:rank]
  16. + gathered_image_features[rank + 1 :]
  17. )
  18. all_text_features = torch.cat(
  19. [text_features]
  20. + gathered_text_features[:rank]
  21. + gathered_text_features[rank + 1 :]
  22. )
  23. # this is needed to send gradients back everywhere.
  24. logits_per_image = logit_scale * all_image_features @ all_text_features.t()
  25. logits_per_text = logits_per_image.t()

测试 

然后我们会进入 evaluate 也就是跑测试集。 一般跑测试集没啥好看的 不过 clip好像不一样。 

和训练得到loss的方法是一模一样的  但是得到loss后测试多了个步骤 我们来看看。 

  1. batch_size = len(images)
  2. cumulative_loss += total_loss * batch_size
  3. num_elements += batch_size
  4. metrics = get_metrics(
  5. image_features=torch.cat(all_image_features),
  6. text_features=torch.cat(all_text_features),
  7. logit_scale=logit_scale
  8. )

get_metrics

  1. def get_metrics(image_features, text_features, logit_scale):
  2. metrics = {}
  3. logits_per_image = (logit_scale * image_features @ text_features.t()).detach().cpu()
  4. logits_per_text = logits_per_image.t().detach().cpu()
  5. logits = {"image_to_text": logits_per_image, "text_to_image": logits_per_text}
  6. ground_truth = torch.arange(len(text_features)).view(-1, 1)

进入函数 并初始化一些东西  上面这些东西跟训练时是一样的 。  但不一样的是他的长度 。 他的长度足足有1000 .  注意GT的形状是1000*1 而不是1*1000 

  1. for name, logit in logits.items():
  2. ranking = torch.argsort(logit, descending=True)

logit  就是 1000*1000类。 先取的是图片预测文字 。 

ranking  大小是1000*1000   第一个1000是样本数 。 argsort得到的是排序后对应位置数的下标。 des表示倒序。   示例如下 。 最大数在第二个位置 所以开始是2 然后 1 , 0,3.

a = torch.tensor([3,5,7 , 1])
print(torch.argsort(a,descending=True))

tensor([2, 1, 0, 3])
  1. preds = torch.where(ranking == ground_truth)[1]
  2. preds = preds.detach().cpu().numpy()
ranking == ground_truth   这个会返回一个1000*1000的矩阵 ranking 和GT不一样  GT会扩充 变成1000*1000 .   就像下图。

之后 ranking 第一行 等于0的地方就会变成True 第二行等于1的地方变成True 以此类推。每行只有一个True。 

torch.where(CON,X,Y)x,y是相同形状的矩阵  con是条件 符合就返回x 不符就y  但是这里没有x,y  只有条件 他会相当于另一个函数:  torch.nonzero(condition, as_tuple=True)  他返回两个元组 分别表示 不为0的 元素的行索引 和列 索引。 比如下面  00 ,01,10 三个位置都不是0.

a = torch.tensor([[1,1],[2,0]])
print(torch.nonzero(a,as_tuple=True))
(tensor([0, 0, 1]), tensor([0, 1, 0]))

我们知道ranking == ground_truth  每行只有一个 1 剩下全是0 也就是说 每行只对应一个列索引 所以 preds  就每行 中为Ture的那个下标。  也就是标签和排序位置对应的标签 他的含义是什么呢 ? 

  1. metrics[f"{name}_mean_rank"] = preds.mean() + 1
  2. metrics[f"{name}_median_rank"] = np.floor(np.median(preds)) + 1

 保存两个值  一个是预测值的均值加1  一个是中位数取整后加1. 

  1. for k in [1, 5, 10]:
  2. metrics[f"{name}_R@{k}"] = np.mean(preds < k)

统计preds小于k的概率 。 到这里我似乎懂了一点点。   我们知道 最大的预测值 的下标都在第一个。 

比如这样一串数 他就是logit 经过argsort  变成了 2,1,0,4  也就是说 我们预测的值 就是2  那么 如果真实标签也是2  那么 在torch.where里就会返回0   也就是说 所有预测正确的都会返回0   就算不是0 我们也希望他越小越好。 就是说 希望预测值里 真值的概率排行比较靠前 。 

        那么问题很清楚了  这个所谓的矩阵  其实就是一个正确率统计罢了 。也就是我们经常在论文里看到的前5准确率 前10准确率。 

 

   

我们可以看到  前1准确率 0.322   前5准确率 0.677  前10准确率 0.817 

 return metrics

返回预测矩阵结果。    这样找预测正确率是不是很高端?  高端的我看了半天才知道在干嘛 .........

至此 clip 怎么用图片和text来训练的步骤就算完了。 

  所以logit_scale有什么用 ? ??

二 : 用预训练的模型去预测。 

给的baseline 预测出来全是0  我倒要看看你是怎么预测的 。 

  1. checkpoint_path = "/home/competition/jingdong/baseLine/src/training/logs/epoch_36.pt"
  2. test_data = "src/data/test.txt"
  3. attr_dict_file = "src/data/attr_to_attrvals.json"
  4. out_file = "test_pred.txt"
  5. # build model
  6. model = load_model(checkpoint_path)
  7. # test
  8. attr_dict = load_attr_dict(attr_dict_file)
  9. rets = []

设置模型 载入数据 

  1. data = json.loads(data)
  2. feature = np.array(data['feature']).astype(np.float32)
  3. texts = [data['title'] if a=='图文' else match_attrval(data['title'], a, attr_dict) for a in data['query']]
  4. features = torch.from_numpy(feature)[None, ].repeat(len(texts), 1)
  5. tokens = tokenize(texts)
  6. features = features.cuda()
  7. tokens = {k: v.cuda() for k, v in tokens.items()}

读数据和写数据 

这里很有意思 。 

一个数据进来  先把图文title 作为图文的字编码  把query也各自编码 比如 如果有两个quert  就会把query跟title一样 等于作为一个样本 然后把图像特征复制 三次 。 跟text对应 

  1. with torch.no_grad():
  2. image_features, text_features, _ = model(features, tokens)
  3. similarities = (image_features*text_features).sum(dim=-1)
  4. similarities = similarities.cpu().tolist()

之后两个特征相乘得到similarity  

  1. ret = {
  2. "img_name": data["img_name"],
  3. "match": {
  4. a: int(s>0.4 if a=='图文' else 0.04) for a, s in zip(data['query'], similarities)
  5. }
  6. }

   这段很有意思  就是说 如果是图文 当匹配度大于0.4 就是1   如果不是图文就输出0

也就是如果是属性 只能输出0  我估计这里是写错了  代码应该这样写 

 a: int(s>0.4 if a=='图文' else s > 0.04) for a, s in zip(data['query'], similarities)

   因为属性匹配的分数都很低 。 

  1. rets.append(json.dumps(ret, ensure_ascii=False)+'\n')
  2. with open(out_file, 'w') as f:
  3. f.writelines(rets)

写入结果 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/376058
推荐阅读
相关标签
  

闽ICP备14008679号