【YUNA】配置Google Calendar MCP Server到自己的服务
很久没有维护过 yuna 的底层架构了，最近准备接入 MCP 了。这次准备接入 Google Calendar MCP Server 来替代之前的 function call 形式的日历调用。什么是 MCP？MCP 全名是 Model Context Protocol（模型上下文协议） ，这是 Anthropic 公司在 2024 年 11 月 26 日推出的技术协议。MCP 旨在定义一套通用通信规则，借助这套规则，AI 助手就能连接外部资源，让给出的回应更准确、更好用。换句话说，MCP 允许第三方工具与数据源构建插件，并将其添加到你使用的各类智能助手（如 Claude、ChatGPT、Cursor 等）中。这些基于文本的大模型依靠一些“工具”来实现非文本操作功能。MCP 的出现让用户可以“自带工具”接入这些系统。在今年的前半年，我的开发计划中其实并没有接入 MCP 的打算，其中最重要的一方面是因为懒我认为 MCP 技术在当时的生态不够丰富，前景尚未可知。并且当时整个工程的复杂度并没有很高，我自己设计的 function call 的注册机制也相对健全，对 MCP 这种新东西并没有太大需求。随着时间推移，我发现情况有所变化，一方面工具的数量发生了质的变化，另一方面上层功能的丰富开始对工具调用产生了越来越高的要求。最终决定重写底层，接入 MCP 服务。对我来说，接入 MCP 服务的好处主要有两个：1. 生态丰富，接口标准。我可以顺畅的从工具中获取上下文信息而不需要在我自设的模板中传递。2. 在模型能力更加强大的当下，我认为有必要为模型拥有更强的自主性，而不需要之前那么多的框架规则环绕。MCP 的接入可以让我的模型变得更加自由。但是很遗憾，MCP 的中文社区生态看起来并不理想，我并没有搜到多少清晰的 Google Calendar MCP Server 使用 or 配置的一些中文博客。因此在这里记述一下，以便日后查阅避坑。整体项目是按照 google-calendar-mcp 的配置来走的，如果熟悉的话可以直接去按照他的指示进行配置。配置 google Cloud首先打开 google Cloud。点击控制台顶部的“选择项目”按钮，然后点击“新建项目”。填写项目名称、组织和位置等信息，点击“创建”按钮。或者进入一个已经有的项目即可。启用 Google 日历 API。进入 API 和服务，点击凭证（Credentials）选项卡创建 OAuth 凭证 id在受众窗口下，将自己添加为测试用户。添加测试用户可能需要几分钟时间。在测试用户生效之前，OAuth 授权将不允许您继续操作。关于测试模式的说明：当应用程序处于测试模式时，身份验证令牌将在 1 周后过期，需要刷新（请参阅下面的重新身份验证部分）。配置到工程这一部分就因人而异了。我是 nodejs 用户，因此我可以直接安装：git clone https://github.com/nspady/google-calendar-mcp.git cd google-calendar-mcp npm install npm run build然后设定你的文件路径并运行：export GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json" # 如果你是windows： $env:GOOGLE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json" npx @cocal/google-calendar-mcp auth然后即可认证成功。参考 MCP 代码JSON-RPC 初始化（含 protocolVersion/capabilities）、env 变量展开、stdio 子进程启动、超时处理。import { Logger } from "../../../shared/logger/logger.js"; import { JsonRpcClient } from "./jsonrpc/JsonRpcClient.js"; import { StdioTransport } from "./jsonrpc/JsonLineRpcTransport.js"; import { McpCallToolResult, McpListToolsResult, McpTool } from "./McpTypes.js"; import fs from "node:fs"; import path from "node:path"; function expandEnvVars(value: string): string { return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? ""); } export type McpServerConfig = { name: string; required: boolean; toolPrefix: string; timeoutMs: number; transport: "stdio" | "http"; command?: string; args?: string[]; env?: Record<string, string>; cwd?: string; baseUrl?: string; }; export class McpClient { private readonly logger: Logger; readonly server: McpServerConfig; private rpc: JsonRpcClient | null = null; private initialized = false; constructor(opts: { logger: Logger; server: McpServerConfig }) { this.logger = opts.logger; this.server = opts.server; } async start(): Promise<void> { if (this.rpc) return; if (this.server.transport === "http") { throw new Error("HTTP transport not implemented in MVP (use stdio)."); } if (!this.server.command) throw new Error(`MCP server ${this.server.name} missing command`); // Preflight: if command is "node" and args[0] looks like a script path, resolve and verify. const args = [...(this.server.args ?? [])]; if (this.server.command === "node" && args.length > 0 && typeof args[0] === "string") { const candidate = args[0]; const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(this.server.cwd ?? process.cwd(), candidate); if (!fs.existsSync(resolved)) { throw new Error( `MCP server ${this.server.name} script not found: ${candidate} (resolved: ${resolved}). ` + "Please set the correct path in mcp.servers[].args[0].", ); } args[0] = resolved; } // Merge env with process.env, with ${VAR} expansion for convenience. const expandedEnv: Record<string, string> = {}; for (const [k, v] of Object.entries(this.server.env ?? {})) { if (typeof v === "string") { const expanded = expandEnvVars(v); if (expanded.length === 0) { this.logger.warn({ key: k }, "mcp env variable expansion produced empty string; check environment"); } expandedEnv[k] = expanded; } } const transport = new StdioTransport({ logger: this.logger, command: this.server.command, args, cwd: this.server.cwd, env: { ...process.env, ...expandedEnv }, }); this.rpc = new JsonRpcClient({ logger: this.logger, transport }); await this.rpc.start(); } async stop(): Promise<void> { await this.rpc?.stop(); this.rpc = null; this.initialized = false; } async initialize(): Promise<void> { await this.start(); if (!this.rpc) throw new Error("rpc not started"); if (this.initialized) return; // MCP initialization: include protocolVersion + capabilities for servers that validate schema strictly. await this.rpc.request( "initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "yuna", version: "0.1.0" }, }, this.server.timeoutMs, ); // Some servers expect "initialized" notification; send as request for simplicity if supported. try { await this.rpc.request("initialized", {}, this.server.timeoutMs); } catch { // ignore if not supported } this.initialized = true; } async listTools(): Promise<McpTool[]> { await this.initialize(); if (!this.rpc) throw new Error("rpc not started"); const res = (await this.rpc.request<McpListToolsResult>("tools/list", {}, this.server.timeoutMs)) as any; const tools = Array.isArray(res?.tools) ? (res.tools as McpTool[]) : []; return tools; } async callTool(name: string, args: unknown): Promise<McpCallToolResult> { await this.initialize(); if (!this.rpc) throw new Error("rpc not started"); const res = await this.rpc.request<McpCallToolResult>("tools/call", { name, arguments: args }, this.server.timeoutMs); return res as any; } } JSON-RPC 2.0 基础（请求、pending map、超时、错误兜底）。import { Logger } from "../../../../shared/logger/logger.js"; import { JsonLineRpcTransport } from "./JsonLineRpcTransport.js"; import { JsonRpcRequest, JsonRpcResponse } from "./JsonRpcTypes.js"; export class JsonRpcClient { private readonly logger: Logger; private readonly transport: JsonLineRpcTransport; private nextId = 1; private pending = new Map<number, { resolve: (v: any) => void; reject: (e: any) => void; timer?: any }>(); constructor(opts: { logger: Logger; transport: JsonLineRpcTransport }) { this.logger = opts.logger; this.transport = opts.transport; } async start(): Promise<void> { await this.transport.start((line) => this.onLine(line)); } async stop(): Promise<void> { for (const [, p] of this.pending) { if (p.timer) clearTimeout(p.timer); p.reject(new Error("transport stopped")); } this.pending.clear(); await this.transport.stop(); } async request<T = unknown>(method: string, params: unknown, timeoutMs: number): Promise<T> { const id = this.nextId++; const req: JsonRpcRequest = { jsonrpc: "2.0", id, method, params }; const payload = JSON.stringify(req); const promise = new Promise<T>((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`JSON-RPC timeout: ${method}`)); }, timeoutMs); this.pending.set(id, { resolve, reject, timer }); }); await this.transport.sendLine(payload); return await promise; } private onLine(line: string) { const s = line.trim(); if (!s) return; let msg: any; try { msg = JSON.parse(s); } catch (err) { this.logger.warn({ line: s }, "mcp received non-json line"); return; } // notifications: ignore for MVP (but log for traceability) if (msg && msg.jsonrpc === "2.0" && msg.method && msg.id === undefined) { this.logger.debug({ method: msg.method }, "mcp notification received"); return; } const resp = msg as JsonRpcResponse; const id = (resp as any)?.id; if (typeof id !== "number") return; const pending = this.pending.get(id); if (!pending) return; this.pending.delete(id); if (pending.timer) clearTimeout(pending.timer); if ((resp as any).error) { const e = (resp as any).error; const err = new Error(`JSON-RPC error ${e.code}: ${e.message}`); (err as any).data = e.data; pending.reject(err); return; } pending.resolve((resp as any).result); } } stdio 行级传输import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import readline from "node:readline"; import { Logger } from "../../../../shared/logger/logger.js"; export type TransportMessageHandler = (line: string) => void; export interface JsonLineRpcTransport { start(onLine: TransportMessageHandler): Promise<void>; sendLine(line: string): Promise<void>; stop(): Promise<void>; } export class StdioTransport implements JsonLineRpcTransport { private readonly logger: Logger; private readonly command: string; private readonly args: string[]; private readonly cwd?: string; private readonly env?: Record<string, string>; private proc: ChildProcessWithoutNullStreams | null = null; private rl: readline.Interface | null = null; constructor(opts: { logger: Logger; command: string; args?: string[]; cwd?: string; env?: Record<string, string> }) { this.logger = opts.logger; this.command = opts.command; this.args = opts.args ?? []; this.cwd = opts.cwd; this.env = opts.env; } async start(onLine: TransportMessageHandler): Promise<void> { if (this.proc) return; this.proc = spawn(this.command, this.args, { cwd: this.cwd, env: { ...process.env, ...(this.env ?? {}) }, stdio: ["pipe", "pipe", "pipe"], }); this.proc.on("exit", (code, signal) => { this.logger.warn({ code, signal }, "mcp stdio process exited"); }); this.proc.stderr.on("data", (buf) => { const s = buf.toString("utf-8").trim(); if (s) this.logger.warn({ stderr: s }, "mcp stdio stderr"); }); this.rl = readline.createInterface({ input: this.proc.stdout }); this.rl.on("line", (line) => onLine(line)); } async sendLine(line: string): Promise<void> { if (!this.proc) throw new Error("transport not started"); this.proc.stdin.write(line + "\n"); } async stop(): Promise<void> { try { this.rl?.close(); } catch { // ignore } this.rl = null; const p = this.proc; this.proc = null; if (!p) return; p.kill(); } }
【论文阅读】PaMIR: Parametric Model-Conditioned Implicit Representation for Image-based Human Reconstruction
通过单张照片进行人体重建的第三篇文章，作者是清华大学的刘烨斌教授。其在 PIFu 的基础上增加了新的人体先验，从而实现了更好的人体重建效果。项目地址在这里。MotivationPIFu 已经证明了隐式函数在人体重建方面相较于显式方法效果更好，但依旧存在速度慢 + 姿势鲁棒性差的情况，此外，PIFu 过高的几何表达自由性带来的是很差的稳定性，而这恰恰是另一个 trade-off 问题。一个有效的解决方法就是增加几何先验，将任务转换为给定一个粗糙的人体几何来雕琢细腻的穿衣人体。因此 PaMIR 应运而生，其将另外一个热门的人体重建表示方法 SMPL 作为人体几何先验引入了隐函数重建当中。相比于刚刚诞生的隐函数人体重建，SMPL 人体重建的工作已经发展了数年的时间，在当时也有了一些不错的成果，比如一些在 SMPL 上加载非参数的体积表示方法 Deephuman。但其大多难以实现高分辨率的重建。另外，我们的任务输入只有一张 RGB 图片，在现有的工作中，我们无法估计出极为准确的 SMPL 模型（训练和测试仍有差距）。于是一个朴素的想法便出现了：我们能不能在 SMPL 估计的时候使用准确的 ground truth 进行监督，而在后续网络的雕琢过程中使用预测出来的 SMPL 模型进行推理呢？这样做不就可以降低后续网络对 SMPL 模型的依赖了吗？理想是丰满的，现实却是骨感的。由于图像在深度信息上的模糊性，不可能保证预测的 SMPL 和 实际的 SMPL（Ground Truth）在深度上保持一致。因此我们会迫使网络在“错误的条件”下给出“正确的答案”，这对网络来说是灾难性的。因此，作者结合这些背景提出了以下创新思路：加入了参数化的人体模型 SMPL 作为先验信息，使得后续网络专注于表面的雕琢而无需关注更多任务。为了避免上述深度模糊的问题，引入了一个新的深度模糊损失函数对网络进行优化。提出了一个基于优化（optimization-based）的人体模型优化方法，在提高精度的同时增强了 SMPL 与隐函数输出的一致性。此外，由于作者使用了不同来源的信息，因此其内部建立了不同域之间的隐式对应关系，使网络更具有拓展性，比如我们可以替换之前的模块 SMPL 来对网络进行训练，从而改变网络的输入等。Methodpipeline我们先来看看 PIFu 的隐式函数：还是熟悉的配方，还是熟悉的味道：我们通过隐函数 F 通过给定一个采样点 P 的特征来判断该点在不在曲面上，从而实现对人体的重建。在 PIFu 当中，特征的提取有以下公式：在这个过程中，有两个基本模块：通过图片提取到的特征 S 和深度信息 Z 。这里对前半部分再次进行解释：Fi 是图像编码器，而 Π（p）则是 p 点在图像上的 2d 投影，网络通过双线性插值 S 对其采样。我们再来看看 SMPL 的参数化身体模型：这是经典的 SMPL 公式，在本文当中，作者使用了当时的 SOTA 模型 GCMR 来提取 SMPL 的人体信息。这个模块是可以被替换的，你可以替换成任意的当前是 SOTA 的 SMPL 估计模型，接下来便是重头戏了，作者直接在 PIFu 的基础上对重建的函数做出了修改：是不是很简单？作者直接将 PIFu 的特征提取模块进行了保留，只把深度信息 Z 替换成了 SMPL 信息。作者认为，SMPL 信息当中已经包含了足够的 3d 信息，不再需要额外的深度信息进行辅助了。在实际操作中，作者将 SMPL 模型转换为了占用体积 V0（可以理解为体素），并通过编码器获取到了公式中的 SMPL 特征 FV，并通过双线性插值采样到了 FV 中 p 点的特征。随后直接怼到隐函数中进行重建。整体的 pipeline 如下：深度模糊的重建损失在实际训练过程中，为了降低对精确 SMPL 模型的依赖，作者也沿用了我们之前提到的思想：在 SMPL 估计的时候使用准确的 ground truth 进行监督，而在后续网络的雕琢过程中使用预测出来的 SMPL 模型进行推理。但面对真实的 SMPL 和推测的 SMPL 的深度不一致问题，传统的使用均方差直接优化预测点和真实点的方法并不够用，因此作者额外提出了深度模糊的重建损失简单的均方差，但是作者额外加入了 Δpi 来补偿人体深度的偏移。动机是，我们其实不在乎回归出的人体模型中心的绝对坐标到底在哪，中心可以是 (0,0,0) ，也可以是 (100,100,100) ,这都没有关系。只要人体的几何曲面保持一定，人体的绝对坐标在哪里其实对我们来说都是一样的。因此最后的损失函数可以剥离掉绝对坐标的部分，只考虑模型几何轮廓的差异，作为损失函数就可以了。而在这里，Δpi 为：也就是说，其补偿了深度。其中 Δzi 通过在网络训练中利用真实的 SMPL 和预测的 SMPL 的显式关系实时计算得到：其中后半部分是 SMPL 顶点的深度值和实际点的预测值之差，Ni 是离采样点 Pi 最近的 SMPL 上的顶点集，其包含了 4 个离 Pi 最近的点。而中间的分数部分上面是其对应的混合权重，下面是对该权重做了归一化：整体的损失函数如图所示：基于优化的身体参数优化在获得 SMPL 参数的领域，基于优化（Optimize-Based）的方法一般都要好于基于回归（Regression-Based）的方法。但是遗憾的是，基于优化的方式不可微，所以我们只能在 inference 的时候用用，我们通过优化以下式子来提高对 SMPL 的估计：其中后一项是正则化项，惩罚 β和 θ 和初始预测 init 的差异：前一项则是通过以下式子意图鼓励最终预测和 SMPL 的预测对齐：同样的，ns 为顶点个数，q 则是定义的惩罚 loss：在实际应用中，η=5，我们可以看到，对于落在 SPML 表面之外的点，我们会施加一个惩罚，并使用其更新姿势和形状参数。来鼓励最终预测和 SMPL 的预测对齐。特别的，我们对落在表面外侧的点施加了更多的惩罚，这是因为我们有可能会遇到裙子等，我们希望这里的点多收缩点，收缩到人体表面，别把 SMPL 带歪了。颜色推理这部分比较简单，PaMIR 把 PIFu 当中的网络搞成了 RGBα 向量，多了一个预测值，作者称其为混合值，通过 α，我们将从图片中观察到的颜色和预测的颜色做了混合：颜色重建损失如下：和 PIFu 的相差不多，应该很好理解，值得一提的是，作者在这里使用了 L1 范数，这进一步使得颜色不那么平滑。
【论文阅读】PIFuHD: Multi-Level Pixel-Aligned Implicit Function for High-Resolution 3D Human Digitization
PIFuHD 是 PIFu 系列的第二篇论文，是 PIFu 的改进版本。本文记述了论文的核心思想。Motivation在之前的工作中，PIFu 已经证明了隐式函数可以优雅的实现通过对单张照片对人进行重建。但 PIFu 依旧存在着缺陷。PIFu 只能处理低分辨率的图像，这主要是计算机领域的经典的 trade-off 问题导致的：要想高质量细节（局部信息）就一定要提升分辨率来增强细节信息，而提升分辨率感知域（全局信息）又上不去，PIFu 之所以有着良好的表现，就是因为其对全局信息的良好把控。这严重影响了 PIFu 的进一步发展，因此为了改进 PIFu 的固有缺陷，有必要对其进行进一步改进。另外，PIFuHD 还提出了一种有效处理未观测区域（如背面）的方法，从而获得了更完整的的重建效果。Method整体流程PIFuHD 通过 coarse to fine 解决了这个问题，要想理解也很简单，其使用了两个分辨率不同的模块，一个负责提取全局信息（上半部分），另一个提取局部细节（下半部分）框架上看，PIFuHD 依旧沿用了 PIFu 初始的框架，是不过分成了两个部分，一个提取全局特征，一个提取局部特征。为了提升重建模型的质量和精度，该文先使用方法预测该图片对应的正面和发面的法向图，和原始 RGB 图片一起作为两个模块的输入图片。也就是说，PIFuHD 的主干部分输入的是三张图片：RGB 相片以及正反面的法向量图。法向量图的预测作者认为，讲法向图放到特征提取之前更能让之后的网络学习到锐利的 3D 几何。作者直接使用了 pix2pixHD 网络，直接从 RGB 彩色图预测改图正面和反面的法向图。作者认为这样得到的看起来合理的法向图对于重建三维人体就足够了。loss与 PIFu 中的简单的正则损失不同，PIFuHD 使用了 BCE(Binary Cross Entropy)损失函数计算一个集合的采样点的损失。其中 S 表示采样点的几何，λ 是采样点中在人体表面外的比例，f*(X)表示 X 的金标准的占有值。f{L,H}则是网络两部分预测出来的值。和 PIFu 一样，训练点的采样方法还是包含两个部分：在 3D bounding box 中的均匀采样和在人体表面顶点的周围进行的一个服从高斯分布的密集采样。
【论文阅读】PIFu: Pixel-Aligned Implicit Function for High-Resolution Clothed Human Digitization
本文介绍一可以通过单张或多张照片来进行三位人体重建的模型 PIFu，这也是 PIFu 系列的第一篇论文。项目地址在这里Motivation人体重建是三维重建当中的一个重要的分支，在元宇宙当中也有重要的应用意义。在某些特定的领域，比如人脸、人体或者是已知形状的人造物体当中，现有的技术已经可以做到通过单张照片对其表面进行重建。然而，由于无效的模型表示，即使对于特定领域的建模任务，重建结果的分辨率和准确性通常也是有限的。因此，作者通过引入一种隐式函数的方法，用于解决服装三维人体从单个或多个输入图像进行纹理表面推理的难题。现有的大多数 2D 图像处理模型都运用了全卷积网络结构来保持输入图像与输出之间的空间对齐。但这在 3D 当中并不能很好的工作。体素表示的方法可以很好的应用到全卷积网络结构当中，但其表示的内存密集型本质限制了它生成精细细节表面的能力。而基于全局上下文的表示拥有更高的内存使用效率，却不能保证保留输入图像的细节。类似地，基于隐式函数的方法依赖于图像的全局上下文来推断整体形状，这可能与输入图像不一致。在 PIFu 当中，其利用全卷积网络结构，将像素级别的局部信息和全局信息进行了对齐，并且不需要像基于体素的表示那样占用大量内存。这在诸如衣服等具有任意拓扑形状、高细节和高形变的物体重建上具有更好的表现。尽管之前也有工作使用了局部信息，但是其缺乏三维感知的特征融合机制，他们的方法无法从单一视图中推理出三维形状。这一点在 PIFu 上面得到了显著的改进。具体的来说，作者训练了一个编码器来学习图片中每个像素单独的特征向量，这个特征向量还考虑了其相对位置的全局上下文。给定这个每像素特征向量和从这个像素发出的相机光线的指定深度 z，我们学习了一个隐式函数，它可以分类与这个 z 深度对应的 3D 点是在曲面内还是在曲面外。特别的，作者的特征向量在空间上将全局三维表面形状对齐到像素上，这使得其能够保留输入图像中存在的局部细节，同时在不可见区域推断出可信的细节。此外，通过隐式函数，模型还可以通过回归 RGB 信息来对表面的颜色纹理信息进行预测。Method概述给定一个单一视图或者多视图照片，我们的目标是重建出一个具有几何形状和纹理的 3D 人物，并且其保留照片当中尽可能多的细节。隐式函数将曲面定义为函数的水平集 f通过隐式函数，我们虽然不能得到每一个点，但是我们可以很容易的判定一个点和这个曲面的关系。（可以参考这篇文章简单看看），并且这种表示方法对内存非常友好。文章提出的方法包括了一个全卷积的图像编码器 g 和由多层感知机（MLPs）表示的连续隐函数曲面 f。这个曲面可以表示为：对于 3D 点 X 来说，x = π(X) 是其在图像上的投影 ，z(X)是在相机坐标空间中的深度值(通过相机内外参数投影计算得到) ,F (x) = g(I(x))是卷积神经网络 g 提取到的图像 I 在 x 点的特征向量。作者假定相机是一个弱透视相机，在这里，由于 X 的投影的值是连续而非离散的，所以文章通过双线性采样来得到其的值。这么做最终的结果是，我们通过像素级别的信息而不是全局信息在 3D 空间学习了一个隐式函数，这样就保持了图片局部的细节信息。这种方式也具有更高的内存效率，并可以处理任意的拓扑结构。此外,PIFu 可以被转换为一个通用的框架，可以扩展到各种 co-domains ，比如 RGB 颜色。我们不需要更改输入，只需要其中有这样就可以直接预测颜色信息了。整个的网络结构可以用这个图来看单视图重建上面我们已经讨论了隐式函数的意义，在保持高效的内存效率的同时，隐式函数还可以轻易的判断一个点在平面内还是平面外，换句话说，我们已经变相重建出了整个平面。但是由于隐式函数的性质导致，我们并不能直接获得这个平面上的任一点。因此我们定义：当 f(X)=1 的时候，这个点在平面内，反之在平面外。这样当 x=0.5 的时候正好在平面上。为了学习到这个平面，我们通过最小化均方误差来将隐式函数和 ground truth 相接近。在这里，多层感知机和全卷积网络是一同更新的。当这两项学习好了之后，在推理阶段，输入就是一张图片，和对应的相机参数，以及该图片中人体所处的一个大致范围（Bounding Box）对于 Bounding Box，我们可以在三个维度上进行离散化，比如每个维度 512，就可以得到 512×512×512 个点阵，对所有的点输入到函数中，那就可以得到如下图一样的 3D Occupacy 信息，然后跑一遍 Marching Cube 就可以得到物体的模型。 Marching Cube 的方法可以参考这篇文章空间采样在实际训练过程中，这 512 维度的点的选取也有很大的学问，作者发现，如果在图像中进行均匀的随机采样，会导致大部分的点在人物外部，在等值面上的点会很少。而只在等值面上采样又会导致过拟合。这直接影响了人体重建的性能。我们更需要关注的是隐函数所表达的这一曲面（即人体表面）的信息，因此作者在表面点附近加上了一个高斯抖动来采样这些点。通过调参，作者发现等值面附近加上 5cm 的高斯抖动，与均匀采样 16：1 混合 这样可以更好的采到表面的点。纹理颜色推断上面我们已经讨论过颜色的问题，我们并不需要改变输入，只需要将其输出改变即可。那么我们就有这个公式和上述的预测曲面的损失函数差不多。只是换成了 RGB 信息 C。但是这样的预测在实际的颜色预测当中具有严重的过拟合现象。这是因为整个网络即 fc 不只是在预测表面的颜色信息，他还需要去预测看不见的 3D 空间中的纹理信息，这样它才可以推断不同 pose 和不同形状的人体的不可见部分（比如背面顶点）的颜色信息。因此作者做以下改动：基于上一步骤中的到用于重建的深度特征 Fv，将公式改动为：此外，作者依照采样那边的方法，在颜色预测方面也加入了一个随机的偏移，这样就可以预测表面周围的颜色信息，这也是增强对看不见的地方的预测能力。多视图预测在单视图的基础上，模型还提供了多视图的重建方法。更多的视图提供了额外的信息，重建出来的结果也会有更好的结果。总的理念来说，作者就是通过单试图进行了简单的推广。就是将提取到的某个 3D 点的特征进行平均 ，再输入到然后输入到 Pixel-Aligned Implicit Function 网络当中进行预测。总结PIFu 的开山之作，也是将隐函数放到三维人体重建当中的一篇经典文章。全篇文章读下来其所用到的方法都比较流畅，也比较有建设性。但是还是有比较多的限制，比如相机 fov 的参数等对重建的结果影响都比较大。在多视图方面，如果多视图相机的 fov、距离等不同，对重建效果都有很大的影响，这些都是亟待改进的地方。
【论文阅读】Self-Supervised Out-of-Distribution Detection and Localization with Natural Synthetic Anomalies (NSA)
Abstract我们引入了一个新的自我监督任务 NSA，用于训练仅使用正常数据进行异常检测和定位的端到端模型。 NSA 使用了泊松图像编辑从单独的图像中无缝混合各种大小的缩放块。 这种编辑产生了范围广泛的异常合成，与以前的自我监督异常检测数据增强策略相比，这些异常更类似于自然图像的不规则性。 我们使用了自然和医学图像评估所提出的方法，对 MVTec AD 数据集的实验表明，经过训练以定位 NSA 异常的模型可以很好地推广到检测现实世界中先验未知类型的制造缺陷。 我们的方法实现了 97.2 的整体检测 AUROC，优于所有以前从头开始学习而无需预训练数据集的方法。Introduction异常检测是一种二元分类任务，其目的是将正常数据与异常示例分开。 根据可用的训练数据和标签，有不同类型的异常检测。 在实际情况中，我们往往只能拿到正常的样本。为了在实际应用中发挥作用，自动化系统必须能够检测到细微和罕见的异常。异常的罕见性导致了我们不可能为有监督的方法获取足够数量的人工注释训练数据。获得精确的真实标注非常耗时，并且需要该应用领域的大量专业知识。正因如此，仅基于正常数据的异常检测在许多领域都有应用例如医学图像中的无监督病变检测、工业生产管道中的缺陷检测以及在监控视频中检测异常事件。无监督方法的主要挑战是设计一种鼓励模型学习与异常检测相关的特征但无需事先了解预期的异常类型的训练方法。许多用于异常检测的无监督方法依赖于学习正常数据的特征，并重建这些特征来定义异常分数。因此，自监督学习正成为异常检测中的一种突出策略。通过设计适当的任务，自我监督可以成为监督学习的有效代理，从而绕过对标记数据的需求。虽然可以使用各种自监督任务，例如上下文预测或估计几何变换，来学习数据的压缩表示，但最近的工作表明，模仿真实缺陷的数据增强策略对于子图像异常特别有效检测。但是，使用 CutPaste 带来的异常具有明显的不连续性，这引起了人们对模型可能过度拟合合成操作的担忧。同时，外部补丁插值 (FPI)是为医学成像应用而设计的，因此可能会产生对工业缺陷检测等其他应用来说过于微妙的异常。我们引入了一种新的用于图像异常检测的自我监督任务，NSA，它使用泊松图像编辑从单独的图像中无缝混合各种尺寸的缩放和移位的补丁，并创建更自然的各种合成异常图像不规则性优于以前的自监督异常检测数据增强策略，例如 FPI 或 CutPaste。与 FPI 一样，NSA 可用于训练用于异常检测和定位的端到端模型，而不是为 multi-stage 类的模型生成压缩表示。我们在 MVTec AD 数据集上评估所提出的方法，该数据集包含正常的训练数据以及针对各种自然和制造缺陷的正常和异常测试数据 10 个对象和 5 个纹理类。 NSA 在从头开始学习的方法中实现了新的最先进的定位 (96.3 AUROC) 和检测 (97.2 AUROC) 性能。 它的性能也与使用在 ImageNet 上预训练的模型的最佳方法相当。 与 ImageNet 中的大量数据相比，我们的方法仅使用 MVTec AD 数据，每个类别包含 60 到 391 个训练图像。NSA 是一种非常通用的方法，用于在图像中创建多样化和逼真的合成异常，其应用不仅限于自然图像。 我们使用公共胸部 X 射线数据集的精选子集来评估 NSA，发现它在疾病检测方面优于其他最先进的自我监督方法。Related Work基于重构的异常检测 其使用了变分自动编码器 (VAE)、贝叶斯自动编码器、生成对抗网络 (GAN) 或使用矢量量化 VAE (VQ-VAE) 的恢复距离从像素级重建误差建立像素级和图像级异常分数用正常数据训练。 可以通过利用从模型中导出的附加信息来提高异常分数，例如使用 GAN 时的鉴别器输出、图像级分数的 VAE 潜在表示的 KL 散度或其像素级的梯度分数，或使用 VQ-VAE 或潜在空间自回归在学习的先验下潜在表示的可能性。 这些方法的一个缺点是难以控制模型的容量。 根据正则化，该模型有可能不能很好地重建正常示例的所有细节。基于嵌入的异常检测 其通过从正常训练图像的嵌入向量与其测试示例之间的差距得出异常分数。 嵌入相似性度量可以使用任何一类分类方法来定义，例如使用支持向量数据描述 (SVDD)、高斯分布，或使用 KNN 算法。 嵌入向量的特征通常是从预训练的深度神经网络中提取的，但也可以使用自监督任务或与 Deep-SVDD 中的 one-class classification 目标一起从头开始学习（或二者的组合）。 当使用整个图像的嵌入时，基于嵌入的方法可以执行异常检测，但无法定位异常位置，因此可解释性较差。 为了规避这个问题，后续研究中使用了补丁级嵌入来创建异常图，而将测试图像与像素级训练集中的最近的图片进行比较从而定位异常。自监督学习 自监督基于未标记数据定义的代理任务产生的监督信号，例如预测补丁的相对位置或估计几何变换，可以帮助模型学习下游任务的有用特征。 在使用从代理任务中学习到的特征来发现不同的对象类别的同时，自监督学习也成功地应用于子图像异常检测。 在《Detecting Outliers with Foreign Patch Interpolation》 中，自监督任务的输出是对插值因子的预测，其中来自训练分布中另一个观察的外部补丁已混合到当前图像中。其直接将此输出用作异常分数，无需任何进一步的训练步骤。 我们依旧为我们的方法采用了这种通用设置。泊松图像编辑 将一个图像的一部分粘贴到另一个图像中会导致明显的不连续性。因此产生了一种将对象从一个图像无缝克隆到另一个图像的方法。由原图像 g 和目标图像目标图像 f∗。我们在区域 Ω 的内部寻找一个插值 f，边界为 δΩ，以解决由 (1) 给出的最小化问题。其具有泊松偏微分方程（2）的唯一解，其中狄利克雷边界条件由目标图像给出。在这里给出了定义梯度场的两种方法：a) 使用源图像梯度 (3) 或 b) 源梯度和目标梯度的混合 (4)。在实际应用中，对 (2) 的有限差分离散化进行了数值求解。无缝克隆在我们在自我监督任务中使用的 OpenCV 库中实现。NSA Self-supervised task由于在训练时只有正常数据可用，因此需要使用代理任务对模型进行训练。 在我们的例子中，任务是通过将源图像中的补丁混合到目标图像中来定位从正常数据创建的合成异常，如下所示：在源图像中选择一个随机的矩形块。随机调整补丁大小并选择不同的目标位置。将补丁无缝融合到目标图像中。或者，重复步骤 1-3 以将多个补丁添加到同一图像。创建一个像素级标签掩码。更正式地说，给定两个正常的 N × N 训练图像 xs 和 xd，我们在源图像 xs 中选择一个宽度为 w、高度为 h 和中心 (cx; cy) 的随机矩形块 ps，其中：从截断的 Gamma 分布中采样宽度和高度意味着我们假设异常是局部的（小），但我们也希望模型也能够识别更大的不规则性。因此，也会生成一些细长的矩形和偶尔的大块。宽度和高度范围是根据对象的尺寸选择的。 对于包含对象和纯背景的图像，我们通过将像素级绝对差与背景亮度进行阈值化来计算对象掩码 ms 和 md。 对于每个像素 i，掩码由下式给出：我们重复应用公式(7) 直到以确保补丁包含对象的一部分。 然后我们调整补丁的大小以获得宽度 w0 = sw 和高度 h0 = sh 的 p0 s。 我们在目标图像 xd 中选择一个具有相同尺寸和中心 (c0 x; c0 y) 的目标补丁 pd 满足：为了防止创建许多漂浮在背景中的补丁示例，我们重复应用 (10) 直到且有其中 mpd 和 mp0 s 是源和目标补丁的对象掩码。 我们在位置 (c0 x; c0 y) 处将 p0 s 无缝融合到 xd 中，以获得训练样本 xe。 在混合第一个补丁之后，我们通过翻转 n - 1 个模块来添加 n - 1 个进一步的补丁， 图 1a 显示了如何创建合成异常的简化轮廓。我们使用引入外来补丁的局部强度差异来创建像素级标签 y~，它是 a) 二元：是否存在差异，b) 基于 C 个颜色通道的平均绝对强度差异连续 , 或 c) 前一个的逻辑函数。 所有标签都经过中值过滤以更加连贯。 在过滤之前，每个像素 i 处的标签值计算如下：相比之下，FPI 使用补丁插值因子作为标签。 这略微有些不合适，因为在不知道源补丁和目标补丁的像素情况下无法确定插值因子。 我们的标签与强度的变化（由补丁混合创建）直接相关，因此提供了更一致的训练信号。当使用有界标签（yebinary 或 yelogistic）时，我们使用二元交叉熵损失来定义我们的逐像素回归目标。 对于无界标签（yecontinuous），我们使用均方误差损失。 损失在 (14)–(15) 中给出，其中 yb = f(xe) 是深度卷积编码器-解码器的输出。通过改变大小、纵横比、源和目标位置以及调整补丁的大小，该方法在训练期间动态创建了广泛的合成异常。 这些示例通过混合包含一些背景的补丁来显示局部图像组件的大小、形状、纹理、位置和颜色的变化以及缺失的组件，同时保持图像的整体分布并避免明显的不连续性。 因此，这些示例是自然子图像异常的更真实的近似，而不是通过简单地将补丁粘贴在不同位置构建的 CutPaste 增强，并且比在 FPI 中从对应位置的两个单独图像插值补丁更多样化，尽管仍然对人类观察者来说特征是人工的（图 1b）。
【神经网络】浅析一种新的目标识别网络——Swin-Unet
最近写了一篇应用类论文，这也是我在初期学习 Swin-Unet 时所作的归纳笔记。突然想起自己还有个博客（笑），遂将他们整理起来放在这里以备日后查阅。前言深度学习是机器学习领域中一个新的研究方向，其在视觉处理、数据挖掘、自然语言处理等领域都取得了很好的成果。目标识别作为其中的一种，已经在人脸识别、车辆检测、卫星图像分析当中取得了广泛的应用。在遥感领域，基于深度学习来进行云分割的方法也受到了研究人员的青睐。在之前的研究中，卷积神经网络（CNN）在目标识别网络中取得了里程碑式的进展，在多种图像处理任务中取得了优秀的成绩，但是，由于卷积操作的局限性所限，其无法学习全局，完成远端的信息交互。Swin-Unet 是一种基于图像分割经典模型 U-Net 改进的纯 Transformer 模型，相比于其他图像分割算法，其在 U 型架构中，将图像块通过跳跃连接送到基于 Transformer 的 U 形编码器-译码器架构中，更好的进行了局部以及全局的语义特征学习，取得了更优的性能。后续，Swin-Unet 也被应用于包括车辆检测在内的其他图像分割领域，提升分割的准确率。transformer 架构的优势目前的图像分割主要基于全卷积神经网络（FCNN)，其中以 U 型网络结构最为常用。传统的 U 型网络，如 U-Net，是一个对称的编码-解码网络结构，并且包含跨层连接（skip connection)。编码器通过一系列卷积和下采样操作提取不同感受野下的语义特征；解码器通过上采样操作将提取到的特征图恢复到原始分辨率，进行像素级别的密集预测；跨层连接则负责将同层的编码器提取的信息与解码器进行融合，从而减少因为下采样操作导致的空间信息丢失问题。通过精巧的结构设计，U-Net 已经在诸多图像分割任务中取得优异的效果，但其仍然局限于 CNN 模型，无法捕捉全局和长程的语义信息。Transformer 最早出现在论文 Attention is all you need 当中，随后在 NLP 领域中受到了广泛的应用。受 Transformer 在 NLP 领域中的启发，目前研究人员整将 Transformer 迁移到计算机视觉领域。本文中使用的 Swin-Unet 即是一种纯 Transformer 架构。具体的架构长什么样网上都有详尽的解释。总而言之，与 CNN 相比，其计算两个位置之间的关联所需的操作次数不随距离增长，这意味着其可以更好地捕捉到全局的语义信息。此外，Transformer 的自注意力(Self-attention)机制可以产生更具可解释性的模型，各个注意头 (Attention head) 可以学会执行不同的任务。Swin-Unet 网络整个 Swin-Unet 网络框架可以分为以下三个部分：编码器（左侧部分），解码器（右侧部分）以及 skipconnection（中间跨线部分），如图 所示。1）编码器该模块首先将输入的图片切分成大小互不重叠的分块，随后度输入分块进行线性映射。 随后映射过的 token 被送入 Swin Transformer 模块和 patch merge 层用来生成不同尺度的特征表述；Swin Transformer 模块负责学习特征，patch merge 层负责下采样操作。2）解码器解码器由多个 Swin Transformer 模块和 patch expanding 层组成，跨线依旧负责特征融合，弥补原始信息的丢失，patchexpanding 层则进行的是上采样操作，将该层 feature map 扩充至 2x 分辨率；最后一个 patch expanding 层会扩充至 4X 分辨率；然后通过线性层 进行像素级别的预测。3）跨线连接和传统的 U-Net 一样，跨线会以级联的方式将特征图输送到解码器当中，以此来融合多尺度特征。4）Swin Transformer 基础模块Swin Transformer 是整个网络最基础的模块，和传统的 MSA 结构并不一样，Swin Transformer 主要是基于滑动窗口（shifted window）构建的，下图展示了两个串联的 SwinTransformer 模块。每个 Swin Transformer 由层归一化(Layer Norm， LN)层、多头自注意力模块、残差连接和 2 个包含有激活函数 GELU 的多层感知机组成。在 2 个连续的 Transformer 模块中分别采用了基于窗口的多头自注意力模块(Window-based Multi-headSelf-attention）和 基于移动窗口的多头自注意力模块（Shifted Window-based Multi-head Self attention），前者用 w(·)表示，后者用 s(·)表示。因此，基于这种窗口划分机制的连续 Swin Transformer Block 可表示为:
本文将简要的整理一下我做的目标识别方向近几年的发展历程.7 月 4 日 新开这个博客，也为了督促自己下个学期多下些苦功。先要整完手头的这个神经网络。7 月 13 日 apex。7 月 14 日 apex。7 月 15 日 apex。7 月 16 日 胡适之啊胡适之！你怎么能如此堕落！先前订下的学习计划你都忘了吗？子曰：“吾日三省吾身。”不能再这样下去了！7 月 17 日 apex。7 月 18 日 apex。咳咳咳，总而言之，派派是真好玩。哪天搞个 switch 打方块可能会更好玩（不是那么今天，我就根据上学期老师讲的深度学习发展史来简要的整理一下我做的目标识别方向的发展历程。前言从人工神经网络被提出开始，这个话题就备受关注，计算机视觉在其中更是占据着重要的地位。而目标识别，作为计算机视觉最基本的方向以及应用，更是人们关注的焦点之一。随着算力的增长，技术也愈发成熟。本文将按照时间顺序来对整个目标识别的发展史进行叙述和整理。SIFT（1999）SIFT，即尺度不变特征变换（Scale-invariant feature transform，SIFT），是用于图像处理领域的一种描述。这种描述具有尺度不变性，可在图像中检测出关键点，是一种局部特征描述子。该方法于 1999 年由 David Lowe 首先发表于计算机视觉国际会议上。SIFT 算子是把图像中检测到的特征点用一个 128 维的特征向量进行描述，因此一幅图像经过 SIFT 算法后表示为一个 128 维的特征向量集，该特征向量集具有对图像缩放，平移，旋转不变的特征，对于光照、仿射和投影变换也有一定的不变性，是一种非常优秀的局部特征描述算法。VJ 检测器--人脸识别的突破（2001）VJ 检测器在 2001 年被 Paul Viola 和 Michael Jones 第一次提出，VJ 检测器在没有任何约束条件(如肤色分割)的情况下首次实现了人脸的实时检测，在当时的条件下，图像分类速度达到了惊人的 25fps，这在当时的算力条件下几乎是一个不可能的结果。这不仅是一个人脸识别方向上的里程碑，更预示着计算机视觉已经初步具备了步入实际应用的能力。VJ 检测器采用了滑动窗口的检测方法，VJ 检测器采用了三种方法来大大增加他的检测效率：特征的快速计算————积分图高效的分类学习方法————AdaBoost高效的分类策略————级联结构HOG 特征算法（2005)HOG（Histogram of Oriented Gridients 定向梯度直方图）本质上来说是一种特征提取算法，也是对当时的尺度不变特征变换（scale-invariant feature transform）和形状上下文（shape contexts）的重要改进。从名字上来看，我们不能难理解他所描述的是什么东西，百度百科中写出了其中心思想：在一副图像中，局部目标的表象和形状能够被梯度或边缘的方向密度分布很好地描述。其本质为：梯度的统计信息，而梯度主要存在于边缘的地方。换句话说，HOG 主要是靠边缘的纹理（梯度）来进行特征的提取。HOG 最终提取到的是梯度的统计信息。在 HOG 中，图像被从小到大分为了：细胞（cells），块（blocks），检测窗口，整个图像。在具体操作中，由于我们统计的是梯度信息，因此我们并不需要色彩等，我们先对图像进行色彩和伽马归一化。然后我们对每一个 cell 中的像素进行梯度的计算，这个计算包括幅值和角度并把它按照一定的格式存放在直方图中，这就是这个 cell 的 descriptor（描述子）。以此类推，几个 cell 的 descriptor 可以组成 blocks 的 descriptor（当然这里需要正则化）。blocks 又可以构成检测窗口的描述子。而这个结果就可以送到 SVM 中来进行分类了。Deformable Part-based Model (基于可变形部件的模型，DPM)（2008）DPM 最初是由 P. Felzenszwalb 提出的，于 2008 年作为 HOG 检测器的扩展，之后 R. Girshick 进行了各种改进。DPM，顾名思义，其采用了分而治之的思想，例如我们要识别人体，其将人体看作为手臂、腿、身体和头的有机组合，并且手和头、躯干距离不能太远，否则也会影响评分。这种方法看似非常简单，但其中所蕴含的数学原理非常深厚。虽然当下的物体检测器的准确度已经远远超过当时，但是其混合模型、硬负挖掘、边界盒回归等应用依旧经常出现在我们的视野中。RCNN 家族———"two-stage detection "RCNN（2014）RCNN，刚开始入门的时候就听过的及其著名的算法。在目标检测方面，RCNN 绝对是祖师爷级别的地位。他属于典型的“ two-stage detection ”，其原理并不难懂：首先提出了很多先验框，每个提案都被看作是一个固定大小的图像，接着再去判断这些图像是否包含所需要识别的东西。RCNN 的原文属实让人一言难尽，归纳起来大概步骤如下：ROI（region of interest 感兴趣区域)的生成。该网络将输入的图像进行初步的提取与拼接（包括梯度、颜色、面积等等），初步形成有一定语义的 ROI接着进行特征的提取，在这里区别于之前的方法，RCNN 采用了卷积神经网络来进行特征的提取。训练使用的是 SGD预测，这里会给每个先验框一个分数。紧接着将分数不高的先验框完全抛弃。对先验框进行线性变换来达到精修的目的。SPPNET（2014）显而易见，RCNN 的缺点是致命的，在每幅图像上，RCNN 会生成近两千个先验框，其庞大的运算导致了该方法在速度上并不占优。事实上，在 GPU 下，每张图片的运算时间达到了 14 秒。而 2014 年晚些时候 SPPNET 的提出，解决了这一问题。我们知道，在卷积这一过程中，卷积核可以适应任何大小的输入，但是全连接层则不可以。如果输入的大小不确定，全连接层连参数的数量都没有办法确定。也正因如此，在之前的 CNN 网络中采用了固定尺寸的输入。而在 SPPNET 中给出的解决方案是：在最后一个卷积层上，使用了一个相对比例的池化层，这个池化层可以将输出固定尺寸的矩阵供全连接层使用。在这里，SPPNET 引入了 SPM 的思路。对于卷积层的特征，我们先将其分成了不同的尺寸，在每个尺寸中提取一个维度的特征，接着进行拼接，就形成了一个固定维度的输入。SPPNET 对于这些方法的使用，大大增加了原本 RCNN 的速度。但其也有不足之处。SPPNET 只对全连接层进行了微调，而对之前的层进行了忽略，其次，在 RCNN 中使用的 selective search 的方法提取特征是在 CPU 上进行的，相对于 GPU 来说还是比较慢的。这些都是限制 SPPNET 的因素。Fast RCNN（2015）FAST-RCNN 是对之前的进一步改进，从 RCNN 我们可以看到，在先验框的计算上我们耗费了太多的时间。FAST-RCNN 在这一点上进行了改进，其先将整个图片进行了输入，在得到特征图后，再特征图上来寻找相对应的先验框。这样做使得原来先验框重叠部分的卷积运算只计算了一次。大大减少了计算数量。此外，该模型在经过 fc 层之后，直接取消了 SVM 分类器和回归器，取而代之的是两个训练器。一个 softmax 用于分类，另一个 regressor 用于先验框的精修。因为有两个任务，所以使用了多人损失将分类和回归损失结合起来，然后对整个网络进行训练即可。Faster RCNN（2015）不难看出，虽然 Fast RCNN 解决了很大一部分重复计算的问题，但是从根本上，2000 多个先验框还是一个庞大的计算量。于是一个问题自然而然的浮现：我们是否可以用 CNN 模型来自己生成对象建议呢？Faster RCNN 则解决了这一问题。Faster RCNN 最主要的贡献是引入了区域建议网络 RPN。在特征提取之后，用 RPN 先生成一堆 Anchor box，对其进行裁剪过滤后通过 softmax 判断 anchors 属于前景(foreground)或者后景(background)，即是物体 or 不是物体，所以这是一个二分类；同时，另一分支 bounding box regression 修正 anchor box，形成较精确的 proposal（注：这里的较精确是相对于后面全连接层的再一次 box regression 而言）。至此，对象建议被神经网络生成完毕。Faster RCNN 是第一个端到端的神经网络检测器，提案、提取、检测、回归等都集成在了一个神经网络当中。这个成就是前所未有的。Faster RCNN 将原本的 2000 多个先验框创造性的减少到了 300 个左右，并且其先验框的质量有了明显的提升。当然，Faster RCNN 的这 300 个先验框还是有提升的空间，在之后出现了各种版本的改进，在此不一一介绍。“ one-stage detection ”家族区别于“ two-stage detection ”，“ one-stage detection ”并没有使用 RPN 网络，而是利用主干网络一步到位，直接给出目标的位置和大小。这样做的算法速度更快，但是精度相应的会损失一部分。目前其中的 yolo、ssd 等算法非常热门，资料也非常多，不像我找个 SIFT 的论文费劲的要死（笑），这一部分我就会进行简要的介绍，毕竟资料太多了。You Only Look Once (YOLO)（2015）YOLO 算是最近非常火爆的框架之一了。我身边有不少同学都在使用 opencv 加 yolo 做一些目标识别方面的项目。YOLO 在 2015 年被第一次提出作为最具标志性的深度学习产物，该网络将图像分割成多个区域，同时预测每个区域的边界框和概率。后来 R. Joseph 在 YOLO 的基础上进行了一系列改进，提出了其 v2 和 v3 版本，在保持很高检测速度的同时进一步提高了检测精度。至今为止 yolo 已经有多个版本。Single Shot MultiBox Detector (SSD)（2015）相比 Yolo，SSD 采用 CNN 来直接进行检测，而不是像 Yolo 那样在全连接层之后做检测。其实采用卷积直接做检测只是 SSD 相比 Yolo 的其中一个不同点，另外还有两个重要的改变，一是 SSD 提取了不同尺度的特征图来做检测，大尺度特征图（较靠前的特征图）可以用来检测小物体，而小尺度特征图（较靠后的特征图）用来检测大物体；二是 SSD 采用了不同尺度和长宽比的先验框。Yolo 算法缺点是难以检测小目标，而且定位不准，但是这几点重要改进使得 SSD 在一定程度上克服这些缺点。RetinaNet（2017）单级检测器速度快、结构简单，但多年来一直落后于两级检测器的精度。T.-Y.Lin 等人发现了背后的原因，并在 2017 年提出了 RetinaNet。他们声称，在密集探测器训练过程中所遇到的极端的前景-背景阶层不平衡（the extreme foreground-background class imbalance）是主要原因。为此，在 RetinaNet 中引入了一个新的损失函数 “ 焦损失（focal loss）”，通过对标准交叉熵损失的重构，使检测器在训练过程中更加关注难分类的样本。焦损耗使得单级检测器在保持很高的检测速度的同时，可以达到与两级检测器相当的精度。后记期末考试之后贪图玩乐，确实耽误了不少时间，整理这篇文章所用到的资料也着实令人头痛。很多想写的东西因为着实不知道该怎么下笔而没有落在纸面上，比如非常重要的 FPN、Faster RCNN 后面涉及到的各种各样的改良版本、前一阵想去研究的 FBNET 等等网络在这里都没有一一介绍。后续我会将这些内容一一补上。（全文完）
本文主要记述了深度学习中 batch_size 这一超参数的选取以及一些其他经验注意：本文所针对的是 SGD 来说，Adam 等方式不在讨论的范围之内，另外似乎我并没有见到有关 Adam 收敛性的有关证明，如果 Adam 其实已经被证明了，烦请给菜狗提个醒，菜狗不胜感激 QAQ。前言前一阵想到了个不错的点子，准备水个论文（不是）。大概的方向是用特殊的网络结构来缩减运算量而不影响精度，经过理论上的推断觉得可行后，在几周前，我开始着手网络结构的搭建和测试。令人感到困惑的是，test 集的 acc 始终上不去（后来证明是数据集的问题，不过那是后话了）。当时的我将注意力放在了超参数上面，而作为最好调的超参数没有之一，batch_size 自然就成了我关注的第一个参数，由此引发了后面的一系列问题。一些问题事情的起因是我看到了知乎上的一篇回答，它的中心思想是也就是最好的实验表现都是在 batch size 处于 2~32 之间得到的，这和最近深度学习界论文中习惯的动辄上千的 batch size 选取有很大的出入。这个想法让我有点迷糊，诚然，现在已经证明了过大的 batch_size 对训练神经网络无益，在训练神经网络的过程中，我们需要小一些的 batch_size 来引入一些必要的噪声，以此来帮助神经网络摆脱鞍点，提升网络的泛化性能。并且受到算力的影响，90% 的用户是没有办法拿出足够的显存来运算上千的 batch_size。但我依然对 2-32 大小这个结果抱有疑问，在我看来，太小的 batch_size 同样存在致命的问题。一方面，过小的 batch_size 引入的噪声可能过大，这并不是一个好消息。噪声小，数据整体分布较稳定，这是 BN 运行良好的条件。过大的噪声会导致是神经网络产生震荡，甚至是无法收敛。另一方面，过小的 batch_size 并不能充分发挥你显卡的性能，并且让整个网络并行度降到非常低的水平。用人话说就是训练过程十分漫长且折磨人（事实上我已经被折磨了，详见后文）。这些都是无法避免的问题。我又检索了一下，事实上网络上用很小很小的 batch_size 的声音还是有一些的，他们中的大多数都指向了一篇文章：Revisiting Small Batch Training for Deep Neural Networks 。这篇文章详细的论述了在 SGD 下小 batch 所体现出的优越性以及一些理论证明。但实验部分我还是存疑的，这实验结果我并不敢苟同，遇到类数多的，噪声较大的数据集，感觉 32-2 大小的 batch_size 甚至并不能收敛。我觉得不能一味听信论文瞎扯，有时候还是得自己试试 23333实验由于是学生党，实验设备着实有限。在这里我用到了两张 3090 和两张 RTX TITAN 来进行实验，3090 跑一遍，TITAN 再来一遍。数据集我选用了 cifar-10 作为数据集，这个数据集类数相对较少，且噪声相对稳定。在论文中其也是用到了这一数据集作为一组实验数据，优化器当然使用了 SGD，每 40 代将学习率乘 0.2，每个不同的 batch_size 都使用了他们自己的最优学习率。每个训练跑 200 个 epoch。epochbatch_sizebatch_size512batch_size128batch_size32batch_size2epoch200.83300.82270.84870.5849epoch400.91800.88200.90140.7467epoch600.91310.89040.90060.7589epoch800.92500.89200.90960.7852epoch1000.92690.89380.91320.7921epoch1200.93190.89320.9146-epoch1400.93340.89380.9129-epoch1600.93230.89460.9140-epoch1800.93170.89500.9149-epoch2000.93160.89630.9154-平均一个 epoch 用时110 s145 s310 s3200 sTITAN 与 3090 跑出来的结果高度一致。可以看到，过小的 batch_size 效果并没有得到多少改善，甚至略有缩水。其间，我观察到了多次小 batch 下 test 集合的 acc 反复震荡， loss 函数也一直不能很好的改善的情况，这确实和我预想的一致。至于 batch 为 2 的实验组，它确实让我感受到了折磨（笑），按照计划，我设置了每个实验组跑 200 个 epoch，512 与 128 组在 24 小时之内就跑了个差不多，一天后 32 组也跑完了全部内容，而截止到这篇文章发布，batch 为 2 的这个实验组只跑了 113 代（已经过去 6 天了，我预计大概 11 天能跑完），并且出现了根本无法收敛的情况，test 的 acc 反复震荡。而且这个用时，我觉得是非常不友好的，至少我认为不是大多数人愿意看到的用时（笑）。可以预见的是，如果数据变得更加复杂（比如数据集变成类数更多的 cifar100 或者其他噪声更大的数据集），batch_size 为 32 的实验组大概也会变得难以收敛，这是非常不好的。相对来说，可能稍微大一些的 batch_size 确实会更加稳定。一些总结和建议在你的显卡不是特别强劲的情况下，请优先考虑把你的显卡盛满当你的显卡比较优异，可以容纳较大的 batch_size 时，请优先考虑 128 或者 256 大小的 batch_size。在我学习深度学习时跑过的诸多模型中，这两个值是我觉得还不错的。在此基础上，如果你觉得并不理想，请乘二或者除二，再来试试看。或者如果没有那么大的内存以及时间充裕的话，64 或者 32 也可能会是不错的想法，但是不要再小了。当然，小 batch 也有小 batch 的好处。在训练完一个模型后，用很小的 batch_size 再跑一跑，没准会有很好的效果（比如多了一个点的准确率），但是千万千万不要直接跑小 batch，这绝对是致命的。