一、概念
文本向量化是自然语言处理领域的重要环节,也是现在大语言模型开发重要基础。计算机程序无法理解文字信息(实际上非数值类型的信息都无法理解),因此我们需要将文字信息转换成计算机程序可理解的数值类型。通俗来说就是我们的算法模型是一系列函数和公式的组合推导,那么变量自然得是数值类型的才能参与计算。因此,这么将文本转换成数值类型的向量的过程就是文本向量化,而转换得到的这个向量又称文本表示。
二、分词
分词是文本向量化之前的必备步骤,为了让得到的文本表示不会损失太多的语义信息,我们需要有一个良好的分词结果。常见的中文分词库有jieba,英文分词库有nltk,这些分词库的原理都是内置一个词典,通过词匹配的方式将词语分隔出来。实际上,英文由于有空格分隔各个单词,分词的步骤较为简单(前提是划分单词而非词组),但由于中文是连续的字符,不依赖词典是无法得到有效的分词结果的(除非按“字”的粒度进行分隔,例如命名实体识别任务)。以中文分词为例:
import jieba
# 示例中文文本
text = "我喜欢学习自然语言处理。"
# 使用jieba进行分词
seg_list = jieba.cut(text)
# 将分词结果转换为列表并打印
print("分词结果:", "/ ".join(seg_list))
结果如下:
内置词典的分词器有个弊端就是无法及时更新词典,从而导致对于新词的划分力度不足,例如如今我们倾向于将“大语言模型”作为一个词组,但是直接使用jieba分词的结果可能并不如我们所想:
这时候,我们可以通过手动添加词典的方式进行词库的更新。
jieba.add_word('大语言模型')
三、文本向量化常见方式
1、词袋模型
词袋模型(Bag-of-Words,BOW)是一种自然语言处理中常用的文本表示方法,它的基本思想是将文本看作一个装着单词的“袋子”,不考虑单词的顺序和语法结构,只关注单词的出现频率。词袋模型的构建过程包括以下几个步骤:
- 分词:将文本分割成单词或词汇单元。
- 构建词汇表:从所有文档中提取唯一的单词,形成一个词汇表(这个过程也包括了去停用词等操作)。
- 特征向量化:对于每个文档,统计出现在词汇表中的每个单词的出现频率,并形成一个特征向量。向量的每个维度对应词汇表中的一个单词,其值表示该单词在文档中的出现次数,未出现则为0(所以是一个及其稀疏的向量)。
- 模型训练与预测:使用特征向量作为输入,训练机器学习模型,并进行预测。
(1)中文示例
import jieba
from sklearn.feature_extraction.text import CountVectorizer
# 示例中文文本数据
texts = [
'苹果和梨子是很常见的水果。',
'我喜欢吃芒果和百香果。',
'苹果今年推出了新的手机。',
'华为的新手机拍照性能非常好。'
]
# 使用jieba进行分词
texts_cut = [" ".join(jieba.cut(text)) for text in texts]
# 初始化CountVectorizer对象
vectorizer = CountVectorizer()
# 使用分词后的文本数据训练Vectorizer,即构建词汇表,并返回文档-词汇矩阵
X = vectorizer.fit_transform(texts_cut)
# 获取词汇表
vocabulary = vectorizer.get_feature_names_out()
# 打印词汇表
print("Vocabulary:", vocabulary)
# 打印文档-词汇矩阵
print("Document-term matrix:")
print(X.toarray())
下面可以看到打印出来的词汇表和向量矩阵,可以发现构建词表的过程中默认会去掉常见的停用词,例如“的”、“和”。同时,文本向量中可以清晰得出词汇表对应词语的出现频次。例如第一句话“苹果和梨子是很常见的水果”对应向量中,第四位“常见”出现了1次,第十位“梨子”和十一位“水果”各出现1次,倒数第二位“苹果”出现了1次。
(2)英文示例
由于英文有空格划分单词,因此单词粒度的词汇表构建过程中不需要进行分词即可直接使用词袋模型。
from sklearn.feature_extraction.text import CountVectorizer
# 示例文本数据
texts = [
"Apple and pear are very common fruits.",
"I like to eat mango and passion fruit.",
"Apple has launched a new phone this year.",
"HuaWei's new phone has excellent camera performance."
]
# 初始化CountVectorizer对象
vectorizer = CountVectorizer()
# 使用文本数据训练Vectorizer,即构建词汇表,并返回文档-词汇矩阵
X = vectorizer.fit_transform(texts)
# 获取词汇表
vocabulary = vectorizer.get_feature_names_out()
# 打印词汇表
print("Vocabulary:", vocabulary)
# 打印文档-词汇矩阵
print("Document-term matrix:")
print(X.toarray())
由此可知,词袋模型的缺陷非常明显:首先,它并没有任何语义信息;其次,词汇表的长度决定了词袋向量的长度,如果词汇表有上百万个词语,那么每一个样本的词袋向量也有上百万维,这是不可容忍的。
2、Word2Vec
Word2Vec由Google在2013年开发,用于生成词嵌入(词语的文本表示)。Word2Vec通过学习单词的上下文关系来生成每个单词的数值向量表示,这些向量能够捕捉单词的语义信息和句法信息,并将单词的语义相似性转化为向量空间中的几何关系。Word2Vec模型的核心思想是:在文本中经常一起出现的单词在向量空间中也会彼此接近。Word2Vec的训练过程如下:
- 初始化词嵌入:为每个单词分配一个随机向量(向量维度由我们根据任务需要定义)。
- 构建词汇表:创建一个包含所有单词的词汇表(实际应用中单词数量可选,如仅使用出现频率Top N的单词)。
- 训练模型:使用文本数据训练模型,通过优化算法(如梯度下降)调整词嵌入向量,以最小化预测误差。
- 生成词嵌入:训练完成后,每个单词都会有一个固定长度的向量表示。
此外,Word2Vec有两种学习模式:
(1)CBOW(Continuous Bag of Words):
CBOW模型的输入是单词的上下文,输出是中心词的预测,即使用周围的单词来预测中间的单词。CBOW模型的目标是最大化给定上下文词时目标词的条件概率,这等价于最小化交叉熵损失。对于CBOW模型,目标函数可以表示为:
其中,T是训练集中样本的总数,V是词汇表的大小,是目标词,是上下文词,是在给定上下文的情况下目标词的条件概率。
(2)Skip-Gram:
Skip-Gram模型的输入是中心词,输出是上下文单词的预测,即它使用一个单词来预测周围的单词。Skip-Gram模型的数学公式可以表示为最大化给定目标词的条件下上下文词的出现概率,即最大化下面的目标函数:
其中,T是训练集中的样本总数,c是上下文窗口大小,是目标词,是上下文词,条件概率可以用softmax函数计算:
这里,和分别是输入词和输出词的向量表示,W是词汇表的大小。
(1)中文示例
import jieba
from gensim.models import Word2Vec
# 示例中文文本数据
texts = [
'苹果和梨子是很常见的水果。',
'我喜欢吃芒果和百香果。',
'苹果今年推出了新的手机。',
'华为的新手机拍照性能非常好。'
]
# 使用jieba进行分词
texts_cut = [list(jieba.cut(text)) for text in texts]
# 训练Word2Vec模型
# 指定生成的词嵌入维度为30,学习的上下文窗口指定为5,用于构建词汇表的单词出现频次不低于min_count
model = Word2Vec(sentences=texts_cut, vector_size=30, window=5, min_count=1)
# 使用模型
vector = model.wv['梨子'] # 获取单词的向量表示
similar_words = model.wv.most_similar('梨子') # 找到最相似的词汇
# 打印结果
print("向量表示:", vector)
print("与'梨子'最相似的词汇:", similar_words[:3])
结果如下,我们指定了向量维度30,并查找模型学习到的词汇语义相似度知识(由于数据量较少模型学习效果欠佳):
(2)英文示例
import jieba
from gensim.models import Word2Vec
# 示例中文文本数据
texts = [
"Apple and pear are very common fruits.",
"I like to eat mango and passion fruit.",
"Apple has launched a new phone this year.",
"HuaWei's new phone has excellent camera performance."
]
# 将文本数据分割成单词列表
texts_split = [text.split() for text in texts]
# 训练Word2Vec模型
# 指定生成的词嵌入维度为30,学习的上下文窗口指定为3,用于构建词汇表的单词出现频次不低于min_count
model = Word2Vec(sentences=texts_split, vector_size=30, window=3, min_count=1, epochs=20)
# 使用模型
vector = model.wv['pear'] # 获取单词的向量表示
similar_words = model.wv.most_similar('pear') # 找到最相似的词汇
# 打印结果
print("向量表示:", vector)
print("与'pear'最相似的词汇:", similar_words[:3])
这里需要注意几个参数:
- sg:取值为0表示使用CBOW模式,为1表示使用skip-gram模式,默认为0。
- vector_size:单词向量的维度数,默认100。
- window:词嵌入训练过程中,句子中当前单词与预测单词的最大距离不能超过该值,默认5。
- min_count:词汇表构建过程中,忽略总频率低于该值的所有单词,默认5。
- epochs:迭代次数,默认5。
3、预训练模型
这里我们使用BERT(Bidirectional Encoder Representations from Transformers)作为示例。Bert是基于Transformer架构的预训练语言模型,它通过预训练的方式学习语言的表示和理解,在多种自然语言处理任务中取得了显著的效果。Bert通过两个主要的任务进行预训练:
- Masked Language Model(MLM):在MLM任务中,模型需要预测被遮盖(masked)的token,这个任务使得模型去学习token之间的关系以及上下文信息,类似于完形填空。MLM过程中,有15%的token会被随机掩盖,其中80%用[MASK]这个token来代替,10%用一个随机token来替换,10%保持原token不变。
- Next Sentence Prediction(NSP):在NSP任务中,模型需要判断两句话是否为连续的上下文句子,这个任务让模型能够理解句子之间的逻辑关系(虽然后面的研究发现这个任务对于Bert性能的提升并无太大作用,更多还是MLM给它带来的能力)。
(1)中文示例
from transformers import BertTokenizer, BertModel
import torch
# 这是一个基础版本的bert中文模型,如果需要更强大的、在特定任务中微调过的模型,可以去HuggingFace查找
# 预训练模型同样依赖自身的词典进行分词,而在bert-base-chinese这个模型中,对中文的分词粒度是字,也就是按照字的粒度划分中文文本,有一些模型是按照词粒度进行中文分词的,这个同样可以到HF上去查找
model_name = 'bert-base-chinese'
# 初始化分词器
tokenizer = BertTokenizer.from_pretrained(model_name)
# 加载模型
model = BertModel.from_pretrained(model_name)
texts = ['苹果和梨子是很常见的水果。', '我喜欢吃芒果和百香果。', '苹果今年推出了新的手机。', '华为的新手机拍照性能非常好。']
# 分词器生成Bert模型必要的输入格式
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# 禁用梯度,打开预测模式
with torch.no_grad():
# 预测输出
outputs = model(**inputs)
# 取最后一层的token平均向量
print('向量维度:')
print(sentence_embeddings.shape)
sentence_embeddings = outputs.pooler_output
print(sentence_embeddings)
(2)英文示例
from transformers import BertTokenizer, BertModel
import torch
# 同样,可以去HuggingFace找更强大的模型
model_name = 'bert-base-uncased'
# 初始化分词器
tokenizer = BertTokenizer.from_pretrained(model_name)
# 加载模型
model = BertModel.from_pretrained(model_name)
texts = ["Apple and pear are very common fruits.", "I like to eat mango and passion fruit.", "Apple has launched a new phone this year.", "HuaWei's new phone has excellent camera performance."]
# 分词器生成Bert必要的输出格式
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# 禁用梯度,打开预测模式
with torch.no_grad():
# 预测输出
outputs = model(**inputs)
# 取最后一层的token平均向量
sentence_embeddings = outputs.pooler_output
print('向量维度:')
print(sentence_embeddings.shape)
print('文本向量:')
print(sentence_embeddings)
可见,Bert生成的向量维度默认为768维。
四、总结
目前,对于文本向量化的任务,基本都会使用大型预训练模型了,这是由于它们最大程度保留了文本的原始语义,在诸多任务中表现良好。此外,传统的模型如Word2Vec对同一个单词的向量化表示是相同的,但我们知道同一个词在不同语境下应当有不同的含义。预训练模型结合了上下文进行文本表示,从而使得同一个词在不同语境下有不同的表征,这就是最明显的优势。例如,下面是第一句话中表示水果的Apple和第三句话中表示手机品牌的Apple的Bert文本表示截取,二者之间有着明显区别:
(1)水果“Apple”的向量:
(2)手机“Apple”的向量
关于预训练模型的内容笔者后续会逐一详解。