【Deep Learning 5】自编码和Transformer

🌞欢迎来到Pytorch的世界 
🌈博客主页:卿云阁

💌欢迎关注🎉点赞👍收藏⭐️留言📝

🌟本文由卿云阁原创!

📆首发时间:🌹2024年2月19日🌹

✉️希望可以和大家一起完成进阶之路!

🙏作者水平很有限,如果发现错误,请留言轰炸哦!万分感谢!


目录

自编码 (Autoencoder)

AutoEncoder实战

Transformer

Transformer 的输入

Self-Attention(自注意力机制)

Encoder 结构

Decoder 结构

Softmax 预测输出单词

实战小项目

视觉领域的transformer(分类vision transformer)


自编码 (Autoencoder)

     神经网络如何进行非监督形式的学习. 也就是 autoencoder, 自编码。 自编码是一种神经网络的形式。

    可以看出图片其实是经过了压缩,再解压的这一道工序. 当压缩的时候, 原有的图片质量被缩减, 解压时用信息量小却包含了所有关键信息的文件恢复出原本的图片. 为什么要这样做呢?

原来有时神经网络要接受大量的输入信息, 比如输入信息是高清图片时, 输入信息量可能达到上千万, 让神经网络直接从上千万个信息源中学习是一件很吃力的工作. 所以, 何不压缩一下, 提取出原图片中的最具代表性的信息, 缩减输入信息量, 再把缩减过后的信息放进神经网络学习. 这样学习起来就简单轻松了. 所以, 自编码就能在这时发挥作用. 通过将原数据黑色的X 压缩, 解压 成红色的X, 然后通过对比黑红 X ,求出预测误差, 进行反向传递, 逐步提升自编码的准确性. 训练好的自编码中间这一部分就是能总结原数据的精髓. 可以看出, 从头到尾, 我们只用到了输入数据 X, 并没有用到 X 对应的数据标签, 所以也可以说自编码是一种非监督学习. 到了真正使用自编码的时候. 通常只会用到自编码前半部分。

编码器 Encoder

      这部分也叫作 encoder 编码器. 编码器能得到原数据的精髓, 然后我们只需要再创建一个小的神经网络学习这个精髓的数据,不仅减少了神经网络的负担, 而且同样能达到很好的效果.

解码器 Decoder

        至于解码器 Decoder, 我们也能拿它来做点事情. 我们知道, 解码器在训练的时候是要将精髓信息解压成原始信息, 那么这就提供了一个解压器的作用, 甚至我们可以认为是一个生成器 (类似于GAN). 


AutoEncoder实战

     神经网络也能进行非监督学习, 只需要训练数据, 不需要标签数据. 自编码就是这样一种形式. 自编码能自动分类数据, 而且也能嵌套在半监督学习的上面, 用少量的有标签样本和大量的无标签样本学习.这次我们还用 MNIST 手写数字数据来压缩再解压图片.然后用压缩的特征进行非监督分类.

训练数据

自编码只用训练集就好了, 而且只需要训练 training data 的 image, 不用训练 labels.

import torch
import torch.nn as nn
import torch.utils.data as Data
import torchvision

# 超参数
EPOCH = 10
BATCH_SIZE = 64
LR = 0.005
DOWNLOAD_MNIST = True   # 下过数据的话, 就可以设置成 False
N_TEST_IMG = 5          # 到时候显示 5张图片看效果, 如上图一

# Mnist digits dataset
train_data = torchvision.datasets.MNIST(
    root='./mnist/',
    train=True,                                     # this is training data
    transform=torchvision.transforms.ToTensor(),    # Converts a PIL.Image or numpy.ndarray to
                                                    # torch.FloatTensor of shape (C x H x W) and normalize in the range [0.0, 1.0]
    download=DOWNLOAD_MNIST,                        # download it if you don't have it
)
AutoEncoder

AutoEncoder 形式很简单, 分别是 encoder 和 decoder, 压缩和解压, 压缩后得到压缩的特征值, 再从压缩的特征值解压成原图片.

class AutoEncoder(nn.Module):
    def __init__(self):
        super(AutoEncoder, self).__init__()

        # 压缩
        self.encoder = nn.Sequential(
            nn.Linear(28*28, 128),
            nn.Tanh(),
            nn.Linear(128, 64),
            nn.Tanh(),
            nn.Linear(64, 12),
            nn.Tanh(),
            nn.Linear(12, 3),   # 压缩成3个特征, 进行 3D 图像可视化
        )
        # 解压
        self.decoder = nn.Sequential(
            nn.Linear(3, 12),
            nn.Tanh(),
            nn.Linear(12, 64),
            nn.Tanh(),
            nn.Linear(64, 128),
            nn.Tanh(),
            nn.Linear(128, 28*28),
            nn.Sigmoid(),       # 激励函数让输出值在 (0, 1)
        )

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return encoded, decoded

autoencoder = AutoEncoder()
训练

训练, 并可视化训练的过程. 我们可以有效的利用 encoder 和 decoder 来做很多事, 比如这里我们用 decoder 的信息输出看和原图片的对比, 还能用 encoder 来看经过压缩后, 神经网络对原图片的理解. encoder 能将不同图片数据大概的分离开来. 这样就是一个无监督学习的过程.

import torch
from torch.utils.data import DataLoader, TensorDataset
# Mnist digits dataset
train_dataset = torchvision.datasets.MNIST(
    root='./mnist/',
    train=True,                                     # this is training data
    transform=torchvision.transforms.ToTensor(),    # Converts a PIL.Image or numpy.ndarray to
                                                    # torch.FloatTensor of shape (C x H x W) and normalize in the range [0.0, 1.0]
    download=DOWNLOAD_MNIST,                        # download it if you don't have it
)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
optimizer = torch.optim.Adam(autoencoder.parameters(), lr=LR)
loss_func = nn.MSELoss()

for epoch in range(EPOCH):
    for step, (x, b_label) in enumerate(train_loader):
        b_x = x.view(-1, 28*28)   # batch x, shape (batch, 28*28)
        b_y = x.view(-1, 28*28)   # batch y, shape (batch, 28*28)

        encoded, decoded = autoencoder(b_x)

        loss = loss_func(decoded, b_y)      # mean square error
        optimizer.zero_grad()               # clear gradients for this training step
        loss.backward()                     # backpropagation, compute gradients
        optimizer.step()                    # apply gradients
画3D图
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from mpl_toolkits.mplot3d import Axes3D
# 要观看的数据
view_data = train_data.train_data[:200].view(-1, 28*28).type(torch.FloatTensor)/255.
encoded_data, _ = autoencoder(view_data)    # 提取压缩的特征值
fig = plt.figure(2)
ax = Axes3D(fig)    # 3D 图
# x, y, z 的数据值
X = encoded_data.data[:, 0].numpy()
Y = encoded_data.data[:, 1].numpy()
Z = encoded_data.data[:, 2].numpy()
values = train_data.train_labels[:200].numpy()  # 标签值
for x, y, z, s in zip(X, Y, Z, values):
    c = matplotlib.cm.rainbow(int(255*s/9))    # 上色
    ax.text(x, y, z, s, backgroundcolor=c)  # 标位子
ax.set_xlim(X.min(), X.max())
ax.set_ylim(Y.min(), Y.max())
ax.set_zlim(Z.min(), Z.max())
plt.show()


Transformer

       Transformer提出之前,我们大部分用的是循环神经网络(RNN),每一步的输出取决于当前的隐藏状态和当前的输入,等上一个步骤完成后才能进行当前的运算,无法并行计算效率低。而且不擅长处理长文本,词之间的距离越远,影响越弱,为了弥补这一个缺点,后来出现了lstm(长短期记忆网络),但是其也没有解决并行计算的能力,后来Transformer的出现啦,其特点在于自注意力机制,他会注意这个词和其它词语之间的相关性有多强。

首先介绍 Transformer 的整体结构,下图是 Transformer 用于中英文翻译的整体结构:

可以看到 Transformer 由 Encoder 和 Decoder 两个部分组成,Encoder 和 Decoder 都包含 6 个 block。Transformer 的工作流程大体如下:

第一步:获取输入句子的每一个单词的表示向量 XX由单词的 Embedding(Embedding就是从原始数据提取出来的Feature) 和单词位置的 Embedding 相加得到。(很多地方叫做位置向量和词向量

第二步:将得到的单词表示向量矩阵 (如上图所示,每一行是一个单词的表示 x) 传入 Encoder 中,经过 6 个 Encoder block 后可以得到句子所有单词的编码信息矩阵 C,如下图。单词向量矩阵用 Xn*d表示, n 是句子中单词个数,d 是表示向量的维度 (论文中 d=512)。每一个 Encoder block 输出的矩阵维度与输入完全一致。

第三步:将 Encoder 输出的编码信息矩阵 C传递到 Decoder 中,Decoder 依次会根据当前翻译过的单词 1~ i 翻译下一个单词 i+1,如下图所示。在使用的过程中,翻译到单词 i+1 的时候需要通过 Mask (掩盖) 操作遮盖住 i+1 之后的单词。解码器是大语言模型生成一个个词的关键,编码器把各个数据的抽象表示传给解码器,还会先接受一个特殊值,这个值表示输出序列的开头(已经生成的文本),文本要经过嵌入层和位置编码,然后被输入多头注意力层,此时的自注意力只会关注这个词和它前面的词(带掩码的多头注意力)。

上图 Decoder 接收了 Encoder 的编码矩阵 C,然后首先输入一个翻译开始符 "<Begin>",预测第一个单词 "I";然后输入翻译开始符 "<Begin>" 和单词 "I",预测单词 "am",以此类推。这是 Transformer 使用时候的大致流程,接下来是里面各个部分的细节。

Transformer 的输入

Transformer 中单词的输入表示 x单词 Embedding 和位置 Embedding (Positional Encoding)相加得到。

单词 Embedding

       单词的 Embedding 有很多种方式可以获取,例如可以采用 Word2Vec、Glove 等算法预训练得到,也可以在 Transformer 中训练得到。我们输入的文本会被token化,也就是先把输入拆成各个的token,token可以理解成文本的一个基本单位,取决于不同的token化方法,长单词可能会被分成多个token。然后每一个token会用一个整数表示,这个数字被叫做token id。

位置 Embedding

       Transformer 中除了单词的 Embedding,还需要使用位置 Embedding 表示单词出现在句子中的位置。因为 Transformer 不采用 RNN 的结构,而是使用全局信息,不能利用单词的顺序信息,而这部分信息对于 NLP 来说非常重要。所以 Transformer 中使用位置 Embedding 保存单词在序列中的相对或绝对位置。

Self-Attention(自注意力机制)

       上图是论文中 Transformer 的内部结构图,左侧为 Encoder block,右侧为 Decoder block。红色圈中的部分为 Multi-Head Attention,是由多个 Self-Attention组成的,可以看到 Encoder block 包含一个 Multi-Head Attention,而 Decoder block 包含两个 Multi-Head Attention (其中有一个用到 Masked)。Multi-Head Attention 上方还包括一个 Add & Norm 层,Add 表示残差连接 (Residual Connection) 用于防止网络退化,Norm 表示 Layer Normalization,用于对每一层的激活值进行归一化。

       因为 Self-Attention是 Transformer 的重点,所以我们重点关注 Multi-Head Attention 以及 Self-Attention,首先详细了解一下 Self-Attention 的内部逻辑。

 Self-Attention 结构

      上图是 Self-Attention 的结构,在计算的时候需要用到矩阵Q(查询),K(键值),V(值)。在实际中,Self-Attention 接收的是输入(单词的表示向量x组成的矩阵X) 或者上一个 Encoder block 的输出。而Q,K,V正是通过 Self-Attention 的输入进行线性变换得到的。

Q, K, V 的计算

Self-Attention 的输入用矩阵X进行表示,则可以使用线性变阵矩阵WQ,WK,WV计算得到Q,K,V。计算如下图所示,注意 X, Q, K, V 的每一行都表示一个单词。

Self-Attention 的输出

得到矩阵 Q, K, V之后就可以计算出 Self-Attention 的输出了,计算的公式如下:

       公式中计算矩阵QK每一行向量的内积,为了防止内积过大,因此除以 dk 的平方根。Q乘以K的转置后,得到的矩阵行列数都为 n,n 为句子单词数,这个矩阵可以表示单词之间的 attention 强度。下图为Q乘以 K的转置,1234 表示的是句子中的单词。

使用 Softmax 计算每一个单词对于其他单词的 attention 系数,公式中的 Softmax 是对矩阵的每一行进行 Softmax,即每一行的和都变为 1.

得到 Softmax 矩阵之后可以和V相乘,得到最终的输出Z

上图中 Softmax 矩阵的第 1 行表示单词 1 与其他所有单词的 attention 系数,最终单词 1 的输出 Z1 等于所有单词 i 的值 Vi 根据 attention 系数的比例加在一起得到,如下图所示:

Multi-Head Attention

先忽略Mask的作用,下图是n个单头attention构成的多头自注意力层。

       在上一步,我们已经知道怎么通过 Self-Attention 计算得到输出矩阵 Z,而 Multi-Head Attention 是由多个 Self-Attention 组合形成的,下图是论文中 Multi-Head Attention 的结构图。从下图可以看到 Multi-Head Attention 包含多个 Self-Attention 层,首先将输入X分别传递到 h 个不同的 Self-Attention 中,计算得到 h 个输出矩阵Z。下图是 h=8 时候的情况,此时会得到 8 个输出矩阵Z

Multi-Head Attention 将它们拼接在一起 (Concat),然后传入一个Linear层,得到 Multi-Head Attention 最终的输出Z。可以看到 Multi-Head Attention 输出的矩阵Z与其输入的矩阵X的维度是一样的。

代码层面非常简单,单头attention操作如下:

class ScaledDotProductAttention(nn.Module):
    ''' Scaled Dot-Product Attention '''

    def __init__(self, temperature, attn_dropout=0.1):
        super().__init__()
        self.temperature = temperature
        self.dropout = nn.Dropout(attn_dropout)

    def forward(self, q, k, v, mask=None):
        # self.temperature是论文中的d_k ** 0.5,防止梯度过大
        # QxK/sqrt(dk)
        attn = torch.matmul(q / self.temperature, k.transpose(2, 3))

        if mask is not None:
            # 屏蔽不想要的输出
            attn = attn.masked_fill(mask == 0, -1e9)
        # softmax+dropout
        attn = self.dropout(F.softmax(attn, dim=-1))
        # 概率分布xV
        output = torch.matmul(attn, v)

        return output, attn

Multi-Head Attention实现在ScaledDotProductAttention基础上构建:

Encoder 结构

   上图红色部分是 Transformer 的 Encoder block 结构,可以看到是由 Multi-Head Attention, Add & Norm, Feed Forward, Add & Norm 组成的。刚刚已经了解了 Multi-Head Attention 的计算过程,现在了解一下 Add & Norm 和 Feed Forward 部分。

Add & Norm

Add & Norm 层由 Add 和 Norm 两部分组成,其计算公式如下:

其中 X表示 Multi-Head Attention 或者 Feed Forward 的输入,MultiHeadAttention(X) 和 FeedForward(X) 表示输出 (输出与输入 维度是一样的,所以可以相加)。

Add指 X+MultiHeadAttention(X),是一种残差连接,通常用于解决多层网络训练的问题,可以让网络只关注当前差异的部分,在 ResNet 中经常用到:

Norm指 Layer Normalization,通常用于 RNN 结构,Layer Normalization 会将每一层神经元的输入都转成均值方差都一样的,这样可以加快收敛。

 Feed Forward

Feed Forward 层比较简单,是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数,对应的公式如下。

X是输入,Feed Forward 最终得到的输出矩阵的维度与X一致。

组成 Encoder

通过上面描述的 Multi-Head Attention, Feed Forward, Add & Norm 就可以构造出一个 Encoder block,Encoder block 接收输入矩阵 X(n×d) ,并输出一个矩阵 O(n×d) 。通过多个 Encoder block 叠加就可以组成 Encoder。第一个 Encoder block 的输入为句子单词的表示向量矩阵,后续 Encoder block 的输入是前一个 Encoder block 的输出,最后一个 Encoder block 输出的矩阵就是编码信息矩阵 C,这一矩阵后续会用到 Decoder 中。

前馈神经网络层

class PositionwiseFeedForward(nn.Module):
    ''' A two-feed-forward-layer module '''

    def __init__(self, d_in, d_hid, dropout=0.1):
        super().__init__()
        # 两个fc层,对最后的512维度进行变换
        self.w_1 = nn.Linear(d_in, d_hid) # position-wise
        self.w_2 = nn.Linear(d_hid, d_in) # position-wise
        self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        residual = x

        x = self.w_2(F.relu(self.w_1(x)))
        x = self.dropout(x)
        x += residual

        x = self.layer_norm(x)

        return x

单个编码层代码如下所示:

class EncoderLayer(nn.Module):
    def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
        super(EncoderLayer, self).__init__()
        self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
        self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)

    def forward(self, enc_input, slf_attn_mask=None):
        # Q K V是同一个,自注意力
        # enc_input来自源单词嵌入向量或者前一个编码器输出
        enc_output, enc_slf_attn = self.slf_attn(
            enc_input, enc_input, enc_input, mask=slf_attn_mask)
        enc_output = self.pos_ffn(enc_output)
        return enc_output, enc_slf_attn

将上述编码过程重复n遍即可,除了第一个模块输入是单词嵌入向量与位置编码的和外,其余编码层输入是上一个编码器输出即后面的编码器输入不需要位置编码向量。如果考虑n个编码器的运行过程,如下所示:

class Encoder(nn.Module):
    def __init__(
            self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
            d_model, d_inner, pad_idx, dropout=0.1, n_position=200):
        # nlp领域的词嵌入向量生成过程(单词在词表里面的索引idx-->d_word_vec长度的向量)
        self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
        # 位置编码
        self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
        self.dropout = nn.Dropout(p=dropout)
        # n个编码器层
        self.layer_stack = nn.ModuleList([
            EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
            for _ in range(n_layers)])
        # 层归一化
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)

    def forward(self, src_seq, src_mask, return_attns=False):
        # 对输入序列进行词嵌入,加上位置编码
        enc_output = self.dropout(self.position_enc(self.src_word_emb(src_seq)))
        enc_output = self.layer_norm(enc_output)
        # 作为编码器层输入
        for enc_layer in self.layer_stack:
            enc_output, _ = enc_layer(enc_output, slf_attn_mask=src_mask)
        return enc_output
Decoder 结构

上图红色部分为 Transformer 的 Decoder block 结构,与 Encoder block 相似,但是存在一些区别:

  • 包含两个 Multi-Head Attention 层。
  • 第一个 Multi-Head Attention 层采用了 Masked 操作。
  • 第二个 Multi-Head Attention 层的K, V矩阵使用 Encoder 的编码信息矩阵C进行计算,而Q使用上一个 Decoder block 的输出计算。
  • 最后有一个 Softmax 层计算下一个翻译单词的概率。
第一个 Multi-Head Attention

Decoder block 的第一个 Multi-Head Attention 采用了 Masked 操作,因为在翻译的过程中是顺序翻译的,即翻译完第 i 个单词,才可以翻译第 i+1 个单词。通过 Masked 操作可以防止第 i 个单词知道 i+1 个单词之后的信息。下面以 "我是一个学生" 翻译成 "I am a student" 为例,了解一下 Masked 操作。

Decoder 可以在训练的过程中使用 Teacher Forcing 并且并行化训练,即将正确的单词序列 (<Begin> I am a student) 和对应输出 (I am a student<end>) 传递到 Decoder。那么在预测第 i 个输出时,就要将第 i+1 之后的单词掩盖住,注意 Mask 操作是在 Self-Attention 的 Softmax 之前使用的,下面用 0 1 2 3 4 5 分别表示 "<Begin> I am a student<end>"。

第一步:是 Decoder 的输入矩阵和 Mask 矩阵,输入矩阵包含 "<Begin> I am a student" (0, 1, 2, 3, 4) 五个单词的表示向量,Mask 是一个 5×5 的矩阵。在 Mask 可以发现单词 0 只能使用单词 0 的信息,而单词 1 可以使用单词 0, 1 的信息,即只能使用之前的信息。

第二步:接下来的操作和之前的 Self-Attention 一样,通过输入矩阵X计算得到Q,K,V矩阵。然后计算Q。

第三步:进行 Softmax,计算 attention score,我们在 Softmax 之前需要使用Mask矩阵遮挡住每一个单词之后的信息,遮挡操作如下:

每一行的和都为 1。但是单词 0 在单词 1, 2, 3, 4 上的 attention score 都为 0。

第四步:得到输出 Z,则单词 1 的输出向量 Z1 是只包含单词 1 信息的。

第五步:通过上述步骤就可以得到一个 Mask Self-Attention 的输出矩阵 Zi ,然后和 Encoder 类似,通过 Multi-Head Attention 拼接多个输出Zi 然后计算得到第一个 Multi-Head Attention 的输出ZZ与输入X维度一样。

第二个 Multi-Head Attention

Decoder block 第二个 Multi-Head Attention 变化不大, 主要的区别在于其中 Self-Attention 的 K, V矩阵不是使用 上一个 Decoder block 的输出计算的,而是使用 Encoder 的编码信息矩阵 C 计算的。

根据 Encoder 的输出 C计算得到 K, V,根据上一个 Decoder block 的输出 Z 计算 Q (如果是第一个 Decoder block 则使用输入矩阵 X 进行计算),后续的计算方法与之前描述的一致。

这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)。

整个解码器代码和编码器非常类似:

class DecoderLayer(nn.Module):
    ''' Compose with three layers '''

    def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
        super(DecoderLayer, self).__init__()
        self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
        self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
        self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)

    def forward(
            self, dec_input, enc_output,
            slf_attn_mask=None, dec_enc_attn_mask=None):
        # 标准的自注意力,QKV=dec_input来自目标单词嵌入或者前一个解码器输出
        dec_output, dec_slf_attn = self.slf_attn(
            dec_input, dec_input, dec_input, mask=slf_attn_mask)
        # KV来自最后一个编码层输出enc_output,Q来自带有mask的self.slf_attn输出
        dec_output, dec_enc_attn = self.enc_attn(
            dec_output, enc_output, enc_output, mask=dec_enc_attn_mask)
        dec_output = self.pos_ffn(dec_output)
        return dec_output, dec_slf_attn, dec_enc_attn

考虑n个解码器模块,其整体流程为:

class Decoder(nn.Module):
    def __init__(
            self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
            d_model, d_inner, pad_idx, n_position=200, dropout=0.1):
        # 目标单词嵌入
        self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx)
        # 位置嵌入向量
        self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
        self.dropout = nn.Dropout(p=dropout)
        # n个解码器
        self.layer_stack = nn.ModuleList([
            DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
            for _ in range(n_layers)])
        # 层归一化
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)

    def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):
        # 目标单词嵌入+位置编码
        dec_output = self.dropout(self.position_enc(self.trg_word_emb(trg_seq)))
        dec_output = self.layer_norm(dec_output)
        # 遍历每个解码器
        for dec_layer in self.layer_stack:  
            # 需要输入3个信息:目标单词嵌入+位置编码、最后一个编码器输出enc_output
            # 和dec_enc_attn_mask,解码时候不能看到未来单词信息
            dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
                dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
        return dec_output
Softmax 预测输出单词

在进行编码器-解码器后输出依然是向量,需要在后面接fc+softmax层进行分类训练。

Decoder block 最后的部分是利用 Softmax 预测下一个单词,在之前的网络层我们可以得到一个最终的输出 Z,因为 Mask 的存在,使得单词 0 的输出 Z0 只包含单词 0 的信息,如下:

Softmax 根据输出矩阵的每一行预测下一个单词:

 这就是 Decoder block 的定义,与 Encoder 一样,Decoder 是由多个 Decoder block 组合而成。

self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)
dec_output, *_ = self.model.decoder(trg_seq, trg_mask, enc_output, src_mask)
return F.softmax(self.model.trg_word_prj(dec_output), dim=-1)
实战小项目
import torch
import torch.nn as nn

# 定义Transformer模型
class TransformerModel(nn.Module):
    def __init__(self, vocab_size, embed_size, num_heads, num_layers):
        super(TransformerModel, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.transformer = nn.Transformer(
            d_model=embed_size,
            nhead=num_heads,
            num_encoder_layers=num_layers,
            num_decoder_layers=num_layers
        )
        self.fc = nn.Linear(embed_size, vocab_size)

    def forward(self, src, tgt):
        src = self.embedding(src)
        tgt = self.embedding(tgt)

        output = self.transformer(src, tgt)
        output = self.fc(output)

        return output

# 示例:创建一个简单的Transformer模型
vocab_size = 1000  # 词汇表大小
embed_size = 256  # 词嵌入维度
num_heads = 8      # 多头注意力的头数
num_layers = 4     # 编码器和解码器的层数

model = TransformerModel(vocab_size, embed_size, num_heads, num_layers)

# 定义输入
src_sequence = torch.randint(0, vocab_size, (10, 32))  # (sequence_length, batch_size)
tgt_sequence = torch.randint(0, vocab_size, (20, 32))  # (sequence_length, batch_size)

# 前向传播
output_sequence = model(src_sequence, tgt_sequence)
print("Output Shape:", output_sequence.shape)

这个代码定义了一个简单的Transformer模型,并使用随机生成的输入进行了前向传播。

  1. TransformerModel 类定义了Transformer模型的结构。该模型由一个嵌入层(embedding layer)、一个Transformer模块和一个全连接层组成。嵌入层用于将输入序列中的每个标记映射到一个高维空间,Transformer模块执行自注意力机制,全连接层用于将Transformer的输出映射回词汇表大小。

  2. forward 方法接受两个输入序列 srctgt,它们分别表示源序列和目标序列。这两个序列首先通过嵌入层进行嵌入,然后传递到Transformer模块。最后,通过全连接层进行输出。

  3. 示例中创建了一个TransformerModel实例,设置了一些超参数(如词汇表大小、嵌入维度、多头注意力的头数和层数)。

  4. 随机生成了一个源序列 src_sequence 和一个目标序列 tgt_sequence,这两个序列的形状分别为 (10, 32) 和 (20, 32)。

  5. 将这两个序列传递给模型的 forward 方法进行前向传播,得到输出序列 output_sequence

  6. 打印输出序列的形状。


视觉领域的transformer(分类vision transformer)

论文题目:An Image is Worth 16x16 Words:Transformers for Image Recognition at Scale

论文地址:https://arxiv.org/abs/2010.11929

github: https://github.com/lucidrains/v

   

其做法超级简单,只含有编码器模块,本文出发点是彻底抛弃CNN,以前的cv领域虽然引入transformer,但是或多或少都用到了cnn或者rnn,本文就比较纯粹了,整个算法几句话就说清楚了,下面直接分析。

图片分块和降维

因为transformer的输入需要序列,所以最简单做法就是把图片切分为patch,然后拉成序列即可。 假设输入图片大小是256x256,打算分成64个patch,每个patch是32x32像素

x = rearrange(img, 'b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=p, p2=p)

这个写法是采用了爱因斯坦表达式,具体是采用了einops库实现,内部集成了各种算子,rearrange就是其中一个,非常高效。不懂这种语法的请自行百度。p就是patch大小,假设输入是b,3,256,256,则rearrange操作是先变成(b,3,8x32,8x32),最后变成(b,8x8,32x32x3)即(b,64,3072),将每张图片切分成64个小块,每个小块长度是32x32x3=3072,也就是说输入长度为64的图像序列,每个元素采用3072长度进行编码。

考虑到3072有点大,故作者先进行降维:

# 将3072变成dim,假设是1024
self.patch_to_embedding = nn.Linear(patch_dim, dim)
x = self.patch_to_embedding(x)

仔细看论文上图,可以发现假设切成9个块,但是最终到transfomer输入是10个向量,额外追加了一个0和。为啥要追加?原因是我们现在没有解码器了,而是编码后直接就进行分类预测,那么该编码器就要负责一点点解码器功能,那就是:需要一个类似开启解码标志非常类似于标准transformer解码器中输入的目标嵌入向量右移一位操作。试下如果没有额外输入,9个块输入9个编码向量输出,那么对于分类任务而言,我应该取哪个输出向量进行后续分类呢?选择任何一个都说不通,所以作者追加了一个可学习嵌入向量输入。那么额外的可学习嵌入向量为啥要设计为可学习,而不是类似nlp中采用固定的token代替?个人不负责任的猜测这应该就是图片领域和nlp领域的差别,nlp里面每个词其实都有具体含义,是离散的,但是图像领域没有这种真正意义上的离散token,有的只是一堆连续特征或者图像像素,如果不设置为可学习,那还真不知道应该设置为啥内容比较合适,全0和全1也说不通。 自此现在就是变成10个向量输出,输出也是10个编码向量,然后取第0个编码输出进行分类预测即可。从这个角度看可以认为编码器多了一点点解码器功能。具体做法超级简单,0就是位置编码向量是可学习的patch嵌入向量。

# dim=1024
self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
# 变成(b,64,1024)
cls_tokens = repeat(self.cls_token, '() n d -> b n d', b=b)
# 额外追加token,变成b,65,1024
x = torch.cat((cls_tokens, x), dim=1)
位置编码

位置编码也是必不可少的,长度应该是1024,这里做的比较简单,没有采用sincos编码,而是直接设置为可学习,效果差不多

# num_patches=64,dim=1024,+1是因为多了一个cls开启解码标志
self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))

相邻位置有相近的位置编码向量,整体呈现2d空间位置排布一样。

将patch嵌入向量和位置编码向量相加即可作为编码器输入

x += self.pos_embedding[:, :(n + 1)]
x = self.dropout(x)
编码器前向过程

作者采用的是没有任何改动的transformer,故没有啥说的。

self.transformer = Transformer(dim, depth, heads, mlp_dim, dropout)

假设输入是(b,65,1024),那么transformer输出也是(b,65,1024)

分类head

在编码器后接fc分类器head即可

self.mlp_head = nn.Sequential(
            nn.LayerNorm(dim),
            nn.Linear(dim, mlp_dim),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(mlp_dim, num_classes)
        )

# 65个输出里面只需要第0个输出进行后续分类即可
self.mlp_head(x[:, 0])

到目前为止就全部写完了,是不是非常简单,外层整体流程为:

class ViT(nn.Module):
    def __init__(self, *, image_size, patch_size, num_classes, dim, depth, heads, mlp_dim, channels=3, dropout=0.,emb_dropout=0.):
        super().__init__()
        # image_size输入图片大小 256
        # patch_size 每个patch的大小 32
        num_patches = (image_size // patch_size) ** 2  # 一共有多少个patch 8x8=64
        patch_dim = channels * patch_size ** 2  # 3x32x32=3072
        self.patch_size = patch_size  # 32
        # 1,64+1,1024,+1是因为token,可学习变量,不是固定编码
        self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
        # 图片维度太大了,需要先降维
        self.patch_to_embedding = nn.Linear(patch_dim, dim)
        # 分类输出位置标志,否则分类输出不知道应该取哪个位置
        self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
        self.dropout = nn.Dropout(emb_dropout)
        # 编码器
        self.transformer = Transformer(dim, depth, heads, mlp_dim, dropout)
        # 输出头
        self.mlp_head = nn.Sequential(
            nn.LayerNorm(dim),
            nn.Linear(dim, mlp_dim),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(mlp_dim, num_classes)
        )

    def forward(self, img, mask=None):
        p = self.patch_size

        # 先把图片变成64个patch,输出shape=b,64,3072
        x = rearrange(img, 'b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=p, p2=p)
        # 输出 b,64,1024
        x = self.patch_to_embedding(x)
        b, n, _ = x.shape
        # 输出 b,1,1024
        cls_tokens = repeat(self.cls_token, '() n d -> b n d', b=b)
        # 额外追加token,变成b,65,1024
        x = torch.cat((cls_tokens, x), dim=1)
        # 加上位置编码1,64+1,1024
        x += self.pos_embedding[:, :(n + 1)]
        x = self.dropout(x)

        x = self.transformer(x, mask)
        # 分类head,只需要x[0]即可
        # x = self.to_cls_token(x[:, 0])
        x = x[:, 0]
        return self.mlp_head(x)
实战项目(水质分类)

项目和数据集介绍

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
from torchvision.datasets import ImageFolder
from tqdm import tqdm
from einops import rearrange, repeat

class WaterQualityDataset(Dataset):
    def __init__(self, root, transform=None):
        self.dataset = ImageFolder(root, transform=transform)

    def __getitem__(self, index):
        return self.dataset[index]

    def __len__(self):
        return len(self.dataset)

class MultiHeadSelfAttention(nn.Module):
    def __init__(self, dim, heads=8, dropout=0.):
        super(MultiHeadSelfAttention, self).__init__()
        self.heads = heads
        self.head_dim = dim // heads
        self.values = nn.Linear(dim, dim)
        self.keys = nn.Linear(dim, dim)
        self.queries = nn.Linear(dim, dim)
        self.fc_out = nn.Linear(heads * self.head_dim, dim)
        self.dropout = nn.Dropout(dropout)
        self.dim = dim  # Add this line

    def forward(self, values, keys, query, mask):
        # Get number of training examples
        N = query.shape[0]

        value = self.values(values).view(N, -1, self.heads, self.head_dim)
        key = self.keys(keys).view(N, -1, self.heads, self.head_dim)
        query = self.queries(query).view(N, -1, self.heads, self.head_dim)

        values = value.permute(0, 2, 1, 3)
        keys = key.permute(0, 2, 1, 3)
        queries = query.permute(0, 2, 1, 3)

        energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])

        if mask is not None:
            energy = energy.masked_fill(mask == 0, float("-1e20"))

        attention = torch.nn.functional.softmax(energy / (self.dim ** (1 / 2)), dim=3)

        out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
            N, -1, self.heads * self.head_dim
        )

        out = self.fc_out(out)
        out = self.dropout(out)
        return out

class ViT(nn.Module):
    def __init__(self, *, image_size, patch_size, num_classes, dim, depth, heads, mlp_dim, channels=3, dropout=0., emb_dropout=0.):
        super().__init__()
        self.dim = dim  # Define dim here
        num_patches = (image_size // patch_size) ** 2
        patch_dim = channels * patch_size ** 2
        self.patch_size = patch_size
        self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
        self.patch_to_embedding = nn.Linear(patch_dim, dim)
        self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
        self.dropout = nn.Dropout(emb_dropout)
        self.attention = MultiHeadSelfAttention(dim, heads, dropout)
        self.mlp_head = nn.Sequential(
            nn.LayerNorm(dim),
            nn.Linear(dim, mlp_dim),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(mlp_dim, num_classes)
        )

    def forward(self, img, mask=None):
        p = self.patch_size
        x = rearrange(img, 'b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=p, p2=p)
        x = self.patch_to_embedding(x)
        b, n, _ = x.shape
        cls_tokens = repeat(self.cls_token, '() n d -> b n d', b=b)
        x = torch.cat((cls_tokens, x), dim=1)
        x += self.pos_embedding[:, :(n + 1)]
        x = self.dropout(x)

        x = self.attention(x, x, x, mask)
        
        x = x[:, 0]
        return self.mlp_head(x)

# 数据预处理和加载
transform = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.ToTensor(),
])

train_dataset = WaterQualityDataset('D:/dataset', transform=transform)
test_dataset = WaterQualityDataset('D:/dataset', transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=0)

# 初始化ViT模型
model = ViT(image_size=256, patch_size=32, num_classes=2, dim=1024, depth=6, heads=8, mlp_dim=2048, dropout=0.1, emb_dropout=0.1)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 训练模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

num_epochs = 10
for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for images, labels in tqdm(train_loader, desc=f'Epoch {epoch + 1}/{num_epochs}'):
        images, labels = images.to(device), labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()

    print(f'Training Loss: {running_loss / len(train_loader)}')

# 在测试集上验证模型
model.eval()
correct = 0
total = 0
with torch.no_grad():
    for images, labels in tqdm(test_loader, desc='Testing'):
        images, labels = images.to(device), labels.to(device)

        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

accuracy = correct / total
print(f'Test Accuracy: {accuracy}')

参考:

【1】3W字长文带你轻松入门视觉transformer - 知乎 (zhihu.com)

【2】 Transformer模型详解(图解最完整版) - 知乎 (zhihu.com)
【3】 AutoEncoder (自编码/非监督学习) | 莫烦Python (mofanpy.com)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

互联网使用代理IP的作用

互联网使用代理IP主要有以下作用&#xff1a; 1. 隐私保护&#xff1a; - 使用代理IP&#xff0c;用户的原始IP地址会被代理服务器的IP地址所替代&#xff0c;从而隐藏用户的真实身份和地理位置信息&#xff0c;增加网络匿名性。 2. 安全防护&#xff1a; - 代理服务器可以作为…

基于Java (spring-boot)的校园二手交易平台

一、项目介绍 基于Java (spring-boot)的校园二手交易平台&#xff1a;前端主要包含登录注册、求购商品、发布商品、举报、评论五个核心管理模块。后端则主要包含系统设置、物品管理、学生管理、评论管理、举报管理、新闻公告六个核心管理模块。通过此模式不同属性的用户可在系统…

服务器遭受 DDoS 攻击的常见迹象有哪些?

服务器遭受 DDoS 攻击的现象很常见&#xff0c;并且有时不容易预防&#xff0c;有部分原因是它们的形式多种多样&#xff0c;而且黑客手段越来越隐蔽。如果您怀疑自己可能遭受 DDoS 攻击&#xff0c;可以寻找多种迹象。以下是 DDoS 攻击的5个常见迹象&#xff1a; 1.网络流量无…

微信美容预约小程序开发实战教程,快速上手的技术解析

随着移动设备的普及和互联网技术的不断发展&#xff0c;小程序成为了一种越来越受欢迎的轻量级应用程序。特别是在美容美发行业&#xff0c;小程序可以提供便捷的服务&#xff0c;吸引更多的客户。本文将为您提供一份详细的美容美发小程序开发搭建指南。 注册并登录乔拓云平台&…

扫码即可快速协作:草料二维码底部协作面板功能详解

功能介绍 在二维码上添加 底部协作面板 功能后 &#xff0c;扫码后不仅可以阅读设备信息、产品资料等基本信息&#xff0c;还可以在二维码底部输入内容评论并他人快速协作&#xff0c;支持添加图文、语言、手写签名等操作。 底部协作面板是提供给组织内部成员快速协作的功能&…

机器学习之梯度下降法直观理解

形象化举例&#xff0c;由上图所示&#xff0c;假如最开始&#xff0c;我们在一座大山上的某处位置&#xff0c;因为到处都是陌生的不知道下山的路&#xff0c;所以只能摸索着根据直觉&#xff0c;走一步算一步。在此过程中&#xff0c;每走到一个位置的时候&#xff0c;都会求…

2024热门韩剧推荐

《与恶魔有约》详情介绍_与恶魔有约已完结在线观看_与恶魔有约迅雷下载_连续剧_萌番(゜-゜)つロ 年轻人都在用~-BILFUN - www.bilfun.cc 《杀人者的难堪》详情介绍_杀人者的难堪已完结在线观看_杀人者的难堪迅雷下载_连续剧_萌番(゜-゜)つロ 年轻人都在用~-BILFUN - www.bilfun…

※【回溯】【深度优先前序】Leetcode 257. 二叉树的所有路径

※【回溯】【深度优先前序】Leetcode 257. 二叉树的所有路径 解法0 迭代法解法1 深度优先 前序解法2 深度优先 前序 添加了StringBulider ---------------&#x1f388;&#x1f388;257. 二叉树的所有路径 题目链接&#x1f388;&#x1f388;------------------- 解法0 迭代法…

CI/CD部署

什么是CI&#xff0c;什么是CD CI和CD是软件开发中持续集成和持续交付的缩写。 CI代表持续集成&#xff08;Continuous Integration&#xff09;&#xff0c;是一种实践&#xff0c;旨在通过自动化构建、测试和代码静态分析等过程&#xff0c;频繁地将代码变更合并到共享存储…

探索Linux系统中HTTP隧道技术的原理与实践

在Linux的世界里&#xff0c;HTTP隧道技术就像是一个神秘的魔法师&#xff0c;它能让你的网络请求穿越重重障碍&#xff0c;安全地到达目的地。今天&#xff0c;我们就来一起探索这个魔法师的奥秘&#xff0c;看看它是如何在Linux系统中施展魔法的。 首先&#xff0c;我们要明…

如何通过AI作画?

网址&#xff1a;https://huggingface.co/spaces/prodia/fast-stable-diffusion 模板网址&#xff1a;https://prompthero.com/prompt/96ee86ae9e2 打开模板网址&#xff0c;选择Stable Diffusion 选择图片&#xff0c;复制prompt和Negative prompt 打开https://huggingface.…

代码随想录算法训练营第54天 | 123.买卖股票的最佳时机III 188.买卖股票的最佳时机IV

买卖股票的最佳时机III 最多只能完成两笔交易&#xff0c;那么对于每一天的股票可以有5种状态&#xff1a; 没有操作第一次持有股票第一次不持有股票第二次持有股票第二次不持有股票 所以设计的 dp 数组应该有5个维度&#xff0c;分别计算可能得到的最大利润。对于如何递推&a…

leetcode(算法) 70.爬楼梯(python版)

需求 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1 阶 1 阶2 阶 示例 2&#xff1…

Vue的个人笔记

Vue学习小tips ctrl s ----> 运行 alt b <scrip> 链接 <script src"https://cdn.jsdelivr.net/npm/vue2.7.16/dist/vue.js"></script> 插值表达式 指令

学习对象原型中的hasOwnProperty()

hasOwnProperty(propertyName)方法 是用来检测属性是否为对象的自有属性&#xff0c;如果是&#xff0c;返回true&#xff0c;否者false; 参数propertyName指要检测的属性名&#xff1b;

【白嫖8k买的机构vip教程】Jmeter_性能测试(2):性能测试的流程和术语

性能测试的流程 一、准备工作 1、系统基础功能验证 一般情况下&#xff0c;只有在系统基础功能测试验证完成、系统趋于稳定的情况下&#xff0c;才会进行性能测试&#xff0c;否则性能测试是无意义的。2、测试团队组建 根据该项目的具体情况&#xff0c;组建一个几人的性能测试…

【软考问题】-- 10 - 知识精讲 - 项目风险管理

一、基本问题 1&#xff1a;按照可预测性&#xff0c;风险分哪三类&#xff1f; &#xff08;1&#xff09;已知风险&#xff1a;如项目目标不明确&#xff0c; 过分乐观的进度计划&#xff0c; 设计或施工变更和材料价格波动等。&#xff08;2&#xff09;可预测风险&#xff…

unity学习(20)——客户端与服务器合力完成注册功能(2)调试注册逻辑

接着上一节的问题&#xff0c;想办法升级成具备数据库功能的服务器&#xff0c;这个是必须的。 至少在初始化要学会把文件转换为session&#xff0c;新知识&#xff0c;有挑战的。 现在是从LoginHandler.cs跳到了AccountBiz.cs的create&#xff0c;跳度还是很大的。 create函…

ChromeDriver | 谷歌浏览器驱动下载地址 及 浏览器版本禁止更新

在使用selenoum时&#xff0c;需要chrome浏览器的版本和chrome浏览器驱动的版本一致匹配&#xff0c;才能进行自动化测试 一、ChromeDriver驱动镜像网址 国内可以搜到的谷歌浏览器下载地址里面最新的驱动器只有114版本的CNPM Binaries Mirror 在其他博主那找到了最新版本12X的…
最新文章