structverify 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structverify-0.3.0/structverify.egg-info → structverify-0.3.2}/PKG-INFO +1 -1
- {structverify-0.3.0 → structverify-0.3.2}/pyproject.toml +1 -1
- {structverify-0.3.0 → structverify-0.3.2}/structverify/__init__.py +1 -1
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/curator.py +5 -1
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/doctor.py +31 -5
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/loop.py +8 -2
- structverify-0.3.2/structverify/training/monitor.py +183 -0
- structverify-0.3.2/structverify/training/recipe/sample.py +71 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/recipe/train_mlx.py +38 -6
- structverify-0.3.2/structverify/training/recipe/train_qlora.py +209 -0
- {structverify-0.3.0 → structverify-0.3.2/structverify.egg-info}/PKG-INFO +1 -1
- {structverify-0.3.0 → structverify-0.3.2}/structverify.egg-info/SOURCES.txt +2 -0
- structverify-0.3.0/structverify/training/recipe/train_qlora.py +0 -104
- {structverify-0.3.0 → structverify-0.3.2}/LICENSE +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/README.md +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/setup.cfg +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/adaptation/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/adaptation/adapter_trainer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/adaptation/feedback_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/adaptation/kosis_crawler.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/adaptation/sample_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/adaptation/synthetic_generator.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/adaptation/update_embeddings.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/builder_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/conformance_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/dependency_planner.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/indexing_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/indexing_planner.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/integration_example.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/loop.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/memory.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/planner.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/prompts/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/prompts/planner_prompts.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/prompts/reflect_prompts.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/reflect.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/runtime_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/schemas.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/source_profiler.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/base.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/calculate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/catalog_search.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/deep_explore.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/explore_catalog.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/fetch_evidence.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/finish.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/meta_explore.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/query_rewriter.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/read_original.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/tools/replan.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/agent/workspace.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/api.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/config/default.yaml +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/core/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/core/config_loader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/core/pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/core/schemas.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/_llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/candidate/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/candidate/heuristic.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/candidate/llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/candidate_scorer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/claim_detector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/claims/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/claims/worthiness.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/domain/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/domain/classify.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/domain/preview.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/domain/registry.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/domain_classifier.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/prompts/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/prompts/candidate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/prompts/claim_worthiness.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/prompts/domain.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/prompts/schema.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/prompts_loader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/schema/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/schema/expand.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/schema/induce.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/schema/regenerate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/schema/temporal_hints.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/schema/validate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/schema_inductor.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/detection/synthetic_generator.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/_llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/explainer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/fallback.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/formatters.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/prompts/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/prompts/match.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/prompts/mismatch.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/prompts/multihop.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/explanation/prompts/unverifiable.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/graph/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/graph/claim_graph.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/graph/document_graph.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/graph/graph_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/graph/graph_multihop.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/graph/graph_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/graph/provenance.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/agent_memory.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/embedder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/exemplar_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/normalizer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/schema.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/storage/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/storage/jsonl_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/memory/working_memory.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/Dockerfile.scraper +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/extractor.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/fields.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/markdown.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/models.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/ocr.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/reader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/pdf/scoring.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/scraper_sandbox.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/segmenter.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/preprocessing/sir_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/progress.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/base.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/base_connector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/catalog_ranker.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/catalog_search.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/chunking.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/custom_csv_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/custom_db_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/custom_docs_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/dimension_resolver.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/evidence_subgraph.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/kosis_connector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/kosis_relevance.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/kosis_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/query_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/registry.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/relevance_judge.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/retrieval/row_matcher.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/storage/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/storage/db_manager.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/storage/dwh_manager.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/storage/init_db.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/storage/raw_storage.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/dataset.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/evalgate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/generate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/training/tasks.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/utils/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/utils/embedding_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/utils/llm_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/utils/logger.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/adapters.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/conformance.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/decide_verdict.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/decide_verdict_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/growth_diff.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/row_match.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/units.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/verdict_thresholds.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify/verification/verifier.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify.egg-info/dependency_links.txt +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify.egg-info/requires.txt +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/structverify.egg-info/top_level.txt +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_api.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_catalog_search_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_custom_csv_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_candidate_heuristic.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_domain_preview.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_domain_registry.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_expand_temporal.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_init_exports.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_prompts_loader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_validate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_detection_worthiness.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_embedding_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_embedding_config_threading.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_explore_catalog_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_is_table_relevant.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_kosis_crawler_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_kosis_guards.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_kosis_json_load_pgvector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_library_entry_smoke.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_llm_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_runtime_datasources.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_step1-9_v3_pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_step1_6_detection.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_step1_9_pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_step9_explainer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_update_embeddings_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.2}/tests/test_verification_decide.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structverify
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Compliance & fact verification for documents — check your files against your own rulebook (PDF) or a data source, and read a plain True/False.
|
|
5
5
|
Author-email: "김예슬 (Yeseul Kim)" <yesul0718@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "structverify"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Compliance & fact verification for documents — check your files against your own rulebook (PDF) or a data source, and read a plain True/False."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -80,10 +80,14 @@ class DataCurator:
|
|
|
80
80
|
t = str(row.get("task", "?"))
|
|
81
81
|
rep.by_task[t] = rep.by_task.get(t, 0) + 1
|
|
82
82
|
rep.kept = len(clean)
|
|
83
|
-
# 편중 경고
|
|
83
|
+
# 편중 경고 — 절대 과소 + 상대 편중(전체의 5% 미만) 둘 다 잡는다.
|
|
84
84
|
for t, n in rep.by_task.items():
|
|
85
85
|
if n < min_per_task:
|
|
86
86
|
rep.warnings.append(f"태스크 '{t}' 예시 {n}개 — {min_per_task}개 미만(과소). 보강 권장")
|
|
87
|
+
elif len(rep.by_task) > 1 and rep.kept and n / rep.kept < 0.05:
|
|
88
|
+
rep.warnings.append(
|
|
89
|
+
f"태스크 '{t}' {n}건 ({n/rep.kept:.1%}) — 편중. 이 태스크는 학습 효과가 미미할 수 "
|
|
90
|
+
f"있음 (generate_dataset·build_example로 보강하거나 학습 대상에서 제외 검토)")
|
|
87
91
|
if not rep.by_task:
|
|
88
92
|
rep.warnings.append("통과한 예시가 없음 — 포맷 확인 필요")
|
|
89
93
|
return clean, rep
|
|
@@ -72,13 +72,39 @@ class TrainDoctor:
|
|
|
72
72
|
d.issues.append(f"[심각] loss 발산 — 최소 {min_v:.3f} 후 {vals[-1]:.3f}로 상승")
|
|
73
73
|
d.prescriptions.append("learning_rate 낮추기(÷2~5), 최소 지점 체크포인트 사용")
|
|
74
74
|
|
|
75
|
-
# spike: 국소 급등(직전 대비 2배↑)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
# spike: 국소 급등(직전 대비 2배↑) — 복귀 여부로 심각도를 가른다.
|
|
76
|
+
# 복귀한 스파이크(다음 기록이 원 수준으로 돌아옴)는 SGD의 정상 노이즈 → 정보성.
|
|
77
|
+
# 복귀 안 한 스파이크만 배치/데이터 문제로 경고.
|
|
78
|
+
spikes, unrecovered = [], []
|
|
79
|
+
for i in range(1, len(vals)):
|
|
80
|
+
if vals[i] > vals[i - 1] * 2.0 and vals[i - 1] > 1e-6:
|
|
81
|
+
spikes.append(losses[i][0])
|
|
82
|
+
nxt = vals[i + 1:i + 3]
|
|
83
|
+
if not nxt or min(nxt) > vals[i - 1] * 1.5:
|
|
84
|
+
unrecovered.append(losses[i][0])
|
|
85
|
+
if unrecovered:
|
|
79
86
|
d.healthy = False
|
|
80
|
-
d.issues.append(f"[주의] loss spike @ step {
|
|
87
|
+
d.issues.append(f"[주의] 복귀 없는 loss spike @ step {unrecovered[:3]} — 특정 배치·데이터 의심")
|
|
81
88
|
d.prescriptions.append("해당 구간 데이터 재검토(DataCurator), batch shuffle·grad clipping 확인")
|
|
89
|
+
elif spikes:
|
|
90
|
+
d.issues.append(f"[정보] 일시적 spike {len(spikes)}건 @ step {spikes[:3]} — 모두 즉시 복귀 (정상 노이즈)")
|
|
91
|
+
|
|
92
|
+
# 수렴 스텝 추정: 이동평균이 최저의 105% 안에 처음 든 지점 이후로 개선이 없으면,
|
|
93
|
+
# 그 뒤 스텝은 낭비 — "얼마나 일찍 멈출 수 있었나"를 정량화해 처방.
|
|
94
|
+
if len(vals) >= 30:
|
|
95
|
+
w = max(5, len(vals) // 50)
|
|
96
|
+
means = [sum(vals[max(0, i - w):i + 1]) / len(vals[max(0, i - w):i + 1])
|
|
97
|
+
for i in range(len(vals))]
|
|
98
|
+
floor = min(means)
|
|
99
|
+
conv_i = next((i for i, m in enumerate(means) if m <= floor * 1.05), len(vals) - 1)
|
|
100
|
+
conv_step, last_step = losses[conv_i][0], losses[-1][0]
|
|
101
|
+
if last_step and conv_step and last_step > conv_step * 2:
|
|
102
|
+
wasted = (last_step - conv_step) / last_step
|
|
103
|
+
d.issues.append(f"[정보] 실질 수렴 @ step ~{conv_step} — 전체 {last_step}스텝 중 "
|
|
104
|
+
f"{wasted:.0%}는 개선 없이 소모됨")
|
|
105
|
+
d.prescriptions.append(
|
|
106
|
+
f"다음 학습은 실시간 감독의 조기 종료(early_stop=True, 기본값)에 맡기거나 "
|
|
107
|
+
f"steps={int(conv_step * 1.3)} 수준으로 지정 — GPU 시간 {wasted:.0%} 절약")
|
|
82
108
|
|
|
83
109
|
# 정체: 후반 개선 미미
|
|
84
110
|
tail = vals[max(0, len(vals) - 5):]
|
|
@@ -59,11 +59,14 @@ class LearningLoop:
|
|
|
59
59
|
|
|
60
60
|
# ── ② 학습: 위임 (레시피 핸드오프 or subprocess 실행) ──
|
|
61
61
|
def train(self, dataset: str, output: str = "./adapter", *, backend: str = "auto",
|
|
62
|
-
run: bool = False, steps: int = 60
|
|
62
|
+
run: bool = False, steps: int = 60, early_stop: bool = True,
|
|
63
|
+
patience: int = 200) -> dict:
|
|
63
64
|
"""학습을 위임. run=False(기본)면 실행 명령만 반환(핸드오프).
|
|
64
65
|
|
|
65
66
|
backend: "qlora"(NVIDIA/3060·T4) | "mlx"(Apple Silicon) | "auto"(플랫폼 감지).
|
|
66
67
|
run=True는 해당 extra + 하드웨어가 있을 때만 — 별도 프로세스(샌드박스)로 실행.
|
|
68
|
+
early_stop: 🩺 실시간 감독이 plateau/발산을 감지하면 조기 종료 (qlora 백엔드).
|
|
69
|
+
patience: 이 스텝 동안 개선이 없으면 수렴 완료로 판단.
|
|
67
70
|
"""
|
|
68
71
|
import platform
|
|
69
72
|
if backend == "auto":
|
|
@@ -81,7 +84,10 @@ class LearningLoop:
|
|
|
81
84
|
recipe = os.path.join(_dir, "train_qlora.py")
|
|
82
85
|
cmd = ["python", "-m", "structverify.training.recipe.train_qlora",
|
|
83
86
|
"--model", self.base_model, "--data", os.path.abspath(dataset),
|
|
84
|
-
"--out", os.path.abspath(output), "--max-steps", str(steps)
|
|
87
|
+
"--out", os.path.abspath(output), "--max-steps", str(steps),
|
|
88
|
+
"--patience", str(patience)]
|
|
89
|
+
if not early_stop:
|
|
90
|
+
cmd.append("--no-early-stop")
|
|
85
91
|
need = 'pip install "structverify[training]" (NVIDIA GPU)'
|
|
86
92
|
_probe = "torch"
|
|
87
93
|
info = {"backend": backend, "recipe": recipe, "command": " ".join(cmd), "output": output}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""structverify.training.monitor — 학습 실시간 감독 (🩺 TrainDoctor live).
|
|
2
|
+
|
|
3
|
+
레시피(HF Trainer 콜백)가 매 로그 스텝마다 :class:`LossMonitor` 에 loss를 먹이면,
|
|
4
|
+
모니터가 ① 상태 판정(정상/스파이크/발산/plateau)과 ② 터미널 한 줄 UI 문자열을 돌려준다.
|
|
5
|
+
순수 파이썬(토치 불필요)이라 코어에서 단위테스트 가능.
|
|
6
|
+
|
|
7
|
+
터미널 UI 한 줄 예::
|
|
8
|
+
|
|
9
|
+
🧠 1234/2496 ▓▓▓▓▓▓░░░░░░ 49% · loss 0.114 ▂▁▁▁▁▁ · lr 1.2e-4 · ETA 1:12:03 · 🩺 수렴 유지
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
|
|
18
|
+
_SPARK = "▁▂▃▄▅▆▇█"
|
|
19
|
+
|
|
20
|
+
# ── 터미널 색 (v0.3.2 팔레트) ────────────────────────────────────────────────
|
|
21
|
+
_ANSI = {"cyan": "\x1b[38;5;117m", "spark": "\x1b[38;5;111m", "dim": "\x1b[2m",
|
|
22
|
+
"bold": "\x1b[1m", "green": "\x1b[38;5;114m", "amber": "\x1b[38;5;179m",
|
|
23
|
+
"red": "\x1b[38;5;210m", "reset": "\x1b[0m"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _color_enabled() -> bool:
|
|
27
|
+
"""NO_COLOR면 끔, SV_COLOR=1이면 강제 켬, 그 외엔 tty 감지."""
|
|
28
|
+
if os.environ.get("NO_COLOR"):
|
|
29
|
+
return False
|
|
30
|
+
if os.environ.get("SV_COLOR"):
|
|
31
|
+
return True
|
|
32
|
+
try:
|
|
33
|
+
return sys.stderr.isatty()
|
|
34
|
+
except Exception: # noqa: BLE001
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _c(name: str, s: str, on: bool) -> str:
|
|
39
|
+
return f"{_ANSI[name]}{s}{_ANSI['reset']}" if on else s
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def sparkline(values: list[float], width: int = 8) -> str:
|
|
43
|
+
"""최근 값들을 ▁▂▃ 스파크라인으로. 값이 없으면 빈 문자열."""
|
|
44
|
+
if not values:
|
|
45
|
+
return ""
|
|
46
|
+
vs = values[-width:]
|
|
47
|
+
lo, hi = min(vs), max(vs)
|
|
48
|
+
span = (hi - lo) or 1.0
|
|
49
|
+
return "".join(_SPARK[int((v - lo) / span * (len(_SPARK) - 1))] for v in vs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fmt_eta(seconds: float) -> str:
|
|
53
|
+
s = max(0, int(seconds))
|
|
54
|
+
h, s = divmod(s, 3600)
|
|
55
|
+
m, s = divmod(s, 60)
|
|
56
|
+
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class LossMonitor:
|
|
61
|
+
"""loss 흐름을 감시해 상태를 판정하는 감독 코어.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
patience: 이만큼의 스텝 동안 개선이 없으면 plateau (early stop 후보).
|
|
65
|
+
min_steps: 이 스텝 전에는 절대 early stop 하지 않음 (워밍업 보호).
|
|
66
|
+
window: 이동평균 창 크기.
|
|
67
|
+
improve_eps: "개선"으로 인정할 최소 상대 하락폭 (예: 0.02 = 2%).
|
|
68
|
+
spike_ratio: 이동평균 대비 이 배수를 넘으면 스파이크로 기록.
|
|
69
|
+
diverge_ratio: 이동평균이 최저 대비 이 배수를 넘어 유지되면 발산 판정.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
patience: int = 200
|
|
73
|
+
min_steps: int = 120
|
|
74
|
+
window: int = 20
|
|
75
|
+
improve_eps: float = 0.02
|
|
76
|
+
spike_ratio: float = 3.0
|
|
77
|
+
diverge_ratio: float = 2.0
|
|
78
|
+
|
|
79
|
+
losses: list[float] = field(default_factory=list)
|
|
80
|
+
means: list[float] = field(default_factory=list) # 이동평균 궤적 (스파크라인용)
|
|
81
|
+
spikes: list[int] = field(default_factory=list) # 스파이크 발생 스텝
|
|
82
|
+
best_mean: float | None = None
|
|
83
|
+
best_step: int = 0
|
|
84
|
+
step: int = 0
|
|
85
|
+
status: str = "워밍업"
|
|
86
|
+
should_stop: bool = False
|
|
87
|
+
stop_reason: str = ""
|
|
88
|
+
_diverge_run: int = 0
|
|
89
|
+
|
|
90
|
+
def update(self, step: int, loss: float) -> str:
|
|
91
|
+
"""한 로그 스텝 반영 → 현재 상태 문자열 반환."""
|
|
92
|
+
self.step = step
|
|
93
|
+
self.losses.append(loss)
|
|
94
|
+
mean = sum(self.losses[-self.window:]) / min(len(self.losses), self.window)
|
|
95
|
+
self.means.append(mean)
|
|
96
|
+
|
|
97
|
+
warm = len(self.losses) < self.window
|
|
98
|
+
if warm:
|
|
99
|
+
self.status = "워밍업"
|
|
100
|
+
return self.status
|
|
101
|
+
|
|
102
|
+
# 스파이크: 단일 loss가 이동평균의 spike_ratio배 초과
|
|
103
|
+
if loss > mean * self.spike_ratio and loss > 0.05:
|
|
104
|
+
self.spikes.append(step)
|
|
105
|
+
|
|
106
|
+
# 개선 추적 (이동평균 기준)
|
|
107
|
+
if self.best_mean is None or mean < self.best_mean * (1 - self.improve_eps):
|
|
108
|
+
self.best_mean = mean
|
|
109
|
+
self.best_step = step
|
|
110
|
+
|
|
111
|
+
# 발산: 이동평균이 최저 대비 크게 나빠진 채 유지
|
|
112
|
+
if self.best_mean and mean > self.best_mean * self.diverge_ratio:
|
|
113
|
+
self._diverge_run += 1
|
|
114
|
+
else:
|
|
115
|
+
self._diverge_run = 0
|
|
116
|
+
if self._diverge_run >= self.window:
|
|
117
|
+
self.status = "발산 의심"
|
|
118
|
+
if step >= self.min_steps:
|
|
119
|
+
self.should_stop = True
|
|
120
|
+
self.stop_reason = (
|
|
121
|
+
f"step {step}: 이동평균 loss {mean:.4f} — 최저 {self.best_mean:.4f} 대비 "
|
|
122
|
+
f"{self.diverge_ratio}배 이상으로 {self.window}스텝 유지 (발산). lr을 낮춰 재시도 권장."
|
|
123
|
+
)
|
|
124
|
+
return self.status
|
|
125
|
+
|
|
126
|
+
# plateau: patience 스텝 동안 개선 없음
|
|
127
|
+
stale = step - self.best_step
|
|
128
|
+
if stale >= self.patience:
|
|
129
|
+
self.status = f"수렴 완료 (개선 없음 {stale}스텝)"
|
|
130
|
+
if step >= self.min_steps:
|
|
131
|
+
self.should_stop = True
|
|
132
|
+
self.stop_reason = (
|
|
133
|
+
f"step {step}: {stale}스텝 동안 이동평균 개선 없음 (최저 {self.best_mean:.4f}, "
|
|
134
|
+
f"step {self.best_step}). 더 학습해도 이득이 없어 조기 종료합니다."
|
|
135
|
+
)
|
|
136
|
+
elif stale >= self.patience // 2:
|
|
137
|
+
self.status = "수렴 유지"
|
|
138
|
+
else:
|
|
139
|
+
self.status = "학습 중"
|
|
140
|
+
return self.status
|
|
141
|
+
|
|
142
|
+
# ── 터미널 UI ───────────────────────────────────────────────────────────
|
|
143
|
+
def render(self, max_steps: int, started_at: float, lr: float | None = None,
|
|
144
|
+
width: int = 12, color: bool | None = None) -> str:
|
|
145
|
+
"""현재 상태를 터미널 한 줄로 (tty면 색 포함 — NO_COLOR/SV_COLOR로 제어)."""
|
|
146
|
+
on = _color_enabled() if color is None else color
|
|
147
|
+
frac = self.step / max_steps if max_steps else 0.0
|
|
148
|
+
filled = int(frac * width)
|
|
149
|
+
bar = "▓" * filled + "░" * (width - filled)
|
|
150
|
+
loss = self.losses[-1] if self.losses else float("nan")
|
|
151
|
+
spark = sparkline(self.means)
|
|
152
|
+
elapsed = time.time() - started_at
|
|
153
|
+
eta = _fmt_eta(elapsed / frac - elapsed) if frac > 0.005 else "--:--"
|
|
154
|
+
st_color = ("red" if "발산" in self.status
|
|
155
|
+
else "amber" if self.status == "워밍업" else "green")
|
|
156
|
+
parts = [
|
|
157
|
+
"🧠 " + _c("cyan", f"{self.step}/{max_steps} {bar} {frac*100:3.0f}%", on),
|
|
158
|
+
_c("bold", f"loss {loss:.3f}", on) + " " + _c("spark", spark, on),
|
|
159
|
+
]
|
|
160
|
+
if lr:
|
|
161
|
+
parts.append(_c("dim", f"lr {lr:.1e}", on))
|
|
162
|
+
parts.append(_c("dim", f"ETA {eta}", on))
|
|
163
|
+
if self.spikes:
|
|
164
|
+
parts.append(_c("amber", f"⚡{len(self.spikes)}", on))
|
|
165
|
+
parts.append("🩺 " + _c(st_color, self.status, on))
|
|
166
|
+
return " · ".join(parts)
|
|
167
|
+
|
|
168
|
+
def summary(self) -> str:
|
|
169
|
+
"""학습 종료 시 요약 (레시피가 마지막에 출력)."""
|
|
170
|
+
if not self.losses:
|
|
171
|
+
return "🩺 기록된 loss 없음"
|
|
172
|
+
lines = [
|
|
173
|
+
f"🩺 실시간 감독 요약 — {self.step}스텝 관찰",
|
|
174
|
+
f" loss {self.losses[0]:.3f} → {self.losses[-1]:.3f} "
|
|
175
|
+
f"(이동평균 최저 {self.best_mean:.4f} @ step {self.best_step})",
|
|
176
|
+
]
|
|
177
|
+
if self.spikes:
|
|
178
|
+
shown = ", ".join(str(s) for s in self.spikes[:5])
|
|
179
|
+
more = f" 외 {len(self.spikes)-5}건" if len(self.spikes) > 5 else ""
|
|
180
|
+
lines.append(f" ⚡ 스파이크 {len(self.spikes)}건 (step {shown}{more}) — 모두 복귀함")
|
|
181
|
+
# 조기 종료 사유(✂)는 정지 시점에 콜백이 이미 출력하므로 여기선 상태만.
|
|
182
|
+
lines.append(f" 상태: {self.status}")
|
|
183
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""StructVerify 어댑터 스모크 테스트 — 학습된 모델이 실제로 뭘 배웠는지 즉석 확인.
|
|
3
|
+
|
|
4
|
+
학습 데이터에 없는 새 문장으로 schema 추출·verdict 판정을 시켜 일반화 여부를 본다.
|
|
5
|
+
GPU 머신(어댑터 학습한 곳)에서 실행:
|
|
6
|
+
|
|
7
|
+
python -m structverify.training.recipe.sample --adapter ./adapter
|
|
8
|
+
python -m structverify.training.recipe.sample --adapter ./adapter \
|
|
9
|
+
--task schema --claim "당사 물류 파트너는 2,300개사에 달했습니다."
|
|
10
|
+
"""
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")
|
|
15
|
+
os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
|
16
|
+
|
|
17
|
+
# 내장 테스트 — 시드/증강 데이터에 없는 새 표현으로 일반화를 확인.
|
|
18
|
+
_DEFAULT_TESTS = [
|
|
19
|
+
dict(task="schema",
|
|
20
|
+
claim_text="당사와 협력하는 물류 파트너는 2,300개사에 달했습니다.", output=""),
|
|
21
|
+
dict(task="schema",
|
|
22
|
+
claim_text="지난해 해외 법인 매출은 1조 2,400억 원을 기록했습니다.", output=""),
|
|
23
|
+
dict(task="verdict", claim_text="분기 반품률은 12.5%였습니다.",
|
|
24
|
+
evidence_value=8.3, evidence_unit="%", output=""),
|
|
25
|
+
dict(task="verdict", claim_text="누적 다운로드는 340만 건입니다.",
|
|
26
|
+
evidence_value=3_400_000, evidence_unit="건", output=""),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
ap = argparse.ArgumentParser()
|
|
32
|
+
ap.add_argument("--adapter", default="./adapter", help="학습 산출 어댑터 폴더")
|
|
33
|
+
ap.add_argument("--task", choices=["schema", "verdict"], help="단일 테스트 태스크")
|
|
34
|
+
ap.add_argument("--claim", help="단일 테스트 문장 (--task와 함께)")
|
|
35
|
+
ap.add_argument("--evidence", type=float, help="verdict용 근거값")
|
|
36
|
+
ap.add_argument("--unit", default="", help="verdict용 단위")
|
|
37
|
+
ap.add_argument("--max-tokens", type=int, default=120)
|
|
38
|
+
args = ap.parse_args()
|
|
39
|
+
|
|
40
|
+
from unsloth import FastLanguageModel
|
|
41
|
+
|
|
42
|
+
from structverify.training.tasks import build_example
|
|
43
|
+
|
|
44
|
+
if args.task and args.claim:
|
|
45
|
+
kw = dict(task=args.task, claim_text=args.claim, output="")
|
|
46
|
+
if args.task == "verdict":
|
|
47
|
+
kw.update(evidence_value=args.evidence, evidence_unit=args.unit)
|
|
48
|
+
tests = [kw]
|
|
49
|
+
else:
|
|
50
|
+
tests = _DEFAULT_TESTS
|
|
51
|
+
|
|
52
|
+
print(f"어댑터 로드: {args.adapter}")
|
|
53
|
+
model, tok = FastLanguageModel.from_pretrained(
|
|
54
|
+
args.adapter, max_seq_length=2048, load_in_4bit=True)
|
|
55
|
+
FastLanguageModel.for_inference(model)
|
|
56
|
+
|
|
57
|
+
for t in tests:
|
|
58
|
+
msgs = build_example(**t)["messages"][:2] # system + user만
|
|
59
|
+
prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
|
60
|
+
ids = tok(prompt, return_tensors="pt").to(model.device)
|
|
61
|
+
out = model.generate(**ids, max_new_tokens=args.max_tokens, do_sample=False)
|
|
62
|
+
answer = tok.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True).strip()
|
|
63
|
+
claim = t.get("claim_text", "")
|
|
64
|
+
print(f"\n[{t['task']}] {claim}")
|
|
65
|
+
if t["task"] == "verdict":
|
|
66
|
+
print(f" 근거: {t.get('evidence_value')} {t.get('evidence_unit')}")
|
|
67
|
+
print(f" → {answer}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -63,6 +63,9 @@ def main():
|
|
|
63
63
|
ap.add_argument("--batch", type=int, default=1)
|
|
64
64
|
ap.add_argument("--rank", type=int, default=8)
|
|
65
65
|
ap.add_argument("--lr", type=float, default=1e-4)
|
|
66
|
+
ap.add_argument("--early-stop", dest="early_stop", action="store_true", default=True)
|
|
67
|
+
ap.add_argument("--no-early-stop", dest="early_stop", action="store_false")
|
|
68
|
+
ap.add_argument("--patience", type=int, default=200)
|
|
66
69
|
args = ap.parse_args()
|
|
67
70
|
|
|
68
71
|
os.makedirs(args.out, exist_ok=True)
|
|
@@ -76,19 +79,48 @@ def main():
|
|
|
76
79
|
"--data", data_dir, "--adapter-path", args.out,
|
|
77
80
|
"--iters", str(args.iters), "--batch-size", str(args.batch),
|
|
78
81
|
"--num-layers", "8", "--learning-rate", str(args.lr),
|
|
79
|
-
"--steps-per-report", "1", #
|
|
82
|
+
"--steps-per-report", "1", # 🩺 실시간 감독용 촘촘한 loss
|
|
83
|
+
"--save-every", "50", # 조기 종료 대비 어댑터 주기 저장
|
|
80
84
|
]
|
|
81
85
|
print("[train_mlx] 실행:", " ".join(cmd))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
|
|
87
|
+
# ── 🩺 실시간 감독: mlx 출력을 스트리밍으로 읽어 LossMonitor에 공급 ──
|
|
88
|
+
import re as _re
|
|
89
|
+
import time as _time
|
|
90
|
+
|
|
91
|
+
from structverify.training.monitor import LossMonitor
|
|
92
|
+
|
|
93
|
+
mon = LossMonitor(patience=args.patience)
|
|
94
|
+
t0 = _time.time()
|
|
95
|
+
log_lines: list[str] = []
|
|
96
|
+
stopped_early = False
|
|
97
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
98
|
+
text=True, bufsize=1)
|
|
99
|
+
assert proc.stdout is not None
|
|
100
|
+
for line in proc.stdout:
|
|
101
|
+
log_lines.append(line)
|
|
102
|
+
m = _re.search(r"Iter\s+(\d+):.*?Train loss\s+([\d.]+)", line)
|
|
103
|
+
if not m:
|
|
104
|
+
continue
|
|
105
|
+
mon.update(int(m.group(1)), float(m.group(2)))
|
|
106
|
+
print("\r\x1b[2K" + mon.render(args.iters, t0), end="", flush=True, file=sys.stderr)
|
|
107
|
+
if mon.should_stop and args.early_stop:
|
|
108
|
+
print("", file=sys.stderr)
|
|
109
|
+
print(f"✂ {mon.stop_reason}", file=sys.stderr)
|
|
110
|
+
proc.terminate()
|
|
111
|
+
stopped_early = True
|
|
112
|
+
break
|
|
113
|
+
proc.wait()
|
|
114
|
+
print("", file=sys.stderr)
|
|
115
|
+
print(mon.summary(), file=sys.stderr)
|
|
85
116
|
shutil.rmtree(data_dir, ignore_errors=True)
|
|
86
117
|
|
|
87
118
|
# mlx 로그 → 표준 trainer_state.json (TrainDoctor 호환)
|
|
88
|
-
hist = parse_mlx_log(
|
|
119
|
+
hist = parse_mlx_log("".join(log_lines))
|
|
89
120
|
with open(os.path.join(args.out, "trainer_state.json"), "w", encoding="utf-8") as f:
|
|
90
121
|
json.dump({"log_history": hist}, f, ensure_ascii=False)
|
|
91
|
-
if proc.returncode != 0:
|
|
122
|
+
if proc.returncode != 0 and not stopped_early:
|
|
123
|
+
print("".join(log_lines)[-2000:])
|
|
92
124
|
print(f"⚠ mlx_lm.lora 종료코드 {proc.returncode} — 로그 확인")
|
|
93
125
|
sys.exit(proc.returncode)
|
|
94
126
|
print(f"\n✅ 완료 — 어댑터: {args.out} (trainer_state.json {len(hist)}개 기록)")
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""StructVerify QLoRA 학습 레시피 — RTX 3060 12GB / Colab T4 검증됨(사양 기준).
|
|
3
|
+
|
|
4
|
+
이 파일은 GPU 머신에서 실행하는 *샌드박스 레시피*다. 코어 라이브러리는 이 파일을 만들지도,
|
|
5
|
+
직접 import 하지도 않는다(무거운 deps 격리). 표준 chat jsonl 만 먹으므로 데이터는 어떤
|
|
6
|
+
경로로 만들어도 됨.
|
|
7
|
+
|
|
8
|
+
준비 (GPU 머신):
|
|
9
|
+
pip install "structverify[training]" # unsloth·trl·peft·bitsandbytes·torch
|
|
10
|
+
# 또는: pip install unsloth trl peft bitsandbytes accelerate datasets
|
|
11
|
+
|
|
12
|
+
실행:
|
|
13
|
+
python train_qlora.py --model unsloth/Qwen2.5-7B-Instruct \
|
|
14
|
+
--data train.jsonl --out ./adapter --max-steps 60
|
|
15
|
+
|
|
16
|
+
산출: {out}/ 에 LoRA 어댑터 + trainer_state.json (→ TrainDoctor가 읽음)
|
|
17
|
+
"""
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
# 학습과 무관한 라이브러리 잡음(TF·경고)을 미리 조용히 — UI 한 줄만 보이게.
|
|
25
|
+
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")
|
|
26
|
+
os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
|
27
|
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
28
|
+
|
|
29
|
+
# "Exception ignored in: <_io.BytesIO ...> BufferError" 도배 차단.
|
|
30
|
+
# Python 3.13 + datasets 멀티프로세스 토큰화의 무해한 정리 단계 잡음인데,
|
|
31
|
+
# 워커가 fork로 뜨면서 이 훅을 물려받아 자식 프로세스 출력까지 조용해진다.
|
|
32
|
+
sys.unraisablehook = lambda *_: None
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
import warnings
|
|
36
|
+
|
|
37
|
+
warnings.filterwarnings("ignore") # Flax deprecated 등
|
|
38
|
+
logging.getLogger("torchao").setLevel(logging.CRITICAL) # torchao .so 로드 WARNING
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_chat_dataset(path):
|
|
42
|
+
from datasets import Dataset
|
|
43
|
+
rows = []
|
|
44
|
+
with open(path, encoding="utf-8") as f:
|
|
45
|
+
for line in f:
|
|
46
|
+
line = line.strip()
|
|
47
|
+
if line:
|
|
48
|
+
rows.append(json.loads(line))
|
|
49
|
+
# messages 만 남김 (task 필드는 학습에 불필요)
|
|
50
|
+
return Dataset.from_list([{"messages": r["messages"]} for r in rows])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
ap = argparse.ArgumentParser()
|
|
55
|
+
ap.add_argument("--model", default="unsloth/Qwen2.5-7B-Instruct",
|
|
56
|
+
help="4bit 베이스 모델 (unsloth 허브 권장). 3060 6GB면 3B 계열로.")
|
|
57
|
+
ap.add_argument("--data", required=True, help="chat jsonl (structverify가 export)")
|
|
58
|
+
ap.add_argument("--out", default="./adapter")
|
|
59
|
+
ap.add_argument("--max-steps", type=int, default=60)
|
|
60
|
+
ap.add_argument("--lr", type=float, default=2e-4)
|
|
61
|
+
ap.add_argument("--rank", type=int, default=16)
|
|
62
|
+
ap.add_argument("--seq-len", type=int, default=2048)
|
|
63
|
+
ap.add_argument("--batch", type=int, default=1)
|
|
64
|
+
ap.add_argument("--grad-accum", type=int, default=8)
|
|
65
|
+
# 🩺 실시간 감독 (LossMonitor) — plateau/발산이면 스스로 멈춘다.
|
|
66
|
+
ap.add_argument("--early-stop", dest="early_stop", action="store_true", default=True)
|
|
67
|
+
ap.add_argument("--no-early-stop", dest="early_stop", action="store_false",
|
|
68
|
+
help="감독은 하되 조기 종료는 하지 않음 (관찰만)")
|
|
69
|
+
ap.add_argument("--patience", type=int, default=200,
|
|
70
|
+
help="이 스텝 동안 개선 없으면 수렴 완료로 판단")
|
|
71
|
+
ap.add_argument("--keep-checkpoints", action="store_true",
|
|
72
|
+
help="중간 체크포인트(checkpoint-*) 보존 — 기본은 성공 시 삭제 (어댑터만 수십 MB)")
|
|
73
|
+
args = ap.parse_args()
|
|
74
|
+
|
|
75
|
+
# unsloth: 3060/T4에서 QLoRA 2배 빠름 + 메모리 절약
|
|
76
|
+
from unsloth import FastLanguageModel
|
|
77
|
+
from trl import SFTTrainer, SFTConfig
|
|
78
|
+
|
|
79
|
+
# tqdm 진행바(체크포인트 로드·Map·Tokenizing) 대신 우리 phase 라인으로.
|
|
80
|
+
try:
|
|
81
|
+
import datasets as _ds
|
|
82
|
+
import transformers as _tf
|
|
83
|
+
_ds.disable_progress_bars()
|
|
84
|
+
_tf.utils.logging.disable_progress_bar()
|
|
85
|
+
except Exception: # noqa: BLE001
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
print(f"⚙ 베이스 모델 로드 — {args.model} (4bit)", file=sys.stderr)
|
|
89
|
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
90
|
+
model_name=args.model,
|
|
91
|
+
max_seq_length=args.seq_len,
|
|
92
|
+
load_in_4bit=True, # QLoRA — 12GB VRAM에 7B 적재
|
|
93
|
+
)
|
|
94
|
+
model = FastLanguageModel.get_peft_model(
|
|
95
|
+
model,
|
|
96
|
+
r=args.rank,
|
|
97
|
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
|
98
|
+
"gate_proj", "up_proj", "down_proj"],
|
|
99
|
+
lora_alpha=args.rank * 2,
|
|
100
|
+
lora_dropout=0.0,
|
|
101
|
+
bias="none",
|
|
102
|
+
use_gradient_checkpointing="unsloth",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print("⚙ 데이터 준비·토큰화 중 …", file=sys.stderr)
|
|
106
|
+
ds = load_chat_dataset(args.data)
|
|
107
|
+
|
|
108
|
+
def fmt(ex):
|
|
109
|
+
return {"text": tokenizer.apply_chat_template(
|
|
110
|
+
ex["messages"], tokenize=False, add_generation_prompt=False)}
|
|
111
|
+
ds = ds.map(fmt)
|
|
112
|
+
|
|
113
|
+
trainer = SFTTrainer(
|
|
114
|
+
model=model, tokenizer=tokenizer, train_dataset=ds,
|
|
115
|
+
args=SFTConfig(
|
|
116
|
+
output_dir=args.out,
|
|
117
|
+
per_device_train_batch_size=args.batch,
|
|
118
|
+
gradient_accumulation_steps=args.grad_accum,
|
|
119
|
+
warmup_steps=5,
|
|
120
|
+
max_steps=args.max_steps,
|
|
121
|
+
learning_rate=args.lr,
|
|
122
|
+
logging_steps=1, # TrainDoctor용 촘촘한 loss 기록
|
|
123
|
+
max_grad_norm=1.0, # 발산 방지
|
|
124
|
+
optim="adamw_8bit",
|
|
125
|
+
lr_scheduler_type="linear",
|
|
126
|
+
seed=42,
|
|
127
|
+
dataset_text_field="text",
|
|
128
|
+
report_to="none",
|
|
129
|
+
disable_tqdm=True, # 진행 표시는 🩺 감독 UI 한 줄이 담당
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# ── 🩺 TrainDoctor 실시간 감독: 상태 한 줄 UI + plateau/발산 조기 종료 ──
|
|
134
|
+
from transformers import TrainerCallback
|
|
135
|
+
from transformers.trainer_callback import PrinterCallback
|
|
136
|
+
|
|
137
|
+
from structverify.training.monitor import LossMonitor, _c, _color_enabled
|
|
138
|
+
|
|
139
|
+
class SVLiveCallback(TrainerCallback):
|
|
140
|
+
def __init__(self, early_stop: bool, patience: int):
|
|
141
|
+
self.mon = LossMonitor(patience=patience)
|
|
142
|
+
self.early_stop = early_stop
|
|
143
|
+
self.t0 = time.time()
|
|
144
|
+
|
|
145
|
+
def on_log(self, targs, state, control, logs=None, **kw):
|
|
146
|
+
if not logs or "loss" not in logs:
|
|
147
|
+
return control
|
|
148
|
+
self.mon.update(state.global_step, logs["loss"])
|
|
149
|
+
line = self.mon.render(state.max_steps, self.t0, lr=logs.get("learning_rate"))
|
|
150
|
+
print("\r\x1b[2K" + line, end="", flush=True, file=sys.stderr)
|
|
151
|
+
if self.mon.should_stop and self.early_stop:
|
|
152
|
+
print("", file=sys.stderr)
|
|
153
|
+
print(_c("amber", f"✂ {self.mon.stop_reason}", _color_enabled()), file=sys.stderr)
|
|
154
|
+
control.should_training_stop = True
|
|
155
|
+
return control
|
|
156
|
+
|
|
157
|
+
def on_train_end(self, targs, state, control, **kw):
|
|
158
|
+
print("", file=sys.stderr)
|
|
159
|
+
print(_c("green", self.mon.summary(), _color_enabled()), file=sys.stderr)
|
|
160
|
+
return control
|
|
161
|
+
|
|
162
|
+
trainer.remove_callback(PrinterCallback) # {'loss': ...} dict 도배 제거
|
|
163
|
+
trainer.add_callback(SVLiveCallback(args.early_stop, args.patience))
|
|
164
|
+
|
|
165
|
+
es = "ON" if args.early_stop else "OFF (관찰만)"
|
|
166
|
+
print(f"🩺 실시간 감독 시작 — patience {args.patience} · 조기 종료 {es}", file=sys.stderr)
|
|
167
|
+
trainer.train()
|
|
168
|
+
|
|
169
|
+
# 어댑터 + trainer_state.json 저장 (TrainDoctor가 읽음)
|
|
170
|
+
model.save_pretrained(args.out)
|
|
171
|
+
tokenizer.save_pretrained(args.out)
|
|
172
|
+
trainer.state.save_to_json(f"{args.out}/trainer_state.json")
|
|
173
|
+
|
|
174
|
+
# 성공했으면 중간 체크포인트(옵티마이저 상태 포함, 수백 MB) 정리 — 최종 어댑터만 남긴다.
|
|
175
|
+
if not args.keep_checkpoints:
|
|
176
|
+
import glob
|
|
177
|
+
import shutil
|
|
178
|
+
for ck in glob.glob(os.path.join(args.out, "checkpoint-*")):
|
|
179
|
+
shutil.rmtree(ck, ignore_errors=True)
|
|
180
|
+
|
|
181
|
+
# 학습 메타 기록 — 이 어댑터가 무엇으로 어떻게 학습됐는지 (주입·추적용).
|
|
182
|
+
by_task: dict[str, int] = {}
|
|
183
|
+
with open(args.data, encoding="utf-8") as f:
|
|
184
|
+
for line in f:
|
|
185
|
+
if line.strip():
|
|
186
|
+
t = json.loads(line).get("task", "?")
|
|
187
|
+
by_task[t] = by_task.get(t, 0) + 1
|
|
188
|
+
hist = [h for h in trainer.state.log_history if "loss" in h]
|
|
189
|
+
meta = {
|
|
190
|
+
"library": "structverify", "backend": "qlora", "base_model": args.model,
|
|
191
|
+
"dataset": os.path.abspath(args.data), "examples": sum(by_task.values()),
|
|
192
|
+
"task_distribution": by_task, "max_steps": args.max_steps,
|
|
193
|
+
"trained_steps": trainer.state.global_step,
|
|
194
|
+
"loss": {"first": hist[0]["loss"] if hist else None,
|
|
195
|
+
"last": hist[-1]["loss"] if hist else None,
|
|
196
|
+
"min": min((h["loss"] for h in hist), default=None)},
|
|
197
|
+
"lora": {"rank": args.rank, "lr": args.lr, "seq_len": args.seq_len},
|
|
198
|
+
"trained_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
199
|
+
}
|
|
200
|
+
with open(os.path.join(args.out, "training_meta.json"), "w", encoding="utf-8") as f:
|
|
201
|
+
json.dump(meta, f, ensure_ascii=False, indent=2)
|
|
202
|
+
|
|
203
|
+
print(f"\n✅ 완료 — 어댑터: {args.out} (trainer_state.json · training_meta.json 포함)")
|
|
204
|
+
print(" 다음: TrainDoctor로 진단, EvalGate로 채택 여부 판정")
|
|
205
|
+
print(f" 스모크 테스트: python -m structverify.training.recipe.sample --adapter {args.out}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structverify
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Compliance & fact verification for documents — check your files against your own rulebook (PDF) or a data source, and read a plain True/False.
|
|
5
5
|
Author-email: "김예슬 (Yeseul Kim)" <yesul0718@gmail.com>
|
|
6
6
|
License: MIT License
|