python神经网络编程入门(二十五)——IMDB 数据集预处理与词汇表构建

引言:模型点好了菜,厨房还得有食材

上一章把"词 → 向量"这最后一层翻译官请到位了:词嵌入就是一张查表,查表就能把词变成向量。可查表之前,得先有一张词表------字典都没有,上哪儿查?

从本章开始,系列进入"实战准备篇"的第二站:给模型准备一顿真材实料的饭。第 14 章要训练的情感分类器,吃的不是玩具数据,而是真实的电影评论------IMDB 数据集。真实世界的文本,远比第 11 章那 8 句极简语料野蛮:有 HTML 标签残留、有大小写、有生僻词、有长有短。本章的任务,就是把"野生的"影评加工成模型能下咽的整数序列,并搭好一张词表,供第 14 章的词嵌入层查。

🎯 本章目标

  1. 看清 IMDB 数据集长什么样(整数序列 + 词表);
  2. 统计词频,看懂"长尾分布",明白为什么 5000 个词就够用;
  3. 亲手构建 word2idx 词表,留好 PAD 和 UNK 两个特殊位;
  4. 把一条真实影评从文字变成整数 ID 序列。

一、认识 IMDB:25000 条影评的"仓库"

IMDB 是全球最大的电影资料库,自带一个经典的二分类数据集:每条样本是一条电影评论,标签 0 表示差评、1 表示好评。规模是:训练集 25000 条,测试集 25000 条,好评差评各占一半------正好 12500 比 12500,模型想靠"全都猜好评"蒙混过关是行不通的。

数据的存储方式很特别:不是文本,而是一串整数。每一条影评已经变成了一个整数列表,每个整数对应词表里的一个词。这种"预编号"的格式省去了重复分词,让注意力集中在"怎么利用这些数字"上。

python 复制代码
# 读取数据的部分已封装好,这里只看结构:
# x 是影评(整数序列),y 是标签(0=差评,1=好评)
(x_train, y_train), (x_test, y_test) = load_imdb()

print(x_train.shape)        # (25000,):25000 条训练影评
print(x_test.shape)         # (25000,):25000 条测试影评
print(set(y_train))         # {0, 1}:0=差评,1=好评
print((y_train == 1).sum()) # 12500:好评数量
print((y_train == 0).sum()) # 12500:差评数量,正好一半

看一眼第一条影评长什么样:

python 复制代码
review0 = x_train[0]
print(len(review0))      # 138:这条影评有 138 个词
print(review0[:10])      # [23022, 309, 6, 3, 1069, 209, 9, 2175, 30, 1]

一串数字,看不出任何感情色彩。数字背后是什么词?需要一张词表来"翻译"。文字到数字的整条流水线,用一张图概括:

二、词表:数字和单词之间的"翻译官"

数据自带一张词表 word_index:单词 → 编号。把它的方向反过来,就能把整数序列还原成英文:

python 复制代码
word_index = load_word_index()                    # 词表:单词 → 编号
idx2word = {v: k for k, v in word_index.items()}  # 反过来:编号 → 单词

words0 = [idx2word[i] for i in review0]
print(' '.join(words0[:40]))
# bromwell high is a cartoon comedy it ran at the same time as some
# other programs about school life such as teachers my 35 years in the
# teaching profession lead me to believe that bromwell high's satire

还原出来的是一条动画剧集《Bromwell High》的短评,标签是 1(好评)。词表一共多大?

python 复制代码
print(len(word_index))   # 88584:近 9 万个不同的词

88584 个词。如果全收进词表,词嵌入层就要维护一张 88584 行的查表,其中大部分词一年都出现不了一次。下一步:数一数每个词到底被用了多少次,看看能不能瘦身。


三、词频统计:谁在刷存在感

把训练集里所有影评的所有词,逐个计数:

python 复制代码
from collections import Counter

counter = Counter()
for review in x_train:
    counter.update(review)          # 每个词的编号计数 +1

print(len(counter))                 # 88584:训练集共 88584 个不同词
total = sum(counter.values())       # 5942841:所有词出现次数合计(约 594 万次)

for wid, cnt in counter.most_common(10):
    print(idx2word[wid], cnt)
# the 336148
# and 164097
# a 163040
# of 145847
# to 135708
# is 107313
# br 101871
# in 93934
# it 79058
# i 77142

第一名是 the,出现 33.6 万次,毫无悬念。有意思的是第 7 名:br。这不是英文单词,它是 HTML 换行标签 <br /> 的残留------影评最初是网页,抓取时标签没清干净,<br /> 被当成一个"词"混了进来。真实数据往往带着这种杂质,数据清洗的重要性在这里看得一清二楚。

再看看榜单构成:除了 br,其余全是 the、and、a、of、to、is、in、it、i 这类冠词、介词、代词------功能词 刷了海量存在感,却几乎不带感情色彩。真正的情绪词(great、awful、boring)都排在几百名开外。这就是词频的典型形态------长尾分布:极少数词出现极多次,绝大多数词只出现几次甚至一次。

把排名和词频画在图上,横轴是词频排名、纵轴是出现次数,两条轴都用对数刻度,尾巴一眼可见:

尾巴到底有多长?训练集里,出现次数不超过 9 次的词有 67987 个,占了全部 88584 个词的 77%。这些"一辈子没露几次脸"的词全收进词表纯属浪费------词嵌入的维度都花在它们身上,却学不到任何语义。


四、截断词表:前 5000 个词就够用

那么词表留多大合适?算一笔账:按词频从高到低累加,前 NNN 个词覆盖了多少文本:

CN=∑i=1Nfi∑i=1VfiC_N = \frac{\sum_{i=1}^{N} f_i}{\sum_{i=1}^{V} f_i}CN=∑i=1Vfi∑i=1Nfi

其中 fif_ifi 是第 iii 高频词的出现次数,V=88584V = 88584V=88584 是全部不同词数。用代码算几个截断点:

python 复制代码
def coverage(n):
    return sum(c for _, c in counter.most_common(n)) / total

print(round(coverage(1000), 4))   # 0.7637:前 1000 词覆盖 76.4%
print(round(coverage(2000), 4))   # 0.8285:前 2000 词覆盖 82.9%
print(round(coverage(5000), 4))   # 0.9004:前 5000 词覆盖 90.0%
print(round(coverage(10000), 4))  # 0.9425:前 1 万词覆盖 94.3%

前 5000 个词就覆盖了 90% 的文本;再翻一倍到 1 万个词,只多覆盖 4 个百分点。这就像一座城市的人口:最大的几个城市装走了大部分人,剩下几千个小城镇分零头。收 5000 个词,词嵌入表只要 5000 行,既够用又不臃肿。

生活里也有现成的类比:字典收字上万,但常用字表只有 3500 个字,日常阅读已经覆盖 99%。词表截断就是这个道理。


五、构建词表 word2idx:留好两个"特殊座位"

现在动手构建自己的词表。除了 5000 个高频词,还要预留两个特殊位:

  • <PAD> = 0:填充位。影评长短不一,下一章要把它们对齐成等长,多出来的位置用 PAD 补齐;
  • <UNK> = 1:未知位。词表外的生僻词统一归入"未知"口袋,避免程序查表时直接报错。
python 复制代码
VOCAB_SIZE = 5000
word2idx = {'<PAD>': 0, '<UNK>': 1}          # 两个特殊位占 0 和 1

for wid, _ in counter.most_common(VOCAB_SIZE):
    word2idx[idx2word[wid]] = len(word2idx)  # 高频词依次排 2, 3, 4, ...

print(len(word2idx))             # 5002:5000 个词 + 2 个特殊位
print(word2idx['the'])           # 2:最高频的词排第一
print(word2idx['movie'])         # 18
print(word2idx['great'])         # 85
print(word2idx.get('bromwell'))  # None:生僻词没资格进词表

"the" 排 2、"movie" 排 18、"great" 排 85------编号越小,词越常见,这是一张"按热度排座"的座位表。生僻词(比如剧名 bromwell)查不到,就归 <UNK>


六、把影评变成整数序列:encode 一行搞定

有了词表,编码函数就一行:

python 复制代码
def encode(words):
    return [word2idx.get(w, 1) for w in words]   # 查不到 → 1(UNK)

把第一条影评完整走一遍:

python 复制代码
words0 = [idx2word[i] for i in x_train[0]]    # 先还原成单词
ids0 = encode(words0)

print(len(ids0))          # 138:词数不变,一一对应
print(ids0[:20])
# [1, 310, 7, 4, 1070, 210, 10, 2162, 31, 2, 170, 56, 15, 47, 83, 1, 42, 393, 111, 139]
print(ids0.count(1))      # 19:138 个词里有 19 个是生僻词 → UNK

注意第一位就是 1:bromwell 是生僻词,直接进了 <UNK> 口袋。138 个词里 19 个 UNK,占比约 14%------因为这是一条评小众剧集的影评,专有名词多,很正常。

再拿一个普通句子验证:

python 复制代码
sentence = 'the movie is great and i loved it'.split()
print(sentence)
# ['the', 'movie', 'is', 'great', 'and', 'i', 'loved', 'it']
print(encode(sentence))
# [2, 18, 7, 85, 3, 11, 445, 10]

"the movie is great" 变成 [2, 18, 7, 85]。第 14 章的词嵌入层拿到这串编号,查表取出对应行向量喂给 GRU------整条链路从此打通。


七、影评有多长?长短差出 400 倍

再统计一个"身材"指标:影评长度。这直接决定下一章的难度。

python 复制代码
lens = [len(r) for r in x_train] + [len(r) for r in x_test]

print(min(lens))                        # 6:最短的影评只有 6 个词
print(max(lens))                        # 2493:最长的有 2493 个词
print(round(sum(lens) / len(lens), 1))  # 233.8:平均 234 词

最短 6 词、最长 2493 词,差了 400 多倍。RNN 按时间步展开,一条 6 词的影评展开 6 步,一条 2493 词的展开 2493 步------长短不一的序列根本没法打包成一批训练。解决办法是下一章的主角:填充(Padding)与掩码(Masking)。先看一眼长度分布的形状:

python 复制代码
print(round(sum(1 for l in lens if l <= 500) / len(lens), 4))
# 0.9199:92% 的影评不超过 500 词

92% 的影评在 500 词以内,把 500 定为填充上限,只损失不到 8% 的数据------这就是下一章 max_len 的来历。


小结与预告

本章把"野生的影评"加工成了模型能吃的整数序列:

  • 数据:25000 条训练 + 25000 条测试,好评差评各半,格式是整数序列 + 词表;
  • 词频:88584 个不同词、合计约 594 万次,the 以 33.6 万次登顶,br 是 HTML 标签残留;
  • 长尾:前 5000 高频词覆盖 90% 文本,词表只留 5000 词;
  • 词表<PAD>=0<UNK>=1 两个特殊位 + 5000 高频词,"the movie is great" → [2, 18, 7, 85]
  • 长度:最短 6 词、最长 2493 词、平均 234 词,92% 影评不超过 500 词。

全流程用一张图收个尾:

下一步,长短不一的整数序列要装进统一大小的"模具"里------第 13 章处理变长序列:填充(Padding)与掩码(Masking),让模型知道哪些位置是真实词、哪些是凑数的空气。

下一篇(二十六):变长序列处理------填充与掩码

相关推荐
指尖的爷1 小时前
RKNN转化环境搭建(rknn_toolkit2新版本)
嵌入式硬件·深度学习·物联网·目标检测
HyperAI超神经3 小时前
在线教程|ProteinGym 第一名!VenusREM 用「检索增强」预测蛋白突变影响,加速蛋白质设计
人工智能·深度学习·生物信息学·大模型推理·生物医学
鬼手点金8 小时前
机器学习、深度学习、强化学习、神经网络、自注意力机制
人工智能·深度学习·神经网络·机器学习·skill·vibecoding·opencode
断眉的派大星8 小时前
CLIP原理详解:图像与文本的跨模态学习
人工智能·深度学习·机器学习
LaughingZhu9 小时前
Product Hunt 每日热榜 | 2026-08-05
人工智能·深度学习·神经网络·搜索引擎·百度
芸翳&Camellia9 小时前
2026年电赛H题钢珠识别——基于深度学习的视觉目标检测与实时速度估计系统技术分析
嵌入式硬件·深度学习·目标检测·电赛
_codemonster9 小时前
手语识别及翻译项目实战系列--认识CSL2018数据集
人工智能·深度学习
triplemeng10 小时前
AI正在改变“真实”的含义:从《黑客帝国》、鲍德里亚到生成式人工智能
人工智能·深度学习·神经网络·生成式·黑客帝国·鲍德里亚·后真相
bittersuite10 小时前
文本预处理,语言模型
人工智能·深度学习·机器学习