Я прохожу курс 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 для модульного тестирования моего кода, и все казалось нормально! Во всех ситуациях, которые я добавляю к своим знаниям, код работает хорошо.
Все, что вам нужно сделать, это настроить отладчик и начать выполнять код построчно. Параллельно вручную вычисляйте, каким должен быть результат на каждом этапе, и в тот момент, когда он не совпадает, вы можете исследовать эту конкретную часть кода, чтобы понять, почему он дает результат, отличный от ручного расчета. Чатгпт — ужасная идея просить о помощи. Вам необходимо понимать и отлаживать свой код. Вам также следует провести модульные тесты, охватывающие крайние случаи, когда ваш код дает сбой.
@СергейКох Я урезал код по твоему совету. Надеюсь, это поможет. Спасибо!
@ Elerium115 Elerium115 Я уже протестировал его с помощью pytest, и он показал хорошие результаты, но, возможно, проблема в том, что я не знаю крайний случай, который охватывает тест CS50, поэтому я не смог найти никаких проблем в своем коде.
Тогда вам следует сначала поработать над воспроизведением проблемы. Рассматривали ли вы случай отсутствия мин, всех мин, доски нулевого размера, доски 1xN, доски Nx1, мины, окруженной 8 минами... Это некоторые крайние случаи, которые стоит рассмотреть среди других.
@ Elerium115 Elerium115 Размер платы фиксированный 8x8, поэтому такого быть не может. Я еще раз подумаю о возможных крайних случаях. Спасибо за ваш ответ.
Вам нужно решить несколько проблем. Начну с самых основных. Просмотрите код метода 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(), но это не сработало. Я проведу рефакторинг своего кода, чтобы он, надеюсь, выглядел чище, и опубликую его в качестве ответа. Спасибо, как только вы получите, вы очень помогли!
Благодаря @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.
Пожалуйста, обрежьте свой код, чтобы облегчить поиск проблемы. Следуйте этим рекомендациям, чтобы создать минимально воспроизводимый пример.