File size: 7,452 Bytes
07cb7d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""
有监督 Keras 模型任务的固定流水线。

这个文件放分类、分割这类“数据源 + 模型构建器 + 训练规则”任务共用的主流程。
任务入口只负责提供数据源、模型构建器和训练规则,具体训练、导出和样例测试由这里统一调度。
"""

from dataclasses import dataclass
from pathlib import Path

from deep_learning.data.spec import SupervisedDataSource
from deep_learning.env.keras import enable_mixed_precision
from deep_learning.env.logger import log
from deep_learning.env.resolve import resolve_saved
from deep_learning.pipeline.specs import Pipeline
from deep_learning.pipeline.specs.configs import CheckpointLoadRules, TrainingRule
from deep_learning.pipeline.services.logging_config import log_config
from deep_learning.models.spec import SupervisedModelBuilder
from deep_learning.pipeline.context import InferenceArtifactState, TrainingArtifactState
from deep_learning.pipeline.env.const import ENV
from deep_learning.pipeline.services import CheckpointService, build_runtime
from deep_learning.pipeline.services.training_callbacks import build_common_callbacks
from deep_learning.pipeline.stages.export import SaveKerasModelStage


@dataclass
class SupervisedModelPipeline(Pipeline):
    """
    有监督 Keras 任务主入口。

    这个类把训练、导出和样例测试固定成显式动作,分类和分割任务只需要替换数据源、
    模型构建器。
    """

    name: str
    data_source: SupervisedDataSource
    model_builder: SupervisedModelBuilder
    training_rule: TrainingRule
    checkpoint_load_rules: CheckpointLoadRules = None
    task_dir: Path = None

    def __post_init__(self):
        runtime = build_runtime(self.name, self.task_dir)
        self.task_dir = runtime.task_dir
        self._runtime_env = runtime
        if self.checkpoint_load_rules is None:
            super().__setattr__("checkpoint_load_rules", CheckpointLoadRules())

    def __setattr__(self, name, value):
        if "_runtime_env" in self.__dict__ and not name.startswith("_"):
            public_fields = {"data_source", "model_builder"}
            if name not in public_fields:
                raise AttributeError(f"cannot assign to field '{name}'")
        super().__setattr__(name, value)

    def train(self):
        with log():
            enable_mixed_precision()

        with log():
            self.log_config()

        with log():
            self._runtime_env.checkpoint_dir.mkdir(parents=True, exist_ok=True)

        with log():
            training_state = self._load_or_build_training_state(
                checkpoint_rule=self.checkpoint_load_rules.resolve_train_rule(
                    default_dirs=[self._runtime_env.checkpoint_dir]
                ),
                checkpoint_must=False
            )

        with log("构建训练计划"):
            self.model_builder.compile_training_model(training_state.training_artifact.model)
            train_ds, validation_ds = self.data_source.training_ds()
            callbacks = build_common_callbacks(
                runtime=self._runtime_env,
                checkpoint_filename="model_epoch_{epoch:03d}.weights.h5",
                save_weights_only=True
            )
            if hasattr(training_state.training_artifact.model, "summary"):
                training_state.training_artifact.model.summary()

        with log("开始训练", "训练结束"):
            return training_state.training_artifact.model.fit(
                train_ds,
                validation_data=validation_ds,
                epochs=self.training_rule.epochs,
                steps_per_epoch=self.training_rule.steps_per_epoch,
                callbacks=callbacks
            ).history

    def export_model(self) -> Path:
        self.log_config()
        self._runtime_env.checkpoint_dir.mkdir(parents=True, exist_ok=True)
        training_state = self._load_or_build_training_state(
            checkpoint_rule=self.checkpoint_load_rules.resolve_export_rule(
                default_dirs=[self._runtime_env.checkpoint_dir]
            ),
            checkpoint_must=True
        )
        inference_artifact = self.model_builder.convert_to_inference_artifact(
            training_artifact=training_state.training_artifact
        )
        inference_state = InferenceArtifactState(
            inference_artifact=inference_artifact,
            inference_resource=None
        )
        export_stage = SaveKerasModelStage(resolve_saved(f"models/{self.name}"))
        return export_stage.run(
            self._runtime_env,
            inference_state,
            training_state.checkpoint_epoch
        )

    def test_examples(self, checkpoint: str = "test") -> None:
        self.log_config()
        if checkpoint == "test":
            checkpoint_rule = self.checkpoint_load_rules.resolve_test_rule(
                default_dirs=[resolve_saved(f"models/{self.name}")]
            )
            if checkpoint_rule["suffix"] is None:
                checkpoint_rule["suffix"] = ".keras"
            checkpoint_service = CheckpointService()
            checkpoint_path, _ = checkpoint_service.resolve_existing_checkpoint(
                checkpoint_rule=checkpoint_rule,
                not_found_message="未找到任何导出模型文件"
            )
            artifact = self.model_builder.load_inference_artifact(checkpoint_path)
            checkpoint_service.log_loaded_checkpoint(checkpoint_path)
            self.data_source.test_examples(artifact.model)
            return

        training_state = self._load_or_build_training_state(
            checkpoint_rule=self.checkpoint_load_rules.resolve_train_rule(
                default_dirs=[self._runtime_env.checkpoint_dir]
            ),
            checkpoint_must=True
        )
        inference_artifact = self.model_builder.convert_to_inference_artifact(
            training_state.training_artifact
        )
        self.data_source.test_examples(
            inference_artifact.model
        )

    def log_config(self):
        self._runtime_env.log_dir.mkdir(parents=True, exist_ok=True)
        config_path = self._runtime_env.log_dir / "config.txt"
        output = log_config(self, config_path, header=f"ENV[{ENV}]")
        print(output)
        print(f"配置已保存到: {config_path}")

    def _load_or_build_training_state(
        self,
        checkpoint_rule: dict,
        checkpoint_must: bool
    ) -> TrainingArtifactState:
        checkpoint_service = CheckpointService()
        checkpoint_path, checkpoint_epoch = checkpoint_service.resolve_checkpoint(**checkpoint_rule)
        if checkpoint_path is not None:
            checkpoint_service.log_loading_checkpoint(checkpoint_path, checkpoint_epoch)
            training_artifact = self.model_builder.build_training_artifact()
            training_artifact.model.load_weights(str(checkpoint_path))
            checkpoint_service.log_loaded_checkpoint(checkpoint_path)
            return TrainingArtifactState(
                training_artifact=training_artifact,
                checkpoint_epoch=checkpoint_epoch
            )

        if checkpoint_must:
            raise ValueError(f"目录 {self._runtime_env.checkpoint_dir} 中未找到检查点文件")

        print("未找到检查点,使用新模型")
        return TrainingArtifactState(
            training_artifact=self.model_builder.build_training_artifact()
        )