Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(363)

Side by Side Diff: appengine/findit/crash/loglinear/model.py

Issue 2625073003: [Predator] Add MetaWeight and MetaFeatureValue to group multiple weights and features together. (Closed)
Patch Set: Fix nits. Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2016 The Chromium Authors. All rights reserved. 1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 # TODO(http://crbug.com/669639): there are lots of ways to make the code 5 # TODO(http://crbug.com/669639): there are lots of ways to make the code
6 # in this file better. We avoid having separate todos per task; instead 6 # in this file better. We avoid having separate todos per task; instead
7 # see that meta-issue ticket. 7 # see that meta-issue ticket.
8 8
9 import logging 9 import logging
10 import math 10 import math
(...skipping 25 matching lines...) Expand all
36 where particular values for that random variable called ``y``. The 36 where particular values for that random variable called ``y``. The
37 partition function is called ``Z``. And (somewhat non-conventionally) 37 partition function is called ``Z``. And (somewhat non-conventionally)
38 we will call the log of the partition function ``zeta``. 38 we will call the log of the partition function ``zeta``.
39 39
40 This class is distinct from ``LogLinearModel`` in that we do not require 40 This class is distinct from ``LogLinearModel`` in that we do not require
41 a specification of ``Y``. This class is sufficient for determining 41 a specification of ``Y``. This class is sufficient for determining
42 which ``y`` is the most likely, though it can only return a "score" 42 which ``y`` is the most likely, though it can only return a "score"
43 rather than returning a probability per se. 43 rather than returning a probability per se.
44 """ 44 """
45 45
46 def __init__(self, feature_function, weights, epsilon=None): 46 def __init__(self, meta_feature, meta_weight, epsilon=None):
47 """Construct a new model with the given weights and feature function. 47 """Construct a new model with the meta_feature and meta_weight.
48 48
49 Args: 49 Args:
50 feature_function: A function ``X -> Y -> list(FeatureValue)``. N.B., 50 wrapped_feature: A function ``X -> Y -> list(FeatureValue)``. N.B.,
chanli 2017/01/19 04:26:06 Not in argument list above?
Sharu Jiang 2017/01/19 23:49:52 Oops, should have deleted it.
51 for all ``x`` and ``y`` the length of ``feature_function(x)(y)`` 51 for all ``x`` and ``y`` the length of ``wrapped_feature(x)(y)``
52 must be the same as the length of ``weights``. 52 must be the same as the length of ``weights``.
53 weights (dict of float): the weights for the features. The keys of 53 meta_feature (MetaFeature): A function ``X -> Y -> MetaFeatureValue``.
54 the dictionary are the names of the feature that weight is 54 N.B., for all ``x`` and ``y`` the length of ``wrapped_feature(x)(y)``
55 must be the same as the length of ``weights``.
56 All features.
57 meta_weight (MetaWeight): All weights. the weights for the features.
58 The keys of the dictionary are the names of the feature that weight is
55 for. We take this argument as a dict rather than as a list so that 59 for. We take this argument as a dict rather than as a list so that
56 callers needn't worry about what order to provide the weights in. 60 callers needn't worry about what order to provide the weights in.
57 epsilon (float): The absolute-error threshold for considering a 61 epsilon (float): The absolute-error threshold for considering a
58 weight to be "equal to zero". N.B., this should be a positive 62 weight to be "equal to zero". N.B., this should be a positive
59 number, as we will compare it against the absolute value of 63 number, as we will compare it against the absolute value of
60 each weight. 64 each weight.
61 """ 65 """
62 if epsilon is None: 66 if epsilon is None:
63 self._epsilon = EPSILON 67 self._epsilon = EPSILON
64 else: 68 else:
65 self._epsilon = epsilon 69 self._epsilon = epsilon
chanli 2017/01/19 04:26:06 This if else can be simplified to self._epsilon
lijeffrey 2017/01/19 15:23:35 even simpler is self._epsilon = epsilon or EPSILON
Sharu Jiang 2017/01/19 23:49:52 if epsilon would be False if epsilon equals to zer
66 70
67 # TODO(crbug.com/680207) Filter zero weights, use sparse representaion of 71 self._meta_weight = meta_weight
68 # weight covector. 72 self._meta_weight.DropZeroWeights(self._epsilon)
69 self._weights = {
70 name: weight for name, weight in weights.iteritems()
71 if self.IsNonZeroWeight(weight)
72 }
73 73
74 self._quadrance = None 74 self._quadrance = None
75
76 # TODO(crbug.com/674752): we need better names for ``self._features``. 75 # TODO(crbug.com/674752): we need better names for ``self._features``.
77 def _Features(x): 76 def _Features(x):
78 """Wrap ``feature_function`` to memoize things and ensure types. 77 """Wrap ``wrapped_feature`` to memoize things and ensure types.
79 78
80 This outer wrapping takes each ``x`` to a memoized instance of 79 This outer wrapping takes each ``x`` to a memoized instance of
81 ``_FeaturesGivenX``. That is, for each ``x`` we return a 80 ``_FeaturesGivenX``. That is, for each ``x`` we return a
82 ``MemoizedFunction`` from ``Y`` to ``dict(str to FeatureValue)``. 81 ``MemoizedFunction`` from ``Y`` to ``dict(str to FeatureValue)``.
83 """ 82 """
84 fx = feature_function(x)
85 def _FeaturesGivenX(y):
86 """Wrap ``feature_function(x)`` to ensure appropriate types.
87
88 This inner wrapper ensures that the resulting ``FeatureValue``
89 array has the same length as the weight covector.
90 """
91 fxy = fx(y)
92 # N.B., we're assuming that ``len(self.weights)`` is O(1).
93 assert len(fxy) == len(self.weights), TypeError(
94 "vector length mismatch: %d != %d" % (len(fxy), len(self.weights)))
95 return fxy
96
97 # Memoize on ``Y``, to ensure we don't need to recompute 83 # Memoize on ``Y``, to ensure we don't need to recompute
98 # ``FeatureValue``s nor recheck the lengths. 84 # ``FeatureValue``s nor recheck the lengths.
99 return MemoizedFunction(_FeaturesGivenX) 85 return MemoizedFunction(meta_feature(x))
100 86
101 # Memoize on ``X``, to ensure we share the memo tables on ``Y``. 87 # Memoize on ``X``, to ensure we share the memo tables on ``Y``.
102 self._features = MemoizedFunction(_Features) 88 self._features = MemoizedFunction(_Features)
103 89
104 # TODO(crbug.com/674752): we need better names for ``self._scores``. 90 # TODO(crbug.com/674752): we need better names for ``self._scores``.
105 # N.B., this is just the inner product of ``self.weights`` 91 # N.B., this is just the inner product of ``self._meta_weight``
106 # against ``self._features(x)``. If we can compute this in some 92 # against ``self._features(x)``. If we can compute this in some
107 # more efficient way, we should. In particular, we will want to 93 # more efficient way, we should. In particular, we will want to
108 # make the weights sparse, in which case we need to use a sparse 94 # make the weights sparse, in which case we need to use a sparse
109 # variant of the dot product. 95 # variant of the dot product.
110 self._scores = MemoizedFunction(lambda x: self._features(x).map( 96 self._scores = MemoizedFunction(lambda x: self._features(x).map(
111 lambda fxy: math.fsum(self.SingleFeatureScore(feature) 97 lambda fxy: self._meta_weight * fxy))
112 for feature in fxy.itervalues())))
113
114 def IsNonZeroWeight(self, weight):
115 return isinstance(weight, float) and math.fabs(weight) >= self._epsilon
116
117 def SingleFeatureScore(self, feature_value):
118 """Returns the score (aka weighted value) of a ``FeatureValue``.
119
120 Args:
121 feature_value (FeatureValue): the feature value to check.
122
123 Returns:
124 The score of the feature value.
125 """
126 return feature_value.value * self._weights.get(feature_value.name, 0.)
127
128 # TODO(crbug.com/673964): something better for detecting "close to log(0)".
129 def LogZeroish(self, x):
130 """Determine whether a float is close enough to log(0).
131
132 If a ``FeatureValue`` has a (log-domain) score of -inf for a given
133 ``Suspect``, then that suspect has zero probability of being the
134 culprit. We want to filter these suspects out, to clean up the
135 output of classification; so this method encapsulates the logic of
136 that check.
137
138 Args:
139 x (float): the float to check
140
141 Returns:
142 ``True`` if ``x`` is close enough to log(0); else ``False``.
143 """
144 return x < 0 and math.isinf(x)
145 98
146 def ClearWeightBasedMemos(self): 99 def ClearWeightBasedMemos(self):
147 """Clear all the memos that depend on the weight covector.""" 100 """Clear all the memos that depend on the weight covector."""
148 self._quadrance = None 101 self._quadrance = None
149 self._scores.ClearMemos() 102 self._scores.ClearMemos()
150 103
151 def ClearAllMemos(self): 104 def ClearAllMemos(self):
152 """Clear all memos, even those independent of the weight covector.""" 105 """Clear all memos, even those independent of the weight covector."""
153 self.ClearWeightBasedMemos() 106 self.ClearWeightBasedMemos()
154 self._features.ClearMemos() 107 self._features.ClearMemos()
155 108
156 @property 109 @property
157 def weights(self): 110 def meta_weight(self):
158 """The weight covector. 111 """The weight covector.
159 112
160 At present we return the weights as an dict mapping feature name to its 113 At present we return the weights as an dict mapping feature name to its
161 weight, but in the future that may be replaced by a more general type which 114 weight, but in the future that may be replaced by a more general type which
162 specifies the semantics rather than the implementation details. 115 specifies the semantics rather than the implementation details.
163 """ 116 """
164 return self._weights 117 return self._meta_weight
118
119 @meta_weight.setter
120 def meta_weight(self, new_meta_weight):
121 self._meta_weight = new_meta_weight
122 self._meta_weight.DropZeroWeights(self._epsilon)
165 123
166 @property 124 @property
167 def l0(self): # pragma: no cover 125 def l0(self): # pragma: no cover
168 """The l0-norm of the weight covector. 126 """The l0-norm of the weight covector.
169 127
170 N.B., despite being popularly called the "l0-norm", this isn't 128 N.B., despite being popularly called the "l0-norm", this isn't
171 actually a norm in the mathematical sense.""" 129 actually a norm in the mathematical sense."""
172 return float(len(self.weights) - self.weights.values().count(0.)) 130 return self._meta_weight.l0
173 131
174 @property 132 @property
175 def l1(self): # pragma: no cover 133 def l1(self): # pragma: no cover
176 """The l1 (aka: Manhattan) norm of the weight covector.""" 134 """The l1 (aka: Manhattan) norm of the weight covector."""
177 return math.fsum(math.fabs(w) for w in self.weights.itervalues()) 135 return self._meta_weight.l1
178 136
179 @property 137 @property
180 def quadrance(self): 138 def quadrance(self):
181 """The square of the l2 norm of the weight covector. 139 """The square of the l2 norm of the weight covector.
182 140
183 This value is often more helpful to have direct access to, as it 141 This value is often more helpful to have direct access to, as it
184 avoids the need for non-rational functions (e.g., sqrt) and shows up 142 avoids the need for non-rational functions (e.g., sqrt) and shows up
185 as its own quantity in many places. Also, computing it directly avoids 143 as its own quantity in many places. Also, computing it directly avoids
186 the error introduced by squaring the square-root of an IEEE-754 float. 144 the error introduced by squaring the square-root of an IEEE-754 float.
187 """ 145 """
188 if self._quadrance is None: 146 return self._meta_weight.quadrance
189 self._quadrance = math.fsum(
190 math.fabs(w)**2 for w in self.weights.itervalues())
191
192 return self._quadrance
193 147
194 @property 148 @property
195 def l2(self): 149 def l2(self):
196 """The l2 (aka Euclidean) norm of the weight covector. 150 """The l2 (aka Euclidean) norm of the weight covector.
197 151
198 If you need the square of the l2-norm, do not use this property. 152 If you need the square of the l2-norm, do not use this property.
199 Instead, use the ``quadrance`` property which is more accurate than 153 Instead, use the ``quadrance`` property which is more accurate than
200 squaring this one. 154 squaring this one.
201 """ 155 """
202 return math.sqrt(self.quadrance) 156 return math.sqrt(self.quadrance)
203 157
158 # TODO(crbug.com/673964): something better for detecting "close to log(0)".
159 def LogZeroish(self, x):
160 """Determine whether a float is close enough to log(0).
161
162 If a ``FeatureValue`` has a (log-domain) score of -inf for a given
163 ``Suspect``, then that suspect has zero probability of being the
164 culprit. We want to filter these suspects out, to clean up the
165 output of classification; so this method encapsulates the logic of
166 that check.
167
168 Args:
169 x (float): the float to check
170
171 Returns:
172 ``True`` if ``x`` is close enough to log(0); else ``False``.
173 """
174 return x < 0 and math.isinf(x)
175
204 def Features(self, x): 176 def Features(self, x):
205 """Returns a function mapping ``y`` to its feature vector given ``x``. 177 """Returns a function mapping ``y`` to its feature vector given ``x``.
206 178
207 Args: 179 Args:
208 x (X): the value of the independent variable. 180 x (X): the value of the independent variable.
209 181
210 Returns: 182 Returns:
211 A ``MemoizedFunction`` of type ``Y -> dict(str to float)``. 183 A ``MemoizedFunction`` of type ``Y -> dict(str to float)``.
212 """ 184 """
213 return self._features(x) 185 return self._features(x)
(...skipping 12 matching lines...) Expand all
226 ``Probability(x)``. 198 ``Probability(x)``.
227 199
228 Args: 200 Args:
229 x (X): the value of the independent variable. 201 x (X): the value of the independent variable.
230 202
231 Returns: 203 Returns:
232 A ``MemoizedFunction`` of type ``Y -> float``. 204 A ``MemoizedFunction`` of type ``Y -> float``.
233 """ 205 """
234 return self._scores(x) 206 return self._scores(x)
235 207
236 def FormatReasons(self, features):
237 """Collect and format a list of all ``FeatureValue.reason`` strings.
238
239 Args:
240 features (iterable of FeatureValue): the values whose ``reason``
241 strings should be collected.
242
243 Returns:
244 A list of ``(str, float, str)`` triples; where the first string is
245 the feature name, the float is some numeric representation of how
246 much influence this feature exerts on the ``Suspect`` being blamed,
247 and the final string is the ``FeatureValue.reason``. The list is
248 sorted by feature name, just to ensure that it comes out in some
249 canonical order.
250
251 At present, the float is the log-domain score of the feature
252 value. However, this isn't the best thing for UX reasons. In the
253 future it might be replaced by the normal-domain score, or by
254 the probability.
255 """
256 formatted_reasons = []
257 for feature in features:
258 feature_score = self.SingleFeatureScore(feature)
259 if self.LogZeroish(feature_score): # pragma: no cover
260 logging.debug('Discarding reasons from feature %s'
261 ' because it has zero probability' % feature.name)
262 continue
263
264 formatted_reasons.append((feature.name, feature_score, feature.reason))
265
266 formatted_reasons.sort(key=lambda formatted_reason: formatted_reason[0])
267 return formatted_reasons
268
269 def AggregateChangedFiles(self, features):
270 """Merge multiple``FeatureValue.changed_files`` lists into one.
271
272 Args:
273 features (iterable of FeatureValue): the values whose ``changed_files``
274 lists should be aggregated.
275
276 Returns:
277 A list of ``ChangedFile`` objects sorted by file name. The sorting
278 is not essential, but is provided to ease testing by ensuring the
279 output is in some canonical order.
280
281 Raises:
282 ``ValueError`` if any file name is given inconsistent ``blame_url``s.
283 """
284 all_changed_files = {}
285 for feature in features:
286 if self.LogZeroish(self.SingleFeatureScore(feature)): # pragma: no cover
287 logging.debug('Discarding changed files from feature %s'
288 ' because it has zero probability' % feature.name)
289 continue
290
291 for changed_file in feature.changed_files or []:
292 accumulated_changed_file = all_changed_files.get(changed_file.name)
293 if accumulated_changed_file is None:
294 all_changed_files[changed_file.name] = changed_file
295 continue
296
297 if (accumulated_changed_file.blame_url !=
298 changed_file.blame_url): # pragma: no cover
299 raise ValueError('Blame URLs do not match: %s != %s'
300 % (accumulated_changed_file.blame_url, changed_file.blame_url))
301 accumulated_changed_file.reasons.extend(changed_file.reasons or [])
302
303 changed_files = all_changed_files.values()
304 changed_files.sort(key=lambda changed_file: changed_file.name)
305 return changed_files
306
307 208
308 class LogLinearModel(UnnormalizedLogLinearModel): 209 class LogLinearModel(UnnormalizedLogLinearModel):
309 """A loglinear probability model. 210 """A loglinear probability model.
310 211
311 This class is distinct from ``UnnormalizedLogLinearModel`` in that 212 This class is distinct from ``UnnormalizedLogLinearModel`` in that
312 we can provide probabilities (not just scores). However, to do so we 213 we can provide probabilities (not just scores). However, to do so we
313 require a specification of the subsets of ``Y`` for each ``x``. 214 require a specification of the subsets of ``Y`` for each ``x``.
314 """ 215 """
315 def __init__(self, Y_given_X, feature_function, weights, epsilon=None): 216 def __init__(self, Y_given_X, meta_feature, meta_weight, epsilon=None):
316 """Construct a new probabilistic model. 217 """Construct a new probabilistic model.
317 218
318 Args: 219 Args:
319 Y_given_X: a function from ``X`` to an iterable object giving the 220 Y_given_X: a function from ``X`` to an iterable object giving the
320 subset of ``Y`` which has non-zero probability given the 221 subset of ``Y`` which has non-zero probability given the
321 ``x``. When in doubt about whether some ``y`` has zero probability 222 ``x``. When in doubt about whether some ``y`` has zero probability
322 or not, it is always safe/correct to return a larger subset of 223 or not, it is always safe/correct to return a larger subset of
323 ``Y`` (it'll just take more computation time is all). This is 224 ``Y`` (it'll just take more computation time is all). This is
324 needed for computing the partition function and expectation. N.B., 225 needed for computing the partition function and expectation. N.B.,
325 we do not actually need to know/enumerate of *all* of ``Y``, 226 we do not actually need to know/enumerate of *all* of ``Y``,
326 only the subsets for each ``x``. 227 only the subsets for each ``x``.
327 feature_function: A function ``X -> Y -> list(float)``. N.B., 228 meta_feature (MetaFeature): A function ``X -> Y -> MetaFeatureValue``.
328 for all ``x`` and ``y`` the length of ``feature_function(x)(y)`` 229 N.B., for all ``x`` and ``y`` the length of ``wrapped_feature(x)(y)``
329 must be the same as the length of ``weights``. 230 must be the same as the length of ``weights``.
330 weights (dict of float): the weights for the features. The keys of 231 All features.
331 the dictionary are the names of the feature that weight is 232 meta_weight (MetaWeight): All weights. the weights for the features.
233 The keys of the dictionary are the names of the feature that weight is
332 for. We take this argument as a dict rather than as a list so that 234 for. We take this argument as a dict rather than as a list so that
333 callers needn't worry about what order to provide the weights in. 235 callers needn't worry about what order to provide the weights in.
334 epsilon (float): The absolute-error threshold for considering a 236 epsilon (float): The absolute-error threshold for considering a
335 weight to be "equal to zero". N.B., this should be a positive 237 weight to be "equal to zero". N.B., this should be a positive
336 number, as we will compare it against the absolute value of 238 number, as we will compare it against the absolute value of
337 each weight. 239 each weight.
338 """ 240 """
339 super(LogLinearModel, self).__init__(feature_function, weights, epsilon) 241 super(LogLinearModel, self).__init__(meta_feature, meta_weight, epsilon)
340 242
341 self._Y = Y_given_X 243 self._Y = Y_given_X
342 244
343 def _LogZ(x): 245 def _LogZ(x):
344 score_given_x = self._scores(x) 246 score_given_x = self._scores(x)
345 return logsumexp([score_given_x(y) for y in self._Y(x)]) 247 return logsumexp([score_given_x(y) for y in self._Y(x)])
346 self._zetas = MemoizedFunction(_LogZ) 248 self._zetas = MemoizedFunction(_LogZ)
347 249
348 def ClearWeightBasedMemos(self): 250 def ClearWeightBasedMemos(self):
349 """Clear all the memos that depend on the weight covector.""" 251 """Clear all the memos that depend on the weight covector."""
(...skipping 22 matching lines...) Expand all
372 x (X): the value of the independent variable. 274 x (X): the value of the independent variable.
373 275
374 Returns: 276 Returns:
375 The normalizing constant of ``LogProbability(x)``. 277 The normalizing constant of ``LogProbability(x)``.
376 """ 278 """
377 return self._zetas(x) 279 return self._zetas(x)
378 280
379 def Probability(self, x): 281 def Probability(self, x):
380 """The normal-domain distribution over ``y`` given ``x``. 282 """The normal-domain distribution over ``y`` given ``x``.
381 283
382 That is, ``self.Probability(x)(y)`` returns ``p(y | x; self.weights)`` 284 That is, ``self.Probability(x)(y)`` returns ``p(y | x; self._meta_weight)``
383 which is the model's estimation of ``Pr(y|x)``. 285 which is the model's estimation of ``Pr(y|x)``.
384 286
385 If you need the log-probability, don't use this method. Instead, 287 If you need the log-probability, don't use this method. Instead,
386 use the ``LogProbability`` method which computes it directly and 288 use the ``LogProbability`` method which computes it directly and
387 thus is more accurate than taking the log of this one. 289 thus is more accurate than taking the log of this one.
388 """ 290 """
389 logprob_given_x = self.LogProbability(x) 291 logprob_given_x = self.LogProbability(x)
390 return lambda y: math.exp(logprob_given_x(y)) 292 return lambda y: math.exp(logprob_given_x(y))
391 293
392 def LogProbability(self, x): 294 def LogProbability(self, x):
(...skipping 20 matching lines...) Expand all
413 the particular array returned may not actually be one that the 315 the particular array returned may not actually be one that the
414 function returns; rather, it's a sort of average of all the results 316 function returns; rather, it's a sort of average of all the results
415 returned. For more information you can take a look at Wikipedia 317 returned. For more information you can take a look at Wikipedia
416 <https://en.wikipedia.org/wiki/Expected_value>. 318 <https://en.wikipedia.org/wiki/Expected_value>.
417 """ 319 """
418 prob_given_x = self.Probability(x) 320 prob_given_x = self.Probability(x)
419 # N.B., the ``*`` below is vector scaling! If we want to make this 321 # N.B., the ``*`` below is vector scaling! If we want to make this
420 # method polymorphic in the return type of ``f`` then we'll need an 322 # method polymorphic in the return type of ``f`` then we'll need an
421 # API that provides both scaling and ``vsum``. 323 # API that provides both scaling and ``vsum``.
422 return vsum([prob_given_x(y) * f(y) for y in self._Y(x)]) 324 return vsum([prob_given_x(y) * f(y) for y in self._Y(x)])
423
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698