ARTICLE · 1034764
AI + 量化开发工程师学习笔记 Day 11:完整训练流程——亲手训练第一个 NLP 情感分类模型
前 10 天,我们已经从神经网络一路走到了 GPT 的 Loss:
文本
↓
Tokenizer
↓
Token IDs
↓
Embedding
↓
Transformer
↓
Logits
↓
Cross Entropy
↓
Loss
Day10 最后还留下了半条链:
Loss
↓
???
↓
模型参数改变
↓
预测越来越准确
今天就把这个 ??? 完整解决:
Forward
↓
Loss
↓
zero_grad()
↓
backward()
↓
optimizer.step()
↓
重复训练
而且今天不是只看公式。
我们会真正训练一个 NLP 情感分类小模型。
今天结束以后,你应该第一次能完整回答:
“一个 PyTorch 模型从随机初始化,到学会分类,代码到底经历了什么?”
一、先明确今天要训练什么
我们先不训练 GPT。
GPT 正式训练留到 Day18。
今天做一个小得多、但训练流程完全真实的任务:
金融文本情感分类
输入:
公司净利润同比大幅增长
输出:
正面
输入:
公司预计全年出现重大亏损
输出:
负面
也就是:
金融文本
↓
Tokenizer
↓
Embedding
↓
Pooling
↓
Linear
↓
Logits
↓
Positive / Negative
这实际上已经是未来:
新闻情绪因子 公告情绪因子
的最小雏形。
二、为什么 Day11 不直接训练 Transformer?
因为今天真正要学习的是:
Training Loop。
如果同时加入:
Transformer Multi-Head Attention Mask Position DataLoader Scheduler Dropout
反而容易把训练本身淹没。
所以今天模型故意保持简单:
Token IDs
↓
Embedding
↓
Mean Pooling
↓
Linear
↓
2个 Logits
等训练机制彻底吃透以后:
Day16 → 组装 MiniGPT Day18 → 真正训练 MiniGPT
三、第一步:准备数据
今天先人为准备一个很小的数据集。
正面:
公司业绩大幅增长 净利润同比增长 公司获得重大订单 营业收入创新高 公司上调全年业绩预期
负面:
公司业绩大幅下降 净利润同比下滑 公司出现重大亏损 营业收入明显下降 公司下调全年业绩预期
标签:
Positive = 1
Negative = 0
所以训练数据本质:
Text Label
公司业绩大幅增长 1
公司出现重大亏损 0
净利润同比增长 1
净利润同比下滑 0
...
四、什么是 Label?
Day10 GPT 的 Target 是:
正确的下一个 Token ID
今天分类模型的 Target 是:
正确类别 ID
例如:
Negative = 0
Positive = 1
模型输出:
[negative_logit, positive_logit]
例如:
[-1.2, 3.7]
意味着模型更倾向:
Positive
然后:
Logits
+
Label
↓
Cross Entropy
↓
Loss
五、先做一个最简单 Tokenizer
今天暂时不用 Hugging Face。
因为我们的目标是看清完整训练流程。
使用:
字符级 Tokenizer
例如:
公司业绩增长
拆成:
公
司
业
绩
增
长
然后建立 Vocabulary:
<PAD> → 0
<UNK> → 1
公 → 2
司 → 3
业 → 4
绩 → 5
增 → 6
长 → 7
...
最终:
公司业绩增长
↓
[2, 3, 4, 5, 6, 7]
六、为什么需要 PAD?
不同文本长度不同:
公司盈利
可能:
4 Token
而:
公司预计全年净利润大幅增长
可能:
13 Token
神经网络 Batch 通常希望统一长度。
假设:
max_len = 16
短句:
[2, 3, 4, 5]
补成:
[2, 3, 4, 5, 0, 0, 0, 0, ...]
其中:
0 = PAD
Day4 学过的 Padding 今天真正开始进入训练。
七、完整数据准备代码
创建:
day11_sentiment.py
先写数据:
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(42)
samples = [
("公司业绩大幅增长", 1),
("净利润同比增长", 1),
("公司获得重大订单", 1),
("营业收入创新高", 1),
("公司上调全年业绩预期", 1),
("主营业务持续增长", 1),
("公司盈利能力明显改善", 1),
("产品销售收入快速增长", 1),
("公司业绩大幅下降", 0),
("净利润同比下滑", 0),
("公司出现重大亏损", 0),
("营业收入明显下降", 0),
("公司下调全年业绩预期", 0),
("主营业务持续恶化", 0),
("公司盈利能力明显下降", 0),
("产品销售收入大幅下滑", 0),
]
八、建立 Vocabulary
PAD_TOKEN = "<PAD>"
UNK_TOKEN = "<UNK>"
vocab = {
PAD_TOKEN: 0,
UNK_TOKEN: 1
}
for text, _ in samples:
for char in text:
if char notin vocab:
vocab[char] = len(vocab)
print(
"Vocabulary Size:",
len(vocab)
)
现在:
文字
↓
字符
↓
Vocabulary
↓
Token ID
九、实现 encode()
max_len = 16
defencode(text):
ids = []
for char in text:
token_id = vocab.get(
char,
vocab[UNK_TOKEN]
)
ids.append(token_id)
# 截断
ids = ids[:max_len]
# Padding
while len(ids) < max_len:
ids.append(
vocab[PAD_TOKEN]
)
return ids
测试:
print(
encode(
"公司业绩增长"
)
)
应该得到:
[若干Token ID, 0, 0, 0, ...]
十、把训练数据变成 Tensor
X = torch.tensor([
encode(text)
for text, label
in samples
])
y = torch.tensor([
label
for text, label
in samples
])
print(
"X Shape:",
X.shape
)
print(
"y Shape:",
y.shape
)
应该:
X Shape:
[16, 16]
y Shape:
[16]
解释:
16 条文本 × 每条 16 个 Token
十一、今天的模型结构
模型非常简单:
Token IDs
[B, T]
↓
Embedding
[B, T, C]
↓
Mean Pooling
[B, C]
↓
Linear
[B, 2]
↓
Logits
为什么要 Pooling?
因为一句话有:
T 个 Token
但最终只需要:
一个情绪分类
所以需要把:
T 个 Token 表示
压成:
一个句子表示
今天使用最简单的:
Mean Pooling
十二、什么是 Mean Pooling?
假设三个 Token:
Token1 = [1, 2, 3]
Token2 = [2, 3, 4]
Token3 = [3, 4, 5]
平均:
Sentence Vector
=
[2, 3, 4]
也就是:
把所有 Token Embedding 求平均。
不过这里有一个问题:
PAD
不能算进去。
否则 Padding 会污染句子表示。
所以我们要做:
Masked Mean Pooling
十三、定义情感分类模型
classSentimentModel(nn.Module):
def__init__(
self,
vocab_size,
embed_dim=32
):
super().__init__()
self.embedding = nn.Embedding(
vocab_size,
embed_dim,
padding_idx=0
)
self.classifier = nn.Linear(
embed_dim,
2
)
defforward(self, x):
# x:
# [B, T]
embeddings = self.embedding(
x
)
# [B, T, C]
# PAD Mask
mask = (
x != 0
).unsqueeze(-1)
# [B, T, 1]
embeddings = (
embeddings
*
mask
)
# 所有有效 Token 相加
summed = embeddings.sum(
dim=1
)
# [B, C]
# 有效 Token 数量
lengths = mask.sum(
dim=1
).clamp(
min=1
)
# [B, 1]
# Mean Pooling
pooled = (
summed
/
lengths
)
# [B, C]
logits = self.classifier(
pooled
)
# [B, 2]
return logits
十四、一定要看懂 Shape
输入:
X
[16, 16]
Embedding:
[16, 16, 32]
Pooling:
[16, 32]
Linear:
[16, 2]
所以:
[B, T]
↓
[B, T, C]
↓
[B, C]
↓
[B, 2]
最后:
2
代表:
Negative
Positive
十五、创建模型
model = SentimentModel(
vocab_size=len(vocab),
embed_dim=32
)
看看模型:
print(model)
大概:
SentimentModel(
embedding
classifier
)
这已经是一个真正可以训练的神经网络。
十六、Loss
今天是:
二分类
但模型输出两个 Logits:
Negative
Positive
所以使用:
criterion = nn.CrossEntropyLoss()
和 Day10 完全一致。
十七、Optimizer
今天暂时使用:
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.01
)
为什么 Adam 好?
今天先把它当成:
负责根据梯度修改参数的优化器。
Day13 会专门拆开:
SGD Momentum Adam AdamW
今天不要抢 Day13 内容。
十八、终于到了今天最核心的 Training Loop
epochs = 300
for epoch in range(epochs):
# =====================
# 1. Forward
# =====================
logits = model(X)
# =====================
# 2. Loss
# =====================
loss = criterion(
logits,
y
)
# =====================
# 3. 清空旧梯度
# =====================
optimizer.zero_grad()
# =====================
# 4. Backward
# =====================
loss.backward()
# =====================
# 5. 更新参数
# =====================
optimizer.step()
if epoch % 30 == 0:
print(
f"Epoch {epoch:3d} "
f"Loss={loss.item():.6f}"
)
这就是今天最重要的代码。
十九、逐行理解 Training Loop
1. Forward
logits = model(X)
模型:
输入
↓
Embedding
↓
Pooling
↓
Linear
↓
Logits
2. Loss
loss = criterion(
logits,
y
)
告诉模型:
你预测得有多差
3. zero_grad()
optimizer.zero_grad()
为什么?
因为 PyTorch 默认:
梯度会累加。
如果上一次:
grad = 0.3
这次:
grad = 0.2
不清空可能变成:
0.5
但普通训练每个 step 通常希望使用当前 batch 的梯度。
所以先:
Gradient → 0
二十、为什么 PyTorch 默认累加梯度?
因为有时候我们真的需要:
Gradient Accumulation
例如 GPU 一次只能放:
Batch = 8
但我们希望等效:
Batch = 32
可以:
Batch1
↓
Backward
不更新
Batch2
↓
Backward
不更新
Batch3
↓
Backward
不更新
Batch4
↓
Backward
↓
optimizer.step()
把四次梯度累积起来。
以后训练大模型时这个非常有用。
但今天普通训练:
optimizer.zero_grad()
每轮清空。
二十一、backward()
loss.backward()
计算:
Loss
对所有参数的:
Gradient
例如:
Embedding Weight Classifier Weight Classifier Bias
都会获得 .grad。
二十二、optimizer.step()
optimizer.step()
这才真正:
修改模型参数。
所以再次强调:
loss.backward()
=
算梯度
optimizer.step()
=
改参数
这是非常常见的面试题,也是真正训练模型时必须形成肌肉记忆的区别。
二十三、一个 Epoch 到底是什么?
今天:
X
包含全部:
16 条训练数据
我们一次全部输入。
所以:
完整看一遍全部训练数据
=
1 Epoch
训练:
epochs = 300
意味着模型把整个数据集看了:
300 遍
注意:
以后使用 DataLoader 后:
一个 Epoch
会包含很多:
Batch / Step
Day12 专门讲。
二十四、Epoch、Batch、Step 的关系
先提前建立概念:
假设:
Dataset = 1000
Batch Size = 100
那么:
1 Epoch
=
10 Batch
=
10 Step
训练:
20 Epoch
就是:
200 Step
所以:
Epoch= 完整遍历数据集一次 Batch= 一次送进模型的数据 Step= 一次参数更新
Day12 会详细实践。
二十五、运行训练
运行:
python day11_sentiment.py
你应该看到类似趋势:
Epoch 0 Loss=0.72
Epoch 30 Loss=0.30
Epoch 60 Loss=0.08
Epoch 90 Loss=0.02
...
Epoch 270 Loss=0.001
具体数值可能不同。
关键不是数字,而是:
Loss
↓
不断下降
这意味着:
模型正在越来越适应训练数据。
注意我特意说的是:
适应训练数据
而不是:
模型一定越来越聪明。
这个区别 Day14 讲 Overfitting 时非常重要。
二十六、训练完成后怎么预测?
写:
defpredict(text):
model.eval()
ids = torch.tensor([
encode(text)
])
with torch.no_grad():
logits = model(
ids
)
probs = F.softmax(
logits,
dim=-1
)
pred = torch.argmax(
probs,
dim=-1
).item()
label = (
"正面"
if pred == 1
else"负面"
)
return (
label,
probs[0].tolist()
)
二十七、测试模型
test_texts = [
"公司净利润大幅增长",
"公司出现严重亏损",
"营业收入持续增长",
"公司盈利明显下降",
]
for text in test_texts:
label, probs = predict(
text
)
print(
text,
"->",
label,
probs
)
你可能看到:
公司净利润大幅增长
→ 正面
公司出现严重亏损
→ 负面
营业收入持续增长
→ 正面
公司盈利明显下降
→ 负面
但是由于数据只有 16 条:
不要把结果好看理解成模型已经拥有真正的金融语义理解能力。
这是一个教学模型。
二十八、model.train() 和 model.eval() 有什么区别?
PyTorch 模型有两种工作模式:
model.train()
表示:
训练模式
而:
model.eval()
表示:
评估 / 推理模式
现在我们的模型只有:
Embedding Pooling Linear
所以两者暂时看不出明显差异。
但是以后加入:
Dropout BatchNorm
以后,两种模式行为会不同。
例如 Dropout:
model.train()
神经元随机丢弃
而:
model.eval()
关闭 Dropout
所以推荐养成习惯:
# 训练
model.train()
# 推理
model.eval()
注意:
model.eval()不会关闭梯度计算。
所以推理时我们通常还写:
with torch.no_grad():
两者作用不同:
model.eval()
→ 改变某些层的运行行为
torch.no_grad()
→ 不构建梯度计算图
二十九、把 Training Loop 写得更规范
刚才我们的代码:
for epoch in range(epochs):
logits = model(X)
loss = criterion(
logits,
y
)
optimizer.zero_grad()
loss.backward()
optimizer.step()
更规范一点:
for epoch in range(epochs):
# 进入训练模式
model.train()
# 清梯度
optimizer.zero_grad()
# Forward
logits = model(X)
# Loss
loss = criterion(
logits,
y
)
# Backward
loss.backward()
# Update
optimizer.step()
你可能发现:
optimizer.zero_grad()
被我移到了 Forward 前面。
两种写法在这里都可以。
我更建议以后形成固定习惯:
zero_grad
↓
forward
↓
loss
↓
backward
↓
step
三十、一个 Step 到底发生了什么?
我们把这一轮彻底拆开。
初始参数:
W₀
Step 1:Forward
X
↓
Model(W₀)
↓
Logits
Step 2:计算 Loss
Logits
+
Target
↓
CrossEntropy
↓
Loss = 0.73
Step 3:Backward
计算:
∂Loss / ∂W
得到:
Gradient
它告诉优化器:
参数往哪个方向变化,可以让 Loss 下降。
Step 4:Optimizer
W₁
=
W₀
-
更新量
参数从:
W₀
变成:
W₁
Step 5:再次 Forward
X
↓
Model(W₁)
↓
新的 Logits
↓
新的 Loss
如果训练正常:
Loss
0.73
↓
0.68
↓
0.59
↓
0.42
↓
...
不断下降。
所以:
训练不是模型突然“懂了”,而是参数经过大量微小更新,逐渐形成能够降低 Loss 的映射关系。
三十一、今日实践升级版:亲眼看参数改变
这个实验我非常建议你运行。
创建:
day11_parameter_update.py
代码:
import torch
import torch.nn as nn
torch.manual_seed(42)
model = nn.Linear(
2,
2
)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(
model.parameters(),
lr=0.1
)
X = torch.tensor([
[1.0, 2.0]
])
y = torch.tensor([
1
])
print("训练前 Weight:")
print(
model.weight.data.clone()
)
# ==========================
# 一个完整 Training Step
# ==========================
model.train()
optimizer.zero_grad()
logits = model(
X
)
loss = criterion(
logits,
y
)
loss.backward()
print(
"\nGradient:"
)
print(
model.weight.grad
)
optimizer.step()
print(
"\n训练后 Weight:"
)
print(
model.weight.data
)
print(
"\nLoss:"
)
print(
loss.item()
)
你会真正看到:
训练前:
Weight A
执行:
loss.backward()
产生:
Gradient
执行:
optimizer.step()
以后:
训练后:
Weight B
并且:
A ≠ B
这就是:
学习
从计算机角度说,没有任何神秘之处:
参数
↓
Gradient
↓
修改参数
三十二、再做一个更漂亮的实验:记录 Loss Curve
修改情感模型训练代码:
loss_history = []
for epoch in range(epochs):
model.train()
optimizer.zero_grad()
logits = model(
X
)
loss = criterion(
logits,
y
)
loss.backward()
optimizer.step()
loss_history.append(
loss.item()
)
训练完成:
import matplotlib.pyplot as plt
plt.plot(
loss_history
)
plt.xlabel(
"Epoch"
)
plt.ylabel(
"Loss"
)
plt.title(
"Training Loss"
)
plt.show()
你应该看到大致:
Loss
│\
│ \
│ \
│ \
│ \____
│
└──────────── Epoch
这张图以后非常重要。
真正训练模型的时候,我们不会只看:
最后 Loss = 0.01
而是看:
Loss Curve
因为曲线可以告诉我们很多问题:
Loss 不下降 → 模型没学会 Loss 剧烈震荡 → Learning Rate 可能过大 Train Loss 下降、Validation Loss 上升 → 可能 Overfitting
这些 Day14、Day15 会继续研究。
三十三、现在有一个严重的问题
我们的训练:
model(X)
一次把全部:
16 条数据
送进模型。
真实数据可能:
100 万条新闻
显然不能:
model(
100万条数据
)
一次全部送 GPU。
所以需要:
Dataset
↓
DataLoader
↓
Batch
例如:
100万条数据
↓
Batch Size = 64
↓
每次训练 64 条
↓
训练完一个 Batch
↓
再拿下一个 Batch
这就是:
Mini-Batch Training
而这正是 Day12 的主题。
三十四、今天这个模型与未来量化有什么关系?
现在我们的模型:
金融文本
↓
Embedding
↓
Pooling
↓
Classifier
↓
Positive / Negative
未来真正做新闻因子:
财经新闻
↓
LLM / Transformer
↓
情绪分析
↓
Sentiment Score
例如:
新闻A
→ +0.82
新闻B
→ -0.71
公告C
→ +0.35
然后:
股票
↓
一天所有新闻
↓
聚合
↓
News Sentiment Factor
最终:
PE
PB
ROE
Momentum
News Sentiment
Announcement Event
↓
Quant Model
↓
Future Return
所以今天虽然只是一个 16 条数据的小模型,它已经第一次把:
NLP
和:
Quant
真正连起来了。
三十五、一个非常重要的提醒:训练集预测准不代表模型好
我们的 16 条数据:
训练
↓
Loss ≈ 0
然后测试:
公司净利润大幅增长
→ 正面
看起来非常厉害。
但可能只是:
Memorization
也就是:
把训练数据记住了。
真正需要的是:
Generalization
例如模型训练从没见过:
公司前三季度盈利水平显著改善
它仍然判断:
正面
这才叫泛化。
所以真正机器学习必须:
Dataset
│
├── Training Set
│
├── Validation Set
│
└── Test Set
不能:
训练数据
=
测试数据
否则你根本不知道模型是否真的学会规律。
这个问题 Day14 的 Overfitting 会重点讲。
三十六、Day11 今日作业
理论题
一个完整 Training Step 包含哪几个核心步骤? optimizer.zero_grad()为什么需要?loss.backward()和optimizer.step()有什么区别?Epoch、Batch、Step 分别是什么? 为什么训练时使用 model.train(),推理时使用model.eval()?model.eval()和torch.no_grad()是一回事吗?为什么训练 Loss 持续下降不代表模型一定很好? 什么叫 Generalization? 为什么真实训练通常使用 Mini-Batch,而不是一次把所有数据送进模型? 今天情感分类模型为什么需要 Pooling?
三十七、作业参考答案
1. Training Step
核心:
zero_grad()
↓
Forward
↓
Loss
↓
Backward
↓
optimizer.step()
对应:
optimizer.zero_grad()
logits = model(X)
loss = criterion(
logits,
y
)
loss.backward()
optimizer.step()
2. 为什么 zero_grad?
因为 PyTorch 默认会:
累加 Gradient
普通训练通常希望当前 Step 使用当前 Batch 的梯度,因此每次训练前清空旧梯度。
3. backward 和 step
loss.backward()
负责:
计算梯度。
结果进入:
parameter.grad
而:
optimizer.step()
负责:
根据梯度真正修改参数。
所以:
Backward
=
算怎么改
Step
=
真的改
4. Epoch / Batch / Step
假设:
Dataset = 1000
Batch Size = 100
那么:
Batch
=
每次 100 条
Step
=
一次参数更新
1 Epoch
=
完整看完 1000 条
=
10 Step
5. train / eval
model.train()
告诉模型:
现在处于训练阶段
某些层按照训练方式工作。
model.eval()
告诉模型:
现在处于评估/推理阶段
例如 Dropout 会关闭随机丢弃行为。
6. eval 和 no_grad 一样吗?
不一样。
model.eval()
改变模型中某些层的行为。
而:
torch.no_grad()
关闭梯度计算。
所以推理通常:
model.eval()
with torch.no_grad():
logits = model(X)
两个一起使用。
7. 为什么 Loss 下降不代表模型一定好?
因为可能出现:
Overfitting
模型只是记住:
Training Data
却不能处理没见过的数据。
所以必须观察:
Training Performance
+
Validation Performance
+
Test Performance
8. Generalization
泛化能力就是:
模型对训练过程中没有见过的新数据,仍然能够做出合理预测的能力。
这比“记住训练集”重要得多。
9. 为什么 Mini-Batch?
真实数据可能非常大,无法一次全部放进内存/GPU。
因此:
Dataset
↓
Batch1
↓
Step
Batch2
↓
Step
Batch3
↓
Step
...
既节省内存,也形成现代神经网络最常见的训练方式。
10. 为什么需要 Pooling?
Embedding 输出:
[B, T, C]
即一句话有:
T 个 Token 表示
但情感分类需要:
一个句子
→
一个类别
所以需要把 Token 表示聚合成:
[B, C]
今天使用:
Masked Mean Pooling
然后:
[B, C]
↓
Linear
↓
[B, 2]
完成句子分类。
三十八、今天建议真正完成的三个实验
目录:
day11/
├── day11_sentiment.py
├── day11_parameter_update.py
└── day11_loss_curve.py
如果今天只有一个小时,优先级:
day11_sentiment.pyday11_parameter_update.pyloss curve
尤其第一个一定亲手跑。
因为今天是课程第一次真正完成:
文本
↓
数据处理
↓
模型
↓
Loss
↓
Backward
↓
Optimizer
↓
训练
↓
预测新文本
这个闭环。
Day 11 最重要的一句话
训练神经网络,就是不断执行 Forward → Loss → Backward → Update,让参数沿着降低 Loss 的方向反复调整。
到这里,我们已经从:
Day2
“知道 Training Loop 长什么样”
走到了:
Day11
“真正训练一个 NLP 模型”
而今天我们还有一个明显不专业的地方:
X = torch.tensor(...)
y = torch.tensor(...)
model(X)
我们自己把全部数据一次塞进模型。
真正工程训练应该是:
原始数据
↓
Dataset
↓
DataLoader
↓
Shuffle
↓
Batch
↓
Training Loop
所以 Day11 到这里完整结束。
下一课严格按照正式计划进入:
Day 12:PyTorch Dataset / DataLoader + Mini-Batch Training
我们会把今天的金融情感分类数据重新工程化,自己实现 Dataset,使用 DataLoader 按 Batch 训练,并真正看清:
Dataset
Epoch
Batch
Step
Shuffle
之间到底是什么关系。
附录:三个程序怎么分工
先把文中三个程序的定位说清楚:
day11_sentiment.py | ||
day11_parameter_update.py | backward()和 step()如何改变参数 | |
day11_loss_curve.py |
所以今天真正应该重点研究和运行的是:
day11_sentiment.py
它的实际使用方式就是:
输入:
"公司净利润大幅增长"
↓ 模型
输出:
正面
正面概率:xx%
负面概率:xx%
下面三个程序都是可以独立运行的完整版本。
附录 A:核心程序 day11_sentiment.py
这个才是今天真正的:
金融文本情感分类模型
完整流程:
训练文本
↓
建立 Vocabulary
↓
Tokenizer / Encode
↓
Token IDs
↓
Embedding
↓
Masked Mean Pooling
↓
Linear
↓
Positive / Negative Logits
↓
CrossEntropyLoss
↓
Backward
↓
Adam
↓
训练完成
↓
输入新文本
↓
情感分类
import torch
import torch.nn as nn
import torch.nn.functional as F
# ============================================================
# 1. 固定随机种子
# ============================================================
#
# 神经网络初始化包含随机过程。
#
# 固定随机种子以后,多次运行程序时,
# 初始参数尽量保持一致,方便学习和实验对比。
#
torch.manual_seed(42)
# ============================================================
# 2. 准备训练数据
# ============================================================
#
# 每条数据:
#
# (
# 文本,
# 标签
# )
#
# 标签定义:
#
# 0 = Negative 负面
# 1 = Positive 正面
#
samples = [
# -------------------------
# Positive
# -------------------------
("公司业绩大幅增长", 1),
("净利润同比增长", 1),
("公司获得重大订单", 1),
("营业收入创新高", 1),
("公司上调全年业绩预期", 1),
("主营业务持续增长", 1),
("公司盈利能力明显改善", 1),
("产品销售收入快速增长", 1),
# -------------------------
# Negative
# -------------------------
("公司业绩大幅下降", 0),
("净利润同比下滑", 0),
("公司出现重大亏损", 0),
("营业收入明显下降", 0),
("公司下调全年业绩预期", 0),
("主营业务持续恶化", 0),
("公司盈利能力明显下降", 0),
("产品销售收入大幅下滑", 0),
]
# ============================================================
# 3. 定义特殊 Token
# ============================================================
PAD_TOKEN = "<PAD>"
UNK_TOKEN = "<UNK>"
# ============================================================
# 4. 建立 Vocabulary
# ============================================================
#
# 今天为了让整个过程足够透明,
# 使用最简单的“字符级 Tokenizer”。
#
# 例如:
#
# 公司业绩增长
#
# 会被拆成:
#
# 公
# 司
# 业
# 绩
# 增
# 长
#
# 然后每个字符获得一个整数 ID。
#
vocab = {
# Padding
PAD_TOKEN: 0,
# Unknown
UNK_TOKEN: 1
}
# 遍历全部训练文本
for text, label in samples:
# 遍历每个汉字
for char in text:
# 如果这个字符还没有进入词表
if char notin vocab:
# 给它分配一个新的 Token ID
vocab[char] = len(vocab)
print(
"Vocabulary Size:",
len(vocab)
)
print(
"\nVocabulary:"
)
print(vocab)
# ============================================================
# 5. 文本最大长度
# ============================================================
#
# 为了组成 Batch,
# 每句话最终都变成固定长度。
#
MAX_LEN = 16
# ============================================================
# 6. 编写 Tokenizer / Encoder
# ============================================================
defencode(text):
"""
把中文字符串转换成固定长度的 Token ID 数组。
例如:
公司增长
↓
[2, 3, 10, 11, 0, 0, ...]
其中:
0 = PAD
"""
ids = []
# -------------------------
# 文本 -> Token ID
# -------------------------
for char in text:
token_id = vocab.get(
char,
# 没见过的字符使用 UNK
vocab[UNK_TOKEN]
)
ids.append(
token_id
)
# -------------------------
# Truncation
# -------------------------
#
# 如果文本太长,只保留前 MAX_LEN 个 Token。
#
ids = ids[:MAX_LEN]
# -------------------------
# Padding
# -------------------------
#
# 如果长度不足 MAX_LEN,
# 后面补 PAD。
#
while len(ids) < MAX_LEN:
ids.append(
vocab[PAD_TOKEN]
)
return ids
# ============================================================
# 7. 看看 encode() 的效果
# ============================================================
example = "公司业绩增长"
print(
"\nExample Text:",
example
)
print(
"Encoded:",
encode(example)
)
# ============================================================
# 8. 把训练数据转换成 Tensor
# ============================================================
X = torch.tensor(
[
encode(text)
for text, label
in samples
],
dtype=torch.long
)
y = torch.tensor(
[
label
for text, label
in samples
],
dtype=torch.long
)
print(
"\nX Shape:",
X.shape
)
print(
"y Shape:",
y.shape
)
# ============================================================
# 9. 定义神经网络
# ============================================================
classSentimentModel(nn.Module):
def__init__(
self,
vocab_size,
embed_dim=32
):
super().__init__()
# ====================================================
# Embedding
# ====================================================
#
# Token ID
#
# ↓
#
# 32维向量
#
# padding_idx=0:
#
# 告诉 PyTorch:
#
# Token 0 是 PAD。
#
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=embed_dim,
padding_idx=0
)
# ====================================================
# 分类器
# ====================================================
#
# 输入:
#
# 32维句子向量
#
# 输出:
#
# 2个 Logits
#
# [
# Negative Logit,
# Positive Logit
# ]
#
self.classifier = nn.Linear(
embed_dim,
2
)
defforward(self, x):
# ====================================================
# 输入 Shape
# ====================================================
#
# x:
#
# [Batch, Sequence]
#
# 例如:
#
# [16, 16]
#
# ====================================================
# Step 1:Embedding
# ====================================================
embeddings = self.embedding(
x
)
# Shape:
#
# [B, T, C]
#
# 例如:
#
# [16, 16, 32]
# ====================================================
# Step 2:创建 PAD Mask
# ====================================================
#
# x != 0
#
# 真正 Token:
#
# True
#
# PAD:
#
# False
#
mask = (
x != 0
).unsqueeze(-1)
# Shape:
#
# [B, T, 1]
# ====================================================
# Step 3:把 PAD Embedding 清零
# ====================================================
masked_embeddings = (
embeddings
*
mask
)
# ====================================================
# Step 4:Token 向量求和
# ====================================================
summed = masked_embeddings.sum(
dim=1
)
# Shape:
#
# [B, C]
# ====================================================
# Step 5:计算真正 Token 数量
# ====================================================
lengths = mask.sum(
dim=1
)
# 防止除 0
lengths = lengths.clamp(
min=1
)
# ====================================================
# Step 6:Mean Pooling
# ====================================================
#
# 把一句话所有 Token 的 Embedding 求平均。
#
# [B, T, C]
#
# ↓
#
# [B, C]
#
pooled = (
summed
/
lengths
)
# ====================================================
# Step 7:分类
# ====================================================
logits = self.classifier(
pooled
)
# Shape:
#
# [B, 2]
return logits
# ============================================================
# 10. 创建模型
# ============================================================
model = SentimentModel(
vocab_size=len(vocab),
embed_dim=32
)
print(
"\nModel:"
)
print(model)
# ============================================================
# 11. Loss
# ============================================================
#
# 两分类问题。
#
# 输入:
#
# [Negative Logit, Positive Logit]
#
# Target:
#
# 0 或 1
#
criterion = nn.CrossEntropyLoss()
# ============================================================
# 12. Optimizer
# ============================================================
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.01
)
# ============================================================
# 13. Training
# ============================================================
EPOCHS = 300
print(
"\n开始训练..."
)
for epoch in range(EPOCHS):
# --------------------------------------------------------
# 训练模式
# --------------------------------------------------------
model.train()
# --------------------------------------------------------
# 清空上一轮梯度
# --------------------------------------------------------
optimizer.zero_grad()
# --------------------------------------------------------
# Forward
# --------------------------------------------------------
logits = model(
X
)
# --------------------------------------------------------
# Loss
# --------------------------------------------------------
loss = criterion(
logits,
y
)
# --------------------------------------------------------
# Backward
# --------------------------------------------------------
loss.backward()
# --------------------------------------------------------
# 更新模型参数
# --------------------------------------------------------
optimizer.step()
# --------------------------------------------------------
# 输出训练信息
# --------------------------------------------------------
if epoch % 30 == 0:
# 当前预测类别
predictions = torch.argmax(
logits,
dim=1
)
# Training Accuracy
accuracy = (
predictions == y
).float().mean()
print(
f"Epoch={epoch:3d} "
f"Loss={loss.item():.6f} "
f"Accuracy={accuracy.item():.2%}"
)
# ============================================================
# 14. 定义预测函数
# ============================================================
defpredict(text):
"""
输入一段金融文本。
返回:
预测标签
Negative 概率
Positive 概率
"""
# --------------------------------------------------------
# 推理模式
# --------------------------------------------------------
model.eval()
# --------------------------------------------------------
# Text -> Token IDs
# --------------------------------------------------------
ids = encode(
text
)
# --------------------------------------------------------
# 增加 Batch 维度
# --------------------------------------------------------
#
# 原来:
#
# [T]
#
# 变成:
#
# [1, T]
#
input_tensor = torch.tensor(
[ids],
dtype=torch.long
)
# --------------------------------------------------------
# 推理不需要梯度
# --------------------------------------------------------
with torch.no_grad():
# Forward
logits = model(
input_tensor
)
# Logits -> Probability
probabilities = F.softmax(
logits,
dim=-1
)
# 最大概率类别
prediction = torch.argmax(
probabilities,
dim=-1
).item()
# --------------------------------------------------------
# 分类结果
# --------------------------------------------------------
if prediction == 1:
label = "正面"
else:
label = "负面"
negative_probability = (
probabilities[0][0].item()
)
positive_probability = (
probabilities[0][1].item()
)
return (
label,
negative_probability,
positive_probability
)
# ============================================================
# 15. 测试模型
# ============================================================
print(
"\n============================"
)
print(
"开始情感分类测试"
)
print(
"============================\n"
)
test_texts = [
"公司净利润大幅增长",
"公司出现严重亏损",
"营业收入持续增长",
"公司盈利明显下降",
"公司获得新的订单",
"主营业务明显恶化",
]
for text in test_texts:
(
label,
negative_prob,
positive_prob
) = predict(text)
print(
f"文本:{text}"
)
print(
f"预测:{label}"
)
print(
f"负面概率:{negative_prob:.2%}"
)
print(
f"正面概率:{positive_prob:.2%}"
)
print(
"-" * 40
)
到底怎么使用这个情感分类程序?
运行:
python day11_sentiment.py
程序实际上分成两个阶段。
第一阶段:训练
程序首先使用:
samples = [...]
这 16 条数据训练。
你会看到:
Epoch= 0 Loss=0.7... Accuracy=...
Epoch= 30 Loss=...
Epoch= 60 Loss=...
...
Epoch=270 Loss=...
这部分就是:
模型训练
第二阶段:真正进行情感分类
训练结束以后:
predict("公司净利润大幅增长")
这里才是:
使用模型进行情感分类
整个过程:
"公司净利润大幅增长"
↓
encode()
↓
Token IDs
↓
Embedding
↓
Mean Pooling
↓
Linear
↓
[-2.1, 3.8]
↓
Softmax
↓
[0.003, 0.997]
↓
argmax
↓
Positive
↓
"正面"
所以以后你想分析一句新文本,只需要:
result = predict(
"公司获得重大合同"
)
print(result)
但这里有一个非常重要的问题:UNK
比如你输入:
公司遭到监管处罚
其中:
遭 / 到 / 监 / 管 / 处 / 罚
有些字可能没有出现在训练集 Vocabulary 中。
那么:
vocab.get(
char,
vocab["<UNK>"]
)
会变成:
<UNK>
所以这个教学模型泛化能力非常有限。
以后真正金融 NLP:
不会自己用 16 条数据造字符词表
而会使用成熟:
Tokenizer
预训练 Transformer / LLM
但今天故意这么写,是为了让你看清楚所有过程。
附录 B:day11_parameter_update.py
这个程序:
不是情感分类模型。
它只解决一个问题:
loss.backward()和 optimizer.step()到底做了什么?
import torch
import torch.nn as nn
# ============================================================
# 固定随机种子
# ============================================================
torch.manual_seed(42)
# ============================================================
# 创建一个极简单模型
# ============================================================
#
# 输入:
#
# 2维
#
# 输出:
#
# 2分类
#
model = nn.Linear(
2,
2
)
# ============================================================
# Loss
# ============================================================
criterion = nn.CrossEntropyLoss()
# ============================================================
# Optimizer
# ============================================================
#
# 今天故意使用 SGD,
# 因为更新过程比较容易理解。
#
optimizer = torch.optim.SGD(
model.parameters(),
lr=0.1
)
# ============================================================
# 一个训练样本
# ============================================================
X = torch.tensor([
[1.0, 2.0]
])
# 正确类别:
#
# Class 1
y = torch.tensor([
1
])
# ============================================================
# 保存训练前参数
# ============================================================
weight_before = (
model.weight.data.clone()
)
bias_before = (
model.bias.data.clone()
)
print(
"训练前 Weight:"
)
print(
weight_before
)
print(
"\n训练前 Bias:"
)
print(
bias_before
)
# ============================================================
# Training Mode
# ============================================================
model.train()
# ============================================================
# 1. 清梯度
# ============================================================
optimizer.zero_grad()
# ============================================================
# 2. Forward
# ============================================================
logits = model(
X
)
print(
"\nLogits:"
)
print(
logits
)
# ============================================================
# 3. Loss
# ============================================================
loss = criterion(
logits,
y
)
print(
"\nLoss:"
)
print(
loss.item()
)
# ============================================================
# 4. Backward
# ============================================================
#
# 根据 Loss 进行反向传播。
#
# 注意:
#
# backward() 只负责计算梯度,
# 这时候模型参数本身还没有发生变化。
#
loss.backward()
# ============================================================
# 5. 查看 Gradient
# ============================================================
print(
"\nWeight Gradient:"
)
print(
model.weight.grad
)
print(
"\nBias Gradient:"
)
print(
model.bias.grad
)
# ============================================================
# 6. 特别验证:
#
# backward() 以后,
# 参数有没有改变?
# ============================================================
print(
"\nBackward之后、Step之前 Weight:"
)
print(
model.weight.data
)
# ============================================================
# 7. optimizer.step()
# ============================================================
#
# 这一步才是真正修改参数。
#
optimizer.step()
# ============================================================
# 8. 保存更新后的参数
# ============================================================
weight_after = (
model.weight.data.clone()
)
bias_after = (
model.bias.data.clone()
)
print(
"\n训练后 Weight:"
)
print(
weight_after
)
print(
"\n训练后 Bias:"
)
print(
bias_after
)
# ============================================================
# 9. 看看参数到底改变了多少
# ============================================================
print(
"\nWeight变化量:"
)
print(
weight_after
-
weight_before
)
print(
"\nBias变化量:"
)
print(
bias_after
-
bias_before
)
# ============================================================
# 10. 再 Forward 一次
# ============================================================
#
# 使用更新后的参数再次预测,
# 看看 Loss 是否发生变化。
#
with torch.no_grad():
new_logits = model(
X
)
new_loss = criterion(
new_logits,
y
)
print(
"\n更新前 Loss:"
)
print(
loss.item()
)
print(
"\n更新后 Loss:"
)
print(
new_loss.item()
)
这个实验到底要观察什么?
运行:
python day11_parameter_update.py
重点不是最终数字,而是顺序:
初始 Weight
↓
Forward
↓
Logits
↓
Loss
↓
loss.backward()
↓
出现 Gradient
↓
但 Weight 还没改变
↓
optimizer.step()
↓
Weight 真正改变
↓
再次 Forward
↓
Loss 通常下降
所以这个程序就是为了证明:
loss.backward()
≠
修改参数
而是:
loss.backward()
=
计算梯度
真正修改参数的是:
optimizer.step()
这两个以后一定不能混。
附录 C:day11_loss_curve.py
这个程序仍然是:
金融文本情感分类模型
但是它的学习重点和第一段不一样。
第一段重点:
怎么训练并使用一个情感分类模型?
第三段重点:
训练过程中 Loss 到底是怎么变化的?
它会画出训练曲线。
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
# ============================================================
# 1. 固定随机种子
# ============================================================
torch.manual_seed(42)
# ============================================================
# 2. 准备金融情感训练数据
# ============================================================
#
# Label:
#
# 0 = Negative
# 1 = Positive
#
samples = [
# Positive
("公司业绩大幅增长", 1),
("净利润同比增长", 1),
("公司获得重大订单", 1),
("营业收入创新高", 1),
("公司上调全年业绩预期", 1),
("主营业务持续增长", 1),
("公司盈利能力明显改善", 1),
("产品销售收入快速增长", 1),
# Negative
("公司业绩大幅下降", 0),
("净利润同比下滑", 0),
("公司出现重大亏损", 0),
("营业收入明显下降", 0),
("公司下调全年业绩预期", 0),
("主营业务持续恶化", 0),
("公司盈利能力明显下降", 0),
("产品销售收入大幅下滑", 0),
]
# ============================================================
# 3. 特殊 Token
# ============================================================
PAD_TOKEN = "<PAD>"
UNK_TOKEN = "<UNK>"
# ============================================================
# 4. Vocabulary
# ============================================================
vocab = {
PAD_TOKEN: 0,
UNK_TOKEN: 1
}
for text, label in samples:
for char in text:
if char notin vocab:
vocab[char] = len(vocab)
print(
"Vocabulary Size:",
len(vocab)
)
# ============================================================
# 5. 最大文本长度
# ============================================================
MAX_LEN = 16
# ============================================================
# 6. Encode
# ============================================================
defencode(text):
ids = []
# Text -> Token IDs
for char in text:
token_id = vocab.get(
char,
vocab[UNK_TOKEN]
)
ids.append(
token_id
)
# Truncation
ids = ids[:MAX_LEN]
# Padding
while len(ids) < MAX_LEN:
ids.append(
vocab[PAD_TOKEN]
)
return ids
# ============================================================
# 7. 构造训练 Tensor
# ============================================================
X = torch.tensor(
[
encode(text)
for text, label
in samples
],
dtype=torch.long
)
y = torch.tensor(
[
label
for text, label
in samples
],
dtype=torch.long
)
# ============================================================
# 8. 定义模型
# ============================================================
classSentimentModel(nn.Module):
def__init__(
self,
vocab_size,
embed_dim=32
):
super().__init__()
# Token Embedding
self.embedding = nn.Embedding(
vocab_size,
embed_dim,
padding_idx=0
)
# 二分类
self.classifier = nn.Linear(
embed_dim,
2
)
defforward(self, x):
# ----------------------------------------------------
# Embedding
# ----------------------------------------------------
embeddings = self.embedding(
x
)
# Shape:
#
# [B, T, C]
# ----------------------------------------------------
# Padding Mask
# ----------------------------------------------------
mask = (
x != 0
).unsqueeze(-1)
# ----------------------------------------------------
# 清除 PAD
# ----------------------------------------------------
embeddings = (
embeddings
*
mask
)
# ----------------------------------------------------
# Token Embedding 求和
# ----------------------------------------------------
summed = embeddings.sum(
dim=1
)
# ----------------------------------------------------
# 有效 Token 数量
# ----------------------------------------------------
lengths = mask.sum(
dim=1
).clamp(
min=1
)
# ----------------------------------------------------
# Mean Pooling
# ----------------------------------------------------
pooled = (
summed
/
lengths
)
# ----------------------------------------------------
# Classification
# ----------------------------------------------------
logits = self.classifier(
pooled
)
return logits
# ============================================================
# 9. 创建模型
# ============================================================
model = SentimentModel(
vocab_size=len(vocab),
embed_dim=32
)
# ============================================================
# 10. Loss
# ============================================================
criterion = nn.CrossEntropyLoss()
# ============================================================
# 11. Optimizer
# ============================================================
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.01
)
# ============================================================
# 12. Training
# ============================================================
EPOCHS = 300
# 用来记录每个 Epoch 的 Loss
loss_history = []
# 用来记录 Accuracy
accuracy_history = []
for epoch in range(EPOCHS):
# --------------------------------------------------------
# Train Mode
# --------------------------------------------------------
model.train()
# --------------------------------------------------------
# Clear Gradient
# --------------------------------------------------------
optimizer.zero_grad()
# --------------------------------------------------------
# Forward
# --------------------------------------------------------
logits = model(
X
)
# --------------------------------------------------------
# Loss
# --------------------------------------------------------
loss = criterion(
logits,
y
)
# --------------------------------------------------------
# Backward
# --------------------------------------------------------
loss.backward()
# --------------------------------------------------------
# Update Parameters
# --------------------------------------------------------
optimizer.step()
# ========================================================
# 保存 Loss
# ========================================================
loss_history.append(
loss.item()
)
# ========================================================
# 计算 Training Accuracy
# ========================================================
predictions = torch.argmax(
logits,
dim=1
)
accuracy = (
predictions == y
).float().mean()
accuracy_history.append(
accuracy.item()
)
# ========================================================
# 输出训练状态
# ========================================================
if epoch % 30 == 0:
print(
f"Epoch={epoch:3d} "
f"Loss={loss.item():.6f} "
f"Accuracy={accuracy.item():.2%}"
)
# ============================================================
# 13. 绘制 Loss Curve
# ============================================================
plt.figure(
figsize=(8, 5)
)
plt.plot(
loss_history
)
plt.xlabel(
"Epoch"
)
plt.ylabel(
"Loss"
)
plt.title(
"Training Loss"
)
plt.grid(
True,
alpha=0.3
)
plt.tight_layout()
plt.show()
# ============================================================
# 14. 绘制 Accuracy Curve
# ============================================================
plt.figure(
figsize=(8, 5)
)
plt.plot(
accuracy_history
)
plt.xlabel(
"Epoch"
)
plt.ylabel(
"Accuracy"
)
plt.title(
"Training Accuracy"
)
plt.grid(
True,
alpha=0.3
)
plt.tight_layout()
plt.show()
# ============================================================
# 15. 定义预测函数
# ============================================================
defpredict(text):
model.eval()
input_tensor = torch.tensor(
[
encode(text)
],
dtype=torch.long
)
with torch.no_grad():
logits = model(
input_tensor
)
probabilities = F.softmax(
logits,
dim=-1
)
prediction = torch.argmax(
probabilities,
dim=-1
).item()
label = (
"正面"
if prediction == 1
else"负面"
)
return (
label,
probabilities[0][0].item(),
probabilities[0][1].item()
)
# ============================================================
# 16. 测试
# ============================================================
print(
"\n训练完成,开始测试:\n"
)
test_texts = [
"公司净利润大幅增长",
"公司出现严重亏损",
"营业收入持续增长",
"公司盈利能力下降"
]
for text in test_texts:
(
label,
negative_prob,
positive_prob
) = predict(text)
print(
f"{text}"
)
print(
f"预测:{label}"
)
print(
f"Negative:{negative_prob:.2%}"
)
print(
f"Positive:{positive_prob:.2%}"
)
print(
"-" * 40
)
三个程序现在彻底区分开
到底哪段代码是进行情感分类的?到底怎么用?
现在可以非常明确地回答。
day11_sentiment.py | ||
day11_parameter_update.py | ||
day11_loss_curve.py |
所以你首先运行:
python day11_sentiment.py
这个才是 Day11 的主项目。
day11_sentiment.py到底由哪两部分组成?
这点一定区分。
第一部分:训练模型
samples
↓
Vocabulary
↓
encode()
↓
X / y
↓
SentimentModel
↓
CrossEntropyLoss
↓
Adam
↓
Training Loop
核心:
for epoch in range(EPOCHS):
model.train()
optimizer.zero_grad()
logits = model(X)
loss = criterion(
logits,
y
)
loss.backward()
optimizer.step()
这叫:
Training
第二部分:使用模型
真正使用模型的是:
predict(text)
例如:
label, negative, positive = predict(
"公司净利润大幅增长"
)
这叫:
Inference
内部:
"公司净利润大幅增长"
↓
encode()
↓
Token IDs
↓
model()
↓
Logits
↓
Softmax
↓
Probability
↓
argmax
↓
正面
训练和推理最关键的区别
训练:
model.train()
Forward
↓
Loss
↓
Backward
↓
Optimizer
↓
修改参数
推理:
model.eval()
torch.no_grad()
Forward
↓
Logits
↓
Softmax
↓
Prediction
推理阶段:
没有 Loss
没有 backward
没有 optimizer.step
因为:
模型已经训练完了,现在只使用参数,不再修改参数。
你可以自己输入一句话测试
最简单就是修改:
test_texts = [
"公司净利润大幅增长",
"公司出现严重亏损",
]
加入:
公司订单快速增长
或者:
公司全年出现巨额亏损
再运行整个程序。
也可以直接:
text = input(
"请输入一段金融文本:"
)
label, negative, positive = predict(
text
)
print(
"预测结果:",
label
)
print(
f"负面概率:{negative:.2%}"
)
print(
f"正面概率:{positive:.2%}"
)
这样运行以后:
请输入一段金融文本:
你输入:
公司净利润大幅增长
程序:
预测结果:正面
负面概率:1.23%
正面概率:98.77%
具体概率会因训练而不同。
这时候它就已经像一个非常简陋的:
金融情感分析小工具
了。
但是不要误解这个模型的能力
这是今天最需要强调的一点。
它只有:
16 条训练数据 字符级 Tokenizer Embedding Mean Pooling Linear
所以它很可能实际上学到的是:
增长 → 正面
改善 → 正面
订单 → 正面
下降 → 负面
亏损 → 负面
恶化 → 负面
而不是像真正 LLM 一样理解:
公司收入虽然增长 30%,但归母净利润下降 60%,主要原因是成本大幅上升。
这种复杂语义。
甚至 Mean Pooling 会大幅弱化词序:
利润增长
和:
利润没有增长
它可能处理得很差。
这恰恰也是为什么我们前面花那么多时间学习:
Position Attention Transformer
后面真正的模型会逐渐解决这些问题。
建议你今天按照这个顺序跑
不要三个一起看。
先:
① day11_sentiment.py
重点理解:
怎么训练一个模型、怎么用模型。
然后:
② day11_parameter_update.py
重点观察:
Weight Before
↓
Gradient
↓
optimizer.step()
↓
Weight After
最后:
③ day11_loss_curve.py
观察:
Epoch ↑
Loss ↓
Accuracy ↑
这样 Day11 的实践逻辑就特别清楚:
第一步
我会训练模型了
↓
第二步
我知道参数为什么会变
↓
第三步
我会观察模型训练过程了
这三段代码合起来,才是 Day11 真正想让你掌握的 PyTorch Training Loop。
预告:Day 12
下一课:
Day 12:PyTorch Dataset / DataLoader + Mini-Batch Training
我们会把今天的金融情感分类数据重新工程化,自己实现 Dataset,使用 DataLoader 按 Batch 训练,并真正看清 Dataset、Epoch、Batch、Step、Shuffle 之间到底是什么关系。