拿到这套书单和笔记的时候,很多人会问:机器学习、深度学习、凸优化、CUDA编程,这四个名字摆在一起,到底是拼盘还是体系?说实话,我刚接触时也以为它们是四个独立方向,直到把笔记整理到第三遍,才意识到这其实是一条链路——机器学习定问题、深度学习选模型、凸优化给算法、CUDA解决算力。这篇笔记合集不是知识百科全书,而是一个从业者从零到一跨过这四个领域后,沉淀下来的核心提纲、实操心得和避坑记录。无论你是刚入门的学生,还是被项目性能卡住想补数学与并行基础的开发者,照着这套笔记的核心脉络走,能少浪费很多时间。1. 为什么我会把机器学习、深度学习、凸优化和CUDA编程放在同一套笔记里1.1 四个方向不是独立学科,而是同一链条上的环节我在早期学习时有个很深的误区:把机器学习当算法工具箱,把深度学习当调参游戏,把凸优化当数学课,把CUDA当独立编程语言。结果就是学一样丢一样,按某个框架跑通一个Demo就觉得自己会了,换一个损失函数、换一个模型结构立刻卡壳。真正让我打通的是在复现某个算法时被逼着搞懂了每一步:给定数据,我们要找到一个函数让损失最小,这个损失函数带有大量参数,传统凸优化理论告诉你目标函数有什么性质、梯度下降在什么条件下能收敛、步长取多大才不飘;当模型变成多层神经网络时,理论上已经不再是凸问题,但计算梯度仍然靠反向传播,而这个计算在GPU上并行执行,就需要理解CUDA的线程模型和内存访问规律。于是我把笔记改成了四种颜色标注的统一笔记:黄色是建模问题,蓝色是数学性质,绿色是算法迭代,红色是硬件约束。一旦某一步跑不通,我可以从链路里定位问题,而不是在某个孤立的教科书章节里打转。1.2 学习顺序的建议与踩坑网上推荐的顺序五花八门,我实际走通的路径是:先学基础机器学习,知道什么是损失函数、过拟合、交叉验证;再学凸优化前几章,只要搞懂凸集、凸函数、梯度下降和次梯度这几个概念;接着进入深度学习,重点吃透反向传播;最后在项目需要加速训练和推理时再系统学CUDA。这样安排的好处是:每学一个技术都能立刻解决前面积累的疑问,不觉得抽象。我最初的错误顺序恰好相反,先啃凸优化教材,结果到处是拉格朗日对偶和KKT条件,没有机器学习背景支撑,完全不知道这些东西为什么要用;后来又先学CUDA写核函数,忽略了对目标函数的数学分析,导致写出的并行代码加速比甚至低于单线程CPU版本。所以我强烈建议,把CUDA放后面,让算力需求自然引导学习。如果你现在正好把四个方向同时摆在面前,不要试图在三个月内全部精通,先拿机器学习当主线,其他三个围绕主线穿插推进。2. 机器学习与深度学习笔记里最值得反复看的内容2.1 传统机器学习的骨架:偏差方差、交叉验证、正则化这一块我的笔记不多,但都是反复翻看的。机器学习本质是在假设空间里选模型,但现实中我们永远拿不到完美的真实函数,所以偏差方差分解是我记在笔记第一页的内容:总误差等于噪声加上偏差的平方加方差。偏差大说明模型太简单,欠拟合;方差大说明模型对数据扰动太敏感,过拟合。所以交叉验证不是可有可无的步骤,它是用来度量方差和选择超参数的方法。我在笔记里画了一张小表记录不同交叉验证的适用场景:简单留出法适合大数据量、K折适合中等数据、留一法适合小样本,分层采样千万别忘。正则化这段我记了一句非常管用的直觉:L1像一个直线刀刃,总能恰好切到坐标轴上,因此解稀疏;L2像一个光滑的圆球等势面,只会整体收缩到原点附近但不会变成精确的零。当年我不理解为什么L1能产生稀疏解,后来在笔记里补了一个二维等高线图,用几何视角一下就懂了。这些内容看着基础,但后来学深度学习的权重衰减、学近端梯度求解Lasso,全都靠这一点直觉打底。2.2 深度学习笔记核心:反向传播与四大件深度学习笔记我重写了不下三版,核心始终围绕反向传播。不管多么复杂的网络,原理只有一个链条:先正向计算,记下每层激活值;然后算损失对输出的梯度,逐层借助链式法则往前回传。很多初学者死记梯度公式,我用笔记里的一个最小网络帮助自己理解:假设一个样本经过权重w和偏置b得到z=wx+b,激活后a=sigmoid(z),损失L=交叉熵。求dL/dw,实际分三步——先算dL/da,再算da/dz,再算dz/dw,三个数一乘。写在纸上看起来是乘法,实际上这就是计算图上的消息传递。我记得当时在笔记里还补充了雅可比矩阵视角:每个操作都是一个雅可比,反向传播就是在整个链路上把这些雅可比矩阵乘起来。后来看各种框架源码,底层梯度代码全都在做这件事。深度学习笔记里我还总结了所谓“四大件”:数据预处理、网络结构、损失函数、优化器。数据量小或不干净,再好的网络也白搭;网络结构里残差连接、归一化层、注意力机制各自解决不同瓶颈;损失函数决定梯度信号的质量,比如二分类用交叉熵而不是均方误差,是因为均方误差配合sigmoid会让梯度出现饱和区。优化器的选择也很有意思:SGD能找到更平的极小值但需要精细调学习率,Adam自适应调节步长适合做初稿迅速验证模型能力。这些不是死知识,而是每次做项目都要做的决策。2.3 笔记中记录的一些“反直觉”结论真正让我觉得笔记有价值的,是一些一开始反直觉、后来想通了的内容。第一个是批归一化有效的原因,论文里一开始说它减少了内部协变量偏移,但后来许多实验发现即使打乱分布也能训练,我笔记里的结论是:它让损失函数的曲面更平滑,因此可以用更大学习率。第二个是残差连接的梯度视角:跨层短路让反向传播时梯度可以直接传到前面层,避免深层的衰减或爆炸。第三个是学习率调度,很多人忽略它,但我的经验是,固定学习率训练容易陷入局部震荡,用正余弦退火或者OneCycle,往往同样的网络只需一半epoch就能收敛。笔记里我曾经记录了某个图像分类实验:换了个学习率调度,验证准确率提高了接近4个百分点,这个差距比换网络结构还大,所以笔记把“学习率是唯一最重要的超参数”加粗了。3. CUDA编程笔记:从能跑到跑得快3.1 先搞懂GPU的并行模型再写kernel我一开始学CUDA很想马上写加法,结果看了半天线程索引就懵了。后来我沉下心来,先理解了GPU的执行模型,再写代码,事倍功半。所以在笔记第一章我就画了一个三层结构图:一个kernel启动时,会生成一个网格(Grid),网格里有若干线程块(Block),每个块里有若干线程(Thread)。块内的线程可以协作并共享显存中的数据,不同的块之间基本不能同步。真正运行的时候,GPU把线程块分配给流多处理器(SM),一个块里的线程以32个为一组称为一个Warp,half-warp和quarter-warp是历史遗留概念,现在的优化主要看Warp。我笔记里写了一个简单类比:GPU像一个大型工厂,Grid是整个订单,Block是车间,Thread是工人,SM是厂区的产线。工人能快速方便地传递小零件(共享内存),但不同车间交流成本极高(全局内存)。理解了这个,你就明白为什么要用二维三维索引了——因为我们处理的数据往往是矩阵、图像、体素,多维索引能贴合数据访问的局部性。这比硬背threadIdx.x和blockIdx.x的公式有用得多。3.2 一个向量加法的笔记示例笔记里始终保留一个最小可运行的向量加法示例,这是检验环境配置和基本语法的“hello world”。代码很简单,定义一个kernel,每个线程负责算一个元素:__global__ void vecAdd(float* A, float* B, float* C, int n) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx < n) C[idx] = A[idx] + B[idx]; } int main() { // 假设 n = 1024, blockSize = 256 dim3 grid((n + blockSize - 1) / blockSize); dim3 block(blockSize); vecAdd<<<grid, block>>>(A, B, C, n); }这段代码里最关键的笔记是:线程数不一定等于数据量,所以一定要加if (idx < n)越界保护。还有内存分配要用cudaMalloc,拷贝用cudaMemcpy,核函数调用后必须cudaDeviceSynchronize()等待执行完成。这些不过是API的常规操作,但我笔记里真正提醒自己的是另一个点:block和grid尺寸怎么选。block一般取128到512之间,如果是老一代显卡考虑128,较新架构可以取256或512,让每个SM的占用率尽量高。grid大小则按“总数据量除以block大小向上取整”来算。我见过很多人在索引公式上算错,就是因为没想清楚blockDim.x * gridDim.x和实际数据长度的关系。3.3 性能调优笔记的checklist跑通很容易,跑得快很费劲。我笔记里的CUDA部分有一页性能调优清单,按优先级排列。第一位是内存访问模式:GPU读取全局内存时,一条指令会让同一Warp里32个线程同时访问内存,如果它们访问的是连续的地址,硬件会合并成一次或几次事务;如果用A[k*width+threadIdx.x]和A[threadIdx.x*width+k]这种二维索引,前者连续,后者跨行步长访问,性能差距可达几倍甚至几十倍。所以写kernel的第一要务就是确保相邻线程访问连续地址。第二位是减少分支发散:Warp是统一执行单元,如果同一Warp内两条线程走进不同的if分支,会串行执行两个分支,等于一半线程在空转。所以尽量把分支控制在Warp边界以内,或者在分支前先做数据重组。第三位是占用率与寄存器限制:每个SM能同时驻留有限数量的线程块和线程,寄存器和共享内存都占用资源。如果你想提升占用率,就要限制每个线程的寄存器数(__launch_bounds__),并让块大小合理。第四位是共享内存的bank conflict:共享内存被划分成若干bank,当同一个warp里的多个线程同时访问同一个bank中不同地址时,会发生冲突,硬件强制串行化,用填充可以规避。这四条清单我从深拷贝的案例到矩阵乘法都一直在用,每次手动对照一遍都能发现可优化点。4. 凸优化笔记:为什么算法都在迭代4.1 从凸集凸函数到可微条件下的凸优化凸优化是整套笔记里数学味道最浓的部分,我当时的策略是“够用即可,抓住本质”。第一页就写了三个定义:一个集合是凸的,如果集合内任意两点的连线还在集合里;一个函数是凸的,如果定义域是凸集,且函数值满足f(θx+(1−θ)y) ≤ θf(x)+(1−θ)f(y),翻译成人话就是函数图像上的任意两点的连线在函数图像上方。对于二次可微函数,还有一个等价条件:Hessian矩阵半正定。这些定义看着绕,但我用一个生活类比记疯了:凸函数像一个碗,无论你往碗里哪个位置倒水,水最后都会流到同一个碗底;非凸函数像起伏的山脉,水流会困在某个山洼里。凸优化之所以重要,是因为局部最优就是全局最优。我在笔记里补了一个证明梗概:如果x*是局部最优,假设存在y使f(y)<f(x*),则利用凸性质,在x*和y之间的连线上的点函数值会比x*更小,这与局部最优矛盾。这个证明逻辑简短却震撼,让我彻底明白了为什么线性回归、岭回归、支持向量机这些模型能放心地反复迭代收敛。4.2 梯度下降的收敛性与步长选择很多人用梯度下降只是调用一个库函数,但一旦需要调参就会茫然。我的笔记里记录了最核心的收敛性条件:如果函数的梯度是L-Lipschitz连续的,即梯度的变化速度被常数L限制住,那么用固定步长t ≤ 1/L进行梯度下降,函数值会以O(1/k)的速度收敛。如果函数还被强凸性质主导,收敛速度会变成线性甚至二次。这些结论的直觉是:梯度下降每一步等于在当前位置做一个局部二次近似,步长太大跳出盆地,步长太小则原地徘徊。我笔记里的一个小实验:对函数f(x)=x²求导,梯度为2x,取步长0.9时收敛,取1.01时直接发散。这个阈值让我对“步长必须小于1/L”有了身体记忆。后来的深度学习实践中,虽然神经网络不是凸优化下的问题,但梯度下降的直觉依然适用。学习率可以从0.1开始,按损失曲线表现调整。如果震荡明显,就减半;如果平坦缓慢,就适当增大。这个调参策略背后就是L-Lipschitz的粗糙估计。4.3 从一阶到近端梯度与ADMM机器学习里很多目标函数不是光滑可微的,比如Lasso的L1正则项。我的笔记里在面对这一类问题时,做了特别标注:梯度不存在的点用次梯度代替,把迭代公式写为x_{k+1} = prox_{t·g}(x_k - t∇f(x_k))。近端投影算子的几何含义是“在让函数值尽量小的同时,又不想离开当前点太远”,对L1范数来说,它就是一个软阈值操作。我当时为了确认,手写了一次软阈值:输入一个数a,阈值λ,如果|a|≤λ则输出0,否则输出a符号乘以(|a|−λ)。就这么一个公式,把稀疏性讲得明明白白。再进阶是约束优化,笔记里ADMM部分我按“分解+对偶上升”两种思想来记。ADMM把大问题拆成多个子问题,每个子问题可以分别用简单算法处理。这在分布式机器学习、资源分配问题中极其常见。虽然看起来复杂,但只要把握住它是在交替更新原始变量和对偶变量,并引入惩罚项保证收敛,实际代码通常并不冗长。我在笔记里写了一个典型例子:共享变量问题下,各节点本地更新、汇总平均、更新对偶变量,三行核心更新循环,对应算法理解立刻变清晰。5. 笔记管理方法:我是怎么让几百条笔记不会吃灰的5.1 用自己的话重写公式和概念我见过太多人把教材抄一遍当成笔记,这种笔记复习时毫无亲切感。我整理这套笔记时有一个硬规矩:每个公式先用数学形式写下,然后在旁边用三五行大白话解释它到底在干什么。比如矩阵乘法,我写的是“左侧矩阵的每一行和右侧矩阵的每一列对应相乘相加”;比如反向传播中的链式法则,我写的是“把梯度顺着网络往回传,每穿过一个操作就乘一个局部导数”。这些解释也许不精炼,但因为是自己的语言,三个月后回头看依然能瞬间唤醒记忆。5.2 每篇笔记都配一个最小可运行Demo纯理论笔记很容易高开低走。我的方法:每一个重要知识点,尽量配一个几十行代码的最小示例。线性回归配sklearn和NumPy手写梯度下降;神经网络配一个单隐藏层分类器;CUDA配向量加法和矩阵乘法;凸优化配梯度下降、软阈值、ADMM求解稀疏问题的小程序。代码跑通后,再把输出结果和关键代码片段贴到笔记区。我后来复习时,只要看到某个概念就能想起当时跑的曲线,这种多模态记忆比死记硬背牢得多。5.3 定期按“问题索引”回顾笔记不能只按学科顺序组织,否则你遇到实际问题时根本想不起来在哪记过。我在整理这套笔记的后期,新增了一个“问题索引”页,把所有笔记按问题维度重新映射:比如“为什么梯度下降发散?”关联到步长、Lipschitz、数据归一化;“怎么加速训练?”关联到CUDA内存合并、混合精度、分布式;“什么是稀疏解?”关联到L1正则、软阈值、近端梯度。这样一来,当我完成某个项目后再回头扫一遍索引,能迅速定位到相关笔记,形成“问题→概念→代码→调优”的闭环,而不是只会按目录翻开某一章。6. 学习中踩过的坑和排查建议6.1 数学符号不统一导致的混乱教材和论文里,同一个概念可能有五花八门的符号:有的用X^T表示转置,有的用X';梯度有的是∇f,有的是grad f;次微分的记号更是千奇百怪。我第一次看不同资料时经常对不上号,在笔记里硬套公式也算错。后来我专门建了一张符号对照表,统一采用最主流的记号:向量加粗小写,矩阵用大写,梯度统一写∇f(x),内积用尖括号。每次从外部资料摘录内容,先转换记号再落笔。这个小改动让我的笔记连贯性大增,查阅效率提升明显。6.2 CUDA核函数写对了但性能很差有段时间我以为只要kernel正确就算搞定CUDA,结果写出来的矩阵乘法比CPU还慢。后来查资料才知道是访存没合并。当时我访问二维数组用的是按行扫描,但每个线程处理的是不同行,导致同一个Warp里的线程访问地址差距过大。把矩阵先转置或把线程索引映射调整为连续访问后,性能立刻提升了几倍。这个教训让我明白:GPU的瓶颈往往不是算力,而是显存带宽。此后我写任何kernel前都会先画一张内存访问草图再动手。如果你也遇到“加速比不到2倍”的问题,请优先检查内存访问模式,而不是纠结加减乘除的指令数。6.3 优化算法不收敛时先查数据预处理凸优化和深度学习中的不收敛,和环境的关系远超参数本身。我踩过最深的坑是:在没做数据标准化的情况下直接跑Lasso,迭代几千步依然不收敛;把每个特征缩放到均值0方差1后,几十步就收敛了。所以遇到算法发散、震荡、不降反升时,我的排查顺序是先看数据是否有NaN、是否标准归一化,再看目标函数是否写对,最后才调学习率。这个顺序我写在了笔记扉页,因为它为我省掉了无数次无效调参。机器学习、深度学习、凸优化、CUDA编程的笔记合集,说到底是我与自己对话的过程。当你把零散的知识点编织成一个可以反复验证的系统时,那些曾经面目模糊的公式和API才能成为你工具箱里的常备工具。我个人的体会是,不要追求一次学完,也不要怕笔记混乱,只要你坚持按“问题→原理→代码→性能”这条线去记录,几个月后再看,你会惊讶地发现原来自己已经能串起一整条技术链路。
拿到这套书单和笔记的时候,很多人会问:机器学习、深度学习、凸优化、CUDA编程,这四个名字摆在一起,到底是拼盘还是体系?说实话,我刚接触时也以为它们是四个独立方向,直到把笔记整理到第三遍,才意识到这其实是一条链路——机器学习定问题、深度学习选模型、凸优化给算法、CUDA解决算力。这篇笔记合集不是知识百科全书,而是一个从业者从零到一跨过这四个领域后,沉淀下来的核心提纲、实操心得和避坑记录。无论你是刚入门的学生,还是被项目性能卡住想补数学与并行基础的开发者,照着这套笔记的核心脉络走,能少浪费很多时间。1. 为什么我会把机器学习、深度学习、凸优化和CUDA编程放在同一套笔记里1.1 四个方向不是独立学科,而是同一链条上的环节我在早期学习时有个很深的误区:把机器学习当算法工具箱,把深度学习当调参游戏,把凸优化当数学课,把CUDA当独立编程语言。结果就是学一样丢一样,按某个框架跑通一个Demo就觉得自己会了,换一个损失函数、换一个模型结构立刻卡壳。真正让我打通的是在复现某个算法时被逼着搞懂了每一步:给定数据,我们要找到一个函数让损失最小,这个损失函数带有大量参数,传统凸优化理论告诉你目标函数有什么性质、梯度下降在什么条件下能收敛、步长取多大才不飘;当模型变成多层神经网络时,理论上已经不再是凸问题,但计算梯度仍然靠反向传播,而这个计算在GPU上并行执行,就需要理解CUDA的线程模型和内存访问规律。于是我把笔记改成了四种颜色标注的统一笔记:黄色是建模问题,蓝色是数学性质,绿色是算法迭代,红色是硬件约束。一旦某一步跑不通,我可以从链路里定位问题,而不是在某个孤立的教科书章节里打转。1.2 学习顺序的建议与踩坑网上推荐的顺序五花八门,我实际走通的路径是:先学基础机器学习,知道什么是损失函数、过拟合、交叉验证;再学凸优化前几章,只要搞懂凸集、凸函数、梯度下降和次梯度这几个概念;接着进入深度学习,重点吃透反向传播;最后在项目需要加速训练和推理时再系统学CUDA。这样安排的好处是:每学一个技术都能立刻解决前面积累的疑问,不觉得抽象。我最初的错误顺序恰好相反,先啃凸优化教材,结果到处是拉格朗日对偶和KKT条件,没有机器学习背景支撑,完全不知道这些东西为什么要用;后来又先学CUDA写核函数,忽略了对目标函数的数学分析,导致写出的并行代码加速比甚至低于单线程CPU版本。所以我强烈建议,把CUDA放后面,让算力需求自然引导学习。如果你现在正好把四个方向同时摆在面前,不要试图在三个月内全部精通,先拿机器学习当主线,其他三个围绕主线穿插推进。2. 机器学习与深度学习笔记里最值得反复看的内容2.1 传统机器学习的骨架:偏差方差、交叉验证、正则化这一块我的笔记不多,但都是反复翻看的。机器学习本质是在假设空间里选模型,但现实中我们永远拿不到完美的真实函数,所以偏差方差分解是我记在笔记第一页的内容:总误差等于噪声加上偏差的平方加方差。偏差大说明模型太简单,欠拟合;方差大说明模型对数据扰动太敏感,过拟合。所以交叉验证不是可有可无的步骤,它是用来度量方差和选择超参数的方法。我在笔记里画了一张小表记录不同交叉验证的适用场景:简单留出法适合大数据量、K折适合中等数据、留一法适合小样本,分层采样千万别忘。正则化这段我记了一句非常管用的直觉:L1像一个直线刀刃,总能恰好切到坐标轴上,因此解稀疏;L2像一个光滑的圆球等势面,只会整体收缩到原点附近但不会变成精确的零。当年我不理解为什么L1能产生稀疏解,后来在笔记里补了一个二维等高线图,用几何视角一下就懂了。这些内容看着基础,但后来学深度学习的权重衰减、学近端梯度求解Lasso,全都靠这一点直觉打底。2.2 深度学习笔记核心:反向传播与四大件深度学习笔记我重写了不下三版,核心始终围绕反向传播。不管多么复杂的网络,原理只有一个链条:先正向计算,记下每层激活值;然后算损失对输出的梯度,逐层借助链式法则往前回传。很多初学者死记梯度公式,我用笔记里的一个最小网络帮助自己理解:假设一个样本经过权重w和偏置b得到z=wx+b,激活后a=sigmoid(z),损失L=交叉熵。求dL/dw,实际分三步——先算dL/da,再算da/dz,再算dz/dw,三个数一乘。写在纸上看起来是乘法,实际上这就是计算图上的消息传递。我记得当时在笔记里还补充了雅可比矩阵视角:每个操作都是一个雅可比,反向传播就是在整个链路上把这些雅可比矩阵乘起来。后来看各种框架源码,底层梯度代码全都在做这件事。深度学习笔记里我还总结了所谓“四大件”:数据预处理、网络结构、损失函数、优化器。数据量小或不干净,再好的网络也白搭;网络结构里残差连接、归一化层、注意力机制各自解决不同瓶颈;损失函数决定梯度信号的质量,比如二分类用交叉熵而不是均方误差,是因为均方误差配合sigmoid会让梯度出现饱和区。优化器的选择也很有意思:SGD能找到更平的极小值但需要精细调学习率,Adam自适应调节步长适合做初稿迅速验证模型能力。这些不是死知识,而是每次做项目都要做的决策。2.3 笔记中记录的一些“反直觉”结论真正让我觉得笔记有价值的,是一些一开始反直觉、后来想通了的内容。第一个是批归一化有效的原因,论文里一开始说它减少了内部协变量偏移,但后来许多实验发现即使打乱分布也能训练,我笔记里的结论是:它让损失函数的曲面更平滑,因此可以用更大学习率。第二个是残差连接的梯度视角:跨层短路让反向传播时梯度可以直接传到前面层,避免深层的衰减或爆炸。第三个是学习率调度,很多人忽略它,但我的经验是,固定学习率训练容易陷入局部震荡,用正余弦退火或者OneCycle,往往同样的网络只需一半epoch就能收敛。笔记里我曾经记录了某个图像分类实验:换了个学习率调度,验证准确率提高了接近4个百分点,这个差距比换网络结构还大,所以笔记把“学习率是唯一最重要的超参数”加粗了。3. CUDA编程笔记:从能跑到跑得快3.1 先搞懂GPU的并行模型再写kernel我一开始学CUDA很想马上写加法,结果看了半天线程索引就懵了。后来我沉下心来,先理解了GPU的执行模型,再写代码,事倍功半。所以在笔记第一章我就画了一个三层结构图:一个kernel启动时,会生成一个网格(Grid),网格里有若干线程块(Block),每个块里有若干线程(Thread)。块内的线程可以协作并共享显存中的数据,不同的块之间基本不能同步。真正运行的时候,GPU把线程块分配给流多处理器(SM),一个块里的线程以32个为一组称为一个Warp,half-warp和quarter-warp是历史遗留概念,现在的优化主要看Warp。我笔记里写了一个简单类比:GPU像一个大型工厂,Grid是整个订单,Block是车间,Thread是工人,SM是厂区的产线。工人能快速方便地传递小零件(共享内存),但不同车间交流成本极高(全局内存)。理解了这个,你就明白为什么要用二维三维索引了——因为我们处理的数据往往是矩阵、图像、体素,多维索引能贴合数据访问的局部性。这比硬背threadIdx.x和blockIdx.x的公式有用得多。3.2 一个向量加法的笔记示例笔记里始终保留一个最小可运行的向量加法示例,这是检验环境配置和基本语法的“hello world”。代码很简单,定义一个kernel,每个线程负责算一个元素:__global__ void vecAdd(float* A, float* B, float* C, int n) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx < n) C[idx] = A[idx] + B[idx]; } int main() { // 假设 n = 1024, blockSize = 256 dim3 grid((n + blockSize - 1) / blockSize); dim3 block(blockSize); vecAdd<<<grid, block>>>(A, B, C, n); }这段代码里最关键的笔记是:线程数不一定等于数据量,所以一定要加if (idx < n)越界保护。还有内存分配要用cudaMalloc,拷贝用cudaMemcpy,核函数调用后必须cudaDeviceSynchronize()等待执行完成。这些不过是API的常规操作,但我笔记里真正提醒自己的是另一个点:block和grid尺寸怎么选。block一般取128到512之间,如果是老一代显卡考虑128,较新架构可以取256或512,让每个SM的占用率尽量高。grid大小则按“总数据量除以block大小向上取整”来算。我见过很多人在索引公式上算错,就是因为没想清楚blockDim.x * gridDim.x和实际数据长度的关系。3.3 性能调优笔记的checklist跑通很容易,跑得快很费劲。我笔记里的CUDA部分有一页性能调优清单,按优先级排列。第一位是内存访问模式:GPU读取全局内存时,一条指令会让同一Warp里32个线程同时访问内存,如果它们访问的是连续的地址,硬件会合并成一次或几次事务;如果用A[k*width+threadIdx.x]和A[threadIdx.x*width+k]这种二维索引,前者连续,后者跨行步长访问,性能差距可达几倍甚至几十倍。所以写kernel的第一要务就是确保相邻线程访问连续地址。第二位是减少分支发散:Warp是统一执行单元,如果同一Warp内两条线程走进不同的if分支,会串行执行两个分支,等于一半线程在空转。所以尽量把分支控制在Warp边界以内,或者在分支前先做数据重组。第三位是占用率与寄存器限制:每个SM能同时驻留有限数量的线程块和线程,寄存器和共享内存都占用资源。如果你想提升占用率,就要限制每个线程的寄存器数(__launch_bounds__),并让块大小合理。第四位是共享内存的bank conflict:共享内存被划分成若干bank,当同一个warp里的多个线程同时访问同一个bank中不同地址时,会发生冲突,硬件强制串行化,用填充可以规避。这四条清单我从深拷贝的案例到矩阵乘法都一直在用,每次手动对照一遍都能发现可优化点。4. 凸优化笔记:为什么算法都在迭代4.1 从凸集凸函数到可微条件下的凸优化凸优化是整套笔记里数学味道最浓的部分,我当时的策略是“够用即可,抓住本质”。第一页就写了三个定义:一个集合是凸的,如果集合内任意两点的连线还在集合里;一个函数是凸的,如果定义域是凸集,且函数值满足f(θx+(1−θ)y) ≤ θf(x)+(1−θ)f(y),翻译成人话就是函数图像上的任意两点的连线在函数图像上方。对于二次可微函数,还有一个等价条件:Hessian矩阵半正定。这些定义看着绕,但我用一个生活类比记疯了:凸函数像一个碗,无论你往碗里哪个位置倒水,水最后都会流到同一个碗底;非凸函数像起伏的山脉,水流会困在某个山洼里。凸优化之所以重要,是因为局部最优就是全局最优。我在笔记里补了一个证明梗概:如果x*是局部最优,假设存在y使f(y)<f(x*),则利用凸性质,在x*和y之间的连线上的点函数值会比x*更小,这与局部最优矛盾。这个证明逻辑简短却震撼,让我彻底明白了为什么线性回归、岭回归、支持向量机这些模型能放心地反复迭代收敛。4.2 梯度下降的收敛性与步长选择很多人用梯度下降只是调用一个库函数,但一旦需要调参就会茫然。我的笔记里记录了最核心的收敛性条件:如果函数的梯度是L-Lipschitz连续的,即梯度的变化速度被常数L限制住,那么用固定步长t ≤ 1/L进行梯度下降,函数值会以O(1/k)的速度收敛。如果函数还被强凸性质主导,收敛速度会变成线性甚至二次。这些结论的直觉是:梯度下降每一步等于在当前位置做一个局部二次近似,步长太大跳出盆地,步长太小则原地徘徊。我笔记里的一个小实验:对函数f(x)=x²求导,梯度为2x,取步长0.9时收敛,取1.01时直接发散。这个阈值让我对“步长必须小于1/L”有了身体记忆。后来的深度学习实践中,虽然神经网络不是凸优化下的问题,但梯度下降的直觉依然适用。学习率可以从0.1开始,按损失曲线表现调整。如果震荡明显,就减半;如果平坦缓慢,就适当增大。这个调参策略背后就是L-Lipschitz的粗糙估计。4.3 从一阶到近端梯度与ADMM机器学习里很多目标函数不是光滑可微的,比如Lasso的L1正则项。我的笔记里在面对这一类问题时,做了特别标注:梯度不存在的点用次梯度代替,把迭代公式写为x_{k+1} = prox_{t·g}(x_k - t∇f(x_k))。近端投影算子的几何含义是“在让函数值尽量小的同时,又不想离开当前点太远”,对L1范数来说,它就是一个软阈值操作。我当时为了确认,手写了一次软阈值:输入一个数a,阈值λ,如果|a|≤λ则输出0,否则输出a符号乘以(|a|−λ)。就这么一个公式,把稀疏性讲得明明白白。再进阶是约束优化,笔记里ADMM部分我按“分解+对偶上升”两种思想来记。ADMM把大问题拆成多个子问题,每个子问题可以分别用简单算法处理。这在分布式机器学习、资源分配问题中极其常见。虽然看起来复杂,但只要把握住它是在交替更新原始变量和对偶变量,并引入惩罚项保证收敛,实际代码通常并不冗长。我在笔记里写了一个典型例子:共享变量问题下,各节点本地更新、汇总平均、更新对偶变量,三行核心更新循环,对应算法理解立刻变清晰。5. 笔记管理方法:我是怎么让几百条笔记不会吃灰的5.1 用自己的话重写公式和概念我见过太多人把教材抄一遍当成笔记,这种笔记复习时毫无亲切感。我整理这套笔记时有一个硬规矩:每个公式先用数学形式写下,然后在旁边用三五行大白话解释它到底在干什么。比如矩阵乘法,我写的是“左侧矩阵的每一行和右侧矩阵的每一列对应相乘相加”;比如反向传播中的链式法则,我写的是“把梯度顺着网络往回传,每穿过一个操作就乘一个局部导数”。这些解释也许不精炼,但因为是自己的语言,三个月后回头看依然能瞬间唤醒记忆。5.2 每篇笔记都配一个最小可运行Demo纯理论笔记很容易高开低走。我的方法:每一个重要知识点,尽量配一个几十行代码的最小示例。线性回归配sklearn和NumPy手写梯度下降;神经网络配一个单隐藏层分类器;CUDA配向量加法和矩阵乘法;凸优化配梯度下降、软阈值、ADMM求解稀疏问题的小程序。代码跑通后,再把输出结果和关键代码片段贴到笔记区。我后来复习时,只要看到某个概念就能想起当时跑的曲线,这种多模态记忆比死记硬背牢得多。5.3 定期按“问题索引”回顾笔记不能只按学科顺序组织,否则你遇到实际问题时根本想不起来在哪记过。我在整理这套笔记的后期,新增了一个“问题索引”页,把所有笔记按问题维度重新映射:比如“为什么梯度下降发散?”关联到步长、Lipschitz、数据归一化;“怎么加速训练?”关联到CUDA内存合并、混合精度、分布式;“什么是稀疏解?”关联到L1正则、软阈值、近端梯度。这样一来,当我完成某个项目后再回头扫一遍索引,能迅速定位到相关笔记,形成“问题→概念→代码→调优”的闭环,而不是只会按目录翻开某一章。6. 学习中踩过的坑和排查建议6.1 数学符号不统一导致的混乱教材和论文里,同一个概念可能有五花八门的符号:有的用X^T表示转置,有的用X';梯度有的是∇f,有的是grad f;次微分的记号更是千奇百怪。我第一次看不同资料时经常对不上号,在笔记里硬套公式也算错。后来我专门建了一张符号对照表,统一采用最主流的记号:向量加粗小写,矩阵用大写,梯度统一写∇f(x),内积用尖括号。每次从外部资料摘录内容,先转换记号再落笔。这个小改动让我的笔记连贯性大增,查阅效率提升明显。6.2 CUDA核函数写对了但性能很差有段时间我以为只要kernel正确就算搞定CUDA,结果写出来的矩阵乘法比CPU还慢。后来查资料才知道是访存没合并。当时我访问二维数组用的是按行扫描,但每个线程处理的是不同行,导致同一个Warp里的线程访问地址差距过大。把矩阵先转置或把线程索引映射调整为连续访问后,性能立刻提升了几倍。这个教训让我明白:GPU的瓶颈往往不是算力,而是显存带宽。此后我写任何kernel前都会先画一张内存访问草图再动手。如果你也遇到“加速比不到2倍”的问题,请优先检查内存访问模式,而不是纠结加减乘除的指令数。6.3 优化算法不收敛时先查数据预处理凸优化和深度学习中的不收敛,和环境的关系远超参数本身。我踩过最深的坑是:在没做数据标准化的情况下直接跑Lasso,迭代几千步依然不收敛;把每个特征缩放到均值0方差1后,几十步就收敛了。所以遇到算法发散、震荡、不降反升时,我的排查顺序是先看数据是否有NaN、是否标准归一化,再看目标函数是否写对,最后才调学习率。这个顺序我写在了笔记扉页,因为它为我省掉了无数次无效调参。机器学习、深度学习、凸优化、CUDA编程的笔记合集,说到底是我与自己对话的过程。当你把零散的知识点编织成一个可以反复验证的系统时,那些曾经面目模糊的公式和API才能成为你工具箱里的常备工具。我个人的体会是,不要追求一次学完,也不要怕笔记混乱,只要你坚持按“问题→原理→代码→性能”这条线去记录,几个月后再看,你会惊讶地发现原来自己已经能串起一整条技术链路。