ИИ Сапера — проблема с каким-то крайним случаем вывода знаний о безопасной ячейке

Я прохожу курс CS50 «Введение в искусственный интеллект с помощью Python», и мне он очень нравится. Когда я запускаю свой сценарий, кажется, что все работает хорошо, но программа проверки CS50 обнаруживает какой-то крайний случай, когда мое программное обеспечение, очевидно, не находит безопасную ячейку при выводе знаний. Вот спецификация CS50, ориентированная на ту часть, которая не прошла тест:

Спецификация

Завершите реализацию класса Sentence и класса MinesweeperAI в minesweeper.py.

  • В классе Sentence выполните реализации known_mines, known_safes, mark_mine и mark_safe.

  • Функция known_mines должна возвращать набор всех ячеек в self.cells, которые, как известно, являются шахтами.

  • Функция known_safes должна возвращать набор всех ячеек в self.cells, о которых известно, что они безопасны.

  • Функция mark_mine должна сначала проверить, является ли cell одной из ячеек, включенных в предложение.

    • Если cell находится в предложении, функция должна обновить предложение так, чтобы cell больше не было в предложении, но по-прежнему представляло логически правильное предложение, учитывая, что cell известно как мое.

    • Если cell нет в предложении, никаких действий не требуется.

  • Функция mark_safe должна сначала проверить, является ли cell одной из ячеек, включенных в предложение.

    • Если cell находится в предложении, функция должна обновить предложение так, чтобы cell больше не было в предложении, но по-прежнему представляло логически правильное предложение, учитывая, что cell известно как безопасное.

    • Если cell нет в предложении, никаких действий не требуется.

В классе MinesweeperAI выполните реализации add_knowledge, make_safe_move и make_random_move.

  • add_knowledge должен принять cell (представленный в виде кортежа (i, j)) и соответствующий ему count и обновить self.mines, self.safes, self.moves_made и self.knowledge любой новой информацией, которую может вывести ИИ, учитывая, что cell известно как безопасная ячейка с count минами соседний с ним.

    • Функция должна пометить cell как один из ходов, сделанных в игре.

    • Функция должна пометить cell как безопасную ячейку, а также обновить все предложения, содержащие cell.

    • Функция должна добавить новое предложение в базу знаний ИИ на основе значений cell и count, чтобы указать, что count из соседей cell — мои. Обязательно включите в предложение только те ячейки, состояние которых еще не определено.

    • Если на основании любого из предложений в self.knowledge новые ячейки можно пометить как безопасные или минные, то функция должна это сделать.

    • Если на основе любого из предложений в self.knowledge можно вывести новые предложения (используя метод подмножества, описанный в разделе «История вопроса»), то эти предложения также следует добавить в базу знаний.

    • Обратите внимание: каждый раз, когда вы вносите какие-либо изменения в знания вашего ИИ, возможно, можно будет сделать новые выводы, которые раньше были невозможны. Убедитесь, что эти новые выводы добавлены в базу знаний, если это возможно.

Вот мой код (большая часть задачи выполнена, так что если не хотите его испортить, не продолжайте):

import itertools
import random
import copy

"""
- cut code related to setting up a board of 8x8 cells with 8 mines spread around randomly in them.
"""


class Sentence:
    """
    Logical statement about a Minesweeper game
    A sentence consists of a set of board cells,
    and a count of the number of those cells which are mines.
    """

    def __init__(self, cells, count):
        self.cells = set(cells)
        self.count = count

    def __eq__(self, other):
        return self.cells == other.cells and self.count == other.count

    def __str__(self):
        return f"{self.cells} = {self.count}"

    def known_mines(self):
        """
        Returns the set of all cells in self.cells known to be mines.
        """
        if len(self.cells) == self.count != 0:
            return self.cells
        else:
            return set()

    def known_safes(self):
        """
        Returns the set of all cells in self.cells known to be safe.
        """
        if self.count == 0:
            return self.cells
        else:
            return set()

    def mark_mine(self, cell):
        """
        Updates internal knowledge representation given the fact that
        a cell is known to be a mine.
        """
        if cell in self.cells:
            self.cells.remove(cell)
            self.count -= 1
            return True
        return False

    def mark_safe(self, cell):
        """
        Updates internal knowledge representation given the fact that
        a cell is known to be safe.
        """
        if cell in self.cells:
            self.cells.remove(cell)
            return True
        return False


class MinesweeperAI:
    """
    Minesweeper game player
    """

    def __init__(self, height=8, width=8):

        # Set initial height and width
        self.height = height
        self.width = width

        # Keep track of which cells have been clicked on
        self.moves_made = set()

        # Keep track of cells known to be safe or mines
        self.mines = set()
        self.safes = set()

        # List of sentences about the game known to be true
        self.knowledge = []

    def mark_mine(self, cell):
        """
        Marks a cell as a mine, and updates all knowledge
        to mark that cell as a mine as well.
        """
        self.mines.add(cell)
        for sentence in self.knowledge:
            sentence.mark_mine(cell)

    def mark_safe(self, cell):
        """
        Marks a cell as safe, and updates all knowledge
        to mark that cell as safe as well.
        """
        self.safes.add(cell)
        for sentence in self.knowledge:
            sentence.mark_safe(cell)

    def nearby_cells(self, cell):
        """
        Returns set of cells around the given cell.
        """
        # Keep count of nearby mines
        cells = set()

        # Loop over all cells within one row and column
        for i in range(cell[0] - 1, cell[0] + 2):
            for j in range(cell[1] - 1, cell[1] + 2):

                # Ignore the cell itself
                if (i, j) == cell:
                    continue

                # Add cell to set if cell in bounds
                if 0 <= i < self.height and 0 <= j < self.width:
                    cells.add((i, j))

        return cells

    def add_sentence(self, cells, count):
        # Create new sentence based on the nearby cells and known mines and safe cells.
        newSentence = Sentence(cells, count)
        self.knowledge.append(newSentence)

        # Check new sentence for discoveries.
        for cell in copy.deepcopy(newSentence.known_safes()):
            if cell not in self.safes:
                self.mark_safe(cell)
        for cell in copy.deepcopy(newSentence.known_mines()):
            if cell not in self.mines:
                self.mark_mine(cell)

        # Remove empty sentences:
        for sentence in self.knowledge:
            if len(sentence.cells) == 0:
                self.knowledge.remove(sentence)

        # Add mines and safes from inferred sentences:
        for sentence in self.knowledge:
            if len(sentence.cells) == sentence.count:
                for cell in copy.deepcopy(sentence.cells):
                    self.mark_mine(cell)
                self.knowledge.remove(sentence)
                continue
            if sentence.count == 0:
                for cell in copy.deepcopy(sentence.cells):
                    self.mark_safe(cell)
                self.knowledge.remove(sentence)
                continue

        # Remove same sentences
        updatedKnowledge = []
        for sentence in self.knowledge:
            if sentence not in updatedKnowledge:
                updatedKnowledge.append(sentence)
        self.knowledge = updatedKnowledge

        # Infer knowledge based on new sentence
        if len(self.knowledge) > 1:
            for sentence in self.knowledge:
                if sentence != newSentence:
                    if sentence.cells.issubset(newSentence.cells):
                        inferredSet = Sentence(
                            newSentence.cells - sentence.cells,
                            newSentence.count - sentence.count,
                        )
                        if inferredSet not in self.knowledge:
                            self.add_sentence(
                                newSentence.cells - sentence.cells,
                                newSentence.count - sentence.count,
                            )
                    if newSentence.cells.issubset(sentence.cells):
                        inferredSet2 = Sentence(
                            sentence.cells - newSentence.cells,
                            sentence.count - newSentence.count,
                        )
                        if inferredSet2 not in self.knowledge:
                            self.add_sentence(
                                sentence.cells - newSentence.cells,
                                sentence.count - newSentence.count,
                            )

    def add_knowledge(self, cell, count):
        """
        Called when the Minesweeper board tells us, for a given
        safe cell, how many neighboring cells have mines in them.

        This function should:
            1) mark the cell as a move that has been made
            2) mark the cell as safe
            3) add a new sentence to the AI's knowledge base
               based on the value of `cell` and `count`
            4) mark any additional cells as safe or as mines
               if it can be concluded based on the AI's knowledge base
            5) add any new sentences to the AI's knowledge base
               if they can be inferred from existing knowledge
        """
        # Mark cell as the move made
        self.moves_made.add(cell)

        # Mark cell as safe
        self.mark_safe(cell)

        # Get nearby cells and substract known mines and safe cells
        NearbyCells = self.nearby_cells(cell)
        validNearbyCells = copy.deepcopy(NearbyCells)
        for cell in NearbyCells:
            if cell in self.safes:
                validNearbyCells.discard(cell)
            if cell in self.mines:
                validNearbyCells.discard(cell)
                count -= 1

        # Add new sentence and infer knowledge based on added sentence
        self.add_sentence(validNearbyCells, count)

После того, как я запустил свой скрипт через функцию проверки CS50, это вывод:

:) minesweeper.py exists
:) minesweeper.py imports
:) Sentence.known_mines returns mines when conclusions possible
:) Sentence.known_mines returns no mines when no conclusion possible
:) Sentence.known_safes returns mines when conclusion possible
:) Sentence.known_safes returns no mines when no conclusion possible
:) Sentence.mark_mine marks mine when cell in sentence
:) Sentence.mark_mine does not mark mine when cell not in sentence
:) Sentence.mark_safe marks safe when cell in sentence
:) Sentence.mark_safe does not mark safe when cell not in sentence
:) MinesweeperAI.add_knowledge marks cell as a move made
:) MinesweeperAI.add_knowledge marks cell as safe
:) MinesweeperAI.add_knowledge adds sentence in middle of board
:) MinesweeperAI.add_knowledge adds sentence in corner of board
:) MinesweeperAI.add_knowledge ignores known mines when adding new sentence
:) MinesweeperAI.add_knowledge ignores known safes when adding new sentence
:) MinesweeperAI.add_knowledge infers additional safe cells
:) MinesweeperAI.add_knowledge can infer mine when given new information
:) MinesweeperAI.add_knowledge can infer multiple mines when given new information
:( MinesweeperAI.add_knowledge can infer safe cells when given new information
did not find (0, 0) in safe cells when possible to conclude safe
:) MinesweeperAI.add_knowledge combines multiple sentences to draw conclusions

Помощь!

Я уже все перепробовал, чат GPT ничуть не помог, я попробовал pytest с test_minesweeper.py для модульного тестирования моего кода, и все казалось нормально! Во всех ситуациях, которые я добавляю к своим знаниям, код работает хорошо.

Пожалуйста, обрежьте свой код, чтобы облегчить поиск проблемы. Следуйте этим рекомендациям, чтобы создать минимально воспроизводимый пример.

Сергей Кох 06.04.2024 10:16

Все, что вам нужно сделать, это настроить отладчик и начать выполнять код построчно. Параллельно вручную вычисляйте, каким должен быть результат на каждом этапе, и в тот момент, когда он не совпадает, вы можете исследовать эту конкретную часть кода, чтобы понять, почему он дает результат, отличный от ручного расчета. Чатгпт — ужасная идея просить о помощи. Вам необходимо понимать и отлаживать свой код. Вам также следует провести модульные тесты, охватывающие крайние случаи, когда ваш код дает сбой.

Elerium115 06.04.2024 10:16

@СергейКох Я урезал код по твоему совету. Надеюсь, это поможет. Спасибо!

Maciej Zamojski 06.04.2024 13:29

@ Elerium115 Elerium115 Я уже протестировал его с помощью pytest, и он показал хорошие результаты, но, возможно, проблема в том, что я не знаю крайний случай, который охватывает тест CS50, поэтому я не смог найти никаких проблем в своем коде.

Maciej Zamojski 06.04.2024 13:29

Тогда вам следует сначала поработать над воспроизведением проблемы. Рассматривали ли вы случай отсутствия мин, всех мин, доски нулевого размера, доски 1xN, доски Nx1, мины, окруженной 8 минами... Это некоторые крайние случаи, которые стоит рассмотреть среди других.

Elerium115 06.04.2024 13:46

@ Elerium115 Elerium115 Размер платы фиксированный 8x8, поэтому такого быть не может. Я еще раз подумаю о возможных крайних случаях. Спасибо за ваш ответ.

Maciej Zamojski 06.04.2024 14:48
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
6
305
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вам нужно решить несколько проблем. Начну с самых основных. Просмотрите код метода add_sentence(), где вы удаляете пустые предложения и находите предложения с известными минами и сейфами. Вы можете протестировать этот фрагмент. Запустите его и проверьте knowledge после каждого цикла for, чтобы увидеть проблемы:

from minesweeper import Sentence
  
knowledge = []
safes = set()
mines = set()
      
s = Sentence(set(), 0)
knowledge.append(s)
s = Sentence(set(), 0)
knowledge.append(s)
s = Sentence(set([(1,1),(2,2),(3,3)]), 3)
knowledge.append(s)
s = Sentence(set([(0,0),(0,1),(0,2)]), 3)
knowledge.append(s)
s = Sentence(set([(1,0),(2,0),(3,0)]), 0)
knowledge.append(s)    
s = Sentence(set([(2,1),(1,2),(1,3),(3,2)]), 0)
knowledge.append(s) 

Совет для цикла «мины/сейфы»: вам не нужны операторы continue, если вы используете if/elif.

После того, как вы исправите этот сегмент, вам придется исправить еще больше вещей. Предположим, у вас есть эти 2 sentences в knowledge (в таком порядке):

knowledge = []
s = Sentence(set([(0,0),(0,1),(1,0),(2,0)]), 2)
knowledge.append(s)
s = Sentence(set([(0,0),(0,1),(0,2)]), 3)
knowledge.append(s)

После того, как вы найдете мины в (0,0),(0,1),(0,2), вы должны обнаружить мины в (1,0),(2,0). Однако одним звонком на add_sentence() его не поймаешь.

Наконец, ваш последний шаг (вывод знаний) требует доработки. У меня есть сообщение на форуме CS50AI ED, которое демонстрирует это. Используйте эту ссылку: Как отладить сапер

Это было очень полезно! Мне удалось исправить свой код. Сначала я проверил, что добавление еще одного алгоритма вывода и дедукции после добавления предложения имело значение, и проверка50 прошла тест. Раньше я надеялся добиться этого с помощью рекурсии в add_sentence(), но это не сработало. Я проведу рефакторинг своего кода, чтобы он, надеюсь, выглядел чище, и опубликую его в качестве ответа. Спасибо, как только вы получите, вы очень помогли!

Maciej Zamojski 06.04.2024 19:33

Благодаря @kcw78 мне удалось найти решение. Я полностью переработал код. Моя ошибка заключалась в том, что база знаний не прошла через логические выводы и маркировку мин и сейфов после последнего изменения в базе знаний. Теперь я позаботился о том, чтобы каждый раз, когда знания обновлялись, они проходили один и тот же процесс маркировки мин и сейфов и рекурсивного вывода.

Вот как сейчас выглядит моя функция add_knowledge(cell, count):

def add_knowledge(self, cell, count):
    """
    Called when the Minesweeper board tells us, for a given
    safe cell, how many neighboring cells have mines in them.

    This function should:
        1) mark the cell as a move that has been made
        2) mark the cell as safe
        3) add a new sentence to the AI's knowledge base
           based on the value of `cell` and `count`
        4) mark any additional cells as safe or as mines
           if it can be concluded based on the AI's knowledge base
        5) add any new sentences to the AI's knowledge base
           if they can be inferred from existing knowledge
    """
    # Mark cell as the move made
    self.moves_made.add(cell)

    # Mark cell as safe
    self.mark_safe(cell)

    # Get nearby cells and substract known mines and safe cells
    NearbyCells = self.nearby_cells(cell)
    validNearbyCells = copy.deepcopy(NearbyCells)
    for cell in NearbyCells:
        if cell in self.safes:
            validNearbyCells.discard(cell)
        elif cell in self.mines:
            validNearbyCells.discard(cell)
            count -= 1

    # Add new sentence
    self.add_sentence(validNearbyCells, count)

    # Mark cells and infer knowledge every time knowledge is changed
    knowledgeChanged = True

    while knowledgeChanged:
        knowledgeChanged = False
        # Mark safe cells and mines
        if self.mark_cells():
            knowledgeChanged = True
        # Infer new knowledge
        if self.infer_knowledge():
            knowledgeChanged = True

Функции mark_cells() и infer_knowledge() возвращают значение True, если они изменяют КБ и снова запускают цикл while.

Другие вопросы по теме