1. 程式人生 > >【Natural Language Processing】基於CRF++的中文分詞

【Natural Language Processing】基於CRF++的中文分詞

一、任務簡介

        中文分詞是中文資訊處理領域中的最重要的任務,它對於智慧資訊處理技術具有重要的意義,當前的各種漢語分詞技術都可以取得不錯的結果。

本任務做的是繁體中文分詞,將訓練語料的30%作為驗證集,70%作為訓練集,按通常的 P/R/F 三個指標進行評測,最後用全部訓練資料進行訓練,用給出的測試資料進行測試,並將結果提交。本次任務使用條件隨機場模型(CRF)進行實驗。

2  實驗環境

名稱                          

詳細資訊

作業系統

Macos x 10.12.5 64位

CPU

2.2GHz*4

記憶體

16G

Python

2.7

CRF++

0.58

三、資料處理

        因為本次任務所給原資料檔案的編碼是utf-16, CRF++能處理該型別的格式,所以要先把資料格式轉成CRF++能處理的utf-8編碼檔案,否則出現異常不會生成模型,我們採用4-tag( B(Begin,詞首), E(End,詞尾), M(Middle,詞中), S(Single,單字詞))標記集,處理文字。本次任務把原訓練資料劃分成3:7,其中30%作為測試集,70%作為訓練集,在劃分資料集上採用了兩種方法:

①選取前70%作為訓練集,剩下作為驗證集;

②隨機選取70%作為訓練集,餘下為測試集。

對應程式碼:process.py

# -*- coding: UTF-8 -*-

import codecs
import random
import numpy as np
import sys
reload(sys)
sys.setdefaultencoding('utf8')

# 訓練集7,3分,用於訓練和測試
def divide(input_file, train, dev):
    input_data = codecs.open(input_file, 'r', 'utf-16')
    fw_train =  codecs.open(train, 'w', 'utf-16')
    fw_dev = codecs.open(dev, 'w', 'utf-16')
    data = input_data.readlines()
    train_len = int(len(data) * 0.7 )
    for i in range(train_len):
        fw_train.write(data[i])
    for i in range(train_len,len(data)):
        fw_dev.write(data[i])
    input_data.close()
    fw_train.close()
    fw_dev.close()

# 隨機將訓練集7,3分,用於訓練和測試
def train_dev_split(input_file, train, dev):
    input_data = codecs.open(input_file, 'r', 'utf-16')
    data = input_data.readlines()
    print len(data)
    fw_train = codecs.open(train, 'w', 'utf-16')
    fw_dev = codecs.open(dev, 'w', 'utf-16')

    shuffle_indices = np.random.permutation(np.arange(len(data)))
    train_lst = shuffle_indices[0:0.7*len(data)]
    test_lst = shuffle_indices[0.7*len(data):]

    for i in train_lst:
        if len(data[i].strip())==0:
            continue
        fw_train.write(data[i])

    for j in test_lst:
        if len(data[j].strip()) == 0:
            continue
        fw_dev.write(data[j])
    input_data.close()
    fw_train.close()
    fw_dev.close()


def trans_dev(devseg, dev):
    fr = codecs.open(devseg, 'r', 'utf-16')
    fw_dev = codecs.open(dev, 'w', 'utf-16')
    for line in fr.readlines():
        line = line.replace('  ', '')
        fw_dev.write(line)
    fr.close()
    fw_dev.close()



if __name__ == '__main__':
    train_dev_split('../CRF_Seg/data/Train_utf16.seg', '../CRF_Seg/data/train.seg', '../CRF_Seg/data/dev.seg')
    trans_dev('../CRF_Seg/data/dev.seg', '../CRF_Seg/data/devs.seg')

四、使用模板

# Unigram
U00:%x[-2,0]
U01:%x[-1,0]
U02:%x[0,0]
U03:%x[1,0]
U04:%x[2,0]
U05:%x[-2,0]/%x[-1,0]/%x[0,0]
U06:%x[-1,0]/%x[0,0]/%x[1,0]
U07:%x[0,0]/%x[1,0]/%x[2,0]
U08:%x[-1,0]/%x[0,0]
U09:%x[0,0]/%x[1,0]
# Bigram
B

五、訓練和測試

        經過前面的資料處理部分得到的驗證階段的訓練集,就可以利用crf的訓練工具crf_learn來訓練模型,然後用得到的模型對驗證集進行分詞,最後進行評測。

同樣地,使用該方法對任務給出的全部訓練集訓練資料,然後使用訓練得到的模型對測試集進行分詞。

        對應程式碼:make_train.py; make_test.py;evaluate.py

①make_train.py

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

#4-tags for character tagging: B(Begin),E(End),M(Middle),S(Single)

import codecs
import sys

def character_tagging(input_file, output_file):
	input_data = codecs.open(input_file, 'r', 'utf-16')
	output_data = codecs.open(output_file, 'w', 'utf-8')
	for line in input_data.readlines():
		word_list = line.strip().split()
		for word in word_list:
			if len(word) == 1:
				output_data.write(word + "\tS\n")
			else:
				output_data.write(word[0] + "\tB\n")
				for w in word[1:len(word)-1]:
					output_data.write(w + "\tM\n")
				output_data.write(word[len(word)-1] + "\tE\n")
		output_data.write("\n")
	input_data.close()
	output_data.close()

if __name__ == '__main__':
	# input_file = '../CRF_Seg/data/train.seg'
	# output_file = '../CRF_Seg/data/train.data'
	# character_tagging(input_file, output_file)

	input_file = '../CRF_Seg/data/Train_utf16.seg'
	output_file = '../CRF_Seg/data/All_train.data'
	character_tagging(input_file, output_file)
②make_test.py
#-*-coding:utf-8-*-

#CRF Segmenter based character tagging:
# 4-tags for character tagging: B(Begin), E(End), M(Middle), S(Single)

import codecs
import sys

import CRFPP

def crf_segmenter(input_file, output_file, tagger):
    input_data = codecs.open(input_file, 'r', 'utf-16')
    output_data = codecs.open(output_file, 'w', 'utf-16')
    for line in input_data.readlines():
        tagger.clear()
        for word in line.strip():
            word = word.strip()
            if word:
                tagger.add((word + "\to\tB").encode('utf-8'))
        tagger.parse()
        size = tagger.size()
        xsize = tagger.xsize()
        for i in range(0, size):
            for j in range(0, xsize):
                char = tagger.x(i, j).decode('utf-8')
                tag = tagger.y2(i)
                if tag == 'B':
                    output_data.write(' ' + char)
                elif tag == 'M':
                    output_data.write(char)
                elif tag == 'E':
                    output_data.write(char + ' ')
                else:
                    output_data.write(' ' + char + ' ')
        output_data.write('\n')
    input_data.close()
    output_data.close()

if __name__ == '__main__':
    crf_model = '/Users/lhy/Documents/CS_work/python/CRF_Seg/data/crf_model'
    input_file = '/Users/lhy/Documents/CS_work/python/CRF_Seg/data/devs.seg'
    output_file = '/Users/lhy/Documents/CS_work/python/CRF_Seg/data/devCRF.seg'
    tagger = CRFPP.Tagger("-m " + crf_model)
    crf_segmenter(input_file, output_file, tagger)

    # crf_model = '/Users/lhy/Documents/CS_work/python/CRF_Seg/data/crf_model'
    # input_file = '/Users/lhy/Documents/CS_work/python/CRF_Seg/data/Test_utf16.seg'
    # output_file = '/Users/lhy/Documents/CS_work/python/CRF_Seg/data/test.seg'
    # tagger = CRFPP.Tagger("-m " + crf_model)
    # crf_segmenter(input_file, output_file, tagger)
③evaluate.py
# -*- coding: utf-8 -*-

import codecs
import sys

def loadResults(FileName) :
    ret = []
    lines = codecs.open(FileName, encoding='utf-16-le')

    for iters, line in enumerate(lines) :
        line = line.strip()
        chr_t= line.split(' ')
        ret.append(chr_t)

    return ret

def evaluate(predictedFile, goldenFile) :
    result_pre = loadResults(predictedFile)
    result_gld = loadResults(goldenFile)

    assert len(result_pre) == len(result_gld)
    # print([len(result_pre), len(result_gld)])

    TPs   = 0.
    TPFPs = 0.
    TPFNs = 0.

    for i in range(len(result_pre)) :
        pre_i, gld_i = result_pre[i], result_gld[i]

        TPs   += len([word for word in gld_i if word in pre_i])
        TPFPs += len(pre_i)
        TPFNs += len(gld_i)

    harmonic_mean = lambda x, y : 2 / (1 / x + 1 / y)

    precisions = 100.*TPs / TPFPs if not TPs == 0 else 0
    recalls= 100.*TPs / TPFNs if not TPs == 0 else 0
    f1_ss= harmonic_mean(precisions, recalls) if not precisions* recalls == 0 else 0
    print('\tprecision: %.2f  recall: %.2f  f1_score: %.2f'%(precisions, recalls, f1_ss))


if __name__ == '__main__':
    pre='../CRF_Seg/data/dev.seg'
    gold='../CRF_Seg/data/devCRF.seg'
    evaluate(pre,gold)

六、結果與分析

P(準確率)

R(召回率)

F值

按順序7:3劃分

96.58%

96.53%

96.56%

隨機劃分資料集

97.18%

97.27%

97.22%

        從驗證集的結果來看,無論是從準確率或者是召回率上分詞的效果還不錯;同時可以發現按照不同的資料劃分方式,結果表現出一定的差別,隨機劃分的方式結果要好於按照順序劃分資料集的方式,經過分析,應該是由於附近的句子存在相近或者相同的可能性更大,,那麼測試集和驗證集的分佈更為相似,最後會導致結果偏高,但在實際中,訓練集和測試集的分佈可能相差比較大,所以我們在這個實驗中,我們應該選擇按順序劃分資料集的方法更為合理,同時結果也以按順序劃分為準。

        由於之前沒有做過中文分詞方面的工作,所以本次大作業我自己嘗試寫了一個一階隱馬爾科夫模型進行分詞,但是效果並不好,主要是比較簡單,如果嘗試二階或者三階隱馬爾科夫模型效果可能會有一定的提升,後面我查詢許多資訊發現都是用CRF來做的,並且效果不錯,並且有CRF++這個現成的工具可用,所以最後我選擇了CRF。