Bootstrap

【Datawhale AI夏令营】 Task2 学习笔记

学习目标:深入理解赛题,精读baseline代码,入门机器学习代码实现

目录

一、概念梳理

    1、机器学习

    2、Seq2Seq模型结构 

    3、中英文分词 

    中文分词

    英文分词

    分词工具

    4、循环神经网络(RNN) 

    RNN的基本结构

    RNN的特点

    RNN的局限性

    改进的RNN变体

    5、门控循环单元(Gated Recurrent Unit,GRU) 

    GRU的结构

    GRU的状态更新

    GRU的特点

二、Baseline精解与实操

    1、 环境配置

    2、数据预处理 

    3、模型构建 

    4、模型训练

    5、总结


一、概念梳理

    1、机器学习

        机器学习(Machine Learning)是一种人工智能(Artificial Intelligence)的分支,旨在通过算法和统计模型使计算机系统能够自动改进其性能。具体来说,机器学习使计算机能从数据中学习和识别模式,从而在没有明确编程指令的情况下进行预测或决策,以下是一些机器学习涉及到的的关键概念:

  1. 训练数据:机器学习模型通过大量的历史数据进行训练,以便在新的数据上表现得更好。

  2. 特征:用于描述数据的属性或变量。在机器学习中,选择和处理特征是非常重要的步骤。

  3. 模型:一个通过学习数据中的模式和关系来做出预测的数学或统计模型。

  4. 监督学习:一种机器学习方法,模型在训练时使用带标签的数据(即输入和期望输出的对应关系)。常见的监督学习算法包括回归、分类等。

  5. 无监督学习:模型在没有标签的数据上进行训练,目标是找到数据的内在结构或模式。常见的无监督学习算法包括聚类和降维等。

  6. 强化学习:一种机器学习方法,通过让智能体在环境中采取行动并根据反馈进行学习,以最大化累积奖励。

  7. 过拟合:当模型在训练数据上表现非常好,但在新数据上表现很差时,称为过拟合。这通常是因为模型过于复杂,捕捉到了数据中的噪声。

  8. 泛化:模型在新的、未见过的数据上的表现。好的机器学习模型应具备良好的泛化能力。

    2、Seq2Seq模型结构 

        Seq2Seq(Sequence to Sequence)模型结构是一种神经网络架构,广泛应用于处理序列数据的任务,如机器翻译、文本生成、对话系统等。它的核心思想是将输入序列编码成一个固定长度的向量,再将这个向量解码为输出序列,Seq2Seq模型通常包括以下两个主要部分:

  1. 编码器(Encoder)

    • 编码器的任务是读取输入序列,并将其编码成一个固定长度的上下文向量(context vector),也称为思想向量(thought vector)。
    • 编码器通常由一个或多个循环神经网络(RNN)、长短期记忆网络(LSTM)或门控循环单元(GRU)层组成。
    • 输入序列的每一个时间步的输入都会传递给编码器,编码器在每个时间步都会更新其隐藏状态,最终隐藏状态或中间层输出即为上下文向量。
  2. 解码器(Decoder)

    • 解码器的任务是将编码器生成的上下文向量解码成输出序列。
    • 解码器也通常由一个或多个RNN、LSTM或GRU层组成。
    • 在解码过程中,解码器会一步步生成输出序列的每个元素,每生成一个元素,就会将其作为下一个时间步的输入,直到生成完整的输出序列。
    • 在训练过程中,解码器每一步的输入通常是实际的输出序列的前一个元素,而在预测过程中,则是上一步生成的元素。

         Seq2Seq模型的关键在于它能处理长度不一的输入和输出序列,这使得它非常适合诸如机器翻译这种输入和输出长度不一致的任务。

        此外,Seq2Seq模型还可以结合注意力机制(Attention Mechanism)来提升性能。注意力机制的引入使得模型在解码每一步时,不仅仅依赖于上下文向量,还可以根据输入序列的不同部分赋予不同的权重,从而更好地捕捉输入和输出之间的关系。

    3、中英文分词 

        中英文分词是指将中文和英文的文本进行分割,识别出单独的词语或单词的过程。这在自然语言处理(NLP)中是一个关键步骤,因为大多数语言处理任务(如文本分类、机器翻译、情感分析等)都需要对文本进行分词,以便进一步分析和处理。

    中文分词

        中文分词相对于英文分词更为复杂,因为中文文本没有明显的词边界(如空格),且汉字可以组成各种不同的词语。中文分词主要有以下几种方法:

  1. 基于规则的方法:利用预定义的词典和规则来进行分词。常见的方法包括正向最大匹配法(从左到右匹配最长的词)、逆向最大匹配法(从右到左匹配最长的词)等。

  2. 基于统计的方法:利用统计信息,如词频、词共现概率等,来判断词的边界。常见的方法包括隐马尔可夫模型(HMM)、条件随机场(CRF)等。

  3. 基于机器学习的方法:使用机器学习模型,如神经网络,结合大量标注好的语料进行训练,从而实现自动分词。现代的方法包括基于深度学习的LSTM、BERT等模型。

    英文分词

        英文分词相对简单,因为英文单词之间通常由空格分隔。常见的方法包括:

  1. 基于规则的方法:简单地以空格和标点符号为边界,将文本切分成单词。

  2. 基于正则表达式的方法:使用正则表达式匹配单词边界,更加灵活和精确。

  3. 基于机器学习的方法:利用机器学习模型,尤其是在处理复杂文本(如含有缩写、连字符等情况)时,效果更好。

    分词工具

  • 中文分词工具:Jieba、HanLP、THULAC等。
  • 英文分词工具:NLTK、spaCy等。

    4、循环神经网络(RNN) 

         循环神经网络(Recurrent Neural Network,简称RNN)是一种用于处理序列数据的神经网络架构。与传统的前馈神经网络不同,RNN具有循环连接,可以在序列的各个时间步之间保留信息,因此非常适合处理时间序列、语言模型、语音识别等任务。

    RNN的基本结构

        RNN的基本单元由一个隐藏层(hidden layer)和输入层(input layer)组成。每个时间步的输入都会影响当前时间步的输出和隐藏状态,同时隐藏状态还会传递到下一个时间步。RNN通过以下公式更新隐藏状态和输出:

  • 隐藏状态: ( h_t = \sigma(W_h h_{t-1} + W_x x_t + b_h) )
  • 输出: ( y_t = \sigma(W_y h_t + b_y) )

        其中,( x_t ) 是当前时间步的输入,( h_t ) 是当前时间步的隐藏状态,( y_t ) 是当前时间步的输出,( W_h )、( W_x )、( W_y ) 是权重矩阵,( b_h )、( b_y ) 是偏置,( \sigma ) 是激活函数。

    RNN的特点

  1. 时间依赖性:RNN能够捕捉输入序列中的时间依赖关系,保留前面时间步的信息,并利用这些信息进行预测或生成输出。
  2. 共享参数:RNN在不同时间步共享相同的权重和偏置参数,使其能够在序列数据上有效地学习模式。

 典型RNN的结构示意图

    RNN的局限性

        尽管RNN在处理序列数据方面有显著优势,但它也存在一些局限性:

  1. 梯度消失和梯度爆炸:在处理长序列数据时,RNN容易出现梯度消失或梯度爆炸的问题,导致模型难以学习长时间依赖关系。
  2. 短期记忆能力有限:标准RNN在处理需要长时间记忆的任务时表现不佳。

    改进的RNN变体

        为了克服标准RNN的局限性,研究人员提出了多种改进的RNN变体,其中最著名的是:

  1. 长短期记忆网络(LSTM)

    • LSTM通过引入门控机制(如输入门、遗忘门、输出门)来控制信息的流动,有效缓解了梯度消失问题。
    • LSTM能够在较长时间跨度上捕捉依赖关系,适用于处理长序列数据。
  2. 门控循环单元(GRU)

    • GRU是LSTM的一种简化版本,同样通过门控机制来控制信息流动,但结构更简单,计算效率更高,这也是我将在下一小节介绍的内容。

    5、门控循环单元(Gated Recurrent Unit,GRU) 

        门控循环单元(Gated Recurrent Unit,简称GRU)是一种改进的循环神经网络(RNN)结构,旨在解决标准RNN在处理长序列数据时面临的梯度消失和梯度爆炸问题。GRU是由Cho等人在2014年提出的,它的设计借鉴了长短期记忆网络(LSTM)的思想,但结构更为简单。

    GRU的结构

        GRU单元通过引入门控机制来控制信息流动,主要包括以下两个门:

  1. 更新门(Update Gate)

    • 更新门决定了当前时间步的隐藏状态应该保留多少前一时间步的信息,以及多少新的输入信息。
    • 更新门的公式为:( z_t = \sigma(W_z \cdot [h_{t-1}, x_t]) )
      其中,( W_z ) 是权重矩阵,( \sigma ) 是sigmoid激活函数,( h_{t-1} ) 是前一时间步的隐藏状态,( x_t ) 是当前时间步的输入。
  2. 重置门(Reset Gate)

    • 重置门决定了当前时间步的隐藏状态应忽略多少前一时间步的信息。
    • 重置门的公式为:( r_t = \sigma(W_r \cdot [h_{t-1}, x_t]) )

    GRU的状态更新

        GRU通过上述门控机制计算新的隐藏状态:

  1. 候选隐藏状态(Candidate Hidden State)

    • 候选隐藏状态结合了当前输入和前一时间步隐藏状态的信息,通过重置门控制前一时间步信息的遗忘程度。
    • 公式为:( \tilde{h}t = \tanh(W_h \cdot [r_t \ast h{t-1}, x_t]) )
  2. 最终隐藏状态(Final Hidden State)

    • 最终隐藏状态是前一时间步隐藏状态和候选隐藏状态的加权和,由更新门控制。
    • 公式为:( h_t = (1 - z_t) \ast h_{t-1} + z_t \ast \tilde{h}_t )

    GRU的特点

  1. 结构简单:与LSTM相比,GRU去除了输出门(Output Gate),只有两个门(更新门和重置门),使得结构更为简单,计算效率更高。
  2. 性能优越:尽管结构更简单,GRU在许多任务中表现出与LSTM相当的性能,有时甚至更好,特别是在较小的数据集或训练时间有限的情况下。
  3. 解决梯度消失问题:通过门控机制,GRU能够有效缓解梯度消失问题,使其能够处理较长时间跨度的依赖关系。

 

二、Baseline精解与实操

    1、 环境配置

!mkdir ../model
!mkdir ../results
!pip install torchtext
!pip install jieba
!pip install sacrebleu

     上述代码作用为项目的环境做好了准备,包括创建目录和安装必要的Python包

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.nn.utils import clip_grad_norm_
from torchtext.data.metrics import bleu_score
from torch.utils.data import Dataset, DataLoader
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
from typing import List, Tuple
import jieba
import random
from torch.nn.utils.rnn import pad_sequence
import sacrebleu
import time
import math

     上述代码导入了用于构建和训练自然语言处理模型所需的各种库和模块。具体来说,包括导入PyTorch库,及其神经网络模块,函数式接口,优化器模块,梯度裁剪函数,torchtext库中的BLEU得分计算函数,数据集和数据加载器模块,分词器获取函数,从迭代器构建词汇表的函数,类型提示模块,jieba中文分词库,随机数生成模块,序列填充函数,sacrebleu库,时间模块,数学模块,

!pip install -U pip setuptools wheel -i https://pypi.tuna.tsinghua.edu.cn/simple
!pip install -U 'spacy[cuda12x]' -i https://pypi.tuna.tsinghua.edu.cn/simple
!pip install ../dataset/en_core_web_trf-3.7.3-py3-none-any.whl
# !python -m spacy download en_core_web_sm

     第一行代码通过清华大学镜像源升级pipsetuptoolswheel

    第二行代码通过清华大学镜像源安装SpaCy,并指定安装适用于CUDA 12.x的GPU加速版本。

    第三行代码安装了指定路径下的en_core_web_trf-3.7.3-py3-none-any.whl离线安装包。这里也可以使用 :

!python -m spacy download en_core_web_sm

    进行在线下载 en_core_web_sm 语言模型,但国内环境下载速度十分缓慢,故这里使用离线安装的方式,离线安装包下载地址https://github.com/explosion/spacy-models/releases

    在实操过程中,清华大学镜像源下载失败,初步推测是个人网络环境问题。

    后续更改至阿里云镜像源以及豆瓣镜像源。

!pip install -U pip setuptools wheel -i https://mirrors.aliyun.com/pypi/simple
!pip install -U 'spacy[cuda12x]' -i https://pypi.douban.com/simple

     安装成功,问题得以解决。

    2、数据预处理 

en_tokenizer = get_tokenizer('spacy', language='en_core_web_trf')
zh_tokenizer = lambda x: list(jieba.cut(x))

    上述代码分别定义了中文和英文分词器。

# 读取数据函数
def read_data(file_path: str) -> List[str]:
    with open(file_path, 'r', encoding='utf-8') as f:
        return [line.strip() for line in f]

# 数据预处理函数
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

# 构建词汇表
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

    上述代码定义了读取数据、预处理数据和构建词汇表的函数。

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

     上述代码这段代码定义了一个 TranslationDataset 类,用于表示翻译任务的数据集。这个类继承自 torch.utils.data.Dataset,主要用于为 PyTorch 数据加载器提供数据。

def collate_fn(batch):
    en_batch, zh_batch = [], []
    for en_item, zh_item in batch:
        if en_item and zh_item:  # 确保两个序列都不为空
            # print("都不为空")
            en_batch.append(torch.tensor(en_item))
            zh_batch.append(torch.tensor(zh_item))
        else:
            print("存在为空")
    if not en_batch or not zh_batch:  # 如果整个批次为空,返回空张量
        return torch.tensor([]), torch.tensor([])
    
    # src_sequences = [item[0] for item in batch]
    # trg_sequences = [item[1] for item in batch]
    
    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>'])

    # en_batch = pad_sequence(en_batch, batch_first=True, padding_value=en_vocab['<pad>'])
    # zh_batch = pad_sequence(zh_batch, batch_first=True, padding_value=zh_vocab['<pad>'])
    
    return en_batch, zh_batch

     上述代码定义了一个 collate_fn 函数,用于处理批次数据,在将数据加载到模型之前,对数据进行适当的填充。该函数主要用于 PyTorch 的 DataLoader,以确保每个批次的数据具有相同的长度,方便模型的输入。

# 数据加载函数
def load_data(train_path: str, dev_en_path: str, dev_zh_path: str, test_en_path: str):
    # 读取训练数据
    train_data = read_data(train_path)
    train_en, train_zh = zip(*(line.split('\t') for line in train_data))
    
    # 读取开发集和测试集
    dev_en = read_data(dev_en_path)
    dev_zh = read_data(dev_zh_path)
    test_en = read_data(test_en_path)

    # 预处理数据
    train_processed = preprocess_data(train_en, train_zh)
    dev_processed = preprocess_data(dev_en, dev_zh)
    test_processed = [(en_tokenizer(en.lower())[:MAX_LENGTH], []) for en in test_en if en.strip()]

    # 构建词汇表
    global en_vocab, zh_vocab
    en_vocab, zh_vocab = build_vocab(train_processed)

    # 创建数据集
    train_dataset = TranslationDataset(train_processed, en_vocab, zh_vocab)
    dev_dataset = TranslationDataset(dev_processed, en_vocab, zh_vocab)
    test_dataset = TranslationDataset(test_processed, en_vocab, zh_vocab)
    
    from torch.utils.data import Subset

    # 假设你有10000个样本,你只想用前1000个样本进行测试
    indices = list(range(N))
    train_dataset = Subset(train_dataset, indices)

    # 创建数据加载器
    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn, drop_last=True)
    dev_loader = DataLoader(dev_dataset, batch_size=BATCH_SIZE, collate_fn=collate_fn, drop_last=True)
    test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, collate_fn=collate_fn, drop_last=True)

    return train_loader, dev_loader, test_loader, en_vocab, zh_vocab

    上述代码定义了一个 load_data 函数,用于读取、预处理数据并创建数据集和数据加载器。

    3、模型构建 

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

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)

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

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

     上述代码实现了基于注意力机制的序列到序列(Seq2Seq)模型,包括编码器(Encoder)、注意力机制(Attention)、解码器(Decoder)和整体模型(Seq2Seq)。

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

    上述代码实现了模型的初始化。

    4、模型训练

def initialize_optimizer(model, learning_rate=0.001):
    return optim.Adam(model.parameters(), lr=learning_rate)

     上述代码定义了一个 initialize_optimizer 函数,用于初始化优化器。这个函数的主要作用是为传入的模型设置 Adam 优化器,并指定学习率。

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

      上述代码定义了一个 epoch_time 函数,用于计算一个时间段的分钟数和秒数。它通常用于记录训练一个 epoch(训练轮次)所花费的时间。

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)

     上述代码定义了两个函数 train 和 evaluate,分别用于训练和评估模型。这两个函数分别用于训练和评估模型。在训练过程中,train 函数会在每个批次上执行前向传播、计算损失、反向传播和参数更新。而在评估过程中,evaluate 函数会在禁用梯度计算的情况下执行前向传播和计算损失。

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>标记

   上述代码定义了一个 translate_sentence 函数,用于将输入的源语言句子翻译成目标语言。该函数使用给定的模型进行推理,并逐步生成目标语言的句子。

def calculate_bleu(dev_loader, src_vocab, tgt_vocab, model, device):
    model.eval()
    translations = []
    references = []
    
    with torch.no_grad():
        for src, tgt in dev_loader:
            src = src.to(device)
            for sentence in src:
                translated = translate_sentence(sentence, src_vocab, tgt_vocab, model, device)
                translations.append(' '.join(translated))
            
            for reference in tgt:
                ref_tokens = [tgt_vocab.get_itos()[idx] for idx in reference if idx not in [tgt_vocab['<bos>'], tgt_vocab['<eos>'], tgt_vocab['<pad>']]]
                references.append([' '.join(ref_tokens)])
    
    bleu = sacrebleu.corpus_bleu(translations, references)
    return bleu.score

    上述代码定义了一个 calculate_bleu 函数,用于计算模型在开发集上的 BLEU 分数。该函数通过对开发集中的每个句子进行翻译,并与参考翻译进行比较来计算 BLEU 分数。

def train_model(model, train_iterator, valid_iterator, optimizer, criterion, N_EPOCHS = 10, CLIP = 1, save_path = '../model/best-model.pt'):
    best_valid_loss = float('inf')
    
    for epoch in range(N_EPOCHS):
        start_time = time.time()
        
        #print(f"Starting Epoch {epoch + 1}")
        train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
        valid_loss = evaluate(model, valid_iterator, criterion)
        
        end_time = time.time()
        epoch_mins, epoch_secs = epoch_time(start_time, end_time)
        
        if valid_loss < best_valid_loss:
            best_valid_loss = valid_loss
            torch.save(model.state_dict(), save_path)
        
        print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s')
        print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
        print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')

     上述代码定义了一个 train_model 函数,用于训练和验证模型,并在验证集上性能最好的情况下保存模型的权重。这样可以确保最终保存的模型是最佳的,并且提供了每个 epoch 的训练和验证损失以及困惑度(PPL)的详细信息,以便于监控训练过程中的性能变化。

MAX_LENGTH = 100  # 最大句子长度
BATCH_SIZE = 32
DEVICE = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')
N = 148363   # 采样训练集的数量

train_path = '../dataset/train.txt'
dev_en_path = '../dataset/dev_en.txt'
dev_zh_path = '../dataset/dev_zh.txt'
test_en_path = '../dataset/test_en.txt'

train_loader, dev_loader, test_loader, en_vocab, zh_vocab = load_data(
    train_path, dev_en_path, dev_zh_path, test_en_path
)


print(f"英语词汇表大小: {len(en_vocab)}")
print(f"中文词汇表大小: {len(zh_vocab)}")
print(f"训练集大小: {len(train_loader.dataset)}")
print(f"开发集大小: {len(dev_loader.dataset)}")
print(f"测试集大小: {len(test_loader.dataset)}")

    上述代码定义了一些重要的参数和路径,并通过调用 load_data 函数加载数据集和词汇表,为后续模型的训练和评估准备了必要的数据和信息。

    后续实操过程中出现报错:CUDA error: invalid device ordinal,询问助教得到解决方案:将cuda:1更改至cuda:0,问题得以解决。

if __name__ == '__main__':
    
    N_EPOCHS = 5
    CLIP=1
    # 模型参数
    INPUT_DIM = len(en_vocab)
    OUTPUT_DIM = len(zh_vocab)
    EMB_DIM = 128
    HID_DIM = 256
    N_LAYERS = 2
    DROPOUT = 0.5
    
    # 初始化模型
    model = initialize_model(INPUT_DIM, OUTPUT_DIM, EMB_DIM, HID_DIM, N_LAYERS, DROPOUT, DEVICE)
    print(f'The model has {sum(p.numel() for p in model.parameters() if p.requires_grad):,} trainable parameters')

    # 定义损失函数
    criterion = nn.CrossEntropyLoss(ignore_index=zh_vocab['<pad>'])
    # 初始化优化器
    optimizer = initialize_optimizer(model)

    # 训练模型
    save_path = '../model/best-model.pt'
    train_model(model, train_loader, dev_loader, optimizer, criterion, N_EPOCHS, CLIP, save_path = save_path)

    print(f"训练完成!模型已保存到:{save_path}")

     主函数,不做赘述

save_dir = '../results/submit_task2.txt'
with open(save_dir, 'w') as f:
    translated_sentences = []
    for batch in test_loader:  # 遍历所有数据
        src, _ = batch
        src = src.to(DEVICE)
        translated = translate_sentence(src[0], en_vocab, zh_vocab, model, DEVICE, max_length=50)  # 翻译结果,max_length生成翻译的最大长度
        #print(translated)
        results = "".join(translated)
        f.write(results + '\n')  # 将结果写入文件
    print(f"翻译完成,结果已保存到{save_dir}")

    上述代码将测试集中的句子通过已训练的模型进行翻译,并将翻译结果保存到指定的文件中,实现测试集句子的自动翻译和结果保存。

    5、总结

    Task2相对于Task1而言,难度复杂度均有提升,主要难度体现在前置知识点的理解掌握,以及环境配置中针对个人问题的解决方案,通过前置知识点梳理,我对机器学习以及其相关知识点更加得心应手,通过Baseline精解,我对于baseline的实现思路有了更深入的理解。

    参考资料:         https://datawhaler.feishu.cn/wiki/PztLwkofsi95oak2Iercw9hkn2g?from=from_parent_docs

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;