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

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 meta_feature (MetaFeature): A function ``X -> Y -> MetaFeatureValue``.
51 for all ``x`` and ``y`` the length of ``feature_function(x)(y)`` 51 N.B., 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 All features.
54 the dictionary are the names of the feature that weight is 54 meta_weight (MetaWeight): All weights. the weights for the features.
55 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 56 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. 57 callers needn't worry about what order to provide the weights in.
57 epsilon (float): The absolute-error threshold for considering a 58 epsilon (float): The absolute-error threshold for considering a
58 weight to be "equal to zero". N.B., this should be a positive 59 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 60 number, as we will compare it against the absolute value of
60 each weight. 61 each weight.
61 """ 62 """
62 if epsilon is None: 63 self._epsilon = EPSILON if epsilon is None else epsilon
63 self._epsilon = EPSILON
64 else:
65 self._epsilon = epsilon
66 64
67 # TODO(crbug.com/680207) Filter zero weights, use sparse representaion of 65 self._meta_weight = meta_weight
68 # weight covector. 66 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 67
74 self._quadrance = None 68 self._quadrance = None
75
76 # TODO(crbug.com/674752): we need better names for ``self._features``. 69 # TODO(crbug.com/674752): we need better names for ``self._features``.
77 def _Features(x): 70 def _Features(x):
78 """Wrap ``feature_function`` to memoize things and ensure types. 71 """Wrap ``meta_feature`` to memoize things.
79 72
80 This outer wrapping takes each ``x`` to a memoized instance of 73 This outer wrapping takes each ``x`` to a memoized instance of
81 ``_FeaturesGivenX``. That is, for each ``x`` we return a 74 ``_FeaturesGivenX``. That is, for each ``x`` we return a
82 ``MemoizedFunction`` from ``Y`` to ``dict(str to FeatureValue)``. 75 ``MemoizedFunction`` from ``Y`` to ``dict(str to FeatureValue)``.
83 """ 76 """
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 77 # Memoize on ``Y``, to ensure we don't need to recompute
98 # ``FeatureValue``s nor recheck the lengths. 78 # ``FeatureValue``s nor recheck the lengths.
99 return MemoizedFunction(_FeaturesGivenX) 79 return MemoizedFunction(meta_feature(x))
100 80
101 # Memoize on ``X``, to ensure we share the memo tables on ``Y``. 81 # Memoize on ``X``, to ensure we share the memo tables on ``Y``.
102 self._features = MemoizedFunction(_Features) 82 self._features = MemoizedFunction(_Features)
103 83
104 # TODO(crbug.com/674752): we need better names for ``self._scores``. 84 # TODO(crbug.com/674752): we need better names for ``self._scores``.
105 # N.B., this is just the inner product of ``self.weights`` 85 # N.B., this is just the inner product of ``self._meta_weight``
106 # against ``self._features(x)``. If we can compute this in some 86 # against ``self._features(x)``. If we can compute this in some
107 # more efficient way, we should. In particular, we will want to 87 # 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 88 # make the weights sparse, in which case we need to use a sparse
109 # variant of the dot product. 89 # variant of the dot product.
110 self._scores = MemoizedFunction(lambda x: self._features(x).map( 90 self._scores = MemoizedFunction(lambda x: self._features(x).map(
111 lambda fxy: math.fsum(self.SingleFeatureScore(feature) 91 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 92
146 def ClearWeightBasedMemos(self): 93 def ClearWeightBasedMemos(self):
147 """Clear all the memos that depend on the weight covector.""" 94 """Clear all the memos that depend on the weight covector."""
148 self._quadrance = None 95 self._quadrance = None
149 self._scores.ClearMemos() 96 self._scores.ClearMemos()
150 97
151 def ClearAllMemos(self): 98 def ClearAllMemos(self):
152 """Clear all memos, even those independent of the weight covector.""" 99 """Clear all memos, even those independent of the weight covector."""
153 self.ClearWeightBasedMemos() 100 self.ClearWeightBasedMemos()
154 self._features.ClearMemos() 101 self._features.ClearMemos()
155 102
156 @property 103 @property
157 def weights(self): 104 def meta_weight(self):
158 """The weight covector. 105 """The weight covector.
159 106
160 At present we return the weights as an dict mapping feature name to its 107 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 108 weight, but in the future that may be replaced by a more general type which
162 specifies the semantics rather than the implementation details. 109 specifies the semantics rather than the implementation details.
163 """ 110 """
164 return self._weights 111 return self._meta_weight
112
113 @meta_weight.setter
114 def meta_weight(self, new_meta_weight):
115 self._meta_weight = new_meta_weight
116 self._meta_weight.DropZeroWeights(self._epsilon)
165 117
166 @property 118 @property
167 def l0(self): # pragma: no cover 119 def l0(self): # pragma: no cover
168 """The l0-norm of the weight covector. 120 """The l0-norm of the weight covector.
169 121
170 N.B., despite being popularly called the "l0-norm", this isn't 122 N.B., despite being popularly called the "l0-norm", this isn't
171 actually a norm in the mathematical sense.""" 123 actually a norm in the mathematical sense."""
172 return float(len(self.weights) - self.weights.values().count(0.)) 124 return self._meta_weight.l0
173 125
174 @property 126 @property
175 def l1(self): # pragma: no cover 127 def l1(self): # pragma: no cover
176 """The l1 (aka: Manhattan) norm of the weight covector.""" 128 """The l1 (aka: Manhattan) norm of the weight covector."""
177 return math.fsum(math.fabs(w) for w in self.weights.itervalues()) 129 return self._meta_weight.l1
178 130
179 @property 131 @property
180 def quadrance(self): 132 def quadrance(self):
181 """The square of the l2 norm of the weight covector. 133 """The square of the l2 norm of the weight covector.
182 134
183 This value is often more helpful to have direct access to, as it 135 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 136 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 137 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. 138 the error introduced by squaring the square-root of an IEEE-754 float.
187 """ 139 """
188 if self._quadrance is None: 140 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 141
194 @property 142 @property
195 def l2(self): 143 def l2(self):
196 """The l2 (aka Euclidean) norm of the weight covector. 144 """The l2 (aka Euclidean) norm of the weight covector.
197 145
198 If you need the square of the l2-norm, do not use this property. 146 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 147 Instead, use the ``quadrance`` property which is more accurate than
200 squaring this one. 148 squaring this one.
201 """ 149 """
202 return math.sqrt(self.quadrance) 150 return math.sqrt(self.quadrance)
203 151
152 # TODO(crbug.com/673964): something better for detecting "close to log(0)".
153 def LogZeroish(self, x):
154 """Determine whether a float is close enough to log(0).
155
156 If a ``FeatureValue`` has a (log-domain) score of -inf for a given
157 ``Suspect``, then that suspect has zero probability of being the
158 culprit. We want to filter these suspects out, to clean up the
159 output of classification; so this method encapsulates the logic of
160 that check.
161
162 Args:
163 x (float): the float to check
164
165 Returns:
166 ``True`` if ``x`` is close enough to log(0); else ``False``.
167 """
168 return x < 0 and math.isinf(x)
169
204 def Features(self, x): 170 def Features(self, x):
205 """Returns a function mapping ``y`` to its feature vector given ``x``. 171 """Returns a function mapping ``y`` to its feature vector given ``x``.
206 172
207 Args: 173 Args:
208 x (X): the value of the independent variable. 174 x (X): the value of the independent variable.
209 175
210 Returns: 176 Returns:
211 A ``MemoizedFunction`` of type ``Y -> dict(str to float)``. 177 A ``MemoizedFunction`` of type ``Y -> dict(str to float)``.
212 """ 178 """
213 return self._features(x) 179 return self._features(x)
(...skipping 12 matching lines...) Expand all
226 ``Probability(x)``. 192 ``Probability(x)``.
227 193
228 Args: 194 Args:
229 x (X): the value of the independent variable. 195 x (X): the value of the independent variable.
230 196
231 Returns: 197 Returns:
232 A ``MemoizedFunction`` of type ``Y -> float``. 198 A ``MemoizedFunction`` of type ``Y -> float``.
233 """ 199 """
234 return self._scores(x) 200 return self._scores(x)
235 201
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 202
308 class LogLinearModel(UnnormalizedLogLinearModel): 203 class LogLinearModel(UnnormalizedLogLinearModel):
309 """A loglinear probability model. 204 """A loglinear probability model.
310 205
311 This class is distinct from ``UnnormalizedLogLinearModel`` in that 206 This class is distinct from ``UnnormalizedLogLinearModel`` in that
312 we can provide probabilities (not just scores). However, to do so we 207 we can provide probabilities (not just scores). However, to do so we
313 require a specification of the subsets of ``Y`` for each ``x``. 208 require a specification of the subsets of ``Y`` for each ``x``.
314 """ 209 """
315 def __init__(self, Y_given_X, feature_function, weights, epsilon=None): 210 def __init__(self, Y_given_X, meta_feature, meta_weight, epsilon=None):
316 """Construct a new probabilistic model. 211 """Construct a new probabilistic model.
317 212
318 Args: 213 Args:
319 Y_given_X: a function from ``X`` to an iterable object giving the 214 Y_given_X: a function from ``X`` to an iterable object giving the
320 subset of ``Y`` which has non-zero probability given the 215 subset of ``Y`` which has non-zero probability given the
321 ``x``. When in doubt about whether some ``y`` has zero probability 216 ``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 217 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 218 ``Y`` (it'll just take more computation time is all). This is
324 needed for computing the partition function and expectation. N.B., 219 needed for computing the partition function and expectation. N.B.,
325 we do not actually need to know/enumerate of *all* of ``Y``, 220 we do not actually need to know/enumerate of *all* of ``Y``,
326 only the subsets for each ``x``. 221 only the subsets for each ``x``.
327 feature_function: A function ``X -> Y -> list(float)``. N.B., 222 meta_feature (MetaFeature): A function ``X -> Y -> MetaFeatureValue``.
328 for all ``x`` and ``y`` the length of ``feature_function(x)(y)`` 223 N.B., for all ``x`` and ``y`` the length of ``wrapped_feature(x)(y)``
329 must be the same as the length of ``weights``. 224 must be the same as the length of ``weights``.
330 weights (dict of float): the weights for the features. The keys of 225 All features.
331 the dictionary are the names of the feature that weight is 226 meta_weight (MetaWeight): All weights. the weights for the features.
227 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 228 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. 229 callers needn't worry about what order to provide the weights in.
334 epsilon (float): The absolute-error threshold for considering a 230 epsilon (float): The absolute-error threshold for considering a
335 weight to be "equal to zero". N.B., this should be a positive 231 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 232 number, as we will compare it against the absolute value of
337 each weight. 233 each weight.
338 """ 234 """
339 super(LogLinearModel, self).__init__(feature_function, weights, epsilon) 235 super(LogLinearModel, self).__init__(meta_feature, meta_weight, epsilon)
340 236
341 self._Y = Y_given_X 237 self._Y = Y_given_X
342 238
343 def _LogZ(x): 239 def _LogZ(x):
344 score_given_x = self._scores(x) 240 score_given_x = self._scores(x)
345 return logsumexp([score_given_x(y) for y in self._Y(x)]) 241 return logsumexp([score_given_x(y) for y in self._Y(x)])
346 self._zetas = MemoizedFunction(_LogZ) 242 self._zetas = MemoizedFunction(_LogZ)
347 243
348 def ClearWeightBasedMemos(self): 244 def ClearWeightBasedMemos(self):
349 """Clear all the memos that depend on the weight covector.""" 245 """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. 268 x (X): the value of the independent variable.
373 269
374 Returns: 270 Returns:
375 The normalizing constant of ``LogProbability(x)``. 271 The normalizing constant of ``LogProbability(x)``.
376 """ 272 """
377 return self._zetas(x) 273 return self._zetas(x)
378 274
379 def Probability(self, x): 275 def Probability(self, x):
380 """The normal-domain distribution over ``y`` given ``x``. 276 """The normal-domain distribution over ``y`` given ``x``.
381 277
382 That is, ``self.Probability(x)(y)`` returns ``p(y | x; self.weights)`` 278 That is, ``self.Probability(x)(y)`` returns ``p(y | x; self._meta_weight)``
383 which is the model's estimation of ``Pr(y|x)``. 279 which is the model's estimation of ``Pr(y|x)``.
384 280
385 If you need the log-probability, don't use this method. Instead, 281 If you need the log-probability, don't use this method. Instead,
386 use the ``LogProbability`` method which computes it directly and 282 use the ``LogProbability`` method which computes it directly and
387 thus is more accurate than taking the log of this one. 283 thus is more accurate than taking the log of this one.
388 """ 284 """
389 logprob_given_x = self.LogProbability(x) 285 logprob_given_x = self.LogProbability(x)
390 return lambda y: math.exp(logprob_given_x(y)) 286 return lambda y: math.exp(logprob_given_x(y))
391 287
392 def LogProbability(self, x): 288 def LogProbability(self, x):
(...skipping 20 matching lines...) Expand all
413 the particular array returned may not actually be one that the 309 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 310 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 311 returned. For more information you can take a look at Wikipedia
416 <https://en.wikipedia.org/wiki/Expected_value>. 312 <https://en.wikipedia.org/wiki/Expected_value>.
417 """ 313 """
418 prob_given_x = self.Probability(x) 314 prob_given_x = self.Probability(x)
419 # N.B., the ``*`` below is vector scaling! If we want to make this 315 # 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 316 # method polymorphic in the return type of ``f`` then we'll need an
421 # API that provides both scaling and ``vsum``. 317 # API that provides both scaling and ``vsum``.
422 return vsum([prob_given_x(y) * f(y) for y in self._Y(x)]) 318 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