踐)
簡(jiǎn)介基于Python與PyQt5實(shí)現(xiàn)的多智能體博弈AI五子棋項(xiàng)目是一份面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)或期末大作業(yè)的完整源碼資料。項(xiàng)目覆蓋人機(jī)博弈、深度搜索與α-β剪枝等核心AI算法代碼附有注釋適合新手閱讀與二次開(kāi)發(fā)也方便快速部署體驗(yàn)。壓縮包共17個(gè)文件包含4個(gè)Python源碼文件、1個(gè)PyQt界面文件(.ui)、1個(gè)資源文件(.qrc)以及9張PNG圖片、1個(gè)ICO圖標(biāo)和1份Markdown文檔整體大小約5.39MB。Python源碼承載游戲邏輯與AI決策UI和資源文件負(fù)責(zé)界面呈現(xiàn)文檔說(shuō)明有助于理解項(xiàng)目結(jié)構(gòu)項(xiàng)目結(jié)構(gòu)清晰適合展示博弈樹(shù)搜索過(guò)程。目前已有338人學(xué)習(xí)/瀏覽。通過(guò)這份資料讀者可以獲得完整的項(xiàng)目源代碼、界面與圖標(biāo)素材、啟動(dòng)入口和README說(shuō)明既能直接運(yùn)行體驗(yàn)人機(jī)對(duì)戰(zhàn)也能依據(jù)注釋和文檔梳理性剪枝、深搜等AI實(shí)現(xiàn)思路為畢業(yè)答辯或課程匯報(bào)提供扎實(shí)參考。1. 多智能體博弈五子棋一個(gè)能落在簡(jiǎn)歷上的畢設(shè)題目如果你在找畢業(yè)設(shè)計(jì)題目大概率會(huì)盯著“基于PythonPyQt5實(shí)現(xiàn)多智能體博弈AI五子棋游戲人機(jī)博弈深搜α-β剪枝”這種長(zhǎng)標(biāo)題琢磨它是把界面、AI、搜索算法串成一個(gè)能玩的程序還是給外行看的包裝詞我的判斷是這個(gè)題目很適合做課設(shè)和畢設(shè)因?yàn)槊總€(gè)詞都能拆成獨(dú)立章節(jié)最后又能合起來(lái)交付一個(gè)交互項(xiàng)目。項(xiàng)目核心價(jià)值不在“會(huì)下五子棋”而在一條完整的落地鏈路用Python和PyQt5搭建圖形界面用深搜實(shí)現(xiàn)AI決策再用α-β剪枝把性能拉到可玩范圍最后把算法分析寫(xiě)進(jìn)文檔。它同時(shí)覆蓋人機(jī)博弈、雙AI對(duì)弈和算法對(duì)比適合有Python基礎(chǔ)但沒(méi)寫(xiě)過(guò)GUI、希望在搜索算法上補(bǔ)課的人。2. 用PyQt5把棋盤畫(huà)出來(lái)最小可交互界面2.1 為什么用PyQt5而不是控制臺(tái)或網(wǎng)頁(yè)做五子棋的第一版我見(jiàn)過(guò)不少同學(xué)用控制臺(tái)打印棋盤邏輯照樣能跑但答辯時(shí)總覺(jué)得少點(diǎn)東西。PyQt5的優(yōu)勢(shì)在于它和Python環(huán)境配合非常直接先確認(rèn)python安裝和pip正常然后執(zhí)行pip install pyqt5就能開(kāi)始寫(xiě)不需要像前端那樣再裝一套Node或調(diào)跨域問(wèn)題。如果沒(méi)裝成功多數(shù)是源太慢換國(guó)內(nèi)pip鏡像即可裝完之后要重啟一下IDE否則vscode的python環(huán)境可能識(shí)別不到新的第三方包。另一個(gè)我看中的點(diǎn)是PyQt5的信號(hào)槽機(jī)制。棋盤點(diǎn)擊、AI落子、線程通信都可以用信號(hào)和槽串起來(lái)比tkinter的command回調(diào)更清晰。QPainter繪制15路棋盤和棋子很快整個(gè)窗口重繪一次毫秒級(jí)別不會(huì)成為后續(xù)深搜的性能瓶頸。所以我的項(xiàng)目結(jié)構(gòu)從一開(kāi)始就分成三層Board類只管棋盤二維數(shù)組和勝負(fù)判斷AI搜索類只讀棋盤并返回落子坐標(biāo)MainWindow負(fù)責(zé)鼠標(biāo)事件和重繪。這樣“人機(jī)博弈”和“多智能體博弈”共用一個(gè)Board接口后面雙AI對(duì)弈時(shí)不需要為窗口操作費(fèi)心。代碼上最大程度避免在UI里直接操作棋盤數(shù)據(jù)。2.2 棋盤繪制與落子坐標(biāo)換算下面是一段可以直接運(yùn)行的PyQt5最小界面。它不包含AI只是用來(lái)驗(yàn)證環(huán)境和確認(rèn)坐標(biāo)映射代碼里有注釋。import sys from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QPainter, QColor from PyQt5.QtCore import Qt class GobangWindow(QMainWindow): def __init__(self, n15): super().__init__() self.n n self.cell 40 self.margin 30 self.board [[0] * n for _ in range(n)] # 0空地 1黑子 2白子 self.size self.margin * 2 self.cell * (self.n - 1) self.setWindowTitle(PyQt5 五子棋) self.setFixedSize(self.size, self.size) def cx(self, c): return self.margin c * self.cell def cy(self, r): return self.margin r * self.cell def paintEvent(self, event): painter QPainter(self) painter.fillRect(self.rect(), QColor(235, 176, 89)) painter.setPen(QColor(0, 0, 0)) for i in range(self.n): y self.cy(i) painter.drawLine(self.margin, y, self.size - self.margin, y) painter.drawLine(y, self.margin, y, self.size - self.margin) for r in range(self.n): for c in range(self.n): if self.board[r][c] 1: painter.setBrush(QColor(0, 0, 0)) painter.drawEllipse(self.cx(c) - 13, self.cy(r) - 13, 26, 26) elif self.board[r][c] 2: painter.setBrush(QColor(255, 255, 255)) painter.setPen(QColor(0, 0, 0)) painter.drawEllipse(self.cx(c) - 13, self.cy(r) - 13, 26, 26) def mousePressEvent(self, event): if event.button() ! Qt.LeftButton: return x, y event.x(), event.y() # 像素坐標(biāo)換算成棋盤坐標(biāo)round讓點(diǎn)擊點(diǎn)自動(dòng)吸附到最近的交叉點(diǎn) c round((x - self.margin) / self.cell) r round((y - self.margin) / self.cell) if 0 r self.n and 0 c self.n: self.board[r][c] 1 self.update() if __name__ __main__: app QApplication(sys.argv) win GobangWindow() win.show() sys.exit(app.exec_())我對(duì)代碼里的幾個(gè)參數(shù)說(shuō)明一下。n15是棋盤路數(shù)對(duì)應(yīng)標(biāo)準(zhǔn)五子棋cell40是每格邊長(zhǎng)窗口尺寸是3024014620像素在筆記本上剛好完整顯示。margin30控制棋盤到窗口邊緣的距離如果想做截圖再調(diào)到40。注意setFixedSize而不是setGeometry它能讓窗口不可拉伸避免棋盤被拖動(dòng)變形。點(diǎn)擊事件的坐標(biāo)換算是最容易錯(cuò)的地方。鼠標(biāo)x、y是相對(duì)于窗口左上角的像素值而棋盤落子要用行列號(hào)。公式是“先減邊距再除以格子寬度”也就是c(x-margin)/cell取整要用round而不是int。原因很簡(jiǎn)單如果用戶點(diǎn)在第7列中心偏右一點(diǎn)int會(huì)把0.8變成0落子唰地跳到上一格round則吸附到最近的交叉點(diǎn)。邊緣的落子手感全靠這一步。這段代碼里的board二維數(shù)組就是后面深搜要操作的數(shù)據(jù)。界面歸界面數(shù)據(jù)歸數(shù)據(jù)后面寫(xiě)AI時(shí)你不需要在這個(gè)類里動(dòng)任何繪圖代碼。2.3 從“人”這一側(cè)接出博弈流程光能落子還不夠鼠標(biāo)點(diǎn)擊后要能轉(zhuǎn)入AI決策。下面只列出新增字段和方法替換2.2里的同名方法即可class GobangWindow(QMainWindow): def __init__(self, n15): # 棋盤、尺寸、paintEvent等初始化見(jiàn)2.2節(jié) self.current_turn 1 self.locked False def mousePressEvent(self, event): if self.locked: return if event.button() ! Qt.LeftButton: return x, y event.x(), event.y() c round((x - self.margin) / self.cell) r round((y - self.margin) / self.cell) if 0 r self.n and 0 c self.n and self.board[r][c] 0: self.board[r][c] 1 # 玩家先手 self.update() self.locked True self.ai_move() def ai_move(self): # 先放一個(gè)占位用QTimer延遲300毫秒避免事件循環(huán)卡頓 from PyQt5.QtCore import QTimer QTimer.singleShot(300, self.place_ai_stone) def place_ai_stone(self): # 占位邏輯找第一個(gè)空位放白子后續(xù)替換成AI搜索結(jié)果 for r in range(self.n): for c in range(self.n): if self.board[r][c] 0: self.board[r][c] 2 self.locked False self.update() return這個(gè)流程里的locked標(biāo)志很關(guān)鍵。如果沒(méi)有它玩家會(huì)在AI思考時(shí)連續(xù)點(diǎn)擊給同一個(gè)棋盤同時(shí)寫(xiě)入黑白子坐標(biāo)全部錯(cuò)亂。我這里用全局的lockedtrue鎖住界面等AI通過(guò)place_ai_stone落子后再解鎖。QTimer.singleShot(300, ...)是演示時(shí)最常見(jiàn)的做法它的作用是讓界面先刷新一次再執(zhí)行AI邏輯。真正做答辯版本建議換成第4.1里的QThread方案因?yàn)樯钏押臅r(shí)時(shí)讓主線程同步等待會(huì)直接把事件循環(huán)堵死。這段代碼跑通后你已經(jīng)完成了人機(jī)博弈的“人”側(cè)。接下來(lái)把place_ai_stone里的循環(huán)換成真正的深搜搜索結(jié)果就是完整的AI落子鏈路。3. 深搜與α-β剪枝從枚舉全盤到秒出結(jié)果3.1 多智能體博弈先建立一個(gè)零和搜索模型多智能體博弈聽(tīng)上去復(fù)雜五子棋其實(shí)是最適合拆開(kāi)的例子。這里的“智能體”不是指深度學(xué)習(xí)模型而是指能獨(dú)立讀棋盤、做決策的單元玩家是一個(gè)智能體AI是另一個(gè)。雙AI對(duì)戰(zhàn)時(shí)黑棋和白棋各是一個(gè)智能體。它們共享同一個(gè)棋盤輪流改變環(huán)境狀態(tài)目標(biāo)完全相反所以這是一個(gè)典型零和博弈。在搜索的角度看當(dāng)前局面是根節(jié)點(diǎn)AI落子的每個(gè)空位都產(chǎn)生一個(gè)分支對(duì)手的每個(gè)回應(yīng)又繼續(xù)產(chǎn)生分支最終長(zhǎng)成一棵博弈樹(shù)。深搜要做的是從根一直往深處探索到搜索深度上限或分出勝負(fù)然后一層一層回傳評(píng)價(jià)值。因?yàn)殡p方目標(biāo)相反一層取最大下一層取最小這就是極大極小搜索也是α-β剪枝算法的基礎(chǔ)。需要提醒一點(diǎn)這里的“深搜”和“圖的深度優(yōu)先遍歷”不一樣。圖遍歷只要訪問(wèn)過(guò)節(jié)點(diǎn)就可以不再訪問(wèn)博弈搜索里每個(gè)節(jié)點(diǎn)狀態(tài)不同不能簡(jiǎn)單去重所以遞歸返回后必須恢復(fù)現(xiàn)場(chǎng)否則兄弟分支會(huì)看到被修改過(guò)的棋盤。3.2 深搜的遞歸核心不剪枝版本先算對(duì)我一般先寫(xiě)一個(gè)不剪枝的minimax確認(rèn)評(píng)估函數(shù)和恢復(fù)邏輯沒(méi)錯(cuò)再加入剪枝。評(píng)估函數(shù)可以先用最簡(jiǎn)單的連續(xù)子計(jì)數(shù)讓AI先“有反應(yīng)”def evaluate_board(board, player): direction [(1, 0), (0, 1), (1, 1), (1, -1)] score 0 for r in range(15): for c in range(15): if board[r][c] ! player: continue for dr, dc in direction: cnt 1 for step in range(1, 5): nr, nc r dr * step, c dc * step if 0 nr 15 and 0 nc 15 and board[nr][nc] player: cnt 1 else: break if cnt 2: score cnt * cnt return score請(qǐng)不要迷信這個(gè)評(píng)分。它的作用是“把棋盤狀態(tài)映射成數(shù)值”如果棋形沒(méi)識(shí)別搜索再深也很難走出活三。但它足以跑通深搜等整體流程穩(wěn)定后再升級(jí)。然后是minimax核心def minimax(board, depth, is_maximizing, ai_color, human_color): winner check_winner(board) if winner ai_color: return 100000 depth if winner human_color: return -100000 - depth if depth 0: return evaluate_board(board, ai_color) - evaluate_board(board, human_color) candidates get_candidates(board) if not candidates: return 0 if is_maximizing: best -float(inf) for r, c in candidates: board[r][c] ai_color best max(best, minimax(board, depth - 1, False, ai_color, human_color)) board[r][c] 0 return best else: best float(inf) for r, c in candidates: board[r][c] human_color best min(best, minimax(board, depth - 1, True, ai_color, human_color)) board[r][c] 0 return best注意幾個(gè)邊界。勝負(fù)判斷要優(yōu)先于depth0否則“已經(jīng)贏了但還沒(méi)到深度”會(huì)繼續(xù)搜索返回100000depth而不用固定100000是為了讓更早獲勝的分支得分更高AI不會(huì)在勝局里故意拖延。遞歸嘗試落子后必須撤銷這是深搜最容易翻車的位置如果漏掉board[r][c]0后面的候選點(diǎn)會(huì)基于臟棋盤計(jì)算輕則算出錯(cuò)誤著法重則遞歸棧溢出。參數(shù)方面搜索深度我一般從2開(kāi)始調(diào)。深度2是最基礎(chǔ)的反應(yīng)深度4配合后面要說(shuō)的候選點(diǎn)限制已經(jīng)能走出像樣的防守深度6則要依賴α-β剪枝和合理評(píng)分。深度不是越大越好因?yàn)槊慷嘁粚訒r(shí)間會(huì)變得更長(zhǎng)AI落子超過(guò)5分鐘會(huì)讓演示翻車。3.3 加α-β剪枝兩個(gè)閾值讓深搜不再全遍歷不剪枝的minimax在空棋盤上幾乎不可用。以深度4、候選點(diǎn)10個(gè)為例最壞要搜10^4個(gè)局面雖然這只是理論上限已經(jīng)能把Python跑出明顯卡頓。α-β剪枝算法的思路是在深搜過(guò)程中維護(hù)兩個(gè)閾值alpha是MAX層目前已經(jīng)能保證的下界beta是MIN層目前能接受的上界。當(dāng)某個(gè)分支讓betaalpha成立說(shuō)明后面的搜索不會(huì)影響根節(jié)點(diǎn)決策可以整棵跳過(guò)。在minimax上改成帶剪枝版本def minimax_ab(board, depth, alpha, beta, is_maximizing, ai_color, human_color): winner check_winner(board) if winner ai_color: return 100000 depth if winner human_color: return -100000 - depth if depth 0: return evaluate_board(board, ai_color) - evaluate_board(board, human_color) candidates get_candidates(board) if not candidates: return 0 if is_maximizing: best -float(inf) for r, c in candidates: board[r][c] ai_color val minimax_ab(board, depth - 1, alpha, beta, False, ai_color, human_color) board[r][c] 0 best max(best, val) alpha max(alpha, best) if beta alpha: break return best else: best float(inf) for r, c in candidates: board[r][c] human_color val minimax_ab(board, depth - 1, alpha, beta, True, ai_color, human_color) board[r][c] 0 best min(best, val) beta min(beta, best) if beta alpha: break return best代碼只比原版多了兩個(gè)參數(shù)和兩個(gè)if break但性能差距可能非常大。這里最關(guān)鍵的是“誰(shuí)更新誰(shuí)”alpha只能MAX節(jié)點(diǎn)更新beta只能MIN節(jié)點(diǎn)更新。如果我在MAX里誤寫(xiě)了beta剪枝條件會(huì)因?yàn)檫吔缰靛e(cuò)亂而提前剪掉正確分支AI會(huì)突然下出莫名其妙的棋。接下來(lái)是剪枝的性價(jià)比候選點(diǎn)順序。同一個(gè)局面先搜一個(gè)“較好的”位置還是先搜一個(gè)“明顯差”的位置alpha/beta收斂速度完全不同。我習(xí)慣在生成候選點(diǎn)時(shí)就排序順便把候選列表限制在一個(gè)小集合里def get_candidates(board, max_candidates10): points set() for r in range(15): for c in range(15): if board[r][c] 0: continue for dr in range(-2, 3): for dc in range(-2, 3): nr, nc r dr, c dc if 0 nr 15 and 0 nc 15 and board[nr][nc] 0: points.add((nr, nc)) if not points: return [] scored [] for r, c in points: # 簡(jiǎn)單啟發(fā)式優(yōu)先落在棋子密集的附近 score 0 for dr in range(-1, 2): for dc in range(-1, 2): nr, nc r dr, c dc if 0 nr 15 and 0 nc 15: if board[nr][nc] 1: score 50 elif board[nr][nc] 2: score 30 scored.append((score, r, c)) scored.sort(reverseTrue) return [(r, c) for _, r, c in scored[:max_candidates]]這函數(shù)做的事有兩件第一只考慮現(xiàn)有棋子兩格范圍內(nèi)的空位把225個(gè)空位收斂到幾十個(gè)第二按簡(jiǎn)單啟發(fā)式倒序排列保證α-β先搜索有價(jià)值的分支。max_candidates是候選點(diǎn)數(shù)量上限我常用10深度大時(shí)可以縮到8。如果你發(fā)現(xiàn)AI總在邊角落子多半是把max_candidates放太寬或啟發(fā)式分值沒(méi)區(qū)分黑白只看自己棋子的權(quán)重卻忘了考慮對(duì)手。這樣搜索層用minimax_ab候選點(diǎn)用get_candidates評(píng)估函數(shù)用evaluate_board三者串起來(lái)就是深搜α-β剪枝的完整閉環(huán)。第4章我會(huì)梳理這幾部分各自最常踩的坑。4. 避坑與常見(jiàn)問(wèn)題從“能跑”變成“能答辯”4.1 界面卡死AI搜索不能放在主線程現(xiàn)象點(diǎn)擊后窗口白塊鼠標(biāo)拖動(dòng)無(wú)反應(yīng)過(guò)幾秒才恢復(fù)。原因在mousePressEvent里直接調(diào)用minimax_ab遞歸計(jì)算把PyQt5事件循環(huán)占滿了。解決用QThread。代碼class AIWorker(QThread): move_done pyqtSignal(int, int) def __init__(self, board_snapshot, depth, parentNone): super().__init__(parent) self.board_snapshot board_snapshot self.depth depth def run(self): r, c ai_search(self.board_snapshot, self.depth) self.move_done.emit(r, c)窗口側(cè)連接信號(hào)worker AIWorker([row[:] for row in self.board], depth) worker.move_done.connect(self.on_ai_move) worker.start()注意復(fù)制board。為什么UI線程還會(huì)繼續(xù)paintEvent它讀到的棋盤對(duì)象如果被AI線程修改繪制內(nèi)容會(huì)跳變。我會(huì)把“搜索用的快照”做成只讀AI線程只輸出一個(gè)坐標(biāo)再由主線程更新正式棋盤。如果你偷懶直接傳self.board偶爾會(huì)出現(xiàn)棋子和棋盤點(diǎn)錯(cuò)位而且很難復(fù)現(xiàn)。4.2 加了剪枝卻沒(méi)提速節(jié)點(diǎn)順序比剪枝本身更重要現(xiàn)象把minimax換成minimax_ab后時(shí)間幾乎沒(méi)降節(jié)點(diǎn)數(shù)也沒(méi)變。原因候選點(diǎn)沒(méi)排序alpha/beta很難觸發(fā)或者剪枝條件寫(xiě)錯(cuò)例如在MAX層卻比較beta。解決先在get_candidates里按啟發(fā)式排序再在遞歸入口放一個(gè)計(jì)數(shù)器驗(yàn)證。剪枝的正確性有個(gè)很實(shí)用的驗(yàn)證方法同一局面跑“不剪枝”和“剪枝”兩版兩者最佳落子坐標(biāo)必須完全一致。如果出現(xiàn)不一致說(shuō)明剪枝代碼邊界寫(xiě)錯(cuò)了而不是“剪枝改變了策略”。計(jì)數(shù)器代碼search_cnt 0 def minimax_ab(...): global search_cnt search_cnt 1 # 其余邏輯保持不變?cè)谶f歸函數(shù)入口自增跑一個(gè)中盤局面分別統(tǒng)計(jì)剪枝前后次數(shù)。如果次數(shù)沒(méi)掉一個(gè)數(shù)量級(jí)回去改搜索順序如果時(shí)間降了幾十倍而節(jié)點(diǎn)數(shù)沒(méi)降多少可能是評(píng)估函數(shù)耗時(shí)小、minimax_ab遞歸外開(kāi)銷占主導(dǎo)這時(shí)優(yōu)先減少候選點(diǎn)數(shù)。4.3 候選點(diǎn)全遍歷225的空位會(huì)把深搜拖死現(xiàn)象棋盤只下了兩三子AI就已經(jīng)想得滿頭汗深度調(diào)到4直接等不出結(jié)果。原因get_candidates未做約束每次遞歸都掃描全部225個(gè)空位。解決只在已有棋子的2格范圍內(nèi)找空位并取前10~15個(gè)。搜一個(gè)固定的中盤局面候選點(diǎn)通常會(huì)降到30個(gè)以內(nèi)再截?cái)嗟?0個(gè)深度4最多一萬(wàn)次葉節(jié)點(diǎn)評(píng)估配合剪枝實(shí)際會(huì)少很多。這張取舍直接決定AI速度比優(yōu)化評(píng)估函數(shù)更立竿見(jiàn)影。如果棋盤上一枚棋子都沒(méi)有g(shù)et_candidates會(huì)返回空列表。這時(shí)我一般讓AI直接下天元附近而不是強(qiáng)行搜索。這種邊界要寫(xiě)進(jìn)文檔否則評(píng)審試一下AI先手卻不下棋印象分很受影響。4.4 評(píng)估函數(shù)只看“連續(xù)子數(shù)”AI沒(méi)有進(jìn)攻意識(shí)現(xiàn)象AI會(huì)應(yīng)“馬上要成五的點(diǎn)”但不會(huì)主動(dòng)制造活三評(píng)優(yōu)時(shí)總選一個(gè)看似安穩(wěn)但沒(méi)威脅的點(diǎn)。原因評(píng)分只用了連子長(zhǎng)度的平方?jīng)]有區(qū)分活形和眠形。比如一個(gè)被兩邊堵死的眠三和兩邊都空的活三對(duì)局意義完全不同。解決給棋形加權(quán)。常見(jiàn)做法是棋形權(quán)重成五100000活四8000沖四2000活三800眠三100活二20把上面evaluate_board里的連續(xù)計(jì)數(shù)換成統(tǒng)計(jì)每種棋形的數(shù)量乘以不同權(quán)重。答辯時(shí)把這張權(quán)重表放進(jìn)文檔說(shuō)明“深搜負(fù)責(zé)找路線評(píng)估函數(shù)負(fù)責(zé)判斷路線價(jià)值”比空談算法更有說(shuō)服力。4.5 “多智能體”被追問(wèn)題目里每個(gè)詞都要有落點(diǎn)現(xiàn)象演示時(shí)只打開(kāi)人機(jī)對(duì)戰(zhàn)評(píng)委問(wèn)“多智能體體現(xiàn)在哪”答不上來(lái)。原因題目里的多智能體博弈沒(méi)有對(duì)應(yīng)可玩功能。解決在菜單里加一個(gè)“AI對(duì)戰(zhàn)”模式黑棋用深搜剪枝白棋用隨機(jī)或貪心策略讓兩個(gè)智能體自行下完一整局并在界面下方打印落子數(shù)、搜索深度、剪枝前后節(jié)點(diǎn)數(shù)。這樣既能把“多智能體”落地還能順帶收集勝率數(shù)據(jù)。文檔里就寫(xiě)兩個(gè)獨(dú)立決策單元共享棋盤狀態(tài)各自優(yōu)化自身收益構(gòu)成一個(gè)零和博弈系統(tǒng)。這一條回答能直接把題目里的三個(gè)關(guān)鍵詞全部引出來(lái)。5. 驗(yàn)證剪枝效果用節(jié)點(diǎn)數(shù)讓答辯眼前一亮5.1 用節(jié)點(diǎn)數(shù)和雙AI勝率驗(yàn)證算法代碼from copy import deepcopy board load_midgame_board() # 讀一個(gè)中盤局面 cnt_no_prune search_stats(deepcopy(board), depth4, use_pruneFalse) cnt_prune search_stats(deepcopy(board), depth4, use_pruneTrue) print(no prune:, cnt_no_prune) print(prune:, cnt_prune)search_stats是一個(gè)把minimax包了一層計(jì)數(shù)的函數(shù)。注意兩版必須用同一個(gè)board對(duì)象深拷貝不能先跑一版再跑另一版時(shí)棋盤已經(jīng)被改掉另外只統(tǒng)計(jì)葉節(jié)點(diǎn)還是所有節(jié)點(diǎn)要在文檔里說(shuō)明否則數(shù)據(jù)對(duì)不上。我的習(xí)慣是統(tǒng)計(jì)“進(jìn)入遞歸的總次數(shù)”因?yàn)檫@個(gè)數(shù)最直觀且不受機(jī)器波動(dòng)影響。第二個(gè)驗(yàn)證是雙AI勝率。讓黑棋AI用剪枝白棋AI用隨機(jī)落子連續(xù)跑20局看黑棋勝率。如果剪枝實(shí)現(xiàn)有問(wèn)題黑棋勝率不會(huì)比隨機(jī)高多少如果棋力正常勝率應(yīng)該接近100%。比較兩件事剪枝前后節(jié)點(diǎn)數(shù)顯著下降剪枝后的棋力不下降這才算有效優(yōu)化。5.2 把參數(shù)表寫(xiě)進(jìn)文檔文檔說(shuō)明別照抄代碼要寫(xiě)清楚四個(gè)數(shù)搜索深度、候選點(diǎn)數(shù)、評(píng)分權(quán)重、剪枝開(kāi)關(guān)。我用過(guò)一個(gè)最簡(jiǎn)單的方式寫(xiě)一個(gè)CONFIG.py集中管理這些參數(shù)文檔里放“depth4max_candidates10成五100000活四8000……”表格并說(shuō)明不同參數(shù)組合在演示機(jī)上耗時(shí)約多少秒。答辯時(shí)直接展示這一頁(yè)比講十分鐘原理更有效。我現(xiàn)在做AI相關(guān)項(xiàng)目第一件事就是給搜索加一個(gè)節(jié)點(diǎn)計(jì)數(shù)開(kāi)關(guān)。沒(méi)有計(jì)數(shù)剪枝效果全靠感覺(jué)就是玄學(xué)有了計(jì)數(shù)你能一眼看出優(yōu)化是不是真的發(fā)生了。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取