chore: 标准化NLTK数据卷并清理测试文件
- 将docker-compose.yml中的NLTK数据挂载从主机路径改为命名卷,确保跨平台一致性 - 移除测试中生成临时文件的副作用,改为控制台输出切分结果 - 修复语义切分工具中NLTK资源检查逻辑,避免重复下载并提供明确错误提示 - 优化语义切分函数签名,支持embed_fn为None时的退化处理
This commit is contained in:
parent
32fef0893c
commit
2b48190b25
@ -85,7 +85,7 @@ def extract_table_block(tokens: list[Any], i: int, original_lines: list[str]) ->
|
||||
|
||||
|
||||
def split_text_by_length_and_newline(
|
||||
text: str, max_length: int, embed_fn: Callable[[list[str]], Any], token_count_fn: Callable[[str], int]
|
||||
text: str, max_length: int, embed_fn: Callable[[list[str]], Any] | None, token_count_fn: Callable[[str], int]
|
||||
) -> list[str]:
|
||||
"""
|
||||
层次化文本切分策略。
|
||||
|
||||
@ -9,11 +9,23 @@ from nltk.tokenize import sent_tokenize
|
||||
from sklearn.cluster import AgglomerativeClustering
|
||||
from sklearn.metrics import silhouette_score
|
||||
|
||||
# 模块加载的时候先检查是否已经下载了punkt_tab模型(用于识别句子的边界)
|
||||
try:
|
||||
nltk.data.find("tokenizers/punkt_tab")
|
||||
except LookupError:
|
||||
nltk.download("punkt_tab")
|
||||
_punkt_checked = False
|
||||
|
||||
|
||||
def _ensure_punkt_tab() -> None:
|
||||
"""首次使用分句能力时检查 NLTK punkt_tab 资源,缺失时给出可操作错误。"""
|
||||
global _punkt_checked
|
||||
if _punkt_checked:
|
||||
return
|
||||
|
||||
try:
|
||||
nltk.data.find("tokenizers/punkt_tab")
|
||||
except LookupError as e:
|
||||
raise RuntimeError(
|
||||
"缺少 NLTK 资源 punkt_tab。请先执行: python -m nltk.downloader punkt_tab"
|
||||
) from e
|
||||
|
||||
_punkt_checked = True
|
||||
|
||||
|
||||
def split_sentences_chinese(text: str) -> list[str]:
|
||||
@ -25,7 +37,7 @@ def split_sentences_chinese(text: str) -> list[str]:
|
||||
- 使用正向/反向预查处理引号:确保如果标点后面紧跟引号(”’"),该引号会被保留在当前句子末尾,而不是被切分到下一句。
|
||||
- 返回去除两端空格且非空的句子列表。
|
||||
"""
|
||||
pattern = r'(?<=[。!?])(?![”’"])|(?<=[。!?])(?![”’"])|(?<=[。!?][”’"])'
|
||||
pattern = r'(?<=[。!?][”’"])|(?<=[。!?])(?![”’"])'
|
||||
sentences = re.split(pattern, text)
|
||||
return [s.strip() for s in sentences if s.strip()]
|
||||
|
||||
@ -50,6 +62,8 @@ def split_mixed_sentences(text: str) -> list[str]:
|
||||
Returns:
|
||||
List[str]: 分割后的句子列表。
|
||||
"""
|
||||
_ensure_punkt_tab()
|
||||
|
||||
chunks = re.split(r"(\n+)", text)
|
||||
sentences = []
|
||||
|
||||
@ -108,7 +122,7 @@ def find_best_num_clusters(embeddings: Any, min_clusters: int = 2, max_clusters:
|
||||
|
||||
|
||||
def semantic_chunking_with_auto_clusters(
|
||||
text: str, embed_fn: Callable[[list[str]], Any], token_count_fn: Callable[[str], int], max_chunk_size: int = 512
|
||||
text: str, embed_fn: Callable[[list[str]], Any] | None, token_count_fn: Callable[[str], int], max_chunk_size: int = 512
|
||||
) -> list[str]:
|
||||
"""
|
||||
对传入的文本进行语义切分,过程中会自动选择最佳的聚集数量。
|
||||
@ -117,26 +131,43 @@ def semantic_chunking_with_auto_clusters(
|
||||
- 先将文本中的句子按语言进行分发,英文/混合文本使用NLTK的sent_tokenize,中文文本使用split_sentences_chinese。
|
||||
- 对每个句子进行嵌入向量化。
|
||||
- 确定最佳的聚类数量(根据轮廓系数)。
|
||||
- 对句子进行聚类,将每个聚类中的句子连接起来,形成一个分块。
|
||||
|
||||
- 对句子进行聚类后,按原文顺序遍历:当聚类标签变化或达到长度上限时切分,形成连续分块。
|
||||
- 如果嵌入模型缺失,则退化为原始切分方式
|
||||
"""
|
||||
sentences = split_mixed_sentences(text)
|
||||
if len(sentences) < 2:
|
||||
return [text.strip()]
|
||||
|
||||
# 向量化每个句子, 得到他们的嵌入向量
|
||||
embeddings = embed_fn(sentences)
|
||||
|
||||
# 计算每个句子的token数量
|
||||
sentence_token_counts = [token_count_fn(s) for s in sentences]
|
||||
total_tokens = sum(sentence_token_counts)
|
||||
|
||||
# 决定合适的聚集数量,需要保证每个分块的token数量都不超过max_chunk_size
|
||||
best_k = max(total_tokens // max_chunk_size, 1) + 1
|
||||
# 如果没有提供向量化函数,或者整体未超长,则直接进行简单合并/返回
|
||||
if embed_fn is None or total_tokens <= max_chunk_size:
|
||||
chunks = []
|
||||
current_chunk = ""
|
||||
current_chunk_tokens = 0
|
||||
for s, cnt in zip(sentences, sentence_token_counts):
|
||||
if current_chunk_tokens + cnt > max_chunk_size and current_chunk:
|
||||
chunks.append(current_chunk.strip())
|
||||
current_chunk = s
|
||||
current_chunk_tokens = cnt
|
||||
else:
|
||||
current_chunk += s
|
||||
current_chunk_tokens += cnt
|
||||
if current_chunk:
|
||||
chunks.append(current_chunk.strip())
|
||||
return chunks
|
||||
|
||||
# 向量化每个句子, 得到他们的嵌入向量
|
||||
embeddings = embed_fn(sentences)
|
||||
|
||||
# 决定合适的聚集数量:超长时按上限向上取整,避免整除时多切一块
|
||||
best_k = (total_tokens + max_chunk_size - 1) // max_chunk_size
|
||||
best_k = min(best_k, len(sentences))
|
||||
|
||||
# 根据指定的聚集数量、相似度判断方式、联动方式,对句子进行聚类
|
||||
# 这里返回的labels是一个每个句子的聚类标签列表,例如[0,0,1,2,2],相同ID的句子被聚类到同一个分块中
|
||||
# labels 是每个句子的聚类标签列表(如 [0,0,1,2,2]),后续会按原文顺序在标签变化处切分连续分块
|
||||
labels = AgglomerativeClustering(n_clusters=best_k, metric="cosine", linkage="average").fit_predict(embeddings)
|
||||
|
||||
chunks = []
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
||||
[
|
||||
"## 风力发电机组 塔架设计规范|1 概述\n\n本标准规定了风力发电机组塔架的设计、制造、运输和安装要求。",
|
||||
"### 风力发电机组 塔架设计规范|6 钢制塔架|6.1 一般要求\n\n钢制塔架的设计应考虑极端载荷和疲劳载荷。在计算连接强度时,需要用到螺纹截面积 Asp 以及承载力设计值 Rd。\n此外,结构应力 σ _ {y, d} 的计算必须符合相关标准要求。",
|
||||
"### 风力发电机组 塔架设计规范|6 钢制塔架|6.2 疲劳极限状态\n\n疲劳计算应基于 Miner 线性累积损伤理论。",
|
||||
"### 风力发电机组 塔架设计规范|7 混凝土塔架|7.1 材料特性\n\n混凝土强度等级不应低于 C50。",
|
||||
"### 风力发电机组 塔架设计规范|7 混凝土塔架|7.2 施工工艺\n\n混凝土塔架可采用现浇或预制拼装方式。对于预制片段,应严格控制拼装精度。"
|
||||
]
|
||||
@ -1,7 +1,5 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
import os
|
||||
import json
|
||||
|
||||
# 现在可以安全地导入了,因为顶层不再有重型依赖
|
||||
from yuxi.knowledge.chunking.ragflow_like.parsers import semantic
|
||||
@ -244,7 +242,7 @@ def test_semantic_chunking_basic(embed_fn, sample_markdown):
|
||||
|
||||
# 配置切分参数
|
||||
parser_config = {
|
||||
"chunk_token_num": 800, # 针对标准文档调整 token 数
|
||||
"chunk_token_num": 1000, # 针对标准文档调整 token 数
|
||||
"overlapped_percent": 0.1
|
||||
}
|
||||
|
||||
@ -304,14 +302,11 @@ def test_semantic_chunking_basic(embed_fn, sample_markdown):
|
||||
print(f"\n[测试成功] 文档成功切分为 {len(chunks)} 个片段")
|
||||
print(f"识别到的关键技术词汇: {found_keywords}")
|
||||
|
||||
# 将切分后的内容写入到 resource 目录
|
||||
output_dir = os.path.join(os.path.dirname(__file__), "resource")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
output_path = os.path.join(output_dir, "semantic_chunks.json")
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(chunks, f, ensure_ascii=False, indent=2)
|
||||
print(f"切分内容已保存至: {output_path}")
|
||||
# 直接输出到控制台预览切分结果,避免文件副作用
|
||||
print("\n--- 语义切分结果开始 ---")
|
||||
for idx, chunk in enumerate(chunks, 1):
|
||||
print(f"\n[Chunk {idx}]\n{chunk}")
|
||||
print("\n--- 语义切分结果结束 ---")
|
||||
|
||||
def test_heading_inference():
|
||||
"""测试标题层级推断工具类"""
|
||||
|
||||
@ -7,8 +7,10 @@ sys.path.append(os.getcwd())
|
||||
|
||||
from yuxi.knowledge.chunking.ragflow_like.dispatcher import chunk_markdown
|
||||
from yuxi.knowledge.chunking.ragflow_like.nlp import bullets_category, count_tokens
|
||||
from yuxi.knowledge.chunking.ragflow_like.utils.semantic_utils import split_sentences_chinese
|
||||
from yuxi.knowledge.chunking.ragflow_like.presets import (
|
||||
CHUNK_ENGINE_VERSION,
|
||||
CHUNK_PRESET_IDS,
|
||||
get_chunk_preset_options,
|
||||
map_to_internal_parser_id,
|
||||
resolve_chunk_processing_params,
|
||||
@ -85,6 +87,13 @@ def test_book_chunking_hierarchical_merge() -> None:
|
||||
assert any("第一章" in ck["content"] for ck in chunks)
|
||||
|
||||
|
||||
def test_split_sentences_chinese_should_keep_quote_boundary() -> None:
|
||||
text = '他说:“你好。”然后问:“你在吗?”最后结束!'
|
||||
sentences = split_sentences_chinese(text)
|
||||
|
||||
assert sentences == ["他说:“你好。”", "然后问:“你在吗?”", "最后结束!"]
|
||||
|
||||
|
||||
def test_markdown_heading_has_higher_weight_in_bullet_category() -> None:
|
||||
sections = [
|
||||
"# 3.2 个人所得项目及计税、申报方式概括",
|
||||
@ -108,7 +117,7 @@ def test_mid_sentence_bullet_marker_should_not_be_treated_as_heading() -> None:
|
||||
|
||||
def test_chunk_preset_options_include_description() -> None:
|
||||
options = get_chunk_preset_options()
|
||||
assert len(options) == 4
|
||||
assert {option["value"] for option in options} == CHUNK_PRESET_IDS
|
||||
assert all(isinstance(option.get("description"), str) and option["description"] for option in options)
|
||||
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ services:
|
||||
- ./.env:/app/.env
|
||||
- ${MODEL_DIR:-./models}:/models # 使用默认值处理未定义的环境变量
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ~/nltk_data:/root/nltk_data # 持久化语义切分使用的NLTK数据包,避免重复下载
|
||||
- nltk_data:/root/nltk_data # 使用命名卷持久化NLTK数据,避免路径展开差异
|
||||
ports:
|
||||
- "5050:5050"
|
||||
networks:
|
||||
@ -431,6 +431,9 @@ services:
|
||||
- app-network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
nltk_data:
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
Loading…
Reference in New Issue
Block a user