简介:这份资源是面向准备参加微软 AI-900 认证考试的考生整理的备考资料,聚焦人工智能基础概念、机器学习工作流与 Azure AI 服务相关考点,适合零基础入门者以及希望系统刷题、查漏补缺的转行或在校学习者使用。压缩包内共 1 个 PDF 文件,约 17.65MB,内容以 ExamTopics 风格的考试题集为主,逐题给出题干、选项、正确答案与社区投票分布,并附带官方文档引用链接,便于边做题边追溯知识点来源。题目覆盖业务价值评估、训练集与评估集划分、混淆矩阵指标(如真阳性、假阴性)解读、自动化机器学习中的模型可解释性与负责任 AI 原则等核心模块,能让读者直观感受考试出题角度与答题逻辑。目前该资源已有 363 人学习下载,可作为 AI-900 冲刺阶段的自测与复盘材料,帮助快速定位薄弱环节并熟悉英文题干表述。
1. AI-900 备考里,ExamTopics 的 PDF 题库到底该怎么用
AI-900 _ ExamTopics认证考试.pdf这个文件名本身就是一条很典型的备考路径:从 ExamTopics 这类题库站点拿到 AI-900 的题目合集,存成 PDF,然后指望在 PDF 阅读器里从头翻到尾就能过。翻完的人大多有同一个体感——题看着都眼熟,真到考场上问「哪个 Azure 服务负责关键短语提取」,脑子里还是一片模糊。
问题不在题源,在载体。PDF 是给人眼看的线性文档,不是给复习流程用的结构。AI-900 是 Azure AI Fundamentals 这一档的考试,考的是 AI 工作负载与负责任 AI、机器学习基础、计算机视觉、自然语言处理、生成式 AI 这五块的判断能力,选项之间差异极小,靠翻页记住的是「位置感」,不是「知识点」。
我一般的做法是把这份 PDF 当数据源而不是读物:pdf 解析成结构化题库,按考试域打标签,再进模拟考试与错题回流。适合已经决定考 AI-900、手上有题库 PDF、并且愿意写几十行 Python 的人。
2. 用 pdfplumber 把 ExamTopics 的 AI-900 PDF 解析成结构化题库
这一步的目标只有一个:把一份几百页、排版不统一的 PDF,变成一份每道题都有题干、选项、答案、解释字段的 JSON。做完这一步,后面所有统计、抽题、错题本才有落脚点。
2.1 先摸清 AI-900 题库 PDF 的排版规律,再决定解析工具
ExamTopics 系的题库 PDF 大致有这些固定元素:页眉页脚(站点名、Topic 编号、社区投票区)、题号(Question #12或12.)、选项(A./A)/ 带方框的列表)、答案行(Answer: A或Correct Answer: A, C多选)、解释段(Explanation)、参考链接(Reference)。脏数据主要来自四类:跨页断题、行尾连字符换行、选项被排成两列、页眉混进正文。
工具选型上,别一上来就找在线 pdf 工具箱或者 pdf 转 word,那类方案零代码但不可复现,处理不了几百页还要重跑的场景。常见做法是主用 pdfplumber 抽词级坐标,PyMuPDF 兜底渲染扫描页。
| 工具 | 适合场景 | 优势 | 短板 |
|---|---|---|---|
| pdfplumber | 文本与表格混排、需要坐标判断分栏 | 提供每个词的 x0/x1/top/bottom,能按列重建阅读顺序 | 速度慢,大文件要分页处理 |
| PyMuPDF (fitz) | 大文件、需要把页渲染成图做 OCR | 快,渲染质量高 | 表格结构要自己重建 |
| pypdf | 只要纯文本、依赖越少越好 | 安装体积小 | 不分栏、不给坐标,双列排版会串行 |
| 在线 pdf 工具箱 / pdf 转 word | 一次性人工校对几页 | 上手快 | 无法批处理,结果不可复现 |
2.2 最小可跑:pdfplumber 抽取全文与词级坐标
先建环境,确认能读通页数和词坐标,再写解析逻辑。切忌一上来就写正则,先看数据长什么样。
python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install pdfplumber pyyaml python - <<'PY' import pdfplumber src = "AI-900 _ ExamTopics认证考试.pdf" with pdfplumber.open(src) as pdf: print("总页数:", len(pdf.pages)) # 先确认页数,顺便统计文件规模 page = pdf.pages[0] words = page.extract_words() # 词级对象:text + 坐标 for w in words[:8]: print(round(w["top"], 1), round(w["x0"], 1), w["text"]) PYextract_words()返回的每个词都带top和x0。判断是否双列排版,看同一页里x0是否明显分成两簇;还原阅读顺序时按top升序、x0升序排序,比直接extract_text()可靠得多。如果某页是扫描图,extract_words()会返回空列表,这类页要单独走 OCR 分支(见第 5 章)。几百页的文件按页 yield,不要一次把全文拼成一个大字符串,内存和调试成本都受不了。
2.3 用状态机把题干、选项、答案切干净
正则只负责识别行的类型,切题靠状态机。每个题号开启一个新的 Question 对象,之后的行按「当前处于题干段、选项段还是解释段」决定归属。
import re from dataclasses import dataclass, field QUESTION_RE = re.compile(r"^\s*(?:Question|QUESTION)\s*#?\s*(\d{1,4})\b", re.I) OPTION_RE = re.compile(r"^\s*([A-F])[\.\)、]\s+(.+?)\s*$") ANSWER_RE = re.compile(r"^\s*(?:Correct\s+)?Answer\s*[::]\s*([A-F](?:\s*[,/]\s*[A-F])*)", re.I) NOISE_RE = re.compile(r"^(?:ExamTopics|Topic\s*\d+|Community\s*vote|Highly\s*Voted|Reference)", re.I) @dataclass class Question: number: int stem: str = "" options: dict = field(default_factory=dict) answers: list = field(default_factory=list) explanation: str = "" def parse_questions(lines): qs, cur, last_opt = [], None, None for raw in lines: line = raw.strip() if not line or NOISE_RE.match(line): # 页眉页脚先清掉 continue m = QUESTION_RE.match(line) if m: if cur: qs.append(cur) cur, last_opt = Question(number=int(m.group(1))), None continue if cur is None: continue m = ANSWER_RE.match(line) if m: cur.answers = [a.strip() for a in re.split(r"[,/]", m.group(1))] last_opt = None # 进入解释段 continue m = OPTION_RE.match(line) if m: last_opt = m.group(1) cur.options[last_opt] = m.group(2) continue if cur.answers: # 答案之后的行都属于解释 cur.explanation += line + " " elif last_opt: cur.options[last_opt] += " " + line # 选项的续行 else: cur.stem += line + " " # 题干的续行 if cur: qs.append(cur) return qs几个必须踩过的坑。第一,ANSWER_RE一定要带行首锚点^,否则解释段里出现的 "the answer: read the image" 会被误判成答案行。第二,选项续行的归属靠last_opt判断,但如果 PDF 把解释排在选项之前,这个逻辑会把解释吞进最后一个选项,解决办法是给状态机加一个phase字段,一旦命中Answer:就锁死不再接受新选项。第三,多选答案的分隔符在不同版本里可能是逗号、斜杠或空格,re.split的字符集要放宽。
2.4 解析完必须做的三项校验
不校验就直接刷题,最后记的可能是一份错位题库。三项指标卡住,问题基本都能暴露。
| 校验项 | 判定标准 | 不通过时先查什么 |
|---|---|---|
| 题号连续性 | 实际条数 / (max - min + 1) ≥ 0.98 | 是否有题号被页眉拆成两行,导致漏题 |
| 选项完整度 | ≥ 90% 的题至少有 4 个选项 | 双列排版是否把 B/C 两列读到同一行 |
| 答案覆盖率 | 100% 有 answers | 答案行是否被换行拆断,Answer:与字母分处两行 |
| 解释归属率 | ≥ 80% 的题 explanation 非空 | 解释是否被判给了最后一个选项 |
def audit(qs): nums = sorted(q.number for q in qs) span = nums[-1] - nums[0] + 1 if nums else 0 print("题数:", len(qs), "题号跨度:", span, "连续性:", round(len(qs) / max(span, 1), 3)) print("选项完整:", round(sum(1 for q in qs if len(q.options) >= 4) / max(len(qs), 1), 3)) print("答案覆盖:", round(sum(1 for q in qs if q.answers) / max(len(qs), 1), 3)) print("解释覆盖:", round(sum(1 for q in qs if q.explanation) / max(len(qs), 1), 3))连续性低于 0.98,说明漏题,去查页眉页脚附近那几页;答案覆盖率不是 1.0,基本可以断定是Answer:与字母被排版拆开,把ANSWER_RE改成分两行匹配即可。
3. 给 AI-900 题目打标签:按考试域权重建一套可统计的题库
结构化之后,题库还是一盘散沙。要让它指导复习,必须给每道题打上考试域标签,否则你永远不知道自己的薄弱环节是计算机视觉还是 NLP。
3.1 AI-900 的考试域权重与关键词信号表
下面这张表里的权重区间来自公开的考试大纲,考前请以官方最新 Study Guide 为准;关键词列是我自己积累的信号词,用来做规则打标。
| 考试域 | 大纲权重 | 强信号关键词 | 典型迷惑项 |
|---|---|---|---|
| AI 工作负载与负责任 AI | 15–20% | responsible AI、fairness、inclusiveness、reliability、transparency、workload | 把 fairness 与 inclusiveness 混为一谈 |
| Azure 机器学习基础 | 20–25% | regression、classification、clustering、feature、label、designer、AutoML | 把 clustering 当成监督学习 |
| 计算机视觉 | 15–20% | Custom Vision、Face、OCR、Read API、image classification、object detection | Read 与表单类服务的职责边界 |
| 自然语言处理 | 15–20% | key phrase、sentiment、entity、LUIS、QnA、speech、translation | LUIS 与问答服务的适用场景 |
| 生成式 AI | 15–20% | Azure OpenAI、prompt、token、grounding、DALL·E、Copilot | 把 Copilot 当成一个具体模型 |
3.2 规则打标还是分类模型:为什么先规则
很多人第一反应是拿一个分类模型甚至大模型来打标。可行,但不该是第一步。规则打标的优势在于可解释、可回归测试、零调用成本,而且题库更新时你只需要往 YAML 里加几个关键词,不用重新标注数据。
我的顺序是:先用规则覆盖 80% 以上的题,把置信度低的挑出来人工过一遍,人工判断的结果再写回规则文件,形成「规则即标注」的循环。只有当低置信度比例长期高于 20%,才值得考虑引入语义分类。
3.3 打标脚本:must / any 双阈值与置信度回捞
把规则写成 YAML,题目文本统一转小写后做匹配。must是强信号,命中才给基础分;any是弱信号,每命中一个加一分。
import yaml RULES = yaml.safe_load(""" - domain: ml_fundamentals must: ["machine learning"] any: ["regression", "classification", "clustering", "feature", "label", "designer", "automl"] - domain: computer_vision must: [] any: ["custom vision", "face", "ocr", "read api", "image classification", "object detection"] - domain: nlp must: [] any: ["key phrase", "sentiment", "entity", "luis", "qna", "speech", "translation"] - domain: generative_ai must: [] any: ["openai", "prompt", "token", "grounding", "dall", "copilot"] - domain: ai_workload must: [] any: ["responsible", "fairness", "inclusiveness", "transparency", "workload"] """) def score(q, rule): text = (q.stem + " " + " ".join(q.options.values()) + " " + q.explanation).lower() must_hit = sum(1 for k in rule["must"] if k in text) any_hit = sum(1 for k in rule["any"] if k in text) return 2.0 * must_hit + any_hit # 强信号权重是弱信号的两倍 def label(q, rules): scored = sorted(((score(q, r), r["domain"]) for r in rules), key=lambda x: -x[0]) (s1, d1), (s2, _) = scored[0], scored[1] conf = s1 / (s1 + s2 + 1e-6) # 第一名占比越高越可信 return d1, round(conf, 3) def split_by_confidence(qs, rules, threshold=0.6): high, low = [], [] for q in qs: domain, conf = label(q, rules) (high if conf >= threshold else low).append((q, domain, conf)) return high, lowthreshold是我最常调的参数:调到 0.75 以上,人工复核的题变少但漏标变多;调到 0.5,低置信度队列会爆炸。0.6 是个能兼顾的起点。score里把 explanation 也算进文本,是因为解释里往往直接点名了服务名,比题干更有信息量。
3.4 用 SQL 统计各域错题率,定位复习盲区
题库和答题记录落到 SQLite 之后,用一条 SQL 就能看出该补哪块,而不是按大纲顺序从第一章背起。
SELECT domain, COUNT(*) AS total, SUM(CASE WHEN wrong_count > 0 THEN 1 ELSE 0 END) AS wrong_total, ROUND(1.0 * SUM(CASE WHEN wrong_count > 0 THEN 1 ELSE 0 END) / COUNT(*), 3) AS wrong_rate FROM questions GROUP BY domain ORDER BY wrong_rate DESC;wrong_rate排在最前面的域,就是下一次复习的起点。注意到COUNT(*)用的是题目总数而不是答题次数,这样不同域的样本量差异才不会让比例失真。如果某个域的总题数不到 30,比例波动会很大,这时把窗口拉长到最近三轮模拟再算一次更稳。
4. 从结构化题库到模拟考试:抽题、计时、错题回流
有了标签,就可以组装一套按权重分布、可重复的模拟卷。这一步的关键不是抽题算法有多复杂,而是参数要贴近真实考试,并且每次结果可对比。
4.1 按权重抽题的模拟考试脚本
import random from collections import defaultdict WEIGHTS = { "ai_workload": 0.175, "ml_fundamentals": 0.225, "computer_vision": 0.175, "nlp": 0.175, "generative_ai": 0.175, } def build_paper(questions, weights=WEIGHTS, total=45, seed=42): rng = random.Random(seed) # 固定种子,同一套卷可复现 buckets = defaultdict(list) for q in questions: buckets[q["domain"]].append(q) paper = [] for domain, w in weights.items(): n = round(total * w) # 按权重换算该域题量 pool = buckets[domain][:] rng.shuffle(pool) paper += pool[:min(n, len(pool))] if len(paper) < total: # 题量不足时从最大域回填 rest = [q for q in questions if q not in paper] rng.shuffle(rest) paper += rest[:total - len(paper)] rng.shuffle(paper) # 打散域顺序,避免同域连出 return paper[:total]seed固定意味着每次生成的卷子完全一致,方便你对比「上周做这套错了 12 题,这周错了 7 题」。total对齐真实题量区间。回填逻辑是为了防止某个域题量太少导致整卷题数缩水,q not in paper在题目对象不多时开销可以接受,量大时换成 id 集合更合适。
4.2 题量、时长、及格线、复习间隔的参数怎么定
| 参数 | 建议值 | 理由 |
|---|---|---|
| 单卷题量 total | 45 | 贴近真实场次的题量区间,太短测不出疲劳度 |
| 计时时长 | 45 分钟 | 平均每题 1 分钟,留出多选和长题干的时间 |
| 及格线 | 70% | 官方常见说法是 700/1000,换算成百分比便于自测 |
| 错题重考间隔 | 3 / 7 / 21 天 | 间隔重复,第一轮别隔太久,否则会忘光 |
| 域内重考窗口 | 7 天 | 同一道题一周内不重复出现,避免记住答案位置 |
计时不要用手机秒表,直接把开始时间写进结果文件,交卷时算差值,避免中途暂停造成的数据失真。
4.3 错题回流 Anki 与 Markdown 输出
错题只记在脑子里等于没记。导出成 Anki 卡片的 CSV,比截图存相册靠谱得多。
import csv def to_anki_csv(questions, path="ai900_wrong.csv"): # Anki 导入用制表符分隔最稳,选项里的逗号不会破坏列 with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f, delimiter="\t") for q in questions: front = q["stem"] + "\n" + "\n".join(f'{k}. {v}' for k, v in q["options"].items()) back = f'答案:{",".join(q["answers"])}\n\n{q["explanation"]}' writer.writerow([front, back, q["domain"]]) # 第三列映射为 Anki 标签 def to_markdown(questions, path="ai900_wrong.md"): with open(path, "w", encoding="utf-8") as f: for q in questions: f.write(f'### Q{q["number"]} · {q["domain"]}\n\n{q["stem"]}\n\n') for k, v in q["options"].items(): f.write(f'- {k}. {v}\n') f.write(f'\n> 答案:{",".join(q["answers"])}\n\n')制表符分隔是刻意的选择:题干里带逗号和引号的情况极多,逗号分隔的 CSV 在 Anki 导入时会大面积错列。Markdown 版本则是给打印和做 PDF 用的,标题里带域标签,翻页时能一眼看出这是哪一块的题。
4.4 中文对照、pdf 翻译与 pdf 转 word 在复习里的正确用法
非英语母语的备考者常想用 pdf 翻译工具把整份题库转成中文。我的建议是只翻解释段,题干和选项保留原文。原因是 AI-900 的题干关键词本身就是考点,比如 grounding、token 这类词一旦被机翻成「接地」「令牌」,你在考场上看到原词反而反应不过来。
如果 PDF 排版实在太乱,可以先用 pdf 转 word 做一次人工校对,把错位的选项调回来,再回到 Python 流水线里重跑解析。这条路径比在解析脚本里硬凑正则要省时间,但要记住:word 只是校对中间产物,最终数据源仍然是结构化的 JSON 或 SQLite,别让手工修改的文档变成唯一副本。
5. 进阶:扫描版题库的歪斜纠偏、Markdown 转 PDF 与解析验证
到这一步,题库已经是可统计的数据了。剩下的问题集中在两类:PDF 本身质量差,以及怎么验证你的解析结果没跑偏。
5.1 扫描版题库的歪斜校正纠偏与 OCR 前处理
有些题库 PDF 是扫描件,extract_words()返回空列表,必须走 OCR。OCR 前先做歪斜校正纠偏,字符错误率会明显下降。
import cv2 import numpy as np img = cv2.imread("page_012.png", cv2.IMREAD_GRAYSCALE) # Otsu 二值化后取前景像素,用最小外接矩形估算倾斜角 _, bw = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) coords = np.column_stack(np.where(bw > 0)) angle = cv2.minAreaRect(coords)[-1] angle = -(90 + angle) if angle < -45 else -angle if abs(angle) > 1.0: # 小角度不值得转,转了反而更糊 h, w = img.shape M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) img = cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) cv2.imwrite("page_012_fixed.png", img)abs(angle) > 1.0这个门槛是有意加的:亚像素级的倾斜 OCR 引擎自己能处理,强行旋转引入的插值模糊反而会伤害识别率。borderMode用BORDER_REPLICATE是为了不让旋转后的黑边被当成文字块。
5.2 用 pandoc 把错题 Markdown 转成带中文字体的 PDF
第 4 章导出的 Markdown 如果想打印,用 pandoc 走 XeLaTeX 通道,中文字体必须显式指定,否则中文会整段丢失。
pandoc ai900_wrong.md -o ai900_wrong.pdf \ --pdf-engine=xelatex \ -V CJKmainfont="Noto Sans CJK SC" \ -V geometry:margin=2cm \ -V fontsize=10pt习惯在编辑器里完成的,用 VSCode 的 Markdown PDF 插件也能一键转,区别是插件走 Chromium 渲染,代码块换行和表格宽度更接近浏览器效果,而 pandoc 走 LaTeX,长表格会自动分页。两种都行,选一种固定下来,别每次换工具导致排版对比失效。
5.3 解析正确率的抽样验证方法
最后一步别省。随机抽 20 道题,把 JSON 里的题干、选项、答案、解释四项和 PDF 原文逐条对照,重点核对多选答案的字母顺序和解释的归属。这一轮抽样如果发现超过 2 道有问题,说明不是偶发,回头检查ANSWER_RE和选项续行归属的逻辑,而不是手工改这 20 条。每次题库换版本,重跑一遍第 2.4 节的audit(),四项指标全部达标再进复习流程。
本文还有配套的精品资源,点击获取