ITFormer / dataset /dataset.py
a12354's picture
Add files using upload-large-folder tool
c8aad8f verified
Raw
History Blame Contribute Delete
17.9 kB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Time Series Question Answering Dataset.
Handles loading and preprocessing of time series data and question-answer pairs.
"""
import sys
from transformers import PretrainedConfig, AutoTokenizer
from transformers import AutoProcessor
import torch
import os
import json
from torch.utils.data import Dataset
from typing import List, Dict, Any
import pandas as pd
import numpy as np
import h5py
import re
import random
from models.TimeLanguageModel import TLMConfig
from utils.log_util import adaptive_print
class PretrainDataset(Dataset):
def __init__(self, ts_path):
super().__init__()
self.ts_path = ts_path
self.load_data()
def load_data(self):
with h5py.File(self.ts_path, 'r') as f:
data = f['seq_data'][:]
self.datas = data
def __len__(self):
return len(self.datas)
def __getitem__(self, index):
return {'ts_values': torch.tensor(self.datas[index], dtype=torch.float)}
def find_assistant_tokens(tokenizer, target):
"""Find assistant token positions in the target sequence.
Args:
tokenizer: Tokenizer instance
target: Target token sequence
Returns:
List of tuples containing start and end positions of assistant tokens
"""
result = []
start_index = 0
end_index = 0
while start_index <= len(target) - 1:
if target[start_index] != tokenizer('assistant')['input_ids'][0]:
start_index += 1
end_index += 1
else:
end_index += 1
if target[end_index] == tokenizer('<|im_end|>')['input_ids'][0]:
result.append((start_index + 1, end_index + 1))
start_index = end_index + 1
return result
class TsQaDataset(Dataset):
"""Time Series Question Answering Dataset with token ID range validation."""
def __init__(self, ts_path, data_path, tokenizer, processor, config, pretrain=False, sft=False, shuffle=False):
"""Initialize the dataset.
Args:
ts_path: Path to time series data file
data_path: Path to question-answer data file
tokenizer: Tokenizer instance
processor: Processor instance
config: Configuration object
pretrain: Whether in pretraining mode
sft: Whether in supervised fine-tuning mode
shuffle: Whether to shuffle the data
"""
super().__init__()
self.ts_path = ts_path
self.data_path = data_path
self.tokenizer = tokenizer
self.processor = processor
self.config = config
self.pretrain = pretrain
self.sft = sft
self.shuffle = shuffle
self.h5_file = None
# Key fix: Ensure vocab_size is correct
self.vocab_size = len(self.tokenizer)
adaptive_print(f"📊 Vocab size: {self.vocab_size}")
# Ensure tokenizer settings are consistent
self.tokenizer.padding_side = 'left'
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
# Validate special tokens
self._validate_special_tokens()
self._build_index()
def _validate_special_tokens(self):
"""Validate that all special token IDs are within valid range."""
special_tokens = {
'pad_token_id': self.tokenizer.pad_token_id,
'eos_token_id': self.tokenizer.eos_token_id,
'bos_token_id': getattr(self.tokenizer, 'bos_token_id', None),
'unk_token_id': getattr(self.tokenizer, 'unk_token_id', None),
}
adaptive_print("🔍 Validating special tokens:")
for name, token_id in special_tokens.items():
if token_id is not None:
if token_id >= self.vocab_size or token_id < 0:
adaptive_print(f"❌ {name} = {token_id} out of range [0, {self.vocab_size})")
# Fix invalid special tokens
if name == 'pad_token_id':
self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
adaptive_print(f"🔧 Fixed: pad_token_id -> {self.tokenizer.pad_token_id}")
else:
adaptive_print(f"✅ {name} = {token_id}")
def _validate_token_ids(self, token_ids, context=""):
"""Validate token IDs for validity.
Args:
token_ids: List of token IDs to validate
context: Context string for error messages
Returns:
List of validated token IDs
"""
if not isinstance(token_ids, list):
return token_ids
valid_ids = []
for i, token_id in enumerate(token_ids):
if token_id < 0 or token_id >= self.vocab_size:
adaptive_print(f"⚠️ {context} position {i}: invalid token_id {token_id}, replacing with unk_token")
# Replace with unk_token, if not available use eos_token
replacement = getattr(self.tokenizer, 'unk_token_id', self.tokenizer.eos_token_id)
valid_ids.append(replacement)
else:
valid_ids.append(token_id)
return valid_ids
def _build_index(self):
"""Build dataset index by loading and processing data files."""
self.datas = []
with open(self.data_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f):
item = json.loads(line)
for i in range(0, len(item['conversations']), 2):
if item['conversations'][i]['stage'] in ['1', '2', '3', '4']:
self.datas.append({
'id': item['id'],
'stage': int(item['conversations'][i]['stage']),
'form': item['conversations'][i]['attribute'],
'question': item['conversations'][i]['value'],
'answer': item['conversations'][i + 1]['value'],
'line_num': line_num
})
if self.shuffle:
adaptive_print(f"🎲 Shuffling dataset: {self.data_path}")
random.shuffle(self.datas)
def _get_h5_file(self):
"""Get HDF5 file handle for time series data."""
if self.h5_file is None and os.path.exists(self.ts_path):
self.h5_file = h5py.File(self.ts_path, 'r')
return self.h5_file
def __len__(self):
"""Return dataset length."""
return len(self.datas)
def add_adaptive_prompt(self, sample):
sample = sample.copy()
if sample['stage'] == 1:
sample['question'] += " Please analyze the change in this signal and explain its physical implication, such as component load, airflow, or temperature stability."
elif sample['stage'] == 2:
sample['question'] += " Carefully analyze the signal pattern (e.g., stability, oscillation, drops) to determine the correct fault status or root cause. Select the most likely option based on observed signal behavior."
elif sample['stage'] == 3:
sample['question'] += " Review the trends across 10 cycles and evaluate the degradation pattern. Select the option that best reflects the long-term health status or risk level indicated by the signal."
elif sample['stage'] == 4:
sample['question'] += " Based on the 10-cycle degradation pattern, propose concrete maintenance actions (e.g., replace, inspect) to ensure safe and efficient operation."
return sample
def _create_chat_input(self, question):
"""Unified chat input creation method."""
messages = [
{"role": "system", "content": 'You are a helpful assistant.'},
{"role": "user", "content": question}
]
try:
# Use a safer tokenization method
chat_text = self.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
# Replace time series placeholder
chat_text = chat_text.replace('<ts>', '<|image_pad|>' * self.config.ts_pad_num)
return chat_text
except Exception as e:
adaptive_print(f"❌ Chat template error: {e}")
# Fallback to a simple format
return f"You are a helpful assistant.\nuser\n{question}\nassistant\n"
def _safe_tokenize(self, text, add_special_tokens=True):
"""Safe tokenization, ensure results are within valid range."""
try:
# Add more tokenization parameters
result = self.tokenizer(
text,
add_special_tokens=add_special_tokens,
padding=False,
truncation=False,
return_tensors=None
)
token_ids = result['input_ids']
# Validate token_ids
token_ids = self._validate_token_ids(token_ids, f"tokenize: {text[:50]}...")
return token_ids
except Exception as e:
adaptive_print(f"❌ Tokenization error for text: {text[:100]}...")
adaptive_print(f"Error: {e}")
# Return a safe default value
return [self.tokenizer.eos_token_id]
def __getitem__(self, idx):
try:
sample = self.datas[idx]
# sample = self.add_adaptive_prompt(sample)
# Load time series data
h5f = self._get_h5_file()
if isinstance(sample['id'], str):
ts = h5f['seq_data'][int(sample['id']) - 1]
elif isinstance(sample['id'], list):
ts_list = [h5f['seq_data'][int(i) - 1][:len(h5f['seq_data'][int(i) - 1]) // 10] for i in sample['id']]
ts = np.concatenate(ts_list, axis=0)
# =========================== Mode 1: Pretraining ===========================
if getattr(self, 'pretrain', False):
return {
'ts_values': torch.tensor(ts, dtype=torch.float)
}
# =========================== Mode 2: SFT Training ===========================
elif getattr(self, 'sft', False):
# Create query_ids: only the original question text, no other information
original_question = sample['question']
query_ids = self._safe_tokenize(original_question, add_special_tokens=False)
# Create input_ids: full input including time series placeholder
q_text = self._create_chat_input(sample['question']) # This includes <|image_pad|>
q_input_ids = self._safe_tokenize(q_text, add_special_tokens=False)
# Ensure answer format is consistent and safe
a_text = sample['answer']
if not a_text.endswith(self.tokenizer.eos_token):
a_text += self.tokenizer.eos_token
a_input_ids = self._safe_tokenize(a_text, add_special_tokens=False)
# Construct training data
input_ids = q_input_ids + a_input_ids
labels = [self.tokenizer.pad_token_id] * len(q_input_ids) + a_input_ids
# Final validation
query_ids = self._validate_token_ids(query_ids, f"query_ids_sample_{idx}")
input_ids = self._validate_token_ids(input_ids, f"final_input_sample_{idx}")
labels = self._validate_token_ids(labels, f"final_labels_sample_{idx}")
# Ensure length matches
final_input_ids = input_ids[:-1] if len(input_ids) > 1 else input_ids
final_labels = labels[1:] if len(labels) > 1 else labels
return {
'form': sample['form'],
'stage': sample['stage'],
'query_ids': query_ids, # Only contains the original question text
'input_ids': final_input_ids,
'labels': final_labels,
'ts_values': torch.tensor(ts, dtype=torch.float),
'index': sample['line_num']
}
# =========================== Mode 3: Inference/Evaluation ===========================
else:
# Create query_ids: only the original question text, no other information
original_question = sample['question']
query_ids = self._safe_tokenize(original_question, add_special_tokens=False)
# Create input_ids: includes time series placeholder
q_text = self._create_chat_input(sample['question'])
q_input_ids = self._safe_tokenize(q_text, add_special_tokens=False)
a_text = sample['answer']
if not a_text.endswith(self.tokenizer.eos_token):
a_text += self.tokenizer.eos_token
a_input_ids = self._safe_tokenize(a_text, add_special_tokens=False)
# Validate results
query_ids = self._validate_token_ids(query_ids, f"infer_query_sample_{idx}")
q_input_ids = self._validate_token_ids(q_input_ids, f"infer_q_sample_{idx}")
a_input_ids = self._validate_token_ids(a_input_ids, f"infer_a_sample_{idx}")
return {
'form': sample['form'],
'stage': sample['stage'],
'query_ids': query_ids, # Only contains the original question text
'input_ids': q_input_ids,
'labels': a_input_ids,
'ts_values': torch.tensor(ts, dtype=torch.float),
'index': sample['line_num']
}
except Exception as e:
adaptive_print(f"❌ Error processing sample {idx}: {e}")
# Return a safe default sample
return self._get_safe_default_sample()
def _get_safe_default_sample(self):
"""Return a safe default sample."""
return {
'form': 'default',
'stage': 1,
'query_ids': [self.tokenizer.eos_token_id], # Simple default query
'input_ids': [self.tokenizer.eos_token_id],
'labels': [self.tokenizer.eos_token_id],
'ts_values': torch.zeros(100, dtype=torch.float),
'index': 0
}
def __del__(self):
if self.h5_file:
self.h5_file.close()
class DataCollator:
def __init__(self, tokenizer):
self.tokenizer = tokenizer
# Ensure tokenizer settings are correct
if self.tokenizer.padding_side != 'left':
adaptive_print("⚠️ Warning: Setting tokenizer.padding_side to 'left' for decoder-only model")
self.tokenizer.padding_side = 'left'
def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, Any]:
# 兼容性处理:检查 key 是否存在,如果不存在(如预训练模式)则跳过相关逻辑
has_text_data = all('input_ids' in f and 'labels' in f and 'query_ids' in f for f in features)
if not has_text_data:
# 预训练模式或数据缺失:仅处理 ts_values
ts_values = [f['ts_values'] for f in features]
batch = {'ts_values': torch.stack(ts_values, dim=0)}
# 尝试包含其他可能存在的字段
for key in ['stage', 'index']:
if all(key in f for f in features):
batch[key] = torch.tensor([f[key] for f in features])
return batch
max_len_inputs = max(len(feature['input_ids']) for feature in features)
max_len_labels = max(len(feature['labels']) for feature in features)
max_len_querys = max(len(feature['query_ids']) for feature in features)
input_ids = []
attention_mask = []
labels = []
ts_values = []
stages = []
index = []
query_ids = []
for feature in features:
input_len = len(feature['input_ids'])
label_len = len(feature['labels'])
query_ids_len = len(feature['query_ids'])
# Left padding is correct (keep original logic)
padded_input = [self.tokenizer.pad_token_id] * (max_len_inputs - input_len) + feature['input_ids']
input_ids.append(padded_input)
# Corresponding attention mask
attention_mask.append([0] * (max_len_inputs - input_len) + [1] * input_len)
# Labels also left-padded
padded_labels = [self.tokenizer.pad_token_id] * (max_len_labels - label_len) + feature['labels'] # Use -100 to ignore pad positions in loss
labels.append(padded_labels)
# query_ids also left-padded
padded_query_ids = [self.tokenizer.pad_token_id] * (max_len_querys - query_ids_len) + feature['query_ids']
query_ids.append(padded_query_ids)
ts_values.append(feature['ts_values'])
stages.append(feature['stage'])
index.append(feature['index'])
return {
'input_ids': torch.tensor(input_ids, dtype=torch.long),
'attention_mask': torch.tensor(attention_mask, dtype=torch.long),
'labels': torch.tensor(labels, dtype=torch.long),
'ts_values': torch.stack(ts_values, dim=0),
'stage': torch.tensor(stages, dtype=torch.int8),
'index': torch.tensor(index, dtype=torch.int32),
'query_ids': torch.tensor(query_ids, dtype=torch.long)
}