datawhale-NLP夏令营

NLP基础实现

数据预处理

清洗和规范化数据

  1. 详解:

    • 去除无关信息:
      删除HTML标签、特殊字符、非文本内容等,确保文本的纯净性(如(掌声)等拟声词)。
    • 统一格式:
      转换所有文本为小写,确保一致性;标准化日期、数字等格式。
    • 分句和分段
      将长文本分割成句子或段落,便于处理和训练。
  2. 代码示例:

    '''分词+分段'''
    MAX_LENGTH: int
    def preprocess_data(en_data: List[str], zh_data: List[str]) -> List[Tuple[List[str], List[str]]]:
        processed_data = []
        for en, zh in zip(en_data, zh_data):
            en_tokens = en_tokenizer(en.lower())[:MAX_LENGTH]
            zh_tokens = zh_tokenizer(zh)[:MAX_LENGTH]
            if en_tokens and zh_tokens:  # 确保两个序列都不为空
                processed_data.append((en_tokens, zh_tokens))
        return processed_data
    
    '''去除无关信息'''
    import torchtext
    from bs4 import BeautifulSoup
    import spacy
    import re
    
    # 加载英文和中文的spaCy模型
    nlp_en = spacy.load("en_core_web_trf")
    nlp_zh = spacy.load("zh_core_web_trf")
    
    def clean_html(text):
        """使用BeautifulSoup来删除HTML标签。"""
        soup = BeautifulSoup(text, "html.parser")
        cleaned_text = soup.get_text()
        return cleaned_text
    
    def clean_special_chars(text):
        """删除特殊字符,只保留字母、数字和常见的标点符号。"""
        pattern = re.compile(r'[^\w\s,.?!,。?!]')
        cleaned_text = re.sub(pattern, '', text)
        return cleaned_text
    
    def clean_non_text_content(text, nlp):
        """使用spaCy删除非文本内容(例如URLs、电子邮件地址)。"""
        doc = nlp(text)
        tokens = [token.text for token in doc if not token.is_stop and token.is_alpha]
        cleaned_text = ' '.join(tokens)
        return cleaned_text
    
    # 示例文本(中英文混合)
    text = "<html>这是一些文本。请联系我们: example@example.com 或访问我们的网站 http://example.com。Some English text.</html>"
    
    # 清理文本
    text = clean_html(text)
    text = clean_special_chars(text)
    text = clean_non_text_content(text, nlp_en)  # 先清理英文
    text = clean_non_text_content(text, nlp_zh)  # 再清理中文
    
    # 访问 网站 English text
    会去掉一些停用词,例如介词、名词、冠词等。
    
  3. 相关内容:

    还会有许多额外步骤:

    • 修正错误:
      • 语法错误
      • 拼写错误
      • 数据类型错误(如数字格式错误)
    • 去除停用词:
      常见的、功能性的词汇(如“的”、“和”、“是”等)
    • 处理缺失值:
      填充缺失数据
      删除含有缺失值的记录
    • 去除重复数据:
      删除完全相同或实质上重复的记录

分词

  1. 详解:
    将句子分解成单词或词素(构成单词的基本组成部分,一个词素可以是一个完整的单词,也可以是单词的一部分,但每一个词素都至少携带一部分语义或语法信息)。

  2. 代码示例:

    '''用jieba对中文进行分词,用spaCy对英文进行分词'''
    from torchtext.data.utils import get_tokenizer
    en_tokenzier = get_tokenizer('basic_english')
    # 确保属于在词汇表中
    en_vocab = Counter(terminology.keys)
    
    '''测试集分词+分段'''
    test_processed = [(en_tokenizer(en.lower())[:MAX_LENGTH], []) for en in test_en if en.strip()]
    
  3. 相关内容:

    基于规则(正则)的分词

    import re
    def regex_tokenize(text):
        return re.findall(r'\b\w+\b', text.lower())
    

    基于统计(隐马尔可夫模型(HMM))的分词

    from hmmlearn.hmm import GaussianHMM
    def hmm_tokenize(text):
        hmm = GaussianHMM(n_components=10)
        hmm.fit(text)
        return hmm.predict(text)
    

    基于深度学习(RNN)的分词

    from keras.models import Sequential
    from keras.layers import LSTM, Dense
    def rnn_tokenize(text):
        model = Sequential()
        model.add(LSTM(128, input_shape=(None, 1)))
        model.add(Dense(1, activation='sigmoid'))
        model.compile(loss='binary_crossentropy', optimizer='adam')
        model.fit(text, text, epochs=100, batch_size=32)
        return model.predict(text)
    

    基于神经网络(CNN)的分词

    from keras.models import Sequential
    from keras.layers import Conv1D, MaxPooling1D, LSTM, Dense
    def cnn_tokenize(text):
        model = Sequential()
        model.add(Conv1D(128, 5, activation='relu', input_shape=(None, 1)))
        model.add(MaxPooling1D(5))
        model.add(LSTM(128))
        model.add(Dense(1, activation='sigmoid'))
        model.compile(loss='binary_crossentropy', optimizer='adam')
        model.fit(text, text, epochs=100, batch_size=32)
        return model.predict(text)
    

    基于注意力机制的分词

    from keras.models import Sequential
    from keras.layers import LSTM, Dense, Attention
    def attention_tokenize(text):
        model = Sequential()
        model.add(LSTM(128, input_shape=(None, 1)))
        model.add(Attention())
        model.add(Dense(1, activation='sigmoid'))
        model.compile(loss='binary_crossentropy', optimizer='adam')
        model.fit(text, text, epochs=100, batch_size=32)
            return model.predict(text)
    

构建词汇表和词向量

  1. 详解:

    • 构建词汇表:
      从训练数据中收集所有出现过的词汇,构建词汇表,并为每个词分配一个唯一的索引。
    • 词向量:
      使用预训练的词向量或自己训练词向量,将词汇表中的词映射到高维空间中的向量,以捕捉语义信息(LLM中的embeding模型用此)。
  2. 代码示例:

    '''构建词汇表'''
    def build_vocab(data: List[Tuple[List[str], List[str]]]):
        en_vocab = build_vocab_from_iterator(
            (en for en, _ in data),
            specials=['<unk>', '<pad>', '<bos>', '<eos>']
        )
        zh_vocab = build_vocab_from_iterator(
            (zh for _, zh in data),
            specials=['<unk>', '<pad>', '<bos>', '<eos>']
        )
        en_vocab.set_default_index(en_vocab['<unk>'])
        zh_vocab.set_default_index(zh_vocab['<unk>'])
        return en_vocab, zh_vocab
    
  3. 相关内容:

    • 加载术语词典:

      def load_terminology_dictionary(dict_file):
          terminology = {}
          with open(dict_file, 'r', encoding='utf-8') as f:
              for line in f:
                  en_term, ch_term = line.strip().split('\t')
                  terminology[en_term] = ch_term
          return terminology
      

序列截断和填充

  1. 详解:

    • 序列截断:
      限制输入序列的长度,降低计算成本,减少冗余信息。
    • 序列填充:
      将所有序列填充至相同的长度,便于批量处理。通常使用<PAD>标记填充。
  2. 代码示例:

    en_batch = nn.utils.rnn.pad_sequence(en_batch, batch_first=True, padding_value=en_vocab['<pad>'])
    zh_batch = nn.utils.rnn.pad_sequence(zh_batch, batch_first=True, padding_value=zh_vocab['<pad>'])
    

添加特殊标记

  1. 详解:

    • 序列开始和结束标记:
      在序列两端添加<SOS>(Sequence Start)和<EOS>(Sequence End)标记,帮助模型识别序列的起始和结束。
    • 未知词标记:
      为不在词汇表中的词添加<UNK>(Unknown)标记,使模型能够处理未见过的词汇。
  2. 代码示例:

    '''创建数据集(对数据进行序列化并)'''
    class TranslationDataset(Dataset):
    def __init__(self, data: List[Tuple[List[str], List[str]]], en_vocab, zh_vocab):
        self.data = data
        self.en_vocab = en_vocab
        self.zh_vocab = zh_vocab
    
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, idx):
        en, zh = self.data[idx]
        en_indices = [self.en_vocab['<bos>']] + [self.en_vocab[token] for token in en] + [self.en_vocab['<eos>']]
        zh_indices = [self.zh_vocab['<bos>']] + [self.zh_vocab[token] for token in zh] + [self.zh_vocab['<eos>']]
        return en_indices, zh_indices
    

数据增强

  1. 详解:

    • 随机替换或删除词:
      在训练数据中随机替换或删除一些词,增强模型的鲁棒性。
    • 同义词替换:
      使用同义词替换原文中的词,增加训练数据的多样性。
  2. 代码示例:

    暂时省略。

数据分割

  1. 详解:
    划分数据集为训练集、验证集和测试集。

  2. 代码示例:

    暂时省略。

创建模型(新Seq2Seq序列到序列模型)

  1. 模型结构:
    编码器(GRU) + 注意力网络 + 解码器

  2. 编码器(嵌入):

    • 作用
      编码器的主要作用是将输入的源语言句子转换为一个固定长度的隐藏状态向量序列,这些向量捕捉了输入句子的语义信息。

    • 过程

      • 嵌入层(Embedding Layer):
        首先,每个词语索引被转换为词向量。这些向量是通过预训练或在训练过程中学习得到的。
      • 循环神经网络(RNN):
        词向量序列被输入到RNN中。RNN通过其隐藏状态来逐步处理每个词语,并生成每个时间步的隐藏状态向量。
    • 结果

      • 隐藏状态向量序列:
        每个词语对应一个隐藏状态向量,表示该词语及其前面词语的语义信息。
      • 最终隐藏状态:
        表示整个输入句子的语义信息,通常用于初始化解码器。
  3. GRU:

    全称为门控循环单元(Gated Recurrent Unit),是一种改进的循环神经网络(RNN)结构,保留传统RNN模型的时间依赖性特征的同时,解决其长时间依赖性不足和梯度消失等问题。

    • 原理:
      GRU使用两个门来控制信息的流动:更新门(update gate)和重置门(reset gate)。这些门允许GRU在保留长期记忆和捕捉短期依赖之间找到平衡。

    • 结构:

      • 重置门(Reset Gate):重置门决定了如何将新输入与过去的记忆结合。具体来说,它控制了先前隐藏状态对当前计算的影响。

      • 更新门(Update Gate):更新门决定了如何将过去的信息传递到未来。它类似于LSTM中的输入门和遗忘门的组合。更新门控制了当前隐藏状态有多少信息是从前一时间步继承而来,有多少信息是从当前时间步获取的。

      • 基本单元结构如下:

        • 重置门计算\(r_t = \sigma(W_r \cdot [h_{t-1}, x_t])\)
        • 更新门计算\(z_t = \sigma(W_z \cdot [h_{t-1}, x_t])\)
        • 候选隐藏状态计算\(\tilde{h}_t = \tanh(W \cdot [r_t \ast h_{t-1}, x_t])\)
        • 隐藏状态更新\(h_t = (1 - z_t) \ast h_{t-1} + z_t \ast \tilde{h}_t\)
    • 优点

      • 减少了参数:与LSTM相比,GRU具有更少的参数,因为它只有两个门,而LSTM有三个门和一个额外的记忆单元。这使得GRU在计算上更加高效。
      • 解决梯度消失问题:通过门控机制,GRU有效地缓解了传统RNN中的梯度消失问题,能够捕捉长期依赖。
      • 性能良好:在许多任务中,GRU的性能与LSTM相当,甚至在某些情况下更好。
    • 示例

      考虑一个简单的序列输入\(x = [x_1, x_2, x_3]\),通过GRU单元处理得到输出序列\(h = [h_1, h_2, h_3]\)。每个时间步的具体计算如下:

      1. 时间步 \(t=1\)

        • \(x_1\)是当前输入
        • \(h_0\)是初始隐藏状态(通常初始化为零)
        • 计算\(r_1\)\(z_1\)\(\tilde{h}_1\),最后得到\(h_1\)
      2. 时间步 \(t=2\)

        • \(x_2\)是当前输入
        • \(h_1\)是前一时间步的隐藏状态
        • 计算\(r_2\)\(z_2\)\(\tilde{h}_2\),最后得到\(h_2\)
      3. 时间步 \(t=3\)

        • \(x_3\)是当前输入
        • \(h_2\)是前一时间步的隐藏状态
        • 计算\(r_3\)\(z_3\)\(\tilde{h}_3\),最后得到\(h_3\)

      每个时间步的隐藏状态\(h_t\)包含了前面所有时间步的信息,同时结合了当前时间步的输入信息。

  4. 注意力机制:

    • 作用
      注意力机制允许解码器在生成目标语言句子的每个词语时,动态地关注源语言句子的不同部分。这使得模型可以在解码每个词语时,根据上下文选择最相关的信息(类似于上下文依赖的词向量分量的权重)。

    • 过程

      • 计算注意力权重:
        对于解码器的每个时间步,计算当前解码器隐藏状态与编码器隐藏状态向量序列之间的相似度,得到注意力权重。
      • 生成上下文向量:
        使用注意力权重对编码器隐藏状态向量进行加权平均,得到上下文向量。
      • 结合上下文向量和当前隐藏状态:
        上下文向量与当前隐藏状态结合,用于生成下一个词语。
  5. 解码器:

    • 作用
      解码器根据编码器提供的隐藏状态和注意力机制生成目标语言句子。每个时间步解码器会生成一个词语,并将其作为下一时间步的输入,直到生成特殊标记<eos>表示句子结束。

    • 过程

      • 初始化:
        解码器使用编码器的最终隐藏状态初始化其隐藏状态。
      • 逐步生成:
        对于每个时间步,解码器接收前一时间步的输出词语(初始为<bos>),结合注意力机制的上下文向量,生成当前时间步的输出词语。
      • 输出词语:
        解码器生成词语后,将其作为下一时间步的输入,重复这一过程直到生成<eos>
  6. 编码器、解码器以及注意力机制向量之间的关系:

    • 编码器

      • 输入:
        输入序列 ( X = (x_1, x_2, \ldots, x_T) ):这是需要翻译或处理的原始输入序列,每个 ( x_i ) 通常是一个词的嵌入向量。

      • 输出:
        隐状态 ( H = (h_1, h_2, \ldots, h_T) ):编码器在每个时间步生成的隐状态。每个 ( h_i ) 包含输入序列直到第 ( i ) 个时间步的上下文信息。

    • 注意力机制

      • 输入
        编码器的隐状态 ( H = (h_1, h_2, \ldots, h_T) ):编码器输出的每个时间步的隐状态。
        解码器当前时间步的隐状态 ( s_t ):解码器在当前时间步的隐状态。

      • 输出
        上下文向量 ( c_t ):通过注意力权重对编码器隐状态加权平均后的向量。

      • 计算步骤

        1. 计算注意力权重 ( \alpha_{t,i} ):对于每个编码器时间步 ( i ) 计算它与解码器当前时间步 ( t ) 的隐状态 ( s_t ) 的相似度(可以使用点积、加法或其他打分函数)。
        2. 归一化权重 ( \alpha_{t,i} ):通过softmax函数将相似度得分转化为注意力权重。
        3. 计算上下文向量 ( c_t ):用注意力权重对编码器隐状态加权平均,得到上下文向量。

        [ c_t = \sum_{i=1}^{T} \alpha_{t,i} h_i ]

    • 解码器

      • 输入
        前一个时间步的目标词向量 ( y_{t-1} ):解码器在上一个时间步生成的词的嵌入表示。如果是在第一个时间步,使用起始标记 <sos>
        前一个时间步的隐状态 ( s_{t-1} ):解码器在上一个时间步生成的隐状态。
        上下文向量 ( c_t ):通过注意力机制计算得到的当前时间步的上下文向量。

      • 输出
        当前时间步的隐状态 ( s_t ):解码器在当前时间步的隐状态。
        当前时间步的输出词 ( y_t ):解码器在当前时间步生成的词,通常通过一个全连接层和softmax函数生成。

    • 具体步骤和向量构成

      1. 编码器处理输入序列
        将输入序列 ( X ) 转化为隐状态序列 ( H )。
      2. 解码器初始化
        用编码器的最后一个隐状态初始化解码器的第一个隐状态 ( s_0 )。
      3. 解码器生成输出
        • 在时间步 ( t ),解码器接收前一个时间步的目标词向量 ( y_{t-1} ) 和隐状态 ( s_{t-1} )。
        • 注意力机制计算上下文向量 ( c_t )。
        • 解码器结合 ( y_{t-1} )、( s_{t-1} ) 和 ( c_t ),生成当前时间步的隐状态 ( s_t )。
        • 解码器基于 ( s_t ) 和 ( c_t ) 生成当前时间步的输出词 ( y_t )。
      4. 实际编码器的词嵌入向量是作为输入的。
  7. 代码示例

    class Encoder(nn.Module):
    """编码器"""
        def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
            '''
            初始化编码器
            input_dim 输入词汇表大小
            emb_dim 词嵌入向量维度
            hid_dim GRU隐状态维度
            n_layers GRU层数
            '''
            super().__init__()
            self.hid_dim = hid_dim
            self.n_layers = n_layers
            
            self.embedding = nn.Embedding(input_dim, emb_dim)
            self.gru = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
            self.dropout = nn.Dropout(dropout)
            
        def forward(self, src):
            '''
            词嵌入并应用dropout
            通过GRU层计算输出和隐藏状态
            '''
            # src = [batch size, src len]
            embedded = self.dropout(self.embedding(src))
            # embedded = [batch size, src len, emb dim]
            
            outputs, hidden = self.gru(embedded)
            # outputs = [batch size, src len, hid dim * n directions]
            # hidden = [n layers * n directions, batch size, hid dim]
            
            return outputs, hidden
    
    
    class Attention(nn.Module):
    """注意力机制"""
        def __init__(self, hid_dim):
            '''
            初始化linear,用于计算注意力权重
            '''
            super().__init__()
            self.attn = nn.Linear(hid_dim * 2, hid_dim)
            self.v = nn.Linear(hid_dim, 1, bias=False)
            
        def forward(self, hidden, encoder_outputs):
            '''
            hidden 解码器当前隐藏状态
            encoder_outputs 编码器输出
            重复hidden并调整形状,使其与encoder_outputs维度匹配
            计算能量energy,并通过linear计算注意力权重
            返回softmax归一化的注意力权重
            '''
            # hidden = [1, batch size, hid dim]
            # encoder_outputs = [batch size, src len, hid dim]
            
            batch_size = encoder_outputs.shape[0]
            src_len = encoder_outputs.shape[1]
            
            hidden = hidden.repeat(src_len, 1, 1).transpose(0, 1)
            # hidden = [batch size, src len, hid dim]
            
            energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim=2)))
            # energy = [batch size, src len, hid dim]
            
            attention = self.v(energy).squeeze(2)
            # attention = [batch size, src len]
            
            return F.softmax(attention, dim=1)
    
    
    class Decoder(nn.Module):
        """解码器"""
        def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout, attention):
            '''
            初始化解码器
            output_dim 输出词汇表的大小
            attention 注意力机制
            '''
            super().__init__()
            self.output_dim = output_dim
            self.hid_dim = hid_dim
            self.n_layers = n_layers
            self.attention = attention
            
            self.embedding = nn.Embedding(output_dim, emb_dim)
            self.gru = nn.GRU(hid_dim + emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
            self.fc_out = nn.Linear(hid_dim * 2 + emb_dim, output_dim)
            self.dropout = nn.Dropout(dropout)
            
        def forward(self, input, hidden, encoder_outputs):
            '''
            input+hidden+encoder_outputs
            将input词嵌入应用dropout
            注意力机制计算当前步注意力权重
            计算上下文向量
            将嵌入向量与上下文向量拼接输入GRU
            通过GRU计算输出和隐藏状态,拼接GRU输出、上下文向量和词嵌入向量,通过全连接层生成最终预测
            '''
            # input = [batch size, 1]
            # hidden = [n layers, batch size, hid dim]
            # encoder_outputs = [batch size, src len, hid dim]
            
            input = input.unsqueeze(1)
            embedded = self.dropout(self.embedding(input))
            # embedded = [batch size, 1, emb dim]
            
            a = self.attention(hidden[-1:], encoder_outputs)
            # a = [batch size, src len]
            
            a = a.unsqueeze(1)
            # a = [batch size, 1, src len]
            
            weighted = torch.bmm(a, encoder_outputs)
            # weighted = [batch size, 1, hid dim]
            
            rnn_input = torch.cat((embedded, weighted), dim=2)
            # rnn_input = [batch size, 1, emb dim + hid dim]
            
            output, hidden = self.gru(rnn_input, hidden)
            # output = [batch size, 1, hid dim]
            # hidden = [n layers, batch size, hid dim]
            
            embedded = embedded.squeeze(1)
            output = output.squeeze(1)
            weighted = weighted.squeeze(1)
            
            prediction = self.fc_out(torch.cat((output, weighted, embedded), dim=1))
            # prediction = [batch size, output dim]
            
            return prediction, hidden
    
    class Seq2Seq(nn.Module):
        """整体模型结构"""
        def __init__(self, encoder, decoder, device):
            '''初始化Seq2Seq模型'''
            super().__init__()
            self.encoder = encoder
            self.decoder = decoder
            self.device = device
            
        def forward(self, src, trg, teacher_forcing_ratio=0.5):
            '''
            src源序列
            trg目标序列、
            teacher_forcing_ratio教师强制比率
            '''
            # src = [batch size, src len]
            # trg = [batch size, trg len]
            
            batch_size = src.shape[0]
            trg_len = trg.shape[1]
            trg_vocab_size = self.decoder.output_dim
            
            outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device)
            encoder_outputs, hidden = self.encoder(src)
            
            input = trg[:, 0]
            
            for t in range(1, trg_len):
                output, hidden = self.decoder(input, hidden, encoder_outputs)
                outputs[:, t] = output
                teacher_force = random.random() < teacher_forcing_ratio
                top1 = output.argmax(1)
                input = trg[:, t] if teacher_force else top1
            
            return outputs
    
    '''初始化模型'''
    def initialize_model(input_dim, output_dim, emb_dim, hid_dim, n_layers, dropout, device):
        attn = Attention(hid_dim)
        enc = Encoder(input_dim, emb_dim, hid_dim, n_layers, dropout)
        dec = Decoder(output_dim, emb_dim, hid_dim, n_layers, dropout, attn)
        model = Seq2Seq(enc, dec, device).to(device)
        return model
    
  8. 代码复现:
    暂时省略。

详解

  1. 编码器

    class Encoder(nn.Module):
        def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
            super().__init__()
            self.hid_dim = hid_dim
            self.n_layers = n_layers
            
            self.embedding = nn.Embedding(input_dim, emb_dim)
            self.gru = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
            self.dropout = nn.Dropout(dropout)
            
        def forward(self, src):
            # src = [batch size, src len]
            embedded = self.dropout(self.embedding(src))
            # embedded = [batch size, src len, emb dim]
            
            outputs, hidden = self.gru(embedded)
            # outputs = [batch size, src len, hid dim * n directions]
            # hidden = [n layers * n directions, batch size, hid dim]
            
            return outputs, hidden
    
    • __init__ 方法:

      • 初始化编码器的各个参数和层。
      • input_dim:输入词汇表的大小。
      • emb_dim:词嵌入向量的维度。
      • hid_dim:GRU隐状态的维度。
      • n_layers:GRU的层数。
      • dropout:dropout概率,用于防止过拟合。
      • self.embedding:词嵌入层,将每个词转换为固定大小的向量。
      • self.gru:GRU层,用于处理嵌入后的序列数据。
      • self.dropout:dropout层,用于在训练过程中随机丢弃部分神经元,防止过拟合。
    • forward 方法:

      • 输入 src:形状为 [batch size, src len],表示一个batch中的所有输入序列。
      • embedded = self.dropout(self.embedding(src)):将输入序列通过嵌入层并应用dropout,得到嵌入后的序列,形状为 [batch size, src len, emb dim]
      • outputs, hidden = self.gru(embedded):将嵌入后的序列传入GRU层,得到输出序列和最后的隐藏状态。
        • outputs:形状为 [batch size, src len, hid dim],表示GRU在每个时间步的输出。
        • hidden:形状为 [n layers, batch size, hid dim],表示GRU在最后一个时间步的隐藏状态。
      • 返回 outputshidden
  2. 注意力机制

    class Attention(nn.Module):
        def __init__(self, hid_dim):
            super().__init__()
            self.attn = nn.Linear(hid_dim * 2, hid_dim)
            self.v = nn.Linear(hid_dim, 1, bias=False)
            
        def forward(self, hidden, encoder_outputs):
            # hidden = [1, batch size, hid dim]
            # encoder_outputs = [batch size, src len, hid dim]
            
            batch_size = encoder_outputs.shape[0]
            src_len = encoder_outputs.shape[1]
            
            hidden = hidden.repeat(src_len, 1, 1).transpose(0, 1)
            # hidden = [batch size, src len, hid dim]
            
            energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim=2)))
            # energy = [batch size, src len, hid dim]
            
            attention = self.v(energy).squeeze(2)
            # attention = [batch size, src len]
            
            return F.softmax(attention, dim=1)
    
    • __init__ 方法:

      • 初始化注意力机制的线性层。
      • self.attn:一个线性层,用于计算注意力权重的能量值。
      • self.v:另一个线性层,用于将能量值转换为注意力权重。
    • forward 方法:

      • 输入 hidden:形状为 [1, batch size, hid dim],表示解码器当前时间步的隐藏状态。
      • 输入 encoder_outputs:形状为 [batch size, src len, hid dim],表示编码器在每个时间步的输出。
      • hidden = hidden.repeat(src_len, 1, 1).transpose(0, 1):将隐藏状态复制 src_len 次,并调整形状,使其与编码器输出的维度匹配。
        • 结果形状为 [batch size, src len, hid dim]
      • energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim=2))):将隐藏状态和编码器输出连接起来,通过线性层 self.attn,然后应用 tanh 激活函数,得到能量值。
        • 结果形状为 [batch size, src len, hid dim]
      • attention = self.v(energy).squeeze(2):将能量值通过线性层 self.v,得到注意力权重。
        • 结果形状为 [batch size, src len]
      • return F.softmax(attention, dim=1):对注意力权重应用softmax函数,使其归一化,返回注意力权重。
  3. 解码器

    class Decoder(nn.Module):
        def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout, attention):
            super().__init__()
            self.output_dim = output_dim
            self.hid_dim = hid_dim
            self.n_layers = n_layers
            self.attention = attention
            
            self.embedding = nn.Embedding(output_dim, emb_dim)
            self.gru = nn.GRU(hid_dim + emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
            self.fc_out = nn.Linear(hid_dim * 2 + emb_dim, output_dim)
            self.dropout = nn.Dropout(dropout)
            
        def forward(self, input, hidden, encoder_outputs):
            # input = [batch size, 1]
            # hidden = [n layers, batch size, hid dim]
            # encoder_outputs = [batch size, src len, hid dim]
            
            input = input.unsqueeze(1)
            embedded = self.dropout(self.embedding(input))
            # embedded = [batch size, 1, emb dim]
            
            a = self.attention(hidden[-1:], encoder_outputs)
            # a = [batch size, src len]
            
            a = a.unsqueeze(1)
            # a = [batch size, 1, src len]
            
            weighted = torch.bmm(a, encoder_outputs)
            # weighted = [batch size, 1, hid dim]
            
            rnn_input = torch.cat((embedded, weighted), dim=2)
            # rnn_input = [batch size, 1, emb dim + hid dim]
            
            output, hidden = self.gru(rnn_input, hidden)
            # output = [batch size, 1, hid dim]
            # hidden = [n layers, batch size, hid dim]
            
            embedded = embedded.squeeze(1)
            output = output.squeeze(1)
            weighted = weighted.squeeze(1)
            
            prediction = self.fc_out(torch.cat((output, weighted, embedded), dim=1))
            # prediction = [batch size, output dim]
            
            return prediction, hidden
    
    • __init__ 方法:

      • 初始化解码器的各个参数和层。
      • output_dim:输出词汇表的大小。
      • attention:注意力机制实例。
      • self.embedding:词嵌入层,将每个词转换为固定大小的向量。
      • self.gru:GRU层,用于处理嵌入后的序列数据。输入维度为 hid_dim + emb_dim,因为每个时间步的输入是嵌入向量和注意力机制的上下文向量的拼接。
      • self.fc_out:全连接层,将GRU输出转换为词汇表大小的向量。
      • self.dropout:dropout层,用于在训练过程中随机丢弃部分神经元,防止过拟合。
    • forward 方法:

      • 输入 input:形状为 [batch size, 1],表示当前时间步的输入词。

      • 输入 hidden:形状为 [n layers, batch size, hid dim],表示解码器在前一个时间步的隐藏状态。

      • 输入 encoder_outputs:形状为 [batch size, src len, hid dim],表示编码器的输出。

      • input = input.unsqueeze(1):将输入词的形状从 [batch size] 调整为 [batch size, 1]

      • embedded = self.dropout(self.embedding(input)):将输入词通过嵌入层并应用dropout,得到嵌入后的序列,形状为 [batch size, 1, emb dim]

      • a = self.attention(hidden[-1:], encoder_outputs):通过注意力机制计算注意力权重。

        • 注意力机制接受解码器当前时间步的隐藏状态hidden[-1:] 和编码器的输出 encoder_outputs
        • 返回的注意力权重a 形状为 [batch size, src len]
      • a = a.unsqueeze(1):将注意力权重的形状调整为 [batch size, 1, src len]

      • weighted = torch.bmm(a, encoder_outputs):通过矩阵乘法计算加权后的上下文向量,形状为 [batch size, 1, hid dim]

      • rnn_input = torch.cat((embedded, weighted), dim=2):将嵌入向量和上下文向量拼接,作为GRU的输入,形状为 [batch size, 1, emb dim + hid dim]

      • output, hidden = self.gru(rnn_input, hidden):将输入通过GRU层,得到输出和新的隐藏状态。

      • output:形状为 [batch size, 1, hid dim],表示GRU在当前时间步的输出。

      • hidden:形状为 [n layers, batch size, hid dim],表示GRU在当前时间步的隐藏状态。

      • embedded = embedded.squeeze(1):将嵌入向量的形状从 [batch size, 1, emb dim] 调整为 [batch size, emb dim]

      • output = output.squeeze(1):将GRU输出的形状从 [batch size, 1, hid dim] 调整为 [batch size, hid dim]

      • weighted = weighted.squeeze(1):将上下文向量的形状从 [batch size, 1, hid dim] 调整为 [batch size, hid dim]

      • prediction = self.fc_out(torch.cat((output, weighted, embedded), dim=1)):将GRU输出、上下文向量和嵌入向量拼接,通过全连接层生成最终预测,形状为 [batch size, output dim]

      • 返回 prediction 和新的隐藏状态 hidden

  4. 整体模型结构

    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):
            # src = [batch size, src len]
            # trg = [batch size, trg len]
            
            batch_size = src.shape[0]
            trg_len = trg.shape[1]
            trg_vocab_size = self.decoder.output_dim
            
            outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device)
            encoder_outputs, hidden = self.encoder(src)
            
            input = trg[:, 0]
            
            for t in range(1, trg_len):
                output, hidden = self.decoder(input, hidden, encoder_outputs)
                outputs[:, t] = output
                teacher_force = random.random() < teacher_forcing_ratio
                top1 = output.argmax(1)
                input = trg[:, t] if teacher_force else top1
            
            return outputs
    
    • __init__ 方法:

      • 初始化整体Seq2Seq模型,包括编码器、解码器和设备信息。
      • self.encoder:编码器实例。
      • self.decoder:解码器实例。
      • self.device:设备信息(如GPU或CPU)。
    • forward 方法:

      • 输入 src:形状为 [batch size, src len],表示一个batch中的所有输入序列。
      • 输入 trg:形状为 [batch size, trg len],表示一个batch中的所有目标序列。
      • teacher_forcing_ratio:教师强制比率,用于决定在训练过程中是否使用真实目标词作为下一个时间步的输入。
      • batch_size:输入序列的批量大小。
      • trg_len:目标序列的长度。
      • trg_vocab_size:目标词汇表的大小。
      • outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device):初始化输出张量,形状为 [batch size, trg len, trg_vocab_size],用于存储解码器在每个时间步的预测。
      • encoder_outputs, hidden = self.encoder(src):通过编码器计算编码器输出和初始隐藏状态。
      • input = trg[:, 0]:将第一个目标词作为解码器的初始输入。
      • for t in range(1, trg_len):逐时间步生成目标序列。
        • output, hidden = self.decoder(input, hidden, encoder_outputs):通过解码器计算当前时间步的预测和新的隐藏状态。
        • outputs[:, t] = output:将当前时间步的预测存储在输出张量中。
        • teacher_force = random.random() < teacher_forcing_ratio:随机决定是否使用教师强制。
        • top1 = output.argmax(1):从当前时间步的预测中选取概率最高的词。
        • input = trg[:, t] if teacher_force else top1:根据教师强制决定下一个时间步的输入。
      • 返回 outputs:形状为 [batch size, trg len, trg_vocab_size],表示解码器在每个时间步的预测。

该模型训练函数与输出函数

# 定义优化器
def initialize_optimizer(model, learning_rate=0.001):
    return optim.Adam(model.parameters(), lr=learning_rate)

# 运行时间
def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs

def train(model, iterator, optimizer, criterion, clip):
    model.train()
    epoch_loss = 0
    
    for i, batch in enumerate(iterator):
        #print(f"Training batch {i}")
        src, trg = batch
        #print(f"Source shape before: {src.shape}, Target shape before: {trg.shape}")
        if src.numel() == 0 or trg.numel() == 0:
            #print("Empty batch detected, skipping...")
            continue  # 跳过空的批次
        
        src, trg = src.to(DEVICE), trg.to(DEVICE)
        
        optimizer.zero_grad()
        output = model(src, trg)
        
        output_dim = output.shape[-1]
        output = output[:, 1:].contiguous().view(-1, output_dim)
        trg = trg[:, 1:].contiguous().view(-1)
        
        loss = criterion(output, trg)
        loss.backward()
        
        clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
        
        epoch_loss += loss.item()

    print(f"Average loss for this epoch: {epoch_loss / len(iterator)}")
    return epoch_loss / len(iterator)

def evaluate(model, iterator, criterion):
    model.eval()
    epoch_loss = 0
    with torch.no_grad():
        for i, batch in enumerate(iterator):
            #print(f"Evaluating batch {i}")
            src, trg = batch
            if src.numel() == 0 or trg.numel() == 0:
                continue  # 跳过空批次
            
            src, trg = src.to(DEVICE), trg.to(DEVICE)
            
            output = model(src, trg, 0)  # 关闭 teacher forcing
            
            output_dim = output.shape[-1]
            output = output[:, 1:].contiguous().view(-1, output_dim)
            trg = trg[:, 1:].contiguous().view(-1)
            
            loss = criterion(output, trg)
            epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)

# 翻译函数
def translate_sentence(src_indexes, src_vocab, tgt_vocab, model, device, max_length=50):
    model.eval()
    
    src_tensor = src_indexes.unsqueeze(0).to(device)  # 添加批次维度
    
    # with torch.no_grad():
    #     encoder_outputs = model.encoder(model.positional_encoding(model.src_embedding(src_tensor) * math.sqrt(model.d_model)))

    trg_indexes = [tgt_vocab['<bos>']]
    for i in range(max_length):
        trg_tensor = torch.LongTensor(trg_indexes).unsqueeze(0).to(device)
        # print("src_tensor:",src_tensor)
        # print("trg_tensor:",trg_tensor)
        with torch.no_grad():
            output = model(src_tensor, trg_tensor)
        
        pred_token = output.argmax(2)[:, -1].item()
        trg_indexes.append(pred_token)
        
        if pred_token == tgt_vocab['<eos>']:
            break
    
    trg_tokens = [tgt_vocab.get_itos()[i] for i in trg_indexes]
    return trg_tokens[1:-1]  # 移除<bos>和<eos>标记

创建模型(transformer模型)

Transformer 机制详解

  1. Transformer 模型的关键组件包括:多头自注意力机制位置编码前馈神经网络

  2. 多头自注意力机制(Multi-Head Self-Attention)

    • 通过多个注意力头(Attention Head)来捕捉不同位置之间的依赖关系。
    • 每个注意力头独立计算注意力分数,然后将其拼接并通过线性变换。
    • 自注意力机制允许每个词与序列中的其他所有词建立直接的联系,不同于 RNN 逐步处理序列的方法。
  3. 位置编码(Positional Encoding)

    • 由于 Transformer 没有内置的序列顺序处理能力,位置编码提供了词在序列中的位置信息。
    • 位置编码使用正弦和余弦函数生成,使得每个位置都有独特的编码,并且编码具有良好的位置区分性。
  4. 前馈神经网络(Feedforward Neural Network, FFN)

    • 每个 Transformer 层中包含两个前馈神经网络层,分别处理输入序列的特征表示。
    • FFN 使模型能够进行更复杂的非线性变换,提高模型的表示能力。
  5. 编码器-解码器架构(Encoder-Decoder Architecture)

    • 编码器:将输入序列映射到一个固定长度的表示(隐状态)。
    • 解码器:根据编码器的表示和自身的输入序列生成输出序列。
  6. 掩码机制(Masking)

    • 目标掩码(Target Mask):防止解码器在生成当前词时看到未来的词。
    • 源掩码(Source Mask):在输入序列中进行填充掩码,防止模型关注填充位置。

代码详解

import torch
import torch.nn as nn
import math

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        # 创建位置编码矩阵
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0).transpose(0, 1)
        self.register_buffer('pe', pe)

    def forward(self, x):
        # 添加位置编码并应用dropout
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

class TransformerModel(nn.Module):
    def __init__(self, src_vocab, tgt_vocab, d_model, nhead, num_encoder_layers, num_decoder_layers, dim_feedforward, dropout):
        super(TransformerModel, self).__init__()
        self.transformer = nn.Transformer(d_model, nhead, num_encoder_layers, num_decoder_layers, dim_feedforward, dropout)
        self.src_embedding = nn.Embedding(len(src_vocab), d_model)
        self.tgt_embedding = nn.Embedding(len(tgt_vocab), d_model)
        self.positional_encoding = PositionalEncoding(d_model, dropout)
        self.fc_out = nn.Linear(d_model, len(tgt_vocab))
        self.src_vocab = src_vocab
        self.tgt_vocab = tgt_vocab
        self.d_model = d_model

    def forward(self, src, tgt):
        # 调整src和tgt的维度
        src = src.transpose(0, 1)  # (seq_len, batch_size)
        tgt = tgt.transpose(0, 1)  # (seq_len, batch_size)

        # 生成掩码
        src_mask = self.transformer.generate_square_subsequent_mask(src.size(0)).to(src.device)
        tgt_mask = self.transformer.generate_square_subsequent_mask(tgt.size(0)).to(tgt.device)

        # 生成填充掩码
        src_padding_mask = (src == self.src_vocab['<pad>']).transpose(0, 1)
        tgt_padding_mask = (tgt == self.tgt_vocab['<pad>']).transpose(0, 1)

        # 嵌入和位置编码
        src_embedded = self.positional_encoding(self.src_embedding(src) * math.sqrt(self.d_model))
        tgt_embedded = self.positional_encoding(self.tgt_embedding(tgt) * math.sqrt(self.d_model))

        # 通过transformer层
        output = self.transformer(src_embedded, tgt_embedded,
                                  src_mask, tgt_mask, None, src_padding_mask, tgt_padding_mask, src_padding_mask)
        return self.fc_out(output).transpose(0, 1)

def initialize_model(src_vocab, tgt_vocab, d_model=512, nhead=8, num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=2048, dropout=0.1):
    model = TransformerModel(src_vocab, tgt_vocab, d_model, nhead, num_encoder_layers, num_decoder_layers, dim_feedforward, dropout)
    return model

Transformer 与传统 Seq2Seq 比较

  1. 架构差异

    • Seq2Seq(基于 RNN)
      由 RNN(通常是 LSTM 或 GRU)构成的编码器和解码器组成,编码器将输入序列编码为固定长度的上下文向量,解码器从该向量生成输出序列。
    • Transformer
      完全基于注意力机制,没有使用 RNN,编码器和解码器都由多层自注意力和前馈神经网络组成。
  2. 并行化

    • Seq2Seq
      由于 RNN 的顺序依赖性,难以并行化,导致训练和推理速度较慢。
    • Transformer
      依赖于自注意力机制,处理整个序列时不依赖于前后顺序,因此可以并行计算,提高了训练和推理效率。
  3. 长距离依赖

    • Seq2Seq
      由于 RNN 的顺序性质,捕捉长距离依赖信息较为困难,容易出现梯度消失或爆炸问题。
    • Transformer
      通过自注意力机制,能够直接关注序列中任意位置的词,捕捉长距离依赖更为有效。
  4. 注意力机制

    • Seq2Seq
      可以在编码器和解码器之间使用注意力机制,但主要依赖于 RNN 结构。
    • Transformer
      内置多头自注意力机制,广泛应用于编码器和解码器的每一层,增强了模型的表达能力。
posted @ 2024-07-20 23:54  LPF05  阅读(8)  评论(0编辑  收藏  举报