自然语言处理中的词嵌入(Part2)

LLM架构专栏持续更新中,词向量部分将会分四次更新完成,以下是已更新文章:
1. LLM大模型架构专栏|| 从NLP基础谈起
2.LLM大模型架构专栏|| 自然语言处理(NLP)之建模
欢迎关注公众号【柏企科技圈】【柏企阅文

此前我们探讨了词嵌入的基础概念,现在让我们深入到具体的技术细节中,从 N-Grams 开始,一步步揭开这些技术的神秘面纱。

3.1.4 N-Grams

N-grams 是在文本分析中可作为一个单元共同使用的单词序列。像 “Mary had a little lamb” 这句话中,“Mary had”、“had a”、“a little” 和 “little lamb” 等都是二元组(N = 2)。然而,许多 N-grams 在数据中可能出现频率过低,导致其表示稀疏且意义有限。

它主要有以下几种类型:

  • Unigram:即单个单词。
  • Bigram:由两个单词组成的对。
  • Trigram:三个单词构成的序列。

其重要性在于能够捕捉文本中的上下文和单词之间的依赖关系。

以下是一段 Python 示例代码,展示如何生成和分析 N-grams:

import nltk
from nltk.util import ngrams
from collections import Counter

text = "The quick brown fox jumps over the lazy dog"
tokens = nltk.word_tokenize(text)
unigrams = list(ngrams(tokens, 1))
print("Unigrams:")
print(unigrams)
bigrams = list(ngrams(tokens, 2))
print("\nBigrams:")
print(bigrams)
trigrams = list(ngrams(tokens, 3))
print("\nTrigrams:")
print(trigrams)

unigram_freq = Counter(unigrams)
bigram_freq = Counter(bigrams)
trigram_freq = Counter(trigrams)

print("\nUnigram Frequencies:")
print(unigram_freq)
print("\nBigram Frequencies:")
print(bigram_freq)
print("\nTrigram Frequencies:")
print(trigram_freq)

输出结果如下:

Unigrams:
[('The',), ('quick',), ('brown',), ('fox',), ('jumps',), ('over',), ('the',), ('lazy',), ('dog',)]
Bigrams:
[('The', 'quick'), ('quick', 'brown'), ('brown', 'fox'), ('fox', 'jumps'), ('jumps', 'over'), ('over', 'the'), ('the', 'lazy'), ('lazy', 'dog')]
Trigrams:
[('The', 'quick', 'brown'), ('quick', 'brown', 'fox'), ('brown', 'fox', 'jumps'), ('fox', 'jumps', 'over'), ('jumps', 'over', 'the'), ('over', 'the', 'lazy'), ('the', 'lazy', 'dog')]
Unigram Frequencies:
Counter({('The',): 1, ('quick',): 1, ('brown',): 1, ('fox',): 1, ('jumps',): 1, ('over',): 1, ('the',): 1, ('lazy',): 1, ('dog',): 1})
Bigram Frequencies:
Counter({('The', 'quick'): 1, ('quick', 'brown'): 1, ('brown', 'fox'): 1, ('fox', 'jumps'): 1, ('jumps', 'over'): 1, ('over', 'the'): 1, ('the', 'lazy'): 1, ('lazy', 'dog'): 1})
Trigram Frequencies:
Counter({('The', 'quick', 'brown'): 1, ('quick', 'brown', 'fox'): 1, ('brown', 'fox', 'jumps'): 1, ('fox', 'jumps', 'over'): 1, ('jumps', 'over', 'the'): 1, ('over', 'the', 'lazy'): 1, ('the', 'lazy', 'dog'): 1})

尽管 N-grams 有一定的作用,但也存在一些局限性:

  • 高维度和稀疏性:会产生大量的 N-grams 组合,导致向量空间维度极高且非常稀疏,增加了计算和存储的负担。
  • 缺乏语义理解:仅仅基于单词的连续组合,难以深入理解单词之间的语义关系。
  • 忽略上下文:不能很好地捕捉长距离的上下文信息,对文本的整体语义把握有限。
  • 可扩展性问题:随着文本数据量的增加,N-grams 的数量会迅速增长,使得处理变得困难。
  • 对噪声和罕见词敏感:容易受到数据中的噪声影响,并且在处理罕见词时效果不佳。
  • 难以捕捉多义词和同义词:不能有效区分具有相同或相似含义的单词,以及一个单词的不同含义。
  • 无法跨语言通用:在不同语言中,N-grams 的组合和语义关系差异较大,难以直接应用。
  • 难以捕捉长时依赖:对于文本中较长距离的单词依赖关系,N-grams 难以有效捕捉。

3.1.5 共现矩阵(Co-occurrence Matrices)

共现矩阵用于捕捉在给定的上下文窗口中单词共同出现的频率。它量化了单词之间的统计关系,为生成词嵌入提供了基础。

例如,假设有一个包含以下三个文档的语料库:

  • 文档 1:“The quick brown fox jumps over the lazy dog.”
  • 文档 2:“The brown dog barks loudly.”
  • 文档 3:“The lazy cat sleeps peacefully.”

我们想基于这些文档构建一个窗口大小为 1 的共现矩阵,这意味着我们考虑每个单词与其紧邻单词的共现情况,同时忽略标点并以不区分大小写的方式处理单词。

首先,根据语料库中的唯一单词构建词汇表:
Vocabulary: [the, quick, brown, fox, jumps, over, lazy, dog, barks, loudly, cat, sleeps, peacefully]

然后创建共现矩阵,其中行和列代表词汇表中的单词。矩阵中每个单元格 (i, j) 的值表示单词 i 在指定窗口大小内与单词 j 共现的次数。

the  quick  brown  fox  jumps  over  lazy  dog  barks  loudly  cat  sleeps peacefully
the         0      1      1    0      0     0     1    1      0       0     1       1         1
quick       1      0      0    1      0     0     0    0      0       0     0       0         0
brown       1      0      0    0      0     0     0    1      1       0     0       0         0
fox         0      1      0    0      1     0     0    0      0       0     0       0         0
jumps       0      0      0    1      0     1     0    0      0       0     0       0         0
over        0      0      0    0      1     0     1    0      0       0     0       0         0
lazy        1      0      0    0      0     1     0    1      0       0     1       0         0
dog         1      0      1    0      0     0     1    0      1       1     0       0         0
barks       0      0      1    0      0     0     0    1      0       1     0       0         0
loudly      0      0      0    0      0     0     0    1      1       0     0       0         0
cat         1      0      0    0      0     0     1    0      0       0     0       1         1
sleeps      1      0      0    0      0     0     0    0      0       0     1       0         1
peacefully  1      0      0    0      0     0     0    0      0       0     1       1         0

这个共现矩阵捕捉了在窗口大小为 1 的情况下每个单词与其他单词的共现频率。例如,“the” 行和 “lazy” 列的条目值为 1,表示在整个语料库中,“lazy” 与 “the” 在指定窗口内共现了一次。

在自然语言处理中,共现矩阵的一个主要应用是生成词嵌入。通过分析语料库中单词的共现模式,共现矩阵可以捕捉每个单词周围的上下文信息。像 Word2Vec 和 GloVe 等技术利用这些矩阵创建密集、低维的单词向量表示,其中向量之间的几何关系反映了单词之间的语义相似性,从而实现单词相似性的测量、类比检测和语义搜索等任务。

以下是一段 Python 示例代码,展示如何构建和处理共现矩阵:

import numpy as np
import pandas as pd
from collections import Counter
from sklearn.preprocessing import normalize

corpus = [
    "I love machine learning",
    "machine learning is great",
    "I love deep learning",
    "deep learning and machine learning are related"
]
corpus = [sentence.lower().split() for sentence in corpus]
vocab = set([word for sentence in corpus for word in sentence])
vocab = sorted(vocab)
vocab_size = len(vocab)
co_occurrence_matrix = np.zeros((vocab_size, vocab_size))
window_size = 2
word2idx = {word: i for i, word in enumerate(vocab)}

for sentence in corpus:
    for i, word in enumerate(sentence):
        word_idx = word2idx[word]
        start = max(0, i - window_size)
        end = min(len(sentence), i + window_size + 1)
        for j in range(start, end):
            if i!= j:
                context_word = sentence[j]
                context_idx = word2idx[context_word]
                co_occurrence_matrix[word_idx, context_idx] += 1

co_occurrence_df = pd.DataFrame(co_occurrence_matrix, index=vocab, columns=vocab)
co_occurrence_normalized = normalize(co_occurrence_matrix, norm='l1', axis=1)
co_occurrence_normalized_df = pd.DataFrame(co_occurrence_normalized, index=vocab, columns=vocab)

print("Co-occurrence Matrix:")
print(co_occurrence_df)
print("\nNormalized Co-occurrence Matrix:")
print(co_occurrence_normalized_df)

然而,共现矩阵也存在一些局限性:

  • 高维度导致效率和存储问题:随着词汇量的增加,矩阵的维度会迅速增长,导致计算效率低下和存储需求巨大。
  • 极度稀疏:由于大多数单词对很少共现,矩阵中大部分元素为 0,造成了数据的稀疏性。
  • 捕捉深层语义关系的能力有限:虽然能反映共现频率,但难以深入挖掘单词之间的深层次语义联系。
  • 上下文独立性:不能区分同一个单词在不同上下文中的不同含义。
  • 对大型语料库的可扩展性挑战:矩阵大小随词汇量呈二次增长,在处理大规模语料库时面临巨大挑战。
  • 对频繁词的偏向:频繁出现的单词在矩阵中的权重较大,对罕见词或短语的表示不可靠。
  • 表达能力有限:无法捕捉复杂的语言结构和语义关系。
  • 噪声数据:由于存在停用词和无意义的单词对,会引入噪声数据。

3.1.6 独热编码(One-Hot Encoding)

独热编码是自然语言处理中表示单词的一种基本方法。词汇表中的每个单词都被表示为一个独特的向量,除了对应单词索引位置的元素为 1 外,其他所有元素都设置为 0。

例如,假设有一个包含 10,000 个单词的词汇表,对于单词 “aardvark”,其向量表示为 [1, 0, 0, 0, …, 0],即在第一个位置为 1,后面跟着 9,999 个 0;对于单词 “ant”,其向量表示为 [0, 1, 0, 0, …, 0],即在第二个位置为 1,其余位置为 0,以此类推。这个过程就称为独热向量编码,它也常用于多类分类问题中表示标签。

假设我们正在构建一个自然语言处理项目中的翻译模型,需要将英文句子 “the cat is black” 翻译成另一种语言。首先要对每个单词进行独热编码。查找第一个单词 “the” 在 10,000 长的词汇表中的索引,假设其索引为 8676,那么 “the” 就可以用一个长度为 10,000 的向量表示,其中除了第 8676 个位置为 1 外,其他位置均为 0。对输入句子中的每个单词都进行这样的索引查找,并创建向量来表示每个输入单词。

以下是一段 Python 示例代码,展示如何实现独热编码:

def one_hot_encode(text):
    words = text.split()
    vocabulary = set(words)
    word_to_index = {word: i for i, word in enumerate(vocabulary)}
    one_hot_encoded = []
    for word in words:
        one_hot_vector = [0] * len(vocabulary)
        one_hot_vector[word_to_index[word]] = 1
        one_hot_encoded.append(one_hot_vector)
    return one_hot_encoded, word_to_index, vocabulary

example_text = "cat in the hat dog on the mat bird in the tree"
one_hot_encoded, word_to_index, vocabulary = one_hot_encode(example_text)
print("Vocabulary:", vocabulary)
print("Word to Index Mapping:", word_to_index)
print("One-Hot Encoded Matrix:")
for word, encoding in zip(example_text.split(), one_hot_encoded):
    print(f"{word}: {encoding}")

输出结果如下:

Vocabulary: {'mat', 'the', 'bird', 'hat', 'on', 'in', 'cat', 'tree', 'dog'}
Word to Index Mapping: {'mat': 0, 'the': 1, 'bird': 2, 'hat': 3, 'on': 4, 'in': 5, 'cat': 6, 'tree': 7, 'dog': 8}
One-Hot Encoded Matrix:
cat: [0, 0, 0, 0, 0, 0, 1, 0, 0]
in: [0, 0, 0, 0, 0, 1, 0, 0, 0]
the: [0, 1, 0, 0, 0, 0, 0, 0, 0]
hat: [0, 0, 0, 1, 0, 0, 0, 0, 0]
dog: [0, 0, 0, 0, 0, 0, 0, 0, 1]
on: [0, 0, 0, 0, 1, 0, 0, 0, 0]
the: [0, 1, 0, 0, 0, 0, 0, 0, 0]
mat: [1, 0, 0, 0, 0, 0, 0, 0, 0]
bird: [0, 0, 1, 0, 0, 0, 0, 0, 0]
in: [0, 0, 0, 0, 0, 1, 0, 0, 0]
the: [0, 1, 0, 0, 0, 0, 0, 0, 0]
tree: [0, 0, 0, 0, 0, 0, 0, 1, 0]

虽然独热编码能够将单词表示为实数值向量,但也存在一些问题:

  • 缺乏语义相似性:无法捕捉单词之间的语义关系。例如,“cat” 和 “tiger” 被表示为完全不同的向量,没有体现出它们的相似性,这对于基于类比的向量操作等任务来说是个很大的问题。
  • 高维度:独热向量的维度与词汇表大小呈线性增长。随着词汇量的增加,特征向量会变得非常大,加剧了维度灾难。这不仅增加了需要估计的参数数量,还需要指数级更多的数据来训练一个泛化良好的模型。
  • 计算效率低下:独热编码向量是稀疏且高维的,大部分元素为 0。许多机器学习模型,尤其是神经网络,在处理这种稀疏数据时会遇到困难。庞大的特征空间也会导致内存和存储方面的挑战,特别是当模型不能有效地处理稀疏矩阵时。

后续我们将继续探讨如何解决这些问题,以及其他更先进的词嵌入技术,敬请期待!

LLM架构专栏持续更新中,词向量部分将会分四次更新完成,以下是已更新文章:
1. LLM大模型架构专栏|| 从NLP基础谈起
2.LLM大模型架构专栏|| 自然语言处理(NLP)之建模
欢迎关注公众号【柏企科技圈】【柏企阅文

希望这篇文章能帮助到大家。