赞
踩
第一章 姓氏分类任务——前馈神经网络
第二章 基于循环神经网络的机器翻译
第三章 基于Transformer的机器翻译
(本文为新手小白进行的NLP实验,内容简单,仅适合新手进行借鉴,也请大佬多多指教)
目录
1.1.1 位置编码(Positional Encoding)
1.1.3 多头注意力机制(Multi-Head Attention)
Transformers是自然语言处理领域的一个突破性模型架构,由Vaswani等人在2017年的论文《Attention is All You Need》中首次提出。该模型彻底改变了序列处理任务的处理方式,尤其是机器翻译任务,通过完全依赖于自注意力(Self-Attention)机制而摒弃了传统的循环神经网络(RNN)和卷积神经网络(CNN)结构。Transformer因其并行处理能力、训练速度和翻译质量的显著提升而受到广泛关注。
由于自注意力机制本身不包含位置信息,为了解决这一问题,Transformer模型引入了“位置编码”(Positional Encoding)。位置编码是一种向输入序列中的每个位置添加特定模式的表示,使得模型能够区分不同位置上的信息。这种编码通常是基于位置的固定函数,比如正弦和余弦函数的组合(可以参考详细解释Position Encoding),从而为每个位置生成一个唯一的向量,保证了模型能理解序列中元素的顺序。
自注意力(Self-Attention)机制允许模型在处理输入序列的每个位置时,能够考虑序列中所有其他位置的信息。它通过计算输入序列中每个单词(或token)的查询(Query)、键(Key)和值(Value)的点积注意力权重,然后加权求和得到当前位置的上下文表示。这种方式有效捕捉了序列内部的长距离依赖关系,极大地提升了模型的表达能力。
自注意力机制图(图片来源于网上)
为了进一步增强模型的注意力能力,Transformer引入了多头注意力(Multi-Head Attention)机制。该机制将自注意力过程分成多个并行的“头”(heads),每个头学习不同位置间的不同关系。具体来说,输入向量被线性映射到多个查询、键和值矩阵,每个头独立执行自注意力操作,最后将所有头的结果合并。这种机制增加了模型的并行性和学习能力,使其能够捕捉更多种类的特征交互。
多头注意力机制图 (图片来源于网上)
补充——关于多头注意力的参数量问题:
掩码(Masking)在Transformer中用于控制注意力的范围,特别是在编码器-解码器结构中尤为重要。在解码器部分,未来信息的掩码(Future Masking)确保在预测当前词时模型不能看到之后的词,从而维持自回归属性。这种设计保证了模型的训练和推理过程符合语言的自然生成顺序。
目标序列的mask形式(图片来源于网上)
一般输入序列在编码器中不需要进行遮掩,因此输入的mask为全零矩阵,而目标序列要遮掩未来信息,所以mask矩阵为下三角矩阵(如上图)。
Transformer中的每个子层(包括自注意力层和前馈神经网络层)之后都紧接着残差连接(RC)和层归一化(LN)。残差连接帮助解决深度网络训练中的梯度消失问题,它使信息能够直接从早期层传递到后期层,简化了优化过程。层归一化则是为了稳定训练过程,它通过标准化每一层的输入,使得输入的分布更加稳定,加速收敛速度并提高模型性能。这两者结合,极大地提高了模型的训练效率和最终性能。
基于自注意力的残差连接和层归一化(图片来源于网上)
Transformer在机器翻译以及更广泛的语言处理任务中展现出多项显著的技术优势,这些优势主要体现在以下几个方面:
1. 并行处理能力
与基于RNN的模型不同,Transformer模型的所有操作都可以并行执行,因为它不依赖于时间序列的递归特性。这大大减少了训练和推理的时间,尤其是在处理长序列数据时,性能提升尤为明显。
2. 长距离依赖捕获
自注意力机制让Transformer能够直接学习输入序列中任意两个位置之间的依赖关系,而无需像RNN那样通过多次时间步传播信息。这使得模型在捕获长距离语义依赖方面更为高效,对于需要理解复杂上下文的语言任务至关重要。
3. 更强的表达能力
多头注意力机制让模型能够同时关注输入的不同表示子空间,增强了对不同类型特征交互的学习能力,这种机制不仅提升了模型的表达能力,也使得模型能够更灵活地处理多种语言结构和语义。
4. 稳定且高效的训练
残差连接加上层归一化的使用,使得Transformer在深层网络结构下也能保持良好的梯度流动和稳定的训练表现。这有助于模型学习更复杂的语言模式,同时避免了训练过程中的梯度消失或爆炸问题。
本次实验需要大量使用GPU,因此使用天池的平台进行运行,此处给出简单教程,没有用过的新手会有三个月的使用额度(注意不要超过时长)。
(注册过程此处略)
注册之后,可以创建交互式建模的实例,其中可以直接选取要配置的环境和设备,不需要自己再进行配置(如有需要也可以自己配置)。
torch == 1.8.2+pai
torchtext == 0.9.2
torchvision == 0.9.2+cu101transformers == 4.2.2
至此环境的配置就可以了,接下来开始实现模型训练。
首先,我们选取中日语料(from JParaCrawl)来进行本次实验,下载已经用spm训练好的中文分词器(此处采用的英文分词器进行分词,效果还可以)和日文分词器对文本进行分词和索引映射处理,并对数据集进行训练集和验证集的划分,
- df = pd.read_csv('zh-en.txt', sep='\\t', engine='python', header=None)
- trainen = df[0].values.tolist()#[:10000]
- trainja = df[1].values.tolist()#[:10000]
- # trainen.pop(5972)
- # trainja.pop(5972)
中日数据集
- en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')
- ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')
- en_tokenizer.encode("设施: 停车场, 准许携带宠物, 24小时前台")
- ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。")
分词器对文本进行编码
- from sklearn.model_selection import train_test_split
- # 划分训练集和验证集
- train_en, val_en, train_ja, val_ja = train_test_split(trainen, trainja, test_size=0.25, random_state=42, shuffle=True, stratify=None)
- print(f"训练集中文长度: {len(train_en)}, 训练集日文长度: {len(train_ja)}")
- print(f"验证集中文长度: {len(val_en)}, 验证集日文长度: {len(val_ja)}")
训练集和验证集大小
然后,我们用分词后的文本去创建输入输出的词汇表(词汇表不要忘记特殊字符的添加),利用词汇表完成整个数据集的映射处理,得到张量形式的训练数据。
- def build_vocab(sentences, tokenizer):
- counter = Counter()
- for sentence in sentences:
- counter.update(tokenizer.encode(sentence, out_type=str))
- return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
- ja_vocab = build_vocab(trainja, ja_tokenizer)
- en_vocab = build_vocab(trainen, en_tokenizer)
- def data_process(ja, en):
- data = []
- for (raw_ja, raw_en) in zip(ja, en):
- ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
- dtype=torch.long) # 现将数据集切分为句子,然后从句子中分别提取tokens,在词汇表中映射为索引,索引为为长整型张量
- en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
- dtype=torch.long)
- data.append((ja_tensor_, en_tensor_)) # 将两个张量作为一个元素放到列表中
- return data
- train_data = data_process(trainja, trainen)
之后,为了便于模型训练,我们将数据加上开始结束索引,转换为批次的形式,并用Dataloader对象储存。
- BATCH_SIZE = 32
- PAD_IDX = ja_vocab['<pad>']
- BOS_IDX = ja_vocab['<bos>']
- EOS_IDX = ja_vocab['<eos>']
- def generate_batch(data_batch):
- ja_batch, en_batch = [], []
- for (ja_item, en_item) in data_batch:
- ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
- en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
- ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
- en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
- return ja_batch, en_batch
- train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
- shuffle=True, collate_fn=generate_batch)
- val_iter = DataLoader(val_data, batch_size=BATCH_SIZE,
- shuffle=True, collate_fn=generate_batch)
我们按照论文中所描述的Transformer架构来搭建模型,其中编码器和解码器由位置编码、词嵌入、多头注意力机制、残差连接、层归一化和前向传播等模块组成,区别在于解码器的自注意力层会采用mask对未来信息进行遮掩,而且解码器需要处理编码器的输出来结合上下文。此处采用nn中模块化的编码器层和解码器层。
- from torch.nn import (TransformerEncoder, TransformerDecoder,
- TransformerEncoderLayer, TransformerDecoderLayer)
-
-
- class Seq2SeqTransformer(nn.Module):
- def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
- emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
- dim_feedforward:int = 512, dropout:float = 0.1):
- # 模型输入维度emb_size ,源语言词汇表大小src_vocab_size,目标语言词汇表大小;tgt_vocab_size:
- super(Seq2SeqTransformer, self).__init__()
- encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
- dim_feedforward=dim_feedforward)
- self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
- decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
- dim_feedforward=dim_feedforward)
- self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
-
- self.generator = nn.Linear(emb_size, tgt_vocab_size) # 为什么没加softmax?
- # 创建词嵌入层
- self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
- self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)
- # 创建位置编码层
- self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)
-
- def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
- tgt_mask: Tensor, src_padding_mask: Tensor,
- tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
- # padding_mask用于处理padding的相关信息,mask用于防止看到未来信息,确保每个位置智能看到过去信息
- src_emb = self.positional_encoding(self.src_tok_emb(src))
- tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))
- memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask) # memory就是编码器输出的键和值(K,V)
- outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
- tgt_padding_mask, memory_key_padding_mask)
- return self.generator(outs) # 对编码器输出结果进行映射,如果加上softmax就是将其映射为预测词的概率分布
-
- def encode(self, src: Tensor, src_mask: Tensor):
- return self.transformer_encoder(self.positional_encoding(
- self.src_tok_emb(src)), src_mask)
-
- def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
- return self.transformer_decoder(self.positional_encoding(
- self.tgt_tok_emb(tgt)), memory,
- tgt_mask)
-
- class PositionalEncoding(nn.Module):
- def __init__(self, emb_size: int, dropout, maxlen: int = 5000):
- super(PositionalEncoding, self).__init__()
- den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)
- # 生成一个只有emb_size一半长度的张量作为缩减因子,不包含emb_size这个位,以避免长度超过
- # emb_size就是paper中的d_model
- pos = torch.arange(0, maxlen).reshape(maxlen, 1)
- pos_embedding = torch.zeros((maxlen, emb_size))
- # 共有maxlen个位置,每个位置由emb_size长度向量进行编码,也就是一个词对应的嵌入向量
- pos_embedding[:, 0::2] = torch.sin(pos * den) # 偶数维度上进行正弦计算
- pos_embedding[:, 1::2] = torch.cos(pos * den) # 奇数维度上进行余弦计算
- pos_embedding = pos_embedding.unsqueeze(-2) # 加入批次维度,允许位置编码在批次维度上进行广播
-
- self.dropout = nn.Dropout(dropout)
- self.register_buffer('pos_embedding', pos_embedding) # 持久化张量,在模型训练和推理过程中保持不变
-
- def forward(self, token_embedding: Tensor):
- return self.dropout(token_embedding +
- self.pos_embedding[:token_embedding.size(0),:]) # 位置向量+嵌入向量
-
- class TokenEmbedding(nn.Module):
- def __init__(self, vocab_size: int, emb_size):
- super(TokenEmbedding, self).__init__()
- self.embedding = nn.Embedding(vocab_size, emb_size)
- self.emb_size = emb_size
- def forward(self, tokens: Tensor):
- return self.embedding(tokens.long()) * math.sqrt(self.emb_size) # 嵌入向量乘以缩放因子
下面是mask的生成机制,解码器的mask是下三角的-inf矩阵(其中-inf表示该位置权重为0,达到遮掩信息的目的),用来遮掩未来信息,而编码器的mask是全零矩阵(0表示对该位置权重不造成影响),不需要对信息进行遮掩。
- def generate_square_subsequent_mask(sz):
- mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1) # 创建一个下三角矩阵,作为mask
- mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
- # 对mask进行填充,-inf在softmax操作中代表了0,即该位置权重为0,达到屏蔽未来信息的目的
- # 0 在 softmax中表示对当前位置权重不造成影响
- return mask
-
- def create_mask(src, tgt):
- src_seq_len = src.shape[0]
- tgt_seq_len = tgt.shape[0]
-
- tgt_mask = generate_square_subsequent_mask(tgt_seq_len) # 生成解码器的掩码矩阵
- src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool) # 生成编码器的全零布尔型掩码矩阵
-
- src_padding_mask = (src == PAD_IDX).transpose(0, 1) # 找到填充的位置,从而生成填充的掩码矩阵
- tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
- return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask
完成上述数据处理与模型搭建后,我们开始正式训练模型,参数采用论文《Attention is All you Need》中的设置,因为数据训练一轮需要7-8分钟左右,此处设置epoch为12。
为了使训练数据可视化和具有可复现性,我们在训练中保存每一轮的loss进行train和val的损失图绘制,并保存训练最好的模型参数。
- SRC_VOCAB_SIZE = len(ja_vocab)
- TGT_VOCAB_SIZE = len(en_vocab)
- EMB_SIZE = 512 # 嵌入向量维度
- NHEAD = 8 # 注意力头数量
- FFN_HID_DIM = 512
- BATCH_SIZE = 16
- NUM_ENCODER_LAYERS = 3
- NUM_DECODER_LAYERS = 3
- NUM_EPOCHS = 16
- transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
- EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
- FFN_HID_DIM)
-
- for p in transformer.parameters(): # 获取模型所有可学习的参数
- if p.dim() > 1: # 一般权重矩阵都是多维的,偏置项是单维度的
- nn.init.xavier_uniform_(p) # 对权重矩阵进行Xavier初始化,保持前向传播时方差一致,加速训练过程
-
- transformer = transformer.to(device) # 将模型转移到训练设备上
-
- loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX) # 采用交叉熵损失并忽略填充位置的损失
-
- optimizer = torch.optim.Adam(
- transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
- ) # 采用paper中所用的optimizer的参数,学习率没有按照paper中的方法计算
- def train_epoch(model, train_iter, optimizer):
- model.train()
- losses = 0
- for idx, (src, tgt) in enumerate(train_iter):
- src = src.to(device) # 将数据放入相应设备
- tgt = tgt.to(device)
-
- tgt_input = tgt[:-1, :] # 与实际应用场景对齐,实际预测中是不知道序列终点位置的,去除结束字符来模拟这种过程
- # 也确保了输入与输出对齐且相差一个位置,输出的结果是从第一个字符开始而非开始字符,因此计算损失时用tgt[1:,:]来计算
- src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input) # 生成掩码矩阵
-
- logits = model(src, tgt_input, src_mask, tgt_mask,
- src_padding_mask, tgt_padding_mask, src_padding_mask)
-
- optimizer.zero_grad() # 参数梯度归零
-
- tgt_out = tgt[1:,:]
- loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
- # 将两个二维张量reshape为一维张量进行loss计算
- loss.backward() # 反向传播
-
- optimizer.step() #根据累计梯度更新参数
- losses += loss.item()
- return losses / len(train_iter)
-
-
- def evaluate(model, val_iter):
- model.eval() # 模型切换到评估模式
- losses = 0 # 过程与训练相似,去除了更新参数的部分
- for idx, (src, tgt) in (enumerate(valid_iter)):
- src = src.to(device)
- tgt = tgt.to(device)
-
- tgt_input = tgt[:-1, :]
-
- src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)
-
- logits = model(src, tgt_input, src_mask, tgt_mask,
- src_padding_mask, tgt_padding_mask, src_padding_mask)
- tgt_out = tgt[1:,:]
- loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
- losses += loss.item()
- return losses / len(val_iter)
- import matplotlib.pyplot as plt
-
- def plot_losses(train_losses, val_losses):
- """Plot and save both training and validation losses."""
- plt.figure(figsize=(10, 5))
-
- # Plot training losses
- plt.plot(train_losses, label='Training Loss', color='blue')
-
- # Plot validation losses
- plt.plot(val_losses, label='Validation Loss', color='green')
-
- plt.title('Training and Validation Loss vs. Epochs')
- plt.xlabel('Epochs')
- plt.ylabel('Loss')
- plt.legend()
-
- # Save the plot
- plt.savefig('training_and_validation_loss.png')
- plt.show()
-
-
- train_losses = []
- val_losses = []
-
- best_val_loss = float('inf')
- for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
- start_time = time.time()
- train_loss = train_epoch(transformer, train_iter, optimizer)
- val_loss = evaluate(transformer, val_iter)
- end_time = time.time()
- print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, Val loss: {val_loss:.3f}, "
- f"Epoch time = {(end_time - start_time):.3f}s"))
- # 记录loss
- train_losses.append(train_loss)
- val_losses.append(val_loss)
- # 保存最优模型
- if val_loss < best_val_loss:
- best_val_loss = val_loss
- torch.save({
- 'epoch': epoch,
- 'model_state_dict': transformer.state_dict(),
- 'optimizer_state_dict': optimizer.state_dict(),
- 'loss': val_loss,
- }, 'best_model_layer4_checkpoint.tar')
- print("模型已更新并保存为'best_model_layer4_checkpoint.tar'")
-
- plot_losses(train_losses,val_losses)
模型训练
(EMB_SIZE = 512
NHEAD = 8
FFN_HID_DIM = 2048
BATCH_SIZE = 32
NUM_ENCODER_LAYERS = 4
NUM_DECODER_LAYERS = 4)
在预测过程中,实验采用较为简单的贪婪解码方法,大致过程与训练是相似的(其中解码器部分要加上mask,否则会违背自回归原则,导致生成序列逻辑混乱),选取解码器输出中概率分布最高的索引。
- def greedy_decode(model, src, src_mask, max_len, start_symbol):
- # 用训练好的模型进行贪婪解码
- src = src.to(device)
- src_mask = src_mask.to(device)
- memory = model.encode(src, src_mask) # 编码得到输出
- ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device) # 初始化预测输出序列,第一个为开始字符
- for i in range(max_len-1):
- memory = memory.to(device)
- # 创建全False掩码,此时不需要对记忆进行遮挡
- memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)
- # 根据逐渐生成的预测输出来生成相应掩码矩阵,保证解码过程中不会泄露未来信息
- tgt_mask = (generate_square_subsequent_mask(ys.size(0))
- .type(torch.bool)).to(device)
- out = model.decode(ys, memory, tgt_mask)
- out = out.transpose(0, 1) # 输出格变为(vocab_size,ys_seq_len)
- prob = model.generator(out[:, -1]) # 选取最后一个维度输出向量,也就是当前时间步预测的词向量进行映射
- _, next_word = torch.max(prob, dim = 1) # 选取概率最大的一个词
- next_word = next_word.item() # 取出标量值
- ys = torch.cat([ys,
- torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0) # 将预测词与先前预测的词序列拼接
- if next_word == EOS_IDX: # 如果预测到结束则终止预测
- break
- return ys
- def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
- model.eval() # 开启model评估模式
- # 对src进行映射并加上开始和结束符号,得到索引表示
- tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)]+ [EOS_IDX]
- num_tokens = len(tokens)
- src = (torch.LongTensor(tokens).reshape(num_tokens, 1) ) # 将其转换为长整型并reshape为一个维度一个词向量形式
- src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool) # 全False掩码
- tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()
- # 进行贪婪解码,max_len中的+5是为了预防模型预测出比输入长太多的序列(这是翻译模型)
- return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")
- # 将输出的索引映射为字符并且替换开始和结束符号为空
我们采用上面训练效果较好的模型训练2得到的模型进行机器翻译,翻译示例如下所示:
预测文本(翻译文本——日译中)
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
translate(transformer, "この効果は1ターンに1度だけ自分のメインフェイズに使用する事ができる。", ja_vocab, en_vocab, ja_tokenizer)
translate(transformer, "をキャンバスのまわりに追加して、ページをめくる際に、本の外側にも紙がはみ出すことができるようにしています。", ja_vocab, en_vocab, ja_tokenizer)
translate(transformer, "気体ポンプ、真空ポンプ、気体圧縮機及びファン並びに換気用又は循環用のフード(ファンを自蔵するものに限るものとし、フィルターを取り付。", ja_vocab, en_vocab, ja_tokenizer)
为了模型更好的分词效果,我们用spm训练一个中文分词器进行分词,训练过程如下(借鉴Github上spm训练中文分词器):
- sentences = []
- with open("ch-eng-large-pro.txt",encoding="utf-8") as file: #加载文档
- data = file.read().split("\n")
- for row in data: #删除无用字符
- row = row.strip()
- sentences.append(row)
- # print(sentence)
- with open("ch-eng-large-processed.txt", "w", encoding="utf-8") as file:
- file.write("\n".join(sentences))
-
- import sentencepiece as spm
- spm.SentencePieceTrainer.train(
- input='ch-eng-large-processed.txt',
- model_type="bpe",
- model_prefix='zh_tokenizer', #输出模型名称前缀。训练完成后将生成 <model_name>.model 和 <model_name>.vocab 文件。
- vocab_size=32000, #目标词表的大小
- user_defined_symbols=[],#可以设置自己特定的token,并且这个token不会被拆成子词
- character_coverage=1, #覆盖字符的比例
- max_sentencepiece_length=6 #最大的基本单元,也就是在这个单元内的词进行合并
- )
-
-
- # 加载训练好的模型
- model_path = "zh_tokenizer.model"
- tokenizer = spm.SentencePieceProcessor()
- tokenizer.load(model_path)
-
- # 测试分词器
- text = "我不知道應該說什麼才好。"
- tokens = tokenizer.encode_as_pieces(text)
-
- print("分词结果:", tokens)
分词结果示例
模型训练1
(EMB_SIZE = 512
NHEAD = 8
FFN_HID_DIM = 1024
BATCH_SIZE = 32
NUM_ENCODER_LAYERS = 6
NUM_DECODER_LAYERS = 6)
模型训练2
(EMB_SIZE = 512
NHEAD = 8
FFN_HID_DIM = 1024
BATCH_SIZE = 32
NUM_ENCODER_LAYERS = 4
NUM_DECODER_LAYERS = 4)
预测文本(翻译文本——中译英)
从上述预测结果来看,与上一篇实验博客的RNN模型相比,Transformer有了明显的进步,训练过程中loss远小于RNN模型。另外,训练好的模型能够将句子的大概语义翻译出来,虽然有些语义翻译不是很准确,但是已经做到了基础的翻译功能。
本篇博客接着上次的基于注意力机制的RNN模型的机器翻译任务,对新手有一定参考价值,还请大佬多多指教。
本文主要讲述采用Transformer模型进行机器翻译,采用编码器-解码器结构进行训练,总的来说简单地复现了论文中所提到的Transformer架构,完成了中日翻译和中英翻译两个任务的训练、预测等流程。
技术背景部分简单的写出作者对Transformer架构的一些理解,如:多头注意力机制、掩码机制、位置编码等独特的技术,还有不借助循环神经网络,只需要靠注意力机制就能完成训练的Transformer模型。后续如果继续深入,还会进一步学习由其延伸出的GPT和BERT模型。
技术实现部分则是从如何搭建环境入手(包括新手如何获取GPU算力资源),学习如何用spm训练分词器,借助分词器搭建词汇表映射,完成对文本数据的处理(一种新的分词方式,针对不能用空格分开单词的语言能够更好地进行分词)。然后,用搭建的Transformer基础架构(位置编码,词嵌入,编码器,解码器)完成机器翻译任务的训练阶段。从训练阶段我们可以看出Transformer比RNN的效果要好了很多,能够更加准确的实施翻译(也有可能是采用的数据集依旧不够大,有条件可以尝试用百万条语料进行尝试,进行更精确的对比)。其中也能够看出英语这一类单词可以用空格符号切分的语言和中文、日语这种不是用空格分开单词的语言之间在分词上的不同之处,体现出自然语言处理的多样性和复杂性。
到此为止,机器翻译就此告一段落,学习实践的过程中,作者对循环神经网络和Transformer有了深入的了解,对NLP任务的实践也有了一定的基础,接下来会尝试学习更多关于NLP模型相关的知识。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。