ppt-web / tests /test_outline_normalization_service.py
26fwyzpz6f-max
Clean deploy without binary files
6aecb2e
Raw
History Blame Contribute Delete
7.89 kB
import ast
import logging
import time
from pathlib import Path
from types import SimpleNamespace
import pytest
from landppt.services.outline.project_outline_normalization_service import (
ProjectOutlineNormalizationService,
)
ROOT = Path(__file__).resolve().parents[1]
SAMPLE_OUTLINE = '''```json
{
"title": "少样本过拟合:挑战与应对策略",
"slides": [
{
"page_number": 1,
"title": "少样本过拟合",
"content_points": [
"技术团队内部培训",
"主讲人:[姓名]",
"日期:2026-04-05"
],
"slide_type": "title"
},
{
"page_number": 2,
"title": "目录",
"content_points": [
"过拟合问题定义与背景",
" "少样本"场景的特殊性",
"过拟合的根源分析",
"核心缓解策略与实践",
"案例分析与代码实现",
"总结与讨论"
],
"slide_type": "agenda"
},
{
"page_number": 3,
"title": "过拟合问题回顾",
"content_points": [
"定义:模型在训练集表现优异,但在测试集/新数据上泛化能力差",
"本质:模型学习了数据中的噪声或特定样本特征,而非普遍规律",
"表现:训练Loss持续下降,验证Loss先降后升(U型曲线)",
"传统解决思路:增加数据量、正则化、Dropout、Early Stopping"
],
"slide_type": "content"
},
{
"page_number": 4,
"title": "少样本学习 (FSL) 的困境",
"content_points": [
"场景:类别样本极少(如每类仅1-5个样本),常见于医疗、工业缺陷检测",
"矛盾:数据量不足以支撑复杂模型训练,极易陷入过拟合陷阱",
"挑战:模型参数量远大于样本数量,缺乏统计显著性",
"影响:微调阶段稍微不慎,模型性能即断崖式下跌"
],
"slide_type": "content"
},
{
"page_number": 5,
"title": "少样本过拟合的根源分析",
"content_points": [
"样本多样性不足:有限样本无法覆盖类内方差",
"模型复杂度过高:参数空间大,模型倾向于“记忆”而非“理解”",
"特征提取器偏差:基类训练得到的特征可能不适应新类",
"任务定义偏差:Support Set与Query Set分布不一致"
],
"slide_type": "content"
},
{
"page_number": 6,
"title": "策略一:数据增强与合成",
"content_points": [
"传统增强:旋转、裁剪、颜色变换(效果有限)",
"高级增强:Mixup、CutMix、CutOut(增加决策边界的平滑性)",
"生成式方法:利用GAN或Diffusion模型生成逼真伪样本扩充Support Set",
"特征空间增强:在特征层添加噪声或进行插值"
],
"slide_type": "content"
},
{
"page_number": 7,
"title": "策略二:模型架构与度量学习",
"content_points": [
"参数高效微调 (PEFT):冻结Backbone,仅训练分类头或使用Adapter/LoRA",
"度量学习:孪生网络、原型网络,将分类问题转化为距离度量问题",
"图神经网络 (GNN):利用样本间关系构建图结构进行传播",
"注意力机制:引入自注意力机制增强特征表达"
],
"slide_type": "content"
},
{
"page_number": 8,
"title": "策略三:正则化与优化技巧",
"content_points": [
"强正则化:大幅提高Weight Decay系数,限制权重幅度",
"Transductive Inference:利用未标注的Query Set信息辅助推理",
"Meta-Regularization:在元学习阶段引入正则项,学习如何避免过拟合",
"减少训练轮次:少样本场景下往往只需极少的Epoch即可收敛"
],
"slide_type": "content"
},
{
"page_number": 9,
"title": "技术实践:代码级建议",
"content_points": [
"冻结BatchNorm层:防止少量样本导致的统计量偏移",
"使用更大的Batch Size(配合Gradient Accumulation)",
"学习率调整:采用更小的学习率或Cosine Annealing",
"交叉验证:使用Leave-One-Out策略最大化验证可靠性"
],
"slide_type": "content"
},
{
"page_number": 10,
"title": "总结与下一步行动",
"content_points": [
"核心观点:少样本过拟合是数据稀缺与模型复杂度的博弈",
"关键策略:数据增强 + 度量学习/PEFT + 强正则化",
"建议:优先尝试冻结Backbone和原型网络,再考虑复杂微调",
"后续计划:团队内部复现基准测试,建立少样本任务开发规范"
],
"slide_type": "conclusion"
},
{
"page_number": 11,
"title": "Q&A",
"content_points": [
"感谢聆听",
"欢迎提问与讨论"
],
"slide_type": "thankyou"
}
]
}
```'''
def _load_class_method(relative_path: str, class_name: str, method_name: str):
tree = ast.parse((ROOT / relative_path).read_text(encoding="utf-8"))
class_node = next(
node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == class_name
)
method_node = next(
node
for node in class_node.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == method_name
)
module = ast.Module(body=[method_node], type_ignores=[])
ast.fix_missing_locations(module)
namespace = {
"time": time,
"logger": logging.getLogger("test-outline-normalization"),
}
exec(compile(module, relative_path, "exec"), namespace)
return namespace[method_name]
def test_parse_outline_content_supports_fenced_json_with_inner_quotes():
service = ProjectOutlineNormalizationService(SimpleNamespace())
project = SimpleNamespace(topic="少样本过拟合:挑战与应对策略")
outline = service._parse_outline_content(SAMPLE_OUTLINE, project)
assert outline["title"] == "少样本过拟合:挑战与应对策略"
assert len(outline["slides"]) == 11
assert outline["slides"][1]["slide_type"] == "agenda"
assert outline["slides"][1]["content_points"][1] == '"少样本"场景的特殊性'
assert outline["slides"][-1]["slide_type"] == "thankyou"
def test_parse_outline_content_no_longer_fabricates_default_slides():
service = ProjectOutlineNormalizationService(SimpleNamespace())
project = SimpleNamespace(topic="测试主题")
with pytest.raises(ValueError):
service._parse_outline_content("这是一段没有结构的大纲文本", project)
@pytest.mark.asyncio
async def test_update_project_outline_uses_normalized_parser():
update_project_outline = _load_class_method(
"src/landppt/services/project_workflow_stage_service.py",
"ProjectWorkflowStageService",
"update_project_outline",
)
project = SimpleNamespace(
topic="少样本过拟合:挑战与应对策略",
outline={},
todo_board=None,
)
class _FakeProjectManager:
async def get_project(self, project_id):
assert project_id == "project-1"
return project
normalizer = ProjectOutlineNormalizationService(SimpleNamespace())
service = SimpleNamespace(
project_manager=_FakeProjectManager(),
_parse_outline_content=normalizer._parse_outline_content,
)
success = await update_project_outline(service, "project-1", SAMPLE_OUTLINE)
assert success is True
assert project.outline["title"] == "少样本过拟合:挑战与应对策略"
assert len(project.outline["slides"]) == 11
assert project.outline["slides"][1]["slide_type"] == "agenda"