当前位置: 首页 > article >正文

自然语言处理:文本分类

介绍

大家好,我这个热衷于分享知识的博主又来啦!之前我们一起深入探讨了自然语言处理领域中非常重要的两个方法:朴素贝叶斯逻辑斯谛回归。在探索的过程中,我们剖析了朴素贝叶斯如何基于概率原理和特征条件独立假设,快速对文本进行分类。也了解了逻辑斯谛回归是怎样通过巧妙地将线性模型与逻辑函数相结合,精准地预测文本的类别归属。

而今天,我要带大家进一步深入自然语言处理的世界,来聊聊文本分类这个关键的主题。有了之前对朴素贝叶斯逻辑斯谛回归的讲解,相信大家对文本分类中涉及的一些基础概念和核心思路已经有了一定的认知。我相信今天关于文本分类的分享,理解起来会更加顺畅,也能让大家更系统地掌握自然语言处理中这一重要的技术应用!

文本分类

概念阐述

自然语言处理中的文本分类,是指运用自然语言处理和机器学习等相关技术,依据文本的内容、结构、语义、情感倾向等多方面特征,将给定的文本自动划分到预先定义好的一个或多个类别中的过程

这些预先设定的类别可以是诸如新闻领域中的政治、经济、文化、体育等主题分类;电商场景里的产品好评、差评、中评的情感分类;或者是垃圾邮件识别中的正常邮件与垃圾邮件分类等。

文本分类通过对文本信息的理解和分析,实现对文本的有效组织与管理,从而为信息检索、情感分析、内容推荐等自然语言处理任务提供支持。

基于规则的文本分类

在文本分类任务里,基于规则的方法常运用正则表达式。具体来说,针对每个文本类别,我们都要编写一条甚至多条正则表达式。就拿商品评论分类举例:

  • 好评的正则表达式可以写成 “.(非常好 | 不错 | 很 (赞 | 满意)).” ,它表示只要文本中出现“非常好”“不错”“很赞”“很满意”等词汇,就可能属于好评。
  • 而差评的正则表达式 “.(太 (差 | 烂)| 糟糕 | 避雷).” ,意味着文本里若有“太差”“太烂”“糟糕”“避雷”这类词,大概率是差评。

我们用代码来演示一下:

完整代码
# 导入正则表达式模块,用于处理正则匹配
import re


# 定义一个自然语言处理文本分类的类
class NLPTextClassification:
    # 类的初始化方法,目前为空,不做任何操作
    def __init__(self):
        pass

    # 定义文本分类方法,接收一个评论文本作为输入
    def text_classification(self, review_text):
        # 定义分类规则列表,包含正则表达式、类别和优先级
        classification_rules = [
            # 定义好评的正则表达式规则,匹配包含特定词汇的文本
            (re.compile(r'.*(非常好|不错|很(赞|满意)).*'), '好评', 1),
            # 定义差评的正则表达式规则,匹配包含特定词汇的文本
            (re.compile(r'.*(太(差|烂)|糟糕|避雷).*'), '差评', 2)
        ]
        # 存储匹配到的规则信息
        matched_classification_rules = []
        # 遍历规则列表
        for regex_pattern, category, priority in classification_rules:
            # 检查评论是否匹配当前规则
            if regex_pattern.match(review_text):
                # 若匹配则添加到匹配规则列表
                matched_classification_rules.append((category, priority))
        # 若没有匹配到任何规则
        if len(matched_classification_rules) == 0:
            # 返回未分类结果
            return '未分类'
        # 若只匹配到一条规则
        elif len(matched_classification_rules) == 1:
            # 返回该规则对应的类别
            return matched_classification_rules[0][0]
        else:
            # 对匹配规则按优先级排序
            matched_classification_rules.sort(key=lambda x: x[1])
            # 返回优先级最高的规则对应的类别
            return matched_classification_rules[0][0]


# 当脚本作为主程序运行时执行以下代码
if __name__ == "__main__":
    # 创建NLPTextClassification类的实例
    nlp_text_normalization = NLPTextClassification()
    # 定义商品评论列表,包含多条不同的评论文本
    product_reviews = [
        "这个商品非常好,我很满意",
        "这个商品太差了,太烂了",
        "这个商品不错,但有点小瑕疵,避雷吧",
        "商品还行,没什么特别的"
    ]
    # 遍历评论列表进行分类
    for review in product_reviews:
        # 调用实例的文本分类方法进行分类
        classification_result = nlp_text_normalization.text_classification(review)
        # 打印评论内容及其分类结果
        print(f"评论: {review},分类结果: {classification_result}")
运行结果
评论: 这个商品非常好,我很满意,分类结果: 好评
评论: 这个商品太差了,太烂了,分类结果: 差评
评论: 这个商品不错,但有点小瑕疵,避雷吧,分类结果: 好评
评论: 商品还行,没什么特别的,分类结果: 未分类

进程已结束,退出代码为 0

当面对一篇文档时,如果它只匹配上了一条正则表达式,那直接就能确定其对应的类别。可要是文档同时匹配了多条对应不同类别的正则表达式,就得提前设定好这些正则表达式的优先级,输出优先级最高的那条正则表达式所对应的类别。

这种用正则表达式进行文本分类的方式,有明显的优点:

  • 首先,规则简单易懂,即便是不太了解技术的人,也能明白分类的依据。
  • 其次,要是分类结果出错,由于规则清晰,能迅速找到是哪条规则出了问题,方便及时修改,比如删掉不合适的规则、增添新规则等。
  • 而且,和基于机器学习的文本分类比起来,它无需大量标注数据,也不用复杂的训练过程,能节省不少时间和精力。

不过,它的缺点也不容忽视:

  • 一方面,编写正则表达式需要既懂分类业务,又熟悉正则表达式的专家,人力门槛较高。
  • 另一方面,由于语言表达的丰富性和多样性,即使是专家,也很难写出能涵盖所有情况的正则表达式,导致覆盖率有限。
  • 另外,在有标注数据的情况下,这种方法难以借助这些数据自动优化规则,缺乏自我提升的能力。

基于机器学习的文本分类

在自然语言处理领域,基于机器学习的文本分类是指利用机器学习中的监督学习算法,借助已标注好类别的训练数据(如由人工仔细标注的文档集合),让模型学习文本特征与类别之间的映射关系,从而对新文本自动归类的过程。

该方式可分为生成式分类器和判别式分类器。生成式分类器着重对每个类别下的文本分布进行建模,然后依据模型计算文本属于各类别的概率;判别式分类器则聚焦于挖掘能有效区分不同类别的文本特征,直接预测文本所属类别。

在这两类分类器中,朴素贝叶斯算法便是典型的生成式分类器的代表。它基于条件独立假设等原理,对每个类别内的文本分布进行建模。通过学习训练数据中每个类别出现的先验概率,以及每个词在各个类别中出现的条件概率,来计算文本属于不同类别的概率。

逻辑斯谛回归则属于判别式分类器,它直接挖掘文本中能够区分不同类别的特征,通过构建逻辑函数将线性回归的输出转化为概率值,以此预测文本的类别。

朴素贝叶斯

概念阐述

朴素贝叶斯是生成式分类器中的基础模型,它基于概率论和贝叶斯定理,通过计算文本在不同类别下的概率来判断文本类别。它假设文本中的各个特征(词)在给定类别下是相互独立的,即条件独立假设;同时采用词袋假设,忽略词的顺序,仅考虑词的出现情况,将文本看作是词的集合。

公式展示

将文档表示为词的序列d=(x_1, x_2,\cdots, x_n) ,类别集合为C。根据贝叶斯公式,计算文档d属于类别c的后验概率:

P(c|d)=\frac{P(d|c)P(c)}{P(d)}

由于P(d)对于所有类别都是相同的常数,在比较不同类别概率大小时可忽略,因此朴素贝叶斯分类的目标是求解:

\hat{c}=\arg\max_{c\in C}P(d|c)P(c)=\arg\max_{c\in C}P(x_1, x_2,\cdots, x_n|c)P(c)

基于条件独立假设,P(x_1, x_2,\cdots, x_n|c)=\prod_{i = 1}^{n}P(x_i|c),所以公式可进一步写为:

\hat{c}=\arg\max_{c\in C}P(c)\prod_{i = 1}^{n}P(x_i|c)

其中,\hat{c}是预测的文档类别;P(c)是类别c的先验概率;P(x_i|c)是在类别c下词x_i出现的条件概率 。

原理解释

朴素贝叶斯的核心在于根据训练数据学习每个类别出现的先验概率,以及每个词在各个类别中出现的条件概率。当对新文本分类时,根据这些已学习到的概率,结合贝叶斯公式计算该文本属于各个类别的后验概率,最终将文本划分到后验概率最大的类别中。

具体而言,先从训练集中统计每个类别文档的数量,以计算先验概率;再统计每个词在不同类别文档中出现的次数,进而得到条件概率。对于新文本,基于条件独立假设,将文本中各个词的条件概率相乘,再乘以先验概率,得到该文本属于每个类别的概率值,以此完成分类判断。

代码演示

接下来的几段代码,将为大家展示朴素贝叶斯模型的训练与预测过程。这里所用到的数据集,是代码自动生成的Books数据集,其中涵盖了大约1万本图书的标题,这些标题被划分成6种主题。

首先,在正式进行模型训练和预测之前,需要先进行数据预处理,针对文本分类的预处理主要包含以下几个关键步骤:

  • 文本统一化:对于英文文本,通常把所有字母转换为小写形式;而中文内容,则统一转换为简体字。这样的操作一般不会对文本所表达的核心内容产生影响。
  • 标点去除:英文里,标点符号与单词紧密相连(比如“Let's go.”),要是去掉标点,“Let' s”“go”会被当作和“Let”“s”“there”不同的词来识别,这显然不符合实际情况。在中文里,去掉标点通常也不会干扰文本想要传达的信息。
  • 分词处理:中文和英文不同,汉字之间没有空格来区分,所以中文分词的难度有时会比英文分词更大,具体的分词方法这里就不详细展开了。
  • 停用词清理:“and”“the”“了”这类词,它们在文本中出现的频率很高,但本身并没有实际的表意价值,所以需要把它们从文本中去除。
  • 构建词表:在构建词表时,一般会把语料库中出现频率极低的词排除在外。
  • 词向量化:将每个词转化为词表中的索引(也就是ID),这样更便于机器学习模型进行数据处理和分析。

然后,在完成分词操作后,筛选出出现次数大于3次的词元,据此构建词表,并将分词后的文档转换为对应的词元ID序列。

最后,我们要着手把数据以及对应的标签整理成一种更有利于进行模型训练的矩阵格式。这种格式能够让数据的结构更加规整,使得后续的训练过程更加高效、顺畅,有助于模型更好地学习数据中的特征和模式,从而提升训练的效果和质量。

在运行下列代码前,我们需要安装spacy库,安装命令:

pip install spacy

注:spacy是一款强大且高效的Python自然语言处理库,广泛应用于多种文本处理任务。它能够实现精准的词法分析,像将文本分割为词或符号的分词操作,为每个词标注词性的词性标注,以及把词还原成基本形式的词形还原。

成功安装spacy库后,我们还需要下载预训练的中文小型语言模型zh_core_web_sm,下载命令为:

python -m spacy download zh_core_web_sm

注:zh_core_web_smspacy库提供的一个预训练的中文语言模型。在自然语言处理(NLP)任务中,我们常常需要对文本进行各种分析和处理,而zh_core_web_sm模型可以帮助我们完成这些任务。例如在处理中文新闻文章时,使用该模型可以进行词性标注,了解每个词在句子中的词性;还能进行命名实体识别,找出文章里的人名、地名、组织机构名等关键信息,这有助于我们对文章内容进行快速理解和分类。 

完整代码

nlp_dataset_processor.py

# 导入用于处理JSON数据的模块
import json
# 从collections模块中导入defaultdict类,用于创建默认值为0的字典
from collections import defaultdict
# 导入自然语言处理库spacy
import spacy
# 从spacy的中文语言模块中导入中文停用词集合
from spacy.lang.zh.stop_words import STOP_WORDS
# 从tqdm模块中导入tqdm函数,用于显示进度条
from tqdm import tqdm
# 导入用于数据可视化的库matplotlib
import matplotlib

# 设置matplotlib的后端为tkAgg,以便在图形界面中显示图表
matplotlib.use('tkAgg')
# 加载中文语言模型,用于后续的自然语言处理任务
nlp_tool = spacy.load('zh_core_web_sm')


# 定义一个名为DatasetProcessor的类,用于处理数据集相关的操作
class DatasetProcessor:
    # 类的初始化方法,用于设置类的属性并初始化数据
    def __init__(self):
        # 初始化标签ID到标签名称的映射为空
        self.id_to_label = None
        # 初始化标签名称到标签ID的映射为空
        self.label_to_id = None
        # 初始化测试数据集为空
        self.test_dataset = None
        # 初始化训练数据集为空
        self.train_dataset = None
        # 初始化词到ID的映射为空
        self.word_to_id = None
        # 设置训练数据文件和测试数据文件的路径
        self.train_file_path, self.test_file_path = 'training_dataset.jsonl', 'test_dataset.jsonl'
        # 调用initialize_data方法初始化数据
        self.initialize_data()

    # 定义一个方法,用于读取指定路径的JSON文件并解析为Python对象列表
    def read_json_file(self, file_path):
        # 以只读模式打开指定路径的文件,并指定编码为utf-8
        with open(file_path, 'r', encoding='utf-8') as file_reader:
            # 读取文件的所有行并存储为列表
            json_lines = list(file_reader)
        # 初始化一个空列表,用于存储解析后的JSON数据
        data_list = []
        # 遍历每一行JSON数据
        for json_line in json_lines:
            # 将JSON字符串解析为Python对象,并添加到数据列表中
            data_list.append(json.loads(json_line))
        # 返回解析后的数据列表
        return data_list

    # 定义一个方法,用于初始化训练数据集、测试数据集以及标签的映射关系
    def initialize_data(self):
        # 调用read_json_file方法读取训练数据文件和测试数据文件
        self.train_dataset, self.test_dataset = self.read_json_file(self.train_file_path), self.read_json_file(
            self.test_file_path)
        # 打印训练数据集和测试数据集的大小
        print('训练数据集大小 =', len(self.train_dataset),
              ', 测试数据集大小 =', len(self.test_dataset))

        # 初始化标签名称到标签ID和标签ID到标签名称的映射字典
        self.label_to_id, self.id_to_label = {}, {}
        # 遍历训练数据集和测试数据集
        for data_subset in [self.train_dataset, self.test_dataset]:
            # 遍历数据集中的每个数据项
            for data_item in data_subset:
                # 获取数据项中的类别标签文本
                label_text = data_item['class']
                # 如果标签文本不在标签名称到标签ID的映射字典中
                if label_text not in self.label_to_id:
                    # 为该标签分配一个新的ID
                    label_index = len(self.label_to_id)
                    # 将标签文本和对应的ID添加到标签名称到标签ID的映射字典中
                    self.label_to_id[label_text] = label_index
                    # 将ID和对应的标签文本添加到标签ID到标签名称的映射字典中
                    self.id_to_label[label_index] = label_text
                # 获取该标签在映射字典中的ID
                label_id = self.label_to_id[label_text]
                # 在数据项中添加标签ID字段
                data_item['label'] = label_id

    # 定义一个方法,用于对数据集中的文本进行分词处理,并去除停用词
    def tokenize_text(self, attribute='book'):
        # 遍历训练数据集和测试数据集
        for data_subset in [self.train_dataset, self.test_dataset]:
            # 使用tqdm显示处理进度条,遍历数据集中的每个数据项
            for data_item in tqdm(data_subset):
                # 获取数据项中指定属性的文本,并转换为小写
                text_content = data_item[attribute].lower()
                # 使用nlp_tool对文本进行分词,并过滤掉停用词,获取分词列表
                tokens_list = [token.text for token in nlp_tool(text_content) if token.text not in STOP_WORDS]
                # 在数据项中添加分词结果字段
                data_item['tokens'] = tokens_list

    # 定义一个方法,用于构建词汇表,并统计词汇的相关信息
    def build_vocabulary(self, min_frequency=3, min_length=2, max_size=None):
        # 初始化一个默认值为0的字典,用于统计每个词的出现频率
        word_frequency = defaultdict(int)
        # 遍历训练数据集中的每个数据项
        for data_item in self.train_dataset:
            # 获取数据项中的分词列表
            tokens = data_item['tokens']
            # 遍历分词列表中的每个词
            for token in tokens:
                # 统计词的出现频率
                word_frequency[token] += 1

        # 打印词汇表的相关统计信息,包括唯一词数、总词数、最大频率和最小频率
        print(f'unique tokens = {len(word_frequency)}, ' + \
              f'total counts = {sum(word_frequency.values())}, ' + \
              f'max freq = {max(word_frequency.values())}, ' + \
              f'min freq = {min(word_frequency.values())}')

        # 初始化词到ID的映射字典
        self.word_to_id = {}
        # 初始化ID到词的映射字典
        self.id_to_word = {}
        # 初始化总词数计数器
        total_token_count = 0
        # 按词频从高到低遍历词频字典
        for token, freq in sorted(word_frequency.items(), key=lambda x: -x[1]):
            # 如果设置了最大词汇表大小,且当前词汇表大小已达到或超过该值,则停止添加词
            if max_size and len(self.word_to_id) >= max_size:
                break
            # 如果词的频率大于最小频率要求
            if freq > min_frequency:
                # 如果未设置最小词长要求,或者词长满足最小词长要求
                if (min_length is None) or (min_length and len(token) >= min_length):
                    # 将词添加到词到ID的映射字典中,并记录其ID
                    self.word_to_id[token] = len(self.word_to_id)
                    # 将ID添加到ID到词的映射字典中,并记录对应的词
                    self.id_to_word[len(self.id_to_word)] = token
                    # 累加总词数
                    total_token_count += freq
            else:
                break
        # 打印词汇表构建的相关参数和统计信息,包括最小频率、最小词长、最大大小、剩余词数和词汇表覆盖率
        print(f'min_freq = {min_frequency}, min_len = {min_length}, ' + \
              f'max_size = {max_size}, '
              f'remaining tokens = {len(self.word_to_id)}, '
              f'in-vocab rate = {total_token_count / sum(word_frequency.values())}')

    # 定义一个方法,用于将数据集中的分词结果转换为对应的词ID列表
    def convert_tokens_to_indices(self):
        # 遍历训练数据集和测试数据集
        for data_subset in [self.train_dataset, self.test_dataset]:
            # 遍历数据集中的每个数据项
            for data_item in data_subset:
                # 初始化一个空列表,用于存储词ID
                data_item['token_indices'] = []
                # 遍历数据项中的每个分词
                for token in data_item['tokens']:
                    # 如果分词在词到ID的映射字典中
                    if token in self.word_to_id:
                        # 将分词对应的ID添加到词ID列表中
                        data_item['token_indices'].append(self.word_to_id[token])

nlp_naive_bayes_classifier.py

# 导入用于数值计算的numpy库
import numpy as np


# 定义一个朴素贝叶斯分类器类
class NaiveBayesClassifier:
    # 类的初始化方法,接收类别数量和词汇表大小作为参数
    def __init__(self, num_classes, vocabulary_size):
        # 存储类别数量
        self.num_classes = num_classes
        # 存储词汇表大小
        self.vocabulary_size = vocabulary_size
        # 初始化先验概率数组,长度为类别数量,元素类型为64位浮点数
        self.prior_probabilities = np.zeros(num_classes, dtype=np.float64)
        # 初始化似然概率矩阵,形状为(类别数量, 词汇表大小),元素类型为64位浮点数
        self.likelihood_probabilities = np.zeros((num_classes, vocabulary_size), dtype=np.float64)

    # 定义训练模型的方法,接收输入特征和对应的标签作为参数
    def train_model(self, input_features, labels):
        # 遍历输入特征和对应的标签
        for feature, label in zip(input_features, labels):
            # 统计每个类别的样本数量,更新先验概率
            self.prior_probabilities[label] += 1
            # 遍历输入特征中的每个词的索引
            for token_index in feature:
                # 统计每个类别下每个词的出现次数,更新似然概率
                self.likelihood_probabilities[label, token_index] += 1

        # 计算先验概率,将每个类别的样本数量除以总样本数量
        self.prior_probabilities /= self.prior_probabilities.sum()
        # 对似然概率矩阵进行拉普拉斯平滑,避免概率为0的情况
        self.likelihood_probabilities += 1
        # 计算似然概率,将每个类别下每个词的出现次数除以该词在所有类别中的总出现次数
        self.likelihood_probabilities /= self.likelihood_probabilities.sum(axis=0)
        # 对先验概率取对数,避免下溢问题
        self.prior_probabilities = np.log(self.prior_probabilities)
        # 对似然概率取对数,避免下溢问题
        self.likelihood_probabilities = np.log(self.likelihood_probabilities)

    # 定义进行预测的方法,接收输入特征作为参数
    def make_predictions(self, input_features):
        # 初始化一个空列表,用于存储预测结果
        predictions = []
        # 遍历每个输入特征
        for feature in input_features:
            # 初始化一个概率数组,用于存储每个类别的预测概率
            probabilities = np.zeros(self.num_classes, dtype=np.float64)
            # 遍历每个类别
            for i in range(self.num_classes):
                # 累加该类别的先验概率
                probabilities[i] += self.prior_probabilities[i]
                # 遍历输入特征中的每个词的索引
                for token in feature:
                    # 累加该类别下该词的似然概率
                    probabilities[i] += self.likelihood_probabilities[i, token]
            # 将概率最大的类别索引作为预测结果,添加到预测结果列表中
            predictions.append(np.argmax(probabilities))
        # 返回预测结果列表
        return predictions

nlp_text_classification.py

# 导入用于数值计算的numpy库
import numpy as np
# 从自定义模块中导入数据集处理类
from nlp_dataset_processor import DatasetProcessor
# 从自定义模块中导入朴素贝叶斯分类器类
from nlp_naive_bayes_classifier import NaiveBayesClassifier


# 定义一个自然语言处理文本分类的类
class NLPTextClassification:
    # 类的初始化方法,目前为空,不做任何操作
    def __init__(self):
        pass

    # 定义文本分类方法
    def text_classification(self):
        # 创建数据集处理对象,用于加载和预处理数据集
        dataset = DatasetProcessor()
        # 对数据集中的文本进行分词处理
        dataset.tokenize_text()
        # 打印训练数据集中第一个样本的分词结果
        print(dataset.train_dataset[0]['tokens'])
        # 打印标签到ID的映射关系
        print(dataset.label_to_id)

        # 构建词汇表,设置最小词频为3
        dataset.build_vocabulary(min_frequency=3)
        # 将分词结果转换为对应的词ID列表
        dataset.convert_tokens_to_indices()
        # 打印训练数据集中第一个样本的词ID列表
        print(dataset.train_dataset[0]['token_indices'])

        # 初始化训练输入特征和训练标签列表
        training_input_features, training_labels = [], []
        # 初始化测试输入特征和测试标签列表
        testing_input_features, testing_labels = [], []

        # 遍历训练数据集
        for data in dataset.train_dataset:
            # 初始化特征向量,长度为词汇表大小,元素类型为32位整数
            feature_vector = np.zeros(len(dataset.word_to_id), dtype=np.int32)
            # 遍历样本的词ID列表
            for token_index in data['token_indices']:
                # 统计每个词的出现次数
                feature_vector[token_index] += 1
            # 将特征向量添加到训练输入特征列表中
            training_input_features.append(feature_vector)
            # 将样本的标签添加到训练标签列表中
            training_labels.append(data['label'])
        # 遍历测试数据集
        for data in dataset.test_dataset:
            # 初始化特征向量,长度为词汇表大小,元素类型为32位整数
            feature_vector = np.zeros(len(dataset.word_to_id), dtype=np.int32)
            # 遍历样本的词ID列表
            for token_index in data['token_indices']:
                # 统计每个词的出现次数
                feature_vector[token_index] += 1
            # 将特征向量添加到测试输入特征列表中
            testing_input_features.append(feature_vector)
            # 将样本的标签添加到测试标签列表中
            testing_labels.append(data['label'])
        # 将训练输入特征和训练标签转换为numpy数组
        training_input_features, training_labels = np.array(training_input_features), np.array(training_labels)
        # 将测试输入特征和测试标签转换为numpy数组
        testing_input_features, testing_labels = np.array(testing_input_features), np.array(testing_labels)

        # 创建朴素贝叶斯分类器对象,传入类别数量和词汇表大小
        naive_bayes_model = NaiveBayesClassifier(len(dataset.label_to_id), len(dataset.word_to_id))
        # 重新初始化训练输入特征和训练标签列表
        training_input_features, training_labels = [], []
        # 遍历训练数据集
        for data in dataset.train_dataset:
            # 将样本的词ID列表添加到训练输入特征列表中
            training_input_features.append(data['token_indices'])
            # 将样本的标签添加到训练标签列表中
            training_labels.append(data['label'])
        # 使用训练数据对朴素贝叶斯模型进行训练
        naive_bayes_model.train_model(training_input_features, training_labels)

        # 打印前3个类别的先验概率
        for i in range(3):
            print(f'P({dataset.id_to_label[i]}) = {np.exp(naive_bayes_model.prior_probabilities[i])}')
        # 打印前3个词在第一个类别下的似然概率
        for i in range(3):
            print(f'P({dataset.id_to_word[i]}|{dataset.id_to_label[0]}) = ' + \
                  f'{np.exp(naive_bayes_model.likelihood_probabilities[0, i])}')

        # 重新初始化测试输入特征和测试标签列表
        testing_input_features, testing_labels = [], []
        # 遍历测试数据集
        for data in dataset.test_dataset:
            # 将样本的词ID列表添加到测试输入特征列表中
            testing_input_features.append(data['token_indices'])
            # 将样本的标签添加到测试标签列表中
            testing_labels.append(data['label'])

        # 使用训练好的朴素贝叶斯模型对测试数据进行预测
        naive_bayes_predictions = naive_bayes_model.make_predictions(testing_input_features)

        # 打印前5个测试样本的预测结果和真实标签
        for i, (prediction, label) in enumerate(zip(naive_bayes_predictions, testing_labels)):
            if i >= 5:
                break
            print(f'test example:{i}, prediction = {prediction}, label = {label}')


# 程序入口,当脚本作为主程序运行时执行
if __name__ == "__main__":
    # 创建自然语言处理文本分类对象
    nlp_text_classification = NLPTextClassification()
    # 调用文本分类方法进行文本分类操作
    nlp_text_classification.text_classification()
运行结果
训练数据集大小 = 8627 , 测试数据集大小 = 2157
100%|██████████| 8627/8627 [00:20<00:00, 413.90it/s]
100%|██████████| 2157/2157 [00:05<00:00, 413.93it/s]
['地质学', '前沿', '研究', '进展']
{'科学类': 0, '计算机类': 1, '艺术传媒类': 2, '经管类': 3, '文学类': 4, '历史类': 5}
unique tokens = 97, total counts = 36235, max freq = 868, min freq = 262
min_freq = 3, min_len = 2, max_size = None, remaining tokens = 93, in-vocab rate = 0.9606181868359321
[80, 36, 37, 38]
P(科学类) = 0.1662223252579112
P(计算机类) = 0.16680190100846182
P(艺术传媒类) = 0.17016344036165526
P(小说|科学类) = 0.0011441647597254007
P(指南|科学类) = 0.0012987012987012991
P(揭秘|科学类) = 0.001362397820163487
测试样本:0, 预测结果 = 2, 真实类别 = 2
测试样本:1, 预测结果 = 2, 真实类别 = 2
测试样本:2, 预测结果 = 1, 真实类别 = 1
测试样本:3, 预测结果 = 5, 真实类别 = 5
测试样本:4, 预测结果 = 2, 真实类别 = 2

进程已结束,退出代码为 0

这段代码的目的是对书籍相关的文本数据进行预处理,构建朴素贝叶斯分类模型,并使用该模型对数据进行分类预测。通过数据处理和模型训练,实现对文本数据的自动分类。 

逻辑斯谛回归

概念阐述

逻辑斯谛回归(Logistic Regression)是一种广义的线性回归分析模型,常用于处理二分类问题(也可扩展到多分类问题),虽然名字里有“回归”,但它的本质是用于分类预测。它通过一个逻辑斯谛函数(也称为Sigmoid函数)将线性回归模型的输出值映射到一个概率值,该概率值表示样本属于某一类别的可能性大小。

例如,在判断一封邮件是否为垃圾邮件的场景中,逻辑斯谛回归可以根据邮件的各种特征(如发件人、主题、内容关键词等)计算出该邮件是垃圾邮件的概率,然后根据设定的阈值(通常为0.5)来决定邮件的类别。

公式展示

线性回归部分:辑斯谛回归首先基于线性回归模型,对于有$n$个特征的样本$x = (x_1, x_2, ..., x_n)$,其线性组合为$z = w_0 + w_1x_1 + w_2x_2 + ... + w_nx_n$,可以写成向量形式$z = w^Tx$,其中$w = (w_0, w_1, ..., w_n)$是模型的参数向量,$x = (1, x_1, x_2, ..., x_n)$(在$x$向量中添加了一个常数项$1$$w_0$对应)。

逻辑斯谛函数(Sigmoid函数):将线性回归的结果$z$作为输入,通过Sigmoid函数$\sigma(z) = \frac{1}{1 + e^{-z}}$,将$z$映射到区间$(0, 1)$上,得到样本属于正类(通常标记为1)的概率$p = \sigma(z) = \frac{1}{1 + e^{-w^Tx}}$。那么样本属于负类(通常标记为0)的概率为$1 - p$

原理解释:

  • 概率估计:逻辑斯谛回归的核心原理是通过最大化样本的似然函数来估计模型的参数$w$。对于一个二分类问题,假设样本$x_i$属于正类的概率为$p_i = \frac{1}{1 + e^{-w^Tx_i}}$,属于负类的概率为$1 - p_i$。给定一组独立同分布的样本$(x_1, y_1), (x_2, y_2), ..., (x_m, y_m)$,其中$y_i \in \{0, 1\}$是样本$x_i$的真实标签,其似然函数为$L(w) = \prod_{i = 1}^{m} p_i^{y_i} (1 - p_i)^{1 - y_i}$
  • 对数似然函数:为了便于计算和求导,通常对似然函数取对数,得到对数似然函数$l(w) = \sum_{i = 1}^{m} [y_i \ln(p_i) + (1 - y_i) \ln(1 - p_i)]$
  • 参数估计:目标是找到一组参数$w$,使得对数似然函数$l(w)$最大化。常用的方法是梯度上升法(与最小化损失函数的梯度下降法相对应),通过不断更新参数$w$,沿着梯度的方向逐步增加对数似然函数的值,直到达到收敛。在实际应用中,也可以通过一些优化算法如随机梯度下降、批量梯度下降等来求解参数$w$,从而得到最优的逻辑斯谛回归模型,用于对新的样本进行分类预测。 
代码演示

我们使用代码来演示,基于博主在自然语言处理:文本表示-CSDN博客介绍的TF-IDF方法构建文档的特征向量表示,并借助PyTorch深度学习框架实现逻辑斯谛回归模型的训练与预测。

逻辑斯谛回归本质上可被视为一种仅有一层的神经网络模型架构,借助PyTorch强大的框架来实现该模型时,能够轻松且高效地运用其自动求导功能,极大地简化了模型训练过程中的梯度计算。

下面给出的代码会利用已经经过训练、能够有效处理相关任务的模型,针对测试集中的数据进行预测。之后,会把预测得到的分类结果报告出来,方便我们了解模型在测试数据上的表现。

完整代码

nlp_tfidf_transformer.py

# 导入用于数值计算的numpy库
import numpy as np


# 定义一个TF-IDF转换器类,用于将文本特征转换为TF-IDF特征
class TFIDFTransformer:
    # 类的初始化方法,接收词汇表大小、归一化方式、是否平滑处理IDF和是否使用子线性TF作为参数
    def __init__(self, vocabulary_size, norm='l2', smooth_idf=True, sublinear_tf=True):
        # 初始化逆文档频率(IDF)为None
        self.idf = None
        # 存储词汇表大小
        self.vocabulary_size = vocabulary_size
        # 存储归一化方式,默认为'l2'
        self.norm = norm
        # 存储是否对IDF进行平滑处理的标志,默认为True
        self.smooth_idf = smooth_idf
        # 存储是否使用子线性TF的标志,默认为True
        self.sublinear_tf = sublinear_tf

    # 定义拟合转换器的方法,用于计算逆文档频率(IDF)
    def fit_transformer(self, input_features):
        # 初始化文档频率数组,长度为词汇表大小,元素类型为64位浮点数
        document_frequency = np.zeros(self.vocabulary_size, dtype=np.float64)
        # 遍历每个输入特征
        for data in input_features:
            # 遍历输入特征中唯一的词索引
            for token_index in set(data):
                # 统计每个词的文档频率
                document_frequency[token_index] += 1
        # 对文档频率进行平滑处理,若smooth_idf为True则加1,否则加0
        document_frequency += int(self.smooth_idf)
        # 计算样本数量,加上平滑处理的样本数
        num_samples = len(input_features) + int(self.smooth_idf)
        # 计算逆文档频率(IDF)
        self.idf = np.log(num_samples / document_frequency) + 1

    # 定义转换特征的方法,将输入特征转换为TF - IDF特征
    def transform_features(self, input_features):
        # 确保已经计算了逆文档频率(IDF)
        assert hasattr(self, 'idf')
        # 初始化词频矩阵,形状为(输入特征数量,词汇表大小),元素类型为64位浮点数
        term_frequency = np.zeros((len(input_features), self.vocabulary_size), dtype=np.float64)
        # 遍历每个输入特征及其索引
        for i, data in enumerate(input_features):
            # 遍历输入特征中的每个词索引
            for token in data:
                # 统计词频
                term_frequency[i, token] += 1
        # 如果使用子线性TF
        if self.sublinear_tf:
            # 对词频进行对数变换
            term_frequency = np.log(term_frequency + 1)
        # 计算TF-IDF特征矩阵
        transformed_features = term_frequency * self.idf
        # 如果设置了归一化方式
        if self.norm:
            # 计算每行的范数
            row_norm = (transformed_features ** 2).sum(axis=1)
            # 将范数为0的行设置为1,避免除零错误
            row_norm[row_norm == 0] = 1
            # 对TF-IDF特征矩阵进行归一化
            transformed_features /= np.sqrt(row_norm)[:, None]
        # 返回转换后的TF-IDF特征矩阵
        return transformed_features

    # 定义拟合并转换特征的方法,先计算IDF再转换输入特征
    def fit_and_transform(self, input_features):
        # 调用fit_transformer方法计算逆文档频率(IDF)
        self.fit_transformer(input_features)
        # 调用transform_features方法将输入特征转换为TF-IDF特征
        return self.transform_features(input_features)

nlp_logistic_regression_model.py

# 从torch模块导入nn子模块,用于构建神经网络
from torch import nn


# 定义一个逻辑回归模型类,继承自nn.Module
class LogisticRegressionModel(nn.Module):
    # 类的初始化方法,接收输入维度和输出维度作为参数
    def __init__(self, input_dimension, output_dimension):
        # 调用父类的初始化方法
        super(LogisticRegressionModel, self).__init__()
        # 定义一个线性层,输入维度为input_dimension,输出维度为output_dimension
        self.linear_layer = nn.Linear(input_dimension, output_dimension)

    # 定义模型的前向传播方法,接收输入特征和可选的标签作为参数
    def forward(self, input_features, labels=None):
        # 通过线性层计算输出
        outputs = self.linear_layer(input_features)

        # 如果传入了标签
        if labels is not None:
            # 定义交叉熵损失函数
            loss_function = nn.CrossEntropyLoss()
            # 计算损失
            loss = loss_function(outputs, labels)
            # 返回损失和输出
            return loss, outputs

        # 如果没有传入标签,直接返回输出
        return outputs

nlp_custom_dataset.py

# 从torch.utils.data模块导入Dataset类,用于自定义数据集
from torch.utils.data import Dataset


# 定义一个自定义数据集类,继承自torch的Dataset类
class CustomDataset(Dataset):
    # 类的初始化方法,接收输入特征和标签作为参数
    def __init__(self, input_features, labels):
        # 存储输入特征
        self.input_features = input_features
        # 存储标签
        self.labels = labels

    # 定义返回数据集长度的方法,用于获取数据集样本数量
    def __len__(self):
        # 返回输入特征的数量,即数据集的样本数
        return len(self.input_features)

    # 定义根据索引获取数据集中单个样本的方法
    def __getitem__(self, index):
        # 根据索引返回对应的输入特征和标签
        return self.input_features[index], self.labels[index]

nlp_data_collation.py

# 导入用于数值计算的numpy库
import numpy as np
# 导入深度学习框架PyTorch
import torch

# 定义一个数据整理类,用于对批量数据进行整理
class DataCollation:
    # 定义一个类方法,用于将批量数据整理成适合模型输入的格式
    @classmethod
    def collate_batch_data(cls, batch):
        # 初始化空列表,用于分别存储特征和标签
        features, labels = [], []
        # 遍历批量数据中的每个样本
        for feature, label in batch:
            # 将样本的特征添加到特征列表中
            features.append(feature)
            # 将样本的标签添加到标签列表中
            labels.append(label)
        # 将特征列表转换为numpy数组,再转换为torch的浮点型张量
        features = torch.tensor(np.array(features), dtype=torch.float)
        # 将标签列表转换为numpy数组,再转换为torch的长整型张量
        labels = torch.tensor(np.array(labels), dtype=torch.long)
        # 返回一个字典,包含整理好的输入特征和标签
        return {'input_features': features, 'labels': labels}

nlp_text_classification.py

# 导入用于数值计算的numpy库
import numpy as np
# 导入深度学习框架PyTorch
import torch
# 从torch.utils.data模块导入DataLoader类,用于批量加载数据
from torch.utils.data import DataLoader
# 从torch.optim模块导入Adam优化器,用于优化模型参数
from torch.optim import Adam
# 从tqdm模块导入trange函数,用于显示进度条
from tqdm import trange
# 导入用于数据可视化的matplotlib库
import matplotlib

# 从自定义模块中导入自定义数据集类
from nlp_custom_dataset import CustomDataset
# 从自定义模块中导入数据整理类
from nlp_data_collation import DataCollation
# 从自定义模块中导入数据集处理类
from nlp_dataset_processor import DatasetProcessor
# 从自定义模块中导入逻辑回归模型类
from nlp_logistic_regression_model import LogisticRegressionModel
# 从自定义模块中导入朴素贝叶斯分类器类
from nlp_naive_bayes_classifier import NaiveBayesClassifier
# 从自定义模块中导入TF - IDF转换器类
from nlp_tfidf_transformer import TFIDFTransformer

# 设置matplotlib的后端为tkAgg,以便在图形界面中显示图表
matplotlib.use('tkAgg')
# 定义一个字典config,用于设置matplotlib的字体和符号显示配置
config = {
    "font.family": 'serif',  # 设置字体族为衬线字体
    "mathtext.fontset": 'stix',  # 设置数学文本的字体集为stix
    "font.serif": 'SimSun',  # 设置衬线字体为宋体
    'axes.unicode_minus': False  # 解决负号显示问题
}
# 使用config字典更新matplotlib的全局参数配置
matplotlib.rcParams.update(config)
# 从matplotlib库中导入pyplot模块,用于绘制图表
import matplotlib.pyplot as plt


# 定义一个自然语言处理文本分类的类
class NLPTextClassification:
    # 类的初始化方法,目前为空,不做任何操作
    def __init__(self):
        pass

    # 定义文本分类方法
    def text_classification(self):
        # 创建数据集处理对象,用于加载和预处理数据集
        dataset = DatasetProcessor()
        # 对数据集中的文本进行分词处理
        dataset.tokenize_text()
        # 打印训练数据集中第一个样本的分词结果
        print(dataset.train_dataset[0]['tokens'])
        # 打印标签到ID的映射关系
        print(dataset.label_to_id)

        # 构建词汇表,设置最小词频为3
        dataset.build_vocabulary(min_frequency=3)
        # 将分词结果转换为对应的词ID列表
        dataset.convert_tokens_to_indices()
        # 打印训练数据集中第一个样本的词ID列表
        print(dataset.train_dataset[0]['token_indices'])

        # 初始化训练输入特征和训练标签列表
        training_input_features, training_labels = [], []
        # 初始化测试输入特征和测试标签列表
        testing_input_features, testing_labels = [], []

        # 遍历训练数据集
        for data in dataset.train_dataset:
            # 初始化特征向量,长度为词汇表大小,元素类型为32位整数
            feature_vector = np.zeros(len(dataset.word_to_id), dtype=np.int32)
            # 遍历样本的词ID列表
            for token_index in data['token_indices']:
                # 统计每个词的出现次数
                feature_vector[token_index] += 1
            # 将特征向量添加到训练输入特征列表中
            training_input_features.append(feature_vector)
            # 将样本的标签添加到训练标签列表中
            training_labels.append(data['label'])
        # 遍历测试数据集
        for data in dataset.test_dataset:
            # 初始化特征向量,长度为词汇表大小,元素类型为32位整数
            feature_vector = np.zeros(len(dataset.word_to_id), dtype=np.int32)
            # 遍历样本的词ID列表
            for token_index in data['token_indices']:
                # 统计每个词的出现次数
                feature_vector[token_index] += 1
            # 将特征向量添加到测试输入特征列表中
            testing_input_features.append(feature_vector)
            # 将样本的标签添加到测试标签列表中
            testing_labels.append(data['label'])
        # 将训练输入特征和训练标签转换为numpy数组
        training_input_features, training_labels = np.array(training_input_features), np.array(training_labels)
        # 将测试输入特征和测试标签转换为numpy数组
        testing_input_features, testing_labels = np.array(testing_input_features), np.array(testing_labels)

        # 创建朴素贝叶斯分类器对象,传入类别数量和词汇表大小
        naive_bayes_model = NaiveBayesClassifier(len(dataset.label_to_id), len(dataset.word_to_id))
        # 重新初始化训练输入特征和训练标签列表
        training_input_features, training_labels = [], []
        # 遍历训练数据集
        for data in dataset.train_dataset:
            # 将样本的词ID列表添加到训练输入特征列表中
            training_input_features.append(data['token_indices'])
            # 将样本的标签添加到训练标签列表中
            training_labels.append(data['label'])
        # 使用训练数据对朴素贝叶斯模型进行训练
        naive_bayes_model.train_model(training_input_features, training_labels)

        # 打印前3个类别的先验概率
        for i in range(3):
            print(f'P({dataset.id_to_label[i]}) = {np.exp(naive_bayes_model.prior_probabilities[i])}')
        # 打印前3个词在第一个类别下的似然概率
        for i in range(3):
            print(f'P({dataset.id_to_word[i]}|{dataset.id_to_label[0]}) = ' + \
                  f'{np.exp(naive_bayes_model.likelihood_probabilities[0, i])}')

        # 重新初始化测试输入特征和测试标签列表
        testing_input_features, testing_labels = [], []
        # 遍历测试数据集
        for data in dataset.test_dataset:
            # 将样本的词ID列表添加到测试输入特征列表中
            testing_input_features.append(data['token_indices'])
            # 将样本的标签添加到测试标签列表中
            testing_labels.append(data['label'])

        # 使用训练好的朴素贝叶斯模型对测试数据进行预测
        naive_bayes_predictions = naive_bayes_model.make_predictions(testing_input_features)

        # 打印前5个测试样本的预测结果和真实标签
        for i, (prediction, label) in enumerate(zip(naive_bayes_predictions, testing_labels)):
            if i >= 5:
                break
            print(f'测试样本:{i}, 预测结果 = {prediction}, 真实类别 = {label}')

        # 创建TF - IDF转换器对象,传入词汇表大小
        tfidf_transformer = TFIDFTransformer(len(dataset.word_to_id))
        # 使用训练数据拟合TF - IDF转换器
        tfidf_transformer.fit_transformer(training_input_features)
        # 将训练数据转换为TF - IDF特征
        training_features = tfidf_transformer.transform_features(training_input_features)
        # 将测试数据转换为TF - IDF特征
        testing_features = tfidf_transformer.transform_features(testing_input_features)

        # 创建逻辑回归模型对象,传入输入维度和输出维度
        logistic_regression_model = LogisticRegressionModel(len(dataset.word_to_id), len(dataset.label_to_id))

        # 设置训练的轮数
        num_epochs = 50
        # 设置批量大小
        batch_size = 128
        # 设置学习率
        learning_rate = 1e-3
        # 设置权重衰减系数
        weight_decay = 0

        # 创建自定义训练数据集对象
        training_dataset = CustomDataset(training_features, training_labels)
        # 创建自定义测试数据集对象
        testing_dataset = CustomDataset(testing_features, testing_labels)

        # 创建数据整理对象
        data_collator = DataCollation()
        # 创建训练数据加载器,设置批量大小、是否打乱数据和数据整理函数
        training_dataloader = DataLoader(training_dataset, batch_size=batch_size, shuffle=True,
                                         collate_fn=data_collator.collate_batch_data)
        # 创建测试数据加载器,设置批量大小、不打乱数据和数据整理函数
        testing_dataloader = DataLoader(testing_dataset, batch_size=batch_size, shuffle=False,
                                        collate_fn=data_collator.collate_batch_data)
        # 创建Adam优化器,传入逻辑回归模型的参数、学习率和权重衰减系数
        optimizer = Adam(logistic_regression_model.parameters(), lr=learning_rate, weight_decay=weight_decay)
        # 清空逻辑回归模型的梯度
        logistic_regression_model.zero_grad()
        # 将逻辑回归模型设置为训练模式
        logistic_regression_model.train()

        # 使用trange显示训练轮数的进度条
        with trange(num_epochs, desc='epoch', ncols=60) as progress_bar:
            # 初始化每一轮的损失列表
            epoch_losses = []
            # 遍历每一轮训练
            for epoch in progress_bar:
                # 将逻辑回归模型设置为训练模式
                logistic_regression_model.train()
                # 遍历训练数据加载器中的每个批次
                for step, batch in enumerate(training_dataloader):
                    # 计算当前批次的损失
                    loss = logistic_regression_model(**batch)[0]
                    # 更新进度条的描述信息,显示当前轮数和损失值
                    progress_bar.set_description(f'epoch-{epoch}, loss={loss.item():.4f}')
                    # 反向传播计算梯度
                    loss.backward()
                    # 更新模型参数
                    optimizer.step()
                    # 清空逻辑回归模型的梯度
                    logistic_regression_model.zero_grad()
                    # 将当前批次的损失添加到损失列表中
                    epoch_losses.append(loss.item())

            # 将每一轮的损失列表转换为numpy数组
            epoch_losses = np.array(epoch_losses)
            # 绘制损失曲线
            plt.plot(range(len(epoch_losses)), epoch_losses)
            # 设置x轴标签为训练轮数
            plt.xlabel('训练轮次')
            # 设置y轴标签为损失值
            plt.ylabel('损失(值)')
            # 显示损失曲线
            plt.show()

            # 将逻辑回归模型设置为评估模式
            logistic_regression_model.eval()
            # 不计算梯度,提高推理速度
            with torch.no_grad():
                # 初始化评估损失列表
                loss_values = []
                # 遍历测试数据加载器中的每个批次
                for batch in testing_dataloader:
                    # 计算当前批次的损失
                    loss = logistic_regression_model(**batch)[0]
                    # 将当前批次的损失添加到评估损失列表中
                    loss_values.append(loss.item())
                # 打印平均评估损失
                print(f'评估损失 = {np.mean(loss_values):.4f}')

        # 初始化逻辑回归模型的预测结果列表
        logistic_regression_predictions = []
        # 将逻辑回归模型设置为评估模式
        logistic_regression_model.eval()
        # 遍历测试数据加载器中的每个批次
        for batch in testing_dataloader:
            # 不计算梯度,提高推理速度
            with torch.no_grad():
                # 计算模型的输出和损失,取预测结果
                _, predictions = logistic_regression_model(**batch)
                # 获取预测结果中概率最大的类别索引
                predictions = np.argmax(predictions, axis=1)
                # 将当前批次的预测结果添加到预测结果列表中
                logistic_regression_predictions.extend(predictions)

        # 打印前5个测试样本的逻辑回归模型预测结果和真实标签
        for i, (prediction, label) in enumerate(zip(logistic_regression_predictions, testing_labels)):
            if i >= 5:
                break
            print(f'测试样本:{i}, 预测结果 = {prediction}, 真实类别 = {label}')


# 程序入口,当脚本作为主程序运行时执行
if __name__ == "__main__":
    # 创建自然语言处理文本分类对象
    nlp_text_classification = NLPTextClassification()
    # 调用文本分类方法进行文本分类操作
    nlp_text_classification.text_classification()
运行结果
训练数据集大小 = 8627 , 测试数据集大小 = 2157
100%|██████████| 8627/8627 [00:20<00:00, 413.90it/s]
100%|██████████| 2157/2157 [00:05<00:00, 413.93it/s]
['地质学', '前沿', '研究', '进展']
{'科学类': 0, '计算机类': 1, '艺术传媒类': 2, '经管类': 3, '文学类': 4, '历史类': 5}
unique tokens = 97, total counts = 36235, max freq = 868, min freq = 262
min_freq = 3, min_len = 2, max_size = None, remaining tokens = 93, in-vocab rate = 0.9606181868359321
[80, 36, 37, 38]
P(科学类) = 0.1662223252579112
P(计算机类) = 0.16680190100846182
P(艺术传媒类) = 0.17016344036165526
P(小说|科学类) = 0.0011441647597254007
P(指南|科学类) = 0.0012987012987012991
P(揭秘|科学类) = 0.001362397820163487
测试样本:0, 预测结果 = 2, 真实类别 = 2
测试样本:1, 预测结果 = 2, 真实类别 = 2
测试样本:2, 预测结果 = 1, 真实类别 = 1
测试样本:3, 预测结果 = 5, 真实类别 = 5
测试样本:4, 预测结果 = 2, 真实类别 = 2
epoch-49, loss=0.0274: 100%|█| 50/50 [00:02<00:00, 20.06it/s
评估损失 = 0.0303
测试样本:0, 预测结果 = 2, 真实类别 = 2
测试样本:1, 预测结果 = 2, 真实类别 = 2
测试样本:2, 预测结果 = 1, 真实类别 = 1
测试样本:3, 预测结果 = 5, 真实类别 = 5
测试样本:4, 预测结果 = 2, 真实类别 = 2

进程已结束,退出代码为 0

注意:博主采用的训练集和测试集是用代码自动生成出来的内容保存到文件中去,目的是为了方便讲解功能和展示效果,代码也是方便给大家参考。请大家根据需求加载自己的训练集和测试集。

分类结果评价

概念阐述

在自然语言处理(NLP)中,分类结果评价是指使用一系列指标和方法,对文本分类模型的性能和预测效果进行评估和分析,从而衡量模型在将文本正确归类到不同类别方面的能力。其主要目的包括评估模型的优劣、比较不同模型的性能、发现模型存在的问题以及为模型的改进提供依据。

评价指标

准确率

Accuracy,指分类模型正确预测的样本数占总样本数的比例,计算公式为:

$Accuracy = \frac{TP + TN}{TP + TN + FP + FN}$

其中 $TP$(真正例)是指实际为正例且被正确预测为正例的样本数,$TN$(真反例)是实际为反例且被正确预测为反例的样本数,$FP$(假正例)是实际为反例但被错误预测为正例的样本数,$FN$(假反例)是实际为正例但被错误预测为反例的样本数。例如,在一个情感分类任务中,总共有100条文本,模型正确分类了80条,那么准确率就是80%

精确率

Precision,也叫查准率,是指在被模型预测为正例的样本中,真正为正例的比例,计算公式为:

$Precision = \frac{TP}{TP + FP}$

例如,模型预测了30条文本为积极情感,其中实际有25条是积极情感,那么精确率为$\frac{25}{30} \approx 83.3\%$

召回率

Recall,又称查全率,是指在实际为正例的样本中,被模型正确预测为正例的比例,计算公式为:

$Recall = \frac{TP}{TP + FN}$

例如,实际有40条积极情感的文本,模型正确预测出了30条,召回率就是$\frac{30}{40} = 75\%$

F1值

F1-score,是精确率召回率调和平均数综合考虑精确率召回率,计算公式为:

$F1 = \frac{2 \times Precision \times Recall}{Precision + Recall}$

F1值越高,说明模型在精确率和召回率方面的综合表现越好。

宏平均

Macro-average,对于多分类问题,先分别计算每个类别的精确率召回率F1值,然后对这些算术平均值,得到宏平均精确率、宏平均召回率和宏平均F1值。它给予每个类别相同的权重,能够反映模型在各个类别上的平均性能。

微平均

Micro-average,在多分类问题中,先计算所有类别的真正例假正例假反例总数,然后基于这些总数计算精确率召回率F1值。它更关注整体的分类性能,尤其是在样本不均衡的情况下,能够突出模型对主要类别的分类能力。

其他

除了这些指标外,还有一些其他的评价方法和指标,如混淆矩阵(Confusion Matrix),可以直观地展示模型在各个类别上的分类情况;ROC曲线(Receiver Operating Characteristic Curve)AUC(Area Under the Curve),用于评估模型在不同阈值下的性能等。通过综合运用这些评价指标和方法,可以全面、客观地评价自然语言处理中的分类结果。  

代码演示

接下来的代码将运用已经完成训练流程、具备预测能力的模型,针对测试集展开预测工作,并且会对预测所得到的分类结果进行报告。

完整代码

nlp_evaluation_metrics.py

# 导入用于数值计算的numpy库
import numpy as np


# 定义一个评估指标类,用于计算评估模型性能的指标
class EvaluationMetrics:
    # 类的初始化方法,目前为空实现
    def __init__(self):
        pass

    # 定义计算微平均F1值的方法,接收数据集、预测结果和真实标签作为参数
    def calculate_micro_f1(self, dataset, predictions, true_labels):
        # 计算预测结果与真实标签相等的数量,即真正例的数量
        true_positives = np.sum(predictions == true_labels)
        # 初始化假负例和假正例的数量为0
        false_negatives = false_positives = 0
        # 遍历数据集中的每个类别
        for i in range(len(dataset.label_to_id)):
            # 累加假负例的数量,即预测为该类但真实不是该类的数量
            false_negatives += np.sum((predictions == i) & (true_labels != i))
            # 累加假正例的数量,即预测不是该类但真实是该类的数量
            false_positives += np.sum((predictions != i) & (true_labels == i))
        # 计算精确率,即真正例在真正例和假正例总和中的比例
        precision = true_positives / (true_positives + false_positives)
        # 计算召回率,即真正例在真正例和假负例总和中的比例
        recall = true_positives / (true_positives + false_negatives)
        # 计算微平均F1值,是精确率和召回率的调和平均值
        f1_score = 2 * precision * recall / (precision + recall)
        # 返回计算得到的微平均F1值
        return f1_score

    # 定义计算宏平均F1值的方法,接收数据集、预测结果和真实标签作为参数
    def calculate_macro_f1(self, dataset, predictions, true_labels):
        # 初始化一个空列表,用于存储每个类别的F1值
        f_scores = []
        # 遍历数据集中的每个类别
        for i in range(len(dataset.label_to_id)):
            # 计算当前类别的真正例数量
            true_positives = np.sum((predictions == i) & (true_labels == i))
            # 计算当前类别的假负例数量
            false_negatives = np.sum((predictions == i) & (true_labels != i))
            # 计算当前类别的假正例数量
            false_positives = np.sum((predictions != i) & (true_labels == i))
            # 计算当前类别的精确率
            precision = true_positives / (true_positives + false_positives)
            # 计算当前类别的召回率
            recall = true_positives / (true_positives + false_negatives)
            # 计算当前类别的F1值
            f1_score = 2 * precision * recall / (precision + recall)
            # 将当前类别的F1值添加到列表中
            f_scores.append(f1_score)
        # 计算所有类别F1值的平均值,即宏平均F1值
        return np.mean(f_scores)

nlp_text_classification.py

# 导入用于数值计算的numpy库
import numpy as np
# 导入深度学习框架PyTorch
import torch
# 从torch.utils.data模块导入DataLoader类,用于批量加载数据
from torch.utils.data import DataLoader
# 从torch.optim模块导入Adam优化器,用于优化模型参数
from torch.optim import Adam
# 从tqdm模块导入trange函数,用于显示进度条
from tqdm import trange
# 导入用于数据可视化的matplotlib库
import matplotlib

# 从自定义模块中导入自定义数据集类
from nlp_custom_dataset import CustomDataset
# 从自定义模块中导入数据整理类
from nlp_data_collation import DataCollation
# 从自定义模块中导入数据集处理类
from nlp_dataset_processor import DatasetProcessor
# 从自定义模块中导入评估指标类
from nlp_evaluation_metrics import EvaluationMetrics
# 从自定义模块中导入逻辑回归模型类
from nlp_logistic_regression_model import LogisticRegressionModel
# 从自定义模块中导入朴素贝叶斯分类器类
from nlp_naive_bayes_classifier import NaiveBayesClassifier
# 从自定义模块中导入TF - IDF转换器类
from nlp_tfidf_transformer import TFIDFTransformer

# 设置matplotlib的后端为tkAgg,以便在图形界面中显示图表
matplotlib.use('tkAgg')
# 定义一个字典config,用于设置matplotlib的字体和符号显示配置
config = {
    "font.family": 'serif',  # 设置字体族为衬线字体
    "mathtext.fontset": 'stix',  # 设置数学文本的字体集为stix
    "font.serif": 'SimSun',  # 设置衬线字体为宋体
    'axes.unicode_minus': False  # 解决负号显示问题
}
# 使用config字典更新matplotlib的全局参数配置
matplotlib.rcParams.update(config)
# 从matplotlib库中导入pyplot模块,用于绘制图表
import matplotlib.pyplot as plt


# 定义一个自然语言处理文本分类的类
class NLPTextClassification:
    # 类的初始化方法,目前为空,不做任何操作
    def __init__(self):
        pass

    # 定义文本分类方法
    def text_classification(self):
        # 创建数据集处理对象,用于加载和预处理数据集
        dataset = DatasetProcessor()
        # 对数据集中的文本进行分词处理
        dataset.tokenize_text()
        # 打印训练数据集中第一个样本的分词结果
        print(dataset.train_dataset[0]['tokens'])
        # 打印标签到ID的映射关系
        print(dataset.label_to_id)

        # 构建词汇表,设置最小词频为3
        dataset.build_vocabulary(min_frequency=3)
        # 将分词结果转换为对应的词ID列表
        dataset.convert_tokens_to_indices()
        # 打印训练数据集中第一个样本的词ID列表
        print(dataset.train_dataset[0]['token_indices'])

        # 初始化训练输入特征和训练标签列表
        training_input_features, training_labels = [], []
        # 初始化测试输入特征和测试标签列表
        testing_input_features, testing_labels = [], []

        # 遍历训练数据集
        for data in dataset.train_dataset:
            # 初始化特征向量,长度为词汇表大小,元素类型为32位整数
            feature_vector = np.zeros(len(dataset.word_to_id), dtype=np.int32)
            # 遍历样本的词ID列表
            for token_index in data['token_indices']:
                # 统计每个词的出现次数
                feature_vector[token_index] += 1
            # 将特征向量添加到训练输入特征列表中
            training_input_features.append(feature_vector)
            # 将样本的标签添加到训练标签列表中
            training_labels.append(data['label'])
        # 遍历测试数据集
        for data in dataset.test_dataset:
            # 初始化特征向量,长度为词汇表大小,元素类型为32位整数
            feature_vector = np.zeros(len(dataset.word_to_id), dtype=np.int32)
            # 遍历样本的词ID列表
            for token_index in data['token_indices']:
                # 统计每个词的出现次数
                feature_vector[token_index] += 1
            # 将特征向量添加到测试输入特征列表中
            testing_input_features.append(feature_vector)
            # 将样本的标签添加到测试标签列表中
            testing_labels.append(data['label'])
        # 将训练输入特征和训练标签转换为numpy数组
        training_input_features, training_labels = np.array(training_input_features), np.array(training_labels)
        # 将测试输入特征和测试标签转换为numpy数组
        testing_input_features, testing_labels = np.array(testing_input_features), np.array(testing_labels)

        # 创建朴素贝叶斯分类器对象,传入类别数量和词汇表大小
        naive_bayes_model = NaiveBayesClassifier(len(dataset.label_to_id), len(dataset.word_to_id))
        # 重新初始化训练输入特征和训练标签列表
        training_input_features, training_labels = [], []
        # 遍历训练数据集
        for data in dataset.train_dataset:
            # 将样本的词ID列表添加到训练输入特征列表中
            training_input_features.append(data['token_indices'])
            # 将样本的标签添加到训练标签列表中
            training_labels.append(data['label'])
        # 使用训练数据对朴素贝叶斯模型进行训练
        naive_bayes_model.train_model(training_input_features, training_labels)

        # 打印前3个类别的先验概率
        for i in range(3):
            print(f'P({dataset.id_to_label[i]}) = {np.exp(naive_bayes_model.prior_probabilities[i])}')
        # 打印前3个词在第一个类别下的似然概率
        for i in range(3):
            print(f'P({dataset.id_to_word[i]}|{dataset.id_to_label[0]}) = ' + \
                  f'{np.exp(naive_bayes_model.likelihood_probabilities[0, i])}')

        # 重新初始化测试输入特征和测试标签列表
        testing_input_features, testing_labels = [], []
        # 遍历测试数据集
        for data in dataset.test_dataset:
            # 将样本的词ID列表添加到测试输入特征列表中
            testing_input_features.append(data['token_indices'])
            # 将样本的标签添加到测试标签列表中
            testing_labels.append(data['label'])

        # 使用训练好的朴素贝叶斯模型对测试数据进行预测
        naive_bayes_predictions = naive_bayes_model.make_predictions(testing_input_features)

        # 打印前5个测试样本的预测结果和真实标签
        for i, (prediction, label) in enumerate(zip(naive_bayes_predictions, testing_labels)):
            if i >= 5:
                break
            print(f'测试样本:{i}, 预测结果 = {prediction}, 真实类别 = {label}')

        # 创建TF - IDF转换器对象,传入词汇表大小
        tfidf_transformer = TFIDFTransformer(len(dataset.word_to_id))
        # 使用训练数据拟合TF - IDF转换器
        tfidf_transformer.fit_transformer(training_input_features)
        # 将训练数据转换为TF - IDF特征
        training_features = tfidf_transformer.transform_features(training_input_features)
        # 将测试数据转换为TF - IDF特征
        testing_features = tfidf_transformer.transform_features(testing_input_features)

        # 创建逻辑回归模型对象,传入输入维度和输出维度
        logistic_regression_model = LogisticRegressionModel(len(dataset.word_to_id), len(dataset.label_to_id))

        # 设置训练的轮数
        num_epochs = 50
        # 设置批量大小
        batch_size = 128
        # 设置学习率
        learning_rate = 1e-3
        # 设置权重衰减系数
        weight_decay = 0

        # 创建自定义训练数据集对象
        training_dataset = CustomDataset(training_features, training_labels)
        # 创建自定义测试数据集对象
        testing_dataset = CustomDataset(testing_features, testing_labels)

        # 创建数据整理对象
        data_collator = DataCollation()
        # 创建训练数据加载器,设置批量大小、是否打乱数据和数据整理函数
        training_dataloader = DataLoader(training_dataset, batch_size=batch_size, shuffle=True,
                                         collate_fn=data_collator.collate_batch_data)
        # 创建测试数据加载器,设置批量大小、不打乱数据和数据整理函数
        testing_dataloader = DataLoader(testing_dataset, batch_size=batch_size, shuffle=False,
                                        collate_fn=data_collator.collate_batch_data)
        # 创建Adam优化器,传入逻辑回归模型的参数、学习率和权重衰减系数
        optimizer = Adam(logistic_regression_model.parameters(), lr=learning_rate, weight_decay=weight_decay)
        # 清空逻辑回归模型的梯度
        logistic_regression_model.zero_grad()
        # 将逻辑回归模型设置为训练模式
        logistic_regression_model.train()

        # 使用trange显示训练轮数的进度条
        with trange(num_epochs, desc='epoch', ncols=60) as progress_bar:
            # 初始化每一轮的损失列表
            epoch_losses = []
            # 遍历每一轮训练
            for epoch in progress_bar:
                # 将逻辑回归模型设置为训练模式
                logistic_regression_model.train()
                # 遍历训练数据加载器中的每个批次
                for step, batch in enumerate(training_dataloader):
                    # 计算当前批次的损失
                    loss = logistic_regression_model(**batch)[0]
                    # 更新进度条的描述信息,显示当前轮数和损失值
                    progress_bar.set_description(f'epoch-{epoch}, loss={loss.item():.4f}')
                    # 反向传播计算梯度
                    loss.backward()
                    # 更新模型参数
                    optimizer.step()
                    # 清空逻辑回归模型的梯度
                    logistic_regression_model.zero_grad()
                    # 将当前批次的损失添加到损失列表中
                    epoch_losses.append(loss.item())

            # 将每一轮的损失列表转换为numpy数组
            epoch_losses = np.array(epoch_losses)
            # 绘制损失曲线
            plt.plot(range(len(epoch_losses)), epoch_losses)
            # 设置x轴标签为训练轮数
            plt.xlabel('训练轮次')
            # 设置y轴标签为损失值
            plt.ylabel('损失(值)')
            # 显示损失曲线
            plt.show()

            # 将逻辑回归模型设置为评估模式
            logistic_regression_model.eval()
            # 不计算梯度,提高推理速度
            with torch.no_grad():
                # 初始化评估损失列表
                loss_values = []
                # 遍历测试数据加载器中的每个批次
                for batch in testing_dataloader:
                    # 计算当前批次的损失
                    loss = logistic_regression_model(**batch)[0]
                    # 将当前批次的损失添加到评估损失列表中
                    loss_values.append(loss.item())
                # 打印平均评估损失
                print(f'评估损失 = {np.mean(loss_values):.4f}')

        # 初始化逻辑回归模型的预测结果列表
        logistic_regression_predictions = []
        # 将逻辑回归模型设置为评估模式
        logistic_regression_model.eval()
        # 遍历测试数据加载器中的每个批次
        for batch in testing_dataloader:
            # 不计算梯度,提高推理速度
            with torch.no_grad():
                # 计算模型的输出和损失,取预测结果
                _, predictions = logistic_regression_model(**batch)
                # 获取预测结果中概率最大的类别索引
                predictions = np.argmax(predictions, axis=1)
                # 将当前批次的预测结果添加到预测结果列表中
                logistic_regression_predictions.extend(predictions)

        # 打印前5个测试样本的逻辑回归模型预测结果和真实标签
        for i, (prediction, label) in enumerate(zip(logistic_regression_predictions, testing_labels)):
            if i >= 5:
                break
            print(f'测试样本:{i}, 预测结果 = {prediction}, 真实类别 = {label}')

        # 将测试标签转换为numpy数组
        testing_labels = np.array(testing_labels)
        # 将朴素贝叶斯模型的预测结果转换为numpy数组
        naive_bayes_predictions = np.array(naive_bayes_predictions)
        # 将逻辑回归模型的预测结果转换为numpy数组
        logistic_regression_predictions = np.array(logistic_regression_predictions)

        # 创建评估指标对象
        evaluation = EvaluationMetrics()
        # 打印朴素贝叶斯模型的微平均F1值和宏平均F1值
        print(
            f'NB: micro-f1 = {evaluation.calculate_micro_f1(dataset, naive_bayes_predictions, testing_labels)}, ' + f'macro-f1 = {evaluation.calculate_macro_f1(dataset, naive_bayes_predictions, testing_labels)}')
        # 打印逻辑回归模型的微平均F1值和宏平均F1值
        print(
            f'LR: micro-f1 = {evaluation.calculate_micro_f1(dataset, logistic_regression_predictions, testing_labels)}, ' + f'macro-f1 = {evaluation.calculate_macro_f1(dataset, logistic_regression_predictions, testing_labels)}')


# 程序入口,当脚本作为主程序运行时执行
if __name__ == "__main__":
    # 创建自然语言处理文本分类对象
    nlp_text_classification = NLPTextClassification()
    # 调用文本分类方法进行文本分类操作
    nlp_text_classification.text_classification()
运行结果
NB: micro-f1 = 1.0, macro-f1 = 1.0
LR: micro-f1 = 1.0, macro-f1 = 1.0

进程已结束,退出代码为 0

注意:博主采用的训练集和测试集是用代码自动生成出来的内容保存到文件中去,目的是为了方便讲解功能和展示效果,代码也是方便给大家参考。请大家根据需求加载自己的训练集和测试集。

结束

好了,以上就是本次关于自然语言处理文本分类的全部内容了。从数据集的预处理、特征工程的构建,到朴素贝叶斯和逻辑回归等不同模型的应用,再到分类结果的评估,我们全面地探讨了文本分类任务的各个关键环节。

在这个过程中,我们看到了不同模型的优势与局限。朴素贝叶斯算法凭借其简单高效,在一些场景下能取得不错的效果;而逻辑回归模型借助强大的深度学习框架PyTorch,通过自动求导等功能,实现了更为精细的参数优化。

当然,自然语言处理文本分类领域仍有许多值得深入研究和探索的方向。比如如何进一步提升模型在小样本、不均衡数据集上的性能,以及怎样更好地融合多种模型的优点来构建更强大的分类器。希望今天的分享能为大家在自然语言处理的学习和实践中提供一些思路和帮助。

那么本次分享就到这里了。最后,博主还是那句话:请大家多去大胆的尝试和使用,成功总是在不断的失败中试验出来的,敢于尝试就已经成功了一半。如果大家对博主分享的内容感兴趣或有帮助,请点赞和关注。大家的点赞和关注是博主持续分享的动力🤭,博主也希望让更多的人学习到新的知识。


http://www.kler.cn/a/572768.html

相关文章:

  • CS144 Lab Checkpoint 5: down the stack (the network interface)
  • Element UI-Select选择器结合树形控件终极版
  • Ruby爬虫如何控制并发数量:爬取京东电子产品
  • 如何在matlab中创建自己的库
  • CSS【实战】模拟 html 的 title 属性(鼠标悬浮显示提示文字)
  • OBOO鸥柏丨LCD液晶室外AI触控屏广告一体机,服务区交互新趋势
  • python:pymunk + pygame 模拟六边形中小球弹跳运动
  • 文件上传漏洞:upload-labs靶场11-20
  • RISCV下Dovetail移植(2)——原子操作
  • TikTok矩阵系统介绍
  • 特辣的海藻!8
  • @PostConstruct注解的作用
  • 【前端学习笔记】Git 原理及面试题
  • 用本地浏览器打开服务器上使用的Tensorboard
  • 自学微信小程序的第十三天
  • 【Spring Boot 应用开发】-04-02 自动配置-数据源-手撸一个最简持久层工具类
  • 驱动开发系列43 - Linux 显卡KMD驱动代码分析(四)- DRM设备操作
  • Golang的数据库分库分表
  • 【网络安全】API安全防护完整指南
  • 【计算机网络入门】TCP拥塞控制