【Datawhale学习笔记】词向量表示
引言
机器学习和深度学习模型,无论结构多么复杂,其处理的输入都必须是数值形式——具体来说,是由数字组成的特征向量或矩阵。因此,在分词之后,必须将这些词元转换为模型可以"消化"的数字形式。这个过程称为词向量表示 (Word Representation) 。词嵌入 (Word Embedding) 通常特指通过神经网络学习得到的稠密向量表示,是词向量表示的一个重要子集。
为什么需要
模型无法直接处理文本,它需要数字化的特征。以最基础的 NLP 任务——文本分类为例,需要判断一段文本“国足爱吃海参”属于“负面”类别。模型无法直接理解这串汉字,它只能处理数字。
词向量的核心任务就是弥合自然语言(符号世界)与数学模型(向量空间)之间的鸿沟。需要一种系统性的方法,将分词后得到的[“国足”, “爱”, “吃”, “海参”]这个词元序列,整体转换成一个或一组有意义的数字(即向量),然后才能将其输入给分类模型进行训练和预测。一个好的词向量表示,不仅要能唯一地标识一个词,更理想的情况是,向量本身能够蕴含词语的语义信息。例如,我们希望“国王”和“女王”的向量在空间中的距离,会比“国王”和“香蕉”的向量距离更近。实现这一目标,也是 NLP 领域的重要发展方向之一。
离散表示
独热编码(One-Hot Encoding)
这是最直观、最基础的词元级别表示方法。它将每个词元都看作一个独立的类别,其思想与机器学习中的类别特征处理一致。
核心步骤
- 构建词典:首先,从整个语料库中收集所有出现过的唯一词语,构成一个词典。
- 分配索引:为词典中的每个词语分配一个从 0 开始的唯一整数索引。
- 创建向量:用一个长度等于词典大小的向量来表示每个词。向量中,该词对应索引的位置为 1,其余所有位置均为 0。
编码示例
假设词典是从句子"我先挣它一个亿"构建的,分词后为[“我”, “先”, “挣”, “它”, “一个”, “亿”]。
- 我 -> [1, 0, 0, 0, 0, 0]
- 先 -> [0, 1, 0, 0, 0, 0]
- 挣 -> [0, 0, 1, 0, 0, 0]
- 它 -> [0, 0, 0, 1, 0, 0]
- 一个 -> [0, 0, 0, 0, 1, 0]
- 亿 -> [0, 0, 0, 0, 0, 1]
优点与缺陷
优点
实现简单,能够清晰地将词语区分开。
缺点
- 维度灾难:如果词典中有数万个词,那么每个词的向量维度就高达数万,造成数据极其稀疏,浪费计算和存储资源。
- 语义鸿沟:任意两个不同词的独热向量都是正交的(它们的点积为 0)。
词袋模型(Bag-of-Words, BoW)
哑编码表示的是单个词,但实际通常需要表示整个句子或文档。词袋模型正是为此而生,它是表示文档级别特征最常用的方法之一。这种方法的理论基础可以追溯到向量空间模型 1的提出。
基本思想
词袋模型的基本思想是忽略文本中的词序和语法,将其仅仅视作一个装满词的"袋子",用袋子中每个词出现的统计量来表示整个文档。
他的实现
将文档中所有词的独热向量相加,得到一个最终的向量。这个向量的维度等于词典大小,每一维的值代表了对应词语在文档中的出现频次。实际实现时,通常直接统计每个词的出现次数,而不需要真正构造和相加 One-Hot 向量。
实现示例
假设词典与上文相同(基于"我先挣它一个亿"),有两个文档:
文档1: 我 先 挣 一个 亿
文档2: 我 挣 它 一个 亿
它们的词袋表示为:
vec(文档1) = vec(我) + vec(先) + vec(挣) + vec(一个) + vec(亿) = [1, 1, 1, 0, 1, 1]
vec(文档2) = vec(我) + vec(挣) + vec(它) + vec(一个) + vec(亿) = [1, 0, 1, 1, 1, 1]
这个结果向量的每一维,代表了对应词典中的词在该文档中出现的次数。通过计算两个向量的距离(如余弦相似度),可以发现这两个文档是比较相似的,因为它们共享了"我"、“挣”、“一个”、"亿"这几个词。
余弦相似度计算
**余弦相似度(Cosine Similarity)**通过计算两个向量夹角的余弦值来衡量它们的相似性。对于非负向量(如词袋模型产生的向量),其值范围在 [0, 1] 之间,值越接近1,表示两个向量越相似。
公式:
令 A = vec(文档1) = [1, 1, 1, 0, 1, 1],B = vec(文档2) = [1, 0, 1, 1, 1, 1]
- 计算点积 A · B:
- 计算模长 ||A|| 和 ||B||:
- 计算相似度:
结果为0.8,这是一个非常接近1的值,这从数学上证明了这两个文档是高度相似的。
统计方式
- 频数: 直接使用单词在文档中出现的次数。这是最简单直接的方式,但会受到文章长度影响,长文章的计数值会普遍偏高。
- 频率: 使用单词在文档中出现的次数除以文档的总词数,即词频(TF)。这在一定程度上缓解了文档长度不同带来的问题。
- 二进制: 只关心单词是否出现,出现即为1,不出现为0,不关心出现的次数。
优点与局限
优点
实现简单,并且在文本分类等任务上,因为这类任务的核心在于判断"文档里有什么词",而非"词与词之间如何关联",所以即使丢失了词序,也常常能取得不错的效果。
缺点
- 丢失词序:“我 爱 你” 和 “你 爱 我” 的词袋表示完全相同,无法区分语义差异。
- 未考虑词的重要性:像"的"、"是"这类在所有文档中都频繁出现的停用词,会获得很高的频次,但它们对区分文档主题几乎没有贡献,反而会形成干扰。
N-gram 模型
词袋模型最大的局限在于丢失了词序信息,而 N-gram(N元语法) 通过统计连续词组的方式弥补了这一短板。它不仅仅是对传统方法的改进,更是现代大语言模型(LLM)的鼻祖,因为它最早引入了预测下一个词的思想 2。
预测下一个词
与词袋模型只关心“有什么词”不同,N-gram(1-gram 是特例)关心的是“词的顺序”。其核心基于马尔可夫假设,也就是认为一个词出现的概率只取决于它前面 N-1 个词。虽然这是一种简化,但它极大地降低了建模的复杂度。
这正是生成式 AI 的雏形。根据依赖的前文长度不同
常见的 N-gram 模型
- Unigram (1-gram): 即词袋模型。假设每个词都是独立的,完全不看前面。
- Bigram (2-gram): 只看前 1 个词。例如看到“喜欢”,预测下一个词是“玩”的概率。
- Trigram (3-gram): 看前 2 个词。例如看到“喜欢 玩”,预测下一个词是“GTA6”的概率。
示例
对于句子 “我 喜欢 玩 GTA6”:
Bigram 特征:{"我 喜欢", "喜欢 玩", "玩 GTA6"}
Trigram 特征:{"我 喜欢 玩", "喜欢 玩 GTA6"}
通过这种方式,模型就能区分 “我 喜欢” 和 “喜欢 我” 了,因为它捕捉到了局部的序列信息。
维度灾难
虽然 N-gram 找回了词序,但它付出了巨大的代价,这也是它被神经网络取代的原因:
- 指数级爆炸:如果词典有 10,000 个词,Bigram 就有 种组合,Trigram 有 种… 这种维度灾难是传统计算机无法承受的。
- 数据稀疏:绝大多数词的组合(如“大象 驾驶 飞机”)在语料中永远不会出现,导致概率为 0。为了解决这个问题,传统 NLP 发展出了复杂的平滑技术,以及通过将词聚类来减少参数的基于类的 N-gram 模型 3。
正是为了从根本上解决这些问题,后续诞生了词向量和神经网络语言模型。
TF-IDF
为了提升文档在向量空间中的区分度,解决"常见词权重过高"导致文档混淆的问题,TF-IDF(Term Frequency-Inverse Document Frequency) 被提出。正如 Salton 等人所指出的,优秀的索引项应该能降低文档空间的密度,使语义不同的文档在空间中距离更远 1。TF-IDF 就是实现这一目标的经典加权技术。
计算公式
它由两部分组成:
- 词频(Term Frequency, TF):衡量一个词在当前文档中出现的频繁程度。常见的计算方式有:
- 原始频数:
- 归一化频率:
其中 表示词 在文档 中出现的次数。
- 逆文档频率(Inverse Document Frequency, IDF):衡量一个词的"稀有"程度或"信息量"。这一概念由 Karen Sparck Jones 在 1972 年提出 4。
其中 是语料库中的总文档数, 是包含词 的文档数。
为避免除零错误,实际应用中常使用平滑版本:
最终, 一个词的 TF-IDF 权重就是这两者的乘积:
一个文档的 TF-IDF 向量, 就是由该文档中每个词的 TF-IDF 值构成的向量。
实际应用
- 关键词提取:计算一篇文章中每个词的 TF-IDF 值,并按降序排列,排在最前面的通常就是这篇文章的关键词。jieba 的关键词提取也内置了这种算法。
- 文本相似度计算:计算两篇文档的 TF-IDF 向量,再通过余弦相似度等方法判断它们的相似性。
- 传统搜索引擎:在早期的搜索引擎中,TF-IDF 是衡量查询词与网页相关性的核心指标之一。
序号化表示
序号化过程
序号化,也称"整数编码",是将分词后的词元序列转换为深度学习模型能够处理的整数序列的核心步骤。其过程如下:
-
构建词典:与 One-Hot 类似,首先从训练语料中构建一个词典。但在深度学习中,这个词典通常是字级别的(如BERT),或是子词级别的(如GPT),而不是词级别的。
-
增加特殊词元:在词典中加入一些有特殊功能的 Token,至少包括:
[PAD] (Padding):用于填充。因为模型通常需要批处理(Batch Processing),一个批次内的所有句子必须长度相同。短句子会用[PAD]填充到与最长句子一致的长度。其对应的 ID 通常是0。
[UNK] (Unknown):用于表示所有在词典中未出现过的词。其对应的 ID 通常是1。
此外,还可能有 [CLS] (Classification), [SEP] (Separator) 等用于特定任务的特殊词元。 -
ID 映射:将文本序列中的每个词元(字/子词)直接映射为其在词典中的整数 ID。
预训练模型的词典:
在实践中,很少从零开始为自己的小数据集构建词典。更常见的做法是,直接使用像 BERT、GPT 这类预训练模型官方提供的 词典文件(vocab.txt) 。这些词典通常包含了数万个字、子词、符号等,是在海量通用语料上构建的,覆盖面非常广。
例如,Google 的中文 BERT 模型词典 vocab.txt 中就包含了约 21128 个词元,其中不仅有常用汉字,还包括了英文字母、数字、标点及 [PAD], [UNK] 等特殊符号。
序号化实例
假设有一个精简词典:
{'[PAD]': 0, '[UNK]': 1, '比': 2, '方': 3, '说': 4, '我': 5, '先': 6, '挣': 7, '它': 8, '一': 9, '个': 10, '亿': 11}
现在有三个句子需要处理:
我挣一个亿
比方说我
我先挣钱
第一步:分词 & 查找ID
句子1 (我挣一个亿): 我 (5), 挣 (7), 一 (9), 个 (10), 亿 (11) -> [5, 7, 9, 10, 11]
句子2 (比方说我): 比 (2), 方 (3), 说 (4), 我 (5) -> [2, 3, 4, 5]
句子3 (我先挣钱): 我 (5), 先 (6), 挣 (7), 钱 (不在词典中) -> [5, 6, 7, 1]
第二步:填充 (Padding)
为了将这三个长短不一的序列组成一个矩阵,需要以最长的序列(句子1,长度为5)为基准,对其他短序列用[PAD]的ID 0进行填充。
序列1 (长度5): [5, 7, 9, 10, 11]
序列2 (长度4→5): [2, 3, 4, 5, 0]
序列3 (长度4→5): [5, 6, 7, 1, 0]
最终,我们得到一个 3x5 的整数矩阵。这个矩阵,就是喂给深度学习模型的最终输入。
最终输入模型的张量(Tensor)
[[5, 7, 9, 10, 11],
[2, 3, 4, 5, 0],
[5, 6, 7, 1, 0]]
参考文献
https://github.com/datawhalechina/base-nlp/blob/main/docs/chapter2/04_word_vector.md
- 点赞
- 收藏
- 关注作者
评论(0)