萬字總結Keras深度學習中文文字分類

語言: CN / TW / HK
摘要:文章將詳細講解Keras實現經典的深度學習文字分類演算法,包括LSTM、BiLSTM、BiLSTM+Attention和CNN、TextCNN。

本文分享自華為雲社群《Keras深度學習中文文字分類萬字總結(CNN、TextCNN、BiLSTM、注意力)》,作者: eastmount。

一.文字分類概述

文字分類旨在對文字集按照一定的分類體系或標準進行自動分類標記,屬於一種基於分類體系的自動分類。文字分類最早可以追溯到上世紀50年代,那時主要通過專家定義規則來進行文字分類;80年代出現了利用知識工程建立的專家系統;90年代開始藉助於機器學習方法,通過人工特徵工程和淺層分類模型來進行文字分類。現在多采用詞向量以及深度神經網路來進行文字分類。

牛亞峰老師將傳統的文字分類流程歸納如下圖所示。在傳統的文字分類中,基本上大部分機器學習方法都在文字分類領域有所應用。主要包括:

  • Naive Bayes
  • KNN
  • SVM
  • 集合類方法
  • 最大熵
  • 神經網路

利用Keras框架進行文字分類的基本流程如下:

  • 步驟 1:文字的預處理,分詞->去除停用詞->統計選擇top n的詞做為特徵詞
  • 步驟 2:為每個特徵詞生成ID
  • 步驟 3:將文字轉化成ID序列,並將左側補齊
  • 步驟 4:訓練集shuffle
  • 步驟 5:Embedding Layer 將詞轉化為詞向量
  • 步驟 6:新增模型,構建神經網路結構
  • 步驟 7:訓練模型
  • 步驟 8:得到準確率、召回率、F1值

注意,如果使用TFIDF而非詞向量進行文件表示,則直接分詞去停後生成TFIDF矩陣後輸入模型。

深度學習文字分類方法包括:

  • 卷積神經網路(TextCNN)
  • 迴圈神經網路(TextRNN)
  • TextRNN+Attention
  • TextRCNN(TextRNN+CNN)
  • BiLSTM+Attention
  • 遷移學習

推薦牛亞峰老師的文章:

  • 基於 word2vec 和 CNN 的文字分類 :綜述 & 實踐

二.資料預處理及分詞

這篇文章主要以程式碼為主,演算法原理知識前面的文章和後續文章再繼續補充。資料集如下圖所示:

  • 訓練集:news_dataset_train.csv
    遊戲主題(10000)、體育主題(10000)、文化主題(10000)、財經主題(10000)
  • 測試集:news_dataset_test.csv
    遊戲主題(5000)、體育主題(5000)、文化主題(5000)、財經主題(5000)
  • 驗證集:news_dataset_val.csv
    遊戲主題(5000)、體育主題(5000)、文化主題(5000)、財經主題(5000)

首先需要進行中文分詞預處理,呼叫Jieba庫實現。程式碼如下:

  • data_preprocess.py
# -*- coding:utf-8 -*-
# By:Eastmount CSDN 2021-03-19
import csv
import pandas as pd
import numpy as np
import jieba
import jieba.analyse

#新增自定義詞典和停用詞典
jieba.load_userdict("user_dict.txt")
stop_list = pd.read_csv('stop_words.txt',
                        engine='python',
                        encoding='utf-8',
                        delimiter="\n",
                        names=['t'])['t'].tolist()

#-----------------------------------------------------------------------
#Jieba分詞函式
def txt_cut(juzi):
    return [w for w in jieba.lcut(juzi) if w not in stop_list]

#-----------------------------------------------------------------------
#中文分詞讀取檔案
def fenci(filename,result):
    #寫入分詞結果
    fw = open(result, "w", newline = '',encoding = 'gb18030')
    writer = csv.writer(fw)  
    writer.writerow(['label','cutword'])

    #使用csv.DictReader讀取檔案中的資訊
    labels = []
    contents = []
    with open(filename, "r", encoding="UTF-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            #資料元素獲取
            labels.append(row['label'])
            content = row['content']
            #中文分詞
            seglist = txt_cut(content)
            #空格拼接
            output = ' '.join(list(seglist))
            contents.append(output)
 
            #檔案寫入
            tlist = []
            tlist.append(row['label'])
            tlist.append(output)
            writer.writerow(tlist)
    print(labels[:5])
    print(contents[:5])
    fw.close()

#-----------------------------------------------------------------------
#主函式
if __name__ == '__main__':
    fenci("news_dataset_train.csv", "news_dataset_train_fc.csv")
    fenci("news_dataset_test.csv", "news_dataset_test_fc.csv")
    fenci("news_dataset_val.csv", "news_dataset_val_fc.csv")

執行結果如下圖所示:

接著我們嘗試簡單檢視資料的長度分佈情況及標籤視覺化。

  • data_show.py
# -*- coding: utf-8 -*-
"""
Created on 2021-03-19
@author: xiuzhang Eastmount CSDN
"""
import pandas as pd
import numpy as np
from sklearn import metrics
import matplotlib.pyplot as plt
import seaborn as sns

#---------------------------------------第一步 資料讀取------------------------------------
## 讀取測資料集
train_df = pd.read_csv("news_dataset_train_fc.csv")
val_df = pd.read_csv("news_dataset_val_fc.csv")
test_df = pd.read_csv("news_dataset_test_fc.csv")
print(train_df.head())

## 解決中文顯示問題
plt.rcParams['font.sans-serif'] = ['KaiTi']  #指定預設字型 SimHei黑體
plt.rcParams['axes.unicode_minus'] = False   #解決儲存影象是負號'

## 檢視訓練集都有哪些標籤
plt.figure()
sns.countplot(train_df.label)
plt.xlabel('Label',size = 10)
plt.xticks(size = 10)
plt.show()

## 分析訓練集中片語數量的分佈
print(train_df.cutwordnum.describe())
plt.figure()
plt.hist(train_df.cutwordnum,bins=100)
plt.xlabel("片語長度", size = 12)
plt.ylabel("頻數", size = 12)
plt.title("訓練資料集")
plt.show()

輸出結果如下圖所示,後面的文章我們會介紹論文如何繪製好看的圖表。

注意,如果報錯“UnicodeDecodeError: ‘utf-8’ codec can’t decode byte 0xce in position 17: invalid continuation byte”,需要將CSV檔案儲存為UTF-8格式,如下圖所示。

三.CNN中文文字分類

1.原理介紹

卷積神經網路(Convolutional Neural Networks, CNN)是一類包含卷積計算且具有深度結構的前饋神經網路(Feedforward Neural Networks),是深度學習(deep learning)的代表演算法之一。它通常應用於影象識別和語音識等領域,並能給出更優秀的結果,也可以應用於影片分析、機器翻譯、自然語言處理、藥物發現等領域。著名的阿爾法狗讓計算機看懂圍棋就是基於卷積神經網路的。

  • 卷積是指不在對每個畫素做處理,而是對圖片區域進行處理,這種做法加強了圖片的連續性,看到的是一個圖形而不是一個點,也加深了神經網路對圖片的理解。
  • 通常卷積神經網路會依次經歷“圖片->卷積->持化->卷積->持化->結果傳入兩層全連線神經層->分類器”的過程,最終實現一個CNN的分類處理。

2.程式碼實現

Keras實現文字分類的CNN程式碼如下:

  • Keras_CNN_cnews.py

# -*- coding: utf-8 -*-

"""
Created on 2021-03-19
@author: xiuzhang Eastmount CSDN
CNN Model
"""
import os
import time
import pickle
import pandas as pd
import numpy as np
from sklearn import metrics
import matplotlib.pyplot as plt
import seaborn as sns
import tensorflow as tf
from sklearn.preprocessing import LabelEncoder,OneHotEncoder
from keras.models import Model
from keras.layers import LSTM, Activation, Dense, Dropout, Input, Embedding
from keras.layers import Convolution1D, MaxPool1D, Flatten
from keras.preprocessing.text import Tokenizer
from keras.preprocessing import sequence
from keras.callbacks import EarlyStopping
from keras.models import load_model
from keras.models import Sequential

## GPU處理 讀者如果是CPU註釋該部分程式碼即可
## 指定每個GPU程序中使用視訊記憶體的上限 0.9表示可以使用GPU 90%的資源進行訓練
os.environ["CUDA_DEVICES_ORDER"] = "PCI_BUS_IS"
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))

start = time.clock()

#----------------------------第一步 資料讀取----------------------------
## 讀取測資料集
train_df = pd.read_csv("news_dataset_train_fc.csv")
val_df = pd.read_csv("news_dataset_val_fc.csv")
test_df = pd.read_csv("news_dataset_test_fc.csv")
print(train_df.head())

## 解決中文顯示問題
plt.rcParams['font.sans-serif'] = ['KaiTi']  #指定預設字型 SimHei黑體
plt.rcParams['axes.unicode_minus'] = False   #解決儲存影象是負號'

#--------------------------第二步 OneHotEncoder()編碼--------------------
## 對資料集的標籤資料進行編碼
train_y = train_df.label
val_y = val_df.label
test_y = test_df.label
print("Label:")
print(train_y[:10])

le = LabelEncoder()
train_y = le.fit_transform(train_y).reshape(-1,1)
val_y = le.transform(val_y).reshape(-1,1)
test_y = le.transform(test_y).reshape(-1,1)
print("LabelEncoder")
print(train_y[:10])
print(len(train_y))

## 對資料集的標籤資料進行one-hot編碼
ohe = OneHotEncoder()
train_y = ohe.fit_transform(train_y).toarray()
val_y = ohe.transform(val_y).toarray()
test_y = ohe.transform(test_y).toarray()
print("OneHotEncoder:")
print(train_y[:10])

#-----------------------第三步 使用Tokenizer對片語進行編碼--------------------
max_words = 6000
max_len = 600
tok = Tokenizer(num_words=max_words)  #最大詞語數為6000
print(train_df.cutword[:5])
print(type(train_df.cutword))

## 防止語料中存在數字str處理
train_content = [str(a) for a in train_df.cutword.tolist()]
val_content = [str(a) for a in val_df.cutword.tolist()]
test_content = [str(a) for a in test_df.cutword.tolist()]
tok.fit_on_texts(train_content)
print(tok)

#當建立Tokenizer物件後 使用fit_on_texts()函式識別每個詞
#tok.fit_on_texts(train_df.cutword)

## 儲存訓練好的Tokenizer和匯入
with open('tok.pickle', 'wb') as handle: #saving
    pickle.dump(tok, handle, protocol=pickle.HIGHEST_PROTOCOL)
with open('tok.pickle', 'rb') as handle: #loading
    tok = pickle.load(handle)

## 使用word_index屬性檢視每個詞對應的編碼
## 使用word_counts屬性檢視每個詞對應的頻數
for ii,iterm in enumerate(tok.word_index.items()):
    if ii < 10:
        print(iterm)
    else:
        break
print("===================")  
for ii,iterm in enumerate(tok.word_counts.items()):
    if ii < 10:
        print(iterm)
    else:
        break

#---------------------------第四步 資料轉化為序列-----------------------------
## 使用sequence.pad_sequences()將每個序列調整為相同的長度
## 對每個詞編碼之後,每句新聞中的每個詞就可以用對應的編碼表示,即每條新聞可以轉變成一個向量了
train_seq = tok.texts_to_sequences(train_content)
val_seq = tok.texts_to_sequences(val_content)
test_seq = tok.texts_to_sequences(test_content)

## 將每個序列調整為相同的長度
train_seq_mat = sequence.pad_sequences(train_seq,maxlen=max_len)
val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
test_seq_mat = sequence.pad_sequences(test_seq,maxlen=max_len)
print("資料轉換序列")
print(train_seq_mat.shape)
print(val_seq_mat.shape)
print(test_seq_mat.shape)
print(train_seq_mat[:2])

#-------------------------------第五步 建立CNN模型--------------------------
## 類別為4個
num_labels = 4
inputs = Input(name='inputs',shape=[max_len], dtype='float64')
## 詞嵌入使用預訓練的詞向量
layer = Embedding(max_words+1, 128, input_length=max_len, trainable=False)(inputs)
## 卷積層和池化層(詞窗大小為3 128核)
cnn = Convolution1D(128, 3, padding='same', strides = 1, activation='relu')(layer)
cnn = MaxPool1D(pool_size=4)(cnn)
## Dropout防止過擬合
flat = Flatten()(cnn) 
drop = Dropout(0.3)(flat)
## 全連線層
main_output = Dense(num_labels, activation='softmax')(drop)
model = Model(inputs=inputs, outputs=main_output)

## 優化函式 評價指標
model.summary()
model.compile(loss="categorical_crossentropy",
              optimizer='adam',      # RMSprop()
              metrics=["accuracy"])

#-------------------------------第六步 模型訓練和預測--------------------------
## 先設定為train訓練 再設定為test測試
flag = "train"
if flag == "train":
    print("模型訓練")
    ## 模型訓練 當val-loss不再提升時停止訓練 0.0001
    model_fit = model.fit(train_seq_mat, train_y, batch_size=128, epochs=10,
                          validation_data=(val_seq_mat,val_y),
                          callbacks=[EarlyStopping(monitor='val_loss',min_delta=0.0001)]  
                         )
    ## 儲存模型
    model.save('my_model.h5')  
    del model  # deletes the existing model
    ## 計算時間
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)
    print(model_fit.history)
 
else:
    print("模型預測")
    ## 匯入已經訓練好的模型
    model = load_model('my_model.h5')
    ## 對測試集進行預測
    test_pre = model.predict(test_seq_mat)
    ## 評價預測效果,計算混淆矩陣
    confm = metrics.confusion_matrix(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1))
    print(confm)
 
    ## 混淆矩陣視覺化
    Labname = ["體育", "文化", "財經", "遊戲"]
    print(metrics.classification_report(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1)))
    plt.figure(figsize=(8,8))
    sns.heatmap(confm.T, square=True, annot=True,
                fmt='d', cbar=False, linewidths=.6,
                cmap="YlGnBu")
    plt.xlabel('True label',size = 14)
    plt.ylabel('Predicted label', size = 14)
    plt.xticks(np.arange(4)+0.5, Labname, size = 12)
    plt.yticks(np.arange(4)+0.5, Labname, size = 12)
    plt.savefig('result.png')
    plt.show()

    #----------------------------------第七 驗證演算法--------------------------
    ## 使用tok對驗證資料集重新預處理,並使用訓練好的模型進行預測
    val_seq = tok.texts_to_sequences(val_df.cutword)
    ## 將每個序列調整為相同的長度
    val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
    ## 對驗證集進行預測
    val_pre = model.predict(val_seq_mat)
    print(metrics.classification_report(np.argmax(val_y,axis=1),np.argmax(val_pre,axis=1)))
 
    ## 計算時間
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)

GPU執行如下圖所示。注意,如果您的電腦是CPU版本,只需要將上述程式碼第一部分註釋掉即可,後面LSTM部分使用GPU對應的庫函式。

訓練輸出模型如下圖所示:

訓練輸出結果如下:

模型訓練
Train on 40000 samples, validate on 20000 samples
Epoch 1/10
40000/40000 [==============================] - 15s 371us/step - loss: 1.1798 - acc: 0.4772 - val_loss: 0.9878 - val_acc: 0.5977
Epoch 2/10
40000/40000 [==============================] - 4s 93us/step - loss: 0.8681 - acc: 0.6612 - val_loss: 0.8167 - val_acc: 0.6746
Epoch 3/10
40000/40000 [==============================] - 4s 92us/step - loss: 0.7268 - acc: 0.7245 - val_loss: 0.7084 - val_acc: 0.7330
Epoch 4/10
40000/40000 [==============================] - 4s 93us/step - loss: 0.6369 - acc: 0.7643 - val_loss: 0.6462 - val_acc: 0.7617
Epoch 5/10
40000/40000 [==============================] - 4s 96us/step - loss: 0.5670 - acc: 0.7957 - val_loss: 0.5895 - val_acc: 0.7867
Epoch 6/10
40000/40000 [==============================] - 4s 92us/step - loss: 0.5074 - acc: 0.8226 - val_loss: 0.5530 - val_acc: 0.8018
Epoch 7/10
40000/40000 [==============================] - 4s 93us/step - loss: 0.4638 - acc: 0.8388 - val_loss: 0.5105 - val_acc: 0.8185
Epoch 8/10
40000/40000 [==============================] - 4s 93us/step - loss: 0.4241 - acc: 0.8545 - val_loss: 0.4836 - val_acc: 0.8304
Epoch 9/10
40000/40000 [==============================] - 4s 92us/step - loss: 0.3900 - acc: 0.8692 - val_loss: 0.4599 - val_acc: 0.8403
Epoch 10/10
40000/40000 [==============================] - 4s 93us/step - loss: 0.3657 - acc: 0.8761 - val_loss: 0.4472 - val_acc: 0.8457
Time used: 52.203992899999996

預測及驗證結果如下:

[[3928  472  264  336]
 [ 115 4529  121  235]
 [ 151  340 4279  230]
 [ 145  593  195 4067]]
             precision    recall  f1-score   support

          0       0.91      0.79      0.84      5000
          1       0.76      0.91      0.83      5000
          2       0.88      0.86      0.87      5000
          3       0.84      0.81      0.82      5000

avg / total       0.85      0.84      0.84     20000

             precision    recall  f1-score   support

          0       0.90      0.77      0.83      5000
          1       0.78      0.92      0.84      5000
          2       0.88      0.85      0.86      5000
          3       0.84      0.85      0.85      5000

avg / total       0.85      0.85      0.85     20000

四.TextCNN中文文字分類

1.原理介紹

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

卷積神經網路的核心思想是捕捉區域性特徵,對於文字來說,區域性特徵就是由若干單片語成的滑動視窗,類似於N-gram。卷積神經網路的優勢在於能夠自動地對N-gram特徵進行組合和篩選,獲得不同抽象層次的語義資訊。下圖是該論文中用於文字分類的卷積神經網路模型架構。

另一篇TextCNN比較經典的論文是《A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification》,其模型結果如下圖所示。主要用於文字分類任務的TextCNN結構描述,詳細解釋了TextCNN架構及詞向量矩陣是如何做卷積的。

假設我們有一些句子需要對其進行分類。句子中每個詞是由n維詞向量組成的,也就是說輸入矩陣大小為m*n,其中m為句子長度。CNN需要對輸入樣本進行卷積操作,對於文字資料,filter不再橫向滑動,僅僅是向下移動,有點類似於N-gram在提取詞與詞間的區域性相關性。

圖中共有三種步長策略,分別是2、3、4,每個步長都有兩個filter(實際訓練時filter數量會很多)。在不同詞窗上應用不同filter,最終得到6個卷積後的向量。然後對每一個向量進行最大化池化操作並拼接各個池化值,最終得到這個句子的特徵表示,將這個句子向量丟給分類器進行分類,最終完成整個文字分類流程。

最後真心推薦下面這些大佬關於TextCNN的介紹,尤其是CSDN的Asia-Lee大佬,很喜歡他的文章,真心棒!

2.程式碼實現

Keras實現文字分類的TextCNN程式碼如下:

  • Keras_TextCNN_cnews.py
# -*- coding: utf-8 -*-
"""
Created on 2021-03-19
@author: xiuzhang Eastmount CSDN
TextCNN Model
"""
import os
import time
import pickle
import pandas as pd
import numpy as np
from sklearn import metrics
import matplotlib.pyplot as plt
import seaborn as sns
import tensorflow as tf
from sklearn.preprocessing import LabelEncoder,OneHotEncoder
from keras.models import Model
from keras.layers import LSTM, Activation, Dense, Dropout, Input, Embedding
from keras.layers import Convolution1D, MaxPool1D, Flatten
from keras.preprocessing.text import Tokenizer
from keras.preprocessing import sequence
from keras.callbacks import EarlyStopping
from keras.models import load_model
from keras.models import Sequential
from keras.layers.merge import concatenate

## GPU處理 讀者如果是CPU註釋該部分程式碼即可
## 指定每個GPU程序中使用視訊記憶體的上限 0.9表示可以使用GPU 90%的資源進行訓練
os.environ["CUDA_DEVICES_ORDER"] = "PCI_BUS_IS"
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))

start = time.clock()

#----------------------------第一步 資料讀取----------------------------
## 讀取測資料集
train_df = pd.read_csv("news_dataset_train_fc.csv")
val_df = pd.read_csv("news_dataset_val_fc.csv")
test_df = pd.read_csv("news_dataset_test_fc.csv")

## 解決中文顯示問題
plt.rcParams['font.sans-serif'] = ['KaiTi']  #指定預設字型 SimHei黑體
plt.rcParams['axes.unicode_minus'] = False   #解決儲存影象是負號'

#--------------------------第二步 OneHotEncoder()編碼--------------------
## 對資料集的標籤資料進行編碼
train_y = train_df.label
val_y = val_df.label
test_y = test_df.label
print("Label:")
print(train_y[:10])

le = LabelEncoder()
train_y = le.fit_transform(train_y).reshape(-1,1)
val_y = le.transform(val_y).reshape(-1,1)
test_y = le.transform(test_y).reshape(-1,1)
print("LabelEncoder")
print(train_y[:10])
print(len(train_y))

## 對資料集的標籤資料進行one-hot編碼
ohe = OneHotEncoder()
train_y = ohe.fit_transform(train_y).toarray()
val_y = ohe.transform(val_y).toarray()
test_y = ohe.transform(test_y).toarray()
print("OneHotEncoder:")
print(train_y[:10])

#-----------------------第三步 使用Tokenizer對片語進行編碼--------------------
max_words = 6000
max_len = 600
tok = Tokenizer(num_words=max_words)  #最大詞語數為6000
print(train_df.cutword[:5])
print(type(train_df.cutword))

## 防止語料中存在數字str處理
train_content = [str(a) for a in train_df.cutword.tolist()]
val_content = [str(a) for a in val_df.cutword.tolist()]
test_content = [str(a) for a in test_df.cutword.tolist()]
tok.fit_on_texts(train_content)
print(tok)

## 儲存訓練好的Tokenizer和匯入
with open('tok.pickle', 'wb') as handle: #saving
    pickle.dump(tok, handle, protocol=pickle.HIGHEST_PROTOCOL)
with open('tok.pickle', 'rb') as handle: #loading
    tok = pickle.load(handle)

#---------------------------第四步 資料轉化為序列-----------------------------
train_seq = tok.texts_to_sequences(train_content)
val_seq = tok.texts_to_sequences(val_content)
test_seq = tok.texts_to_sequences(test_content)

## 將每個序列調整為相同的長度
train_seq_mat = sequence.pad_sequences(train_seq,maxlen=max_len)
val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
test_seq_mat = sequence.pad_sequences(test_seq,maxlen=max_len)
print("資料轉換序列")
print(train_seq_mat.shape)
print(val_seq_mat.shape)
print(test_seq_mat.shape)
print(train_seq_mat[:2])

#-------------------------------第五步 建立TextCNN模型--------------------------
## 類別為4個
num_labels = 4
inputs = Input(name='inputs',shape=[max_len], dtype='float64')
## 詞嵌入使用預訓練的詞向量
layer = Embedding(max_words+1, 256, input_length=max_len, trainable=False)(inputs)
## 詞窗大小分別為3,4,5
cnn1 = Convolution1D(256, 3, padding='same', strides = 1, activation='relu')(layer)
cnn1 = MaxPool1D(pool_size=4)(cnn1)
cnn2 = Convolution1D(256, 4, padding='same', strides = 1, activation='relu')(layer)
cnn2 = MaxPool1D(pool_size=4)(cnn2)
cnn3 = Convolution1D(256, 5, padding='same', strides = 1, activation='relu')(layer)
cnn3 = MaxPool1D(pool_size=4)(cnn3)

# 合併三個模型的輸出向量
cnn = concatenate([cnn1,cnn2,cnn3], axis=-1)
flat = Flatten()(cnn) 
drop = Dropout(0.2)(flat)
main_output = Dense(num_labels, activation='softmax')(drop)
model = Model(inputs=inputs, outputs=main_output)
model.summary()
model.compile(loss="categorical_crossentropy",
              optimizer='adam',      # RMSprop()
              metrics=["accuracy"])

#-------------------------------第六步 模型訓練和預測--------------------------
## 先設定為train訓練 再設定為test測試
flag = "train"
if flag == "train":
    print("模型訓練")
    ## 模型訓練 當val-loss不再提升時停止訓練 0.0001
    model_fit = model.fit(train_seq_mat, train_y, batch_size=128, epochs=10,
                          validation_data=(val_seq_mat,val_y),
                          callbacks=[EarlyStopping(monitor='val_loss',min_delta=0.0001)]  
                         )
    model.save('my_model.h5')  
    del model
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)
    print(model_fit.history)
 
else:
    print("模型預測")
    ## 匯入已經訓練好的模型
    model = load_model('my_model.h5')
    ## 對測試集進行預測
    test_pre = model.predict(test_seq_mat)
    ## 評價預測效果,計算混淆矩陣
    confm = metrics.confusion_matrix(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1))
    print(confm)
 
    ## 混淆矩陣視覺化
    Labname = ["體育", "文化", "財經", "遊戲"]
    print(metrics.classification_report(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1)))
    plt.figure(figsize=(8,8))
    sns.heatmap(confm.T, square=True, annot=True,
                fmt='d', cbar=False, linewidths=.6,
                cmap="YlGnBu")
    plt.xlabel('True label',size = 14)
    plt.ylabel('Predicted label', size = 14)
    plt.xticks(np.arange(4)+0.5, Labname, size = 12)
    plt.yticks(np.arange(4)+0.5, Labname, size = 12)
    plt.savefig('result.png')
    plt.show()

    #----------------------------------第七 驗證演算法--------------------------
    ## 使用tok對驗證資料集重新預處理,並使用訓練好的模型進行預測
    val_seq = tok.texts_to_sequences(val_df.cutword)
    ## 將每個序列調整為相同的長度
    val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
    ## 對驗證集進行預測
    val_pre = model.predict(val_seq_mat)
    print(metrics.classification_report(np.argmax(val_y,axis=1),np.argmax(val_pre,axis=1)))
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)

訓練模型如下所示:

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
inputs (InputLayer)             (None, 600)          0                                            
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, 600, 256)     1536256     inputs[0][0]                     
__________________________________________________________________________________________________
conv1d_1 (Conv1D)               (None, 600, 256)     196864      embedding_1[0][0]                
__________________________________________________________________________________________________
conv1d_2 (Conv1D)               (None, 600, 256)     262400      embedding_1[0][0]                
__________________________________________________________________________________________________
conv1d_3 (Conv1D)               (None, 600, 256)     327936      embedding_1[0][0]                
__________________________________________________________________________________________________
max_pooling1d_1 (MaxPooling1D)  (None, 150, 256)     0           conv1d_1[0][0]                   
__________________________________________________________________________________________________
max_pooling1d_2 (MaxPooling1D)  (None, 150, 256)     0           conv1d_2[0][0]                   
__________________________________________________________________________________________________
max_pooling1d_3 (MaxPooling1D)  (None, 150, 256)     0           conv1d_3[0][0]                   
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 150, 768)     0           max_pooling1d_1[0][0]            
                                                                 max_pooling1d_2[0][0]            
                                                                 max_pooling1d_3[0][0]            
__________________________________________________________________________________________________
flatten_1 (Flatten)             (None, 115200)       0           concatenate_1[0][0]              
__________________________________________________________________________________________________
dropout_1 (Dropout)             (None, 115200)       0           flatten_1[0][0]                  
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 4)            460804      dropout_1[0][0]                  
==================================================================================================
Total params: 2,784,260
Trainable params: 1,248,004
Non-trainable params: 1,536,256
__________________________________________________________________________________________________

預測結果如下:

[[4448  238  182  132]
 [ 151 4572  124  153]
 [ 185  176 4545   94]
 [ 181  394  207 4218]]
             precision    recall  f1-score   support

          0       0.90      0.89      0.89      5000
          1       0.85      0.91      0.88      5000
          2       0.90      0.91      0.90      5000
          3       0.92      0.84      0.88      5000

avg / total       0.89      0.89      0.89     20000

             precision    recall  f1-score   support

          0       0.90      0.88      0.89      5000
          1       0.86      0.93      0.89      5000
          2       0.91      0.89      0.90      5000
          3       0.92      0.88      0.90      5000

avg / total       0.90      0.90      0.90     20000

五.LSTM中文文字分類

1.原理介紹

Long Short Term 網路(LSTM) 是一種RNN(Recurrent Neural Network)特殊的型別,可以學習長期依賴資訊。LSTM 由Hochreiter & Schmidhuber (1997)提出,並在近期被Alex Graves進行了改良和推廣。在很多問題,LSTM都取得相當巨大的成功,並得到了廣泛的使用。

由於RNN存在梯度消失的問題,人們對於序列索引位置t的隱藏結構做了改進,通過一些技巧讓隱藏結構複雜起來,來避免梯度消失的問題,這樣的特殊RNN就是我們的LSTM。LSTM的全稱是Long Short-Term Memory。LSTM由於其設計的特點,非常適合用於對時序資料的建模,如文字資料。LSTM的結構如下圖:

LSTM 通過刻意的設計來避免長期依賴問題。記住長期的資訊在實踐中是 LSTM 的預設行為,而非需要付出很大代價才能獲得的能力。LSTM是在普通的RNN上面做了一些改進,LSTM RNN多了三個控制器,即:

  • 輸入控制器
  • 輸出控制器
  • 忘記控制器

左邊多了個條主線,例如電影的主線劇情,而原本的RNN體系變成了分線劇情,並且三個控制器都在分線上。

  • 輸入控制器(write gate): 在輸入input時設定一個gate,gate的作用是判斷要不要寫入這個input到我們的記憶體Memory中,它相當於一個引數,也是可以被訓練的,這個引數就是用來控制要不要記住當下這個點。
  • 輸出控制器(read gate): 在輸出位置的gate,判斷要不要讀取現在的Memory。
  • 忘記控制器(forget gate): 處理位置的忘記控制器,判斷要不要忘記之前的Memory。

LSTM工作原理為:如果分支內容對於最終結果十分重要,輸入控制器會將這個分支內容按重要程度寫入主線內容,再進行分析;如果分線內容改變了我們之前的想法,那麼忘記控制器會將某些主線內容忘記,然後按比例替換新內容,所以主線內容的更新就取決於輸入和忘記控制;最後的輸出會基於主線內容和分線內容。通過這三個gate能夠很好地控制我們的RNN,基於這些控制機制,LSTM是延緩記憶的良藥,從而帶來更好的結果。

2.程式碼實現

Keras實現文字分類的LSTM程式碼如下:

  • Keras_LSTM_cnews.py
"""
Created on 2021-03-19
@author: xiuzhang Eastmount CSDN
LSTM Model
"""
import os
import time
import pickle
import pandas as pd
import numpy as np
from sklearn import metrics
import matplotlib.pyplot as plt
import seaborn as sns
import tensorflow as tf
from sklearn.preprocessing import LabelEncoder,OneHotEncoder
from keras.models import Model
from keras.layers import LSTM, Activation, Dense, Dropout, Input, Embedding
from keras.layers import Convolution1D, MaxPool1D, Flatten
from keras.preprocessing.text import Tokenizer
from keras.preprocessing import sequence
from keras.callbacks import EarlyStopping
from keras.models import load_model
from keras.models import Sequential

#GPU加速 CuDNNLSTM比LSTM快
from keras.layers import CuDNNLSTM, CuDNNGRU

## GPU處理 讀者如果是CPU註釋該部分程式碼即可
## 指定每個GPU程序中使用視訊記憶體的上限 0.9表示可以使用GPU 90%的資源進行訓練
os.environ["CUDA_DEVICES_ORDER"] = "PCI_BUS_IS"
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))

start = time.clock()

#----------------------------第一步 資料讀取----------------------------
## 讀取測資料集
train_df = pd.read_csv("news_dataset_train_fc.csv")
val_df = pd.read_csv("news_dataset_val_fc.csv")
test_df = pd.read_csv("news_dataset_test_fc.csv")
print(train_df.head())

## 解決中文顯示問題
plt.rcParams['font.sans-serif'] = ['KaiTi']  #指定預設字型 SimHei黑體
plt.rcParams['axes.unicode_minus'] = False   #解決儲存影象是負號'

#--------------------------第二步 OneHotEncoder()編碼--------------------
## 對資料集的標籤資料進行編碼
train_y = train_df.label
val_y = val_df.label
test_y = test_df.label
print("Label:")
print(train_y[:10])

le = LabelEncoder()
train_y = le.fit_transform(train_y).reshape(-1,1)
val_y = le.transform(val_y).reshape(-1,1)
test_y = le.transform(test_y).reshape(-1,1)
print("LabelEncoder")
print(train_y[:10])
print(len(train_y))

## 對資料集的標籤資料進行one-hot編碼
ohe = OneHotEncoder()
train_y = ohe.fit_transform(train_y).toarray()
val_y = ohe.transform(val_y).toarray()
test_y = ohe.transform(test_y).toarray()
print("OneHotEncoder:")
print(train_y[:10])

#-----------------------第三步 使用Tokenizer對片語進行編碼--------------------
max_words = 6000
max_len = 600
tok = Tokenizer(num_words=max_words)  #最大詞語數為6000
print(train_df.cutword[:5])
print(type(train_df.cutword))

## 防止語料中存在數字str處理
train_content = [str(a) for a in train_df.cutword.tolist()]
val_content = [str(a) for a in val_df.cutword.tolist()]
test_content = [str(a) for a in test_df.cutword.tolist()]
tok.fit_on_texts(train_content)
print(tok)

## 儲存訓練好的Tokenizer和匯入
with open('tok.pickle', 'wb') as handle: #saving
    pickle.dump(tok, handle, protocol=pickle.HIGHEST_PROTOCOL)
with open('tok.pickle', 'rb') as handle: #loading
    tok = pickle.load(handle)

#---------------------------第四步 資料轉化為序列-----------------------------
train_seq = tok.texts_to_sequences(train_content)
val_seq = tok.texts_to_sequences(val_content)
test_seq = tok.texts_to_sequences(test_content)

## 將每個序列調整為相同的長度
train_seq_mat = sequence.pad_sequences(train_seq,maxlen=max_len)
val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
test_seq_mat = sequence.pad_sequences(test_seq,maxlen=max_len)
print("資料轉換序列")
print(train_seq_mat.shape)
print(val_seq_mat.shape)
print(test_seq_mat.shape)
print(train_seq_mat[:2])

#-------------------------------第五步 建立LSTM模型--------------------------
## 定義LSTM模型
inputs = Input(name='inputs',shape=[max_len],dtype='float64')
## Embedding(詞彙表大小,batch大小,每個新聞的詞長)
layer = Embedding(max_words+1, 128, input_length=max_len)(inputs)
#layer = LSTM(128)(layer)
layer = CuDNNLSTM(128)(layer)

layer = Dense(128, activation="relu", name="FC1")(layer)
layer = Dropout(0.1)(layer)
layer = Dense(4, activation="softmax", name="FC2")(layer)
model = Model(inputs=inputs, outputs=layer)
model.summary()
model.compile(loss="categorical_crossentropy",
              optimizer='adam',  # RMSprop()
              metrics=["accuracy"])

#-------------------------------第六步 模型訓練和預測--------------------------
## 先設定為train訓練 再設定為test測試
flag = "train"
if flag == "train":
    print("模型訓練")
    ## 模型訓練 當val-loss不再提升時停止訓練 0.0001
    model_fit = model.fit(train_seq_mat, train_y, batch_size=128, epochs=10,
                          validation_data=(val_seq_mat,val_y),
                          callbacks=[EarlyStopping(monitor='val_loss',min_delta=0.0001)]  
                         )
    model.save('my_model.h5')  
    del model
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)
    print(model_fit.history)
 
else:
    print("模型預測")
    ## 匯入已經訓練好的模型
    model = load_model('my_model.h5')
    ## 對測試集進行預測
    test_pre = model.predict(test_seq_mat)
    ## 評價預測效果,計算混淆矩陣
    confm = metrics.confusion_matrix(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1))
    print(confm)
 
    ## 混淆矩陣視覺化
    Labname = ["體育", "文化", "財經", "遊戲"]
    print(metrics.classification_report(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1)))
    plt.figure(figsize=(8,8))
    sns.heatmap(confm.T, square=True, annot=True,
                fmt='d', cbar=False, linewidths=.6,
                cmap="YlGnBu")
    plt.xlabel('True label',size = 14)
    plt.ylabel('Predicted label', size = 14)
    plt.xticks(np.arange(4)+0.8, Labname, size = 12)
    plt.yticks(np.arange(4)+0.4, Labname, size = 12)
    plt.savefig('result.png')
    plt.show()

    #----------------------------------第七 驗證演算法--------------------------
    ## 使用tok對驗證資料集重新預處理,並使用訓練好的模型進行預測
    val_seq = tok.texts_to_sequences(val_df.cutword)
    ## 將每個序列調整為相同的長度
    val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
    ## 對驗證集進行預測
    val_pre = model.predict(val_seq_mat)
    print(metrics.classification_report(np.argmax(val_y,axis=1),np.argmax(val_pre,axis=1)))
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)

訓練輸出模型如下所示:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
inputs (InputLayer)          (None, 600)               0         
_________________________________________________________________
embedding_1 (Embedding)      (None, 600, 128)          768128    
_________________________________________________________________
cu_dnnlstm_1 (CuDNNLSTM)     (None, 128)               132096    
_________________________________________________________________
FC1 (Dense)                  (None, 128)               16512     
_________________________________________________________________
dropout_1 (Dropout)          (None, 128)               0         
_________________________________________________________________
FC2 (Dense)                  (None, 4)                 516       
=================================================================
Total params: 917,252
Trainable params: 917,252
Non-trainable params: 0

預測結果如下所示:

[[4539  153  188  120]
 [  47 4628  181  144]
 [ 113  133 4697   57]
 [ 101  292  157 4450]]
             precision    recall  f1-score   support

          0       0.95      0.91      0.93      5000
          1       0.89      0.93      0.91      5000
          2       0.90      0.94      0.92      5000
          3       0.93      0.89      0.91      5000

avg / total       0.92      0.92      0.92     20000

             precision    recall  f1-score   support

          0       0.96      0.89      0.92      5000
          1       0.89      0.94      0.92      5000
          2       0.90      0.93      0.92      5000
          3       0.94      0.92      0.93      5000

avg / total       0.92      0.92      0.92     20000

六.BiLSTM中文文字分類

1.原理介紹

BiLSTM是Bi-directional Long Short-Term Memory的縮寫,是由前向LSTM與後向LSTM組合而成。它和LSTM在自然語言處理任務中都常被用來建模上下文資訊。前向的LSTM與後向的LSTM結合成BiLSTM。比如,我們對“我愛中國”這句話進行編碼,模型如圖所示。

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

2.程式碼實現

Keras實現文字分類的BiLSTM程式碼如下:

  • Keras_BiLSTM_cnews.py
"""
Created on 2021-03-19
@author: xiuzhang Eastmount CSDN
BiLSTM Model
"""
import os
import time
import pickle
import pandas as pd
import numpy as np
from sklearn import metrics
import matplotlib.pyplot as plt
import seaborn as sns
import tensorflow as tf
from sklearn.preprocessing import LabelEncoder,OneHotEncoder
from keras.models import Model
from keras.layers import LSTM, Activation, Dense, Dropout, Input, Embedding
from keras.layers import Convolution1D, MaxPool1D, Flatten
from keras.preprocessing.text import Tokenizer
from keras.preprocessing import sequence
from keras.callbacks import EarlyStopping
from keras.models import load_model
from keras.models import Sequential

#GPU加速 CuDNNLSTM比LSTM快
from keras.layers import CuDNNLSTM, CuDNNGRU
from keras.layers import Bidirectional

## GPU處理 讀者如果是CPU註釋該部分程式碼即可
## 指定每個GPU程序中使用視訊記憶體的上限 0.9表示可以使用GPU 90%的資源進行訓練
os.environ["CUDA_DEVICES_ORDER"] = "PCI_BUS_IS"
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))

start = time.clock()

#----------------------------第一步 資料讀取----------------------------
## 讀取測資料集
train_df = pd.read_csv("news_dataset_train_fc.csv")
val_df = pd.read_csv("news_dataset_val_fc.csv")
test_df = pd.read_csv("news_dataset_test_fc.csv")
print(train_df.head())

## 解決中文顯示問題
plt.rcParams['font.sans-serif'] = ['KaiTi']  #指定預設字型 SimHei黑體
plt.rcParams['axes.unicode_minus'] = False   #解決儲存影象是負號'

#--------------------------第二步 OneHotEncoder()編碼--------------------
## 對資料集的標籤資料進行編碼
train_y = train_df.label
val_y = val_df.label
test_y = test_df.label
print("Label:")
print(train_y[:10])

le = LabelEncoder()
train_y = le.fit_transform(train_y).reshape(-1,1)
val_y = le.transform(val_y).reshape(-1,1)
test_y = le.transform(test_y).reshape(-1,1)
print("LabelEncoder")
print(train_y[:10])
print(len(train_y))

## 對資料集的標籤資料進行one-hot編碼
ohe = OneHotEncoder()
train_y = ohe.fit_transform(train_y).toarray()
val_y = ohe.transform(val_y).toarray()
test_y = ohe.transform(test_y).toarray()
print("OneHotEncoder:")
print(train_y[:10])

#-----------------------第三步 使用Tokenizer對片語進行編碼--------------------
max_words = 6000
max_len = 600
tok = Tokenizer(num_words=max_words)  #最大詞語數為6000
print(train_df.cutword[:5])
print(type(train_df.cutword))

## 防止語料中存在數字str處理
train_content = [str(a) for a in train_df.cutword.tolist()]
val_content = [str(a) for a in val_df.cutword.tolist()]
test_content = [str(a) for a in test_df.cutword.tolist()]
tok.fit_on_texts(train_content)
print(tok)

## 儲存訓練好的Tokenizer和匯入
with open('tok.pickle', 'wb') as handle: #saving
    pickle.dump(tok, handle, protocol=pickle.HIGHEST_PROTOCOL)
with open('tok.pickle', 'rb') as handle: #loading
    tok = pickle.load(handle)

#---------------------------第四步 資料轉化為序列-----------------------------
train_seq = tok.texts_to_sequences(train_content)
val_seq = tok.texts_to_sequences(val_content)
test_seq = tok.texts_to_sequences(test_content)

## 將每個序列調整為相同的長度
train_seq_mat = sequence.pad_sequences(train_seq,maxlen=max_len)
val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
test_seq_mat = sequence.pad_sequences(test_seq,maxlen=max_len)
print("資料轉換序列")
print(train_seq_mat.shape)
print(val_seq_mat.shape)
print(test_seq_mat.shape)
print(train_seq_mat[:2])

#-------------------------------第五步 建立BiLSTM模型--------------------------
num_labels = 4
model = Sequential()
model.add(Embedding(max_words+1, 128, input_length=max_len))
model.add(Bidirectional(CuDNNLSTM(128)))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(num_labels, activation='softmax'))
model.summary()
model.compile(loss="categorical_crossentropy",
              optimizer='adam',  # RMSprop()
              metrics=["accuracy"])

#-------------------------------第六步 模型訓練和預測--------------------------
## 先設定為train訓練 再設定為test測試
flag = "train"
if flag == "train":
    print("模型訓練")
    ## 模型訓練 當val-loss不再提升時停止訓練 0.0001
    model_fit = model.fit(train_seq_mat, train_y, batch_size=128, epochs=10,
                          validation_data=(val_seq_mat,val_y),
                          callbacks=[EarlyStopping(monitor='val_loss',min_delta=0.0001)]  
                         )
    model.save('my_model.h5')  
    del model
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)
    print(model_fit.history)
 
else:
    print("模型預測")
    ## 匯入已經訓練好的模型
    model = load_model('my_model.h5')
    ## 對測試集進行預測
    test_pre = model.predict(test_seq_mat)
    ## 評價預測效果,計算混淆矩陣
    confm = metrics.confusion_matrix(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1))
    print(confm)
 
    ## 混淆矩陣視覺化
    Labname = ["體育", "文化", "財經", "遊戲"]
    print(metrics.classification_report(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1)))
    plt.figure(figsize=(8,8))
    sns.heatmap(confm.T, square=True, annot=True,
                fmt='d', cbar=False, linewidths=.6,
                cmap="YlGnBu")
    plt.xlabel('True label',size = 14)
    plt.ylabel('Predicted label', size = 14)
    plt.xticks(np.arange(4)+0.5, Labname, size = 12)
    plt.yticks(np.arange(4)+0.5, Labname, size = 12)
    plt.savefig('result.png')
    plt.show()

    #----------------------------------第七 驗證演算法--------------------------
    ## 使用tok對驗證資料集重新預處理,並使用訓練好的模型進行預測
    val_seq = tok.texts_to_sequences(val_df.cutword)
    ## 將每個序列調整為相同的長度
    val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
    ## 對驗證集進行預測
    val_pre = model.predict(val_seq_mat)
    print(metrics.classification_report(np.argmax(val_y,axis=1),np.argmax(val_pre,axis=1)))
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)

訓練輸出模型如下所示,GPU時間還是非常快。

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 600, 128)          768128    
_________________________________________________________________
bidirectional_1 (Bidirection (None, 256)               264192    
_________________________________________________________________
dense_1 (Dense)              (None, 128)               32896     
_________________________________________________________________
dropout_1 (Dropout)          (None, 128)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 4)                 516       
=================================================================
Total params: 1,065,732
Trainable params: 1,065,732
Non-trainable params: 0

Train on 40000 samples, validate on 20000 samples
Epoch 1/10
40000/40000 [==============================] - 23s 587us/step - loss: 0.5825 - acc: 0.8038 - val_loss: 0.2321 - val_acc: 0.9246
Epoch 2/10
40000/40000 [==============================] - 21s 521us/step - loss: 0.1433 - acc: 0.9542 - val_loss: 0.2422 - val_acc: 0.9228
Time used: 52.763230400000005

預測結果如下圖所示:

[[4593  143  113  151]
 [  81 4679   60  180]
 [ 110  199 4590  101]
 [  73  254   82 4591]]
             precision    recall  f1-score   support

          0       0.95      0.92      0.93      5000
          1       0.89      0.94      0.91      5000
          2       0.95      0.92      0.93      5000
          3       0.91      0.92      0.92      5000

avg / total       0.92      0.92      0.92     20000

             precision    recall  f1-score   support

          0       0.94      0.90      0.92      5000
          1       0.89      0.95      0.92      5000
          2       0.95      0.90      0.93      5000
          3       0.91      0.94      0.93      5000

avg / total       0.92      0.92      0.92     20000

七.BiLSTM+Attention中文文字分類

1.原理介紹

Attention機制是模仿人類注意力而提出的一種解決問題的辦法,簡單地說就是從大量資訊中快速篩選出高價值資訊。主要用於解決LSTM/RNN模型輸入序列較長的時候很難獲得最終合理的向量表示問題,做法是保留LSTM的中間結果,用新的模型對其進行學習,並將其與輸出進行關聯,從而達到資訊篩選的目的。

What is attention?

先簡單描述一下attention機制是什麼。相信做NLP的同學對這個機制不會很陌生,它在論文 《Attention is all you need》 中可以說是大放異彩,在machine translation任務中,幫助深度模型在效能上有了很大的提升,輸出了當時最好的state-of-art model。當然該模型除了attention機制外,還用了很多有用的trick,以幫助提升模型效能。但是不能否認的時,這個模型的核心就是attention。

attention機制又稱為注意力機制,顧名思義,是一種能讓模型對重要資訊重點關注並充分學習吸收的技術,它不算是一個完整的模型,應當是一種技術,能夠作用於任何序列模型中。

Why attention?

為什麼要引入attention機制。比如在seq2seq模型中,對於一段文字序列,我們通常要使用某種機制對該序列進行編碼,通過降維等方式將其encode成一個固定長度的向量,用於輸入到後面的全連線層。一般我們會使用CNN或者RNN(包括GRU或者LSTM)等模型來對序列資料進行編碼,然後採用各種pooling或者對RNN直接取最後一個t時刻的hidden state作為句子的向量輸出。

但這裡會有一個問題: 常規的編碼方法,無法體現對一個句子序列中不同語素的關注程度,在自然語言中,一個句子中的不同部分是有不同含義和重要性的,比如上面的例子中:I hate this movie.如果做情感分析,明顯對hate這個詞語應當關注更多。當然是用CNN和RNN能夠編碼這種資訊,但這種編碼能力也是有上限的,對於較長的文字,模型效果不會再提升太多。

Attention的應用領域非常廣泛,文字、圖片等都有應用。

  • 文字:應用於seq2seq模型,最常見的應用是翻譯
  • 圖片:應用於卷積神經網路的圖片提取
  • 語音

下圖是一個比較經典的BiLSTM+Attention模型,也是我們接下來需要建立的模型。

2.程式碼實現

Keras實現文字分類的BiLSTM+Attention程式碼如下:

  • Keras_Attention_BiLSTM_cnews.py
"""
Created on 2021-03-19
@author: xiuzhang Eastmount CSDN
BiLSTM+Attention Model
"""
import os
import time
import pickle
import pandas as pd
import numpy as np
from sklearn import metrics
import matplotlib.pyplot as plt
import seaborn as sns
import tensorflow as tf
from sklearn.preprocessing import LabelEncoder,OneHotEncoder
from keras.models import Model
from keras.layers import LSTM, Activation, Dense, Dropout, Input, Embedding
from keras.layers import Convolution1D, MaxPool1D, Flatten
from keras.preprocessing.text import Tokenizer
from keras.preprocessing import sequence
from keras.callbacks import EarlyStopping
from keras.models import load_model
from keras.models import Sequential

#GPU加速 CuDNNLSTM比LSTM快
from keras.layers import CuDNNLSTM, CuDNNGRU
from keras.layers import Bidirectional

## GPU處理 讀者如果是CPU註釋該部分程式碼即可
## 指定每個GPU程序中使用視訊記憶體的上限 0.9表示可以使用GPU 90%的資源進行訓練
os.environ["CUDA_DEVICES_ORDER"] = "PCI_BUS_IS"
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))

start = time.clock()

#----------------------------第一步 資料讀取----------------------------
## 讀取測資料集
train_df = pd.read_csv("news_dataset_train_fc.csv")
val_df = pd.read_csv("news_dataset_val_fc.csv")
test_df = pd.read_csv("news_dataset_test_fc.csv")
print(train_df.head())

## 解決中文顯示問題
plt.rcParams['font.sans-serif'] = ['KaiTi']  #指定預設字型 SimHei黑體
plt.rcParams['axes.unicode_minus'] = False   #解決儲存影象是負號'

#--------------------------第二步 OneHotEncoder()編碼--------------------
## 對資料集的標籤資料進行編碼
train_y = train_df.label
val_y = val_df.label
test_y = test_df.label
print("Label:")
print(train_y[:10])

le = LabelEncoder()
train_y = le.fit_transform(train_y).reshape(-1,1)
val_y = le.transform(val_y).reshape(-1,1)
test_y = le.transform(test_y).reshape(-1,1)
print("LabelEncoder")
print(train_y[:10])
print(len(train_y))

## 對資料集的標籤資料進行one-hot編碼
ohe = OneHotEncoder()
train_y = ohe.fit_transform(train_y).toarray()
val_y = ohe.transform(val_y).toarray()
test_y = ohe.transform(test_y).toarray()
print("OneHotEncoder:")
print(train_y[:10])

#-----------------------第三步 使用Tokenizer對片語進行編碼--------------------
max_words = 6000
max_len = 600
tok = Tokenizer(num_words=max_words)  #最大詞語數為6000
print(train_df.cutword[:5])
print(type(train_df.cutword))

## 防止語料中存在數字str處理
train_content = [str(a) for a in train_df.cutword.tolist()]
val_content = [str(a) for a in val_df.cutword.tolist()]
test_content = [str(a) for a in test_df.cutword.tolist()]
tok.fit_on_texts(train_content)
print(tok)

## 儲存訓練好的Tokenizer和匯入
with open('tok.pickle', 'wb') as handle: #saving
    pickle.dump(tok, handle, protocol=pickle.HIGHEST_PROTOCOL)
with open('tok.pickle', 'rb') as handle: #loading
    tok = pickle.load(handle)

#---------------------------第四步 資料轉化為序列-----------------------------
train_seq = tok.texts_to_sequences(train_content)
val_seq = tok.texts_to_sequences(val_content)
test_seq = tok.texts_to_sequences(test_content)

## 將每個序列調整為相同的長度
train_seq_mat = sequence.pad_sequences(train_seq,maxlen=max_len)
val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
test_seq_mat = sequence.pad_sequences(test_seq,maxlen=max_len)
print("資料轉換序列")
print(train_seq_mat.shape)
print(val_seq_mat.shape)
print(test_seq_mat.shape)
print(train_seq_mat[:2])

#---------------------------第五步 建立Attention機制----------------------
"""
由於Keras目前還沒有現成的Attention層可以直接使用,我們需要自己來構建一個新的層函式。
Keras自定義的函式主要分為四個部分,分別是:
init:初始化一些需要的引數
bulid:具體來定義權重是怎麼樣的
call:核心部分,定義向量是如何進行運算的
compute_output_shape:定義該層輸出的大小
推薦文章:
    https://blog.csdn.net/huanghaocs/article/details/95752379
    https://zhuanlan.zhihu.com/p/29201491
"""

# Hierarchical Model with Attention
from keras import initializers
from keras import constraints
from keras import activations
from keras import regularizers
from keras import backend as K
from keras.engine.topology import Layer

K.clear_session()

class AttentionLayer(Layer):
    def __init__(self, attention_size=None, **kwargs):
        self.attention_size = attention_size
        super(AttentionLayer, self).__init__(**kwargs)
 
    def get_config(self):
        config = super().get_config()
        config['attention_size'] = self.attention_size
        return config
 
    def build(self, input_shape):
        assert len(input_shape) == 3

        self.time_steps = input_shape[1]
        hidden_size = input_shape[2]
        if self.attention_size is None:
            self.attention_size = hidden_size
 
        self.W = self.add_weight(name='att_weight', shape=(hidden_size, self.attention_size),
                                initializer='uniform', trainable=True)
        self.b = self.add_weight(name='att_bias', shape=(self.attention_size,),
                                initializer='uniform', trainable=True)
        self.V = self.add_weight(name='att_var', shape=(self.attention_size,),
                                initializer='uniform', trainable=True)
        super(AttentionLayer, self).build(input_shape)
 
    def call(self, inputs):
        self.V = K.reshape(self.V, (-1, 1))
        H = K.tanh(K.dot(inputs, self.W) + self.b)
        score = K.softmax(K.dot(H, self.V), axis=1)
        outputs = K.sum(score * inputs, axis=1)
        return outputs
 
    def compute_output_shape(self, input_shape):
        return input_shape[0], input_shape[2]

#-------------------------------第六步 建立BiLSTM模型--------------------------
## 定義BiLSTM模型
## BiLSTM+Attention
num_labels = 4
inputs = Input(name='inputs',shape=[max_len],dtype='float64')
layer = Embedding(max_words+1, 256, input_length=max_len)(inputs)
#lstm = Bidirectional(LSTM(100, dropout=0.2, recurrent_dropout=0.1, return_sequences=True))(layer)
bilstm = Bidirectional(CuDNNLSTM(128, return_sequences=True))(layer)  #引數保持維度3
layer = Dense(128, activation='relu')(bilstm)
layer = Dropout(0.2)(layer)
## 注意力機制
attention = AttentionLayer(attention_size=50)(layer)
output = Dense(num_labels, activation='softmax')(attention)
model = Model(inputs=inputs, outputs=output)
model.summary()
model.compile(loss="categorical_crossentropy",
              optimizer='adam',      # RMSprop()
              metrics=["accuracy"])

#-------------------------------第七步 模型訓練和預測--------------------------
## 先設定為train訓練 再設定為test測試
flag = "test"
if flag == "train":
    print("模型訓練")
    ## 模型訓練 當val-loss不再提升時停止訓練 0.0001
    model_fit = model.fit(train_seq_mat, train_y, batch_size=128, epochs=10,
                          validation_data=(val_seq_mat,val_y),
                          callbacks=[EarlyStopping(monitor='val_loss',min_delta=0.0001)]  
                         )
    ## 儲存模型
    model.save('my_model.h5')  
    del model  # deletes the existing model
    ## 計算時間
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)
    print(model_fit.history)
 
else:
    print("模型預測")
    ## 匯入已經訓練好的模型
    model = load_model('my_model.h5', custom_objects={'AttentionLayer': AttentionLayer(50)}, compile=False)
    ## 對測試集進行預測
    test_pre = model.predict(test_seq_mat)
    ## 評價預測效果,計算混淆矩陣
    confm = metrics.confusion_matrix(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1))
    print(confm)
 
    ## 混淆矩陣視覺化
    Labname = ["體育", "文化", "財經", "遊戲"]
    print(metrics.classification_report(np.argmax(test_y,axis=1),np.argmax(test_pre,axis=1)))
    plt.figure(figsize=(8,8))
    sns.heatmap(confm.T, square=True, annot=True,
                fmt='d', cbar=False, linewidths=.6,
                cmap="YlGnBu")
    plt.xlabel('True label',size = 14)
    plt.ylabel('Predicted label', size = 14)
    plt.xticks(np.arange(4)+0.5, Labname, size = 12)
    plt.yticks(np.arange(4)+0.5, Labname, size = 12)
    plt.savefig('result.png')
    plt.show()

    #----------------------------------第七 驗證演算法--------------------------
    ## 使用tok對驗證資料集重新預處理,並使用訓練好的模型進行預測
    val_seq = tok.texts_to_sequences(val_df.cutword)
    ## 將每個序列調整為相同的長度
    val_seq_mat = sequence.pad_sequences(val_seq,maxlen=max_len)
    ## 對驗證集進行預測
    val_pre = model.predict(val_seq_mat)
    print(metrics.classification_report(np.argmax(val_y,axis=1),np.argmax(val_pre,axis=1)))
 
    ## 計算時間
    elapsed = (time.clock() - start)
    print("Time used:", elapsed)

訓練輸出模型如下所示:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
inputs (InputLayer)          (None, 600)               0         
_________________________________________________________________
embedding_1 (Embedding)      (None, 600, 256)          1536256   
_________________________________________________________________
bidirectional_1 (Bidirection (None, 600, 256)          395264    
_________________________________________________________________
dense_1 (Dense)              (None, 600, 128)          32896     
_________________________________________________________________
dropout_1 (Dropout)          (None, 600, 128)          0         
_________________________________________________________________
attention_layer_1 (Attention (None, 128)               6500      
_________________________________________________________________
dense_2 (Dense)              (None, 4)                 516       
=================================================================
Total params: 1,971,432
Trainable params: 1,971,432
Non-trainable params: 0

預測結果如下圖所示:

[[4625  138  100  137]
 [  63 4692   77  168]
 [ 129  190 4589   92]
 [  82  299   78 4541]]
             precision    recall  f1-score   support

          0       0.94      0.93      0.93      5000
          1       0.88      0.94      0.91      5000
          2       0.95      0.92      0.93      5000
          3       0.92      0.91      0.91      5000

avg / total       0.92      0.92      0.92     20000

             precision    recall  f1-score   support

          0       0.95      0.91      0.93      5000
          1       0.88      0.95      0.91      5000
          2       0.95      0.90      0.92      5000
          3       0.92      0.93      0.93      5000

avg / total       0.92      0.92      0.92     20000

 

點選關注,第一時間瞭解華為雲新鮮技術~