1. 程式人生 > >tensorflow1: nn與cnn實現情感分類

tensorflow1: nn與cnn實現情感分類

0.資料集以及執行環境

資料集的地址:情緒分析的資料集,能稍微看懂英文就應該知道如何下載了

執行環境:Windows10,IDE:pycharm或者是Linux

0.資料預處理

"0","1467810369","Mon Apr 06 22:19:45 PDT 2009","NO_QUERY","_TheSpecialOne_","@switchfoot http://twitpic.com/2y1zl - Awww, that's a bummer.  You shoulda got David Carr of Third Day to do it. ;D"

上圖是原始資料的情況,總共有6個欄位,主要是第一個欄位(情感評價的結果)以及最後一個欄位(tweet內容)是有用的。針對csv檔案,我們使用pandas進行讀取然後進行處理。處理的程式碼如下:

# 提取檔案中的有用的欄位
def userfull_filed(org_file, outuput_file):
    data = pd.read_csv(os.path.join(data_dir, org_file), header=None, encoding='latin-1')
    clf = data.values[:, 0]
    content = data.values[:, -1]
    new_clf = []
    for temp in clf:
        # 這個處理就是將情感評論結果進行所謂的one_hot編碼
        if temp == 0:
            new_clf.append([1, 0]) # 消極評論
        # elif temp == 2:
        #     new_clf.append([0, 1, 0]) # 中性評論
        else:
            new_clf.append([0, 1]) # 積極評論

    df = pd.DataFrame(np.c_[new_clf, content], columns=['emotion0', 'emotion1', 'content'])
    df.to_csv(os.path.join(data_dir, outuput_file), index=False)

這樣處理的原因是,將情感評論值變成一個one-hot編碼形式,這樣我們進行nn或者是cnn處理的最後一層的輸出單元為2(但有個巨大的bug那就是實際上訓練集沒有中性評論)

接下來利用nltk(這個簡直是英語文字的自然語言的神奇呀)來生成單詞集合,程式碼如下:

def sentence_english_manage(line):
    # 英文句子的預處理
    pattern = re.compile(r"[!#$%&'()*+,-./:;<=>[email protected][\]^_`{|}~0123456789]")
    line = re.sub(pattern, '', line)
    # line = [word for word in line.split() if word not in stopwords]
    return line

def create_lexicon(train_file):
    lemmatizer = WordNetLemmatizer()
    df = pd.read_csv(os.path.join(data_dir, train_file))
    count_word = {} # 統計單詞的數量
    all_word = []
    for content in df.values[:, 2]:
        words = word_tokenize(sentence_english_manage(content.lower())) # word_tokenize就是一個分詞處理的過程
        for word in words:
            word = lemmatizer.lemmatize(word) # 提取該單詞的原型
            all_word.append(word) # 儲存所有的單詞

    count_word = Counter(all_word)
    # count_word = OrderetodDict(sorted(count_word.items(), key=lambda t: t[1]))
    lex = []
    for word in count_word.keys():
        if count_word[word] < 100000 and count_word[word] > 100: # 過濾掉一些單詞
            lex.append(word)

    with open('lexcion.pkl', 'wb') as file_write:
        pickle.dump(lex, file_write)

    return lex, count_word

最後生成一個只含有有用資訊的文字內容

1.利用NN進行文字情感分類

1.1神經網路結構的搭建

在這裡我設計了兩層的隱藏層,單元數分別為1500/1500。神經網路的整個結構程式碼如下:

with open('lexcion.pkl', 'rb') as file_read:
    lex = pickle.load(file_read)

n_input_layer = len(lex) # 輸入層的長度
n_layer_1 = 1500 # 有兩個隱藏層
n_layer_2 = 1500
n_output_layer = 2 # 輸出層的大小

X = tf.placeholder(shape=(None, len(lex)), dtype=tf.float32, name="X")
Y = tf.placeholder(shape=(None, 2), dtype=tf.float32, name="Y")
batch_size = 500
dropout_keep_prob = tf.placeholder(tf.float32)

def neural_network(data):
    layer_1_w_b = {
        'w_': tf.Variable(tf.random_normal([n_input_layer, n_layer_1])),
        'b_': tf.Variable(tf.random_normal([n_layer_1]))
    }
    layer_2_w_b = {
        'w_': tf.Variable(tf.random_normal([n_layer_1, n_layer_2])),
        'b_': tf.Variable(tf.random_normal([n_layer_2]))
    }
    layer_output_w_b = {
        'w_': tf.Variable(tf.random_normal([n_layer_2, n_output_layer])),
        'b_': tf.Variable(tf.random_normal([n_output_layer]))
    }
    # wx+b
    # 這裡有點需要注意那就是最後輸出層不需要加啟用函式
    # 同時加入了dropout引數
    full_conn_dropout_1 = tf.nn.dropout(data, dropout_keep_prob)
    layer_1 = tf.add(tf.matmul(full_conn_dropout_1, layer_1_w_b['w_']), layer_1_w_b['b_'])
    layer_1 = tf.nn.sigmoid(layer_1)
    full_conn_dropout_2 = tf.nn.dropout(layer_1, dropout_keep_prob)
    layer_2 = tf.add(tf.matmul(full_conn_dropout_2, layer_2_w_b['w_']), layer_2_w_b['b_'])
    layer_2 = tf.nn.sigmoid(layer_2)
    layer_output = tf.add(tf.matmul(layer_2, layer_output_w_b['w_']), layer_output_w_b['b_'])
    # layer_output = tf.nn.softmax(layer_output)

    return layer_output

比起我看的程式碼,這裡我加入了dropout的引數,使得訓練不會過擬合。但實際操作中,我發現加不加這個資料對於整個實驗的影響並沒有那麼大。

1.2獲取訓練與測試資料

原本的教程其實有一套自己的提取資料的過程,但我看了一眼感覺有些麻煩,我就自己寫了一個數據提取的方法,程式碼如下:

def get_random_n_lines(i, data, batch_size):
    # 從訓練集中找訓批量訓練的資料
    # 這裡的邏輯需要理解,同時我們要理解要從積極與消極的兩個集合中分層取樣
    if ((i * batch_size) % len(data) + batch_size) > len(data):
        rand_index = np.arange(start=((i*batch_size) % len(data)),
                               stop=len(data))
    else:
        rand_index = np.arange(start=((i*batch_size) % len(data)),
                               stop=((i*batch_size) % len(data) + batch_size))

    return data[rand_index, :]

def get_test_data(test_file):
    # 獲取測試集的資料用於測試
    lemmatizer = WordNetLemmatizer()
    df = pd.read_csv(os.path.join('data', test_file))
    # groups = df.groupby('emotion1')
    # group_neg_pos = groups.get_group(0).values # 獲取非中性評論的資訊
    group_neg_pos = df.values

    test_x = group_neg_pos[:, 2]
    test_y = group_neg_pos[:, 0:2]

    new_test_x = []
    for tweet in test_x:
        words = word_tokenize(tweet.lower())
        words = [lemmatizer.lemmatize(word) for word in words]
        features = np.zeros(len(lex))
        for word in words:
            if word in lex:
                features[lex.index(word)] = 1

        new_test_x.append(features)

    return new_test_x, test_y

上面是提取訓練集所需的程式碼,設計了一個提取一個batch_size大小的資料,因為我是分別從積極評論與消極評論中分別提取一定量的資料,所以要呼叫兩次該程式碼。

下面這個則是提取測試集的程式碼,借用了詞袋模型的思想,對一條tweet在字典長度的維度上進行一個編碼過程。其實訓練集也是這樣處理的,但我寫了兩次有點傻。

1.3訓練過程

借用別人部落格中的程式碼,我進行了細微的修改,主要調整了程式碼的部分位置,添加了tensorboard的內容以及模型儲存的內容,程式碼如下:

def train_neural_network():
    # 配置tensorboard
    tensorboard_dir = "tensorboard/nn"
    if not os.path.exists(tensorboard_dir):
        os.makedirs(tensorboard_dir)

    # 損失函式
    predict = neural_network(X)
    cost_func = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=predict, labels=Y))
    tf.summary.scalar("loss", cost_func)
    optimizer = tf.train.AdamOptimizer().minimize(cost_func)

    # 準確率
    correct = tf.equal(tf.argmax(predict, 1), tf.argmax(Y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct, 'float'))
    tf.summary.scalar("accuracy", accuracy)

    merged_summary = tf.summary.merge_all()
    writer = tf.summary.FileWriter(tensorboard_dir)

    df = pd.read_csv(os.path.join('data', 'new_train_data.csv'))
    # data = df.values

    group_by_emotion0 = df.groupby('emotion0')
    group_neg = group_by_emotion0.get_group(0).values
    group_pos = group_by_emotion0.get_group(1).values

    test_x, test_y = get_test_data('new_test_data.csv')

    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())

        writer.add_graph(sess.graph)

        lemmatizer = WordNetLemmatizer() # 判斷詞幹所用的
        saver = tf.train.Saver()

        i = 0
        # pre_acc = 0 # 儲存前一次的準確率以和後一次的進行比較

        while i < 5000:
            rand_neg_data = get_random_n_lines(i, group_neg, batch_size)
            rand_pos_data = get_random_n_lines(i, group_pos, batch_size)
            rand_data = np.vstack((rand_neg_data, rand_pos_data)) # 矩陣合併
            np.random.shuffle(rand_data) # 打亂順序

            batch_y = rand_data[:, 0:2] # 獲取得分情況
            batch_x = rand_data[:, 2] # 獲取內容資訊

            new_batch_x = []
            for tweet in batch_x:
                words = word_tokenize(tweet.lower())
                words = [lemmatizer.lemmatize(word) for word in words]

                features = np.zeros(len(lex))
                for word in words:
                    if word in lex:
                        features[lex.index(word)] = 1  # 一個句子中某個詞可能出現兩次,可以用+=1,其實區別不大
                new_batch_x.append(features)

            # batch_y = group_neg[:, 0: 3] + group_pos[:, 0: 3]

            loss, _, train_acc = sess.run([cost_func, optimizer, accuracy],
                                          feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.6})

            if i % 100 == 0:
                print("第{}次迭代,損失函式為{}, 訓練的準確率為{}".format(i, loss, train_acc))
                s = sess.run(merged_summary, feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.6})
                writer.add_summary(s, i)

            if i % 100 == 0:
                # print(sess.run(accuracy, feed_dict={X: new_batch_x, Y: batch_y}))
                test_acc = accuracy.eval({X: test_x[:200], Y: test_y[:200], dropout_keep_prob: 1.0})
                print('測試集的準確率:', test_acc)
            i += 1

        if not os.path.isdir('./checkpoint'):
            os.mkdir('./checkpoint')
        saver.save(sess, './checkpoint/model.ckpt')  # 儲存session

其實這個過程沒什麼可說的,基本是一個套路。

1.4執行結果

我是在實驗室的伺服器上跑的上述程式碼,跑了幾個小時,主要感覺還是模型的儲存的時間比較長。因為整個模型的引數還是相當大的。最後的結果在tensorboard上顯示結果為:


整體來說效果還可以。

2.利用CNN進行情感分類

這個專案實際上是一個CNN在NLP上的一個應用。一般而言,CNN是用來處理像影象、視訊等矩陣類的東西,而他是如何應用到自然語言的呢?我們可以引入詞嵌入矩陣這個概念,類似於Word2Vec這種東西,我們可以將一個詞語或是字元拓展為一個長度為K的特徵向量空間,這樣藉助詞袋模型,我們就可以將一個句子或是文件拓展成一個矩陣。這樣我們就可以引入卷積以及池化等方法來分析這些資料。

這裡分享一個大神的部落格,是專門針對CNN在NLP上的應用implementing-a-cnn-for-text-classification-in-tensorflow,通過這篇論文你可以充分了解如何利用tensorflow來實現分文分類以及cnn的一些解釋。情緒分析與這個是基本一致的方法,我們也可以從這篇部落格中詳細瞭解整個流程。

接下來我們開始講解CNN的情感分類的過程。從資料的預處理、CNN網路的構建、訓練這三個方面進行講解:

1.1神經網路的搭建:

這是一個重點的內容,我先引用一個圖片,然後使用程式碼慢慢解釋,圖片如下:


圖片有點大,順便附上程式碼:

with open('lexcion.pkl', 'rb') as file_read:
    lex = pickle.load(file_read)

input_size = len(lex) # 輸入的長度
num_classes = 2 # 分類的數量
batch_size = 64
seq_length = 100 # 一個tweet的固定長度

X = tf.placeholder(tf.int32, [None, seq_length])
Y = tf.placeholder(tf.float32, [None, num_classes])

dropout_keep_prob = tf.placeholder(tf.float32)
def neural_network():
    '''
    整個流程的解釋:
    輸入為一個X,shape=[None, 8057],8057為字典的長度
    首先利用embedding_lookup的方法,將X轉換為[None, 8057, 128]的向量,但有個疑惑就是emdeding_lookup的實際用法,在ceshi.py中有介紹
    接著expande_dims使結果變成[None, 8057, 128, 1]的向量,但這樣做的原因不是很清楚,原因就是通道數的設定

    然後進行卷積與池化:
    卷積核的大小有3種,每種卷積後的feature_map的數量為128
    卷積核的shape=[3/4/5, 128, 1, 128],其中前兩個為卷積核的長寬,最後一個為卷積核的數量,第三個就是通道數
    卷積的結果為[None, 8057-3+1, 1, 128],矩陣的寬度已經變為1了,這裡要注意下

    池化層的大小需要注意:shape=[1, 8055, 1, 1]這樣的化池化後的結果為[None, 1, 1, 128]
    以上就是一個典型的文字CNN的過程
    :return:
    '''

    ''' 進行修改採用短編碼 '''

    ''' tf.name_scope() 與 tf.variable_scope()的作用基本一致'''
    with tf.name_scope("embedding"):
        embedding_size = 64
        '''
        這裡出現了一個問題沒有註明上限與下限
        '''
        # embeding = tf.get_variable("embedding", [input_size, embedding_size]) # 詞嵌入矩陣
        embedding = tf.Variable(tf.random_uniform([input_size, embedding_size], -1.0, 1.0)) # 詞嵌入矩陣
        # with tf.Session() as sess:
        #     # sess.run(tf.initialize_all_variables())
        #     temp = sess.run(embedding)
        embedded_chars = tf.nn.embedding_lookup(embedding, X)
        embedded_chars_expanded = tf.expand_dims(embedded_chars, -1) # 設定通道數

    # 卷積與池化層
    num_filters = 256 # 卷積核的數量
    filter_sizes = [3, 4, 5] # 卷積核的大小
    pooled_outputs = []

    for i, filter_size in enumerate(filter_sizes):
        with tf.name_scope("conv_maxpool_{}".format(filter_size)):
            filter_shape = [filter_size, embedding_size, 1, num_filters] # 要注意下卷積核大小的設定
            W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1))
            b = tf.Variable(tf.constant(0.1, shape=[num_filters]))

            conv = tf.nn.conv2d(embedded_chars_expanded, W, strides=[1, 1, 1, 1], padding="VALID")
            h = tf.nn.relu(tf.nn.bias_add(conv, b)) # 煞筆忘了加這個偏置的加法

            pooled = tf.nn.max_pool(h, ksize=[1, seq_length - filter_size + 1, 1, 1],
                                    strides=[1, 1, 1, 1], padding='VALID')
            pooled_outputs.append(pooled)


    num_filters_total = num_filters * len(filter_sizes)
    '''
    # tensor t3 with shape [2, 3]
    # tensor t4 with shape [2, 3]
    tf.shape(tf.concat([t3, t4], 0))  # [4, 3]
    tf.shape(tf.concat([t3, t4], 1))  # [2, 6]
    '''
    h_pool = tf.concat(pooled_outputs, 3) # 原本是一個[None, 1, 1, 128]變成了[None, 1, 1, 384]
    h_pool_flat = tf.reshape(h_pool, [-1, num_filters_total]) # 拉平處理 [None, 384]

    # dropout
    with tf.name_scope("dropout"):
        h_drop = tf.nn.dropout(h_pool_flat, dropout_keep_prob)

    # output
    with tf.name_scope("output"):
        # 這裡就是最後的一個全連線層處理
        # from tensorflow.contrib.layers import xavier_initializer
        W = tf.get_variable("w", shape=[num_filters_total, num_classes],
                            initializer=tf.contrib.layers.xavier_initializer()) # 這個初始化要記住
        b = tf.Variable(tf.constant(0.1, shape=[num_classes]))

        output = tf.nn.xw_plus_b(h_drop, W, b)
        # output = tf.nn.relu(output)

    return output

在程式碼的最上面實際上比較詳細的介紹了整個流程,首先這個卷積過程和一般的卷積過程不一樣。卷積核是一個矩形的,其寬度與詞嵌入矩陣的維度是一樣的,他是在長度的方向進行卷積過程,實際含義就是尋找3個詞(或者是5個或者是4個)詞之間的特徵屬性,這樣卷積的結果如上圖所示成了一個1維的向量結果,然後我們進行一個max_pool操作,其他大小與卷積後生成的向量一樣,最後我們得到了一個個1*1大小的向量。我們利用concat方法將這些向量合併,最後接入一個全連線層,然後softmax之後我們就可以得到分類的結果。整體流程在程式碼中有些,而且所有的tensor大小也註明了。

這裡我需要說的是,一開始我使用的與NN一樣的tweet向量化的方法,即將tweet對映到一個字典長度的維度上,但實驗之後發現效果並不好,於是我再檢視資料是發現了另一種文字向量化的方法,就是規定每條tweet長度固定均為100,這是在統計後得出的一個基本結果(我統計過訓練集中tweet的長度一般在100以下,而且之後我還進行了資料的一個預處理,就是刪除標點符號與數字),這樣我們就可以將tweet對映到一個100維的向量上,這樣矩陣就比較稠密點,而且訓練的batch_size可以調的比較大,我們通常採用“多了截斷/少了補充”的策略。

1.2就是最後的訓練

在訓練階段,我順便學習了tensorboard以及模型儲存的方法,程式碼如下,基本上是一種常見的訓練格式:

def train_neural_netword():
    # 配置tensorboard
    tensorboard_dir = "tensorboard/cnn"
    if not os.path.exists(tensorboard_dir):
        os.makedirs(tensorboard_dir)

    output = neural_network()

    # 構建準確率的計算過程
    predictions = tf.argmax(output, 1)
    correct_predictions = tf.equal(predictions, tf.argmax(Y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float")) # 強制轉換
    tf.summary.scalar("accuracy", accuracy)

    # 構建損失函式的計算過程
    optimizer = tf.train.AdamOptimizer(0.001)
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=Y))
    tf.summary.scalar("loss", loss) # 將損失函式加入
    grads_and_vars = optimizer.compute_gradients(loss)
    train_op = optimizer.apply_gradients(grads_and_vars)

    # 將引數儲存如tensorboard中
    merged_summary = tf.summary.merge_all()
    writer = tf.summary.FileWriter(tensorboard_dir)

    # 構建模型的儲存模型
    saver = tf.train.Saver(tf.global_variables())

    # 資料集的獲取
    df = pd.read_csv(os.path.join('data', 'new_train_data.csv'))

    group_by_emotion0 = df.groupby('emotion0')
    group_neg = group_by_emotion0.get_group(0).values
    group_pos = group_by_emotion0.get_group(1).values

    test_x, test_y = get_test_data('new_test_data.csv')

    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())
        # sess.run(tf.global_variables_initializer())
        # 將影象加入tensorboard中
        writer.add_graph(sess.graph)

        lemmatizer = WordNetLemmatizer()
        i = 0
        pre_acc = 0
        while i < 10000:
            rand_neg_data = get_random_n_lines(i, group_neg, batch_size)
            rand_pos_data = get_random_n_lines(i, group_pos, batch_size)
            rand_data = np.vstack((rand_neg_data, rand_pos_data))
            np.random.shuffle(rand_data)

            batch_x = rand_data[:, 3]
            batch_y = rand_data[:, 0: 3]

            new_batch_x = []
            for tweet in batch_x:
                # 這段迴圈的意義就是將單詞提取詞幹,並將字元轉換成下標
                words = word_tokenize(tweet.lower())
                words = [lemmatizer.lemmatize(word) for word in words]

                features = np.zeros(len(lex))
                for word in words:
                    if word in lex:
                        features[lex.index(word)] = 1  # 一個句子中某個詞可能出現兩次,可以用+=1,其實區別不大
                new_batch_x.append(features)

            _, loss_ = sess.run([train_op, loss], feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.5})

            if i % 20 == 0:
                # 每二十次儲存一次tensorboard
                s = sess.run(merged_summary, feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.5})
                writer.add_summary(s, i)

            if i % 10 == 0:
                # 每10次打印出一個損失函式與準確率(這是指評測的準確率)
                print(loss_)
                accur = sess.run(accuracy, feed_dict={X: test_x, Y: test_y, dropout_keep_prob: 1.0})

                if accur > pre_acc:
                    # 當前的準確率高於之前的準確率,更新模型
                    pre_acc = accur
                    print("準確率:", pre_acc)
                    tf.summary.scalar("accur", accur)
                    saver.save(sess, "cnn_model/model.ckpt")
            i += 1

2.3執行結果

在tensorboard上視覺化的結果如下:



說實話最後的訓練效果並不好,我也不清楚是為什麼,希望知道的同學告訴我一聲吧。

3.後記

感謝CSDN使用者“MachineLP”,我的整個流程是在他的基礎上進行修改。實在是受益匪淺。

以上的程式碼會貼到我的github上(點選開啟連結)