للتصنيف الثنائي. الجميع يحب المنطقة تحت منحنى المنحنى (AUC) ، ولكن لا أحد يستهدفها مباشرة في وظيفة الخسارة الخاصة بهم. بدلاً من ذلك ، يستخدم الناس وظيفة وكيل مثل الانتروبيا الثنائية (BCE).
هذا يعمل بشكل جيد إلى حد ما ، معظم الوقت. لكننا تركنا سؤالًا مزعجًا: هل يمكننا الحصول على درجة أعلى مع وظيفة الخسارة أقرب إلى طبيعتها إلى AUC؟
يبدو أنه من المحتمل أن يكون BCE حقًا علاقة ضئيلة جدًا بـ AUC. كانت هناك العديد من المحاولات لإيجاد وظيفة خسارة تستهدف AUC بشكل مباشر. (أحد التكتيكات الشائعة هو شكل من أشكال وظائف فقدان الترتيب مثل فقدان الرتبة المفصلية.) في الممارسة العملية ، لم يظهر أي فائز واضح على الإطلاق. لم يكن هناك تحد خطير ل BCE.
هناك أيضا اعتبارات تتجاوز الأداء. نظرًا لأن BCE يختلف بشكل أساسي عن AUC ، فإن BCE تميل إلى سوء التصرف في الامتداد النهائي للتدريب حيث نحاول توجيهه نحو أعلى درجة AUC.
هناك قدر كبير من تحسين AUC في الواقع يحدث في ضبط المعلمات المفرطة. يصبح التوقف المبكر ضرورة غير مريحة لأن النموذج قد يتباعد بشكل حاد في أي وقت من درجته العالية.
نود وظيفة الخسارة التي تعطينا درجات أعلى وأقل مشكلة.
نقدم هذه الوظيفة هنا.
تعريف العمل المفضل لدي لـ AUC هو: دعنا نسمي ملصقات الفئة الثنائية "Black" (0) و "White" (1). اختر عنصرًا أسود واحد بشكل عشوائي ودع x يكون قيمته المتوقعة. الآن اختر عنصرًا أبيض عشوائيًا مع القيمة Y. ثم،
AUC = احتمال أن تكون العناصر بالترتيب الصحيح. هذا هو ، x < y .
هذا كل شيء. بالنسبة لأي مجموعة معينة من النقاط مثل مجموعة التدريب ، يمكننا الحصول على هذا الاحتمال من خلال إجراء حساب القوة الغاشمة. مسح مجموعة من جميع الأزواج السوداء/البيضاء الممكنة ، وحساب الجزء الذي تم ترتيبه الأيمن.
يمكننا أن نرى أن درجة AUC غير قابلة للتفاضلة (منحنى سلس فيما يتعلق بأي x أو y . يبقى AUC كما هو. بمجرد أن تعبر النقطة أحد الجيران ، لدينا فرصة لتقليب واحدة من مقارنات X <y - والتي تغير AUC. لذلك لا يقوم AUC بالتحولات السلسة.
هذه مشكلة بالنسبة للشبكات العصبية ، حيث نحتاج إلى وظيفة خسارة قابلة للتمييز.
لذلك شرعنا في العثور على وظيفة قابلة للتمييز قريبة قدر الإمكان من AUC.
لقد حفرت من خلال الأدبيات الموجودة ولم أجد شيئًا يعمل في الممارسة العملية. أخيرًا ، صادفت قطعة رمز غريبة قام بها شخص ما إلى قاعدة Tflearn Codebase.
بدون ضجة ، وعدت الخلاص القابل للتمييز من BCE في شكل وظيفة خسارة جديدة.
( لا تحاول ذلك ، فهي تهب .): http://tflearn.org/objectives/#roc-auc-score
def roc_auc_score(y_pred, y_true):
"""Bad code, do not use.
ROC AUC Score.
Approximates the Area Under Curve score, using approximation based on
the Wilcoxon-Mann-Whitney U statistic.
Yan, L., Dodier, R., Mozer, M. C., & Wolniewicz, R. (2003).
Optimizing Classifier Performance via an Approximation to the Wilcoxon-Mann-Whitney Statistic.
Measures overall performance for a full range of threshold levels.
Arguments:
y_pred: `Tensor`. Predicted values.
y_true: `Tensor` . Targets (labels), a probability distribution.
"""
with tf.name_scope("RocAucScore"):
pos = tf.boolean_mask(y_pred, tf.cast(y_true, tf.bool))
neg = tf.boolean_mask(y_pred, ~tf.cast(y_true, tf.bool))
.
.
more bad code)
لا يعمل على الإطلاق. (النفخ هو في الواقع أقل مشاكلها). لكنه يذكر الورقة التي كانت تستند إليها.
على الرغم من أن الورقة قديمة ، والتي تعود إلى عام 2003 ، وجدت أنه مع القليل من العمل - بعض تمديد الرياضيات والترميز الدقيق - فهي تعمل بالفعل. إنه سريع الحجم ، مع السرعة مماثلة لـ BCE (ومثل قابلة للتجزئة ل GPU/MPP) *. في اختباراتي ، يعطي درجات أعلى من AUC من BCE ، أقل حساسية لمعدل التعلم (تجنب الحاجة إلى جدولة في اختباراتي) ، ويزيل بالكامل الحاجة إلى التوقف المبكر.
حسنًا ، دعنا ننتقل إلى الورقة الأصلية: تحسين أداء المصنف من خلال تقريبًا إلى Wilcoxon - Mann - Whitney Statistic .
المؤلفون ، يان وآخرون. AL ، تحفيز المناقشة من خلال كتابة درجة AUC في شكل معين. أذكر مثالنا حيث نحسب AUC عن طريق القيام بعدد القوة الغاشمة على مجموعة الأزواج السوداء/البيضاء المحتملة للعثور على الجزء الذي يتم الترتيب الأيمن. دع B يكون مجموعة من القيم السوداء و W مجموعة القيم البيضاء. يتم تقديم جميع الأزواج الممكنة بواسطة المنتج الديكارتي ب x ث . لحساب الأزواج المرتبة اليمنى التي نكتبها:
هذا هو في الحقيقة مجرد تدوين رياضي ليقول "عد الأزواج المرتبة اليمنى". إذا قمنا بتقسيم هذا المبلغ على إجمالي عدد الأزواج ، | ب | * | ث | ، نحصل بالضبط على مقياس AUC. (تاريخيا ، هذا يسمى إحصاء Wilcoxon-Mann-Whitney (WMW) المعتاد.)
لجعل وظيفة الخسارة من هذا ، يمكننا فقط قلب X <y مقارنة بـ x> y من أجل معاقبة الأزواج ذات الطلب الخاطئ. المشكلة ، بالطبع ، هي تلك القفزة المتقطعة عندما تعبر x .
يان وآخرون. الدراسات الاستقصائية - ثم ترفض - عمليات العمل السابقة باستخدام تقريب مستمر لوظيفة الخطوة (heaviside) ، مثل منحنى السيني. ثم يسحبون هذا من قبعة:
حصلت Yann على هذا المنتدى من خلال تطبيق سلسلة من التغييرات على WMW:
سنذهب من خلال هذه بدوره. 1 واضح بما فيه الكفاية. هناك ما يزيد قليلاً عن 2 مما تراه العين. من المنطقي أنه ينبغي إعطاء الأزواج ذات الطلب الخاطئ مع فصل واسع الخسارة أكبر. لكن هناك شيء مثير للاهتمام يحدث أيضًا مع اقتراب هذا الفصل 0. الخسارة تصل إلى الصفر خطيًا ، بدلاً من الوظيفة الخطية. لذلك تخلصنا من التوقف.
في الواقع ، إذا كانت P 1 و γ كانت 0 ، فإن الخسارة ستكون ببساطة صديقنا القديم RELU (XY). لكن في هذه الحالة ، نلاحظ وجود الفواق ، والذي يكشف عن الحاجة إلى الأسس p . Relu غير قابل للتمييز في 0. هذا ليس مشكلة كبيرة في دور Relu الأكثر معتادًا كدالة تنشيط ، ولكن لأغراضنا ، فإن التفرد في 0 يهبط مباشرة على الشيء الأكثر اهتمامًا به: النقاط حيث العناصر البيضاء والأسود مرور بعضنا البعض.
لحسن الحظ ، فإن رفع RELU إلى الطاقة يحدد هذا. RELU^P مع p> 1 يمكن التمييز في كل مكان. طيب ، لذلك p> 1.
الآن العودة إلى γ: γ يوفر "حشوة" يتم تنفيذها بين نقطتين. نحن نعاقب ليس فقط الأزواج ذات الطلب الخاطئ ، ولكن أيضًا الأزواج المرتبة اليمنى والتي تكون قريبة جدًا . إذا كان الزوج الذي تم الترتيب الأيمن قريبًا جدًا ، فإن عناصره معرضة لخطر تبديل في المستقبل من خلال الهزات العشوائية لشبكة عصبية عشوائية. الفكرة هي إبقائهم يتحركون بعيدًا حتى يصلوا إلى مسافة مريحة.
وهذه هي الفكرة الأساسية كما هو موضح في الورقة. نحن الآن نلتقي بعض التحسينات بخصوص γ و P.
هنا نقطع قليلا مع الورقة. يان وآخرون. يبدو أن A Al suceamish قليلاً حول موضوع اختيار γ و P ، مما يوفر فقط أن p = 2 أو p = 3 يبدو جيدًا وأنه يجب أن يكون في مكان ما بين 0.10 و 0.70. يان يتمنى لنا التوفيق في هذه المعلمات والأقواس.
أولاً ، نصلح بشكل دائم p = 2 ، لأن أي وظيفة خسارة تحترم ذاتية يجب أن تكون مجموع المربعات. (أحد أسباب ذلك هو أنه يضمن أن وظيفة الخسارة لا يمكن تفكيكها فحسب ، بل أيضًا محدب )
ثانياً والأهم من ذلك ، دعونا نلقي نظرة على γ. تبدو مجريات الأمور "في مكان ما من 0.10 إلى 0.70" غريبًا على وجهه ؛ حتى لو تم تطبيع التنبؤات لتكون 0 <x <1 ، فإن هذا التوجيه يبدو أنه غير مبال ، غير مبال بالتوزيعات الأساسية ، والغريبة.
سنشتق γ من مجموعة التدريب.
النظر في مجموعة التدريب وأزواجها السوداء/البيضاء ، ب x ث . هناك | ب || ث | أزواج في هذه المجموعة. من هؤلاء ، | ب | | ث | AUC هي أمر يمين. لذلك ، فإن عدد الأزواج ذات الطلب الخاطئ هو (1-AUC) | ب | | ث |
عندما يكون γ صفرًا ، فإن هذه الأزواج ذات الطلب الخاطئ هي فقط في الحركة (لها خسارة إيجابية) بدلاً من القلق بشأن القيمة الرقمية لـ γ ، سنحدد عدد أزواج الإشعاع التي نريد ضبطها في الحركة:
نحن نحدد Δ ثابتًا يحدد نسبة الأزواج الإدارية للغاية إلى أزواج ذات طلب خاطئ.
نصلح هذا Δ خلال التدريب والتحديث γ لتوافقه. بالنسبة إلى Δ ، نجد γ مثل هذا
في تجاربنا ، وجدنا أن Δ يمكن أن تتراوح من 0.5 إلى 2.0 ، و 1.0 هو خيار افتراضي جيد.
لذلك قمنا بتعيين Δ إلى 1 ، p إلى 2 ، وننسى γ تمامًا ،
تبدو وظيفة الخسارة الخاصة بنا (1) باهظة الثمن لحسابها. يتطلب الأمر مسح مجموعة التدريب بأكملها لكل تنبؤ فردي.
نتجاوز هذه المشكلة مع قرص الأداء:
لنفترض أننا نحسب وظيفة الخسارة لنقطة بيانات بيضاء معينة ، x . لحساب (3) ، نحتاج إلى مقارنة x مقابل مجموعة التدريب بأكملها من التنبؤات السوداء ، ذ . نأخذ اختصارًا ونستخدم عينة فرعية عشوائية من نقاط البيانات السوداء. إذا قمنا بتعيين حجم العينة الفرعية ليكون ، على سبيل المثال ، 1000 - نحصل على تقريب عن كثب إلى وظيفة الخسارة الحقيقية. [1]
ينطبق التفكير المماثل على وظيفة فقدان نقطة البيانات السوداء ؛ نستخدم عينة فرعية عشوائية من جميع عناصر التدريب الأبيض.
وبهذه الطريقة ، تناسب العينات الفرعية البيضاء والأسود بسهولة في ذاكرة GPU. عن طريق إعادة استخدام العينة الفرعية نفسها عبر مجموعة معينة ، يمكننا موازاة العملية على دفعات. ينتهي بنا المطاف بوظيفة الخسارة التي تكون بسرعة في BCE.
إليكم وظيفة خسارة الدُفعات في Pytorch:
def roc_star_loss( _y_true, y_pred, gamma, _epoch_true, epoch_pred):
"""
Nearly direct loss function for AUC.
See article,
C. Reiss, "Roc-star : An objective function for ROC-AUC that actually works."
https://github.com/iridiumblue/articles/blob/master/roc_star.md
_y_true: `Tensor`. Targets (labels). Float either 0.0 or 1.0 .
y_pred: `Tensor` . Predictions.
gamma : `Float` Gamma, as derived from last epoch.
_epoch_true: `Tensor`. Targets (labels) from last epoch.
epoch_pred : `Tensor`. Predicions from last epoch.
"""
#convert labels to boolean
y_true = (_y_true>=0.50)
epoch_true = (_epoch_true>=0.50)
# if batch is either all true or false return small random stub value.
if torch.sum(y_true)==0 or torch.sum(y_true) == y_true.shape[0]: return torch.sum(y_pred)*1e-8
pos = y_pred[y_true]
neg = y_pred[~y_true]
epoch_pos = epoch_pred[epoch_true]
epoch_neg = epoch_pred[~epoch_true]
# Take random subsamples of the training set, both positive and negative.
max_pos = 1000 # Max number of positive training samples
max_neg = 1000 # Max number of positive training samples
cap_pos = epoch_pos.shape[0]
cap_neg = epoch_neg.shape[0]
epoch_pos = epoch_pos[torch.rand_like(epoch_pos) < max_pos/cap_pos]
epoch_neg = epoch_neg[torch.rand_like(epoch_neg) < max_neg/cap_pos]
ln_pos = pos.shape[0]
ln_neg = neg.shape[0]
# sum positive batch elements agaionst (subsampled) negative elements
if ln_pos>0 :
pos_expand = pos.view(-1,1).expand(-1,epoch_neg.shape[0]).reshape(-1)
neg_expand = epoch_neg.repeat(ln_pos)
diff2 = neg_expand - pos_expand + gamma
l2 = diff2[diff2>0]
m2 = l2 * l2
len2 = l2.shape[0]
else:
m2 = torch.tensor([0], dtype=torch.float).cuda()
len2 = 0
# Similarly, compare negative batch elements against (subsampled) positive elements
if ln_neg>0 :
pos_expand = epoch_pos.view(-1,1).expand(-1, ln_neg).reshape(-1)
neg_expand = neg.repeat(epoch_pos.shape[0])
diff3 = neg_expand - pos_expand + gamma
l3 = diff3[diff3>0]
m3 = l3*l3
len3 = l3.shape[0]
else:
m3 = torch.tensor([0], dtype=torch.float).cuda()
len3=0
if (torch.sum(m2)+torch.sum(m3))!=0 :
res2 = torch.sum(m2)/max_pos+torch.sum(m3)/max_neg
#code.interact(local=dict(globals(), **locals()))
else:
res2 = torch.sum(m2)+torch.sum(m3)
res2 = torch.where(torch.isnan(res2), torch.zeros_like(res2), res2)
return res2
لاحظ أن هناك بعض المعلمات الإضافية. نحن نمر في مجموعة التدريب من آخر الحقبة . نظرًا لأن مجموعة التدريب بأكملها لا تتغير كثيرًا من عصر إلى آخر ، يمكن أن تقارن وظيفة الخسارة كل تنبؤ مرة أخرى بمجموعة تدريب خارج التاريخ. هذا يبسط تصحيح الأخطاء ، ويبدو أنه يستفيد من الأداء لأن عصر "الخلفية" لا تتغير من دفعة واحدة إلى أخرى.
وبالمثل ، γ هو حساب باهظ الثمن. نستخدم مرة أخرى خدعة أخذ العينات الفرعية ، لكننا نزيد من حجم العينات الفرعية إلى حوالي 10،000 لضمان تقدير دقيق. للحفاظ على قطع الأداء على طول ، نقوم بإعادة حساب هذه القيمة مرة واحدة فقط لكل فترة. ها هي الوظيفة للقيام بذلك:
def epoch_update_gamma(y_true,y_pred, epoch=-1,delta=2):
"""
Calculate gamma from last epoch's targets and predictions.
Gamma is updated at the end of each epoch.
y_true: `Tensor`. Targets (labels). Float either 0.0 or 1.0 .
y_pred: `Tensor` . Predictions.
"""
DELTA = delta
SUB_SAMPLE_SIZE = 2000.0
pos = y_pred[y_true==1]
neg = y_pred[y_true==0] # yo pytorch, no boolean tensors or operators? Wassap?
# subsample the training set for performance
cap_pos = pos.shape[0]
cap_neg = neg.shape[0]
pos = pos[torch.rand_like(pos) < SUB_SAMPLE_SIZE/cap_pos]
neg = neg[torch.rand_like(neg) < SUB_SAMPLE_SIZE/cap_neg]
ln_pos = pos.shape[0]
ln_neg = neg.shape[0]
pos_expand = pos.view(-1,1).expand(-1,ln_neg).reshape(-1)
neg_expand = neg.repeat(ln_pos)
diff = neg_expand - pos_expand
ln_All = diff.shape[0]
Lp = diff[diff>0] # because we're taking positive diffs, we got pos and neg flipped.
ln_Lp = Lp.shape[0]-1
diff_neg = -1.0 * diff[diff<0]
diff_neg = diff_neg.sort()[0]
ln_neg = diff_neg.shape[0]-1
ln_neg = max([ln_neg, 0])
left_wing = int(ln_Lp*DELTA)
left_wing = max([0,left_wing])
left_wing = min([ln_neg,left_wing])
default_gamma=torch.tensor(0.2, dtype=torch.float).cuda()
if diff_neg.shape[0] > 0 :
gamma = diff_neg[left_wing]
else:
gamma = default_gamma # default=torch.tensor(0.2, dtype=torch.float).cuda() #zoink
L1 = diff[diff>-1.0*gamma]
ln_L1 = L1.shape[0]
if epoch > -1 :
return gamma
else :
return default_gamma
إليك طريقة عرض المروحية التي توضح كيفية استخدام وظيفتين أثناء حلقاتنا على الحقبة ، ثم على دفعات:
train_ds = CatDogDataset(train_files, transform)
train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE)
#initialize last epoch with random values
last_epoch_y_pred = torch.tensor( 1.0-numpy.random.rand(len(train_ds))/2.0, dtype=torch.float).cuda()
last_epoch_y_t = torch.tensor([o for o in train_tt],dtype=torch.float).cuda()
epoch_gamma = 0.20
for epoch in range(epoches):
epoch_y_pred=[]
epoch_y_t=[]
for X, y in train_dl:
preds = model(X)
# .
# .
loss = roc_star_loss(y,preds,epoch_gamma, last_epoch_y_t, last_epoch_y_pred)
# .
# .
epoch_y_pred.extend(preds)
epoch_y_t.extend(y)
last_epoch_y_pred = torch.tensor(epoch_y_pred).cuda()
last_epoch_y_t = torch.tensor(epoch_y_t).cuda()
epoch_gamma = epoch_update_gamma(last_epoch_y_t, last_epoch_y_pred, epoch)
#...
يمكن العثور على مثال عمل كامل هنا ، مثال
أدناه نقوم برسم أداء ROC-Star مقابل نفس النموذج باستخدام BCE. تُظهر التجربة أنه يمكن ببساطة تبديل نجوم ROC في أي نموذج باستخدام BCE مع فرصة جيدة لزيادة الأداء.