【医药AI实战系列④】医药NLP的天花板在哪里,从PubMed挖矿到监管文件解析,BioBERT到GPT-4o的能力边界
从一个真实需求说起
之前有个医学事务团队找到我,需求是这样的:
“我们竞品的说明书更新了,想知道他们在安全性部分加了什么新内容,顺便把最近两年所有相关的不良反应文献整理一下,下周汇报用。”
下周。两年文献。竞品说明书对比。
如果手工做,这是两名医学信息专员干三周的活。我们用NLP管道,4小时出了初稿,人工复核半天,第二天就交付了。
但在这个过程里,我也清楚地看到了NLP在医药场景的边界在哪里——有些地方快得让人惊讶,有些地方慢得让人绝望。今天全部说清楚。
医药NLP的四个主战场
医药行业的NLP需求,基本可以归为四类:
① 文献挖掘 PubMed/EMBASE → 靶点发现、安全信号、竞品情报 ② 临床文本处理 电子病历/出院小结 → 结构化、编码、表型提取 ③ 监管文件解析 说明书/IB/CTD → 信息提取、差异比对、合规检查 ④ 药物警戒 自发报告/社交媒体 → 不良事件信号检测这四个场景,技术难度和数据可及性差异极大。我们按从易到难的顺序逐一拆解。
第一战场:PubMed文献挖掘
为什么PubMed是医药NLP最好的练兵场
PubMed有几个对NLP极友好的特点:
- 超过3600万篇文章,持续更新
- 提供免费API(Entrez E-utilities),无需爬虫
- 摘要结构化程度高(背景/方法/结果/结论)
- MeSH(医学主题词)标注体系成熟
这让PubMed成为医药NLP的标准数据源。我们来写一个完整的、可直接运行的文献批量抽取管道。
可跑通代码:PubMed批量抽取管道
环境准备:
pipinstallbiopython pandas requests tqdm完整抽取代码:
""" PubMed批量文献抽取管道 功能:检索 → 批量下载摘要 → 结构化解析 → 输出CSV """importtimeimportpandasaspdfromBioimportEntrezfromtqdmimporttqdmfromtypingimportOptional# ① 配置(必须填写真实邮箱,否则NCBI可能封禁请求)Entrez.email="your_email@example.com"Entrez.api_key="your_ncbi_api_key"# 免费申请,有key限速从3次/秒提升到10次/秒# 申请地址:https://www.ncbi.nlm.nih.gov/account/defsearch_pubmed(query:str,max_results:int=500,date_range:Optional[tuple]=None)->list:""" 检索PubMed,返回PMID列表 Args: query: PubMed检索式,支持MeSH标签、布尔运算符 max_results: 最大返回数量 date_range: 日期范围元组 ("2022/01/01", "2024/12/31") Returns: pmid_list: 文章PMID列表 示例检索式: 'pembrolizumab[Title] AND "adverse events"[Title/Abstract]' '"KRAS G12C"[All Fields] AND "clinical trial"[Publication Type]' """search_params={"db":"pubmed","term":query,"retmax":max_results,"sort":"relevance","usehistory":"y",}ifdate_range:search_params["mindate"]=date_range[0]search_params["maxdate"]=date_range[1]search_params["datetype"]="pdat"# 发表日期handle=Entrez.esearch(**search_params)record=Entrez.read(handle)handle.close()pmid_list=record["IdList"]total_count=int(record["Count"])print(f"检索到{total_count}篇文章,返回前{len(pmid_list)}篇")returnpmid_listdeffetch_abstracts_batch(pmid_list:list,batch_size:int=100)->list:""" 批量下载文章摘要,自动处理速率限制 Args: pmid_list: PMID列表 batch_size: 每批下载数量(建议不超过200) Returns: records: 解析后的文章记录列表 """all_records=[]foriintqdm(range(0,len(pmid_list),batch_size),desc="下载摘要"):batch=pmid_list[i:i+batch_size]try:handle=Entrez.efetch(db="pubmed",id=",".join(batch),rettype="xml",retmode="xml")records=Entrez.read(handle)handle.close()all_records.extend(records["PubmedArticle"])exceptExceptionase:print(f"批次{i//batch_size+1}下载失败:{e}")time.sleep(5)continue# 有API key时3次/秒足够,无key时需要更保守time.sleep(0.15)returnall_recordsdefparse_pubmed_record(record:dict)->dict:""" 解析单条PubMed XML记录,提取关键字段 """article=record["MedlineCitation"]["Article"]medline=record["MedlineCitation"]# 基础字段pmid=str(medline["PMID"])title=str(article.get("ArticleTitle",""))# 摘要(可能是结构化摘要,也可能是普通段落)abstract_texts=[]if"Abstract"inarticle:abstract_obj=article["Abstract"]if"AbstractText"inabstract_obj:forsectioninabstract_obj["AbstractText"]:# 结构化摘要有Label属性label=getattr(section,"attributes",{}).get("Label","")text=str(section)iflabel:abstract_texts.append(f"{label}:{text}")else:abstract_texts.append(text)abstract=" | ".join(abstract_texts)# 发表日期pub_date=""try:date_obj=article["Journal"]["JournalIssue"]["PubDate"]year=str(date_obj.get("Year",""))month