diff --git a/best_model.pth b/best_model.pth index ef30ecb..d42eb89 100644 Binary files a/best_model.pth and b/best_model.pth differ diff --git a/src/__pycache__/config.cpython-310.pyc b/src/__pycache__/config.cpython-310.pyc index 25fd288..f445408 100644 Binary files a/src/__pycache__/config.cpython-310.pyc and b/src/__pycache__/config.cpython-310.pyc differ diff --git a/src/__pycache__/dataset.cpython-310.pyc b/src/__pycache__/dataset.cpython-310.pyc index ba9d6e0..62ffa0f 100644 Binary files a/src/__pycache__/dataset.cpython-310.pyc and b/src/__pycache__/dataset.cpython-310.pyc differ diff --git a/src/config.py b/src/config.py index a64e54d..1b610a7 100644 --- a/src/config.py +++ b/src/config.py @@ -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' diff --git a/src/dataset.py b/src/dataset.py index 1f24a3b..1c87f65 100644 --- a/src/dataset.py +++ b/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): """ diff --git a/src/evaluate.py b/src/evaluate.py index aca6659..f51a50d 100644 --- a/src/evaluate.py +++ b/src/evaluate.py @@ -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) diff --git a/src/train.py b/src/train.py index 0d8faf1..e71a811 100644 --- a/src/train.py +++ b/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() diff --git a/test_results.png b/test_results.png new file mode 100644 index 0000000..4178f66 Binary files /dev/null and b/test_results.png differ