transformers 框架使用详解,bert-base-chinese

news2024/11/6 20:36:30

以 bert-base-chinese 模型为例,模型目录 model_name = "C:/Users/Administrator.DESKTOP-TPJL4TC/.cache/modelscope/hub/tiansz/bert-base-chinese"

bert-base-chinese 模型大小只有400多兆,参数的量级在百万级别,与现在动辄几十个G,几十亿几百亿的参数量级不在一个层次,所以 bert 的主要功能是理解语义,它的双向编码其实就是transformer论文中的自注意力的实现。既然能够理解语义,它就能实现一些延伸的能力。

1、两个句子相似度的比较。

2、实现简单的QA,即给它一段话作为context,然后根据这段话提问,它能定位到你这个问题的答案在context中的位置,然后将答案揪出来,当然,它不是generate模型,它的参数量也做不到generate,它只是简单的截取一句话作为最符合的答案。

3、命名实体识别NER

4、在NLP领域,你可以定义很多下游任务,当然要自己实现输出层的逻辑。

transformers的三大组件configuration, tokenizer和model都可以通过一致的from_pertrained()方法来实例化。

Transformers提供了三个主要的组件。

  • Configuration配置类。存储模型和分词器的参数,诸如词表大小,隐层维数,dropout rate等。配置类对深度学习框架是透明的。
  • Tokenizer分词器类。每个模型都有对应的分词器,存储token到index的映射,负责每个模型特定的序列编码解码流程,比如BPE(Byte Pair Encoding),SentencePiece等等。也可以方便地添加特殊token或者调整词表大小,如CLS、SEP等等。
  • Model模型类。提供一个基类,实现模型的计算图和编码过程,实现前向传播过程,通过一系列self-attention层直到最后一个隐藏状态层。在最后一层基础上,根据不同的应用会再做些封装,比如XXXForSequenceClassification,XXXForMaskedLM这些派生类。

Transformers的作者们还为以上组件提供了一系列Auto Classes,能够从一个短的别名(如bert-base-cased)里自动推测出来应该实例化哪种配置类、分词器类和模型类。

Transformers提供两大类的模型架构,一类用于语言生成NLG任务,比如GPT、GPT-2、Transformer-XL、XLNet和XLM,另一类主要用于语言理解任务,如Bert、DistilBert、RoBERTa、XLM.

tokenizer.encode() 方法

经过层层继承,最终的实现是在文件transformers\tokenization_utils_base.py中的 class PreTrainedTokenizerBase(SpecialTokensMixin, PushToHubMixin):

def encode(
        self,
        text: Union[TextInput, PreTokenizedInput, EncodedInput],
        text_pair: Optional[Union[TextInput, PreTokenizedInput, EncodedInput]] = None,
        add_special_tokens: bool = True,
        padding: Union[bool, str, PaddingStrategy] = False,
        truncation: Union[bool, str, TruncationStrategy] = None,
        max_length: Optional[int] = None,
        stride: int = 0,
        padding_side: Optional[bool] = None,
        return_tensors: Optional[Union[str, TensorType]] = None,
        **kwargs,
    ) -> List[int]:
        """
        Converts a string to a sequence of ids (integer), using the tokenizer and vocabulary.

        Same as doing `self.convert_tokens_to_ids(self.tokenize(text))`.

        Args:
            text (`str`, `List[str]` or `List[int]`):
                The first sequence to be encoded. This can be a string, a list of strings (tokenized string using the
                `tokenize` method) or a list of integers (tokenized string ids using the `convert_tokens_to_ids`
                method).
            text_pair (`str`, `List[str]` or `List[int]`, *optional*):
                Optional second sequence to be encoded. This can be a string, a list of strings (tokenized string using
                the `tokenize` method) or a list of integers (tokenized string ids using the `convert_tokens_to_ids`
                method).
        """
        encoded_inputs = self.encode_plus(
            text,
            text_pair=text_pair,
            add_special_tokens=add_special_tokens,
            padding=padding,
            truncation=truncation,
            max_length=max_length,
            stride=stride,
            padding_side=padding_side,
            return_tensors=return_tensors,
            **kwargs,
        )

        return encoded_inputs["input_ids"]

model.eval() 的作用:模型在默认状态下是激活了 Dropout 模块,你此时给他输入数据会导致模型参数发生变化,所以需要调用eval()方法将模型设置为评估(evaluation)模式,deactivate DropOut modules。

python中的 __call__方法

它的作用为:当你把对象当做函数来调用时,例如 objectA(xxx),就会被重定向到__call__方法。

在类PreTrainedTokenizerBase

def __call__(
        self,
        text: Union[TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]] = None,
        text_pair: Optional[Union[TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]]] = None,
        text_target: Union[TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]] = None,
        text_pair_target: Optional[
            Union[TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]]
        ] = None,
        add_special_tokens: bool = True,
        padding: Union[bool, str, PaddingStrategy] = False,
        truncation: Union[bool, str, TruncationStrategy] = None,
        max_length: Optional[int] = None,
        stride: int = 0,
        is_split_into_words: bool = False,
        pad_to_multiple_of: Optional[int] = None,
        padding_side: Optional[bool] = None,
        return_tensors: Optional[Union[str, TensorType]] = None,
        return_token_type_ids: Optional[bool] = None,
        return_attention_mask: Optional[bool] = None,
        return_overflowing_tokens: bool = False,
        return_special_tokens_mask: bool = False,
        return_offsets_mapping: bool = False,
        return_length: bool = False,
        verbose: bool = True,
        **kwargs,
    ) -> BatchEncoding:
    	"""
        Main method to tokenize and prepare for the model one or several sequence(s) or one or several pair(s) of sequences.

        Args:
            text (`str`, `List[str]`, `List[List[str]]`, *optional*):
                The sequence or batch of sequences to be encoded. Each sequence can be a string or a list of strings
                (pretokenized string). If the sequences are provided as list of strings (pretokenized), you must set
                `is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
            text_pair (`str`, `List[str]`, `List[List[str]]`, *optional*):
                The sequence or batch of sequences to be encoded. Each sequence can be a string or a list of strings
                (pretokenized string). If the sequences are provided as list of strings (pretokenized), you must set
                `is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
            text_target (`str`, `List[str]`, `List[List[str]]`, *optional*):
                The sequence or batch of sequences to be encoded as target texts. Each sequence can be a string or a
                list of strings (pretokenized string). If the sequences are provided as list of strings (pretokenized),
                you must set `is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
            text_pair_target (`str`, `List[str]`, `List[List[str]]`, *optional*):
                The sequence or batch of sequences to be encoded as target texts. Each sequence can be a string or a
                list of strings (pretokenized string). If the sequences are provided as list of strings (pretokenized),
                you must set `is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
        """
    	...
        ...

此时,你就看到有这样的调用

tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForQuestionAnswering.from_pretrained(model_name)

inputs = tokenizer(question, context, return_tensors="pt")
outputs = model(**inputs)

model() 实际上调用的就是 model.forward() 。

而 tokenizer() 并不是 tokenizer.decode() ,从返回值类型就能看出来。从代码上看,tokenizer() 做了一些判断,返回值为 BatchEncoding;而 tokenizer.decode() 返回值为 BatchEncoding[‘input_ids’],所以也可以

input_ids = tokenizer.encode(question, context, return_tensors="pt")
outputs = model(input_ids)
model = xxx.from_pretrained(model_name) 的问题

同一个模型,可以有不同的下游任务,网络模型包括输入层,中间隐藏层,输出层三部分。我们所说的下游任务就是指输出层,我们拿到隐藏层的最后一层的计算结果之后,就可以在输出层上做些文章以实现不同的功能,所以在实例化模型的时候会有多种方式,AutoModelForxxxx,或者 BertForxxxx,所以 model() 的输出结果就不一样,参数个数也可能不一样,这个要去看它的 forward() 方法。

多去看看代码,基本上都有说明。我们以BertForQuestionAnswering为例

from transformers import BertTokenizer, BertForQuestionAnswering
model = BertForQuestionAnswering.from_pretrained(model_name)
......
outputs = model(**inputs)

类的定义如下

@add_start_docstrings(
    """
    Bert Model with a span classification head on top for extractive question-answering tasks like SQuAD (a linear
    layers on top of the hidden-states output to compute `span start logits` and `span end logits`).
    """,
    BERT_START_DOCSTRING,
)
class BertForQuestionAnswering(BertPreTrainedModel):
    def __init__(self, config):
        ......

    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,
        start_positions: Optional[torch.Tensor] = None,
        end_positions: Optional[torch.Tensor] = None,
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
    ) -> Union[Tuple[torch.Tensor], QuestionAnsweringModelOutput]:

从说明就知道此类提供question-answering任务,它的返回值是 Tuple[torch.Tensor] 或者 QuestionAnsweringModelOutput,通过传入的参数 return_dict 来决定返回值类型,默认就是返回 QuestionAnsweringModelOutput,它是一个dataclass,可以访问它的属性。

再比如

from transformers import BertTokenizer, AutoModel
model = AutoModel.from_pretrained(model_name)
print(type(model)) # <class 'transformers.models.bert.modeling_bert.BertModel'>

类的定义如下

@add_start_docstrings(
    "The bare Bert Model transformer outputting raw hidden-states without any specific head on top.",
    BERT_START_DOCSTRING,
)
class BertModel(BertPreTrainedModel):
    """

    The model can behave as an encoder (with only self-attention) as well as a decoder, in which case a layer of
    cross-attention is added between the self-attention layers, following the architecture described in [Attention is
    all you need](https://arxiv.org/abs/1706.03762) by Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit,
    Llion Jones, Aidan N. Gomez, Lukasz Kaiser and Illia Polosukhin.

    To behave as an decoder the model needs to be initialized with the `is_decoder` argument of the configuration set
    to `True`. To be used in a Seq2Seq model, the model needs to initialized with both `is_decoder` argument and
    `add_cross_attention` set to `True`; an `encoder_hidden_states` is then expected as an input to the forward pass.
    """

    _no_split_modules = ["BertEmbeddings", "BertLayer"]

    def __init__(self, config, add_pooling_layer=True):
        ......

    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,
        encoder_hidden_states: Optional[torch.Tensor] = None,
        encoder_attention_mask: Optional[torch.Tensor] = None,
        past_key_values: Optional[List[torch.FloatTensor]] = None,
        use_cache: Optional[bool] = None,
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
    ) -> Union[Tuple[torch.Tensor], BaseModelOutputWithPoolingAndCrossAttentions]:

从说明就知道此类只能作为encoder和decoder用,其返回值为 Tuple[torch.Tensor] 或者 BaseModelOutputWithPoolingAndCrossAttentions

@dataclass 的说明
@dataclass
class QuestionAnsweringModelOutput(ModelOutput):
    """
    Base class for outputs of question answering models.

    Args:
        loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `labels` is provided):
            Total span extraction loss is the sum of a Cross-Entropy for the start and end positions.
        start_logits (`torch.FloatTensor` of shape `(batch_size, sequence_length)`):
            Span-start scores (before SoftMax).
        end_logits (`torch.FloatTensor` of shape `(batch_size, sequence_length)`):
            Span-end scores (before SoftMax).
        hidden_states (`tuple(torch.FloatTensor)`, *optional*, returned when `output_hidden_states=True` is passed or when `config.output_hidden_states=True`):
            Tuple of `torch.FloatTensor` (one for the output of the embeddings, if the model has an embedding layer, +
            one for the output of each layer) of shape `(batch_size, sequence_length, hidden_size)`.

            Hidden-states of the model at the output of each layer plus the optional initial embedding outputs.
        attentions (`tuple(torch.FloatTensor)`, *optional*, returned when `output_attentions=True` is passed or when `config.output_attentions=True`):
            Tuple of `torch.FloatTensor` (one for each layer) of shape `(batch_size, num_heads, sequence_length,
            sequence_length)`.

            Attentions weights after the attention softmax, used to compute the weighted average in the self-attention
            heads.
    """

    loss: Optional[torch.FloatTensor] = None
    start_logits: torch.FloatTensor = None
    end_logits: torch.FloatTensor = None
    hidden_states: Optional[Tuple[torch.FloatTensor, ...]] = None
    attentions: Optional[Tuple[torch.FloatTensor, ...]] = None

此装饰器的作用相当于定义了一系列的类的属性

def __init__(self, 
    loss: Optional[torch.FloatTensor] = None
    start_logits: torch.FloatTensor = None
    end_logits: torch.FloatTensor = None
    hidden_states: Optional[Tuple[torch.FloatTensor, ...]] = None
    attentions: Optional[Tuple[torch.FloatTensor, ...]] = None
):
@classmethod 的说明
@classmethod
def from_pretrained(
    cls,
    pretrained_model_name_or_path: Union[str, os.PathLike],
    *init_inputs,
    cache_dir: Optional[Union[str, os.PathLike]] = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: Optional[Union[str, bool]] = None,
    revision: str = "main",
    trust_remote_code=False,
    **kwargs,
):
    ......
    ......

此方法是类的方法,不需要实例化就能访问,且第一个参数是类(cls),而不是对象(self)。此方法可以访问类属性和cls的方法,而不能访问self的方法。

with torch.no_grad() 的作用

torch.no_grad()是PyTorch中的一个上下文管理器(context manager),用于指定在其内部的代码块中不进行梯度计算。当你不需要计算梯度时,可以使用该上下文管理器来提高代码的执行效率,尤其是在推断(inference)阶段和梯度裁剪(grad clip)阶段的时候。不需要进行梯度计算和反向传播,只需要进行前向传播计算。,从而提高计算效率并节省内存。with torch.no_grad()常见于eval()验证集和测试集中。另外,This context manager is thread local; it will not affect computation in other threads.

logits

在神经网络中,logits通常是指模型在最后一层(全连接层)产生的原始输出,该层有多少个神经元就会有多少个值,这些输出还没有经过任何激活函数(如softmax或sigmoid)处理,根据不同的目的,将这些值输入到不同的激活函数中,就能归纳出不同的结果。
比如在多分类任务中,我们使用 logits.argmax(1)来得到预测的 label_id,也就是分类id。
其中torch.argmax(dim)函数的意思是返回最大值对应的索引,dim不指定任何参数就是指的所有元素;dim指定为0时求得是列的argmax;dim指定为1时求得是行的argmax;因为 torch 支持批量喂数据,所以很多地方得到的结果都是 [batch_size, n] 维的矩阵,batch_size 就是批的大小,相当于将这些样本的结果一行一行堆起来了,所以 dim = 1 就是逐个样本的求argmax。
比如在train的时候会按批(epoch)计算准确度

acc = (logits.argmax(1) == label).float().mean()

'''
(logits.argmax(1) == label).float() 得到 batch_size 个 0,1,0,1...
然后使用 mean() 求平均值,而这正好就是 1 出现的频率,即准确度
'''

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

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

相关文章

DICOM标准:DICOM图像核心属性概念详解——关于参考帧、病人位置、病人方位、图像位置和图像方向、切片位置、图像像素等重要概念解析

目录 1、参考帧模块属性 2、模态(Modality): 3、病人位置(Patient Position): 4、 病人方位(Patient Orientation): 5、 图像位置和图像方向: 6、切片位置 7、图像像素模块 7.1 图像像素属性描述 7.1.1 每个像素的样本 7.1.2 光度解释 7.1.3 平面结构 7.1.…

短视频矩阵系统的源码, OEM贴牌源码

针对矩阵短视频系统的源码功能设计&#xff0c;我们开发设计了以下核心模块&#xff1a; 1. 短视频一键发布功能&#xff1a;允许用户快速、便捷地发布短视频内容 2. 批量图文剪辑&#xff1a;支持同时编辑和处理多张图片与文本的组合&#xff0c;提高编辑效率 3. 批量剪辑短…

6款IntelliJ IDEA插件,让Spring和Java开发如虎添翼

文章目录 1、SonarLint2、JRebel for IntelliJ3、SwaggerHub插件4、Lombok插件5、RestfulTool插件6、 Json2Pojo插件7、结论 对于任何Spring Boot开发者来说&#xff0c;两个首要的目标是最大限度地提高工作效率和确保高质量代码。IntelliJ IDEA 是目前最广泛使用的集成开发环境…

新能源汽车与公共充电桩布局

近年来,全球范围内对新能源汽车产业的推动力度不断增强,中国新能源汽车市场也呈现蓬勃发展的势头,在政策与市场的共同推动下,新能源汽车销量持续增长。然而,据中国充电联盟数据显示,充电基础设施建设滞后于新能源汽车数量增长的现状导致充电桩供需不平衡,公共充电桩服务空白区域…

健身房管理智能化:SpringBoot技术指南

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了健身房管理系统的开发全过程。通过分析健身房管理系统管理的不足&#xff0c;创建了一个计算机管理健身房管理系统的方案。文章介绍了健身房管理系统的系统分析部…

Abaqus随机骨料过渡区孔隙三维网格插件:Random Agg ITZ Pore 3D (Mesh)

插件介绍 Random Agg ITZ Pore 3D (Mesh) V1.0 - AbyssFish 插件可在Abaqus内参数化建立包含水泥浆基体、粗细骨料、界面过渡区&#xff08;ITZ&#xff09;、孔隙在内的多相材料混凝土细观背景网格模型。 模型说明 插件采用材料映射单元的方式&#xff0c;将不同相材料赋值…

lora训练模型 打造个人IP

准备工作 下载秋叶炼丹器整理自己的照片下载底膜 https://rentry.org/lycoris-experiments 实操步骤 解压整合包 lora-scripts,先点击“更新” 训练图片收集 比如要训练一个自己头像的模型&#xff0c;就可以拍一些自己的照片&#xff08;20-50张&#xff0c;最少15张&…

sheng的学习笔记-tidb框架原理

目录 TiDB整体架构 TiDB架构图 组件-TiDB Server 架构图 流程 关系型数据转成kv ​编辑 组件-TiKV Server​ 架构图 主要功能&#xff1a; 列簇 组件-列存储TiFlash 组件-分布式协调层&#xff1a;PD PD架构图 路由 Region Cache back off TSO分配 概念 解…

计算机网络:网络层 —— 边界网关协议 BGP

文章目录 路由选择协议动态路由协议边界网关协议 BGPBGP 的基本概念BGP-4 的四种报文 路由选择协议 因特网是全球最大的互联网&#xff0c;它所采取的路由选择协议具有以下三个主要特点&#xff1a; 自适应&#xff1a;因特网采用动态路由选择&#xff0c;能较好地适应网络状态…

【在Linux世界中追寻伟大的One Piece】多路转接select

目录 1 -> I/O多路转接之select 1.1 -> 初识select 1.2 -> select函数原型 1.3 -> 关于fd_set结构 1.4 -> 关于timeval结构 2 -> 理解select执行过程 2.1 -> Socket就绪条件 2.2 -> select特点 2.3 -> select缺点 3 -> select使用示例…

小语言模型介绍与LLM的比较

小模型介绍 小语言模型&#xff08;SLM&#xff09;与大语言模型&#xff08;LLM&#xff09;相比&#xff0c;具有不同的特点和应用场景。大语言模型通常拥有大量的参数&#xff08;如 GPT-3 拥有 1750 亿个参数&#xff09;&#xff0c;能够处理复杂的自然语言任务&#xff…

双11花了“一部手机钱”买手机壳的年轻人,究竟在买什么?

【潮汐商业评论/原创】 这个双十一&#xff0c;Elsa在天猫多了一笔新支出——手机壳。和大家都熟悉的“义乌制造”不同的是&#xff0c;她的手机壳支出单件就已经到了500块&#xff0c;加上配套的手机链、支架、卡包、耳机壳&#xff0c;总共1000多元&#xff0c;足够买一部学…

03WIFI与蓝牙1——基于全志V3S的Linux开发板教程笔记

1. Kernel支持 1&#xff09;配置 终端输入&#xff1a; make menuconfig使能如下部分&#xff1a; 2&#xff09;编译 保存并退出后编译内核&#xff1a; make licheepi_zero_defconfig make menuconfig #配置内核&#xff0c;有需要的话配置 make -j16 make -j16 modu…

02系统跑起来——基于全志V3S的Linux开发板教程笔记

开发环境&#xff1a;Linux系统为Ubuntu 18.04.6 LTS&#xff0c;如在下面操作中提示部分环境/工具缺失未安装&#xff0c;请单独安装。 硬件基本介绍请参考第一篇文章&#xff1a;基于全志V3S的Linux开发板学习笔记01——简介 1. 环境准备 推荐直接下载完整资源包&#xff0c;…

用户思维,才是银行理财的杀手锏

近些年&#xff0c;伴随着居民理财需求的迸发&#xff0c;银行理财市场规模逐步扩大。 在前不久的2024Inclusion外滩大会上&#xff0c;麦肯锡发布了《银行理财六大趋势》&#xff0c;其中提到在财富管理客户风险偏好普遍下移的大背景下&#xff0c;银行理财将是客户财富管理配…

信息安全工程师(79)网络安全测评概况

一、定义与目的 网络安全测评是指参照一定的标准规范要求&#xff0c;通过一系列的技术、管理方法&#xff0c;获取评估对象的网络安全状况信息&#xff0c;并对其给出相应的网络安全情况综合判定。其对象主要为信息系统的组成要素或信息系统自身。网络安全测评的目的是为了提高…

学习笔记:微服务技术栈(一)服务治理框架SpringCloud

教学视频链接&#xff1a; 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 目录 前言一、认识微服务1.1 服务架构1.2 微服务架构1.3 SpringCloud 二、服务拆分及远程调用2.1 服务拆分细节2.2 服务间调用 …

医院数字影像信息系统源码,拥有完整知识产权,能够同HIS、电子病历、体检系统无缝对接

数字医学影像系统源码&#xff0c;RIS/PACS系统源码&#xff0c;医院数字影像信息系统源码。 开发技术&#xff1a;基于C/S架构&#xff0c;C#开发语言&#xff0c;数据库服务器采用Oracle数据库。三甲以下的医院都能满足 医学影像系统PACS系统是应用在医院影像科室的系统&…

LabVIEW for Linux 介绍

LabVIEW for Linux 介绍 1. 兼容性 LabVIEW for Linux 设计用于多种 Linux 发行版&#xff0c;包括 CentOS、Ubuntu 等。在安装之前&#xff0c;务必检查与您特定发行版版本的兼容性。 2. 程序移植 可移植性&#xff1a;在许多情况下&#xff0c;LabVIEW 程序&#xff08;VI…

使用NVM自由切换nodejs版本

一、NVM介绍 在日常开发中&#xff0c;我们可能需要同时进行多个不同NodeJS版本的项目开发&#xff0c;每个项目所依赖的nodejs版本可能不一致&#xff0c;我们如果只安装一个版本的nodejs&#xff0c;就可能出现node版本冲突问题&#xff0c;导致项目无法启动。这种情况下&am…