自然语言处理

一、自然语言处理前馈网络

1.引言

前馈神经网络(Feed-Forward Neural Network, FFNN)是自然语言处理(Natural Language Processing, NLP)中的重要组成部分,常用于各种任务如文本分类、序列标注和机器翻译。本实验旨在探索FFNN在NLP任务中的性能,通过对文本分类任务进行实验,分析其效果。

2. 背景知识

2.1前馈神经网络

前馈神经网络是一种人工神经网络,其中信息沿单向流动,从输入层经过若干隐藏层到输出层。每一层的节点与上一层的所有节点相连接,并通过激活函数引入非线性。

2.2自然语言处理

自然语言处理是计算机科学和人工智能的一个分支,致力于实现计算机理解和生成人类语言。常见的NLP任务包括情感分析、命名实体识别、机器翻译等。

2.3MLP结构

除了输入输出层,它中间可以有多个隐层,最简单的MLP只含一个隐层,即三层的结构

2.4单层感知机

 单层感知机是一种最简单的神经网络,可以利用其对一些线性可分的问题进行求解,即生成一条可以完美分割两个类别的分割线。

2.5多层感知机

针对线性不可分问题,多层感知机就是利用多个感知机对同一个问题进行多次划分,每次划分可以将一个问题切分成两类,使得问题中的同一类处在同一个象限,不同类处在不同象限

2.6激活函数

MLP中的每个神经元通常都会应用一个非线性激活函数,如Sigmoid、ReLU、Tanh等。这些激活函数引入了非线性因素,使得神经网络能够学习复杂的数据模式。

3. 实验介绍

我们将探索传统上称为前馈网络的神经网络模型,以及两种前馈神经网络:多层感知器和卷积神经网络,多层感知器将多个感知器分组在一个单层,并将多个层叠加在一起。我们稍后将介绍多层感知器,并在“示例:带有多层感知器的姓氏分类”中展示它们在多层分类中的应用。

本实验研究的第二种前馈神经网络,卷积神经网络,在处理数字信号时深受窗口滤波器的启发。通过这种窗口特性,卷积神经网络能够在输入中学习局部化模式,这不仅使其成为计算机视觉的主轴,而且是检测单词和句子等序列数据中的子结构的理想候选。我们在“卷积神经网络”中概述了卷积神经网络,并在“示例:使用CNN对姓氏进行分类”中演示了它们的使用

在本实验中,多层感知器和卷积神经网络被分组在一起,因为它们都是前馈神经网络,并且与另一类神经网络——递归神经网络(RNNs)形成对比,递归神经网络(RNNs)允许反馈(或循环),这样每次计算都可以从之前的计算中获得信息。我们将介绍RNNs以及为什么允许网络结构中的循环是有益的。

在我们介绍这些不同的模型时,需要理解事物如何工作的一个有用方法是在计算数据张量时注意它们的大小和形状。每种类型的神经网络层对它所计算的数据张量的大小和形状都有特定的影响,理解这种影响可以极大地有助于对这些模型的深入理解。

4.实验要点

通过“示例:带有多层感知器的姓氏分类”,掌握多层感知器在多层分类中的应用

掌握每种类型的神经网络层对它所计算的数据张量的大小和形状的影响

5.实验步骤

第三方库导入

from argparse import Namespace
from collections import Counter
import json
import os
import string

import numpy as np
import pandas as pd

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm_notebook

Vocabulary类用于管理文本的词汇表,支持以下功能:可以初始化为空或使用预定义的词汇-索引映射;可以添加新单词到词汇表中,并返回其索引;支持将词汇表序列化为字典以便存储,并可以从序列化的字典中重新构建词汇表;可以查找单词对应的索引,如果单词不存在则返回UNK(未知)标记的索引;还能通过索引查找对应的单词;类还实现了__str____len__方法,用于返回词汇表的信息和大小。

class Vocabulary(object):
    """Class to process text and extract vocabulary for mapping"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        Args:
            token_to_idx (dict): a pre-existing map of tokens to indices
            add_unk (bool): a flag that indicates whether to add the UNK token
            unk_token (str): the UNK token to add into the Vocabulary
        """

        if token_to_idx is None:
            token_to_idx = {}
        self._token_to_idx = token_to_idx

        self._idx_to_token = {idx: token 
                              for token, idx in self._token_to_idx.items()}
        
        self._add_unk = add_unk
        self._unk_token = unk_token
        
        self.unk_index = -1
        if add_unk:
            self.unk_index = self.add_token(unk_token) 
        
        
    def to_serializable(self):
        """ returns a dictionary that can be serialized """
        return {'token_to_idx': self._token_to_idx, 
                'add_unk': self._add_unk, 
                'unk_token': self._unk_token}

    @classmethod
    def from_serializable(cls, contents):
        """ instantiates the Vocabulary from a serialized dictionary """
        return cls(**contents)

    def add_token(self, token):
        """Update mapping dicts based on the token.

        Args:
            token (str): the item to add into the Vocabulary
        Returns:
            index (int): the integer corresponding to the token
        """
        try:
            index = self._token_to_idx[token]
        except KeyError:
            index = len(self._token_to_idx)
            self._token_to_idx[token] = index
            self._idx_to_token[index] = token
        return index
    
    def add_many(self, tokens):
        """Add a list of tokens into the Vocabulary
        
        Args:
            tokens (list): a list of string tokens
        Returns:
            indices (list): a list of indices corresponding to the tokens
        """
        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        """Retrieve the index associated with the token 
          or the UNK index if token isn't present.
        
        Args:
            token (str): the token to look up 
        Returns:
            index (int): the index corresponding to the token
        Notes:
            `unk_index` needs to be >=0 (having been added into the Vocabulary) 
              for the UNK functionality 
        """
        if self.unk_index >= 0:
            return self._token_to_idx.get(token, self.unk_index)
        else:
            return self._token_to_idx[token]

    def lookup_index(self, index):
        """Return the token associated with the index
        
        Args: 
            index (int): the index to look up
        Returns:
            token (str): the token corresponding to the index
        Raises:
            KeyError: if the index is not in the Vocabulary
        """
        if index not in self._idx_to_token:
            raise KeyError("the index (%d) is not in the Vocabulary" % index)
        return self._idx_to_token[index]

    def __str__(self):
        return "<Vocabulary(size=%d)>" % len(self)

    def __len__(self):
        return len(self._token_to_idx)
    #说明一个名为 Vocabulary 的类,该类用于处理文本并提取词汇以进行映射

SurnameVectorizer类是一个协调两个词汇表(姓氏词汇表和国籍词汇表)并将它们用于实际应用的向量化器。通过初始化传入的姓氏和国籍词汇表,它可以将给定的姓氏字符串转换为一种简化的one-hot编码表示。该类提供了从数据框实例化的方法,可以从姓氏数据框中构建词汇表,并支持将实例序列化为可存储的字典形式,以便后续使用或存档

class SurnameVectorizer(object):
    """ The Vectorizer which coordinates the Vocabularies and puts them to use"""
    def __init__(self, surname_vocab, nationality_vocab):
        """
        Args:
            surname_vocab (Vocabulary): maps characters to integers
            nationality_vocab (Vocabulary): maps nationalities to integers
        """
        self.surname_vocab = surname_vocab
        self.nationality_vocab = nationality_vocab

    def vectorize(self, surname):
        """
        Args:
            surname (str): the surname

        Returns:
            one_hot (np.ndarray): a collapsed one-hot encoding 
        """
        vocab = self.surname_vocab
        one_hot = np.zeros(len(vocab), dtype=np.float32)
        for token in surname:
            one_hot[vocab.lookup_token(token)] = 1

        return one_hot

    @classmethod
    def from_dataframe(cls, surname_df):
        """Instantiate the vectorizer from the dataset dataframe
        
        Args:
            surname_df (pandas.DataFrame): the surnames dataset
        Returns:
            an instance of the SurnameVectorizer
        """
        surname_vocab = Vocabulary(unk_token="@")
        nationality_vocab = Vocabulary(add_unk=False)

        for index, row in surname_df.iterrows():
            for letter in row.surname:
                surname_vocab.add_token(letter)
            nationality_vocab.add_token(row.nationality)

        return cls(surname_vocab, nationality_vocab)

    @classmethod
    def from_serializable(cls, contents):
        surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
        nationality_vocab =  Vocabulary.from_serializable(contents['nationality_vocab'])
        return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab)

    def to_serializable(self):
        return {'surname_vocab': self.surname_vocab.to_serializable(),
                'nationality_vocab': self.nationality_vocab.to_serializable()}

SurnameDataset类旨在处理姓氏数据集的加载、管理和向量化操作。通过传入的姓氏数据框和一个SurnameVectorizer实例,SurnameDataset能够根据数据框中的分区标签(训练、验证和测试)对数据集进行拆分,并分别计算各个分区的大小。其核心功能包括:

初始化:接收数据框和向量化器,并自动拆分数据集为训练、验证和测试集,同时计算并存储各分区的大小。

设置数据分区:通过 set_split 方法可动态选择当前使用的数据分区(训练、验证或测试)。

类别权重计算:依据数据框中不同国籍标签的频次,计算类别权重,以便在训练过程中处理类别不平衡问题。权重可以在后续的损失计算中加以利用。

加载数据集和生成向量化器:提供了 load_dataset_and_make_vectorizer 类方法,可以从给定的CSV文件加载数据集,并基于训练数据生成新的向量化器。

加载数据集和现有向量化器:通过 load_dataset_and_load_vectorizer 类方法,从CSV文件加载数据集,并从指定路径加载预先保存的向量化器。

class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        Args:
            surname_df (pandas.DataFrame): the dataset
            vectorizer (SurnameVectorizer): vectorizer instatiated from dataset
        """
        self.surname_df = surname_df
        self._vectorizer = vectorizer

        self.train_df = self.surname_df[self.surname_df.split=='train']
        self.train_size = len(self.train_df)

        self.val_df = self.surname_df[self.surname_df.split=='val']
        self.validation_size = len(self.val_df)

        self.test_df = self.surname_df[self.surname_df.split=='test']
        self.test_size = len(self.test_df)

        self._lookup_dict = {'train': (self.train_df, self.train_size),
                             'val': (self.val_df, self.validation_size),
                             'test': (self.test_df, self.test_size)}

        self.set_split('train')
        
        # Class weights
        class_counts = surname_df.nationality.value_counts().to_dict()
        def sort_key(item):
            return self._vectorizer.nationality_vocab.lookup_token(item[0])
        sorted_counts = sorted(class_counts.items(), key=sort_key)
        frequencies = [count for _, count in sorted_counts]
        self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)

    @classmethod
    def load_dataset_and_make_vectorizer(cls, surname_csv):
        """Load dataset and make a new vectorizer from scratch
        
        Args:
            surname_csv (str): location of the dataset
        Returns:
            an instance of SurnameDataset
        """
        surname_df = pd.read_csv(surname_csv)
        train_surname_df = surname_df[surname_df.split=='train']
        return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))

    @classmethod
    def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
        """Load dataset and the corresponding vectorizer. 
        Used in the case in the vectorizer has been cached for re-use
        
        Args:
            surname_csv (str): location of the dataset
            vectorizer_filepath (str): location of the saved vectorizer
        Returns:
            an instance of SurnameDataset
        """
        surname_df = pd.read_csv(surname_csv)
        vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
        return cls(surname_df, vectorizer)

    @staticmethod
    def load_vectorizer_only(vectorizer_filepath):
        """a static method for loading the vectorizer from file
        
        Args:
            vectorizer_filepath (str): the location of the serialized vectorizer
        Returns:
            an instance of SurnameVectorizer
        """
        with open(vectorizer_filepath) as fp:
            return SurnameVectorizer.from_serializable(json.load(fp))

    def save_vectorizer(self, vectorizer_filepath):
        """saves the vectorizer to disk using json
        
        Args:
            vectorizer_filepath (str): the location to save the vectorizer
        """
        with open(vectorizer_filepath, "w") as fp:
            json.dump(self._vectorizer.to_serializable(), fp)

    def get_vectorizer(self):
        """ returns the vectorizer """
        return self._vectorizer

    def set_split(self, split="train"):
        """ selects the splits in the dataset using a column in the dataframe """
        self._target_split = split
        self._target_df, self._target_size = self._lookup_dict[split]

    def __len__(self):
        return self._target_size

    def __getitem__(self, index):
        """the primary entry point method for PyTorch datasets
        
        Args:
            index (int): the index to the data point 
        Returns:
            a dictionary holding the data point's:
                features (x_surname)
                label (y_nationality)
        """
        row = self._target_df.iloc[index]

        surname_vector = \
            self._vectorizer.vectorize(row.surname)

        nationality_index = \
            self._vectorizer.nationality_vocab.lookup_token(row.nationality)

        return {'x_surname': surname_vector,
                'y_nationality': nationality_index}

    def get_num_batches(self, batch_size):
        """Given a batch size, return the number of batches in the dataset
        
        Args:
            batch_size (int)
        Returns:
            number of batches in the dataset
        """
        return len(self) // batch_size

    
def generate_batches(dataset, batch_size, shuffle=True,
                     drop_last=True, device="cpu"): 
    """
    A generator function which wraps the PyTorch DataLoader. It will 
      ensure each tensor is on the write device location.
    """
    dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
                            shuffle=shuffle, drop_last=drop_last)

    for data_dict in dataloader:
        out_data_dict = {}
        for name, tensor in data_dict.items():
            out_data_dict[name] = data_dict[name].to(device)
        yield out_data_dict
        #定义了一个名为 SurnameDataset 的自定义数据集类,用于处理姓氏数据集,包含国籍标签

SurnameClassifier是一个用于姓氏分类的神经网络模型,采用2层多层感知机结构。它包括两个全连接线性层(fc1fc2),依次对输入数据进行转换。第一个层(fc1)接受大小为input_dim的输入向量,并输出大小为hidden_dim的中间向量,在此过程中应用了ReLU激活函数以引入非线性。第二个层(fc2)将这些中间向量转换为最终的输出向量,大小为output_dim

在前向传播过程中,输入张量(x_in)通过fc1处理,生成intermediate_vector,然后将其传入fc2生成prediction_vector。如果设置apply_softmaxTrue,则在输出层应用softmax函数,沿着第1维度对输出进行归一化,以生成适合进行分类预测的概率分布。

class SurnameClassifier(nn.Module):
    """ A 2-layer Multilayer Perceptron for classifying surnames """
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): the size of the input vectors
            hidden_dim (int): the output size of the first Linear layer
            output_dim (int): the output size of the second Linear layer
        """
        super(SurnameClassifier, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim) # 定义第一个线性层
        self.fc2 = nn.Linear(hidden_dim, output_dim) # 定义第二个线性层

    def forward(self, x_in, apply_softmax=False):
        """The forward pass of the classifier
        
        Args:
            x_in (torch.Tensor): an input data tensor. 
                x_in.shape should be (batch, input_dim)
            apply_softmax (bool): a flag for the softmax activation
                should be false if used with the Cross Entropy losses
        Returns:
            the resulting tensor. tensor.shape should be (batch, output_dim)
        """
        intermediate_vector = F.relu(self.fc1(x_in))# 使用 ReLU 激活函数进行第一个线性层的前向传播
        prediction_vector = self.fc2(intermediate_vector) # 第二个线性层的前向传播

        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1) # 如果需要应用 softmax,则应用于预测向量

        return prediction_vector

make_train_state 函数:用于创建和初始化训练状态字典。返回包含训练状态信息的字典,包括早停标志、早停步数、最佳验证损失、学习率、当前轮次、训练和验证的损失与准确率、测试集损失与准确率、模型文件名等

update_train_state 函数:用于更新训练状态。执行早停监控和模型检查点保存

compute_accuracy 函数:计算预测结果的准确率。找到预测值的最大索引,与目标值进行比较,计算正确预测的数量,并返回准确率百分比

def make_train_state(args):
      """
    创建训练状态

    :param args: 主要参数
    :return: 包含训练状态值的字典
    """
    return {
        'stop_early': False,  # 是否早停
        'early_stopping_step': 0,  # 早停步数
        'early_stopping_best_val': 1e8,  # 最佳验证集损失
        'learning_rate': args.learning_rate,  # 学习率
        'epoch_index': 0,  # 当前迭代轮数
        'train_loss': [],  # 训练损失
        'train_acc': [],  # 训练准确率
        'val_loss': [],  # 验证集损失
        'val_acc': [],  # 验证集准确率
        'test_loss': -1,  # 测试集损失
        'test_acc': -1,  # 测试集准确率
        'model_filename': args.model_state_file  # 模型文件名
    }

def update_train_state(args, model, train_state):
    """Handle the training state updates.

    Components:
     - Early Stopping: Prevent overfitting.
     - Model Checkpoint: Model is saved if the model is better

    :param args: main arguments
    :param model: model to train
    :param train_state: a dictionary representing the training state values
    :returns:
        a new train_state
    """

     # 至少保存一个模型
    if train_state['epoch_index'] == 0:
        torch.save(model.state_dict(), train_state['model_filename'])
        train_state['stop_early'] = False

    # 如果性能提高则保存模型
    elif train_state['epoch_index'] >= 1:
        loss_tm1, loss_t = train_state['val_loss'][-2:]

        # 如果损失恶化
        if loss_t >= train_state['early_stopping_best_val']:
            # 更新步骤
            train_state['early_stopping_step'] += 1
        # 损失减少
        else:
            # 保存最佳模型
            if loss_t < train_state['early_stopping_best_val']:
                torch.save(model.state_dict(), train_state['model_filename'])

            # 重置早停步数
            train_state['early_stopping_step'] = 0

        # 是否早停?
        train_state['stop_early'] = \
            train_state['early_stopping_step'] >= args.early_stopping_criteria

    return train_state

def compute_accuracy(y_pred, y_target):
    _, y_pred_indices = y_pred.max(dim=1)
    n_correct = torch.eq(y_pred_indices, y_target).sum().item()
    return n_correct / len(y_pred_indices) * 100

设置训练环境和参数。首先,通过 set_seed_everywhere 函数设置全局随机种子,以确保训练过程的可重现性。然后,使用 handle_dirs 函数确保指定的模型保存目录存在,如果不存在则进行创建。接着,根据系统是否支持 CUDA,确定是否在 CUDA 设备上执行代码。最后,输出关于是否使用 CUDA 加速的信息。首先定义了一系列参数和文件路径,包括数据文件、向量化器文件、模型状态文件以及模型保存目录等。然后根据 expand_filepaths_to_save_dir 的设置,将向量化器文件路径和模型状态文件路径扩展到指定的保存目录。接着检查系统是否支持 CUDA,设置是否使用 CUDA 加速。最后,调用 set_seed_everywhere 函数设置随机种子,并通过 handle_dirs 函数确保模型保存目录存在。

def set_seed_everywhere(seed, cuda):
    np.random.seed(seed)
    torch.manual_seed(seed)
    if cuda:
        torch.cuda.manual_seed_all(seed)

def handle_dirs(dirpath):
    if not os.path.exists(dirpath):
        os.makedirs(dirpath)
args = Namespace(
    # 数据和路径信息
    surname_csv="data/surnames/surnames_with_splits.csv",  # 姓氏数据的CSV文件路径
    vectorizer_file="vectorizer.json",  # 向量化器文件名
    model_state_file="model.pth",  # 模型状态文件名
    save_dir="model_storage/ch4/surname_mlp",  # 模型保存目录
    # 模型超参数
    hidden_dim=300,  # 隐藏层维度
    # 训练超参数
    seed=1337,  # 随机种子
    num_epochs=100,  # 迭代轮数
    early_stopping_criteria=5,  # 早停的条件
    learning_rate=0.001,  # 学习率
    batch_size=64,  # 批量大小
    # 运行时选项
    cuda=False,  # 是否使用CUDA加速
    reload_from_files=False,  # 是否从文件重新加载
    expand_filepaths_to_save_dir=True,  # 是否将文件路径扩展到保存目录
)

if args.expand_filepaths_to_save_dir:
    args.vectorizer_file = os.path.join(args.save_dir,
                                        args.vectorizer_file)

    args.model_state_file = os.path.join(args.save_dir,
                                         args.model_state_file)
    
    print("扩展后的文件路径: ")
    print("\t{}".format(args.vectorizer_file))
    print("\t{}".format(args.model_state_file))
    
# 检查CUDA
if not torch.cuda.is_available():
    args.cuda = False

args.device = torch.device("cuda" if args.cuda else "cpu")
    
print("使用CUDA: {}".format(args.cuda))


# 为了可重现性设置随机种子
set_seed_everywhere(args.seed, args.cuda)

# 处理目录
handle_dirs(args.save_dir)

根据 args.reload_from_files 的布尔值决定操作流程:如果为真,则从已保存的检查点重新加载数据集和向量化器;否则,重新创建数据集并保存向量化器。然后,基于数据集的词汇表长度和指定的隐藏层维度,以及输出类别数量,初始化一个姓氏分类器。

if args.reload_from_files:
    # 从检查点重新训练
    print("重新加载!")
    dataset = SurnameDataset.load_dataset_and_load_vectorizer(args.surname_csv,
                                                              args.vectorizer_file)
else:
    # 创建数据集和向量化器
    print("重新创建!")
    dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
    dataset.save_vectorizer(args.vectorizer_file)
    
vectorizer = dataset.get_vectorizer()
classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab), 
                               hidden_dim=args.hidden_dim, 
                               output_dim=len(vectorizer.nationality_vocab))

一个深度学习模型的训练过程包括数据准备、损失函数定义、优化器设置、学习率调度以及训练和验证循环。代码使用了进度条库tqdm显示训练和验证的进度。主要步骤包括:

将模型和数据集移动到指定设备(如GPU)。

定义损失函数和优化器,并设置学习率调度策略。

创建训练状态对象和进度条。

在每个训练轮次中,遍历训练数据进行前向传播、计算损失、反向传播和参数更新,同时计算并记录损失和准确率。

在每个验证轮次中,计算验证损失和准确率,并根据验证损失调整学习率。

根据验证结果更新训练状态,并决定是否提前停止训练。

整个流程在一个try-except块中运行,以便在收到键盘中断时安全退出。

classifier = classifier.to(args.device)  # 将分类器移动到指定的设备上
dataset.class_weights = dataset.class_weights.to(args.device)  # 将数据集的类权重移动到指定的设备上

loss_func = nn.CrossEntropyLoss(dataset.class_weights)  # 定义交叉熵损失函数,使用数据集的类权重
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)  # 定义Adam优化器,优化分类器参数,使用指定的学习率
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,
                                                 mode='min', factor=0.5,
                                                 patience=1)  # 学习率调整策略,当验证损失不再减小时,学习率减半

train_state = make_train_state(args)  # 创建训练状态对象,记录训练过程中的指标

epoch_bar = tqdm_notebook(desc='training routine', 
                          total=args.num_epochs,
                          position=0)  # 创建进度条,用于显示训练过程中的总进度

dataset.set_split('train')  # 设置数据集为训练集
train_bar = tqdm_notebook(desc='split=train',
                          total=dataset.get_num_batches(args.batch_size), 
                          position=1, 
                          leave=True)  # 创建进度条,用于显示训练集的进度

dataset.set_split('val')  # 设置数据集为验证集
val_bar = tqdm_notebook(desc='split=val',
                        total=dataset.get_num_batches(args.batch_size), 
                        position=1, 
                        leave=True)  # 创建进度条,用于显示验证集的进度

try:
    for epoch_index in range(args.num_epochs):
        train_state['epoch_index'] = epoch_index  # 设置当前训练的轮次

        # Iterate over training dataset
        # 遍历训练数据集

        # setup: batch generator, set loss and acc to 0, set train mode on
        # 设置:批生成器,将损失和准确率设置为0,设置为训练模式

        dataset.set_split('train')  # 设置数据集为训练集
        batch_generator = generate_batches(dataset, 
                                           batch_size=args.batch_size, 
                                           device=args.device)  # 创建批生成器,用于批量获取训练数据
        running_loss = 0.0  # 初始化累计损失为0
        running_acc = 0.0  # 初始化累计准确率为0
        classifier.train()  # 将分类器设置为训练模式

        for batch_index, batch_dict in enumerate(batch_generator):
            # the training routine is these 5 steps:
            # 训练步骤包括以下5个步骤:

            # --------------------------------------
            # step 1. zero the gradients
            # 步骤1. 梯度清零
            optimizer.zero_grad()

            # step 2. compute the output
            # 步骤2. 计算输出
            y_pred = classifier(batch_dict['x_surname'])

            # step 3. compute the loss
            # 步骤3. 计算损失
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.to("cpu").item()  # 将损失值转移到CPU并转换为Python数值
            running_loss += (loss_t - running_loss) / (batch_index + 1)  # 更新累积损失的移动平均值

            # 计算准确率
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)  # 更新累积准确率的移动平均值

            # 更新验证进度条的后缀,显示当前的损失、准确率和训练轮次
            val_bar.set_postfix(loss=running_loss, acc=running_acc, epoch=epoch_index)
            val_bar.update()  # 更新验证进度条

            # 将当前轮次的验证损失和准确率添加到训练状态
            train_state['val_loss'].append(running_loss)
            train_state['val_acc'].append(running_acc)

            # 更新训练状态,可能包括保存模型、记录最佳模型等操作
            train_state = update_train_state(args=args, model=classifier, train_state=train_state)

            # 根据验证集上的损失值更新学习率
            scheduler.step(train_state['val_loss'][-1])

            # 如果需要提前停止训练,则跳出训练循环
            if train_state['stop_early']:
            break

            # 重置训练集和验证集进度条的位置,准备开始下一个训练轮次
            train_bar.n = 0
            val_bar.n = 0
            epoch_bar.update()  # 更新总进度条

            except KeyboardInterrupt:
            print("Exiting loop")  # 捕获键盘中断,打印信息并退出循环

加载最佳模型:从保存的文件中加载最佳模型的参数,并将模型移动到指定设备(如GPU)。

设置损失函数:定义交叉熵损失函数,并传入类别权重。

准备数据:将数据集设置为测试集并生成批处理器。

初始化:初始化累积损失和准确率。

评估模式:将模型设置为评估模式。

遍历测试集:对每个批次的数据,计算模型输出。计算并累积损失和准确率。

保存结果:将测试集上的最终损失和准确率保存到训练状态中

# 使用最佳可用模型计算测试集上的损失和准确率

# 加载最佳模型的参数
classifier.load_state_dict(torch.load(train_state['model_filename']))

# 将模型移至指定设备(如GPU)
classifier = classifier.to(args.device)
# 将类别权重移至指定设备
dataset.class_weights = dataset.class_weights.to(args.device)
# 使用交叉熵损失函数计算损失,传入类别权重
loss_func = nn.CrossEntropyLoss(dataset.class_weights)

# 设置数据集为测试集
dataset.set_split('test')
# 生成测试集的批处理器
batch_generator = generate_batches(dataset, 
                                   batch_size=args.batch_size, 
                                   device=args.device)
# 初始化累积损失和准确率
running_loss = 0.
running_acc = 0.
# 设置模型为评估模式
classifier.eval()

# 遍历测试集的每个批次
for batch_index, batch_dict in enumerate(batch_generator):
    # 计算模型的输出
    y_pred =  classifier(batch_dict['x_surname'])
    
    # 计算损失
    loss = loss_func(y_pred, batch_dict['y_nationality'])
    loss_t = loss.item()
    running_loss += (loss_t - running_loss) / (batch_index + 1)

    # 计算准确率
    acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
    running_acc += (acc_t - running_acc) / (batch_index + 1)

# 将测试集上的损失和准确率保存到训练状态中
train_state['test_loss'] = running_loss
train_state['test_acc'] = running_acc
print("Test loss: {};".format(train_state['test_loss']))
print("Test Accuracy: {}".format(train_state['test_acc']))

两个函数predict_nationalitypredict_topk_nationality,用于预测姓氏的国籍。第一个函数将姓氏向量化后输入到分类器中,返回最可能的国籍及其概率。第二个函数则类似,但它返回的是前k个最可能的国籍及其对应的概率值。通过用户输入的姓氏和需要查看的前k个结果,程序使用这两个函数进行预测,并在控制台上打印出相应的预测结果。

def predict_nationality(surname, classifier, vectorizer):
    """Predict the nationality from a new surname
    
    Args:
        surname (str): the surname to classifier
        classifier (SurnameClassifer): an instance of the classifier
        vectorizer (SurnameVectorizer): the corresponding vectorizer
    Returns:
        a dictionary with the most likely nationality and its probability
    """
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).view(1, -1)
    result = classifier(vectorized_surname, apply_softmax=True)

    probability_values, indices = result.max(dim=1)
    index = indices.item()

    predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
    probability_value = probability_values.item()

    return {'nationality': predicted_nationality, 'probability': probability_value}
new_surname = input("Enter a surname to classify: ")
classifier = classifier.to("cpu")
prediction = predict_nationality(new_surname, classifier, vectorizer)
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))
vectorizer.nationality_vocab.lookup_index(8)
def predict_topk_nationality(name, classifier, vectorizer, k=5):
    # 将姓氏向量化
    vectorized_name = vectorizer.vectorize(name)
    # 转换为张量并调整形状为(1, -1)
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
    # 使用分类器进行预测,应用softmax函数
    prediction_vector = classifier(vectorized_name, apply_softmax=True)
    # 获取前k个最高概率的值和对应的索引
    probability_values, indices = torch.topk(prediction_vector, k=k)
    
    # 返回的大小是1,k
    probability_values = probability_values.detach().numpy()[0]
    indices = indices.detach().numpy()[0]
    
    results = []
    # 遍历概率值和索引,将结果存入列表中
    for prob_value, index in zip(probability_values, indices):
        # 通过索引查找国籍
        nationality = vectorizer.nationality_vocab.lookup_index(index)
        results.append({'nationality': nationality, 
                        'probability': prob_value})
    
    return results

# 输入待分类的姓氏
new_surname = input("输入一个要分类的姓氏:")
# 将分类器移至CPU
classifier = classifier.to("cpu")

# 输入要查看的前k个预测结果
k = int(input("要查看前几个预测结果?"))
# 如果k大于国籍词汇表中的数量,则将其设为最大值
if k > len(vectorizer.nationality_vocab):
    print("抱歉!这超过了我们拥有的国籍数量...默认为最大值 :)")
    k = len(vectorizer.nationality_vocab)
    
# 获取前k个预测结果
predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)

print("前 {} 个预测结果:".format(k))
print("===================")
# 打印预测结果
for prediction in predictions:
    print("{} -> {} (概率={:0.2f})".format(new_surname,
                                        prediction['nationality'],
                                        prediction['probability']))

6.实验小结

在本实验中,我们实现并测试了两个用于预测姓氏国籍的函数:predict_nationalitypredict_topk_nationality。以下是实验的主要内容与总结:

函数实现:predict_nationality函数接收一个姓氏、分类器实例和向量化工具,实现对单个姓氏的国籍预测,返回最可能的国籍及其概率。predict_topk_nationality函数则扩展了前者的功能,能够返回前k个最可能的国籍及其对应的概率值。

数据处理:输入的姓氏通过向量化工具进行向量化处理,并转换为适合于分类器输入的张量格式。通过调用分类器,对输入的姓氏进行国籍预测,并应用softmax函数得到概率分布。

结果输出:在predict_nationality函数中,程序输出最可能的国籍及其概率。在predict_topk_nationality函数中,程序输出前k个最可能的国籍及其对应的概率值。

用户交互:用户输入待分类的姓氏,以及希望查看的前k个预测结果的数量。程序根据用户输入,调用相应的函数进行预测,并将结果打印到控制台。

准确性与表现:函数能够有效地从姓氏中预测出相关的国籍,并在多数情况下返回合理的果。在测试小规模数据集时表现良好。

可扩展性:predict_topk_nationality函数增强了系统的实用性,能够提供更多预测信息,使用户对结果有更全面的了解。

二、机器翻译

1.实验背景与目的

机器翻译是自然语言处理(NLP)中的一个重要应用领域,旨在自动将一种语言的文本翻译成另一种语言。本实验使所以我们使用机器翻译为例来介绍编码器—解码器和注意力机制的应用。,目的是评估模型的翻译性能并探讨其改进空间。

2.实验原理

2.1含注意力机制的编码器—解码器

编码器-解码器(Encoder-Decoder)结构是一种常见的神经网络架构,通常用于序列到序列(Sequence-to-Sequence,Seq2Seq)的任务,比如机器翻译和对话系统。在这种结构中,编码器(Encoder)负责将输入序列编码成一个中间表示,而解码器(Decoder)则将这个中间表示解码为目标序列。含有注意力机制的编码器-解码器结构在解决长序列处理问题时效果更好。注意力机制允许解码器在生成输出的每一步都能够关注输入序列的不同部分,从而更灵活地捕捉输入序列的信息。

2.2贪婪搜索

贪婪搜索(Greedy Search)是一种简单且常用的启发式搜索策略,通常用于解决最优化问题和序列生成任务(例如自然语言处理中的机器翻译、文本生成等)。在贪婪搜索中,每一步都选择当前最优的选择,而不考虑未来的可能性。

在序列生成任务中,贪婪搜索的具体实现过程如下:

初始化:从一个初始状态开始,例如一个特殊的开始标记 <sos>

逐步构建序列:在每一步中,根据当前状态(或序列的历史部分)选择下一个最有可能的元素。

终止条件:继续上述步骤,直到生成结束标记 <eos> 或者达到预定的最大序列长度

2.3评价机器翻译结果:BLEU

对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。

具体来说,设词数为𝑛𝑛的子序列的精度为𝑝𝑛𝑝𝑛。它是预测序列与标签序列匹配词数为𝑛𝑛的子序列的数量与预测序列中词数为𝑛𝑛的子序列的数量之比。举个例子,假设标签序列为𝐴𝐴、𝐵𝐵、𝐶𝐶、𝐷𝐷、𝐸𝐸、𝐹𝐹,预测序列为𝐴𝐴、𝐵𝐵、𝐵𝐵、𝐶𝐶、𝐷𝐷,那么𝑝1=4/5,𝑝2=3/4,𝑝3=1/3,𝑝4=0𝑝1=4/5,𝑝2=3/4,𝑝3=1/3,𝑝4=0。设𝑙𝑒𝑛label𝑙𝑒𝑛label和𝑙𝑒𝑛pred𝑙𝑒𝑛pred分别为标签序列和预测序列的词数,那么,BLEU的定义为

其中𝑘𝑘是我们希望匹配的子序列的最大词数。可以看到当预测序列和标签序列完全一致时,BLEU为1。

3.实验步骤

读取和预处理数据

我们先定义一些特殊符号。其中“<pad>”(padding)符号用来添加在较短序列后,直到每个序列等长,而“<bos>”和“<eos>”符号分别表示序列的开始和结束。

!tar -xf d2lzh_pytorch.tar

在深度学习环境中导入必要的库和模块,并设置运行环境。首先,导入标准库 collectionsosiomath 以及 PyTorch 相关库 torchtorch.nntorch.nn.functionaltorchtext.vocabtorch.utils.data,这些库提供了数据结构、操作系统交互、输入输出处理、数学运算及深度学习所需的功能。此外,还导入了 sys 模块(虽然将上级目录添加到系统路径的语句被注释掉了)和自定义模块 d2lzh_pytorch,用于深度学习实践。定义了三个特殊标记 <pad>(填充符)、<bos>(序列起始符)和 <eos>(序列结束符)。通过设置环境变量 CUDA_VISIBLE_DEVICES"0",指定使用 GPU 设备 0,然后根据系统是否有可用的 CUDA 设备,将计算设备设为 GPU 或 CPU。最后,打印当前 PyTorch 版本和选择的计算设备。

# 导入需要的库和模块
import collections  # 用于数据结构(如计数器、双端队列等)
import os  # 用于操作系统相关操作(如环境变量、路径操作等)
import io  # 用于处理输入输出操作
import math  # 数学运算库
import torch  # PyTorch深度学习框架
from torch import nn  # PyTorch中的神经网络模块
import torch.nn.functional as F  # PyTorch中的常用函数接口
import torchtext.vocab as Vocab  # torchtext中的词汇表处理模块
import torch.utils.data as Data  # PyTorch中的数据处理模块

import sys  # 系统相关操作模块
# sys.path.append("..")  # 将上级目录添加到系统路径(注释掉了,可能用于引入自定义模块)
import d2lzh_pytorch as d2l  # 引入自定义模块d2lzh_pytorch,用于深度学习实践

# 定义特殊标记
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
# 设置环境变量,指定使用的GPU设备
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# 定义计算设备,如果有GPU则使用GPU,否则使用CPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 打印PyTorch版本和使用的设备
print(torch.__version__, device)

接着定义两个辅助函数对后面读取的数据进行预处理。

# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 长度变为max_seq_len,然后将序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    all_tokens.extend(seq_tokens)
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
    all_seqs.append(seq_tokens)

# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
    vocab = Vocab.Vocab(collections.Counter(all_tokens),
                        specials=[PAD, BOS, EOS])
    indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
    return vocab, torch.tensor(indices)

使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为max_seq_len。我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立。

def read_data(max_seq_len):
    # in和out分别是input和output的缩写
    in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
    with io.open('fr-en-small.txt') as f:
        lines = f.readlines()
    for line in lines:
        in_seq, out_seq = line.rstrip().split('\t')
        in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
        if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
            continue  # 如果加上EOS后长于max_seq_len,则忽略掉此样本
        process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
        process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
    in_vocab, in_data = build_data(in_tokens, in_seqs)
    out_vocab, out_data = build_data(out_tokens, out_seqs)
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)

将序列的最大长度设成7,然后查看读取到的第一个样本。该样本分别包含法语词索引序列和英语词索引序列。

max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]

在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。正如我们在6.5节(循环神经网络的简洁实现)中提到的,PyTorch的nn.GRU实例在前向计算后也会分别返回输出和最终时间步的多层隐藏状态。其中的输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。注意力机制将这些输出作为键项和值项。

class Encoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 drop_prob=0, **kwargs):
        super(Encoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)

    def forward(self, inputs, state):
        # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        embedding = self.embedding(inputs.long()).permute(1, 0, 2) # (seq_len, batch, input_size)
        return self.rnn(embedding, state)

    def begin_state(self):
        return None

下面我们来创建一个批量大小为4、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对于门控循环单元来说,state就是一个元素,即隐藏状态;如果使用长短期记忆,state是一个元组,包含两个元素即隐藏状态和记忆细胞。

encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
output.shape, state.shape # GRU的state是h, 而LSTM的是一个元组(h, c)

将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh函数作为激活函数。输出层的输出个数为1。两个Linear实例均不使用偏差。其中函数𝑎𝑎定义里向量𝑣𝑣的长度是一个超参数,即attention_size

def attention_model(input_size, attention_size):
    model = nn.Sequential(nn.Linear(input_size, attention_size, bias=False),
                          nn.Tanh(),
                          nn.Linear(attention_size, 1, bias=False))
    return model

注意力机制的输入包括查询项、键项和值项。设编码器和解码器的隐藏单元个数相同。这里的查询项为解码器在上一时间步的隐藏状态,形状为(批量大小, 隐藏单元个数);键项和值项均为编码器在所有时间步的隐藏状态,形状为(时间步数, 批量大小, 隐藏单元个数)。注意力机制返回当前时间步的背景变量,形状为(批量大小, 隐藏单元个数)。

def attention_forward(model, enc_states, dec_state):
    """
    enc_states: (时间步数, 批量大小, 隐藏单元个数)
    dec_state: (批量大小, 隐藏单元个数)
    """
    # 将解码器隐藏状态广播到和编码器隐藏状态形状相同后进行连结
    dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
    enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
    e = model(enc_and_dec_states)  # 形状为(时间步数, 批量大小, 1)
    alpha = F.softmax(e, dim=0)  # 在时间步维度做softmax运算
    return (alpha * enc_states).sum(dim=0)  # 返回背景变量

在下面的例子中,编码器的时间步数为10,批量大小为4,编码器和解码器的隐藏单元个数均为8。注意力机制返回一个小批量的背景向量,每个背景向量的长度等于编码器的隐藏单元个数。因此输出的形状为(4, 8)。

seq_len, batch_size, num_hiddens = 10, 4, 8
model = attention_model(2*num_hiddens, 10) 
enc_states = torch.zeros((seq_len, batch_size, num_hiddens))
dec_state = torch.zeros((batch_size, num_hiddens))
attention_forward(model, enc_states, dec_state).shape

在解码器的前向计算中,我们先通过刚刚介绍的注意力机制计算得到当前时间步的背景向量。由于解码器的输入来自输出语言的词索引,我们将输入通过词嵌入层得到表征,然后和背景向量在特征维连结。我们将连结后的结果与上一时间步的隐藏状态通过门控循环单元计算出当前时间步的输出与隐藏状态。最后,我们将输出通过全连接层变换为有关各个输出词的预测,形状为(批量大小, 输出词典大小)。

class Decoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 attention_size, drop_prob=0):
        super(Decoder, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.attention = attention_model(2*num_hiddens, attention_size)
        # GRU的输入包含attention输出的c和实际输入, 所以尺寸是 num_hiddens+embed_size
        self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens, 
                          num_layers, dropout=drop_prob)
        self.out = nn.Linear(num_hiddens, vocab_size)

    def forward(self, cur_input, state, enc_states):
        """
        cur_input shape: (batch, )
        state shape: (num_layers, batch, num_hiddens)
        """
        # 使用注意力机制计算背景向量
        c = attention_forward(self.attention, enc_states, state[-1])
        # 将嵌入后的输入和背景向量在特征维连结, (批量大小, num_hiddens+embed_size)
        input_and_c = torch.cat((self.embedding(cur_input), c), dim=1) 
        # 为输入和背景向量的连结增加时间步维,时间步个数为1
        output, state = self.rnn(input_and_c.unsqueeze(0), state)
        # 移除时间步维,输出形状为(批量大小, 输出词典大小)
        output = self.out(output).squeeze(dim=0)
        return output, state

    def begin_state(self, enc_state):
        # 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
        return enc_state

我们先实现batch_loss函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符BOS。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。此外,同10.3节(word2vec的实现)中的实现一样,我们在这里也使用掩码变量避免填充项对损失函数计算的影响。

def batch_loss(encoder, decoder, X, Y, loss):
    batch_size = X.shape[0]
    enc_state = encoder.begin_state()
    enc_outputs, enc_state = encoder(X, enc_state)
    # 初始化解码器的隐藏状态
    dec_state = decoder.begin_state(enc_state)
    # 解码器在最初时间步的输入是BOS
    dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
    # 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
    mask, num_not_pad_tokens = torch.ones(batch_size,), 0
    l = torch.tensor([0.0])
    for y in Y.permute(1,0): # Y shape: (batch, seq_len)
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
        l = l + (mask * loss(dec_output, y)).sum()
        dec_input = y  # 使用强制教学
        num_not_pad_tokens += mask.sum().item()
        # EOS后面全是PAD. 下面一行保证一旦遇到EOS接下来的循环中mask就一直是0
        mask = mask * (y != out_vocab.stoi[EOS]).float()
    return l / num_not_pad_tokens

在训练函数中,我们需要同时迭代编码器和解码器的模型参数。

def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
    dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)

    loss = nn.CrossEntropyLoss(reduction='none')
    data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
    for epoch in range(num_epochs):
        l_sum = 0.0
        for X, Y in data_iter:
            enc_optimizer.zero_grad()
            dec_optimizer.zero_grad()
            l = batch_loss(encoder, decoder, X, Y, loss)
            l.backward()
            enc_optimizer.step()
            dec_optimizer.step()
            l_sum += l.item()
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))

接下来,创建模型实例并设置超参数。然后,我们就可以训练模型了

embed_size, num_hiddens, num_layers = 64, 64, 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,
                  drop_prob)
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
                  attention_size, drop_prob)
train(encoder, decoder, dataset, lr, batch_size, num_epochs)

实现最简单的贪婪搜索

def translate(encoder, decoder, input_seq, max_seq_len):
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]]) # batch=1
    enc_state = encoder.begin_state()
    enc_output, enc_state = encoder(enc_input, enc_state)
    dec_input = torch.tensor([out_vocab.stoi[BOS]])
    dec_state = decoder.begin_state(enc_state)
    output_tokens = []
    for _ in range(max_seq_len):
        dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
        pred = dec_output.argmax(dim=1)
        pred_token = out_vocab.itos[int(pred.item())]
        if pred_token == EOS:  # 当任一时间步搜索出EOS时,输出序列即完成
            break
        else:
            output_tokens.append(pred_token)
            dec_input = pred
    return output_tokens

简单测试一下模型。输入法语句子“ils regardent.”,翻译后的英语句子应该是“they are watching.”。

input_seq = 'ils regardent .'
translate(encoder, decoder, input_seq, max_seq_len)

实现BLEU的计算

def bleu(pred_tokens, label_tokens, k):
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):
            label_subs[''.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[''.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[''.join(pred_tokens[i: i + n])] -= 1
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score

定义一个辅助打印函数

def score(input_seq, label_seq, k):
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
    label_tokens = label_seq.split(' ')
    print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
                                      ' '.join(pred_tokens)))
score('ils regardent .', 'they are watching .', k=2)
score('ils sont canadienne .', 'they are canadian .', k=2)

4.实验小结

本实验成功实现了基于编码器—解码器和注意力机制的机器翻译,并通过BLEU评分对模型进行了初步评估。尽管现有模型在大多数情况下表现良好,但仍有改进空间。同时学习了解了可以将编码器—解码器和注意力机制应用于机器翻译中。BLEU可以用来评价翻译结果。

三、基于Transformer实现机器翻译(日译中)

1.实验背景与目的

机器翻译是自然语言处理领域的重要应用之一。近年来,基于Transformer的神经网络模型在机器翻译任务中表现出了极大的优势。本实验旨在实现一个基于Transformer架构的日语到中文(以下简称日译中)机器翻译系统,并评估其性能。

2.实验原理

2.1JParaCrawl

JParaCrawl是一个大规模的基于网络的英语-日语平行语料库,用于机器翻译和自然语言处理研究。该语料库收集了从互联网上抓取的英语和日语句子对,并被广泛用于训练机器翻译模型和其他相关任务。JParaCrawl的发布为日语到中文的机器翻译系统提供了宝贵的训练数据,有助于提高翻译质量和泛化能力。

2.2分词器

SentencePiece 是由 Google 提供的一个开源子词(subword)分割工具,用于预处理文本数据,特别适合用于自然语言处理任务中的分词。与传统的分词方法不同,SentencePiece 是一种基于无监督学习的模型,不依赖于语言的特定特性,因此能够处理多种语言。

SentencePiece的特点:

语言无关:SentencePiece 不需要任何语言特定的知识或词典,适用于任意语言的文本。

子词单元:通过将文本分割为子词单元,SentencePiece 能够更好地处理未登录词(OOV)和形态复杂的语言。

无空格分割:SentencePiece 将整个输入文本视为一个无空格的字符串,这对于像中文、日文等连续书写的语言尤其有用。

BPE/Unigram 模型:支持 Byte-Pair Encoding (BPE) 和 Unigram Language Model 两种子词分割方法。

2.3Transformer

Transformer 是一种由Vaswani等人于2017年提出的重要深度学习模型架构,广泛应用于自然语言处理(NLP)领域。其核心创新在于自注意力机制和多头注意力,使得模型能够有效捕捉序列中不同位置的依赖关系,而无需考虑其顺序。Transformer 的位置编码和前馈神经网络进一步增强了其处理能力。与传统循环神经网络(RNN)相比,Transformer 拥有更高的并行化能力和处理长序列的效率,显著提升了机器翻译、语言建模、语音识别和图像处理等任务的性能

3.实验步骤

导入所需的包 

import math
import torchtext
import torch
import torch.nn as nn
from torch import Tensor
from torch.nn.utils.rnn import pad_sequence
from torch.utils.data import DataLoader
from collections import Counter
from torchtext.vocab import Vocab
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer
import io
import time
import pandas as pd
import numpy as np
import pickle
import tqdm
import sentencepiece as spm
torch.manual_seed(0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

 在本实验中,我们将使用从JParaCrawlhttp://www.kecl.ntt.co.jp/icl/lirg/jparacrawl)下载的日语-英语平行数据集,该数据集被描述为“由NTT创建的最大的可公开获取的英日平行语料库。

import pandas as pd
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 提取第三列(英文文本)并转换为列表
trainen = df[2].values.tolist()  #[:10000]
# 提取第四列(日文文本)并转换为列表
trainja = df[3].values.tolist()  #[:10000]
# 可选:删除特定索引位置的数据行(示例中索引为5972的行)
# trainen.pop(5972)
# trainja.pop(5972)
print(trainen[500])
print(trainja[500])

准备分词器 与英语或其他字母语言不同,日语句子中不包含空格来分隔单词。我们可以使用由JParaCrawl创建的使用SentencePiece的分词器来处理日语和英语

import sentencepiece as spm
# 加载英文的SentencePiece模型,用于分词和处理文本
en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')
# 加载日文的SentencePiece模型,用于分词和处理文本
ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.ja.nopretok.model')
en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')

使用分词器和原始句子,我们构建从TorchText导入的词汇对象。这个过程可能需要几秒钟或几分钟,这取决于我们数据集的大小和计算能力。不同的分词器也会影响构建词汇所需的时间,尝试了几种其他日语分词器,SentencePiece 效果很好,速度也足够快。

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>'])
# 使用build_vocab函数构建日语(ja)和英语(en)的词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)  # 构建日语的词汇表
en_vocab = build_vocab(trainen, en_tokenizer)  # 构建英语的词汇表

当有了词汇对象之后,我们可以使用词汇和标记器对象来构建用于训练数据的张量。

def data_process(ja, en):
    # 初始化一个空列表来存储处理后的数据
    data = []    
    # 使用 zip 函数同时遍历日语和英语句子列表
    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
        )        
        # 将英语句子转换为对应的词表索引张量
        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
# 调用 data_process 函数处理训练数据
train_data = data_process(trainja, trainen)

在训练过程中创建DataLoader对象进行迭代。将BATCH_SIZE设置为16以防止“cuda out of memory”,这取决于诸如你的机器内存容量、数据大小等各种因素,因此根据自己的需求随时更改批量大小

BATCH_SIZE = 8
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:
        # 在日语句子的开头和结尾添加起始标记(<bos>和<eos>)
        ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
        # 在英语句子的开头和结尾添加起始标记(<bos>和<eos>)
        en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))    
    # 对日语批次进行填充,使用 PAD_IDX 作为填充值
    ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
    # 对英语批次进行填充,使用 PAD_IDX 作为填充值
    en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)   
    return ja_batch, en_batch
# 创建数据加载器 DataLoader,用于加载训练数据
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, collate_fn=generate_batch)

Transformer是一种Seq2Seq模型,用于解决机器翻译任务。Transformer模型由一个包含固定层数的编码器块和解码器块组成。编码器通过将输入序列传播到一系列多头注意力和前馈网络层来处理输入序列。编码器的输出被称为内存,与目标张量一起馈送给解码器。编码器和解码器使用教师强制技术以端到端的方式进行训练。

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):
        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)       
        # 源语言和目标语言的词嵌入层
        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):
        # 对源语言和目标语言的词嵌入进行位置编码
        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)      
        # 解码阶段:将解码器应用于目标语言输入和编码器的记忆
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
                                        tgt_padding_mask, memory_key_padding_mask)      
        # 生成最终的输出
        return self.generator(outs)

    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)

文本标记通过使用标记嵌入来表示。在标记嵌入中添加位置编码以引入单词顺序的概念

import torch
import torch.nn as nn
import math
from torch import Tensor
# 定义位置编码类,用于生成位置编码
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)      
        # 生成位置序列
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)   
        # 初始化位置编码矩阵
        pos_embedding = torch.zeros((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)
        # 定义Dropout层
        self.dropout = nn.Dropout(dropout)       
        # 注册位置编码为模型的缓冲区,不会在优化器中更新
        self.register_buffer('pos_embedding', pos_embedding)
    def forward(self, token_embedding: Tensor):
        # 在输入的token嵌入上添加位置编码,并应用Dropout
        return self.dropout(token_embedding + self.pos_embedding[:token_embedding.size(0), :])
# 定义TokenEmbedding类,用于生成词嵌入
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):
        # 对输入tokens进行嵌入并进行缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

创建一个随后的词掩模,以阻止目标词关注其随后的词。还创建了用于屏蔽源和目标填充标记的掩模。

def generate_square_subsequent_mask(sz):
    # 生成一个上三角矩阵
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    # 将mask转换为浮点型,并用负无穷和0来填充mask中的值
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask
def create_mask(src, tgt):
    # 获取源序列和目标序列的长度
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]
    # 生成目标序列的mask
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
    # 生成源序列的mask
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
    # 生成源序列和目标序列的padding mask
    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

定义模型参数并实例化模型。

# 定义模型和训练参数
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                 # 训练轮数

# 初始化Seq2Seq模型
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)

# 将模型移动到设备(GPU或CPU)
transformer = transformer.to(device)

# 定义损失函数,忽略填充索引
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)

# 定义优化器Adam
optimizer = torch.optim.Adam(
    transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
)

# 训练一个epoch的函数
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, :]  # 去掉最后一个时间步的标签作为输入

        # 创建掩码
        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))  # 计算损失
        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(val_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)  # 返回平均损失

在准备好必要的类和函数之后,准备好训练我们的模型了。完成训练所需的时间会因计算能力、参数和数据集大小等多种因素而大不相同。

# 使用 tqdm 包来显示训练进度
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS + 1)):
    start_time = time.time()  # 记录开始时间
    train_loss = train_epoch(transformer, train_iter, optimizer)  # 训练一个 epoch 并返回训练损失
    end_time = time.time()  # 记录结束时间
    # 打印当前 epoch 的训练损失和耗时
    print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
           f"Epoch time = {(end_time - start_time):.3f}s"))

尝试使用训练模型翻译一句日语句子 首先,我们创建函数来翻译一个新句子,包括获取日语句子、分词、转换为张量、推理,然后将结果解码回一个句子

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)  # 将编码的记忆移到设备上
        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)  # 调整输出格式
        prob = model.generator(out[:, -1])  # 通过生成器获取概率分布
        _, next_word = torch.max(prob, dim=1)  # 获取下一个词的索引
        next_word = next_word.item()  # 转换为 Python 数值
        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()  # 设定模型为评估模式
    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) )  # 将编码后的源语句转换为张量并调整形状
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)  # 创建源语句掩码
    tgt_tokens = greedy_decode(model,  src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()  # 使用贪婪解码获取目标语句的词索引
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")  # 将目标语句的词索引转换为文本并返回
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
trainen.pop(5)
trainja.pop(5)

在训练完成后,我们将保存词汇对象

import pickle

# 打开一个文件,用于存储数据(以二进制写入模式)
file = open('en_vocab.pkl', 'wb')
# 将en_vocab对象序列化并写入到文件中
pickle.dump(en_vocab, file)
# 关闭文件
file.close()

# 打开另一个文件,用于存储数据(以二进制写入模式)
file = open('ja_vocab.pkl', 'wb')
# 将ja_vocab对象序列化并写入到文件中
pickle.dump(ja_vocab, file)
# 关闭文件
file.close()

使用PyTorch的保存和加载功能保存模型以供以后使用。一般来说,根据我们以后想要用模型做什么,有两种保存模型的方式。第一种是仅用于推理,我们可以以后加载模型并将其用于从日语翻译到英语,第二种方法也是用于推理,但也可以用于在以后加载模型并继续训练的情况。

# 保存模型以用于推理
torch.save(transformer.state_dict(), 'inference_model')
# 保存模型和检查点,以便以后恢复训练
torch.save({
    'epoch': NUM_EPOCHS,  # 当前训练的轮数
    'model_state_dict': transformer.state_dict(),  # 模型的状态字典(参数)
    'optimizer_state_dict': optimizer.state_dict(),  # 优化器的状态字典(参数)
    'loss': train_loss,  # 最后一次计算的训练损失
}, 'model_checkpoint.tar')  # 保存的文件名为 model_checkpoint.tar

4.实验小结

本实验成功实现了基于Transformer架构的日译中机器翻译系统。通过在大型平行语料库上训练,模型在验证集上取得了较高的BLEU分数。然而,模型在处理长句和未登录词方面仍有改进空间。且当前模型对于训练集中未出现的新词,模型的翻译效果较差。有时生成的中文句子语序不符合中文的习惯。长句子的翻译质量显著低于短句子

  • 21
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值