ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于可解释Transformer的EHR临床预测:从原理到医疗AI实践

2026/9/1 7:51:44 拓冰建站 浏览量
基于可解释Transformer的EHR临床预测:从原理到医疗AI实践 在医疗健康领域临床预测任务如疾病诊断、再入院风险预测或并发症预警对于提升诊疗质量和患者预后至关重要。传统的机器学习模型在处理电子健康记录这类复杂的结构化数据时往往面临特征工程繁琐、模型可解释性差等挑战。近年来Transformer架构凭借其强大的序列建模和注意力机制在自然语言处理领域取得了巨大成功。那么能否将这种强大的模型应用于结构化的EHR数据并解决其“黑箱”问题使其决策过程对医生透明可信呢这正是本文要探讨的核心。本文将围绕“可解释的Transformer模型在结构化电子健康记录临床预测任务中的应用”这一主题从零开始系统性地拆解其原理、实现步骤、代码细节以及可解释性分析方法。无论你是医疗AI领域的研究者还是希望将前沿深度学习技术应用于实际业务场景的工程师都能从中获得一套从理论到实践的完整方案。1. 背景与核心概念在深入技术细节之前我们有必要厘清几个关键概念理解为什么Transformer和可解释性在临床预测中如此重要。1.1 结构化电子健康记录电子健康记录是患者在医疗机构就诊过程中产生的数字化信息集合。结构化EHR特指那些能够以规整的表格形式存储的数据例如人口统计学信息年龄、性别。生命体征血压、心率、体温。实验室检验结果血糖、白细胞计数。诊断与手术编码通常使用ICD-10国际疾病分类或CPT当前操作术语编码。药物处方使用RxNorm或ATC编码。这些数据本质上是多变量时间序列。每个患者可以被视为一个序列序列中的每个时间点如一次就诊包含多个特征如血压、诊断编码。我们的预测任务例如预测患者未来30天内是否会再入院就是基于这个序列进行的。1.2 Transformer 架构简介Transformer最初是为机器翻译设计的其核心是自注意力机制。与RNN或LSTM顺序处理序列不同自注意力机制允许序列中的任意两个位置直接交互从而更好地捕捉长距离依赖关系。一个标准的Transformer编码器层主要包含多头自注意力层计算输入序列中所有位置之间的关联权重。前馈神经网络层对每个位置的表示进行非线性变换。残差连接与层归一化用于稳定和加速深度网络的训练。对于EHR数据我们可以将患者的一次次就诊视为序列中的“词”将每次就诊的各种特征视为“词”的嵌入表示。这样Transformer就能学习就诊之间的复杂时序模式和特征间的交互关系。1.3 模型可解释性的重要性在临床场景中模型的可靠性不仅取决于其预测精度AUC、F1分数更取决于其决策是否可信、可审计、可理解。医生信任如果一个模型仅给出“再入院风险85%”的结论医生无法据此制定干预措施。他们需要知道是哪些历史就诊、哪些关键指标如反复出现的高血压记录、异常的肾功能指标导致了高风险判断。伦理与合规许多地区如欧盟的GDPR要求对自动化决策提供解释。模型调试可解释性工具能帮助开发者发现模型是否学习了错误的关联例如根据医院ID而非病情进行预测。因此我们的目标不仅是构建一个高性能的Transformer预测模型更要使其成为一个可解释的Transformer模型。2. 环境准备与版本说明我们将使用PyTorch深度学习框架来实现模型并使用Captum库进行可解释性分析。以下是推荐的环境配置操作系统Linux (Ubuntu 20.04) 或 macOS Windows也可行需注意部分依赖。Python3.8 或 3.9。深度学习框架PyTorch 1.9。关键Python库torch: 模型构建与训练。pandas,numpy: 数据处理。scikit-learn: 数据划分与评估指标。captum: 模型可解释性分析。jupyter: 交互式实验可选。你可以使用以下命令创建环境并安装依赖# 创建并激活虚拟环境可选 conda create -n ehr-transformer python3.8 conda activate ehr-transformer # 安装PyTorch请根据你的CUDA版本访问官网获取对应命令 # 例如对于CUDA 11.3 pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113 # 安装其他依赖 pip install pandas numpy scikit-learn captum jupyter版本兼容性提示Captum库与PyTorch版本需匹配建议使用较新的稳定版组合。本文示例代码基于PyTorch 1.12和Captum 0.6.0编写核心逻辑在不同版本间通用。3. 核心原理与模型架构拆解我们将构建一个专为结构化EHR数据设计的Transformer预测模型。整个流程分为数据预处理与序列化、模型构建、训练与评估、可解释性分析。3.1 数据预处理与序列化这是最关键的一步目标是将原始的EHR表格转换为模型可接受的张量序列。假设我们有一个简化的EHR数据集包含以下字段patient_id,visit_date,age,systolic_bp,diagnosis_code简化label是否再入院 0/1。步骤1按患者分组并排序import pandas as pd import numpy as np # 假设 df 是原始的EHR DataFrame df[visit_date] pd.to_datetime(df[visit_date]) df df.sort_values([patient_id, visit_date]) # 按患者和时间排序 # 对分类特征进行编码例如诊断编码 from sklearn.preprocessing import LabelEncoder diag_encoder LabelEncoder() df[diagnosis_code_encoded] diag_encoder.fit_transform(df[diagnosis_code].astype(str))步骤2构建患者序列我们需要为每个患者生成一个序列序列长度可变。同时需要处理数值特征和分类特征。def create_patient_sequences(df): patient_sequences [] patient_labels [] feature_names [age, systolic_bp, diagnosis_code_encoded] # 示例特征 for pid, group in df.groupby(patient_id): # 提取该患者所有就诊的特征按时间排序 sequence group[feature_names].values.astype(np.float32) # (seq_len, num_features) label group[label].iloc[-1] # 假设标签在最后一次就诊时确定预测未来事件 patient_sequences.append(sequence) patient_labels.append(label) return patient_sequences, patient_labels, feature_names sequences, labels, feature_names create_patient_sequences(df)此时sequences是一个列表其中每个元素是一个形状为(seq_len, num_features)的NumPy数组。seq_len代表该患者的就诊次数。步骤3序列填充与掩码生成Transformer要求批处理中的序列长度一致。我们需要进行填充并生成注意力掩码以忽略填充位置。from torch.nn.utils.rnn import pad_sequence import torch # 将列表中的NumPy数组转换为PyTorch张量 sequence_tensors [torch.from_numpy(s) for s in sequences] # 填充序列 padded_sequences pad_sequence(sequence_tensors, batch_firstTrue, padding_value0) # shape: (batch_size, max_seq_len, num_features) # 生成注意力掩码 (1表示真实数据0表示填充) attention_mask (padded_sequences.sum(dim-1) ! 0).float() # 假设特征全为0是填充值 # 更鲁棒的做法是记录原始长度 seq_lengths torch.tensor([len(s) for s in sequences]) attention_mask torch.arange(padded_sequences.size(1))[None, :] seq_lengths[:, None] attention_mask attention_mask.float()3.2 Transformer 模型架构设计我们将设计一个简单的Transformer编码器用于分类。关键点在于如何将多特征的时间步输入转换为Transformer所需的维度。import torch.nn as nn import torch.nn.functional as F class EHRTransformer(nn.Module): def __init__(self, num_features, d_model128, nhead8, num_layers3, dim_feedforward256, dropout0.1, num_classes2): super(EHRTransformer, self).__init__() self.num_features num_features self.d_model d_model # 1. 特征投影层将原始特征维度投影到d_model self.feature_projection nn.Linear(num_features, d_model) # 2. 可学习的位置编码因为就诊序列顺序至关重要 self.pos_encoder nn.Parameter(torch.randn(1, 5000, d_model)) # 假设最大序列长度5000 # 3. Transformer编码器层 encoder_layer nn.TransformerEncoderLayer(d_modeld_model, nheadnhead, dim_feedforwarddim_feedforward, dropoutdropout, batch_firstTrue) self.transformer_encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) # 4. 全局池化与分类头 # 我们使用最后一个时间步的输出或所有时间步的平均/最大池化 self.pooling last # 可选 mean, max, last self.classifier nn.Sequential( nn.Linear(d_model, dim_feedforward), nn.ReLU(), nn.Dropout(dropout), nn.Linear(dim_feedforward, num_classes) ) def forward(self, x, attention_maskNone): Args: x: 输入张量形状 (batch_size, seq_len, num_features) attention_mask: 形状 (batch_size, seq_len)1表示有效0表示填充 Returns: logits: 分类logits (batch_size, num_classes) encoder_outputs: Transformer最后一层的输出 (batch_size, seq_len, d_model)用于可解释性分析 batch_size, seq_len, _ x.shape # 特征投影 x_proj self.feature_projection(x) # (batch_size, seq_len, d_model) # 添加位置编码 # 只取pos_encoder中对应序列长度的部分 x_proj x_proj self.pos_encoder[:, :seq_len, :] # 调整mask形状以适应Transformer (batch_size, seq_len) - (batch_size, seq_len, seq_len) # PyTorch Transformer 需要 (batch_size, seq_len) 的src_key_padding_mask if attention_mask is not None: # src_key_padding_mask: (batch_size, seq_len), True表示需要被mask的位置填充位 src_key_padding_mask (attention_mask 0) else: src_key_padding_mask None # 通过Transformer编码器 encoder_outputs self.transformer_encoder(x_proj, src_key_padding_masksrc_key_padding_mask) # 池化获取患者整体表示 if self.pooling last: # 获取每个序列最后一个有效时间步的表示 # 首先需要根据mask找到最后一个有效索引 if src_key_padding_mask is not None: # src_key_padding_mask为True的位置是填充位我们取最后一个False的位置 lengths seq_len - src_key_padding_mask.sum(dim1) # 实际长度 patient_repr encoder_outputs[torch.arange(batch_size), lengths - 1] else: patient_repr encoder_outputs[:, -1, :] # 简单取最后一个 elif self.pooling mean: if src_key_padding_mask is not None: # 对有效位置求平均 encoder_outputs_masked encoder_outputs * (1 - src_key_padding_mask.unsqueeze(-1).float()) patient_repr encoder_outputs_masked.sum(dim1) / (lengths.unsqueeze(-1).float() 1e-9) else: patient_repr encoder_outputs.mean(dim1) else: # max pooling if src_key_padding_mask is not None: encoder_outputs_masked encoder_outputs.clone() encoder_outputs_masked[src_key_padding_mask] -float(inf) # 将填充位置设为负无穷 patient_repr, _ encoder_outputs_masked.max(dim1) else: patient_repr, _ encoder_outputs.max(dim1) # 分类 logits self.classifier(patient_repr) return logits, encoder_outputs关键设计解释特征投影层EHR特征维度如年龄、血压、编码通常远小于Transformer的标准d_model如512。这个线性层将特征映射到高维语义空间。位置编码就诊顺序包含重要信息病情演变。我们使用可学习的位置编码比固定的正弦编码更灵活。注意力掩码正确使用src_key_padding_mask确保模型忽略填充的就诊记录只关注真实数据。池化策略如何从就诊序列输出汇聚为患者表示是关键。last池化假设最近一次就诊信息最重要mean池化考虑所有就诊max池化捕捉最显著的就诊信号。可根据任务选择。4. 完整实战案例心力衰竭再入院预测我们以一个公开数据集如MIMIC-III的简化子集为例模拟心力衰竭患者再入院预测任务。4.1 数据加载与预处理模拟由于真实EHR数据获取复杂我们创建一个模拟数据集来演示完整流程。import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, LabelEncoder import torch from torch.utils.data import Dataset, DataLoader class SimulatedEHRDataset(Dataset): 模拟EHR数据集 def __init__(self, num_patients1000, max_visits20, num_features10, seq_lengthsNone): super().__init__() self.num_features num_features # 模拟患者序列长度就诊次数 if seq_lengths is None: self.seq_lengths np.random.randint(3, max_visits, sizenum_patients) else: self.seq_lengths seq_lengths self.data [] self.labels [] self.feature_names [ffeature_{i} for i in range(num_features-2)] [diag_code, med_code] for pid in range(num_patients): seq_len self.seq_lengths[pid] # 生成序列数据大部分是数值特征最后两个是分类编码 numeric np.random.randn(seq_len, num_features-2).astype(np.float32) diag np.random.randint(0, 50, size(seq_len, 1)).astype(np.float32) # 50种诊断 med np.random.randint(0, 30, size(seq_len, 1)).astype(np.float32) # 30种药物 sequence np.concatenate([numeric, diag, med], axis1) self.data.append(torch.from_numpy(sequence)) # 模拟标签与某些特征和模式相关 risk_score sequence[-1, 0] 0.5*sequence[-1, 3] - 0.8*(sequence[-2, 1] if seq_len1 else 0) label 1 if risk_score 0.5 else 0 self.labels.append(label) def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx], self.labels[idx] def collate_fn(batch): 自定义批处理函数用于处理变长序列 sequences, labels zip(*batch) labels torch.tensor(labels, dtypetorch.long) # 填充序列 padded_sequences pad_sequence(sequences, batch_firstTrue, padding_value0) # 生成注意力掩码 seq_lengths torch.tensor([len(s) for s in sequences]) max_len padded_sequences.size(1) attention_mask torch.arange(max_len)[None, :] seq_lengths[:, None] attention_mask attention_mask.float() return padded_sequences, attention_mask, labels # 创建数据集并划分 full_dataset SimulatedEHRDataset(num_patients2000) train_idx, test_idx train_test_split(np.arange(len(full_dataset)), test_size0.2, random_state42) train_dataset torch.utils.data.Subset(full_dataset, train_idx) test_dataset torch.utils.data.Subset(full_dataset, test_idx) # 创建数据加载器 train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, collate_fncollate_fn) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse, collate_fncollate_fn)4.2 模型训练与评估import torch.optim as optim from sklearn.metrics import roc_auc_score, accuracy_score, f1_score device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) # 初始化模型 num_features full_dataset.num_features model EHRTransformer(num_featuresnum_features, d_model64, nhead4, num_layers2, dim_feedforward128, dropout0.2).to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) def train_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0 all_preds [] all_labels [] for batch_idx, (sequences, mask, labels) in enumerate(dataloader): sequences, mask, labels sequences.to(device), mask.to(device), labels.to(device) optimizer.zero_grad() logits, _ model(sequences, mask) loss criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 梯度裁剪 optimizer.step() total_loss loss.item() preds torch.argmax(logits, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) avg_loss total_loss / len(dataloader) acc accuracy_score(all_labels, all_preds) f1 f1_score(all_labels, all_preds, averagemacro) return avg_loss, acc, f1 def evaluate(model, dataloader, criterion, device): model.eval() total_loss 0 all_preds [] all_labels [] all_probs [] with torch.no_grad(): for sequences, mask, labels in dataloader: sequences, mask, labels sequences.to(device), mask.to(device), labels.to(device) logits, _ model(sequences, mask) loss criterion(logits, labels) total_loss loss.item() probs F.softmax(logits, dim1) preds torch.argmax(logits, dim1) all_probs.extend(probs[:, 1].cpu().numpy()) # 正类概率 all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) avg_loss total_loss / len(dataloader) acc accuracy_score(all_labels, all_preds) f1 f1_score(all_labels, all_preds, averagemacro) auc roc_auc_score(all_labels, all_probs) return avg_loss, acc, f1, auc, all_probs, all_preds, all_labels # 训练循环 num_epochs 30 for epoch in range(num_epochs): train_loss, train_acc, train_f1 train_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc, val_f1, val_auc, _, _, _ evaluate(model, test_loader, criterion, device) if (epoch 1) % 5 0: print(fEpoch {epoch1:03d}:) print(f Train Loss: {train_loss:.4f}, Acc: {train_acc:.4f}, F1: {train_f1:.4f}) print(f Val Loss: {val_loss:.4f}, Acc: {val_acc:.4f}, F1: {val_f1:.4f}, AUC: {val_auc:.4f})4.3 模型可解释性分析训练好模型后我们使用Captum库来理解模型的决策依据。我们将重点分析特征归因和就诊重要性。from captum.attr import IntegratedGradients, LayerConductance from captum.attr import visualization as viz # 选择一个测试样本进行解释 model.eval() test_batch next(iter(test_loader)) sequences, mask, labels test_batch sample_idx 0 # 解释第一个样本 sequence sequences[sample_idx:sample_idx1].to(device).requires_grad_(True) mask_sample mask[sample_idx:sample_idx1].to(device) true_label labels[sample_idx].item() # 获取模型预测 logits, encoder_outputs model(sequence, mask_sample) pred_prob F.softmax(logits, dim1) pred_class torch.argmax(logits, dim1).item() print(fTrue label: {true_label}, Predicted: {pred_class} (prob: {pred_prob[0, pred_class]:.3f})) # 方法1综合梯度Integrated Gradients - 针对输入特征 ig IntegratedGradients(model) # 我们需要定义一个包装函数因为我们的模型返回两个值logits和encoder_outputs def model_forward_wrapper(inputs, attention_mask): logits, _ model(inputs, attention_mask) return logits # 计算特征归因相对于零基线 attributions, delta ig.attribute( inputssequence, baselinestorch.zeros_like(sequence), # 基线全零输入 additional_forward_args(mask_sample,), targetpred_class, # 解释模型为何预测为该类 return_convergence_deltaTrue ) # attributions形状: (1, seq_len, num_features) # 可视化哪个就诊的哪个特征最重要 seq_len int(mask_sample.sum().item()) # 实际序列长度 attr_np attributions[0, :seq_len, :].detach().cpu().numpy() # 取有效部分 print(\n特征归因分析正值表示对预测有正向贡献) for t in range(seq_len): print(f就诊 {t1}:) for f_idx in range(num_features): attr_val attr_np[t, f_idx] if abs(attr_val) 0.01: # 显示贡献较大的特征 print(f {full_dataset.feature_names[f_idx]}: {attr_val:.4f}) # 方法2层传导Layer Conductance - 分析Transformer内部注意力层 # 分析最后一层Transformer编码器的贡献 lc LayerConductance(model_forward_wrapper, model.transformer_encoder.layers[-1]) layer_attributions lc.attribute(sequence, additional_forward_args(mask_sample,), targetpred_class) # 可以分析layer_attributions来理解不同注意力头或神经元的作用 # 方法3自定义分析 - 就诊级别重要性将每个就诊的所有特征归因求和 visit_importance attr_np.sum(axis1) # (seq_len,) print(f\n各就诊对预测的总贡献度) for t, imp in enumerate(visit_importance): print(f 就诊 {t1}: {imp:.4f})运行上述代码你可以得到类似以下的输出这直观地展示了模型决策的依据True label: 1, Predicted: 1 (prob: 0.872) 特征归因分析正值表示对预测有正向贡献 就诊 1: feature_0: -0.0123 feature_3: 0.0456 就诊 3: diag_code: 0.1287 med_code: 0.0821 就诊 5: feature_1: 0.2345 各就诊对预测的总贡献度 就诊 1: 0.0333 就诊 2: -0.0056 就诊 3: 0.2108 就诊 4: 0.0122 就诊 5: 0.2345解读模型预测该患者会再入院标签1。分析显示第3次就诊贡献度0.21和第5次就诊贡献度0.23对做出“再入院”预测的贡献最大。在第3次就诊中诊断编码和药物编码是主要贡献特征在第5次就诊中feature_1可能代表某个关键实验室指标是主要贡献者。这为医生提供了一个清晰的“证据链”。5. 常见问题与排查思路在实际实现和应用过程中你可能会遇到以下典型问题问题现象可能原因解决思路训练损失不下降或波动大1. 学习率设置不当。2. 序列填充值影响如用0填充但0是有效值。3. 梯度爆炸/消失。4. 数据标签不平衡。1. 尝试使用学习率预热Warmup和衰减策略。2. 使用无信息量的特殊值填充如-999并在投影层前处理。3. 添加梯度裁剪clip_grad_norm_。4. 使用加权交叉熵损失或过采样/欠采样。模型在验证集上性能差过拟合1. 模型复杂度太高d_model、层数过多。2. 训练数据量太少。3. Dropout比率太低。1. 简化模型架构减少参数量。2. 尝试数据增强如对序列进行随机掩码、时间窗口缩放。3. 增加Dropout比率或在特征投影后也添加Dropout。可解释性分析结果杂乱无章1. 归因方法选择不当。2. 基线Baseline选择不合理。3. 模型本身预测置信度低。1. 尝试多种归因方法如Saliency,DeepLift,Shapley Value进行交叉验证。2. 尝试不同的基线如特征均值、随机采样。3. 只对高置信度如预测概率0.8的样本进行解释。GPU内存溢出OOM1. 批次大小Batch Size太大。2. 序列最大长度过长。3. Transformer层数或d_model太大。1. 减小batch_size累积梯度。2. 设定序列长度上限或使用动态批处理。3. 使用梯度检查点Gradient Checkpointing或混合精度训练。位置编码效果不佳1. 可学习位置编码过拟合。2. 正弦位置编码无法适应长序列外推。1. 尝试固定的正弦位置编码。2. 使用相对位置编码如Transformer-XL、T5使用的。3. 在位置编码上添加Dropout。6. 最佳实践与工程建议要将可解释的Transformer模型成功应用于真实的临床预测项目除了跑通代码还需关注以下工程和实践细节6.1 数据预处理与特征工程缺失值处理EHR数据缺失严重。不要简单用0填充。对于数值特征考虑用同一患者的均值、中位数或上一次观测值填充纵向填充。对于分类特征可创建“缺失”类别。特征标准化对数值特征如年龄、实验室值进行标准化Z-score或归一化以稳定训练。编码高基数特征诊断编码如ICD-10数量庞大。直接使用One-hot编码会导致维度爆炸。建议使用分层编码利用ICD代码的树状结构。使用预训练嵌入如从大规模医疗文本中学习的编码表示。使用特征哈希Hashing Trick。时间信息利用除了就诊顺序就诊间隔时间也是重要信号。可以将时间间隔作为单独特征或用于调整位置编码。6.2 模型架构优化池化策略选择根据任务特性选择池化方式。对于预后预测如死亡风险last池化关注最近状态可能更有效。对于疾病分类mean或max池化关注整体模式可能更好。可以尝试注意力池化Attention Pooling让模型自己学习如何聚合就诊信息。加入先验知识在Transformer之前或之后加入医学先验知识。例如可以添加一个知识图谱嵌入模块将诊断编码映射到其在疾病图谱中的向量表示。多任务学习同时预测多个相关临床结局如再入院、死亡、并发症可以共享底层特征表示提升模型泛化能力和鲁棒性。6.3 可解释性报告的生成生成自然语言解释将归因结果如“就诊3的诊断编码A和药物B对高风险预测贡献最大”与医学知识库结合自动生成一段医生易懂的文本描述。可视化工具集成开发交互式可视化界面用热力图展示患者时间线上各特征的重要性支持医生下钻查看具体就诊详情。反事实解释回答“如果患者上次就诊的血压控制得更好预测风险会降低多少”这类问题。这需要构建反事实样本并重新进行预测。6.4 生产环境部署考量延迟与吞吐量Transformer的解码是序列长度的平方复杂度。对于长序列患者推理可能变慢。考虑使用线性注意力Linear Attention等高效变体。设定就诊序列的滑动窗口只关注最近N次就诊。模型监控与更新医疗数据分布可能随时间漂移如新的诊疗指南。建立模型性能的持续监控流水线定期用新数据评估和更新模型。安全与合规确保模型部署符合医疗数据隐私法规如HIPAA。所有可解释性报告中的患者信息必须去标识化。通过将强大的Transformer模型与严谨的可解释性分析相结合我们不仅能构建出高精度的临床预测工具更能打开模型“黑箱”提供可信、可操作的决策支持真正赋能临床医生实现人工智能与医疗 expertise 的深度融合。你可以从本文提供的代码框架出发接入真实的EHR数据不断迭代优化探索其在具体临床场景中的巨大潜力。