当前位置:   article > 正文

DatawhaleAI夏令营第二期NLP方向Task1笔记

DatawhaleAI夏令营第二期NLP方向Task1笔记

DatawhaleAI夏令营第二期NLP方向Task1笔记


前言

在当今数字化和信息化的时代,自然语言处理(NLP)作为人工智能(AI)领域的重要分支,正以惊人的速度改变着我们与信息交互的方式。NLP不仅仅是理解和处理人类语言的技术,它更是连接人与机器之间沟通桥梁的核心技术之一。

随着大数据的兴起和计算能力的增强,NLP在多个领域展现出了其巨大的潜力和广泛的应用。从语音识别到文本理解,从机器翻译到智能客服,NLP技术正在深刻影响着我们的生活和工作方式。它不仅使得信息的获取和处理更加高效,也为个性化服务和智能决策提供了技术支持。
赛题链接:赛题链接


对baseline的分析

1.引入库

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchtext.data.utils import get_tokenizer
from collections import Counter
import random
from torch.utils.data import Subset, DataLoader
import time
import contractions
import unicodedata
import re
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

torch: 这是PyTorch深度学习框架的核心库,提供了张量计算和自动求导等功能,是构建神经网络模型的基础。

torch.nn: 这是PyTorch中用于构建神经网络模型的模块,包含了各种层和损失函数的定义,能够帮助你定义复杂的神经网络结构。

torch.optim: 这个模块包含了各种优化算法,例如SGD、Adam等,用来优化神经网络模型的参数。

Dataset, DataLoader: 这两个类来自于torch.utils.data模块,用于帮助你加载和处理数据。Dataset用于自定义数据集的加载,DataLoader则用于将数据集分批次地提供给模型进行训练。

get_tokenizer: 这是来自torchtext.data.utils的一个函数,用于获取文本数据的分词器,可以帮助你将文本分解为单词或子词的序列。

Counter: 这是Python标准库collections中的一个类,用于快速统计可哈希对象(如字符串)的出现次数,通常用于文本预处理中的词频统计等任务。

random: 这是Python标准库中的一个模块,提供了各种用于生成随机数的函数,可以用来在数据处理或模型训练中引入随机性。

time: 这也是Python标准库中的一个模块,提供了各种时间相关的函数,用于在程序中测量和控制时间。

contractions: 这是一个第三方库,用于在英语文本中扩展缩略词,例如将"can’t"扩展为"cannot"等。

unicodedata, re: 这两个是Python标准库中的模块,分别提供了对Unicode数据和正则表达式的支持,通常用于文本的规范化和处理。

2.为所给训练集进行处理

由于题目所提供的数据集有些并不符合规范,不适合进行分词或者是不应出现。因此需要做出清理,主要使用正则表达式的方法清除。

#数据清洗
#unicode编码转ascii
def unicodeToAscii(text):
    return ''.join(c for c in unicodedata.normalize('NFD', text) if unicodedata.category(c) != 'Mn')

def preprocess_en(text):
    text = unicodeToAscii(text.strip())
    text = contractions.fix(text)
    text = re.sub(r'\([^)]*\)', '', text)
    text = re.sub(r"[^a-zA-Z0-9.!?]+", r" ", text)  # 保留数字
    return text
def preprocess_zh(text):
    # 去除中文里的括弧
    text = re.sub(r'\([^)]*\)', '', text)
    text = re.sub(r"[^\u4e00-\u9fa5,。!?0-9]", "", text)  # 保留数字
    return text

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

然后就是baseline对所给训练集的分词填充列表
代码如下

# 定义数据集类
# 修改TranslationDataset类以处理术语
class TranslationDataset(Dataset):
    def __init__(self, filename, terminology):
        self.data = []
        with open(filename, 'r', encoding='utf-8') as f:
            for line in f:
                en, zh = line.strip().split('\t')
                #清洗英文
                en = preprocess_en(en)
                #清洗中文
                zh = preprocess_zh(zh)
                self.data.append((en, zh))
        
        self.terminology = terminology
        
        # 创建词汇表,注意这里需要确保术语词典中的词也被包含在词汇表中
        self.en_tokenizer = get_tokenizer('basic_english')
        self.zh_tokenizer = list  # 使用字符级分词
        
        en_vocab = Counter(self.terminology.keys())  # 确保术语在词汇表中
        zh_vocab = Counter()
        
        for en, zh in self.data:
            en_vocab.update(self.en_tokenizer(en))
            zh_vocab.update(self.zh_tokenizer(zh))
        
        # 添加术语到词汇表
        self.en_vocab = ['<pad>', '<sos>', '<eos>'] + list(self.terminology.keys()) + [word for word, _ in en_vocab.most_common(10000)]
        self.zh_vocab = ['<pad>', '<sos>', '<eos>'] + [word for word, _ in zh_vocab.most_common(10000)]
        
        self.en_word2idx = {word: idx for idx, word in enumerate(self.en_vocab)}
        self.zh_word2idx = {word: idx for idx, word in enumerate(self.zh_vocab)}


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

    def __getitem__(self, idx):
        en, zh = self.data[idx]
        en_tensor = torch.tensor([self.en_word2idx.get(word, self.en_word2idx['<sos>']) for word in self.en_tokenizer(en)] + [self.en_word2idx['<eos>']])
        zh_tensor = torch.tensor([self.zh_word2idx.get(word, self.zh_word2idx['<sos>']) for word in self.zh_tokenizer(zh)] + [self.zh_word2idx['<eos>']])
        return en_tensor, zh_tensor

def collate_fn(batch):
    en_batch, zh_batch = [], []
    for en_item, zh_item in batch:
        en_batch.append(en_item)
        zh_batch.append(zh_item)
    
    # 对英文和中文序列分别进行填充
    en_batch = nn.utils.rnn.pad_sequence(en_batch, padding_value=0, batch_first=True)
    zh_batch = nn.utils.rnn.pad_sequence(zh_batch, padding_value=0, batch_first=True)
    
    return en_batch, zh_batch
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

3.基于Seq2Seq进行模型训练

在Seq2Seq模型中,常见的参数包括:

(1)编码器(Encoder):
输入序列的词嵌入维度(embedding_dim):将输入序列中的每个单词映射到一个固定大小的向量。
隐藏状态的维度(hidden_size):编码器RNN单元的隐藏状态的维度,决定了模型的表示能力和复杂度。
编码器的层数(num_layers):编码器RNN的层数,可以是单层或多层的RNN。
是否使用双向RNN(bidirectional):编码器是否是双向的,即同时考虑正向和反向的输入序列信息。
(2)解码器(Decoder):
输出序列的词嵌入维度(embedding_dim):与编码器的词嵌入维度类似,将目标序列中的每个单词映射到一个向量。
隐藏状态的维度(hidden_size):解码器RNN单元的隐藏状态的维度,通常与编码器的隐藏状态维度相同。
解码器的层数(num_layers):解码器RNN的层数,可以与编码器不同。
是否使用注意力机制(attention):用于在解码过程中对编码器的输出进行加权组合,以便更好地对应源语言的不同部分。
(3)其他参数:

批处理大小(batch_size):每次输入模型进行训练或推理的样本数。
词汇表大小(vocab_size):输入和输出序列的词汇表大小,即词嵌入层的输入维度。
最大序列长度(max_length):输入和输出序列的最大长度,超过该长度的序列将被截断或填充。

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()
        self.embedding = nn.Embedding(input_dim, emb_dim)
        self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
        self.dropout = nn.Dropout(dropout)

    def forward(self, src):
        # src shape: [batch_size, src_len]
        embedded = self.dropout(self.embedding(src))
        # embedded shape: [batch_size, src_len, emb_dim]
        outputs, hidden = self.rnn(embedded)
        # outputs shape: [batch_size, src_len, hid_dim]
        # hidden shape: [n_layers, batch_size, hid_dim]
        return outputs, hidden

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()
        self.output_dim = output_dim
        self.embedding = nn.Embedding(output_dim, emb_dim)
        self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
        self.fc_out = nn.Linear(hid_dim, output_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, input, hidden):
        # input shape: [batch_size, 1]
        # hidden shape: [n_layers, batch_size, hid_dim]
        
        embedded = self.dropout(self.embedding(input))
        # embedded shape: [batch_size, 1, emb_dim]
        
        output, hidden = self.rnn(embedded, hidden)
        # output shape: [batch_size, 1, hid_dim]
        # hidden shape: [n_layers, batch_size, hid_dim]
        
        prediction = self.fc_out(output.squeeze(1))
        # prediction shape: [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 shape: [batch_size, src_len]
        # trg shape: [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)
        
        _, hidden = self.encoder(src)
        
        input = trg[:, 0].unsqueeze(1)  # Start token
        
        for t in range(1, trg_len):
            output, hidden = self.decoder(input, hidden)
            outputs[:, t, :] = output
            teacher_force = random.random() < teacher_forcing_ratio
            top1 = output.argmax(1)
            input = trg[:, t].unsqueeze(1) if teacher_force else top1.unsqueeze(1)

        return outputs
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

在Seq2Seq模型中,“forward”(前向传播)是一个重要的方法,用于定义模型在输入数据上的计算过程。具体来说,在PyTorch中,每个nn.Module(包括Seq2Seq模型)都必须实现forward方法。这个方法接受输入数据作为参数,并返回模型在输入数据上的输出结果。

对于Seq2Seq模型来说,"forward"方法的主要功能是:

接收输入序列(如源语言句子)并通过编码器将其转换为上下文向量(context vector)或者编码器的输出。
将编码器的输出(上下文向量或者编码器的最后隐藏状态)作为解码器的初始隐藏状态。
解码器根据这个初始状态和目标语言的起始标记(如符号)来生成目标语言的序列。
返回解码器的输出序列,这个序列通常被用来计算损失或者直接作为模型的预测结果。
因此,"forward"方法是整个模型中最核心的部分,定义了数据从输入到输出的流动过程,同时也决定了模型在训练和推断时的行为。

4.新增术语加载词典(将所给术语集按照水平制表符划分为英文词条与中文词条并存入两个list中)与模型训练

# 新增术语词典加载部分
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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

训练模型

def train(model, iterator, optimizer, criterion, clip):
    model.train()
    epoch_loss = 0
    for i, (src, trg) in enumerate(iterator):
        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()
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
        epoch_loss += loss.item()
    return epoch_loss / len(iterator)
# 主函数
if __name__ == '__main__':
    start_time = time.time()  # 开始计时

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    #terminology = load_terminology_dictionary('../dataset/en-zh.dic')
    terminology = load_terminology_dictionary('../dataset/en-zh.dic')

    # 加载数据
    dataset = TranslationDataset('../dataset/train.txt',terminology = terminology)
    # 选择数据集的前N个样本进行训练,这里选择全集
    N = int(len(dataset) * 1)  #int(len(dataset) * 1)  # 或者你可以设置为数据集大小的一定比例,如 int(len(dataset) * 0.1)
    subset_indices = list(range(N))
    subset_dataset = Subset(dataset, subset_indices)
    train_loader = DataLoader(subset_dataset, batch_size=32, shuffle=True, collate_fn=collate_fn)

    # 定义模型参数
    INPUT_DIM = len(dataset.en_vocab)
    OUTPUT_DIM = len(dataset.zh_vocab)
    ENC_EMB_DIM = 256
    DEC_EMB_DIM = 256
    HID_DIM = 512
    N_LAYERS = 2
    ENC_DROPOUT = 0.5
    DEC_DROPOUT = 0.5

    # 初始化模型
    enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
    dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)
    model = Seq2Seq(enc, dec, device).to(device)

    # 定义优化器和损失函数
    optimizer = optim.Adam(model.parameters())
    criterion = nn.CrossEntropyLoss(ignore_index=dataset.zh_word2idx['<pad>'])

    # 训练模型
    N_EPOCHS = 10
    CLIP = 1

    for epoch in range(N_EPOCHS):
        train_loss = train(model, train_loader, optimizer, criterion, CLIP)
        print(f'Epoch: {epoch+1:02} | Train Loss: {train_loss:.3f}')
        
    # 在训练循环结束后保存模型
    torch.save(model.state_dict(), './translation_model_GRU.pth')
    
    end_time = time.time()  # 结束计时

    # 计算并打印运行时间
    elapsed_time_minute = (end_time - start_time)/60
    print(f"Total running time: {elapsed_time_minute:.2f} minutes")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

运行时

Epoch: 01 | Train Loss: 5.324
Epoch: 02 | Train Loss: 5.006
Epoch: 03 | Train Loss: 4.905
Epoch: 04 | Train Loss: 4.839
Epoch: 05 | Train Loss: 4.794
Epoch: 06 | Train Loss: 4.753
Epoch: 07 | Train Loss: 4.719
Epoch: 08 | Train Loss: 4.692
Epoch: 09 | Train Loss: 4.663
Epoch: 10 | Train Loss: 4.643
Total running time: 92.78 minutes
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5.BLEU评分

BLEU(Bilingual Evaluation Understudy)评分是一种用于评估机器翻译结果质量的指标,最初由Kishore Papineni等人在2002年提出。它通过比较机器翻译的输出与一个或多个参考翻译之间的相似性来进行评估。

具体来说,BLEU评分的计算过程如下:

精确度计算(Precision Calculation):

对于每个被翻译的句子,BLEU考虑了不同长度的n-gram(通常是1到4-gram)。
对于每种n-gram,计算机器翻译输出中匹配参考翻译的数量。
然后计算这些匹配的数量与机器翻译输出中相应n-gram总数的比例(即精确度)。
修改精确度(Modified Precision):

为了处理短句子中n-gram匹配的问题,引入了一个修正项,确保只计算存在于参考翻译中的n-gram。
加权几何平均(Weighted Geometric Mean):

将不同n-gram精确度的对数加权平均,用以平衡长短句子的影响。
BP(Brevity Penalty)惩罚:

考虑到机器翻译可能生成较短的句子,BLEU引入一个惩罚因子来调整分数,以确保翻译的长度与参考翻译相匹配。
最终BLEU分数:

将以上步骤的计算结果综合起来,得到最终的BLEU分数。通常情况下,BLEU分数越高,表示机器翻译的质量越好。
代码略过

6.在测试集上进行推理

def inference(model: Seq2Seq, dataset: TranslationDataset, src_file: str, save_dir:str, terminology, device: torch.device):
    model.eval()
    src_sentences = load_sentences(src_file)
    
    translated_sentences = []
    for src in src_sentences:
        translated = translate_sentence(src, model, dataset, terminology, device)
        #print(translated)
        translated_sentences.append(translated)
        #print(translated_sentences)

    # 将列表元素连接成一个字符串,每个元素后换行
    text = '\n'.join(translated_sentences)

    # 打开一个文件,如果不存在则创建,'w'表示写模式
    with open(save_dir, 'w', encoding='utf-8') as f:
        # 将字符串写入文件
        f.write(text)

    #return translated_sentences
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

7.调用模型输出submit文件

# 主函数
if __name__ == '__main__':
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    # 加载术语词典
    terminology = load_terminology_dictionary('../dataset/en-zh.dic')
    # 加载数据集和模型
    dataset = TranslationDataset('../dataset/train.txt',terminology = terminology)

    # 定义模型参数
    INPUT_DIM = len(dataset.en_vocab)
    OUTPUT_DIM = len(dataset.zh_vocab)
    ENC_EMB_DIM = 256
    DEC_EMB_DIM = 256
    HID_DIM = 512
    N_LAYERS = 2
    ENC_DROPOUT = 0.5
    DEC_DROPOUT = 0.5

    # 初始化模型
    enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
    dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)
    model = Seq2Seq(enc, dec, device).to(device)

    # 加载训练好的模型
    model.load_state_dict(torch.load('./translation_model_GRU.pth'))
    
    save_dir = '../dataset/submit.txt'
    inference(model, dataset, src_file="../dataset/test_en.txt", save_dir = save_dir, terminology = terminology, device = device)
    print(f"翻译完成!文件已保存到{save_dir}")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

8.提交

在这里插入图片描述

总结

只是简单得过了一遍baseline,最后输出的结果还是很差,需要进行更多的训练,与修改。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/黑客灵魂/article/detail/913375
推荐阅读
相关标签
  

闽ICP备14008679号