大厂真题 / 网易

网易算法岗 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)$ 用填充后坐标,特征图边长公式里的除法向下取整。