φυβλαςのβλογ
บล็อกของ phyblas



[python] วาดเส้นกราฟ ROC เพื่อประเมินผลการทำนาย
เขียนเมื่อ 2017/10/16 10:12
ในตอนที่แล้วได้แนะนำคะแนน f1 ไป https://phyblas.hinaboshi.com/20171014

นอกจากนี้แล้วก็ยังมีอีกวิธีหนึ่งในการพิจารณาประสิทธิภาพของแบบจำลองที่ใช้ทำนาย นั่นคือการดูกราฟ ROC (Receiver operating characteristic)

การจะอธิบายว่า ROC คืออะไรนั้นหากเริ่มจากอธิบายเป็นคำพูดเลยคงจะยาก ดังนั้นขอเริ่มด้วยการยกตัวอย่าง

สมมุติว่าข้อมูลการตรวจโรคจากสารในร่างกายของคน 1000 คนออกมาเป็นดังนี้ โดยแกนนอนคือปริมาณสาร a แกนนอนคือปริมาณสาร b สีเขียวคือผลบวก (มีโรค) สีน้ำเงินคือผลลบ (ไม่มีโรค)



โค้ดสำหรับสร้างข้อมูลและวาดภาพเขียนได้ดังนี้
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
X,z = datasets.make_blobs(n_samples=1000,n_features=2,centers=2,cluster_std=3,center_box=[3,20],random_state=2)
X = np.abs(X)
plt.figure(figsize=[7,6])
plt.gca(aspect=1)
plt.xlabel('ปริมาณสาร a',family='Tahoma')
plt.ylabel('ปริมาณสาร b',family='Tahoma')
plt.scatter(X[:,0],X[:,1],c=z,s=30,alpha=0.5,edgecolor='k',cmap='winter')
plt.show()

จากนั้นเราอาจใช้ LogisticRegression ของ sklearn เพื่อหาจุดแบ่งในการทำนายว่าถ้าตรวจพบคนไข้ที่มีปริมาณสาร a และสาร b เท่านี้จะสรุปว่าเป็นโรคหรือไม่

เราสามารถคำนวณความน่าจะเป็นของการเกิดผลบวกในแต่ละบริเวณได้โดยใช้เมธอด .predict_proba แบบนี้
from sklearn.linear_model import LogisticRegression as Lori
lori = Lori()
lori.fit(X,z)
mX = np.meshgrid(np.linspace(X[:,0].min()-1,X[:,0].max()+1,101),np.linspace(-1,X[:,1].max()+1,101))
mX = np.stack(mX,2)
mz = lori.predict_proba(mX.reshape(-1,2))[:,1].reshape(101,101)
plt.figure(figsize=[7,6])
plt.gca(aspect=1)
plt.scatter(X[:,0],X[:,1],c=z,s=30,edgecolor='k',cmap='winter')
plt.contourf(mX[:,:,0],mX[:,:,1],mz,100,vmin=0,vmax=1,alpha=0.5,cmap='winter',zorder=0)
plt.colorbar(pad=0.01,aspect=50)
plt.show()


จากผลที่ได้ตรงนี้ ปกติแล้วเวลาตัดสินว่าผลเป็นบวกหรือเปล่าก็จะดูว่าความเป็นไปได้ถึง 0.5 หรือไม่ กล่าวคือ ขีดเส้นประสีแดงคั่นแบบนี้
proba = lori.predict_proba(X)[:,1]
threshold = 0.5
ox = (proba>threshold)==z
plt.figure(figsize=[7,6])
plt.gca(aspect=1)
plt.scatter(X[ox,0],X[ox,1],c=z[ox],s=30,edgecolor='k',cmap='winter')
plt.scatter(X[~ox,0],X[~ox,1],c=z[~ox],s=30,edgecolor='r',cmap='winter')
plt.contour(mX[:,:,0],mX[:,:,1],mz>threshold,1,vmin=0,vmax=1,linestyles='--',colors='r')
plt.contourf(mX[:,:,0],mX[:,:,1],mz,100,vmin=0,vmax=1,alpha=0.5,cmap='winter',zorder=0)
plt.colorbar(pad=0.01,aspect=50)
plt.show()


จุดที่ถูกล้อมกรอบสีแดงคือส่วนที่ถูกจำแนกผิด

อย่างไรก็ตาม หากเราลองปรับเปลี่ยนว่าให้ผลความน่าจะเป็นเป็น 0.8 จึงจะตัดสินว่าเป็นผลบวก จะเป็นแบบนี้ (ลองแก้ threshlod เป็น 0.8 แล้ววาดใหม่)



จะเห็นว่าผลที่ถูกทายว่าเป็นผลบวกมีน้อยลง

ส่วนทางกลับกันหากลองปรับให้เป็น 0.2 เส้นแบ่งก็จะเลื่อนลงมาด้านล่าง



การเปลี่ยนค่าจุดแบ่งนี้มีผลทำให้อัตราผลบวกจริง (true positive rate, tpr) และอัตราผลบวกปลอม (false positive rate, fpr) มีค่าเปลี่ยนแปลงไป

อัตราผลบวกจริง ก็คือค่าความระลึก (recall) ซึ่งกล่าวถึงไปในบทความก่อน คำนวณได้จาก
อัตราผลบวกจริง = บวกจริง/(บวกจริง+ลบปลอม)

ส่วนอัตราผลบวกปลอม คำนวณได้จาก
อัตราผลบวกปลอม = บวกปลอม/(บวกปลอม+ลบจริง)

ค่าทั้งสองนี้อาจคำนวณได้ดังนี้
t = proba>threshold
tpr = (z==t).T[z==1].mean()
fpr = (z!=t).T[z==0].mean()

threshold คือจุดแบ่ง อาจมีค่าตั้งแต่ 0 จนถึง 1 ค่าจุดแบ่งมีผลต่อค่า tpr และ fpr

แล้วถ้าหากเราลองทำการวาดกราฟที่แสดงความสัมพันธ์ระหว่างค่าจุดแบ่ง โดยเขียนตามนี้
mX = np.meshgrid(np.linspace(X[:,0].min()-1,X[:,0].max()+1,101),np.linspace(-1,X[:,1].max()+1,101))
mX = np.stack(mX,2)
mz = lori.predict_proba(mX.reshape(-1,2))[:,1].reshape(101,101)
proba = lori.predict_proba(X)[:,1]
plt.figure(figsize=[7,6]).gca(aspect=1)
plt.scatter(X[:,0],X[:,1],c=z,s=30,edgecolor='k',cmap='winter')
plt.contour(mX[:,:,0],mX[:,:,1],mz,np.linspace(0,1,11),vmin=0,vmax=1,levels=np.linspace(0,1,11),linestyles='--',cmap='Reds')
plt.contourf(mX[:,:,0],mX[:,:,1],mz,100,vmin=0,vmax=1,cmap='winter',zorder=0)
plt.colorbar(pad=0.01,aspect=50)
plt.show()

proba = lori.predict_proba(X)[:,1]
t = proba>np.linspace(0,1,11)[:,None]
tpr = (z==t).T[z==1].mean(0)
fpr = (z!=t).T[z==0].mean(0)
plt.figure(figsize=[7,6]).gca(aspect=1,xlim=[-0.05,1],ylim=[0,1.05])
plt.plot(fpr,tpr,'m',zorder=0)
plt.scatter(fpr,tpr,c=np.linspace(0,1,11),edgecolor='k',cmap='Reds')
plt.colorbar(pad=0.01,aspect=50)
plt.show()

ก็จะได้แบบนี้




ในที่นี้สีของจุดในกราฟจะตรงกับสีของเส้นแบ่งในแผนภาพด้านบน

เส้นกราฟนี้ล่ะที่เรียกว่าเส้นกราฟ ROC

เส้นกราฟจะเริ่มจาก (0,0) และจบที่ (1,1) เสมอ เพราะถ้าให้ค่าจุดแบ่งเป็น 0 หมายความว่าผลที่ทายจะเป็นลบหมด ดังนั้นไม่ว่าบวกจริงหรือบวกปลอมก็จะมีอยู่ 0 ค่า tpr และ fpr จึง 0 ทั้งคู่

แต่ถ้าให้จุดแบ่งเป็น 1 ผลที่ทายจะเป็นบวกหมด แบบนั้นทั้งค่าลบจริงและลบปลอมก็จะมีอยู่ 0 แบบนั้น tpr=บวกจริง(บวกจริง+0)=1, fpr = ลบจริง(ลบจริง+0)=1

หากดูเส้นกราฟนี้แล้ว จะสามารถพิจารณาได้ว่าผลการทำนายของเรานั้นดีแค่ไหน

โดยทั่วไปจะถือว่าหากกราฟยิ่งชิดด้านบนมากก็ยิ่งแปลว่าทำนายได้ดี แต่หากกราฟเป็นเส้นตรงจะแสดงว่าไม่ดี

กราฟชิดด้านบนมากหมายความว่าพื้นที่ใต้กราฟมาก ค่าพื้นที่ใต้กราฟ ROC นี้ถูกเรียกว่า AUC (area under curve) เป็นค่าหนึ่งที่ใช้วัด

เส้นกราฟ ROC อาจเป็นดังภาพนี้


(ที่มาของภาพ http://blog.csdn.net/pipisorry/article/details/51788927)

หากกราฟชิดซ้ายบนแบบในภาพซ้ายค่าพื้นที่ใต้กราฟก็จะเป็น 1 นั่นคือสภาพในอุดมคติ แต่ในความเป็นจริงแค่โค้งค่อนไปบนซ้ายหน่อยก็ถือว่าดีแล้ว เช่นภาพกลาง ถือว่าทายถูกเยอะ

ส่วนถ้ากราฟออกมาเกือบตรงแบบภาพขวาพื้นที่ใต้กราฟก็จะเป็น 0.5 แบบนั้นคือทำนายอะไรแทบไม่ได้ คือมีผิดถูกพอๆกัน

แต่ถ้าหากกราฟออกมาค่อนไปทางขวาล่าง แบบนั้นค่าพื้นที่ใต้กราฟจะต่ำกว่า 0.5 กรณีแบบนี้ไม่ใช่ว่าผลการทำนายแย่ แต่หมายความว่าผลการทายมีแต่ผิดเต็มไปหมด

การที่ทายผลออกมาได้ผิดเยอะนั้นหากมองในมุมกลับก็ถือว่าต้องยอดเยี่ยม เพราะการจะทายอะไรให้ผิดได้ตลอดแสดงว่าต้องรู้คำตอบแต่จงใจตอบให้ผิด

ดังนั้นแค่พลิกผลการทายจากบวกเป็นลบ ก็จะกลายเป็นว่าทายถูกเยอะทันที

เคยมีกรณีที่นักสืบสอบสวนคดีโดยแกล้งถามคำถามที่เกี่ยวข้องกับคดีให้คนร้ายตอบ ปรากฏว่าคนร้ายจงใจตอบให้ผิดหมด นั่นเท่ากับคนร้ายติดกับดักเข้าเต็มเปา เพราะถ้าไม่รู้อะไรเลยก็จะไม่มีทางตอบให้ผิดหมดได้ ต้องมีถูกบ้างผิดบ้าง ดังนั้นจึงโดนจับได้ทันที

การหาพื้นที่ใต้กราฟก็คือการหาปริพันธ์ (integrate) นั่นเอง ซึ่งเราก็อาจจะเขียนโค้ดเพื่อคำนวณเองก็ได้ แต่ในที่นี้จะใช้ฟังก์ชันที่มีอยู่แล้วของ sklearn
from sklearn.metrics import auc
print(auc(fpr,tpr)) # ได้ 0.952204

ผลที่ได้เข้าใกล้ 1 แสดงว่าทำนายออกมาได้ดีมาก

หรือถ้าไม่ได้คำนวนค่า fpr และ tpr มาแล้วตั้งแต่ต้น sklearn ก็มีคำสั่งที่ใช้หาค่า AUC ได้ทันที
from sklearn.metrics import roc_auc_score
print(roc_auc_score(z,proba)) # 0.956916

ผลที่ได้จะออกมาต่างกันเล็กน้อยเนื่องจากตอนที่เราคำนวณค่า fpr และ tpr ตอนแรกได้แบ่งกราฟเป็นแค่ 10 ช่วง (11 จุด) ความละเอียดในการคำนวณค่อนข้างต่ำ แต่ในฟังก์ชันที่คำนวณให้เองนี้จะคำนวณละเอียดกว่า ค่าที่ได้จึงมีความแม่นยำมากกว่า

ถ้าแค่ต้องการ fpr กับ tpr เพื่อไปคำนวณ auc เองทีหลัง sklearn ก็มีคำสั่งนั้นให้เช่นกัน
from sklearn.metrics import roc_curve
fpr,tpr,threshold = roc_curve(z,proba)



ต่อมาทีนี้หากเราลองแกล้งเปลี่ยนค่าน้ำหนักและไบแอสของตัวทำนายของเราให้ผลการทำนายออกมาเปลี่ยนไปจะเป็นยังไง
เช่นลองปรับค่า
lori.coef_ = -lori.coef_
lori.intercept_ = -lori.intercept_

แก้ค่าเป็นแบบนี้แล้วก็รันโค้ดเดิมเพื่อวาดภาพแบบเดิมใหม่ก็จะได้ภาพแบบนี้




เส้นแบ่งจะเหมือนเดิมแต่ว่าสีเป็นตรงกันข้าม

ส่วนกราฟ ROC จะออกมาชิดขวาล่าง หมายความว่าผลการทายเลี่ยงคำตอบถูกได้อย่างยอดเยี่ยม

และค่าพื้นที่ใต้กราฟที่ได้ก็คือ 0.043084



หรือถ้าลองเปลี่ยนไปคนละทางกันเลยให้ผลการทำนายออกมาไม่แม่น เช่นลองปรับเป็นค่านี้
lori.coef_ = np.array([[-0.4,0.2]])
lori.intercept_ = np.array([3])




เส้นแบ่งจะวางไปคนละแนวกับที่ควรเลย ทำให้ไม่สามารถแบ่งแยกคำตอบได้ จึงออกมาถูกผิดครึ่งๆ

ส่วนกราฟ ROC ก็แทบจะเป็นเส้นตรง

แล้วพอคำนวณพื้นที่ใต้กราฟก็จะออกมาเป็น 0.598972 นั่นคือผลค่อนข้างแย่



โดยสรุปแล้ว ROC สามารถใช้ประเมินประสิทธิภาพของแบบจำลองการเรียนรู้ของเครื่องได้เช่นเดียวกับค่าความแม่นยำหรือคะแนน f1 เพียงแต่ว่า ROC ใช้วิเคราะห์ปัญหาที่มีแค่ ๒ คำตอบ (บวกหรือลบ) เท่านั้น



อ้างอิง


-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> ปัญญาประดิษฐ์
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> numpy
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> matplotlib
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> sklearn

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
python
-- numpy
-- matplotlib

-- pandas
-- pytorch
maya
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
เรียนภาษาจีน
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  ค้นหาบทความ

  บทความแนะนำ

หลักการเขียนทับศัพท์ภาษาจีนกลาง
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
บ้านเก่าของจางเสวียเหลียงในเทียนจิน
เที่ยวจิ่นโจว ๓ วัน ๒ คืน 23 - 25 พ.ค. 2015
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
บันทึกการเที่ยวสวีเดน 1-12 พ.ค. 2014
แนะนำองค์การวิจัยและพัฒนาการสำรวจอวกาศญี่ปุ่น (JAXA)
เที่ยวฮ่องกงในคืนคริสต์มาสอีฟ เดินทางไกลจากสนามบินมาทานติ่มซำอร่อยโต้รุ่ง
เล่าประสบการณ์ค่ายอบรมวิชาการทางดาราศาสตร์โดยโซวเคนได 10 - 16 พ.ย. 2013
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
บันทึกการเที่ยวญี่ปุ่นครั้งแรกในชีวิต - ทุกอย่างเริ่มต้นที่สนามบินนานาชาติคันไซ
หลักการเขียนคำทับศัพท์ภาษาญี่ปุ่น
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ
ทำไมถึงอยากมาเรียนต่อนอก
เหตุผลอะไรที่ต้องใช้ภาษาวิบัติ?

บทความแต่ละเดือน

2019年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2018年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2017年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2016年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2015年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文