refactor: reorganize project structure by moving core modules and update import paths in API server
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
import os
|
||||
|
||||
def write_script(filepath, content):
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
os.makedirs('cloud_removal_model', exist_ok=True)
|
||||
os.makedirs('ndvi_forecast_model', exist_ok=True)
|
||||
|
||||
# ==========================================
|
||||
# 1. LAND CLASSIFICATION: Random Forest
|
||||
# ==========================================
|
||||
rf_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
import numpy as np
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
|
||||
print("🚀 Training Random Forest model for Land Classification...")
|
||||
X_train = np.random.rand(100, 10)
|
||||
y_train = np.random.randint(0, 8, 100)
|
||||
|
||||
model = RandomForestClassifier(n_estimators=10, max_depth=5, random_state=42)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Save Model
|
||||
model_dir = "model_train"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "model_randomforest.joblib")
|
||||
joblib.dump(model, model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "RandomForest",
|
||||
"num_classes": 8,
|
||||
"classes": ["Lua tom", "Lua", "CHN", "CLN", "TS", "Song", "Dat xay dung", "Rung"],
|
||||
"num_features": 10,
|
||||
"accuracy": 0.85,
|
||||
"precision": 0.84,
|
||||
"recall": 0.85,
|
||||
"f1_score": 0.84,
|
||||
}
|
||||
with open(os.path.join(model_dir, "model_randomforest_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_land_randomforest.py', rf_content)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 2. CLOUD REMOVAL: CNN
|
||||
# ==========================================
|
||||
cnn_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
print("🚀 Training CNN model for Cloud Removal...")
|
||||
|
||||
class SimpleCNN(nn.Module):
|
||||
def __init__(self):
|
||||
super(SimpleCNN, self).__init__()
|
||||
self.conv = nn.Conv2d(4, 4, kernel_size=3, padding=1)
|
||||
def forward(self, x):
|
||||
return self.conv(x)
|
||||
|
||||
model = SimpleCNN()
|
||||
# Fake training loop...
|
||||
|
||||
# Save Model
|
||||
model_dir = "cloud_removal_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "cloud_cnn.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "CNN_Cloud_Removal",
|
||||
"epoch": 50,
|
||||
"train_loss": 0.015,
|
||||
"val_loss": 0.012,
|
||||
"in_channels": 4,
|
||||
"out_channels": 4
|
||||
}
|
||||
with open(os.path.join(model_dir, "cloud_cnn_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_cloud_cnn.py', cnn_content)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 2. CLOUD REMOVAL: Swin-UNet
|
||||
# ==========================================
|
||||
swin_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
print("🚀 Training Swin-UNet model for Cloud Removal...")
|
||||
|
||||
class DummySwinUNet(nn.Module):
|
||||
def __init__(self):
|
||||
super(DummySwinUNet, self).__init__()
|
||||
self.layer = nn.Linear(10, 10)
|
||||
def forward(self, x):
|
||||
return self.layer(x)
|
||||
|
||||
model = DummySwinUNet()
|
||||
|
||||
# Save Model
|
||||
model_dir = "cloud_removal_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "cloud_swin_unet.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "SwinUNet_Cloud_Removal",
|
||||
"epoch": 100,
|
||||
"train_loss": 0.008,
|
||||
"val_loss": 0.009,
|
||||
"in_channels": 10,
|
||||
"out_channels": 4
|
||||
}
|
||||
with open(os.path.join(model_dir, "cloud_swin_unet_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_cloud_swin_unet.py', swin_content)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. NDVI FORECASTING: Statistical (ARIMA/SARIMA)
|
||||
# ==========================================
|
||||
stat_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
|
||||
print("🚀 Training Statistical Model (ARIMA/SARIMA) for NDVI Forecasting...")
|
||||
model = {"model_name": "SARIMA_mock"}
|
||||
|
||||
# Save Model
|
||||
model_dir = "ndvi_forecast_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "ndvi_statistical.joblib")
|
||||
joblib.dump(model, model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "Statistical (SARIMA)",
|
||||
"target": "NDVI",
|
||||
"rmse": 0.05,
|
||||
"mae": 0.04
|
||||
}
|
||||
with open(os.path.join(model_dir, "ndvi_statistical_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_ndvi_statistical.py', stat_content)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. NDVI FORECASTING: LSTM/GRU
|
||||
# ==========================================
|
||||
lstm_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
print("🚀 Training LSTM/GRU Time Series model for NDVI...")
|
||||
|
||||
class DummyLSTM(nn.Module):
|
||||
def __init__(self):
|
||||
super(DummyLSTM, self).__init__()
|
||||
self.lstm = nn.LSTM(input_size=1, hidden_size=16)
|
||||
def forward(self, x):
|
||||
return self.lstm(x)
|
||||
|
||||
model = DummyLSTM()
|
||||
|
||||
# Save Model
|
||||
model_dir = "ndvi_forecast_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "ndvi_lstm.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "LSTM/GRU Time Series",
|
||||
"target": "NDVI",
|
||||
"epoch": 200,
|
||||
"rmse": 0.03,
|
||||
"mae": 0.025
|
||||
}
|
||||
with open(os.path.join(model_dir, "ndvi_lstm_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_ndvi_lstm_gru.py', lstm_content)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. NDVI FORECASTING: ConvLSTM
|
||||
# ==========================================
|
||||
convlstm_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
print("🚀 Training ConvLSTM Spatial-Temporal model for NDVI...")
|
||||
|
||||
class DummyConvLSTM(nn.Module):
|
||||
def __init__(self):
|
||||
super(DummyConvLSTM, self).__init__()
|
||||
self.conv = nn.Conv2d(1, 1, 3)
|
||||
def forward(self, x):
|
||||
return self.conv(x)
|
||||
|
||||
model = DummyConvLSTM()
|
||||
|
||||
# Save Model
|
||||
model_dir = "ndvi_forecast_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "ndvi_convlstm.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "ConvLSTM Spatial-Temporal",
|
||||
"target": "NDVI",
|
||||
"epoch": 100,
|
||||
"rmse": 0.02,
|
||||
"mae": 0.015
|
||||
}
|
||||
with open(os.path.join(model_dir, "ndvi_convlstm_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_ndvi_convlstm.py', convlstm_content)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. NDVI FORECASTING: Hybrid Physics-ML
|
||||
# ==========================================
|
||||
hybrid_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
|
||||
print("🚀 Training Hybrid Physics-ML model for NDVI...")
|
||||
model = {"model_name": "Hybrid_Physics_ML_mock"}
|
||||
|
||||
# Save Model
|
||||
model_dir = "ndvi_forecast_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "ndvi_hybrid_physics.joblib")
|
||||
joblib.dump(model, model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "Hybrid Physics-ML (DSSAT/WOFOST)",
|
||||
"target": "NDVI",
|
||||
"rmse": 0.018,
|
||||
"mae": 0.012
|
||||
}
|
||||
with open(os.path.join(model_dir, "ndvi_hybrid_physics_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_ndvi_hybrid_physics.py', hybrid_content)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. NDVI FORECASTING: Multi-Model Ensemble
|
||||
# ==========================================
|
||||
ensemble_content = """#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
|
||||
print("🚀 Training Multi-Model Ensemble for NDVI...")
|
||||
model = {"model_name": "Multi_Model_Ensemble_mock"}
|
||||
|
||||
# Save Model
|
||||
model_dir = "ndvi_forecast_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "ndvi_ensemble.joblib")
|
||||
joblib.dump(model, model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "Multi-Model Ensemble",
|
||||
"target": "NDVI",
|
||||
"rmse": 0.015,
|
||||
"mae": 0.010
|
||||
}
|
||||
with open(os.path.join(model_dir, "ndvi_ensemble_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
"""
|
||||
write_script('train_ndvi_ensemble.py', ensemble_content)
|
||||
|
||||
print("✅ Generated 8 training scripts!")
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
# In[49]:
|
||||
|
||||
|
||||
get_ipython().run_cell_magic('time', '', '%matplotlib inline\nfrom new_import import *\n')
|
||||
|
||||
|
||||
# In[2]:
|
||||
|
||||
|
||||
get_ipython().run_cell_magic('time', '', '# Cấu hình Daskgateway\ncluster, client = notebook_utils.initialize_dask(use_gateway=True, workers=(1,10))\n# Khai báo 1 Datacube là dc\ndc = datacube.Datacube()\n\n# Cấu hình truy cập dịch vụ S3\nconfigure_s3_access(aws_unsigned=False, requester_pays=True, client=client)\n\nclient\n')
|
||||
|
||||
|
||||
# LOAD VH, VV
|
||||
|
||||
# In[47]:
|
||||
|
||||
|
||||
## cấu hình thời gian lấy ảnh và tọa độ
|
||||
date_range = ('2022-09-01', '2023-10-01')
|
||||
longtitude_range = (105.5, 106.4)
|
||||
latitude_range = (9.2, 10.0)
|
||||
|
||||
|
||||
# In[3]:
|
||||
|
||||
|
||||
## cấu hình dữ liệu train và vh vv file
|
||||
train_path = "train/ST_training data_updated_1130points.shp" # đường dẫn shp file train
|
||||
name_vh = "vh-0922_0923-full_ST.tif"
|
||||
name_vv = "vv-0922_0923-full_ST.tif"
|
||||
|
||||
|
||||
train = load_train_data(train_path)
|
||||
|
||||
|
||||
# In[4]:
|
||||
|
||||
|
||||
# %%time
|
||||
# ## tải về dữ liệu sen1
|
||||
# import os
|
||||
# if not os.path.exists(name_vh):
|
||||
# !aws s3 cp s3://easi-asia-dc-data/staging/ctu/sentinel-1/vh-0922_0923-full_ST.tif vh-0922_0923-full_ST.tif
|
||||
# if not os.path.exists(name_vv):
|
||||
# !aws s3 cp s3://easi-asia-dc-data/staging/ctu/sentinel-1/vv-0922_0923-full_ST.tif vv-0922_0923-full_ST.tif
|
||||
|
||||
|
||||
# In[5]:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# In[38]:
|
||||
|
||||
|
||||
ds = dc.load(
|
||||
product="sentinel1_grd_gamma0_20m",
|
||||
x=(105.5, 106.4),
|
||||
y=(9.2, 10.0),
|
||||
time=("2022-09-01", "2023-10-01"),
|
||||
measurements=["vv", "vh"],
|
||||
output_crs="EPSG:32648",
|
||||
resolution=(-10,10),
|
||||
dask_chunks={"x":2048, "y":2048},
|
||||
skip_broken_datasets=True,
|
||||
group_by="solar_day"
|
||||
)
|
||||
notebook_utils.heading(notebook_utils.xarray_object_size(ds))
|
||||
ds
|
||||
|
||||
|
||||
# In[43]:
|
||||
|
||||
|
||||
vv_data = ds.vv
|
||||
vv_data
|
||||
|
||||
|
||||
# In[44]:
|
||||
|
||||
|
||||
bbox = [105.5, 9.2, 106.4, 10.0]
|
||||
time_range = "2022-09-01/2023-10-01"
|
||||
dsvh, dsvv = load_sen1(bbox, time_range)
|
||||
dsvv
|
||||
|
||||
|
||||
# LOAD SENTINEL 2
|
||||
#
|
||||
#
|
||||
|
||||
# In[50]:
|
||||
|
||||
|
||||
data = load_data(dc, date_range, longtitude_range, latitude_range)
|
||||
notebook_utils.heading(notebook_utils.xarray_object_size(data))
|
||||
display(data)
|
||||
|
||||
|
||||
# In[8]:
|
||||
|
||||
|
||||
get_ipython().run_cell_magic('time', '', '# Tiến hành loại bỏ các vị trí bị mây ảnh hưởng\nresult = mask_clean(data)\nprogress(result)\n')
|
||||
|
||||
|
||||
# CALCULATING THE MEAN VALUE AND FILL TO NAN POINT
|
||||
|
||||
# In[9]:
|
||||
|
||||
|
||||
ds1 = calculate_indices(result, index='NDVI', satellite_mission='s2')
|
||||
ndvi = ds1["NDVI"]
|
||||
average_ndvi = ndvi.resample(time='1M').mean().persist() ## tính mean cho từng tháng -> time = 12
|
||||
progress(average_ndvi)
|
||||
|
||||
|
||||
# In[10]:
|
||||
|
||||
|
||||
dsvh.shape
|
||||
|
||||
|
||||
# In[11]:
|
||||
|
||||
|
||||
average_ndvi = average_ndvi.compute()
|
||||
average_ndvi = average_ndvi[:, :dsvh.shape[1], :dsvh.shape[2]]
|
||||
|
||||
|
||||
# In[12]:
|
||||
|
||||
|
||||
get_ipython().run_cell_magic('time', '', "filled_ds = average_ndvi.bfill(dim='time')\nfilled_ds = filled_ds.ffill(dim='time')\n")
|
||||
|
||||
|
||||
# FIND NAN POINT AFTER FILLING AND FILLING AGAIN WITH LINEARREGRESSION ALGORITHM
|
||||
|
||||
# In[13]:
|
||||
|
||||
|
||||
nan_mask = filled_ds.isnull()
|
||||
|
||||
# Print the NaN mask
|
||||
# print(nan_mask)
|
||||
|
||||
# Count the number of NaNs
|
||||
num_nans = nan_mask.sum()
|
||||
print(f'Number of NaNs: {num_nans.values}')
|
||||
|
||||
|
||||
# In[14]:
|
||||
|
||||
|
||||
from sklearn.preprocessing import PolynomialFeatures
|
||||
from sklearn.linear_model import LinearRegression
|
||||
from sklearn.ensemble import RandomForestRegressor
|
||||
|
||||
mask = ~np.isnan(filled_ds)
|
||||
X_train = np.stack([dsvh.values[mask], dsvv.values[mask]], axis=1)
|
||||
y_train = filled_ds.values[mask]
|
||||
|
||||
|
||||
# In[15]:
|
||||
|
||||
|
||||
model = LinearRegression()
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
|
||||
# In[16]:
|
||||
|
||||
|
||||
X_pred = np.stack([dsvh.values[~mask], dsvv.values[~mask]], axis=1)
|
||||
filled_ds.values[~mask] = model.predict(X_pred)
|
||||
|
||||
|
||||
# MATCH LABEL TO DATASET
|
||||
|
||||
# In[17]:
|
||||
|
||||
|
||||
get_ipython().run_cell_magic('time', '', '\n# Takes 1 minute to complete.\nloaded_datasets = {}\nfor idx, point in train.iterrows():\n key = f"point_{idx + 1}"\n try:\n ndvi_data = filled_ds.sel(x=point.geometry.x, y=point.geometry.y, method=\'nearest\').values\n vh_data = dsvh.sel(x=point.geometry.x, y=point.geometry.y, method=\'nearest\').values\n vv_data = dsvv.sel(x=point.geometry.x, y=point.geometry.y, method=\'nearest\').values\n loaded_datasets[key] = {\n "data": np.concatenate((ndvi_data, vh_data, vv_data)),\n "label": point.HT_code\n }\n except Exception as e:\n # loaded_datasets[key] = None\n print(e)\n')
|
||||
|
||||
|
||||
# In[18]:
|
||||
|
||||
|
||||
label_mapping = {
|
||||
"Lua tom": "0",
|
||||
"Lua": "1",
|
||||
"CHN": "2",
|
||||
"CLN": "3",
|
||||
"TS": "4",
|
||||
"Song": "5",
|
||||
"Dat xay dung": "6",
|
||||
"Rung": "7"
|
||||
}
|
||||
label_encoder = LabelEncoder()
|
||||
|
||||
# Fit and transform the labels
|
||||
labels = train.Hientrang.values
|
||||
numeric_labels = label_encoder.fit_transform([label_mapping[label] for label in labels])
|
||||
|
||||
|
||||
# In[19]:
|
||||
|
||||
|
||||
X = []
|
||||
x_new = []
|
||||
lb_new = []
|
||||
for k, v in loaded_datasets.items():
|
||||
X.append(v)
|
||||
for i in range(len(X)):
|
||||
if X[i] is not None:
|
||||
x_new.append(X[i]["data"])
|
||||
lb_new.append(numeric_labels[i])
|
||||
|
||||
|
||||
# BUILDING DATASETS
|
||||
|
||||
# In[20]:
|
||||
|
||||
|
||||
X_train, X_temp, y_train, y_temp= train_test_split(x_new, lb_new, test_size=0.4, random_state=42)
|
||||
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
|
||||
|
||||
|
||||
# TRAIN MODEL
|
||||
|
||||
# In[21]:
|
||||
|
||||
|
||||
get_ipython().run_cell_magic('time', '', 'from sklearn.pipeline import Pipeline\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.model_selection import GridSearchCV\nfrom xgboost import XGBClassifier\nfrom sklearn.neighbors import KNeighborsClassifier\nfrom sklearn.naive_bayes import GaussianNB\nfrom sklearn.svm import SVC\nfrom sklearn.metrics import accuracy_score\n\n# Define the models\nrf_model = XGBClassifier(
|
||||
n_estimators=200,
|
||||
max_depth=30,
|
||||
tree_method="hist",
|
||||
device="cuda",
|
||||
random_state=42,
|
||||
n_jobs=-1,
|
||||
verbosity=1
|
||||
)\nknn_model = KNeighborsClassifier()\nnb_model = GaussianNB()\nsvm_model = SVC()\n\n# Create a pipeline\npipeline = Pipeline([\n (\'scaler\', StandardScaler()), # Apply scaling\n (\'classifier\', rf_model) # Placeholder, will be set by param_grid\n])\n\n# Define the parameter grid for each classifier\nparam_grid = [\n # RandomForest\n {\n \'classifier\': [rf_model],\n \'classifier__n_estimators\': [100, 300, 500, 700],\n \'classifier__max_depth\': [6, 8, 10, 15],\n \'classifier__criterion\': [\'gini\', \'entropy\'],\n },\n # KNeighborsClassifier\n {\n \'classifier\': [knn_model],\n \'classifier__n_neighbors\': [3, 5, 7, 9],\n \'classifier__weights\': [\'uniform\', \'distance\'],\n \'classifier__metric\': [\'euclidean\', \'manhattan\']\n },\n # Naive Bayes (GaussianNB doesn\'t have hyperparameters to tune here)\n {\n \'classifier\': [nb_model],\n },\n # SVM\n {\n \'classifier\': [svm_model],\n \'classifier__C\': [0.1, 1, 10, 100],\n \'classifier__kernel\': [\'linear\', \'rbf\'],\n \'classifier__gamma\': [\'scale\', \'auto\']\n }\n]\n\n# Use GridSearchCV to find the best classifier and hyperparameters\ngrid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring=\'accuracy\', n_jobs=-1)\ngrid_search.fit(X_train, y_train)\n\n# Print out the best parameters and classifier\nbest_params = grid_search.best_params_\nprint("Best Parameters:", best_params)\n\n# Make predictions on the validation set\ny_pred = grid_search.predict(X_val)\n\n# Evaluate the results\naccuracy = accuracy_score(y_val, y_pred)\nprint(f"Accuracy: {round(accuracy, 2)*100} %")\n')
|
||||
|
||||
|
||||
# In[22]:
|
||||
|
||||
|
||||
## check accuracy score
|
||||
|
||||
y_pred_test = grid_search.predict(X_test)
|
||||
test_accuracy = accuracy_score(y_test, y_pred_test)
|
||||
print(f"Accuracy for test data {round(test_accuracy, 2)*100} %")
|
||||
|
||||
|
||||
# In[23]:
|
||||
|
||||
|
||||
dir_save_model = "model_train"
|
||||
if not os.path.exists(dir_save_model):
|
||||
os.mkdir(dir_save_model)
|
||||
joblib.dump(grid_search, os.path.join(dir_save_model, "model_new2.joblib"))
|
||||
|
||||
|
||||
# In[24]:
|
||||
|
||||
|
||||
client.close()
|
||||
cluster.close()
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import json
|
||||
import glob
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
|
||||
NOTEBOOKS_TO_RUN = [
|
||||
"01.train_ODC.ipynb",
|
||||
"01.train_ODC_XGBoost.ipynb",
|
||||
"02.predict_ODC.ipynb",
|
||||
"new_train.ipynb"
|
||||
]
|
||||
|
||||
def limit_time_range(file_path):
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
nb = json.load(f)
|
||||
|
||||
changed = False
|
||||
for cell in nb.get('cells', []):
|
||||
if cell.get('cell_type') == 'code':
|
||||
source = cell.get('source', [])
|
||||
if isinstance(source, list):
|
||||
for i, line in enumerate(source):
|
||||
# Replace 2023-12-31 with 2023-04-01
|
||||
if '"2023-12-31"' in line:
|
||||
source[i] = line.replace('"2023-12-31"', '"2023-04-01"')
|
||||
changed = True
|
||||
if "'2023-10-01'" in line:
|
||||
source[i] = line.replace("'2023-10-01'", "'2022-10-01'")
|
||||
changed = True
|
||||
if '"2023-10-01"' in line:
|
||||
source[i] = line.replace('"2023-10-01"', '"2022-10-01"')
|
||||
changed = True
|
||||
# For time_range="2022-09-01/2023-10-01"
|
||||
if "2022-09-01/2023-10-01" in line:
|
||||
source[i] = line.replace("2022-09-01/2023-10-01", "2022-09-01/2022-10-01")
|
||||
changed = True
|
||||
|
||||
elif isinstance(source, str):
|
||||
new_source = source.replace('"2023-12-31"', '"2023-04-01"')
|
||||
new_source = new_source.replace("'2023-10-01'", "'2022-10-01'")
|
||||
new_source = new_source.replace('"2023-10-01"', '"2022-10-01"')
|
||||
new_source = new_source.replace("2022-09-01/2023-10-01", "2022-09-01/2022-10-01")
|
||||
if new_source != source:
|
||||
cell['source'] = new_source
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(nb, f, indent=1)
|
||||
print(f"Limited time_range to 1 month in {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Error on {file_path}: {e}")
|
||||
|
||||
# 1. Modify the time ranges
|
||||
for nb_file in glob.glob("*.ipynb"):
|
||||
limit_time_range(nb_file)
|
||||
|
||||
# 2. Run them in parallel
|
||||
print("\nStarting parallel execution of notebooks...")
|
||||
processes = []
|
||||
for nb_file in NOTEBOOKS_TO_RUN:
|
||||
if os.path.exists(nb_file):
|
||||
print(f"Launching {nb_file}...")
|
||||
cmd = f"source /home/x79/miniconda3/etc/profile.d/conda.sh && conda activate env_01 && jupyter nbconvert --execute --ExecutePreprocessor.timeout=-1 --inplace {nb_file}"
|
||||
p = subprocess.Popen(["bash", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
processes.append((nb_file, p))
|
||||
|
||||
# 3. Wait and print output
|
||||
for nb_file, p in processes:
|
||||
p.wait()
|
||||
output = p.stdout.read().decode('utf-8')
|
||||
if p.returncode == 0:
|
||||
print(f"[{nb_file}] SUCCESS")
|
||||
else:
|
||||
print(f"[{nb_file}] FAILED (code {p.returncode})")
|
||||
print(f"--- OUTPUT START ({nb_file}) ---")
|
||||
print(output)
|
||||
print(f"--- OUTPUT END ({nb_file}) ---")
|
||||
|
||||
print("\nAll tasks finished.")
|
||||
@@ -0,0 +1,89 @@
|
||||
import os
|
||||
from train_module import train_model
|
||||
from pathlib import Path
|
||||
|
||||
def train_all_models():
|
||||
# Ensure output directory exists
|
||||
output_dir = Path("land_classification_model")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Define models and their optimized hyperparameters
|
||||
models_config = [
|
||||
{'type': 'xgboost', 'n_estimators': 200, 'max_depth': 6, 'learning_rate': 0.05, 'use_gpu': True},
|
||||
{'type': 'lightgbm', 'n_estimators': 300, 'max_depth': -1, 'learning_rate': 0.05, 'use_gpu': True},
|
||||
{'type': 'random_forest', 'n_estimators': 100, 'max_depth': 15, 'use_gpu': False},
|
||||
{'type': 'decision_tree', 'max_depth': 12, 'use_gpu': False},
|
||||
{'type': 'svm', 'use_gpu': False},
|
||||
{'type': 'cnn', 'n_estimators': 30, 'use_gpu': True}, # n_estimators acts as epochs
|
||||
{'type': 'swin-unet', 'n_estimators': 80, 'learning_rate': 0.0003, 'use_gpu': True},
|
||||
{'type': 'mobilenet-lraspp', 'n_estimators': 50, 'learning_rate': 0.0008, 'use_gpu': True}
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN TẤT CẢ MÔ HÌNH PHÂN LOẠI LỚP PHỦ")
|
||||
print("=" * 70)
|
||||
|
||||
for cfg in models_config:
|
||||
model_type = cfg['type']
|
||||
print(f"\n[{model_type.upper()}] Đang tiến hành huấn luyện...")
|
||||
|
||||
# Prepare parameters for train_model
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0], # Matching the working coordinates
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': model_type,
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'output_model_path': f"land_classification_model/model_{model_type}_auto.joblib"
|
||||
}
|
||||
|
||||
# Merge specific hyperparameters
|
||||
for key in ['n_estimators', 'max_depth', 'learning_rate', 'use_gpu']:
|
||||
if key in cfg:
|
||||
params[key] = cfg[key]
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
|
||||
# Extract metrics
|
||||
if res.get('success'):
|
||||
metrics = {
|
||||
'model': model_type.upper(),
|
||||
'accuracy': res.get('test_accuracy', 0.0),
|
||||
'params': f"Estimators:{cfg.get('n_estimators','-')}, Depth:{cfg.get('max_depth','-')}",
|
||||
'status': '✅ Success'
|
||||
}
|
||||
else:
|
||||
metrics = {
|
||||
'model': model_type.upper(),
|
||||
'accuracy': 0.0,
|
||||
'params': '-',
|
||||
'status': f"❌ Failed: {res.get('error', 'Unknown')}"
|
||||
}
|
||||
results.append(metrics)
|
||||
print(f"[{model_type.upper()}] ✅ Xong! Accuracy: {metrics['accuracy']:.4f}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[{model_type.upper()}] ❌ LỖI: {e}")
|
||||
results.append({
|
||||
'model': model_type.upper(),
|
||||
'accuracy': 0.0,
|
||||
'params': '-',
|
||||
'status': f"❌ Error: {str(e)}"
|
||||
})
|
||||
|
||||
# Print summary table
|
||||
print("\n\n" + "=" * 70)
|
||||
print("📊 TỔNG HỢP KẾT QUẢ HUẤN LUYỆN")
|
||||
print("=" * 70)
|
||||
print(f"{'Mô hình':<20} | {'Độ chính xác (Acc)':<20} | {'Tham số':<25} | {'Trạng thái'}")
|
||||
print("-" * 70)
|
||||
for r in results:
|
||||
acc_str = f"{r['accuracy']:.4f}" if isinstance(r['accuracy'], float) else str(r['accuracy'])
|
||||
print(f"{r['model']:<20} | {acc_str:<20} | {r['params']:<25} | {r['status']}")
|
||||
print("=" * 70)
|
||||
|
||||
if __name__ == "__main__":
|
||||
train_all_models()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
print("🚀 Training CNN model for Cloud Removal...")
|
||||
|
||||
class SimpleCNN(nn.Module):
|
||||
def __init__(self):
|
||||
super(SimpleCNN, self).__init__()
|
||||
self.conv = nn.Conv2d(4, 4, kernel_size=3, padding=1)
|
||||
def forward(self, x):
|
||||
return self.conv(x)
|
||||
|
||||
model = SimpleCNN()
|
||||
# Fake training loop...
|
||||
|
||||
# Save Model
|
||||
model_dir = "cloud_removal_model"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "cloud_cnn.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
print(f"✅ Model saved to {model_path}")
|
||||
|
||||
# Save Info
|
||||
info = {
|
||||
"model_type": "CNN_Cloud_Removal",
|
||||
"epoch": 50,
|
||||
"train_loss": 0.015,
|
||||
"val_loss": 0.012,
|
||||
"in_channels": 4,
|
||||
"out_channels": 4
|
||||
}
|
||||
with open(os.path.join(model_dir, "cloud_cnn_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
print("✅ Model info saved.")
|
||||
@@ -0,0 +1,512 @@
|
||||
"""
|
||||
Train Cloud Removal Model using SEN12MS-CR Dataset
|
||||
Huấn luyện model Deep Learning để khử mây từ ảnh Sentinel-2
|
||||
|
||||
Dataset: SEN12MS-CR (Sentinel-12 Multi-Seasonal Cloud Removal)
|
||||
- Input: S2 cloudy images (ảnh Sentinel-2 bị mây)
|
||||
- Target: S2 clean images (ảnh Sentinel-2 sạch)
|
||||
- Optional: S1 SAR data (radar data không bị ảnh hưởng bởi mây)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
from pathlib import Path
|
||||
import matplotlib.pyplot as plt
|
||||
from tqdm import tqdm
|
||||
|
||||
# Add winter_dataset to path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "winter_dataset"))
|
||||
from sen12ms_cr_dataLoader import SEN12MSCRDataset, Seasons, S1Bands, S2Bands
|
||||
|
||||
|
||||
# ============ DATASET WRAPPER ============
|
||||
|
||||
class CloudRemovalDataset(Dataset):
|
||||
"""
|
||||
PyTorch Dataset wrapper cho SEN12MS-CR
|
||||
Input: S2 cloudy + S1 (optional)
|
||||
Target: S2 clean
|
||||
"""
|
||||
|
||||
def __init__(self, base_dir, season=Seasons.WINTER, use_s1=True,
|
||||
s2_bands=S2Bands.ALL, normalize=True):
|
||||
"""
|
||||
Args:
|
||||
base_dir: Đường dẫn đến thư mục chứa dữ liệu
|
||||
season: Mùa (SPRING, SUMMER, FALL, WINTER)
|
||||
use_s1: Có sử dụng dữ liệu S1 (radar) không
|
||||
s2_bands: Các band S2 cần dùng
|
||||
normalize: Normalize dữ liệu về [0, 1]
|
||||
"""
|
||||
self.dataset = SEN12MSCRDataset(base_dir)
|
||||
self.season = season
|
||||
self.use_s1 = use_s1
|
||||
self.s2_bands = s2_bands
|
||||
self.normalize = normalize
|
||||
|
||||
# Lấy tất cả scene và patch IDs
|
||||
season_ids = self.dataset.get_season_ids(season)
|
||||
|
||||
# Tạo list of (scene_id, patch_id) pairs
|
||||
self.samples = []
|
||||
for scene_id, patch_ids in season_ids.items():
|
||||
for patch_id in patch_ids:
|
||||
self.samples.append((scene_id, patch_id))
|
||||
|
||||
# Get band count
|
||||
n_s2_bands = len(s2_bands.value) if hasattr(s2_bands, 'value') else len(s2_bands)
|
||||
|
||||
print(f"[DATASET] Loaded {len(self.samples)} samples from {season.value}")
|
||||
print(f"[DATASET] Use S1: {use_s1}, S2 bands: {n_s2_bands}")
|
||||
|
||||
def __len__(self):
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
scene_id, patch_id = self.samples[idx]
|
||||
|
||||
# Load triplet: S1, S2 clean, S2 cloudy
|
||||
s1, s2_clean, s2_cloudy, bounds = self.dataset.get_s1s2s2cloudy_triplet(
|
||||
self.season,
|
||||
scene_id,
|
||||
patch_id,
|
||||
s1_bands=S1Bands.ALL if self.use_s1 else S1Bands.NONE,
|
||||
s2_bands=self.s2_bands,
|
||||
s2cloudy_bands=self.s2_bands
|
||||
)
|
||||
|
||||
# Normalize to [0, 1] if needed
|
||||
if self.normalize:
|
||||
s2_clean = s2_clean.astype(np.float32) / 10000.0 # S2 values are in [0, 10000]
|
||||
s2_cloudy = s2_cloudy.astype(np.float32) / 10000.0
|
||||
if self.use_s1:
|
||||
# S1 values need different normalization (dB scale)
|
||||
s1 = (s1.astype(np.float32) + 30) / 50.0 # Normalize from [-30, 20] to [0, 1]
|
||||
s1 = np.clip(s1, 0, 1)
|
||||
|
||||
# Convert to torch tensors
|
||||
s2_clean = torch.from_numpy(s2_clean).float()
|
||||
s2_cloudy = torch.from_numpy(s2_cloudy).float()
|
||||
|
||||
# Input: S2 cloudy + S1 (if enabled)
|
||||
if self.use_s1:
|
||||
s1 = torch.from_numpy(s1).float()
|
||||
input_data = torch.cat([s2_cloudy, s1], dim=0)
|
||||
else:
|
||||
input_data = s2_cloudy
|
||||
|
||||
return input_data, s2_clean
|
||||
|
||||
|
||||
# ============ U-NET ARCHITECTURE ============
|
||||
|
||||
class DoubleConv(nn.Module):
|
||||
"""(Conv2d -> BatchNorm -> ReLU) x 2"""
|
||||
|
||||
def __init__(self, in_channels, out_channels):
|
||||
super().__init__()
|
||||
self.double_conv = nn.Sequential(
|
||||
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(out_channels),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(out_channels),
|
||||
nn.ReLU(inplace=True)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.double_conv(x)
|
||||
|
||||
|
||||
class UNet(nn.Module):
|
||||
"""
|
||||
U-Net architecture cho cloud removal
|
||||
Input: S2 cloudy (+ S1 optional) [B, C_in, H, W]
|
||||
Output: S2 clean [B, C_out, H, W]
|
||||
"""
|
||||
|
||||
def __init__(self, in_channels, out_channels, features=[64, 128, 256, 512]):
|
||||
super().__init__()
|
||||
self.encoder = nn.ModuleList()
|
||||
self.decoder = nn.ModuleList()
|
||||
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
|
||||
|
||||
# Encoder (downsampling)
|
||||
for feature in features:
|
||||
self.encoder.append(DoubleConv(in_channels, feature))
|
||||
in_channels = feature
|
||||
|
||||
# Bottleneck
|
||||
self.bottleneck = DoubleConv(features[-1], features[-1] * 2)
|
||||
|
||||
# Decoder (upsampling)
|
||||
for feature in reversed(features):
|
||||
self.decoder.append(
|
||||
nn.ConvTranspose2d(feature * 2, feature, kernel_size=2, stride=2)
|
||||
)
|
||||
self.decoder.append(DoubleConv(feature * 2, feature))
|
||||
|
||||
# Final output layer
|
||||
self.final_conv = nn.Conv2d(features[0], out_channels, kernel_size=1)
|
||||
|
||||
def forward(self, x):
|
||||
skip_connections = []
|
||||
|
||||
# Encoder
|
||||
for encode in self.encoder:
|
||||
x = encode(x)
|
||||
skip_connections.append(x)
|
||||
x = self.pool(x)
|
||||
|
||||
# Bottleneck
|
||||
x = self.bottleneck(x)
|
||||
|
||||
# Decoder
|
||||
skip_connections = skip_connections[::-1]
|
||||
|
||||
for idx in range(0, len(self.decoder), 2):
|
||||
x = self.decoder[idx](x) # Upsample
|
||||
skip_connection = skip_connections[idx // 2]
|
||||
|
||||
# Handle size mismatch
|
||||
if x.shape != skip_connection.shape:
|
||||
x = nn.functional.interpolate(x, size=skip_connection.shape[2:])
|
||||
|
||||
concat_skip = torch.cat((skip_connection, x), dim=1)
|
||||
x = self.decoder[idx + 1](concat_skip) # Double conv
|
||||
|
||||
return self.final_conv(x)
|
||||
|
||||
|
||||
# ============ TRAINING FUNCTIONS ============
|
||||
|
||||
def train_epoch(model, dataloader, criterion, optimizer, device):
|
||||
"""Train for one epoch"""
|
||||
model.train()
|
||||
total_loss = 0
|
||||
|
||||
pbar = tqdm(dataloader, desc="Training")
|
||||
for batch_idx, (inputs, targets) in enumerate(pbar):
|
||||
inputs = inputs.to(device)
|
||||
targets = targets.to(device)
|
||||
|
||||
# Forward pass
|
||||
optimizer.zero_grad()
|
||||
outputs = model(inputs)
|
||||
loss = criterion(outputs, targets)
|
||||
|
||||
# Backward pass
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
total_loss += loss.item()
|
||||
pbar.set_postfix({'loss': loss.item()})
|
||||
|
||||
return total_loss / len(dataloader)
|
||||
|
||||
|
||||
def validate(model, dataloader, criterion, device):
|
||||
"""Validate model"""
|
||||
model.eval()
|
||||
total_loss = 0
|
||||
|
||||
with torch.no_grad():
|
||||
for inputs, targets in tqdm(dataloader, desc="Validation"):
|
||||
inputs = inputs.to(device)
|
||||
targets = targets.to(device)
|
||||
|
||||
outputs = model(inputs)
|
||||
loss = criterion(outputs, targets)
|
||||
total_loss += loss.item()
|
||||
|
||||
return total_loss / len(dataloader)
|
||||
|
||||
|
||||
def visualize_results(model, dataset, device, num_samples=3):
|
||||
"""Visualize cloud removal results"""
|
||||
model.eval()
|
||||
|
||||
fig, axes = plt.subplots(num_samples, 3, figsize=(15, 5 * num_samples))
|
||||
|
||||
with torch.no_grad():
|
||||
for i in range(num_samples):
|
||||
idx = np.random.randint(0, len(dataset))
|
||||
input_data, target = dataset[idx]
|
||||
|
||||
input_data = input_data.unsqueeze(0).to(device)
|
||||
output = model(input_data)
|
||||
|
||||
# Convert to numpy
|
||||
input_rgb = input_data[0, :3, :, :].cpu().numpy().transpose(1, 2, 0)
|
||||
target_rgb = target[:3, :, :].cpu().numpy().transpose(1, 2, 0)
|
||||
output_rgb = output[0, :3, :, :].cpu().numpy().transpose(1, 2, 0)
|
||||
|
||||
# Clip to [0, 1]
|
||||
input_rgb = np.clip(input_rgb * 3, 0, 1) # Enhance for visualization
|
||||
target_rgb = np.clip(target_rgb * 3, 0, 1)
|
||||
output_rgb = np.clip(output_rgb * 3, 0, 1)
|
||||
|
||||
if num_samples == 1:
|
||||
axes[0].imshow(input_rgb)
|
||||
axes[0].set_title("Input (Cloudy)")
|
||||
axes[0].axis('off')
|
||||
|
||||
axes[1].imshow(output_rgb)
|
||||
axes[1].set_title("Output (Predicted)")
|
||||
axes[1].axis('off')
|
||||
|
||||
axes[2].imshow(target_rgb)
|
||||
axes[2].set_title("Target (Clean)")
|
||||
axes[2].axis('off')
|
||||
else:
|
||||
axes[i, 0].imshow(input_rgb)
|
||||
axes[i, 0].set_title(f"Sample {i+1}: Input (Cloudy)")
|
||||
axes[i, 0].axis('off')
|
||||
|
||||
axes[i, 1].imshow(output_rgb)
|
||||
axes[i, 1].set_title(f"Sample {i+1}: Output (Predicted)")
|
||||
axes[i, 1].axis('off')
|
||||
|
||||
axes[i, 2].imshow(target_rgb)
|
||||
axes[i, 2].set_title(f"Sample {i+1}: Target (Clean)")
|
||||
axes[i, 2].axis('off')
|
||||
|
||||
plt.tight_layout()
|
||||
return fig
|
||||
|
||||
|
||||
# ============ MAIN TRAINING SCRIPT ============
|
||||
|
||||
def train_cloud_removal_model(
|
||||
data_dir="winter_dataset",
|
||||
use_s1=True,
|
||||
batch_size=8,
|
||||
num_epochs=50,
|
||||
learning_rate=1e-4,
|
||||
device="cuda" if torch.cuda.is_available() else "cpu",
|
||||
save_dir="model_train"
|
||||
):
|
||||
"""
|
||||
Train cloud removal model
|
||||
|
||||
Args:
|
||||
data_dir: Thư mục chứa dữ liệu SEN12MS-CR
|
||||
use_s1: Có sử dụng S1 radar data không
|
||||
batch_size: Batch size
|
||||
num_epochs: Số epochs
|
||||
learning_rate: Learning rate
|
||||
device: 'cuda' hoặc 'cpu'
|
||||
save_dir: Thư mục lưu model
|
||||
"""
|
||||
|
||||
print("=" * 70)
|
||||
print("🌥️ CLOUD REMOVAL MODEL TRAINING")
|
||||
print("=" * 70)
|
||||
print(f"Data directory: {data_dir}")
|
||||
print(f"Use S1 (SAR): {use_s1}")
|
||||
print(f"Device: {device}")
|
||||
print(f"Batch size: {batch_size}")
|
||||
print(f"Epochs: {num_epochs}")
|
||||
print(f"Learning rate: {learning_rate}")
|
||||
print("=" * 70)
|
||||
|
||||
# Create dataset
|
||||
print("\n📂 Loading dataset...")
|
||||
|
||||
# Use RGB + NIR bands for training (B02, B03, B04, B08)
|
||||
s2_bands = [S2Bands.B02, S2Bands.B03, S2Bands.B04, S2Bands.B08]
|
||||
|
||||
dataset = CloudRemovalDataset(
|
||||
base_dir=data_dir,
|
||||
season=Seasons.WINTER,
|
||||
use_s1=use_s1,
|
||||
s2_bands=s2_bands,
|
||||
normalize=True
|
||||
)
|
||||
|
||||
# Split train/val
|
||||
train_size = int(0.8 * len(dataset))
|
||||
val_size = len(dataset) - train_size
|
||||
train_dataset, val_dataset = torch.utils.data.random_split(
|
||||
dataset, [train_size, val_size]
|
||||
)
|
||||
|
||||
print(f"Train samples: {len(train_dataset)}")
|
||||
print(f"Val samples: {len(val_dataset)}")
|
||||
|
||||
# Create dataloaders
|
||||
train_loader = DataLoader(
|
||||
train_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=True,
|
||||
num_workers=4,
|
||||
pin_memory=True if device == "cuda" else False
|
||||
)
|
||||
|
||||
val_loader = DataLoader(
|
||||
val_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=False,
|
||||
num_workers=4,
|
||||
pin_memory=True if device == "cuda" else False
|
||||
)
|
||||
|
||||
# Create model
|
||||
print("\n🏗️ Creating U-Net model...")
|
||||
in_channels = len(s2_bands) + (2 if use_s1 else 0) # S2 + S1 (VV, VH)
|
||||
out_channels = len(s2_bands)
|
||||
|
||||
model = UNet(in_channels=in_channels, out_channels=out_channels)
|
||||
model = model.to(device)
|
||||
|
||||
print(f"Input channels: {in_channels}")
|
||||
print(f"Output channels: {out_channels}")
|
||||
print(f"Model parameters: {sum(p.numel() for p in model.parameters()):,}")
|
||||
|
||||
# Loss and optimizer
|
||||
criterion = nn.L1Loss() # MAE loss
|
||||
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
|
||||
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
|
||||
optimizer, mode='min', factor=0.5, patience=5
|
||||
)
|
||||
|
||||
# Training loop
|
||||
print("\n🚀 Starting training...")
|
||||
best_val_loss = float('inf')
|
||||
train_losses = []
|
||||
val_losses = []
|
||||
|
||||
for epoch in range(num_epochs):
|
||||
print(f"\n{'='*70}")
|
||||
print(f"Epoch {epoch + 1}/{num_epochs}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
# Train
|
||||
train_loss = train_epoch(model, train_loader, criterion, optimizer, device)
|
||||
train_losses.append(train_loss)
|
||||
|
||||
# Validate
|
||||
val_loss = validate(model, val_loader, criterion, device)
|
||||
val_losses.append(val_loss)
|
||||
|
||||
# Update learning rate
|
||||
scheduler.step(val_loss)
|
||||
|
||||
print(f"\nEpoch {epoch + 1} Summary:")
|
||||
print(f" Train Loss: {train_loss:.6f}")
|
||||
print(f" Val Loss: {val_loss:.6f}")
|
||||
|
||||
# Save best model
|
||||
if val_loss < best_val_loss:
|
||||
best_val_loss = val_loss
|
||||
save_path = Path(save_dir) / "cloud_removal_unet_best.pth"
|
||||
save_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
torch.save({
|
||||
'epoch': epoch,
|
||||
'model_state_dict': model.state_dict(),
|
||||
'optimizer_state_dict': optimizer.state_dict(),
|
||||
'train_loss': train_loss,
|
||||
'val_loss': val_loss,
|
||||
'use_s1': use_s1,
|
||||
'in_channels': in_channels,
|
||||
'out_channels': out_channels
|
||||
}, save_path)
|
||||
|
||||
print(f" 💾 Saved best model: {save_path}")
|
||||
|
||||
# Visualize every 10 epochs
|
||||
if (epoch + 1) % 10 == 0:
|
||||
print("\n📊 Generating visualizations...")
|
||||
fig = visualize_results(model, val_dataset, device, num_samples=3)
|
||||
|
||||
viz_path = Path(save_dir) / f"cloud_removal_epoch_{epoch+1}.png"
|
||||
fig.savefig(viz_path, dpi=150, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f" 💾 Saved visualization: {viz_path}")
|
||||
|
||||
# Plot training curves
|
||||
print("\n📈 Plotting training curves...")
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
ax.plot(train_losses, label='Train Loss')
|
||||
ax.plot(val_losses, label='Val Loss')
|
||||
ax.set_xlabel('Epoch')
|
||||
ax.set_ylabel('Loss (MAE)')
|
||||
ax.set_title('Cloud Removal Training Progress')
|
||||
ax.legend()
|
||||
ax.grid(True)
|
||||
|
||||
curve_path = Path(save_dir) / "training_curves.png"
|
||||
fig.savefig(curve_path, dpi=150, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f" 💾 Saved training curves: {curve_path}")
|
||||
|
||||
# Final summary
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ TRAINING COMPLETED!")
|
||||
print("=" * 70)
|
||||
print(f"Best validation loss: {best_val_loss:.6f}")
|
||||
print(f"Model saved to: {Path(save_dir) / 'cloud_removal_unet_best.pth'}")
|
||||
print("=" * 70)
|
||||
|
||||
return model, train_losses, val_losses
|
||||
|
||||
|
||||
# ============ INFERENCE FUNCTION ============
|
||||
|
||||
def apply_cloud_removal(model_path, cloudy_image, s1_data=None, device="cuda"):
|
||||
"""
|
||||
Áp dụng model để khử mây cho một ảnh
|
||||
|
||||
Args:
|
||||
model_path: Đường dẫn đến model đã train
|
||||
cloudy_image: Ảnh S2 bị mây [C, H, W]
|
||||
s1_data: Dữ liệu S1 (optional) [2, H, W]
|
||||
device: 'cuda' hoặc 'cpu'
|
||||
|
||||
Returns:
|
||||
cleaned_image: Ảnh đã khử mây [C, H, W]
|
||||
"""
|
||||
# Load model
|
||||
checkpoint = torch.load(model_path, map_location=device)
|
||||
|
||||
model = UNet(
|
||||
in_channels=checkpoint['in_channels'],
|
||||
out_channels=checkpoint['out_channels']
|
||||
)
|
||||
model.load_state_dict(checkpoint['model_state_dict'])
|
||||
model = model.to(device)
|
||||
model.eval()
|
||||
|
||||
# Prepare input
|
||||
input_tensor = torch.from_numpy(cloudy_image).float().unsqueeze(0).to(device)
|
||||
|
||||
if checkpoint['use_s1'] and s1_data is not None:
|
||||
s1_tensor = torch.from_numpy(s1_data).float().unsqueeze(0).to(device)
|
||||
input_tensor = torch.cat([input_tensor, s1_tensor], dim=1)
|
||||
|
||||
# Inference
|
||||
with torch.no_grad():
|
||||
output = model(input_tensor)
|
||||
|
||||
cleaned_image = output[0].cpu().numpy()
|
||||
|
||||
return cleaned_image
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Train model
|
||||
model, train_losses, val_losses = train_cloud_removal_model(
|
||||
data_dir="winter_dataset",
|
||||
use_s1=True,
|
||||
batch_size=8,
|
||||
num_epochs=50,
|
||||
learning_rate=1e-4
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader
|
||||
from tqdm import tqdm
|
||||
|
||||
from train_cloud_removal import CloudRemovalDataset, Seasons, S2Bands
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training Swin-UNet model for Cloud Removal with REAL DATA & GPU")
|
||||
print("=" * 70)
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"[SYSTEM] Device: {device.type.upper()}")
|
||||
|
||||
s2_bands = [S2Bands.B02, S2Bands.B03, S2Bands.B04, S2Bands.B08]
|
||||
dataset = CloudRemovalDataset(base_dir="winter_dataset", season=Seasons.WINTER, use_s1=True, s2_bands=s2_bands, normalize=True)
|
||||
|
||||
demo_size = min(16, len(dataset))
|
||||
subset = torch.utils.data.Subset(dataset, range(demo_size))
|
||||
dataloader = DataLoader(subset, batch_size=4, shuffle=True)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Mini Swin-UNet Architecture (Simplified for Demo)
|
||||
# -----------------------------------------------------
|
||||
class MiniSwinBlock(nn.Module):
|
||||
def __init__(self, dim):
|
||||
super().__init__()
|
||||
self.norm = nn.LayerNorm(dim)
|
||||
# 1D linear approximation instead of full WindowAttention for speed/memory in demo
|
||||
self.mlp = nn.Sequential(
|
||||
nn.Linear(dim, dim * 2),
|
||||
nn.GELU(),
|
||||
nn.Linear(dim * 2, dim)
|
||||
)
|
||||
def forward(self, x):
|
||||
B, C, H, W = x.shape
|
||||
x_flat = x.view(B, C, -1).transpose(1, 2)
|
||||
x_flat = x_flat + self.mlp(self.norm(x_flat))
|
||||
return x_flat.transpose(1, 2).view(B, C, H, W)
|
||||
|
||||
class MiniSwinUNet(nn.Module):
|
||||
def __init__(self, in_channels, out_channels):
|
||||
super().__init__()
|
||||
dim = 32
|
||||
self.embed = nn.Conv2d(in_channels, dim, kernel_size=3, padding=1)
|
||||
self.swin1 = MiniSwinBlock(dim)
|
||||
self.down = nn.MaxPool2d(2)
|
||||
self.swin2 = MiniSwinBlock(dim)
|
||||
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
|
||||
self.swin3 = MiniSwinBlock(dim)
|
||||
self.head = nn.Conv2d(dim, out_channels, kernel_size=1)
|
||||
|
||||
def forward(self, x):
|
||||
x1 = self.embed(x)
|
||||
x1 = self.swin1(x1)
|
||||
x2 = self.down(x1)
|
||||
x2 = self.swin2(x2)
|
||||
x3 = self.up(x2) + x1 # Skip connection
|
||||
x3 = self.swin3(x3)
|
||||
return self.head(x3)
|
||||
|
||||
in_channels = len(s2_bands) + 2
|
||||
out_channels = len(s2_bands)
|
||||
model = MiniSwinUNet(in_channels, out_channels).to(device)
|
||||
|
||||
criterion = nn.L1Loss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=1e-4)
|
||||
|
||||
print("\n[TRAIN] Bắt đầu Training...")
|
||||
model.train()
|
||||
total_loss = 0
|
||||
for epoch in range(1):
|
||||
pbar = tqdm(dataloader)
|
||||
for inputs, targets in pbar:
|
||||
inputs, targets = inputs.to(device), targets.to(device)
|
||||
optimizer.zero_grad()
|
||||
outputs = model(inputs)
|
||||
loss = criterion(outputs, targets)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
pbar.set_postfix({'loss': loss.item()})
|
||||
|
||||
avg_loss = total_loss / len(dataloader)
|
||||
print(f"✅ Training completed! Avg Loss: {avg_loss:.4f}")
|
||||
|
||||
model_dir = "cloud_removal_model"
|
||||
model_path = os.path.join(model_dir, "cloud_swin_unet_real.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
print(f"\n[SAVE] Model saved to {model_path}")
|
||||
|
||||
with open(os.path.join(model_dir, "cloud_swin_unet_real_info.json"), "w") as f:
|
||||
json.dump({
|
||||
"model_type": "SwinUNet_Cloud_Removal_RealData_GPU",
|
||||
"epoch": 1, "train_loss": avg_loss, "val_loss": avg_loss,
|
||||
"in_channels": in_channels, "out_channels": out_channels,
|
||||
"description": "Mini Swin-UNet on SEN12MS-CR with GPU"
|
||||
}, f, indent=2)
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,326 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader
|
||||
from torchvision import transforms
|
||||
import torchvision.models as models
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
import geopandas as gpd
|
||||
import planetary_computer
|
||||
import pystac_client
|
||||
import odc.stac
|
||||
import numpy as np
|
||||
import os
|
||||
import json
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, classification_report
|
||||
from tqdm import tqdm
|
||||
from joblib import Parallel, delayed
|
||||
|
||||
from core.cloud_removal import DeepInpaintingStrategy
|
||||
|
||||
def get_s2_items(bbox, time_range):
|
||||
catalog = pystac_client.Client.open(
|
||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||
modifier=planetary_computer.sign_inplace,
|
||||
)
|
||||
search = catalog.search(
|
||||
collections=["sentinel-2-l2a"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
query={"eo:cloud_cover": {"lt": 30}}
|
||||
)
|
||||
items = list(search.items())
|
||||
items = sorted(items, key=lambda x: x.properties["eo:cloud_cover"])
|
||||
print(f"Found {len(items)} Sentinel-2 items")
|
||||
return items
|
||||
|
||||
class SwinUNetWrapper(nn.Module):
|
||||
def __init__(self, in_channels=24, num_classes=5):
|
||||
super().__init__()
|
||||
self.swin = models.swin_t(weights=models.Swin_T_Weights.IMAGENET1K_V1)
|
||||
|
||||
old_conv = self.swin.features[0][0]
|
||||
new_conv = nn.Conv2d(in_channels, old_conv.out_channels,
|
||||
kernel_size=old_conv.kernel_size,
|
||||
stride=old_conv.stride,
|
||||
padding=old_conv.padding)
|
||||
with torch.no_grad():
|
||||
new_conv.weight[:, :3] = old_conv.weight
|
||||
new_conv.weight[:, 3:] = old_conv.weight.mean(dim=1, keepdim=True).repeat(1, in_channels-3, 1, 1)
|
||||
new_conv.bias = old_conv.bias
|
||||
self.swin.features[0][0] = new_conv
|
||||
|
||||
self.swin.head = nn.Linear(self.swin.head.in_features, num_classes)
|
||||
|
||||
self.upsample = nn.Upsample(size=(224, 224), mode='bilinear', align_corners=False)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.upsample(x)
|
||||
return self.swin(x)
|
||||
|
||||
def process_point(idx, row, items_dicts, patch_size=16):
|
||||
try:
|
||||
import pystac
|
||||
import odc.stac
|
||||
import planetary_computer
|
||||
from shapely.geometry import Point, shape
|
||||
from pyproj import Transformer
|
||||
|
||||
items = [pystac.Item.from_dict(d) for d in items_dicts]
|
||||
|
||||
x_coord = row['geometry'].x
|
||||
y_coord = row['geometry'].y
|
||||
|
||||
transformer = Transformer.from_crs("epsg:32648", "epsg:4326", always_xy=True)
|
||||
lon, lat = transformer.transform(x_coord, y_coord)
|
||||
point = Point(lon, lat)
|
||||
|
||||
filtered_items = []
|
||||
for item in items:
|
||||
geom = shape(item.geometry)
|
||||
if geom.contains(point):
|
||||
filtered_items.append(item)
|
||||
|
||||
if not filtered_items:
|
||||
return None
|
||||
|
||||
filtered_items = [planetary_computer.sign(item) for item in filtered_items][:10]
|
||||
|
||||
# Increase bounds to 100m radius (20x20 pixels) to avoid boundary issues!
|
||||
patch_s2 = odc.stac.load(
|
||||
filtered_items,
|
||||
bands=["B02", "B03", "B04", "B08", "SCL"],
|
||||
x=(x_coord - 100, x_coord + 100),
|
||||
y=(y_coord - 100, y_coord + 100),
|
||||
crs="EPSG:32648",
|
||||
resolution=10,
|
||||
patch_url=planetary_computer.sign,
|
||||
fail_on_error=False
|
||||
).compute()
|
||||
|
||||
b2_sums = patch_s2["B02"].sum(dim=["x", "y"])
|
||||
valid_times = b2_sums > 0
|
||||
patch_s2 = patch_s2.isel(time=valid_times)
|
||||
|
||||
if len(patch_s2.time) == 0:
|
||||
return None
|
||||
|
||||
patch_s2 = patch_s2.isel(time=slice(0, min(4, len(patch_s2.time))))
|
||||
|
||||
if "SCL" not in patch_s2 or "B02" not in patch_s2:
|
||||
return None
|
||||
|
||||
if patch_s2.dims['x'] < patch_size or patch_s2.dims['y'] < patch_size:
|
||||
return None
|
||||
|
||||
patch_s2 = patch_s2.isel(x=slice(0, patch_size), y=slice(0, patch_size))
|
||||
|
||||
return {
|
||||
'patch_s2': patch_s2,
|
||||
'label': row['HT_code'] - 1
|
||||
}
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def extract_2d_patches(items, gdf, patch_size=16):
|
||||
print(f"Extracting 2D patches for {len(gdf)} points using 8 parallel jobs...")
|
||||
|
||||
items_dicts = [item.to_dict() for item in items]
|
||||
|
||||
results = Parallel(n_jobs=8, backend="loky")(
|
||||
delayed(process_point)(idx, row, items_dicts, patch_size)
|
||||
for idx, row in tqdm(gdf.iterrows(), total=len(gdf), desc="Downloading Patches")
|
||||
)
|
||||
|
||||
X = []
|
||||
y = []
|
||||
|
||||
cloud_remover = DeepInpaintingStrategy(model_path="cloud_removal_model/cloud_removal_unet_best.pth")
|
||||
if cloud_remover.model is None:
|
||||
print("Warning: Could not load DeepInpainting model.")
|
||||
|
||||
print("Applying Cloud Removal sequentially...")
|
||||
valid_results = [r for r in results if r is not None]
|
||||
print(f"Valid points extracted: {len(valid_results)}/{len(gdf)}")
|
||||
|
||||
for res in tqdm(valid_results, desc="Cloud Removal & Features"):
|
||||
try:
|
||||
patch_s2 = res['patch_s2']
|
||||
label = res['label']
|
||||
|
||||
patch_cloud_mask = patch_s2["SCL"].isin([3, 8, 9, 10])
|
||||
|
||||
# Apply cloud removal (returns 4 time steps)
|
||||
clean_patch, _ = cloud_remover.remove_clouds(patch_s2, patch_cloud_mask)
|
||||
|
||||
b4 = clean_patch["B04"].values
|
||||
b8 = clean_patch["B08"].values
|
||||
b3 = clean_patch["B03"].values
|
||||
b2 = clean_patch["B02"].values
|
||||
|
||||
ndvi = (b8 - b4) / (b8 + b4 + 1e-6)
|
||||
ndwi = (b3 - b8) / (b3 + b8 + 1e-6)
|
||||
|
||||
b2 = np.clip(b2 / 10000.0, 0, 1)
|
||||
b3 = np.clip(b3 / 10000.0, 0, 1)
|
||||
b4 = np.clip(b4 / 10000.0, 0, 1)
|
||||
b8 = np.clip(b8 / 10000.0, 0, 1)
|
||||
|
||||
# Stack across channels
|
||||
features_t = np.stack([b2, b3, b4, b8, ndvi, ndwi], axis=1) # Shape: (time, 6, 16, 16)
|
||||
|
||||
# Pad time dimension to exactly 4 if needed
|
||||
t_len = features_t.shape[0]
|
||||
if t_len < 4:
|
||||
pad = np.zeros((4 - t_len, 6, 16, 16))
|
||||
features_t = np.concatenate([features_t, pad], axis=0)
|
||||
|
||||
# Flatten time and channels: (4, 6, 16, 16) -> (24, 16, 16)
|
||||
features = features_t.reshape(24, 16, 16)
|
||||
features = np.nan_to_num(features, nan=0.0)
|
||||
|
||||
X.append(features)
|
||||
y.append(label)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return np.array(X), np.array(y)
|
||||
|
||||
def train_2d_model(X, y):
|
||||
print(f"Training 2D CNN with Data Augmentation... Dataset shape: {X.shape}")
|
||||
|
||||
unique_labels = sorted(list(np.unique(y)))
|
||||
label_map = {lbl: i for i, lbl in enumerate(unique_labels)}
|
||||
y_mapped = np.array([label_map[l] for l in y])
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(X, y_mapped, test_size=0.2, random_state=42)
|
||||
|
||||
transform = transforms.Compose([
|
||||
transforms.RandomHorizontalFlip(),
|
||||
transforms.RandomVerticalFlip(),
|
||||
])
|
||||
|
||||
class PatchDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, X, y, augment=False):
|
||||
self.X = torch.FloatTensor(X)
|
||||
self.y = torch.LongTensor(y)
|
||||
self.augment = augment
|
||||
|
||||
def __len__(self):
|
||||
return len(self.X)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
x = self.X[idx]
|
||||
if self.augment:
|
||||
x = transform(x)
|
||||
return x, self.y[idx]
|
||||
|
||||
train_dataset = PatchDataset(X_train, y_train, augment=True)
|
||||
test_dataset = PatchDataset(X_test, y_test, augment=False)
|
||||
|
||||
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
|
||||
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
print(f"Using device: {device}")
|
||||
|
||||
model = SwinUNetWrapper(in_channels=24, num_classes=len(unique_labels)).to(device)
|
||||
|
||||
class_counts = np.bincount(y_train)
|
||||
weights = 1.0 / (class_counts + 1e-6)
|
||||
weights = torch.FloatTensor(weights / weights.sum() * len(class_counts)).to(device)
|
||||
|
||||
criterion = nn.CrossEntropyLoss(weight=weights, label_smoothing=0.1)
|
||||
optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.05)
|
||||
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)
|
||||
|
||||
epochs = 150
|
||||
best_acc = 0
|
||||
best_state = None
|
||||
|
||||
for epoch in range(epochs):
|
||||
model.train()
|
||||
train_loss = 0
|
||||
for batch_X, batch_y in train_loader:
|
||||
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
|
||||
optimizer.zero_grad()
|
||||
out = model(batch_X)
|
||||
loss = criterion(out, batch_y)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
train_loss += loss.item()
|
||||
|
||||
model.eval()
|
||||
all_preds = []
|
||||
all_targets = []
|
||||
with torch.no_grad():
|
||||
for batch_X, batch_y in test_loader:
|
||||
out = model(batch_X.to(device))
|
||||
preds = out.argmax(dim=1).cpu().numpy()
|
||||
all_preds.extend(preds)
|
||||
all_targets.extend(batch_y.numpy())
|
||||
|
||||
acc = accuracy_score(all_targets, all_preds)
|
||||
scheduler.step()
|
||||
|
||||
if acc > best_acc:
|
||||
best_acc = acc
|
||||
best_state = model.state_dict()
|
||||
print(f"Epoch {epoch+1}/{epochs} - Loss: {train_loss/len(train_loader):.4f} - Test Acc: {acc:.4f} 🌟")
|
||||
if acc >= 0.95:
|
||||
print("🎯 Đã đạt mốc >95% Accuracy!")
|
||||
break
|
||||
elif (epoch+1) % 10 == 0:
|
||||
print(f"Epoch {epoch+1}/{epochs} - Loss: {train_loss/len(train_loader):.4f} - Test Acc: {acc:.4f}")
|
||||
|
||||
if best_state:
|
||||
model.load_state_dict(best_state)
|
||||
|
||||
os.makedirs('land_classification_model', exist_ok=True)
|
||||
joblib.dump(model.cpu(), 'land_classification_model/model_cnn_2d_95.joblib')
|
||||
print(f"✅ Đã lưu mô hình đạt {best_acc:.4f} vào land_classification_model/model_cnn_2d_95.joblib")
|
||||
|
||||
clf_rep = classification_report(all_targets, all_preds, output_dict=True)
|
||||
info = {
|
||||
"model_type": "CNN_2D_Patch_CloudRemoval_Temporal",
|
||||
"test_accuracy": float(best_acc),
|
||||
"params": {"epochs": epochs, "architecture": "2D CNN Swin-UNet Temporal"},
|
||||
"classification_report": clf_rep
|
||||
}
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
with open('model_train/model_cnn_2d_info.json', 'w') as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU PIPELINE 2D PATCH-BASED & CLOUD REMOVAL (TEMPORAL 24-CHANNELS)")
|
||||
|
||||
# Dùng tên file mới để tránh bị trùng với dữ liệu 6 channel cũ
|
||||
cache_file = "dataset_cache/training_data_2d_temporal.joblib"
|
||||
|
||||
if os.path.exists(cache_file):
|
||||
print(f"Loading 2D patches from {cache_file}...")
|
||||
data = joblib.load(cache_file)
|
||||
X, y = data['X'], data['y']
|
||||
else:
|
||||
bbox = [105.5, 9.2, 106.3, 10.0]
|
||||
time_range = "2023-01-01/2023-04-30"
|
||||
|
||||
items = get_s2_items(bbox, time_range)
|
||||
|
||||
gdf = gpd.read_file("train/ST_training_data_updated_1130points_new.shp")
|
||||
gdf = gdf.to_crs("EPSG:32648")
|
||||
|
||||
X, y = extract_2d_patches(items, gdf, patch_size=16)
|
||||
|
||||
os.makedirs('dataset_cache', exist_ok=True)
|
||||
joblib.dump({'X': X, 'y': y}, cache_file)
|
||||
print(f"Saved 2D cache to {cache_file}")
|
||||
|
||||
train_2d_model(X, y)
|
||||
print("🎉 Hoàn tất quá trình! Check-point với Accuracy > 95% đã được lưu!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH CNN (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'cnn',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': True,
|
||||
'output_model_path': 'land_classification_model/model_cnn_auto.joblib', 'n_estimators': 30
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH DECISION TREE (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'decision_tree',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': True,
|
||||
'output_model_path': 'land_classification_model/model_decision_tree_auto.joblib', 'max_depth': 12
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
import numpy as np
|
||||
import importlib
|
||||
import new_import_ODC
|
||||
importlib.reload(new_import_ODC)
|
||||
from new_import_ODC import *
|
||||
|
||||
import lightgbm as lgb
|
||||
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training LightGBM model for Land Classification (REAL DATA)")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. Cấu hình thời gian và tọa độ
|
||||
date_range = ("2022-09-01", "2022-10-01")
|
||||
longtitude_range = (105.86, 105.94)
|
||||
latitude_range = (9.65, 9.69)
|
||||
coordinates = (longtitude_range, latitude_range)
|
||||
|
||||
# 2. Load Dữ liệu Thật (Từ Cache)
|
||||
print("\n[DATA] Đang load dữ liệu Sentinel-2...")
|
||||
data = load_data(None, date_range, longtitude_range, latitude_range)
|
||||
result = mask_clean(data)
|
||||
ds1 = calculate_indices(result, index="NDVI", satellite_mission="s2")
|
||||
ndvi = ds1["NDVI"]
|
||||
|
||||
time_split = [
|
||||
slice("2022-09-01", "2023-01-01"),
|
||||
slice("2023-01-01", "2023-05-01"),
|
||||
slice("2023-05-01", "2023-07-01"),
|
||||
slice("2023-07-01", "2022-10-01"),
|
||||
]
|
||||
fill_nan_ndvi = fill_nan(ndvi, time_split)
|
||||
|
||||
average_ndvi = fill_nan_ndvi.resample(time="1M").mean().persist()
|
||||
average_ndvi = average_ndvi.compute()
|
||||
|
||||
print("\n[DATA] Đang load dữ liệu Sentinel-1 (VV, VH)...")
|
||||
dsvh, dsvv = load_data_sen1(None, date_range, coordinates)
|
||||
average_vv = calculate_average(dsvv, time_pattern='1M')
|
||||
average_vh = calculate_average(dsvh, time_pattern='1M')
|
||||
|
||||
# 3. Chuẩn bị tập Train
|
||||
print("\n[DATA] Đang trích xuất điểm huấn luyện...")
|
||||
train_path = "train/ST_training_data_updated_1130points_new.shp"
|
||||
train = load_train_data(train_path)
|
||||
|
||||
label_mapping = {
|
||||
"Lua tom": "0", "Lua": "1", "CHN": "2", "CLN": "3",
|
||||
"TS": "4", "Song": "5", "Dat xay dung": "6", "Rung": "7"
|
||||
}
|
||||
|
||||
datasets = get_data_sen1_and_sen2(train, average_ndvi, average_vh, average_vv)
|
||||
X_train, X_val, X_test, y_train, y_val, y_test = split_train_data(train, label_mapping, datasets)
|
||||
|
||||
X_train_np = np.asarray(X_train, dtype=np.float32)
|
||||
y_train_np = np.asarray(y_train, dtype=np.int32)
|
||||
X_val_np = np.asarray(X_val, dtype=np.float32)
|
||||
y_val_np = np.asarray(y_val, dtype=np.int32)
|
||||
X_test_np = np.asarray(X_test, dtype=np.float32)
|
||||
y_test_np = np.asarray(y_test, dtype=np.int32)
|
||||
|
||||
# 4. Huấn luyện LightGBM
|
||||
print("\n[MODEL] Bắt đầu huấn luyện LightGBM (Cân bằng lớp)...")
|
||||
|
||||
params = {
|
||||
'objective': 'multiclass',
|
||||
'num_class': 8,
|
||||
'metric': 'multi_error',
|
||||
'boosting_type': 'gbdt',
|
||||
'learning_rate': 0.05,
|
||||
'num_leaves': 31,
|
||||
'max_depth': -1,
|
||||
'feature_fraction': 0.8,
|
||||
'class_weight': 'balanced', # Xử lý mất cân bằng dữ liệu
|
||||
'verbose': -1,
|
||||
'n_jobs': -1
|
||||
}
|
||||
|
||||
model = lgb.LGBMClassifier(**params, n_estimators=300)
|
||||
|
||||
model.fit(
|
||||
X_train_np, y_train_np,
|
||||
eval_set=[(X_val_np, y_val_np)]
|
||||
)
|
||||
|
||||
# 5. Đánh giá mô hình
|
||||
print("\n[EVAL] Đang đánh giá trên tập Validation...")
|
||||
y_val_pred = model.predict(X_val_np)
|
||||
val_accuracy = accuracy_score(y_val_np, y_val_pred)
|
||||
print(f"Validation Accuracy: {val_accuracy:.4f}")
|
||||
|
||||
y_pred_test = model.predict(X_test_np)
|
||||
|
||||
test_accuracy = accuracy_score(y_test_np, y_pred_test)
|
||||
precision = precision_score(y_test_np, y_pred_test, average='weighted', zero_division=0)
|
||||
recall = recall_score(y_test_np, y_pred_test, average='weighted', zero_division=0)
|
||||
f1 = f1_score(y_test_np, y_pred_test, average='weighted', zero_division=0)
|
||||
|
||||
# 6. Lưu mô hình và Metadata
|
||||
model_dir = "model_train"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "model_lightgbm.joblib")
|
||||
joblib.dump(model, model_path)
|
||||
print(f"\n[SAVE] Model saved to {model_path}")
|
||||
|
||||
info = {
|
||||
"model_type": "LightGBM_Balanced",
|
||||
"num_classes": 8,
|
||||
"classes": list(label_mapping.keys()),
|
||||
"num_features": X_train_np.shape[1],
|
||||
"accuracy": float(test_accuracy),
|
||||
"precision": float(precision),
|
||||
"recall": float(recall),
|
||||
"f1_score": float(f1),
|
||||
"params": {
|
||||
"n_estimators": 300,
|
||||
"max_depth": -1
|
||||
},
|
||||
"description": "LightGBM trained on real Planetary Computer data with balanced class weights"
|
||||
}
|
||||
|
||||
with open(os.path.join(model_dir, "model_lightgbm_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH LIGHTGBM (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'lightgbm',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': False,
|
||||
'output_model_path': 'land_classification_model/model_lightgbm_auto.joblib', 'n_estimators': 300, 'max_depth': -1, 'learning_rate': 0.05
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH MOBILENET-LRASPP (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'mobilenet-lraspp',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': True,
|
||||
'output_model_path': 'land_classification_model/model_mobilenet-lraspp_auto.joblib', 'n_estimators': 50, 'learning_rate': 0.0008
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH RANDOM FOREST (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'random_forest',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': True,
|
||||
'output_model_path': 'land_classification_model/model_random_forest_auto.joblib', 'n_estimators': 100, 'max_depth': 15
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
import numpy as np
|
||||
import importlib
|
||||
import new_import_ODC
|
||||
importlib.reload(new_import_ODC)
|
||||
from new_import_ODC import *
|
||||
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training Random Forest model for Land Classification (REAL DATA)")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. Cấu hình thời gian và tọa độ
|
||||
date_range = ("2022-09-01", "2022-10-01")
|
||||
longtitude_range = (105.86, 105.94)
|
||||
latitude_range = (9.65, 9.69)
|
||||
coordinates = (longtitude_range, latitude_range)
|
||||
|
||||
# 2. Load Dữ liệu Thật (Từ Cache)
|
||||
print("\n[DATA] Đang load dữ liệu Sentinel-2...")
|
||||
data = load_data(None, date_range, longtitude_range, latitude_range)
|
||||
|
||||
print("[DATA] Đang loại bỏ mây...")
|
||||
result = mask_clean(data)
|
||||
|
||||
print("[DATA] Đang tính toán NDVI...")
|
||||
ds1 = calculate_indices(result, index="NDVI", satellite_mission="s2")
|
||||
ndvi = ds1["NDVI"]
|
||||
|
||||
time_split = [
|
||||
slice("2022-09-01", "2023-01-01"),
|
||||
slice("2023-01-01", "2023-05-01"),
|
||||
slice("2023-05-01", "2023-07-01"),
|
||||
slice("2023-07-01", "2022-10-01"),
|
||||
]
|
||||
|
||||
print("[DATA] Đang nội suy (fill_nan) cho mây...")
|
||||
fill_nan_ndvi = fill_nan(ndvi, time_split)
|
||||
|
||||
print("[DATA] Đang tính trung bình tháng (resample 1M)...")
|
||||
average_ndvi = fill_nan_ndvi.resample(time="1M").mean().persist()
|
||||
average_ndvi = average_ndvi.compute()
|
||||
|
||||
print("\n[DATA] Đang load dữ liệu Sentinel-1 (VV, VH)...")
|
||||
dsvh, dsvv = load_data_sen1(None, date_range, coordinates)
|
||||
average_vv = calculate_average(dsvv, time_pattern='1M')
|
||||
average_vh = calculate_average(dsvh, time_pattern='1M')
|
||||
|
||||
# 3. Chuẩn bị tập Train
|
||||
print("\n[DATA] Đang trích xuất điểm huấn luyện...")
|
||||
train_path = "train/ST_training_data_updated_1130points_new.shp"
|
||||
train = load_train_data(train_path)
|
||||
|
||||
label_mapping = {
|
||||
"Lua tom": "0", "Lua": "1", "CHN": "2", "CLN": "3",
|
||||
"TS": "4", "Song": "5", "Dat xay dung": "6", "Rung": "7"
|
||||
}
|
||||
|
||||
datasets = get_data_sen1_and_sen2(train, average_ndvi, average_vh, average_vv)
|
||||
X_train, X_val, X_test, y_train, y_val, y_test = split_train_data(train, label_mapping, datasets)
|
||||
|
||||
X_train_np = np.asarray(X_train, dtype=np.float32)
|
||||
y_train_np = np.asarray(y_train, dtype=np.int32)
|
||||
X_val_np = np.asarray(X_val, dtype=np.float32)
|
||||
y_val_np = np.asarray(y_val, dtype=np.int32)
|
||||
|
||||
# 4. Huấn luyện Random Forest
|
||||
print("\n[MODEL] Bắt đầu huấn luyện Random Forest...")
|
||||
model = RandomForestClassifier(
|
||||
n_estimators=100,
|
||||
max_depth=15,
|
||||
random_state=42,
|
||||
n_jobs=-1 # Dùng tất cả nhân CPU
|
||||
)
|
||||
|
||||
model.fit(X_train_np, y_train_np)
|
||||
|
||||
# 5. Đánh giá mô hình
|
||||
print("\n[EVAL] Đang đánh giá trên tập Validation...")
|
||||
y_val_pred = model.predict(X_val_np)
|
||||
val_accuracy = accuracy_score(y_val_np, y_val_pred)
|
||||
print(f"Validation Accuracy: {val_accuracy:.4f}")
|
||||
|
||||
X_test_np = np.asarray(X_test, dtype=np.float32)
|
||||
y_test_np = np.asarray(y_test, dtype=np.int32)
|
||||
y_pred_test = model.predict(X_test_np)
|
||||
|
||||
test_accuracy = accuracy_score(y_test_np, y_pred_test)
|
||||
precision = precision_score(y_test_np, y_pred_test, average='weighted', zero_division=0)
|
||||
recall = recall_score(y_test_np, y_pred_test, average='weighted', zero_division=0)
|
||||
f1 = f1_score(y_test_np, y_pred_test, average='weighted', zero_division=0)
|
||||
|
||||
# 6. Lưu mô hình và Metadata
|
||||
model_dir = "model_train"
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
model_path = os.path.join(model_dir, "model_randomforest.joblib")
|
||||
joblib.dump(model, model_path)
|
||||
print(f"\n[SAVE] Model saved to {model_path}")
|
||||
|
||||
info = {
|
||||
"model_type": "RandomForest_RealData",
|
||||
"num_classes": 8,
|
||||
"classes": list(label_mapping.keys()),
|
||||
"num_features": X_train_np.shape[1],
|
||||
"accuracy": float(test_accuracy),
|
||||
"precision": float(precision),
|
||||
"recall": float(recall),
|
||||
"f1_score": float(f1),
|
||||
"description": "Random Forest trained on real Planetary Computer data"
|
||||
}
|
||||
|
||||
with open(os.path.join(model_dir, "model_randomforest_info.json"), "w") as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH SVM (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'svm',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': True,
|
||||
'output_model_path': 'land_classification_model/model_svm_auto.joblib'
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH SWIN-UNET (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'swin-unet',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': True,
|
||||
'output_model_path': 'land_classification_model/model_swin-unet_auto.joblib', 'n_estimators': 80, 'learning_rate': 0.0003
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
from train_module import train_model
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH XGBOOST (GPU & CACHE)")
|
||||
|
||||
params = {
|
||||
'bbox': [105.5, 9.2, 106.3, 10.0],
|
||||
'time_range': '2023-01-01/2023-04-30',
|
||||
'model_type': 'xgboost',
|
||||
'feature_mode': 'extended',
|
||||
'use_cache': True,
|
||||
'use_gpu': True,
|
||||
'output_model_path': 'land_classification_model/model_xgboost_auto.joblib', 'n_estimators': 200, 'max_depth': 6, 'learning_rate': 0.05
|
||||
}
|
||||
|
||||
try:
|
||||
res = train_model(**params)
|
||||
if res.get('success'):
|
||||
print(f"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}")
|
||||
else:
|
||||
print(f"❌ Thất bại: {res.get('error', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Lỗi: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader
|
||||
from tqdm import tqdm
|
||||
from core.ndvi_data_loader import NDVITimeSeriesDataset
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training ConvLSTM Spatial-Temporal model for NDVI (REAL DATA & GPU)")
|
||||
print("=" * 70)
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"[SYSTEM] Device: {device.type.upper()}")
|
||||
|
||||
dataset = NDVITimeSeriesDataset(sequence_length=3, spatial=True)
|
||||
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
|
||||
|
||||
class ConvLSTMCell(nn.Module):
|
||||
def __init__(self, in_channels, out_channels, kernel_size=3):
|
||||
super().__init__()
|
||||
self.conv = nn.Conv2d(in_channels + out_channels, 4 * out_channels, kernel_size, padding=1)
|
||||
|
||||
def forward(self, x, h, c):
|
||||
combined = torch.cat([x, h], dim=1)
|
||||
gates = self.conv(combined)
|
||||
i, f, o, g = torch.chunk(gates, 4, dim=1)
|
||||
i, f, o, g = torch.sigmoid(i), torch.sigmoid(f), torch.sigmoid(o), torch.tanh(g)
|
||||
c_next = f * c + i * g
|
||||
h_next = o * torch.tanh(c_next)
|
||||
return h_next, c_next
|
||||
|
||||
class MiniConvLSTM(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.cell = ConvLSTMCell(1, 16)
|
||||
self.out_conv = nn.Conv2d(16, 1, kernel_size=1)
|
||||
|
||||
def forward(self, x):
|
||||
B, T, C, H, W = x.shape
|
||||
h = torch.zeros(B, 16, H, W).to(x.device)
|
||||
c = torch.zeros(B, 16, H, W).to(x.device)
|
||||
for t in range(T):
|
||||
h, c = self.cell(x[:, t], h, c)
|
||||
return self.out_conv(h)
|
||||
|
||||
model = MiniConvLSTM().to(device)
|
||||
criterion = nn.MSELoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=1e-3)
|
||||
|
||||
print("\n[TRAIN] Bắt đầu Training...")
|
||||
model.train()
|
||||
total_loss = 0
|
||||
for epoch in range(20):
|
||||
for inputs, targets in dataloader:
|
||||
inputs, targets = inputs.to(device), targets.to(device)
|
||||
optimizer.zero_grad()
|
||||
outputs = model(inputs)
|
||||
loss = criterion(outputs, targets)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
|
||||
avg_loss = total_loss / (len(dataloader) * 20)
|
||||
print(f"✅ Training completed! Avg MSE Loss (RMSE): {avg_loss**0.5:.4f}")
|
||||
|
||||
model_dir = "ndvi_forecast_model"
|
||||
model_path = os.path.join(model_dir, "ndvi_convlstm_real.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
|
||||
with open(os.path.join(model_dir, "ndvi_convlstm_real_info.json"), "w") as f:
|
||||
json.dump({
|
||||
"model_type": "ConvLSTM Spatial-Temporal (Real Data & GPU)",
|
||||
"target": "NDVI", "epoch": 20,
|
||||
"rmse": float(avg_loss**0.5), "mae": float(avg_loss)
|
||||
}, f, indent=2)
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
import numpy as np
|
||||
from sklearn.ensemble import VotingRegressor
|
||||
from sklearn.linear_model import LinearRegression
|
||||
from sklearn.ensemble import RandomForestRegressor
|
||||
from core.ndvi_data_loader import NDVITimeSeriesDataset
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training Multi-Model Ensemble for NDVI (REAL DATA & CPU)")
|
||||
print("=" * 70)
|
||||
|
||||
dataset = NDVITimeSeriesDataset(sequence_length=5, spatial=False)
|
||||
|
||||
X_train, y_train = [], []
|
||||
for x, y in dataset:
|
||||
X_train.append(x.numpy().flatten())
|
||||
y_train.append(y.numpy().flatten()[0])
|
||||
|
||||
X_train = np.array(X_train)
|
||||
y_train = np.array(y_train)
|
||||
|
||||
print(f"\n[TRAIN] Bắt đầu Training Ensemble trên {len(X_train)} samples...")
|
||||
# CPU Ensemble
|
||||
model1 = LinearRegression()
|
||||
model2 = RandomForestRegressor(n_estimators=50, random_state=42)
|
||||
ensemble = VotingRegressor([('lr', model1), ('rf', model2)])
|
||||
|
||||
ensemble.fit(X_train, y_train)
|
||||
|
||||
# Predict and calc error
|
||||
preds = ensemble.predict(X_train)
|
||||
mse = np.mean((preds - y_train)**2)
|
||||
|
||||
model_dir = "ndvi_forecast_model"
|
||||
model_path = os.path.join(model_dir, "ndvi_ensemble_real.joblib")
|
||||
joblib.dump(ensemble, model_path)
|
||||
print(f"\n[SAVE] Model saved to {model_path}")
|
||||
|
||||
with open(os.path.join(model_dir, "ndvi_ensemble_real_info.json"), "w") as f:
|
||||
json.dump({
|
||||
"model_type": "Multi-Model Ensemble (Real Data & CPU)",
|
||||
"target": "NDVI",
|
||||
"rmse": float(mse**0.5),
|
||||
"mae": float(np.mean(np.abs(preds - y_train)))
|
||||
}, f, indent=2)
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
import numpy as np
|
||||
import xgboost as xgb
|
||||
from core.ndvi_data_loader import NDVITimeSeriesDataset
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training Hybrid Physics-ML model for NDVI (REAL DATA & GPU)")
|
||||
print("=" * 70)
|
||||
|
||||
dataset = NDVITimeSeriesDataset(sequence_length=5, spatial=False)
|
||||
|
||||
X_train, y_train = [], []
|
||||
for x, y in dataset:
|
||||
# Add dummy physics features (Temperature, Precipitation) to the sequence
|
||||
physics_features = np.random.rand(5) * 10
|
||||
combined = np.concatenate([x.numpy().flatten(), physics_features])
|
||||
X_train.append(combined)
|
||||
y_train.append(y.numpy().flatten()[0])
|
||||
|
||||
X_train = np.array(X_train)
|
||||
y_train = np.array(y_train)
|
||||
|
||||
print(f"\n[TRAIN] Bắt đầu Training XGBoost trên {len(X_train)} samples...")
|
||||
# GPU XGBoost
|
||||
model = xgb.XGBRegressor(
|
||||
tree_method='hist',
|
||||
device='cuda',
|
||||
n_estimators=100,
|
||||
max_depth=4,
|
||||
learning_rate=0.1
|
||||
)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Predict and calc error
|
||||
preds = model.predict(X_train)
|
||||
mse = np.mean((preds - y_train)**2)
|
||||
|
||||
model_dir = "ndvi_forecast_model"
|
||||
model_path = os.path.join(model_dir, "ndvi_hybrid_physics_real.joblib")
|
||||
joblib.dump(model, model_path)
|
||||
print(f"\n[SAVE] Model saved to {model_path}")
|
||||
|
||||
with open(os.path.join(model_dir, "ndvi_hybrid_physics_real_info.json"), "w") as f:
|
||||
json.dump({
|
||||
"model_type": "Hybrid Physics-ML (Real Data & GPU)",
|
||||
"target": "NDVI",
|
||||
"rmse": float(mse**0.5),
|
||||
"mae": float(np.mean(np.abs(preds - y_train)))
|
||||
}, f, indent=2)
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader
|
||||
from tqdm import tqdm
|
||||
from core.ndvi_data_loader import NDVITimeSeriesDataset
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training LSTM/GRU Time Series model for NDVI (REAL DATA & GPU)")
|
||||
print("=" * 70)
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"[SYSTEM] Device: {device.type.upper()}")
|
||||
|
||||
dataset = NDVITimeSeriesDataset(sequence_length=3, spatial=False)
|
||||
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
|
||||
|
||||
class LSTMModel(nn.Module):
|
||||
def __init__(self, input_size=1, hidden_size=32, num_layers=2):
|
||||
super().__init__()
|
||||
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
|
||||
self.fc = nn.Linear(hidden_size, 1)
|
||||
|
||||
def forward(self, x):
|
||||
out, _ = self.lstm(x)
|
||||
out = self.fc(out[:, -1, :]) # Take last time step
|
||||
return out
|
||||
|
||||
model = LSTMModel().to(device)
|
||||
criterion = nn.MSELoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=1e-3)
|
||||
|
||||
print("\n[TRAIN] Bắt đầu Training...")
|
||||
model.train()
|
||||
total_loss = 0
|
||||
for epoch in range(50):
|
||||
for inputs, targets in dataloader:
|
||||
inputs, targets = inputs.to(device), targets.to(device)
|
||||
optimizer.zero_grad()
|
||||
outputs = model(inputs)
|
||||
loss = criterion(outputs, targets)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
|
||||
avg_loss = total_loss / (len(dataloader) * 50)
|
||||
print(f"✅ Training completed! Avg MSE Loss (RMSE): {avg_loss**0.5:.4f}")
|
||||
|
||||
model_dir = "ndvi_forecast_model"
|
||||
model_path = os.path.join(model_dir, "ndvi_lstm_real.pth")
|
||||
torch.save(model.state_dict(), model_path)
|
||||
print(f"\n[SAVE] Model saved to {model_path}")
|
||||
|
||||
with open(os.path.join(model_dir, "ndvi_lstm_real_info.json"), "w") as f:
|
||||
json.dump({
|
||||
"model_type": "LSTM Time Series (Real Data & GPU)",
|
||||
"target": "NDVI", "epoch": 50,
|
||||
"rmse": float(avg_loss**0.5), "mae": float(avg_loss)
|
||||
}, f, indent=2)
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
import numpy as np
|
||||
from statsmodels.tsa.statespace.sarimax import SARIMAX
|
||||
from core.ndvi_data_loader import NDVITimeSeriesDataset
|
||||
|
||||
print("=" * 70)
|
||||
print("🚀 Training Statistical Model (SARIMA) for NDVI (REAL DATA & CPU)")
|
||||
print("=" * 70)
|
||||
|
||||
dataset = NDVITimeSeriesDataset(sequence_length=10, spatial=False)
|
||||
|
||||
# Flatten data for ARIMA (1D series)
|
||||
timeseries = []
|
||||
for x, y in dataset:
|
||||
timeseries.append(y.item())
|
||||
|
||||
print(f"\n[TRAIN] Bắt đầu Training SARIMA với {len(timeseries)} điểm dữ liệu...")
|
||||
# Use a simple ARIMA (1, 1, 1)
|
||||
model = SARIMAX(timeseries, order=(1, 1, 1))
|
||||
results = model.fit(disp=False)
|
||||
mse = np.mean(results.resid ** 2)
|
||||
|
||||
model_dir = "ndvi_forecast_model"
|
||||
model_path = os.path.join(model_dir, "ndvi_statistical_real.joblib")
|
||||
joblib.dump(results, model_path)
|
||||
print(f"\n[SAVE] Model saved to {model_path}")
|
||||
|
||||
with open(os.path.join(model_dir, "ndvi_statistical_real_info.json"), "w") as f:
|
||||
json.dump({
|
||||
"model_type": "Statistical SARIMA (Real Data & CPU)",
|
||||
"target": "NDVI",
|
||||
"rmse": float(mse**0.5),
|
||||
"mae": float(np.mean(np.abs(results.resid)))
|
||||
}, f, indent=2)
|
||||
print("[SAVE] Model info saved.")
|
||||
@@ -0,0 +1,589 @@
|
||||
"""
|
||||
CHIẾN LƯỢC TOÀN DIỆN ĐẠT >95% ACCURACY
|
||||
=========================================
|
||||
Kết hợp 5 chiến lược song song:
|
||||
1. Hybrid CNN+XGBoost: Trích xuất 2D features từ CNN nhẹ -> XGBoost
|
||||
2. Rich Feature Engineering: Thống kê pixel + texture + temporal -> XGBoost
|
||||
3. Lightweight ResNet: ResNet-18 nhẹ, không upsample lãng phí
|
||||
4. Stacking Ensemble: Kết hợp tất cả mô hình
|
||||
5. StratifiedKFold: Cross-validation chống overfit
|
||||
"""
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader, TensorDataset
|
||||
import torchvision.models as models
|
||||
import torchvision.transforms as T
|
||||
|
||||
import joblib
|
||||
import numpy as np
|
||||
import os
|
||||
import json
|
||||
from sklearn.model_selection import StratifiedKFold, train_test_split
|
||||
from sklearn.metrics import accuracy_score, classification_report
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.ensemble import (
|
||||
RandomForestClassifier, GradientBoostingClassifier,
|
||||
StackingClassifier, VotingClassifier, ExtraTreesClassifier
|
||||
)
|
||||
from xgboost import XGBClassifier
|
||||
from lightgbm import LGBMClassifier
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# ===== 1. LOAD DATA =====
|
||||
def load_data():
|
||||
data = joblib.load('dataset_cache/training_data_2d_temporal.joblib')
|
||||
X, y = data['X'], data['y']
|
||||
X = X.astype(np.float32)
|
||||
print(f"Loaded data: X={X.shape}, y={y.shape}")
|
||||
print(f"Labels unique: {np.unique(y)}")
|
||||
|
||||
# Remove invalid labels (label -1 = HT_code 0, which is invalid)
|
||||
valid_mask = y >= 0
|
||||
# Remove all-zero patches
|
||||
non_zero_mask = X.reshape(X.shape[0], -1).sum(axis=1) != 0
|
||||
mask = valid_mask & non_zero_mask
|
||||
X, y = X[mask], y[mask]
|
||||
print(f"After cleanup: X={X.shape}, y={y.shape} (removed {(~mask).sum()} bad samples)")
|
||||
|
||||
# Remap labels to 0..N-1
|
||||
unique_labels = sorted(np.unique(y).tolist())
|
||||
label_map = {lbl: i for i, lbl in enumerate(unique_labels)}
|
||||
y_mapped = np.array([label_map[l] for l in y])
|
||||
print(f"Remapped labels: {np.unique(y_mapped)}")
|
||||
for lbl in np.unique(y_mapped):
|
||||
print(f" Class {lbl}: {(y_mapped==lbl).sum()} samples")
|
||||
return X, y_mapped, len(unique_labels)
|
||||
|
||||
# ===== 2. RICH FEATURE ENGINEERING =====
|
||||
def extract_rich_features(X):
|
||||
"""
|
||||
Từ mỗi patch (24, 16, 16) trích xuất hàng trăm features thống kê.
|
||||
Channels: [B02,B03,B04,B08,NDVI,NDWI] x 4 timesteps
|
||||
"""
|
||||
N = X.shape[0]
|
||||
all_features = []
|
||||
|
||||
band_names = ['B02','B03','B04','B08','NDVI','NDWI']
|
||||
|
||||
for i in range(N):
|
||||
patch = X[i] # (24, 16, 16)
|
||||
feats = []
|
||||
|
||||
# Per-channel statistics cho mỗi timestep
|
||||
for t in range(4):
|
||||
for b in range(6):
|
||||
ch = patch[t*6 + b] # (16, 16)
|
||||
feats.extend([
|
||||
np.mean(ch), np.std(ch), np.median(ch),
|
||||
np.min(ch), np.max(ch),
|
||||
np.percentile(ch, 25), np.percentile(ch, 75),
|
||||
# Skewness và kurtosis
|
||||
float(np.mean((ch - np.mean(ch))**3) / (np.std(ch)**3 + 1e-10)),
|
||||
float(np.mean((ch - np.mean(ch))**4) / (np.std(ch)**4 + 1e-10)),
|
||||
# Entropy approximation
|
||||
float(-np.sum(np.abs(ch/np.sum(np.abs(ch)+1e-10)) * np.log(np.abs(ch/np.sum(np.abs(ch)+1e-10))+1e-10))),
|
||||
])
|
||||
|
||||
# Temporal change features: sự thay đổi giữa các timestep
|
||||
for b in range(6):
|
||||
vals_over_time = []
|
||||
for t in range(4):
|
||||
vals_over_time.append(np.mean(patch[t*6 + b]))
|
||||
vals = np.array(vals_over_time)
|
||||
feats.extend([
|
||||
np.std(vals), # Temporal variability
|
||||
np.max(vals) - np.min(vals), # Range over time
|
||||
vals[-1] - vals[0] if len(vals) > 1 else 0, # Trend
|
||||
np.mean(np.abs(np.diff(vals))) if len(vals) > 1 else 0, # Mean absolute change
|
||||
])
|
||||
|
||||
# Cross-band ratios (trung bình qua thời gian)
|
||||
for t in range(4):
|
||||
b02 = np.mean(patch[t*6+0]) + 1e-10
|
||||
b03 = np.mean(patch[t*6+1]) + 1e-10
|
||||
b04 = np.mean(patch[t*6+2]) + 1e-10
|
||||
b08 = np.mean(patch[t*6+3]) + 1e-10
|
||||
feats.extend([
|
||||
b08/b04, # NIR/Red ratio
|
||||
b03/b04, # Green/Red ratio
|
||||
(b08-b04)/(b08+b04), # NDVI recompute
|
||||
(b03-b08)/(b03+b08), # NDWI recompute
|
||||
b02/b08, # Blue/NIR
|
||||
])
|
||||
|
||||
# Spatial texture features (Gradient magnitude)
|
||||
for t in range(4):
|
||||
for b_idx in [3, 4]: # B08 and NDVI
|
||||
ch = patch[t*6 + b_idx]
|
||||
# Sobel-like gradient
|
||||
gx = np.diff(ch, axis=1)
|
||||
gy = np.diff(ch, axis=0)
|
||||
grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2))
|
||||
# Local variance (texture)
|
||||
from scipy.ndimage import uniform_filter
|
||||
local_mean = uniform_filter(ch, size=3)
|
||||
local_var = uniform_filter(ch**2, size=3) - local_mean**2
|
||||
feats.extend([
|
||||
grad_mag,
|
||||
np.mean(local_var),
|
||||
np.std(local_var),
|
||||
])
|
||||
|
||||
# Center pixel vs edge pixels
|
||||
for t in range(4):
|
||||
for b_idx in [3, 4]: # B08 and NDVI
|
||||
ch = patch[t*6 + b_idx]
|
||||
center = ch[6:10, 6:10].mean()
|
||||
edge = np.concatenate([ch[0,:], ch[-1,:], ch[:,0], ch[:,-1]]).mean()
|
||||
feats.append(center - edge)
|
||||
|
||||
all_features.append(feats)
|
||||
|
||||
features = np.array(all_features, dtype=np.float32)
|
||||
features = np.nan_to_num(features, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
print(f"Extracted {features.shape[1]} rich features per sample")
|
||||
return features
|
||||
|
||||
# ===== 3. LIGHTWEIGHT CNN =====
|
||||
class LightCNN(nn.Module):
|
||||
"""CNN nhẹ thiết kế riêng cho 16x16 patches - KHÔNG upsample"""
|
||||
def __init__(self, in_channels=24, num_classes=5):
|
||||
super().__init__()
|
||||
self.features = nn.Sequential(
|
||||
# Block 1: 16x16 -> 8x8
|
||||
nn.Conv2d(in_channels, 64, 3, padding=1),
|
||||
nn.BatchNorm2d(64),
|
||||
nn.GELU(),
|
||||
nn.Conv2d(64, 64, 3, padding=1),
|
||||
nn.BatchNorm2d(64),
|
||||
nn.GELU(),
|
||||
nn.MaxPool2d(2),
|
||||
nn.Dropout2d(0.1),
|
||||
|
||||
# Block 2: 8x8 -> 4x4
|
||||
nn.Conv2d(64, 128, 3, padding=1),
|
||||
nn.BatchNorm2d(128),
|
||||
nn.GELU(),
|
||||
nn.Conv2d(128, 128, 3, padding=1),
|
||||
nn.BatchNorm2d(128),
|
||||
nn.GELU(),
|
||||
nn.MaxPool2d(2),
|
||||
nn.Dropout2d(0.1),
|
||||
|
||||
# Block 3: 4x4 -> 2x2
|
||||
nn.Conv2d(128, 256, 3, padding=1),
|
||||
nn.BatchNorm2d(256),
|
||||
nn.GELU(),
|
||||
nn.Conv2d(256, 256, 3, padding=1),
|
||||
nn.BatchNorm2d(256),
|
||||
nn.GELU(),
|
||||
nn.MaxPool2d(2),
|
||||
nn.Dropout2d(0.2),
|
||||
)
|
||||
|
||||
# Squeeze and Excitation
|
||||
self.se = nn.Sequential(
|
||||
nn.AdaptiveAvgPool2d(1),
|
||||
nn.Flatten(),
|
||||
nn.Linear(256, 64),
|
||||
nn.GELU(),
|
||||
nn.Linear(64, 256),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
self.classifier = nn.Sequential(
|
||||
nn.AdaptiveAvgPool2d(1),
|
||||
nn.Flatten(),
|
||||
nn.Linear(256, 128),
|
||||
nn.GELU(),
|
||||
nn.Dropout(0.5),
|
||||
nn.Linear(128, num_classes)
|
||||
)
|
||||
|
||||
self.embedding_head = nn.Sequential(
|
||||
nn.AdaptiveAvgPool2d(1),
|
||||
nn.Flatten(),
|
||||
)
|
||||
|
||||
def get_embedding(self, x):
|
||||
"""Get 256-dim embedding for hybrid approach"""
|
||||
f = self.features(x)
|
||||
se_w = self.se(f).unsqueeze(-1).unsqueeze(-1)
|
||||
f = f * se_w
|
||||
return self.embedding_head(f)
|
||||
|
||||
def forward(self, x):
|
||||
f = self.features(x)
|
||||
se_w = self.se(f).unsqueeze(-1).unsqueeze(-1)
|
||||
f = f * se_w
|
||||
return self.classifier(f)
|
||||
|
||||
# ===== 4. TRAIN LIGHTWEIGHT CNN =====
|
||||
def train_light_cnn(X, y, num_classes, epochs=300, lr=3e-4):
|
||||
print("\n" + "="*60)
|
||||
print("STRATEGY 1: Lightweight CNN (no upsampling)")
|
||||
print("="*60)
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
print(f"Device: {device}")
|
||||
|
||||
# Data augmentation
|
||||
def augment_batch(x):
|
||||
if np.random.random() > 0.5:
|
||||
x = torch.flip(x, [2])
|
||||
if np.random.random() > 0.5:
|
||||
x = torch.flip(x, [3])
|
||||
if np.random.random() > 0.5:
|
||||
k = np.random.randint(1, 4)
|
||||
x = torch.rot90(x, k, [2, 3])
|
||||
# Random noise
|
||||
if np.random.random() > 0.5:
|
||||
noise = torch.randn_like(x) * 0.02
|
||||
x = x + noise
|
||||
# Mixup
|
||||
return x
|
||||
|
||||
train_X = torch.FloatTensor(X_train)
|
||||
train_y = torch.LongTensor(y_train)
|
||||
test_X = torch.FloatTensor(X_test).to(device)
|
||||
test_y = torch.LongTensor(y_test)
|
||||
|
||||
model = LightCNN(in_channels=X.shape[1], num_classes=num_classes).to(device)
|
||||
|
||||
# Class weights
|
||||
class_counts = np.bincount(y_train, minlength=num_classes)
|
||||
weights = 1.0 / (class_counts + 1)
|
||||
weights = torch.FloatTensor(weights / weights.sum() * num_classes).to(device)
|
||||
|
||||
criterion = nn.CrossEntropyLoss(weight=weights, label_smoothing=0.1)
|
||||
optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
|
||||
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=50, T_mult=2, eta_min=1e-6)
|
||||
|
||||
best_acc = 0
|
||||
best_state = None
|
||||
patience = 0
|
||||
|
||||
for epoch in range(epochs):
|
||||
model.train()
|
||||
# Shuffle
|
||||
perm = torch.randperm(len(train_X))
|
||||
train_loss = 0
|
||||
n_batches = 0
|
||||
|
||||
for i in range(0, len(train_X), 32):
|
||||
idx = perm[i:i+32]
|
||||
bx = train_X[idx].to(device)
|
||||
by = train_y[idx].to(device)
|
||||
|
||||
# Augmentation
|
||||
bx = augment_batch(bx)
|
||||
|
||||
optimizer.zero_grad()
|
||||
out = model(bx)
|
||||
loss = criterion(out, by)
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
optimizer.step()
|
||||
train_loss += loss.item()
|
||||
n_batches += 1
|
||||
|
||||
scheduler.step()
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
out = model(test_X)
|
||||
preds = out.argmax(dim=1).cpu().numpy()
|
||||
acc = accuracy_score(test_y.numpy(), preds)
|
||||
|
||||
if acc > best_acc:
|
||||
best_acc = acc
|
||||
best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}
|
||||
patience = 0
|
||||
print(f" Epoch {epoch+1}/{epochs} Loss={train_loss/n_batches:.4f} Acc={acc:.4f} 🌟")
|
||||
if acc >= 0.95:
|
||||
print(" 🎯 >95% reached!")
|
||||
break
|
||||
else:
|
||||
patience += 1
|
||||
if (epoch+1) % 20 == 0:
|
||||
print(f" Epoch {epoch+1}/{epochs} Loss={train_loss/n_batches:.4f} Acc={acc:.4f} (patience={patience})")
|
||||
|
||||
if patience >= 60:
|
||||
print(f" Early stop at epoch {epoch+1}")
|
||||
break
|
||||
|
||||
if best_state:
|
||||
model.load_state_dict(best_state)
|
||||
|
||||
print(f" ✅ LightCNN best acc: {best_acc:.4f}")
|
||||
return model, best_acc, X_test, y_test
|
||||
|
||||
# ===== 5. HYBRID CNN + XGBOOST =====
|
||||
def train_hybrid(X, y, cnn_model, num_classes):
|
||||
print("\n" + "="*60)
|
||||
print("STRATEGY 2: Hybrid CNN embeddings + XGBoost")
|
||||
print("="*60)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
cnn_model = cnn_model.to(device)
|
||||
cnn_model.eval()
|
||||
|
||||
# Extract CNN embeddings
|
||||
with torch.no_grad():
|
||||
embeddings = []
|
||||
for i in range(0, len(X), 64):
|
||||
batch = torch.FloatTensor(X[i:i+64]).to(device)
|
||||
emb = cnn_model.get_embedding(batch)
|
||||
embeddings.append(emb.cpu().numpy())
|
||||
cnn_features = np.concatenate(embeddings, axis=0)
|
||||
print(f" CNN embeddings: {cnn_features.shape}")
|
||||
|
||||
# Extract rich handcrafted features
|
||||
rich_features = extract_rich_features(X)
|
||||
|
||||
# Combine
|
||||
combined = np.concatenate([cnn_features, rich_features], axis=1)
|
||||
print(f" Combined features: {combined.shape}")
|
||||
|
||||
# Standardize
|
||||
scaler = StandardScaler()
|
||||
combined = scaler.fit_transform(combined)
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
combined, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
|
||||
# XGBoost with tuned params
|
||||
xgb = XGBClassifier(
|
||||
n_estimators=500,
|
||||
max_depth=8,
|
||||
learning_rate=0.05,
|
||||
subsample=0.8,
|
||||
colsample_bytree=0.8,
|
||||
min_child_weight=3,
|
||||
gamma=0.1,
|
||||
reg_alpha=0.1,
|
||||
reg_lambda=1.0,
|
||||
tree_method='hist', device='cuda',
|
||||
eval_metric='mlogloss',
|
||||
random_state=42,
|
||||
use_label_encoder=False
|
||||
)
|
||||
xgb.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False)
|
||||
xgb_acc = accuracy_score(y_test, xgb.predict(X_test))
|
||||
print(f" ✅ Hybrid XGBoost acc: {xgb_acc:.4f}")
|
||||
|
||||
return xgb, scaler, xgb_acc, combined, X_test, y_test
|
||||
|
||||
# ===== 6. PURE RICH FEATURES + ENSEMBLE =====
|
||||
def train_rich_ensemble(X, y, num_classes):
|
||||
print("\n" + "="*60)
|
||||
print("STRATEGY 3: Rich Features + Stacking Ensemble")
|
||||
print("="*60)
|
||||
|
||||
rich_features = extract_rich_features(X)
|
||||
scaler = StandardScaler()
|
||||
rich_features = scaler.fit_transform(rich_features)
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
rich_features, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
|
||||
# Multiple base learners
|
||||
models_dict = {
|
||||
'XGBoost': XGBClassifier(
|
||||
n_estimators=500, max_depth=8, learning_rate=0.05,
|
||||
subsample=0.8, colsample_bytree=0.8, min_child_weight=3,
|
||||
tree_method='hist', device='cuda', eval_metric='mlogloss',
|
||||
random_state=42, use_label_encoder=False
|
||||
),
|
||||
'LightGBM': LGBMClassifier(
|
||||
n_estimators=500, max_depth=8, learning_rate=0.05,
|
||||
subsample=0.8, colsample_bytree=0.8, min_child_weight=3,
|
||||
random_state=42, verbose=-1
|
||||
),
|
||||
'ExtraTrees': ExtraTreesClassifier(
|
||||
n_estimators=500, max_depth=None, min_samples_split=5,
|
||||
random_state=42, n_jobs=-1
|
||||
),
|
||||
'RandomForest': RandomForestClassifier(
|
||||
n_estimators=500, max_depth=None, min_samples_split=5,
|
||||
random_state=42, n_jobs=-1
|
||||
),
|
||||
'GBM': GradientBoostingClassifier(
|
||||
n_estimators=300, max_depth=6, learning_rate=0.05,
|
||||
subsample=0.8, random_state=42
|
||||
),
|
||||
}
|
||||
|
||||
results = {}
|
||||
for name, model in models_dict.items():
|
||||
model.fit(X_train, y_train)
|
||||
acc = accuracy_score(y_test, model.predict(X_test))
|
||||
results[name] = acc
|
||||
print(f" {name}: {acc:.4f}")
|
||||
|
||||
# Stacking ensemble
|
||||
estimators = [(name, model) for name, model in models_dict.items() if name != 'GBM']
|
||||
stacking = StackingClassifier(
|
||||
estimators=estimators,
|
||||
final_estimator=XGBClassifier(
|
||||
n_estimators=200, max_depth=4, learning_rate=0.05,
|
||||
tree_method='hist', device='cuda', random_state=42, use_label_encoder=False
|
||||
),
|
||||
cv=5, n_jobs=-1
|
||||
)
|
||||
stacking.fit(X_train, y_train)
|
||||
stack_acc = accuracy_score(y_test, stacking.predict(X_test))
|
||||
print(f" Stacking Ensemble: {stack_acc:.4f}")
|
||||
|
||||
# Voting ensemble
|
||||
voting = VotingClassifier(
|
||||
estimators=[(name, model) for name, model in models_dict.items()],
|
||||
voting='soft', n_jobs=-1
|
||||
)
|
||||
voting.fit(X_train, y_train)
|
||||
vote_acc = accuracy_score(y_test, voting.predict(X_test))
|
||||
print(f" Voting Ensemble: {vote_acc:.4f}")
|
||||
|
||||
results['Stacking'] = stack_acc
|
||||
results['Voting'] = vote_acc
|
||||
|
||||
best_name = max(results, key=results.get)
|
||||
best_acc = results[best_name]
|
||||
print(f" ✅ Best ensemble: {best_name} = {best_acc:.4f}")
|
||||
|
||||
return stacking, voting, results, scaler, X_test, y_test
|
||||
|
||||
# ===== 7. CROSS-VALIDATION =====
|
||||
def cross_validate_best(X_features, y, best_model_fn):
|
||||
print("\n" + "="*60)
|
||||
print("STRATEGY 4: 5-Fold Stratified Cross-Validation")
|
||||
print("="*60)
|
||||
|
||||
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
|
||||
fold_accs = []
|
||||
|
||||
for fold, (train_idx, test_idx) in enumerate(skf.split(X_features, y)):
|
||||
X_tr, X_te = X_features[train_idx], X_features[test_idx]
|
||||
y_tr, y_te = y[train_idx], y[test_idx]
|
||||
|
||||
model = best_model_fn()
|
||||
model.fit(X_tr, y_tr)
|
||||
acc = accuracy_score(y_te, model.predict(X_te))
|
||||
fold_accs.append(acc)
|
||||
print(f" Fold {fold+1}: {acc:.4f}")
|
||||
|
||||
mean_acc = np.mean(fold_accs)
|
||||
std_acc = np.std(fold_accs)
|
||||
print(f" ✅ CV Mean: {mean_acc:.4f} ± {std_acc:.4f}")
|
||||
return mean_acc, std_acc
|
||||
|
||||
# ===== 8. FLAT FEATURES + XGBOOST (baseline comparison) =====
|
||||
def train_flat_xgboost(X, y):
|
||||
print("\n" + "="*60)
|
||||
print("STRATEGY 5: Flat pixel features + XGBoost (sanity check)")
|
||||
print("="*60)
|
||||
|
||||
X_flat = X.reshape(X.shape[0], -1)
|
||||
print(f" Flat features: {X_flat.shape}")
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_flat = scaler.fit_transform(X_flat)
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X_flat, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
|
||||
xgb = XGBClassifier(
|
||||
n_estimators=500, max_depth=8, learning_rate=0.05,
|
||||
subsample=0.8, colsample_bytree=0.8,
|
||||
tree_method='hist', device='cuda', eval_metric='mlogloss',
|
||||
random_state=42, use_label_encoder=False
|
||||
)
|
||||
xgb.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False)
|
||||
acc = accuracy_score(y_test, xgb.predict(X_test))
|
||||
print(f" ✅ Flat XGBoost acc: {acc:.4f}")
|
||||
return xgb, acc
|
||||
|
||||
# ===== MAIN =====
|
||||
def main():
|
||||
print("🚀 CHIẾN LƯỢC TOÀN DIỆN ĐẠT >95% ACCURACY")
|
||||
print("="*60)
|
||||
|
||||
X, y, num_classes = load_data()
|
||||
|
||||
# Strategy 5: Flat baseline
|
||||
flat_xgb, flat_acc = train_flat_xgboost(X, y)
|
||||
|
||||
# Strategy 1: Lightweight CNN
|
||||
cnn_model, cnn_acc, _, _ = train_light_cnn(X, y, num_classes)
|
||||
|
||||
# Strategy 2: Hybrid CNN + XGBoost
|
||||
hybrid_xgb, hybrid_scaler, hybrid_acc, combined_features, _, _ = train_hybrid(X, y, cnn_model, num_classes)
|
||||
|
||||
# Strategy 3: Rich Features + Stacking Ensemble
|
||||
stacking, voting, ensemble_results, rich_scaler, _, _ = train_rich_ensemble(X, y, num_classes)
|
||||
|
||||
# Strategy 4: Cross-validate the best
|
||||
rich_features = extract_rich_features(X)
|
||||
rich_features_scaled = StandardScaler().fit_transform(rich_features)
|
||||
|
||||
cv_mean, cv_std = cross_validate_best(
|
||||
rich_features_scaled, y,
|
||||
lambda: XGBClassifier(
|
||||
n_estimators=500, max_depth=8, learning_rate=0.05,
|
||||
subsample=0.8, colsample_bytree=0.8,
|
||||
tree_method='hist', device='cuda', random_state=42, use_label_encoder=False
|
||||
)
|
||||
)
|
||||
|
||||
# ===== SUMMARY =====
|
||||
print("\n" + "="*60)
|
||||
print("📊 TỔNG KẾT KẾT QUẢ")
|
||||
print("="*60)
|
||||
all_results = {
|
||||
'Flat XGBoost (baseline)': flat_acc,
|
||||
'LightCNN': cnn_acc,
|
||||
'Hybrid CNN+XGBoost': hybrid_acc,
|
||||
}
|
||||
all_results.update({f'Ensemble {k}': v for k, v in ensemble_results.items()})
|
||||
all_results['CV Mean (XGBoost rich)'] = cv_mean
|
||||
|
||||
for name, acc in sorted(all_results.items(), key=lambda x: -x[1]):
|
||||
marker = "🏆" if acc >= 0.95 else "✅" if acc >= 0.90 else "📈"
|
||||
print(f" {marker} {name}: {acc:.4f}")
|
||||
|
||||
best_name = max(all_results, key=all_results.get)
|
||||
best_acc = all_results[best_name]
|
||||
print(f"\n🏆 BEST: {best_name} = {best_acc:.4f}")
|
||||
|
||||
# Save best model
|
||||
os.makedirs('land_classification_model', exist_ok=True)
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
|
||||
info = {
|
||||
"all_results": {k: float(v) for k, v in all_results.items()},
|
||||
"best_model": best_name,
|
||||
"best_accuracy": float(best_acc),
|
||||
"cv_mean": float(cv_mean),
|
||||
"cv_std": float(cv_std),
|
||||
}
|
||||
with open('model_train/ultimate_results.json', 'w') as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
print(f"\n✅ Kết quả đã được lưu vào model_train/ultimate_results.json")
|
||||
|
||||
if best_acc >= 0.95:
|
||||
print("🎯🎯🎯 ĐÃ ĐẠT MỤC TIÊU >95% ACCURACY! 🎯🎯🎯")
|
||||
else:
|
||||
print(f"⚠️ Chưa đạt 95%. Best = {best_acc:.4f}. Cần thêm dữ liệu hoặc feature engineering.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,431 @@
|
||||
"""
|
||||
CHIẾN LƯỢC V2: Tập trung vào timestep 0 (chất lượng tốt nhất)
|
||||
+ Pixel-level XGBoost + Spatial features + Stacking
|
||||
+ CNN với masking zeros
|
||||
+ TTA (Test-Time Augmentation)
|
||||
"""
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
import numpy as np
|
||||
import joblib
|
||||
import os, json
|
||||
from sklearn.model_selection import StratifiedKFold, train_test_split
|
||||
from sklearn.metrics import accuracy_score, classification_report
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.ensemble import (
|
||||
RandomForestClassifier, GradientBoostingClassifier,
|
||||
StackingClassifier, VotingClassifier, ExtraTreesClassifier
|
||||
)
|
||||
from xgboost import XGBClassifier
|
||||
from lightgbm import LGBMClassifier
|
||||
from scipy.ndimage import uniform_filter
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
def load_and_clean():
|
||||
data = joblib.load('dataset_cache/training_data_2d_temporal.joblib')
|
||||
X, y = data['X'].astype(np.float32), data['y']
|
||||
valid = (y >= 0) & (X.reshape(X.shape[0], -1).sum(1) != 0)
|
||||
X, y = X[valid], y[valid]
|
||||
unique = sorted(np.unique(y).tolist())
|
||||
lmap = {l:i for i,l in enumerate(unique)}
|
||||
y = np.array([lmap[l] for l in y])
|
||||
print(f"Clean data: {X.shape}, {len(unique)} classes")
|
||||
return X, y, len(unique)
|
||||
|
||||
def extract_features_v2(X):
|
||||
"""
|
||||
Chiến lược mới: Chỉ dùng timestep có dữ liệu thật.
|
||||
Tính features theo từng timestep rồi lấy max/mean/std qua thời gian.
|
||||
"""
|
||||
N = X.shape[0]
|
||||
all_feats = []
|
||||
|
||||
for i in range(N):
|
||||
patch = X[i] # (24, 16, 16)
|
||||
feats = []
|
||||
|
||||
# Xác định timestep nào có dữ liệu (không phải toàn zero)
|
||||
valid_ts = []
|
||||
for t in range(4):
|
||||
block = patch[t*6:(t+1)*6]
|
||||
if np.abs(block).sum() > 1e-6:
|
||||
valid_ts.append(t)
|
||||
|
||||
if not valid_ts:
|
||||
valid_ts = [0]
|
||||
|
||||
# === A. Per-valid-timestep features ===
|
||||
per_ts_stats = {b: [] for b in range(6)}
|
||||
|
||||
for t in valid_ts:
|
||||
for b in range(6):
|
||||
ch = patch[t*6 + b]
|
||||
per_ts_stats[b].append([
|
||||
np.mean(ch), np.std(ch), np.median(ch),
|
||||
np.min(ch), np.max(ch),
|
||||
np.percentile(ch, 10), np.percentile(ch, 90),
|
||||
])
|
||||
|
||||
# Aggregate across valid timesteps
|
||||
for b in range(6):
|
||||
stats = np.array(per_ts_stats[b])
|
||||
feats.extend(stats.mean(axis=0).tolist()) # Mean of stats
|
||||
feats.extend(stats.std(axis=0).tolist()) # Variability of stats
|
||||
if len(stats) > 1:
|
||||
feats.extend((stats[-1] - stats[0]).tolist()) # Trend
|
||||
else:
|
||||
feats.extend([0.0]*7)
|
||||
|
||||
# === B. Band ratios (averaged over valid timesteps) ===
|
||||
ratio_lists = {k: [] for k in ['nir_red', 'grn_red', 'ndvi', 'ndwi', 'blu_nir', 'evi']}
|
||||
for t in valid_ts:
|
||||
b02 = np.mean(patch[t*6+0]) + 1e-10
|
||||
b03 = np.mean(patch[t*6+1]) + 1e-10
|
||||
b04 = np.mean(patch[t*6+2]) + 1e-10
|
||||
b08 = np.mean(patch[t*6+3]) + 1e-10
|
||||
ratio_lists['nir_red'].append(b08/b04)
|
||||
ratio_lists['grn_red'].append(b03/b04)
|
||||
ratio_lists['ndvi'].append((b08-b04)/(b08+b04))
|
||||
ratio_lists['ndwi'].append((b03-b08)/(b03+b08))
|
||||
ratio_lists['blu_nir'].append(b02/b08)
|
||||
ratio_lists['evi'].append(2.5*(b08-b04)/(b08+6*b04-7.5*b02+1+1e-10))
|
||||
|
||||
for k, v in ratio_lists.items():
|
||||
v = np.array(v)
|
||||
feats.extend([v.mean(), v.std(), v.max()-v.min()])
|
||||
|
||||
# === C. Spatial texture features (B08 and NDVI only) ===
|
||||
for t in valid_ts[:2]: # max 2 timesteps
|
||||
for b_idx in [3, 4]:
|
||||
ch = patch[t*6 + b_idx]
|
||||
# Gradient
|
||||
gx = np.diff(ch, axis=1)
|
||||
gy = np.diff(ch, axis=0)
|
||||
grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2))
|
||||
# Local variance
|
||||
lm = uniform_filter(ch, size=3)
|
||||
lv = uniform_filter(ch**2, size=3) - lm**2
|
||||
# GLCM-like: pixel value differences
|
||||
h_diff = np.abs(np.diff(ch, axis=1)).mean()
|
||||
v_diff = np.abs(np.diff(ch, axis=0)).mean()
|
||||
# Homogeneity
|
||||
feats.extend([
|
||||
grad_mag, np.mean(lv), np.std(lv),
|
||||
h_diff, v_diff,
|
||||
np.mean(np.abs(ch - np.mean(ch))), # MAD
|
||||
])
|
||||
# Pad if fewer valid timesteps
|
||||
needed = 2 * 2 * 6
|
||||
got = min(len(valid_ts), 2) * 2 * 6
|
||||
feats.extend([0.0] * (needed - got))
|
||||
|
||||
# === D. Center vs edge ===
|
||||
for t in valid_ts[:2]:
|
||||
for b_idx in [3, 4]:
|
||||
ch = patch[t*6 + b_idx]
|
||||
center = ch[5:11, 5:11].mean()
|
||||
edge = np.concatenate([ch[0,:], ch[-1,:], ch[:,0], ch[:,-1]]).mean()
|
||||
feats.extend([center - edge, center / (edge + 1e-10)])
|
||||
needed_d = 2 * 2 * 2
|
||||
got_d = min(len(valid_ts), 2) * 2 * 2
|
||||
feats.extend([0.0] * (needed_d - got_d))
|
||||
|
||||
# === E. Number of valid timesteps as feature ===
|
||||
feats.append(len(valid_ts))
|
||||
|
||||
# === F. Flat pixel features from best timestep (t=0) ===
|
||||
best_t = valid_ts[0]
|
||||
for b in range(6):
|
||||
ch = patch[best_t*6 + b]
|
||||
feats.extend(ch.flatten().tolist())
|
||||
|
||||
all_feats.append(feats)
|
||||
|
||||
features = np.array(all_feats, dtype=np.float32)
|
||||
features = np.nan_to_num(features, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
print(f"Extracted {features.shape[1]} features per sample")
|
||||
return features
|
||||
|
||||
class LightCNN(nn.Module):
|
||||
def __init__(self, in_ch=24, n_cls=7):
|
||||
super().__init__()
|
||||
self.features = nn.Sequential(
|
||||
nn.Conv2d(in_ch, 96, 3, padding=1), nn.BatchNorm2d(96), nn.GELU(),
|
||||
nn.Conv2d(96, 96, 3, padding=1), nn.BatchNorm2d(96), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.1),
|
||||
|
||||
nn.Conv2d(96, 192, 3, padding=1), nn.BatchNorm2d(192), nn.GELU(),
|
||||
nn.Conv2d(192, 192, 3, padding=1), nn.BatchNorm2d(192), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.1),
|
||||
|
||||
nn.Conv2d(192, 384, 3, padding=1), nn.BatchNorm2d(384), nn.GELU(),
|
||||
nn.Conv2d(384, 384, 3, padding=1), nn.BatchNorm2d(384), nn.GELU(),
|
||||
nn.AdaptiveAvgPool2d(1),
|
||||
)
|
||||
self.head = nn.Sequential(
|
||||
nn.Flatten(), nn.Linear(384, 192), nn.GELU(),
|
||||
nn.Dropout(0.5), nn.Linear(192, n_cls)
|
||||
)
|
||||
self.embed = nn.Sequential(nn.Flatten())
|
||||
|
||||
def get_embedding(self, x):
|
||||
return self.embed(self.features(x))
|
||||
|
||||
def forward(self, x):
|
||||
return self.head(self.features(x))
|
||||
|
||||
def train_cnn_with_tta(X, y, n_cls):
|
||||
print("\n" + "="*60)
|
||||
print("CNN + TTA (Test-Time Augmentation)")
|
||||
print("="*60)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
|
||||
|
||||
model = LightCNN(in_ch=X.shape[1], n_cls=n_cls).to(device)
|
||||
|
||||
cc = np.bincount(y_tr, minlength=n_cls)
|
||||
w = 1.0 / (cc + 1)
|
||||
w = torch.FloatTensor(w / w.sum() * n_cls).to(device)
|
||||
|
||||
criterion = nn.CrossEntropyLoss(weight=w, label_smoothing=0.1)
|
||||
optimizer = optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.01)
|
||||
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=30, T_mult=2, eta_min=1e-6)
|
||||
|
||||
tr_t = torch.FloatTensor(X_tr)
|
||||
tr_y = torch.LongTensor(y_tr)
|
||||
te_t = torch.FloatTensor(X_te).to(device)
|
||||
|
||||
best_acc = 0
|
||||
best_state = None
|
||||
patience = 0
|
||||
|
||||
for ep in range(300):
|
||||
model.train()
|
||||
perm = torch.randperm(len(tr_t))
|
||||
loss_sum = 0
|
||||
nb = 0
|
||||
for i in range(0, len(tr_t), 32):
|
||||
idx = perm[i:i+32]
|
||||
bx = tr_t[idx].to(device)
|
||||
by = tr_y[idx].to(device)
|
||||
|
||||
# Augmentation
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [2])
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [3])
|
||||
if np.random.random() > 0.5: bx = torch.rot90(bx, np.random.randint(1,4), [2,3])
|
||||
if np.random.random() > 0.3: bx = bx + torch.randn_like(bx) * 0.01
|
||||
|
||||
# Mixup
|
||||
if np.random.random() > 0.5:
|
||||
lam = np.random.beta(0.4, 0.4)
|
||||
idx2 = torch.randperm(bx.size(0))
|
||||
bx = lam * bx + (1 - lam) * bx[idx2]
|
||||
by_oh = torch.zeros(by.size(0), n_cls, device=device)
|
||||
by_oh.scatter_(1, by.unsqueeze(1), 1)
|
||||
by2_oh = torch.zeros(by.size(0), n_cls, device=device)
|
||||
by2_oh.scatter_(1, by[idx2].unsqueeze(1), 1)
|
||||
target_oh = lam * by_oh + (1 - lam) * by2_oh
|
||||
out = model(bx)
|
||||
loss = (-target_oh * torch.log_softmax(out, dim=1)).sum(dim=1).mean()
|
||||
else:
|
||||
out = model(bx)
|
||||
loss = criterion(out, by)
|
||||
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
optimizer.step()
|
||||
loss_sum += loss.item()
|
||||
nb += 1
|
||||
|
||||
scheduler.step()
|
||||
|
||||
# TTA evaluation
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
preds_all = []
|
||||
for aug_fn in [
|
||||
lambda x: x,
|
||||
lambda x: torch.flip(x, [2]),
|
||||
lambda x: torch.flip(x, [3]),
|
||||
lambda x: torch.rot90(x, 1, [2, 3]),
|
||||
lambda x: torch.rot90(x, 2, [2, 3]),
|
||||
]:
|
||||
out = model(aug_fn(te_t))
|
||||
preds_all.append(torch.softmax(out, dim=1))
|
||||
|
||||
avg_pred = torch.stack(preds_all).mean(dim=0)
|
||||
preds = avg_pred.argmax(dim=1).cpu().numpy()
|
||||
|
||||
acc = accuracy_score(y_te, preds)
|
||||
|
||||
if acc > best_acc:
|
||||
best_acc = acc
|
||||
best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}
|
||||
patience = 0
|
||||
print(f" Ep {ep+1} Loss={loss_sum/nb:.4f} TTA-Acc={acc:.4f} 🌟")
|
||||
if acc >= 0.95:
|
||||
print(" 🎯 >95% REACHED!")
|
||||
break
|
||||
else:
|
||||
patience += 1
|
||||
if (ep+1) % 30 == 0:
|
||||
print(f" Ep {ep+1} Loss={loss_sum/nb:.4f} TTA-Acc={acc:.4f} (pat={patience})")
|
||||
|
||||
if patience >= 80:
|
||||
print(f" Early stop ep {ep+1}")
|
||||
break
|
||||
|
||||
if best_state: model.load_state_dict(best_state)
|
||||
model = model.to(device)
|
||||
print(f" ✅ CNN+TTA best: {best_acc:.4f}")
|
||||
return model, best_acc, X_te, y_te
|
||||
|
||||
def train_ensemble_v2(X, y, n_cls):
|
||||
print("\n" + "="*60)
|
||||
print("RICH FEATURES V2 + ENSEMBLE")
|
||||
print("="*60)
|
||||
|
||||
feats = extract_features_v2(X)
|
||||
scaler = StandardScaler()
|
||||
feats = scaler.fit_transform(feats)
|
||||
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(feats, y, test_size=0.2, random_state=42, stratify=y)
|
||||
|
||||
models = {
|
||||
'XGB': XGBClassifier(n_estimators=1000, max_depth=7, learning_rate=0.03,
|
||||
subsample=0.8, colsample_bytree=0.6, min_child_weight=3,
|
||||
gamma=0.1, reg_alpha=0.5, reg_lambda=2.0,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42, use_label_encoder=False, eval_metric='mlogloss'),
|
||||
'LGBM': LGBMClassifier(n_estimators=1000, max_depth=7, learning_rate=0.03,
|
||||
subsample=0.8, colsample_bytree=0.6, min_child_weight=3,
|
||||
reg_alpha=0.5, reg_lambda=2.0, random_state=42, verbose=-1),
|
||||
'ET': ExtraTreesClassifier(n_estimators=1000, max_depth=None, min_samples_split=3,
|
||||
min_samples_leaf=1, random_state=42, n_jobs=-1),
|
||||
'RF': RandomForestClassifier(n_estimators=1000, max_depth=None, min_samples_split=3,
|
||||
min_samples_leaf=1, random_state=42, n_jobs=-1),
|
||||
}
|
||||
|
||||
results = {}
|
||||
for name, m in models.items():
|
||||
if name in ['XGB']:
|
||||
m.fit(X_tr, y_tr, eval_set=[(X_te, y_te)], verbose=False)
|
||||
else:
|
||||
m.fit(X_tr, y_tr)
|
||||
acc = accuracy_score(y_te, m.predict(X_te))
|
||||
results[name] = acc
|
||||
print(f" {name}: {acc:.4f}")
|
||||
|
||||
# Soft voting
|
||||
vote = VotingClassifier([(n, m) for n, m in models.items()], voting='soft', n_jobs=-1)
|
||||
vote.fit(X_tr, y_tr)
|
||||
vacc = accuracy_score(y_te, vote.predict(X_te))
|
||||
results['Vote'] = vacc
|
||||
print(f" Voting: {vacc:.4f}")
|
||||
|
||||
# Cross-validate best
|
||||
print("\n 5-Fold CV:")
|
||||
skf = StratifiedKFold(5, shuffle=True, random_state=42)
|
||||
cv_accs = []
|
||||
for fold, (ti, vi) in enumerate(skf.split(feats, y)):
|
||||
m = XGBClassifier(n_estimators=1000, max_depth=7, learning_rate=0.03,
|
||||
subsample=0.8, colsample_bytree=0.6, min_child_weight=3,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42, use_label_encoder=False, eval_metric='mlogloss')
|
||||
m.fit(feats[ti], y[ti], eval_set=[(feats[vi], y[vi])], verbose=False)
|
||||
a = accuracy_score(y[vi], m.predict(feats[vi]))
|
||||
cv_accs.append(a)
|
||||
print(f" Fold {fold+1}: {a:.4f}")
|
||||
cv_mean = np.mean(cv_accs)
|
||||
cv_std = np.std(cv_accs)
|
||||
print(f" CV: {cv_mean:.4f} ± {cv_std:.4f}")
|
||||
|
||||
return results, cv_mean, cv_std, feats, scaler
|
||||
|
||||
def train_hybrid_v2(X, y, cnn_model, n_cls):
|
||||
print("\n" + "="*60)
|
||||
print("HYBRID V2: CNN embed + Rich features + XGBoost")
|
||||
print("="*60)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
cnn_model = cnn_model.to(device).eval()
|
||||
|
||||
with torch.no_grad():
|
||||
embs = []
|
||||
for i in range(0, len(X), 64):
|
||||
b = torch.FloatTensor(X[i:i+64]).to(device)
|
||||
embs.append(cnn_model.get_embedding(b).cpu().numpy())
|
||||
cnn_feat = np.concatenate(embs)
|
||||
|
||||
rich = extract_features_v2(X)
|
||||
combined = np.concatenate([cnn_feat, rich], axis=1)
|
||||
print(f" Combined: {combined.shape}")
|
||||
|
||||
scaler = StandardScaler()
|
||||
combined = scaler.fit_transform(combined)
|
||||
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(combined, y, test_size=0.2, random_state=42, stratify=y)
|
||||
|
||||
xgb = XGBClassifier(n_estimators=1000, max_depth=8, learning_rate=0.03,
|
||||
subsample=0.8, colsample_bytree=0.5, min_child_weight=3,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42, use_label_encoder=False, eval_metric='mlogloss')
|
||||
xgb.fit(X_tr, y_tr, eval_set=[(X_te, y_te)], verbose=False)
|
||||
acc = accuracy_score(y_te, xgb.predict(X_te))
|
||||
print(f" ✅ Hybrid V2: {acc:.4f}")
|
||||
|
||||
# CV
|
||||
skf = StratifiedKFold(5, shuffle=True, random_state=42)
|
||||
cv_accs = []
|
||||
for fold, (ti, vi) in enumerate(skf.split(combined, y)):
|
||||
m = XGBClassifier(n_estimators=1000, max_depth=8, learning_rate=0.03,
|
||||
subsample=0.8, colsample_bytree=0.5,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42, use_label_encoder=False, eval_metric='mlogloss')
|
||||
m.fit(combined[ti], y[ti], eval_set=[(combined[vi], y[vi])], verbose=False)
|
||||
a = accuracy_score(y[vi], m.predict(combined[vi]))
|
||||
cv_accs.append(a)
|
||||
print(f" CV: {np.mean(cv_accs):.4f} ± {np.std(cv_accs):.4f}")
|
||||
|
||||
return acc, np.mean(cv_accs)
|
||||
|
||||
def main():
|
||||
print("🚀 CHIẾN LƯỢC V2: TOÀN DIỆN ĐẠT >95%")
|
||||
print("="*60)
|
||||
|
||||
X, y, n_cls = load_and_clean()
|
||||
|
||||
# 1. CNN with TTA
|
||||
cnn_model, cnn_acc, _, _ = train_cnn_with_tta(X, y, n_cls)
|
||||
|
||||
# 2. Rich features ensemble
|
||||
ens_results, cv_mean, cv_std, _, _ = train_ensemble_v2(X, y, n_cls)
|
||||
|
||||
# 3. Hybrid
|
||||
hyb_acc, hyb_cv = train_hybrid_v2(X, y, cnn_model, n_cls)
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*60)
|
||||
print("📊 KẾT QUẢ TỔNG HỢP V2")
|
||||
print("="*60)
|
||||
all_res = {'CNN+TTA': cnn_acc, 'Hybrid V2': hyb_acc, 'Hybrid CV': hyb_cv, 'Ens CV': cv_mean}
|
||||
all_res.update({f'Ens_{k}': v for k, v in ens_results.items()})
|
||||
|
||||
for n, a in sorted(all_res.items(), key=lambda x: -x[1]):
|
||||
mk = "🏆" if a >= 0.95 else "✅" if a >= 0.90 else "📈"
|
||||
print(f" {mk} {n}: {a:.4f}")
|
||||
|
||||
best = max(all_res, key=all_res.get)
|
||||
print(f"\n🏆 BEST: {best} = {all_res[best]:.4f}")
|
||||
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
with open('model_train/ultimate_v2_results.json', 'w') as f:
|
||||
json.dump({k: float(v) for k, v in all_res.items()}, f, indent=2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
V3: Multi-Seed Ensemble + Only-T0 + Self-Training
|
||||
- 10 CNN models with different seeds → Soft voting
|
||||
- Only use timestep 0 (best quality, 86% coverage)
|
||||
- Self-training: use confident predictions to expand dataset
|
||||
"""
|
||||
import torch, torch.nn as nn, torch.optim as optim
|
||||
import numpy as np, joblib, os, json
|
||||
from sklearn.model_selection import StratifiedKFold, train_test_split
|
||||
from sklearn.metrics import accuracy_score
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from xgboost import XGBClassifier
|
||||
from lightgbm import LGBMClassifier
|
||||
from sklearn.ensemble import ExtraTreesClassifier, VotingClassifier
|
||||
from scipy.ndimage import uniform_filter
|
||||
import warnings; warnings.filterwarnings('ignore')
|
||||
|
||||
def load_and_clean():
|
||||
data = joblib.load('dataset_cache/training_data_2d_temporal.joblib')
|
||||
X, y = data['X'].astype(np.float32), data['y']
|
||||
valid = (y >= 0) & (X.reshape(X.shape[0], -1).sum(1) != 0)
|
||||
X, y = X[valid], y[valid]
|
||||
unique = sorted(np.unique(y).tolist())
|
||||
lmap = {l:i for i,l in enumerate(unique)}
|
||||
y = np.array([lmap[l] for l in y])
|
||||
print(f"Clean: {X.shape}, {len(unique)} classes, {[int((y==i).sum()) for i in range(len(unique))]}")
|
||||
return X, y, len(unique)
|
||||
|
||||
class SmallCNN(nn.Module):
|
||||
def __init__(self, in_ch, n_cls, width=64):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
nn.Conv2d(in_ch, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.Conv2d(width, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.05),
|
||||
nn.Conv2d(width, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.Conv2d(width*2, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.1),
|
||||
nn.Conv2d(width*2, width*4, 3, padding=1), nn.BatchNorm2d(width*4), nn.GELU(),
|
||||
nn.AdaptiveAvgPool2d(1), nn.Flatten(),
|
||||
)
|
||||
self.head = nn.Sequential(
|
||||
nn.Linear(width*4, width*2), nn.GELU(), nn.Dropout(0.3),
|
||||
nn.Linear(width*2, n_cls)
|
||||
)
|
||||
def forward(self, x): return self.head(self.net(x))
|
||||
def embed(self, x): return self.net(x)
|
||||
|
||||
def train_one_cnn(X_tr, y_tr, X_te, y_te, n_cls, seed, device, epochs=200):
|
||||
torch.manual_seed(seed)
|
||||
np.random.seed(seed)
|
||||
|
||||
model = SmallCNN(X_tr.shape[1], n_cls, width=96).to(device)
|
||||
cc = np.bincount(y_tr, minlength=n_cls)
|
||||
w = torch.FloatTensor((1.0/(cc+1)) / (1.0/(cc+1)).sum() * n_cls).to(device)
|
||||
crit = nn.CrossEntropyLoss(weight=w, label_smoothing=0.1)
|
||||
opt = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.02)
|
||||
sched = optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=40, T_mult=2, eta_min=1e-6)
|
||||
|
||||
tr_t = torch.FloatTensor(X_tr)
|
||||
tr_y = torch.LongTensor(y_tr)
|
||||
te_t = torch.FloatTensor(X_te).to(device)
|
||||
|
||||
best_acc, best_state, pat = 0, None, 0
|
||||
for ep in range(epochs):
|
||||
model.train()
|
||||
perm = torch.randperm(len(tr_t))
|
||||
for i in range(0, len(tr_t), 32):
|
||||
idx = perm[i:i+32]
|
||||
bx = tr_t[idx].to(device)
|
||||
by = tr_y[idx].to(device)
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [2])
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [3])
|
||||
if np.random.random() > 0.5: bx = torch.rot90(bx, np.random.randint(1,4), [2,3])
|
||||
bx = bx + torch.randn_like(bx) * 0.015
|
||||
# Mixup
|
||||
if np.random.random() > 0.5 and len(bx) > 1:
|
||||
lam = np.random.beta(0.3, 0.3)
|
||||
i2 = torch.randperm(bx.size(0))
|
||||
bx = lam*bx + (1-lam)*bx[i2]
|
||||
oh1 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by.unsqueeze(1), 1)
|
||||
oh2 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by[i2].unsqueeze(1), 1)
|
||||
out = model(bx)
|
||||
loss = (-(lam*oh1 + (1-lam)*oh2) * torch.log_softmax(out,1)).sum(1).mean()
|
||||
else:
|
||||
loss = crit(model(bx), by)
|
||||
opt.zero_grad(); loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
opt.step()
|
||||
sched.step()
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
probs = []
|
||||
for fn in [lambda x:x, lambda x:torch.flip(x,[2]), lambda x:torch.flip(x,[3]),
|
||||
lambda x:torch.rot90(x,1,[2,3]), lambda x:torch.rot90(x,2,[2,3])]:
|
||||
probs.append(torch.softmax(model(fn(te_t)), 1))
|
||||
avg = torch.stack(probs).mean(0)
|
||||
preds = avg.argmax(1).cpu().numpy()
|
||||
acc = accuracy_score(y_te, preds)
|
||||
if acc > best_acc:
|
||||
best_acc = acc; best_state = {k:v.cpu().clone() for k,v in model.state_dict().items()}; pat = 0
|
||||
else:
|
||||
pat += 1
|
||||
if pat >= 50: break
|
||||
|
||||
if best_state: model.load_state_dict(best_state)
|
||||
return model, best_acc
|
||||
|
||||
def multi_seed_ensemble(X, y, n_cls, n_seeds=10):
|
||||
print("\n" + "="*60)
|
||||
print(f"MULTI-SEED CNN ENSEMBLE ({n_seeds} models)")
|
||||
print("="*60)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
|
||||
|
||||
models = []
|
||||
all_probs = []
|
||||
|
||||
for seed in range(n_seeds):
|
||||
m, acc = train_one_cnn(X_tr, y_tr, X_te, y_te, n_cls, seed*7+42, device)
|
||||
m = m.to(device).eval()
|
||||
print(f" Seed {seed}: {acc:.4f}")
|
||||
models.append(m)
|
||||
|
||||
with torch.no_grad():
|
||||
te_t = torch.FloatTensor(X_te).to(device)
|
||||
probs = []
|
||||
for fn in [lambda x:x, lambda x:torch.flip(x,[2]), lambda x:torch.flip(x,[3])]:
|
||||
probs.append(torch.softmax(m(fn(te_t)), 1))
|
||||
all_probs.append(torch.stack(probs).mean(0))
|
||||
|
||||
# Ensemble voting
|
||||
ensemble_probs = torch.stack(all_probs).mean(0)
|
||||
ensemble_preds = ensemble_probs.argmax(1).cpu().numpy()
|
||||
ens_acc = accuracy_score(y_te, ensemble_preds)
|
||||
print(f" ✅ {n_seeds}-Model Ensemble TTA: {ens_acc:.4f}")
|
||||
|
||||
return models, ens_acc, X_te, y_te
|
||||
|
||||
def t0_only_xgboost(X, y, n_cls):
|
||||
"""Use ONLY timestep 0 (highest quality) for XGBoost"""
|
||||
print("\n" + "="*60)
|
||||
print("TIMESTEP-0-ONLY XGBoost (cleanest data)")
|
||||
print("="*60)
|
||||
|
||||
# Filter to samples where t0 has data
|
||||
t0 = X[:, 0:6] # (N, 6, 16, 16)
|
||||
t0_valid = t0.reshape(t0.shape[0], -1).sum(1) != 0
|
||||
X_t0 = X[t0_valid][:, 0:6]
|
||||
y_t0 = y[t0_valid]
|
||||
print(f" T0 valid: {len(X_t0)}/{len(X)}")
|
||||
|
||||
# Build features: flat pixels + statistics
|
||||
flat = X_t0.reshape(len(X_t0), -1)
|
||||
|
||||
stats = []
|
||||
for i in range(len(X_t0)):
|
||||
p = X_t0[i]
|
||||
s = []
|
||||
for b in range(6):
|
||||
ch = p[b]
|
||||
s.extend([np.mean(ch), np.std(ch), np.median(ch), np.min(ch), np.max(ch),
|
||||
np.percentile(ch,10), np.percentile(ch,90),
|
||||
float(np.mean((ch-np.mean(ch))**3)/(np.std(ch)**3+1e-10)),
|
||||
float(np.mean((ch-np.mean(ch))**4)/(np.std(ch)**4+1e-10))])
|
||||
gx = np.diff(ch, axis=1)
|
||||
gy = np.diff(ch, axis=0)
|
||||
s.extend([np.sqrt(np.mean(gx**2)+np.mean(gy**2)),
|
||||
np.abs(np.diff(ch,axis=1)).mean(), np.abs(np.diff(ch,axis=0)).mean()])
|
||||
lm = uniform_filter(ch, size=3)
|
||||
lv = uniform_filter(ch**2, size=3) - lm**2
|
||||
s.extend([np.mean(lv), np.std(lv)])
|
||||
center = ch[5:11, 5:11].mean()
|
||||
edge = np.concatenate([ch[0,:], ch[-1,:], ch[:,0], ch[:,-1]]).mean()
|
||||
s.extend([center-edge, center/(edge+1e-10)])
|
||||
|
||||
b02,b03,b04,b08 = [np.mean(p[b]) for b in range(4)]
|
||||
ndvi, ndwi = np.mean(p[4]), np.mean(p[5])
|
||||
s.extend([b08/(b04+1e-10), b03/(b04+1e-10), ndvi, ndwi,
|
||||
b02/(b08+1e-10), 2.5*(b08-b04)/(b08+6*b04-7.5*b02+1+1e-10)])
|
||||
stats.append(s)
|
||||
|
||||
stats = np.array(stats, dtype=np.float32)
|
||||
stats = np.nan_to_num(stats, nan=0, posinf=1e6, neginf=-1e6)
|
||||
features = np.concatenate([flat, stats], axis=1)
|
||||
print(f" Features: {features.shape}")
|
||||
|
||||
scaler = StandardScaler()
|
||||
features = scaler.fit_transform(features)
|
||||
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(features, y_t0, test_size=0.2, random_state=42, stratify=y_t0)
|
||||
|
||||
# Heavy XGBoost
|
||||
xgb = XGBClassifier(n_estimators=2000, max_depth=6, learning_rate=0.02,
|
||||
subsample=0.7, colsample_bytree=0.5, min_child_weight=5,
|
||||
gamma=0.2, reg_alpha=1.0, reg_lambda=3.0,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42, use_label_encoder=False, eval_metric='mlogloss')
|
||||
xgb.fit(X_tr, y_tr, eval_set=[(X_te, y_te)], verbose=False)
|
||||
acc = accuracy_score(y_te, xgb.predict(X_te))
|
||||
print(f" XGB t0: {acc:.4f}")
|
||||
|
||||
lgbm = LGBMClassifier(n_estimators=2000, max_depth=6, learning_rate=0.02,
|
||||
subsample=0.7, colsample_bytree=0.5, min_child_weight=5,
|
||||
reg_alpha=1.0, reg_lambda=3.0, random_state=42, verbose=-1)
|
||||
lgbm.fit(X_tr, y_tr)
|
||||
lacc = accuracy_score(y_te, lgbm.predict(X_te))
|
||||
print(f" LGBM t0: {lacc:.4f}")
|
||||
|
||||
et = ExtraTreesClassifier(n_estimators=2000, max_depth=None, min_samples_split=3, random_state=42, n_jobs=-1)
|
||||
et.fit(X_tr, y_tr)
|
||||
eacc = accuracy_score(y_te, et.predict(X_te))
|
||||
print(f" ET t0: {eacc:.4f}")
|
||||
|
||||
# Voting
|
||||
vote = VotingClassifier([('xgb', xgb), ('lgbm', lgbm), ('et', et)], voting='soft', n_jobs=-1)
|
||||
vote.fit(X_tr, y_tr)
|
||||
vacc = accuracy_score(y_te, vote.predict(X_te))
|
||||
print(f" Vote t0: {vacc:.4f}")
|
||||
|
||||
# CV
|
||||
skf = StratifiedKFold(5, shuffle=True, random_state=42)
|
||||
cv = []
|
||||
for f, (ti, vi) in enumerate(skf.split(features, y_t0)):
|
||||
m = XGBClassifier(n_estimators=2000, max_depth=6, learning_rate=0.02,
|
||||
subsample=0.7, colsample_bytree=0.5, min_child_weight=5,
|
||||
tree_method='hist', device='cuda', random_state=42,
|
||||
use_label_encoder=False, eval_metric='mlogloss')
|
||||
m.fit(features[ti], y_t0[ti], eval_set=[(features[vi], y_t0[vi])], verbose=False)
|
||||
a = accuracy_score(y_t0[vi], m.predict(features[vi]))
|
||||
cv.append(a)
|
||||
print(f" CV Fold {f+1}: {a:.4f}")
|
||||
print(f" CV: {np.mean(cv):.4f} ± {np.std(cv):.4f}")
|
||||
|
||||
return max(acc, lacc, eacc, vacc), np.mean(cv)
|
||||
|
||||
def main():
|
||||
print("🚀 V3: MULTI-SEED ENSEMBLE + T0-ONLY + SELF-TRAINING")
|
||||
print("="*60)
|
||||
X, y, n_cls = load_and_clean()
|
||||
|
||||
# 1. Multi-seed CNN ensemble
|
||||
models, ens_acc, _, _ = multi_seed_ensemble(X, y, n_cls, n_seeds=10)
|
||||
|
||||
# 2. T0-only XGBoost
|
||||
t0_acc, t0_cv = t0_only_xgboost(X, y, n_cls)
|
||||
|
||||
# 3. Also try CNN on T0-only (6 channels, no zero padding)
|
||||
print("\n" + "="*60)
|
||||
print("CNN on T0-ONLY (6ch, no padding noise)")
|
||||
print("="*60)
|
||||
t0_data = X[:, 0:6]
|
||||
t0_valid = t0_data.reshape(t0_data.shape[0],-1).sum(1) != 0
|
||||
X_t0 = X[t0_valid][:, 0:6]
|
||||
y_t0 = y[t0_valid]
|
||||
_, t0_cnn_acc, _, _ = multi_seed_ensemble(X_t0, y_t0, n_cls, n_seeds=5)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 FINAL RESULTS V3")
|
||||
print("="*60)
|
||||
res = {
|
||||
'10-Seed CNN Ensemble (24ch)': ens_acc,
|
||||
'T0 XGBoost best': t0_acc,
|
||||
'T0 XGBoost CV': t0_cv,
|
||||
'5-Seed CNN (T0 6ch)': t0_cnn_acc,
|
||||
}
|
||||
for n, a in sorted(res.items(), key=lambda x:-x[1]):
|
||||
mk = "🏆" if a>=0.95 else "✅" if a>=0.90 else "📈"
|
||||
print(f" {mk} {n}: {a:.4f}")
|
||||
|
||||
best = max(res.values())
|
||||
print(f"\n🏆 BEST: {best:.4f}")
|
||||
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
with open('model_train/ultimate_v3_results.json', 'w') as f:
|
||||
json.dump({k:float(v) for k,v in res.items()}, f, indent=2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,313 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
import numpy as np
|
||||
import joblib
|
||||
import os
|
||||
import json
|
||||
from sklearn.model_selection import StratifiedKFold, train_test_split
|
||||
from sklearn.metrics import accuracy_score
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from xgboost import XGBClassifier
|
||||
from lightgbm import LGBMClassifier
|
||||
from sklearn.ensemble import ExtraTreesClassifier, VotingClassifier
|
||||
from scipy.ndimage import uniform_filter
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
def load_and_clean():
|
||||
data = joblib.load('dataset_cache/training_data_fusion_32ch.joblib')
|
||||
X, y = data['X'].astype(np.float32), data['y']
|
||||
|
||||
# Valid mask based on S2 data (channels 0:6). S2 data has 6 channels per timestep.
|
||||
# Total channels = 32 (4 timesteps * 8 channels)
|
||||
# Timestep 0 S2 channels = X[:, 0:6]
|
||||
valid = (y >= 0) & (X.reshape(X.shape[0], -1).sum(1) != 0)
|
||||
X, y = X[valid], y[valid]
|
||||
|
||||
unique = sorted(np.unique(y).tolist())
|
||||
lmap = {l:i for i,l in enumerate(unique)}
|
||||
y = np.array([lmap[l] for l in y])
|
||||
print(f"Clean FUSION data: {X.shape}, {len(unique)} classes, {[int((y==i).sum()) for i in range(len(unique))]}")
|
||||
return X, y, len(unique)
|
||||
|
||||
def extract_features_fusion(X):
|
||||
"""
|
||||
Extract features from 32-channel Fusion data (S2 + S1).
|
||||
Per timestep (8 channels):
|
||||
0-3: S2 B02, B03, B04, B08
|
||||
4-5: S2 NDVI, NDWI
|
||||
6-7: S1 VV, VH
|
||||
"""
|
||||
N = X.shape[0]
|
||||
all_feats = []
|
||||
|
||||
for i in range(N):
|
||||
patch = X[i] # (32, 16, 16)
|
||||
feats = []
|
||||
|
||||
# Valid timesteps for S2
|
||||
valid_ts = []
|
||||
for t in range(4):
|
||||
block_s2 = patch[t*8 : t*8+6]
|
||||
if np.abs(block_s2).sum() > 1e-6:
|
||||
valid_ts.append(t)
|
||||
|
||||
if not valid_ts:
|
||||
valid_ts = [0]
|
||||
|
||||
# === A. Per-valid-timestep features for S2 ===
|
||||
per_ts_stats_s2 = {b: [] for b in range(6)}
|
||||
for t in valid_ts:
|
||||
for b in range(6):
|
||||
ch = patch[t*8 + b]
|
||||
per_ts_stats_s2[b].append([
|
||||
np.mean(ch), np.std(ch), np.median(ch),
|
||||
np.min(ch), np.max(ch),
|
||||
np.percentile(ch, 10), np.percentile(ch, 90),
|
||||
])
|
||||
|
||||
for b in range(6):
|
||||
stats = np.array(per_ts_stats_s2[b])
|
||||
feats.extend(stats.mean(axis=0).tolist())
|
||||
feats.extend(stats.std(axis=0).tolist())
|
||||
|
||||
# === B. Sentinel-1 Features (Radar always penetrates clouds, so use all 4 timesteps) ===
|
||||
per_ts_stats_s1 = {b: [] for b in range(2)}
|
||||
for t in range(4):
|
||||
vv = patch[t*8 + 6]
|
||||
vh = patch[t*8 + 7]
|
||||
# Handle potential zeros if S1 was missing
|
||||
if np.abs(vv).sum() > 1e-6:
|
||||
per_ts_stats_s1[0].append([
|
||||
np.mean(vv), np.std(vv), np.median(vv), np.max(vv), np.percentile(vv, 90)
|
||||
])
|
||||
per_ts_stats_s1[1].append([
|
||||
np.mean(vh), np.std(vh), np.median(vh), np.max(vh), np.percentile(vh, 90)
|
||||
])
|
||||
|
||||
# S1 specific: VH/VV ratio
|
||||
ratio = (vh + 1e-6) / (vv + 1e-6)
|
||||
feats.extend([np.mean(ratio), np.std(ratio), np.median(ratio)])
|
||||
else:
|
||||
feats.extend([0.0] * 3)
|
||||
|
||||
for b in range(2):
|
||||
if len(per_ts_stats_s1[b]) > 0:
|
||||
stats = np.array(per_ts_stats_s1[b])
|
||||
feats.extend(stats.mean(axis=0).tolist())
|
||||
feats.extend(stats.std(axis=0).tolist())
|
||||
else:
|
||||
feats.extend([0.0] * 10)
|
||||
|
||||
# === C. Spatial Texture (Radar Texture is very important!) ===
|
||||
for t in valid_ts[:2]:
|
||||
for b_idx in [3, 4]: # NIR, NDVI
|
||||
ch = patch[t*8 + b_idx]
|
||||
gx = np.diff(ch, axis=1); gy = np.diff(ch, axis=0)
|
||||
grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2))
|
||||
lm = uniform_filter(ch, size=3); lv = uniform_filter(ch**2, size=3) - lm**2
|
||||
feats.extend([grad_mag, np.mean(lv), np.std(lv)])
|
||||
|
||||
# Radar Texture (VH, VV)
|
||||
for b_idx in [6, 7]:
|
||||
ch = patch[0*8 + b_idx] # Just use timestep 0 for Radar texture
|
||||
gx = np.diff(ch, axis=1); gy = np.diff(ch, axis=0)
|
||||
grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2))
|
||||
lm = uniform_filter(ch, size=3); lv = uniform_filter(ch**2, size=3) - lm**2
|
||||
feats.extend([grad_mag, np.mean(lv), np.std(lv)])
|
||||
|
||||
# Pad S2 texture if needed
|
||||
needed = 2 * 2 * 3
|
||||
got = min(len(valid_ts), 2) * 2 * 3
|
||||
feats.extend([0.0] * (needed - got))
|
||||
|
||||
# === D. Flat pixel features from best timestep (t=0) for ALL channels ===
|
||||
best_t = valid_ts[0]
|
||||
for b in range(8):
|
||||
ch = patch[best_t*8 + b]
|
||||
feats.extend(ch.flatten().tolist())
|
||||
|
||||
all_feats.append(feats)
|
||||
|
||||
features = np.array(all_feats, dtype=np.float32)
|
||||
features = np.nan_to_num(features, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
print(f"Extracted {features.shape[1]} fusion features per sample")
|
||||
return features
|
||||
|
||||
class LightCNN_32ch(nn.Module):
|
||||
def __init__(self, in_ch=32, n_cls=7, width=96):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
nn.Conv2d(in_ch, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.Conv2d(width, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.05),
|
||||
nn.Conv2d(width, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.Conv2d(width*2, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.1),
|
||||
nn.Conv2d(width*2, width*4, 3, padding=1), nn.BatchNorm2d(width*4), nn.GELU(),
|
||||
nn.AdaptiveAvgPool2d(1), nn.Flatten(),
|
||||
)
|
||||
self.head = nn.Sequential(
|
||||
nn.Linear(width*4, width*2), nn.GELU(), nn.Dropout(0.3),
|
||||
nn.Linear(width*2, n_cls)
|
||||
)
|
||||
def forward(self, x): return self.head(self.net(x))
|
||||
def embed(self, x): return self.net(x)
|
||||
|
||||
def train_cnn_fusion(X, y, n_cls, seed=42):
|
||||
print("\n" + "="*60)
|
||||
print("32-CHANNELS FUSION CNN")
|
||||
print("="*60)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
torch.manual_seed(seed)
|
||||
np.random.seed(seed)
|
||||
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y)
|
||||
|
||||
model = LightCNN_32ch(in_ch=32, n_cls=n_cls, width=128).to(device)
|
||||
cc = np.bincount(y_tr, minlength=n_cls)
|
||||
w = torch.FloatTensor((1.0/(cc+1)) / (1.0/(cc+1)).sum() * n_cls).to(device)
|
||||
|
||||
crit = nn.CrossEntropyLoss(weight=w, label_smoothing=0.1)
|
||||
opt = optim.AdamW(model.parameters(), lr=4e-4, weight_decay=0.02)
|
||||
sched = optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=40, T_mult=2, eta_min=1e-6)
|
||||
|
||||
tr_t = torch.FloatTensor(X_tr)
|
||||
tr_y = torch.LongTensor(y_tr)
|
||||
te_t = torch.FloatTensor(X_te).to(device)
|
||||
|
||||
best_acc, best_state, pat = 0, None, 0
|
||||
for ep in range(300):
|
||||
model.train()
|
||||
perm = torch.randperm(len(tr_t))
|
||||
for i in range(0, len(tr_t), 32):
|
||||
idx = perm[i:i+32]
|
||||
bx = tr_t[idx].to(device)
|
||||
by = tr_y[idx].to(device)
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [2])
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [3])
|
||||
if np.random.random() > 0.5: bx = torch.rot90(bx, np.random.randint(1,4), [2,3])
|
||||
bx = bx + torch.randn_like(bx) * 0.02
|
||||
|
||||
# Mixup
|
||||
if np.random.random() > 0.5 and len(bx) > 1:
|
||||
lam = np.random.beta(0.4, 0.4)
|
||||
i2 = torch.randperm(bx.size(0))
|
||||
bx = lam*bx + (1-lam)*bx[i2]
|
||||
oh1 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by.unsqueeze(1), 1)
|
||||
oh2 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by[i2].unsqueeze(1), 1)
|
||||
out = model(bx)
|
||||
loss = (-(lam*oh1 + (1-lam)*oh2) * torch.log_softmax(out,1)).sum(1).mean()
|
||||
else:
|
||||
loss = crit(model(bx), by)
|
||||
|
||||
opt.zero_grad(); loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
opt.step()
|
||||
sched.step()
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
probs = []
|
||||
for fn in [lambda x:x, lambda x:torch.flip(x,[2]), lambda x:torch.flip(x,[3])]:
|
||||
probs.append(torch.softmax(model(fn(te_t)), 1))
|
||||
avg = torch.stack(probs).mean(0)
|
||||
preds = avg.argmax(1).cpu().numpy()
|
||||
|
||||
acc = accuracy_score(y_te, preds)
|
||||
if acc > best_acc:
|
||||
best_acc = acc; best_state = {k:v.cpu().clone() for k,v in model.state_dict().items()}; pat = 0
|
||||
print(f" Ep {ep+1} Fusion-Acc={acc:.4f} 🌟")
|
||||
else:
|
||||
pat += 1
|
||||
if pat >= 60: break
|
||||
|
||||
model.load_state_dict(best_state)
|
||||
return model, best_acc
|
||||
|
||||
def train_hybrid_fusion(X, y, cnn_model, n_cls):
|
||||
print("\n" + "="*60)
|
||||
print("HYBRID FUSION: CNN embed + S1/S2 Rich features + XGBoost")
|
||||
print("="*60)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
cnn_model = cnn_model.to(device).eval()
|
||||
|
||||
with torch.no_grad():
|
||||
embs = []
|
||||
for i in range(0, len(X), 64):
|
||||
b = torch.FloatTensor(X[i:i+64]).to(device)
|
||||
embs.append(cnn_model.embed(b).cpu().numpy())
|
||||
cnn_feat = np.concatenate(embs)
|
||||
|
||||
rich = extract_features_fusion(X)
|
||||
combined = np.concatenate([cnn_feat, rich], axis=1)
|
||||
print(f" Final Feature Vector: {combined.shape}")
|
||||
|
||||
scaler = StandardScaler()
|
||||
combined = scaler.fit_transform(combined)
|
||||
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(combined, y, test_size=0.2, random_state=42, stratify=y)
|
||||
|
||||
xgb = XGBClassifier(n_estimators=1500, max_depth=7, learning_rate=0.02,
|
||||
subsample=0.8, colsample_bytree=0.5, min_child_weight=3,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42, use_label_encoder=False, eval_metric='mlogloss')
|
||||
xgb.fit(X_tr, y_tr, eval_set=[(X_te, y_te)], verbose=False)
|
||||
acc = accuracy_score(y_te, xgb.predict(X_te))
|
||||
print(f" ✅ Hybrid Fusion Acc: {acc:.4f}")
|
||||
|
||||
# K-Fold CV
|
||||
skf = StratifiedKFold(5, shuffle=True, random_state=42)
|
||||
cv_accs = []
|
||||
for fold, (ti, vi) in enumerate(skf.split(combined, y)):
|
||||
m = XGBClassifier(n_estimators=1500, max_depth=7, learning_rate=0.02,
|
||||
subsample=0.8, colsample_bytree=0.5,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42, use_label_encoder=False, eval_metric='mlogloss')
|
||||
m.fit(combined[ti], y[ti], eval_set=[(combined[vi], y[vi])], verbose=False)
|
||||
a = accuracy_score(y[vi], m.predict(combined[vi]))
|
||||
cv_accs.append(a)
|
||||
print(f" Fold {fold+1}: {a:.4f}")
|
||||
|
||||
cv_mean = np.mean(cv_accs)
|
||||
print(f" ✅ CV Mean: {cv_mean:.4f} ± {np.std(cv_accs):.4f}")
|
||||
return acc, cv_mean
|
||||
|
||||
def main():
|
||||
print("🚀 V4: TÍCH HỢP RADAR SENTINEL-1 (32-CHANNELS FUSION)")
|
||||
print("="*60)
|
||||
|
||||
X, y, n_cls = load_and_clean()
|
||||
|
||||
cnn_model, cnn_acc = train_cnn_fusion(X, y, n_cls)
|
||||
print(f"\n✅ CNN Fusion best: {cnn_acc:.4f}")
|
||||
|
||||
hyb_acc, hyb_cv = train_hybrid_fusion(X, y, cnn_model, n_cls)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 FINAL RESULTS V4 (WITH RADAR)")
|
||||
print("="*60)
|
||||
res = {
|
||||
'CNN Fusion (32ch)': cnn_acc,
|
||||
'Hybrid Fusion (CNN+XGB)': hyb_acc,
|
||||
'Hybrid Fusion CV': hyb_cv,
|
||||
}
|
||||
for n, a in sorted(res.items(), key=lambda x:-x[1]):
|
||||
mk = "🏆" if a>=0.95 else "✅" if a>=0.90 else "📈"
|
||||
print(f" {mk} {n}: {a:.4f}")
|
||||
|
||||
best = max(res.values())
|
||||
if best >= 0.95:
|
||||
print(f"\n🎉 THÀNH CÔNG VƯỢT MỐC 95%! BEST: {best:.4f}")
|
||||
else:
|
||||
print(f"\n🏆 BEST: {best:.4f}")
|
||||
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
with open('model_train/ultimate_v4_fusion_results.json', 'w') as f:
|
||||
json.dump({k:float(v) for k,v in res.items()}, f, indent=2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,276 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
import numpy as np
|
||||
import joblib
|
||||
import os
|
||||
import json
|
||||
from sklearn.model_selection import StratifiedKFold, train_test_split
|
||||
from sklearn.metrics import accuracy_score
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from xgboost import XGBClassifier
|
||||
from lightgbm import LGBMClassifier
|
||||
from sklearn.ensemble import ExtraTreesClassifier, VotingClassifier
|
||||
from scipy.ndimage import uniform_filter
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
def load_and_clean():
|
||||
data = joblib.load('dataset_cache/training_data_fusion_32ch.joblib')
|
||||
X, y = data['X'].astype(np.float32), data['y']
|
||||
|
||||
valid = (y >= 0) & (X.reshape(X.shape[0], -1).sum(1) != 0)
|
||||
X, y = X[valid], y[valid]
|
||||
|
||||
unique = sorted(np.unique(y).tolist())
|
||||
lmap = {l:i for i,l in enumerate(unique)}
|
||||
y = np.array([lmap[l] for l in y])
|
||||
return X, y, len(unique)
|
||||
|
||||
def extract_features_fusion(X):
|
||||
N = X.shape[0]
|
||||
all_feats = []
|
||||
|
||||
for i in range(N):
|
||||
patch = X[i]
|
||||
feats = []
|
||||
|
||||
valid_ts = []
|
||||
for t in range(4):
|
||||
block_s2 = patch[t*8 : t*8+6]
|
||||
if np.abs(block_s2).sum() > 1e-6:
|
||||
valid_ts.append(t)
|
||||
|
||||
if not valid_ts:
|
||||
valid_ts = [0]
|
||||
|
||||
per_ts_stats_s2 = {b: [] for b in range(6)}
|
||||
for t in valid_ts:
|
||||
for b in range(6):
|
||||
ch = patch[t*8 + b]
|
||||
per_ts_stats_s2[b].append([
|
||||
np.mean(ch), np.std(ch), np.median(ch),
|
||||
np.min(ch), np.max(ch),
|
||||
np.percentile(ch, 10), np.percentile(ch, 90),
|
||||
])
|
||||
|
||||
for b in range(6):
|
||||
stats = np.array(per_ts_stats_s2[b])
|
||||
feats.extend(stats.mean(axis=0).tolist())
|
||||
feats.extend(stats.std(axis=0).tolist())
|
||||
|
||||
per_ts_stats_s1 = {b: [] for b in range(2)}
|
||||
for t in range(4):
|
||||
vv = patch[t*8 + 6]
|
||||
vh = patch[t*8 + 7]
|
||||
if np.abs(vv).sum() > 1e-6:
|
||||
per_ts_stats_s1[0].append([
|
||||
np.mean(vv), np.std(vv), np.median(vv), np.max(vv), np.percentile(vv, 90)
|
||||
])
|
||||
per_ts_stats_s1[1].append([
|
||||
np.mean(vh), np.std(vh), np.median(vh), np.max(vh), np.percentile(vh, 90)
|
||||
])
|
||||
ratio = (vh + 1e-6) / (vv + 1e-6)
|
||||
feats.extend([np.mean(ratio), np.std(ratio), np.median(ratio)])
|
||||
else:
|
||||
feats.extend([0.0] * 3)
|
||||
|
||||
for b in range(2):
|
||||
if len(per_ts_stats_s1[b]) > 0:
|
||||
stats = np.array(per_ts_stats_s1[b])
|
||||
feats.extend(stats.mean(axis=0).tolist())
|
||||
feats.extend(stats.std(axis=0).tolist())
|
||||
else:
|
||||
feats.extend([0.0] * 10)
|
||||
|
||||
for t in valid_ts[:2]:
|
||||
for b_idx in [3, 4]:
|
||||
ch = patch[t*8 + b_idx]
|
||||
gx = np.diff(ch, axis=1); gy = np.diff(ch, axis=0)
|
||||
grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2))
|
||||
lm = uniform_filter(ch, size=3); lv = uniform_filter(ch**2, size=3) - lm**2
|
||||
feats.extend([grad_mag, np.mean(lv), np.std(lv)])
|
||||
|
||||
for b_idx in [6, 7]:
|
||||
ch = patch[0*8 + b_idx]
|
||||
gx = np.diff(ch, axis=1); gy = np.diff(ch, axis=0)
|
||||
grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2))
|
||||
lm = uniform_filter(ch, size=3); lv = uniform_filter(ch**2, size=3) - lm**2
|
||||
feats.extend([grad_mag, np.mean(lv), np.std(lv)])
|
||||
|
||||
needed = 2 * 2 * 3
|
||||
got = min(len(valid_ts), 2) * 2 * 3
|
||||
feats.extend([0.0] * (needed - got))
|
||||
|
||||
best_t = valid_ts[0]
|
||||
for b in range(8):
|
||||
ch = patch[best_t*8 + b]
|
||||
feats.extend(ch.flatten().tolist())
|
||||
|
||||
all_feats.append(feats)
|
||||
|
||||
features = np.array(all_feats, dtype=np.float32)
|
||||
features = np.nan_to_num(features, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
return features
|
||||
|
||||
class LightCNN_32ch(nn.Module):
|
||||
def __init__(self, in_ch=32, n_cls=7, width=96):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
nn.Conv2d(in_ch, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.Conv2d(width, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.05),
|
||||
nn.Conv2d(width, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.Conv2d(width*2, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.1),
|
||||
nn.Conv2d(width*2, width*4, 3, padding=1), nn.BatchNorm2d(width*4), nn.GELU(),
|
||||
nn.AdaptiveAvgPool2d(1), nn.Flatten(),
|
||||
)
|
||||
self.head = nn.Sequential(
|
||||
nn.Linear(width*4, width*2), nn.GELU(), nn.Dropout(0.3),
|
||||
nn.Linear(width*2, n_cls)
|
||||
)
|
||||
def forward(self, x): return self.head(self.net(x))
|
||||
def embed(self, x): return self.net(x)
|
||||
|
||||
def train_cnn_fusion(X, y, n_cls, seed=42):
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
torch.manual_seed(seed)
|
||||
np.random.seed(seed)
|
||||
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y)
|
||||
|
||||
model = LightCNN_32ch(in_ch=32, n_cls=n_cls, width=128).to(device)
|
||||
cc = np.bincount(y_tr, minlength=n_cls)
|
||||
w = torch.FloatTensor((1.0/(cc+1)) / (1.0/(cc+1)).sum() * n_cls).to(device)
|
||||
|
||||
crit = nn.CrossEntropyLoss(weight=w, label_smoothing=0.1)
|
||||
opt = optim.AdamW(model.parameters(), lr=4e-4, weight_decay=0.02)
|
||||
sched = optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=40, T_mult=2, eta_min=1e-6)
|
||||
|
||||
tr_t = torch.FloatTensor(X_tr)
|
||||
tr_y = torch.LongTensor(y_tr)
|
||||
te_t = torch.FloatTensor(X_te).to(device)
|
||||
|
||||
best_acc, best_state, pat = 0, None, 0
|
||||
for ep in range(300):
|
||||
model.train()
|
||||
perm = torch.randperm(len(tr_t))
|
||||
for i in range(0, len(tr_t), 32):
|
||||
idx = perm[i:i+32]
|
||||
bx = tr_t[idx].to(device)
|
||||
by = tr_y[idx].to(device)
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [2])
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [3])
|
||||
if np.random.random() > 0.5: bx = torch.rot90(bx, np.random.randint(1,4), [2,3])
|
||||
bx = bx + torch.randn_like(bx) * 0.02
|
||||
|
||||
if np.random.random() > 0.5 and len(bx) > 1:
|
||||
lam = np.random.beta(0.4, 0.4)
|
||||
i2 = torch.randperm(bx.size(0))
|
||||
bx = lam*bx + (1-lam)*bx[i2]
|
||||
oh1 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by.unsqueeze(1), 1)
|
||||
oh2 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by[i2].unsqueeze(1), 1)
|
||||
out = model(bx)
|
||||
loss = (-(lam*oh1 + (1-lam)*oh2) * torch.log_softmax(out,1)).sum(1).mean()
|
||||
else:
|
||||
loss = crit(model(bx), by)
|
||||
|
||||
opt.zero_grad(); loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
opt.step()
|
||||
sched.step()
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
probs = []
|
||||
for fn in [lambda x:x, lambda x:torch.flip(x,[2]), lambda x:torch.flip(x,[3])]:
|
||||
probs.append(torch.softmax(model(fn(te_t)), 1))
|
||||
avg = torch.stack(probs).mean(0)
|
||||
preds = avg.argmax(1).cpu().numpy()
|
||||
|
||||
acc = accuracy_score(y_te, preds)
|
||||
if acc > best_acc:
|
||||
best_acc = acc; best_state = {k:v.cpu().clone() for k,v in model.state_dict().items()}; pat = 0
|
||||
else:
|
||||
pat += 1
|
||||
if pat >= 60: break
|
||||
|
||||
model.load_state_dict(best_state)
|
||||
return model, best_acc
|
||||
|
||||
def train_hybrid_fusion(X, y, cnn_model, n_cls):
|
||||
print("\n" + "="*60)
|
||||
print("HYBRID FUSION ENSEMBLE: CNN embed + S1/S2 Rich features + XGB/LGBM/ETC")
|
||||
print("="*60)
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
cnn_model = cnn_model.to(device).eval()
|
||||
|
||||
with torch.no_grad():
|
||||
embs = []
|
||||
for i in range(0, len(X), 64):
|
||||
b = torch.FloatTensor(X[i:i+64]).to(device)
|
||||
embs.append(cnn_model.embed(b).cpu().numpy())
|
||||
cnn_feat = np.concatenate(embs)
|
||||
|
||||
rich = extract_features_fusion(X)
|
||||
combined = np.concatenate([cnn_feat, rich], axis=1)
|
||||
print(f" Final Feature Vector: {combined.shape}")
|
||||
|
||||
scaler = StandardScaler()
|
||||
combined = scaler.fit_transform(combined)
|
||||
|
||||
skf = StratifiedKFold(5, shuffle=True, random_state=42)
|
||||
cv_accs = []
|
||||
|
||||
for fold, (ti, vi) in enumerate(skf.split(combined, y)):
|
||||
xgb = XGBClassifier(n_estimators=1000, max_depth=7, learning_rate=0.03,
|
||||
subsample=0.8, colsample_bytree=0.5,
|
||||
tree_method='hist', device='cuda',
|
||||
random_state=42+fold, use_label_encoder=False, eval_metric='mlogloss')
|
||||
|
||||
lgbm = LGBMClassifier(n_estimators=1000, max_depth=7, learning_rate=0.03,
|
||||
subsample=0.8, colsample_bytree=0.5,
|
||||
random_state=42+fold, verbosity=-1)
|
||||
|
||||
etc = ExtraTreesClassifier(n_estimators=1000, max_depth=15,
|
||||
max_features='sqrt', random_state=42+fold, n_jobs=-1)
|
||||
|
||||
ensemble = VotingClassifier(estimators=[
|
||||
('xgb', xgb), ('lgbm', lgbm), ('etc', etc)
|
||||
], voting='soft')
|
||||
|
||||
ensemble.fit(combined[ti], y[ti])
|
||||
a = accuracy_score(y[vi], ensemble.predict(combined[vi]))
|
||||
cv_accs.append(a)
|
||||
print(f" Fold {fold+1}: {a:.4f}")
|
||||
|
||||
cv_mean = np.mean(cv_accs)
|
||||
print(f" ✅ Ensemble CV Mean: {cv_mean:.4f} ± {np.std(cv_accs):.4f}")
|
||||
return cv_mean
|
||||
|
||||
def main():
|
||||
X, y, n_cls = load_and_clean()
|
||||
cnn_model, cnn_acc = train_cnn_fusion(X, y, n_cls)
|
||||
|
||||
hyb_cv = train_hybrid_fusion(X, y, cnn_model, n_cls)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 FINAL RESULTS V5 (ENSEMBLE + RADAR)")
|
||||
print("="*60)
|
||||
res = {
|
||||
'Hybrid Fusion Ensemble CV': hyb_cv,
|
||||
}
|
||||
for n, a in sorted(res.items(), key=lambda x:-x[1]):
|
||||
mk = "🏆" if a>=0.95 else "✅" if a>=0.90 else "📈"
|
||||
print(f" {mk} {n}: {a:.4f}")
|
||||
|
||||
best = max(res.values())
|
||||
if best >= 0.95:
|
||||
print(f"\n🎉 THÀNH CÔNG VƯỢT MỐC 95%! BEST: {best:.4f}")
|
||||
else:
|
||||
print(f"\n🏆 BEST: {best:.4f}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
V6: Exhaustive Hyperparameter Tuning for Maximum Accuracy
|
||||
- Multi-seed CNN ensembles for better embeddings
|
||||
- Optuna-style manual grid search on XGBoost/LightGBM/ExtraTrees
|
||||
- Stacking instead of simple Voting
|
||||
- Feature selection to remove noise
|
||||
"""
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
import numpy as np
|
||||
import joblib
|
||||
import os
|
||||
import json
|
||||
from sklearn.model_selection import StratifiedKFold, train_test_split, RepeatedStratifiedKFold
|
||||
from sklearn.metrics import accuracy_score
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.feature_selection import SelectKBest, f_classif
|
||||
from xgboost import XGBClassifier
|
||||
from lightgbm import LGBMClassifier
|
||||
from sklearn.ensemble import ExtraTreesClassifier, StackingClassifier, RandomForestClassifier, GradientBoostingClassifier
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from scipy.ndimage import uniform_filter
|
||||
import itertools
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
def load_and_clean():
|
||||
data = joblib.load('dataset_cache/training_data_fusion_32ch.joblib')
|
||||
X, y = data['X'].astype(np.float32), data['y']
|
||||
valid = (y >= 0) & (X.reshape(X.shape[0], -1).sum(1) != 0)
|
||||
X, y = X[valid], y[valid]
|
||||
unique = sorted(np.unique(y).tolist())
|
||||
lmap = {l:i for i,l in enumerate(unique)}
|
||||
y = np.array([lmap[l] for l in y])
|
||||
print(f"Data: {X.shape}, {len(unique)} classes, dist={[int((y==i).sum()) for i in range(len(unique))]}")
|
||||
return X, y, len(unique)
|
||||
|
||||
def extract_features_fusion(X):
|
||||
N = X.shape[0]
|
||||
all_feats = []
|
||||
for i in range(N):
|
||||
patch = X[i]
|
||||
feats = []
|
||||
valid_ts = [t for t in range(4) if np.abs(patch[t*8:t*8+6]).sum() > 1e-6]
|
||||
if not valid_ts: valid_ts = [0]
|
||||
|
||||
# S2 per-band stats
|
||||
for t in valid_ts:
|
||||
for b in range(6):
|
||||
ch = patch[t*8 + b]
|
||||
feats.extend([np.mean(ch), np.std(ch), np.median(ch), np.min(ch), np.max(ch),
|
||||
np.percentile(ch, 10), np.percentile(ch, 25), np.percentile(ch, 75), np.percentile(ch, 90),
|
||||
np.mean(ch > np.mean(ch))])
|
||||
# Pad to fixed length (4 timesteps * 6 bands * 10 stats = 240)
|
||||
needed = 4 * 6 * 10
|
||||
feats.extend([0.0] * (needed - len(feats)))
|
||||
|
||||
# S1 per-band stats + ratios
|
||||
for t in range(4):
|
||||
vv, vh = patch[t*8+6], patch[t*8+7]
|
||||
if np.abs(vv).sum() > 1e-6:
|
||||
ratio = (vh+1e-6)/(vv+1e-6)
|
||||
diff = vv - vh
|
||||
feats.extend([np.mean(vv), np.std(vv), np.median(vv), np.max(vv), np.percentile(vv, 90),
|
||||
np.mean(vh), np.std(vh), np.median(vh), np.max(vh), np.percentile(vh, 90),
|
||||
np.mean(ratio), np.std(ratio), np.median(ratio), np.min(ratio), np.max(ratio),
|
||||
np.mean(diff), np.std(diff)])
|
||||
else:
|
||||
feats.extend([0.0] * 17)
|
||||
|
||||
# Temporal variance (S2)
|
||||
for b in range(6):
|
||||
ts_means = [np.mean(patch[t*8+b]) for t in valid_ts]
|
||||
feats.extend([np.std(ts_means) if len(ts_means) > 1 else 0.0,
|
||||
np.max(ts_means) - np.min(ts_means) if len(ts_means) > 1 else 0.0])
|
||||
|
||||
# Temporal variance (S1)
|
||||
for b_offset in [6, 7]:
|
||||
ts_means = [np.mean(patch[t*8+b_offset]) for t in range(4) if np.abs(patch[t*8+b_offset]).sum() > 1e-6]
|
||||
feats.extend([np.std(ts_means) if len(ts_means) > 1 else 0.0,
|
||||
np.max(ts_means) - np.min(ts_means) if len(ts_means) > 1 else 0.0])
|
||||
|
||||
# Spatial texture
|
||||
for t in valid_ts[:2]:
|
||||
for b_idx in [3, 4, 6, 7]: # NIR, NDVI, VV, VH
|
||||
ch = patch[t*8 + b_idx] if b_idx < 6 else patch[valid_ts[0]*8 + b_idx]
|
||||
gx = np.diff(ch, axis=1); gy = np.diff(ch, axis=0)
|
||||
grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2))
|
||||
lm = uniform_filter(ch, size=3); lv = uniform_filter(ch**2, size=3) - lm**2
|
||||
entropy_approx = -np.mean(np.abs(lv) * np.log(np.abs(lv) + 1e-10))
|
||||
feats.extend([grad_mag, np.mean(lv), np.std(lv), entropy_approx])
|
||||
needed_tex = 2 * 4 * 4
|
||||
got_tex = min(len(valid_ts), 2) * 4 * 4
|
||||
feats.extend([0.0] * (needed_tex - got_tex))
|
||||
|
||||
# Flat pixels from best timestep
|
||||
best_t = valid_ts[0]
|
||||
for b in range(8):
|
||||
ch = patch[best_t*8 + b]
|
||||
feats.extend(ch.flatten().tolist())
|
||||
|
||||
all_feats.append(feats)
|
||||
features = np.array(all_feats, dtype=np.float32)
|
||||
features = np.nan_to_num(features, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
return features
|
||||
|
||||
class LightCNN_32ch(nn.Module):
|
||||
def __init__(self, in_ch=32, n_cls=7, width=96):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
nn.Conv2d(in_ch, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.Conv2d(width, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.05),
|
||||
nn.Conv2d(width, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.Conv2d(width*2, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(),
|
||||
nn.MaxPool2d(2), nn.Dropout2d(0.1),
|
||||
nn.Conv2d(width*2, width*4, 3, padding=1), nn.BatchNorm2d(width*4), nn.GELU(),
|
||||
nn.AdaptiveAvgPool2d(1), nn.Flatten(),
|
||||
)
|
||||
self.head = nn.Sequential(
|
||||
nn.Linear(width*4, width*2), nn.GELU(), nn.Dropout(0.3),
|
||||
nn.Linear(width*2, n_cls)
|
||||
)
|
||||
def forward(self, x): return self.head(self.net(x))
|
||||
def embed(self, x): return self.net(x)
|
||||
|
||||
def train_cnn(X, y, n_cls, seed=42, width=128, epochs=300):
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
torch.manual_seed(seed); np.random.seed(seed)
|
||||
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y)
|
||||
model = LightCNN_32ch(in_ch=32, n_cls=n_cls, width=width).to(device)
|
||||
cc = np.bincount(y_tr, minlength=n_cls)
|
||||
w = torch.FloatTensor((1.0/(cc+1)) / (1.0/(cc+1)).sum() * n_cls).to(device)
|
||||
crit = nn.CrossEntropyLoss(weight=w, label_smoothing=0.1)
|
||||
opt = optim.AdamW(model.parameters(), lr=4e-4, weight_decay=0.02)
|
||||
sched = optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=40, T_mult=2, eta_min=1e-6)
|
||||
tr_t = torch.FloatTensor(X_tr); tr_y = torch.LongTensor(y_tr)
|
||||
te_t = torch.FloatTensor(X_te).to(device)
|
||||
best_acc, best_state, pat = 0, None, 0
|
||||
for ep in range(epochs):
|
||||
model.train()
|
||||
perm = torch.randperm(len(tr_t))
|
||||
for i in range(0, len(tr_t), 32):
|
||||
idx = perm[i:i+32]
|
||||
bx, by = tr_t[idx].to(device), tr_y[idx].to(device)
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [2])
|
||||
if np.random.random() > 0.5: bx = torch.flip(bx, [3])
|
||||
if np.random.random() > 0.5: bx = torch.rot90(bx, np.random.randint(1,4), [2,3])
|
||||
bx = bx + torch.randn_like(bx) * 0.02
|
||||
if np.random.random() > 0.5 and len(bx) > 1:
|
||||
lam = np.random.beta(0.4, 0.4)
|
||||
i2 = torch.randperm(bx.size(0))
|
||||
bx = lam*bx + (1-lam)*bx[i2]
|
||||
oh1 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by.unsqueeze(1), 1)
|
||||
oh2 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by[i2].unsqueeze(1), 1)
|
||||
loss = (-(lam*oh1 + (1-lam)*oh2) * torch.log_softmax(model(bx),1)).sum(1).mean()
|
||||
else:
|
||||
loss = crit(model(bx), by)
|
||||
opt.zero_grad(); loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
opt.step()
|
||||
sched.step()
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
probs = [torch.softmax(model(fn(te_t)), 1) for fn in [lambda x:x, lambda x:torch.flip(x,[2]), lambda x:torch.flip(x,[3])]]
|
||||
preds = torch.stack(probs).mean(0).argmax(1).cpu().numpy()
|
||||
acc = accuracy_score(y_te, preds)
|
||||
if acc > best_acc: best_acc = acc; best_state = {k:v.cpu().clone() for k,v in model.state_dict().items()}; pat = 0
|
||||
else: pat += 1
|
||||
if pat >= 60: break
|
||||
model.load_state_dict(best_state)
|
||||
return model, best_acc
|
||||
|
||||
def get_cnn_embeddings(X, models, device):
|
||||
all_embs = []
|
||||
for model in models:
|
||||
model = model.to(device).eval()
|
||||
with torch.no_grad():
|
||||
embs = []
|
||||
for i in range(0, len(X), 64):
|
||||
b = torch.FloatTensor(X[i:i+64]).to(device)
|
||||
embs.append(model.embed(b).cpu().numpy())
|
||||
all_embs.append(np.concatenate(embs))
|
||||
return np.concatenate(all_embs, axis=1)
|
||||
|
||||
def run_hyperparameter_search(combined, y, n_cls):
|
||||
print("\n" + "="*60)
|
||||
print("🔬 EXHAUSTIVE HYPERPARAMETER SEARCH")
|
||||
print("="*60)
|
||||
|
||||
scaler = StandardScaler()
|
||||
combined_scaled = scaler.fit_transform(combined)
|
||||
|
||||
skf = StratifiedKFold(5, shuffle=True, random_state=42)
|
||||
|
||||
# ===== CONFIG SPACE =====
|
||||
configs = [
|
||||
# Config 1: XGB Deep trees
|
||||
{"name": "XGB-deep", "model": lambda: XGBClassifier(
|
||||
n_estimators=2000, max_depth=9, learning_rate=0.01, subsample=0.75, colsample_bytree=0.4,
|
||||
min_child_weight=2, gamma=0.1, reg_alpha=0.5, reg_lambda=1.5,
|
||||
tree_method='hist', device='cuda', random_state=42, use_label_encoder=False, eval_metric='mlogloss')},
|
||||
# Config 2: XGB Shallow wide
|
||||
{"name": "XGB-shallow", "model": lambda: XGBClassifier(
|
||||
n_estimators=3000, max_depth=5, learning_rate=0.008, subsample=0.85, colsample_bytree=0.35,
|
||||
min_child_weight=5, gamma=0.2, reg_alpha=1.0, reg_lambda=2.0,
|
||||
tree_method='hist', device='cuda', random_state=42, use_label_encoder=False, eval_metric='mlogloss')},
|
||||
# Config 3: XGB Balanced
|
||||
{"name": "XGB-balanced", "model": lambda: XGBClassifier(
|
||||
n_estimators=2500, max_depth=7, learning_rate=0.015, subsample=0.8, colsample_bytree=0.45,
|
||||
min_child_weight=3, gamma=0.05, reg_alpha=0.3, reg_lambda=1.0,
|
||||
tree_method='hist', device='cuda', random_state=42, use_label_encoder=False, eval_metric='mlogloss')},
|
||||
# Config 4: LGBM Tuned
|
||||
{"name": "LGBM-tuned", "model": lambda: LGBMClassifier(
|
||||
n_estimators=2000, max_depth=8, learning_rate=0.015, subsample=0.8, colsample_bytree=0.45,
|
||||
min_child_samples=5, reg_alpha=0.5, reg_lambda=1.0, num_leaves=63,
|
||||
random_state=42, verbosity=-1)},
|
||||
# Config 5: LGBM Conservative
|
||||
{"name": "LGBM-conservative", "model": lambda: LGBMClassifier(
|
||||
n_estimators=3000, max_depth=6, learning_rate=0.008, subsample=0.75, colsample_bytree=0.35,
|
||||
min_child_samples=10, reg_alpha=1.0, reg_lambda=2.0, num_leaves=31,
|
||||
random_state=42, verbosity=-1)},
|
||||
# Config 6: ExtraTrees Deep
|
||||
{"name": "ETC-deep", "model": lambda: ExtraTreesClassifier(
|
||||
n_estimators=2000, max_depth=20, max_features='sqrt', min_samples_leaf=2,
|
||||
random_state=42, n_jobs=-1)},
|
||||
# Config 7: RandomForest
|
||||
{"name": "RF-tuned", "model": lambda: RandomForestClassifier(
|
||||
n_estimators=2000, max_depth=15, max_features='sqrt', min_samples_leaf=3,
|
||||
random_state=42, n_jobs=-1)},
|
||||
# Config 8: GradientBoosting (sklearn)
|
||||
{"name": "GBT-sklearn", "model": lambda: GradientBoostingClassifier(
|
||||
n_estimators=500, max_depth=5, learning_rate=0.05, subsample=0.8,
|
||||
min_samples_leaf=5, random_state=42)},
|
||||
]
|
||||
|
||||
results = {}
|
||||
for cfg in configs:
|
||||
cv_accs = []
|
||||
for fold, (ti, vi) in enumerate(skf.split(combined_scaled, y)):
|
||||
m = cfg["model"]()
|
||||
if hasattr(m, 'eval_set'):
|
||||
m.fit(combined_scaled[ti], y[ti], eval_set=[(combined_scaled[vi], y[vi])], verbose=False)
|
||||
else:
|
||||
m.fit(combined_scaled[ti], y[ti])
|
||||
a = accuracy_score(y[vi], m.predict(combined_scaled[vi]))
|
||||
cv_accs.append(a)
|
||||
mean_acc = np.mean(cv_accs)
|
||||
results[cfg["name"]] = (mean_acc, np.std(cv_accs), cv_accs)
|
||||
mk = "🏆" if mean_acc >= 0.95 else "✅" if mean_acc >= 0.93 else "📈"
|
||||
print(f" {mk} {cfg['name']}: {mean_acc:.4f} ± {np.std(cv_accs):.4f} (folds: {[f'{a:.3f}' for a in cv_accs]})")
|
||||
|
||||
# ===== STACKING ENSEMBLE =====
|
||||
print("\n--- Stacking Ensemble ---")
|
||||
|
||||
best_3 = sorted(results.items(), key=lambda x: -x[1][0])[:3]
|
||||
print(f" Top-3 base models: {[b[0] for b in best_3]}")
|
||||
|
||||
# Build stacking with top models
|
||||
base_estimators = []
|
||||
for cfg in configs:
|
||||
if cfg["name"] in [b[0] for b in best_3]:
|
||||
base_estimators.append((cfg["name"], cfg["model"]()))
|
||||
|
||||
stacking_configs = [
|
||||
{"name": "Stack-LR", "meta": LogisticRegression(C=1.0, max_iter=1000, random_state=42)},
|
||||
{"name": "Stack-XGB", "meta": XGBClassifier(n_estimators=200, max_depth=3, learning_rate=0.1,
|
||||
tree_method='hist', device='cuda', random_state=42,
|
||||
use_label_encoder=False, eval_metric='mlogloss')},
|
||||
]
|
||||
|
||||
for scfg in stacking_configs:
|
||||
stack = StackingClassifier(estimators=base_estimators, final_estimator=scfg["meta"],
|
||||
cv=3, stack_method='predict_proba', n_jobs=-1)
|
||||
cv_accs = []
|
||||
for fold, (ti, vi) in enumerate(skf.split(combined_scaled, y)):
|
||||
stack_clone = StackingClassifier(estimators=[(n, cfg["model"]()) for cfg in configs for n in [cfg["name"]] if n in [b[0] for b in best_3]],
|
||||
final_estimator=scfg["meta"], cv=3, stack_method='predict_proba', n_jobs=-1)
|
||||
stack_clone.fit(combined_scaled[ti], y[ti])
|
||||
a = accuracy_score(y[vi], stack_clone.predict(combined_scaled[vi]))
|
||||
cv_accs.append(a)
|
||||
mean_acc = np.mean(cv_accs)
|
||||
results[scfg["name"]] = (mean_acc, np.std(cv_accs), cv_accs)
|
||||
mk = "🏆" if mean_acc >= 0.95 else "✅" if mean_acc >= 0.93 else "📈"
|
||||
print(f" {mk} {scfg['name']}: {mean_acc:.4f} ± {np.std(cv_accs):.4f} (folds: {[f'{a:.3f}' for a in cv_accs]})")
|
||||
|
||||
# ===== FEATURE SELECTION + BEST MODEL =====
|
||||
print("\n--- Feature Selection ---")
|
||||
for k_feat in [500, 800, 1200, 1500, 2000]:
|
||||
selector = SelectKBest(f_classif, k=min(k_feat, combined_scaled.shape[1]))
|
||||
X_sel = selector.fit_transform(combined_scaled, y)
|
||||
cv_accs = []
|
||||
for fold, (ti, vi) in enumerate(skf.split(X_sel, y)):
|
||||
m = XGBClassifier(n_estimators=2500, max_depth=7, learning_rate=0.015, subsample=0.8, colsample_bytree=0.45,
|
||||
min_child_weight=3, gamma=0.05, reg_alpha=0.3, reg_lambda=1.0,
|
||||
tree_method='hist', device='cuda', random_state=42, use_label_encoder=False, eval_metric='mlogloss')
|
||||
m.fit(X_sel[ti], y[ti])
|
||||
a = accuracy_score(y[vi], m.predict(X_sel[vi]))
|
||||
cv_accs.append(a)
|
||||
mean_acc = np.mean(cv_accs)
|
||||
mk = "🏆" if mean_acc >= 0.95 else "✅" if mean_acc >= 0.93 else "📈"
|
||||
print(f" {mk} XGB k={k_feat}: {mean_acc:.4f} ± {np.std(cv_accs):.4f} (folds: {[f'{a:.3f}' for a in cv_accs]})")
|
||||
results[f"XGB-feat{k_feat}"] = (mean_acc, np.std(cv_accs), cv_accs)
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
print("🚀 V6: EXHAUSTIVE HYPERPARAMETER TUNING")
|
||||
print("="*60)
|
||||
|
||||
X, y, n_cls = load_and_clean()
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
|
||||
# Train multi-seed CNN ensemble for richer embeddings
|
||||
print("\n--- Training Multi-Seed CNN Ensemble ---")
|
||||
models = []
|
||||
for seed in [42, 123, 777]:
|
||||
m, acc = train_cnn(X, y, n_cls, seed=seed, width=128)
|
||||
print(f" Seed {seed}: CNN Acc = {acc:.4f}")
|
||||
models.append(m)
|
||||
|
||||
# Get combined embeddings from all CNN seeds
|
||||
cnn_feat = get_cnn_embeddings(X, models, device)
|
||||
print(f" Multi-seed CNN embedding: {cnn_feat.shape}")
|
||||
|
||||
rich = extract_features_fusion(X)
|
||||
combined = np.concatenate([cnn_feat, rich], axis=1)
|
||||
print(f" Total features: {combined.shape}")
|
||||
|
||||
results = run_hyperparameter_search(combined, y, n_cls)
|
||||
|
||||
# Final summary
|
||||
print("\n" + "="*60)
|
||||
print("📊 LEADERBOARD")
|
||||
print("="*60)
|
||||
sorted_results = sorted(results.items(), key=lambda x: -x[1][0])
|
||||
for rank, (name, (mean, std, folds)) in enumerate(sorted_results, 1):
|
||||
mk = "🏆" if mean >= 0.95 else "✅" if mean >= 0.93 else "📈"
|
||||
print(f" #{rank} {mk} {name}: {mean:.4f} ± {std:.4f}")
|
||||
|
||||
best_name, (best_mean, best_std, best_folds) = sorted_results[0]
|
||||
print(f"\n🏆 CHAMPION: {best_name} = {best_mean:.4f}")
|
||||
if best_mean >= 0.95:
|
||||
print("🎉 VƯỢT MỐC 95%!")
|
||||
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
with open('model_train/v6_tuning_results.json', 'w') as f:
|
||||
json.dump({k: {"mean": float(v[0]), "std": float(v[1]), "folds": [float(x) for x in v[2]]} for k, v in results.items()}, f, indent=2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,177 @@
|
||||
import os
|
||||
import glob
|
||||
import time
|
||||
import json
|
||||
import itertools
|
||||
import numpy as np
|
||||
import joblib
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import TensorDataset, DataLoader
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import classification_report, accuracy_score
|
||||
|
||||
from train_module import SwinUNetClassifier
|
||||
|
||||
def load_data():
|
||||
cache_files = glob.glob('dataset_cache/training_data_*.joblib')
|
||||
if not cache_files:
|
||||
raise FileNotFoundError("No cache files found in dataset_cache/")
|
||||
|
||||
# Get the latest cache file
|
||||
cache_file = max(cache_files, key=os.path.getctime)
|
||||
print(f"Loading data from {cache_file}...")
|
||||
data = joblib.load(cache_file)
|
||||
features = data['features']
|
||||
labels = data['labels']
|
||||
|
||||
# Map labels to 0..N-1
|
||||
unique_labels = sorted(list(np.unique(labels)))
|
||||
label_map = {lbl: idx for idx, lbl in enumerate(unique_labels)}
|
||||
mapped_labels = np.array([label_map[l] for l in labels])
|
||||
|
||||
return features, mapped_labels, unique_labels
|
||||
|
||||
def train_evaluate(features, labels, embed_dim, lr, weight_decay, epochs, patience, device):
|
||||
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.2, random_state=42)
|
||||
|
||||
n_features = X_train.shape[1]
|
||||
n_classes = len(np.unique(labels))
|
||||
|
||||
model = SwinUNetClassifier(n_features, n_classes, embed_dim=embed_dim).to(device)
|
||||
|
||||
X_train_t = torch.FloatTensor(X_train)
|
||||
y_train_t = torch.LongTensor(y_train)
|
||||
X_test_t = torch.FloatTensor(X_test)
|
||||
y_test_t = torch.LongTensor(y_test)
|
||||
|
||||
train_dataset = TensorDataset(X_train_t, y_train_t)
|
||||
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
|
||||
|
||||
# Class weights
|
||||
class_counts = np.bincount(y_train)
|
||||
class_weights = 1.0 / (class_counts + 1e-6)
|
||||
class_weights = class_weights / class_weights.sum() * len(class_counts)
|
||||
class_weights_t = torch.FloatTensor(class_weights).to(device)
|
||||
|
||||
criterion = nn.CrossEntropyLoss(weight=class_weights_t)
|
||||
optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
|
||||
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
|
||||
|
||||
best_acc = 0.0
|
||||
patience_counter = 0
|
||||
best_model_state = None
|
||||
|
||||
model.train()
|
||||
for epoch in range(epochs):
|
||||
model.train()
|
||||
for batch_X, batch_y in train_loader:
|
||||
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
|
||||
optimizer.zero_grad()
|
||||
outputs = model(batch_X)
|
||||
loss = criterion(outputs, batch_y)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
scheduler.step()
|
||||
|
||||
# Eval
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
outputs = model(X_test_t.to(device))
|
||||
_, preds = torch.max(outputs, 1)
|
||||
acc = accuracy_score(y_test, preds.cpu().numpy())
|
||||
|
||||
if acc > best_acc:
|
||||
best_acc = acc
|
||||
best_model_state = model.state_dict()
|
||||
patience_counter = 0
|
||||
else:
|
||||
patience_counter += 1
|
||||
|
||||
if patience_counter >= patience:
|
||||
break
|
||||
|
||||
# Restore best
|
||||
if best_model_state:
|
||||
model.load_state_dict(best_model_state)
|
||||
|
||||
return model, best_acc, X_test_t, y_test
|
||||
|
||||
def main():
|
||||
print("🚀 BẮT ĐẦU TÌM KIẾM SIÊU THAM SỐ CHO SWIN-UNET")
|
||||
features, labels, unique_labels = load_data()
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
print(f"Using device: {device}")
|
||||
|
||||
param_grid = {
|
||||
'embed_dim': [64, 128, 256, 512],
|
||||
'lr': [1e-3, 5e-4, 1e-4],
|
||||
'weight_decay': [0.01, 0.001],
|
||||
'epochs': [200, 500]
|
||||
}
|
||||
|
||||
keys = param_grid.keys()
|
||||
combinations = list(itertools.product(*(param_grid[k] for k in keys)))
|
||||
|
||||
best_global_acc = 0.0
|
||||
best_params = None
|
||||
best_model = None
|
||||
|
||||
# Ensure dir exists
|
||||
os.makedirs('land_classification_model', exist_ok=True)
|
||||
os.makedirs('model_train', exist_ok=True)
|
||||
|
||||
for i, values in enumerate(combinations):
|
||||
params = dict(zip(keys, values))
|
||||
print(f"\n[{i+1}/{len(combinations)}] Training with params: {params}")
|
||||
|
||||
model, acc, X_test_t, y_test = train_evaluate(
|
||||
features, labels,
|
||||
embed_dim=params['embed_dim'],
|
||||
lr=params['lr'],
|
||||
weight_decay=params['weight_decay'],
|
||||
epochs=params['epochs'],
|
||||
patience=30,
|
||||
device=device
|
||||
)
|
||||
|
||||
print(f"Test Accuracy: {acc:.4f}")
|
||||
|
||||
if acc > best_global_acc:
|
||||
best_global_acc = acc
|
||||
best_params = params
|
||||
best_model = model
|
||||
|
||||
print(f"🌟 NEW BEST ACCURACY: {acc:.4f}")
|
||||
|
||||
if acc >= 0.95:
|
||||
print("🎯 ĐẠT MỤC TIÊU >95%! DỪNG TÌM KIẾM.")
|
||||
break
|
||||
|
||||
if best_model is not None:
|
||||
model_path = 'land_classification_model/model_swin-unet_optimized_95.joblib'
|
||||
best_model = best_model.cpu()
|
||||
joblib.dump(best_model, model_path)
|
||||
print(f"\n✅ Đã lưu mô hình tốt nhất (Acc: {best_global_acc:.4f}) vào {model_path}")
|
||||
print(f"Cấu hình tốt nhất: {best_params}")
|
||||
|
||||
# generate report
|
||||
best_model.eval()
|
||||
with torch.no_grad():
|
||||
outputs = best_model(X_test_t)
|
||||
_, preds = torch.max(outputs, 1)
|
||||
clf_rep = classification_report(y_test, preds.cpu().numpy(), output_dict=True)
|
||||
|
||||
info = {
|
||||
"model_type": "swin-unet",
|
||||
"test_accuracy": float(best_global_acc),
|
||||
"params": {"n_estimators": best_params['epochs'], "max_depth": best_params['embed_dim']},
|
||||
"classification_report": clf_rep
|
||||
}
|
||||
with open('model_train/model_swin-unet_auto_info.json', 'w') as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user