-
Notifications
You must be signed in to change notification settings - Fork 33
/
ffm.cpp
471 lines (329 loc) · 15.4 KB
/
ffm.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
#include "ffm.h"
#include "ffm-model.h"
#include "ffm-nn-model.h"
#include "ftrl-model.h"
#include "nn-model.h"
#include <iostream>
#include <iomanip>
#include <fstream>
#include <random>
#include <algorithm>
#include <immintrin.h>
#include <omp.h>
#include <boost/program_options.hpp>
// Batch configuration
const ffm_uint batch_size = 20000;
const ffm_uint mini_batch_size = 24;
// Dropout configuration
const ffm_uint dropout_mask_max_size = 4000; // in 64-bit words
std::default_random_engine rnd(2017);
template <typename T>
T min(T a, T b) {
return a < b ? a : b;
}
struct ffm_dataset {
ffm_index index;
std::string data_file_name;
};
std::vector<std::pair<ffm_ulong, ffm_ulong>> generate_batches(const ffm_index & index, bool shuffle) {
std::vector<std::pair<ffm_ulong, ffm_ulong>> batches;
for (ffm_ulong batch_start = 0; batch_start < index.size; batch_start += batch_size)
batches.push_back(std::make_pair(batch_start, min(batch_start + batch_size, index.size)));
if (shuffle)
std::shuffle(batches.begin(), batches.end(), rnd);
return batches;
}
std::vector<std::pair<ffm_ulong, ffm_ulong>> generate_mini_batches(ffm_ulong begin, ffm_ulong end) {
std::vector<std::pair<ffm_ulong, ffm_ulong>> batches;
for (ffm_ulong mini_batch_start = begin; mini_batch_start < end; mini_batch_start += mini_batch_size)
batches.push_back(std::make_pair(mini_batch_start, min(mini_batch_start + mini_batch_size, end)));
return batches;
}
void fill_mask_rand(uint64_t * mask, int size, int zero_prob_log) {
memset(mask, 0, size * sizeof(uint64_t));
for (int p = 0; p < size; ++ p) {
for (int i = 0; i < zero_prob_log; ++ i) {
long long unsigned int v;
if (_rdrand64_step(&v) != 1)
throw std::runtime_error("Error generating random number!");
mask[p] |= v;
}
}
}
void fill_mask_ones(uint64_t * mask, int size) {
memset(mask, 0xFF, size * sizeof(uint64_t));
}
ffm_double compute_ap(const std::vector<ffm_float> & predictions, ffm_uint begin_idx, ffm_uint end_idx, ffm_uint positive_idx) {
ffm_uint rank = 0;
for (ffm_uint j = begin_idx; j < end_idx; ++ j)
if (predictions[j] >= predictions[positive_idx])
rank ++;
if (rank > 0 && rank <= 12)
return 1.0 / rank;
else
return 0.0;
}
ffm_double compute_map(const ffm_index & index, const std::vector<ffm_float> & predictions) {
ffm_double total = 0.0;
ffm_uint count = 0;
ffm_uint cur_group = 0;
ffm_uint cur_group_start_idx = 0;
ffm_uint positive_idx = index.size + 1;
for (ffm_uint i = 0; i < index.size; ++ i) {
if (index.groups[i] < cur_group)
throw std::logic_error("Groups must be ordered!");
if (index.groups[i] > cur_group) {
if (cur_group > 0) {
total += compute_ap(predictions, cur_group_start_idx, i, positive_idx);
count ++;
}
cur_group = index.groups[i];
cur_group_start_idx = i;
positive_idx = index.size + 1;
}
if (index.labels[i] > 0)
positive_idx = i;
}
total += compute_ap(predictions, cur_group_start_idx, index.size, positive_idx);
count ++;
return total / count;
}
template <typename M>
ffm_double train_on_dataset(const std::vector<M*> & models, const ffm_dataset & dataset, uint dropout_prob_log) {
float dropout_mult = (1 << dropout_prob_log) / ((1 << dropout_prob_log) - 1.0f);
time_t start_time = time(nullptr);
std::cout << " Training... ";
std::cout.flush();
auto batches = generate_batches(dataset.index, true);
ffm_double loss = 0.0;
ffm_ulong cnt = 0;
// Iterate over batches, read each and then iterate over examples
#pragma omp parallel for schedule(dynamic, 1) reduction(+: loss) reduction(+: cnt)
for (ffm_ulong bi = 0; bi < batches.size(); ++ bi) {
auto batch_start_index = batches[bi].first;
auto batch_end_index = batches[bi].second;
auto batch_start_offset = dataset.index.offsets[batch_start_index];
auto batch_end_offset = dataset.index.offsets[batch_end_index];
auto mini_batches = generate_mini_batches(batch_start_index, batch_end_index);
std::vector<ffm_feature> batch_features = ffm_read_batch(dataset.data_file_name, batch_start_offset, batch_end_offset);
ffm_feature * batch_features_data = batch_features.data();
uint64_t dropout_mask[dropout_mask_max_size];
std::vector<float> ts(batch_end_index - batch_start_index);
std::vector<uint> tc(batch_end_index - batch_start_index);
for (uint mi = 0; mi < models.size(); ++ mi) {
std::shuffle(mini_batches.begin(), mini_batches.end(), rnd);
for (auto mb = mini_batches.begin(); mb != mini_batches.end(); ++ mb) {
for (auto ei = mb->first; ei < mb->second; ++ ei) {
ffm_float y = dataset.index.labels[ei];
ffm_float norm = dataset.index.norms[ei];
auto start_offset = dataset.index.offsets[ei] - batch_start_offset;
auto end_offset = dataset.index.offsets[ei+1] - batch_start_offset;
auto dropout_mask_size = models[mi]->get_dropout_mask_size(batch_features_data + start_offset, batch_features_data + end_offset);
fill_mask_rand(dropout_mask, (dropout_mask_size + 63) / 64, dropout_prob_log);
float t = models[mi]->predict(batch_features_data + start_offset, batch_features_data + end_offset, norm, dropout_mask, dropout_mult);
float expnyt = exp(-y*t);
models[mi]->update(batch_features_data + start_offset, batch_features_data + end_offset, norm, -y * expnyt / (1+expnyt), dropout_mask, dropout_mult);
uint i = ei - batch_start_index;
ts[i] += t;
tc[i] ++;
}
}
}
for (uint i = 0; i < batch_end_index - batch_start_index; ++ i)
loss += log(1+exp(-dataset.index.labels[i + batch_start_index]*ts[i]/tc[i]));
cnt += batch_end_index - batch_start_index;
}
std::cout << cnt << " examples processed in " << (time(nullptr) - start_time) << " seconds, loss = " << std::fixed << std::setprecision(5) << (loss / cnt) << std::endl;
return loss;
}
template <typename M>
ffm_double evaluate_on_dataset(const std::vector<M*> & models, const ffm_dataset & dataset) {
time_t start_time = time(nullptr);
std::cout << " Evaluating... ";
std::cout.flush();
auto batches = generate_batches(dataset.index, false);
uint64_t dropout_mask[dropout_mask_max_size];
fill_mask_ones(dropout_mask, dropout_mask_max_size);
ffm_double loss = 0.0;
ffm_uint cnt = 0;
std::vector<ffm_float> predictions(dataset.index.size);
// Iterate over batches, read each and then iterate over examples
#pragma omp parallel for schedule(dynamic, 1) reduction(+: loss) reduction(+: cnt)
for (ffm_uint bi = 0; bi < batches.size(); ++ bi) {
auto batch_start_index = batches[bi].first;
auto batch_end_index = batches[bi].second;
auto batch_start_offset = dataset.index.offsets[batch_start_index];
auto batch_end_offset = dataset.index.offsets[batch_end_index];
std::vector<ffm_feature> batch_features = ffm_read_batch(dataset.data_file_name, batch_start_offset, batch_end_offset);
ffm_feature * batch_features_data = batch_features.data();
for (auto ei = batch_start_index; ei < batch_end_index; ++ ei) {
ffm_float y = dataset.index.labels[ei];
ffm_float norm = dataset.index.norms[ei];
auto start_offset = dataset.index.offsets[ei] - batch_start_offset;
auto end_offset = dataset.index.offsets[ei+1] - batch_start_offset;
float ts = 0.0;
uint tc = 0;
for (uint mi = 0; mi < models.size(); ++ mi) {
ts += models[mi]->predict(batch_features_data + start_offset, batch_features_data + end_offset, norm, dropout_mask, 1);
tc ++;
}
loss += log(1+exp(-y*ts/tc));
predictions[ei] = 1 / (1+exp(-ts/tc));
}
cnt += batch_end_index - batch_start_index;
}
// Compute map metric
ffm_double map = compute_map(dataset.index, predictions);
std::cout << cnt << " examples processed in " << (time(nullptr) - start_time) << " seconds, loss = " << std::fixed << std::setprecision(5) << (loss / cnt) << ", map = " << map << std::endl;
return loss;
}
template <typename M>
void predict_on_dataset(const std::vector<M*> & models, const ffm_dataset & dataset, std::ostream & out) {
time_t start_time = time(nullptr);
std::cout << " Predicting... ";
std::cout.flush();
auto batches = generate_batches(dataset.index, false);
uint64_t dropout_mask[dropout_mask_max_size];
fill_mask_ones(dropout_mask, dropout_mask_max_size);
ffm_ulong cnt = 0;
// Iterate over batches, read each and then iterate over examples
for (ffm_ulong bi = 0; bi < batches.size(); ++ bi) {
auto batch_start_index = batches[bi].first;
auto batch_end_index = batches[bi].second;
auto batch_start_offset = dataset.index.offsets[batch_start_index];
auto batch_end_offset = dataset.index.offsets[batch_end_index];
std::vector<ffm_feature> batch_features = ffm_read_batch(dataset.data_file_name, batch_start_offset, batch_end_offset);
ffm_feature * batch_features_data = batch_features.data();
for (auto ei = batch_start_index; ei < batch_end_index; ++ ei) {
ffm_float norm = dataset.index.norms[ei];
auto start_offset = dataset.index.offsets[ei] - batch_start_offset;
auto end_offset = dataset.index.offsets[ei+1] - batch_start_offset;
float ts = 0.0;
uint tc = 0;
for (uint mi = 0; mi < models.size(); ++ mi) {
ts += models[mi]->predict(batch_features_data + start_offset, batch_features_data + end_offset, norm, dropout_mask, 1);
tc ++;
}
out << 1/(1+exp(-ts/tc)) << std::endl;
}
cnt += batch_end_index - batch_start_index;
}
std::cout << cnt << " examples processed in " << (time(nullptr) - start_time) << " seconds" << std::endl;
}
ffm_dataset open_dataset(const std::string & file_name) {
ffm_dataset res;
std::cout << "Loading " << file_name << ".index... ";
std::cout.flush();
res.index = ffm_read_index(file_name + ".index");
res.data_file_name = file_name + ".data";
std::cout << res.index.size << " examples" << std::endl;
return res;
}
class program_options {
boost::program_options::options_description desc;
public:
std::string train_file_name;
std::string val_file_name;
std::string test_file_name;
std::string pred_file_name;
std::string model_name;
uint n_epochs;
uint n_threads;
uint n_models;
uint seed;
bool restricted;
uint dropout_prob_log;
float eta, lambda;
public:
program_options(int ac, char* av[]):
desc("Allowed options"), model_name("ffm"), n_epochs(10), n_threads(4), n_models(1), seed(2017),
dropout_prob_log(1), eta(0), lambda(0)
{
using namespace boost::program_options;
desc.add_options()
("help", "train dataset file")
("model", value<std::string>(&model_name), "model name")
("train", value<std::string>(&train_file_name)->required(), "train dataset file")
("val", value<std::string>(&val_file_name), "validation dataset file")
("test", value<std::string>(&test_file_name), "test dataset file")
("pred", value<std::string>(&pred_file_name), "file to save predictions")
("epochs", value<uint>(&n_epochs), "number of epochs (default 10)")
("threads", value<uint>(&n_threads), "number of threads (default 4)")
("average", value<uint>(&n_models), "number of models to average (default 1)")
("seed", value<uint>(&seed), "random seed")
("dropout-log", value<uint>(&dropout_prob_log), "binary log of dropout probability (default 1)")
("eta", value<float>(&eta), "learning rate")
("lambda", value<float>(&lambda), "l2 regularization coeff")
("restricted", "restrict feature interactions to (E+C) * (C+A)")
;
variables_map vm;
store(parse_command_line(ac, av, desc), vm);
if (vm.count("help") > 0) {
std::cout << desc << std::endl;
exit(0);
}
restricted = vm.count("restricted") > 0;
notify(vm);
}
};
template <typename M>
void apply(const std::vector<M*> & models, program_options & opts) {
using namespace std;
if (opts.val_file_name.empty()) { // No validation set given, just train
auto ds_train = open_dataset(opts.train_file_name);
for (ffm_uint epoch = 0; epoch < opts.n_epochs; ++ epoch) {
cout << "Epoch " << epoch << "..." << endl;
train_on_dataset(models, ds_train, opts.dropout_prob_log);
}
} else { // Train with validation each epoch
auto ds_train = open_dataset(opts.train_file_name);
auto ds_val = open_dataset(opts.val_file_name);
for (ffm_uint epoch = 0; epoch < opts.n_epochs; ++ epoch) {
cout << "Epoch " << epoch << "..." << endl;
train_on_dataset(models, ds_train, opts.dropout_prob_log);
evaluate_on_dataset(models, ds_val);
}
}
if (!opts.test_file_name.empty() && !opts.pred_file_name.empty()) {
auto ds_test = open_dataset(opts.test_file_name);
ofstream out(opts.pred_file_name);
predict_on_dataset(models, ds_test, out);
}
}
int main(int ac, char* av[]) {
program_options opts(ac, av);
// Init global state
omp_set_num_threads(opts.n_threads);
rnd.seed(opts.seed);
// Run model
if (opts.model_name == "ffm") {
float eta = opts.eta > 0 ? opts.eta : 0.2;
float lambda = opts.lambda > 0 ? opts.lambda : 0.00002;
std::vector<ffm_model*> models;
for (uint i = 0; i < opts.n_models; ++ i)
models.push_back(new ffm_model(opts.seed + 100 + i * 17, opts.restricted, eta, lambda));
apply(models, opts);
} else if (opts.model_name == "ffm-nn") {
float eta = opts.eta > 0 ? opts.eta : 0.05;
float lambda = opts.lambda > 0 ? opts.lambda : 0.00002;
std::vector<ffm_nn_model*> models;
for (uint i = 0; i < opts.n_models; ++ i)
models.push_back(new ffm_nn_model(opts.seed + 100 + i * 17, opts.restricted, eta, lambda, 0.0001));
apply(models, opts);
} else if (opts.model_name == "ftrl") {
std::vector<ftrl_model*> models;
for (uint i = 0; i < opts.n_models; ++ i)
models.push_back(new ftrl_model(24, 1.0, 2.0, 2e-4, 5e-4));
apply(models, opts);
} else if (opts.model_name == "nn") {
float eta = opts.eta > 0 ? opts.eta : 0.02;
float lambda = opts.lambda > 0 ? opts.lambda : 0.00002;
std::vector<nn_model*> models;
for (uint i = 0; i < opts.n_models; ++ i)
models.push_back(new nn_model(opts.seed + 100 + i * 17, eta, lambda));
apply(models, opts);
} else {
throw std::runtime_error(std::string("Unknown model ") + opts.model_name);
}
std::cout << "Done." << std::endl;
}