structverify 0.3.0__tar.gz → 0.3.1__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.1}/PKG-INFO +1 -1
- {structverify-0.3.0 → structverify-0.3.1}/pyproject.toml +1 -1
- {structverify-0.3.0 → structverify-0.3.1}/structverify/__init__.py +1 -1
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/curator.py +5 -1
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/doctor.py +31 -5
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/loop.py +8 -2
- structverify-0.3.1/structverify/training/monitor.py +150 -0
- structverify-0.3.1/structverify/training/recipe/sample.py +71 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/recipe/train_mlx.py +38 -6
- structverify-0.3.1/structverify/training/recipe/train_qlora.py +185 -0
- {structverify-0.3.0 → structverify-0.3.1/structverify.egg-info}/PKG-INFO +1 -1
- {structverify-0.3.0 → structverify-0.3.1}/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.1}/LICENSE +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/README.md +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/setup.cfg +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/adapter_trainer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/feedback_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/kosis_crawler.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/sample_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/synthetic_generator.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/update_embeddings.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/builder_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/conformance_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/dependency_planner.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/indexing_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/indexing_planner.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/integration_example.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/loop.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/memory.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/planner.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/prompts/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/prompts/planner_prompts.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/prompts/reflect_prompts.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/reflect.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/runtime_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/schemas.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/source_profiler.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/base.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/calculate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/catalog_search.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/deep_explore.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/explore_catalog.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/fetch_evidence.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/finish.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/meta_explore.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/query_rewriter.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/read_original.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/replan.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/workspace.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/api.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/config/default.yaml +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/core/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/core/config_loader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/core/pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/core/schemas.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/_llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate/heuristic.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate/llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate_scorer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/claim_detector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/claims/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/claims/worthiness.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/classify.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/preview.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/registry.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain_classifier.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/candidate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/claim_worthiness.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/domain.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/schema.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts_loader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/expand.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/induce.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/regenerate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/temporal_hints.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/validate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema_inductor.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/synthetic_generator.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/_llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/explainer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/fallback.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/formatters.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/match.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/mismatch.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/multihop.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/unverifiable.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/claim_graph.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/document_graph.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/graph_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/graph_multihop.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/graph_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/provenance.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/agent_memory.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/embedder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/exemplar_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/normalizer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/schema.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/storage/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/storage/jsonl_store.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/working_memory.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/Dockerfile.scraper +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/extractor.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/fields.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/markdown.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/models.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/ocr.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/reader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/scoring.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/scraper_sandbox.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/segmenter.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/sir_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/progress.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/base.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/base_connector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/catalog_ranker.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/catalog_search.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/chunking.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/custom_csv_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/custom_db_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/custom_docs_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/dimension_resolver.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/evidence_subgraph.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/kosis_connector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/kosis_relevance.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/kosis_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/query_builder.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/registry.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/relevance_judge.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/row_matcher.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/db_manager.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/dwh_manager.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/init_db.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/raw_storage.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/dataset.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/evalgate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/generate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/training/tasks.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/embedding_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/llm_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/logger.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/__init__.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/adapters.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/conformance.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/decide_verdict.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/decide_verdict_agent.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/growth_diff.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/row_match.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/units.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/verdict_thresholds.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/verifier.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify.egg-info/dependency_links.txt +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify.egg-info/requires.txt +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/structverify.egg-info/top_level.txt +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_api.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_catalog_search_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_custom_csv_source.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_candidate_heuristic.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_config.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_domain_preview.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_domain_registry.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_expand_temporal.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_init_exports.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_llm.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_prompts_loader.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_validate.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_worthiness.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_embedding_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_embedding_config_threading.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_explore_catalog_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_is_table_relevant.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_kosis_crawler_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_kosis_guards.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_kosis_json_load_pgvector.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_library_entry_smoke.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_llm_client.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_runtime_datasources.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_step1-9_v3_pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_step1_6_detection.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_step1_9_pipeline.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_step9_explainer.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/tests/test_update_embeddings_embedding.py +0 -0
- {structverify-0.3.0 → structverify-0.3.1}/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.1
|
|
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.1"
|
|
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,150 @@
|
|
|
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 time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
_SPARK = "▁▂▃▄▅▆▇█"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def sparkline(values: list[float], width: int = 8) -> str:
|
|
20
|
+
"""최근 값들을 ▁▂▃ 스파크라인으로. 값이 없으면 빈 문자열."""
|
|
21
|
+
if not values:
|
|
22
|
+
return ""
|
|
23
|
+
vs = values[-width:]
|
|
24
|
+
lo, hi = min(vs), max(vs)
|
|
25
|
+
span = (hi - lo) or 1.0
|
|
26
|
+
return "".join(_SPARK[int((v - lo) / span * (len(_SPARK) - 1))] for v in vs)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fmt_eta(seconds: float) -> str:
|
|
30
|
+
s = max(0, int(seconds))
|
|
31
|
+
h, s = divmod(s, 3600)
|
|
32
|
+
m, s = divmod(s, 60)
|
|
33
|
+
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class LossMonitor:
|
|
38
|
+
"""loss 흐름을 감시해 상태를 판정하는 감독 코어.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
patience: 이만큼의 스텝 동안 개선이 없으면 plateau (early stop 후보).
|
|
42
|
+
min_steps: 이 스텝 전에는 절대 early stop 하지 않음 (워밍업 보호).
|
|
43
|
+
window: 이동평균 창 크기.
|
|
44
|
+
improve_eps: "개선"으로 인정할 최소 상대 하락폭 (예: 0.02 = 2%).
|
|
45
|
+
spike_ratio: 이동평균 대비 이 배수를 넘으면 스파이크로 기록.
|
|
46
|
+
diverge_ratio: 이동평균이 최저 대비 이 배수를 넘어 유지되면 발산 판정.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
patience: int = 200
|
|
50
|
+
min_steps: int = 120
|
|
51
|
+
window: int = 20
|
|
52
|
+
improve_eps: float = 0.02
|
|
53
|
+
spike_ratio: float = 3.0
|
|
54
|
+
diverge_ratio: float = 2.0
|
|
55
|
+
|
|
56
|
+
losses: list[float] = field(default_factory=list)
|
|
57
|
+
means: list[float] = field(default_factory=list) # 이동평균 궤적 (스파크라인용)
|
|
58
|
+
spikes: list[int] = field(default_factory=list) # 스파이크 발생 스텝
|
|
59
|
+
best_mean: float | None = None
|
|
60
|
+
best_step: int = 0
|
|
61
|
+
step: int = 0
|
|
62
|
+
status: str = "워밍업"
|
|
63
|
+
should_stop: bool = False
|
|
64
|
+
stop_reason: str = ""
|
|
65
|
+
_diverge_run: int = 0
|
|
66
|
+
|
|
67
|
+
def update(self, step: int, loss: float) -> str:
|
|
68
|
+
"""한 로그 스텝 반영 → 현재 상태 문자열 반환."""
|
|
69
|
+
self.step = step
|
|
70
|
+
self.losses.append(loss)
|
|
71
|
+
mean = sum(self.losses[-self.window:]) / min(len(self.losses), self.window)
|
|
72
|
+
self.means.append(mean)
|
|
73
|
+
|
|
74
|
+
warm = len(self.losses) < self.window
|
|
75
|
+
if warm:
|
|
76
|
+
self.status = "워밍업"
|
|
77
|
+
return self.status
|
|
78
|
+
|
|
79
|
+
# 스파이크: 단일 loss가 이동평균의 spike_ratio배 초과
|
|
80
|
+
if loss > mean * self.spike_ratio and loss > 0.05:
|
|
81
|
+
self.spikes.append(step)
|
|
82
|
+
|
|
83
|
+
# 개선 추적 (이동평균 기준)
|
|
84
|
+
if self.best_mean is None or mean < self.best_mean * (1 - self.improve_eps):
|
|
85
|
+
self.best_mean = mean
|
|
86
|
+
self.best_step = step
|
|
87
|
+
|
|
88
|
+
# 발산: 이동평균이 최저 대비 크게 나빠진 채 유지
|
|
89
|
+
if self.best_mean and mean > self.best_mean * self.diverge_ratio:
|
|
90
|
+
self._diverge_run += 1
|
|
91
|
+
else:
|
|
92
|
+
self._diverge_run = 0
|
|
93
|
+
if self._diverge_run >= self.window:
|
|
94
|
+
self.status = "발산 의심"
|
|
95
|
+
if step >= self.min_steps:
|
|
96
|
+
self.should_stop = True
|
|
97
|
+
self.stop_reason = (
|
|
98
|
+
f"step {step}: 이동평균 loss {mean:.4f} — 최저 {self.best_mean:.4f} 대비 "
|
|
99
|
+
f"{self.diverge_ratio}배 이상으로 {self.window}스텝 유지 (발산). lr을 낮춰 재시도 권장."
|
|
100
|
+
)
|
|
101
|
+
return self.status
|
|
102
|
+
|
|
103
|
+
# plateau: patience 스텝 동안 개선 없음
|
|
104
|
+
stale = step - self.best_step
|
|
105
|
+
if stale >= self.patience:
|
|
106
|
+
self.status = f"수렴 완료 (개선 없음 {stale}스텝)"
|
|
107
|
+
if step >= self.min_steps:
|
|
108
|
+
self.should_stop = True
|
|
109
|
+
self.stop_reason = (
|
|
110
|
+
f"step {step}: {stale}스텝 동안 이동평균 개선 없음 (최저 {self.best_mean:.4f}, "
|
|
111
|
+
f"step {self.best_step}). 더 학습해도 이득이 없어 조기 종료합니다."
|
|
112
|
+
)
|
|
113
|
+
elif stale >= self.patience // 2:
|
|
114
|
+
self.status = "수렴 유지"
|
|
115
|
+
else:
|
|
116
|
+
self.status = "학습 중"
|
|
117
|
+
return self.status
|
|
118
|
+
|
|
119
|
+
# ── 터미널 UI ───────────────────────────────────────────────────────────
|
|
120
|
+
def render(self, max_steps: int, started_at: float, lr: float | None = None,
|
|
121
|
+
width: int = 12) -> str:
|
|
122
|
+
"""현재 상태를 터미널 한 줄로."""
|
|
123
|
+
frac = self.step / max_steps if max_steps else 0.0
|
|
124
|
+
filled = int(frac * width)
|
|
125
|
+
bar = "▓" * filled + "░" * (width - filled)
|
|
126
|
+
loss = self.losses[-1] if self.losses else float("nan")
|
|
127
|
+
spark = sparkline(self.means)
|
|
128
|
+
elapsed = time.time() - started_at
|
|
129
|
+
eta = _fmt_eta(elapsed / frac - elapsed) if frac > 0.01 else "--:--"
|
|
130
|
+
lr_s = f" · lr {lr:.1e}" if lr else ""
|
|
131
|
+
spike_s = f" · ⚡{len(self.spikes)}" if self.spikes else ""
|
|
132
|
+
return (f"🧠 {self.step}/{max_steps} {bar} {frac*100:3.0f}% · "
|
|
133
|
+
f"loss {loss:.3f} {spark}{lr_s} · ETA {eta}{spike_s} · 🩺 {self.status}")
|
|
134
|
+
|
|
135
|
+
def summary(self) -> str:
|
|
136
|
+
"""학습 종료 시 요약 (레시피가 마지막에 출력)."""
|
|
137
|
+
if not self.losses:
|
|
138
|
+
return "🩺 기록된 loss 없음"
|
|
139
|
+
lines = [
|
|
140
|
+
f"🩺 실시간 감독 요약 — {self.step}스텝 관찰",
|
|
141
|
+
f" loss {self.losses[0]:.3f} → {self.losses[-1]:.3f} "
|
|
142
|
+
f"(이동평균 최저 {self.best_mean:.4f} @ step {self.best_step})",
|
|
143
|
+
]
|
|
144
|
+
if self.spikes:
|
|
145
|
+
shown = ", ".join(str(s) for s in self.spikes[:5])
|
|
146
|
+
more = f" 외 {len(self.spikes)-5}건" if len(self.spikes) > 5 else ""
|
|
147
|
+
lines.append(f" ⚡ 스파이크 {len(self.spikes)}건 (step {shown}{more}) — 모두 복귀함")
|
|
148
|
+
# 조기 종료 사유(✂)는 정지 시점에 콜백이 이미 출력하므로 여기선 상태만.
|
|
149
|
+
lines.append(f" 상태: {self.status}")
|
|
150
|
+
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,185 @@
|
|
|
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
|
+
|
|
30
|
+
def load_chat_dataset(path):
|
|
31
|
+
from datasets import Dataset
|
|
32
|
+
rows = []
|
|
33
|
+
with open(path, encoding="utf-8") as f:
|
|
34
|
+
for line in f:
|
|
35
|
+
line = line.strip()
|
|
36
|
+
if line:
|
|
37
|
+
rows.append(json.loads(line))
|
|
38
|
+
# messages 만 남김 (task 필드는 학습에 불필요)
|
|
39
|
+
return Dataset.from_list([{"messages": r["messages"]} for r in rows])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
ap = argparse.ArgumentParser()
|
|
44
|
+
ap.add_argument("--model", default="unsloth/Qwen2.5-7B-Instruct",
|
|
45
|
+
help="4bit 베이스 모델 (unsloth 허브 권장). 3060 6GB면 3B 계열로.")
|
|
46
|
+
ap.add_argument("--data", required=True, help="chat jsonl (structverify가 export)")
|
|
47
|
+
ap.add_argument("--out", default="./adapter")
|
|
48
|
+
ap.add_argument("--max-steps", type=int, default=60)
|
|
49
|
+
ap.add_argument("--lr", type=float, default=2e-4)
|
|
50
|
+
ap.add_argument("--rank", type=int, default=16)
|
|
51
|
+
ap.add_argument("--seq-len", type=int, default=2048)
|
|
52
|
+
ap.add_argument("--batch", type=int, default=1)
|
|
53
|
+
ap.add_argument("--grad-accum", type=int, default=8)
|
|
54
|
+
# 🩺 실시간 감독 (LossMonitor) — plateau/발산이면 스스로 멈춘다.
|
|
55
|
+
ap.add_argument("--early-stop", dest="early_stop", action="store_true", default=True)
|
|
56
|
+
ap.add_argument("--no-early-stop", dest="early_stop", action="store_false",
|
|
57
|
+
help="감독은 하되 조기 종료는 하지 않음 (관찰만)")
|
|
58
|
+
ap.add_argument("--patience", type=int, default=200,
|
|
59
|
+
help="이 스텝 동안 개선 없으면 수렴 완료로 판단")
|
|
60
|
+
ap.add_argument("--keep-checkpoints", action="store_true",
|
|
61
|
+
help="중간 체크포인트(checkpoint-*) 보존 — 기본은 성공 시 삭제 (어댑터만 수십 MB)")
|
|
62
|
+
args = ap.parse_args()
|
|
63
|
+
|
|
64
|
+
# unsloth: 3060/T4에서 QLoRA 2배 빠름 + 메모리 절약
|
|
65
|
+
from unsloth import FastLanguageModel
|
|
66
|
+
from trl import SFTTrainer, SFTConfig
|
|
67
|
+
|
|
68
|
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
69
|
+
model_name=args.model,
|
|
70
|
+
max_seq_length=args.seq_len,
|
|
71
|
+
load_in_4bit=True, # QLoRA — 12GB VRAM에 7B 적재
|
|
72
|
+
)
|
|
73
|
+
model = FastLanguageModel.get_peft_model(
|
|
74
|
+
model,
|
|
75
|
+
r=args.rank,
|
|
76
|
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
|
77
|
+
"gate_proj", "up_proj", "down_proj"],
|
|
78
|
+
lora_alpha=args.rank * 2,
|
|
79
|
+
lora_dropout=0.0,
|
|
80
|
+
bias="none",
|
|
81
|
+
use_gradient_checkpointing="unsloth",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
ds = load_chat_dataset(args.data)
|
|
85
|
+
|
|
86
|
+
def fmt(ex):
|
|
87
|
+
return {"text": tokenizer.apply_chat_template(
|
|
88
|
+
ex["messages"], tokenize=False, add_generation_prompt=False)}
|
|
89
|
+
ds = ds.map(fmt)
|
|
90
|
+
|
|
91
|
+
trainer = SFTTrainer(
|
|
92
|
+
model=model, tokenizer=tokenizer, train_dataset=ds,
|
|
93
|
+
args=SFTConfig(
|
|
94
|
+
output_dir=args.out,
|
|
95
|
+
per_device_train_batch_size=args.batch,
|
|
96
|
+
gradient_accumulation_steps=args.grad_accum,
|
|
97
|
+
warmup_steps=5,
|
|
98
|
+
max_steps=args.max_steps,
|
|
99
|
+
learning_rate=args.lr,
|
|
100
|
+
logging_steps=1, # TrainDoctor용 촘촘한 loss 기록
|
|
101
|
+
max_grad_norm=1.0, # 발산 방지
|
|
102
|
+
optim="adamw_8bit",
|
|
103
|
+
lr_scheduler_type="linear",
|
|
104
|
+
seed=42,
|
|
105
|
+
dataset_text_field="text",
|
|
106
|
+
report_to="none",
|
|
107
|
+
disable_tqdm=True, # 진행 표시는 🩺 감독 UI 한 줄이 담당
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# ── 🩺 TrainDoctor 실시간 감독: 상태 한 줄 UI + plateau/발산 조기 종료 ──
|
|
112
|
+
from transformers import TrainerCallback
|
|
113
|
+
from transformers.trainer_callback import PrinterCallback
|
|
114
|
+
|
|
115
|
+
from structverify.training.monitor import LossMonitor
|
|
116
|
+
|
|
117
|
+
class SVLiveCallback(TrainerCallback):
|
|
118
|
+
def __init__(self, early_stop: bool, patience: int):
|
|
119
|
+
self.mon = LossMonitor(patience=patience)
|
|
120
|
+
self.early_stop = early_stop
|
|
121
|
+
self.t0 = time.time()
|
|
122
|
+
|
|
123
|
+
def on_log(self, targs, state, control, logs=None, **kw):
|
|
124
|
+
if not logs or "loss" not in logs:
|
|
125
|
+
return control
|
|
126
|
+
self.mon.update(state.global_step, logs["loss"])
|
|
127
|
+
line = self.mon.render(state.max_steps, self.t0, lr=logs.get("learning_rate"))
|
|
128
|
+
print("\r\x1b[2K" + line, end="", flush=True, file=sys.stderr)
|
|
129
|
+
if self.mon.should_stop and self.early_stop:
|
|
130
|
+
print("", file=sys.stderr)
|
|
131
|
+
print(f"✂ {self.mon.stop_reason}", file=sys.stderr)
|
|
132
|
+
control.should_training_stop = True
|
|
133
|
+
return control
|
|
134
|
+
|
|
135
|
+
def on_train_end(self, targs, state, control, **kw):
|
|
136
|
+
print("", file=sys.stderr)
|
|
137
|
+
print(self.mon.summary(), file=sys.stderr)
|
|
138
|
+
return control
|
|
139
|
+
|
|
140
|
+
trainer.remove_callback(PrinterCallback) # {'loss': ...} dict 도배 제거
|
|
141
|
+
trainer.add_callback(SVLiveCallback(args.early_stop, args.patience))
|
|
142
|
+
|
|
143
|
+
trainer.train()
|
|
144
|
+
|
|
145
|
+
# 어댑터 + trainer_state.json 저장 (TrainDoctor가 읽음)
|
|
146
|
+
model.save_pretrained(args.out)
|
|
147
|
+
tokenizer.save_pretrained(args.out)
|
|
148
|
+
trainer.state.save_to_json(f"{args.out}/trainer_state.json")
|
|
149
|
+
|
|
150
|
+
# 성공했으면 중간 체크포인트(옵티마이저 상태 포함, 수백 MB) 정리 — 최종 어댑터만 남긴다.
|
|
151
|
+
if not args.keep_checkpoints:
|
|
152
|
+
import glob
|
|
153
|
+
import shutil
|
|
154
|
+
for ck in glob.glob(os.path.join(args.out, "checkpoint-*")):
|
|
155
|
+
shutil.rmtree(ck, ignore_errors=True)
|
|
156
|
+
|
|
157
|
+
# 학습 메타 기록 — 이 어댑터가 무엇으로 어떻게 학습됐는지 (주입·추적용).
|
|
158
|
+
by_task: dict[str, int] = {}
|
|
159
|
+
with open(args.data, encoding="utf-8") as f:
|
|
160
|
+
for line in f:
|
|
161
|
+
if line.strip():
|
|
162
|
+
t = json.loads(line).get("task", "?")
|
|
163
|
+
by_task[t] = by_task.get(t, 0) + 1
|
|
164
|
+
hist = [h for h in trainer.state.log_history if "loss" in h]
|
|
165
|
+
meta = {
|
|
166
|
+
"library": "structverify", "backend": "qlora", "base_model": args.model,
|
|
167
|
+
"dataset": os.path.abspath(args.data), "examples": sum(by_task.values()),
|
|
168
|
+
"task_distribution": by_task, "max_steps": args.max_steps,
|
|
169
|
+
"trained_steps": trainer.state.global_step,
|
|
170
|
+
"loss": {"first": hist[0]["loss"] if hist else None,
|
|
171
|
+
"last": hist[-1]["loss"] if hist else None,
|
|
172
|
+
"min": min((h["loss"] for h in hist), default=None)},
|
|
173
|
+
"lora": {"rank": args.rank, "lr": args.lr, "seq_len": args.seq_len},
|
|
174
|
+
"trained_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
175
|
+
}
|
|
176
|
+
with open(os.path.join(args.out, "training_meta.json"), "w", encoding="utf-8") as f:
|
|
177
|
+
json.dump(meta, f, ensure_ascii=False, indent=2)
|
|
178
|
+
|
|
179
|
+
print(f"\n✅ 완료 — 어댑터: {args.out} (trainer_state.json · training_meta.json 포함)")
|
|
180
|
+
print(" 다음: TrainDoctor로 진단, EvalGate로 채택 여부 판정")
|
|
181
|
+
print(f" 스모크 테스트: python -m structverify.training.recipe.sample --adapter {args.out}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structverify
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -151,7 +151,9 @@ structverify/training/doctor.py
|
|
|
151
151
|
structverify/training/evalgate.py
|
|
152
152
|
structverify/training/generate.py
|
|
153
153
|
structverify/training/loop.py
|
|
154
|
+
structverify/training/monitor.py
|
|
154
155
|
structverify/training/tasks.py
|
|
156
|
+
structverify/training/recipe/sample.py
|
|
155
157
|
structverify/training/recipe/train_mlx.py
|
|
156
158
|
structverify/training/recipe/train_qlora.py
|
|
157
159
|
structverify/utils/__init__.py
|