צייר לי 4 – חלק ראשון המרחב הלטנטי, הצייר ודרווין

מי לא שמע על ההתפתחויות האחרונות באומנות ממוחשבת, ואני לא מדבר על NFT אלא על אלגוריתמים מגניבים שמייצרים תוכן מהמם מאינפוט של טקסט בלבד (ממליץ להסתכל על הטוויטר של אבוק-דוס שעושה קסמים עם DALL-E2). כך שהחלטתי גם לצלול לעולם ובהשפעת הפרויקט המגניב של מתי מריאנסקי ואיל גרוס בחרתי ללמד אלגוריתם לצייר את הספרה 4.
המשימה התבררה כמורכבת יותר משחשבתי והחזירה אותי אחריי כמה שנים שלא יצא לי לנגוע בלמידת מכונה חזרה אל המרחב הלטנטי המקום בו הכל יכול להיות מיוצג, אז למה אנחנו מחכים בואו ונתחיל.
איך לצייר את הספרה 4 בעזרת רשת נוירונים
בשביל האתגר הבא אני צריך שלושה אבני בניין בסיסיים:
- אלגוריתם אבולוציוני – אלגוריתם המקבל וקטור כקלט שלו והוא מחזיר חזרה וקטור דומה לוקטור הקלט עם כמה מוטציות קטנות.
- אלגוריתם ציור – מקבל כקלט וקטור והוא הופך אותו לתמונה.
- אלגוריתם זיהוי – האלגוריתם מקבל כקלט תמונה ופלט כמה התמונה דומה ל 4 (סוג של OCR). כך שבעצם יש לנו אלגוריתם שאומר לנו חם-קר על כל תמונה שאנחנו מכניסים לו. אלגוריתם זה משמש כפונקציית המטרה שלנו.
כעת נבנה אלגוריתם אפשר לשלב בין האלגוריתם באופן פשוט על מנת לצייר את הספרה 4:
שלב ראשון: נבחר וקטור התחלתי אקראי.
שלב שני: נייצר 10 מוטציות שונות של וקטור (בעזרת אלגוריתם #1).
שלב שלישי: נהפוך כל מוטציה (וקטור) לציור (אלגוריתם #2).
שלב רביעי: עבור כל ציור נבדוק עד כמה הוא דומה לספרה 4 (בעזרת אלגוריתם #3).
שלב חמישי: נבחר את הווקטור שהכי דומה לספרה 4. אם הוא 4 במעל 99% אז נעצור אחרת נחזור לשלב 2 עם הוקטור החדש.
בקוד של python זה נראה ככה:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
VECTOR_SIZE = 12 # 3 lines since evrey line represnt by 4 numbers MAX_ITERATIONS = 300 def main(): best_vector = tuple(np.random.randint(0, 100, VECTOR_SIZE)) itr = 0 print(itr, best_vector) while True: itr += 1 best_score = -1 for vector in make_x_mutation(best_vector, 0, 10): draw("tmp_image", vector) score = evluate_png("tmp_image.png") # print(score, best_score) if score > best_score: # print("==>",score, best_score) best_vector = vector best_score = score print(itr, best_vector, best_score) if best_score > 0.99: break if itr > MAX_ITERATIONS: break draw("best_image", best_vector) return best_vector, best_score |
כעת לאחר שראינו את האלגוריתם ממעוף הציפור נתחיל לצלול ליסודות ולפרטים:
וקטורים בכל מקום וקטורים
בראשית היה סקלר – מספר בודד שאין לו כיוון והוא מייצג רק את עצמו ויאמר אלוהים יהי וקטור ויהי וקטור עם גודל וכיוון.
אייזיק ניוטון – זכרונות שהמצאתי
אז למה הווקטורים כל כך חשובים בלמידת מכונה? בו נחשוב לרגע על תמונה דיגיטלית קטנה, לצורך הפשטות נבחר שהיא תמונה של שחור ולבן בגודל של 10×10 פיקסלים. מה יהיה גודל התמונה בזיכרון (ללא דחיסה)? בהנחה שכל פיקסל יכול להיות או שחור או לבן אזי אנחנו צריכים 100 ביטים של מידע בשביל לייצג את התמונה. ובמרחב של כל התמונות האפשריות אזי יש לנו 2 בחזקת 100 אפשרויות שונות של תמונות.
כעת נניח שאני מעוניין רק בציורים שעשויים מקו אחד בלבד (בעובי קבוע) כיצד אפשר לייצג את זה באופן יעיל? לצורך כך אפשר להשתמש באלגברה לינארית נניח במקום לכתוב מטריצה שלימה של פיקסלים שמייצגת תמונה עם קו מצד אחד לצד השני – אני יכול רק לכתוב את נקודת ההתחלה של הקו ואת נקודת הסיום ולהעביר קו באמצע כך שהקו מיוצג כ: (0,0,10,1). במידה ונבחר שכל קואורדינטה מיוצגת במספר טבעי אזי המרחב של כל האפשרויות לצייר קו או נקודה (במקרים שהקו מתחיל ונגמר באותו המקום) יהיה 10^4 בלבד. בנוסף לכך בגלל שאנחנו מציירים באופן וקטורי אזי אנחנו יכולים להגדיל את הציור ללא תלות במספר הפיקסלים הסופי שאותו אנחנו מייצגים ממליץ להעמיק בערך הויקיפדיה על גרפיקה וקטורית. כמו כן על וקטור בודד הרבה יותר קל לעשות מניפולציות כמו לשנות נקודה ועדיין לקבל קו ישר שמתחיל במקום אחר או לקצר ולהאריך את הקו.
אלגוריתם ציור
בחרתי לממש אלגוריתם ציור פשוט בשפת python שמקבל וקטור המורכב מרביעיות המייצגות נקודת התחלה וסיום (ייצוג מקביל יכול להיות נקודה אחת + זווית + אורך כמו הצגה פולרית של מספר מורכב) ומצייר אותו בצבע שחור על גביי קנבס לבן. את הציור עצמו שמרתי בשני אופנים בPNG – כתמונה ברזולוציה נמוכה של 28×28 פיקסלים בשביל האלגוריתם של הOCR (הרזולוציה הנמוכה נבחרה בגלל שהיא משמרת מספיק מידע וככה הכל רץ הרבה יותר מהר שכן לא צריך להכפיל מטריצות גדולות במודלים) ובSVG בשביל שאני יוכל להציג את התוצאה באיזה גודל שאני מעוניין.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import cairo SIZE = 4 PIXEL_SIZE = 28 def draw_vector(context, vector): x1, y1, x2, y2 = vector context.set_line_width(0.05) context.move_to(x1/100, y1/100) context.line_to(x2/100, y2/100) context.stroke() def draw(name, vectors): with cairo.SVGSurface(f"{name}.svg", PIXEL_SIZE, PIXEL_SIZE) as surface: context = cairo.Context(surface) context.set_source_rgb(1, 1, 1) context.paint() context.set_source_rgb(0, 0, 0) context.scale(PIXEL_SIZE, PIXEL_SIZE) i = 0 while i < len(vectors): draw_vector(context, vectors[i:i+SIZE]) i += SIZE surface.write_to_png(f"{name}.png") draw("X", (15, 15, 85, 85, 15, 85, 85, 15)) |
כך תוך שימוש בספריית cairo ושתי פונקציות פשוטות יצרתי תוכנת ציור פשוטה שיכולה לצייר כמה קווים ישרים שצריך.
אלגוריתם אבולוציוני
המטרה של האלגוריתם הזה היא להכניס שינויים קטנים ואקראיים לוקטור של הקלט כך שאם יש לנו מזל נקבל גרסה שעונה טוב יותר על פונקציית המטרה שלנו.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import math import numpy as np def mutate(vector, prev_score): chance_of_change = max(0.1, math.sqrt(1 - prev_score)/2) value_of_change = int(max(12 * (1-prev_score) ** 1.3, 2)) new_vec = [] for v in vector: new_value = v if np.random.rand() < chance_of_change: new_value += np.random.randint(-value_of_change, value_of_change) new_value = new_value % 100 new_vec.append(new_value) return tuple(new_vec) print(mutate((15, 15, 85, 85, 15, 85, 85, 15), 0)) # => (18, 15, 89, 77, 22, 73, 91, 15) print(mutate((15, 15, 85, 85, 15, 85, 85, 15), 0.5)) # => (12, 15, 85, 85, 15, 81, 83, 14) print(mutate((15, 15, 85, 85, 15, 85, 85, 15), 1)) # => (15, 15, 85, 84, 15, 85, 85, 15) |
האלגוריתם עובר על וקטור ועבור כל ערך מחליט האם לשנות אותו – ובמידה והוא משנה אותו אזי בכמה הוא משנה אותו. הוספתי ארגומנט המשפיע על ה״טמפרטורה״ של המוטציה כך שכאשר הזיהוי רחוק מפונקציית המטרה אנחנו מאפשרים יותר מוטציות ובתחום יותר גדול לעומת זאת כאשר אנחנו קרובים למטרה אזי המוטציות קטנות. (למה בחרתי דווקא את הערכים האלא בשורות המודגשות? לא סתם אומרים שלמידת מכונה זה יותר אומנות מאשר מדע מדויק…)
בשלב הבא אנחנו נכתוב פונקציה שמייצרת X מוטציות שונות (כך שנוודא שלא יוצא לנו פעמיים את אותו וקטור)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def make_x_mutation(vector, prev_score, x): new_vectors = set() new_vectors.add(vector) while len(new_vectors) < x: new_vectors.add(mutate(vector, prev_score)) new_vectors.remove(vector) return list(new_vectors) for vector in make_x_mutation((15, 15, 85, 85, 15, 85, 85, 15), 0.25, 10): print(vector) # => # (15, 15, 85, 89, 12, 85, 85, 15) # (11, 15, 91, 79, 15, 85, 85, 15) # (15, 16, 79, 85, 15, 83, 85, 18) # (22, 15, 92, 85, 15, 83, 81, 15) # (15, 15, 86, 77, 20, 85, 81, 15) # (11, 15, 85, 85, 15, 85, 87, 15) # (15, 15, 85, 85, 19, 85, 85, 15) # (15, 15, 88, 83, 18, 85, 85, 15) # (15, 12, 85, 85, 15, 85, 85, 18) |
Stay Tuned
בשבוע הבא אכנס לעומק כיצד בונים את פונקציית המטרה (אלגוריתם למידת המכונה שיודע לזהות האם קלט של תמונה הוא הספרה 4 או לא) ואראה כל מיני בעיות מעניינות עם הפלט ואיך התמודדתי עם זה. הירשמו לקבלת עדכונים!
ובנתיים הנה כמה דוגמאות לספרה 4 שהאלגוריתם יצר:

כתיבת תגובה