Skip to content

Commit a0c292e

Browse files
authored
Merge pull request #247 from twitter/jbaxter/2024_07_26
Allow scorers to firm_reject, finish split scorer refactoring & QOL++
2 parents 85cb2ec + fed3955 commit a0c292e

19 files changed

+524
-149
lines changed

sourcecode/scoring/constants.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
# Max flip rates
3737
prescoringAllUnlockedNotesMaxCrhChurn = 0.04
3838
finalUnlockedNotesWithNoNewRatingsMaxCrhChurn = 0.03
39-
finalNotesWithNewRatingsMaxCrhChurn = 0.40
39+
finalNotesWithNewRatingsMaxNewCrhChurn = 0.80
40+
finalNotesWithNewRatingsMaxOldCrhChurn = 0.25
4041
finalNotesThatJustFlippedStatusMaxCrhChurn = 1e8
4142
finalNotesThatFlippedRecentlyMaxCrhChurn = 1e8
4243

@@ -236,6 +237,9 @@ def rater_factor_key(i):
236237
currentlyRatedHelpful = "CURRENTLY_RATED_HELPFUL"
237238
currentlyRatedNotHelpful = "CURRENTLY_RATED_NOT_HELPFUL"
238239
needsMoreRatings = "NEEDS_MORE_RATINGS"
240+
# FIRM_REJECT is set by individual scorers to indicate downstream scorers should not CRH
241+
# a note, but is never set as the finalRatingStatus of a note.
242+
firmReject = "FIRM_REJECT"
239243

240244
# Boolean Note Status Labels
241245
currentlyRatedHelpfulBoolKey = "crhBool"
@@ -511,7 +515,7 @@ def rater_factor_key(i):
511515
(successfulRatingNeededToEarnIn, np.int64),
512516
(timestampOfLastStateChange, np.int64),
513517
(timestampOfLastEarnOut, np.double), # double because nullable.
514-
(modelingPopulationKey, str),
518+
(modelingPopulationKey, "category"),
515519
(modelingGroupKey, np.float64),
516520
(numberOfTimesEarnedOutKey, np.int64),
517521
]
@@ -801,6 +805,9 @@ class PrescoringMetaScorerOutput:
801805
globalIntercept: Optional[float]
802806
lowDiligenceGlobalIntercept: Optional[ReputationGlobalIntercept]
803807
tagFilteringThresholds: Optional[Dict[str, float]] # tag => threshold
808+
finalRoundNumRatings: Optional[int]
809+
finalRoundNumNotes: Optional[int]
810+
finalRoundNumUsers: Optional[int]
804811

805812

806813
@dataclass
@@ -886,5 +893,6 @@ class RescoringRuleID(Enum):
886893
@dataclass
887894
class NoteSubset:
888895
noteSet: Optional[set]
889-
maxCrhChurnRate: float
896+
maxNewCrhChurnRate: float
897+
maxOldCrhChurnRate: float
890898
description: RescoringRuleID

sourcecode/scoring/matrix_factorization/matrix_factorization.py

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(
6565
if logging:
6666
print(f"Using pos weight: {self._posWeight} with BCEWithLogitsLoss")
6767
self.criterion = torch.nn.BCEWithLogitsLoss(
68-
pos_weight=torch.Tensor(np.array(self._posWeight)), reduction="none"
68+
pos_weight=torch.FloatTensor(np.array(self._posWeight)), reduction="none"
6969
)
7070
else:
7171
if logging:
@@ -84,6 +84,9 @@ def __init__(
8484
self.trainModelData: Optional[ModelData] = None
8585
self.validateModelData: Optional[ModelData] = None
8686

87+
self._ratingPerNoteLossRatio: Optional[float] = None
88+
self._ratingPerUserLossRatio: Optional[float] = None
89+
8790
def get_final_train_error(self) -> Optional[float]:
8891
return self.train_errors[-1] if self.train_errors else None
8992

@@ -214,7 +217,7 @@ def _initialize_parameters(
214217
if self._logging:
215218
print("initialized global intercept")
216219
self.mf_model.global_intercept = torch.nn.parameter.Parameter(
217-
torch.ones(1, 1) * globalInterceptInit
220+
torch.ones(1, 1, dtype=torch.float32) * globalInterceptInit
218221
)
219222

220223
def _get_parameters_from_trained_model(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
@@ -359,16 +362,55 @@ def _get_loss(self, epoch: Optional[int] = None):
359362
return loss
360363

361364
def _get_reg_loss(self):
362-
l2_reg_loss = torch.tensor(0.0).to(self.mf_model.device)
363-
l2_reg_loss += self._userFactorLambda * (self.mf_model.user_factors.weight**2).mean()
364-
l2_reg_loss += self._noteFactorLambda * (self.mf_model.note_factors.weight**2).mean()
365-
l2_reg_loss += self._userInterceptLambda * (self.mf_model.user_intercepts.weight**2).mean()
366-
l2_reg_loss += self._noteInterceptLambda * (self.mf_model.note_intercepts.weight**2).mean()
365+
l2_reg_loss = torch.tensor(0.0, dtype=torch.float32).to(self.mf_model.device)
366+
367+
if self._ratingPerUserLossRatio is None:
368+
l2_reg_loss += self._userFactorLambda * (self.mf_model.user_factors.weight**2).mean()
369+
l2_reg_loss += self._userInterceptLambda * (self.mf_model.user_intercepts.weight**2).mean()
370+
else:
371+
simulatedNumberOfRatersForLoss = (
372+
len(self.trainModelData.rating_labels) / self._ratingPerUserLossRatio
373+
)
374+
l2_reg_loss += (
375+
self._userFactorLambda
376+
* (self.mf_model.user_factors.weight**2).sum()
377+
/ simulatedNumberOfRatersForLoss
378+
)
379+
l2_reg_loss += (
380+
self._userInterceptLambda
381+
* (self.mf_model.user_intercepts.weight**2).sum()
382+
/ simulatedNumberOfRatersForLoss
383+
)
384+
385+
if self._ratingPerNoteLossRatio is None:
386+
l2_reg_loss += self._noteFactorLambda * (self.mf_model.note_factors.weight**2).mean()
387+
l2_reg_loss += self._noteInterceptLambda * (self.mf_model.note_intercepts.weight**2).mean()
388+
l2_reg_loss += (
389+
self._diamondLambda
390+
* (self.mf_model.note_factors.weight * self.mf_model.note_intercepts.weight).abs().mean()
391+
)
392+
else:
393+
simulatedNumberOfNotesForLoss = (
394+
len(self.trainModelData.rating_labels) / self._ratingPerNoteLossRatio
395+
)
396+
l2_reg_loss += (
397+
self._noteFactorLambda
398+
* (self.mf_model.note_factors.weight**2).sum()
399+
/ simulatedNumberOfNotesForLoss
400+
)
401+
l2_reg_loss += (
402+
self._noteInterceptLambda
403+
* (self.mf_model.note_intercepts.weight**2).sum()
404+
/ simulatedNumberOfNotesForLoss
405+
)
406+
l2_reg_loss += (
407+
self._diamondLambda
408+
* (self.mf_model.note_factors.weight * self.mf_model.note_intercepts.weight).abs().sum()
409+
/ simulatedNumberOfNotesForLoss
410+
)
411+
367412
l2_reg_loss += self._globalInterceptLambda * (self.mf_model.global_intercept**2).mean()
368-
l2_reg_loss += (
369-
self._diamondLambda
370-
* (self.mf_model.note_factors.weight * self.mf_model.note_intercepts.weight).abs().mean()
371-
)
413+
372414
return l2_reg_loss
373415

374416
def _fit_model(
@@ -434,10 +476,10 @@ def prepare_features_and_labels(
434476
rating_labels = torch.FloatTensor(ratingFeaturesAndLabels[self._labelCol].values).to(
435477
self.mf_model.device
436478
)
437-
user_indexes = torch.LongTensor(ratingFeaturesAndLabels[Constants.raterIndexKey].values).to(
479+
user_indexes = torch.IntTensor(ratingFeaturesAndLabels[Constants.raterIndexKey].values).to(
438480
self.mf_model.device
439481
)
440-
note_indexes = torch.LongTensor(ratingFeaturesAndLabels[Constants.noteIndexKey].values).to(
482+
note_indexes = torch.IntTensor(ratingFeaturesAndLabels[Constants.noteIndexKey].values).to(
441483
self.mf_model.device
442484
)
443485
self.modelData = ModelData(rating_labels, user_indexes, note_indexes)
@@ -451,6 +493,9 @@ def run_mf(
451493
specificNoteId: Optional[int] = None,
452494
validatePercent: Optional[float] = None,
453495
freezeRaterParameters: bool = False,
496+
freezeGlobalParameters: bool = False,
497+
ratingPerNoteLossRatio: Optional[float] = None,
498+
ratingPerUserLossRatio: Optional[float] = None,
454499
):
455500
"""Train matrix factorization model.
456501
@@ -469,13 +514,33 @@ def run_mf(
469514
raterParams: contains one row per rating, including raterId and learned rater parameters
470515
globalIntercept: learned global intercept parameter
471516
"""
517+
self._ratingPerNoteLossRatio = ratingPerNoteLossRatio
518+
self._ratingPerUserLossRatio = ratingPerUserLossRatio
519+
472520
self._initialize_note_and_rater_id_maps(ratings)
473521

474522
self._create_mf_model(noteInit, userInit, globalInterceptInit)
475523
assert self.mf_model is not None
476524

525+
print(
526+
f"Ratings per note in dataset: {len(ratings)/self.mf_model.note_factors.weight.data.shape[0]}"
527+
)
528+
print(
529+
f"Ratings per user in dataset: {len(ratings)/self.mf_model.user_factors.weight.data.shape[0]}"
530+
)
531+
if ratingPerNoteLossRatio is not None:
532+
print(
533+
f"Correcting loss function to simulate rating per note loss ratio = {ratingPerNoteLossRatio}"
534+
)
535+
if ratingPerUserLossRatio is not None:
536+
print(
537+
f"Correcting loss function to simulate rating per user loss ratio = {ratingPerUserLossRatio}"
538+
)
539+
477540
if freezeRaterParameters:
478541
self.mf_model._freeze_parameters(set({"user"}))
542+
if freezeGlobalParameters:
543+
self.mf_model._freeze_parameters(set({"global"}))
479544
if specificNoteId is not None:
480545
self.mf_model.freeze_rater_and_global_parameters()
481546
self.prepare_features_and_labels(specificNoteId)

sourcecode/scoring/matrix_factorization/model.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
@dataclass
88
class ModelData:
99
rating_labels: Optional[torch.FloatTensor]
10-
user_indexes: Optional[torch.LongTensor]
11-
note_indexes: Optional[torch.LongTensor]
10+
user_indexes: Optional[torch.IntTensor]
11+
note_indexes: Optional[torch.IntTensor]
1212

1313

1414
class BiasedMatrixFactorization(torch.nn.Module):
@@ -35,14 +35,14 @@ def __init__(
3535

3636
self._logging = logging
3737

38-
self.user_factors = torch.nn.Embedding(n_users, n_factors, sparse=False)
39-
self.note_factors = torch.nn.Embedding(n_notes, n_factors, sparse=False)
38+
self.user_factors = torch.nn.Embedding(n_users, n_factors, sparse=False, dtype=torch.float32)
39+
self.note_factors = torch.nn.Embedding(n_notes, n_factors, sparse=False, dtype=torch.float32)
4040

41-
self.user_intercepts = torch.nn.Embedding(n_users, 1, sparse=False)
42-
self.note_intercepts = torch.nn.Embedding(n_notes, 1, sparse=False)
41+
self.user_intercepts = torch.nn.Embedding(n_users, 1, sparse=False, dtype=torch.float32)
42+
self.note_intercepts = torch.nn.Embedding(n_notes, 1, sparse=False, dtype=torch.float32)
4343

4444
self.use_global_intercept = use_global_intercept
45-
self.global_intercept = torch.nn.parameter.Parameter(torch.zeros(1, 1))
45+
self.global_intercept = torch.nn.parameter.Parameter(torch.zeros(1, 1, dtype=torch.float32))
4646
torch.nn.init.xavier_uniform_(self.user_factors.weight)
4747
torch.nn.init.xavier_uniform_(self.note_factors.weight)
4848
self.user_intercepts.weight.data.fill_(0.0)

sourcecode/scoring/matrix_factorization/normalized_loss.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,16 @@ def __init__(
125125
# Finalize weights
126126
weightMap = dict(
127127
((rater, note), weight)
128-
for (rater, note, weight) in ratings[[c.raterParticipantIdKey, c.noteIdKey, "weights"]].values
128+
for (rater, note, weight) in zip(
129+
ratings[c.raterParticipantIdKey], ratings[c.noteIdKey], ratings["weights"]
130+
)
131+
)
132+
self.weights = torch.FloatTensor(
133+
[
134+
weightMap[(rater, note)]
135+
for (rater, note) in zip(ratingOrder[c.raterParticipantIdKey], ratingOrder[c.noteIdKey])
136+
]
129137
)
130-
self.weights = torch.tensor([weightMap[(rater, note)] for (rater, note) in ratingOrder.values])
131138
assert len(self.weights) == len(self.targets)
132139

133140
def forward(self, pred):

sourcecode/scoring/matrix_factorization/pseudo_raters.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .. import constants as c
44
from .matrix_factorization import Constants as mf_c, MatrixFactorization
55

6+
import numpy as np
67
import pandas as pd
78
import torch
89

@@ -236,6 +237,13 @@ def _create_dataset_with_extreme_rating_on_each_note(self, ratingToAddWithoutNot
236237
extremeRatingsToAdd = pd.DataFrame(ratingsWithNoteIds).drop(
237238
[c.internalRaterInterceptKey, c.internalRaterFactor1Key], axis=1
238239
)
240+
extremeRatingsToAdd[c.noteIdKey] = extremeRatingsToAdd[c.noteIdKey].astype(np.int64)
241+
if isinstance(self.ratingFeaturesAndLabels[c.raterParticipantIdKey].dtype, pd.Int64Dtype):
242+
# Only convert ID type from string to Int64 if is necessary to match existing IDs (which is
243+
# expected when running in prod, but not always in unit tests or public data.)
244+
extremeRatingsToAdd[c.raterParticipantIdKey] = extremeRatingsToAdd[
245+
c.raterParticipantIdKey
246+
].astype(pd.Int64Dtype())
239247
ratingFeaturesAndLabelsWithExtremeRatings = pd.concat(
240248
[self.ratingFeaturesAndLabels, extremeRatingsToAdd]
241249
)

sourcecode/scoring/mf_base_scorer.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def __init__(
184184
useReputation: bool = True,
185185
tagFilterPercentile: int = 95,
186186
incorrectFilterThreshold: float = 2.5,
187+
firmRejectThreshold: Optional[float] = None,
187188
):
188189
"""Configure MatrixFactorizationScorer object.
189190
@@ -256,6 +257,7 @@ def __init__(
256257
self._useReputation = useReputation
257258
self._tagFilterPercentile = tagFilterPercentile
258259
self._incorrectFilterThreshold = incorrectFilterThreshold
260+
self._firmRejectThreshold = firmRejectThreshold
259261
mfArgs = dict(
260262
[
261263
pair
@@ -611,6 +613,7 @@ def _prescore_notes_and_users(
611613
incorrectFilterThreshold=self._incorrectFilterThreshold,
612614
tagFilterThresholds=None,
613615
finalRound=False,
616+
firmRejectThreshold=self._firmRejectThreshold,
614617
)
615618
if self._saveIntermediateState:
616619
self.prescoringScoredNotes = scoredNotes
@@ -826,6 +829,7 @@ def _prescore_notes_and_users(
826829
incorrectFilterThreshold=self._incorrectFilterThreshold,
827830
finalRound=False,
828831
factorThreshold=self._factorThreshold,
832+
firmRejectThreshold=self._firmRejectThreshold,
829833
)
830834

831835
# Compute meta output
@@ -847,6 +851,9 @@ def _prescore_notes_and_users(
847851
+ c.notHelpfulTagsTSVOrder
848852
],
849853
),
854+
finalRoundNumRatings=len(finalRoundRatings),
855+
finalRoundNumNotes=finalRoundRatings[c.noteIdKey].nunique(),
856+
finalRoundNumUsers=finalRoundRatings[c.raterParticipantIdKey].nunique(),
850857
)
851858

852859
# Compute user incorrect tag aggregates
@@ -950,6 +957,16 @@ def _score_notes_and_users(
950957
if self._saveIntermediateState:
951958
self.finalRoundRatings = finalRoundRatings
952959

960+
assert (
961+
prescoringMetaScorerOutput.finalRoundNumNotes is not None
962+
), "Missing final round num notes"
963+
assert (
964+
prescoringMetaScorerOutput.finalRoundNumRatings is not None
965+
), "Missing final round num ratings"
966+
assert (
967+
prescoringMetaScorerOutput.finalRoundNumUsers is not None
968+
), "Missing final round num users"
969+
953970
# Re-runs matrix factorization using only ratings given by helpful raters.
954971
with self.time_block("Final helpfulness-filtered MF"):
955972
noteParams, raterParams, globalBias = self._mfRanker.run_mf(
@@ -958,6 +975,9 @@ def _score_notes_and_users(
958975
userInit=prescoringRaterModelOutput,
959976
globalInterceptInit=prescoringMetaScorerOutput.globalIntercept,
960977
freezeRaterParameters=True,
978+
freezeGlobalParameters=True,
979+
ratingPerNoteLossRatio=prescoringMetaScorerOutput.finalRoundNumRatings
980+
/ prescoringMetaScorerOutput.finalRoundNumNotes,
961981
)
962982

963983
if self._saveIntermediateState:
@@ -994,6 +1014,10 @@ def _score_notes_and_users(
9941014
noteInitStateDiligence=prescoringNoteModelOutput,
9951015
raterInitStateDiligence=prescoringRaterModelOutput,
9961016
globalInterceptDiligence=prescoringMetaScorerOutput.lowDiligenceGlobalIntercept,
1017+
ratingsPerNoteLossRatio=prescoringMetaScorerOutput.finalRoundNumRatings
1018+
/ prescoringMetaScorerOutput.finalRoundNumNotes,
1019+
ratingsPerUserLossRatio=prescoringMetaScorerOutput.finalRoundNumRatings
1020+
/ prescoringMetaScorerOutput.finalRoundNumUsers,
9971021
)
9981022
print(f"diligenceNP cols: {diligenceNoteParams.columns}")
9991023
noteParams = noteParams.merge(diligenceNoteParams, on=c.noteIdKey)
@@ -1033,6 +1057,7 @@ def _score_notes_and_users(
10331057
lowDiligenceThreshold=self._lowDiligenceThreshold,
10341058
finalRound=True,
10351059
factorThreshold=self._factorThreshold,
1060+
firmRejectThreshold=self._firmRejectThreshold,
10361061
)
10371062
print(f"sn cols: {scoredNotes.columns}")
10381063

sourcecode/scoring/mf_core_scorer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def __init__(
1212
useStableInitialization: bool = True,
1313
saveIntermediateState: bool = False,
1414
threads: int = c.defaultNumThreads,
15+
firmRejectThreshold: Optional[float] = 0.3,
1516
) -> None:
1617
"""Configure MFCoreScorer object.
1718
@@ -29,6 +30,7 @@ def __init__(
2930
useStableInitialization=useStableInitialization,
3031
saveIntermediateState=saveIntermediateState,
3132
threads=threads,
33+
firmRejectThreshold=firmRejectThreshold,
3234
)
3335

3436
def get_name(self):

sourcecode/scoring/mf_expansion_scorer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(
1414
useStableInitialization: bool = True,
1515
saveIntermediateState: bool = False,
1616
threads: int = c.defaultNumThreads,
17+
firmRejectThreshold: Optional[float] = 0.3,
1718
) -> None:
1819
"""Configure MFExpansionScorer object.
1920
@@ -30,6 +31,7 @@ def __init__(
3031
useStableInitialization=useStableInitialization,
3132
saveIntermediateState=saveIntermediateState,
3233
threads=threads,
34+
firmRejectThreshold=firmRejectThreshold,
3335
)
3436

3537
def get_name(self):

0 commit comments

Comments
 (0)