医疗人读懂 LLM 的第二课: 使用 Transformer

32 阅读10分钟

听诊器的发明,让医生第一次能听见心肺的内部声音。 Transformer 的诞生,则让机器第一次真正理解了语言的上下文。对医疗人来说,这不只是技术的突破,它意味着我们可以重新思考如何让 AI 在临床、科研和患者服务中发挥作用。

作为医疗从业者,我们每天都要处理庞杂的信息:病历、科研文献……而 Transformer 正是当下所有大语言模型(LLM)的核心。理解它,等于理解了 ChatGPT、医学辅助诊断系统、自动科研写作工具的根基。

所以,接下来我希望带你一步步走入 Transformer 学习课程,用通俗的方式来理解复杂的技术。

需要课程代码以及问题咨询通过留言 📮在路上的蟹老板(pqjrkwem@gmail.com

课程简介

本课程将教你如何使用 Transformaer 生态系统的库进行自然语言处理(NLP)。

涵盖的库

  • Transformers - 核心模型库
  • Datasets - 数据集处理
  • Tokenizers - 文本分词
  • Accelerate - 训练加速
  • Hugging Face Hub - 模型共享平台

课程结构

第 1-4 章:Transformers 基础

介绍 Transformers 库的主要概念。完成后你将:

  • 了解 Transformer 模型的工作原理
  • 使用 Transformer 模型
  • 微调一个预训练模型
  • 分享模型和标记器

第 5-8 章:拥有一个自己的模型

  • Datasets库
  • Tokenizers库
  • 主要的 NLP 任务
  • 构建和分享你的模型

学习要求

具备:

良好的 Python 知识, 要是熟悉 深度学习和机器学习就更好了, 对于tensorflow 或者 pytorch 熟悉更更更加分


Pipeline 的内部原理

完整示例

让我们从一个完整的示例开始,看看在第一章中执行以下代码时在幕后发生了什么:

from transformers import pipeline

classifier = pipeline("sentiment-analysis")
classifier([
    "I've been waiting for a HuggingFace course my whole life.",
    "I hate this so much!",
])

获得输出:

[{'label': 'POSITIVE', 'score': 0.9598047137260437},
 {'label': 'NEGATIVE', 'score': 0.9994558095932007}]

这个 pipeline 集成了三个步骤:预处理模型计算后处理

┌─────────────────────────────────────────────────────────────┐
│                    完整的 NLP Pipeline                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原始文本  →  Tokenizer  →  Token IDs  →  Model  →  输出   │
│    "Hi!"        ↓           [101,123]      ↓        Logits  │
│              分词+编码                   Transformer         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

使用 Tokenizer 进行预处理

与其他神经网络一样,Transformer 模型无法直接处理原始文本,因此管道的第一步是将文本输入转换为模型能够理解的数字。Tokenizer 负责:

  • 将输入拆分为单词、子单词或符号(如标点符号),称为 token
  • 将每个 token 映射到一个数字,称为 input ID
  • 添加模型需要的其他输入,例如:
    • 特殊标记(如 [CLS][SEP]
    • 位置编码
    • 段落标记

加载 Tokenizer

我们使用 AutoTokenizer 类和 from_pretrained() 方法:

from transformers import AutoTokenizer

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

有了 tokenizer 后,可以直接传递句子:

raw_inputs = [
    "I've been waiting for a HuggingFace course my whole life.",
    "I hate this so much!",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)

输出结果(PyTorch 张量):

{
    'input_ids': tensor([
        [  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172, 2607,  2026,  2878,  2166,  1012,   102],
        [  101,  1045,  5223,  2023,  2061,  2172,   999,   102,     0,     0,     0,     0,     0,     0,     0,     0]
    ]), 
    'attention_mask': tensor([
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
    ])
}

探索模型

加载模型

我们可以像使用 tokenizer 一样下载预训练模型:

from transformers import AutoModel

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)

这个模型只包含基本的 Transformer 模块,输出 隐状态(hidden states),也称为特征。

高维向量

Transformers 模块的向量输出通常有三个维度:

  • Batch size(批次大小):一次处理的序列数
  • Sequence length(序列长度):序列的长度
  • Hidden size(隐藏层大小):每个模型输入的向量维度

如果将预处理后的值输入到模型:

outputs = model(**inputs)
print(outputs.last_hidden_state.shape)

输出:

torch.Size([2, 16, 768])

模型头(Model Head)

模型头将隐状态转换为特定任务的输出,通常由一个或几个线性层组成。

┌──────────────────────────────────────────────────────┐
│          Transformer 模型 + Head 结构图               │
├──────────────────────────────────────────────────────┤
│                                                      │
│  Input IDs  →  Embedding Layer                      │
│                      ↓                               │
│              Transformer Layers                      │
│              (Self-Attention +                       │
│               Feed Forward)                          │
│                      ↓                               │
│              Hidden States                           │
│              [batch, seq, 768]                       │
│                      ↓                               │
│              ┌──────────────┐                       │
│              │  Model Head  │                       │
│              │ (Linear层)    │                       │
│              └──────────────┘                       │
│                      ↓                               │
│              Task-specific Output                   │
│              (分类/生成/问答等)                        │
│                                                      │
└──────────────────────────────────────────────────────┘

对于情感分类任务,我们使用 AutoModelForSequenceClassification

from transformers import AutoModelForSequenceClassification

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)

查看输出形状:

print(outputs.logits.shape)
# torch.Size([2, 2])

对输出进行后处理

模型输出的是 logits(对数几率),需要通过 SoftMax 转换为概率:

print(outputs.logits)
# tensor([[-1.5607,  1.6123],
#         [ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)

import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
# tensor([[4.0195e-02, 9.5980e-01],
#         [9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)

获取标签:

model.config.id2label
# {0: 'NEGATIVE', 1: 'POSITIVE'}

最终结果:

  • 第一句:消极 0.0402,积极 0.9598
  • 第二句:消极 0.9995,积极 0.0005

处理多个序列

模型需要批次输入

模型默认需要句子列表。发送单个句子会失败:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

sequence = "I've been waiting for a HuggingFace course my whole life."
tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = torch.tensor(ids)
# 这一行会失败
model(input_ids)  # IndexError

需要添加批次维度:

input_ids = torch.tensor([ids])
output = model(input_ids)
print("Logits:", output.logits)

填充输入(Padding)

处理不同长度的句子时需要填充:

batched_ids = [
    [200, 200, 200],
    [200, 200, tokenizer.pad_token_id],
]

但直接填充会影响注意力计算,需要使用 注意力掩码(attention mask)

注意力掩码

注意力掩码用 0 和 1 标识哪些 token 需要关注:

batched_ids = [
    [200, 200, 200],
    [200, 200, tokenizer.pad_token_id],
]

attention_mask = [
    [1, 1, 1],
    [1, 1, 0],
]

outputs = model(
    torch.tensor(batched_ids), 
    attention_mask=torch.tensor(attention_mask)
)
print(outputs.logits)

更长的句子

大多数模型处理 512 或 1024 个 token 的序列。解决方案:

  1. 使用支持更长序列的模型(如 Longformer、LED)
  2. 截断序列:
sequence = sequence[:max_sequence_length]

Tokenizer 详解

Tokenization 算法

基于单词(Word-based)

tokenized_text = "Jim Henson was a puppeteer".split()
# ['Jim', 'Henson', 'was', 'a', 'puppeteer']
基于单词的分词示例:
┌─────────────────────────────────────────────────┐
│  原始文本: "Jim Henson was a puppeteer"         │
│                    ↓                            │
│  分词结果: [Jim] [Henson] [was] [a] [puppeteer] │
│                    ↓                            │
│  Token IDs: [5234] [6721] [1098] [23] [9876]   │
└─────────────────────────────────────────────────┘

优点:简单易用 缺点:词汇表很大,相似词被视为不同(如 "dog" 和 "dogs")

基于字符(Character-based)

基于字符的分词示例:
┌──────────────────────────────────────────────────┐
│  原始文本: "Hello"                                │
│                ↓                                 │
│  分词结果: [H] [e] [l] [l] [o]                   │
│                ↓                                 │
│  Token IDs: [72] [101] [108] [108] [111]        │
└──────────────────────────────────────────────────┘

优点:词汇表小,未知 token 少 缺点:单个字符语义信息少,序列长度大幅增加

基于子词(Subword)

结合两种方法的优点,常用词保持完整,罕见词分解为子词。

基于子词的分词示例:
┌──────────────────────────────────────────────────────┐
│  原始文本: "Let's do tokenization!"                   │
│                        ↓                             │
│  分词结果: [Let] ['s] [do] [token] [ization] [!]    │
│                        ↓                             │
│  Token IDs: [2421] [188] [342] [19204] [2734] [999] │
└──────────────────────────────────────────────────────┘

例如:"annoyingly" → "annoying" + "ly"
     "tokenization" → "token" + "ization"

常见算法:

  • Byte-level BPE(GPT-2)
  • WordPiece(BERT)
  • SentencePiece/Unigram(多语言模型)

加载和保存 Tokenizer

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

# 使用
tokenizer("Using a Transformer network is simple")

# 保存
tokenizer.save_pretrained("directory_on_my_computer")

编码过程

编码分两步:分词转换为 ID

分词

sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens)
# ['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']

转换为 ID

ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
# [7993, 170, 11303, 1200, 2443, 1110, 3014]

解码

decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
# 'Using a Transformer network is simple'

综合应用

Tokenizer 的高级功能:

from transformers import AutoTokenizer

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

# 单个句子
sequence = "I've been waiting for a HuggingFace course my whole life."
model_inputs = tokenizer(sequence)

# 多个句子
sequences = [
    "I've been waiting for a HuggingFace course my whole life.", 
    "So have I!"
]
model_inputs = tokenizer(sequences)

# 填充
model_inputs = tokenizer(sequences, padding="longest")
model_inputs = tokenizer(sequences, padding="max_length")
model_inputs = tokenizer(sequences, padding="max_length", max_length=8)

# 截断
model_inputs = tokenizer(sequences, truncation=True)
model_inputs = tokenizer(sequences, max_length=8, truncation=True)

# 返回特定框架的张量
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")  # PyTorch
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")  # TensorFlow
model_inputs = tokenizer(sequences, padding=True, return_tensors="np")  # NumPy

特殊 Token

Tokenizer 会自动添加特殊 token:

sequence = "I've been waiting for a HuggingFace course my whole life."
model_inputs = tokenizer(sequence)
print(tokenizer.decode(model_inputs["input_ids"]))
# "[CLS] i've been waiting for a huggingface course my whole life. [SEP]"

完整流程

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

sequences = [
    "I've been waiting for a HuggingFace course my whole life.", 
    "So have I!"
]

tokens = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
output = model(**tokens)

章末小测验

1. 自然语言处理流程的顺序是什么?

❌ 首先是模型它处理文本并返回原始预测。然后 tokenizer 会对这些预测进行解释,并在将它们转换回文本

❌ 首先 Tokenizer 处理文本并返回 id。模型根据这些 id 并输出预测,可以是一些文本。

Tokenizer 处理文本并返回 ID。模型处理这些 ID 并输出预测。然后可以再次使用 tokenizer 将这些预测转换回文本。


2. Transformer 模型输出张量的维度是?

❌ 2 个维度:序列长度(Sequence Length)和批次大小(Batch Size)

❌ 2 个维度:序列长度(Sequence Length)和隐藏层大小(Hidden Size)

3 个维度:序列长度(Sequence Length)、批次大小(Batch Size)和隐藏层大小(Hidden Size)


3. 下列哪些是子词分词的例子?

WordPiece

❌ 基于单个字符的分词

❌ 基于空格和标点符号的分割

BPE (Byte Pair Encoding)

Unigram

❌ 以上都不是


4. 什么是模型头(Head)?

❌ 原始 Transformer 网络的一种组件,直接将张量输入到正确的层

❌ 也称为自注意力(self-attention)机制,它会根据序列的其他 tokens 调整一个 token 的表示

一个附加组件,通常由一个或几个层组成,用于将 Transformer 的预测转换为特定任务的输出


5. 什么是 AutoModel?

❌ 根据你的数据自动进行训练的模型

一个根据 checkpoint 返回模型体系结构的对象

❌ 一种可以自动检测输入语言来加载正确权重的模型


6. 批处理不同长度的句子需要哪些处理?

截断

❌ 直接将 Tensors 返回

填充

注意力掩码(Attention masking)


7. 对 logits 使用 SoftMax 的意义?

❌ 它软化了 logits 输出,使结果更可靠

❌ 它限定了上下界,使模型的输出结果可以被解释

输出的和是 1,从而产生概率解释


8. Tokenizer API 的核心方法是?

encode 因为它可以将文本编码为 ID,将预测的 ID 解码为文本

直接调用 Tokenizer 对象

pad(填充)

tokenize


9. tokenizer.tokenize("Hello!") 返回什么?

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
result = tokenizer.tokenize("Hello!")

字符串列表,每个字符串都是一个 token

❌ 一个 ID 的列表

❌ 包含所有分词后的字符串


10. 以下代码有什么错误?

from transformers import AutoTokenizer, AutoModel

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
model = AutoModel.from_pretrained("gpt2")

encoded = tokenizer("Hey!", return_tensors="pt")
result = model(**encoded)

❌ 不,看起来是对的

Tokenizer 和模型应该来自相同的 checkpoint

❌ 由于模型输入需要是一个 Batch,因此可以使用 tokenizer 对其进行截断或填充来改进这段代码


总结

恭喜完成本章学习!你已经掌握:

  • Transformer 模型的基本构造
  • Tokenizer 管道的组成
  • 如何使用 Transformers 模型
  • 如何利用 tokenizer 转换文本
  • Input IDs 的局限性和注意力掩码
  • 灵活可配置的 Tokenizer 方法