前言

我們在第五章學習了循環神經網絡,在第五章中我們使用循環神經網絡實現了一個文本分類的模型,不過使用的數據集是PaddlePaddle自帶的一個數據集,我們並沒有瞭解到PaddlePaddle是如何使用讀取文本數據集的,那麼本章我們就來學習一下如何使用PaddlePaddle訓練自己的文本數據集。我們將會從中文文本數據集的製作開始介紹,一步步講解如何使用訓練一箇中文文本分類神經網絡模型。

GitHub地址:https://github.com/yeyupiaoling/LearnPaddle2/tree/master/note12

爬取文本數據集

網絡上一些高質量的中文文本分類數據集相當少,經過充分考慮之後,絕對自己從網絡中爬取自己的中文文本數據集。在GitHub中有一個開源的爬取今日頭條中文新聞標題的代碼,鏈接地址請查看最後的參考資料。我們在這個開源代碼上做一些簡單修改後,就使用他來爬取數據。

創建一個download_text_data.py文件,這個就是爬取數據集的程序。首先導入相應的依賴包。

import os
import random
import requests
import json
import time

然後設置新聞的分類列表,這些是我們將要爬取的新聞類別。第一個值是分類的標籤,第二個值是分類的中文名稱,第三個是網絡訪問的請求頭,通過值獲取相應類別的新聞。

# 分類新聞參數
news_classify = [
    [0, '民生', 'news_story'],
    [1, '文化', 'news_culture'],
    [2, '娛樂', 'news_entertainment'],
    [3, '體育', 'news_sports'],
    [4, '財經', 'news_finance'],
    [5, '房產', 'news_house'],
    [6, '汽車', 'news_car'],
    [7, '教育', 'news_edu'],
    [8, '科技', 'news_tech'],
    [9, '軍事', 'news_military'],
    [10, '旅遊', 'news_travel'],
    [11, '國際', 'news_world'],
    [12, '證券', 'stock'],
    [13, '農業', 'news_agriculture'],
    [14, '遊戲', 'news_game']
]

以下代碼片段是爬取數據的核心代碼。get_data函數的tup參數是上面定義的新聞類別,data_path參數是保存爬取的文本數據。爲了讓爬取的程序更像正常的網絡訪問,這裏還設置了一個訪問請求頭參數querystring和請求頭headers,然後通過requests.request進行網絡訪問,爬取新聞數據,並對其進行解析,最後把需要的數據保存到本地文件中。

# 已經下載的新聞標題的ID
downloaded_data_id = []
# 已經下載新聞標題的數量
downloaded_sum = 0


def get_data(tup, data_path):
    global downloaded_data_id
    global downloaded_sum
    print('============%s============' % tup[1])
    url = "http://it.snssdk.com/api/news/feed/v63/"
    # 分類新聞的訪問參數,模仿正常網絡訪問
    t = int(time.time() / 10000)
    t = random.randint(6 * t, 10 * t)
    querystring = {"category": tup[2], "max_behot_time": t, "last_refresh_sub_entrance_interval": "1524907088",
                   "loc_mode": "5",
                   "tt_from": "pre_load_more", "cp": "51a5ee4f38c50q1", "plugin_enable": "0", "iid": "31047425023",
                   "device_id": "51425358841", "ac": "wifi", "channel": "tengxun", "aid": "13",
                   "app_name": "news_article", "version_code": "631", "version_name": "6.3.1",
                   "device_platform": "android",
                   "ab_version": "333116,297979,317498,336556,295827,325046,239097,324283,170988,335432,332098,325198,336443,330632,297058,276203,286212,313219,328615,332041,329358,322321,327537,335710,333883,335102,334828,328670,324007,317077,334305,280773,335671,319960,333985,331719,336452,214069,31643,332881,333968,318434,207253,266310,321519,247847,281298,328218,335998,325618,333327,336199,323429,287591,288418,260650,326188,324614,335477,271178,326588,326524,326532",
                   "ab_client": "a1,c4,e1,f2,g2,f7", "ab_feature": "94563,102749", "abflag": "3", "ssmix": "a",
                   "device_type": "MuMu", "device_brand": "Android", "language": "zh", "os_api": "19",
                   "os_version": "4.4.4", "uuid": "008796762094657", "openudid": "b7215ea70ca32066",
                   "manifest_version_code": "631", "resolution": "1280*720", "dpi": "240",
                   "update_version_code": "6310", "_rticket": "1524907088018", "plugin": "256"}

    headers = {
        'cache-control': "no-cache",
        'postman-token': "26530547-e697-1e8b-fd82-7c6014b3ee86",
        'User-Agent': 'Dalvik/1.6.0 (Linux; U; Android 4.4.4; MuMu Build/V417IR) NewsArticle/6.3.1 okhttp/3.7.0.2'
    }

    # 進行網絡請求
    response = requests.request("GET", url, headers=headers, params=querystring)
    # 獲取返回的數據
    new_data = json.loads(response.text)
    with open(data_path, 'a', encoding='utf-8') as fp:
        for item in new_data['data']:
            item = item['content']
            item = item.replace('\"', '"')
            item = json.loads(item)
            # 判斷數據中是否包含id和新聞標題
            if 'item_id' in item.keys() and 'title' in item.keys():
                item_id = item['item_id']
                print(downloaded_sum, tup[0], tup[1], item['item_id'], item['title'])
                # 通過新聞id判斷是否已經下載過
                if item_id not in downloaded_data_id:
                    downloaded_data_id.append(item_id)
                    # 安裝固定格式追加寫入文件中
                    line = u"{}_!_{}_!_{}_!_{}".format(item['item_id'], tup[0], tup[1], item['title'])
                    line = line.replace('\n', '').replace('\r', '')
                    line = line + '\n'
                    fp.write(line)
                    downloaded_sum += 1

有時候爬取時間比較長,可能中途需要中斷。所以就需要以下的代碼進行處理,讀取已經保存的文本數據的文件中的數據ID,通過使用這個數據集,在爬取數據的時候就不再重複保存數據了。

def get_routine(data_path):
    global downloaded_sum
    # 從文件中讀取已經有的數據,避免數據重複
    if os.path.exists(data_path):
        with open(data_path, 'r', encoding='utf-8') as fp:
            lines = fp.readlines()
            downloaded_sum = len(lines)
            for line in lines:
                item_id = int(line.split('_!_')[0])
                downloaded_data_id.append(item_id)
            print('在文件中已經讀起了%d條數據' % downloaded_sum)
    else:
        os.makedirs(os.path.dirname(data_path))

    while 1:
        #  開始下載數據
        time.sleep(10)
        for classify in news_classify:
            get_data(classify, data_path)
        # 當下載量超過300000就停止下載
        if downloaded_sum >= 300000:
            break

最後在main入口中啓動爬取文本數據的函數。

if __name__ == '__main__':
    data_path = 'datasets/news_classify_data.txt'
    dict_path = "datasets/dict_txt.txt"
    # 下載數據集
    get_routine(data_path)

在爬取過程中,輸出信息:

============文化============
17 1 文化 6646565189942510093 世界第一豪宅,坐落於北京,一根柱子27個億,世界首富都買不起!
18 1 文化 6658382232383652104 俗語講:“男怕初一,女怕十五”,這話什麼意思?有道理嗎?
19 1 文化 6636596124998173192 浙江一員工請假條火了,內容令人狂笑不止,字跡卻讓人念念不忘
20 1 文化 6658848073562718734 難怪悟空被趕下山後菩提神祕消失,你看看方寸山門口對聯寫了啥?
21 1 文化 6658952207871771140 他把183件國寶無償捐給美國,捐回中國卻收了450萬美元

製作訓練數據

上面爬取的文本數據並不能直接拿來訓練,因爲PaddlePaddle訓練的數據不能是字符串的,所以需要對這些文本數據轉換成整型類型的數據。就是把一個字對應上唯一的數字,最後把全部的文字轉換成數字。

創建create_data.py文件。創建create_dict()函數,這個函數用來創建一個數據字典。數字字典就是把每個字都對應一個一個數字,包括標點符號。

import os

# 把下載得數據生成一個字典
def create_dict(data_path, dict_path):
    dict_set = set()
    # 讀取已經下載得數據
    with open(data_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
    # 把數據生成一個元組
    for line in lines:
        title = line.split('_!_')[-1].replace('\n', '')
        for s in title:
            dict_set.add(s)
    # 把元組轉換成字典,一個字對應一個數字
    dict_list = []
    i = 0
    for s in dict_set:
        dict_list.append([s, i])
        i += 1
    # 添加未知字符
    dict_txt = dict(dict_list)
    end_dict = {"<unk>": i}
    dict_txt.update(end_dict)
    # 把這些字典保存到本地中
    with open(dict_path, 'w', encoding='utf-8') as f:
        f.write(str(dict_txt))

    print("數據字典生成完成!")

生成的數據字典類型如下:

{'港': 712, '選': 367, '所': 0, '斯': 1,

創建一個數據自己之後,就使用這個數據字典把下載數據轉換成數字,還有標籤。

def create_data_list(data_root_path):
    with open(data_root_path + 'test_list.txt', 'w') as f:
        pass
    with open(data_root_path + 'train_list.txt', 'w') as f:
        pass

    with open(os.path.join(data_root_path, 'dict_txt.txt'), 'r', encoding='utf-8') as f_data:
        dict_txt = eval(f_data.readlines()[0])

    with open(os.path.join(data_root_path, 'news_classify_data.txt'), 'r', encoding='utf-8') as f_data:
        lines = f_data.readlines()
    i = 0
    for line in lines:
        title = line.split('_!_')[-1].replace('\n', '')
        l = line.split('_!_')[1]
        labs = ""
        if i % 10 == 0:
            with open(os.path.join(data_root_path, 'test_list.txt'), 'a', encoding='utf-8') as f_test:
                for s in title:
                    lab = str(dict_txt[s])
                    labs = labs + lab + ','
                labs = labs[:-1]
                labs = labs + '\t' + l + '\n'
                f_test.write(labs)
        else:
            with open(os.path.join(data_root_path, 'train_list.txt'), 'a', encoding='utf-8') as f_train:
                for s in title:
                    lab = str(dict_txt[s])
                    labs = labs + lab + ','
                labs = labs[:-1]
                labs = labs + '\t' + l + '\n'
                f_train.write(labs)
        i += 1
    print("數據列表生成完成!")

轉換後的數據如下:

321,364,535,897,322,263,354,337,441,815,943 12
540,299,884,1092,671,938    13

這裏順便增加獲取字典長度的函數,因爲在訓練的時候獲取神經網絡分類器的時候需要用到。

# 獲取字典的長度
def get_dict_len(dict_path):
    with open(dict_path, 'r', encoding='utf-8') as f:
        line = eval(f.readlines()[0])

    return len(line.keys())

最後執行創建數據字典和生成數據列表的函數就可以生成待訓練的數據了。

if __name__ == '__main__':
    # 把生產的數據列表都放在自己的總類別文件夾中
    data_root_path = "datasets/"
    data_path = os.path.join(data_root_path, 'news_classify_data.txt')
    dict_path = os.path.join(data_root_path, "dict_txt.txt")
    # 創建數據字典
    create_dict(data_path, dict_path)
    # 創建數據列表
    create_data_list(data_root_path)

在執行的過程中會輸出信息:

數據字典生成完成!
數據列表生成完成!

定義模型

然後我們定義一個文本分類模型,這裏使用的是雙向單層LSTM模型,據說百度的情感分析也是使用這個模型的。我們創建一個bilstm_net.py文件,用於定義雙向單層LSTM模型。

import paddle.fluid as fluid

def bilstm_net(data, dict_dim, class_dim, emb_dim=128, hid_dim=128, hid_dim2=96, emb_lr=30.0):
    # embedding layer
    emb = fluid.layers.embedding(input=data,
                                 size=[dict_dim, emb_dim],
                                 param_attr=fluid.ParamAttr(learning_rate=emb_lr))

    # bi-lstm layer
    fc0 = fluid.layers.fc(input=emb, size=hid_dim * 4)

    rfc0 = fluid.layers.fc(input=emb, size=hid_dim * 4)

    lstm_h, c = fluid.layers.dynamic_lstm(input=fc0, size=hid_dim * 4, is_reverse=False)

    rlstm_h, c = fluid.layers.dynamic_lstm(input=rfc0, size=hid_dim * 4, is_reverse=True)

    # extract last layer
    lstm_last = fluid.layers.sequence_last_step(input=lstm_h)
    rlstm_last = fluid.layers.sequence_last_step(input=rlstm_h)

    # concat layer
    lstm_concat = fluid.layers.concat(input=[lstm_last, rlstm_last], axis=1)

    # full connect layer
    fc1 = fluid.layers.fc(input=lstm_concat, size=hid_dim2, act='tanh')
    # softmax layer
    prediction = fluid.layers.fc(input=fc1, size=class_dim, act='softmax')
    return prediction

定義數據讀取

接下來我們定義text_reader.py文件,用於讀取文本數據集。這相對圖片讀取來說,這比較簡單。

首先導入相應的依賴包。

from multiprocessing import cpu_count
import numpy as np
import paddle

因爲在上一個程序已經把文本轉換成PaddlePaddle可讀數據,所以直接就可以在文件中讀取數據成了。

# 訓練數據的預處理
def train_mapper(sample):
    data, label = sample
    data = [int(data) for data in data.split(',')]
    return data, int(label)

# 訓練數據的reader
def train_reader(train_list_path):

    def reader():
        with open(train_list_path, 'r') as f:
            lines = f.readlines()
            # 打亂數據
            np.random.shuffle(lines)
            # 開始獲取每張圖像和標籤
            for line in lines:
                data, label = line.split('\t')
                yield data, label

    return paddle.reader.xmap_readers(train_mapper, reader, cpu_count(), 1024)

這裏跟訓練的讀取方式一樣,只是沒有一個打亂數據的操作。

# 測試數據的預處理
def test_mapper(sample):
    data, label = sample
    data = [int(data) for data in data.split(',')]
    return data, int(label)

# 測試數據的reader
def test_reader(test_list_path):

    def reader():
        with open(test_list_path, 'r') as f:
            lines = f.readlines()
            for line in lines:
                data, label = line.split('\t')
                yield data, label
    return paddle.reader.xmap_readers(test_mapper, reader, cpu_count(), 1024)

訓練模型

然後編寫train.py文件,開始訓練文本分類模型。首先到如相應的依賴包。

import os
import shutil
import paddle
import paddle.fluid as fluid
import create_data
import text_reader
import bilstm_net

定義網絡輸入層,數據是一條文本數據,所以只有一個維度。

# 定義輸入數據, lod_level不爲0指定輸入數據爲序列數據
words = fluid.layers.data(name='words', shape=[1], dtype='int64', lod_level=1)
label = fluid.layers.data(name='label', shape=[1], dtype='int64')

接着是獲取雙向單層LSTM模型的分類器,這裏需要用到文本數據集的字典大小,然後還需要分類器的大小,因爲我們的文本數據有15個類別,所以這裏分類器的大小是15。

# 獲取數據字典長度
dict_dim = create_data.get_dict_len('datasets/dict_txt.txt')
# 獲取長短期記憶網絡
model = bilstm_net.bilstm_net(words, dict_dim, 15)

然後是定義一系列的損失函數,準確率函數,克隆預測程序和優化方法。這裏使用的優化方法是Adagrad優化方法,Adagrad優化方法多用於處理稀疏數據。

# 獲取損失函數和準確率
cost = fluid.layers.cross_entropy(input=model, label=label)
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=model, label=label)

# 獲取預測程序
test_program = fluid.default_main_program().clone(for_test=True)

# 定義優化方法
optimizer = fluid.optimizer.AdagradOptimizer(learning_rate=0.002)
opt = optimizer.minimize(avg_cost)

# 創建一個執行器,CPU訓練速度比較慢
# place = fluid.CPUPlace()
place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
# 進行參數初始化
exe.run(fluid.default_startup_program())

這裏就是獲取我們在上一個文件中定義讀取數據的reader,根據不同的文本文件加載訓練和預測的數據,準備進行訓練。

# 獲取訓練和預測數據
train_reader = paddle.batch(reader=text_reader.train_reader('datasets/train_list.txt'), batch_size=128)
test_reader = paddle.batch(reader=text_reader.test_reader('datasets/test_list.txt'), batch_size=128)

最後在這裏進行訓練和測試,我們然執行器在訓練的過程中輸出訓練時的是損失值和準確率。然後每40個batch打印一次信息和執行一次測試操作,查看網絡模型在測試集中的準確率。

# 定義輸入數據的維度
feeder = fluid.DataFeeder(place=place, feed_list=[words, label])

# 開始訓練
for pass_id in range(10):
    # 進行訓練
    for batch_id, data in enumerate(train_reader()):
        train_cost, train_acc = exe.run(program=fluid.default_main_program(),
                             feed=feeder.feed(data),
                             fetch_list=[avg_cost, acc])

        if batch_id % 40 == 0:
            print('Pass:%d, Batch:%d, Cost:%0.5f, Acc:%0.5f' % (pass_id, batch_id, train_cost[0], train_acc[0]))
            # 進行測試
            test_costs = []
            test_accs = []
            for batch_id, data in enumerate(test_reader()):
                test_cost, test_acc = exe.run(program=test_program,
                                              feed=feeder.feed(data),
                                              fetch_list=[avg_cost, acc])
                test_costs.append(test_cost[0])
                test_accs.append(test_acc[0])
            # 計算平均預測損失在和準確率
            test_cost = (sum(test_costs) / len(test_costs))
            test_acc = (sum(test_accs) / len(test_accs))
            print('Test:%d, Cost:%0.5f, ACC:%0.5f' % (pass_id, test_cost, test_acc))

我可以在每pass訓練結束之後保存一次預測模型,可以用於之後的預測。

    # 保存預測模型
    save_path = 'infer_model/'
    # 刪除舊的模型文件
    shutil.rmtree(save_path, ignore_errors=True)
    # 創建保持模型文件目錄
    os.makedirs(save_path)
    # 保存預測模型
    fluid.io.save_inference_model(save_path, feeded_var_names=[words.name], target_vars=[model], executor=exe)

訓練輸出的信息:

Pass:0, Batch:0, Cost:2.70816, Acc:0.07812
Test:0, Cost:2.68423, ACC:0.14427
Pass:0, Batch:40, Cost:2.01647, Acc:0.34375
Test:0, Cost:1.99191, ACC:0.34301
Pass:0, Batch:80, Cost:1.61981, Acc:0.47656
Test:0, Cost:1.69227, ACC:0.46456
Pass:0, Batch:120, Cost:1.40459, Acc:0.57812
Test:0, Cost:1.47188, ACC:0.53961
Pass:0, Batch:160, Cost:1.15466, Acc:0.65625
Test:0, Cost:1.32585, ACC:0.59393
Pass:0, Batch:200, Cost:1.08597, Acc:0.67188
Test:0, Cost:1.20917, ACC:0.63793
Pass:0, Batch:240, Cost:1.08081, Acc:0.66406
Test:0, Cost:1.14794, ACC:0.66145

預測文本

在上面的訓練中,我們已經訓練到了一個文本分類預測模型。接下來我們就使用這個模型來預測我們想要預測文本。

創建infer.py文件開始進行預測,首先導入依賴包。

import numpy as np
import paddle.fluid as fluid

然後創建執行器,並加載預測模型文件,獲取到預測程序和輸入數據的名稱和網絡分類器。

# 創建執行器
place = fluid.CPUPlace()
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())

# 保存預測模型路徑
save_path = 'infer_model/'
# 從模型中獲取預測程序、輸入數據名稱列表、分類器
[infer_program, feeded_var_names, target_var] = fluid.io.load_inference_model(dirname=save_path, executor=exe)

因爲我們輸入的是文本數據,但是PaddlePaddle讀取的數據是整型數據,所以我們需要一個函數幫助我們把文本字符根據數據集的字典轉換成整型數據。

# 獲取數據
def get_data(sentence):
    # 讀取數據字典
    with open('datasets/dict_txt.txt', 'r', encoding='utf-8') as f_data:
        dict_txt = eval(f_data.readlines()[0])
    dict_txt = dict(dict_txt)
    # 把字符串數據轉換成列表數據
    keys = dict_txt.keys()
    data = []
    for s in sentence:
        # 判斷是否存在未知字符
        if not s in keys:
            s = '<unk>'
        data.append(int(dict_txt[s]))
    return data

然後在這裏獲取數據。

data = []
# 獲取圖片數據
data1 = get_data('京城最值得你來場文化之旅的博物館')
data2 = get_data('謝娜爲李浩菲澄清網絡謠言,之後她的兩個行爲給自己加分')
data.append(data1)
data.append(data2)

因爲輸入的不定長度的文本數據,所以我們需要根據不同的輸入數據的長度創建張量數據。

# 獲取每句話的單詞數量
base_shape = [[len(c) for c in data]]

# 生成預測數據
tensor_words = fluid.create_lod_tensor(data, base_shape, place)

最後執行預測程序,獲取預測結果。

# 執行預測
result = exe.run(program=infer_program,
                 feed={feeded_var_names[0]: tensor_words},
                 fetch_list=target_var)

獲取預測結果之後,獲取預測結果的最大概率的標籤,然後根據這個標籤獲取類別的名字。

# 分類名稱
names = ['民生', '文化', '娛樂', '體育', '財經',
         '房產', '汽車', '教育', '科技', '軍事',
         '旅遊', '國際', '證券', '農業', '遊戲']

# 獲取結果概率最大的label
for i in range(len(data)):
    lab = np.argsort(result)[0][i][-1]
    print('預測結果標籤爲:%d, 名稱爲:%s, 概率爲:%f' % (lab, names[lab], result[0][i][lab]))

預測輸出的信息:

預測結果標籤爲:10, 名稱爲:旅遊, 概率爲:0.848075
預測結果標籤爲:2, 名稱爲:娛樂, 概率爲:0.894570


上一章:《PaddlePaddle從入門到煉丹》十一——自定義圖像數據集識別
下一章:《PaddlePaddle從入門到煉丹》十三——自定義圖像數生成


參考資料

  1. https://github.com/fate233/toutiao-text-classfication-dataset
  2. https://github.com/baidu/Senta
小夜