多模态大语言模型(MLLM)-Blip2深度解读

news2024/11/26 2:59:44

前言

在这里插入图片描述
Blip2是一个多模态大语言模型,因其提出时间较早(2023年),且效果较好,很快成为一个标杆性工作。Blip2中提出的Q-former也成为衔接多模态和文本的重要桥梁。

在这里插入图片描述Blip2发表时间是2023年,现在引用已经3288了,表明大家对Blip2背后的多模态语言模型是多么的追捧。

作者前期的工作还有Albef(先对齐再融合)、Blip1,都是十分硬核的工作。

创新点

  • Blip2利用冻结的预训练图像模型和语言模型,来有效减小纯多模态模型的训练成本。提出Q-Former框架,通过两个训练阶段(图文对齐+图文指令微调)来弥补模态GAP。在visual question answering, image captioning, and image-text retrieval等典型视觉-语言任务上表现出色。
  • 由LLM驱动,BLIP-2 可以zero-shot得执行图像到文本生成的。因为LLM具备涌现效应,Blip2能够实现视觉知识推理、视觉问答等较难的任务。
  • 由于使用了冻结的预训练图像模型和语言模型,Blip2的训练成本更低,例如,BLIP-2 在零样本 VQAv2 上比 Flamingo高出 8.7%,同时可训练参数减少了 54 倍。

具体细节

Blip2通过两阶段训练,来学习Q-Former模块。两阶段训练包含:

  • 一阶段视觉-语言表示学习(vision-language representation learning stage)
  • 二阶段视觉-语言生成学习(vision-to-language generative learning stage)

Q-former模块在这里插入图片描述

如上图,Q-Former包括左右两列并行的attention模块。左列为self attention+cross attention+feed forward,右列为self attention+feed forward。
左列用于提取图像特征
右列用于提取文本特征
左列+右列用于提取多模态特征

左列-图像特征

左列做的事情整体可以理解为输入N个learned query,对N个learned query做self attention,对输入图像做cross attention,得到N个总结后的图像输出(类似于目标检测的DETR算法,不同query关注不同区域)。论文中,N等于32。
具体如下:

输入learned queries(随机初始化的embedding)

query_tokens = nn.Parameter(
     torch.zeros(1, num_query_token, encoder_config.hidden_size)
 )
 query_tokens.data.normal_(mean=0.0, std=encoder_config.initializer_range)

先过self attention层,再和冻结的图像编码器输出的特征做cross attention,代码如下:

class BertLayer(nn.Module):
    def __init__(self, config, layer_num):
        super().__init__()
        self.config = config
        self.chunk_size_feed_forward = config.chunk_size_feed_forward
        self.seq_len_dim = 1
        self.attention = BertAttention(config)
        self.layer_num = layer_num
        if (
            self.config.add_cross_attention
            and layer_num % self.config.cross_attention_freq == 0
        ):
            self.crossattention = BertAttention(
                config, is_cross_attention=self.config.add_cross_attention
            )
            self.has_cross_attention = True
        else:
            self.has_cross_attention = False
        self.intermediate = BertIntermediate(config)
        self.output = BertOutput(config)

        self.intermediate_query = BertIntermediate(config)
        self.output_query = BertOutput(config)

    def forward(
        self,
        hidden_states,
        attention_mask=None,
        head_mask=None,
        encoder_hidden_states=None,
        encoder_attention_mask=None,
        past_key_value=None,
        output_attentions=False,
        query_length=0,
    ):
        # decoder uni-directional self-attention cached key/values tuple is at positions 1,2
        self_attn_past_key_value = (
            past_key_value[:2] if past_key_value is not None else None
        )
        self_attention_outputs = self.attention(
            hidden_states,
            attention_mask,
            head_mask,
            output_attentions=output_attentions,
            past_key_value=self_attn_past_key_value,
        )
        attention_output = self_attention_outputs[0]
        outputs = self_attention_outputs[1:-1]

        present_key_value = self_attention_outputs[-1]

        if query_length > 0:
            query_attention_output = attention_output[:, :query_length, :]

            if self.has_cross_attention:
                assert (
                    encoder_hidden_states is not None
                ), "encoder_hidden_states must be given for cross-attention layers"
                cross_attention_outputs = self.crossattention(
                    query_attention_output,
                    attention_mask,
                    head_mask,
                    encoder_hidden_states,
                    encoder_attention_mask,
                    output_attentions=output_attentions,
                )
                query_attention_output = cross_attention_outputs[0]
                outputs = (
                    outputs + cross_attention_outputs[1:-1]
                )  # add cross attentions if we output attention weights

            layer_output = apply_chunking_to_forward(
                self.feed_forward_chunk_query,
                self.chunk_size_feed_forward,
                self.seq_len_dim,
                query_attention_output,
            )
            if attention_output.shape[1] > query_length:
                layer_output_text = apply_chunking_to_forward(
                    self.feed_forward_chunk,
                    self.chunk_size_feed_forward,
                    self.seq_len_dim,
                    attention_output[:, query_length:, :],
                )
                layer_output = torch.cat([layer_output, layer_output_text], dim=1)
        else:
            layer_output = apply_chunking_to_forward(
                self.feed_forward_chunk,
                self.chunk_size_feed_forward,
                self.seq_len_dim,
                attention_output,
            )
        outputs = (layer_output,) + outputs

        outputs = outputs + (present_key_value,)

        return outputs

其中,self.crossattention输入包含query_attention_output(self attention输出),encoder_hidden_states(图像编码器输出)。Q-Former的实现就是对BertLayer进行魔改,通过cross_attention_freq参数来控制cross attention的频率,如果等于3,则第0、3、6层BertLayer会对图像特征做cross attention,其他实现流程和原实现的(huggingface实现)BertLayer类似。

提取图像特征的Q-Former调用方式为:

        image = samples["image"]

        image_embeds = self.ln_vision(self.visual_encoder(image))
        image_atts = torch.ones(image_embeds.size()[:-1], dtype=torch.long).to(
            image.device
        )

        query_tokens = self.query_tokens.expand(image_embeds.shape[0], -1, -1)

        query_output = self.Qformer.bert(
            query_embeds=query_tokens,
            encoder_hidden_states=image_embeds,
            encoder_attention_mask=image_atts,
            use_cache=True,
            return_dict=True,
        )

        image_feats = F.normalize(
            self.vision_proj(query_output.last_hidden_state), dim=-1
        )
右列-文本特征

右列的实现流程和左列类似,不同之处在于将BertLayer层中hidden_states改为文本编码,编码方式的实现也类似于原实现的(huggingface实现),如下:


class BertEmbeddings(nn.Module):
    """Construct the embeddings from word and position embeddings."""

    def __init__(self, config):
        super().__init__()
        self.word_embeddings = nn.Embedding(
            config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id
        )
        self.position_embeddings = nn.Embedding(
            config.max_position_embeddings, config.hidden_size
        )

        # self.LayerNorm is not snake-cased to stick with TensorFlow model variable name and be able to load
        # any TensorFlow checkpoint file
        self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
        self.dropout = nn.Dropout(config.hidden_dropout_prob)

        # position_ids (1, len position emb) is contiguous in memory and exported when serialized
        self.register_buffer(
            "position_ids", torch.arange(config.max_position_embeddings).expand((1, -1))
        )
        self.position_embedding_type = getattr(
            config, "position_embedding_type", "absolute"
        )

        self.config = config

    def forward(
        self,
        input_ids=None,
        position_ids=None,
        query_embeds=None,
        past_key_values_length=0,
    ):
        if input_ids is not None:
            seq_length = input_ids.size()[1]
        else:
            seq_length = 0

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

        if input_ids is not None:
            embeddings = self.word_embeddings(input_ids)
            if self.position_embedding_type == "absolute":
                position_embeddings = self.position_embeddings(position_ids)
                embeddings = embeddings + position_embeddings

            if query_embeds is not None:
                embeddings = torch.cat((query_embeds, embeddings), dim=1)
        else:
            embeddings = query_embeds

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

即将输入文本转为token,再将token转为embedding,embedding作为BertLayer层中hidden_states的输入。
其中,不涉及到cross attention。

提取文本特征的Q-Former调用方式为:

        text = samples["text_input"]

        text_tokens = self.tokenizer(
            text,
            padding="max_length",
            truncation=True,
            max_length=self.max_txt_len,
            return_tensors="pt",
        ).to(image.device)
        text_output = self.Qformer.bert(
            text_tokens.input_ids,
            attention_mask=text_tokens.attention_mask,
            return_dict=True,
        )
        text_feat = F.normalize(
            self.text_proj(text_output.last_hidden_state[:, 0, :]), dim=-1
        )
左列+右列

即输入图像特征,又输入文本,能够提取图文多模态表征,一般用作ITM的loss计算。

一阶段视觉-语言表示学习

该阶段的目的类似于clip,将视觉表征和文本表征拉到统一空间,具备三个损失函数,分别是Image-Text Contrastive Learning (ITC)、Image-grounded Text Generation (ITG)和Image-Text Matching (ITM)

Image-Text Contrastive Learning (ITC)

通过阶梯式的对比学习,缩小pair内图文距离,扩大pair间图文距离。
图像特征:Q-Former左列输出的图像特征,有N个
文本特征:Q-Former右列输出的CLS文本特征,有一个

因为图像特征有N个,每一次仅选择N个中和文本特征最近的那个来算ITC。在计算ITC时,负样本数量非常重要,通过多卡in-batch采样来获取多卡的负样本。代码详见:

		image_feats_all = concat_all_gather(
            image_feats
        )  # [batch_size*num_gpu, num_query_tokens, embed_dim]
        text_feat_all = concat_all_gather(text_feat)  # [batch_size*num_gpu, embed_dim]

        sim_q2t = torch.matmul(
            image_feats.unsqueeze(1), text_feat_all.unsqueeze(-1)
        ).squeeze()
        # [batch_size, batch_size*num_gpu, num_query_tokens]

        # image-text similarity: aggregate across all query tokens
        sim_i2t, _ = sim_q2t.max(-1)
        sim_i2t = sim_i2t / self.temp

        # text-query similarity: [batch_size, batch_size*num_gpu, num_query_tokens]
        sim_t2q = torch.matmul(
            text_feat.unsqueeze(1).unsqueeze(1), image_feats_all.permute(0, 2, 1)
        ).squeeze()

        # text-image similarity: aggregate across all query tokens
        sim_t2i, _ = sim_t2q.max(-1)
        sim_t2i = sim_t2i / self.temp  # [batch_size, batch_size*num_gpu]

        rank = dist.get_rank()
        bs = image.size(0)
        targets = torch.linspace(rank * bs, rank * bs + bs - 1, bs, dtype=int).to(
            image.device
        )
         
        loss_itc = (
              F.cross_entropy(sim_i2t, targets, label_smoothing=0.1)
              + F.cross_entropy(sim_t2i, targets, label_smoothing=0.1)
          ) / 2
Image-grounded Text Generation (ITG)

通过Q-Former架构,实现image caption任务。首先利用Q-Former提取图像特征,将图像特征作为输入,迭代式得利用LM loss约束文本输出。

       	text_tokens = self.tokenizer(
            text,
            padding="max_length",
            truncation=True,
            max_length=self.max_txt_len,
            return_tensors="pt",
        ).to(image.device)
        
        image_embeds = self.ln_vision(self.visual_encoder(image))
        image_atts = torch.ones(image_embeds.size()[:-1], dtype=torch.long).to(
            image.device
        )

        query_tokens = self.query_tokens.expand(image_embeds.shape[0], -1, -1)

        query_output = self.Qformer.bert(
            query_embeds=query_tokens,
            encoder_hidden_states=image_embeds,
            encoder_attention_mask=image_atts,
            use_cache=True,
            return_dict=True,
        )
        
        decoder_input_ids = text_tokens.input_ids.clone()
        decoder_input_ids[:, 0] = self.tokenizer.bos_token_id
        labels = decoder_input_ids.masked_fill(
            decoder_input_ids == self.tokenizer.pad_token_id, -100
        )

        query_atts = torch.ones(query_tokens.size()[:-1], dtype=torch.long).to(
            image.device
        )
        attention_mask = torch.cat([query_atts, text_tokens.attention_mask], dim=1)
        lm_output = self.Qformer(
            decoder_input_ids,
            attention_mask=attention_mask,
            past_key_values=query_output.past_key_values,
            return_dict=True,
            labels=labels,
        )

        loss_lm = lm_output.loss
Image-Text Matching (ITM)

ITM的本质在于输入一个图文pair,用以判断图文pair是否匹配。借助二分类来实现,输出1为匹配;输出0为不匹配。
Blip2在构造图文pair时,采用了多种采样策略

  • 匹配的图文pair,label均为1,表示匹配
  • 图固定,图-文距离为权值,利用torch.multinomial采样文,构成图文pair,label均为0
  • 文固定,图-文距离为权值,利用torch.multinomial采样图,构成图文pair,label均为0
    代码详见:
		text_input_ids_world = concat_all_gather(text_tokens.input_ids)
        text_attention_mask_world = concat_all_gather(text_tokens.attention_mask)
        image_embeds_world = all_gather_with_grad(image_embeds)
        with torch.no_grad():
            if "image_id" in samples.keys():
                mask = torch.eq(image_ids, image_ids_all.t())
                sim_t2i.masked_fill_(mask, -10000)
                sim_i2t.masked_fill_(mask, -10000)
            else:    
                sim_t2i[:, rank * bs : rank * bs + bs].fill_diagonal_(-10000)
                sim_i2t[:, rank * bs : rank * bs + bs].fill_diagonal_(-10000)            
                
            weights_t2i = F.softmax(sim_t2i, dim=1)
            weights_i2t = F.softmax(sim_i2t, dim=1)

        # select a negative image for each text
        image_embeds_neg = []
        for b in range(bs):
            neg_idx = torch.multinomial(weights_t2i[b], 1).item()
            image_embeds_neg.append(image_embeds_world[neg_idx])
        image_embeds_neg = torch.stack(image_embeds_neg, dim=0)

        # select a negative text for each image
        text_ids_neg = []
        text_atts_neg = []
        for b in range(bs):
            neg_idx = torch.multinomial(weights_i2t[b], 1).item()
            text_ids_neg.append(text_input_ids_world[neg_idx])
            text_atts_neg.append(text_attention_mask_world[neg_idx])

        text_ids_neg = torch.stack(text_ids_neg, dim=0)
        text_atts_neg = torch.stack(text_atts_neg, dim=0)

        text_ids_all = torch.cat(
            [text_tokens.input_ids, text_tokens.input_ids, text_ids_neg], dim=0
        )  # pos, pos, neg
        text_atts_all = torch.cat(
            [text_tokens.attention_mask, text_tokens.attention_mask, text_atts_neg],
            dim=0,
        )

        query_tokens_itm = self.query_tokens.expand(text_ids_all.shape[0], -1, -1)
        query_atts_itm = torch.ones(query_tokens_itm.size()[:-1], dtype=torch.long).to(
            image.device
        )
        attention_mask_all = torch.cat([query_atts_itm, text_atts_all], dim=1)

        image_embeds_all = torch.cat(
            [image_embeds, image_embeds_neg, image_embeds], dim=0
        )  # pos, neg, pos
        image_atts_all = torch.ones(image_embeds_all.size()[:-1], dtype=torch.long).to(
            image.device
        )

        output_itm = self.Qformer.bert(
            text_ids_all,
            query_embeds=query_tokens_itm,
            attention_mask=attention_mask_all,
            encoder_hidden_states=image_embeds_all,
            encoder_attention_mask=image_atts_all,
            return_dict=True,
        )

        vl_embeddings = output_itm.last_hidden_state[:, : query_tokens_itm.size(1), :]
        vl_output = self.itm_head(vl_embeddings)
        logits = vl_output.mean(dim=1)

        itm_labels = torch.cat(
            [torch.ones(bs, dtype=torch.long), torch.zeros(2 * bs, dtype=torch.long)],
            dim=0,
        ).to(image.device)
        loss_itm = F.cross_entropy(logits, itm_labels)

输入图像特征、文本,提取图文pair的多模态特征,再送到二元分类器,实现ITM的loss计算。

总体loss

总体loss等于上述三个loss的加和

        return BlipOutput(
            loss=loss_itc + loss_itm + loss_lm,
            loss_itc=loss_itc,
            loss_itm=loss_itm,
            loss_lm=loss_lm,
        )

二阶段视觉-语言生成学习

在这里插入图片描述
在生成阶段,需要做

  • 输入图像,借助冻结的图像编码器,提取图像特征。图像特征送入Q-Former,提取图文对齐的图像特征(32个embedding)
  • 采用一个FC层,将图像特征维度与LLM维度对齐
  • 将图像特征作为soft visual prompts,输入到LLM中,借助图文指令微调数据,训练Q-Former、FC层。

实验数据

预训练

Blip2采用129M图片,包括COCO、Visual Genome、CC3M、CC12M、SBU,以及LAION400M。其中115M来自于LAION400M,使用CapFilt对网图进行生成caption,具体步骤如下:
1、使用Blip模型 生成10个caption;
2、10个caption+原始web caption通过CLIP模型计算图像-caption排序;
3、选取top2作为该图的caption,以此作为训练数据;

预训练图像编码器与LLM

两个SOTA视觉transformer预训练模型:
ViT-L/14 from CLIP、ViT-G/14 from EVA-CLIP
移除ViT最后一层,使用倒数第二层特征。
LLM模型:
无监督训练的OPT作为decoder-based LLM
基于指令训练的FlanT5作为encoder-decoder-based LLM

预训练设置

第一阶段训练250k step,第二阶段训练80k step;ViT和LLM 转为FP16,FlanT5转为BFloat16,作者发现相对于32-bit,性能无下降;由于使用frozen模型,作者预训练比现在大规模VLP方法计算量都小,在16个A100(40G)上,对于ViT-G和FlanT5-XXL第一阶段训练耗时6天,第二阶段少于3天。
因为绝大部分参数(图像编码器、LLM)都冻结,所以训练成本较低,这也是Blip2较流行的一个原因


插一句嘴,目前的多模态LLM范式较Blip2更加简单。Blip2采用Q-Former实现图文对齐,现在的大部分工作直接采用FC层实现图文对齐,效果和Q-Former类似,但训练成本更低。相关工作有Llava1.5等

后续会持续更新多模态LLM的相关论文

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2194171.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

产品经理内容分享(一):AI产品经理需必备那些能力

目录 必备的AI技术知识 第一章:AI产品经理是否需要懂技术及其程度 第二章:AI产品经理必备的AI技术基础知识——基础算法与机器学习方法 第三章:AI产品经理必须要懂的AI技术知识——场景应用 第四章:AI算法与模型的关系 第五…

PhotoMaker部署文档

一、介绍 PhotoMaker:一种高效的、个性化的文本转图像生成方法,能通过堆叠 ID 嵌入自定义逼真的人类照片。相当于把一张人的照片特征提取出来,然后可以生成你想要的不同风格照片,如写真等等。 主要特点: 在几秒钟内…

求1000以内的完数

题目:一个数如果恰好等于他的因子之和(包括1,但不包括这个数),这个数就是完数。编写算法找出1000之内的所有完数,并按下面格式输出其因子:28 its factors are 1,2,4,7,14 代码如下:…

Dell服务器电源配置

Dell服务器电源配置规则 PowerEdge 电源设置

医院综合服务系统小程序的设计

管理员账户功能包括:系统首页,个人中心,患者管理,医生管理,就诊信息管理,科室信息管理,挂号信息管理,系统管理 微信端账号功能包括:系统首页,订单排队&#…

《PyTorch深度学习快速入门教程》学习笔记(第15周)

目录 摘要 Abstract 1. 安装Anaconda 2. 查看显卡驱动 3. 安装Pytorch 4. Pytorch加载数据 5. 常用数据集两种形式 6. 路径直接加载数据 7. Dataset加载数据 摘要 本周报的目的在于汇报《PyTorch深度学习快速入门教程》课程第一周的学习成果,主要聚焦于py…

微服务Sleuth解析部署使用全流程

目录 1、Sleuth链路追踪 1、添加依赖 2、修改日志配置文件 3、测试 2、zipkin可视化界面 1、docker安装 2、添加依赖 3、修改配置文件 4、查看页面 5、ribbon配置 1、Sleuth链路追踪 sleuth是链路追踪框架,用于在微服务架构下开发,各个微服务之…

轻松部署大模型:Titan Takeoff入门指南

轻松部署大模型:Titan Takeoff入门指南 在人工智能的快速发展中,处理自然语言处理(NLP)任务的大规模语言模型(LLM)至关重要。然而,部署这些模型往往具有挑战性,需要高性能的硬件和优…

设计模式之适配器模式(Adapter)

一、适配器模式介绍 适配器模式(adapter pattern )的原始定义是:将类的接口转换为客户期望的另一个接口, 适配器可以让不兼容的两个类一起协同工作。 适配器模式是用来做适配,它将不兼容的接口转换为可兼容的接口,让原本由于接口…

YOLOv10改进,YOLOv10添加CA注意力机制,二次创新C2f结构,助力涨点

改进前训练结果: 二次创新C2f结构训练结果: 摘要 在本文中,提出了一种新的移动网络注意力机制,将位置信息嵌入到信道注意力中称之为“协调注意力”。与渠道关注不同通过 2D 全局池将特征张量转换为单个特征向量,坐标注意力因子将通道注意力转化为两个 1D 特征编码过程…

如何在AI时代成为优秀的AI产品经理?全面解析与全套学习路径分享!!!

前言 在当前人工智能技术飞速发展的时代背景下,AI产品经理无疑成为了职场中的一片蓝海。随着AI技术在各行各业的广泛应用,AI产品经理的角色变得越来越重要,成为了众多求职者眼中的优质赛道。那么,如何在AI的大环境下成为一名优秀…

李宏毅深度学习-自注意力机制

输入是向量序列的情况 在图像识别的时候,假设输入的图像大小都是一样的。但如果问题变得复杂,如图6.2所示,输入是一组向量,并且输入的向量的数量是会改变的,即每次模型输入的序列长度都不一样,这个时候应该…

搬砖 网盘一键转存源码

网盘一键转存源码,免费资源没测试 网盘一键转存源码,可以将您的百度网盘资源一键转存到。并支持后台设置开屏广告 源码截图: 下载地址: https://yuncv.lanzouw.com/i8dZk2btyl4h

六自由度机械重力补偿控制

1.动力学方程 六自由度机械臂动力学方程形式如下: 进行重力补偿,就是在驱动力矩中对重力G进行补偿,从而消除重力的影响,这样就能够在进行闭环控制的时候避免重力影响带来的大超调问题,使得机器人更好的实现轨迹跟踪控…

如何使用BlinkShot.io生成照片

在当今的数字时代,AI生成照片已经成为一项令人惊叹的技术。而BlinkShot.io就是这样一个平台,它可以让你轻松生成各种类型的照片。以下是详细步骤,教你如何使用BlinkShot.io生成照片。 第一步:访问网站 首先,打开Blin…

python调用父类同名成员

语法 print(f"父类的厂商是:{Phone.producer}“) Phone.call_by_5g(self) print(f"父类的厂商是:{super().producer}”) print(f"父类的序列号是:{super().IMEI}") super().call_by_5g() print(“关闭CPU单核模式&…

AIGC下的数据战略,助力还是阻力?

AIGC下的数据战略,助力还是阻力? 前言一、生成式AI的崛起与影响二、企业数据战略的关键要点(一)找准应用方向,激发创新价值(二)准备专有数据,确保数据安全(三&#xff09…

毕业设计项目(难度高)——文本驱动的可控人体动作生成方法(论文/代码)

完整的论文代码见文章末尾 以下为核心内容 摘要 本文实现了一种基于扩散模型的文本驱动的可控人体动作生成方法。本文利用先进的交叉模态线性变换器及细粒度控制技术,根据自然语言描述生成逼真的人体动作序列。扩散模型在生成高质量图像和视频方面有较大优点&…

爆火!最新大模型算法岗 100 道面试题全解析,赶紧收藏!

大模型应该是目前当之无愧的最有影响力的AI技术,它正在革新各个行业,包括自然语言处理、机器翻译、内容创作和客户服务等等,正在成为未来商业环境的重要组成部分。 截至目前大模型已经超过200个,在大模型纵横的时代,不…

【C++】多线程编程图文详解(多角度详解,小白一看就懂!!)

目录 一、前言 二、什么是C多线程? 💢线程与进程 💢并发与并行 💢多线程 三、 线程库 - thread 1. 线程对象的构造方式 无参构造 带可变参数包的构造 移动构造 2. thread类的成员函数 join 和 joinable deta…