Kronos微调训练详解

209 阅读5分钟

第7章:微调训练详解

📖 章节概述

欢迎来到第7章!在这一章中,我们将深入学习Kronos模型的微调技术。微调是将预训练模型适应特定任务或市场的关键技术,能够显著提升模型在特定场景下的表现。

⏱️ 预计学习时间:10小时

🎯 学习目标:掌握Kronos模型的完整微调流程

📋 主要内容:Qlib集成、CSV微调、分布式训练、最佳实践


7.1 微调训练基础

7.1.1 什么是模型微调

微调(Fine-tuning)是在预训练模型的基础上,使用特定任务的数据继续训练模型的过程。对于Kronos来说,微调可以:

  • 提升特定市场表现:适应不同金融市场的特点

  • 优化预测精度:针对特定时间周期和资产类型

  • 个性化模型:基于机构自有数据进行定制

  • 降低数据需求:相比从头训练需要更少的数据

7.1.2 微调的技术优势

🎯 数据效率
  • 预训练模型已经学习了通用的金融模式
  • 只需要相对较少的特定数据进行适配
  • 大大降低了训练成本和时间
🎯 性能提升
  • 专注学习特定市场的特征
  • 适应当前的市场环境和结构
  • 提高在特定任务上的准确性
🎯 灵活性
  • 可以针对不同的资产类型进行微调
  • 支持不同的预测目标和时间尺度
  • 便于持续更新和改进

7.1.3 微调 vs 从头训练

特性微调从头训练

| 数据需求 | 较少(数千-数万条) | 大量(数百万条) |

| 训练时间 | 较短(几小时-几天) | 很长(几周-几个月) |

| 计算资源 | 中等(单GPU-多GPU) | 大量(大规模集群) |

| 泛化能力 | 保留通用知识 | 需要从零学习 |

| 技术门槛 | 较低 | 很高 |


7.2 Qlib集成微调管道

7.2.1 Qlib框架介绍

Qlib是微软开源的量化投资平台,提供了完整的量化研究和交易框架:

🔧 核心功能
  • 数据管理:统一的金融数据存储和访问

  • 因子计算:丰富的技术指标和因子库

  • 模型训练:内置的机器学习训练框架

  • 回测系统:专业的策略回测和评估

🌐 支持的 数据源
  • 中国A股市场
  • 美国股票市场
  • 加密货币市场
  • 外汇市场

7.2.2 Qlib数据准备

📊 数据获取和存储
import qlib
from qlib.data import D
from qlib.constant import REG_CN
from qlib.utils import init_instance_by_config

# 初始化Qlib
qlib.init(provider_uri='~/.qlib/qlib_data/cn_data', region=REG_CN)

# 配置数据提供者
data_provider = init_instance_by_config({
    'class': 'LocalDataProvider',
    'module_path': 'qlib.data.dataset',
    'kwargs': {
        'region': REG_CN,
        'provider_uri': '~/.qlib/qlib_data/cn_data'
    }
})

# 获取股票数据
instruments = D.instruments(market='all')
fields = ['$close', '$volume', '$high', '$low', '$open', '$factor']
start_time = '2010-01-01'
end_time = '2023-12-31'

# 获取数据
data = D.features(instruments, fields, start_time=start_time, end_time=end_time)
print(f"获取数据形状: {data.shape}")
print(f"数据列: {data.columns.tolist()}")
📈 数据预处理
import pandas as pd
import numpy as np

class QlibDataPreprocessor:
    """Qlib数据预处理器"""

    def __init__(self):
        self.scaler = None
        self.feature_columns = None

    def prepare_training_data(self, data, target_stock='000001.SZ',
                           lookback_window=252, prediction_horizon=20):
        """
        准备训练数据

        Args:
            data: Qlib格式的数据
            target_stock: 目标股票代码
            lookback_window: 历史窗口
            prediction_horizon: 预测时间步长

        Returns:
            处理后的训练数据
        """
        print(f"准备{target_stock}的训练数据...")

        # 提取目标股票数据
        target_data = data.loc[target_stock].copy()

        # 计算技术指标
        target_data = self.calculate_technical_indicators(target_data)

        # 计算收益率
        target_data['returns'] = target_data['$close'].pct_change()

        # 创建特征和标签
        features = []
        labels = []

        for i in range(lookback_window, len(target_data) - prediction_horizon):
            # 特征:历史数据窗口
            feature_window = target_data.iloc[i-lookback_window:i]
            feature_vector = self.extract_features(feature_window)
            features.append(feature_vector)

            # 标签:未来收益率
            future_returns = target_data['returns'].iloc[i:i+prediction_horizon]
            label = self.calculate_label(future_returns)
            labels.append(label)

        features = np.array(features)
        labels = np.array(labels)

        print(f"特征矩阵形状: {features.shape}")
        print(f"标签向量形状: {labels.shape}")

        return features, labels, target_data.index[lookback_window:len(target_data)-prediction_horizon]

    def calculate_technical_indicators(self, data):
        """计算技术指标"""
        # 移动平均
        data['MA5'] = data['$close'].rolling(window=5).mean()
        data['MA20'] = data['$close'].rolling(window=20).mean()
        data['MA60'] = data['$close'].rolling(window=60).mean()

        # RSI
        delta = data['$close'].diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
        rs = gain / loss
        data['RSI'] = 100 - (100 / (1 + rs))

        # MACD
        exp1 = data['$close'].ewm(span=12).mean()
        exp2 = data['$close'].ewm(span=26).mean()
        data['MACD'] = exp1 - exp2
        data['MACD_signal'] = data['MACD'].ewm(span=9).mean()

        # 布林带
        ma20 = data['$close'].rolling(window=20).mean()
        std20 = data['$close'].rolling(window=20).std()
        data['BOLL_upper'] = ma20 + (std20 * 2)
        data['BOLL_lower'] = ma20 - (std20 * 2)

        return data

    def extract_features(self, window_data):
        """从历史窗口提取特征"""
        features = []

        # 价格特征
        features.extend([
            window_data['$close'].iloc[-1],
            window_data['$close'].pct_change(5).iloc[-1],
            window_data['$close'].pct_change(20).iloc[-1],
        ])

        # 移动平均特征
        features.extend([
            window_data['MA5'].iloc[-1],
            window_data['MA20'].iloc[-1],
            window_data['MA60'].iloc[-1],
            (window_data['$close'].iloc[-1] - window_data['MA20'].iloc[-1]) / window_data['MA20'].iloc[-1]
        ])

        # 技术指标特征
        features.extend([
            window_data['RSI'].iloc[-1],
            window_data['MACD'].iloc[-1],
            window_data['MACD_signal'].iloc[-1],
        ])

        # 波动率特征
        returns_20 = window_data['$close'].pct_change(20)
        features.extend([
            returns_20.std(),
            returns_20.skew(),
            returns_20.kurtosis()
        ])

        # 成交量特征
        features.extend([
            window_data['$volume'].iloc[-1],
            window_data['$volume'].rolling(5).mean().iloc[-1],
            window_data['$volume'].rolling(20).mean().iloc[-1],
        ])

        return np.array(features)

    def calculate_label(self, future_returns):
        """计算标签(分类任务)"""
        mean_return = future_returns.mean()

        # 分类标签:0=下跌, 1=横盘, 2=上涨
        if mean_return < -0.02:
            return 0
        elif mean_return > 0.02:
            return 2
        else:
            return 1

# 使用示例
if __name__ == "__main__":
    # 初始化预处理器
    preprocessor = QlibDataPreprocessor()

    # 准备数据(这里需要实际的Qlib数据)
    print("请确保已正确配置Qlib环境")
    print("运行示例需要真实的Qlib数据")

7.2.3 微调数据集类

import torch
from torch.utils.data import Dataset, DataLoader
import numpy as np

class FinetuningDataset(Dataset):
    """微调数据集类"""

    def __init__(self, features, labels, timestamps=None):
        """
        初始化数据集

        Args:
            features: 特征矩阵 (n_samples, n_features)
            labels: 标签向量 (n_samples,)
            timestamps: 时间戳列表 (n_samples,)
        """
        self.features = torch.FloatTensor(features)
        self.labels = torch.LongTensor(labels)
        self.timestamps = timestamps or list(range(len(features)))

        # 标准化特征
        self.feature_mean = torch.mean(self.features, dim=0)
        self.feature_std = torch.std(self.features, dim=0)
        self.feature_std = torch.clamp(self.feature_std, min=1e-8)  # 避免除零

        self.features = (self.features - self.feature_mean) / self.feature_std

    def __len__(self):
        return len(self.features)

    def __getitem__(self, idx):
        return {
            'features': self.features[idx],
            'labels': self.labels[idx],
            'timestamp': self.timestamps[idx]
        }

    def get_normalization_params(self):
        """获取标准化参数"""
        return self.feature_mean, self.feature_std

class TimeSeriesDataLoader:
    """时间序列数据加载器"""

    def __init__(self, dataset, batch_size=32, shuffle=True, drop_last=False):
        """
        初始化数据加载器

        Args:
            dataset: 数据集
            batch_size: 批次大小
            shuffle: 是否打乱数据
            drop_last: 是否丢弃最后一个不完整的批次
        """
        self.dataset = dataset
        self.batch_size = batch_size
        self.shuffle = shuffle
        self.drop_last = drop_last

        self.indices = list(range(len(dataset)))
        self.current_position = 0

    def __iter__(self):
        if self.shuffle:
            np.random.shuffle(self.indices)
        self.current_position = 0
        return self

    def __next__(self):
        if self.current_position >= len(self.dataset):
            raise StopIteration

        # 收集批次数据
        batch_indices = []
        while (len(batch_indices) < self.batch_size and
               self.current_position < len(self.dataset)):
            batch_indices.append(self.indices[self.current_position])
            self.current_position += 1

        # 如果不足一个批次且drop_last为True,抛出异常
        if len(batch_indices) < self.batch_size and self.drop_last:
            raise StopIteration

        # 构建批次数据
        batch_features = []
        batch_labels = []
        batch_timestamps = []

        for idx in batch_indices:
            item = self.dataset[idx]
            batch_features.append(item['features'])
            batch_labels.append(item['labels'])
            batch_timestamps.append(item['timestamp'])

        return {
            'features': torch.stack(batch_features),
            'labels': torch.stack(batch_labels),
            'timestamps': batch_timestamps
        }

# 数据集使用示例
def create_training_pipeline(data, target_stock='000001.SZ'):
    """创建训练流水线"""
    print("创建训练流水线...")

    # 数据预处理
    preprocessor = QlibDataPreprocessor()
    features, labels, timestamps = preprocessor.prepare_training_data(
        data, target_stock, lookback_window=252, prediction_horizon=20
    )

    # 数据集划分
    train_ratio, val_ratio, test_ratio = 0.7, 0.2, 0.1
    total_samples = len(features)

    train_end = int(total_samples * train_ratio)
    val_end = int(total_samples * (train_ratio + val_ratio))

    train_features = features[:train_end]
    train_labels = labels[:train_end]
    train_timestamps = timestamps[:train_end]

    val_features = features[train_end:val_end]
    val_labels = labels[train_end:val_end]
    val_timestamps = timestamps[train_end:val_end]

    test_features = features[val_end:]
    test_labels = labels[val_end:]
    test_timestamps = timestamps[val_end:]

    print(f"训练集大小: {len(train_features)}")
    print(f"验证集大小: {len(val_features)}")
    print(f"测试集大小: {len(test_features)}")

    # 创建数据集
    train_dataset = FinetuningDataset(train_features, train_labels, train_timestamps)
    val_dataset = FinetuningDataset(val_features, val_labels, val_timestamps)
    test_dataset = FinetuningDataset(test_features, test_labels, test_timestamps)

    # 创建数据加载器
    train_loader = TimeSeriesDataLoader(train_dataset, batch_size=32, shuffle=True)
    val_loader = TimeSeriesDataLoader(val_dataset, batch_size=32, shuffle=False)
    test_loader = TimeSeriesDataLoader(test_dataset, batch_size=32, shuffle=False)

    return train_loader, val_loader, test_loader, preprocessor

if __name__ == "__main__":
    print("微调数据集类定义完成")

7.3 微调模型架构

7.3.1 模型修改策略

微调Kronos模型时,我们需要考虑以下几个关键方面:

🎯 预测头设计
  • 分类任务:涨跌方向预测

  • 回归任务:价格或收益率预测

  • 多任务学习:同时预测多个目标

🎯 输入适配
  • 特征维度调整:适应不同的输入特征

  • 序列长度调整:支持不同的历史窗口

  • 多模态输入:结合价格和文本信息

7.3.2 微调模型实现

import torch
import torch.nn as nn
import torch.nn.functional as F
from model import Kronos, KronosTokenizer

class FinetuningKronos(nn.Module):
    """微调版Kronos模型"""

    def __init__(self, base_model, num_classes=3, feature_dim=32, hidden_dim=256):
        """
        初始化微调模型

        Args:
            base_model: 预训练的Kronos模型
            num_classes: 分类数量
            feature_dim: 特征维度
            hidden_dim: 隐藏层维度
        """
        super().__init__()

        self.base_model = base_model
        self.num_classes = num_classes

        # 冻结预训练模型的参数(可选)
        self.freeze_base = False

        # 特征处理层
        self.feature_processor = nn.Sequential(
            nn.Linear(feature_dim, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.1),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.1)
        )

        # 分类头
        self.classifier = nn.Sequential(
            nn.Linear(hidden_dim + base_model.d_model, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(hidden_dim, num_classes)
        )

        # 回归头(可选)
        self.regressor = nn.Sequential(
            nn.Linear(hidden_dim + base_model.d_model, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(hidden_dim, 1)
        )

    def freeze_base_parameters(self):
        """冻结基础模型参数"""
        for param in self.base_model.parameters():
            param.requires_grad = False
        self.freeze_base = True
        print("基础模型参数已冻结")

    def unfreeze_base_parameters(self):
        """解冻基础模型参数"""
        for param in self.base_model.parameters():
            param.requires_grad = True
        self.freeze_base = False
        print("基础模型参数已解冻")

    def forward(self, input_ids, attention_mask=None, features=None, mode='classification'):
        """
        前向传播

        Args:
            input_ids: 输入token IDs
            attention_mask: 注意力掩码
            features: 额外特征
            mode: 模式 ('classification' 或 'regression')
        """
        # 获取基础模型输出
        base_output = self.base_model(input_ids, attention_mask=attention_mask)

        # 假设base_output包含最后的隐藏状态
        if isinstance(base_output, dict):
            hidden_states = base_output['last_hidden_state']
        else:
            hidden_states = base_output

        # 取最后一个时间步的隐藏状态
        last_hidden = hidden_states[:, -1, :]  # (batch_size, hidden_dim)

        # 处理额外特征
        if features is not None:
            processed_features = self.feature_processor(features)
            combined_features = torch.cat([last_hidden, processed_features], dim=-1)
        else:
            combined_features = last_hidden

        # 分类预测
        if mode == 'classification':
            logits = self.classifier(combined_features)
            return logits

        # 回归预测
        elif mode == 'regression':
            predictions = self.regressor(combined_features)
            return predictions

        else:
            raise ValueError(f"不支持的模式: {mode}")

    def predict_proba(self, input_ids, attention_mask=None, features=None):
        """预测概率分布"""
        self.eval()
        with torch.no_grad():
            logits = self.forward(input_ids, attention_mask, features, 'classification')
            probabilities = F.softmax(logits, dim=-1)
            return probabilities

    def predict(self, input_ids, attention_mask=None, features=None):
        """预测类别"""
        self.eval()
        with torch.no_grad():
            logits = self.forward(input_ids, attention_mask, features, 'classification')
            predictions = torch.argmax(logits, dim=-1)
            return predictions

class KronosFinetuningTrainer:
    """Kronos微调训练器"""

    def __init__(self, model, tokenizer, device='cuda'):
        """
        初始化训练器

        Args:
            model: 微调模型
            tokenizer: 分词器
            device: 计算设备
        """
        self.model = model.to(device)
        self.tokenizer = tokenizer
        self.device = device

        # 优化器
        self.optimizer = None
        self.scheduler = None

        # 损失函数
        self.criterion = nn.CrossEntropyLoss()
        self.regression_criterion = nn.MSELoss()

        # 训练历史
        self.train_losses = []
        self.val_losses = []
        self.val_accuracies = []

    def setup_optimizer(self, learning_rate=1e-4, weight_decay=1e-5):
        """设置优化器"""
        # 分离基础模型和新增层的学习率
        base_params = []
        new_params = []

        for name, param in self.model.named_parameters():
            if 'base_model' in name:
                base_params.append(param)
            else:
                new_params.append(param)

        self.optimizer = torch.optim.AdamW([
            {'params': base_params, 'lr': learning_rate * 0.1},  # 基础模型使用较小学习率
            {'params': new_params, 'lr': learning_rate}
        ], weight_decay=weight_decay)

        # 学习率调度器
        self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
            self.optimizer, T_max=100, eta_min=1e-6
        )

        print("优化器设置完成")

    def train_epoch(self, train_loader, epoch):
        """训练一个epoch"""
        self.model.train()
        total_loss = 0
        correct = 0
        total = 0

        for batch_idx, batch in enumerate(train_loader):
            # 移动数据到设备
            features = batch['features'].to(self.device)
            labels = batch['labels'].to(self.device)

            # 将数值特征转换为token序列(简化处理)
            # 这里需要根据实际需求设计合适的转换方法
            input_ids = self._features_to_input_ids(features)
            attention_mask = torch.ones_like(input_ids)

            # 前向传播
            self.optimizer.zero_grad()
            outputs = self.model(input_ids, attention_mask, features, 'classification')

            # 计算损失
            loss = self.criterion(outputs, labels)

            # 反向传播
            loss.backward()

            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)

            # 优化器步骤
            self.optimizer.step()

            # 统计
            total_loss += loss.item()
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

            if batch_idx % 100 == 0:
                print(f'Epoch {epoch}, Batch {batch_idx}, Loss: {loss.item():.6f}')

        avg_loss = total_loss / len(train_loader)
        accuracy = 100. * correct / total

        return avg_loss, accuracy

    def validate_epoch(self, val_loader):
        """验证一个epoch"""
        self.model.eval()
        total_loss = 0
        correct = 0
        total = 0

        with torch.no_grad():
            for batch in val_loader:
                features = batch['features'].to(self.device)
                labels = batch['labels'].to(self.device)

                input_ids = self._features_to_input_ids(features)
                attention_mask = torch.ones_like(input_ids)

                outputs = self.model(input_ids, attention_mask, features, 'classification')
                loss = self.criterion(outputs, labels)

                total_loss += loss.item()
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()

        avg_loss = total_loss / len(val_loader)
        accuracy = 100. * correct / total

        return avg_loss, accuracy

    def _features_to_input_ids(self, features):
        """将特征转换为输入ID(简化版本)"""
        # 这是一个简化的实现,实际应用中需要更复杂的转换逻辑
        # 例如:使用预训练的编码器或特定的量化方法

        batch_size = features.size(0)
        seq_length = 50  # 固定序列长度

        # 创建随机ID(仅用于演示)
        input_ids = torch.randint(0, 1000, (batch_size, seq_length), device=self.device)

        return input_ids

    def train(self, train_loader, val_loader, epochs=50, save_dir='./checkpoints'):
        """完整训练流程"""
        print(f"开始微调训练,总epoch数: {epochs}")

        # 创建保存目录
        import os
        os.makedirs(save_dir, exist_ok=True)

        best_val_accuracy = 0.0

        for epoch in range(epochs):
            print(f'\nEpoch {epoch+1}/{epochs}')
            print('-' * 50)

            # 训练
            train_loss, train_acc = self.train_epoch(train_loader, epoch)
            self.train_losses.append(train_loss)

            # 验证
            val_loss, val_acc = self.validate_epoch(val_loader)
            self.val_losses.append(val_loss)
            self.val_accuracies.append(val_acc)

            # 学习率调度
            self.scheduler.step()

            print(f'Train Loss: {train_loss:.6f}, Train Acc: {train_acc:.2f}%')
            print(f'Val Loss: {val_loss:.6f}, Val Acc: {val_acc:.2f}%')

            # 保存最佳模型
            if val_acc > best_val_accuracy:
                best_val_accuracy = val_acc
                torch.save({
                    'epoch': epoch,
                    'model_state_dict': self.model.state_dict(),
                    'optimizer_state_dict': self.optimizer.state_dict(),
                    'val_accuracy': val_acc,
                }, os.path.join(save_dir, 'best_model.pth'))
                print(f'保存最佳模型,验证准确率: {val_acc:.2f}%')

            # 定期保存检查点
            if (epoch + 1) % 10 == 0:
                torch.save({
                    'epoch': epoch,
                    'model_state_dict': self.model.state_dict(),
                    'optimizer_state_dict': self.optimizer.state_dict(),
                    'val_accuracy': val_acc,
                }, os.path.join(save_dir, f'checkpoint_epoch_{epoch+1}.pth'))

        print(f'\n训练完成!最佳验证准确率: {best_val_accuracy:.2f}%')

        return self.train_losses, self.val_losses, self.val_accuracies

# 训练器使用示例
if __name__ == "__main__":
    print("微调模型类定义完成")
    print("请结合Qlib数据使用这些类进行实际的微调训练")

7.4 完整微调流程

7.4.1 端到端微调管道

import os
import json
import torch
import argparse
from datetime import datetime
import sys
sys.path.append('../../../')

try:
    import qlib
    from qlib.data import D
    from qlib.constant import REG_CN
    QLIB_AVAILABLE = True
except ImportError:
    QLIB_AVAILABLE = False
    print("Qlib不可用,请安装: pip install pyqlib")

from model import Kronos, KronosTokenizer
from ch07_finetuning.dataset import create_training_pipeline
from ch07_finetuning.model import FinetuningKronos, KronosFinetuningTrainer

class FinetuningPipeline:
    """完整的微调管道"""

    def __init__(self, config):
        """
        初始化微调管道

        Args:
            config: 配置字典
        """
        self.config = config
        self.results = {}

    def setup_environment(self):
        """设置环境"""
        print("🔧 设置微调环境...")

        # 设置设备
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        print(f"使用设备: {self.device}")

        # 创建保存目录
        os.makedirs(self.config['save_dir'], exist_ok=True)
        os.makedirs(self.config['log_dir'], exist_ok=True)

        print("✅ 环境设置完成")

    def load_base_model(self):
        """加载预训练模型"""
        print("📦 加载预训练模型...")

        # 加载分词器
        self.tokenizer = KronosTokenizer.from_pretrained(
            self.config['tokenizer_name']
        )

        # 加载基础模型
        self.base_model = Kronos.from_pretrained(
            self.config['base_model_name']
        )

        # 创建微调模型
        self.model = FinetuningKronos(
            base_model=self.base_model,
            num_classes=self.config['num_classes'],
            feature_dim=self.config['feature_dim'],
            hidden_dim=self.config['hidden_dim']
        )

        # 冻结基础模型参数(可选)
        if self.config['freeze_base']:
            self.model.freeze_base_parameters()

        print(f"✅ 模型加载完成")
        print(f"模型参数数量: {sum(p.numel() for p in self.model.parameters()):,}")

    def prepare_data(self):
        """准备数据"""
        print("📊 准备微调数据...")

        if not QLIB_AVAILABLE:
            print("❌ Qlib不可用,无法准备真实数据")
            return None, None, None

        try:
            # 初始化Qlib
            qlib.init(
                provider_uri=self.config['qlib_data_path'],
                region=self.config['qlib_region']
            )

            # 获取数据
            instruments = D.instruments(market=self.config['qlib_market'])
            fields = self.config['qlib_fields']

            data = D.features(
                instruments,
                fields,
                start_time=self.config['start_time'],
                end_time=self.config['end_time']
            )

            # 创建训练流水线
            train_loader, val_loader, test_loader, preprocessor = create_training_pipeline(
                data,
                target_stock=self.config['target_stock']
            )

            self.preprocessor = preprocessor

            print(f"✅ 数据准备完成")
            print(f"训练批次数: {len(train_loader)}")
            print(f"验证批次数: {len(val_loader)}")
            print(f"测试批次数: {len(test_loader)}")

            return train_loader, val_loader, test_loader

        except Exception as e:
            print(f"❌ 数据准备失败: {e}")
            return None, None, None

    def setup_training(self):
        """设置训练"""
        print("🚀 设置训练环境...")

        # 创建训练器
        self.trainer = KronosFinetuningTrainer(
            model=self.model,
            tokenizer=self.tokenizer,
            device=self.device
        )

        # 设置优化器
        self.trainer.setup_optimizer(
            learning_rate=self.config['learning_rate'],
            weight_decay=self.config['weight_decay']
        )

        print("✅ 训练环境设置完成")

    def train_model(self, train_loader, val_loader):
        """训练模型"""
        print("🎯 开始模型微调...")

        # 训练模型
        train_losses, val_losses, val_accuracies = self.trainer.train(
            train_loader=train_loader,
            val_loader=val_loader,
            epochs=self.config['epochs'],
            save_dir=self.config['save_dir']
        )

        # 保存训练历史
        self.results['train_losses'] = train_losses
        self.results['val_losses'] = val_losses
        self.results['val_accuracies'] = val_accuracies

        print("✅ 模型微调完成")

        return train_losses, val_losses, val_accuracies

    def evaluate_model(self, test_loader):
        """评估模型"""
        print("📊 评估模型性能...")

        val_loss, val_acc = self.trainer.validate_epoch(test_loader)

        print(f"测试集损失: {val_loss:.6f}")
        print(f"测试集准确率: {val_acc:.2f}%")

        self.results['test_loss'] = val_loss
        self.results['test_accuracy'] = val_acc

        return val_loss, val_acc

    def save_results(self):
        """保存结果"""
        print("💾 保存训练结果...")

        # 保存配置
        config_path = os.path.join(self.config['save_dir'], 'config.json')
        with open(config_path, 'w') as f:
            json.dump(self.config, f, indent=2)

        # 保存结果
        results_path = os.path.join(self.config['save_dir'], 'results.json')
        with open(results_path, 'w') as f:
            json.dump(self.results, f, indent=2)

        # 保存训练历史
        history_path = os.path.join(self.config['save_dir'], 'training_history.json')
        history = {
            'train_losses': self.results['train_losses'],
            'val_losses': self.results['val_losses'],
            'val_accuracies': self.results['val_accuracies']
        }
        with open(history_path, 'w') as f:
            json.dump(history, f, indent=2)

        print(f"✅ 结果已保存到: {self.config['save_dir']}")

    def plot_training_history(self):
        """绘制训练历史"""
        import matplotlib.pyplot as plt

        print("📈 生成训练历史图表...")

        fig, axes = plt.subplots(1, 3, figsize=(15, 5))

        # 训练损失
        axes[0].plot(self.results['train_losses'])
        axes[0].set_title('Training Loss')
        axes[0].set_xlabel('Epoch')
        axes[0].set_ylabel('Loss')
        axes[0].grid(True)

        # 验证损失
        axes[1].plot(self.results['val_losses'], color='orange')
        axes[1].set_title('Validation Loss')
        axes[1].set_xlabel('Epoch')
        axes[1].set_ylabel('Loss')
        axes[1].grid(True)

        # 验证准确率
        axes[2].plot(self.results['val_accuracies'], color='green')
        axes[2].set_title('Validation Accuracy')
        axes[2].set_xlabel('Epoch')
        axes[2].set_ylabel('Accuracy (%)')
        axes[2].grid(True)

        plt.tight_layout()

        # 保存图表
        plot_path = os.path.join(self.config['save_dir'], 'training_history.png')
        plt.savefig(plot_path, dpi=300, bbox_inches='tight')
        plt.show()

        print(f"✅ 图表已保存到: {plot_path}")

    def run_complete_pipeline(self):
        """运行完整的微调管道"""
        print("🚀 开始完整的微调管道")
        print("=" * 60)

        try:
            # 设置环境
            self.setup_environment()

            # 加载模型
            self.load_base_model()

            # 准备数据
            train_loader, val_loader, test_loader = self.prepare_data()

            if train_loader is None:
                print("❌ 数据准备失败,终止流程")
                return

            # 设置训练
            self.setup_training()

            # 训练模型
            train_losses, val_losses, val_accuracies = self.train_model(train_loader, val_loader)

            # 评估模型
            test_loss, test_acc = self.evaluate_model(test_loader)

            # 保存结果
            self.save_results()

            # 绘制训练历史
            self.plot_training_history()

            print("\n🎉 微调管道执行完成!")
            print(f"最终测试准确率: {test_acc:.2f}%")

        except Exception as e:
            print(f"❌ 微调管道执行失败: {e}")
            import traceback
            traceback.print_exc()

def create_default_config():
    """创建默认配置"""
    return {
        # 模型配置
        'base_model_name': 'NeoQuasar/Kronos-small',
        'tokenizer_name': 'NeoQuasar/Kronos-Tokenizer-base',
        'num_classes': 3,  # 下跌、横盘、上涨
        'feature_dim': 32,
        'hidden_dim': 256,
        'freeze_base': True,

        # 训练配置
        'epochs': 50,
        'batch_size': 32,
        'learning_rate': 1e-4,
        'weight_decay': 1e-5,

        # 数据配置
        'qlib_data_path': '~/.qlib/qlib_data/cn_data',
        'qlib_region': REG_CN,
        'qlib_market': 'all',
        'qlib_fields': ['$close', '$volume', '$high', '$low', '$open'],
        'target_stock': '000001.SZ',
        'start_time': '2010-01-01',
        'end_time': '2023-12-31',

        # 保存配置
        'save_dir': './finetuning_checkpoints',
        'log_dir': './finetuning_logs'
    }

def main():
    """主函数"""
    print("Kronos微调训练管道")
    print("=" * 50)

    # 创建配置
    config = create_default_config()

    # 创建并运行管道
    pipeline = FinetuningPipeline(config)
    pipeline.run_complete_pipeline()

if __name__ == "__main__":
    main()

7.5 章节小结

🎯 核心要点回顾

通过本章学习,您应该掌握:

  1. 微调理论基础:理解微调的概念、优势和适用场景

  2. Qlib集成:掌握Qlib框架的使用和数据准备

  3. 模型架构:理解微调模型的设计和实现

  4. 训练流程:掌握完整的微调训练流程

  5. 最佳实践:了解微调的技巧和注意事项

💡 重要技能

  • 设计和实现微调数据管道
  • 配置和管理分布式训练环境
  • 监控和优化训练过程
  • 评估微调模型的效果
  • 保存和管理训练结果

🚀 下一步行动

现在您已经掌握了Kronos的微调技术,接下来:

  1. 继续学习:前往第8章:Web界面使用

  2. 实践项目:使用真实数据尝试微调训练

  3. 优化实验:调整超参数和训练策略

  4. 应用部署:将微调模型应用到实际项目

📚 推荐资源

❓ 自我检查

回答以下问题来检验您的理解:

  1. 什么是模型微调?它与从头训练有什么区别?
  2. Qlib框架在微调中起什么作用?
  3. 如何设计一个有效的微调数据集?
  4. 微调时应该冻结哪些模型参数?
  5. 如何评估微调模型的效果?

恭喜完成第7章的学习! 🎉

现在您已经掌握了Kronos的微调技术,让我们继续前往第8章,学习如何构建Web界面。

➡️ 前往第8章:Web界面使用