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