我们将使用sklearn-crfsuite在我们的数据集上训练用于命名实体识别的CRF模型。
一,介绍需要用到的库
(1), sklearn_crfsuite这个库的安装导入看我之前的博客https://mp.csdn.net/mp_blog/creation/editor/119104841
import scipy
import sklearn_crfsuite
from sklearn.metrics import make_scorer
from sklearn.model_selection import train_test_split
from sklearn_crfsuite import scorers
from sklearn_crfsuite import metrics
from collections import Counter
from sklearn. model_selection import cross_val_score#交叉验证,只需将 cross_validation 改为 model_selection 即可
from sklearn.model_selection import GridSearchCV#sklearn.grid_search在版本0.18就被弃用了,
# 版本0.2用sklearn.model_selection模块代替
# from sklearn.model_selection import GridSearchCV 就对了
(2)更多详细内容请康康我其他博客
https://blog.csdn.net/weixin_51130521/article/details/119205748
二,数据
(1)数据介绍
数据是IOB和POS标签注释的特征设计语料库在如下链接
https://www.kaggle.com/abhinavwalia95/how-to-loading-and-fitting-dataset-to-scikit/data
数据大概是这样式的
有关实体的基本信息:
- geo =区域实体(Geographical Entity)
- org =组织(Organization)
- per =人(Person)
- gpe =地缘政治实体(Geopolitical Entity)
- tim =时间指示器(Time indicator)
- art =人工制品(Artifact)
- eve =事件(Event)
- nat =自然现象(Natural Phenomenon)
Inside–outside–beginning(标记)
IOB (Inside–outside–beginning)是用于标记标志的通用标记格式。
- I-标签前的前缀表示标签位于块内。
- B-标签前的前缀表示标签是块的开头。
- O标记表示标志不属于任何块(outside)
(2)数据读取
整个数据集不能装入一台计算机的内存中,因此我们选择前100,000个记录,并使用外存学习算法(Out-of-core learning algorithm)来有效地获取和处理数据。
df= pd.read_csv('./ner_dataset.csv',encoding="ISO-8859-1")##ISO-8859-1默认为英文编码ISO-8859-1改成gbk支持中文
df= df [:10000]
df.head()#将excel表格中的第一行看作列名,默认输出之后的5行
- df= pd.read_csv('./ner_dataset.csv',encoding="ISO-8859-1")
- './ner_dataset.csv' ——表示一个相对路径,需要把数据和代码放到同一个路径的目录下边就可以读取数据了。
- encoding="ISO-8859-1"——ISO-8859-1默认为英文编码ISO-8859-1改成gbk支持中文
2. df= df [:10000]
-
整个数据集不能装入一台计算机的内存中,因此我们选择前100,000个记录,
3. df.head()#将excel表格中的第一行看作列名,默认输出之后的5行
其实跟excel表格的差别不大
(3)数据统计
判断缺失值然后将列中为空的个数统计出来
df.isnull().sum()#判断缺失值然后将列中为空的个数统计出来
详见https://blog.csdn.net/weixin_51130521/article/details/119220708
(4)数据预处理
df= df.fillna(method='ffill')#我们注意到“Sentence#”列中有很多NaN值,我们用前面的值填充NaN。
df['Sentence #'].nunique(), df.Word.nunique(), df.Tag.nunique()#(4544,10922,17)
#我们有4,544个句子,其中包含10,922个独特单词并标记为17个标签。
#标签分布不均匀。
df.groupby('Tag').size().reset_index(name='counts')#获取标签的行数
1. df= df.fillna(method='ffill')#我们注意到“Sentence#”列中有很多NaN值,我们用前面的值填充NaN。
2. df['Sentence #'].nunique(), df.Word.nunique(), df.Tag.nunique()
- nuinque()是查看该序列(axis=0/1对应着列或行)的不同值的数量。用这个函数可以查看数据有多少个不同值。 后边同理。
- 的出来的结果是我们有4,544个句子,其中包含10,922个独特单词并标记为17个标签。
3. df.groupby('Tag').size().reset_index(name='counts')#获取标签的行数
- 我们发现标签分布不均匀
(5)使用DictVectorizer将文本转换为向量
X= df.drop('Tag',axis= 1)#代表将'Tag'对应的列标签(们)沿着水平的方向依次删掉。
v= DictVectorizer(sparse= False)#特征转换
X= v.fit_transform(X.to_dict('records'))
y= df.Tag.values
classes= np.unique(y)#该函数是去除数组中的重复数字,并进行排序之后输出。
classes= classes.tolist()#将数组或者矩阵转化成列表
1. X= df.drop('Tag',axis= 1)#代表将'Tag'对应的列标签(们)沿着水平的方向依次删掉。
2. v= DictVectorizer(sparse= False)#特征转换
3. X= v.fit_transform(X.to_dict('records'))
参考连接https://blog.csdn.net/qq_27328197/article/details/113807051
4. X= v.fit_transform(X.to_dict('records'))
参考连接https://blog.csdn.net/weixin_38278334/article/details/82971752
5. classes= np.unique(y)#该函数是去除数组中的重复数字,并进行排序之后输出。
6. classes= classes.tolist()#将数组或者矩阵转化成列表
-
该函数是去除数组中的重复数字,并进行排序之后输出。
-
将数组或者矩阵转化成列表
参考连接https://blog.csdn.net/qq_35290785/article/details/96165472
三,检索带有POS和标签的句子。
这个类负责将每个具有命名实体(标记)的句子转换为元组列表[(单词,命名实体),…]
class SentenceGetter(object):
# 这个类负责将每个具有命名实体(标记)的句子转换为元组列表[(单词,命名实体),…]
def __init__(self, data):
self.n_sent= 1
self.data= data
self.empty= False
agg_func= lambda s: [(w, p, t )for w, p, t in zip(s['Word'].values.tolist(),
s['POS'].values.tolist(),
s['Tag'].values.tolist())]
self.grouped= self.data.groupby('Sentence #').apply(agg_func)
self.sentences= [s for s in self.grouped]
def get_next(self):
try:
s= self.grouped['Sentence: {}'.format(self.n_sent)]
self.n_sent+= 1
return s
except:
return None
getter = SentenceGetter(df) # 实例化 sent = getter.get_next() # 输出元组中每一个单词对应得B-I-O sentences = getter.sentences # ”拿到句子“,比如len(sentences)就是句子得个数
四,特征处理
""" 特征处理流程,主要选择处理了如下几个特征: - 当前词的小写格式 - 当前词的后缀 - 当前词是否全大写 isupper - 当前词的首字母大写,其他字母小写判断 istitle - 当前词是否为数字 isdigit - 当前词的词性 - 当前词的词性前缀 - 还有就是与之前后相关联的词的上述特征(类似于特征模板的定义) """
def word2features(sent, i): # sent作为一个句子单位(字+标签)
''' 特征提取器 '''
word = sent[i][0] # 词
postag = sent[i][1] # 词性
features = {
'bias': 1.0,
'word.lower()': word.lower(),
'word[-3:]': word[-3:],
'word[-2:]': word[-2:],
'word.isupper()': word.isupper(),
'word.istitle()': word.istitle(),
# 如果字符串是以标题格式开头的字符串,并且至少有一个字符,则返回 True,否则返回 False。
'word.isdigit()': word.isdigit(),
'postag': postag,
'postag[:2]': postag[:2],
}
if i > 0: # 前一个词的特征
word1 = sent[i - 1][0]
postag1 = sent[i - 1][1]
features.update({
'-1:word.lower()': word1.lower(),
'-1:word.istitle()': word1.istitle(),
'-1:word.isupper()': word1.isupper(),
'-1:postag': postag1,
'-1:postag[:2]': postag1[:2],
})
else:
features['BOS'] = True
if i < len(sent) - 1: # 后一个词特征
word1 = sent[i + 1][0]
postag1 = sent[i + 1][1]
features.update({
'+1:word.lower()': word1.lower(),
'+1:word.istitle()': word1.istitle(),
'+1:word.isupper()': word1.isupper(),
'+1:postag': postag1,
'+1:postag[:2]': postag1[:2],
})
else:
features['EOS'] = True
return features
五,特征提取
def sent2features(sent): ''' 提取句子特征 ''' return [word2features(sent, i) for i in range(len(sent))] def sent2labels(sent): ''' 提取句子 label ''' return [label for token, postag, label in sent] def sent2tokens(sent): ''' 提取句子词 ''' return [token for token, postag, label in sent]
六,拆分训练和测试集
X = [sent2features(s) for s in sentences]
y = [sent2labels(s) for s in sentences]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=0)
七,剩下代码自己康康叭
def print_transitions(trans_features):
for (label_from, label_to), weight in trans_features:
print("%-6s -> %-7s %.6f" % (label_from, label_to, weight))
def print_state_features(state_features):
for (attr, label), weight in state_features:
print("%.6f %-8s %s" % (weight, label, attr))
if __name__ == "__main__":
# 注意,迭代取最优模型时一定要在 if __name__ == "__main__" 中
# 设定模型和超参数
crf = sklearn_crfsuite.CRF(
algorithm='lbfgs',
c1=0.1,
c2=0.1, # L2 正则化的系数。
max_iterations=100,
all_possible_transitions=True)
# 开始训练
crf.fit(X_train, y_train)
labels = list(crf.classes_)
print
labels # ['B-LOC','O','B-ORG','B-PER','I-PER','B-MISC','I-ORG','I-LOC','I-MISC']
labels.remove('O') # 标签“O”(outside)是最常见的标签,它会使我们的结果看起来比实际更好。
# 因此,当我们评估分类指标时,我们会删除标记“O”
# 使用测试集评测
Y_pred = crf.predict(X_test)
metrics.flat_f1_score(y_test, Y_pred, average='weighted', labels=classes)
# 获得标记是 B 或者 I 的结果
sorted_labels = sorted(labels,
key=lambda x: (x[1:], x[0]))
print("初始模型效果如下...")
print(metrics.flat_classification_report(y_test,
Y_pred,
labels=sorted_labels,
digits=3)) # digits 表示保留几位小数
# 定义超参数和参数查找空间
crf = sklearn_crfsuite.CRF(
algorithm='lbfgs',
max_iterations=100,
all_possible_transitions=True)
params_space = {'c1': [scipy.stats.expon(scale=0.5)],
'c2': [scipy.stats.expon(scale=0.05)]}#概率
# 使用相同的基准评估数据
f1_scorer = make_scorer(metrics.flat_f1_score, average='weighted', labels=labels)
# 查询最佳模型
rs = GridSearchCV(crf, params_space,
cv=3,
verbose=1,
n_jobs=-1,
# n_iter=50,
scoring=f1_scorer)
rs.fit(X_train, y_train)
# 输出最佳模型参数
print("The Best Params:", rs.best_params_)
print("The Best CV score:", rs.best_score_)
print("Model Size:{:.2f}M".format(rs.best_estimator_.size_ / 1000000))
crf = rs.best_estimator_
Y_pred = crf.predict(X_test)
print("最佳模型效果如下...")
print(metrics.flat_classification_report(y_train,
Y_pred,
labels=sorted_labels,
digits=3))
print("\n最大转移概率")
print_transitions(Counter(crf.transition_features_).most_common(20))
print("\n最低转移概率")
print_transitions(Counter(crf.transition_features_).most_common()[-20:])
print("\nTop Positive")
print_state_features(Counter(crf.state_features_).most_common(30))
print("\nTop Negative")
print_state_features(Counter(crf.state_features_).most_common()[-30:])