Skip to content

Commit 095a04d

Browse files
committed
feat(model): add model_config.summaries_set for PCOC and scalars
Configure training TensorBoard summaries on model_config (not eval_config). PCOC reports global predicted/observed CTR ratio; scalars supports feature-sliced mean predictions with numeric RawFeature matching. Fixes #532
1 parent cbb4355 commit 095a04d

7 files changed

Lines changed: 223 additions & 0 deletions

File tree

easy_rec/python/model/easy_rec_estimator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def _train_model_fn(self, features, labels, run_config):
162162
is_training=True)
163163
predict_dict = model.build_predict_graph()
164164
loss_dict = model.build_loss_graph()
165+
model.build_summary_graph()
165166

166167
regularization_losses = tf.get_collection(
167168
tf.GraphKeys.REGULARIZATION_LOSSES)

easy_rec/python/model/easy_rec_model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ def build_loss_graph(self):
178178
def build_metric_graph(self, eval_config):
179179
return self._metric_dict
180180

181+
def build_summary_graph(self):
182+
return
183+
181184
@abstractmethod
182185
def get_outputs(self):
183186
pass

easy_rec/python/model/rank_model.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,90 @@ def build_metric_graph(self, eval_config):
509509
))
510510
return self._metric_dict
511511

512+
def _resolve_pred_tensor(self, pred_name):
513+
if pred_name not in self._prediction_dict:
514+
raise ValueError(
515+
'summaries pred_name "%s" not found in prediction_dict keys: %s' %
516+
(pred_name, sorted(self._prediction_dict.keys())))
517+
pred = tf.to_float(self._prediction_dict[pred_name])
518+
if pred_name == 'logits' or pred_name.startswith('logits_'):
519+
pred = tf.sigmoid(pred)
520+
# multi-class probs: take positive class when rank-2
521+
if pred.shape.ndims is not None and pred.shape.ndims > 1:
522+
if pred.shape.ndims == 2 and int(pred.shape[-1]) == 2:
523+
pred = pred[:, 1]
524+
else:
525+
pred = tf.reshape(pred, [-1])
526+
return pred
527+
528+
def _build_feature_mask(self, feature_name, feature_value):
529+
if feature_name not in self._feature_dict:
530+
raise ValueError(
531+
'summaries feature_name "%s" not found in feature_dict keys: %s' %
532+
(feature_name, sorted(self._feature_dict.keys())))
533+
feat = self._feature_dict[feature_name]
534+
if isinstance(feat, tf.SparseTensor):
535+
dense = tf.sparse_to_dense(
536+
feat.indices,
537+
feat.dense_shape,
538+
feat.values,
539+
default_value='' if feat.values.dtype == tf.string else 0)
540+
feat = tf.reshape(dense, [-1])
541+
else:
542+
feat = tf.reshape(feat, [-1])
543+
if feat.dtype in (tf.float32, tf.float64, tf.int32, tf.int64):
544+
try:
545+
target = float(feature_value)
546+
except (TypeError, ValueError):
547+
target = None
548+
if target is not None:
549+
return tf.equal(tf.to_float(feat), tf.constant(target, dtype=tf.float32))
550+
if feat.dtype != tf.string:
551+
feat = tf.as_string(feat)
552+
return tf.equal(feat, str(feature_value))
553+
554+
def _masked_mean(self, values, mask):
555+
values = tf.reshape(tf.to_float(values), [-1])
556+
masked = tf.boolean_mask(values, mask)
557+
count = tf.size(masked)
558+
return tf.cond(
559+
count > 0,
560+
lambda: tf.reduce_mean(masked),
561+
lambda: tf.constant(0.0, dtype=tf.float32))
562+
563+
def _build_summary_impl(self, summary):
564+
summary_type = summary.WhichOneof('summary')
565+
if summary_type == 'pcoc':
566+
if self._labels is None:
567+
return
568+
pred_name = summary.pcoc.pred_name or 'probs'
569+
preds = self._resolve_pred_tensor(pred_name)
570+
label = tf.to_float(self._labels[self._label_name])
571+
label = tf.reshape(label, [-1])
572+
predicted_ctr = tf.reduce_mean(preds)
573+
observed_ctr = tf.reduce_mean(label)
574+
epsilon = summary.pcoc.epsilon
575+
pcoc = predicted_ctr / (observed_ctr + epsilon)
576+
tf.summary.scalar('summary/predicted_ctr', predicted_ctr)
577+
tf.summary.scalar('summary/observed_ctr', observed_ctr)
578+
tf.summary.scalar('summary/pcoc', pcoc)
579+
elif summary_type == 'scalars':
580+
cfg = summary.scalars
581+
pred_name = cfg.pred_name or 'probs'
582+
preds = self._resolve_pred_tensor(pred_name)
583+
if cfg.HasField('feature_name') and cfg.HasField('feature_value'):
584+
mask = self._build_feature_mask(cfg.feature_name, cfg.feature_value)
585+
value = self._masked_mean(preds, mask)
586+
else:
587+
value = tf.reduce_mean(preds)
588+
tf.summary.scalar('summary/%s' % cfg.name, value)
589+
elif summary_type is not None:
590+
raise ValueError('unsupported summary type: %s' % summary_type)
591+
592+
def build_summary_graph(self):
593+
for summary in self._base_model_config.summaries_set:
594+
self._build_summary_impl(summary)
595+
512596
def _get_outputs_impl(self, loss_type, num_class=1, suffix=''):
513597
binary_loss_set = {
514598
LossType.F1_REWEIGHTED_LOSS,

easy_rec/python/protos/easy_rec_model.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import "easy_rec/python/protos/pdn.proto";
3030
import "easy_rec/python/protos/dssm_senet.proto";
3131
import "easy_rec/python/protos/simi.proto";
3232
import "easy_rec/python/protos/dat.proto";
33+
import "easy_rec/python/protos/summary.proto";
3334
// for input performance test
3435
message DummyModel {
3536
}
@@ -158,4 +159,9 @@ message EasyRecModel {
158159

159160
// label name for rank_model to select one label between multiple labels
160161
optional string label_name = 18;
162+
163+
// Training TensorBoard summaries, e.g.:
164+
// summaries_set { pcoc {} }
165+
// summaries_set { scalars { name: "c1_1005_pred" feature_name: "c1" feature_value: "1005" } }
166+
repeated ModelSummaries summaries_set = 19;
161167
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
syntax = "proto2";
2+
package protos;
3+
4+
message PCOC {
5+
optional float epsilon = 1 [default = 1e-7];
6+
optional string pred_name = 2 [default = "probs"];
7+
}
8+
9+
message Scalars {
10+
required string name = 1;
11+
optional string pred_name = 2 [default = "probs"];
12+
optional string feature_name = 3;
13+
optional string feature_value = 4;
14+
}
15+
16+
message ModelSummaries {
17+
oneof summary {
18+
PCOC pcoc = 1;
19+
Scalars scalars = 2;
20+
}
21+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# -*- encoding:utf-8 -*-
2+
# Copyright (c) Alibaba, Inc. and its affiliates.
3+
from __future__ import division
4+
5+
import logging
6+
import unittest
7+
8+
from google.protobuf import text_format
9+
10+
from easy_rec.python.protos.easy_rec_model_pb2 import EasyRecModel
11+
from easy_rec.python.protos.summary_pb2 import ModelSummaries
12+
from easy_rec.python.model.rank_model import RankModel
13+
14+
if __name__ == '__main__':
15+
import tensorflow as tf
16+
if tf.__version__ >= '2.0':
17+
tf = tf.compat.v1
18+
tf.disable_eager_execution()
19+
20+
21+
def _tf_v1():
22+
import tensorflow as tf
23+
return tf.compat.v1 if tf.__version__ >= '2.0' else tf
24+
25+
26+
class _FakeRankModel(object):
27+
_resolve_pred_tensor = RankModel._resolve_pred_tensor
28+
_build_feature_mask = RankModel._build_feature_mask
29+
_masked_mean = RankModel._masked_mean
30+
31+
32+
def _run_summary_tags(model, summary_text):
33+
tf = _tf_v1()
34+
tf.reset_default_graph()
35+
model = model()
36+
summary = ModelSummaries()
37+
text_format.Parse(summary_text, summary)
38+
RankModel._build_summary_impl(model, summary)
39+
with tf.Session() as sess:
40+
summary_str = sess.run(tf.summary.merge_all())
41+
summary_proto = tf.Summary()
42+
summary_proto.ParseFromString(summary_str)
43+
return {v.tag: v.simple_value for v in summary_proto.value}
44+
45+
46+
class SummariesSetTest(unittest.TestCase):
47+
48+
def setUp(self):
49+
logging.info('Testing %s.%s' % (type(self).__name__, self._testMethodName))
50+
51+
def test_proto_and_pcoc_graph(self):
52+
model = EasyRecModel()
53+
text_format.Parse(
54+
'model_class: "DeepFM" deepfm {} summaries_set { pcoc {} }', model)
55+
self.assertEqual(model.summaries_set[0].WhichOneof('summary'), 'pcoc')
56+
57+
class _M(_FakeRankModel):
58+
59+
def __init__(self):
60+
tf = _tf_v1()
61+
self._labels = {'label': tf.constant([1, 0, 1, 0], dtype=tf.float32)}
62+
self._prediction_dict = {
63+
'probs': tf.constant([0.8, 0.4, 0.6, 0.2], dtype=tf.float32)
64+
}
65+
self._feature_dict = {}
66+
self._label_name = 'label'
67+
68+
values = _run_summary_tags(_M, 'pcoc { epsilon: 1e-7 }')
69+
self.assertAlmostEqual(values['summary/pcoc'], 1.0, places=5)
70+
71+
def test_scalars_feature_slice(self):
72+
class _M(_FakeRankModel):
73+
74+
def __init__(self):
75+
tf = _tf_v1()
76+
self._labels = {'label': tf.constant([1, 0], dtype=tf.float32)}
77+
self._prediction_dict = {
78+
'probs': tf.constant([0.9, 0.1], dtype=tf.float32)
79+
}
80+
self._feature_dict = {'c1': tf.constant([1005.0, 1002.0], dtype=tf.float32)}
81+
self._label_name = 'label'
82+
83+
values = _run_summary_tags(
84+
_M,
85+
'scalars { name: "c1_1005_pred" feature_name: "c1" feature_value: "1005" }')
86+
self.assertAlmostEqual(values['summary/c1_1005_pred'], 0.9, places=5)
87+
88+
89+
if __name__ == '__main__':
90+
unittest.main()

samples/model_config/deepfm_combo_on_avazu_ctr.config

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,24 @@ model_config:{
365365
l2_regularization: 1e-5
366366
}
367367
# embedding_regularization: 1e-7
368+
369+
# Training TensorBoard summaries (model_config.summaries_set, not eval_config)
370+
# PCOC = mean(pred)/mean(label); tags: summary/pcoc, summary/predicted_ctr, summary/observed_ctr
371+
summaries_set {
372+
pcoc {
373+
# pred_name: "probs" # prediction_dict key; "logits" applies sigmoid first
374+
# epsilon: 1e-7 # divisor stabilizer when observed ctr is near zero
375+
}
376+
}
377+
# Feature-sliced scalar (mean pred on c1=="1005"); tag: summary/c1_1005_pred
378+
summaries_set {
379+
scalars {
380+
name: "c1_1005_pred"
381+
pred_name: "probs"
382+
feature_name: "c1"
383+
feature_value: "1005"
384+
}
385+
}
368386
}
369387

370388
export_config {

0 commit comments

Comments
 (0)