Frederick

Welcome to my Alter Ego's site!

May 24, 2025 - 9 minute read - Comments

NLP for beginners

分词

深度学习模型期望数字作为输入,而不是英语句子!所以我们需要做两件事:

  • Tokenization: Split each text up into words (or actually, as we’ll see, into tokens) 分词:将每段文本分割成单词(或者,实际上,我们会看到,分割成标记)
  • Numericalization: Convert each word (or token) into a number. 数字化:将每个单词(或标记)转换为一个数字。
from datasets import Dataset,DatasetDict

ds = Dataset.from_pandas(df)
#选择模型
model_nm = 'microsoft/deberta-v3-small'
#分词
from transformers import AutoModelForSequenceClassification,AutoTokenizer
tokz = AutoTokenizer.from_pretrained(model_nm)
def tok_func(x): return tokz(x["input"])
tok_ds = ds.map(tok_func, batched=True)

分词器中有一个名为 vocab 的列表,其中包含每个可能的标记字符串的唯一整数。我们可以这样查找它们,例如,为了找到单词"of"的标记:

tokz_vocab['_of']

我们需要准备我们的标签。Transformer 始终假设你的标签列名为 labels ,但我们的数据集中目前是 score 。因此,我们需要将其重命名:

tok_ds = tok_ds.rename_columns({'score':'labels'})

简单RNN

# using keras tokenizer here
token = text.Tokenizer(num_words=None)
max_len = 1500

token.fit_on_texts(list(xtrain) + list(xvalid))
xtrain_seq = token.texts_to_sequences(xtrain)
xvalid_seq = token.texts_to_sequences(xvalid)

#zero pad the sequences
xtrain_pad = sequence.pad_sequences(xtrain_seq, maxlen=max_len)
xvalid_pad = sequence.pad_sequences(xvalid_seq, maxlen=max_len)

word_index = token.word_index

初始化分词器,nums=None说明分词器会学习并保留文本中找到的所有单词

构建词汇表,为每个单词分配正式索引

将文本转换为序列

对序列进行进行零填充,因为神经网络要求所有的输入序列都必须又完全相同的长度

词汇索引,把分词器创建的词汇表保存到一个名为word_index的变量里,这是一个字典

%%time
with strategy.scope():
    # A simpleRNN without any pretrained embeddings and one dense layer
    model = Sequential()
    model.add(Embedding(len(word_index) + 1,
                     300,
                     input_length=max_len))
    model.add(SimpleRNN(100))
    model.add(Dense(1, activation='sigmoid'))
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    
model.summary()

初始化:一个不适用任何与训练词嵌入的SimpleRNN和一个全连接层

嵌入层:词汇表大小,词向量维度,输入序列长度

简单循环神经网络层,按时间步(单词的顺序)处理输入词向量序列

输出层/全连接层,输出单元数量,激活函数

模型编译,选择损失函数,优化器,评估指标

模型概览

# 1. 训练模型
model.fit(xtrain_pad, ytrain, epochs=5, batch_size=64*strategy.num_replicas_in_sync) 

# 2. 在验证集上生成预测
scores = model.predict(xvalid_pad)

# 3. 计算并打印 AUC 分数
auc_score = roc_auc(scores, yvalid)
print("Auc: %.2f%%" % (auc_score * 100)) # AUC通常是小数,乘以100变成百分比

# 4. 存储模型和分数以备比较
scores_model = [] # 初始化列表
scores_model.append({
    'Model': 'SimpleRNN',
    'AUC_Score': auc_score
})

LSTM

嵌入矩阵:这个矩阵的作用是利用预训练的词向量(例如 GloVe, Word2Vec 或 FastText)来初始化模型中的嵌入层 (Embedding Layer),而不是让模型从零开始学习词向量。

# create an embedding matrix for the words we have in the dataset
embedding_matrix = np.zeros((len(word_index) + 1, 300))
for word, i in tqdm(word_index.items()):
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        embedding_matrix[i] = embedding_vector

weights=[embedding_matrix]: 这是关键!它告诉 Embedding 层不要随机初始化权重,而是直接使用我们之前精心准备好的 embedding_matrix 作为初始权重。

trainable=False: 这个参数同样至关重要。它会“冻结”嵌入层,意味着在模型训练过程中,这些来自 GloVe 的词向量不会被更新。我们这样做是为了保留预训练词向量中已经包含的丰富语义信息,防止模型在训练我们这个(可能较小的)数据集时破坏掉这些有价值的信息。

%%time
with strategy.scope():
    
    # A simple LSTM with glove embeddings and one dense layer
    model = Sequential()
    model.add(Embedding(len(word_index) + 1,
                     300,
                     weights=[embedding_matrix],
                     input_length=max_len,
                     trainable=False))

    model.add(LSTM(100, dropout=0.3, recurrent_dropout=0.3))
    model.add(Dense(1, activation='sigmoid'))
    model.compile(loss='binary_crossentropy', optimizer='adam',metrics=['accuracy'])
    
model.summary()

GRU

解决梯度消失问题

%%time
with strategy.scope():
    # GRU with glove embeddings and two dense layers
     model = Sequential()
     model.add(Embedding(len(word_index) + 1,
                     300,
                     weights=[embedding_matrix],
                     input_length=max_len,
                     trainable=False))
     model.add(SpatialDropout1D(0.3))
     model.add(GRU(300))
     model.add(Dense(1, activation='sigmoid'))

     model.compile(loss='binary_crossentropy', optimizer='adam',metrics=['accuracy'])   
    
model.summary()

SpatialDropout1D(0.3): 和常规的 Dropout 随机丢弃单个神经元不同,SpatialDropout1D 会丢弃整个一维的特征图。在词嵌入的上下文中,这意味着它会随机地将整个词向量置为零(丢弃率为 30%),而不是词向量中的某个单独的数值。这种方法被证明在处理序列数据时能更有效地防止过拟合,因为它能促使模型不依赖于任何一个特定的词。

门控循环单元 (GRU) 是 LSTM 的一种变体,通常被认为性能与 LSTM 相当,但结构更简单,参数更少,因此训练速度可能更快。它同样通过“门”机制来解决长期依赖问题。300 是 GRU 层的单元数(即输出维度)。

预处理

清理语料库

def clean_text(text):
    '''Make text lowercase, remove text in square brackets,remove links,remove punctuation
    and remove words containing numbers.'''
    text = str(text).lower()
    text = re.sub('\[.*?\]', '', text)
    text = re.sub('https?://\S+|www\.\S+', '', text)
    text = re.sub('<.*?>+', '', text)
    text = re.sub('[%s]' % re.escape(string.punctuation), '', text)
    text = re.sub('\n', '', text)
    text = re.sub('\w*\d\w*', '', text)
    return text

去除停止词

stop_words = stopwords.words('english')
more_stopwords = ['u', 'im', 'c']
stop_words = stop_words + more_stopwords

def remove_stopwords(text):
    text = ' '.join(word for word in text.split(' ') if word not in stop_words)
    return text
    
df['message_clean'] = df['message_clean'].apply(remove_stopwords)
df.head()

词干提取

stemmer = nltk.SnowballStemmer("english")

def stemm_text(text):
    text = ' '.join(stemmer.stem(word) for word in text.split(' '))
    return text

合并

def preprocess_data(text):
    # Clean puntuation, urls, and so on
    text = clean_text(text)
    # Remove stopwords
    text = ' '.join(word for word in text.split(' ') if word not in stop_words)
    # Stemm all the words in the sentence
    text = ' '.join(stemmer.stem(word) for word in text.split(' '))
    
    return text

目标编码

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
le.fit(df['target'])
#学习标签
df['target_encoded'] = le.transform(df['target'])#转换标签
df.head()

将文本形式的分类标签转换成机器可以理解的数字形式

向量化

1.统计每个消息中单词出现的次数(词频)

2.加权技术,频繁的次元获得较低的权重(逆文档频率)

3.向量唯一化,忽略原始文本长度

每个向量将有与 SMS 语料库中唯一单词数量相同的维度。我们将首先使用 SciKit Learn 的 CountVectorizer。该模型将文本文档集合转换为标记计数的矩阵。

image-20250525191641342

from sklearn.feature_extraction.text import CountVectorizer

# instantiate the vectorizer
vect = CountVectorizer()
vect.fit(x_train)
# Use the trained to create a document-term matrix from train and test sets
x_train_dtm = vect.transform(x_train)
x_test_dtm = vect.transform(x_test)

调整CountVectorizer

一些参数

stop_words:一组不想用作特征的词

ngram_range:n个连续单词组成的字符串

min_df,max_df:单词必须具有的最小和最大文档频率才能用作特征

max_features:选择频率最高的单词/特征构成词汇表

在信息检索中,tf–idf,TF-IDF,或 TFIDF,全称 term frequency–inverse document frequency,是一个数值统计量,旨在反映一个词在文档集合或语料库中对文档的重要性。它通常用作信息检索、文本挖掘和用户建模中搜索的加权因子。tf–idf 值随着一个词在文档中出现的次数成正比增加,并受到语料库中包含该词的文档数量的影响,这有助于调整某些词在总体中更频繁出现的事实

tf–idf 是目前最流行的词频-逆文档频率方案之一。2015 年进行的一项调查表明,数字图书馆中 83%基于文本的推荐系统使用 tf–idf。

from sklearn.feature_extraction.text import TfidfTransformer

tfidf_transformer = TfidfTransformer()

tfidf_transformer.fit(x_train_dtm)
x_train_tfidf = tfidf_transformer.transform(x_train_dtm)

x_train_tfidf

词嵌入:Glove

全局矩阵分解 (Global Matrix Factorization):这类方法(如 LSA)能有效地利用语料库的全局统计信息,但它们在词汇类比任务(如 “king” - “man” + “woman” = “queen”)上表现不佳。

局部上下文窗口 (Local Context Window):这类方法(如 Word2Vec)专注于利用单词的局部上下文信息,它们在词汇类比任务上表现出色,但没有充分利用全局的统计信息。

GloVe 的核心思想是:词与词之间的共现概率的比率 (Ratio of Co-occurrence Probabilities) 能够蕴含丰富的、可量化的语义信息。

让我们通过一个非常经典的例子来理解这一点:

假设我们想考察 ice (冰) 和 steam (蒸汽) 这两个词。

  • 我们选择一个探针词 solid (固体)。icesolid 一起出现的概率会远大于 steamsolid 一起出现的概率。所以,概率比率 P(solid | ice) / P(solid | steam) 会是一个很大的数。
  • 我们选择另一个探针词 gas (气体)。icegas 一起出现的概率会远小于 steamgas 一起出现的概率。所以,概率比率 P(gas | ice) / P(gas | steam) 会是一个很小的数 (接近0)。
  • 如果探针词是 water (水) 或者 fashion (时尚),这两个比率可能都接近 1,因为它们与 icesteam 的关系差不多(或者都无关)。

GloVe 发现,这种概率的比率比概率本身更能揭示词与词之间的线性关系。因此,算法的设计目标就是让学习到的词向量能够很好地拟合这些共现概率的比率。

好的,我们来详细解释一下 GloVe 算法。

GloVe (发音与 “glove” 相同) 的全称是 Global Vectors for Word Representation,即“用于词语表示的全局向量”。它是由斯坦福大学的研究人员在 2014 年提出的一种强大的词嵌入 (Word Embedding) 算法。

简单来说,GloVe 的核心目标和 Word2Vec 一样,都是为词汇表中的每个单词学习一个稠密的向量(即“词向量”),这个向量能够捕捉单词的语义和语法信息。

  1. GloVe 的核心思想是什么?

GloVe 的独特之处在于它同时利用了两种主流词嵌入方法的优点:

全局矩阵分解 (Global Matrix Factorization):这类方法(如 LSA)能有效地利用语料库的全局统计信息,但它们在词汇类比任务(如 "king" - "man" + "woman" = "queen")上表现不佳。
局部上下文窗口 (Local Context Window):这类方法(如 Word2Vec)专注于利用单词的局部上下文信息,它们在词汇类比任务上表现出色,但没有充分利用全局的统计信息。

GloVe 的核心思想是:词与词之间的共现概率的比率 (Ratio of Co-occurrence Probabilities) 能够蕴含丰富的、可量化的语义信息。

让我们通过一个非常经典的例子来理解这一点:

假设我们想考察 ice (冰) 和 steam (蒸汽) 这两个词。

我们选择一个探针词 solid (固体)。ice 和 solid 一起出现的概率会远大于 steam 和 solid 一起出现的概率。所以,概率比率 P(solid | ice) / P(solid | steam) 会是一个很大的数。
我们选择另一个探针词 gas (气体)。ice 和 gas 一起出现的概率会远小于 steam 和 gas 一起出现的概率。所以,概率比率 P(gas | ice) / P(gas | steam) 会是一个很小的数 (接近0)。
如果探针词是 water (水) 或者 fashion (时尚),这两个比率可能都接近 1,因为它们与 ice 和 steam 的关系差不多(或者都无关)。

GloVe 发现,这种概率的比率比概率本身更能揭示词与词之间的线性关系。因此,算法的设计目标就是让学习到的词向量能够很好地拟合这些共现概率的比率。 2. GloVe 算法是如何工作的?

GloVe 的工作流程可以概括为以下三个步骤: 第一步:构建词-词共现矩阵 (Word-Word Co-occurrence Matrix)

这是算法的基础。算法首先会遍历整个语料库,统计在一个固定大小的“上下文窗口”内,任意两个词共同出现的次数。

共现矩阵 X:这是一个巨大的方阵,行和列都是词汇表中的所有单词。矩阵中的元素 Xij 表示单词 j 出现在单词 i 上下文中的次数。
权重衰减:通常,距离中心词越远的上下文词,其贡献权重会越小。例如,距离为 d 的词,其贡献权重可以是 1/d。

这个矩阵包含了整个语料库的全局统计信息。 第二步:定义词向量与共现概率的关系

GloVe 的核心公式旨在将词向量和共现概率联系起来。其最终推导出的目标函数形式如下:

J=i,j=1∑Vf(Xij)(wiTwj+bi+bj−log(Xij))2

让我们来拆解这个复杂的公式:

wi 和 w~j:分别是要学习的中心词 i 的词向量和上下文词 j 的词向量。
bi 和 b~j:是两个额外的偏置项,帮助模型更好地拟合。
log(Xij):是单词 i 和 j 共现次数的对数。
wiTw~j:是两个词向量的点积,这个点积的结果应该尽可能地接近它们共现次数的对数。
f(Xij):这是一个权重函数,非常重要。它的作用是:
    对那些非常高频的词对(如 "the" 和 "is")进行降权,因为它们虽然共现次数多,但提供的语义信息有限。
    对那些非常低频、几乎不出现的词对也进行降权,因为它们可能是噪音,统计意义不大。
    对于共现次数为 0 的词对,权重为 0,直接忽略,大大减少了计算量。

第三步:优化目标函数

算法的目标就是通过调整词向量 w 和偏置 b,来最小化上述目标函数 J 的值。这个过程通常使用随机梯度下降 (SGD) 等优化算法来完成。

训练结束后,对于每个单词,我们会得到两个词向量(一个作为中心词,一个作为上下文词)。通常的做法是将这两个向量相加或取平均,得到最终的词向量表示

算法八股文常见问题 强化学习

comments powered by Disqus