一文讲清 CNN 为什么能“看懂”图像
从像素到语义:一文讲清 CNN 为什么能“看懂”图像
(底部附有html可以本地浏览器打开进行测评)
引言
如果把一张图片直接交给传统的神经网络,它看到的往往只是一长串数字。比如一张 28×28 的灰度图,输入模型时通常会被摊平成 784 个像素值。问题在于,这种“拉平成向量”的做法虽然方便计算,却几乎抹掉了图像最重要的信息:空间结构。原本相邻的像素、边缘的连续变化、局部纹理之间的关系,在展开之后都变得模糊了。
卷积神经网络,英文叫 Convolutional Neural Network,简称 CNN,正是为了解决这个问题而诞生的。它并不是简单地把图像当作数字表格来处理,而是尽量保留图像的二维结构,让模型在“看图”的过程中逐层提取边缘、纹理、形状,最后再组合成更抽象的语义概念。也正因为如此,CNN 长期以来一直是计算机视觉领域最核心、最经典的模型之一。
这篇文章不会把 CNN 写成一份术语堆砌的教材笔记,而是从“它为什么适合图像”这个问题出发,梳理它的核心结构、工作方式、优势与边界,以及它和 MLP、传统机器学习、Transformer 之间的关系。
什么是 CNN
CNN 本质上是一类专门处理网格结构数据的神经网络,而图像正是最典型的网格数据:宽度和高度组成二维平面,如果是彩色图像,还会多出颜色通道。和普通的全连接神经网络不同,CNN 不要求每个输入像素都和下一层的每个神经元相连,而是让网络先关注局部区域,再逐步扩大“视野”。
可以把它想象成一个会扫描图像的系统。它不会一开始就试图理解整张图,而是先用一个小窗口去看局部,比如看某个 3×3 或 5×5 的区域中有没有边缘、拐角或者纹理;然后再把这些局部特征组合起来,形成更高层的模式,比如眼睛、轮廓、轮胎、文字区域等;最终才完成分类、检测或分割等任务。
这种处理方式有两个非常重要的特点。第一,它保留了图像的空间结构。第二,它极大减少了参数数量。对于图像这种高维输入来说,这两点决定了 CNN 比普通 MLP 更适合视觉任务。
CNN 的核心结构与工作原理
CNN 的经典结构通常由卷积层、激活函数、池化层、全连接层组成。在很多实际模型中,还会加入归一化、Dropout 等机制,但核心思想主要围绕前面这几类模块展开。
卷积层:用小窗口寻找局部模式
卷积层是 CNN 的核心。它通过一个小矩阵在图像上滑动,这个小矩阵通常被称为卷积核、滤波器或 kernel。卷积核里的数值不是人工写死的,而是模型训练过程中自动学习到的参数。
卷积核每移动到一个位置,就会和当前位置覆盖的像素区域做一次加权计算,得到一个新的数值。整张图扫描完成后,就形成一张新的二维结果图,这张图通常叫特征图,也叫激活图。直观地说,一张特征图表示“某种特定模式在图像不同位置上的响应强度”。
如果一个卷积层有 32 个卷积核,那么它就会输出 32 张特征图,也就是输出深度为 32。这里有一个很容易忽略但非常关键的事实:输出深度不取决于输入图像有多大,而取决于这一层用了多少个滤波器。每个滤波器负责学习一种模式,因此滤波器越多,模型能够提取的特征种类通常也越丰富。
局部感受野:只看附近,而不是一口吃下整张图
CNN 之所以有效,一个重要原因是它采用了局部连接,而不是全连接。卷积核每次只关注输入中的一个小区域,这个局部区域就叫感受野。对图像来说,这是非常合理的,因为很多有意义的视觉模式本来就是局部出现的,比如边缘、角点、纹理、斑点、局部轮廓。
这种“先看局部,再逐渐整合”的方式,既符合图像的结构特性,也让网络能够分层学习。从浅层的边缘和纹理,到中层的形状片段,再到高层的物体部件和语义概念,CNN 的层次感很强。
参数共享:同一个滤波器在全图复用
卷积层还有一个经典优势,叫参数共享。一个 3×3 的卷积核,无论在图像左上角还是右下角使用,它的那 9 个权重都保持不变。也就是说,模型不是为每个位置单独学习一套参数,而是让同一个滤波器在整张图上重复使用。
这件事带来的收益极大。假设输入是一张很大的图像,如果使用全连接层,参数数量会随着输入尺寸爆炸增长;而卷积层的参数量只和卷积核尺寸、输入通道数、输出通道数有关,和图像宽高没有直接线性关系。参数更少,意味着更容易训练、更不容易过拟合,也更符合“同一种边缘在图像不同位置都应该被识别出来”的直觉。
激活函数:给网络加入非线性表达能力
如果卷积后只做线性运算,那么再叠很多层,本质上仍然是一个线性变换,表达能力非常有限。因此卷积层后通常会接激活函数。最常见的是 ReLU,它会把负数截断为 0,保留正数。
ReLU 的好处是简单、高效,并且在深层网络中训练稳定。它让网络能够学习复杂的非线性映射,从而真正捕捉图像中的复杂模式。
池化层:压缩空间尺寸,保留关键信息
池化层通常出现在卷积层之后,最常见的是最大池化。它会在一个小窗口内取最大值,比如 2×2 池化窗口配合步幅 2,就会把宽和高各缩小一半。假设输入是 28×28×32,经过一次这样的池化后,就会变成 14×14×32。
要注意的是,池化通常只改变空间尺寸,不改变深度。也就是说,它压缩的是“图有多大”,不是“有多少种特征”。
池化的作用一方面是降维,减少后续计算量;另一方面是让模型对小范围的位置变化更鲁棒。比如目标稍微平移一点,池化后的结果通常不会发生剧烈变化,这对于图像识别很重要。
全连接层:把特征映射成最终输出
经过多层卷积和池化之后,网络会得到一组高层特征图。此时常常需要把这些三维特征展开成一维向量,这一步叫 Flatten。比如一个 7×7×64 的特征体,展平后就是 3136 维向量。
接下来,这个向量会送入全连接层,用于完成最终的分类或回归任务。对于多分类问题,输出层常常使用 Softmax,它会把每个类别的得分转换成一个总和为 1 的概率分布,从而表示模型对各类别的置信度。
Dropout:抑制过拟合的常见手段
在靠近输出端的全连接部分,常常会加入 Dropout。它的做法是在训练时随机“关闭”一部分神经元,迫使网络不要过度依赖某几个固定连接。这样可以减少神经元之间的共适应,提升模型泛化能力。
很多 CNN 结构里,Dropout 更常放在 Flatten 之后、全连接层之间,而不是随意插在每个卷积层里。
CNN 为什么适合处理图像
CNN 对图像友好,不是偶然,而是它的设计本身就在贴合图像的特点。
首先,图像有明显的局部相关性。相邻像素往往共同构成边缘、角点、纹理,局部区域里隐藏着最基础的视觉信息。CNN 用局部感受野处理图像,天然符合这一事实。
其次,图像中的模式具有平移重复性。一个边缘不管出现在左上角还是右下角,本质上仍然是边缘。CNN 通过参数共享,让同一个卷积核可以在全图复用,既节省参数,也更符合视觉规律。
再次,视觉理解本身就是分层的。人类识别物体时,也不是先看整张图再硬算,而是从局部细节逐渐拼出整体印象。CNN 的多层堆叠正好实现了这种从低级特征到高级语义的逐层抽象。
相比之下,如果用传统的 MLP 直接处理图像,通常需要先把图像展平。这样做虽然简单,但会打乱二维结构,而且参数量巨大。以大尺寸图像为例,输入维度一旦变高,全连接层的参数数量就会迅速膨胀,训练成本和过拟合风险都会大幅上升。这也是 CNN 长期占据视觉主流的重要原因。
CNN 的典型应用场景
CNN 最经典的应用是图像分类,也就是判断一张图属于哪一类。例如识别手写数字、猫狗分类、工业零件分类、医学影像分类等。这类任务中,CNN 会把整张图编码成一组语义特征,最后输出类别概率。
第二类常见任务是目标检测。这里不只是判断“图里有没有目标”,还要定位目标出现的位置。很多经典检测模型,例如早期的 R-CNN 系列、YOLO 中的卷积骨干网络,都大量依赖 CNN 提取视觉特征。
第三类是语义分割和实例分割。它要求模型对图像中的每个像素做判断,比如区分道路、行人、天空、建筑。这类任务对空间结构要求很高,而 CNN 在提取局部和多尺度特征方面表现稳定,因此长期是分割模型的重要基础。
此外,CNN 还广泛用于人脸识别、视频分析、自动驾驶感知、遥感图像处理、OCR 文字识别、医学 CT/MRI 分析等场景。即便在一些新模型架构中,卷积也没有完全消失,而是继续作为高效的局部特征提取模块存在。
CNN 的优势与局限
CNN 的最大优势在于它把“结构归纳偏置”做得很好。它默认世界中的视觉信息具有局部性和空间层次,并据此设计出局部连接、参数共享和层次特征抽取机制。这样的偏置让它在图像任务上训练效率高、参数利用率高,也更容易在中小规模数据集上取得不错效果。
另一个优势是可解释性相对较强。虽然深度模型整体仍然是复杂系统,但 CNN 的每一层通常能对应某种视觉处理功能:浅层学边缘,中层学纹理和局部形状,高层学语义组合。相比一些完全黑盒的高维映射,这种结构更容易理解和调试。
不过,CNN 也有明显局限。第一,它更擅长建模局部关系,而对长距离依赖的捕捉相对弱一些。虽然可以通过加深网络、扩大感受野或引入空洞卷积等方式缓解,但从结构上说,它天然更偏向局部模式。
第二,池化和步幅卷积虽然能降维,但也可能带来部分细节损失。在需要极高空间精度的任务里,这种压缩未必总是有利。
第三,CNN 的性能往往依赖任务设计和网络结构调优。例如卷积核大小、通道数、层数、步幅、归一化方式等,都会影响结果。和一些更统一的新架构相比,它在工程上可能显得更“讲经验”。
CNN 与其他模型的对比
如果和传统机器学习相比,CNN 最大的不同在于它能自动学习特征。传统图像任务中,工程师通常需要先手工设计特征,比如边缘描述子、纹理统计量、SIFT、HOG 等,然后再交给 SVM、决策树等分类器。CNN 则把“特征提取”和“任务预测”合并到一个端到端系统里,减少了人工特征工程的负担。
如果和 MLP 相比,CNN 的优势更直接。MLP 对输入结构几乎没有假设,因此在图像上通常需要展平输入,导致空间信息破坏、参数量激增。CNN 则通过卷积保留二维结构,通过参数共享显著压缩模型规模,因此更适合视觉任务。可以说,CNN 的成功很大程度上正是因为它比 MLP 更懂图像。
至于和 Transformer 或 ViT 的比较,这是当前视觉领域最值得讨论的话题之一。Vision Transformer 把图像切成若干 patch,再用自注意力机制建模全局关系。它的优点是更容易捕获远距离依赖,在大规模数据和算力条件下往往表现很强。但 ViT 对数据量和训练策略通常更敏感,缺少 CNN 那种天然的局部归纳偏置。
因此,在数据规模有限、部署效率重要、任务更强调局部纹理特征的情况下,CNN 仍然非常有竞争力;而在超大规模预训练、多模态融合、全局建模能力特别重要的场景中,Transformer 系模型则越来越强。现实中,两者并不是简单替代关系,很多新架构也在尝试把卷积和注意力机制结合起来,兼顾局部建模与全局建模。
总结
CNN 之所以成为计算机视觉的经典基石,不是因为它“神奇”,而是因为它非常聪明地利用了图像本身的结构特征。局部感受野让它先理解局部细节,参数共享让它高效地在全图复用模式,池化帮助它压缩信息并增强鲁棒性,多层堆叠则让它能够从边缘一路抽象到语义。
从本质上说,CNN 解决的是一个非常关键的问题:如何让神经网络不仅“接收像素”,还真正“理解图像中的结构”。即使今天 Transformer 在视觉领域不断扩张,CNN 依然没有过时。它仍然是理解现代视觉模型的起点,也是很多实际系统中最稳健、最高效的选择之一。
如果你刚开始学习深度学习,那么 CNN 几乎是绕不过去的一站。理解它,不只是为了会搭一个卷积网络,更是为了真正理解:机器是怎样一步步从像素走向语义的。
、、、html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CNN Chapter 3 — Comprehensive Evaluation</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f0; color: #1a1a18; line-height: 1.6; }
.header { background: #fff; border-bottom: 1px solid #e5e3db; padding: 16px 32px; position: sticky; top: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.header h1 { font-size: 16px; font-weight: 600; color: #1a1a18; }
.header .sub { font-size: 12px; color: #888; }
.nav { background: #fff; border-bottom: 1px solid #e5e3db; padding: 0 32px; display: flex; gap: 0; overflow-x: auto; }
.nav-btn { padding: 12px 20px; font-size: 13px; font-weight: 500; border: none; border-bottom: 2px solid transparent; background: transparent; cursor: pointer; color: #666; white-space: nowrap; transition: all .18s; }
.nav-btn:hover { color: #1a1a18; background: #f9f9f7; }
.nav-btn.active { color: #185fa5; border-bottom-color: #185fa5; }
.page { display: none; max-width: 820px; margin: 0 auto; padding: 32px 24px 60px; }
.page.active { display: block; }
/* Dashboard */
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 28px; }
.stat-card { background: #fff; border: 1px solid #e5e3db; border-radius: 10px; padding: 16px 18px; }
.stat-label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
.stat-val { font-size: 28px; font-weight: 600; color: #1a1a18; }
.stat-max { font-size: 12px; color: #aaa; margin-top: 2px; }
.desc-card { background: #fff; border: 1px solid #e5e3db; border-radius: 10px; padding: 20px 22px; margin-bottom: 20px; }
.desc-card h2 { font-size: 17px; font-weight: 600; margin-bottom: 6px; }
.desc-card p { font-size: 13px; color: #555; }
.progress-label { font-size: 12px; color: #888; margin-bottom: 6px; }
.pbar-bg { height: 8px; background: #e5e3db; border-radius: 4px; overflow: hidden; margin-bottom: 6px; }
.pbar-fill { height: 100%; background: #185fa5; border-radius: 4px; transition: width .4s ease; }
.pbar-text { font-size: 12px; color: #888; }
.module-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 20px; }
.module-card { background: #fff; border: 1px solid #e5e3db; border-radius: 10px; padding: 16px 18px; cursor: pointer; transition: border-color .15s, box-shadow .15s; }
.module-card:hover { border-color: #185fa5; box-shadow: 0 0 0 3px rgba(24,95,165,.08); }
.module-card h3 { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.module-card p { font-size: 12px; color: #888; }
.module-card .badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; margin-bottom: 6px; }
.badge-blue { background: #e6f1fb; color: #185fa5; }
.badge-green { background: #eaf3de; color: #3b6d11; }
.badge-amber { background: #faeeda; color: #854f0b; }
.badge-purple { background: #eeedfe; color: #534ab7; }
/* Sections */
.section-header { margin-bottom: 20px; }
.section-header h2 { font-size: 19px; font-weight: 600; margin-bottom: 4px; }
.section-header p { font-size: 13px; color: #666; }
/* Questions */
.q-block { background: #fff; border: 1px solid #e5e3db; border-radius: 10px; padding: 20px 22px; margin-bottom: 14px; }
.q-tag { font-size: 11px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px; }
.q-text { font-size: 14px; font-weight: 500; color: #1a1a18; margin-bottom: 14px; line-height: 1.55; }
.opt { display: flex; align-items: flex-start; gap: 10px; padding: 10px 13px; border: 1px solid #e5e3db; border-radius: 8px; margin-bottom: 7px; cursor: pointer; font-size: 13px; color: #444; transition: all .14s; user-select: none; }
.opt:hover { border-color: #b5d4f4; background: #f4f9ff; }
.opt.selected { border-color: #185fa5; background: #e6f1fb; color: #185fa5; }
.opt.correct { border-color: #3b6d11; background: #eaf3de; color: #3b6d11; font-weight: 500; }
.opt.wrong { border-color: #a32d2d; background: #fcebeb; color: #a32d2d; }
.opt.reveal { border-color: #3b6d11; background: #eaf3de; color: #3b6d11; }
.opt-circle { width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; flex-shrink: 0; margin-top: 1px; display: flex; align-items: center; justify-content: center; }
.opt-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; display: none; }
.opt.selected .opt-dot, .opt.correct .opt-dot, .opt.wrong .opt-dot, .opt.reveal .opt-dot { display: block; }
.feedback { display: none; margin-top: 12px; padding: 10px 13px; border-radius: 8px; font-size: 12px; line-height: 1.6; }
.feedback.show { display: block; }
.feedback.ok { background: #eaf3de; color: #3b6d11; border-left: 3px solid #3b6d11; }
.feedback.bad { background: #fcebeb; color: #a32d2d; border-left: 3px solid #a32d2d; }
/* Fill blanks */
.fill-block { background: #fff; border: 1px solid #e5e3db; border-radius: 10px; padding: 20px 22px; margin-bottom: 14px; }
.fill-sentence { font-size: 14px; color: #1a1a18; line-height: 2.2; }
.blank-input { display: inline-block; min-width: 160px; padding: 4px 10px; border: 1.5px solid #b5d4f4; border-radius: 6px; font-size: 13px; font-family: inherit; color: #1a1a18; background: #fff; outline: none; transition: border-color .15s; }
.blank-input:focus { border-color: #185fa5; }
.blank-input.ok { border-color: #3b6d11; background: #eaf3de; color: #3b6d11; }
.blank-input.bad { border-color: #a32d2d; background: #fcebeb; color: #a32d2d; }
/* Matching */
.match-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 14px; }
.match-col-head { font-size: 11px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px; }
.match-item { padding: 10px 13px; border: 1px solid #e5e3db; border-radius: 8px; margin-bottom: 7px; cursor: pointer; font-size: 13px; color: #444; transition: all .14s; background: #fff; line-height: 1.45; }
.match-item:hover { border-color: #b5d4f4; background: #f4f9ff; }
.match-item.selected { border-color: #185fa5; background: #e6f1fb; color: #185fa5; font-weight: 500; }
.match-item.paired { border-color: #888; background: #f5f5f0; color: #888; cursor: default; }
.match-item.correct { border-color: #3b6d11; background: #eaf3de; color: #3b6d11; cursor: default; font-weight: 500; }
.match-item.wrong { border-color: #a32d2d; background: #fcebeb; color: #a32d2d; cursor: default; }
.match-status { font-size: 13px; color: #666; background: #f5f5f0; border-radius: 8px; padding: 10px 13px; margin-bottom: 12px; }
/* Calc lab */
.calc-block { background: #fff; border: 1px solid #e5e3db; border-radius: 10px; padding: 20px 22px; margin-bottom: 14px; }
.calc-title { font-size: 14px; font-weight: 600; color: #1a1a18; margin-bottom: 6px; }
.calc-desc { font-size: 13px; color: #555; margin-bottom: 14px; line-height: 1.6; }
.calc-formula { font-family: 'Courier New', monospace; font-size: 12px; background: #f5f5f0; border: 1px solid #e5e3db; border-radius: 6px; padding: 8px 12px; margin-bottom: 12px; color: #444; }
.calc-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.calc-label { font-size: 13px; color: #555; }
.calc-input { width: 110px; padding: 7px 12px; border: 1.5px solid #b5d4f4; border-radius: 6px; font-size: 14px; font-family: 'Courier New', monospace; font-weight: 600; color: #1a1a18; background: #fff; outline: none; }
.calc-input:focus { border-color: #185fa5; }
.calc-input.ok { border-color: #3b6d11; background: #eaf3de; color: #3b6d11; }
.calc-input.bad { border-color: #a32d2d; background: #fcebeb; color: #a32d2d; }
.hint-btn { padding: 5px 12px; border: 1px solid #e5e3db; border-radius: 6px; font-size: 12px; cursor: pointer; background: #fff; color: #888; transition: all .14s; }
.hint-btn:hover { background: #faeeda; border-color: #854f0b; color: #854f0b; }
.hint-box { display: none; margin-top: 10px; padding: 9px 12px; background: #faeeda; border-radius: 8px; font-size: 12px; color: #633806; line-height: 1.6; }
.calc-result { display: none; margin-top: 10px; padding: 9px 12px; border-radius: 8px; font-size: 12px; line-height: 1.6; }
.calc-result.ok { background: #eaf3de; color: #3b6d11; }
.calc-result.bad { background: #fcebeb; color: #a32d2d; }
/* Results */
.result-hero { border-radius: 12px; padding: 28px 24px; text-align: center; margin-bottom: 24px; }
.result-hero.great { background: #eaf3de; color: #27500a; }
.result-hero.ok { background: #faeeda; color: #633806; }
.result-hero.low { background: #fcebeb; color: #791f1f; }
.result-hero .score { font-size: 52px; font-weight: 700; margin-bottom: 6px; }
.result-hero .msg { font-size: 15px; }
.result-section { background: #fff; border: 1px solid #e5e3db; border-radius: 10px; padding: 20px 22px; margin-bottom: 14px; }
.result-section h3 { font-size: 14px; font-weight: 600; margin-bottom: 14px; }
.topic-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.topic-name { font-size: 13px; color: #555; min-width: 200px; flex-shrink: 0; }
.topic-bar-bg { flex: 1; height: 8px; background: #e5e3db; border-radius: 4px; overflow: hidden; }
.topic-bar-fill { height: 100%; border-radius: 4px; transition: width .5s; }
.topic-pct { font-size: 12px; color: #888; min-width: 38px; text-align: right; }
/* Buttons */
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; }
.btn { padding: 9px 20px; border: 1px solid #d0cec5; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; background: #fff; color: #444; transition: all .15s; }
.btn:hover { background: #f5f5f0; border-color: #b5b3a8; }
.btn-primary { background: #185fa5; border-color: #185fa5; color: #fff; }
.btn-primary:hover { background: #0c447c; border-color: #0c447c; }
.btn-green { background: #3b6d11; border-color: #3b6d11; color: #fff; }
.btn-green:hover { background: #27500a; }
.btn-sm { padding: 6px 14px; font-size: 12px; }
.divider { height: 1px; background: #e5e3db; margin: 24px 0; }
@media (max-width: 600px) {
.stat-grid { grid-template-columns: repeat(2,1fr); }
.module-grid { grid-template-columns: 1fr; }
.match-columns { grid-template-columns: 1fr; }
.page { padding: 20px 14px 60px; }
.nav-btn { padding: 10px 14px; font-size: 12px; }
}
</style>
</head>
<body>
<div class="header">
<div>
<div class="header h1" style="font-size:16px;font-weight:600">Deep Learning for Vision Systems</div>
<div class="sub">Chapter 3 — Convolutional Neural Networks · Comprehensive Evaluation</div>
</div>
<div style="font-size:13px;color:#888">32 points total</div>
</div>
<div class="nav">
<button class="nav-btn active" onclick="goTo('dashboard',this)">Dashboard</button>
<button class="nav-btn" onclick="goTo('mcq',this)">MCQ — 12 questions</button>
<button class="nav-btn" onclick="goTo('fill',this)">Fill in the blanks</button>
<button class="nav-btn" onclick="goTo('match',this)">Matching</button>
<button class="nav-btn" onclick="goTo('calc',this)">Calculation lab</button>
<button class="nav-btn" onclick="goTo('results',this)">Results</button>
</div>
<!-- ═══════════════════════════════ DASHBOARD ═══════════════════════════════ -->
<div id="page-dashboard" class="page active">
<div class="stat-grid">
<div class="stat-card"><div class="stat-label">MCQ score</div><div class="stat-val" id="d-mcq">—</div><div class="stat-max">of 12 pts</div></div>
<div class="stat-card"><div class="stat-label">Fill-in score</div><div class="stat-val" id="d-fill">—</div><div class="stat-max">of 7 pts</div></div>
<div class="stat-card"><div class="stat-label">Matching score</div><div class="stat-val" id="d-match">—</div><div class="stat-max">of 8 pts</div></div>
<div class="stat-card"><div class="stat-label">Calc score</div><div class="stat-val" id="d-calc">—</div><div class="stat-max">of 5 pts</div></div>
</div>
<div class="desc-card">
<h2>Chapter 3 — Comprehensive Evaluation</h2>
<p>32 points total across 4 assessment types. Complete all modules, then view your Results breakdown.</p>
<div class="divider"></div>
<div class="progress-label">Overall progress</div>
<div class="pbar-bg"><div class="pbar-fill" id="d-pbar" style="width:0%"></div></div>
<div class="pbar-text" id="d-pbar-text">0 / 32 pts</div>
</div>
<div class="module-grid">
<div class="module-card" onclick="goTo('mcq',null)">
<span class="badge badge-blue">12 pts</span>
<h3>MCQ — 12 questions</h3>
<p>Convolution, pooling, architecture, dropout, parameters</p>
</div>
<div class="module-card" onclick="goTo('fill',null)">
<span class="badge badge-green">7 pts</span>
<h3>Fill in the blanks</h3>
<p>Key terms and definitions from the chapter</p>
</div>
<div class="module-card" onclick="goTo('match',null)">
<span class="badge badge-amber">8 pts</span>
<h3>Matching — 8 pairs</h3>
<p>Match concepts to their correct definitions</p>
</div>
<div class="module-card" onclick="goTo('calc',null)">
<span class="badge badge-purple">5 pts</span>
<h3>Calculation lab — 5 problems</h3>
<p>Compute parameters, output shapes, and flattened sizes</p>
</div>
</div>
</div>
<!-- ═══════════════════════════════ MCQ ═══════════════════════════════ -->
<div id="page-mcq" class="page">
<div class="section-header">
<h2>Multiple Choice — 12 Questions</h2>
<p>Select the best answer for each question. Submit when done to see your score and explanations.</p>
</div>
<div class="pbar-bg" style="margin-bottom:20px"><div class="pbar-fill" id="mcq-pbar" style="width:0%"></div></div>
<!-- Q1 -->
<div class="q-block">
<div class="q-tag">Question 1 of 12</div>
<div class="q-text">What is the primary reason MLPs struggle with image data compared to CNNs?</div>
<div class="opt" onclick="selectOpt(this,1,0)"><div class="opt-circle"><div class="opt-dot"></div></div>They require too many training epochs</div>
<div class="opt" onclick="selectOpt(this,1,1)"><div class="opt-circle"><div class="opt-dot"></div></div>Flattening the image to a 1D vector destroys spatial structure between pixels</div>
<div class="opt" onclick="selectOpt(this,1,2)"><div class="opt-circle"><div class="opt-dot"></div></div>They cannot use activation functions like ReLU</div>
<div class="opt" onclick="selectOpt(this,1,3)"><div class="opt-circle"><div class="opt-dot"></div></div>They only support binary classification tasks</div>
<div class="feedback" id="fb1">Flattening a 2D image matrix to a 1D vector destroys pixel relationships — the MLP no longer knows which pixels are spatial neighbors. A 28×28 image's 784 values lose all 2D structure.</div>
</div>
<!-- Q2 -->
<div class="q-block">
<div class="q-tag">Question 2 of 12</div>
<div class="q-text">In CNN terminology, what is a "feature map" (also called an activation map)?</div>
<div class="opt" onclick="selectOpt(this,2,0)"><div class="opt-circle"><div class="opt-dot"></div></div>The raw flattened input image vector</div>
<div class="opt" onclick="selectOpt(this,2,1)"><div class="opt-circle"><div class="opt-dot"></div></div>The weight matrix of the fully connected output layer</div>
<div class="opt" onclick="selectOpt(this,2,2)"><div class="opt-circle"><div class="opt-dot"></div></div>The output produced by applying one filter to the previous layer</div>
<div class="opt" onclick="selectOpt(this,2,3)"><div class="opt-circle"><div class="opt-dot"></div></div>A visualization of the final class probabilities</div>
<div class="feedback" id="fb2">A feature map is the result of convolving one filter across the entire input. It spatially maps where that filter's particular feature (e.g., an edge or curve) activates. Each filter produces exactly one feature map.</div>
</div>
<!-- Q3 -->
<div class="q-block">
<div class="q-tag">Question 3 of 12</div>
<div class="q-text">What do the values inside a convolutional kernel (filter) represent?</div>
<div class="opt" onclick="selectOpt(this,3,0)"><div class="opt-circle"><div class="opt-dot"></div></div>Pixel values sampled directly from the training images</div>
<div class="opt" onclick="selectOpt(this,3,1)"><div class="opt-circle"><div class="opt-dot"></div></div>Learnable weights initialized randomly and updated during backpropagation</div>
<div class="opt" onclick="selectOpt(this,3,2)"><div class="opt-circle"><div class="opt-dot"></div></div>Fixed Gaussian blur coefficients defined before training</div>
<div class="opt" onclick="selectOpt(this,3,3)"><div class="opt-circle"><div class="opt-dot"></div></div>Output class probabilities from the softmax layer</div>
<div class="feedback" id="fb3">Kernel values ARE the weights — randomly initialized and learned by the network during training exactly like weights in an MLP. They are updated via gradient descent and backpropagation.</div>
</div>
<!-- Q4 -->
<div class="q-block">
<div class="q-tag">Question 4 of 12</div>
<div class="q-text">Which padding setting preserves the width and height of the image after a Conv2D layer with stride=1?</div>
<div class="opt" onclick="selectOpt(this,4,0)"><div class="opt-circle"><div class="opt-dot"></div></div>padding="valid"</div>
<div class="opt" onclick="selectOpt(this,4,1)"><div class="opt-circle"><div class="opt-dot"></div></div>padding="zeros"</div>
<div class="opt" onclick="selectOpt(this,4,2)"><div class="opt-circle"><div class="opt-dot"></div></div>padding="same"</div>
<div class="opt" onclick="selectOpt(this,4,3)"><div class="opt-circle"><div class="opt-dot"></div></div>padding="full"</div>
<div class="feedback" id="fb4">"same" padding adds zeros around the image border so the spatial output dimensions (W×H) match the input. "valid" means no padding, causing the output to shrink.</div>
</div>
<!-- Q5 -->
<div class="q-block">
<div class="q-tag">Question 5 of 12</div>
<div class="q-text">A 2×2 max pooling layer (stride=2) is applied to a 28×28×32 feature volume. What is the output shape?</div>
<div class="opt" onclick="selectOpt(this,5,0)"><div class="opt-circle"><div class="opt-dot"></div></div>28×28×32</div>
<div class="opt" onclick="selectOpt(this,5,1)"><div class="opt-circle"><div class="opt-dot"></div></div>14×14×32</div>
<div class="opt" onclick="selectOpt(this,5,2)"><div class="opt-circle"><div class="opt-dot"></div></div>14×14×16</div>
<div class="opt" onclick="selectOpt(this,5,3)"><div class="opt-circle"><div class="opt-dot"></div></div>7×7×32</div>
<div class="feedback" id="fb5">Max pooling halves W and H (28→14, 28→14) but leaves depth unchanged (32 stays 32). Output: 14×14×32. Pooling never changes depth — it only compresses the spatial dimensions.</div>
</div>
<!-- Q6 -->
<div class="q-block">
<div class="q-tag">Question 6 of 12</div>
<div class="q-text">Why do pooling layers show Param # = 0 in model.summary()?</div>
<div class="opt" onclick="selectOpt(this,6,0)"><div class="opt-circle"><div class="opt-dot"></div></div>They use pretrained weights from ImageNet that aren't counted</div>
<div class="opt" onclick="selectOpt(this,6,1)"><div class="opt-circle"><div class="opt-dot"></div></div>They apply a fixed statistical operation (max/average) with no learnable weights</div>
<div class="opt" onclick="selectOpt(this,6,2)"><div class="opt-circle"><div class="opt-dot"></div></div>They are skipped entirely during backpropagation</div>
<div class="opt" onclick="selectOpt(this,6,3)"><div class="opt-circle"><div class="opt-dot"></div></div>They share weights with the preceding Conv layer</div>
<div class="feedback" id="fb6">Pooling layers simply pick the max (or average) value in each window — there are no weights to learn or update. Nothing is trained, so Param # = 0. The same is true for Flatten layers.</div>
</div>
<!-- Q7 -->
<div class="q-block">
<div class="q-tag">Question 7 of 12</div>
<div class="q-text">Overfitting in a neural network is best described as:</div>
<div class="opt" onclick="selectOpt(this,7,0)"><div class="opt-circle"><div class="opt-dot"></div></div>The model is too simple and fails to fit the training data</div>
<div class="opt" onclick="selectOpt(this,7,1)"><div class="opt-circle"><div class="opt-dot"></div></div>The model memorizes training data and fails to generalize to unseen examples</div>
<div class="opt" onclick="selectOpt(this,7,2)"><div class="opt-circle"><div class="opt-dot"></div></div>The training loss is always decreasing monotonically</div>
<div class="opt" onclick="selectOpt(this,7,3)"><div class="opt-circle"><div class="opt-dot"></div></div>The model uses too few convolutional filters</div>
<div class="feedback" id="fb7">Overfitting = very high training accuracy but poor test/validation accuracy. The model has memorized specific patterns in the training examples rather than learning generalizable features. The fix is dropout, regularization, or more data.</div>
</div>
<!-- Q8 -->
<div class="q-block">
<div class="q-tag">Question 8 of 12</div>
<div class="q-text">What is the correct Keras activation function for a multi-class (10 class) output layer?</div>
<div class="opt" onclick="selectOpt(this,8,0)"><div class="opt-circle"><div class="opt-dot"></div></div>relu</div>
<div class="opt" onclick="selectOpt(this,8,1)"><div class="opt-circle"><div class="opt-dot"></div></div>sigmoid</div>
<div class="opt" onclick="selectOpt(this,8,2)"><div class="opt-circle"><div class="opt-dot"></div></div>tanh</div>
<div class="opt" onclick="selectOpt(this,8,3)"><div class="opt-circle"><div class="opt-dot"></div></div>softmax</div>
<div class="feedback" id="fb8">Softmax outputs a probability distribution over all classes that sums to 1. It is the standard choice for multi-class classification output layers. ReLU is used in hidden layers, sigmoid for binary classification.</div>
</div>
<!-- Q9 -->
<div class="q-block">
<div class="q-tag">Question 9 of 12</div>
<div class="q-text">How does output depth change when a Conv2D(64, 3×3) layer is applied, regardless of input depth?</div>
<div class="opt" onclick="selectOpt(this,9,0)"><div class="opt-circle"><div class="opt-dot"></div></div>Output depth becomes 64</div>
<div class="opt" onclick="selectOpt(this,9,1)"><div class="opt-circle"><div class="opt-dot"></div></div>Output depth doubles the input depth</div>
<div class="opt" onclick="selectOpt(this,9,2)"><div class="opt-circle"><div class="opt-dot"></div></div>Output depth equals kernel_size × input_depth</div>
<div class="opt" onclick="selectOpt(this,9,3)"><div class="opt-circle"><div class="opt-dot"></div></div>Output depth remains the same as input depth</div>
<div class="feedback" id="fb9">Each filter produces exactly one feature map. With 64 filters, output depth is always 64 — independent of the input depth. This is why adding more filters deepens the layer.</div>
</div>
<!-- Q10 -->
<div class="q-block">
<div class="q-tag">Question 10 of 12</div>
<div class="q-text">Why are convolutional (locally connected) layers preferred over fully connected layers for feature extraction from images?</div>
<div class="opt" onclick="selectOpt(this,10,0)"><div class="opt-circle"><div class="opt-dot"></div></div>They are always faster to train on any hardware</div>
<div class="opt" onclick="selectOpt(this,10,1)"><div class="opt-circle"><div class="opt-dot"></div></div>They use far fewer parameters due to weight sharing (same kernel across all positions)</div>
<div class="opt" onclick="selectOpt(this,10,2)"><div class="opt-circle"><div class="opt-dot"></div></div>They always achieve higher accuracy than MLPs</div>
<div class="opt" onclick="selectOpt(this,10,3)"><div class="opt-circle"><div class="opt-dot"></div></div>They eliminate the need for activation functions</div>
<div class="feedback" id="fb10">A 3×3 kernel has only 9 weights regardless of image size, and those same 9 weights are shared across all positions in the image. A fully connected layer on a 1000×1000 image needs 1 billion weights per neuron in the first layer.</div>
</div>
<!-- Q11 -->
<div class="q-block">
<div class="q-tag">Question 11 of 12</div>
<div class="q-text">Where should dropout layers be placed in a CNN architecture?</div>
<div class="opt" onclick="selectOpt(this,11,0)"><div class="opt-circle"><div class="opt-dot"></div></div>After each convolutional layer, before pooling</div>
<div class="opt" onclick="selectOpt(this,11,1)"><div class="opt-circle"><div class="opt-dot"></div></div>Before the input image is fed to the first layer</div>
<div class="opt" onclick="selectOpt(this,11,2)"><div class="opt-circle"><div class="opt-dot"></div></div>Between fully connected (dense) layers, after the flatten step</div>
<div class="opt" onclick="selectOpt(this,11,3)"><div class="opt-circle"><div class="opt-dot"></div></div>Inside each pooling layer</div>
<div class="feedback" id="fb11">Dropout is placed between the FC (dense) layers after Flatten. The architecture pattern is: ...CONV→POOL→FLATTEN→Dropout(0.3)→FC→Dropout(0.5)→Output. Its effect in convolutional layers is not well studied.</div>
</div>
<!-- Q12 -->
<div class="q-block">
<div class="q-tag">Question 12 of 12</div>
<div class="q-text">After two CONV→POOL blocks (each 2×2 pool) on a 28×28 input with 64 filters in the last conv, what is the flattened vector size?</div>
<div class="opt" onclick="selectOpt(this,12,0)"><div class="opt-circle"><div class="opt-dot"></div></div>784</div>
<div class="opt" onclick="selectOpt(this,12,1)"><div class="opt-circle"><div class="opt-dot"></div></div>1,568</div>
<div class="opt" onclick="selectOpt(this,12,2)"><div class="opt-circle"><div class="opt-dot"></div></div>3,136</div>
<div class="opt" onclick="selectOpt(this,12,3)"><div class="opt-circle"><div class="opt-dot"></div></div>12,544</div>
<div class="feedback" id="fb12">After pool 1: 28→14. After pool 2: 14→7. Final volume: 7×7×64. Flatten: 7 × 7 × 64 = 3,136. This is the input size of the first fully connected layer.</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="gradeMCQ()">Submit answers</button>
<button class="btn" onclick="resetMCQ()">Reset</button>
</div>
<div id="mcq-score-msg" style="margin-top:14px;font-size:14px;font-weight:500;color:#185fa5;display:none"></div>
</div>
<!-- ═══════════════════════════════ FILL ═══════════════════════════════ -->
<div id="page-fill" class="page">
<div class="section-header">
<h2>Fill in the Blanks — 7 Items</h2>
<p>Type the correct term in each blank. Capitalization does not matter.</p>
</div>
<div class="fill-block">
<div class="q-tag">Blank 1</div>
<div class="fill-sentence">The process of converting a 2D image matrix into a 1D vector before feeding it to an MLP is called image <input class="blank-input" id="f1" placeholder="___________">.</div>
<div class="feedback" id="ffb1"></div>
</div>
<div class="fill-block">
<div class="q-tag">Blank 2</div>
<div class="fill-sentence">A convolutional filter is also called a <input class="blank-input" id="f2" placeholder="___________">, and its values are the learnable weights of the network.</div>
<div class="feedback" id="ffb2"></div>
</div>
<div class="fill-block">
<div class="q-tag">Blank 3</div>
<div class="fill-sentence">The local area of the input image that a convolutional filter processes at each sliding position is called the <input class="blank-input" id="f3" placeholder="___________">.</div>
<div class="feedback" id="ffb3"></div>
</div>
<div class="fill-block">
<div class="q-tag">Blank 4</div>
<div class="fill-sentence">Max pooling reduces image width and height, but leaves the layer's <input class="blank-input" id="f4" placeholder="___________"> unchanged.</div>
<div class="feedback" id="ffb4"></div>
</div>
<div class="fill-block">
<div class="q-tag">Blank 5</div>
<div class="fill-sentence">Adding rows and columns of zeros around the image border before convolution is called <input class="blank-input" id="f5" placeholder="___________">.</div>
<div class="feedback" id="ffb5"></div>
</div>
<div class="fill-block">
<div class="q-tag">Blank 6</div>
<div class="fill-sentence">Dropout prevents neurons from developing <input class="blank-input" id="f6" placeholder="___________"> among each other, which leads to overfitting of training data.</div>
<div class="feedback" id="ffb6"></div>
</div>
<div class="fill-block">
<div class="q-tag">Blank 7</div>
<div class="fill-sentence">In CNN architecture text notation such as "INPUT→CONV→POOL→DO→FC→SOFTMAX", DO stands for <input class="blank-input" id="f7" placeholder="___________"> layer.</div>
<div class="feedback" id="ffb7"></div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="gradeFill()">Check answers</button>
<button class="btn" onclick="resetFill()">Reset</button>
</div>
<div id="fill-score-msg" style="margin-top:14px;font-size:14px;font-weight:500;color:#185fa5;display:none"></div>
</div>
<!-- ═══════════════════════════════ MATCHING ═══════════════════════════════ -->
<div id="page-match" class="page">
<div class="section-header">
<h2>Matching — 8 Pairs</h2>
<p>Click a term on the left, then click its matching definition on the right. Matched pairs will be highlighted. Submit when all 8 are paired.</p>
</div>
<div class="match-status" id="match-status">0 / 8 pairs matched. Click a term to start.</div>
<div class="match-columns">
<div>
<div class="match-col-head">Terms</div>
<div class="match-item" id="mt0" onclick="clickTerm(0)">Feature map</div>
<div class="match-item" id="mt1" onclick="clickTerm(1)">Receptive field</div>
<div class="match-item" id="mt2" onclick="clickTerm(2)">Stride</div>
<div class="match-item" id="mt3" onclick="clickTerm(3)">Max pooling</div>
<div class="match-item" id="mt4" onclick="clickTerm(4)">Kernel</div>
<div class="match-item" id="mt5" onclick="clickTerm(5)">Dropout layer</div>
<div class="match-item" id="mt6" onclick="clickTerm(6)">Softmax</div>
<div class="match-item" id="mt7" onclick="clickTerm(7)">Flatten</div>
</div>
<div>
<div class="match-col-head">Definitions</div>
<div class="match-item" id="md0" onclick="clickDef(0)">Output of applying one filter to the previous layer; maps where a feature is detected</div>
<div class="match-item" id="md1" onclick="clickDef(1)">The local image region that a filter processes at each position as it slides</div>
<div class="match-item" id="md2" onclick="clickDef(2)">Controls how many pixels the filter jumps per step during convolution</div>
<div class="match-item" id="md3" onclick="clickDef(3)">Downsamples by selecting the maximum value within each window; no learnable weights</div>
<div class="match-item" id="md4" onclick="clickDef(4)">The weight matrix (filter) that slides across the image; its values are learned by the network</div>
<div class="match-item" id="md5" onclick="clickDef(5)">Randomly deactivates a percentage of neurons each epoch to prevent co-dependency</div>
<div class="match-item" id="md6" onclick="clickDef(6)">Activation function that converts logits into a probability distribution summing to 1</div>
<div class="match-item" id="md7" onclick="clickDef(7)">Reshapes a 3D feature volume (W×H×D) into a 1D vector for the FC layer</div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="gradeMatch()">Submit matches</button>
<button class="btn" onclick="resetMatch()">Reset</button>
</div>
<div id="match-score-msg" style="margin-top:14px;font-size:14px;font-weight:500;color:#185fa5;display:none"></div>
</div>
<!-- ═══════════════════════════════ CALC ═══════════════════════════════ -->
<div id="page-calc" class="page">
<div class="section-header">
<h2>Calculation Lab — 5 Problems</h2>
<p>Enter your computed numerical answer in each field. Use the hint button if you get stuck.</p>
</div>
<!-- C1 -->
<div class="calc-block">
<div class="calc-title">Problem 1 — Conv2D parameter count (first layer)</div>
<div class="calc-desc">A Conv2D(32, kernel_size=3×3) layer receives a grayscale input (depth = 1). How many total parameters does this layer have? (Include bias terms.)</div>
<div class="calc-formula">Formula: params = filters × kH × kW × prev_depth + filters (biases)</div>
<div class="calc-row">
<span class="calc-label">Your answer:</span>
<input class="calc-input" id="c1" type="text" placeholder="0">
<button class="hint-btn" onclick="toggleHint('h1')">Show hint</button>
</div>
<div class="hint-box" id="h1">32 filters × 3 × 3 kernel × 1 depth + 32 biases = ?</div>
<div class="calc-result" id="cr1"></div>
</div>
<!-- C2 -->
<div class="calc-block">
<div class="calc-title">Problem 2 — Output depth after Conv2D</div>
<div class="calc-desc">Input shape: 28×28×1. Apply Conv2D(32, kernel_size=3×3, padding="same", strides=1). What is the output DEPTH of this layer?</div>
<div class="calc-formula">Rule: output depth = number of filters. padding=same preserves W and H.</div>
<div class="calc-row">
<span class="calc-label">Output depth:</span>
<input class="calc-input" id="c2" type="text" placeholder="0">
<button class="hint-btn" onclick="toggleHint('h2')">Show hint</button>
</div>
<div class="hint-box" id="h2">How many filters are specified in Conv2D(32, ...)? Each filter = one feature map = one unit of depth.</div>
<div class="calc-result" id="cr2"></div>
</div>
<!-- C3 -->
<div class="calc-block">
<div class="calc-title">Problem 3 — Output size after max pooling</div>
<div class="calc-desc">Input volume: 28×28×32. Apply MaxPooling2D(pool_size=2×2, strides=2). What is the output WIDTH (or HEIGHT) of this layer?</div>
<div class="calc-formula">Rule: output_W = input_W ÷ pool_size = 28 ÷ 2 = ?</div>
<div class="calc-row">
<span class="calc-label">Output width:</span>
<input class="calc-input" id="c3" type="text" placeholder="0">
<button class="hint-btn" onclick="toggleHint('h3')">Show hint</button>
</div>
<div class="hint-box" id="h3">A 2×2 pool with stride 2 halves both width and height. 28 ÷ 2 = ?</div>
<div class="calc-result" id="cr3"></div>
</div>
<!-- C4 -->
<div class="calc-block">
<div class="calc-title">Problem 4 — Flattened vector size</div>
<div class="calc-desc">After two CONV→POOL blocks on a 28×28 input (both 2×2 max pools), the final feature volume is 7×7×64. What is the size of the flattened 1D vector passed to the FC layer?</div>
<div class="calc-formula">Flatten: output_size = W × H × D</div>
<div class="calc-row">
<span class="calc-label">Flat vector size:</span>
<input class="calc-input" id="c4" type="text" placeholder="0">
<button class="hint-btn" onclick="toggleHint('h4')">Show hint</button>
</div>
<div class="hint-box" id="h4">Multiply all three dimensions: 7 × 7 × 64 = ?</div>
<div class="calc-result" id="cr4"></div>
</div>
<!-- C5 -->
<div class="calc-block">
<div class="calc-title">Problem 5 — Second Conv2D parameter count</div>
<div class="calc-desc">Conv2D(64, kernel_size=3×3) receives the output of a previous layer that has depth = 32. Calculate the exact total parameter count (including biases).</div>
<div class="calc-formula">params = filters × kH × kW × prev_depth + filters (biases)</div>
<div class="calc-row">
<span class="calc-label">Your answer:</span>
<input class="calc-input" id="c5" type="text" placeholder="0">
<button class="hint-btn" onclick="toggleHint('h5')">Show hint</button>
</div>
<div class="hint-box" id="h5">64 × 3 × 3 × 32 = 18,432 weights, then add 64 bias terms.</div>
<div class="calc-result" id="cr5"></div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="gradeCalc()">Check calculations</button>
<button class="btn" onclick="resetCalc()">Reset</button>
</div>
<div id="calc-score-msg" style="margin-top:14px;font-size:14px;font-weight:500;color:#185fa5;display:none"></div>
</div>
<!-- ═══════════════════════════════ RESULTS ═══════════════════════════════ -->
<div id="page-results" class="page">
<div id="results-content">
<div style="background:#fff;border:1px solid #e5e3db;border-radius:10px;padding:24px;text-align:center;color:#888">
<p style="font-size:15px;font-weight:500;margin-bottom:8px">Complete all 4 modules first</p>
<p style="font-size:13px">Finish MCQ, Fill in the blanks, Matching, and Calculation lab — then return here for your full breakdown and personalized feedback.</p>
</div>
</div>
</div>
<script>
/* ── Navigation ── */
var scores = { mcq:null, fill:null, match:null, calc:null };
function goTo(id, btn) {
document.querySelectorAll('.page').forEach(function(p){ p.classList.remove('active'); });
document.querySelectorAll('.nav-btn').forEach(function(b){ b.classList.remove('active'); });
document.getElementById('page-'+id).classList.add('active');
if (btn) btn.classList.add('active');
else {
document.querySelectorAll('.nav-btn').forEach(function(b){
if (b.getAttribute('onclick') && b.getAttribute('onclick').indexOf("'"+id+"'") > -1) b.classList.add('active');
});
}
if (id === 'results') buildResults();
updateDash();
}
function updateDash() {
var total = 0;
['mcq','fill','match','calc'].forEach(function(k){
var v = scores[k];
document.getElementById('d-'+k).textContent = v === null ? '—' : v;
if (v !== null) total += v;
});
var pct = Math.round(total / 32 * 100);
document.getElementById('d-pbar').style.width = pct + '%';
document.getElementById('d-pbar-text').textContent = total + ' / 32 pts';
}
/* ── MCQ ── */
var mcqAnswers = { 1:1, 2:2, 3:1, 4:2, 5:1, 6:1, 7:1, 8:3, 9:0, 10:1, 11:2, 12:2 };
var mcqSelected = {};
var mcqGraded = false;
function selectOpt(el, qn, oi) {
if (mcqGraded) return;
var siblings = el.parentElement.querySelectorAll('.opt');
siblings.forEach(function(s){ s.classList.remove('selected'); });
el.classList.add('selected');
mcqSelected[qn] = oi;
var done = Object.keys(mcqSelected).length;
document.getElementById('mcq-pbar').style.width = Math.round(done/12*100) + '%';
}
function gradeMCQ() {
var score = 0;
var blocks = document.querySelectorAll('#page-mcq .q-block');
for (var qn = 1; qn <= 12; qn++) {
var block = blocks[qn - 1];
if (!block) continue;
var opts = block.querySelectorAll('.opt');
opts.forEach(function(o){ o.classList.remove('selected'); });
var correct = mcqAnswers[qn];
opts[correct].classList.add('reveal');
var fb = document.getElementById('fb'+qn);
fb.classList.add('show');
if (mcqSelected[qn] === correct) {
opts[correct].classList.remove('reveal');
opts[correct].classList.add('correct');
fb.classList.add('ok');
score++;
} else {
fb.classList.add('bad');
if (mcqSelected[qn] !== undefined) opts[mcqSelected[qn]].classList.add('wrong');
}
}
mcqGraded = true;
scores.mcq = score;
var msg = document.getElementById('mcq-score-msg');
msg.style.display = 'block';
msg.textContent = 'Score: ' + score + '/12. Scroll up to review each explanation.';
updateDash();
}
function resetMCQ() {
mcqSelected = {}; mcqGraded = false;
document.getElementById('mcq-pbar').style.width = '0%';
document.getElementById('mcq-score-msg').style.display = 'none';
document.querySelectorAll('#page-mcq .opt').forEach(function(o){ o.classList.remove('selected','correct','wrong','reveal'); });
document.querySelectorAll('#page-mcq .feedback').forEach(function(f){ f.classList.remove('show','ok','bad'); });
scores.mcq = null; updateDash();
}
/* ── Fill in blanks ── */
var fillAnswers = {
f1: ['flattening'],
f2: ['kernel'],
f3: ['receptive field','receptivefield'],
f4: ['depth'],
f5: ['padding','zero-padding','zero padding'],
f6: ['codependency','co-dependency','co dependency'],
f7: ['dropout','drop out']
};
function gradeFill() {
var score = 0;
for (var k in fillAnswers) {
var inp = document.getElementById(k);
var val = inp.value.trim().toLowerCase().replace(/\s+/g,' ');
var keys = fillAnswers[k];
var ok = keys.some(function(key){ return val === key; });
var fb = document.getElementById('f'+k.slice(1)+'b'[0]) || document.getElementById('ffb'+k.slice(1));
inp.classList.remove('ok','bad');
if (ok) {
inp.classList.add('ok'); score++;
document.getElementById('ffb'+k.slice(1)).textContent = 'Correct! Answer: "' + keys[0] + '"';
document.getElementById('ffb'+k.slice(1)).className = 'feedback show ok';
} else {
inp.classList.add('bad');
document.getElementById('ffb'+k.slice(1)).textContent = 'Correct answer: "' + keys[0] + '". You wrote: "' + (inp.value || '(blank)') + '"';
document.getElementById('ffb'+k.slice(1)).className = 'feedback show bad';
}
}
scores.fill = score;
var msg = document.getElementById('fill-score-msg');
msg.style.display = 'block';
msg.textContent = 'Score: ' + score + '/7.';
updateDash();
}
function resetFill() {
for (var k in fillAnswers) {
var inp = document.getElementById(k);
inp.value = ''; inp.classList.remove('ok','bad');
document.getElementById('ffb'+k.slice(1)).className = 'feedback';
document.getElementById('ffb'+k.slice(1)).textContent = '';
}
document.getElementById('fill-score-msg').style.display = 'none';
scores.fill = null; updateDash();
}
/* ── Matching ── */
var matchCorrect = { 0:0, 1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7 };
var matchSelTerm = null;
var matchPairs = {};
var matchLocked = {};
function clickTerm(i) {
if (matchLocked['t'+i]) return;
if (matchSelTerm !== null) document.getElementById('mt'+matchSelTerm).classList.remove('selected');
matchSelTerm = i;
document.getElementById('mt'+i).classList.add('selected');
updateMatchStatus();
}
function clickDef(i) {
if (matchSelTerm === null) { alert('Please select a term first (left column).'); return; }
if (matchLocked['d'+i]) return;
matchPairs[matchSelTerm] = i;
document.getElementById('mt'+matchSelTerm).classList.remove('selected');
document.getElementById('mt'+matchSelTerm).classList.add('paired');
document.getElementById('md'+i).classList.add('paired');
matchLocked['t'+matchSelTerm] = true;
matchLocked['d'+i] = true;
matchSelTerm = null;
updateMatchStatus();
}
function updateMatchStatus() {
var n = Object.keys(matchPairs).length;
document.getElementById('match-status').textContent = n + ' / 8 pairs matched.' + (matchSelTerm !== null ? ' Now click a definition.' : (n < 8 ? ' Click a term to continue.' : ' All paired! Click Submit.'));
}
function gradeMatch() {
var score = 0;
for (var ti = 0; ti < 8; ti++) {
var tEl = document.getElementById('mt'+ti);
tEl.classList.remove('paired');
if (matchPairs[ti] === undefined) { tEl.classList.add('wrong'); continue; }
var di = matchPairs[ti];
var dEl = document.getElementById('md'+di);
dEl.classList.remove('paired');
if (di === matchCorrect[ti]) { score++; tEl.classList.add('correct'); dEl.classList.add('correct'); }
else { tEl.classList.add('wrong'); dEl.classList.add('wrong'); }
}
scores.match = score;
var msg = document.getElementById('match-score-msg');
msg.style.display = 'block';
msg.textContent = 'Score: ' + score + '/8. Green = correct pair, red = incorrect.';
updateDash();
}
function resetMatch() {
matchSelTerm = null; matchPairs = {}; matchLocked = {};
for (var i = 0; i < 8; i++) {
document.getElementById('mt'+i).className = 'match-item';
document.getElementById('mt'+i).setAttribute('onclick','clickTerm('+i+')');
document.getElementById('md'+i).className = 'match-item';
document.getElementById('md'+i).setAttribute('onclick','clickDef('+i+')');
}
document.getElementById('match-status').textContent = '0 / 8 pairs matched. Click a term to start.';
document.getElementById('match-score-msg').style.display = 'none';
scores.match = null; updateDash();
}
/* ── Calc ── */
var calcAnswers = { c1:320, c2:32, c3:14, c4:3136, c5:18496 };
var calcExplain = {
c1: '32 × 3 × 3 × 1 + 32 = 288 + 32 = 320 parameters.',
c2: 'padding=same preserves W and H. 32 filters → output depth = 32. Output shape: 28×28×32.',
c3: '28 ÷ 2 = 14. A 2×2 pool with stride 2 halves both W and H. Depth stays 32.',
c4: '7 × 7 × 64 = 3,136. Flatten multiplies all dimensions of the feature volume.',
c5: '64 × 3 × 3 × 32 + 64 = 18,432 + 64 = 18,496 parameters.'
};
function toggleHint(id) {
var el = document.getElementById(id);
el.style.display = el.style.display === 'block' ? 'none' : 'block';
}
function gradeCalc() {
var score = 0;
for (var k in calcAnswers) {
var inp = document.getElementById(k);
var val = parseInt((inp.value || '').replace(/,/g,''));
var ok = val === calcAnswers[k];
var res = document.getElementById('cr'+k.slice(1));
inp.classList.remove('ok','bad');
res.style.display = 'block';
if (ok) {
score++; inp.classList.add('ok');
res.className = 'calc-result ok';
res.textContent = 'Correct! ' + calcExplain[k];
} else {
inp.classList.add('bad');
res.className = 'calc-result bad';
res.textContent = 'Incorrect. ' + calcExplain[k] + ' (You entered: ' + (inp.value || 'nothing') + ')';
}
}
scores.calc = score;
var msg = document.getElementById('calc-score-msg');
msg.style.display = 'block';
msg.textContent = 'Score: ' + score + '/5.';
updateDash();
}
function resetCalc() {
for (var k in calcAnswers) {
var inp = document.getElementById(k);
inp.value = ''; inp.classList.remove('ok','bad');
document.getElementById('cr'+k.slice(1)).style.display = 'none';
}
document.getElementById('calc-score-msg').style.display = 'none';
scores.calc = null; updateDash();
}
/* ── Results ── */
function buildResults() {
var allDone = scores.mcq !== null && scores.fill !== null && scores.match !== null && scores.calc !== null;
if (!allDone) {
var done = [];
if (scores.mcq === null) done.push('MCQ');
if (scores.fill === null) done.push('Fill in the blanks');
if (scores.match === null) done.push('Matching');
if (scores.calc === null) done.push('Calculation lab');
document.getElementById('results-content').innerHTML =
'<div style="background:#fff;border:1px solid #e5e3db;border-radius:10px;padding:24px;text-align:center;color:#888">' +
'<p style="font-size:15px;font-weight:500;margin-bottom:8px">Still in progress</p>' +
'<p style="font-size:13px">Remaining: <strong style="color:#185fa5">' + done.join(', ') + '</strong>.<br>Complete all modules then return here.</p></div>';
return;
}
var total = scores.mcq + scores.fill + scores.match + scores.calc;
var pct = Math.round(total / 32 * 100);
var heroClass = pct >= 85 ? 'great' : pct >= 60 ? 'ok' : 'low';
var heroMsg = pct >= 85 ? 'Outstanding! You have mastered Chapter 3.' : pct >= 60 ? 'Good work — review the weaker topics below.' : 'Keep studying — focus on the red areas below.';
var topics = [
{ name:'Convolution operation & kernels', pct: Math.round((scores.mcq >= 12 ? 3 : Math.min(3, scores.mcq)) / 3 * 100) },
{ name:'Hyperparameters (stride, padding)', pct: Math.round(scores.calc / 5 * 100) },
{ name:'Pooling layers', pct: Math.round((scores.match / 8) * 100) },
{ name:'Overfitting & dropout', pct: Math.round((scores.fill / 7) * 100) },
{ name:'Architecture & parameters', pct: pct },
];
var barColors = topics.map(function(t){ return t.pct >= 80 ? '#3b6d11' : t.pct >= 50 ? '#854f0b' : '#a32d2d'; });
var modules = [
{ name:'MCQ (12 pts)', score:scores.mcq, max:12 },
{ name:'Fill in blanks (7 pts)', score:scores.fill, max:7 },
{ name:'Matching (8 pts)', score:scores.match, max:8 },
{ name:'Calculation lab (5 pts)', score:scores.calc, max:5 }
];
var modRows = modules.map(function(m){
var p = Math.round(m.score/m.max*100);
var c = p>=80?'#3b6d11':p>=50?'#854f0b':'#a32d2d';
return '<div class="topic-row"><div class="topic-name">'+m.name+'</div>' +
'<div class="topic-bar-bg"><div class="topic-bar-fill" style="width:'+p+'%;background:'+c+'"></div></div>' +
'<div class="topic-pct">'+m.score+'/'+m.max+'</div></div>';
}).join('');
var topicRows = topics.map(function(t,i){
return '<div class="topic-row"><div class="topic-name">'+t.name+'</div>' +
'<div class="topic-bar-bg"><div class="topic-bar-fill" style="width:'+t.pct+'%;background:'+barColors[i]+'"></div></div>' +
'<div class="topic-pct">'+t.pct+'%</div></div>';
}).join('');
var study = pct < 85 ? '<div style="margin-top:10px;padding:12px;background:#e6f1fb;border-radius:8px;font-size:13px;color:#185fa5">'+
(pct < 60 ? 'Focus on: re-reading sections 3.1–3.3 on convolution mechanics, then re-attempt the Calculation lab and MCQ.' :
'Focus on: reviewing your incorrect MCQ answers and their explanations, then re-attempt the weak modules.') + '</div>' : '';
document.getElementById('results-content').innerHTML =
'<div class="result-hero '+heroClass+'"><div class="score">'+total+' / 32</div><div class="msg">'+pct+'% — '+heroMsg+'</div></div>' +
'<div class="result-section"><h3>Score by module</h3>'+modRows+'</div>' +
'<div class="result-section"><h3>Estimated topic performance</h3>'+topicRows+study+'</div>' +
'<div class="btn-row">' +
'<button class="btn btn-primary" onclick="resetAll()">Retake all</button>' +
'</div>';
}
function resetAll() {
resetMCQ(); resetFill(); resetMatch(); resetCalc();
goTo('dashboard', document.querySelector('.nav-btn'));
}
</script>
</body>
</html>

浙公网安备 33010602011771号