| 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" |
|
|