סיכום תחרות Kaggle SIIM-ISIC Melanoma Classification (קבוצת MelANOVA)

מאת ים פלג, 23 באוגוסט 2020

הירשמו לערוץ יוטיוב שלנו!

כל הסרטונים מאירועי הקהילה

סיכום תחרות זו עלה כחלק מפוסט של ים פלג.

קבוצת MelANOVA:
על התחרות:
מלנומה – סרטן שרוב גידולו מתחילים בעור.
דיאגנוזה למלנומה כוללת בין היתר התבוננות בנקודות חן ושומות.
הבעיה ברורה: אנחנו מקבלים תמונות של שומות -> צריכים להחזיר: "כן סרטני" / "לא סרטני".
התחרות רצה שנה אחרי שנה כבר כמה שנים ברציפות.
את התחרות פתחנו במקום הראשון.
הגשה של ספי ונתי מהרצת האלגוריתם שפיתחו לתחרות של שנה שעברה.
אף אחד מאיתנו לא חשב שהמצב ימשיך כך לאורך זמן, ישבנו וחשבנו על אסטרטגיה מנצחת להמשך התחרות.
אז איך מנצחים תחרויות מדע נתונים? [מסודר לפי סדר חשיבות]
  1. אסטרטגית Cross Validation טובה.
  2. ניקוי טוב של הנתונים.
  3. הנדסת פיצ'רים טובה.
  4. מציאת טריקים\"רמאויות קטנות" בדאטה או במטריקה.
  5. מודל טוב.
  6. אופטימיזציה טובה להיפרפרמטרים.
  7. שיטה טובה לאנסמבל [בדגש על סטאקינג].
  8. שיטה טובה לאוגמנטציות (גם בזמן האימון וגם בזמן ההרצה – TTA).
  9. כוח מחשוב. זה תמיד עוזר.
  10. מזל. (כן כן! מזל מאוד עוזר)

מיותר לציין, המנצחים בתחרויות מהסוג הזה (סיווג תמונות – Non-Notebook Competition) בדרך כלל משתמשים בכוח מחשוב רב עוצמה.
מצוין. אנחנו מאשרים.
אחד היתרונות היחסיים שעמדו לרשותינו הוא כוח מחשוב. אבל (וזה אבל גדול) בסופו של דבר תמיד יהיה מישהו עם יותר כוח מחשוב.
לכולנו זכור הפתרון המנצח בGoogle AI Open Images 2018 שהשתמש בלא פחות מ512 GPUs.
גם הנהלת קאגל זוכרים. מאז הם נוקטים בגישה יצירתית ומצויינת כדי להילחם בתופעה: הם פשוט מחלקים לכולם כוח מחשוב כבד בחינם – לכולם יש 30 שעות TPU שבועיות בחינם.
ישבנו התייעצנו כיצד נוכל להביא את היתרון היחסי שלנו לידי ביטוי בתחרות אם לכולם יש TPUs חינם?
התשובה ברורה: אנחנו צריכים לגנוב TPU.
החלטנו "לגנוב" את הכוח של הTPU מרחוק.
נשתמש בכוח של הTPUs בשביל pretraining ואת המשקולות המאותחלות נוריד אלינו ונאמן לוקאלית המון רשתות במהירות שיא הרבה יותר מהר משאר המתחרים.
התחלנו לממש מאמרי Self-Supervision:
  • Auto Encoder – סתם אוטואנקודר
  • GAN – Using the generator – סתם WGAN
  • GAN – Using the discriminator – סתם WGAN
  • Colorization – סתם לצבוע מחדש תמונות שחורלבן
VQVAutoEncoder – 'Neural Discrete Representation Learning' https://arxiv.org/abs/1711.00937
אמ;לק: אוטואנקודר עם מרחב נסתר קטגוריאלי. עובד ממש טוב.
כזכור, רכיבי VAE מורכבים משלושה חלקים:
. רשת מקודדת שלומדת את הפוסטיריור q (z | x) ל גבי latents.
פריור התפלגות קודמת p (z), מפענח עם התפלגות p (x | z) על נתוני קלט.
בדרך כלל אנו מניחים שהפריור והפוסטיריור מתפלגים נורמלית כלל עם diagonal variance. ואז משתמשים במקודד כדי לחזות את הממוצע והשונות של הפוסטיריור.
בעבודה הזאת, לעומת זאת, המחברים משתמשים במשתנים דיסקרטים לlatent (במקום התפלגות נורמלית). ההתפלגויות של הפריור והפוסטיריור הן קטגוריות, והדגימות שנדגמו מהן הן אינדקס לטבלת אמבדינגס. במילים אחרות:
המקודד לומדים התפלגות קטגוריאלית בדגימה ממנה מקבלים ערכים אינטגרליים וערכים אינטגרליים אלה משמשים לאינדקס של אמבדינגס.
משם במפענח זה Auto Encoder רגיל.
SimCLR – 'A Simple Framework for Contrastive Learning of Visual Representations' https://arxiv.org/abs/2002.05709
אמ;לק: מריצים כמה אוגמנטציות על אותה התמונה ומלמדים את הרשת האם שתי התמונות שהיא רואה הגיעו מאותה התמונה המקורית או לא.
בהרחבה : "למידת ניגודים" – נרצה ללמוד ייצוגים על ידי ניגוד בין זוגות חיוביים כנגד זוגות שליליים.
נניח שיש לנו את באצ'25 תמונות. לכל אחת מ -25 התמונות הללו נריץ הרכבה של שתי אוגמנטציות: הראשונה היא crop אקראי ושינוי גודל והשנייה היא עיוות צבעים. פעמיים לכל תמונה, נקבל שתי חדשות תמונות.
אנו מגדירים זוגות חיוביים כצמד התמונות שקיבלנו מאותה תמונה מקורית בהינתן צמד חיובי מסוים (i, j) מתמונות 2N אלה, אנו רואים בתמונות 2 (N-1) האחרות כדוגמאות שליליות עבור i ו- j. אנו רוצים שאוגמנטציות שונות של אותה תמונה יקבלו יצוגים דומים.
משתמשים גם בלוס מיוחד שלא ארחיב עליו: Normalized temperature-scaled cross-entropy loss.
BYOL – 'Bootstrap Your Own Latent: A New Approach to Self-Supervised Learning' https://arxiv.org/abs/2006.07733
השיטה מסתמכת על שתי רשתות, Online וTarget, המקיימות אינטראקציה ולומדות זו מזו. מאמנים את רשת הOnline לחזות את הייצוג של רשת הTarget עם אותה תמונה תחת אוגמנטציות שונות. במקביל, מעדכנים את רשת הTarget עם ממוצע נע של רשת הOnline.
CycleGAN between the classes – 'Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks' https://arxiv.org/abs/1703.10593
נניח ויש לנו ערימת תמונות ממחלקה אחת וערימת תמונות ממחלקה אחרת (תפוזים ותפוחים) אבל אין לנו תמונה מתוייגת ממחלקה אחת ואת הזוג שלה מהמחלקה האחרת (אותה הקערה בדיוק שיש בה תפוח במקום תפוז). נאמן GAN לצד אחד ואז GAN לצד השני ונמדוד שני לוסים, הראשון למעבר הראשון והשני לשחזור.
כך נלמד להעביר בין המחלקות. [אנחנו עשינו את זה עם "חולה" ו"לא חולה"]
RotNet – 'Unsupervised Representation Learning by Predicting Image Rotations' https://arxiv.org/abs/1803.07728
נסובב את התמונה ונאמן את הרשת לאמר לנו מה זווית הסיבוב. רעיון גאוני.
התוצאות:
BYOL היה כמו SimCLR ושניהם היו הכי טובים אבל יקרים חישובית.
קרוב אליהם מאוד:
VQVAutoEncoder
התקבענו [בשל מגבלת כוח חישוב, לא ביצועים] על VQVAutoEncoder כשהאנקודר שלו הוא גנרטור של GAN.
אני אומר לכם, לאמן אוטואנקודר על הדאטה הזה יצר את הרשת המגעילה בעולם. התמונות שהיא יצרה במסגרת thisdoesnotexist.com היו פשוט דוחות!
צחקנו מכל הסיטואציה אבל זה לא הספיק לנו, החלטנו שהBackbone של הכל חייבת להיות רשת שכבר מאומנת על Imagenet ולא רנדומלית.
נבחרה משפחת הרשתות של EfficientNet [הסבר בהמשך] כBackbone. לשם כך נאלצתי לפתח חבילה שלמה סביב משפחת רשתות אלו: Efficient-UNet – רשתות Encoder-Decoder רק שהן תמונת מראה של EfficientNet המאומנות מראש לסיווג.
הטריק האמיתי היה שבאותה החבילה היתה היכולת "לתלוש" את הEfficientNet הפנימית שהיתה בתוך הEncoder-Decoder ולהשתמש בה אחרי זה כרשת סיווג רגילה.
כל רעיון הזה של להריץ Transfer Learning מImagenet מוכר כבר בקאגל כסאב-אופטימלי מאוד וידוע שרשתות שונות שאומנו מראש על דאטהסטים שונים (ואפילו בחבילות שונות!!) מגיעות לתוצאות שונות לחלוטין באימון מחדש למשימות אחרות.
במקום לעשות את זה, הלכנו עוד יותר "ראש בקיר": נכון הפילטרים של הקונבולוציה? ניסינו את כל הקומבינציות של כל הפילטרים מכל הEfficientNetים מכל הספריות (המרנו את המשקולות של פייטורצ' וטנזורפלו2 למשקולות של קרס).
פשוט הרכבנו Pretraining Weights מוצלחים יותר (לדאטה הנוכחי) על ידי אלגוריתם גנטי שבוחר מאיזו רשת לקחת איזה פילטר לאיזו שכבה ואז הוא מאמן אפוק אחד על הדאטה.
לילה אחד ויחיד על ארבע GPUs ובבוקר היו לנו משקולות התחלתיות טובות יותר מהמקוריות [בהרבה].
התחלנו במבצע: גוגל אמנם מחלקים כוח חישוב בחינם אבל לא כוח חישוב לא מוגבל. כמו שאמרתי, לכל אחד יש 30 רק שעות שבועיות.
אז כך היה, אימנתי מחברת בקגל במשך 29 שעות, 59 דקות ו58 שניות (באמת! יש סקרינשוט של זה!).
ואז היא עברה לספי לעוד אימון..
ואז לנתי לעוד אימון..
בסוף התהליך קיבלנו את קובץ המשקולות הקסומות: "efn_magic_weights.h5".
מעכשיו, התחלנו ליצור רשתות בבית שלנו, לאתחל אותן למשקולות הקסם ובתוך אפוק אחד [של כמה דקות] הן הגיעו לתוצאות קומפטטיביות.
כמובן שלא אימנו רק אפוק אחד.
מתחנו גבולות, אימנו את כל הEfficientNetים משורשרות (את B0 עד B7) בתוך מודל אחד.

הגשנו: טופ 50 – מדליית כסף!

עם מודל אחד – לא רע!
ועכשיו כשבידיינו שיטה חזקה ל"דחיסה" של חשבון החשמל של גוגל אל תוך רשתות נוירונים. התחלנו לדבר על הדאטה, המודל והתחרות עצמה.
החלטנו להתפצל, שאר חברי הצוות התרכזו בדאטה ואוגמנטציות ואני התרכזתי במודל עצמו.
בלי להפעיל יותר מידי את הראש, נכנסתי ל papers with code ופירקתי שם סוף שבוע.
עברתי מאמר מאמר שהיה SOTA על Imagenet ומימשתי את כולם בקרס:
בשביל שיטות אימון, מימשתי את:
AutoAugment – 'AutoAugment: Learning Augmentation Policies from Data' https://arxiv.org/abs/1805.09501
מגדירים מרחב אוגמנטציות ומחפשים אוגמנטציה אופטימלית לכל תמונה כשתמקסם את תוצאת הרשת. כמו שאני אוהב! המאמר הזה הוא שהוא קונספט ואפשר פשוט "לזרוק אותו" על איזו רשת שנרצה עם איזה אוגמנטציות שנרצה.
בהרחבה: מרחב המדיניות מורכבת מתת מדיניות אוגמטנציה, אשר אחת מהן נבחרת באופן אקראי עבור כל תמונה בכל באצ'. תת מדיניות מורכבת משתי פעולות, כאשר הפעולה היא פונ' אוגמנטציה על התמונה כמו טרנסלציה, סיבוב או crop, וההסתברויות והעוצמה בהן מיישמים את הפונקציות. משתמשים באלגוריתם חיפוש [כרצוננו] כדי למצוא את המדיניות הטובה ביותר כך שהרשת תניב את דיוק הגבוה ביותר על הולידציה.
RandAugment – 'RandAugment: Practical automated data augmentation with a reduced search space' https://arxiv.org/abs/1909.13719
הם מצאו שלא צריך לבחור אוגמנטציה ומספיק לבחור עוצמות שונות לכל האוגמנטציות ביחד.
בהרחבה: בעוד שבעבודה הקודמת דרשו חיפוש הן בעוצמה והן בהסתברות של כל פעולה באופן עצמאי, מסתבר שמספיק רק לחפש גודל עיוות יחיד ששולט על כל הפעולות. מכאן הם מציעים מרחב חיפוש מפושט ומקטינים באופן משמעותי את החישובים.
למרות הפשטות, השיטה שלהם משיגה ביצועים טובים יותר ב- CIFAR-10/100, SVHN, ImageNet ו- COCO.
AdvProp – 'Adversarial Examples Improve Image Recognition' https://arxiv.org/abs/1911.09665
אמ;לק: משתמשים בדוגמאות אדברסריאליות ובBatchNorm אחר רק לאותן הדוגמאות.
בהרחבה: מציגים בפעם הראשונה שניתן להשתמש בדוגמאות אדברסריאליות כדי לשפר מודלים. מציעים שיטת אימון משופרת המתייחסת לדוגמאות האדברסריאליות כדוגמאות נוספות. הטריק לשיטה הוא השימוש בנורמה של Auxiliary Batch נפרד לדוגמאות שליליות, מכיוון שיש להם התפלגויות שונה מהדוגמאות רגילות.
NoisyStudent – 'Self-training with Noisy Student improves ImageNet classification' https://arxiv.org/abs/1911.04252
אמ;לק: נשתמש בפסודו-תיוגים (pseudo labeling – פרדיקציה של מודל על ולידציה כתיוג למודל אחר), בלופ: המודל יחזה ואז נשתמש בפרדיציה שלו על הולידציה כעוד דגימות לאימון. נעשה זאת בלולאה כמה שנרצה. הטריק: אוגמנטציה כבדה ל"סטודנט" ואפס אוגמנטציה ל"מורה". זה לא עובד בלי זה – ניסיתי.
Mean Teacher <=> Noisy Student – 'Mean teachers are better role models: Weight-averaged consistency targets improve semi-supervised deep learning results' https://arxiv.org/abs/1703.01780
אמ;לק: אותו הדבר כמו Noisy Student רק שנשתמש בפרדיקציות של המורה עם ממוצע נע ממשקולות קודמות שלו בשלבים מוקדמים יותר באימון.
הוספתי עוד כמה שיטות אימון שלא היו SOTA בImagenet:
הרצתי import Leslie N. Smith. הבן אדם פשוט תותח. מי שלא מכיר: ממליץ!
CyclicLearningRate – 'Cyclical Learning Rates for Training Neural Networks' https://arxiv.org/abs/1506.01186
אמ;לק: נעלה ונוריד את הlr לאורך האימון כדי "לקפוץ" מעל גבעות במשטח האופטימיזציה.
LRFinder – 'Super-Convergence: Very Fast Training of Neural Networks Using Large Learning Rates' https://arxiv.org/abs/1708.07120
'A disciplined approach to neural network hyper-parameters: Part 1 — learning rate, batch size, momentum, and weight decay' https://arxiv.org/abs/1803.09820
אמ;לק: נעלה את הlr לאורך חצי מהאימון ונוריד את הlr בחצי השני. נתבונן בלוס, הlr עבורו הלוס היה הנמוך ביותר הוא הlr האופטימלי לרשת.

על הדרך: הרצתי ברוטפורס של כל קומבינציות שיטות האימון, המנצח: NoisyStudent + RandAugment עם LRFinder וCyclicLR. בדיוק כמו בImagenet.

אופטימייזר:
השתמשנו בPAdam החדש. לא הספקתי לקרוא את המאמר. הרצתי בלי להבין איך הוא עובד והוא שיפר תוצאות – מספיק טוב בנתיים!
מודלים:
DenseNet – 'Densely Connected Convolutional Networks' https://arxiv.org/abs/1608.06993
אמ;לק: כמו Resnet רק שכל הSkipים מחוברים לסוף הבלוק עבור כל בלוק. [מפשט קצת מחוסר רלוונטיות, יש עוד כמה טריקים במאמר]
PreResNet – 'Identity Mappings in Deep Residual Networks' https://arxiv.org/abs/1603.05027
אמ;לק: כמו רזנט רק עם "טוויסט" בבלוק שיש לו הצדקה תיאורטית.
def presnet_block(x, filters, kernel_size):
if bottle_neck: x = Conv2D(filters // 4, 1)(Activation("relu")(BatchNormalization()(x)))
Add()([Conv2D(filters, kernel_size)(Activation("relu")(BatchNormalization()(x))), Conv2D(filters, 1)(x)]
SE-PreResNet – 'Squeeze-and-Excitation Networks' https://arxiv.org/abs/1709.01507
אמ;לק: SE Blocks – הם Fully Connected על הChannelים בשילוב עם GlobalAvgPooling והכפלה בצ'נאלים שוב.
בעברית: אטנשן על הצ'אנלים.
def se_block(x_inp, filters):
x = AvgPool2D(x_inp.shape[1:3])(x_inp)
x = Conv2D(filters // 16, 1, activation='relu')(x)
x = Conv2D(filters // 16, 1, activation='linear')(x)
x = Activation('sigmoid')(x)
return Multiply()([x_inp, x])
DarkNet – 'Darknet: Open source neural networks in c' https://github.com/pjreddie/darknet
אמ;לק: הארכיטקטורה ממנה נגזרת YOLO של Object Detection. אין פה משהו מיוחד מידי, רצים על X ו Y (כi,j) גדלי בלוקים לבחירתינו ובונים בלוקים מהצורה הבאה:
def darknet_block(x, filters, i, j, odd_pointwise):
if not(((j + 1) % 2 == 1) ^ odd_pointwise):
x = Conv2D(filters, 1)(x)
x = BatchNormalization()(x)
x = LeakyReLU()(x)
else:
x = Conv2D(filters, (3, 3))(x)
x = BatchNormalization()(x)
x = LeakyReLU()(x)
return x
ShuffleNet – 'ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices' https://arxiv.org/abs/1707.01083
אמ;לק: ניסו לערבב את הצ'אנלים בין כל כמה בלוקים של קונבולוציה 3×3.
הlambda הבאה בקרס עושה את זה:
def channel_shuffle(x, groups):
batch, height, width, channels = x.shape
return K.reshape(K.permute_dimensions(K.reshape(x, shape=(-1, height, width, groups, channels // groups)), pattern=(0, 1, 2, 4, 3)), shape=(-1, height, width, channels))
MEnet – 'Merging and Evolution: Improving Convolutional Neural Networks for Mobile Applications' https://arxiv.org/abs/1803.09127
אמ;לק: קונספט חדש: קונבולוציה בקבוצות (Group Conv), לחלק מהפילטרים נריץ קונבולוציה אחת ולחלק קונבולוציה אחרת. כלומר: לא נשתף מידע ביניהם בזמן העיבוד.
def GroupConv2D(x, in_channels, out_channels, kernel_size, groups):
in_group_channels = in_channels // groups
out_group_channels = out_channels // groups
group_list = []
for gi in range(groups):
xi = Lambda( (lambda z: z[:, gi * in_group_channels:(gi + 1) * in_group_channels, :, ) if is_channels_first() else (lambda z: z[:, :, :, gi * in_group_channels:(gi + 1) * in_group_channels]))(x)
xi = Conv2D()( filters=out_group_channels, kernel_size=kernel_size, group_list.append(xi))
x = Concatenate()(group_list)
return x
המשך אמ;לק: רשתות נוירונים למובייל מנצלות את הspasity של קובנולוציות עומק וGroup Convolutions. הפעולות האלה אלה חסכוניות יותר מבחינה חישובית. עם זאת, הן גם חוסמות את העברת המידע בין הGroups, והביצועים בהתאם. הטריק במאמר הוא רפרזנטציה של המידע בין הקבוצות עם feature map צר, ואז שילוב של הפיצ'רים שנוצרו עם הכניסה המקורית לבלוק בשביל לייצוג טוב יותר.
def me_block(identity, filters):
x = Conv2D(filters, 1)(identity)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = Lambda(lambda x: channel_shuffle(x))(x)
y = Conv2D(filters, 1)(x)
y = BatchNormalization()(y)
y = Activation("relu")(y)
x = DepthwiseConv2D(filters // 4, (3, 3))(x)
x = BatchNormalization()(x)
# Evolution Block
y = Conv2D(filters // 4, (3, 3))(y)
y = BatchNormalization()(y)
y = Activation("relu")(y)
y = Conv2D(filters // 4, 1)(y)
y = BatchNormalization()(y)
y = Activation("sigmoid")(y)
x = Multiply()([x, y])
x = DepthwiseConv2D(filters, (3, 3))(x) # Pointwise group convolution
x = BatchNormalization()(x)
return Add()([x, identity])
Igcv3 – 'IGCV3: Interleaved Low-Rank Group Convolutions for Efficient Deep Neural Networks' https://arxiv.org/abs/1806.00178
אמ;לק: נריץ Group Conv עם Blottle Necks. לא מוסיף את הקוד כי אין סיבה, זה קל מדי.
MNasnet – 'MnasNet: Platform-Aware Neural Architecture Search for Mobile' https://arxiv.org/abs/1807.11626
אמ;לק: השתמשו בחיפוש ארכיטקטורה כדי למצוא בלוקים אופטימליים (NasNet) לבנות מהם רשת, רק שהם גם הוסיפו לReward את זמן הinference (פשוט time.time בקוד). מיקסמו בסופו של דבר:
maximize m:
ACC(m)
subject to LAT(m) ≤ T
כל בלוק מאפטם את:
  • Search Space Per Block i:
  • ConvOp: dconv, conv, …
  • KernelSize: 3×3, 5×5
  • SERatio: 0, 0.25, …
  • SkipOp: identity, pool, …
  • FilterSize: Fi
  • Layers: Ni
יצא להם:
Conv (3×3) -> SepConv (k3x3) x1 -> MBConv6 (k3x3) x2 -> MBConv3 (k5x5), SE x3 -> MBConv6 (k3x3), x4 -> MBConv6 (k3x3), SE x2 -> MBConv6 (k5x5), SE x3 -> MBConv6 (k3x3), x1 -> Pooling, FC – > logits
לא אוסיף את הקוד, יש פה את כל הבלוקים. המתעניינים יכולים להרכיב לבד.
SEResNeXt – 'Squeeze-and-Excitation Networks,' https://arxiv.org/abs/1709.01507
אמ;לק: שילוב של SE עם ResNet עם ResNext. שוב כל הקוד כאן. מוזמנים להרכיב.
SEDenseNext – 'Learning Channel Inter-dependencies at Multiple Scales on Dense Networks for Face Recognition' https://arxiv.org/pdf/1711.10103
אמ;לק:
חצי תפירה שלי, חצי מהמאמר: אותו דבר כמו קודם רק DenseNet.
Big Transfer – 'Big Transfer (BiT): General Visual Representation Learning' https://arxiv.org/abs/1912.11370
אמ;לק: רשתות Big Transfer הן רשתות מיוחדות לTransfer Learning, הטריקים: Group Normalization ו Weight Standardization בכל שכבות הקונבולוציה.
במקום BatchNormalization.
הם מוסיפים כללי אצבע לאימון רשתות טובות לTransfer Learning (שלדעתי ספציפיים מידי):
הם ממליצים להשתמש ב SGD עם מומנטום. lr ראשוני של 0.03 ומומנטום 0.9 משתמשים בCrop ושיקוף אופקי אקראי ואחריו שינוי גודל. מאמנים ל90 אפוקים ומורידים lr בפקטור של 10 באפוקים 30, 60 ו 80. משתמשים בבאצ' ענקי של של 4096.משתמשים lr warmup לינארי של 5000 שלבים ומכפילים את גודל הlr בגוגל הבאצ'. בwarmup הם משתמשים weight decay של 0.0001.

הערת שוליים: אתם צודקים! זה באמת מתבקש: בסוף הרצתי ברוטפורס ארכיטקטורה של כל הבלוקים מכל הרשתות על 12 GPUs – יצא גרוע.

ואיך לא, ה-רשת:
EfficientNet
הגיע הזמן. אני רוצה לעצור רגע ולדבר על EfficientNet ועל כמה שהמאמר הזה מצוין.
מה זאת הEfficientNet הזאת שאני לא מפסיק להתלהב ממנה? סיכום מאמר:
המאמר:
EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks introduces a new principle method to scale up ConvNets – https://arxiv.org/abs/1905.11946
המאמר EfficientNet מתאר שיטלה לשינוי גודלם של מודלים ומציג שיטה עקרונית חדשה להגדלה של ConvNets.
בשביל להשיג דיוק טוב יותר, על CNN להיות מסוגלת לאזן בזהירות בין רוחב עומק לרזולוציה. עם זאת, מעולם לא הובן לפני המאמר כיצד השלושה משפיעים על CNNים.
הדרך הנפוצה ביותר הייתה להרחיב CNNים לפי עומקם או רוחבם. דרך פחות נפוצה נוספת הייתה ליישר את המודל לפי רזולוציית תמונה. עד כה אנחנו דנים בשינוי רק במימד אחד.
עומק:
רשתות עמוקות יותר יותר תופסות תכונות מורכבות יותר ומכלילות טווב יותר. עם זאת, קשה יותר לאמן אותן בלי סקיפים בגלל פיצוצי גרדייאנט. למרות כל הטריקים השיפור בדיוק פוחת עבור רשתות עמוקות מאוד.
רוחב:
רשתות רחבות יותר נוטות לתפוס פיצ'רים מדויקים יותר וקל יותר לאמן אותן. עם זאת, הדיוק ברשת כזו מגיע לרוויה במהירות.
רזולוציה:
עם תמונות קלט ברזולוציה גבוהה יותר, CNNים יכולות לתפוס דפוסים עדינים יותר. עם זאת, עבור רזולוציות גבוהות מאוד, הדיוק מפסיק לעלות.
ספציפית בתחרות הזו החלטתי "לבזבז" קצת זמן TPU על ברוטפורס כל הרזולוציות מול כל העומקים מול כל ה"רוחבים" (אני מרגיש שזה לא בסינטקס).
התוצאות היו חד משמעיות: השיפור הניכר ביותר הגיע מרזולוצית התמונה.
יש לי דרך אגב את כל התוצאות אם מישהו מתעניין באיך נראה גרף השיפור.
נחזור לאמר, נחשוב עכשיו על קנה מידה של מימד מרובה בכל פעם. ניתן לשנות קנה מידה של שניים או שלושה ממדים באופן שרירותי, אבל זה מחייב כוונון ידני שלעתים קרובות מביא לדיוק ויעילות סאב-אופטימליים.
במאמר הם מנסים לטפל בנושא הבא:
"האם קיימת שיטה עקרונית להגדלת CNNים בכל המימדים שיכולה להשיג דיוק ויעילות טובים יותר?"
המחקר האמפירי שלהם מראה כי חשוב לאזן את כל ממדי הרשת (רוחב / עומק / רזולוציה) בו זמנית.
ניתן להשיג איזון כזה על ידי קנה מידה של כל אחד מהם ביחס קבוע.
שיטה זו מכונה "Compound Scaling", המורכבת מאיזון בקנה מידה של רוחב, עומק והרזולוציה באמצעות קבוצה של מקדמי קנה מידה קבועים.
האינטואיציה נובעת מהעובדה הבאה:
אם תמונת הקלט גדולה יותר (רזולוציה), יש תכונות מורכבות יותר ודפוסים עדינים. בכדי לתפוס תכונה מורכבת יותר, הרשת זקוקה Receptive Field גדול יותר אשר מושג על ידי הוספת שכבות נוספות (עומק). כדי לתפוס דפוסים עדינים יותר, הרשת זקוקה ליותר ערוצים.
השיטה "Compund Scaling" משתמשת במקדם מורכב: ϕ
לקנה מידה אחיד של רוחב הרוחב, העומק והרזולוציה באופן עקרוני:
עומק: φ^d = α
רוחב: φw = β
רזולוציה: ^φr = γ
s.t.
α · β^2 · γ^2 ≈ 2
α ≥ 1, β ≥ 1, γ ≥ 1

[זה בטוח ישבר בהדבקה בפייסבוק..]

כש α, β, γ הם קבועים שנמצאו בברוטפו.. סליחה! אסור לומר את המילה הזאת, התכוונתי GridSearch.
הם קבועים שניתן לקבוע באמצעות חיפוש Grid Search קטן.
במאמר זה, האילוץ מקשר בין כוח החישוב (FLOPS) לבין גודל הרשת [לא אכנס אליו].
כדי להשתמש בשיטה זו, יש צורך במודל בסיסי, המכונה "EfficientNet B0". מצאו אותו בברוט..פטימיזציה של מבנה בלוקים.
ולבסוף הם השתמשו בנוסחה כדי להגדיל את הרשת לכמה גדלים שמסומנים בB0 עד B7.
הנה כל הEfficientNetים בקרס:
def EfficientNet_B0(channels, expansion_s, repeats, strides, kernel_sizes, d_, w_, r_, dropout_rate, include_top, se_ratio = 0.25, classes=1000):
inputs = Input(shape=(224, 224, 3))
stage1 = ConvBlock(inputs, filters=32, kernel_size=3, stride=2)
stage2 = MBConvBlock(stage1, scaled_channels(channels[0], w_), scaled_channels(channels[1], w_), kernel_sizes[0], expansion_s[0], se_ratio, strides[0], scaled_repeats(repeats[0], d_), dropout_rate=dropout_rate)
stage3 = MBConvBlock(stage2, scaled_channels(channels[1], w_), scaled_channels(channels[2], w_), kernel_sizes[1], expansion_s[1], se_ratio, strides[1], scaled_repeats(repeats[1], d_), dropout_rate=dropout_rate)
stage4 = MBConvBlock(stage3, scaled_channels(channels[2], w_), scaled_channels(channels[3], w_), kernel_sizes[2], expansion_s[2], se_ratio, strides[2], scaled_repeats(repeats[2], d_), dropout_rate=dropout_rate)
stage5 = MBConvBlock(stage4, scaled_channels(channels[3], w_), scaled_channels(channels[4], w_), kernel_sizes[3], expansion_s[3], se_ratio, strides[3], scaled_repeats(repeats[3], d_), dropout_rate=dropout_rate)
stage6 = MBConvBlock(stage5, scaled_channels(channels[4], w_), scaled_channels(channels[5], w_), kernel_sizes[4], expansion_s[4], se_ratio, strides[4], scaled_repeats(repeats[4], d_), dropout_rate=dropout_rate)
stage7 = MBConvBlock(stage6, scaled_channels(channels[5], w_), scaled_channels(channels[6], w_),kernel_sizes[5], expansion_s[5], se_ratio, strides[5], scaled_repeats(repeats[5], d_), dropout_rate=dropout_rate)
stage8 = MBConvBlock(stage7, scaled_channels(channels[6], w_), scaled_channels(channels[7], w_), kernel_sizes[6], expansion_s[6], se_ratio, strides[6], scaled_repeats(repeats[6], d_), dropout_rate=dropout_rate)
stage9 = ConvBlock(stage8, filters=scaled_channels(channels[8], w_), kernel_size=1,padding='same')
return stage9
כמו שאמרתי, EfficientNet מגיעה בכמה גדלים: B0 עד B7 ועוד שתי רשתות "מפלצות" L1 ו L2 כשהגדולה ביניהן היא עם 700~ מיליון פרמטרים.
אף אדם שפוי לא מאמן את EfficientNet L2 במציאות, אין לה אפילו משקולות Pretrained, הרשת עם כמעט מיליארד פרמטרים ונראה נוצרה למטרת "להיות הראשונה בImagenet".
כל סקאלת הגדלים הזו של EfficientNet היא בסך הכל פרמטמר לנוסחת Compound Scaling שבמאמר, לאותה הנוסחה בדיוק אפשר במקום 7 להכניס 8 ולקבל את EfficientNet-B8 או 200 ולקבל את EfficientNet-B200 (בהצלחה עם לאמן אותה).
אז בשילוב עם המשפט המרכזי בתיאוריה של למידה עמוקה: "יותר גדול <=> יותר טוב" החלטנו (החלטתי) שאנחנו חייבים לאמן את הEfficientNet הגדולה בעולם כי אין ברירה אחרת: EfficientNet L3. עם מיליארד וחצי הפרמטרים.
אבל איך?
משקולות הקסם שאימנו מקודם הם בכלל של EfficientNet B5. מה לרשת הזאת ולEfficientNet L3?
טוויסט בעלילה – כל רשתות הEfficientNet מוכלות אחת בשניה: אנחנו יכולים לקחת את משקולות הקסם ולהכניס אותן לתוך EfficientNet-L3, הן פשוט "לא יכסו" את כל הרשת.
כמובן שבדיוק באותו הטריק אפשר להשתמש [עם הפעלת האלגוריתם הגנטי ממקודם] בEfficientNet קטנה יותר מזו שאימנו.
כל זה נתן לנו חופש פעולה לבניה של אנסמבלים שונים ומשונים בשילוב עם רשתות ענקיות שאין לשאר המתחרים.
שיטות נוספות ששילבנו בתחרות:
טריק Meta-Ensemble: מאמנים המון רשתות, זורקים את חלק הFulyConnected שלהן ואז משרשרים את כל הFeature Maps שלהן ביחד לרשת אחת גדולה. מקפיאים משקולות של כל שכבות הקונבולוציה ומאמנים רק את הFC [ככה ניצחו בתחרות סיווג פרחים בקאגל בתחילת השנה].
טריק אימון מקדים לBatchNorm:
'Training BatchNorm and Only BatchNorm: On the Expressive Power of Random Features in CNNs' https://arxiv.org/abs/2003.00152
אמ;לק: אני חושב שזה מסביר את עצמו.
אוגמנטציות:
  • שיקופים בשני הצירים
  • סיבובים
  • סיבובים ומתיחה
  • מתיחה
  • ערבוב לפי Grid
  • Cutmix
  • Cutout
  • AugMix
  • Mixup
  • Coarse dropout
  • RandomBrightness
ניסינו גם נרמול צבעים אבל התוצאות לא היו מובהקות, השארנו בכל מקרה.
חשבנו להשאיל משלמה ושאר הקבוצה המנצחת בGlobal Wheat Detection את הStyle Transfer אבל לא עשינו זאת בשל מחסור בזמן.
קצת Error Analysis:
חלק מהתמונות מצומות דרך מיקרוסקופ – אין בעיה: הרצנו Crop פשוט כדי להמנע מעדשת המיקרוסקופ שנכנסה בקצוות.
בחלק מהתמונות יש סרגל [כדי לתעד את גודל השומה] – אין בעיה: הוספנו סגרלים מלאכותית למלא תמונות שאין בהן סרגל.
חלק מהתמונות מצולמות באיזורים בגוף בהם יש שיער – אין בעיה: אוגמנטציה שיודעת "להעביר שיערות" מתמונות עם שיער לתמונות בלי שיער. (מישהו אמר שML זה לא מצחיק?)
טריקים באימון:
  • learning rate – "צעד תימני" (שניים קדימה, אחד אחורה)
  • loss-Label Smoothing
  • שילוב של הדאטה מ2018 ו2020 [משום מה 2019 לא עזר, הוספנו אותו אבל לPretraining]
  • אימון בגדלי תמונה משתנים.
  • המון צעדי TTA [מעל 20 לפעמים].
שיטת ולידציה:
השתמשנו ב"סטרטיפיקציה משולשת": נרצה לחלק את הדאטה לחלקים [בשביל KFold] וגם לשמור על ההתפלגות החולים והבריאים בין החלוקות כי הדאטה מאוד לא מאוזן. [סטרטיפיקציה ראשונה – Stratified KFold].
וגם, נרצה שלא יהיה את אותו הפציינט בין הtrain לולידציה של כל חלוקה [סטרטיפיקציה שניה].
וגם, לחלק מהפציינטים יש יותר תמונות מאחרים, נרצה שהתפלגות "כמות התמונות לכל פציינט" תהיה זהה בין סט האימון לסט הולידציה כדי שלא בטעות נראה את אותו הפציינט הרבה פעמים כאן או כאן. כלומר: אם יש חולה עם הרבה תמונות בסט האימון, נרצה שיהיה חולה אחר עם הרבה תמונות (עד כמה שאפשר) בסט הולידציה.
וידוי נוגע ללב:
לאורך התחרות התחלנו לריב על סביבת העבודה, נאלצתי בלית ברירה לעשות מעשה שאני לא גאה בו.
כתבתי בפייטורצ'..
..מנוע פנימי חדש לFastAI עם תמיכה בקרס.
כי אחרת איך אוכל להשתמש בFastAI?!
האמת שאני זומם כבר הרבה זמן על הFastAI הזאת שכולם מדברים עליה אבל לצערי אני לא יכול להשתמש בה כי היא כתובה בסביבה הלא נכונה שמשתמשיה כמובן טועים.
לאורך כל התחרות זיגזגתי בין סביבות ומימשתי חלקים מFastAI בקרס. שמרתי על אינטרפייסים לרמה שהוספתי בתחילת חלק מהמחברות:
try: import keras_fastai as fastai
except: import fastai
וכשהמחברות של חברי לצוות הגיע אלי הרצתי אותן כמו שהן ובדקתי שהתוצאות יוצאות אותו הדבר עם מנוע הקרס.
אז עכשיו אחרי שחטאתי קצת אני אשמח לספר לכם שאין הבדל בביצועים של PyTorch לעומת TensorFlow. בתחילת התחרות, כדי שנשאר קומפטטיבים נאלצתי להעביר את קוד הפתרון של ספי ונתי משנה שעברה ל-TensorFlow ולהגיע לאותן התוצאות שהם קיבלו בPyTorch. בהתחלה לא הבנתי איך TF יכול להיות גרוע יותר, ואז הבנתי שההבדל היה חוסר הכישורים שלי ליישם אותו אלגוריתם אימון! לא הסביבה! [אני לא מחבב את טנזורפלו – למקרה שלא ציינתי זאת עדיין בפוסט הזה]
לאחר קצת דיבאגינג הגעתי כמעט לאותן תוצאות (הבדלים של 0.000001) כמו ב-PyTorch.
כולם אומרים ש- PyTorch עדיף למחקר, אני מאמין שזה נכון רק אם אתם לא מכירים (או לא מעוניינים להכיר) את הספריה שאתם עובדים איתה מבפנים.
אחרי חודשיים עם שניהם, אני לא חושב שיש הבדל אמיתי. מעברי הקוד היו קשים במיוחד בגלל שהייתי צריך ללמוד את tf 1.15 "בברזלים" [בלי קרס], את tf 2.2 [עם הפיצ'רים החדשים] את PyTorch כמעט מאפס וFastAI באותו זמן, אבל זה היה שווה את זה!
הדיונים על PyTorch לעומת Tensorflow היו מהנים איפשהו בשנת 2017. הייתה תחרות אמיתית והבדלים פילוסופיים כמו שהיו בזמנו בין ת'יאנו לטורצ' (המקורי).
עכשיו שניהם כמעט נראים זהים לחלוטין, הוויכוחים שטותיים ומשעממים..
במשך חודש שלם הצלחנו לאמן:
  • – שש EfficientNet-B6 ו EfficientNet-B4 עם Grouped KFold על מזהה הפציינט.
  • – שלוש EfficientNet-B0-7 (מזכיר: כולן ביחד Concatenated ברשת אחת בכל צעד GradientDescent)
  • – שמונה EfficientNet-B6 עם סטרטיפיקציה משולשת ומעליהן MetaClassifier אחד עם סטרטיפיקציה משולשת.
  • – עשרים טנק-נט (EfficientNet-L3) עם סטרטפיקציה משולשת. (בלי MetaClassifier).
כולן מאותחלות ממשקולות הקסם.
מעל הפרדיקציות של כל אלה, אימנו עוד כמה רשתות קטנות מהרשימה למעלה [Restacking עם התמונות והפיצ'רים הטבלאיים המקוריים]. התוצאות לא היו לרוחינו והן לא נכללו באנסמבל הסופי.
הימים האחרונים:
חמושים בחמישה טיטאן RTXים ו120 שעות TPU – פתחנו באש על הטבלה!
ספי העביר אלי אנסמבל מתחרות קודמת. הבעיה היחידה היתה שסיבוכיות האנסמבל היא מעריכית [מנסים את כל קומבינציות הפיצ'רים: n chooses k)=2^n)∑].
אימון מהקצה לקצה של הSeffiClassifier על הדאטה של התחרות לוקח 34 שנה.
עם ביטול הgil, טיפה מקבול, קצת קוד שעבר לGPU, החלפת Solverים לחלק מהאלגוריתמים של sklearn, קצת קוד ב@jit, וכוח מחשוב מטומטם לחלוטין הצלחנו להריץ את הSeffiClassifier בתשע שעות במקום 34 שנה.
איך?! הרחבנו את StackNet. הוספנו תמיכה במספר מכונות במקביל.

למתעניינים: בסוף בשביל להריץ השתמשתי בכוורת שרתי Blade. הפעם חשבון החשמל לא היה על גוגל..

באמצעות המערכת המבוזרת הצלחנו ביומיים האחרונים לאמן על הפרדיקציות של הרשתות ועל הפיצ'רים הטבלאיים כמה מאות אלפים של מודלים מטיפוסים רנדומלים מהרשימה: KNN, ExtraTrees, XgbRandomForest, LogisticRegressionCV, Keras_FullyConnected, Keras_EntityEmbeddings, Keras_TabNet, Catboost, Xgboost, LightGBM.
מתוך המודלים המאומנים נבחרו לאנסמבל הסופי 24,160 מודלים על פי קורלציה לתיוגים הם חולקו ל1208 אנסמבלים [20 חלוקות לכל אחד], אין לנו מידע על כמות המודלים המדוייקת שאימנו כי לא שמרנו לוגים.
בשלוש השעות האחרונות של התחרות פיצלנו כוחות:
הצוות עבד על שילוב אופטימלי של המודלים בזמן שאני תחזקתי את הקלאסטר כדי שיאמן כמה שיותר מודלים.
תוכנית ב': הוחלט שב02:00 – שעה לפני סגירה, במידה ולא יהיה אף אנסמבל מוכן "נשרוף הגשה" על חציון פרדיקציות של כל המודלים המוכנים. [בלי לחשוב בכלל – כדי למנוע מצבי "עוד רגע אני מסיים" ולפספס. קורה במצבים כאלה]
וכך היה, ב02:00 הגשנו את החציון ובנוסף ב02:30 כבר היה לנו אנסמבל.
תוצאת החציון נראתה לא מבטיחה: 0.9530 אבל תוצאת האנסמבל המתוחכם נראתה מבטיחה מאוד: 0.9640.
מוזר. על פניו החציון אמור היה להיות רובאסטי. בכל מקרה, דקות האחרונות לתחרות וכולנו סחוטים מעייפות בשעה כזו.
הלכנו על האנסמבל בגלל התוצאה שלו בטבלה!
וככה בדיוק הפסדנו את המדליה.

"כשהמקצוע שלך הוא לאמן מודלים ללמוד מהטעויות שלהם אבל אתה לא מחיל את אותו הרעיון בעולם האמיתי"

הצטרפו לערוץ הטלגרם שלנו!

כל ההודעות שאתם לא רוצים לפספס

X