-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.json
More file actions
1 lines (1 loc) · 280 KB
/
search.json
File metadata and controls
1 lines (1 loc) · 280 KB
1
[{"title":"Gosper-Zeilberger算法","url":"/archives/42403/","content":"\n\nGosper-Zeilberger 算法是 Gosper 算法的推广,用于计算任意 hypergeometric 项的求和。\n\n<!-- more -->\n\n# 主要思路\n\n对于和式\n\n$$\nS=\\sum_{k=1}^nt(k)\n$$\n\n如果其中的项 $t(k)$ 是 hypergeometric 的,那么我们可以将其分解为 $t(k)=T(k+1)-T(k)$,从而使和式变为\n\n$$\nS=\\sum_{k=1}^nt(k)=\\sum_{k=1}^n[T(k+1)-T(k)]=T(n+1)-T(1)\n$$\n\n# 算法步骤\n\n1. 首先我们基于 hypergeometric 项构造另一个 hypergeometric 项:\n\n $$\n \\hat{t}(n,k)=\\beta_0(n)t(n,k)+\\beta_1(n)t(n+1,k)\n $$\n\n2. 然后对该 hypergeometric 项,先分解\n\n $$\n \\frac{\\hat{t}(n, k+1)}{\\hat{t}(n, k)}=\\frac{\\hat{p}(n, k+1)}{\\hat{p}(n, k)}\\frac{q(k)}{r(k+1)}\n $$\n\n3. 从 $\\hat{p}(n,k)=1$ 开始,找到所有 $\\alpha, \\beta$ 满足\n\n $$\n (k+\\alpha)|q(k), (k+\\beta)|r(k), \\alpha-\\beta<0\n $$\n\n 当 $\\alpha-\\beta>0$ 时,我们更新 $\\hat{p}(n,k)\\to\\hat{p}(n,k)(k+\\alpha-1)(k+\\alpha-2)...(k+\\beta+1)$.\n\n4. 令 $Q(k)=q(k)-r(k), R(k)=q(k)+r(k)$,并作比较:\n\n * 若 $d_Q\\geq d_R$,则 $d=d_p-d_Q$\n * 若 $d_Q<d_R$,则 $d=d_p-d_R+1$\n\n 其中 $d_p, d_Q, d_R$ 分别是 $\\hat{p(k)}, Q(k), R(k)$ 中 $k$ 的阶。注意对零值函数有特殊的取值,即若 $f(k)=0$,则 $d_f=-1$。\n\n5. 若 $d<0$,则需要进入下一轮的 Gosper-Zeilberger 算法,更新 \n\n $$\n \\hat{t}(n,k)=\\beta_0(n)t(n,k)+\\beta_1(n)t(n+1,k)+\\beta_2(n)t(n+2,k)\n $$\n\n 反之,进入下一步。\n\n6. 从 Gosper 等式中求出 $\\beta_i,i=0,1,...$:\n\n $$\n \\hat{p}(n,k)=q(k)s(k+1)-r(k)s(k)\n $$\n\n 其中 $d_s=d,s(k)=s_dk^d+s_{d-1}k^{d-1}+...+s_1k+s_0$.\n\n7. 最后,我们可以作分解:$t(k)=T(k+1)-T(k)$,其中 $T(k)=\\frac{r(k)s(k)t(k)}{p(k)}$.\n\n# 例子\n\n计算 \n\n$$\nT(r,l)=\\sum_{s=1}^M\\begin{pmatrix} r \\\\ s \\end{pmatrix}\\begin{pmatrix} l-1 \\\\ s-1 \\end{pmatrix}(-1)^{r-s}\n$$\n\nLet $t(r,l,s)=\\begin{pmatrix}\n r \\\\ s\n \\end{pmatrix}\\begin{pmatrix}\n l-1 \\\\ s-1\n \\end{pmatrix}(-1)^{r-s}$. If we fix $l$ and increase $r$ by one, then we have $t(r+1,l,s)=-\\frac{r+1}{r-s+1}t(r,l,s)$, which means\n$$\\begin{align*}\n \\hat{t}(r,l,s)&=\\beta_0t(r,l,s)+\\beta_1t(r+1,l,s) \\\\\n &=[\\beta_0(r-s+1)-\\beta_1(r+1)]\\frac{t(r,l,s)}{r-s+1} \\\\\n &=\\hat{p}(r,l,s)\\frac{t(r,l,s)}{r-s+1}\n\\end{align*}$$\nwhere $\\beta_0$ and $\\beta_1$ are constants. So we have\n$$\\begin{align*}\n \\frac{\\hat{t}(r,l,s+1)}{\\hat{t}(r,l,s)}=\\frac{\\hat{p}(r,l,s+1)}{\\hat{p}(r,l,s)}\\left(-\\frac{r-s+1}{r-s}\\frac{r-s}{s+1}\\frac{l-s}{s}\\right)=\\frac{\\hat{p}(r,l,s+1)}{\\hat{p}(r,l,s)}\\left(-\\frac{(r-s+1)(l-s)}{s(s+1)}\\right)\n\\end{align*}$$\nLet $p(s)=\\hat{p}(r,l,s)$, $q(s)=-(r-s+1)(l-s)=-s^2+(r+l+1)s-l(r+1)$, $r(s)=(s-1)s=s^2-s$, then we have\n$$\\begin{align*}\n Q(s)&=q(s)-r(s)=-2s^2+(r+l+1)s-l(r+1) \\\\\n R(s)&=q(s)+r(s)=(r+l)s-l(r+1)\n\\end{align*}$$\nthat is, \n$$deg_Q(s)=2>deg_R(s)=1\\implies d=deg_p(s)-deg_q(s)=1-2=-1<0$$\nwhich means we need one more iteration of Gosper-Zeilberger algorithm. The steps are similar.\n\nSince $t(r+2,l,k)=\\frac{r+2}{r-s+2}t(r+1,l,s)=\\frac{(r+2)(r+1)}{(r-s+2)(r-s+1)}t(r,l,s)$, we have\n$$\\begin{align*}\n \\hat{t}(r,l,s)&=\\beta_0t(r,l,s)+\\beta_1t(r+1,l,s)+\\beta_2t(r+2,l,s) \\\\\n &=[\\beta_0(r-s+2)(r-s+1)-\\beta_1(r+1)(r-s+2)+\\beta_2(r+1)(r+2)]\\frac{t(r,l,s)}{(r-s+2)(r-s+1)} \\\\\n &=\\hat{p}(r,l,s)\\frac{t(r,l,s)}{(r-s+2)(r-s+1)}\n\\end{align*}$$\nwhere $\\beta_0$, $\\beta_1$ and $\\beta_2$ are constants. So we have\n$$\\begin{align*}\n \\frac{\\hat{t}(r,l,s+1)}{\\hat{t}(r,l,s)}=\\frac{\\hat{p}(r,l,s+1)}{\\hat{p}(r,l,s)}\\left(-\\frac{r-s+2}{r-s}\\frac{r-s}{s+1}\\frac{l-s}{s}\\right)=\\frac{\\hat{p}(r,l,s+1)}{\\hat{p}(r,l,s)}\\left(-\\frac{(r-s+2)(l-s)}{s(s+1)}\\right)\n\\end{align*}$$\nLet $p(s)=\\hat{p}(r,l,s)$, $q(s)=-(r-s+2)(l-s)=-s^2+(r+l+2)s-l(r+2)$, $r(s)=(s-1)s=s^2-s$, then we have\n$$\\begin{align*}\n Q(s)&=q(s)-r(s)=-2s^2+(r+l+3)s-l(r+2) \\\\\n R(s)&=q(s)+r(s)=(r+l+1)s-l(r+2)\n\\end{align*}$$\nthat is, \n$$deg_Q(s)=2>deg_R(s)=1\\implies d=deg_p(s)-deg_q(s)=2-2=0$$\nso we can let $u(s)=u$, a constant. From the equation $p(s)=q(s)u(s+1)-r(s)u(s)$, we have\n$$\\begin{align*}\n &\\beta_0(r-s+2)(r-s+1)-\\beta_1(r+1)(r-s+2)+\\beta_2(r+1)(r+2)=-2us^2+(r+l+3)us-l(r+2)u \\\\\n \\implies &\\beta_0s^2-[(2r+3)\\beta_0-(r+1)\\beta_1]s+(r+1)(r+2)(\\beta_0-\\beta_1+\\beta_2)\\\\&=-2us^2+(r+l+3)us-l(r+2)u\n\\end{align*}$$\nwhich means,\n$$\\begin{align*}\n \\left\\{\\begin{array}{l}\n \\beta_0 = -2u \\\\\n -(2r+3)\\beta_0+(r+1)\\beta_1 = (r+l+3)u \\\\\n (r+1)(r+2)(\\beta_0-\\beta_1+\\beta_2) = -l(r+2)u\n \\end{array}\\right. \\implies \n \\left\\{\\begin{array}{l}\n \\beta_0 = -2u \\\\\n (r+1)\\beta_1 = (l-3r-4)u \\\\\n (r+1)\\beta_2 = -(r+2)u\n \\end{array}\\right.\n\\end{align*}$$\nWe can let $u=r+1$, so we have \n$$\\begin{align*}\n \\left\\{\\begin{array}{l}\n \\beta_0 = -2(r+1) \\\\\n \\beta_1 = l-3r-4 \\\\\n \\beta_2 = -(r+2)\n \\end{array}\\right.\n\\end{align*}$$\nTherefore,\n$$\\begin{align*}\n &\\beta_0t(r,l,s)+\\beta_1t(r+1,l,s)+\\beta_2t(r+2,l,s)=\\hat{t}(r,l,s) \\\\\n \\implies &\\beta_0\\sum_{s=1}^Mt(r,l,s)+\\beta_1\\sum_{s=1}^Mt(r+1,l,s)+\\beta_2\\sum_{s=1}^Mt(r+2,l,s)=\\sum_{s=1}^M\\hat{t}(r,l,s) \\\\\n \\implies &-2(r+1)T(r,l,s)+(l-3r-4)T(r+1,l,s)-(r+2)T(r+2,l,s)=0 \\\\\n \\implies &(r+2)T(r+2,l,s)=-2(r+1)T(r,l,s)+(l-3r-4)T(r+1,l,s)\n\\end{align*}$$\n","tags":["离散数学","求和"],"categories":["math"]},{"title":"名言","url":"/archives/4099/","content":"\n我们的非法操作时常会导致数学定义失效, 不过定义总可以改良, 方法总可以推广, 我们的思想才是不会失效的东西. 所以你问我你这个积分是什么定义的积分啊, 我愿称之为物理人积分. 它的小名叫做:“先这么用着吧, 出了问题再去请数学人帮帮忙想个好用点儿的定义积分.”","categories":["life"]},{"title":"量子计算:量子算法","url":"/archives/1880/","content":"\n# Deutsch-Jozsa Algorithm\n\n这个算法仅仅为了说明量子计算在加速计算上的优越性,并没有什么实际作用\n\n## Deutsch Algorithm — Problem Description\n\n假设我们有一个函数:$f:\\{0,1\\}\\mapsto\\{0,1\\}$。若对所有输入都有相同输出的话,则称之为 constant;若一半输出与另一半不同,则为 balanced。\n\nDeutsch Algorithm 目的在于给定一个这样的函数,确定该函数是 constant 还是 balanced。\n\n\n\n\n\n\n\n","tags":["量子计算"],"categories":["physic"]},{"title":"量子计算:量子电路","url":"/archives/27241/","content":"\n\n## Quantum Circuits\n\n量子电路即对 qubit 操作的图形表示,时间顺序为从左至右。有两种 qubit 操作:量子逻辑门(quantum logic gate)、测量(measurement)。\n\n\n\n## Quantum Logic Gates\n\n所有的量子逻辑门都是酉算子(unitary operator)\n\n\n\n\n\nExample:\n\n将 Pauli X matrix 作用于基底,可以得到相反的基底,所以 Pauli X matrix 也被称作 NOT 门\n\n$$\n\\begin{align*}\nX\\ket{0}=\\left[\\begin{matrix}\n 0 & 1 \\\\\n 1 & 0\n\\end{matrix}\\right]\\left[\\begin{matrix}\n 1 \\\\\n 0\n\\end{matrix}\\right]=\\left[\\begin{matrix}\n 0 \\\\\n 1\n\\end{matrix}\\right]=\\ket{1} \\\\\n \nX\\ket{1}=\\left[\\begin{matrix}\n 0 & 1 \\\\\n 1 & 0\n\\end{matrix}\\right]\\left[\\begin{matrix}\n 0 \\\\\n 1\n\\end{matrix}\\right]=\\left[\\begin{matrix}\n 1 \\\\\n 0\n\\end{matrix}\\right]=\\ket{0}\n\\end{align*}\n$$\n\n将 Hadamard gate 作用于基底,可以获得基底的叠加态\n\n$$\n\\begin{align*}\nH\\ket{0}=\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 & 1 \\\\\n 1 & -1\n\\end{matrix}\\right]\\left[\\begin{matrix}\n 1 \\\\\n 0\n\\end{matrix}\\right]=\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 \\\\\n 1\n\\end{matrix}\\right]=\\frac{\\ket{0}+\\ket{1}}{\\sqrt{2}} \\\\\nH\\ket{1}=\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 & 1 \\\\\n 1 & -1\n\\end{matrix}\\right]\\left[\\begin{matrix}\n 0 \\\\\n 1\n\\end{matrix}\\right]=\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 \\\\\n -1\n\\end{matrix}\\right]=\\frac{\\ket{0}-\\ket{1}}{\\sqrt{2}}\n\\end{align*}\n$$\n\n另外还有常用的 Controlled NOT gate(CNOT),这个门会根据第一个 qubit 的取值而决定是否往第二个 qubit 上作用 NOT 门,即第一个 qubit 为 0 时不作用,为 1 时作用。\n\n$$\n\\begin{align*}\n&CX\\ket{00}=\\left[\\begin{matrix}\n 1 & 0 & 0 & 0 \\\\\n 0 & 1 & 0 & 0 \\\\\n 0 & 0 & 0 & 1 \\\\\n 0 & 0 & 1 & 0 \\\\\n\\end{matrix}\\right]\\left[\\begin{matrix}\n 1 \\\\\n 0 \\\\\n 0 \\\\\n 0\n\\end{matrix}\\right]=\\left[\\begin{matrix}\n 1 \\\\\n 0 \\\\\n 0 \\\\\n 0\n\\end{matrix}\\right]=\\ket{00} \\\\\n&CX\\ket{01}=\\ket{01} \\quad CX\\ket{10}=\\ket{11} \\quad CX\\ket{11}=\\ket{10} \\quad\n\\end{align*}\n$$\n\n\n\n## No-cloning Theorem\n\n没有在 $H\\otimes H$ 上的酉算子 $U$,能对 $H$ 中的归一化状态 $\\ket{\\psi}_A$ 和状态 $\\ket{e}_B$ 有如下的操作\n\n$$\nU(\\ket{\\psi}_A\\ket{e}_B)=e^{i\\alpha(\\psi,e)}\\ket{\\psi}_A\\ket{\\psi}_B\n$$\n\nProof:\n\n假设存在这样的 $U$,那么有\n\n$$\n\\begin{align*}\nU\\ket{\\psi}_A\\ket{e}_B=\\ket{\\psi}_A\\ket{\\psi}_B \\\\\nU\\ket{\\phi}_A\\ket{e}_B=\\ket{\\phi}_A\\ket{\\phi}_B\n\\end{align*}\n$$\n\n将两式相点乘,可以得到\n\n$$\n\\begin{align*}\n\\text{left}=\\bra{e}_B\\bra{\\psi}_AU^{\\dagger} U\\ket{\\psi}_A\\ket{e}_B=(\\braket{\\psi|\\psi})_A(\\braket{e|e})_B=\\braket{\\psi|\\psi} \\\\\n\\text{right}=\\bra{\\psi}_B\\bra{\\psi}_A\\ket{\\phi}_A\\ket{\\phi}_B=(\\braket{\\psi|\\phi})_A(\\braket{\\psi|\\phi})_B=|\\braket{\\psi|\\phi}|^2\n\\end{align*}\n$$\n\n因此有\n\n$$\n\\braket{\\psi|\\psi}=|\\braket{\\psi|\\phi}|^2 \\implies \\braket{\\psi|\\psi}=0\\text{ or }1\n$$\n\n所以除了互相正交的基底,我们不可能仅通过当前的状态情况复制该状态。\n\n注意:这不意味着我们不可以复制量子状态,而是说不可能有人能窃听到量子通信中的信息。\n\n## Measurement\n\n薛定谔的猫(Schrödinger's cat),只有当测量的时候才能知道真实的情况。\n\n对 qubit 的测量会导致其以一定概率坍缩到确定的状态($\\ket{0}$ 或 $\\ket{1}$),但要获得最终结果,必须要测量 qubit\n\n\n\nExample:\n\n测量如下的 qubit,将会得到什么预期结果呢?\n\n$$\nH\\ket{1}=\\frac{\\ket{0}-\\ket{1}}{\\sqrt{2}}\n$$\n\n预期情况下,会有 50% 的概率测量得到 $\\ket{0}$,会有 50% 的概率测量得到 $\\ket{1}$\n\n一般来说,对 qubit $\\ket{\\psi}=\\alpha\\ket{0}+\\beta\\ket{1}$,会有 $|\\alpha|^2$ 的概率测量得到 $\\ket{0}$,会有 $|\\beta|^2$ 的概率测量得到 $\\ket{1}$。而对归一化的 qubit 来说,$|\\alpha|^2+|\\beta|^2=1$\n\n## Reversibility in Quantum Computing\n\n函数是可逆的(reversible)当且仅当它是双射的(bijective)。\n\n\n\n经典计算机中的逻辑门不一定是可逆的,如 NOT 门、AND 门,将它们作用于 bit 之后,会丢失一些信息。\n\n\n\n但我们可以通过添加一些辅助 bit 将逻辑门转化成可逆的。\n\n假设 $f:i\\to f(i)$ 有 $n$ 个 输入和 $m$ 个输出,我们需要增加额外 $n$ 个 输出和 $m$ 个输入,并将函数改为 $f_{rev}:i,j\\to i,f(i)\\oplus j$,其中 $\\oplus$ 为 XOR\n\n\n\n量子电路中的逻辑门必须是酉的(unitary),而逻辑门 $U$ 的逆是唯一的,即 $U^{\\dagger}$\n\nExample:\n\nHadamard 门的逆\n\n$$\n\\begin{align*}\nH\\ket{0}=\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 & 1 \\\\\n 1 & -1\n\\end{matrix}\\right]\\left[\\begin{matrix}\n 1 \\\\\n 0\n\\end{matrix}\\right]=\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 \\\\\n 1\n\\end{matrix}\\right]=\\frac{\\ket{0}+\\ket{1}}{\\sqrt{2}} \\\\\nH^{\\dagger}\\frac{\\ket{0}+\\ket{1}}{\\sqrt{2}}=\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 & 1 \\\\\n 1 & -1\n\\end{matrix}\\right]\\frac{1}{\\sqrt{2}}\\left[\\begin{matrix}\n 1 \\\\\n 1\n\\end{matrix}\\right]=\\left[\\begin{matrix}\n 1 \\\\\n 0\n\\end{matrix}\\right]=\\ket{0}\n\\end{align*}\n$$\n\n要将经典电路中的逻辑门改成量子电路中的逻辑门,可根据先前所说的增加一些辅助 qubit 即可。\n\n量子 AND 门、量子 NAND 门\n\n\n\n量子 OR 门、量子 NOR 门\n\n\n\n## Universal Gate Sets\n\n我们可以通过有限的门数,将任意酉算子近似到任意给定的精度(类似于泰勒展开式)。\n\n完成这项工作的门的集合被称为通用门集合(universal gate set),如\n\n* {Hadamard,Phase,$\\pi/8$,CNOT}\n* {Hadamard,Phase,Toffoli,CNOT}\n\n## Decomposition of Unitary Matrix\n\n一个 $d$ 维的酉矩阵 $U$ 可被分解为二级酉矩阵的乘积\n\n$$\nU=V_1V_2...V_k\n$$\n\n其中 $k\\le(d-1)+(d-2)+...+1=d(d-1)/2$\n\n这意味着我们可以用二级酉矩阵来表示任意酉矩阵。\n\n具体推导可以参考文章 [Decomposition of unitary matrices and quantum gates](https://arxiv.org/abs/1210.7366)\n\n\n## Reference\n\n[大一新生也能懂的量子计算](https://www.bilibili.com/video/BV1oq4y1j7iZ/?spm_id_from=333.337.search-card.all.click&vd_source=201635f84ae101b427649f98a07303b1)\n\n\n[Quantum Computing阅读笔记:第四章](https://zhuanlan.zhihu.com/p/158725989)","tags":["量子计算"],"categories":["physic"]},{"title":"量子计算:数学基础","url":"/archives/17052/","content":"\n量子计算的数学基础\n\n<!-- more -->\n\n## Abstract vector space\n\n向量空间需要满足以下几个公理。向量空间中的元素即向量。\n\n\n\n## Inner product and Hilbert space\n\n定义 $\\ket{\\phi}$ 和 $\\ket{\\psi}$ 的内积为:\n\n* 若 $\\ket{\\phi}$ 和 $\\ket{\\psi}$ 是函数:\n\n$$\n\\braket{\\phi|\\psi}=\\int_{-\\infty}^{+\\infty}\\bar{\\phi}(x)\\psi(x) dx\n$$\n\n* 若 $\\ket{\\phi}$ 和 $\\ket{\\psi}$ 是列向量:\n\n$$\n\\braket{\\phi|\\psi}=\\phi^\\dagger\\psi=\\bar{\\phi}^T\\psi\n$$\n\n内积的物理解释:如果粒子的初始态为 $\\ket{\\phi}$,那么在 $\\ket{\\psi}$ 态发现该粒子的概率为 $|\\braket{\\phi|\\psi}|^2$\n\n希尔伯特空间即一个带内积的复向量空间\n\n## Qubits\n\nQubit 满足以下两个性质:\n\n* Nomalized(归一的):\n\n$$\n\\braket{0|0}=1, \\braket{1|1}=1\n$$\n\n* Orthogonal(正交的):\n\n$$\n\\braket{0|1}=0, \\braket{1|0}=0\n$$\n\n因此,qubit 形成了 Hilbert 空间的一个基,即任何量子态都能用 $\\ket{0}$ 和 $\\ket{1}$ 的线性组合表示。\n\nqubit 内积也可以表示为:\n\n$$\n\\braket{i|j}=\\delta_{ij}\n$$\n\n$\\delta_{ij}$ 即为 Kronecker delta,满足\n\n$$\n\\delta_{ij}=\n \\begin{cases}\n 0, i\\neq j \\\\\n 1, i=j\n \\end{cases}\n$$\n\n## Superposition of states\n\n叠加态即为 qubit 的线性组合\n\n$$\n\\braket{\\psi}=\\alpha\\ket{0}+\\beta\\ket{1}\n$$\n\n其中,$|\\alpha|^2+|\\beta|^2=1$,且 $|\\alpha|^2$ 表示 $\\ket{0}$ 测量出的概率。\n\n## Tensor product\n\n两个向量的张量积即为\n\n$$\nu\\otimes v=\n \\left[\\begin{array}{c}\n u_1 \\\\\n u_2\n \\end{array}\\right]\\otimes\n \\left[\\begin{array}{c}\n v_1 \\\\\n v_2\n \\end{array}\\right]=\n \\left[\\begin{array}{c}\n u_1\\left[\\begin{array}{c}\n v_1 \\\\\n v_2\n \\end{array}\\right] \\\\\n u_2\\left[\\begin{array}{c}\n v_1 \\\\\n v_2\n \\end{array}\\right]\n \\end{array}\\right]=\n \\left[\\begin{array}{c}\n u_1v_1 \\\\\n u_1v_2 \\\\\n u_2v_1 \\\\\n u_2v_2\n \\end{array}\\right]\n$$\n\n使用 Dirac 符号表示即为\n\n$$\n\\ket{\\psi}\\otimes\\ket{\\phi}\\equiv\\ket{\\psi}\\ket{\\phi}\\equiv\\ket{\\psi,\\phi}\\equiv\\ket{\\psi\\phi}\n$$\n\n张量积具有如下性质\n\n$$\n(v+w)\\otimes u = v\\otimes u+w\\otimes u \\\\\nu\\otimes(v+w) = u\\otimes v+u\\otimes w \\\\\nc(v\\otimes u) = (cv)\\otimes u = v\\otimes(cu)\n$$\n\n张量积是不能交换的!!\n\n## Inner product on the tensor products\n\nSuppose $\\ket{\\psi_1}, \\ket{\\phi_1}\\in H_1$ and $\\ket{\\psi_2}, \\ket{\\phi_2}\\in 𝐻_2$ where $𝐻_1$ and $𝐻_2$ are two Hilbert spaces. Then\n\n$$\n\\braket{\\phi_1,\\phi_2|\\psi_1,\\psi_2}=\\braket{\\phi_1|\\psi_1}_1\\braket{\\phi_2|\\psi_2}_2\n$$\n\nWhere $\\braket{\\cdot|\\cdot}_1$ and $\\braket{\\cdot|\\cdot}_2$ are inner product defined on $𝐻_1$ and $𝐻_2$, respectively.\n\nExample:\n\n$$\n\\braket{01|01}=\\braket{0|0}\\braket{1|1}=1\\cdot1=1 \\\\\n\\braket{011|010}=\\braket{0|0}\\braket{1|1}\\braket{1|0}=1\\cdot1\\cdot0=0\n$$\n\n## Outer product\n\nThe outer product of two vectors $\\ket{\\psi}$ and $\\ket{\\phi}$ are defined as $\\ket{\\psi_1}\\bra{\\phi}=\\ket{\\psi_1}\\otimes\\bra{\\phi}$\n\nOuter product is used to produce operators (算符).\n\nExample: \n\nLet $\\ket{\\psi}=\\sqrt{1/2}\\ket{0}+\\sqrt{1/2}\\ket{1}$. Then $\\hat{𝑃}_0\\equiv\\ket{0}\\bra{0}$ is a projection operator (投影算符) and it projects the wave function onto $\\ket{0}$ state.\n\n$$\n\\hat{𝑃}_0\\ket{\\psi}=\\sqrt{1/2}\\ket{0}\\bra{0}\\ket{0}+\\sqrt{1/2}\\ket{0}\\bra{0}\\ket{1}=\\sqrt{1/2}\\ket{0}\n$$\n\n## Eigenvalues and Eigenvectors\n\nLet $\\hat{A}$ be an operator. If\n\n$$\n\\hat{A}\\ket{\\psi}=\\lambda\\ket{\\psi}\n$$\n\nThen $\\lambda$ is an eigenvalue (特征值) of $\\hat{A}$ and $\\ket{\\psi}$ is an eigenvector (特征向量) associated with $\\lambda$.\n\n\nExample: \n\nFind the eigenvalues and eigenvectors of projection operator\n$\\hat{𝑃}_0\\equiv\\ket{0}\\bra{0}$\n\nSolution: Eigenvalues are 1 and 0, cooresponding eigenvectors are $\\ket{0}$ and $\\ket{1}$.\n\n$$\n\\hat{𝑃}_0\\ket{0}=1\\cdot\\ket{0} \\\\\n\\hat{𝑃}_0\\ket{1}=0\\cdot\\ket{1}\n$$\n\n## Spectral theorem \n\nSuppose the eigenvalues $\\lambda_1,\\lambda_2,...,\\lambda_n$ of $\\hat{A}$ are real, and their corresponding eigenvectors are $\\ket{\\psi_1},\\ket{\\psi_2},...,\\ket{\\psi_n}$. Then the operator $\\hat{A}$ can be written as\n\n$$\n\\hat{A}=\\sum_{i=1}^n\\lambda_i\\ket{\\psi_i}\\bra{\\psi_i}\n$$\n\nMoreover, $\\{\\ket{\\psi_1},\\ket{\\psi_2},...,\\ket{\\psi_n}\\}$ form a complete basis for the Hilbert space $\\hat{A}$ living in.\n\nSometimes, this is called spectral decomposition of operator $\\hat{A}$.\n\n## Completeness relation\n\nFrom spectral theorem, we can easily get the so-called completeness relation:\n\n$\\{\\ket{\\psi_1},\\ket{\\psi_2},...,\\ket{\\psi_n}\\}$ is a complete basis \n\nif and only if\n\n$$\nI=\\sum_{i=1}^n\\ket{\\psi_i}\\bra{\\psi_i}\n$$\n\nWhere 𝐼 is an identity matrix.\n\n## Bloch Sphere\n\nThe coefficients of a normalized quantum states must satisfy\n\n$$\n\\ket{\\psi}=\\alpha\\ket{0}+\\beta\\ket{1},\\text{with }|\\alpha|^2+|\\beta|^2=1\n$$\n\nA normalized quantum state must be on a “sphere” (Bloch sphere).\nThis sphere is called Bloch sphere.\n\nUsing polar angle $\\theta$, and azimuthal angle $\\phi$, we can write\n\n$$\n\\ket{\\psi}=\n \\left[\\begin{array}{c}\n \\cos\\frac{\\theta}{2} \\\\\n e^{i\\theta}\\sin\\frac{\\theta}{2}\n \\end{array}\\right]\n$$\n\nBesides, in Cartesian coordinate system, we can write\n\n$$\n\\begin{cases}\n x = \\sin\\theta\\cos\\phi \\\\\n y = \\sin\\theta\\sin\\phi \\\\\n z = \\cos\\theta\n\\end{cases}\n$$\n\n\n\n\n\n## Global and relative phase\n\nIn computational basis, we can express a quantum state as\n\n$$\n\\ket{\\psi}=e^{i\\gamma}(\\cos\\frac{\\theta}{2}\\ket{0}+e^{i\\phi}\\sin\\frac{\\theta}{2}\\ket{1})\n$$\n\nWhere $e^{i\\gamma}$ is called global phase, $e^{i\\phi}$ is called relative phase.\n\n* Global phase DOES NOT affect anything (we can ignore it).\n* Relative phase IS important, DO NOT forget it.\n\nExample:\n\n\n\n## Unitary operators\n\nAn operator $U$ is unitary if and only if $U^\\dagger U=UU^\\dagger=I$.\n\nUnitary operator PRESERVES the norm:\n\n$$\n||U\\ket{\\psi}||^2=\\bra{\\psi}U^\\dagger U\\ket{\\psi}=\\bra{\\psi}\\ket{\\psi}=||\\ket{\\psi}||^2\n$$\n\nIn other words, unitary operators keep quantum states on the Bloch sphere.\n\nExamples: \n\nPauli matrices:\n\n$$\n\\sigma_x=X=\n\\left[\\begin{array}{c c}\n 0 & 1 \\\\\n 1 & 0\n\\end{array}\\right],\n\\sigma_y=Y=\n\\left[\\begin{array}{c c}\n 0 & -i \\\\\n i & 0\n\\end{array}\\right],\n\\sigma_z=Z=\n\\left[\\begin{array}{c c}\n 1 & 0 \\\\\n 0 & -1\n\\end{array}\\right],\n$$\n\n## Rotation about arbitrary axis\n\nPauli-Euler relation:\n\n$$\n\\exp(i\\alpha\\hat{n}\\cdot\\vec{\\sigma})=\\cos(\\alpha)I+i\\sin(\\alpha)\\hat{n}\\cdot\\vec{\\sigma}\n$$\n\nwhere $\\hat{n}$ is a unit vector in $\\mathbb{R}^3$.\n\n$\\vec{\\sigma}=X\\hat{x}+Y\\hat{y}+Z\\hat{z}$ is a vector of Pauli matrices.\n\nFor example: \n\n$$\n\\hat{x}\\cdot\\vec{\\sigma}=X=\n\\left[\\begin{array}{c c}\n 0 & 1 \\\\\n 1 & 0\n\\end{array}\\right]\n$$\n\nTo rotate a state $\\ket{\\psi}$ along unit vector $\\hat{n}$ with angle $\\alpha$, multiply the rotation matrix $R_{\\hat{n}}(\\alpha)$,\n\n$$\nR_{\\hat{n}}(\\alpha)\\ket{\\psi}=\\exp(-i\\frac{\\alpha}{2}\\hat{n}\\cdot\\vec{\\sigma})\\ket{\\psi}\n$$\n\n\n\n## Pure state and mixed state\n\nA pure state is a quantum state can be represented as a single vector.\n\nThat means, all vectors on the surface Bloch sphere are pure states! Since they can be written as\n\n$$\n\\ket{\\psi}=\n \\left[\\begin{array}{c}\n \\cos\\frac{\\theta}{2} \\\\\n e^{i\\theta}\\sin\\frac{\\theta}{2}\n \\end{array}\\right]\n$$\n\nA mixed state is a vector inside Bloch sphere.\n\n## Density matrix and Bloch vector\n\nFor a given pure state $\\ket{\\psi}$, define the density operator as\n\n$$\n\\rho=\\ket{\\psi}\\bra{\\psi}\n$$\n\nLet $\\ket{\\psi}=[\\cos\\frac{\\theta}{2},e^{i\\theta}\\sin\\frac{\\theta}{2}]^T$, then we have\n\n$$\n\\rho=\\frac{1}{2}\n\\left[\\begin{array}{c c}\n 1+\\cos\\theta & e^{-i\\phi}\\sin\\theta \\\\\n e^{i\\phi}\\sin\\theta & 1-\\cos\\theta\n\\end{array}\\right]\n=\\frac{1}{2}(I+xX+yY+zZ)=\\frac{1}{2}(I+\\vec{r}_\\rho\\cdot\\vec{\\sigma})\n$$\n\nwhere $\\vec{\\sigma}=(X,Y,Z)$ is the vector of Pauli matrices and\n\n$$\n\\vec{r}_\\rho=(x,y,z)=(\\sin\\theta\\cos\\phi,\\sin\\theta\\sin\\phi,\\cos\\theta)\n$$\n\nis called the unit Bloch vector. It is the Cartesian representation of $\\ket{\\psi}$.\n\n## Purity\n\nDefine purity of a quantum state $\\ket{\\psi}$ as\n\n$$\n\\gamma=Tr(\\rho^2)\n$$\n\nwhere $\\rho=\\ket{\\psi}\\bra{\\psi}$ is the density operator of $\\ket{\\psi}$.\n* Purity measures how much a state is mixed.\n* $1/d\\leq\\gamma\\leq1$ where $d$ is the dimension of the Hilbert space.\n* $\\gamma=1$ if it’s a pure state, $\\gamma=1/d$ if it’s a completely mixed state.\n\nSuppose $\\vec{r}_1$ and $\\vec{r}_2$ are the Bloch vectors of two pure states $\\ket{\\psi_1}$ and $\\ket{\\psi_2}$, then\n\n$$\n|\\braket{\\psi_1|\\psi_2}|^2=Tr(\\rho_1\\rho_2)=\\frac{1}{2}(1+\\vec{r}_1\\cdot\\vec{r}_2)\n$$\n\nThis formula relates the inner product in $H$ and the inner product in $\\mathbb{R}^3$.\n\nExample:\n\n* Bloch sphere geometry \n\nSuppose $\\braket{\\psi_1|\\psi_2}=0$ (orthogonal in $H$), how do they look like on Bloch sphere?\n\nUsing the formula,\n\n$$\n|\\braket{\\psi_1|\\psi_2}|^2=\\frac{1}{2}(1+\\vec{r}_1\\cdot\\vec{r}_2)\n$$\n\nSince $\\braket{\\psi_1|\\psi_2}=0$, so we have $\\vec{r}_1\\cdot\\vec{r}_2=-1$. $\\ket{\\psi_1}$ and $\\ket{\\psi_2}$ have opposite directions!\n\n* Purity\n\nLet $\\ket{\\psi}=\\frac{\\ket{0}-\\ket{1}}{\\sqrt{2}}$, calculate its purity.\n\n$$\n\\rho_{\\psi}=\\frac{1}{2}(\\ket{0}\\bra{0}-\\ket{0}\\bra{1}-\\ket{1}\\bra{0}+\\ket{1}\\bra{1})=\\frac{1}{2}\\left[\\begin{array}{c c}\n 1 & -1 \\\\\n -1 & 1\n\\end{array}\\right] \\\\\n\\implies\n\\rho_{\\psi}^2=\\frac{1}{4}\\left[\\begin{array}{c c}\n 2 & -2 \\\\\n -2 & 2\n\\end{array}\\right]\n\\implies\nTr(\\rho_{\\psi}^2)=1\n$$\n\nThis is obvious, since $\\ket{\\psi}$ can be drawn on Bloch sphere, so it is a pure state.\n\n\n\n\n\n## Reference\n\n[大一新生也能懂的量子计算](https://www.bilibili.com/video/BV1oq4y1j7iZ/?spm_id_from=333.337.search-card.all.click&vd_source=201635f84ae101b427649f98a07303b1)\n\n[CMPT 409/981: Quantum Circuits and Compilation](https://www.cs.sfu.ca/~meamy/Teaching/f22/cmpt981/)","tags":["量子计算"],"categories":["physic"]},{"title":"NDC空间","url":"/archives/49371/","content":"\n# 相机模型\n\n\n\n\n\n\n\n\n\n\n\n\n# 参考资料\n\n- [详解NeRF中的NDC ray space](https://zhuanlan.zhihu.com/p/628675070)\n- [OpenGL Projection Matrix](http://www.songho.ca/opengl/gl_projectionmatrix.html)\n- [投影与相机空间](https://sighingnow.github.io/graphics/projection_and_camera.html)\n- [如何从NDC坐标转换为摄像机空间坐标](https://zhuanlan.zhihu.com/p/356771429)\n- [NeRF神经辐射场中关于光线从世界坐标系转换为NDC坐标系](https://blog.csdn.net/qq_40514113/article/details/131746384)\n- [Generating Camera Rays with Ray-Tracing](https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-generating-camera-rays/standard-coordinate-systems.html)\n- [nerf 视图与坐标系](https://zhuanlan.zhihu.com/p/651524053)","tags":["CV&CG"],"categories":["computer"]},{"title":"相机参数与坐标系变换","url":"/archives/27886/","content":"\n# 坐标系\n\n\n\n为了唯一地描述每一个空间点的坐标,以及相机的位置和朝向,我们需要先定义一个世界坐标系。一个坐标系其实就是由原点的位置与 XYZ 轴的方向决定。为了建立三维空间点到相机平面的映射关系以及多个相机之间的相对关系,也要对每一个相机定义一个局部的相机坐标系。下图为常见的坐标系定义习惯:\n\n\n\n> 在 OpenCV/COLMAP 的相机坐标系里相机朝向 +z 轴,在 LLFF/NeRF 的相机坐标系中里相机朝向 -z 轴。有时我们会按坐标系的 xyz 朝向描述坐标系,如 OpenCV/COLMAP 里使用的 RDF 表述 X 轴指向 right,Y 轴指向 Down,Z 轴指向 Foward。\n\n# 相机内外参数\n\n相机的**位置**和**朝向**由相机的**外参(extrinsic matrix)**决定,**投影属性**由相机的**内参(intrinsic matrix)**决定。\n\n> 接下来的介绍假设矩阵是列矩阵(column-major matrix),变换矩阵左乘坐标向量实现坐标变换(这也是 OpenCV/OpenGL/NeRF 里使用的形式)。\n\n## 相机外参\n\n相机外参是一个 $4\\times4$ 的矩阵 $M$,其作用是将世界坐标系的点 $P_{world}=[x,y,z,1]$ 变换到相机坐标系 $P_{camera}=MP_{world}$ 下。因此也把相机外参叫做 **world-to-camera (w2c) 矩阵**。(注意用的是4维的齐次坐标)\n\n相机外参的逆矩阵被称为 **camera-to-world (c2w) 矩阵**,其作用是把相机坐标系的点变换到世界坐标系。c2w 矩阵是一个 $4\\times4$ 的矩阵,左上角 $3\\times3$ 是旋转矩阵 $R$,右上角的 $3\\times1$ 向量是平移向量 $T$。有时写的时候可以忽略最后一行 $[0,0,0,1]$。\n\n\n\nc2w 矩阵的值直接描述了相机坐标系的朝向和原点:**旋转矩阵的第一列到第三列分别表示了相机坐标系的 X, Y, Z 轴在世界坐标系下对应的方向;平移向量表示的是相机原点在世界坐标系的对应位置**。\n\n\n\n如果将 c2w 作用到(即左乘)相机坐标系下的 X 轴 $[1,0,0,0]$,Y 轴 $[0,1,0,0]$, Z 轴 $[0,0,1,0]$,以及原点 $[0,0,0,1]$(注意方向向量的齐次坐标第四维等于 0,点坐标第四维等于 1),就能得到它们在世界坐标系的坐标表示:\n\n```python\n[R, T][1, 0, 0, 0]^T = [r11, r21, r31]^T # X轴对应的是c2w矩阵的第一列\n[R, T][0, 1, 0, 0]^T = [r12, r22, r32]^T # Y轴对应的是c2w矩阵的第二列\n[R, T][0, 0, 1, 0]^T = [r13, r23, r33]^T # Y轴对应的是c2w矩阵的第三列\n[R, T][0, 0, 0, 1]^T = [t1, t2, t3]^T # 原点对应的是c2w矩阵的第四列\n```\n\n## 相机内参\n\n相机的内参矩阵将相机坐标系下的三维坐标映射到二维的图像平面。\n\n\n\n这里以针孔相机(Pinhole camera)为例介绍相机的内参矩阵 $K$:\n\n$$\nK=\\left[\\begin{array}{c c c}\n f_x & 0 & c_x \\\\\n 0 & f_y & c_y \\\\\n 0 & 0 & 1\n\\end{array}\\right]\n$$\n\n内参矩阵 $K$ 包含 $4$ 个值,其中 $f_x$ 和 $f_y$ 是相机的水平和垂直**焦距(focal length)**(对于理想的针孔相机,$f_x=f_y$)。焦距的物理含义是相机中心到成像平面的距离,长度以像素为单位。$c_x$ 和 $c_y$ 是图像原点相对于相机光心的水平和垂直偏移量。$c_x$,$c_y$ 有时候可以用图像宽和高的 $1/2$ 近似:\n\n```python\n# NeRF run_nerf.py有这么一段构造K的代码\n if K is None:\n K = np.array([\n [focal, 0, 0.5*W],\n [0, focal, 0.5*H],\n [0, 0, 1]\n ])\n```\n\n# 如何获得相机参数\n\nNeRF 算法假设相机的内外参数是提供的,那么怎么得到所需要的相机参数呢?这里分合成数据集和真实数据集两种情况。\n\n## 合成数据\n\n对于合成数据集,我们需要通过指定相机参数来渲染图像,所以得到图像的时候已经知道对应的相机参数,比如像 NeRF 用到的 Blender Lego 数据集。常用的渲染软件还有 Mitsuba、OpenGL、PyTorch3D、Pyrender 等。渲染数据比较简单,但是把得到的相机数据转到 NeRF 代码坐标系牵扯到坐标系之间的变换,有时候会比较麻烦。\n\n## 真实数据\n\n对于真实场景,比如我们用手机拍摄了一组图像,怎么获得相机位姿?目前常用的方法是利用**运动恢复结构(structure-from-motion, SFM)**技术估计几个相机间的相对位姿。这个技术比较成熟了,现在学术界里用的比较多的开源软件包是 [COLMAP](https://colmap.github.io/)。输入多张图像,COLMAP 可以估计出相机的内参和外参(也就是 sparse model)。\n\n\n\n下面是 COLMAP 官网教程给的三个命令行操作步骤,简单来说:\n\n1. 第一步是对所有的图像进行特征点检测与提取;\n2. 第二步是进行特征点匹配;\n3. 第三步是进行 SFM 恢复相机位姿和稀疏的三维特征点。\n\n具体的使用方法和原理还请阅读其官方文档。其实 COLMAP 也集成了 multiview stereo (MVS) 算法用于重建场景完整的三维结构(也称为 dense model)。不过 NeRF 本身是一种新颖的场景表征和重建算法,我们只需要相机的位姿信息,所以我们不需要跑 MVS 进行 dense 重建。\n\n> 如果没有标定信息,基于单目的 SFM 无法获得场景的绝对尺度。\n\n```bash\n# The project folder must contain a folder \"images\" with all the images.\n$ DATASET_PATH=/path/to/dataset\n\n$ colmap feature_extractor \\\n --database_path $DATASET_PATH/database.db \\\n --image_path $DATASET_PATH/images\n\n$ colmap exhaustive_matcher \\\n --database_path $DATASET_PATH/database.db\n\n$ mkdir $DATASET_PATH/sparse\n\n$ colmap mapper \\\n --database_path $DATASET_PATH/database.db \\\n --image_path $DATASET_PATH/images \\\n --output_path $DATASET_PATH/sparse\n```\n\n使用 COLMAP 得到相机参数后只需要转成 NeRF 可以读取的格式即可以用于模型训练了。那这里面需要做什么操作?\n\n# LLFF真实数据格式\n\nLLFF(Local Light Field Fusion),是 NeRF 作者上一篇做新视角合成的工作 [Code release for Local Light Field Fusion at SIGGRAPH 2019](https://github.com/fyusion/llff)。为了和 LLFF 方法保持一致的数据格式,NeRF 使用 load_llff.py 读取 LLFF 格式的真实数据,并建议大家使用 LLFF 提供的的 [imgs2poses.py](https://github.com/Fyusion/LLFF/blob/master/imgs2poses.py) 文件获取所需相机参数。\n\n## COLMAP到LLFF数据格式\n\nimgs2poses.py 这个文件其实很简单,就干了两件事。\n\n1. 调用 COLMAP 软件估计相机的参数,在 sparse/0/ 文件夹下生成一些二进制文件:cameras.bin, images.bin, points3D.bin, project.ini;\n2. 读取上一步得到的二进制文件,保存成一个 poses_bounds.npy 文件;\n\n这里有一个细节需要注意,就是在 pose_utils.py 文件里 load_colmap_data() 函数的倒数第二行,有一个操作将 COLMAP 得到的 c2w 旋转矩阵中的第一列和第二列互换,第三列乘以负号:\n\n```python\n# LLFF/llff/poses/pose_utils.py\ndef load_colmap_data(realdir):\n ... \n # must switch to [-u, r, -t] from [r, -u, t], NOT [r, u, -t]\n poses = np.concatenate([poses[:, 1:2, :], poses[:, 0:1, :], -poses[:, 2:3, :], poses[:, 3:4, :], poses[:, 4:5, :]], 1)\n return poses, pts3d, perm\n```\n\n上述操作实际上就是把相机坐标系轴的朝向进行了变换:X 和 Y 轴调换,Z 轴取反,如下图所示:\n\n\n\n## poses_bounds.npy里有什么\n\nload_llff.py 会直接读取 poses_bounds.npy 文件获得相机参数。poses_bounds.npy 是一个 $N\\times17$ 的矩阵,其中 $N$ 是图像的数量,即每一张图像有 $17$ 个参数。其中前面 $15$ 个参数可以重排成 $3\\times5$ 的矩阵形式:\n\n$$\n\\left[\\begin{array}{c c c c c}\n r_{11} & r_{12} & r_{13} & t_1 & H \\\\\n r_{21} & r_{22} & r_{23} & t_2 & W \\\\\n r_{31} & r_{32} & r_{33} & t_3 & f \\\\\n\\end{array}\\right]\n$$\n\n左边 $3\\times3$ 矩阵是 c2w 的旋转矩阵,第四列是 c2w 的平移向量,第五列分别是图像的高 $H$、宽 $W$ 和相机的焦距 $f$。\n\n最后两个参数用于表示**场景的范围 Bounds (bds)**,是该相机视角下场景点离相机中心最近(near)和最远(far)的距离,所以 near/far 肯定是大于 0 的。\n\n- 这两个值是怎么得到的?是在 imgs2poses.py 中,计算 colmap 重建的三维稀疏点在各个相机视角下最近和最远的距离得到的。\n- 这两个值有什么用?之前提到体素渲染需要在一条射线上采样三维点,这就需要一个采样区间,而 near 和 far 就是定义了采样区间的最近点和最远点。贴近场景边界的 near/far 可以使采样点分布更加密集,从而有效地提升收敛速度和渲染质量。\n\n\n\n# load_llff.py代码解读\n\n接着,我们介绍 NeRF 代码里 load_llff.py 代码里的一些细节。对三维视觉不熟悉的读者,早期读代码的时候可能会有不少困惑。\n\n## DRB到RUB的变换\n\n第一个疑问是,为什么读进 poses_bounds.npy 里的c2w矩阵之后,对c2w的旋转矩阵又做了一些列变换?\n\n```python\n# load_llff.py文件\ndef load_llff_data(basedir, factor=8, recenter=True, bd_factor=.75, spherify=False, path_zflat=False):\n \n poses, bds, imgs = _load_data(basedir, factor=factor) # factor=8 downsamples original imgs by 8x\n print('Loaded', basedir, bds.min(), bds.max())\n \n # Correct rotation matrix ordering and move variable dim to axis 0\n poses = np.concatenate([poses[:, 1:2, :], -poses[:, 0:1, :], poses[:, 2:, :]], 1)\n ...\n```\n\n上面的代码段的最后一行实际上是把旋转矩阵的第一列(X轴)和第二列(Y轴)互换,并且对第二列(Y轴)做了一个反向。这样做的目的是将 LLFF 的相机坐标系变成 OpenGL/NeRF 的相机坐标系,如下图所示:\n\n![poses = np.concatenate([poses[:, 1:2, :], -poses[:, 0:1, :], poses[:, 2:, :]], 1)](相机参数与坐标系变换/DRB_to_RUB.png)\n\n## 缩放图像需要修改什么相机参数?\n\n在 _load_data() 函数里,有一个用于图像缩放的 factor 比例参数,将 $H\\times W$ 的图像缩放成 $(H/factor)\\times(W/factor)$。这里面有一个问题是如果缩放了图像尺寸,相机的参数需要相应的做什么变化?\n\n**外参(位置和朝向)不变,相机的焦距 $f$,$c_x$, 和 $c_y$ 等比例缩放**。下图的示意图展示了当相机位置不变,相机视野(Field of view, FOV)不变的情况下,图像的高和焦距长短的关系。\n\n\n\n## viewmatrix()\n\nview_matrix 是一个构造相机矩阵的的函数,输入是相机的 **Z 轴朝向、up 轴的朝向**(即相机平面朝上的方向 Y)、以及相机中心。输出下图所示的camera-to-world(c2w)矩阵。因为 Z 轴朝向,Y 轴朝向,和相机中心都已经给定,所以只需求 X 轴的方向即可。又由于 X 轴同时与 Z 轴和 Y 轴垂直,我们可以用 Y 轴与 Z 轴的叉乘得到 X 轴方向。\n\n\n\n下面是 load_llff.py 里关于 view_matrix() 的定义,看起来复杂一些。其实就是比刚刚的描述比多了一步:在用 Y 轴与 Z 轴叉乘得到 X 轴后,再次用 Z 轴与 X 轴叉乘得到新的 Y 轴。为什么这么做呢?这是因为传入的 up(Y) 轴是通过一些计算得到的,不一定和 Z 轴垂直,所以多这么一步。\n\n```python\n# load_llff.py\ndef viewmatrix(z, up, pos):\n vec2 = normalize(z)\n vec1_avg = up\n vec0 = normalize(np.cross(vec1_avg, vec2))\n vec1 = normalize(np.cross(vec2, vec0))\n m = np.stack([vec0, vec1, vec2, pos], 1)\n return m\n```\n\n## poses_avg()\n\n这个函数顾名思义就是多个相机的平均位姿(包括位置和朝向)。输入是多个相机的位姿。\n\n1. 对多个相机的中心进行求均值得到 center;\n2. 对所有相机的 Z 轴求平均得到 vec2 向量(方向向量相加其实等效于平均方向向量);\n3. 对所有的相机的 Y 轴求平均得到 up 向量;\n4. 将 vec2, up, 和 center 输入到刚刚介绍的 viewmatrix() 函数就可以得到平均的相机位姿了。\n\n```python\ndef poses_avg(poses):\n\n hwf = poses[0, :3, -1:]\n\n center = poses[:, :3, 3].mean(0)\n vec2 = normalize(poses[:, :3, 2].sum(0))\n up = poses[:, :3, 1].sum(0)\n c2w = np.concatenate([viewmatrix(vec2, up, center), hwf], 1)\n \n return c2w\n```\n\n下图展示了一个 poses_avg() 函数的例子。左边是多个输入相机的位姿,右边是返回的平均相机姿态。可以看出平均相机位姿的位置和朝向是之前所有相机的均值。\n\n\n\n> 中间大的坐标系是世界坐标系,每一个小的坐标系对应一个相机的局部坐标系。红绿蓝(RGB)轴分别代表 XYZ 轴。\n\n## recenter_poses()\n\nrecenter_poses() 函数的名字听起来是中心化相机位姿(同样包括位置和朝向)的意思。输入 $N$ 个相机位姿,会返回N个相机位姿。\n\n具体的操作了解起来可能有点跳跃。第一步先用刚刚介绍的 poses_avg(poses) 得到多个输入相机的平均位姿 c2w,接着用这个平均位姿 c2w 的逆左乘到输入的相机位姿上就完成了归一化。\n\n```python\ndef recenter_poses(poses):\n\n poses_ = poses+0\n bottom = np.reshape([0,0,0,1.], [1,4])\n c2w = poses_avg(poses)\n c2w = np.concatenate([c2w[:3,:4], bottom], -2)\n bottom = np.tile(np.reshape(bottom, [1,1,4]), [poses.shape[0],1,1])\n poses = np.concatenate([poses[:,:3,:4], bottom], -2)\n\n poses = np.linalg.inv(c2w) @ poses\n poses_[:,:3,:4] = poses[:,:3,:4]\n poses = poses_\n return poses\n```\n\n首先我们要知道利用同一个旋转平移变换矩阵左乘所有的相机位姿是对所有的相机位姿做一个全局的旋转平移变换,那下一个问题就是这些相机会被变到什么样的一个位置?我们可以用平均相机位姿作为支点理解,如果把平均位姿的逆 $\\text{c2w}^{-1}$ 左乘平均相机位姿 c2w,返回的相机位姿中旋转矩阵为单位矩阵,平移量为零向量。也就是变换后的平均相机位姿的位置处在世界坐标系的原点,XYZ 轴朝向和世界坐标系的向一致。\n\n下图我们用一个例子帮助理解。左边和右边分别是输入和输出的相机位姿示意图。我们可以看到变换后的多个相机的平均位姿处在世界坐标系的原点,并且相机坐标系的 XYZ 轴与世界坐标系保持一致了。\n\n\n\n> 中间大的坐标系是世界坐标系,每一个小的坐标系对应一个相机的局部坐标系。红绿蓝(RGB)轴分别代表 XYZ 轴。\n\n## render_path_spiral()\n\n这个函数写的有点复杂,它和模型训练没有关系,主要是用来生成一个相机轨迹用于新视角的合成。\n\n下面只放了render_path_spiral()函数的定义,NeRF代码里还有一段是在准备输入参数,由于相关代码比较长就不贴出来。\n\n```python\ndef render_path_spiral(c2w, up, rads, focal, zdelta, zrate, rots, N):\n render_poses = []\n rads = np.array(list(rads) + [1.])\n hwf = c2w[:,4:5]\n \n for theta in np.linspace(0., 2. * np.pi * rots, N+1)[:-1]:\n c = np.dot(c2w[:3,:4], np.array([np.cos(theta), -np.sin(theta), -np.sin(theta*zrate), 1.]) * rads) \n z = normalize(c - np.dot(c2w[:3,:4], np.array([0,0,-focal, 1.])))\n render_poses.append(np.concatenate([viewmatrix(z, up, c), hwf], 1))\n return render_poses\n```\n\n需要知道这个函数它是想生成一段螺旋式的相机轨迹,相机绕着一个轴旋转,其中相机始终注视着一个焦点,相机的 up 轴保持不变。简单说一下上面的代码:\n\n首先是一个 for 循环,每一迭代生成一个新的相机位置。`c` 是当前迭代的相机在世界坐标系的位置,`np.dot(c2w[:3,:4], np.array([0,0,-focal, 1.])` 是焦点在世界坐标系的位置,`z` 是相机 z 轴在世界坐标系的朝向。接着使用介绍的 viewmatrix(z, up, c) 构造当前相机的矩阵。\n\n下面这个图可视化了 render_path_spiral() 生成的轨迹:\n\n\n\n> 中间大的坐标系是世界坐标系,每一个小的坐标系对应一个相机的局部坐标系。红绿蓝(RGB)轴分别代表 XYZ 轴。\n\n## spherify_poses()\n\n刚刚介绍的 render_path_spiral() 假设所有相机都朝向某一个方向,也就是所谓的 faceforward 场景。对于相机围绕着一个物体拍摄的 360 度场景,NeRF 代码提供了一个 spherify_poses() 的函数用于\"球面化\"相机分布并返回一个环绕的相机轨迹用于新视角合成。这里插一句,在训练 360 度场景的时候,需要配合 `--no_ndc --spherify --lindisp` 三个参数以得到好的结果,具体原理这里不展开介绍。\n\n```python\nif spherify:\n poses, render_poses, bds = spherify_poses(poses, bds)\n```\n\n这个函数也比较复杂,前半部分是在将输入的相机参数进行归一化,后半部分是生成一段相机轨迹用于合成新视角。对输入相机参数进行归一化时,思路是:\n\n- 用 `pt_mindist = min_line_dist(rays_o, rays_d)` 找到离所有相机中心射线距离之和最短的点(可以先简单理解成场景的中心位置)\n\n```python\n rays_d = poses[:,:3,2:3]\n rays_o = poses[:,:3,3:4]\n\n def min_line_dist(rays_o, rays_d):\n A_i = np.eye(3) - rays_d * np.transpose(rays_d, [0,2,1])\n b_i = -A_i @ rays_o\n pt_mindist = np.squeeze(-np.linalg.inv((np.transpose(A_i, [0,2,1]) @ A_i).mean(0)) @ (b_i).mean(0))\n return pt_mindist\n\n pt_mindist = min_line_dist(rays_o, rays_d)\n```\n\n- 将得到的场景中心位置移到世界坐标系的原点,同时将所有相机 z 轴的平均方向转到和世界坐标系的 z 轴相同\n\n```python\n center = pt_mindist\n up = (poses[:,:3,3] - center).mean(0)\n\n vec0 = normalize(up)\n vec1 = normalize(np.cross([.1,.2,.3], vec0))\n vec2 = normalize(np.cross(vec0, vec1))\n pos = center\n c2w = np.stack([vec1, vec2, vec0, pos], 1)\n\n poses_reset = np.linalg.inv(p34_to_44(c2w[None])) @ p34_to_44(poses[:,:3,:4])\n```\n\n- 最后将相机的位置缩放到单位圆内\n\n```python\n rad = np.sqrt(np.mean(np.sum(np.square(poses_reset[:,:3,3]), -1)))\n sc = 1./rad\n poses_reset[:,:3,3] *= sc\n```\n\n下面这个图可视化了 spherify_poses() 返回的结果:\n\n\n\n> 中间大的坐标系是世界坐标系,每一个小的坐标系对应一个相机的局部坐标系。红绿蓝(RGB)轴分别代表 XYZ 轴。\n\n# 3D空间射线怎么构造\n\n最后我们看一下这个射线是怎么构造的。**给定一张图像的一个像素点,我们的目标是构造以相机中心为起始点,经过相机中心和像素点的射线。**\n\n首先,明确两件事:\n\n1. 一条射线包括一个起始点和一个方向,起点的话就是相机中心。对于射线方向,我们都知道两点确定一条直线,所以除了相机中心我们还需另一个点,而这个点就是成像平面的像素点。\n2. NeRF 代码是在相机坐标系下构建射线,然后再通过 camera-to-world (c2w) 矩阵将射线变换到世界坐标系。\n\n通过上述的讨论,我们第一步是要先写出相机中心和像素点在相机坐标系的三维坐标。下面我们以 OpenCV/Colmap 的相机坐标系为例介绍。相机中心的坐标很明显就是 $[0,0,0]$ 了。像素点的坐标可能复杂一点:首先三维像素点的 x 和 y 坐标是二维的图像坐标 $(i, j)$ 减去光心坐标 $(c_x,c_y)$,然后 z 坐标其实就是焦距 $f$ (因为图像平面距离相机中心的距离就是焦距 $f$)。\n\n所以我们就可以得到射线的方向向量是 $(i-c_x,j-c_y,f)-(0,0,0)=(i-c_x,j-c_y,f)$。因为是向量,我们可以把整个向量除以焦距 $f$ 归一化 z 坐标,得到 $(\\frac{i-c_x}{f},\\frac{i-c_y}{f},1)$。\n\n接着只需要用 c2w 矩阵把相机坐标系下的相机中心和射线方向变换到世界坐标系就搞定了。\n\n\n\n下面是 NeRF 的实现代码。但关于这里面有一个细节需要注意一下:为什么函数的第二行中 dirs 的 y 和 z 的方向值需要乘以负号,和我们刚刚推导的的 $(\\frac{i-c_x}{f},\\frac{i-c_y}{f},1)$ 不太一样呢?\n\n```python\ndef get_rays_np(H, W, K, c2w):\n i, j = np.meshgrid(np.arange(W, dtype=np.float32), np.arange(H, dtype=np.float32), indexing='xy')\n dirs = np.stack([(i-K[0][2])/K[0][0], -(j-K[1][2])/K[1][1], -np.ones_like(i)], -1)\n # Rotate ray directions from camera frame to the world frame\n rays_d = np.sum(dirs[..., np.newaxis, :] * c2w[:3,:3], -1) # dot product, equals to: [c2w.dot(dir) for dir in dirs]\n # Translate camera frame's origin to the world frame. It is the origin of all rays.\n rays_o = np.broadcast_to(c2w[:3,-1], np.shape(rays_d))\n return rays_o, rays_d\n```\n\n这是因为 OpenCV/Colmap 的相机坐标系里相机的 Up/Y 朝下, 相机光心朝向 +Z 轴,而 NeRF/OpenGL 相机坐标系里相机的 Up/Y 朝上,相机光心朝向 -Z 轴,所以这里代码在方向向量 dir 的第二和第三项乘了个负号。\n\n\n\n# 参考资料\n\n- [NeRF代码解读-相机参数与坐标系变换](https://zhuanlan.zhihu.com/p/593204605/)\n- [The Perspective Camera - An Interactive Tour](https://ksimek.github.io/2012/08/13/introduction/)","tags":["CV"],"categories":["computer"]},{"title":"常微分方程","url":"/archives/26715/","content":"\n\n所谓常微分方程,指的是方程中包括函数 $y(x)$ 以及导数 $y'(x)$,并且只包含一个变量 $x$。如果只包含一阶导,则称为一阶常微分方程。\n\n## 线性\n\n这是最基础的一类微分方程,方程 $F(x,y',...,y^{(n)})$ 对于 $y$ 和其各阶导数而言是线性的。这类方程中,$x, y$ 每一项的次数是相等的。\n\n### 齐次\n\n齐次方程指的是可化为 $\\frac{dy}{dx}=f(\\frac{y}{x})$ 形式的一阶微分方程。\n\n#### 系数为常数\n\n这是最简单的一类常微分方程:$\\frac{dy}{dx}+\\alpha y(x)=0$\n\n我们只需要将常数分离再两边积分即可,求解过程如下:\n\n$$\n\\begin{align*}\n &\\frac{dy}{y}=-\\alpha dx \\\\\n \\implies&\\int\\frac{1}{y}dy=-\\int\\alpha dx \\\\\n \\implies&\\ln y+C'=-\\alpha x \\\\\n \\implies&y(x)=e^{-C'}e^{-\\alpha x}=Ce^{-\\alpha x}\n\\end{align*}\n$$\n\n#### 系数为函数\n\n接下来升级一下,把常数替换为函数:$\\frac{dy}{dx}+p(x)y(x)=0$\n\n同样也是先将与函数 $y$ 有关的部分与其他部分分离,求解过程如下:\n\n$$\n\\begin{align*}\n &\\frac{dy}{y}=-pdx \\\\\n \\implies&\\int\\frac{1}{y}dy=-\\int p dx \\\\\n \\implies&\\ln y+C'=-\\int p dx \\\\\n \\implies&y(x)=e^{-C'}e^{-\\int p(x)dx}=Ce^{-\\int p(x)dx}\n\\end{align*}\n$$\n\n> 对于可分离变量的微分方程:\n> $$\n> ydy=f(x)dx\n> $$\n> 通常为分离变量后取积分。\n> 对于齐次微分方程:\n> $$\n> \\frac{dy}{dx}=f(\\frac{y}{x})\n> $$\n> 令 $u=\\frac{y}{x}$,则 $y=ux,\\frac{dy}{dx}=x\\frac{du}{dx}+u$,回代后可得:\n> $$\n> x\\frac{du}{dx}+u=f(u) \\implies \\frac{du}{f(u)-u}=\\frac{dx}{x}\n> $$\n> 即可使用可分离变量的微分方程的方法求解。\n\n### 非齐次\n\n接下来再进一步泛化:$\\frac{dy}{dx}+p(x)y(x)=q(x)$\n\n#### 常数变易法\n\n由拉格朗日提出。与前一类类似操作得到\n\n$$\n\\begin{align*}\n&\\frac{dy}{dx}+p(x)y(x)=q(x) \\\\\n\\implies &\\frac{dy}{y}=-p(x)dx+\\frac{q(x)}{y}dx \\\\\n\\implies &y=e^{-\\int p(x)dx}e^{\\int\\frac{q(x)}{y}dx}\n\\end{align*}\n$$\n设未知的项 $e^{\\int\\frac{q(x)}{y}dx}=C(x)$,则有 $y=C(x)e^{-\\int p(x)dx}$,从而得到\n$$\n\\begin{align*}\n&y'=C'(x)e^{-\\int p(x)dx}-p(x)C(x)e^{-\\int p(x)dx}=C'(x)e^{-\\int p(x)dx}-p(x)y \\\\\n\\implies &y'+p(x)y=C'(x)e^{-\\int p(x)dx}=q(x) \\\\\n\\implies &C(x)=C+\\int q(x) e^{\\int p(x)dx} dx\n\\end{align*}\n$$\n\n因此最终方程的通解为 $y=e^{-\\int p(x)dx}(\\int q(x) e^{\\int p(x)dx} dx + C)$\n\n#### 积分因子法\n\n由欧拉和克莱罗分别独立提出。主要思想是利用乘法求导的逆推:$f'(x)g(x)+f(x)g'(x)=(f(x)g(x))'$.\n\n观察微分方程,如果两边同乘上 $H(x)$,则有\n\n$$\ny'H(x)+p(x)H(x)y=q(x)H(x)\n$$\n\n倘若我们假设 $H'(x)=p(x)H(x)$,则方程可进一步化为\n\n$$\n(yH(x))'=q(x)H(x)\n$$\n\n两边积分可得到\n\n$$\nyH(x)=\\int q(x)H(x)dx + C\n$$\n\n因此方程的解即为 $y=H^{-1}(x)(\\int q(x)H(x)dx + C)$.\n\n而根据 $H(x)$ 的建立假设,有\n\n$$\n\\begin{align*}\n H'(x)=p(x)H(x) \\implies \\frac{H'(x)}{H(x)}=p(x) \\implies H(x)=e^{\\int p(x)dx}\n\\end{align*}\n$$\n\n因此最终方程的通解为 $y=e^{-\\int p(x)dx}(\\int q(x)e^{\\int p(x)dx}dx + C)$.\n\n> 线性方程的解有如下的性质:\n> \n> 1. 一阶齐次线性微分方程的解要么恒为 $0$,要么恒不为 $0$(解为指数形式);\n> \n> 2. 线性方程的解的存在区间是整个区间;\n> \n> 3. 线性方程的初值问题的解存在且唯一;\n> \n> 4. 线性方程的解的叠加还是线性方程的解。\n\n## 伯努利方程\n\n对于方程:$y'(x)+p(x)y(x)=q(x)y^n(x)\\ (n>1)$,用 $y^n$ 除以两侧,有\n\n$$\ny'(x)y^{-n}(x)+p(x)y^{1-n}(x)=q(x)\n$$\n\n令 $z=y^{1-n}$,有 $z'=(1-n)y^{-n}y'$。所以我们有\n\n$$\nz'(x)+(1-n)p(x)z(x)=(1-n)q(x)\n$$\n\n此后可用线性非齐次方法求解。\n\n## 高阶微分方程\n\n高阶微分方程是指二阶及二阶以上的微分方程。\n\n### 可降阶的高阶微分方程\n\n#### $y^{(n)}=f(x)$ 型\n\n连续积分 $n$ 次,可得通解:\n$$\ny=\\int[\\int\\dots\\int[\\int [f(x)dx+C_1] dx+C_2]dx\\dots dx+C_n]dx\n$$\n\n#### $y''=f(x,y')$ 型\n\n令 $y'=p(x)$,则 $y''=p'(x)$,于是原方程可化为关于 $p, x$ 的一阶微分方程:\n\n$$\np'=f(x,p)\n$$\n\n在求解完 $p$ 后,回代求解 $y$ 即可(也是一个一阶微分方程)。\n\n#### $y''=f(y,y')$ 型\n\n令 $y'=p(y)$,则 $y''=p'(y)p(y)$,于是原方程可化为关于 $p, y$ 的一阶微分方程:\n\n$$\np\\frac{dp}{dy}=f(y,p)\n$$\n\n同样在求解完 $p$ 后,回代求解 $y$ 即可(也是一个一阶微分方程)。\n\n> 可降阶的高阶微分方程缺少了部分元素:\n> \n> * $y^{(n)}=f(x)$ 型缺少了 $y,y',\\dots,y^{(n-1)}$;\n> \n> * $y''=f(x,y')$ 型缺少了 $y$;\n> \n> * $y''=f(y,y')$ 型缺少了 $x$.\n\n### 常系数齐次线性微分方程\n形如\n\n$$\ny^{(n)}+a_1(x)y^{(n-1)}+...+a_{n-1}(x)y'+a_n(x)y=0\n$$\n\n且 $a_n(x)$ 均为常数。通常使用特征方程法求解。\n\n对于上面的微分方程,其对应的特征方程为\n\n$$\nr^n+a_1(x)r^{n-1}+...+a_{n-1}(x)r+a_n(x)=0\n$$\n\n求解出 $n$ 个根 $r_n$ 后,根据根的情形,可获得通解:\n\n|特征方程的根|通解中的对应项|\n|---|---|\n|单实根 $r$|给出一项:$Ce^{rx}$|\n|一对单复根 $r_{1,2}=\\alpha+i\\beta$|给出两项:$e^{\\alpha x}(C_1\\cos{\\beta}x+C_2\\sin{\\beta}x)$|\n|$k$ 重实根 $r$|给出 $k$ 项:$e^{rx}(C_1+C_2x+C_3x^2+...+C_kx^{k-1})$|\n|一对 $k$ 重复根 $r_{1,2}=\\alpha+i\\beta$|给出两项:$e^{\\alpha x}[(C_1+C_2x+C_3x^2+...+C_kx^{k-1})\\cos{\\beta x}+(D_1+D_2x+D_3x^2+...+D_kx^{k-1})\\sin{\\beta x}]$|\n\n注意,若有 $n$ 个根 $r_n$,则通解中必有 $n$ 个项。\n\n### 常系数非齐次线性微分方程\n\n\n## 欧拉方程\n形如\n\n$$\nx^ny^{(n)}+a_1x^{n-1}y^{(n-1)}+...+a_{n-1}xy'+a_ny=0\n$$\n\n且 $a_n$ 均为常数。\n\n令 $x=e^t,t=\\ln x$,把 $y$ 看作是关于 $t$ 的函数,有\n\n$$\n\\begin{align*}\n \\frac{dy}{dx}&=\\frac{1}{x}\\frac{dy}{dt} \\\\\n \\frac{d^2y}{dx^2}&=\\frac{1}{x^2}(\\frac{d^2y}{dt^2}-\\frac{dy}{dt}) \\\\\n \\frac{d^3y}{dx^3}&=\\frac{1}{x^3}(\\frac{d^3y}{dt^3}-3\\frac{d^2y}{dx^2}+2\\frac{dy}{dt}) \\\\\n \\dots\n\\end{align*}\n$$\n从而有\n$$\n\\begin{align*}\n xy'&=\\frac{dy}{dt} \\\\\n x^2y''&=\\frac{d^2y}{dt^2}-\\frac{dy}{dt}=\\frac{d}{dt}(\\frac{d}{dt}-1)y \\\\\n x^3y'''&=\\frac{d^3y}{dt^3}-3\\frac{d^2y}{dx^2}+2\\frac{dy}{dt}=\\frac{d}{dt}(\\frac{d}{dt}-1)(\\frac{d}{dt}-2)y \\\\\n \\dots \\\\\n x^ky^{(k)}&=\\frac{d}{dt}(\\frac{d}{dt}-1)\\cdots(\\frac{d}{dt}-k+1)y \\\\\n \\dots\n\\end{align*}\n$$\n将其回代入欧拉方程,就能获得一个以 $t$ 为自变量的常系数线性微分方程。\n\n## 常系数线性微分方程组\n\n$$\n\\left\\{\\begin{array}{l}\n \\frac{dx}{dt}=f(x,y) \\\\\n \\frac{dy}{dt}=g(x,y)\n\\end{array}\\right.\n$$\n\n求解步骤如下:\n1. 消元:得到只含有一个变量 $t$ 的高阶常系数线性微分方程;\n2. 求解:求解此微分方程,得到 $x$ 或者 $y$ 的通解,再代入原方程组得到另一个变量的通解。\n\n## Reference\n[1] [数学物理方法(第五章:常微分方程)](https://zhuanlan.zhihu.com/p/433331729)\n\n[2] [常微分方程求解](https://www.zhihu.com/tardis/bd/art/640503002)\n\n[3] [微分方程——所有类型的微分方程汇总,看看有你不会的吗?](https://zhuanlan.zhihu.com/p/693250289)\n\n[4] [常微分方程(1):常微分方程的基本解法](https://zhuanlan.zhihu.com/p/363987756)","tags":["数学物理方法"],"categories":["math"]},{"title":"体渲染","url":"/archives/61387/","content":"\n# 体渲染\n\n体渲染属于整个渲染技术的分支,它的目的主要是为了解决云、烟、果冻这类非刚性物体的渲染建模,可以简单理解为是为了处理密度较小的非固体的渲染。\n\n为了建模这种非刚性物体的渲染,体渲染把气体等物质抽象成一团飘忽不定的粒子群。光线在穿过这类物体时,其实就是光子在跟粒子发生碰撞的过程。\n\n下图是体渲染建模的示意图。光沿直线方向穿过一堆粒子 (粉色部分),如果能计算出每根光线从最开始发射,到最终打到成像平面上的辐射强度,我们就可以渲染出投影图像。而体渲染要做的,就是对这个过程进行建模。为了简化计算,我们就假设光子只跟它附近的粒子发生作用,这个范围就是图中圆柱体大小的区间。\n\n\n\n体渲染把光子与粒子发生作用的过程,进一步细化为四种类型:\n\n- 吸收 (absorption):光子被粒子吸收,会导致入射光的辐射强度**减弱**;\n- 放射 (emission):粒子本身可能发光,比如气体加热到一定程度就会离子化,变成发光的「火焰」。这会进一步**增大**辐射强度;\n- 外散射 (out-scattering):光子在撞击到粒子后,可能会发生弹射,导致方向发生偏移,会**减弱**入射光强度;\n- 内散射 (in-scattering):其他方向的光子在撞到粒子后,可能和当前方向上的光子重合,从而**增强**当前光路上的辐射强度。\n\n下图是这四种情况的示意图,$L_i$ 表示入射光,$L_o$ 表示出射光。\n\n\n\n出射光与入射光之间的变化量,可以表示为这四个过程的叠加:\n\n$$\ndL(x,w)=\\text{emission}+\\text{inscattering}-\\text{outscattering}-\\text{absorption}\n$$\n\n其中 $x$ 表示光线上某个位置点,$w$ 表示光线发射方向。\n\n## 吸收 (absorbing)\n\n\n\n假设粒子都是半径为 $r$ 的球体,那么每个粒子的投影面积是 $A=\\pi r^2$ (也就是每个粒子对光线的遮挡面积)。假设圆柱体中粒子的密度是 $\\rho$,圆柱体的底面积是 $E$。\n\n当圆柱体足够薄 (薄到跟粒子一样厚) 的时候,可以认为粒子之间不会互相重叠 (也就是粒子都平铺在圆柱体一个横截面上)。\n\n假定这个厚度是 $\\Delta s$,那么在这个厚度内,圆柱体体积为 $E\\Delta s$,粒子总数为 $\\rho E\\Delta s$。这些粒子遮挡的面积为 $\\rho E\\Delta sA$,占整个底面积的比例为 $\\rho E\\Delta sA/E=\\rho A\\Delta s$。也就是说,**当一束光通过这个圆柱体的时候,有 $\\rho A\\Delta s$ 的概率会被遮挡。**\n\n换句话说,如果我们在圆柱体的一端发射无数光线 (假设都朝相同的方向),在另一端接收,会发现有些光线安然通过,有些则被粒子遮挡 (吸收)。**整体上,这些接受到的光线总强度,相比入射光线总强度而言,会有 $\\rho A\\Delta s$ 比例的衰减,即出射光的强度均值是入射光的 $\\rho A\\Delta s$ 倍。**\n\n$$\n\\begin{align}\n I_o-I_i=\\Delta I=-\\rho(s)AI(s)\\Delta s\\notag \\\\\n \\implies dI=-\\rho(s)AI(s)ds\n\\end{align}\n$$\n\n粒子密度 $\\rho$ 是一个关于 $s$ 的函数,因为每个区域的密度都是不同的。\n\n(1) 式可以转换为常微分方程:\n\n$$\n\\begin{equation}\n \\frac{dI}{ds}=-\\rho(s)AI(s)=-\\tau_a(s)I(s)\n\\end{equation}\n$$\n\n解这个方程可以得到:\n\n$$\n\\begin{equation}\n I(s)=I_0\\exp(-\\int_0^s\\tau_a(t)dt)\n\\end{equation}\n$$\n\n公式里面的 $I_0$ (注意区分 $I_o$) 是常微分方程中的常数项,物理意义上表示光线的起始点。而 $I_o$ 应为 $I_o=I_i\\exp(-\\int_i^o\\tau_a(t)dt)$\n\n如果介质 (粒子群) 是均匀的,即 $\\tau_a(t)$ 处处相等,那么入射光在经过介质 (粒子群) 后,辐射强度会呈指数衰减。这被称为**比尔-朗伯吸收定律 (Beer-Lambert Law)**:\n\n\n\n可以由此定义**透射比 (transmittance)**:\n\n$$\n\\begin{equation}\n T(s)=\\frac{I_o}{I_i}=\\exp(-\\int_i^o\\tau_a(t)dt)\n\\end{equation}\n$$\n\n它表示粒子群某一点的透明度,数值越大,说明粒子群越透明,光线衰减的幅度就越小。\n\n而透明度本身是关于 $\\tau_a(t)$ 的方程,$\\tau_a(t)$ 越大,$T(s)$ 就越小。由公式 (2) 可知,$\\tau_a(t)=\\rho(t)A$,它是由粒子密度和投影面积决定的。这在直觉上也很好理解,如果粒子密度大,粒子本身也比较大,那么遮住光线的概率也会相应提升,自然透明度也就下降了。\n\n$\\tau_a(t)$ 也被称为**光学厚度 (optical depth)**。\n\n\n\n## 放射 (emission)\n\n除了吸收之外,粒子本身也可能发光。\n\n假设单个粒子发射一束光的辐射强度为 $I_e$,那么按照前文的描述,在圆柱体足够薄的情况下,粒子总数是 $\\rho AE\\Delta s$,则总的发光强度为 $I_e\\rho AE\\Delta s$。\n\n如果我们在圆柱体一端去接收粒子们放射的光线,会发现有时候能接收到,有时候刚好接收点所在的光路上没有粒子,就接收不到。能接收到光线的概率为 $\\rho AE\\Delta s/E=\\rho A\\Delta s$,那么接收到的光线的平均强度为 $I_e\\rho A\\Delta s$。\n\n同样地,可以得到放射光强的常微分方程:\n\n$$\n\\begin{equation}\n \\frac{dI}{ds}=I_e(s)\\rho(s)A=I_e(s)\\tau_a(s)\n\\end{equation}\n$$\n\n$I_e$ 是一个关于 $s$ 的函数,因为圆柱体不同位置的粒子,所放射的光强都是有差异的。这里我们也看到,类似吸收,粒子放射的光强同样和 $\\tau_a(s)$ 有关,这在直觉上也是合理的,如果粒子能发光,那粒子密度和粒子颗粒越大,放射的辐射均值也就越大。\n\n## 外散射 (out-scattering)\n\n粒子除了吸收光子,也可能会弹射光子,这个过程称为外散射,即光子被弹射出原本的光路,导致光线强度减弱。\n\n同吸收一样,外散射对光线的「削弱」程度,也跟光学厚度相关,不过过程相对吸收来说又复杂一些,因此我们用 $\\tau_s$ 来表示外散射对光线的削弱比例,以区别于 $\\tau_a$。\n\n同样地,这一过程可以表示为:\n\n$$\n\\begin{equation}\n \\frac{dI}{ds}=-\\tau_s(s)I(s)\n\\end{equation}\n$$\n\n## 内散射 (in-scattering)\n\n光子可以被弹射走,自然就有其他光路的光子被弹射到当前光路,这一过程就是内散射。\n\n内散射的过程比外散射又更加复杂,因为弹射到当前光路的光子可能来自多条不同的光路,因此需要综合考虑其他光路的辐射强度以及各种弹射角度。\n\n不过本文不打算深入探究这一点,我们就认为其他光路的辐射强度为 $I_s$,而弹射到当前光路的能量损失比为 $\\tau_s$ (注意这里和外散射使用的是相同的系数,既然都是散射,那有些性质上自然是共通的)。\n\n内散射的过程可以表示为:\n\n$$\n\\begin{equation}\n \\frac{dI}{ds}=\\tau_s(s)I_s(s)\n\\end{equation}\n$$\n\n# 体渲染方程\n\n我们把以上四个过程都综合到一个公式中:\n\n$$\n\\begin{equation}\n \\frac{dI}{ds}=-\\tau_a(s)I(s)-\\tau_s(s)I(s)+\\tau_a(s)I_e(s)+\\tau_s(s)I_s(s)\n\\end{equation}\n$$\n\n其中,**吸收**和**外散射**都会削弱光线的辐射强度,并且由于它们都和入射光有关,因此它们共同构成了体渲染中的**衰减项 (attenuation item)**,而粒子**发光**和**内散射**都来自独立的光源,因此被称为**源项 (source item)**。\n\n为了求解方便,定义 $\\tau_t=\\tau_a+\\tau_s$,公式 (8) 就简化为:\n\n$$\n\\begin{equation}\n \\frac{dI}{ds}=-\\tau_t(s)I(s)+\\tau_a(s)I_e(s)+\\tau_s(s)I_s(s)\n\\end{equation}\n$$\n\n求解这个微分方程,得到:\n\n$$\n\\begin{equation}\n I(s)=\\int_0^s\\exp(-\\int_0^t\\tau_t(u)du)[\\tau_a(s)I_e(s)+\\tau_s(s)I_s(s)]dt+I_0\\exp(-\\int_0^s\\tau_t(t)dt)\n\\end{equation}\n$$\n\n公式 (10) 就是体渲染的渲染方程了,其中 $I_0$ 就是入射光最开始的强度,在 NeRF 中把它当作是背景光。\n\n这个公式告诉我们,出射光的强度主要由入射光 (背景光) 和源项 (粒子发光以及内散射) 等构成的,而这两者在传递过程中都伴随着相同的衰减 ($\\tau_t$)。同时由于从 $I_0$ 到 $I_s$ 的整个光路上,处处都可能存在粒子发光和内散射现象,因此源项是需要从 $0$ 开始相加一直到 $s$ 的,对应到公式中就是积分 $\\int_0^s$。\n\n有了这个公式,我们就可以建模出每一条光线在传播介质中,辐射强度的变化,并最终渲染出图像 (当然前提要知道介质中每一点的 $\\tau_t$、$I_e$ 等信息)。\n\n**注意,虽然我们在讨论这个模型的时候一直是用辐射强度这个概念,但直接把 $I(s)$ 替换成颜色也是可以的。**\n\n**现在,我们做一个大胆的假设,假设 $\\tau_t$、$\\tau_a$、$\\tau_s$ 这些都相等,统一用 $\\sigma$ 表示**,同时令 $C=I_e+I_s$,那么公式 (10) 可以进一步简化成:\n\n$$\n\\begin{align}\n I(s)&=\\int_0^s\\exp(-\\int_0^t\\sigma(u)du)\\sigma(t)C(t)dt+I_0\\exp(-\\int_0^s\\sigma(t)dt)\\notag \\\\\n &=\\int_0^sT(t)\\sigma(t)C(t)dt+I_0T(s)\n\\end{align}\n$$\n\n其中 $T(t)=\\exp(-\\int_0^t\\sigma(u)du)$\n\n# 计算机的体渲染\n\n公式 (11) 在计算机中是无法表达的,需要进一步离散化。\n\n我们将整个光路 $[0, s]$ 划分为 $N$ 个相等间距的区间:$[t_n,t_{n+1}]$。那么,只要能算出每个 $[t_n,t_{n+1}]$ 区间内的辐射强度 $I(t_n\\rightarrow t_{n+1})$,最后把 $N$ 个区间的辐射加起来,就可以得到最终的光线强度了。$N$ 越大,则越接近理论数值 (公式 (11))。\n\n为了方便运算,我们假设在每个区间内,$\\sigma(t)$ 处处等于 $\\sigma_n$\n,$C(t)$ 处处等于 $C_n$。那么可以得到:\n\n$$\n\\begin{align}\n I(t_n\\rightarrow t_{n+1})&=\\int_{t_n}^{t_{n+1}}T(t)\\sigma_nC_ndt \\notag\\\\\n &=\\sigma_nC_n\\int_{t_n}^{t_{n+1}}T(t)dt\n\\end{align}\n$$\n\n注意 $T(t)=\\exp(-\\int_0^t\\sigma(u)du)$,是需要从光线起点处开始积分的。不过,好在 $T(t)$ 可以拆分为两段的乘积:\n\n$$\n\\begin{align}\n T(t)&=\\exp(-\\int_0^t\\sigma(u)du) \\notag\\\\\n &=\\exp(-[\\int_0^{t_n}\\sigma(u)du+\\int_{t_n}^t\\sigma(u)du]) \\notag\\\\\n &=\\exp(-\\int_0^{t_n}\\sigma(u)du)\\exp(-\\int_{t_n}^t\\sigma(u)du) \\notag\\\\\n &=T(0\\rightarrow t_n)T(t_n\\rightarrow t)\n\\end{align}\n$$\n\n这样公式 (12) 就变成:\n\n$$\n\\begin{align}\n I(t_n\\rightarrow t_{n+1})&=\\sigma_nC_n\\int_{t_n}^{t_{n+1}}T(0\\rightarrow t_n)T(t_n\\rightarrow t)dt \\notag\\\\\n &=\\sigma_nC_nT(0\\rightarrow t_n)\\int_{t_n}^{t_{n+1}}T(t_n\\rightarrow t)dt \\notag\\\\\n &=\\sigma_nC_nT(0\\rightarrow t_n)\\int_{t_n}^{t_{n+1}}\\exp(-\\int_{t_n}^t\\sigma_ndu)dt \\notag\\\\\n &=\\sigma_nC_nT(0\\rightarrow t_n)\\int_{t_n}^{t_{n+1}}\\exp(-\\sigma_nu|_{t_n}^t)dt \\notag\\\\\n &=\\sigma_nC_nT(0\\rightarrow t_n)\\int_{t_n}^{t_{n+1}}\\exp(-\\sigma_n(t-t_n))dt \\notag\\\\\n &=\\sigma_nC_nT(0\\rightarrow t_n)\\frac{\\exp(-\\sigma_n(t-t_n))}{-\\sigma_n}|_{t_n}^{t_{n+1}} \\notag\\\\\n &=C_nT(0\\rightarrow t_n)(1-\\exp(-\\sigma_n(t_{n+1}-t_n)))\n\\end{align}\n$$\n\n现在我们得到了 $I(t_n\\rightarrow t_{n+1})$,接下来就是把每一段得累加起来了:\n\n$$\n\\begin{align}\n I(s)&=\\int_0^sT(t)\\sigma(t)C(t)dt+I_0T(s) \\notag\\\\\n &\\approx\\sum_{n=1}^NI(t_n\\rightarrow t_{n+1})+I_0T(s) \\notag\\\\\n &=\\sum_{n=1}^NC_nT(0\\rightarrow t_n)(1-\\exp(-\\sigma_n(t_{n+1}-t_n)))+I_0T(s)\n\\end{align}\n$$\n\n假设 $\\delta_n=t_{n+1}-t_n$,那么 $\\delta_n$ 表示的就是每个小区间的长度,我们令 $T_n=T(0\\rightarrow t_n)$,则 $T_n$ 也可以离散化为:\n\n$$\n\\begin{align}\n T_n&=\\exp(-\\int_0^{t_n}\\sigma(u)du) \\notag\\\\\n &\\approx\\exp(\\sum_{k=1}^{n-1}-\\sigma_k\\delta_k)\n\\end{align}\n$$\n\n将公式 (16) 代入 (15) 后,可以得到最终计算机使用的体渲染公式:\n\n$$\n\\begin{equation}\n I(s)=\\sum_{n=1}^NT_n(1-\\exp(-\\sigma_k\\delta_k))C_n+I_0T(s)\n\\end{equation}\n$$\n\n这个公式就是 NeRF 论文中公式 (3) 的出处,只不过加了背景光:\n\n\n\n# 参考资料\n\n- [Volume Rendering](https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/volume-rendering-summary-equations.html)\n- [Optical Models for Direct Volume Rendering](https://courses.cs.duke.edu/spring03/cps296.8/papers/max95opticalModelsForDirectVolumeRendering.pdf)\n- [Volume Rendering Digest (for NeRF)](https://arxiv.org/abs/2209.02417)\n- [NeRF入门之体渲染 (Volume Rendering)](https://zhuanlan.zhihu.com/p/595117334)","tags":["CG"],"categories":["computer"]},{"title":"图像评价指标","url":"/archives/2070/","content":"\n# PSNR\n\nPeak Signal to Noise Ratio,峰值信噪比,用于评价图像的质量。\n\n衡量最大值信号和背景噪音之间的图像质量参考值。单位为 dB,其值越大,图像失真越少。一般来说,PSNR 高于 40dB 说明图像质量几乎与原图一样好;在 30-40dB 之间通常表示图像质量的失真损失在可接受范围内;在 20-30dB 之间说明图像质量比较差;PSNR 低于 20dB 说明图像失真严重。\n\n$$\nPSNR=10\\cdot\\log_{10}\\frac{MAX_I^2}{MSE} \\\\\nMSE=\\frac{1}{mn}\\sum_{i=0}^{m-1}\\sum_{j=0}^{n-1}[I(i,j)-K(i,j)]^2\n$$\n\n其中\n- $I$ 为 $m\\times n$ 的灰度图;\n- $K$ 为 $m\\times n$ 的噪声图;\n- $MAX_I$ 为图片可能的最大像素值,即 $n$-bit 的图像的 $MAX_I=2^n-1$。\n\n# SSIM\n\nstructural similarity index,结构相似性,用于衡量两幅图像间的结构相似性。\n\n使用亮度、对比度以及结构量化图像的属性,用均值估计亮度,方差估计对比度,协方差估计结构相似度。SSIM 值的范围为 $[0,1]$,越大代表图像越相似。如果两张图片完全一样时,SSIM 值为 1。\n\n$$\nSSIM(x,y)=\\frac{(2\\mu_x\\mu_y+c_1)(2\\sigma_{xy}+c_2)}{(\\mu_x^2+\\mu_y^2+c_1)(\\sigma_x^2+\\sigma_y^2+c_2)}\n$$\n\n其中\n- $u$ 为均值;\n- $\\sigma$ 为方差/协方差;\n- $c_i=(k_iL)^2$,$L$ 为像素范围,$n$-bit 的图像的 $L=2^n-1$,$k_i$ 为超参数。\n\n# LPIPS\n\nLearned Perceptual Image Patch Similarity,学习感知图像块相似度,也称为感知损失(perceptual loss),用于度量两张图像之间的差别。\n\n给定两个输入,利用 $\\mathcal{F}$ 网络提取 $L$ 层的特征,然后利用 $w_l$ 点乘来放缩通道,最后计算 $L_2$ 损失\n\n\n\n$$\nd(x,x_0)=\\sum_l\\frac{1}{H_lW_l}\\sum_{h,w}||w_l\\odot(\\hat{y}_{hw}^l-\\hat{y}_{0hw}^l)||_2^2\n$$\n\n其中\n- $x$ 为 ground truth 图像块;\n- $x_0$ 为含噪声图像失真块。\n\n相关论文:\n- [ZHANG R, ISOLA P, EFROS A A, et al. The Unreasonable Effectiveness of Deep Features as a Perceptual Metric [J]. IEEE, 2018.](https://ieeexplore.ieee.org/document/8578166)\n\n# LMD\n\nLandmark Distance,用于评估面部图像生成质量的指标。\n\n度量生成的面部图像与真实面部图像之间的面部特征点的距离,以评估生成图像的面部特征的准确性。\n\n计算LMD的一般步骤如下:\n1. 从生成的面部图像和真实面部图像中检测面部特征点(例如,眼睛、鼻子、嘴巴等)的位置;\n2. 计算每个特征点之间的欧几里得距离;\n3. 汇总或平均这些距离,以获得 LMD 的最终值。\n\n$$\nLMD=\\frac{1}{N}\\sum_{i=1}^N\\sqrt{(x_{g_i}-x_{r_i})^2+(y_{g_i}-y_{r_i})^2}\n$$\n\n其中\n- $N$ 为特征点数量;\n- $x_{g_i}$ 和 $y_{g_i}$ 分别是**生成图像**上第 $i$ 个特征点的 x 和 y 坐标;\n- $x_{r_i}$ 和 $y_{r_i}$ 分别是**真实图像**上第 i 个特征点的 x 和 y 坐标。\n\n# FID\n\nFréchet Inception Distance,用于评估生成模型性能的指标,特别是在生成对抗网络(GANs)中广泛使用。\n\n测量生成图像与真实图像分布之间的差异,即**生成图像的质量和多样性**。较低的 FID 值表示生成图像更接近真实图像的分布,对应的生成模型更好。\n\nFID 的计算基于两个图像分布之间的特征向量空间的 Fréchet距离。\n\n$$\nFID(P,G)=||\\mu_p-\\mu_G||^2+Tr(\\Sigma_P+\\Sigma_G-2\\sqrt{\\Sigma_P\\Sigma_G})\n$$\n\n其中 \n- $P$ 表示真实图像分布的特征向量集合,通常使用 Inception 网络的中间层的输出来表示;\n- $G$ 表示生成图像分布的特征向量集合,也使用相同的方式表示;\n- $\\mu_p$ 和 $\\mu_G$ 分别是 $P$ 和 $G$ 的特征向量集合的均值;\n- $\\Sigma_P$ 和 $\\Sigma_G$ 分别是 $P$ 和 $G$ 的特征向量集合的协方差矩阵;\n- $Tr(\\Sigma_P+\\Sigma_G-2\\sqrt{\\Sigma_P\\Sigma_G})$ 表示协方差矩阵的迹的平方根。\n\n# 参考资料\n\n- [【图像评价指标】PSNR LPIPS LMD SSIM FID](https://zhuanlan.zhihu.com/p/658827245)","tags":["CV"],"categories":["computer"]},{"title":"三维模型","url":"/archives/48868/","content":"\n# 三维模型\n\n三维模型由**形状(shape)**和**外观(appearance)**组成,外观大致可分为**材质(material)**和**光照(lighting)**。在得到这些信息后,只需要经过一定的算法,就能得到图像。像这样从模型到图像的正向过程,称之为**渲染(rendering)**;而从图像得到模型的这个逆向过程,称为**反渲染(inverse rendering)**。\n\n\n\n渲染过程是通过一个渲染方程来完成的,也即将一系列的参数输入这个渲染方程,最终渲染出来的图像就是这个方程的解。\n\n但是否可以根据这个方程的解来反向推导出初始参数呢?这就是NeRF要解决的问题,即从图像中得到模型的信息。\n\n# 形状表征\n\n主流的形状表征方式有以下四种:\n- 网格(Mesh):用多个多边形表征物体;\n- 点云(Point Cloud):某个坐标系下的点的数据的集合,数据可以是三维坐标(x, y, z)、颜色、强度值、时间等等;\n- 占据场(Occupancy Field):物体占据的空间点的值为 1,未占据的空间点的值为 0;\n- 有向距离场(Signed distance Field):从物体表面射出向量,往物体内部为正,外部为负。\n\n\n\n此外还有一些表示,如**体素(Voxel)**,**多视图(Multi-view)**等等。\n\n# 外观表征\n\n最常见是**材质纹理贴图(Material Texture Map)+环境光照**。将三维物体 unwrap 成二维的图片,并在图片中各点存储对应三维物体的材质的性质。同时使用环境光照获取图片各点对应的光照。\n\n这种方法易于编辑修改,用途十分广泛。但缺点是非常理想且十分繁琐,因为需要将材质和光照分开,且不仅要对贴图进行映射,还分不同的表面对不同的光线进行求交。\n\n此外还有**辐射场(Radiance Field)**或者**表面光场(Surface light filed)**,这种方法将材质与光照打包,给出每个表面点 $(x,y,z)$ 处不同的观察角度 $(\\theta,\\phi)$ 的颜色 $(r,b,g)$。用这种方式可以很方便的描述物体表面的颜色,反射,和阴影等效果。但缺点是不易于修改和编辑。\n\n\n\n# 参考资料\n\n- [NeRF(Neural Radiance Fields)基础知识点和研究方向](https://zhuanlan.zhihu.com/p/622380174)\n- [NeRF:今年最火的AI技术!详解](https://www.bilibili.com/video/BV1LG4y1P7hY/?vd_source=5f3724657962d635e839ed96a53c359a)","tags":["CG"],"categories":["computer"]},{"title":"NeRF学习","url":"/archives/59645/","content":"\nNeRF(Neural Radiance Field)即神经辐射场。仅利用多个视角的 RGB 图像,训练神经网络构建一个隐式的三维表征,即可渲染得到模型没有见过的新视角下的图像。其主要贡献在于实现了**神经场(Neural Field)与体渲染(Volume Rendering)的有效结合**。\n\n<!-- more -->\n\n# 基础知识\n\n- {% post_link 三维模型 %}\n- {% post_link 体渲染 %}\n- {% post_link 图像评价指标 %}\n- {% post_link 相机参数与坐标系变换 %}\n\n# 表征方法\n\n## Soft Shape\n\n这是 NeRF 成功及其重要的因素。\n\nNeRF 使用了**软不透明度场(soft opacity field)**来生成形状,即从一片三维虚空中,按图像需要逐渐生成出具有特定结构三维物体。这样生成的形状不像 Mesh 等 Hard Geometry 一样具有确定性的空间边界,而是以一定概率连续地弥散在整个空间中的。\n\n\n\n\n\nSoft Geometry 有两大好处:\n\n1. 不需要使用对象分割掩码(object segmentation masks);\n2. 没有边界不连续的(boundary discontinuity)问题,这意味着更易于做可微渲染。\n\n同时也有一些缺点,如渲染成本高,编辑困难等等。\n\n\n\n相关论文:\n\n- [Yu A , Fridovich-Keil S , Tancik M , et al. Plenoxels: Radiance Fields without Neural Networks[J]. 2021.](https://arxiv.org/abs/2112.05131)\n\n- [Sun C , Sun M , Chen H T . Direct Voxel Grid Optimization: Super-fast Convergence for Radiance Fields Reconstruction[C]. 2021.](https://arxiv.org/abs/2111.11215)\n\n## Radiance Field\n\n给出每个表面点 $(x,y,z)$ 处不同的观察角度 $(\\theta,\\phi)$ 的颜色 $(r,b,g)$。用这种方式可以很方便的描述物体表面的颜色,反射,和阴影等效果。但缺点是不易于修改和编辑。\n\n相关论文:\n\n- [Wood D N ,Azuma D I ,Aldinger K , et al. Surface light fields for 3D photography[C]. SIGGRAPH conference. 2000.](https://grail.cs.washington.edu/projects/slf/papers/siggraph2000/talk/)\n\n> 以上这两种表征方法都可以视为空间中的函数,因此也方便后续使用神经网络来拟合。\n\n# 方法概述\n\n\n\nNeRF 工作的过程可以分成两部分:**三维重建**和**渲染**。\n\n1. 三维重建:从二维多视角图像构建三维场景(反渲染)\n \n 利用三维点的位置 $(x,y,z)$ 及方位视角 $(\\theta, \\phi)$ 作为输入,通过多层感知机(MLP)建模该点对应的颜色(Color,RGB)及体密度 (Volume Density,$\\sigma$),该建模过程 $F_\\Theta$ 形成了三维场景的**隐式表示**。\n\n2. 渲染:计算某视角下的二维图像(体渲染)\n \n 使用可微分渲染器(Differentiable Renderer),利用重建部分得到的 Scene Representation 沿着 ray 结合得到最终的二维图像像素值.\n\n在训练的时候,利用渲染部分得到的二维图像,通过优化与 Ground Truth 的 L2 Loss 训练网络。\n\n## Pipeline\n\n\n\n## Neural Radiance Field Scene Representation\n\n该部分的输入是三维位置 $(x,y,z)$ 和二维方位视角 $(\\theta,\\phi)$,输出是颜色 $c=(r,g,b)$ 和体密度 $\\sigma$,即利用一个 MLP 网络近似地表示这种映射 $F_\\Theta: (x,d)\\rightarrow(c,\\sigma)$,这个映射 $F_\\Theta$ 就是一种三维场景的**隐式表示**。\n\n该 MLP 网络先使用 8 层 256 通道的全连接层接收 $(x,y,z)$ 输入,并使用 ReLU 激活,输出 $\\sigma$ 和一个 256 维的向量。然后这个向量与 $(\\theta,\\phi)$ 连接,并输入 1 层 128 通道的全连接层,并使用 ReLU 激活,输出颜色 $c=(r,g,b)$。\n\n\n\n## Volume Rendering with Radiance Fields\n\n我们可以将体密度 $\\sigma(\\mathbf{x})$ 视为 ray 终止于位置 $\\mathbf{x}$ 的无限小粒子的微分概率。\n\n在 ray $\\mathbf{r}(t)=\\mathbf{o}+t\\mathbf{d}$ 上 $[t_n,t_f]$ 段的期望颜色 $C(\\mathbf{r})$ 为:\n\n$$\nC(\\mathbf{r})=\\int_{t_n}^{t_f}T(t)\\sigma(\\mathbf{r}(t))\\mathbf{c}(\\mathbf{r}(t),\\mathbf{d})dt,\\text{ where }T(t)=\\exp(-\\int_{t_n}^t\\sigma(\\mathbf{r}(s))ds)\n$$\n\n其中 $T(t)$ 表示从 $t_n$ 到 $t$ 沿 ray 的累积透射率,即 ray 从 $t_n$ 到 $t$ 不碰到任何粒子的概率。\n\n实际训练中,需要对积分进行**离散化处理**,NeRF 采用了 stratified sampling 方式(Monte Carlo 法的采样方法):将射线需要积分的**区域分为 N 等份**,然后在每一个小区域中进行均匀随机采样:\n\n$$\nt_i\\sim\\mathcal{U}[t_n+\\frac{i-1}{N}(t_f-t_n),t_n+\\frac{i}{N}(t_f-t_n)]\n$$\n\n离散化后,原渲染公式变为:\n\n$$\n\\hat{C}(\\mathbf{r})=\\sum_{i=1}^NT_i(1-\\exp(-\\sigma_i\\delta_i))\\mathbf{c}_i,\\text{ where }T_i=\\exp(-\\sum_{j=1}^{i-1}\\sigma_j\\delta_j)\n$$\n\n其中 $\\delta_i=t_{i+1}-t_i$ 为相邻样本间的距离。\n\n关于体渲染的推导,可参考{% post_link 体渲染 %}.\n\nNeRF 在一条 ray 上采样多个不同的点,不同的点有各自的颜色,该颜色是通过神经网络预测出的。将 ray 上所有点的颜色结合起来,即可获得这个 ray 的颜色。\n\n\n\n## Positional encoding\n\n尽管可以利用神经网络把表征的各种映射关系拟合成一个函数,比如映射成颜色的函数,映射成是否 occupy 的函数,映射成光照密度(density)的函数等等。\n\n但问题是神经网络表示的是**有偏的(bias)**,通过神经网络可能无法得到高质量的结果,如下图所示,它更偏向于得到光滑(smooth)的结果(也即学习**低频**信号),但我们期待的是得到锐利(sharp)的结果(也即学习**高频**信号)。因此 NeRF 提出了在将 $(x,y,z,\\theta,\\phi)$ 传递给网络之前,使用高频函数将输入映射到更高维度的空间,可以更好地拟合包含高频变化的数据。\n\nNeRF 使用了一个具有傅里叶特征(Fourier features)的函数 $\\gamma$ 将 $\\mathbb{R}$ 数据映射到 $\\mathbb{R}^{2L}$,即 $F_\\Theta=F_\\Theta'\\circ\\gamma$。该高频编码函数为:\n\n$$\n\\gamma(p)=(\\sin(2^0\\pi p),\\cos(2^0\\pi p),...,\\sin(2^{L-1}\\pi p),\\cos(2^{L-1}\\pi p))\n$$\n\n其中 $p$ 就是 $(x,y,z,\\theta,\\phi)$ 输入,并且输入均归一化于 $[-1,1]$。\n\n根据这篇论文:\n\n- [Tancik, M., Srinivasan, P. P., Mildenhall, B., Fridovich-Keil, S., Raghavan, N., Singhal, U., Ramamoorthi, R., Barron, J. T., & Ng, R. (2020). Fourier features let networks learn high frequency functions in low dimensional domains.](https://arxiv.org/abs/2006.10739)\n\nNeRF 对 $(x,y,z)$ 取 $L=10$,对 $(\\theta,\\phi)$ 取 $L=4$.\n\n下图可以看出来,结果相较之前有十分明显的提升。\n\n\n\n使用神经网络表示信号的方式,称为**神经场(Neural Field)**。这种方式有三个好处:\n1. 每个三维模型大约只需要10MB就可以表示,利于传输;\n2. 场景不需要离散化(discretized);\n3. 更加灵活且易于优化:例如在正则化方面更方便,泛化能力更强。\n\n\n\n## Hierarchical volume sampling\n\nNeRF 的任务就是将一系列的微元采样点输入网络,计算体密度和颜色。那么微元的采样就成为一个关键的问题:采样点过多开销过大,采样点过少近似误差太大。直观的一个想法是,最好使得样本点分布在光线与物体相交的表面附近,而尽可能地避免在空缺部分以及被遮挡了的部分进行过多的采样,因为这些部分对最好的颜色贡献是很少的。\n\n基于这一想法,NeRF 提出 Hierarchical volume sampling,**即先按照均匀随机采样进行一次粗采样,将粗采样的输出的结果转化为分布,再根据分布进行一次精采样,最后 NeRF 训练的损失也是粗采样和精采样结果相加的结果**。这样就实现了一个自动化 Coarse-To-Fine 的训练过程。如下图所示。\n\n\n\n\n1. Coarse 阶段:\n \n 在 $[t_n,t_f]$ 中进行 stratified sampling 采样出 $N_c$ 个位置,输入 coarse 网络,并将输出输入到第二阶段。\n\n2. Fine 阶段:\n \n 考虑到离散化的渲染公式可视为沿着 ray 的所有采样颜色 $c_i$ 的加权和\n \n $$\n \\hat{C}(\\mathbf{r})=\\sum_{i=1}^{N_c}w_i\\mathbf{c}_i,\\quad w_i=T_i(1-\\exp(-\\sigma_i\\delta_i))\n $$\n\n 所以归一化权重 $\\hat{w}_i=\\frac{w_i}{\\sum_{j=1}^{N_c}w_j}$ 就是 $\\hat{C}(\\mathbf{r})$ 在 ray 上的**概率密度函数(PDF)**。\n\n 因此,这一阶段基于 inverse transform sampling,在这个分布中采样出 $N_f$ 个位置,然后将 $N_c+N_f$ 采样得到的数据输入 fine 网络,并计算最终渲染的光线颜色 $C(r)$\n\n这上述两阶段的采样达到了类似 importance sampling 的效果,但将采样点视为非均匀的离散化,而非对每个点进行独立的概率估计。\n\n对每个场景都需要优化一个单独的 neural continuous volume representation network。需要优化的 $L_2$ 损失为(同时优化 coarse 和 fine 网络):\n\n$$\n\\mathcal{L}=\\sum_{r\\in\\mathcal{R}}[||\\hat{C}_c(\\mathbf{r})-C(\\mathbf{r})||_2^2+||\\hat{C}_f(\\mathbf{r})-C(\\mathbf{r})||_2^2]\n$$\n\n其中\n\n- $\\mathcal{R}$:ray 的集合;\n- $C(\\mathbf{r})$:ground truth RGB;\n- $\\hat{C}_c(\\mathbf{r})$:coarse volume predicted RGB;\n- $\\hat{C}_f(\\mathbf{r})$:fine volume predicted RGB;\n\n虽然 Coarse-MLP 和 Fine-MLP 仅在样本点的输入上有所不同(同样的 Positional Encoding 和 MLP),但其样本点的分布本身就决定了各自 MLP 能够在何种程度“看到”低频和高频信号。Coarse-MLP 的样本点是光线在 near/far 区间上的均匀采样,这意味着这条光线上样本点的高频信号失真了(远处的也会包含高频信号);而 Fine-MLP 是在表面交点附近的密集采样,这意味着 MLP 能够密集地“感受”到这一段区域的高频信号(尽可能保证不失真)。\n\n可以理解为采用不同的采样策略就是施加了不同的“滤波器”。对同一条光线,两组不同的采样使得 MLP “看到”截然不同的两组信号;如果想让一个 MLP 同时处理好这两组截然不同的信号,可能得高超的调参技巧与更大的网络了。\n\n\n# 指标\n\nNeRF 使用了 **PSNR,SSIM,LPIPS** 三个指标。关于这三个指标的详细信息,可参考{% post_link 图像评价指标 %}。\n\n# 拍摄场景\n\n1. 物体在中央,相机在四周拍摄,往往用于重建主体;\n2. 相机方向固定,在小范围移动;\n3. 类似全景图拍摄,相机在中间,朝各个方向拍摄,通常用于重建背景;\n4. 在固定空间内随机方向和分布拍摄;\n5. 是 1 和 3 的结合,既重建物体主体,又重建背景。\n\n\n\n目前 NeRF 需要解决的问题就是,在空间内重建近景时远景会模糊,反之重建远景的时候近景会模糊。\n\n为了解决这一问题,NeRF++ 提出了一种解决方案,即将 NeRF 进行分解和组合,在**近景(Foreground)**和**远景(Background)**处都分别使用一个 NeRF 来进行表征,最后再**组合(combined)**到一起。这也就是第五种拍摄场景。\n\n\n\n相关论文:\n- [Zhang K , Riegler G , Snavely N , et al. NeRF++: Analyzing and Improving Neural Radiance Fields[J]. 2020.](http://vladlen.info/publications/nerf-analyzing-improving-neural-radiance-fields/)\n\nNeRF 实现的几个关键部分:\n\n1. 有一个**三维空间**,用一个连续的场表示;\n2. 空间里存在一个感兴趣的**物体区域**;\n3. 处于**不同位置和朝向**的相机拍摄多视角图像;\n4. 对于一张**图像**,根据相机中心和图像平面的一个像素点,两点确定一条**射线**穿过三维空间;\n5. 在射线上采样多个**离散的三维点**并利用体素渲染像素的颜色。\n\n这里面涉及到了三维空间、物体区域、相机位置和朝向、图像、射线、以及三维采样点等。要想优化 NeRF,我们需要能够表达刚刚提到的这些东西,这就涉及到了如何将拍摄的数据进行转化。\n\n关于如何正确转换相机参数和坐标系,可详见于{% post_link 相机参数与坐标系变换 %}。\n\n# 参考资料\n\n- [NeRF](https://www.matthewtancik.com/nerf)\n- [NeRF 论文](https://arxiv.org/abs/2003.08934)\n- [NeRF介绍](https://zhuanlan.zhihu.com/p/651081306)\n- [辐射神经场算法——NeRF算法详解](https://blog.csdn.net/weixin_44580210/article/details/122284120)\n- [NeRF神经辐射场学习笔记(一)——NeRF论文翻译以及原理解读](https://blog.csdn.net/weixin_44292547/article/details/126042398)\n- [NeRF原理解析](https://blog.csdn.net/weixin_46363611/article/details/125608625)\n- [反渲染(Inverse Rendering)三维重建及神经辐射场(NeRF)核心](https://zhuanlan.zhihu.com/p/628804009)\n- [A Surge in NeRF](https://yconquesty.github.io/blog/ml/nerf/)","tags":["CG"],"categories":["computer"]},{"title":"缓存算法","url":"/archives/47690/","content":"\n内存是有限的,不可能无限制地添加数据。假定我们设置缓存能够使用的内存大小为 N,那么在某一个时间点,添加了某一条缓存记录之后,占用内存超过了 N,这个时候就需要从缓存中移除一条或多条数据了。\n\n<!-- more -->\n\n# 常用缓存算法\n\n- 最不经常使用算法(Least Frequently Used, LFU):首先移除最低访问数的条目。\n - 关键:使用一个计数器来记录条目被访问的频率。\n - 缺点:不常用。若某个条目在历史上访问次数奇高,且在某个时间点之后长时间几乎不再被访问,则该条目迟迟不能被淘汰。\n- 最近最少使用算法(Least Recently Used. LRU):将最近使用的条目存放到靠近缓存顶部的位置。当一个新条目被访问时,LRU 将它放置到缓存的顶部。当缓存达到极限时,较早之前访问的条目将从缓存底部开始被移除。这里会使用到昂贵的算法,而且它需要记录“年龄位”来精确显示条目是何时被访问的。此外,当一个LRU缓存算法删除某个条目后,“年龄位”将随其他条目发生改变。\n- 自适应缓存替换算法(ARC):在 IBM Almaden 研究中心开发,这个缓存算法同时跟踪记录 LFU 和 LRU,以及驱逐缓存条目,来获得可用缓存的最佳使用。\n- 最近最常使用算法(Most Recently Used, MRU):首先移除最近最常使用的条目。\n - 优点:擅长处理一个条目越久,越容易被访问的情况。\n- 先进先出算法(First In First Out, FIFO):先进入缓存的先移除。\n - 关键:栈式结构。\n - 优点:实现简单。\n - 缺点:只能顺序写入和移除,无法指定特定的地址。\n\n# 算法实现\n\n","tags":["算法"],"categories":["computer"]},{"title":"矩阵计算","url":"/archives/61043/","content":"\n# 向量 (Vector)\n\n## 定义\n\n数域 $F$ 上的 $n$ 个数 $a_1,a_2,...,a_n$ 所组成的有序数组 $[a_1,a_2,...,a_n]$ 称为 $n$ 维向量,其中第 $i$ 个数称为该向量的第 $i$ 个分量. 通常用加粗小写字母 $\\mathbf{a}$,$\\mathbf{b}$ 表示.\n\n## 概念\n\n- 行向量:$\\mathbf{\\alpha}=(a_1,a_2,...,a_n)$\n- 列向量:$\\mathbf{\\beta}=\\left(\\begin{matrix}\n b_1 \\\\\n b_2 \\\\\n \\vdots \\\\\n b_n\n\\end{matrix}\\right)=(b_1,b_2,...,b_n)^T$\n\n## 向量的线性运算\n\n对向量 $\\mathbf{a}=(a_1,a_2,...,a_n)$ 和 $\\mathbf{b}=(b_1,b_2, b_n)$,有\n\n- 相等\n\n$$\n\\mathbf{a}=\\mathbf{b} \\Leftrightarrow a_i=b_i(i=1,2,...,n)\n$$\n\n- 加法\n\n$$\n\\mathbf{a}\\pm\\mathbf{b}=(a_1\\pm b_1,a_2\\pm b_2,...,a_n\\pm b_n)\n$$\n\n- 数乘\n\n$$\nk\\mathbf{a}=(ka_1,ka_2,...,ka_n)\n$$\n\n- 运算律\n\n1. $\\mathbf{a}+\\mathbf{b}=\\mathbf{b}+\\mathbf{a}$\n2. $(\\mathbf{a}+\\mathbf{b})+\\mathbf{c}=\\mathbf{a}+(\\mathbf{b}+\\mathbf{c})$\n3. $k(\\mathbf{a}+\\mathbf{b})=k\\mathbf{a}+k\\mathbf{b}$\n4. $(k+l)\\mathbf{a}=k\\mathbf{a}+l\\mathbf{a}$\n\n## 模与角度 (Norm and Angle)\n\n### 向量内积\n\n- 定义\n\n给定 $\\mathbb{R}^n$ 中的向量 $\\mathbf{a}=(a_1,a_2,...,a_n)^T, \\mathbf{b}=(b_1,b_2, b_n)^T$,称实数\n\n$$\n\\sum_{i=1}^na_ib_i=a_1b_1+a_2b_2+...+a_nb_n\n$$\n\n为 $\\mathbf{a}$ 和 $\\mathbf{b}$ 的内积,记为 $\\mathbf{a}^T\\mathbf{b}$ .\n\n- 运算性质\n\n设 $\\alpha, \\beta, \\gamma$ 为 $\\mathbb{R}^n$ 中的向量,$k$ 为实数:\n\n1. $\\alpha^T\\beta=\\beta^T\\alpha$\n2. $(k\\alpha)^T\\beta=k(\\alpha^T\\beta)=\\alpha^T(k\\beta)$\n3. $(\\alpha+\\beta)^T\\gamma=\\alpha^T\\gamma+\\beta^T\\gamma$\n4. $\\alpha^T\\alpha=0\\Leftrightarrow\\alpha=\\mathbf{0}$\n\n### 向量的模\n\n- 定义\n\n给定 $\\mathbb{R}^n$ 中的向量 $\\mathbf{a}=(a_1,a_2,...,a_n)^T, \\mathbf{b}=(b_1,b_2, b_n)^T$,称实数\n\n$$\n\\sqrt{\\mathbf{a}^T\\mathbf{a}}=\\sqrt{a_1^2+a_2^2+...+a_n^2}=\\sqrt{\\sum_{i=1}^na_i^2}\n$$\n\n为向量 $\\mathbf{a}$ 的长度,记作 $||\\mathbf{a}||$\n\n- 性质\n\n1. 非负性:$||\\mathbf{a}||\\geq 0$\n2. 齐次性:$||k\\mathbf{a}||=|k|\\cdot||\\mathbf{a}||$\n3. 柯西不等式:\n\n$$\n\\begin{align*}\n &\\left|\\sum_{i=1}^na_ib_i\\right|\\leq\\sqrt{\\sum_{i=1}^na_i^2}\\sqrt{\\sum_{i=1}^nb_i^2} \\\\\n \\Leftrightarrow \\quad& |\\mathbf{a}^T\\mathbf{b}|\\leq||\\mathbf{a}||\\cdot||\\mathbf{b}||\n\\end{align*}\n$$\n\n等号成立当且仅当 $\\mathbf{a}$ 和 $\\mathbf{b}$ 线性相关。\n\n4. 若向量 $\\mathbf{a}$ 的模为 $1$,则称 $\\mathbf{a}$ 为单位向量。设非零向量 $\\mathbf{a}$,令 $\\eta=\\frac{\\mathbf{a}}{||\\mathbf{a}||}$,则 $||\\eta||=1$,且方向与 $\\mathbf{a}$ 相同。\n\n### 向量夹角\n\n\n\n- 定义\n\n若 $x=(x_1,x_2)^T, y=(y_1,y_2)^T$,则\n\n$$\n\\begin{align}\n &L_x=||x||=\\sqrt{x_1^2+x_2^2},\\quad L_y=||y||=\\sqrt{y_1^2+y_2^2} \\\\\n &\\cos{\\theta_1}=\\frac{x_1}{||x||},\\quad \\cos{\\theta_2}=\\frac{y_1}{||y||} \\\\\n &\\sin{\\theta_1}=\\frac{x_2}{||x||},\\quad \\sin{\\theta_2}=\\frac{y_2}{||y||} \\\\\n \\cos{\\theta}&=\\cos{(\\theta_2-\\theta_1)}=\\cos{\\theta_2}\\cos{\\theta_1}+\\sin{\\theta_2}\\sin{\\theta_1}\\notag \\\\\n &=\\frac{x_1y_1+x_2y_2}{||x||\\cdot||y||}=\\frac{x^Ty}{||x||\\cdot||y||}\n\\end{align}\n$$\n\n### 向量正交(orthogonal)\n\n- 定义\n\n若两个 $n$ 维向量 $\\mathbf{a}$ 和 $\\mathbf{b}$ 满足 \n\n$$\n\\mathbf{a}^T\\mathbf{b}=a_1b_1+a_2b_2+...+a_nb_n=0\n$$\n\n则称 $\\mathbf{a}$ 和 $\\mathbf{b}$ 正交.\n\n- 性质\n\n1. 零向量与任何向量都正交\n2. 与自身正交的向量只有零向量\n\n- 正交向量组\n\n若非零向量组 $(\\mathbf{a}_1,\\mathbf{a}_2,...,\\mathbf{a}_n)$ 中,向量两两正交,即 $\\mathbf{a}_i^T\\mathbf{a}_j=0 (i\\neq j)$,则称该向量组为正交向量组.\n\n# 泰勒公式与线性化\n\n## 定义\n\n若函数 $f(x)$ 在 $x_0$ 处有 $n+1$ 阶导数,则存在 $x_0$ 的一个邻域,对于该邻域内任一 $x$,有\n\n$$\nf(x)=f(x_0)+f'(x_0)(x-x_0)+\\frac{f^{(2)}(x_0)}{2!}(x-x_0)^2+…+\\frac{f^{(n)}(x_0)}{n!}(x-x_0)^n+R_n(x)\n$$\n\n该式子称为函数 $f(x)$ 在 $x_0$ 的泰勒公式. 其中,$R_n(x)=o((x-x_0)^n)$ 称为拉格朗日余项,表示用 $n$ 次泰勒多项式来近似表示 $f(x)$ 所产生的误差. $R_n(x)$的具体表示为:\n\n$$\nR_n(x)=\\frac{f^{n+1}(\\xi)}{(n+1)!}(x-x_0)^{n+1}\\quad (\\xi \\in (x_0,x))\n$$\n\n当 $n=0$ 时,是拉格朗日中值公式:\n\n$$\nf(x)=f(x_0)+f'(\\xi)(x-x_0)\\quad (\\xi \\in (x_0,x))\n$$\n\n当 $x_0=0$ 时,是麦克劳林公式:\n\n$$\nf(x)=f(0)+f'(\\xi)x+\\frac{f^{(2)}(0)}{2!}x^2+…+\\frac{f^{(n)}(0)}{n!}x^n+R_n(x)\\quad (\\xi \\in (x_0,x))\n$$\n\n# Jacobian 矩阵\n\n## n 维欧氏空间\n\n### 定义\n\n所有 $n$ 个有序实数组 $(x_1,x_2,…,x_n)$ 的全体称为 $n$ 维向量空间,或简称 $n$ 维空间,其中每个有序实数组称为 $n$ 维空间中的一个向量(或一个点),记作\n\n$$\nx=(x_1,x_2,…,x_n)^T\n$$\n\n设 $x=(x_1,x_2,…,x_n)^T$ 与 $y=(y_1,y_2,…,y_n)^T$ 是 $n$ 维空间中的任意两个向量,则向量 $x$ 与 $y$ 的内积定义为:\n\n$$\nx^Ty=x_1y_1+x_2y_2+…+x_ny_n\n$$\n\n定义了内积的 $n$ 维空间叫做 $n$ 维欧几里得空间(简称 $n$ 维欧式空间),记作 $R^n$,利用内积定义向量 $x \\in R^n$ 的模为:\n\n$$\n|x|=\\sqrt{x^Tx}=\\sqrt{\\sum_{i=1}^nx_i^2}\n$$\n\n## 向量函数与实值函数\n\n- 实值函数\n \n即 $n$ 维空间中的点集到实数集的映射($R^n \\rightarrow R$\n\n- 向量函数\n\n即 $n$ 维欧氏空间中的点集到 $m$ 维欧氏空间点集的映射($R^n \\rightarrow R^m$) 一般地,当 $f_1,f_2,…,f_m$ 为 $f$ 的分量函数(或坐标函数)时,可写作\n\n$$\nf(x)=\\left(\\begin{array}{c}\nf_1 \\\\\n\\vdots \\\\\nf_m\n\\end{array}\\right)=\\left(\\begin{array}{c}\nf_1(x) \\\\\n\\vdots \\\\\nf_m(x)\n\\end{array}\\right)=\\left(\\begin{array}{c}\nf_1(x_1,x_2,…,x_n) \\\\\n\\vdots \\\\\nf_m(x_1,x_2,…,x_n)\n\\end{array}\\right)\n$$\n\n两个相同维数的向量函数 $f$ 和 $g$ 在相同的定义域上的和(差)函数为:\n\n$$\nf \\pm g =\\left(\\begin{array}{c}\nf_1 \\pm g_1 \\\\\n\\vdots \\\\\nf_n \\pm g_n\n\\end{array}\\right)\n$$\n\n一个实值函数 $\\alpha$ 向量函数 $f$ 在相同的定义域上的乘积函数为:\n\n$$\n\\alpha f =\\left(\\begin{array}{c}\n\\alpha f_1 \\\\\n\\vdots \\\\\n\\alpha f_n\n\\end{array}\\right)\n$$\n\n两个向量函数 $f$ 和 $h$ 的复合函数为:\n\n$$\nh \\cdot f =\\left(\\begin{array}{c}\nh_1 \\cdot f \\\\\n\\vdots \\\\\nh_n \\cdot f\n\\end{array}\\right)\n$$\n\n## 梯度\n\n### 分类\n\n根据自变量和因变量的不同可分为:\n\n1. 自变量为实向量的标量函数 $f$ 关于向量的梯度: 函数 $f$ 关于行向量 $x^T$ 的梯度为:\n\n $$\n \\frac{\\partial f}{\\partial x^T} = [\\frac{\\partial f}{\\partial x_1},...,\\frac{\\partial f}{\\partial x_n}] = \\nabla_{x^T}f(x)\n $$\n\n 函数 $f$ 关于列向量 $x$ 的梯度计算公式为:\n\n $$\n \\frac{\\partial f}{\\partial x} = [\\frac{\\partial f}{\\partial x_1},...,\\frac{\\partial f}{\\partial x_n}]^T = \\nabla_{x}f(x)\n $$\n\n 若 $f$ 为常数,则对应的梯度为 $\\mathbf{0}$.\n\n2. 自变量为实向量($x_{n \\times 1}$)的向量函数 $f$ 关于向量的梯度:\n\n $$\n f \\in R^m,\n \\nabla_xf = \\left[\\begin{matrix}\n \\frac{\\partial f_1}{\\partial x_1} & \\frac{\\partial f_2}{\\partial x_1} & \\cdots & \\frac{\\partial f_m}{\\partial x_1} \\\\\n \\frac{\\partial f_1}{\\partial x_2} & \\frac{\\partial f_2}{\\partial x_2} & \\cdots & \\frac{\\partial f_m}{\\partial x_2} \\\\\n \\vdots & \\vdots & \\ddots & \\vdots \\\\\n \\frac{\\partial f_1}{\\partial x_n} & \\frac{\\partial f_2}{\\partial x_n} & \\cdots & \\frac{\\partial f_m}{\\partial x_n}\n \\end{matrix}\\right]\n = \\frac{\\partial f}{\\partial x}\n $$\n\n3. 自变量为矩阵($x_{m \\times n}$)的标量函数 $f$ 关于矩阵的梯度:\n\n $$\n f \\in R,\n \\nabla_Xf = \\left[\\begin{matrix}\n \\frac{\\partial f_1}{\\partial x_{11}} & \\frac{\\partial f_2}{\\partial x_{12}} & \\cdots & \\frac{\\partial f_m}{\\partial x_{1n}} \\\\\n \\frac{\\partial f_1}{\\partial x_{21}} & \\frac{\\partial f_2}{\\partial x_{22}} & \\cdots & \\frac{\\partial f_m}{\\partial x_{2n}} \\\\\n \\vdots & \\vdots & \\ddots & \\vdots \\\\\n \\frac{\\partial f_1}{\\partial x_{m1}} & \\frac{\\partial f_2}{\\partial x_{m2}} & \\cdots & \\frac{\\partial f_m}{\\partial x_{mn}}\n \\end{matrix}\\right]\n = \\frac{\\partial f}{\\partial X}\n $$\n\n### 运算法则\n\n- 加法法则:若 $f(x),g(x)$ 均为 $x$ 的实值函数,则\n\n$$\n\\frac{\\partial[pf(x)+qg(x)]}{\\partial x}=p\\frac{\\partial f(x)}{\\partial x}+q\\frac{\\partial g(x)}{\\partial x}\n$$\n\n- 乘法法则:若 $f(x),g(x)$ 均为 $x$ 的实值函数,则\n\n$$\n\\frac{\\partial[f(x)g(x)]}{\\partial x}=f(x)\\frac{\\partial g(x)}{\\partial x}+g(x)\\frac{\\partial f(x)}{\\partial x}\n$$\n\n- 除法法则:若 $f(x)$ 为向量 $x$ 的向量值函数,则\n\n$$\n\\frac{\\partial[f(x)/g(x)]}{\\partial x}=\\frac{1}{g^2(x)}\\left[g(x)\\frac{\\partial f(x)}{\\partial x}-f(x)\\frac{\\partial g(x)}{\\partial x}\\right]\n$$\n\n- 链式法则:若$g(x)$为$x$的向量值函数,则\n\n$$\n\\frac{\\partial g(f(x))}{\\partial x}=\\frac{\\partial g(f)}{\\partial f}\\frac{\\partial f(x)}{\\partial x}\n$$\n\n- 常见类型的梯度\n$$\n\\begin{align*}\n \\nabla(x^Tx)=\\frac{\\partial x^Tx}{\\partial x} = 2x,\\quad \\nabla(a^Tx)=\\frac{\\partial a^Tx}{\\partial x} = a,\\\\\n \\nabla(x^TA)=\\frac{\\partial x^TA}{\\partial x} = A,\\quad \\nabla(x^TAx)=\\frac{\\partial x^TAx}{\\partial x} = (A+A^T)x\n\\end{align*}\n$$\n\n### Jacobian 矩阵\n\n若函数 $f(x):\\mathbb{R}^n \\rightarrow \\mathbb{R}^m$,有:\n\n$$\nx=[x_1,x_2,…,x_n]^T,\nf(x)=\\left[\\begin{array}{c}\nf_1(x_1,x_2,…,x_n) \\\\\nf_2(x_1,x_2,…,x_n) \\\\\n\\vdots \\\\\nf_m(x_1,x_2,…,x_n)\n\\end{array}\\right]\n$$\n\n则 Jacobian 矩阵 $J(x)$ 可以写为:\n\n$$\nf \\in \\mathbb{R}^{m \\times 1},\nJ(x) =\\left[\\begin{matrix}\n\\frac{\\partial f_1}{\\partial x_1} & \\frac{\\partial f_1}{\\partial x_2} & \\cdots & \\frac{\\partial f_1}{\\partial x_n} \\\\\n\\frac{\\partial f_2}{\\partial x_1} & \\frac{\\partial f_2}{\\partial x_2} & \\cdots & \\frac{\\partial f_2}{\\partial x_n} \\\\\n\\vdots & \\vdots & \\ddots & \\vdots \\\\\n\\frac{\\partial f_m}{\\partial x_1} & \\frac{\\partial f_m}{\\partial x_2} & \\cdots & \\frac{\\partial f_m}{\\partial x_n}\n\\end{matrix}\\right]\n= \\nabla_x^T f\n$$\n\nJacobian 矩阵表现了向量函数的最佳线性逼近,也即\n\n$$\nf(x) \\approx f(p) + J(p)(x-p)\n$$\n\n# Hessian 矩阵\n\n若 $f:\\mathbb{R}^n \\rightarrow \\mathbb{R}$ 为二次可导函数,$x=[x_1,x_2,…,x_n]^T$,则 $f$ 的Hessian矩阵为:\n\n$$\nH(f)=\\left[\\begin{matrix}\n\\frac{\\partial^2 f}{\\partial x_1 \\partial x_1} & \\frac{\\partial^2 f}{\\partial x_1 \\partial x_2} & \\cdots & \\frac{\\partial^2 f}{\\partial x_1 \\partial x_n} \\\\\n\\frac{\\partial^2 f}{\\partial x_2 \\partial x_1} & \\frac{\\partial^2 f}{\\partial x_2 \\partial x_2} & \\cdots & \\frac{\\partial^2 f}{\\partial x_2 \\partial x_n} \\\\\n\\vdots & \\vdots & \\ddots & \\vdots \\\\\n\\frac{\\partial^2 f}{\\partial x_n \\partial x_1} & \\frac{\\partial^2 f}{\\partial x_n \\partial x_2} & \\cdots & \\frac{\\partial^2 f}{\\partial x_n \\partial x_n}\n\\end{matrix}\\right]\n=\\nabla_x^2f\n$$\n\nHessian矩阵使用了函数的二阶信息,常用于解决优化问题。当 $f$ 的二阶混合偏导数连续时,它是一个对称矩阵。这时,$f$ 在 $x_0$ 的二阶泰勒公式可简单地写成:\n\n$$\nf(x)=f(x_0)+f'(x_0)(x-x_0)+\\frac{1}{2}(x-x_0)^Tf^2(x_0)(x-x_0)+o(|x-x_0|^2)\n$$\n\n# 贝叶斯决策理论和 HMM\n\n## 贝叶斯决策理论\n\n### 贝叶斯定理\n\n设 $n$ 个事件 $A_1,A_2,…,A_n$ 两两互斥,$A_1+A_2+…+A_n=\\Omega$(满足这两个条件的事件组称为一个完备事件组),且 $P(A_i)>0(i=1,2,…,n)$ 则\n\n$$\nP(A_i|B)=\\frac{P(A_i)P(B|A_i)}{\\sum_{j=1}^nP(A_i)P(B|A_j)}\n$$\n\n$P(A_i|B)$ 是在事件 $B$ 发生的条件下,某个原因 $A_i$ 发生的概率,称为后验概率,称 $P(A_i)$ 为先验概率.\n\n### MLE、MAP 和 Bayesian\n\n令:\n\n- $D={(x_1,y_1),…,(x_n,y_n)}$ 表示训练样本集\n- $\\theta$ 表示模型所有的参数\n- $x^*$ 表示一个新的样本(训练完的模型需要做预测),$\\hat y$ 表示模型对新样本的预测值\n\n#### 最大似然估计(Maximum likelihood estimation, MLE)\n\n- 在给定一个模型参数的情况下,计算样本集的概率 $p(D|\\theta)$\n- 目标是找到最优的参数 $\\theta^*$使得$p(D|\\theta)$ 最大化\n- 预测 $p(\\hat y|x^,\\theta^)$\n\n目标:\n\n$$\n\\theta^{MLE}=\\arg\\max\\theta p(D|\\theta)\n$$\n\n要找到最优参数 $\\theta^{MLE}$ 使得 $p(D|\\theta)$ 最大化,通常可通过令导数等于0等到:\n\n$$\n\\frac{\\partial p(D|\\theta)}{\\partial \\theta}=0\n$$\n\nMLE 未考虑到先验,且容易造成过拟合.\n\n#### 最大后验估计(Maximum a posteriori estimation,MAP)\n\n- 在给定样本的情况下,计算参数的概率 $p(\\theta|D)$ \n- 目标是找到最优的参数 $\\theta^*$ 使得 $p(\\theta|D)$ 最大化\n- 预测 $p(\\hat y|x^,\\theta^)$\n\n目标:\n\n$$\n\\theta^{MAP}=\\arg\\max\\theta p(\\theta|D)\n$$\n\n根据贝叶斯公式有:\n\n$$\np(\\theta|D)=\\frac{p(D|\\theta)p(\\theta)}{p(D)}\n$$\n\n目标可以转为:\n\n$$\n\\theta^{MAP}=\\arg\\max\\theta p(D|\\theta)p(\\theta)\n$$\n\n进一步取对数形式:\n\n$$\n\\theta^{MAP}=\\arg\\max\\theta \\log p(D|\\theta)+\\log p(\\theta)\n$$\n\nMAP 与 MLE 最大不同在于 $p(\\theta)$ 项,MAP 解决了 MLE 缺乏先验的缺点.\n\n#### 贝叶斯估计(Bayesian estimation)\n\n- 在给定样本的情况下,计算参数的分布 $p(\\theta|D)$\n- 预测 $p(\\hat y|x^*,D)=\\int_\\theta p(\\hat y|x^*,\\theta)p(\\theta|D)d\\theta$\n\n目标:\n\n对于一个新样本$x^*$,模型的预测为: \n\n$$ \np(\\hat y|x^*,D)=\\int_\\theta p(\\hat y|x^*,\\theta)p(\\theta|D)d\\theta\n$$\n\n需要计算后验概率 $p(\\theta|D)$:\n\n$$\np(\\theta|D)=\\frac{p(D|\\theta)p(\\theta)}{\\int p(D|\\theta)p(\\theta)d\\theta}\n$$\n\n后验概率需要近似计算.\n\n\n\n### 蒙特卡洛方法\n\n通过随机抽样方法,以随机事件出现的频率估计其概率,或者以抽样的数字特征估算随机变量的数字特征,并将其作为问题的解。\n\n## 隐马尔科夫模型(Hidden Markov Model, HMM)\n\n- 一种简单的有向图模型,常用于处理时间序列数据\n- 在多个领域应用广泛:语音识别、自然语言处理、词性标注等等\n\n### 变量\n\nHMM的变量分为两种:\n\n- 因变量 $y \\in Y$ :在$i$时刻系统所处的状态(通常离散型)\n- 观察变量 $x \\in X$ :在$i$时刻所处的观察值(离散、连续)\n\n\n\n### 状态\n\n下一个状态仅由当前状态决定,与其他状态变量无关.\n\n$x$ 在给定 $y$ 的情况下条件独立,HMM 中因变量和观察变量的联合分布可以写为\n\n$$\n\\begin{align*}\n p(\\textbf{x}^n,\\textbf{y}^n)=p(y_1)p(x_1|y_1)\\prod_{t=2}^Tp(x_t|y_t)p(y_t|y_{t-1})\\\\\n \\textbf{x}^n=(x_1,x_2,...,x_T),\\quad \\textbf{y}^n=(y_1,y_2,...,y_T)\n\\end{align*}\n$$\n","tags":["矩阵"],"categories":["math"]},{"title":"wsl杂项","url":"/archives/45330/","content":"\n# `Error code: Wsl/Service/0x8007273d`\n\n可能是电脑中存在的代理软件与 wsl 发生冲突导致的。\n\n- 临时解决方案\n \n 以管理员身份在 powershell 中运行:\n\n ```bash\n netsh winsock reset\n ```\n\n\n[wsl2更新后,报错0x8007273d解决方案](https://blog.csdn.net/weixin_44411509/article/details/131195308)\n\n[Winsock module breaks WSL2 ](https://github.com/microsoft/WSL/issues/4177)","tags":["配置"],"categories":["computer"]},{"title":"Linux杂项","url":"/archives/2776/","content":"\n本文用于记录使用 Linux 过程中遇到的问题。\n\n<!-- more -->\n\n# 删除 .nfs 文件\n\n1. 查找 .nfs 文件的 PID\n\n```bash\nlsof .nfs0000000009e000d700000012\n```\n\n2. 弹出大量信息,最后一行如下\n\n```\nCOMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n\ntar 32961 t3g 5r REG 0,57 8076 0023639 .nfs0000000007b000d700000049\n```\n\n3. 杀死占用的进程,从而删除 .nfs 文件\n\n```\nkill -9 32961\n```\n\n# Could not get lock /var/lib/dpkg/lock-frontend\n\n## 何时触发\n\n配置刚安装的系统的其他包的时候\n\n## 成因\n\n这个错误通常是因为已经有一个 apt 或 dpkg 进程在运行,导致无法获得锁定文件。\n\n## 解决方案\n\n1. 等待一段时间,可能是其他正在运行的 apt 或 dpkg 进程完成后自动释放锁。\n2. 检查是否有其他终端或软件包管理器正在使用 apt 或 dpkg 命令。如果是,请等待其完成或手动终止该进程。\n3. 如果没有其他 apt 或 dpkg 进程在运行,则可能是锁文件未正确释放。您可以手动删除锁文件,并重新运行 apt 或 dpkg 命令:\n\n ```bash\n sudo rm /var/lib/dpkg/lock-frontend \n sudo dpkg --configure -a\n ```\n\n4. 如果上述方法都没有解决问题,您可以尝试重启系统并再次运行 apt 或 dpkg 命令。\n\n## 参考资料\n- [Could not get lock /var/lib/dpkg/lock-frontend ,怎么处理](https://zhuanlan.zhihu.com/p/623220351?utm_id=0)","tags":["Linux"],"categories":["computer"]},{"title":"TTC测距碰撞算法","url":"/archives/27269/","content":"\n创新实践又忘记搞个定量指标了,要被老板痛批了,赶快赶一个。\n\n<!-- more -->\n\n\n\n\n\n\n\n\n\n\n# 参考资料\n\n- [一文读懂TTC碰撞时间算法](https://mp.weixin.qq.com/s/9Yq15GsSlM8i9rxsCZ22Mw)\n- [ADAS系统横纵向控制策略之碰撞时间计算方法](https://baijiahao.baidu.com/s?id=1782615956499586607&wfr=spider&for=pc)\n- [评估车辆之间安全距离的指标](https://blog.csdn.net/m0_66159157/article/details/129653950)\n- [TTC测距碰撞算法](https://zhuanlan.zhihu.com/p/112342540)\n- [Time to contact estimation using interest points](https://ieeexplore.ieee.org/document/5309851)","tags":["自动驾驶"],"categories":["computer"]},{"title":"俳句","url":"/archives/1937/","content":"\n- 露の世は 露の世ながら さりながら\n\n> 我知这世界本如露水般短暂。然而,然而\n\n- さびしさや 一尺消えて ゆくほたる\n\n> 流萤断续光,一明一灭一尺间,寂寞何以堪\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","tags":["日本"],"categories":["life"]},{"title":"如何提交pr","url":"/archives/16781/","content":"\n原文是大二时写的,但是找不到了,于是重新写了一篇。默认是有 git 经验的,没有的话可以看看这篇文章 {% post_link Git信息泄露 %}\n\n<!-- more -->\n\n# 流程\n\n- issue\n\n可以先提一个 issue 与原作者交流\n\n\n\n- Fork 仓库\n\nFork 需要提交 pr 的仓库到自己的仓库中。\n\n\n\n- git clone 到本地\n\ngit clone 的是刚刚 Fork 的仓库,不是原始项目的仓库。\n\n```bash\ngit clone https://github.com/Lynchrocket/hexo-theme-tranquility.git\n```\n\n- 创建一个分支\n\n```bash\ngit checkout -b add_mermaid\n```\n\n\n\n- 做出修改\n\n做出你需要的修改。\n\n- 提交修改\n\n由于分支是在本地创建的,git push 需要加上 --set-upstream\n\n```bash\ngit add .\ngit commit -m \"add mermaid\"\ngit push --set-upstream origin add_mermaid\n```\n\n\n\n- 创建 pull request\n\n在 Fork 的仓库中,点击 `Compare & pull request`\n\n\n\n选择你想并入的原项目分支,标题和描述信息。如果有对应的 issue,就通过键入 `#` 添加(Github 会自动展示 issues 列表)\n\n\n\n点击 `Create pull request`,就行了。结果如下:\n\n\n\n# 参考资料\n\n- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)\n- [如何在 Github 上规范的提交 PR(图文详解)](https://zhuanlan.zhihu.com/p/584834288)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","tags":["Git"],"categories":["computer"]},{"title":"深度学习:卷积神经网络(CNN)","url":"/archives/33779/","content":"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# 参考资料\n\n- [【DL笔记6】从此明白了卷积神经网络(CNN)](https://zhuanlan.zhihu.com/p/42559190)","tags":["深度学习"],"categories":["computer"]},{"title":"归约证明","url":"/archives/33031/","content":"\n归约(reduction)是指问题 A 的任何实例能用问题 B 的方法来解决(判断),并且 A 的解为 true,当且仅当 B 的解也是 true。此时可标为 $A\\leq_p B$。也就是说,归约可以将难以直接证明(或直接证明较为复杂)的问题转化为证明另一个较为简单问题。\n\n<!-- more -->\n\n# 密码学中的归约证明\n\n在现代密码学中要证明方案或协议的安全性,常常使用 game 的形式来构造证明过程。归约证明(proof by reduction)就是其中的核心。\n\n一般来说,一个密码学方案或协议 $\\Pi$ 是建立在已被证明安全的方案或协议,或者是某些困难问题之上(记为 $X$)。困难问题就相当于地基,密码学方案或协议就相当于高楼。只要地基是稳固的,高楼一般就安全了。因此我们希望下面的命题是为真的:\n\n> Prop.1(命题):若 $X$ 是安全的,那么方案 $\\Pi$ 就是安全的。若 $X$ 被有效攻破,那么方案 $\\Pi$ 就不再安全。\n\n实际上,这就是离散数学中假言命题的逻辑等价式:\n\n$$\np\\rightarrow q \\equiv \\neg q\\rightarrow\\neg p\n$$\n\n同时这个等价式也是**反证法**的基础。因此,若要在归约证明中证明某一个新提出的方案 X 的安全性:\n\n> Thm.(定理):Y 是安全的 $\\rightarrow$ X 是安全的\n\n也就是要证明:\n\n> Prop.2(命题):X 不是安全的 $\\rightarrow$ Y 不是安全的\n\n进一步说:**如果存在一个 PPT 的 attacker A 可以攻破 X,那么我们能构造一个 PPT 的 attacker B 来攻破 Y**。其中,X 是新提出的方案,Y 是某一个困难问题,PPT 是多项式概率时间。“构造”一词可以理解为模拟。\n\n如果能使条件命题 2 成立,那么就完成了归约证明。\n\n# 证明过程\n\n在密码协议的安全性证明中,一般使用 game 的方式来定义**什么是安全的/不安全的**。一个 game 中有两个 party,一个是 attacker,另一个是 challenger。attacker 试图破坏协议的安全,challenger 则是帮助构造协议的安全。\n\n假设已知问题 $X$ 难以解决,想要证明某构造方案 $\\Pi$ 安全,就要将成功攻破该方案 $\\Pi$ 的有效 attacker $\\mathcal{A}$ 转化为成功解决问题 $X$ 的有效 attacker $\\mathcal{A}'$,这与 $X$ 困难的假设相矛盾,所以方案 $\\Pi$ 无法被攻破,因此该方案是安全的。\n\n其中 $X$ 和 $\\Pi$ 分别有两个 party,即 $X$ 的 challenger 和 attacker 与 $\\Pi$ 的 challenger 和 attacker。我们还需要分别以两个 game 来定义 $X$ 和 $\\Pi$ 的安全性,且这两个 game 是互相独立的。在归约证明中,我们让 $X$ 的 attacker 来模拟 $\\Pi$ 的 challenger,且要求:\n\n1. $X$ 的 attacker 所模拟的 $\\Pi$ 的 challenger 够“逼真”,使得 $\\Pi$ 的 attacker 无法区分它的 challenger 是真实的还是模拟的,除了 negligible 的概率;\n2. 所模拟的 $\\Pi$ 的 challenger 是 PPT 的。\n\n\n\n如上图所示,证明的过程如下:\n\n1. 指定 PPT attacker $\\mathcal{A}$ 攻击方案 $\\Pi$,且成功的概率为 $\\varepsilon(n)$;\n2. 假设:问题 $X$ 难以解决,即无法在 PPT 内以 not negligible 的概率解决;\n3. 归约:构造 efficient attacker $\\mathcal{A}'$,它将 $\\mathcal{A}$ \n当作 subroutine 运行,从问题 $X$ 的实例 $x$ 模拟出一个 $\\Pi$ 的实例,输入给 $\\mathcal{A}$。如果 $\\mathcal{A}$ 被成功攻破,则 $\\mathcal{A}'$ 成功解决实例 $x$ 的概率至少为多项式的倒数 $1/p(n)$;\n1. 矛盾:那么 $\\mathcal{A}'$ 解决问题 $X$ 的概率为 $\\varepsilon(n)/p(n)$。若 $\\varepsilon(n)$ not negligible,则 $\\varepsilon(n)/p(n)$ 也 not negligible,这与 $X$ 困难的假设相矛盾。\n2. 结论:attacker $\\mathcal{A}$ 无法以 not negligible 的概率攻破方案\n,也即 $\\Pi$ 是 computationally secure 的。\n\n\n通俗的说,在 attacker $\\mathcal{A}'$ 接收到外部的 challenger 的输入数据后,对其进行适当变换,然后模拟为 challenger,将变换后的数据输入给 attacker $\\mathcal{A}$。然后 $\\mathcal{A}'$ 根据 $\\mathcal{A}$ 的输出,适当变换后输出给外部,完成这次的 challenge。可以看到,$\\mathcal{A}'$ 对 $X$ 的 game($X$ 的安全性)转移到 $\\mathcal{A}$ 对 $\\Pi$ 的 game($\\Pi$ 的安全性)上了。这样的转移即构成了命题中的 implies($\\rightarrow$)。\n\n# 证明例子\n\n## PRG\n\n伪随机数发生器(pseudorandom generator, PRG)输入长度为 $n$ 的种子 $s$\n,算法 $G$ 输出长度为 $l(n)$ 的字符串。若无法区分它和均匀随机选择的字符串,则称其满足伪随机性。用数学语言描述就是,对于 PPT 的 distinguisher $\\mathcal{D}$ 来说,存在 negligible 函数 $negl(n)$ 满足:\n\n$$\n|\\Pr[D(r)=1]-\\Pr[D(G(s))=1]|\\le negl(n)\n$$\n\n其中 $r$ 是从 $\\{0,1\\}^{l(n)}$ 中均与随机选择的,种子 $s$ 是从 $\\{0,1\\}^n$ 中均匀随机选择的。\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# 参考资料\n\n- [能否通俗地解释下密码学中的归约证明?](https://www.zhihu.com/question/49441102/answer/1737942968)\n- [密码学中的归约证明过程](https://panqiincs.me/2022/11/22/crypto-proof-method/)\n","tags":["数理逻辑","证明"],"categories":["math"]},{"title":"深度学习:Pytorch基本操作","url":"/archives/18730/","content":"\n# ndarray (tensor)\n\nN-dimensional array.\n\n\n\n\n\n# 数据操作\n\n## 访问元素\n\n\n\n## 广播机制 (broadcasting mechanism)\n\n在某些情况下,即使形状不同,我们仍然可以通过调用 *广播机制* 来执行按元素操作。这种机制的工作方式如下:\n\n1. 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;\n2. 对生成的数组执行按元素操作。\n\n\n\n后面有空再补","tags":["深度学习","Pytorch"],"categories":["computer"]},{"title":"ClimateNeRF环境配置","url":"/archives/40010/","content":"\n最近在学习这个,需要配置环境。总的来说挺好配的,但是有点问题,需要记录一下。\n\n<!-- more -->\n\n# 环境问题\n\n- 克隆仓库:\n```bash\ngit clone --recursive https://github.com/y-u-a-n-l-i/Climate_NeRF.git\n``` \n\n- 创建环境:\n```bash\nconda create -n climatenerf python=3.8 和 conda activate climatenerf\n```\n\n- 安装 torch 等: \n```bash\npip install torch==1.11.0 torchvision==0.12.0 --extra-index-url https://download.pytorch.org/whl/cu113\n```\n\n- 安装torch-scatter:\n```bash\npip install torch-scatter -f https://data.pyg.org/whl/torch-1.11.0+cu113.html\n```\n\n- 安装 cmake:\n```bash\nconda install cmake\n```\n\n- 安装 nvcc:\n - 需要指定版本 \n ```bash\n conda install -c \"nvidia/label/cuda-11.7.0\" cuda-nvcc\n ``` \n\n - 头文件:\n ```bash\n conda install -c conda-forge cudatoolkit-dev\n ```\n\n- 安装 tinycudann 的 PyTorch extension:需要注意要根据这个 \n\n <cardLink link=\"https://github.com/NVlabs/tiny-cuda-nn/issues/51\">Complie PyTorch Bindings with float32 precision</cardLink>\n \n 来安装 float32 precision 的 \n- 克隆 tinycudann:\n```bash\ngit clone --recursive https://github.com/nvlabs/tiny-cuda-nn\n```\n\n- `cd tiny-cuda-nn`\n- 这个地方由于服务器环境的原因 cmake 参数需要设置一下,同时在 gpu 节点上运行:\n```bash\ncmake . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -S . -G Ninja\ncmake --build build --config RelWithDebInfo -j\n``` \n\n- 安装 PyTorch extension:\n ```bash\n cd bindings/torch\n python setup.py install\n ```\n\n- 安装 mmsegmentation:[GET STARTED: INSTALL AND RUN MMSEG](https://mmsegmentation.readthedocs.io/en/latest/get_started.html):\n - Install MMCV using MIM\n ```bash\n pip install -U openmim\n mim install mmengine\n mim install \"mmcv>=2.0.0\"\n ```\n\n - Install MMSegmentation \n ```bash\n pip install \"mmsegmentation>=1.0.0\"\n ```\n\n - 下载 segmentation model `segformer_mit-b5_8xb1-160k_cityscapes-1024x1024` 到合适的文件夹中:\n ```bash\n mim download mmsegmentation --config segformer_mit-b5_8xb1-160k_cityscapes-1024x1024 --dest .\n ```\n\n- 安装 MTMT:\n ```bash\n pip3 install --no-build-isolation git+https://github.com/lucasb-eyer/pydensecrf.git\n ``` \n\n - 安装这个的时候出现了一些问题(如下图)。错误的原因是缺失 pydensecrf/eigen.cpp 这个文件。需要换用另一种方式下载 pydensecrf \n ```bash\n conda install -c conda-forge pydensecrf\n ```\n \n \n\n- 下载 MTMT 的 checkpoint:[Trained Model](https://github.com/eraserNut/MTMT?tab=readme-ov-file#trained-model)\n- 安装剩余的依赖:\n ```bash\n pip install -r requirements.txt\n ```\n\n - 在这其中安装 cupy 的时候出错了。使用命令 \n ```bash\n pip install cupy-cuda113\n ``` \n 单独安装(要根据自己的 cuda 版本来),然后删掉 requirements.txt 中的 cupy 这一项\n \n \n\n \n\n- 安装 cuda extension:\n ```bash\n pip install models/csrc\n ```\n \n 注意需要 `pip >= 22.1`(照着流程来的话 pip 版本应该不会错),并且该安装要在 gpu 节点上进行\n\n然后应该就可以运行了。跑个 training 试试看。\n\n# 训练问题\n\n1. ftfy 和 regex\n\n直接安装:`pip install ftfy` 和 `pip install regex`\n\n\n\n\n\n\n2. cuda 头文件(这个是在另一个服务器上发生的问题,也记录一下)\n\ntrain 跑起来后出现了这样的错误,错误的原因是找不到 cuda 的某个头文件。这是由于作者把头文件路径写死了,但是我们的服务器环境路径不一样。\n\n\n\n翻下 conda 环境,发现该头文件在这个路径:`/home/haoq_lab/cse30013594/miniconda3/envs/climatenerf/include/math_functions.h`\n\n修改 `./datasets/stylize_tools/smooth_filter.py` 文件\n\n\n\n\n\n然后就能正常跑了。\n\n# 参考资料\n\n- [Climate_NeRF](https://github.com/y-u-a-n-l-i/Climate_NeRF)\n- [丝滑安装pydensecrf库](https://zhuanlan.zhihu.com/p/657765799?utm_id=0)\n- [cupy 安装时提示报错 Exception: Your CUDA environment is invalid.](https://blog.csdn.net/xueying_2017/article/details/116045080)","tags":["配置","深度学习"],"categories":["computer"]},{"title":"Paxos算法","url":"/archives/1235/","content":"\n# 简介\n\nPaxos 算法是 Leslie Lamport 在 1990 年提出的一种分布式系统共识算法,也是第一个被证明完备的分布式系统共识算法(前提是不存在拜占庭将军问题,也即无恶意节点)。Paxos 算法主要包括 2 个部分:\n\n- Basic Paxos 算法:多节点之间如何就某个值(提案 value)达成共识;\n- Multi-Paxos 思想:执行多个 Basic Paxos 实例,就一系列值达成共识。\n\n# Basic Paxos Algorithm\n\nBasic Paxos 中存在 3 个重要的角色:\n\n1. Proposer(提议者):也可称作 Coordinator(协调者)。负责接收 client 端的请求并发起提案。提案信息通常包括提案编号(Proposal ID)和提议的值(Value)。\n2. Acceptor(接受者):也可称作 Voter(投票员)。负责对 Proposer 的提案进行投票,同时需要记住自己的投票历史。\n3. Learner(学习者):如果有超过半数的 Acceptor 对某个提议达成了共识,那么 Learner 就需要接受这个提议,并就该提议做运算,然后将运算结果返回 client。\n\n\n\n为了减少实现该算法所需的节点数,一个节点可以身兼多个角色。并且,一个提案被选定需要被半数以上的 Acceptor 接受。这样的话,Basic Paxos 算法还具备容错性,在少于一半的节点出现故障时,集群仍能正常工作。\n\n# Multi-Paxos thought\n\nBasic Paxos 算法的仅能就单个值达成共识,为了能够对一系列的值达成共识,我们需要用到 Multi-Paxos 思想。\n\n> Multi-Paxos 只是一种思想,这种思想的核心就是通过多个 Basic Paxos 实例就一系列值达成共识。也就是说,Basic Paxos 是 Multi-Paxos 思想的核心,Multi-Paxos 就是多执行几次 Basic Paxos。\n\n# 参考资料\n\n- [Paxos算法](https://zh.wikipedia.org/wiki/Paxos%E7%AE%97%E6%B3%95#)","tags":["分布式"],"categories":["computer"]},{"title":"共识算法","url":"/archives/2533/","content":"\n共识算法的作用是让分布式系统中的多个节点之间对某个提案(Proposal)达成一致的看法。提案的含义在分布式系统中十分宽泛,像哪一个节点是 Leader 节点、多个事件发生的顺序等等都可以是一个提案。\n\n针对没有恶意节点的情况,当前最常用的一些共识算法都是基于 **Paxos** 算法改进的,如 **Raft** 算法、**ZAB** 协议、**Fast Paxos** 算法。\n\n针对存在恶意节点的情况,一般使用的是**工作量证明(PoW,Proof-of-Work)**、**权益证明(PoS,Proof-of-Stake)**等共识算法。典型应用就是区块链。\n\n> 区块链系统中使用的共识算法需要解决的核心问题是**拜占庭将军问题**。\n\n<!-- more -->\n\n# 算法\n\n{% post_link Paxos算法 %}\n\n# 参考资料\n\n- [分布式系统中的一致性与共识算法](http://www.xuyasong.com/?p=1970#i)","tags":["分布式"],"categories":["computer"]},{"title":"CAP & BASE","url":"/archives/55509/","content":"\n# CAP\n\n## Definition\n\nCAP 也即 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)\n\n[]()\n\nCAP Theorem 指出,对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:\n\n- Consistency(一致性):所有节点的数据副本的内容相同;\n- Availability(可用性):非故障的节点在合理的时间内返回合理的响应;\n- Partition Tolerance(分区容错性):分布式系统中部分分区出现故障时(也即出现网络分区),整个系统仍然能够对外提供服务。\n\n> Network Partition(网络分区)\n分布式系统中,多个节点之间的网络本来是连通的,但是由于某些故障,某些节点之间不连通了,整个网络就分成了几块区域,这就是网络分区\n> \n\n## Not 2 in 3\n\n当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后,P 是前提,决定了 P 之后才有 C 和 A 的选择,即分区容错性(Partition tolerance)我们是必须要实现的。\n\n简而言之:**CAP 理论中 P 是一定要满足的,在此基础上,只能再满足 C 或 A。**\n\n因此,**分布式系统理论上不可能选择 CA 架构,只能选择 CP 或 AP 架构。**如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。\n\n**选择 CP 还是 AP 的关键在于当前的业务场景,没有定论。**\n\n> 为什么不可能选择 CA 架构?\n当系统出现网络分区后,某个节点在进行写操作,若为了保证 C,就必须禁止其他节点的读写操作,这就违背了 A;若为了保证 A,其他节点也能正常读写,就违背了 C。\n> \n\n**如果网络分区正常的话,也即无需保证 P 的时候,C 和 A 能同时保证。**因此,**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。**\n\n# BASE\n\n## Definition\n\nBASE 也即 Basically Available(基本可用)、Soft-state(软状态)和 Eventually Consistent(最终一致)。\n\nBASE 理论是对 CAP 中的 C 和 A 权衡的结果。\n\n## 核心思想\n\n即便无法做到强一致性,每个应用都可以根据自身特点以适当方式来时系统达到最终一致性。\n\n> 也就是说以牺牲数据的一致性来满足系统的可用性,当系统中的部分数据不可用或者不一致时,仍需要保持系统整体主要可用。\n> \n\nBASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 架构的补充。AP 架构是在系统发生分区的时候放弃 C,而不是永远放弃 C。在分区故障恢复后,系统应该达到最终一致性,这一点就是 BASE 理论延伸的地方。\n\n## BASE 三要素\n\n[]()\n\n### 基本可用\n\n分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等价于系统不可用。\n\n- 响应时间上的损失:正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3s。\n- 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。\n\n### 软状态\n\n允许系统中的数据存在中间状态(也即初先了数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,也即允许系统的不同节点的数据副本之间进行数据同步的过程中存在延时。\n\n### 最终一致性\n\n系统中所有的数据副本,在经过一段时间的同步后,最终能达到一个一致的状态。不需要实时保证数据的强一致性。\n\n> 分布式一致性的 3 个级别:\n1. 强一致性:能立刻读出写入的最新数据;\n2. 弱一致性:不一定可以读到最新写入的数据,也不保证多久之后能读到最新的数据,只是尽量保证某个时刻达到数据一致的状态;\n3. 最终一致性:系统保证在一定时间内达到数据一致的状态。\n> \n\n实现最终一致性的具体方式:\n\n- 读时修复:系统在读取数据时,检测数据的不一致,并修复。\n- 写时修复:系统在写入数据时,检测数据的不一致,并修复。\n- 异步修复:系统通过定时对账来检测副本数据的一致性,并修复。\n\n写时修复的性能消耗较低。\n\n# 总结\n\n**ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 架构的延伸。**","tags":["分布式"],"categories":["computer"]},{"title":"认证与授权","url":"/archives/48844/","content":"\n# 认证与授权\n\n## 认证(Authentication)\n\n用户认证,就是验证此用户的身份,解决的是“我是谁”的问题。也就是从用户请求信息中获取用户信息的过程。\n\n> 在登录时,输入用户名密码,系统判断用户名与密码是否在已有的用户内并给出结果反馈,这个过程就是用户认证。\n> \n> 用户名+密码登录,手机、邮箱验证码,第三方登录等都属于用户认证的一种。\n\n## 授权(Authorization)\n\n用户授权,就是授予用户权限,使其能够进行后续的某些访问和操作,解决的是“我能干哪些事”的问题。也就是从得到用户信息到授予用户权限的过程。\n\n> 每天乘电梯刷卡,刷卡的过程就是授权,授予了我们访问某某层的权限。就相当于是某个人或者某个机构赋予某人干某事的权限的过程。\n\n| 角度 | Authentication | Authorization |\n| --- | --- | --- |\n| 作用 | 确认用户的身份,以授予对系统的访问权限 | 授权用户以访问资源的权限 |\n| 过程 | 验证用户凭据以获得用户的访问权限 | 验证用户是否有权访问 |\n| 范围 | 判断用户是否伪称 | 判断用户可否访问内容 |\n| 时间 | 始终是第一步 | 授权在认证成功后完成 |\n\n# Cookie\n\n\n\n","tags":["软件开发","权限管理"],"categories":["computer"]},{"title":"URL去重策略(布隆过滤器)","url":"/archives/36029/","content":"\n在网络爬虫爬取数据时需要解决海量数据的存在性问题,也即判断给定数据是否存在。比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,上亿)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)、邮箱的垃圾邮件过滤(判断一个邮件地址是否在垃圾邮件列表中)、黑名单功能(判断一个IP地址或手机号码是否在黑名单中)等等。\n\n<!-- more -->\n\n# 有问题的去重策略\n\n| 策略 | 问题 |\n| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 在数据库中创建字段的 UNIQUE 属性 | 在多次数据库的 UNIQUE 报错之后,程序可能会直接崩溃 |\n| 在数据库中创建一个唯一的索引,在插入数据之前检查待插入的数据是否存在 | 如果在每一次插入数据之前都去检查待插入的数据是否存在,需要频繁地对数据库进行 exists 查询,当系统并发量很高时,数据库是很难扛住压力的,而且也势必会影响程序的效率 |\n| 使用 Redis 缓存 | 将海量的历史记录全部缓存起来,对存储空间的负担非常大 |\n| 使用 Set 或 Map 保存数据,确保唯一 | 容易 OOM,从待访问队列中解析出来的URL要远比它本身要多得多,程序占用的内存太大了 |\n\n# 布隆过滤器\n\n## 优缺点\n\nBloom Filter 是由 Bloom 在1970年提出的一种多哈希函数映射的快速查找算法。通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。\n\n- 优点:占用空间小,插入和查询的效率高,即以正确率换空间和时间。\n- 缺点:\n - 有一定的误判率,且添加到集合中的元素越多,误报的可能性就越大:\n - 当布隆过滤器说某个值存在时,这个值可能不存在\n - 当布隆过滤器说某个值不存在时,这个值一定不存在\n - 存放在布隆过滤器的数据不容易删除\n\n## 原理\n\n布隆过滤器实际就是一系列的 Hash 函数 + BitSet 实现的。\n\n### 初始化\n\n创建一个 m 位的 BitSet,先将所有位都初始化为 0,然后选择 k 个不同的哈希函数。第 i 个哈希函数作用于字符串 str 的结果记为 $H_i(str)$,且 $H_i(str)$ 的范围是 $[0, m-1]$。\n\n### 加入字符串\n\n对于字符串 str,分别计算 $H_1(str), H_2(str), ..., H_k(str)$,然后将 BitSet 的第 $H_1(str), H_2(str), ..., H_k(str)$ 位设为 1。\n\n/add_string.png)\n\n### 检查字符串\n\n对于字符串 str,分别计算 $H_1(str), H_2(str), ..., H_k(str)$,然后检查 BitSet 的第 $H_1(str), H_2(str), ..., H_k(str)$ 位是否为 1:\n\n- 若存在其中任何一位不为 1,则可以判定 str 一定没有被记录过;\n- 若全部位为 1,则可以“认为”该 str 已被记录过。\n\n实际上,即便是全部位都为 1,也不能完全确定该字符串被记录过,因为可能存在哈希碰撞。这种将字符串划分错误的情况,称为 false positive。\n\n### 删除字符串\n\n一般来说,字符串加入了布隆过滤器之后就被不能删除了,因为删除会影响到其他字符串。实在需要删除字符串的可以使用 Counting bloomfilter(CBF),这是一种基本 Bloom Filter 的变体,CBF 将基本 Bloom Filter 每一个 Bit 改为一个计数器,这样就可以实现删除字符串的功能了。\n\n## 哈希碰撞问题\n\n布隆过滤器误判的原因是出现了哈希碰撞,但实际上该概率非常小。假设上述使用的哈希函数中,$H_i$ 的碰撞概率为 $p_i$,则误判概率为 $\\prod_{i=1}^kp_i$。\n\n## 参数选择\n\n- Hash 函数\n \n 好的哈希函数要能近似等概率的将字符串映射到各个 bit。选择 k 个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入 k 个不同的参数。\n \n- BitSet 大小\n \n 哈希函数个数 k、位数组大小 m、加入的字符串数量 n 的关系可以参考 [Bloom Filters - the math](http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html)。该文献证明了对于给定的m、n,当 $k = ln(2) \\cdot m/n$ 时出错的概率是最小的。同时该文献还给出了特定的 k,m,n 的出错概率。例如:取 k=10,设 m = 20n 时,false positive 发生的概率是 0.0000889 ,这个概率基本能满足网络爬虫的需求了。\n\n## 布隆过滤器的应用\n\n1. 网页爬虫对 URL 去重,避免爬取相同的 URL 地址;\n2. 邮箱系统的从数十亿的垃圾邮件过滤一些垃圾邮件和黑名单查询\n3. 集合重复元素的判别\n4. Google Chrome 使用布隆过滤器识别恶意 URL;\n5. 查询加速(比如基于key-value的存储系统)\n6. 新闻推荐系统,使用布隆过滤器避免推荐给用户已经读过的文章;\n7. **redis缓存穿透问题的解决**\n\n所谓的缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。\n\n所以先将需要查询的数据存入布隆过滤器,如果布隆过滤器不存在则直接返回;如果布隆过滤器存在则再从redis查询(此时只会有少数误差数据);如果redis中还不存在则查询数据库(此时的访问很小了),并在查询数据库可以通过并发加锁处理,保证只有一个线程可以查询该数据并写入缓存,从而避免了缓存穿透的问题。\n\n## Java 实现\n\n```java\nimport java.util.BitSet;\n\npublic class MyBloomFilter {\n /**\n * 位数组的大小\n */\n private static final int DEFAULT_SIZE = 2 << 24;\n /**\n * 通过这个数组可以创建 6 个不同的哈希函数\n */\n private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};\n /**\n * 位数组。数组中的元素只能是 0 或者 1\n */\n private final BitSet bits = new BitSet(DEFAULT_SIZE);\n /**\n * 存放包含 hash 函数的类的数组\n */\n private final SimpleHash[] func = new SimpleHash[SEEDS.length];\n\n /**\n * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样\n */\n public MyBloomFilter() {\n // 初始化多个不同的 Hash 函数\n for (int i = 0; i < SEEDS.length; i++) {\n func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);\n }\n }\n\n /**\n * 添加元素到位数组\n */\n public void add(Object value) {\n for (SimpleHash f : func) {\n bits.set(f.hash(value), true);\n }\n }\n\n /**\n * 判断指定元素是否存在于位数组\n */\n public boolean contains(Object value) {\n boolean ret = true;\n for (SimpleHash f : func) {\n ret = ret && bits.get(f.hash(value));\n }\n return ret;\n }\n\n /**\n * 静态内部类。用于 hash 操作\n */\n public static class SimpleHash {\n private final int cap;\n private final int seed;\n\n public SimpleHash(int cap, int seed) {\n this.cap = cap;\n this.seed = seed;\n }\n\n /**\n * 计算 hash 值\n */\n public int hash(Object value) {\n int h;\n return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));\n }\n }\n}\n```\n\n# 参考资料\n\n- [Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter)\n- [网络爬虫:URL去重策略之布隆过滤器(BloomFilter)的使用](https://blog.csdn.net/lemon_tree12138/article/details/47973715)\n- [BloomFilter——大规模数据处理利器](https://www.cnblogs.com/heaad/archive/2011/01/02/1924195.html)\n- [亿级数据过滤,优选布隆过滤器](https://mp.weixin.qq.com/s/711MELnmRwWKSd3NopjF-Q)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","tags":["软件开发","数据结构"],"categories":["computer"]},{"title":"MQTT","url":"/archives/33401/","content":"\n`MQTT` (Message Queue Telemetry Transport,消息队列遥测传输协议),是一种基于发布-订阅(`publish-subscribe`)模式的`轻量级`通讯协议,该协议构建于 `TCP/IP` 协议上。\n\n<!-- more -->\n\nMQTT 协议的主要特征是开放、简单、轻量级和易于实现,这些特征使得它适用于受约束的应用环境,如:\n\n> `网络受限`:网络带宽较低且传输不可靠\n> \n> `终端受限`:协议运行在嵌入式设备上,嵌入式终端的处理器、内存等是受限的\n\n# MQTT 协议概要\n\n## 发布-订阅模式\n\n发布-订阅模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。\n\n该协议将消息的发布者(`publisher`)与订阅者(`subscriber`)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似。\n\n\n\n发布-订阅模式最重要的方面是消息的发布者与订阅者的解耦。这种解耦有几个维度:\n\n- 空间解耦:发布者和订阅者不需要相互了解(例如,不交换 IP 地址和端口)。\n- 时间解耦:发布者和订阅者不需要同时运行。\n- 同步解耦:两个组件的操作在发布或接收时不需要中断。\n\n总之,发布-订阅模式消除了传统客户端-服务器之间的直接通信,把通信这个操作交给了 broker 进行代理,并在空间、时间、同步三个维度上进行了解藕。\n\n发布-订阅模式比传统的客户端-服务器模式有了更好的扩展性:\n\n- broker 高度并行化,基于事件驱动;\n- 消息的缓存和消息的智能路由;\n- 可以通过集群代理来实现数百万的连接,使用负载均衡器将负载分配到更多的单个服务器上。\n\n## 消息过滤\n\n很明显,broker 在发布-订阅过程中起着举足轻重的作用。但是代理如何过滤所有消息,以便每个订阅者只接收感兴趣的消息?broker 有几个可以过滤的选项:\n\n- 基于主题的过滤:\n \n 此过滤基于属于每条消息的主题。接收客户端向代理订阅感兴趣的主题,订阅后,broker 就会确保客户端收到发布到 topic 中的消息。\n \n- 基于内容的过滤:\n \n 在基于内容的过滤中,broker 会根据特定的内容过滤消息,接受客户端会经过过滤他们感兴趣的内容。这种方法的一个显著的缺点就是必须事先知道消息的内容,不能加密或者轻易修改。\n \n- 基于类型的过滤:\n \n 当使用面向对象的语言时,基于消息(事件)的类型/类进行过滤是一种常见做法。例如,订阅者可以收听所有类型为 Exception 或任何子类型的消息。\n\n## MQTT 与消息队列的区别\n\n- 消息队列存储消息直到消息被消费:\n \n 使用消息队列时,每条传入消息都存储在队列中,直到被客户端(通常称为消费者)接收。如果没有客户端接收到消息,消息将保持在队列中并等待被消费。在消息队列中,不会存在消息没有客户端消费的情况,但是在 MQTT 中,却存在 topic 无 subscriber 订阅的情况。\n \n- 一条消息只被一个客户端消费:\n \n 在传统的消息队列中,一条消息只能被一个消费者处理。负载分布在队列的所有消费者之间。在 MQTT 中,行为完全相反:订阅主题的每个订阅者都会收到消息,每个订阅者有相同的负载。\n \n- 队列是命名的,必须显式创建:\n \n 队列比主题严格得多。在使用队列之前,必须使用单独的命令显式创建队列。只有在队列命名和创建之后,才可以发布或消费消息。相比之下,MQTT 主题非常灵活,可以即时创建。\n\n# MQTT 重要概念\n\n## MQTT Client\n\n`publisher` 和 `subscriber` 都属于 MQTT Client。发布者和订阅者是一种相对的概念,指的是当前客户端是在发布消息还是在接收消息。发布和订阅的功能也可以由同一个 MQTT Client 实现。\n\n## MQTT Broker\n\n与 MQTT Client 对应的就是 MQTT Broker。 `Broker` 是任何发布-订阅协议的核心,根据实现的不同,代理可以处理多达数百万连接的 MQTT Client。\n\nBroker 的责任:\n\n- 接收所有消息,过滤消息,确定是哪个 Client 订阅了消息,并将消息发送给对应的 Client;\n- 保存会话数据,这些数据包括订阅的和错过的消息;\n- 客户端的身份验证和授权。\n\n## MQTT Connection\n\n`TCP` 协议位于传输层,`MQTT` 协议位于应用层,`MQTT` 协议构建于 `TCP/IP` 协议上。客户端和代理都需要有一个 TCP/IP 协议支持。\n\n\n\nMQTT 连接始终位于一个客户端和代理之间。客户端从不直接相互连接。要发起连接,客户端向代理发送 `CONNECT` 消息。代理使用 `CONNACK` 消息和状态代码进行响应。建立连接后,代理将保持打开状态,直到客户端发送断开连接命令或连接中断。\n\n\n\n# 消息列表\n\n## CONNECT\n\n为了创建连接,客户端向代理发送此命令消息。如果此 CONNECT 消息格式错误或打开网络套接字和发送连接消息之间的时间过长,代理将关闭连接。\n\n一个 MQTT 客户端发送一条 CONNECT 连接,这条 CONNECT 连接可能会包含下面这些信息:\n\n\n\n- `clientId`:\n \n ClientId 的长度可以是 1-23 个字符,在一个服务器上 ClientId 不能重复。如果超过 23 个字符,则服务器返回 CONNACK 消息中的返回码为 Identifier Rejected。在 MQTT 3.1.1 中,如果不需要代理持有状态,可以发送一个空的 ClientId。空的 ClientId 导致连接没有任何状态。在这种情况下,clean session 标志必须设置为 true,否则代理将拒绝连接。\n \n- `cleanSession`:\n \n Clean Session 标志告诉代理客户端是否要建立持久会话。在持久会话 (CleanSession = false) 中,代理存储客户端的所有订阅以及以服务质量(QoS)级别 1 或 2 订阅的客户端的所有丢失消息。 如果会话不是持久的 (CleanSession = true ),代理不为客户端存储任何内容,并清除任何先前持久会话中的所有信息。\n \n- `username/password`:\n \n MQTT 可以发送用户名和密码进行客户端认证和授权。但是,如果此信息未加密或散列,则密码将以纯文本形式发送。我们强烈建议将用户名和密码与安全传输一起使用。像 HiveMQ 这样的代理可以使用 SSL 证书对客户端进行身份验证,因此不需要用户名和密码。\n \n- `willMessage`:\n \n LastWillxxx 表示的是遗愿,client 在连接 broker 的时候将会设立一个遗愿,这个遗愿会保存在 broker 中,当 client 因为非正常原因断开与 broker 的连接时,broker 会将遗愿发送给订阅了这个 topic(订阅遗愿的 topic)的 client。\n \n- `keepAlive`:\n \n keepAlive 是 client 在连接建立时与 broker 通信的时间间隔,通常以秒为单位。这个时间指的是 client 与 broker 在不发送消息下所能承受的最大时长。\n\n## CONNACK\n\n当 broker 收到 CONNECT 消息时,它有义务回复 CONNACK 消息进行响应。CONNACK 消息包括两部分内容:\n\n\n\n- `sessionPresent`:\n \n 会话当前标志,这个标志会告诉 client 当前 broker 是否有一个持久性会话与 client 进行交互。SessionPresent 标志和 CleanSession 标志有关,当 client 在 CleanSession 设置为 true 的情况下连接时,SessionPresent 始终为 false,因为没有持久性会话可以使用。如果 CleanSession 设置为 false,则有两种可能性:\n \n - 如果 ClientId 的会话信息可用,并且 broker 已经存储了会话信息,那么 SessionPresent 为 true;\n - 否则如果没有 ClientId 的任何会话信息,那么 SessionPresent 为 false。\n\n \n\n- `returnCode`:\n \n 连接返回码,告诉客户端连接尝试是否成功。连接确认标志有下面这些选项:\n\n \n\n## PUBLISH\n\nMQTT 客户端可以在连接到 broker 后立即发布消息。MQTT 使用的是基于 topic 主题的过滤。每条消息都必须包含一个主题,broker 可以使用该主题将消息转发给感兴趣的客户端。通常,每条消息都有一个负载(Payload),其中包含要以字节格式传输的数据。MQTT 是数据无关性的,也就是说数据是由发布者决定要发送的是 XML 、JSON 还是二进制数据、文本数据。\n\n\n\n- `packetId`:\n \n 这个 packetId 标识在 client 和 broker 之间唯一的消息标识。packetId 仅与大于零的 QoS 级别相关。\n \n- `topicName`:\n \n 主题名称是一个简单的字符串,它以正斜杠作为分隔符进行分层结构。例如,“我的家/客厅/温度”或“德国/慕尼黑/十月节/人”。\n \n- `qos`:\n \n 此数字表示消息的服务质量 (QoS)。有三个级别:0、1 和 2。服务级别决定了消息到达预期接收者(客户端或代理)的保证类型。\n \n- `retainFlag`:\n \n 此标志表示 broker 将最近收到的一条 RETAIN 标志位为 true 的消息保存在服务器端(内存或者文件)。\n \n- `payload`:\n \n 这个是每条消息的实际内容。MQTT 是数据无关性的。可以发送任何文本、图像、加密数据以及二进制数据。\n \n- `dupFlag`:\n \n 该标志表明该消息是重复的并且由于预期的接收者(客户端或代理)没有确认原始消息而被重新发送。仅与 QoS 大于 0 的级别相关。\n \n\n当客户端向 MQTT broker 发送消息进行发布时,broker 读取消息、确认消息(根据 QoS 级别)并处理消息。broker 的处理包括确定哪些客户端订阅了主题并将消息发送给他们。\n\n\n\n最初发布消息的客户端只关心将 PUBLISH 消息传递给 broker。一旦 broker 收到 PUBLISH 消息,broker 就有责任将消息传递给所有订阅者。发布客户端不会得到关于是否有人对发布的消息感兴趣或有多少客户端从 broker 收到消息的任何反馈。\n\n## SUBSCRIBE\n\nclient 会向 broker 发送 SUBSCRIBE 消息来接收有关感兴趣的 topic。\n\n\n\n- `packetId`:\n \n 消息的唯一标识符。与 PUBLISH 消息的 packetId 一样。\n \n- `List of Subscriptions`:\n \n 订阅列表。一个 SUBSCRIBE 消息可以包含一个客户端的多个订阅。每个订阅由一个主题和一个 QoS 级别组成。订阅消息中的主题可以包含通配符,使订阅主题模式而不是特定主题成为可能。如果一个客户端存在重叠订阅,则代理会传送该主题具有最高 QoS 级别的消息。\n\n## SUBACK\n\n为了确认每个订阅,broker 向客户端发送一个 SUBACK 确认消息。\n\n\n\n- `packetId`:\n \n 消息的唯一标识符。与 SUBSCRIBE 消息的一样。\n \n- `returnCode`:\n \n broker 为它在 SUBSCRIBE 消息中收到的每个主题/QoS 对发送一个返回代码。例如,如果 SUBSCRIBE 消息有五个订阅,则 SUBACK 消息包含五个返回码。返回码确认每个主题并显示 broker 授予的 QoS 级别。如果 broker 拒绝订阅,则 SUBACK 消息包含该特定主题的失败返回代码。例如,如果客户端没有足够的权限订阅主题或主题格式错误。\n\n \n\n客户端成功发送 SUBSCRIBE 消息并收到 SUBACK 消息后,它会获取与 SUBSCRIBE 消息包含的订阅中的主题匹配的每条已发布消息。\n\n\n\n## UNSUBSCRIBE\n\nSUBSCRIBE 消息的对应是 UNSUBSCRIBE 消息。此消息删除 broker 上客户端的现有订阅。\n\n\n\n- `packetId`:\n \n 消息的唯一标识符。与 PUBLISH 消息的 packetId 一样。\n \n- `List of Topics`:\n \n 主题列表。需要取消订阅的主题。\n \n\n## UNSUBACK\n\n为了确认取消订阅,broker 向客户端发送一个 UNSUBACK 确认消息。\n\n\n\n- `packetId`:\n \n 消息的唯一标识符。与 UNSUBSCRIBE 消息的 packetId 一样。\n \n客户端收到来自 broker 的 UNSUBACK 后,可以认为 UNSUBSCRIBE 消息中的订阅被删除了。\n\n\n\n# 主题(Topic)\n\n在 MQTT 中,主题指的是 broker 用于为每个连接的客户端过滤消息的 UTF-8 字符串。主题由一个或多个主题级别组成。每个主题级别由正斜杠(主题级别分隔符)分隔。\n\n\n\n与消息队列相比,MQTT 主题非常轻量级。客户端在发布或订阅它之前不需要创建所需的主题。broker 接受每个有效主题而无需任何事先初始化。\n\n## 通配符\n\n当客户端订阅主题时,它可以订阅已发布消息的确切主题,也可以使用通配符同时订阅多个主题。通配符只能用于订阅主题,不能用于发布消息。有两种不同类型的通配符:单级和多级。\n\n- 单级:+\n \n 单级通配符只替换一个主题级别。\n\n \n\n 上述的主题匹配可以产生以下结果\n\n \n\n- 多级:#\n \n 多级通配符涵盖多个主题级别。多级通配符必须作为主题中的最后一个字符放置,并以正斜杠开头。\n\n \n\n 上述的主题匹配可以产生以下结果\n\n \n\n## 以 $ 开头的主题\n\n$ 主题保留用于 MQTT 代理的内部统计信息。客户端无法向这些主题发布消息。目前,此类主题尚无官方标准化,代理实现各不相同,但通常用 `$SYS/` 来表示这类信息。[MQTT GitHub wiki](https://github.com/mqtt/mqtt.org/wiki/SYS-Topics) 中提供了对 `$SYS-topics` 的一项建议。\n\n> $SYS/broker/clients/connected\n> \n> $SYS/broker/clients/disconnected\n> \n> $SYS/broker/clients/total\n> \n> $SYS/broker/messages/sent\n> \n> $SYS/broker/uptime\n\n# 参考资料\n\n- [一文读懂物联网 MQTT 协议之基础特性篇](https://blog.csdn.net/riemann_/article/details/118686072)\n\n\n\n\n\n\n\n\n\n\n","tags":["软件开发"],"categories":["computer"]},{"title":"二分查找","url":"/archives/42877/","content":"\n前提:有序数组\n\n边界条件:根据寻找区间的定义有所不同\n\n1. 寻找区间为 [left, right]\n * 循环用 while(left ≤ right),因为当 left == right 时, [left, right] 有意义;\n * 更新用 right = mid - 1 和 left = mid + 1。\n ```java\n public int BinarySearch(int[] nums, int target) {\n int n = nums.length;\n int left = 0, right = n - 1;\n while (left <= right) {\n int mid = left + (right - left) / 2;\n if (nums[mid] > target) {\n right = mid - 1;\n } else if (nums[mid] < target) {\n left = mid + 1;\n } else {\n return mid;\n }\n }\n return -1;\n }\n ```\n\n1. 寻找区间为 [left, right)\n * 循环用 while(left < right),因为当 left == right 时, [left, right) 无意义;\n * 更新用 right = mid 和 left = mid + 1。\n ```java\n public int BinarySearch(int[] nums, int target) {\n int n = nums.length;\n int left = 0, right = n;\n while (left < right) {\n int mid = left + (right - left) / 2;\n if (nums[mid] > target) {\n right = mid;\n } else if (nums[mid] < target) {\n left = mid + 1;\n } else {\n return mid;\n }\n }\n return -1;\n }\n ```\n","tags":["算法"],"categories":["computer"]},{"title":"Maven","url":"/archives/38008/","content":"\nMaven 作为一个构建工具,不仅能自动化构建项目,还能够抽象构建过程,提供构建任务实现;它跨平台,对外提供了一致的操作接口,这一切足以使它成为优秀的、流行的构建工具。Maven 不仅是构建工具,还是一个依赖管理工具和项目管理工具。它提供了中央仓库,且能按照配置自动下载构件。\n\n<!-- more -->\n\n# Maven 坐标\n\n项目中依赖的第三方库及插件可统称为构件(artifact),每个 artifact 都可用 Maven 坐标唯一标识。坐标元素包括:\n\n- 必须元素\n - **groupId**:定义了当前 Maven 项目隶属的组织或公司。groupId 一般分为多段,通常情况下,第一段为域,第二段为公司名称。\n - **artifactId**:定义了当前 Maven 项目的名称,项目的唯一的标识符,对应项目根目录的名称。\n - **version**:定义了 Maven 项目当前所处版本。\n- 可选元素\n - **packaging**(可选):定义了 Maven 项目的打包方式(如jar,war,……),默认为 jar\n - **classifier**(可选):常用于区分从同一 POM 构建的具有不同内容的构件,可以是任意的字符串,附加在版本号之后。\n\n引入依赖,一般来说只需要指定其中三个坐标元素即可。\n\n```xml\n<dependency>\n <groupId>com.alibaba</groupId>\n <artifactId>easyexcel</artifactId>\n <version>3.1.1</version>\n</dependency>\n```\n\n# Maven 依赖\n\n## 依赖配置\n\nMaven 依赖配置均在项目的 pom.xml 下。\n\n### 配置信息示例\n\n```xml\n<project>\n <dependencies>\n <dependency>\n <groupId></groupId>\n <artifactId></artifactId>\n <version></version>\n <type>...</type>\n <scope>...</scope>\n <optional>...</optional>\n <exclusions>\n <exclusion>\n <groupId>...</groupId>\n <artifactId>...</artifactId>\n </exclusion>\n </exclusions>\n </dependency>\n </dependencies>\n</project>\n```\n\n### 配置说明\n\n- **dependencies**:一个 pom.xml 文件中只能存在一个这样的标签,是用来管理依赖的总标签。\n- **dependency**:包含在 dependencies 标签中,可有多个,每个表示项目的一个依赖。\n- **groupId**,**artifactId**,**version**:依赖的基本坐标。\n- 可选配置\n - **type**:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下使用默认值 jar 即可。\n - **scope**:依赖的范围,默认值为 compile。\n - **optional**:标记依赖是否可选。\n - **exclusions**:用于排除传递性依赖,避免 jar 包冲突。\n\n## 依赖范围\n\n**classpath** 用于指定 `.class` 文件存放的位置,类加载器会从该路径中加载所需的 `.class` 文件到内存中。\n\nMaven 在编译、执行测试、实际运行有着三套不同的 classpath:\n\n- **编译 classpath**:编译主代码有效\n- **测试 classpath**:编译、运行测试代码有效\n- **运行 classpath**:项目运行时有效\n\nMaven 的依赖范围如下:\n\n- **compile**(默认):编译依赖范围。使用该依赖范围对于编译、测试、运行三种都有效,即在编译、测试、运行的时候都要使用该依赖。\n- **test**:测试依赖范围。表明该依赖只能用于测试。\n- **provided**:对编译、测试有效,对运行无效。\n- **runtime**:运行时依赖范围。对测试和运行有效,但是在编译主代码时无效。\n- **system**:系统依赖范围。使用有该范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径,而不依赖 Maven 仓库解析,所以可能会造成构建的不可移植。\n\n## 传递依赖\n\n### 依赖冲突\n\n1. 同一个 `groupId` 和同一个 `artifactId` 下,只能使用一个 `version`。若有不同,则只会引入后一个声明的依赖。\n\n```xml\n<dependency>\n <groupId>in.hocg.boot</groupId>\n <artifactId>mybatis-plus-spring-boot-starter</artifactId>\n <version>1.0.48</version>\n</dependency>\n<!-- 只会使用 1.0.49 这个版本的依赖 -->\n<dependency>\n <groupId>in.hocg.boot</groupId>\n <artifactId>mybatis-plus-spring-boot-starter</artifactId>\n <version>1.0.49</version>\n</dependency>\n```\n\n2. 项目的两个依赖同时引入了某个依赖\n\n 即对于项目 A,存在下面的依赖关系:\n\n```mermaid\ngraph LR\n A --> B --> C --> X_2.0\n A --> D --> X_1.0\n```\n\n依赖路径上有两个版本的 X,为了避免依赖重复,Maven 只会选择其中的一个进行解析。\n\n#### Maven 依赖调解\n\n在遇到上述问题的时候,Maven 会遵循 **路径最短优先** 和 **声明顺序优先** 两大原则来解决,解决的过程被称作 **Maven 依赖调解**。\n\n- **路径最短优先**\n \n 根据上述的依赖路径,X 2.0 会被解析使用。这是因为第二条路径长度(2)小于第一条(3)。\n \n 但是该原则也并非通用的,若遇到路径长度相等的情况,就需要使用声明顺序优先原则了。\n\n```mermaid\ngraph LR\n A --> B --> Y_2.0\n A --> D --> Y_1.0\n```\n\n- **声明顺序优先**\n \n 在依赖路径长度相等的前提下,在 `pom.xml` 中依赖声明的顺序决定了谁会被解析使用,顺序最前的那个依赖优胜。在上述例子中,若 B 的依赖声明在 D 之前,那么 Y 1.0 就会被解析使用。\n\n```xml\n<!-- A pom.xml -->\n<dependencies>\n ...\n dependency B\n ...\n dependency D\n\t\t...\n</dependencies>\n```\n\n### 排除依赖\n\n有时侯需要我们手动排除依赖来解决依赖冲突问题。\n\n同样的例子,根据路径最短优先原则,X 1.0 会被解析使用。但是如果依赖 C 用到了 X 2.0 中才有的类或方法时,运行项目就会报 `NoClassDefFoundError` 或 `NoSuchMethodError` 错误。\n\n```mermaid\ngraph LR\n A --> B --> C --> X_2.0\n A --> D --> X_1.0\n```\n\n要解决该问题,需要在引入依赖时通过 `exclusion` 标签手动将 X 1.0 排除。\n\n```xml\n<dependency>\n ......\n <exclusions>\n <exclusion>\n <artifactId>x</artifactId>\n <groupId>group.x</groupId>\n </exclusion>\n </exclusions>\n</dependency>\n```\n\n一般都会优先保留版本较高的,因为大部分 jar 在升级的时候都会做到向下兼容。\n\n但如果出现 jar 不向下兼容的情况,应考虑升级其上层依赖。如同样的例子,若 X 2.0 删除了一些 X 1.0 的类或方法,那么要考虑升级依赖 D 的版本到一个 X 2.0 兼容的版本。\n\n# Maven 仓库\n\nMaven 通过仓库来统一管理构件(artifact),每个构件都有一组坐标唯一标识。通过该坐标我们可以在仓库中找到该构件。\n\nMaven 仓库分为:\n\n- 本地仓库:\n \n 运行 Maven 的计算机上的一个目录,缓存有远程下载的构件并包括尚未发布的临时构件。`settings.xml` 文件中可以看到 Maven 的本地仓库路径配置,默认本地仓库路径是在 `${user.home}/.m2/repository`。\n \n- 远程仓库:\n \n 官方或其他组织维护的 Maven 仓库。\n \nMaven 远程仓库分为:\n\n- 中央仓库:\n \n 该仓库由 Maven 社区维护,是 Maven 的默认远程仓库。另外为了方便查询,还提供了一个查询地址,开发者可以通过这个地址更快的搜索需要构件的坐标。[Maven Repository](https://mvnrepository.com/)\n\n- 私服:\n \n 私服是一种特殊的远程 Maven 仓库,它是架设在局域网内的仓库服务,私服一般被配置为互联网远程仓库的镜像,供局域网内的 Maven 用户使用。\n \n- 其他公共仓库\n \n 有一些公共仓库是为了加速访问(比如阿里云 Maven 镜像仓库)或者部分构件不存在于中央仓库中。\n\n**Maven 依赖包寻找顺序**\n\n1. 先去本地仓库找,若有,直接使用。\n2. 若本地仓库没有找到,会去远程仓库找,并下载到本地仓库。\n3. 若远程仓库没有找到,会报错。\n\n# Maven 生命周期\n\nMaven 的生命周期是为了对所有的构建过程进行抽象和统一,包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。\n\nMaven 定义了 3 个生命周期 `META-INF/plexus/components.xml`:\n\n- `default`\n- `clean`\n- `site`\n\n这些生命周期是相互独立的,每个生命周期包含多个阶段(phase)。并且,这些阶段是有序的,也就是说,后面的阶段依赖于前面的阶段。当执行某个阶段的时候,会先执行它前面的阶段。\n\n执行 Maven 生命周期的命令格式如下:\n\n```shell\nmvn 阶段 [阶段2] ... [阶段n]\n```\n\n## default 生命周期\n\n`default` 生命周期是在没有任何关联插件的情况下定义的,是 Maven 的主要生命周期,用于构建应用程序,共包含 23 个阶段。\n\n```xml\n<phases>\n <!-- 验证项目是否正确,并且所有必要的信息可用于完成构建过程 -->\n <phase>validate</phase>\n <!-- 建立初始化状态,例如设置属性 -->\n <phase>initialize</phase>\n <!-- 生成要包含在编译阶段的源代码 -->\n <phase>generate-sources</phase>\n <!-- 处理源代码 -->\n <phase>process-sources</phase>\n <!-- 生成要包含在包中的资源 -->\n <phase>generate-resources</phase>\n <!-- 将资源复制并处理到目标目录中,为打包阶段做好准备。 -->\n <phase>process-resources</phase>\n <!-- 编译项目的源代码 -->\n <phase>compile</phase>\n <!-- 对编译生成的文件进行后处理,例如对 Java 类进行字节码增强/优化 -->\n <phase>process-classes</phase>\n <!-- 生成要包含在编译阶段的任何测试源代码 -->\n <phase>generate-test-sources</phase>\n <!-- 处理测试源代码 -->\n <phase>process-test-sources</phase>\n <!-- 生成要包含在编译阶段的测试源代码 -->\n <phase>generate-test-resources</phase>\n <!-- 处理从测试代码文件编译生成的文件 -->\n <phase>process-test-resources</phase>\n <!-- 编译测试源代码 -->\n <phase>test-compile</phase>\n <!-- 处理从测试代码文件编译生成的文件 -->\n <phase>process-test-classes</phase>\n <!-- 使用合适的单元测试框架(Junit 就是其中之一)运行测试 -->\n <phase>test</phase>\n <!-- 在实际打包之前,执行任何的必要的操作为打包做准备 -->\n <phase>prepare-package</phase>\n <!-- 获取已编译的代码并将其打包成可分发的格式,例如 JAR、WAR 或 EAR 文件 -->\n <phase>package</phase>\n <!-- 在执行集成测试之前执行所需的操作。 例如,设置所需的环境 -->\n <phase>pre-integration-test</phase>\n <!-- 处理并在必要时部署软件包到集成测试可以运行的环境 -->\n <phase>integration-test</phase>\n <!-- 执行集成测试后执行所需的操作。 例如,清理环境 -->\n <phase>post-integration-test</phase>\n <!-- 运行任何检查以验证打的包是否有效并符合质量标准。 -->\n <phase>verify</phase>\n <!-- \t将包安装到本地仓库中,可以作为本地其他项目的依赖 -->\n <phase>install</phase>\n <!-- 将最终的项目包复制到远程仓库中与其他开发者和项目共享 -->\n <phase>deploy</phase>\n</phases>\n```\n\n根据前面提到的阶段间依赖关系理论,当我们执行 `mvn test` 命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。\n\n## clean 生命周期\n\nclean 生命周期的目的是清理项目,共包含 3 个阶段:\n\n1. pre-clean\n2. clean\n3. post-clean\n\n```xml\n<phases>\n <!-- 执行一些需要在clean之前完成的工作 -->\n <phase>pre-clean</phase>\n <!-- 移除所有上一次构建生成的文件 -->\n <phase>clean</phase>\n <!-- 执行一些需要在clean之后立刻完成的工作 -->\n <phase>post-clean</phase>\n</phases>\n<default-phases>\n <clean>\n org.apache.maven.plugins:maven-clean-plugin:2.5:clean\n </clean>\n</default-phases>\n```\n\n根据前面提到的阶段间依赖关系理论,当我们执行 `mvn clean` 的时候,会执行 clean 生命周期中的 pre-clean 和 clean 阶段。\n\n## site 生命周期\n\nsite 生命周期的目的是建立和发布项目站点,共包含 4 个阶段:\n\n1. pre-site\n2. site\n3. post-site\n4. site-deploy\n\n```xml\n<phases>\n <!-- 执行一些需要在生成站点文档之前完成的工作 -->\n <phase>pre-site</phase>\n <!-- 生成项目的站点文档作 -->\n <phase>site</phase>\n <!-- 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备 -->\n <phase>post-site</phase>\n <!-- 将生成的站点文档部署到特定的服务器上 -->\n <phase>site-deploy</phase>\n</phases>\n<default-phases>\n <site>\n org.apache.maven.plugins:maven-site-plugin:3.3:site\n </site>\n <site-deploy>\n org.apache.maven.plugins:maven-site-plugin:3.3:deploy\n </site-deploy>\n</default-phases>\n```\n\nMaven 能够基于 `pom.xml` 所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。\n\n# Maven 插件\n\nMaven 本质上是一个插件执行框架,所有的执行过程都由一个个插件独立完成。上述所使用到的 install、clean 等命令,底层都是一个个 Maven 插件。本地的默认插件路径为 `${user.home}/.m2/repository/org/apache/maven/plugins`。关于 Maven 的核心插件可以参考 [Maven Plugins](https://maven.apache.org/plugins/index.html)。\n\nMaven 插件可以理解为一组任务的集合,用户可以通过命令行直接运行指定插件的任务,也可以将插件任务挂载到构建生命周期,随着生命周期运行。\n\nMaven 插件被分为下面两种类型:\n\n- **Build plugins**:在构建时执行。\n- **Reporting plugins**:在网站生成过程中执行。\n\n# Maven 多模块管理\n\n多模块管理简单地来说就是将一个项目分为多个模块,每个模块只负责单一的功能实现。直观的表现就是一个 Maven 项目中不止有一个 `pom.xml` 文件,会在不同的目录中有多个 `pom.xml` 文件,进而实现多模块管理。\n\n多模块管理除了可以更加便于项目开发和管理,还有如下好处:\n\n1. 降低代码之间的耦合性(从类级别的耦合提升到 jar 包级别的耦合);\n2. 减少重复,提升复用性;\n3. 每个模块都可以是自解释的(通过模块名或者模块文档);\n4. 模块还规范了代码边界的划分,开发者很容易通过模块确定自己所负责的内容。\n\n多模块管理下,会有一个父模块,其他的都是子模块。父模块通常只有一个 `pom.xml`,没有其他内容。父模块的 `pom.xml` 一般只定义了各个依赖的版本号、包含哪些子模块以及插件有哪些。不过,要注意的是,如果依赖只在某个子项目中使用,则可以在子项目的 pom.xml 中直接引入,防止父 pom 的过于臃肿。\n","tags":["配置","软件开发","Maven"],"categories":["computer"]},{"title":"设计模式","url":"/archives/41682/","content":"\n设计模式(Design pattern)代表了软件开发中最佳的实践,通常被有经验的面向对象的软件开发人员所采用。\n\n<!-- more -->\n\n# GOF(Gang of Four)\n\n由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 **Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素)** 的书,该书首次提到了软件开发中设计模式的概念。\n\n# 设计模式的类型\n\n| 类型 | 描述 | 设计模式 |\n| --- | --- | --- |\n| 创建型模式 | 在创建对象的同时隐藏创建逻辑的方式 | 工厂模式(Factory Pattern)<br> 抽象工厂模式(Abstract Factory Pattern)<br> 单例模式(Singleton Pattern)<br> 建造者模式(Builder Pattern)<br> 原型模式(Prototype Pattern) |\n| 结构型模式 | 关注对象之间的组合和关系,旨在解决如何构建灵活且可复用的类和对象结构 | 适配器模式(Adapter Pattern)<br> 桥接模式(Bridge Pattern)<br> 过滤器模式(Filter、Criteria Pattern)<br> 组合模式(Composite Pattern)<br> 装饰器模式(Decorator Pattern)<br> 外观模式(Facade Pattern)<br> 享元模式(Flyweight Pattern)<br> 代理模式(Proxy Pattern) |\n| 行为型模式 | 关注对象之间的通信和交互,旨在解决对象之间的责任分配和算法的封装。 | 责任链模式(Chain of Responsibility Pattern)<br> 命令模式(Command Pattern)<br> 解释器模式(Interpreter Pattern)<br> 迭代器模式(Iterator Pattern)<br> 中介者模式(Mediator Pattern)<br> 备忘录模式(Memento Pattern)<br> 观察者模式(Observer Pattern)<br> 状态模式(State Pattern)<br> 空对象模式(Null Object Pattern)<br> 策略模式(Strategy Pattern)<br> 模板模式(Template Pattern)<br> 访问者模式(Visitor Pattern) |\n| J2EE 模式 | 特别关注表示层,是由 Sun Java Center 鉴定的 | MVC 模式(MVC Pattern)<br> 业务代表模式(Business Delegate Pattern)<br> 组合实体模式(Composite Entity Pattern)<br> 数据访问对象模式(Data Access Object Pattern)<br> 前端控制器模式(Front Controller Pattern)<br> 拦截过滤器模式(Intercepting Filter Pattern)<br> 服务定位器模式(Service Locator Pattern)<br> 传输对象模式(Transfer Object Pattern) |\n\n# 设计模式六大原则\n\n1. 开闭原则(Open Close Principle)\n \n 对扩展开放,对修改关闭。\n \n2. 里氏替换原则(Liskov Substitution Principle)\n \n 任何基类可以出现的地方,派生类一定可以出现。里氏代换原则是对开闭原则的补充。\n \n3. 依赖倒转原则(Dependence Inversion Principle)\n \n 这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。\n \n4. 接口隔离原则(Interface Segregation Principle)\n \n 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。\n \n5. 迪米特法则(Demeter Principle)\n \n 又称最少知识原则(Least Knowledge Principle),指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。\n \n6. 合成复用原则(Composite Reuse Principle)\n \n 合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。\n\n\n\n","tags":["软件开发","设计模式"],"categories":["computer"]},{"title":"设计模式:工厂模式","url":"/archives/17223/","content":"\n**工厂方法模式**是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。\n\n<!-- more -->\n\n**意图:** 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。\n\n**主要解决:** 主要解决接口选择的问题。\n\n**何时使用:** 我们明确地计划不同条件下创建不同实例时。\n\n**如何解决:** 让其子类实现工厂接口,返回的也是一个抽象的产品。\n\n**关键代码:** 创建过程在其子类执行。\n\n**优点:**\n\n1. 一个调用者想创建一个对象,只要知道其名称就可以了。 \n2. 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。\n3. 屏蔽产品的具体实现,调用者只关心产品的接口。\n\n**缺点:** 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。","tags":["软件开发","设计模式"],"categories":["computer"]},{"title":"垃圾回收策略与算法","url":"/archives/64652/","content":"\n垃圾收集器(garbage collector)是一种动态存储分配器,它自动释放程序不再需要的内存块,这些块也称为`垃圾`。自动回收垃圾的过程则称为`垃圾收集(garbage collection)`。在一个支持垃圾收集的语言中,程序显式地申请内存,但从不需要显式的释放它们。垃圾收集器会定期识别垃圾块,并将垃圾块放回空闲链表中。\n\n<!-- more -->\n\n# 垃圾回收策略\n\n## 引用计数法\n\n当对象被引用时程序计数器 +1,释放时 -1。当为 0 时证明对象未被引用,可以回收。\n\n但是这个算法有明显的缺陷,对于循环引用的情况下,循环引用的对象就不会被回收。例如下图:对象 A与 B 循环引用,即便没有其他的对象引用 A 和 B,A 和 B 也都不会被回收。\n\n\n\n## 可达性分析\n\n通过一系列称之为 `GC Roots` 的对象作为起点,从此起点向下搜索,所走过的路径称之为引用链。当一个对象到 GC Roots 没有任何引用链相连接,代表此对象不可达。\n\n\n\n在 Java 可以作为 GC Roots 的对象包括:\n\n1. 虚拟机栈(帧栈中的局部变量表)中的引用对象;\n2. 方法区中类静态属性(static)引用的对象;\n3. 方法区中常量(constant)引用的对象;\n4. 本地方法栈中 JNI(即一般说的 Native 方法)的引用对象;\n\n# 垃圾回收算法\n\n## 标记-清除算法(Mark-Sweep)\n\n首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记结束后统一将所有标记的对象回收掉。\n\n- 标记阶段:标记出所有需要回收的对象;\n- 清除阶段:回收被标记的对象所占用的空间。\n\n\n\n优点:实现简单\n\n缺点:效率较低,并且会产生大量不连续的内存空间碎片,后续可能发生大对象不能找到可利用空间的问题\n\n## 标记-整理算法(Mark-Compact)\n\n标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。\n\n\n\n## 复制算法(Copying)\n\n按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。\n\n\n\n优点:实现简单,运行高效,不易产生碎片\n\n缺点:可用内存缩小为了原来的一半,浪费空间。若存活对象增多的话,该算法的效率会大大降低\n\n## 分代收集算法(Generational Collection)\n\n分代收集法是目前大部分 JVM 所采用的方法,GC 堆划分为新生代(Young Generation)和老生代(Tenured/Old Generation)。新生代和老年代的默认比例为 1 : 2;新生代又分为 Eden 区, from Survivor 区(简称 S0 ),to Survivor 区(简称 S1 ),三者的比例为 8: 1 : 1。\n\n\n\n我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC ),老年代发生的 GC 称为 Old GC(也称为 Full GC )。新老生代会使用不同的垃圾回收算法,一般情况下\n\n- 新生代使用复制算法\n- 老年代使用标记清除算法或者标记整理算法\n\n在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。\n\n\n\n对象的内存分配主要在新生代的 `Eden Space` 和 Survivor 区的 `From Space`(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。\n\n# 参考资料\n\n- [垃圾回收算法之分代回收算法](https://blog.csdn.net/sinat_36246371/article/details/52998505)\n- [垃圾回收策略和算法,看这篇就够了](https://baijiahao.baidu.com/s?id=1674235021824640450&wfr=spider&for=pc)","tags":["GC"],"categories":["computer"]},{"title":"RBAC权限管理","url":"/archives/27652/","content":"\nRBAC(Role-Based Access Control)作为当前使用范围最广的一种权限设计模型,有 3 个基础组成部分,分别是:用户、角色和权限。RBAC 通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离(区别于ACL模型)。不同角色对应不同权限,用户可充当多个角色。\n\n<!-- more -->\n\n# 原则\n\n* 权限划分\n \n 在权限管理系统当中一般划分为以下几种权限:\n * 页面权限:可看到的页面\n * 操作权限:可进行的交互行为\n * 数据权限:可查看的数据\n\n* 安全原则\n\n RBAC的三个著名安全原则:最小权限原则、责任分离原则和数据抽象原则\n * 最小权限原则:RBAC可以将角色配置成其完成任务所需的最小权限集合。\n * 责任分离原则:可以通过调用相互独立互斥的角色来共同完成敏感的任务,例如要求一个计账员和财务管理员共同参与统一过账操作。\n * 数据抽象原则:可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读、写、执行权限。\n\n# 分类\n\n标准的RBAC模型包括四个部件模型,分别为基本模型RABC0、角色分级模型RABC1、角色限制模型RABC2、统一模型RABC3。\n\n* RBAC0(基本模型)定义了完全支持RBAC概念的任何系统的**最低需求**。RBAC0的模型中包括用户(U)、角色(R)和许可权(P)等3类实体集合,RABC0是权限管理的核心部分,其他的版本都是建立在0的基础上。\n* RBAC1(角色分级模型)基于RBAC0模型,引入角色间的**继承关系**(Hierarchical Role),即角色上有了上下级的区别,角色间的继承关系可分为一般继承关系和受限继承关系:\n * 一般继承关系仅要求角色继承关系是一个绝对偏序关系,允许角色间的多继承。\n * 受限继承关系则进一步要求角色继承关系是一个树结构,实现角色间的单继承。这种模型合适于角色之间的层次明确,包含明确。\n\n| 一般继承关系:上级可继承下级角色的权限,而不受到下级角色所属的限制 | 受限继承关系:上级继承下级角色的权限,但是必须是下级角色所属的上级 |\n| ---------------------------------------------------- | ---------------------------------------------------- |\n|  |  |\n\n* RBAC2(角色限制模型)引入了角色间的**约束关系**,主要约束规则包括:角色间的互斥关系,在处理用户和这些角色之间的关系时,包括静态分离和动态分离,静态分离指互斥的角色不能同时赋予同一个用户;动态分离指用户不能同时操作两个互斥的角色进行登录。\n\n| 当采用静态分离时,互斥的角色不能同时被赋予同一个用户 | 动态分离时,互斥的角色可以同时被赋予同一个用户,则用户登录后需要选择使用的角色,同时要支持根据需要切换角色 |\n| ---------------------------------------------------- | ---------------------------------------------------- |\n|  |  |\n\n* RBAC3(统一模型)同时包含了1和2的特性(继承关系和约束关系)。\n\n# 参考资料\n\n- [权限管理_RABC的权限模型分析](https://zhuanlan.zhihu.com/p/157646519)\n\n","tags":["软件开发","权限管理"],"categories":["computer"]},{"title":"配置中心","url":"/archives/6253/","content":"\n## 问题\n\n1. 静态化配置\n\n大多数在项目配置写在例如 **application.yml**,然后将各类参数配置、应用配置、环境配置、安全配置、业务配置都写到这个文件里。当项目代码逻辑中需要使用配置的时候,就从这个配置文件中读取。这种做法虽然简单,但如果参数需要修改,**就需要重启运行中的项目才能生效**。\n\n2. 配置文件无法区分环境。\n\n由于配置文件是放在项目中的,但是我们项目可能会有多个环境,例如:测试环境、预发布环境、生产环境。每一个环境所使用的配置参数理论上都是不同的,所以我们在配置文件中根据**不同环境配置不同的参数,这些都是手动维护**,在项目发布的时候,极其容易因开发人员的失误导致出错。\n\n3. 配置文件过于分散\n\n如果一个项目中存在多个逻辑模块独立部署,每个模块所使用的配置内容又不相同,传统的做法是会在每一个模块中都放一个配置文件,甚至不同模块的配置文件格式还不一样。那么长期的结果就是**配置文件过于分散混乱,难以管理**。\n\n4. 配置修改无法追溯\n\n因为采用的静态配置文件方式,所以当配置进行修改之后,**不容易形成记录**,更无法追溯是谁修改的、修改时间是什么、修改前是什么内容。既然无法追溯,那么当配置出错时,更没办法回滚配置了。\n\n## 目标\n\n是否有一种方法能在不重启项目也能动态修改配置呢?\n\n## 解决办法\n\nnacos 的配置中心提供了这个功能。\n\n我们可以在配置管理 -> 配置列表中创建我们的配置,每次修改这个配置时服务会热更新而不用重启。\n\n\n\n同时,我们在项目的每个服务的配置文件 application.yml 中修改如下:\n\n```yaml\nserver:\n port: xxx # 端口当然要不一样\n\nspring:\n application:\n name: @artifactId@\n cloud:\n nacos:\n discovery:\n server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}\n namespace: @nacos.namespace@\n config:\n server-addr: ${spring.cloud.nacos.discovery.server-addr}\n namespace: @nacos.namespace@\n config:\n import:\n - optional:nacos:application-@profiles.active@.@file.extension@\n - optional:nacos:${spring.application.name}-@profiles.active@.@file.extension@\n```\n\n其中,@artifactId@ 在当前服务的 pom.xml 中配置,也即当前服务的 artifactId\n\n```xml\n<artifactId>the_artifactId_to_change</artifactId>\n```\n\n其他@@的内容在最外层的 pom.xml 中配置,主要是用于区分开发(dev)、测试(test)、上线(prod)配置。\n\n```xml\n<!-- using in application.yml, surrounded with @ -->\n <profiles>\n <profile>\n <id>dev</id>\n <properties>\n <profiles.active>dev</profiles.active>\n <nacos.namespace>the_namspace_to_change</nacos.namespace>\n <file.extension>yml</file.extension>\n </properties>\n <activation>\n <!-- if default environment -->\n <activeByDefault>true</activeByDefault>\n </activation>\n </profile>\n </profiles>\n```\n\n## 参考资料\n\n- [微服务架构之「 配置中心 」](https://zhuanlan.zhihu.com/p/66097586)\n- [为什么需要分布式配置中心?](https://zhuanlan.zhihu.com/p/58113078)","tags":["配置","软件开发"],"categories":["computer"]},{"title":"数据库事务","url":"/archives/25534/","content":"\n# 事务的定义\n\n事务是逻辑上的一组原子操作,即要么都执行,要么都不执行。在数据库中,`START TRANSACTION;` 和 `COMMIT;` 及其之间的所有语句被视为一个事务。\n\n```\n# 开启一个事务\nSTART TRANSACTION;\n# 多条 SQL 语句\nSQL1,SQL2...\n# 提交事务\nCOMMIT;\n\n# 回滚\nROLLBACK;\n```\n\n```mermaid\nflowchart LR\n start((开启事务)) --> sql(sql1 --> sql2 --> ... --> sqln) --sql全部执行成功--> commit((提交事务))\n sql --有一个sql执行失败--> rollback((回滚事务)) \n```\n\n# ACID\n\n现代关系型数据库中的事务都有 ACID 的特性\n\n- 原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;\n- 一致性(Consistency):执行事务前后,数据保持一致。例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;\n- 隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;\n- 持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。\n\n但是,这四个特性不是平级关系。A、I、D 是手段,C 是目的。我们需要保证事务的原子性、隔离性、持久性之后,才能有事务的一致性。\n\n```mermaid\nflowchart TB\n Atomicity --> Consistency\n Isolation --> Consistency\n Durability --> Consistency\n```\n\n# 事务并发问题\n\n在多个事务并发操作数据库时,如果没有有效的机制进行避免就会导致丢失修改(Lost to modify),脏读(Dirty Read),不可重复读(Unrepeatable Read),幻读(Phantom Read)。\n\n## 丢失修改\n\n1. 事务 A 对数据进行了修改,然后事务 B 在事务 A 提交之前也对数据进行了修改;\n2. 事务 B 提交后,事务 A 的修改被覆盖。\n\n| 事务 A | 事务 B |\n| ------------------------------------ | ------------------------------------ |\n| `UPDATE user SET age=21 WHERE id=1;` | |\n| | `UPDATE user SET age=20 WHERE id=1;` |\n| `COMMIT;` | |\n| | `COMMIT;` |\n\n```mermaid\nflowchart LR\n A((A)) --> age1A[age=10] --SET age=21--> age2[age=21] --COMMIT--> age3[age=10]\n B((B)) --> age1B[age=10] --SET age=20--> age4[age=20] --COMMIT---> age5[age=20]\n```\n\n## 脏读\n\n1. 事务 A 对数据进行了修改,事务 B 读取了事务 A 修改后的数据;\n2. 由于某些原因,事务 A 并没有完成提交,回滚了操作,则事务 B 读取的数据就是脏数据。\n\n| 事务 A | 事务 B |\n| ------------------------------------ | -------------------------------- |\n| `UPDATE user SET age=21 WHERE id=1;` | |\n| | `SELECT * FROM user WHERE id=1;` |\n| `ROLLBACK;` | |\n\n```mermaid\nflowchart LR\n A((A)) --> age1[age=10] --SET age=21--> age2[age=21] --ROLLBACK--> age3[age=10]\n B((B)) ---> age4[age=21]\n```\n\n## 不可重复读\n\n1. 在事务 A 未结束时,事务 B 先读取了一次数据;\n2. 事务 A 修改了数据,事务 B 再次读取同一数据,两次读取的数据不一样。\n\n| 事务 A | 事务 B |\n| ------------------------------------ | -------------------------------- |\n| | `SELECT * FROM user WHERE id=1;` |\n| `UPDATE user SET age=21 WHERE id=1;` | |\n| | `SELECT * FROM user WHERE id=1;` |\n\n```mermaid\nflowchart LR\n A((A)) --> age1[age=10] --SET age=21--> age2[age=21]\n B((B)) --SELECT--> age3[age=10] --SELECT---> age4[age=21]\n```\n\n\n## 幻读\n\n1. 在事务 B 读取数据行数后,事务 A 插入数据;\n2. 事务 B 再次读取数据行数,发现有新的记录。\n\n| 事务 A | 事务 B |\n| -------------------------------------- | ---------------------------- |\n| | `SELECT count(*) FROM user;` |\n| `INSERT INTO user VALUES (1,2,3,4,5);` | |\n| | `SELECT count(*) FROM user;` |\n\n```mermaid\nflowchart LR\n A((A)) ----INSERT---> END((end))\n B((B)) --SELECT count(*)--> num2[num=10] --SELECT count(*)---> num3[num=11] --> END\n```\n\n## 不可重复读和幻读的区别\n\n- 不可重复读的重点在于内容`修改`或者记录`减少`;\n- 幻读的重点在于记录`新增`。\n\n幻读本质上是不可重复读的特殊情况,单独区分幻读的原因是解决幻读和不可重复读的方案不一样。\n\n> 执行 `delete` 和 `update` 操作的时候,可以直接对记录加锁。而执行 `insert` 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 `insert` 操作的时候需要依赖 Next-Key Lock(Record Lock+Gap Lock)来保证不出现幻读。\n\n# 事务隔离\n\nSQL 标准定义了四个隔离级别\n\n- 读取未提交(READ-UNCOMMITTED):允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。\n- 读取已提交(READ-COMMITTED):只能读取已经提交的事务所做的修改。\n- 可重复读(REPEATABLE-READ):保证在事务中多次读取同一数据的结果是一样的。\n- 可串行化(SERIALIZABLE):完全服从 ACID 的隔离级别。强制所有事务串行执行,这样事务之间就完全互不干扰。\n\n\n| 隔离级别 | 脏读 | 不可重复读 | 幻读 |\n| ---------------- | ---- | ---------- | ---- |\n| READ-UNCOMMITTED | √ | √ | √ |\n| READ-COMMITTED | × | √ | √ |\n| REPEATABLE-READ | × | × | √ |\n| SERIALIZABLE | × | × | × |\n\n# 如何实现隔离级别\n\n由锁和 MVCC(Multiversion concurrency control)机制共同实现。\n\nSERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。\n\n# MySQL 的默认隔离级别\n\nMySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重复读)。可以通过 `SELECT @@tx_isolation;` 命令来查看\n\n# 参考资料\n\n- [MySQL常见面试题总结](https://javaguide.cn/database/mysql/mysql-questions-01.html#mysql-%E4%BA%8B%E5%8A%A1)\n- [数据库系统原理](https://cyc2018.xyz/%E6%95%B0%E6%8D%AE%E5%BA%93/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B3%BB%E7%BB%9F%E5%8E%9F%E7%90%86.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B3%BB%E7%BB%9F%E5%8E%9F%E7%90%86)","tags":["数据库"],"categories":["computer"]},{"title":"MySQL锁","url":"/archives/1461/","content":"\n锁(lock)是计算机用以协调多个进程间并发(concurrent)访问同一共享资源的一种机制。MySQL 中为了保证数据访问的一致性与有效性等功能,实现了锁机制。MySQL 中的锁是在服务器层或者存储引擎层实现的。\n\n<!-- more -->\n\n# 锁的功能\n\n我们需要在最大程度地利用数据库的并发访问能力的同时,也要确保每个用户能以一致的方式读取和修改数据。但是,事务并发执行时可能会带来各种问题(详见{% post_link 数据库事务 %})。要解决并发事务的问题,就需要借助到锁。\n\n# 锁的分类\n\n```mermaid\nflowchart TB\n 模式分类 --> 乐观锁\n 模式分类 --> 悲观锁\n\n 粒度分类 --> 全局锁\n 粒度分类 --> 表级锁\n 粒度分类 --> 页级锁\n 粒度分类 --> 行级锁\n\n 属性分类 --> id1[共享锁(S)]\n 属性分类 --> id2[排他锁(X)]\n\n 状态分类 --> 意向共享锁\n 状态分类 --> 意向排他锁\n\n 算法分类 --> 间隙锁\n 算法分类 --> 临键锁\n 算法分类 --> 记录锁\n```\n\n# 乐观/悲观锁\n\n## 乐观锁\n\n乐观锁认为数据一般情况下不会造成冲突,所以在数据提交更新的时候才会去检测。适用于读多写少的场景。\n\n### 实现\n\n乐观锁是基本版本号机制实现的。表中增加一个 `version` 字段,读取数据时将 version 一起读出。数据每更新一次,`version += 1`。当修改需要提交时,将读取时的 version 与表当前的 version 做比较:如果一致,说明在此期间无人修改这条记录,不一致则说明已经被修改了,提交失败。\n\n## 悲观锁\n\n悲观锁认为数据每次操作都会被修改,所以在每次操作数据时都会加上锁。适用于并发量不大、写多读少的场景。\n\n### 实现\n\n通过共享锁和排他锁实现。\n\n# 共享/排他锁\n\n## 共享锁\n\n共享锁,又称读锁,简称 S 锁。当事务对某数据加上读锁后,其他事务只能对该数据加读锁,不能加写锁。\n\n### 实现\n\n```sql\n# MySQL 5.7 和 MySQL 8.0\nSELECT ... LOCK IN SHARE; MODE\n# MySQL 8.0\nSELECT ... FOR SHARE;\n```\n\n## 排他锁\n\n排他锁,又称为写锁,简称 X 锁,当事务对数据加上排他锁后,其他事务无法对该数据进行查询或者修改。\n\nMySQL InnoDB 引擎默认 `update`,`delete`,`insert` 都会自动给涉及到的数据加上排他锁,`select` 语句默认不会加任何锁类型。\n\n### 实现\n\n```sql\nSELECT ... FOR UPDATE;\n```\n\n## 兼容性\n\n| | 共享锁(S) | 排他锁(X) |\n| ----------- | ----------- | ----------- |\n| 共享锁(S) | 兼容 | 互斥 |\n| 排他锁(X) | 互斥 | 互斥 |\n\n# 粒度锁\n\n## 全局锁\n\n对整个 MySQL 数据库实例加锁,加锁期间,对数据库的任何增删改操作都无法执行。只适用于全库数据备份。\n\n### 实现\n\n```bash\nFlush tables with read lock (FTWRL)\n```\n\n## 表级锁\n\n给当前操作的这张表加锁。\n\n### 实现\n\n```sql\nLOCK TABLE READ\nLOCK TABLE WRITE\n```\n\n## 页级锁\n\n表级锁速度快,但冲突多,行级冲突少,但速度慢。因此采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 引擎支持页级锁。\n\n## 行级锁\n\n发生锁冲突概率最低,但是加锁慢,开销大。MySQL 中只有 InnoDB 引擎支持行锁,其他不支持。\n\n### 实现\n\nMySQL 中,行级锁并不是之间锁记录,而是锁的索引。MySQL 在执行 `update`、`delete` 语句时会自动加上行锁。\n\n# 意向锁\n\n意向锁是表锁。为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。\n\n## 作用\n\n当有事务 A 有行锁时,MySQL会自动为该表添加意向锁。事务 B 如果想申请整个表的写锁,那么不需要遍历每一行判断是否存在行锁,而直接判断是否存在意向锁,增强性能。\n\n## 实现\n\n意向锁是由数据引擎自己维护的,用户无法手动操作意向锁。在为数据行加共享/排他锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。\n\n## 兼容性\n\n| | 意向共享锁(IS) | 意向排他锁(IX) |\n| -------------- | -------------- | -------------- |\n| 意向共享锁(IS) | 兼容 | 兼容 |\n| 意向排他锁(IX) | 兼容 | 兼容 |\n\n| | 意向共享锁(IS) | 意向排他锁(IX) |\n| ------ | -------------- | -------------- |\n| 共享锁 | 兼容 | 互斥 |\n| 排他锁 | 互斥 | 互斥 |\n\n# 间隙/临键/记录锁\n\n记录锁、间隙锁、临键锁都是排它锁。\n\n## 记录锁(Record Lock)\n\n记录锁锁定一条记录,记录锁也叫行锁。\n\n```sql\nselect * from user where id = 1 for update;\n```\n\n## 间隙锁(Gap Lock)\n\n间隙锁锁定一段区间内的索引记录,而不仅仅是这个区间中的每一条数据。\n\n```sql\nselect * from user where id < 10 for update;\n```\n\n即所有在 `[1,10)` 区间内的记录行都会被锁住,所有id 为 1、2、3、4、5、6、7、8、9 的数据行的插入会被阻塞\n\n## 临键锁(Next-Key Lock)\n\n临键锁是记录锁与间隙锁的组合。其封锁范围既包含索引记录,又包含左闭右开的索引区间。临键锁主要为了避免幻读。\n\n每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。\n\n# 参考资料\n\n- [一文读懂 MySQL 锁](https://www.cnblogs.com/wugongzi/p/16784313.html)\n- [MySQL 锁](https://javaguide.cn/database/mysql/mysql-questions-01.html#mysql-%E9%94%81)","tags":["数据库"],"categories":["computer"]},{"title":"Windows下使用Redis","url":"/archives/63125/","content":"\n\nRedis 官方不支持 Windows,只支持 Linux。但是微软开发了 Windows 版本的,可见于[Releases · microsoftarchive/redis (github.com)](https://github.com/MicrosoftArchive/redis/releases)\n\n下载需要的版本到合适的位置并解压,然后进入相应的目录,打开命令行并输入命令 `redis-server.exe redis.windows.conf` 即可启动 Redis 服务器。\n\n\n\n\n\n# 常见问题\n\n> Creating Server TCP listening socket *:6379: bind: Unknown error\n\n\n\n同样的目录下,输入如下命令,就可以正常使用了。\n\n```bash\nredis-cli.exe # 进入命令行\nshutdown # 关闭 redis\nexit # 推出命令行\nredis-server.exe redis.windows.conf # 再次启动 redis\n```\n\n\n","tags":["配置"],"categories":["computer"]},{"title":"Kotlin:内置排序方法","url":"/archives/10423/","content":"\nKotlin中内置的数组排序的方法有`sort`, `sorted`, `sortBy`, `sortedBy`, `sortWith`, `sortedWith`。可以粗略地对这些方法进行分类。\n\n<!-- more -->\n\n- 按返回值分:\n\n`sort`, `sortBy`, `sortWith`:没有返回值,排序完成后会改变原有的数组;\n\n`sorted`, `sortedBy`, `sortedWith`:直接返回一个排序完之后的数组,不会改变原有的数组。\n\n- 按参数分:\n\n`sort`, `sorted`:无传参,且只适用于元素为基本数据类型的数组。如果是自定义对象,则无法使用。按照递增排序;\n\n`sortBy`, `sortedBy`:传入数组的某个元素或者对象的某个属性值,然后按照该属性值进行递增排序;\n\n`sortWith`, `sortedWith`:传入一个comparator,可以自定义排序规则,适用于多要素排序。","tags":["Kotlin"],"categories":["computer"]},{"title":"Kotlin:构造器和初始化","url":"/archives/20650/","content":"\nKotlin 中有两种构造器:主构造器和次构造器。都用关键字 constructor 来表示。但是在每个类中,主构造器**有且仅有一个**(如果没有定义,则编译器会自动提供一个空构造器),而次构造器可以有 0 个或多个。\n\n<!-- more -->\n\n# 主构造器声明\n\n主构造器声明在类名后面,每个类有且仅有一个。\n\n## 空参/有参主构造器\n\n```kotlin\n// 无参\nclass People constructor() {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n}\n\n// 有参\nclass People constructor(name: String, age: Int) {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n}\n```\n\n主构造器的 constructor 关键字可以省略\n\n```kotlin\n// 无参\nclass People() {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n}\n\n// 有参\nclass People(name: String?, age: Int) {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n}\n```\n\n当然,如果需要加权限修饰符,那么就不能省略 constructor 关键字了,而且该权限修饰符在 constructor 关键字前面。\n\n```kotlin\n// 无参\nclass People private constructor() {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n}\n\n// 有参\nclass People private constructor(name: String?, age: Int) {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n}\n```\n\n## `init` 代码块\n\n**Kotlin的主构造器是没有方法体的**,这就意味着我们无法在主构造器中进行任何操作。\n\n但 Kotlin 为我们提供了一个 `init` 代码块,这个代码块的执行顺序**在主构造方法之后,次构造方法之前**,我们可以在这个代码块中进行各种初始化的操作,包括访问主构造方法中的参数。\n\n```kotlin\nclass People(name: String?, age: Int) {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n \n init {\n this.name = name\n this.age = age\n }\n}\n```\n\n可以看到在这里,init 代码块将属性 name 和 age 初始化了。但是,这样类似 Java 的做法看起来非常繁琐,Kotlin 提供了一个比较简洁的方法,即在主构造器中声明属性。下面的写法与上面的完全等价。\n\n```kotlin\nclass People(var name: String? = null, var age: Int = 10) {\n lateinit var cards: MutableList<Card>\n}\n```\n\n当然,主构造器中声明的属性也可以加上权限修饰符。\n\n```kotlin\nclass People(private var name: String? = null, private var age: Int = 10) {\n lateinit var cards: MutableList<Card>\n}\n```\n\n# 次构造器声明\n\n次构造器声明在类内,每个类可以有 0 个或多个。\n\n## 空参/有参次构造器\n\n```kotlin\nclass People {\n var name: String? = null\n var age: Int = 10\n lateinit var Cards: MutableList<Card>\n\n // 空参次构造器\n constructor() {}\n\n // 有参次构造器\n constructor(name: String?, age: Int) {\n this.name = name\n this.age = age\n }\n\n\t\t// 当构造器方法体内部没有逻辑的时候,可以省略大括号,即\n\t\t// constructor()\n\t\t// 有参构造器也可以省略没有逻辑的方法体,即\n\t\t// constructor(name: String?, age: Int)\n\t\t// 但是有参的空构造方法是没有意义的\n\n\t\t// 也可以加权限修饰符\n\t\tprivate constructor(name: String?, age: Int, cards: MutableList<Card>) {\n this.name = name\n this.age = age\n this.cards = cards\n }\n}\n```\n\n没有主构造器时,次构造器就代替了主构造器的作用。(但是其实仍然调用了空的主构造器)\n\n当主构造器和次构造器同时存在时,次构造器**必须**直接或者间接调用主构造器。\n\n```kotlin\nclass People(name: String?) {\n var name: String? = null\n var age: Int = 10\n lateinit var cards: MutableList<Card>\n\n // 调用了下面的有参构造器,间接地调用了主构造器\n constructor() : this(\"你好\", 100)\n\n // 直接调用了主构造器\n constructor(name: String?, age: Int) : this(name) {\n this.name = name\n this.age = age\n }\n}\n```\n\n可以看到,**在执行顺序上,主构造器 > init代码块 > 次构造器**","tags":["Kotlin"],"categories":["computer"]},{"title":"Java字节码","url":"/archives/57096/","content":"\n在 Java 虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。\n\n<!-- more -->\n\n## 1.将常量压入操作数栈中的指令\n\n### (1)iconst_n、 lconst_n、fconst_n、dconst_n\n\n- aconst_null ——将null对象引用压入栈\n- iconst_m1 ——将int类型常量-1压入栈\n- iconst_n —— 将int类型常量n压入栈 (n为任意整数)\n- lconst_n——将long类型常量n压入栈\n- fconst_n——将float类型常量n压入栈\n- dconst_n——将double类型常量n压入栈\n\n### (2)bipush、sipush\n\n- bipush——将一个8位带符号整数压入栈\n- sipush——将16位带符号整数压入栈\n\n### (3)idc\n\n- ldc ——把常量池中的项压入栈\n- ldc_w ——把常量池中的项压入栈(使用宽索引)\n- ldc2_w——把常量池中long类型或者double类型的项压入栈(使用宽索引)\n\n## 2.将操作数栈中的值存入局部变量表中的指令\n\n### (1)istore_n、lstore_n、fstore_n、dstore_n、astore_n\n\n- istore_n——将int类型值存入局部变量n (n为任意整数)\n- lstore_n—— 将long类型值存入局部变量n\n- fstore_n——将float类型值存入局部变量n\n- dstore_n——将double类型值存入局部变量n\n\n### (2)astore_n、iastore、lastore、fastore 、dastore 、aastore 、bastore、castore、sastore\n\n- astore_n——将引用类型或returnAddress类型值存入局部变量n\n- iastore——将int类型值存入数组中\n- lastore——将long类型值存入数组中\n- fastore ——将float类型值存入数组中\n- dastore——将double类型值存入数组中\n- aastore——将引用类型值存入数组中\n- bastore——将byte类型或者boolean类型值存入数组中\n- castore ——将char类型值存入数组中\n- sastore——将short类型值存入数组中\n\n### (3)wide\n\n- wide——使用附加字节扩展局部变量索引\n\n## 3.从栈中给局部变量中装载值的指令\n\n### (1)iload_n、lload_n 、fload_n、dload_n\n\n- iload_n——从局部变量n中装载int类型值\n- lload_n——从局部变量n中装载long类型值\n- fload_n——从局部变量n中装载float类型值\n- dload_n——从局部变量n中装载double类型值\n\n### (2)aload_n、iaload、laload、faload 、daload、aaload、baload、caload、saload\n\n- aload_n——从局部变量n中装载引用类型值\n- iaload——从数组中装载int类型值\n- laload——从数组中装载long类型值\n- faload——从数组中装载float类型值\n- daload——从数组中装载double类型值\n- aaload ——从数组中装载引用类型值\n- baload——从数组中装载byte类型或boolean类型值\n- caload——从数组中装载char类型值\n- saload ——从数组中装载short类型值\n\n## 4.类型转化的指令(i2l、i2d、l2f、f2d、i2b、i2c、i2s…)\n\n- i2l——把int类型的数据转化为long类型\n- i2f——把int类型的数据转化为float类型\n- i2d——把int类型的数据转化为double类型\n- l2i——把long类型的数据转化为int类型\n- l2f——把long类型的数据转化为float类型\n- l2d——把long类型的数据转化为double类型\n- f2i——把float类型的数据转化为int类型\n- f2l ——把float类型的数据转化为long类型\n- f2d——把float类型的数据转化为double类型\n- d2i ——把double类型的数据转化为int类型\n- d2l ——把double类型的数据转化为long类型\n- d2f——把double类型的数据转化为float类型\n- i2b——把int类型的数据转化为byte类型\n- i2c—— 把int类型的数据转化为char类型\n- i2s —— 把int类型的数据转化为short类型\n\n## 5.整数运算(iadd、ladd 、isub、imul 、idiv、irem 、ineg 、iinc …)\n\n- iadd——执行int类型的加法\n- ladd—— 执行long类型的加法\n- isub——执行int类型的减法\nlsub——执行long类型的减法\n- imul——执行int类型的乘法\n- lmul——执行long类型的乘法\n- idiv——执行int类型的除法\n- ldiv——执行long类型的除法\n- irem——计算int类型除法的余数\n- lrem——计算long类型除法的余数\n- ineg——对一个int类型值进行取反操作\n- lneg——对一个long类型值进行取反操作\n- iinc (如1 by 1) ——把一个常量值加到一个int类型的局部变量上\n\n## 6.逻辑运算\n\n### (1)移位操作 (ishl、lshl、ishr、lshr、iushr、lushr)\n\n- ishl——执行int类型的向左移位操作\n- lshl——执行long类型的向左移位操作\n- ishr—— 执行int类型的向右移位操作\n- lshr——执行long类型的向右移位操作\n- iushr——执行int类型的向右逻辑移位操作\n- lushr——执行long类型的向右逻辑移位操作\n\n### (2)按位布尔运算 (iand、land、ior 、lor、ixor、lxor)\n\n- iand——对int类型值进行“逻辑与”操作\n- land——对long类型值进行“逻辑与”操作\n- ior——对int类型值进行“逻辑或”操作\n- lor——对long类型值进行“逻辑或”操作\n- ixor——对int类型值进行“逻辑异或”操作\n- lxor——对long类型值进行“逻辑异或”操作\n\n### (3)浮点运算\n\n- fadd——执行float类型的加法\n- dadd——执行double类型的加法\n- fsub——执行float类型的减法\n- dsub——执行double类型的减法\n- fmul——执行float类型的乘法\n- dmul——执行double类型的乘法\n- fdiv——执行float类型的除法\n- ddiv——执行double类型的除法\n- frem——计算float类型除法的余数\n- drem—— 计算double类型除法的余数\n- fneg——将一个float类型的数值取反\n- dneg——将一个double类型的数值取反\n\n## 7.对象和数组指令\n\n### (1)对象操作指令 (getstatic、putstatic…)\n\n- new——创建一个新对象\n- checkcast——确定对象为所给定的类型\n- getfield——从对象中获取字段\n- putfield——设置对象中字段的值\n- getstatic——从类中获取静态字段\n- putstatic——设置类中静态字段的值\n- instanceof——判断对象是否为给定的类型\n\n### (2)数组操作指令 (newarray、arraylength…)\n\n- newarray——分配数据成员类型为基本上数据类型的新数组\n- anewarray——分配数据成员类型为引用类型的新数组\n- arraylength——获取数组长度\n- multianewarray——分配新的多维数组\n\n## 7.控制流相关指令\n\n### (1)条件分支指令 (ifeq、iflt 、ifge 、ifle、if_icmpcq、if_icmplt、if_icmple、if_icmpne、ifnull、if_acmpeq 、if_acmpnc、… )\n\n- ifeq——如果等于0,则跳转\n- ifne—— 如果不等于0,则跳转\n- iflt—— 如果小于0,则跳转\n- ifge——如果大于等于0,则跳转\n- ifgt——如果大于0,则跳转\n- ifle——如果小于等于0,则跳转\n- if_icmpcq—— 如果两个int值相等,则跳转\n- if_icmpne——如果两个int类型值不相等,则跳转\n- if_icmplt——如果一个int类型值小于另外一个int类型值,则跳转\n- if_icmpge——如果一个int类型值大于或者等于另外一个int类型值,则跳转\n- if_icmpgt——如果一个int类型值大于另外一个int类型值,则跳转\n- if_icmple——如果一个int类型值小于或者等于另外一个int类型值,则跳转\n- ifnull——如果等于null,则跳转\n- ifnonnull——如果不等于null,则跳转\n- if_acmpeq—— 如果两个对象引用相等,则跳转\n- if_acmpnc——如果两个对象引用不相等,则跳转\n\n### (2)比较指令 (lcmp、fcmpl 、fcmpg 、dcmpl、dcmpg )\n\n- lcmp——比较long类型值\n- fcmpl——比较float类型值(当遇到NaN时,返回-1)\n- fcmpg—— 比较float类型值(当遇到NaN时,返回1)\n- dcmpl——比较double类型值(当遇到NaN时,返回-1)\n- dcmpg—— 比较double类型值(当遇到NaN时,返回1)\n\n// NaN 是计算机科学中数值数据类型的一类值,表示未定义或不可表示的值。\n\n### (3)无条件转移指令 (goto、tableswitch、lookupswitch)\n\n- goto——无条件跳转\n- goto_w——无条件跳转(宽索引)\n\n// 表跳转指令\n\n- tableswitch——通过索引访问跳转表,并跳转\n- lookupswitch——通过键值匹配访问跳转表,并执行跳转操作\n\n### (4)异常 (athrow)\n\n- athrow——抛出异常或错误\n\n### (5)finally子句 (jsr、jsr_w、rct)\n\n- jsr—— 跳转到子例程\n- jsr_w——跳转到子例程(宽索引)\n- rct—— 从子例程返回\n\n### (6)方法调用与返回\n\n<1>方法调用指令(invokcvirtual、invokespecial、invokestatic、invokcinterface)\n\n- invokcvirtual——运行时按照对象的类来调用实例方法\n- invokespecial——根据编译时类型来调用实例方法\n- invokestatic——调用类(静态)方法\n- invokcinterface——调用接口方法\n\n<2>方法返回指令 (ireturn 、lreturn、freturn、dreturn、areturn、return)\n\n- ireturn——从方法中返回int类型的数据\n- lreturn——从方法中返回long类型的数据\n- freturn——从方法中返回float类型的数据\n- dreturn—— 从方法中返回double类型的数据\n- areturn——从方法中返回引用类型的数据\n- return—— 从方法中返回,返回值为void\n\n### (8)线程同步 (montiorenter、monitorexit)\n\n- montiorenter——进入并获取对象监视器\n- monitorexit—— 释放并退出对象监视器","tags":["逆向","Java"],"categories":["computer"]},{"title":"Base64编码","url":"/archives/24050/","content":"\nBase64 是一种基于**64个可打印字符**来表示**二进制数据**的表示方法。\n\n<!-- more -->\n\n因为\n\n$$\n\\begin{align*}\n 64 &= 2^{6} \\\\\n 3 \\times 8 &= 4 \\times 6 = 24\n\\end{align*}\n$$\n\n所以以**每6个bit**为一组来表示某个可打印字符。**每3个byte**的二进制数据可由**4个可打印字符**来表示,然后把每个可打印字符的6bit再添两位高位0,组成8bit,也就是说,转换后的字符串理论上将要比原来的**长1/3**。\n\n在Base64中的可打印字符包括大小写字母 `A-Z`、`a-z`、数字 `0-9`,以及两个可打印符号(这两个可打印符号在不同的系统中不同)。下表是标准的Base64表\n\n| Index | Char | Index | Char | Index | Char | Index | Char |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | A | 16 | Q | 32 | g | 48 | w |\n| 1 | B | 17 | R | 33 | h | 49 | x |\n| 2 | C | 18 | S | 34 | i | 50 | y |\n| 3 | D | 19 | T | 35 | j | 51 | z |\n| 4 | E | 20 | U | 36 | k | 52 | 0 |\n| 5 | F | 21 | V | 37 | l | 53 | 1 |\n| 6 | G | 22 | W | 38 | m | 54 | 2 |\n| 7 | H | 23 | X | 39 | n | 55 | 3 |\n| 8 | I | 24 | Y | 40 | o | 56 | 4 |\n| 9 | J | 25 | Z | 41 | p | 57 | 5 |\n| 10 | K | 26 | a | 42 | q | 58 | 6 |\n| 11 | L | 27 | b | 43 | r | 59 | 7 |\n| 12 | M | 28 | c | 44 | s | 60 | 8 |\n| 13 | N | 29 | d | 45 | t | 61 | 9 |\n| 14 | O | 30 | e | 46 | u | 62 | + |\n| 15 | P | 31 | f | 47 | v | 63 | / |\n\n注意到第62,63个字符,这两个字符会随系统的不同而不同。\n\n- URL编码器会把标准Base64中的 ‘/’ 和 ‘+’ 字符变为形如 ’%XX’ 的形式,而这些 ‘%’ 号在存入数据库时还需要再进行转换,因为ANSI SQL中已将 ’%’ 号用作通配符。为解决此问题,可采用一种用于URL的改进Base64编码,它在末尾填充 '=' 号,并将标准Base64中的 ‘+’ 和 ‘/’ 分别改成了 ‘-’ 和 ‘_’ 。\n- 用于正则表达式的改进Base64变种,将 ’+’ 和 ‘/’ 改成了 ‘!’ 和 ‘-’\n- 将 ‘+/’ 改为 ‘_-’ 或 ‘*._’*(用作编程语言中的标识符名称)或 ‘.-’(用于XML中的Nmtoken)甚至 ‘_:’(用于XML中的Name)\n\n下面给出了标准 Base64 编码实现的算法(C语言和Python)\n\n```c\n#include <stdio.h>\n#include <stdlib.h>\n \n// base64 转换表, 共64个\nstatic const char base64_alphabet[] = {\n 'A', 'B', 'C', 'D', 'E', 'F', 'G',\n 'H', 'I', 'J', 'K', 'L', 'M', 'N',\n 'O', 'P', 'Q', 'R', 'S', 'T',\n 'U', 'V', 'W', 'X', 'Y', 'Z',\n 'a', 'b', 'c', 'd', 'e', 'f', 'g',\n 'h', 'i', 'j', 'k', 'l', 'm', 'n',\n 'o', 'p', 'q', 'r', 's', 't',\n 'u', 'v', 'w', 'x', 'y', 'z',\n '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',\n '+', '/'\n };\n \n// 解码时使用 base64DecodeChars\nstatic const unsigned char base64_suffix_map[256] = {\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 253, 255,\n 255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 253, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,\n 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255,\n 255, 254, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6,\n 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,\n 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,\n 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,\n 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,\n 49, 50, 51, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,\n 255, 255, 255, 255 \n };\n \nstatic char cmove_bits(unsigned char src, unsigned lnum, unsigned rnum) {\n src <<= lnum; \n src >>= rnum;\n return src;\n}\n \nint base64_encode( char *indata, int inlen, char *outdata, int *outlen) {\n int ret = 0; // return value\n if (indata == NULL || inlen == 0) {\n return ret = -1;\n }\n \n int in_len = 0; // 源字符串长度, 如果in_len不是3的倍数, 那么需要补成3的倍数\n int pad_num = 0; // 需要补齐的字符个数, 这样只有2, 1, 0(0的话不需要拼接, )\n if (inlen % 3 != 0) {\n pad_num = 3 - inlen % 3;\n }\n in_len = inlen + pad_num; // 拼接后的长度, 实际编码需要的长度(3的倍数)\n \n int out_len = in_len * 8 / 6; // 编码后的长度\n \n char *p = outdata; // 定义指针指向传出data的首地址\n \n //编码, 长度为调整后的长度, 3字节一组\n for (int i = 0; i < in_len; i+=3) {\n int value = *indata >> 2; // 将indata第一个字符向右移动2bit(丢弃2bit)\n char c = base64_alphabet[value]; // 对应base64转换表的字符\n *p = c; // 将对应字符(编码后字符)赋值给outdata第一字节\n \n //处理最后一组(最后3字节)的数据\n if (i == inlen + pad_num - 3 && pad_num != 0) {\n if(pad_num == 1) {\n *(p + 1) = base64_alphabet[(int)(cmove_bits(*indata, 6, 2) + cmove_bits(*(indata + 1), 0, 4))];\n *(p + 2) = base64_alphabet[(int)cmove_bits(*(indata + 1), 4, 2)];\n *(p + 3) = '=';\n } else if (pad_num == 2) { // 编码后的数据要补两个 '='\n *(p + 1) = base64_alphabet[(int)cmove_bits(*indata, 6, 2)];\n *(p + 2) = '=';\n *(p + 3) = '=';\n }\n } else { // 处理正常的3字节的数据\n *(p + 1) = base64_alphabet[cmove_bits(*indata, 6, 2) + cmove_bits(*(indata + 1), 0, 4)];\n *(p + 2) = base64_alphabet[cmove_bits(*(indata + 1), 4, 2) + cmove_bits(*(indata + 2), 0, 6)];\n *(p + 3) = base64_alphabet[*(indata + 2) & 0x3f];\n }\n \n p += 4;\n indata += 3;\n }\n \n if(outlen != NULL) {\n *outlen = out_len;\n }\n \n return ret;\n}\n \n \nint base64_decode(const char *indata, int inlen, char *outdata, int *outlen) {\n int ret = 0;\n if (indata == NULL || inlen <= 0 || outdata == NULL || outlen == NULL) {\n return ret = -1;\n }\n if (inlen % 4 != 0) { // 需要解码的数据不是4字节倍数\n return ret = -2;\n }\n \n int t = 0, x = 0, y = 0, i = 0;\n unsigned char c = 0;\n int g = 3;\n \n //while (indata[x] != 0) {\n while (x < inlen) {\n // 需要解码的数据对应的ASCII值对应base64_suffix_map的值\n c = base64_suffix_map[indata[x++]];\n if (c == 255) return -1;// 对应的值不在转码表中\n if (c == 253) continue;// 对应的值是换行或者回车\n if (c == 254) { c = 0; g--; }// 对应的值是'='\n t = (t<<6) | c; // 将其依次放入一个int型中占3字节\n if (++y == 4) {\n outdata[i++] = (unsigned char)((t>>16)&0xff);\n if (g > 1) outdata[i++] = (unsigned char)((t>>8)&0xff);\n if (g > 2) outdata[i++] = (unsigned char)(t&0xff);\n y = t = 0;\n }\n }\n if (outlen != NULL) {\n *outlen = i;\n }\n return ret;\n}\n```\n\n```python\nbase64_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',\n 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',\n 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',\n 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/']\n\n\ndef base64_encode(string: str) -> str:\n oldstr = ''\n newstr = []\n base64 = ''\n global base64_list\n # 把原始字符串转换为二进制,用bin转换后是0b开头的,所以把b替换了,首位补0补齐8位\n for i in string:\n oldstr += '{:08}'.format(int(str(bin(ord(i))).replace('0b', '')))\n # 把转换好的二进制按照6位一组分好,最后一组不足6位的后面补0\n for j in range(0, len(oldstr), 6):\n newstr.append('{:<06}'.format(oldstr[j:j + 6]))\n # 在base_list中找到对应的字符,拼接\n for k in range(len(newstr)):\n base64 += base64_list[int(newstr[k], 2)]\n # 判断base字符结尾补几个'='\n if len(string) % 3 == 1:\n base64 += '=='\n elif len(string) % 3 == 2:\n base64 += '='\n return base64\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","tags":["编码"],"categories":["computer"]},{"title":"Git信息泄露","url":"/archives/8426/","content":"\n在配置不当的情况下,可能会将 `.git` 文件夹直接部署到线上环境,这就造成了 Git 泄露问题。攻击者利用该漏洞下载 .git 文件夹中的所有内容。如果文件夹中存在敏感信息(数据库账号密码、源码等),通过白盒的审计等方式就可能直接获得控制服务器的权限和机会。\n\n<!-- more -->\n\n# Git 简介\n\n- Git 始终保存快照而不是文件差异。\n- 任何数据存储前始终使用 SHA-1 计算校验和,保证内容完整性。\n- 使用分布式仓库设计,让大多数操作都在本地进行,保证了使用效率。几乎所有操作都是向数据库增加数据,提交之后就很难丢失数据。\n\n\n\nGit 是一个[内容寻址文件系统(content-addressed filesystem)](https://en.wikipedia.org/wiki/Content-addressable_storage),其存储内容都是通过内容地址维护,可以把它理解成一个**键值对**存储方式:即给定一个存储文件,该系统根据文件信息和内容,使用SHA-1算法计算,返回一个由40个十六进制字符组成的字符串,之后只需要通过该字符串即可访问该文件,这个字符串就是 Git 中通常所说的**校验和**。\n\n- 内容寻址\n \n> 谈到内容寻址,有必要了解一下的就是本地寻址,或者叫物理寻址。对于物理寻址系统,其所有数据存储在物理媒介的可用空间,与其内容无关,系统记录其物理地址(physical location)供随后使用,这些物理地址通常通过使用一个列表或者目录来维护,当再次请求特定数据时,需要使用其物理地址,如路径和文件名。而对于一个内容寻址系统,系统记录的是一个内容地址(content-address),该内容地址是对应数据的一个唯一且持久的识别符,它是通过加密哈希算法(如,SHA-1或MD5)计算出来的一串值,当我们需要数据时,提供该内容地址,系统即可通过该地址获取数据的物理地址,返回数据;同时,对于数据的任何变更都将导致内容地址发生变化。\n>\n\n- SHA-1哈希值\n \n> 在 Git 系统中,每个 Git 对象都通过哈希值来代表这个对象。哈希值是通过 SHA-1 算法计算出来的,长度为40个字符(40-digit)。\n\n# SVN与Git的主要区别\n\nSVN是集中式版本控制系统,版本库是集中放在中央服务器的。而干活的时候,用的都是自己的电脑,所以首先要从中央服务器哪里得到最新的版本,然后干活,干完后,需要把自己做完的活推送到中央服务器。集中式版本控制系统是必须联网才能工作,如果在局域网还可以,带宽够大,速度够快,如果在互联网下,如果网速慢的话,就纳闷了。\n\nGit是分布式版本控制系统,那么它就没有中央服务器的,每个人的电脑就是一个完整的版本库,这样,工作的时候就不需要联网了,因为版本都是在自己的电脑上。既然每个人的电脑都有一个完整的版本库,那多个人如何协作呢?比如说自己在电脑上改了文件A,其他人也在电脑上改了文件A,这时,你们两之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。\n\n# Git 存储目录结构\n\n在初始化 Git 仓库的时候(git clone 或 git init),Git 会生成 .git 隐藏目录。Git 会将所有的文件,目录,提交等转化为 Git 对象,压缩存储在这个目录当中。该目录内容大致如下:\n\n- 文件:\n - COMMIT_EDITMSG:保存最新的commit message,Git系统不会用到这个文件,是给用户的一个参考文件\n - config:Git仓库的配置文件\n - description:仓库的描述信息,主要给gitweb等git托管系统使用\n - HEAD:这个文件包含了一个档期分支(branch)的引用,通过这个文件Git可以得到下一次commit的parent\n - hooks:这个目录存放一些shell脚本,可以设置特定的git命令后触发相应的脚本;在搭建gitweb系统或其他 Git 托管系统会经常用到hook script(钩子脚本)\n - index:这个文件是暂存区(stage),一个二进制文件\n - ORIG_HEAD:HEAD指针的前一个状态\n- 文件夹\n - info:包含仓库的一些信息\n - logs:保存所有更新的引用记录\n - objects:所有的 Git 对象都会存放在这个目录中,对象的SHA-1哈希值的前两位是文件夹名称,后38位作为对象文件名\n - refs:这个目录一般包括三个子文件夹,heads、remotes 和 tags,heads 中的文件标识了项目中的各个分支指向的当前commit\n\n# Git 仓库文件状态\n\nGit 仓库文件有五种状态(或者四种,即前四种):\n\n- **Untracked**:未跟踪。此文件在文件夹中,但并没有加入到 git 库,不参与版本控制。通过`git add` 将状态变为`Staged`。\n- **Unmodified**: 文件已经入库,但未修改。即版本库中的文件快照内容与文件夹中完全一致。这种类型的文件有两种去处:如果它被修改,则变为`Modified`;如果使用`git rm`移出版本库,则成为`Untracked`文件。\n- **Modified**:文件已修改,但仅仅是修改。这个文件也有两个去处:通过`git add`可进入暂存区,成为`staged`状态;使用`git checkout`丢弃修改过程,返回到`unmodify`状态。这个`git checkout`即从库中取出文件,覆盖当前修改。\n- **Staged**:暂存状态。执行`git commit`则将修改同步到库中,进入`Untracked`状态,这时库中的文件和本地文件又变为一致,文件为`Unmodify`状态. 执行`git reset HEAD filename`取消暂存, 文件状态为`Modified`。\n- **Committed**:文件已提交到远程仓库。\n\n\n\n由上面的文件状态可以推出四个工作区域(或者三个,即前三个):\n\n- **Workspace**:工作区,就是平时存放项目代码的地方。\n- **Index / Stage**:暂存区,用于临时存放项目的改动,事实上它只是一个文件,保存即将提交到文件列表信息。\n- **Repository**:仓库区(或版本库),就是安全存放数据的位置,这里面有提交到所有版本的数据。其中HEAD指向最新放入仓库的版本。\n- **Remote**:远程仓库,托管代码的服务器,可以简单的认为是项目组中的一台电脑用于远程数据交换。\n\n文件在这四个区域之间的转换关系如下(箭头上面是对应的 git 指令):\n\n\n\n各种状态和区域之间的转换关系:\n\n\n\n或者可以简化一下,不同工作区域对应的 .git 文件:\n\n\n\n# Git 对象\n\n在 Git 系统中有四种对象,所有的 Git 操作都是基于这些对象。\n\n- blob:用来保存文件的内容。\n - 二进制大对象\n - 主要用于存放文件的内容(数据的二进制流)\n - Git中所有数据都是通过该方式存储。\n- tree:可以理解成一个对象关系树,用来管理一些 tree 和 blob 对象。\n - 代表 blob 的哈希值;\n - 指向 tree 对象的哈希值。\n- commit:指向一个 tree,它用来标记项目某个特定时间点的状态。它包括以下关于时间点的元数据,如时间戳、最近一次提交的作者、指向上次提交。\n - 代表 commit 的哈希值\n - 指向 tree 对象的哈希值\n - 作者 / 提交者信息\n - 注释\n- tag:给某个提交增添的标记。\n - 可当成一个指针,指向某个 commit (其实就是 Git 版本库的一个快照)\n - 目前的项目开发中,当发布版本时一般会使用tag(例如 v1.1,v1.2,……)\n\nGit 对象之间的个关系如下图:\n\n\n\n# Git 对象的哈希值对应关系\n\n每个 Git 对象都有个哈希值代表这个对象(即 {hash : object})。这个哈希值是通过 SHA-1 算法计算出来的,长度为40个字符。\n\n`git init`\n\n此命令初始化一个新本地仓库,它在工作目录下生成一个名为 .git 的隐藏文件夹。Git会将所有的文件、目录、提交等转化为 Git 对象,压缩存储在这个文件夹当中。\n\n初始的 .git 文件夹内只有七个文件(夹),在objects目录下只有两个文件夹。\n\n|||\n|---|---|\n|||\n\n`git status`\n\n此命令可以查看当前状态。创建一个 test.txt 文件并写入内容,此时 test.txt 文件位于`工作目录`,状态为 `untracked`。\n\n\n\n`git add`\n\n此命令将文件放到暂存区,并将更改写入到 .git/index 文件中。通过git add跟踪这个文件,把test.txt文件放到`暂存区`。(可以使用`git rm --cached`命令来取消暂存)\n\n\n\n执行完 git add 后,.git 文件夹下会多出一个二进制文件 index,用十六进制编辑器打开可以查看该文件的内容。\n\n\n\n似乎其中有刚刚新创建的文件。因为 Git 存储的都是对象信息,这份文件在 Git 系统中也会以对象的形式存储,可以看到在 .git/objects 文件夹内确实多了一个文件夹。\n\n\n\n再次查看 index 文件信息,果然存储了该对象的地址信息(也就是校检和)。(该地址是经过 SHA-1 算法加密后得到的)\n\n\n\n`git cat-file`\n\n此命令会解密校验和。如下就是解密了刚刚新建的文件。\n\n\n\n因为在 Git 中,这些对象存储文件都是通过 zlib 进行压缩的,所以我们同样可以使用 zlib 将其解压出来,来看看这个文件里面存储的内容是什么。\n\n```python\nimport zlib\nimport requests\n\nurl = 'http://192.168.192.160:8080/d/ctf_tools/tmp_files/git_leak/.git/objects/a0/423896973644771497bdc03eb99d5281615b51'\nre = requests.get(url)\nss = re.content\nword = zlib.decompress(ss)\nprint(word)\n```\n\n(不知为何打不开,换用如下方式,道理还是一样的)\n\n```python\nimport zlib\n\nfile_name = '423896973644771497bdc03eb99d5281615b51'\nwith open(file_name, 'rb') as f:\n word = zlib.decompress(f.read())\n print(word)\n```\n\n\n\n发现 objects 对应文件标记了是 blob 对象,存储的是新建的 test.txt 文件的内容。\n\n`git commit`\n\n在暂存区的文件使用git commit提交到版本库(`数据目录`)中。-m 参数表示提交的注释,如果不使用 -m 参数那么会跳出页面提示主动输入注释。commit 完成后,working tree 就会清空。\n\n|||\n|---|---|\n|||\n\n修改 test.txt 文件,并用 git status 查看文件状态,得知文件已被修改,但是未提交修改到版本库中。\n\n\n\n`git diff`\n\n对比工作区和暂存区文件的差异,查看文件被修改的地方\n\n\n\n确认修改无误后将修改提交到版本库的方法和提交初始文件一样,需要先提交到暂存区,然后提交到版本库。\n\n\n\n`git log`\n\n 此命令能查看 commit 的历史记录。(HEAD -> master) 与 .git/HEAD 文件的内容的意义一样,是指到最后提交的版本的指针。\n\n\n\n`git reset --hard`\n\n此命令能回退版本。特别地,若想回退到上一个版本,可以用`git reset --hard HEAD^`,若要回退到上上个版本只需把 **HEAD\\^ 改成 HEAD\\^\\^**,以此类推。若要回退到前100个版本的话,可以使用这个简便命令操作:`git reset --hard HEAD~100`\n\n\n\n可以看到 HEAD 指针也指到了提交的版本,修改的版本看不到了。\n\n但是要是因为操作失误,怎么追回第二次修改的版本呢?可以使用 `git reset --hard 版本号` 追回。\n\n`git reflog`\n\n获取版本号。获得修改的版本号为 `93bf5b9`。`git reset --hard 93bf5b9` 恢复\n\n\n\n当然,用 git log 查询时 commit 后面的 SHA-1 字符串也可以当作版本号来回退。\n\n\n\n在工作区修改的文件 git add 提交到暂存区后,也可以使用 `git checkout -- test.txt` 把工作区的修改撤销。这样,文件就会回退到上一次提交时的状态。\n\n## Git 信息泄露漏洞\n\n由上面一些简单的过程,可以很形象地了解到 Git 在解析存储版本信息的时候,利用的是内存地址寻址的方式分布式存储版本信息的。\n\n如果配置不当,可能会将 .git 文件夹泄露,这就引起了 Git 泄露漏洞。我们就可以通过 zlib 解压等方式获取到网站的相关版本信息等等,进而进行代码审计等操作,分析应用的漏洞以及社工等等。也就是说 Git 信息泄露漏洞的入手点就是 .git 文件夹。\n\n## Git 信息泄露利用总结\n\n首先,我们需要对应的授权站点是否存在这个漏洞:\n\n- 可以先观察一下站点是否有醒目地指出 Git,如果有的话,那就说明站点很大可能存在这个问题。\n- 如果站点没有醒目的提示的话,可以利用 dirsearch 这类扫描工具,如果存在 ./git 泄露的问题的话,会被扫描出来。\n- 最直观的方式,就是直接通过网页访问 .git 目录,如果能访问就说明存在。\n\n当确认存在这个漏洞之后,就可以通过工具来下载 Git 泄露的全部源码(工具例如:GitHack等等)\n\n## Git 信息泄露发现\n\n- 通过泄露的 `.git` 文件夹下的文件,还原重建工程源代码。\n- 解析 `.git/index` 文件,找到工程中所有的(文件名,文件 SHA-1)。\n- 去 `.git/objects` 文件夹下下载对应的文件。\n- `zlib` 解压文件,按原始的目录结构写入源代码。\n\n## 例题\n\n可以看最近 2022 DiceCTF @ HOPE 比赛的一道 Misc 题 orphan。\n\n下载附件解压后是一个叫 orphan 的文件夹,里面只有 .git 文件夹和 foo.txt,很显然考的是 Git 信息泄露。\n\n\n\n在此文件夹下开启 Git Bash,可以通过 `git reflog` 看到过去的修改,显然版本号 `b53c9e6` 对应着 flag。用 `git reset --hard b53c9e6` 回退,可以看到 foo.txt 变成了 flag.txt。打开即得 `flag:hope{ba9f11ecc3497d9993b933fdc2bd61e5}`\n\n\n\n\n\n# 参考资料\n\n- [Git中文件的4种状态Untracked、Unmodify、Modified、Staged](http://t.zoukankan.com/thirteen-yang-p-13878118.html)\n- [Git由浅入深之存储原理](https://zhuanlan.zhihu.com/p/25989135)\n- [git信息泄露漏洞](https://blog.csdn.net/xy_sunny/article/details/107620695)\n- [Git信息泄露原理解析及利用总结](https://blog.csdn.net/wulanlin/article/details/122409259)\n- [Git使用教程--最详细、最傻瓜、最浅显、真正手把手教!(一)](https://www.jianshu.com/p/93738d041381)\n- [Git使用教程--最详细、最傻瓜、最浅显、真正手把手教!(二)](https://www.jianshu.com/p/f5e4b058a7b0)\n\n","tags":["逆向","Git"],"categories":["computer"]},{"title":"http请求走私","url":"/archives/63051/","content":"\n在复杂的网络环境下,不同的服务器对RFC( Request For Comments )标准实现的方式不同,程度不同。这样一来,对同一个HTTP请求,不同的服务器可能会产生不同的处理结果,这样就会产生安全风险。\n\n<!-- more -->\n\n## 概述\n\nHTTP有着无连接这个重要的特性,它是应用层的协议,在传输层使用的是TCP协议。\n\n无连接是指每次发送HTTP 请求,都需要建立一次TCP链接,但这会造成网络的利用率比较低。\n\n> 现代的Web网络页面是由多种资源组成的,在浏览一个网页内容时,不仅要请求HTML文档,还有JS、CSS、图片等各种各样的资源。\n>\n> 如果每次请求都需要建立一个TCP链接,当有大量用户同时发起请求的时候,便会给服务器造成很多不必要的消耗。\n\n于是,从HTTP 1.1版本开始,增加了Keep-Alive和Pipeline两个特性。\n\n### Keep-Alive\n\n在 HTTP 协议中加了 **Connection: keep-alive** 这个请求头,告诉 Server 接受完这次HTTP请求后,不要关闭TCP链接,后续会复用这个链接。这样只需要一个TCP握手的过程,就可以发送多个文件,节约资源,还能加快访问的速度。当有请求带着 **Connection: close** 这个请求头后,在这次通信完成之后,服务器才会断开TCP链接。\n\n如此解决了额外消耗的问题,但是服务器处理请求的方式依然是请求一次响应一次,然后再处理下一个请求。这中间还有着很大的时延消耗。为此提出了Pipelining解决这个问题。\n\n### Pipelining\n\nPipelining 这个特性允许客户端在等待上一个报文回复的同时,发送下一个报文。这让客户端可以一次发送多个请求,服务器也可以一次接受多个请求,遵循先进先出的原则处理这些请求,再将响应发送给客户端。\n\n 现在,浏览器默认不启用`Pipeline`的,但是一般的服务器都提供了对`Pipleline`的支持。 \n\n \n\n以上的两种特性节约了网络资源,提高了这些资源的利用效率。但是,这也有新的问题,那就是服务器要如何区分不同的请求报文。\n\n关于如何界定数据包的边界,HTTP请求头中有两个文段,Content-Length 和 Transfer-Encoding。\n\n#### Content-Length\n\n简称 CL,是指请求体或者响应体的长度,用**十进制**来表示。\n\n> POST / HTTP/1.1\n> Host: test.com\n> Content-Length: 44\n>\n> abcdowjncowmco kqmcoqkmcimpmqi \n\n如果 Content-Length 的值比实际长度小,会造成内容被截断;如果比实际长度大,服务器会一直等待后面的内容,直到超时。\n\n#### Transfer-Encoding\n\n简称 TE,有多个常见的值:\n\n> Transfer-Encoding: chunked\n>\n> Transfer-Encoding: compress\n>\n> Transfer-Encoding: deflate\n>\n> Transfer-Encoding: gzip\n>\n> Transfer-Encoding: identity\n\nHTTP请求走私中一般采用 chunked,即将消息正文使用分块编码。这时,报文中的实体需要改为用一系列分块来传输。每个分块包含**十六进制**的长度值和数据,长度值占一行,长度不包括结尾的换行符,也不包括分块数据结尾的换行符,但是包括分块中的换行,算2个值(\\r\\n)。\n\n最后一个分块长度值必须为0,对应的分块数据没有内容,表示实体结束。\n\n```http\nPost /langdetect HTTP/ 1.1\nHost: fanyi.baidu.com\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0\nContent-Type: application/x-www-form-urlencoded\nTransfer-Encoding: chunked\n\n2\nab\n3\nery\n1\n=\n2\nja\n2\nck\n0\n\n```\n\n这是两种表示报文长度的方式,而在一个报文段中,允许两种方式同时出现。且优先以**Transfer-Encoding**为准。\n\n> 根据RFC标准,如果接收到的消息同时具有 **Content-Length** 和 **Transfer-Encoding** 两个字段,则必须忽略 **Content-Length** 字段,但是存在不遵循标准的例外。\n>\n> 根据标准,当接受到 **Transfer-Encoding**: chunked,error 这样有多个值或者不识别的值的时候,应该返回400错误。但存在一些方法可以绕过。\n\n## 漏洞原理\n\nHTTP 规范提供了两种不同的方式来指定请求结束的位置,当前/后端对于数据包边界的校验不一致时,会干扰正常的请求序列, 使得攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。 \n\n \n\n同时使用两种不同的方法时,`Content-Length`无效。当使用多个服务器时,对客户端传入的数据理解不一致时,就会出现有些服务器认为`Content-Length`的长度有效,有些以`Transfer-Encoding`有效。而一般情况下,反向代理服务器与后端的源站服务器之间,会重用TCP链接。这样超出的长度就会拼接到下一次请求进行请求,从而导致HTTP请求走私漏洞 。\n\n**RFC2616规范**\n\n> 如果接收的消息同时包含传输编码头字段(Transfer-Encoding)和内容长度头(Content-Length)字段,则必须忽略后者。\n\n由于规范默许可以使用`Transfer-Encoding`和`Content-Length`处理请求,因此很少有服务器拒绝此类请求。每当我们找到一种方法,将`Transfer-Encoding`隐藏在服务端的一个`chain`中时,它将会回退到使用`Content-Length`去发送请求。\n\n**走私攻击实现**\n当向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,代理服务器可能认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。\n\n### 如何执行HTTP请求走私攻击\n\nHTTP请求走私攻击涉及将`Content-Length`标头和`Transfer-Encoding`标头都放置在单个HTTP请求中并进行处理,以便前端服务器和后端服务器以不同的方式处理请求。完成此操作的确切方式取决于两个服务器的行为:\n\n> CL.TE:前端服务器使用Content-Length标头,而后端服务器使用Transfer-Encoding标头。\n> TE.CL:前端服务器使用Transfer-Encoding标头,而后端服务器使用Content-Length标头。\n> TE.TE:前端服务器和后端服务器都支持Transfer-Encoding标头,但是可以通过对标头进行某种方式的混淆来诱导其中一台服务器不对其进行处理。\n\n### HTTP请求走私攻击的五种方式\n\n#### CL不为0\n\n所有不携带请求体的HTTP请求都有可能受此影响。这里用GET请求举例。\n前端代理服务器允许GET请求携带请求体;后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的`Content-Length`头,不进行处理。这就有可能导致请求走私。\n\n**构造请求示例**:\n\n```\nGET / HTTP/1.1\\r\\n\nHost: test.com\\r\\n\nContent-Length: 44\\r\\n\n\nGET /secret HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n\\r\\n\n```\n\n> ```\n> \\r\\n`是换行的意思,windows的换行是`\\r\\n`,unix的是`\\n`,mac的是`\\r\n> ```\n\n**攻击流程**:\n前端服务器收到该请求,读取`Content-Length`,判断这是一个完整的请求。\n然后转发给后端服务器,后端服务器收到后,因为它认为这是一个 Get 请求,因此它不对`Content-Length`进行处理,由于`Pipeline`性质的存在,后端服务器就认为这是收到了两个请求,分别是:\n\n第一个:\n\n```\nGET / HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n```\n\n第二个:\n\n```\nGET /secret HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n```\n\n所以造成了请求走私。\n\n#### CL-CL\n\n**RFC7230规范**\n\n> 在RFC7230的第3.3.3节中的第四条中,规定当服务器收到的请求中包含两个`Content-Length`,而且两者的值不同时,需要返回400错误。\n\n有些服务器不会严格的实现该规范,假设中间的代理服务器和后端的源站服务器在收到类似的请求时,都不会返回400错误。\n但是中间代理服务器按照第一个`Content-Length`的值对请求进行处理,而后端源站服务器按照第二个`Content-Length`的值进行处理。\n**构造请求示例**:\n\n```\nPOST / HTTP/1.1\\r\\n\nHost: test.com\\r\\n\nContent-Length: 8\\r\\n\nContent-Length: 7\\r\\n\n\n12345\\r\\n\na\n```\n\n**攻击流程**:\n中间代理服务器获取到的数据包的长度为8,将上述整个数据包原封不动的转发给后端的源站服务器。\n而后端服务器获取到的数据包长度为7。当读取完前7个字符后,后端服务器认为已经读取完毕,然后生成对应的响应,发送出去。而此时的缓冲区去还剩余一个字母`a`,对于后端服务器来说,这个`a`是下一个请求的一部分,但是还没有传输完毕。\n如果此时有一个其他的正常用户对服务器进行了请求:\n\n```\nGET /index.html HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n```\n\n因为代理服务器与源站服务器之间一般会重用TCP连接。所以正常用户的请求就拼接到了字母`a`的后面,当后端服务器接收完毕后,它实际处理的请求其实是:\n\n```\naGET /index.html HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n```\n\n这时,用户就会收到一个类似于`aGET request method not found`的报错。这样就实现了一次HTTP走私攻击,而且还对正常用户的行为造成了影响,而且还可以扩展成类似于CSRF( Cross—Site Request Forgery )的攻击方式。\n\n但是一般的服务器都不会接受这种存在两个请求头的请求包。该怎么办呢?\n所以想到前面所说的\n**RFC2616规范**\n\n> 如果收到同时存在`Content-Length`和`Transfer-Encoding`这两个请求头的请求包时,在处理的时候必须忽略`Content-Length`。\n\n所以请求包中同时包含这两个请求头并不算违规,服务器也不需要返回400错误。导致服务器在这里的实现更容易出问题。\n\n#### CL-TE\n\nCL-TE,就是当收到存在两个请求头的请求包时,前端代理服务器只处理`Content-Length`请求头,而后端服务器会遵守`RFC2616`的规定,忽略掉`Content-Length`,处理`Transfer-Encoding`请求头。\n\n**chunk传输数据(size的值由16进制表示)**\n\n```\n[chunk size][\\r\\n][chunk data][\\r\\n][chunk size][\\r\\n][chunk data][\\r\\n][chunk size = 0][\\r\\n][\\r\\n]\n```\n\n**构造请求示例**:\n\n```\nPOST / HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n......\nConnection: keep-alive\\r\\n\nContent-Length: 6\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n0\\r\\n\n\\r\\n\na\n```\n\n连续发送几次请求就可以获得响应。\n**攻击流程**:\n由于前端服务器处理`Content-Length`,所以这个请求对于它来说是一个完整的请求,请求体的长度为6,也就是\n\n```\n0\\r\\n\n\\r\\n\na\n```\n\n当请求包经过代理服务器转发给后端服务器时,后端服务器处理`Transfer-Encoding`,当它读取到\n\n```\n0\\r\\n\n\\r\\n\n```\n\n认为已经读取到结尾了。\n但剩下的字母`a`就被留在了缓冲区中,等待下一次请求。当我们重复发送请求后,发送的请求在后端服务器拼接成了类似下面这种请求:\n\n```\naPOST / HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n......\n```\n\n服务器在解析时就会产生报错了,从而造成HTTP请求走私。\n\n#### TE-CL\n\nTE-CL,就是当收到存在两个请求头的请求包时,前端代理服务器处理`Transfer-Encoding`请求头,后端服务器处理`Content-Length`请求头。\n**构造请求示例**:\n\n```\nPOST / HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n......\nContent-Length: 4\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n12\\r\\n\naPOST / HTTP/1.1\\r\\n\n\\r\\n\n0\\r\\n\n\\r\\n\n```\n\n**攻击流程**:\n前端服务器处理`Transfer-Encoding`,当其读取到\n\n```\n0\\r\\n\n\\r\\n\n```\n\n认为是读取完毕了。\n此时这个请求对代理服务器来说是一个完整的请求,然后转发给后端服务器,后端服务器处理`Content-Length`请求头,因为请求体的长度为`4`.也就是当它读取完\n\n```\n12\\r\\n\n```\n\n就认为这个请求已经结束了。后面的数据就认为是另一个请求:\n\n```\naPOST / HTTP/1.1\\r\\n\n\\r\\n\n0\\r\\n\n\\r\\n\n```\n\n成功报错,造成HTTP请求走私。\n\n#### TE-TE\n\nTE-TE,当收到存在两个请求头的请求包时,前后端服务器都处理`Transfer-Encoding`请求头,确实是实现了RFC的标准。不过前后端服务器不是同一种。这就有了一种方法,我们可以对发送的请求包中的`Transfer-Encoding`进行某种混淆操作(如某个字符改变大小写),从而使其中一个服务器不处理`Transfer-Encoding`请求头。在某种意义上这还是`CL-TE`或者`TE-CL`。\n**构造请求示例**:\n\n```\nPOST / HTTP/1.1\\r\\n\nHost: test.com\\r\\n\n......\nContent-length: 4\\r\\n\nTransfer-Encoding: chunked\\r\\n\n\\r\\n\n5c\\r\\n\naPOST / HTTP/1.1\\r\\n\nContent-Type: application/x-www-form-urlencoded\\r\\n\nContent-Length: 15\\r\\n\n\\r\\n\nx=1\\r\\n\n0\\r\\n\n\\r\\n\n```\n\n**攻击流程**:\n前端服务器处理`Transfer-Encoding`,当其读取到\n\n```\n0\\r\\n\n\\r\\n\n```\n\n认为是读取结束。\n此时这个请求对代理服务器来说是一个完整的请求,然后转发给后端服务器处理`Transfer-encoding`请求头,将`Transfer-Encoding`隐藏在服务端的一个`chain`中时,它将会回退到使用`Content-Length`去发送请求。读取到\n\n```\n5c\\r\\n\n```\n\n认为是读取完毕了。后面的数据就认为是另一个请求:\n\n```\naPOST / HTTP/1.1\\r\\n\nContent-Type: application/x-www-form-urlencoded\\r\\n\nContent-Length: 15\\r\\n\n\\r\\n\nx=1\\r\\n\n0\\r\\n\n\\r\\n\n```\n\n成功报错,造成HTTP请求走私。\n\n### 攻击思路\n\n1. 用户劫持\n\n 在有的网络环境下,前端代理服务器在收到请求后,不会直接转发给后端服务器,而是先添加一些必要的字段,然后再转发给后端服务器。这些字段是后端服务器对请求进行处理所必须的,比如:\n\n * 描述TLS链接所使用的协议和密码\n * 包含用户IP地址的XFF头\n * 用户的会话令牌ID\n\n 我们可以用三大步骤来获取这些信息\n\n > 1. 找一个能够将请求参数的值输出到响应中的POST请求\n > 2. 把该POST请求中,找到的这个特殊的参数放在消息的最后面\n > 3. 走私这个请求,然后直接发送一个普通的请求,前端服务器对这个请求重写的一些字段就会显现出来 \n\n 假设前端服务器允许GET请求携带请求体,而后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的Content-Length字段,不进行处理,这就有可能导致请求走私。\n\n ```\n GET / HTTP/1.1\n Host: example.com\n Content-Length: 72\n \n POST /Comment HTTP/1.1\n Host: example.com\n Content-Length: 666\n \n mag=aaa\n ```\n\n 前端服务器通过读取Content-Length,确认了这是一个完整的请求,然后转发到后端服务器,而后端服务器不对Content-Length进行判断,于是这在服务器中变成了两个请求。\n\n 而第二个为POST请求,假定这是一个会将信息显示出来的请求,比如弹幕或者评论,再假定后端服务器会通过 Content-Length 来界定数据包。\n\n 那么由于数据包的长度为666,那么服务器会等待后面的数据,等待正常用户的请求包到来,与其拼接,变为 mas=aaaGET / HTTP/1.1......,然后会将其显示在正常的页面上,导致用户的Cookie等信息发生泄露。\n\n2. 配合 Reflected XSS 进行无交互XSS\n\n 将 Reflected XSS走私到缓冲区,等待正常用户访问。\n\n3. 恶意重定向\n\n > 许多应用程序执行从一个URL到另一个URL的重定向,会将来自请求的HOST标头的主机名放入重定向URL。\n\n ```\n 请求\n GET /home HTTP/1.1\n Host: normal-website.com\n \n 响应\n HTTP/1.1 301 Moved Permanently\n Location: https://normal-website.com/home/\n ```\n\n 通常,这样的行为是无害的,但是可以在HTTP走私请求攻击中利用它来将用户重定向到外部域中。\n\n ```\n POST / HTTP/1.1\n Host: vulnerable-website.com\n content-Length: 54\n Transfer-Encoding: chunked\n \n 0\n \n GET /home HTTP/1.1\n Host: attacker-website.com\n Foo: x\n ```\n\n 走私的请求将触发重定向到攻击者的网站,这将影响后端服务器处理的下一个用户请求\n\n ```\n 拼接后的正常请求\n GET /home HTTP/1.1\n Host: attacker-website.com\n Foo: xGET /scripts/include.js HTTP/1.1\n Host: vulnerable-website.com\n \n 恶意响应\n HTTP/1.1 301 Moved Permanently\n Location: https://attacker-website.com/home/\n ```\n\n 如果用户请求的是一个JavaScript文件,该文件是由网站上的页面导入的。攻击者可以通过在响应中返回恶意的JavaScript文件来完全破坏用户。\n\n4. 缓存投毒\n\n 一般来说,前端服务器由于性能原因,会对后端服务器的一些资源进行缓存,如果存在HTTP请求走私漏洞,则有可能使用重定向来进行缓存投毒,从而影响后续访问的所有用户。\n\n### 漏洞修复\n\n1. 将前端服务器配置为只使用HTTP/2与后端系统通信\n2. 完全禁用后端连接重用来解决此漏洞的所有变体 \n3. 确保连接中的所有服务器运行具有相同配置的相同web服务器软件。\n4. 彻底拒绝模糊的请求,并删除关联的连接。","tags":["CTF"],"categories":["computer"]},{"title":"排序算法","url":"/archives/61237/","content":"\n排序算法是算法的入门知识,其经典思想可以用于很多算法当中。\n\n<!-- more -->\n\n# 十大经典排序算法\n\n| 排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |\n| -------- | -------------- | ------------- | ------------- | ----------- | ------ |\n| 冒泡 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 稳定 |\n| 选择 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 不稳定 |\n| 插入 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 稳定 |\n| 希尔 | $O(n\\log n)$ | $O(n\\log^2n)$ | $O(n\\log^2n)$ | $O(1)$ | 不稳定 |\n| 归并 | $O(n\\log n)$ | $O(n\\log n)$ | $O(n\\log n)$ | $O(n)$ | 稳定 |\n| 快速 | $O(n\\log n)$ | $O(n\\log n)$ | $O(n^2)$ | $O(\\log n)$ | 不稳定 |\n| 堆 | $O(n\\log n)$ | $O(n\\log n)$ | $O(n\\log n)$ | $O(1)$ | 不稳定 |\n| 计数 | $O(n+k)$ | $O(n+k)$ | $O(n+k)$ | $O(k)$ | 稳定 |\n| 桶 | $O(n+k)$ | $O(n+k)$ | $O(n^2)$ | $O(n+k)$ | 稳定 |\n| 基数 | $O(nk)$ | $O(nk)$ | $O(nk)$ | $O(n+k)$ | 稳定 |\n\n# 工具函数\n\n交换函数\n\n```java\npublic void swap(int[] nums, int i, int j) {\n int tmp = nums[i];\n nums[i] = nums[j];\n nums[j] = tmp;\n}\n```\n\n## 冒泡排序\n\n冒泡排序要对一个列表多次重复遍历。它要比较相邻的两项,并且交换顺序排错的项。每对 列表实行一次遍历,就有一个最大项排在了正确的位置。\n\n普通版:\n\n```java\npublic void bubbleSort(int[] nums) {\n int n = nums.length;\n for (int i = 1; i < n; i++) {\n for (int j = 0; j < n - i; j++) {\n if (nums[j] > nums[j + 1])\n swap(nums, j, j + 1);\n }\n }\n}\n```\n\n如果发现列表已经排好序了,那么就可以提前结束。\n\n优化版:\n\n```java\npublic void bubbleSort(int[] nums) {\n int n = nums.length;\n boolean flag = false;\n for (int i = 1; i < n; i++) {\n for (int j = 0; j < n - i; j++) {\n if (nums[j] > nums[j + 1]) {\n swap(nums, j, j + 1);\n flag = true;\n }\n }\n if (!flag) \n break;\n }\n}\n```\n\n\n\n\n# 参考资料\n\n- [常用的排序算法总结](https://zhuanlan.zhihu.com/p/40695917)","tags":["算法"],"categories":["computer"]},{"title":"高等数学:泰勒公式","url":"/archives/48914/","content":"\n泰勒公式虽然形式不算复杂,但来路比较诡异。几乎所有的教材都是直接给出这个公式,然后再进行相应的结论证明,显得过于突兀,也不便于理解。本文尝试给出一种由基本的导数公式和极限定理推导泰勒公式的方法,希望能对读者诸君有所帮助。\n\n<!-- more -->\n\n# 从一阶泰勒公式说起\n\n我们首先从一阶导数着手。假设 $f(x)$ 在 $x_0$ 有一阶导数,那么根据定义,就有\n\n$$\n\\begin{equation}\n \\lim_{x\\rightarrow x_0}\\frac{f(x)-f(x_0)}{x-x_0}=f'(x_0)\n\\end{equation}\n$$\n\n现在回顾一下关于函数极限的一个结论:\n\n$$\n\\begin{equation}\n \\lim_{x\\rightarrow x^*}f(x)=L \\Leftrightarrow f(x)=L+\\alpha(x)\n\\end{equation}\n$$\n\n其中, $\\alpha(x)$ 是该极限过程下的某个无穷小,即 $\\alpha\\rightarrow 0$ 当 $x\\rightarrow x^*$. 利用这个结论,可以将 公式(1) 改写为\n\n$$\n\\begin{equation}\n \\frac{f(x)-f(x_0)}{x-x_0}=f'(x_0)+\\alpha_1(x)\n\\end{equation}\n$$\n\n其中,$\\alpha\\rightarrow 0$ 当 $x\\rightarrow x^*$. 再进一步变形,就可以得到\n\n$$\n\\begin{equation}\n f(x)=f(x_0)+f'(x_0)(x-x_0)+\\alpha_1(x)\\cdot(x-x_0)\n\\end{equation}\n$$\n\n注意到 公式(4) 末尾那一项,显然,它是 $(x-x_0)$ 的高阶无穷小,这是因为\n\n$$\n\\begin{equation}\n \\lim_{x\\rightarrow x_0}\\frac{\\alpha_1(x)\\cdot(x-x_0)}{x-x_0}=\\lim_{x\\rightarrow x_0}\\alpha_1(x)=0\n\\end{equation}\n$$\n\n于是,我们可以直接将它记作\n\n$$\n\\begin{equation}\n \\alpha_1(x)\\cdot(x-x_0)=\\omicron_1(x-x_0)\n\\end{equation}\n$$\n\n这样的话, 公式(4) 式可进一步改写为\n\n$$\n\\begin{equation}\n f(x)=f(x_0)+f'(x_0)(x-x_0)+\\omicron_1(x-x_0)\n\\end{equation}\n$$\n\n好了!这就是一阶泰勒公式。\n\n# 一阶到二阶\n\n那么如何得到二阶的呢?\n\n先比较一下二阶泰勒和一阶泰勒形式上的差别吧。它们前两项都是一样的,只不过二阶的又多出了一项。注意到,高阶无穷小的记号实际上是一个「收纳筐」,它里面装着很多隐藏着的东西。如此,我们猜测,二阶泰勒多出来的这一项,一定是从一阶泰勒那个高阶无穷小中「分析」出来的。\n\n这启发我们来考察这样一个极限\n\n$$\n\\begin{equation}\n \\lim_{x\\rightarrow x_0}\\frac{\\omicron_1(x-x_0)}{(x-x_0)^2}=\\lim_{x\\rightarrow x_0}\\frac{f(x)-f(x_0)-f'(x_0)(x-x_0)}{(x-x_0)^2}\n\\end{equation}\n$$\n\n这是一个 $0/0$ 的极限,要求解它可以考虑使用洛必达。但是,请注意,我们现在只有 $f(x)$ 在 $x_0$ 一点一阶可导的条件,这还不足以让我们使用洛必达。不过,这并没有太大困难,只要加强条件就行,比如:我们让 $f(x)$ 在 $x_0$ 二阶可导,这样的话,就不仅保证了 $f''(x_0)$ 存在,还同时保证了 $f(x)$ 在 $x_0$ 某邻域内一阶可导,这就满足了洛必达的使用条件。\n\n好了!开始洛必达!\n\n$$\n\\begin{align}\n &\\lim_{x\\rightarrow x_0}\\frac{\\omicron_1(x-x_0)}{(x-x_0)^2}\\notag \\\\\n &=\\lim_{x\\rightarrow x_0}\\frac{f(x)-f(x_0)-f'(x_0)(x-x_0)}{(x-x_0)^2}\\notag \\\\\n &=\\lim_{x\\rightarrow x_0}\\frac{f'(x)-f'(x_0)}{2(x-x_0)}\\notag \\\\\n &=\\frac{1}{2}f''(x_0)\n\\end{align}\n$$\n\n现在,我们又利用 公式(2) 的结论,将这个极限改写为\n\n$$\n\\begin{equation}\n \\omicron_1(x-x_0)=\\frac{1}{2}f''(x_0)(x-x_0)^2+\\alpha_2(x)\\cdot(x-x_0)^2\n\\end{equation}\n$$\n\n基于同样的理由, $\\alpha_2(x)\\cdot(x-x_0)^2=\\omicron_2((x-x_0)^2)$ 我们将它代入 公式(10) 并连同 公式(10) 一起代回 公式(7) ,就将得到\n\n$$\n\\begin{equation}\n f(x)=f(x_0)+f'(x_0)(x-x_0)+\\frac{1}{2}f''(x_0)(x-x_0)^2+\\omicron_2((x-x_0)^2)\n\\end{equation}\n$$\n\n显然,这就是二阶泰勒公式!\n\n# 如法炮制……\n\n至此,已经十分明显了,只要给定 $f(x)$ 在 $x_0$ 三阶、四阶……直至 $n$ 阶可导,我们就可以沿用此法反复地将低一阶公式中的高阶无穷小分析着展开,依次得到三阶、四阶……直至 $n$ 阶的泰勒公式。\n\n# 注记\n\n当然,细心的读者可能注意到一点,我们在这里给定的 $n$ 阶泰勒公式展开条件,仅仅是 $f(x)$ 在 $x_0$ 一点处的 $n$ 阶可导,而并不是通行教材中约定的「在包含 $x_0$ 的某闭区间 $[a,b]$ 上 $n$ 阶可导、且在开区间 $(a,b)$ 内 $n+1$ 阶可导」!\n\n这又是为什么呢?\n\n事实上,我们这里推导的泰勒公式是带皮亚诺余项的泰勒公式,而不是带拉格朗日余项的泰勒公式,而要得出那个拉格朗日余项,就还必须再用一次拉格朗日中值定理,于是就需要有更强的条件!关于这一点,读者只需要回顾一下拉格朗日中值定理的条件,就很容易理解了。\n\n# 愿意试试手吗?\n\n讲了这么多,现在就请你拿出纸笔从分析 $\\lim_{x\\rightarrow x_0}\\frac{\\omicron_2((x-x_0)^2)}{(x-x_0)^3}$ 入手推导三阶泰勒,我相信你能行!\n\n# 参考资料\n\n- [我们来更自然地推导泰勒公式!](https://zhuanlan.zhihu.com/p/89717331)","tags":["高数"],"categories":["math"]},{"title":"大学物理:热学","url":"/archives/13332/","content":"\n公式是真尼玛多!!!!!!!!\n\n- 温度(temperature):符号:T、t;SI单位:开尔文(Kelvin,K);常用单位:摄氏度(Celsius,℃),华氏度(Fahrenheit,℉)\n\n 用来表示物体冷热程度的物理量,微观上来讲是物体分子热运动的剧烈程度。表明了分子平均速率的大小。\n\n- 热平衡(Thermal equilibrium):\n\n 同外界接触的物体,其内部温度各处均匀且等于外界温度的状况。在热平衡时,物体各部分以及物体同外界之间都没有热量交换。\n\n- 热力学第零定律(热平衡定律)(The Zeroth Law of Thermodynamics):\n\n 如果两个热力学系统中的每一个都与第三个热力学系统处于热平衡(温度相同),则它们彼此也必定处于热平衡。\n\n- 测量温度:\n\n 水的三相点(triple point of water):此状态下水的固液气态共存:$T_3=273.16K , P_3=611.73Pa$\n\n- 定容气体温度计(constant-volume gas thermometer):\n\n 原理:查理定律:$T=Cp$\n\n $T=(273.16K)(\\lim\\limits_{gas \\to 0}\\frac{p}{p_1})$\n\n- 温标(temperature scale):\n\n 摄氏度:0℃:水的凝固点;100℃:水的沸点。$T_C=T-273.15℃$\n\n 华氏度:$T_F=\\frac{9}{5}T_C+32℉$\n\n- 热膨胀(thermal expansion):\n\n 当物体温度改变时,物体在形状、面积、体积方面有改变的趋势。\n\n 线性膨胀:$\\Delta L=L\\alpha \\Delta T$ ; 体膨胀:$\\Delta V=V\\beta \\Delta T$\n\n 其中,$\\beta =3\\alpha$\n\n- 水体积的收缩:在3.984℃时,水的密度达到最大。\n\n- 热(Heat):符号:Q;SI单位:焦耳(joule,J);常用单位:卡路里(calorie)\n\n- 热的传递方式:\n\n 1. 传导(conduction):两个有接触的物体之间;\n\n 2. 对流(convection):因流体运动,发生在物体与环境之间;\n\n 3. 辐射(radiation):分子发出的电磁能量。\n\n- 热传导:\n\n $$\n P_{cond}=\\frac{Q}{t}=kA\\frac{T_H-T_C}{L}=\\frac{\\Delta T}{R}\n $$ \n\n 其中,t:时间,k:热导率,R:$R=\\frac{L}{kA}$)。\n\n- 稳态导热过程(steady-state thermal conduction process):\n\n 材料中的任何一点的温度和通过材料的能量传递效率都不随时间变化。\n\n- 多物体热传导:\n\n1. 串联:\n\n $$\n P_{cond}=P_1=P_2=\\dots =P_n\n $$\n\n 或者,\n\n $$\n P_{cond}=k_iA\\frac{T_{i-1}-T_i}{L_i} (i=1,2,\\dots ,n)\n $$\n\n 于是,\n \n $$\n T_0-T_n=T_0-T_1+T_1-T_2+\\dots =P_{cond}\\sum\\limits_{i=1}^n\\frac{L_i}{Ak_i}\n $$\n \n 或者,\n \n $$\n R_{tot}=\\sum\\limits_{i=1}R_i\n $$\n\n2. 并联:\n\n $$\n P_{cond}=P_1+P_2+\\dots +P_n\n $$\n \n 故,\n \n $$\n P_{cond}=\\sum\\limits_{i=1}k_iA_i\\frac{T_H-T_C}{L}=(T_H-T_C)\\sum\\limits_{i=1}\\frac{k_iA_i}{L}\n $$\n \n 或者,\n \n $$\n P_{cond}=\\frac{T_H-T_C}{R_{tot}},\\frac{1}{R_{tot}} =\\sum\\limits_{i=1}\\frac{1}{R_i}\n $$\n\n后面有空再写\n","tags":["大物"],"categories":["physic"]},{"title":"线性代数:行列式定义与展开法则","url":"/archives/25238/","content":"\n对于一个 $n\\times n$ 的矩阵 $A$ 的行列式,我们有\n\n1、当 $n=1$ 时,$\\det{(A)}=a_{11}$\n\n2、当 $n\\ge 2$ 时,令 $M_{ij} (1\\le i, j \\ge n)$ 为删除掉矩阵 $A$ 的第 $i$ 行和第 $j$ 列的一个 $(n-1)\\times(n-1)$ 矩阵。若按 $\\det{(A)}$ 的第一行展开,则有:\n\n$$\n\\begin{vmatrix} A \\end{vmatrix} =\\det{(A)}=a_{11}\\begin{vmatrix} M_{11} \\end{vmatrix}-a_{12}\\begin{vmatrix} M_{12} \\end{vmatrix}+\\dots+(-1)^{n+1}a_{1n}\\begin{vmatrix} M_{1n} \\end{vmatrix} \\\\ =\\sum_{j=1}^n (-1)^{j+1} a_{1j} \\det{(M_{1j})}\n$$\n\n当然,我们也可不必沿着第一行展开,此时我们只需将公式中的 “1” 换成其他数字就好,而且由于行列式与转置行列式相等,按列展开也是同理进行。\n\n特殊地,若 $n\\times n$ 矩阵 $A$ 是三角阵(上、下三角)或对角阵,则其行列式的值为\n\n$$\n\\begin{vmatrix} A \\end{vmatrix}=a_{11}a_{22}\\dots a_{nn}\n$$\n\n即对角元之积。若为反三角阵或反对角阵,则其行列式的值为\n\n$$\n\\begin{vmatrix} A \\end{vmatrix}=(-1)^{\\frac{n(n-1)}{2}}b_1b_2\\dots b_n\n$$\n\n其中 $b_i (i=1\\dots n)$ 是该阵的反对角元。\n\n后面有空再补\n","tags":["线代"],"categories":["math"]},{"title":"线性代数:厄米特矩阵、酉矩阵","url":"/archives/16976/","content":"\n这篇文章主要讲的是笔者上上周刚学的两个复数域下的矩阵:厄米特矩阵和酉矩阵。\n\n<!-- more -->\n\n想必大家高中都已经学过了复数,再补充一个极坐标形式的复数后,我们就直接进入正题吧!\n\n极坐标形式(polar form):\n\n$$\nz=re^{i\\theta}=r(cos\\theta+isin\\theta)\n$$\n\n其满足 $z_1·z_2=r_1r_2e^{i(\\alpha+\\theta)}$ , $\\frac{z_1}{z_2}=\\frac{r_1}{r_2}e^{i(\\alpha-\\theta)}$. 这都是一些非常好的性质。\n\n厄米特矩阵(Hermitian matrix):\n\n即自共轭矩阵,共轭转置等于自身。矩阵中每一个第i行第j列的元素都与第j行第i列的元素的共轭相等。厄米特特矩阵主对角线上的元素都是实数的,其特征值也是实数。(可类比为实对称矩阵)\n\n共轭转置(conjugate transpose)操作,可类比为实矩阵的转置操作(对实矩阵来说,转置和厄米共轭操作是相同的),同样有如下性质:\n\n$(AB)^H=B^HA^H$ ;$(A^H)^H=A$ ;$(A+B)^H=A^H+B^H$\n\n厄米特矩阵的性质:\n\n① $A^H=A$ :这条性质要求厄米特矩阵的主对角元必须为实数;\n\n② 对厄米特矩阵 $A$ 和复向量 $x$,数 $x^HAx$ 必为实数;\n\n证:\n\n$$\n(x^HAx)^H=x^HA(x^H)^H=X^HAx\n$$ \n\n即 $x^HAx$ 等于它自身的共轭,故 $x^HAx$ 必是实数.\n\n③ 厄米特矩阵的特征值是实数;\n\n证:法一:设 $Ax=\\lambda x (x\\neq 0)$,则有\n\n$$\nx^HAx=x^H\\lambda x=\\lambda x^Hx=\\lambda\\begin{Vmatrix}x\\end{Vmatrix}^2\n$$\n\n由性质①可知 $x^HAx$ 必为实数,并且由于 $x\\neq 0$,$\\begin{Vmatrix}x\\end{Vmatrix}^2$ 必为正实数,所以,特征值 $\\lambda=\\frac{x^Hx}{\\begin{Vmatrix}x\\end{Vmatrix}^2}$ 必为实数.\n\n法二:设$Ax=\\lambda x (x\\neq 0)$,$\\overline \\lambda x^Hx=(\\lambda x)^Hx=(Ax)^Hx=x^HA^Hx=x^HAx=x^H\\lambda x=\\lambda x^Hx$ ,所以,我们有 $\\overline \\lambda$,故 $\\lambda$ 必为实数.\n\n④ 厄米特矩阵 $A$ 的属于不同特征值的特征向量是正交的(实对称矩阵也同样满足);\n\n证:设 $x_1, x_2$ 是分别对应于特征值 $\\lambda_1 , \\lambda_2$ 的 $A$ 的特征向量,则\n\n$$\nAx_1=\\lambda_1 x_1 , Ax_2=\\lambda_2 x_2\n$$\n\n因此,\n\n$$\n\\lambda_1 x_1^Hx_2=(\\lambda_1 x_1)^Hx_2=(Ax_1)^Hx_2=x_1^HA^Hx_2=x_1^HAx_2=x_1^H\\lambda_2 x_2=\\lambda_2 x_1^Hx_2\n$$\n\n又因为 $\\lambda_1=\\lambda_2$,所以我们有 $x_1^Hx_2=0$,它们是正交的.\n\n酉矩阵(幺正矩阵,unitary matrix):\n\n可类比于实正交矩阵,满足厄米共轭矩阵等于逆阵,即 $U^H=U^{-1}$,等价有 $U^HU=I , UU^H=I$\n\n酉矩阵的性质:\n\n① 内积和向量长度可被酉矩阵保留?(这段话不知如何翻译,意思可看证明过程);\n\n证:$(Ux)^H(Uy)=x^HU^HUy=x^Hy$ 故同理有 $\\begin{Vmatrix}U\\end{Vmatrix}^2=\\begin{Vmatrix}x\\end{Vmatrix}^2$\n\n② 酉矩阵的特征值的绝对值等于 $1$;\n\n证:设 $Ux=\\lambda x$,则 $\\begin{Vmatrix}Ux\\end{Vmatrix}=\\begin{Vmatrix}\\lambda x\\end{Vmatrix}=\\begin{vmatrix}\\lambda\\end{vmatrix} \\begin{Vmatrix}x\\end{Vmatrix}$ 由性质①我们有,$\\begin{Vmatrix}Ux\\end{Vmatrix}=\\begin{Vmatrix}x\\end{Vmatrix}^2$,所以$\\begin{Vmatrix}x\\end{Vmatrix}=\\begin{vmatrix}\\lambda\\end{vmatrix}·\\begin{Vmatrix}x\\end{Vmatrix}$,故$\\begin{vmatrix}\\lambda\\end{vmatrix}=1$\n\n③ 酉矩阵 $U$ 的属于不同特征值的特征向量是正交的。\n\n证:\n\n后面有空再补了","tags":["线代"],"categories":["math"]},{"title":"线性代数:高斯消元","url":"/archives/15357/","content":"\n笔者是今年9月份开始的大学生活,也是从9月份开始线性代数的学习。在线代中,有许多新鲜的定义和运算,所以与高等数学相比,笔者觉得线代更难学得通透。因此,笔者希望通过该篇以及后续文章来梳理下线代中的知识。\n\n<!-- more -->\n\n笔者所用的线代教材是 *Gilbert Strang* 所编写的 Linear Algebra and its Application (the 4th edition) ,在B站上也有他的相关教学视频 [MIT《线性代数》吉尔伯特.斯特朗](https://www.bilibili.com/video/BV134411y7uH?from=search&seid=2698493779032149614)\n\n好,接下来我们开始线代的学习。初入线代,不同于同济紫本从行列式开始(刚看同济书的时候头都大了),我们先从矩阵与高斯消元法讲起。\n\n线代的中心问题就是如何求解线性方程组,因此我们有必要了解什么是线性方程和线性方程组,以及其中的专有名词。\n\n- 线性方程(linear equation):\n\n 正如其名,线性方程(linear equation)就是形如\n $$\n a_1x_1+a_2x_2+...+a_nx_n=b\n $$\n 的方程。其中,$a_i$ 是系数(coefficient),$x_i$ 是变量(variable,或称未知数unknown),$b$ 是常数(constant)。\n\n由此,我们可以有线性方程组(system of linear equations),即多个线性方程方程所构成的方程组,其解取决于系数与常数项!于是,为了简便美化写法,我们便引入了矩阵来表示线性方程组。\n\n- 矩阵记号(matrix notation):\n - 系数矩阵(coefficient matrix):\n $$\n \\begin{bmatrix}\n a_{11} & a_{12} & \\dots & a_{1n} \\\\\n a_{21} & a_{22} & \\dots & a_{2n} \\\\\n \\dots & \\dots & \\dots & \\dots \\\\\n a_{m1} & a_{m2} & \\dots & a_{mn}\n \\end{bmatrix}\n $$\n - 增广矩阵(augmented matrix):\n $$\n \\begin{bmatrix}\n a_{11} & a_{12} & \\dots & a_{1n} & b_1 \\\\\n a_{21} & a_{22} & \\dots & a_{2n} & b_2 \\\\\n \\dots & \\dots & \\dots & \\dots \\\\\n a_{m1} & a_{m2} & \\dots & a_{mn} & b_m\n \\end{bmatrix}\n $$\n\n- 高斯消元法(Guassian elimination):\n\n 是通过初等行变换(Elementary row operation, ERO)来将矩阵转化为上三角矩阵(upper triangular matrix)或行阶梯形矩阵(row echelon form matrix),进而通过回代求解线性方程组的方法。\n\n 1. 对换(interchange):交换任意两行;\n 2. 倍乘(scaling):用非零实数乘以某行;\n 3. 倍加(replacement):将某个方程的倍数加到另一个方程上。\n\n 经过ERO处理前后的矩阵行等价(row equivalent),故ERO可逆。\n\n- 回代(back-substitution):\n \n 矩阵化为上三角阵或行阶梯形后,从最后一个未知数开始由尾至头逐步代回,得出解的过程。\n\n- 解的情况\n - 相容(consistent):方程组有解:\n 1. 唯一解;\n 2. 无穷多解。\n - 不相容(inconsistent):方程组无解。\n\n- 总结:\n\n 高斯消元法实际上是个很简单的过程:\n\n 原线性方程组 -> 高斯消元(行变换)-> 三角矩阵 -> 回代 -> 得出解","tags":["线代"],"categories":["math"]}]