Sentence-BERT实现文本匹配【对比损失函数】

news2024/12/23 22:48:06

引言

还是基于Sentence-BERT架构,或者说Bi-Encoder架构,但是本文使用的是参考2中提出的对比损失函数。

架构

image-20210923000654664

如上图,计算两个句嵌入 u \pmb u u v \pmb v v​之间的距离(1-余弦相似度),然后使用参考2中提出的对比损失函数作为目标函数:
L = y × 1 2 ( distance ( u , v ) ) 2 + ( 1 − y ) × 1 2 { max ⁡ ( 0 , m − distance ( u , v ) ) } 2 \mathcal L= y\times \frac{1}{2} (\text{distance}(\pmb u,\pmb v))^2 + (1-y)\times \frac{1}{2} \{ \max(0, m - \text{distance}(\pmb u,\pmb v)) \}^2\\ L=y×21(distance(u,v))2+(1y)×21{max(0,mdistance(u,v))}2
这里的 y y y是真实标签,相似为1,不相似为0; m m m​表示margin(间隔值),默认为0.5。

这里 m m m的意思是,如果 u \pmb u u v \pmb v v不相似( y = 0 y=0 y=0),那么它们之间的距离只要足够大,大于等于间隔值0.5就好了。假设距离为0.6,那么 max ⁡ ( 0 , 0.5 − 0.6 ) = 0 \max(0,0.5-0.6)=0 max(0,0.50.6)=0,如果距离不够大( 0.2 0.2 0.2),那么 max ⁡ ( 0 , 0.5 − 0.2 ) = 0.3 \max(0,0.5-0.2)=0.3 max(0,0.50.2)=0.3,就会产生损失值。

整个公式的目的是拉近相似的文本对,推远不相似的文本对到一定程度就可以了。实现的时候 max ⁡ \max max可以用relu来表示。

实现

实现采用类似Huggingface的形式,每个文件夹下面有一种模型。分为modelingargumentstrainer等不同的文件。不同的架构放置在不同的文件夹内。

modeling.py:

from dataclasses import dataclass

import torch
from torch import Tensor, nn

from transformers.file_utils import ModelOutput

from transformers import (
    AutoModel,
    AutoTokenizer,
)

import numpy as np
from tqdm.autonotebook import trange
from typing import Optional

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

# 定义了三种距离函数
# 余弦相似度值越小表示越不相似,1减去它就变成了距离函数,越小(余弦越接近1)表示越相似。
class SiameseDistanceMetric(Enum):
    """The metric for the contrastive loss"""

    EUCLIDEAN = lambda x, y: F.pairwise_distance(x, y, p=2)
    MANHATTAN = lambda x, y: F.pairwise_distance(x, y, p=1)
    COSINE_DISTANCE = lambda x, y: 1 - F.cosine_similarity(x, y)


@dataclass
class BiOutput(ModelOutput):
    loss: Optional[Tensor] = None
    scores: Optional[Tensor] = None


class SentenceBert(nn.Module):
    def __init__(
        self,
        model_name: str,
        trust_remote_code: bool = True,
        max_length: int = None,
        margin: float = 0.5,
        distance_metric=SiameseDistanceMetric.COSINE_DISTANCE,
        pooling_mode: str = "mean",
        normalize_embeddings: bool = False,
    ) -> None:
        super().__init__()
        self.model_name = model_name
        self.normalize_embeddings = normalize_embeddings

        self.device = "cuda" if torch.cuda.is_available() else "cpu"

        self.tokenizer = AutoTokenizer.from_pretrained(
            model_name, trust_remote_code=trust_remote_code
        )
        self.model = AutoModel.from_pretrained(
            model_name, trust_remote_code=trust_remote_code
        ).to(self.device)

        self.max_length = max_length
        self.pooling_mode = pooling_mode

        self.distance_metric = distance_metric
        self.margin = margin

    def sentence_embedding(self, last_hidden_state, attention_mask):
        if self.pooling_mode == "mean":
            attention_mask = attention_mask.unsqueeze(-1).float()
            return torch.sum(last_hidden_state * attention_mask, dim=1) / torch.clamp(
                attention_mask.sum(1), min=1e-9
            )
        else:
            # cls
            return last_hidden_state[:, 0]

    def encode(
        self,
        sentences: str | list[str],
        batch_size: int = 64,
        convert_to_tensor: bool = True,
        show_progress_bar: bool = False,
    ):
        if isinstance(sentences, str):
            sentences = [sentences]

        all_embeddings = []

        for start_index in trange(
            0, len(sentences), batch_size, desc="Batches", disable=not show_progress_bar
        ):
            batch = sentences[start_index : start_index + batch_size]

            features = self.tokenizer(
                batch,
                padding=True,
                truncation=True,
                return_tensors="pt",
                return_attention_mask=True,
                max_length=self.max_length,
            ).to(self.device)

            out_features = self.model(**features, return_dict=True)
            embeddings = self.sentence_embedding(
                out_features.last_hidden_state, features["attention_mask"]
            )
            if not self.training:
                embeddings = embeddings.detach()

            if self.normalize_embeddings:
                embeddings = torch.nn.functional.normalize(embeddings, p=2, dim=1)

            if not convert_to_tensor:
                embeddings = embeddings.cpu()

            all_embeddings.extend(embeddings)

        if convert_to_tensor:
            all_embeddings = torch.stack(all_embeddings)
        else:
            all_embeddings = np.asarray([emb.numpy() for emb in all_embeddings])

        return all_embeddings

    def compute_loss(self, source_embed, target_embed, labels):
        labels = torch.tensor(labels).float().to(self.device)
		# 计算距离
        distances = self.distance_metric(source_embed, target_embed)
		# 实现损失函数
        loss = 0.5 * (
            labels * distances.pow(2)
            + (1 - labels) * F.relu(self.margin - distances).pow(2)
        )
        return loss.mean()

    def forward(self, source, target, labels) -> BiOutput:
        """
        Args:
            source :
            target :
        """
        source_embed = self.encode(source)
        target_embed = self.encode(target)

        loss = self.compute_loss(source_embed, target_embed, labels)
        return BiOutput(loss, None)

    def save_pretrained(self, output_dir: str):
        state_dict = self.model.state_dict()
        state_dict = type(state_dict)(
            {k: v.clone().cpu().contiguous() for k, v in state_dict.items()}
        )
        self.model.save_pretrained(output_dir, state_dict=state_dict)

整个模型的实现放到modeling.py文件中。

arguments.py:

from dataclasses import dataclass, field
from typing import Optional

import os


@dataclass
class ModelArguments:
    model_name_or_path: str = field(
        metadata={
            "help": "Path to pretrained model"
        }
    )
    config_name: Optional[str] = field(
        default=None,
        metadata={
            "help": "Pretrained config name or path if not the same as model_name"
        },
    )
    tokenizer_name: Optional[str] = field(
        default=None,
        metadata={
            "help": "Pretrained tokenizer name or path if not the same as model_name"
        },
    )


@dataclass
class DataArguments:
    train_data_path: str = field(
        default=None, metadata={"help": "Path to train corpus"}
    )
    eval_data_path: str = field(default=None, metadata={"help": "Path to eval corpus"})
    max_length: int = field(
        default=512,
        metadata={
            "help": "The maximum total input sequence length after tokenization for input text."
        },
    )

    def __post_init__(self):
        if not os.path.exists(self.train_data_path):
            raise FileNotFoundError(
                f"cannot find file: {self.train_data_path}, please set a true path"
            )
        
        if not os.path.exists(self.eval_data_path):
            raise FileNotFoundError(
                f"cannot find file: {self.eval_data_path}, please set a true path"
            )

定义了模型和数据相关参数。

dataset.py:

from torch.utils.data import Dataset
from datasets import Dataset as dt
import pandas as pd

from utils import build_dataframe_from_csv


class PairDataset(Dataset):
    def __init__(self, data_path: str) -> None:

        df = build_dataframe_from_csv(data_path)
        self.dataset = dt.from_pandas(df, split="train")

        self.total_len = len(self.dataset)

    def __len__(self):
        return self.total_len

    def __getitem__(self, index) -> dict[str, str]:
        query1 = self.dataset[index]["query1"]
        query2 = self.dataset[index]["query2"]
        label = self.dataset[index]["label"]
        return {"query1": query1, "query2": query2, "label": label}


class PairCollator:
    def __call__(self, features) -> dict[str, list[str]]:
        queries1 = []
        queries2 = []
        labels = []

        for feature in features:
            queries1.append(feature["query1"])
            queries2.append(feature["query2"])
            labels.append(feature["label"])

        return {"source": queries1, "target": queries2, "labels": labels}

数据集类考虑了LCQMC数据集的格式,即成对的语句和一个数值标签。类似:

Hello.	Hi.	1
Nice to see you.	Nice	0

trainer.py:

import torch
from transformers.trainer import Trainer

from typing import Optional
import os
import logging

from modeling import SentenceBert

TRAINING_ARGS_NAME = "training_args.bin"
logger = logging.getLogger(__name__)


class BiTrainer(Trainer):

    def compute_loss(self, model: SentenceBert, inputs, return_outputs=False):
        outputs = model(**inputs)
        loss = outputs.loss

        return (loss, outputs) if return_outputs else loss

    def _save(self, output_dir: Optional[str] = None, state_dict=None):
        # If we are executing this function, we are the process zero, so we don't check for that.
        output_dir = output_dir if output_dir is not None else self.args.output_dir
        os.makedirs(output_dir, exist_ok=True)
        logger.info(f"Saving model checkpoint to {output_dir}")

        self.model.save_pretrained(output_dir)

        if self.tokenizer is not None:
            self.tokenizer.save_pretrained(output_dir)

        # Good practice: save your training arguments together with the trained model
        torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))

继承🤗 Transformers的Trainer类,重写了compute_loss_save方法。

这样我们就可以利用🤗 Transformers来训练我们的模型了。

utils.py:

import torch
import pandas as pd
from scipy.stats import pearsonr, spearmanr
from typing import Tuple


def build_dataframe_from_csv(dataset_csv: str) -> pd.DataFrame:
    df = pd.read_csv(
        dataset_csv,
        sep="\t",
        header=None,
        names=["query1", "query2", "label"],
    )

    return df


def compute_spearmanr(x, y):
    return spearmanr(x, y).correlation


def compute_pearsonr(x, y):
    return pearsonr(x, y)[0]


def find_best_acc_and_threshold(scores, labels, high_score_more_similar: bool):
    """Copied from https://github.com/UKPLab/sentence-transformers/tree/master"""
    assert len(scores) == len(labels)
    rows = list(zip(scores, labels))

    rows = sorted(rows, key=lambda x: x[0], reverse=high_score_more_similar)
    print(rows)

    max_acc = 0
    best_threshold = -1
    # positive examples number so far
    positive_so_far = 0
    # remain negative examples
    remaining_negatives = sum(labels == 0)

    for i in range(len(rows) - 1):
        score, label = rows[i]
        if label == 1:
            positive_so_far += 1
        else:
            remaining_negatives -= 1

        acc = (positive_so_far + remaining_negatives) / len(labels)
        if acc > max_acc:
            max_acc = acc
            best_threshold = (rows[i][0] + rows[i + 1][0]) / 2

    return max_acc, best_threshold


def metrics(y: torch.Tensor, y_pred: torch.Tensor) -> Tuple[float, float, float, float]:
    TP = ((y_pred == 1) & (y == 1)).sum().float()  # True Positive
    TN = ((y_pred == 0) & (y == 0)).sum().float()  # True Negative
    FN = ((y_pred == 0) & (y == 1)).sum().float()  # False Negatvie
    FP = ((y_pred == 1) & (y == 0)).sum().float()  # False Positive
    p = TP / (TP + FP).clamp(min=1e-8)  # Precision
    r = TP / (TP + FN).clamp(min=1e-8)  # Recall
    F1 = 2 * r * p / (r + p).clamp(min=1e-8)  # F1 score
    acc = (TP + TN) / (TP + TN + FP + FN).clamp(min=1e-8)  # Accurary
    return acc, p, r, F1


def compute_metrics(predicts, labels):
    return metrics(labels, predicts)

定义了一些帮助函数,从sentence-transformers库中拷贝了寻找最佳准确率阈值的实现find_best_acc_and_threshold

除了准确率,还计算了句嵌入的余弦相似度与真实标签之间的斯皮尔曼等级相关系数指标。

最后定义训练和测试脚本。

train.py:

from transformers import set_seed, HfArgumentParser, TrainingArguments

import logging
from pathlib import Path

from datetime import datetime

from modeling import SentenceBert
from trainer import BiTrainer
from arguments import DataArguments, ModelArguments
from dataset import PairCollator, PairDataset

logger = logging.getLogger(__name__)
logging.basicConfig(
    format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
    datefmt="%m/%d/%Y %H:%M:%S",
    level=logging.INFO,
)


def main():
    parser = HfArgumentParser((TrainingArguments, DataArguments, ModelArguments))
    training_args, data_args, model_args = parser.parse_args_into_dataclasses()
	# 根据当前时间生成输出目录
    output_dir = f"{training_args.output_dir}/{model_args.model_name_or_path.replace('/', '-')}-{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}"
    training_args.output_dir = output_dir

    logger.info(f"Training parameters {training_args}")
    logger.info(f"Data parameters {data_args}")
    logger.info(f"Model parameters {model_args}")
	# 设置随机种子
    set_seed(training_args.seed)
	# 加载预训练模型
    model = SentenceBert(
        model_args.model_name_or_path,
        trust_remote_code=True,
        max_length=data_args.max_length,
    )
	
    tokenizer = model.tokenizer
	# 构建训练和测试集
    train_dataset = PairDataset(data_args.train_data_path)
    eval_dataset = PairDataset(data_args.eval_data_path)
	# 传入参数
    trainer = BiTrainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        data_collator=PairCollator(),
        tokenizer=tokenizer,
    )
    Path(training_args.output_dir).mkdir(parents=True, exist_ok=True)
	# 开始训练
    trainer.train()
    trainer.save_model()


if __name__ == "__main__":
    main()

训练

基于train.py定义了train.sh传入相关参数:

timestamp=$(date +%Y%m%d%H%M)
logfile="train_${timestamp}.log"

# change CUDA_VISIBLE_DEVICES
CUDA_VISIBLE_DEVICES=3 nohup python train.py \
    --model_name_or_path=hfl/chinese-macbert-large \
    --output_dir=output \
    --train_data_path=data/train.txt \
    --eval_data_path=data/dev.txt \
    --num_train_epochs=3 \
    --save_total_limit=5 \
    --learning_rate=2e-5 \
    --weight_decay=0.01 \
    --warmup_ratio=0.01 \
    --bf16=True \
    --eval_strategy=epoch \
    --save_strategy=epoch \
    --per_device_train_batch_size=64 \
    --report_to="none" \
    --remove_unused_columns=False \
    --max_length=128 \
    > "$logfile" 2>&1 &


以上参数根据个人环境修改,这里使用的是哈工大的chinese-macbert-large预训练模型。

注意:

  • --remove_unused_columns是必须的。
  • 通过bf16=True可以加速训练同时不影响效果。
  • 其他参数可以自己调整。
100%|██████████| 11193/11193 [47:15<00:00,  4.26it/09/03/2024 18:35:18 - INFO - trainer - Saving model checkpoint to output/hfl-chinese-macbert-large-2024-09-03_17-47-58/checkpoint-11193
100%|██████████| 11193/11193 [47:29<00:00,  3.93it/s]
09/03/2024 18:35:32 - INFO - trainer - Saving model checkpoint to output/hfl-chinese-macbert-large-2024-09-03_17-47-58
{'eval_loss': 0.010313387028872967, 'eval_runtime': 58.5945, 'eval_samples_per_second': 150.219, 'eval_steps_per_second': 18.79, 'epoch': 3.0}
{'train_runtime': 2849.8189, 'train_samples_per_second': 251.349, 'train_steps_per_second': 3.928, 'train_loss': 0.006717115574075615, 'epoch': 3.0}

这里仅训练了3轮,我们拿最后保存的模型output/hfl-chinese-macbert-large-2024-09-03_17-47-58进行测试。

测试

test.py: 测试脚本见后文的完整代码。

test.sh:

# change CUDA_VISIBLE_DEVICES
CUDA_VISIBLE_DEVICES=0 python test.py \
    --model_name_or_path=output/hfl-chinese-macbert-large-2024-09-03_17-47-58 \
    --test_data_path=data/test.txt
  

输出:

TestArguments(model_name_or_path='output/hfl-chinese-macbert-large-2024-09-03_17-47-58/checkpoint-11193', test_data_path='data/test.txt', max_length=64, batch_size=128)
Batches: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:11<00:00,  8.76it/s]
Batches: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:11<00:00,  8.85it/s]
max_acc: 0.8944, best_threshold: 0.899751
spearman corr: 0.7950 |  pearson_corr corr: 0.7434 | compute time: 22.30s
accuracy=0.894 precision=0.899 recal=0.889 f1 score=0.8938

测试集上的准确率达到89.4%,达到了目前本系列文章的SOTA结果。spearman系数也是最佳的。

这是默认基于余弦距离训练的,修改成欧几里得距离(EUCLIDEAN),其他参数不变的情况下得到结果:

TestArguments(model_name_or_path='output/hfl-chinese-macbert-large-2024-09-03_17-50-30/checkpoint-11193', test_data_path='data/test.txt', max_length=64, batch_size=128)
Batches: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:11<00:00,  8.79it/s]
Batches: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:11<00:00,  8.85it/s]
max_acc: 0.8881, best_threshold: 0.999987
spearman corr: 0.7795 |  pearson_corr corr: 0.5701 | compute time: 22.26s
accuracy=0.888 precision=0.890 recal=0.885 f1 score=0.8876

准确率是88.8%,这倒没什么, 主要是最佳阈值0.999987太不合理了。

完整代码

完整代码: →点此←

本文代码是和某次提交相关的,Master分支上的代码随时可能会被优化。

参考

  1. [论文笔记]Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks
  2. [论文笔记]Dimensionality Reduction by Learning an Invariant Mapping

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

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

相关文章

docker 安装 rabbitmq

参考文档&#xff1a; https://hub.docker.com/_/rabbitmq/ https://www.rabbitmq.com/docs/download https://www.kuangstudy.com/zl/rabbitmq#1366643532940484610 执行下面的命令 docker run -d -it --name myrabbit -e RABBITMQ_DEFAULT_USERadmin -e RABBITMQ_DEFAULT_PA…

“解决 Docker 启动失败:排查和修复 overlay2 存储驱动与网络模块问题”。

目录 1.报错如下 2.报错详解 1. ” 表明 overlay2 存储驱动挂载失败&#xff0c;找不到相应设备。 2.表明在路径中找不到 fuse-overlayfs 可执行文件。 3.表明加载 bridge 和 br_netfilter 模块失败。 4.及后续一系列关于停止服务的信息&#xff0c;是由于前面的错误导致的…

硬件生产厂家运维系统思路

当前硬件生产厂家运维已经逐渐摆脱原有的现场调试&#xff0c;初步诊断和运维已经进化为远程运维&#xff1b;主要方式为厂家建立运维系统&#xff0c;使用人员只需要关注厂家公众号或者登录官网&#xff0c;即可完成原来必须到现场才能解决的问题&#xff1b; 原弊端&#xff…

探讨 | 大模型在传统NLP任务的使用姿势

写在前面 今天给大家带来一篇震宇兄&#xff08;知乎邱震宇&#xff09;探讨大模型技术在提升传统NLP类任务效果上的应用方式的文章&#xff0c;主要从文本分类任务出发。 知乎&#xff1a;https://zhuanlan.zhihu.com/p/704983302PS&#xff1a;长文警告&#xff01;建议收藏…

Burp Suite Professional 2024.8 发布下载,新增功能概览

Burp Suite Professional 2024.8 (macOS, Linux, Windows) - Web 应用安全、测试和扫描 Burp Suite Professional, Test, find, and exploit vulnerabilities. 请访问原文链接&#xff1a;https://sysin.org/blog/burp-suite-pro/&#xff0c;查看最新版。原创作品&#xff0…

退火吗?C#/WinForm演示退火算法

退火模型&#xff1a;模拟退火算法&#xff08;Simulated Annealing, SA&#xff09;是一种概率型全局优化算法&#xff0c;灵感来源于物理学中的退火过程。它通过模拟金属退火过程中的加热和缓慢冷却&#xff0c;来寻找问题的近似全局最优解。算法开始时&#xff0c;初始温度设…

70万个哺乳动物功能基因集!这个数据库值得重视

生信碱移 Rummagene数据库 Rummagene 从 PubMed Central (PMC) 出版物提取了超70万个基因集&#xff0c;用于各类基因功能关联注释。 组学技术的引入逐渐将生物和生物医学研究从研究单个基因和蛋白质转向研究基因集、基因簇、分子复合物和基因表达模块。许多生物医学和生物研究…

log4j 清除MDC上下文 MDC分类日志

在项目里需要分类收集处理日志信息&#xff0c;使用 log4j的MDC在线程中添加分类信息。不过最近却出现日志信息记录错误的情况&#xff0c;具体来说&#xff0c;就是会出现本来是属于下一个分类的一部分信息莫名的记录到上一个分类的日志文件中了。这很显然是MDC信息错误造成的…

【nnUNet】环境安装

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ&#xff1a;870202403 公众号&#xff1a;VTK忠粉 前言 本文分享医疗分割模型nnUNet的环境安装教程&#xff0c;希望对各位小伙伴有所帮助&#xff01; 感谢各位小伙伴的点赞关注&#xff0c;小易会继续努力分享&#xff0…

数据中台 | 数据智能平台产品系列文章,企业开发和盘活数据资产的利器!(上篇)

引言 发展数字经济&#xff0c;实现数字中国是我国的国家战略&#xff0c;坚定且不可动摇&#xff0c;近期随着《数据二十条》、《“数据要素”三年行动计划》、《关于加强数据资产管理的指导意见》等重磅政策的发布&#xff0c;使数字化转型成为越来越多企业增强竞争、扩大营收…

LabVIEW程序员错误排查思路

当LabVIEW程序员在开发过程中遇到难以解决的错误且网上搜不到答案时&#xff0c;需要采取系统性的方法进行排查和解决。这包括回顾代码逻辑、深入理解LabVIEW的底层机制、参考专业文献和求助社区等方式。下面将从多角度详细解读专业程序员在面对这种困境时的应对策略&#xff0…

【系统分析师】-面向对象方法

目录 1、基本概念 2、UML 2.1、基本结构 2.1.1.构造块 2.1.1.1、事物 2.1.1.2、关系 2.1.1.3、图形 2.1.2.规则 2.1.3.公共机制 2.2、41视图 3、面向对象分析OOA 3.1、用例模型 3.2、分析模型 4、面向对象设计OOD 4.1、细分 4.2、设计原则 5、面向对象的程序设…

一字线模组厂家的选择与使用技巧

在当今自动化与智能制造的浪潮中&#xff0c;一字线模组作为精密定位与传输的核心部件&#xff0c;其性能与质量直接关系到整个生产线的效率与稳定性。因此&#xff0c;选择合适的一字线模组厂家并掌握其使用技巧&#xff0c;对于提升企业竞争力至关重要。接下来我们跟着鑫优威…

四、配置三层交换实验组网

一、实验拓扑 二、实验目的 通过配置交换机&#xff0c;令不同vlan间的主机能够互相通信 三、实验步骤 SW12 <Huawei>undo terminal monitor Info: Current terminal monitor is off. <Huawei>system-view Enter system view, return user view with CtrlZ. [H…

Spring 学习笔记

概述 Spring 是一个企业级 J2EE 应用开发一站式解决方案&#xff0c;其提供的功能贯穿了项目开发的表现层、业务层和持久化层&#xff0c;同时&#xff0c;Spring 可以和其他应用框架无缝整合 Spring 的特性包括以下几个方面&#xff1a; 轻量&#xff1a;Spring 是一个轻量…

CISAW软件安全开发模型的核心思想

在软件安全开发的领域中&#xff0c;采用的模型与信息安全保障体系紧密相连&#xff0c;它们共同构成了一个坚实的防御机制。 这种模型不仅注重程序、数据和文档三个实体对象的全生命周期管理&#xff0c;还强调实现这些对象的可用性、完整性、真实性、机密性和不可否认性等关…

Cesium 展示——绘制等值线图

文章目录 需求分析资料需求 分析 首先需要开启地形数据 开启方式见:地形开启否则会遇到以下问题直接上实例代码export function getPoints

mysql 8.0 的 建表 和八种 建表引擎实例

文章目录 MySQL 8.0 中&#xff0c;主要有以下八种常见的建表引擎一、InnoDB 引擎建表注意点建表知识点 二、MyISAM 引擎建表使用场景 三、Memory 引擎使用场景 四、Archive 引擎五、BLACKHOLE 引擎一、特点二、适用场景三、注意事项 六、MRG_MyISAM 引擎MRG_MyISAM 和 MyISAM …

Echarts可视化

echarts是一个基于javascripts的开源可视化图表库 画图步骤&#xff1a; 1.引入echarts.js文件 <script src" https://cdn.jsdelivr.net/npm/echarts5.5.1/dist/echarts.min.js"></script> 也可将文件下载到本地通过src引入。 2. 准备一个呈现图表的…

spring boot3框架@Validated失效

项目中使用的springboot3.2.1,在使用Validated校验controller里参数时始终不生效&#xff1b;在网上查了相关资料&#xff0c;添加了spring-boot-starter-validation依赖但还是不行 经过层层调试&#xff0c;终于发现问题&#xff1b; springboot3添加Validated后校验的是 ja…