在医学文献、电子病历、临床指南、基因注释这些资料里,藏着一大批高质量文本数据。生物医学背景的同学往往比一般工程师更懂这些数据的含义,却在面对算法模型时容易卡住。很多医学生想转AI方向,第一反应是去刷公开课,刷到RNN就开始吃力,再往后看到Transformer直接放弃。这套路本身就有问题——零基础转NLP,更需要一条清晰的学习路线,而不是资料合集。
这篇文章就围绕一条主线来写:RNN → Transformer → Prompt微调 → 生物医学NLP实战。读完之后,你会理解NLP中的序列建模为什么从循环网络走向注意力机制,大模型时代为什么大家开始谈Prompt微调,以及如何把这类技术落地到医学文本场景。文章以PyTorch和Hugging Face生态为示例环境,重点讲思路,配套的代码可以直接改写复现。
1. 背景与核心概念
1.1 生物医学背景转AI,为什么首选NLP
先问一个问题:医学信息里,结构化数据占比高,还是非结构化文本占比高?答案是后者。电子病历中的主诉、现病史、诊断意见,文献中的方法学描述,影像报告中的文字结论,药物说明中的相互作用信息,这些都是文本。想让计算机辅助医生处理这些信息,自然要依赖NLP。
所以,生物医学背景的人转AI,NLP是最自然的切入点。它不需要像计算机视觉那样大量依赖图像处理经验,核心工具是语言——生物医学专业的同学本身就擅长语言和结构化阅读。另一个原因是NLP技术栈相对成熟,尤其从预训练模型出现之后,开发者不需要自己设计复杂特征工程,直接借助开源模型就能处理真实任务。
1.2 NLP模型演进的主线
NLP模型经历了一条清晰的演进路线:从基于规则,到统计机器学习,再到深度学习的循环神经网络(RNN),接着是Transformer,最后是预训练大模型。本文重点讲后三个环节,因为它们构成了从「小模型定制」到「大模型微调」的完整逻辑。
RNN出现是为了解决序列建模问题。语言是连续的,前一个词会影响后一个词,RNN通过循环结构保留历史信息。但RNN的长距离依赖能力有限,训练也难以并行化。Transformer用注意力机制取代循环,使得任意两个位置的词都能直接建立关联,并且可以并行计算。大模型则是在大量文本上预训练,再通过Prompt微调适配具体任务。理解了这条主线,以后再学BERT、GPT、LLaMA都会轻松很多。
1.3 生物医学NLP的典型任务
生物医学NLP不是单一任务,而是一个任务族。目前常见的方向包括:
- 医学命名实体识别(NER):识别文本中的疾病、药物、症状、解剖部位等实体。
- 关系抽取:判断两个实体之间是否存在治疗关系、副作用关系。
- 文本分类:对病历、文献、问题进行分类,比如分诊、疾病类型判断。
- 医学问答:根据医学知识回答患者或医生的问题。
- 临床概念标准化:把口语化描述映射到标准ICD编码或医学本体术语。
这些任务并不需要全部掌握。新手可以从一个NER任务或文本分类任务入手,因为数据集好构造,评估指标直观,技术栈也和通用NLP一致。后面的实战环节,我会以医学NER为例带大家走一遍完整流程。
2. 环境准备与版本说明
2.1 基础环境
本文所有示例代码基于Python和PyTorch。建议使用虚拟环境管理依赖,避免系统环境被不同项目污染。可以按如下方式创建:
python -m venv nlp_med source nlp_med/bin/activate # Linux/macOS # 或 nlp_med\Scripts\activate # Windows具体Python版本建议在3.8到3.10之间。不同时间安装的PyTorch版本差异较大,下面命令表示安装常见稳定版本,具体版本号以你当前环境的官方源为准:
pip install torch torchvision pip install transformers datasets evaluate scikit-learn pandas如果计算机有NVIDIA显卡并希望用GPU加速,需要单独安装匹配CUDA版本的PyTorch。建议到PyTorch官网选择对应命令,不要盲目复制。
2.2 需要用到的核心库
transformers:Hugging Face生态的核心库,预训练模型、Tokenizer、Trainer都从这里导入。datasets:加载和预处理数据集。peft:用于LoRA等高效微调方法。torch/pytorch:深度学习框架。scikit-learn:计算评估指标。
有些同学会问:一定要GPU吗?做小规模实验时CPU也可以跑小模型,比如BERT-base可能很慢,但BioBERT这种规模的模型在CPU上也能跑,只是时间较长。建议先用小规模数据验证流程,再上GPU。
2.3 示例项目结构
为方便后续操作,建议项目按以下结构组织:
biomedical_nlp/ ├── data/ # 原始数据和预处理后数据 ├── models/ # 训练好的模型存储位置 ├── scripts/ │ ├── train_ner.py # 训练脚本 │ ├── predict.py # 推理脚本 │ └── preprocess.py # 数据预处理脚本 └── requirements.txt # 依赖文件这种结构并不复杂,但对于医学生开始写代码很重要——数据和代码隔离,脚本职责明确,避免后期项目变大时找不到文件。
3. RNN原理拆解:循环网络为什么能处理序列
3.1 为什么需要RNN
在传统神经网络中,输入和输出都是定长的,比如把一张图片识别成类别。但文本是变长序列,而且每个词的理解依赖上下文。比如「cell」在生物医学文本里,可能是「细胞」,也可能是「牢房」,甚至「电池」。只有看了前后文才能判断。
RNN的思路是:在时间步上共享同一组权重,每个时刻都接收当前输入和上一个时刻的隐状态,输出当前隐状态,再传递下去。这样网络就有了「记忆」。
隐藏状态更新公式可以简单写成:
h_t = tanh(W_ih * x_t + W_hh * h_{t-1} + b)
其中x_t是当前词向量,h_t是当前时刻的隐状态。这个公式不需要背得很熟,但要理解:信息会沿着时间轴传递。
3.2 RNN最小示例:人名分类
以「人名分类」为例,我们可以构建一个最简单的RNN分类模型。任务输入是一个人名字符序列,输出它属于哪个语言群体或文化类别。这是一个非常经典的学习案例。
先定义一个简化版本的数据结构:
# 脚本:scripts/train_rnn_name_classifier.py import torch import torch.nn as nn # 定义字符表 CHARS = "abcdefghijklmnopqrstuvwxyz" char2idx = {c: i+1 for i, c in enumerate(CHARS)} # 0 留作 padding NUM_CHARS = len(char2idx) + 1 class NameRNN(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super().__init__() self.hidden_size = hidden_size self.embedding = nn.Embedding(input_size, hidden_size) self.rnn = nn.GRU(hidden_size, hidden_size, batch_first=True) self.fc = nn.Linear(hidden_size, num_classes) def forward(self, x): emb = self.embedding(x) # [batch, seq_len, hidden] out, _ = self.rnn(emb) # [batch, seq_len, hidden] last = out[:, -1, :] # 取最后一个时刻的隐状态 return self.fc(last)这里用了GRU而不是原始RNN,GRU和LSTM都是为了缓解梯度消失问题而设计的变体,结构上增加了门控机制,但入门阶段可以先把它们当作一种更稳定的RNN来看待。
3.3 RNN存在什么问题
RNN最大的问题有两个。
第一,长距离依赖能力不足。如果输入序列很长,信息经过多个时间步传递后会衰减,网络很难捕捉到句子开头和结尾之间的依赖关系。
第二,训练效率低。RNN必须按时间步顺序计算,当前时刻依赖上一时刻的结果,因此很难并行化。在动辄几千上万词的文本上,这种代价很致命。
此外,RNN还面临梯度消失或梯度爆炸的问题。为了解决这个问题,LSTM和GRU被设计出来,但这仍然是沿着同一条技术路线的修补。Transformer走了一条不同的路——不做循环,直接让所有位置两两交互。
4. Transformer架构:取代RNN的注意力革命
4.1 自注意力机制的核心思想
Transformer的核心是自注意力机制(Self-Attention)。它解决了一个很朴素的问题:一个词在当前语境里,应该重点关注哪些其他词?
比如句子「患者服用了阿司匹林后出现了胃部不适」,模型在处理「阿司匹林」时,可能最需要关注「胃部不适」这两个词,因为它们之间存在副作用关联。自注意力机制做的就是为每个词计算一组权重,表示它对其他词的注意力强度。
具体做法:先通过线性变换把每个词向量映射为三个向量——Query、Key、Value。然后计算某个词的Query与所有词的Key的点积,经过缩放和softmax得到注意力权重,再用权重对Value做加权求和。公式如下:
Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V
这里的sqrt(d_k)是缩放因子,作用是防止点积结果过大导致softmax进入饱和区。
4.2 多头注意力与位置编码
单个自注意力层只能捕捉一种关系模式,但语言中的关系是多样的。Transformer把自注意力扩展到多个头——多头注意力(Multi-Head Attention),每个头学习不同的注意力模式,最后拼接在一起。就像一位医生从症状、用药、检查结果、病史多个角度同时分析同一个病例。
另一个关键机制是位置编码(Positional Encoding)。自注意力本身对位置不敏感,把句子顺序打乱,每个词的注意力结果仍然一样。这不符合语言规律,因为「A打B」和「B打A」含义完全不同。Transformer通过加入位置编码曲线,让模型感知词序。
4.3 最小自注意力实现
下面是一个简化版的自注意力实现,用来理解核心计算流程。这个代码强调逻辑,不追求最优性能:
import torch import torch.nn as nn import math class SelfAttention(nn.Module): def __init__(self, embed_dim): super().__init__() self.query = nn.Linear(embed_dim, embed_dim) self.key = nn.Linear(embed_dim, embed_dim) self.value = nn.Linear(embed_dim, embed_dim) self.scale = math.sqrt(embed_dim) def forward(self, x): # x: [batch, seq_len, embed_dim] Q = self.query(x) K = self.key(x) V = self.value(x) scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale weights = torch.softmax(scores, dim=-1) out = torch.matmul(weights, V) return out实际Transformer还会加上残差连接、LayerNorm和前馈网络,但这些组件都可以在后续学习过程中逐步理解。
4.4 为什么最终是Transformer
结合前面的分析,Transformer胜出的原因可以归纳为三点。
第一,长距离依赖能力强。任意两个词之间的交互路径长度都是1,理论上不存在信息衰减问题。
第二,并行性好。自注意力可以一次性计算序列中所有位置的关系,不需要逐步迭代,这会大幅提升训练速度。
第三,可扩展性强。Transformer架构可以堆更多层、扩大参数规模,在大数据上持续取得收益。后来的BERT、GPT、T5,以及各类大模型,底座都是Transformer。这也是为什么你今天去看任何大模型的技术博客,几乎都会先讲Transformer。
5. Prompt微调:大模型时代的适配方式
5.1 从预训练到微调的范式转变
在Transformer出现之后,NLP进入预训练时代。先在海量无标注文本上训练一个语言模型,再在有标签的小数据上进行微调。BERT、RoBERTa为代表的模型在这个范式下取得了巨大成功。
传统做法的核心是「全量微调」:把预训练模型当成一个初始化权重,在目标任务数据上继续训练,更新全部参数。这样做的优点是效果好,但缺点也很明显——每个任务都要存一份完整模型,训练成本高,标注数据需求量也不小。
5.2 Prompt微调为什么出现
到了大模型时代,模型的参数量动辄几十亿、上百亿,全量微调变得不现实。研究者发现,大型语言模型本身已经隐含了大量知识,只要改变输入形式,给它设计合适的提示模板(Prompt),模型就能以较少样本完成任务。
Prompt微调的思路不是大规模修改模型参数,而是通过设计提示文本,让模型「回忆」出已经学会的能力。比如对于医学文本分类,传统微调可能需要专门加一个分类头;Prompt方式则