ทำไมถึง predict แบบนี้กันนะ? (LIME)

machine learning deep learning model interpretation local interpretation

List of Variables

Learning Performance vs. Interpretability

ในความเป็นจริงแล้ว โมเดลที่เราแปลความหมายได้ (มี interpretability) กับ โมเดลที่มีความสามารถในการเรียนรู้ complex function มาก ๆ (มี learning performance) นั้นมักจะสวนทางกัน

alt text

ยกตัวอย่างเช่นใน linear regression คือเราพยายามจะโมเดลข้อมูลด้วยสมการเส้นตรง

\begin{equation} \label{eq:linear_reg} y= \beta_0 + \beta_1 x_1 + \beta_2 x_2 + … + \beta_n x_n \end{equation}

เพราะฉะนั้น ถ้าเราอยากรู้่ว่า effect ของฟีเจอร์ \(x_1\) ต่อผลทำนาย \(y\) เป็นเท่าไหร่ เราก็แค่ดูค่า \(\beta_1\) การที่เป็นแบบนี้คือเราสามารถแปลความหมาย หรือดูพฤติกรรมของโมเดลมันได้ ซึ่งเราทำได้เพราะว่าโมเดลมันเป็นสมการง่าย ๆ

แต่ด้วยความที่มันเป็นสมการง่าย ๆ มันก็ไม่สามารถใช้กับปัญหาที่ complex มากนัก ซึ่งเราก็เลยต้องขยับไปใช้พวกโมเดลที่ complex ขึ้นไป เช่น neural network แต่ว่าใน neural network นั้น เต็มไปด้วยตัวแปร weight และ bias จำนวนมาก การที่เราจะไปไล่แกะสมการของ neural network ออกมาดูมันก็เป็นไปไม่ได้

ทำไมการหาว่า Model คิดอย่างไรถึงสำคัญ ?

  1. อาจจะช่วยหา insight ของ business ได้ เช่นถ้าเราสร้างโมเดลทำนายราคาบ้านขึ้นมาด้วยการใช้ neural network ซึ่งสมมติว่าแม่นมาก แล้วเกิดเราอยากรู้ว่าทำไมบ้านแต่ละหลังถึงได้ราคานี้ล่ะ? ฟีเจอร์ใดเป็นส่วนเสริมราคาบ้านและฟีเจอร์ใดเป็นส่วนทำให้ราคาบ้านถูกลง?

  2. ช่วยตรวจสอบความน่าเชื่อถือให้กับโมเดลและ dataset

    alt text Image Ref: “Why Should I Trust You?”,Explaining the Predictions of Any Classifier

    ยกตัวอย่างดังรูปด้านบน เค้าทำ text classification แบ่งว่า text นั้นเกี่ยวข้องกับ “Christianity” หรือ “Atheism” ผลการเทรนข้อมูลจากสอง model หรือ algorithm นั้นพบว่าค่า accuracy ใน validation set ของ algorithm ที่ 2 นั้นมากกว่า algorithm ที่ 1

    แต่ว่าเมื่อเค้าหยิบ text มาอันนึง (instance นึง) มาหาว่าทำไม text นี้ถึงถูกทำนายว่าเกี่ยวกับ “Atheism” ด้วยการใช้ LIME ปรากฎว่า

    • algorithm แรกให้ความสนใจไปที่คำว่า {God, mean, anyone, this, koresh, through}
    • algorithm ที่สองให้ความสนใจกับคำว่า {Posting, Host, Re, by, in , Nntp}

    จะเห็นได้ว่า algorithm แรกนั้นน่าเชื่อถือมากกว่า เนื่องจากคำที่มันสนใจนั้น มีความ make sense กว่า แต่ที่จริงแล้วอีกอย่างนึงที่พบก็คือ dataset นี้มีปัญหา ซึ่งเค้าพบว่าคำว่า Posting นั้นโผล่อยู่ในทั้ง training และ validation set โดยที่ 99% ของ text ที่มีคำว่า Posting นั้น เป็น class “Altheism” โมเดลที่ 2 มันเลย overfit กับคำนี้ไปเลย

LIME คืออะไร?

LIME ย่อมาจาก Local Interpretable Model-agnostic Explanations ซึ่งหมายความว่า

ซึ่ง LIME นี้ คนออกแบบเค้าออกแบบมาให้ใช้ได้กับทั้งข้อมูลแบบ table, text, แล้วก็ image เลยทีเดียวเชียว

Concept ของ LIME

แนวคิดของ LIME คือแนวคิดของ surrogate model ซึ่งเป็นการใช้ model อื่นที่เบสิคและสามารถแปลความได้ (เช่น linear regression) มาช่วยอธิบายผลการทำนายในบริเวณของข้อมูลที่เราสนใจ แทนที่จะไปนั่งหาว่า model เราทั้งหมดคิดยังไง

model ที่เบสิคที่ใช้ในการแปลความเราเรียกว่า explainer

ยกตัวอย่างดังรูปด้านล่าง

alt text Image Ref: “Why Should I Trust You?”,Explaining the Predictions of Any Classifier

ดังรูป สมมติเราใช้ neural network ในการแยกคลาส + กับคลาส o ออกจากกันด้วยฟีเจอร์ \(x_1\) และ \(x_2\) ต่อมาเราเกิดอยากรู้ว่าที่จุด + (ตัวหนา) นั้น ทำไมถึงถูกทำนายออกมาเป็น class + ได้ (อยากรู้ว่า \(x_1\) และ \(x_2\) ส่งผลอย่างไร ต่อ probability ในการเป็น class +)

คือถ้าเราอยากรู้แค่การอธิบายบนจุดนั้น เราก็ไม่จำเป็นต้องไปนั่งไล่แกะ complex function ใน neural network ให้เวียนหัวกัน (ซึ่งจริง ๆ มันก็ไม่น่ามีใครทำนะ) แต่ว่าเราสามารถพุ่งเป้าในการวิเคราะห์ไปที่จุดนั้น และข้อมูลที่ใกล้เคียงจุดนั้น ๆ และสร้าง explainer มา classify ข้อมูลในละแวกนั้นก็พอ ซึ่งจะเห็นจากรูปด้านบนว่า explainer เราสร้าง boundary decsion ใหม่ที่เป็นเส้นตรงธรรมดาขึ้นมาเส้นนึง ในขณะที่จริง ๆ แล้ว boundary decision ของโมเดลเรามันยึกยือกว่านั้น (เส้นสีเขียว) ซึ่งเราไม่ได้แคร์ว่าเส้นยึกยือทั้งหมดนั้นจะเป็นยังไง เราแคร์แค่ ณ จุดที่เราสนใจ มันเป็นยังไง

เพราะฉะนั้นแล้ว เวลาเราดูผลจาก LIME เราต้องพึงระลึกเสมอว่า พวก contribution ของฟีเจอร์ที่เราเห็นนั้น มันเป็นแค่ local หรือของข้อมูลในแถบนั้น ไม่ใช่ของทั้ง dataset

โดยที่ในการเทรน Explainer นั้น (อย่าลืมว่า explainer ก็เป็น machine learning model เหมือนกัน และเราก็ต้องเทรนเหมือนกัน) เราจะ weight ข้อมูลแต่ละจุดด้วยค่าความเหมือนกัน (proximity) ของ sample นั้น ๆ กับข้อมูลจุดที่เราสนใจ อย่างเช่นรูปด้านบนนั้น ขนาดของ + และ o บ่งบอกถึง weight ของข้อมูลนั้น ๆ ในการเทรน explainer จะเห็นได้ว่าจุดที่อยู่ใกล้จะมีขนาดใหญ่ กว่าจุดที่อยู่ไกล ซึ่งหมายความว่ายิ่ง sample มีความใกล้เคียงกับข้อมูลที่เราสนใจมากเท่าไหร่ เรายิ่งให้ความสำคัญมันมากเท่านั้น

*** ส่วนด้านล่างนี้ ถ้าอยากข้าม ก็ข้ามไปเลยก็ได้ อ่านอีกที section ขั้นตอนของ LIME เลย

ซึ่งถ้าเราเขียน objective ของ LIME ให้อยู่ในรูปของสมการจะได้ว่า LIME นั้นต้องการจะ minimize สมการด้านล่าง

\begin{equation} \label{eq:objective} \xi (x) = argmin_{g \in G} \mathcal{L}(f,g,\pi_x) + \Omega(g) \end{equation}

โดยที่

จากสมการที่ \eqref{eq:objective} นั้น เราสนใจเทอม \(\mathcal{L}(f,g,\pi_x)\) เป็นหลัก ซึ่งมันคือค่าที่ช่วยเราวัดว่าตัว explainer นั้น สามารถให้ผลได้เหมือนกับโมเดลจริง ๆ ที่เราใช้ได้ขนาดไหน

ยิ่ง \(\mathcal{L}(f,g,\pi_x)\) น้อยแสดงว่า ตัว explainer เราสามารถให้ผลการทำนายได้ใกล้เคียงกับโมเดลจริง ๆ ที่เราใช้ ซึ่งก็สามารถ imply ต่อไปได้ว่าการที่เราพยายามหา contribution ของแต่ละฟีเจอร์ต่อผลการทำนายจาก explainer นั้นจะให้ค่า contribution ใกล้เคียงกับที่เราหาได้จากของโมเดลจริง ๆ อย่างไรก็ตาม เราไม่รู้นะว่าค่า contribution ที่คำนวณจากโมเดลจริง ๆ เป็นเท่าไหร่ เพราะมัน complex เกินไป

ซึ่งเราสามารถเขียนสมการของเทอมนี้ออกมาได้ดังนี้

\begin{equation} \label{eq:loss} \mathcal{L}(f,g,\pi_x) = \sum_{z,z’ \in \mathcal{Z}} \pi_x(z) (f(z)-g(z’))^2 \end{equation}

โดยที่

โดยที่วิธี minimize เทอม \(\mathcal{L}(f,g,\pi_x)\) นี้ ก็คือการที่เราจะ train ตัว explainer model \(g\) ให้ทำนายผลการทำนายของโมเดลหลักเรา หรือก็คือ $f(z)$ ด้วยข้อมูล $z$ โดยตอนเทรนนั้น เราจะตั้ง sample weight ของ $z$ แต่ละตัวด้วยค่า $\pi_x(z)$ นั่นเอง

นอกจากนี้ จากสมการที่ \eqref{eq:objective} ถ้าเราคิดดี ๆ จะพบว่าเทอม \(\mathcal{L}(f,g,\pi_x)\) กับ \(\Omega(g)\) มักจะกลับกัน หรือก็คือถ้า \(\mathcal{L}(f,g,\pi_x)\) มีค่ามาก อาจจะเป็นเพราะว่า ตัว explainer \(g\) นั้น มี learning performance ต่ำ ไม่สามารถบิดไปตาม complex function ของตัวโมเดลหลัก \(f\) ได้ แต่ถ้าเราเพิ่ม learning performance มันก็คือการเพิ่ม complexity หรือเทอม \(\Omega(g)\) ไปด้วย

ขั้นตอนของ LIME โดยคร่าว (Toy Problem)

สำหรับโค้ดทั้งหมด สามารถดูได้ที่ link นี้ ครับ

สิ่งที่ต้องมีก่อนเริ่มทำ

ขั้นตอน

  1. สร้าง fake dataset (บางที่ก็เรียกว่า perturbed dataset) ต่อจากนี้เราจะเรียก dataset นี้ว่า $Z$ และเรียกสมาชิกของ $Z$ ด้วยตัวแปร $z$
     number_of_fake_datapoints = 500
     Z = np.random.normal(loc=[0,0], scale=0.5 ,size = (number_of_fake_datapoints,2))
    
  2. หา euclidean distance ระหว่าง fake dataset แต่ละจุด ($z$ แต่ละตัว) กับจุดที่เราสนใจ ($x$) และนำมาคำนวณ weight ($\pi$) ด้วยสมการที่ \eqref{eq:pi}
     ## Calculate Distance
     dist = np.linalg.norm(Z-np.array([x]), axis=1)#np.sum((Z - np.array(x))**2,axis=1) #np.linalg.norm(Z-np.array([x]), axis=1)
     ## Calculate Weight
     kernel_width = 0.1
     pi = np.sqrt(np.exp(-((dist ** 2) / kernel_width ** 2)))
    
  3. นำ dataset $Z$ ไป classify ด้วย model หลักของเรา ที่เราเทรนไว้แล้ว แล้วดึง probability ของ class ที่ 1 ออกมา (ขั้นตอนนี้คือการหา $f(z)$)
     f_z = f.predict_proba(Z)[:,1]
    
  4. สร้าง explainer ด้วยการเทรน Linear regression ด้วยข้อมูล dataset $Z$ และ label $f(z)$ ที่หามาในข้อก่อนหน้า แล้วอย่าลืมตั้ง sample weight เป็น $\pi$ ด้วย

     from sklearn.linear_model import LinearRegression
     lr = LinearRegression(intercept=False)
     explainer = lr.fit(X=Z, y=f_z, sample_weight=pi.ravel())
    
    • แล้วก็ print ตัว coefficient ของ $x_1$ และ $x_2$ ออกมาดู
        print ('Coefficient: ', explainer.coef_)
        # Result
        Coefficient:  [-1.81405856 -0.76955154]
      

    ก็คือเราได้สมการของ linear regression ดังนี้

    \begin{equation} \label{eq:reg_toy} probabilty \; of \; class \; 1 = x_1(-1.814) + x_2(-0.7695) \end{equation}

    แสดงว่า

    • contribution ของฟีเจอร์ $x_1$ ต่อ probability ของ class 1 ณ จุด $(-0.25,0.25)$ เท่ากับ $-0.25 \times -1.814 = +0.4535$
    • contribution ของฟีเจอร์ $x_2$ ต่อ probability ของ class 2 ณ จุด $(-0.25,0.25)$ เท่ากับ $0.25 \times -0.7695 = -0.192$

    เมื่่อรวม contribution ของทั้ง 2 ฟีเจอร์ เราจะได้ probability ในการเป็น class 1 ของจุด $x$ ที่ประมาณโดย explainer ได้เท่ากับ $0.4535 - 0.192 = 0.2615$ ซึ่งก็ใกล้เคียงกับที่เราหาไว้ตอนแรก

    โดยที่เราสามารถพล็อต decision boundary ของ explainer ได้ดังรูปด้านล่าง

    วิธีพล็อต decision boundary ก็แค่แก้สมการ $y = x_1(-1.814) + x_2(-0.7695)$ โดยการหาคู่อันดับ $(x_1,x_2)$ ทีทำให้ค่า $y=0.5$

ขั้นตอนของ LIME สำหรับ Image Classification

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

สิ่งที่ต้องมีก่อนเริ่มทำ

จะเห็นได้ว่าสิ่งที่เราต้องมีก็เหมือนกับใน Toy problem ด้านบน แต่ว่าเมื่อเราใช้ LIME ใน image classification นั้น สิ่งที่เราต้องมีเพิ่มเติมขึ้นมาคือ

ขั้นตอน

  1. แปลงจุดข้อมูลที่เราสนใจ \(x\) ให้เป็น \(x'\)
    • ซึ่งในปัญหาของ image นั้น เราจะทำการ segment รูปตามสี (แบ่ง super-pixel)
       from skimage.segmentation import slic,quickshift
       from skimage.segmentation import mark_boundaries
       import matplotlib.pyplot as plt
       segments = quickshift(x, kernel_size=4, max_dist=200, ratio=0.2)
       skimage.io.imshow(mark_boundaries(x, segments)) 
       number_of_superpixel = np.unique(segments).shape[0]
       print ('There are {} super-pixels in image'.format(number_of_superpixel)) # 60 super pixels
      
    • แล้วเราก็จะทำการสร้าง array ที่เป็นเลข 1 ทั้งหมด และมีความยาวเท่ากับจำนวน superpixels ในรูป ซึ่งแต่ละตำแหน่งของสมาชิกใน array จะอิงไปแต่ละ super pixel และตัวเลขก็คือแทนการเปิด-ปิดส่วนนั้น ๆ ของภาพ (1 เปิด, 0 ปิด) จะเห็นได้ว่าในรูป \(x\) ของเรานั้น คือรูปเต็ม ๆ ที่สมบูรณ์ ตัว \(x'\) เราเลยเป็น 1 ทั้งหมด
       x_dat = np.ones(number_of_superpixel)
      
  2. สร้าง fake dataset หรือ purturbed dataset โดยการ permute ค่าของแต่ละฟีเจอร์ใน \(x'\) ในที่นี้จะขอแทน fake dataset ว่า \(Z'\) และแทน datapoint แต่ละจุดของ \(Z'\) ด้วย \(z'\)
    • ซึ่งเราจะทำการสุ่มสร้าง array ที่ประกอบไปด้วย 0 และ 1 มีความยาวเท่ากับจำนวน superpixels ขึ้นมาหลาย ๆ array
        number_of_fake_datapoints = 200
        Z_dat = np.random.randint(0,2,size=(number_of_fake_datapoints,number_of_superpixel))
        # Z_dat shape is (200,60)
      
  3. เปลี่ยน fake dataset ซึ่งตอนนี้อยู่ใน representation ของ input ของ explainer ให้อยู่ใน representation ของ input ของโมเดลหลัก หรือก็คือเปลี่ยน \(Z'\) เป็น \(Z\)

     Z = []
     for i in range(number_of_fake_datapoints):
       z = np.nonzero(Z_dat[i])
       mask = np.isin(segments,z)
       z = x*mask[...,None]
       Z.append(z)
     Z = np.array(Z)
     Z.shape
    

    ในภาพด้านล่างจะเห็นว่าใน dataset $Z$ นั้นจะเป็นภาพที่ขาด ๆ หาย ๆ ไปบางส่วน โดยที่แต่ละภาพก็จะขาดไปไม่เท่ากัน

  4. นำ dataset \(Z\) ไปเข้าโมเดล \(f\) และหา probability ของ acoustic guitar ของแต่ละ fake datapoint ออกมา หรือก็คือจะได้ \(f(z)\)

     # ดึงคอลัมน์ที่ 402 ซึ่งก็คือ index ของ probability ของ 'acoustic guitar'
     f_z = model.predict(Z)[:,402]
    
  5. หา distance ระหว่าง fake datapoint แต่ละจุดหรือ \(z'\) กับข้อมูลจริงหรือ \(x'\) ด้วยฟังก์ชัน \(D\) และนำไปคำนวณ weight ของ fake datapoint นั้น ๆ ด้วยสมการที่ \eqref{eq:pi} (แต่ที่จริงสมการนี้เราจะ design เองใหม่ก็ได้)
    • หา distance ระหว่าง fake datapoint \(z'\) กับ \(x'\)
        from sklearn.metrics import pairwise_distances
        distance = pairwise_distances(x_dat.reshape(1,-1),Z_dat)
      
    • คำนวณ weight หรือ \(\pi\) ของแต่ละ \(z\)
        pi = np.sqrt(np.exp(-(distance ** 2) / kernel_width ** 2))
      
  6. นำ dataset \(Z'\) ไปเทรนโมเดล explainer หรือ \(g\) โดยที่มี label เป็น \(f(z)\) ที่เพิ่งได้มาในข้อที่ 4 และกำหนด sample weight ตามค่า \(\pi\) ด้วย
     from sklearn.linear_model import LinearRegression
     lr = LinearRegression()
     lr_model = lr.fit(X=Z_dat, y=f_z, sample_weight=pi.ravel())
    
  7. ดู coefficient ของแต่ละฟีเจอร์ในโมเดล \(g\) เราก็จะรู้ว่า แต่ละฟีเจอร์ใน \(z'\) (ในที่นี้คือแต่ละ super pixel) ส่งผลยังไงกับผลการทำนายบ้าง
    • อย่างในกรณีนี้จะทำการเลือก superpixel ที่มี coeffficient ที่มากที่สุดมา 10 อันดับ มาพล็อตดูว่า โมเดลเราดู segment ไหน ถึงบอกว่านี่คือรูปของ acoustic guitar
        ## เลือก index ของ 5 coefficient มีที่ค่าสูงที่สุด
        importance_part = lr_model.coef_.argsort()[-5:][::-1]
        ## สร้างรูปแสดงส่วนที่ model เราดู
        mask = np.isin(segments,importance_part)
        importance_image = x*mask[...,None]
        skimage.io.imshow((mark_boundaries(importance_image, segments)))
      
  8. ที่นี้ถ้าเราย้อนไปดูจะพบว่า โมเดลเราบอกว่ารูปนี้เป็นรูปของ golden retriever ด้วย เราก็ลองทำของ golden retriever บ้าง ด้วยการเปลี่ยน index ของ column ที่เราสนใจที่ข้อ 4 แล้วก็รันลงมาใหม่

     #### ข้อ 4
     f_z = model.predict(Z)
     # ดึงคอลัมน์ที่ 207 ซึ่งก็คือ index ของ probability ของ 'golden retriever'
     f_z = f_z[:,207] 
     f_z.shape
    
     #### ข้อ 5
     from sklearn.metrics import pairwise_distances
     kernel_width = 0.25
     distance = pairwise_distances(x_dat.reshape(1,-1),Z_dat)
     pi = np.sqrt(np.exp(-(distance ** 2) / kernel_width ** 2))
     pi.shape
    
     #### ข้อ 6
     from sklearn.linear_model import LinearRegression
     lr = LinearRegression()
     lr_model = lr.fit(X=Z_dat, y=f_z, sample_weight=pi.ravel())
    
     #### ข้อ 7
     importance_part = lr_model.coef_.argsort()[-5:][::-1]
     mask = np.isin(segments,importance_part)
     importance_image = x*mask[...,None]
     skimage.io.imshow((mark_boundaries(importance_image, segments)))
    

ในการทำ LIME กับ image classification นั้น ถ้าสรุปเพื่อให้เข้าใจง่ายแล้วก็คือเราลองปิดภาพบางส่วนไป แล้วเอาไปให้โมเดลเราทำนาย แล้วดู probability ของการเป็น guitar

จากขั้นตอนทั้งหมดข้างบนนั้น จริง ๆ แล้วคนเขียนเปเปอร์ LIME นี้เค้า implement มาเป็น library ให้หมดแล้ว ซึ่งเราจะเหลือโค้ดแค่ไม่กี่บรรทัดเท่านั้น

  1. อ่านรูปกับ model มาก่อน

     ## อ่านรูป
     import skimage.io 
     x = skimage.io.imread("dog-guitar.jpg")
     x = skimage.transform.resize(x, (299,299)) 
     ## อ่านโมเดล
     import keras
     model = keras.applications.inception_v3.InceptionV3()
    
  2. เอารูปกับ model ไปสร้าง explainer ได้เลย
     from lime import lime_image
     explainer = lime_image.LimeImageExplainer()
     explanation = explainer.explain_instance(x, model.predict, top_labels=5, hide_color=0, num_samples=1000)
    
  3. พล็อตออกมาดู
    • segment ที่สำคัญของ acoustic guitar
       from skimage.segmentation import mark_boundaries
       import matplotlib.pyplot as plt
       # 402 เป็น index ของ acoustic guitar
       temp, mask = explanation.get_image_and_mask(402, positive_only=True, num_features=5, hide_rest=True)
       plt.imshow(mark_boundaries(temp, mask))
      
    • segment ที่สำคัญของ golden retriever
       # 207 เป็น index ของ golden retriever
       temp, mask = explanation.get_image_and_mask(207, positive_only=True, num_features=5, hide_rest=True)
       plt.imshow(mark_boundaries(temp, mask))
      

หวังว่าในบทความนี้พอจะทำให้เห็นภาพว่าจริง ๆ แล้ว concept และขั้นตอนของ LIME มันเป็นอย่างไร จะได้เอาไปใช้ประโยชน์ได้ถูก สำหรับผู้อ่านที่คิดจะใช้ LIME จริง ๆ ก็อย่าเมื่อยตุ้มเขียนโค้ดเองแบบที่เราทำกันในบทความนี้ ทางผู้เขียนเปเปอร์เค้าทำ lib ไว้ให้เราใช้อย่างง่ายดายพร้อมกับตัวอย่างการใช้งาน LIME กับข้อมูลหลากหลายประเภท เช่น tabular data, text, image ซึ่งสามารถตาม link นี้ ไปได้เลย ซึ่งเค้าทำออกมาดูดีมากเลยแหละ ไปดูเถอะ

ในบทความต่อไปน่าจะพูดถึง DeepLIFT และ SHAP ที่เป็นวิธีในการทำ local interpretation เหมือนกัน

Discliamer

รายละเอียดในบทความนี้มาจากความเข้าใจส่วนตัว อาจมีข้อผิดพลาด หากพบจุดผิดพลาด ขอความกรุณาแจ้งทาง facebook หรือ email: thammasorn.han@hotmail.com

Reference: