[深度學習] 自然語言處理 --- 文本分類模型總結

文本分類

包括基於word2vec預訓練的文本分類,與及基於最新的預訓練模型(ELMO,BERT等)的文本分類

  1. fastText 模型
  2. textCNN 模型
  3. charCNN 模型
  4. Bi-LSTM 模型
  5. Bi-LSTM + Attention 模型
  6. RCNN 模型
  7. Adversarial LSTM 模型
  8. Transformer 模型
  9. ELMO 預訓練模型
  10. BERT 預訓練模型

一 fastText 模型

fastText模型架構和word2vec中的CBOW很相似, 不同之處是fastText預測標籤而CBOW預測的是中間詞,即模型架構類似但是模型的任務不同。

 

其中x1,x2,...,xN−1,xN表示一個文本中的n-gram向量,每個特徵是詞向量的平均值。這和前文中提到的cbow相似,cbow用上下文去預測中心詞,而此處用全部的n-gram去預測指定類別。

 

import torch.nn as nn
import torch.nn.functional as F

class FastText(nn.Module):
    def __init__(self, vocab_size, embedding_dim, output_dim, pad_idx):
        
        super().__init__()
        
        self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
        
        self.fc = nn.Linear(embedding_dim, output_dim)
        
    def forward(self, text):
        
        #text = [sent len, batch size]
        
        embedded = self.embedding(text)
                
        #embedded = [sent len, batch size, emb dim]
        
        embedded = embedded.permute(1, 0, 2)
        
        #embedded = [batch size, sent len, emb dim]
        
        pooled = F.avg_pool2d(embedded, (embedded.shape[1], 1)).squeeze(1) 
        
        #pooled = [batch size, embedding_dim]
                
        return self.fc(pooled)

 

二 TextCNN模型

TextCNN 是利用卷積神經網絡對文本進行分類的算法,由 Yoon Kim 在 “Convolutional Neural Networks for Sentence Classification” 一文  中提出. 是2014年的算法.

將Text的詞向量拼接在一起,就好比一張圖,只不過這個圖只是一個channel的.這裏使用的就是Conv1d.

模型的結構是:

  1. Embedding layer
  2. Convolutional layer:可以用不同尺度的filter產生多個feature map
  3. MaxPooling Layer
  4. Feedfoward layer
  5. Softmax Layer

class CNN1d(nn.Module):
    def __init__(self, vocab_size, embedding_dim, n_filters, filter_sizes, output_dim, 
                 dropout, pad_idx):
        
        super().__init__()
        
        self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx)
        
        self.convs = nn.ModuleList([
                                    nn.Conv1d(in_channels = embedding_dim, 
                                              out_channels = n_filters, 
                                              kernel_size = fs)
                                    for fs in filter_sizes
                                    ])
        
        self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, text):
        
        #text = [sent len, batch size]
        
        text = text.permute(1, 0)
        
        #text = [batch size, sent len]
        
        embedded = self.embedding(text)
                
        #embedded = [batch size, sent len, emb dim]
        
        embedded = embedded.permute(0, 2, 1)
        
        #embedded = [batch size, emb dim, sent len]
        
        conved = [F.relu(conv(embedded)) for conv in self.convs]
            
        #conved_n = [batch size, n_filters, sent len - filter_sizes[n] + 1]
        
        pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]
        
        #pooled_n = [batch size, n_filters]
        
        cat = self.dropout(torch.cat(pooled, dim = 1))
        
        #cat = [batch size, n_filters * len(filter_sizes)]
            
        return self.fc(cat)

 

三 CharCNN模型

在charCNN論文Character-level Convolutional Networks for Text Classification中提出了6層卷積層 + 3層全連接層的結構,

在此之前很多基於深度學習的模型都是使用更高層面的單元對文本或者語言進行建模,比如單詞(統計信息或者 n-grams、word2vec 等),短語(phrases),句子(sentence)層面,或者對語義和語法結構進行分析,但是CharCNN則提出了從字符層面進行文本分類,提取出高層抽象概念。

字符編碼層 爲了實現 CharCNN,首先要做的就是構建字母表,本文中使用的字母標如下,共有 69 個字符,對其使用 one-hot 編碼,外加一個全零向量(用於處理不在該字符表中的字符),所以共 70 個,所以每個字符轉化爲一個 70 維的向量。文中還提到要反向處理字符編碼,即反向讀取文本,這樣做的好處是最新讀入的字符總是在輸出開始的地方。:

 

模型卷積 - 池化層 文中提出了兩種規模的神經網絡–large 和 small。(kernel——size的不同)都由 6 個卷積層和 3 個全連接層共 9 層神經網絡組成。這裏使用的是 1-D 卷積神經網絡。除此之外,在三個全連接層之間加入兩個 dropout 層以實現模型正則化。

import torch
from torch import nn
import numpy as np
from utils import *

class CharCNN(nn.Module):
    def __init__(self, config, vocab_size, embeddings):
        super(CharCNN, self).__init__()
        self.config = config
        embed_size = vocab_size
        
        # Embedding Layer
        self.embeddings = nn.Embedding(vocab_size, embed_size)
        self.embeddings.weight = nn.Parameter(embeddings, requires_grad=False)
        
        # This stackoverflow thread explains how conv1d works
        # https://stackoverflow.com/questions/46503816/keras-conv1d-layer-parameters-filters-and-kernel-size/46504997
        conv1 = nn.Sequential(
            nn.Conv1d(in_channels=embed_size, out_channels=self.config.num_channels, kernel_size=7),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=3)
        ) # (batch_size, num_channels, (seq_len-6)/3)
        conv2 = nn.Sequential(
            nn.Conv1d(in_channels=self.config.num_channels, out_channels=self.config.num_channels, kernel_size=7),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=3)
        ) # (batch_size, num_channels, (seq_len-6-18)/(3*3))
        conv3 = nn.Sequential(
            nn.Conv1d(in_channels=self.config.num_channels, out_channels=self.config.num_channels, kernel_size=3),
            nn.ReLU()
        ) # (batch_size, num_channels, (seq_len-6-18-18)/(3*3))
        conv4 = nn.Sequential(
            nn.Conv1d(in_channels=self.config.num_channels, out_channels=self.config.num_channels, kernel_size=3),
            nn.ReLU()
        ) # (batch_size, num_channels, (seq_len-6-18-18-18)/(3*3))
        conv5 = nn.Sequential(
            nn.Conv1d(in_channels=self.config.num_channels, out_channels=self.config.num_channels, kernel_size=3),
            nn.ReLU()
        ) # (batch_size, num_channels, (seq_len-6-18-18-18-18)/(3*3))
        conv6 = nn.Sequential(
            nn.Conv1d(in_channels=self.config.num_channels, out_channels=self.config.num_channels, kernel_size=3),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=3)
        ) # (batch_size, num_channels, (seq_len-6-18-18-18-18-18)/(3*3*3))
        
        # Length of output after conv6        
        conv_output_size = self.config.num_channels * ((self.config.seq_len - 96) // 27)
        
        linear1 = nn.Sequential(
            nn.Linear(conv_output_size, self.config.linear_size),
            nn.ReLU(),
            nn.Dropout(self.config.dropout_keep)
        )
        linear2 = nn.Sequential(
            nn.Linear(self.config.linear_size, self.config.linear_size),
            nn.ReLU(),
            nn.Dropout(self.config.dropout_keep)
        )
        linear3 = nn.Sequential(
            nn.Linear(self.config.linear_size, self.config.output_size),
            nn.Softmax()
        )
        
        self.convolutional_layers = nn.Sequential(conv1,conv2,conv3,conv4,conv5,conv6)
        self.linear_layers = nn.Sequential(linear1, linear2, linear3)
    
    def forward(self, x):
        embedded_sent = self.embeddings(x).permute(1,2,0) # shape=(batch_size,embed_size,seq_len)
        conv_out = self.convolutional_layers(embedded_sent)
        conv_out = conv_out.view(conv_out.shape[0], -1)
        linear_output = self.linear_layers(conv_out)
        return linear_output
    

 

四 Bi-LSTM模型

Bi-LSTM即雙向LSTM,較單向的LSTM,Bi-LSTM能更好地捕獲句子中上下文的信息。



雙向循環神經網絡(BRNN)的基本思想是提出每一個訓練序列向前和向後分別是兩個循環神經網絡(RNN),而且這兩個都連接着一個輸出層。這個結構提供給輸出層輸入序列中每一個點的完整的過去和未來的上下文信息。下圖展示的是一個沿着時間展開的雙向循環神經網絡。六個獨特的權值在每一個時步被重複的利用,六個權值分別對應:輸入到向前和向後隱含層(w1, w3),隱含層到隱含層自己(w2, w5),向前和向後隱含層到輸出層(w4, w6)。值得注意的是:向前和向後隱含層之間沒有信息流,這保證了展開圖是非循環的。

import torch
from torch import nn
import numpy as np
from utils import *

class TextRNN(nn.Module):
    def __init__(self, config, vocab_size, word_embeddings):
        super(TextRNN, self).__init__()
        self.config = config
        
        # Embedding Layer
        self.embeddings = nn.Embedding(vocab_size, self.config.embed_size)
        self.embeddings.weight = nn.Parameter(word_embeddings, requires_grad=False)
        
        self.lstm = nn.LSTM(input_size = self.config.embed_size,
                            hidden_size = self.config.hidden_size,
                            num_layers = self.config.hidden_layers,
                            dropout = self.config.dropout_keep,
                            bidirectional = self.config.bidirectional)
        
        self.dropout = nn.Dropout(self.config.dropout_keep)
        
        # Fully-Connected Layer
        self.fc = nn.Linear(
            self.config.hidden_size * self.config.hidden_layers * (1+self.config.bidirectional),
            self.config.output_size
        )
        
        # Softmax non-linearity
        self.softmax = nn.Softmax()
        
    def forward(self, x):
        # x.shape = (max_sen_len, batch_size)
        embedded_sent = self.embeddings(x)
        # embedded_sent.shape = (max_sen_len=20, batch_size=64,embed_size=300)

        lstm_out, (h_n,c_n) = self.lstm(embedded_sent)
        final_feature_map = self.dropout(h_n) # shape=(num_layers * num_directions, 64, hidden_size)
        
        # Convert input to (64, hidden_size * hidden_layers * num_directions) for linear layer
        final_feature_map = torch.cat([final_feature_map[i,:,:] for i in range(final_feature_map.shape[0])], dim=1)
        final_out = self.fc(final_feature_map)
        return self.softmax(final_out)

 

五 Bi-LSTM+Attention 模型

Bi-LSTM + Attention模型來源於論文Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification。關於Attention的介紹見這篇

  Bi-LSTM + Attention 就是在Bi-LSTM的模型上加入Attention層,在Bi-LSTM中我們會用最後一個時序的輸出向量 作爲特徵向量,然後進行softmax分類。Attention是先計算每個時序的權重,然後將所有時序 的向量進行加權和作爲特徵向量,然後進行softmax分類。在實驗中,加上Attention確實對結果有所提升。其模型結構如下圖:

 

六 RCNN模型

Here, we have implemented Recurrent Convolutional Neural Network model for text classification, as proposed in the paper Recurrent Convolutional Neural Networks for Text Classification.

在文本表示方面,會有超過filter_size的上下文的語義缺失,因此本篇文章利用RNN來進行文本表示,中心詞左側和右側的詞設爲trainable,然後將中心詞左側和右側的詞concat作爲中心詞的表示。 當前step的中心詞不輸入lstm,僅僅與左側詞和右側詞在lstm的輸出concat。

先經過1層雙向LSTM,該詞的左側的詞正向輸入進去得到一個hidden state(從上往下),該詞的右側反向輸入進去得到一個hidden state(從下往上)。再結合該詞的詞向量,生成一個 1 * 3k 的向量。

再經過全連接層,tanh爲非線性函數,得到y2。
再經過最大池化層,得出最大化向量y3.
再經過全連接層,sigmod爲非線性函數,得到最終的多分類。

1、結合了中心詞窗口的輸入,其輸出的representation能很好的保留上下文語義信息
2、全連接層+pooling進行特徵選擇,獲取全局最重要的特徵


RCNN 整體的模型構建流程如下:
1)利用Bi-LSTM獲得上下文的信息,類似於語言模型。
2)將Bi-LSTM獲得的隱層輸出和詞向量拼接[fwOutput,wordEmbedding, bwOutput]。
3)將拼接後的向量非線性映射到低維。
4)向量中的每一個位置的值都取所有時序上的最大值,得到最終的特徵向量,該過程類似於max-pool。
5)softmax分類。

import torch
from torch import nn
import numpy as np
from torch.nn import functional as F
from utils import *

class RCNN(nn.Module):
    def __init__(self, config, vocab_size, word_embeddings):
        super(RCNN, self).__init__()
        self.config = config
        
        # Embedding Layer
        self.embeddings = nn.Embedding(vocab_size, self.config.embed_size)
        self.embeddings.weight = nn.Parameter(word_embeddings, requires_grad=False)
        
        # Bi-directional LSTM for RCNN
        self.lstm = nn.LSTM(input_size = self.config.embed_size,
                            hidden_size = self.config.hidden_size,
                            num_layers = self.config.hidden_layers,
                            dropout = self.config.dropout_keep,
                            bidirectional = True)
        
        self.dropout = nn.Dropout(self.config.dropout_keep)
        
        # Linear layer to get "convolution output" to be passed to Pooling Layer
        self.W = nn.Linear(
            self.config.embed_size + 2*self.config.hidden_size,
            self.config.hidden_size_linear
        )
        
        # Tanh non-linearity
        self.tanh = nn.Tanh()
        
        # Fully-Connected Layer
        self.fc = nn.Linear(
            self.config.hidden_size_linear,
            self.config.output_size
        )
        
        # Softmax non-linearity
        self.softmax = nn.Softmax()
        
    def forward(self, x):
        # x.shape = (seq_len, batch_size)
        embedded_sent = self.embeddings(x)
        # embedded_sent.shape = (seq_len, batch_size, embed_size)

        lstm_out, (h_n,c_n) = self.lstm(embedded_sent)
        # lstm_out.shape = (seq_len, batch_size, 2 * hidden_size)
        
        input_features = torch.cat([lstm_out,embedded_sent], 2).permute(1,0,2)
        # final_features.shape = (batch_size, seq_len, embed_size + 2*hidden_size)
        
        linear_output = self.tanh(
            self.W(input_features)
        )
        # linear_output.shape = (batch_size, seq_len, hidden_size_linear)
        
        linear_output = linear_output.permute(0,2,1) # Reshaping fot max_pool
        
        max_out_features = F.max_pool1d(linear_output, linear_output.shape[2]).squeeze(2)
        # max_out_features.shape = (batch_size, hidden_size_linear)
        
        max_out_features = self.dropout(max_out_features)
        final_out = self.fc(max_out_features)
        return self.softmax(final_out)
    

Adversarial LSTM模型

模型來源於論文Adversarial Training Methods For Semi-Supervised Text Classification

        上圖中左邊爲正常的LSTM結構,右圖爲Adversarial LSTM結構,可以看出在輸出時加上了噪聲。

  Adversarial LSTM的核心思想是通過對word Embedding上添加噪音生成對抗樣本,將對抗樣本以和原始樣本 同樣的形式餵給模型,得到一個Adversarial Loss,通過和原始樣本的loss相加得到新的損失,通過優化該新 的損失來訓練模型,作者認爲這種方法能對word embedding加上正則化,避免過擬合。

八 Transformer模型

創新之處在於使用了scaled Dot-Product Attention和Multi-Head Attention

Encoder部分


上面的Q,K和V,被作爲一種抽象的向量,主要目的是用來做計算和輔助attention。根據文章我們知道Attention的計算公式如下:

接着是Multi-head Attention:

 



這裏的positional encoding需要說明一下:


公式中pos就代表了位置index,然後i就是index所對應的向量值,是一個標量,然後dmodel就是512了。之所以選擇這個函數是因爲作者假設它能夠讓模型通過相關的位置學習Attend。
(引入這個的原因就是因爲模型裏沒有用到RNN和CNN,不能編碼序列順序,因此需要顯示的輸入位置信息.之前用到的由position embedding,作者發現上述方法與這個方法差不多.位置特徵在這裏是一種重要特徵。)

Decoder部分

對比單個encoder和decoder,可以看出,decoder多出了一個encoder-decoder Attention layer,接收encoder部分輸出的向量和decoder自身的self attention出來的向量,然後再進入到全連接的前饋網絡中去,最後向量輸出到下一個的decoder



最後一個decoder輸出的向量會經過Linear層和softmax層。Linear層的作用就是對decoder部分出來的向量做映射成一個logits向量,然後softmax層根據這個logits向量,將其轉換爲了概率值,最後找到概率最大值的位置。這樣就完成了解碼的輸出了。

 

 

九  ELMO 預訓練模型

ELMo模型是利用BiLM(雙向語言模型)來預訓練詞的向量表示,可以根據我們的訓練集動態的生成詞的向量表示。ELMo預訓練模型來源於論文:Deep contextualized word representations。具體的ELMo模型的詳細介紹見ELMO模型(Deep contextualized word representation)

  ELMo的模型代碼發佈在github上,我們在調用ELMo預訓練模型時主要使用到bilm中的代碼,因此可以將bilm這個文件夾拷貝到自己的項目路徑下,之後需要導入這個文件夾中的類和函數。此外,usage_cached.py,usage_character.py,usage_token.py這三個文件中的代碼是告訴你該怎麼去調用ELMo模型動態的生成詞向量。在這裏我們使用usage_token.py中的方法,這個計算量相對要小一些。

  在使用之前我們還需要去下載已經預訓練好的模型參數權重,打開https://allennlp.org/elmo鏈接,在Pre-trained ELMo Models 這個版塊下總共有四個不同版本的模型,可以自己選擇,我們在這裏選擇Small這個規格的模型,總共有兩個文件需要下載,一個"options"的json文件,保存了模型的配置參數,另一個是"weights"的hdf5文件,保存了模型的結構和權重值(可以用h5py讀取看看)。

 

十 BERT 預訓練模型

    BERT 模型來源於論文BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding。BERT模型是谷歌提出的基於雙向Transformer構建的語言模型。BERT模型和ELMo有大不同,在之前的預訓練模型(包括word2vec,ELMo等)都會生成詞向量,這種類別的預訓練模型屬於domain transfer。而近一兩年提出的ULMFiT,GPT,BERT等都屬於模型遷移。

  BERT 模型是將預訓練模型和下游任務模型結合在一起的,也就是說在做下游任務時仍然是用BERT模型,而且天然支持文本分類任務,在做文本分類任務時不需要對模型做修改。谷歌提供了下面七種預訓練好的模型文件。

  

  BERT模型在英文數據集上提供了兩種大小的模型,Base和Large。Uncased是意味着輸入的詞都會轉變成小寫,cased是意味着輸入的詞會保存其大寫(在命名實體識別等項目上需要)。Multilingual是支持多語言的,最後一個是中文預訓練模型。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章