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

news2025/4/20 11:29:28

引言

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

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

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

多头注意力

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

import torch
from torch import nn, Tensor

import math
from dataclasses import dataclass


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


class MultiHeadAttention(nn.Module):
    def __init__(self, args: ModelArgs) -> None:
        super().__init__()
        self.hidden_size = args.hidden_size
        self.num_heads = args.num_heads
        self.head_dim = self.hidden_size // self.num_heads
        self.attention_dropout = args.attention_dropout

        self.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.shape

        query_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/17437
        attn_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 = 512
    num_heads: int = 8
    num_key_value_heads: int = 4
    attention_dropout: float = 0.1

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

class GroupedQueryAttention(nn.Module):
    def __init__(self, args: ModelArgs) -> None:
        super().__init__()

        self.hidden_size = args.hidden_size
        self.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_heads
        self.attention_dropout = args.attention_dropout

        self.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.shape

        query_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/17437
        attn_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.shape
    if n_rep == 1:
        # do nothing
        return hidden_states
    # add a new dimension and repeat n_rep times
    hidden_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 dim
        self.max_position_embeddings = max_position_embeddings
        self.theta = theta
        inv_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.shape
    if n_rep == 1:
        # do nothing
        return hidden_states
    # add a new dimension and repeat n_rep times
    hidden_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_size
        self.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_heads
        self.attention_dropout = args.attention_dropout

        self.max_position_embeddings = args.max_position_embeddings
        self.rope_theta = args.theta

        self.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.shape

        query_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/17437
        attn_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 = 512
    num_heads: int = 8
    num_key_value_heads: int = 4
    attention_dropout: float = 0.1
    max_position_embeddings: int = 2048
    theta: int = 10000
    
if __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.coloradmin.cn/o/1719487.html

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

相关文章

C++双层Vector容器详解

双层Vector容器 关于C中二维vector使用 双层vector的运用细节 插入元素 //正确的插入方式 vector<vector<int> > A; //A.push_back里必须是vector vector<int> B; B.push_back(0); B.push_back(1); B.push_back(2); A.push_back(B); B.clear(); B.push_back…

AI边缘计算盒子在智慧交通的应用

方案背景 随着经济增长&#xff0c;交通出行需求大幅增长&#xff0c;但道路建设增长缓慢&#xff0c;交通供需矛盾日益显著&#xff0c;中心城区主要道路高峰时段交通拥堵严重&#xff0c;道路交通拥堵逐渐常态化&#xff0c;成为制约城市可持续发展的重要因素之一。 痛点问题…

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

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

3位新加坡华人交易员分享:交易策略、风险管理与心态

交易与投资似乎是一对“双胞胎”,它们都是金融市场中获得收益的重要途径。 区别在于投资者购买自以为长期将有出色业绩的资产组合&#xff0c;并且长期持有这些资产组合&#xff0c;交易者依靠交易技巧借助资产工具价格瞬息波动在短期内产生利润。交易资产的手段有&#xff0c…

MySQL统计字符长度:CHAR_LENGTH(str)

对于SQL表&#xff0c;用于计算字符串中字符数的最佳函数是 CHAR_LENGTH(str)&#xff0c;它返回字符串 str 的长度。 另一个常用的函数 LENGTH(str) 在这个问题中也适用&#xff0c;因为列 content 只包含英文字符&#xff0c;没有特殊字符。否则&#xff0c;LENGTH() 可能会返…

unicloud 云对象

背景和优势 20年前&#xff0c;restful接口开发开始流行&#xff0c;服务器编写接口&#xff0c;客户端调用接口&#xff0c;传输json。 现在&#xff0c;替代restful的新模式来了。 云对象&#xff0c;服务器编写API&#xff0c;客户端调用API&#xff0c;不再开发传输json…

AI图书推荐:使用GitHub Copilot和ChatGPT辅助的Python编程

使用Python编写计算机程序变得更加简单了&#xff01;使用像GitHub Copilot和ChatGPT这样的AI辅助编码工具&#xff0c;将你的想法快速转化为应用程序。人工智能已经改变了我们编写计算机程序的方式。有了像Copilot和ChatGPT这样的工具&#xff0c;你可以用简单的英语描述你想要…

QT5:调用qt键盘组件实现文本框输入

目录 一、环境与目标 二、Qt VirtualKeyboard 1.勾选Qt VirtualKeyboard 2.ui设计流程 3.注意事项及问题点 三、参考代码 参考博客 一、环境与目标 qt版本&#xff1a;5.12.7 windows 11 下的 Qt Designer &#xff08;已搭建&#xff09; 目标&#xff1a;创建一个窗…

【Nacos源码分析01-服务注册与集群间数据是同步】

文章目录 了解CAPBASE理论Nacos支持CP还是AP集群数据同步实现集群数据一致性源码 了解CAP CAP理论的核心观点是&#xff0c;一个分布式系统无法同时完全满足一致性、可用性和分区容错性这三个特性。具体而言&#xff0c;当发生网络分区时&#xff0c;系统必须在一致性和可用性之…

【Vue】v-for中的key

文章目录 一、引入问题二、分析问题 一、引入问题 语法&#xff1a; key属性 "唯一值" 作用&#xff1a;给列表项添加的唯一标识。便于Vue进行列表项的正确排序复用。 为什么加key&#xff1a;Vue 的默认行为会尝试原地修改元素&#xff08;就地复用&#xff09;…

华媒舍:10种欧洲地区媒体发稿推广技巧

1.了解欧洲地区媒体自然环境必须掌握欧洲地区媒体的发稿推广方法&#xff0c;首先要对欧洲地区媒体自然环境有一定的了解。包含不一样国家的主力媒体&#xff0c;他的阅读者人群、销售市场遮盖及其报导风格等。仅有熟悉媒体自然环境&#xff0c;才能更好的制订营销推广策略。 …

【Unity Shader入门精要 第11章】让画面动起来(一)

1. Unity Shader中的时间变量 Shader控制这物体的显示&#xff0c;当向Shader中引入时间变量后&#xff0c;就可以让物体的显示效果随时间发生变化&#xff0c;以实现动画效果。 Unity中常见的时间变量如下表&#xff1a; 变量类型描述_Timefloat4(t/20, t, 2t, 3t)&#xf…

Visual Studio 2022创建dll并调用

需求&#xff1a; 创建A项目&#xff0c;有函数和类&#xff0c;将A项目生成DLL动态链接库 创建B项目&#xff0c;使用A项目生成的dll和lib相关文件 正常项目开发.h用于函数声明&#xff0c;.cpp用于函数实现&#xff0c;但是项目开发往往不喜欢将.cpp函数实现的代码发给别人&…

git使用流程与规范

原文网址&#xff1a;git代码提交流程与规范-CSDN博客 简介 本文git提交流程与规范是宝贵靠谱的经验&#xff0c;它能解决如下问题&#xff1a; 分支差距过大&#xff0c;导致合代码无数的冲突合完代码后发现代码丢失分支不清晰&#xff0c;无法追溯问题合代码耗时很长&…

计算机视觉与模式识别实验1-1 图像的直方图平衡

文章目录 &#x1f9e1;&#x1f9e1;实验流程&#x1f9e1;&#x1f9e1;1.读入图像‘rice.png’&#xff0c;在一个窗口中显示灰度级n64&#xff0c;128和256的图像直方图。2.调解图像灰度范围&#xff0c;观察变换后的图像及其直方图的变化。3.分别对图像‘pout.tif’和‘ti…

unity2D跑酷游戏

项目成果 项目网盘 导入资源包 放入Assets文件Assets资源文件 游戏流程分析 摄像机size调小&#xff0c;让图片占满屏幕 人跑本质&#xff0c;相对运动&#xff0c;图片无限向右滚动 图片720&#xff0c;缩小100倍第二个图片x为7.2每unit px100两张图片刚好挨着连贯 空对象Bg…

(奇幻森林)POLYGON - Enchanted Forest - Nature Biomes - 3D Environment Art by Synty

各种雄伟的树木,装饰着优雅简化的树叶,在头顶形成了一个天堂般的树冠,在苔藓覆盖的森林地面上投下了宁静的咒语。 每一项资产,从引人入胜的环境材料到平缓的波浪状山丘,都经过精心制作,将您带到魔法和自然融合的地方。POLYGON-魔法森林-自然生物技术为数字领域注入真正魔…

搭载算能 BM1684 芯片,面向AI推理计算加速卡

搭载算能 BM1684 芯片&#xff0c;是面向AI推理的算力卡。可集成于服务器、工控机中&#xff0c;高效适配市场上所有AI算法&#xff0c;实现视频结构化、人脸识别、行为分析、状态监测等应用&#xff0c;为智慧城市、智慧交通、智慧能源、智慧金融、智慧电信、智慧工业等领域进…

FreeRtos进阶——软件定时器内部逻辑

在FreeRtos软件定时器&#xff0c;是根据Systick来判断定时是否到达&#xff0c;可以是单次定时器也可以是循环定时器。在创建定时器任务后&#xff0c;在每一次SysTick中断中&#xff0c;会将定时器时钟到的任务写入定时器任务队列。在prvTimerTask任务&#xff08;守护任务&a…

JVM之【运行时数据区1】

JVM简图 运行时数据区简图 一、程序计数器&#xff08;Program Counter Register&#xff09; 1.程序计数器是什么&#xff1f; 程序计数器是JVM内存模型中的一部分&#xff0c;它可以看作是一个指针&#xff0c;指向当前线程所执行的字节码指令的地址。每个线程在执行过程中…