命名实体识别(二)——基于条件随机场的命名实体识别

article/2025/9/19 7:56:26

一、条件随机场

首先,我们看一下条件随机场的定义:在给定一组输入序列的条件下,另一组输出序列的条件概率分布模型。设X=(X_{1},X_{2},.X_{3},...X_{n})(X_{1},X_{2},X_{3},...X_{n})和Y=(Y_{1},Y_{2},Y_{3},...Y_{m})是联合随机变量,若随机变量Y构成一个无向图G=(V,E)表示的马尔科夫模型,则其条件概率分布P(Y|X)称为条件随机场,即

                                                        P(Y_{v}|X,Y_{w},w \neq v )=P(Y_{v}|X,Y_{w},w \sim v )

其中,w\sim v表示图G=(V,E)中与节点v有边连接的所有节点,w\neq v表示结点v以外的所有节点。在实际命名实体识别应用中,X是字符,Y是标签。

二、线性链条件随机场

如图,我们假设X与Y具有相同的结构,X=(X_{1},X_{2},.X_{3},...X_{n})(X_{1},X_{2},X_{3},...X_{n}),Y=(Y_{1},Y_{2},Y_{3},...Y_{n}),均为线性链表示的随机变量序列,若在给定的随机变量序列X的条件下,随机变量序列Y的条件概率分布P(Y|X)构成条件随机场,且满足马尔科夫性:

                                              P(Y_{i}\mid X,Y_{1},Y_{2},...Y_{n})=P(Y_{i}\mid X,Y_{i-1},Y_{i+1})

则称P(Y|X)为线性链的条件随机场。

与HMM相比,HMM是一个有向图,线性链CRF是一个无向图;HMM只考虑当前状态的上一个状态,线性链CRF依赖于当前状态的周围结点状态。

线性链CRF应用到命名实体识别就是在大量可选标注序列中,找出最靠谱的作为句子的标注。我们需要定义一个特征函数集合,用这个特征集合为标注序列打分,根据分数高低选择最靠谱的标注序列。

CRF有两种特征函数,分别是转移函数t_{k}(y_{i-1},y_{i},i)和状态函数s_{l}(y_{i},X,i)

1)转移函数t_{k}(y_{i-1},y_{i},i)依赖于当前和前一个位置,表示从标注序列中位置i-1的标记y_{i-1}转移到位置i上的标记y_{i}的概率;

2)状态函数s_{l}(y_{i},X,i)依赖当前位置,表示标记序列在位置i上为标记y_{i}的概率。通常特征函数取值1或0,表示符不符合该条规则约束,具体公式如下:

 

                                           P(y|x)=\frac{1}{Z(x)}exp(\sum_{i,k}\lambda _{k}t_{k}(y_{i-1},y_{i},i)+\sum_{i,l}\mu _{l}s_{l}(y_{i},X,i))

其中,Z(x)是规范化因子,\lambda _{k}\mu _{l}是转移函数和状态函数对应的权值,目的是求\arg max_{y}P(y|x)。求解类似于HMM,可用维特比算法。

三、CRF算法

这里以地名识别为例,使用CRF++工具来实现。

1)语料处理

CRF++的训练数据要求一定的格式,一般是一行一个token,一句话由多行token组成,多个句子之间用空行分开。其中每行又分为多列,最后一列是要预测的标签(B,M,E,S,O),前面几列是特征,这里我们采用字符这一个特征。例如:

      

2)特征模板设计

特征模板的基本格式为%x[row,col],用于确定输入数据的一个token,row确定与当前的token的相对行数,col用于确定决定列数。CRF++有两种模板类型:

  • 以字母U开头,是 Unigram template,CRF++会自动为其生成一个特征函数集合;
  • 以字母B开头,是Bigram template,系统会自动生成当前输出与前一个输出token的集合,根据该组合特征构造函数。

模型使用可参考:https://blog.csdn.net/u010626937/article/details/78414292

模型训练和测试完成后,可以使用以下代码输出准确率、召回率、F值等评估值。

import sys
import re
import codecs
from collections import defaultdict, namedtupleANY_SPACE = '<SPACE>'class FormatError(Exception):passMetrics = namedtuple('Metrics', 'tp fp fn prec rec fscore')class EvalCounts(object):def __init__(self):self.correct_chunk = 0    # number of correctly identified chunksself.correct_tags = 0     # number of correct chunk tagsself.found_correct = 0    # number of chunks in corpusself.found_guessed = 0    # number of identified chunksself.token_counter = 0    # token counter (ignores sentence breaks)# counts by typeself.t_correct_chunk = defaultdict(int)self.t_found_correct = defaultdict(int)self.t_found_guessed = defaultdict(int)def parse_args(argv):import argparseparser = argparse.ArgumentParser(description='evaluate tagging results using CoNLL criteria',formatter_class=argparse.ArgumentDefaultsHelpFormatter)arg = parser.add_argumentarg('-b', '--boundary', metavar='STR', default='-X-',help='sentence boundary')arg('-d', '--delimiter', metavar='CHAR', default=ANY_SPACE,help='character delimiting items in input')arg('-o', '--otag', metavar='CHAR', default='O',help='alternative outside tag')arg('file', nargs='?', default=None)return parser.parse_args(argv)def parse_tag(t):m = re.match(r'^([^-]*)-(.*)$', t)return m.groups() if m else (t, '')def evaluate(iterable, options=None):if options is None:options = parse_args([])    # use defaultscounts = EvalCounts()num_features = None       # number of features per linein_correct = False        # currently processed chunks is correct until nowlast_correct = 'O'        # previous chunk tag in corpuslast_correct_type = ''    # type of previously identified chunk taglast_guessed = 'O'        # previously identified chunk taglast_guessed_type = ''    # type of previous chunk tag in corpusfor line in iterable:line = line.rstrip('\r\n')if options.delimiter == ANY_SPACE:features = line.split()else:features = line.split(options.delimiter)if num_features is None:num_features = len(features)elif num_features != len(features) and len(features) != 0:raise FormatError('unexpected number of features: %d (%d)' %(len(features), num_features))if len(features) == 0 or features[0] == options.boundary:features = [options.boundary, 'O', 'O']if len(features) < 3:raise FormatError('unexpected number of features in line %s' % line)guessed, guessed_type = parse_tag(features.pop())correct, correct_type = parse_tag(features.pop())first_item = features.pop(0)if first_item == options.boundary:guessed = 'O'end_correct = end_of_chunk(last_correct, correct,last_correct_type, correct_type)end_guessed = end_of_chunk(last_guessed, guessed,last_guessed_type, guessed_type)start_correct = start_of_chunk(last_correct, correct,last_correct_type, correct_type)start_guessed = start_of_chunk(last_guessed, guessed,last_guessed_type, guessed_type)if in_correct:if (end_correct and end_guessed andlast_guessed_type == last_correct_type):in_correct = Falsecounts.correct_chunk += 1counts.t_correct_chunk[last_correct_type] += 1elif (end_correct != end_guessed or guessed_type != correct_type):in_correct = Falseif start_correct and start_guessed and guessed_type == correct_type:in_correct = Trueif start_correct:counts.found_correct += 1counts.t_found_correct[correct_type] += 1if start_guessed:counts.found_guessed += 1counts.t_found_guessed[guessed_type] += 1if first_item != options.boundary:if correct == guessed and guessed_type == correct_type:counts.correct_tags += 1counts.token_counter += 1last_guessed = guessedlast_correct = correctlast_guessed_type = guessed_typelast_correct_type = correct_typeif in_correct:counts.correct_chunk += 1counts.t_correct_chunk[last_correct_type] += 1return countsdef uniq(iterable):seen = set()return [i for i in iterable if not (i in seen or seen.add(i))]def calculate_metrics(correct, guessed, total):tp, fp, fn = correct, guessed-correct, total-correctp = 0 if tp + fp == 0 else 1.*tp / (tp + fp)r = 0 if tp + fn == 0 else 1.*tp / (tp + fn)f = 0 if p + r == 0 else 2 * p * r / (p + r)return Metrics(tp, fp, fn, p, r, f)def metrics(counts):c = countsoverall = calculate_metrics(c.correct_chunk, c.found_guessed, c.found_correct)by_type = {}for t in uniq(list(c.t_found_correct) + list(c.t_found_guessed)):by_type[t] = calculate_metrics(c.t_correct_chunk[t], c.t_found_guessed[t], c.t_found_correct[t])return overall, by_typedef report(counts, out=None):if out is None:out = sys.stdoutoverall, by_type = metrics(counts)c = countsout.write('processed %d tokens with %d phrases; ' %(c.token_counter, c.found_correct))out.write('found: %d phrases; correct: %d.\n' %(c.found_guessed, c.correct_chunk))if c.token_counter > 0:out.write('accuracy: %6.2f%%; ' %(100.*c.correct_tags/c.token_counter))out.write('precision: %6.2f%%; ' % (100.*overall.prec))out.write('recall: %6.2f%%; ' % (100.*overall.rec))out.write('FB1: %6.2f\n' % (100.*overall.fscore))for i, m in sorted(by_type.items()):out.write('%17s: ' % i)out.write('precision: %6.2f%%; ' % (100.*m.prec))out.write('recall: %6.2f%%; ' % (100.*m.rec))out.write('FB1: %6.2f  %d\n' % (100.*m.fscore, c.t_found_guessed[i]))def report_notprint(counts, out=None):if out is None:out = sys.stdoutoverall, by_type = metrics(counts)c = countsfinal_report = []line = []line.append('processed %d tokens with %d phrases; ' %(c.token_counter, c.found_correct))line.append('found: %d phrases; correct: %d.\n' %(c.found_guessed, c.correct_chunk))final_report.append("".join(line))if c.token_counter > 0:line = []line.append('accuracy: %6.2f%%; ' %(100.*c.correct_tags/c.token_counter))line.append('precision: %6.2f%%; ' % (100.*overall.prec))line.append('recall: %6.2f%%; ' % (100.*overall.rec))line.append('FB1: %6.2f\n' % (100.*overall.fscore))final_report.append("".join(line))for i, m in sorted(by_type.items()):line = []line.append('%-17s:' % i)line.append('precision: %6.2f%%; ' % (100.*m.prec))line.append('recall: %6.2f%%; ' % (100.*m.rec))line.append('FB1: %6.2f  %d\n' % (100.*m.fscore, c.t_found_guessed[i]))final_report.append("".join(line))return final_reportdef end_of_chunk(prev_tag, tag, prev_type, type_):# check if a chunk ended between the previous and current word# arguments: previous and current chunk tags, previous and current typeschunk_end = Falseif prev_tag == 'E': chunk_end = Trueif prev_tag == 'S': chunk_end = Trueif prev_tag == 'B' and tag == 'B': chunk_end = Trueif prev_tag == 'B' and tag == 'S': chunk_end = Trueif prev_tag == 'B' and tag == 'O': chunk_end = Trueif (prev_tag == 'I' or prev_tag == 'M') and tag == 'B': chunk_end = Trueif (prev_tag == 'I' or prev_tag == 'M') and tag == 'S': chunk_end = Trueif (prev_tag == 'I' or prev_tag == 'M') and tag == 'O': chunk_end = Trueif prev_tag != 'O' and prev_tag != '.' and prev_type != type_:chunk_end = True# these chunks are assumed to have length 1if prev_tag == ']': chunk_end = Trueif prev_tag == '[': chunk_end = Truereturn chunk_enddef start_of_chunk(prev_tag, tag, prev_type, type_):# check if a chunk started between the previous and current word# arguments: previous and current chunk tags, previous and current typeschunk_start = Falseif tag == 'B': chunk_start = Trueif tag == 'S': chunk_start = Trueif prev_tag == 'E' and tag == 'E': chunk_start = Trueif prev_tag == 'E' and (tag == 'I' or tag == 'M'): chunk_start = Trueif prev_tag == 'S' and tag == 'E': chunk_start = Trueif prev_tag == 'S' and (tag == 'I' or tag == 'M'): chunk_start = Trueif prev_tag == 'O' and tag == 'E': chunk_start = Trueif prev_tag == 'O' and (tag == 'I' or tag == 'M'): chunk_start = Trueif tag != 'O' and tag != '.' and prev_type != type_:chunk_start = True# these chunks are assumed to have length 1if tag == '[': chunk_start = Trueif tag == ']': chunk_start = Truereturn chunk_startdef return_report(input_file):with codecs.open(input_file, "r", "utf8") as f:counts = evaluate(f)return report_notprint(counts)def main(argv):args = parse_args(argv[1:])if args.file is None:counts = evaluate(sys.stdin, args)else:with open(args.file) as f:counts = evaluate(f, args)report(counts)if __name__ == '__main__':sys.exit(main(sys.argv))

 


http://chatgpt.dhexx.cn/article/t3hSXkVT.shtml

相关文章

正则表达式字符数字匹配

基础知识 数字&#xff1a;[0-9]或者[\d],不止一个就用 字母&#xff1a;[a-z]或者[A-Z]区分大小写 |&#xff1a;或的意思 工具 在线测试网站 拿不准的先可以测试一下&#xff0c;输入输出如下&#xff1a; 实战 改名字&#xff0c;其中注意正则式的小括号括起来的才可…

1.3 正则表达式【匹配数字】

数字匹配符 \d \d 可以配置 0到9的整数&#xff0c;等价于上一节 中的 [0-9] 。 测试实例 被匹配字符串 private static final String test1 "a12adf31d2tt"; 匹配公式1 匹配公式&#xff1a; String expression1 "\\d"; 匹配结果&#xff1a; 匹…

1.4 正则表达式【匹配非数字】

数字匹配符 \D \D 可以配置非数字&#xff0c;等价于上一节 中的 [^0-9] 。 测试实例 被匹配字符串 private static final String test1 "a12adf31d2tt"; 匹配公式3 匹配公式 String expression3 "\\D"; 匹配结果 匹配公式4 匹配公式 String exp…

1.6 正则表达式【匹配非字母和数字】

字母和数字匹配符 \W \W 可以配置 非字母和数字&#xff0c;等价于 [^a-zA-Z0-9] 。 测试实例 被匹配字符串 private static final String test1 "a12.a,df3.1d-2tt.*"; 匹配公式3 匹配公式 String expression3 "\\W"; 匹配结果 匹配公式4 匹配…

Python正则表达式匹配字符串中的数字

导读这篇文章主要介绍了Python正则表达式匹配字符串中的数字&#xff0c;本文通过实例代码给大家介绍的非常详细&#xff0c;具有一定的参考借鉴价值,需要的朋友可以参考下 1.使用“\d”匹配全数字 代码&#xff1a; import re zen "Arizona 479, 501, 870. Carliforn…

正则表达式匹配数字、字母和汉字等各类汇总

最近在开发中遇到一个需求是只匹配字母和汉字&#xff0c;于是在网上找了一个比较全的记录一下。日后再用~ 正则表达式来匹配规范一段文本中的特定种类字符&#xff0c;下面是对常用的正则匹配做了一个归纳整理。 1、匹配中文:[\u4e00-\u9fa5] 2、英文字母:[a-zA-Z] 3、数字…

类加载机制、类加载顺序

1 类加载顺序 Java 的类加载过程可以分为 5 个阶段&#xff1a;载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的&#xff0c;但在动态绑定的情况下&#xff0c;解析阶段发生在初始化阶段之后。 1&#xff09;Loading&#xff08;载入&#xff09; JVM 在该阶段…

深入理解——Java类加载机制

我们知道&#xff0c;我们写的java文件是不能直接运行的&#xff0c;我们可以在IDEA中右键文件名点击运行&#xff0c;这中间其实掺杂了一系列的复杂处理过程。这篇文章&#xff0c;我们只讨论我们的代码在运行之前的一个环节&#xff0c;叫做类的加载。按照我写文章的常规惯例…

理解类加载机制

一般来说&#xff0c;我们日常的开发都是在IDE上进行的&#xff0c;这能让我们将更多的注意力放在业务的处理上&#xff0c;但是久而久之我们就忘记了其底层的实现原理。这是一把双刃剑&#xff0c;我们看不到底层实现&#xff0c;但是当有某些问题出现的时候&#xff0c;也只有…

谈谈类加载机制

前言 类的加载其实就是将.class文件加载的jvm的内存之中。在JVM中并不是一次性把所有的文件都加载到&#xff0c;而是一步一步的&#xff0c;按照需要来加载。JVM启动时会通过不同的类加载器加载不同的类&#xff0c;而且同一个类也不可能由多个加载器来进行加载。正是这种分级…

【JVM】详解类加载机制

JVM的类加载机制 一、类的生命周期二、类加载的过程1.加载2.连接3.初始化 三、类加载器的介绍3.1 启动类加载器&#xff08;根类加载器/引导类加载器&#xff09;&#xff08;Bootstrap ClassLoader&#xff09;3.2 扩展类加载器3.3 系统类加载器 四、双亲委派模型4.1 双亲委派…

tomcat类加载机制

目录 一、JVM类加载机制简介 二、TOMCAT类加载机制 三、违反双亲委托机制 一、JVM类加载机制简介 简述JVM双亲委派模型&#xff1a; JVM中包括集中类加载器&#xff1a; BootStrapClassLoader 引导类加载器ExtClassLoader 扩展类加载器AppClassLoader 应用类加载器Custom…

JAVA类加载机制详解

上一篇文章我们简单说了一下类的创建过程&#xff0c;但是如果JVM需要加载类&#xff0c;会经过哪些具体的过程呢&#xff1f;下面我们就来谈一谈。 要了解加载类的过程&#xff0c;我们就必须要了解类加载器。 在很多初学者刚听到类加载器的时候觉得很高大上&#xff0c;其实…

Android 类加载机制

1.类加载机制 .java文件不是可执行的文件,需要先编译成.class文件才可以被虚拟机执行。而类加载就是指通过类加载器把.class文件加载到虚拟机的内存空间,具体来说是方法区。类通常是按需加载,即第一次使用该类时才加载。 Java与Android都是把类加载到虚拟机内存中,然后由…

面试题:请介绍 JVM 类加载机制

JVM 类加载机制 Java 代码执行流程类的生命周期加载验证准备解析初始化clinit 和 init 方法 类加载的时机被动引用 类加载器双亲委派机制 我们在前面分析JVM架构解析的时候&#xff0c;简单介绍了 Java 类加载机制&#xff0c;本文带大家深入分析一下。 Java 代码执行流程 根据…

Tomcat 的类加载机制

在前面 Java虚拟机&#xff1a;对象创建过程与类加载机制、双亲委派模型 文章中&#xff0c;我们介绍了 JVM 的类加载机制以及双亲委派模型&#xff0c;双亲委派模型的类加载过程主要分为以下几个步骤&#xff1a; &#xff08;1&#xff09;初始化 ClassLoader 时需要指定自己…

java类加载机制

最近开通了一个订阅号 gexiaolong 在其中记录一些关于java总是记了又忘&#xff0c;忘了又记的一些知识点&#xff0c;所以还是写一篇日志记录一下吧 老规矩&#xff0c;抄作业&#xff0c;关于java的类加载机制的问题也是看了忘&#xff0c;忘了又在看 在此梳理记录一下 说…

类加载机制详解

一、类加载机制 Java虚拟机把描述类的数据从Class文件加载进内存&#xff0c;并对数据进行校验&#xff0c;转换解析和初始化&#xff0c;最终形成可以被虚拟机直接使用的Java类型&#xff0c;这就是虚拟机的类加载机制。 虚拟机设计团队把类加载阶段中的“通过一个类的全限定…

类的加载机制

文章目录 前言类加载的生命周期&#xff1a;加载&#xff08;Loading&#xff09;、验证&#xff08;Verification&#xff09;、准备&#xff08;PreParation&#xff09;、解析(Resolution)、初始化、使用、销毁&#xff0c;其中验证&#xff0c;准备&#xff0c;解析又叫做连…

类加载机制

1、JVM 和 类 &#xff08;1&#xff09;运行java命令&#xff1a; java 带有main方法的类名 ■ 命令的作用&#xff1a;启动jvm&#xff0c;并加载字节码&#xff0c;执行程序 当调用java命令来运行某个java程序时&#xff0c;该命令将会启动一个jvm进程&#xff08;同一个…