Cross-Encoder实现文本匹配(重排序模型)

news2024/9/21 16:49:17

引言

前面几篇文章都是基于表示型的方法训练BERT进行文本匹配,而本文是以交互型的方法。具体来说,将待匹配的两个句子拼接成一个输入喂给BERT模型,最后让其输出一个相似性得分。

文本匹配系列文章先更新到此,目前为止都是基于监督学习Sentence Pair的方式,后续有时间继续更新对比学习三元组(anchor, positive, negative)的方式和无监督学习的方式。

架构

image-20231018135433256

Cross-Encoder会利用自注意力机制不断计算这两个句子之间的交互(注意力),最后接一个分类器输出一个分数(logits)代表相似度(可以经过sigmoid变成一个概率)。

实现

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

modeling.py:

import torch
from torch import nn
import numpy as np

from tqdm import tqdm

from transformers import (
    AutoTokenizer,
    AutoConfig,
    AutoModelForSequenceClassification,
)
from torch.utils.data import DataLoader
from transformers.modeling_outputs import SequenceClassifierOutput
from transformers.tokenization_utils_base import BatchEncoding


import logging

logger = logging.getLogger(__name__)


class SentenceBert(nn.Module):
    def __init__(
        self,
        model_name: str,
        max_length: int = None,
        trust_remote_code: bool = True,
    ) -> None:
        super().__init__()
        self.config = AutoConfig.from_pretrained(
            model_name, trust_remote_code=trust_remote_code
        )
        self.config.num_labels = 1

        # reranker
        self.model = AutoModelForSequenceClassification.from_pretrained(
            model_name, config=self.config, trust_remote_code=trust_remote_code
        )
        self.tokenizer = AutoTokenizer.from_pretrained(
            model_name, trust_remote_code=trust_remote_code
        )

        self.max_length = max_length

        self.loss_fct = nn.BCEWithLogitsLoss()

    def batching_collate(self, batch: list[tuple[str, str]]) -> BatchEncoding:
        texts = [[] for _ in range(len(batch[0]))]

        for example in batch:
            for idx, text in enumerate(example):
                texts[idx].append(text.strip())

        tokenized = self.tokenizer(
            *texts,
            padding=True,
            truncation="longest_first",
            return_tensors="pt",
            max_length=self.max_length
        ).to(self.model.device)

        return tokenized

    def predict(
        self,
        sentences: list[tuple[str, str]],
        batch_size: int = 64,
        convert_to_tensor: bool = True,
        show_progress_bar: bool = False,
    ):
        dataloader = DataLoader(
            sentences,
            batch_size=batch_size,
            collate_fn=self.batching_collate,
            shuffle=False,
        )

        preds = []

        for batch in tqdm(
            dataloader, disable=not show_progress_bar, desc="Running Inference"
        ):
            with torch.no_grad():
                logits = self.model(**batch).logits
                logits = torch.sigmoid(logits)

                preds.extend(logits)

        if convert_to_tensor:
            preds = torch.stack(preds)
        else:
            preds = np.asarray([pred.cpu().detach().float().numpy() for pred in preds])

        return preds

    def forward(self, inputs, labels=None):

        outputs = self.model(**inputs, return_dict=True)

        if labels is not None:
            labels = labels.float()

            logits = outputs.logits
            logits = logits.view(-1)

            loss = self.loss_fct(logits, labels)

            return SequenceClassifierOutput(loss, **outputs)

        return outputs

    def save_pretrained(self, output_dir: str) -> None:
        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文件中。

这里首先设置类别数为1num_labels = 1;然后通过AutoModelForSequenceClassification增加一个序列分类器头,该分类器头核心代码为:

class BertForSequenceClassification(BertPreTrainedModel):
    def __init__(self, config):
        super().__init__(config)
        self.num_labels = config.num_labels
        self.config = config
		# 实例化BERT模型
        self.bert = BertModel(config)
  
		# 增加一个线性层,从hidden_size映射为num_labels维度,这里是1
        self.classifier = nn.Linear(config.hidden_size, config.num_labels)

    

    def forward(
        self,
        input_ids: Optional[torch.Tensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        token_type_ids: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.Tensor] = None,
        head_mask: Optional[torch.Tensor] = None,
        inputs_embeds: Optional[torch.Tensor] = None,
        labels: Optional[torch.Tensor] = None,
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
    ) -> Union[Tuple[torch.Tensor], SequenceClassifierOutput]:
       
		# 先得到bert模型的输出
        outputs = self.bert(
            input_ids,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids,
            position_ids=position_ids,
            head_mask=head_mask,
            inputs_embeds=inputs_embeds,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
        )
		# 实际上是cls 标记对应的表示
        pooled_output = outputs[1]
		# 得到一个一维的logits
        logits = self.classifier(pooled_output)

BERT模型中所谓的pooled_output实际上是:

class BertPooler(nn.Module):
    def __init__(self, config):
        super().__init__()
        # 从hidden_size空间映射到另一个hidden_size空间
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        # 经过tanh激活函数
        self.activation = nn.Tanh()

    def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
        # 取最后一层隐藏状态第一个token: [cls]
        first_token_tensor = hidden_states[:, 0]
        pooled_output = self.dense(first_token_tensor)
        pooled_output = self.activation(pooled_output)
        return pooled_output

回到我们的modeling.py,训练时利用forward方法;推理时利用predict方法,支持批处理。输入是表示语句对的元组。

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 or model identifier from huggingface"
        }
    )
    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 transformers import PreTrainedTokenizer, DataCollatorWithPadding

from datasets import Dataset as dt

from typing import Any

from utils import build_dataframe_from_csv


class PairDataset(Dataset):
    def __init__(
        self, data_path: str, tokenizer: PreTrainedTokenizer, max_len: int
    ) -> None:

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

        self.total_len = len(self.dataset)
        self.tokenizer = tokenizer

        self.max_len = max_len

    def __len__(self):
        return self.total_len

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

        encoding = self.tokenizer.encode_plus(
            query1,
            query2,
            truncation="only_second",
            max_length=self.max_len,
            padding=False,
        )

        encoding["label"] = label

        return encoding

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

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

这里数据集的处理和之前的有所不同,主要是调用encode_plus将文本对拼接在一起,并且仅阶段第二个文本。

这里没有进行padding,交给DataCollatorWithPadding来做。

trainer.py:

import torch
from transformers.trainer import Trainer

from typing import Optional
import os
import logging

TRAINING_ARGS_NAME = "training_args.bin"


from modeling import SentenceBert

logger = logging.getLogger(__name__)


class CrossTrainer(Trainer):

    def compute_loss(self, model: SentenceBert, inputs, return_outputs=False):
        labels = inputs.pop("labels")

        return model(inputs, labels)["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)

    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,
    DataCollatorWithPadding,
)


import logging
import os
from pathlib import Path

from datetime import datetime

from modeling import SentenceBert
from trainer import CrossTrainer
from arguments import DataArguments, ModelArguments
from dataset import 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,
        max_length=data_args.max_length,
        trust_remote_code=True,
    )

    tokenizer = model.tokenizer

    train_dataset = PairDataset(
        data_args.train_data_path,
        tokenizer,
        data_args.max_length,
    )

    eval_dataset = PairDataset(
        data_args.eval_data_path,
        tokenizer,
        data_args.max_length,
    )

    trainer = CrossTrainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        data_collator=DataCollatorWithPadding(tokenizer),
        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=1 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 \
    --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预训练模型。

注意:

  • 通过bf16=True可以加速训练同时不影响效果,不支持可以尝试fp16
  • 其他参数可以自己调整。
100%|██████████| 18655/18655 [1:15:47<00:00,  5.06it/s]
100%|██████████| 18655/18655 [1:15:47<00:00,  4.10it/s]
{'loss': 0.0464, 'grad_norm': 4.171152591705322, 'learning_rate': 1.6785791639592811e-07, 'epoch': 4.96}
{'train_runtime': 4547.2543, 'train_samples_per_second': 262.539, 'train_steps_per_second': 4.102, 'train_loss': 0.11396670312096753, 'epoch': 5.0}

这里训练了5轮,为了测试效果,但发现实际上3轮的结果还好一些,因此最终拿它来测试。

测试

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

test.sh:

# change CUDA_VISIBLE_DEVICES
CUDA_VISIBLE_DEVICES=0 python test.py \
    --model_name_or_path=output/checkpoint-11193 \
    --test_data_path=data/test.txt

输出:

TestArguments(model_name_or_path='output/checkpoint-11193', test_data_path='data/test.txt', max_length=64, batch_size=128)
Running Inference: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:19<00:00,  5.05it/s]
max_acc: 0.8954, best_threshold: 0.924443
spearman corr: 0.7996 |  pearson_corr corr: 0.8021 | compute time: 19.44s
accuracy=0.895 precision=0.911 recal=0.876 f1 score=0.8934

测试集上的准确率达到89.5%,spearman系数达到79.96,这两个指标都是本系列文章的SOTA结果,但是没有期望的那么高。可能一般用cross-encoder 模型做精排,选出top-k啥的。

下面是近期几种训练方法的一个对比:

模型(目标函数)准确率(%)spearman(*100)pearson(*100)
Bi-Encoder(Classifier)89.1879.8275.14
Bi-Encoder(Regression)88.3277.9576.68
Bi-Encoder(Contrastive)88.8177.9557.01
Bi-Encoder(CoSENT)89.4079.8977.03
Cross-Encoder89.5479.9680.21

完整代码

完整代码: →点此←

参考

  1. [论文笔记]Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks

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

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

相关文章

AI大模型行业专题报告:大模型发展迈入爆发期,开启AI新纪元

大规模语言模型&#xff08;Large Language Models&#xff0c;LLM&#xff09;泛指具有超大规模参数或者经过超大规模数据训练所得到的语言模型。与传统语言模型相比&#xff0c;大语言模型的构建过程涉及到更为复杂的训练方法&#xff0c;进而展现出了强大的自然语言理解能力…

网络药理学:1、文章基本思路、推荐参考文献、推荐视频

文章基本思路 选择一味中药或者中药复方&#xff08;常见的都是选择一味中药&#xff0c;如&#xff1a;大黄、银柴胡等&#xff09;&#xff0c;同时选择一个要研究的疾病&#xff08;如食管癌等&#xff09;获得中药的主要化学成分或者说活性成分&#xff08;有时候也以化合…

一款专为网络专业人士设计的多功能扫描工具,支持主机存活探测、端口扫描、服务爆破、漏洞扫描等功能

前言 在网络维护和安全检测中&#xff0c;有效的工具对于提高-效率至关重要。传统的网络扫描工具往往功能单一&#xff0c;需要多个工具配合使用&#xff0c;这不仅增加了工作的复杂度&#xff0c;还可能因为工具间的兼容性问题导致工作效率下降。面对这样的挑战&#xff0c;我…

深度解析 | 二元Logistic回归模型(单因素筛查、软件操作及结果解读)

一、Logistic回归的类型 Logistic回归&#xff08;又称逻辑回归&#xff09;是一种广义的线性回归分析模型&#xff0c;用于研究分类型因变量与自变量之间影响关系。Logistic回归分析根据因变量的不同可分为二元Logistic回归、多分类Logistic回归&#xff0c;有序Logistic回归…

大牛直播SDK最经典的一句

搜索引擎搜大牛直播SDK&#xff0c;居然提示我搜“大牛直播SDK最经典的一句”&#xff0c;闲来无事&#xff0c;点开看看&#xff0c;AI智能问答&#xff0c;给出了答案&#xff1a; ‌大牛直播SDK最经典的一句是&#xff1a;"我们只做最擅长的部分,我们不做的,提供对接接…

《向量数据库指南》——解锁AI新篇章:高效处理非结构化数据的五大策略

在探讨如何有效处理非结构化数据这一AI发展的核心挑战时,我们首先需要深入理解非结构化数据的本质特性及其带来的技术难题,进而探讨当前技术生态中的不足与机遇,并提出一系列专业且可操作的解决方案。 非结构化数据的四大挑战 1. 数量庞大: 非结构化数据,如文本、图像、…

图为科技基于昇腾AI,打造智慧工厂检测解决方案

中国作为全球制造业的翘楚&#xff0c;在工业领域成就斐然。因工业生产的特殊环境与工艺要求&#xff0c;面临着高温、高压、易燃易爆等多重高危因素。 其生产装置通常大型化且密集&#xff0c;生产工艺复杂&#xff0c;生产过程紧密耦合。在这样的背景下&#xff0c;围绕“人…

springboot汽车租赁系统-计算机毕业设计源码65876

目录 第 1 章 引 言 1.1 选题背景 1.2 研究现状 1.3 论文结构安排 第 2 章 系统的需求分析 2.1 系统可行性分析 2.1.1 技术方面可行性分析 2.1.2 经济方面可行性分析 2.1.3 法律方面可行性分析 2.1.4 操作方面可行性分析 2.2 系统功能需求分析 2.3 系统性需求分析 …

金智维K-RPA基本介绍

一、K-RPA基本组成 K-RPA软件机器人管理系统基于“RPAX”数字化技术打造&#xff0c;其核心系统由管理中心(Server)、设计器(Control)、机器人(Robot/Agent)三大子系统组成&#xff0c;各子系统协同工作&#xff0c;易于构建协同式环境。 管理中心&#xff08;Server&#xff…

测试人员必备的linux命令(已分组)

文件与目录管理 查看当前目录&#xff1a;pwd 列出目录内容&#xff1a;ls [-l] &#xff08;-l 参数显示详细信息&#xff09; 切换目录&#xff1a;cd [目录名] 创建目录&#xff1a;mkdir [-p] 目录名 &#xff08;-p 可以递归创建目录&#xff09; 删除空目录&#xf…

NVDLA专题14:Runtime environment-用户模式驱动

运行时环境&#xff08;runtime environment&#xff09;包括在兼容的NVDLA硬件上运行编译神经网络的软件。 它由两部分组成: 用户模式驱动&#xff08;User Mode Driver, UMD&#xff09;: 这是应用程序的主接口&#xff0c;正如Compile library中所详述的&#xff0c;对神经…

数据看板多端查看无压力,教你轻松设置响应式布局

最近&#xff0c;山海鲸可视化新增了一个非常实用的功能&#xff0c;叫作“响应式布局”。今天我来为大家介绍一下这个新功能以及它如何提升我们在不同设备上的使用体验。 你可能在用手机浏览网页时注意到&#xff0c;有些网站在手机和电脑上的显示方式几乎相同。然而&#xff…

讯方·智汇云校北京校区

讯方智汇云校北京校区介绍 讯方技术紧抓国家数智化转型契机&#xff0c;依托京西智谷&#xff0c;建立AI智算产业人才能力中心&#xff0c;提供智算全流程服务和智算人才培养。研发了讯方AI场景创新工坊、讯方AI行业支撑智能体等核心产品&#xff0c;同时导入华为全系列智算人…

C++20中头文件bit的使用

C20中头文件bit是数字库(numeric library)的一部分&#xff0c;定义用于访问、操作和处理单个位和位序列(individual bits and sequences of bits)的函数。 1.std::endian:指示标量类型的字节序(byte order)&#xff0c;支持little(小端序)、big(大端序)、native: 如果所有标量…

AI大模型行业深度:行业现状、应用情况、产业链及相关公司深度梳理

随着人工智能技术的迅猛发展&#xff0c;AI大模型已经成为全球科技竞争的焦点、未来产业的关键赛道以及经济发展的新动力&#xff0c;展现出巨大的发展潜力和广阔的应用前景。目前&#xff0c;AI大模型的应用落地引发行业关注。技术的持续进步促使AI大模型的应用逐步从云端向终…

GitLab CI Runner安装

参考文章&#xff1a;[花了两天&#xff0c;搞了Gitlab-Runner CI/CD实现自动化部署&#xff0c;可比Jenkins香太多啦&#xff01;&#xff01;&#xff01;&#xff01;_gitlab的cicd取代jenkens-CSDN博客] Gitlab的CI需要安装CI专用的GitLab Runner&#xff0c;否则跑不起来…

Vue+SpringBoot+数据库整体开发流程 2

本篇是继我的另一篇博客VueSpringBoot数据库整体开发流程 1-CSDN博客 目录 四、前端开发 简单开发 启动项目 五、前后端联通 四、前端开发 简单开发 &#xff08;1&#xff09;直接修改项目的App.vue文件中的route-link&#xff0c; &#xff08;2&#xff09;新建这个Fe…

【北京迅为】《STM32MP157开发板使用手册》- 第二十章 Trusted Firmware-A 移植+第二十一章 U-Boot移植

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

炸裂!新版 SD WebUI Forge 出图速度更快!支持最新Flux 模型!(保姆级安装教程)

大家是不是经常为SD WebUI卡顿、爆显存而苦恼?一启动SD 电脑就开始发烫&#xff0c; 尤其低显存用户屡屡"中招",不得不一遍遍重启。作为AI绘画的必备工具&#xff0c;WebUI却还有这么多"坑"&#xff0c;着实让人不爽!&#x1f620; 好消息是&#xff0c;…

盘点2024年8月Sui生态发展,了解Sui近期成长历程

随着技术的不断沉淀和产品的不断打磨&#xff0c;Sui生态在2024年8月取得了令人欣喜的进步。作为创新的L1协议&#xff0c;Sui不仅在技术革新方面表现突出&#xff0c;还在DeFi、游戏应用和开发者工具等领域展现出强大的潜力。 本篇文章将全面盘点Sui在过去一个月内的技术创新…