توضیحپذیری: چرا مدل این را گفت
جواب کوتاه
توضیحپذیری یعنی بتونیم بفهمیم یه مدل چرا یه جواب رو داده. مهمه، چون مدلی که جواب درست میده، شاید به دلیل غلط درست بگه؛ مثل مدلی که عکس اسب رو درست تشخیص میداد ولی در واقع به یه برچسب کوچیک گوشهی عکس نگاه میکرد. مدلهای ساده، مثل خط و درخت تصمیم، خودشون رو توضیح میدن؛ ولی شبکههای بزرگ جعبهی سیاهان. برای اونها چند راه هست: بههم زدن یه ستون و دیدن اینکه خطا چقدر بالا میره، تقسیم یه پیشبینی به سهم هر ویژگی، پررنگ کردن بخشهایی از عکس که بیشترین اثر رو داشتن، و گفتن اینکه چی باید عوض میشد تا جواب عوض بشه. ولی یادتون باشه: توضیح میگه مدل به چی نگاه کرده، نه اینکه توی دنیای واقعی چی علت چیه.
چرا «چرا»
سال ۲۰۱۷ یه مدل تشخیص تصویر، عکسهای اسب رو خیلی خوب پیدا میکرد. بعد یکی پرسید: «دقیقا به چی نگاه میکنه؟»
جواب غافلگیرکننده بود: مدل اصلا به اسب نگاه نمیکرد. توی دادهی آموزش، خیلی از عکسهای اسب یه برچسب حق نشر کوچیک گوشهشون داشتن. مدل یاد گرفته بود «هر جا این برچسب هست، یعنی اسب»، و روی عکسهایی که داشت، خوب هم جواب میداد. ولی اگه یه عکس اسب بی برچسب بهش میدادید، یا یه عکس ماشین با همون برچسب، کاملا اشتباه میکرد.
این درس اصلی توضیحپذیریه: درست گفتن کافی نیست؛ باید به دلیل درست، درست بگه. و تا نپرسید «چرا»، نمیفهمید. عدد دقت هیچوقت این رو نشون نمیداد.
دلیلهای دیگه هم هست. کسی که وامش رد شده، حق داره بدونه چرا. کسی که مدل رو میسازه، باید بتونه سوگیری رو پیدا کنه. و بعضی قانونها هم این رو میخوان؛ مثلا اتحادیهی اروپا توی یه قانون حفاظت از داده، که از ۲۰۱۸ اجرا شد، برای تصمیمهای خودکار یه حق توضیح گذاشته.
جعبهی سفید و سیاه
همهی مدلها یه اندازه سخت توضیح داده نمیشن. دو دستهان:
جعبهی سفید مدلیه که خودش، همینجوری، قابل فهمه. مثلا یه خط: فرض کنید مدل نمره این باشه، «۱٫۵ ضرب در ساعت درس، به اضافهی ۰٫۸ ضرب در ساعت خواب، به اضافهی ۲». خودش میگه هر ساعت درس ۱٫۵ نمره میارزه و هر ساعت خواب ۰٫۸. یا یه درخت تصمیم کوچیک: میشه مسیر هر جواب رو، سوال به سوال، دنبال کرد.
جعبهی سیاه مدلیه که حتی سازندهش هم نمیتونه بگه چرا یه جواب رو داد. یه شبکهی عصبی با میلیونها وزن، یا یه جنگل با صدها درخت. تکتک عددها رو میبینید، ولی هیچکدوم بهتنهایی معنیای نداره.
اولین سوال همیشه اینه: لازمه جعبهی سیاه باشه؟ اگه یه مدل ساده تقریبا همونقدر خوبه، و تصمیم روی زندگی آدمها اثر داره، شاید بهتر باشه همون جعبهی سفید رو انتخاب کنید. اگه نه، راههای بعدی به کمکتون میان.
کدوم ستون مهمه
اولین سوال دربارهی هر مدلی: کلا به کدوم ستونها بیشتر تکیه میکنه؟ یه راه خیلی ساده و زیرکانه برای جوابش هست:
مدل آموزشدیده رو دست نمیزنیم. فقط توی دادهی آزمون، مقدارهای یه ستون رو، مثلا ساعت درس، شانسی بین ردیفها بههم میزنیم؛ انگار ساعت درس هر بچه رو به یه بچهی دیگه بدیم. بعد دوباره خطای مدل رو حساب میکنیم.
اگه خطا خیلی بالا رفت، یعنی مدل به اون ستون خیلی تکیه میکرد؛ خرابش کردیم و مدل گیج شد. اگه خطا تقریبا عوض نشد، یعنی مدل اصلا بهش اهمیت نمیداد. این کار رو برای هر ستون جدا انجام میدیم و ستونها رو به ترتیب اهمیتشون میچینیم. به این میگن اهمیت ویژگی با بههم زدن.
قشنگیش اینه که برای هر مدلی کار میکنه، حتی جعبهی سیاه. و جوابهای عجیبش خیلی چیز یاد میده: اگه دیدید برای مدل نمره، «رنگ جلد دفتر» مهمترین ستونه، یه جای کار میلنگه؛ مثل همون برچسب حق نشر.
بههم زدن، با کد
این رو خودمون امتحان کردیم. یه دادهی ساختگی ساختیم که نمرهش از ساعت درس و ساعت خواب درمیاد، و یه ستون سوم هم گذاشتیم که هیچ ربطی نداره: رنگ جلد دفتر، یه عدد شانسی. یه خط ساده آموزش دادیم و بعد هر ستون رو جدا بههم زدیم:
base = error(test)
print("error:", round(base, 2))
for i, name in enumerate(["hours", "sleep", "color"]):
col = [r[i] for r in test]
random.shuffle(col)
mixed = [r[:i] + [c] + r[i + 1:] for r, c in zip(test, col)]
print(name, "shuffled:", round(error(mixed), 2))error: 0.8 hours shuffled: 4.09 sleep shuffled: 1.65 color shuffled: 0.8
بی هیچ بههم زدنی، مدل بهطور میانگین ۰٫۸ نمره اشتباه میکرد. ساعت درس رو بههم زدیم، اشتباه پرید به ۴٫۰۹؛ پس مدل خیلی بهش تکیه داشت. ساعت خواب، ۱٫۶۵؛ تکیه داشت، ولی کمتر. رنگ جلد، همون ۰٫۸؛ مدل اصلا بهش اهمیت نمیداد. این همون ترتیبیه که داده رو باهاش ساخته بودیم: هر ساعت درس ۱٫۵ نمره، هر ساعت خواب ۰٫۸، و رنگ هیچی. یعنی روش، چیزی که واقعا توی داده بود رو درست پیدا کرد.
برچسب اسب، دوباره
حالا ببینیم همین روش، مشکل اسب و برچسب رو پیدا میکنه یا نه. عکسهای ساختگی ساختیم با دو تا نشونه. یکی «شکل»، که واقعا به اسب ربط داره ولی نویز زیادی داره. یکی «علامت گوشه»، که توی عکسهای آموزش ۹۵ درصد وقتها دقیقا با اسب بودن جوره، ولی توی عکسهای تازه هیچ ربطی نداره:
مدل روی عکسهایی شبیه آموزش، ۹۵ درصد درست میگفت؛ عالی به نظر میاد. روی عکسهای تازه، ۵۳ درصد؛ تقریبا شیر یا خط. وزنی که مدل به علامت داده بود ۵٫۵۸ بود، و به شکل فقط ۰٫۹۵. اگه بههم زدن رو روی همون عکسهای آموزشمانند انجام بدیم، بههم زدن شکل درستی رو از ۹۵ به ۹۰ میرسونه، و بههم زدن علامت به ۵۵. یعنی همین آزمون ساده، قبل از اینکه حتی یه عکس تازه ببینیم، داد میزنه «مدل تقریبا فقط به علامت نگاه میکنه».
وقتی علامت رو از داده برداشتیم و مدل رو فقط با شکل آموزش دادیم، روی عکسهای تازه ۷۵ درصد درست گفت. عدد کمتری روی آموزش، ولی خیلی بیشتر توی دنیای واقعی. این همون حرف اول درسه: درست گفتن کافی نیست؛ باید به دلیل درست، درست بگه.
شپلی، با کد
برای خط، سهمها سادهان. ولی وقتی دو ویژگی با هم کار میکنن، مثلا اثر ساعت درس به اینکه چقدر خوابیدی بستگی داره، دیگه نمیشه هر کدوم رو جدا حساب کرد؛ سهم هر کدوم به اینکه کدوم اول اضافه بشه بستگی پیدا میکنه. شپلی میگه: همهی ترتیبها رو امتحان کن و میانگین بگیر. با یه مدل ساختگی که ساعت و خواب توش ضرب در هم میشن:
from itertools import permutations
def model(hours, sleep):
return 2 + 0.4 * hours * sleep # made up: hours and sleep work together
usual = {"hours": 4, "sleep": 7}
child = {"hours": 6, "sleep": 5}
def value(known): # what the model says if only these are the child's own
x = {k: (child[k] if k in known else usual[k]) for k in usual}
return model(**x)
share = {k: 0.0 for k in usual}
orders = list(permutations(usual))
for order in orders:
known = set()
for k in order:
before = value(known)
known.add(k)
share[k] += (value(known) - before) / len(orders)
print("usual child:", round(value(set()), 2), " this child:", round(value(set(usual)), 2))
print({k: round(v, 2) for k, v in share.items()})usual child: 13.2 this child: 14.0
{'hours': 4.8, 'sleep': -4.0}بچهی معمولی، با ۴ ساعت درس و ۷ ساعت خواب، ۱۳٫۲ میگیره؛ این بچه، با ۶ و ۵، ۱۴ میگیره. اگه اول ساعت درسش رو بذاریم و خواب معمولی بمونه، نمره ۵٫۶ بالا میره؛ اگه اول خوابش رو بذاریم، ساعت درس فقط ۴ بالا میبره. شپلی میانگین میگیره: ۴٫۸. برای خواب هم همینطور: منفی ۴. و جمعشون، ۰٫۸، دقیقا همون فاصلهی ۱۳٫۲ تا ۱۴ هست. این ویژگی همیشه برقراره: سهمها دقیقا جمع میشن و به جواب میرسن.
با دو ویژگی، فقط دو تا ترتیب داریم. با ۱۰ ویژگی، ۳ میلیون و ۶۲۸ هزار و ۸۰۰ ترتیب؛ و با ۳۰ ویژگی، عددی که از تعداد ثانیههای عمر جهان خیلی بیشتره. برای همین اسکات لاندبرگ و سو این لی سال ۲۰۱۷ راههایی دادن که شپلی رو برای مدلهای واقعی تقریبی و سریع حساب کنه.
یه خط کوچیک دور یه نفر
یه راه دیگه برای توضیح یه نفر، که مارکو ریبیرو و همکارهاش سال ۲۰۱۶ دادن، اینه: دور همون نفر، کلی نقطهی خیلی نزدیک بساز، جواب مدل رو برای همهشون بپرس، و یه خط ساده از وسطشون رد کن. مدل کل دنیا شاید پیچیده باشه، ولی توی یه همسایگی کوچیک، تقریبا صافه؛ مثل زمین، که گرده ولی حیاط خونهتون صاف به نظر میاد. با همون مدل ساختگی که ساعت و خواب توش ضرب میشن:
import random, statistics
def model(hours, sleep):
return 2 + 0.4 * hours * sleep
random.seed(1)
near = [(6 + random.uniform(-0.5, 0.5), 5 + random.uniform(-0.5, 0.5)) for _ in range(200)]
said = [model(h, s) for h, s in near]
slope_h, _ = statistics.linear_regression([h for h, s in near], said)
slope_s, _ = statistics.linear_regression([s for h, s in near], said)
print("near this child: each hour of study", round(slope_h, 2), " each hour of sleep", round(slope_s, 2))
far = [(4 + random.uniform(-0.5, 0.5), 8 + random.uniform(-0.5, 0.5)) for _ in range(200)]
said = [model(h, s) for h, s in far]
print("near another child:", round(statistics.linear_regression([h for h, s in far], said)[0], 2),
round(statistics.linear_regression([s for h, s in far], said)[0], 2))near this child: each hour of study 2.07 each hour of sleep 2.48 near another child: 3.2 1.58
نزدیک بچهای که ۶ ساعت درس خونده و ۵ ساعت خوابیده، خط کوچیک میگه هر ساعت درس بیشتر حدود ۲ نمره و هر ساعت خواب بیشتر حدود ۲٫۵ نمره میارزه؛ حساب دقیقش ۲ و ۲٫۴ هست، پس تقریب خوبیه. ولی نزدیک یه بچهی دیگه، با ۴ ساعت درس و ۸ ساعت خواب، همون مدل میگه هر ساعت درس ۳٫۲ و هر ساعت خواب ۱٫۶. یعنی جواب «کدوم مهمتره؟» برای هر نفر فرق میکنه؛ برای بچهی اول خواب، برای دومی درس.
این هم قدرت توضیح محلیه هم حدش. قدرتش اینه که برای هر مدلی، هر چقدر پیچیده، کار میکنه و یه جواب ساده و قابل فهم میده. حدش اینه که فقط دربارهی همون همسایگیه؛ اگه کسی از توضیح یه نفر نتیجه بگیره که «مدل کلا به خواب اهمیت بیشتری میده»، اشتباه کرده. و اندازهی همسایگی هم مهمه؛ اگه نقطهها رو خیلی دور بسازید، خط کوچیک دیگه شبیه مدل نیست.
توی عکس
برای عکس، «ستونها» پیکسلها هستن، و هزاران تا. پس به جای جدول، یه عکس دیگه میکشیم:
به این میگن نقشهی برجستگی: یه عکس هماندازهی عکس اصلی، که بخشهایی که بیشترین اثر رو روی جواب مدل داشتن، پررنگ شدن. اگه مدل گفته «گربه» و جای پررنگ روی صورت گربهست، خیالتون راحتتر. اگه جای پررنگ گوشهی عکسه، یا روی پسزمینه، یه چیزی درست نیست.
مشکلی مثل اسب و برچسب، با همین نقشهها خیلی زود دیده میشه: کافیه ببینید جای پررنگ روی خود اسبه، یا روی گوشهی پایین عکس.
نقشه، با پوشوندن
سادهترین راه ساختن همچین نقشهای، همون ایدهی بههم زدنه، برای عکس: هر بار یه تیکه از عکس رو بپوشونید و ببینید جواب مدل چقدر افت میکنه. یه عکس ۶ در ۶ ساختگی و یه مدل ساختگی که بیشتر به گوشهی پایین تکیه داره، مثل همون برچسب حق نشر، و کمی هم به وسط:
image = [[1] * 6 for _ in range(6)] # a plain 6 by 6 picture, every pixel lit
def model(img): # made up: leans on the bottom corner, a little on the middle
middle = img[2][2] + img[2][3] + img[3][2] + img[3][3]
return 0.5 * middle + 3 * img[5][5]
full = model(image)
for r in range(0, 6, 2):
row = []
for c in range(0, 6, 2):
covered = [line[:] for line in image]
for i in (r, r + 1):
for j in (c, c + 1):
covered[i][j] = 0 # hide one 2 by 2 patch
row.append(full - model(covered)) # how much the answer dropped
print(row)[0.0, 0.0, 0.0] [0.0, 2.0, 0.0] [0.0, 0.0, 3.0]
هر عدد این جدول یعنی «اگه این تیکه رو بپوشونی، جواب اینقدر کم میشه». پوشوندن وسط، ۲ کمش کرد؛ پوشوندن گوشهی پایین، ۳. بقیه هیچ اثری نداشتن. اگه این جدول رو به رنگ روی عکس بکشید، گوشهی پایین از همه پررنگتر میشه، و همون لحظه میفهمید مدل به جای اشتباهی نگاه میکنه. برای عکسهای واقعی با هزاران پیکسل، این کار کنده، چون برای هر تیکه یه بار باید جواب مدل رو گرفت؛ برای همین کارن سیمونیان و همکارهاش سال ۲۰۱۳ از شیب جواب نسبت به پیکسلها استفاده کردن، که با یه برگشت پسانتشار همهی پیکسلها رو یهجا میده.
چی باید عوض میشد
برای کسی که مدل دربارهش تصمیم گرفته، یه جور توضیح از همه به درد بخورتره:
به جای اینکه بگیم «درآمدت اینقدر اثر داشت و بدهیت اونقدر»، میگیم: «اگه بدهیت ۳ تا کمتر بود، قبول میشدی». این توضیح هم فهمیدنش راحتتره، هم میگه آدم چی کار میتونه بکنه. به این مدل توضیحها میگن اگهای، یا «خلاف واقع».
پیدا کردنش هم ایدهی سادهای داره: کوچیکترین تغییری توی ورودی رو پیدا کن که جواب مدل رو عوض کنه. فقط باید حواستون باشه تغییر شدنی باشه؛ «اگه ده سال جوونتر بودی» توضیح به درد بخوری نیست، چون هیچکس نمیتونه انجامش بده.
اگهای، با کد
پیدا کردن یه توضیح اگهای، توی سادهترین شکلش، فقط امتحان کردنه. یه قانون وام ساختگی داریم و کسی که ۲۵ درآمد و ۹ بدهی داره:
def approve(income, debt): # a made-up loan rule standing in for any model
return 0.6 * income - 0.9 * debt > 10
person = {"income": 25, "debt": 9}
print("now:", approve(**person))
for cut in range(0, 10):
if approve(person["income"], person["debt"] - cut):
print("smallest debt cut that flips it:", cut); break
for more in range(0, 40):
if approve(person["income"] + more, person["debt"]):
print("or more income:", more); breaknow: False smallest debt cut that flips it: 4 or more income: 6
جواب الان «نه»ه. برنامه بدهی رو یکییکی کم میکنه تا جواب عوض بشه: با ۴ تا کمتر. یه بار هم درآمد رو یکییکی زیاد میکنه: با ۶ تا بیشتر. حالا میشه به این آدم گفت «اگه ۴ تا از بدهیت کم بود، یا ۶ تا درآمدت بیشتر بود، قبول میشدی». برای مدلهای واقعی با دهها ستون، بهجای امتحان یکییکی، دنبال نزدیکترین نقطهای میگردن که جواب عوض بشه، و فقط ستونهایی رو تغییر میدن که آدم واقعا میتونه عوضشون کنه. سندرا واچتر و همکارهاش سال ۲۰۱۷ همین رو بهعنوان یه راه توضیح برای قانون حق توضیح پیشنهاد دادن.
حد توضیح
توضیحها خیلی کمک میکنن، ولی سه تا چیز رو هیچوقت فراموش نکنید.
توضیح مدل، علت دنیا نیست. اگه توضیح بگه «ساعت خواب روی نمره اثر داشت»، یعنی مدل به ساعت خواب تکیه کرده؛ نه اینکه ثابت شده خواب بیشتر نمره رو بالا میبره. شاید بچههایی که بیشتر میخوابن، به یه دلیل دیگه نمرهی بهتری دارن. مدل همبستگیها رو پیدا میکنه؛ علت رو باید جای دیگه ثابت کرد.
توضیح، خودش یه سادهسازیه. وقتی یه جعبهی سیاه رو با چند تا عدد توضیح میدیم، داریم یه چیز پیچیده رو خلاصه میکنیم. خلاصهها بخشهایی رو جا میندازن، و گاهی دو روش توضیح، برای یه پیشبینی، دو داستان مختلف میگن.
ستونهای بههم وابسته گیجکنندهان. اگه دو تا ستون تقریبا یه چیز رو بگن، مثل قد به سانتیمتر و قد به اینچ، بههم زدن یکیشون شاید خطا رو زیاد بالا نبره، چون مدل از اون یکی کمک میگیره. پس «کماهمیت» همیشه یعنی «بیاهمیت» نیست.
دو ستون، یه حرف
گفتیم ستونهای بههم وابسته گیجکنندهان. این رو هم با عدد ببینیم. یه مدل ساختیم که وزن آدم رو از قدش حدس بزنه، و قد رو دو بار بهش دادیم: یه بار به سانتیمتر، یه بار به اینچ، با یه کم نویز:
مدل تقریبا همهی وزنش رو به اینچ داد، ۱٫۲۶، و به سانتیمتر فقط ۰٫۰۳؛ که کاملا شانسی بود و میتونست برعکس باشه. حالا اگه سانتیمتر رو بههم بزنیم، اشتباه از ۲٫۵ فقط به ۲٫۵۳ میرسه؛ انگار سانتیمتر هیچ اهمیتی نداره. ولی وقتی مدلی ساختیم که فقط سانتیمتر داشت، بههم زدنش اشتباه رو به ۶٫۳۱ رسوند. یعنی قد خیلی مهمه؛ فقط چون دو جا بود، اهمیتش بین دو تا ستون تقسیم شد، اونم نابرابر.
راهش اینه که قبل از بههم زدن، ستونهایی که خیلی به هم شبیهان رو پیدا کنید و یا با هم بههم بزنید، یا یکیشون رو نگه دارید. توی دادهی واقعی این خیلی پیش میاد؛ مثلا درآمد ماهانه و درآمد سالانه، یا سن و سال تولد.
چند نکته
همیشه بپرسید «چرا»، مخصوصا وقتی مدل بیش از اندازه خوبه؛ اون ۹۵ درصد یادتون باشه. اگه ساده کافیه، ساده، چون جعبهی سفید خودش توضیحه. برای کل مدل، ستونها رو بههم بزنید؛ عجیبترین ستون مهم، بزرگترین هشداره. برای یه نفر، سهم هر ویژگی رو پله به پله از میانگین تا جواب نشون بدید. برای کاربر، «اگه…» بگید؛ چیزی که شدنیه و جواب رو عوض میکنه. و توضیح رو علت نگیرید؛ مدل میگه به چی نگاه کرده، نه اینکه دنیا چطوری کار میکنه.
و یادتون باشه توضیح برای کیه. سازندهی مدل دنبال اینه که مدل کجا اشتباه فکر میکنه، و نقشه و بههم زدن به کارش میاد. کسی که مدل دربارهش تصمیم گرفته، دنبال اینه که چی کار میتونه بکنه، و توضیح اگهای به کارش میاد. کسی که باید بر کار نظارت کنه، دنبال اینه که مدل برای همه یهجور رفتار میکنه یا نه، و سنجیدن جدا برای هر گروه، که توی درس قبل دیدیم، به کارش میاد. یه توضیح برای همه کافی نیست.
اشتباههای رایج
رایجترینش اعتماد به یه عدد درستی بالا، بی پرسیدن «چرا»؛ دیدیم ۹۵ درصد میتونه روی دنیای واقعی ۵۳ درصد باشه. بعدی نتیجه گرفتن «بیاهمیته» از بههم زدن یه ستون، وقتی دوقلوش هنوز توی دادهست. بعدی خوندن توضیح بهعنوان علت. بعدی توضیح اگهای که چیز نشدنی پیشنهاد میده. و آخری، باور کردن یه روش توضیح بهتنهایی؛ وقتی دو روش دو داستان میگن، یعنی مدل پیچیدهتر از اونیه که هر دو خلاصه نشون میدن.
از کجا اومد
بیشتر ابزارهای توضیح، تازهان، جز یکی که از ریاضی بازیها اومده:
لوید شپلی سال ۱۹۵۱ ایدهی سهم منصفانه رو داد. لئو بریمن سال ۲۰۰۱ اهمیت با بههم زدن رو برای جنگل تصادفی گفت. کارن سیمونیان و همکارهاش سال ۲۰۱۳ نقشهی برجستگی رو برای شبکههای عکس. مارکو ریبیرو و همکارهاش سال ۲۰۱۶ یه روش توضیح محلی دادن که دور هر پیشبینی یه مدل سادهی کوچیک میسازه. و سال ۲۰۱۷ هم شپلی به مدلها رسید، هم توضیح اگهای.
این آخرین درس مسیر بود. درس بعد کل راه رو، از داده تا مدل و از خطا تا تحویل، توی یه تصویر جمع میکنه:حالا یادگیری ماشین رو میشناسید.
جمعبندی. آنچه از این درس با خودتان میبرید.
- درست گفتن کافی نیست؛ به دلیل درست درست بگه.
- جعبهی سفید خودش رو توضیح میده؛ جعبهی سیاه نه.
- بههم زدن یه ستون: اون ستون چقدر مهمه.
- سهم هر ویژگی: چرا برای این یه نفر این جواب.
- توضیح مدل، علت دنیای واقعی نیست.
خودتون رو بسنجید
۱۵ پرسش، هر بار تازه از میان ۳۰ پرسش این درس. آخرش فقط کارنامه رو میبینید: چند تا درست، چند تا نادرست.