[{"content":"这篇文章不是“按目录从上往下念一遍文件名”，而是把 Claude Code 当成一套生产级 Agent Runtime 来拆：它如何启动、如何构造上下文、如何驱动查询循环、如何执行工具、如何治理权限、如何承载异步任务与远程运行时，以及为什么这些设计能在复杂产品里长期活下来。\n为什么这份源码值得看： 这不是一个普通 CLI 项目的实现细节，而是一套成熟 AI Agent 产品的运行时样本。源码快照只是入口，真正有价值的是它背后的工程取舍：哪些能力被做成统一抽象，哪些风险被放进治理层，哪些复杂度被明确地收口了。 先说结论： Claude Code 的本体并不是一个终端聊天 UI，而是一套以 query.ts 为心脏、以 Tool/Task/Command 为统一抽象、以权限系统和扩展总线为边界控制的 Agent Runtime。 1902src/ 下源码文件数量 512,664TypeScript / TSX 总行数 564utils 文件数，说明基础设施极重 389components 文件数，终端 UI 也非常复杂 207commands 文件数，命令面很宽 184tools 文件数，工具系统是第一等公民 130services 文件数，协议与运行时服务层很厚 104hooks 文件数，UI 与策略被大量 hook 化 重要前提： 这个仓库不是一份完全“原汁原味”的 TSX 源码快照。像 src/components/App.tsx、src/state/AppState.tsx、src/screens/REPL.tsx、src/hooks/useCanUseTool.tsx 这类文件，已经出现了 react/compiler-runtime 和 sourcemap 内联痕迹。也就是说，这份仓库更接近“可阅读的构建产物快照”而不是最初作者提交的源码。因此本文会以运行时结构和调用关系为主，不会假装自己能还原所有原始编码细节。 全局结构 启动链路 状态与上下文 查询引擎 工具系统 权限与任务 MCP/插件/技能 远程与 Bridge 记忆与压缩 外围模块 模块索引 一、先给结论：这到底是一个什么系统 如果只看 UI，你会以为它是一个终端版聊天应用；如果只看 tools/，你会以为它是一个“给大模型挂了很多工具”的壳；如果只看 bridge/ 和 remote/，又会以为它是一个远程控制产品。真正准确的说法是：\nClaude Code 是一套面向 Agent 的运行时平台。UI、CLI、远程控制、MCP、插件、技能、异步任务，都只是这个运行时的不同入口和不同扩展面。 图 1：高层架构图。真正的主线是“入口分流 → 运行时装配 → 查询循环 → 工具/任务/权限 → 扩展总线 → 远程与持久化”。 从职责上看，这个系统可以拆成八层：\n入口层\n负责命令分流、懒加载和冷启动优化，核心文件是 entrypoints/cli.tsx。 组合根\n负责把配置、认证、策略、工具、命令、技能、插件、MCP、LSP 拼成一个完整运行时，核心文件是 main.tsx。 交互层\n终端 UI 不是被动显示，而是一个协调器，核心是 screens/REPL.tsx。 查询层\n每一轮 assistant 行为都由 query.ts 这个状态机驱动。 工具层\n工具不是简单函数，而是带 schema、权限、并发语义、进度语义的运行时实体。 治理层\n权限、任务、代理、预算、压缩都属于治理层，而不是某个单点功能。 扩展层\nMCP、插件、技能、命令、Agent 定义都被汇总到统一抽象上。 远程层\nBridge、远程查看器、直连会话、本地/远程 Agent，本质上都在复用同一套 Query Runtime。 这一点直接解释了为什么这个仓库会出现两个很明显的“偏置”：第一，utils/ 极多，因为这套运行时要处理大量杂事；第二，tools/、commands/、services/ 很厚，因为产品能力几乎都在这些边界里沉淀。\n二、启动链路：先短路，再装配，再进入交互 Claude Code 的入口设计非常成熟。很多 CLI 程序虽然有“主入口”，但实际上会把大量模块在顶层一股脑 import 进来，结果是任何命令都要付出完整冷启动成本。Claude Code 明显不是这么做的。\n图 2：启动链路。重点不是“主函数多短”，而是能否把不同命令路径在足够早的阶段分流出去。 src/entrypoints/cli.tsx 的职责可以概括成一句话：在尽可能少加载模块的前提下，把不同运行模式分流到对应子系统。 它做了三件很关键的事。\n把纯信息类命令做成零或极少依赖的快路径，比如 --version。 把长驻型命令做成独立支路，比如 bridge、daemon、ps/logs/attach/kill。 只有在需要完整会话时，才进入 init.ts 与 main.tsx。 async function main(): Promise\u0026lt;void\u0026gt; { const args = process.argv.slice(2) if (args.length === 1 \u0026amp;\u0026amp; (args[0] === '--version' || args[0] === '-v')) { console.log(`${MACRO.VERSION} (Claude Code)`) return } const { profileCheckpoint } = await import('../utils/startupProfiler.js') profileCheckpoint('cli_entry') if (args[0] === 'remote-control' || args[0] === 'bridge') { const { bridgeMain } = await import('../bridge/bridgeMain.js') await bridgeMain(args.slice(1)) return } } 这里最值得学的，不是“if 写得多”，而是把“命令分发”和“运行时装配”明确拆开了。这样一来，冷启动优化有了真正的抓手。\n进入 src/entrypoints/init.ts 之后，系统开始做真正的环境准备。这个文件的重要性在于，它不是简单“初始化配置”，而是在建立一条信任前初始化与信任后初始化之间的边界：\n先加载安全环境变量、关闭清理器、探测 IDE / 仓库 / 平台上下文。 并行启动远端策略、组织限制、代理证书、网络代理、OAuth 信息缓存。 为后面的完整会话准备一个“可信但尽量还没太重”的基础环境。 然后是整个系统的组合根：src/main.tsx。这个文件的重要结论不是“代码很多”，而是它承担了所有运行时对象的拼装工作。尤其有三个细节值得注意：\n它在 very early 阶段就触发 startMdmRawRead() 和 startKeychainPrefetch() 这样的并行预取，说明作者对冷启动路径做过实打实优化。 它不是先画 UI 再慢慢加载功能，而是先把模型、工具、命令、插件、技能、权限模式、策略限制这些运行时能力准备好。 它大量使用特性开关和动态导入，所以这套代码库不是一个固定产品，而是一套支持多发行形态的可裁剪运行时。 最后，interactiveHelpers.tsx 和 replLauncher.tsx 把信任与交互兜起来。前者负责 setup screens、trust dialog、MCP 审批、API key 审批等交互边界；后者则非常克制，只负责在真正需要 REPL 时再懒加载 App 与 REPL。这说明作者很清楚：对一个终端 Agent 来说，真正昂贵的不是画 UI，而是把完整运行时装进进程。\n三、状态与上下文：不是 Redux，而是轻量 store 加显式上下文装配 这套系统的状态管理很有意思。它没有上来就用 Redux 或 Zustand，而是自己实现了一个极轻的外部 store，再用 React 做桥接。\nsrc/state/store.ts 基本就是一个最小实现：getState、setState、subscribe。真正重量级的不是 store 本身，而是 AppState 的定义。\nsrc/state/AppStateStore.ts 几乎相当于系统“控制面”的数据模型，里面包含：\n用户设置、当前模型、权限上下文、视图状态。 本地任务、远程会话状态、Bridge 状态、后台任务数。 MCP 客户端、MCP 工具、MCP 资源、插件状态、Agent 定义。 文件历史、Todo、推断状态、通知、Prompt Suggestion 等等。 也就是说，Claude Code 的状态设计不是“页面状态”意义上的状态，而是整台 Agent Runtime 的实时控制面。这和普通前端应用完全不是一个量级。\nexport type AppState = DeepImmutable\u0026lt;{ settings: SettingsJson mainLoopModel: ModelSetting toolPermissionContext: ToolPermissionContext remoteSessionUrl: string | undefined remoteConnectionStatus: 'connecting' | 'connected' | 'reconnecting' | 'disconnected' replBridgeEnabled: boolean }\u0026gt; \u0026amp; { tasks: { [taskId: string]: TaskState } mcp: { clients: MCPServerConnection[]; tools: Tool[]; resources: Record\u0026lt;string, ServerResource[]\u0026gt; } plugins: { enabled: LoadedPlugin[]; disabled: LoadedPlugin[]; needsRefresh: boolean } } src/state/AppState.tsx 负责把这个 store 桥接进 React。它做得比较克制：通过 useSyncExternalStore 暴露 selector，不允许随便把整个状态对象拿出来用，也防止嵌套 Provider。这说明作者很清楚，真正的风险不是“状态多”，而是“状态对象被到处随手读取”。\n与状态并行的，是上下文系统。src/context.ts 和 src/utils/queryContext.ts 负责把系统上下文与用户上下文装配出来：\nsystemContext 主要负责 Git 快照与运行环境信息。 userContext 主要负责 CLAUDE.md、记忆文件等用户持久上下文。 constants/prompts.ts 则负责把模块化的系统提示词片段拼起来。 这个设计的关键点是：上下文不是在 query loop 内临时拼字符串，而是被单独抽象成一套可缓存、可注入、可替换的构造过程。 这会大幅降低循环内部的复杂度，也避免依赖环。\n四、查询引擎：真正的核心是可恢复的多轮状态机 如果让我只挑一个文件来代表 Claude Code 的“灵魂”，我会选 src/query.ts。因为这里真正定义了系统“怎么像一个 Agent 一样工作”。\n图 3：查询循环。用户输入不是直接发给模型，而是先进入一套预处理、上下文组装、流式采样、工具调度、预算控制和压缩恢复的状态机。 这一层至少做了七件事：\n把输入消息、系统提示词、用户上下文、系统上下文拼成 API 请求。 流式接收 assistant 输出，并在 tool_use block 出现时触发工具执行。 决定哪些工具可以并发、哪些工具必须独占。 跟踪 token budget、max output token 恢复、stop hooks。 在上下文超限时触发自动压缩、微压缩或其他恢复路径。 把工具结果和压缩边界重新注回消息流。 最终把 assistant 输出与 usage 信息回写到会话状态。 这意味着 query.ts 本质上不是一个“调用模型 API 的函数”，而是一个带副作用、带恢复逻辑、带预算约束的回合状态机。\n其配套的 src/QueryEngine.ts 则把这套查询机制封装成 headless / SDK 可复用的对话引擎。它会保存：\n累计消息列表。 总 usage。 文件缓存。 权限拒绝记录。 跨回合的 abort controller。 这一步很关键，因为它说明 Claude Code 不是把“终端 UI 逻辑”和“模型回合逻辑”绑死了。UI 只是一个消费者，QueryEngine 才是可复用的内核。\n查询循环的前门是 src/utils/handlePromptSubmit.ts 和 src/utils/processUserInput/processUserInput.ts。这两层的职责是把“用户原始输入”编译成“模型真正看到的消息”。这里的实现思路很像编译器前端：\n先识别 slash command、本地 JSX 命令、桥接消息、元消息。 再处理粘贴内容、图片、附件、IDE 选区、hook 附加上下文。 最后决定这次输入究竟是触发模型查询，还是只做本地控制流。 也就是说，输入在进入模型之前，已经被完整归一化过了。这正是成熟 Agent Runtime 与“把文本直接发给 LLM”的根本区别。\n五、工具系统：不是函数库，而是一套协议化运行时 Claude Code 最强的部分之一，就是工具系统设计得足够“像运行时”而不是“像 util 函数”。\n核心契约在 src/Tool.ts。这个文件定义的不是一个简单的 call() 接口，而是一整套工具元数据：\n输入输出 schema。 用户可见名称与描述。 权限检查钩子。 只读属性、并发安全属性。 中断行为、进度事件、结果裁剪策略。 这意味着任何工具要进入系统，都必须先回答：它如何被模型理解、它是否可并发、它是否可能危险、它如何报告进度、它的结果如何进入 transcript。这样的契约足够厚，系统才可能稳定地不断扩展。\nsrc/tools.ts 则是全局工具注册表。这个文件透露出两个很重要的架构意图：\n工具集合不是静态的，会根据 feature flag、发行形态、环境能力动态变化。 工具的来源非常杂，但最终必须汇总到 getAllBaseTools() 这一处统一真相源。 export function getAllBaseTools(): Tools { return [ AgentTool, TaskOutputTool, BashTool, ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]), FileReadTool, FileEditTool, FileWriteTool, WebFetchTool, WebSearchTool, SkillTool, EnterPlanModeTool, ...(WebBrowserTool ? [WebBrowserTool] : []), ] } 这个注册表的“厚”是必要的。因为你只要支持 Agent、MCP、插件、工作流、远程触发、计划模式、Web 浏览器、Shell、文件编辑，这些能力就一定会在同一个地方相撞。Claude Code 没有回避这个复杂度，而是正面把它收口到了注册层。\n更精彩的是 src/services/tools/StreamingToolExecutor.ts。这个类说明 Claude Code 的工具不是“等模型整段输出完再执行”，而是一边采样一边执行工具。它同时满足三件看起来互相冲突的事：\n允许并发安全工具并行执行。 要求独占型工具串行执行。 即便并发执行，也要按原始工具出现顺序回放结果。 private canExecuteTool(isConcurrencySafe: boolean): boolean { const executingTools = this.tools.filter(t =\u0026gt; t.status === 'executing') return ( executingTools.length === 0 || (isConcurrencySafe \u0026amp;\u0026amp; executingTools.every(t =\u0026gt; t.isConcurrencySafe)) ) } 这段逻辑背后的思想很重要：并发不是全局开关，而是工具级别的语义。 一旦某个工具声明自己需要独占，它就会阻塞队列；如果全是并发安全工具，则允许流水线化执行。这个设计比“固定串行”或“无脑并行”都高明。\n再往下一层，src/services/tools/toolOrchestration.ts 负责把工具调用分成并发安全批和串行批，并且在并发工具执行后，再统一应用 context modifier，避免上下文被并发写乱。这说明作者不仅考虑了执行性能，也考虑了状态一致性。\n六、权限、任务与 Agent：Claude Code 的治理中台 很多 Agent 产品的失败点不在模型效果，而在副作用治理。Claude Code 显然把这件事当成了独立子系统。\nsrc/hooks/useCanUseTool.tsx 是权限入口。它不是单纯“给用户弹个确认框”，而是把多种决策源汇总到一起：\n静态配置规则。 自动分类器决策。 协调器 / swarm worker 的上层决策。 交互式确认对话框。 拒绝后的通知、埋点与原因记录。 这说明 Claude Code 的权限设计不是 UI 行为，而是运行时行为。只有这样，权限系统才能在终端、远程、队列任务、群体代理之间复用。\n与权限并列的，是任务系统。src/Task.ts 定义任务抽象，src/tasks.ts 则像工具注册表一样，维护所有任务类型：\nexport function getAllTasks(): Task[] { const tasks: Task[] = [ LocalShellTask, LocalAgentTask, RemoteAgentTask, DreamTask, ] if (LocalWorkflowTask) tasks.push(LocalWorkflowTask) if (MonitorMcpTask) tasks.push(MonitorMcpTask) return tasks } 这套设计有一个很漂亮的地方：任务系统是 Agent Runtime 的异步基座。 也就是说，后台 shell、后台 agent、远程 agent、监控任务并不是不同功能，它们只是在同一任务框架里的不同 task type。\n这直接为 tools/AgentTool/AgentTool.tsx 铺平了路。AgentTool 不是简单“再开一个 Claude 实例”，而是把下面这些能力一次性绑起来了：\nAgent 类型选择与模型覆盖。 工作目录覆盖与 Git worktree 隔离。 后台运行与前台接管。 远程 agent 与本地 agent 的分流。 多 Agent / teammate / swarm 模式。 Agent 元数据写入与进度跟踪。 换句话说，Agent 在 Claude Code 里不是 prompt trick，而是有完整生命周期管理的任务实体。这是它跟大量“多开几个子对话”产品的本质差异。\nAgent 定义本身则由 tools/AgentTool/loadAgentsDir.ts 加载。这个设计也很关键：Agent 不是硬编码在某个 switch 里，而是可以被 Markdown / frontmatter / JSON 声明。声明内容还包括工具白名单、禁用工具、需要的 MCP、记忆范围、执行强度、隔离模式、权限模式等。也就是说，Claude Code 里的 Agent 更接近“声明式运行策略”。\n七、MCP、插件、技能与命令：扩展很多，但抽象被收得很稳 如果只看目录数量，你可能会觉得扩展系统会乱成一团。实际情况恰恰相反：Claude Code 的扩展点很多，但抽象边界 surprisingly 稳定。\n图 4：扩展生态图。不同扩展面最后都要接入统一运行时，而不是各玩各的。 1. 命令系统 src/commands.ts 是 slash command 的全局注册表。它的特点是：\n静态命令与 feature-gated 命令混合存在。 命令既可以是纯本地命令，也可以是 prompt 命令。 技能、插件命令、MCP prompt command 也能被并入统一命令表。 这意味着“命令”在 Claude Code 里其实是一个统一的控制入口，而不是 UI 语法糖。\n2. 技能系统 src/skills/loadSkillsDir.ts 把技能看成一类结构化提示资产。它支持从用户目录、项目目录、策略目录、插件目录、MCP 目录加载 Markdown 技能文件，并解析 frontmatter 中的工具限制、hook、参数、模型、effort、执行环境等。这个设计的价值在于：技能不是死文本，而是可带运行时约束的 prompt-programmed unit。\n3. 插件系统 插件分两部分：发现与安装。\nplugins/builtinPlugins.ts 描述系统自带插件，可以注入技能、hook、MCP server。 services/plugins/PluginInstallationManager.ts 则负责后台 reconcile marketplace，并把安装状态写回 AppState。 这个后台安装管理器尤其说明架构成熟：插件安装不是阻塞启动的同步步骤，而是在 REPL 已经起来以后后台进行；安装完成后再刷新插件缓存或提示用户 reload。这里明显体现出“启动时延优先”的设计哲学。\n4. MCP 系统 MCP 是整套扩展体系里最重的一块，核心在 services/mcp/client.ts。这个文件本质上做的是协议适配层：\n支持 stdio、SSE、streamable HTTP、WebSocket 等传输。 支持认证、OAuth 刷新、会话过期恢复。 把 MCP 的 tool / prompt / resource 统一转换成 Claude Code 自己的工具和资源抽象。 处理结果裁剪、二进制持久化、鉴权错误与输出持久化。 要点不在“支持很多协议”，而在于 MCP 最终并没有旁路掉 Claude Code 的运行时，而是被重新映射回同一套 Tool/Resource 接口。这保证了权限、消息流、UI、转录、埋点、预算控制都还能继续工作。\n八、远程、Bridge 与直连：其实是在复用同一套运行时 这个仓库最容易被低估的部分，是远程能力。很多人会把远程支持当成“多写了一套 server/client”，但从这份源码来看，作者显然在做更大的一件事：把本地 Agent Runtime 平滑搬到远端环境里运行。\nbridge/bridgeMain.ts 是核心，它负责：\n环境注册。 会话孵化与多会话调度。 工作轮询、心跳、重连和超时回收。 需要时创建和清理 worktree。 在 token 过期、网络失败、系统休眠等情况下做恢复。 从实现细节看，这不是一次性任务，而是一个长驻型 control loop。也就是说，Bridge 更像一个“远程环境运行器”，而不是简单的“把 stdout 发到网页”。\nremote/RemoteSessionManager.ts 则处理 viewer 侧的会话同步：通过 WebSocket 收事件、通过 HTTP 发请求、桥接权限确认、管理重连状态。它说明“远程查看器”看到的并不是一个 fake transcript，而是真实运行时的事件流镜像。\n至于 server/createDirectConnectSession.ts，它提供了另一条入口：直接创建可连接的 session。于是整个远程层其实有三种模式：\nBridge 模式：把本机当成可调度环境。 Remote viewer 模式：连接到远端会话看事件流。 Direct connect 模式：直接创建并接入会话。 这三者看似不同，但底层都回到同一套 Query Runtime。这也是我一直强调的重点：Claude Code 的核心不是某种前端形态，而是底层运行时的一致性。\n九、长期记忆与上下文压缩：所谓“大上下文”本质上是治理策略 Agent 产品一旦进入长会话，就一定会遇到两个问题：\n哪些信息要跨会话保留？ 当前会话太长时，什么信息该保、什么信息该丢？ Claude Code 对这两件事都给了工程化回答。\n1. Memory 目录不是“隐式记忆”，而是显式文件系统 src/memdir/memdir.ts 定义了持久记忆系统。它的设计非常值得学习：\n记忆是文件，而不是隐式向量库。 MEMORY.md 是索引，不是内容仓库。 有明确的类型体系：用户、反馈、项目、参考。 有严格的入口文件行数和字节上限，防止索引膨胀。 这种设计非常“工程师友好”，因为记忆变成了可审计、可版本化、可手工修正的资产，而不是产品黑箱。\n2. 压缩不是“简单摘要”，而是带恢复语义的上下文管理 services/compact/compact.ts 的实现揭示了一个现实：所谓“超长上下文”并不是靠模型 magically 无限记住，而是靠系统在关键节点主动做上下文治理。这个文件至少做了这些事：\n压缩前剥离图片和文档，减少无效 token 消耗。 剥离那些下一轮本来就会重新注入的附件，避免摘要污染。 为重要文件、技能、工具发现结果保留预算。 压缩后重新注入关键 attachment 和必要文件摘要。 所以 Claude Code 真正的设计思想不是“我有无限上下文”，而是“我知道什么该进入长期记忆，什么该进入当前回合上下文，什么该在超限后被浓缩成可恢复摘要”。这是一个非常重要的认知差别。\n十、那些不在主干里、却暴露成熟度的外围模块 如果只盯着 query.ts、tools.ts、AgentTool 这些主干文件，很容易漏掉一件事：真正能把产品做成熟的，往往是那些看起来“不那么核心”的外围系统。Claude Code 在这部分也很完整。\n1. Hook 系统：把产品行为开放成生命周期，而不是硬编码分支 src/schemas/hooks.ts 和 SDK 的 HOOK_EVENTS 暴露出一个很完整的 Hook 面。它不是只有简单的前置后置两个事件，而是覆盖了工具、权限、压缩、任务、工作树、配置变更等多个生命周期点。\nexport const HOOK_EVENTS = [ 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart', 'PermissionRequest', 'SubagentStart', 'PreCompact', 'PostCompact', 'FileChanged', ] 更重要的是，Hook 本身也不是一种实现方式，而是四种：command、prompt、http、agent。这意味着平台作者没有把“扩展性”限制成 shell 脚本，而是明确允许用户在生命周期里注入本地命令、额外模型判断、HTTP 回调，甚至独立 Agent 验证器。对于一个 Agent 平台来说，这种设计比“不断往核心里塞新功能”更可持续。\n2. 快捷键系统：终端 UI 不是玩具，而是一套上下文化输入系统 src/keybindings/schema.ts 和 src/keybindings/defaultBindings.ts 显示，这套终端 UI 的输入系统已经非常产品化了。它支持：\n多上下文绑定，比如 Global、Chat、Confirmation、Transcript、Footer、Plugin。 动作绑定和命令绑定并存，既能触发 UI action，也能直接映射 slash command。 平台差异适配，比如 Windows、Bun、不同终端是否支持 VT 模式。 默认绑定叠加用户覆盖，而不是完全替换。 这背后的信号非常明确：Claude Code 的 REPL 不是“把网页聊天搬进终端”，而是一套认真经营的终端交互产品。\n3. 定时任务与 Loop：Agent 不只服务当前会话，还能进入调度系统 src/tools/ScheduleCronTool/CronCreateTool.ts 和 src/hooks/useScheduledTasks.ts 展现的是另一个重要方向：Agent 从“响应一次用户提问”升级成“可以被调度的工作单元”。\n这里最值得注意的实现点有四个：\nCron 创建工具本身是一个正式 Tool，有输入 schema、验证、结果映射和持久化路径。 定时任务分成 session-only 和 durable 两类，后者会写入磁盘。 调度器不是写死在命令里，而是通过 useScheduledTasks 作为独立运行时挂件常驻。 如果定时任务是发给 teammate 的，系统还会把 prompt 精确路由给对应 Agent；teammate 不存在时，自动清理孤儿 cron。 这说明 Claude Code 并没有把 Agent 看成一个“临时聊天对象”，而是把它纳入了更长生命周期的任务调度模型里。\n4. Buddy：彩蛋模块也暴露了产品工程的认真程度 src/buddy/ 表面上像一个玩笑功能，但它暴露了很有意思的工程习惯。比如 src/buddy/prompt.ts 明确规定 companion 和主 assistant 的职责边界，避免模型误把自己当成宠物角色；而 src/buddy/types.ts 则把物种、稀有度、帽子、属性都做成了确定义的类型系统。\n更有意思的是，物种名不是直接硬编码字符串，而是通过 String.fromCharCode 构造。这种实现细节说明，哪怕是彩蛋功能，也要和主构建链路、关键字扫描、配置持久化这些工程约束协同工作。成熟产品往往就体现在这种地方。\n十一、对 Agent 开发最有启发的五个设计取舍 入口先分流，组合根后加载。 这让冷启动优化有了结构性抓手。 工具协议足够厚。 Tool 不是函数，是运行时实体，这样权限、预算、并发、UI 才接得住。 任务是异步基座。 后台 Agent、远程 Agent、Shell、监控任务都被收口到 Task 抽象上。 扩展点多，但都回到统一抽象。 命令、技能、插件、MCP、远程能力最终都接入同一个 Query Runtime。 上下文治理显式化。 Memory、compact、attachment reinjection、token budget 都不是补丁，而是核心机制。 如果把这些取舍再压缩成一句话，那就是：Claude Code 把“能力增长”这件事交给统一抽象，把“风险增长”这件事交给治理层，而不是让每个新功能都各自发明一套机制。\n十二、顶层模块索引：把“所有模块”一次看全 下面这张表不是逐文件展开 1902 个文件，而是按 src/ 顶层目录和单文件入口做完整索引。对于如此大的仓库，这是既能覆盖全貌又不至于失真的合理粒度。\n模块 类型 职责 main.tsx组合根装配配置、认证、工具、命令、插件、技能、策略、远程与 UI。 interactiveHelpers.tsx交互边界负责 trust/setup screens、审批对话、renderAndRun 之类的会话启动控制。 replLauncher.tsx懒加载入口真正挂载 App 与 REPL 的轻薄入口。 context.ts上下文入口构造并缓存 systemContext 与 userContext。 query.ts查询状态机驱动完整 assistant 回合、流式采样、工具、预算与恢复。 QueryEngine.tsSDK 引擎对外暴露可复用的 headless conversation engine。 tools.ts注册表统一声明所有基础工具与启用逻辑。 Tool.ts核心协议定义 Tool 的 schema、权限、并发、安全和执行契约。 tasks.ts注册表统一声明所有任务类型。 Task.ts核心协议定义任务类型、状态与标识模型。 setup.ts启动辅助配合初始化流程做环境搭建。 ink.tsUI 出口统一导出 Ink 运行时能力。 cost-tracker.ts成本追踪统计会话成本与模型消耗。 costHook.ts成本 hook把成本追踪接到查询生命周期中。 history.ts会话历史管理历史记录、恢复与展示相关逻辑。 dialogLaunchers.tsx对话入口集中启动若干交互对话框。 projectOnboardingState.ts项目入门状态管理项目 onboarding 相关的状态持久化。 assistant/子系统Assistant 模式的会话历史等辅助能力。 bootstrap/子系统跨启动阶段共享的 bootstrap state。 bridge/子系统远程控制、桥接环境、工作轮询、会话孵化、重连与日志。 buddy/子系统伙伴精灵、宠物交互、陪伴式提示等 UI 能力。 cli/子系统CLI handlers、传输层、结构化输出、远程 IO。 commands/子系统所有 slash command 的实现。 components/子系统终端 UI 组件库。 constants/子系统系统提示词、产品常量、限制、键位、输出风格常量。 context/子系统React Context：通知、模态框、队列消息、FPS、邮箱等。 coordinator/子系统协调器模式的开关与模式识别。 entrypoints/子系统CLI、初始化等不同进程入口。 hooks/子系统输入、权限、设置、IDE、任务、通知等各类 hook。 ink/子系统终端渲染、事件分发、ansi/osc/csi 处理。 keybindings/子系统快捷键 schema、解析、匹配、用户绑定加载。 memdir/子系统长期记忆目录、索引、扫描、提示词与年龄策略。 migrations/子系统配置与模型版本迁移脚本。 moreright/子系统额外的 UI 或交互能力扩展。 native-ts/子系统原生能力桥接，如 color diff、file index、yoga layout。 outputStyles/子系统输出风格定义与加载。 plugins/子系统内建插件注册与 bunded plugin 入口。 query/子系统查询配置、依赖注入、停止钩子、token budget。 remote/子系统远程会话管理、权限桥接、WS 适配。 schemas/子系统hooks 等结构化 schema 定义。 screens/子系统REPL、Doctor、Resume Conversation 等页面级终端视图。 server/子系统直连会话管理与服务端会话入口。 services/子系统API、分析埋点、MCP、压缩、插件安装、语音、提示建议等服务层。 skills/子系统技能加载、内建技能与 MCP 技能桥接。 state/子系统AppState store、selector、状态变更监听。 tasks/子系统本地 shell、Agent、远程 Agent、Dream 等任务实现。 tools/子系统Bash、文件、技能、MCP、计划、任务、浏览器等工具实现。 types/子系统命令、权限、插件、消息、事件等类型定义。 upstreamproxy/子系统上游代理与 relay 能力。 utils/子系统这套运行时的底座工具箱，包括 Git、会话、路径、hooks、model、权限、token、日志等。 vim/子系统Vim motions、operators、text objects。 voice/子系统语音模式开关与能力判定。 十三、最后的判断：这份源码真正难的不是“LLM 接口”，而是运行时工程 如果只盯着 services/api/claude.ts 看，很容易得出一个错误结论：Claude Code 的难点在于 API 包装。实际上，API 适配只是它的一部分。真正难的是如何把下面这些东西放进一个统一系统里，还不至于崩：\n多模型、多 provider、多传输协议。 流式工具执行与权限治理。 任务、子 Agent、远程环境、Bridge。 技能、插件、MCP、命令、记忆。 长会话压缩、上下文恢复、成本与预算控制。 Claude Code 这份源码最值得学习的地方，不是某个聪明 prompt，也不是某个花哨 UI，而是它把 Agent 产品里最容易变成一团乱麻的部分，全都收束成了可维护的运行时边界。真正稀缺的不是“能不能接模型”，而是“当工具、权限、记忆、任务、远程、插件都长出来以后，系统还能不能继续演化”。这才是它真正的工程价值。\n","permalink":"https://pp-tech-blog.pages.dev/posts/claude-code-source-analysis/","summary":"基于源码快照，从启动链路、查询循环、工具系统、权限治理、任务/Agent、MCP 与远程运行时完整拆解 Claude Code。","title":"Claude Code 源码深度解析：从 CLI 启动、查询循环到工具与远程运行时"},{"content":"1、unique_ptr template\u0026lt;typename T\u0026gt; class MyUniquePtr { public: explicit MyUniquePtr(T* ptr = nullptr) :mPtr(ptr) {} ~MyUniquePtr() { if(mPtr) delete mPtr; } MyUniquePtr(MyUniquePtr \u0026amp;amp;\u0026amp;amp;p) noexcept; MyUniquePtr\u0026amp;amp; operator=(MyUniquePtr \u0026amp;amp;\u0026amp;amp;p) noexcept; MyUniquePtr(const MyUniquePtr \u0026amp;amp;p) = delete; MyUniquePtr\u0026amp;amp; operator=(const MyUniquePtr \u0026amp;amp;p) = delete; T* operator*() const noexcept {return mPtr;} T\u0026amp;amp; operator-\u0026amp;gt;()const noexcept {return *mPtr;} explicit operator bool() const noexcept{return mPtr;} void reset(T* q = nullptr) noexcept { if(q != mPtr){ if(mPtr) delete mPtr; mPtr = q; } } T* release() noexcept { T* res = mPtr; mPtr = nullptr; return res; } T* get() const noexcept {return mPtr;} void swap(MyUniquePtr \u0026amp;amp;p) noexcept { using std::swap; swap(mPtr, p.mPtr); } private: T* mPtr; };\ntemplate\u0026lt;typename T\u0026gt; MyUniquePtr\u0026lt;T\u0026gt;\u0026amp; MyUniquePtr\u0026lt;T\u0026gt;::operator=(MyUniquePtr \u0026amp;\u0026amp;p) noexcept { swap(*this, p); return *this; }\ntemplate\u0026lt;typename T\u0026gt; MyUniquePtr\u0026lt;T\u0026gt; :: MyUniquePtr(MyUniquePtr \u0026amp;\u0026amp;p) noexcept : mPtr(p.mPtr) { p.mPtr == NULL; }\nkey 1、noexcept关键字 该关键字能够告知标准库，本函数不会产生异常，如果未标记noexcept，标准库将产生一个额外的操作，增加了开销。移动赋值操作符以及部分接口函数的声明和定义中需要标记noexcept.\n2、缺失bool值转换功能 观察IO类、unique_ptr、shared_ptr等资源管理相关的类，它们一般都会重新实现：\n\u0026lt;code\u0026gt;explicit operation bool() const noexcept\u0026lt;/code\u0026gt;\n以便能直接将对象放置于条件语句中进行判断。\nint main() { std::unique_ptr\u0026lt;int\u0026gt; ptr(new int(42)); if (ptr) std::cout \u0026amp;lt;\u0026amp;lt; \u0026quot;before reset, ptr is: \u0026quot; \u0026amp;lt;\u0026amp;lt; *ptr \u0026amp;lt;\u0026amp;lt; '\\n'; ptr.reset(); if (ptr) std::cout\u0026amp;lt;\u0026amp;lt; \u0026quot;after reset, ptr is: \u0026quot; \u0026amp;lt;\u0026amp;lt; *ptr \u0026amp;lt;\u0026amp;lt; '\\n'; }\nexplicit关键字不能少，否则可能出现隐式转换，导致如下问题：\nstd::unique_ptr\u0026lt;int\u0026gt; p1(new int(13)); std::unique_ptr\u0026lt;int\u0026gt; p2(new int(14)); if(p1 == p2) { //p1 p2 都会被转换为bool值，都为true,因此结果是两者相等。 std::cout \u0026lt;\u0026lt; \u0026ldquo;p1 is equal p2\u0026rdquo; \u0026lt;\u0026lt; endl; } //输出： p1 is equal p2\n这时p1==p2将返回true 编译器为了使两个对象能够互相比较，就会把他们都转换为bool型，从而导致错误。\n3、三/五法则和阻止拷贝 释放动态内存----\u0026gt;需要自定义析构函数，而根据三/五法则：\n如果一个类需要自定义析构函数，几乎可以肯定它也需要自定义拷贝构造函数和拷贝赋值运算符。 总之，不能让编译器产生合成版本的拷贝构造函数和拷贝赋值运算符。 而unique_ptr要求独占资源，不允许拷贝，因此，上述代码中将拷贝赋值操作删除(通过delete关键字)，就不会产生合成版本的了。\n4、隐式的类类型转换 如果构造函数只接受一个实参，则它实际上定义了转换此类类型的隐式转换机制，有时我们把这种构造函数称为转换构造函数(converting constructor)\n是否允许隐式的类类型转换，是由类的语义和上下文环境决定的。 同样的，使用explicit可以抑制这种转换。也由此，unique_ptr只能进行直接初始化。\n5、移动构造、移动赋值及自赋值问题 unique_ptr不可拷贝，但是可以移动。这是它的重要特征，因此，绝对不能忘记自定义移动构造函数和移动赋值运算符。 而重新实现移动赋值运算符时，要记得考虑自赋值问题。上述代码考虑了该问题。\n优化：使用swap函数来实现operator=(移动赋值运算符)，巧妙解决自赋值问题。且相比判断*this == p而言，提高了效率。\ntemplate\u0026lt;typename T\u0026gt; MyUniquePtr\u0026lt;T\u0026gt;\u0026amp; MyUniquePtr\u0026lt;T\u0026gt;::operator=(MyUniquePtr \u0026amp;\u0026amp;p) noexcept { //交换本对象和右值p的内容 this-\u0026gt;swap(*this, p);//p.mPtr现在指向本对象曾经指向的内存 return *this; //返回后，右值p将被销毁，从而指向delete p.mPtr } 为什么*this == p版本不好： 1.自赋值检查: 自赋值情况的发生频率并不高，而无论何种情况都做自赋值检查，更加耗时。 为什么swap版本更好： 无需再做自赋值检查。由于自赋值检查发生频率很低，因此，所有在自赋值情况下复制相等的指针，其所浪费的时间并不多。 代码复用。复用了swap函数，精简了代码. 使用交换技术，能够延后针对unique_ptr内置指针成员的delete操作。这意味着临时的unique_ptr超出作用域之前，是潜在的可再次使用的。当其超出作用域后，unique_ptr的析构函数会将其正确销毁。 2、shared_ptr todo\n","permalink":"https://pp-tech-blog.pages.dev/posts/int_ptr/","summary":"1、unique_ptr template\u003ctypename T\u003e class MyUniquePtr { public: explicit MyUniquePtr(T*。","title":"智能指针的实现"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n编写类tips 1、构造函数尽量用列表初始化为成员变量赋值 初始化比普通赋值更快\n2、数据放入private中,大部分函数放入public中.\n3、在类的body里的函数，若函数体内不改变数据成员时，应该加const，如果不加，后续可能会出现问题 声明一个成员函数的时候用const关键字是用来说明这个函数是 “只读(read-only)”函数，也就是说明这个函数不会修改任何数据成员(object)。 为了声明一个const成员函数， 把const关键字放在函数括号的后面。声明和定义的时候都应该放const关键字。 任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时，不慎修改了数据成员，或者调用了其它非const成员函数，编译器将指出错误，这无疑会提高程序的健壮性。\n值传递、引用传递 1、参数尽可能用reference来传，要不要加const看状况、 传引底层是通过传指针的方式，可以提高代码效率，减少内存复制。如果函数不对参数进行改动，则加const，同时可以避免不小心对参数修改的情况 2、返回值也尽量用reference来传，在可以的情况下（首先考虑reference） 当返回一个局部变量的引用，函数结束时局部变量被销毁，无法正确返回，这种情况不可以返回引用 3、操作符重载时，返回值使用reference来传可以实现链式传输（==、+=、\u0026lt;\u0026lt;等） ","permalink":"https://pp-tech-blog.pages.dev/posts/choujie/","summary":"编写类tips 1、构造函数尽量用列表初始化为成员变量赋值 初始化比普通赋值更快 2、数据放入private中,大部分函数放入public中. 3、在类的body里的函数。","title":"c++面向对象"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\nHTTP报文是面向文本的，报文中的每一个字段都是一些ASCII码串，各个字段的长度是不确定的。HTTP有两类报文：请求报文和响应报文。\nHTTP请求报文 一个HTTP请求报文由请求行（request line）、请求头部（header）、空行和请求数据4个部分组成，下图给出了请求报文的一般格式。\n1.请求头 请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成，它们用空格分隔。例如，GET /index.html HTTP/1.1。\nHTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。\n而常见的有如下几种：\n1).GET 最常见的一种请求方式，当客户端要从服务器中读取文档时，当点击网页上的链接或者通过在浏览器的地址栏输入网址来浏览网页的，使用的都是GET方式。GET方法要求服务器将URL定位的资源放在响应报文的数据部分，回送给客户端。使用GET方法时，请求参数和对应的值附加在URL后面，利用一个问号（“?”）代表URL的结尾与请求参数的开始，传递参数长度受限制。例如，/index.jsp?id=100\u0026amp;op=bind,这样通过GET方式传递的数据直接表示在地址中，所以我们可以把请求结果以链接的形式发送给好友。以用google搜索domety为例，Request格式如下：\nGET /search?hl=zh-CN\u0026amp;source=hp\u0026amp;q=domety\u0026amp;aq=f\u0026amp;oq= HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash, */* Referer: \u0026lt;a href=\"http://www.google.cn/\"\u0026gt;http://www.google.cn/\u0026lt;/a\u0026gt; Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld) Host: \u0026lt;a href=\"http://www.google.cn\"\u0026gt;www.google.cn\u0026lt;/a\u0026gt; Connection: Keep-Alive Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y-FxlRugatx63JLv7CWMD6UB_O_r 可以看到，GET方式的请求一般不包含”请求内容”部分，请求数据以地址的形式表现在请求行。地址链接如下：\n\u0026lt;a href=\"http://www.google.cn/search?hl=zh-CN\u0026amp;source=hp\u0026amp;q=domety\u0026amp;aq=f\u0026amp;oq=\"\u0026gt;http://www.google.cn/search?hl=zh-CN\u0026amp;source=hp \u0026amp;q=domety\u0026amp;aq=f\u0026amp;oq=\u0026lt;/a\u0026gt; 地址中”?”之后的部分就是通过GET发送的请求数据，我们可以在地址栏中清楚的看到，各个数据之间用”\u0026amp;”符号隔开。显然，这种方式不适合传送私密数据。另外，由于不同的浏览器对地址的字符限制也有所不同，一般最多只能识别1024个字符，所以如果需要传送大量数据的时候，也不适合使用GET方式。\n2).POST 对于上面提到的不适合使用GET方式的情况，可以考虑使用POST方式，因为使用POST方法可以允许客户端给服务器提供信息较多。POST方法将请求参数封装在HTTP请求数据中，以名称/值的形式出现，可以传输大量数据，这样POST方式对传送的数据大小没有限制，而且也不会显示在URL中。还以上面的搜索domety为例，如果使用POST方式的话，格式如下：\nPOST /search HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash, */* Referer: \u0026lt;a href=\"http://www.google.cn/\"\u0026gt;http://www.google.cn/\u0026lt;/a\u0026gt; Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld) Host: \u0026lt;a href=\"http://www.google.cn\"\u0026gt;www.google.cn\u0026lt;/a\u0026gt; Connection: Keep-Alive Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y-FxlRugatx63JLv7CWMD6UB_O_r hl=zh-CN\u0026amp;source=hp\u0026amp;q=domety\n可以看到，POST方式请求行中不包含数据字符串，这些数据保存在”请求内容”部分，各数据之间也是使用”\u0026amp;”符号隔开。POST方式大多用于页面的表单中。因为POST也能完成GET的功能，因此多数人在设计表单的时候一律都使用POST方式，其实这是一个误区。GET方式也有自己的特点和优势，我们应该根据不同的情况来选择是使用GET还是使用POST。\n3).HEAD HEAD就像GET，只不过服务端接受到HEAD请求后只返回响应头，而不会发送响应内容。当我们只需要查看某个页面的状态的时候，使用HEAD是非常高效的，因为在传输的过程中省去了页面内容。\n2.请求头部 请求头部由关键字/值对组成，每行一对，关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息，典型的请求头有：\nUser-Agent：产生请求的浏览器类型。\nAccept：客户端可识别的内容类型列表。\nHost：请求的主机名，允许多个域名同处一个IP地址，即虚拟主机。\n3.空行 最后一个请求头之后是一个空行，发送回车符和换行符，通知服务器以下不再有请求头。\n4.请求数据 请求数据不在GET方法中使用，而是在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求数据相关的最常使用的请求头是Content-Type和Content-Length。\nHTTP响应报文 HTTP响应也由三个部分组成，分别是：状态行、消息报头、响应正文。\n如下所示，HTTP响应的格式与请求的格式十分类似：\n＜status-line＞\n＜headers＞\n＜blank line＞\n[＜response-body＞]\n如上所示，在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行（status line）通过提供一个状态码来说明所请求的资源情况。\n状态行格式如下：\nHTTP-Version Status-Code Reason-Phrase CRLF\n其中，HTTP-Version表示服务器HTTP协议的版本；Status-Code表示服务器发回的响应状态代码；Reason-Phrase表示状态代码的文本描述。状态代码由三位数字组成，第一个数字定义了响应的类别，且有五种可能取值。\n1xx：指示信息--表示请求已接收，继续处理。 2xx：成功--表示请求已被成功接收、理解、接受。 3xx：重定向--要完成请求必须进行更进一步的操作。 4xx：客户端错误--请求有语法错误或请求无法实现。 5xx：服务器端错误--服务器未能实现合法的请求。\n下面给出一个HTTP响应报文例子\nHTTP/1.1 200 OK Date: Sat, 31 Dec 2005 23:59:59 GMT Content-Type: text/html;charset=ISO-8859-1 Content-Length: 122 ＜html＞ ＜head＞ ＜title＞Wrox Homepage＜/title＞ ＜/head＞ ＜body＞ ＜!\u0026ndash; body goes here \u0026ndash;＞ ＜/body＞ ＜/html＞\n关于HTTP请求GET和POST的区别 1.GET提交，请求的数据会附在URL之后（就是把数据放置在HTTP协议头＜request-line＞中），以?分割URL和传输数据，多个参数用\u0026amp;连接;例如：login.action?name=hyddd\u0026amp;password=idontknow\u0026amp;verify=%E4%BD%A0 %E5%A5%BD。如果数据是英文字母/数字，原样发送，如果是空格，转换为+，如果是中文/其他字符，则直接把字符串用BASE64加密，得出如： %E4%BD%A0%E5%A5%BD，其中％XX中的XX为该符号以16进制表示的ASCII。\nPOST提交：把提交的数据放置在是HTTP包的包体＜request-body＞中。上文示例中红色字体标明的就是实际的传输数据\n因此，GET提交的数据会在地址栏中显示出来，而POST提交，地址栏不会改变\n2.传输数据的大小：\n首先声明,HTTP协议没有对传输的数据大小进行限制，HTTP协议规范也没有对URL长度进行限制。 而在实际开发中存在的限制主要有：\nGET:特定浏览器和服务器对URL长度有限制，例如IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器，如Netscape、FireFox等，理论上没有长度限制，其限制取决于操作系统的支持。\n因此对于GET提交时，传输数据就会受到URL长度的限制。\nPOST:由于不是通过URL传值，理论上数据不受限。但实际各个WEB服务器会规定对post提交数据大小进行限制，Apache、IIS6都有各自的配置。\n3.安全性： POST的安全性要比GET的安全性高。注意：这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改，而这里安全的含义是真正的Security的含义，比如：通过GET提交数据，用户名和密码将明文出现在URL上，因为(1)登录页面有可能被浏览器缓存， (2)其他人查看浏览器的历史纪录，那么别人就可以拿到你的账号和密码了，\n","permalink":"https://pp-tech-blog.pages.dev/posts/httpmessage/","summary":"HTTP报文是面向文本的，报文中的每一个字段都是一些ASCII码串，各个字段的长度是不确定的。HTTP有两类报文：请求报文和响应报文。 HTTP请求报文 一个HTTP请求报。","title":"HTTP请求报文和HTTP响应报文"},{"content":"1、状态分类 分类 分类描述 1** 信息响应，服务器收到请求，需要请求者继续执行操作 2** 成功响应，操作被成功接收并处理 3** 重定向，需要进一步的操作以完成请求 4** 客户端错误，请求包含语法错误或无法完成请求 5** 服务器错误，服务器在处理请求的过程中发生了错误 HTTP 响应状态代码指示特定 HTTP 请求是否已成功完成。响应分为五类：信息响应(100–199)，成功响应(200–299)，重定向(300–399)，客户端错误(400–499)和服务器错误 (500–599)。状态代码由 section 10 of RFC 2616定义\n摘自： https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status\n2常用状态码 1信息响应 100 Continue 这个临时响应表明，迄今为止的所有内容都是可行的，客户端应该继续请求，如果已经完成，则忽略它。\n102 Processing (WebDAV) 此代码表示服务器已收到并正在处理该请求，但没有响应可用。\n2成功响应 200 OK 请求成功。成功的含义取决于HTTP方法：\nGET：资源已被提取并在消息正文中传输。 HEAD：实体标头位于消息正文中。 POST：描述动作结果的资源在消息体中传输。 TRACE：消息正文包含服务器收到的请求消息\n201 Created 该请求已成功，并因此创建了一个新的资源。这通常是在POST请求，或是某些PUT请求之后返回的响应。\n202 Accepted 请求已经接收到，但还未响应，没有结果。意味着不会有一个异步的响应去表明当前请求的结果，预期另外的进程和服务去处理请求，或者批处理。\n3重定向 300 Multiple Choice 被请求的资源有一系列可供选择的回馈信息，每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。\n301 Moved Permanently 被请求的资源已永久移动到新位置，并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能，拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定，否则这个响应也是可缓存的。\n302 Found 请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的，客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下，这个响应才是可缓存的。\n4客户端响应 400 Bad Request 1、语义有误，当前请求无法被服务器理解。除非进行修改，否则客户端不应该重复提交这个请求。 2、请求参数有误。\n401 Unauthorized 当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书，那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问，且浏览器已经至少尝试了一次验证，那么浏览器应当向用户展示响应中包含的实体信息，因为这个实体信息中可能包含了相关诊断信息。\n403 Forbidden 服务器已经理解请求，但是拒绝执行它。与 401 响应不同的是，身份验证并不能提供任何帮助，而且这个请求也不应该被重复提交。如果这不是一个 HEAD 请求，而且服务器希望能够讲清楚为何请求不能被执行，那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个 404 响应，假如它不希望让客户端获得任何信息。\n404 Not Found 请求失败，请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话，应当使用410状态码来告知旧资源因为某些内部的配置机制问题，已经永久的不可用，而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。\n5服务端响应 500 Internal Server Error 服务器遇到了不知道如何处理的情况。\n501 Not Implemented 此请求方法不被服务器支持且无法被处理。只有GET和HEAD是要求服务器支持的，它们必定不会返回此错误代码。\n502 Bad Gateway 此错误响应表明服务器作为网关需要得到一个处理这个请求的响应，但是得到一个错误的响应。\n504 Gateway Timeout 当服务器作为网关，不能及时得到响应时返回此错误代码。\n505 HTTP Version Not Supported 服务器不支持请求中所使用的HTTP协议版本。\n","permalink":"https://pp-tech-blog.pages.dev/posts/httpstatecode/","summary":"1、状态分类 分类 分类描述 1** 信息响应，服务器收到请求，需要请求者继续执行操作 2** 成功响应，操作被成功接收并处理 3** 重定向，需要进一步的操作以完成请求。","title":"http状态码"},{"content":"def print_params_1(params): print params def print_params_2(*params): print params def print_params_3(**params): print params 上述3个函数参数，分别传递单参数、tuple、dictionary。\n","permalink":"https://pp-tech-blog.pages.dev/posts/pythonparam/","summary":"def print_params_1(params): print params def print_params_2(*params): print params def。","title":"python函数参数传递*/**"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n一 概念说明 在进行解释之前，首先要说明几个概念：\n用户空间和内核空间 进程切换 进程的阻塞 文件描述符 缓存 I/O 用户空间与内核空间 现在操作系统都是采用虚拟存储器，那么对32位操作系统而言，它的寻址空间（虚拟存储空间）为4G（2的32次方）。操作系统的核心是内核，独立于普通的应用程序，可以访问受保护的内存空间，也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核（kernel），保证内核的安全，操心系统将虚拟空间划分为两部分，一部分为内核空间，一部分为用户空间。针对linux操作系统而言，将最高的1G字节（从虚拟地址0xC0000000到0xFFFFFFFF），供内核使用，称为内核空间，而将较低的3G字节（从虚拟地址0x00000000到0xBFFFFFFF），供各个进程使用，称为用户空间。\n进程切换 为了控制进程的执行，内核必须有能力挂起正在CPU上运行的进程，并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说，任何进程都是在操作系统内核的支持下运行的，是与内核紧密相关的。\n从一个进程的运行转到另一个进程上运行，这个过程中经过下面这些变化：\n保存处理机上下文，包括程序计数器和其他寄存器。 更新PCB信息。 把进程的PCB移入相应的队列，如就绪、在某事件阻塞等队列。 选择另一个进程执行，并更新其PCB。 更新内存管理的数据结构。 恢复处理机上下文。 注：总而言之就是很耗资源，具体的可以参考这篇文章：进程切换\n进程的阻塞 正在执行的进程，由于期待的某些事件未发生，如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等，则由系统自动执行阻塞原语(Block)，使自己由运行状态变为阻塞状态。可见，进程的阻塞是进程自身的一种主动行为，也因此只有处于运行态的进程（获得CPU），才可能将其转为阻塞状态。当进程进入阻塞状态，是不占用CPU资源的。\n文件描述符fd 文件描述符（File descriptor）是计算机科学中的一个术语，是一个用于表述指向文件的引用的抽象化概念。\n文件描述符在形式上是一个非负整数。实际上，它是一个索引值，指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时，内核向进程返回一个文件描述符。在程序设计中，一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。\n缓存 I/O 缓存 I/O 又被称作标准 I/O，大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中，操作系统会将 I/O 的数据缓存在文件系统的页缓存（ page cache ）中，也就是说，数据会先被拷贝到操作系统内核的缓冲区中，然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。\n缓存 I/O 的缺点： 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作，这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。\n二 IO模式 刚才说了，对于一次IO访问（以read举例），数据会先被拷贝到操作系统内核的缓冲区中，然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说，当一个read操作发生时，它会经历两个阶段：\n等待数据准备 (Waiting for the data to be ready) 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 正式因为这两个阶段，linux系统产生了下面五种网络模式的方案。\n阻塞 I/O（blocking IO） 非阻塞 I/O（nonblocking IO） I/O 多路复用（ IO multiplexing） 信号驱动 I/O（ signal driven IO） 异步 I/O（asynchronous IO） 阻塞 I/O（blocking IO） 在linux中，默认情况下所有的socket都是blocking，一个典型的读操作流程大概是这样： 当用户进程调用了recvfrom这个系统调用，kernel就开始了IO的第一个阶段：准备数据（对于网络IO来说，很多时候数据在一开始还没有到达。比如，还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来）。这个过程需要等待，也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边，整个进程会被阻塞（当然，是进程自己选择的阻塞）。当kernel一直等到数据准备好了，它就会将数据从kernel中拷贝到用户内存，然后kernel返回结果，用户进程才解除block的状态，重新运行起来。\n所以，blocking IO的特点就是在IO执行的两个阶段都被block了。\n非阻塞 I/O（nonblocking IO） linux下，可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时，流程是这个样子： 当用户进程发出read操作时，如果kernel中的数据还没有准备好，那么它并不会block用户进程，而是立刻返回一个error。从用户进程角度讲 ，它发起一个read操作后，并不需要等待，而是马上就得到了一个结果。用户进程判断结果是一个error时，它就知道数据还没有准备好，于是它可以再次发送read操作。一旦kernel中的数据准备好了，并且又再次收到了用户进程的system call，那么它马上就将数据拷贝到了用户内存，然后返回。\n所以，nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。\nI/O 多路复用（ IO multiplexing） IO multiplexing就是我们说的select，poll，epoll，有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select，poll，epoll这个function会不断的轮询所负责的所有socket，当某个socket有数据到达了，就通知用户进程。\n当用户进程调用了select，那么整个进程会被block，而同时，kernel会“监视”所有select负责的socket，当任何一个socket中的数据准备好了，select就会返回。这个时候用户进程再调用read操作，将数据从kernel拷贝到用户进程。\n所以，I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符，而这些文件描述符（套接字描述符）其中的任意一个进入读就绪状态，select()函数就可以返回。\n这个图和blocking IO的图其实并没有太大的不同，事实上，还更差一些。因为这里需要使用两个system call (select 和 recvfrom)，而blocking IO只调用了一个system call (recvfrom)。但是，用select的优势在于它可以同时处理多个connection。\n所以，如果处理的连接数不是很高的话，使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好，可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快，而是在于能处理更多的连接。）\n在IO multiplexing Model中，实际中，对于每一个socket，一般都设置成为non-blocking，但是，如上图所示，整个用户的process其实是一直被block的。只不过process是被select这个函数block，而不是被socket IO给block。\n信号驱动 I/O（Signal-Driven I/O） 首先我们允许 socket 进行信号驱动 I/O,并安装一个信号处理函数，进程继续运行并不阻塞。当数据准备好时，进程会收到一个 SIGIO 信号，可以在信号处理函数中调用 I/O 操作函数处理数据。 异步 I/O（asynchronous IO） inux下的asynchronous IO其实用得很少。先看一下它的流程： 用户进程发起read操作之后，立刻就可以开始去做其它的事。而另一方面，从kernel的角度，当它受到一个asynchronous read之后，首先它会立刻返回，所以不会对用户进程产生任何block。然后，kernel会等待数据准备完成，然后将数据拷贝到用户内存，当这一切都完成之后，kernel会给用户进程发送一个signal，告诉它read操作完成了。\n总结 blocking和non-blocking的区别 调用blocking IO会一直block住对应的进程直到操作完成，而non-blocking IO在kernel还准备数据的情况下会立刻返回。\nsynchronous IO和asynchronous IO的区别 在说明synchronous IO和asynchronous IO的区别之前，需要先给出两者的定义。POSIX的定义是这样子的：\nA synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes; An asynchronous I/O operation does not cause the requesting process to be blocked; 两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义，之前所述的blocking IO，non-blocking IO，IO multiplexing都属于synchronous IO。\n有人会说，non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方，定义中所指的”IO operation”是指真实的IO操作，就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候，如果kernel的数据没有准备好，这时候不会block进程。但是，当kernel中数据准备好的时候，recvfrom会将数据从kernel拷贝到用户内存中，这个时候进程是被block了，在这段时间内，进程是被block的。\n而asynchronous IO则不一样，当进程发起IO 操作之后，就直接返回再也不理睬了，直到kernel发送一个信号，告诉进程说IO完成。在这整个过程中，进程完全没有被block。\n各个IO Model的比较如图所示： 通过上面的图片，可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中，虽然进程大部分时间都不会被block，但是它仍然要求进程去主动的check，并且当数据准备完成以后，也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人（kernel）完成，然后他人做完后发信号通知。在此期间，用户进程不需要去检查IO操作的状态，也不需要主动的去拷贝数据。\n三 I/O 多路复用之select、poll、epoll详解 select，poll，epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制，一个进程可以监视多个描述符，一旦某个描述符就绪（一般是读就绪或者写就绪），能够通知程序进行相应的读写操作。但select，poll，epoll本质上都是同步I/O，因为他们都需要在读写事件就绪后自己负责进行读写，也就是说这个读写过程是阻塞的，而异步I/O则无需自己负责进行读写，异步I/O的实现会负责把数据从内核拷贝到用户空间。（这里啰嗦下）\nselect int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); select 函数监视的文件描述符分3类，分别是writefds、readfds、和exceptfds。调用后select函数会阻塞，直到有描述副就绪（有数据 可读、可写、或者有except），或者超时（timeout指定等待时间，如果立即返回设为null即可），函数返回。当select函数返回后，可以 通过遍历fdset，来找到就绪的描述符。\nselect目前几乎在所有的平台上支持，其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制，在Linux上一般为1024，可以通过修改宏定义甚至重新编译内核的方式提升这一限制，但 是这样也会造成效率的降低。\npoll int poll (struct pollfd *fds, unsigned int nfds, int timeout); 不同与select使用三个位图来表示三个fdset的方式，poll使用一个 pollfd的指针实现。\nstruct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ }; pollfd结构包含了要监视的event和发生的event，不再使用select“参数-值”传递的方式。同时，pollfd并没有最大数量限制（但是数量过大后性能也是会下降）。 和select函数一样，poll返回后，需要轮询pollfd来获取就绪的描述符。\n从上面看，select和poll都需要在返回后，通过遍历文件描述符来获取已经就绪的socket。事实上，同时连接的大量客户端在一时刻可能只有很少的处于就绪状态，因此随着监视的描述符数量的增长，其效率也会线性下降。\nepoll epoll是在2.6内核中提出的，是之前的select和poll的增强版本。相对于select和poll来说，epoll更加灵活，没有描述符限制。epoll使用一个文件描述符管理多个描述符，将用户关系的文件描述符的事件存放到内核的一个事件表中，这样在用户空间和内核空间的copy只需一次。\n一 epoll操作过程 epoll操作过程需要三个接口，分别如下：\nint epoll_create(int size)；//创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)； int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); int epoll_create(int size); 创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大，这个参数不同于select()中的第一个参数，给出最大监听的fd+1的值，参数size并不是限制了epoll所能监听的描述符最大个数，只是对内核初始分配内部数据结构的一个建议。 当创建好epoll句柄后，它就会占用一个fd值，在linux下如果查看/proc/进程id/fd/，是能够看到这个fd的，所以在使用完epoll后，必须调用close()关闭，否则可能导致fd被耗尽。\nint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)； 函数是对指定描述符fd执行op操作。\nepfd：是epoll_create()的返回值。 op：表示op操作，用三个宏来表示：添加EPOLL_CTL_ADD，删除EPOLL_CTL_DEL，修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。 fd：是需要监听的fd（文件描述符） epoll_event：是告诉内核需要监听什么事，struct epoll_event结构如下： struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; //events可以是以下几个宏的集合： EPOLLIN ：表示对应的文件描述符可以读（包括对端SOCKET正常关闭）； EPOLLOUT：表示对应的文件描述符可以写； EPOLLPRI：表示对应的文件描述符有紧急的数据可读（这里应该表示有带外数据到来）； EPOLLERR：表示对应的文件描述符发生错误； EPOLLHUP：表示对应的文件描述符被挂断； EPOLLET： 将EPOLL设为边缘触发(Edge Triggered)模式，这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT：只监听一次事件，当监听完这次事件之后，如果还需要继续监听这个socket的话，需要再次把这个socket加入到EPOLL队列里\nint epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 等待epfd上的io事件，最多返回maxevents个事件。 参数events用来从内核得到事件的集合，maxevents告之内核这个events有多大，这个maxevents的值不能大于创建epoll_create()时的size，参数timeout是超时时间（毫秒，0会立即返回，-1将不确定，也有说法说是永久阻塞）。该函数返回需要处理的事件数目，如返回0表示已超时。 二 工作模式 epoll对文件描述符的操作有两种模式：LT（level trigger）和ET（edge trigger）。LT模式是默认模式，LT模式与ET模式的区别如下： LT模式：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，应用程序可以不立即处理该事件。下次调用epoll_wait时，会再次响应应用程序并通知此事件。 ET模式：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，应用程序必须立即处理该事件。如果不处理，下次调用epoll_wait时，不会再次响应应用程序并通知此事件。\n1. LT模式 LT(level triggered)是缺省的工作方式，并且同时支持block和no-block socket.在这种做法中，内核告诉你一个文件描述符是否就绪了，然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作，内核还是会继续通知你的。\n2. ET模式 ET(edge-triggered)是高速工作方式，只支持no-block socket。在这种模式下，当描述符从未就绪变为就绪时，内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪，并且不会再为那个文件描述符发送更多的就绪通知，直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如，你在发送，接收或者接收请求，或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误）。但是请注意，如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)，内核不会发送更多的通知(only once)\nET模式在很大程度上减少了epoll事件被重复触发的次数，因此效率要比LT模式高。epoll工作在ET模式的时候，必须使用非阻塞套接口，以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。\n3. 总结 假如有这样一个例子：\n我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符 这个时候从管道的另一端被写入了2KB的数据 调用epoll_wait(2)，并且它会返回RFD，说明它已经准备好读取操作 然后我们读取了1KB的数据 调用epoll_wait(2)...... LT模式： 如果是LT模式，那么在第5步调用epoll_wait(2)之后，仍然能受到通知。\nET模式： 如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志，那么在第5步调用epoll_wait(2)之后将有可能会挂起，因为剩余的数据还存在于文件的输入缓冲区内，而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候，调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。\n当使用epoll的ET模型来工作时，当产生了一个EPOLLIN事件后， 读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小，那么很有可能是缓冲区还有数据未读完，也意味着该次事件还没有处理完，所以还需要再次读取：\nwhile(rs){ buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0); if(buflen \u0026lt; 0){ // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读 // 在这里就当作是该次事件已处理处. if(errno == EAGAIN){ break; } else{ return; } } else if(buflen == 0){ // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf){ rs = 1; // 需要再次读取 } else{ rs = 0; } }\nLinux中的EAGAIN含义\nLinux环境下开发经常会碰到很多错误(设置errno)，其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。 从字面上来看，是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。\n例如，以 O_NONBLOCK的标志打开文件/socket/FIFO，如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回，read函数会返回一个错误EAGAIN，提示你的应用程序现在没有数据可读请稍后再试。 又例如，当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败，返回EAGAIN提示其再调用一次(也许下次就能成功)。\n三 代码演示 下面是一段不完整的代码且格式不对，意在表述上面的过程，去掉了一些模板代码。\n#define IPADDRESS \"127.0.0.1\" #define PORT 8787 #define MAXSIZE 1024 #define LISTENQ 5 #define FDSIZE 1000 #define EPOLLEVENTS 100 listenfd = socket_bind(IPADDRESS,PORT);\nstruct epoll_event events[EPOLLEVENTS];\n//创建一个描述符 epollfd = epoll_create(FDSIZE);\n//添加监听描述符事件 add_event(epollfd,listenfd,EPOLLIN);\n//循环等待 for ( ; ; ){ //该函数返回已经准备好的描述符事件数目 ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1); //处理接收到的连接 handle_events(epollfd,events,ret,listenfd,buf); }\n//事件处理函数 static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf) { int i; int fd; //进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。 for (i = 0;i \u0026lt; num;i++) { fd = events[i].data.fd; //根据描述符的类型和事件类型进行处理 if ((fd == listenfd) \u0026amp;\u0026amp;(events[i].events \u0026amp; EPOLLIN)) handle_accpet(epollfd,listenfd); else if (events[i].events \u0026amp; EPOLLIN) do_read(epollfd,fd,buf); else if (events[i].events \u0026amp; EPOLLOUT) do_write(epollfd,fd,buf); } }\n//添加事件 static void add_event(int epollfd,int fd,int state){ struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,\u0026amp;ev); }\n//处理接收到的连接 static void handle_accpet(int epollfd,int listenfd){ int clifd; struct sockaddr_in cliaddr; socklen_t cliaddrlen; clifd = accept(listenfd,(struct sockaddr*)\u0026amp;cliaddr,\u0026amp;cliaddrlen); if (clifd == -1) perror(\u0026ldquo;accpet error:\u0026rdquo;); else { printf(\u0026ldquo;accept a new client: %s:%d\\n\u0026rdquo;,inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //添加一个客户描述符和事件 add_event(epollfd,clifd,EPOLLIN); } }\n//读处理 static void do_read(int epollfd,int fd,char *buf){ int nread; nread = read(fd,buf,MAXSIZE); if (nread == -1) { perror(\u0026ldquo;read error:\u0026rdquo;); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLIN); //删除监听 } else if (nread == 0) { fprintf(stderr,\u0026ldquo;client close.\\n\u0026rdquo;); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLIN); //删除监听 } else { printf(\u0026ldquo;read message is : %s\u0026rdquo;,buf); //修改描述符对应的事件，由读改为写 modify_event(epollfd,fd,EPOLLOUT); } }\n//写处理 static void do_write(int epollfd,int fd,char *buf) { int nwrite; nwrite = write(fd,buf,strlen(buf)); if (nwrite == -1){ perror(\u0026ldquo;write error:\u0026rdquo;); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLOUT); //删除监听 }else{ modify_event(epollfd,fd,EPOLLIN); } memset(buf,0,MAXSIZE); }\n//删除事件 static void delete_event(int epollfd,int fd,int state) { struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,\u0026amp;ev); }\n//修改事件 static void modify_event(int epollfd,int fd,int state){ struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,\u0026amp;ev); } //注：另外一端我就省了\n四 epoll总结 在 select/poll中，进程只有在调用一定的方法后，内核才对所有监视的文件描述符进行扫描，而epoll事先通过epoll_ctl()来注册一 个文件描述符，一旦基于某个文件描述符就绪时，内核会采用类似callback的回调机制，迅速激活这个文件描述符，当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符，而是通过监听回调的的机制。这正是epoll的魅力所在。)\nepoll的优点主要是一下几个方面：\n监视的描述符数量不受限制，它所支持的FD上限是最大可以打开文件的数目，这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右，具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的)，不过虽然linux上面创建进程的代价比较小，但仍旧是不可忽视的，加上进程间数据同步远比不上线程间同步的高效，所以也不是一种完美的方案。 IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式，而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。 如果没有大量的idle -connection或者dead-connection，epoll的效率并不会比select/poll高很多，但是当遇到大量的idle- connection，就会发现epoll的效率大大高于select/poll。\n","permalink":"https://pp-tech-blog.pages.dev/posts/iomodel/","summary":"一 概念说明 在进行解释之前，首先要说明几个概念： 用户空间和内核空间 进程切换 进程的阻塞 文件描述符 缓存 I/O 用户空间与内核空间 现在操作系统都是采用虚拟存储器。","title":"IO模型"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n引言 传统的 Linux 操作系统的标准 I/O 接口是基于数据拷贝操作的，即 I/O 操作会导致数据在操作系统内核地址空间的缓冲区和应用程序地址空间定义的缓冲区之间进行传输。这样做最大的好处是可以减少磁盘 I/O 的操作，因为如果所请求的数据已经存放在操作系统的高速缓冲存储器中，那么就不需要再进行实际的物理磁盘 I/O 操作。但是数据传输过程中的数据拷贝操作却导致了极大的 CPU 开销，限制了操作系统有效进行数据传输操作的能力。 零拷贝（ zero-copy ）技术可以有效地改善数据传输的性能，在内核驱动程序（比如网络堆栈或者磁盘存储驱动程序）处理 I/O 数据的时候，零拷贝技术可以在某种程度上减少甚至完全避免不必要 CPU 数据拷贝操作。\n什么是零拷贝？ 零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。针对操作系统中的设备驱动程序、文件系统以及网络协议堆栈而出现的各种零拷贝技术极大地提升了特定应用程序的性能，并且使得这些应用程序可以更加有效地利用系统资源。这种性能的提升就是通过在数据拷贝进行的同时，允许 CPU 执行其他的任务来实现的。 零拷贝技术可以减少数据拷贝和共享总线操作的次数，消除传输数据在存储器之间不必要的中间拷贝次数，从而有效地提高数据传输效率。而且，零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。进行大量的数据拷贝操作其实是一件简单的任务，从操作系统的角度来说，如果 CPU 一直被占用着去执行这项简单的任务，那么这将会是很浪费资源的；如果有其他比较简单的系统部件可以代劳这件事情，从而使得 CPU 解脱出来可以做别的事情，那么系统资源的利用则会更加有效。综上所述，零拷贝技术的目标可以概括如下：\n避免数据拷贝 ①避免操作系统内核缓冲区之间进行数据拷贝操作。 ②避免操作系统内核和用户应用程序地址空间这两者之间进行数据拷贝操作。 ③用户应用程序可以避开操作系统直接访问硬件存储。 ④数据传输尽量让 DMA 来做。 将多种操作结合在一起 ①避免不必要的系统调用和上下文切换。 ②需要拷贝的数据可以先被缓存起来。 ③对数据进行处理尽量让硬件来做。 接下来就探讨Linux中主要的几种零拷贝技术以及零拷贝技术适用的场景。为了迅速建立起零拷贝的概念，我们拿一个常用的场景进行引入：\n引文 在写一个服务端程序时（Web Server或者文件服务器），文件下载是一个基本功能。这时候服务端的任务是：将服务端主机磁盘中的文件不做修改地从已连接的socket发出去，我们通常用下面的代码完成：\nwhile((n = read(diskfd, buf, BUF_SIZE)) \u0026gt; 0) write(sockfd, buf , n); 基本操作就是循环的从磁盘读入文件内容到缓冲区，再将缓冲区的内容发送到socket。但是由于Linux的I/O操作默认是缓冲I/O。这里面主要使用的也就是read和write两个系统调用，我们并不知道操作系统在其中做了什么。实际上在以上I/O操作中，发生了多次的数据拷贝。\n当应用程序访问某块数据时，操作系统首先会检查，是不是最近访问过此文件，文件内容是否缓存在内核缓冲区，如果是，操作系统则直接根据read系统调用提供的buf地址，将内核缓冲区的内容拷贝到buf所指定的用户空间缓冲区中去。如果不是，操作系统则首先将磁盘上的数据拷贝的内核缓冲区，这一步目前主要依靠DMA来传输，然后再把内核缓冲区上的内容拷贝到用户缓冲区中。 接下来，write系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中，最后socket再把内核缓冲区的内容发送到网卡上。 说了这么多，不如看图清楚： 数据拷贝\n从上图中可以看出，共产生了四次数据拷贝，即使使用了DMA来处理了与硬件的通讯，CPU仍然需要处理两次数据拷贝，与此同时，在用户态与内核态也发生了多次上下文切换，无疑也加重了CPU负担。\n在此过程中，我们没有对文件内容做任何修改，那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费，而零拷贝主要就是为了解决这种低效性。\n什么是零拷贝技术（zero-copy）？ 零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储，主要就是利用各种零拷贝技术，避免让CPU做大量的数据拷贝任务，减少不必要的拷贝，或者让别的组件来做这一类简单的数据传输任务，让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。\n我们继续回到引文中的例子，我们如何减少数据拷贝的次数呢？一个很明显的着力点就是减少数据在内核空间和用户空间来回拷贝，这也引入了零拷贝的一个类型：\n让数据传输不需要经过user space\n使用mmap 我们减少拷贝次数的一种方法是调用mmap()来代替read调用：\nbuf = mmap(diskfd, len); write(sockfd, buf, len); 应用程序调用mmap()，磁盘上的数据会通过DMA被拷贝的内核缓冲区，接着操作系统会把这段内核缓冲区与应用程序共享，这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中，这一切都发生在内核态，最后，socket缓冲区再把数据发到网卡去。 同样的，看图很简单：\n使用mmap替代read很明显减少了一次拷贝，当拷贝数据量很大时，无疑提升了效率。但是使用mmap是有代价的。当你使用mmap时，你可能会遇到一些隐藏的陷阱。例如，当你的程序map了一个文件，但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被SIGBUS信号终止。SIGBUS信号默认会杀死你的进程并产生一个coredump,如果你的服务器这样被中止了，那会产生一笔损失。\n通常我们使用以下解决方案避免这种问题：\n为SIGBUS信号建立信号处理程序 当遇到SIGBUS信号时，信号处理程序简单地返回，write系统调用在被中断之前会返回已经写入的字节数，并且errno会被设置成success,但是这是一种糟糕的处理办法，因为你并没有解决问题的实质核心。 使用文件租借锁 通常我们使用这种方法，在文件描述符上使用租借锁，我们为文件向内核申请一个租借锁，当其它进程想要截断这个文件时，内核会向我们发送一个实时的RT_SIGNAL_LEASE信号，告诉我们内核正在破坏你加持在文件上的读写锁。这样在程序访问非法内存并且被SIGBUS杀死之前，你的write系统调用会被中断。write会返回已经写入的字节数，并且置errno为success。 我们应该在mmap文件之前加锁，并且在操作完文件后解锁： if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) { perror(\"kernel lease set signal\"); return -1; } /* l_type can be F_RDLCK F_WRLCK 加锁*/ /* l_type can be F_UNLCK 解锁*/ if(fcntl(diskfd, F_SETLEASE, l_type)){ perror(\"kernel lease set type\"); return -1; } 使用sendfile 从2.1版内核开始，Linux引入了sendfile来简化操作:\n#include\u0026lt;sys/sendfile.h\u0026gt; ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 系统调用sendfile()在代表输入文件的描述符in_fd和代表输出文件的描述符out_fd之间传送文件内容（字节）。描述符out_fd必须指向一个套接字，而in_fd指向的文件必须是可以mmap的。这些局限限制了sendfile的使用，使sendfile只能将数据从文件传递到套接字上，反之则不行。 使用sendfile不仅减少了数据拷贝的次数，还减少了上下文切换，数据传送始终只发生在kernel space。\nsendfile系统调用过程 在我们调用sendfile时，如果有其它进程截断了文件会发生什么呢？假设我们没有设置任何信号处理程序，sendfile调用仅仅返回它在被中断之前已经传输的字节数，errno会被置为success。如果我们在调用sendfile之前给文件加了锁，sendfile的行为仍然和之前相同，我们还会收到RT_SIGNAL_LEASE的信号。\n目前为止，我们已经减少了数据拷贝的次数了，但是仍然存在一次拷贝，就是页缓存到socket缓存的拷贝。那么能不能把这个拷贝也省略呢？\n借助于硬件上的帮助，我们是可以办到的。之前我们是把页缓存的数据拷贝到socket缓存中，实际上，我们仅仅需要把缓冲区描述符传到socket缓冲区，再把数据长度传过去，这样DMA控制器直接将页缓存中的数据打包发送到网络中就可以了。\n总结一下，sendfile系统调用利用DMA引擎将文件内容拷贝到内核缓冲区去，然后将带有文件位置和长度信息的缓冲区描述符添加socket缓冲区去，这一步不会将内核中的数据拷贝到socket缓冲区中，DMA引擎会将内核缓冲区的数据拷贝到协议引擎中去，避免了最后一次拷贝。\n带DMA的sendfile 不过这一种收集拷贝功能是需要硬件以及驱动程序支持的。\n使用splice sendfile只适用于将数据从文件拷贝到套接字上，限定了它的使用范围。Linux在2.6.17版本引入splice系统调用，用于在两个文件描述符中移动数据：\n#define _GNU_SOURCE /* See feature_test_macros(7) */ #include \u0026lt;fcntl.h\u0026gt; ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); splice调用在两个文件描述符之间移动数据，而不需要数据在内核空间和用户空间来回拷贝。他从fd_in拷贝len长度的数据到fd_out，但是有一方必须是管道设备，这也是目前splice的一些局限性。flags参数有以下几种取值：\nSPLICE_F_MOVE ：尝试去移动数据而不是拷贝数据。这仅仅是对内核的一个小提示：如果内核不能从pipe移动数据或者pipe的缓存不是一个整页面，仍然需要拷贝数据。Linux最初的实现有些问题，所以从2.6.21开始这个选项不起作用，后面的Linux版本应该会实现。 SPLICE_F_NONBLOCK ：splice 操作不会被阻塞。然而，如果文件描述符没有被设置为不可被阻塞方式的 I/O ，那么调用 splice 有可能仍然被阻塞。 SPLICE_F_MORE： 后面的splice调用会有更多的数据。 splice调用利用了Linux提出的管道缓冲区机制， 所以至少一个描述符要为管道。 以上几种零拷贝技术都是减少数据在用户空间和内核空间拷贝技术实现的，但是有些时候，数据必须在用户空间和内核空间之间拷贝。这时候，我们只能针对数据在用户空间和内核空间拷贝的时机上下功夫了。Linux通常利用写时复制(copy on write)来减少系统开销，这个技术又时常称作COW。\n由于篇幅原因，本文不详细介绍写时复制。大概描述下就是：如果多个程序同时访问同一块数据，那么每个程序都拥有指向这块数据的指针，在每个程序看来，自己都是独立拥有这块数据的，只有当程序需要对数据内容进行修改时，才会把数据内容拷贝到程序自己的应用空间里去，这时候，数据才成为该程序的私有数据。如果程序不需要对数据进行修改，那么永远都不需要拷贝数据到自己的应用空间里。这样就减少了数据的拷贝。写时复制的内容可以再写一篇文章了。。。\n除此之外，还有一些零拷贝技术，比如传统的Linux I/O中加上O_DIRECT标记可以直接I/O，避免了自动缓存，还有尚未成熟的fbufs技术，本文尚未覆盖所有零拷贝技术，只是介绍常见的一些，如有兴趣，可以自行研究，一般成熟的服务端项目也会自己改造内核中有关I/O的部分，提高自己的数据传输速率。\n零拷贝原理 1.io读写的方式 1.1中断 1.2DMA 2.中断方式 2.1中断方式的流程图 如下： ①用户进程发起数据读取请求 ②系统调度为该进程分配cpu ③cpu向io控制器(ide,scsi)发送io请求 ④用户进程等待io完成，让出cpu ⑤系统调度cpu执行其他任务 ⑥数据写入至io控制器的缓冲寄存器 ⑦缓冲寄存器满了向cpu发出中断信号 ⑧cpu读取数据至内存\n2.2缺点 中断次数取决于缓冲寄存器的大小\n3.DMA ： 直接内存存取 3.1DMA方式的流程图 如下： ①用户进程发起数据读取请求 ②系统调度为该进程分配cpu ③cpu向DMA发送io请求 ④用户进程等待io完成，让出cpu ⑤系统调度cpu执行其他任务 ⑥数据写入至io控制器的缓冲寄存器 ⑦DMA不断获取缓冲寄存器中的数据（需要cpu时钟） ⑧传输至内存（需要cpu时钟） ⑨所需的全部数据获取完毕后向cpu发出中断信号\n3.2优点 减少cpu中断次数，不用cpu拷贝数据\n4.数据拷贝 4.1下面展示了 传统方式读取数据后并通过网络发送 所发生的数据拷贝： ①一个read系统调用后，DMA执行了一次数据拷贝，从磁盘到内核空间 ②read结束后，发生第二次数据拷贝，由cpu将数据从内核空间拷贝至用户空间 ③send系统调用，cpu发生第三次数据拷贝，由cpu将数据从用户空间拷贝至内核空间(socket缓冲区) ④send系统调用结束后，DMA执行第四次数据拷贝，将数据从内核拷贝至协议引擎 ⑤另外，这四个过程中，每个过程都发生一次上下文切换\n4.2内存缓冲数据，主要是为了提高性能，内核可以预读部分数据，当所需数据小于内存缓冲区大小时，将极大的提高性能。 4.3零拷贝是为了消除这个过程中冗余的拷贝 ","permalink":"https://pp-tech-blog.pages.dev/posts/zerocopy/","summary":"引言 传统的 Linux 操作系统的标准 I/O 接口是基于数据拷贝操作的，即 I/O 操作会导致数据在操作系统内核地址空间的缓冲区和应用程序地址空间定义的缓冲区之间进行传。","title":"零拷贝原理详解"},{"content":" 1、非静态成员变量被配置于每一个class object之内 2、静态成员变量则被存放在个别的class object之外 3、静态和非静态成员函数都被放在个别的class object之外 4、虚函数： （1）每一个class产生一堆指向虚函数的指针，放在virtual table(vtbl) （2）每个class object被安插一个指针，指向相关的virtual table。通常这个指针被称为vptr。每一个class所关联的type_info object也经由vtbl被指出来，放在表格的第一个slot。 ","permalink":"https://pp-tech-blog.pages.dev/posts/cobjectmodel/","summary":"1、非静态成员变量被配置于每一个class object之内 2、静态成员变量则被存放在个别的class object之外 3、静态和非静态成员函数都被放在个别的class。","title":"C++对象模型"},{"content":"4.3 C++对象模型和this指针 4.3.1 成员变量和成员函数分开存储 在C++中，类内的成员变量和成员函数分开存储\n只有非静态成员变量才属于类的对象上\n非静态成员变量：\n静态成员变量 静态成员函数 非静态成员函数 4.3.2 this指针概念 通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的\n每一个非静态成员函数只会诞生一份函数实例，也就是说多个同类型的对象会共用一块代码\n那么问题是：这一块代码是如何区分那个对象调用自己的呢？\nc++通过提供特殊的对象指针，this指针，解决上述问题。this指针指向被调用的成员函数所属的对象\nthis指针是隐含每一个非静态成员函数内的一种指针\nthis指针不需要定义，直接使用即可\nthis指针的用途：\n当形参和成员变量同名时，可用this指针来区分 在类的非静态成员函数中返回对象本身，可使用return *this\nclass Person { public: Person(int age) { //1、当形参和成员变量同名时，可用this指针来区分 this-\u0026amp;gt;age = age; } Person\u0026amp;amp; PersonAddPerson(Person p) { this-\u0026amp;gt;age += p.age; //返回对象本身，this是指向原对象的指针，*this为原对象 return *this; } int age; };\nvoid test01() { Person p1(10); cout \u0026lt;\u0026lt; \u0026ldquo;p1.age = \u0026quot; \u0026lt;\u0026lt; p1.age \u0026lt;\u0026lt; endl;\nPerson p2(10); p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); //每次返回的还是p2,所以add函数需要是person\u0026amp;amp; 引用类型，直接在原对象上操作，否则是值传递，返回的对象是重新拷贝的不是p2了 cout \u0026amp;lt;\u0026amp;lt; \u0026quot;p2.age = \u0026quot; \u0026amp;lt;\u0026amp;lt; p2.age \u0026amp;lt;\u0026amp;lt; endl; } 4.3.3 空指针访问成员函数 C++中空指针也是可以调用成员函数的，但是也要注意有没有用到this指针\n如果用到this指针，需要加以判断保证代码的健壮性\n//空指针访问成员函数 class Person { public: void ShowClassName() { cout \u0026amp;lt;\u0026amp;lt; \u0026quot;我是Person类!\u0026quot; \u0026amp;lt;\u0026amp;lt; endl; } void ShowPerson() { if (this == NULL) { //健壮性条件 return; } cout \u0026amp;lt;\u0026amp;lt; mAge \u0026amp;lt;\u0026amp;lt; endl; //默认this-\u0026amp;gt;mAge } public: int mAge; };\nvoid test01() { Person * p = NULL; p-\u0026gt;ShowClassName(); //空指针，可以调用成员函数 p-\u0026gt;ShowPerson(); //但是如果成员函数中用到了this指针，就不可以了 } const 修饰成员函数 常函数 成员函数后面加const 成为常函数 常函数内不可以修改成员属性 成员属性声明时加关键字mutable后，在常函数中依然可以修改 常对象 声明对象前加const 成为常对象 常对象只能调用常函数 class Person { public: Person() { m_A = 0; m_B = 0; } //this指针的本质是一个指针常量，指针的指向不可修改 //如果想让指针指向的值也不可以修改，需要声明常函数 void ShowPerson() const { //const Type* const pointer; //this = NULL; //不能修改指针的指向 Person* const this; //this-\u0026amp;gt;mA = 100; //但是this指针指向的对象的数据是可以修改的 //const修饰成员函数，表示指针指向的内存空间的数据不能修改，除了mutable修饰的变量 this-\u0026amp;gt;m_B = 100; } void MyFunc() { //mA = 10000; } public: int m_A; mutable int m_B; //可修改 可变的 };\nconst Person person; //常量对象\nperson.ShowPerson(); //常对象只能调用常函数 person.MyFunc() // 错误\nperson.m_B = 100; //常对象可以修改mutable修饰成员变量\n","permalink":"https://pp-tech-blog.pages.dev/posts/cpp4-3/","summary":"4.3 C++对象模型和this指针 4.3.1 成员变量和成员函数分开存储 在C++中，类内的成员变量和成员函数分开存储 只有非静态成员变量才属于类的对象上 非静态成员变。","title":"cpp核心编程_4类和对象3 C++对象模型和this指针"},{"content":"4.2 对象的初始化和清理 4.2.1 构造函数和析构函数 对象的初始化和清理也是两个非常重要的安全问题\n一个对象或者变量没有初始状态，对其使用后果是未知\n同样的使用完一个对象或变量，没有及时清理，也会造成一定的安全问题\nc++利用了构造函数和析构函数解决上述问题，这两个函数将会被编译器自动调用，完成对象初始化和清理工作。\n对象的初始化和清理工作是编译器强制要我们做的事情，因此如果我们不提供构造和析构，编译器会提供\n编译器提供的构造函数和析构函数是空实现。\n构造函数：主要作用在于创建对象时为对象的成员属性赋值，构造函数由编译器自动调用，无须手动调用。 析构函数：主要作用在于对象销毁前系统自动调用，执行一些清理工作。\n构造函数 语法：类名(){}\n构造函数，没有返回值也不写void 函数名称与类名相同 构造函数可以有参数，因此可以发生重载 程序在调用对象时候会自动调用构造，无须手动调用,而且只会调用一次 析构函数 语法： ~类名(){}\n析构函数，没有返回值也不写void 函数名称与类名相同,在名称前加上符号 ~ 析构函数不可以有参数，因此不可以发生重载 程序在对象销毁前会自动调用析构，无须手动调用,而且只会调用一次 class Person { public: //构造函数 Person() { cout \u0026lt;\u0026lt; \"Person的构造函数调用\" \u0026lt;\u0026lt; endl; } //析构函数 ~Person() { cout \u0026lt;\u0026lt; \"Person的析构函数调用\" \u0026lt;\u0026lt; endl; } };\nvoid test01() { Person p; }\nint main() {\ntest01(); system(\u0026quot;pause\u0026quot;); return 0; }\n//输出 //构造函数的调用 //析构函数的调用\n4.2.2 构造函数的分类及调用 两种分类方式：\n按参数分为： 有参构造和无参构造 按类型分为： 普通构造和拷贝构造 三种调用方式：\n括号法* 显示法 隐式转换法 //1、构造函数分类 // 按照参数分类分为 有参和无参构造 无参又称为默认构造函数 // 按照类型分类分为 普通构造和拷贝构造 class Person { public: //无参（默认）构造函数 Person() { cout \u0026lt;\u0026lt; \u0026ldquo;无参构造函数!\u0026rdquo; \u0026lt;\u0026lt; endl; } //有参构造函数 Person(int a) { age = a; cout \u0026lt;\u0026lt; \u0026ldquo;有参构造函数!\u0026rdquo; \u0026lt;\u0026lt; endl; } //拷贝构造函数 Person(const Person\u0026amp; p) { age = p.age; cout \u0026lt;\u0026lt; \u0026ldquo;拷贝构造函数!\u0026rdquo; \u0026lt;\u0026lt; endl; } //析构函数 ~Person() { cout \u0026lt;\u0026lt; \u0026ldquo;析构函数!\u0026rdquo; \u0026lt;\u0026lt; endl; } public: int age; };\n//2、构造函数的调用 //调用无参构造函数 void test01() { Person p; //调用无参构造函数 }\n//调用有参的构造函数 void test02() {\n//2.1 括号法，常用 Person p1(10); //注意1：调用无参构造函数不能加括号，如果加了编译器认为这是一个函数声明 //Person p2(); //2.2 显式法 Person p2 = Person(10); Person p3 = Person(p2); //Person(10)单独写就是匿名对象 当前行结束之后，马上析构 //2.3 隐式转换法 Person p4 = 10; // Person p4 = Person(10); Person p5 = p4; // Person p5 = Person(p4); //注意2：不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明 //Person(p5); }\nint main() {\ntest01(); //test02(); system(\u0026quot;pause\u0026quot;); return 0; }\n4.2.3 拷贝构造函数调用时机 C++中拷贝构造函数调用时机通常有三种情况\n使用一个已经创建完毕的对象来初始化一个新对象 值传递的方式给函数参数传值 以值方式返回局部对象 class Person { public: Person() { cout \u0026lt;\u0026lt; \"无参构造函数!\" \u0026lt;\u0026lt; endl; mAge = 0; } Person(int age) { cout \u0026lt;\u0026lt; \"有参构造函数!\" \u0026lt;\u0026lt; endl; mAge = age; } Person(const Person\u0026amp; p) { cout \u0026lt;\u0026lt; \"拷贝构造函数!\" \u0026lt;\u0026lt; endl; mAge = p.mAge; } //析构函数在释放内存之前调用 ~Person() { cout \u0026lt;\u0026lt; \"析构函数!\" \u0026lt;\u0026lt; endl; } public: int mAge; }; //1. 使用一个已经创建完毕的对象来初始化一个新对象 void test01() {\nPerson man(100); //p对象已经创建完毕 Person newman(man); //调用拷贝构造函数 Person newman2 = man; //拷贝构造 //Person newman3; //newman3 = man; //不是调用拷贝构造函数，赋值操作 }\n//2. 值传递的方式给函数参数传值 //相当于Person p1 = p; void doWork(Person p1) {} void test02() { Person p; //无参构造函数 doWork(p); }\n//3. 以值方式返回局部对象 Person doWork2() { Person p1; cout \u0026lt;\u0026lt; (int *)\u0026amp;p1 \u0026lt;\u0026lt; endl; return p1; }\nvoid test03() { Person p = doWork2(); cout \u0026lt;\u0026lt; (int *)\u0026amp;p \u0026lt;\u0026lt; endl; }\nint main() {\n//test01(); //test02(); test03(); system(\u0026quot;pause\u0026quot;); return 0; }\n4.2.4 构造函数调用规则 默认情况下，c++编译器至少给一个类添加3个函数\n1．默认构造函数(无参，函数体为空)\n2．默认析构函数(无参，函数体为空)\n3．默认拷贝构造函数，对属性进行值拷贝\n构造函数调用规则如下：\n如果用户定义有参构造函数，c++不在提供默认无参构造，但是会提供默认拷贝构造\n如果用户定义拷贝构造函数，c++不会再提供其他构造函数\nvoid test01() { Person p1(18); //如果不写拷贝构造，编译器会自动添加拷贝构造，并且做浅拷贝操作 Person p2(p1); cout \u0026amp;lt;\u0026amp;lt; \u0026quot;p2的年龄为： \u0026quot; \u0026amp;lt;\u0026amp;lt; p2.age \u0026amp;lt;\u0026amp;lt; endl; }\nvoid test02() { //如果用户提供有参构造，编译器不会提供默认构造，会提供拷贝构造 Person p1; //此时如果用户自己没有提供默认构造，会出错 Person p2(10); //用户提供的有参 Person p3(p2); //此时如果用户没有提供拷贝构造，编译器会提供\n//如果用户提供拷贝构造，编译器不会提供其他构造函数 Person p4; //此时如果用户自己没有提供默认构造，会出错 Person p5(10); //此时如果用户自己没有提供有参，会出错 Person p6(p5); //用户自己提供拷贝构造 }\n4.2.5 深拷贝与浅拷贝 深浅拷贝是面试经典问题，也是常见的一个坑\n浅拷贝：简单的赋值拷贝操作\n深拷贝：在堆区重新申请空间，进行拷贝操作\n//有参构造函数 Person(int age ,int height) { cout \u0026amp;lt;\u0026amp;lt; \u0026quot;有参构造函数!\u0026quot; \u0026amp;lt;\u0026amp;lt; endl; m_age = age; m_height = new int(height); } //拷贝构造函数\nPerson(const Person\u0026amp; p) { cout \u0026lt;\u0026lt; \u0026ldquo;拷贝构造函数!\u0026rdquo; \u0026lt;\u0026lt; endl; //m_height = p.m_height;//编译器默认实现这个操作（浅拷贝） //如果不利用深拷贝在堆区创建新内存，会导致浅拷贝带来的重复释放堆区问题 m_age = p.m_age; m_height = new int(*p.m_height); //深拷贝\n}\n//析构函数 ~Person() { cout \u0026lt;\u0026lt; \u0026ldquo;析构函数!\u0026rdquo; \u0026lt;\u0026lt; endl; if (m_height != NULL) { delete m_height; } }\n总结：如果属性有在堆区开辟的，一定要自己提供拷贝构造函数，防止浅拷贝带来的问题 4.2.6 初始化列表 作用：\nC++提供了初始化列表语法，用来初始化属性\n语法：构造函数()：属性1(值1),属性2（值2）... {} 示例：\nclass Person { public: ////传统方式初始化 //Person(int a, int b, int c) { // m_A = a; // m_B = b; // m_C = c; //} //初始化列表方式初始化 Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {} int m_A; int m_B; int m_C; };\n4.2.7 类对象作为类成员 C++类中的成员可以是另一个类的对象，我们称该成员为 对象成员 构造的顺序是 ：\n先调用对象成员的构造，再调用本类构造 析构顺序与构造相反 4.2.8 静态成员 静态成员就是在成员变量和成员函数前加上关键字static，称为静态成员\n静态成员 静态成员变量 所有对象共享同一份数据 在编译阶段分配内存 类内声明，类外初始化 静态成员函数 所有对象共享同一个函数 静态成员函数只能访问静态成员变量 因为所有对象共享静成员，不属于某个特定的对象，函数无法区分他是属于哪一个对象，所以静态成员函数无法访问静态成员变量\nclass Person { public:\n//静态成员函数特点： //1 程序共享一个函数 //2 静态成员函数只能访问静态成员变量 static void func() { cout \u0026amp;lt;\u0026amp;lt; \u0026quot;func调用\u0026quot; \u0026amp;lt;\u0026amp;lt; endl; m_A = 100; //m_B = 100; //错误，不可以访问非静态成员变量 } static int m_A; //静态成员变量，类内声明 int m_B; // private:\n//静态成员函数也是有访问权限的 static void func2() { cout \u0026amp;lt;\u0026amp;lt; \u0026quot;func2调用\u0026quot; \u0026amp;lt;\u0026amp;lt; endl; } }; int Person::m_A = 10; //类外初始化\nvoid test01() { //静态成员变量两种访问方式\n//1、通过对象 Person p1; p1.func(); //2、通过类名 Person::func(); //Person::func2(); //私有权限访问不到 }\n","permalink":"https://pp-tech-blog.pages.dev/posts/cpp4-2/","summary":"4.2 对象的初始化和清理 4.2.1 构造函数和析构函数 对象的初始化和清理也是两个非常重要的安全问题 一个对象或者变量没有初始状态，对其使用后果是未知 同样的使用完一个。","title":"cpp核心编程_4类和对象2 对象的初始化和清理"},{"content":"3.1 函数默认参数 在C++中，函数的形参列表中的形参是可以有默认值的。\n语法： 返回值类型 函数名 （参数= 默认值）{}\nint func(int a, int b = 10, int c = 10) { return a + b + c; } //1. 如果某个位置参数有默认值，那么从这个位置往后，从左向右，必须都要有默认值 //2. 如果函数声明有默认值，函数实现的时候就不能有默认参数（声明和实现只能由一个有默认参数） int func2(int a = 10, int b = 10); int func2(int a, int b) { return a + b; }\nint main() {\ncout \u0026amp;lt;\u0026amp;lt; \u0026quot;ret = \u0026quot; \u0026amp;lt;\u0026amp;lt; func(20, 20) \u0026amp;lt;\u0026amp;lt; endl; cout \u0026amp;lt;\u0026amp;lt; \u0026quot;ret = \u0026quot; \u0026amp;lt;\u0026amp;lt; func(100) \u0026amp;lt;\u0026amp;lt; endl; system(\u0026quot;pause\u0026quot;); return 0; }\n3.2 函数占位参数 C++中函数的形参列表里可以有占位参数，用来做占位，调用函数时必须填补该位置 语法： 返回值类型 函数名 (数据类型){}\n//函数占位参数 ，占位参数也可以有默认参数 void func(int a, int) { cout \u0026lt;\u0026lt; \"this is func\" \u0026lt;\u0026lt; endl; } int main() {\nfunc(10,10); //占位参数必须填补 system(\u0026quot;pause\u0026quot;); return 0; }\n3.3 函数重载 3.3.1 函数重载概述 作用：函数名可以相同，提高复用性\n函数重载满足条件：\n同一个作用域下 函数名称相同 函数参数类型不同 或者 个数不同 或者 顺序不同 注意: 函数的返回值不可以作为函数重载的条件 //函数重载需要函数都在同一个作用域下 void func() { cout \u0026lt;\u0026lt; \"func 的调用！\" \u0026lt;\u0026lt; endl; } void func(int a) { cout \u0026lt;\u0026lt; \"func (int a) 的调用！\" \u0026lt;\u0026lt; endl; } void func(double a) { cout \u0026lt;\u0026lt; \"func (double a)的调用！\" \u0026lt;\u0026lt; endl; } void func(int a ,double b) { cout \u0026lt;\u0026lt; \"func (int a ,double b) 的调用！\" \u0026lt;\u0026lt; endl; } void func(double a ,int b) { cout \u0026lt;\u0026lt; \"func (double a ,int b)的调用！\" \u0026lt;\u0026lt; endl; } //函数返回值不可以作为函数重载条件 //int func(double a, int b) //{ // cout \u0026lt;\u0026lt; \u0026ldquo;func (double a ,int b)的调用！\u0026rdquo; \u0026lt;\u0026lt; endl; //}\nint main() {\nfunc(); func(10); func(3.14); func(10,3.14); func(3.14 , 10); system(\u0026quot;pause\u0026quot;); return 0; }\n3.3.2 函数重载注意事项 引用作为重载条件 函数重载碰到函数默认参数\n//函数重载注意事项 //1、引用作为重载条件 void func(int \u0026amp;a) { cout \u0026lt;\u0026lt; \u0026ldquo;func (int \u0026amp;a) 调用 \u0026quot; \u0026lt;\u0026lt; endl; }\nvoid func(const int \u0026amp;a) { cout \u0026lt;\u0026lt; \u0026ldquo;func (const int \u0026amp;a) 调用 \u0026quot; \u0026lt;\u0026lt; endl; }\n//2、函数重载碰到函数默认参数\nvoid func2(int a, int b = 10) { cout \u0026lt;\u0026lt; \u0026ldquo;func2(int a, int b = 10) 调用\u0026rdquo; \u0026lt;\u0026lt; endl; }\nvoid func2(int a) { cout \u0026lt;\u0026lt; \u0026ldquo;func2(int a) 调用\u0026rdquo; \u0026lt;\u0026lt; endl; }\nint main() {\nint a = 10; func(a); //调用无const func(10);//调用有const //func2(10); //碰到默认参数产生歧义，需要避免 system(\u0026quot;pause\u0026quot;); return 0; }\n尽量避免在函数重载中用默认参数\n","permalink":"https://pp-tech-blog.pages.dev/posts/cppfunupup/","summary":"3.1 函数默认参数 在C++中，函数的形参列表中的形参是可以有默认值的。 语法： 返回值类型 函数名 （参数= 默认值）{} int func(int a, int b。","title":"cpp核心编程_3函数提高"},{"content":"4 类和对象 C++面向对象的三大特性为：==封装、继承、多态==\nC++认为==万事万物都皆为对象==，对象上有其属性和行为\n4.1 封装 4.1.1 封装的意义 封装是C++面向对象三大特性之一\n封装的意义：\n将属性和行为作为一个整体，表现生活中的事物 将属性和行为加以权限控制\n-封装意义一： 在设计类的时候，属性和行为写在一起，表现事物 语法： class 类名{ 访问权限： 属性 / 行为 };\n//学生类 class Student { public: void setName(string name) { m_name = name; } void setID(int id) { m_id = id; } void showStudent() { cout \u0026amp;lt;\u0026amp;lt; \u0026quot;name:\u0026quot; \u0026amp;lt;\u0026amp;lt; m_name \u0026amp;lt;\u0026amp;lt; \u0026quot; ID:\u0026quot; \u0026amp;lt;\u0026amp;lt; m_id \u0026amp;lt;\u0026amp;lt; endl; } public: string m_name; int m_id; };\nint main() {\nStudent stu; stu.setName(\u0026quot;德玛西亚\u0026quot;); stu.setID(250); stu.showStudent(); system(\u0026quot;pause\u0026quot;); return 0; }\n封装意义二： 类在设计时，可以把属性和行为放在不同的权限下，加以控制\n访问权限有三种：\npublic 公共权限 类内可以访问 类外可以访问 protected 保护权限 类内可以访问 类外不可以访问 儿子可以访问父亲中的保护内容 private 私有权限 类内可以访问 类外不可以访问 儿子不可以访问父亲中的私有内容 4.1.2 struct和class区别 在C++中 struct和class唯一的区别就在于 默认的访问权限不同\n区别：\nstruct 默认权限为公共 class 默认权限为私有\n4.1.3 成员属性设置为私有 优点1：将所有成员属性设置为私有，可以自己控制读写权限\n优点2：对于写权限，我们可以检测数据的有效性\n立方体封装 设计立方体类(Cube) 求出立方体的面积和体积 分别用全局函数和成员函数判断两个立方体是否相等。\n#include \u0026lt;iostream\u0026gt; using namespace std; #include \u0026lt;string\u0026gt; template \u0026lt;typename T\u0026gt; class lifangti { private: T m_h; T m_l; T m_w; public: void set_m_h(T f1) { m_h = f1; } T get_m_h() { return m_h; } void set_m_l(T f1) { m_l = f1; } T get_m_l() { return m_l; } void set_m_w(T f1) { m_w = f1; } T get_m_w() { return m_w; } T get_v() { T v = m_hm_lm_w; return v; } bool issame(lifangti \u0026amp;l1) //成员函数判断 { if (l1.get_m_h() == m_h \u0026amp;\u0026amp; l1.get_m_l() == m_l \u0026amp;\u0026amp; l1.get_m_w() == m_w) { return true; } else { return false; }\n} };\nbool issame(lifangti \u0026amp;l1, lifangti \u0026amp;l2) //全局函数判断 { if (l1.get_m_h() == l2.get_m_h() \u0026amp;\u0026amp; l1.get_m_l() == l2.get_m_l() \u0026amp;\u0026amp; l1.get_m_w() == l2.get_m_w()) { return true; } else { return false; }\n} T main(T argc, char const *argv[]) { lifangti l1; l1.set_m_h(10); l1.set_m_l(10); l1.set_m_w(10); cout \u0026lt;\u0026lt; l1.get_v() \u0026lt;\u0026lt; endl; lifangti l2; l2.set_m_h(10); l2.set_m_l(10); l2.set_m_w(10); if (issame(l1, l2)) { cout \u0026lt;\u0026lt; \u0026ldquo;1yang\u0026rdquo; \u0026lt;\u0026lt;endl; } cout \u0026lt;\u0026lt; l1.issame(l2) \u0026lt;\u0026lt; endl;\nreturn 0; } ","permalink":"https://pp-tech-blog.pages.dev/posts/cppclass/","summary":"4 类和对象 C++面向对象的三大特性为：==封装、继承、多态== C++认为==万事万物都皆为对象==，对象上有其属性和行为 4.1 封装 4.1.1 封装的意义 封装是。","title":"cpp核心编程_4类和对象1封装"},{"content":"引用 2.1 引用的基本使用 作用： 给变量起别名\n语法： 数据类型 \u0026amp;别名 = 原名\nint main(int argc, char const *argv[]) { int a = 10; cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; int \u0026amp; q = a; q = 20; cout \u0026lt;\u0026lt; q \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; return 0; } 共同指向一个内存\n2.2 引用注意事项 引用必须初始化 引用在初始化后，不可以改变\nint main(int argc, char const *argv[]) { int a = 10; cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; // int \u0026amp;b; //错误，引用必须初始化 int \u0026amp; b = a; int c = 20; b = c; //赋值操作，而不是更改引用，将3复制到b指向的内存 cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; //20 cout \u0026lt;\u0026lt; b \u0026lt;\u0026lt; endl; //20 cout \u0026lt;\u0026lt; c \u0026lt;\u0026lt; endl; //20 return 0; } 2.3 引用做函数参数 作用：函数传参时，可以利用引用的技术让形参修饰实参\n优点：可以简化指针修改实参\n//3. 引用传递 void mySwap03(int\u0026amp; a, int\u0026amp; b) { int temp = a; a = b; b = temp; } 2.4 引用做函数返回值 作用：引用是可以作为函数的返回值存在的\n注意：不要返回局部变量引用\n用法：函数调用作为左值\n// 函数引用作为左值 int \u0026amp; test(){ static int a =10; //静态变量在全局区，程序结束后由系统释放 return a; } int main(int argc, char const *argv[]) { int \u0026amp; a = test(); cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; //10 test() = 1000; cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; //1000\nreturn 0; test调用完返回a的引用，相当于把a返回，test() = 1000;-\u0026gt; a = 1000;\n2.5 引用的本质 本质：引用的本质在c++内部实现是一个指针常量.\n//发现是引用，转换为 int* const ref = \u0026amp;a; void func(int\u0026amp; ref){ ref = 100; // ref是引用，转换为*ref = 100 } int main(){ int a = 10; //自动转换为 int* const ref = \u0026amp;amp;a; 指针常量是指针指向不可改，也说明为什么引用不可更改 int\u0026amp;amp; ref = a; ref = 20; //内部发现ref是引用，自动帮我们转换为: *ref = 20; cout \u0026amp;lt;\u0026amp;lt; \u0026quot;a:\u0026quot; \u0026amp;lt;\u0026amp;lt; a \u0026amp;lt;\u0026amp;lt; endl; cout \u0026amp;lt;\u0026amp;lt; \u0026quot;ref:\u0026quot; \u0026amp;lt;\u0026amp;lt; ref \u0026amp;lt;\u0026amp;lt; endl; func(a); return 0; }\nC++推荐用引用技术，因为语法方便，引用本质是指针常量，但是所有的指针操作编译器都帮我们做了\n2.6 常量引用 作用：常量引用主要用来修饰形参，防止误操作\n在函数形参列表中，可以加==const修饰形参==，防止形参改变实参\n//引用使用的场景，通常用来修饰形参 void showValue(const int\u0026amp; v) { //v += 10; cout \u0026lt;\u0026lt; v \u0026lt;\u0026lt; endl; } int main() {\n//int\u0026amp;amp; ref = 10; 引用本身需要一个合法的内存空间，因此这行错误 //加入const就可以了，编译器优化代码，int temp = 10; const int\u0026amp;amp; ref = temp; const int\u0026amp;amp; ref = 10; //ref = 100; //加入const后不可以修改变量 cout \u0026amp;lt;\u0026amp;lt; ref \u0026amp;lt;\u0026amp;lt; endl; //函数中利用常量引用防止误操作修改实参 int a = 10; showValue(a); system(\u0026quot;pause\u0026quot;); return 0; }\n","permalink":"https://pp-tech-blog.pages.dev/posts/cpp2yinyong/","summary":"引用 2.1 引用的基本使用 作用： 给变量起别名 语法： 数据类型 \u0026amp;别名 = 原名 int main(int argc, char const *argv[]) { i。","title":"cpp核心编程_2 引用"},{"content":"1 内存分区模型 C++程序在执行时，将内存大方向划分为4个区域\n代码区：存放函数体的二进制代码，由操作系统进行管理的 全局区：存放全局变量和静态变量以及常量 栈区：由编译器自动分配释放, 存放函数的参数值,局部变量等 堆区：由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收 内存四区意义： 不同区域存放的数据，赋予不同的生命周期, 给我们更大的灵活编程\n1.1 程序运行前 在程序编译后，生成了exe可执行程序，未执行该程序前分为两个区域\n代码区： 存放 CPU 执行的机器指令\n代码区是共享的，共享的目的是对于频繁被执行的程序，只需要在内存中有一份代码即可\n代码区是只读的，使其只读的原因是防止程序意外地修改了它的指令\n全局区： 全局变量和静态变量存放在此.\n全局区还包含了常量区, 字符串常量和其他常量也存放在此.\n==该区域的数据在程序结束后由操作系统释放==.\n总结： C++中在程序运行前分为全局区和代码区 代码区特点是共享和只读 全局区中存放全局变量、静态变量、常量 常量区中存放 const修饰的全局常量 和 字符串常量\n1.2 程序运行后 栈区： 由编译器自动分配释放, 存放函数的参数值,局部变量等 注意事项：不要返回局部变量的地址，栈区开辟的数据由编译器自动释放\nint * func() { int a = 10; return \u0026amp;a; } int main() {\nint *p = func(); cout \u0026amp;lt;\u0026amp;lt; *p \u0026amp;lt;\u0026amp;lt; endl; cout \u0026amp;lt;\u0026amp;lt; *p \u0026amp;lt;\u0026amp;lt; endl; system(\u0026quot;pause\u0026quot;); return 0; }\n不能输出10，被自动释放\n堆区： 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收\n在C++中主要利用new在堆区开辟内存\nint * func() { int * p = new int(10); return p; } int main() {\nint *p = func(); cout \u0026amp;lt;\u0026amp;lt; *p \u0026amp;lt;\u0026amp;lt; endl; cout \u0026amp;lt;\u0026amp;lt; *p \u0026amp;lt;\u0026amp;lt; endl; return 0; }\n成功输出10 10\n总结：\n堆区数据由程序员管理开辟和释放\n堆区数据利用new关键字进行开辟内存\n1.3 new操作符 C++中利用==new==操作符在堆区开辟数据\n堆区开辟的数据，由程序员手动开辟，手动释放，释放利用操作符 ==delete==\n语法： new 数据类型\n利用new创建的数据，会返回该数据对应的类型的指针\ndelete释放 int * func() { int * p = new int(10); return p; } int main() {\nint *p = func(); cout \u0026amp;lt;\u0026amp;lt; *p \u0026amp;lt;\u0026amp;lt; endl; cout \u0026amp;lt;\u0026amp;lt; *p \u0026amp;lt;\u0026amp;lt; endl; delete p; cout \u0026amp;lt;\u0026amp;lt; *p \u0026amp;lt;\u0026amp;lt; endl; return 0; }\ndelete释放后最后一个输出变为0\nnew数组并delete[]释放 int main() { int * array = new int[10]; for (int i = 0; i \u0026amp;lt; 10; i++) { array[i] = i+100; std::cout \u0026amp;lt;\u0026amp;lt; array[i] \u0026amp;lt;\u0026amp;lt; endl; } delete[] array; return 0; }\n释放数组：delete[] array; delete后加一个[] ","permalink":"https://pp-tech-blog.pages.dev/posts/cppcoreprogramming/","summary":"1 内存分区模型 C++程序在执行时，将内存大方向划分为4个区域 代码区：存放函数体的二进制代码，由操作系统进行管理的 全局区：存放全局变量和静态变量以及常量 栈区：由编译。","title":"cpp核心编程_1 内存分区、内存分配与释放"},{"content":"/*time：2020年9月22日 auther：wpp */ #include \u0026lt;iostream\u0026gt; using namespace std; #include \u0026lt;string\u0026gt; #include \u0026lt;math.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define MAX 1000 struct Person { string name; int age; string phone; }; struct yellowpage { struct Person personArray[MAX]; int size; }; //展示目录 void showMenu() { cout \u0026lt;\u0026lt; \"***********************\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"*****1、添加联系人*****\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"*****2、显示联系人*****\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"*****3、删除联系人*****\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"*****4、查找联系人*****\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"*****5、修改联系人*****\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"*****6、清空联系人*****\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"*****0、退出通讯录*****\" \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \"***********************\" \u0026lt;\u0026lt; endl; } //添加联系人 void addperson(yellowpage *yp) { if (yp-\u0026gt;size == MAX) { cout \u0026lt;\u0026lt; \"空间已满\" \u0026lt;\u0026lt; endl; return; } else { cout \u0026lt;\u0026lt; \"姓名：\" \u0026lt;\u0026lt; endl; string name1; cin \u0026gt;\u0026gt; name1; yp-\u0026gt;personArray[yp-\u0026gt;size].name = name1; cout \u0026lt;\u0026lt; \"年龄：\" \u0026lt;\u0026lt; endl; int age1; cin \u0026gt;\u0026gt; age1; yp-\u0026gt;personArray[yp-\u0026gt;size].age = age1; cout \u0026lt;\u0026lt; \"号码：\" \u0026lt;\u0026lt; endl; string number; cin \u0026gt;\u0026gt; number; yp-\u0026gt;personArray[yp-\u0026gt;size].phone = number; cout \u0026lt;\u0026lt; \"导入完成！\" \u0026lt;\u0026lt; endl; yp-\u0026gt;size++; } } // 显示联系人 void printinfo(yellowpage *yp) { if (yp-\u0026gt;size == 0) { cout \u0026lt;\u0026lt; \"通讯录为空\" \u0026lt;\u0026lt; endl; } else { for (int i = 0; i \u0026lt; yp-\u0026gt;size; i++) { cout \u0026lt;\u0026lt; \"年龄：\" \u0026lt;\u0026lt; yp-\u0026gt;personArray[i].age \u0026lt;\u0026lt; \"\\t\"; cout \u0026lt;\u0026lt; \"姓名：\" \u0026lt;\u0026lt; yp-\u0026gt;personArray[i].name \u0026lt;\u0026lt; \"\\t\"; cout \u0026lt;\u0026lt; \"电话：\" \u0026lt;\u0026lt; yp-\u0026gt;personArray[i].phone \u0026lt;\u0026lt; endl; } } system(\"read -p 'Press Enter to continue...' var\"); } // 检测联系人是否存在,存在返回具体位置，不存在返回-1 int isexist(yellowpage *yp, string name) { for (int i = 0; i \u0026lt; yp-\u0026gt;size; i++) { if (yp-\u0026gt;personArray[i].name == name) { return i; } } return -1; } // 删除通讯录中i位置的人 void dropperson(yellowpage *yp) { cout \u0026lt;\u0026lt; \"请输入要删除人的姓名：\" \u0026lt;\u0026lt; endl; string name; cin \u0026gt;\u0026gt; name; if (isexist(yp, name)) { cout \u0026lt;\u0026lt; \"查不到，爬！\" \u0026lt;\u0026lt; endl; } else { for (int i = isexist(yp, name); i \u0026lt; yp-\u0026gt;size; i++) { yp-\u0026gt;personArray[i] = yp-\u0026gt;personArray[i + 1]; } yp-\u0026gt;size--; cout \u0026lt;\u0026lt; \"删除成功！\" \u0026lt;\u0026lt; endl; } } // 查找联系人 void selectperson(yellowpage *yp) { cout \u0026lt;\u0026lt; \"输入查找的姓名：\" \u0026lt;\u0026lt; endl; string name; cin \u0026gt;\u0026gt; name; if (isexist(yp, name) == -1) { cout \u0026lt;\u0026lt; \"查无此人\" \u0026lt;\u0026lt; endl; } else { int ret = isexist(yp, name); cout \u0026lt;\u0026lt; \"姓名\" \u0026lt;\u0026lt; yp-\u0026gt;personArray[ret].name \u0026lt;\u0026lt; \"\\t\"; cout \u0026lt;\u0026lt; \"年龄\" \u0026lt;\u0026lt; yp-\u0026gt;personArray[ret].age \u0026lt;\u0026lt; \"\\t\"; cout \u0026lt;\u0026lt; \"电话\" \u0026lt;\u0026lt; yp-\u0026gt;personArray[ret].phone \u0026lt;\u0026lt; endl; } system(\"read -p 'Press Enter to continue...' var\"); } // 修改联系人 void alterperson(yellowpage *yp) { cout \u0026lt;\u0026lt; \"输入要修改的姓名：\" \u0026lt;\u0026lt; endl; string name; cin \u0026gt;\u0026gt; name; if (isexist(yp, name) == -1) { cout \u0026lt;\u0026lt; \"查无此人\" \u0026lt;\u0026lt; endl; } else { int ret = isexist(yp, name); cout \u0026lt;\u0026lt; \"姓名：\" \u0026lt;\u0026lt; endl; string name1; cin \u0026gt;\u0026gt; name1; yp-\u0026gt;personArray[ret].name = name1; cout \u0026lt;\u0026lt; \"年龄：\" \u0026lt;\u0026lt; endl; int age1; cin \u0026gt;\u0026gt; age1; yp-\u0026gt;personArray[ret].age = age1; cout \u0026lt;\u0026lt; \"号码：\" \u0026lt;\u0026lt; endl; string number; cin \u0026gt;\u0026gt; number; yp-\u0026gt;personArray[ret].phone = number; cout \u0026lt;\u0026lt; \"修改完成！\" \u0026lt;\u0026lt; endl; } system(\"read -p 'Press Enter to continue...' var\"); } // 清空通讯录 void clearyp(yellowpage *yp) { yp-\u0026gt;size = 0; cout \u0026lt;\u0026lt; \"已清空！\" \u0026lt;\u0026lt; endl; system(\"read -p 'Press Enter to continue...' var\"); } // #define a 100 int main(int argc, char const *argv[]) { yellowpage yp; int select = 0; while (true) { showMenu(); cin \u0026gt;\u0026gt; select; switch (select) { case 1: addperson(\u0026amp;yp); break; case 2: printinfo(\u0026amp;yp); break; case 3: dropperson(\u0026amp;yp); break; case 4: selectperson(\u0026amp;yp); break; case 5: alterperson(\u0026amp;yp); break; case 6: clearyp(\u0026amp;yp); break; case 0: cout \u0026lt;\u0026lt; \"拜拜了您内\" \u0026lt;\u0026lt; endl; system(\"read -p 'Press Enter to continue...' var\"); return 0; break; default: break; } } return 0; } ","permalink":"https://pp-tech-blog.pages.dev/posts/yp/","summary":"/*time：2020年9月22日 auther：wpp */ #include \u003ciostream\u003e using namespace std; #include \u0026lt;str。","title":"通讯录管理系统"},{"content":"常量指针 int a = 10; int b = 10;\nconst int * p = \u0026a; //指针指向的值不可以改，指针的指向可以改\n*p = 20; //错误 p = \u0026b; //正确\n指针常量 int const p = \u0026a; //指针的指向不可以改，指针指向的值可以改 p = 20; //正确 p = \u0026b; //错误\nconst又修饰指针又修饰常量 const int const p = \u0026a; //指针的指向不可以改，指针指向的值也不可以改\n主要看const后面修饰的是指针还是常量，是指针则是常量指针，是常量则是指针常量 修饰指针：const int p则 p不能被修改 修饰常量： const p 则p不能被修改\n#include \u0026lt;iostream\u0026gt; using namespace std; #include \u0026lt;string\u0026gt; #include \u0026lt;math.h\u0026gt; struct student { int age; string name; /* data */ };\n// 将函数中的形参改为指针可以减少内存空间，不会复制新的副本出来 void printstudent(const student * s){ // s-\u0026gt;age = 33; //加入const后一旦有修改的操作就会报错 cout \u0026lt;\u0026lt; s-\u0026gt;name \u0026lt;\u0026lt; s-\u0026gt;age \u0026lt;\u0026lt; endl; }\nint main(int argc, char const *argv[]) { student stu = {23, \u0026ldquo;SPP\u0026rdquo;}; student * p = \u0026amp; stu; // printstudent(p);\nreturn 0; } ","permalink":"https://pp-tech-blog.pages.dev/posts/cpp_pointer/","summary":"常量指针 int a = 10; int b = 10; const int * p = \u0026amp;a; //指针指向的值不可以改，指针的指向可以改 *p = 20; //错误 p。","title":"cpp_指针"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n选择最合适的评价指标是一个关键的任务。而且，我遇到了两个重要的指标：除了MAE/MSE/RMSE，有R方和调整R方。这两者有什么区别？\n残差平方和 一个简单的回归图 黄点代表数据点，蓝线是我们预测的回归线。如你所见，我们的回归模型并不能完美地预测所有的数据点。\n那么我们如何利用这些数据来评估回归线的预测呢？我们可以从确定数据点的残差开始。 数据中某一点的残差是实际值与线性回归模型预测值之间的差值。 残差图告诉我们回归模型是否适合数据。残差的平方实际上是回归模型优化的目标函数。\n利用残差值，我们可以确定残差的平方和，也称为残差平方和或RSS。。 RSS值越低，模型预测值越好。或者我们可以这样说——如果回归线使RSS值最小化，那么回归线就是最佳拟合线。\n但这其中有一个缺陷——RSS是一个尺度变量统计。由于RSS是实际值和预测值的平方差之和，因此该值取决于目标变量的大小。\n例子： 假设你的目标变量是销售产品所产生的收入。残差取决于目标的大小。如果收入大小以“1百卢比”为单位计算的话（即目标可能是1、2、3等），那么我们可能会得到0.54左右的RSS（假设）。\n但是如果收入目标变量以“卢比”为单位（即目标值为100、200、300等），那么我们可能会得到一个更大的RSS，即5400。即使数据没有变化，RSS的值也会随着目标的大小而变化。这使得很难判断什么是好的RSS值。\n那么，我们能想出一个更好的尺度不变的统计量吗？这就是R方出现的地方。\nR方统计量 R方统计量是一种尺度不变的统计量，它给出了线性回归模型解释的目标变量的变化比例。 这可能看起来有点复杂，所以让我在这里把它分解。为了确定模型解释的目标变化比例，我们需要首先确定以下内容-\n平方和(TSS) 目标变量的总变化是实际值与其平均值之差的平方和。 TSS或总平方和给出了Y的总变化量。我们可以看到它与Y的方差非常相似。虽然方差是实际值和数据点之间差的平方和的平均值，TSS是平方和的总和。\n既然我们知道了目标变量的总变化量，我们如何确定模型解释的这种变化的比例？我们回到RSS。\n残差平方和(RSS) 正如我们前面讨论的，RSS给出了实际点到回归线距离的总平方。残差，我们可以说是回归线没有捕捉到的距离。 因此，RSS作为一个整体给了我们目标变量中没有被我们的模型解释的变化。 R方 现在，如果TSS给出Y的总变化量，RSS给出不被X解释的Y的变化量，那么TSS-RSS给出了Y的变化，并且这部分变化是由我们的模型解释的！我们可以简单地再除以TSS，得到由模型解释的Y中的变化比例。这是我们的R方统计量！\nR方=（TSS-RSS）/TSS = 解释变化/总变化 = 1–未解释的变化/总变化 因此，R方给出了目标变量的可变性程度，由模型或自变量解释。如果该值为0.7，则意味着自变量解释了目标变量中70%的变化。\nR方始终介于0和1之间。R方越高，说明模型解释的变化越多，反之亦然。\n如果RSS值很低，这意味着回归线非常接近实际点。这意味着自变量解释了目标变量的大部分变化。在这种情况下，我们会有一个非常高的R方值。 相反，如果RSS值非常高，则意味着回归线远离实际点。因此，自变量无法解释目标变量中的大部分变量。这会给我们一个很低的R方值。\n关于R方统计量的问题 R方统计并不完美。事实上，它有一个主要缺陷。不管我们在回归模型中添加多少变量，它的值永远不会减少。\n也就是说，即使我们在数据中添加冗余变量，R方的值也不会减少。它要么保持不变，要么随着新的自变量的增加而增加。\n这显然没有意义，因为有些自变量在确定目标变量时可能没有用处。调整R方处理了这个问题。\n调整R方统计量 调整R方考虑了用于预测目标变量的自变量数量。在这样做的时候，我们可以确定在模型中添加新的变量是否会增加模型的拟合度。\n让我们看看调整R方的公式，以便更好地理解它的工作原理。 n表示数据集中的数据点数量,k表示自变量的个数,R代表模型确定的R方值\n因此，如果R方在增加一个新的自变量时没有显著增加，那么调整R方值实际上会减少。\n另一方面，如果增加新的自变量，我们看到R方值显著增加，那么调整R方值也会增加。 添加随机独立变量无助于解释目标变量的变化。我们的R方值保持不变。因此，给我们一个错误的指示，这个变量可能有助于预测输出。然而，调整R方值下降，表明这个新变量实际上没有捕捉到目标变量的趋势。 显然，当回归模型中存在多个变量时，最好使用调整R方。这将使我们能够比较具有不同数量独立变量的模型。\n","permalink":"https://pp-tech-blog.pages.dev/posts/r_r2/","summary":"选择最合适的评价指标是一个关键的任务。而且，我遇到了两个重要的指标：除了MAE/MSE/RMSE，有R方和调整R方。这两者有什么区别？ 残差平方和 一个简单的回归图 黄点代。","title":"回归分析中R方和调整R方的区别"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n概念 平稳性 平稳性：要求经由样本时间序列所得到的拟合曲线 在未来的一段时间内仍能顺着现有的形态“惯性”地延续下去 平稳性要求序列的均值和方差不发生明显变化 严平稳与弱平稳 严平稳：分布不随时间的改变而改变 举例：白噪声（正态），无论怎么取，都是期望为0，方差为1\n弱平稳：期望与相关系数（依赖性）不变 未来某时刻的t的值就要依赖于它的过去信息，所以需要依赖性\n差分法：时间序列在t与t-1时刻的差值 一阶差分： dataframe['diff_1'] = dataframe['data'].diff(1) 二阶差分：对一阶差分做差分 dataframe['diff_2'] = dataframe['diff_1'].diff(1) ARIMA 自回归模型（AR） 描述当前值与历史值之间的关系，用变量自身的历史时间数据对自身进行预测\n自回归模型必须满足平稳性的要求\np阶自回归过程的公式定义：（p阶表当前值与前面多少个历史值有关系） $y_t$是当前值，$\\mu$是常数项 ， p是阶数，$\\gamma_i$是自相关西是，$\\epsilon_t$是误差\n限制：\n用自身数据预测 必须具有平稳性 必须具有自相关性，如果自相关系数小于0.5，则不宜采用 只适用于预测与自身前期相关的现象 移动平均模型（MA） 移动平均模型关注的是自回归模型中的误差项的累加 q阶自回归过程的公式定义： 移动平均法能有效的消除预测中的随机波动\n自回归移动平均模型（ARMA） 自回归与移动平均的结合 公式定义： ARIMA ARIMA(p,d,q)全称:差分自回归移动平均模型 autoregressive integrated moving average model p为自回归项，q为移动平均项数，d为时间序列成为平稳时所做的差分次数\n原理：将非平稳时间序列转化为平稳时间序列然后将因变量仅对他的滞后值以及随机误差项的现值和滞后值进行回归所建立的模型\n相关函数 自相关函数ACF(autocorrelation function) 有序的随机变量序列与其自身相比较 自相关函数反映了同意序列在不同时序的取值之间的相关性 公式： $p_k$的取值范围为[-1,1]\n偏自相关函数 PACF(patial autocorrelation function)\n对于一个平稳AR(p)模型，求出滞后k自相关系数p(k)实际上得到的并不是x(t)与x(t-k)之间单纯的相关关系 所以自相关系数p(k)里实际掺杂了其他变量对x(t)与x(t-k)的影响 剔除了中间k-1个随机变量x(t-1)、x(t-2)、~、x(t-k+1)的干扰之后x(t-k) 对x(t)影响的相关程度 ACF还包含了其他变量的影响 PACF是严格这两个变量之间的相关性 ARIMA模型的建立 ARIMA(p,d,q)阶数确定： AR(p)主要观察PACF 截尾：落在置信区间\np阶后截尾：比如第二阶就落在置信区间，则p取2 MA(q300200400)主要观察ACF 拖尾：始终有非零取值，不会在k大于某个常数后就恒等于零(或在0附近随机波动) 截尾：在大于某个常数k后快速趋于0为k阶截尾\nAR模型：自相关系数拖尾，偏自相关系数截尾； MA模型：自相关系数截尾，偏自相关函数拖尾； ARMA模型：自相关函数和偏自相关函数均拖尾。\nARIMA建模流程：\n将序列平稳（差分法确定d） p和q阶确定：ACF与PACF ARIMA(p,d,q) 模型选择 AIC\\BIC:选择更简单的模型\nAIC：赤池信息准则 AIC = 2k - 2ln(L) BIC：贝叶斯信息准则 BIC = kln(n) - 2ln(L) K：模型参数个数，n:样本数量 L：似然函数\n","permalink":"https://pp-tech-blog.pages.dev/posts/timeseries/","summary":"概念 平稳性 平稳性：要求经由样本时间序列所得到的拟合曲线 在未来的一段时间内仍能顺着现有的形态“惯性”地延续下去 平稳性要求序列的均值和方差不发生明显变化 严平稳与弱平稳。","title":"时间序列"},{"content":"记录正则表达式 元字符 符号 说明 . 匹配除换行符以外的任意字符 \\w 匹配字母，数字，或下划线 \\s 匹配空白字符，包括\\r,\\t,\\n \\d 匹配数字 \\b 匹配单词的开始或者结束 ^ 匹配字符串的开始 $ 匹配字符串的结束 字符转义 如果要查找元字符本身，如 . ，是无法直接指定的，需要使用 \\ 进行转义，例如 www.baidu\\.com 匹配www.baidu.com。\n重复 符号 说明 * 重复零次或多次 + 重复一次或多次 ? 重复零次或一次 {n} 重复n次 {n,} 至少重复n次 {n,m} 重复n到m次 字符类 匹配没有预定义元字符的字符集合的方式，如元音字母a,e,i,o,u。只需要在方括号里列出它们就可以了，像 [aeiou] 匹配任何一个元音字母， [.?!] 匹配标点符号。也可以轻松指定一个字符范围，像 [0-9] 代表的含义与 \\d 完全一致， [a-z0-9A-Z] 也完全等同于 \\w (如果只考虑英文的话)。\n一个更复杂的表达式： \\(?0\\d{2}[)-]?\\d{8} 。\n上面这个表达式可以匹配几种格式的电话号码，(010)12345678，或者010-12345678，或者01012345678等。\n分枝条件 正则表达式里的分枝条件指的是有几种规则，如果满足其中任意一种规则都应当成匹配，具体方法是用 | 把不同的规则分隔开。\n例子：0\\d{2}-\\d{8}|0\\d{3}-\\d{7}这个表达式能匹配两种以连字号分隔的电话号码，一种是3位区号加8位本地号，另一种是4位区号加7位本地号。\n\\d{5}-\\d{4}|\\d{5}这个例子用于说明使用分枝条件时，要注意各个条件的顺序。匹配分枝条件时，将会从左至右测试每个条件，如果满足了其中一个分枝条件，就不会去管其他的条件了。\n分组 重复单个字符直接在字符后面加上限定符就可以了，若想重复多个字符，则需要用小括号来指定子表达式（也叫分组）。例子：(\\d{1,3}.){3}\\d{1,3}\n反义 符号 说明 \\W 匹配非字母，数字，下划线字符 \\S 匹配非空白字符 \\D 匹配非数字字符 \\B 匹配非单词开头或结尾的位置 [^X] 匹配除了X以外的字符 [^aeiou] 匹配除了aeiou这几个字母以外的字符 零宽断言 断言用来声明一个应该为真的事实，正则表达式中只有当断言为真时才会继续进行匹配。\n(?=exp) 也叫 零宽度正预测先行断言 ,它断言自身出现的位置后面能够匹配表达式exp。例子：\n\\b\\w+(?=ing\\b)，匹配以ing结尾的单词的前部分，如查找I'm singing while you're dancing.时，它会匹配sing和danc。\n(?\u0026lt;=exp) 也叫 零宽度正回顾后发断言 ,它断言自身出现位置的前面能匹配表达式exp。例子：\n(?\u0026lt;=\\bre)\\w+\\b，匹配以re开头的单词的后部分，如查找reading a book.时，它会匹配ading。\n负向零宽断言 (?!exp) 零宽度负预测先行断言 ,断言此位置后面不能匹配表达式exp。例如：\\d{3}(?!\\d)匹配三维数字，且这三位数字后面不能是数字。\n(?\u0026lt;!exp) 零宽度负回顾后发断言 ,断言此位置前面不能匹配表达式exp。例如: (?\u0026lt;![a-z])\\d{7}匹配前面不是小写字母的7位数字。\n注释 小括号的另一种用途是通过语法 (?#comment) 来包含注释。例如： 2[0-4]\\d(?#200-249)|25[0-5](?#250-255)|[01]?\\d\\d?(?#0-199)。\n贪婪与懒惰 贪婪匹配尽可能多的字符，懒惰匹配尽可能少的字符。\n以aabab为例，贪婪：a.b,匹配结果为aabab,懒惰：a.\\?b,匹配结果为aab和ab（为什么第一个匹配到的是aab[1-3]字符而不是ab[2-3]字符 ??? 简单的说，因为正则表达式有另外一条规则，其优先级高于贪婪/懒惰规则：最先开始的匹配拥有最高的优先权）。\n符号 说明 *? 重复0次或多次，但尽可能少重复 +? 重复1次或多次，但尽可能少重复 ?? 重复0次或1次，但尽可能少重复 {n,m}? 重复n到m次，但尽可能少重复 {n,}? 重复n此以上，但尽可能少重复 ","permalink":"https://pp-tech-blog.pages.dev/posts/re/","summary":"记录正则表达式 元字符 符号 说明 . 匹配除换行符以外的任意字符 \\w 匹配字母，数字，或下划线 \\s 匹配空白字符，包括\\r,\\t,\\n \\d 匹配数字 \\b 匹配单词。","title":"正则表达式"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n概述 Fama-French三因子模型概述\nFama和French 1992年对美国股票市场决定不同股票回报率差异的因素的研究发现，股票的市场的beta值不能解释不同股票回报率的差异，而上市公司的市值、账面市值比、市盈率可以解释股票回报率的差异。Fama and French认为，上述超额收益是对CAPM 中β未能反映的风险因素的补偿。”\n假设条件 Fama-French三因子模型的假设条件[1] 1、理论假设\n在探讨Fama—French三因子模型的应用时，是以“有限理性”理论假设为基础。并在此基础上得出若干基本假定：\n(1)存在着大量投资者；\n(2)所有投资者都在同一证券持有期计划自己的投资资产组合；\n(3)投资者投资范围仅限于公开金融市场上交易的资产；\n(4)不存在证券交易费用(佣金和服务费用等)及税赋；\n(5)投资者们对于证券回报率的均值、方差及协方差具有相同的期望值；\n(6)所有投资者对证券的评价和经济局势的看法都一致。\n2、统计假设\n从模型的表达式可以看出，FF模型属于多元回归模型。其基本假设为：\n(1)(Rm − Rf)、SMB、HML与随机误差项u不相关；\n(2)零均值假定：$E(\\varepsilon_i)=0$；\n(3)同方差假定，即$\\varepsilon$的方差为一常量：$Var(\\varepsilon_i)=S^2$；\n(4)无自相关假定：$cov(\\varepsilon_i,\\varepsilon_j)=0,i\\ne j$；\n(5)解释变量之间不存在线性相关关系。即两个解释变量之间无确切的线性关系；\n(6)假定随机误差项$\\varepsilon$服从均值为零，方差为$S^2$正态分布，即$\\varepsilon_{i}\\sim N(0,S^2)$。\n表达式 Fama和French 1993年指出可以建立一个三因子模型来解释股票回报率。模型认为，一个投资组合(包括单个股票)的超额回报率可由它对三个因子的暴露来解释，这三个因子是：市场资产组合(Rm − Rf)、市值因子(SMB)、账面市值比因子(HML)。后来，Fama和French发现在上述风险之外，还有盈利水平风险、投资水平风险也能带来个股的超额收益，并于2013年提出了五因子模型。 这个多因子均衡定价模型可以表示为： Fama-French三因子模型的表达式 其中$R_{ft}$表示时间t的无风险收益率,\n$R_{mt}$表示时间t的市场收益率,\n$R_{it}$表示资产i在时间t的收益率,\nE(Rmt) − Rft是市场相对无风险投资的期望超额收益率,\n$SMB_t$为小市值公司相对大市值公司股票的期望超额收益率, $HMI_t$为高B/M公司股票比起低B/M的公司股票的期望超额收益率。 βi、si和hi分别是三个因子的系数，回归模型表示如下：\na 即观测值，线性回归的常数项。 εi 是回归残差项。 三因子模型的本质就是把CAPM 中的未被解释的超额收益分解掉，将其分解成SMB、BM 、RM 和其他未能解释的因素(a)，\n计算方法： E(SMB) 的计算方式如下： 把市场里面的所有股票按市值排序，随后分成两份：第一份 是大市值股票 （市值前20% 的股票），第二份是小市值股票 （市值后 20% 的股票）。记大市 值股票的平均期望收益率为 E(rS) ，小市值股票的期望收益率为 E(rB) 。那么E(SMB)=E(rS) −E(rB) 。E(HML) 的定义也类似。 假设三因子模型是正确的，市场风险、市值风险、账面市值比这三类风险能很好地解释 个股的超额收益，那么a 的长期均值应该是 0 。如果短期内对于某个时期的股票，回归得到a \u0026lt; 0，说明这段时间个股收益率偏低。根据有效市场假设，任何非理性的价格最终都会回归 理性，这些短期内收益率偏低的个股，最终都会涨回去。 但是，我们应该看到，三因子模型并不代表资本定价模型的完结，在最近的研究发现，三因子模型中还有很多未被解释的部分，如短期反转、中期动量、波动、偏度、赌博等因素。\n策略实现 策略每隔1个月定时触发,根据Fama-French三因子模型对每只股票进行回归，得到其alpha值。 假设Fama-French三因子模型可以完全解释市场，则alpha为负表明市场低估该股，因此应该买入。 策略思路： 计算市场收益率、个股的账面市值比和市值,并对后两个进行了分类, 根据分类得到的组合分别计算其市值加权收益率、SMB和HML. 对各个股票进行回归(假设无风险收益率等于0)得到alpha值. 选取alpha值小于0并为最小的10只股票进入标的池 平掉不在标的池的股票并等权买入在标的池的股票 回测数据:SHSE.000300的成份股\n1.账面市值比(BM)=股东权益/公司市值.2.股东权益(净资产)=资产总额-负债总额 (每股净资产x流通股数)3.公司市值=流通股数x每股股价4.账面市值比(BM)=股东权益/公司市值=(每股净资产x流通股数)/(流通股数x每股股价)=每股净资产/每股股价=B/P=市净率的倒数\n","permalink":"https://pp-tech-blog.pages.dev/posts/fama-french/","summary":"概述 Fama-French三因子模型概述 Fama和French 1992年对美国股票市场决定不同股票回报率差异的因素的研究发现，股票的市场的beta值不能解释不同股票回。","title":"Fama-French三因子模型"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\nCAPM模型 一 假设条件 资本资产定价模型（CAPM）是现代金融学的奠基石，它以马柯威茨证券组合理论为基础，研究如果投资者都按照分散化的理念去投资，最终证券市场达到均衡时，价格和收益率如何决定的问题。\n该模型对资产风险与其期望收益率之间的关系给出了精确的预测。这一关系发挥着两个重要作用：\n它为评估各项资产提供了一个基准收益率 该模型帮助我们对还没有上市交易资产的期望收益率做出合理估计\n1. 资本资产定价模型概述 资本字段定价模型任务只有证券或证券组合的系统性风险才能获得收益补偿，其非系统性风险将得不到收益补偿。非系统性风险可以被投资者进行充分分散化投资消除。\n2. 资本资产定价模型的假设条件 投资者都依据期望收益率评价证券组合的收益水平，依据方差（或标准差）评价证券组合的风险水平，并按照投资者共同偏好规则选择最优证券组合。 投资者对证券的收益、风险及证券间的关联性具有完全相同的预期。 资本市场没有摩擦。（不考虑交易成本和税收、信息自由流动、交易单位无限可分，只有一个无风险借贷利率，在借贷和卖空上没有限制。） 以上第一、二项是对投资者的规范，第三项是对市场的简化。 二 资本市场线方程 在CAPM模型假设下，当市场达到均衡时，市场组合M成为一个有效组合；所有有效组合都可视为无风险证券F与市场组合M的在组合。\n资本市场线揭示了有效组合的收益和风险之间的均衡关系，这种关系可以用资本市场线方程来描述：\n式中：\n$R_m$是市场投资组合的收益率，也就是整个市场所有风险资产的收益情况。它的作用是作为一个基准，评估目标投资组合相对于基准的提升量。在实际应用中，我们常用大盘指数来作为Rm。Rm代表我们的投资组合的收益率，Rf代表无风险资产收益率，比如国债等固收资产。 $\\beta_{qm}$是投资组合q的Beta值，这里的β与我们常提到的β是一回事，|β|大于1代表我们的投资组合比大盘的波动更剧烈，|β|小于1代表我们的投资组合的波动小于大盘，如果β为负，则说明我们的投资组合与大盘的波动方向相反。当然，我们的投资组合也可以是个股 $E(Rq)−R_f$是我们的风险投资组合q比无风险资产高出的期望收益率，这部分被我们称作风险溢酬。这部分额外的期望收益率是由于我们的投资组合q的风险与大盘不同决定的。 这个公式的含义是它将资产的收益分解为无风险收益和和风险收益，后面一坨就是风险收益。capm认为风险收益的唯一来源是市场风险。\n为什么要加上一个无风险回报率？ 无风险回报率可以简单理解为最少得赚这些钱，就是短期国债的利率。任何股票投资应该要赚的比国债多，不然就没必要承担不确定性的风险了。\n市场风险溢价为正是投资股票的前提 E(rm)−rf就是投资股票平均收益水平减去无风险利率还剩下的利润，只有这个差值大于0，还有必要承担风险，如果还小于零，就是说投资股票还不如买国债，国债可是近乎稳赚不赔的。\n系统性风险 好了，我们是为了E(rm)−rf这个收益率才投资股票的，可以理解为大盘收益。那某一个具体的股票收益怎么计算呢？就是这个β值确定。 β衡量的是个别投资工具针对整个市场的波动情况，如果β\u0026gt;1说明该股波动大于市场平均水平。 在金融学里衡量一只股票的风险就是利用波动性。β就是用来衡量股票风险的指数。\nATP 套利定价理论（arbitrage pricing theory , ATP） 套利定价理论是将因素模型与无套利条件相结合从而得到期望收益和风险之间的关系。史蒂芬·罗斯在1976年提出的套利定价理论（arbitrage pricing theory , ATP），它基于三个基本假设：因素模型能描述证券收益；市场上有足够的证券来分散风险；完善的证券市场不允许任何套利机会存在。我们从这三个假设出发来解释ATP：\n1、因素模型能描述证券收益假设一个证券收益率发生了变化，股价发生了变动，我们认为股价代表着公司的经营状况，因此造成资产收益不确定性（不论是涨还是跌）有两个源头：公共因素（系统性）的和公司特有因素（非系统性）。比如说你开了一家果园，那么今年的经营情况可以归结于气候的好坏（公共因素）和你个人经营的好坏（公司特有因素）。如果系统性的因素只有一个，那么我们可以用单因素模型表示证券的收益率： 假设你们家的果园发行了股票，投资者对果园以及以往的气候情况进行调查后，对股票的收益和气候情况都是有一个预期的，用E(ri)来表示股票的期望收益，但是你们家今年的实际收益有不确定性，比如说今年的气候比大家根据历史数据估计的好很多，那么用$F$表示公共因素偏离其期望值的差，在这里$F$就是正的，$\\beta_i$表示敏感程度，越大你家果园收成受气候影响越大，然后$e_i$表示公司特有的扰动项，比如说你今年摘果子摔断了腿，然后雇佣了隔壁老王，结果老王摘果子比你还快，那么这个事情对收成的影响可以看做是你家特有的。最后，实际的收益就可以用期望收益+系统因素+非系统因素来解释啦。\n随着你家果园越做越好，果子不仅在国内销售，还有出口，那么汇率也成了影响你家收益的因素之一，有两个系统性因素同时影响你家的收益。如果系统性因素不止一个，单因素模型就成为了多因素模型。 这里分别用$F_1$和$F_2$来表示气候和汇率偏离其期望值的离差。\n如果认为系统性风险就是市场风险，那么APT模型就会退化为单因素的CAPM模型。APT可以视为多因子版的CAPM。\n","permalink":"https://pp-tech-blog.pages.dev/posts/capm_apt/","summary":"CAPM模型 一 假设条件 资本资产定价模型（CAPM）是现代金融学的奠基石，它以马柯威茨证券组合理论为基础，研究如果投资者都按照分散化的理念去投资，最终证券市场达到均衡时。","title":"CAPM模型与APT模型"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n价量新因子测试 研究目的： 本文参考海通证券冯佳睿、袁林青撰写的《选股因子系列研究(十八)——价格形态选股因子》，根据研报分析，主要测试了开盘冲高、盘低回升以及均价偏离这三种价格类因子。基于该文章，本文对文章中提到的三种价格类因子进行因子有效性分析，从而实现对股票未来收益的预测，为 alpha 因子的挖掘提供了一定思路。\n研究内容： （1）构建 Neg_pos_ID 因子，然后根据单因子有效性分析的方法，对该因子分别进行因子收益率显著性分析、因子 IC 分析以及分层组合回测分析； （2）构建 CO 因子，然后进行单因子有效性分析，紧接着与市值、反转、换手率、波动率这四个因子进行组合分析； （3）对这两个因子进行深入分析，增加了 6 因子(市值、反转、换手、波动、价值、营业利润同比增长率)，分析因子对组合收益的贡献及其预测效果； （4）分析因子敏感性，分别从成交额替代换手率以及分析不同构建期的情况，对 CO 因子进行深入分析。\n研究结论： （1）Neg_pos_ID 在一定程度上是一种价格动量因子，且通过对该因子进行有效性分析来看，该因子有效性较高，有较好的选股效果； （2）CO 因子的选股效果并非线性，然后通过对该因子与市值、反转、换手、 波动、价值等四个因子的组合特征来看，CO 因子与市值负相关， 与反转因子正相关，与换手率因子正相关，与波动率因子正相关，然后分别对这四个因子进行控制，与原始 CO 因子选股相比，前几个组合的月均收益得到了明显提高。 （3）进一步对这两个因子进行分析，首先对 6 因子(市值、反转、换手、 波动、价值、营业利润同比增长率)与 Neg_pos_ID 以及 CO 分解得到的 CO_pos 和 CO_pos 因子进行横截面回归，得知新因子的增加会提高模型的预测能力，换言之，新因子包含了有用的额外信息。 （4）当以成交额代替换手率时，CO 因子的选股效果得到提升；当选择不同的构建期进行分析时，构建期较短时，选股效果更佳。\n1 Neg_pos_ID 因子 根据研报内容，提到由 Da，Gurun 和 Warachka 在 2014 年发表的文章《Frog in the pan:Continuous information and momentum》中产生的的因子 PosID 和 NegID 。并在考虑到这两个因子在 A 股市场应用效果不好的情况，对因子进行改进，将上涨和下跌样本同等对待，从而产生了因子 Neg_pos_ID 。 在每个月最后一个交易日，获取过去一年（本文取 250 个交易日）的涨跌幅数据，计算其中收益为正的天数，记为 %pos ，计算其中收益为负的天数，记为 %neg ，然后根据这两个数据，计算新的因子数据，计算方式如下所示：\nNeg_pos_ID=%neg−%pos\n1.1 因子数据采集 本文设置时间区间为 2010 年至 2017 年，样本范围是全市场所有可交易股票去除 ST 股、上市不足 3 月新股。\nfrom jqdata import * import datetime import pandas as pd import numpy as np from six import StringIO import warnings import time import pickle from jqfactor import winsorize_med from jqfactor import neutralize from jqfactor import standardlize import statsmodels.api as sm from jqfactor import get_factor_values warnings.filterwarnings(\"ignore\") #获取指定周期的日期列表 'W、M、Q' def get_period_date(peroid,start_date, end_date): #设定转换周期period_type 转换为周是'W',月'M',季度线'Q',五分钟'5min',12天'12D' stock_data = get_price('000001.XSHE',start_date,end_date,'daily',fields=['close']) #记录每个周期中最后一个交易日 stock_data['date']=stock_data.index #进行转换，周线的每个变量都等于那一周中最后一个交易日的变量值 period_stock_data=stock_data.resample(peroid,how='last') date=period_stock_data.index pydate_array = date.to_pydatetime() date_only_array = np.vectorize(lambda s: s.strftime('%Y-%m-%d'))(pydate_array ) date_only_series = pd.Series(date_only_array) start_date = datetime.datetime.strptime(start_date, \"%Y-%m-%d\") start_date=start_date-datetime.timedelta(days=1) start_date = start_date.strftime(\"%Y-%m-%d\") date_list=date_only_series.values.tolist() date_list.insert(0,start_date) return date_list #去除上市距beginDate不足 3 个月的股票 def delect_stop(stocks,beginDate,n=30*3): stockList = [] beginDate = datetime.datetime.strptime(beginDate, \"%Y-%m-%d\") for stock in stocks: start_date = get_security_info(stock).start_date if start_date \u0026lt; (beginDate-datetime.timedelta(days = n)).date(): stockList.append(stock) return stockList #获取股票池 def get_stock_A(begin_date): begin_date = str(begin_date) stockList = get_index_stocks(\u0026lsquo;000002.XSHG\u0026rsquo;,begin_date)+get_index_stocks(\u0026lsquo;399107.XSHE\u0026rsquo;,begin_date) #剔除ST股 st_data = get_extras(\u0026lsquo;is_st\u0026rsquo;, stockList, count = 1, end_date=begin_date) stockList = [stock for stock in stockList if not st_data[stock][0]] #剔除停牌、新股及退市股票 stockList = delect_stop(stockList, begin_date) return stockList\nstart = time.clock() begin_date = '2010-01-01' end_date = '2017-01-01' dateList = get_period_date('M', begin_date, end_date) factorData = {} for date in dateList: stockList = get_stock_A(date) df_date = get_price(stockList, count = 251,end_date=date, frequency = '1d',fields = ['close'])['close'] #pct_change表示当前元素与先前元素的相差百分比，当然指定periods=n,表示当前元素与先前n个元素的相差百分比。默认为1 df_date = df_date.pct_change().iloc[1:] #每天相对于前一天的涨跌幅 temp = df_date[df_date\u0026lt;0] #收益为负的天数 Neg_Pos_ID = temp.count() - (250 - temp.count()) #收益为负的天数减去正的天数 Neg_Pos_ID = standardlize(Neg_Pos_ID, inf2nan=True, axis=0) factorData[date] = pd.DataFrame(Neg_Pos_ID, columns = ['Neg_Pos_ID']) elapsed = (time.clock() - start) print(\"Time used:\",elapsed) print(factorData) ('Time used:', 319.861577)\n1.2 因子有效性检验 1.2.1 因子收益率有效性检验 主要通过 T 检验分析，根据 APT 模型，对历史数据进行进行多元线性回归，从而得到需要分析的因子收益率的 t 值，然后进行以下两个方面的分析： （1）t 值绝对值序列的均值: 之所以要取绝对值，是因为只要 t 值显著不等于 0 即可以认为在当期，因子和收益率存在明显的相关性。但是这种相关性有的时候为正，有的时候为负，如果不取绝对值，则很多正负抵消，会低估因子的有效性; （2）t 值绝对值序列大于2的比例: 检验 |t| \u0026gt; 2 的比例主要是为了保证 |t| 平均值的稳定性， 避免出现少数数值特别大的样本值拉高均值。\ndef factor_t_test(factorData, Field, begin_date, end_date): dateList = get_period_date('M', begin_date, end_date) WLS_params = {} WLS_t_test = {} for date in dateList[:-1]: R_T = pd.DataFrame() #取股票池 stockList = list(factorData[date].index) #获取横截面收益率 df_close = get_price(stockList, date, dateList[dateList.index(date)+1], 'daily', ['close']) if df_close.empty: continue df_pchg=df_close['close'].iloc[-1,:]/df_close['close'].iloc[0,:]-1 R_T['pchg'] = df_pchg #获取因子数据 factor_data = factorData[date][Field] R_T['factor'] = factor_data R_T = R_T.dropna() X = R_T['factor'] y = R_T['pchg'] # WLS回归 wls = sm.OLS(y, X) result = wls.fit() WLS_params[date] = result.params[-1] WLS_t_test[date] = result.tvalues[-1] t_test = pd.Series(WLS_t_test).dropna() print 't值序列绝对值平均值: ',np.sum(np.abs(t_test.values))/len(t_test) n = [x for x in t_test.values if np.abs(x)\u0026gt;2] print 't值序列绝对值大于2的占比——判断因子的显著性是否稳定',len(n)/float(len(t_test)) print 't值序列均值的绝对值除以t值序列的标准差: ',np.abs(t_test.mean())/t_test.std() return WLS_t_test WLS_t_test = factor_t_test(factorData, 'Neg_pos_ID', begin_date, end_date) 输出： t值序列绝对值平均值: 3.68306186203 t值序列绝对值大于2的占比——判断因子的显著性是否稳定 0.666666666667 t值序列均值的绝对值除以t值序列的标准差: 0.379593650011\n根据上面结果分析，t 值绝对值序列的均值为 3.68，符合大于 2 的特征，且 t 值绝对值序列大于 2 的比例为 66.67%，根据因子收益率显著性检验的标准，该因子为有效因子。\n1.2.2 因子 IC 分析 因子 k 的 IC 值一般是指个股第 T 期在因子 k 上的暴露度与 T + 1 期的收益率的相关系数。当得到因子 IC 值序列后，根据以下分析方法进行计算: （1）IC 值序列的均值及绝对值均值: 判断因子有效性; （2）IC 值序列的标准差：判断因子稳定性; （3）IC 值系列的均值与标准差比值（IR）：分析分析有效性 （4）IC 值序列大于零（或小于零）的占比：判断因子效果的一致性。\nimport scipy.stats as st def factor_IC_analysis(factorData, Field, begin_date, end_date, rule = 'normal'): dateList = get_period_date('M', begin_date, end_date) IC = {} R_T = pd.DataFrame() for date in dateList[:-1]: stockList = list(factorData[date].index) df_close = get_price(stockList, date, dateList[dateList.index(date)+1], 'daily', ['close']) if df_close.empty: continue df_pchg=df_close['close'].iloc[-1,:]/df_close['close'].iloc[0,:]-1 R_T['pchg']=df_pchg factor_data = factorData[date][Field] factor_data = standardlize(factor_data, inf2nan = True,axis = 0) #inf:infinite无限大 R_T['factor'] = factor_data R_T = R_T.dropna() if rule == 'normal': # IC[date] = st.pearsonr(R_T.pchg, R_T['factor'])[0] IC[date] = st.pearsonr(R_T['pchg'], R_T['factor'])[0] elif rule == 'rank': # IC[date] = st.pearsonr(R_T.pchg.rank(), R_T['factor'].rank())[0] IC[date] = st.pearsonr(R_T['pchg'].rank(), R_T['factor'].rank())[0] IC = pd.Series(IC).dropna() print('IC值序列均值大小', IC.mean()) print('IC值序列绝对值的均值大小', np.mean(np.abs(IC))) print('IC值序列的标准差', IC.std()) print('IR比率', IC.mean()/IC.std()) n = [x for x in IC.values if x\u0026gt;0] print(\"IC值序列大于零的比例\", len(n)/float(len(IC))) factor_IC_analysis(factorData, 'Neg_Pos_ID', begin_date, end_date) 输出： IC值序列均值大小 -0.043363427016258914 IC值序列绝对值的均值大小 0.07778510125754556 IC值序列的标准差 0.08850819835610833 IR比率 -0.4899368399951869 IC值序列大于零的比例 0.36904761904761907\n上表给出每月因子与次月收益的相关系数 IC，IC均值为-0.043，IR比率为-0.49.在2010到2017年间，IC值小于0的占比为0.63，总体倾向为负，表明当月因子值越小，次月收益越高、\n1.2.3 分层组合回测分析 为了进一步分析因子有效性，本文对该因子进行分层回测分析，策略步骤如下所示： （1）在每个月最后一个交易日，统计全 A 股因子的值； （2）根据因子值按照从小到大的顺序排序，并将其等分为 5 组 （3）每个调仓日对每组股票池进行调仓交易，从而获得 5 组股票组合的月均收益 注：设置每次交易手续费为千 2\ndef GetPchg(i, Field): pchg = [] cost = 0.002 for date in dateList[:-1]: tempData = factorData[date].copy() tempData = tempData.sort_values(Field) stockList = list(tempData.index) stocks = stockList[int(len(stockList)*(i-1)/5):int(len(stockList)*i/5)] df_close = get_price(stocks, date, dateList[dateList.index(date)+1], 'daily', ['close'])['close'] pchg.append(np.mean(df_close.iloc[-1] / df_close.iloc[0] -1) - cost) return pchg tempPchg = [] for i in range(1,6): tempPchg.append(np.mean(GetPchg(i, 'Neg_Pos_ID'))) fig = plt.figure(figsize=(20,8)) ax = fig.add_subplot(111) plt.bar(range(len(tempPchg)), tempPchg, 0.3) # 添加图例 plt.legend() plt.show() 2 CO 因子 根据研报内容，由 Suk,Sonya 和 Sang 在 2014 年发表的文章 《Overreaction and Stock Return Predictability》中提出了持续过度反应度量指标 CO。研报分析由于 A 股和美国股市的投资者结构存在本质区别，因此按照日度形式计算该指标。计算方式如下所示：在每个月最后一个交易日，分析过去一个月的个股的涨跌情况和换手率情况，然后根据以下公式实现 CO 指标的计算。 用上涨时的交易活跃度（换手率、或成交额）与下跌时交易活跃度之差表示\n2.1 因子数据采集 本文设置时间区间为 2010 年至 2017 年，样本范围是全市场所有可交易股票去除 ST 股、上市不足 3 月新股。\nstart = time.clock() begin_date = '2010-01-01' end_date = '2017-01-01' # 市值 最近一个月的股价增长率 20日平均换手率 20日夏普比率（每承受一单位的总风险，会产生多少超额的报酬） Fields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\"] dateList = get_period_date('M',begin_date, end_date) for date in dateList: stockList = get_stock_A(date) #取前一个月的数据 df_close = get_price(stockList, count = 21, end_date = date, frequency='1d', fields=['close'])['close'] df_pchg = df_close.pct_change().iloc[1:] #df_pchg.index为日期（date前一个月的开市日） temp_turnover = pd.DataFrame(index = df_pchg.index, columns = stockList) for day in df_pchg.index: #get_fundamentals:查询财务数据 code股票代码 turnover_ratio换手率 #get_fundamentals返回一个dataframe，columns索引为要查询的字段 df_turnover = get_fundamentals(query(valuation.code, \\ valuation.turnover_ratio).filter\\ (valuation.code.in_(stockList)),\\ date = day) #设index为code，股票代码 df_turnover = df_turnover.set_index(['code']) #将df_turnover的turnover_ratio（换手率）列复制到temp_turnover的day行 temp_turnover.loc[day] = df_turnover['turnover_ratio'] tempSum = 0 for i in range(len(temp_turnover)): #len(temp_turnover)为行数（前一个月开市天数） tempSum += i+1 for i in range(len(temp_turnover)): #乘以Wi（CO公式） temp_turnover.iloc[i] = temp_turnover.iloc[i] * float(i+1) / tempSum CO = temp_turnover[df_pchg\u0026gt;0].sum() + (-1*temp_turnover[df_pchg\u0026lt;0]).sum() #CO公式 #获取因子值 返回一个 dict： key 是因子名称， value 是 pandas.dataframe。 #dataframe 的 index 是日期， column 是股票代码 temp = get_factor_values(stockList, Fields, end_date = date, count = 1) factorData[date][\"CO\"] = CO factorData[date][\"market_cap\"] = temp[\"market_cap\"].T factorData[date][\"Price1M\"] = temp[\"Price1M\"].T factorData[date][\"VOL20\"] = temp[\"VOL20\"].T factorData[date][\"sharpe_ratio_20\"] = temp[\"sharpe_ratio_20\"].T for Field in Fields: #中性化函数:将序列与 how 中输入的相关变量做回归取残差，去除序列与相关变量的相关性 factorData[date][Field+\"_neu\"] = neutralize(factorData[date][\"CO\"], how=[Field], date=date, axis=0) factorData[date] = standardlize(factorData[date], inf2nan=True, axis=0) elapsed = (time.clock() - start) print(\"Time used:\",elapsed) print(factorData) 2.2 因子有效性检验 2.2.1 因子收益率显著性检验 #见1.2.1 WLS_t_test = factor_t_test(factorData, 'CO', begin_date, end_date) 输出： t值序列绝对值平均值: 3.1390689096177615 t值序列绝对值大于2的占比——判断因子的显著性是否稳定 0.5952380952380952 t值序列均值的绝对值除以t值序列的标准差: 0.5315264180334107\nt值绝对值平均值为3.14， 符合大于2的特征，且t值序列绝对值大于2的占比为59.5%，根据因子收益率显著性检验的标准，该因子为有效因子\n2.2.2 因子 IC 分析 #见1.2.2 factor_IC_analysis(factorData, 'CO', begin_date, end_date) 输出： IC值序列均值大小 -0.04150199179395132 IC值序列绝对值的均值大小 0.07374672739993604 IC值序列的标准差 0.08452195586246326 IR比率 -0.4910202487680792 IC值序列大于零的比例 0.27380952380952384\nIC值序列均值大小为-0.041，IR比率达到了-0.49.IC值小于零的占比为0.73.总体倾向为负，表明当月因子值越小，次月收益越高。\n2.2.3 分层组合回测分析 为了进一步分析因子有效性，本文对该因子进行分层回测分析，策略步骤如下所示： （1）在每个月最后一个交易日，统计全 A 股因子的值； （2）根据因子值按照从小到大的顺序排序，并将其等分为 5 组 （3）每个调仓日对每组股票池进行调仓交易，从而获得 5 组股票组合的月均收益 注：设置每次交易手续费为千 2\ntempPchg = [] for i in range(1, 6): #GetPchg见1.2.3 返回第i组的平均收益率 tempPchg.append(np.mean(GetPchg(i, 'CO'))) fig = plt.figure(figsize= (20,8)) ax = fig.add_subplot(111) plt.bar(range(len(tempPchg)), tempPchg, 0.3) plt.legend() plt.show() 从上图来看，5个组合的收益与CO指标值呈现出较为明显的负相关性，但在前两组间负单调性并不明显。从组合1和组合5来看，组合1 的越军收益显然高于5，但是考虑单调性较差，因此本文对该因子进行更深入的分析。\n2.3因子组合特征 本文对该因子进行更深入的分析，在每个月最后一个交易日，分别采集个股的市值、反转、换手率、波动这四个因子，根据原始因子 CO 的值分析该因子与这四个因子的相关性，具体分析如下。\nFields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\"] def GetCorr(i): resultCorr = pd.DataFrame() for date in dateList: tempData = factorData[date].copy() tempData.sort_values('CO') #升序 temp = tempData.iloc[int(len(tempData)*(i-1)/5):int(len(tempData)*(i)/5),:] tempCorr = temp.corr()[\"CO\"] #corr()返回dataframe，columns为各个因子，index也为这些因子，value为横轴与纵轴因子的相关系数 resultCorr[date] = tempCorr[Fields] #co与各因子的相关系数代表相关性 return resultCorr #index为除co的因子，columns为date，values为co与各因子的相关系数 # GetCorr(1) result = pd.DataFrame() for i in range(1,6): result[i] = GetCorr(i).mean(axis =1) #行平均值（所有日期的系数的平均值） result 由上可以看出，CO 因子与其余因子呈现出一定的相关性，具体如下所示： （1）CO 因子与市值负相关，且 CO 值较小的组合对应股票市值较大，因此，如果对市值因子进行控制，那么能够实现对 CO 因子的优化； （2）CO 因子与反转因子正相关，且相关性较高，CO 最小的股票前期跌幅最大，因此，如果对反转因子进行控制，那么能够实现对 CO 因子的优化； （3）CO 因子与换手率因子正相关，因此，如果对换手率因子进行控制，那么能够实现对 CO 因子的优化； （4）CO 因子与波动率因子正相关，CO 最小的股票波动率最大，因此，如果对反转因子进行控制，那么能够实现对 CO 因子的优化。\n实现了上述分析后，接下来对 CO 因子进行这四种因子的控制后的情况进行分析。\nFields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\"] resultPchg = pd.DataFrame(index = range(1,6), columns = Fields) for i in range(1,6): for Field in Fields: resultPchg.loc[i, Field] = np.mean(GetPchg(i, Field)) resultPchg 上表展示分别对这四种因子进行控制后，进行分层回测的月均收益表，基本上符合随着 CO 增加，组合收益率呈现先增加后减小的态势。但是与原始 CO 因子选股相比，前几个组合的月均收益得到了明显提高。\n3因子深入研究 3.1横截面收益率回归 前文内容对因子 Neg_pos_ID 和因子 CO 进行了分析，根据研报介绍，参考因子 Neg_pos_ID 的构建方式，对因子 CO 进行分解为 CO_pos 和 CO_neg。当 CO \u0026gt; 0 时则将该股票的 CO_pos 设为 CO，CO_neg 设为 0；当 CO \u0026lt; 0 时将该股票的 CO_neg 设为 CO，CO_pos 设为 0。 接下来根据研报内容，进行横截面收益率回归分析，具体回归方程如下所示：\n$ r_{i,t} = \\alphat + \\beta{1,t}Size{i,t} + \\beta{2,t}Pret{i,t} + \\beta{3,t}Turn{i,t} + \\beta{4,t}Vol{i,t} + \\beta{5,t}PB{i,t} + \\beta{6,t}YOY_OP{i,t} + \\epsilon{i,t} $\n其中，Size 表示市值，Pret 表示反转，Turn 表示换手率，Vol 表示波动率，PB 表示价值，YOY_OP 表示营业利润同比增长率。 接下来根据研报分析情况实现横截面收益率回归，并进行分析。\nfor date in dateList: stockList = list(factorData[date].index) #pb_ratioa为市盈率 indicator.inc_operation_profit_year_on_year营业利润同比增长率 df = get_fundamentals(query(valuation.code, valuation.pb_ratio,\\ indicator.inc_operation_profit_year_on_year).\\ filter(valuation.code.in_(stockList)), date = date) df = df.set_index(['code']) factorData[date]['pb_ratio'] = df['pb_ratio'] factorData[date]['inc_operation_profit_year_on_year'] = df['inc_operation_profit_year_on_year'] factorData[date]['CO_neg'] = [i if i\u0026lt;0 else 0 for i in factorData[date]['CO']] factorData[date]['CO_pos'] = [i if i\u0026gt;0 else 0 for i in factorData[date]['CO']] factorData[date] = standardlize(factorData[date], inf2nan = True, axis = 0) def LRData(factorData, Fields): result_t = pd.DataFrame() result_params = pd.DataFrame() result_r2 = [] for date in dateList[:-1]: R_T = pd.DataFrame() stockList = list(factorData[date].index) df_close = get_price(stockList, date, dateList[dateList.index(date)+1], 'daily', ['close']) if df_close.empty: continue df_pchg = df_close['close'].iloc[-1,:]/df_close['close'].iloc[0,:] - 1 R_T['pchg'] = df_pchg factor_data = factorData[date] R_T[Fields] = factor_data[Fields] R_T = R_T.dropna() x = R_T[Fields] y = R_T['pchg'] wls = sm.OLS(y, x) result = wls.fit() tvalues = result.tvalues params = result.params r2 = result.rsquared_adj #校正决定系数用于判定一个多元线性回归方程的拟合程度(0-1)越大越好 result_t[date] = tvalues result_params[date] = params result_r2.append(r2) result = pd.DataFrame() result['t统计量'] = result_t.mean(axis = 1)#行平均值 result['参数估计'] = result_params.mean(axis = 1) print(\"调整 r2 值: \", np.nanmean(result_r2)) return result.T Fields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\", \"pb_ratio\", \"inc_operation_profit_year_on_year\"] LRData(factorData, Fields) 调整 r2 值: 0.03425017596289862 Fields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\", \"pb_ratio\", \\ \"inc_operation_profit_year_on_year\", \"Neg_Pos_ID\"] LRData(factorData,Fields) 调整 r2 值: 0.0423381606029065 Fields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\", \"pb_ratio\", \\ \"inc_operation_profit_year_on_year\", \"CO_neg\", \"CO_pos\"] LRData(factorData,Fields) 调整 r2 值: 0.03736065330273979 Fields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\", \"pb_ratio\", \\ \"inc_operation_profit_year_on_year\", \"Neg_Pos_ID\", \"CO_neg\", \"CO_pos\"] LRData(factorData, Fields) 调整 r2 值: 0.04523055137630058 以上四个表格分别对应研报中表格中提到的方程1 - 方程4，从以上表格数据中可以得到以下结论： （1）包含市值、反转、换手率、波动率、价值、YOY_OP 的 6 因子模型，在 2010-2017 年的调整 R 方为 3.42%，其中，换手率因子(VOL20)的因子收益率最高，回归得到的系数为 -0.0039，检验 t 值为 -1.99；价值因子（PB）与营业利润同比增长率因子 （inc_operation_profit_year_on_year）因子收益率最低，在整个样本期间效果不显著。 （2）从第二个表格来看，调整 R 方略微增加，从 3.42% 增加到 4.23%，增加 Neg_pos_ID 因子后，模型预测能力得到提高；从第三个表格来看，与表格一对比，调整 R 方从 3.42% 增加到 3.76%，但是略低于表格二，可见 CO 两个因子选股效果低于 Neg_pos_ID 因子。 （3）从表格 4 来看，其调整 R 方值最大，达到了 4.55%。从 t 统计量和因子收益率来看，因子 Neg_pos_ID 预测能力最好，其次是换手率因子以及反转因子。\n3.2 因子其他计算形式 参考研报，分别对以下两方面进行分析： （1）以成交额代替换手率计算 CO 指标； （2）采用不同时间长度计算因子值。\n3.2.1 以成交额代替换手率计算 CO start = time.clock() begin_date = '2010-01-01' end_date = '2017-01-01' dateList = get_period_date('M',begin_date, end_date) for date in dateList: stockList = get_stock_A(date) df_data = get_price(stockList, count = 21, end_date = date, frequency='1d', fields=['close', 'money']) df_pchg = df_data[\"close\"].pct_change().iloc[1:] temp_amount = df_data[\"money\"].iloc[1:] tempSum = 0 for i in range(len(temp_amount)): tempSum += i+1 for i in range(len(temp_amount)): temp_amount.iloc[i] = temp_amount.iloc[i] * float(i+1) / tempSum CO = temp_amount[df_pchg\u0026gt;0].sum() + (-1*temp_amount[df_pchg\u0026lt;0]).sum() factorData[date][\"CO_amount\"] = CO factorData[date]['CO_amount_neg'] = [i if i\u0026lt;0 else 0 for i in factorData[date]['CO_amount']] factorData[date]['CO_amount_pos'] = [i if i\u0026gt;0 else 0 for i in factorData[date]['CO_amount']] factorData[date] = standardlize(factorData[date], inf2nan=True, axis=0) elapsed = (time.clock() - start) print(\"Time used:\",elapsed) Fields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\", \"pb_ratio\", \"inc_operation_profit_year_on_year\", \"Neg_pos_ID\", \"CO_amount_neg\", \"CO_amount_pos\"] LRData(factorData, Fields) 调整 r2 值: 0.0482490866918\n指标 market_cap Price1M VOL20 sharpe_ratio_20 pb_ratio inc_operation_profit_year_on_year Neg_pos_ID CO_amount_neg CO_amount_pos t 统计量 -0.293751 -1.142062 -1.946750 0.673835 0.111308 0.168377 -1.400564 1.035387 -1.434746 参数估计 -0.000868 -0.003306 -0.004074 0.002023 -0.000085 0.000148 -0.003454 0.002783 -0.004192 根据上述结果，参考利用换手率计算 CO 因子的结果，采用成交额计算 CO 因子，在以下方面得到了改进： （1）从调整 R 方来看，采用成交额的方法有了明显增加； （2）从 CO_neg 和 CO_pos 这两个因子来看，CO 因子收益率得到了提升。\n3.2.2 构建期长短对选股效果的影响 将dataList换成以2W、Q为周期的 求出相应的factorData\nFields = [\"market_cap\", \"Price1M\", \"VOL20\", \"sharpe_ratio_20\", \"pb_ratio\", \\ \"inc_operation_profit_year_on_year\", \"Neg_Pos_ID\", \"CO_neg\", \"CO_pos\"] LRData(factorData, Fields) 以上面的代码求出r2和每个因子对应的t值和参数，得到结论\n上面结果分别实现了以周为周期和以季度为周期的因子分析结果，与月度为周期的分析结果相比，可以得到如下结论： （1）构建期为 2 周时，调整 R 方值达到了最大，为 4.25%，构建期为季度时，调整 R 方值最小，为 3.42%，可见构建期较短时能够获得更好地预测结果； （2）构建期较短时，CO 因子的因子收益率更高，且不管构建期多长，换手率因子和反转因子均具有较好的因子预测性。\n结论 海通金工本篇报告的研究主要测试了两种价量类新因子的选股效果，即 Neg_pos_ID 和 CO。其中前者是下跌天数与上涨天数之差的衡量，后者是上涨时的换手率与下跌时的换手率之差。 本文通过上述分析，得到了以下结论： （1）Neg_pos_ID 在一定程度上是一种价格动量因子，且通过对该因子进行有效性分析来看，该因子有效性较高，有较好的选股效果； （2）CO 因子的选股效果并非线性，然后通过对该因子与市值、反转、换手、 波动、价值等四个因子的组合特征来看，CO 因子与市值负相关， 与反转因子正相关，与换手率因子正相关，与波动率因子正相关，然后分别对这四个因子进行控制，与原始 CO 因子选股相比，前几个组合的月均收益得到了明显提高。 （3）进一步对这两个因子进行分析，首先对 6 因子(市值、反转、换手、 波动、价值、营业利润同比增长率)与 Neg_pos_ID 以及 CO 分解得到的 CO_pos 和 CO_pos 因子进行横截面回归，得知新因子的增加会提高模型的预测能力，换言之，新因子包含了有用的额外信息。 （4）当以成交额代替换手率时，CO 因子的选股效果得到提升；当选择不同的构建期进行分析时，构建期较短时，选股效果更佳。\n","permalink":"https://pp-tech-blog.pages.dev/posts/price_mount_test/","summary":"价量新因子测试 研究目的： 本文参考海通证券冯佳睿、袁林青撰写的《选股因子系列研究(十八)——价格形态选股因子》，根据研报分析，主要测试了开盘冲高、盘低回升以及均价偏离这三。","title":"价量新因子测试"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\nRNN RNN可通过记忆体实现短期记忆进行连续数据的预测 以连续60天的开盘价作为输入特征x_train，第61天的数据作为标签\nimport numpy as np import tensorflow as tf from tensorflow.keras.layers import Dropout, Dense, SimpleRNN import matplotlib.pyplot as plt import os import pandas as pd from sklearn.preprocessing import MinMaxScaler from sklearn.metrics import mean_squared_error, mean_absolute_error import math maotai = pd.read_csv(\u0026rsquo;./SH600519.csv\u0026rsquo;) # 读取股票文件\ntraining_set = maotai.iloc[0:2426 - 300, 2:3].values # 前(2426-300=2126)天的开盘价作为训练集,表格从0开始计数，2:3 是提取[2:3)列，前闭后开,故提取出C列开盘价 test_set = maotai.iloc[2426 - 300:, 2:3].values # 后300天的开盘价作为测试集\n归一化 sc = MinMaxScaler(feature_range=(0, 1)) # 定义归一化：归一化到(0，1)之间 training_set_scaled = sc.fit_transform(training_set) # 求得训练集的最大值，最小值这些训练集固有的属性，并在训练集上进行归一化 test_set = sc.transform(test_set) # 利用训练集的属性对测试集进行归一化\nx_train = [] y_train = []\nx_test = [] y_test = []\n测试集：csv表格中前2426-300=2126天数据 利用for循环，遍历整个训练集，提取训练集中连续60天的开盘价作为输入特征x_train，第61天的数据作为标签，for循环共构建2426-300-60=2066组数据。 for i in range(60, len(training_set_scaled)): x_train.append(training_set_scaled[i - 60:i, 0]) y_train.append(training_set_scaled[i, 0])\n对训练集进行打乱 np.random.seed(7) np.random.shuffle(x_train) np.random.seed(7) np.random.shuffle(y_train) tf.random.set_seed(7)\n将训练集由list格式变为array格式 x_train, y_train = np.array(x_train), np.array(y_train)\n使x_train符合RNN输入要求：[送入样本数， 循环核时间展开步数， 每个时间步输入特征个数]。 此处整个数据集送入，送入样本数为x_train.shape[0]即2066组数据；输入60个开盘价，预测出第61天的开盘价，循环核时间展开步数为60; 每个时间步送入的特征是某一天的开盘价，只有1个数据，故每个时间步输入特征个数为1 x_train = np.reshape(x_train, (x_train.shape[0], 60, 1))\n测试集：csv表格中后300天数据 利用for循环，遍历整个测试集，提取测试集中连续60天的开盘价作为输入特征x_train，第61天的数据作为标签，for循环共构建300-60=240组数据。 for i in range(60, len(test_set)): x_test.append(test_set[i - 60:i, 0]) y_test.append(test_set[i, 0])\n测试集变array并reshape为符合RNN输入要求：[送入样本数， 循环核时间展开步数， 每个时间步输入特征个数] x_test, y_test = np.array(x_test), np.array(y_test) x_test = np.reshape(x_test, (x_test.shape[0], 60, 1))\nmodel = tf.keras.Sequential([ SimpleRNN(80, return_sequences=True), Dropout(0.2), SimpleRNN(100), Dropout(0.2), Dense(1) ])\nmodel.compile(optimizer=tf.keras.optimizers.Adam(0.001), loss=\u0026lsquo;mean_squared_error\u0026rsquo;) # 损失函数用均方误差\n该应用只观测loss数值，不观测准确率，所以删去metrics选项，一会在每个epoch迭代显示时只显示loss值 checkpoint_save_path = \u0026ldquo;./checkpoint/rnn_stock.ckpt\u0026rdquo;\nif os.path.exists(checkpoint_save_path + \u0026lsquo;.index\u0026rsquo;): print(\u0026rsquo;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-load the model\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;\u0026rsquo;) model.load_weights(checkpoint_save_path)\ncp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path, save_weights_only=True, save_best_only=True, monitor=\u0026lsquo;val_loss\u0026rsquo;)\nhistory = model.fit(x_train, y_train, batch_size=64, epochs=50, validation_data=(x_test, y_test), validation_freq=1, callbacks=[cp_callback])\nmodel.summary()\nfile = open(\u0026rsquo;./weights.txt\u0026rsquo;, \u0026lsquo;w\u0026rsquo;) # 参数提取 for v in model.trainable_variables: file.write(str(v.name) + \u0026lsquo;\\n\u0026rsquo;) file.write(str(v.shape) + \u0026lsquo;\\n\u0026rsquo;) file.write(str(v.numpy()) + \u0026lsquo;\\n\u0026rsquo;) file.close()\nloss = history.history[\u0026rsquo;loss\u0026rsquo;] val_loss = history.history[\u0026lsquo;val_loss\u0026rsquo;]\nplt.plot(loss, label=\u0026lsquo;Training Loss\u0026rsquo;) plt.plot(val_loss, label=\u0026lsquo;Validation Loss\u0026rsquo;) plt.title(\u0026lsquo;Training and Validation Loss\u0026rsquo;) plt.legend() plt.show()\n################## predict ######################\n测试集输入模型进行预测 predicted_stock_price = model.predict(x_test)\n对预测数据还原\u0026mdash;从（0，1）反归一化到原始范围 predicted_stock_price = sc.inverse_transform(predicted_stock_price)\n对真实数据还原\u0026mdash;从（0，1）反归一化到原始范围 real_stock_price = sc.inverse_transform(test_set[60:])\n画出真实数据和预测数据的对比曲线 plt.plot(real_stock_price, color=\u0026lsquo;red\u0026rsquo;, label=\u0026lsquo;MaoTai Stock Price\u0026rsquo;) plt.plot(predicted_stock_price, color=\u0026lsquo;blue\u0026rsquo;, label=\u0026lsquo;Predicted MaoTai Stock Price\u0026rsquo;) plt.title(\u0026lsquo;MaoTai Stock Price Prediction\u0026rsquo;) plt.xlabel(\u0026lsquo;Time\u0026rsquo;) plt.ylabel(\u0026lsquo;MaoTai Stock Price\u0026rsquo;) plt.legend() plt.show()\n##########evaluate##############\ncalculate MSE 均方误差 \u0026mdash;\u0026gt; E[(预测值-真实值)^2] (预测值减真实值求平方后求均值) mse = mean_squared_error(predicted_stock_price, real_stock_price)\ncalculate RMSE 均方根误差\u0026mdash;\u0026gt;sqrt[MSE] (对均方误差开方) rmse = math.sqrt(mean_squared_error(predicted_stock_price, real_stock_price))\ncalculate MAE 平均绝对误差\u0026mdash;\u0026ndash;\u0026gt;E[|预测值-真实值|](预测值减真实值求绝对值后求均值） mae = mean_absolute_error(predicted_stock_price, real_stock_price) print(\u0026lsquo;均方误差: %.6f\u0026rsquo; % mse) print(\u0026lsquo;均方根误差: %.6f\u0026rsquo; % rmse) print(\u0026lsquo;平均绝对误差: %.6f\u0026rsquo; % mae)\n预测结果： LSTM 当连续数据的序列变长时，会使展开时间步过长，在反向传播更新参数时，梯度要按照时间步连续相乘，会导致梯度消失，题出长短记忆网络LSTM。\nLSTM计算过程 W是待训练参数，h是待训练偏置项，都经过sigmod使门限范围在0-1 细胞态表长期记忆，等于上个时期的长期记忆乘以遗忘门，加上当前时刻归纳出的新知识乘以输入门 记忆体表示短期记忆，属于长期记忆的一部分，是细胞态过tanh函数乘以输出门的结果 候选态 表示归纳出的待存入细胞态的新知识\nTF实现 tf.keras.layers.LSTM(记忆体个数，return_sequences=是否返回输出) return_sequences=True 各时间步输出ht return_sequences=False 仅最后时间步输出ht（默认）\n股票预测代码 改动上面代码的model\nmodel = tf.keras.Sequential([ LSTM(80, return_sequences=True), Dropout(0.2), LSTM(100), Dropout(0.2), Dense(1) ]) 预测效果 GRU GRU使记忆体ht融合了长期记忆和短期记忆\nGRU计算过程 TF实现 tf.keras.layers.GRU(记忆体个数，return_sequences=是否返回输出) return_sequences=True 各时间步输出ht return_sequences=False 仅最后时间步输出ht（默认）\n代码 model = tf.keras.Sequential([ GRU(80, return_sequences=True), Dropout(0.2), GRU(100), Dropout(0.2), Dense(1) ]) 预测结果： ","permalink":"https://pp-tech-blog.pages.dev/posts/predict_stockprice/","summary":"RNN RNN可通过记忆体实现短期记忆进行连续数据的预测 以连续60天的开盘价作为输入特征x_train，第61天的数据作为标签 import numpy as np im。","title":"神经网络预测股票价格"},{"content":"fit(): Method calculates the parameters μ and σ and saves them as internal objects. 解释：简单来说，就是求得训练集X的均值，方差，最大值，最小值,这些训练集X固有的属性。\ntransform(): Method using these calculated parameters apply the transformation to a particular dataset. 解释：在fit的基础上，进行标准化，降维，归一化等操作（看具体用的是哪个工具，如PCA，StandardScaler等）。\nfit_transform(): joins the fit() and transform() method for transformation of dataset. 解释：fit_transform是fit和transform的组合，既包括了训练又包含了转换。 transform()和fit_transform()二者的功能都是对数据进行某种统一处理（比如标准化~N(0,1)，将数据缩放(映射)到某个固定区间，归一化，正则化等）\nfit_transform(trainData)对部分数据先拟合fit，找到该part的整体指标，如均值、方差、最大值最小值等等（根据具体转换的目的），然后对该trainData进行转换transform，从而实现数据的标准化、归一化等等。\n根据对之前部分trainData进行fit的整体指标，对剩余的数据（testData）使用同样的均值、方差、最大最小值等指标进行转换transform(testData)，从而保证train、test处理方式相同。所以，一般都是这么用：\nfrom sklearn.preprocessing import StandardScaler sc = StandardScaler() sc.fit_tranform(X_train) sc.tranform(X_test) ","permalink":"https://pp-tech-blog.pages.dev/posts/fit_transformfittransform/","summary":"fit(): Method calculates the parameters μ and σ and saves them as internal objects. 解释。","title":"fit_transform,fit,transform区别和作用"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\nTotal Returns（策略收益） Total Annualized Returns（策略年化收益） Benchmark Returns（基准收益） Benchmark Annualized Returns（基准年化收益） Alpha（阿尔法） 投资中面临着系统性风险（即Beta）和非系统性风险（即Alpha），Alpha是投资者获得与市场波动无关的回报。比如投资者获得了15%的回报，其基准获得了10%的回报，那么Alpha或者价值增值的部分就是5%。 Alpha值 α\u0026gt;0 策略相对于风险，获得了超额收益 α=0 策略相对于风险，获得了适当收益 α\u0026lt;0 策略相对于风险，获得了较少收益 Beta（贝塔） 表示投资的系统性风险，反映了策略对大盘变化的敏感性。例如一个策略的Beta为1.5，则大盘涨1%的时候，策略可能涨1.5%，反之亦然；如果一个策略的Beta为-1.5，说明大盘涨1%的时候，策略可能跌1.5%，反之亦然。 Beta值 β\u0026lt;0 投资组合和基准的走向通常反方向，如空头头寸类 β=0 投资组合和基准的走向没有相关性，如固定收益类 0\u0026lt;β\u0026lt;1 投资组合和基准的走向相同，但是比基准的移动幅度更小 β=1 投资组合和基准的走向相同，并且和基准的移动幅度贴近 β\u0026gt;1 投资组合和基准的走向相同，但是比基准的移动幅度更大 Sharpe（夏普比率） 表示每承受一单位总风险，会产生多少的超额报酬，可以同时对策略的收益与风险进行综合考虑。 Sortino（索提诺比率） 表示每承担一单位的下行风险，将会获得多少超额回报。 Information Ratio（信息比率） 衡量单位超额风险带来的超额收益。信息比率越大，说明该策略单位跟踪误差所获得的超额收益越高，因此，信息比率较大的策略的表现要优于信息比率较低的基准。合理的投资目标应该是在承担适度风险下，尽可能追求高信息比率。 Algorithm Volatility（策略波动率） 用来测量策略的风险性，波动越大代表策略风险越高。 Benchmark Volatility（基准波动率） 用来测量基准的风险性，波动越大代表基准风险越高。 Max Drawdown（最大回撤） 描述策略可能出现的最糟糕的情况，最极端可能的亏损情况。 Downside Risk（下行波动率） 策略收益下行波动率。和普通收益波动率相比，下行标准差区分了好的和坏的波动。 胜率(%) 盈利次数在总交易次数中的占比。 日胜率(%) 策略盈利超过基准盈利的天数在总交易数中的占比。 盈亏比 周期盈利亏损的比例。 ","permalink":"https://pp-tech-blog.pages.dev/posts/post-473/","summary":"Total Returns（策略收益） Total Annualized Returns（策略年化收益） Benchmark Returns（基准收益） Benchmark。","title":"投资收益和风险指标"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n循环神经网络 循环核 参数时间共享，循环层提取时间信息。 结构： 前向传播时：记忆体内存储的状态信息ht ，在每个时刻都被刷新，三个参数矩阵$w_xh, w_hh, w_hy$自始至终都是固定不变的。\n反向传播时：三个参数矩阵$w_xh, w_hh, w_hy$被梯度下降法更新。 循环核按时间步展开 按时间步展开,就是把循环核按照时间轴方向展开，每个时刻记忆体状态信息ht被刷新，记忆体周围的参数矩阵$w_xh, w_hh, w_hy$是固定不变的，训练优化的就是这些参数矩阵，训练完成后使用效果最好的参数矩阵，执行前向传播，输出预测结果。 循环神经网络：借助循环核提取时间特征后，送入全连接网络。\n循环计算层 每个循环核构成一层循环计算层，循环计算层的层数是向输出方向增长的。每个循环核中记忆体的个数，可以根据需求任意指定。\nTF描述计算层 tf.keras.layers.SimpleRNN(记忆体个数，activation=‘激活函数’ ， return_sequences=是否每个时刻输出ht到下一层) activation=‘激活函数’ （不写，默认使用tanh） return_sequences=True 各时间步输出ht return_sequences=False 仅最后时间步输出ht（默认） 一般最后一层的循环核用False,仅最后一个时间步输出ht，中间层用True，每个时间步都把ht输出给下一层 例：\u0026lt;code\u0026gt;SimpleRNN(3, return_sequences=True)\u0026lt;/code\u0026gt; API对送入循环层的数据维度有要求，要求送入循环层的数据是三维的。\n入RNN时， x_train维度： [送入样本数， 循环核时间展开步数， 每个时间步输入特征个数] 例如： 循环计算过程1 ABCDE字母预测（输入一个字母预测下一个字母） 字母预测：输入a预测出b，输入b预测出c，输入c预测出d，输入d预测出e，输入e预测出a 用独热码对字母进行编码，随机生成$w_xh, w_hh, w_hy$三个参数矩阵，记忆体个数选取3,最开始时记忆体信息等于0：h(t-1) = [0.0, 0.0, 0.0]，ht.yt公式如上 具体计算过程如图： 可以看到模型认为有91%的可能性下一个字母是c 代码如下：\nimport numpy as np import tensorflow as tf from tensorflow.keras.layers import Dense, SimpleRNN import matplotlib.pyplot as plt import os input_word = \u0026ldquo;abcde\u0026rdquo; w_to_id = {\u0026lsquo;a\u0026rsquo;: 0, \u0026lsquo;b\u0026rsquo;: 1, \u0026lsquo;c\u0026rsquo;: 2, \u0026rsquo;d\u0026rsquo;: 3, \u0026rsquo;e\u0026rsquo;: 4} # 单词映射到数值id的词典 id_to_onehot = {0: [1., 0., 0., 0., 0.], 1: [0., 1., 0., 0., 0.], 2: [0., 0., 1., 0., 0.], 3: [0., 0., 0., 1., 0.], 4: [0., 0., 0., 0., 1.]} # id编码为one-hot\nx_train = [id_to_onehot[w_to_id[\u0026lsquo;a\u0026rsquo;]], id_to_onehot[w_to_id[\u0026lsquo;b\u0026rsquo;]], id_to_onehot[w_to_id[\u0026lsquo;c\u0026rsquo;]], id_to_onehot[w_to_id[\u0026rsquo;d\u0026rsquo;]], id_to_onehot[w_to_id[\u0026rsquo;e\u0026rsquo;]]] y_train = [w_to_id[\u0026lsquo;b\u0026rsquo;], w_to_id[\u0026lsquo;c\u0026rsquo;], w_to_id[\u0026rsquo;d\u0026rsquo;], w_to_id[\u0026rsquo;e\u0026rsquo;], w_to_id[\u0026lsquo;a\u0026rsquo;]]\nnp.random.seed(7) np.random.shuffle(x_train) np.random.seed(7) np.random.shuffle(y_train) tf.random.set_seed(7)\n使x_train符合SimpleRNN输入要求：[送入样本数， 循环核时间展开步数， 每个时间步输入特征个数]。 此处整个数据集送入，送入样本数为len(x_train)；输入1个字母出结果，循环核时间展开步数为1; 表示为独热码有5个输入特征，每个时间步输入特征个数为5 x_train = np.reshape(x_train, (len(x_train), 1, 5)) y_train = np.array(y_train)\nmodel = tf.keras.Sequential([ SimpleRNN(3), #搭建具有3个记忆体的循环层。记忆体个数可自定义，记忆体个数越多，记忆力越好，占用资源更多 Dense(5, activation=\u0026lsquo;softmax\u0026rsquo;) #全连接层 实现了y_t的计算 ])\nmodel.compile(optimizer=tf.keras.optimizers.Adam(0.01), loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=[\u0026lsquo;sparse_categorical_accuracy\u0026rsquo;])\ncheckpoint_save_path = \u0026ldquo;./checkpoint/rnn_onehot_1pre1.ckpt\u0026rdquo;\nif os.path.exists(checkpoint_save_path + \u0026lsquo;.index\u0026rsquo;): print(\u0026rsquo;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-load the model\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;\u0026rsquo;) model.load_weights(checkpoint_save_path)\ncp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path, save_weights_only=True, save_best_only=True, monitor=\u0026lsquo;loss\u0026rsquo;) # 由于fit没有给出测试集，不计算测试集准确率，根据loss，保存最优模型\nhistory = model.fit(x_train, y_train, batch_size=32, epochs=100, callbacks=[cp_callback])\nmodel.summary()\nprint(model.trainable_variables) file = open(\u0026rsquo;./weights.txt\u0026rsquo;, \u0026lsquo;w\u0026rsquo;) # 参数提取 for v in model.trainable_variables: file.write(str(v.name) + \u0026lsquo;\\n\u0026rsquo;) file.write(str(v.shape) + \u0026lsquo;\\n\u0026rsquo;) file.write(str(v.numpy()) + \u0026lsquo;\\n\u0026rsquo;) file.close()\n############################################### show ###############################################\n显示训练集和验证集的acc和loss曲线 acc = history.history[\u0026lsquo;sparse_categorical_accuracy\u0026rsquo;] loss = history.history[\u0026rsquo;loss\u0026rsquo;]\nplt.subplot(1, 2, 1) plt.plot(acc, label=\u0026lsquo;Training Accuracy\u0026rsquo;) plt.title(\u0026lsquo;Training Accuracy\u0026rsquo;) plt.legend()\nplt.subplot(1, 2, 2) plt.plot(loss, label=\u0026lsquo;Training Loss\u0026rsquo;) plt.title(\u0026lsquo;Training Loss\u0026rsquo;) plt.legend() plt.show()\n############### predict #############\npreNum = int(input(\u0026ldquo;input the number of test alphabet:\u0026rdquo;)) for i in range(preNum): alphabet1 = input(\u0026ldquo;input test alphabet:\u0026rdquo;) alphabet = [id_to_onehot[w_to_id[alphabet1]]] # 使alphabet符合SimpleRNN输入要求：[送入样本数， 循环核时间展开步数， 每个时间步输入特征个数]。此处验证效果送入了1个样本，送入样本数为1；输入1个字母出结果，所以循环核时间展开步数为1; 表示为独热码有5个输入特征，每个时间步输入特征个数为5 alphabet = np.reshape(alphabet, (1, 1, 5)) result = model.predict([alphabet]) pred = tf.argmax(result, axis=1) pred = int(pred) tf.print(alphabet1 + \u0026lsquo;-\u0026gt;\u0026rsquo; + input_word[pred])\nABCDE字母预测（输入几个字母预测下一个字母） 把时间核按时间步展开，连续输入几个字母预测下一个字母 连续输入4个字母预测下一个字母具体计算过程如图： 实现代码：\nimport numpy as np import tensorflow as tf from tensorflow.keras.layers import Dense, SimpleRNN import matplotlib.pyplot as plt import os input_word = \"abcde\" w_to_id = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4} # 单词映射到数值id的词典 id_to_onehot = {0: [1., 0., 0., 0., 0.], 1: [0., 1., 0., 0., 0.], 2: [0., 0., 1., 0., 0.], 3: [0., 0., 0., 1., 0.], 4: [0., 0., 0., 0., 1.]} # id编码为one-hot x_train = [ [id_to_onehot[w_to_id['a']], id_to_onehot[w_to_id['b']], id_to_onehot[w_to_id['c']], id_to_onehot[w_to_id['d']]], [id_to_onehot[w_to_id['b']], id_to_onehot[w_to_id['c']], id_to_onehot[w_to_id['d']], id_to_onehot[w_to_id['e']]], [id_to_onehot[w_to_id['c']], id_to_onehot[w_to_id['d']], id_to_onehot[w_to_id['e']], id_to_onehot[w_to_id['a']]], [id_to_onehot[w_to_id['d']], id_to_onehot[w_to_id['e']], id_to_onehot[w_to_id['a']], id_to_onehot[w_to_id['b']]], [id_to_onehot[w_to_id['e']], id_to_onehot[w_to_id['a']], id_to_onehot[w_to_id['b']], id_to_onehot[w_to_id['c']]], ] y_train = [w_to_id['e'], w_to_id['a'], w_to_id['b'], w_to_id['c'], w_to_id['d']] np.random.seed(7) np.random.shuffle(x_train) np.random.seed(7) np.random.shuffle(y_train) tf.random.set_seed(7) # 使x_train符合SimpleRNN输入要求：[送入样本数， 循环核时间展开步数， 每个时间步输入特征个数]。 # 此处整个数据集送入，送入样本数为len(x_train)；输入4个字母出结果，循环核时间展开步数为4; 表示为独热码有5个输入特征，每个时间步输入特征个数为5 x_train = np.reshape(x_train, (len(x_train), 4, 5)) y_train = np.array(y_train) model = tf.keras.Sequential([ SimpleRNN(3), Dense(5, activation='softmax') ]) model.compile(optimizer=tf.keras.optimizers.Adam(0.01), loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=['sparse_categorical_accuracy']) checkpoint_save_path = \"./checkpoint/rnn_onehot_4pre1.ckpt\" if os.path.exists(checkpoint_save_path + '.index'): print('-------------load the model-----------------') model.load_weights(checkpoint_save_path) cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path, save_weights_only=True, save_best_only=True, monitor='loss') # 由于fit没有给出测试集，不计算测试集准确率，根据loss，保存最优模型 history = model.fit(x_train, y_train, batch_size=32, epochs=100, callbacks=[cp_callback]) model.summary() # print(model.trainable_variables) file = open('./weights.txt', 'w') # 参数提取 for v in model.trainable_variables: file.write(str(v.name) + '\\n') file.write(str(v.shape) + '\\n') file.write(str(v.numpy()) + '\\n') file.close() ############################################### show ############################################### # 显示训练集和验证集的acc和loss曲线 acc = history.history['sparse_categorical_accuracy'] loss = history.history['loss'] plt.subplot(1, 2, 1) plt.plot(acc, label='Training Accuracy') plt.title('Training Accuracy') plt.legend() plt.subplot(1, 2, 2) plt.plot(loss, label='Training Loss') plt.title('Training Loss') plt.legend() plt.show() ############### predict ############# preNum = int(input(\"input the number of test alphabet:\")) for i in range(preNum): alphabet1 = input(\"input test alphabet:\") alphabet = [id_to_onehot[w_to_id[a]] for a in alphabet1] # 使alphabet符合SimpleRNN输入要求：[送入样本数， 循环核时间展开步数， 每个时间步输入特征个数]。此处验证效果送入了1个样本，送入样本数为1；输入4个字母出结果，所以循环核时间展开步数为4; 表示为独热码有5个输入特征，每个时间步输入特征个数为5 alphabet = np.reshape(alphabet, (1, 4, 5)) result = model.predict([alphabet]) pred = tf.argmax(result, axis=1) pred = int(pred) tf.print(alphabet1 + '-\u0026gt;' + input_word[pred]) Embedding 独热码：数据量大 过于稀疏，映射之间是独立的，没有表现出关联性 Embedding：是一种单词编码方法，用低维向量实现了编码，这种编码通过神经网络训练优化，能表达出单词间的相关性。 TF中Embedding实现编码的函数： tf.keras.layers.Embedding(词汇表大小，编码维度) 编码维度就是用几个数字表达一个单词 例 ：对1-100进行编码， [4] 编码为 [0.25, 0.1, 0.11] tf.keras.layers.Embedding(100, 3 )\nEmbedding层对输入数据的维度要求 入Embedding时， x_train维度(二维)： [送入样本数， 循环核时间展开步数]\n上面字母预测代码中的独热码改为Embedding：\n# 使x_train符合Embedding输入要求：[送入样本数， 循环核时间展开步数] ， # 此处整个数据集送入所以送入，送入样本数为len(x_train)；输入4个字母出结果，循环核时间展开步数为4。 x_train = np.reshape(x_train, (len(x_train), 4)) y_train = np.array(y_train) model = tf.keras.Sequential([ Embedding(26, 2), SimpleRNN(10), Dense(26, activation=\u0026lsquo;softmax\u0026rsquo;) ])\n股票预测 ","permalink":"https://pp-tech-blog.pages.dev/posts/tensorflow_6/","summary":"循环神经网络 循环核 参数时间共享，循环层提取时间信息。 结构： 前向传播时：记忆体内存储的状态信息ht ，在每个时刻都被刷新，三个参数矩阵$w_xh, w_hh, w_h。","title":"tensorflow_6 循环神经网络"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n卷积神经网络概念 卷积(Convolutional) • 卷积计算可认为是一种有效提取图像特征的方法 • 一般会用一个正方形的卷积核，按指定步长，在输入特征图上滑动，遍历输入特征图中的每个像素点。每一个步长，卷积核会与输入特征图出现重合区域，重合区域对应元素相乘、求和再加上偏置项,得到输出特征的一个像素点\n输入特征图的深度（channel数），决定了当前层卷积核的深度； 当前层卷积核的个数，决定了当前层输出特征图的深度。 感受野（Receptive Field） 卷积神经网络各输出特征图中的每个像素点，在原始输入图片上映射区域的大小。 如图，一个5 5像素点的图像经过两层3 3卷积核作用与一层5 5卷积核作用都得到一个感受野是5的输出特征图 带训练参数与计算量： 设输入特征图宽、高为x，卷积计算步长为1 3 3：参数量：9+9=18 计算量：18x2 - 108x + 180 5 5：参数量：25 计算量：25x2 - 200x + 400 当x\u0026gt;10 时，两层3 3卷积核 优于 一层5 * 5卷积核\n全零填充(Padding) 若希望卷积计算保持输入特征图的尺寸不变，可以使用全零填充，在输入特征图周围填充0，输出特征图边长等于输入特征图边长除以步长（向上取整） 不全零填充时，输出特征图边长等于输入特征图边长减核长加1除步长 TF描述全零填充 用参数padding = ‘SAME’ 或 padding = ‘VALID’表示 TF描述卷积层 tf计算卷积的函数：\ntf.keras.layers.Conv2D ( filters = 卷积核个数, kernel_size = 卷积核尺寸, #正方形写核长整数，或（核高h，核宽w） strides = 滑动步长, #横纵向相同写步长整数，或(纵向步长h，横向步长w)，默认1 padding = “same” or “valid”, #使用全零填充是“same”，不使用是“valid”（默认） activation = “ relu ” or “ sigmoid ” or “ tanh ” or “ softmax”等 , #选用什么激活函数，如有BN（Batch Normalization批量标准化操作）此处不写 input_shape = (高, 宽 , 通道数) #输入特征图维度，可省略 ) 例如：\nmodel = tf.keras.models.Sequential([ Conv2D(6, 5, padding='valid', activation='sigmoid'), MaxPool2D(2, 2), Conv2D(6, (5, 5), padding='valid', activation='sigmoid'), MaxPool2D(2, (2, 2)), Conv2D(filters=6, kernel_size=(5, 5),padding='valid', activation='sigmoid'), MaxPool2D(pool_size=(2, 2), strides=2), Flatten(), Dense(10, activation='softmax') ]) 批标准化（Batch Normalization， BN） 随着网络层数的增加，特征数据会出现偏离均值0的情况，标准化可以使数据符合以0为均值，1为标准差的正态分布 标准化：使数据符合0均值，1为标准差的分布。 批标准化：对一小批数据（batch），做标准化处理 。 批标准化后，第 k个卷积核的输出特征图（feature map）中第 i 个像素点 $H_i^{'k} = \\frac{H_i^k-\\tau_batch^k}{\\sigma_batch^k}$ $H_i^k$:批标准化前，第k个卷积核，输出特征图中第 i 个像素点 $\\tau_batch^k$:批标准化前，第k个卷积核，batch张输出特征图中所有像素点平均值 $\\sigma_batch^k$:批标准化前，第k个卷积核，batch张输出特征图中所有像素点标准差 批标准化前后： BN操作将原本偏移的特征数据重新拉回到0均值，使进入激活函数的数据分布在激活函数线性区，使得输入数据的微小变化更明显的体现到激活函数的输出，提升了激活函数对输入数据的区分力，但是这种简单的特征数据标准化，使特征数据完全满足标准正态分布，集中在激活函数中心的线性区域，使激活函数丧失了非线性特性，因此在BN操作中为每个卷积核引入了两个可训练参数，缩放因$\\gamma$和偏移因子$\\beta$. $X_i^k = \\gamma_k H_i^{\u0026lsquo;k} + \\beta_k$ 反向传播时，这两个因子会与其他待训练参数一同被训练优化，使标准正态分布后的特征数据通过这两个因子优化了特征数据分布的宽窄和偏移量，保证了网络的非线性表达力。\nTF中BN操作的函数： tf.keras.layers.BatchNormalization() 例如：\nmodel = tf.keras.models.Sequential([ Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷积层 BatchNormalization(), # BN层 Activation('relu'), # 激活层 MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化层 Dropout(0.2), # dropout层 池化(Pooling) 池化用于减少特征数据量。有最大值池化和均值池化。 最大值池化可提取图片纹理，均值池化可保留背景特征。 池化过程： TF池化函数：\n最大值池化tf.keras.layers.MaxPool2D( pool_size=池化核尺寸，#正方形写核长整数，或（核高h，核宽w） strides=池化步长，#步长整数， 或(纵向步长h，横向步长w)，默认为pool_size padding=‘valid’or‘same’ #使用全零填充是“same”，不使用是“valid”（默认） ) 均值池化\ntf.keras.layers.AveragePooling2D( pool_size=池化核尺寸，#正方形写核长整数，或（核高h，核宽w） strides=池化步长，#步长整数， 或(纵向步长h，横向步长w)，默认为pool_size padding=‘valid’or‘same’ #使用全零填充是“same”，不使用是“valid”（默认） ) 实例：\nmodel = tf.keras.models.Sequential([ Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷积层 BatchNormalization(), # BN层 Activation('relu'), # 激活层 MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化层 Dropout(0.2), # dropout层 ]) 舍弃(Dropout) 为了缓解神经网络过拟合，在神经网络训练时，将一部分神经元按照一定概率从神经网络中暂时舍弃。神经网络使用时，被舍弃的神经元恢复链接。 TF池化函数：tf.keras.layers.Dropout(舍弃的概率) 实例见上代码 Dropout(0.2), # dropout层 0.2表示随机舍弃掉20%的神经元\n卷积神经网络 本质 卷积神经网络：借助卷积核提取特征后，送入全连接网络。\n卷积神经网络网络的主要模块： 卷积、批标准化、激活、池化 通过以上四个模块对输入特征进行特征提取 卷积是什么？ 卷积就是特征提取器，就是CBAPD !!!!!model = tf.keras.models.Sequential([ Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷积层 C BatchNormalization(), # BN层 B Activation('relu'), # 激活层 A MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化层 P Dropout(0.2), # dropout层 D ]) Cifar10数据集： 提供 5万张 3232 像素点的十分类彩色图片和标签，用于训练。 提供 1万张 3232 像素点的十分类彩色图片和标签，用于测试。 每张图片有32*32列像素点的rgb三通道数据 导入cifar10数据集： cifar10 = tf.keras.datasets.cifar10 (x_train, y_train),(x_test, y_test) = cifar10.load_data() 其查看数据集shape以及图片方法和上文 mnist一样\n神经网络搭建 用卷积神经网络训练cifar10数据集，搭建一个一层卷积、两层全连接的网络，使用6个5 5的卷积核，过2 2的池化核，过128个神经元的全连接层，由于cifar10是十分类，最后还要过一层十个神经元的全连接层。简化过程如下：\nC（核：655，步长：1，填充：same ） B（Yes） A（relu） P（max，核：2*2，步长：2，填充：same） D（0.2） Flatten Dense（神经元：128，激活：relu，Dropout：0.2） Dense（神经元：10，激活：softmax） 卷积神经网络训练Cifar10数据集baseline：\nimport tensorflow as tf import os import numpy as np from matplotlib import pyplot as plt from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Dropout, Flatten, Dense from tensorflow.keras import Model np.set_printoptions(threshold=np.inf)\ncifar10 = tf.keras.datasets.cifar10 (x_train, y_train), (x_test, y_test) = cifar10.load_data() x_train, x_test = x_train / 255.0, x_test / 255.0\n#由于网络相对复杂，所以用class类搭建网络结构 class Baseline(Model): def init(self): super(Baseline, self).init() self.c1 = Conv2D(filters=6, kernel_size=(5, 5), padding=\u0026lsquo;same\u0026rsquo;) # 卷积层 self.b1 = BatchNormalization() # BN层 self.a1 = Activation(\u0026lsquo;relu\u0026rsquo;) # 激活层 self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding=\u0026lsquo;same\u0026rsquo;) # 池化层 self.d1 = Dropout(0.2) # dropout层\nself.flatten = Flatten() self.f1 = Dense(128, activation='relu') self.d2 = Dropout(0.2) self.f2 = Dense(10, activation='softmax') def call(self, x): x = self.c1(x) x = self.b1(x) x = self.a1(x) x = self.p1(x) x = self.d1(x) x = self.flatten(x) x = self.f1(x) x = self.d2(x) y = self.f2(x) return y model = Baseline()\nmodel.compile(optimizer=\u0026lsquo;adam\u0026rsquo;, loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=[\u0026lsquo;sparse_categorical_accuracy\u0026rsquo;])\ncheckpoint_save_path = \u0026ldquo;./checkpoint/Baseline.ckpt\u0026rdquo; if os.path.exists(checkpoint_save_path + \u0026lsquo;.index\u0026rsquo;): print(\u0026rsquo;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-load the model\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;\u0026rsquo;) model.load_weights(checkpoint_save_path)\ncp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path, save_weights_only=True, save_best_only=True)\nhistory = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1, callbacks=[cp_callback]) model.summary()\nprint(model.trainable_variables) file = open(\u0026rsquo;./weights.txt\u0026rsquo;, \u0026lsquo;w\u0026rsquo;) for v in model.trainable_variables: file.write(str(v.name) + \u0026lsquo;\\n\u0026rsquo;) file.write(str(v.shape) + \u0026lsquo;\\n\u0026rsquo;) file.write(str(v.numpy()) + \u0026lsquo;\\n\u0026rsquo;) file.close() ############################################### show ###############################################\n显示训练集和验证集的acc和loss曲线 acc = history.history[\u0026lsquo;sparse_categorical_accuracy\u0026rsquo;] val_acc = history.history[\u0026lsquo;val_sparse_categorical_accuracy\u0026rsquo;] loss = history.history[\u0026rsquo;loss\u0026rsquo;] val_loss = history.history[\u0026lsquo;val_loss\u0026rsquo;]\nplt.subplot(1, 2, 1) plt.plot(acc, label=\u0026lsquo;Training Accuracy\u0026rsquo;) plt.plot(val_acc, label=\u0026lsquo;Validation Accuracy\u0026rsquo;) plt.title(\u0026lsquo;Training and Validation Accuracy\u0026rsquo;) plt.legend()\nplt.subplot(1, 2, 2) plt.plot(loss, label=\u0026lsquo;Training Loss\u0026rsquo;) plt.plot(val_loss, label=\u0026lsquo;Validation Loss\u0026rsquo;) plt.title(\u0026lsquo;Training and Validation Loss\u0026rsquo;) plt.legend() plt.show()\n下面在介绍经典卷积神经网络结构时，只替换 class Baseline（）部分，其余代码不变。\n经典卷积神经网络 LeNet 网络结构： 两层卷积层，三层全连接 前两层卷积 输入：32 32 3 C（核：6 5 5，步长：1，填充：valid ） B（None） A（sigmoid） P（max，核：2 * 2，步长：2，填充：valid ） D（None）\nC（核：16 5 5，步长：1，填充： valid ） B（None） A（sigmoid） P（max，核：2 * 2，步长：2，填充：valid ） D（None） 全连接： Flatten Dense（神经元：120，激活：sigmoid） Dense（神经元：84，激活：sigmoid） Dense（神经元：10，激活：softmax） 网络结构代码：\nclass LeNet5(Model): def __init__(self): super(LeNet5, self).__init__() self.c1 = Conv2D(filters=6, kernel_size=(5, 5), activation='sigmoid') self.p1 = MaxPool2D(pool_size=(2, 2), strides=2) self.c2 = Conv2D(filters=16, kernel_size=(5, 5), activation='sigmoid') self.p2 = MaxPool2D(pool_size=(2, 2), strides=2) self.flatten = Flatten() self.f1 = Dense(120, activation='sigmoid') self.f2 = Dense(84, activation='sigmoid') self.f3 = Dense(10, activation='softmax')\u0026lt;/code\u0026gt;\u0026lt;/pre\u0026gt; AlexNet 八层 五层卷积，三层全连接 class AlexNet8(Model): def __init__(self): super(AlexNet8, self).__init__() self.c1 = Conv2D(filters=96, kernel_size=(3, 3)) self.b1 = BatchNormalization() self.a1 = Activation('relu') self.p1 = MaxPool2D(pool_size=(3, 3), strides=2) self.c2 = Conv2D(filters=256, kernel_size=(3, 3)) self.b2 = BatchNormalization() self.a2 = Activation('relu') self.p2 = MaxPool2D(pool_size=(3, 3), strides=2) self.c3 = Conv2D(filters=384, kernel_size=(3, 3), padding='same', activation='relu') self.c4 = Conv2D(filters=384, kernel_size=(3, 3), padding='same', activation='relu') self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding='same', activation='relu') self.p3 = MaxPool2D(pool_size=(3, 3), strides=2) self.flatten = Flatten() self.f1 = Dense(2048, activation='relu') self.d1 = Dropout(0.5) self.f2 = Dense(2048, activation='relu') self.d2 = Dropout(0.5) self.f3 = Dense(10, activation='softmax')\u0026lt;/code\u0026gt;\u0026lt;/pre\u0026gt; VGGNet VGGNet使用小尺寸卷积核，在减少参数的同时，提高了识别准确率\nclass VGG16(Model): def __init__(self): super(VGG16, self).__init__() self.c1 = Conv2D(filters=64, kernel_size=(3, 3), padding='same') # 卷积层1 self.b1 = BatchNormalization() # BN层1 self.a1 = Activation('relu') # 激活层1 self.c2 = Conv2D(filters=64, kernel_size=(3, 3), padding='same', ) self.b2 = BatchNormalization() # BN层1 self.a2 = Activation('relu') # 激活层1 self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same') self.d1 = Dropout(0.2) # dropout层 self.c3 = Conv2D(filters=128, kernel_size=(3, 3), padding='same') self.b3 = BatchNormalization() # BN层1 self.a3 = Activation('relu') # 激活层1 self.c4 = Conv2D(filters=128, kernel_size=(3, 3), padding='same') self.b4 = BatchNormalization() # BN层1 self.a4 = Activation('relu') # 激活层1 self.p2 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same') self.d2 = Dropout(0.2) # dropout层 self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding='same') self.b5 = BatchNormalization() # BN层1 self.a5 = Activation('relu') # 激活层1 self.c6 = Conv2D(filters=256, kernel_size=(3, 3), padding='same') self.b6 = BatchNormalization() # BN层1 self.a6 = Activation('relu') # 激活层1 self.c7 = Conv2D(filters=256, kernel_size=(3, 3), padding='same') self.b7 = BatchNormalization() self.a7 = Activation('relu') self.p3 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same') self.d3 = Dropout(0.2) self.c8 = Conv2D(filters=512, kernel_size=(3, 3), padding='same') self.b8 = BatchNormalization() # BN层1 self.a8 = Activation('relu') # 激活层1 self.c9 = Conv2D(filters=512, kernel_size=(3, 3), padding='same') self.b9 = BatchNormalization() # BN层1 self.a9 = Activation('relu') # 激活层1 self.c10 = Conv2D(filters=512, kernel_size=(3, 3), padding='same') self.b10 = BatchNormalization() self.a10 = Activation('relu') self.p4 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same') self.d4 = Dropout(0.2) self.c11 = Conv2D(filters=512, kernel_size=(3, 3), padding='same') self.b11 = BatchNormalization() # BN层1 self.a11 = Activation('relu') # 激活层1 self.c12 = Conv2D(filters=512, kernel_size=(3, 3), padding='same') self.b12 = BatchNormalization() # BN层1 self.a12 = Activation('relu') # 激活层1 self.c13 = Conv2D(filters=512, kernel_size=(3, 3), padding='same') self.b13 = BatchNormalization() self.a13 = Activation('relu') self.p5 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same') self.d5 = Dropout(0.2) self.flatten = Flatten() self.f1 = Dense(512, activation='relu') self.d6 = Dropout(0.2) self.f2 = Dense(512, activation='relu') self.d7 = Dropout(0.2) self.f3 = Dense(10, activation='softmax')\u0026lt;/code\u0026gt;\u0026lt;/pre\u0026gt; Inception Net Inception Net引入了Inception结构块，在同一层网络内使用不同尺寸的卷积核，提升了模型感知力，使用了批标准化，缓解了梯度消失 卷积连接器把这四个分支按照深度方向堆叠在一起，构成Inception结构块的输出\nResNet ResNet提出了层间残差跳连，引入了前方信息，缓解梯度消失，使神经网络层数增成为可能 如图，将前边的特征直接接到了后边，使这里的输出结果H(x)包含了堆叠卷积的非线性输出F(x)和跳过这两层堆叠卷积直接连接过来的恒等映射x，让他们对应元素相加，有效缓解了神经网络模型堆叠导致的退化，使神经网络可以想着更深层级发展。\nResNet块： ResNet块中有两种情况，一种情况用实线表示，两层堆叠卷积没有改变特征图的维度，可直接将F(x)与x相加 另一种情况用虚线表示，两层堆叠卷积改变了特征图的维度，需要借助1 * 1的卷积来调整x的维度\n网络结构： ResNet18的第一层是个卷积，然后是8个ResNet块，最后一个全连接层，每一个ResNet块有两层卷积，一共18层网络。 网络结构代码： class ResnetBlock(Model): def __init__(self, filters, strides=1, residual_path=False): super(ResnetBlock, self).__init__() self.filters = filters self.strides = strides self.residual_path = residual_path self.c1 = Conv2D(filters, (3, 3), strides=strides, padding='same', use_bias=False) self.b1 = BatchNormalization() self.a1 = Activation('relu') self.c2 = Conv2D(filters, (3, 3), strides=1, padding='same', use_bias=False) self.b2 = BatchNormalization() # residual_path为True时，对输入进行下采样，即用1x1的卷积核做卷积操作，保证x能和F(x)维度相同，顺利相加 if residual_path: self.down_c1 = Conv2D(filters, (1, 1), strides=strides, padding='same', use_bias=False) self.down_b1 = BatchNormalization() self.a2 = Activation('relu') def call(self, inputs): residual = inputs # residual等于输入值本身，即residual=x # 将输入通过卷积、BN层、激活层，计算F(x) x = self.c1(inputs) x = self.b1(x) x = self.a1(x) x = self.c2(x) y = self.b2(x) if self.residual_path: residual = self.down_c1(inputs) residual = self.down_b1(residual) out = self.a2(y + residual) # 最后输出的是两部分的和，即F(x)+x或F(x)+Wx,再过激活函数 return out class ResNet18(Model):\ndef __init__(self, block_list, initial_filters=64): # block_list表示每个block有几个卷积层 super(ResNet18, self).__init__() self.num_blocks = len(block_list) # 共有几个block self.block_list = block_list self.out_filters = initial_filters self.c1 = Conv2D(self.out_filters, (3, 3), strides=1, padding='same', use_bias=False) self.b1 = BatchNormalization() self.a1 = Activation('relu') self.blocks = tf.keras.models.Sequential() # 构建ResNet网络结构 for block_id in range(len(block_list)): # 第几个resnet block for layer_id in range(block_list[block_id]): # 第几个卷积层 if block_id != 0 and layer_id == 0: # 对除第一个block以外的每个block的输入进行下采样 block = ResnetBlock(self.out_filters, strides=2, residual_path=True) else: block = ResnetBlock(self.out_filters, residual_path=False) self.blocks.add(block) # 将构建好的block加入resnet self.out_filters *= 2 # 下一个block的卷积核数是上一个block的2倍 self.p1 = tf.keras.layers.GlobalAveragePooling2D()#平均全局池化 self.f1 = tf.keras.layers.Dense(10, activation='softmax', kernel_regularizer=tf.keras.regularizers.l2()) def call(self, inputs): x = self.c1(inputs) x = self.b1(x) x = self.a1(x) x = self.blocks(x) x = self.p1(x) y = self.f1(x) return y\u0026lt;/code\u0026gt;\u0026lt;/pre\u0026gt; ","permalink":"https://pp-tech-blog.pages.dev/posts/tensorflow_5/","summary":"卷积神经网络概念 卷积(Convolutional) • 卷积计算可认为是一种有效提取图像特征的方法 • 一般会用一个正方形的卷积核，按指定步长，在输入特征图上滑动，遍历输。","title":"tensorflow_5 卷积神经网络"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\nbaseline 在baseline基础上扩展\nimport tensorflow as tf mnist = tf.keras.datasets.mnist (x_train, y_train), (x_test, y_test) = mnist.load_data() x_train, x_test = x_train / 255.0, x_test / 255.0\nmodel = tf.keras.models.Sequential([ tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation=\u0026lsquo;relu\u0026rsquo;), tf.keras.layers.Dense(10, activation=\u0026lsquo;softmax\u0026rsquo;) ])\nmodel.compile(optimizer=\u0026lsquo;adam\u0026rsquo;, loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=[\u0026lsquo;sparse_categorical_accuracy\u0026rsquo;])\nmodel.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1) model.summary()\n一 自制数据集，解决本领域应用 MNIST数据集shape： x_train.shape:(60000, 28, 28) y_train.shape:(60000,) x_test.shape:(10000, 28, 28) y_test.shape:(10000,) 文件中数据格式：第一列是图片名，用来索引图片，第二列是标签值 通过本地数据自制数据库：\nimport tensorflow as tf from PIL import Image import numpy as np import os train_path = \u0026lsquo;./mnist_image_label/mnist_train_jpg_60000/\u0026rsquo; train_txt = \u0026lsquo;./mnist_image_label/mnist_train_jpg_60000.txt\u0026rsquo; x_train_savepath = \u0026lsquo;./mnist_image_label/mnist_x_train.npy\u0026rsquo; #训练集输入特征存储文件 y_train_savepath = \u0026lsquo;./mnist_image_label/mnist_y_train.npy\u0026rsquo; #训练集标签存储文件\ntest_path = \u0026lsquo;./mnist_image_label/mnist_test_jpg_10000/\u0026rsquo; test_txt = \u0026lsquo;./mnist_image_label/mnist_test_jpg_10000.txt\u0026rsquo; x_test_savepath = \u0026lsquo;./mnist_image_label/mnist_x_test.npy\u0026rsquo; #测试集输入特征存储文件 y_test_savepath = \u0026lsquo;./mnist_image_label/mnist_y_test.npy\u0026rsquo; #测试集标签存储文件\ndef generateds(path, txt): f = open(txt, \u0026lsquo;r\u0026rsquo;) # 以只读形式打开txt文件 contents = f.readlines() # 读取文件中所有行 f.close() # 关闭txt文件 x, y_ = [], [] # 建立空列表 for content in contents: # 逐行取出 value = content.split() # 以空格分开，图片路径为value[0] , 标签为value[1] , 存入列表 img_path = path + value[0] # 拼出图片路径和文件名 img = Image.open(img_path) # 读入图片 img = np.array(img.convert(\u0026lsquo;L\u0026rsquo;)) # 图片变为8位宽灰度值的np.array格式 img = img / 255. # 数据归一化 （实现预处理） x.append(img) # 归一化后的数据，贴到列表x y_.append(value[1]) # 标签贴到列表y_ print(\u0026rsquo;loading : \u0026rsquo; + content) # 打印状态提示\nx = np.array(x) # 变为np.array格式 y_ = np.array(y_) # 变为np.array格式 y_ = y_.astype(np.int64) # 变为64位整型 return x, y_ # 返回输入特征x，返回标签y_ if os.path.exists(x_train_savepath) and os.path.exists(y_train_savepath) and os.path.exists( x_test_savepath) and os.path.exists(y_test_savepath): print(\u0026rsquo;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-Load Datasets\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;\u0026rsquo;) x_train_save = np.load(x_train_savepath) y_train = np.load(y_train_savepath) x_test_save = np.load(x_test_savepath) y_test = np.load(y_test_savepath) x_train = np.reshape(x_train_save, (len(x_train_save), 28, 28)) x_test = np.reshape(x_test_save, (len(x_test_save), 28, 28)) else: print(\u0026rsquo;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-Generate Datasets\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;\u0026rsquo;) x_train, y_train = generateds(train_path, train_txt) x_test, y_test = generateds(test_path, test_txt)\nprint('-------------Save Datasets-----------------') x_train_save = np.reshape(x_train, (len(x_train), -1)) x_test_save = np.reshape(x_test, (len(x_test), -1)) np.save(x_train_savepath, x_train_save) np.save(y_train_savepath, y_train) np.save(x_test_savepath, x_test_save) np.save(y_test_savepath, y_test) model = tf.keras.models.Sequential([ tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation=\u0026lsquo;relu\u0026rsquo;), tf.keras.layers.Dense(10, activation=\u0026lsquo;softmax\u0026rsquo;) ])\nmodel.compile(optimizer=\u0026lsquo;adam\u0026rsquo;, loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=[\u0026lsquo;sparse_categorical_accuracy\u0026rsquo;])\nmodel.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1) model.summary()\n二 数据增强，扩充数据集 数据增强（增大数据量）函数：\nimage_gen_train = tf.keras.preprocessing.image.ImageDataGenerator( rescale = 所有数据将乘以该数值 rotation_range = 随机旋转角度数范围 width_shift_range = 随机宽度偏移量 height_shift_range = 随机高度偏移量 水平翻转：horizontal_flip = 是否随机水平翻转 随机缩放：zoom_range = 随机缩放的范围 [1-n，1+n] ) image_gen_train.fit(x_train) 这里fit函数需要输入一个四维数据，所以要对x_train做reshape，同时里面的参数变化如下： model.fit(x_train, y_train,batch_size=32, ……) 变为 model.fit(image_gen_train.flow(x_train, y_train,batch_size=32), ……)\n三 断点续训，存取模型 读取模型： load_weights(路径文件名） 代码：checkpoint_save_path = \"./checkpoint/mnist.ckpt\" #定义存放模型路径 if os.path.exists(checkpoint_save_path + '.index'): #生成ckpt文件时会同步生成索引表，所以通过索引表可以知道是否保存过模型参数 print('-------------load the model-----------------') model.load_weights(checkpoint_save_path) #读取模型参数 保存模型 保存模型可以使用tf的回调参数直接保存训练出来的模型参数：tf.keras.callbacks.ModelCheckpoint( filepath=路径文件名, save_weights_only=True/False, #是否只保留模型参数 save_best_only=True/False) #是否只保留最优结果 history = model.fit（ callbacks=[cp_callback] ）#fit中需要加入回调选项 代码：\ncp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path, save_weights_only=True, save_best_only=True) history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1, callbacks=[cp_callback])\n第一次运行之后能发现路径中有保存下来的checkpoint文件夹模型参数，再次运行能发现模型准确率是基于保存的模型参数的基础上继续提升的。\n四 参数提取，把参数存入文本 提取可训练参数 model.trainable_variables 返回模型中可训练的参数 要想看到这些参数，可用print打印，用set_printoptions设置print的打印效果 np.set_printoptions(threshold=超过多少省略显示) np.set_printoptions(threshold=np.inf) # np.inf表示无限大，代表打印过程中不使用省略号，所有内容都打印 代码：\nnp.set_printoptions(threshold=np.inf) print(model.trainable_variables) file = open('./weights.txt', 'w') for v in model.trainable_variables: file.write(str(v.name) + '\\n') file.write(str(v.shape) + '\\n') file.write(str(v.numpy()) + '\\n') file.close() 最后输出的参数： 五 acc/loss可视化，查看训练效果 在fit执行训练过程中，同步记录了训练集loss、测试集los是、训练集准确率、测试集准确率。可以用history.history提取出来\nhistory=model.fit(训练集数据, 训练集标签, batch_size= , epochs=, validation_split=用作测试数据的比例,validation_data=测试集, validation_freq=测试频率) history： 训练集loss： loss 测试集loss： val_loss 训练集准确率： sparse_categorical_accuracy 测试集准确率： val_sparse_categorical_accuracy\nacc = history.history['sparse_categorical_accuracy'] val_acc = history.history['val_sparse_categorical_accuracy'] loss = history.history['loss'] val_loss = history.history['val_loss'] 用plt.plot画出以上四个数据的曲线： 六 应用程序，给图识物 前向传播执行应用 predict(输入特征, batch_size=整数) 返回前向传播计算结果\n复现模型（前向传播）model = tf.keras.models.Sequential([ tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dense(10, activation='softmax’)]) 加载参数 model.load_weights(model_save_path) 预测结果 result = model.predict(x_predict) 实现代码：\nfrom PIL import Image import numpy as np import tensorflow as tf #模型参数路径 model_save_path = './checkpoint/mnist.ckpt' #复现网络 model = tf.keras.models.Sequential([ tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dense(10, activation='softmax')]) #加载参数 model.load_weights(model_save_path) #图像识别任务次数 preNum = int(input(\"input the number of test pictures:\")) #读入待识别图片 for i in range(preNum): image_path = input(\"the path of test picture:\") img = Image.open(image_path) img = img.resize((28, 28), Image.ANTIALIAS) #resize成标准尺寸 img_arr = np.array(img.convert('L')) #转化为灰度图 #数据预处理 img_arr = 255 - img_arr #训练数据黑底白字，预测数据白底黑字，所以要处理像素点颜色取反，255纯白，0纯黑 # for i in range(28): # for j in range(28): # if img_arr[i][j] \u0026lt; 200: #另一种数据预处理，将灰度值小于200的像素点变白色 # img_arr[i][j] = 255 #能在保留图片的有效信息过滤图片噪声 # else: # img_arr[i][j] = 0 #归一化 img_arr = img_arr / 255.0 print(\"img_arr:\",img_arr.shape) x_predict = img_arr[tf.newaxis, ...] #img_arr:(28,28)——\u0026gt;x_predicy:(1,28,28) print(\"x_predict:\",x_predict.shape) result = model.predict(x_predict) pred = tf.argmax(result, axis=1) print('\\n') tf.print(pred)\u0026lt;/code\u0026gt;\u0026lt;/pre\u0026gt; ","permalink":"https://pp-tech-blog.pages.dev/posts/tensorflow_4/","summary":"baseline 在baseline基础上扩展 import tensorflow as tf mnist = tf.keras.datasets.mnist (x_tra。","title":"tensorflow_4 网络八股扩展"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n一 神经网络搭建八股 用Tensorflow API：tf.keras搭建网络八股\n六步法 import 导入相关模块 train, test 指定训练集和测试集 model = tf.keras.models.Squential 搭建网络结构，逐层描述每层网络，相当于前向传播 model.compile 配置训练方法，告知训练时选择哪种优化器、损失函数、评测指标 model.fit 执行训练过程 model.summary 打印网络的结构和参数统计Squential Squential可以认为是个容器，其中封装了一个神经网络结构，要描述从输入层到输出层每一层的网络结构\n网络结构举例： 拉直层：tf.keras.layers.Flatten() 全连接层：tf.keras.layers.Dense(神经元个数，activation = \"激活函数\", kernel_regularizer = 哪种正则化) 激活函数和正则化方法见上一节 卷积层：tf.keras.layers.Conv2D(filters = 卷积核个数, kernel_size = 卷积核尺寸, strides = 卷积步长, padding = \"valid\" or \"same\") LSTM层：tf.keras.layers.LSTM()compile model.compile(optimizer = 优化器， loss = 损失函数， metrics = [\"准确率\"]) 其中参数可以选择上一节介绍的名字，例如优化器的\"sgd\"、\"adagrad\"等，也可以使用函数形式，比如tf.keras.optimizers.SGD(lr = 学习率， momentum = 动量参数)，使用函数的形式可以设定学习率、动量等超参数，具体的：\nOptimizer可选: ‘sgd’ or tf.keras.optimizers.SGD (lr=学习率,momentum=动量参数) ‘adagrad’ or tf.keras.optimizers.Adagrad (lr=学习率) ‘adadelta’ or tf.keras.optimizers.Adadelta (lr=学习率) ‘adam’ or tf.keras.optimizers.Adam (lr=学习率, beta_1=0.9, beta_2=0.999) loss可选: ‘mse’ or tf.keras.losses.MeanSquaredError() ‘sparse_categorical_crossentropy’ or tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False) Metrics可选: ‘accuracy’ ：y和y都是数值，如y=[1] y=[1] ‘categorical_accuracy’ ：y和y都是独热码(概率分布)，如y=[0,1,0] y=[0.256,0.695,0.048] ‘sparse_categorical_accuracy’ ：y是数值，y是独热码(概率分布),如y=[1] y=[0.256,0.695,0.048]，其中from_logits参数表示神经网络预测结果输出前是否经过概率分布，有 = False， 没有 = Truefit model.fit(训练集的输入特征， 训练集的标签， batch_size= , epochs= , validation_data = (测试集的输入特征， 测试集的标签)， validation_split = 从训练集划出多少比例给测试集， validation_freq = 多少次epoch测试一次 )\nclass MyModel Squential可以搭出顺序网络结构，无法写出一些带有跳连的非顺序网络结构，这时可以选择用类class搭建神经网络结构 class MyModel(Model) model = MyModel 二 iris代码复现 Squential # 1 import import tensorflow as tf from sklearn import datasets import numpy as np # 2 train test x_train = datasets.load_iris().data y_train = datasets.load_iris().target #设随机种子 np.random.seed(116) np.random.shuffle(x_train) np.random.seed(116) np.random.shuffle(y_train) tf.random.set_seed(116) # 3 models.Squential model = tf.keras.models.Sequential([ tf.keras.layers.Dense(3, activation='softmax', ker nel_regularizer=tf.keras.regularizers.l2()) ]) # 4 model.compile model.compile(optimizer=tf.keras.optimizers.SGD(lr=0.1), loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=['sparse_categorical_accuracy']) # 5 model.fit model.fit(x_train, y_train, batch_size=32, epochs=500, validation_split=0.2, validation_freq=20) # 6 model.summary model.summary() ### class 非顺序网络结构时 将上面的代码中的Squential部分改为class ```python #class MyModel class IrisModel(Model): def __init__(self): #如果想在进行子类的初始化的同时也继承父类的__init__(), 就需要在子类中显示地通过super()来调用父类的__init__()函数。 super(IrisModel, self).__init__() #super()是用于调用父类(超类)的一个方法 self.d1 = Dense(3, activation='softmax', kernel_regularizer=tf.keras.regularizers.l2()) def call(self, x): y = self.d1(x) return y model = IrisModel()\n三 MNIST数据集 MNIST数据集一共有7万张图片，都是28*28像素的手写数字图片，6万用于训练，1万用于测试。 导入MNIST数据集： mnist = tf.keras.datasets.mnist (x_train, y_train), (x_test, y_test) = mnist.load_data() 作为输入特征，输入神经网络时，要先将数据拉伸为一维数组(784个像素点) tf.keras.layers.Flatten() 四 训练MNIST数据集 Squential代码如下： import tensorflow as tf mnist = tf.keras.datasets.mnist (x_train, y_train), (x_test, y_test) = mnist.load_data() x_train, x_test = x_train / 255.0, x_test / 255.0 #归一化，使数据变为0-1之间的数值，更适合神经网络吸收\nmodel = tf.keras.models.Sequential([ tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation=\u0026lsquo;relu\u0026rsquo;), tf.keras.layers.Dense(10, activation=\u0026lsquo;softmax\u0026rsquo;) ])\nmodel.compile(optimizer=\u0026lsquo;adam\u0026rsquo;, loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=[\u0026lsquo;sparse_categorical_accuracy\u0026rsquo;])\nmodel.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1) model.summary()\nclass方法 将squential部分改写成class：\nclass MnistModel(Model): def __init__(self): super(MnistModel, self).__init__() self.flatten = Flatten() self.d1 = Dense(128, activation='relu') self.d2 = Dense(10, activation='softmax') def call(self, x): x = self.flatten(x) x = self.d1(x) y = self.d2(x) return y model = MnistModel()\n五 Fashion数据集 Fashion数据集数据集一共有7万张图片，都是28*28像素的灰度值数据，6万用于训练，1万用于测试。一共有是十个分类 训练方法同上mnist\n","permalink":"https://pp-tech-blog.pages.dev/posts/tensorflow_3/","summary":"一 神经网络搭建八股 用Tensorflow API：tf.keras搭建网络八股 六步法 import 导入相关模块 train, test 指定训练集和测试集 mode。","title":"tensorflow_3 神经网络八股"},{"content":"python装饰器\npython装饰器 python中函数的参数传递 python中对象的查找顺序：局部命名空间===\u0026gt;全局命名空间===\u0026gt;python内置\n一般函数的不同类型参数的排列顺序为：位置参数 ===\u0026gt; 默认参数 ===\u0026gt; *args ===\u0026gt; 关键字参数 ===\u0026gt; **kwargs\n闭包函数 当一个函数需要参数而又不可直接从函数的参数列表直接传递时，可在函数体外部给定参数，然后将参数与函数体整体缩进到另一个外部函数的内部，最后外部函数return内部函数的内存地址。\n注：也就是用外部函数用一个大麻袋包住参数和内部函数，内部函数的不可直接传递的参数通过外部函数从麻袋口投递。另外投递的参数不可处于全局命名空间。\ndef outter(): #投递参数 def inner(*args,**kwargs): #投递进来的参数 *args,**kwargs return inner #注意，这里是inner不是inner() 那么这就是闭包函数，闭包函数可实现参数的传递。\n初识装饰器 现有一场景：需要为某个函数扩展功能，有两点要求，一不可修改源代码，二不可修改函数调用方式。\ndef index(x, y, z): time.sleep(1) print(\"x is %s,y is %s,z is %s\" % (x, y, z)) return \"xyz\" def home(name): time.sleep(1) print(\u0026ldquo;Hello,%s!\u0026rdquo; % name) return name\n假设现在要添加一个计算index函数耗时的功能。\nimport time def timer(*args, **kwargs): start = time.time() index(*args, **kwargs) stop = time.time() print(\"time:\",stop - start) 直接在源代码基础上扩展功能，然后封装到函数里(这样修改了函数的调用方式) 原函数调用index(*args, **kwargs)，现函数调用timer(*args, **kwargs) 在上面的基础上，为多个函数扩展功能，这里为index和home函数添加计算耗时功能。\nimport time def timmer(func): def timer(*args, **kwargs):#这里传递参数是给func()使用的 start = time.time() func(*args, **kwargs) #func本身需要参数传入，此处意为func是index还是home #这里不可通过timer参数列表传入，所以使用闭包函数外包 stop = time.time() print(\"time:\",stop - start) return timer 这里还是修改了调用方式，改进如下： func = timmer(func) #func可以为其他任意函数\n虽然此func非彼func，但是从使用者角度来看，调用方式是一样的 现在还有问题是这个func装的还不对\nimport time def timmer(func): def timer(*args, **kwargs): start = time.time() res = func(*args, **kwargs) #将实际的func返回值赋值给res stop = time.time() print(\"time:\", stop - start) return res #返回res return timer 装的不对的地方在于，装的func实际执行的是timer函数，timer函数没写return的情况下 return默认返回None，要将实际的func的返回值返回。 装饰器语法糖 装饰器的语法糖：在被装饰的函数的上方一行(只能是上方的第一行)加上@和装饰器名称。 注意：装饰函数需要写在被装饰函数的前面。\nimport time # 装饰函数需要写在被装饰函数的前面。 def timmer(func): def timer(*args, **kwargs): start = time.time() res = func(*args, **kwargs) stop = time.time() print(\"time:\", stop - start) return res return timer 装饰器的语法糖：在被装饰的函数的上方一行(只能是上方的第一行)加上@和装饰器名称 @timmer # index= timmer(index) def index(x, y, z): time.sleep(1) print(\u0026ldquo;x is %s,y is %s,z is %s\u0026rdquo; % (x, y, z)) return \u0026ldquo;xyz\u0026rdquo;\n@timmer # home = timmer(home) def home(name): time.sleep(1) print(\u0026ldquo;Hello,%s!\u0026rdquo; % name) return name\n将timer函数装的更像func函数。被装饰过的index函数本质上是调用timer函数，所以被装饰过的index函数的函数属性诸如index.__name__,index.__doc__全部都是timer函数的函数属性。python提供了一个@wraps装饰器可以将func函数属性包装给timer函数。\nimport time from functools import wraps #导入wraps def timmer(func): @wraps(func) #装饰timer函数,注意这个装饰器是有参数装饰器，这里将func传入，将func函数属性包装给timer函数 def timer(*args, **kwargs): \"\"\"timer doc\"\"\" start = time.time() res = func(*args, **kwargs) stop = time.time() print(\"time:\", stop - start) return res return timer\u0026lt;/code\u0026gt;\u0026lt;/pre\u0026gt; 那么这时候就实现了对原func函数的调包，偷偷的将原index函数通过装饰器装饰出一个新的index函数，调用方式和以前一样，没有修改源代码，完成了功能扩展。\n有参数的装饰器 在前面的装饰器中，函数timer实现了对原函数func的功能扩展，但是在传递参数的过程中注意到，timer函数的参数为(*args,**kwargs),这样传递参数的目的是为了将参数原封不动的传递给func函数，这样就固定死了timer函数的参数，要对多个不同的func函数扩展timer功能时，func成了变量，但此时没办法通过timer函数传递func参数(func本身是一个变量，需要参数)，于是利用闭包函数传递参数。\n在前面扩展功能时计算index函数耗时，扩展的计时功能没有引入参数，当扩展的功能还需要参数的时候，参数该怎样传递进去。\ndef timmer(func): #(2) def timer(*args,**kwargs): #(1) #功能扩展...(需要参数传入，例如这里代码为print(a)) res = func(*args,**kwargs) return res return timer (1)处(*args,**kwargs)参数是固定的，无法添加参数 (2)处(func)是可以添加参数，但是我们使用装饰器的语法糖，传递参数会出现错误\n@timmer # index= timmer(index) 这里语法糖限制是将@timmer下面的index函数内存地址当做唯一的一个参数传入index=timmer(index内存地址) def index(x, y, z): # 在(2)处添加的参数因语法糖的限制不能顺利传递 解决方法可通过闭包的方式再\"嵌套\"一层函数\nimport time from functools import wraps def deco(a): def timmer(func): @wraps(func) def timer(*args, **kwargs): \"\"\"timer doc\"\"\" print(a) start = time.time() res = func(*args, **kwargs) stop = time.time() print(\"time:\", stop - start) return res return timer return timmer @deco(5) # @deco(a) \u0026mdash;\u0026gt; @timmer \u0026mdash;\u0026gt; index = timmer(index) \u0026mdash;\u0026gt; index = timer def index(x, y, z): \u0026ldquo;\u0026ldquo;\u0026ldquo;index doc\u0026rdquo;\u0026rdquo;\u0026rdquo; time.sleep(1) print(\u0026ldquo;x is %s,y is %s,z is %s\u0026rdquo; % (x, y, z)) return \u0026ldquo;xyz\u0026rdquo;\n注意:deco函数外不需要再用闭包\"嵌套\"函数，因为这里的deco函数的参数已经没有了限制，内部需要什么函数就在deco()里传递什么参数。\n","permalink":"https://pp-tech-blog.pages.dev/posts/pythondec/","summary":"python装饰器 python装饰器 python中函数的参数传递 python中对象的查找顺序：局部命名空间===\u0026gt;全局命名空间===\u0026gt;python内置 一般函数的不同。","title":"Python装饰器"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n一、TF2常用函数 1 tf.Variable #Variable将变量标记为可训练，被标记的变量会在反向传播中记录梯度信息，神经网络训练中，常用该函数标记待训练参数 w = tf.Variable(tf.random.normal([2,2], mean = 0, stddev = 1)) 2 tf.constant #创建一个张量 tf.constant(张量内容, dtype = 数据类型) a = tf.constant([[[1,3],[2,5]]], dtype=tf.int64) 3 tf.convert_to_tensor 将numpy的数据类型转换为Tensor数据类型\na = np.arange(0, 5) b = tf.convert_to_tensor(a, dtype=tf.int64) #print(\"a:\", a) 结果[0 1 2 3 4] 4 tf.zeros/ones/fill 创建指定元素的张量\na = tf.zeros([2, 3]) b = tf.ones(4) c = tf.fill([2, 2], 9) 5 tf.random.normal/truncated_normal 生成符合正太分布的随机数\n#生成符合正态分布的随机数,默认均值为0，标准差为1 d = tf.random.normal([3,3,3], mean = 0.5, stddev = 1) #截断式正态分布的随机数，取值在（u-2x, u+2x）之内，更集中 e = tf.random.truncated_normal([3,3,3],mean = 0.5, stddev = 1) 6 tf.random.uniform 生成平均分布随机数\n#minval和maxval之间的平均分布 f = tf.random.uniform([2,2],minval= 0, maxval = 1) 7 tf.constant 强制类型转化\nx1 = tf.constant([1., 2., 3.], dtype=tf.float64) print(\"x1:\", x1) x2 = tf.cast(x1, tf.int32) #强制类型转换 print(\"x2\", x2) 8 tf.reduce_mean/sum 求和/均值\nx = tf.constant([[1, 2, 3], [2, 2, 3]]) print(\"x:\", x) print(\"mean of x:\", tf.reduce_mean(x)) # 求x中所有数的均值 print(\"sum of x:\", tf.reduce_sum(x, axis=1)) # 求每一行的和 9 tf.add/subtract/multiply/divide 四则运算\na = tf.ones([1, 3]) b = tf.fill([1, 3], 3.) #四则运算（只有维度相同才可以做四则运算） print(\"a+b:\", tf.add(a, b)) print(\"a-b:\", tf.subtract(a, b)) print(\"a*b:\", tf.multiply(a, b)) print(\"b/a:\", tf.divide(b, a)) 10 tf.pow/square/sqrt 数学运算\na = tf.fill([1, 2], 3.) print(\"a的3次方:\", tf.pow(a, 3)) #tf.pow(x, n) x的n次方 print(\"a的平方:\", tf.square(a)) print(\"a的开方:\", tf.sqrt(a)) 11 tf.matmul 矩阵相乘\na = tf.ones([3, 2]) b = tf.fill([2, 3], 3.) # tf.matmul(a,b) 矩阵a，b相乘 print(\"a*b:\", tf.matmul(a, b)) 12 tf.data.Dataset.from_tensor_slices 标签配对\nfeatures = tf.constant([12, 23, 10, 17]) #特征 labels = tf.constant([0, 1, 1, 0]) #标签 # 特征和标签配对的函数，numpy和Tensor格式都可用该语句读入数据 dataset = tf.data.Dataset.from_tensor_slices((features, labels)) print(dataset) for element in dataset: print(element) #结果 #\u0026lt;TensorSliceDataset shapes: ((), ()), types: (tf.int32, tf.int32)\u0026gt; #(\u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=12\u0026gt;, \u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=0\u0026gt;) #(\u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=23\u0026gt;, \u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=1\u0026gt;) #(\u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=10\u0026gt;, \u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=1\u0026gt;) #(\u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=17\u0026gt;, \u0026lt;tf.Tensor: shape=(), dtype=int32, numpy=0\u0026gt;) 13 tf.GradientTape 函数对指定参数的求导\n#可用tf.GradientTape()实现某个函数对指定参数的求导运算 with tf.GradientTape() as tape: #with结构记录计算过程，gradient求出张量梯度 w = tf.Variable(tf.constant(3.0)) #待训练参数,用Variable将变量标记为可训练 loss = tf.pow(w, 2) grad = tape.gradient(loss, w) #loss：函数， w：对谁求导 print(grad) #结果 #tf.Tensor(6.0, shape=(), dtype=float32) loss对w求导=2w，w= 3., 2w = 6. 14 tf.one_hot 独热码 #独热码：分类问题中，常用独热码做标签，标记类别：1表示是，0表示非 #例 # 0白 1黑 2灰 #标签 1 #独热码（ 0. 1. 0.) #tf.one_hot(待转换数据，depth=几分类) classes = 3 #三分类 labels = tf.constant([1, 0, 2]) # 输入的元素值最小为0，最大为2 output = tf.one_hot(labels, depth=classes) #独热码 print(\"result of labels1:\", output) #结果： '''result of labels1: tf.Tensor( [[0. 1. 0.] [1. 0. 0.] [0. 0. 1.]], shape=(3, 3), dtype=float32)''' 15 tf.assign_sub 自减 #调用assign_sub前，先用tf.Variable定义变量w为可训练 x = tf.Variable(4) # assign_sub自减 x.assign_sub(1) print(\"x:\", x) # 4-1=3 16 tf.argmax/argmin 最值索引 test = np.array([[1, 2, 3], [2, 3, 4], [5, 4, 3], [8, 7, 2]]) print(\"每一列的最大值的索引：\", tf.argmax(test, axis=0)) # 返回每一列最大值的索引：tf.Tensor([3 3 1], shape=(3,), dtype=int64) print(\"每一行的最大值的索引\", tf.argmax(test, axis=1)) # 返回每一行最大值的索引：tf.Tensor([2 2 0 0], shape=(4,), dtype=int64) 二、神经网络设计过程 概念 MP模型：每个输入特征乘以线上的权重，再通过一个非线性函数，得到输出 简化MP模型：在输入特征乘以线上的权重之后加上偏置项b。y = x * w + b\n损失函数：预测值（y）与标准答案（y_）的差距 损失函数可以定量判断w，b的优劣，当损失函数输出最小值时，参数w、b会出现最优值 训练神经网络的过程就是找到一组参数w、b，使得损失函数最小。\n梯度下降法：延损失函数梯度下降的方向寻找损失函数的最小值，得到最优参数的方法。 梯度更新： w1 = w1 - lr w1_grad ，w1_grad为损失函数对于w1的偏导（梯度） b = b - lr b_grad ，b_grad损失函数对于b的偏导（梯度） lr: 学习率，梯度下降的速度，是一个超参数，过小将使得收敛过程变得十分缓慢，过大可能造成梯度在最小值附近震荡，甚至无法收敛\n过程 初始化：首先根据输入数据的维度和输出结果的维度确定模型参数w、b的维度，随机初始化所有参数\n前向传播：将训练集数据带入模型，计算出结果，如图所示 通过损失函数计算出预测值与真实值的差距\n反向传播（梯度下降法）：从后向前，逐层求损失函数对每层神经元参数的偏导数，迭代更新所有参数，一直到损失函数达到最小值\n测试：将测试集数据带入训练好的模型，将预测值与真实值比较并计算准确率\n三、实例_利用鸢尾花数据集，实现前向传播、反向传播，可视化loss曲线 运用上述函数及过程方法，实现对于鸢尾花的分类模型 代码如下：\n# -*- coding: UTF-8 -*- # 利用鸢尾花数据集，实现前向传播、反向传播，可视化loss曲线 导入所需模块 import tensorflow as tf from sklearn import datasets from matplotlib import pyplot as plt import numpy as np\n导入数据，分别为输入特征和标签 x_data = datasets.load_iris().data y_data = datasets.load_iris().target\n随机打乱数据（因为原始数据是顺序的，顺序不打乱会影响准确率） seed: 随机数种子，是一个整数，当设置之后，每次生成的随机数都一样（为方便教学，以保每位同学结果一致） np.random.seed(116) # 使用相同的seed，保证输入特征和标签一一对应 np.random.shuffle(x_data) np.random.seed(116) np.random.shuffle(y_data) tf.random.set_seed(116)\n将打乱后的数据集分割为训练集和测试集，训练集为前120行，测试集为后30行 x_train = x_data[:-30] y_train = y_data[:-30] x_test = x_data[-30:] y_test = y_data[-30:]\n转换x的数据类型，否则后面矩阵相乘时会因数据类型不一致报错 x_train = tf.cast(x_train, tf.float32) x_test = tf.cast(x_test, tf.float32)\nfrom_tensor_slices函数使输入特征和标签值一一对应。（把数据集分批次，每个批次batch组数据）喂入神经网络会以batch为单位喂入 train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(32) test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(32)\n生成神经网络的参数，4个输入特征故，输入层为4个输入节点；因为3分类，故输出层为3个神经元 用tf.Variable()标记参数可训练 使用seed使每次生成的随机数相同（方便教学，使大家结果都一致，在现实使用时不写seed） w1 = tf.Variable(tf.random.truncated_normal([4, 3], stddev=0.1, seed=1)) b1 = tf.Variable(tf.random.truncated_normal([3], stddev=0.1, seed=1))\nlr = 0.1 # 学习率为0.1 train_loss_results = [] # 将每轮的loss记录在此列表中，为后续画loss曲线提供数据 test_acc = [] # 将每轮的acc记录在此列表中，为后续画acc曲线提供数据 epoch = 500 # 循环500轮 loss_all = 0 # 每轮分4个step，loss_all记录四个step生成的4个loss的和\n训练部分 for epoch in range(epoch): #数据集级别的循环，每个epoch循环一次数据集 for step, (x_train, y_train) in enumerate(train_db): #batch级别的循环 ，每个step循环一个batch with tf.GradientTape() as tape: # with结构记录梯度信息 y = tf.matmul(x_train, w1) + b1 # 神经网络乘加运算 y = tf.nn.softmax(y) # 使输出y(预测值)符合概率分布（此操作后与独热码同量级，可相减求loss） y_ = tf.one_hot(y_train, depth=3) # 将标签值(标准答案)转换为独热码格式，方便计算loss和accuracy loss = tf.reduce_mean(tf.square(y_ - y)) # 采用均方误差损失函数mse = mean(sum(y-out)^2) loss_all += loss.numpy() # 将每个step计算出的loss累加，为后续求loss平均值提供数据，这样计算的loss更准确 # 计算loss对各个参数的梯度 grads = tape.gradient(loss, [w1, b1])\n# 实现梯度更新 w1 = w1 - lr * w1_grad b = b - lr * b_grad w1.assign_sub(lr * grads[0]) # 参数w1自更新 b1.assign_sub(lr * grads[1]) # 参数b自更新 # 每个epoch，打印loss信息 print(\u0026quot;Epoch {}, loss: {}\u0026quot;.format(epoch, loss_all/4)) train_loss_results.append(loss_all / 4) # 将4个step的loss求平均记录在此变量中 loss_all = 0 # loss_all归零，为记录下一个epoch的loss做准备 # 测试部分 # total_correct为预测对的样本个数, total_number为测试的总样本数，将这两个变量都初始化为0 total_correct, total_number = 0, 0 for x_test, y_test in test_db: # 使用更新后的参数进行预测 y = tf.matmul(x_test, w1) + b1 y = tf.nn.softmax(y) pred = tf.argmax(y, axis=1) # 返回y中最大值的索引，即预测的分类 # 将pred转换为y_test的数据类型 pred = tf.cast(pred, dtype=y_test.dtype) # 若分类正确，则correct=1，否则为0，将bool型的结果转换为int型 correct = tf.cast(tf.equal(pred, y_test), dtype=tf.int32) # 将每个batch的correct数加起来 correct = tf.reduce_sum(correct) # 将所有batch中的correct数加起来 total_correct += int(correct) # total_number为测试的总样本数，也就是x_test的行数，shape[0]返回变量的行数 total_number += x_test.shape[0] # 总的准确率等于total_correct/total_number acc = total_correct / total_number test_acc.append(acc) print(\u0026quot;Test_acc:\u0026quot;, acc) print(\u0026quot;--------------------------\u0026quot;) 绘制 loss 曲线 plt.title(\u0026lsquo;Loss Function Curve\u0026rsquo;) # 图片标题 plt.xlabel(\u0026lsquo;Epoch\u0026rsquo;) # x轴变量名称 plt.ylabel(\u0026lsquo;Loss\u0026rsquo;) # y轴变量名称 plt.plot(train_loss_results, label=\u0026quot;$Loss$\u0026quot;) # 逐点画出trian_loss_results值并连线，连线图标是Loss plt.legend() # 画出曲线图标 plt.show() # 画出图像\n绘制 Accuracy 曲线 plt.title(\u0026lsquo;Acc Curve\u0026rsquo;) # 图片标题 plt.xlabel(\u0026lsquo;Epoch\u0026rsquo;) # x轴变量名称 plt.ylabel(\u0026lsquo;Acc\u0026rsquo;) # y轴变量名称 plt.plot(test_acc, label=\u0026quot;$Accuracy$\u0026quot;) # 逐点画出test_acc值并连线，连线图标是Accuracy plt.legend() plt.show()\n结果如下： 经过185次迭代，使得模型分类的准确率达到100%\n损失函数曲线如图： 准确率变化曲线如图： ","permalink":"https://pp-tech-blog.pages.dev/posts/tensorflow_1/","summary":"一、TF2常用函数 1 tf.Variable #Variable将变量标记为可训练，被标记的变量会在反向传播中记录梯度信息，神经网络训练中，常用该函数标记待训练参数 w。","title":"tensorflow_1 神经网络计算"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n一 函数 tf.where(条件语句，真返回A，假返回B)\na = tf.constant([1, 2, 3, 1, 1]) b = tf.constant([0, 1, 3, 4, 5]) c = tf.where(tf.greater(a, b), a, b) # 若a\u0026gt;b，返回a对应位置的元素，否则返回b对应位置的元素 结果：tf.Tensor([1 2 3 4 5], shape=(5,), dtype=int32) np.random.RandomState.rand()\n#返回一个[0,1)之间的随机数 rdm = np.random.RandomState(seed=1) a = rdm.rand() b = rdm.rand(2, 3) #维度 np.vstack((a, b)) 将两个数组按垂直方向叠加\na = np.array([1, 2, 3]) b = np.array([4, 5, 6]) c = np.vstack((a, b)) output：c: [[1 2 3] [4 5 6]] np.mgrid()/np.ravel()/np.c_() 一起使用生成网格坐标点 np.mgrid[起始值：结束值：步长，起始值：结束值：步长，~] 返回若干组维度相同的等差数组，[起始值：结束值) x.ravel()将x变为一维数组 np.c_()使返回的间隔数值点配对\n# 生成等间隔数值点 x, y = np.mgrid[1:3:1, 2:4:0.5] # 将x, y拉直，并合并配对为二维张量，生成二维坐标点 grid = np.c_[x.ravel(), y.ravel()] print(\"x:\\n\", x) print(\"y:\\n\", y) print(\"x.ravel():\\n\", x.ravel()) print(\"y.ravel():\\n\", y.ravel()) print('grid:\\n', grid) output:x: [[1. 1. 1. 1.] [2. 2. 2. 2.]] y: [[2. 2.5 3. 3.5] [2. 2.5 3. 3.5]] x.ravel(): [1. 1. 1. 1. 2. 2. 2. 2.] y.ravel(): [2. 2.5 3. 3.5 2. 2.5 3. 3.5] grid: [[1. 2. ] [1. 2.5] [1. 3. ] [1. 3.5] [2. 2. ] [2. 2.5] [2. 3. ] [2. 3.5]] 二 概念 1 神经网络复杂度 神经网络(NN)复杂度:多用NN层数和NN参数的个数表示\n空间复杂度： 层数 = 隐藏层的层数 +1个输出层 上图为2层NN 总参数 = 总w + 总b 上图 3 4 + 4 + 4 2+2 = 26 时间复杂度 乘加运算次数 上图 3 4 + 4 2 = 20 2 指数衰减学习率 可以先用较大的学习率，快速得到较优解，然后逐步减小学习率，使模型在训练后期稳定 指数衰减学习率 = 初始学习率 * 学习率衰减率 ^(当前轮数/多少轮衰减一次) 粗体为超参数\nw = tf.Variable(tf.constant(5, dtype=tf.float32)) epoch = 40 LR_BASE = 0.2 # 最初学习率 LR_DECAY = 0.99 # 学习率衰减率 LR_STEP = 1 # 喂入多少轮BATCH_SIZE后，更新一次学习率 for epoch in range(epoch): # for epoch 定义顶层循环，表示对数据集循环epoch次，此例数据集数据仅有1个w,初始化时候constant赋值为5，循环40次迭代。 lr = LR_BASE * LR_DECAY ** (epoch / LR_STEP) #**代表次方 with tf.GradientTape() as tape: # with结构到grads框起了梯度的计算过程。 loss = tf.square(w + 1) grads = tape.gradient(loss, w) # .gradient函数告知谁对谁求导\nw.assign_sub(lr * grads) # .assign_sub 对变量做自减 即：w -= lr*grads 即 w = w - lr*grads print(\u0026quot;After %s epoch,w is %f,loss is %f,lr is %f\u0026quot; % (epoch, w.numpy(), loss, lr)) 可以看出随着迭代次数的增加，lr在递减\n3 激活函数 概念 MP模型 简化的MP模型是线性函数，多层神经网络还是线性函数，模型表达力不够。 上图中非线性函数叫做激活函数，激活函数的加入使得多层神经网络不再是输入x的线性组合，可以随层数的增加提升表达力。\n优秀的损失函数应具有的特性和输出值的范围 常用的激活函数 1. sigmoid函数 tf.nn.sigmoid(x) ——\u0026gt; f(x) = 1 / (1 + e^(-x)) sigmoid函数及其导数图像为 特点\n易造成梯度消失 深层神经网络更新参数时需要从输出层到输入层逐层进行链式求导，sigmoid函数的导数输出是0-0.25之间的小数，链式求导需要多层导数连续相乘，多个0-0.25连续相乘结果会趋于0，导致梯度消失，使得参数无法继续更新。 输出非0均值，收敛慢 幂运算复杂，训练时间长 2. tanh函数 tf.math.tanh(x) ——\u0026gt; f(x) = (1-e^(-2x))/(1+e^(-2x)) tanh函数及其导数图像为 特点\n输出是0均值 易造成梯度消失 类似于sigmoid函数 幂运算复杂，训练时间长 3. relu函数（首选） tf.nn.relu(x) ——\u0026gt; f(x) = max(x, 0) ——\u0026gt; f(x) = 0 ,x \u0026lt; 0\nf(x) = x ,x \u0026gt;= 0 relu函数及其导数图像为 优点\n解决了梯度消失问题（正区间） 只需判断输入是否大于0，计算速度快 收敛速度快于sigmoid和tanh 缺点 输出非0均值，收敛慢 Dead RelU 问题，某些神经元可能被激活，导致相关的参数永远不能被更新 输入特征是负数时，激活函数输出是0，反向传播得到的梯度是0，导致参数无法更新，造成神经元死亡。可以通过改进随机初始化，避免负数特征送入relu函数，设置更小的学习率，减少参数分布的巨大变化，避免训练中产生过多负数特征 4. Leaky relu函数 tf.nn.leaky_relu(x) ——\u0026gt; f(x) = max(ax, x) 图像 导数图像\n理论上有relu所有优点且不会有DEAD REALU问题，实际上并没有比relu更好，使用relu更多。\n总结： 首选relu激活函数 学习率设置较小值 输入特征标准化，即让输入特征满足以0为均值，1为标准差的正态分布 初始参数中心化，即让随机生成的参数满足以0为均值，（2/当前层输入特征个数）^(1/2)为标准差的正态分布 4 损失函数 损失函数就是预测值y与已知答案y_的差距，NN的优化目标就是计算出参数使得loss最小 主流loss有三种计算方法：\n1.均方误差mse(mean squared error) 均方误差mse：MSE( y , y ) = (∑( y - y )^2)/n loss_mse = tf.nn.reducemean(tf.square( y - y ))\n2.自定义损失函数 自定义损失函数： loss( y , y ) = ∑f( y , y )\n3. 交叉熵损失函数CE(cross entropy) 表征两个概率分布之间的距离 H(y, y) = -∑y * lny 函数：tf.losses.categoricalcrossentropy(y, y)\nloss_ce1 = tf.losses.categorical_crossentropy([1, 0], [0.6, 0.4]) loss_ce2 = tf.losses.categorical_crossentropy([1, 0], [0.8, 0.2]) print(\"loss_ce1:\", loss_ce1) #输出loss_ce1: tf.Tensor(0.5108256, shape=(), dtype=float32) print(\"loss_ce2:\", loss_ce2) #输出loss_ce2: tf.Tensor(0.22314353, shape=(), dtype=float32) #第二组损失函数更小 4. softmax与交叉熵结合 输出先通过softmax函数，再计算y与y_的交叉熵损失函数 函数：tf.nn.softmax_cross_entropy_withlogits(y, y)\n5 欠拟合与过拟合 欠拟合的解决方法： 增加输入特征项 增加网络参数 减少正则化参数 过拟合的解决方法： 数据清洗 增大训练集 采用正则化 增大正则化参数 正则化缓解过拟合 正则化再损失函数中引入模型复杂度指标，利用给w加权值，弱化了训练数据的噪声（一般不正则化b） 正则化后 loss = loss(y与y_) + REGULARIZER * loss(w) REGULARIZER：超参数，参数 w 在总loss中的比例，即正则化的权重 loss(w):需要正则化的参数 loss(w)的计算有两种方法\nL1正则化：L1_loss(w) = ∑|w| 大概率会使很多参数变为零，因此该方法可通过稀疏参数，即减少参数的数量，降低复杂度\nL2正则化：L2_loss(w) = 1/2*∑|w^2| 会使参数很接近零但不等于零，因此该方法课通过减小参数值的大小降低复杂度\nh1 = tf.matmul(x_train, w1) + b1 # 两层网络w1，b1 ；w2 b2 h1 = tf.nn.relu(h1) y = tf.matmul(h1, w2) + b2 # 采用均方误差损失函数mse = mean(sum(y-out)^2) loss_mse = tf.reduce_mean(tf.square(y_train - y)) # 添加L2正则化 loss_regularization = [] # tf.nn.l2_loss(w)=sum(w ** 2) / 2 loss_regularization.append(tf.nn.l2_loss(w1)) loss_regularization.append(tf.nn.l2_loss(w2)) #L2正则化求和 # loss_regularization = tf.reduce_sum(tf.stack(loss_regularization)) tf.stack增加一个维度 loss_regularization = tf.reduce_sum(loss_regularization) loss = loss_mse + 0.03 * loss_regularization #REGULARIZER = 0.03 正则化缓解过拟合实例 # 读入数据/标签 生成x_train y_train df = pd.read_csv('dot.csv') x_data = np.array(df[['x1', 'x2']]) y_data = np.array(df['y_c']) x_train = np.vstack(x_data).reshape(-1,2) #reshape成两列，行数自动生成 y_train = np.vstack(y_data).reshape(-1,1)\nY_c = [[\u0026lsquo;red\u0026rsquo; if y else \u0026lsquo;blue\u0026rsquo;] for y in y_train]\n转换x的数据类型，否则后面矩阵相乘时会因数据类型问题报错 x_train = tf.cast(x_train, tf.float32) y_train = tf.cast(y_train, tf.float32)\nfrom_tensor_slices函数切分传入的张量的第一个维度，生成相应的数据集，使输入特征和标签值一一对应 train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(32)\n生成神经网络的参数，输入层为2个神经元，隐藏层为11个神经元，1层隐藏层，输出层为1个神经元 用tf.Variable()保证参数可训练 w1 = tf.Variable(tf.random.normal([2, 11]), dtype=tf.float32) b1 = tf.Variable(tf.constant(0.01, shape=[11]))\nw2 = tf.Variable(tf.random.normal([11, 1]), dtype=tf.float32) b2 = tf.Variable(tf.constant(0.01, shape=[1]))\nlr = 0.01 # 学习率 epoch = 400 # 循环轮数\n训练部分 for epoch in range(epoch): for step, (x_train, y_train) in enumerate(train_db): with tf.GradientTape() as tape: # 记录梯度信息\nh1 = tf.matmul(x_train, w1) + b1 # 记录神经网络乘加运算 h1 = tf.nn.relu(h1) y = tf.matmul(h1, w2) + b2 # 采用均方误差损失函数mse = mean(sum(y-out)^2) loss = tf.reduce_mean(tf.square(y_train - y)) # 计算loss对各个参数的梯度 variables = [w1, b1, w2, b2] grads = tape.gradient(loss, variables) # 实现梯度更新 # w1 = w1 - lr * w1_grad tape.gradient是自动求导结果与[w1, b1, w2, b2] 索引为0，1，2，3 w1.assign_sub(lr * grads[0]) b1.assign_sub(lr * grads[1]) w2.assign_sub(lr * grads[2]) b2.assign_sub(lr * grads[3]) # 每20个epoch，打印loss信息 if epoch % 20 == 0: print('epoch:', epoch, 'loss:', float(loss)) 预测部分 print(\u0026quot;predict\u0026quot;)\nxx在-3到3之间以步长为0.01，yy在-3到3之间以步长0.01,生成间隔数值点 xx, yy = np.mgrid[-3:3:.1, -3:3:.1]\n将xx , yy拉直，并合并配对为二维张量，生成二维坐标点 grid = np.c_[xx.ravel(), yy.ravel()] grid = tf.cast(grid, tf.float32)\n将网格坐标点喂入神经网络，进行预测，probs为输出 probs = [] for x_test in grid: # 使用训练好的参数进行预测 h1 = tf.matmul([x_test], w1) + b1 h1 = tf.nn.relu(h1) y = tf.matmul(h1, w2) + b2 # y为预测结果 probs.append(y)\n取第0列给x1，取第1列给x2 x1 = x_data[:, 0] x2 = x_data[:, 1]\nprobs的shape调整成xx的样子 probs = np.array(probs).reshape(xx.shape) plt.scatter(x1, x2, color=np.squeeze(Y_c)) #squeeze去掉纬度是1的纬度,相当于去掉[[\u0026lsquo;red\u0026rsquo;],[\u0026lsquo;\u0026lsquo;blue]],内层括号变为[\u0026lsquo;red\u0026rsquo;,\u0026lsquo;blue\u0026rsquo;]\n把坐标xx yy和对应的值probs放入contour函数，给probs值为0.5的所有点上色 plt点show后 显示的是红蓝点的分界线 plt.contour(xx, yy, probs, levels=[.5]) plt.show()\n读入红蓝点，画出分割线，不包含正则化 结果 在代码中加入对w的正则化后\n# 添加l2正则化 loss_regularization = [] # tf.nn.l2_loss(w)=sum(w ** 2) / 2 loss_regularization.append(tf.nn.l2_loss(w1)) loss_regularization.append(tf.nn.l2_loss(w2)) # loss_regularization = tf.reduce_sum(tf.stack(loss_regularization)) loss_regularization = tf.reduce_sum(loss_regularization) loss = loss_mse + 0.03 * loss_regularization #REGULARIZER = 0.03 结果变为： 可以看到正则化有效的缓解了过拟合\n6 神经网络参数优化器 神经网络是基于连接的人工智能，当网络结构固定后，不同参数的选取对模型的表达力影响很大，优化器就是引导神经网络更新参数的工具。\n更新参数的步骤 基本变量：待优化参数w， 损失函数loss， 学习率lr， 每次迭代一个batch， 他，表示当前迭代的总次数\n计算t时刻损失函数对于当前参数的梯度：$g_t$ = $\\frac{\\partial loss}{\\partial w_t}$ 计算t时刻一阶栋梁$m_t$和二阶动量$V_t$ 计算t时刻下降梯度：$\\eta_t = l_r * m_t / \\sqrt V_t$ 计算t+1时刻参数：$w_{t+1} = w_t - \\eta_t = w_t - l_r * m_t / \\sqrt V_t$ 一阶动量：与梯度相关的函数 二阶动量：与梯度平方相关的函数 不同的优化器实质上只是定义了不同的一阶动量和二阶动量公式 常用的神经网络优化器 1.SGD(无momentum)随机梯度下降 也就是上一节的梯度更新公式\n# 计算loss对各个参数的梯度 grads = tape.gradient(loss, [w1, b1]) # 实现梯度更新 w1 = w1 - lr * w1_grad b = b - lr * b_grad w1.assign_sub(lr * grads[0]) # 参数w1自更新 b1.assign_sub(lr * grads[1]) # 参数b自更新 2.SGDM(含momentum的SGD) 在SGD基础上增加一阶动量 m_w, m_b = 0, 0 #w和b的一阶动量初始值都为0 beta = 0.9 # 计算loss对各个参数的梯度 grads = tape.gradient(loss, [w1, b1]) ########################################################################## # sgd-momentun m_w = beta * m_w + (1 - beta) * grads[0] m_b = beta * m_b + (1 - beta) * grads[1] w1.assign_sub(lr * m_w) b1.assign_sub(lr * m_b) 3.Adagrad 在SGD基础上增加二阶动量 v_w, v_b = 0, 0 # 计算loss对各个参数的梯度 grads = tape.gradient(loss, [w1, b1]) # adagrad v_w += tf.square(grads[0]) v_b += tf.square(grads[1]) w1.assign_sub(lr * grads[0] / tf.sqrt(v_w)) b1.assign_sub(lr * grads[1] / tf.sqrt(v_b)) 4.RMSprop 在SGD基础上增加二阶动量 V是各时刻梯度平方的指数滑动平均，使用指数滑动平均值计算，表征的是过去一段时间的平均值\nv_w, v_b = 0, 0 beta = 0.9 grads = tape.gradient(loss, [w1, b1]) # rmsprop v_w = beta * v_w + (1 - beta) * tf.square(grads[0]) v_b = beta * v_b + (1 - beta) * tf.square(grads[1]) w1.assign_sub(lr * grads[0] / tf.sqrt(v_w)) b1.assign_sub(lr * grads[1] / tf.sqrt(v_b)) 5.Adam 同时结合SGDM一阶动量和RMSProp二阶动量 m_w, m_b = 0, 0 v_w, v_b = 0, 0 beta1, beta2 = 0.9, 0.999 delta_w, delta_b = 0, 0 global_step = 0 grads = tape.gradient(loss, [w1, b1]) m_w = beta1 * m_w + (1 - beta1) * grads[0] m_b = beta1 * m_b + (1 - beta1) * grads[1] v_w = beta2 * v_w + (1 - beta2) * tf.square(grads[0]) v_b = beta2 * v_b + (1 - beta2) * tf.square(grads[1]) m_w_correction = m_w / (1 - tf.pow(beta1, int(global_step))) m_b_correction = m_b / (1 - tf.pow(beta1, int(global_step))) v_w_correction = v_w / (1 - tf.pow(beta2, int(global_step))) v_b_correction = v_b / (1 - tf.pow(beta2, int(global_step)))\nw1.assign_sub(lr * m_w_correction / tf.sqrt(v_w_correction)) b1.assign_sub(lr * m_b_correction / tf.sqrt(v_b_correction))\n优化器对比 优化器对比\n","permalink":"https://pp-tech-blog.pages.dev/posts/tensorflow_2/","summary":"一 函数 tf.where(条件语句，真返回A，假返回B) a = tf.constant([1, 2, 3, 1, 1]) b = tf.constant([0, 1,。","title":"tensorflow_2 神经网络优化"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\n1 概念 归一化： 把数据变成(0，1)或者（1，1）之间的小数。主要是为了数据处理方便提出来的，把数据映射到0～1范围之内处理，更加便捷快速。 把有量纲表达式变成无量纲表达式，便于不同单位或量级的指标能够进行比较和加权。归一化是一种简化计算的方式，即将有量纲的表达式，经过变换，化为无量纲的表达式，成为纯量。 preprocessing.MinMaxScaler(feature_range=(min, max)) 标准化： 在机器学习中，我们可能要处理不同种类的资料，例如，音讯和图片上的像素值，这些资料可能是高维度的，资料标准化后会使每个特征中的数值平均变为0(将每个特征的值都减掉原始资料中该特征的平均)、标准差变为1，这个方法被广泛的使用在许多机器学习算法中(例如：支持向量机、逻辑回归和类神经网络)。 中心化： 平均值为0，对标准差无要求 归一化和标准化的区别： 归一化是将样本的特征值转换到同一量纲下把数据映射到[0,1]或者[-1, 1]区间内，仅由变量的极值决定，因区间放缩法是归一化的一种。标准化是依照特征矩阵的列处理数据，其通过求z-score的方法，转换为标准正态分布，和整体样本分布相关，每个样本点都能对标准化产生影响。它们的相同点在于都能取消由于量纲不同引起的误差；都是一种线性变换，都是对向量X按照比例压缩再进行平移。 标准化和中心化的区别： 标准化是原始分数减去平均数然后除以标准差，中心化是原始分数减去平均数。 所以一般流程为先中心化再标准化。 无量纲：我的理解就是通过某种方法能去掉实际过程中的单位，从而简化计算。\n2 为什么要归一化/标准化？ 如前文所说，归一化/标准化实质是一种线性变换，线性变换有很多良好的性质，这些性质决定了对数据改变后不会造成“失效”，反而能提高数据的表现，这些性质是归一化/标准化的前提。比如有一个很重要的性质：线性变换不会改变原始数据的数值排序。\n（1）某些模型求解需要 1）在使用梯度下降的方法求解最优化问题时， 归一化/标准化后可以加快梯度下降的求解速度，即提升模型的收敛速度。如左图所示，未归一化/标准化时形成的等高线偏椭圆，迭代时很有可能走“之”字型路线（垂直长轴），从而导致迭代很多次才能收敛。而如右图对两个特征进行了归一化，对应的等高线就会变圆，在梯度下降进行求解时能较快的收敛。 2）一些分类器需要计算样本之间的距离(如欧氏距离)，例如KNN。如果一个特征值域范围非常大，那么距离计算就主要取决于这个特征，从而与实际情况相悖(比如这时实际情况是值域范围小的特征更重要)。\n（2）无量纲化 例如房子数量和收入，因为从业务层知道，这两者的重要性一样，所以把它们全部归一化。 这是从业务层面上作的处理。\n（3）避免数值问题 太大的数会引发数值问题。\n3 数据预处理时 3.1 归一化 （1）Min-Max Normalization x' = (x - X_min) / (X_max - X_min)\n（2）平均归一化 x' = (x - μ) / (MaxValue - MinValue) （1）和（2）有一个缺陷就是当有新数据加入时，可能导致max和min的变化，需要重新定义。\n（3）非线性归一化 1）对数函数转换：y = log10(x) 2）反余切函数转换：y = atan(x) * 2 / π （3）经常用在数据分化比较大的场景，有些数值很大，有些很小。通过一些数学函数，将原始值进行映射。该方法包括 log、指数，正切等。需要根据数据分布的情况，决定非线性函数的曲线，比如log(V, 2)还是log(V, 10)等。\n3.2 标准化 （1）Z-score规范化（标准差标准化 / 零均值标准化） x' = (x - μ)／σ\n3.3 中心化 x' = x - μ\n4 什么时候用归一化？什么时候用标准化？ 如果对输出结果范围有要求，用归一化。 如果数据较为稳定，不存在极端的最大最小值，用归一化。 如果数据存在异常值和较多噪音，用标准化，可以间接通过中心化避免异常值和极端值的影响。 5 哪些模型必须归一化/标准化？ （1）SVM 不同的模型对特征的分布假设是不一样的。比如SVM 用高斯核的时候，所有维度共用一个方差，这不就假设特征分布是圆的么，输入椭圆的就坑了人家，所以简单的归一化都还不够好，来杯白化才有劲。比如用树的时候就是各个维度各算各的切分点，没所谓。\n（2）KNN 需要度量距离的模型，一般在特征值差距较大时，都会进行归一化/标准化。不然会出现“大数吃小数”。\n（3）神经网络 1）数值问题 归一化/标准化可以避免一些不必要的数值问题。输入变量的数量级未致于会引起数值问题吧，但其实要引起也并不是那么困难。因为tansig（tanh）的非线性区间大约在[-1.7，1.7]。意味着要使神经元有效，tansig( w1x1 + w2x2 +b) 里的 w1x1 +w2x2 +b 数量级应该在 1 （1.7所在的数量级）左右。这时输入较大，就意味着权值必须较小，一个较大，一个较小，两者相乘，就引起数值问题了。 假如你的输入是421，你也许认为，这并不是一个太大的数，但因为有效权值大概会在1/421左右，例如0.00243，那么，在matlab里输入 421·0.00243 == 0.421·2.43，会发现不相等，这就是一个数值问题。\n2）求解需要 a. 初始化：在初始化时我们希望每个神经元初始化成有效的状态，tansig函数在[-1.7, 1.7]范围内有较好的非线性，所以我们希望函数的输入和神经元的初始化都能在合理的范围内使得每个神经元在初始时是有效的。（如果权值初始化在[-1,1]且输入没有归一化且过大，会使得神经元饱和） b. 梯度：以输入-隐层-输出这样的三层BP为例，我们知道对于输入-隐层权值的梯度有2ew(1-a^2)*x的形式（e是誤差，w是隐层到输出层的权重，a是隐层神经元的值，x是输入），若果输出层的数量级很大，会引起e的数量级很大，同理，w为了将隐层（数量级为1）映身到输出层，w也会很大，再加上x也很大的话，从梯度公式可以看出，三者相乘，梯度就非常大了。这时会给梯度的更新带来数值问题。 c. 学习率：由（2）中，知道梯度非常大，学习率就必须非常小，因此，学习率（学习率初始值）的选择需要参考输入的范围，不如直接将数据归一化，这样学习率就不必再根据数据范围作调整。 隐层到输出层的权值梯度可以写成 2ea，而输入层到隐层的权值梯度为 2ew(1-a^2)x ，受 x 和 w 的影响，各个梯度的数量级不相同，因此，它们需要的学习率数量级也就不相同。对w1适合的学习率，可能相对于w2来说会太小，若果使用适合w1的学习率，会导致在w2方向上步进非常慢，会消耗非常多的时间，而使用适合w2的学习率，对w1来说又太大，搜索不到适合w1的解。如果使用固定学习率，而数据没归一化，则后果可想而知。 d.搜索轨迹：已解释\n","permalink":"https://pp-tech-blog.pages.dev/posts/normalization_standardization/","summary":"1 概念 归一化： 把数据变成(0，1)或者（1，1）之间的小数。主要是为了数据处理方便提出来的，把数据映射到0～1范围之内处理，更加便捷快速。 把有量纲表达式变成无量纲表。","title":"归一化 （Normalization）、标准化 （Standardization）和中心化/零均值化 （Zero-centered）"},{"content":" 说明：这篇文章来自 2020-2021 年的 WordPress 备份。原始图片附件未随 XML 一起保存，旧服务器图片地址也已失效，因此这里保留正文并移除了失效图片。\nPycharm 常用快捷键 常用快捷键 快捷键功能 Ctrl + Q 快速查看文档 Ctrl + F1 显示错误描述或警告信息 ","permalink":"https://pp-tech-blog.pages.dev/posts/pycharm_shortkey/","summary":"Pycharm 常用快捷键 常用快捷键 快捷键 功能 Ctrl + Q 快速查看文档 Ctrl + F1 显示错误描述或警告信息 Ctrl + / 行注释（可选中多行） C。","title":"pycharm快捷键"},{"content":"编辑后台 这里是站内编辑入口。博客本体仍然是 Hugo 静态站，所以真正的内容写入会落到 GitHub 仓库，再由 Cloudflare 自动发布；但你以后不需要再记外部流程，直接从这个页面进去就行。\n打开在线编辑器 新建 Markdown 导入 Markdown 你平时怎么用 点击“打开在线编辑器”，登录 GitHub。 选中仓库 pppppppw/pp-tech-blog。 直接编辑 content/posts/ 里的文章。 保存后，GitHub Actions 会自动发布到 Cloudflare Pages。 如果你已经有 .md 文件 直接点“导入 Markdown”。\n上传位置固定在：\ncontent/posts/\n文章建议使用下面这类 frontmatter：\n--- title: \u0026#34;文章标题\u0026#34; date: 2026-04-08 lastmod: 2026-04-08 description: \u0026#34;一句话摘要\u0026#34; summary: \u0026#34;列表页摘要\u0026#34; tags: - \u0026#34;后端\u0026#34; - \u0026#34;系统设计\u0026#34; featured: false draft: false --- 直接查看内容仓库 仓库首页：https://github.com/pppppppw/pp-tech-blog 文章目录：https://github.com/pppppppw/pp-tech-blog/tree/main/content/posts 说明：这已经是站内入口版编辑后台。真正把完整编辑器直接嵌到页面里，需要额外托管一套 CMS 前端和 OAuth 流程；对你现在这套免费静态博客来说，稳定性和维护成本都不划算，所以我保留了最轻、最稳的方案。\n","permalink":"https://pp-tech-blog.pages.dev/admin/","summary":"站内文章编辑入口。","title":"编辑后台"},{"content":"后端 / 架构研发工程师，长期做复杂链路的平台化建设 我现在在百度搜索架构组，主要技术栈覆盖 Golang、C++、Python、PHP。过去几年做得最多的事情，是把训练、发布、干预、监控、回滚、任务调度这些容易碎片化的流程，收敛成统一、可追踪、可治理的平台能力。\n基本信息 关注领域：平台工程 / 分布式服务 / 稳定性治理 / 量化工程 联系邮箱：596662595@qq.com 在线简历：查看 PDF 经历 2022.07 - 至今 百度搜索架构组｜后端 / 架构研发工程师\n长期负责星图平台、GSSDA 与阿拉丁平台的架构设计与核心功能建设，集中处理模型训练平台、上线流程、任务队列、监控治理与 Agent 化通路。\n2020.07 - 2021.03 西安蒙特卡罗基金管理有限公司｜量化策略研究员（实习）\n参与股票、期货、可转债等量化策略研究，搭建本地量化数据平台与回测平台，积累了数据密集型系统建设经验。\n我做系统时最看重什么 复杂链路不能靠口口相传，应该沉淀成标准化系统能力。 能否稳定上线，比单次功能开发是否完成更重要。 平台不是做大而全，而是让接入成本下降、风险暴露提前、问题定位更快。 ","permalink":"https://pp-tech-blog.pages.dev/about/","summary":"\u003ch1 id=\"后端--架构研发工程师长期做复杂链路的平台化建设\"\u003e后端 / 架构研发工程师，长期做复杂链路的平台化建设\u003c/h1\u003e\n\u003cp\u003e我现在在百度搜索架构组，主要技术栈覆盖 Golang、C++、Python、PHP。过去几年做得最多的事情，是把训练、发布、干预、监控、回滚、任务调度这些容易碎片化的流程，收敛成统一、可追踪、可治理的平台能力。\u003c/p\u003e\n\u003ch2 id=\"基本信息\"\u003e基本信息\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e关注领域：平台工程 / 分布式服务 / 稳定性治理 / 量化工程\u003c/li\u003e\n\u003cli\u003e联系邮箱：596662595@qq.com\u003c/li\u003e\n\u003cli\u003e在线简历：\u003ca href=\"/resume_2026.pdf\"\u003e查看 PDF\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"经历\"\u003e经历\u003c/h2\u003e\n\u003ch3 id=\"202207---至今\"\u003e2022.07 - 至今\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e百度搜索架构组｜后端 / 架构研发工程师\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e长期负责星图平台、GSSDA 与阿拉丁平台的架构设计与核心功能建设，集中处理模型训练平台、上线流程、任务队列、监控治理与 Agent 化通路。\u003c/p\u003e\n\u003ch3 id=\"202007---202103\"\u003e2020.07 - 2021.03\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e西安蒙特卡罗基金管理有限公司｜量化策略研究员（实习）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e参与股票、期货、可转债等量化策略研究，搭建本地量化数据平台与回测平台，积累了数据密集型系统建设经验。\u003c/p\u003e\n\u003ch2 id=\"我做系统时最看重什么\"\u003e我做系统时最看重什么\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e复杂链路不能靠口口相传，应该沉淀成标准化系统能力。\u003c/li\u003e\n\u003cli\u003e能否稳定上线，比单次功能开发是否完成更重要。\u003c/li\u003e\n\u003cli\u003e平台不是做大而全，而是让接入成本下降、风险暴露提前、问题定位更快。\u003c/li\u003e\n\u003c/ul\u003e","title":"关于"}]