GPT‑1(Generative Pre-trained Transformer 1)是 OpenAI 于 2018 年 发布的第一个「生成式预训练 Transformer」模型。虽然与后来 GPT‑2、GPT‑3 甚至 GPT‑4 相比,它的规模很小(约 1.17 亿参数),但它奠定了整个 GPT 系列的核心理念。
下面是 GPT‑1 的核心功能与创新点:
🌱 一、核心功能
-
自然语言生成(NLG)
GPT‑1 能根据输入的文本上下文生成连贯的自然语言文本,例如补全句子、回答简单问题、写段落等。 -
文本理解与表示
通过在大规模语料上预训练,它能学习语言的结构和语义,理解词与句子之间的关系,可用于分类、问答、情感判断等任务。 -
迁移学习能力
它首次展示了「预训练 + 微调(Pre-training + Fine-tuning)」的强大效果:- 先在海量无监督语料上进行语言建模预训练;
- 再在特定任务上通过少量标注数据进行微调。
这种模式后来成为 NLP 的主流范式。
⚙️ 二、技术核心
-
Transformer 编码结构(只使用解码器部分)
基于自注意力机制(Self-Attention),可以捕捉长距离依赖关系。 -
单向语言建模
GPT‑1 使用的是**单向(从左到右)**的语言模型,预测下一个词的概率。 -
通用文本表示
通过预训练得到的语言表征可用于各种下游任务,这意味着模型学习到了一种通用的语言理解能力。
🧠 三、重要意义
- GPT‑1 证明了大规模预训练语言模型可以显著提升下游任务性能。
- 它开创了后来「GPT 系列」的技术路线。
- 为 GPT‑2、GPT‑3 的大规模无监督学习奠定了实验与理论基础。
如果你想,我可以帮你画一张 GPT‑1 的架构简图或和 GPT‑2、GPT‑3 的对比表,看它是如何一步步演进的。你希望我展示哪一种?
这是个很好的问题。
虽然 GPT‑1 本身已经比较老(原始模型和权重并未公开),但我们可以用它的原理来模拟:即「预训练语言模型 + 微调到情感分类任务」的流程。下面我用 PyTorch(配合 Hugging Face 的 transformers
库)演示一个近似的 GPT‑1 情感分类流程。
我们可以用 DistilGPT‑2 或 GPT‑2 small 来代替 GPT‑1(结构类似,但参数更多),以体现 GPT‑1 的思路。
💻 示例代码:微调 GPT 模型进行情感分类
# 安装依赖 !pip install transformers datasets torch -qfrom transformers import GPT2Tokenizer, GPT2ForSequenceClassification, Trainer, TrainingArguments from datasets import load_dataset import torch# 1. 加载数据集(IMDB 为经典情感分类任务) dataset = load_dataset("imdb") dataset = dataset.shuffle(seed=42)# 2. 加载 GPT2 的分词器和模型 tokenizer = GPT2Tokenizer.from_pretrained("gpt2") tokenizer.pad_token = tokenizer.eos_token # GPT 没有 pad_token,用 eos 代替# 3. 数据预处理 def tokenize(batch):return tokenizer(batch["text"], truncation=True, padding="max_length", max_length=128)tokenized = dataset.map(tokenize, batched=True) tokenized.set_format(type="torch", columns=["input_ids", "attention_mask", "label"])train_set = tokenized["train"].select(range(2000)) # 为演示缩小数据规模 test_set = tokenized["test"].select(range(500))# 4. 定义模型(GPT2 + 分类头) model = GPT2ForSequenceClassification.from_pretrained("gpt2", num_labels=2) model.config.pad_token_id = tokenizer.eos_token_id# 5. 设置训练参数 args = TrainingArguments(output_dir="./results",evaluation_strategy="epoch",learning_rate=2e-5,per_device_train_batch_size=2,per_device_eval_batch_size=2,num_train_epochs=2,weight_decay=0.01,logging_dir="./logs", )# 6. 训练模型 trainer = Trainer(model=model,args=args,train_dataset=train_set,eval_dataset=test_set,tokenizer=tokenizer, )trainer.train()# 7. 测试预测 text = "The movie was absolutely wonderful!" inputs = tokenizer(text, return_tensors="pt", padding=True) with torch.no_grad():logits = model(**inputs).logitspred = torch.argmax(logits, dim=-1).item()print("情感预测:", "正面" if pred == 1 else "负面")
🔍 原理说明
- 预训练阶段(在 GPT‑1 中)
使用大规模语料做语言建模训练:
-
微调阶段
把最后的语言模型输出接一个 分类头(Linear Layer),用标注数据(如正/负情感标签)做监督微调。 -
推理阶段
输入一句话,模型根据最后一层隐藏状态预测情感标签。
要模拟 真正的 GPT‑1 实验配置(1.17 亿参数、12 层、768 隐藏维度),我们只能用类似规模的 GPT‑2 small;
若你想要我写一份更**轻量级、资源友好版(可在笔记本本地跑)**的代码,我也可以帮你重写一份简化版本。
是否希望我帮你简化一下(比如改成更小模型、更快训练)?
非常好的观察 👍
你说得对——从流程上看,GPT‑1 和 BERT 在情感分类任务上的方法确实非常相似,都属于「预训练 + 微调」范式。
但两者在结构、预训练目标、信息流方向、适用场景上有一些关键差别,这些差别会导致在具体任务(比如情感分类)上表现和使用方式并不完全相同。
让我们细看👇
🧠 一、相同点
项目 | GPT‑1 | BERT |
---|---|---|
核心思想 | 预训练语言模型,然后针对具体任务微调 | 同上 |
微调思路 | 增加任务特定的分类头,训练判别情感(正/负等) | 同上 |
微调目标 | 监督分类损失(例如交叉熵) | 同上 |
应用方式 | 适合用同样的 HuggingFace 接口训练 | 同上 |
👉 所以从代码角度看,你几乎可以把 GPT2ForSequenceClassification
换成 BertForSequenceClassification
,逻辑完全通。
⚙️ 二、本质区别
核心差异不在“用法”,而在模型结构和预训练目标👇
维度 | GPT‑1 / GPT‑2 风格 | BERT 风格 |
---|---|---|
结构方向 | 单向(从左到右)语言模型 | 双向(上下文同时可见) |
预训练任务 | 语言建模(预测下一个单词) | Masked Language Modeling(预测被遮盖的单词)+ Next Sentence Prediction |
信息利用 | 只能看到前文上下文,语义特征偏向生成 | 同时看到前后文,更适合理解类任务 |
应用类型 | 更偏生成(写作、对话、补全) | 更偏理解(分类、匹配、问答) |
推理方式 | 计算最后 token 的隐藏状态作为特征 | 取 [CLS] 标记的向量做分类 |
💬 三、在情感分类任务中的影响
- BERT 通常表现更好,因为它能同时理解整句话的上下文(双向信息让判断更准确)。
- GPT‑类模型也能做分类,但它天生更适合“生成”任务;分类只是把生成模型“挤”成判别模型使用的一种方式。
- 随着模型规模增大(GPT‑3、GPT‑4 级别),GPT‑类模型通过「少样本」甚至「零样本学习」已能弥补这种结构劣势。
✳️ 总结一句话
GPT‑1 和 BERT 的情感分类在方法流程上相同(预训练+微调),但在模型结构和预训练目标上不同,因此各自的语言理解偏好不一样。
BERT 更像「读者」,GPT 更像「作者」。
如果你愿意,我可以帮你写一个对比的 BERT 版本代码(与前面的 GPT‑风格代码对应),这样你能直观看出两者在输入、输出、训练逻辑上的细微差异。
要我帮你写这个对比版本吗?
当然可以 👍
下面我给出一个与前面 GPT‑2 情感分类版本完全对应的 BERT 版本代码。
这个例子同样使用 Hugging Face 的 transformers
库,并以 IMDB 情感分类 为例。
这样你可以清楚看到:
两者的代码框架几乎一样,主要区别在于
- 模型类从
GPT2ForSequenceClassification
→BertForSequenceClassification
- 分词器也换成了
BertTokenizer
- BERT 输入包含
[CLS]
、[SEP]
这些标记。
💻 示例代码:使用 BERT 进行情感分类
# 安装依赖 !pip install transformers datasets torch -qfrom transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments from datasets import load_dataset import torch# 1. 加载数据集 dataset = load_dataset("imdb") dataset = dataset.shuffle(seed=42)# 2. 加载 BERT 的分词器和模型 tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")# 3. 数据预处理 def tokenize(batch):return tokenizer(batch["text"], truncation=True, padding="max_length", max_length=128)tokenized = dataset.map(tokenize, batched=True) tokenized.set_format(type="torch", columns=["input_ids", "attention_mask", "label"])train_set = tokenized["train"].select(range(2000)) # 演示用较小数据 test_set = tokenized["test"].select(range(500))# 4. 定义模型(BERT + 分类头) model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)# 5. 设置训练参数 args = TrainingArguments(output_dir="./bert_results",evaluation_strategy="epoch",learning_rate=2e-5,per_device_train_batch_size=4,per_device_eval_batch_size=4,num_train_epochs=2,weight_decay=0.01,logging_dir="./bert_logs", )# 6. 训练 trainer = Trainer(model=model,args=args,train_dataset=train_set,eval_dataset=test_set,tokenizer=tokenizer, )trainer.train()# 7. 测试推理 text = "The movie was absolutely wonderful!" inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True) with torch.no_grad():logits = model(**inputs).logitspred = torch.argmax(logits, dim=-1).item()print("情感预测:", "正面" if pred == 1 else "负面")
🔬 和 GPT‑版本的核心区别
项目 | GPT‑版 | BERT‑版 |
---|---|---|
模型类 | GPT2ForSequenceClassification |
BertForSequenceClassification |
分词器 | GPT2Tokenizer (无 [CLS]/[SEP]) |
BertTokenizer (自动加 [CLS]/[SEP]) |
模型结构 | 单向 Transformer 解码器 | 双向 Transformer 编码器 |
特征来源 | 最后 token 的隐藏状态 | [CLS] token 的隐藏状态 |
预训练任务 | 语言建模(预测下一个词) | Masked LM + NSP |
擅长任务 | 生成、续写、少样本推理 | 理解、判别、分类 |
💡 结论:
- 从代码和接口层面看,两者「几乎一样」,可以互换。
- 从底层机制看,BERT 的双向上下文理解更适合情感判别或文本分类;
GPT‑类模型则更偏向生成型任务,但在大模型时代(GPT‑3/4)这种差距逐渐被缩小。
是否希望我帮你画一个简单的「GPT 与 BERT 的结构对比图」?
比如直观地展示单向(左→右) vs 双向(←→)的注意力信息流?
你问得非常关键,这个细节很多人第一次用 BERT 时都会搞混 👏
确实,BERT 在输入文本时需要 [CLS]
和 [SEP]
特殊标记,但是否“自动加”,取决于你调用分词器的方式。
我们来把这个机制讲清楚👇
🔖 一、BERT 模型的输入格式(原理)
在原始的 BERT 论文中,每个输入序列都被处理成以下形式:
[CLS] 句子A [SEP] (句子B [SEP] 可选)
[CLS]
:放在开头,用于聚合整句话的语义。分类任务(例如情感分类)通常直接取它的隐藏向量做分类。[SEP]
:分隔句子(或标记单句结束)。
⚙️ 二、在 Hugging Face 里的实现细节
取决于你调用 tokenizer 的方式:
✅ 方式1:使用 encode_plus()
或直接调用 tokenizer(...)
inputs = tokenizer("I love this movie!", return_tensors="pt")
👉 会自动添加 [CLS]
和 [SEP]
。
解释:
tokenizer
在初始化时会有字段:pythontokenizer.cls_token = '[CLS]' tokenizer.sep_token = '[SEP]' tokenizer.add_special_tokens({'cls_token': '[CLS]', 'sep_token': '[SEP]'})
- 调用时,它会在内部加上这两个标记并生成对应的
token_type_ids
(区分句子 A/B)。
⚠️ 方式2:如果你手动 tokenize 或传入参数 add_special_tokens=False
inputs = tokenizer("I love this movie!", add_special_tokens=False)
👉 此时不会加 [CLS]
/ [SEP]
,输入就只是词元化结果。
这种做法通常只在下游自定义任务、预训练阶段或处理特定数据格式时使用。
📦 在你上面的示例代码中:
tokenizer(batch["text"], truncation=True, padding="max_length", max_length=128)
默认参数 add_special_tokens=True
✅
→ 所以 BERT 的 [CLS]
和 [SEP]
都会被自动添加。
换句话说:在我们的情感分类代码里,你不需要手动拼接
[CLS]
和[SEP]
,
tokenizer 会帮你做好,模型的输入张量中第一位和最后一位 token 就是它们。
🔍 想验证的话:
你可以执行以下小实验看看:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")tokens = tokenizer("I love this movie!", add_special_tokens=True)
print(tokens.tokens())
输出一般是:
['[CLS]', 'i', 'love', 'this', 'movie', '!', '[SEP]']
是否希望我进一步说明 [CLS]
的隐藏状态是如何被用作情感分类输入的?
我可以结合 BERT 的最后一层张量结构帮你可视化这个过程。
太好了 👍,你问到了 BERT 情感分类的关键环节。
理解 [CLS]
的作用,就能真正明白为什么 BERT 这么适合分类类任务。
🧠 一、BERT 输出结构回顾
当一句话经过 BERT 输入后,模型会输出一个三维张量:
last_hidden_state∈RB×T×Hlast_hidden_state∈RB×T×H- BB:batch size
- TT:序列长度(包括
[CLS]
、[SEP]
等) - HH:隐藏层维度(BERT-base 为 768)
也就是说,输出里每个 token(词元)都会对应一个 HH 维的向量表示。
🔖 二、[CLS] 的特殊位置
-
在输入时,
[CLS]
被插入在句首,它是第一个 token。 -
BERT 在预训练阶段就被显式训练过,使得
[CLS]
的隐藏状态可以聚合整句全局语义。 -
在下游任务(比如情感判别)中,我们通常做的事情就是取输出中第一个位置的向量,即:
这个 hCLShCLS 就是整句话的语义“摘要”。
⚙️ 三、分类头(Classification Head)
在微调阶段,我们通常在 BERT 顶部添加一个线性分类层:
logits=W⋅hCLS+blogits=W⋅hCLS+b- WW 是一个 [num_labels×H][num_labels×H] 的权重矩阵
- bb 是偏置项
logits
就是对每个情感类别(如正/负)的分数预测
最后经过 softmax 转为概率:
P(y∣X)=softmax(logits)P(y∣X)=softmax(logits)训练时使用交叉熵损失:
L=−∑iyilogP(yi∣X)L=−i∑yilogP(yi∣X)📦 四、对应到 Hugging Face 实现
当我们使用:
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
模型内部其实做了三件事:
- 加载预训练的 BERT 编码器;
- 取最后一层的
[CLS]
向量(隐含在模型 forward 中); - 在顶层自动加一个线性层 + dropout;
(这一步对应前面公式中的 W⋅hCLS+bW⋅hCLS+b)
所以在调用 model(**inputs)
时,框架自动完成:
- 前向传播获取
[CLS]
向量 - 计算 logits
- (若提供标签
labels
)自动返回 loss
🔍 五、可视化理解
假设输入序列是:
[CLS] The movie was great ! [SEP]
经过 BERT 后输出:
Token | 向量 | 用途 |
---|---|---|
[CLS] |
→ 语义汇聚向量 (768维) | 用于分类 |
The | → 局部词义向量 | 辅助特征 |
movie | → 局部词义向量 | 辅助特征 |
was | ... | ... |
great | ... | ... |
! | ... | ... |
[SEP] |
... | 句子结束标记 |
最终:
➡ 只取 [CLS]
的隐藏状态 → 全连接层 → softmax → 输出“正面/负面”。
💡 总结一句话:
[CLS]
就像句子的「语义代表」,BERT 把整句的理解都集中到它身上,
张量第一个位置的那根 768 维向量,就是分类器唯一真正关注的输入。
如果你愿意,我可以帮你画一张 小示意图(结构简图),展示 [CLS]
如何从输入流经 BERT 层最终进入分类头。
要我画一下吗?
你这个问题太精彩了👏——这正是实际情感分类(甚至所有文本分类)中最“微妙”的地方:
即使我们在实现上只取 [CLS]
向量做分类,但是模型在计算 [CLS]
向量的时候,其实已经融合了所有 token 的特征信息——包括那些出现在句尾、真正表达情感的词。
我们来把这个过程完整地拆开讲👇
🧩 一、[CLS] 并不是“孤立”的向量
虽然最终只拿 [CLS]
来分类,但它的语义是通过多层 Transformer 自注意力(self-attention) 把整句的内容「看过一遍」之后形成的。
在每一层的 self-attention 计算中,[CLS]
作为一个 query,会去“关注”句子中的所有 token:
也就是说:
[CLS]
的最终表示是所有词的加权融合;- 被“权重更高”的位置(例如“great”、“terrible”、“not good”)会对
[CLS]
的表示贡献更大。
所以从本质上讲,[CLS] 已经包含了情感词的语义影响。
💬 二、模型如何捕捉句尾的情感信息?
举例:
“前半段平平无奇,但最后一句真是太感人了!”
虽然情感关键词在“最后一句”,
但 BERT 的多层 attention 能让 [CLS]
在最终层把这种情感信号捕获进来。
例如在第 L
层,一部分 attention head 可能专注“句末词汇”,另一部分专注“否定词”等。
BERT 把这些不同注意力视角融合起来。
最终 [CLS]
像一个“收音机”,接收了整个句子范围的高频语义信号。
⚙️ 三、那中间这些 token 的向量(如 "great")到底起什么作用?
- 它们本身一般不会被直接用于分类(除非用 pooling 或 attention 池化结构)。
- 但在 BERT 的多层传播中,它们动态地影响了
[CLS]
的演化。
每层的自注意力都可以改变[CLS]
向量,使它更丰富、更“集体化”。
👉 所以,它们不是被丢弃,而是“间接贡献”。
🧠 四、如果希望模型更直接利用这些局部线索?
确实,在某些长文本或多情绪文本场景(如多句影评中最后一句最关键),
研究者往往会用更复杂的汇聚策略(pooling strategy),例如:
技术 | 思路 |
---|---|
Mean pooling / Max pooling | 对所有 token 向量取平均或最大值,再分类 |
Attention pooling | 学一个 attention 权重,聚合整句信息,而不是只取 [CLS] |
Hierarchical attention | 每句话提取一个向量,再对句子级向量做注意力汇聚(适合多句情感分析) |
最后一层 + 倒数几层拼接 | 取多层 hidden state 拼在一起,捕获不同层级信息 |
这些方法让模型能显式捕捉句尾或局部情感更强的 token。
✳️ 五、小结一句话:
[CLS]
是“加权汇聚所有词的结果”,理论上已经包含句尾的情感信息。
但如果任务对局部情感词特别敏感,可以改用更灵活的 pooling 策略来突出这些特征。
如果你想,我可以帮你写两段对比代码:
- 一段是标准
[CLS]
分类; - 一段是 mean pooling 或 attention pooling 的版本,看看性能和逻辑有什么不同。