[{"content":"在学习并且整理完MIT 6.S184后，我开始了MIT 6.S978的学习。\n这是一门研究生课程，课程内容以论文阅读为主，但是由于我仅仅是对这个方向感兴趣，而all-in的话风险太高了，所以就没有读论文。\n课程的6个assignment以生成MNIST手写数字图片为主线。\nassignment1的内容是实现AutoEncoder以及Variational AutoEncoder。 assignment2的内容是针对的是AutoRegressive，然后要用pixelCNN来生成图片，这次作业令我印象比较深刻的有两点： AR在语言生成模型中是统治级别的存在，但是在图像生成中，由于它不满足构图原则的先验知识，所以最终的效果宛如鬼画符 第一次直观感受到AR在训练的时候防止数据泄露有多重要了：训练的时候loss为0生成图像也很标准，但是测试的时候生成的全是黑色，debug半天才发现原来是写并行CNN的时候出现了数据泄露的问题。 assignment3的内容是使用GAN生成图片，给我的主要收获是明白了多网络深度学习系统应该如何训练(代码细节)。 assignment4的内容是使用DDPM生成图片，第一次看DDPM原论文内容，花了半天才搞明白和MIT 6.S184中的区别到底在哪，这个论文中学习的网络是为了预测加入的噪声，但是后来被证明可以从SDE和ODE的角度去理解这个方法。 assignment5的内容是使用Flow Matching生成图片，最熟悉的一集。 assignment6的内容是使用Consistency model生成图片，但是我最后还是没有调好，生成的一直是噪声，感觉是skeleton code里面的某些细节我没有注意到，不管了开摆。Consistency model的目标是学习一个函数直接把一张加了噪声的图像映射回原图，训练的主要方式有一致性蒸馏和一致性训练，前者需要一个与训练好的diffusion模型，后者直接学，但是训练目标都是为了加噪声过程中的任何图像预测出来的结果尽可能相近。原论文足足40多页，附录部分的理论推导有20多页，让人叹为观止。 总的来说这个课我还是速通了，基本了解了Generative model的发展大致脉络。这个领域目前来看还是非常理论驱动的，需要大量的数学推导以及代码实践细节和硬件支持才能做出来一个demo级别的模型。向炼丹师们致敬。\n最后扔上课程网站，总的来说不是太推荐这门课，可以随便做做assignment消遣一下。https://mit-6s978.github.io/schedule.html\n","date":"2026-05-24T00:00:00Z","image":"https://PeterZhang9595.github.io/p/6.s978/image_hu_67f524eaa11663ac.png","permalink":"https://PeterZhang9595.github.io/p/6.s978/","title":"MIT 6.S978速通"},{"content":"这个权当学习MIT6.S184的总结性学习笔记了。强推这门课程，课程体量不大，但是设计非常用心，从头到尾、从理论到实践讲解了现代常用图像生成模型-Diffusion Model and Flow Model的原理和实现。具体内容可以参考课程官网。大概20-30小时就可以完成这门课程的全部内容。\nDiffusion Model and Flow Model 作为工业界视频和图片生成的主流模型，Diffusion Model和Flow Model兼具复杂的数学原理以及简单的训练逻辑。想要把握住二者的平衡并不容易，因此我尝试在这里梳理出基于扩散和流的生成模型从原理到算法实现的主体脉络。本文主要目的在于动机和基本步骤的介绍，略去了数学证明和实现细节。\n理解 Distribution of images 首先我们需要尝试把生成图片这个复杂的过程抽象为一个数学过程。\n这里我们可以把一张图片视作一个高维$H\\times W\\times C$的空间中的一个点。世界上所有的图片都可以视作这个空间中的点。显然，我们见过的所有正常图片在这个空间中都有一定的分布规律与聚集规律，而不是随机分布的。因此我们就可以建立一个抽象的分布模型$p_{data}$，这个模型实现了从$R^{H\\times W \\times C}\\to R\\in[0,1]$的一个映射。\n然而由于这个空间太大了，我们完全无法写出$p_{data}$的显式表达式，这是现实世界分布过于复杂带来的必然情况。但是这个$p_{data}$有一个先天而来的优势，就是我们可以轻松的从中采样，$z_1,\\dots,z_n$。\n这就为我们带来了相应的灵感：可不可以构建这样的一个生成模型，它模拟了$p_{data}$的分布。每次我们使用它生成图片，都相当于从$p_{data}$采样。也就是说给定一个输入图片$X\\sim N(0,I_d)$，经过生成模型加工之后，得到了$Z\\sim p_{data}$。（PS：这里之所以要采取高斯噪音作为初始输入的理论基础涉及信息论中熵部分的内容，同时这也是一个久经验证的工业常识了）。\n如果把握住了上面的动机，就可以正式开始构建我们的生成模型了。\n定性理解 Diffusion 和 Flow 模型 最近我们常用的各种图片和视频生成模型（已经去世的Sora,Nano Banana,Seedance等）都是基于diffusion或者flow模型框架基础设计的，它们在现实应用中已经获得了比较好的效果。然而在diffusion和flow横空出世之前，业界的主流是GAN模型。GAN模型有着较快的生成速度，但是生成效果相对较差，存在伪影，面对客户各种各样的要求的泛化能力比较差，这也是one-step方法面对复杂的高维的图像难以避免的问题。因此科研人员提出了来源于物理世界的diffusion（扩散）和flow（流）模型，在step-by-step中不断更新生成的图片。\n尽管在学术界的是扩散模最先得到发展的是扩散模型，但是从理解的角度来说按照先flow后diffusion的方式会更直观一点。\nFlow Model 不妨想象这样的情景：把一艘小纸船放到一个水池里，水池的中心有一个漩涡，那么我们可以知道这个小纸船会不断地转圈向漩涡中心靠近，直到被吸入漩涡中心。定性地讲，这里涉及两个定义：一个是运动轨迹，即小纸船在$t$时刻所处的位置，在这个情境中是二维的$(x,y)$，还有一个是速度方向，也就是向量场，说明小纸船在当前位置$(x,y)$以及时刻$t$的速度方向$(v_x,v_y)$。\n如何离散化地表达这个过程?这里我们使用最简单的方式，也就是显式欧拉法。 $$ X_{t+h} = X_t + h u_t(X_t)$$ 尽管在物理仿真中显式欧拉法的效果很差，但是在我们今天讨论的流模型和扩散模型中，它的效果是足够好的。\n我们可以把生成图片的step-by-step过程视作是上面的情形。小纸船的位置，就是我们想要生成的那张图片在$t$时刻的状态，只不过现在是一个高维的向量，每一位都是图片的像素值。而图片的速度方向，即向量场，决定了图片中的像素值要怎么改变。我们的生成模型的目标，就是使用深度学习的方式训练出一系列向量场，使得输入噪音图片在向量场的作用下一点点变化，最后生成我们想要的、符合现实世界分布的图片。\nDiffusion Model 相比于宏观角度的Flow Model，Diffusion Model更多是借鉴了布朗运动的一些内容进行建模。这里我们略去复杂的推导，给出最终的表达式： $$X_{t+h}=X_t +hu_t(X_t)+\\sigma_t \\sqrt{h} \\epsilon,\\epsilon\\sim N(0,I_d)$$ 可以看到相比于流模型，扩散模型多出了一个随机项，其中$\\sigma_t$是扩散系数，至于其合理性，就交给物理学家以及分子学家来解释了。\n可以发现我们现在的探讨还是停留于抽象的意识流层面，在前面我们已经提到了，没有任何方法去显式地表达$p_{data}$，所以即使我们想要构建一个向量场来引导图片，我们也不知道我们的终点$p_{data}$到底是什么。\n如何解决这种不可定量计算描述的困局？\n这个概率论的问题对于聪明的数学家们来说还是太简单了，这里我们引入四个重要的概念。\nConditional Prob Path 首先引入Probablity Path，可以把它理解成为一个随时间不断变化的分布。由于$p_{data}$不可知，因此我们无法求出$p_{data}(X)$，然而如果我们把最终生成的图像限于一张图片$Z$，我们最终的目标就确定了，这样的话我们的对输入图片$X$的引导的计算表述难度就大大地降低了。\n给出定义:\nDirac Distribution $$ \\delta_z: X \\sim \\delta_z \\iff X=z $$ 这个分布意思很显然，无论取样多少次，都只会产生一个固定的结果$z$。\n所以我们可以把向量场的目标设定为：让$X$变成$z$。用相对严谨的术语表达为：\nConditional Probability Path\n满足任意一个时刻的$P_t(x|z)$本身满足概率分布的性质 $P_0(x|z)=p_{init}$，与$z$无关 $P_1(x|z)=\\delta_z$ Marginal Prob Path 通过建立上面的条件概率路径模型，我们可以联想到概率统计里面学习到的边缘概率，此处我们可以利用边缘概率的原理，来计算出$P_t(x)$。\n$$P_t(x)=\\int p_t(x|z)p_{data}(z)dz$$ 从离散的角度，我们可以理解为从$p_{data}$中不断取样，然后对不同的条件概率求平均值。\nInsights:可以把Prob Path视作从$p_{init}$到$p_{data}$的插值逼近过程\nConditional Vector Field Prob Path定义了关于“走什么样的路线”的问题，而Vector Field则回答了“如何走出预定路线”的问题。\n$$ X_0\\sim p_{init},\\frac{d}{dt}X_t=u_t(X_t|z) \\Rightarrow X_t\\sim p(\\cdot|z) $$Marginal Vector Field 同理，当我们获得了对于每个特定的条件点$z$的CVF之后，也可以求出对应的MVF。但是值得注意的是，这里的公式相比于Prob Path部分，并不是那么non-trivial的。\n我们不妨先来简单地定性分析一下MPP那个积分式：$x$游走于高维空间中，高维空间中有很多满足图片特征分布的目标点$z$，$x$走的路径就是以任何一个$z$为目标点的时候的路径的按概率加权平均。\n而对于MFV，我最初的想法是，那就用相同的方法算不就得了？但是后来发现事情没有这么简单。这里涉及到一些数学连续性上的推导，我们直接省略，给出最终的定义。\n$$ \\begin{aligned} u_t(x)\u0026=\\int u_t(x|z)\\frac{p_t(x|z)p_{data}(z)}{p_t(x)}dz \\\\ \u0026=\\int u_t(x|z) p(z|x)dz \\end{aligned} $$ 定性理解就是，我们现在要考虑的是，在当前的位置$x$条件下，有多大的概率能到达$z$？这决定了我们给对应的向量场分配的权重。\n给出了这几个基本的定义之后，我们就成功实现了把一个抽象的不可计算不可建模的问题转化成了可计算可建模的问题。下面我们来进一步给这个过程施加更多约束，来简化这个建模。\nGassuian Prob Path 或许一个最朴素的想法：我们可以让Prob Path服从高斯分布？ $$ P_t(\\cdot|z)=N(\\alpha_tz,\\beta_t^2I_d) $$ 其中$\\alpha_t,\\beta_t$被称为噪音系数，我们可以任意调整他们，只要可以满足$\\alpha_0=\\beta_1=0$且$\\alpha_1=\\beta_0=1$，即满足prob path定义的最低标准就可以了。常见的有$\\alpha_t=t,\\beta_t=1-t$。\n在此定义上，经过数学运算，可以显式求出高斯条件向量场的表达式： $$ \\begin{aligned} u_t^{target}(x|z)=(\\hat{\\alpha}_t-\\frac{\\hat{\\beta}_t}{\\beta_t}\\alpha_t)z+\\frac{\\hat{\\beta}_t}{\\beta_t}x\\\\ \\hat{\\alpha_t}=\\partial_t{\\alpha_t}\\\\ \\hat{\\beta_t}=\\partial_t{\\beta_t} \\end{aligned} $$ 如果我们把$\\alpha_t,\\beta_t$设计的很简单，那么求完偏导之后上面那个看似复杂的式子也会变得简单很多。不然用$\\alpha_t=t,\\beta_t=1-t$试试？\nFlow Matching and Score Matching 所以到底该如何使用深度学习的方法来求$u_t^{target}$? 如果你未曾学习过深度学习相关知识，可能需要修习一些入门课程才能理解这部分内容。 在我看来，深度学习的pipeline本质上是同构的，其整体框架都是三大块：输入、深度神经网络模型、输出。监督模型尤其如此。\n输入：表格化数据、图片、音频、文字等等，一般会利用一些数据处理的方式把它们转为量化的可计算的数据形式。但是这里我们一般只进行数据清洗和数据转换，传统机器学习中常见的特征工程并不一定能带来好的效果。 深度神经网络模型（DNN）：架构的核心部分。我们可以把它抽象为一个巨大的函数，由分层的线性函数和非线性函数构成。函数的类型本身已经定义好了，是hard-code的，但是函数的参数是需要在训练的过程中不断更新的。比方说$y=a_1x_1+a_2x_2$，这个函数本身是线性函数不可改变，但是$a_1,a_2$这两个权重是可以改变的。 输出：最终希望得到的结果。我们肯定希望模型输出的结果是好的，但是怎样算好？这就需要我们需要一个评判结果的标准，然后利用这个标准设置一个合理的可导的损失函数。随后利用反向传播和梯度下降对DNN部分的参数进行更新，来让模型输出的结果更好。 Flow Matching 训练目标：$u_t^{\\theta}(x) = u_t^{target}(x)$ 这也就引出了我们的损失函数 $$ L_{FM}(\\theta)=E[||u_t^{\\theta}(x)-u_t^{target}(x)||^2] $$ 这是深度学习中很经典的MSE误差损失函数。然而问题在于，$u_t^{target}$这个东西我们是无法写出具体表达式的，只能通过不断抽样进行逼近。但是如果我们连目标都无法确定的话，那么训练就毫无稳定性可言，因此我们只能退而求其次，使用可以确定的条件向量场。 $$ L_{CFM}(\\theta)=E_{t,z,x}[||u_t^{\\theta}(x)-u_t^{target}(x|z)||^2]$$通过数学推导，可以计算出$L_{FM}(\\theta)=L_{CFM}(\\theta)+C$，那么这两个函数的梯度是一样的，因此在$L_{CFM}$上进行优化得到的最优值点$\\theta^$可以满足我们$u_t^{\\theta^}=u_t^{target}$的目标。\n在实际训练的时候，这个看似复杂的求期望符号，其实已经被离散化消除掉了。由于要对$t,z,x$求期望，所以在每个回合中，先从图片数据集中取样一个$z$，再从均匀分布中取样一个$t$，最后根据$p(\\cdot|z)$取样一个$x$，最后计算$L(\\theta)=||u_t^{\\theta}(x)-u_t^{target}(x|z)||^2$，然后进行反向传播和对参数的梯度下降更新即可。\n读者可以尝试把高斯概率路径的一系列公式代入。\nScore Matching 还记得我们之前说到的Diffusion Model吗？由于扩散模型是基于SDE的演化路径的，所以要比Flow Model复杂很多。\n在流模型的ODE场景下，想让图片$x$演变为$z\\sim p_{data}$只需满足$dX_t=u_t^{target}(X_t)dt$就可以最终$X_t\\sim P_t$了。\n但是在扩散模型的SDE场景下，我们在路径中加入了噪声扰动，然而这噪声扰动对最终结果的分布也是有影响的，因此需要从数学上纠正噪声来保证实现目标Prob Path。\n数学家高手们定义了Score Function，满足$S_t(X)=\\nabla_x \\log P_t(x)$，这个函数可以满足： $$ \\begin{aligned} X_0 \u0026\\sim P_{init} \\\\ dX_t\u0026= u_t^{target(X_t)}dt+\\frac{1}{2}\\sigma_t^2S_t(X_t)dt + \\sigma_t dW_t \\\\ \\Rightarrow X_t\u0026\\sim P_t \\end{aligned} $$ 其中$W_t$的具体定义就不赘述了，大家可以把它理解为一个高斯扰动。\n但你可能会疑惑，好了好了，这下好了，原来只需要训练一个$u_t^{\\theta}$，现在还得再多训练一个$S_t^{\\theta}$，这不没事找事吗？\n所以这里不得不说清楚SDE的优势到底在哪里：\n扩散 SDE 的反向过程每一步都注入适量噪声。当数值求解引入误差（比如步长过大导致轨迹偏离）时，这种随机扰动会把样本重新推向高概率区域，相当于在采样中自动纠错。 希望这可以说服你，如果不能，也无所谓，因为现在主流确实是速度更快更简单的Flow Matching，但是从方法的出现顺序上来看其实是基于添加噪声的Score Matching出现的更早。\n同时，在常用的高斯路径中，通过数学推导可以实现$u_t$和$S_t$的相互表示，这里就不赘述了，可以直接参考讲义中的数学公式（我也懒得敲了）。\n在实际训练中基本逻辑和Flow Matching一致，只需要让程序模拟离散版本的SDE就行了。\nGuidance:Condition on a Prompt 我们训练生成模型的本意肯定不是为了搞出来一个你按一下按钮然后就蹦出来一张图片的机器塞博斗蛐蛐用，我们肯定希望生成的图片能够满足一定的要求，而表达这种要求最自然的方式就是：语言。不妨将它记为一个抽象的符号$y$，意为guidance/prompt。\n现在的问题变得奇妙起来了，我们不仅仅需要生成的图片是一张人类能够正常理解的真实的图片，更重要的是我们需要这个图片是满足我们最初用任何prompt方式表达的要求的一张图片。也就是说我们需要在评判标准里加上一项来评定生成的图片与prompt的相符程度。\nVanilla Guidance 最朴素的方式就是直接给每种标签或者prompt对应训练一个向量场$u_t^{\\theta}(x|y)$。 然后按照 $$L_{CFM}=E_{t\\sim[0,1],(z,y)\\sim p_{data},x}[||u_t^{\\theta}(x|y)-u_t^{target}(x|z)||^2] $$ 进行相应的优化。\n然而在实际应用过程中发现这种方法生成的图片的效果不是太好，往往出现不是太遵守prompt限制这一问题。\nClassifier Guidance 为了让生成的图片能够更好地去满足prompt的要求，我们考虑加入一个分类器：$p_t(y|x)$，即我们希望生成的图片能够被分类器判断为对应的标签。\n利用一系列数学推导，可以得出： $$u_t^{target}(x|y)=u_t^{target}(x)+a_t\\nabla \\log p_t(y|x)$$ 也就是我们从数学上严谨地证明了，在Prompt限制下的向量场可以表示为原始的无条件向量场与分类器梯度的矢量和。 这部分证明其实是用到了不少的trick以及前面的知识的，有兴趣的读者可以自己尝试推导，如果懒得推的就记住条件向量场由无条件向量场和分类器梯度求和而得到。\n为了更进一步体现出Guidance，我们考虑加大分类器梯度的权重。 $$u_t^w(x|y)=u_t^{target}(x)+wa_t\\nabla_x \\log p_t(y|x)$$但是问题来了，这里的分类器，其实是我们在视觉领域一直喜闻乐见的入门任务：图像分类，但是这也就说明我们不仅需要去尝试训练一个向量场$u_t(x)$，还需要单独训练一个分类器，能不能通过数学运算把这个分类器的训练省去呢？\nClassifier-free Guidance 还真可以。利用 $$\\nabla \\log p_t(x|y)=\\nabla p_t(x) + \\nabla p_t(y|x)$$ 进行替换，得到 $$u_t^w(x|y)=wu_t^{target}(x|y)+(1-w)u_t^{target}(x)$$？这不还是得训练两个model吗？你说得对，但是强大的研究人员们使用了一个小trick。我们不妨把$u_t(x)$视作$u_t(x|\\emptyset)$，其中$\\emptyset$就是人造的新标签。虽然训练集中可能所有图片都有标签，但是我们只需要随机选出一小部分把它们的标签设置为$\\emptyset$就可以开始跑训练了。真是神之一手啊！\n所以自此我们的最终目标就变成为训练一个$u_t^{\\theta}(x|y)$。然后在生成的过程中使用classifier-free Guidance。 $$u_t^w(x|y)=wu_t^{target}(x|y)+(1-w)u_t^{target}(x|\\emptyset)$$细心的你已经发现了，这个东西看起来很好，但是实操的时候，我们可以很轻松地表示出高斯路径下的$u_t^{target}(x|z)$，但是这个$u_t(x|y)$呢？难道我们真的要每种标签y都对应练一个向量场？那这样的话如果面对复杂的语境、没见过的标签不久没法泛化了吗？所以必须得想个办法把$y$进行处理，比如某种embedding方式，或者特殊化设计神经网络的结构。这是后文的内容。\nBuild a Conditional Generative Model for Images 到现在为止，我们已经基本上完成了基于流和扩散的生成模型的理论的学习，如果你只是为了了解一下现代图像生成模型的简单原理，那么看到这里就可以了。 但是，任何理论和实践之间，都有着巨大的鸿沟。而demo类型的实践和工业级别的实践更有着巨大的鸿沟。为了达成理论推导下可行的目标，我们往往需要探索比底层理论更多、更复杂、甚至更加高深的实践细节。而MIT6.S184的作业三正是给我们提供了一个完整的框架，让我们体会如何从零到一搭建起一个在MNIST数据集上训练的demo级别的图片生成模型。如果你已经做过了这个作业，你会发现这个作业远远比作业一和作业二那种单纯可视化概念的题目要难很多。\nReferences: William Peebles and Saining Xie. Scalable Diffusion Models with Transformers. 2023. arXiv: 2212 . 09748 [cs.CV]. url: https://arxiv.org/abs/2212.09748. Robin Rombach et al. High-Resolution Image Synthesis with Latent Diffusion Models. 2022. arXiv: 2112.10752 [cs.CV]. url: https://arxiv.org/abs/2112.10752. 作业三的基础框架是基于上面两个论文搭建的，有兴趣的读者可以去看看论文里面的摘要和方法部分。 下面这一部分，我决定直接回归到lab3.ipynb，直接在对应的代码里通过注释来解释对应的细节。如果你自己在实现这部分代码的过程中（由于课程讲义中写的不是很清楚）出现了问题，可以参考 labnotes。\n关于attention部分，我之前参考了3Blue1Brown的课程，绘制了一些基本的原理笔记，放在这里以供参考。 VAE 突然发现有一个重要的知识点Variational Auto Encoder没有写，所以得在这里补上。 在主流的图像生成模型中，由于一般想要生成的图像的像素数量太大了，会带来巨大的计算压力，因此我们考虑使用一个编码器，以某种方式把图像压缩到Latent Space,然后在这个Latent Space中对向量场进行相关计算训练，然后再把生成的图像通过一个解码器映射回原图的分辨率。\n那么我们该如何设计相应的图像Encoder-Decoder呢？通常，VAE和扩散模型是分开训练的，而不是端到端直接训练的，这一方面是为了训练的稳定性，另外一方面由于VAE其实在其它图像处理任务中也有诸多应用，因此单独预训练一个还是很划算的。\n那么我们从最naive的想法进入，encoder:$\\mu_{\\theta}:R^k \\to R^d$，decoder$\\mu_{\\phi}:R^d \\to R^k$，它们二者先后作用，理论上应该可以保持输入$x$不变。 $$L_{Recon}(\\phi,\\theta)=E_{x \\sim p_{data}}[||\\mu_{\\theta}(\\mu_{\\phi}(x))-x||^2]$$这个想法很好，但是问题在于，这个方式更侧重编码器和解码器放在一起的效果，但是我们需要diffusion model跑在编码器生成的latent space的分布上，因此如果我们的编码器把$p_{data}$变成了一个很难训练的$p_{latent}$，那么最终的效果可能是不好的，所以我们需要人为加入一点heuristic启发式引导，给编码器生成的分布情况加上约束。\n这里就可以介绍我们我们的Variational Autoencoder,即变分自编码器。看到variational这个词，不难想象的是相比于刚才介绍的标准方法，这种方法不再是确定性的映射，而转向了概率式的分布采样。\n可能有些难以理解，我先把公式扔上来。 $$q_{\\phi}(z|x)=N(z;\\mu_{\\phi}(x),diag(\\sigma^2_{\\phi}(x))),p_{\\theta}(x|z)=N(x;\\mu_{\\theta}(z),\\sigma^2_{\\theta}(z)I_d)$$注意到，我们现在的编码器和解码器不是直接使用一个固定参数的网络实现映射，而是从正态分布里面采样，对应的正态分布的均值和方差才是神经网络参数化的内容。\n这样的话，我们的生成模型的基本Pipeline就成型了，给定输入$x$，先编码采样$z\\sim q_{\\phi}(\\cdot|x)$，然后把这个$z$扔到DiT里面一顿操作生成$z\u0026rsquo;$，再解码采样$x\u0026rsquo;\\sim p_{\\theta}(\\cdot|z\u0026rsquo;)$获得最终生成的图像。\n基本思路已经明确了，那么该如何训练这个VAE呢？首先就应该明确损失函数。\n重建损失 这里的想法是，考虑经过编码之后的图片再扔进解码器之后能生成原图的置信度。 $$L_{VAE-Recon}(\\phi,\\theta)=-E_{x\\sim p_{data}(x),z\\sim q_{\\phi}(\\cdot|x)}[\\log p_{\\theta}(x|z)]$$ 先验损失 这里就是为了解决我们在前面那部分提到的生成的latent space分布的先验约束问题，为了让这个分布变得容易训练，我们引入$p_{prior}(z)=N(0,I_k)$，我们的目标是让$q_{\\phi}(z|x)$尽可能接近先验的标准正态分布。 为了衡量两个分布的相似性，我们常用的标准是KL-divergence，$D_{KL}(p(x)||q(x))=E_{X\\sim p}[\\log\\frac{p(x)}{q(x)}]$。 $$K_{VAE-Prior}(\\phi)=E_{x\\sim p_{data}(x)}D_{KL}(q_{\\phi}(\\cdot|x)||p_{prior})$$ 从而 $$L_{VAE}(\\phi,\\theta)=L_{VAE-Recon}(\\phi,\\theta)+\\beta L_{VAE-Prior}(\\phi)$$ 至于带入高斯分布之后的一些数学计算和简化就直接参考课程讲义就行了，这里就不赘述了。\n最后这部分可能看起来相对零散，其实对于这种模型架构的时候，尤其是在作业中已经给了骨架代码的情况下，还是边写边学，比如写到一个地方不知道这部分代码在干嘛或者不知道具体该如何实现的时候去查阅资料，这样的效率更高一些。\n至此，本文已经基本施工完毕。接下来我将开始学习MIT6.S978，尝试对生成模型有一个更全面且深入的了解。\n当下即是最好。加油。頑張れ。\n","date":"2026-05-01T00:00:00Z","image":"https://PeterZhang9595.github.io/p/diffusion-flow/why-are-you-like-this_hu_cffdf069fc37fe9f.jpeg","permalink":"https://PeterZhang9595.github.io/p/diffusion-flow/","title":"初探基于扩散模型和流模型的图像生成模型"},{"content":"随着本周最后一节课上完，我也算是处理完了一周的任务，准备承接上周末刚刚上完的Nand2Tetris课程，完成Project8。然而刚刚打开项目文件，我忽然意识到一件事情：**我已经几乎忘掉了全部细节！**除了知道我要写VM translator的第二部分，其中包含了分支判断以及函数调用这两个模块的内容，但是关于它们的具体信息、实现细节，我已经一点都不记得了，尽管我上一次观看课程视频是在四天之前。\n说句实在话，如果在高中发生这种情况，我可能会开始猛烈地反思自己，然后找个时间把那些知识重新过一遍，再在意识里把它们标注为易忘记的知识，日后要多次拿出来复习。\n而这次，我只是拿出了自己记得笔记、以及课程教材中的API文档，又看了一遍，然后没回想明白的部分问了一下大模型，最终也是成功地完成了这次略带难度的Project。\n我不禁思考，这样的学习方式，是否是正常且有效的？\n而问题的核心，在于是否只有能够从头到尾地默写所有知识、复述所有细节，才算是学会了一部分内容？\n我想，问题的答案很显然，如果能够做到上面的那一点，无疑是很好的，说明了对知识很高的掌握程度。但冥冥中我又总觉得这种伴随我初中、高中的学习理念，到了大学，开始出现一些不对劲的地方。\n首先是时间问题。大学，即使是本科时期，知识量、覆盖面、难度，是高中望尘莫及的。如果想要像高中那样把所有的知识全部记下来，那恐怕是要付出大量的时间和精力的，个人的记忆能力能不能保证完全记住还是一件另说的事情。\n其次是考察方式问题。相比于初高中评判标准里考试“一言堂”，大学期间的考试重要性已然有很大程度的下降，以计算机方向为例，通过编程项目考察一些基本的编程能力、团队合作能力、报告书写能力，也是考核的一部分，并且更加接近日后的科研或者工作情景。在这种情况下，如果坚持对知识细节进行记忆，诚然能帮助考试；然而对于编程项目来说，往往涉及到的不只是知识上的细节，还有很多课本上没有讲到的工程上的细节，这些细节非常繁琐，但是往往都可以随用随查，因此记忆带来的收益并不会太高。而在一些科研方面，更是会频繁涉及还没有加入到课本的新鲜内容，与其一心想着搜罗他们，不如真正遇到的时候再去了解。\n最后是现代AI工具的问题。我认为现今的大模型就是一个非常先进的搜索工具，它解决问题的效率会远远高于以往的基于匹配算法的搜索引擎，而它的记忆能力也极其的强大，如果我们想了解某个方面的内容，一般来说问一下大模型就可以获得相关的细节内容，而且如果只是知识性的，大模型给出的答案准确率很高。\n综上，其实已经否认了过去的学习方式。它真的过时了。\n然而我们往往希望破除一个旧观念之后，能够立起来另外一个更好的新观念。在这个时代，我们应当如何建构自己的学习过程呢？\n这里我要提出我的核心思想：“函数化”。\n所谓“函数化”，就是用函数的角度去理解世界上的一切事物对象。我们看待一切事物，都只看两个事情：它的input和output，输入和输出，而至于它是如何完成从输入到输出的转化这一点的，我们并不关心。这种思想，在软件编程里面非常常见，叫作封装。而使用输入和输出描述一个对象，则往往称为abstraction，即抽象。当然这种“函数化”的思想，在社会的各种运行中也非常常见，比如常见的劳务外包，就是一个函数，输入是甲方的要求和薪资，输出是相关人员完成甲方要求，而中间的各种具体措施、人员管理，则完全是劳务公司内部自己实现的，无人关心。\n之所以说到“函数化”，是因为我觉得它可以推至我们一般的学习过程乃至工作过程。在大多数时候，我们只涉及两部分：call and implement，即调用和实现。当然它们二者并不是绝对分开的。很多时候我们通过组合各种被调用的函数，也能实现一个名义上新的功能；而很多时候为了去实现一个新的功能，我们也不是从盘古开天辟地开始造轮子，也是需要调用很多已经完成好的函数的。\n书归正传。在“函数化”思想的指导下，我们真正需要记忆的，只有两个东西：函数的输入和输出。当然这是双向的的：在implement的时候，我们需要精准地定义函数的输入和输出，即提炼出核心问题并且构想出目标结果，才能让函数的实现过程更加有针对性；而call的时候，我们需要根据输入和输出的特征回想起哪些函数可以用来完成这个转化。一言以蔽之，我们需要记忆的，不是知识本身，而是知识和知识之间的联系。\n因此也就可以引出我最终新的学习理念了。我认为，在这个时代，学习不应只是为了完成任务而进行刷课程、看教材这样的传统方式，而应该带有强烈的问题意识，在问题的指导下进行学习，才是关键。\n如果面对的问题有人解决过，那么就需要我们找到最合适的函数，并且调用它。这就是我们平时学习那些课本上的知识的意义。\n如果面对的问题是无人解决过的，那么就需要我们实现它。\n然而面对这种没有可参考经验的问题，实现它又谈何容易？所以应当拆解成为两步，第一步是定位目前可以解决的部分，然后调用相关函数，其实大多数问题到这里也就结束了；第二步是找到问题的核心所在，明确暂时无人实现的函数的输入和输出，然后自己尝试不同的实现方法，直到某一个实现方法可以完成预期中函数的功能，这其实已经属于科研的范畴了。\n因此，这也就要求我们在学习的过程中，要积极复现他人写好的函数的实现过程：注意这里不是指的具体的实现步骤，而是实现的动机、尝试过程、优化过程等等，这能为日后我们在实现相应的函数时候给予很多的指导。\n所以最后回到我最初面临的情况，即在笔记、教材、AI的帮助下完成了VM translator的实现过程。这是一个典型的implement过程。而我在这个过程中，参考的都是各类API，即我这个VM translator中涉及的所有功能。所以这是属于有效的范畴。但如果我面对的问题是：请设计一个方式把VM语言转化为Assemble语言，那么我的所作所为就不再有意义。因为面对这个问题，我接受的输入是VM语言，目标输出是汇编语言，那么这个函数内部的机制都应该我自己完成设计。\n所以，归根结底，大多数时候我们做的Project，虽说是实现一个函数，但是本质上这些函数已经处于一个输入和输出都被缩小到很小尺度的问题了，基本上只需要考虑调用函数的组合方式就可以了；而那些真正有难度的，往往输入和输出本身的“距离”更远，也需要更多的设计和分解。\n这也正是工程师和科学家的区别。仰望星空，脚踏实地。\n","date":"2026-03-19T00:00:00Z","permalink":"https://PeterZhang9595.github.io/p/essay/","title":"随笔"},{"content":"Project 6: Assembly 简介 在本项目中我使用JAVA编程语言进行代码的编写。课程已经给我们提供了贴心的API设计，我们只需要根据API进行补充就可以了。\nAssembler由以下四个类组成：\nParser: read an instruction from the .asm file,ignore the white space and comments,interpret the type of instruction,parse it when it is a C-instruction Code: translate hack assembly languable into binary codes by using the code map SymbolTable:use a map to store all the symbols and its corresponding address Main: initialize a SymbolTable-\u0026gt;create a parser-\u0026gt;first pass-\u0026gt;recreate a parser-\u0026gt;second pass 这是典型的面向对象编程，不涉及算法内容，基本上就是进行简单的字符串读写，但是需要支持各个类之间较为复杂的依赖关系。用JAVA和IntelliJ IDEA进行开发是一个很不错的选择。\n其实这个作业在总体统筹上的规划难度几乎是0，因为在课本里面已经给了所有的API，只需要完成每个类的API以及在Main函数中实现整个pipeline即可。所以我在这里就不放具体代码了，而是把我在完成的过程中遇到的一些问题记录一下。\n一些insights 关于Java的Map 在本次作业中，为了实现一些指令到binary code的转化，我们需要使用java标准库的Map容器。\n1 2 3 4 5 6 7 8 9 10 private Map\u0026lt;String,Integer\u0026gt; symbolTable = new HashMap\u0026lt;\u0026gt;(); //add key-value to map symbolTable.put(key,val); //access to a value val = symbolTable.get(key); //check the existence of key symbolTable.containsKey(key); Parser的读入操作 这里还是比较复杂的，尤其是涉及一些基本的字符串处理操作。\n使用BufferedReader进行整行的读入。 1 reader = new BufferedReader(new FileReader(filename)); 关键的advance函数实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void advance()throws IOException { currentInstruction = nextInstruction;//we define a nextInstruction for the convenience of the implementation of hasMoreCommands nextInstruction = null; String line; while((line = reader.readLine()) != null){ line = line.split(\u0026#34;//\u0026#34;)[0].trim();//remove the comments or empty row if(!line.isEmpty()){ nextInstruction = line; return; } // if no instruction is read in,then reader will keep on reading } } 注意在一个parser对象结束工作之后及时关闭内部的reader，读者可自行设置函数完成。 关于ROM和RAM 在完成代码的时候，我曾经出现了一个混淆，就是label和variable的存储位置。我原本意味他们是存储在一起的，所以可能出现覆盖的问题。\n但是事实上，label指向的是ROM里面的指令内存，而variable指向的是RAM里面的数据内存，二者可能地址的int值是一样的，但是实际上并不是存在一块内存中的，也就不涉及互相覆盖的问题。而在硬件端运行的时候，会自动根据指令是A还是C指令访问对应的内存的。\n写在最后 在这里我说几句闲话，算是表达一下自己的一些关于学习方法的思考。 在我看来，如果进行一项学习活动只追求一方面的增益，那么相应的效率就是比较低下的。而如果同时追求三方面以上的增益，那么就容易陷入多线程工作造成的注意力涣散问题中去。比方说在学习data100相关课程的时候，使用pandas库进行数据处理。在这个过程中，我获得了三个方向的收益：\n掌握数据处理的基本流水线 Pandas的基本语法 阅读官方教程和API文档的能力 而本次Project中，由于需要使用一种高级编程语言来完成一个基础的汇编器，而对我来说python编程总有一种很随意的感觉、缺少工程的味道；而C++编程则缺少合适的IDE（Visual Studio太丑了，而用CMake则需要把大量的时间放在编译和debug上面）且代码略显复杂，因此我决定使用一种我并不熟悉的（但是很简单）的编程语言——Java.\n这时候我突然想起我曾经关注的一门课——MIT:Software Construction。我想，何不就这个机会，切入软件工程这个领域，进行一些中等规模编程实训，培养自己的代码习惯和编程系统思维（古法手搓代码爱好者的选择，氛围编程用户看个乐呵即可）？因此，我从这个Project中获得的增益如下：\n汇编器的运行原理 java编程语法 系统编程思维 那么，这个项目对于我来说，就是有意义的、且多方面收益的学习过程。 絮絮叨叨说了这些，其实我想表达的，是心理学家斯金纳多年前就已经说过的。 Education is what remains after one has forgotten everything he learned in school. 具体的知识是无穷无尽的。我们终其一生只能居在一隅内。唯有通过在知识中实践，掌握普适的学习方法，我们才能尽自己的努力，发挥人类在泛化能力上的优势，才能不断接近“无涯”的境界。 不多说了，准备开启Nand2Tetris的第二部分。奥利给！\n","date":"2026-03-12T00:00:00Z","image":"https://PeterZhang9595.github.io/p/nand2tetris6/asm_hu_80160a8c811fba55.png","permalink":"https://PeterZhang9595.github.io/p/nand2tetris6/","title":"Nand2Tetris Project(6)"},{"content":"Project 5: Computer 在本项目中我们将对之前完成的（并非）各个计算机组成部分进行整合、连接，最终制造成为一个可以运行的Hack计算机。\nCPU 首先回归课程中的几个核心知识点。 首先需要明确的一点，也是我们操作的入手点，就是我们将要写的这种架构的CPU究竟能处理哪些类型的指令？显然我们可以处理A类型的指令和C类型的指令，A主要是涉及到向A-寄存器中存入数据，相对简单；而C主要是操控ALU进行一些运算，同时涉及一些向内存中的读写，以及对程序计数器的状态修改。\n我一开始写CPU的时候迟迟无法下笔，就是因为没有搞清楚CPU实现的根本入手点，导致无法推测出图1中第一个$MUX16$的含义。\n而如果搞明白这一点，我们就可以知道第一个$MUX16$的作用：让CPU知道自己处理的到底是A-instruction还是C-instruction。这需要我们读入机器指令的第15位作为Control bit。如果$c=0$,那么执行A-instruction，把instruction作为地址输出；反之则执行C-instruction。\n这里的第二个难点是，执行C-instruction的时候，为什么要把 ALU output 传出去？\n这里还涉及到对整个架构运行顺序的一点理解。在第一个时钟周期内，我们无法进行任何ALU的运算，因为我们没有在A或者D这两个寄存器中存入任何数据，而寄存器只有在下一个时钟周期的时候才会把它存储的值发射出去。\n所以比如说我们想要让ALU运行一个指令。\n1 2 @R10 M;JMP 它会被拆成两句话，第一句话是取数据地址，作为下一轮的inM。而此时PC++，新的instruction指令传入，但是它不是直接通过那张图里面显式画出的方式传到对应的ALU和PC里面的，而是通过一些未画出的线，以control bit(s)的身份传入其中的。\n因此我们只需要按照图中的顺序，分别完成每个部分的传输即可。翻阅课程教材第2课和第4课的内容，找到对应的指令编码各个位置的含义即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 CHIP CPU { IN inM[16], // M value input (M = contents of RAM[A]) instruction[16], // Instruction for execution reset; // Signals whether to re-start the current // program (reset==1) or continue executing // the current program (reset==0). OUT outM[16], // M value output writeM, // Write to M? addressM[15], // Address in data memory (of M) pc[15]; // address of next instruction PARTS: Mux16(a=instruction,b=aluOutput1,sel=instruction[15],out=output1); //here is important,decide whether store the alu output to A-register Not(in=instruction[15],out=isA); Or(a=isA,b=instruction[5],out=loadA); ARegister(in=output1,load=loadA,out=output2,out=outputAddress,out=outputForPC,out[0..14]=addressM); // decide the second input of alu comes from A or from M Mux16(a=output2,b=inM,sel=instruction[12],out=output3); // decide whether store the alu output to D-register And(a=instruction[15],b=instruction[4],out=loadD); DRegister(in=aluOutput2,load=loadD,out=output4); ALU(x=output4,y=output3,zx=instruction[11],nx=instruction[10],zy=instruction[9],ny=instruction[8],f=instruction[7],no=instruction[6],out=outM,out=aluOutput1,out=aluOutput2,zr=zr,ng=ng); And(a=instruction[15],b=instruction[3],out=writeM); // judge according to the output of alu with the instruction on \u0026#39;jump\u0026#39; Not(in=zr,out=notzr); Not(in=ng,out=notng); And(a=notzr,b=notng,out=ps); And(a=ps,b=instruction[0],out=load1); And(a=zr,b=instruction[1],out=load2); And(a=ng,b=instruction[2],out=load3); Or(a=load1,b=load2,out=temp1); Or(a=load3,b=temp1,out=temp2); And(a=instruction[15],b=temp2,out=loadPC); PC(in=outputForPC,load=loadPC,inc=true,reset=reset,out=outpc,out[0..14]=pc); } 自此，我们基本完成了一个CPU架构。这部分的设计精巧而复杂，值得反复品鉴。\nMemory 和CPU相比，内存的实现就要简单很多很多了。 这个过程可以基本拆解为以下几步：\n判断输入的数据是否应该被存入内存，应该被存入哪个内存。（这一部分我一开始出错了） 并行从RAM,SCREEN,KBD中分别获得output 使用MUX根据地址判断应该输出哪个部分的内存读取结果 这里还有一点需要我们注意，就是在硬件电路中我们虽然无法直接实现两个数比较大小，但是由于所有数字存在的形式都是二进制的格式，所以我们可以根据特定的二进制位进行比较。\n比如本题，我们想比较address和$16384=2^{14}$，如果address比后者小的话，第15位应该是0，反之是1。这就为我们使用MUX16提供了一个很好的入手点。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 CHIP Memory { IN in[16], load, address[15]; OUT out[16]; PARTS: And(a=address[14],b=load,out=loadScreen); Not(in=address[14],out=isRAM); And(a=isRAM,b=load,out=loadRAM); RAM16K(in=in,load=loadRAM,address=address[0..13],out=outRAM); Screen(in=in,load=loadScreen,address=address[0..12],out=outScreen); Keyboard(out=outKBD); Mux16(a=outScreen,b=outKBD,sel=address[13],out=out1); Mux16(a=outRAM,b=out1,sel=address[14],out=out); } Computer 按照此图直接无脑连接即可。\n1 2 3 4 5 6 7 8 9 CHIP Computer { IN reset; PARTS: ROM32K(address=pc,out=instruction); CPU(inM=inM,instruction=instruction,reset=reset,outM=outM,writeM=writeM,addressM=addressM,pc=pc); Memory(in=outM,load=writeM,address=addressM,out=inM); } 这一章Project的难度已经有了比较明显的提升，当然关键不是在于编程本身，而是在于理解整个电路进行各种运算的内在逻辑。在写这个Project之前我以为我已经很了解电脑内部的运行机制了，但是代码实现的时候我发现我只是知道了各个部分的作用，但是没有把它们串联成为一个整体，而这份Project很好地帮助我加深了对电脑内部体系结构的理解。\n","date":"2026-03-08T00:00:00Z","image":"https://PeterZhang9595.github.io/p/nand2tetris5/xiangyu_hu_e016063af56d1290.png","permalink":"https://PeterZhang9595.github.io/p/nand2tetris5/","title":"Nand2Tetris Project(5)"},{"content":"Project 4: Machine Language 在本次Project里面，我们将从代码填空变为整段代码编程以实现一个乘法器和一个画图程序。我们将初步接触Hack language这种low-level编程语言，并且着重实现一些赋值、循环等等在high-level编程中很简单的功能。\nMult 使用循环累加，逻辑很简单，但是如果要转化为机器语言，初步想法需要以下几步：\n给一个变量赋值为sum=0，用于累加记录 建立一个Loop：提前存好两个变量，num=RAM[0],time=RAM[1] 用一个变量i来检测是否已经超过预定的循环次数 累加到sum里面 把sum赋给RAM[2] 无限循环终止程序 但是上面的代码有一个问题，就是我们需要处理的不仅仅是正数乘法，还有负数乘法。这就需要我们同时判断两个乘数中负数的个数从而判断最终的结果的符号。\n本题的难点不在思路上，而是在对于Hack机器语言的熟悉上，在第一遍实现的过程中，我遇到了以下的问题：\n注释要单开一行写 要写课本表格里面有的表达式，只要是课本里没有的都无法通过编译 不要自行多加分号，同样无法通过编译（C++受害者） 把D视作一个临时储存器，把中间变量都存到这个里面 下面贴出代码，仅供参考。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 //record the number of negative numbers @minusCount M=0 // record the total num @sum M=0 // count of iterations @count M=0 @R2 M=0 // D=RAM[0] (MAKESIGN1) @0 D=M @MAKEABS1 D;JGE //if ram[0] is negative,then minusCount+1,and we set the number to its abs @minusCount M=M+1 @MAKEABS2 0;JMP (MAKEABS1) @0 D=M @a M=D @MAKESIGN2 0;JMP (MAKEABS2) @0 D=M @a M=-D (MAKESIGN2) @1 D=M @MAKEABS3 D;JGE @minusCount M=M+1 @MAKEABS4 0;JMP (MAKEABS3) @1 D=M @b M=D @ODDEVEN 0;JMP (MAKEABS4) @1 D=M @b M=-D //if the minusCount is odd,the final result need to be negative (ODDEVEN) @minusCount M=M-1 // we set isNegative to 0 at first @isNegative M=0 // if minusCount=0,,which means it is odd before minusing 1,jump to odd @minusCount D=M @ODD D;JEQ //else, jump to even @EVEN 0;JMP (ODD) @isNegative M=1 @LOOP 0;JMP (EVEN) @isNegative M=0 (LOOP) //we use a as base and b as iterations // D = count @count D=M // D = b - count @b D=M-D // IF b=count,then finish the loop @CHANGESYMBOL D;JEQ @count M=M+1 @a D=M @sum D=M+D M=D @LOOP 0;JMP (CHANGESYMBOL) @isNegative D=M @GIVE2 D;JEQ @sum M=-M (GIVE2) @sum D=M @2 M=D (END) @END 0;JMP fill 尝试实现设备的I/O交互。\n处理输入 Whenever a key is pressed on the physical keyboard, its 16-bit ASCII code appears inRAM[24576]. When no key is pressed, the code 0 appears in this location. 因此我们应该使用KBD读取键盘map的地址，然后对它进行操作。显然这里我们只需要判断它是不是0就可以了。\n处理输出 读取SCREEN地址 设置一个待绘制的指针 如果键盘被按下，那么所有像素应该是绘制为黑色（-1），反之则绘制为白色（1），这里是一个关键点，我们需要保证先按下后松起之后屏幕重新变成白色。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 // set 8192 as the total number of ram spaces to be drawn // here we need to know that if we want to set a specific number to a variable // rather than the specific number at that address // we need to do it like follows @8192 D=A @total M=D (CHECK) //so do the specific number of address of screen @SCREEN D=A @addr M=D @KBD D=M @WHITE D;JEQ @BLACK 0;JMP (WHITE) @i M=0 (LOOP1) @total D=M @i D=D-M @DRAW0 D;JGT @CHECK 0;JMP //here is an example of set a specific address (DRAW0) @addr A=M M=0 @addr M=M+1 @i M=M+1 @LOOP1 0;JMP @CHECK 0;JMP (BLACK) @i M=0 (LOOP2) @total D=M @i D=D-M @DRAW1 D;JGT @CHECK 0;JMP (DRAW1) @addr A=M M=-1 @addr M=M+1 @i M=M+1 @LOOP2 0;JMP @CHECK 0;JMP 参考代码如上。要记得在CPU simulator里面把对应的模式改成No animation。\n这部分内容写起来还是比较恶心的，因为它反直觉，需要我们把操作掰开揉碎了喂给电脑。但是通过查阅课程代码以及“面向报错和AI编程”，还是可以完成的。\n我之所以选择Nand2Tetris，是因为我不满足于平日写的一些high-level编程代码，而希望可以更深入地了解计算机运行的底层机制。然而通过本节课繁琐的机器语言的代码实战，让我意识到了high-level代码的意义，真可谓“真让你学了你又不乐意了”。这也正体现了计算机知识体系本身强烈的层次性。\n接下来我们将进一步接触计算机架构和编译器，继续加油吧。\n","date":"2026-03-04T00:00:00Z","image":"https://PeterZhang9595.github.io/p/nand2tetris4/ml_hu_df1298c7b6e0d458.png","permalink":"https://PeterZhang9595.github.io/p/nand2tetris4/","title":"Nand2Tetris Project(4)"},{"content":"Project3: Memory 在本次项目中我们将从寄存器出发，逐步实现不同大小的RAM。\nBit 这个问题的主要难点在于如何去设置每一轮中DFF的输出作为Mux的输入。\n而为了解决这一个问题，我们需要澄清一个潜在的误区，即HDL与平时我们使用的编程语言不同，它是一种声明式语言，所有的语句是并行执行的。\n所以我们可以直接在Mux的接口就使用一个feedback作为传输的中间值，而在DFF中再对它进行定义即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * 1-bit register: * If load is asserted, the register\u0026#39;s value is set to in; * Otherwise, the register maintains its current value: * if (load(t)) out(t+1) = in(t), else out(t+1) = out(t) */ CHIP Bit { IN in, load; OUT out; PARTS: //// Replace this comment with your code. Mux(a=feedback,b=in,sel=load,out=input); DFF(in=input,out=out,out=feedback); } Register 使用Bit进行逐位置运算即可。\nRAM 核心步骤一共有三步：\n根据地址选择到底应该在哪个Register上面进行操作。这里我们使用DMux8Way。 根据第一步拆解出的8个分量，分配给各个Register。 对于8个分量的输出，同样根据地址选择应该输出哪个output作为RAM的最终输出。这里使用Mux8Way16。 在硬件层面编程，实现分支判断是一场灾难。所以我们不得不把所有分支的结果全部进行计算，然后使用已经提前完成的分支选择器选择一个作为输出。这和我们常见的编程语言的基础思想是不一样的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * Memory of eight 16-bit registers. * If load is asserted, the value of the register selected by * address is set to in; Otherwise, the value does not change. * The value of the selected register is emitted by out. */ CHIP RAM8 { IN in[16], load, address[3]; OUT out[16]; PARTS: DMux8Way(in=load,sel=address,a=a,b=b,c=c,d=d,e=e,f=f,g=g,h=h); Register(in=in ,load=a ,out=out0 ); Register(in=in ,load=b ,out=out1 ); Register(in=in ,load=c ,out=out2 ); Register(in=in ,load=d ,out=out3 ); Register(in=in ,load=e ,out=out4 ); Register(in=in ,load=f ,out=out5 ); Register(in=in ,load=g ,out=out6 ); Register(in=in ,load=h ,out=out7 ); Mux8Way16(a=out0,b=out1,c=out2,d=out3,e=out4,f=out5,g=out6,h=out7,sel=address,out=out); } 完成RAM8之后，更大的RAM使用相同的方式也可完成，不再赘述。\nPC 插播一个HDL里面的坑：变量名里面不要加_ !!!!!!!!!!否则会报错。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * A 16-bit counter. * if reset(t): out(t+1) = 0 * else if load(t): out(t+1) = in(t) * else if inc(t): out(t+1) = out(t) + 1 * else out(t+1) = out(t) */ CHIP PC { IN in[16],inc, load, reset; OUT out[16]; PARTS: Inc16(in=state,out=outinc); Mux16(a=feedback,b=outinc,sel=inc,out=temp1); Mux16(a=temp1,b=in,sel=load,out=temp2); Mux16(a=temp2,b=false,sel=reset,out=seloutput); Register(in=seloutput,load=true,out=state,out=out); } 依然是经典使用HDL完成分支判断。本题里面的分支判断蕴含有较强的优先性大小，所以需要先使用Register获取state，然后从优先级较低的inc变量开始逐级判断，完成优先级高的变量对优先级低的变量的覆盖。这与传统的纯粹并行分支判断不同。\n以上就是本次Project的全部解答了。可以说本章引入的时序逻辑问题还是有一点绕的，但是如果理解了HDL的声明式编程原理，我们在编程的时候就会更少的受到曾经学过的编程语言的思想的束缚，写出更符合电路设计的代码。\n","date":"2026-03-02T00:00:00Z","image":"https://PeterZhang9595.github.io/p/nand2tetris3/RAM-vs-ROM_hu_b938288bf0c9d805.png","permalink":"https://PeterZhang9595.github.io/p/nand2tetris3/","title":"Nand2Tetris Project(3)"},{"content":"Project2:Boolean Arithmetic Nand2Tetris的第二课Project需要我们逐步利用逻辑门实现基本的运算逻辑单元。\nHalfAdder 实现一个单Bit的加法器，输入a和b，输出sum和carry(进位)。 这是一个再简单不过的加法运算了，然而如果我们想用已有的逻辑门完成还需要导一下。 经过观察，carry符合AND门的输出规律，sum符合XOR门的输出规律。调用原有API即可。\n1 2 3 4 5 6 7 8 9 10 11 12 /** * Computes the sum of two bits. */ CHIP HalfAdder { IN a, b; // 1-bit inputs OUT sum, // Right bit of a + b carry; // Left bit of a + b PARTS: And(a=a,b=b,out=carry); Xor(a=a,b=b,out=sum); } FullAdder 实现3个bit相加，这里我们调用前面已经完成好的HalfAdder即可。但由于加法的省略机制，这里要注意处理好进位。 我们同样需要注意的一点是，虽然在实现的时候我们是按照存在先后顺序的方式完成的，但是进位不会和sum产生任何纠葛。\n1 2 3 4 5 6 7 8 9 10 11 12 13 /** * Computes the sum of three bits. */ CHIP FullAdder { IN a, b, c; // 1-bit inputs OUT sum, // Right bit of a + b + c carry; // Left bit of a + b + c PARTS: HalfAdder(a=a,b=b,sum=t1,carry=t2); HalfAdder(a=t1,b=c,sum=sum,carry=t3); HalfAdder(a=t2,b=t3,sum=carry,carry=t4); } Add16 实现两个16bit数字的相加，难点在于进位的处理。 我们采用15个FullAdder+1个HalfAdder的构建方式。此处不列出代码了。 要注意我们在现成的HDL语言中无法直接使用特定数字为变量赋值。看了一下大神的笔记，原来可以用True or False赋值。\nInc16 一个实现给输入加1的累加器。\n1 2 3 4 5 6 7 8 9 10 11 /** * 16-bit incrementer: * out = in + 1 */ CHIP Inc16 { IN in[16]; OUT out[16]; PARTS: Add16(a=in,b[0]=true,out=out); } 这里值得一提的是HDL中给一个变量赋值，要使用布尔值。同时对于一个多bit变量，其各位数值默认为false(0),如果要指定为true(1)要利用角标手动访问指定。\nALU 最后是经典的黑盒，ALU。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 /** * ALU (Arithmetic Logic Unit): * Computes out = one of the following functions: * 0, 1, -1, * x, y, !x, !y, -x, -y, * x + 1, y + 1, x - 1, y - 1, * x + y, x - y, y - x, * x \u0026amp; y, x | y * on the 16-bit inputs x, y, * according to the input bits zx, nx, zy, ny, f, no. * In addition, computes the two output bits: * if (out == 0) zr = 1, else zr = 0 * if (out \u0026lt; 0) ng = 1, else ng = 0 */ // Implementation: Manipulates the x and y inputs // and operates on the resulting values, as follows: // if (zx == 1) sets x = 0 // 16-bit constant // if (nx == 1) sets x = !x // bitwise not // if (zy == 1) sets y = 0 // 16-bit constant // if (ny == 1) sets y = !y // bitwise not // if (f == 1) sets out = x + y // integer 2\u0026#39;s complement addition // if (f == 0) sets out = x \u0026amp; y // bitwise and // if (no == 1) sets out = !out // bitwise not CHIP ALU { IN x[16], y[16], // 16-bit inputs zx, // zero the x input? nx, // negate the x input? zy, // zero the y input? ny, // negate the y input? f, // compute (out = x + y) or (out = x \u0026amp; y)? no; // negate the out output? OUT out[16], // 16-bit output zr, // if (out == 0) equals 1, else 0 ng; // if (out \u0026lt; 0) equals 1, else 0 PARTS: Mux16(a=x,b=false,sel=zx,out=x1); Not16(in=x1,out=negateX); Mux16(a=x1,b=negateX,sel=nx,out=x2); Mux16(a=y,b=false,sel=zy,out=y1); Not16(in=y1,out=negateY); Mux16(a=y1,b=negateY,sel=ny,out=y2); Add16(a=x2,b=y2,out=xPlusy); And16(a=x2,b=y2,out=xAndy); Mux16(a=xAndy,b=xPlusy,sel=f,out=fxy); Not16(in=fxy,out=notFxy); Mux16(a=fxy,b=notFxy,sel=no,out=out,out[15]=ng,out[0..7]=outCopy1,out[8..15]=outCopy2); Or8Way(in=outCopy1,out=allZero1); Or8Way(in=outCopy2,out=allZero2); Or(a=allZero1,b=allZero2,out=sel); Mux(a=true,b=false,sel=sel,out=zr); } 代码实现还是比较简单的，但是要仔细一点。\n关注Mux16(a=fxy,b=notFxy,sel=no,out=out,out[15]=ng,out[0..7]=outCopy1,out[8..15]=outCopy2);的写法。我们必须在这里把总线拆开分给后面的Or8Way。因为在Nand2Tetris的编程语言中，out是只写的。 Or8Way是好东西，别忘了这个的存在。 判断二进制数是否负数就看第一位；判断是否为0就看所有位是否都是0. 第二次project到此就基本完成了。任务量和难度比第一次都要小一点，更多是一些编程的练习。 出发，Project3!\n","date":"2026-02-25T00:00:00Z","image":"https://PeterZhang9595.github.io/p/nand2tetris2/alu_hu_5cb8b66a4943a38d.jpg","permalink":"https://PeterZhang9595.github.io/p/nand2tetris2/","title":"Nand2Tetris Project(2)"},{"content":"Project1:Elementary Logic Gates 在Nand2Tetris的第一个project中，我们将使用基础的HDL语言逐步实现一系列基础的逻辑门。\n实现方式 在作业中我们通过编写底层的HDL代码实现逻辑门以满足更高的抽象层面对逻辑门的功能要求。\n使用课程提供的HardwareSimulator运行chip代码。利用课程编写好的script(脚本文件)以及cmp(对比文件)对HDL代码进行测试。\n逻辑门具体实现思路 为了一步步地构建起所有的逻辑门，我们需要有一个逻辑门作为其它逻辑门的基础，而在本次课程中我们使用的便是Built-In 地逻辑门Nand，因为从理论上可证明Nand可以表示所有的布尔函数。\n具体可参照课程教材原文： 注意，以下的逻辑门建构顺序很重要，要关注前后的依赖关系\nNot 显然有 $$Not \\ x = x \\ Nand \\ x$$ 1 2 3 4 5 6 7 8 9 10 11 /** * Not gate: * if (in) out = 0, else out = 1 */ CHIP Not { IN in; OUT out; PARTS: Nand(a=in,b=in,out=out); } And 1 2 3 4 5 6 7 8 9 10 11 12 /** * And gate: * if (a and b) out = 1, else out = 0 */ CHIP And { IN a, b; OUT out; PARTS: Nand(a=a,b=b,out=aAndb); Not(in=aAndb,out=out); } Or 由德摩根定律可知 $$\ra \\ Or \\ b = Not(Not(a) \\ And \\ Not(b))\r$$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * Or gate: * if (a or b) out = 1, else out = 0 */ CHIP Or { IN a, b; OUT out; PARTS: Not(in=a,out=nota); Not(in=b,out=notb); And(a=nota,b=notb,out=notaAndnotb); Not(in=notaAndnotb,out=out); } 有了Not,And,Or三剑客，我们就可以在它们的基础上实现各种各样新的逻辑门而不依赖于Nand了。这也正是计算机设计的巧妙之处——单元测试与抽象API调用。\nXor a b out 0 0 0 0 1 1 1 0 1 1 1 0 我们使用课程中教授的布尔函数反推方法。 选取out为1的行。然后拟合它们。 可以得到 $$\r(not(a) \\ and \\ b) \\ or \\ (a \\ and \\ not(b))\r$$ 这里之所以要用or进行连接并且只连接给定的标准输出为1的布尔表达式，我认为原因在于外界给我们要设计的Xor的限制就是在输入为表格中的那几种情况的时候它应当可以作出对应的回应，剩余情况（虽然这里其实已经便利了所有输入组合，但为了方便我们也可以视为还存在剩余情况）无所谓，因此我们只需要拟合多个表达式让它们满足映射条件。而在每一轮输入中只可能输入这其中的一种情况，因此我们每次只用满足一个就可以了，所以要用Or进行连接。又由于对那些输出为0的行，即使输入满足了，相应也应当是0，所以不用把它们加入到Or连接的不同布尔表达式中去。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * Exclusive-or gate: * if ((a and Not(b)) or (Not(a) and b)) out = 1, else out = 0 */ CHIP Xor { IN a, b; OUT out; PARTS: Not (in=a,out=nota); Not (in=b,out=notb); And (a=a,b=notb,out=aAndnotb); And (a=nota,b=b,out=notaAndb); Or (a=aAndnotb,b=notaAndb,out=out); } Mux 这是一个比较新颖的逻辑门概念。简单理解它就是一个“条件判断型”逻辑门。\na b sel out 0 0 0 0 0 0 1 0 0 1 0 0 0 1 1 1 1 0 0 1 1 0 1 0 1 1 0 1 1 1 1 1 直接使用传统艺能，难免发现，这个表达式连接起来未免太长了，需要我们进行简化。\n$$\r((not a) \\ and \\ b \\ and \\ sel) \\\\\rOr \\\\\r(a \\ and \\ not(b) \\ and\\ not(sel)) \\\\\rOr \\\\\r(a \\ and \\ b \\ and \\ not(sel)) \\\\\rOr \\\\\r(a \\ and \\ b \\ and \\ sel)\r$$ 发现在markdown里面手打单词和空格太难受了也不清晰，接下来将使用逻辑符号。\n我们对前面的逻辑表达式进行简化合并。合并Or式子的时候，我们可以看式子中每个变量是否一样，如果一样的话那么这个变量就保留，反之则删除。以此得到 $$\r(a\\land\\neg sel) \\lor (b \\land sel)\r$$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * Multiplexor: * if (sel = 0) out = a, else out = b */ CHIP Mux { IN a, b, sel; OUT out; PARTS: Not(in=sel,out=notsel); And(a=a,b=notsel,out=w1); And(a=b,b=sel,out=w2); Or(a=w1,b=w2,out=out); } 这样我们就实现了基本的条件判断语句。\nDMux 这是我们遇到的第一个多输出逻辑门。sel相同时，它可以视作是Mux的逆运算。\nin sel a b 0 0 0 0 0 1 0 0 1 0 1 0 1 1 0 1 我们其实可以拆解为a和b分别进行输出，这样问题就转化为两个简单的构造。\n$$\ra = in\\land (\\neg sel)\\\\\rb= in \\land sel\r$$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * Demultiplexor: * [a, b] = [in, 0] if sel = 0 * [0, in] if sel = 1 */ CHIP DMux { IN in, sel; OUT a, b; PARTS: Not(in=sel,out=notsel); And(a=in,b=notsel,out=a); And(a=in,b=sel,out=b); } 实现了前面几个基础的单bit位逻辑门，接下来我们就要进行多bit位逻辑门的研究了。 多 bit 位逻辑门本质上是对总线中每一位信号并行地应用相同的单 bit 逻辑运算，各位之间相互独立，不存在跨位影响。这里可能会混淆的一点是，虽然在功能描述中写的是for循环，但是我们实际上运行的时候是并行运行的空间复制。\nNot16 我们从Not16入手进行逐元素取反。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * 16-bit Not gate: * for i = 0, ..., 15: * out[i] = Not(a[i]) */ CHIP Not16 { IN in[16]; OUT out[16]; PARTS: Not(in=in[0], out=out[0]); Not(in=in[1], out=out[1]); ... Not(in=in[15], out=out[15]); } And16 很遗憾，由于我们没有实现Nand16，所以我们不得不在实现And16 的时候也采取逐元素取反的方式。\nOr16 我们现在可以调用前面的16位运算，不用再费劲写逐元素运算了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * 16-bit Or gate: * for i = 0, ..., 15: * out[i] = a[i] Or b[i] */ CHIP Or16 { IN a[16], b[16]; OUT out[16]; PARTS: Not16(in=a, out=nota); Not16(in=b, out=notb); And16(a=nota, b=notb, out=temp); Not16(in=temp, out=out); } 在输入输出的声明中，需要声明[16]来指定总线数量，但是在Parts部分，如果要对总线进行整体调用，就不用再加上[]声明了。\nMux16 自由选择你的实现方式吧！可以逐元素，也可以调用前面的16bit专门API。\n接下来我们将实现有多个输入的逻辑门。\nOr8Way 使用最基础的平衡二叉树结构进行两两Or运算。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * 8-way Or gate: * out = in[0] Or in[1] Or ... Or in[7] */ CHIP Or8Way { IN in[8]; OUT out; PARTS: Or(a=in[0],b=in[1],out=w1); Or(a=in[2],b=in[3],out=w2); Or(a=in[4],b=in[5],out=w3); Or(a=in[6],b=in[7],out=w4); Or(a=w1,b=w2,out=p1); Or(a=w3,b=w4,out=p2); Or(a=p1,b=p2,out=out); } Multi-Way\\Multi-Bit Mux 一个更全面的分支选择器。原理很简单，关键在于底层如何利用sel与输入的运算来实现“选择”这一功能。 由于我们只实现过2个输出的16bit形式，因此我们仍应采用类似于平衡二叉树的并行方式进行选择。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /** * 4-way 16-bit multiplexor: * out = a if sel = 00 * b if sel = 01 * c if sel = 10 * d if sel = 11 */ CHIP Mux4Way16 { IN a[16], b[16], c[16], d[16], sel[2]; OUT out[16]; PARTS: Mux16(a=a,b=b,sel=sel[0],out=w1); Mux16(a=c,b=d,sel=sel[0],out=w2); Mux16(a=w1,b=w2,sel=sel[1],out=out); } 实现了4way版本之后，再写8way的时候我们就可以调用4way版本了。这其实也是一种“递归”思想指导函数实现的体现。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * 8-way 16-bit multiplexor: * out = a if sel = 000 * b if sel = 001 * c if sel = 010 * d if sel = 011 * e if sel = 100 * f if sel = 101 * g if sel = 110 * h if sel = 111 */ CHIP Mux8Way16 { IN a[16], b[16], c[16], d[16], e[16], f[16], g[16], h[16], sel[3]; OUT out[16]; PARTS: Mux4Way16(a=a,b=b,c=c,d=d,sel=sel[0..1],out=w1); Mux4Way16(a=e,b=f,c=g,d=h,sel=sel[0..1],out=w2); Mux16(a=w1,b=w2,sel=sel[2],out=out); } 有点意思。\nMulti-way\\Multi-bit DMux 这个有一点难度。我们之前使用的是由叶子到根的归一化的平衡二叉树，而现在要使用的是从根到叶子的发散的平衡二叉树，也就是说，我们要不断地通过调用DMux来判断唯一的输入in应当落在哪个子区域，然后在这个子区域内不断地进一步递归调用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /** * 4-way demultiplexor: * [a, b, c, d] = [in, 0, 0, 0] if sel = 00 * [0, in, 0, 0] if sel = 01 * [0, 0, in, 0] if sel = 10 * [0, 0, 0, in] if sel = 11 */ CHIP DMux4Way { IN in, sel[2]; OUT a, b, c, d; PARTS: DMux(in=in,sel=sel[1],a=t1,b=t2); DMux(in=t1,sel=sel[0],a=a,b=b); DMux(in=t2,sel=sel[0],a=c,b=d); } 以上就是Project1的全部实现思路及代码了。其实编程的很多基本思想，比如递归、抽象层等等在这个project里面就已经涉及到了。期待下一个project。\n","date":"2026-02-24T00:00:00Z","image":"https://PeterZhang9595.github.io/p/nand2tetris1/duola_hu_eb85cff8d060bada.png","permalink":"https://PeterZhang9595.github.io/p/nand2tetris1/","title":"Nand2Tetris Project(1)"},{"content":"课程简介 所属大学：UCB 先修要求：线性代数、Python编程、基础统计 课程难度：🌟🌟 内容关键词：pandas;visulization;SQL;机器学习 资源开源度：🌟🌟🌟🌟 本课程是UCB的数据科学入门课程，介绍了在数据处理、分析过程中经常使用的一些工具比如pandas,matplotlib,sql等等，同时引入了一些基础的机器学习内容如线性回归、逻辑回归、主成分分析、聚类等等。在学习这门课之后，基本上就掌握了“调包侠”的基础知识了。然而课程大多数时候还是手把手教学，给学生独立实践的机会并不多，后续可能还需要更多的特征工程、机器学习类实践才能融会贯通。\n总体来说，其实这门课的难度并不大。\n课堂：基本上在数学以及算法方面的难点都是一笔带过的，对一些编程操作、数据处理过程的讲解很详细，而且机器学习部分虽然难度低但是逻辑链非常完善，适合入门者观看。累计27次课，每次课80min左右。 作业：使用jupyter notebook完成，引导非常到位甚至有点太到位了。难度不大，一共有大概25次作业，每次基本上都是按照要求进行代码填空即可，1-2小时即可完成。课程还有两个project，但是其实和平时作业难度相当。 资源汇总 课程网站：website 课程作业：labs 作业需要本地配环境运行，但是难度不大。 课程教材：book 没怎么看过这个教材，如果想快速过一下这门课的话看课程网站的Course Notes就可以了。 我的资料：笔记+作业 里面是我的一些简单的课程notes以及作业的实现，仅供参考。 学习收获 这是我第一个真正意义上自学完成的课程，也算是突破了之前每次自学课程的时候新鲜劲一过就不知不觉半途而废的“自学魔咒”。\n一直以来UCB的课程都是我的首选，因为他们给的实在太多了。这门课也延续了以往UCB课程的特点，课程资源及其丰富详实，如果是在PKU内的一门课，我会觉得它太过于冗余且耗费精力，但是作为自学资源，这反而有利于更加个性化的学习路径的规划。\n感谢Narges Norouzi以及Joseph E. Gonzalez两位教授开源了课程的全部作业代码以及笔记，也感谢你们在课程中设计的一个个通俗易懂的demo。\n这门课中难能可贵的一点是，它指出了数据科学中一些“不那么科学”的内容，比如在房价预测project中，深入探讨了如何用量化的方式去定义诸如“公平”这类的非定量但是是社会核心价值观念所追求的东西。在现实应用场景中亦如是，如何制定数据的评判标准？如何利用数据对人、人群的行为进行归因？这不仅仅是数学和计算机的问题，更是历史、政治和社会学的综合问题。与人斗，其乐无穷。\n上完此课，我才意识到我们平时的机器学习、深度学习中常常忽略的东西，那就是数据处理、特征工程这一部分。即使随着深度学习的发展，神经网络能自动对特征进行提取，但是一些基础的数据清洗、补全、整合等等的内容，仍然是不可或缺的。因此应当完善整个技术栈，从一些基础的网页爬虫，到数据清洗、特征工程、数据库管理，最后再到深度学习来进行回归、分类等任务，都要有所涉猎乃至精通。\n当然，data100仅仅是一个入门级别的课程，我们的征途，是星辰大海。\n","date":"2026-02-20T00:00:00Z","image":"https://PeterZhang9595.github.io/p/data100/pandas_hu_cb7b83d39a99fbbb.png","permalink":"https://PeterZhang9595.github.io/p/data100/","title":"UCB Data100 学习总结"}]