1. 程式人生 > 程式設計 >Python使用迴圈神經網路解決文字分類問題的方法詳解

Python使用迴圈神經網路解決文字分類問題的方法詳解

本文例項講述了Python使用迴圈神經網路解決文字分類問題的方法。分享給大家供大家參考,具體如下:

1、概念

1.1、迴圈神經網路

迴圈神經網路(Recurrent Neural Network,RNN)是一類以序列資料為輸入,在序列的演進方向進行遞迴且所有節點(迴圈單元)按鏈式連線的遞迴神經網路。

卷積網路的輸入只有輸入資料X,而迴圈神經網路除了輸入資料X之外,每一步的輸出會作為下一步的輸入,如此迴圈,並且每一次採用相同的啟用函式和引數。在每次迴圈中,x0乘以係數U得到s0,再經過係數W輸入到下一次,以此迴圈構成迴圈神經網路的正向傳播。

在反向傳播中要求損失函式E對引數W的導數,通過鏈式求導法則可以得到右下的公式

迴圈神經網路與卷積神經網路作比較,卷積神經網路是一個輸出經過網路產生一個輸出。而迴圈神經網路可以實現一個輸入多個輸出(生成圖片描述)、多個輸入一個輸出(文字分類)、多輸入多輸出(機器翻譯、視訊解說)。

RNN使用的是tan啟用函式,輸出在-1到1之間,容易梯度消失。距離輸出較遠的步驟對於梯度貢獻很小。

將底層的輸出作為高層的輸入就構成了多層的RNN網路,而且高層之間也可以進行傳遞,並且可以採用殘差連線防止過擬合。

1.2、長短期記憶網路

RNN的每次傳播之間只有一個引數W,用這一個引數很難描述大量的、複雜的資訊需求,為了解決這個問題引入了長短期記憶網路(Long Short Term Memory,LSTM)

。這個網路可以進行選擇性機制,選擇性的輸入、輸出需要使用的資訊以及選擇性地遺忘不需要的資訊。選擇性機制的實現是通過Sigmoid門實現的,sigmoid函式的輸出介於0到1之間,0代表遺忘,1代表記憶,0.5代表記憶50%

LSTM網路結構如下圖所示,

如上右圖所示為本輪運算的隱含狀態state,當前狀態由上一狀態和遺忘門結果作點積,再加上傳入們結果得到

如下左圖所示為遺忘門結構,上一輪的輸出ht-1和資料xt在經過遺忘門選擇是否遺忘之後,產生遺忘結果ft

如下中圖所示為傳入門結構,ht-1和xt在經過遺忘門的結果it和tanh的結果Ct作點積運算得到本次運算的輸入

如下右圖所示為輸出門結構,ht-1和xt經過遺忘門的結果ot與當狀態作點積產生本次的輸出

如下實現LSTM網路,首先定義_generate_params函式用於生成每個門所需的引數,呼叫該函式定義輸入門、輸出門、遺忘門、和中間狀態tanh的引數。每個門的引數都是三個,輸入x、h的權重和偏置值。

接著開始進行LSTM的每輪迴圈計算,輸入門計算就是將輸入embedded_input矩陣乘以輸入門引數x_in,再加上h和對應引數相乘的結果,最後再加上偏置值b_in經過sigmoid便得到輸入門結果。

同理進行矩陣相乘加偏置操作得到遺忘門、輸出門的結果。中間態tanh與三個門的操作類似,只不過最後經過tanh函式。

將上一個隱含態state乘以遺忘門加上輸入門乘以中間態的結果就得到當前的隱含態state

將當前的state經過tanh函式再加上輸出門就得到本輪的輸出h

經過多輪輸入迴圈得到的就是LSTM網路的最後輸出。

# 實現LSTM網路
  # 生成Cell網格所需引數
  def _generate_paramas(x_size,h_size,b_size):
    x_w = tf.get_variable('x_weight',x_size)
    h_w = tf.get_variable('h_weight',h_size)
    bias = tf.get_variable('bias',b_size,initializer=tf.constant_initializer(0.0))
    return x_w,h_w,bias
 
  scale = 1.0 / math.sqrt(embedding_size + lstm_nodes[-1]) / 3.0
  lstm_init = tf.random_uniform_initializer(-scale,scale)
  with tf.variable_scope('lstm_nn',initializer=lstm_init):
    # 輸入門引數
    with tf.variable_scope('input'):
      x_in,h_in,b_in = _generate_paramas(
        x_size=[embedding_size,lstm_nodes[0]],h_size=[lstm_nodes[0],b_size=[1,lstm_nodes[0]]
      )
    # 輸出門引數
    with tf.variable_scope('output'):
      x_out,h_out,b_out = _generate_paramas(
        x_size=[embedding_size,lstm_nodes[0]]
      )
    # 遺忘門引數
    with tf.variable_scope('forget'):
      x_f,h_f,b_f = _generate_paramas(
        x_size=[embedding_size,lstm_nodes[0]]
      )
    # 中間狀態引數
    with tf.variable_scope('mid_state'):
      x_m,h_m,b_m = _generate_paramas(
        x_size=[embedding_size,lstm_nodes[0]]
      )
 
    # 兩個初始化狀態,隱含狀態state和初始輸入h
    state = tf.Variable(tf.zeros([batch_size,lstm_nodes[0]]),trainable=False)
    h = tf.Variable(tf.zeros([batch_size,trainable=False)
    # 遍歷LSTM每輪迴圈,即每個詞的輸入過程
    for i in range(max_words):
      # 取出每輪輸入,三維陣列embedd_inputs的第二維代表訓練的輪數
      embedded_input = embedded_inputs[:,i,:]
      # 將取出的結果reshape為二維
      embedded_input = tf.reshape(embedded_input,[batch_size,embedding_size])
      # 遺忘門計算
      forget_gate = tf.sigmoid(tf.matmul(embedded_input,x_f) + tf.matmul(h,h_f) + b_f)
      # 輸入門計算
      input_gate = tf.sigmoid(tf.matmul(embedded_input,x_in) + tf.matmul(h,h_in) + b_in)
      # 輸出門
      output_gate = tf.sigmoid(tf.matmul(embedded_input,x_out) + tf.matmul(h,h_out) + b_out)
      # 中間狀態
      mid_state = tf.tanh(tf.matmul(embedded_input,x_m) + tf.matmul(h,h_m) + b_m)
      # 計算隱含狀態state和輸入h
      state = state * forget_gate + input_gate * mid_state
      h = output_gate + tf.tanh(state)
    # 最後遍歷的結果就是LSTM的輸出
    last_output = h

1.3、文字分類

文字分類問題就是對輸入的文字字串進行分析判斷,之後再輸出結果。字串無法直接輸入到RNN網路,因此在輸入之前需要先對文字拆分成單個片語,將片語進行embedding編碼成一個向量,每輪輸入一個片語,當最後一個片語輸入完畢時得到輸出結果也是一個向量。embedding將一個詞對應為一個向量,向量的每一個維度對應一個浮點值,動態調整這些浮點值使得embedding編碼和詞的意思相關。這樣網路的輸入輸出都是向量,再最後進行全連線操作對應到不同的分類即可。

RNN網路不可避免地帶來問題就是最後的輸出結果受最近的輸入較大,而之前較遠的輸入可能無法影響結果,這就是資訊瓶頸問題,為了解決這個問題引入了雙向LSTM。雙向LSTM不僅增加了反向資訊傳播,而且每一輪的都會有一個輸出,將這些輸出進行組合之後再傳給全連線層。

另一個文字分類模型就是HAN(Hierarchy Attention Network),首先將文字分為句子、詞語級別,將輸入的詞語進行編碼然後相加得到句子的編碼,然後再將句子編碼相加得到最後的文字編碼。而attention是指在每一個級別的編碼進行累加前,加入一個加權值,根據不同的權值對編碼進行累加。

由於輸入的文字長度不統一,所以無法直接使用神經網路進行學習,為了解決這個問題,可以將輸入文字的長度統一為一個最大值,勉強採用卷積神經網路進行學習,即TextCNN。文字卷積網路的卷積過程採用的是多通道一維卷積,與二維卷積相比一維卷積就是卷積核只在一個方向上移動。例如下左圖所示,1×1+5×2+2×2+4×3+3×3+3×4=48,之後卷積核向下移動一格得到45,以此類推。如下右圖所示,輸入長短不一的多個詞彙。首先將其全部填充為六通道的embedding陣列,然後採用六通道的一維卷積核從上到下進行卷積,得到一維的陣列,然後再經過池化層和全連線層後輸出。

可以看到CNN網路不能完美處理輸入長短不一的序列式問題,但是它可以並行處理多個片語,效率更高,而RNN可以更好地處理序列式的輸入,將兩者的優勢結合起來就構成了R-CNN模型。首先通過雙向RNN網路對輸入進行特徵提取,再使用CNN進一步提取,之後通過池化層將每一步的特徵融合在一起,最後經過全連線層進行分類。

無論什麼模型都需要使用embedding將輸入轉化為一個向量,當輸入過大時,轉化的embedding層引數就會過大,不僅不利於儲存,還會造成過擬合,因此需要對embedding層進行壓縮。原來的embedding編碼是一個引數對應一個輸入,例如wait對應引數x1,for對應x2,the對應x3。如果輸入過多,編碼引數就會很大,可以採用兩個引數對組合的方式來編碼輸入,例如wait對應(x1,x2),for對應(x1,x3)...,這樣就可以極大的節省引數的數量,這就是共享壓縮

2、通過Text RNN進行文字分類

2.1、資料預處理

在網上下載的文字分類資料集檔案如下,分為測試集和訓練集資料,每個訓練集下有四個資料夾,每個資料夾是一個分類,每個分類有1000個txt檔案,每個檔案中有一條該分類的文字

通過os.walk遍歷所有訓練集檔案,將分類文字通過jieba庫拆分成單個片語,用空格分隔。然後將分類文字新增到開頭,並用製表符分隔,最後將結果輸出到train_segment.txt,

# 將檔案中的句子通過jieba庫拆分為單個詞
def segment_word(input_file,output_file):
  # 迴圈遍歷訓練資料集的每一個檔案
  for root,folders,files in os.walk(input_file):
    print('root:',root)
    for folder in folders:
      print('dir:',folder)
    for file in files:
      file_dir = os.path.join(root,file)
      with open(file_dir,'rb') as in_file:
        # 讀取檔案中的文字
        sentence = in_file.read()
        # 通過jieba函式庫將句子拆分為單個片語
        words = jieba.cut(sentence)
        # 資料夾路徑最後兩個字即為分類名
        content = root[-2:] + '\t'
        # 去除片語中的空格,排除為空的片語
        for word in words:
          word = word.strip(' ')
          if word != '':
            content += word + ' '
      # 換行並將文字寫入輸出檔案
      content += '\n'
      with open(output_file,'a') as outfile:
        outfile.write(content.strip(' '))

結果如下:

由於一些片語出現次數很少,不具有統計意義,所以需要排除,通過get_list()方法統計每個片語出現的頻率。利用python自帶的dictionary資料型別可以輕易實現片語資料統計,格式為{"keyword":frequency},frequency記錄keyword出現的次數。如果一個片語是新出現的則作為新詞條加入詞典,否則將frequency值+1。

# 統計每個詞出現的頻率
def get_list(segment_file,out_file):
  # 通過詞典儲存每個片語出現的頻率
  word_dict = {}
  with open(segment_file,'r') as seg_file:
    lines = seg_file.readlines()
    # 遍歷檔案的每一行
    for line in lines:
      line = line.strip('\r\n')
      # 將一行按空格拆分為每個詞,統計詞典
      for word in line.split(' '):
        # 如果這個片語沒有在word_dict詞典中出現過,則新建詞典項並設為0
        word_dict.setdefault(word,0)
        # 將詞典word_dict中片語word對應的項計數加一
        word_dict[word] += 1
    # 將詞典中的列表排序,關鍵字為列表下標為1的項,且逆序
    sorted_list = sorted(word_dict.items(),key=lambda d: d[1],reverse=True)
    with open(out_file,'w') as outfile:
      # 將排序後的每條詞典項寫入檔案
      for item in sorted_list:
        outfile.write('%s\t%d\n' % (item[0],item[1]))

統計結果如下:

2.2、資料讀入

直接使用片語無法進行編碼學習,需要將片語轉化為embedding編碼,根據剛才生成的train_list列表,按照從前往後的順序為每個片語編號,如果片語頻率小於閾值則排除掉。通過Word_list類來構建訓練資料、測試資料的片語物件,在類的建構函式__init__()實現片語的編碼。並定義類方法sentence2id將拆分好的句子片語轉化為對應的id陣列,如果片語列表中沒有該詞,則將該值置為-1。

在定義類之前首先規定一些超引數供後續使用:

# 定義超引數
embedding_size = 32 # 每個片語向量的長度
max_words = 10 # 一個句子最大片語長度
lstm_layers = 2 # lstm網路層數
lstm_nodes = [64,64] # lstm每層結點數
fc_nodes = 64 # 全連線層結點數
batch_size = 100 # 每個批次樣本資料
lstm_grads = 1.0 # lstm網路梯度
learning_rate = 0.001 # 學習率
word_threshold = 10 # 詞表頻率門限,低於該值的詞語不統計
num_classes = 4 # 最後的分類結果有4類
class Word_list:
  def __init__(self,filename):
    # 用詞典型別來儲存需要統計的片語及其頻率
    self._word_dic = {}
    with open(filename,'r',encoding='GB2312',errors='ignore') as f:
      lines = f.readlines()
    for line in lines:
      word,freq = line.strip('\r\n').split('\t')
      freq = int(freq)
      # 如果片語的頻率小於閾值,跳過不統計
      if freq < word_threshold:
        continue
      # 片語列表中每個片語都是不重複的,按序新增到word_dic中即可,下一個片語id就是當前word_dic的長度
      word_id = len(self._word_dic)
      self._word_dic[word] = word_id
 
  def sentence2id(self,sentence):
    # 將以空格分割的句子返回word_dic中對應片語的id,若不存在返回-1
    sentence_id = [self._word_dic.get(word,-1)
            for word in sentence.split()]
    return sentence_id
 
 
train_list = Word_list(train_list_dir)

定義TextData類來完成資料的讀入和管理,在__init__()函式中讀取剛才處理好的train_segment.txt檔案,根據製表符分割類別標記和句子片語,將類別和句子分別轉化為數字id。如果句子的片語超過了最大閾值,則截去後面多餘的,如果不夠則用-1填充。定義類函式_shuffle_data()用於清洗資料,next_batch()用於按批次返回資料和標籤,get_size()用於返回片語總條數。

class TextData:
  def __init__(self,segment_file,word_list):
    self.inputs = []
    self.labels = []
    # 通過詞典管理文字類別
    self.label_dic = {'體育': 0,'校園': 1,'女性': 2,'出版': 3}
    self.index = 0
 
    with open(segment_file,'r') as f:
      lines = f.readlines()
      for line in lines:
        # 文字按製表符分割,前面為類別,後面為句子
        label,content = line.strip('\r\n').split('\t')[0:2]
        self.content_size = len(content)
        # 將類別轉換為數字id
        label_id = self.label_dic.get(label)
        # 將句子轉化為embedding陣列
        content_id = word_list.sentence2id(content)
        # 如果句子的片語長超過最大值,擷取max_words長度以內的id值
        content_id = content_id[0:max_words]
        # 如果不夠則填充-1,直到max_words長度
        padding_num = max_words - len(content_id)
        content_id = content_id + [-1 for i in range(padding_num)]
        self.inputs.append(content_id)
        self.labels.append(label_id)
    self.inputs = np.asarray(self.inputs,dtype=np.int32)
    self.labels = np.asarray(self.labels,dtype=np.int32)
    self._shuffle_data()
 
  # 對資料按照(input,label)對來打亂順序
  def _shuffle_data(self):
    r_index = np.random.permutation(len(self.inputs))
    self.inputs = self.inputs[r_index]
    self.labels = self.labels[r_index]
 
  # 返回一個批次的資料
  def next_batch(self,batch_size):
    # 當前索引+批次大小得到批次的結尾索引
    end_index = self.index + batch_size
    # 如果結尾索引大於樣本總數,則打亂所有樣本從頭開始
    if end_index > len(self.inputs):
      self._shuffle_data()
      self.index = 0
      end_index = batch_size
    # 按索引返回一個批次的資料
    batch_inputs = self.inputs[self.index:end_index]
    batch_labels = self.labels[self.index:end_index]
    self.index = end_index
    return batch_inputs,batch_labels
 
  # 獲取詞表數目
  def get_size(self):
    return self.content_size
 
# 訓練資料集物件
train_set = TextData(train_segment_dir,train_list)
# print(data_set.next_batch(10))
# 訓練資料集片語條數
train_list_size = train_set.get_size()

2.3、構建計算圖模型

定義函式create_model來實現計算圖模型的構建。首先定義模型輸入的佔位符,分別為輸入文字inputs、輸出標籤outputs、Dropout的比率keep_prob。

首先構建embedding層,將輸入的inputs編碼抽取出來拼接成一個矩陣,例如輸入[1,8,3]則抽取embeding[1]、embedding[8]和embedding[3]拼接成一個矩陣

接下來構建LSTM網路,這裡構建了兩層網路,每層的結點數在之前的引數lstm_node[]陣列中定義。每個cell的構建通過函式tf.contrib.rnn.BasicLSTMCell實現,之後經過Dropout操作。再將兩個cell合併為一個LSTM網路,通過函式tf.nn.dynamic_rnn將輸入embedded_inputs輸入到LSTM網路中進行訓練得到輸出rnn_output。這是一個三維陣列,第二維表示訓練的步數,我們只取最後一維的結果,即下標值為-1.

接下來構建全連線層,通過tf.layers.dense函式定義全連線層,再經過一個dropout操作後將輸出對映到類別上,類別的種類的引數num_classes,得到估計值logits

接下來就可以求損失、精確率等評估值了。計算算預測值logits與標籤值outputs之間的交叉熵損失值,接下來通過arg_max計算預測值,進而求準確率

接下來定義訓練方法,通過梯度裁剪應用到變數上以防止梯度消失。

最後將輸入佔位符、損失等評估值、其他訓練引數返回到呼叫函式的外部。

# 建立計算圖模型
def create_model(list_size,num_classes):
  # 定義輸入輸出佔位符
  inputs = tf.placeholder(tf.int32,(batch_size,max_words))
  outputs = tf.placeholder(tf.int32,))
  # 定義是否dropout的比率
  keep_prob = tf.placeholder(tf.float32,name='keep_rate')
  # 記錄訓練的總次數
  global_steps = tf.Variable(tf.zeros([],tf.float32),name='global_steps',trainable=False)
 
  # 將輸入轉化為embedding編碼
  with tf.variable_scope('embedding',initializer=tf.random_normal_initializer(-1.0,1.0)):
    embeddings = tf.get_variable('embedding',[list_size,embedding_size],tf.float32)
    # 將指定行的embedding數值抽取出來
    embedded_inputs = tf.nn.embedding_lookup(embeddings,inputs)
 
  # 實現LSTM網路
  scale = 1.0 / math.sqrt(embedding_size + lstm_nodes[-1]) / 3.0
  lstm_init = tf.random_uniform_initializer(-scale,initializer=lstm_init):
    # 構建兩層的lstm,每層結點數為lstm_nodes[i]
    cells = []
    for i in range(lstm_layers):
      cell = tf.contrib.rnn.BasicLSTMCell(lstm_nodes[i],state_is_tuple=True)
      # 實現Dropout操作
      cell = tf.contrib.rnn.DropoutWrapper(cell,output_keep_prob=keep_prob)
      cells.append(cell)
    # 合併兩個lstm的cell
    cell = tf.contrib.rnn.MultiRNNCell(cells)
    # 將embedded_inputs輸入到RNN中進行訓練
    initial_state = cell.zero_state(batch_size,tf.float32)
    # runn_output:[batch_size,num_timestep,lstm_outputs[-1]
    rnn_output,_ = tf.nn.dynamic_rnn(cell,embedded_inputs,initial_state=initial_state)
    last_output = rnn_output[:,-1,:]
 
  # 構建全連線層
  fc_init = tf.uniform_unit_scaling_initializer(factor=1.0)
  with tf.variable_scope('fc',initializer=fc_init):
    fc1 = tf.layers.dense(last_output,fc_nodes,activation=tf.nn.relu,name='fc1')
    fc1_drop = tf.contrib.layers.dropout(fc1,keep_prob)
    logits = tf.layers.dense(fc1_drop,num_classes,name='fc2')
 
  # 定義評估指標
  with tf.variable_scope('matrics'):
    # 計算損失值
    softmax_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,labels=outputs)
    loss = tf.reduce_mean(softmax_loss)
    # 計算預測值,求第1維中最大值的下標,例如[1,1,5,3,2] argmax=> 2
    y_pred = tf.argmax(tf.nn.softmax(logits),output_type=tf.int32)
    # 求準確率
    correct_prediction = tf.equal(outputs,y_pred)
    accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
 
  # 定義訓練方法
  with tf.variable_scope('train_op'):
    train_var = tf.trainable_variables()
    # for var in train_var:
    #   print(var)
    # 對梯度進行裁剪防止梯度消失或者梯度爆炸
    grads,_ = tf.clip_by_global_norm(tf.gradients(loss,train_var),clip_norm=lstm_grads)
    # 將梯度應用到變數上去
    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
    train_op = optimizer.apply_gradients(zip(grads,global_steps)
 
  # 以元組的方式將結果返回
  return ((inputs,outputs,keep_prob),(loss,accuracy),(train_op,global_steps))
 
# 呼叫構建函式,接收解析返回的引數
placeholders,matrics,others = create_model(train_list_size,num_classes)
inputs,keep_prob = placeholders
loss,accuracy = matrics
train_op,global_steps = others

2.4、進行訓練

通過Session執行計算圖模型,從train_set中按批次獲取訓練集資料並填充佔位符,執行sess.run,獲取損失值、準確率等中間值列印

# 進行訓練
init_op = tf.global_variables_initializer()
train_keep_prob = 0.8    # 訓練集的dropout比率
train_steps = 10000
 
with tf.Session() as sess:
  sess.run(init_op)
 
  for i in range(train_steps):
    # 按批次獲取訓練集資料
    batch_inputs,batch_labels = train_set.next_batch(batch_size)
    # 執行計算圖
    res = sess.run([loss,accuracy,train_op,global_steps],feed_dict={inputs: batch_inputs,outputs: batch_labels,keep_prob: train_keep_prob})
    loss_val,acc_val,_,g_step_val = res
    if g_step_val % 20 == 0:
      print('第%d輪訓練,損失:%3.3f,準確率:%3.5f' % (g_step_val,loss_val,acc_val))

在我的資料集進行一萬輪訓練後,訓練集的準確率在90%左右徘徊

原始碼及相關資料檔案:https://github.com/SuperTory/MachineLearning/tree/master/TextRNN

更多關於Python相關內容感興趣的讀者可檢視本站專題:《Python資料結構與演算法教程》、《Python加密解密演算法與技巧總結》、《Python編碼操作技巧總結》、《Python函式使用技巧總結》、《Python字串操作技巧彙總》及《Python入門與進階經典教程》

希望本文所述對大家Python程式設計有所幫助。