大模型应用:基于本地大模型的中文命名实体识别技术实践与应用 - 指南

一. 引言

        命名实体识别(NER)作为自然语言处理的基础任务,在信息抽取、知识图谱构建、智能问答等应用中具有重要作用。随着大模型技术的快速发展,基于本地部署的大模型在NER任务中展现出显著优势。本文通过两个典型示例——通用领域中文NER和医疗领域专用NER,深入探讨本地大模型在实际应用中的技术实现和性能表现。

        本地大模型相比云端API具有数据安全、响应速度快、可定制性强等优势。特别是在医疗、金融等敏感领域,本地部署能够确保数据不离开企业环境,满足严格的合规要求。同时,本地模型支持针对特定领域进行微调,显著提升在专业文本上的识别准确率。

二、通用领域中文NER实现

        在通用领域中文NER示例中,我们采用了基于RoBERTa的预训练模型,该模型在CLUENER2020数据集上进行了专门微调。CLUENER2020包含10种实体类型,涵盖了人名、地名、组织机构等常见实体类别。

        我们选用的uer/roberta-base-finetuned-cluener2020-chinese 是一个在细粒度中文NER任务上表现优秀的模型,特别适合处理新闻、社交媒体、企业信息等通用领域的中文文本。

1. 模型基本信息

  • 模型名称:uer/roberta-base-finetuned-cluener2020-chinese
  • 模型类型:基于RoBERTa的中文命名实体识别模型
  • 训练数据:CLUENER2020 细粒度中文NER数据集
  • 模型架构:
    • 基础架构:RoBERTa (Robustly Optimized BERT Pretraining Approach)
    • 参数规模:Base版本 (~110M 参数)
    • 层数:12层 Transformer
    • 隐藏层维度:768
    • 注意力头数:12
    • 最大序列长度:512
    • 词汇表大小:21128 (中文)

2. 训练数据集

训练数据集CLUENER2020 是一个细粒度的中文命名实体识别数据集,包含10种实体类型:

实体类型英文标签描述示例
地址address详细地址信息"北京市海淀区"
书籍book书籍、作品名称《红楼梦》
公司company公司、企业、机构"腾讯科技有限公司"
游戏game游戏、软件名称"王者荣耀"
政府government政府机构、部门"教育部"
电影movie电影、影视作品《流浪地球》
姓名name人物姓名"张三"
组织organization组织、团体"中国红十字会"
职位position职位、职务"首席执行官"
景点scene景点、景区"故宫博物院"

数据集统计:

  • 训练集: 10,748 个句子
  • 验证集: 1,343 个句子
  • 测试集: 1,345 个句子
  • 共计: 13,436 个句子

3. 模型性能

各实体类型的F1分数:

  • 地址 (address): ~65.38%
  • 书籍 (book): ~73.91%
  • 公司 (company): ~82.29%
  • 游戏 (game): ~81.94%
  • 政府 (government): ~86.84%
  • 电影 (movie): ~79.45%
  • 姓名 (name): ~88.37%
  • 组织 (organization): ~76.47%
  • 职位 (position): ~77.78%
  • 景点 (scene): ~66.67%

4. 标签映射

模型使用的BIO标注体系:

  • 0: "O",        # 非实体
  • 1: "B-address",     2: "I-address",      # 地址
  • 3: "B-book",          4: "I-book",            # 书籍
  • 5: "B-company",   6: "I-company",      # 公司
  • 7: "B-game",         8: "I-game",            # 游戏
  • 9: "B-government", 10: "I-government", # 政府
  • 11: "B-movie",      12: "I-movie",        # 电影
  • 13: "B-name",       14: "I-name",          # 姓名
  • 15: "B-organization", 16: "I-organization", # 组织
  • 17: "B-position",    18: "I-position",  # 职位
  • 19: "B-scene",       20: "I-scene"         # 景点

5. 示例分析

5.1 模型初始化部分

def __init__(self, model_name="uer/roberta-base-finetuned-cluener2020-chinese"):
    print(f"加载可靠NER模型: {model_name}")
    try:
        # 尝试直接加载huggingface模型
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForTokenClassification.from_pretrained(model_name)
    except:
        # 如果失败,通过modelscope下载
        model_path = snapshot_download(model_name)
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)
        self.model = AutoModelForTokenClassification.from_pretrained(model_path)

详细说明:

  • 双重加载机制:先尝试从HuggingFace直接加载,失败后通过ModelScope下载
  • AutoTokenizer:自动识别模型对应的分词器
  • AutoModelForTokenClassification:专门用于序列标注任务的模型类
  • snapshot_download:ModelScope的模型下载函数,会缓存到本地

5.2 标签映射系统

# CLUENER2020的标签映射
self.label_map = {
    0: 'O',
    1: 'B-address', 2: 'I-address',      # 地址
    3: 'B-book', 4: 'I-book',            # 书籍
    # ... 其他标签
}
# 简化标签映射到常用类型
self.simple_label_map = {
    'address': '地理位置',
    'company': '组织机构',
    'government': '组织机构',
    'name': '人物',
    # ... 其他映射
}
  • BIO标注体系:
    • B-:实体开始 (Begin)
    • I-:实体内部 (Inside)
    • O:非实体 (Outside)
  • 原始标签:CLUENER2020的10种细粒度实体类型
  • 简化映射:将相似类型合并,如company、government、organization都映射为"组织机构"

5.3 核心预测流程

def predict(self, text: str) -> Dict:
    # Tokenize
    inputs = self.tokenizer(
        text,
        return_tensors="pt",
        padding=True,
        truncation=True,
        max_length=512,
        return_offsets_mapping=True
    )
    # 移动到设备
    inputs = {k: v.to(self.device) for k, v in inputs.items()}
    offset_mapping = inputs.pop('offset_mapping').cpu().numpy()[0]
    # 预测
    with torch.no_grad():
        outputs = self.model(**inputs)
        predictions = torch.argmax(outputs.logits, dim=-1)[0].cpu().numpy()

参数详解:

  • return_tensors="pt":返回PyTorch张量
  • padding=True:自动填充到相同长度
  • truncation=True:超过最大长度时自动截断
  • max_length=512:BERT系列模型的最大输入长度
  • return_offsets_mapping=True:返回token在原始文本中的位置映射
  • torch.no_grad():禁用梯度计算,提高推理速度

5.4 实体提取算法

def _extract_entities(self, text: str, predictions: List[int], offset_mapping: List) -> List[Dict]:
    entities = []
    current_entity = None
    for i, (pred, offset) in enumerate(zip(predictions, offset_mapping)):
        # 跳过特殊token和填充token
        if offset[0] == offset[1] == 0:
            if current_entity:
                entities.append(current_entity)
                current_entity = None
            continue
        label = self.label_map.get(pred, 'O')
        if label.startswith('B-'):
            # 开始新实体
            if current_entity:
                entities.append(current_entity)
            entity_type = label[2:]
            simple_type = self.simple_label_map.get(entity_type, entity_type)
            start, end = offset
            current_entity = {
                'text': text[start:end],
                'type': simple_type,
                'start': start,
                'end': end,
                'raw_type': entity_type
            }
        elif label.startswith('I-') and current_entity:
            # 继续当前实体
            entity_type = label[2:]
            simple_type = self.simple_label_map.get(entity_type, entity_type)
            # 检查类型是否匹配
            if simple_type == current_entity['type']:
                _, end = offset
                current_entity['text'] = text[current_entity['start']:end]
                current_entity['end'] = end

算法逻辑详解:

初始状态: current_entity = None
                ↓
遇到 B-标签: 开始新实体
                ↓  
遇到 I-标签: 扩展当前实体(如果类型匹配)
                ↓
遇到 O标签: 结束当前实体
                ↓
遇到特殊token: 结束当前实体

关键处理:

  • 特殊token跳过:[CLS]、[SEP]、[PAD]等token的offset为(0,0)
  • 实体边界处理:使用offset_mapping精确定位原始文本位置
  • 类型一致性检查:确保I-标签与当前实体类型匹配
  • 实体合并:连续的B-I-I序列合并为一个完整实体

5.5 基于正则的备用方案

class SimpleButEffectiveNER:
    def __init__(self):
        self.patterns = {
            '人物': [
                r'[李王张刘陈杨赵黄周吴徐孙胡朱高林何郭马罗梁宋郑谢韩唐冯于董萧][\u4e00-\u9fa5]{1,3}'
            ],
            '组织机构': [
                r'[\u4e00-\u9fa5a-zA-Z0-9]{2,20}(公司|集团|银行|大学|学院|医院|政府|局|所|中心)'
            ],
            # ... 其他模式
        }

正则模式详解:

  • 人物匹配:[常见姓氏][1-3个中文字符]
  • 组织机构:[2-20个字符] + [机构后缀]
  • 地理位置:[2-10个中文字符] + [地理后缀] 或 [特定城市名]
  • 时间匹配:数字+年/月/日/世纪

5.6 输出数据结构

return {
    'text': text,  # 原始文本
    'entities': entities,  # 实体列表
    'entities_by_type': entities_by_type  # 按类型分组的实体
}
# 单个实体结构:
{
    'text': '马效云',           # 实体文本
    'type': '人物',           # 实体类型
    'start': 0,              # 起始位置
    'end': 2,                # 结束位置
    'raw_type': 'name'       # 原始标签类型
}

6. 完整实例

# -*- coding: utf-8 -*-
import torch
import re
from typing import List, Dict
from transformers import AutoTokenizer, AutoModelForTokenClassification
from modelscope import snapshot_download
class ReliableChineseNER:
    """中文命名实体识别"""
    def __init__(self, model_name="uer/roberta-base-finetuned-cluener2020-chinese"):
        """
        使用在CLUENER2020上微调过的模型,这个数据集专门用于中文NER
        """
        print(f"加载NER模型: {model_name}")
        try:
            # 尝试直接加载huggingface模型
            self.tokenizer = AutoTokenizer.from_pretrained(model_name)
            self.model = AutoModelForTokenClassification.from_pretrained(model_name)
        except:
            # 如果失败,通过modelscope下载
            model_path = snapshot_download(model_name)
            self.tokenizer = AutoTokenizer.from_pretrained(model_path)
            self.model = AutoModelForTokenClassification.from_pretrained(model_path)
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model.to(self.device)
        self.model.eval()
        # CLUENER2020的标签映射
        self.label_map = {
            0: 'O',
            1: 'B-address', 2: 'I-address',      # 地址
            3: 'B-book', 4: 'I-book',            # 书籍
            5: 'B-company', 6: 'I-company',      # 公司
            7: 'B-game', 8: 'I-game',            # 游戏
            9: 'B-government', 10: 'I-government', # 政府
            11: 'B-movie', 12: 'I-movie',        # 电影
            13: 'B-name', 14: 'I-name',          # 人名
            15: 'B-organization', 16: 'I-organization', # 组织
            17: 'B-position', 18: 'I-position',  # 职位
            19: 'B-scene', 20: 'I-scene'         # 场景
        }
        # 简化标签映射到常用类型
        self.simple_label_map = {
            'address': '地理位置',
            'company': '组织机构',
            'government': '组织机构',
            'name': '人物',
            'organization': '组织机构',
            'position': '职位',
            'scene': '场景'
        }
        print("NER模型加载完成!")
    def predict(self, text: str) -> Dict:
        """预测命名实体"""
        try:
            # Tokenize
            inputs = self.tokenizer(
                text,
                return_tensors="pt",
                padding=True,
                truncation=True,
                max_length=512,
                return_offsets_mapping=True
            )
            # 移动到设备
            inputs = {k: v.to(self.device) for k, v in inputs.items()}
            offset_mapping = inputs.pop('offset_mapping').cpu().numpy()[0]
            # 预测
            with torch.no_grad():
                outputs = self.model(**inputs)
                predictions = torch.argmax(outputs.logits, dim=-1)[0].cpu().numpy()
            # 处理结果
            entities = self._extract_entities(text, predictions, offset_mapping)
            # 按类型分组
            entities_by_type = {}
            for entity in entities:
                entity_type = entity['type']
                if entity_type not in entities_by_type:
                    entities_by_type[entity_type] = []
                entities_by_type[entity_type].append(entity)
            return {
                'text': text,
                'entities': entities,
                'entities_by_type': entities_by_type
            }
        except Exception as e:
            print(f"NER预测错误: {e}")
            return {'text': text, 'entities': [], 'entities_by_type': {}}
    def _extract_entities(self, text: str, predictions: List[int], offset_mapping: List) -> List[Dict]:
        """提取实体 - 改进版本"""
        entities = []
        current_entity = None
        for i, (pred, offset) in enumerate(zip(predictions, offset_mapping)):
            # 跳过特殊token和填充token
            if offset[0] == offset[1] == 0:
                if current_entity:
                    entities.append(current_entity)
                    current_entity = None
                continue
            label = self.label_map.get(pred, 'O')
            if label.startswith('B-'):
                # 开始新实体
                if current_entity:
                    entities.append(current_entity)
                entity_type = label[2:]
                # 转换为简化标签
                simple_type = self.simple_label_map.get(entity_type, entity_type)
                start, end = offset
                current_entity = {
                    'text': text[start:end],
                    'type': simple_type,
                    'start': start,
                    'end': end,
                    'raw_type': entity_type
                }
            elif label.startswith('I-') and current_entity:
                # 继续当前实体
                entity_type = label[2:]
                simple_type = self.simple_label_map.get(entity_type, entity_type)
                # 检查类型是否匹配
                if simple_type == current_entity['type']:
                    _, end = offset
                    current_entity['text'] = text[current_entity['start']:end]
                    current_entity['end'] = end
                else:
                    # 类型不匹配,结束当前实体
                    entities.append(current_entity)
                    current_entity = None
            else:
                # O标签或其他,结束当前实体
                if current_entity:
                    entities.append(current_entity)
                    current_entity = None
        # 添加最后一个实体
        if current_entity:
            entities.append(current_entity)
        return entities
def test_reliable_ner():
    """测试的NER模型"""
    print("=" * 60)
    print("NER模型测试")
    print("=" * 60)
    try:
        ner = ReliableChineseNER()
        test_texts = [
            "马云在杭州创立了阿里巴巴集团,该公司总部位于浙江省杭州市。",
            "苹果公司由史蒂夫·乔布斯于1976年4月1日在美国加利福尼亚州创立。",
            "李彦宏在北京创立了百度公司,百度是全球最大的中文搜索引擎。",
            "清华大学位于北京市海淀区,创建于1911年。",
            "周树人显示了他对中国传统文化的深刻理解,并对中国现代文学产生了深远的影响。"
        ]
        for i, text in enumerate(test_texts, 1):
            print(f"\n 示例 {i}: {text}")
            result = ner.predict(text)
            print(" 识别到的实体:")
            if result['entities']:
                for entity_type, entities in result['entities_by_type'].items():
                    entity_texts = [e['text'] for e in entities]
                    print(f" {entity_type}: {', '.join(entity_texts)}")
            else:
                print("  ℹ️  未识别到实体")
    except Exception as e:
        print(f"NER模型测试失败: {e}")
def test_multiple_models():
    """测试多个模型"""
    print("\n" + "=" * 60)
    print("多模型对比测试")
    print("=" * 60)
    models_to_test = [
        # ("uer/roberta-base-finetuned-cluener2020-chinese", "CLUENER微调模型"),
        ("hfl/chinese-bert-wwm-ext", "中文BERT"),
        # 如果上面的不行,尝试这个
    ]
    for model_name, description in models_to_test:
        print(f"\n 测试模型: {description}")
        try:
            # 简单的测试,不进行完整NER
            tokenizer = AutoTokenizer.from_pretrained(model_name)
            print(f"✅ {description} 加载成功")
        except Exception as e:
            print(f"❌ {description} 加载失败: {e}")
class SimpleButEffectiveNER:
    """简单但有效的中文NER"""
    def __init__(self):
        self.patterns = {
            '人物': [
                r'[李王张刘陈杨赵黄周吴徐孙胡朱高林何郭马罗梁宋郑谢韩唐冯于董萧][\u4e00-\u9fa5]{1,3}'
            ],
            '组织机构': [
                r'[\u4e00-\u9fa5a-zA-Z0-9]{2,20}(公司|集团|银行|大学|学院|医院|政府|局|所|中心)'
            ],
            '地理位置': [
                r'[\u4e00-\u9fa5]{2,10}[省市县区州国]',
                r'[北京|上海|天津|重庆|广州|深圳|杭州|南京|武汉|成都|西安|郑州|青岛|大连|厦门|苏州|宁波|无锡|佛山|东莞|沈阳|长沙|济南|哈尔滨|长春|石家庄|太原|合肥|南昌|福州|昆明|贵阳|兰州|银川|西宁|乌鲁木齐|呼和浩特|南宁|海口|香港|澳门|台北|台中|台南|高雄]{2,4}'
            ],
            '时间': [
                r'\d{4}年',
                r'\d{1,2}月',
                r'\d{1,2}日',
                r'\d{1,2}世纪'
            ]
        }
    def extract_entities(self, text: str) -> Dict:
        """使用正则表达式提取实体"""
        entities = []
        for entity_type, pattern_list in self.patterns.items():
            for pattern in pattern_list:
                matches = re.finditer(pattern, text)
                for match in matches:
                    entities.append({
                        'text': match.group(),
                        'type': entity_type,
                        'start': match.start(),
                        'end': match.end()
                    })
        # 去重
        unique_entities = []
        seen = set()
        for entity in entities:
            key = (entity['text'], entity['type'], entity['start'])
            if key not in seen:
                seen.add(key)
                unique_entities.append(entity)
        # 按类型分组
        entities_by_type = {}
        for entity in unique_entities:
            entity_type = entity['type']
            if entity_type not in entities_by_type:
                entities_by_type[entity_type] = []
            entities_by_type[entity_type].append(entity)
        return {
            'text': text,
            'entities': unique_entities,
            'entities_by_type': entities_by_type,
            'method': 'regex'
        }
def main():
    """主函数"""
    print("中文命名实体识别解决方案")
    print("=" * 60)
    # 测试NER模型
    test_reliable_ner()
if __name__ == "__main__":
    main()

输出结果:

============================================================
NER模型测试
============================================================
加载NER模型: uer/roberta-base-finetuned-cluener2020-chinese
NER模型加载完成!

示例 1: 马云在杭州创立了阿里巴巴集团,该公司总部位于浙江省杭州市。
识别到的实体:
  人物: 马云
  地理位置: 杭州, 浙江省杭州市
  组织机构: 阿里巴巴集团

示例 2: 苹果公司由史蒂夫·乔布斯于1976年4月1日在美国加利福尼亚州创立。
识别到的实体:
   组织机构: 苹果公司
   人物: 史蒂夫·乔布斯
   地理位置: 美国加利福尼亚州

示例 3: 李彦宏在北京创立了百度公司,百度是全球最大的中文搜索引擎。
识别到的实体:
  人物: 李彦宏
  地理位置: 北京
  组织机构: 百度公司

示例 4: 清华大学位于北京市海淀区,创建于1911年。
  识别到的实体:
  组织机构: 清华大学
  地理位置: 北京市海淀区

示例 5: 周树人显示了他对中国传统文化的深刻理解,并对中国现代文学产生了深远的影响。
  识别到的实体:
  人物: 周树人

7. 示例总结

  • 模型选择:使用在中文NER基准测试上表现优秀的RoBERTa模型
  • 双重保障:HuggingFace + ModelScope双加载路径
  • 精确映射:通过offset_mapping确保实体位置准确
  • 状态机算法:可靠的BIO序列解析
  • 备用方案:正则表达式作为降级方案
  • 完整输出:提供原始文本、实体列表、分组视图

三、医疗领域专用NER实现

        医疗文本具有高度的专业性和术语密度,通用NER模型往往难以准确识别医疗实体。基于CMEEE数据集的医疗专用NER模型通过领域自适应训练,显著提升了在医疗文本上的表现。

        iic/nlp_raner_named-entity-recognition_chinese-base-cmeee模型是一个基于BERT架构的命名实体识别模型,专门用于识别中文医疗文本中的实体。

1. 模型基本信息

  • 模型名称:iic/nlp_raner_named-entity-recognition_chinese-base-cmeee
  • 模型类型:基于BERT的中文医疗命名实体识别模型
  • 训练数据:CMEEE (Chinese Medical Entity Extraction) 数据集
  • 模型架构:
    • 专门领域:中文医疗文本
    • 基础架构:BERT (Bidirectional Encoder Representations from Transformers)
    • 预训练模型:bert-base-chinese
    • 参数规模:~110M 参数
    • 层数:12层 Transformer
    • 隐藏层维度:768
    • 注意力头数:12
    • 最大序列长度:512
    • 词汇表大小:21128 (中文)
    • 微调任务:医疗命名实体识别

2. 训练数据集

        CMEEE,全称Chinese Medical Entity Extraction,是一个专门的中文医疗实体抽取数据集,包含丰富的医疗实体类型:

实体类型英文标签描述示例
疾病dis疾病名称"糖尿病", "高血压"
症状sym临床症状"头痛", "发热"
药物dru药品名称"胰岛素", "阿司匹林"
检查pro检查检验项目"CT", "血常规"
身体部位bod解剖部位"心脏", "肺部"
科室dep医疗科室"内科", "急诊科"
治疗tre治疗方法"手术", "化疗"
微生物mic微生物种类"细菌", "病毒"
医疗设备equ医疗器械"呼吸机", "监护仪"

数据集特点:

  • 专业性强: 专门针对医疗领域设计
  • 覆盖面广: 包含门诊、住院、检查报告等多种医疗文本
  • 标注质量高: 由医学专业人员参与标注
  • 实体密度大: 医疗文本中实体出现频率高

3. 模型性能

各实体类型的典型F1分数:

  • 疾病 (dis): ~87%
  • 症状 (sym): ~85%
  • 药物 (dru): ~86%
  • 检查 (pro): ~84%
  • 身体部位 (bod): ~83%

4. 标签映射

模型使用标准的BIO标注格式:

  • 疾病相关
    • 0: "B-dis", 1: "I-dis",      # 疾病
    • 2: "B-sym", 3: "I-sym",      # 症状
  • 治疗相关
    • 4: "B-dru", 5: "I-dru",      # 药物
    • 6: "B-tre", 7: "I-tre",      # 治疗
    • 8: "B-pro", 9: "I-pro",      # 检查
  • 解剖相关
    • 10: "B-bod", 11: "I-bod",    # 身体部位
    • 12: "B-dep", 13: "I-dep",    # 科室
  • 其他医疗实体
    • 14: "B-mic", 15: "I-mic",    # 微生物
    • 16: "B-equ", 17: "I-equ",    # 设备
    • 18: "O"                      # 非实体

5. 示例分析

5.1 模型初始化与加载

class CMEEMedicalNER:
    def __init__(self, model_name="iic/nlp_raner_named-entity-recognition_chinese-base-cmeee"):
        print(f"加载医疗NER模型: {model_name}")
        try:
            # 下载模型
            model_dir = snapshot_download(model_name)
            print(f"模型下载完成: {model_dir}")
            # 创建pipeline
            self.ner_pipeline = pipeline(
                Tasks.named_entity_recognition,
                model=model_dir
            )

ModelScope集成:

  • snapshot_download(): ModelScope的核心下载函数,自动处理模型缓存和版本管理
  • 模型缓存机制: 首次下载后缓存到本地,后续使用无需重复下载
  • 自动依赖解析: 自动下载模型相关的配置文件和词汇表

Pipeline创建:

  • Tasks.named_entity_recognition: 明确指定命名实体识别任务
  • model=model_dir: 使用下载到本地的模型目录
  • 自动配置: Pipeline自动加载tokenizer、模型配置等

5.2 实体类型映射系统

# 实体类型映射
self.entity_type_mapping = {
    'dis': '疾病',
    'sym': '症状',
    'pro': '医疗程序',
    'equ': '医疗设备',
    'bod': '身体部位',
    'dur': '药物',
    'ite': '检查检验',
    'mic': '微生物',
    'dep': '科室',
    'phy': '医务人员'
}

映射关系详解:

英文标签中文含义示例
dis疾病糖尿病、高血压、肺炎
sym症状头痛、发热、咳嗽
dur药物胰岛素、阿司匹林
ite检查检验CT、血常规、心电图
bod身体部位头颅、胸部、肝脏
dep科室儿科、急诊科、内科
pro医疗程序手术、检查、治疗
equ医疗设备呼吸机、监护仪
mic微生物细菌、病毒
phy医务人员医生、护士

5.3 核心实体提取流程

def extract_entities(self, text: str) -> Dict:
    if self.ner_pipeline is None:
        return {"text": text, "entities": [], "entities_by_type": {}}
    try:
        # 使用pipeline进行实体识别
        result = self.ner_pipeline(text)
        # 处理结果
        entities = self._process_ner_result(result, text)
        # 按类型分组
        entities_by_type = self._group_entities_by_type(entities)

self.ner_pipeline is None 容错处理:

  • 模型加载失败保护: 确保即使模型加载失败,代码也不会崩溃
  • 一致性输出: 始终返回相同结构的结果

ner_pipeline 结果自动处理:

  • 文本分词和编码
  • 模型推理
  • 结果解码和后处理

5.4 结果处理的多格式适配

def _process_ner_result(self, result: Dict, text: str) -> List[Dict]:
    entities = []
    if 'output' not in result:
        return entities
    # 处理不同的输出格式
    if isinstance(result['output'], list):
        # 格式1: 直接是实体列表
        for entity in result['output']:
            if 'span' in entity and 'type' in entity:
                entity_type = self.entity_type_mapping.get(entity['type'], entity['type'])
                entities.append({
                    'text': entity['span'],
                    'type': entity_type,
                    'start': entity.get('start', text.find(entity['span'])),
                    'end': entity.get('end', text.find(entity['span']) + len(entity['span'])),
                    'score': entity.get('score', 1.0)
                })

直接列表格式:

  • {'span': '糖尿病', 'type': 'dis', 'start': 0, 'end': 3, 'score': 0.95},
  • {'span': '胰岛素', 'type': 'dur', 'start': 10, 'end': 13, 'score': 0.92}

字典包装格式:

  • 'entities': [
  •         {'span': '糖尿病', 'type': 'dis', 'start': 0, 'end': 3},
  •         {'span': '胰岛素', 'type': 'dur', 'start': 10, 'end': 13}
  •     ]

位置回退机制:

  • 优先使用模型输出的位置
  • 回退到字符串查找:当模型未提供位置信息时

5.5 智能去重算法

# 去重
unique_entities = []
seen = set()
for entity in entities:
    key = (entity['text'], entity['type'], entity['start'])
    if key not in seen:
        seen.add(key)
        unique_entities.append(entity)

去重逻辑:

  • 复合键去重:使用(文本, 类型, 起始位置)作为唯一标识
  • 避免重复:同一位置相同类型的实体只保留一个
  • 保持顺序:按识别顺序保留第一个出现的实体

5.6 医疗文本分析器

class MedicalTextAnalyzer:
    def __init__(self):
        self.ner = CMEEMedicalNER()
    def analyze_text(self, text: str) -> Dict:
        result = self.ner.extract_entities(text)
        # 统计分析
        stats = self._calculate_statistics(result)
        # 临床信息分析
        clinical_info = self._analyze_clinical_info(result)

统计分析:

  • 实体总数:反映文本信息密度
  • 类型分布:了解各类实体的出现频率
  • 实体密度:实体数/文本长度,衡量信息浓缩程度

临床四要素:

  • 疾病:诊断信息
  • 症状:临床表现
  • 药物:治疗方案
  • 检查:诊断依据

5.7 性能测试框架

def benchmark_performance():
    import time
    ner = CMEEMedicalNER()
    test_texts = [
        "患者发热咳嗽",
        "糖尿病胰岛素治疗",
        # ... 测试用例
    ]
    total_time = 0
    total_entities = 0
    for text in test_texts:
        start_time = time.time()
        result = ner.extract_entities(text)
        end_time = time.time()

性能指标:

  • 处理时间:单文本处理耗时(毫秒)
  • 实体识别率:平均每文本识别实体数
  • 吞吐量估算:基于平均时间的处理能力

6. 完整实例

# -*- coding: utf-8 -*-
import torch
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks
from modelscope import snapshot_download
from typing import List, Dict
import re
class CMEEMedicalNER:
    """基于CMEEE数据集的医疗命名实体识别"""
    def __init__(self, model_name="iic/nlp_raner_named-entity-recognition_chinese-base-cmeee"):
        print(f"加载医疗NER模型: {model_name}")
        try:
            # 下载模型
            model_dir = snapshot_download(model_name)
            print(f"模型下载完成: {model_dir}")
            # 创建pipeline
            self.ner_pipeline = pipeline(
                Tasks.named_entity_recognition,
                model=model_dir
            )
            # 实体类型映射
            self.entity_type_mapping = {
                'dis': '疾病',
                'sym': '症状',
                'pro': '医疗程序',
                'equ': '医疗设备',
                'bod': '身体部位',
                'dur': '药物',
                'ite': '检查检验',
                'mic': '微生物',
                'dep': '科室',
                'phy': '医务人员'
            }
            print("医疗NER模型加载成功!")
        except Exception as e:
            print(f"模型加载失败: {e}")
            self.ner_pipeline = None
    def extract_entities(self, text: str) -> Dict:
        """提取医疗实体"""
        if self.ner_pipeline is None:
            return {"text": text, "entities": [], "entities_by_type": {}}
        try:
            # 使用pipeline进行实体识别
            result = self.ner_pipeline(text)
            # 处理结果
            entities = self._process_ner_result(result, text)
            # 按类型分组
            entities_by_type = self._group_entities_by_type(entities)
            return {
                'text': text,
                'entities': entities,
                'entities_by_type': entities_by_type
            }
        except Exception as e:
            print(f"实体识别错误: {e}")
            return {"text": text, "entities": [], "entities_by_type": {}}
    def _process_ner_result(self, result: Dict, text: str) -> List[Dict]:
        """处理NER结果"""
        entities = []
        if 'output' not in result:
            return entities
        # 处理不同的输出格式
        if isinstance(result['output'], list):
            # 格式1: 直接是实体列表
            for entity in result['output']:
                if 'span' in entity and 'type' in entity:
                    entity_type = self.entity_type_mapping.get(entity['type'], entity['type'])
                    entities.append({
                        'text': entity['span'],
                        'type': entity_type,
                        'start': entity.get('start', text.find(entity['span'])),
                        'end': entity.get('end', text.find(entity['span']) + len(entity['span'])),
                        'score': entity.get('score', 1.0)
                    })
        elif isinstance(result['output'], dict):
            # 格式2: 包含在字典中
            output_data = result['output']
            if 'entities' in output_data:
                for entity in output_data['entities']:
                    if 'span' in entity and 'type' in entity:
                        entity_type = self.entity_type_mapping.get(entity['type'], entity['type'])
                        entities.append({
                            'text': entity['span'],
                            'type': entity_type,
                            'start': entity.get('start', text.find(entity['span'])),
                            'end': entity.get('end', text.find(entity['span']) + len(entity['span'])),
                            'score': entity.get('score', 1.0)
                        })
        # 去重
        unique_entities = []
        seen = set()
        for entity in entities:
            key = (entity['text'], entity['type'], entity['start'])
            if key not in seen:
                seen.add(key)
                unique_entities.append(entity)
        return unique_entities
    def _group_entities_by_type(self, entities: List[Dict]) -> Dict[str, List[Dict]]:
        """按类型分组实体"""
        grouped = {}
        for entity in entities:
            entity_type = entity['type']
            if entity_type not in grouped:
                grouped[entity_type] = []
            grouped[entity_type].append(entity)
        return grouped
class MedicalTextAnalyzer:
    """医疗文本分析器"""
    def __init__(self):
        self.ner = CMEEMedicalNER()
    def analyze_text(self, text: str) -> Dict:
        """分析医疗文本"""
        result = self.ner.extract_entities(text)
        # 统计分析
        stats = self._calculate_statistics(result)
        # 临床信息分析
        clinical_info = self._analyze_clinical_info(result)
        return {
            'extraction_result': result,
            'statistics': stats,
            'clinical_info': clinical_info
        }
    def _calculate_statistics(self, result: Dict) -> Dict:
        """计算统计信息"""
        entities = result['entities']
        stats = {
            'total_entities': len(entities),
            'entities_by_type': {},
            'entity_density': len(entities) / len(result['text']) * 100 if result['text'] else 0
        }
        for entity in entities:
            entity_type = entity['type']
            if entity_type in stats['entities_by_type']:
                stats['entities_by_type'][entity_type] += 1
            else:
                stats['entities_by_type'][entity_type] = 1
        return stats
    def _analyze_clinical_info(self, result: Dict) -> Dict:
        """分析临床信息"""
        entities = result['entities']
        # 检查关键临床信息是否存在
        has_disease = any(e['type'] == '疾病' for e in entities)
        has_symptom = any(e['type'] == '症状' for e in entities)
        has_drug = any(e['type'] == '药物' for e in entities)
        has_examination = any(e['type'] == '检查检验' for e in entities)
        completeness_score = sum([has_disease, has_symptom, has_drug, has_examination])
        return {
            'has_disease': has_disease,
            'has_symptom': has_symptom,
            'has_drug': has_drug,
            'has_examination': has_examination,
            'completeness_score': completeness_score,
            'completeness_percentage': (completeness_score / 4) * 100
        }
def test_cmee_medical_ner():
    """测试CMEEE医疗NER"""
    print("=" * 80)
    print("CMEEE医疗命名实体识别测试")
    print("=" * 80)
    analyzer = MedicalTextAnalyzer()
    test_cases = [
        "老年男性患者,因突发左侧肢体无力伴言语不清2小时入院。头颅CT提示右侧基底节区脑梗死。",
        "女性患者,45岁,因反复右上腹痛伴发热黄疸一周就诊。超声检查提示胆囊结石伴急性胆囊炎。",
        "患儿,3岁,因发热、咳嗽、气促3天就诊儿科。听诊双肺湿啰音,胸部X线显示支气管肺炎。",
        "患者主诉头痛、发热伴咳嗽三天,体温38.5℃,诊断为上呼吸道感染。",
        "糖尿病患者需要定期监测血糖,使用胰岛素控制血糖水平。",
        "患者因持续性胸痛和呼吸困难就诊急诊科,心电图显示心肌梗死,立即给予阿司匹林治疗。",
        "患者因咳嗽、咳痰伴胸痛就诊,胸部CT显示右肺上叶结节,建议进一步行支气管镜检查。",
        "高血压患者长期服用硝苯地平控制血压,近期出现头晕乏力症状。"
    ]
    for i, text in enumerate(test_cases, 1):
        print(f"\n 测试案例 {i}:")
        print(f"   {text}")
        analysis = analyzer.analyze_text(text)
        result = analysis['extraction_result']
        stats = analysis['statistics']
        clinical = analysis['clinical_info']
        print(" 识别结果:")
        if result['entities']:
            for entity_type, entities in result['entities_by_type'].items():
                entity_list = [e['text'] for e in entities]
                print(f"   ✅ {entity_type}: {', '.join(entity_list)}")
        else:
            print("   ❌ 未识别到实体")
        print(f"统计: {stats['total_entities']}个实体, 密度: {stats['entity_density']:.1f}%")
        print(f"临床完整性: {clinical['completeness_percentage']:.1f}%")
def compare_with_previous():
    """与之前问题案例对比"""
    print("\n" + "=" * 80)
    print("改进效果对比")
    print("=" * 80)
    ner = CMEEMedicalNER()
    # 之前有问题的案例
    problem_cases = [
        ("糖尿病患者需要定期监测血糖,使用胰岛素控制血糖水平。",
         "之前问题: 只识别出'胰岛素',漏掉'糖尿病'"),
        ("患者主诉头痛、发热伴咳嗽三天,体温38.5℃,诊断为上呼吸道感染。",
         "之前问题: 将'患者'、'三天'误识别为症状"),
        ("头颅CT提示右侧基底节区脑梗死。",
         "之前问题: 将'右侧'误识别为疾病")
    ]
    for text, problem_desc in problem_cases:
        print(f"\n {problem_desc}")
        print(f"   文本: {text}")
        result = ner.extract_entities(text)
        print("✅ 改进后结果:")
        if result['entities']:
            for entity_type, entities in result['entities_by_type'].items():
                entity_list = [e['text'] for e in entities]
                print(f"   {entity_type}: {', '.join(entity_list)}")
        else:
            print("   未识别到实体")
def test_complex_medical_cases():
    """测试复杂医疗案例"""
    print("\n" + "=" * 80)
    print("复杂医疗案例测试")
    print("=" * 80)
    analyzer = MedicalTextAnalyzer()
    complex_cases = [
        "老年男性,因突发右侧肢体活动障碍伴言语不清4小时入院。头颅MRI显示左侧基底节区急性脑梗死,NIHSS评分15分。给予阿司匹林、氯吡格雷双抗治疗,并予阿托伐他汀强化降脂。",
        "女性患者,68岁,因反复胸痛、胸闷1周,加重2小时急诊入院。心电图示II、III、aVF导联ST段抬高,心肌酶谱异常,诊断为急性下壁心肌梗死。行急诊PCI术,于右冠脉植入支架1枚。",
        "患儿,5岁,因发热、皮疹3天就诊。体检:体温39.2℃,咽部充血,扁桃体II度肿大,见脓性分泌物,全身可见弥漫性红色斑丘疹。血常规示白细胞18.5×10^9/L,中性粒细胞85%。诊断为猩红热,给予青霉素抗感染治疗。",
        "患者因右上腹持续性胀痛伴皮肤黄染、尿色加深1月余就诊。腹部CT提示肝内胆管扩张,肝门部占位,考虑肝门部胆管癌。行ERCP检查,留置胆道支架引流。"
    ]
    for i, text in enumerate(complex_cases, 1):
        print(f"\n 复杂案例 {i}:")
        print(f"   {text}")
        analysis = analyzer.analyze_text(text)
        result = analysis['extraction_result']
        print(" 识别结果:")
        if result['entities']:
            # 只显示主要的医疗实体类型
            medical_types = ['疾病', '症状', '药物', '检查检验', '身体部位', '科室', '医疗程序']
            for entity_type in medical_types:
                if entity_type in result['entities_by_type']:
                    entities = result['entities_by_type'][entity_type]
                    entity_list = [e['text'] for e in entities]
                    print(f"  {entity_type}: {', '.join(entity_list)}")
        clinical = analysis['clinical_info']
        print(f" 临床完整性: {clinical['completeness_percentage']:.0f}%")
def benchmark_performance():
    """性能基准测试"""
    print("\n" + "=" * 80)
    print("性能基准测试")
    print("=" * 80)
    import time
    ner = CMEEMedicalNER()
    test_texts = [
        "患者发热咳嗽",
        "糖尿病胰岛素治疗",
        "头痛伴呕吐腹泻",
        "胸部CT显示肺炎",
        "高血压服用硝苯地平"
    ]
    total_time = 0
    total_entities = 0
    for text in test_texts:
        start_time = time.time()
        result = ner.extract_entities(text)
        end_time = time.time()
        processing_time = (end_time - start_time) * 1000  # 转换为毫秒
        total_time += processing_time
        total_entities += len(result['entities'])
        print(f" '{text}' -> {len(result['entities'])}个实体, 耗时: {processing_time:.1f}ms")
    avg_time = total_time / len(test_texts)
    avg_entities = total_entities / len(test_texts)
    print(f"\n 平均性能: {avg_time:.1f}ms/文本, {avg_entities:.1f}个实体/文本")
def main():
    """主函数"""
    print("基于CMEEE的医疗命名实体识别系统")
    print("=" * 80)
    print("\n模型信息:")
    print("  模型: iic/nlp_raner_named-entity-recognition_chinese-base-cmeee")
    print("  数据集: CMEEE (Chinese Medical Entity Extraction)")
    print("  实体类型: 疾病(diss), 症状(sym), 药物(dur), 检查检验(ite), 身体部位(bod)等")
    # 运行测试
    test_cmee_medical_ner()
    compare_with_previous()
    test_complex_medical_cases()
    benchmark_performance()
    print("\n" + "=" * 80)
    print("测试完成!")
    print("=" * 80)
if __name__ == "__main__":
    main()

输出结果:

基于CMEEE的医疗命名实体识别系统
=====================================================================

模型信息:
  模型: iic/nlp_raner_named-entity-recognition_chinese-base-cmeee
  数据集: CMEEE (Chinese Medical Entity Extraction)
  实体类型: 疾病(diss), 症状(sym), 药物(dur), 检查检验(ite), 身体部位(bod)等    
=====================================================================
CMEEE医疗命名实体识别测试
=====================================================================
加载医疗NER模型: iic/nlp_raner_named-entity-recognition_chinese-base-cmeee      
Downloading Model from https://www.modelscope.cn to directory: C:\Users\du\.cache\modelscope\hub\models\iic\nlp_raner_named-entity-recognition_chinese-base-cmeee
2025-11-21 21:42:48,174 - modelscope - WARNING - Model revision not specified, use revision: v1.0.0
模型下载完成: C:\Users\du\.cache\modelscope\hub\models\iic\nlp_raner_named-entity-recognition_chinese-base-cmeee
医疗NER模型加载成功!

测试案例 1:
   老年男性患者,因突发左侧肢体无力伴言语不清2小时入院。头颅CT提示右侧基底节区脑梗死。
识别结果:
   ✅ 症状: 左侧肢体无力, 言语不清
   ✅ 医疗程序: 头颅CT
   ✅ 疾病: 右侧基底节区脑梗死
统计: 4个实体, 密度: 9.3%
临床完整性: 50.0%

测试案例 2:
   女性患者,45岁,因反复右上腹痛伴发热黄疸一周就诊。超声检查提示胆囊结石伴急性胆囊炎。
 识别结果:
   ✅ 症状: 反复右上腹痛, 发热, 黄疸
   ✅ 医疗程序: 超声检查
   ✅ 疾病: 胆囊结石, 急性胆囊炎
 统计: 6个实体, 密度: 14.0%
 临床完整性: 50.0%

测试案例 3:
   患儿,3岁,因发热、咳嗽、气促3天就诊儿科。听诊双肺湿啰音,胸部X线显示支气管肺炎。
识别结果:
   ✅ 症状: 发热, 咳嗽, 气促, 双肺湿啰音
   ✅ 科室: 儿科
   ✅ 医疗程序: 胸部X线
   ✅ 疾病: 支气管肺炎
统计: 7个实体, 密度: 16.7%
临床完整性: 50.0%

测试案例 4:
   患者主诉头痛、发热伴咳嗽三天,体温38.5℃,诊断为上呼吸道感染。
识别结果:
   ✅ 症状: 头痛, 发热, 咳嗽, 体温38.5℃
   ✅ 疾病: 上呼吸道感染
统计: 5个实体, 密度: 15.2%
临床完整性: 50.0%

测试案例 5:
   糖尿病患者需要定期监测血糖,使用胰岛素控制血糖水平。
 识别结果:
   ✅ 疾病: 糖尿病
   ✅ 检查检验: 血糖, 血糖
   ✅ dru: 胰岛素
统计: 4个实体, 密度: 15.4%
临床完整性: 50.0%

测试案例 6:
   患者因持续性胸痛和呼吸困难就诊急诊科,心电图显示心肌梗死,立即给予阿司匹林治疗。
识别结果:
   ✅ 症状: 持续性胸痛, 呼吸困难
   ✅ 科室: 急诊科
   ✅ 医疗程序: 心电图
   ✅ 疾病: 心肌梗死
   ✅ dru: 阿司匹林
统计: 6个实体, 密度: 15.0%
临床完整性: 50.0%

测试案例 7:
   患者因咳嗽、咳痰伴胸痛就诊,胸部CT显示右肺上叶结节,建议进一步行支气管镜检查。
识别结果:
   ✅ 症状: 咳嗽, 咳痰, 胸痛, 右肺上叶结节
   ✅ 医疗程序: 胸部CT, 支气管镜检查
统计: 6个实体, 密度: 15.0%
临床完整性: 25.0%

测试案例 8:
   高血压患者长期服用硝苯地平控制血压,近期出现头晕乏力症状。
识别结果:
   ✅ 疾病: 高血压
   ✅ dru: 硝苯地平
   ✅ 检查检验: 血压
   ✅ 症状: 头晕乏力
统计: 4个实体, 密度: 13.8%
临床完整性: 75.0%

=====================================================================
改进效果对比
=====================================================================

之前问题: 只识别出'胰岛素',漏掉'糖尿病'
   文本: 糖尿病患者需要定期监测血糖,使用胰岛素控制血糖水平。
✅ 改进后结果:
   疾病: 糖尿病
   检查检验: 血糖, 血糖
   dru: 胰岛素

之前问题: 将'患者'、'三天'误识别为症状
   文本: 患者主诉头痛、发热伴咳嗽三天,体温38.5℃,诊断为上呼吸道感染。
✅ 改进后结果:
   症状: 头痛, 发热, 咳嗽, 体温38.5℃
   疾病: 上呼吸道感染

之前问题: 将'右侧'误识别为疾病
   文本: 头颅CT提示右侧基底节区脑梗死。
✅ 改进后结果:
   医疗程序: 头颅CT
   疾病: 右侧基底节区脑梗死

=====================================================================
复杂医疗案例测试
=====================================================================
复杂案例 1:
   老年男性,因突发右侧肢体活动障碍伴言语不清4小时入院。头颅MRI显示左侧基底节区急性脑梗死,NIHSS评分15分。给予阿司匹林、氯吡格雷双抗治疗,并予阿托伐他汀强化降脂。
识别结果:
     疾病: 急性脑梗死
     症状: 突发右侧肢体活动障碍, 言语不清
     医疗程序: 头颅MRI, 强化降脂
临床完整性: 50%

复杂案例 2:
   女性患者,68岁,因反复胸痛、胸闷1周,加重2小时急诊入院。心电图示II、III、aVF导联ST段抬高,心肌酶谱异常,诊断为急性下壁心肌梗死。行急诊PCI术,于右冠脉植入支架1枚。
识别结果:
     疾病: 急性下壁心肌梗死
     症状: 胸痛, 胸闷, II、III、aVF导联ST段抬高, 心肌酶谱异常
     身体部位: 右冠脉
     医疗程序: 心电图, 急诊PCI术
临床完整性: 50%

复杂案例 3:
   患儿,5岁,因发热、皮疹3天就诊。体检:体温39.2℃,咽部充血,扁桃体II度肿大,见脓性分泌物,全身可见弥漫性红色斑丘疹。血常规示白细胞18.5×10^9/L,中性粒细胞85%。诊断为猩红热,给予青霉素抗感染治疗。
识别结果:
     疾病: 猩红热
     症状: 发热, 皮疹, 体温39.2℃, 咽部充血, 扁桃体II度肿大, 见脓性分泌物, 全身可见弥漫性红色斑丘疹
     检查检验: 血常规, 白细胞, 中性粒细胞
临床完整性: 75%

复杂案例 4:
   患者因右上腹持续性胀痛伴皮肤黄染、尿色加深1月余就诊。腹部CT提示肝内胆管扩张,肝门部占位,考虑肝门部胆管癌。行ERCP检查,留置胆道支架引流。
识别结果:
     疾病: 肝门部胆管癌
     症状: 右上腹持续性胀痛, 皮肤黄染, 尿色加深, 肝内胆管扩张, 肝门部占位
     医疗程序: 腹部CT, ERCP检查, 留置胆道支架引流
临床完整性: 50%

=====================================================================
性能基准测试
=====================================================================
'患者发热咳嗽' -> 2个实体, 耗时: 601.0ms
'糖尿病胰岛素治疗' -> 2个实体, 耗时: 585.0ms
'头痛伴呕吐腹泻' -> 3个实体, 耗时: 583.0ms
'胸部CT显示肺炎' -> 2个实体, 耗时: 559.0ms
'高血压服用硝苯地平' -> 1个实体, 耗时: 590.0ms

平均性能: 583.6ms/文本, 2.0个实体/文本

7. 流程总结

输入文本
    ↓
CMEEE医疗NER模型
    ↓
实体识别结果(多格式)
    ↓
统一结果处理
    ├── 类型映射(英文→中文)
    ├── 位置信息处理
    ├── 置信度整合
    └── 去重处理
    ↓
结构化输出
    ├── 实体列表
    ├── 按类型分组
    └── 统计分析
    ↓
临床信息评估
    ├── 疾病识别
    ├── 症状识别  
    ├── 药物识别
    └── 检查识别

四、总结

        基于本地大模型的命名实体识别技术在实际应用中展现出显著优势。通过通用领域和医疗领域的两个典型示例,我们验证了本地模型在准确性、响应速度、数据安全等方面的卓越表现。随着模型优化技术的不断进步和硬件成本的持续下降,本地大模型必将在更多场景中发挥重要作用,为企业智能化转型提供坚实的技术基础。

        本地部署不仅解决了数据隐私和合规性问题,还通过领域自适应大幅提升了专业场景的识别精度。这种技术路径为各行业的NER应用提供了可靠的技术方案,具有广阔的推广应用前景。

posted @ 2025-12-18 10:45  yangykaifa  阅读(64)  评论(0)    收藏  举报