自然语言处理——02 文本预处理(下)

1 文本数据分析

1.1 概述

  • 文本数据分析的作用:

    • 文本数据分析能够有效帮助我们理解数据语料,快速检查出语料可能存在的问题,指导模型训练过程中一些超参数的选择;
    • 比如:关于标签Y,分类问题查看标签是否均匀;关于数据X, 数据有没有脏数据、数据长度分布是否合理等;
  • 常用的几种文本数据分析方法:

    • 标签数量分布
    • 句子长度分布
    • 词频统计与关键词词云
  • 下面基于中文酒店评论,来讲解常用的几种文本数据分析方法

    • 该中文酒店评论,属于二分类的中文情感分析语料;

    • 其中train.tsv代表训练集,dev.tsv代表验证集,二者数据样式相同;

    • train.tsv数据格式:共两列

      • 第一列数据代表具有感情色彩的评论文本;
      • 第二列数据为0或1,代表每条文本数据是积极或者消极的评论。0代表消极,1代表积极;

1.2 获取标签数量分布

  • 什么是标签数量分布?就是求某一个标签的数量有多少个,占总数的多少......

  • 训练深度学习模型时,比如分类问题,一般需要将正负样本比例维持在1:1左右;

  • 若不符合1:1比例,需进行数据增强或删减;

  • 代码:

    py 复制代码
    import seaborn as sns
    import pandas as pd
    import matplotlib.pyplot as plt
    python 复制代码
    # 设置显示风格
    plt.style.use('fivethirtyeight')
    
    # 读训练集、验证集
    train_data = pd.read_csv('cn_data/train.tsv', sep='\t')
    dev_data = pd.read_csv('cn_data/dev.tsv', sep='\t')
    
    # 查看训练集标签数量分布情况
    # sns.countplot()统计label标签的0、1分组数量
    # x='label'表示按照这个字段进行分组,data表示数据来源 
    sns.countplot(x='label', data=train_data)  # 方式1
    # sns.countplot(x=train_data['label']) # 方式2
    
    plt.title('train_data')
    plt.show()
    python 复制代码
    # 查看验证集标签数量分布情况
    # sns.countplot(x='label', data=dev_data)  # 方式1
    sns.countplot(x=dev_data['label']) # 方式2
    
    plt.title('dev_data')
    plt.show()

1.3 获取句子长度分布

  • 若模型对输入的数据长度有要求,可以对句子进行截断或补齐操作,规范长度对于模型的训练会起到关键的指导作用;

  • 代码:

    python 复制代码
    plt.style.use('fivethirtyeight')
    # 新增数据长度列
    # 利用map和lambda表达式,遍历train_data['sentence']中的每个元素,计算其长度,
    # 并将结果转为列表,赋值给train_data新增的'sentence_length'列,用于后续分析句子长度分布
    train_data['sentence_length'] = list(map(lambda x: len(x), train_data['sentence']))
    
    # 绘制数据长度分布图-柱状图
    sns.countplot(x='sentence_length', data=train_data)
    plt.xticks([]) # 清空x轴刻度显示,不展示具体的x轴刻度值,让图表x轴看起来更简洁(如需看具体长度,可注释掉这行)
    plt.show()
    python 复制代码
    # 绘制数据长度分布图-曲线图
    sns.displot(x='sentence_length', data=train_data)
    plt.yticks([])
    plt.show()

1.4 获取正负样本长度散点分布

  • 就是按照x正负样本进行分组,再按照y长度绘制散点图;

  • 通过查看正负样本长度散点图,可有效定位异常点的出现位置,帮助我们更准确地进行人工语料审查;

  • 代码:

    python 复制代码
    # 训练集-散点图
    sns.stripplot(y='sentence_length', x='label', data=train_data)
    plt.show()
    • 上图中在训练集的正样本中出现了异常点,它的句子长度近3500左右,需要我们人工审查;
    python 复制代码
    # 利用map和lambda表达式,遍历dev_data['sentence']中的每个元素,计算其长度,
    # 并将结果转为列表,赋值给dev_data新增的'sentence_length'列,
    dev_data['sentence_length'] = list(map(lambda x: len(x), dev_data['sentence']))
    # 验证集-散点图
    sns.stripplot(y='sentence_length', x='label', data=dev_data)
    plt.show()

1.5 获取不同词汇总数统计

python 复制代码
# chain()函数用于将多个可迭代对象连接成一个
from itertools import chain
import jieba
python 复制代码
# 对训练集中的每一个句子进行分词处理
# 1. map(lambda x: jieba.lcut(x), train_data['sentence']):
#    用map函数遍历train_data['sentence']中的每个句子,
#    对每个句子应用jieba.lcut(x)进行分词,得到一个包含多个分词列表的map对象
# 2. *map(...):将map对象解包成多个独立的列表,作为chain函数的参数
# 3. chain(...):将所有独立的分词列表连接成一个连续的可迭代对象(扁平化成一维序列)
# 4. set(...):将连接后的序列转换为集合,自动去除重复词汇,得到训练集中的所有不重复词汇
train_vocab = set(chain(*map(lambda x: jieba.lcut(x), train_data['sentence'])))
print("训练集共包含不同词汇总数为:", len(train_vocab))

# 验证集的句子进行分词, 并统计出不同词汇的总数
dev_vocab = set(chain(*map(lambda x: jieba.lcut(x), dev_data['sentence'])))
print("训练集共包含不同词汇总数为:", len(dev_vocab))
  • chain()函数可以把多层嵌套的可迭代对象(比如列表套列表 [[...], [...]])转化为单层的可迭代对象,就像把 "嵌套的俄罗斯套娃" 拆成 "排成一排的单个套娃",让数据结构更简单,方便后续处理;

    • 没扁平化 :数据是 [[...], [...]](列表套列表),像 "套娃" 一样多层嵌套;
    • chain() 扁平化 :把嵌套的层级 "拆平",变成 [...,...] 单层结构,方便后续统计、去重等操作;
    python 复制代码
    # 初始化itag变量,控制循环执行 2 次
    itag = 0
    # 遍历:对 train_data["sentence"] 里的每个句子,用 jieba.lcut(x) 分词,得到的分词结果(列表)赋值给 i 
    for i in map(lambda x: jieba.lcut(x), train_data["sentence"]):  
        # 打印当前句子的分词结果(是一个列表,每个元素是分词后的词语 )
        print('i--->', i)  
        # 解包分词列表,把列表里的词语用空格隔开打印,更直观看分词内容
        print('*i --->', *i)  
        # 用 chain(*i) 把分词列表 "扁平化",chain 会把列表里的元素连起来变成一个可迭代对象(类似把 ['a','b'] 变成 'a','b' 连续迭代 )
        print('chain(*i) --->', chain(*i))  
        # 用 set 对 chain 处理后的内容去重,得到不重复词语的集合,看看有哪些独特的词
        print('set(chain(*i))--->', set(chain(*i)))  
        # 计算去重后的词语数量,看当前句子分词后有多少不同的词
        print('len ( set(chain(*i)))', len(set(chain(*i))) )  
        # 当 itag 等于 1 时,停止循环(因为初始化是 0,第一次循环后 itag 变 1,第二次循环就会触发 break )
        if itag == 1:  
            break  
        # 每次循环结束,itag 加 1,实现 "只执行前两次循环" 的控制
        itag = itag + 1  
    复制代码
    i---> ['早餐', '不好', ',', '服务', '不', '到位', ',', '晚餐', '无', '西餐', ',', '早餐', '晚餐', '相同', ',', '房间', '条件', '不好', ',', '餐厅', '不', '分', '吸烟区', '.', '房间', '不分', '有', '无烟', '房', '.']
    *i ---> 早餐 不好 , 服务 不 到位 , 晚餐 无 西餐 , 早餐 晚餐 相同 , 房间 条件 不好 , 餐厅 不 分 吸烟区 . 房间 不分 有 无烟 房 .
    chain(*i) ---> <itertools.chain object at 0x00000150590B4160>
    set(chain(*i))---> {'不', '相', '件', '务', '无', '有', '早', ',', '房', '位', '条', '区', '服', '分', '好', '烟', '到', '间', '餐', '.', '同', '厅', '吸', '晚', '西'}
    len ( set(chain(*i))) 25
    i---> ['去', '的', '时候', ' ', ',', '酒店', '大厅', '和', '餐厅', '在', '装修', ',', '感觉', '大厅', '有点', '挤', '.', '由于', '餐厅', '装修', '本来', '该', '享受', '的', '早饭', ',', '也', '没有', '享受', '(', '他们', '是', '8', '点', '开始', '每个', '房间', '送', ',', '但是', '我', '时间', '来不及', '了', ')', '不过', '前台', '服务员', '态度', '好', '!']
    *i ---> 去 的 时候   , 酒店 大厅 和 餐厅 在 装修 , 感觉 大厅 有点 挤 . 由于 餐厅 装修 本来 该 享受 的 早饭 , 也 没有 享受 ( 他们 是 8 点 开始 每个 房间 送 , 但是 我 时间 来不及 了 ) 不过 前台 服务员 态度 好 !
    chain(*i) ---> <itertools.chain object at 0x000001505C369090>
    set(chain(*i))---> {'8', '装', '由', '不', '来', '享', '度', '和', '点', '务', '了', '员', '个', '台', '有', '修', '于', '早', ' ', '及', ',', '房', '大', '态', '酒', '也', '前', '受', '但', '服', '(', '去', '的', '本', '我', '店', '感', '送', '候', '时', '好', '在', '该', '他', '间', '每', '!', '餐', '挤', '觉', '.', ')', '始', '们', '过', '饭', '没', '厅', '开', '是'}
    len ( set(chain(*i))) 60

1.6 获取训练集高频词云

  • 安装包:

    sh 复制代码
    pip install wordcloud
  • 代码:

    python 复制代码
    # 导入 jieba 的词性标注模块,用于分词和词性识别
    import jieba.posseg as pseg
    from wordcloud import WordCloud
    python 复制代码
    # 从文本中提取形容词列表
    def get_a_list(text):
        r = []
        # 使用 jieba 的词性标注方法切分文本,返回一个可迭代的词性标注结果(每个元素包含词和词性)
        for g in pseg.lcut(text):
            # 判断词性标记是否为 "a"
            if g.flag == "a":
                # 如果是形容词,就将该词添加到列表 r 中
                r.append(g.word)
        return r
    python 复制代码
    # 根据形容词列表生成词云
    def get_word_cloud(keywords_list):
        # 初始化词云对象,设置字体路径(SimHei.ttf 是中文字体,避免中文显示为方框)、最大词数、最小词长度、背景颜色
        wordcloud = WordCloud(font_path="cn_data/SimHei.ttf", max_words=100, min_word_length=2, background_color='white')
        # 将形容词列表用空格连接成字符串,因为 WordCloud.generate 需要传入字符串作为参数
        keywords_string = " ".join(keywords_list)
        # 根据拼接好的字符串生成词云(统计词频并布局)
        wordcloud.generate(keywords_string)
    
        # 开始绘制词云图像
        plt.figure()
        # 以双线性插值的方式显示词云图像,让图像更平滑
        plt.imshow(wordcloud, interpolation="bilinear")
        # 隐藏坐标轴(词云一般不需要显示坐标轴)
        plt.axis('off')
        plt.show()
    python 复制代码
    # 定义主函数:整合流程,生成词云(分别处理正样本和负样本)
    def word_cloud():
        # 读取训练集数据
        train_data = pd.read_csv(filepath_or_buffer='cn_data/train.tsv', sep='\t')
        # 筛选出训练集中 label 为 1 的正样本的 sentence 列数据
        p_train_data = train_data[train_data['label'] == 1]['sentence']
    
        # 获取正样本句子中的形容词:
        # 先对每个句子用 get_a_list 提取形容词,再用 chain 扁平化结果(因为 map 返回的是多个列表,chain 把它们连成一个可迭代对象 )
        p_a_train_vocab = chain(*map(lambda x: get_a_list(x), p_train_data))
        # 打印可迭代对象(方便调试查看内容)
        # print(p_a_train_vocab)
        # 转换成列表打印,直观看到正样本提取的形容词
        # print(list(p_a_train_vocab))
    
        # 为正样本形容词生成词云
        get_word_cloud(p_a_train_vocab)
    
        # 分割线,方便区分正、负样本的输出
        print('*' * 60)
        # 处理负样本:筛选出训练集中 label 为 0 的负样本的 sentence 列数据
        n_train_data = train_data[train_data['label'] == 0]['sentence']
    
        n_a_train_vocab = chain(*map(lambda x: get_a_list(x), n_train_data))
        # print(n_a_train_vocab)
        # print(list(n_a_train_vocab))
    
        # 为负样本形容词生成词云
        get_word_cloud(n_a_train_vocab)
    word_cloud()
  • 根据高频形容词词云显示,对当前语料质量进行简单评估,同时对违反语料标签含义的词汇进行人工审查和修正,来保证绝大多数语料符合训练标准;

  • 上图中的正样本大多数是褒义词,而负样本大多数是贬义词,基本符合要求,但是负样本词云中也存在"豪华"这样的褒义词,因此可人工进行审查。

2 文本特征处理

2.1 概述

  • 给文本语料数据添加具有普适性的文本特征,让模型更有效的处理数据,提高模型性能指标;
  • 常用方法:
    • 添加n-gram特征 (两个单词总是相邻并共现,可以认为是一个特征)
    • 文本长度规范(文本的长度是多少,也可以认为是一个特征)

2.2 n-gram特征

  • 核心概念:相邻共现的 n 个词/字,作为一个特征

    • 一句话/一段文本,把相邻的 n 个词(或字)看成一个整体,这个整体就叫"n-gram 特征" 。比如:
      • n=2(叫 bi-gram/二元语法)→ 看"相邻 2 个词",像"好吃""便宜"
      • n=3(叫 tri-gram/三元语法)→ 看"相邻 3 个词",像"很好吃""不便宜"
  • 为啥要这么做?因为语言里,"词和词的相邻关系"能传递语义。比如"好吃""便宜"连在一起,能表达"这家店不错";只看单个词"好""吃""便""宜",就很难判断意思;

  • 实际处理文本时,二元、三元语法最常用(n 太大容易出现"没意义的组合",还会让数据变多);

    • 比如分析用户评论:

      • 原句:"环境 舒服 服务 好"
      • bi-gram(n=2)特征:"环境 舒服""舒服 服务""服务 好"
      • tri-gram(n=3)特征:"环境 舒服 服务""舒服 服务 好"
    • 这些"相邻词组合",能帮模型更好理解"环境舒服""服务好"这样的语义;

  • 例:

    • 原始数据:

      • 分词列表(把句子拆成单个词):["是谁", "敲动", "我心"]

      • 每个词对应"数值映射"(可以理解成给词编个号,方便计算机处理):[1, 34, 21]

    • 加入 bi-gram(n=2)特征,找 相邻 2 个词的组合

      • 第 1、2 个词:"是谁" + "敲动" → 假设编号 1000(代表这俩词相邻共现)

      • 第 2、3 个词:"敲动" + "我心" → 假设编号 1001(代表这俩词相邻共现)

    • 把这些"相邻组合的编号",加到原始数值列表里 ,就得到:[1, 34, 21, 1000, 1001]

    • 这么做的意义:让计算机不仅能看到"单个词"(1、34、21),还能看到"词的相邻关系"(1000、1001 代表的组合),理解更丰富的语义;

  • 实际用途:帮模型更好理解语言

    • 比如做"情感分析"(判断评论是好评还是差评):

      • 单个词:"好""差""一般" → 能判断,但不够准;
      • 加上 bi-gram:"很好""不差""一般般" → 语义更明确,模型判断更准;
    • 再比如搜索关键词:用户搜"机器学习",如果只看单个词"机器"+"学习",可能匹配到"机器维修""学习资料";但用 bi-gram 把"机器学习"当整体,就能更精准找到你想要的内容;

  • 代码示例:计算一个文本序列有多少个2-gram特征

    py 复制代码
    # 定义输入的文本序列(已转换为数字编码的词列表)
    input_list = [1, 3, 2, 1, 5, 3]
    # 设置要计算的n-gram类型,这里是2-gram(二元语法)
    ngram_range = 2  # 一般取2或3
    
    # 核心逻辑:生成所有可能的2-gram并去重
    # 1. [input_list[i:] for i in range(ngram_range)] 
    #    生成两个错位的子列表:
    #    - 当i=0时:input_list[0:] → [1, 3, 2, 1, 5, 3](从第0个元素开始的完整列表)
    #    - 当i=1时:input_list[1:] → [3, 2, 1, 5, 3](从第1个元素开始的子列表)
    #    结果是:[[1, 3, 2, 1, 5, 3], [3, 2, 1, 5, 3]]
    
    # 2. zip(*[...]) 
    #    对两个错位列表进行拉链式组合,每次各取一个元素组成元组:
    #    - 第1组:1和3 → (1, 3)
    #    - 第2组:3和2 → (3, 2)
    #    - 第3组:2和1 → (2, 1)
    #    - 第4组:1和5 → (1, 5)
    #    - 第5组:5和3 → (5, 3)
    #    结果是包含这些元组的可迭代对象
    
    # 3. set(...) 
    #    将zip结果转换为集合,自动去除重复的2-gram
    res = set(zip(*[input_list[i:] for i in range(ngram_range)]))
    
    # 打印最终结果:所有不重复的2-gram特征
    print(res)  # 输出 {(1, 3), (3, 2), (2, 1), (1, 5), (5, 3)}

2.3 文本长度规范

  • 送给模型的数据一般都是有长度要求的;比如批量(每次送8个样本)样本长度要一样,这样需要对批量数据进行文本长度规范;

  • 比如:文本过长需要截断,文本过短需要打pad补齐(补零),这个操作就是文本长度规范;

  • 例:

    python 复制代码
    # 从 TensorFlow 的 Keras 预处理模块中导入 sequence 工具,它常用于序列数据(如文本序列)的预处理
    from tensorflow.keras.preprocessing import sequence
    
    # 设置要统一调整到的序列长度,这里设定为 10
    cutlen = 10
    
    # 构造训练集样本数据,包含两条文本序列(用数字模拟分词后的编码序列)
    # 第一条序列长度是 12(大于 cutlen=10),第二条序列长度是 5(小于 cutlen=10)
    x_train = [
        [1, 23, 5, 32, 55, 63, 2, 21, 78, 32, 23, 1],  # 长度为 12 的序列
        [2, 32, 1, 23, 1]  # 长度为 5 的序列
    ]
    
    # 调用 sequence.pad_sequences 函数,对序列进行填充或截断,让所有序列长度统一为 cutlen
    # sequences=x_train:要处理的序列数据
    # maxlen=cutlen:指定所有序列最终要调整到的长度
    # padding='post':在序列的末尾(post 位置)进行填充
    # truncating='post':如果序列长度超过 maxlen,在序列的末尾(post 位置)进行截断
    res = sequence.pad_sequences(
        sequences=x_train, 
        maxlen=cutlen, 
        padding='post', 
        truncating='post'
    )  
    
    # 打印处理后的数据,查看填充/截断后的结果
    print('res padding以后的数据--->', res)

3 文本数据增强

  • 核心逻辑:"翻译→回译"造新数据

    • 操作流程 :中文 → 翻译成小语种(比如"冰岛语""僧伽罗语"这种小众语言)>>再翻译回中文>>得到和原文本意思差不多,但表述可能有差异的新句子,把这些新句子加到原数据里,就实现"数据增强";

    • 为什么能增强数据?

      • 模型训练需要"多样的样本",但实际场景里,标注数据(带标签的文本)很难找;

      • 用翻译回译的方式,不改变原标签(比如原文本是"好评",回译后还是"好评"),但能造出"新表述"的样本,让模型见更多"说法",提升泛化能力;

  • 优点:操作简单,能快速搞到新数据

    • 门槛低:只要能调用翻译接口(谷歌、百度翻译 API 等),写几行代码就能跑通流程;

    • 质量相对高:小语种翻译回译后,句子"大框架"不变(比如"这家店好吃" → 翻译回译后可能变成"该店铺味道不错"),标签(好评/差评)基本不会变,造出来的新数据能用;

  • 缺点:有局限,不是万能的

    • 重复率高,特征空间没扩大

      • 比如原文本是"很好吃,推荐",回译后可能是"非常美味,建议尝试"。虽然表述变了,但核心语义、用词特征("好吃/美味""推荐/建议尝试")很像
      • 模型学来学去,还是在"同样的特征"里打转,没法学到新东西(比如原数据没有"性价比高"这类表述,回译也造不出来);
    • 翻译次数越多,越容易出问题

      • 翻译 1~2 次(比如中→韩→中):可能还能保持意思;
      • 翻译 3 次以上(中→韩→日→英→中):句子会越来越离谱(比如"好吃" → 翻译成"delicious" → 再翻成"美味" → 再翻成"可口" → 最后可能变成"味道不错" ,但次数太多,也可能出现"语义漂移",比如原句是"一般",回译后变成"还可以",看着没问题;但如果原句是"极差",多次翻译可能变成"不太好" ,语义被弱化,标签就失效了);
  • 实操建议:

    1. 控制翻译次数:别贪多,一般"中→小语种→中" 1~2 次就行,超过 3 次容易翻车;
    2. 搭配其他增强方法:别只依赖翻译回译。可以结合"同义词替换"(把"好吃" 换成"美味""可口")、"随机插入/删除词"(在句子里加个"非常",或删个"很"),让样本特征更丰富;
    3. 做好过滤 :回译后的句子,人工抽检一下,把"语义漂移严重""标签变了" 的样本筛掉,别让垃圾数据进训练集;
  • 例:

    python 复制代码
    # 先执行 pip install deep-translator 安装
    from deep_translator import GoogleTranslator
    
    # 中译英,source 使用 'zh-CN' 表示中文(简体)
    en_text = GoogleTranslator(source='zh-CN', target='en').translate("这个价格非常便宜")
    # 英译中,source 使用 'en' 表示英文
    zh_text = GoogleTranslator(source='en', target='zh-CN').translate(en_text)
    
    print("回译结果:", zh_text)
相关推荐
文心快码BaiduComate3 小时前
百度云与光本位签署战略合作:用AI Agent 重构芯片研发流程
前端·人工智能·架构
风象南3 小时前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
Mintopia4 小时前
OpenClaw 对软件行业产生的影响
人工智能
陈广亮5 小时前
构建具有长期记忆的 AI Agent:从设计模式到生产实践
人工智能
会写代码的柯基犬5 小时前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia5 小时前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区5 小时前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两8 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
前端付豪8 小时前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain