一、概念

        命名实体识别(Named Entity Recognition,NER)中的“命名实体”一般是指文本中具有特别意义或者指代性非常强的实体,可分为三大类(实体类、时间类和数字类)和七小类(人名、机构名、地名、时间、日期、货币和百分比)。

        命名实体识别的任务就是识别出文本中的命名实体,通常分为两个过程:实体边界识别和实体类别的确定。

二、问题转化

中文命名实体识别的本质就是序列标注。

        设定3种命名实体标注符号PERLOCORG分别代表人名、地名、机构名;3种命名实体标注符号BIO分别代表实体开始、实体中间、其它。

        这样就有7种标记L={B-PER,I-PER,B-LOC,I-LOC,B-ORG,I-ORG,O},这样就把命名实体识别转换成了每个字的分类问题。例如:

尼克松是一个人名吧,那就把开始的一个字‘尼’标注为 实体开始 ,即B-PER,其他的字标注为实体中间,即I-PER,依次类推标注其他命名实体。

这样就可以用分类模型去做了吧,但要注意以下几点:

  • 中文词灵活多变:有些词语在不同语境下可能是不同的实体类型,辽宁有个市叫“沈阳”,也有一些人名叫“沈阳”。有些取名叫“高富帅”,但“高富帅” 是一个现代流行的形容词。
  • 中文词的嵌套情况复杂:一些中文的命名实体中常常嵌套另外一个命名实体,如“北京大学附属中学”
  • 中文词存在简化表达现象:通常对一些较长的命名实体词进行简化表达,如“北京大学”通常简化为“北大”,“北京大学附属中学”通常简化为“北大附中”

所以也并不是一个简单的分类,要把这些问题都考虑进模型中。

三、方法

主要有三大类方法

有了方法,那就构建模型呗,下面是基于统计的模型

四、CRF模型

这里简单介绍一下CRF模型,

        **条件随机场(Conditional Random Fields, CRF)**是一种用于序列标注的概率图模型,广泛应用于自然语言处理任务,如命名实体识别(NER)、词性标注等。   

        CRF模型思想主要来源于最大熵模型。观察序列 X 和标注序列 Y 的条件概率 P(Y∣X),直接最大化条件概率来训练模型,相对于HMM和最大熵马尔可夫模型(MEMM),CRF模型它没有HMM那样严格的独立性假设,克服了MEMM标记偏置的缺点。

  • 序列标注任务:给定输入序列 ,预测对应标注序列
  • CRF假设 Y 的条件概率可以通过一个图结构建模,常用的是线性链CRF。

1. 特征设计

        因为输入序列X一般是一段文本序列,机器学习算法是不能直接使用的,需要将它们转化成机器学习算法可以识别的数值特征,然后再交给机器学习的算法进行操作。对于序列标注问题,需要先对输入序列进行特征提取

        "设随机变量X取值记为x,Y取值记为y。序列x的特征提取是通过构建特征函数来完成的。特征函数包含四个参数,分别为文本x、参数i(表示文本x中第i个词)、y_{i}(第i个词的标注值)和y_{i+1}(第i+1个词的标注值)。因为linear-CRF 满足马尔科夫性,所以只有上下文相关的局部特征函数,没有不相邻节点之间的特征函数。

  

那么特征函数可以按照以下去定义,主要有两类:状态特征函数s和转移特征函数t,其中s特征函数只和当前节点有关,t特征函数与当前节点和上一个节点有关。

例如:假设x=“我去广州旅游”,标注的状态集合为S={B,I.O},分别代表命名实体的开始、实体中间、其它。设定两个特征函数s、t。

对于“广”字来说:S是一个标签集合,里面有三个标签,对于每个标签类都有一个值(0或者1)去表示,即s特征函数。t函数就是yi-1和yi满足某种搭配条件,这个特征函数考虑了当前节点和上一个节点。

   

2. 模型构建

线性链CRF的条件概率分布定义为:

其中:

  • f_{k}​:特征函数,描述标注y_{t-1}y{_{t}}^{}和观察序列 X 的关系。
  • \lambda _{k}:特征权重,通过训练学习得到。
  • Z(X):归一化因子,是所有可能标记序列的和。

3. 模型训练

训练目标是最大化条件概率 P(Y∣X)

即最小化对数似然损失:

min

有了目标函数,那就用最优化的方法求呗,通过梯度下降(如LBFGS、AdaGrad等)求得最优参数 \lambda _{k}​。

五、当前主流的命名实体识别(NER)模型

随着深度学习的发展,NER任务从传统机器学习逐渐转向深度学习,当前主流的NER模型可以分为以下几类:

1. 基于BiLSTM-CRF的模型

这种模型结合了双向LSTM和CRF的优势:

  • BiLSTM:从上下文中捕捉长距离依赖信息。

  • CRF:对输出序列进行全局优化,解决标注一致性问题。

特点

  • 高效处理序列依赖。

  • 已成为NER的经典深度学习基线模型。

2. 基于Transformer的模型

Transformer是当前最主流的架构,尤其是基于预训练模型的NER方法取得了显著进展。

  • BERT-based NER:BERT预训练模型通过Fine-tuning用于NER,利用其强大的上下文表示能力。

  • RoBERTa, GPT, T5等模型也可用于NER任务。

特点

  • 不需要额外的特征工程。

  • 对低资源场景表现出色。

3. 基于混合架构的模型

  • CNN-BiLSTM-CRF:结合CNN捕捉局部特征、BiLSTM获取全局特征,以及CRF的序列标注能力。
  • Transformer + CRF:在Transformer输出层加上CRF,进一步增强序列依赖建模。

六、使用sklearn-crfsuite库进行命名实体识别的代码实现

        中文命名实体识别中,比较常见的是对文本中的时间、人名、地名及组织机构名四种类型进行识别。

        sklearn-crfsuite是基于CRFsuite库的一款轻量级的CRF库,提供了条件随机场的训练、预测及评测方法。该库可兼容sklearn算法,因此可以结合sklearn库的算法设计命名实体识别系统。CRFsuite 是基于C/C++实现了条件随机场模型,可用于快速训练和序列标注。

代码运行准备前工作!!!!看重点

1、降级numpy,降到1.x版本,因为有的模块可能不支持2.x版本。

如果你用的pycharm,在pycharm里找到设置,在右侧找到:

接着找到numpy和上边“-”卸载,再点“+”重新安装:

2、安装其他用到的库,输入代码后,代码检查器会自动提示你没有哪些库,按照上边的安装即可。

3、修改代码中训练数据路径,我用的是1980_01rmrb.txt文本进行训练的,我也会把这个数据放到文章末尾;修改保存预处理后的文本数据路径和名字,我用的wzm.txt保存的;修改保存的模型参数文件路径和名字,我用的wzm.pkl。而且他们都在一个data的文件夹下,所以路径这点小问题你就看着办吧。

        我建议你在你代码所在路径下新建一个data文件夹,然后只用把1980_01rmrb.txt文件放进去,其他不用管。

啰嗦这么多,终于上代码。第一次训练时间有点长,没事,也很快。

训练数据格式:词/词性  以空格分开

# 代码3-22
import joblib
import sklearn
import sklearn_crfsuite
from sklearn_crfsuite import metrics
class CorpusProcess(object):
    def __init__(self):
        # 定义属性 process_corpus_path 和 raw_corpus_path
        self.process_corpus_path = r"data\wzm.txt"
        self.raw_corpus_path = r"data\1980_01rmrb.txt"
        self.tag_seq = []
        self.word_seq = []
        self._maps = {
            "nr": "PER",  # 人名
            "ns": "LOC",  # 地名
            "nt": "ORG",  # 机构名
            # 可以根据具体需求补充映射规则
        }


# 由词性提取标签
    def pos_to_tag(self, p):
        t = self._maps.get(p, None)
        return t if t else 'O'
# 标签使用BIO模式
    def tag_perform(self, tag, index):
        if index == 0 and tag != 'O':
            return 'B_{}'.format(tag)
        elif tag != 'O':
            return 'I_{}'.format(tag)
        else:
            return tag
#全角转半角
    def q_to_b(self, q_str):
        b_str = ""
        for uchar in q_str:
            inside_code = ord(uchar)
            if inside_code == 12288:  # 全角空格直接转换
                inside_code = 32
            elif 65374 >= inside_code >= 65281:  # 全角字符(除空格)根据关系转化
                inside_code -= 65248
            b_str += chr(inside_code)
        return b_str

# 语料初始化
    def initialize(self):
        self.process_corpus_path = r"data\wzm.txt"
        lines = self.read_corpus_from_file(self.process_corpus_path)
        words_list = [line.strip().split('  ') for line in lines if line.strip()]
        del lines
        self.init_sequence(words_list)

# 初始化字序列、词性序列
    def init_sequence(self, words_list):
        words_seq = [[word.split('/')[0] for word in words] for words in words_list]
        pos_seq = [[word.split('/')[1] for word in words] for words in words_list]
        tag_seq = [[self.pos_to_tag(p) for p in pos] for pos in pos_seq]
        self.tag_seq = [[[self.tag_perform(tag_seq[index][i], w)
                          for w in range(len(words_seq[index][i]))]
                            for i in range(len(tag_seq[index]))]
                                for index in range(len(tag_seq))]
        self.tag_seq = [[t for tag in tag_seq for t in tag] for tag_seq in self.tag_seq]
        self.word_seq = [['<BOS>'] + [w for word in word_seq for w in word]
                         + ['<EOS>'] for word_seq in words_seq]
# 窗口切分
    def segment_by_window(self, words_list, window=3):
        words = []
        begin, end = 0, window
        for _ in range(1, len(words_list)):
            if end > len(words_list):
                break
            words.append(words_list[begin: end])
            begin = begin + 1
            end = end + 1
        return words
# 特征提取
    def extract_feature(self, word_grams):
        features, feature_list = [], []
        for index in range(len(word_grams)):
            for i in range(len(word_grams[index])):
                word_gram = word_grams[index][i]
                feature = {'w-1': word_gram[0],
                           'w': word_gram[1], 'w+1': word_gram[2],
                           'w-1:w': word_gram[0] + word_gram[1],
                           'w:w+1': word_gram[1] + word_gram[2],
                           'bias': 1.0}
                feature_list.append(feature)
            features.append(feature_list)
            feature_list = []
        return features
# 训练数据
    def generator(self):
        word_grams = [self.segment_by_window(word_list) for word_list in self.word_seq]
        features = self.extract_feature(word_grams)
        return features, self.tag_seq
# 读取文件
    def read_corpus_from_file(self, process_corpus_path):
        with open(process_corpus_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        return lines

    def pre_process(self):
        self.raw_corpus_path = r"data\1980_01rmrb.txt"
        # 加载原始语料文件
        lines = self.read_corpus_from_file(self.raw_corpus_path)
        # 清洗和规范化处理
        processed_lines = [self.q_to_b(line.strip()) for line in lines if line.strip()]
        # 保存预处理结果到新的路径
        with open(self.process_corpus_path, 'w', encoding='utf-8') as f:
            f.writelines('\n'.join(processed_lines))


# 代码3-23
class  CRF_NER(object):
# 初始化CRF模型参数
    def __init__(self):
        self.algorithm = 'lbfgs'
        self.c1 = '0.1'
        self.c2 = '0.1'
        self.max_iterations = 100  # 迭代次数
        self.model_path = 'data/wzm.pkl'
        self.corpus = CorpusProcess()  # 加载语料预处理模块
        self.model = None
# 定义模型
    def initialize_model(self):
        self.corpus.pre_process()  # 语料预处理
        self.corpus.initialize()  # 初始化语料
        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, y = self.corpus.generator()
        x_train, y_train = x[500:], y[500:]
        x_test, y_test = x[:500], y[:500]
        self.model.fit(x_train, y_train)

        # 获取标签
        labels = list(self.model.classes_)
        labels.remove('O')

        # 预测
        y_predict = self.model.predict(x_test)

        # 计算 F1 分数
        f1_score = metrics.flat_f1_score(
            y_test, y_predict, average='weighted', labels=labels
        )
        print(f"F1 Score: {f1_score}")

        # 分类报告
        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
        ))

        # 保存模型
        joblib.dump(self.model, self.model_path)


    def predict(self, sentence):
        # 尝试加载模型
        try:
            self.model = joblib.load(self.model_path)
        except FileNotFoundError:
            raise FileNotFoundError(f"模型文件未找到: {self.model_path}")
        except EOFError:
            raise EOFError(f"模型文件损坏或不完整: {self.model_path}")
        except Exception as e:
            raise RuntimeError(f"加载模型时发生未知错误: {e}")

        # 预处理输入语句
        u_sent = self.corpus.q_to_b(sentence)
        word_lists = [['<BOS>'] + [c for c in u_sent] + ['<EOS>']]
        word_grams = [
            self.corpus.segment_by_window(word_list) for word_list in word_lists]
        features = self.corpus.extract_feature(word_grams)

        # 使用模型预测
        y_predict = self.model.predict(features)
        entity = ''
        for index in range(len(y_predict[0])):
            if y_predict[0][index] != 'O':
                if index > 0 and (
                        y_predict[0][index][-1] != y_predict[0][index - 1][-1]):
                    entity += ' '
                entity += u_sent[index]
            elif entity and entity[-1] != ' ':
                entity += ''
        return entity



# 代码3-24:主程序逻辑,确保训练后再进行预测
import os
# 初始化实体识别类
ner = CRF_NER()
# 检查模型是否存在
if not os.path.exists(ner.model_path):
    print(f"模型文件未找到: {ner.model_path},开始训练模型...")
    ner.train()
    print("模型训练完成!")

# 进行预测
sentence1 = '香港回归23周年升旗仪式今天(7月1日)8时在金紫荆广场举行。行政长官'\
            '林郑月娥、一众特区政府官员、中央机构驻港代表及外国驻港使节等约100人出席。'
output1 = ner.predict(sentence1)
print("预测结果:", output1)

训练数据下载,记得替换文件名为1980_01rmrb.txt

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐