赞
踩
个人转行NLP领域,准备重新学习,先用fudannlp的NLP-Beginner项目来练练手。
我将按照FuDanNLP的NLP-Beginner项目中的任务一:基于机器学习的文本分类,实现基于logistic/softmax regression的文本分类。我们将使用《神经网络与深度学习》第2/3章作为参考资料,并使用Rotten Tomatoes数据集进行实验。实现要求使用NumPy,需要了解文本特征表示、分类器、数据集划分等知识点,并进行实验分析。
本任务要求我们实现基于机器学习的文本分类,主要包括以下内容:
使用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方法将文本表示为一个固定长度的向量,向量的每个元素代表一个词或词组在文本中出现的次数。具体步骤如下:
构建词汇表:遍历所有文本,将所有出现的词或词组(如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个单词组成的片段。常用的是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函数用于将线性模型的输出转换为概率分布。
- 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。