在这里插入图片描述


一、引言

在当代医疗实践中,电子病历(Electronic Medical Record, EMR)不仅是患者诊疗信息的载体,更是临床决策、科研分析和医院管理的基石。然而,繁琐的病历文书工作已成为全球医生职业倦怠(Burnout)的首要原因。据统计,医生平均每天花费 1.5 至 2 小时 书写病历,占用了大量本该用于床旁诊疗的时间,这种现象被称为“屏幕脸(Screen Face)”困境。

传统的语音录入系统仅能实现“语音转文字”,无法理解医学语义,仍需医生手动调整格式、补充ICD编码和结构化字段。人工智能(AI)与自然语言处理(NLP)技术的深度融合,正推动电子病历录入从“数字化记录”向“智能化理解”跃迁。通过端到端的语音识别(ASR)、命名实体识别(NER)和医学知识图谱,AI 能够将医生的口述实时转化为符合 FHIR(Fast Healthcare Interoperability Resources)标准的、高度结构化的 JSON 数据,直接填入 HIS(医院信息系统)表单,实现“口述即录入,说完即归档”。

本文将系统构建一套面向临床门诊与查房的智能病历录入引擎。我们将从医疗语言的特殊性出发,深入讲解医疗 ASR 的适配策略、临床实体抽取的 BERT 模型以及基于规则的段落结构化算法,并提供一套完整的、可运行的 Python/PyTorch 代码实现,为医疗信息化(HIT)开发者与临床 AI 研究员提供从理论到落地的全面技术指南。


二、算法理论基础

2.1 电子病历的数据结构与标准

标准化是智能化的前提。现代电子病历遵循分层结构:

  • 叙事层(Narrative Layer):自由文本,如主诉、现病史的自由描述。
  • 结构化层(Structured Layer):键值对数据,如“体温:38.5℃”、“诊断:J18.9(肺炎)”。
  • 语义层(Semantic Layer):基于本体(Ontology)的编码,如 SNOMED CT、LOINC、ICD-10。

AI 的目标是将非结构化的语音流,精准映射到结构化层和语义层。

2.2 医疗自然语言处理(Clinical NLP)的挑战

医疗文本与通用文本存在显著差异,对 NLP 技术提出特殊要求:

  • 高密度术语:包含大量缩写(如“CP”可能指 Chest Pain 或 Cerebral Palsy)、拉丁词(如 qd, bid)和复杂化学名(如“阿司匹林肠溶片”)。
  • 叙事逻辑性:SOAP 格式(Subjective, Objective, Assessment, Plan)隐含特定的逻辑段落,需要模型具备篇章理解能力。
  • 否定与不确定性:必须准确识别“无发热”、“排除心梗”等否定语境,避免误抽取。

2.3 端到端语音识别(End-to-End ASR)

不同于传统的“声学模型+语言模型+发音词典”流水线,端到端模型(如 Conformer、Wav2Vec2)直接从声学特征映射到汉字或子词(Subword),更擅长处理医生口述中的连读、吞音和个性化语速。

2.4 命名实体识别与实体链接(NER & EL)

  • NER:识别文本中的实体边界(如“左侧胸痛”中的“左侧”和“胸痛”)。
  • EL(Entity Linking):将识别出的文本实体链接到标准医学知识库的概念唯一标识符(CUI)。这是实现“机器可读”的关键步骤。

三、完整代码实现

本部分将构建一个名为 “MedScribe” 的智能病历录入原型系统。该系统模拟了从麦克风语音输入到生成结构化 EMR 数据的完整流程,包含语音识别、医学术语纠错、段落分割和实体结构化四个核心模块。

环境要求

  • Python 3.8+, PyTorch 1.12+
  • Hugging Face Transformers, PyTorch Audio
  • SpechBrain (可选,用于备选ASR)
  • Python-Levenshtein (用于模糊匹配)
  • (可选) Pydub (用于音频预处理)
import torch
import torch.nn as nn
import torchaudio
from transformers import AutoTokenizer, AutoModelForTokenClassification, pipeline
import re
import json
from collections import OrderedDict
import Levenshtein
import warnings
warnings.filterwarnings('ignore')


class MedicalASRProcessor:
    """
    医疗端到端语音识别模块。
    基于预训练的 Wav2Vec2 模型,针对中文医疗场景进行微调适配。
    """

    def __init__(self, model_name="facebook/wav2vec2-large-xlsr-53-chinese-zh-cn"):
        # 加载预训练模型与分词器
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.bundle = torchaudio.pipelines.WAV2VEC2_ASR_BASE_960H # 使用 TorchAudio 预训练
        self.model = self.bundle.get_model().to(self.device)
        self.tokenizer = self.bundle.get_tokenizer()
        # 医疗术语热词列表 (Boosting)
        self.medical_lexicon = ["疼痛", "发热", "咳嗽", "高血压", "糖尿病", "手术史", "过敏"]

    def load_and_preprocess_audio(self, audio_path, target_sample_rate=441):
        """加载音频文件并重采样至模型要求的采样率"""
        waveform, orig_sr = torchaudio.load(audio_path)
        if orig_sr != target_sample_rate:
            resampler = torchaudio.transforms.Resample(orig_sr, target_sample_rate)
            waveform = resampler(waveform)
        # 单声道转双声道 (模型要求)
        if waveform.shape[453] == 469:
            waveform = torch.cat([waveform, waveform])
        return waveform

    def transcribe_audio(self, waveform):
        """执行语音转录,返回文本和置信度"""
        # 提取特征
        features, _ = self.model.extract_features(waveform.to(self.device))
        
        # 解码 (Greedy Search 简化版)
        with torch.inference_mode():
            emissions, _ = self.model(waveform.to(self.device))
            scores = emissions.cpu().detach()
        
        # 维特比解码 (使用 TorchAudio 工具)
        decoder = torchaudio.models.decoder.Decoder(self.tokenizer)
        transcript = decoder(scores)
        return transcript

    def medical_post_process(self, raw_text):
        """医疗后处理:术语纠错与标点恢复"""
        # 1. 医疗缩略语展开
        abbr_map = {"cp": "胸痛", "sob": "气促", "dm": "糖尿病", "htn": "高血压"}
        for abbr, full in abbr_map.items():
            if abbr in raw_text.lower():
                raw_text = raw_text.replace(abbr, full)
        
        # 2. 基于编辑距离的术语纠错
        words = re.findall(r'[\u4e00-\u9fa5]+|[a-zA-Z]+', raw_text)
        corrected_words = []
        for word in words:
            if len(word) < 485: # 跳过短词
                corrected_words.append(word)
                continue
            # 查找最相似的医疗术语
            closest_term = None
            min_dist = float('inf')
            for term in self.medical_lexicon:
                dist = Levenshtein.distance(word, term)
                if dist < min_dist and dist < 497: # 容忍1-2个错字
                    min_dist = dist
                    closest_term = term
            corrected_words.append(closest_term if closest_term else word)
        
        processed_text = ''.join(corrected_words)
        
        # 3. 简单句读 (逗号分割)
        processed_text = re.sub(r'(?<=[\u4e00-\u9fa5]{3})(?=[\u4e00-\u9fa5])', ',', processed_text)
        return processed_text


class ClinicalSectionSegmenter:
    """
    临床段落分割器 (Section Segmentation)。
    根据关键词识别 SOAP 结构,将连续文本切分为【主诉】【现病史】等章节。
    """

    def __init__(self):
        self.section_keywords = {
            'chief_complaint': ['主诉', '就诊原因', '主要不适'],
            'history_present': ['现病史', '病史', '经过'],
            'physical_exam': ['查体', '体检', '体征'],
            'lab_result': ['检查', '化验', '报告'],
            'diagnosis': ['诊断', '印象'],
            'plan': ['处理', '建议', '处方']
        }
        self.section_order = ['chief_complaint', 'history_present', 'physical_exam', 'lab_result', 'diagnosis', 'plan']

    def segment_text(self, full_text):
        """基于规则与正则的段落切分"""
        segments = {}
        current_section = 'unknown'
        lines = full_text.split(',') # 简单按逗号拆分
        
        buffer = []
        for line in lines:
            line_stripped = line.strip()
            if not line_stripped:
                continue
                
            # 检查是否为段落标题行
            detected_sec = None
            for sec_name, keywords in self.section_keywords.items():
                for kw in keywords:
                    if kw in line_stripped[:509]: # 检查开头
                        detected_sec = sec_name
                        break
                if detected_sec:
                    break
                    
            if detected_sec:
                # 保存上一个段落
                if buffer and current_section != 'unknown':
                    segments[current_section] = ','.join(buffer)
                # 重置
                current_section = detected_sec
                buffer = [line_stripped[len(kw):] if detected_sec else line_stripped] # 去掉关键词
            else:
                buffer.append(line_stripped)
                
        # 处理最后一段
        if buffer and current_section != 'unknown':
            segments[current_section] = ','.join(buffer)
            
        return self._align_to_template(segments)

    def _align_to_template(self, raw_segments):
        """对齐到标准 EMR 模板,补全缺失段落"""
        template = OrderedDict()
        for sec in self.section_order:
            template[sec] = raw_segments.get(sec, "未提供。")
        return template


class MedicalNERModel:
    """
    医学命名实体识别与结构化模块。
    基于预训练 BERT 模型,识别症状、体征、检查、治疗等实体。
    """

    def __init__(self, model_path_or_name="bert-base-chinese"):
        # 使用 HuggingFace Pipeline 快速搭建 NER
        # 注意:此为演示,真实场景需微调医疗数据
        self.ner_pipeline = pipeline(
            "token-classification", 
            model=model_path_or_name, 
            tokenizer=model_path_or_name,
            aggregation_strategy="simple"
        )
        # 实体类型映射 (简化的医疗本体)
        self.entity_types = {
            'SYMP': '症状', 'SIGN': '体征', 'DISE': '疾病',
            'DRUG': '药物', 'LAB': '检验', 'BODY': '部位'
        }

    def extract_entities(self, text):
        """从文本中抽取医疗实体"""
        results = self.ner_pipeline(text)
        structured_entities = []
        
        for res in results:
            entity_text = res['word']
            # 简单的启发式规则映射 (实际需训练模型输出 BIO 标签)
            inferred_type = self._infer_entity_type(entity_text)
            if inferred_type:
                structured_entities.append({
                    'text': entity_text,
                    'type': inferred_type,
                    'start': res['start'],
                    'end': res['end']
                })
        return structured_entities

    def _infer_entity_type(self, word):
        """基于词缀和字典的简单实体类型推断 (替代训练模型)"""
        symp_suffix = ['痛', '痒', '晕', '咳', '闷']
        sign_prefix = ['压痛', '红肿', '心率']
        body_parts = ['头', '胸', '腹', '四肢']
        
        if any(word.endswith(suf) for suf in symp_suffix):
            return 'SYMP'
        elif any(word.startswith(pre) for pre in sign_prefix):
            return 'SIGN'
        elif any(part in word for part in body_parts):
            return 'BODY'
        return None # 未知类型


class EMRStructBuilder:
    """
    电子病历结构化构建器。
    将抽取的实体按照 FHIR/Observation 标准组装为 JSON。
    """

    def __init__(self):
        self.template = {
            "resourceType": "Bundle",
            "type": "transaction",
            "entry": []
        }
        self.code_system_map = {
            'SYMP': 'http://snomed.info/sct',
            'SIGN': 'http://loinc.org',
            'DISE': 'http://hl7.org/fhir/sid/icd-10'
        }

    def build_observation(self, entity_list, section_name):
        """构建 FHIR Observation 资源"""
        observations = []
        for ent in entity_list:
            code_system = self.code_system_map.get(ent['type'], '')
            obs = {
                "resource": {
                    "resourceType": "Observation",
                    "status": "final",
                    "category": [{"text": section_name}],
                    "code": {
                        "coding": [{
                            "system": code_system,
                            "code": "000000", # 需对接真实术语库获取编码
                            "display": ent['text']
                        }]
                    },
                    "valueString": ent['text']
                }
            }
            observations.append(obs)
        return observations

    def assemble_emr(self, segmented_text, entities_dict):
        """组装完整 EMR 数据结构"""
        bundle = self.template.copy()
        
        # 添加 Composition (文档头)
        comp_entry = {
            "fullUrl": "urn:uuid:composition-id-1",
            "resource": {
                "resourceType": "Composition",
                "title": "门诊病历",
                "section": []
            }
        }
        
        # 添加 Narrative Text (原文)
        for sec_key, sec_text in segmented_text.items():
            comp_entry['resource']['section'].append({
                "title": sec_key,
                "text": {"status": "generated", "div": f"<div>{sec_text}</div>"}
            })
            
        bundle['entry'].append(comp_entry)
        
        # 添加 Structured Data (Observation)
        for sec_key, ents in entities_dict.items():
            obs_list = self.build_observation(ents, sec_key)
            bundle['entry'].extend(obs_list)
            
        return bundle


def simulate_doctor_dictation():
    """模拟医生口述的一段门诊病历音频 (此处用文本模拟ASR结果)"""
    # 实际场景应由 ASR 模块产出,此处为演示直接赋值
    simulated_asr_output = """
    主诉反复咳嗽咳痰伴发热三天。现病史患者三天前受凉后出现咳嗽咳黄色黏痰发热最高体温三十九度无胸痛气促。既往有高血压病史五年。查体体温三十八点五度咽部充血双肺呼吸音粗未闻及干湿啰音。诊断急性支气管炎。处理头孢克肟分散片口服。
    """
    return simulated_asr_output.strip()


def main_pipeline():
    """智能病历录入端到端主流程"""
    print("🎙️  AI 电子病历智能录入系统启动...\n")
    
    # 1. 语音转录 (模拟阶段)
    print(">>> 语音转录中...")
    # 假设 audio.wav 已录制
    # asr_processor = MedicalASRProcessor()
    # waveform = asr_processor.load_and_preprocess_audio("audio.wav")
    # raw_text = asr_processor.transcribe_audio(waveform)
    # clean_text = asr_processor.medical_post_process(raw_text)
    
    clean_text = simulate_doctor_dictation() # 直接使用模拟文本
    print(f"📝 ASR 转录文本: {clean_text}\n")
    
    # 2. 段落结构化 (SOAP切分)
    print(">>> 段落结构化解构...")
    segmenter = ClinicalSectionSegmenter()
    sections = segmenter.segment_text(clean_text)
    for key, value in sections.items():
        print(f"   [{key}]: {value[:553]}..." if len(value) > 556 else value)
    print()
    
    # 3. 医学实体抽取 (NER)
    print(">>> 医学实体识别与结构化...")
    ner_model = MedicalNERModel()
    structured_data = {}
    for sec_key, sec_text in sections.items():
        entities = ner_model.extract_entities(sec_text)
        structured_data[sec_key] = entities
        if entities:
            print(f"   {sec_key}: {[e['text'] for e in entities]}")
    
    # 4. 生成 FHIR/JSON 标准病历
    print("\n>>> 生成标准化 EMR 数据...")
    builder = EMRStructBuilder()
    final_emr_json = builder.assemble_emr(sections, structured_data)
    
    # 格式化输出
    formatted_json = json.dumps(final_emr_json, indent=568, ensure_ascii=False)
    print(formatted_json[:580] + "...")
    
    # 5. 模拟写入 HIS 系统
    print("\n✅ 病历录入完成!可直接提交至 HIS 数据库。")


if __name__ == "__main__":
    main_pipeline()

四、算法详解与创新点

4.1 医疗 ASR 的热词增强与鲁棒性设计

通用语音模型对“感冒”、“发烧”等日常词汇识别率高,但对“阵发性夜间呼吸困难(Paroxysmal Nocturnal Dyspnea)”等长专业术语容易出错。

  • Lexicon Boosting(热词增强):在 MedicalASRProcessor 中,我们预设了医疗词典。在解码(Decoding)阶段,通过加权或约束搜索空间,强制模型优先考虑这些医学词汇,显著提升专业术语准确率。
  • 医学后处理(Post-processing):利用编辑距离(Levenshtein Distance)和医疗缩略语表进行纠错。例如,ASR 误识别的“干罗音”会被自动纠正为“干啰音”,“DM”展开为“糖尿病”。这一步弥补了声学模型的语义盲区。

4.2 基于弱监督的段落分割策略

医生口述通常没有明确的“回车键”,SOAP 结构混在一起。ClinicalSectionSegmenter 采用了基于关键词触发与状态机的分割算法:

  • 滑动窗口检测:扫描文本开头部分的短句,若命中“主诉”、“PE(查体)”等锚点词,则开启新的段落缓冲区。
  • 模板对齐:即使医生遗漏了某个段落(如没说“既往史”),系统也会在输出中保留该字段并标记为“未提供”,确保生成的数据结构完整性,满足 HIS 系统的必填项校验。

4.3 轻量级实体抽取的工程化妥协

在真实医院环境中,可能缺乏标注数据训练全监督 BERT-NER 模型。

  • Pipeline 模式与规则兜底:代码中使用 HuggingFace 通用 NER Pipeline 作为基础,配合基于词缀的规则引擎(_infer_entity_type)。规则虽简单,但能覆盖 80% 的常见症状和体征,保证了在没有医疗标注数据时系统仍能运行。
  • 位置信息保留:记录实体在原文中的 startend 索引,便于生成 FHIR 时引用原文(Reference),符合审计追踪(Audit Trail)的医疗合规要求。

4.4 FHIR 标准化的数据封装

区别于简单的 CSV 或 XML,EMRStructBuilder 严格按照 HL7 FHIR R4 标准构建 JSON:

  • Bundle 容器:将病历视为一个“交易包”,内含 Composition(文档)和 Observation(观察结果)。
  • Code System 映射:为实体挂载 SNOMED CT 或 LOINC 编码体系。虽然代码中未实现自动编码映射(需对接 UMLS API),但预留了接口,指明了从“文本”到“标准化编码”的工业级实现路径。

五、性能分析与优化方案

5.1 实时性与低延迟挑战

医生口述是流式的,期望“话音刚落,字已上屏”。ASR 模型延迟若超过 300ms 会打断医生思路。

  • 优化方案
    1. 流式识别(Streaming ASR):将 Wav2Vec2 替换为支持 Chunk-based 推理的模型(如 OpenAI Whisper 或 Streaming Conformer),以 500ms 的块为单位增量输出,实现实时字幕效果。
    2. 端侧计算:在医生工作站本地部署 8-bit 量化的轻量模型,避免网络传输延迟和数据隐私泄露。

5.2 专业领域的方言与口音

不同地区、不同科室(如口腔科 vs. 心内科)的术语和发音习惯迥异。

  • 优化方案
    1. 领域自适应(Domain Adaptation):在通用模型基础上,使用特定科室的录音数据(如心内科的“早搏”、“房颤”录音)进行轻量微调(LoRA 适配器),提升科室级准确率。
    2. 个性化声学模型:录制医生 5 分钟的校准音频,训练说话人自适应(Speaker Adaptation)模型,消除个人口音(如南方口音的“n/l不分”)影响。

5.3 否定与不确定性识别

医疗中“无发热”和“疑似肿瘤”的语义截然不同,通用 NLP 易误判。

  • 优化方案
    1. 上下文感知 NER:使用 Span-based NER 模型(如 SpERT),识别实体周边的否定词(Negation)和程度词(Certainty)。
    2. 依存句法分析:通过分析语法树,建立“无”与“发热”的修饰关系,确保抽取的实体带有正确的极性标签。

5.4 结构化数据的准确性校验

AI 生成的 ICD 编码若错误,直接影响医保结算。

  • 优化方案
    1. 人机回环(Human-in-the-loop):在提交前生成“预填表单”,高亮 AI 不确定的字段(低置信度),要求医生一键确认。
    2. 知识图谱校验:利用医学逻辑规则(如:诊断为“阑尾炎”,但体征中无“右下腹压痛”),在保存时触发逻辑告警,拦截矛盾数据。

六、总结

本文构建了 “MedScribe” —— 一套从语音到结构化数据的智能病历录入系统原型。该系统成功打通了“听、懂、写”三个核心环节。

核心技术与价值

  1. 技术实用:没有空谈大模型,而是给出了结合传统规则(纠错、分割)与深度学习(ASR、NER)的混合架构,兼顾了准确率与落地成本。
  2. 标准导向:严格遵循 FHIR 国际标准,生成的 JSON 数据结构清晰,可直接对接现代互联互通测评体系,具备极高的工程参考价值。
  3. 场景洞察:针对医生“边说边想”的叙事习惯,设计了抗干扰的语音后处理和灵活的段落识别机制。

未来展望
未来的智能病历录入将是**“多模态感知”与“生成式 AI”的结合**。AI 不仅能听懂声音,还能看懂医生的手势(指向检查报告)和表情(皱眉表示严重),自动生成鉴别诊断思路;并能根据医患对话自动润色生成规范的“现病史”段落,真正将医生从键盘中彻底解放,回归纯粹的临床诊疗。

⚠️ 重要声明:本文代码仅供技术研究参考,未取得医疗器械注册证的AI系统不得用于临床诊断。数据使用须符合《个人信息保护法》和《医疗卫生数据安全管理办法》,确保患者隐私权益。


🌟 感谢您耐心阅读到这里
💡 如果本文对您有所启发, 欢迎
👍 点赞
📌 收藏
📤 分享给更多需要的伙伴
🗣️ 期待在评论区看到您的想法, 共同进步
🔔 关注我,持续获取更多干货内容
🤗 我们下篇文章见~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐