/
kernel_estimators.py
339 lines (287 loc) · 13.5 KB
/
kernel_estimators.py
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
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Estimators that combine explicit kernel mappings with linear models."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import six
from tensorflow.contrib import layers
from tensorflow.contrib.kernel_methods.python.mappers import dense_kernel_mapper as dkm
from tensorflow.contrib.learn.python.learn.estimators import estimator
from tensorflow.contrib.learn.python.learn.estimators import head as head_lib
from tensorflow.contrib.learn.python.learn.estimators import linear
from tensorflow.contrib.learn.python.learn.estimators import prediction_key
from tensorflow.python.ops import array_ops
from tensorflow.python.platform import tf_logging as logging
_FEATURE_COLUMNS = "feature_columns"
_KERNEL_MAPPERS = "kernel_mappers"
_OPTIMIZER = "optimizer"
def _check_valid_kernel_mappers(kernel_mappers):
"""Checks that the input kernel_mappers are valid."""
if kernel_mappers is None:
return True
for kernel_mappers_list in six.itervalues(kernel_mappers):
for kernel_mapper in kernel_mappers_list:
if not isinstance(kernel_mapper, dkm.DenseKernelMapper):
return False
return True
def _check_valid_head(head):
"""Returns true if the provided head is supported."""
if head is None:
return False
# pylint: disable=protected-access
return isinstance(head, head_lib._BinaryLogisticHead) or isinstance(
head, head_lib._MultiClassHead)
# pylint: enable=protected-access
def _update_features_and_columns(features, feature_columns,
kernel_mappers_dict):
"""Updates features and feature_columns based on provided kernel mappers.
Currently supports the update of `RealValuedColumn`s only.
Args:
features: Initial features dict. The key is a `string` (feature column name)
and the value is a tensor.
feature_columns: Initial iterable containing all the feature columns to be
consumed (possibly after being updated) by the model. All items should be
instances of classes derived from `FeatureColumn`.
kernel_mappers_dict: A dict from feature column (type: _FeatureColumn) to
objects inheriting from KernelMapper class.
Returns:
updated features and feature_columns based on provided kernel_mappers_dict.
"""
if kernel_mappers_dict is None:
return features, feature_columns
# First construct new columns and features affected by kernel_mappers_dict.
mapped_features = dict()
mapped_columns = set()
for feature_column in kernel_mappers_dict:
column_name = feature_column.name
# Currently only mappings over RealValuedColumns are supported.
if not isinstance(feature_column, layers.feature_column._RealValuedColumn): # pylint: disable=protected-access
logging.warning(
"Updates are currently supported on RealValuedColumns only. Metadata "
"for FeatureColumn {} will not be updated.".format(column_name))
continue
mapped_column_name = column_name + "_MAPPED"
# Construct new feature columns based on provided kernel_mappers.
column_kernel_mappers = kernel_mappers_dict[feature_column]
new_dim = sum([mapper.output_dim for mapper in column_kernel_mappers])
mapped_columns.add(
layers.feature_column.real_valued_column(mapped_column_name, new_dim))
# Get mapped features by concatenating mapped tensors (one mapped tensor
# per kernel mappers from the list of kernel mappers corresponding to each
# feature column).
output_tensors = []
for kernel_mapper in column_kernel_mappers:
output_tensors.append(kernel_mapper.map(features[column_name]))
tensor = array_ops.concat(output_tensors, 1)
mapped_features[mapped_column_name] = tensor
# Finally update features dict and feature_columns.
features = features.copy()
features.update(mapped_features)
feature_columns = set(feature_columns)
feature_columns.update(mapped_columns)
return features, feature_columns
def _kernel_model_fn(features, labels, mode, params, config=None):
"""model_fn for the Estimator using kernel methods.
Args:
features: `Tensor` or dict of `Tensor` (depends on data passed to `fit`).
labels: `Tensor` of shape [batch_size, 1] or [batch_size] labels of
dtype `int32` or `int64` in the range `[0, n_classes)`.
mode: Defines whether this is training, evaluation or prediction. See
`ModeKeys`.
params: A dict of hyperparameters.
The following hyperparameters are expected:
* head: A `Head` instance.
* feature_columns: An iterable containing all the feature columns used by
the model.
* optimizer: string, `Optimizer` object, or callable that defines the
optimizer to use for training. If `None`, will use a FTRL optimizer.
* kernel_mappers: Dictionary of kernel mappers to be applied to the input
features before training.
config: `RunConfig` object to configure the runtime settings.
Returns:
A `ModelFnOps` instance.
Raises:
ValueError: If mode is not any of the `ModeKeys`.
"""
feature_columns = params[_FEATURE_COLUMNS]
kernel_mappers = params[_KERNEL_MAPPERS]
updated_features, updated_columns = _update_features_and_columns(
features, feature_columns, kernel_mappers)
params[_FEATURE_COLUMNS] = updated_columns
return linear._linear_model_fn( # pylint: disable=protected-access
updated_features, labels, mode, params, config)
class _KernelEstimator(estimator.Estimator):
"""Generic kernel-based linear estimator."""
def __init__(self,
feature_columns=None,
model_dir=None,
weight_column_name=None,
head=None,
optimizer=None,
kernel_mappers=None,
config=None):
"""Constructs a `_KernelEstimator` object."""
if not feature_columns and not kernel_mappers:
raise ValueError(
"You should set at least one of feature_columns, kernel_mappers.")
if not _check_valid_kernel_mappers(kernel_mappers):
raise ValueError("Invalid kernel mappers.")
if not _check_valid_head(head):
raise ValueError(
"head type: {} is not supported. Supported head types: "
"_BinaryLogisticHead, _MultiClassHead.".format(type(head)))
params = {
"head": head,
_FEATURE_COLUMNS: feature_columns or [],
_OPTIMIZER: optimizer,
_KERNEL_MAPPERS: kernel_mappers,
}
super(_KernelEstimator, self).__init__(
model_fn=_kernel_model_fn,
model_dir=model_dir,
config=config,
params=params)
class KernelLinearClassifier(_KernelEstimator):
"""Linear classifier using kernel methods as feature preprocessing.
It trains a linear model after possibly mapping initial input features into
a mapped space using explicit kernel mappings. Due to the kernel mappings,
training a linear classifier in the mapped (output) space can detect
non-linearities in the input space.
The user can provide a list of kernel mappers to be applied to all or a subset
of existing feature_columns. This way, the user can effectively provide 2
types of feature columns:
* those passed as elements of feature_columns in the classifier's constructor
* those appearing as a key of the kernel_mappers dict.
If a column appears in feature_columns only, no mapping is applied to it. If
it appears as a key in kernel_mappers, the corresponding kernel mappers are
applied to it. Note that it is possible that a column appears in both places.
Currently kernel_mappers are supported for _RealValuedColumns only.
Example usage:
```
real_column_a = real_valued_column(name='real_column_a',...)
sparse_column_b = sparse_column_with_hash_bucket(...)
kernel_mappers = {real_column_a : [RandomFourierFeatureMapper(...)]}
optimizer = ...
# real_column_a is used as a feature in both its initial and its transformed
# (mapped) form. sparse_column_b is not affected by kernel mappers.
kernel_classifier = KernelLinearClassifier(
feature_columns=[real_column_a, sparse_column_b],
model_dir=...,
optimizer=optimizer,
kernel_mappers=kernel_mappers)
# real_column_a is used as a feature in its transformed (mapped) form only.
# sparse_column_b is not affected by kernel mappers.
kernel_classifier = KernelLinearClassifier(
feature_columns=[sparse_column_b],
model_dir=...,
optimizer=optimizer,
kernel_mappers=kernel_mappers)
# Input builders
def train_input_fn: # returns x, y
...
def eval_input_fn: # returns x, y
...
kernel_classifier.fit(input_fn=train_input_fn)
kernel_classifier.evaluate(input_fn=eval_input_fn)
kernel_classifier.predict(...)
```
Input of `fit` and `evaluate` should have following features, otherwise there
will be a `KeyError`:
* if `weight_column_name` is not `None`, a feature with
`key=weight_column_name` whose value is a `Tensor`.
* for each `column` in `feature_columns`:
- if `column` is a `SparseColumn`, a feature with `key=column.name`
whose `value` is a `SparseTensor`.
- if `column` is a `WeightedSparseColumn`, two features: the first with
`key` the id column name, the second with `key` the weight column name.
Both features' `value` must be a `SparseTensor`.
- if `column` is a `RealValuedColumn`, a feature with `key=column.name`
whose `value` is a `Tensor`.
"""
def __init__(self,
feature_columns=None,
model_dir=None,
n_classes=2,
weight_column_name=None,
optimizer=None,
kernel_mappers=None,
config=None):
"""Construct a `KernelLinearClassifier` estimator object.
Args:
feature_columns: An iterable containing all the feature columns used by
the model. All items in the set should be instances of classes derived
from `FeatureColumn`.
model_dir: Directory to save model parameters, graph etc. This can also be
used to load checkpoints from the directory into an estimator to
continue training a previously saved model.
n_classes: number of label classes. Default is binary classification.
Note that class labels are integers representing the class index (i.e.
values from 0 to n_classes-1). For arbitrary label values (e.g. string
labels), convert to class indices first.
weight_column_name: A string defining feature column name representing
weights. It is used to down weight or boost examples during training. It
will be multiplied by the loss of the example.
optimizer: The optimizer used to train the model. If specified, it should
be an instance of `tf.Optimizer`. If `None`, the Ftrl optimizer is used
by default.
kernel_mappers: Dictionary of kernel mappers to be applied to the input
features before training a (linear) model. Keys are feature columns and
values are lists of mappers to be applied to the corresponding feature
column. Currently only _RealValuedColumns are supported and therefore
all mappers should conform to the `DenseKernelMapper` interface (see
./mappers/dense_kernel_mapper.py).
config: `RunConfig` object to configure the runtime settings.
Returns:
A `KernelLinearClassifier` estimator.
Raises:
ValueError: if n_classes < 2.
ValueError: if neither feature_columns nor kernel_mappers are provided.
ValueError: if mappers provided as kernel_mappers values are invalid.
"""
super(KernelLinearClassifier, self).__init__(
feature_columns=feature_columns,
model_dir=model_dir,
weight_column_name=weight_column_name,
head=head_lib.multi_class_head(
n_classes=n_classes, weight_column_name=weight_column_name),
optimizer=optimizer,
kernel_mappers=kernel_mappers,
config=config)
def predict_classes(self, input_fn=None):
"""Runs inference to determine the predicted class per instance.
Args:
input_fn: The input function providing features.
Returns:
A generator of predicted classes for the features provided by input_fn.
Each predicted class is represented by its class index (i.e. integer from
0 to n_classes-1)
"""
key = prediction_key.PredictionKey.CLASSES
predictions = super(KernelLinearClassifier, self).predict(
input_fn=input_fn, outputs=[key])
return (pred[key] for pred in predictions)
def predict_proba(self, input_fn=None):
"""Runs inference to determine the class probability predictions.
Args:
input_fn: The input function providing features.
Returns:
A generator of predicted class probabilities for the features provided by
input_fn.
"""
key = prediction_key.PredictionKey.PROBABILITIES
predictions = super(KernelLinearClassifier, self).predict(
input_fn=input_fn, outputs=[key])
return (pred[key] for pred in predictions)