引言模型点好了菜厨房还得有食材上一章把词 → 向量这最后一层翻译官请到位了词嵌入就是一张查表查表就能把词变成向量。可查表之前得先有一张词表——字典都没有上哪儿查从本章开始系列进入实战准备篇的第二站给模型准备一顿真材实料的饭。第 14 章要训练的情感分类器吃的不是玩具数据而是真实的电影评论——IMDB 数据集。真实世界的文本远比第 11 章那 8 句极简语料野蛮有 HTML 标签残留、有大小写、有生僻词、有长有短。本章的任务就是把野生的影评加工成模型能下咽的整数序列并搭好一张词表供第 14 章的词嵌入层查。本章目标看清 IMDB 数据集长什么样整数序列 词表统计词频看懂长尾分布明白为什么 5000 个词就够用亲手构建 word2idx 词表留好 PAD 和 UNK 两个特殊位把一条真实影评从文字变成整数 ID 序列。一、认识 IMDB25000 条影评的仓库IMDB 是全球最大的电影资料库自带一个经典的二分类数据集每条样本是一条电影评论标签 0 表示差评、1 表示好评。规模是训练集 25000 条测试集 25000 条好评差评各占一半——正好 12500 比 12500模型想靠全都猜好评蒙混过关是行不通的。数据的存储方式很特别不是文本而是一串整数。每一条影评已经变成了一个整数列表每个整数对应词表里的一个词。这种预编号的格式省去了重复分词让注意力集中在怎么利用这些数字上。# 读取数据的部分已封装好这里只看结构# 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_train1).sum())# 12500好评数量print((y_train0).sum())# 12500差评数量正好一半看一眼第一条影评长什么样review0x_train[0]print(len(review0))# 138这条影评有 138 个词print(review0[:10])# [23022, 309, 6, 3, 1069, 209, 9, 2175, 30, 1]一串数字看不出任何感情色彩。数字背后是什么词需要一张词表来翻译。文字到数字的整条流水线用一张图概括二、词表数字和单词之间的翻译官数据自带一张词表 word_index单词 → 编号。把它的方向反过来就能把整数序列还原成英文word_indexload_word_index()# 词表单词 → 编号idx2word{v:kfork,vinword_index.items()}# 反过来编号 → 单词words0[idx2word[i]foriinreview0]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 highs satire还原出来的是一条动画剧集《Bromwell High》的短评标签是 1好评。词表一共多大print(len(word_index))# 88584近 9 万个不同的词88584 个词。如果全收进词表词嵌入层就要维护一张 88584 行的查表其中大部分词一年都出现不了一次。下一步数一数每个词到底被用了多少次看看能不能瘦身。三、词频统计谁在刷存在感把训练集里所有影评的所有词逐个计数fromcollectionsimportCounter counterCounter()forreviewinx_train:counter.update(review)# 每个词的编号计数 1print(len(counter))# 88584训练集共 88584 个不同词totalsum(counter.values())# 5942841所有词出现次数合计约 594 万次forwid,cntincounter.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∑i1Nfi∑i1VfiC_N \frac{\sum_{i1}^{N} f_i}{\sum_{i1}^{V} f_i}CN∑i1Vfi∑i1Nfi其中fif_ifi是第iii高频词的出现次数V88584V 88584V88584是全部不同词数。用代码算几个截断点defcoverage(n):returnsum(cfor_,cincounter.most_common(n))/totalprint(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未知位。词表外的生僻词统一归入未知口袋避免程序查表时直接报错。VOCAB_SIZE5000word2idx{PAD:0,UNK:1}# 两个特殊位占 0 和 1forwid,_incounter.most_common(VOCAB_SIZE):word2idx[idx2word[wid]]len(word2idx)# 高频词依次排 2, 3, 4, ...print(len(word2idx))# 50025000 个词 2 个特殊位print(word2idx[the])# 2最高频的词排第一print(word2idx[movie])# 18print(word2idx[great])# 85print(word2idx.get(bromwell))# None生僻词没资格进词表“the” 排 2、“movie” 排 18、“great” 排 85——编号越小词越常见这是一张按热度排座的座位表。生僻词比如剧名 bromwell查不到就归UNK。六、把影评变成整数序列encode 一行搞定有了词表编码函数就一行defencode(words):return[word2idx.get(w,1)forwinwords]# 查不到 → 1UNK把第一条影评完整走一遍words0[idx2word[i]foriinx_train[0]]# 先还原成单词ids0encode(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))# 19138 个词里有 19 个是生僻词 → UNK注意第一位就是 1bromwell 是生僻词直接进了UNK口袋。138 个词里 19 个 UNK占比约 14%——因为这是一条评小众剧集的影评专有名词多很正常。再拿一个普通句子验证sentencethe 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 倍再统计一个身材指标影评长度。这直接决定下一章的难度。lens[len(r)forrinx_train][len(r)forrinx_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。先看一眼长度分布的形状print(round(sum(1forlinlensifl500)/len(lens),4))# 0.919992% 的影评不超过 500 词92% 的影评在 500 词以内把 500 定为填充上限只损失不到 8% 的数据——这就是下一章 max_len 的来历。小结与预告本章把野生的影评加工成了模型能吃的整数序列数据25000 条训练 25000 条测试好评差评各半格式是整数序列 词表词频88584 个不同词、合计约 594 万次the 以 33.6 万次登顶br 是 HTML 标签残留长尾前 5000 高频词覆盖 90% 文本词表只留 5000 词词表PAD0、UNK1两个特殊位 5000 高频词“the movie is great” →[2, 18, 7, 85]长度最短 6 词、最长 2493 词、平均 234 词92% 影评不超过 500 词。全流程用一张图收个尾下一步长短不一的整数序列要装进统一大小的模具里——第 13 章处理变长序列填充Padding与掩码Masking让模型知道哪些位置是真实词、哪些是凑数的空气。下一篇二十六变长序列处理——填充与掩码