Huffman编码压缩算法及其实现

哈弗曼编码是一个很经典的压缩算法,压缩率能达到50%,甚至更低。它的基本原理包括四个步骤: 统计文件中每个字符出现的频率。 构建一个哈弗曼树。建树的过程是不断的合并频率最小的两个节点,父亲节点的频率为两个孩子节点的频率之和。如此循环直到合并成一个根节点。叶子节点为不同的字符及其频率。 生成哈弗曼编码。从树根开始对树进行编码,比如进入左孩子的边标记为0,进入右孩子的边标记为1,这里的0和1都是二进制位。这样之后,每个叶子节点都有一个唯一的二进制编码,这就是哈弗曼编码。频率越低的字符哈弗曼编码越长,频率越高的字符哈弗曼编码越短,这样就能起到压缩的效果。 第二遍扫描文件,把字符转换为对应的哈弗曼编码,保存成压缩文件。 解压缩的过程就是解析二进制位,然后查找哈弗曼树,每找到一个叶子节点,就解析出一个字符,直到解析完所有二进制位。下面详细解释我的C++实现。 首先定义一个哈弗曼编码类,对外只提供压缩Compress和解压缩Decompress两个接口。值得注意的是有一个Node结构体,用于构成哈弗曼树的节点。此外count_node的key是字符频率,value是所在节点,且是multimap类型的,所以count_node会自动按字符频率有小到大排序,在构建哈弗曼树时,每次只需要取count_node的前两个节点进行合并即可。 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 class HuffmanCode { public: HuffmanCode(); void Compress(string src, string dest); void Decompress(string src, string dest); virtual ~HuffmanCode(); private: void CountLetter(string src); void ConstructHuffmanTree(); void GenerateHuffmanCode(); void WriteHuffmanCode(ofstream &os); void Compressing(string src, string dest); void InsertIntoHuffmanTree(char letter, string &code, int &k); void ConstructHuffmanTreeFromFile(ifstream &is); void Decompressing(ifstream &is, ofstream &os); map<char, int> letter_count; typedef struct Node { int id; bool is_leaf; char letter; int parent, lchild, rchild; Node() { } Node(int i, bool il, char lt, int p, int lc, int rc) : id(i), is_leaf(il), letter(lt), parent(p), lchild(lc), rchild(rc) { } }; multimap<int, Node> count_node; vector<Node> huffman_tree; map<char, vector<char>> letter_hcode; // hufman code for each letter }; 压缩函数Compress串起压缩的整个流程,包括统计字符频率、构建哈弗曼树、生成哈弗曼编码以及最后将原始文件转换成哈弗曼编码的二进制文件。 ...

August 18, 2016 · 3 min

还原谷歌PageRank算法真相

之前写了七篇博客详细介绍了搜索引擎的工作原理。彼时的搜索引擎主要讲查询和网页的相关性匹配,是动态的、在线的、实时的。相关性匹配有一个问题,网页很容易作弊,比如可以在一个网页中写满诸如“免费”、“美容”之类的垃圾关键词,进而提升查询相关性。但是用户在查询时,一定希望返回的网页比较权威可信,比如同样搜索“苹果电脑”,排名第一的应该是Apple的官网,而不应该是中关村在线之类的第三方网站。 权威性是一个静态的(或者说变化较慢的)衡量网页重要性的指标。但是应该怎样度量权威性呢,HITS算法使用authority来度量,即指向自身的网页数量越多,则自身的authority值越大。谷歌的PageRank算法是用PageRank值来衡量权威性的。HITS和PageRank一个比较大的区别是HITS和查询有关,而PageRank和查询无关,所以PageRank可以离线计算。下面主要介绍PageRank算法。 PageRank’s thesis is that a webpage is important if it is pointed to by other important pages. 我先不加解释的给出PageRank的公式,然后带领大家一步步推导出这个公式。 $$\pi^T=\pi^T(\alpha S+(1-\alpha)E)$$我们首先明确目标:PageRank计算的是网页的静态权威度(PR值),也就是如果给定了一个网络结构,则每个网页的PR值就可以通过PageRank算法计算出。假设网页\(P_i\)的PR值为\(r(P_i)\),则\(r(P_i)\)等于所有指向\(P_i\)的网页的PR值之和,即 $$\begin{equation}r(P_i)=\sum\limits_{P_j\in B_{P_i}}\frac{r(P_j)}{|P_j|}\end{equation}$$其中\(B_{P_i}\)为指向\(P_i\)的网页集合,\(|P_j|\)为\(P_j\)的出边的数量。这个式子很好理解,包括两方面内容:1)\(\sum\limits_{P_j\in B_{P_i}}\)表示如果指向\(P_i\)的网页数量越多,说明网页\(P_i\)越重要;2)\(\frac{r(P_j)}{|P_j|}\)表示如果\(P_j\)指向的页面数量越少,但有一个指向了\(P_i\),说明网页\(P_i\)越重要(如果一个大牛写了很多推荐信(\(|P_j|\)大),则这些推荐信的效力就下降了,如果大牛只给你写了推荐信(\(|P_j|=1\)),则这封推荐信的效力一定很高)。 (1)式有一个问题,初始给定一个网络结构时,并不知道\(r(P_i), r(P_j)\),如何计算呢?Brin和Page利用递归的思想求解,初始假设所有网页的PR值相等,都为\(\frac{1}{n}\),其中\(n\)为网络中网页的数量。则第\(k+1\)轮的PR计算公式为: $$\begin{equation}r_{k+1}(P_i)=\sum\limits_{P_j\in B_{P_i}}\frac{r_k(P_j)}{|P_j|}\end{equation}$$初始对所有网页\(P_i\)有\(r_0(P_i)=\frac{1}{n}\),迭代\(k\)步之后,可以计算出所有网页的PR值,然后按PR值从大到小排序,就可以知道每个网页的重要性了。 对于上图的小网络,我们可以计算出其每一步的PR值: 可以看到经过2次迭代之后,节点4的PR值最大,从图中也可以看出,节点4的出入边较多,它可能比较重要。 注意到对于(2)式,当\(i,j\)之间有边时,\(\frac{1}{|P_j|}\)相当于对\(P_j\)出度的归一化,设矩阵\(H\)为图的邻接矩阵的行归一化矩阵,对于上图,为 设行向量\(\pi^{(k)T}\)为第\(k\)轮迭代时所有网页的PR值,则式(2)可以转换为如下的矩阵形式: $$\begin{equation}\pi^{(k+1)T}=\pi^{(k)T}H\end{equation}$$初始有\(\pi^{(0)T}=\frac{1}{n}e^T\),\(e^T\)为全1的行向量。我们可以从(3)式观测出几点信息: (3)式的每一轮计算涉及到向量和矩阵的乘法,复杂度为\(O(n^2)\),\(n\)为矩阵\(H\)的大小 \(H\)是一个稀疏矩阵,因为大部分网页只和很少的网页有链接关系,所以上述向量和矩阵的乘法复杂度还可以降低 \(H\)有点像马尔科夫链中的随机转移矩阵,但又不完全是,因为如果有dangling nodes,则这一行就是全0,所以\(H\)被称为substochastic matrix 上图中的节点3就是一个dangling node,它只有入边,没有出边,也就是说,每一轮迭代,PR值只会流入3号节点,不会从3号节点流出,久而久之,3就像一个水槽(sink)一样,吸走了大部分的PR,导致PR值虚高。 所以问题随之而来,怎样保证(3)式一定能够收敛到一个平稳概率分布\(\pi^T\),\(\pi^T\)和\(\pi^{(0)T}\)有关吗,怎样解决dangling nodes问题,等等。此时需要引入一点马尔科夫链理论的知识。 在马尔科夫理论呢中,如果一个矩阵\(P\)是随机的(stochastic)、不可约的(irreducible)和非周期的(aperiodic),则对于任意的起始向量,都能收敛到一个唯一的平稳正向量。所以如果PageRank矩阵\(H\)满足上述三个条件,则可以用幂法(Power Method)找到一个平稳概率分布\(\pi^T\)。幂法是用来计算最大特征值的特征向量。因为\(H\)的最大特征值为1,所以可以用幂法找到稳态时(\(\pi^T=\pi^TH\))的概率分布\(\pi^T\)。 下面我们就将矩阵\(H\)调整为随机的(stochastic)、不可约的(irreducible)和非周期的(aperiodic)。 行随机矩阵是指行和为1的非负矩阵。如果图中含有dangling nodes,则\(H\)不是随机的,比如上面的例子,第二行为全0。所以第一个调整是对于所有dangling nodes,都加上一个随机跳转向量\(e^T/n\),含义就是如果进入死胡同(dangling nodes),则随机跳转到网络中的任意一个网页。定义向量\(a\): $$\begin{equation}a_i=\begin{cases}1\quad\text{if page}~i\text{ is a dangling node}\\0\quad\text{otherwise}\end{cases}\end{equation}$$则新的Google矩阵为: $$\begin{equation}S=H+a\frac{1}{n}e^T\end{equation}$$新矩阵\(S\)就是一个行随机矩阵了。对于上图的例子,有 为了保证矩阵\(S\)满足不可约性(irreducible)和非周期性(aperiodic),必须使\(S\)对应的图是强连通的且每个节点有自回路。所以再次调整为: $$\begin{equation}G=\alpha S+(1-\alpha)\frac{1}{n}ee^T\end{equation}$$令 $$\begin{equation}E=\frac{1}{n}ee^T\end{equation}$$则得到本博客开头的Google矩阵公式: $$\begin{equation}G=\alpha S+(1-\alpha)E\end{equation}$$\(E\)即为随机平均游走矩阵。矩阵\(G\)也很好解释,大家上网的时候以\(\alpha\)的概率沿着某个网页里面的链接一步步深入进去(\(S\)),当沿着链接走累的时候,以\(1-\alpha\)的概率在地址栏输入一个新地址,随机跳走了(\(E\))。 此时的矩阵\(G\)满足随机性(stochastic)、不可约性(irreducible)和非周期性(aperiodic),所以可以根据幂法(Power Method)找到一个平稳概率分布\(\pi^T\),\(\pi^T_i\)就衡量了网页\(P_i\)的重要性或者权威性。 ...

August 4, 2016 · 1 min

调查问卷的有效性(2)相对误差

$$\begin{equation}Pr(|\hat{p}-p|\geq 5\%)\leq 5\%\end{equation}$$上一回我们讲到当\(p\)本身很小的时候,容易被5%(绝对误差)给淹没掉,导致结果的不可信。我们可以引入相对误差,把(1)式转换为如下的不等式 $$\begin{equation}Pr(|\hat{p}-p|\geq\delta p)\leq\epsilon\end{equation}$$同理,我们可以用 $$\begin{equation}\hat{p}=\frac{x_1+x_2+…+x_n}{n}\end{equation}$$代替\(\hat{p}\)(建议先看上一篇博客),转换为 $$\begin{equation}Pr(|X-np|\geq\delta np)\end{equation}$$类似的,\(X=x_1+x_2+…+x_n\),\(E(X)=\mu=np\),所以(4)式等价为 $$\begin{equation}Pr(|X-\mu|\geq\delta\mu)\end{equation}$$这个时候,因为不等号右边和均值\(\mu\)有关,不能再用切比雪夫不等式了,我们需要另外一个武器:Chernoff bound。它有两种形式: $$\begin{equation}Pr(X\geq (1+\delta)\mu)\leq[\frac{e^\delta}{(1+\delta)^{1+\delta}}]^\mu\leq e^{-\frac{\mu}{3}\delta^2}\quad\forall\delta>0\end{equation}$$$$\begin{equation}Pr(X\leq (1-\delta)\mu)\leq[\frac{e^{-\delta}}{(1-\delta)^{1-\delta}}]^\mu\leq e^{-\frac{\mu}{2}\delta^2}\quad\forall 0<\delta<1\end{equation}$$Chernoff bound的证明需要用到马尔可夫不等式,有一点技巧。以上两种形式可以统一成 $$\begin{equation}Pr(|X-\mu|\geq\delta\mu)\leq 2e^{-\frac{\mu}{3}\delta^2}\end{equation}$$也是一个很漂亮的不等式。 利用Chernoff bound求解(5)式: $$\begin{equation}Pr(|X-\mu|\geq\delta\mu)\leq 2e^{-\frac{\mu}{3}\delta^2}\\=2e^{-\frac{np}{3}\delta^2}\leq\epsilon\end{equation}$$解得 $$\begin{equation}n\geq\left\lceil\frac{3ln\frac{2}{\epsilon}}{p\delta^2}\right\rceil\end{equation}$$这个结果看起来就很复杂了。也就是说,如果要设计调查问卷使满足(2)式的精度,抽样的样本数必须满足(10)式。从(10)式可知,当要求的精度越高(即\(\delta\)和\(\epsilon\)越小),所需的样本数越大。并且结果还和真实值\(p\)有关。

July 23, 2016 · 1 min

调查问卷的有效性(1)绝对误差

每年春晚过后,央视又要吹嘘说今年春晚收视率创新高了,但是我们总感觉央视在骗我们,因为我是越长大越不看春晚了[笑cry],所以收视率到底是怎么统计出来的,央视的说法是否靠谱呢? 最近的美国大选真是热闹,很多机构都会发放一些调查问卷,然后统计出希拉里或者唐纳德的民众支持率是多少,但是我并没有收到调查问卷,凭什么就得出了民众支持率了,意思是把我排除在民众之外咯?所以引出这样一个问题,调查问卷是否可信,即调查问卷的有效性。 其实,央视统计收视率并不要问全中国14亿人口有多少人看了春晚,他只需要从14亿人口里面随机抽\(n\)个人,问一下这\(n\)个人里有多少人看了春晚,然后把看的人数除以总数就大概估计出全国的收视率了。同理调查民众支持率也是一样,只需要随机调查\(n\)个人的意向,把支持希拉里的人数除以总数就大概得到了希拉里的支持率。 但是你要问了,通过抽样调查出来的收视率和支持率靠谱吗,需要随机抽样多少人才能得到一个比较好的全局近似解呢?今天我们就来解决这个问题。 假设我们随机抽样了\(n\)个人,分别是\(x_1,x_2,…,x_n\)。如果第\(i\)个人看了春晚,则\(x_i=1\),否则\(x_i=0\)。那么通过这\(n\)个人的收视情况,我们可以估计出一个收视率 $$\begin{equation}\hat{p}=\frac{x_1+x_2+…+x_n}{n}\end{equation}$$假设全国的真实收视率是\(p\),那么平均到每一个人,他看了春晚的概率就是\(p\),也即\(Pr(x_i=1)=p\),所以有 $$\begin{equation}E(x_i)=p\quad E(x_i^2)=p\quad Var(x_i)=p(1-p)\end{equation}$$我们的目的就是希望通过\(n\)个人估计出来的\(\hat{p}\)和\(p\)越接近越好。换句话说,我们希望\(\hat{p}\)和\(p\)相差大于5%的概率要小于5%。再换句话说就是有至少95%的概率,\(\hat{p}\)和\(p\)相差在5%以内,即\(\hat{p}\)和\(p\)很接近。注意这里的两个5%都是可以换成任意你想要的精度。用数学语言表示就是,\(n\)至少为多少时,以下不等式可以被满足。 $$\begin{equation}Pr(|\hat{p}-p|\geq 5\%)\leq 5\%\end{equation}$$把(1)式代入(3)式,用\(\frac{1}{20}\)代替5%,得到等价形式: $$\begin{equation}Pr(|(\frac{x_1+x_2+…+x_n}{n})-p|\geq\frac{1}{20})\\ \Longleftrightarrow~Pr(|X-np|\geq\frac{n}{20})\end{equation}$$其中\(X=x_1+x_2+…+x_n\)。根据期望的线性可加性,有 $$\begin{equation}E(X)=E(x_1+x_2+…+x_n)=E(x_1)+E(x_2)+…+E(x_n)=np\end{equation}$$所以(4)又等价于 $$\begin{equation}Pr(|X-E(X)|\geq\frac{n}{20})\end{equation}$$我们需要利用著名的切比雪夫不等式来求解上式,切比雪夫不等式如下: $$\begin{equation}Pr(|X-E(X)|\geq~c)\leq\frac{Var(X)}{c^2}\end{equation}$$切比雪夫不等式可以直接由马尔可夫不等式得到,马尔可夫不等式的证明也不难,略过。 利用切比雪夫不等式求解(6)式 $$\begin{equation}Pr(|X-E(X)|\geq\frac{n}{20})\leq\frac{Var(X)}{n^2}*400\\ =\frac{n*Var(x_i)}{n^2}*400\\ =\frac{p(1-p)}{n}*400\\ \leq\frac{1/4}{n}*400=\frac{100}{n} \end{equation}$$第一个等号是因为\(n\)个变量是独立同分布的,所以方差也有类似于(5)式的线性性质。最后一个不等号是因为\(p(1-p)\)是一个开口向下的抛物线,在\(p=1/2\)时取到极值\(1/4\)。 回到最初的不等式(3),则(8)式要满足\(\frac{100}{n}\leq 5\%\),解得\(n\geq 2000\)。注意到求出的\(n\)和总体人数是无关的,也就是说,虽然全中国有十几亿人口,但是央视只要随机抽样调查2000个人的收视情况,就能以比较高的概率准确估计出全国的收视率。 这个结论还是很漂亮的,但是这种方法有两个限制条件: 采样满足独立同分布,即这\(n\)个人是独立同分布的,不能针对某一特定人群调查 (3)式的5%是一个绝对误差,当\(p\)本身很小的时候,容易被5%淹没 对于第1个问题,稍微好处理一点,抽样的时候尽量随机一点。对于第2个问题,比较好的解决办法是引入相对误差,即把(3)式转换为如下的不等式 $$\begin{equation}Pr(|\hat{p}-p|\geq\delta p)\leq\epsilon\end{equation}$$(9)式的求解就比较复杂了,得出的结论也没有上面那么简单,具体的求解方法请听下回分解。

July 23, 2016 · 1 min

有趣的交互式证明

你是否想过如下问题:怎样向色盲证明两只袜子的颜色是不一样的?怎样证明两个图是不同构的?怎样证明一个数是二次非剩余的? 咋听起来觉得很有意思吧,色盲是区分不了颜色的,怎么能让他相信两只袜子的颜色不一样呢。图同构问题目前既没有被证明属于P,也没有被证明属于NP-Complete。二次非剩余问题也没有被证明属于NP。 这些听起来很“难”的问题,却可以通过交互式证明进行证明,下面先通过“向色盲证明两只袜子的颜色不同”这个有趣的例子一窥交互式证明的强大。 向色盲证明两只袜子的颜色不同 P有一只红袜子和黄袜子,她的一个色盲朋友V不相信P的袜子颜色不同,P如何才能让V相信这是真的呢?一个简单的办法如下: P把两只袜子给V,V每只手拿了一只袜子 P转过身背对V V抛一枚硬币,如果头面朝上,则保持两只袜子不动,否则交换左右手的袜子 P转过身,V问P是否交换过袜子 如果P回答错误,则V不相信;否则,重复100次实验,如果P都回答正确,则V相信这两只袜子是不同颜色的 如果两只袜子的颜色确实不一样,则P通过区分两只袜子的颜色能正确回答V有没有交换过袜子。但是如果两只袜子颜色一样,则不管V有没有交换过,P都无法分辨这两只袜子,所以只好猜V有没有交换,而猜对的概率只有1/2,重复100次,都猜对的概率只有\((1/2)^{100}\),这是一个非常小的数,可以认为几乎不会发生,即出错的概率极低。 这就是交互式证明的一个例子,上述证明有三个特点:1)交互过程,整个证明需要P和V进行交互才能完成;2)具有随机性,即V需要抛一枚硬币,来决定是否交换袜子;3)零知识,虽然V最终相信了这两只袜子是不同颜色的,但V还是不知道这两只袜子是什么颜色的。 下面我们给出交互式证明的形式化定义。 交互式证明(Interactive Proofs, IP) 令\(k\)是\(N\rightarrow N\)的一个函数,我们称语言\(L\)属于\(IP[k]\),如果存在一个\(k(|x|)\)多项式时间概率图灵机TM \(V\),使得: $$ \begin{equation} \begin{cases} x\in L \Longrightarrow\exists P\quad Pr[V \text{ accepts }x, V(x)=1]\geq 2/3 \\ x\notin L \Longrightarrow\forall P\quad Pr[V \text{ accepts }x, V(x)=1]\leq 1/3 \end{cases} \end{equation} $$定义 $$IP=\underset{c}{\bigcup} IP[n^c]$$上述定义的第一条称为“完备性”(Completeness):如果\(x\in L\),则存在一个证明者P(prover),使得验证者V(verfier)能以多项式时间接受\(x\),且接受的概率大于2/3;第二条称为“可靠性”(Soundness),如果\(x\notin L\),则对于所有证明者P,V接受\(x\)的概率都不会超过1/3。 对应到上面的例子,完备性:当两只袜子的颜色确实不同时,V接受的概率为1>2/3;可靠性:当两只袜子的颜色相同时,重复100次实验,V接受的概率只有\((1/2)^{100}<1/3\)。 IP这个复杂性类就是所有IP[k]的并集。在IP中,P的能力是无穷的,但它不一定是诚实的;V能力较弱,只能进行多项式时间的计算。 下面我们给出另外两个交互式证明的协议。 图不同构(Graph Non-isomorphism, GNI)的交互式证明 如果图\(G_1\)和\(G_2\)可以通过对顶点进行恰当标记来将它们转换为同一个图,则称\(G_1\)和\(G_2\)同构,记为\(G_1\cong G_2\)。换句话说,如果\(G_1\)和\(G_2\)同构,则在\(G_1\)的所有顶点标签上存在一个置换\(\pi\)使得\(\pi (G_1)=G_2\),其中\(\pi (G_1)\)是将\(\pi\)作用到\(G_1\)的各个顶点上之后得到的图。下图就是两个同构图,右边给出了置换\(\pi\)。 图同构的补集为图不同构(Graph Non-isomorphism, GNI),即判定给定的两个图是否不同构。下面是GNI的一个交互式证明过程。 给定两个图\(G_1\)和\(G_2\),证明者P想要向验证者V证明\(G_1\ncong G_2\)。 V:随机选一个\(i\in \{1,2\}\),对\(G_i\)做一个随机的置换,得到新图\(H\),则有\(H\cong G_i\),将\(H\)发送给P P:发送\(j\)给V V:如果\(i\neq j\),则拒绝;否则重复100次实验,都有\(i==j\),则相信\(G_1\ncong G_2\) 完备性:如果\(G_1\ncong G_2\),则\(H\)只和\(G_1, G_2\)中的一个图同构,P因为能力无穷,一定能找出和\(H\)同构的图\(G_j\),且满足\(j==i\)。 ...

July 14, 2016 · 1 min

LaTeX写作完美解决方案

\(\LaTeX\)的强大我就不赘述了,这里简单介绍一下怎样在Windows快速配置一个完美好用的\(\LaTeX\)环境。 我大学刚接触\(\LaTeX\)的时候,使用的是CTeX,CTeX是一个大礼包,整合了编译器编辑器等,但是由于久不更新,很多宏包和语法都变了,而且CTeX附带的WinEdt是商业软件,30天之后需要收费,我又不想用盗版,所以就打算自己配置\(\LaTeX\)环境。 目前使用的是MiKTeX+Texmaker的完美组合!MiKTeX是\(\LaTeX\)编译器,Texmaker是\(\LaTeX\)编辑器。两者都是开源软件。 MiKTeX非常棒的地方在于“MiKTeX has the ability to install needed packages automatically (on-the-fly)”,就是说,你用MiKTeX时,不需要担心某个宏包是否存在,你只管用就是了,MiKTeX会在你第一次用到某个宏包时,自动从网上下载,非常方便。正因为这样,MiKTeX的安装包很小,只有175MB。当然,因为是on-the-fly的,所以必须联网使用,而且MiKTeX只有Windows版本。 MiKTeX自带了一个TeXworks编辑器的,但是这软件用户体验并不好。我以前一直都用WinEdt,很好用,但是它是商业软件,我又不想盗版(说到底是没钱…),所以换了Texmaker。Texmaker可以媲美WinEdt,软件布局合理,各种快捷键用起来也很方便。不过在上手之前要简单配置一下。 如果是写英文文章,点击“快速构建”左边的箭头(或者F1快捷键),就能一键编译并刷新pdf视图。但是默认的快速构建使用的引擎是PdfLaTeX,如果你是中文用户,使用了xeCJK宏包,则必须使用XeLaTeX引擎编译,所以依次点击“选项->配置Texmaker->(左边)快速构建”,选择快速构建命令为”XeLaTeX + View PDF”。 构建好的PDF默认是以弹窗的形式展现的,我们可以设置让代码和PDF并排显示,这样方便在PDF和源代码之间切换,配置如下: Texmaker自带了一个PDF阅读器,当然你也可以使用外部阅读器,比如非常棒的Sumatra PDF,只需填入Sumatra PDF的路径跟上%.pdf,并选中External Viewer。 Texmaker还有一个很好用的功能是“正向/反向搜索”。反向搜索是点击PDF某个位置,会跳到tex源代码对应位置,快捷方式是ctrl+click。正向搜索是点击tex源代码某个位置,会跳到PDF对应的位置,默认快捷方式ctrl+space,但是这个快捷方式好像用不了,可以自行配置成其他快捷方式,比如ctrl+1,我当时是打开下图的快捷方式窗口才发现这个问题的。 正反向搜索都可以通过鼠标右键菜单实现,但是快捷键还是更方便的。最重要的一点是,源文件*.tex所在路径不能有中文!!!要不然正反向搜索不能用,这点很重要,我当时郁闷了好久。 另外还可以配置一下编辑器的字体,勾选”Backup documents every 10 min”之类的。 OK,大功告成,这种三段式的界面、F1快速构建以及正向/反向搜索,用起来真是太顺手了,Just Enjoy \(\LaTeX\)~ 下面是我常用的\(\LaTeX\)中文模板: LaTeXDemo.pdf LaTeXDemo.tex

May 16, 2016 · 1 min

SVM之序列最小最优化算法(SMO算法)

SVM回顾 支持向量机(SVM)的一大特点是最大化间距(max margin)。对于如上图的二分类问题,虽然有很多线可以将左右两部分分开,但是只有中间的红线效果是最好的,因为它的可活动范围(margin)是最大的,从直观上来说很好理解。 对于线性二分类问题,假设分类面为 $$\begin{equation} u=\vec w \cdot \vec x-b \end{equation}$$则margin为 $$\begin{equation} m=\frac{1}{||w||_2} \end{equation}$$根据max margin规则和约束条件,得到如下优化问题,我们要求的就是参数\(\vec w\)和\(b\): $$\begin{equation} \min\limits_{\vec w,b}\frac{1}{2}||\vec w||^2 \quad\text{subject to}\quad y_i(\vec w\cdot \vec x_i-b) \geq 1, \forall i,\end{equation}$$对于正样本,类标号\(y_i\)为+1,反之则为-1。根据拉格朗日对偶,(3)可以转换为如下的二次规划(QP)问题,其中\(\alpha_i\)为拉格朗日乘子。 $$\begin{equation} \min\limits_{\vec \alpha}\Psi(\vec\alpha)=\min\limits_{\vec \alpha}\frac{1}{2}\sum_{i=1}^N\sum_{j=1}^Ny_iy_j(\vec x_i\cdot\vec x_j)\alpha_i\alpha_j-\sum_{i=1}^N\alpha_i,\end{equation}$$其中N为样本数量。上式还需满足如下两个约束条件: $$\begin{equation} \alpha_i\geq 0, \forall i,\end{equation}$$$$\begin{equation} \sum_{i=1}^Ny_i\alpha_i=0.\end{equation}$$一旦求解出所有的拉格朗日乘子,则我们可以通过如下的公式得到分类面参数\(\vec w\)和\(b\)。 $$\begin{equation}\vec w=\sum_{i=1}^Ny_i\alpha_i\vec x_i,\quad b=\vec w\cdot\vec x_k-y_k\quad\text{for some}\quad\alpha_k>0.\end{equation}$$当然并不是所有的数据都可以完美的线性划分,可能有少量数据就是混在对方阵营,这时可以通过引入松弛变量\(\xi_i\)得到软间隔形式的SVM: $$\begin{equation} \min\limits_{\vec w,b,\vec\xi}\frac{1}{2}||\vec w||^2+C\sum_{i=1}^N\xi_i \quad\text{subject to}\quad y_i(\vec w\cdot \vec x_i-b) \geq 1-\xi_i, \forall i,\end{equation}$$其中的\(\xi_i\)为松弛变量,能假装把错的样本分对,\(C\)对max margin和margin failures的trades off。对于这个新的优化问题,约束变成了一个box constraint: $$\begin{equation}0\leq\alpha_i \leq C,\forall i.\end{equation}$$而松弛变量\(\xi_i\)不再出现在对偶公式中了。 ...

May 2, 2016 · 5 min

和我一起构建搜索引擎(七)总结展望

至此,整个新闻搜索引擎构建完毕,总体效果令人满意,不过还是有很多可以改进的地方。下面总结一下本系统的优点和不足。 优点 倒排索引存储方式。因为不同词项的倒排记录表长度一般不同,所以没办法以常规的方式存入关系型数据库。通过将一个词项的倒排记录表序列化成一个字符串再存入数据库,读取的时候通过反序列化获得相关数据,整个结构类似于邻接表的形式。 推荐阅读实现方式。利用特征提取的方法,用25个关键词表示一篇新闻,大大减小了文档词项矩阵规模,提高计算效率的同时不影响推荐新闻相关性。 借用了Reddit的热度公式,融合了时间因素。 不足 构建索引时,为了降低索引规模,提高算法速度,我们将纯数字词项过滤了,同时忽略了词项大小写。虽然索引规模下降了,但是牺牲了搜索引擎的正确率。 构建索引时,采用了jieba的精确分词模式,比如句子“我来到北京清华大学”的分词结果为“我/ 来到/ 北京/ 清华大学”,“清华大学”作为一个整体被当作一个词项,如果搜索关键词是“清华”,则该句子不能匹配,但显然这个句子和“清华”相关。所以后续可以采用结巴的搜索引擎分词模式,虽然索引规模增加了,但能提升搜索引擎的召回率。 在推荐阅读模块,虽然进行了维度约减,但是当数据量较大时(数十万条新闻),得到的文档词项矩阵也是巨大的,会远远超过现有PC的内存大小。所以可以先对新闻进行粗略的聚类,再在类内计算两两cosine相似度,得到值得推荐的新闻。 在热度公式中,虽然借用了Reddit的公式,大的方向是正确的,但是引入了参数\(k_1\)和\(k_2\),而且将其简单的设置为1。如果能够由专家给出或者经过机器学习训练得到,则热度公式的效果会更好。 完整可运行的新闻搜索引擎Demo请看我的Github项目news_search_engine。 以下是系列博客: 和我一起构建搜索引擎(一)简介 和我一起构建搜索引擎(二)网络爬虫 和我一起构建搜索引擎(三)构建索引 和我一起构建搜索引擎(四)检索模型 和我一起构建搜索引擎(五)推荐阅读 和我一起构建搜索引擎(六)系统展示 和我一起构建搜索引擎(七)总结展望

January 9, 2016 · 1 min

和我一起构建搜索引擎(六)系统展示

前几个博客已经介绍完搜索引擎的所有功能,为了实现更好的用户体验,需要一个web界面。这一部分是另一个队员做的,我这里借用他的代码。 我们利用开源的Flask Web框架搭建了展示系统,搜索引擎只需要两个界面,一个是搜索界面,另一个是展示详细新闻的页面(实际搜索引擎没有这个页面)。编写好这两个模板页面并调用前面给出的接口,得到数据,展示出来就可以。 这一部分没有太多需要讲解的算法,直接上效果图(点击图片可以查看大图)。 图1. 搜索页面 图2. 新闻详情页面 由于数据量不大,只有1000条新闻,所以第一页中后面几个结果相关度就不是很高了。但是经过测试,在大数据量的情况下,不论是搜索的速度、准确率、召回率以及推荐阅读的相关度,都达到了不错的效果。 完整可运行的新闻搜索引擎Demo请看我的Github项目news_search_engine。 以下是系列博客: 和我一起构建搜索引擎(一)简介 和我一起构建搜索引擎(二)网络爬虫 和我一起构建搜索引擎(三)构建索引 和我一起构建搜索引擎(四)检索模型 和我一起构建搜索引擎(五)推荐阅读 和我一起构建搜索引擎(六)系统展示 和我一起构建搜索引擎(七)总结展望

January 9, 2016 · 1 min

和我一起构建搜索引擎(五)推荐阅读

虽然主要的检索功能实现了,但是我们还需要一个“推荐阅读”的功能。当用户浏览某条具体新闻时,我们在页面底端给出5条和该新闻相关的新闻,也就是一个最简单的推荐系统。 搜狐新闻“相关新闻”模块 推荐模块的思路是度量两两新闻之间的相似度,取相似度最高的前5篇新闻作为推荐阅读的新闻。 我们前面讲过,一篇文档可以用一个向量表示,向量中的每个值是不同词项t在该文档d中的词频tf。但是一篇较短的文档(如新闻)的关键词并不多,所以我们可以提取每篇新闻的关键词,用这些关键词的tfidf值构成文档的向量表示,这样能够大大减少相似度计算量,同时保持较好的推荐效果。 jieba分词组件自带关键词提取功能,并能返回关键词的tfidf值。所以对每篇新闻,我们先提取tfidf得分最高的前25个关键词,用这25个关键词的tfidf值作为文档的向量表示。由此能够得到一个1000*m的文档词项矩阵M,矩阵每行表示一个文档,每列表示一个词项,m为1000个文档的所有互异的关键词(大概10000个)。矩阵M当然也是稀疏矩阵。 得到文档词项矩阵M之后,我们利用sklearn的pairwise_distances函数计算M中行向量之间的cosine相似度,对每个文档,得到与其最相似的前5篇新闻id,并把结果写入数据库。 推荐阅读模块的代码如下: 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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 # -*- coding: utf-8 -*- """ Created on Wed Dec 23 14:06:10 2015 @author: bitjoy.net """ from os import listdir import xml.etree.ElementTree as ET import jieba import jieba.analyse import sqlite3 import configparser from datetime import * import math import pandas as pd import numpy as np from sklearn.metrics import pairwise_distances class RecommendationModule: stop_words = set() k_nearest = [] config_path = '' config_encoding = '' doc_dir_path = '' doc_encoding = '' stop_words_path = '' stop_words_encoding = '' idf_path = '' db_path = '' def __init__(self, config_path, config_encoding): self.config_path = config_path self.config_encoding = config_encoding config = configparser.ConfigParser() config.read(config_path, config_encoding) self.doc_dir_path = config['DEFAULT']['doc_dir_path'] self.doc_encoding = config['DEFAULT']['doc_encoding'] self.stop_words_path = config['DEFAULT']['stop_words_path'] self.stop_words_encoding = config['DEFAULT']['stop_words_encoding'] self.idf_path = config['DEFAULT']['idf_path'] self.db_path = config['DEFAULT']['db_path'] f = open(self.stop_words_path, encoding = self.stop_words_encoding) words = f.read() self.stop_words = set(words.split('\n')) def write_k_nearest_matrix_to_db(self): conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("'DROP TABLE IF EXISTS knearest'") c.execute("'CREATE TABLE knearest(id INTEGER PRIMARY KEY, first INTEGER, second INTEGER, third INTEGER, fourth INTEGER, fifth INTEGER)'") for docid, doclist in self.k_nearest: c.execute("INSERT INTO knearest VALUES (?, ?, ?, ?, ?, ?)", tuple([docid] + doclist)) conn.commit() conn.close() def is_number(self, s): try: float(s) return True except ValueError: return False def construct_dt_matrix(self, files, topK = 200): jieba.analyse.set_stop_words(self.stop_words_path) jieba.analyse.set_idf_path(self.idf_path) M = len(files) N = 1 terms = {} dt = [] for i in files: root = ET.parse(self.doc_dir_path + i).getroot() title = root.find('title').text body = root.find('body').text docid = int(root.find('id').text) tags = jieba.analyse.extract_tags(title + '。' + body, topK=topK, withWeight=True) #tags = jieba.analyse.extract_tags(title, topK=topK, withWeight=True) cleaned_dict = {} for word, tfidf in tags: word = word.strip().lower() if word == '' or self.is_number(word): continue cleaned_dict[word] = tfidf if word not in terms: terms[word] = N N += 1 dt.append([docid, cleaned_dict]) dt_matrix = [[0 for i in range(N)] for j in range(M)] i =0 for docid, t_tfidf in dt: dt_matrix[i][0] = docid for term, tfidf in t_tfidf.items(): dt_matrix[i][terms[term]] = tfidf i += 1 dt_matrix = pd.DataFrame(dt_matrix) dt_matrix.index = dt_matrix[0] print('dt_matrix shape:(%d %d)'%(dt_matrix.shape)) return dt_matrix def construct_k_nearest_matrix(self, dt_matrix, k): tmp = np.array(1 – pairwise_distances(dt_matrix[dt_matrix.columns[1:]], metric = "cosine")) similarity_matrix = pd.DataFrame(tmp, index = dt_matrix.index.tolist(), columns = dt_matrix.index.tolist()) for i in similarity_matrix.index: tmp = [int(i),[]] j = 0 while j <= k: max_col = similarity_matrix.loc[i].idxmax(axis = 1) similarity_matrix.loc[i][max_col] = -1 if max_col != i: tmp[1].append(int(max_col)) #max column name j += 1 self.k_nearest.append(tmp) def gen_idf_file(self): files = listdir(self.doc_dir_path) n = float(len(files)) idf = {} for i in files: root = ET.parse(self.doc_dir_path + i).getroot() title = root.find('title').text body = root.find('body').text seg_list = jieba.lcut(title + '。' + body, cut_all=False) seg_list = set(seg_list) – self.stop_words for word in seg_list: word = word.strip().lower() if word == '' or self.is_number(word): continue if word not in idf: idf[word] = 1 else: idf[word] = idf[word] + 1 idf_file = open(self.idf_path, 'w', encoding = 'utf-8') for word, df in idf.items(): idf_file.write('%s %.9f\n'%(word, math.log(n / df))) idf_file.close() def find_k_nearest(self, k, topK): self.gen_idf_file() files = listdir(self.doc_dir_path) dt_matrix = self.construct_dt_matrix(files, topK) self.construct_k_nearest_matrix(dt_matrix, k) self.write_k_nearest_matrix_to_db() if __name__ == "__main__": print('—–start time: %s—–'%(datetime.today())) rm = RecommendationModule('../config.ini', 'utf-8') rm.find_k_nearest(5, 25) print('—–finish time: %s—–'%(datetime.today())) 这个模块的代码量最多,主要原因是需要构建文档词项矩阵,并且计算k邻居矩阵。矩阵数据结构的设计需要特别注意,否则会严重影响系统的效率。我刚开始把任务都扔给了pandas.DataFrame,后来发现当两个文档向量合并时,需要join连接操作,当数据量很大时,非常耗时,所以改成了先用python原始的list存储,最后一次性构造一个完整的pandas.DataFrame,速度比之前快了不少。 ...

January 9, 2016 · 4 min