跳至主要内容

使用 PyTorch 进行 NLP:全面指南

NLP 入门:面向初学者的 PyTorch 教程
· 12分钟 阅读

使用 AI 探索

NLP 与 PyTorch 简介

自然语言处理(NLP)是现代人工智能的重要组成部分,使机器能够理解并回应人类语言。随着数字交互的激增,NLP 的重要性与日俱增。PyTorch 作为广受欢迎的开源机器学习库,凭借其灵活性和高效的张量计算,为 NLP 任务提供了强大的工具。其动态计算图也便于轻松修改和构建复杂模型,非常适合本教程的使用场景。

如果您想进一步学习 NLP,欢迎查看我们的 Python 自然语言处理技能路径;如果您更愿意用 R 学习 NLP 的方法,请查看 DataCamp 上的 R 语言自然语言处理入门课程。

环境配置

由于操作系统、包管理器偏好、编程语言以及计算平台等因素,配置 PyTorch 环境有时会较具挑战性。安装过程可能会因这些因素略有不同,需要您运行相应的特定命令。

要获取合适的安装命令,您可以访问 PyTorch 官方的本地安装页面,在那里选择您的偏好设置并获取所需的安装说明。

image7.png

本教程将使用 DataLab。若您希望跟着实操,完整的基于 PyTorch 训练情感分析模型的代码可在此 DataLab 工作簿中获取。

张量简介

image2.png

张量(Tensor)是数学和物理中用于推广标量、向量和矩阵的基础数据结构。它是能够高效存储和处理大量数值数据的多维数组。张量具有明确的形状、大小和数据类型,适用于多种计算操作。

在 PyTorch 中,张量是主要的构建块和数据表示对象。PyTorch 的张量类似于 NumPy 数组,但拥有专为深度学习计算设计的额外功能与优化。PyTorch 的张量操作可利用硬件加速(如 GPU)来高效计算复杂的神经网络。

由于语言数据天然具有序列性和层次性,张量在自然语言处理(NLP)任务中发挥着关键作用。

NLP 涉及对文本信息的处理与理解。通过将词、句子或文档编码为数值向量,张量使文本数据的表示与操作成为可能。

这种数值表示允许运用深度学习模型高效处理并从文本数据中学习。张量支持对大规模语言数据集的高效处理,促进神经网络的训练,并使注意力机制等先进技术得以应用,从而构建更准确的 NLP 模型。

词向量

image5.png

词向量(Word Embeddings)是在连续向量空间中对词语进行的稠密向量表示。它旨在捕捉词语之间的语义和句法关系,帮助更好地理解和语境化文本数据。通过将词表示为数值向量,词向量能够反映语义相似与差异,使算法可以将词作为有意义的数值输入来处理。

通俗来说,嵌入是一种把词变成数字的巧妙方法。这些数字带有特殊含义,能捕捉词与词之间的关系。就像一套“密码”,帮助计算机更轻松地理解和处理词语。

Word2Vec 和 GloVe 是两种常见的词向量生成方法。Word2Vec 是一种基于神经网络的模型,它通过在给定目标词的情况下预测其上下文(连续词袋——CBOW),或根据上下文预测目标词(Skip-gram)来学习词表示。

GloVe(Global Vectors for Word Representation)是一种基于计数的方法,它利用大规模语料中的词共现统计来构建词向量。它能够捕捉全局的词关系,在词类比任务中通常具有更好的表现。

NLP 模型架构

image4.png

情感分析任务的示例模型架构。

情感分析是 NLP 中的常见任务,目标是理解一段文本中表达的情感,通常分为正向、负向或中性。要完成此任务,可以使用简单的循环神经网络(RNN)或更高级的变体长短期记忆网络(LSTM)。

RNN

RNN 架构按序处理文本,将每个词依次输入。网络维护一个随每次输入而更新的隐藏状态,用于捕捉到目前为止处理过的序列信息。

该隐藏状态相当于网络的记忆。然而,标准 RNN 在处理长序列时会遇到“梯度消失”问题,即信息贡献会随时间呈几何级衰减,使网络遗忘较早的输入。您可以在我们的RNN 教程中了解更多关于循环神经网络的内容。

LSTM

为解决这一问题,研发了 RNN 的一种变体——LSTM。LSTM 通过在隐藏状态中引入更复杂的内部结构来维持更长的上下文或“记忆”。它包含一系列“门”(输入门、遗忘门和输出门),用于控制信息在记忆状态中的流入与流出。

输入门决定应在多大程度上将新信息存入记忆状态;遗忘门决定需要丢弃哪些信息;输出门决定向序列中下一个 LSTM 单元暴露多少内部状态。

RNN 或 LSTM 模型通常将句子或文档中的词序列作为输入。每个词通常表示为一个稠密向量(嵌入),用于捕捉词的语义。

网络按词逐个处理序列,根据当前词及前一状态更新其内部状态。

最终的网络状态用于预测情感。该状态会传入全连接层,并经由 softmax 激活函数输出情感类别(如正向、负向、中性)的概率分布。

概率最高的类别即为模型的预测结果。

这是一个基础设置,还可以通过双向 LSTM(双向处理序列)、注意力机制(使模型聚焦于序列中重要部分)等技术进一步增强。

在 PyTorch 中训练用于情感分析的 LSTM 模型

使用 PyTorch 构建情感分析模型的端到端 Python 代码示例

1. 加载数据集

在本示例中,我们将使用 IMDB 5 万条影评数据集。目标是训练一个 LSTM 模型来预测情感。可能的取值有两个:"positive" 和 "negative"。因此,这是一个二分类任务。

file_name = 'IMDB Dataset.csv'
df = pd.read_csv(file_name)
df.head()

Sample dataset

2. 探索性数据分析

X,y = df['review'].values,df['sentiment'].values
x_train,x_test,y_train,y_test = train_test_split(X,y,stratify=y)
print(f'train data shape: {x_train.shape}')
print(f'test data shape: {x_test.shape}')


dd = pd.Series(y_train).value_counts()
sns.barplot(x=np.array(['negative','positive']),y=dd.values)
plt.show()

输出:

>>> train data shape: (37500,)

>>> test data shape: (12500,)

image14.png

3. 文本预处理

文本预处理和分词是关键的第一步。首先,我们通过去除标点、多余空格和数字来清洗文本数据。

然后将句子拆分为单词,移除常见词(即“停用词”),并统计数据集中最常用的 1000 个词。随后为这些词分配唯一标识符,形成用于独热编码的词典。

本质上,这段代码将原始文本句子转换为这些唯一标识符的序列,把人类语言翻译成机器学习模型能够理解的格式。

def preprocess_string(s):
    # Remove all non-word characters (everything except numbers and letters)
    s = re.sub(r"[^\w\s]", '', s)
    # Replace all runs of whitespaces with no space
    s = re.sub(r"\s+", '', s)
    # replace digits with no space
    s = re.sub(r"\d", '', s)
    return s


def tokenize(x_train,y_train,x_val,y_val):
    word_list = []


    stop_words = set(stopwords.words('english'))
    for sent in x_train:
        for word in sent.lower().split():
            word = preprocess_string(word)
            if word not in stop_words and word != '':
                word_list.append(word)
 
    corpus = Counter(word_list)
    # sorting on the basis of most common words
    corpus_ = sorted(corpus,key=corpus.get,reverse=True)[:1000]
    # creating a dict
    onehot_dict = {w:i+1 for i,w in enumerate(corpus_)}
   
    # tokenize
    final_list_train,final_list_test = [],[]
    for sent in x_train:
            final_list_train.append([onehot_dict[preprocess_string(word)] for word in sent.lower().split()
                                     if preprocess_string(word) in onehot_dict.keys()])
    for sent in x_val:
            final_list_test.append([onehot_dict[preprocess_string(word)] for word in sent.lower().split()
                                    if preprocess_string(word) in onehot_dict.keys()])
           
    encoded_train = [1 if label =='positive' else 0 for label in y_train]  
    encoded_test = [1 if label =='positive' else 0 for label in y_val]
    return np.array(final_list_train), np.array(encoded_train),np.array(final_list_test), np.array(encoded_test),onehot_dict


x_train,y_train,x_test,y_test,vocab = tokenize(x_train,y_train,x_test,y_test)

我们来分析一下 x_train 中的标记长度。

rev_len = [len(i) for i in x_train]
pd.Series(rev_len).hist()

image1.png

4. 为模型准备数据

鉴于每条评论的标记长度不一,需要将其标准化以保持一致性。由于大多数评论少于 500 个标记,我们将 500 设为所有评论的固定长度。

def padding_(sentences, seq_len):
    features = np.zeros((len(sentences), seq_len),dtype=int)
    for ii, review in enumerate(sentences):
        if len(review) != 0:
            features[ii, -len(review):] = np.array(review)[:seq_len]
    return features


x_train_pad = padding_(x_train,500)
x_test_pad = padding_(x_test,500)

接下来,我们使用 DataLoader 类创建用于模型训练的最终数据集。

# create Tensor datasets
train_data = TensorDataset(torch.from_numpy(x_train_pad), torch.from_numpy(y_train))
valid_data = TensorDataset(torch.from_numpy(x_test_pad), torch.from_numpy(y_test))


# dataloaders
batch_size = 50


# make sure to SHUFFLE your data
train_loader = DataLoader(train_data, shuffle=True, batch_size=batch_size)
valid_loader = DataLoader(valid_data, shuffle=True, batch_size=batch_size)


# obtain one batch of training data
dataiter = iter(train_loader)
sample_x, sample_y = next(dataiter)


print('Sample input size: ', sample_x.size()) # batch_size, seq_length
print('Sample input: \n', sample_x)
print('Sample output: \n', sample_y)

输出:

image3.png

5. 定义 LSTM 模型

这部分代码定义了一个使用循环神经网络(RNN)架构的情感分析模型,具体来说是我们上文提到的长短期记忆网络(LSTM)。SentimentRNN 类是一个 PyTorch 模型,其起始为嵌入层,用于将词索引转换为能捕捉词语语义的稠密表示。随后是处理词嵌入序列的 LSTM 层。

LSTM 的隐藏状态会经过一个 dropout 层(用于正则化模型并防止过拟合)和一个全连接层,将 LSTM 的输出映射到最终预测。预测再通过 sigmoid 激活函数,将原始输出值转换为概率。forward 方法定义了数据在网络中的前向传播,而 init_hidden 方法将 LSTM 层的隐藏状态初始化为零。

class SentimentRNN(nn.Module):
    def __init__(self,no_layers,vocab_size,hidden_dim,embedding_dim,drop_prob=0.5):
        super(SentimentRNN,self).__init__()
 
        self.output_dim = output_dim
        self.hidden_dim = hidden_dim
 
        self.no_layers = no_layers
        self.vocab_size = vocab_size
   
        # embedding and LSTM layers
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
       
        #lstm
        self.lstm = nn.LSTM(input_size=embedding_dim,hidden_size=self.hidden_dim,
                           num_layers=no_layers, batch_first=True)
       
        # dropout layer
        self.dropout = nn.Dropout(0.3)
   
        # linear and sigmoid layer
        self.fc = nn.Linear(self.hidden_dim, output_dim)
        self.sig = nn.Sigmoid()
       
    def forward(self,x,hidden):
        batch_size = x.size(0)
        # embeddings and lstm_out
        embeds = self.embedding(x)  # shape: B x S x Feature   since batch = True
        #print(embeds.shape)  #[50, 500, 1000]
        lstm_out, hidden = self.lstm(embeds, hidden)
       
        lstm_out = lstm_out.contiguous().view(-1, self.hidden_dim)
       
        # dropout and fully connected layer
        out = self.dropout(lstm_out)
        out = self.fc(out)
       
        # sigmoid function
        sig_out = self.sig(out)
       
        # reshape to be batch_size first
        sig_out = sig_out.view(batch_size, -1)


        sig_out = sig_out[:, -1] # get last batch of labels
       
        # return last sigmoid output and hidden state
        return sig_out, hidden
       
    def init_hidden(self, batch_size):
        ''' Initializes hidden state '''
        # Create two new tensors with sizes n_layers x batch_size x hidden_dim,
        # initialized to zero, for hidden state and cell state of LSTM
        h0 = torch.zeros((self.no_layers,batch_size,self.hidden_dim)).to(device)
        c0 = torch.zeros((self.no_layers,batch_size,self.hidden_dim)).to(device)
        hidden = (h0,c0)
        return hidden 

现在我们将用所需参数初始化上面定义的 SentimentRNN 类。

no_layers = 2
vocab_size = len(vocab) + 1 #extra 1 for padding
embedding_dim = 64
output_dim = 1
hidden_dim = 256


model = SentimentRNN(no_layers,vocab_size,hidden_dim,embedding_dim,drop_prob=0.5)


#moving to gpu
model.to(device)
print(model)

image10.png

开始训练之前的最后一步是定义损失函数和优化器。本部分侧重于为我们的情感分析模型定义损失函数、优化方法以及用于计算准确率的辅助函数。

损失函数使用二元交叉熵损失(nn.BCELoss),这在本类二分类任务中十分常用。优化方法为 Adam(torch.optim.Adam),因其高效且内存占用低而广泛采用。此处 Adam 的学习率设为 0.001。

acc 函数是用于计算模型预测准确率的辅助函数。它将预测概率四舍五入到最接近的整数(0 或 1),与真实标签对比,然后计算正确预测的比例。

# loss and optimization functions


lr=0.001


criterion = nn.BCELoss()


optimizer = torch.optim.Adam(model.parameters(), lr=lr)


# function to predict accuracy
def acc(pred,label):
    pred = torch.round(pred.squeeze())
    return torch.sum(pred == label.squeeze()).item()

6. 开始训练

这部分代码对情感分析模型进行训练与验证。每个 epoch(迭代)包含训练阶段和验证阶段。训练阶段通过调整参数以最小化损失来让模型学习。

在验证阶段,使用独立数据集评估模型表现,以确保其学习到的是可泛化的模式,而非仅记住训练数据。

训练循环从初始化 LSTM 的隐藏状态并将模型置于训练模式开始。对于每个批次的数据,将模型预测与真实标签进行比较以计算损失,然后反向传播以更新模型参数。

为防止梯度过大(RNN 和 LSTM 训练中的常见问题),会对梯度进行最大值裁剪。

在验证循环中,模型被置于评估模式,使用验证数据评估其性能而不更新任何参数。在训练与验证阶段,代码都会跟踪每个 epoch 的损失和准确率。

如果验证损失得到改善,则保存当前模型参数,以记录训练过程中表现最好的模型。

最后,在每个 epoch 结束后打印该轮的平均损失和准确率,以便了解模型的学习进展。

clip = 5
epochs = 5
valid_loss_min = np.Inf
# train for some number of epochs
epoch_tr_loss,epoch_vl_loss = [],[]
epoch_tr_acc,epoch_vl_acc = [],[]


for epoch in range(epochs):
    train_losses = []
    train_acc = 0.0
    model.train()
    # initialize hidden state
    h = model.init_hidden(batch_size)
    for inputs, labels in train_loader:
       
        inputs, labels = inputs.to(device), labels.to(device)  
        # Creating new variables for the hidden state, otherwise
        # we'd backprop through the entire training history
        h = tuple([each.data for each in h])
       
        model.zero_grad()
        output,h = model(inputs,h)
       
        # calculate the loss and perform backprop
        loss = criterion(output.squeeze(), labels.float())
        loss.backward()
        train_losses.append(loss.item())
        # calculating accuracy
        accuracy = acc(output,labels)
        train_acc += accuracy
        #`clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
        nn.utils.clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
       
    val_h = model.init_hidden(batch_size)
    val_losses = []
    val_acc = 0.0
    model.eval()
    for inputs, labels in valid_loader:
            val_h = tuple([each.data for each in val_h])


            inputs, labels = inputs.to(device), labels.to(device)


            output, val_h = model(inputs, val_h)
            val_loss = criterion(output.squeeze(), labels.float())


            val_losses.append(val_loss.item())
           
            accuracy = acc(output,labels)
            val_acc += accuracy
           
    epoch_train_loss = np.mean(train_losses)
    epoch_val_loss = np.mean(val_losses)
    epoch_train_acc = train_acc/len(train_loader.dataset)
    epoch_val_acc = val_acc/len(valid_loader.dataset)
    epoch_tr_loss.append(epoch_train_loss)
    epoch_vl_loss.append(epoch_val_loss)
    epoch_tr_acc.append(epoch_train_acc)
    epoch_vl_acc.append(epoch_val_acc)
    print(f'Epoch {epoch+1}')
    print(f'train_loss : {epoch_train_loss} val_loss : {epoch_val_loss}')
    print(f'train_accuracy : {epoch_train_acc*100} val_accuracy : {epoch_val_acc*100}')
    if epoch_val_loss <= valid_loss_min:
        torch.save(model.state_dict(), 'state_dict.pt')
        print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(valid_loss_min,epoch_val_loss))
        valid_loss_min = epoch_val_loss
    print(25*'==')

输出:

image9.png

7. 模型评估

这部分代码生成两幅图,直观展示模型训练过程中训练与验证的准确率和损失。第一幅子图为每个 epoch 后训练与验证准确率的折线图,有助于观察模型的学习效果与泛化情况。

第二幅子图为训练与验证损失的折线图,帮助我们判断模型是过拟合、欠拟合还是拟合得恰到好处。

fig = plt.figure(figsize = (20, 6))
plt.subplot(1, 2, 1)
plt.plot(epoch_tr_acc, label='Train Acc')
plt.plot(epoch_vl_acc, label='Validation Acc')
plt.title("Accuracy")
plt.legend()
plt.grid()
   
plt.subplot(1, 2, 2)
plt.plot(epoch_tr_loss, label='Train loss')
plt.plot(epoch_vl_loss, label='Validation loss')
plt.title("Loss")
plt.legend()
plt.grid()


plt.show()

输出:

image6.png

8. 推理 / 预测

在这部分,我们创建了一个 predict_text 函数用于预测给定文本的情感,并演示其用法。predict_text 函数接收一段文本字符串作为输入,按照预定义词表将其转换为词索引序列,并通过填充与重整形准备为模型输入。随后函数初始化 LSTM 的隐藏状态,将输入馈送给模型,并返回该原始文本的输出概率。

def predict_text(text):
        word_seq = np.array([vocab[preprocess_string(word)] for word in text.split()
                         if preprocess_string(word) in vocab.keys()])
        word_seq = np.expand_dims(word_seq,axis=0)
        pad =  torch.from_numpy(padding_(word_seq,500))
        inputs = pad.to(device)
        batch_size = 1
        h = model.init_hidden(batch_size)
        h = tuple([each.data for each in h])
        output, h = model(inputs, h)
        return(output.item())


index = 30
print(df['review'][index])
print('='*70)
print(f'Actual sentiment is  : {df["sentiment"][index]}')
print('='*70)
pro = predict_text(df['review'][index])
status = "positive" if pro > 0.5 else "negative"
pro = (1 - pro) if status == "negative" else pro
print(f'Predicted sentiment is {status} with a probability of {pro}')

输出:

image13.png

这个完整笔记本是在 DataLab 中开发的,可在此工作簿访问。请注意,如果您使用 CPU 执行,代码运行可能耗时较长;而使用 GPU 则可显著缩短训练时间。

后续步骤 / 模型改进

改进 NLP 模型通常需要多种策略,需根据具体任务的需求与约束进行定制。超参数调优是一种常见方法,涉及调整学习率、批量大小或神经网络层数等参数。

这些超参数会显著影响模型性能,通常通过网格搜索或随机搜索等技术进行优化。

迁移学习,尤其是基于 BERT 或 GPT 的模型,已在改进 NLP 任务方面展现出巨大潜力。这些模型在大规模文本语料上进行预训练,然后在特定任务上进行微调,从而利用预训练阶段获得的一般语言理解能力。该方法在包括情感分析在内的广泛 NLP 任务中持续取得业界领先的效果。

使用 PyTorch 的 NLP 真实应用

自然语言处理(NLP)模型,特别是基于 PyTorch 等框架实现的模型,已在真实场景中得到广泛应用,重塑了我们数字生活的诸多方面。

聊天机器人已成为客户服务平台不可或缺的一部分,它们利用 NLP 模型理解并响应用户问题。这些模型能够处理自然语言输入、推断意图并生成类人回复,提供流畅的交互体验。

在推荐系统领域,NLP 模型可分析用户评论与反馈,理解用户偏好,从而提升推荐的个性化程度。

情感分析工具同样高度依赖 NLP。此类工具可审视社交媒体帖子、客户评论或任意文本数据,并推断其背后的情感。企业常据此进行市场研究或评估公众对其产品或服务的看法,从而做出数据驱动的决策。

在这篇自然语言处理教程中,了解更多使用 Google BERT 的真实用例。

结语

PyTorch 为构建 NLP 模型提供了强大而灵活的平台。在本教程中,我们演示了使用 LSTM 架构开发情感分析模型的全过程,重点涵盖文本数据预处理、模型构建、训练与验证,以及对未见数据的预测。

这只是使用 NLP 与 PyTorch 的冰山一角。NLP 的应用极为广泛,从聊天机器人与推荐系统到情感分析工具,远不止于此。

该领域持续演进,尤其是随着 BERT 和 GPT 等迁移学习模型的出现,为未来探索带来更多令人兴奋的可能性。掌握基于 PyTorch 的 NLP 虽具挑战,但回报丰厚,它将为您开启理解并与周遭世界交互的新维度。

如果您想深入学习 PyTorch,请查看我们的 Deep Learning with PyTorch 课程。在这里,您将从 PyTorch 入门开始,探索其在神经网络与深度学习中的应用。随后,您将学习人工神经网络以及如何用真实数据对其进行训练。

Moez Ali

作者

Moez Ali

数据科学家,PyCaret 的创始人兼作者

主题