采样方法(二)MCMC相关算法介绍及代码实现

2017-12-30 15:32:14 Dark_Scope 阅读数 10509更多

分类专栏: 机器学习
 
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

0.引子

书接前文,在采样方法(一)中我们讲到了拒绝采样、重要性采样一系列的蒙特卡洛采样方法,但这些方法在高维空间时都会遇到一些问题,因为很难找到非常合适的可采样Q分布,同时保证采样效率以及精准度。
本文将会介绍采样方法中最重要的一族算法,MCMC(Markov Chain Monte Carlo),在之前我们的蒙特卡洛模拟都是按照如下公式进行的:
E[f(x)]≈1m∑mi=1f(xi).  xi∼p.iid{E}[f(x)] \approx \frac{1}{m}\sum_{i=1}^m{f(x_i)}.\ \ x_i \sim p.iidE[f(x)]≈m1​i=1∑m​f(xi​).  xi​∼p.iid
我们的x都是独立采样出来的,而在MCMC中,它变成了
E[f(x)]≈1m∑mi=1f(xi).  (x0,x1,...,xm)∼MC(p){E}[f(x)] \approx \frac{1}{m}\sum_{i=1}^m{f(x_i)}.\ \ (x_0,x_1,...,x_m)\sim MC(p)E[f(x)]≈m1​i=1∑m​f(xi​).  (x0​,x1​,...,xm​)∼MC(p)
其中的MC(p)MC(p)MC(p)就是我们本文的主角之一,马尔可夫过程(Markov Process)生成的马尔可夫链(Markov Chain)。

1.Markov Chain(马尔可夫链)

序列的算法(一·a)马尔可夫模型中我们就说到了马尔可夫模型的马尔可夫链,简单来说就是满足马尔可夫假设
P(sn∣s0,s1,...,sn−1)=P(sn∣sn−1)P(s_n|s_0,s_1,...,s_{n-1}) = P(s_n|s_{n-1})P(sn​∣s0​,s1​,...,sn−1​)=P(sn​∣sn−1​)
的状态序列,由马尔可夫过程(Markov Process)生成。
一个马尔可夫过程由两部分组成,一是状态集合Ω\OmegaΩ,二是转移概率矩阵TTT。
其流程是:选择一个初始的状态分布π0\pi_0π0​,然后进行状态的转移:
πn=πn−1T\pi_n = \pi_{n-1}Tπn​=πn−1​T
得到的π0,π1,π2...πn\pi_0,\pi_1,\pi_2...\pi_nπ0​,π1​,π2​...πn​状态分布序列。

注意:我们在这里讲的理论和推导都是基于离散变量的,但其实是可以直接推广到连续变量。

马尔可夫链在很多场景都有应用,比如大名鼎鼎的pagerank算法,都用到了类似的转移过程;
马尔可夫链在某种特定情况下,有一个奇妙的性质:

在某种条件下,当你从一个分布π0\pi_0π0​开始进行概率转移,无论你一开始π0\pi_0π0​的选择是什么,最终会收敛到一个固定的分布π\piπ,叫做稳态(steady-state)。
稳态满足条件:
π=πT\pi = \pi Tπ=πT
这里可以参考《LDA数学八卦0.4.2》的例子,非常生动地描述了社会阶层转化的一个例子,也对MCMC作了非常好的讲解

书归正传,回到我们采样的场景,我们知道,采样的难点就在于概率密度函数过于复杂而无法进行有效采样,如果我们可以设计一个马尔可夫过程,使得它最终收敛的分布是我们想要采样的概率分布,不就可以解决我们的问题了么。

前面提到了在某种特定情况下,这就是所有MCMC算法的理论基础Ergodic Theorem:
如果一个离散马尔可夫链(x0,x1...xm)(x_0,x_1...x_m)(x0​,x1​...xm​)是一个与时间无关的Irreducible的离散,并且有一个稳态分布π\piπ,则:
E[f(x)]≈1m∑mi=1f(xi).  x∼π{E}[f(x)] \approx \frac{1}{m}\sum_{i=1}^m{f(x_i)}.\ \ x\sim \piE[f(x)]≈m1​i=1∑m​f(xi​).  x∼π
它需要满足的条件有这样几个,我们直接列在这里,不作证明:

1.Time homogeneous: 状态转移与时间无关,这个很好理解。
2.Stationary Distribution: 最终是会收敛到稳定状态的。
3.Irreducible: 任意两个状态之间都是可以互相到达的。
4.Aperiodic:马尔可夫序列是非周期的,我们所见的绝大多数序列都是非周期的。

虽然这里要求是离散的马尔可夫链,但实际上对于连续的场景也是适用的,只是转移概率矩阵变成了转移概率函数。

2.MCMC

在上面马尔可夫链中我们的所说的状态都是某个可选的变量值,比如社会等级上、中、下,而在采样的场景中,特别是多元概率分布,并不是量从某个维度转移到另一个维度,比如一个二元分布,二维平面上的每一个点都是一个状态,所有状态的概率和为1! 这里比较容易产生混淆,一定小心。

在这里我们再介绍一个概念:
Detail balance: 一个马尔可夫过程是细致平稳的,即对任意a,b两个状态:
π(a)Tab=π(b)Tba\pi(a)T_{ab} = \pi(b)T_{ba}π(a)Tab​=π(b)Tba​细致平稳条件也可以推导出一个非周期的马尔可夫链是平稳的,因为每次转移状态i从状态j获得的量与j从i获得的量是一样的,那毫无疑问最后πT=π\pi T=\piπT=π.

所以我们的目标就是需要构造这样一个马尔可夫链,使得它最后能够收敛到我们期望的分布π\piπ,而我们的状态集合其实是固定的,所以最终目标就是构造一个合适的T,就大功告成了。

一般来说我们有:
π(x)=π˜(x)Z\pi(x) = \frac {\tilde{\pi}(x)}{Z}π(x)=Zπ~(x)​
其中ZZZ是归一化参数Z=Σx′π˜(x′)Z=\Sigma_{x'}{\tilde{\pi}(x')}Z=Σx′​π~(x′),因为我们通常能够很方便地计算分子π˜(x)\tilde{\pi}(x)π~(x),但是分母的计算因为要枚举所有的状态所以过于复杂而无法计算。我们希望最终采样出来的样本符合π\piπ分布。

2.1.Metropolis

2.1.1原理描述

Metropolis算法算是MCMC的开山鼻祖了,它这里假设我们已经有了一个状态转移概率函数T来表示转移概率,T(a,b)表示从a转移到b的概率(这里T的选择我们稍后再说),显然通常情况下一个T是不满足细致平稳条件的:
π(a)T(a,b)≠π(b)T(b,a)\pi(a)T(a,b) \ne \pi(b)T(b,a)π(a)T(a,b)̸​=π(b)T(b,a)
所以我们需要进行一些改造,加入一项QQQ使得等式成立:
π(a)T(a,b)Q(a,b)=π(b)T(b,a)Q(b,a)\pi(a)T(a,b)Q(a,b) = \pi(b)T(b,a)Q(b,a)π(a)T(a,b)Q(a,b)=π(b)T(b,a)Q(b,a)
基于对称的原则,我们直接让
Q(a,b)=π(b)T(b,a).Q(b,a)=π(a)T(a,b).Q(a,b) = \pi(b)T(b,a).\\Q(b,a) = \pi(a)T(a,b).Q(a,b)=π(b)T(b,a).Q(b,a)=π(a)T(a,b).
所以我们改造后的满足细致平稳条件的转移矩阵就是:
T′(a,b)=T(a,b)Q(a,b)=T(a,b)(π(b)T(b,a))\begin{aligned}T'(a,b) &= T(a,b)Q(a,b)\\ &=T(a,b)\left(\pi(b)T(b,a)\right)\end{aligned}T′(a,b)​=T(a,b)Q(a,b)=T(a,b)(π(b)T(b,a))​
在Metropolis算法中,这个加入的这个Q项是此次转移的接受概率,是不是和拒绝采样有点神似。

MCMC采样算法
1.有初始状态x0x_0x0​
2.从t=0,1,2,3开始:

  • 根据T(x∣xt)T(x|x_t)T(x∣xt​)采样出x′x'x′
  • 以概率Q(xt,x′)=π(x′)T(x′,xt)Q(x_t,x')=\pi(x')T(x',x_t)Q(xt​,x′)=π(x′)T(x′,xt​)接受,即xt+1=x′x_{t+1}=x'xt+1​=x′
  • 否则使xt+1=xtx_{t+1} = x_txt+1​=xt​

但这里还有一个问题,我们的接受概率Q可能会非常小,而且其中还需要精确计算出π(x′)\pi(x')π(x′),这个我们之前提到了是非常困难的,再回到我们的细致平稳条件:
π(a)T(a,b)Q(a,b)=π(b)T(b,a)Q(b,a)\pi(a)T(a,b)Q(a,b) = \pi(b)T(b,a)Q(b,a)π(a)T(a,b)Q(a,b)=π(b)T(b,a)Q(b,a)
如果两边同时乘以一个数值,它也是成立的,比如
π(a)T(a,b)Q(a,b)∗10=π(b)T(b,a)Q(b,a)∗10\pi(a)T(a,b)Q(a,b)*10 = \pi(b)T(b,a)Q(b,a)*10π(a)T(a,b)Q(a,b)∗10=π(b)T(b,a)Q(b,a)∗10
所以我们可以同步放大Q(a,b)和Q(b,a),使得其中最大的一个值为1,也就是说:
Q¯¯¯(a,b)=min(1,Q(a,b)Q(b,a))=min(1,π(b)T(b,a)π(a)T(a,b))=min(1,π˜(b)T(b,a)π˜(a)T(a,b))\begin{aligned}\bar Q(a,b) &= \min\left(1,\frac{Q(a,b)}{Q(b,a)}\right)\\&= \min\left(1,\frac{\pi(b)T(b,a)}{\pi(a)T(a,b)}\right)\\&= \min\left(1,\frac{\tilde\pi(b)T(b,a)}{\tilde\pi(a)T(a,b)}\right)\end{aligned}Qˉ​(a,b)​=min(1,Q(b,a)Q(a,b)​)=min(1,π(a)T(a,b)π(b)T(b,a)​)=min(1,π~(a)T(a,b)π~(b)T(b,a)​)​
这样在提高接受率的同时,因为除式的存在我们还可以约掉难以计算的Z。

Metropolis-Hastings算法
1.有初始状态x0x_0x0​
2.从t=0,1,2,3…开始:

  • 根据T(x∣xt)T(x|x_t)T(x∣xt​)采样出x′x'x′
  • 以概率Q¯¯¯(a,b)=min(1,π˜(b)T(b,a)π˜(a)T(a,b))\bar Q(a,b)=\min\left(1,\frac{\tilde\pi(b)T(b,a)}{\tilde\pi(a)T(a,b)}\right)Qˉ​(a,b)=min(1,π~(a)T(a,b)π~(b)T(b,a)​)接受,即xt+1=x′x_{t+1}=x'xt+1​=x′
  • 否则使xt+1=xtx_{t+1} = x_txt+1​=xt​

2.1.2.代码实验

我们之前提到状态转移函数T的选择,可以看到如果我们选择一个对称的转移函数,即T(a,b)=T(b,a)T(a,b)=T(b,a)T(a,b)=T(b,a),上面的接受概率还可以简化为
Q¯¯¯(a,b)=min(1,π˜(b)π˜(a))\bar Q(a,b)=\min\left(1,\frac{\tilde\pi(b)}{\tilde\pi(a)}\right)Qˉ​(a,b)=min(1,π~(a)π~(b)​)
这也是一般Metropolis算法中采用的方法,T使用一个均匀分布即可,所有状态之间的转移概率都相同。
实验中我们使用一个二元高斯分布来进行采样模拟

其概率密度函数这样计算的,x是一个二维坐标:
p(x)=12πe−x(0)2−x(1)2p(x) = \frac{1}{2\pi}e^{-{x(0)}^2-{x(1)}^2}p(x)=2π1​e−x(0)2−x(1)2

  1. def get_p(x):
  2. # 模拟pi函数
  3. return 1/(2*PI)*np.exp(- x[0]**2 - x[1]**2)
  4. def get_tilde_p(x):
  5. # 模拟不知道怎么计算Z的PI,20这个值对于外部采样算法来说是未知的,对外只暴露这个函数结果
  6. return get_p(x)*20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

每轮采样的函数:

  1. def domain_random(): #计算定义域一个随机值
  2. return np.random.random()*3.8-1.9
  3. def metropolis(x):
  4. new_x = (domain_random(),domain_random()) #新状态
  5. #计算接收概率
  6. acc = min(1,get_tilde_p((new_x[0],new_x[1]))/get_tilde_p((x[0],x[1])))
  7. #使用一个随机数判断是否接受
  8. u = np.random.random()
  9. if u<acc:
  10. return new_x
  11. return x
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然后就可以完整地跑一个实验了:

  1. def testMetropolis(counts = 100,drawPath = False):
  2. plotContour() #可视化
  3. #主要逻辑
  4. x = (domain_random(),domain_random()) #x0
  5. xs = [x] #采样状态序列
  6. for i in range(counts):
  7. xs.append(x)
  8. x = metropolis(x) #采样并判断是否接受
  9. #在各个状态之间绘制跳转的线条帮助可视化
  10. if drawPath:
  11. plt.plot(map(lambda x:x[0],xs),map(lambda x:x[1],xs),'k-',linewidth=0.5)
  12. ##绘制采样的点
  13. plt.scatter(map(lambda x:x[0],xs),map(lambda x:x[1],xs),c = 'g',marker='.')
  14. plt.show()
  15. pass
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15


可以看到采样结果并没有想象的那么密集,因为虽然我们提高了接受率,但还是会拒绝掉很多点,所以即使采样了5000次,绘制的点也没有密布整个画面。

2.2.Gibbs Sampling

2.2.1.算法分析

通过分析Metropolis的采样轨迹,我们发现前后两个状态之间并没有特别的联系,新的状态都是从T采样出来的,而因为原始的分布很难计算,所以我们选择的T是均匀分布,因此必须以一个概率进行拒绝,才能保证最后收敛到我们期望的分布。
如果我们限定新的状态只改变原状态的其中一个维度,即
xnew=(x0t,x1t,..xjnew..,xnt)x_{new} = (x_t^0,x_t^1,..x_{new}^j..,x_t^n)xnew​=(xt0​,xt1​,..xnewj​..,xtn​)
只改变了其中第j个维度,则有π(xt)=P(x−jt)∗P(xjt∣x−jt)π(xnew)=P(x−jt)∗P(xjnew∣x−jt)\pi(x_{t}) = P(x_{t}^{-j})*P(x_{t}^j|x_{t}^{-j})\\\pi(x_{new}) = P(x_{t}^{-j})*P(x_{new}^j|x_{t}^{-j})π(xt​)=P(xt−j​)∗P(xtj​∣xt−j​)π(xnew​)=P(xt−j​)∗P(xnewj​∣xt−j​)其中x−jx^{-j}x−j表示除了第j元其他的变量。
所以有(以P(x−jt)P(x_{t}^{-j})P(xt−j​)为桥梁作转换很好得到):
π(xt)∗P(xjnew∣x−jt)=π(xnew)∗P(xjt∣x−jt)\pi(x_{t})*P(x_{new}^j|x_{t}^{-j}) = \pi(x_{new})*P(x_{t}^j|x_{t}^{-j})π(xt​)∗P(xnewj​∣xt−j​)=π(xnew​)∗P(xtj​∣xt−j​)

所以我们如果构造这样一个转移概率函数T:
1.如果状态a和b之间只在第j个元上值不同,则是的
$T(a,b)=P(b{(j)}|a{(-j)}) $
2.否则T(a,b)=0T(a,b) = 0T(a,b)=0

结论很清晰:这样一个转移概率函数T是满足细致平稳条件的,而且和Metropolis里面不同:它不是对称的
我们能够以1为概率接受它的转移结果。

以一个二元分布为例,在平面上:

A只能跳转到位于统一条坐标线上的B,C两个点,对于D,它无法一次转移到达,但是可以通过两次变换到达,仍然满足Irreducible的条件。

这样构造出我们需要的转移概率函数T之后,就直接得到我们的Gibbs采样算法了:

Gibbs Sampling算法
1.有初始状态x0x_0x0​
2.从t=0,1,2,3…开始进行循环采样:

  • 将xtx_txt​复制过来,主要是为了循环采样:xt+1=xtx_{t+1} = x_txt+1​=xt​
  • 枚举维度j = 0…N,修改每个维度的值:
      • 使得xjt+1∼P(xj∣x−jt+1)x_{t+1}^j\sim P(x^j|x_{t+1}^{-j})xt+1j​∼P(xj∣xt+1−j​)

2.2.2.代码实验

想必大家发现了,如果要写代码,我们必须要知道如何从P(xj∣x−jt+1)P(x^j|x_{t+1}^{-j})P(xj∣xt+1−j​)进行采样,这是一个后验的概率分布,在很多应用中是能够定义出函数表达的。

在我们之前实验的场景(二元正态分布),确实也能精确地写出这个概率分布的概率密度函数(也是一个正态分布)。

但退一步想,现在我们只关心一元的采样了,所以其实是有很多方法可以用到的,比如拒绝采样等等。

而最简单的,直接在这一维度上随机采几个点,然后按照它们的概率密度函数值为权重选择其中一个点作为采样结果即可。

代码里这样做的目的主要是为了让代码足够简单,只依赖一个均匀分布的随机数生成器。

  1. def partialSampler(x,dim):
  2. xes = []
  3. for t in range(10): #随机选择10个点
  4. xes.append(domain_random())
  5. tilde_ps = []
  6. for t in range(10): #计算这10个点的未归一化的概率密度值
  7. tmpx = x[:]
  8. tmpx[dim] = xes[t]
  9. tilde_ps.append(get_tilde_p(tmpx))
  10. #在这10个点上进行归一化操作,然后按照概率进行选择。
  11. norm_tilde_ps = np.asarray(tilde_ps)/sum(tilde_ps)
  12. u = np.random.random()
  13. sums = 0.0
  14. for t in range(10):
  15. sums += norm_tilde_ps[t]
  16. if sums>=u:
  17. return xes[t]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

主程序的结构基本上和之前是一样的,

  1. def gibbs(x):
  2. rst = np.asarray(x)[:]
  3. path = [(x[0],x[1])]
  4. for dim in range(2): #维度轮询,这里加入随机也是可以的。
  5. new_value = partialSampler(rst,dim)
  6. rst[dim] = new_value
  7. path.append([rst[0],rst[1]])
  8. #这里最终只画出一轮轮询之后的点,但会把路径都画出来
  9. return rst,path
  10. def testGibbs(counts = 100,drawPath = False):
  11. plotContour()
  12. x = (domain_random(),domain_random())
  13. xs = [x]
  14. paths = [x]
  15. for i in range(counts):
  16. xs.append([x[0],x[1]])
  17. x,path = gibbs(x)
  18. paths.extend(path) #存储路径
  19. if drawPath:
  20. plt.plot(map(lambda x:x[0],paths),map(lambda x:x[1],paths),'k-',linewidth=0.5)
  21. plt.scatter(map(lambda x:x[0],xs),map(lambda x:x[1],xs),c = 'g',marker='.')
  22. plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

采样的结果:

其转移的路径看到都是与坐标轴平行的直线,并且可以看到采样5000词的时候跟Metropolis相比密集了很多,因为它没有拒绝掉的点。

后注

本文我们讲述了MCMC里面两个最常见的算法Metropolis和Gibbs Sampling,以及它们各自的实现,从采样路径来看:

  • Metropolis是完全随机的,以一个概率进行拒绝;
  • 而Gibbs Sampling则是在某个维度上进行转移。


如果我们仍然希望最后使用独立同分布的数据进行蒙特卡洛模拟,只需要进行多次MCMC,然后拿每个MCMC的第n个状态作为一个样本使用即可。

完整的代码见链接:
https://github.com/justdark/dml/blob/master/dml/tool/mcmc.py
因为从头到尾影响分布的只有get_p()函数,所以如果我们想对其他分布进行实验,只需要改变这个函数的定义就好了,比如说我们对一个相关系数为0.5的二元正态分布,只需要修改get_p()函数:
p(x)=12π1−0.52√exp(−12(1−0.52)(x(0)2−x(0)x(1)+x(1)2))p(x) = \frac{1}{2\pi\sqrt{1-0.5^2}}exp\left({-\frac{1}{2(1-0.5^2)}({x(0)}^2 - x(0)x(1) + {x(1)}^2)}\right)p(x)=2π1−0.52​1​exp(−2(1−0.52)1​(x(0)2−x(0)x(1)+x(1)2))

  1. def get_p(x):
  2. return 1/(2*PI*math.sqrt(1-0.25))*np.exp(-1/(2*(1-0.25))*(x[0]**2 -x[0]*x[1] + x[1]**2))
  • 1
  • 2

就可以得到相应的采样结果:

而且因为这里并不要求p是一个归一化后的分布,可以尝试任何>0的函数,比如

  1. def get_p(x):
  2. return np.sin(x[0]**2 + x[1]**2)+1
  • 1
  • 2

也可以得到采样结果:

PPS

终于赶在2017年结束之前填了一个小坑,很久没写博客了。

Reference

【1】LDA数学八卦
【2】Pattern Recognition and Machine Learning
【3】Mathematicalmonk’s machine learning video

采样方法(二)MCMC相关算法介绍及代码实现的更多相关文章

  1. LabelRank非重叠社区发现算法介绍及代码实现(A Stabilized Label Propagation Algorithm for Community Detection in Networks)

    最近在研究基于标签传播的社区分类,LabelRank算法基于标签传播和马尔科夫随机游走思路上改装的算法,引用率较高,打算将代码实现,便于加深理解. 这个算法和Label Propagation 算法不 ...

  2. 快速掌握RabbitMQ(二)——四种Exchange介绍及代码演示

    在上一篇的最后,编写了一个C#驱动RabbitMQ的简单栗子,了解了C#驱动RabbitMQ的基本用法.本章介绍RabbitMQ的四种Exchange及各种Exchange的使用场景. 1 direc ...

  3. K-近邻算法介绍与代码实现

    声明:如需转载请先联系我. 最近学习了k近邻算法,在这里进行了总结. KNN介绍 k近邻法(k-nearest neighbors)是由Cover和Hart于1968年提出的,它是懒惰学习(lazy ...

  4. Linux IO调度器相关算法介绍(转)

    IO调度器(IO Scheduler)是操作系统用来决定块设备上IO操作提交顺序的方法.存在的目的有两个,一是提高IO吞吐量,二是降低IO响应时间.然而IO吞吐量和IO响应时间往往是矛盾的,为了尽量平 ...

  5. Hibernate基础学习(二)—Hibernate相关API介绍

    一.Hibernate的核心接口      所有的Hibernate应用中都会访问Hibernate的5个核心接口.      (1)Configuration接口: 配置Hibernate,启动Hi ...

  6. JVM学习总结二——垃圾回收算法

    昨天总结了JVM内存分区相关的知识,这次我们将来了解下JVM的另一个核心知识点——垃圾回收算法.这一部分其实并不太难,如果对操作系统的内存处理算法有所了解,那么这部分算法其实只看名字就能明白,两者在原 ...

  7. 【STL学习】堆相关算法详解与C++编程实现(Heap)

    转自:https://blog.csdn.net/xiajun07061225/article/details/8553808 堆简介   堆并不是STL的组件,但是经常充当着底层实现结构.比如优先级 ...

  8. 常用的排序算法介绍和在JAVA的实现(二)

    一.写随笔的原因:本文接上次的常用的排序算法介绍和在JAVA的实现(一) 二.具体的内容: 3.交换排序 交换排序:通过交换元素之间的位置来实现排序. 交换排序又可细分为:冒泡排序,快速排序 (1)冒 ...

  9. Jsoup代码解读之二-DOM相关对象

    Jsoup代码解读之二-DOM相关对象   之前在文章中说到,Jsoup使用了一套自己的DOM对象体系,和Java XML API互不兼容.这样做的好处是从XML的API里解脱出来,使得代码精炼了很多 ...

随机推荐

  1. linux 下使用dd制作启动U盘 安装linux

    1.找到U盘: sudo fdisk -l 2.卸载U盘:(这个不是必须,如果没有挂载u盘,可以省略) sudo umount /dev/sdb1 3.建立文件系统,格式化U盘: sudo mkfs. ...

  2. CentOS7.4中配置jdk环境

    参考:https://www.linuxidc.com/Linux/2016-09/135556.htm 1.下载jdk 首先创建安装包放置位置 mkdir -p /usr/local/java 然后 ...

  3. 删除顺序表L中下标为p(0<=p<=length-1)的元素,成功返回1,不成功返回0,并将删除元素的值赋给e

    原创:转载请注明出处. [天勤2-2]删除顺序表L中下标为p(0<=p<=length-1)的元素,成功返回1,不成功返回0,并将删除元素的值赋给e 代码: //删除顺序表L中下标为p(0 ...

  4. vue 中引入第三方js库

    以 jQuery 为例 一.绝对路径直接引入,全局可用 主入口页面 index.html 中用 script 标签引入: <script src="./static/jquery-1. ...

  5. 如何隐藏EFI分区?

      U盘制作PE 后多了个EFI分区, 傲梅分区助手,,里面可以进行隐藏分区   文章来源:刘俊涛的博客 欢迎关注,有问题一起学习欢迎留言.评论

  6. JavaWeb——Get、Post请求中文乱码问题

    最近在重温JavaWeb基础内容,碰到了之前也时常遇到的中文乱码问题,想着反正是经常要处理的,不如当即就把它整理出来放在博客里,省得遇到时再去到处搜. 1. Post请求乱码的解决方案: 手工创建一个 ...

  7. Androidstudio 编译慢 这样的体验肯定很多人都有!!!

    本人也是经历过的   在老板站在你身后  说看下你做的东西怎么样啦   然后你开始编译你刚写代码     然后过了老长一段时间    你默默的拿起水来喝   缓解尴尬   boss一直站在后面   忍 ...

  8. 网络编程三要素之IP

    用来标示我们计算机在互联网上的唯一性 每个设备在网络中的唯一标识 每台网络终端在网络中都有一个独立的地址,我们在网络中传输数据就是使用这个地址. ipconfig:查看本机IP192.168.12.4 ...

  9. Tornado实现监控数据实时展示

    前言: It has been a while since I last updated my blogs. 使用Tornado开发一个实时监控信息系统,其中包括 CUP.内存.网卡.磁盘使用率. 涉 ...

  10. 数据分析 - sql 业务相关练习题

    数据库    userinfo , orderinfo 表 两个 userId 彼此对应 题目 解题 不同月份的下单人数 用户在同一个月份会下多个单,这里进行去重 未支付的脏数据去除 统计用户三月份的 ...