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

【从0到1搞懂大模型】transformer先导:seq2seq、注意力机制、残差网络等(6)

(一)Seq2Seq&编码器-解码器

1. Seq2Seq

seq2seq 表示序列到到序列,即你的输入是一个序列数据,输出也是一个序列数据,我们常见的 seq2seq 有以下几种类型(区别于基于多个零散特征的分类问题)

  • 一对多

每个输入是独立的,与相邻输入无关,输出是由多个单词(不是字母)组成的序列。

每个单词(而不是一个字母,也不是一句话)是一个输出,为了满足自然语言的特征,单词与单词之间,是有关联的,不是相互独立的。即输入虽然没有序列,但输出是有序列的。

例子:文本生成、音乐生成、图文标注

  • 多对一

多个输入之间是有时序关系。即多个输入,如“我”、“打”、“你”,如果时序关系不同,其含义也是不相同的,即为不同的输入,也就需要有不同的输出。

例子:情感分析、新闻分类、关键词提取

  • 多对多

    • 数量相同的多对多

    输入与输出,都是有多个独立单元组成的序列,但输入和输出单元的个数是相同的。

    例子:视频标注、视频识别行为

    • 数量不相同的多对多

    输入与输出,都是有多个独立单元组成的序列,但输入和输出单元的个数是不一定相同的。

    例子:语音识别、机器翻译

2. 编码器解码器结构

机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出, 我们可以设计一个包含两个主要组件的架构: 第一个组件是一个编码器(encoder): 它接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。 第二个组件是解码器(decoder): 它将固定形状的编码状态映射到长度可变的序列。

假设我们当前的编码器和解码器都是 RNN 结构, 编码器的作用是将输入数据编码成一个特征向量,然后解码器将这个特征向量解码成预测结果

(1) 编码器 

#@save
class Seq2SeqEncoder():
    """用于序列到序列学习的循环神经网络编码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        # 嵌入层
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
                          dropout=dropout)

    def forward(self, X, *args):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X)
        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)
        # 如果未提及状态,则默认为0
        output, state = self.rnn(X)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output, state

(2)解码器

class Seq2SeqDecoder():
    """用于序列到序列学习的循环神经网络解码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,
                          dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]

    def forward(self, X, state):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X).permute(1, 0, 2)
        # 广播context,使其具有与X相同的num_steps
        context = state[-1].repeat(X.shape[0], 1, 1)
        X_and_context = torch.cat((X, context), 2)
        output, state = self.rnn(X_and_context, state)
        output = self.dense(output).permute(1, 0, 2)
        # output的形状:(batch_size,num_steps,vocab_size)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output, state

 (3)串起来

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable

dtype = torch.FloatTensor
char_list = [c for c in 'SEPabcdefghijklmnopqrstuvwxyz']
char_dic = {n:i for i,n in enumerate(char_list)}
seq_data = [['man', 'women'], ['black', 'white'], ['king', 'queen'], ['girl', 'boy'], ['up', 'down'], ['high', 'low']]
seq_len = 8
n_hidden = 128
n_class = len(char_list)
batch_size = len(seq_data)
def make_batch(seq_data):
    batch_size = len(seq_data)
    input_batch,output_batch,target_batch = [],[],[]
    for seq in seq_data:
        for i in range(2):
            seq[i] += 'P' * (seq_len - len(seq[i]))
        input = [char_dic[n] for n in seq[0]]
        output = [char_dic[n] for n in ('S' + seq[1])]
        target = [char_dic[n] for n in (seq[1] + 'E')]
    
        input_batch.append(np.eye(n_class)[input])
        output_batch.append(np.eye(n_class)[output])
        target_batch.append(target)
    
    return Variable(torch.Tensor(input_batch)),Variable(torch.Tensor(output_batch)),Variable(torch.LongTensor(target_batch))

input_batch,output_batch,target_batch = make_batch(seq_data)
class Seq2Seq(nn.Module):
    def __init__(self):
        super(Seq2Seq,self).__init__()
        self.encoder = nn.RNN(input_size = n_class,hidden_size = n_hidden)
        self.decoder = nn.RNN(input_size = n_class,hidden_size = n_hidden)
        self.fc = nn.Linear(n_hidden,n_class)
    def forward(self,enc_input,enc_hidden,dec_input):
        enc_input = enc_input.transpose(0,1)
        dec_input = dec_input.transpose(0,1)
        
        _,h_states = self.encoder(enc_input,enc_hidden)
        outputs,_ = self.decoder(dec_input,h_states)
        outputs = self.fc(outputs)
        return outputs
    
model = Seq2Seq()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),lr=0.001)

for epoch in range(5001):
    hidden = Variable(torch.zeros(1,batch_size,n_hidden))
    optimizer.zero_grad()
    
    outputs = model(input_batch,hidden,output_batch)
    outputs = outputs.transpose(0,1)
    
    loss = 0
    for i in range(batch_size):
        loss += criterion(outputs[i],target_batch[i])
    if (epoch % 500) == 0:
        print('epoch:{},loss:{}'.format(epoch,loss))
    loss.backward()
    optimizer.step()

def translated(word):
    input_batch,output_batch,_ = make_batch([[word,'P'*len(word)]])
    hidden = Variable(torch.zeros(1,1,n_hidden))
    outputs = model(input_batch,hidden,output_batch)
    predict = outputs.data.max(2,keepdim=True)[1]
    decode = [char_list[i] for i in predict]
    end = decode.index('P')
    translated = ''.join(decode[:end])
    print(translated)

 (二)seq2Seq + Attention

可以发现上面的 Seq2Seq 方式只是将编码器的最后一个节点的结果进行了输出,但是对于一个序列长度特别长的特征来说,这种方式无疑将会遗忘大量的前面时间片的特征

我们如何给解码器提供更好的特征呢,与其输入最后一个时间片的结果,不如将每个时间片的输出都提供给解码器。那么解码器如何使用这些特征?——attention

1. 解释 Attention

注意力机制核心:关注重点的信息,忽略非重点信息

这里就涉及到 QKV

怎么理解QKV——以搜索为例

  • ​Query(Q)​​:用户的搜索词(如"巴黎旅游攻略"),表示当前需要解决的问题。
  • ​Key(K)​​:网页的关键词(如"巴黎景点""旅行住宿"),用于匹配Query的相关性。
  • ​Value(V)​​:网页的实际内容(如景点介绍文本),是被提取的信息主体。

注意力机制通过 ​​Q-K匹配确定权重​​,再对 ​​V加权求和提取信息​​。

整体如下图 

2. 详细解释Attention

下面我用更详细的动图和示例解释一下 Attention 是如何工作的,动态情况可以详情见资源包(动图没办法上传了)

 

 

3. 代码解释

(这里不方便写公式直接截图了) 

(1)encoder

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):
        super().__init__()
        self.embedding = nn.Embedding(input_dim, emb_dim)
        self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True)
        self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, src): 
        '''
        src = [src_len, batch_size]
        '''
        src = src.transpose(0, 1) # src = [batch_size, src_len]
        embedded = self.dropout(self.embedding(src)).transpose(0, 1) 
        enc_output, enc_hidden = self.rnn(embedded) 
        s = torch.tanh(self.fc(torch.cat((enc_hidden[-2,:,:], enc_hidden[-1,:,:]), dim = 1)))
        return enc_output, s

(2)Attention

class Attention(nn.Module):
    def __init__(self, enc_hid_dim, dec_hid_dim):
        super().__init__()
        self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim, bias=False)
        self.v = nn.Linear(dec_hid_dim, 1, bias = False)
        
    def forward(self, s, enc_output):
        batch_size = enc_output.shape[1]
        src_len = enc_output.shape[0]
        s = s.unsqueeze(1).repeat(1, src_len, 1)
        enc_output = enc_output.transpose(0, 1)
        energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim = 2)))
        attention = self.v(energy).squeeze(2)
        return F.softmax(attention, dim=1)

 (3) decoder

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):
        super().__init__()
        self.output_dim = output_dim
        self.attention = attention
        self.embedding = nn.Embedding(output_dim, emb_dim)
        self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)
        self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, dec_input, s, enc_output):
        
        dec_input = dec_input.unsqueeze(1)
        embedded = self.dropout(self.embedding(dec_input)).transpose(0, 1) 
        a = self.attention(s, enc_output).unsqueeze(1)
        enc_output = enc_output.transpose(0, 1)
        c = torch.bmm(a, enc_output).transpose(0, 1)
        rnn_input = torch.cat((embedded, c), dim = 2)
        dec_output, dec_hidden = self.rnn(rnn_input, s.unsqueeze(0))
        embedded = embedded.squeeze(0)
        dec_output = dec_output.squeeze(0)
        c = c.squeeze(0)
        pred = self.fc_out(torch.cat((dec_output, c, embedded), dim = 1))
        
        return pred, dec_hidden.squeeze(0)

(4) Seq2Seq

class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.device = device
        
    def forward(self, src, trg, teacher_forcing_ratio = 0.5):
        batch_size = src.shape[1]
        trg_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim
        outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
        enc_output, s = self.encoder(src)
        dec_input = trg[0,:]
        for t in range(1, trg_len):
            dec_output, s = self.decoder(dec_input, s, enc_output)
            outputs[t] = dec_output
            teacher_force = random.random() < teacher_forcing_ratio
            top1 = dec_output.argmax(1) 
            dec_input = trg[t] if teacher_force else top1

        return outputs

之后再进行训练就 ok 啦

上面主要是讲了注意力机制的基本原理和使用方法(也算是之后 transform 的多头自注意力机制的基础)

(三)残差网络

在深度学习中,网络层数增多一般会伴着下面几个问题

1. 计算资源的消耗
2. 模型容易过拟合
3. 梯度消失 / 梯度爆炸问题的产生

问题1可以通过GPU集群来解决,对于一个企业资源并不是很大的问题;

问题2的过拟合通过采集海量数据,并配合Dropout正则化等方法也可以有效避免;

问题3通过Batch Normalization也可以避免。

但有研究发现,随着网络层数的增加,网络发生了退化(degradation)的现象:随着网络层数的增多,训练集loss逐渐下降,然后趋于饱和,当你再增加网络深度的话,训练集loss反而会增大。注意这并不是过拟合,因为在过拟合中训练loss是一直减小的。

这里提供了一种想法:既然深层网络相比于浅层网络具有退化问题,那么是否可以保留深层网络的深度,又可以有浅层网络的优势去避免退化问题呢?

残差网络依旧让非线形层满足 H(w,x),然后从输入直接引入一个短连接到非线形层的输出上,使得整个映射变为

Y = H(w,x) + x

(四)Layer normalization

针对 LN,我们要先了解一下 BN

(1)Batch Normalization

Batch Normalization是2015年一篇论文中提出的数据归一化方法,往往用在深度神经网络中激活层之前。其作用可以加快模型训练时的收敛速度,使得模型训练过程更加稳定,避免梯度爆炸或者梯度消失。

​对同一特征通道的所有样本进行归一化​​,适用于批量数据。

  • ​计算方式​​:对每个特征维度(通道)在批量的所有样本上计算均值和方差。

(2)Layer Normalization

Layer Normalization (LN)​ 对单个样本的所有特征进行归一化​​,独立于批量大小。

  • ​计算方式​​:对每个样本的所有特征维度计算均值和方差。

 

更直观区别如下

 如果用三维表示的话

如果我们将一批文本组成一个batch,那么BN的操作方向是,对每句话的按单词进行操作。但语言文本的复杂性是很高的,任何一个词都有可能放在初始位置,且词序可能并不影响我们对句子的理解。而BN是针对每个位置进行缩放,这不符合NLP的规律。

而LN则是针对一句话进行缩放的,且LN一般用在第三维度,如[batchsize, seq_len, dims]中的dims,一般为词向量的维度,或者是RNN的输出维度等等,这一维度各个特征的量纲应该相同。因此也不会遇到上面因为特征的量纲不同而导致的缩放问题

以上就是在详细了解 transform 前可以提前掌握的知识啦  下篇文章就可以迎来我们最最最最经典的神——transformer 了! 

相关文章:

  • C++ 数据结构之图:从理论到实践
  • React(1)基础入门
  • 【模拟电路】PIN光电二极管和APD雪崩光电二极管
  • I/O进程5
  • fio的资料
  • 基于FPGA的一维时间序列idct变换verilog实现,包含testbench和matlab辅助验证程序
  • L1 第6次课 for循环
  • Python学生信息查询
  • Lesson 11 One good turn deserves another
  • AtCoder Beginner Contest 401 E题 题解
  • 文件包含靶场
  • sql 向Java的映射
  • docker compose搭建博客wordpress
  • 基于springboot的“流浪动物管理系统”的设计与实现(源码+数据库+文档+PPT)
  • Losson 4 NFS(network file system(网络文件系统))
  • 地毯填充luogu
  • 【CS*N是狗】亲测可用!!WIN11上禁用Chrome自动更新IDM插件
  • 使用PyTorch实现目标检测边界框转换与可视化
  • 深入探讨MySQL存储引擎:选择最适合你的数据库解决方案
  • 【微服务】SpringBoot 整合 Lock4j 分布式锁使用详解
  • 澎湃思想周报|《混沌少年时》与青少年社媒禁令;自雇陷阱
  • 前四月国家铁路发送货物12.99亿吨,同比增长3.6%
  • LPR名副其实吗?如果有所偏离又该如何调整?
  • 官方数据显示:我国心血管疾病患者已超3亿人
  • 2人恶意传播刘国梁谣言被处罚,媒体:以法律利剑劈谣斩邪,加快推进依法治体
  • 当“小铁人”遇上青浦,看00后如何玩转长三角铁三