一、词嵌入的概念
自然语言处理的突破在2023年震撼世界,chatgpt3出来,之后chatgpt4、Gemini、Claude3等出来,问答越来越智能,非常厉害,其中有个基础性的概念,计算机要如何理解语言,基础工作就在于将词向量化,关键的概念便是词嵌入(word embeddings),之前我觉得这个词读的非常绕口,想为什么不直接叫词向量?其实形成词向量的方法有很多,包括词袋模型(Bag-of-Words model)也是一种方法,而词嵌入很厉害,可以让算法自动的理解一些类似的词,也就是自监督,通过词嵌入的概念就可以方便的构建NLP应用了!当然现在也有人在思考如何改进词嵌入的理解深度。
构建词嵌入的步骤有两步:
1、词嵌入第一步是给所有词构建one-hot向量,形成一个巨大的稀疏矩阵,每个词对应到某一个位置为1的向量。
2、通过深度学习来产生词嵌入,比如skip-grams学习词嵌入矩阵,词嵌入矩阵的格式有点像下面这样,当然每个维度表示什么含义属于自监督的事情,也就是说人是不知道的,一般设置成100~300维,词嵌入实际上属于“辅助产品”。以skip-grams算法为例,核心思想是构建一个监督学习问题:根据已有一个目标词,在多少个词内(自己定)找到另一个词,做成一个合理搭配。本身这个问题也很难解,但转成一个求概率问题之后,定义它的损失函数,经过反向传播算法它便是可以计算的了。
但是上面这个算法的计算复杂度是很高的,一旦数据量增大,计算会变得非常慢。因此就有分级(hierarchical)的softmax分类器和负采样(Negative Sampling)。分级的思路就是算法中常用的分而治之的思路,这种思路诞生了二分法、动态规划等算法,其实就是在softmax中不用先直接分到某个类,先确定区间,是前50%的分类还是后50%的分类。负采样的逻辑也简单,除了给正确词之外,也给不正确词,两者做搭配,从而减少运算量。
二、代码示例
正常项目都需要经过数据处理,比如去除html标签、特殊字符,先将数据放入到clean_text中:
# bs4 nltk gensim
import os
import re
import numpy as np
import pandas as pd
from bs4 import BeautifulSoup
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
from sklearn.linear_model import LogisticRegression
import nltk
from nltk.corpus import stopwords
df['clean_review'] = df.review.apply(clean_text)
定义文本清理函数,包括取出html文本中数据等,re.sub表示替换函数、将非a-z、A-Z的字符替换成空格,之后剔除停用词:
def clean_text(text):
text = BeautifulSoup(text, 'html.parser').get_text()
text = re.sub(r'[^a-zA-Z]', ' ', text)
words = text.lower().split()
words = [w for w in words if w not in eng_stopwords]
return ' '.join(words)
处理分词,tokenizer是分词器,最后得到单独句子的集合:
review_part = df['clean_review']
# 加载分词器
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
def split_sentences(review):
# 将长文本转换成句子的list
raw_sentences = tokenizer.tokenize(review.strip())
sentences = [clean_text(s) for s in raw_sentences if s]
return sentences
sentences = sum(review_part.apply(split_sentences), [])
然后进一步将将句子中的词提出放置到sentences_list中:
sentences_list = []
for line in sentences:
sentences_list.append(nltk.word_tokenize(line))
准备训练word2vec,相关参数说明如下:
sentences:可以是一个list
sg: 用于设置训练算法,默认为0,对应CBOW算法;sg=1则采用skip-gram算法。
size:是指特征向量的维度,默认为100。大的size需要更多的训练数据,但是效果会更好. 推荐值为几十到几百。
window:表示当前词与预测词在一个句子中的最大距离是多少
alpha: 是学习速率
seed:用于随机数发生器。与初始化词向量有关。
min_count: 可以对字典做截断. 词频少于min_count次数的单词会被丢弃掉, 默认值为5
max_vocab_size: 设置词向量构建期间的RAM限制。如果所有独立单词个数超过这个,则就消除掉其中最不频繁的一个。每一千万个单词需要大约1GB的RAM。设置成None则没有限制。
workers参数控制训练的并行数。
hs: 如果为1则会采用hierarchica·softmax技巧。如果设置为0(defau·t),则negative sampling会被使用。
negative: 如果>0,则会采用negativesamp·ing,用于设置多少个noise words
iter: 迭代次数,默认为5
设置参数:
# 设定词向量训练的参数
num_features = 300 # Word vector dimensionality
min_word_count = 40 # Minimum word count
num_workers = 4 # Number of threads to run in parallel
context = 10 # Context window size
model_name = '{}features_{}minwords_{}context.model'.format(num_features, min_word_count, context)
进行训练:
from gensim.models.word2vec import Word2Vec
model = Word2Vec(sentences_list, workers=num_workers, \
vector_size=num_features, min_count = min_word_count, \
window = context)
# If you don't plan to train the model any further, calling
# init_sims will make the model much more memory-efficient.
model.init_sims(replace=True)
# It can be helpful to create a meaningful model name and
# save the model for later use. You can load it later using Word2Vec.load()
model.save(os.path.join('..', 'models', model_name))
测试下模型情况:
model.most_similar("boy")
输出结果:
[('girl', 0.7018299698829651), ('astro', 0.6647905707359314), ('teenage', 0.6317306160926819), ('frat', 0.60948246717453), ('dad', 0.6011481285095215), ('yr', 0.6010577082633972), ('teenager', 0.5974895358085632), ('brat', 0.5941195487976074), ('joshua', 0.5832049250602722), ('father', 0.5825375914573669)]
很神奇是不是!数据量大了之后会有更好的效果,词嵌入可以直接从文本中读出关系来!