Llama改进之——分组查询注意力

引言

今天介绍LLAMA2模型引入的关于注意力的改进——分组查询注意力(Grouped-query attention,GQA)1

Transformer中的多头注意力在解码阶段来说是一个性能瓶颈。多查询注意力2通过共享单个key和value头,同时不减少query头来提升性能。多查询注意力可能导致质量下降和训练不稳定,因此常用的是分组查询注意力。

然后我们结合上篇文章3探讨的旋转位置编码,将选择位置编码应用到分组查询注意力上。

多头注意力

我们先回顾以下原始多头注意力的实现。

import torch
from torch import nn, Tensorimport math
from dataclasses import dataclass@dataclass
class ModelArgs:hidden_size: int = 512num_heads: int = 8attention_dropout: float = 0.1class MultiHeadAttention(nn.Module):def __init__(self, args: ModelArgs) -> None:super().__init__()self.hidden_size = args.hidden_sizeself.num_heads = args.num_headsself.head_dim = self.hidden_size // self.num_headsself.attention_dropout = args.attention_dropoutself.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)self.k_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)self.v_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)self.o_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)def forward(self, hidden_states: Tensor, attention_mask: Tensor = None):batch_size, seq_len, _ = hidden_states.shapequery_states, key_states, value_states = (self.q_proj(hidden_states),self.k_proj(hidden_states),self.v_proj(hidden_states),)query_states = query_states.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)key_states = key_states.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)value_states = value_states.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)if attention_mask is not None:causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]attn_weights = attn_weights + causal_mask# upcast attention to fp32 see https://github.com/huggingface/transformers/pull/17437attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)attn_weights = nn.functional.dropout(attn_weights, p=self.attention_dropout, training=self.training)attn_output = torch.matmul(attn_weights, value_states)attn_output = attn_output.transpose(1, 2).contiguous()attn_output = attn_output.reshape(batch_size, seq_len, self.hidden_size)attn_output = self.o_proj(attn_output)return attn_output

别忘了测试一下:

    args = ModelArgs()attention = MultiHeadAttention(args)inputs = torch.randn(32, 8, args.hidden_size)print(attention(inputs).shape)
torch.Size([32, 8, 512])

原始多头注意力就不再赘述了,之前的文章有过详细介绍。

分组查询注意力

分组查询注意力使用折中数量的key-value头(超过一个,但少于多头注意力全部的头数量)来提升性能。

多头注意力、分组查询注意力以及多查询注意力之间的区别如下:

image-20240413222803653

该图来自参考1中的论文。

202405301726

如上图所示,分组查询注意力是针对多头注意力的一种改进,每组Query头(这里两个Query一组)共享同一个Key和Value头,使得推理更加高效。

实际上在实现的时候,会将共享的Key和Value头进行广播(复制)成与Query头相同的数量:

202405301734

这样,我们就可以像普通多头注意力一样去计算了。

我们增加num_key_value_heads表示key、value头数;num_heads还是表示query头数。

@dataclass
class ModelArgs:hidden_size: int = 512num_heads: int = 8num_key_value_heads: int = 4attention_dropout: float = 0.1

分组查询注意力和多查询注意力可以合并在一起实现:

class GroupedQueryAttention(nn.Module):def __init__(self, args: ModelArgs) -> None:super().__init__()self.hidden_size = args.hidden_sizeself.num_heads = args.num_heads# 每个头的维度计算和之前一样self.head_dim = self.hidden_size // self.num_heads# 保存key/value头数self.num_key_value_heads = args.num_key_value_heads# 每组内要复制的次数,若为1,即退化为多头注意力;若为num_heads,则为多查询注意力self.num_key_value_groups = self.num_heads // args.num_key_value_headsself.attention_dropout = args.attention_dropoutself.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)# 注意Key和Value的映射这里节省了参数,加速了推理效率。self.k_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)self.v_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)# 最后的输出映射和之前一样self.o_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)def forward(self, hidden_states: Tensor, attention_mask: Tensor = None):batch_size, seq_len, _ = hidden_states.shapequery_states, key_states, value_states = (self.q_proj(hidden_states),self.k_proj(hidden_states),self.v_proj(hidden_states),)query_states = query_states.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)# 转换为对应的形状key_states = key_states.view(batch_size, seq_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)value_states = value_states.view(batch_size, seq_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)# 重复num_key_value_groups次,使得和query头数一致key_states = repeat_kv(key_states, self.num_key_value_groups)value_states = repeat_kv(value_states, self.num_key_value_groups)# 后面和普通多头注意力一样计算attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)if attention_mask is not None:causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]attn_weights = attn_weights + causal_mask# upcast attention to fp32 see https://github.com/huggingface/transformers/pull/17437attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)attn_weights = nn.functional.dropout(attn_weights, p=self.attention_dropout, training=self.training)attn_output = torch.matmul(attn_weights, value_states)attn_output = attn_output.transpose(1, 2).contiguous()attn_output = attn_output.reshape(batch_size, seq_len, self.hidden_size)attn_output = self.o_proj(attn_output)return attn_output

其中num_key_value_groups为每组内要复制的次数,若为1,即退化为多头注意力;若为num_heads,则为多查询注意力。

复制时调用repeat_kv方法,如其名所示,只针对key和value:

def repeat_kv(hidden_states: Tensor, n_rep: int) -> Tensor:"""The hidden states go from (batch, num_key_value_heads, seq_len, head_dim) to (batch, num_attention_heads, seq_len, head_dim)n_rep is the number of repeat times."""batch, num_key_value_heads, seq_len, head_dim = hidden_states.shapeif n_rep == 1:# do nothingreturn hidden_states# add a new dimension and repeat n_rep timeshidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, seq_len, head_dim)# reshape to (batch, num_attention_heads, seq_len, head_dim)return hidden_states.reshape(batch, num_key_value_heads * n_rep, seq_len, head_dim)

有了分组查询注意力,下面我们来看如何应用上篇文章3介绍的旋转位置编码到query和key上。

应用旋转位置编码

注意,实现的时候要考虑维度,因此代码和上篇文章的旋转位置编码3有所不同。

首先,我们实现RotaryEmbedding,它缓存了频率张量inv_freq的计算。

class RotaryEmbedding(nn.Module):def __init__(self, dim: int, max_position_embeddings: int = 2048, theta: int = 10000):super().__init__()self.dim = dim  # head dimself.max_position_embeddings = max_position_embeddingsself.theta = thetainv_freq = 1.0 / (theta** (torch.arange(0, self.dim, 2, dtype=torch.int64).float() / self.dim))self.register_buffer("inv_freq", inv_freq, persistent=False)# 不需要计算梯度@torch.no_grad()def forward(self, position_ids: torch.LongTensor):freqs = torch.outer(position_ids, self.inv_freq).float()return torch.polar(torch.ones_like(freqs), freqs)

该实现修改自旋转位置编码文章3中的precompute_freqs_cis函数。

然后我们改写apply_rotary_emb函数,主要是确定了输入和输出维度的正确性:

def apply_rotary_emb(q: Tensor, k: Tensor, freq_cis: Tensor):"""Args:q (Tensor): (batch_size, num_heads, seq_len, head_dim)k (Tensor): (batch_size, num_key_value_heads, seq_len, head_dim)freq_cis (Tensor): (seq_len, batch_size)"""# q_ (batch_size, num_heads, seq_len, head_dim // 2, 2)q_ = q.float().reshape(*q.shape[:-1], -1, 2)# k_ (batch_size, num_key_value_heads, seq_len, head_dim // 2, 2)k_ = k.float().reshape(*k.shape[:-1], -1, 2)# turn to complex# q_ (batch_size, num_heads, seq_len, head_dim // 2)q_ = torch.view_as_complex(q_)# k_ (batch_size, num_key_value_heads, seq_len, head_dim // 2)k_ = torch.view_as_complex(k_)# freq_cis (batch_size, 1, seq_len, 1)freq_cis = reshape_for_broadcast(freq_cis, q_)# 应用旋转操作,然后将结果转回实数# view_as_real (batch_size, num_heads, seq_len, head_dim // 2, 2)# xq_out (batch_size, num_heads, seq_len, head_dim)xq_out = torch.view_as_real(q_ * freq_cis).flatten(-2)# view_as_real (batch_size, num_key_value_heads, seq_len, head_dim // 2, 2)# xk_out (batch_size, num_key_value_heads, seq_len, head_dim)xk_out = torch.view_as_real(k_ * freq_cis).flatten(-2)return xq_out.type_as(q), xk_out.type_as(k)

其中需要调用reshape_for_broadcast将频率张量的维度从(seq_len, batch_size)调整到(batch_size, 1, seq_len, 1)

def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):"""Args:freqs_cis (torch.Tensor): (seq_len, batch_size)x (torch.Tensor): (batch_size, num_heads, seq_len, head_dim // 2)"""# enumerate(x.shape) = [(0, batch_size), (1, num_heads), (2, seq_len), (3, head_dim // 2)]# (batch_size, 1, seq_len, 1)shape = [d if i == 0 or i == 2 else 1 for i, d in enumerate(x.shape)]return freqs_cis.view(*shape)

我们把每个维度都写出来就不会出错。

再确保下repeat_kv函数的维度:

def repeat_kv(hidden_states: Tensor, n_rep: int) -> Tensor:"""The hidden states go from (batch, num_key_value_heads seq_len, head_dim) to (batch, num_attention_heads, seq_len, head_dim)n_rep is the number of repeat times."""batch, num_key_value_heads, seq_len, head_dim = hidden_states.shapeif n_rep == 1:# do nothingreturn hidden_states# add a new dimension and repeat n_rep timeshidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, seq_len, head_dim)# reshape to (batch, num_attention_heads, seq_len, head_dim)return hidden_states.reshape(batch, num_key_value_heads * n_rep, seq_len, head_dim)

最后将旋转位置编码整合到GroupedQueryAttention中:

class GroupedQueryAttention(nn.Module):def __init__(self, args: ModelArgs) -> None:super().__init__()self.hidden_size = args.hidden_sizeself.num_heads = args.num_heads# 每个头的维度计算和之前一样self.head_dim = self.hidden_size // self.num_heads# 保存key/value头数self.num_key_value_heads = args.num_key_value_heads# 每组内要复制的次数,若为1,即退化为多头注意力;若为num_heads,则为多查询注意力self.num_key_value_groups = self.num_heads // args.num_key_value_headsself.attention_dropout = args.attention_dropoutself.max_position_embeddings = args.max_position_embeddingsself.rope_theta = args.thetaself.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)# 注意Key和Value的映射这里节省了参数,加速了推理效率。self.k_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)self.v_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)# 最后的输出映射和之前一样self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False)# 定义了RotaryEmbedding实例self.rotary_emb = RotaryEmbedding(self.head_dim,max_position_embeddings=self.max_position_embeddings,theta=self.rope_theta,)def forward(self,hidden_states: Tensor,attention_mask: Tensor = None,position_ids: torch.LongTensor = None,):batch_size, seq_len, _ = hidden_states.shapequery_states, key_states, value_states = (self.q_proj(hidden_states),self.k_proj(hidden_states),self.v_proj(hidden_states),)# query_states(batch_size, num_heads, seq_len, head_dim)query_states = query_states.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)# 转换为对应的形状# key_states (batch_size, num_key_value_heads, seq_len, head_dim)key_states = key_states.view(batch_size, seq_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)# value_states (batch_size, num_key_value_heads, seq_len, head_dim)value_states = value_states.view(batch_size, seq_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)# 计算频率张量# freq_cis (seq_len, batch_size)freq_cis = self.rotary_emb(position_ids)# 针对query和key应用旋转位置编码# query_states (batch_size, num_heads, seq_len, head_dim)# key_states (batch_size, num_key_value_heads, seq_len, head_dim)query_states, key_states = apply_rotary_emb(query_states, key_states, freq_cis)# 重复num_key_value_groups次,使得和query头数一致# key_states (batch_size, num_heads, seq_len, head_dim)key_states = repeat_kv(key_states, self.num_key_value_groups)# value_states (batch_size, num_heads, seq_len, head_dim)value_states = repeat_kv(value_states, self.num_key_value_groups)# 后面和普通多头注意力一样计算attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)if attention_mask is not None:causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]attn_weights = attn_weights + causal_mask# upcast attention to fp32 see https://github.com/huggingface/transformers/pull/17437attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)attn_weights = nn.functional.dropout(attn_weights, p=self.attention_dropout, training=self.training)attn_output = torch.matmul(attn_weights, value_states)attn_output = attn_output.transpose(1, 2).contiguous()attn_output = attn_output.reshape(batch_size, seq_len, self.hidden_size)attn_output = self.o_proj(attn_output)return attn_output

主要修改是在调用repeat_kv之前应用旋转位置编码到(每个Attention的)query和key中:

# 计算频率张量
# freq_cis (seq_len, batch_size)
freq_cis = self.rotary_emb(position_ids)# 针对query和key应用旋转位置编码
# query_states (batch_size, num_heads, seq_len, head_dim)
# key_states (batch_size, num_key_value_heads, seq_len, head_dim)
query_states, key_states = apply_rotary_emb(query_states, key_states, freq_cis)

这里简单探讨下为什么旋转位置编码只是应用到query和key上,没有应用到value上,考虑Attention的计算公式:
a m , n = exp ⁡ ( q m T k n d ) ∑ j = 1 N exp ⁡ q m T k j d o m = ∑ n = 1 N a m , n v n \begin{aligned} a_{m,n} &= \frac{\exp(\frac{\pmb q^T_m \pmb k_n}{\sqrt d})}{\sum_{j=1}^N \exp \frac{\pmb q^T_m \pmb k_j}{\sqrt d}} \\ \pmb o_m &= \sum_{n=1}^N a_{m,n}\pmb v_n \\ \end{aligned} am,nooom=j=1Nexpd qqqmTkkkjexp(d qqqmTkkkn)=n=1Nam,nvvvn

我们可以看到,实际上只有query和key之间会进行交互(点乘),而value只是用于计算加权和,不参与交互,因此没有必要应用旋转位置编码,但也可以尝试应用到value上。

苏神在博客也说了:“通过在q,k中施行该位置编码,那么效果就等价于相对位置编码,而如果还需要显式的绝对位置信息,则可以同时在v上也施行这种位置编码。总的来说,我们通过绝对位置的操作,可以达到绝对位置的效果,也能达到相对位置的效果。”

最后,进行一个简单的测试:

@dataclass
class ModelArgs:hidden_size: int = 512num_heads: int = 8num_key_value_heads: int = 4attention_dropout: float = 0.1max_position_embeddings: int = 2048theta: int = 10000if __name__ == "__main__":args = ModelArgs()attention = GroupedQueryAttention(args)inputs = torch.randn(32, 16, args.hidden_size)seq_len = inputs.size(1)position_ids = torch.arange(seq_len, dtype=torch.long)print(attention(inputs, position_ids=position_ids).shape)
torch.Size([32, 16, 512])

参考


  1. [论文翻译]GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints ↩︎

  2. Fast Transformer Decoding: One Write-Head is All You Need ↩︎

  3. Llama改进之——RoPE旋转位置编码 ↩︎ ↩︎ ↩︎ ↩︎

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

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

相关文章

易联众智能自动办理平台,AI赋能让数字政务服务“触手可及”

“城乡居民参保怎么办”“要去XX省工作了,帮我办理异地就医备案”……通过口语化的文字、语音提问,易联众智能自动办理平台的AI助理都可以准确理解对话,并依据政策文件给出详细回答,人机对话像聊天一样轻松。 近日,宁德市民王先生高兴地说:“过去办理医保业务不懂流程,容易走弯…

Vue常用自定义指令、纪录篇

文章目录 一、元素尺寸发生变化时二、点击元素外自定义指令三、元素拖拽自定义指令四、防抖自定义指令五、节流自定义指令六、权限判断自定义指令 一、元素尺寸发生变化时 使用场景: 当元素的尺寸发生变化时需要去适配一些元素时。 或者在元素尺寸发生变化时要去适配…

C# 命名空间和 using 指令详解

在C#编程中,命名空间(Namespaces)用于组织代码元素,如类、接口、枚举等,以避免命名冲突。using 指令用于导入命名空间,使得代码中可以方便地引用其中的类型,而不必每次都使用完整的命名空间路径…

WPS/Office(Word、Excel、PPT) 自动测评方法

在各高等、中等院校的计算机类课程中,计算机基本应用技能的上机操作考试,广受重视,大为盛行。其中,office(word、excel、ppt)上机考试最为普遍。于是,实现这类Office文档操作的自动阅卷评分,很有必要。本人最近项目上刚好遇到需要解决这种自动评分的问题,所以再次记录下解决的…

TiDB学习9:Ti Cloud简介

目录 1. 为什么选择TiDB 2. 多租户 3. TiDB架构 4. 什么是TiDB Cloud 5. TiDB Cloud Provider Region 6. TiDB Cloud 入门 6.1 在浏览器中打开TiDB Cloud 6.2 创建您的账户 6.3 Developer Tier 与Dedicated Tier 6.3.1 Developer Tier 6.3.2 Dedicated Tier 6.3.2.…

[HUBUCTF 2022 新生赛]RSAaaa

题目: EXP 就你小子是黑客? 我忘记怎么解密了! 靠你了,大黑阔!(536970330703, 65537) message: 473878130775 40132555282 40132555282 94619939727 72818765591 208015808884 42561234694 159353248388 27748063975 1…

逆天工具一键修复图片,视频去码。简直不要太好用!

今天,我要向您推荐一款功能强大的本地部署软件,它能够在您的计算机上一键修复图片和视频,去除令人不悦的码赛克(轻度马赛克)。这款软件是开源的,并在GitHub上公开可用,您可以免费下载并使用。 …

Scala 柯里化、sortBy方法

Scala高级特性 小白的Scala学习笔记 2024/5/30 8:42 文章目录 Scala高级特性柯里化sortBy方法 柯里化 参数可以写在两个括号里面 object TestKeli {def add(a:Int)(b:Int)abdef main(args: Array[String]): Unit {val res add(22)(33)println(res)} }可以填隐式参数&#x…

vector的功能讲解与底层实现

本文主要介绍vector的内容以及使用和模拟实现。 vector在英文翻译中是矢量的意思,但在c中他的本质是一个顺序表(容器),是一个类模板,(用模板创建变量就要参考我们之前的实例化内容了)用可以改变…

dnsrecon一键开始负载平衡检测(KALI工具系列十四)

目录 1、KALI LINUX简介 2、lbd工具简介 3、在KALI中使用lbd 3.1 测试目标域名是否存在负载不平衡 4、总结 1、KALI LINUX简介 Kali Linux 是一个功能强大、多才多艺的 Linux 发行版,广泛用于网络安全社区。它具有全面的预安装工具和功能集,使其成为…

杭州服务器的性能如何?

挥洒激情,开启杭州服务器的无限可能! 互联网时代,服务器的性能就如同一艘航空母舰,承载着企业的发展梦想,指引着行业的发展方向。而对于杭州服务器,其性能究竟如何?让我来告诉您。 杭州服务器…

撸广告赚金币小游戏app开发

在app上投放广告有哪些注意事项? 在app上投放广告需要注意以下几个方面。 首先,要选择合适的广告形式。根据自己的需求和目标受众,选择合适的广告形式,如横幅广告、插屏广告、视频广告等。不同的广告形式适用于不同的场景和目标…

TCP协议详解及其相关的10个核心机制(面试重点)

TCP协议的报文格式 TCP协议有连接,可靠性传输,面向字节流,全双工。 他的数据格式如图: 根据他的数据格式,在这里我们只知道 16位源端口号(表示客户端这里的端口号),16位目的端口号&…

算法简单笔记4

5月31号,明天决赛,今天脑子也是一滩浆糊,踏马的一道题也做不出来,超级难受,只好简单复盘一下两道之前的题目,看完就差不多了,再学也没啥用了,写完这两题题解我就回去打把steam绝地求…

零基础学会asp.net做AI大模型网站/小程序之五:实战初体验(简单网站教学--动态网页制作)

关注我,持续分享逻辑思维&管理思维&面试题; 可提供大厂面试辅导、及定制化求职/在职/管理/架构辅导; 博主在互联网大厂深耕近二十年,从一线码农做起,到人工智能公司副总裁。希望把过往经验总结出来,帮助到更多同学。有兴趣可关注博主后加个人微信(平台规定文章…

深度学习聚类再升级!新算法实现强悍性能,准确率超98%

深度聚类不仅继承了传统聚类算法的优点,在对高维和非线性数据的处理能力,以及自适应性和抗噪性方面也具有很大优势。 具体来说,结合深度学习的聚类算法通过利用深度神经网络的强大特征提取能力,自动学习和识别数据中的复杂结构和…

lru_cache 装饰器的作用

lru_cache 装饰器的主要作用是缓存函数的结果,以减少不必要的计算。在您的代码中,lru_cache(maxsizeNone) 装饰器被用于 load_model 函数。这意味着当 load_model 函数被多次调用,且参数 model_path 相同时,函数的结果会被缓存&am…

全志H616(BIGTREETECH CB1)和 博通BCM2711(树莓派4B)CPU对比测试

一,实物对比图: BIGTREETECH CB1的底板接口的分布和树莓派4B是一样的,但是没有树莓派的音频接口,底板也不能放到树莓派4B的官方外壳里,因为底板的背面有一个DSI接口,高度超出了。 二,开发板硬…

HBSL-22Q/K定时限过电流继电器 板前接线 JOSEF约瑟

HBSL系列静态定时限过电流继电器 系列型号: HBSL-11A/E静态定时限过电流继电器;HBSL-11A/K静态定时限过电流继电器;HBSL-12A/E静态定时限过电流继电器; HBSL-12A/K静态定时限过电流继电器;HBSL-21A/E静态定时限过电…

JS-09-es6常用知识1

目录 1 模板字符串 1.1 模板字符串基本用法 1.2 模板字符串解决了一些痛点 2 解构赋值 2.1 对象的解构赋值 2.2 函数参数的解构赋值 2.3 补写:属性的简写 3 rest参数 3.1 arguments 3.2 rest参数 3.3 补充:判断数据类型 4 箭头函数 4.1 …