简介:这份资源是人工智能课程配套的完整代码合集,面向正在学习AI基础算法与经典实验的高校学生及自学者,覆盖搜索、博弈、强化学习与深度学习等核心模块。包内共50个文件,以20个Python源码为主体,辅以png结果图、xml工程配置、gz与idx格式的MNIST数据集、pth模型权重及xlsx启发函数表等,压缩包约22.55MB,按work1至work8分目录组织,结构清晰便于逐项对照学习。内容涵盖罗马尼亚度假问题的代价一致宽度优先、贪婪与A*搜索,并延伸至蚁群算法求解;同时包含8皇后问题、Wumpus怪兽世界的联机搜索与强化学习、α-β剪枝井字棋、MCYTS算法以及LeNet-5手写数字识别。已有109人学习下载,适合作为课程作业参考、算法对比实验与期末复习的实操素材,帮助读者理解不同搜索策略的代价与启发式差异,并快速复现经典AI实验流程。
1. 从罗马尼亚到 Wumpus:一套 AI 课代码到底能跑出什么
如果你正在上人工智能这门课,大概率绕不开这几个名字:罗马尼亚问题、8 皇后、Wumpus 怪兽世界。它们不是随便起的名字,而是搜索算法教学里最经典的几个沙盘。这套代码把代价一致搜索、贪婪最佳优先、A*、联机搜索、蚁群算法全部串在同一个场景里,用 Python 实现,拿来就能跑。适合谁?正在赶课程设计的学生、想补搜索算法实操的转行者、以及需要一份能改能扩的算法基线代码的开发者。它解决的核心问题是:把课本上那些伪代码变成能打印路径、能对比代价、能调参的活代码。下面我按实际拆包和跑通的顺序,把每个模块的用法和坑讲清楚。
2. 代价一致、贪婪与 A*:三种搜索策略的代码骨架与参数调法
2.1 罗马尼亚地图的数据结构怎么建
这套代码里,罗马尼亚问题通常用字典嵌套字典来表示城市和道路代价。常见做法是定义一个graph字典,键是城市名,值是一个字典,里面存邻居城市和对应距离。比如:
# 罗马尼亚地图的邻接表表示 romania_map = { 'Arad': {'Zerind': 75, 'Sibiu': 140, 'Timisoara': 118}, 'Zerind': {'Arad': 75, 'Oradea': 71}, 'Oradea': {'Zerind': 71, 'Sibiu': 151}, 'Sibiu': {'Arad': 140, 'Oradea': 151, 'Fagaras': 99, 'Rimnicu Vilcea': 80}, 'Timisoara': {'Arad': 118, 'Lugoj': 111}, 'Lugoj': {'Timisoara': 111, 'Mehadia': 70}, 'Mehadia': {'Lugoj': 70, 'Drobeta': 75}, 'Drobeta': {'Mehadia': 75, 'Craiova': 120}, 'Craiova': {'Drobeta': 120, 'Rimnicu Vilcea': 146, 'Pitesti': 138}, 'Rimnicu Vilcea': {'Sibiu': 80, 'Craiova': 146, 'Pitesti': 97}, 'Fagaras': {'Sibiu': 99, 'Bucharest': 211}, 'Pitesti': {'Rimnicu Vilcea': 97, 'Craiova': 138, 'Bucharest': 101}, 'Bucharest': {'Fagaras': 211, 'Pitesti': 101, 'Giurgiu': 90, 'Urziceni': 85}, 'Giurgiu': {'Bucharest': 90}, 'Urziceni': {'Bucharest': 85, 'Hirsova': 98, 'Vaslui': 142}, 'Hirsova': {'Urziceni': 98, 'Eforie': 86}, 'Eforie': {'Hirsova': 86}, 'Vaslui': {'Urziceni': 142, 'Iasi': 92}, 'Iasi': {'Vaslui': 92, 'Neamt': 87}, 'Neamt': {'Iasi': 87} }这段字典就是整个搜索问题的输入。键是出发城市,内层字典的键是可达城市,值是公里数。参数说明:如果你要换地图,只需要按同样格式替换这个字典,搜索函数不用改。注意城市名大小写要统一,代码里通常用字符串精确匹配,写成'arad'和'Arad'会被当成两个城市,这是新手最容易翻车的地方。
2.2 代价一致搜索:优先队列怎么用才对
代价一致搜索的核心是每次扩展累计代价最小的节点。Python 里用heapq实现优先队列,但有个经典坑:如果两个节点代价相同,heapq会去比较第二个元素,如果第二个元素是字符串或自定义对象,就会报TypeError。常见解法是在元组里加一个自增计数器。
import heapq def uniform_cost_search(graph, start, goal): # 优先队列元素:(累计代价, 计数器, 当前城市, 路径) counter = 0 frontier = [(0, counter, start, [start])] explored = set() while frontier: cost, _, city, path = heapq.heappop(frontier) if city == goal: return path, cost if city in explored: continue explored.add(city) for neighbor, distance in graph.get(city, {}).items(): if neighbor not in explored: counter += 1 heapq.heappush(frontier, (cost + distance, counter, neighbor, path + [neighbor])) return None, float('inf')逻辑说明:每次从堆里弹出累计代价最小的节点,如果已经探索过就跳过,否则标记为已探索并扩展邻居。参数说明:counter是为了避免堆在代价相同时比较城市名,这个计数器必须全局唯一且递增。path直接存在堆元素里,方便最后回溯,代价是内存占用会随路径长度增加。跑uniform_cost_search(romania_map, 'Arad', 'Bucharest')应该返回总代价 418 的路径。如果你发现返回的路径不是最优,先检查explored集合是不是在弹出时就加入,而不是在生成时就加入——这是代价一致搜索和 A* 在实现上的关键区别。
2.3 贪婪最佳优先与 A*:启发函数怎么设
贪婪最佳优先只看启发值,A* 看累计代价加启发值。两者共用同一个启发函数,通常是直线距离表。代码里一般会单独定义一个heuristics字典:
# 到 Bucharest 的直线距离启发值 heuristics = { 'Arad': 366, 'Bucharest': 0, 'Craiova': 160, 'Drobeta': 242, 'Eforie': 161, 'Fagaras': 176, 'Giurgiu': 77, 'Hirsova': 151, 'Iasi': 226, 'Lugoj': 244, 'Mehadia': 241, 'Neamt': 234, 'Oradea': 380, 'Pitesti': 100, 'Rimnicu Vilcea': 193, 'Sibiu': 253, 'Timisoara': 329, 'Urziceni': 80, 'Vaslui': 199, 'Zerind': 374 } def greedy_best_first(graph, start, goal, h): frontier = [(h[start], start, [start])] explored = set() while frontier: _, city, path = heapq.heappop(frontier) if city == goal: return path if city in explored: continue explored.add(city) for neighbor in graph.get(city, {}): if neighbor not in explored: heapq.heappush(frontier, (h[neighbor], neighbor, path + [neighbor])) return None def a_star(graph, start, goal, h): counter = 0 frontier = [(h[start], 0, counter, start, [start])] explored = set() while frontier: f, g, _, city, path = heapq.heappop(frontier) if city == goal: return path, g if city in explored: continue explored.add(city) for neighbor, distance in graph.get(city, {}).items(): if neighbor not in explored: counter += 1 new_g = g + distance heapq.heappush(frontier, (new_g + h[neighbor], new_g, counter, neighbor, path + [neighbor])) return None, float('inf')逻辑说明:贪婪最佳优先的堆元素是(启发值, 城市, 路径),A* 是(f值, g值, 计数器, 城市, 路径)。参数说明:h必须满足可采纳性,也就是不能高估到目标的真实代价,否则 A* 不保证最优。罗马尼亚问题的直线距离表满足这个条件。跑a_star(romania_map, 'Arad', 'Bucharest', heuristics)应该也返回 418,但扩展节点数比代价一致少。如果你发现 A* 结果比代价一致差,先检查启发值是不是写大了,或者explored的加入时机是不是在弹出时——A* 在弹出时加入explored才能保证最优。
3. 8 皇后与 Wumpus:从约束满足到联机搜索的代码拆解
3.1 8 皇后:回溯法的冲突检测怎么写才不慢
8 皇后是约束满足问题的入门案例。代码里通常用一维数组表示棋盘,queens[i] = j表示第 i 行的皇后在第 j 列。冲突检测只查列和对角线,不查行,因为每行只放一个。
def solve_8_queens(n=8): solutions = [] queens = [-1] * n # queens[row] = col def is_safe(row, col): for r in range(row): c = queens[r] if c == col or abs(c - col) == abs(r - row): return False return True def backtrack(row): if row == n: solutions.append(queens[:]) return for col in range(n): if is_safe(row, col): queens[row] = col backtrack(row + 1) queens[row] = -1 backtrack(0) return solutions逻辑说明:is_safe只检查已放置的行,列冲突看c == col,对角线冲突看行差绝对值等于列差绝对值。参数说明:n默认 8,改成 4 可以快速验证。跑solve_8_queens(8)会返回 92 个解。常见坑是queens[row] = -1的回溯复位漏写,导致后续分支拿到脏数据。另一个坑是is_safe里用了range(n)而不是range(row),多算了未放置的行,结果永远返回 False。
3.2 Wumpus 世界:联机搜索的感知与动作循环
Wumpus 世界的联机搜索和离线搜索不同,智能体一开始不知道地图,只能靠感知( breeze、stench、glitter)逐步探索。代码里通常用一个WumpusWorld类维护状态,智能体每步选择动作。
class WumpusWorld: def __init__(self, size=4): self.size = size self.agent_pos = (0, 0) self.agent_dir = 'right' self.has_gold = False self.has_arrow = True self.wumpus_alive = True # 固定布局,实际代码可能随机生成 self.pits = {(1, 2), (3, 0), (3, 2)} self.wumpus = (0, 2) self.gold = (1, 1) def percepts(self): r, c = self.agent_pos breeze = any(abs(r - pr) + abs(c - pc) == 1 for pr, pc in self.pits) stench = abs(r - self.wumpus[0]) + abs(c - self.wumpus[1]) == 1 glitter = self.agent_pos == self.gold return {'breeze': breeze, 'stench': stench, 'glitter': glitter} def step(self, action): # 动作:forward, turn_left, turn_right, grab, shoot if action == 'forward': dr, dc = {'up': (-1, 0), 'down': (1, 0), 'left': (0, -1), 'right': (0, 1)}[self.agent_dir] nr, nc = self.agent_pos[0] + dr, self.agent_pos[1] + dc if 0 <= nr < self.size and 0 <= nc < self.size: self.agent_pos = (nr, nc) if self.agent_pos in self.pits: return 'death' if self.agent_pos == self.wumpus and self.wumpus_alive: return 'death' elif action == 'grab': if self.agent_pos == self.gold: self.has_gold = True return 'gold' elif action == 'shoot': if self.has_arrow: self.has_arrow = False # 简化:朝当前方向射击 dr, dc = {'up': (-1, 0), 'down': (1, 0), 'left': (0, -1), 'right': (0, 1)}[self.agent_dir] sr, sc = self.agent_pos[0] + dr, self.agent_pos[1] + dc if (sr, sc) == self.wumpus: self.wumpus_alive = False return 'scream' return 'ok'逻辑说明:percepts返回当前格子的感知,step执行动作并返回结果。参数说明:size默认 4,pits和wumpus是固定坐标,实际代码可能用随机种子生成。联机搜索的智能体需要维护一个知识库,记录已访问格子和推断出的危险格子。常见坑是forward动作没有检查边界,导致智能体走出地图后坐标越界。另一个坑是shoot只朝当前方向射一箭,如果 Wumpus 不在相邻格,箭就浪费了。
3.3 蚁群算法解罗马尼亚问题:信息素更新与参数调优
蚁群算法在这套代码里通常用来解罗马尼亚问题的路径规划。核心是信息素矩阵和启发式信息的加权选择。
import random def ant_colony_optimization(graph, start, goal, n_ants=10, n_iter=50, alpha=1.0, beta=2.0, rho=0.5, q=100): # 初始化信息素 pheromone = {city: {nb: 1.0 for nb in graph[city]} for city in graph} best_path, best_cost = None, float('inf') for _ in range(n_iter): paths = [] for _ in range(n_ants): path = [start] visited = {start} current = start while current != goal: neighbors = [nb for nb in graph[current] if nb not in visited] if not neighbors: break # 计算转移概率 probs = [] for nb in neighbors: tau = pheromone[current][nb] ** alpha eta = (1.0 / graph[current][nb]) ** beta probs.append(tau * eta) total = sum(probs) if total == 0: break probs = [p / total for p in probs] nxt = random.choices(neighbors, weights=probs, k=1)[0] path.append(nxt) visited.add(nxt) current = nxt if path[-1] == goal: cost = sum(graph[path[i]][path[i+1]] for i in range(len(path)-1)) paths.append((path, cost)) if cost < best_cost: best_cost, best_path = cost, path # 信息素挥发 for city in pheromone: for nb in pheromone[city]: pheromone[city][nb] *= (1 - rho) # 信息素增强 for path, cost in paths: for i in range(len(path)-1): pheromone[path[i]][path[i+1]] += q / cost return best_path, best_cost逻辑说明:每只蚂蚁从起点出发,按信息素和启发式信息的加权概率选择下一个城市,直到到达终点或走不通。参数说明:alpha控制信息素权重,beta控制启发式权重,rho是挥发率,q是信息素增强常数。常见坑是alpha设得太大导致早熟收敛,所有蚂蚁走同一条路;beta设得太大退化成贪婪搜索。我一般先用alpha=1, beta=2, rho=0.5跑一遍,看收敛曲线再调。另一个坑是信息素矩阵没有按边初始化,无向图需要双向更新,否则反向路径的信息素永远是初始值。
4. 避坑与排查:跑这套代码时最容易翻车的五个地方
4.1 现象:A* 返回的路径不是最优,代价比代价一致大
原因:启发函数不满足可采纳性,或者explored集合在生成节点时就加入,导致最优路径被提前截断。解决:检查heuristics字典里每个值是不是小于等于真实最短距离,罗马尼亚问题的直线距离表可以直接用。把explored.add(city)移到弹出节点之后,而不是生成邻居时。
4.2 现象:优先队列报TypeError: '<' not supported between instances of 'str' and 'str'
原因:heapq在第一个元素相同时会比较第二个元素,如果第二个元素是城市名,两个字符串比较不会报错,但如果第二个元素是字典或自定义对象就会报错。更常见的是元组里混了None和字符串。解决:在堆元素里加一个自增计数器,放在比较键的第二位,确保任何情况下都不会比较到城市名或对象。
4.3 现象:8 皇后跑出来解的数量不对,比如 8 皇后只返回 10 个解
原因:回溯时queens[row] = -1的复位漏写,或者is_safe里用了range(n)而不是range(row)。解决:在backtrack的循环里,递归调用之后必须复位queens[row] = -1。is_safe只检查range(row),因为row之后的行还没放皇后。
4.4 现象:Wumpus 智能体第一步就死亡,或者原地转圈
原因:step函数里forward没有检查边界,智能体走出地图后坐标越界,或者感知函数返回的breeze和stench判断条件写反。解决:在forward里加边界检查,越界时保持原地不动并返回'bump'。感知函数里abs(r - pr) + abs(c - pc) == 1是曼哈顿距离为 1,表示相邻格,不要写成<= 1,否则当前格也会触发。
4.5 现象:蚁群算法跑多次结果波动很大,有时找不到路径
原因:信息素初始值太小,或者q设得太大导致信息素爆炸,或者蚂蚁数量太少。解决:信息素初始值设为 1.0,q一般取 100 左右,蚂蚁数量取城市数量的 1 到 2 倍。如果图是无向的,信息素更新要双向写,pheromone[a][b]和pheromone[b][a]都要加。跑之前先固定随机种子,方便复现。
5. 进阶技巧:把联机搜索和蚁群算法接在一起跑
这套代码里最值得动手改的地方,是把 Wumpus 世界的联机搜索和蚁群算法的路径规划接在一起。思路是:智能体在 Wumpus 世界里探索时,每访问一个新格子就更新一张局部地图,然后用蚁群算法在已知安全格子上规划到未探索区域的最短路径。这样比纯反应式智能体走得更有目的性。
具体做法分三步。第一步,在WumpusWorld类里加一个visited集合和一个safe_graph字典,每次step成功后把当前格子和相邻安全格子加入。第二步,写一个plan_path(start, targets)函数,用蚁群算法在safe_graph上找从当前位置到最近未访问格子的路径。第三步,在主循环里,如果plan_path返回非空,就按路径走;如果为空,就随机选一个安全方向探索。
def plan_path(safe_graph, start, targets, n_ants=5, n_iter=20): # 在已知安全图上跑蚁群,找离 start 最近的 target best_path, best_cost = None, float('inf') for target in targets: path, cost = ant_colony_optimization(safe_graph, start, target, n_ants, n_iter) if path and cost < best_cost: best_cost, best_path = cost, path return best_path参数说明:n_ants和n_iter不用设太大,因为局部地图通常只有十几个节点,跑 5 只蚂蚁 20 代就够。targets是未访问的安全格子集合,每次规划前更新。注意safe_graph里只包含已确认没有陷阱和 Wumpus 的格子,不要把breeze或stench相邻的格子加进去,否则智能体会直接撞死。
验证方法:跑 100 次随机布局,统计智能体拿到金子的成功率和平均步数。如果成功率低于 60%,先检查safe_graph的更新逻辑是不是把危险格子也加进去了。我一般会在step返回'death'时打印最后几步的感知和动作,看是哪一步推断错了。
从那以后我每次跑联机搜索,都强制先跑一遍纯随机智能体做基线,再跑蚁群规划的版本,对比步数和成功率。没有基线的优化都是玄学。希望帮到你。
本文还有配套的精品资源,点击获取