Kaggle competition: Rainforest Connection Species Audio Detection
<SOS> metric: WeightedLabelRankingAveragePrecision Kaggle data source: rainforest-connection-species-audio-detection<data_type_conversions>
!pip -q install --upgrade pip
!pip -q install timm
!pip -q install torchlibrosa
!pip -q install audiomentations
all_users['date_account_created'] = pd.to_datetime(all_users['date_account_created'])
 all_users['timestamp_first_active'] = pd.to_datetime(( all_users.timestamp_first_active // 1000000), format='%Y%m%d')
 all_users['date_account_created'] = [datetime.timestamp(d)for d in all_users['date_account_created']]
all_users['timestamp_first_active'] = [datetime.timestamp(d)for d in all_users['timestamp_first_active']]<categorify>
import audiomentations as A
import os, time, librosa, random
from functools import partial
import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold
import torch
import torch.nn.functional as F
import torch.nn as nn
from torch.utils.data import Dataset
from timm.models import resnet34d
from torchlibrosa.stft import Spectrogram, LogmelFilterBank
from torchlibrosa.augmentation import SpecAugmentation
from tqdm import tqdm
import soundfile as sf
from contextlib import nullcontext
categorical_features = [
'affiliate_channel',
'affiliate_provider',
'first_affiliate_tracked',
'first_browser',
'first_device_type',
'gender',
'language',
'signup_app',
'signup_method'
]
for categorical_feature in categorical_features:
all_users_dummies = pd.get_dummies(all_users[categorical_feature], prefix=categorical_feature)
 all_users = all_users.drop([categorical_feature], axis=1)
 all_users = pd.concat(( all_users, all_users_dummies), axis=1 )<categorify>
NO_LABEL = -1
NUM_CLASSES = 24
class config:
seed = 42
device = "cuda:0"
train_tp_csv = '.. /input/rfcx-species-audio-detection/train_tp.csv'
test_csv = '.. /input/rfcx-species-audio-detection/sample_submission.csv'
save_path = './'
encoder = resnet34d
encoder_features = 512
percent_unlabeled = 1.0
consistency_weight = 100.0
consistency_rampup = 1000
ema_decay = 0.995
positive_weight = 2.0
lr = 1e-3
epochs = 25
batch_size = 32
num_workers = 4
train_5_folds = True
period = 6
step = 1
model_params = {
'sample_rate': 48000,
'window_size': 2048,
'hop_size': 512,
'mel_bins': 384,
'fmin': 20,
'fmax': 48000 // 2,
'classes_num': NUM_CLASSES
}
augmenter = A.Compose([
A.AddGaussianNoise(p=0.33, max_amplitude=0.02),
A.AddGaussianSNR(p=0.33),
A.FrequencyMask(min_frequency_band=0.01, max_frequency_band=0.25, p=0.33),
A.TimeMask(min_band_part=0.01, max_band_part=0.25, p=0.33),
A.Gain(p=0.33)
 ])
train_users_n = train_users.shape[0]
X_train = all_users.values[:train_users_n]
le = LabelEncoder()
y_train = le.fit_transform(labels)
 X_test = all_users.values[train_users_n:]<save_to_csv>
def get_n_fold_df(csv_path, folds=5):
df = pd.read_csv(csv_path)
 df_group = df.groupby("recording_id")[["species_id"]].first().reset_index()
df_group = df_group.sample(frac=1, random_state=config.seed ).reset_index(drop=True)
 df_group.loc[:, 'fold'] = -1
X = df_group["recording_id"].values
y = df_group["species_id"].values
kfold = StratifiedKFold(n_splits=folds, random_state=config.seed)
 for fold,(t_idx, v_idx)in enumerate(kfold.split(X, y)) :
df_group.loc[v_idx, "fold"] = fold
return df.merge(df_group[['recording_id', 'fold']], on="recording_id", how="left")
 def init_layer(layer):
nn.init.xavier_uniform_(layer.weight)
 if hasattr(layer, "bias"):
if layer.bias is not None:
layer.bias.data.fill_(0.)
def init_bn(bn):
bn.bias.data.fill_(0.)
bn.weight.data.fill_(1.0)
 def sigmoid_rampup(current, rampup_length):

 if rampup_length == 0:
return 1.0
else:
current = np.clip(current, 0.0, rampup_length)
 phase = 1.0 - current / rampup_length
return float(np.exp(-5.0 * phase * phase))
class AverageMeter(object):

 def __init__(self):
self.reset()
def reset(self):
self.val = 0
self.avg = 0
self.sum = 0
self.count = 0
def update(self, val, n=1):
self.val = val
self.sum += val * n
self.count += n
self.avg = self.sum / self.count
class MetricMeter(object):
def __init__(self):
self.reset()
def reset(self):
self.y_true = []
self.y_pred = []
def update(self, y_true, y_pred):
try:
self.y_true.extend(y_true.detach().cpu().numpy().tolist())
 self.y_pred.extend(torch.sigmoid(y_pred ).cpu().detach().numpy().tolist())
 except:
print("UPDATE FAILURE")
 def update_list(self, y_true, y_pred):
self.y_true.extend(y_true)
 self.y_pred.extend(y_pred)
 @property
def avg(self):
score_class, weight = lwlrap(np.array(self.y_true), np.array(self.y_pred))
self.score =(score_class * weight ).sum()
return self.score
def interpolate(x: torch.Tensor, ratio: int):

(batch_size, time_steps, classes_num)= x.shape
upsampled = x[:, :, None, :].repeat(1, 1, ratio, 1)
 upsampled = upsampled.reshape(batch_size, time_steps * ratio, classes_num)
 return upsampled
def _one_sample_positive_class_precisions(scores, truth):
num_classes = scores.shape[0]
pos_class_indices = np.flatnonzero(truth > 0)
 if not len(pos_class_indices):
return pos_class_indices, np.zeros(0)
 retrieved_classes = np.argsort(scores)[::-1]
class_rankings = np.zeros(num_classes, dtype=np.int)
 class_rankings[retrieved_classes] = range(num_classes)
 retrieved_class_true = np.zeros(num_classes, dtype=np.bool)
 retrieved_class_true[class_rankings[pos_class_indices]] = True
retrieved_cumulative_hits = np.cumsum(retrieved_class_true)
 precision_at_hits =(
 retrieved_cumulative_hits[class_rankings[pos_class_indices]] / 
(1 + class_rankings[pos_class_indices].astype(np.float)))
 return pos_class_indices, precision_at_hits
def lwlrap(truth, scores):
assert truth.shape == scores.shape
num_samples, num_classes = scores.shape
precisions_for_samples_by_classes = np.zeros(( num_samples, num_classes))
for sample_num in range(num_samples):
pos_class_indices, precision_at_hits = _one_sample_positive_class_precisions(scores[sample_num, :],
truth[sample_num, :])
 precisions_for_samples_by_classes[sample_num, pos_class_indices] = precision_at_hits
labels_per_class = np.sum(truth > 0, axis=0)
 weight_per_class = labels_per_class / float(np.sum(labels_per_class))
per_class_lwlrap =(np.sum(precisions_for_samples_by_classes, axis=0)/
np.maximum(1, labels_per_class))
return per_class_lwlrap, weight_per_class
def pretty_print_metrics(fold, epoch, optimizer, train_loss_metrics, val_loss_metrics):
print(f)
 class TestDataset(Dataset):
def __init__(self, df, data_path, period=10, step=1):
self.data_path = data_path
self.period = period
self.step = step
self.recording_ids = list(df["recording_id"].unique())
 def __len__(self):
return len(self.recording_ids)
 def __getitem__(self, idx):
recording_id = self.recording_ids[idx]
y, sr = sf.read(f"{self.data_path}/{recording_id}.flac")
 len_y = len(y)
 effective_length = sr * self.period
effective_step = sr * self.step
y_ = []
i = 0
while i+effective_length <= len_y:
y__ = y[i:i + effective_length]
y_.append(y__)
 i = i + effective_step
y = np.stack(y_)
 label = np.zeros(NUM_CLASSES, dtype='f')
 return {
"waveform": y,
"target": torch.tensor(label, dtype=torch.float),
"id": recording_id
}
def predict_on_test(model, test_loader):
model.eval()
pred_list = []
id_list = []
with torch.no_grad() :
t = tqdm(test_loader)
 for i, sample in enumerate(t):
input = sample["waveform"].to(config.device)
 bs, seq, w = input.shape
input = input.reshape(bs * seq, w)
 id = sample["id"]
output, _ = model(input)
 output = output.reshape(bs, seq, -1)
 output, _ = torch.max(output, dim=1)
 output = output.cpu().detach().numpy().tolist()
pred_list.extend(output)
 id_list.extend(id)
 return pred_list, id_list
def generate_answer(y_pred, classifer_name):
ids = []
cts = []
for i in range(len(id_test)) :
idx = id_test[i]
ids += [idx] * 5
cts += le.inverse_transform(np.argsort(y_pred[i])[::-1])[:5].tolist()
sub = pd.DataFrame(np.column_stack(( ids, cts)) , columns=['id', 'country'])
 sub.to_csv(classifer_name+'.csv',index=False)
 return sub<train_model>
class AttentionHead(nn.Module):
def __init__(self, in_features: int, out_features: int):
super().__init__()
self.conv_attention = nn.Conv1d(in_channels=in_features,
out_channels=out_features,
kernel_size=1, stride=1,
padding=0, bias=True)
 self.conv_classes = nn.Conv1d(in_channels=in_features,
out_channels=out_features,
kernel_size=1, stride=1,
padding=0, bias=True)
 self.batch_norm_attention = nn.BatchNorm1d(out_features)
 self.init_weights()
def init_weights(self):
init_layer(self.conv_attention)
 init_layer(self.conv_classes)
 init_bn(self.batch_norm_attention)
 def forward(self, x):
norm_att = torch.softmax(torch.tanh(self.conv_attention(x)) , dim=-1)
 classes = self.conv_classes(x)
 x = torch.sum(norm_att * classes, dim=2)
 return x, norm_att, classes
class SEDAudioClassifier(nn.Module):
def __init__(self, sample_rate, window_size, hop_size,
mel_bins, fmin, fmax, classes_num):
super().__init__()
self.interpolate_ratio = 32
self.spectrogram_extractor = Spectrogram(n_fft=window_size,
hop_length=hop_size,
win_length=window_size,
window='hann', center=True,
pad_mode='reflect',
freeze_parameters=True)
 self.logmel_extractor = LogmelFilterBank(sr=sample_rate, n_fft=window_size,
n_mels=mel_bins, fmin=fmin,
fmax=fmax, ref=1.0,
amin=1e-10, top_db=None,
freeze_parameters=True)
 self.batch_norm = nn.BatchNorm2d(mel_bins)
 self.encoder = partial(config.encoder, pretrained=True, in_chans=1 )()
self.fc = nn.Linear(config.encoder_features,
config.encoder_features, bias=True)
 self.att_head = AttentionHead(config.encoder_features, classes_num)
 self.avg_pool = nn.modules.pooling.AdaptiveAvgPool2d(( 1, 1))
self.init_weight()
def init_weight(self):
init_bn(self.batch_norm)
 init_layer(self.fc)
 self.att_head.init_weights()
def forward(self, input, spec_aug=False,
mixup_lambda=None, return_encoding=False):
x = self.spectrogram_extractor(input.float())
 x = self.logmel_extractor(x)
 x = x.transpose(1, 3)
 x = self.batch_norm(x)
 x = x.transpose(1, 3)
 x = self.encoder.forward_features(x)
 x = torch.mean(x, dim=3)
 x1 = F.max_pool1d(x, kernel_size=3, stride=1, padding=1)
 x2 = F.avg_pool1d(x, kernel_size=3, stride=1, padding=1)
 x = x1 + x2
x = F.dropout(x, p=0.5, training=self.training)
 x = x.transpose(1, 2)
 x = F.relu_(self.fc(x))
x = x.transpose(1, 2)
 x = F.dropout(x, p=0.5, training=self.training)
(clipwise_output, norm_att, segmentwise_output)= self.att_head(x)
 segmentwise_output = segmentwise_output.transpose(1, 2)
 framewise_output = interpolate(segmentwise_output, self.interpolate_ratio)
 return clipwise_output, framewise_output
def get_model(is_mean_teacher=False):
model = SEDAudioClassifier(**config.model_params)
 model = model.to(config.device)
 if is_mean_teacher:
for param in model.parameters() :
param.detach_()
return model
mlp = MLPClassifier()
mlp.fit(X_train, y_train)
 y_pred_mlp = mlp.predict_proba(X_test)
 generate_answer(y_pred_mlp, 'MLP' )<train_model>
def sigmoid_mse_loss(input_logits, target_logits):
assert input_logits.size() == target_logits.size()
input_softmax = torch.sigmoid(input_logits)
 target_softmax = torch.sigmoid(target_logits)
 num_classes = input_logits.size() [1]
return F.mse_loss(input_softmax, target_softmax, size_average=False 
)/ num_classes
class MeanTeacherLoss(nn.Module):
def __init__(self):
super().__init__()
self.positive_weight = torch.ones(
 NUM_CLASSES ).to(config.device)* config.positive_weight
self.class_criterion = nn.BCEWithLogitsLoss(
 reduction='none', pos_weight=self.positive_weight)
 self.consistency_criterion = sigmoid_mse_loss
def make_safe(self, pred):
pred = torch.where(torch.isnan(pred), torch.zeros_like(pred), pred)
 return torch.where(torch.isinf(pred), torch.zeros_like(pred), pred)
 def get_consistency_weight(self, epoch):
return config.consistency_weight * sigmoid_rampup(
 epoch, config.consistency_rampup)
 def forward(self, student_pred, teacher_pred, target, classif_weights, epoch):
student_pred = self.make_safe(student_pred)
 teacher_pred = self.make_safe(teacher_pred ).detach().data
batch_size = len(target)
 labeled_batch_size = target.ne(NO_LABEL ).all(axis=1 ).sum().item() + 1e-3
student_classif, student_consistency = student_pred, student_pred
student_class_loss =(self.class_criterion(
 student_classif, target)* classif_weights / labeled_batch_size ).sum()
consistency_weights = self.get_consistency_weight(epoch)
 consistency_loss = consistency_weights * self.consistency_criterion(
 student_consistency, teacher_pred)/ batch_size
loss = student_class_loss + consistency_loss
return loss, student_class_loss, consistency_loss, consistency_weights
xgb = XGBClassifier(max_depth=6, learning_rate=0.3, n_estimators=25,
objective='multi:softprob', subsample=0.5, colsample_bytree=0.5, seed=0)
 xgb.fit(X_train, y_train)
 y_pred_xgb = xgb.predict_proba(X_test)
 generate_answer(y_pred_xgb, 'XGB' )<import_modules>
class MeanTeacherDataset(Dataset):
def __init__(self, df, transforms, period=5,
data_path=".. /input/rfcx-species-audio-detection/train",
val=False, percent_unlabeled=0.0):
self.period = period
self.transforms = transforms
self.data_path = data_path
self.val = val
self.percent_unlabeled = percent_unlabeled
dfgby = df.groupby("recording_id" ).agg(lambda x: list(x)).reset_index()
self.recording_ids = dfgby["recording_id"].values
self.species_ids = dfgby["species_id"].values
self.t_mins = dfgby["t_min"].values
self.t_maxs = dfgby["t_max"].values
def __len__(self):
return int(len(self.recording_ids)*(1 + self.percent_unlabeled))
def __getitem__(self, idx):
if idx >= len(self.recording_ids):
audio, label, rec_id, sr = self.get_unlabeled_item(idx)
 classif_weights = np.zeros(NUM_CLASSES, dtype='f')
 else:
audio, label, rec_id, sr = self.get_labeled_item(idx)
 classif_weights = np.ones(NUM_CLASSES, dtype='f')
 audio_teacher = np.copy(audio)
 audio = self.transforms(samples=audio, sample_rate=sr)
 audio_teacher = self.transforms(samples=audio_teacher, sample_rate=sr)
 return {
"waveform": audio,
"teacher_waveform": audio_teacher,
"target": torch.tensor(label, dtype=torch.float),
"classification_weights": classif_weights,
"id": rec_id
}
def get_labeled_item(self, idx):
recording_id = self.recording_ids[idx]
species_id = self.species_ids[idx]
t_min, t_max = self.t_mins[idx], self.t_maxs[idx]
rec, sr = sf.read(f"{self.data_path}/{recording_id}.flac")
 len_rec = len(rec)
 effective_length = sr * self.period
rint = np.random.randint(len(t_min))
tmin, tmax = round(sr * t_min[rint]), round(sr * t_max[rint])
 dur = tmax - tmin
min_dur = min(dur, round(sr * self.period))
center = round(( tmin + tmax)/ 2)
 rand_start = center - effective_length + max(min_dur - dur//2, 0)
 if rand_start < 0:
rand_start = 0
rand_end = center - max(min_dur - dur//2, 0)
 start = np.random.randint(rand_start, rand_end)
 rec = rec[start:start + effective_length]
if len(rec)< effective_length:
new_rec = np.zeros(effective_length, dtype=rec.dtype)
 start1 = np.random.randint(effective_length - len(rec))
new_rec[start1:start1 + len(rec)] = rec
rec = new_rec.astype(np.float32)
 else:
rec = rec.astype(np.float32)
 start_time = start / sr
end_time =(start + effective_length)/ sr
label = np.zeros(NUM_CLASSES, dtype='f')
 for i in range(len(t_min)) :
if(t_min[i] >= start_time)&(t_max[i] <= end_time):
label[species_id[i]] = 1
elif start_time <=(( t_min[i] + t_max[i])/ 2)<= end_time:
label[species_id[i]] = 1
return rec, label, recording_id, sr
def get_unlabeled_item(self, idx, random_sample=False):
real_idx = idx - len(self.recording_ids)
 if self.val:
rec_id = self.recording_ids[real_idx]
else:
rec_id = random.sample(list(self.recording_ids), 1)[0]
rec, sr = sf.read(f"{self.data_path}/{rec_id}.flac")
 effective_length = int(sr * self.period)
 max_end = len(rec)- effective_length
if self.val:
start = int(idx * 16963 % max_end)
 else:
start = np.random.randint(0, max_end)
 rec = rec[start:(start+effective_length)]
rec = rec.astype(np.float32)
 label = np.ones(NUM_CLASSES, dtype='f')* NO_LABEL
return rec, label, rec_id, sr
def get_data_loader(df, is_val=False):
dataset = MeanTeacherDataset(
 df=df,
transforms=config.augmenter,
period=config.period,
percent_unlabeled=config.percent_unlabeled 
)
 return torch.utils.data.DataLoader(
 dataset,
batch_size=config.batch_size,
shuffle=not is_val,
drop_last=not is_val,
num_workers=config.num_workers
)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import accuracy_score
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder<load_from_csv>
def update_teacher_params(student, teacher, alpha, global_step):
alpha = min(1 - 1 /(global_step + 1), alpha)
 for ema_param, param in zip(teacher.parameters() , student.parameters()):
ema_param.data.mul_(alpha ).add_(1 - alpha, param.data)
 def train_one_epoch(student, mean_teacher, loader,
criterion, optimizer, scheduler, epoch, is_val=False):
global_step = 0
losses = AverageMeter()
consistency_loss_avg = AverageMeter()
class_loss_avg = AverageMeter()
comp_metric = MetricMeter()
if is_val:
student.eval()
mean_teacher.eval()
context = torch.no_grad()
else:
student.train()
mean_teacher.train()
context = nullcontext()
with context:
t = tqdm(loader)
 for i, sample in enumerate(t):
student_input = sample['waveform'].to(config.device)
 teacher_input = sample['teacher_waveform'].to(config.device)
 target = sample['target'].to(config.device)
 classif_weights = sample['classification_weights'].to(config.device)
 batch_size = len(target)
 student_pred, _ = student(student_input)
 teacher_pred, _ = mean_teacher(teacher_input)
 loss, class_loss, consistency_loss, consistency_weight = criterion(
 student_pred, teacher_pred, target, classif_weights, epoch)
 if not is_val:
optimizer.zero_grad()
loss.backward()
optimizer.step()
update_teacher_params(student, mean_teacher,
config.ema_decay, global_step)
 scheduler.step()
comp_metric.update(target, student_pred)
 losses.update(loss.item() , batch_size)
 consistency_loss_avg.update(consistency_loss.item() , batch_size)
 class_loss_avg.update(class_loss.item() , batch_size)
 global_step += 1
t.set_description(f"Epoch:{epoch} - Loss:{losses.avg:0.4f}")
 t.close()
return {'lwlrap':comp_metric.avg,
'loss':losses.avg,
'consistency_loss':consistency_loss_avg.avg,
'class_loss':class_loss_avg.avg,
'consistency_weight':consistency_weight}
train = pd.read_csv('/kaggle/input/airbnb-recruiting-new-user-bookings/train_users_2.csv.zip')
 test = pd.read_csv('/kaggle/input/airbnb-recruiting-new-user-bookings/test_users.csv.zip')
 age_gender = pd.read_csv('/kaggle/input/airbnb-recruiting-new-user-bookings/age_gender_bkts.csv.zip')
 sessions = pd.read_csv('/kaggle/input/airbnb-recruiting-new-user-bookings/sessions.csv.zip' )<drop_column>
def train(df, fold):
train_df = df[df.fold != fold]
val_df = df[df.fold == fold]
train_loader = get_data_loader(train_df)
 val_loader = get_data_loader(val_df)
 student_model = get_model()
teacher_model = get_model(is_mean_teacher=True)
 optimizer = torch.optim.AdamW(student_model.parameters() , lr=config.lr)
 num_train_steps = int(len(train_loader)* config.epochs)
 scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
 optimizer, T_max=num_train_steps)
 criterion = MeanTeacherLoss()
best_val_metric = -np.inf
val_metrics = []
train_metrics = []
for epoch in range(0, config.epochs):
train_loss_metrics = train_one_epoch(
 student_model, teacher_model, train_loader,
criterion, optimizer, scheduler, epoch)
 val_loss_metrics = train_one_epoch(
 student_model, teacher_model, val_loader,
criterion, optimizer, scheduler, epoch, is_val=True)
 train_metrics.append(train_loss_metrics)
 val_metrics.append(val_loss_metrics)
 pretty_print_metrics(fold, epoch, optimizer,
train_loss_metrics, val_loss_metrics)
 if val_loss_metrics['lwlrap'] > best_val_metric:
print(f" LWLRAP Improved from {best_val_metric} --> {val_loss_metrics['lwlrap']}
")
 torch.save(teacher_model.state_dict() ,
os.path.join(config.save_path, f'fold-{fold}.bin'))
best_val_metric = val_loss_metrics['lwlrap']
df = get_n_fold_df(config.train_tp_csv)
 for fold in range(5 if config.train_5_folds else 1):
train(df, fold )
X_train = train.drop(['date_first_booking', 'country_destination'], axis=1)
 X_test = test.drop(['date_first_booking'], axis=1 )<concatenate>
def test(test_df, train_fold):
test_dataset = TestDataset(
 df=test_df,
data_path=".. /input/rfcx-species-audio-detection/test",
period=config.period,
step=config.step 
)
 test_loader = torch.utils.data.DataLoader(
 test_dataset,
batch_size=1,
shuffle=False,
drop_last=False,
num_workers=config.num_workers 
)
 weights_path = os.path.join(config.save_path, f'fold-{train_fold}.bin')
 model = get_model()
model.load_state_dict(torch.load(weights_path, map_location=config.device), strict=False)
 test_pred, ids = predict_on_test(model, test_loader)
 test_pred_df = pd.DataFrame({
"recording_id": test_df.recording_id.values
})
 target_cols = test_df.columns[1:].values.tolist()
test_pred_df = test_pred_df.join(pd.DataFrame(np.array(test_pred),
columns=target_cols))
test_pred_df.to_csv(os.path.join(config.save_path,
f"fold-{train_fold}-submission.csv"),
index=False)
 test_df = pd.read_csv(config.test_csv)
 for fold in range(5 if config.train_5_folds else 1):
test(test_df, fold )
<feature_engineering><EOS> 
def ensemble(submission_path):
dfs = [pd.read_csv(os.path.join(
 config.save_path, f"fold-{i}-submission.csv")) for i in range(5)]
anchor = dfs[0].copy()
cols = anchor.columns[1:]
for c in cols:
total = 0
for df in dfs:
total += df[c]
anchor[c] = total / len(dfs)
 anchor.to_csv(submission_path, index=False)
 submission_path = os.path.join(config.save_path, f"submission.csv")
 if config.train_5_folds:
ensemble(submission_path)
 else:
fold0_submission = os.path.join(config.save_path, f"fold-0-submission.csv")
 os.rename(fold0_submission, submission_path)