用深度學習作命名實體識別(四)——模型訓練

經過本文你將瞭解如何訓練一我的名、地址、組織、公司、產品、時間,共6個實體的命名實體識別模型。html

準備訓練樣本

下面的連接中提供了已經用brat標註好的數據文件以及brat的配置文件,由於標註內容較多放到brat里加載會比較慢,因此拆分紅了10份,每份包括3000多條樣本數據,將這10份文件和相應的配置文件放到brat目錄/data/project路徑下,而後就能夠從瀏覽器訪問文件內容以及相應的標註狀況了。python

若是你還不知道什麼是brat,或還不清楚如何使用brat,強烈建議先閱讀前兩篇文章《用深度學習作命名實體識別(二):文本標註工具brat》、《用深度學習作命名實體識別(三):文本數據標註過程》。github

標註數據雖然有了,可是還不能知足咱們的訓練要求,由於咱們須要根據ann和txt,將其轉成訓練所需的數據格式,格式以下:
算法

能夠看到,每一行一個字符,字符後面跟上空格,而後跟上該字符的標註, 每一個樣本之間用空行分隔。
另外,也能夠看到這裏採用的是BIO的標註方式:json

  • B,即Begin,表示開始
  • I,即Intermediate,表示中間
  • O,即Other,表示其餘,用於標記無關字符

轉換代碼以下:api

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

"""
數據格式轉化
"""
import codecs
import os

__author__ = '程序員一一滌生'

tag_dic = {"時間": "TIME",
           "地點": "LOCATION",
           "人名": "PERSON_NAME",
           "組織名": "ORG_NAME",
           "公司名": "COMPANY_NAME",
           "產品名": "PRODUCT_NAME"}


# 轉換成可訓練的格式,最後以"END O"結尾
def from_ann2dic(r_ann_path, r_txt_path, w_path):
    q_dic = {}
    print("開始讀取文件:%s" % r_ann_path)
    with codecs.open(r_ann_path, "r", encoding="utf-8") as f:
        line = f.readline()
        line = line.strip("\n\r")
        while line != "":
            line_arr = line.split()
            print(line_arr)
            cls = tag_dic[line_arr[1]]
            start_index = int(line_arr[2])
            end_index = int(line_arr[3])
            length = end_index - start_index
            for r in range(length):
                if r == 0:
                    q_dic[start_index] = ("B-%s" % cls)
                else:
                    q_dic[start_index + r] = ("I-%s" % cls)
            line = f.readline()
            line = line.strip("\n\r")

    print("開始讀取文件:%s" % r_txt_path)
    with codecs.open(r_txt_path, "r", encoding="utf-8") as f:
        content_str = f.read()
        # content_str = content_str.replace("\n", "").replace("\r", "").replace("//////", "\n")
    print("開始寫入文本%s" % w_path)
    with codecs.open(w_path, "w", encoding="utf-8") as w:
        for i, str in enumerate(content_str):
            if str is " " or str == "" or str == "\n" or str == "\r":
                print("===============")
            elif str == "/":
                if i == len(content_str) - len("//////") + 1:  # 表示到達末尾
                    # w.write("\n")
                    break
                # 連續六個字符首尾都是/,則表示換一行
                elif content_str[i + len("//////") - 1] == "/" and content_str[i + len("//////") - 2] == "/" and \
                        content_str[i + len("//////") - 3] == "/" and content_str[i + len("//////") - 4] == "/" and \
                        content_str[i + len("//////") - 5] == "/":
                    w.write("\n")
                    i += len("//////")
            else:
                if i in q_dic:
                    tag = q_dic[i]
                else:
                    tag = "O"  # 大寫字母O
                w.write('%s %s\n' % (str, tag))
        w.write('%s\n' % "END O")


# 去除空行
def drop_null_row(r_path, w_path):
    q_list = []
    with codecs.open(r_path, "r", encoding="utf-8") as f:
        line = f.readline()
        line = line.strip("\n\r")
        while line != "END O":
            if line != "":
                q_list.append(line)
            line = f.readline()
            line = line.strip("\n\r")
    with codecs.open(w_path, "w", encoding="utf-8") as w:
        for i, line in enumerate(q_list):
            w.write('%s\n' % line)


# 生成train.txt、dev.txt、test.txt
# 除8,9-new.txt分別用於dev和test外,剩下的合併成train.txt
def rw0(data_root_dir, w_path):
    if os.path.exists(w_path):
        os.remove(w_path)
    for file in os.listdir(data_root_dir):
        path = os.path.join(data_root_dir, file)
        if file.endswith("8-new.txt"):
            # 重命名爲dev.txt
            os.rename(path, os.path.join(data_root_dir, "dev.txt"))
            continue
        if file.endswith("9-new.txt"):
            # 重命名爲test.txt
            os.rename(path, os.path.join(data_root_dir, "test.txt"))
            continue
        q_list = []
        print("開始讀取文件:%s" % file)
        with codecs.open(path, "r", encoding="utf-8") as f:
            line = f.readline()
            line = line.strip("\n\r")
            while line != "END O":
                q_list.append(line)
                line = f.readline()
                line = line.strip("\n\r")
        print("開始寫入文本%s" % w_path)
        with codecs.open(w_path, "a", encoding="utf-8") as f:
            for item in q_list:
                if item.__contains__('\ufeff1'):
                    print("===============")
                f.write('%s\n' % item)


if __name__ == '__main__':
    data_dir = "datas"
    for file in os.listdir(data_dir):
        if file.find(".") == -1:
            continue
        file_name = file[0:file.find(".")]
        r_ann_path = os.path.join(data_dir, "%s.ann" % file_name)
        r_txt_path = os.path.join(data_dir, "%s.txt" % file_name)
        w_path = "%s/new/%s-new.txt" % (data_dir, file_name)
        from_ann2dic(r_ann_path, r_txt_path, w_path)
    # 生成train.txt、dev.txt、test.txt
    rw0("%s/new" % data_dir, "%s/new/train.txt" % data_dir)

注意把該代碼文件和datas目錄放在一級,而後把從雲盤下載的10個標註數據文件放在datas目錄下,而後再執行上面的代碼,執行完成後,會在datas/new目錄下生成一些文件,咱們要的是其中的train、dev、test三個txt文件。瀏覽器

ok,到此咱們的訓練數據就準備好了,接下來咱們須要準備預訓練模型。服務器

準備預訓練模型

使用預訓練模型作微調的訓練方式稱爲遷移學習,不太明白什麼意思也不要緊,只要知道這樣作可讓咱們的訓練收斂的更快,而且可使得在較少的訓練樣本上訓練也能獲得不錯的效果。這裏咱們將使用目前最好的天然語言表徵模型之一的bert的中文預訓練模型。若是你還不清楚bert,也不要緊,這裏你只要知道使用bert能夠獲得比word2vec(詞向量)更好的表徵便可。

bert在中文維基百科上預訓練的模型下載地址:

https://storage.googleapis.com/bert_models/2018_11_03/chinese_L-12_H-768_A-12.zip

下載下來,解壓後會看到以下幾個文件:

這裏咱們已經將bert_model.ckpt.data-00000-of-00001文件複製一份,命名爲bert_model.ckpt,因此多了一個bert_model.ckpt文件。由於不這樣作的話,後續的訓練會報錯,找不到ckpt。

以上工做都完成後,就能夠進入訓練環節了。

準備訓練環境

強烈建議使用GPU來訓練,不然你會瘋的。關於GPU環境的搭建能夠參考這篇文章《如何在阿里雲租一臺GPU服務器作深度學習?》。

訓練

本文的模型訓練參考的是github上一個開源的項目,該項目是基於bert+crf算法來訓練命名實體模型的,比基於lstm+crf的項目的效果要好,下面是該項目的地址:

https://github.com/macanv/BERT-BiLSTM-CRF-NER

筆者基於該項目作了一些代碼修改,修改的目的以下:

  • 原來的項目是採用install的方式直接將項目安裝到你的python虛擬環境下,而後經過命令行執行訓練,筆者直接調整了源代碼,爲了能夠基於源代碼執行一些調試;
  • 原來的項目訓練的時候幾乎沒有日誌信息,修改後的項目能夠看到訓練日誌;
  • 原來的項目只能在訓練結束後輸出評估結果,修改後的項目可讓評估脫離訓練過程獨立進行。

修改後的項目地址:

修改後的項目下載下來解壓後,須要作3件事情:

  1. 將以前下載的bert預訓練模型chinese_L-12_H-768_A-12目錄以及目錄中的文件放到項目的models目錄下。
  2. 將以前準備的train、dev、test三個文件放到person_data目錄下。
  3. 爲該項目新建一個python的虛擬環境,而後安裝所須要的依賴包,關於須要哪些依賴包,項目中的requirement.txt是這麼描述的:

    tensorflow的安裝,由於咱們是在GPU上訓練,因此只須要安裝tensorflow-gpu,筆者安裝的是tensorflow1.13.1版本,由於筆者的CUDA版本是10.0。

接下來,執行如下命令進行訓練:
nohup python bert_lstm_ner.py -max_seq_length 500 -batch_size 2 -learning_rate 2e-5 -num_train_epochs 3.0 -filter_adam_var True -verbose -data_dir person_data -output_dir output -init_checkpoint models/chinese_L-12_H-768_A-12/bert_model.ckpt -bert_config_file models/chinese_L-12_H-768_A-12/bert_config.json -vocab_file models/chinese_L-12_H-768_A-12/vocab.txt >log.out 2>&1 &

讓咱們對命令中的參數作一些解釋:

  • nohup
    使用nohup命令,能夠保證在命令窗口被關閉,或遠程連接中斷的狀況下,不影響遠端python程序的執行。python程序執行過程當中的日誌信息會保存在當前文件夾下的log.out文件中。
  • max_seq_length
    每一個樣本的最大長度,不能超過512。若是你的某些樣本超過了這個長度,須要截斷。截斷代碼可使用項目根路徑下的data_process.py文件。
  • batch_size
    每次送到模型進行訓練的樣本數量。通常是2冪次方。若是你的GPU顯存夠大,能夠嘗試增大batch_size。
  • learning_rate
    初始學習率,用於調整模型的學習速度,過大太小都很差。剛開始訓練時:學習率以 0.01 ~ 0.001 爲宜。接近訓練結束:學習速率的衰減應該在100倍以上。這裏由於咱們採用的是遷移學習,因爲預模型自己已經在原始數據集上收斂,此時學習率應該設置的較小,因此這裏設置成0.00002。
  • num_train_epochs
    每次用完全部樣本後,記爲一個epoch。這裏是指設置多少個epoch後訓練結束。
  • filter_adam_var
    保存訓練模型的時候是否過濾掉Adam的參數,默認爲False。設置爲True能夠減少模型的大小。
  • verbose
    加上該參數就會打開tensorflow的日誌。
  • data_dir
    train、dev、test數據所在的目錄。
  • output_dir
    模型輸出目錄。
  • init_checkpoint
    預訓練模型的路徑,這裏咱們使用了bert的中文預訓練模型。
  • bert_config_file
    bert模型的配置文件所在路徑。
  • vocab_file
    bert的詞彙表文件路徑。

開始訓練後,經過如下命令查看訓練過程的日誌信息:
tail -f log.out
下圖截取自訓練結束後的部分輸出日誌:

能夠看到評估損失值降到了0.04862。

訓練會持續3個多小時(在一塊Nvidia Geforce RTX2060 GPU上),結束後,會看到對test.txt樣本進行測試的結果:

測試

每訓練500步,程序會在output目錄下保存一個模型文件,咱們能夠經過修改output目錄下的checkpoint文件來指定要用來測試的模型文件。

而後執行以下命令來對test.txt中的內容進行測試(注意bert_lstm_ner-test.py中的配置要和訓練時指定的參數配置一致):
python bert_lstm_ner-test.py

測試輸出的結果和上面訓練完成後輸出的結果的格式是同樣的。若是你按照本文的步驟,完整的走到這裏了,那麼你已經有了一個能夠識別 人名、地址、組織、公司、產品、時間,共6個實體的命名實體識別模型,下一篇文章《用深度學習作命名實體識別(五):模型使用》將介紹如何使用這個模型來提供一個rest風格的實體識別接口,對該接口傳入一個句子參數,接口會返回句子中的人名、地址、組織、公司、產品、時間信息。

ok,本篇就這麼多內容啦~,感謝閱讀O(∩_∩)O,88~

名句分享


勿以小惡棄人大美,勿以小怨忘人大恩。——曾國藩

爲您推薦


pycharm2019和idea2019版永久激活
手把手教你用深度學習作物體檢測(一): 快速感覺物體檢測的酷炫

本博客內容來自公衆號「程序員一一滌生」,歡迎掃碼關注 o(∩_∩)o

相關文章
相關標籤/搜索