Skip to content

Commit 419dd21

Browse files
committed
Include all folds in cross-validation
There is no dedicated testset in recommended procedure
1 parent 806d27c commit 419dd21

File tree

5 files changed

+144
-34
lines changed

5 files changed

+144
-34
lines changed

microesc/jobs.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def parse(args):
138138
help='Only run a pre-flight check')
139139
a('--jobs', type=int, default=5,
140140
help='Number of parallel jobs')
141-
a('--folds', type=int, default=9,
141+
a('--folds', type=int, default=10,
142142
help='Number of folds to test')
143143

144144
a('--start', type=int, default=0,
@@ -160,7 +160,8 @@ def main():
160160
experiments = experiments.loc[range(args.start, stop)]
161161

162162
overrides = {}
163-
folds = list(range(0, args.folds))
163+
folds = list(range(1, args.folds+1))
164+
assert max(folds) <= 10
164165
if args.check:
165166
batches = 2
166167
overrides['batch'] = 10

microesc/test.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,18 +106,35 @@ def score(model, data):
106106
out['test'] = out['test_foreground'] + out['test_background']
107107
return out
108108

109-
def evaluate(models, folds, testset, predictor, out_dir, dry_run=False):
109+
def evaluate(models, folds_data, predictor, out_dir, dry_run=False):
110110

111111
def eval_experiment(df):
112112
results = {}
113113
by_fold = df.sort_index(level="fold", ascending=True)
114114

115115
for idx, row in by_fold.iterrows():
116-
print('Testing model {} fold={}'.format(row['experiment'], row['fold']))
117-
116+
fold = row['fold']
117+
assert fold > 0, 'fold number should be 1 indexed'
118+
print('Testing model {} fold={}'.format(row['experiment'], fold))
119+
118120
model_path = row['model_path']
119-
val = folds[row['fold']][1]
120-
test = testset
121+
val = folds_data[fold-1][1]
122+
test = folds_data[fold-1][2]
123+
test_folds = test.fold.unique()
124+
assert len(test_folds) == 1
125+
assert test_folds[0] == fold
126+
val_folds = val.fold.unique()
127+
assert len(val_folds) == 1
128+
assert val_folds[0] != fold
129+
130+
train_data = folds_data[fold-1][0]
131+
train_files = set(train_data.slice_file_name.unique())
132+
assert len(train_files) > 7000, len(train_files)
133+
test_files = set(test.slice_file_name.unique())
134+
assert len(test_files) > 700
135+
common_files = train_files.intersection(test_files)
136+
assert len(common_files) == 0, common_files
137+
121138
if dry_run:
122139
val = test[0:20]
123140
test = test[0:20]
@@ -173,22 +190,14 @@ def main():
173190

174191
urbansound8k.maybe_download_dataset(args.datasets_dir)
175192
data = urbansound8k.load_dataset()
176-
folds, test = urbansound8k.folds(data)
193+
folds = urbansound8k.folds(data)
177194
exsettings = common.load_settings_path(args.settings_path)
178195
frames = exsettings['frames']
179196
voting = exsettings['voting']
180197
overlap = exsettings['voting_overlap']
181198
settings = features.settings(exsettings)
182199

183200

184-
all_folds = pandas.concat([f[0] for f in folds])
185-
train_files = set(all_folds.slice_file_name.unique())
186-
test_files = set(test.slice_file_name.unique())
187-
assert len(train_files) > 7000
188-
assert len(test_files) > 700
189-
common_files = train_files.intersection(test_files)
190-
assert len(common_files) == 0
191-
192201
def load_sample(sample):
193202
return features.load_sample(sample, settings, start_time=sample.start,
194203
window_frames=frames, feature_dir=args.features_dir)
@@ -219,7 +228,7 @@ def get_stats(row):
219228
model_stats.to_csv(os.path.join(out_dir, 'stm32stats.csv'))
220229

221230
print('Testing models...')
222-
results = evaluate(best, folds, test, predictor=predict, out_dir=out_dir, dry_run=args.check)
231+
results = evaluate(best, folds, predictor=predict, out_dir=out_dir, dry_run=args.check)
223232

224233

225234
if __name__ == '__main__':

microesc/train.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def on_epoch_end(self, epoch, logs):
8181

8282

8383

84-
def train_model(out_dir, fold, builder,
84+
def train_model(out_dir, train, val, builder,
8585
loader, val_loader, settings, seed=1):
8686
"""Train a single model"""
8787

@@ -93,7 +93,7 @@ def train_model(out_dir, fold, builder,
9393
batch_size = settings['batch']
9494
learning_rate = settings.get('learning_rate', 0.01)
9595

96-
train, val = fold
96+
assert len(train) > len(val) * 5, 'training data should be much larger than validation'
9797

9898
def top3(y_true, y_pred):
9999
return keras.metrics.top_k_categorical_accuracy(y_true, y_pred, k=3)
@@ -159,7 +159,7 @@ def parse(args):
159159
common.add_arguments(parser)
160160
Settings.add_arguments(parser)
161161

162-
a('--fold', type=int, default=0,
162+
a('--fold', type=int, default=1,
163163
help='')
164164
a('--skip_model_check', action='store_true', default=False,
165165
help='Skip checking whether model fits on STM32 device')
@@ -182,6 +182,17 @@ def setup_keras():
182182
sess = tf.Session(config=session_config)
183183
B.set_session(sess)
184184

185+
def load_training_data(data, fold):
186+
assert fold >= 1 # should be 1 indexed
187+
folds = urbansound8k.folds(data)
188+
assert len(folds) == 10
189+
train_data = folds[fold-1][0]
190+
val_data = folds[fold-1][1]
191+
test_folds = folds[fold-1][2].fold.unique()
192+
assert len(test_folds) == 1
193+
assert test_folds[0] == fold, (test_folds[0], '!=', fold) # by convention, test fold is fold number
194+
return train_data, val_data
195+
185196
def main():
186197
setup_keras()
187198

@@ -216,10 +227,8 @@ def main():
216227

217228
features.maybe_download(feature_settings, feature_dir)
218229

219-
220230
data = urbansound8k.load_dataset()
221-
folds, test = urbansound8k.folds(data)
222-
assert len(folds) == 9
231+
train_data, val_data = load_training_data(data, fold)
223232

224233
def load(sample, validation):
225234
augment = not validation and train_settings['augment'] != 0
@@ -245,7 +254,7 @@ def build_model():
245254
print('Training model', name)
246255
print('Settings', json.dumps(exsettings))
247256

248-
h = train_model(output_dir, folds[fold],
257+
h = train_model(output_dir, train_data, val_data,
249258
builder=build_model,
250259
loader=functools.partial(load, validation=False),
251260
val_loader=functools.partial(load, validation=True),

microesc/urbansound8k.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import urllib.request
44
import tarfile
55

6+
import numpy
67
import pandas
78

89
here = os.path.dirname(__file__)
@@ -74,17 +75,70 @@ def sample_path(sample, dataset_path = None):
7475
return os.path.join(dataset_path, 'audio', 'fold'+str(sample.fold), sample.slice_file_name)
7576

7677

77-
# Use fold=10 for testing, as recommended by Urbansound8k dataset authors
78+
# Split the 10 folds into training, testing, and
7879
def folds(data):
79-
test_fold = 10
80-
train = data[data.fold != test_fold]
81-
test = data[data.fold == test_fold]
80+
fold_idxs = folds_idx(n_folds=10)
81+
assert len(fold_idxs) == 10
8282

8383
folds = []
84-
for fold in range(1, 10):
85-
assert fold != test_fold
86-
fold_train = train[train.fold != fold]
87-
fold_val = train[train.fold == fold]
88-
folds.append((fold_train, fold_val))
84+
for fold in fold_idxs:
85+
train, val, test = fold
86+
87+
# our folds are 1-indexed instead of 0...
88+
train = numpy.array(train) + 1
89+
val = numpy.array(val) + 1
90+
test = numpy.array(test) + 1
91+
fold_train = data[data.fold.isin(train)]
92+
fold_val = data[data.fold.isin(val)]
93+
fold_test = data[data.fold.isin(test)]
94+
95+
# post-condition
96+
train_folds = set(fold_train.fold.unique())
97+
val_folds = set(fold_val.fold.unique())
98+
test_folds = set(fold_test.fold.unique())
99+
assert len(train_folds) == 8, len(train_folds)
100+
assert train_folds.intersection(val_folds) == set()
101+
assert train_folds.intersection(test_folds) == set()
102+
assert val_folds.intersection(test_folds) == set()
103+
104+
folds.append((fold_train, fold_val, fold_test))
89105

90-
return folds, test
106+
return folds
107+
108+
109+
def ensure_valid_fold(fold, n_folds=10):
110+
train, val, test = fold
111+
assert len(train) == n_folds-2, len(train)
112+
assert 0 <= train[0] < n_folds, train[0]
113+
assert len(val) == 1, len(val)
114+
assert 0 <= val[0] < n_folds, val[0]
115+
assert len(test) == 1, len(test)
116+
assert 0 <= test[0] < n_folds, test[0]
117+
assert test[0] != val[0]
118+
test_overlap = set(train).intersection(set(test))
119+
val_overlap = set(train).intersection(set(val))
120+
assert test_overlap == set(), test_overlap
121+
assert val_overlap == set(), val_overlap
122+
assert sorted(train + val + test) == list(range(0, n_folds))
123+
return True
124+
125+
def folds_idx(n_folds):
126+
"""Generate fold indices for cross-validation.
127+
Each fold has 1 validation, 1 test set and the remaining train"""
128+
test_fold = 10
129+
130+
folds = []
131+
all_folds = list(range(0, n_folds))
132+
for idx in range(0, n_folds):
133+
test = [ all_folds[idx] ]
134+
# using Python negative index support for lists to wrap around at edges of array
135+
val = [ all_folds[idx-1] ]
136+
train = list(set(all_folds).difference(set(test+val)))
137+
fold = ( train, val, test )
138+
ensure_valid_fold(fold)
139+
folds.append(fold)
140+
141+
assert len(folds) == n_folds, len(folds)
142+
return folds
143+
144+

test/test_urbansound.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import shutil
44

55
import numpy
6+
import pytest
67

78
from microesc import preprocess, urbansound8k, features, report
89

@@ -65,3 +66,39 @@ def test_grouped_confusion():
6566

6667
# danger, only one class
6768
assert(gcm[3][3] == 82)
69+
70+
71+
folds = urbansound8k
72+
CORRECT_FOLDS={
73+
'8val-9train': ((0,1,2,3,4,5,6,7), (8,), (9,)),
74+
}
75+
WRONG_FOLDS={
76+
'train too short': ((0,1,2), (3,), (4,)),
77+
'out-of-bounds train': ((0,1,2,3,4,5,6,7), (4,), (5,)),
78+
'out-of-bounds val': ((0,2,3,4,5,6,7,8), (10,), (5,)),
79+
}
80+
81+
@pytest.mark.parametrize('example', CORRECT_FOLDS.keys())
82+
def test_ensure_valid_fold_passes_correct(example):
83+
fold = CORRECT_FOLDS[example]
84+
folds.ensure_valid_fold(fold)
85+
86+
@pytest.mark.parametrize('example', WRONG_FOLDS.keys())
87+
def test_ensure_valid_fold_detects_wrong(example):
88+
fold = WRONG_FOLDS[example]
89+
with pytest.raises(AssertionError) as e_info:
90+
folds.ensure_valid_fold(fold)
91+
92+
def test_folds_idx():
93+
f = folds.folds_idx(10)
94+
print('\n'+'\n'.join([ str(i) for i in f ]))
95+
assert f[0][2][0] == 0, "first test fold should be 0"
96+
assert f[-1][2][0] == 9, "last test fold should be 9"
97+
98+
99+
def test_folds():
100+
data = urbansound8k.load_dataset()
101+
f = urbansound8k.folds(data)
102+
assert len(f) == 10
103+
104+

0 commit comments

Comments
 (0)