Transformer总结(一):基础介绍(注意力机制、自注意力机制、位置编码、多头注意力机制)


一.注意力机制

1.背景介绍

早在60年代就有了非参数的注意力机制Nadaraya-Watson核回归,使用核回归计算距离,作为权重;在数据足够的条件下可以拟合出原函数;

  • q q q为询问

  • ( K i , V i ) (K_i,V_i) (Ki,Vi)为键值对,键值对之间在transformer中一般是相同的,也允许不同,但一定存在某种联系;更广义来说,三者的维度可以完全不一样

  • f ( q , ( k 1 , v 1 ) , … , ( k m , v m ) ) = ∑ i = 1 m α ( q , k i ) v i f(q,(k_1,v_1),\dots,(k_m,v_m))=\sum_{i=1}^{m} \alpha(q,k_i)v_i f(q,(k1,v1),,(km,vm))=i=1mα(q,ki)vi

  • α ( q , k i ) = s o f t m a x ( a ( q , k i ) ) = e x p ( a ( q , k i ) ) ∑ j = 1 m e x p ( a ( q , k j ) ) \alpha(q,k_i)=softmax(a(q,k_i))=\frac{exp(a(q,k_i))}{\sum_{j=1}^{m}exp(a(q,k_j))} α(q,ki)=softmax(a(q,ki))=j=1mexp(a(q,kj))exp(a(q,ki)),称为注意力权重,也就是处于0~1之间,本质上是键对应的值的概率分布,概率小,选择该键值对的机会就小,对应得到更少的注意力。

其中的重点便是 a ( q , k i ) a(q,k_i) a(q,ki)的设计,称为注意力分数,也可以将其理解为二者间的关联度,当前可以采用两种方法: A d d i t i v e   a t t e n i o n Additive\ attenion Additive attenion d o t − p r o d u c t   a t t e n t i o n dot-product\ attention dotproduct attention

加型注意力机制和点积注意力机制有着相同的计算复杂度,但点积注意力机制运算可以使用高度优化的并行矩阵乘法代码,会更快也更节省空间

2.Additive attenion(加型注意力机制)

  • 一般来说,当查询和键是不同长度的矢量时,可以使用加性注意力作为评分函数

  • 可学参数: h ∗ k h*k hk的矩阵 W k W_k Wk h ∗ q h*q hq的矩阵 W q W_q Wq、长为 h h h的向量 W W W

  • a ( q , k ) = W T t a n h ( W k k + W q q ) a(q,k)=W^T tanh(W_kk+W_qq) a(q,k)=WTtanh(Wkk+Wqq),tanh为激活函数;等价于将key和query合并起来后放入到一个隐藏大小为h,输出大小为1的单隐藏层MLP;h是隐藏单元数,是一个超参数。

class AdditiveAttention(nn.Module):
    """加性注意力"""
    def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
        super(AdditiveAttention, self).__init__(**kwargs)
        self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
        self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
        self.w_v = nn.Linear(num_hiddens, 1, bias=False)
        self.dropout = nn.Dropout(dropout)
    def forward(self, queries, keys, values, valid_lens):
        queries, keys = self.W_q(queries), self.W_k(keys)
        # 通过unsqueeze在指定位置添加维度,实现维度扩展后,
        # queries的形状:(batch_size,查询的个数,1,num_hidden)
        # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
        # 选择在不同的位置添加新的维度是为了在加性注意力中正确地对齐查询和键,以便进行广播相加
        # features 的形状为 (batch_size, num_queries, num_keys, num_hiddens)
        features = queries.unsqueeze(2) + keys.unsqueeze(1)
        features = torch.tanh(features)
        # self.w_v线性变换仅有一个输出,因此从形状中移除最后那个维度。
        # scores的形状:(batch_size,查询的个数,“键-值”对的个数)
        scores = self.w_v(features).squeeze(-1)
        self.attention_weights = masked_softmax(scores, valid_lens)
        # values的形状:(batch_size,“键-值”对的个数,值的维度)
        #最终的返回结果的形状:(batch_size,查询的个数,值的维度)
        #通过加权求和,将所有键值对的加权结果合并成了最终的输出值
        return torch.bmm(self.dropout(self.attention_weights), values)

masked_softmax

​ 掩码Softmax操作的用处在于在处理序列数据时,对于某些位置的输入可能需要进行忽略或者特殊处理。通过使用掩码张量,可以将这些无效或特殊位置的权重设为负无穷大,从而在进行Softmax操作时,使得这些位置的输出为0。
  这种操作通常在序列模型中使用,例如自然语言处理中的文本分类任务。在文本分类任务中,输入是一个句子或一个段落,长度可能不一致。为了保持输入的统一性,需要进行填充操作,使得所有输入的长度相同。然而,在经过填充操作后,一些位置可能对应于填充字符,这些位置的权重应该被忽略。通过使用掩码Softmax操作,可以确保填充位置的输出为0,从而在计算损失函数时不会对填充位置产生影响。

#queries有2个批量,每个批量有 1 个查询,查询向量的长度为 20
#keys有2 个批量,每个批量有 10 个键,每个键的向量长度为 2
queries, keys = torch.normal(0, 1, (2, 1, 20)), torch.ones((2, 10, 2))
# 创建了值矩阵 values,形状为 (2, 10, 4),表示有2个批量,每个批量有10个值,每个值的向量长度为 4
values = torch.arange(40, dtype=torch.float32).reshape(1, 10, 4).repeat(2, 1, 1)
valid_lens = torch.tensor([2, 6])
attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8,
dropout=0.1)
#模型设置为评估模式,以保证在测试阶段不会应用 dropout 等训练阶段的操作
attention.eval()
attention(queries, keys, values, valid_lens)
tensor([[[ 2.0000,  3.0000,  4.0000,  5.0000]],
        [[10.0000, 11.0000, 12.0000, 13.0000]]], grad_fn=<BmmBackward0>)

3.dot-product attention(点积型注意力机制)

  • 查询和键长度相同,可以用缩放点积注意力评分函数,仅含超参数Dropout

  • 当key和query长度均为d时

    a ( q , k ) = < q , k > d a(q,k)=\frac{<q,k>}{\sqrt{d}} a(q,k)=d <q,k> d d d若过大,可能导致内积结果过大,除以 d \sqrt{d} d 可以让它不会太大,也就是对长度不敏感

  • 推广到批量询问: Q Q Q n ∗ d n*d nd的矩阵、 K K K m ∗ d m*d md的矩阵、 V V V m ∗ v m*v mv的矩阵, n n n为批量数、 m m m为键值对个数

    • a ( Q , K ) = Q K T d a(Q,K)=\frac{QK^T}{\sqrt{d}} a(Q,K)=d QKT,注意力分数为 n ∗ m n*m nm的矩阵
    • f = s o f t m a x ( a ( Q , K ) ) V f=softmax(a(Q,K))V f=softmax(a(Q,K))V,最终的结果为 n ∗ v n*v nv的矩阵
class DotProductAttention(nn.Module):
    """缩放点积注意力"""
    def __init__(self, dropout, **kwargs):
        super(DotProductAttention, self).__init__(**kwargs)
        self.dropout = nn.Dropout(dropout)
        # queries的形状:(batch_size,查询的个数,d)
        # keys的形状:(batch_size,“键-值”对的个数,d)
        # values的形状:(batch_size,“键-值”对的个数,值的维度)
        # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
    def forward(self, queries, keys, values, valid_lens=None):
        d = queries.shape[-1]
        # 设置transpose_b=True为了交换keys的最后两个维度
        scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
        self.attention_weights = masked_softmax(scores, valid_lens)
        return torch.bmm(self.dropout(self.attention_weights), values)


二.自注意力机制

  • 给定序列 x 1 , … , x n , ∀ x i ∈ R d x_1,\dots,x_n,\forall x_i\in \mathbb{R}^d x1,,xn,xiRd
  • 自注意力池化层将 x i x_i xi作为query、key、value,来对序列抽取特征得到 y 1 , … , y n y_1,\dots,y_n y1,,yn
    • y i = f ( x i , ( x 1 , x 1 ) , … , ( x n , x n ) ) ∈ R d y_i=f(x_i,(x_1,x_1),\dots,(x_n,x_n))\in \mathbb{R}^d yi=f(xi,(x1,x1),,(xn,xn))Rd
    • x i x_i xi作为query, ( x j , x j ) (x_j,x_j) (xj,xj)作为使用的键值对,此时键值对的内容相同

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 完全并行,但对长序列的计算复杂度高

在这里插入图片描述


三.位置编码

  • 在处理序列数据时,不同于CNN和RNN,自注意力并没有记录位置信息,为了使用序列的顺序信息,通过在输入表示中添加位置编码,来注入绝对的或相对的位置信息

  • 位置编码将位置信息注入到输入里:假设长度为 n n n的序列是 X ∈ R n × d X\in \mathbb{R}^{n\times d} XRn×d

    • 使用位置编码矩阵 P ∈ R n × d P\in \mathbb{R}^{n\times d} PRn×d,输出 X + P X+P X+P作为自编码输入

    • P P P中的元素根据列的奇偶差异,分为: p i , 2 j = sin ⁡ ( i 1000 0 2 j d ) p_{i,2j}=\sin(\frac{i}{10000^{\frac{2j}{d}}}) pi,2j=sin(10000d2ji) p i , 2 j + 1 = cos ⁡ ( i 1000 0 2 j d ) p_{i,2j+1}=\cos(\frac{i}{10000^{\frac{2j}{d}}}) pi,2j+1=cos(10000d2ji) j j j始于0

    • 绝对位置信息:在上述公式中,频率 ω j = 1 1000 0 2 j d \omega_j=\frac{1}{10000^{\frac{2j}{d}}} ωj=10000d2j1,从函数定义中可以得出,频率沿向量维度减小, j j j越大,波长越长;不同词元的位置编码仅由其位置唯一决定

      在这里插入图片描述

    • 相对位置之间的线性关系:选择正弦余弦曲线函数,可以让模型更加轻易的学习关注相对位置信息
      [ cos ⁡ ( δ ω j ) sin ⁡ ( δ ω j ) − sin ⁡ ( δ ω j ) cos ⁡ ( δ ω j ) ] [ p i , 2 j p i , 2 j + 1 ] = [ sin ⁡ ( ( i + δ ) ω j ) cos ⁡ ( ( i + δ ) ω j ) ] = [ p i + δ , 2 j p i + δ , 2 j + 1 ] \begin{bmatrix}\cos(\delta\omega_j) & \sin(\delta\omega_j)\\-\sin(\delta\omega_j) & \cos(\delta\omega_j)\end{bmatrix}\begin{bmatrix}p_{i,2j}\\p_{i,2j+1}\end{bmatrix}=\begin{bmatrix}\sin((i+\delta)\omega_j)\\\cos((i+\delta)\omega_j)\end{bmatrix}=\begin{bmatrix}p_{i+\delta,2j}\\p_{i+\delta,2j+1}\end{bmatrix} [cos(δωj)sin(δωj)sin(δωj)cos(δωj)][pi,2jpi,2j+1]=[sin((i+δ)ωj)cos((i+δ)ωj)]=[pi+δ,2jpi+δ,2j+1]
      通过一个 2 × 2 2\times2 2×2的投影矩阵(与 i i i值无关),便可以实现线性投影,也可以理解为某个单词的位置信息是其他单词位置信息的线性组合,这种线性组合就意味着位置向量中蕴含了相对位置信息。

class PositionalEncoding(nn.Module):
    """位置编码"""
    def __init__(self, num_hiddens, dropout, max_len=1000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(dropout)
        # 创建一个足够长的P,batch_size为1
        self.P = torch.zeros((1, max_len, num_hiddens))
        X = torch.arange(max_len, dtype=torch.float32).reshape(
            -1, 1) / torch.pow(10000, torch.arange(
            0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
        self.P[:, :, 0::2] = torch.sin(X)
        self.P[:, :, 1::2] = torch.cos(X)

    def forward(self, X):
        #self.P第一个参数可通过广播,第二个参数由于输入序列的长度可能小于max_len,因此只取前X.shape[1]个位置的编码
        #to(X.device)确保位置编码矩阵与输入数据张量位于同一设备上,以便执行张量相加操作,增加代码鲁棒性
        X = X + self.P[:, :X.shape[1], :].to(X.device)
        return self.dropout(X)

四.多头注意力机制

在实践中,当给定相同的查询、键和值的集合时, 我们希望模型可以基于相同的注意力机制学习到不同的行为, 然后将不同的行为作为知识组合起来, 捕获序列内各种范围的依赖关系 (例如,短距离依赖和长距离依赖关系)。

给定查询 q ∈ R d q q\in \mathbb{R}^{d_q} qRdq、键 k ∈ R d k k\in \mathbb{R}^{d_k} kRdk和值 v ∈ R d v v\in \mathbb{R}^{d_v} vRdv,每个注意力头的计算方法为:
h i = f ( W i ( q ) q , W i ( k ) k , W i ( v ) v ) ∈ R p v h_i=f(W_i^{(q)}q,W_i^{(k)}k,W_i^{(v)}v)\in\mathbb{R}^{p_v} hi=f(Wi(q)q,Wi(k)k,Wi(v)v)Rpv
多头注意力的输出需要经过另一个线性转换, 它对应着 h h h个头连结后的结果:
W o [ h 1 ⋮ h h ] ∈ R p v W_o\begin{bmatrix}h_1\\\vdots\\h_h\end{bmatrix}\in \mathbb{R}^{p_v} Wo h1hh Rpv

  • 其中可学习的参数包括: W i ( q ) ∈ R p q × d q W_i^{(q)}\in\mathbb{R}^{p_q\times d_q} Wi(q)Rpq×dq W i ( k ) ∈ R p k × d k W_i^{(k)}\in\mathbb{R}^{p_k\times d_k} Wi(k)Rpk×dk W i ( v ) ∈ R p v × d v W_i^{(v)}\in\mathbb{R}^{p_v\times d_v} Wi(v)Rpv×dv W o ∈ R p o × h p v W_o\in\mathbb{R}^{p_o\times hp_v} WoRpo×hpv
  • 代表注意力汇聚的函数 f f f,可以是加性注意力、也可以是缩放点积注意力,不讨论涉及参数

在这里插入图片描述

  • 参数说明
    • num_hiddens对应上述的 p o p_o po,num_heads对应上述的 h h h
    • num_hiddens/num_heads对应上述的 p q p_q pq p k p_k pk p v p_v pv
    • 上述参数满足关系: p q = p k = p v = p o h p_q=p_k=p_v=\frac{p_o}{h} pq=pk=pv=hpo,但在实际代码中将查询、键和值的线性变换输出设置为 p q h = p k h = p v h = p o p_qh=p_kh=p_vh=p_o pqh=pkh=pvh=po,其目的是合并所有头的学习参数,再结合之后的DotProductAttention,实现整体的并行计算
  • transpose_qkv作用说明
    • 初始状态:(batch_size,查询或者“键-值”对的个数,num_hiddens)
    • 变换结果:(batch_size*num_heads,查询或者“键-值”对的个数,num_hiddens/num_heads)
    • 个人理解:理论中,每个头应当使用不同的 W ( q ) W^{(q)} W(q) W ( k ) W^{(k)} W(k) W ( v ) W^{(v)} W(v),但此时仅使用了一套,其实相当于把所有头的学习参数叠加在一起,然后借助transpose_qkv实现子空间划分,每个头能够独立地学习不同的查询、键和值的表示,从而实现多头注意力的效果。
class MultiHeadAttention(nn.Module):
    """多头注意力"""
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 num_heads, dropout, bias=False, **kwargs):
        super(MultiHeadAttention, self).__init__(**kwargs)
        self.num_heads = num_heads
        self.attention = d2l.DotProductAttention(dropout)
        self.W_q = nn.Linear(query_size, num_hiddens, bias=bias)
        self.W_k = nn.Linear(key_size, num_hiddens, bias=bias)
        self.W_v = nn.Linear(value_size, num_hiddens, bias=bias)
        self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias)

    def forward(self, queries, keys, values, valid_lens):
        # queries,keys,values的初始形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
        # 经过变换后,输出的queries,keys,values 的形状:
        # (batch_size*num_heads,查询或者“键-值”对的个数,num_hiddens/num_heads)
        queries = transpose_qkv(self.W_q(queries), self.num_heads)
        keys = transpose_qkv(self.W_k(keys), self.num_heads)
        values = transpose_qkv(self.W_v(values), self.num_heads)
        
        # valid_lens 的形状:(batch_size,)或(batch_size,查询的个数)
        if valid_lens is not None:
            # 在轴0,将第一项(标量或者矢量)复制num_heads次,
            # 然后如此复制第二项,然后诸如此类。
            valid_lens = torch.repeat_interleave(
                valid_lens, repeats=self.num_heads, dim=0)

        output = self.attention(queries, keys, values, valid_lens)
        
		# output的初始形状:(batch_size*num_heads,查询的个数,num_hiddens/num_heads)
        # output_concat的形状:(batch_size,查询的个数,num_hiddens)
        output_concat = transpose_output(output, self.num_heads)
        return self.W_o(output_concat)
def transpose_qkv(X, num_heads):
    """为了多注意力头的并行计算而变换形状"""
    # 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
    # 参数 -1 表示由函数自动计算该维度的大小,以保持原始张量的总大小不变
    # 经过reshape输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads,num_hiddens/num_heads)
    X = X.reshape(X.shape[0], X.shape[1], num_heads, -1)

    #对变换后的张量X进行维度置换,将每个头的信息放在一起,以便并行计算
    # 经过permute输出X的形状:(batch_size,num_heads,查询或者“键-值”对的个数,num_hiddens/num_heads)
    X = X.permute(0, 2, 1, 3)

    # 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数,num_hiddens/num_heads)
    return X.reshape(-1, X.shape[2], X.shape[3])


def transpose_output(X, num_heads):
    """逆转transpose_qkv函数的操作"""
    X = X.reshape(-1, num_heads, X.shape[1], X.shape[2])
    X = X.permute(0, 2, 1, 3)
    return X.reshape(X.shape[0], X.shape[1], -1)
  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
是的,在Transformer中有多头注意力机制和交叉注意力机制多头注意力机制是指将输入分成多个头,每个头都进行注意力计算,最后将它们合并在一起。这种方法可以使模型更好地捕捉输入中的不同特征,从而提高模型的性能。 交叉注意力机制是指在编码器和解码器之间引入注意力机制,以便解码器可以在生成输出时关注编码器中的不同部分。这种方法可以帮助模型更好地理解输入和输出之间的关系,从而提高模型的性能。 下面是一个使用多头注意力机制和交叉注意力机制Transformer模型的示例代码: ```python import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers # 定义一个多头注意力层 class MultiHeadAttention(layers.Layer): def __init__(self, embed_dim, num_heads): super(MultiHeadAttention, self).__init__() self.num_heads = num_heads self.embed_dim = embed_dim if embed_dim % num_heads != 0: raise ValueError( f"embedding dimension = {embed_dim} should be divisible by number of heads = {num_heads}" ) self.projection_dim = embed_dim // num_heads self.query_dense = layers.Dense(embed_dim) self.key_dense = layers.Dense(embed_dim) self.value_dense = layers.Dense(embed_dim) self.combine_heads = layers.Dense(embed_dim) def attention(self, query, key, value): score = tf.matmul(query, key, transpose_b=True) dim_key = tf.cast(tf.shape(key)[-1], tf.float32) scaled_score = score / tf.math.sqrt(dim_key) weights = tf.nn.softmax(scaled_score, axis=-1) output = tf.matmul(weights, value) return output, weights def separate_heads(self, x, batch_size): x = tf.reshape(x, (batch_size, -1, self.num_heads, self.projection_dim)) return tf.transpose(x, perm=[0, 2, 1, 3]) def call(self, inputs): # 获取输入 query, key, value, mask = inputs["query"], inputs["key"], inputs["value"], inputs["mask"] batch_size = tf.shape(query)[0] # 将输入通过全连接层进行变换 query = self.query_dense(query) key = self.key_dense(key) value = self.value_dense(value) # 将输入分成多个头 query = self.separate_heads(query, batch_size) key = self.separate_heads(key, batch_size) value = self.separate_heads(value, batch_size) # 计算注意力 attention, weights = self.attention(query, key, value) # 将多个头合并在一起 attention = tf.transpose(attention, perm=[0, 2, 1, 3]) concat_attention = tf.reshape(attention, (batch_size, -1, self.embed_dim)) output = self.combine_heads(concat_attention) return output # 定义一个Transformer模型 class Transformer(keras.Model): def __init__(self, num_layers, embed_dim, num_heads, fully_connected_dim, input_vocab_size, target_vocab_size, dropout_rate=0.1): super(Transformer, self).__init__() self.embed_dim = embed_dim self.num_layers = num_layers # 定义编码器 self.encoder = keras.Sequential( [layers.Embedding(input_vocab_size, embed_dim),] + [ layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), layers.Dense(fully_connected_dim, activation="relu"), layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), MultiHeadAttention(embed_dim, num_heads), layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), layers.Dense(embed_dim, activation="relu"), layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), ] * num_layers ) # 定义解码器 self.decoder = keras.Sequential( [layers.Embedding(target_vocab_size, embed_dim),] + [ layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), layers.Dense(fully_connected_dim, activation="relu"), layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), MultiHeadAttention(embed_dim, num_heads), layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), MultiHeadAttention(embed_dim, num_heads), layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), layers.Dense(embed_dim, activation="relu"), layers.Dropout(dropout_rate), layers.LayerNormalization(epsilon=1e-6), ] * num_layers ) # 定义输出层 self.final_layer = layers.Dense(target_vocab_size) def call(self, inputs): # 获取输入 input_seq, target_seq, enc_padding_mask, look_ahead_mask, dec_padding_mask = inputs # 将输入通过编码器 enc_output = self.encoder(input_seq) # 将编码器的输出通过解码器 dec_output = self.decoder( target_seq, attention_mask=look_ahead_mask, encoder_output=enc_output ) # 将解码器的输出通过输出层 final_output = self.final_layer(dec_output) return final_output # 创建一个Transformer模型 transformer = Transformer( num_layers=2, embed_dim=32, num_heads=2, fully_connected_dim=32, input_vocab_size=1000, target_vocab_size=1000, dropout_rate=0.1, ) # 定义输入 input_seq = tf.random.uniform((64, 10), dtype=tf.int64, minval=0, maxval=200) target_seq = tf.random.uniform((64, 10), dtype=tf.int64, minval=0, maxval=200) enc_padding_mask = tf.random.uniform((64, 1, 1, 10), dtype=tf.float32, minval=0, maxval=1) look_ahead_mask = tf.random.uniform((64, 1, 10, 10), dtype=tf.float32, minval=0, maxval=1) dec_padding_mask = tf.random.uniform((64, 1, 1, 10), dtype=tf.float32, minval=0, maxval=1) # 运行模型 output = transformer( inputs=(input_seq, target_seq, enc_padding_mask, look_ahead_mask, dec_padding_mask) ) print(output.shape) # 输出:(64, 10, 1000) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值