Keras實現CNN文本分類

本文以CAIL司法挑戰賽的數據爲例,敘述利用Keras框架進行文本分類的一般流程及基本的深度學習模型。
步驟 1:文本的預處理,分詞->去除停用詞->統計選擇top n的詞做爲特徵詞
步驟 2:爲每個特徵詞生成ID
步驟 3:將文本轉化成ID序列,並將左側補齊
步驟 4:訓練集shuffle
步驟 5:Embedding Layer 將詞轉化爲詞向量
步驟 6:添加模型
步驟 7:訓練模型
步驟 8:得到準確率
(如果使用TFIDF而非詞向量進行文檔表示,則直接分詞去停後生成TFIDF矩陣後輸入模型)

二、文本預處理

2.1 數據集說明

本文的數據集來自CAIL2018挑戰賽,數據集是來自“中國裁判文書網”公開的刑事法律文書,其中每份數據由法律文書中的案情描述和事實部分組成,同時也包括每個案件所涉及的法條、被告人被判的罪名和刑期長短等要素。
數據集共包括268萬刑法法律文書,共涉及202條罪名,183條法條,刑期長短包括0-25年、無期、死刑。
數據利用json格式儲存,每一行爲一條數據,每條數據均爲一個字典。

  • fact: 事實描述
  • meta: 標註信息,標註信息中包括:
    • criminals: 被告(數據中均只含一個被告)
    • punish_of_money: 罰款(單位:元)
    • accusation: 罪名
    • relevant_articles: 相關法條
    • term_of_imprisonment: 刑期
      刑期格式(單位:月)
      • death_penalty: 是否死刑
      • life_imprisonment: 是否無期
      • imprisonment: 有期徒刑刑期

比賽有三個任務,
任務一(罪名預測):根據刑事法律文書中的案情描述和事實部分,預測被告人被判的罪名;
任務二(法條推薦):根據刑事法律文書中的案情描述和事實部分,預測本案涉及的相關法條;
任務三(刑期預測):根據刑事法律文書中的案情描述和事實部分,預測被告人的刑期長短。

2.2 讀取數據集

將json中的文本和標籤讀取到list中,每個list的元素爲一條文本/標籤。

def read_train_data(path):
    print('reading train data...')
    fin = open(path, 'r', encoding='utf8')

    alltext = []

    accu_label = []
    law_label = []
    time_label = []

    line = fin.readline()
    while line:
        d = json.loads(line)
        alltext.append(d['fact'])
        accu_label.append(get_label(d, 'accu'))
        law_label.append(get_label(d, 'law'))
        time_label.append(get_label(d, 'time'))
        line = fin.readline()
    fin.close()

    return alltext, accu_label, law_label, time_label
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

然後對文本進行分詞,因爲後續要表示成詞向量,是否去停意義不大,所以沒有去停。分詞後可以將分詞後的文本每一條爲一行存爲txt,以免以後每次運行程序都要重新分詞。

def cut_text(alltext):
    print('cut text...')
    count = 0
    cut = thulac.thulac(seg_only=True)
    train_text = []
    for text in alltext:
        count += 1
        if count % 2000 == 0:
            print(count)
        train_text.append(cut.cut(text, text=True)) #分詞結果以空格間隔,每個fact一個字符串
    print(len(train_text))

    print(train_text)
    fileObject = codecs.open("./cuttext_all_large.txt", "w", "utf-8")  #必須指定utf-8否則word2vec報錯
    for ip in train_text:
        fileObject.write(ip)
        fileObject.write('\n')
    fileObject.close()
    print('cut text over')
    return train_text
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

用分詞後的文本文檔訓練word2vec詞向量模型並保存,這裏使用了默認的size=100,即每個詞由100維向量表示。

def word2vec_train():
    print("start generate word2vec model...")
    sentences = word2vec.Text8Corpus("cuttext_all_large.txt")
    model = word2vec.Word2Vec(sentences)         #默認size=100 ,100維
    model.save('./predictor/model/word2vec')
    print('finished and saved!')
    return model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.3 使用Tokenizer將法律文書轉換成數字特徵

從txt中讀取分好詞的文本,轉換成詞袋序列。同樣,tokenizer對象生成過程較慢,也可以通過pickle保存下來,以便下次訓練或者測試時使用,具體tokenizer的用法及作用可以參見前文:Keras入門簡介
最後得到一個文本矩陣sequences,每一行爲一個用詞編號序列表示的文本,有多少個文本就有多少列。

    train_data = []
    with open('./cuttext_all_large.txt') as f:
        train_data = f.read().splitlines()
    print(len(train_data))

    # 轉換成詞袋序列
    maxlen = 1500
    # 詞袋模型的最大特徵束
    max_features = 20000

    # 設置分詞最大個數 即詞袋的單詞個數
    # with open('./predictor/model/tokenizer.pickle', 'rb') as f:
    #   tokenizer = pickle.load(f)
    tokenizer = Tokenizer(num_words=max_features, lower=True)  # 建立一個max_features個詞的字典
    tokenizer.fit_on_texts(train_data)  # 使用一系列文檔來生成token詞典,參數爲list類,每個元素爲一個文檔。可以將輸入的文本中的每個詞編號,編號是根據詞頻的,詞頻越大,編號越小。
    global word_index
    word_index = tokenizer.word_index      # 長度爲508242
    # with open('./predictor/model/tokenizer_large.pickle', 'wb') as handle:
    #   pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)
    # print("tokenizer has been saved.")
    # self.tokenizer.fit_on_texts(train_data)  # 使用一系列文檔來生成token詞典,參數爲list類,每個元素爲一個文檔。可以將輸入的文本中的每個詞編號,編號是根據詞頻的,詞頻越大,編號越小。

    sequences = tokenizer.texts_to_sequences(
        train_data)  # 對每個詞編碼之後,每個文本中的每個詞就可以用對應的編碼表示,即每條文本已經轉變成一個向量了 將多個文檔轉換爲word下標的向量形式,shape爲[len(texts),len(text)] -- (文檔數,每條文檔的長度)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

2.4 讓每句數字影評長度相同

x = sequence.pad_sequences(sequences, maxlen)  # 將每條文本的長度設置一個固定值。
  • 1

2.5 使用Embedding層將每個詞編碼轉換爲詞向量

調用Keras的Embedding層,該層只能作爲模型的第一層,將每個詞編碼轉換爲詞向量。
下面是最簡單的形式,詞向量隨機初始化。max_features即每條文本取多少個單詞表示,embedding_dims即每個單詞由多少維向量表示。表示完後即得到一個三維向量。shape爲(max_features)x(embedding_dims)x len(texts)(文檔數)

Embedding(max_features, embedding_dims)
  • 1

如果用預訓練的word2vec詞向量進行初始化,則需要先把訓練好的模型轉化爲矩陣的形式,

    model = gensim.models.Word2Vec.load('./predictor/model/word2vec')
    word2idx = {"_PAD": 0}  # 初始化 `[word : token]` 字典,後期 tokenize 語料庫就是用該詞典。
    vocab_list = [(k, model.wv[k]) for k, v in model.wv.vocab.items()]
    # 存儲所有 word2vec 中所有向量的數組,留意其中多一位,詞向量全爲 0, 用於 padding
    embeddings_matrix = np.zeros((len(model.wv.vocab.items()) + 1, model.vector_size))
    print('Found %s word vectors.' % len(model.wv.vocab.items()))
    for i in range(len(vocab_list)):
        word = vocab_list[i][0]
        word2idx[word] = i + 1
        embeddings_matrix[i + 1] = vocab_list[i][1]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

再令矩陣爲Embedding的weight:

Embedding(len(embeddings_matrix),       #表示文本數據中詞彙的取值可能數,從語料庫之中保留多少個單詞。 因爲Keras需要預留一個全零層, 所以+1
    embedding_dims,       # 嵌入單詞的向量空間的大小。它爲每個單詞定義了這個層的輸出向量的大小
    weights=[embeddings_matrix], #構建一個[num_words, EMBEDDING_DIM]的矩陣,然後遍歷word_index,將word在W2V模型之中對應vector複製過來。換個方式說:embedding_matrix 是原始W2V的子集,排列順序按照Tokenizer在fit之後的詞順序。作爲權重餵給Embedding Layer
    input_length=maxlen,     # 輸入序列的長度,也就是一次輸入帶有的詞彙個數
    trainable=False        # 我們設置 trainable = False,代表詞向量不作爲參數進行更新
                        )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

三、CNN模型搭建

CNN除了處理圖像數據之外,還適用於文本分類。CNN模型首次使用在文本分類,是Yoon Kim發表的“Convolutional Neural Networks for Sentence Classification”論文中。
CNN的基本結構包括兩層,其一爲特徵提取層,每個神經元的輸入與前一層的局部接受域相連,並提取該局部的特徵。一旦該局部特徵被提取後,它與其它特徵間的位置關係也隨之確定下來;其二是特徵映射層,網絡的每個計算層由多個特徵映射組成,每個特徵映射是一個平面,平面上所有神經元的權值相等。特徵映射結構採用影響函數核小的sigmoid函數作爲卷積網絡的激活函數,使得特徵映射具有位移不變性。此外,由於一個映射面上的神經元共享權值,因而減少了網絡自由參數的個數。卷積神經網絡中的每一個卷積層都緊跟着一個用來求局部平均與二次提取的計算層,這種特有的兩次特徵提取結構減小了特徵分辨率。
本節主要使用一維卷積核的CNN進行文本分類(二維卷積主要用於圖像處理),keras使用序貫模型。

3.1 基礎版CNN

def baseline_model(y,max_features,embedding_dims,filters):
    kernel_size = 3

    model = Sequential()
    model.add(Embedding(max_features, embedding_dims))        # 使用Embedding層將每個詞編碼轉換爲詞向量
    model.add(Conv1D(filters,
                     kernel_size,
                     padding='valid',
                     activation='relu',
                     strides=1))
    # 池化
    model.add(GlobalMaxPooling1D())

    model.add(Dense(y.shape[1], activation='softmax')) #第一個參數units: 全連接層輸出的維度,即下一層神經元的個數。
    model.add(Dropout(0.2))
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])

    model.summary()

    return model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.2 簡單版textCNN

這是省略掉多通道和微調的簡單版textCNN,用了四個卷積核:

def test_cnn(y,maxlen,max_features,embedding_dims,filters = 250):
    #Inputs
    seq = Input(shape=[maxlen],name='x_seq')

    #Embedding layers
    emb = Embedding(max_features,embedding_dims)(seq)

    # conv layers
    convs = []
    filter_sizes = [2,3,4,5]
    for fsz in filter_sizes:
        conv1 = Conv1D(filters,kernel_size=fsz,activation='tanh')(emb)
        pool1 = MaxPooling1D(maxlen-fsz+1)(conv1)
        pool1 = Flatten()(pool1)
        convs.append(pool1)
    merge = concatenate(convs,axis=1)

    out = Dropout(0.5)(merge)
    output = Dense(32,activation='relu')(out)

    output = Dense(units=y.shape[1],activation='sigmoid')(output)

    model = Model([seq],output)
    model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
    return model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

3.3 使用了word2vec詞向量的CNN:

def cnn_w2v(y,max_features,embedding_dims,filters,maxlen):
    # CNN參數
    kernel_size = 3

    model = gensim.models.Word2Vec.load('./predictor/model/word2vec')
    word2idx = {"_PAD": 0}  # 初始化 `[word : token]` 字典,後期 tokenize 語料庫就是用該詞典。
    vocab_list = [(k, model.wv[k]) for k, v in model.wv.vocab.items()]
    # 存儲所有 word2vec 中所有向量的數組,留意其中多一位,詞向量全爲 0, 用於 padding
    embeddings_matrix = np.zeros((len(model.wv.vocab.items()) + 1, model.vector_size))
    print('Found %s word vectors.' % len(model.wv.vocab.items()))
    for i in range(len(vocab_list)):
        word = vocab_list[i][0]
        word2idx[word] = i + 1
        embeddings_matrix[i + 1] = vocab_list[i][1]

    model = Sequential()
    # 使用Embedding層將每個詞編碼轉換爲詞向量
    model.add(Embedding(len(embeddings_matrix),       #表示文本數據中詞彙的取值可能數,從語料庫之中保留多少個單詞。 因爲Keras需要預留一個全零層, 所以+1
                                embedding_dims,       # 嵌入單詞的向量空間的大小。它爲每個單詞定義了這個層的輸出向量的大小
                                weights=[embeddings_matrix], #構建一個[num_words, EMBEDDING_DIM]的矩陣,然後遍歷word_index,將word在W2V模型之中對應vector複製過來。換個方式說:embedding_matrix 是原始W2V的子集,排列順序按照Tokenizer在fit之後的詞順序。作爲權重餵給Embedding Layer
                                input_length=maxlen,     # 輸入序列的長度,也就是一次輸入帶有的詞彙個數
                                trainable=False        # 我們設置 trainable = False,代表詞向量不作爲參數進行更新
                        ))
    model.add(Conv1D(filters,
                     kernel_size,
                     padding='valid',
                     activation='relu',
                     strides=1))
    # 池化
    model.add(GlobalMaxPooling1D())

    model.add(Dense(y.shape[1], activation='softmax')) #第一個參數units: 全連接層輸出的維度,即下一層神經元的個數。
    model.add(Dropout(0.2))
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])

    model.summary()

    return model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

四、模型訓練與測試

因爲是多分類問題,這部分主要是訓練前對標籤的one-hot處理和對訓練數據的打亂。
訓練時使用了early stopping。
最後保存模型。

def runcnn(x,label, label_name):
    y = np_utils.to_categorical(label) #多分類時,此方法將1,2,3,4,....這樣的分類轉化成one-hot 向量的形式,最終使用softmax做爲輸出
    print(x.shape,y.shape)
    indices = np.arange(len(x))
    lenofdata = len(x)
    np.random.shuffle(indices)
    x_train = x[indices][:int(lenofdata*0.8)]
    y_train = y[indices][:int(lenofdata*0.8)]
    x_test = x[indices][int(lenofdata*0.8):]
    y_test = y[indices][int(lenofdata*0.8):]

    model = baseline_model(y)
    keras.callbacks.EarlyStopping(
        monitor='val_loss',
        patience=0,
        verbose=0,
        mode='auto')
    print("training model")
    history = model.fit(x_train,y_train,validation_split=0.2,batch_size=64,epochs=10,verbose=2,shuffle=True)
    accy=history.history['acc']
    np_accy=np.array(accy)
    np.savetxt('save.txt',np_accy)

    print("pridicting...")
    scores = model.evaluate(x_test,y_test)
    print('test_loss:%f,accuracy: %f'%(scores[0],scores[1]))

    print("saving %s_textcnnmodel" % label_name)
    model.save('./predictor/model/%s_cnn_large.h5' % label_name)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

五、常見問題

5.1 如何利用Keras處理超過機器內存的數據集?

可以使用model.train_on_batch(X,y)model.test_on_batch(X,y)。或編寫一個每次產生一個batch樣本的生成器函數,並調用model.fit_generator(data_generator, samples_per_epoch, nb_epoch)進行訓練。

5.2 如何保存Keras模型?

官方文檔推薦使用model.save(filepath),將Keras模型和權重保存在一個HDF5文件中,該文件將包含:
模型的結構,以便重構該模型
模型的權重
訓練配置(損失函數,優化器等)
優化器的狀態,以便於從上次訓練中斷的地方開始
使用keras.models.load_model(filepath)來重新實例化你的模型,如果文件中存儲了訓練配置的話,該函數還會同時完成模型的編譯.

5.3 如何將Tokenizer對象保存到文件以進行評分?

很多比賽提交模型後用測試集進行評分,如果不保存Tokenizer對象,則需要在對每一個句子評分的時候都重新加載整個語料庫並生成Tokenizer對象。在網上找到的保存方法是使用pickle或joblib,使用pickle保存的代碼如下:

import pickle

# saving
with open('tokenizer.pickle', 'wb') as handle:
    pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)

# loading
with open('tokenizer.pickle', 'rb') as handle:
    tokenizer = pickle.load(handle)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

5.4 如何在多張GPU卡上使用Keras?

官方建議有多張GPU卡可用時,使用TnesorFlow後端。有兩種方法可以在多張GPU上運行一個模型:數據並行/設備並行
大多數情況下,你需要的很可能是“數據並行”數據並行
數據並行將目標模型在多個設備上各複製一份,並使用每個設備上的複製品處理整個數據集的不同部分數據。Keras在keras.utils.multi_gpu_model中提供有內置函數,該函數可以產生任意模型的數據並行版本,最高支持在8片GPU上並行。 請參考utils中的multi_gpu_model文檔。
設備並行
設備並行是在不同設備上運行同一個模型的不同部分,當模型含有多個並行結構,例如含有兩個分支時,這種方式很適合。

5.5 如何在執行程序時設置使用的GPU?

首先可以用nvidia-smi命令在服務器上查看GPU使用情況,如果要在python代碼中設置使用的GPU(如使用pycharm進行調試時),可以使用下面的代碼

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "1"
  • 1
  • 2

5.6 多分類問題應怎樣設置?

如出現下列錯誤,

ValueError: Error when checking target: expected dense_2 to have shape (None, 1) but got array with shape (123673, 202)

可能是多分類label設置的問題。
多分類問題的類別設置與單分類問題不同之處在於以下幾點:

  • 首先需要將類別通過y = np_utils.to_categorical(accu_label) 設置成one-hot的形式;
  • 然後最後一層輸出的unit個數應設置爲最後的分類個數,激活函數應選softmax而不應是sigmoid,即Dense(y.shape[1], activation='softmax')
  • 最後compile函數裏的loss參數,也要設置爲loss="categorical_crossentropy"

遇到的其他問題

問題1

File “/usr/local/lib/python3.5/dist-packages/keras/preprocessing/text.py”, line 267, in texts_to_sequences for vect in self.texts_to_sequences_generator(texts): File “/usr/local/lib/python3.5/dist-packages/keras/preprocessing/text.py”,
line 302, in texts_to_sequences_generator elif self.oov_token is not None: AttributeError: ‘Tokenizer’ object has no attribute ‘oov_token’

查看keras2.1.1版本的源碼發現texts_to_sequences_generator中沒有oov_token,手動設置tokenizer.oov_token = None來解決這個問題。
Pickle並不是序列化對象的可靠方法,因爲它假定您導入的底層Python代碼/模塊沒有改變。通常,不要使用與pickle時使用的庫版本不同的pickle對象。這不是Keras問題,而是一個通用的Python/Pickle問題。在這種情況下,有一個簡單的修復(設置屬性),但是在很多情況下不會。
參考:https://stackoverflow.com/questions/49861842/attributeerror-tokenizer-object-has-no-attribute-oov-token-in-keras

問題2

softmax() got an unexpected keyword argument ‘axis’

將keras升級到2.1.6之後TensorFlow和keras的版本不一致

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