| |
| |
| """ |
| 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 |
| |
| |
| self.vocab_size = len(self.tokenizer) |
| adaptive_print(f"📊 Vocab size: {self.vocab_size}") |
| |
| |
| self.tokenizer.padding_side = 'left' |
| if self.tokenizer.pad_token is None: |
| self.tokenizer.pad_token = self.tokenizer.eos_token |
| |
| |
| 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})") |
| |
| 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") |
| |
| 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: |
| |
| chat_text = self.tokenizer.apply_chat_template( |
| messages, |
| tokenize=False, |
| add_generation_prompt=True |
| ) |
| |
| 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}") |
| |
| 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: |
| |
| result = self.tokenizer( |
| text, |
| add_special_tokens=add_special_tokens, |
| padding=False, |
| truncation=False, |
| return_tensors=None |
| ) |
| token_ids = result['input_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 [self.tokenizer.eos_token_id] |
|
|
| def __getitem__(self, idx): |
| try: |
| sample = self.datas[idx] |
| |
|
|
| |
| 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) |
|
|
| |
| if getattr(self, 'pretrain', False): |
| return { |
| 'ts_values': torch.tensor(ts, dtype=torch.float) |
| } |
|
|
| |
| elif getattr(self, 'sft', False): |
| |
| original_question = sample['question'] |
| query_ids = self._safe_tokenize(original_question, add_special_tokens=False) |
| |
| |
| 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) |
|
|
| |
| input_ids = q_input_ids + a_input_ids |
| labels = [self.tokenizer.pad_token_id] * len(q_input_ids) + a_input_ids |
|
|
| |
| 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}") |
|
|
| |
| 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, |
| 'input_ids': final_input_ids, |
| 'labels': final_labels, |
| 'ts_values': torch.tensor(ts, dtype=torch.float), |
| 'index': sample['line_num'] |
| } |
|
|
| |
| else: |
| |
| original_question = sample['question'] |
| query_ids = self._safe_tokenize(original_question, add_special_tokens=False) |
| |
| |
| 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) |
|
|
| |
| 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, |
| '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 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], |
| '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 |
| |
| 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]: |
| |
| 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 = [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']) |
| |
| padded_input = [self.tokenizer.pad_token_id] * (max_len_inputs - input_len) + feature['input_ids'] |
| input_ids.append(padded_input) |
| |
| |
| attention_mask.append([0] * (max_len_inputs - input_len) + [1] * input_len) |
| |
| |
| padded_labels = [self.tokenizer.pad_token_id] * (max_len_labels - label_len) + feature['labels'] |
| labels.append(padded_labels) |
| |
| |
| 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) |
| } |
|
|
|
|
|
|