当前位置:   article > 正文

从零开始NLP(一):基于机器学习的文本分类_nlp 基于机器学习

nlp 基于机器学习

介绍

个人转行NLP领域,准备重新学习,先用fudannlp的NLP-Beginner项目来练练手。

FuDanNLP地址

我将按照FuDanNLP的NLP-Beginner项目中的任务一:基于机器学习的文本分类,实现基于logistic/softmax regression的文本分类。我们将使用《神经网络与深度学习》第2/3章作为参考资料,并使用Rotten Tomatoes数据集进行实验。实现要求使用NumPy,需要了解文本特征表示、分类器、数据集划分等知识点,并进行实验分析。

实现基于logistic/softmax regression的文本分类

任务要求和目标

本任务要求我们实现基于机器学习的文本分类,主要包括以下内容:

  • 使用Bag-of-Word和N-gram作为文本特征表示方法

  • 使用logistic/softmax regression作为分类器

  • 实现损失函数、(随机)梯度下降、特征选择等功能

数据集

我们将使用Rotten Tomatoes数据集,该数据集包含了电影评论和相应的情感标签(正面或负面)。我们将把数据集划分为训练集、验证集和测试集,以便进行模型训练和评估。

实验目标

本实验旨在分析不同的特征表示、损失函数、学习率等因素对文本分类性能的影响,从而深入理解基于机器学习的文本分类方法。

实验步骤
  1. 数据预处理

    • 从Rotten Tomatoes数据集中加载数据

    • 将文本转换为特征表示(Bag-of-Word和N-gram)

  2. 模型设计

    • 实现logistic/softmax regression模型

    • 实现损失函数(交叉熵)

    • 实现梯度下降算法(随机梯度下降)

  3. 实验设置

    • 不同的特征表示:Bag-of-Word和N-gram

    • 不同的损失函数:交叉熵

    • 不同的学习率:0.01、0.001、0.0001

  4. 实验流程

    • 使用训练集训练模型,并在验证集上调整参数

    • 使用测试集评估模型性能

实验环境
  • Python 3.x

  • NumPy

实验步骤

数据预处理

首先,我们需要从Rotten Tomatoes数据集中加载数据,并将文本转换为特征表示。这里我们使用简单的Bag-of-Word和N-gram方法。

Bag-of-Word(词袋模型)和N-gram是常用的文本特征表示方法,用于将文本转换为机器学习模型可以处理的数值向量。

Bag-of-Word(词袋模型)

Bag-of-Word方法将文本表示为一个固定长度的向量,向量的每个元素代表一个词或词组在文本中出现的次数。具体步骤如下:

  1. 构建词汇表:遍历所有文本,将所有出现的词或词组(如unigram、bigram等)收集起来构建一个词汇表。

  2. 向量化:对于每个文本,统计词汇表中每个词或词组在文本中出现的次数,构成文本的向量表示。

例如,假设有以下两个文本:

  • 文本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的方法:

  1. def build_ngram_vocab(data, n):
  2. """
  3. 构建n-gram词汇表。
  4. Args:
  5. data (List[Tuple[str, int]]): 原始数据集,每个元素为(文本, 标签)的元组。
  6. n (int): n-gram中的n值,即每个n-gram包含的单词数。
  7. Returns:
  8. Dict[Tuple[str, ...], int]: 构建的n-gram词汇表,键为n-gram元组,值为对应的索引值。
  9. """
  10. # 初始化n-gram词汇表
  11. ngram_vocab = {}
  12. # 初始化索引值
  13. index = 0
  14. # 遍历数据集
  15. for text, label in data:
  16. # 将文本转换为小写,并去除标点符号
  17. text = text.lower().translate(str.maketrans('', '', string.punctuation))
  18. # 按空格分割成单词列表
  19. tokens = text.split()
  20. # 遍历单词列表,构建n-gram
  21. for i in range(len(tokens) - n + 1):
  22. # 构建n-gram元组
  23. ngram = tuple(tokens[i:i+n])
  24. # 如果n-gram不在词汇表中
  25. if ngram not in ngram_vocab:
  26. # 将n-gram添加到词汇表中,并为其分配索引值
  27. ngram_vocab[ngram] = index
  28. # 索引值递增
  29. index += 1
  30. return ngram_vocab
  31. def text_to_ngram_vector(text, ngram_vocab, n):
  32. """
  33. 将文本转换为n-gram向量。
  34. Args:
  35. text (str): 需要转换的文本。
  36. ngram_vocab (dict): n-gram词汇表,键为n-gram元组,值为对应的索引。
  37. n (int): n-gram的阶数。
  38. Returns:
  39. np.ndarray: 转换后的n-gram向量,形状为(len(ngram_vocab),)。
  40. """
  41. # 初始化一个与ngram_vocab长度相同的零向量
  42. vector = np.zeros(len(ngram_vocab))
  43. # 将文本转换为小写并分割成单词列表
  44. tokens = text.lower().split()
  45. # 遍历tokens列表,从第一个单词开始到倒数第n个单词结束
  46. for i in range(len(tokens) - n + 1):
  47. # 取出当前位置开始的n个单词组成的ngram
  48. ngram = tuple(tokens[i:i+n])
  49. # 如果ngram在ngram_vocab中
  50. if ngram in ngram_vocab:
  51. # 将vector中对应ngram的索引位置的计数加1
  52. vector[ngram_vocab[ngram]] += 1
  53. return vector

数据预处理与训练集/验证集/测试集的划分

在进行机器学习任务之前,通常需要对数据进行预处理,并将数据集划分为训练集、验证集和测试集。本文将使用Python中的NumPy和pandas库来实现这些步骤。

数据预处理

数据样式:

首先,我们定义了一个函数process_data,用于将指定文件中的数据处理为n-gram向量表示,并返回向量矩阵X、标签数组y和n-gram词汇表ngram_vocab。具体实现如下:

  1. def process_data(file_path, n, sample_rate=0.1):
  2. """
  3. 将指定文件中的数据处理为n-gram向量表示,并返回向量矩阵X、标签数组y和n-gram词汇表ngram_vocab。
  4. Args:
  5. file_path (str): 数据文件的路径。
  6. n (int): n-gram中n的值。
  7. sample_rate (float, optional): 数据采样率,默认为0.1。这个主要是针对n_gram无法处理大数据集的问题。
  8. Returns:
  9. tuple: 包含三个元素的元组,分别为:
  10. - X (np.ndarray): 形状为(len(data), len(ngram_vocab))的n-gram向量矩阵。
  11. - y (np.ndarray): 形状为(len(data), )的标签数组。
  12. - ngram_vocab (dict): n-gram词汇表,键为n-gram字符串,值为对应的索引。
  13. """
  14. # 读取数据文件
  15. df = pd.read_csv(file_path, sep='\t')
  16. df = df.sample(frac=sample_rate, random_state=42)
  17. # 解析成适当的格式
  18. data = [(row['Phrase'], row['Sentiment']) for _, row in df.iterrows()]
  19. # 构建n-gram词汇表
  20. ngram_vocab = build_ngram_vocab(data, n)
  21. # 初始化进度条
  22. progress_bar = tqdm(total=len(data), desc="Processing data", unit=" samples")
  23. # 将文本转换为n-gram向量
  24. X = np.empty((len(data), len(ngram_vocab)), dtype=np.int32)
  25. y = np.empty(len(data), dtype=np.int32)
  26. for i, (text, label) in enumerate(data):
  27. ngram_vector = text_to_ngram_vector(text, ngram_vocab, n)
  28. X[i] = ngram_vector
  29. y[i] = label
  30. # 更新进度条
  31. progress_bar.update(1)
  32. # 关闭进度条
  33. progress_bar.close()
  34. # X = np.array(X)
  35. # y = np.array(y)
  36. return X, y, ngram_vocab
训练集/验证集/测试集的划分

接着,我们定义了一个函数train_test_split,用于将数据集划分为训练集和测试集。具体实现如下:

  1. def train_test_split(X, y, test_size=0.2, random_state=None):
  2. """
  3. 将数据集划分为训练集和测试集。
  4. Args:
  5. X (np.ndarray): 特征矩阵。
  6. y (np.ndarray): 标签数组。
  7. test_size (float): 测试集所占比例。
  8. random_state (int): 随机种子。
  9. Returns:
  10. Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: 训练集和测试集的特征和标签。
  11. """
  12. if random_state is not None:
  13. np.random.seed(random_state)
  14. # 计算测试集的样本数
  15. num_samples = X.shape[0]
  16. num_test_samples = int(num_samples * test_size)
  17. # 生成随机的索引
  18. indices = np.random.permutation(num_samples)
  19. # 划分训练集和测试集
  20. test_indices = indices[:num_test_samples]
  21. train_indices = indices[num_test_samples:]
  22. X_train = X[train_indices]
  23. X_test = X[test_indices]
  24. y_train = y[train_indices]
  25. y_test = y[test_indices]
  26. return X_train, X_test, y_train, y_test

模型设计与实现

接下来,我们将设计并实现一个基于logistic/softmax回归的文本分类模型。该模型包括softmax函数、分类交叉熵损失函数、梯度计算函数以及一个逻辑回归类。

softmax函数

softmax函数用于将线性模型的输出转换为概率分布。

  1. import numpy as np
  2. def softmax(z):
  3. """
  4. 计算softmax函数值。
  5. Args:
  6. z (np.ndarray): 形状为 (N, D) 的二维数组,其中 N 为样本数量,D 为特征维度。
  7. Returns:
  8. np.ndarray: 形状为 (N, D) 的二维数组,表示每个样本在每个特征上的概率分布。
  9. """
  10. exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))
  11. return exp_z / np.sum(exp_z, axis=1, keepdims=True)
分类交叉熵损失函数

分类交叉熵损失函数用于衡量预测概率与真实标签之间的差异。

  1. def categorical_cross_entropy(y_true, y_pred):
  2. """
  3. 计算分类交叉熵损失函数。
  4. Args:
  5. y_true (np.ndarray): 真实标签,形状为 (batch_size, num_classes)。
  6. y_pred (np.ndarray): 预测概率,形状为 (batch_size, num_classes)。
  7. Returns:
  8. float: 交叉熵损失值。
  9. """
  10. epsilon = 1e-15
  11. y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
  12. loss = -np.mean(np.sum(y_true * np.log(y_pred), axis=1))
  13. return loss
梯度计算函数

梯度计算函数用于计算模型参数的梯度,以便进行梯度下降更新。

  1. def compute_gradient(X, y_true, y_pred):
  2. """
  3. 计算线性回归的梯度。
  4. Args:
  5. X (np.ndarray): 形状为 (n_features, m) 的输入特征矩阵,其中 n_features 为特征数,m 为样本数。
  6. y_true (np.ndarray): 形状为 (m,) 的真实标签数组。
  7. y_pred (np.ndarray): 形状为 (m,) 的预测标签数组。
  8. Returns:
  9. Tuple[np.ndarray, np.ndarray]: 包含两个元素的元组,分别为:
  10. - 梯度 (np.ndarray): 形状为 (n_features,) 的梯度数组。
  11. - 平均误差 (np.ndarray): 形状为 (1,) 的平均误差数组。
  12. """
  13. m = y_true.shape[0]
  14. gradient = np.dot(X.T, (y_pred - y_true)) / m
  15. return gradient, np.mean(y_pred - y_true, axis=0)
逻辑回归模型

最后,我们定义一个逻辑回归类,实现模型的训练和预测功能。

  1. class LogisticRegression:
  2. def __init__(self, learning_rate=0.05, num_iterations=100):
  3. """
  4. 初始化线性回归模型的参数。
  5. Args:
  6. learning_rate (float, optional): 学习率,用于梯度下降算法中更新权重和偏置项,默认为0.05。
  7. num_iterations (int, optional): 梯度下降算法的迭代次数,默认为100。
  8. """
  9. self.learning_rate = learning_rate
  10. self.num_iterations = num_iterations
  11. self.weights = None
  12. self.bias = None
  13. def fit(self, X, y):
  14. """
  15. 对模型进行训练,通过梯度下降更新权重和偏置。
  16. Args:
  17. X (ndarray): 形状为(m, n)的输入数据,m表示样本数量,n表示特征数量。
  18. y (ndarray): 形状为(m,)的标签数据,表示每个样本的类别。
  19. """
  20. m, n = X.shape
  21. num_classes = len(np.unique(y))
  22. self.weights = np.zeros((n, num_classes))
  23. self.bias = np.zeros(num_classes)
  24. y_onehot = np.eye(num_classes, dtype=int)[y.astype(int)]
  25. for i in range(self.num_iterations):
  26. z = np.dot(X, self.weights) + self.bias
  27. y_pred = softmax(z)
  28. loss = categorical_cross_entropy(y_onehot, y_pred)
  29. dw, db = compute_gradient(X, y_onehot, y_pred)
  30. self.weights -= self.learning_rate * dw
  31. self.bias -= self.learning_rate * db
  32. if i % 10 == 0:
  33. print(f'Iteration {i}, Loss: {loss:.4f}')
  34. def predict(self, X):
  35. """
  36. 对输入数据X进行预测,返回预测结果。
  37. Args:
  38. X (numpy.ndarray): 输入的待预测数据,形状为(n_samples, n_features),其中n_samples为样本数量,n_features为特征数量。
  39. Returns:
  40. numpy.ndarray: 预测结果,形状为(n_samples,),其中每个元素为对应样本的预测类别标签。
  41. """
  42. z = np.dot(X, self.weights) + self.bias
  43. y_pred = softmax(z)
  44. 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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/848490
推荐阅读
相关标签
  

闽ICP备14008679号