句法分析是自然語言處理中的關鍵技術之一,其基本任務是確定句子的句法結構或者句子中詞彙之間的依存關係。主要包括兩方面的內容,一是確定語言的語法體系,即對語言中合法句子的語法結構給予形式化的定義;另一方面是句法分析技術,即根據給定的語法體系,自動推導出句子的句法結構,分析句子所包含的句法單位和這些句法單位之間的關係。
依存關係本身是一個樹結構,每一個詞看成一個節點,依存關係就是一條有向邊。本文主要通過清華大學的句法標註語料庫,來實現基於 CRF 的中文句法依存分析模型。
語料特徵生成
語料特徵提取,主要採用 N-gram 模型來完成。使用 3-gram 完成提取,將詞性與詞語兩兩進行匹配,分別返回特徵集合和標籤集合,需要注意整個語料採用的是 UTF-8 編碼格式。
引入需要的庫,然後對語料進行讀文件操作。語料採用 UTF-8 編碼格式,以句子爲單位,按 Tab 鍵作分割處理,從而實現句子 3-gram 模型的特徵提取。具體實現如下。
import sklearn_crfsuite
from sklearn_crfsuite import metrics
from sklearn.externals import joblib
其目的是使用模型 sklearn_crfsuite .CRF
,metrics 用來進行模型性能測試,joblib 用來保存和加載訓練好的模型。
接着,定義包含特徵處理方法的類,命名爲 CorpusProcess,類結構定義如下:
class CorpusProcess(object):
def __init__(self):
"""初始化"""
pass
def read_corpus_from_file(self, file_path):
"""讀取語料"""
pass
def write_corpus_to_file(self, data, file_path):
"""寫語料"""
pass
def process_sentence(self,lines):
"""處理句子"""
pass
def initialize(self):
"""語料初始化"""
pass
def generator(self, train=True):
"""特徵生成器"""
pass
def extract_feature(self, sentences):
"""提取特徵"""
pass
CorpusProcess 類中各個方法的具體實現。
第1步, 實現 init 構造函數,目的初始化預處理好的語料的路徑:
def __init__(self):
"""初始化"""
self.train_process_path = dir + "data//train.data" #預處理之後的訓練集
self.test_process_path = dir + "data//dev.data" #預處理之後的測試集
這裏的路徑可以自定義,這裏的語料之前已經完成了預處理過程。
第2-3步,read_corpus_from_file
方法和 write_corpus_to_file
方法,分別定義了語料文件的讀和寫操作:
def read_corpus_from_file(self, file_path):
"""讀取語料"""
f = open(file_path, 'r',encoding='utf-8')
lines = f.readlines()
f.close()
return lines
def write_corpus_to_file(self, data, file_path):
"""寫語料"""
f = open(file_path, 'w')
f.write(str(data))
f.close()
這一步,主要用 open 函數來實現語料文件的讀和寫。
第4-5步,process_sentence
方法和 initialize 方法,用來處理句子和初始化語料,把語料按句子結構用 list 存儲起來,存儲到內存中:
def process_sentence(self,lines):
"""處理句子"""
sentence = []
for line in lines:
if not line.strip():
yield sentence
sentence = []
else:
lines = line.strip().split(u'\t')
result = [line for line in lines]
sentence.append(result)
def initialize(self):
"""語料初始化"""
train_lines = self.read_corpus_from_file(self.train_process_path)
test_lines = self.read_corpus_from_file(self.test_process_path)
self.train_sentences = [sentence for sentence in self.process_sentence(train_lines)]
self.test_sentences = [sentence for sentence in self.process_sentence(test_lines)]
這一步,通過 process_sentence
把句子收尾的空格去掉,然後通過 initialize 函數調用上面 read_corpus_from_file
方法讀取語料,分別加載訓練集和測試集。
第6步,特徵生成器,分別用來指定生成訓練集或者測試集的特徵集:
def generator(self, train=True):
"""特徵生成器"""
if train:
sentences = self.train_sentences
else:
sentences = self.test_sentences
return self.extract_feature(sentences)
這一步,對訓練集和測試集分別處理,如果參數 train 爲 True,則表示處理訓練集,如果是 False,則表示處理測試集。
第7步,特徵提取,簡單的進行 3-gram
的抽取,將詞性與詞語兩兩進行匹配,分別返回特徵集合和標籤集合:
def extract_feature(self, sentences):
"""提取特徵"""
features, tags = [], []
for index in range(len(sentences)):
feature_list, tag_list = [], []
for i in range(len(sentences[index])):
feature = {"w0": sentences[index][i][0],
"p0": sentences[index][i][1],
"w-1": sentences[index][i-1][0] if i != 0 else "BOS",
"w+1": sentences[index][i+1][0] if i != len(sentences[index])-1 else "EOS",
"p-1": sentences[index][i-1][1] if i != 0 else "un",
"p+1": sentences[index][i+1][1] if i != len(sentences[index])-1 else "un"}
feature["w-1:w0"] = feature["w-1"]+feature["w0"]
feature["w0:w+1"] = feature["w0"]+feature["w+1"]
feature["p-1:p0"] = feature["p-1"]+feature["p0"]
feature["p0:p+1"] = feature["p0"]+feature["p+1"]
feature["p-1:w0"] = feature["p-1"]+feature["w0"]
feature["w0:p+1"] = feature["w0"]+feature["p+1"]
feature_list.append(feature)
tag_list.append(sentences[index][i][-1])
features.append(feature_list)
tags.append(tag_list)
return features, tags
經過第6步,確定處理的是訓練集還是測試集之後,通過 extract_feature
對句子進行特徵抽取,使用 3-gram 模型,得到特徵集合和標籤集合的對應關係。
模型訓練及預測
預定義模型需要的一些參數,並初始化模型對象,進而完成模型訓練和預測,以及模型的保存與加載。
定義模型 ModelParser 類,進行初始化參數、模型初始化,以及模型訓練、預測、保存和加載,類的結構定義如下:
class ModelParser(object):
def __init__(self):
"""初始化參數"""
pass
def initialize_model(self):
"""模型初始化"""
pass
def train(self):
"""訓練"""
pass
def predict(self, sentences):
"""模型預測"""
pass
def load_model(self, name='model'):
"""加載模型 """
pass
def save_model(self, name='model'):
"""保存模型"""
pass
分析 ModelParser 類中方法的具體實現。
第1步,init 方法實現算法模型參數和語料預處理 CorpusProcess 類的實例化和初始化:
def __init__(self):
"""初始化參數"""
self.algorithm = "lbfgs"
self.c1 = 0.1
self.c2 = 0.1
self.max_iterations = 100
self.model_path = "model.pkl"
self.corpus = CorpusProcess() #初始化CorpusProcess類
self.corpus.initialize() #語料預處理
self.model = None
這一步,init 方法初始化參數以及 CRF 模型的參數,算法選用 LBFGS,c1 和 c2 分別爲0.1,最大迭代次數100次。然後定義模型保存的文件名稱,以及完成對 CorpusProcess 類 的初始化。
第2-3步,initialize_model
方法和 train 實現模型定義和訓練:
def initialize_model(self):
"""模型初始化"""
algorithm = self.algorithm
c1 = float(self.c1)
c2 = float(self.c2)
max_iterations = int(self.max_iterations)
self.model = sklearn_crfsuite.CRF(algorithm=algorithm, c1=c1, c2=c2,
max_iterations=max_iterations, all_possible_transitions=True)
def train(self):
"""訓練"""
self.initialize_model()
x_train, y_train = self.corpus.generator()
self.model.fit(x_train, y_train)
labels = list(self.model.classes_)
x_test, y_test = self.corpus.generator(train=False)
y_predict = self.model.predict(x_test)
metrics.flat_f1_score(y_test, y_predict, average='weighted', labels=labels)
sorted_labels = sorted(labels, key=lambda name: (name[1:], name[0]))
print(metrics.flat_classification_report(y_test, y_predict, labels=sorted_labels, digits=3))
self.save_model()
這一步,initialize_model
方法實現 了 sklearn_crfsuite.CRF
模型的初始化。然後在 train 方法中,先通過 fit 方法訓練模型,再通過 metrics.flat_f1_score
對測試集進行 F1 性能測試,最後將模型保存。
第4-6步,分別實現模型預測、保存和加載方法。
def predict(self, sentences):
"""模型預測"""
self.load_model()
features, _ = self.corpus.extract_feature(sentences)
return self.model.predict(features)
def load_model(self, name='model'):
"""加載模型 """
self.model = joblib.load(self.model_path)
def save_model(self, name='model'):
"""保存模型"""
joblib.dump(self.model, self.model_path)
最後,實例化類,並進行模型訓練:
model = ModelParser()
model.train()
對模型進行預測,預測數據輸入格式爲三維,表示完整的一句話:
[[['堅決', 'a', 'ad', '1_v'],
['懲治', 'v', 'v', '0_Root'], ['貪污', 'v', 'v', '1_v'], ['賄賂', 'n', 'n', '-1_v'], ['等', 'u', 'udeng', '-1_v'], ['經濟', 'n', 'n', '1_v'], ['犯罪', 'v', 'vn', '-2_v']]]
模型預測的結果如下圖所示:
precision recall f1-score support -1_a 0.811 0.796 0.804 221 -1_b 0.783 0.770 0.777 61 -1_c 0.000 0.000 0.000 5 -1_d 0.711 0.409 0.519 66 -1_f 0.867 0.565 0.684 23 -1_h 0.000 0.000 0.000 0 -1_k 0.667 1.000 0.800 2 -1_m 0.905 0.895 0.900 256 -1_n 0.720 0.754 0.737 967 11_n 0.000 0.000 0.000 0 -1_ng 0.000 0.000 0.000 23 -1_nl 0.750 0.143 0.240 21 -1_nr 1.000 0.059 0.111 17 -1_nr1 0.000 0.000 0.000 0 -1_nr2 0.000 0.000 0.000 0 -1_nrf 0.000 0.000 0.000 25 -1_nrj 0.000 0.000 0.000 2 -1_ns 0.870 0.400 0.548 50 -1_nsf 0.822 0.402 0.540 92 -1_nt 0.667 0.286 0.400 14 -1_nz 0.000 0.000 0.000 7 -1_o 0.000 0.000 0.000 1 -1_p 0.524 0.214 0.303 103 -1_q 0.706 0.649 0.676 37 -1_r 0.946 0.841 0.891 63 -1_s 0.737 0.933 0.824 15 -1_t 0.952 0.894 0.922 66 -1_u 0.000 0.000 0.000 3 -1_v 0.628 0.669 0.648 2396 -1_x 0.000 0.000 0.000 0 -1_z 0.875 0.636 0.737 11 -2_a 0.800 0.364 0.500 11 -2_b 0.000 0.000 0.000 1 -2_c 0.000 0.000 0.000 0 -2_d 0.000 0.000 0.000 2 -2_f 0.000 0.000 0.000 1 -2_m 0.897 0.876 0.886 89 -2_n 0.095 0.021 0.034 95 -2_ng 0.000 0.000 0.000 0 -2_nl 0.000 0.000 0.000 1 -2_nr 0.000 0.000 0.000 1 -2_nr2 0.000 0.000 0.000 0 -2_nrf 0.000 0.000 0.000 3 -2_ns 0.000 0.000 0.000 3 -2_nsf 0.000 0.000 0.000 3 -2_nz 0.000 0.000 0.000 1 -2_p 0.000 0.000 0.000 9 -2_q 0.000 0.000 0.000 2 -2_r 0.000 0.000 0.000 3 -2_s 0.000 0.000 0.000 0 -2_t 1.000 0.920 0.958 25 -2_u 0.000 0.000 0.000 0 -2_v 0.326 0.211 0.256 445 -2_z 0.000 0.000 0.000 0 -3_a 1.000 0.500 0.667 2 -3_b 0.000 0.000 0.000 0 -3_d 0.000 0.000 0.000 0 -3_m 0.625 0.750 0.682 20 -3_n 0.000 0.000 0.000 20 -3_nl 0.000 0.000 0.000 0 -3_nsf 0.000 0.000 0.000 0 -3_p 0.000 0.000 0.000 1 -3_q 0.000 0.000 0.000 0 -3_t 1.000 0.571 0.727 14 -3_v 0.125 0.045 0.066 112 -4_b 0.000 0.000 0.000 0 -4_d 0.000 0.000 0.000 0 -4_m 0.500 0.200 0.286 5 -4_n 0.000 0.000 0.000 8 -4_nsf 0.000 0.000 0.000 0 -4_p 0.000 0.000 0.000 1 -4_t 0.000 0.000 0.000 0 -4_v 0.000 0.000 0.000 33 -5_d 0.000 0.000 0.000 0 -5_m 0.000 0.000 0.000 2 -5_n 0.000 0.000 0.000 3 -5_v 0.000 0.000 0.000 4 -6_m 0.000 0.000 0.000 0 -6_n 0.000 0.000 0.000 1 -6_v 0.000 0.000 0.000 2 -7_m 0.000 0.000 0.000 0 -7_n 0.000 0.000 0.000 0 -7_v 0.000 0.000 0.000 0 -8_n 0.000 0.000 0.000 0 -8_v 0.000 0.000 0.000 0 1_R 0.000 0.000 0.000 0 0_Root 0.640 0.781 0.704 1997 0_a 0.000 0.000 0.000 0 1_a 0.655 0.599 0.626 282 2_a 0.000 0.000 0.000 6 3_a 0.000 0.000 0.000 0 1_b 0.706 0.436 0.539 55 2_b 0.000 0.000 0.000 2 3_b 0.000 0.000 0.000 0 1_c 0.500 0.400 0.444 5 2_c 0.000 0.000 0.000 1 3_c 0.000 0.000 0.000 0 1_d 0.439 0.305 0.360 59 2_d 0.000 0.000 0.000 8 3_d 0.000 0.000 0.000 2 1_f 0.738 0.649 0.691 74 2_f 0.000 0.000 0.000 3 3_f 0.000 0.000 0.000 0 1_h 0.000 0.000 0.000 0 1_k 0.818 0.750 0.783 12 0_m 0.750 0.375 0.500 8 1_m 0.715 0.628 0.669 148 2_m 0.541 0.541 0.541 37 3_m 0.429 0.167 0.240 18 4_m 1.000 0.600 0.750 5 5_m 0.333 0.250 0.286 4 6_m 0.000 0.000 0.000 1 1_n 0.673 0.804 0.732 3348 2_n 0.224 0.090 0.129 587 3_n 0.308 0.053 0.090 152 4_n 0.000 0.000 0.000 40 5_n 0.000 0.000 0.000 15 6_n 0.000 0.000 0.000 4 7_n 0.000 0.000 0.000 3 8_n 0.000 0.000 0.000 0 9_n 0.000 0.000 0.000 0 1_ng 0.333 0.036 0.065 83 2_ng 0.000 0.000 0.000 3 3_ng 0.000 0.000 0.000 0 1_nl 0.600 0.079 0.140 38 2_nl 0.000 0.000 0.000 4 3_nl 0.000 0.000 0.000 0 1_nr 0.000 0.000 0.000 9 2_nr 0.000 0.000 0.000 3 3_nr 0.000 0.000 0.000 0 1_nr1 0.000 0.000 0.000 0 1_nr2 0.000 0.000 0.000 0 2_nr2 0.000 0.000 0.000 0 1_nrf 0.000 0.000 0.000 9 2_nrf 0.000 0.000 0.000 1 3_nrf 0.000 0.000 0.000 0 1_nrj 0.000 0.000 0.000 1 2_nrj 0.000 0.000 0.000 0 1_ns 0.536 0.294 0.380 51 2_ns 0.000 0.000 0.000 2 3_ns 0.000 0.000 0.000 0 4_ns 0.000 0.000 0.000 0 5_ns 0.000 0.000 0.000 0 1_nsf 0.649 0.333 0.440 72 2_nsf 0.000 0.000 0.000 4 3_nsf 0.000 0.000 0.000 0 1_nt 0.889 0.421 0.571 19 2_nt 0.000 0.000 0.000 0 3_nt 0.000 0.000 0.000 0 1_nz 1.000 0.200 0.333 10 2_nz 1.000 0.333 0.500 3 3_nz 0.000 0.000 0.000 0 1_o 0.000 0.000 0.000 0 1_p 0.705 0.518 0.597 83 2_p 0.000 0.000 0.000 0 3_p 0.000 0.000 0.000 0 1_q 0.819 0.831 0.825 349 2_q 0.000 0.000 0.000 1 1_r 0.750 0.774 0.762 62 2_r 0.000 0.000 0.000 1 1_s 0.711 0.818 0.761 33 2_s 0.000 0.000 0.000 0 3_s 0.000 0.000 0.000 0 1_t 0.907 0.801 0.851 146 2_t 0.667 0.606 0.635 33 3_t 0.375 0.300 0.333 10 4_t 0.000 0.000 0.000 2 5_t 0.000 0.000 0.000 0 1_u 1.000 0.200 0.333 20 2_u 0.000 0.000 0.000 1 3_u 0.000 0.000 0.000 0 1_v 0.670 0.814 0.735 3331 2_v 0.456 0.187 0.265 445 3_v 0.091 0.011 0.020 87 4_v 0.000 0.000 0.000 20 5_v 0.000 0.000 0.000 8 6_v 0.000 0.000 0.000 2 7_v 0.000 0.000 0.000 0 1_x 1.000 0.333 0.500 3 1_y 0.000 0.000 0.000 0 1_z 0.333 0.500 0.400 2 micro avg 0.660 0.660 0.660 17297 macro avg 0.245 0.175 0.191 17297 weighted avg 0.624 0.660 0.630 17297