NOAI 2024 复赛题解|第3题:1000条数据,10分钟,能训出什么NLP模型?

NOAI 2024 复赛题解|第3题:1000条数据,10分钟,能训出什么NLP模型?

本文核心观点
NOAI 2024复赛第3题要求在10分钟CPU限时内完成新闻文本分类,用F1-Score评分。核心思路:Embedding+LSTM,处理好类别不均衡和时间管理是关键。

NOAI 2024 复赛题解|第3题:1000条数据,10分钟,能训出什么NLP模型?

NOAI复赛 自然语言处理

需要真题、资料,请拉到文末添加艾斯老师微信。

题目回顾

给一个新闻文本分类数据集(csv),包含 text(新闻内容)和 category(类别)两列。训练集 1,000 条,测试集 200 条。

任务:用 PyTorch 训练一个 NLP 模型,输入新闻语句,输出类别。建议使用 Embedding + LSTM。

约束条件

CPU 训练 + 测试总时间不超过 10 分钟

评分 = 所有类别 F1-Score 的平均值,超时记 0 分

这道题的核心矛盾

数据少(1,000 条)+ 时间紧(10 分钟 CPU)+ 评分用 F1 不是 Accuracy。

三个约束同时存在,决定了你不能用大模型,不能训太久,还不能只盯着整体准确率——如果某个类别的样本很少,你全猜成大类也能拿到不错的 accuracy,但那个小类的 F1 会是 0,拖垮平均分。

思路一:Embedding + LSTM

题目直接提示了方向,先照做。

整体流程:建词表 → 文本转 token 序列 → Embedding 层把 token 变成向量 → LSTM 处理序列 → 取最后一个时间步的输出 → 线性层分类

class TextClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True) self.fc = nn.Linear(hidden_dim, num_classes) def forward(self, x): x = self.embedding(x) _, (h_n, _) = self.lstm(x) out = self.fc(h_n.squeeze(0)) return out

关键参数:embed_dim 和 hidden_dim 设 64~128 就够了,数据量太小撑不起更大的维度。

建词表是第一个分水岭

英文新闻按空格分词,取出现频率最高的 N 个词,其余标记为 <UNK>。

from collections import Counter def build_vocab(texts, max_vocab=5000): counter = Counter() for text in texts: counter.update(text.lower().split()) vocab = {'<PAD>': 0, '<UNK>': 1} for word, _ in counter.most_common(max_vocab): vocab[word] = len(vocab) return vocab

容易忽略的细节:

全部转小写,否则 "The" 和 "the" 是两个词

去掉标点符号,或者至少把标点和单词分开

<PAD> 用于补齐序列长度,<UNK> 代替不在词表里的词

词表大小 3,000~8,000 一般够用

思路二:双向 LSTM

普通 LSTM 只从前往后读句子。双向 LSTM 同时从前往后、从后往前读,然后拼接两个方向的输出:

self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=True) self.fc = nn.Linear(hidden_dim * 2, num_classes) # 维度翻倍

时间成本大约是单向的 1.5~2 倍。1,000 条数据 + 10 分钟限制,通常不会超时。对短文本分类,双向通常比单向好——因为关键信息可能在开头也可能在结尾。

思路三:序列长度处理

新闻文本长度不一,但 batch 训练要求每条数据长度相同。

截断 + 填充:设定一个最大长度 max_len,超过的截断,不足的用 <PAD> 填充。

max_len 怎么定:看训练集里文本长度的分布。90% 的文本在 200 词以内,就设 200。设太大浪费时间(10 分钟限制),设太小丢失信息。

更好的做法:使用 pack_padded_sequence 让 LSTM 跳过 padding 部分,既省时间又避免 padding 干扰模型。但实现稍复杂,时间紧的话先用固定长度截断。

容易踩的坑

坑 1F1-Score 不是 Accuracy

如果某个类别只有 30 条样本,模型从来没预测对过这个类别,那这个类的 F1 = 0。即使其他类别全对,平均 F1 也会被大幅拖低。

对策:检查各类别样本数。如果严重不均衡,给少数类加权重:

class_counts = Counter(train_labels) weights = [1.0 / class_counts[c] for c in range(num_classes)] weights = torch.tensor(weights) / sum(weights) * num_classes criterion = nn.CrossEntropyLoss(weight=weights)

坑 2超时 = 0 分

10 分钟 CPU 时间包括训练和测试。LSTM 在 CPU 上比 GPU 慢得多。

保守策略:先跑一个 epoch 看耗时,据此推算最多能跑几个 epoch,留 1~2 分钟给测试和输出。不要用 GPU 调好参数后直接提交,CPU 耗时可能是 GPU 的 10~50 倍 ⚠️

坑 3词表只能在训练集上建

把测试集的词也纳入词表 = 使用了测试集信息。测试集本来就不可见,建词表必须只用训练集。

坑 4忘了处理未知词

测试集里必然有训练集没出现过的词。encode 函数没有 <UNK> 的兜底逻辑,遇到新词就会报错。

坑 5类别标签的编码

category 列可能是字符串("sports"、"politics"),需要映射成数字。预测完之后还要把数字映射回字符串写入提交文件。

区分度在哪

这道题的"技术上限"不高——Embedding + LSTM 就是答案,不需要 Transformer,不需要预训练模型。

区分度在工程细节:

有没有正确处理类别不均衡,决定了小类的 F1 是 0 还是 0.6

建词表的质量(大小写、标点、停用词),决定了模型拿到的输入干不干净。"哪些预处理有用、哪些是浪费时间"这种直觉,靠的是平时做 NLP 项目的积累

有没有在 CPU 上实际计时,决定了是拿到分还是超时 0 分

双向 LSTM 比单向好,但你得算清楚时间够不够

这道题的胜负手不是模型架构,是对时间和数据的管理。

微信二维码

扫码备注【NOAI】加交流群