สำหรับการจำแนกประเภทไบนารี ทุกคนชอบพื้นที่ภายใต้ตัวชี้วัดเส้นโค้ง (AUC) แต่ไม่มีใครกำหนดเป้าหมายโดยตรงในฟังก์ชั่นการสูญเสียของพวกเขา ผู้คนใช้ฟังก์ชั่นพร็อกซีเช่นไบนารีข้ามเอนโทรปี (BCE)
วิธีนี้ใช้ได้ดีพอสมควร แต่เรายังคงมีคำถามที่จู้จี้: เราจะได้คะแนนที่สูงขึ้นด้วยฟังก์ชั่นการสูญเสียใกล้เคียงกับ AUC หรือไม่?
ดูเหมือนว่าจะเป็นเพราะ BCE มีความสัมพันธ์กับ AUC น้อยมาก มีความพยายามมากมายในการค้นหาฟังก์ชั่นการสูญเสียที่กำหนดเป้าหมายโดยตรง AUC (ชั้นเชิงหนึ่งทั่วไปคือรูปแบบของฟังก์ชั่นการสูญเสียอันดับเช่นบานพับการสูญเสียอันดับ) อย่างไรก็ตามในทางปฏิบัติอย่างไรก็ตามไม่มีผู้ชนะที่ชัดเจนเกิดขึ้น ไม่มีความท้าทายอย่างจริงจังต่อ BCE
นอกจากนี้ยังมีการพิจารณานอกเหนือจากประสิทธิภาพ เนื่องจาก BCE นั้นแตกต่างจาก AUC เป็นหลัก BCE จึงมีแนวโน้มที่จะประพฤติตัวไม่เหมาะสมในการฝึกซ้อมครั้งสุดท้ายที่เราพยายามที่จะนำไปสู่คะแนน AUC สูงสุด
การเพิ่มประสิทธิภาพ AUC ที่ดีจริง ๆ แล้วเกิดขึ้นในการปรับพารามิเตอร์ไฮเปอร์พารามิเตอร์ การหยุดก่อนกำหนดกลายเป็นสิ่งจำเป็นที่ไม่สบายใจเนื่องจากแบบจำลองอาจแตกต่างอย่างมากเมื่อใดก็ได้จากคะแนนสูง
เราต้องการฟังก์ชั่นการสูญเสียที่ทำให้เรามีคะแนนสูงขึ้นและมีปัญหาน้อยลง
เรานำเสนอฟังก์ชั่นดังกล่าวที่นี่
คำจำกัดความการทำงานที่ฉันโปรดปรานของ AUC คือ: เรียกป้ายชื่อไบนารีคลาส "แบล็ก" (0) และ "ขาว" (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 แต่ฉันก็พบว่าด้วยงานเล็ก ๆ น้อย ๆ - ส่วนขยายของคณิตศาสตร์และการเข้ารหัสอย่างระมัดระวัง - มันใช้งานได้จริง มันเป็น Uber-fast โดยมีความเร็วเทียบเคียงได้กับ BCE (และเช่นเดียวกับ vectorizable สำหรับ GPU/MPP) * ในการทดสอบของฉันมันให้คะแนน AUC สูงกว่า BCE นั้นมีความอ่อนไหวต่ออัตราการเรียนรู้น้อยกว่า (หลีกเลี่ยงความจำเป็นในการกำหนดตารางเวลาในการทดสอบของฉัน) และลดความจำเป็นในการหยุดเร็ว
โอเคหันไปหาบทความต้นฉบับ: เพิ่มประสิทธิภาพการทำงานของตัวจําแนกผ่านการประมาณค่าวิลคอกซอน - แมนน์ - วิทนีย์สถิติ
ผู้เขียน Yan et อัล กระตุ้นการสนทนาโดยการเขียนคะแนน AUC ในรูปแบบเฉพาะ จำตัวอย่างของเราที่เราคำนวณ AUC โดยการนับจำนวนเดรัจฉานในชุดคู่ดำ/ขาวที่เป็นไปได้เพื่อค้นหาส่วนที่สั่งซื้อขวา ให้ b เป็นชุดของค่าสีดำและ เป็น ชุดของค่าสีขาว คู่ที่เป็นไปได้ทั้งหมดได้รับจากผลิตภัณฑ์คาร์ทีเซียน b x w เพื่อนับคู่ที่สั่งซื้อขวาที่เราเขียน:
นี่เป็นเพียงการ จำกัด สัญกรณ์ทางคณิตศาสตร์ที่จะพูดว่า 'นับคู่ที่สั่งซื้อขวา' หากเราแบ่งผลรวมนี้ด้วยจำนวนคู่ทั้งหมด | B | - W | เราได้รับ AUC Metric (ในอดีตสิ่งนี้เรียกว่าสถิติ Wilcoxon-Mann-Whitney (WMW) ปกติ)
ในการสร้างฟังก์ชั่นการสูญเสียจากสิ่งนี้เราสามารถพลิก X <y เปรียบเทียบกับ x> y เพื่อลงโทษคู่ที่สั่งซื้อผิด แน่นอนว่าปัญหาคือการกระโดดที่ไม่ต่อเนื่องเมื่อ x ข้าม y
Yan et. การสำรวจ อัล - และจากนั้นปฏิเสธ - การทำงานที่ผ่านมาโดยใช้การประมาณอย่างต่อเนื่องกับฟังก์ชั่นขั้นตอน (heaviside) เช่นเส้นโค้ง sigmoid จากนั้นพวกเขาก็ดึงมันออกมาจากหมวก:
Yann ได้ฟอร์ลูมูลานี้โดยใช้ชุดของการเปลี่ยนแปลงกับ WMW:
เราจะผ่านสิ่งเหล่านี้ในทางกลับกัน 1 ชัดเจนพอ มีมากกว่า 2 เล็กน้อยกว่าที่ตา มันสมเหตุสมผลที่เข้าใจง่ายว่าคู่ที่สั่งซื้อผิดที่มีการแยกกว้างควรได้รับการสูญเสียที่ใหญ่กว่า แต่สิ่งที่น่าสนใจก็เกิดขึ้นเช่นกันเมื่อการแยกนั้นเข้าใกล้ 0. การสูญเสียไปเป็นศูนย์เป็นเส้นตรงแทนที่จะใช้งานขั้นตอน ดังนั้นเราจึงกำจัดความไม่ต่อเนื่อง
ในความเป็นจริงถ้า p คือ 1 และγเป็น 0 การสูญเสียจะเป็นเพียงเพื่อนเก่าของเรา Relu (XY) แต่ในกรณีนี้เราสังเกตเห็นอาการสะอึกซึ่งเผยให้เห็นถึงความจำเป็นสำหรับเลขชี้กำลัง p Relu ไม่แตกต่างกันที่ 0 นั่นไม่ใช่ปัญหามากนักในบทบาทที่คุ้นเคยมากขึ้นของ Relu ในฐานะฟังก์ชั่นการเปิดใช้งาน แต่เพื่อจุดประสงค์ของเราความเป็นเอกเทศที่ 0 ลงจอดโดยตรงกับสิ่งที่เราสนใจมากที่สุดใน: จุดที่องค์ประกอบสีขาวและสีดำ ผ่านกันและกัน
โชคดีที่เพิ่มความสัมพันธ์กับพลังในการแก้ไขปัญหานี้ relu^p กับ p> 1 มีความแตกต่างกันทุกที่ ตกลงดังนั้น p> 1.
ตอนนี้กลับไปที่γ: γให้ 'ช่องว่างภายใน' ซึ่งบังคับใช้ระหว่างสองจุด เราลงโทษไม่เพียง แต่คู่ที่สั่งไม่ถูกต้อง แต่ยังมีคู่ที่สั่งซื้อขวาซึ่งอยู่ ใกล้เกินไป หากคู่ที่สั่งซื้ออยู่ใกล้เกินไปองค์ประกอบของมันมีความเสี่ยงที่จะได้รับการสลับในอนาคตโดยการกระตุกแบบสุ่มของตาข่ายประสาทสุ่ม ความคิดคือทำให้พวกเขาเคลื่อนไหวออกจากกันจนกว่าพวกเขาจะไปถึงระยะทางที่สะดวกสบาย
และนั่นคือความคิดพื้นฐานที่ระบุไว้ในกระดาษ ตอนนี้เรามีการปรับแต่งบางอย่างเกี่ยวกับγและ p
ที่นี่เราทำลายกระดาษเล็กน้อย Yan et. อัล ดูคลื่นไส้เล็กน้อยในหัวข้อการเลือกγและ P โดยเสนอว่า P = 2 หรือ P = 3 ดูเหมือนจะดีและควรอยู่ที่ไหนสักแห่งระหว่าง 0.10 ถึง 0.70 Yan เป็นหลักหวังว่าเราจะโชคดีกับพารามิเตอร์เหล่านี้และโค้งคำนับ
ก่อนอื่นเราแก้ไข P = 2 อย่างถาวรเนื่องจากฟังก์ชั่นการสูญเสียที่เคารพตนเองควรเป็นผลรวมของสแควร์ส (เหตุผลหนึ่งสำหรับเรื่องนี้ก็คือมันทำให้มั่นใจได้ว่าฟังก์ชั่นการสูญเสียไม่เพียง แต่แตกต่างกัน แต่ยัง นูน )
ประการที่สองและที่สำคัญลองมาดูγ ฮิวริสติกของ 'ที่ไหนสักแห่งจาก 0.10 ถึง 0.70' ดูแปลก ๆ บนใบหน้าของมัน; แม้ว่าการคาดการณ์จะถูกทำให้เป็นมาตรฐานเป็น 0 <x <1 แต่คำแนะนำนี้ดูเหมือนจะมากเกินไปไม่แยแสต่อการแจกแจงพื้นฐานและแปลก ๆ
เราจะได้รับγจากชุดฝึกอบรม
พิจารณาชุดการฝึกอบรมและคู่สีดำ/ขาว b x w มี | B || W | pairs in this set. Of these, | B | - W | AUC มีการสั่งซื้อขวา ดังนั้นจำนวนคู่ที่สั่งผิดคือ (1-AUC) | B | - W |
เมื่อγเป็นศูนย์เฉพาะคู่ที่สั่งผิดเหล่านี้จะเคลื่อนไหว (มีการสูญเสียในเชิงบวก) บวกจะขยายชุดคู่ที่เคลื่อนไหวเพื่อรวมบางคู่ที่มีการสั่งซื้อขวา แต่ใกล้เกินไป แทนที่จะกังวลเกี่ยวกับค่าตัวเลขของγเราจะระบุจำนวนคู่ที่ใกล้เกินไปที่เราต้องการตั้งค่าในการเคลื่อนไหว:
เรากำหนดค่าคงที่Δซึ่งจะแก้ไขสัดส่วนของคู่ที่ใกล้ชิดเกินไปกับคู่ที่สั่งผิด
เราแก้ไขΔนี้ตลอดการฝึกอบรมและอัปเดตγเพื่อให้สอดคล้องกับมัน สำหรับΔเราพบว่าγนั้น
ในการทดลองของเราเราพบว่าΔสามารถอยู่ในช่วง 0.5 ถึง 2.0 และ 1.0 เป็นตัวเลือกเริ่มต้นที่ดี
ดังนั้นเราจึงตั้งค่าΔเป็น 1, p เป็น 2 และลืมเกี่ยวกับγทั้งหมด
ฟังก์ชั่นการสูญเสียของเรา (1) ดูมีราคาแพงมากในการคำนวณ มันต้องการให้เราสแกนชุดการฝึกอบรมทั้งหมดสำหรับการทำนายแต่ละครั้ง
เราข้ามปัญหานี้ด้วยการปรับแต่งประสิทธิภาพ:
Suppose we are calculating the loss function for a given white data point, x . To calculate (3), we need to compare x against the entire training set of black predictions, y . We take a short-cut and use a random sub-sample of the black data points. If we set the size of the sub-sample to be, say, 1000 - we get a very (very) close approximation to the true loss function. [1]
Similar reasoning applies to the loss function of a black data-point; we use a random sub-sample of all white training elements.
ด้วยวิธีนี้ตัวอย่างย่อยสีขาวและสีดำพอดีกับหน่วยความจำ 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)
#...
ตัวอย่างการทำงานที่สมบูรณ์สามารถพบได้ที่นี่ตัวอย่าง py สำหรับการกระโดดที่เร็วขึ้นคุณสามารถแยกเคอร์เนลนี้บน kaggle: เคอร์เนล
ด้านล่างเราจัดทำแผนภูมิประสิทธิภาพของ ROC-Star กับรุ่นเดียวกันโดยใช้ BCE ประสบการณ์แสดงให้เห็นว่า Roc-Star มักจะถูกเปลี่ยนเป็นรุ่นใด ๆ โดยใช้ BCE ด้วยโอกาสที่ดีในการเพิ่มประสิทธิภาพ