使用Pytorch从零开始实现CLIP

生成式建模知识回顾:
[1] 生成式建模概述
[2] Transformer I,Transformer II
[3] 变分自编码器
[4] 生成对抗网络,高级生成对抗网络 I,高级生成对抗网络 II
[5] 自回归模型
[6] 归一化流模型
[7] 基于能量的模型
[8] 扩散模型 I, 扩散模型 II
在这里插入图片描述

引言

2021 年 1 月,OpenAI 宣布了两种新模型:DALL-E 和 CLIP,这两种模型都是以某种方式连接文本和图像的多模态模型。在本文中,我们将在PyTorch中从零开始实现 CLIP 模型。OpenAI 开源了一些与 CLIP 模型相关的代码,但我发现它令人生畏,而且并不简洁。

CLIP 有什么作用?为什么有趣?

在《Learning Transferable Visual Models From Natural Language Supervision》论文中,OpenAI 介绍了他们的新模型,称为CLIP,用于Contrastive Language-Image Pre-training。简而言之,该模型学习整个句子与其描述的图像之间的关系;从某种意义上说,当训练模型时,给定一个输入句子,它将能够检索与该句子相对应的最相关的图像。这里重要的是,它是在完整的句子上进行训练,而不是像car、dog等单一类别一样。直觉上,当在整个句子上进行训练时,模型可以学习更多的东西,并找到图像和文本之间的一些模式。

他们还表明,当该模型在巨大的图像数据集及其相应文本上进行训练时,它也可以充当分类器。我鼓励你研究论文原文,以更多地了解这个令人兴奋的模型及其在基准数据集上的惊人结果。仅举一例,使用此策略训练的 CLIP 模型对 ImageNet 的分类效果比在 ImageNet 本身上训练的 SOTA 模型更好,该 SOTA 模型专门针对单一分类任务进行了优化!

先跳过过程,让我们看看我们将在本文中从头开始构建的最终模型能够实现什么功能:给出诸如“一个男孩用滑板跳跃”或“一个女孩从秋千上跳跃”这样的查询,模型将检索最相关的图像:
在这里插入图片描述

开始

让我们直接看它的 PyTorch 实现。首先,我们需要一个包含图像和一些描述它们的文本的数据集。坦率地说,网上有很多可用的。我们将使用Flickr 8k 数据集(您可以使用更大的 30k 版本,最终模型的性能会更好),该数据集主要用于图像字幕任务。但是, 我们也可以用它来训练 CLIP 模型。

以下代码将下载 8k(如果取消注释最后几行,则下载 30k)并解压缩它们。Kaggle数据集之下载可参考前文。

!pip install kaggle --upgrade
import os
os.environ['KAGGLE_USERNAME'] = "XXXXXX"
os.environ['KAGGLE_KEY'] = "XXXXXXXXXXXXXXXXXXXXXX" # Enter your Kaggle key here

# For Flickr 8k
!kaggle datasets download -d adityajn105/flickr8k
!unzip flickr8k.zip
dataset = "8k"


# For Flickr 30k
# !kaggle datasets download -d hsankesara/flickr-image-dataset
# !unzip flickr-image-dataset.zip
# dataset = "30k"

关于此数据集需要注意的一件事是: 每张图像都有 5 个标题。后面写损失函数的时候再讲这个!

数据集

正如您在本文的标题图片中看到的,我们需要对图像及其描述文本进行编码。因此,数据集需要返回图像和文本。当然,我们不会将原始文本提供给我们的文本编码器!我们将使用HuggingFace库中的DistilBERT模型(它比 BERT 小,但性能几乎与 BERT 一样)作为我们的文本编码器;因此,我们需要使用 DistilBERT 分词器对句子(标题)进行分词,然后将分词 id (input_ids) 和注意力掩码提供给 DistilBERT。因此,数据集也需要处理标记化。您可以在下面看到数据集的代码。下面我将解释代码中发生的最重要的事情。

关于配置和CFG的说明:我用 python 脚本编写了代码,然后将其转换为 Jupyter Notebook。因此,对于 python 脚本,config 是一个普通的 python 文件,我在其中放置所有超参数,对于 Jupyter Notebook,它是在笔记本开头定义的一个类,用于保留所有超参数。查看GitHub 存储库或笔记本以查看所有超参数。

import os
import cv2
import torch
import albumentations as A

import config as CFG

class CLIPDataset(torch.utils.data.Dataset):
    def __init__(self, image_filenames, captions, tokenizer, transforms):
        """
        image_filenames and cpations must have the same length; so, if there are
        multiple captions for each image, the image_filenames must have repetitive
        file names 
        """

        self.image_filenames = image_filenames
        self.captions = list(captions)
        self.encoded_captions = tokenizer(
            list(captions), padding=True, truncation=True, max_length=CFG.max_length
        )
        self.transforms = transforms

    def __getitem__(self, idx):
        item = {
            key: torch.tensor(values[idx])
            for key, values in self.encoded_captions.items()
        }

        image = cv2.imread(f"{CFG.image_path}/{self.image_filenames[idx]}")
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        image = self.transforms(image=image)['image']
        item['image'] = torch.tensor(image).permute(2, 0, 1).float()
        item['caption'] = self.captions[idx]

        return item


    def __len__(self):
        return len(self.captions)



def get_transforms(mode="train"):
    if mode == "train":
        return A.Compose(
            [
                A.Resize(CFG.size, CFG.size, always_apply=True),
                A.Normalize(max_pixel_value=255.0, always_apply=True),
            ]
        )
    else:
        return A.Compose(
            [
                A.Resize(CFG.size, CFG.size, always_apply=True),
                A.Normalize(max_pixel_value=255.0, always_apply=True),
            ]
        )

init 中,我们收到一个 tokenizer 对象,它实际上是一个 HuggingFace tokinzer;运行模型时将加载此标记生成器。我们将字幕填充并截断为指定的 max_length。在 getitem 中,我们将首先加载一个编码的标题,它是一个带有 input_ids 和 Attention_mask 键的字典,根据其值创建张量,然后我们将加载相应的图像,对其进行变换和增强(如果有的话!),然后我们将其设为张量并将其放入以“image”为键的字典中。最后,我们将带有“caption”键的标题的原始文本放入字典中,仅用于可视化目的。

我没有使用额外的数据增强,但如果您想提高模型的性能,可以添加它们。

图像编码器

图像编码器代码很简单。我在这里使用 PyTorch 图像模型库 (timm),它提供了从 ResNets 到 EfficientNets 等许多不同的图像模型。这里我们将使用ResNet50作为我们的图像编码器。如果您不想安装新的库,您可以轻松地使用 torchvision 库来使用 ResNets。

class ImageEncoder(nn.Module):
    """
    Encode images to a fixed size vector
    """

    def __init__(
        self, model_name=CFG.model_name, pretrained=CFG.pretrained, trainable=CFG.trainable
    ):
        super().__init__()
        self.model = timm.create_model(
            model_name, pretrained, num_classes=0, global_pool="avg"
        )
        for p in self.model.parameters():
            p.requires_grad = trainable

    def forward(self, x):
        return self.model(x)

该代码将每个图像编码为固定大小的向量,其大小与模型输出通道的大小相同(在 ResNet50 的情况下,向量大小将为2048)。这是 nn.AdaptiveAvgPool2d() 层之后的输出。

文本编码器

正如我之前提到的,我将使用 DistilBERT 作为文本编码器。与它的大哥 BERT 一样,两个特殊的标记将被添加到实际的输入标记中:CLS和SEP,它们标记句子的开始和结束。为了获取句子的完整表示(正如相关的 BERT 和 DistilBERT 论文所指出的那样),我们使用 CLS 标记的最终表示,并且我们希望该表示能够捕获句子(标题)的整体含义。这样想的话,就类似于我们对图像的处理,将其转换为固定大小的向量。

from transformers import DistilBertModel, DistilBertConfig

class TextEncoder(nn.Module):
    def __init__(self, model_name=CFG.text_encoder_model, pretrained=CFG.pretrained, trainable=CFG.trainable):
        super().__init__()
        if pretrained:
            self.model = DistilBertModel.from_pretrained(model_name)
        else:
            self.model = DistilBertModel(config=DistilBertConfig())
            
        for p in self.model.parameters():
            p.requires_grad = trainable

        # we are using the CLS token hidden representation as the sentence's embedding
        self.target_token_idx = 0

    def forward(self, input_ids, attention_mask):
        output = self.model(input_ids=input_ids, attention_mask=attention_mask)
        last_hidden_state = output.last_hidden_state
        return last_hidden_state[:, self.target_token_idx, :]

对于 DistilBERT(以及 BERT),每个标记的输出隐藏表示是一个大小为768的向量。因此,整个标题将被编码为大小为 768 的 CLS 令牌表示形式。

Projection Head

我使用Keras 代码示例实现在 PyTorch 中编写了以下内容。

现在我们已经将图像和文本编码为固定大小的向量(图像为 2048,文本为 768),我们需要将它们带(投影)到一个图像和文本具有相似尺寸的新世界(!),以便能够对它们进行比较,将不相关的图像和文本分开,并将匹配的图像和文本放在一起。因此,以下代码将把 2048 和 768 维向量带入 256 (projection_dim) 维世界,我们可以在其中比较它们:

import torch
from torch import nn

class ProjectionHead(nn.Module):
    def __init__(
        self,
        embedding_dim,
        projection_dim=CFG.projection_dim,
        dropout=CFG.dropout
    ):
        super().__init__()
        self.projection = nn.Linear(embedding_dim, projection_dim)
        self.gelu = nn.GELU()
        self.fc = nn.Linear(projection_dim, projection_dim)
        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(projection_dim)
    
    def forward(self, x):
        projected = self.projection(x)
        x = self.gelu(projected)
        x = self.fc(x)
        x = self.dropout(x)
        x = x + projected
        x = self.layer_norm(x)
        return x

“embedding_dim”是输入向量的大小(图像为 2048,文本为 768),“projection_dim”是输出向量的大小,在我们的例子中为 256。要了解这部分的详细信息,您可以参考CLIP 论文。

CLIP模型

这部分是最有趣的!这里我还要讲一下损失函数。我将Keras 代码示例中的一些代码翻译成 PyTorch 来编写这部分。查看代码,然后阅读该代码块下面的说明。

import torch
from torch import nn
import torch.nn.functional as F

import config as CFG
from modules import ImageEncoder, TextEncoder, ProjectionHead


class CLIPModel(nn.Module):
    def __init__(
        self,
        temperature=CFG.temperature,
        image_embedding=CFG.image_embedding,
        text_embedding=CFG.text_embedding,
    ):
        super().__init__()
        self.image_encoder = ImageEncoder()
        self.text_encoder = TextEncoder()
        self.image_projection = ProjectionHead(embedding_dim=image_embedding)
        self.text_projection = ProjectionHead(embedding_dim=text_embedding)
        self.temperature = temperature

    def forward(self, batch):
        # Getting Image and Text Features
        image_features = self.image_encoder(batch["image"])
        text_features = self.text_encoder(
            input_ids=batch["input_ids"], attention_mask=batch["attention_mask"]
        )
        # Getting Image and Text Embeddings (with same dimension)
        image_embeddings = self.image_projection(image_features)
        text_embeddings = self.text_projection(text_features)

        # Calculating the Loss
        logits = (text_embeddings @ image_embeddings.T) / self.temperature
        images_similarity = image_embeddings @ image_embeddings.T
        texts_similarity = text_embeddings @ text_embeddings.T
        targets = F.softmax(
            (images_similarity + texts_similarity) / 2 * self.temperature, dim=-1
        )
        texts_loss = cross_entropy(logits, targets, reduction='none')
        images_loss = cross_entropy(logits.T, targets.T, reduction='none')
        loss =  (images_loss + texts_loss) / 2.0 # shape: (batch_size)
        return loss.mean()


def cross_entropy(preds, targets, reduction='none'):
    log_softmax = nn.LogSoftmax(dim=-1)
    loss = (-targets * log_softmax(preds)).sum(1)
    if reduction == "none":
        return loss
    elif reduction == "mean":
        return loss.mean()

在这里,我们将使用之前构建的模块来实现主模型。init 函数是不言自明的。在前向函数中,我们首先将图像和文本分别编码为固定大小的向量(具有不同的维度)。之后,使用单独的投影模块,我们将它们投影到我之前谈到的共享世界(空间)。这里的编码将变得相似的形状(在我们的例子中是 256)。之后我们将计算损失。我再次建议阅读 CLIP 论文以使其更好,但我会尽力解释这部分。

在线性代数中,衡量两个向量是否具有相似特征(它们彼此相似)的一种常见方法是计算它们的点积(将匹配项相乘并取它们的总和);如果最终的数字很大,那么它们是相似的,如果最后的数字很小,那么它们就不相似(相对而言)!

好的!我刚才所说的是理解这个损失函数需要牢记的最重要的事情。我们继续吧。我们讨论了两个向量,但是,我们这里有什么?我们有 image_embeddings,形状为 (batch_size, 256) 的矩阵和形状为 (batch_size, 256) 的 text_embeddings。够简单的!这意味着我们有两组向量而不是两个单个向量。我们如何测量两组向量(两个矩阵)彼此的相似程度?同样,使用点积(在这种情况下,PyTorch 中的 @ 运算符执行点积或矩阵乘法)。为了能够将这两个矩阵相乘,我们转置第二个矩阵。好的,我们得到一个形状为 (batch_size, batch_size) 的矩阵,我们将其称为logits。(在我们的例子中,温度等于 1.0,因此,它没有什么区别。您可以使用它,看看它会产生什么差异。另请参阅论文,了解它为什么在这里!)。

我希望你还在我身边!如果不是也没关系,只需检查代码并检查它们的形状即可。现在我们有了逻辑,我们需要目标。我需要说的是,有一种更直接的方法来获取目标,但我必须为我们的案例这样做(我将在下一段中讨论原因)。

让我们考虑一下我们希望这个模型学习什么:我们希望它学习给定图像和描述它的标题的“相似表示(向量)”。这意味着我们要么给它一个图像,要么给它描述它的文本,我们希望它为两者生成相同的 256 大小的向量。

因此,在最好的情况下,text_embeddings 和 image_embedding 矩阵应该相同,因为它们描述的是相似的事物。现在我们想一下:如果发生这种情况,logits 矩阵会是什么样子?让我们看一个简单的例子!

import torch
from torch import nn
import torch.nn.functional as F
import matplotlib.pyplot as plt

batch_size = 4
dim = 256
embeddings = torch.randn(batch_size, dim)
out = embeddings @ embeddings.T
print(F.softmax(out, dim=-1))

-----------
# tensor([[1., 0., 0., 0.],
#         [0., 1., 0., 0.],
#         [0., 0., 1., 0.],
#         [0., 0., 0., 1.]])

因此,在最好的情况下,logits 将是一个矩阵,如果我们采用其 softmax,对角线中将有 1.0(一个用奇特的词来称呼它的单位矩阵!)。由于损失函数的作用是使模型的预测与目标相似(至少在大多数情况下!),因此我们希望这样的矩阵作为我们的目标。这就是我们在上面的代码块中计算 images_similarity 和 texts_similarity 矩阵的原因。

现在我们已经有了目标矩阵,我们将使用简单的交叉熵来计算实际损失。我已经将交叉熵的完整矩阵形式编写为函数,您可以在代码块的底部看到。好的!我们完了!是不是很简单?!好吧,你可以忽略下一段,但如果你好奇的话,里面有一个重要的注释。

这就是为什么我没有使用更简单的方法:我需要承认在 PyTorch 中有一种更简单的方法来计算这种损失;通过这样做:nn.CrossEntropyLoss()(logits, torch.arange(batch_size))。为什么我这里没有使用它?有两个原因。1- 我们使用的数据集对单个图像有多个标题;因此,批次中可能存在两个具有相似标题的相同图像(这种情况很少见,但可能会发生)。使用这种更简单的方法获取损失将忽略这种可能性,并且模型学会分离实际上相同的两个表示(假设它们不同)。显然,我们不希望这种情况发生,因此我以照顾这些边缘情况的方式计算了整个目标矩阵。2-按照我的方式做,让我更好地理解了这个损失函数中发生的事情;所以,我认为这也会给你更好的直觉!

训练

这是一个训练模型的函数。这里没有发生太多事情;只需加载批次,将它们输入模型并步进优化器和 lr_scheduler。

def train_epoch(model, train_loader, optimizer, lr_scheduler, step):
    loss_meter = AvgMeter()
    tqdm_object = tqdm(train_loader, total=len(train_loader))
    for batch in tqdm_object:
        batch = {k: v.to(CFG.device) for k, v in batch.items() if k != "caption"}
        loss = model(batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if step == "batch":
            lr_scheduler.step()

        count = batch["image"].size(0)
        loss_meter.update(loss.item(), count)

        tqdm_object.set_postfix(train_loss=loss_meter.avg, lr=get_lr(optimizer))
    return loss_meter

好的!我们已经完成了模型的训练。现在,我们需要进行推理,在我们的例子中,将给模型一段文本,并希望它从看不见的验证(或测试)集中检索最相关的图像。

获取图像嵌入

在此函数中,我们加载训练后保存的模型,向其提供验证集中的图像,并返回形状为 (valid_set_size, 256) 的 image_embeddings 和模型本身。

def get_image_embeddings(valid_df, model_path):
    tokenizer = DistilBertTokenizer.from_pretrained(CFG.text_tokenizer)
    valid_loader = build_loaders(valid_df, tokenizer, mode="valid")
    
    model = CLIPModel().to(CFG.device)
    model.load_state_dict(torch.load(model_path, map_location=CFG.device))
    model.eval()
    
    valid_image_embeddings = []
    with torch.no_grad():
        for batch in tqdm(valid_loader):
            image_features = model.image_encoder(batch["image"].to(CFG.device))
            image_embeddings = model.image_projection(image_features)
            valid_image_embeddings.append(image_embeddings)
    return model, torch.cat(valid_image_embeddings)

寻找匹配项

该函数执行我们希望模型能够完成的最终任务:它获取模型、image_embeddings 和文本查询。它将显示验证集中最相关的图像!是不是很神奇呢?让我们看看它到底表现如何!

def find_matches(model, image_embeddings, query, image_filenames, n=9):
    tokenizer = DistilBertTokenizer.from_pretrained(CFG.text_tokenizer)
    encoded_query = tokenizer([query])
    batch = {
        key: torch.tensor(values).to(CFG.device)
        for key, values in encoded_query.items()
    }
    with torch.no_grad():
        text_features = model.text_encoder(
            input_ids=batch["input_ids"], attention_mask=batch["attention_mask"]
        )
        text_embeddings = model.text_projection(text_features)
    
    image_embeddings_n = F.normalize(image_embeddings, p=2, dim=-1)
    text_embeddings_n = F.normalize(text_embeddings, p=2, dim=-1)
    dot_similarity = text_embeddings_n @ image_embeddings_n.T
    
    # multiplying by 5 to consider that there are 5 captions for a single image
    # so in indices, the first 5 indices point to a single image, the second 5 indices
    # to another one and so on.
    values, indices = torch.topk(dot_similarity.squeeze(0), n * 5)
    matches = [image_filenames[idx] for idx in indices[::5]]
    
    _, axes = plt.subplots(math.sqrt(n), math.sqrt(n), figsize=(10, 10))
    for match, ax in zip(matches, axes.flatten()):
        image = cv2.imread(f"{CFG.image_path}/{match}")
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        ax.imshow(image)
        ax.axis("off")
    
    plt.show()

让我们看一些例子!此时,当我看到输出时,我高兴地尖叫起来,并惊讶于该模型实际上正在学习图像和文本之间的这种关系!这种感觉简直难以置信。

find_matches(model, 
             image_embeddings,
             query="one dog sitting on the grass",
             image_filenames=valid_df['image'].values,
             n=9)

这就是我们使用这个函数的方式。结果如下:
在这里插入图片描述
我当时就想:哇!这个模型知道一些东西!当然它并不完美,因为有些图片中有两只狗,但考虑到训练集小和训练时间短,我认为这很棒!

让我们看看其他一些输出。Quert写在每个图像的顶部。
在这里插入图片描述
看!它还可以算数!将此与上一个进行比较。该模型知道“两个”的含义,并提供了有两只狗的图像,与之前的查询形成鲜明对比!这一刻我第二次震惊得尖叫起来:)

文章开头的输出:
在这里插入图片描述
对于下面的示例,模型犯了一些错误,但总的来说,它显然对文本和图像都有很好的理解。
在这里插入图片描述

资源

  • 本文对应的Github代码库

本博文译自Moein Shariatnia的博客

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网络协议与 IP 编址

网络协议与 IP 编址 之前大概了解过了网络的一些基础概念,见文章: 网络基础概念。 之前简单了解OSI模型分层: TCP/IP模型OSI模型TCP/IP对等模型应用层应用层表示层应用层会话层主机到主机层传输层传输层因特网层网络层网络层网络接入层数据链…

iOS17苹果备忘录怎么设置提醒?

在我们快节奏的生活中,苹果备忘录成了记录灵感、任务和重要事项的得力助手,面对着一个让人头疼的问题——备忘录竟然不能设置提醒!突然感觉我的备忘录只是个寂寞的清单,没有提醒的陪伴。 于是,我着手寻找解决之道&…

前后端验证码分析(字母计算)

样式&#xff1a; 前端&#xff1a; login.vue <template> <view class"normal-login-container"> <view class"login-form-content"> <view class"input-item flex align-center"> <view class"iconfont ic…

【EI会议投稿】第四届物联网与智慧城市国际学术会议(IoTSC 2024)

第四届物联网与智慧城市国际学术会议 2024 4th International Conference on Internet of Things and Smart City 继IoTSC前三届的成功举办&#xff0c;第四届物联网与智慧城市国际学术会议&#xff08;IoTSC 2024&#xff09;将于2024年3月22-24日在河南洛阳举办。 智慧城市的…

新版IDEA中,module模块无法被识别,类全部变成咖啡杯无法被识

新版IDEA中&#xff0c;module模块无法被识别&#xff0c;类全部变成咖啡杯无法被识 如下图&#xff1a; 解决方法&#xff1a;java的Directory文件没有被设置为根目录&#xff0c;解决方法如下&#xff1a; 这是方法之一&#xff0c;还有很多的原因 可能的原因&#xff1a; …

深入了解Java Period类,对时间段的精确控制

阅读建议 嗨&#xff0c;伙计&#xff01;刷到这篇文章咱们就是有缘人&#xff0c;在阅读这篇文章前我有一些建议&#xff1a; 本篇文章大概2900多字&#xff0c;预计阅读时间长需要3分钟。本篇文章的实战性、理论性较强&#xff0c;是一篇质量分数较高的技术干货文章&#x…

聚焦数据库Serverless创新,就在2023亚马逊云科技re:Invent

11月28日&#xff0c;亚马逊云科技在其最新的re:Invent 2023大会上宣布了三项重要的serverless创新&#xff0c;这些创新将极大地简化客户在任何规模上分析和管理数据的能力。以下是这些发布的主要要点总结和分析。 Amazon Aurora Limitless Database的新功能&#xff1a; 功能…

骨传导耳机会影响听力么?盘点骨传导耳机的好处与坏处都有哪些?

先说结论&#xff0c;使用骨传导耳机是不会影响听力的&#xff01;并且由于骨传导耳机的特殊传声原理&#xff0c;相比于传统的入耳式耳机&#xff0c;骨传导耳机拥有更多的优点&#xff0c;下面带大家了解一下骨传导耳机的优点和缺点都有哪些。 一、骨传导耳机的优点是什么&a…

分享80个菜单导航JS特效,总有一款适合您

分享80个菜单导航JS特效&#xff0c;总有一款适合您 80个菜单导航JS特效下载链接&#xff1a;https://pan.baidu.com/s/1NgNc759Kg1of_8vR7kaj6A?pwd6666 提取码&#xff1a;6666 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;…

二叉树题目:二叉树的完全性检验

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二叉树的完全性检验 出处&#xff1a;958. 二叉树的完全性检验 难度 5 级 题目描述 要求 给定一个二叉树的根结点 root \texttt{root} root&…

Python更改YOLOv5、v7、v8,实现调用val.py或者test.py后生成pr.csv,然后再整合绘制到一张图上(使用matplotlib绘制)

1. 前提 效果图 不错的链接&#xff1a;YOLOV7训练模型分析 关于map的绘图、loss绘图&#xff0c;可参考&#xff1a;根据YOLOv5、v8、v7训练后生成的result文件用matplotlib进行绘图 v5、v8调用val.py&#xff0c;v7调用test.py&#xff08;作用都是一样的&#xff0c;都是…

LED广告机在密闭箱体内的散热方法

在现代电子产品中&#xff0c;尤其是LED广告机这类大型设备的连续使用过程中&#xff0c;发热问题一直备受关注。以下简要介绍LED广告机在密闭的箱体内如何进行散热降温的有效方法。 1. 设计出风口 LED广告机背面通常设计有镂空矩形&#xff0c;即出风口&#xff0c;以确保空气…

Ubuntu20.04/Linux中常用软件的安装

文章目录 一、安裝与卸载微信二、安裝与卸载QQ三、安装Chrome浏览器并加入apt更新四、安裝VScode4.1 安装常用插件4.2 减小Ipch缓存&#xff1a; 五、安装代码对比工具Meld六、安裝WPS七、安装PDF阅读器Foxit Reader八、安装文献管理软件Zotero九、安装有道云笔记十、安装远程控…

分享66个菜单导航JS特效,总有一款适合您

分享66个菜单导航JS特效&#xff0c;总有一款适合您 66个菜单导航JS特效下载链接&#xff1a;https://pan.baidu.com/s/1dpGGbptx6hEKcBnTMNLIdA?pwd6666 提取码&#xff1a;6666 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;…

mysql有哪些锁,理解各种表锁和行锁

全局锁 主要用于数据库的备份&#xff0c;但会使得备份期间不能有任何事务插入删除更新数据&#xff0c;这很影响实际业务。所以通常不用这个全局锁来完成数据库的备份。假设数据库的存储引擎支持可重复读&#xff0c;那么常见的方法是通过MVCC来实现的&#xff0c;也就是备份…

Shopee过期的折扣活动如何删除?Shopee促销商品如何下架?——站斧浏览器

商家们可以轻松删除虾皮过期活动以及下架促销商品&#xff0c;保持店铺的整洁和顾客的购物体验。那么shopee过期的折扣活动如何删除&#xff0c;shopee促销商品如何下架。 Shopee过期的折扣活动如何删除&#xff1f; 在删除虾皮过期活动时&#xff0c;商家们需要遵循以下步骤…

Java基本数据类型、包装类及拆装箱详解

Java的基本数据类型和对应的包装类是Java语言中处理数据的两个关键概念。基本数据类型提供了简单而高效的方式来存储数据&#xff0c;而包装类使得基本数据类型具有对象的特性。本文将深入探讨基本数据类型与包装类的应用场景及详细描述&#xff0c;并对自动拆箱和装箱的源码实…

SpringBoot:SpringMVC(上)

文章目录 前言一、SpringMVC是什么&#xff1f;1.1 MVC的定义&#xff1a;1.2 MVC 和 Spring MVC 的关系 二、Spring MVC 创建和连接2.1创建springmvc2.2接下来&#xff0c;创建⼀个 UserController 类&#xff0c;实现⽤户到 Spring 程序的互联互通&#xff0c;具体实现代码如…

Python 自动化办公:文件快速整理分类

平时桌面或文件夹内鱼龙混杂&#xff0c;各种类型的文件都有怎么办&#xff1f; 本篇文章中&#xff0c;我们将学习如何使用 Python 编写一个文件整理分类的脚本。 该脚本能够自动获取文件类型&#xff0c;并将文件按照类型整理到不同的子文件夹中。 先看下效果&#xff0c;…

低代码如何降低门槛、快速交付、实现可持续IT架构?

目录 低代码开发模式期望达成的目标 1.降低开发门槛 2.加快系统交付 3.建立可持续发展的IT架构 写在最后 低代码的概念&#xff0c;最早提出的时间是在2014年左右&#xff0c;随后一直处于上升期&#xff0c;随着前两年阿里、腾讯的相继入场&#xff0c;竞争逐步加大。低代…
最新文章