Chronos:学习时间序列的大语言模型(训练、微调代码解析)

前言

  • 《Chronos: Learning the Language of Time Series》原文地址,Github开源代码地址
  • Chronos:学习时间序列的大语言模型(论文解读)CSDN地址
  • Chronos:学习时间序列的大语言模型(代码解析)CSDN地址
  • GitHub项目地址Some-Paper-CN。本项目是译者在学习长时间序列预测、CV、NLP和机器学习过程中精读的一些论文,并对其进行了中文翻译。还有部分最佳示例教程
  • 如果有帮助到大家,请帮忙点亮Star,也是对译者莫大的鼓励,谢谢啦~
  • 本文代码已同步至项目Some-Paper-CN
  • 本来准备自己写一个Lora微调的教程,结果官方先放了预训练(微调)代码,那就先看看官方写的微调代码是怎样的吧~

数据转换

  • 将开源代码从Github上下载到本地,关键文件在chronos-forecasting/scripts/training下,train.py文件。
  • chronos-forecasting/scripts/README.md文件中给出了数据组织方式,还有论文中提到的数据合成方法KernelSynth
from pathlib import Path
from typing import List, Optional, Union

import numpy as np
from gluonts.dataset.arrow import ArrowWriter


def convert_to_arrow(
    path: Union[str, Path],
    time_series: Union[List[np.ndarray], np.ndarray],
    start_times: Optional[Union[List[np.datetime64], np.ndarray]] = None,
    compression: str = "lz4",
):
    if start_times is None:
        # Set an arbitrary start time
        start_times = [np.datetime64("2000-01-01 00:00", "s")] * len(time_series)

    assert len(time_series) == len(start_times)

    dataset = [
        {"start": start, "target": ts} for ts, start in zip(time_series, start_times)
    ]
    ArrowWriter(compression=compression).write_to_file(
        dataset,
        path=path,
    )


if __name__ == "__main__":
    # 创建20个长度为1024的时间序列
    time_series = [np.random.randn(1024) for i in range(20)]

    # 转换为GluonTS arrow格式
    convert_to_arrow("./noise-data.arrow", time_series=time_series)
  • 在进行下面的步骤之前,需要先将数据转换为GluonTSarrow这一点非常重要

训练、微调代码解析

  • 打开chronos-forecasting/scripts/training文件夹下的train.py文件,我们先看主函数main()
  • 主函数上的装饰器@app.command()是库typer提供的,其主要作用是构建CLI 程序。Github地址,官方文档,对代码理解没什么太大影响,感兴趣的可以去看看特性。
  • 另外一个装饰器@use_yaml_config()typer-config,Github地址提供的,主要作用是读取配置文件,官方文档,建议大家去看看,使用方法,有助于理解代码。
@app.command()
@use_yaml_config(param_name="config")
def main(
    training_data_paths: str,
    probability: Optional[str] = None,
    context_length: int = 512,
    prediction_length: int = 64,
    min_past: int = 64,
    max_steps: int = 200_000,
    save_steps: int = 50_000,
    log_steps: int = 500,
    per_device_train_batch_size: int = 32,
    learning_rate: float = 1e-3,
    optim: str = "adamw_torch_fused",
    shuffle_buffer_length: int = 100,
    gradient_accumulation_steps: int = 2,
    model_id: str = "google/t5-efficient-tiny",
    model_type: str = "seq2seq",
    random_init: bool = False,
    tie_embeddings: bool = False,
    output_dir: Path = Path("./output/"),
    tf32: bool = True,
    torch_compile: bool = True,
    tokenizer_class: str = "MeanScaleUniformBins",
    tokenizer_kwargs: str = "{'low_limit': -15.0, 'high_limit': 15.0}",
    n_tokens: int = 4096,
    n_special_tokens: int = 2,
    pad_token_id: int = 0,
    eos_token_id: int = 1,
    use_eos_token: bool = True,
    lr_scheduler_type: str = "linear",
    warmup_ratio: float = 0.0,
    dataloader_num_workers: int = 1,
    max_missing_prop: float = 0.9,
    num_samples: int = 20,
    temperature: float = 1.0,
    top_k: int = 50,
    top_p: float = 1.0,
    seed: Optional[int] = None,
):
    # ast.literal_eval对字符串进行合理转换
    training_data_paths = ast.literal_eval(training_data_paths)
    # 检查是否为list类型
    assert isinstance(training_data_paths, list)

    # 若probability为str
    if isinstance(probability, str):
        # 对字符串进行合理转换
        probability = ast.literal_eval(probability)
    # 若没有传入,则平分采样概率
    elif probability is None:
        probability = [1.0 / len(training_data_paths)] * len(training_data_paths)
    # 检查是否为list类型
    assert isinstance(probability, list)

    # 若tokenizer_kwargs为str
    if isinstance(tokenizer_kwargs, str):
        # 对tokenizer_kwargs进行合理转换
        tokenizer_kwargs = ast.literal_eval(tokenizer_kwargs)
    # 检查tokenizer_kwargs是否为dict
    assert isinstance(tokenizer_kwargs, dict)

    # model_type是否为seq2seq或causal
    assert model_type in ["seq2seq", "causal"]
    # 如果是seq2seq模型,则不支持
    if not model_type == "seq2seq":
        raise NotImplementedError("Only seq2seq models are currently supported")

    # 若没有传入随机数种子,则随机生成
    if seed is None:
        seed = random.randint(0, 2**32)

    log_on_main(f"Using SEED: {seed}", logger)
    # 设定transformer库的随机数种子
    transformers.set_seed(seed=seed)
    # 设定output_dir
    output_dir = get_next_path("run", base_dir=output_dir, file_type="")

    log_on_main(f"Logging dir: {output_dir}", logger)
    log_on_main(
        f"Loading and filtering {len(training_data_paths)} datasets "
        f"for training: {training_data_paths}",
        logger,
    )

    log_on_main(
        f"Mixing probabilities: {probability}",
        logger,
    )

    # 构造训练数据集
    # partial:包装函数,从左到右固定默认参数
    # has_enough_observations:检查传入的目标列表是否满足最小观测值数量
    # FileDataset:读取时间序列数据,path为路径,freq为频率
    # Filter:筛选出满足特定条件的元素,partial中为判断函数,FileDataset为待过滤的可迭代对象
    train_datasets = [
        Filter(
            partial(
                has_enough_observations,
                min_length=min_past + prediction_length,
                max_missing_prop=max_missing_prop,
            ),
            FileDataset(path=Path(data_path), freq="h"),
        )
        for data_path in training_data_paths
    ]

    log_on_main("Initializing model", logger)

    # 读取预训练模型
    model = load_model(
        model_id=model_id,
        model_type=model_type,
        vocab_size=n_tokens,
        random_init=random_init,
        tie_embeddings=tie_embeddings,
        pad_token_id=pad_token_id,
        eos_token_id=eos_token_id,
    )

    # 读取配置文件
    chronos_config = ChronosConfig(
        tokenizer_class=tokenizer_class,
        tokenizer_kwargs=tokenizer_kwargs,
        n_tokens=n_tokens,
        n_special_tokens=n_special_tokens,
        pad_token_id=pad_token_id,
        eos_token_id=eos_token_id,
        use_eos_token=use_eos_token,
        model_type=model_type,
        context_length=context_length,
        prediction_length=prediction_length,
        num_samples=num_samples,
        temperature=temperature,
        top_k=top_k,
        top_p=top_p,
    )

    # 为模型配置添加额外键值对,以便保存在ckpt中
    model.config.chronos_config = chronos_config.__dict__

    # 整合训练数据并进行打乱
    shuffled_train_dataset = ChronosDataset(
        datasets=train_datasets,
        probabilities=probability,
        tokenizer=chronos_config.create_tokenizer(),
        context_length=context_length,
        prediction_length=prediction_length,
        min_past=min_past,
        mode="training",
    ).shuffle(shuffle_buffer_length=shuffle_buffer_length)

    # 设置训练参数
    training_args = TrainingArguments(
        output_dir=str(output_dir),
        per_device_train_batch_size=per_device_train_batch_size,
        learning_rate=learning_rate,
        lr_scheduler_type=lr_scheduler_type,
        warmup_ratio=warmup_ratio,
        optim=optim,
        logging_dir=str(output_dir / "logs"),
        logging_strategy="steps",
        logging_steps=log_steps,
        save_strategy="steps",
        save_steps=save_steps,
        report_to=["tensorboard"],
        max_steps=max_steps,
        gradient_accumulation_steps=gradient_accumulation_steps,
        dataloader_num_workers=dataloader_num_workers,
        tf32=tf32,  # remove this if not using Ampere GPUs (e.g., A100)
        torch_compile=torch_compile,
        ddp_find_unused_parameters=False,
        remove_unused_columns=False,
    )

    # 创建训练实例
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=shuffled_train_dataset,
    )
    log_on_main("Training", logger)

    trainer.train()

    if is_main_process():
        model.save_pretrained(output_dir / "checkpoint-final")
  • 上面的main函数有一个函数has_enough_observations,用于检查时序长度是否满足要求,逐行代码解析如下
def has_enough_observations(
    entry: dict, min_length: int = 0, max_missing_prop: float = 1.0
) -> bool:
    def has_enough_observations(
    entry: dict, min_length: int = 0, max_missing_prop: float = 1.0
) -> bool:
    """
    检查数据中的 ``"target"`` 是否满足最小需求量

    参数
    ----------
    entry
        需要进行检测的数据
    min_length
         ``"target"`` 的最小长度
    max_missing_prop
         ``"target"``的最大缺失率
    """
    if (
        len(entry["target"]) >= min_length
        and np.isnan(entry["target"]).mean() <= max_missing_prop
    ):
        return True
    return False
    if (
        len(entry["target"]) >= min_length
        and np.isnan(entry["target"]).mean() <= max_missing_prop
    ):
        return True
    return False
  • 上面的main函数有一个很重要的类ChronosDataset用于组织数据,逐行代码解析如下
class ChronosDataset(IterableDataset, ShuffleMixin):
    """
    数据包装器, 使用 ``ChronosTokenizer`` 将时间序列数据变为HuggingFace的格式`` input_ids ``,
    ``attention_mask``和``labels``。

    假定原始数据集中有键 ``"start"`` (值的类型为``pd.Period``), 键 ``"target"`` (值的类型为``
    np.ndarray``).

    参数
    ----------
    datasets
        包含原始时间序列数据的数据集。
    probabilities
        在训练模式下,将按照概率列表从每个原始数据集中抽取数据。
    tokenizer
        用于将数值序列转换为 ``token ID`` 。
    context_length
        输入 ``token ID`` 的长度。
    prediction_length
        预测长度
    drop_prob
        样本中的观测值将按此概率变成 ``np.nan``,即缺失值。
    min_past
        只有至少有 ``min_past`` 个历史观测数据时,才会考虑样本。
    mode
        模式为``"training"``, ``"validation"``或 ``"test"``中的一个。
    np_dtype
        Numpy浮点数据类型。
    """

    def __init__(
        self,
        datasets: list,
        probabilities: List[float],
        tokenizer: ChronosTokenizer,
        context_length: int = 512,
        prediction_length: int = 64,
        drop_prob: float = 0.2,
        min_past: Optional[int] = None,
        mode: str = "training",
        np_dtype=np.float32,
    ) -> None:
        super().__init__()

        assert len(probabilities) == len(datasets)
        assert mode in ("training", "validation", "test")

        self.datasets = datasets
        self.probabilities = probabilities
        self.tokenizer = tokenizer
        self.context_length = context_length
        self.prediction_length = prediction_length
        self.drop_prob = drop_prob
        self.min_past = min_past or prediction_length
        self.mode = mode
        self.np_dtype = np_dtype

    # 对输入的字典进行预处理。
    def preprocess_entry(self, entry: dict, mode: str) -> dict:
        # 将entry字典的start和target字段提取出来,重新构造出一个新的字典
        entry = {f: entry[f] for f in ["start", "target"]}
        # 将target字段的值转换为NumPy数组,数据类型设定为初始化时参数np_dtype
        entry["target"] = np.asarray(entry["target"], dtype=self.np_dtype)
        # 检测target是否为一维
        assert entry["target"].ndim == 1, f"got {entry['target'].ndim=}, expected 1"

        # 如果mode为training,并且数据丢失概率大于0,则根据丢失概率将数值置为np.nan,同时在mask中标出
        if mode == "training" and self.drop_prob > 0:
            target = entry["target"].copy()
            drop_p = np.random.uniform(low=0.0, high=self.drop_prob)
            mask = np.random.choice(
                [True, False], size=len(target), p=[drop_p, 1 - drop_p]
            )
            target[mask] = np.nan
            entry["target"] = target

        return entry

    # 创建数据分割加载器
    def _create_instance_splitter(self, mode: str):
        assert mode in ["training", "test", "validation"]

        # ExpectedNumInstanceSampler:计算时间序列的平均长度,使每个时间序列平均生成num_instances个训练实例
        # TestSplitSampler:从每个时间序列的末尾开始抽取样本作为测试集
        # ValidationSplitSampler:根据限制的样本数量从每个时间序列的末尾开始抽取样本作验证集
        instance_sampler = {
            "training": ExpectedNumInstanceSampler(
                num_instances=1.0,
                min_instances=1,
                min_past=self.min_past,
                min_future=self.prediction_length,
            ),
            "test": TestSplitSampler(),
            "validation": ValidationSplitSampler(min_future=self.prediction_length),
        }[mode]

        # InstanceSplitter:通过在指定采样器选择的时间点上切分目标和其他时间序列字段
        return InstanceSplitter(
            target_field="target",
            is_pad_field="is_pad",
            start_field="start",
            forecast_start_field="forecast_start",
            instance_sampler=instance_sampler,
            past_length=self.context_length,
            future_length=self.prediction_length,
            dummy_value=np.nan,
        )

    # 创建训练数据集
    def create_training_data(self, data):
        # Cyclic生成循环的数据
        data = Cyclic(data)
        # 根据过滤规则加载训练数据集
        split_transform = self._create_instance_splitter(
            "training"
        ) + FilterTransformation(
            condition=lambda entry: (~np.isnan(entry["past_target"])).sum() > 0
        )
        # 将加载器应用在数据上
        data = split_transform.apply(data, is_train=True)
        return data

    # 创建测试数据集
    def create_test_data(self, data):
        data = self._create_instance_splitter("test").apply(data, is_train=False)
        return data

    # 创建验证数据集
    def create_validation_data(self, data):
        data = self._create_instance_splitter("validation").apply(data, is_train=False)
        return data

    # 将数据转换为hugging face大语言模型输入的格式
    def to_hf_format(self, entry: dict) -> dict:
        past_target = torch.tensor(entry["past_target"]).unsqueeze(0)
        input_ids, attention_mask, scale = self.tokenizer.input_transform(past_target)
        future_target = torch.tensor(entry["future_target"]).unsqueeze(0)
        labels, labels_mask, _ = self.tokenizer.input_transform(future_target, scale)
        labels[labels_mask == 0] = -100
        return {
            "input_ids": input_ids.squeeze(0),
            "attention_mask": attention_mask.squeeze(0),
            "labels": labels.squeeze(0),
        }

    # 生成可迭代对象
    def __iter__(self) -> Iterator:
        preprocessed_datasets = [
            Map(
                partial(self.preprocess_entry, mode=self.mode),
                dataset,
            )
            for dataset in self.datasets
        ]

        if self.mode == "training":
            iterables = [
                self.create_training_data(dataset) for dataset in preprocessed_datasets
            ]
        elif self.mode == "test":
            iterables = [
                self.create_test_data(dataset) for dataset in preprocessed_datasets
            ]
        else:
            iterables = [
                self.create_validation_data(dataset)
                for dataset in preprocessed_datasets
            ]

        worker_info = get_worker_info()
        if worker_info is None:
            probs = list(self.probabilities)
        else:
            worker_id = worker_info.id
            num_workers = worker_info.num_workers
            iterables = list(itertools.islice(iterables, worker_id, None, num_workers))
            probs = list(
                itertools.islice(self.probabilities, worker_id, None, num_workers)
            )

        probs = [prob / sum(probs) for prob in probs]

        iterators = list(map(iter, iterables))
        if self.mode == "training":
            while True:
                idx = np.random.choice(range(len(iterators)), p=probs)
                try:
                    yield self.to_hf_format(next(iterators[idx]))
                except StopIteration:
                    probs[idx] = 0
                    if sum(probs) == 0:
                        return
                    probs = [prob / sum(probs) for prob in probs]
        else:
            for entry in itertools.chain(*iterators):
                yield self.to_hf_format(entry)
  • ChronosDataset类中有大量的gluonts中的方法,gluonts的Github地址,这些方法可以在官方文档中搜索
  • train.py文件最重要的几部分就是main函数和ChronosDataset类,要多理解逐行看,实际上是很简单的,比前面的算法代码解析要简单一些,就是gluonts抽象化的东西太多了,不是很好理解。
  • 其实我觉得这个代码封装程度太高了,很多东西不是那么好理解,并且在gluonts的官方文档中,方法的确搜的到,但是很多没有注释,只能去看源代码,希望后面gluonts能完善API文档的说明吧。

配置文件

  • 训练的参数需要使用配置文件来设定,配置文件的目录在scripts/training/configs,第一次尝试跑通代码可以从预训练chronos-t5-tiny模型开始,模型体量小,计算要求不高,需要注意的是chronos的官方代码是不支持CPU微调的,必须要使用GPU
  • chronos-t5-tiny.yaml文件及参数说明如下
# 数据路径
training_data_paths:
- "/home/ubuntu/tsmixup-data.arrow"
- "/home/ubuntu/kernelsynth-data.arrow"
# 数据采样概率
probability:
- 0.9
- 0.1
# 时序窗口长度
context_length: 512
# 预测长度
prediction_length: 64
# 最小历史样本数
min_past: 60
# 最大训练步数
max_steps: 200_000
# 保存间隔步数
save_steps: 100_000
# 过程指标报告步数
log_steps: 500
# batch size
per_device_train_batch_size: 32
# 学习率
learning_rate: 0.001
# 优化器
optim: adamw_torch_fused
num_samples: 20
shuffle_buffer_length: 100_000
# 梯度累积
gradient_accumulation_steps: 1
# 模型路径
model_id: google/t5-efficient-tiny
# 模型种类
model_type: seq2seq
# 随机初始化
random_init: true
tie_embeddings: true
# 输出路径
output_dir: ./output/
# 是否启用tf32
tf32: true
# torch计算优化
torch_compile: true
# tokenizer类型
tokenizer_class: "MeanScaleUniformBins"
# 数据规整化的上限和下限
tokenizer_kwargs:
  low_limit: -15.0
  high_limit: 15.0
# tokens数量
n_tokens: 4096
# 学习率策略
lr_scheduler_type: linear
# 学习率预热
warmup_ratio: 0.0
# 数据加载器使用CPU核心数量
dataloader_num_workers: 1
# 最大数据缺失率
max_missing_prop: 0.9
# 使用结束标识符
use_eos_token: true
  • 如果想要微调已经训练好的模型,可以将model_id改为amazon/chronos-t5-small,关闭no-random-init,调整learning_ratestep参数。

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

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

相关文章

apk右键一键签名方法

使用说明 1 修改reg文件最后一行&#xff0c;修改为自己的电脑路径 2 修改bat文件apksigner_path路径为自己的SDK路径&#xff0c;将签名文件命名为platform.keystore放在该文件夹内 3 运行reg文件添加注册表后&#xff0c;要签名的apk右键选择“cux”系统签名即可 一键cux系…

ABAP开发:动态Open SQL编程案例介绍

动态Open SQL是Open SQL的扩展。它不是要求整个SQL语句都是动态指定的。通过熟悉的静态ABAP编码表达静态已知的部分&#xff0c;动态元素的部分通过动态标记指定。动态片段不明确包含在ABAP源代码中&#xff0c;而是源代码包含一个ABAP变量&#xff0c;用括号括起来作为占位符。…

【网络架构】lvs集群

目录 一、集群与分布式 1.1 集群介绍 1.2 分布式系统 1.3 集群设计原则 二、LVS 2.1 lvs工作原理 2.2 lvs集群体系架构 ​编辑 2.3 lvs功能及组织架构 2.4 lvs集群类型中术语 三、LVS工作模式和命令 3.1 lvs集群的工作模式 3.1.1 lvs的nat模式 3.1.2 lvs的dr模式 …

python-docx 使用xml为docx不同的章节段落设置不同字体

本文目录 前言一、完整代码二、代码详细解析1、处理过程解释(1) 引入库并定义路径(2) 创建docx的备份文件(3) 定义命名空间(4) 打开并处理.docx文件(5) 分析和组织文档结构(6) 设置字体(7) 保存结果前言 本文主要解决的内容,就是为一个docx的不同章节段落设置不同的字体,因为…

2024年公司加密软件排行榜(企业加密软件推荐)

在信息时代&#xff0c;企业数据安全至关重要&#xff0c;防止数据泄露和未授权访问是首要任务之一。以下是2024年备受好评的企业加密软件排行榜&#xff1a; 固信加密软件https://www.gooxion.com/ 1.固信加密软件 固信加密软件是新一代企业级加密解决方案&#xff0c;采用先…

7月开始,考研数学0️⃣基础线代30天满分规划

线代零基础&#xff1f; 那千万不要去跟李永乐老师的线代课程&#xff0c;因为李永乐老师的线代课程比较进阶&#xff0c;适合有一定基础的同学去听&#xff0c;下面这两位才是零基础线代的神&#xff01; 一个是喻老&#xff0c;另外一个是汤家凤&#xff01; 这两个老师的…

stencil 组件

stencil 组件 装饰器生命周期应用加载事件 组件定义组件如何响应数据变化 组件使用如何传递 slot如何暴露组件内部的方法供外部使用&#xff1f;Element 装饰器 Host 组件样式函数组件 stencil 提供一些装饰器、生命周期钩子和渲染函数去编写一个组件。 装饰器 装饰器是一组用…

Web网页端IM产品RainbowChat-Web的v7.0版已发布

一、关于RainbowChat-Web RainbowChat-Web是一套Web网页端IM系统&#xff0c;是RainbowChat的姊妹系统&#xff08;RainbowChat是一套基于开源IM聊天框架 MobileIMSDK (Github地址) 的产品级移动端IM系统&#xff09;。 ► 详细介绍&#xff1a;http://www.52im.net/thread-2…

【Sklearn驯化-回归指标】一文搞懂机器学习中回归算法评估指标:mae、rmse等

【Sklearn驯化-回归指标】一文搞懂机器学习中回归算法评估指标&#xff1a;mae、rmse等 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 免…

大学计算机

项目一 了解计算机 1.1 了解计算机的诞生及发展阶段 1.2 认识计算机的特点、应用和分类 1&#xff0e;计算机的特点 1. 计算机的特点 2.计算机的应用 3.计算机的分类 4.数量单位 1.3 了解计算机操作系统的概念、功能与种类 1.操作系统概念 2.操作系统的作用 1&#xff0e…

Linux 中变量的取用与设定

优质博文&#xff1a;IT-BLOG-CN Linux是一个多人多任务的环境&#xff0c;每个人登录系统都能取得一个bash shell&#xff0c;每个人都能够使用bash下达mail这个指令来接收自己的邮箱等等。问题是&#xff0c;bash如何得知你的邮箱是那个文件&#xff1f;这就需要『变量』的帮…

jmeter性能测试

一.jmeter基本使用 1.元件执行顺序 配置元件&#xff1b; 前置处理器&#xff1b; 定时器&#xff1b; sampler&#xff1b; 后置处理器&#xff1b;&#xff08;关联&#xff0c;正则表达式提取器&#xff09; 断言&#xff1b; 监听&#xff1b;&#xff08;不涉及顺序&…

Windows 电脑类别怎么区分?不同类别区分总结

电脑类别 Windows 电脑的类别有哪些&#xff1f;我们可以大致分为这三类&#xff1a;CopilotPC、AI PC、普通 PC。下面就来看看这些电脑类别的区别。 普通 PC 普通 PC 就是指那些标准的台式电脑或者笔记本电脑&#xff0c;它们是由中央处理器&#xff08;CPU&#xff09;以及…

【面试题】信息安全风险评估要做些什么

信息安全风险评估是识别、评估和管理信息系统中潜在风险的重要过程。它具有以下几个关键步骤&#xff1a; 1.资产识别&#xff1a; 确定需要保护的信息资产&#xff0c;如硬件、软件、数据、人员等。例如&#xff0c;企业的客户数据库、重要的业务文档等。 2.威胁评估&#…

手把手教你打造高精度STM32数字时钟,超详细步骤解析

STM32数字时钟项目详解 1. 项目概述 STM32数字时钟是一个集成了时间显示、闹钟功能、温湿度检测等多功能于一体的小型电子设备。它利用STM32的实时时钟(RTC)功能作为核心,配合LCD显示屏、按键输入、温湿度传感器等外设,实现了一个功能丰富的数字时钟系统。 2. 硬件组成 STM…

文献解读-基因编辑-第十二期|《CRISPR-detector:快速、准确地检测、可视化和注释基因组编辑事件引起的全基因组范围突变》

关键词&#xff1a;基因组变异检测&#xff1b;全基因组测序&#xff1b;基因编辑&#xff1b; 文献简介 标题&#xff08;英文&#xff09;&#xff1a;CRISPR-detector: fast and accurate detection, visualization, and annotation of genome-wide mutations induced by g…

做外贸有些事说早了,未必是好事

如果说能说话&#xff0c;其实谁也会&#xff0c;但是能把话说好却并不是一个简单的事&#xff0c;而且说话的时机往往也影响着事情的结局和走向&#xff0c; 所以才有了老人常提起的那句话&#xff1a;三岁学说话&#xff0c;一生学闭嘴。 最近我又因为图省事而犯了一个错误&…

云通SIPX,您的码号资源智能调度专家!

在数字化转型的浪潮中&#xff0c;号码资源作为企业与客户沟通的重要桥梁&#xff0c;其管理效率直接关系到企业运营的成败。随着运营商对号码资源管理的规范化和精细化&#xff0c;企业对高效、智能的号码资源管理需求日益增长&#xff0c;以实现对外呼叫的降本增效。 一、什么…

JAVA编程题期末题库【中】

8.计算邮资 程序代码: public static void main(String[] args) {// 计算邮资//if多分支语句//创建对象java.util.Scanner inputnew java.util.Scanner(System.in); //提示输入用户&#xff0c;输入邮件的重量System.out.println("邮件的重量&#xff1a;");int wei…

VMware ESXi 8.0U2c macOS Unlocker OEM BIOS Huawei (华为) FusionServer 定制版

VMware ESXi 8.0U2c macOS Unlocker & OEM BIOS Huawei (华为) FusionServer 定制版 ESXi 8.0U2 标准版&#xff0c;Dell (戴尔)、HPE (慧与)、Lenovo (联想)、Inspur (浪潮)、Cisco (思科)、Hitachi (日立)、Fujitsu (富士通)、NEC (日电)、Huawei (华为)、xFusion (超聚…