Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

趣味题——优惠券收集问题


题目描述

  • 餐馆有12生肖优惠券,每天给小明等概率随机发放一张,问: 小明集齐12中生肖优惠券的天数
  • 相关延伸: 掷骰子, 要求每个面都出现一次, 求投掷次数的期望

问题求解

  • 思路:
    • 把天数 \(X\) 这个随机变量变成12个随机变量的和$$X = \sum_{i=1}^{12}x_{i}$$
    • 于是可以把期望分解为12个随机变量的期望和$$E(X) = \sum_{i=1}^{12}E(x_{i})$$
  • 第一种优惠券(12种中的任意一种均可):
    • 第一天随机拿到一个优惠券一定能够对应其中某一个生肖,概率为 \(p_{1} = 1\)
    • 期望天数 \(E(x_{1})\) 与概率的关系为 \(E(x_{1})\cdot p_{1} = 1\)
      • 一个直观的说明,为什么 \(E(x_{1})\cdot p_{1} = 1\):由于每一天成功的概率为 \(p_{1}\),不成功概率为 \(1-p_{1}\),显然为二项分布,由二项分布的期望应该为1次 ,即 \(np_{1}=1\),其中n就是我们天数的期望(注意:二项分布的期望与这里的天数期望不同,后者为使得二项分布期望为1所需要的天数)
      • 上面的证明不够严谨,严谨的数学证明如下:
        • 假设期望天数是 \(E\),每天成功的概率为 \(p\),下面我们求成功和不成功的概率分布
        • 第一天成功的概率为 \(p\),成功时只需要 \(1\) 天即可, 即有 \(p\) 的概率需要 \(1\) 天
        • 第一天没成功的概率为 \(1-p\),不成功时需要将当前日子(\(1\) 天)算上,然后回到原点,重新开始,即有 \(1-p\) 的概率需要 \(1+E\) 天
        • 期望公式
          $$E = p*1 + (1-p)(1+E)$$
        • 化简为:
          $$pE = 1$$
    • 需要天数的期望为$$E(x_{1}) = \frac{1}{p_{1}} = 1$$
  • 第二种优惠券(11种中的任意一种均可):
    • 每天随机收到的优惠券有 \(p_{2} = \frac{11}{12}\) 的概率满足剩余的11个生肖中的一个
    • 期望天数 \(E(x_{2})\) 与概率的关系为 \(x_{2}\cdot p_{2} = 1\)
    • 需要的天数期望为$$E(x_{2}) = \frac{1}{p_{2}} = \frac{12}{11}$$
  • …
  • 以此类推可得:
    $$
    \begin{align}
    E(X) &= \sum_{i=1}^{n}E(x_{i}) \\
    &= \sum_{i=0}^{11}\frac{12}{12-i} \\
    &= \sum_{i=1}^{12}\frac{12}{i} \\
    &= 12\sum_{i=1}^{12}\frac{1}{i} \\
    \end{align}
    $$
  • 由于调和级数的和与n的自然对数有关系,详情参考调和级数的和
    $$
    \begin{align}
    \sum_{i=1}^{n}\frac{1}{i} = ln(n) + \gamma + \epsilon_{n}
    \end{align}
    $$
    • \(\gamma\) 为欧拉-马歇罗尼常数,近似值为γ≈0.57721
    • \(\epsilon_{n}\) 约等于 \(\frac{1}{2n}\),随着n的不断增大, \(\epsilon_{n}\) 趋于0
  • 所以有
    $$
    \begin{align}
    E(X) &= 12\sum_{i=1}^{12}\frac{1}{i} \\
    &\approx 12(ln(12)+\gamma+\frac{1}{24})
    \end{align}
    $$
  • 将12生肖推广到n,有
    $$
    \begin{align}
    E(X) &= n\sum_{i=1}^{n}\frac{1}{i} \\
    &\approx n(ln(n)+\gamma+\frac{1}{2n}) \\
    &\approx nln(n)
    \end{align}
    $$

趣味题——寻找最优者

寻找最优者:不可逆的做出选择,如何使得选到最优者的概率最大,又称为1/e法则,“秘书问题”或“相亲问题“


题目描述

  • 由于无人知道缘分何时到来,假设人的一生会遇到100个异性,这100个人参差不齐,其中有个人是最好的,100人随机的排着队出现,而我们对每个人必须决定是否接受,而且一旦决定后不能修改,那么用什么策略选择更有机会选到最好的那一个呢?

解决方案

策略设想

  • 拒绝前 k 个人,然后从第 k+1 个开始,只要 优于前面的所有人 ,则接受
  • k 值的设定比较难,太小了容易找到不好的,太多了容易错过最好的

证明

  • 假设最优者出现在第 \(i(k< i \leq n)\) 个(事件A),那么最优者被选中(事件B)的概率为前i-1个中的最优者在前k个人中的概率
    $$ P(B|A) = \frac{k}{i-1} $$
    • \(P(B|A) = P(最优者被选中|第 i 个为最优者)\)
  • 最优者出现在第 \(i\) 个的概率为
    $$P(B) = \frac{1}{n}$$
  • 最优者被选中的概率为
  • 接下来的证明用到了调和级数的和,详情参考调和级数的和
    $$
    \begin{align}
    \sum_{i=1}^{n}\frac{1}{i} = ln(n) + \gamma + \epsilon_{n}
    \end{align}
    $$
    • \(\gamma\) 为欧拉-马歇罗尼常数,近似值为γ≈0.57721,下面的推导都不需要精确解的,所以可以使用该公式
    • \(\epsilon_{n}\) 约等于 \(\frac{1}{2n}\),随着n的不断增大, \(\epsilon_{n}\) 趋于0
      $$
      \begin{align}
      P(B) &= \sum_{A}P(B,A) \\
      &= \sum_{A}P(B|A)P(A) \\
      &= \sum_{i=k+1}^{n}\frac{k}{i-1}\frac{1}{n} \\
      &= \sum_{i=k+1}^{n}\frac{k}{n}\frac{1}{i-1} \\
      &= \frac{k}{n}\sum_{i=k+1}^{n}\frac{1}{i-1} \\
      &= \frac{k}{n}\sum_{i=k}^{n-1}\frac{1}{i} \\
      &= \frac{k}{n}(\sum_{i=1}^{n-1}\frac{1}{i} - \sum_{i=1}^{k-1}\frac{1}{i}) \\
      &\approx \frac{k}{n}((ln(n-1)+C)-(ln(k-1)+C)) \\
      &= \frac{k}{n}ln(\frac{n-1}{k-1}) \\
      \end{align}
      $$
  • 当n和k足够大时有
    • (不能证明 k 也会足够大,但是直觉上 n 足够大 k 也会足够大,因为 n 足够大时往往需要拒绝更多的人)
      $$\frac{n-1}{k-1} = \frac{n}{k}$$
  • 令 \(x=\frac{k}{n}\),则有
    $$
    \begin{align}
    f(x) &= xln\frac{1}{x} \\
    {f}’(x) &= ln(\frac{1}{x})+(-\frac{1}{x^2}\cdot x \cdot x) \\
    &= ln(\frac{1}{x})-1
    \end{align}
    $$
  • 求 \(f(x)\) 的最大值,令 \({f}’(x)=0\) 有
    $$ ln(\frac{1}{x})-1 = 0 $$
  • 解方程得
    $$ x = \frac{1}{e} $$
  • 也就是说当 \(\frac{k}{n} = \frac{1}{e} \) 时,找到最优者的概率最大

策略描述

  • 拒绝前 \(\frac{n}{e}\) 个人(也就是拒绝前37%的人),然后从第 \(\frac{n}{e}+1\) 个开始,只要优于前面的所有人 ,则接受
  • 这种方式可以保证我们有最大的概率找到最优者

熵——机器学习中各种熵的总结

  • 参考链接:
    • 参考博客: https://www.cnblogs.com/kyrieng/p/8694705.html

自信息(Self-Information):\(I(x)\)

自信息的来源讨论

  • 一条信息的信息量大小和它的不确定性有直接的关系,信息量的度量就等于不确定性的多少
  • 考虑一个离散随机变量 \(x\),随机变量的信息量由概率分布 \(p(x)\) 确定
  • 我们的目标是找到一个函数 \(I(x) = f(p(x))\) (自变量是 \(p(x)\),因为随机变量的信息量由概率分布确定),能够衡量 \(x\) 信息内容的多少
    • 且必须是概率分布 \(p(x)\) 的单调函数?
  • 现在考虑同时出现两个相互独立的随机变量 \(x\) 和 \(y\),那么他们的信息量和应该是
    $$I(x,y) = I(x)+I(y) = f(p(x)) + f(p(y))$$
    • 同时他们的联合概率分布为
      $$p(x,y) = p(x)p(y)$$
  • 由上面的两个表达式可以知道 \(f(\cdot)\) 应该是一个对数相关的函数,因为
    $$log_{a}(MN) = log_{a}M + log_{a}N$$
  • 所以我们可以考虑把原始信息量定义为
    $$I(x) = f(p(x)) = -log p(x)$$
    • 其中负号是用来保证信息量是正数或者零
    • log 函数基的选择是任意的(信息论中基常常选择为 2,因此信息的单位为比特 bits;而机器学习中基常常选择为自然常数,因此单位常常被称为奈特 nats)
  • \(I(x)\) 也被称为随机变量 \(x\) 的自信息(self-information),描述的是随机变量的某个事件发生所带来的信息量

自信息精确定义

  • 在信息论中,自信息(Self-Information) 是衡量单个随机事件发生时所携带的信息量的指标
    • 自信息的核心思想是:事件发生的概率越低,它携带的自信息就越大;反之,概率越高,自信息越小
  • 定义:对于离散随机变量 \(X\),若某个事件 \(x\) 发生的概率为 \(P(x)\),则该事件的自信息定义为:
    $$
    I(x) = -\log_b P(x)
    $$
    • 底数 \(b\) 决定自信息的单位:
      • 当 \(b=2\) 时,单位为 比特(bit) (这是最常用的设定)
      • 当 \(b=e\) 时,单位为 奈特(nat)
      • 当 \(b=10\) 时,单位为 哈特(hart)
  • 自信息的核心性质
    1)非负性:由于 \(0 \leq P(x) \leq 1\),\(\log P(x) \leq 0\),因此 \(I(x) \geq 0\)
    2)单调性:自信息与事件概率成反比
      * 若事件必然发生(\\(P(x)=1\\)),则 \\(I(x)=0\\),该事件不携带任何新信息;
      * 若事件概率极低(\\(P(x)\rightarrow0\\)),则 \\(I(x)\rightarrow\infty\\),该事件发生时会携带极大的信息量
    3)独立性:若两个事件 \(x\) 和 \(y\) 相互独立,则联合事件 \(xy\) 的自信息等于两个事件自信息之和,即
      $$
      I(xy) = I(x) + I(y)
      $$

熵(也称为信息熵):\(H(x)\)

熵的起源讨论

  • 现在假设一个发送者想传送一个随机变量的值给接收者
  • 那么在这个过程中,他们传输的平均信息量可以通过求 \(I(x)=−logp(x)\) 关于概率分布 \(p(x)\) 的期望得到:
    $$
    \begin{align}
    H(x) &= \sum_{x}p(x)(-logp(x)) \\
    &= -\sum_{x}p(x)logp(x) \\
    &= -\sum_{i=1}^{n}p(x_{i})logp(x_{i})
    \end{align}
    $$
  • \(H(X)\) 就被称为随机变量 \(x\) 的熵,它是表示随机变量不确定的度量,是对所有可能发生的事件产生的信息量的期望
  • 从公式可得,随机变量的取值个数越多,状态数也就越多,信息熵就越大,混乱程度就越大,当随机分布为均匀分布时,熵最大,且
    $$0 ≤ H(X) ≤ logn$$
  • 上式的证明如下:
    $$p(1)+p(2)+\dots+p(n)=1$$
    • 目标函数
      $$f(p(1),p(2),\dots,p(n))=-(p(1)logp(1)+p(2)logp(2)+\dots+p(n)logp(n))$$
    • 约束条件
      $$g(p(1),p(2),\dots,p(n),\lambda)=p(1)+p(2)+\dots+p(n)-1=0$$
    • 定义拉格朗日函数
      $$
      L(p(1),p(2),\dots,p(n),\lambda)=-(p(1)logp(1)+p(2)logp(2)+\dots+p(n)logp(n))+\lambda(p(1)+p(2)+\dots+p(n)-1)
      $$
    • 对 \(p(i)\) 和 \(\lambda\) 求偏导,并令其等于0得
      $$p(1)=p(2)=\dots=p(n)=\displaystyle\frac{1}{n}$$
    • 带入目标函数得
      $$
      \begin{align}
      f(\displaystyle\frac{1}{n},\frac{1}{n},\dots,\frac{1}{n}) &= -(\frac{1}{n}log\frac{1}{n} + \frac{1}{n}log\frac{1}{n} + \dots + \frac{1}{n}log\frac{1}{n}) \\
      & = -log(\frac{1}{n}) \\
      & =\log(n)
      \end{align}
      $$

熵的精确定义

  • 在信息论与概率论中,熵(Entropy) 是衡量随机变量不确定性大小的核心量化指标,其本质是随机变量所有可能事件的自信息的数学期望
  • 根据随机变量的类型,熵的定义分为离散型和连续型两类
离散随机变量的熵(信息熵)
  • 设离散随机变量 \(X\) 的取值集合为 \(\{x_1,x_2,\dots,x_n\}\),对应的概率分布为 \(P(X=x_i)=p_i\)(满足归一化条件 \(\sum_{i=1}^n p_i = 1\)),则 \(X\) 的熵定义为:
    $$
    H(X) = -\sum_{i=1}^n p_i \log_b p_i
    $$
    • 其中底数 \(b\) 决定熵的单位:
      • \(b=2\):单位为 比特(bit) ,是信息论中最常用的单位
      • \(b=e\):单位为 奈特(nat) ,适用于理论推导(自然对数)
      • \(b=10\):单位为 哈特(hart) ,较少使用
离散随机变量的熵的核心性质
  • 非负性:\(H(X) \geq 0\),当且仅当 \(X\) 为确定事件(某一 \(p_i=1\),其余 \(p_i=0\))时,\(H(X)=0\);
  • 极值性:当 \(X\) 的所有取值等概率分布时,熵达到最大值 \(H_{\text{max} }(X)=\log_b n\),此时不确定性最高;
  • 可加性:若离散随机变量 \(X\) 与 \(Y\) 相互独立,则联合熵 \(H(X,Y)=H(X)+H(Y)\)
连续随机变量的熵(微分熵)
  • 对于连续随机变量 \(X\),其概率密度函数为 \(p(x)\),则微分熵定义为:
    $$
    h(X) = -\int_{-\infty}^{\infty} p(x)\log_b p(x) dx
    $$
连续随机变量的熵 vs 离散随机变量的熵
  • 微分熵不满足非负性 ,其值可以为负数(例如均匀分布 \(U(a,b)\) 的微分熵为 \(\log(b-a)\),当 \(b-a<1\) 时熵为负)
  • 微分熵的物理意义与离散信息熵略有不同,它衡量的是连续变量分布的“相对不确定性”,而非绝对信息量
自信息与熵的关系
  • 两者的关系公式为:
    $$
    H(X) = \mathbb{E}_[I(x)] = \sum_{x\in X} P(x) \cdot I(x) = -\sum_{x\in X} P(x)\log P(x)
    $$
  • 简单来说:
    • 自信息描述单个事件的信息量
    • 熵描述整个随机变量的平均信息量

互信息(Mutual Information, MI):\(I(X;Y)\)

  • 在概率论与信息论中,两个随机变量 \(X\) 和 \(Y\) 的互信息(Mutual Information, MI) 用于衡量它们之间的相互依赖程度
  • 互信息的核心是量化“已知一个随机变量的信息后,另一个随机变量的不确定性减少的量”

离散随机变量的互信息定义

  • 若 \(X\) 和 \(Y\) 为离散随机变量,联合概率分布为 \(P(X,Y)\),边缘概率分布分别为 \(P(X)\) 和 \(P(Y)\),则互信息的定义为:
    $$
    I(X;Y)=\sum_{x\in X}\sum_{y\in Y}P(x,y)\log\frac{P(x,y)}{P(x)P(y)}
    $$
    • 当 \(X\) 和 \(Y\) 相互独立时,\(P(x,y)=P(x)P(y)\),此时 \(I(X;Y)=0\);
    • 当 \(X\) 和 \(Y\) 完全依赖时,\(I(X;Y)\) 达到最大值,等于 \(X\) 或 \(Y\) 的熵(\(H(X)\) 或 \(H(Y)\))

连续随机变量的互信息定义

  • 若 \(X\) 和 \(Y\) 为连续随机变量,联合概率密度函数为 \(p(x,y)\),边缘概率密度函数分别为 \(p(x)\) 和 \(p(y)\),则互信息的定义为:
    $$
    I(X;Y)=\int_{-\infty}^{\infty}\int_{-\infty}^{\infty}p(x,y)\log\frac{p(x,y)}{p(x)p(y)}dxdy
    $$

互信息的等价公式

  • 互信息可以通过熵(Entropy) 和条件熵(Conditional Entropy) 推导,核心等价关系为:
    $$
    I(X;Y)=H(X)-H(X|Y)=H(Y)-H(Y|X)=H(X)+H(Y)-H(X,Y)
    $$
    • \(H(X)\) 是 \(X\) 的熵,衡量 \(X\) 的不确定性
    • \(H(X|Y)\) 是给定 \(Y\) 时 \(X\) 的条件熵,衡量已知 \(Y\) 后 \(X\) 剩余的不确定性
    • \(H(X,Y)\) 是 \(X\) 和 \(Y\) 的联合熵

互信息 与 InfoNCE 的关联

  • InfoNCE 损失的设计目标是最大化互信息的下界 ,而非直接计算互信息
  • 当负样本数量 \(K\rightarrow\infty\) 时,InfoNCE 损失对应的下界会收敛到真实的互信息 \(I(X;Y)\),这也是该 InfoNCE 损失函数命名的核心原因

趣味题——绳子剪三段后能构成三角形的概率

初始题目为1米长的绳子剪成三段后能够构成三角形的概率,实际上绳子的长度可以是任意的,解法都一样


题目描述

  • 一根绳子长度为 n,剪三段后能构成三角形的概率是多少?
  • 由于长度与概率无关,所以接下来我们把绳子当做长度为 1 米来处理

解决方案

  • 三段绳子的长度分别为 x,y,1-x-y
  • 首先需要满足
    $$
    \begin{align}
    x &> 0 \\
    y &> 0\\
    1-x-y &> 0 \\
    \end{align}
    $$
    • 以 x 为横坐标,y 为纵坐标画图得到阴影区域面积为 \(\frac{1}{2}\)
  • 图片来源于:https://blog.csdn.net/fanoluo/article/details/40374571*
  • 进一步分析三段绳子能生成组成三角形的概率
    $$
    \begin{align}
    x+y &> 1-x-y \\
    y + 1-x-y &> x\\
    x + 1-x-y &> y \\
    \end{align}
    $$
    • 进一步分析得到阴影部分面积为 \(\frac{1}{8}\)
  • 图片来源于:https://blog.csdn.net/fanoluo/article/details/40374571*
  • 所以有
    $$P = \frac{1/8}{1/2} = \frac{1}{4}$$

趣味题——蓄水池采样算法


整体说明

  • 蓄水池采样 (Reservoir Sampling) 算法是一种在数据流中进行随机抽样的方法
  • 它的独特之处在于,不知道数据流的总长度 ,但可以实现对样本等概率采样
  • 场景:处理一个无法放入内存的巨大文件,或者一个实时传入的无限数据流
    • 在这种情况下,需要从这个流中随机选择 \(k\) 个元素,并且要确保每个元素被选中的概率是相等的

算法流程

  • 蓄水池采样算法的核心思想是,随着数据流的不断输入,动态地更新一个大小为 \(k\) 的“蓄水池”
  • 以下是该算法的详细流程:
  • 第一步:初始化蓄水池
    • 创建一个大小为 \(k\) 的数组或列表,这就是我们的蓄水池 (Reservoir)
      • 1)从数据流中读取前 \(k\) 个元素
      • 2)将这 \(k\) 个元素直接放入蓄水池中
  • 第二步:处理后续元素
    • 从数据流中继续读取第 \(i\) 个元素,其中 \(i > k\)。对于每一个新元素,执行以下操作:
      • 1)生成一个 \(1\) 到 \(i\) 之间的随机整数 \(j\)
      • 2)如果 \(j\) 满足条件 \(1 \le j \le k\),则将当前元素替换蓄水池中索引为 \(j\) 的元素
      • 3)如果 \(j > k\),则不做任何操作,直接丢弃该元素
  • 第三步:重复
    • 1)重复第二步,直到数据流结束
    • 2)当数据流处理完毕后,蓄水池中剩下的 \(k\) 个元素就是我们最终的随机样本

算法证明

  • 蓄水池采样算法之所以有效,是因为它能确保数据流中的每个元素被选入蓄水池的概率都是 \(k/N\) ,其中 \(N\) 是数据流的总长度。我们通过数学归纳法来简单理解一下
  • 初始阶段 :前 \(k\) 个元素被直接放入蓄水池,因此它们被选中的概率是 \(1\)
  • 第 \(i\) 个元素 (\(i > k\)):
    • 第 \(i\) 个元素被选中的概率是 \(k/i\)。这是因为我们生成了一个 \(1\) 到 \(i\) 的随机数,只有当它落在前 \(k\) 个位置时,第 \(i\) 个元素才会被选中并替换掉蓄水池中的一个元素
    • 关键是保持之前元素的概率不变。我们来看一下,在第 \(i\) 步,蓄水池中某个旧元素被保留的概率
      • 在第 \(i-1\) 步结束时,蓄水池中的每个元素被选中的概率是 \(k/(i-1)\)
      • 当第 \(i\) 个元素进入时,这个旧元素被替换的概率是 \(1/i\)(因为它有 \(1/i\) 的概率被选中并替换掉蓄水池中的一个随机位置)
      • 所以,一个旧元素被保留的概率是 \(1 - 1/i = (i-1)/i\)
      • 那么,这个旧元素在第 \(i\) 步结束后仍然留在蓄水池中的概率是:
        $$P(\text{留在蓄水池}) = P(\text{在第 }i-1 \text{ 步被选中}) \times P(\text{在第 }i \text{ 步被保留}) = \frac{k}{i-1} \times \frac{i-1}{i} = \frac{k}{i}$$
    • 通过这个过程,我们可以看到,在处理完第 \(i\) 个元素后,所有 \(i\) 个元素(包括新来的和之前的)被选中的概率都精确地保持在 \(k/i\)。当数据流结束时,假设总长度为 \(N\),则每个元素被选中的概率都是 \(k/N\)

Python——做算法题时应该注意的细节

和Java类似,Python实现了丰富的基本数据结构,但是使用方式和Java有所不同


字符类型

  • Python中没有Java以及C++一样的字符类型,只有字符串类型

字符和数字的比较

  • 'a'表示的不是字符’a’, 而是字符串”a”,也就是说0 < 'a'这样的写法是不可以的,因为'a'不是一个数字
  • 若非要和数字比较,方式如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    string = "12_abc*ABZ-="
    alphanumeric_str = ""
    for char in string:
    if (ord(char) >= ord('0') and ord(char) <= ord('9'))\
    or (ord(char) >= ord('a') and ord(char) <= ord('z'))\
    or (ord(char) >= ord('A') and ord(char) <= ord('Z')):
    alphanumeric_str += char
    print(alphanumeric_str)

    # # output:
    # 12abcABZ

集合类型的复制

浅拷贝

以list为例

1
2
3
4
5
6
7
8
l = [1, 2, 3]
# method 1
lcopy = l[:] # notice: ls.append(l[:]), 添加的是副本!!!
# method 2
lcopy = l.copy()
# method 3
import copy
lcopy = copy.copy(l)

深拷贝

1
2
3
4
import copy

l = [1, 2, 3, [4, 5, 6]]
l = copy.deepcopy(l)

format函数的使用

  • format是用于格化式字符串的函数

  • format是字符串自带的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # format string
    print "{} {}".format("hello", "world")
    print "{0} {1}".format("hello", "world")
    print "{1} {0} {1}".format("hello", "world")
    # output
    'hello world'
    'hello world'
    'world hello world'

    # format number
    print "{:.2f}".format(3.1415926)
  • 其他使用方式之后再补充


字符串到数字的转换

1
2
3
4
5
6
7
8
9
# string to integer, int(str, base=10)
a = "101"
integer = int(a, 10)
integer = int(a, 2)
integer = int(a, 7)

# string to float, float(str)
b = "123.4"
f = float(b)

进制转换

转换示例

1
2
3
4
5
6
7
8
9
# binary, octonary, hexadecimal to decimal(from string)
dec_num = int("1001", 2) # or int("0b1001", 2)
dec_num = int("657", 8) # or int("0657", 8)
dec_num = int("ff", 16) # or int("0xff", 16)

# decimal to binary, octonary, hexadecimal(or string)
bin_num = bin(123) # or str(bin(123))
oct_num = oct(123) # or str(oct(123))
hex_num = hex(123) # or str(hex(123))

总结:

  • 词的表示为十进制(123), 二进制(0b110), 八进制(0567), 十六进制(0xff1)
  • 词的转换函数如下:
十进制 二进制 八进制 十六进制
表示 123 0b110 0567 0xff1
函数 int bin oct hex

map函数的使用

定义

  • Python3

    1
    map(function, iterable, ...) -> map
  • Python2

    1
    map(function, iterable, ...) -> list

示例

1
2
3
4
5
6
7
8
9
10
11
12
# Python2
print map(lambda x: x**2, [1, 2, 3])
# output:
[1, 4, 9]

print map(lambda x, y: x+y, [1, 2, 3], [5, 6, 7])
# output:
[6, 8, 10]

# Python3
result = map(lambda x: x**2, [1, 2, 3])
print(list(result))

Python——常见函数总结


chain()

  • chain() 是 itertools 模块提供的一个非常实用的函数,它能够把多个可迭代对象连接起来,形成一个连续的迭代器

  • 基本用法为:

    1
    2
    from itertools import chain
    chain(*iterables) # 参数是多个可迭代对象(注意是展开形式)
  • 链接多个可迭代对象,可以是不同对象,对象内部的元素也可以是不同类型

    1
    2
    3
    4
    5
    6
    7
    8
    from itertools import chain

    set1 = {1, '2'}
    set2 = {3, 4}
    list1 = [5, 6]

    combined = list(chain(set1, set2, list1))
    print(combined) # 输出:[1, '2', 3, 4, 5, 6](集合元素顺序可能不同)
  • 特别说明,chain() 对可迭代对象是依次访问的,只有迭代访问完前面一个采集继续访问另一个


zip() 函数

  • 当 zip 函数输入长度不一致时,不会报错,会适配最短长度
  • 示例:
    1
    2
    3
    4
    5
    6
    for a, b in zip([1,2,3],[1,4,4,5,4,5,6,7]):
    print(a, b)

    # 1 1
    # 2 4
    # 3 4

for…else 语句

  • 在 Python 中,for...else 是一种特殊的流程控制结构,for...else 的核心是通过 else 子句判断循环是否“自然完成”,而非被 break 强制终止,这在需要验证“遍历完整性”的场景中非常实用

  • 其基本语法为:

    1
    2
    3
    4
    for 变量 in 可迭代对象:
    循环体(当迭代继续时执行)
    else:
    else子句(当循环正常结束时执行)
  • 核心逻为:

    • else 子句的执行条件 :当 for 循环完整遍历了所有元素(即没有通过 break 语句提前退出循环),则循环结束后会执行 else 中的代码
    • else 子句不执行的情况 :如果循环中遇到 break 语句并跳出循环(提前终止),则 else 子句不会执行
  • 示例 1:循环正常结束(执行 else)

    1
    2
    3
    4
    5
    6
    7
    8
    for i in range(3):
    print(i)
    else:
    print("循环正常结束,执行 else") # 循环完整遍历了 `range(3)` 的所有元素(0、1、2),没有 `break`,因此 `else` 被执行
    # 0
    # 1
    # 2
    # 循环正常结束,执行 else
  • 示例 2:循环被 break 终止(不执行 else)

    1
    2
    3
    4
    5
    6
    7
    8
    for i in range(3):
    print(i)
    if i == 1:
    break # 当i=1时提前退出循环
    else:
    print("循环正常结束,执行 else") # 循环在 `i=1` 时通过 `break` 提前终止,因此 `else` 子句未执行
    # 0
    # 1
  • 典型应用场景:for...else 常用于判断“是否在循环中找到目标” ,例如在列表中查找元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    target = 5
    nums = [1, 3, 4, 6]

    for num in nums:
    if num == target:
    print(f"找到目标:{target}")
    break
    else:
    # 若循环完整执行(未触发break),说明未找到目标
    print(f"未找到目标:{target}") # 由于 `nums` 中没有 5,循环完整遍历后执行 `else`,提示“未找到目标”

    # 未找到目标:5

dict 特殊初始化逻辑

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    default_config = { # 默认值定义
    "pass_score": 60,
    "check_mode": "all",
    "time_limit": 30
    }

    custom_config = {
    "pass_score": 75, # 覆盖默认:及格线提高到75分
    "need_detail": True # 新增:需要详细报告
    }

    # 配置合并,第一个是默认配置,第二个是新增或修改配置
    final_config = {**default_config, **custom_config}

    print(f"最终配置:{final_config}")

    # 最终配置:{'pass_score': 75, 'check_mode': 'all', 'time_limit': 30, 'need_detail': True}
  • 注:Python 3.9+ 还支持更简洁的 | 字典合并运算符(效果完全一致):

    1
    grader_params = grader_config.defaults | grader_dict
  • 运行原代码后,grader_params 的结果的是:

    1
    2
    3
    4
    5
    6
    {
    "score_threshold": 70, # 自定义覆盖了默认
    "check_type": "full", # 保留默认(无自定义冲突)
    "timeout": 30, # 保留默认
    "check_detail": True # 新增自定义配置
    }

string 的 rindex 和 rfind 函数用法

  • rindex() 和 rfind() 是字符串(str)的内置方法,核心作用是从字符串右侧(末尾)开始查找指定子串的最后一次出现位置 ,返回其索引值

    • 两者用法高度相似,仅在“子串未找到”时的行为不同
      • rfind():未找到返回 -1
      • rindex():未找到抛出 ValueError 异常
    • 两者返回子串都是在原字符串中最后一次出现的起始索引(索引从 0 开始)
  • 完整语法

    1
    2
    3
    4
    5
    # rfind():未找到返回 -1
    str.rfind(sub, start=0, end=len(str))

    # rindex():未找到抛出 ValueError 异常
    str.rindex(sub, start=0, end=len(str))
    • sub:必需参数,要查找的子串(可以是单个字符、多个字符的字符串)
    • start:可选参数,查找的起始索引(从左往右的索引,默认 0,即从字符串开头开始限定查找范围)
    • end:可选参数,查找的结束索引(不包含 end 本身,默认 len(str),即到字符串末尾结束)
  • 选择建议:如果不确定子串是否存在,优先用 rfind()(避免异常);如果明确子串一定存在,用 rindex()(逻辑更简洁)

使用示例

  • 基础用法(无 start/end 参数):从字符串末尾开始查找子串的最后一次出现位置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    s = "hello world, hello python"

    # 查找 "hello" 的最后一次出现
    print(s.rfind("hello")) # 输出 13(第二个 "hello" 的起始索引)
    print(s.rindex("hello")) # 输出 13

    # 查找单个字符 "o" 的最后一次出现
    print(s.rfind("o")) # 输出 18("python" 中的 "o")
    print(s.rindex("o")) # 输出 18

    # 查找不存在的子串
    print(s.rfind("java")) # 输出 -1(不报错)
    # print(s.rindex("java")) # 抛出 ValueError: substring not found
  • 指定 start/end 参数(限定查找范围):start 和 end 限定“从原字符串的 [start, end) 区间内,从右往左查找”:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    s = "abcdeabcde"  # 索引 0-9

    # 限定查找范围:start=2,end=8(即子字符串 "cdeabc",索引 2-7)
    # 从该区间右侧开始找 "abc",最后一次出现是索引 5
    print(s.rfind("abc", 2, 8)) # 输出 5
    print(s.rindex("abc", 2, 8)) # 输出 5

    # 限定范围:start=0,end=5(子字符串 "abcde")
    # 该区间内 "abc" 只在索引 0 出现,从右找也是 0
    print(s.rfind("abc", 0, 5)) # 输出 0
  • 查找空字符串(特殊情况):如果 sub 是空字符串 "",两者都会返回 end - 1(因为空字符串在任何位置都存在,最后一次位置是 end 前一个索引):

    1
    2
    3
    s = "test"
    print(s.rfind("")) # 新版本输出4,3.9前的旧版本输出 3,
    print(s.rindex("", 0, 2)) # 新版本输出2,3.9前的旧版本输出 1

附录:与 index()/find() 的对比

  • find() / index():从左侧开始查找子串的第一次出现位置(未找到时 find 返回 -1,index 抛异常)
  • rfind() / rindex():从右侧开始查找子串的最后一次出现位置(未找到时行为同上)
  • 示例对比:
    1
    2
    3
    4
    5
    s = "abacada"
    print(s.find("a")) # 0(左侧第一次)
    print(s.rfind("a")) # 6(右侧第一次,即最后一次)
    print(s.index("a")) # 0
    print(s.rindex("a")) # 6

Java——自定义对象作为Map的Key


自定义的HashKey类

什么样的自定义类可以用作HashMap的Key?

  • 实现了HashCode方法和equals方法的类

作为Key后的对象有什么要求?

  • 该对象的值不能再修改,否则将导致containsKey找不到已经存储的Key
  • 注意,Key值被修改后是无论如何都找不到的,因为hash对象变化导致hash方式变了
    • 能正确找到hash桶的对象与目标对象(修改后的值)不相等
    • 与对象与目标对象(修改后的值)相等的对象找不到正确的Hash桶
    • 除非将修改的值改回来

拓展

  • 如果是使用TreeMap,则不是考虑hashCode方法,而是其他方法

DL——RNN

本文将介绍一般RNN,RNN的变种GRU(门控循环单元)和LSTM(长短期记忆网络)等神经网络结构

  • LSTM原始论文: NIPS 2014, Sequence to Sequence Learning with Neural Networks
  • GRU原始论文: EMNLP 2014, Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation

一般的RNN

循环神经网络(Recurrent Neural Network,RNN)

  • 将神经单元展开后的示意图如下:

RNN与CNN优缺点比较

此处我们之比较 CNN 和 RNN 在序列预测问题中的优劣

  • 关于前馈神经网络:
    • 是一种最简单的神经网络
    • 各神经元分层排列,每个神经元只接受前一层的输入,输出到下一层,已知到输出层
    • 整个网络中没有反馈(后面的 backward 求梯度不能算反馈,这里指一次输入到输出计算各层间没有反馈,数据流只能从上一层到下一层单向流动)
    • 可以用一个有向无环图表示
  • CNN:
    • 是一种前馈神经网络(多层感知机),是多层感知机的变体
    • 采用固定大小的输入并生成固定大小的输出
    • CNN是图像和视频处理的理想选择
  • RNN:
    • 不是前馈神经网络,有环
    • 可以处理任意长度的输入输出
    • 使用时间序列信息,能用之前的状态影响下一个状态
    • RNN是文本和语音分析的理想选择
  • RNN在处理序列信息的缺点:
    • 对短期输入非常敏感,但是对长期输入不敏感
    • 难以处理长文本中长距离的单词间的关系

LSTM

长短期记忆网络(Long Short-Term Memory, LSTM)

  • 整体示意图如下:
  • 核心是”(Gate)门”结构
    • 其中Sigmoid是输出0到1的数值,描述每个部分有多少量可以通过当前门
    • 0表示拒绝所有,1表示接受所有
  • 多个门和状态共同作用,使LSTM能够有效捕捉长期依赖关系并避免梯度消失问题:
    • 细胞状态 \( C_t \)部分 :长期记忆,传递历史信息
    • 隐藏状态 \( h_t \)部分 :短期记忆,当前时间步的输出
    • 输入门 \( i_t \)部分 :控制新信息的加入
    • 遗忘门 \( f_t \)部分 :控制历史信息的保留
    • 输出门 \( o_t \)部分 :控制细胞状态对隐藏状态的影响
    • 候选细胞状态 \( \tilde{C}_t \)部分 :当前时间步的候选更新值

遗忘门(Forget Gate,\( f_t \) )

  • 作用 :控制前一个细胞状态 \( C_{t-1} \) 的遗忘程度:\(f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)\),输出在0到1之间,决定保留或丢弃多少历史信息

输入门(Input Gate,\( i_t \))和 候选细胞状态(Candidate Cell State,\( \tilde{C}_t \))

  • 输入门作用 :控制当前输入信息对细胞状态的更新程度, \(i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)\),输入门的输出在0到1之间,决定哪些新信息需要加入细胞状态
  • 候选细胞状态作用 :表示当前时间步的候选更新值, \(\tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C)\),用于更新细胞状态,候选细胞状态是当前输入和前一隐藏状态的非线性组合

细胞状态(Cell State, \( C_t \) )

  • 细胞状态 \( C_t \) 作用 :细胞状态是LSTM的核心,负责长期记忆的传递。\(C_t = f_t \cdot C_{t-1} + i_t \cdot \tilde{C}_t\),它像一个“传送带”,能够在时间步之间传递信息,并通过门控机制(遗忘门控制历史信息的保留,输入门控制新信息的加入)决定保留或丢弃哪些信息,细胞状态的变化相对平缓,能够捕捉长期依赖关系,通过遗忘门和输入门更新:

输出门(Output Gate,\( o_t \))和 隐藏状态(Hidden State,\( h_t \))

  • 输出门 \( o_t \) 作用 :控制细胞状态对当前隐藏状态的影响程度,\(o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)\),输出门的输出在0到1之间,决定细胞状态如何影响当前隐藏状态
  • 隐藏状态 \( h_t \) 作用 :隐藏状态是LSTM的短期记忆,表示当前时间步的输出,\(h_t = o_t \cdot \tanh(C_t)\),它基于细胞状态和当前输入生成(输出门控制细胞状态对当前隐藏状态的影响,生成当前时间步的输出),并传递到下一个时间步,隐藏状态是LSTM对外的输出(即 output_t),它既用于当前时间步的输出,也用于下一个时间步的计算
  • 特别说明 :LSTM中的第 \(t\) 步的输出不是 \(o_t\)(\(o_t\)是输出门,而不是输出), LSTM的通用输出就是 \(h_t\) 本身,但具体将LSTM应用到不同场景中时,可以接入不同的层来将 \(h_t\) 解码/转换为真实的输出 \(output_t = TransLayer(h_t)\)
  • 补充问题 :在文本翻译中,解码阶段,是否LSTM的 \(h_{t-1}\) 和 \(x_t\) 相等?一般不相等,因为解码时有:
    $$x_t = Embedding(output_{t-1}) = Embedding(TransLayer(h_{t-1}))$$
    • 比如在文本翻译中 \(h_{t-1}\) 是一个向量,而 \(output_{t-1}\) 是一个具体的 token,而上述公式中的 \(x_t \) 则一般是输入 \(input_t\)(\(= output_{t-1}\)) 经过 Embedding 后的向量

LSTM简单代码实现

  • LSTM的PyTorch实现Demo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 定义 LSTM 单元
    class LSTMCell(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(LSTMCell, self).__init__()
    self.input_size = input_size
    self.hidden_size = hidden_size
    # 定义门参数
    self.W_i = nn.Linear(input_size + hidden_size, hidden_size) # 输入门参数
    self.W_f = nn.Linear(input_size + hidden_size, hidden_size) # 遗忘门参数
    self.W_c = nn.Linear(input_size + hidden_size, hidden_size) # 候选记忆单元参数
    self.W_o = nn.Linear(input_size + hidden_size, hidden_size) # 输出门参数

    def forward(self, x, h_prev, c_prev):
    combined = torch.cat((x, h_prev), dim=1)
    i = torch.sigmoid(self.W_i(combined))
    f = torch.sigmoid(self.W_f(combined))
    c_tilde = torch.tanh(self.W_c(combined))
    o = torch.sigmoid(self.W_o(combined))
    c_next = f * c_prev + i * c_tilde
    h_next = o * torch.tanh(c_next)
    return h_next, c_next

GRU

门控循环单元(Gated Recurrent Unit, GRU)

  • 整体示意图及推导如下:

  • 特别说明 :上述公式中,没有明确包含输出 \(output_t\),GRU中的第 \(t\) 步的通用输出和LSTM一致,就是 \(h_t\) 本身,但具体将GRU应用到不同场景中时,可以接入不同的层来将 \(h_t\) 解码/转换为真实的输出 \(output_t = TransLayer(h_t)\)

GRU简单代码实现

  • GRU的PyTorch实现Demo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 定义 GRU 单元
    class GRUCell(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(GRUCell, self).__init__()
    self.input_size = input_size
    self.hidden_size = hidden_size

    # 定义门参数
    self.W_z = nn.Linear(input_size + hidden_size, hidden_size)
    self.W_r = nn.Linear(input_size + hidden_size, hidden_size)
    self.W_h = nn.Linear(input_size + hidden_size, hidden_size)

    def forward(self, x, h_prev):
    combined = torch.cat((x, h_prev), dim=1)
    z = torch.sigmoid(self.W_z(combined))
    r = torch.sigmoid(self.W_r(combined))
    h_tilde = torch.tanh(self.W_h(torch.cat((x, r * h_prev), dim=1)))
    h_next = (1 - z) * h_prev + z * h_tilde
    return h_next

GRU与LSTM对比

  • 二者提出的时间基本相同
  • 都是用来解决RNN不能处理长期依赖的问题
  • LSTM有三个”门”结构, GRU只有两个”门结构”
  • GRU较简单,参数比较少,不容易过拟合,LSTM 比较复杂一点,但是常用

附录:一个基于nn.LSTM的Decoder-Encoder翻译模型实现

  • 一个基于nn.LSTM的Decoder-Encoder翻译实现示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    import torch
    import torch.nn as nn
    import torch.optim as optim

    # 编码器
    class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(Encoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(input_size, hidden_size)
    self.lstm = nn.LSTM(hidden_size, hidden_size)

    def forward(self, input, hidden):
    embedded = self.embedding(input).view(1, 1, -1)
    output = embedded
    output, hidden = self.lstm(output, hidden)
    return output, hidden

    def initHidden(self):
    return (torch.zeros(1, 1, self.hidden_size),
    torch.zeros(1, 1, self.hidden_size))

    # 解码器
    class Decoder(nn.Module):
    def __init__(self, hidden_size, output_size):
    super(Decoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(output_size, hidden_size)
    self.lstm = nn.LSTM(hidden_size, hidden_size)
    self.out = nn.Linear(hidden_size, output_size)
    self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
    output = self.embedding(input).view(1, 1, -1)
    output = torch.relu(output)
    output, hidden = self.lstm(output, hidden)
    output = self.softmax(self.out(output[0]))
    return output, hidden

    def initHidden(self):
    return (torch.zeros(1, 1, self.hidden_size),
    torch.zeros(1, 1, self.hidden_size))


    # 数据集
    source_vocab = {'hello': 0, 'world': 1, '!': 2}
    target_vocab = {'你好': 0, '世界': 1, '!': 2, '<EOS>': 3}
    input_size = len(source_vocab)
    output_size = len(target_vocab)
    hidden_size = 256

    encoder = Encoder(input_size, hidden_size)
    decoder = Decoder(hidden_size, output_size)

    # 合并编码器和解码器的参数
    params = list(encoder.parameters()) + list(decoder.parameters())

    # 创建一个优化器来更新所有参数
    optimizer = optim.SGD(params, lr=0.01)
    criterion = nn.NLLLoss()

    # 训练数据
    train_data = [
    (["hello", "world", "!"], ["你好", "世界", "!", "<EOS>"])
    ]

    # 训练
    for epoch in range(100):
    total_loss = 0
    for src, tgt in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    target_tensor = torch.tensor([target_vocab[word] for word in tgt])

    optimizer.zero_grad()

    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    loss = 0
    for di in range(len(target_tensor)):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1)
    decoder_input = topi.squeeze().detach()
    loss += criterion(decoder_output, target_tensor[di].unsqueeze(0))

    loss.backward()
    optimizer.step()
    total_loss += loss.item()

    if (epoch + 1) % 10 == 0:
    print(f'Epoch {epoch + 1}, Loss: {total_loss / len(train_data)}')


    # 评估
    reverse_target_vocab = {idx: word for word, idx in target_vocab.items()}
    for src, _ in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    decoded_words = []
    for di in range(10):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1) # topk(1)函数在默认维度(最后一维)上抽取最大的1个数字,并返回其在这个维度上的index
    decoded_word = reverse_target_vocab[topi.item()]
    decoded_words.append(decoded_word)
    if decoded_word == '<EOS>':
    break
    decoder_input = topi.squeeze().detach()

    print(f'Input: {" ".join(src)}, Translation: {" ".join(decoded_words)}')

    # Epoch 10, Loss: 4.957141399383545
    # Epoch 20, Loss: 4.250643253326416
    # Epoch 30, Loss: 3.5705487728118896
    # Epoch 40, Loss: 2.95521879196167
    # Epoch 50, Loss: 2.438739538192749
    # Epoch 60, Loss: 2.025095224380493
    # Epoch 70, Loss: 1.698549509048462
    # Epoch 80, Loss: 1.438602328300476
    # Epoch 90, Loss: 1.2278329133987427
    # Epoch 100, Loss: 1.0538270473480225
    # Input: hello world !, Translation: 你好 世界 ! <EOS>

附录:一个基于 nn.GRU 的 Decoder-Encoder 翻译模型实现

  • 一个基于 nn.GRU 的 Decoder-Encoder 翻译实现示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    import torch
    import torch.nn as nn
    import torch.optim as optim

    # 编码器
    class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size):
    super(Encoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(input_size, hidden_size)
    self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
    embedded = self.embedding(input).view(1, 1, -1)
    output = embedded
    output, hidden = self.gru(output, hidden)
    return output, hidden

    def initHidden(self):
    return torch.zeros(1, 1, self.hidden_size)

    # 解码器
    class Decoder(nn.Module):
    def __init__(self, hidden_size, output_size):
    super(Decoder, self).__init__()
    self.hidden_size = hidden_size
    self.embedding = nn.Embedding(output_size, hidden_size)
    self.gru = nn.GRU(hidden_size, hidden_size)
    self.out = nn.Linear(hidden_size, output_size)
    self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
    output = self.embedding(input).view(1, 1, -1)
    output = torch.relu(output)
    output, hidden = self.gru(output, hidden)
    output = self.softmax(self.out(output[0]))
    return output, hidden

    def initHidden(self):
    return torch.zeros(1, 1, self.hidden_size) # 这里LSTM是2个隐向量


    # 数据集
    source_vocab = {'hello': 0, 'world': 1, '!': 2}
    target_vocab = {'你好': 0, '世界': 1, '!': 2, '<EOS>': 3}
    input_size = len(source_vocab)
    output_size = len(target_vocab)
    hidden_size = 256

    encoder = Encoder(input_size, hidden_size)
    decoder = Decoder(hidden_size, output_size)

    # 合并编码器和解码器的参数
    params = list(encoder.parameters()) + list(decoder.parameters())

    # 创建一个优化器来更新所有参数
    optimizer = optim.SGD(params, lr=0.01)
    criterion = nn.NLLLoss()

    # 训练数据
    train_data = [
    (["hello", "world", "!"], ["你好", "世界", "!", "<EOS>"])
    ]

    # 训练
    for epoch in range(100):
    total_loss = 0
    for src, tgt in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    target_tensor = torch.tensor([target_vocab[word] for word in tgt])

    optimizer.zero_grad()

    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    loss = 0
    for di in range(len(target_tensor)):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1)
    decoder_input = topi.squeeze().detach()
    loss += criterion(decoder_output, target_tensor[di].unsqueeze(0))

    loss.backward()
    optimizer.step()
    total_loss += loss.item()

    if (epoch + 1) % 10 == 0:
    print(f'Epoch {epoch + 1}, Loss: {total_loss / len(train_data)}')


    # 评估
    reverse_target_vocab = {idx: word for word, idx in target_vocab.items()}
    for src, _ in train_data:
    input_tensor = torch.tensor([source_vocab[word] for word in src])
    encoder_hidden = encoder.initHidden()
    for ei in range(len(input_tensor)):
    encoder_output, encoder_hidden = encoder(input_tensor[ei].unsqueeze(0), encoder_hidden)

    decoder_input = torch.tensor([[0]])
    decoder_hidden = encoder_hidden
    decoded_words = []
    for di in range(10):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    topv, topi = decoder_output.topk(1) # topk(1)函数在默认维度(最后一维)上抽取最大的1个数字,并返回其在这个维度上的index
    decoded_word = reverse_target_vocab[topi.item()]
    decoded_words.append(decoded_word)
    if decoded_word == '<EOS>':
    break
    decoder_input = topi.squeeze().detach()

    print(f'Input: {" ".join(src)}, Translation: {" ".join(decoded_words)}')

    # Epoch 10, Loss: 4.1139960289001465
    # Epoch 20, Loss: 2.4189529418945312
    # Epoch 30, Loss: 1.5635995864868164
    # Epoch 40, Loss: 1.0992240905761719
    # Epoch 50, Loss: 0.8163852691650391
    # Epoch 60, Loss: 0.6290442943572998
    # Epoch 70, Loss: 0.49852463603019714
    # Epoch 80, Loss: 0.4044916033744812
    # Epoch 90, Loss: 0.3349364101886749
    # Epoch 100, Loss: 0.2822900116443634
    # Input: hello world !, Translation: 你好 世界 ! <EOS>
  • 与LSTM实现的主要区别是,output, hidden = self.gru(output, hidden) 中输入和输出的 hiden 的数量不同,LSTM 包含两个向量(\(C_t\) 和 \(h_t\)),而 GRU 只包含一个 \(h_t\)

DL——模型训练预热


整体说明

  • 预热(Warm-up)是一种训练技巧:
    • 在模型训练初期采用一些策略,逐步调整超参数(如学习率、 Batch Size 大小等)或模型状态 ,使得训练过程更加稳定、高效的初始化阶段
    • 通过合理预热,可以显著提升训练稳定性、收敛速度和最终性能
  • 预热的核心目的是避免训练初期因参数随机初始化或学习率过高导致的梯度不稳定、收敛困难等问题
  • 常见的预热技术主要包含两类:
    • 学习率预热(Learning Rate Warm-up) :训练初期从极小的学习率(如0)逐步线性或非线性增加到预设值
    • 优化器预热 :
      • Adam 预热阶段可用小学习率,比如正常值的 \(1/10\)(Adam 优化器的自适应动量在初期可能不准确);
      • Adam 在预热阶段启用偏差修正 ,避免初期估计偏差过大
  • 其他预热技术还包括:Batch Size 预热(Batch Size 从小到大),模型参数预热(逐步解冻模型层) 和 混合精度预热等(初期禁用混合精度)
  • 最常见的预热技术是学习率预热,其中 Transformer 常使用 学习率线性预热(比如 BERT 训练中常用 10,000 步线性预热)
  • 术语:warm-up ratio
    • 如 warm-up ratio 等于 0.03,表示 warm-up 阶段(学习率上升阶段)步数占总训练阶段步数的 3%

学习率预热的相关策略

  • 学习率预热(Learning Rate Warm-up)是训练初期逐步增加学习率的策略,旨在稳定训练并提升最终性能。以下是常见的具体方法及其细节:

线性预热(Linear Warm-up)

  • 在预热步数 \(N\) 内,学习率从 \(0\)(或极小值 \(\epsilon\))线性增长到初始学习率 \(lr_{\text{base} }\)
    $$
    lr_t = \epsilon + \left(\frac{t}{N}\right) \cdot (lr_{\text{base} } - \epsilon)
    $$
    • 其中 \(t\) 是当前步数,\(t \leq N\)
  • 最常用的方式之一

余弦预热(Cosine Warm-up)

  • 结合余弦函数曲线调整学习率,初期缓慢增长,后期平滑过渡到目标值
    $$
    lr_t = \frac{1}{2} \left(1 + \cos\left(\pi \cdot \left(1 - \frac{t}{N}\right)\right)\right) \cdot lr_{\text{base} }
    $$
  • 注:也可与余弦退火结合,预热后直接进入衰减阶段
  • 更平滑的过渡,减少初期学习率突变
  • 一些大模型中会使用到

指数预热(Exponential Warm-up)

  • 学习率从 \(\epsilon\) 开始指数增长到 \(lr_{\text{base} }\)
    $$
    lr_t = \epsilon \cdot \left(\frac{lr_{\text{base} } }{\epsilon}\right)^{\frac{t}{N} }
    $$
  • 较少使用,因可能过早进入高学习率阶段

阶梯预热(Step Warm-up)

  • 将预热阶段分为多个离散区间,逐步跳跃式增加学习率

附录:torch 自带预热和学习率调度代码示例

  • 一个完整的PyTorch示例:先进行学习率预热,再正常训练模型

  • 以简单的图像分类任务(CIFAR-10)为基础,结合线性预热和余弦退火调度器

  • 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.optim.lr_scheduler import LambdaLR, CosineAnnealingLR
    from torchvision import datasets, transforms
    from torch.utils.data import DataLoader
    import matplotlib.pyplot as plt

    class SimpleCNN(nn.Module):
    def__init__(self):
    super().__init__()
    self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
    self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
    self.fc = nn.Linear(32 * 8 * 8, 10) # CIFAR-10输入为32x32,经过两次池化后为8x8
    self.pool = nn.MaxPool2d(2, 2)
    self.relu = nn.ReLU()

    def forward(self, x):
    x = self.pool(self.relu(self.conv1(x)))
    x = self.pool(self.relu(self.conv2(x)))
    x = x.view(-1, 32 * 8 * 8)
    x = self.fc(x)
    return x

    transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])
    train_set = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    train_loader = DataLoader(train_set, batch_size=128, shuffle=True)

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = SimpleCNN().to(device)
    ## 注:学习率包含在优化器 optimizer 中,使用不同的学习率调度器来执行 step,就可以实现不同的学习率调度
    optimizer = optim.AdamW(model.parameters(), lr=0.001) # 初始学习率设为0.001(预热目标值)

    warmup_steps = 500 # 预热步数
    total_steps = 5000 # 总训练步数

    # 线性预热函数
    def warmup_lambda(current_step):
    if current_step < warmup_steps:
    return float(current_step) / float(max(1, warmup_steps))
    else:
    return 1.0 # 预热结束后保持学习率

    # 预热阶段调度器
    warmup_scheduler = LambdaLR(optimizer, lr_lambda=warmup_lambda) # 基于优化器初始化调度器

    # 预热后的余弦退火调度器(从预热结束开始)
    cosine_scheduler = CosineAnnealingLR(
    optimizer, # 与预热阶段调度器初始化相同的优化器
    T_max=total_steps - warmup_steps, # 余弦周期长度
    eta_min=1e-6 # 最小学习率
    )

    criterion = nn.CrossEntropyLoss()
    lr_history = []
    for step in range(total_steps):
    inputs = torch.randn(128, 3, 32, 32).to(device)
    labels = torch.randint(0, 10, (128,)).to(device)

    optimizer.zero_grad()
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

    # 更新学习率
    if step < warmup_steps:
    warmup_scheduler.step() # 预热阶段,step 函数会按照 warmup_scheduler 的定义来修改学习率
    else:
    cosine_scheduler.step() # 预热后余弦退火,step 函数会按照 cosine_scheduler 的定义来修改学习率

    # 记录学习率,可打印出来观测
    lr_history.append(optimizer.param_groups[0]['lr'])

    if step % 200 == 0:
    print(f"Step {step}: LR = {optimizer.param_groups[0]['lr']:.6f}, Loss = {loss.item():.4f}")
  • 预热阶段(前500步):学习率从 0 线性增长到初始值 0.001
    $$ lr = \text{base_lr} \times \frac{\text{current_step} }{\text{warmup_steps} } $$

  • 正常训练阶段(500步后):切换为余弦退火调度器(CosineAnnealingLR),学习率从 0.001 逐渐衰减到 1e-6

    • 注: 余弦退火的周期长度 \( T_{\text{max} } \) 设为总步数减去预热步数
  • 总体来说,学习率曲线是先线性上升,后余弦式下降(平滑振荡衰减)的过程


附录:transformers 库的模型训练预热调度示例

  • transformers 库中使用模型训练预热代码(按照初始学习率 1e-4, epochs= )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    import matplotlib.pyplot as plt
    import transformers
    import torch

    initial_lr = 1.0e-4 # 初始学习率
    warmup_ratio = 0.1 # 预热比例

    num_training_steps = 1000 # 总训练 step 数
    num_warmup_steps = int(num_training_steps * warmup_ratio) # 计算 warmup 的 step 数

    optimizer = torch.optim.AdamW([torch.tensor(0.0)], lr=initial_lr) # [torch.tensor(0.0)] 是虚拟的模型参数,可随意设置

    # 使用 transformers 库创建余弦退火学习率调度器
    lr_scheduler = transformers.get_cosine_schedule_with_warmup(
    optimizer=optimizer,
    num_warmup_steps=num_warmup_steps, # warmup step 数
    num_training_steps=num_training_steps, # 训练总 step 数
    # num_cycles=0.5, # 对应 cosine 曲线的周期,默认值是0.5,也就是半周期(递减)
    # last_epoch=-1, # 用于从 checkpoint 启动时恢复训练,设置为 ckpt 对应 step-1 即可
    # 比如从第 500 步的 ckpt启动,设置为499,从第0步启动,设置为-1(默认值)
    )

    learning_rates = []
    for _ in range(num_training_steps):
    learning_rates.append(optimizer.param_groups[0]["lr"])
    lr_scheduler.step() # 更新 optimizer.param_groups[0]["lr"]

    # 设置中文字体
    plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

    # 以下为可视化代码
    plt.figure(figsize=(10, 6))
    plt.plot(learning_rates)
    plt.title('学习率变化曲线')
    plt.xlabel('训练步骤')
    plt.ylabel('学习率')
    plt.grid(True)
    plt.axvline(x=num_warmup_steps, color='r', linestyle='--', label='预热结束')
    plt.legend()

    plt.annotate(f'初始学习率: {initial_lr}', xy=(num_warmup_steps, initial_lr),
    xytext=(num_warmup_steps + 50, initial_lr * 1.5),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.annotate(f'预热起点: 0', xy=(0, 0),
    xytext=(50, initial_lr * 0.2),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.annotate(f'最终学习率: {learning_rates[-1]:.8f}', xy=(num_training_steps-1, learning_rates[-1]),
    xytext=(num_training_steps-200, learning_rates[-1] * 10),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.tight_layout()
    plt.savefig('warmup_learning_rate_curve_cycles_0.5.png', dpi=300)
    # plt.show()
  • 可视化结果(半周期余弦 num_cycles=0.5 的结果):

    • warmup 阶段,学习率从 0 开始逐步提升到最大值
    • 正式训练阶段,学习率按照余弦调度器波动
  • 如果设置为 num_cycles=1,则会在指定训练步数内完成两个周期的学习率变化:

  • 如果设置为 num_cycles=1.5,则会在指定训练步数内完成两个周期的学习率变化:

  • 如果设置为 num_cycles=2,则会在指定训练步数内完成两个周期的学习率变化:


附录:预热有什么用?

  • 解决梯度不稳定问题 :模型初始阶段参数随机初始化,直接使用高学习率可能导致梯度爆炸或震荡
  • 解决学习率敏感性问题 :过大的初始学习率可能使模型跳过最优解附近区域;过小则导致收敛缓慢
  • 保证优化器适应性 :如 Adam 等自适应优化器在初期需要积累梯度统计量(如动量、方差),预热阶段可为优化器提供更稳定的初始估计

附录:一般预热多少步更合适?

  • 预热步数通常取决于模型规模和数据集大小:
    • 小规模数据:数百到几千步
    • 大规模训练(如LLM):数万步甚至更长(例如 GPT-3 的数千批次预热)
  • 另一种设置方式是:通常为总训练步数的 5-10%(例如 BERT 的 10k 步预热,总步数 100k)
1…434445…61
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

608 posts
49 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4