Skip to content
This repository was archived by the owner on Jan 6, 2026. It is now read-only.

Commit a7a52a1

Browse files
authored
feat: 🎸 add inference feature (GestaltCogTeam#246)
* feat: 🎸 add inference script add experimental script for inference processing * feat: 🎸 add inference server add web page and api server for inference processing * update review
1 parent 63167f8 commit a7a52a1

35 files changed

Lines changed: 1646 additions & 14 deletions

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ ckpt/
2424
*.py[cod]
2525
*$py.class
2626

27+
.DS_Store
28+
2729
# C extensions
2830
# *.so
2931

@@ -177,4 +179,10 @@ cython_debug/
177179
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
178180
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
179181
# and can be added to the global gitignore or merged into this file. For a more nuclear
180-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
182+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
183+
184+
# MacOS
185+
.DS_Store
186+
187+
# not ignore requirements.txt
188+
!*requirements.txt

.pylintrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
[MASTER]
99

1010
# Files or directories to be skipped. They should be base names, not paths.
11-
ignore=baselines,assets,checkpoints
11+
ignore=baselines,assets,checkpoints,examples
1212

1313
# Files or directories matching the regex patterns are skipped. The regex
1414
# matches against base names, not paths.
15-
ignore-patterns=^\.|^_|^.*\.md|^.*\.txt|^.*\.CFF|^LICENSE
15+
ignore-patterns=^\.|^_|^.*\.md|^.*\.txt|^.*\.csv|^.*\.CFF|^LICENSE
1616

1717
# Pickle collected data for later comparisons.
1818
persistent=no

baselines/ChronosBolt/config/chronos_small.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,8 @@
108108
'target_length': predict_length,
109109
'num_valid_samples': 1000
110110
})
111+
112+
############################## Inference Configuration ##############################
113+
CFG.INFERENCE = EasyDict()
114+
CFG.INFERENCE.GENERATION_PARAMS = EasyDict({
115+
})

baselines/TimeMoE/config/timemoe_base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
# Dataset & Metrics configuration
1616
# Model architecture and parameters
1717

18+
pretrained = False # Whether to use a pretrained model
19+
1820
MODEL_ARCH = TimeMoE
1921

2022
MODEL_PARAM = {
2123
'model_id': "baselines/TimeMoE/ckpt/TimeMoE-50M",
22-
'from_pretrained': False,
24+
'from_pretrained': pretrained,
2325
'context_length': 4095,
2426
'trust_remote_code': True,
2527
}
@@ -110,3 +112,9 @@
110112
CFG.DATASET.PARAM = EasyDict({
111113
'num_valid_samples': 1000
112114
})
115+
116+
############################## Inference Configuration ##############################
117+
CFG.INFERENCE = EasyDict()
118+
CFG.INFERENCE.GENERATION_PARAMS = EasyDict({
119+
'normalize': not pretrained
120+
})

baselines/TimeMoE/config/timemoe_large.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
# Dataset & Metrics configuration
1616
# Model architecture and parameters
1717

18+
pretrained = False # Whether to use a pretrained model
19+
1820
MODEL_ARCH = TimeMoE
1921

2022
MODEL_PARAM = {
2123
'model_id': "baselines/TimeMoE/ckpt/TimeMoE-200M",
22-
'from_pretrained': False,
24+
'from_pretrained': pretrained,
2325
'context_length': 4095,
2426
'trust_remote_code': True,
2527
}
@@ -110,3 +112,8 @@
110112
CFG.DATASET.PARAM = EasyDict({
111113
'num_valid_samples': 1000
112114
})
115+
############################## Inference Configuration ##############################
116+
CFG.INFERENCE = EasyDict()
117+
CFG.INFERENCE.GENERATION_PARAMS = EasyDict({
118+
'normalize': not pretrained
119+
})

basicts/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from .launcher import launch_evaluation, launch_training
1+
from .launcher import launch_evaluation, launch_inference, launch_training
22
from .runners import BaseEpochRunner
33

4-
__version__ = '0.5.1'
4+
__version__ = '0.5.2'
55

6-
__all__ = ['__version__', 'launch_training', 'launch_evaluation', 'BaseEpochRunner']
6+
__all__ = ['__version__', 'launch_training', 'launch_evaluation', 'BaseEpochRunner', 'launch_inference']
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import json
2+
import logging
3+
from typing import List, Tuple, Union
4+
5+
import numpy as np
6+
import pandas as pd
7+
8+
from .base_dataset import BaseDataset
9+
10+
11+
class TimeSeriesInferenceDataset(BaseDataset):
12+
"""
13+
A dataset class for time series inference tasks, where the input is a sequence of historical data points
14+
15+
Attributes:
16+
description_file_path (str): Path to the JSON file containing the description of the dataset.
17+
description (dict): Metadata about the dataset, such as shape and other properties.
18+
data (np.ndarray): The loaded time series data array.
19+
raw_data (str): The raw data path or data list of the dataset.
20+
last_datetime (pd.Timestamp): The last datetime in the dataset. Used to generate time features of future data.
21+
"""
22+
23+
# pylint: disable=unused-argument
24+
def __init__(self, dataset_name:str, dataset: Union[str, list], input_len: int, output_len: int,
25+
logger: logging.Logger = None, **kwargs) -> None:
26+
"""
27+
Initializes the TimeSeriesInferenceDataset by setting up paths, loading data, and
28+
preparing it according to the specified configurations.
29+
30+
Args:
31+
dataset_name (str): The name of the dataset. If dataset_name is None, the dataset is expected to be passed directly.
32+
dataset(str or array): The data path of the dataset or data itself.
33+
input_len(str): The length of the input sequence (number of historical points).
34+
output_len(str): The length of the output sequence (number of future points to predict).
35+
logger (logging.Logger): logger.
36+
"""
37+
train_val_test_ratio: List[float] = []
38+
mode: str = 'inference'
39+
overlap = False
40+
super().__init__(dataset_name, train_val_test_ratio, mode, input_len, output_len, overlap)
41+
self.logger = logger
42+
43+
self.description = {}
44+
if dataset_name:
45+
self.description_file_path = f'datasets/{dataset_name}/desc.json'
46+
self.description = self._load_description()
47+
48+
self.last_datetime:pd.Timestamp = pd.Timestamp.now()
49+
self._raw_data = dataset
50+
self.data = self._load_data()
51+
52+
def _load_description(self) -> dict:
53+
"""
54+
Loads the description of the dataset from a JSON file.
55+
56+
Returns:
57+
dict: A dictionary containing metadata about the dataset, such as its shape and other properties.
58+
59+
Raises:
60+
FileNotFoundError: If the description file is not found.
61+
json.JSONDecodeError: If there is an error decoding the JSON data.
62+
"""
63+
try:
64+
with open(self.description_file_path, 'r') as f:
65+
return json.load(f)
66+
except FileNotFoundError as e:
67+
raise FileNotFoundError(f'Description file not found: {self.description_file_path}') from e
68+
except json.JSONDecodeError as e:
69+
raise ValueError(f'Error decoding JSON file: {self.description_file_path}') from e
70+
71+
def _load_data(self) -> np.ndarray:
72+
"""
73+
Loads the time series data from a file or list and processes it according to the dataset description.
74+
Returns:
75+
np.ndarray: The data array for the specified mode (train, validation, or test).
76+
77+
Raises:
78+
ValueError: If there is an issue with loading the data file or if the data shape is not as expected.
79+
"""
80+
81+
if isinstance(self._raw_data, str):
82+
df = pd.read_csv(self._raw_data, header=None)
83+
else:
84+
df = pd.DataFrame(self._raw_data)
85+
86+
df_index = pd.to_datetime(df[0].values, format='%Y-%m-%d %H:%M:%S').to_numpy()
87+
df = df[df.columns[1:]]
88+
df.index = pd.Index(df_index)
89+
df = df.astype('float32')
90+
self.last_datetime = df.index[-1]
91+
92+
data = np.expand_dims(df.values, axis=-1)
93+
data = data[..., [0]]
94+
95+
# if description is not provided, we assume the data is already in the correct shape.
96+
if not self.dataset_name:
97+
# calc frequency form df
98+
freq = int((df.index[1] - df.index[0]).total_seconds() / 60) # convert to minutes
99+
if freq <= 0:
100+
raise ValueError('Frequency must be a positive number.')
101+
self.description = {
102+
'shape': data.shape,
103+
'frequency (minutes)': freq,
104+
}
105+
106+
data_with_features = self._add_temporal_features(data, df)
107+
108+
data_set_shape = self.description['shape']
109+
_, n, c = data_with_features.shape
110+
if data_set_shape[1] != n or data_set_shape[2] != c:
111+
raise ValueError(f'Error loading data. Shape mismatch: expected {data_set_shape[1:]}, got {[n,c]}.')
112+
113+
return data_with_features
114+
115+
def _add_temporal_features(self, data, df) -> np.ndarray:
116+
'''
117+
Add time of day and day of week as features to the data.
118+
119+
Args:
120+
data (np.ndarray): The data array.
121+
df (pd.DataFrame): The dataframe containing the datetime index.
122+
123+
Returns:
124+
np.ndarray: The data array with added time of day and day of week features.
125+
'''
126+
127+
_, n, _ = data.shape
128+
feature_list = [data]
129+
130+
# numerical time_of_day
131+
tod = (df.index.hour*60 + df.index.minute) / (24*60)
132+
tod_tiled = np.tile(tod, [1, n, 1]).transpose((2, 1, 0))
133+
feature_list.append(tod_tiled)
134+
135+
# numerical day_of_week
136+
dow = df.index.dayofweek / 7
137+
dow_tiled = np.tile(dow, [1, n, 1]).transpose((2, 1, 0))
138+
feature_list.append(dow_tiled)
139+
140+
# numerical day_of_month
141+
dom = (df.index.day - 1) / 31 # df.index.day starts from 1. We need to minus 1 to make it start from 0.
142+
dom_tiled = np.tile(dom, [1, n, 1]).transpose((2, 1, 0))
143+
feature_list.append(dom_tiled)
144+
145+
# numerical day_of_year
146+
doy = (df.index.dayofyear - 1) / 366 # df.index.month starts from 1. We need to minus 1 to make it start from 0.
147+
doy_tiled = np.tile(doy, [1, n, 1]).transpose((2, 1, 0))
148+
feature_list.append(doy_tiled)
149+
150+
data_with_features = np.concatenate(feature_list, axis=-1).astype('float32') # L x N x C
151+
152+
# Remove extra features
153+
data_set_shape = self.description['shape']
154+
data_with_features = data_with_features[..., range(data_set_shape[2])]
155+
156+
return data_with_features
157+
158+
def append_data(self, new_data: np.ndarray) -> None:
159+
"""
160+
Append new data to the existing data
161+
162+
Args:
163+
new_data (np.ndarray): The new data to append to the existing data.
164+
"""
165+
166+
freq = self.description['frequency (minutes)']
167+
l, _, _ = new_data.shape
168+
169+
data_with_features, datetime_list = self._gen_datetime_list(new_data, self.last_datetime, freq, l)
170+
self.last_datetime = datetime_list[-1]
171+
172+
self.data = np.concatenate([self.data, data_with_features], axis=0)
173+
174+
def _gen_datetime_list(self, new_data: np.ndarray, start_datetime: pd.Timestamp, freq: int, num_steps: int) -> Tuple[np.ndarray, List[pd.Timestamp]]:
175+
"""
176+
Generate a list of datetime objects based on the start datetime, frequency, and number of steps.
177+
178+
Args:
179+
start_datetime (pd.Timestamp): The starting datetime for the sequence.
180+
freq (int): The frequency of the data in minutes.
181+
num_steps (int): The number of steps in the sequence.
182+
183+
Returns:
184+
List[pd.Timestamp]: A list of datetime objects corresponding to the sequence.
185+
"""
186+
datetime_list = [start_datetime]
187+
for _ in range(num_steps):
188+
datetime_list.append(datetime_list[-1] + pd.Timedelta(minutes=freq))
189+
new_index = pd.Index(datetime_list[1:])
190+
new_df = pd.DataFrame()
191+
new_df.index = new_index
192+
data_with_features = self._add_temporal_features(new_data, new_df)
193+
194+
return data_with_features, datetime_list
195+
196+
def __getitem__(self, index: int) -> dict:
197+
"""
198+
Retrieves a sample from the dataset, considering both the input and output lengths.
199+
For inference, the input data is the last 'input_len' points in the dataset, and the output data is the next 'output_len' points.
200+
201+
Args:
202+
index (int): The index of the desired sample in the dataset.
203+
204+
Returns:
205+
dict: A dictionary containing 'inputs' and 'target', where both are slices of the dataset corresponding to
206+
the historical input data and future prediction data, respectively.
207+
"""
208+
history_data = self.data[-self.input_len:]
209+
210+
freq = self.description['frequency (minutes)']
211+
_, n, _ = history_data.shape
212+
future_data = np.zeros((self.output_len, n, 1))
213+
214+
data_with_features, _ = self._gen_datetime_list(future_data, self.last_datetime, freq, self.output_len)
215+
return {'inputs': history_data, 'target': data_with_features}
216+
217+
def __len__(self) -> int:
218+
"""
219+
Calculates the total number of samples available in the dataset.
220+
For inference, there is only one valid sample, as the input data is the last 'input_len' points in the dataset.
221+
222+
Returns:
223+
int: The number of valid samples that can be drawn from the dataset, based on the configurations of input and output lengths.
224+
"""
225+
return 1

0 commit comments

Comments
 (0)