modified: src/config.py
modified: src/dataset.py modified: src/evaluate.py modified: src/train.py
This commit is contained in:
BIN
best_model.pth
BIN
best_model.pth
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,17 +10,21 @@ BATCH_SIZE = 256
|
||||
# Features Configuration
|
||||
INPUT_SENSOR = 'WSMS00012'
|
||||
OUTPUT_SENSORS = ['WSMS00007', 'WSMS00008', 'WSMS00009', 'WSMS00010', 'WSMS00011']
|
||||
INPUT_AXIS = 'value1' # 底部传感器输入轴(课程要求:X轴)
|
||||
OUTPUT_AXIS = 'value3' # 目标传感器输出轴
|
||||
|
||||
# Model Configuration
|
||||
CHANNELS = [64, 128, 128, 256] # TCN 各层通道数
|
||||
KERNEL_SIZE = 3
|
||||
CHANNELS = [64, 64, 128, 128, 256, 256] # TCN 各层通道数
|
||||
KERNEL_SIZE = 5
|
||||
DROPOUT = 0.2
|
||||
|
||||
# Training Configuration
|
||||
LEARNING_RATE = 1e-3
|
||||
LEARNING_RATE = 1e-4
|
||||
EPOCHS = 50
|
||||
WEIGHT_DECAY = 1e-4
|
||||
WEIGHT_DECAY = 1e-3
|
||||
ENABLE_EARLY_STOP = False
|
||||
EARLY_STOP_PATIENCE = 15
|
||||
|
||||
# Device
|
||||
import torch
|
||||
DEVICE = 'cuda'
|
||||
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
|
||||
104
src/dataset.py
104
src/dataset.py
@@ -7,72 +7,146 @@ from torch.utils.data import Dataset, DataLoader
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from config import *
|
||||
|
||||
class MultiOutputStandardizer:
|
||||
"""Per-channel standardization that ignores missing labels via masks."""
|
||||
def __init__(self, n_outputs):
|
||||
self.n_outputs = n_outputs
|
||||
self.mean_ = np.zeros(n_outputs, dtype=np.float32)
|
||||
self.scale_ = np.ones(n_outputs, dtype=np.float32)
|
||||
self.fitted = False
|
||||
|
||||
def fit(self, y_sequences, mask_sequences):
|
||||
means = []
|
||||
scales = []
|
||||
for c in range(self.n_outputs):
|
||||
valid_values = []
|
||||
for y_seq, m_seq in zip(y_sequences, mask_sequences):
|
||||
valid = m_seq[:, c] > 0.5
|
||||
if np.any(valid):
|
||||
valid_values.append(y_seq[valid, c])
|
||||
if len(valid_values) == 0:
|
||||
means.append(0.0)
|
||||
scales.append(1.0)
|
||||
continue
|
||||
vals = np.concatenate(valid_values, axis=0)
|
||||
mean = float(np.mean(vals))
|
||||
std = float(np.std(vals))
|
||||
if std < 1e-6:
|
||||
std = 1.0
|
||||
means.append(mean)
|
||||
scales.append(std)
|
||||
|
||||
self.mean_ = np.asarray(means, dtype=np.float32)
|
||||
self.scale_ = np.asarray(scales, dtype=np.float32)
|
||||
self.fitted = True
|
||||
|
||||
def transform(self, y):
|
||||
if not self.fitted:
|
||||
raise RuntimeError("MultiOutputStandardizer must be fitted before transform.")
|
||||
return (y - self.mean_) / self.scale_
|
||||
|
||||
def inverse_transform(self, y):
|
||||
if not self.fitted:
|
||||
raise RuntimeError("MultiOutputStandardizer must be fitted before inverse_transform.")
|
||||
return y * self.scale_ + self.mean_
|
||||
|
||||
|
||||
class BuildingDataset(Dataset):
|
||||
def __init__(self, file_paths, seq_len, step_size, scaler_X=None, scaler_Y=None, fit_scaler=False):
|
||||
self.seq_len = seq_len
|
||||
self.X_data = []
|
||||
self.Y_data = []
|
||||
self.M_data = []
|
||||
self.scaler_X = scaler_X if scaler_X is not None else StandardScaler()
|
||||
self.scaler_Y = scaler_Y if scaler_Y is not None else StandardScaler()
|
||||
self.scaler_Y = scaler_Y if scaler_Y is not None else MultiOutputStandardizer(len(OUTPUT_SENSORS))
|
||||
|
||||
raw_X = []
|
||||
raw_Y = []
|
||||
raw_M = []
|
||||
|
||||
for f in file_paths:
|
||||
# 读取数据
|
||||
df = pd.read_csv(f)
|
||||
# 使用长表格式: code, type, time, value1, value2, value3
|
||||
|
||||
# 提取 012 的 X 轴作为基准
|
||||
df_in = df[df['code'] == INPUT_SENSOR][['time', 'value1']].rename(columns={'value1': 'input_x'})
|
||||
# 提取 012 的输入轴作为基准
|
||||
df_in = df[df['code'] == INPUT_SENSOR][['time', INPUT_AXIS]].rename(columns={INPUT_AXIS: 'input_signal'})
|
||||
if len(df_in) == 0:
|
||||
# 若无输入传感器(如自由衰减数据),则补零
|
||||
df_in = pd.DataFrame({'time': df['time'].unique()})
|
||||
df_in['input_signal'] = 0.0
|
||||
|
||||
# 提取 007~011 的 Z 轴并按时间戳逐步合并 (使用 left join 确保以 df_in 的时间为基准)
|
||||
df_merged = df_in
|
||||
for sens in OUTPUT_SENSORS:
|
||||
df_out_sens = df[df['code'] == sens][['time', 'value3']].rename(columns={'value3': f'out_{sens}'})
|
||||
df_out_sens = df[df['code'] == sens][['time', OUTPUT_AXIS]].rename(columns={OUTPUT_AXIS: f'out_{sens}'})
|
||||
df_merged = pd.merge(df_merged, df_out_sens, on='time', how='left')
|
||||
|
||||
df_merged = df_merged.sort_values('time').reset_index(drop=True)
|
||||
df_merged = df_merged.interpolate(method='linear').bfill().ffill().fillna(0)
|
||||
|
||||
if len(df_merged) == 0:
|
||||
print(f"Warning: Skipping file {f} due to no overlapping timestamps across required sensors.")
|
||||
continue
|
||||
|
||||
x_seq = df_merged['input_x'].values.reshape(-1, 1)
|
||||
# 提取所有 target 传感器的列
|
||||
x_seq = df_merged['input_signal'].values.reshape(-1, 1).astype(np.float32)
|
||||
|
||||
# 提取所有 target 传感器列与可用性掩码
|
||||
out_cols = [f'out_{sens}' for sens in OUTPUT_SENSORS]
|
||||
y_seq = df_merged[out_cols].values
|
||||
y_seq = np.zeros((len(df_merged), len(OUTPUT_SENSORS)), dtype=np.float32)
|
||||
m_seq = np.zeros((len(df_merged), len(OUTPUT_SENSORS)), dtype=np.float32)
|
||||
for c, col in enumerate(out_cols):
|
||||
series = df_merged[col]
|
||||
observed = ~series.isna()
|
||||
m_seq[:, c] = observed.astype(np.float32)
|
||||
if observed.any():
|
||||
filled = series.interpolate(method='linear').bfill().ffill()
|
||||
y_seq[:, c] = filled.fillna(0.0).values.astype(np.float32)
|
||||
else:
|
||||
y_seq[:, c] = 0.0
|
||||
|
||||
raw_X.append(x_seq)
|
||||
raw_Y.append(y_seq)
|
||||
raw_M.append(m_seq)
|
||||
|
||||
if len(raw_X) == 0:
|
||||
raise ValueError("未能从文件中构造出有效序列,请检查数据路径与传感器编码配置。")
|
||||
|
||||
# 拼接所有文件数据进行 fit
|
||||
X_all = np.vstack(raw_X)
|
||||
Y_all = np.vstack(raw_Y)
|
||||
|
||||
if fit_scaler:
|
||||
self.scaler_X.fit(X_all)
|
||||
self.scaler_Y.fit(Y_all)
|
||||
self.scaler_Y.fit(raw_Y, raw_M)
|
||||
|
||||
# 切分窗口
|
||||
for x_seq, y_seq in zip(raw_X, raw_Y):
|
||||
for x_seq, y_seq, m_seq in zip(raw_X, raw_Y, raw_M):
|
||||
x_seq_scaled = self.scaler_X.transform(x_seq)
|
||||
y_seq_scaled = self.scaler_Y.transform(y_seq)
|
||||
y_seq_scaled = np.where(m_seq > 0.5, y_seq_scaled, 0.0).astype(np.float32)
|
||||
|
||||
for i in range(0, len(x_seq_scaled) - seq_len + 1, step_size):
|
||||
self.X_data.append(x_seq_scaled[i:i+seq_len])
|
||||
self.Y_data.append(y_seq_scaled[i:i+seq_len])
|
||||
x_win = x_seq_scaled[i:i+seq_len]
|
||||
y_win = y_seq_scaled[i:i+seq_len]
|
||||
m_win = m_seq[i:i+seq_len]
|
||||
if np.sum(m_win) <= 0:
|
||||
continue
|
||||
self.X_data.append(x_win)
|
||||
self.Y_data.append(y_win)
|
||||
self.M_data.append(m_win)
|
||||
|
||||
self.X_data = np.array(self.X_data)
|
||||
self.Y_data = np.array(self.Y_data)
|
||||
self.M_data = np.array(self.M_data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.X_data)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return torch.tensor(self.X_data[idx], dtype=torch.float32), \
|
||||
torch.tensor(self.Y_data[idx], dtype=torch.float32)
|
||||
return (
|
||||
torch.tensor(self.X_data[idx], dtype=torch.float32),
|
||||
torch.tensor(self.Y_data[idx], dtype=torch.float32),
|
||||
torch.tensor(self.M_data[idx], dtype=torch.float32),
|
||||
)
|
||||
|
||||
def get_dataloaders(condition='Non_TMD', include_free_vib=False):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,8 @@ from model import BuildingTCN
|
||||
|
||||
def evaluate_model():
|
||||
_, _, test_loader, scaler_X, scaler_Y = get_dataloaders()
|
||||
if test_loader is None:
|
||||
raise ValueError("当前数据配置下没有可用的测试集。")
|
||||
|
||||
model = BuildingTCN(input_size=1, output_size=5, num_channels=CHANNELS,
|
||||
kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE)
|
||||
@@ -17,7 +19,7 @@ def evaluate_model():
|
||||
all_targets = []
|
||||
|
||||
with torch.no_grad():
|
||||
for inputs, targets in test_loader:
|
||||
for inputs, targets, masks in test_loader:
|
||||
inputs = inputs.to(DEVICE)
|
||||
outputs = model(inputs)
|
||||
|
||||
|
||||
42
src/train.py
42
src/train.py
@@ -5,11 +5,10 @@ from config import *
|
||||
from dataset import get_dataloaders
|
||||
from model import BuildingTCN
|
||||
|
||||
def fft_loss(pred, target):
|
||||
"""计算频域损失"""
|
||||
pred_fft = torch.fft.rfft(pred, dim=1)
|
||||
target_fft = torch.fft.rfft(target, dim=1)
|
||||
return nn.L1Loss()(torch.abs(pred_fft), torch.abs(target_fft))
|
||||
def masked_l1_loss(pred, target, mask):
|
||||
diff = torch.abs(pred - target) * mask
|
||||
denom = mask.sum().clamp(min=1.0)
|
||||
return diff.sum() / denom
|
||||
|
||||
def train_model():
|
||||
train_loader, val_loader, _, _, _ = get_dataloaders()
|
||||
@@ -19,25 +18,24 @@ def train_model():
|
||||
kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE)
|
||||
|
||||
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)
|
||||
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=3, factor=0.5)
|
||||
mse_criterion = nn.MSELoss()
|
||||
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=8, factor=0.5)
|
||||
|
||||
best_val_loss = float('inf')
|
||||
no_improve_epochs = 0
|
||||
early_stop_patience = EARLY_STOP_PATIENCE
|
||||
|
||||
for epoch in range(EPOCHS):
|
||||
model.train()
|
||||
train_loss = 0.0
|
||||
|
||||
for inputs, targets in train_loader:
|
||||
inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)
|
||||
for inputs, targets, masks in train_loader:
|
||||
inputs = inputs.to(DEVICE)
|
||||
targets = targets.to(DEVICE)
|
||||
masks = masks.to(DEVICE)
|
||||
|
||||
optimizer.zero_grad()
|
||||
outputs = model(inputs)
|
||||
|
||||
# 混合损失:时域 MSE + 频域 FFT (权重可调)
|
||||
loss_t = mse_criterion(outputs, targets)
|
||||
loss_f = fft_loss(outputs, targets)
|
||||
loss = loss_t + 0.1 * loss_f
|
||||
loss = masked_l1_loss(outputs, targets, masks)
|
||||
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
|
||||
@@ -49,11 +47,13 @@ def train_model():
|
||||
model.eval()
|
||||
val_loss = 0.0
|
||||
with torch.no_grad():
|
||||
for inputs, targets in val_loader:
|
||||
inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)
|
||||
for inputs, targets, masks in val_loader:
|
||||
inputs = inputs.to(DEVICE)
|
||||
targets = targets.to(DEVICE)
|
||||
masks = masks.to(DEVICE)
|
||||
outputs = model(inputs)
|
||||
loss_t = mse_criterion(outputs, targets)
|
||||
val_loss += loss_t.item()
|
||||
loss = masked_l1_loss(outputs, targets, masks)
|
||||
val_loss += loss.item()
|
||||
|
||||
train_loss /= len(train_loader)
|
||||
val_loss /= len(val_loader)
|
||||
@@ -66,6 +66,12 @@ def train_model():
|
||||
best_val_loss = val_loss
|
||||
torch.save(model.state_dict(), 'best_model.pth')
|
||||
print(" --> Saved Best Model")
|
||||
no_improve_epochs = 0
|
||||
else:
|
||||
no_improve_epochs += 1
|
||||
if ENABLE_EARLY_STOP and no_improve_epochs >= early_stop_patience:
|
||||
print(f"Early stopping at epoch {epoch+1}")
|
||||
break
|
||||
|
||||
if __name__ == '__main__':
|
||||
train_model()
|
||||
|
||||
BIN
test_results.png
Normal file
BIN
test_results.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
Reference in New Issue
Block a user