modified: src/config.py

modified:   src/dataset.py
	modified:   src/evaluate.py
	modified:   src/train.py
This commit is contained in:
2026-04-30 14:59:35 +08:00
parent 45d1fd1139
commit a655143ec2
8 changed files with 126 additions and 40 deletions

Binary file not shown.

View File

@@ -10,17 +10,21 @@ BATCH_SIZE = 256
# Features Configuration # Features Configuration
INPUT_SENSOR = 'WSMS00012' INPUT_SENSOR = 'WSMS00012'
OUTPUT_SENSORS = ['WSMS00007', 'WSMS00008', 'WSMS00009', 'WSMS00010', 'WSMS00011'] OUTPUT_SENSORS = ['WSMS00007', 'WSMS00008', 'WSMS00009', 'WSMS00010', 'WSMS00011']
INPUT_AXIS = 'value1' # 底部传感器输入轴课程要求X轴
OUTPUT_AXIS = 'value3' # 目标传感器输出轴
# Model Configuration # Model Configuration
CHANNELS = [64, 128, 128, 256] # TCN 各层通道数 CHANNELS = [64, 64, 128, 128, 256, 256] # TCN 各层通道数
KERNEL_SIZE = 3 KERNEL_SIZE = 5
DROPOUT = 0.2 DROPOUT = 0.2
# Training Configuration # Training Configuration
LEARNING_RATE = 1e-3 LEARNING_RATE = 1e-4
EPOCHS = 50 EPOCHS = 50
WEIGHT_DECAY = 1e-4 WEIGHT_DECAY = 1e-3
ENABLE_EARLY_STOP = False
EARLY_STOP_PATIENCE = 15
# Device # Device
import torch import torch
DEVICE = 'cuda' DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'

View File

@@ -7,72 +7,146 @@ from torch.utils.data import Dataset, DataLoader
from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import StandardScaler
from config import * 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): class BuildingDataset(Dataset):
def __init__(self, file_paths, seq_len, step_size, scaler_X=None, scaler_Y=None, fit_scaler=False): def __init__(self, file_paths, seq_len, step_size, scaler_X=None, scaler_Y=None, fit_scaler=False):
self.seq_len = seq_len self.seq_len = seq_len
self.X_data = [] self.X_data = []
self.Y_data = [] self.Y_data = []
self.M_data = []
self.scaler_X = scaler_X if scaler_X is not None else StandardScaler() 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_X = []
raw_Y = [] raw_Y = []
raw_M = []
for f in file_paths: for f in file_paths:
# 读取数据 # 读取数据
df = pd.read_csv(f) df = pd.read_csv(f)
# 使用长表格式: code, type, time, value1, value2, value3 # 使用长表格式: code, type, time, value1, value2, value3
# 提取 012 的 X 轴作为基准 # 提取 012 的输入轴作为基准
df_in = df[df['code'] == INPUT_SENSOR][['time', 'value1']].rename(columns={'value1': 'input_x'}) 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 的时间为基准) # 提取 007~011 的 Z 轴并按时间戳逐步合并 (使用 left join 确保以 df_in 的时间为基准)
df_merged = df_in df_merged = df_in
for sens in OUTPUT_SENSORS: 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 = 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.sort_values('time').reset_index(drop=True)
df_merged = df_merged.interpolate(method='linear').bfill().ffill().fillna(0)
if len(df_merged) == 0: if len(df_merged) == 0:
print(f"Warning: Skipping file {f} due to no overlapping timestamps across required sensors.") print(f"Warning: Skipping file {f} due to no overlapping timestamps across required sensors.")
continue continue
x_seq = df_merged['input_x'].values.reshape(-1, 1) x_seq = df_merged['input_signal'].values.reshape(-1, 1).astype(np.float32)
# 提取所有 target 传感器的列
# 提取所有 target 传感器列与可用性掩码
out_cols = [f'out_{sens}' for sens in OUTPUT_SENSORS] 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_X.append(x_seq)
raw_Y.append(y_seq) raw_Y.append(y_seq)
raw_M.append(m_seq)
if len(raw_X) == 0:
raise ValueError("未能从文件中构造出有效序列,请检查数据路径与传感器编码配置。")
# 拼接所有文件数据进行 fit # 拼接所有文件数据进行 fit
X_all = np.vstack(raw_X) X_all = np.vstack(raw_X)
Y_all = np.vstack(raw_Y)
if fit_scaler: if fit_scaler:
self.scaler_X.fit(X_all) 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) x_seq_scaled = self.scaler_X.transform(x_seq)
y_seq_scaled = self.scaler_Y.transform(y_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): 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]) x_win = x_seq_scaled[i:i+seq_len]
self.Y_data.append(y_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.X_data = np.array(self.X_data)
self.Y_data = np.array(self.Y_data) self.Y_data = np.array(self.Y_data)
self.M_data = np.array(self.M_data)
def __len__(self): def __len__(self):
return len(self.X_data) return len(self.X_data)
def __getitem__(self, idx): def __getitem__(self, idx):
return torch.tensor(self.X_data[idx], dtype=torch.float32), \ return (
torch.tensor(self.Y_data[idx], dtype=torch.float32) 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): def get_dataloaders(condition='Non_TMD', include_free_vib=False):
""" """

View File

@@ -7,6 +7,8 @@ from model import BuildingTCN
def evaluate_model(): def evaluate_model():
_, _, test_loader, scaler_X, scaler_Y = get_dataloaders() _, _, 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, model = BuildingTCN(input_size=1, output_size=5, num_channels=CHANNELS,
kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE) kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE)
@@ -17,7 +19,7 @@ def evaluate_model():
all_targets = [] all_targets = []
with torch.no_grad(): with torch.no_grad():
for inputs, targets in test_loader: for inputs, targets, masks in test_loader:
inputs = inputs.to(DEVICE) inputs = inputs.to(DEVICE)
outputs = model(inputs) outputs = model(inputs)

View File

@@ -5,11 +5,10 @@ from config import *
from dataset import get_dataloaders from dataset import get_dataloaders
from model import BuildingTCN from model import BuildingTCN
def fft_loss(pred, target): def masked_l1_loss(pred, target, mask):
"""计算频域损失""" diff = torch.abs(pred - target) * mask
pred_fft = torch.fft.rfft(pred, dim=1) denom = mask.sum().clamp(min=1.0)
target_fft = torch.fft.rfft(target, dim=1) return diff.sum() / denom
return nn.L1Loss()(torch.abs(pred_fft), torch.abs(target_fft))
def train_model(): def train_model():
train_loader, val_loader, _, _, _ = get_dataloaders() train_loader, val_loader, _, _, _ = get_dataloaders()
@@ -19,25 +18,24 @@ def train_model():
kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE) kernel_size=KERNEL_SIZE, dropout=DROPOUT).to(DEVICE)
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY) optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=3, factor=0.5) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=8, factor=0.5)
mse_criterion = nn.MSELoss()
best_val_loss = float('inf') best_val_loss = float('inf')
no_improve_epochs = 0
early_stop_patience = EARLY_STOP_PATIENCE
for epoch in range(EPOCHS): for epoch in range(EPOCHS):
model.train() model.train()
train_loss = 0.0 train_loss = 0.0
for inputs, targets in train_loader: for inputs, targets, masks in train_loader:
inputs, targets = inputs.to(DEVICE), targets.to(DEVICE) inputs = inputs.to(DEVICE)
targets = targets.to(DEVICE)
masks = masks.to(DEVICE)
optimizer.zero_grad() optimizer.zero_grad()
outputs = model(inputs) outputs = model(inputs)
loss = masked_l1_loss(outputs, targets, masks)
# 混合损失:时域 MSE + 频域 FFT (权重可调)
loss_t = mse_criterion(outputs, targets)
loss_f = fft_loss(outputs, targets)
loss = loss_t + 0.1 * loss_f
loss.backward() loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
@@ -49,11 +47,13 @@ def train_model():
model.eval() model.eval()
val_loss = 0.0 val_loss = 0.0
with torch.no_grad(): with torch.no_grad():
for inputs, targets in val_loader: for inputs, targets, masks in val_loader:
inputs, targets = inputs.to(DEVICE), targets.to(DEVICE) inputs = inputs.to(DEVICE)
targets = targets.to(DEVICE)
masks = masks.to(DEVICE)
outputs = model(inputs) outputs = model(inputs)
loss_t = mse_criterion(outputs, targets) loss = masked_l1_loss(outputs, targets, masks)
val_loss += loss_t.item() val_loss += loss.item()
train_loss /= len(train_loader) train_loss /= len(train_loader)
val_loss /= len(val_loader) val_loss /= len(val_loader)
@@ -66,6 +66,12 @@ def train_model():
best_val_loss = val_loss best_val_loss = val_loss
torch.save(model.state_dict(), 'best_model.pth') torch.save(model.state_dict(), 'best_model.pth')
print(" --> Saved Best Model") 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__': if __name__ == '__main__':
train_model() train_model()

BIN
test_results.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB