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.
Files changed (206) hide show
  1. {structverify-0.3.0/structverify.egg-info → structverify-0.3.1}/PKG-INFO +1 -1
  2. {structverify-0.3.0 → structverify-0.3.1}/pyproject.toml +1 -1
  3. {structverify-0.3.0 → structverify-0.3.1}/structverify/__init__.py +1 -1
  4. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/curator.py +5 -1
  5. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/doctor.py +31 -5
  6. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/loop.py +8 -2
  7. structverify-0.3.1/structverify/training/monitor.py +150 -0
  8. structverify-0.3.1/structverify/training/recipe/sample.py +71 -0
  9. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/recipe/train_mlx.py +38 -6
  10. structverify-0.3.1/structverify/training/recipe/train_qlora.py +185 -0
  11. {structverify-0.3.0 → structverify-0.3.1/structverify.egg-info}/PKG-INFO +1 -1
  12. {structverify-0.3.0 → structverify-0.3.1}/structverify.egg-info/SOURCES.txt +2 -0
  13. structverify-0.3.0/structverify/training/recipe/train_qlora.py +0 -104
  14. {structverify-0.3.0 → structverify-0.3.1}/LICENSE +0 -0
  15. {structverify-0.3.0 → structverify-0.3.1}/README.md +0 -0
  16. {structverify-0.3.0 → structverify-0.3.1}/setup.cfg +0 -0
  17. {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/__init__.py +0 -0
  18. {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/adapter_trainer.py +0 -0
  19. {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/feedback_store.py +0 -0
  20. {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/kosis_crawler.py +0 -0
  21. {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/sample_builder.py +0 -0
  22. {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/synthetic_generator.py +0 -0
  23. {structverify-0.3.0 → structverify-0.3.1}/structverify/adaptation/update_embeddings.py +0 -0
  24. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/__init__.py +0 -0
  25. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/builder_agent.py +0 -0
  26. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/conformance_agent.py +0 -0
  27. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/dependency_planner.py +0 -0
  28. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/indexing_agent.py +0 -0
  29. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/indexing_planner.py +0 -0
  30. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/integration_example.py +0 -0
  31. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/loop.py +0 -0
  32. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/memory.py +0 -0
  33. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/planner.py +0 -0
  34. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/prompts/__init__.py +0 -0
  35. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/prompts/planner_prompts.py +0 -0
  36. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/prompts/reflect_prompts.py +0 -0
  37. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/reflect.py +0 -0
  38. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/runtime_agent.py +0 -0
  39. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/schemas.py +0 -0
  40. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/source_profiler.py +0 -0
  41. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/__init__.py +0 -0
  42. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/base.py +0 -0
  43. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/calculate.py +0 -0
  44. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/catalog_search.py +0 -0
  45. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/deep_explore.py +0 -0
  46. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/explore_catalog.py +0 -0
  47. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/fetch_evidence.py +0 -0
  48. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/finish.py +0 -0
  49. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/meta_explore.py +0 -0
  50. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/query_rewriter.py +0 -0
  51. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/read_original.py +0 -0
  52. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/tools/replan.py +0 -0
  53. {structverify-0.3.0 → structverify-0.3.1}/structverify/agent/workspace.py +0 -0
  54. {structverify-0.3.0 → structverify-0.3.1}/structverify/api.py +0 -0
  55. {structverify-0.3.0 → structverify-0.3.1}/structverify/config/default.yaml +0 -0
  56. {structverify-0.3.0 → structverify-0.3.1}/structverify/core/__init__.py +0 -0
  57. {structverify-0.3.0 → structverify-0.3.1}/structverify/core/config_loader.py +0 -0
  58. {structverify-0.3.0 → structverify-0.3.1}/structverify/core/pipeline.py +0 -0
  59. {structverify-0.3.0 → structverify-0.3.1}/structverify/core/schemas.py +0 -0
  60. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/__init__.py +0 -0
  61. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/_config.py +0 -0
  62. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/_llm.py +0 -0
  63. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate/__init__.py +0 -0
  64. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate/heuristic.py +0 -0
  65. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate/llm.py +0 -0
  66. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/candidate_scorer.py +0 -0
  67. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/claim_detector.py +0 -0
  68. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/claims/__init__.py +0 -0
  69. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/claims/worthiness.py +0 -0
  70. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/__init__.py +0 -0
  71. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/classify.py +0 -0
  72. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/preview.py +0 -0
  73. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain/registry.py +0 -0
  74. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/domain_classifier.py +0 -0
  75. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/__init__.py +0 -0
  76. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/candidate.py +0 -0
  77. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/claim_worthiness.py +0 -0
  78. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/domain.py +0 -0
  79. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts/schema.py +0 -0
  80. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/prompts_loader.py +0 -0
  81. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/__init__.py +0 -0
  82. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/expand.py +0 -0
  83. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/induce.py +0 -0
  84. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/regenerate.py +0 -0
  85. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/temporal_hints.py +0 -0
  86. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema/validate.py +0 -0
  87. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/schema_inductor.py +0 -0
  88. {structverify-0.3.0 → structverify-0.3.1}/structverify/detection/synthetic_generator.py +0 -0
  89. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/__init__.py +0 -0
  90. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/_config.py +0 -0
  91. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/_llm.py +0 -0
  92. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/explainer.py +0 -0
  93. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/fallback.py +0 -0
  94. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/formatters.py +0 -0
  95. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/__init__.py +0 -0
  96. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/match.py +0 -0
  97. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/mismatch.py +0 -0
  98. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/multihop.py +0 -0
  99. {structverify-0.3.0 → structverify-0.3.1}/structverify/explanation/prompts/unverifiable.py +0 -0
  100. {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/__init__.py +0 -0
  101. {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/claim_graph.py +0 -0
  102. {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/document_graph.py +0 -0
  103. {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/graph_builder.py +0 -0
  104. {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/graph_multihop.py +0 -0
  105. {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/graph_store.py +0 -0
  106. {structverify-0.3.0 → structverify-0.3.1}/structverify/graph/provenance.py +0 -0
  107. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/__init__.py +0 -0
  108. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/agent_memory.py +0 -0
  109. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/embedder.py +0 -0
  110. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/exemplar_store.py +0 -0
  111. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/normalizer.py +0 -0
  112. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/schema.py +0 -0
  113. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/storage/__init__.py +0 -0
  114. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/storage/jsonl_store.py +0 -0
  115. {structverify-0.3.0 → structverify-0.3.1}/structverify/memory/working_memory.py +0 -0
  116. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/Dockerfile.scraper +0 -0
  117. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/__init__.py +0 -0
  118. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/extractor.py +0 -0
  119. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/__init__.py +0 -0
  120. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/fields.py +0 -0
  121. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/markdown.py +0 -0
  122. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/models.py +0 -0
  123. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/ocr.py +0 -0
  124. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/pipeline.py +0 -0
  125. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/reader.py +0 -0
  126. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/pdf/scoring.py +0 -0
  127. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/scraper_sandbox.py +0 -0
  128. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/segmenter.py +0 -0
  129. {structverify-0.3.0 → structverify-0.3.1}/structverify/preprocessing/sir_builder.py +0 -0
  130. {structverify-0.3.0 → structverify-0.3.1}/structverify/progress.py +0 -0
  131. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/__init__.py +0 -0
  132. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/base.py +0 -0
  133. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/base_connector.py +0 -0
  134. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/catalog_ranker.py +0 -0
  135. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/catalog_search.py +0 -0
  136. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/chunking.py +0 -0
  137. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/custom_csv_source.py +0 -0
  138. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/custom_db_source.py +0 -0
  139. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/custom_docs_source.py +0 -0
  140. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/dimension_resolver.py +0 -0
  141. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/evidence_subgraph.py +0 -0
  142. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/kosis_connector.py +0 -0
  143. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/kosis_relevance.py +0 -0
  144. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/kosis_source.py +0 -0
  145. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/query_builder.py +0 -0
  146. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/registry.py +0 -0
  147. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/relevance_judge.py +0 -0
  148. {structverify-0.3.0 → structverify-0.3.1}/structverify/retrieval/row_matcher.py +0 -0
  149. {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/__init__.py +0 -0
  150. {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/db_manager.py +0 -0
  151. {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/dwh_manager.py +0 -0
  152. {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/init_db.py +0 -0
  153. {structverify-0.3.0 → structverify-0.3.1}/structverify/storage/raw_storage.py +0 -0
  154. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/__init__.py +0 -0
  155. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/dataset.py +0 -0
  156. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/evalgate.py +0 -0
  157. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/generate.py +0 -0
  158. {structverify-0.3.0 → structverify-0.3.1}/structverify/training/tasks.py +0 -0
  159. {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/__init__.py +0 -0
  160. {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/embedding_client.py +0 -0
  161. {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/llm_client.py +0 -0
  162. {structverify-0.3.0 → structverify-0.3.1}/structverify/utils/logger.py +0 -0
  163. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/__init__.py +0 -0
  164. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/_config.py +0 -0
  165. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/adapters.py +0 -0
  166. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/conformance.py +0 -0
  167. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/decide_verdict.py +0 -0
  168. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/decide_verdict_agent.py +0 -0
  169. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/growth_diff.py +0 -0
  170. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/row_match.py +0 -0
  171. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/units.py +0 -0
  172. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/verdict_thresholds.py +0 -0
  173. {structverify-0.3.0 → structverify-0.3.1}/structverify/verification/verifier.py +0 -0
  174. {structverify-0.3.0 → structverify-0.3.1}/structverify.egg-info/dependency_links.txt +0 -0
  175. {structverify-0.3.0 → structverify-0.3.1}/structverify.egg-info/requires.txt +0 -0
  176. {structverify-0.3.0 → structverify-0.3.1}/structverify.egg-info/top_level.txt +0 -0
  177. {structverify-0.3.0 → structverify-0.3.1}/tests/test_api.py +0 -0
  178. {structverify-0.3.0 → structverify-0.3.1}/tests/test_catalog_search_embedding.py +0 -0
  179. {structverify-0.3.0 → structverify-0.3.1}/tests/test_custom_csv_source.py +0 -0
  180. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_candidate_heuristic.py +0 -0
  181. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_config.py +0 -0
  182. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_domain_preview.py +0 -0
  183. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_domain_registry.py +0 -0
  184. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_expand_temporal.py +0 -0
  185. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_init_exports.py +0 -0
  186. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_llm.py +0 -0
  187. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_prompts_loader.py +0 -0
  188. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_validate.py +0 -0
  189. {structverify-0.3.0 → structverify-0.3.1}/tests/test_detection_worthiness.py +0 -0
  190. {structverify-0.3.0 → structverify-0.3.1}/tests/test_embedding_client.py +0 -0
  191. {structverify-0.3.0 → structverify-0.3.1}/tests/test_embedding_config_threading.py +0 -0
  192. {structverify-0.3.0 → structverify-0.3.1}/tests/test_explore_catalog_embedding.py +0 -0
  193. {structverify-0.3.0 → structverify-0.3.1}/tests/test_is_table_relevant.py +0 -0
  194. {structverify-0.3.0 → structverify-0.3.1}/tests/test_kosis_crawler_embedding.py +0 -0
  195. {structverify-0.3.0 → structverify-0.3.1}/tests/test_kosis_guards.py +0 -0
  196. {structverify-0.3.0 → structverify-0.3.1}/tests/test_kosis_json_load_pgvector.py +0 -0
  197. {structverify-0.3.0 → structverify-0.3.1}/tests/test_library_entry_smoke.py +0 -0
  198. {structverify-0.3.0 → structverify-0.3.1}/tests/test_llm_client.py +0 -0
  199. {structverify-0.3.0 → structverify-0.3.1}/tests/test_pipeline.py +0 -0
  200. {structverify-0.3.0 → structverify-0.3.1}/tests/test_runtime_datasources.py +0 -0
  201. {structverify-0.3.0 → structverify-0.3.1}/tests/test_step1-9_v3_pipeline.py +0 -0
  202. {structverify-0.3.0 → structverify-0.3.1}/tests/test_step1_6_detection.py +0 -0
  203. {structverify-0.3.0 → structverify-0.3.1}/tests/test_step1_9_pipeline.py +0 -0
  204. {structverify-0.3.0 → structverify-0.3.1}/tests/test_step9_explainer.py +0 -0
  205. {structverify-0.3.0 → structverify-0.3.1}/tests/test_update_embeddings_embedding.py +0 -0
  206. {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.0
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.0"
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" }
@@ -54,7 +54,7 @@ _TRAINING_SYMBOLS = frozenset({
54
54
  })
55
55
 
56
56
  __all__ = sorted(_API_SYMBOLS | _PIPELINE_SYMBOLS | _PROGRESS_SYMBOLS | _TRAINING_SYMBOLS)
57
- __version__ = "0.3.0"
57
+ __version__ = "0.3.1"
58
58
 
59
59
 
60
60
  def __getattr__(name: str):
@@ -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
- spikes = [losses[i][0] for i in range(1, len(vals))
77
- if vals[i] > vals[i - 1] * 2.0 and vals[i - 1] > 1e-6]
78
- if spikes:
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 {spikes[:3]} — 특정 배치 의심")
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) -> dict:
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", # TrainDoctor용 촘촘한 loss
82
+ "--steps-per-report", "1", # 🩺 실시간 감독용 촘촘한 loss
83
+ "--save-every", "50", # 조기 종료 대비 어댑터 주기 저장
80
84
  ]
81
85
  print("[train_mlx] 실행:", " ".join(cmd))
82
- proc = subprocess.run(cmd, capture_output=True, text=True)
83
- log = proc.stdout + "\n" + proc.stderr
84
- print(log[-2000:])
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(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.0
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