NLP中的样本不平衡、长句问题

探讨关系抽取任务中遇到的样本类别不平衡与句子长度差距大的问题,介绍数据增强、深度预训练及模型层面改进策略,包括EDA_NLP工具包、BERT预训练、FocalLoss损失函数的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

遇到的问题

这次关系抽取任务,在数据方面,遇到了两个问题。

  • 样本类别不平衡
  • 样本句子长度差距过大

关系最多实体对有4W多个,最少的只有十几条。
句子长度从短句(个位数长度)到300不等。

从验证集分析后,模型对于训练样本中关系数目少以及长句均不是有很好地表现。

从数据的角度进行提升自然想到了数据增强。

1.数据层面的思路

1.1数据增强

在机器学习中,对于结构化数据的样本不平衡问题,常见的做法有过采样以及欠采样。
数据不平衡的处理(文本)

但对于文本数据,embedding之后的向量的插值是否意义?或者对于一个句子,我用个编码器将其变为sentence vertor,然后进行采样?这样做法十分少见。

经过调研,对于文本数据,数据增强两个思路。

  • 增、删、换、替【随机增加、删除词、词序交换、同义词替换】
  • 生成【回译、生成模型】

对于第一个,参考这个论文对应的工具包EDA_NLP

对于生成,回译通常是调用现有翻译工具的API来进行,生成模型也有相关论文,个人想到就是生成对抗网络,编码解码的方式来重构文本。

但是,如果要保留文本中特定的信息,比如实体、关系,用生成模型就会很不稳定;回译没有测试过,但应该也会有噪声。

因此,最终对于不平衡的样本就采用了EDA的方式,在原来工具包基础上进行了修改,主要是固定实体的位置,保证其不被替换,删除。

1.2深度预训练

结合语料的预训练模型

参考:邱锡鹏老师团队的文章:How to Fine-Tune BERT for Text Classification?里面有提到如何用Bert融合语料进行深入预训练。项目仓库

其实就是在bert的基础上,继续用其预训练的办法,mask LM与next sentence prediction。主要看你的语料适合哪一种。

但其实,如果是中文的语料,ERNIE的效果可能会更好。BERT和ERNIE谁更强?

2.模型层面的思路

2.1损失函数的设计

样本不平衡会导致什么问题?答案很显然,如果1:99的样本,全部预测为0,(负样本)也会有99%的正确率。

从损失函数的角度,就是误分类的代价太小了,全部预测为负样本,意味着我只分错一个样本,而其他都分对了,这是因为对于错分与正确分类,我是平等的看待。
做法:

  • 根据样本量加权。这个在深度学习框架中有自带的包,加权交叉熵。
  • 提高误分类的代价。在CV领域一个比较好的做法:Focal Loss.
    可以看这两篇文章:理解focal lossFocal loss的思考剖析focal loss
  • 个人思考:考虑到这个数据的特点,在长句表现不好,长句也是另一种类别不平衡。是否可以对于句子长度进行惩罚,提高长句的误分类代价。

Focal Loss的效果
试着简单写了多标签分类的例子,直观体验下focal loss。

alpha = 0.5
gamma = 2
labels = np.array([1,0,1,0])
logits = np.array([0.7,0.25,0.1,0.8])
log_logits = np.log(logits)
log_logits_neg = np.log(1-logits)
pos = labels * (labels - logits )
neg = (1 - labels) * logits
fl_loss = 0. - alpha * (pos ** gamma) * log_logits - (1 - alpha) * (neg ** gamma) * log_logits_neg
---------------
y_true = tf.cast(tf.convert_to_tensor(labels),dtype = tf.float32)
y_pred = tf.cast(tf.convert_to_tensor(logits),dtype = tf.float32)
tf.nn.sigmoid_cross_entropy_with_logits(y_true,y_pred)

fl loss = [0.01605037, 0.00899006, 0.93254696, 0.51502013]
ce loss = [0.40318602, 0.8259394 , 0.64439666, 1.1711006 ]

结果十分直观,可以看出,对focal loss而言,误分类与正确分类的损失不是一个数量级,相差较多。而交叉熵的Loss则没有那么明显。

这边有两个超参数 γ 与 α \gamma与\alpha γα

  • γ \gamma γ的作用就是加大力度,无论是正类负类, γ \gamma γ越大,分正确与分错误的差距就会越大。
  • α \alpha α的作用在于针对不平衡的问题,如果 α \alpha α=0.25,那么对于负类错分的损失就会比正类错分的损失更大。这就一定程度减小了,全部预测为负类的情况。
    这其实是创作者引入了α-balanced loss后,最终形成focal loss。

有用的链接

在文本分类任务中,有哪些论文中很少提及却对性能有重要影响的tricks?
炼丹笔记三:数据增强
CV层面解读focal loss

<think>嗯,用户提出了关于RecursiveCharacterTextSplitter在处理中文文本时字符长度计算准确的问题。从引用的资料来看,这个分割器默认使用len()函数计算长度,但len()在处理中文字符时每个汉字算一个长度,而GPT模型实际是UTF-8字节编码,这会导致计算差异。用户明显是个技术开发者,正在处理中文NLP任务。ta的疑问很具体,说明已经实际操作过并遇到了问题。可能正在构建某种中文文档处理系统,需要精确控制token数量。深层需求应该是要确保文本分割结果与LLM的tokenizer完全匹配。之前的三条引用都很有价值:[1]展示基础用法,[2]提到根据内容类型调整分隔符,[3]直接对比了两种分割器实现。但都没解决中文字符的编码问题。关键点在于:-Python的len()统计Unicode字符数而非字节数-GPTtokenizer实际使用字节对编码(BPE)-中文每个汉字约占2-3个UTF-8字节最好的解决方案确实是自定义length_function。考虑到用户熟悉LangChain,可以直接推荐tiktoken库的方案。但需注意安装复杂度,所以给出替代方案也很重要——手动换算字符数虽然粗糙但见效快。另外观察到引用[3]中提到add_start_index选项,这个在调试时特别有用,应该要强调。用户可能需要定位错误分片位置,保留原始索引能大大降低调试难度。测试建议部分,用户可能需要更明确的验证方法。加上推荐打印前三个分片+最后分片,因为开头和结尾最容易出错。用emoji做分隔符纯属中文特色需求,用户可能处理社交媒体数据。</think> 针对 `RecursiveCharacterTextSplitter` 处理中文时字符长度计算匹配的问题,以下是关键原因和解决方案: --- ### **问题原因** 1. **默认长度函数匹配** - `RecursiveCharacterTextSplitter` 默认使用 `len` 函数统计字符数(每个中文/英文字符均计为1) - **但LLM的tokenizer(如GPT)对中文分词时,1个中文字符可能被拆分为多个token** *例:通过 `tiktoken` 统计 `"你好"` 时:* ```python # 实际token数可能为2(如:['你', '好']) len(encoder.encode("你好")) # 返回2,而非 len("你好") 返回的2 ``` 2. **递归切分的逻辑依赖** 算法优先按分隔符(如`\n\n`、`\n`)拆分,若未达到`chunk_size`,则继续合并小片段,**最终合并时若未用LLM的tokenizer统计,会导致长度超限**。 --- ### **解决方案** #### 方法1:**自定义 `length_function`**(推荐) ```python from langchain_text_splitters import RecursiveCharacterTextSplitter import tiktoken # 使用目标模型的tokenizer encoder = tiktoken.encoding_for_model("gpt-4") # 定义统计token数量的函数 def tiktoken_len(text: str) -> int: return len(encoder.encode(text)) # 将token统计函数传入分割器 text_splitter = RecursiveCharacterTextSplitter( chunk_size=100, # 单位:token数量 chunk_overlap=20, # 单位:token数量 length_function=tiktoken_len, # 关键:使用token统计函数 separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] # 优化中文分隔符 ) ``` #### 方法2:**使用 `TokenTextSplitter` 替代** ```python from langchain_text_splitters import TokenTextSplitter text_splitter = TokenTextSplitter( encoding_name="cl100k_base", # GPT-4使用的编码 chunk_size=100, chunk_overlap=20 ) ``` #### 方法3:**字符长度换算(应急方案)** ```python # 按经验比例换算:中文字符数 ≈ 目标token数 * 0.4 adjusted_chunk_size = int(100 * 0.4) # 例如100 token ≈ 40字符 text_splitter = RecursiveCharacterTextSplitter( chunk_size=adjusted_chunk_size, chunk_overlap=int(20 * 0.4), separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] ) ``` --- ### **关键调整点** 1. **优化分隔符顺序** ```python separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] # 优先按段落/句子切分 ``` 2. **测试实际token数** ```python text = "这是一段中文示例文本..." chunks = text_splitter.split_text(text) print(encoder.encode(chunks[0])) # 检查首段的token列表 ``` 3. **验证重叠机制** 检查相邻分片是否有20个token的重叠内容,避免信息断层。 --- ### **为何此方案有效?** - **直接对齐LLM的token计算**:使用 `tiktoken` 精确统计token数量,消除字符与token的差异[^1][^2] - **保留语义完整性**:通过中文标点分隔符(`。!?`)优先切分句子,避免截断词语[^3] - **动态递归合并**:小片段合并时会重新计算token长度,确保最终分片符合约束[^1] > **测试建议**:对典型中文样本(含长句/专业术语)运行分片,并用 `tiktoken` 验证每个 `chunk` 的token数是否符合 `chunk_size`。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值