从零开始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的方法:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值