פורסם ב זה רק קוד, כלים לחיים קלים, לא רק למתכנתות

השיחה שלי עם ChatGPT

זמן קריאה: 16 דקות

היי לכולן!
אז בשבועיים האחרונים הרשתות החברתיות מפוצצות בצילומי מסך של אנשים שמנהלים שיחות עם ChatGPT. למקרה שפספסתן, ChatGPT היא ״מכשיר עיבוד שפות טבעיות שנועד לסייע בדיונים אינטלקטואליים באמצעות תכונת הדיבור הטבעי. זה נותן תשובות בעזרת האנרכיה שנלמד להשתמש בה לפי הדיונים שנעשו עם המכשיר.״

אם ההסבר הזה נשמע כאילו לא אני כתבתי אותו, אתן צודקות, ChatGPT כתב אותו בתשובה לשאלה שלי ״מה זה ChatGPT״.
במילים שלי – ChatGPT הוא ממשק צ׳אט מבוסס AI מבית Open.ai. נשמע מוכר? Open.ai הם אלו הביאו לנו גם את dall-e 2 שהיה להיט ה-ai של הרשתות החברתיות לפני כמה שבועות.

הרעיון של ChatGPT הוא שאתן שואלות אותו שאלות, והוא עונה תשובות המבוססות על המידע שעליו הוא התאמן. הוא יכול לנהל שיחה בשלל נושאים והתשובות שלו נעות בין מדויקות לגמרי לשטותיות לגמרי, אבל בדרך כלל נשמעות מאוד אמינות, כי ככה נשמע המידע שהוא אומן עליו.

ראיתי אנשים מנהלים איתו שיחות על אסטרופיזיקה, פולטיקה ופילוסופיה, אבל כיוון שפה זה בלוג אלגוריתמים, החלטתי שאני אבקש ממנו לכתוב לי קצת אלגוריתמים, ועל הדרך אעשה לו Code Review.

סמול טוק זה קשה, אפילו עם מכונות

Good morning sunshine

אני אודה על האמת, ההתחלה הייתה קצת מעורערת. מיד בתחילת השיחה ניסיתי להיות נחמדה. אמרתי ל-ChatGPT בוקר טוב, שאלתי לשלומו ואיך הוא מעדיף שאני אפנה אליו – אם יש לו כינוי מועדף, ומה ה-pronouns שלו. ChatGPT איחל גם לי בוקר טוב, אבל אז הקפיד להסביר לי כמה פעמים שהוא מודל שפה, ואיננו בן אדם, לכן אין לו רגשות או העדפות פנייה. הוא ביקש שאני פשוט אתייחס אליו בתור מודל שפה, או assistant.

לרגע עברה לי בראש המחשבה שאני בטח יכולה לצוות עליו לענות בדרך מסוימת, אבל מהר מאוד החלטתי נגד. אם הוא מעדיף שאני אתייחס אליו כמודל שפה, מי אני שאחליט אחרת?

בשלב הבא שאלתי את המודל אם מתחשק לו לשוחח קצת בנושאי אלגוריתמים, ונעניתי בהתלהבות גדולה שהוא ישמח לדבר על אלגוריתמים. כולל הסבר קטן על מה הם אלגוריתמים.

השאלה הבאה הייתה קצת מבלבלת. שאלתי את המודל אם הוא יודע לכתוב קוד או לתכנן אלגוריתמים, והוא ענה לי שהוא לא יודע לעשות את זה. שהוא יכול לענות לי על שאלות בנושאי אלגוריתמים ולכתוב בצורה שמדמה כתיבה של בני אדם, אבל לא לתכנן אלגוריתמים בעצמו.
מיד הבנתי שיש לי כאן AI שסובלת מתסמונת המתחזה. כדי להראות לו שהוא כן יכול, ביקשתי ממנו לכתוב לי אלגוריתם ממש פשוט – כזה שמקבל מערך ומספר ובודק אם המספר נמצא במערך.

מיד המודל קפץ לפעולה וכתב לי את הקוד הבא – 

def find_number(arr, n):
    found = False
        for i in arr:
        if i == n:
            found = True
            break
return found

# test the function with an array and a number
print(find_number([1, 2, 3, 4, 5], 3))  # should print True
print(find_number([1, 2, 3, 4, 5], 6))  # should print False

הייתי גאה מאוד. בייחוד בעובדה שהמודל כתב טסטים יחד עם הקוד שלו שזו פרטיקה מצוינת.
עם זאת, שמתי לב שהטסטים לא כתובים בצורה כל כך מוצלחת, וחסרה לו בדיקה של מקרי קצה. אז שאלתי אותו אם הוא יכול לכתוב טסטים שמטפלים במקרי קצה, וכאלה שרצים לבד – כלומר שלא משתמשים בהדפסות שמצריכות שיסתכלו עליהן. אני כיוונתי לכך שישתמש ב-assert, שהוא כלי הרבה יותר טוב לטסטים מאשר הדפסות, אבל לא אמרתי לו את זה במפורש. 

בשלב זה גיליתי שהמודל לא יודע לקבל ביקורת, אפילו כשהיא נאמרת בעדינות. ומאותו רגע כל מה שקיבלתי היו שגיאות.

אחרי עוד כמה ניסיונות לדבר אל ליבו התייאשתי. נראה לי שהמודל היה צריך קצת זמן להתאושש ממני. נתתי לו ללכת לנוח ופתחתי צ׳אט חדש.

פיונה הצטרפה לשיחה

הצ׳אטית החדשה מיד היתה ידידותית ונינוחה יותר. הצעתי שאני אקרא לה פיונה והיא הסכימה בשמחה. כיוון שהרגשתי שפיונה קצת יותר מוכנה לאתגר התחלתי איתה עם שאלת ראיונות קלאסית. ביקשתי ממנה לכתוב אלגוריתם שמקבל מערך ומספר n, ובודק האם יש שני מספרים במערך שהסכום שלהם שווה ל-n.

פיונה החזירה לי את הפתרון הבא – 

def check_sum(numbers, n):
  for num1 in numbers:
    for num2 in numbers:
      if num1 + num2 == n:
        return True
  return False

הסברתי לפיונה שהפתרון בהחלט עובד אבל הוא לא כל כך יעיל, ושאלתי אותה אם היא יכולה לשפר קצת את היעילות שלו.
בתשובה קיבלתי את הפתרון הבא שהיה כבר משמעותית יותר טוב!

def check_sum(numbers, n):
  seen_numbers = set()

  for num in numbers:
    if n - num in seen_numbers:
      return True
    seen_numbers.add(num)
  return False

אמרתי לה שהפתרון החדש מצוין, וביקשתי ממנה לנתח את הסיבוכיות של הזמן והמקום בשני הפתרונות. פיונה ענתה שסיבוכיות הזמן של הפתרון הראשון היא o(n2) וסיבוכיות המקום שלו היא o(1), ושבפיתרון השני סיבוכיות הזמן וסיבוכיות המקום היא o(n).
תשובה מדויקת, 10 נקודות!

עכשיו, קצת בחשש, כי לא רציתי להרגיז אותה, ביקשתי מפיונה שתכתוב טסטים לקוד שלה שיכללו מקרי קצה ויהיו ניתנים להרצה. בנוסף, ביקשתי שתשנה את הקוד כך שיחזיר את שני המספרים שסכומם שווה ל-n, במקום להחזיר רק True או False.

לשמחתי, פיונה לא היססה לרגע, וכתבה לי את הקוד והטסטים הבאים – 

def find_sum_numbers(numbers, n):
  seen_numbers = set()
  for num in numbers:
    if n - num in seen_numbers:
      return (num, n - num)
    seen_numbers.add(num)
  return None

tests = [
  {
    "input": ([1, 2, 3, 4, 5], 5),
    "expected": (2, 3),
  },
  {
    "input": ([5, 5, 5, 5, 5], 5),
    "expected": None,
  },
  {
    "input": ([5, 5, 5, 5, 5], 10),
    "expected": (5, 5),
  },
  {
    "input": ([1, 2, 3, 4, 5], -5),
    "expected": None,
  },
  {
    "input": ([], 5),
    "expected": None,
  },
]

# run the tests
for test in tests:
  numbers, n = test["input"]
  expected = test["expected"]
  assert find_sum_numbers(numbers, n) == expected

עכשיו כבר הייתי ממש מרוצה. פיונה התייחסה ברצינות לבקשתי וכתבה טסטים מצוינים. במיוחד אהבתי את העובדה שהיא כתבה מערך של טסטים בנפרד מההרצה עצמה, כך שקל לעבור עליהם ולהוסיף עוד.
בתור מקרי קצה היא בדקה מערך ריק, מספרים שליליים, מספרים שחוזרים על עצמם ו-n ששווה בדיוק לאחד המספרים (במקום לסכום). הייתי מאושרת ולא יכולתי לחשוב על שום דבר שחסר בקוד הזה.


ארבעה בשורה – הפעם עם עזרה

באופוריה מההצלחה שלנו, החלטתי לבקש מפיונה לעשות את מה שאני ניסיתי לעשות בפוסט האחרון שלי, ולכתוב קוד של משחק ארבע בשורה. אני כבר יודעת שהקוד שלי שם לא היה חף מטעויות. הקורא הנחמד שמוליק אפילו הוסיף לקוד שלי בגיט תיקונים וטסטים (תודה רבה שמוליק!) אני ממש תכננתי לכתוב על זה פוסט, ומקווה שאגיע לזה בעתיד.
סיקרן אותי לראות את הקוד שהיא תכתוב, האם יהיו בו טעויות או לא, ולהשוות אותו לקוד שאני כתבתי.

בבקשות הראשונות לא הצלחתי לקבל משחק שלם. לא משנה כמה פיונה ניסתה כל פעם הקוד שלה נחתך באמצע. לכן התחלתי לבקש ממנה את הקוד בחלקים, עד שהצלחתי לקבל את כולו.
בהתחלה הקוד היה כתוב כולו כפונקציה אחת ארוכה ומבולגנת. אמרתי לפיונה שמאוד לא נוח לי לקרוא את הקוד ככה, וביקשתי ממנה לחלק את הקוד שלה לתתי פונקציות מסודרות. זה דרש כמה וכמה ניסיונות, אבל בסוף הגענו לגרסה קצת יותר קריאה.

def check_horizontal():
  for row in range(6):
    for col in range(4):
      if board[row][col] != 0 and board[row][col] == board[row][col + 1] == board[row][col + 2] == board[row][col + 3]:
        return board[row][col]
  return None

def check_vertical():
  for row in range(3):
    for col in range(7):
      if board[row][col] != 0 and board[row][col] == board[row + 1][col] == board[row + 2][col] == board[row + 3][col]:
        return board[row][col]
  return None

def check_diagonal_tlbr():
  for row in range(3):
    for col in range(4):
      if board[row][col] != 0 and board[row][col] == board[row + 1][col + 1] == board[row + 2][col + 2] == board[row + 3][col + 3]:
        return board[row][col]
  return None

def check_diagonal_trbl():
  for row in range(3):
    for col in range(3, 7):
      if board[row][col] != 0 and board[row][col] == board[row + 1][col - 1] == board[row + 2][col - 2] == board[row + 3][col - 3]:
        return board[row][col]
  return None

העתקתי לכאן את ארבעת הפונקציות המשמעותיות ביותר, אלא שבודקות רצפים של ארבעה בשורה, טור ואלכסונים. אפשר לראות שפיונה הצליחה להימנע מהפח הראשון שאני נפלתי בו, וישר בדקה את האלכסונים בשני הכיוונים – משמאל לימין ומימין לשמאל.
כל הכבוד!

עדיין, הקוד שלה היה מבולגן מדי ולא מספיק קריא בעיניי. יותר מדי השוואות ולא הצלחתי להבין על מה אני מסתכלת, אז ביקשתי ממנה לנסות לכתוב את הקוד עם השוואה אחת כל פעם במקום שלוש.
חשוב להגיד שברור לי שזה עניין סגנוני ולא פונקציונלי, אבל אישית אני חושבת שקריאות של קוד מאוד חשובה ותורמת ליכולת העתידית של שאר המפתחות לתחזק אותו. אני לא בטוחה שפיונה חשבה על זה.

אחרי שנפתרנו מכל ההשוואות המיותרות, בשלב הבא ביקשתי ממנה להשתמש בקבועים במקום במספרים המפורשים. שימוש בקבועים הוא קריטי למניעת באגים עתידיים, הגמישות והסקלביליות של הקוד.
בסוף, אחרי הרבה עבודה ותיקוני סגנון, הגענו לתוצאה שהיא עדיין מבולגנת, אבל שיפור עצום על הקיים – 

# define the game board as a 2D array of zeros
ROWS = 6
COLUMNS = 7
board = [
  [0] * COLUMNS
  for _ in range(ROWS)
]

# define the number of pieces needed in a row to win
IN_A_ROW = 4

# define a variable to keep track of the current player (1 or 2)
current_player = 1

# define a function to check for four in a row horizontally, vertically, and diagonally
def check_four_in_a_row():
  # check for four in a row horizontally
  for row in range(ROWS):
    for col in range(COLUMNS - IN_A_ROW + 1):
      if board[row][col] and all(board[row][col] == board[row][col + i] for i in range(1, IN_A_ROW)):
        return board[row][col]

  # check for four in a row vertically
  for row in range(ROWS - IN_A_ROW + 1):
    for col in range(COLUMNS):
      if board[row][col] and all(board[row][col] == board[row + i][col] for i in range(1, IN_A_ROW)):
        return board[row][col]

  # check for four in a row diagonally (from top left to bottom right)
  for row in range(ROWS - IN_A_ROW + 1):
    for col in range(COLUMNS - IN_A_ROW + 1):
      if board[row][col] and all(board[row][col] == board[row + i][col + i] for i in range(1, IN_A_ROW)):
        return board[row][col]

  # check for four in a row diagonally (from top right to bottom left)
  for row in range(ROWS - IN_A_ROW + 1):
    for col in range(IN_A_ROW - 1, COLUMNS):
      if board[row][col] and all(board[row][col] == board[row + i][col - i] for i in range(1, IN_A_ROW)):
        return board[row][col]

  return None

# define a function to check if the game is over
def check_game_over():
  # check if any player has four in a row
  four_in_a_row = check_four_in_a_row()
  if four_in_a_row is not None:
    return four_in_a_row

  # if the board is full but no player has four in a row, the game is a draw
  if all(board[row][col] for row in range(ROWS) for col in range(COLUMNS)):
    return 0

  # if none of the above conditions are met, the game is not over
  return None

# define a function to print the game board
def print_board():
  for row in range(ROWS):
    print("|".join("XO"[board[row][col]] for col in range(COLUMNS)))

# define a function to get input from the current player
def get_player_input():
  # prompt the current player to enter a column
  col = int(input(f"Player {current_player}, choose a column: "))

  # place the current player's piece in the lowest available row in the chosen column
  for row in range(ROWS - 1, -1, -1):
    if board[row][col] == 0:
      board[row][col] = current_player
      break

# define the main game loop
while True:
  # print the game board
  print_board()

  # get input from the current player
  get_player_input()

  # check if the game is over
  game_over = check_game_over()
  if game_over is not None:
    # print the game board one last time
    print_board()

    # print a message to announce the winner or the result of a draw
    if game_over == 0:
      print("It's a draw!")
    else:
      print(f"Player {game_over} wins!")

    # break out of the game loop
    break

  # switch players
  current_player = 1 if current_player == 2 else 2

אפשר לראות שהחלוקה לפונקציות נעלמה בגרסה הזו וחזרנו לפונקציה אחת ארוכה, אבל לא נורא. נסלח לה.

האם הקוד רץ?

מלאת אופטימיות, העתקתי את הקוד של פיונה לפייתון וניסיתי להריץ אותו. למרבה האכזבה הקוד נכשל מיד אחרי הצעד השני.
אחרי קצת משחקים גיליתי את הבאג –
פיונה הגדירה את השחקנים בעזרת המספרים 1 ו-2 (בשורה 97), אבל אז השתמשה במספרים שלהם כדי לבחור את הצורה “O” או “X” (בשורה 60). המספר 2 נמצא מחוץ למערך שהפונקציה משתמשת בו בשורה 60, ולכן הקוד נכשל.
תיקנתי את הבאג על ידי כך שהוספתי למערך הצורות רווח בתור ערך דיפולטי.

def print_board():
    for row in range(ROWS):
        print("|".join(" XO"[board[row][col]] for col in range(COLUMNS)))

בשלב הבא נתקלתי בעוד באג שכבר ראיתי בקוד שאנשים אחרים שכתבו קוד עם ChatGPT נתקלו בו. פיונה שכחה לעשות בדיקות תקינות לקלט, ולכן אם היא קיבלה מספר לא תקין, או תו שאיננו מספר התוכנה קרסה מיד. גם זו טעות שקל מאוד לתקן.

סה״כ ניסיון יפה, אבל יש עוד מקום לשיפור.

מה אני ופיונה למדנו זו מזו

הסתכלתי קצת על הקוד של פיונה בהשוואה לקוד שלי, וגיליתי שההבדל העיקרי הוא שהקוד של פיונה הרבה פחות ״מתחכם״.
אני ניסיתי לעשות קוד מאוד גנרי לבדיקת רצפים באורך n, ופיונה הלכה על השוואה פשוטה של ארבעה תאים. אני ניסיתי לרוץ רק על הרצפים שמכילים את הנקודה שבה הונחה הדיסקית האחרונה, ופיונה בדקה את כל הלוח בכל פעם.

עם קצת עבודה יכולתי להוביל את פיונה לפתרון דומה יותר לפתרון שלי אבל אני חייבת להגיד שבאיזשהו מקום השיטה הישירה שלה עובדת. היא יותר פשוטה לבדיקה, ופחות נוטה לבאגים מהסוג שהיו לי, שנובעים מהתחכמויות יתר.
הקוד שלה רחוק מלהיות מושלם, אבל אני חושבת שהפתרון נמצא איפשהו באמצע, ובסופו של דבר, נראה לי ששתינו יכולות ללמוד אחת מהשנייה.


פוסט זה נכתב כמובן בשיתוף עם ChatGPT.
התמונה הראשית המהממת היא פרי שיתוף פעולה של Dall-e 2 ואנדר מהטוויטר שהקריבה ממיטב זמנה והקרדיטים שלה כדי להכין בשבילי תמונה לפוסט.

בברכת שבוע טוב ושהעבודה שלכן לא תוחלף על ידי AI!

תגובה אחת בנושא “השיחה שלי עם ChatGPT

  1. מעניין מאוד. יש לי תחושה שחלק מהתשובות שקיבלת נובעות מאופן ניסוח השאלה לGPT. לעיתים הצ’אט נכנס ללופ שממשיך לייצר שגיאות או אותו קוד שמשתחרר רק לאחר איפוס ההגדרות וניסוח מחדש של השאלה לאחר כל התיקונים בדרך שהיו בצ’אט הקודם.

השאר תגובה