BiLSTM介紹及程式碼實現

哈工大SCIR發表於2018-10-24

一、介紹

1.1 文章組織

本文簡要介紹了BiLSTM的基本原理,並以句子級情感分類任務為例介紹為什麼需要使用LSTM或BiLSTM進行建模。在文章的最後,我們給出在PyTorch下BiLSTM的實現程式碼,供讀者參考。

1.2 情感分類任務

自然語言處理中情感分類任務是對給定文字進行情感傾向分類的任務,粗略來看可以認為其是分類任務中的一類。對於情感分類任務,目前通常的做法是先對詞或者短語進行表示,再通過某種組合方式把句子中詞的表示組合成句子的表示。最後,利用句子的表示對句子進行情感分類。

舉一個對句子進行褒貶二分類的例子。

句子:我愛賽爾

情感標籤:褒義

1.3 什麼是LSTM和BiLSTM

LSTM的全稱是Long Short-Term Memory,它是RNN(Recurrent Neural Network)的一種。LSTM由於其設計的特點,非常適合用於對時序資料的建模,如文字資料。BiLSTM是Bi-directional Long Short-Term Memory的縮寫,是由前向LSTM與後向LSTM組合而成。兩者在自然語言處理任務中都常被用來建模上下文資訊。

1.4 為什麼使用LSTM與BiLSTM

將詞的表示組合成句子的表示,可以採用相加的方法,即將所有詞的表示進行加和,或者取平均等方法,但是這些方法沒有考慮到詞語在句子中前後順序。如句子“我不覺得他好”。“不”字是對後面“好”的否定,即該句子的情感極性是貶義。使用LSTM模型可以更好的捕捉到較長距離的依賴關係。因為LSTM通過訓練過程可以學到記憶哪些資訊和遺忘哪些資訊。

但是利用LSTM對句子進行建模還存在一個問題:無法編碼從後到前的資訊。在更細粒度的分類時,如對於強程度的褒義、弱程度的褒義、中性、弱程度的貶義、強程度的貶義的五分類任務需要注意情感詞、程度詞、否定詞之間的互動。舉一個例子,“這個餐廳髒得不行,沒有隔壁好”,這裡的“不行”是對“髒”的程度的一種修飾,通過BiLSTM可以更好的捕捉雙向的語義依賴。

二、BiLSTM原理簡介

2.1 LSTM介紹

2.1.1 總體框架

LSTM模型是由BiLSTM介紹及程式碼實現時刻的輸入詞BiLSTM介紹及程式碼實現,細胞狀態 BiLSTM介紹及程式碼實現,臨時細胞狀態BiLSTM介紹及程式碼實現,隱層狀態BiLSTM介紹及程式碼實現遺忘門BiLSTM介紹及程式碼實現,記憶門BiLSTM介紹及程式碼實現,輸出門BiLSTM介紹及程式碼實現組成。LSTM的計算過程可以概括為,通過對細胞狀態中資訊遺忘和記憶新的資訊使得對後續時刻計算有用的資訊得以傳遞,而無用的資訊被丟棄,並在每個時間步都會輸出隱層狀態BiLSTM介紹及程式碼實現,其中遺忘,記憶與輸出由通過上個時刻的隱層狀態BiLSTM介紹及程式碼實現和當前輸入BiLSTM介紹及程式碼實現計算出來的遺忘門BiLSTM介紹及程式碼實現,記憶門BiLSTM介紹及程式碼實現,輸出門BiLSTM介紹及程式碼實現來控制。

總體框架如圖1所示。

BiLSTM介紹及程式碼實現圖1. LSTM總體框架

2.1.2 詳細介紹計算過程

計算遺忘門,選擇要遺忘的資訊。

輸入:前一時刻的隱層狀態BiLSTM介紹及程式碼實現,當前時刻的輸入詞 BiLSTM介紹及程式碼實現

輸出:遺忘門的值BiLSTM介紹及程式碼實現

BiLSTM介紹及程式碼實現圖2. 計算遺忘門

計算記憶門,選擇要記憶的資訊。

輸入:前一時刻的隱層狀態BiLSTM介紹及程式碼實現,當前時刻的輸入詞 BiLSTM介紹及程式碼實現

輸出:記憶門的值BiLSTM介紹及程式碼實現,臨時細胞狀態BiLSTM介紹及程式碼實現

BiLSTM介紹及程式碼實現圖3. 計算記憶門和臨時細胞狀態

計算當前時刻細胞狀態

輸入:記憶門的值BiLSTM介紹及程式碼實現遺忘門的值BiLSTM介紹及程式碼實現,臨時細胞狀態BiLSTM介紹及程式碼實現,上一刻細胞狀態BiLSTM介紹及程式碼實現

輸出:當前時刻細胞狀態BiLSTM介紹及程式碼實現

BiLSTM介紹及程式碼實現圖4. 計算當前時刻細胞狀態計算輸出門和當前時刻隱層狀態

輸入:前一時刻的隱層狀態BiLSTM介紹及程式碼實現,當前時刻的輸入詞BiLSTM介紹及程式碼實現 ,當前時刻細胞狀態BiLSTM介紹及程式碼實現

輸出:輸出門的值BiLSTM介紹及程式碼實現,隱層狀態BiLSTM介紹及程式碼實現

BiLSTM介紹及程式碼實現圖5. 計算輸出門和當前時刻隱層狀態

最終,我們可以得到與句子長度相同的隱層狀態序列{BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現, ..., BiLSTM介紹及程式碼實現}。

2.2 BiLSTM介紹

前向的LSTM與後向的LSTM結合成BiLSTM。比如,我們對“我愛中國”這句話進行編碼,模型如圖6所示。

BiLSTM介紹及程式碼實現圖6. 雙向LSTM編碼句子前向的BiLSTM介紹及程式碼實現依次輸入“我”,“愛”,“中國”得到三個向量{BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現}。後向的BiLSTM介紹及程式碼實現依次輸入“中國”,“愛”,“我”得到三個向量{BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現}。最後將前向和後向的隱向量進行拼接得到{[BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現], [BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現], [BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現]},即{BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現}。

對於情感分類任務來說,我們採用的句子的表示往往是[BiLSTM介紹及程式碼實現, BiLSTM介紹及程式碼實現]。因為其包含了前向與後向的所有資訊,如圖7所示。

BiLSTM介紹及程式碼實現圖7. 拼接向量用於情感分類

三、BiLSTM程式碼實現樣例

3.1 模型搭建

使用PyTorch搭建BiLSTM樣例程式碼。程式碼地址為https://github.com/albertwy/BiLSTM/。

  1. class BLSTM(nn.Module):

  2.    """

  3.        Implementation of BLSTM Concatenation for sentiment classification task

  4.    """

  5.    def __init__(self, embeddings, input_dim, hidden_dim, num_layers, output_dim, max_len=40, dropout=0.5):

  6.        super(BLSTM, self).__init__()

  7.        self.emb = nn.Embedding(num_embeddings=embeddings.size(0),

  8.                                embedding_dim=embeddings.size(1),

  9.                                padding_idx=0)

  10.        self.emb.weight = nn.Parameter(embeddings)

  11.        self.input_dim = input_dim

  12.        self.hidden_dim = hidden_dim

  13.        self.output_dim = output_dim

  14.        # sen encoder

  15.        self.sen_len = max_len

  16.        self.sen_rnn = nn.LSTM(input_size=input_dim,

  17.                               hidden_size=hidden_dim,

  18.                               num_layers=num_layers,

  19.                               dropout=dropout,

  20.                               batch_first=True,

  21.                               bidirectional=True)

  22.        self.output = nn.Linear(2 * self.hidden_dim, output_dim)

  23.    def bi_fetch(self, rnn_outs, seq_lengths, batch_size, max_len):

  24.        rnn_outs = rnn_outs.view(batch_size, max_len, 2, -1)

  25.        # (batch_size, max_len, 1, -1)

  26.        fw_out = torch.index_select(rnn_outs, 2, Variable(torch.LongTensor([0])).cuda())

  27.        fw_out = fw_out.view(batch_size * max_len, -1)

  28.        bw_out = torch.index_select(rnn_outs, 2, Variable(torch.LongTensor([1])).cuda())

  29.        bw_out = bw_out.view(batch_size * max_len, -1)

  30.        batch_range = Variable(torch.LongTensor(range(batch_size))).cuda() * max_len

  31.        batch_zeros = Variable(torch.zeros(batch_size).long()).cuda()

  32.        fw_index = batch_range + seq_lengths.view(batch_size) - 1

  33.        fw_out = torch.index_select(fw_out, 0, fw_index)  # (batch_size, hid)

  34.        bw_index = batch_range + batch_zeros

  35.        bw_out = torch.index_select(bw_out, 0, bw_index)

  36.        outs = torch.cat([fw_out, bw_out], dim=1)

  37.        return outs

  38.    def forward(self, sen_batch, sen_lengths, sen_mask_matrix):

  39.        """

  40.        :param sen_batch: (batch, sen_length), tensor for sentence sequence

  41.        :param sen_lengths:

  42.        :param sen_mask_matrix:

  43.        :return:

  44.        """

  45.        ''' Embedding Layer | Padding | Sequence_length 40'''

  46.        sen_batch = self.emb(sen_batch)

  47.        batch_size = len(sen_batch)

  48.        ''' Bi-LSTM Computation '''

  49.        sen_outs, _ = self.sen_rnn(sen_batch.view(batch_size, -1, self.input_dim))

  50.        sen_rnn = sen_outs.contiguous().view(batch_size, -1, 2 * self.hidden_dim)  # (batch, sen_len, 2*hid)

  51.        ''' Fetch the truly last hidden layer of both sides

  52.        '''

  53.        sentence_batch = self.bi_fetch(sen_rnn, sen_lengths, batch_size, self.sen_len)  # (batch_size, 2*hid)

  54.        representation = sentence_batch

  55.        out = self.output(representation)

  56.        out_prob = F.softmax(out.view(batch_size, -1))

  57.        return out_prob

__init__()函式中對網路進行初始化,設定詞向量維度,前向/後向LSTM中隱層向量的維度,還有要分類的類別數等。

bi_fetch()函式的作用是將BiLSTM介紹及程式碼實現BiLSTM介紹及程式碼實現拼接起來並返回拼接後的向量。由於使用了batch,所以需要使用句子長度用來定位開始padding時前一個時刻的輸出的隱層向量。

forward()函式裡進行前向計算,得到各個類別的概率值。

3.2 模型訓練

  1. def train(model, training_data, args, optimizer, criterion):

  2.    model.train()

  3.    batch_size = args.batch_size

  4.    sentences, sentences_seqlen, sentences_mask, labels = training_data

  5.    # print batch_size, len(sentences), len(labels)

  6.    assert batch_size == len(sentences) == len(labels)

  7.    ''' Prepare data and prediction'''

  8.    sentences_, sentences_seqlen_, sentences_mask_ = \

  9.        var_batch(args, batch_size, sentences, sentences_seqlen, sentences_mask)

  10.    labels_ = Variable(torch.LongTensor(labels))

  11.    if args.cuda:

  12.        labels_ = labels_.cuda()

  13.    assert len(sentences) == len(labels)

  14.    model.zero_grad()

  15.    probs = model(sentences_, sentences_seqlen_, sentences_mask_)

  16.    loss = criterion(probs.view(len(labels_), -1), labels_)

  17.    loss.backward()

  18.     optimizer.step()

程式碼中training_data是一個batch的資料,其中包括輸入的句子sentences(句子中每個詞以詞下標表示),輸入句子的長度sentences_seqlen,輸入的句子對應的情感類別labels。 訓練模型前,先清空遺留的梯度值,再根據該batch資料計算出來的梯度進行更新模型。

  1.    model.zero_grad()

  2.    probs = model(sentences_, sentences_seqlen_, sentences_mask_)

  3.    loss = criterion(probs.view(len(labels_), -1), labels_)

  4.    loss.backward()

  5.    optimizer.step()

3.3 模型測試

以下是進行模型測試的程式碼。

  1. def test(model, dataset, args, data_part="test"):

  2.    """

  3.    :param model:

  4.    :param args:

  5.    :param dataset:

  6.    :param data_part:

  7.    :return:

  8.    """

  9.    tvt_set = dataset[data_part]

  10.    tvt_set = yutils.YDataset(tvt_set["xIndexes"],

  11.                              tvt_set["yLabels"],

  12.                              to_pad=True, max_len=args.sen_max_len)

  13.    test_set = tvt_set

  14.    sentences, sentences_seqlen, sentences_mask, labels = test_set.next_batch(len(test_set))

  15.    assert len(test_set) == len(sentences) == len(labels)

  16.    tic = time.time()

  17.    model.eval()

  18.    ''' Prepare data and prediction'''

  19.    batch_size = len(sentences)

  20.    sentences_, sentences_seqlen_, sentences_mask_ = \

  21.        var_batch(args, batch_size, sentences, sentences_seqlen, sentences_mask)

  22.    probs = model(sentences_, sentences_seqlen_, sentences_mask_)

  23.    _, pred = torch.max(probs, dim=1)

  24.    if args.cuda:

  25.        pred = pred.view(-1).cpu().data.numpy()

  26.    else:

  27.        pred = pred.view(-1).data.numpy()

  28.    tit = time.time() - tic

  29.    print "  Predicting {:d} examples using {:5.4f} seconds".format(len(test_set), tit)

  30.    labels = numpy.asarray(labels)

  31.    ''' log and return prf scores '''

  32.    accuracy = test_prf(pred, labels)

  33.    return accuracy

  1. def cal_prf(pred, right, gold, formation=True, metric_type=""):

  2.    """

  3.    :param pred: predicted labels

  4.    :param right: predicting right labels

  5.    :param gold: gold labels

  6.    :param formation: whether format the float to 6 digits

  7.    :param metric_type:

  8.    :return: prf for each label

  9.    """

  10.    num_class = len(pred)

  11.    precision = [0.0] * num_class

  12.    recall = [0.0] * num_class

  13.    f1_score = [0.0] * num_class

  14.    for i in xrange(num_class):

  15.        ''' cal precision for each class: right / predict '''

  16.        precision[i] = 0 if pred[i] == 0 else 1.0 * right[i] / pred[i]

  17.        ''' cal recall for each class: right / gold '''

  18.        recall[i] = 0 if gold[i] == 0 else 1.0 * right[i] / gold[i]

  19.        ''' cal recall for each class: 2 pr / (p+r) '''

  20.        f1_score[i] = 0 if precision[i] == 0 or recall[i] == 0 \

  21.            else 2.0 * (precision[i] * recall[i]) / (precision[i] + recall[i])

  22.        if formation:

  23.            precision[i] = precision[i].__format__(".6f")

  24.            recall[i] = recall[i].__format__(".6f")

  25.            f1_score[i] = f1_score[i].__format__(".6f")

  26.    ''' PRF for each label or PRF for all labels '''

  27.    if metric_type == "macro":

  28.        precision = sum(precision) / len(precision)

  29.        recall = sum(recall) / len(recall)

  30.        f1_score = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

  31.    elif metric_type == "micro":

  32.        precision = 1.0 * sum(right) / sum(pred) if sum(pred) > 0 else 0

  33.        recall = 1.0 * sum(right) / sum(gold) if sum(recall) > 0 else 0

  34.        f1_score = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

  35.    return precision, recall, f1_score

四、總結

本文中,我們結合情感分類任務介紹了LSTM以及BiLSTM的基本原理,並給出一個BiLSTM樣例程式碼。除了情感分類任務,LSTM與BiLSTM自然語言處理領域的其它任務上也得到了廣泛應用,如機器翻譯任務中使用其進行源語言的編碼和目標語言的解碼,機器閱讀理解任務中使用其對文章和問題的編碼等。

五、參考資料

http://colah.github.io/posts/2015-08-Understanding-LSTMs/

相關文章