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

目录

bert

embedding层

encoder

pool层

text 后处理

回到clip 、

测试 

get_metrics

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

读数据和写数据 


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

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

我们直接从clip训练开始。

    dataloader, sampler = data['train'].dataloader,  data['train'].sampler

    loss_img = nn.CrossEntropyLoss()
    loss_txt = nn.CrossEntropyLoss()
    if args.gpu is not None:
        loss_img = loss_img.cuda(args.gpu)
        loss_txt = loss_txt.cuda(args.gpu)

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

    for i, batch in enumerate(dataloader):
        step = num_batches_per_epoch * epoch + i
        scheduler(step)

        optimizer.zero_grad()

        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的编码就这两句  第一句编图像 第二句编文字 但这里图像编码好了 所以去编码文字 。 

        image_features = self.encode_image(image)
        text_features = self.encode_text(text)
class TextModel(torch.nn.Module):
    def __init__(self, model_name='M-CLIP/M-BERT-Base-69', out_features=2048):
        super().__init__()
        self.model_name = model_name
        self.transformer = transformers.AutoModel.from_pretrained(model_name, cache_dir='~/.cache')
        in_features = self.transformer.pooler.dense.out_features
        self.clip_head = torch.nn.Linear(in_features=in_features, out_features=out_features)
    
    def forward(self, txt_tok):
        embs = self.transformer(**txt_tok)[0]
        att = txt_tok['attention_mask']
        embs = (embs * att.unsqueeze(2)).sum(dim=1) / att.sum(dim=1)[:, None]
        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。  

        seq_length = input_shape[1]

        if position_ids is None:
            position_ids = self.position_ids[:, past_key_values_length : seq_length + past_key_values_length]

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

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

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

        embeddings = self.LayerNorm(embeddings)
        embeddings = self.dropout(embeddings)
        return embeddings

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

encoder

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

下面是其中一层。 

class BertLayer(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.chunk_size_feed_forward = config.chunk_size_feed_forward
        self.seq_len_dim = 1
        self.attention = BertAttention(config)
        self.is_decoder = config.is_decoder
        self.add_cross_attention = config.add_cross_attention
        if self.add_cross_attention:
            if not self.is_decoder:
                raise ValueError(f"{self} should be used as a decoder model if cross attention is added")
            self.crossattention = BertAttention(config, position_embedding_type="absolute")
        self.intermediate = BertIntermediate(config)
        self.output = BertOutput(config)


class BertAttention(nn.Module):
    def __init__(self, config, position_embedding_type=None):
        super().__init__()
        self.self = BertSelfAttention(config, position_embedding_type=position_embedding_type)
        self.output = BertSelfOutput(config)
        self.pruned_heads = set()

        self.num_attention_heads = config.num_attention_heads
        self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
        self.all_head_size = self.num_attention_heads * self.attention_head_size

        self.query = nn.Linear(config.hidden_size, self.all_head_size)
        self.key = nn.Linear(config.hidden_size, self.all_head_size)
        self.value = nn.Linear(config.hidden_size, self.all_head_size)

        self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
        self.position_embedding_type = position_embedding_type or getattr(
            config, "position_embedding_type", "absolute"
        )
        if self.position_embedding_type == "relative_key" or self.position_embedding_type == "relative_key_query":
            self.max_position_embeddings = config.max_position_embeddings
            self.distance_embedding = nn.Embedding(2 * config.max_position_embeddings - 1, self.attention_head_size)

        self.is_decoder = config.is_decoder

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

mixed_query_layer = self.query(hidden_states)
query_layer = self.transpose_for_scores(mixed_query_layer)
 key_layer = self.transpose_for_scores(self.key(hidden_states))
value_layer = self.transpose_for_scores(self.value(hidden_states))

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

attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
attention_scores = attention_scores / math.sqrt(self.attention_head_size)


attention_scores = attention_scores + attention_mask   #注意这一句  attention 前面做了反向 也就是说 pad的位置上 都是-10000 很小 然后加起来  pad位置的注意力都很小 这样softmax后 几乎为 0 就不会注意到pad

    
        context_layer = torch.matmul(attention_probs, value_layer)

        context_layer = context_layer.permute(0, 2, 1, 3).contiguous()


        new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,)
        context_layer = context_layer.view(new_context_layer_shape)

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

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

pool层

        sequence_output = encoder_outputs[0]
        pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
class BertPooler(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        self.activation = nn.Tanh()

    def forward(self, hidden_states):
        # We "pool" the model by simply taking the hidden state corresponding
        # to the first token.
        first_token_tensor = hidden_states[:, 0]
        pooled_output = self.dense(first_token_tensor)
        pooled_output = self.activation(pooled_output)
        return pooled_output

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

        return BaseModelOutputWithPoolingAndCrossAttentions(
            last_hidden_state=sequence_output,
            pooler_output=pooled_output,
            past_key_values=encoder_outputs.past_key_values,
            hidden_states=encoder_outputs.hidden_states,
            attentions=encoder_outputs.attentions,
            cross_attentions=encoder_outputs.cross_attentions,
        )

看着很多但其实就返回了两个值 一个 是 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 

        logits_per_image = logit_scale * image_features @ text_features.t()
        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是交叉熵损失。 

                scaler.scale(total_loss).backward()
                scaler.step(optimizer)
            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个数据进行对比学习  。也就是说  是一个阉割版的 。主要是官方很贴心 知道大家都没时间(没钱)所以写了个判断 如果在分布式系统上进行训练 ,才会 进行全部数据的对比学习。 

    if args.distributed and args.aggregate:
        world_size = dist.get_world_size()
        rank = dist.get_rank()

        # We gather tensors from all gpus to get more negatives to contrast with.
        gathered_image_features = [
            torch.zeros_like(image_features) for _ in range(world_size)
        ]
        gathered_text_features = [
            torch.zeros_like(text_features) for _ in range(world_size)
        ]
        dist.all_gather(gathered_image_features, image_features)
        dist.all_gather(gathered_text_features, text_features)

        all_image_features = torch.cat(
            [image_features]
            + gathered_image_features[:rank]
            + gathered_image_features[rank + 1 :]
        )
        all_text_features = torch.cat(
            [text_features]
            + gathered_text_features[:rank]
            + gathered_text_features[rank + 1 :]
        )

        # this is needed to send gradients back everywhere.
        logits_per_image = logit_scale * all_image_features @ all_text_features.t()
        logits_per_text = logits_per_image.t()

测试 

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

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

            batch_size = len(images)
            cumulative_loss += total_loss * batch_size
            num_elements += batch_size

        metrics = get_metrics(
            image_features=torch.cat(all_image_features),
            text_features=torch.cat(all_text_features),
            logit_scale=logit_scale
        )

get_metrics

def get_metrics(image_features, text_features, logit_scale):
    metrics = {}
    logits_per_image = (logit_scale * image_features @ text_features.t()).detach().cpu()
    logits_per_text = logits_per_image.t().detach().cpu()

    logits = {"image_to_text": logits_per_image, "text_to_image": logits_per_text}
    ground_truth = torch.arange(len(text_features)).view(-1, 1)

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

    for name, logit in logits.items():
        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])
preds = torch.where(ranking == ground_truth)[1] 
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的那个下标。  也就是标签和排序位置对应的标签 他的含义是什么呢 ? 

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

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

        for k in [1, 5, 10]:
            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  我倒要看看你是怎么预测的 。 

checkpoint_path = "/home/competition/jingdong/baseLine/src/training/logs/epoch_36.pt"
test_data = "src/data/test.txt"
attr_dict_file = "src/data/attr_to_attrvals.json"
out_file = "test_pred.txt"

# build model
model = load_model(checkpoint_path)

# test
attr_dict = load_attr_dict(attr_dict_file)
rets = []

设置模型 载入数据 

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

读数据和写数据 

这里很有意思 。 

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

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

之后两个特征相乘得到similarity  

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

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

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

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

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

       
        rets.append(json.dumps(ret, ensure_ascii=False)+'\n')

with open(out_file, 'w') as f:
    f.writelines(rets)

写入结果 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值