介绍
个人转行NLP领域,准备重新学习,先用fudannlp的NLP-Beginner项目来练练手。
我将按照FuDanNLP的NLP-Beginner项目中的任务一:基于机器学习的文本分类,实现基于logistic/softmax regression的文本分类。我们将使用《神经网络与深度学习》第2/3章作为参考资料,并使用Rotten Tomatoes数据集进行实验。实现要求使用NumPy,需要了解文本特征表示、分类器、数据集划分等知识点,并进行实验分析。
实现基于logistic/softmax regression的文本分类
任务要求和目标
本任务要求我们实现基于机器学习的文本分类,主要包括以下内容:
-
使用Bag-of-Word和N-gram作为文本特征表示方法
-
使用logistic/softmax regression作为分类器
-
实现损失函数、(随机)梯度下降、特征选择等功能
数据集
我们将使用Rotten Tomatoes数据集,该数据集包含了电影评论和相应的情感标签(正面或负面)。我们将把数据集划分为训练集、验证集和测试集,以便进行模型训练和评估。
实验目标
本实验旨在分析不同的特征表示、损失函数、学习率等因素对文本分类性能的影响,从而深入理解基于机器学习的文本分类方法。
实验步骤
-
数据预处理
-
从Rotten Tomatoes数据集中加载数据
-
将文本转换为特征表示(Bag-of-Word和N-gram)
-
-
模型设计
-
实现logistic/softmax regression模型
-
实现损失函数(交叉熵)
-
实现梯度下降算法(随机梯度下降)
-
-
实验设置
-
不同的特征表示:Bag-of-Word和N-gram
-
不同的损失函数:交叉熵
-
不同的学习率:0.01、0.001、0.0001
-
-
实验流程
-
使用训练集训练模型,并在验证集上调整参数
-
使用测试集评估模型性能
-
实验环境
-
Python 3.x
-
NumPy
实验步骤
数据预处理
首先,我们需要从Rotten Tomatoes数据集中加载数据,并将文本转换为特征表示。这里我们使用简单的Bag-of-Word和N-gram方法。
Bag-of-Word(词袋模型)和N-gram是常用的文本特征表示方法,用于将文本转换为机器学习模型可以处理的数值向量。
Bag-of-Word(词袋模型)
Bag-of-Word方法将文本表示为一个固定长度的向量,向量的每个元素代表一个词或词组在文本中出现的次数。具体步骤如下:
-
构建词汇表:遍历所有文本,将所有出现的词或词组(如unigram、bigram等)收集起来构建一个词汇表。
-
向量化:对于每个文本,统计词汇表中每个词或词组在文本中出现的次数,构成文本的向量表示。
例如,假设有以下两个文本:
-
文本1: "I love natural language processing."
-
文本2: "Natural language processing is fun and interesting."
构建词汇表:{I, love, natural, language, processing, is, fun, and, interesting}
则文本1的向量表示为:[1, 1, 1, 1, 1, 0, 0, 0, 0]
文本2的向量表示为:[0, 0, 1, 1, 1, 1, 1, 1, 1]
N-gram方法
N-gram方法是在词袋模型的基础上考虑了相邻单词之间的关系。N-gram指的是连续的N个单词组成的片段。常用的是unigram(单个词)、bigram(两个相邻词)、trigram(三个相邻词)等。
对于N-gram方法,构建词汇表时需要考虑N个相邻单词组成的词组,然后统计每个词组在文本中出现的次数。
以bigram为例,对于文本"I love natural language processing.",其bigram为["I love", "love natural", "natural language", "language processing"],则文本的向量表示为统计这些bigram在文本中出现的次数。
比较
-
Bag-of-Word更简单,只考虑词语出现的频率,忽略了单词之间的顺序,适用于文本分类等任务。
-
N-gram考虑了相邻单词之间的关系,能够更好地捕捉上下文信息,适用于语言建模、机器翻译等任务。
在这里我们采用N-gram的方法:
def build_ngram_vocab(data, n):
"""
构建n-gram词汇表。
Args:
data (List[Tuple[str, int]]): 原始数据集,每个元素为(文本, 标签)的元组。
n (int): n-gram中的n值,即每个n-gram包含的单词数。
Returns:
Dict[Tuple[str, ...], int]: 构建的n-gram词汇表,键为n-gram元组,值为对应的索引值。
"""
# 初始化n-gram词汇表
ngram_vocab = {}
# 初始化索引值
index = 0
# 遍历数据集
for text, label in data:
# 将文本转换为小写,并去除标点符号
text = text.lower().translate(str.maketrans('', '', string.punctuation))
# 按空格分割成单词列表
tokens = text.split()
# 遍历单词列表,构建n-gram
for i in range(len(tokens) - n + 1):
# 构建n-gram元组
ngram = tuple(tokens[i:i+n])
# 如果n-gram不在词汇表中
if ngram not in ngram_vocab:
# 将n-gram添加到词汇表中,并为其分配索引值
ngram_vocab[ngram] = index
# 索引值递增
index += 1
return ngram_vocab
def text_to_ngram_vector(text, ngram_vocab, n):
"""
将文本转换为n-gram向量。
Args:
text (str): 需要转换的文本。
ngram_vocab (dict): n-gram词汇表,键为n-gram元组,值为对应的索引。
n (int): n-gram的阶数。
Returns:
np.ndarray: 转换后的n-gram向量,形状为(len(ngram_vocab),)。
"""
# 初始化一个与ngram_vocab长度相同的零向量
vector = np.zeros(len(ngram_vocab))
# 将文本转换为小写并分割成单词列表
tokens = text.lower().split()
# 遍历tokens列表,从第一个单词开始到倒数第n个单词结束
for i in range(len(tokens) - n + 1):
# 取出当前位置开始的n个单词组成的ngram
ngram = tuple(tokens[i:i+n])
# 如果ngram在ngram_vocab中
if ngram in ngram_vocab:
# 将vector中对应ngram的索引位置的计数加1
vector[ngram_vocab[ngram]] += 1
return vector
数据预处理与训练集/验证集/测试集的划分
在进行机器学习任务之前,通常需要对数据进行预处理,并将数据集划分为训练集、验证集和测试集。本文将使用Python中的NumPy和pandas库来实现这些步骤。
数据预处理
数据样式:
首先,我们定义了一个函数process_data
,用于将指定文件中的数据处理为n-gram向量表示,并返回向量矩阵X、标签数组y和n-gram词汇表ngram_vocab。具体实现如下:
def process_data(file_path, n, sample_rate=0.1):
"""
将指定文件中的数据处理为n-gram向量表示,并返回向量矩阵X、标签数组y和n-gram词汇表ngram_vocab。
Args:
file_path (str): 数据文件的路径。
n (int): n-gram中n的值。
sample_rate (float, optional): 数据采样率,默认为0.1。这个主要是针对n_gram无法处理大数据集的问题。
Returns:
tuple: 包含三个元素的元组,分别为:
- X (np.ndarray): 形状为(len(data), len(ngram_vocab))的n-gram向量矩阵。
- y (np.ndarray): 形状为(len(data), )的标签数组。
- ngram_vocab (dict): n-gram词汇表,键为n-gram字符串,值为对应的索引。
"""
# 读取数据文件
df = pd.read_csv(file_path, sep='\t')
df = df.sample(frac=sample_rate, random_state=42)
# 解析成适当的格式
data = [(row['Phrase'], row['Sentiment']) for _, row in df.iterrows()]
# 构建n-gram词汇表
ngram_vocab = build_ngram_vocab(data, n)
# 初始化进度条
progress_bar = tqdm(total=len(data), desc="Processing data", unit=" samples")
# 将文本转换为n-gram向量
X = np.empty((len(data), len(ngram_vocab)), dtype=np.int32)
y = np.empty(len(data), dtype=np.int32)
for i, (text, label) in enumerate(data):
ngram_vector = text_to_ngram_vector(text, ngram_vocab, n)
X[i] = ngram_vector
y[i] = label
# 更新进度条
progress_bar.update(1)
# 关闭进度条
progress_bar.close()
# X = np.array(X)
# y = np.array(y)
return X, y, ngram_vocab
训练集/验证集/测试集的划分
接着,我们定义了一个函数train_test_split
,用于将数据集划分为训练集和测试集。具体实现如下:
def train_test_split(X, y, test_size=0.2, random_state=None):
"""
将数据集划分为训练集和测试集。
Args:
X (np.ndarray): 特征矩阵。
y (np.ndarray): 标签数组。
test_size (float): 测试集所占比例。
random_state (int): 随机种子。
Returns:
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: 训练集和测试集的特征和标签。
"""
if random_state is not None:
np.random.seed(random_state)
# 计算测试集的样本数
num_samples = X.shape[0]
num_test_samples = int(num_samples * test_size)
# 生成随机的索引
indices = np.random.permutation(num_samples)
# 划分训练集和测试集
test_indices = indices[:num_test_samples]
train_indices = indices[num_test_samples:]
X_train = X[train_indices]
X_test = X[test_indices]
y_train = y[train_indices]
y_test = y[test_indices]
return X_train, X_test, y_train, y_test
模型设计与实现
接下来,我们将设计并实现一个基于logistic/softmax回归的文本分类模型。该模型包括softmax函数、分类交叉熵损失函数、梯度计算函数以及一个逻辑回归类。
softmax函数
softmax函数用于将线性模型的输出转换为概率分布。
import numpy as np
def softmax(z):
"""
计算softmax函数值。
Args:
z (np.ndarray): 形状为 (N, D) 的二维数组,其中 N 为样本数量,D 为特征维度。
Returns:
np.ndarray: 形状为 (N, D) 的二维数组,表示每个样本在每个特征上的概率分布。
"""
exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))
return exp_z / np.sum(exp_z, axis=1, keepdims=True)
分类交叉熵损失函数
分类交叉熵损失函数用于衡量预测概率与真实标签之间的差异。
def categorical_cross_entropy(y_true, y_pred):
"""
计算分类交叉熵损失函数。
Args:
y_true (np.ndarray): 真实标签,形状为 (batch_size, num_classes)。
y_pred (np.ndarray): 预测概率,形状为 (batch_size, num_classes)。
Returns:
float: 交叉熵损失值。
"""
epsilon = 1e-15
y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
loss = -np.mean(np.sum(y_true * np.log(y_pred), axis=1))
return loss
梯度计算函数
梯度计算函数用于计算模型参数的梯度,以便进行梯度下降更新。
def compute_gradient(X, y_true, y_pred):
"""
计算线性回归的梯度。
Args:
X (np.ndarray): 形状为 (n_features, m) 的输入特征矩阵,其中 n_features 为特征数,m 为样本数。
y_true (np.ndarray): 形状为 (m,) 的真实标签数组。
y_pred (np.ndarray): 形状为 (m,) 的预测标签数组。
Returns:
Tuple[np.ndarray, np.ndarray]: 包含两个元素的元组,分别为:
- 梯度 (np.ndarray): 形状为 (n_features,) 的梯度数组。
- 平均误差 (np.ndarray): 形状为 (1,) 的平均误差数组。
"""
m = y_true.shape[0]
gradient = np.dot(X.T, (y_pred - y_true)) / m
return gradient, np.mean(y_pred - y_true, axis=0)
逻辑回归模型
最后,我们定义一个逻辑回归类,实现模型的训练和预测功能。
class LogisticRegression:
def __init__(self, learning_rate=0.05, num_iterations=100):
"""
初始化线性回归模型的参数。
Args:
learning_rate (float, optional): 学习率,用于梯度下降算法中更新权重和偏置项,默认为0.05。
num_iterations (int, optional): 梯度下降算法的迭代次数,默认为100。
"""
self.learning_rate = learning_rate
self.num_iterations = num_iterations
self.weights = None
self.bias = None
def fit(self, X, y):
"""
对模型进行训练,通过梯度下降更新权重和偏置。
Args:
X (ndarray): 形状为(m, n)的输入数据,m表示样本数量,n表示特征数量。
y (ndarray): 形状为(m,)的标签数据,表示每个样本的类别。
"""
m, n = X.shape
num_classes = len(np.unique(y))
self.weights = np.zeros((n, num_classes))
self.bias = np.zeros(num_classes)
y_onehot = np.eye(num_classes, dtype=int)[y.astype(int)]
for i in range(self.num_iterations):
z = np.dot(X, self.weights) + self.bias
y_pred = softmax(z)
loss = categorical_cross_entropy(y_onehot, y_pred)
dw, db = compute_gradient(X, y_onehot, y_pred)
self.weights -= self.learning_rate * dw
self.bias -= self.learning_rate * db
if i % 10 == 0:
print(f'Iteration {i}, Loss: {loss:.4f}')
def predict(self, X):
"""
对输入数据X进行预测,返回预测结果。
Args:
X (numpy.ndarray): 输入的待预测数据,形状为(n_samples, n_features),其中n_samples为样本数量,n_features为特征数量。
Returns:
numpy.ndarray: 预测结果,形状为(n_samples,),其中每个元素为对应样本的预测类别标签。
"""
z = np.dot(X, self.weights) + self.bias
y_pred = softmax(z)
return np.argmax(y_pred, axis=1)
最后的实验效果:
效果不佳的几个原因:
1. Logistic回归模型在面对复杂的文本分类任务时可能表现不足。
2. 学习率、迭代次数等超参数选择不当会影响模型性能。
3. 数据预处理(如去除停用词、标点符号,进行词形还原等)不足会影响模型性能。
4. 简单的Bag-of-Words或N-gram模型可能不足以捕捉文本的语义信息。
总结:
由于是第一个实验,要求又是整体用numpy来实现,采用的词袋模型又非常简单,效果不好我个人觉得非常正常,后面的几个实验会加大难度,使用更复杂的模型、词的embedding的方式,应该可以有更好的效果。
完整的代码:
https://github.com/EdvinCecilia/FuDanNLP_practice/tree/master/exp1
如果觉得有用,欢迎star