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
|
# 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'
|
||||||
|
|||||||
106
src/dataset.py
106
src/dataset.py
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
42
src/train.py
42
src/train.py
@@ -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
BIN
test_results.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
Reference in New Issue
Block a user