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常用自定义指令、纪录篇

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

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上公开可用,您可以免费下载并使用。 …

vector的功能讲解与底层实现

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

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

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

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

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

算法简单笔记4

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

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

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

全志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 …

SpringBoot-世界杯足球赛网站-28567

Springboot世界杯足球赛网站 摘 要 信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题。针对世界杯足球赛…

jsmug:一个针对JSON Smuggling技术的测试PoC环境

关于jsmug jsmug是一个代码简单但功能强大的JSON Smuggling技术环境PoC,该工具可以帮助广大研究人员深入学习和理解JSON Smuggling技术,并辅助提升Web应用程序的安全性。 背景内容 JSON Smuggling技术可以利用目标JSON文档中一些“不重要”的字节数据实…

C++ 特殊运算符

一 赋值运算符 二 等号作用 三 优先级和结合顺序 四 左值和右值 五 字节数运算符 条件运算符 使用条件运算符注意 逗号运算符 优先级和结合顺序 总结

实战15:bert 命名实体识别、地址解析、人名电话地址抽取系统-完整代码数据

直接看项目视频演示: bert 命名实体识别、关系抽取、人物抽取、地址解析、人名电话地址提取系统-完整代码数据_哔哩哔哩_bilibili 项目演示: 代码: import re from transformers import BertTokenizer, BertForTokenClassification, pipeline import os import torch im…

【linux】docker下nextcloud安装人脸识别插件2

接上文 【linux】docker下nextcloud安装人脸识别插件-CSDN博客 由于作者不再维护此插件,转而开发新的插件 recognize ,因此同步更新插件使用教程。 1、下载人脸识别app:recognize Recognize - Apps - App Store - Nextcloud 2、将插件recog…

Java解析JSON并修改属性值:从JSON到JsonObject的逐层解析

哈喽,大家好,我是木头左! 在Java中,可以使用各种库来解析和操作JSON数据。其中,Gson和Jackson是两个非常流行且功能强大的库。在这篇文章中,将使用Gson库来解析给定的JSON字符串,修改operationB…

python移位操作符(左移位操作符<<、右移位操作符>>)(允许开发者对整数进行位操作,乘2或除2)(左移操作、右移操作)(位掩码操作|=)

文章目录 Python 中的移位操作符详解移位操作符简介左移位操作符 (<<)语法和使用示例代码输出 右移位操作符 (>>)语法和使用示例代码输出 移位操作符的应用场景快速乘除运算&#xff1a;使用移位操作符代替传统的乘法和除法运算&#xff0c;可以提高计算速度。位掩…