当前位置: 首页 > news >正文

Transformer前传:Seq2Seq与注意力机制Attention

前言

参考了以下大佬的博客
https://blog.csdn.net/v_july_v/article/details/127411638
https://blog.csdn.net/andy_shenzl/article/details/140146699
https://blog.csdn.net/weixin_42475060/article/details/121101749
https://blog.csdn.net/weixin_43334693/article/details/130189238
https://blog.csdn.net/weixin_42110638/article/details/134016569
https://www.cnblogs.com/nickchen121/p/16470569.html
非常感谢!

Seq2Seq

定义

Seq2Seq是一个Encoder-Decoder结构的网络,它的输入是一个序列,输出也是一个序列,

Encoder使用循环神经网络(RNN,GRU,LSTM等),将一个可变长度的信号序列(输入句子)变为固定维度的向量编码表达,

Decoder使用循环神经网络(RNN,GRU,LSTM等),将这个固定长度的编码变成可变长度的目标信号序列(生成目标语言)

372f36c72e28a3d4a48d8d3b14f594ec.png

image.png

图中的圆角矩形即为cell:可以是RNN,GRU,LSTM等结构

  • 相当于将RNN中的h0h_0h0输入Encoder (将Decoder部分理解成RNNEncoder输出的Encoder state作为h0h_0h0)

Seq2Seq模型通过 端到端(?) 的训练方式,将输入序列和目标序列直接关联起来,避免了传统方法中繁琐的特征工程和手工设计的对齐步骤。这使得模型能够自动学习从输入到输出的映射关系,提高了序列转换任务的性能和效率

端到端(End-to-End Learning)

如何理解这个端到端(End-to-End Learning)?

输入序列到输出序列的全过程都由模型学习,我们不需要进行设计规则或特征

工作原理

Seq2Seq中的编码器使用循环神经网络将输入序列转换为固定维度的上下文向量,而解码器使用这个向量和另一个循环神经网络逐步生成输出序列

编解码器

编码器Encoder

把一个不定长的输入序列x1,⋯ ,xtx_1,\cdots,x_tx1,,xt输出到一个编码状态CCC

  • 编码过程中,编码器会逐个读取输入序列的元素,并更新其内部隐藏状态
  • 编码完成后,编码器将最后一个输出的隐藏状态或经过某种变换的隐藏状态作为上下文向量/编码状态传递给解码器

解码器Decoder

输出yty^tyt的条件概率将基于之前的输出序列y1,⋯ ,yt−1y^1,\cdots,y^{t-1}y1,,yt1和编码器输出的编码状态CCC

  • 在每个时间步ttt上,解码器将根据上一个时间步的输出yt−1y^{t-1}yt1,当前隐藏状态以及编码状态C编码状态C编码状态C来生成当前时间步输出yty^tyt

作用

对于给定的输入序列x1,⋯ ,xt,⋯ ,xTx_1,\cdots,x_t,\cdots,x_Tx1,,xt,,xT,使输出序列y1,⋯ ,yt′,⋯ ,yT′y^1,\cdots,y^{t'},\cdots,y^{T'}y1,,yt,,yT的条件概率值最大

就相当于求:
argmax(P(y1,⋯ ,⋯ ,yT′∣x1,⋯ ,xT)) \begin{align} argmax(P(y^1,\cdots,\cdots,y^{T'}|x_1,\cdots,x_T)) \end{align} argmax(P(y1,,,yTx1,,xT))

得到一组使条件概率最大的输出序列y1,⋯ ,yt′,⋯ ,yT′y^1,\cdots,y^{t'},\cdots,y^{T'}y1,,yt,,yT

最大似然估计

对于上面的做法,我们要使用最大似然估计来求出使得条件概率最大的输出序列y1,⋯ ,yt′,⋯ ,yT′y^1,\cdots,y^{t'},\cdots,y^{T'}y1,,yt,,yT

由于解码器的时间步输出值由前面的时间步输出以及当前时间步隐藏状态和编码状态C共同决定

最大似然估计为:

P(y1,⋯ ,⋯ ,yT′∣x1,⋯ ,xT)=∏t′=1T′P(yt′∣y1,⋯ ,yt′−1,C) P(y^1,\cdots,\cdots,y^{T'}|x_1,\cdots,x_T) = \prod_{t'=1}^{T'}P(y^{t'}|y^1,\cdots,y^{t'-1},C) P(y1,,,yTx1,,xT)=t=1TP(yty1,,yt1,C)
多个概率连乘,最后的值会很小,不利于存储,进行取对数操作

logP(y1,⋯ ,⋯ ,yT′∣x1,⋯ ,xT)=∑t′=1T′logP(yt′∣y1,⋯ ,yt′−1,C) logP(y^1,\cdots,\cdots,y^{T'}|x_1,\cdots,x_T) = \sum_{t'=1}^{T'}logP(y^{t'}|y^1,\cdots,y^{t'-1},C) logP(y1,,,yTx1,,xT)=t=1TlogP(yty1,,yt1,C)

转换为使每个yt′y^{t'}yt的条件概率最大的问题,最终得到最优的输出序列

注意力机制Attention Mechanism

核心逻辑

从关注全部到关注重点

  • Attention机制处理长文本时,能够抓住重点,不丢失重要信息
  • Attention机制类似于人类看图片的逻辑,会将注意力放在图片的焦点上

怎么做注意力

例子

image.png

比如对于这张图片,红色部分是人们更关注的

人看这张图片的过程可以描述为:(查询对象Q(Query))看这张图片(被查询对象V(Values)),判断图片中哪些信息更重要,哪些信息更不重要(计算Q和V里事物的重要度(相关程度))

相似度计算

attention-计算图.png

K (Key):与Q匹配的键,通过计算QK的相似度来得到注意力权重

类比:

  • Q:用户的搜索请求,比如搜索科幻电影
  • K:所有电影标签
  • V:电影本身内容

计算Q与K的相似度,得到权重向量,最终与V加权求和

方法

使用点乘的形式计算相似度(i=1,2,...,n)(i=1,2,...,n)(i=1,2,...,n)

f(Q,Ki)=QT⋅Ki \begin{align} f(Q,K_i) = Q^T\cdot K_i \end{align} f(Q,Ki)=QTKi

感知器

f(Q,Ki)=vTTanh(W⋅Q+U⋅Ki)v,W,U:可学习的参数矩阵 \begin{align} f(Q,K_i) = v^TTanh(W\cdot Q+U \cdot K_i)\\ v,W,U:可学习的参数矩阵\\ \end{align} f(Q,Ki)=vTTanh(WQ+UKi)vWU:可学习的参数矩阵
权重点乘

f(Q,Ki)=Q⋅W⋅KiW:可学习的参数矩阵 \begin{align} f(Q,K_i) = Q\cdot W \cdot K_i \\ W:可学习的参数矩阵 \end{align} f(Q,Ki)=QWKiW:可学习的参数矩阵

权重拼接

f(Q,Ki)=W[Q;Ki]将Q和K向量拼接在一起,通过可学习参数矩阵W做线性变化 \begin{align} f(Q,K_i) = W[Q;K_i] \\ 将Q和K向量拼接在一起,通过可学习参数矩阵W做线性变化 \end{align} f(Q,Ki)=W[Q;Ki]QK向量拼接在一起,通过可学习参数矩阵W做线性变化

softMax+归一化

αi=softMax(f(Q,Ki)dk)dk:缩放因子,是Query向量和K向量的维度 \begin{align} \alpha_i = softMax(\frac{f(Q,K_i)}{\sqrt{d_k}})\\ d_k:缩放因子,是Query向量和K向量的维度 \end{align} αi=softMax(dkf(Q,Ki))dk:缩放因子,是Query向量和K向量的维度
为什么要除以一个dk\sqrt{d_k}dk(?)

假设Q,K的每个元素是独立随机变量,均值为0,方差为1,那么点乘操作之后,均值为0,每个元素的方差为Q和K的维度dkd_kdk

dkd_kdk很大时,分布就会集中在绝对值大的区域,造成概率两极化(接近0/1),造成梯度消失

加权求和

针对计算出的αi,对V中所有的value进行加权求和,得到Attention向量:针对计算出的\alpha_i,对V中所有的value进行加权求和,得到Attention向量:针对计算出的αi,V中所有的value进行加权求和,得到Attention向量:

Attention(Q,K,V)=∑i=1nαi⋅Vi=∑i=1nsoftMax(f(Q,Ki)dk)⋅Vi \begin{align} Attention(Q,K,V) = \sum_{i=1}^{n} \alpha_i \cdot V_i = \sum_{i=1}^{n} softMax(\frac{f(Q,K_i)}{\sqrt{d_k}}) \cdot V_i \end{align} Attention(Q,K,V)=i=1nαiVi=i=1nsoftMax(dkf(Q,Ki))Vi

seq2seq with Attention

在传统的seq2seq中有一个显著的缺点:

长句子问题

最初引入注意力机制是为了解决机器翻译中遇到的长句子(超过50字)性能下降问题

传统的机器翻译在长句子上的效果并不理想,本质原因是:

Encoder-Decoder结构中,Encoder会将输入序列中的所有词统一编码成一个语义特征C后,再进行解码;因此C中就包含了输入序列的所有信息,它的长度就成了限制性能的瓶颈。对于长句子,语义特征C可能并不能存下所有的信息,造成了效果不佳

结构

在最初的seq2seq中,结构为

c6de925e2cdcf2aa7448d481fdcbe44f.png

整个输入序列被编码为一个固定长度的编码状态C

而引入Attention机制之后,整个输入序列不再被编码成一个定长的编码状态C,而是编码成一个向量序列C=[C1,⋯ ,Cn]C=[C_1,\cdots,C_n]C=[C1,,Cn]

核心在于:Ci根据输出生成单词yi不断变化C_i根据输出生成单词y_i不断变化Ci根据输出生成单词yi不断变化

74acee126c5946da911dfb54dfa97673.png

从解码器输出的角度:

  • 每个输出词y都会受到每个输入词x1,x2,x3,x4的影响,但是每个输入词对最终输出词y的影响权重不一样,这个权重由Attention计算得到

工作原理

image.png

此时Decodert时刻有3个输入: t-1时刻的隐藏状态输出和循环神经网络输出,以及Encoder输出的context-vec

编码器Encoder
结构

image.png

双向循环神经网络

Seq2Seq with Attention的编码器中,使用了双向的RNN,GRU,LSTM结构,简称为RNNs

双向RNNs(BiRNNs)由前向RNNs和后向RNNs,分别处理序列前半部分和后半部分

比如对于"I love deep-learning",通过BiRNNs可以同时知道love前面是I,后面是deep-learning

流程
  • 输入处理
    • 将语料分词后的token_id分批次传入embedding层,转换为词向量(Word2Vec形式,one-hot形式…)
      什么是token-id(?)
      就是分词后的单词在词库V中对应的唯一整数索引
  • 特征提取
    • 将前一步得到的词向量作为输入,传入Encoder的特征提取器-使用BiRNNs
  • 状态输出
    • 两个方向的RNNs都会产生隐藏状态输出,将这两个隐藏状态输出拼接,作为1个完整的隐藏状态输出h
解码器Decoder
结构

image.png

流程
  • 隐藏状态
    • 前一时刻的隐藏状态输出st−1s_{t-1}st1
  • 计算相似度score
    • 计算前一时刻隐藏状态输出st−1s_{t-1}st1与Encoder的第j步隐藏状态输出hjh_jhj的相似度score
  • 计算注意力权重
    • 将上一步的相似度score进行softMax归一化
计算上下文向量Context Vec

image.png

得到的hj与权重αij进行加权求和,得到上下文向量 得到的h_j与权重\alpha_{ij}进行加权求和,得到上下文向量 得到的hj与权重αij进行加权求和,得到上下文向量

公式

注意力分数计算

使用Decoder前一时刻隐藏状态st−1s_{t-1}st1,Encoder中第j个时间步的隐藏状态hjh_jhj,进行计算得到注意力权重αij\alpha_{ij}αij

计算匹配得分

匹配得分eij表示Decoder在第i−1步的隐藏状态输出si−1,与Encoder在第j步的隐藏状态输出hj之间的匹配程度 匹配得分e_{ij}表示Decoder在第i-1步的隐藏状态输出s_{i-1},与Encoder在第j步的隐藏状态输出h_j之间的匹配程度 匹配得分eij表示Decoder在第i1步的隐藏状态输出si1,与Encoder在第j步的隐藏状态输出hj之间的匹配程度

  • 方法1:感知器
    eij=vT⋅Tanh(W⋅si−1+U⋅hj) \begin{align} e_{ij} = v^T\cdot Tanh(W\cdot s_{i-1} + U\cdot h_j) \end{align} eij=vTTanh(Wsi1+Uhj)
  • 方法2:使用二次型矩阵计算

二次型:一个向量和矩阵的乘积

eij=si−1⋅Wi−1,j⋅hj e_{ij} = s_{i-1}\cdot W_{i-1,j} \cdot h_j eij=si1Wi1,jhj

计算权重 αij\alpha_{ij}αij:

使用softMax函数进行归一化

αij=eij∑i=1neik \alpha_{ij} = \frac{e_{ij}}{\sum_{i=1}^{n}e_{ik}} αij=i=1neikeij

计算上下文向量 Context Vec

Ci=∑j=1nαij⋅hjn:序列长度Ci:解码步骤i的上下文向量αij:输入序列中第j个词对解码步骤i的注意力权重hj:编码器输出的第j个词的隐藏状态 \begin{align} C_i = \sum_{j=1}^{n}\alpha_{ij}\cdot h_j \\ n:序列长度\\ C_i:解码步骤i的上下文向量\\ \alpha_{ij}:输入序列中第j个词对解码步骤i的注意力权重\\ h_j:编码器输出的第j个词的隐藏状态\\ \end{align} Ci=j=1nαijhjn:序列长度Ci:解码步骤i的上下文向量αij:输入序列中第j个词对解码步骤i的注意力权重hj:编码器输出的第j个词的隐藏状态

比如对于"我 爱 机器 学习" -> "I love machine learning"这个翻译过程

对于i=1,就是要计算 "I""我","爱","机器","学习" 每个时刻输入的相关性

类别

自注意力机制Self Attention

Self-Attention的关键点:Q,K,V都是来源于同一个序列X,通过X找到X里面的关键点

架构

self-attention.png

Self-Attention 有3个输入 Q,K,V

这三个输入来源于同一个输入序列X的词向量x的线性变换

定义三个可学习的参数矩阵:WQ,WK,WVW_Q,W_K,W_VWQ,WK,WV

Q,K,V定义如下:
Q=x⋅WQK=x⋅WKV=x⋅WV \begin{align} Q = x \cdot W_Q\\ K = x\cdot W_K \\ V = x\cdot W_V \end{align} Q=xWQK=xWKV=xWV

流程

先以向量为例子:

  • 得到初始的Q,K,V
    对于序列X中第i个单词Xi的词向量xi  (i ∈(0,n]):Qi=xi⋅WQKi=xi⋅WKVi=xi⋅WV \begin{align} 对于序列X中第i个单词X_i的词向量x_i\ \ (i \ \in (0,n]):\\ Q_i = x_i \cdot W_Q\\ K_i = x_i \cdot W_K\\ V_i = x_i \cdot W_V\\ \end{align} 对于序列X中第i个单词Xi的词向量xi  (i (0,n]):Qi=xiWQKi=xiWKVi=xiWV
    qkv.jpg

  • 计算当前查询Qi与其他Kj(包括当前Qi对应的Ki)的相似度计算当前查询Q_i与其他K_j(包括当前Q_i对应的K_i)的相似度计算当前查询Qi与其他Kj(包括当前Qi对应的Ki)的相似度
    使用点乘计算相似度:sij=Qi⋅Kj   (i,j ∈[0,n]) \begin{align} 使用点乘计算相似度:\\ s_{ij} = Q_i \cdot K_j\ \ \ (i,j\ \in [0,n]) \end{align} 使用点乘计算相似度:sij=QiKj   (i,j [0,n])
    Q-K乘积.jpg

  • 归一化+softMax
    需要对前面得到的sij进行归一化:除以缩放因子dKdK:K的维度sij′=sijdKAi=softMax(S′)=exp(sij′)∑k=1nexp(sik′) \begin{align} 需要对前面得到的s_{ij}进行归一化:除以缩放因子\sqrt{d_K}\\ d_K:K的维度\\ s_{ij}' = \frac{s_{ij}}{\sqrt{d_{K}}}\\ A_i = softMax(S') = \frac{exp(s'_{ij})}{\sum_{k=1}^{n}exp(s'_{ik})} \end{align} 需要对前面得到的sij进行归一化:除以缩放因子dKdK:K的维度sij=dKsijAi=softMax(S)=k=1nexp(sik)exp(sij)

qk-scale.jpg

  • 加权求和
    Zi=Ai⋅Vi \begin{align} Z_i = A_i \cdot V_i\\ \end{align} Zi=AiVi
    qk-softmax.jpg

以矩阵形式:

QKV-矩阵表示.jpg

QKVZ-结果.jpg

对于这个softMax

Qi⋅KiT是一个word2word的attention Map经过softMax后:矩阵内每个元素和为1,相当于每个元素之间都有对应的权重 \begin{align} Q_i\cdot K_i^T是一个word2word的attention\ Map \\ 经过softMax后:\\ 矩阵内每个元素和为1,相当于每个元素之间都有对应的权重 \end{align} QiKiT是一个word2wordattention Map经过softMax:矩阵内每个元素和为1,相当于每个元素之间都有对应的权重
假设输入"I have a dream":

注意力机制矩阵图.jpg

形成了一张4×4的注意力图,每个单词与单词之间都有对应的权重

与普通Attention机制的区别
  • 普通Attention:
    • 在Encoder-Decoder模型中,查询向量Q是Decoder的输出,K和V位于Encoder
  • self Attention:
    • 在Encoder-Decoder模型中,Q,K,V都是Encoder中的元素
与RNNs相比
  • self Attention 将任意两个单词通过一个计算步骤Qi⋅KjQ_i\cdot K_jQiKj动态联系在一起,不会存在RNNs中有效信息随时间序列推移而变得难以捕获的问题
  • 并且RNNs计算是通过时间步序列进行串行,而self Attention可以对一句话中的每个单词单独的进行 Attention 值的计算,也就是说可以并行计算

Masked Self Attention掩码自注意力机制

架构

masked-attention.png

相较于self Attention:在归一化和softMax之间多了一层Masked操作

Masked指的是:在做语言模型(如机器翻译,文本生成…)时,不给模型看到未来的信息

假设在Masked之前,我们已经得到了一个attentionMap:

image.png

Masked要做的就是,只保留下三角矩阵

如这个形式:

mask-attention-map.jpg

如何理解(?)

  • 对于单一个单词"I"
    • 只能和"I"自己有attention
  • 对于第二个单词"have"
    • 只能和"I","have"有attention
  • 对于第三个单词"a"
    • 只能和"I","have","a"有attention
  • 对于第四个单词"dream"
    • 只能和"I","have","a","dream"有attention

Masked后的attentionMap矩阵,经softMax处理后,横轴的attention weight之和为1

mask-attention-map-softmax.jpg

Attention(Q,K,V)计算公式变为:

Attention(Q,K,V)=Zi=softMax(sijdK+Mask)⋅V Attention(Q,K,V) = Z_i = softMax(\frac{s_{ij}}{\sqrt{d_K}}+Mask)\cdot V Attention(Q,K,V)=Zi=softMax(dKsij+Mask)V

Multi-head Self Attention多头自注意力机制

回顾一下self Attention的整个流程:
对于序列X中第i个单词Xi的词向量xi  (i ∈(0,n]):Qi=xi⋅WQ,Ki=xi⋅WK,Vi=xi⋅WV使用点乘计算相似度:sij=Qi⋅Kj   (i,j ∈[0,n])sij′=sijdKAi=softMax(S′)=exp(sij′)∑k=1nexp(sik′)Zi=Ai⋅Vi \begin{align} 对于序列X中第i个单词X_i的词向量x_i\ \ (i \ \in (0,n]):\\ Q_i = x_i \cdot W_Q, K_i = x_i \cdot W_K, V_i = x_i \cdot W_V \\ 使用点乘计算相似度:\\ s_{ij} = Q_i \cdot K_j\ \ \ (i,j\ \in [0,n]) \\ s_{ij}' = \frac{s_{ij}}{\sqrt{d_{K}}}\\ A_i = softMax(S') = \frac{exp(s'_{ij})}{\sum_{k=1}^{n}exp(s'_{ik})}\\ Z_i = A_i \cdot V_i\\ \end{align} 对于序列X中第i个单词Xi的词向量xi  (i (0,n]):Qi=xiWQ,Ki=xiWK,Vi=xiWV使用点乘计算相似度:sij=QiKj   (i,j [0,n])sij=dKsijAi=softMax(S)=k=1nexp(sik)exp(sij)Zi=AiVi

所谓的多头,就是对于self Attention的加权求和得到的Z分割成n份 Z1,Z2,⋯ ,ZnZ_1,Z_2,\cdots,Z_nZ1,Z2,,Zn

n通常取8

经过全连接层后,获得一个新的Z′Z'Z

架构

multi-head-attention.png

输入序列x通过与h个不同的WQ,WK,WV进行线性变换,得到h个不同的Q,K,V,最后得到h个Z,拼接后与另一个矩阵W进行线性变化后,得到Z′输入序列x通过与h个不同的W_Q,W_K,W_V进行线性变换,得到h个不同的Q,K,V,最后得到h个Z,拼接后与另一个矩阵W进行线性变化后,得到Z'输入序列x通过与h个不同的WQ,WK,WV进行线性变换,得到h个不同的Q,K,V,最后得到hZ,拼接后与另一个矩阵W进行线性变化后,得到Z

流程
  • 定义多组W,生成多组Q,K,V
    Qik=xi⋅WkQKik=xi⋅WkKVik=xi⋅WkVk∈[0,h−1] \begin{align} Q_{ik} = x_i\cdot W_{k}^{Q}\\ K_{ik} = x_i\cdot W_{k}^{K}\\ V_{ik} = x_i\cdot W_{k}^{V}\\ k \in [0,h-1] \end{align} Qik=xiWkQKik=xiWkKVik=xiWkVk[0,h1]
  • 得到多组Attention(Q,K,V)
    sik,jk=Qik⋅KjkAik=softMax(sik,jkdK)Zik=Aik⋅Vik \begin{align} s_{ik,jk} = Q_{ik}\cdot K_{jk} \\ A_{ik} = softMax(\frac{s_{ik,jk}}{\sqrt{d_{K}}})\\ Z_{ik} = A_{ik}\cdot V_{ik}\\ \end{align} sik,jk=QikKjkAik=softMax(dKsik,jk)Zik=AikVik
  • 多组输出拼接后,与W0进行线性变换,降低维度
    拼接:Zi=concat([Zi1,Zi2,⋯ ,Zih]))Zi′=W0⋅Zi \begin{align} 拼接:\\ Z_i = concat([Z_{i1},Z_{i2},\cdots,Z_{ih}])) \\ Z_i' = W_0\cdot Z_i \end{align} 拼接:Zi=concat([Zi1,Zi2,,Zih]))Zi=W0Zi

ccaf18c2ba751ab569c0621c6e376f3b.png

注意:除了第0个Encoder,其他Encoder都不需要进行word Embedding;它可以将前面一层Encoder的输出作为输入

为什么(?)
因为还没学到Transformer,所以暂时回答不上来qaq;
学完Transformer的架构后再解答

http://www.lryc.cn/news/617859.html

相关文章:

  • Haystack:面向大模型应用的模块化检索增强生成(RAG)框架
  • 什么情况下会导致日本服务器变慢?解决办法
  • Linux kernel network stack, some good article
  • Flink + Hologres构建实时数仓
  • Spring JDBC
  • TDengine IDMP 基本功能(1.界面布局和操作)
  • 【华为机试】208. 实现 Trie (前缀树)
  • openGauss逻辑备份恢复工具gs_dump/gs_restore
  • AI生成代码时代的商业模式重构:从“软件即产品”到“价值即服务”
  • 大模型落地实践:从技术重构到行业变革的双重突破
  • 亚马逊广告底层逻辑重构:从流量博弈到价值创造的战略升维
  • 思科交换机的不同级别IOS软件有什么区别?
  • Oracle数据库中的Library cache lock和pin介绍
  • Qt——实现”Hello World“、认识对象树与Qt坐标系
  • 力扣109:有序链表转换二叉搜索树
  • Linux下安装jdk
  • 分享一款基于STC8H8K32U-45I-LQFP48单片机的4路数字量输入输出模块
  • STM32——system文件夹
  • Day12 Maven高级
  • 2025牛客多校第七场 双生、象牙 个人题解
  • 大模型提示词工程实践:大语言模型文本转换实践
  • python之uv使用
  • 深度学习和神经网络最基础的mlp,从最基础的开始讲
  • OpenBMC中的snk-psu-manager:架构、原理与应用深度解析
  • 排错000
  • HTML应用指南:利用GET请求获取全国一加授权零售店位置信息
  • 工业相机与智能相机的区别
  • 【05】昊一源科技——昊一源科技 嵌入式笔试, 校招,题目记录及解析
  • 【unity实战】在Unity中实现不规则模型的网格建造系统(附项目源码)
  • 十二、Linux Shell脚本:正则表达式