大厂真题 / 网易
网易算法岗 2026-09-19
内容边界:本场笔试共披露 4 道编程题,没有选择题。本文只收录这 4 道题,不补写原文未披露的题目、样例与约束。
本场考试概述
考试时间:2026年9月19日
考试岗位:算法岗
难度评级:简单偏中等
考点分析:
- 第一题:TF-IDF 文本特征统计(简单)。
- 第二题:线性回归与批量梯度下降(简单)。
- 第三题:二层感知机前向传播与交叉熵损失(简单)。
- 第四题:CNN 卷积计算与特征图维度(中等)。
建议策略:
- 四道题的公式都在题面里给出,难点不在算法推导而在统计口径:TF 统计的是词在一条语句内出现几次,IDF 中的 $N_{w}$ 统计的是有几条语句含这个词,两者的计数单位不同;感知机的损失是各输出节点相加,而不是取平均。
- 梯度下降必须从全 $0$ 出发并跑满给定轮数,不能因为梯度变小就提前停止,也不能改用正规方程求解;输出直接打印 NumPy 数组,改成打印列表会多出逗号。
- 卷积题给出的 $(i,j)$ 是填充后矩阵里的坐标,特征图边长公式里的除法需要向下取整。
- 四道题都规定了输出格式,建议每题写完先用样例逐字符核对一遍输出。
第 1 题:TF-IDF 值计算
题目描述
某电商平台计划借助大语言模型改善用户的购物体验,在训练模型之前,先用传统机器学习中的 TF-IDF 方法对训练数据做一次摸底分析。TF-IDF 是信息检索与文本挖掘中常用的加权技术。
其中词频 TF 表示为「词 $w$ 在某条语句中出现的次数」除以「该语句中的总词数」;逆文档频率 IDF 表示为
\[IDF=\log\left(\frac{N}{N_{w}+1}+1\right)\]其中 $N$ 是语料库中语句的总数,$N_{w}$ 是语料库中包含单词 $w$ 的语句总数。TF-IDF 表示为 TF 与 IDF 的乘积。
注意:$\log$ 为自然对数,单词按空格切分且区分大小写。
输入描述
输入是两个 list,各占一行,均为 Python 列表字面量,可以用 eval 函数还原。第一个 list 中的每个元素表示一条训练语句,每条语句里的词用空格隔开;第二个 list 长度为 $2$,第一个元素表示某条训练语句在第一个 list 中的索引,第二个元素表示该语句中的某个词汇。
语句总数 $N$ 满足 $1 \le N \le 100$,每条语句包含 $1$ 到 $20$ 个单词,每个单词由 $1$ 到 $10$ 个英文字母组成;给定的索引合法,给定的词汇一定出现在该语句中。
输出描述
给出在给定语句和给定 Word 下该 Word 的 TF-IDF 值,结果为 float,保留小数点后 3 位(与 round 函数的格式一致)。
样例 1
输入
['An Apple orange Apple','Chinese is an banana','Chinese KongFu','Apple is not pen']
[0, 'Apple']
输出
0.424
样例解释:第 1 条语句中 Apple 的 TF-IDF 值为 $0.424$。
思路分析
第一步:先看清两个计数口径。 题面把 TF 和 IDF 的公式都给了,剩下的事情只有一件事——分清分子分母统计的是什么。TF 的分子是「词 $w$ 在某一条语句内出现的次数」,分母是「这条语句的词数」;IDF 里的 $N_{w}$ 是「含 $w$ 的语句条数」,不是全语料里 $w$ 的出现总次数。两处都属于计数,但一个数词、一个数句,一旦混用就会算错。
第二步:切词并严格区分大小写。 每条语句按空格切成词列表,比较时逐词精确匹配。样例里第 1 句同时出现了 An 和 an,题面明确说明区分大小写,所以这两个是不同的词,不能顺手做 lower() 归一。
第三步:算 TF。 设给定索引为 $k$、给定词为 $w$,目标语句是 $d_k$,则
\[TF=\frac{\operatorname{count}(w, d_k)}{\lvert d_k\rvert}\]其中 $\operatorname{count}(w, d_k)$ 是 $w$ 在 $d_k$ 中的出现次数,$\lvert d_k\rvert$ 是 $d_k$ 的词数。如果 $w$ 在目标句里出现了两次,分子就是 $2$。
第四步:算 IDF,注意去重。 $N_{w}$ 统计语句条数,同一句里出现多次也只记一次,所以每条语句先转成集合再判断成员:
\[N_{w}=\sum_{i=1}^{N}\left[w \in \operatorname{set}(d_i)\right]\]分母上的 $+1$ 是对 $N_{w}$ 做平滑;外层再 $+1$,保证对数的真数始终大于 $1$,即使 $N_{w}+1>N$ 时 IDF 也为正,不会出现负权重。若把全语料的出现总次数当作 $N_{w}$,一句里重复出现的词会被多算,IDF 随之偏小。
第五步:乘起来并按格式输出。 两者相乘后取 3 位小数,直接打印浮点数即可,末尾的 $0$ 会被省略,例如 $0.5$ 不会写成 $0.500$。
手动模拟样例:语料共 $N=4$ 条语句;Apple 出现在第 1 条和第 4 条语句中,所以 $N_{w}=2$;目标语句是第 1 条,词数为 $4$,Apple 在其中出现 $2$ 次,于是 $TF=2/4=0.5$,$IDF=\ln(4/3+1)=\ln(7/3)\approx0.8473$,乘积约为 $0.4237$,保留 3 位小数得 $0.424$,与样例一致。
题解代码
import ast
import math
import sys
input = sys.stdin.readline
# 统计包含 word 的语句条数,同一句里出现多次只算一次
def count_docs(docs, word):
count = 0
for words in docs:
# 先转集合再判断成员,一句里出现几次都只记 1
if word in set(words):
count += 1
return count
# 计算 word 在第 idx 条语句中的 TF-IDF 值
def tf_idf(sentences, idx, word):
# 按空格切词,比较时区分大小写,apple 与 Apple 是两个词
docs = [sentence.split() for sentence in sentences]
n = len(docs)
# TF:目标句中该词的出现次数 / 目标句的词数
target = docs[idx]
tf = target.count(word) / len(target)
# IDF:分母 +1 做平滑,外层再 +1 保证对数的真数大于 1、结果为正
nw = count_docs(docs, word)
idf = math.log(n / (nw + 1) + 1)
return tf * idf
def solve():
# 两行输入都是 Python 列表字面量,用 literal_eval 安全还原
sentences = ast.literal_eval(input())
query = ast.literal_eval(input())
idx, word = int(query[0]), query[1]
# 输出与 round(x, 3) 的格式一致,例如 0.424、0.5
print(round(tf_idf(sentences, idx, word), 3))
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:$O(L)$,其中 $L$ 是全部语句的总词数。瓶颈是切词与逐句建集合,每个词只被处理常数次;统计 $N_{w}$ 必须看遍所有语句,不可能更快。
空间复杂度:$O(L)$,存放切分后的词列表与用于判重的集合。
易错点
- TF 的分子是目标句内的出现次数,IDF 的 $N_{w}$ 是含该词的语句条数,两个口径不能混用。
- 切词后必须区分大小写,
An与an是两个词。 - 输出用
round(x, 3)的格式,末尾的 $0$ 会被省略;手工格式化成固定三位小数反而与样例不符。
第 2 题:梯度下降线性回归
题目描述
编写一个使用梯度下降执行线性回归的过程。函数以 NumPy 数组 $X$(已带截距列的特征)和 $y$(目标)为输入,另外接收学习率 $\alpha$ 与迭代次数,返回表示线性回归模型系数的 NumPy 数组。
系数初始化为全 $0$,每次迭代使用全部样本计算均方误差的平均梯度
\[\nabla=\frac{1}{m}X^{\top}(X\theta-y)\]并按
\[\theta \leftarrow \theta-\alpha\nabla\]更新,其中 $m$ 为样本数。支持 numpy、scipy、pandas、scikit-learn 库。
输入描述
第 1 行输入 $X$,第 2 行输入 $y$,第 3 行输入 $\alpha$,第 4 行输入迭代次数。
$X$ 为 $m$ 行 $d$ 列的二维列表,第一列恒为 $1$(截距列);$y$ 为长度 $m$ 的一维列表。样本数与特征数满足 $1 \le m \le 50$、$2 \le d \le 4$,学习率满足 $0<\alpha \le 0.1$,迭代次数满足 $1 \le \text{iters} \le 5000$。
输出描述
输出线性回归模型的系数,四舍五入到小数点后四位,按 NumPy 数组的默认打印格式输出,即 print(np.round(theta, 4)) 的结果。
样例 1
输入
[[1, 1], [1, 2], [1, 3], [1, 4]]
[2, 3, 4, 5]
0.01
1000
输出
[0.8678 1.045 ]
样例解释:数据恰好满足 $y=1+x$,但在学习率 $0.01$ 下迭代 $1000$ 次尚未完全收敛,此时截距约为 $0.8678$、斜率约为 $1.0450$。NumPy 打印数组时按最宽的元素统一列宽,0.8678 与 1.045 等宽对齐后仍留一个分隔空格,所以右括号前有一个空格;改成打印列表会多出逗号。
思路分析
第一步:确认模型与初值。 线性模型为 $\hat{y}=X\theta$。截距已经作为第一列全 $1$ 放进 $X$,$\theta_{0}$ 本身就是截距,不需要再单独维护一个偏置项。系数初值题面规定为全 $0$,这是口径的一部分——迭代次数固定时,换初值会得到不同的中间结果。
第二步:写清楚梯度是怎么来的。 损失取均方误差的一半:
\[L(\theta)=\frac{1}{2m}\sum_{i=1}^{m}\left(x_{i}^{\top}\theta-y_{i}\right)^{2}\]系数 $\frac{1}{2}$ 与求导时平方落下的 $2$ 恰好抵消,梯度里只剩 $\frac{1}{m}$,写成矩阵形式正是题面给出的
\[\nabla=\frac{1}{m}X^{\top}(X\theta-y)\]残差 $X\theta-y$ 的形状是 $(m,)$;左乘 $X^{\top}$ 把每条样本的误差按特征摊回各维,得到形状 $(d,)$ 的梯度。
第三步:向量化实现。 逐样本、逐维度用两层循环算梯度,单轮就是 $O(md)$ 次解释器层面的操作,$5000$ 轮下来是百万级常数;改写成两次矩阵乘向量交给底层 BLAS 执行,常数能小两个数量级,代码也更贴近公式。
第四步:守住两个口径。 第一,必须跑满题面给定的轮数,不能因为梯度已经足够小就提前退出。样例在 $1000$ 轮后得到 $[0.8678,1.045]$,距离真解 $[1,1]$ 还有明显差距,提前停止或者改用正规方程一步求解都会直接判错。第二,输出必须走 print(np.round(theta, 4)),NumPy 会自动对齐列宽并把 $1.045$ 补成 1.045 加一个空格;换成打印 Python 列表会得到逗号分隔的格式,与样例不一致。
题解代码
import ast
import sys
import numpy as np
input = sys.stdin.readline
# 用批量梯度下降拟合线性回归,返回跑满 iters 轮后的系数向量
def linear_regression_gd(X, y, alpha, iters):
m = X.shape[0]
# 系数从全 0 出发,这是题面规定的初值
theta = np.zeros(X.shape[1])
for _ in range(iters):
# 预测值与真实值的残差:(m, d) @ (d,) -> (m,)
residual = X @ theta - y
# 均方误差的平均梯度:(d, m) @ (m,) -> (d,),除以 m 取样本平均
gradient = X.T @ residual / m
# 沿负梯度方向走一步,步长由学习率 alpha 控制
theta = theta - alpha * gradient
return theta
def solve():
# 前两行是 Python 列表字面量,用 literal_eval 安全解析
X = np.array(ast.literal_eval(input()), dtype=float)
y = np.array(ast.literal_eval(input()), dtype=float)
alpha = float(input())
iters = int(input())
theta = linear_regression_gd(X, y, alpha, iters)
# 直接打印 NumPy 数组,题面要求的就是它的默认格式(含对齐补出的空格)
print(np.round(theta, 4))
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:$O(T \times m \times d)$,其中 $T$ 是迭代次数。每轮两次矩阵乘向量是瓶颈,轮数由输入钉死省不掉;$m \le 50$、$d \le 4$、$T \le 5000$ 时总量约 $10^{6}$ 次浮点运算。
空间复杂度:$O(md)$,存放特征矩阵与目标向量;系数与梯度只占 $O(d)$。
易错点
- 必须跑满题面给定的轮数,不能按梯度大小提前收敛退出,也不能改用正规方程直接求解。
- 系数初值是全 $0$,初值不同会导致有限轮数后的结果不同。
- 梯度里的除法是除以样本数 $m$,漏掉这个因子等于放大了学习率。
- 输出要直接打印 NumPy 数组,用列表打印会多出逗号,空格对齐方式也不同。
第 3 题:二层感知机前向传播损失
题目描述
某电商平台计划在大促到来之前优化商品个性化推荐排序模型,在深度学习排序模型的基础上引入感知机对数据做初步分析。以二层感知机为例,第一层前向传播计算结果为 $Z_{1}=f(W_{1}X+b_{1})$,在此基础上第二层为 $Z_{2}=f(W_{2}Z_{1}+b_{2})$,激活函数都是 sigmoid。使用交叉熵损失函数,对应单条训练样本的表达式为
\[Loss=-y\cdot \log h-(1-y)\log(1-h)\]其中 $y,h$ 分别表示样本的真值和预测的输出。
注意:输出层有多个节点时,总损失为各节点损失之和;$\log$ 为自然对数。
输入描述
以两个二维 list 的形式给定 $W_{1}$ 和 $W_{2}$,以同一个标量给定 $b_{1},b_{2}$(题面说明两者数值相同),再以两个二维 list 的形式给定训练数据 $X$ 和标签值 $Y$。五项在同一行内以逗号分隔,使用形如 Python 中 eval 函数的方式可以获取并还原到原始数据。
$W_{1}$ 为 $h$ 行 $d$ 列,$W_{2}$ 为 $o$ 行 $h$ 列,$X$ 为 $d$ 行 $1$ 列,$Y$ 为 $o$ 行 $1$ 列,其中 $1 \le d,h,o \le 10$;权重、偏置与特征取值均在 $[-1,1]$ 内,标签取值为 $0$ 或 $1$。
输出描述
给出在给定损失函数下首次前向传播完成后的 loss 值,结果以 float 类型返回,保留小数点后 3 位(与 round 函数的格式一致)。
样例 1
输入
[[0.1,0.2],[0.3,0.4]],[[0.5,0.6],[0.7,0.8]],0.1,[[0.05],[0.10]],[[1],[0]]
输出
1.65
样例解释:第一层 $W_{1}X+b_{1}=[0.125,0.155]$,经 sigmoid 得 $Z_{1}\approx[0.5312,0.5387]$;第二层 $W_{2}Z_{1}+b_{2}\approx[0.6888,0.9028]$,经 sigmoid 得 $h\approx[0.6657,0.7115]$。第一个节点标签为 $1$,损失 $-\log 0.6657\approx0.4069$;第二个节点标签为 $0$,损失 $-\log(1-0.7115)\approx1.2431$,两者相加约为 $1.650$,按 round 的格式输出 1.65。
思路分析
第一步:看清维度与乘法方向。 $W_{1}$ 的形状是 $(h,d)$,左乘列向量 $X$ 并加上偏置,再过一次 sigmoid:
\[Z_{1}=\sigma(W_{1}X+b),\qquad \sigma(z)=\frac{1}{1+e^{-z}}\]维度变化为 $(h,d)\times(d,1)\to(h,1)$。偏置 $b$ 是一个标量,靠广播加到每个隐藏节点上。这里 $X$ 是列向量、权重在左,如果改成右乘或者把矩阵转置,维度立刻对不上,这是本题最容易翻车的地方。
第二步:同样的结构再走一层。 第二层的形状完全相同,只是输入换成 $Z_{1}$,而且偏置仍然用同一个 $b$(题面说明 $b_{1},b_{2}$ 给定数值相同,因此两份代码共用一个标量即可):
\[h=\sigma(W_{2}Z_{1}+b)\]维度变化为 $(o,h)\times(h,1)\to(o,1)$,$h$ 的每个分量是一个输出节点的预测概率。
第三步:逐节点算交叉熵,再求和。 对每个输出节点套题面公式,然后把 $o$ 个节点的损失相加:
\[Loss=\sum_{k=1}^{o}\left[-y_{k}\ln h_{k}-(1-y_{k})\ln(1-h_{k})\right]\]标签为 $1$ 的节点只剩前一项,标签为 $0$ 的只剩后一项,逐节点代入时不要漏项。样例两个节点分别贡献 $0.4069$ 与 $1.2431$,相加为 $1.650$;若改成取平均会得到 $0.825$,与样例不符,所以本题口径确定是求和。
第四步:数值稳定性的取舍。 工程实现通常会给 $\ln$ 加一个极小量 $\varepsilon$ 防止 $h$ 取到 $0$ 或 $1$。本题权重、偏置与特征都在 $[-1,1]$ 内,隐藏层维数最多为 $10$,第二层输入的绝对值不超过 $10 \times 1+1=11$,sigmoid 的输出严格落在 $(0,1)$ 内且与两端都留有距离,不会出现 $\ln 0$,因此照公式原样实现即可。
题解代码
import ast
import sys
import numpy as np
input = sys.stdin.readline
# sigmoid 激活:把任意实数压到 (0, 1),两层共用
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
# 做一次前向传播,返回输出层所有节点的交叉熵损失之和
def forward_loss(W1, W2, b, X, Y):
# 第一层:隐藏层输出,(h, d) @ (d, 1) -> (h, 1)
Z1 = sigmoid(W1 @ X + b)
# 第二层:输出层预测值,偏置与第一层是同一个 b,(o, h) @ (h, 1) -> (o, 1)
H = sigmoid(W2 @ Z1 + b)
# 逐节点算交叉熵,取值范围内 H 严格落在 (0, 1) 内,log 不会取到 0
per_node = -Y * np.log(H) - (1 - Y) * np.log(1 - H)
# 题面要求多个输出节点时损失相加,不取平均
return float(np.sum(per_node))
def solve():
# 一行里五项用逗号隔开,literal_eval 直接还原成五元组
W1, W2, b, X, Y = ast.literal_eval(input())
# 转成浮点矩阵,保证 @ 按矩阵乘法计算
W1 = np.array(W1, dtype=float)
W2 = np.array(W2, dtype=float)
X = np.array(X, dtype=float)
Y = np.array(Y, dtype=float)
print(round(forward_loss(W1, W2, b, X, Y), 3))
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:$O(hd+oh)$,两次矩阵乘向量是全部开销,sigmoid 与损失都只是逐元素 $O(h+o)$ 的运算。
空间复杂度:$O(hd+oh)$,存放两个权重矩阵;中间结果 $Z_{1}$ 与 $h$ 只占 $O(h+o)$。
易错点
- $X$ 是列向量、权重左乘;转置或改成右乘会直接判错。
- 两层共用同一个偏置 $b$,不要给第二层另配一个偏置。
- 多个输出节点的损失是相加而不是取平均,改成平均后样例会输出 $0.825$。
- 标签为 $1$ 的节点只剩 $-\log h$,为 $0$ 的只剩 $-\log(1-h)$,逐节点代入时不要漏项。
第 4 题:卷积结果与特征图维度
题目描述
零售行业可以在商品识别与计价、货架库存管理等场景中使用图像相关技术,以卷积网络(CNN)为基础的机器学习模型在这些场景中广泛使用。要求编程实现与 CNN 相关的子功能。
补充说明:卷积核是一个小的权重矩阵,让它逐步在输入的图像矩阵上扫描,计算权重矩阵与扫描到的子矩阵的内积,再把结果汇总成一个输出像素,这也是引入卷积的重要原因——在保证足够精度的前提下减少全连接网络的计算量。填充是为了减少输入矩阵边界信息的损失,在输入矩阵上下左右的 4 条边界上填充一些数值,通常用 $0$ 填充。步长是每次滑动卷积核时往右滑动的列数或往下滑动的行数,它对信息的压缩程度与输出矩阵的尺寸至关重要。偏置项与全连接网络的偏置项作用一致;为了简化计算,本题不考虑偏置项 bias。
输入描述
输入是 3 个 list。第一个输入是一个 3 维 list,用于表示图像数据,形如 [ [ [0, 1, 0, 0, 2],[1, 2, 1, 2, 1],[1, 0, 1, 0, 0],[0, 1, 2, 0, 0],[1, 1, 0, 0, 2] ] ],其中所有 list 组成的都是方阵,示例给出的即为一个 $5\times5$ 的图像。第二个输入是卷积核矩阵,也是一个 3 维 list,形如 [ [ [0, 1, 1], [0, 0, 1], [1, 1, 0] ] ]。
第三个输入是和卷积相关的一维 list 参数,长度固定为 6,形如 [0, 0, 3, 3, 0, 1];这 6 个元素分别表示当前卷积框对应最左上角的位置索引横坐标 $i$ 和纵坐标 $j$、卷积框的宽度和高度(宽度等于高度)、填充数 padding_size 和卷积步长 stride。
图像的形状为 $C\times n\times n$,卷积核的形状为 $C\times k\times k$,多通道时卷积结果为各通道内积之和。$i,j$ 是卷积框左上角在填充后矩阵中的行下标与列下标(从 $0$ 开始),保证卷积框完整落在填充后的矩阵内。数据范围:$1 \le C \le 3$、$1 \le n \le 20$、$1 \le k \le n+2p$、$0 \le p \le 3$、$1 \le s \le 3$;图像元素取值为 $[0,9]$ 内的整数,卷积核元素取值为 $[-3,3]$ 内的整数。
输出描述
返回 2 个 int 类型的整数:第一个返回值是在当前索引 $i$ 和 $j$ 下经过卷积核计算的结果,第二个返回值是经过卷积操作以后的输出 feature_map 的维度。两个整数在同一行输出,以一个空格分隔。
样例 1
输入
[ [ [0, 1, 0, 0, 2],[1, 2, 1, 2, 1],[1, 0, 1, 0, 0],[0, 1, 2, 0, 0],[1, 1, 0, 0, 2] ] ]
[ [ [0, 1, 1], [0, 0, 1], [1, 1, 0] ] ]
[0, 0, 3, 3, 0, 1]
输出
3 3
样例解释:不填充时卷积框左上角在 $(0,0)$,覆盖的子矩阵为 $[[0,1,0],[1,2,1],[1,0,1]]$,与卷积核逐元素相乘再求和为 $1+1+1=3$。输出特征图的边长为 $\lfloor (5+2 \times 0-3)/1\rfloor+1=3$。
思路分析
第一步:先做零填充。 在每个通道的上下左右各补 $p$ 圈 $0$,只补高、宽两个轴,通道轴不补,图像由 $(C,n,n)$ 变成 $(C,n+2p,n+2p)$。题目给出的 $(i,j)$ 就是这张填充后矩阵里的行、列下标,可以直接拿去切片,不必再换算回原图坐标;坐标轴不能补错,否则整体会错位一圈。
第二步:算单点卷积值。 取出以 $(i,j)$ 为左上角、边长为 $k$ 的窗口,与卷积核逐元素相乘,再对通道和窗口内全部位置求和:
\[out(i,j)=\sum_{c=1}^{C}\sum_{a=0}^{k-1}\sum_{b=0}^{k-1}P_{c}[i+a][j+b]\cdot K_{c}[a][b]\]窗口的形状是 $(C,k,k)$,与卷积核形状相同,逐元素相乘后仍是 $(C,k,k)$,全部加总成一个标量。多通道时各通道结果必须相加而不是各算各的,因为只有一组卷积核,只输出一张特征图。窗口伸进填充区的部分乘的都是 $0$,所以卷积框完全落在填充圈里时结果恰为 $0$。
第三步:算特征图边长。 窗口起点从 $0$ 开始每次跳 $s$ 格,最后一个起点不能超过 $n+2p-k$,起点个数就是输出边长:
\[L=\left\lfloor\frac{n+2p-k}{s}\right\rfloor+1\]剩下不足一步的尾巴放不下完整窗口,直接丢弃,所以这里是向下取整而不是四舍五入。例如 $n=5,p=1,k=3$ 时填充后边长为 $7$,$s=2$ 得 $L=\lfloor 4/2\rfloor+1=3$;改成 $s=3$ 得 $L=\lfloor 4/3\rfloor+1=2$,最后一列只被第二个窗口覆盖到一部分。
第四步:按格式输出。 两个结果都是整数,在同一行用空格分隔输出即可。
题解代码
import ast
import sys
import numpy as np
input = sys.stdin.readline
# 在填充后的图像上取出 (i, j) 处的卷积框,与卷积核逐元素相乘后求和
def conv_value(image, kernel, i, j, k, p):
# 只在高、宽两个轴的四周补 p 圈 0,通道轴不补
padded = np.pad(image, ((0, 0), (p, p), (p, p)), mode="constant")
# (C, n+2p, n+2p) 切片 -> (C, k, k),i、j 已是填充后坐标,直接切
window = padded[:, i:i + k, j:j + k]
# 逐元素相乘后把通道与窗口位置全部加起来,得到一个标量
return int(np.sum(window * kernel))
# 计算输出特征图的边长:能完整放下卷积框的起点个数
def feature_size(n, k, p, s):
# 填充后边长为 n+2p,框占 k 格,起点每次跳 s 格,不足一步的尾巴丢掉所以向下取整
return (n + 2 * p - k) // s + 1
def solve():
# 三行 list 字面量,literal_eval 能容忍行内的多余空格
image = np.array(ast.literal_eval(input()), dtype=np.int64)
kernel = np.array(ast.literal_eval(input()), dtype=np.int64)
params = ast.literal_eval(input())
# 参数依次是 i、j、宽高、padding、stride
i, j, k, p, s = params[0], params[1], params[2], params[4], params[5]
n = image.shape[1]
print(conv_value(image, kernel, i, j, k, p), feature_size(n, k, p, s))
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:$O(C\times(n+2p)^{2}+C\times k^{2})$。瓶颈是构造填充后的矩阵,单点卷积只碰 $C\times k^{2}$ 个元素,边长公式是 $O(1)$。
空间复杂度:$O(C\times(n+2p)^{2})$,存放填充后的图像;卷积核与参数都是常数规模。
易错点
- $(i,j)$ 是填充后矩阵的坐标,再换算回原图坐标会整体错位。
- 通道轴不能填充,
np.pad的三个轴宽度要写成((0, 0), (p, p), (p, p))。 - 边长公式里的除法是向下取整,不是四舍五入。
- 多通道要先分别做内积再相加,得到一个输出值,不要输出成每个通道一个数。
小结
- 四道题全部是机器学习与深度学习的手写实现,公式都在题面里给出,真正决定成败的是计数口径、迭代口径与输出格式。
- 第一题:TF 统计的是词在一条语句内出现的次数,IDF 中的 $N_{w}$ 统计的是含该词的语句条数,两个口径必须分开统计。
- 第二题:系数从全 $0$ 出发、跑满给定轮数,梯度写成两次矩阵乘向量交给 BLAS,并按 NumPy 数组的默认格式输出。
- 第三题:两层共用同一个偏置,$X$ 是列向量、权重左乘,多输出节点的交叉熵损失是求和而不是取平均。
- 第四题:先做零填充再切片,$(i,j)$ 用填充后坐标,特征图边长公式里的除法向下取整。