欺诈文本分类微调(三):生成反向数据集

1. 引言

前文欺诈文本分类微调(二):生成正向数据集介绍了生成正向数据集的方法和过程,本文会使用一些真实会议的ASR文本,来构建欺诈文本分类微调的反向数据集(非欺诈)。大概思路如下:

  1. 读取所有的会议文本,过滤会议文本中的语气助词。
  2. 大的发言段落二次切割成100字以内的小段落,用以和欺诈文本长度尽量一致。
  3. 将切割后的小段落按照正向数据集的格式来组织,并保存对话集到文件,作为基础数据集供后续复用。

需要注意的是:正标签和负标签的对话文本数量需要基本一致,避免因数量不一致而导致模型学习的程度不一样。

2. 加载文件

定义要加载的asr文件所在目录以及有效asr的文件列表。

asr_dir = "/Users/a200007/Downloads/summary/docs/"

file_names = [
    "39046229_20221103_095950.txt", 
    "比亚迪专家会.stt",
    "五一座谈会.stt",
    "证券投资介绍咨询会.stt",
    "merge-summary-2ba2c48b-a1b1-434e-a877-58ece02c72e2.stt",
    "专业术语-1.stt",
    "线下4月15日第一场网络会议.stt",
    "merge-summary-b92ee22a4d5c43ca8ee12ac7327568ecsum.stt",
    "merge-summary-e25f867df4d1487ebdf8426674d71fasum.stt", 
    "merge-summary-ff3c24d2a88d48d3bffc1cef65b76cc9sum.stt",
    "商社顺周期沙龙.stt",
    "农业深度报告巡礼.stt",
    ……
]

导入基础包,并复用前文中的一些工具函数。

import os
import re
import textwrap
import pandas as pd
from typing import List

def filename(path):
    filename_with_ext = os.path.basename(path)
    filename, _ = os.path.splitext(filename_with_ext)
    return filename

def pretty_print(text):
    wrapped_text = textwrap.fill(text, width=80)  # 设定每行的最大字符数
    print(wrapped_text)

定义两个方法,功能分别如下:

  • load_file:用于加载文件内容,并按"\n\n"切割成段落。
  • filter_interjections:用于去掉段落中的语气助词,避免无效内容的干扰。
def filter_interjections(content: str) -> str:
    interjections = ['啊','呃','呀','吧','唉','嘛','噢','嗯','喂','喽','喔','哦','呵','呸','嘿','嚯','嘞','哎','噫','哟','啧','咦']
    # 构建简化的正则表达式模式,直接将所有语气词连接在一起
    pattern = "|".join(interjections)
    # 使用re.sub()函数替换语气词为空字符串
    filtered_text = re.sub(pattern, "", content)
    return filtered_text

def load_file(file_path, filter_func=None):
    file_content = ""
    with open(file_path, "r", encoding='utf8') as f:
        file_content = f.read()
    contents = file_content.split('\n\n')
    if filter_func:
        contents = [filter_func(content) for content in contents]
    return contents

last_file_path = os.path.join(asr_dir, file_names[-1])
contents = load_file(last_file_path, filter_interjections)
print("content_length: ", len(contents[6]))
pretty_print(contents[6])
    content_length:  528
    2024-01-30 16:10:49 发言人3: 好的,各位投资人晚上好,我是天丰农业的林一丹。那今天呢是我们这个8:30的这个巡礼的第四场,然后给各位领导汇
    报一下种植板块,特别是种子的这个整体的框架和机会。那我们看到这个其实种子的话也就是一个我们觉得呢就是它本身有一定的弱周期性,就是比如说这个粮价上涨的话,呢这个对
    种子的需求会有一定的拉动的作用,然后包括像行业的这个供给,这种数量的话呢对种子的供给呢会起到一些影影响的作用。所以我们看今年其实重点可能放在粮价这块,这个如果说
    粮食价格总体比较坚挺的话,呢对。这个年底开始的整个种子的这个销售情况,我们也可以更乐观一些。另外呢就是行业具备成长性,就是种子其实它是生物行业就是或者说叫生物育
    种这个行业,它本身技术变革其实还是比较这个比如说增长这个幅度还是很大的。我们看这个20世纪主要的技术呢是杂交技术,杂交技术也培育出了非常优质的品种,包括中国的杂
    交水稻在全球都处于全球领先的水平。然后包括像这个也有一些优秀的育种家,像杨文平老先生,李东李邓老先生分别是分别是优秀的这个水稻的育种家和玉米的育种家。那这个杂交
    技术的话,中国其实也是相对比较熟练的掌握了。那下一个大的技术变革呢就是转基因技术或者叫生物育种。

3. 长段落切割

由于我们收集的所有会议ASR文件中的段落都是按照[时间戳] [发言人]: [发言内容]的格式组织的,这里定义一个段落格式解析函数,用于从段落中解析出发言人和发言内容。

def parse_paragraph(text: str):
    paragraph_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([^:]+):(.*)"
    match = re.match(paragraph_pattern, text.strip())
    if not match:
        print(f"not match paragraph pattern: {text}")
        return "", "", ""
    return match.group(1), match.group(2), match.group(3).strip()

timestamp, speaker, content = parse_paragraph(contents[6])
timestamp, speaker, content 
    ('2024-01-30 16:10:49',
     '发言人3',
     '好的,各位投资人晚上好,我是天丰农业的xxx。那今天呢是我们这个8:30的这个巡礼的第四场,然后给各位领导汇报一下种植板块,特别是种子的这个整体的框架和机会。那我们看到这个其实种子的话也就是一个我们觉得呢就是它本身有一定的弱周期性,就是比如说这个粮价上涨的话,呢这个对种子的需求会有一定的拉动的作用,然后包括像行业的这个供给,这种数量的话呢对种子的供给呢会起到一些影影响的作用。所以我们看今年其实重点可能放在粮价这块,这个如果说粮食价格总体比较坚挺的话,呢对。这个年底开始的整个种子的这个销售情况,我们也可以更乐观一些。另外呢就是行业具备成长性,就是种子其实它是生物行业就是或者说叫生物育种这个行业,它本身技术变革其实还是比较这个比如说增长这个幅度还是很大的。我们看这个20世纪主要的技术呢是杂交技术,杂交技术也培育出了非常优质的品种,包括中国的杂交水稻在全球都处于全球领先的水平。然后包括像这个也有一些优秀的育种家,像杨文平老先生,李东李邓老先生分别是分别是优秀的这个水稻的育种家和玉米的育种家。那这个杂交技术的话,中国其实也是相对比较熟练的掌握了。那下一个大的技术变革呢就是转基因技术或者叫生物育种。')

得到段落的发言内容后,有些发言内容比较长,我们有必要将发言内容切割,尽可能让正、反向的发言长度保持一致。

注:上面示例的段落有500多字,而前文用gpt生成的单条对话内容普遍在100字左右,最长也就150字,将正、反数据集的发言长度处理成一致,可以避免模型在训练时误将内容长度学习成区分正反向数据集的特征。

首先定义一个按照指定标点符号来对文本进行切割的函数,使用一句话的结束符。?!来切割段落。

在正则表达式中,前面的^[{punctuation_marks}]*用于匹配不包含结束符的正常文本,后面的[{punctuation_marks}]用于匹配结束符,把两者合起来就能捕获完整的一句话。

# 提取标点符号为一个字符串变量
punctuation_marks = '。?!.?!'

# 使用变量构建正则表达式模式,如同普通字段串前面加`f`一样,允许在字符串中嵌入{}包裹的表达式。
pattern = rf'([^{punctuation_marks}]*[{punctuation_marks}])'

# 预编译正则表达式,大量使用时可以提高性能
regex = re.compile(pattern, re.UNICODE)

def split_text_with_punct(text):
    # 使用finditer查找所有匹配项
    matches = list(regex.finditer(text))
    # 提取匹配到的段落
    paragraphs = [match.group(0).strip() for match in matches if match.group(0).strip()]
    # 找到最后一个匹配的结束位置
    last_match_end = matches[-1].end() if paragraphs else 0
    # 如果最后一个匹配的结束位置不是文本的结尾,即段落未以结束符结束,则将剩余文本作为单独一段
    if last_match_end < len(text):
        remaining_text = text[last_match_end:].strip()
        if remaining_text:
            paragraphs.append(remaining_text)
    
    return paragraphs

定义切割长段落的函数,支持指定最小长度和最大长度来切割。其中,最小长度用于过滤太短的无效文本,最大长度用于切割长段落内容。

def split_long_paragraph(text, min_length=0, max_length=150):
    # 先分割文本
    paragraphs = split_text_with_punct(text)
    
    # 合并分割段落以满足长度要求
    result = []
    current_paragraph = ''
    for p in paragraphs:
        if len(current_paragraph) + len(p) <= max_length:  
            current_paragraph += p  
        else:
            # 如果当前段落过长,则将其添加到结果中,并开始新段落
            if len(current_paragraph) >= min_length:
                result.append(current_paragraph)
            current_paragraph = p
    
    # 添加最后一个段落
    if len(current_paragraph) >= min_length:
        result.append(current_paragraph)
    
    return result

split_long_paragraph(content, 50, 100)
    ['好的,各位投资人晚上好,我是天丰农业的xxx。那今天呢是我们这个8:30的这个巡礼的第四场,然后给各位领导汇报一下种植板块,特别是种子的这个整体的框架和机会。',
     '那我们看到这个其实种子的话也就是一个我们觉得呢就是它本身有一定的弱周期性,就是比如说这个粮价上涨的话,呢这个对种子的需求会有一定的拉动的作用,然后包括像行业的这个供给,这种数量的话呢对种子的供给呢会起到一些影影响的作用。',
     '所以我们看今年其实重点可能放在粮价这块,这个如果说粮食价格总体比较坚挺的话,呢对。这个年底开始的整个种子的这个销售情况,我们也可以更乐观一些。',
     '另外呢就是行业具备成长性,就是种子其实它是生物行业就是或者说叫生物育种这个行业,它本身技术变革其实还是比较这个比如说增长这个幅度还是很大的。',
     '我们看这个20世纪主要的技术呢是杂交技术,杂交技术也培育出了非常优质的品种,包括中国的杂交水稻在全球都处于全球领先的水平。',
     '然后包括像这个也有一些优秀的育种家,像杨文平老先生,李东李邓老先生分别是分别是优秀的这个水稻的育种家和玉米的育种家。那这个杂交技术的话,中国其实也是相对比较熟练的掌握了。',
     '那下一个大的技术变革呢就是转基因技术或者叫生物育种。']

上面是针对单个段落的切割,而实际场景中我们需要处理的是一个文件中的所有段落,而下面的paragraphs_to_dataset则用于将一批段落切割后以json对话集的格式返回。

def paragraphs_to_dialogs(paragraphs):
    dataset = []
    for text in paragraphs:
        _, speaker, content = parse_paragraph(text)
        if content == "":
            continue
        small_paragraphs = split_long_paragraph(content, min_length=5, max_length=100)

        dialogs = [{'speaker': speaker, 'content':child_text, 'is_fraud': False} for child_text in small_paragraphs]
        dataset.extend(dialogs)
    return dataset

paragraphs_to_dialogs(contents)
    [{'speaker': '发言人1',
      'content': '大家好,欢迎参加农业深度报告巡礼,种子板块深度行业研究框架培训,目前所有的参会者均处于静音状态,在演讲结束后将给大家留有提问时间,现在开始播报声明,声明播报完毕后,有请主持人开始发言,谢谢。',
      'is_fraud': False},
	……
     {'speaker': '发言人3',
      'content': '那线上的投资人如果没有这个问题的话,呢我们今天的汇报呢就到此结束。',
      'is_fraud': False},
     {'speaker': '发言人3',
      'content': '那今天天风农业的这个行礼,关于种子板块这边的这个汇报就到此结束。我们明后天,然后包括接下来也有一系列的就是农业相关的行业的深度和公司的深度,继续给各位领导汇报。',
      'is_fraud': False},
     {'speaker': '发言人1', 'content': '感谢大家参加今天的会议,祝大家生活愉快,再见。', 'is_fraud': False}]

4. 主流程

写文件操作复用前文定义的几个工具函数。

def build_to_rows_func(case_prefix=""):
    def to_rows(dialog: list):
        rows = []
        for item in dialog:
            data = {'case': case_prefix}
            for k in ['speaker', 'content', 'is_fraud']:
                data[k] = item[k]
            rows.append(data)
            
        return rows

    return to_rows

def build_write_func(file_name):
    def write(rows, header=False):
        df_new = pd.DataFrame(rows, index=range(len(rows)))
        df_new.to_csv(file_name, mode='a', header=header, index=False)
    return write

generate_dialogs用于为一个文件生成对话集,此处不再需要依赖GPT,直接将切割好的小段落按照指定的格式批量写入目标文件即可。

def generate_dialogs(file_path, output_path):
    texts = load_file(file_path, filter_interjections)
    dataset = paragraphs_to_dialogs(texts)
   
    write_fn = build_write_func(output_path)
    to_rows_fn = build_to_rows_func(filename(file_path))
    write_fn(to_rows_fn(dataset))
    
    print(f"{filename(file_path)}:write dialog count: {len(dataset)}")

反向数据集就不再区分那么多文件,处理后的对话集全部写入一个文件,在开始主循环之前先写入csv的列头。

# 目标文件
output_path = f"../dataset/fraud/csv_dialogs/meeting_label_false_test.csv"

# 写header
header_df = pd.DataFrame(columns = ['case', 'speaker', 'content', 'is_fraud'])
header_df.to_csv(output_path, mode='a', header=True, index=False)

# 主循环
for fname in file_names[-1:]:
    file_path = os.path.join(asr_dir, fname)
    if not os.path.exists(file_path):
        print(f"file_path: {file_path} not exists")
        continue
    print(f"generate for file_path: {file_path} ...")
    generate_dialogs(file_path, output_path)

    generate for file_path: /Users/a200007/Downloads/summary/docs/农业深度报告巡礼.stt ...
    农业深度报告巡礼:write dialog count: 128

反向数据全部是本地处理,生成速度很快,结果预览如下:

pd.read_csv(output_path).head()
casespeakercontentis_fraud
0农业深度报告巡礼发言人1大家好,欢迎参加农业深度报告巡礼,种子板块深度行业研究框架培训,目前所有的参会者均处于静音状...False
1农业深度报告巡礼发言人2本次会议为天风证券研究所闭门会议,仅限受邀嘉宾参会,未经天风证券研究所和演讲嘉宾书面许可。False
2农业深度报告巡礼发言人2机构和个人不得以任何形式将会议内容和相关信息对外公公布、转发、转载、传播、复制、编辑、修改等...False
3农业深度报告巡礼发言人3投资人请问可以听到会议助理,请问可以听到吗?False
4农业深度报告巡礼发言人1可以的,李老师你继续发言。False

小结:本文主要以会议的ASR文本为基础,通过格式解析、噪音去除、长发言切割等方法来构造出不带欺诈内容的对话数据集。

相关阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉下心来学鲁班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值