注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

yeye55的博客

编程就象品一杯清茶,清淡却又深厚。

 
 
 

日志

 
 
关于我

宅男一只,喜欢编程。没事的时候就喜欢抱着电脑看看电影、看看动漫、听听音乐、打打网游、逛逛论坛。清静的时候喜欢专研编程技术,但是发现自己至今仍然只是一只三脚猫。

网易考拉推荐

PPM压缩算法的分析与实现 - 第三章 结构优化  

2011-11-08 10:19:23|  分类: 数据压缩算法 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

  本论文题目:PPM压缩算法的分析与实现,作者:叶叶,于2011年11月8日在网易博客上发表。页面地址:http://yeye55blog.blog.163.com/blog/static/197241021201110715759577/ 。本论文全文及相关配套程序可以在上述页面中下载。请尊重他人劳动成果,转载或引用时请注明出处。

第三章 结构优化

3.1 前言

  上一章介绍的Trie结构实际上是早期PPM算法使用的建模结构。使用Trie结构进行建模有许多缺点。比如上下文中会出现零频符号,模型占用内存较大。本章中将介绍对Trie结构进行优化的方案。使用这些优化方案,可以有效的降低内存占用和算法耗时。本章将对区分削减、内存共用、上下文Trie等内容进行介绍。

3.2 区分削减

  当我们把一个符号添加到一个上下文中时,一般将符号的初始频度设置为1。在更新符号频度时,如果当前符号频度达到限定值就需要进行削减频度。对一个上下文进行削减频度时,如果有的符号频度为1,那么削减频度后这些符号的频度将变成0。所以说,上下文中出现的零频符号都是在削减频度时产生的。这种情况的后果是,上下文对应的TNode.Count成员无法代表m(s)。因为在编码时,视零频符号没有出现在当前上下文中。这样在编码时需要对上下文中的所有符号进行遍历以便统计m(s)。

  如何防止上下文中出现零频符号?第一种解决方案是在削减频度时不允许出现零频符号。这种方案很容易实现,但是测试表明这种方案会降低压缩率。第二种解决方案是将零频符号从上下文中删除。值得注意的是,符号对应的TItem.Link成员指向下层节点。如果删除符号对应的TItem那么也要删除对应的下层节点。但是一个上下文节点一旦建立就不能被删除,因为上下文节点之间使用TNode.Lesser成员彼此连接。删除一个上下文节点,并更新所有相关的TNode.Lesser成员是相当耗时的。不仅如此,删除一个上下文节点也会对模型造成较大的破坏。这会丢失很多已经积累的频度信息。所以,第二种方案是无法实现的。不过我们可以发现,如果一个符号出现在最高阶上下文中,符号对应的TItem.Link成员指向的是兄弟上下文节点。这时符号对应的TItem可以被删除。删除操作不会对模型造成很大的影响。于是,针对上述两种解决方案可以提出一种折中方案:在非最高阶上下文中削减频度时不允许出现零频符号。在最高阶上下文中削减频度时允许出现零频符号。如果出现零频符号,那么就将该符号从当前上下文中删除。这种方案被称为区分削减。测试表明使用区分削减方案只会轻微的影响算法的压缩率。

  区分削减方案使得削减频度操作变得有些复杂。削减频度时需要判断当前上下文的阶数,以确定使用哪种削减方式。在最高阶上下文中进行削减频度时,需要将削减后出现的零频符号对应的TItem移动到TItemAry数组的末尾。削减完成时,如果出现了零频符号,需要将对应的TNode.Items成员指向的内存空间进行收缩。回收零频符号对应的TItem所占用的内存。同时还要修改TNode.Count成员的值。

  使用区分削减方案后,上下文中将不会出现零频符号。此时上下文对应的TNode.Count成员就代表了m(s)。编码时可以非常方便的利用Count成员获得m(s)的值。在随后介绍的变体方案中需要频繁的使用m(s)。所以区分削减方案的使用可以有效的提高算法的效率。

3.3 内存共用

  如果在一个上下文中只出现了一个符号,我们称这样的上下文为二元上下文(Binary Contexts,Bin-Context)。二元上下文在编码时具有二元特性,要么逃逸输出逃逸码的预测概率,要么匹配输出当前符号的预测概率。与之相对应的,如果一个上下文中出现了两个或两个以上的符号,我们称这样的上下文为多元上下文(Multivariate Contexts,Mul-Context)。之所以把二元上下文独立出来讨论,是因为在模型中二元上下文占有很大的比例。现在让我们来看一份测试数据。测试使用5阶的方案2.1对语料库[14]中的18个文件进行压缩。测试输出编码结束时模型中节点的数量和相关比例。测试结果如表3.1所示。

文件 节点项
个数
(个)
上下文
节点个数
(个)
二元上下文
节点个数
(个)
二元上下文
所占比例
(%)
bib 95241 57629 44341 76.94
book1 417292 189295 118341 62.52
book2 338011 175142 121266 69.24
geo 309697 214571 190422 88.75
news 374685 217036 164237 75.67
obj1 52961 38876 34973 89.96
obj2 294451 191866 157716 82.20
paper1 66582 40504 30610 75.57
paper2 85921 48408 34699 71.68
paper3 64583 38447 28511 74.16
paper4 25393 16569 13022 78.59
paper5 24114 16101 12869 79.93
paper6 52073 32585 25071 76.94
pic 165696 107257 87847 81.90
progc 54557 34826 27490 78.94
progl 53146 32497 24473 75.31
progp 41616 26801 21044 78.52
trans 61528 39694 31453 79.24
总计 2577547 1518104 1168385 76.96

表3.1 方案2.1模型中节点的数量和比例

  从表3.1的数据中可以看出,在模型生成的所有上下文中二元上下文占有很大的比例。不仅如此,测试表明随着阶数的升高二元上下文的比例还会进一步的提高。正因为有如此众多的二元上下文,我们可以采用内存共用的方案将二元上下文中仅有的TItem保存在对应的TNode中。这样可以节约大量的内存。改进后的TNode定义如代码3.1所示。

01 //上下文节点
02 TNode = packed record
03     Count : Word; //节点项的个数
04     case Integer of
05         1 : (OnlyItem : TItem); //仅有符号的相关信息
06         2 : (
07             Total : Word; //总计频度
08             Items : PItemAry; //节点项数组
09             Lesser : PNode; //指向对应的下一级上下文节点
10         );
11 end;

代码3.1 改进后的TNode的定义

  代码3.1中TNode的定义与代码2.1中的相比,增加了一个Total成员用以保存T(s)。另外增加了一个OnlyItem成员进行内存共用。如果当前上下文是二元上下文,则使用OnlyItem成员保存仅有的符号的频度信息,Total成员和Items成员不使用。如果当前上下文是多元上下文,则使用Total成员和Items成员,不使用OnlyItem成员。二元上下文与多元上下文相比,节约了一个TItemAry空间。符号的频度信息直接保存在对应的TNode中。编码时可以根据Count成员来确定当前的上下文是否是二元上下文。更新时,如果需要在二元上下文中添加符号。可以先分配两个元素的TItemAry,将当前TNode中的符号频度信息拷贝过去。再设置Total成员和Items成员。TNode本身不需要重新分配。当使用内存共用方案时,对于图2.1中所示的Trie结构,二元上下文“AB”与多元上下文“A”其内存空间的占用如图3.1所示。

图3.1 两种上下文节点的内存空间占用

  可以看出改进后的TNode节点的内存占用比以前增加了2个字节。现在一个TNode占用12个字节,一个TItem占用6个字节。由此我们可以推算出,当二元上下文占所有上下文的比例不低于三分之一时,采用内存共用方案可以减少模型占用的内存。而且二元上下文的比例越高,减少模型占用的内存也越多。以文件“book1”为例。不使用内存共用方案时,模型占用内存4396702个字节(≈4.19MB)。使用内存共用方案后,模型占用内存4065246个字节(≈ 3.88MB)。减少模型内存占用331456个字节(≈ 323.69KB)。可以看出,内存共用方案是非常有效的。

  需要说明的是,在实现一般PPM算法时不需要使用到TNode.Total成员。因为我们使用了排除符号方案,我们需要临时统计T’(s)。这时我们并不关心当前的T(s)。不过,在使用本章随后介绍的变体方案时会大量的访问到T(s)。所以这里设置并保留Total成员。

3.4 上下文Trie

  首先让我们来看一个例子,假设使用最高3阶的上下文,当前上下文为“ABC”,现在我们要编码符号“D”。假设在上下文“C”中找到了符号“D”,在上下文“ABC”和“BC”中都没有找到符号“D”。在更新时我们需要建立上下文节点“BCD”,但是这是一个空上下文节点。因为符号“D”第一次出现在上下文“BC”中,此时不可能有符号出现在上下文“BCD”中。既然如此,在下一轮编码中我们可以直接从上下文“CD”开始编码。没必要从最高阶上下文“BCD”开始。不仅如此,如果上下文节点“BCD”可以暂时不建立,等到需要使用时再建立。那么这将节约大量的内存。这样的建模结构就是本节将要介绍的上下文Trie结构。

  上下文Trie(Context Trie)最早由John G. Cleary、W. J. Teahan和Ian H. Witten在1995年提出[5]的。当初提出上下文Trie结构(以下简称“CT结构”)是为了解决无限长度上下文建模的内存占用问题。在提出CT结构的论文中[5],使用CT结构建模是不限制最高阶数的。但是只要稍加修改,CT结构也是可以应用在有限阶模型上的。本节介绍的就是CT结构在有限阶模型上的应用。另外,注意将CT结构与本文1.4节提到的上下文树(Context Tree)结构相区别。虽然它们的简称都是一样的,但是这是两种完全不同的数据结构。在本文中,CT结构指的是上下文Trie结构。

  CT结构是以Trie结构为基础,并增加了一个符号列表。每次编码一个符号时,就将这个符号追加到符号列表中。当一个符号第一次出现在上下文中时,需要将该符号添加到上下文节点中。此时并不建立对应的下层节点,而是将符号对应TItem.Link成员指向符号列表的相应位置。当这个符号第二次出现在对应的上下文中时,需要进行一系列的扩展操作。这个操作被称为扩展上下文(Extend Context)。扩展上下文时,需要建立符号对应的下层节点。同时,根据当前TItem.Link成员的指向来确定新建下层节点中需要添加的符号。不仅当前上下文需要扩展,比当前上下文低阶的其它上下文都需要进行扩展。以1.4节提出的例子为例,使用最高3阶的上下文编码数据“ABEACADABEA”。编码过程中,CT结构的变化过程如图3.2所示。

图3.2 模型结构的变化过程

  图3.2由多个小图组成,每个小图分为上下两个部份。上部为一个Trie结构,下部为符号列表。图中使用一个矩形方块来表示一个上下文节点。方块的上部标示出对应的上下文。方块下部的每个小方块对应于一个TItem。对于TItem中的Link成员使用箭头来表示。Link成员有两种情况。如果Link成员指向的是一个上下文节点,那么就使用实线箭头来表示。如果Link成员指向的是符号列表的相关位置,那么就使用虚线箭头来表示。对于图3.2的分析和说明如下。

  第1步,我们需要建立一个初始模型。初始模型包括一个空的0阶上下文节点“^”和一个符号列表。符号列表中只有一个空位。此时默认开始编码的上下文为“^”。初始化完成后的模型如图3.2 1)所示。

  第2步,我们需要编码符号“A”。我们将符号“A”放入到符号列表的当前空位。同时对符号列表进行扩展,在列表末尾增加一个空位。此时的当前上下文为“^”,符号“A”没有出现在上下文“^”中。编码完成后的当前上下文为-1阶上下文s-1。更新时,需要将符号“A”添加到上下文“^”中。由于这是符号“A”第一次出现在上下文“^”中。所以我们不需要建立对应的下层节点。而是将符号“A”对应的TItem.Link成员指向符号列表的当前空位。下一轮开始的上下文的阶数,比当前编码结束时的上下文的阶数高一阶。所以,下一轮编码从0阶上下文“^”开始。更新完成后的模型如图3.2 2)所示。

  第3步,编码符号“B”,过程同第2步基本一致。更新完成后的模型如图3.2 3)所示。

  第4步,编码符号“E”,过程同第2步基本一致。更新完成后的模型如图3.2 4)所示。

  第5步,编码符号“A”,当前的上下文为“^”。这是符号“A”在上下文“^”中的第二次出现。编码完成后当前上下文停留在0阶上下文“^”上。更新时,我们需要对符号“A”的频度加1。另外需要对上下文“^”进行扩展。此时需要建立一个新的1阶上下文节点“A”。新建节点“A”中必需添加一个符号,这个符号由符号“A”在上下文“^”中对应的TItem.Link成员来指定。对应的Link成员指向符号列表中的符号“B”,所以符号“B”将被添加到新建节点“A”中。符号“B”在上下文“A”中对应的Link成员的指向等于符号“A”在上下文“^”中对应的Link成员的指向顺序下移一个位置。最后,修改符号“A”在上下文“^”中对应的Link成员,将其指向新建的上下文节点“A”。这样一个上下文的扩展就完成了。此时,编码完成后的上下文为0阶上下文“^”,所以下一轮开始编码的上下文为1阶上下文“A”。更新完成后的模型如图3.2 5)所示。此时模型在内存中的分布如图3.3所示。

图3.3 模型的内存分布

  第6步,编码符号“C”,当前的上下文为“A”。符号“C”没有出现在上下文“A”中。后退1阶,符号“C”也没有出现在上下文“^”中。编码完成后的当前上下文为-1阶上下文s-1。更新时,需要将符号“C”添加到上下文“A”和上下文“^”中。符号“C”在这两个上下文中对应的TItem.Link成员都指向相同的位置。即,符号列表的当前空位。此时,下一轮开始编码的上下文是0阶上下文“^”。更新完成后的模型如图3.2 6)所示。

  第7步,编码符号“A”,当前的上下文为“^”。这是符号“A”第三次出现在上下文“^”中。所以更新时不需要扩展上下文“^”,直接切换到对应的上下文节点即可。与第5步类似,下一轮开始编码的上下文为上下文“A”。更新完成后的模型如图3.2 7)所示。

  第8步,编码符号“D”。与第6步类似,需要在两个上下文中添加符号。更新完成后的模型如图3.2 8)所示。

  第9步,编码符号“A”,过程同第7步基本一致。更新完成后的模型如图3.2 9)所示。

  第10步,编码符号“B”,当前的上下文为“A”。这是符号“B”在上下文“A”中的第二次出现。编码完成后当前上下文停留在上下文“A”上。更新时,我们需要对符号“B”的频度加1。进行扩展操作时,除了要扩展当前上下文“A”,还要扩展比上下文“A”低阶的上下文“^”。扩展时分别建立2阶上下文节点“AB”和1阶上下文节点“B”。注意一点,由于我们使用了排除更新方案,所以在扩展上下文“^”时不需要更新符号“B”在上下文“^”中的频度。比当前上下文“A”高一阶,下一轮开始编码的上下文为上下文“AB”。更新完成后的模型如图3.2 10)所示。

  第11步,编码符号“E”。这是符号“E”在上下文“AB”中的第二次出现。与第10步类似,这次需要扩展3个上下文。注意一点,在新建的3阶上下文“ABE”中包含了符号“A”。此时符号“A”对应的TItem.Link成员仍然指向符号列表。但是我们不需要利用Link成员来获得符号值,这时的Link成员只是为了区分指针的指向。因为3阶上下文已经是最高阶的上下文了,不需要再建立下层节点。与第10步类似,下一轮开始编码的上下文为“ABE”。更新完成后的模型如图3.2 11)所示。

  第12步,编码符号“A”。这是符号“A”在上下文“ABE”中的第二次出现。此时进行扩展操作时需要注意几点。首先,不能对上下文“ABE”进行扩展。因为上下文“ABE”是最高阶上下文。当对2阶上下文“BE”进行扩展时,会建立3阶上下文节点“BEA”。此时上下文“ABE”中的符号“A”对应的TItem.Link成员将指向上下文“BEA”。因为在我们的模型中,最高阶上下文中对应的Link成员指向兄弟上下文。其次,在本轮扩展操作中只需要对2阶上下文“BE”和1阶上下文“E”进行扩展。符号“A”在上下文“^”中已经被扩展过了。相同的符号在上下文中不能重复扩展。还有一点,符号列表的存在是为了在进行扩展操作时确定相邻的下一个符号。此时符号“A”出现在最高阶的上下文中,从3阶上下文“ABE”到0阶上下文“^”所有与符号“A”有关的扩展操作都已经完成。所以符号“A”可以从符号列表中删除。删除后的符号列表与第11步完成后的符号列表完全一致,就好象符号“A”没有被追加过一样。更新完成后的模型如图3.2 12)所示。

  从上面的例子中可以看出。当使用CT结构时,进行每一轮编码的时候,并不一定是从最高阶上下文sD开始的。如果当前匹配的上下文不是最高阶上下文,即,d(a) < D,那么下一轮编码从d(a) + 1阶上下文开始。如果当前匹配的上下文是最高阶上下文,那么下一轮编码也从最高阶上下文开始。

  另外在扩展上下文时,不仅是当前上下文要进行扩展,Zn中的更低阶的上下文也要进行扩展。扩展时需要对sd, sd-1, …, s0 , si ∈ Zn, d = d(a)每个上下文进行遍历,逐一进行扩展。如果扩展时遇到已经扩展过的上下文,那么扩展操作就可以停止了。因为如果一个上下文已经扩展过了,那么比它低阶的上下文肯定也已经扩展过了。有一个规律可以注意一下,如果一个符号在对应的上下文中没有被扩展过,那么这个符号在Zn中的不同上下文中对应的TItem.Link成员都指向符号列表的同一个位置。例如图3.2 12)中,符号“C”在上下文“BEA”、“EA”、“A”、“^”中对应的Link成员都指向符号列表中的符号“A”。利用这个规律在编写程序代码的时候可以进行适当的优化。

  从例子中我们还可以发现,当使用CT结构时,只有在扩展上下文时才会建立新的上下文节点。而且每次新建的上下文节点都是二元上下文节点。当把一个符号添加到上下文节点中时,才会出现多元上下文节点。这就意味着在CT结构中不会出现没有包含符号的空上下文节点。所以与Trie结构相比,CT结构可以使用更少的节点来建立模型。

  使用CT结构时同样可以使用排除更新方案。我们使用sH (D ≥ H ≥ 0)来表示每轮编码开始的上下文。使用排除更新时,对Zn中的sH, sH-1, …, sd , si ∈ Zn, d = d(a)上下文进行更新。这样与一般实现相比,使用CT结构时每轮只需要更新更少的上下文节点。这将会有助于降低算法的耗时。

  更新一个匹配上下文时需要判断对应的指针指向,以确定是否需要扩展上下文。一个符号在上下文中对应的TItem.Link成员有两种指向。一种指向上下文节点,另一种指向符号列表。如果指向上下文节点,那么说明这个符号在上下文中已经被扩展过了。判断Link成员的指向实际上很麻烦。首先,不能根据符号频度来判断Link成员的指向。因为模型更新时有可能需要进行削减频度。如果一个符号的频度为2,那么对应的Link成员肯定指向上下文节点。但是如果此时进行削减频度,那么这个符号的频度会变成1。下次更新时如果对这个符号进行扩展就会出现错误。其次,编码时符号列表需要不断的扩展,这也会给指针的指向带来麻烦。解决这个问题的方法是在TItem中增加一个成员,用以标记对应的符号是否已经被扩展过。不过这种方法会增加内存的占用。在本文的第四章将会介绍一个更好的解决方法,虽然这个方法需要同定量内存一起使用。

  比较图2.1和图3.2 12)可以发现,使用最高3阶编码同一份数据,Trie结构生成了20个上下文节点,而CT结构生成了9个上下文节点。虽然CT结构增加了一个符号列表,但是模型总体的内存占用比Trie结构减少了很多。而且在CT结构上也是可以使用内存共用方案的。现在先使用方案4.1进行测试,方案4.1的详细说明将在第四章中进行。方案4.1使用CT结构进行建模,同时使用了内存共用方案。现在使用5阶的方案4.1对语料库[14]中的18个文件进行压缩。测试输出编码结束时模型中节点的数量和相关比例。测试结果如表3.2所示。

文件 符号列表
长度
(字节)
节点项
个数
(个)
上下文
节点个数
(个)
二元上下文
节点个数
(个)
二元上下文
所占比例
(%)
bib 35148 50884 28298 15017 53.07
book1 175261 298820 104994 34052 32.43
book2 137376 216665 93067 39196 42.12
geo 95116 119264 44670 20521 45.94
news 144369 210357 103064 50271 48.78
obj1 14043 17956 7366 3464 47.03
obj2 99332 136646 77197 43048 55.76
paper1 24636 35964 19912 10022 50.33
paper2 33836 51214 24257 10555 43.51
paper3 24417 36064 17655 7726 43.76
paper4 8564 12363 6781 3238 47.75
paper5 7809 11237 6332 3107 49.07
paper6 18566 26994 15587 8077 51.82
pic 56702 77358 25421 6016 23.67
progc 18969 27059 15748 8416 53.44
progl 19609 28651 18684 10664 57.08
progp 14307 20523 15183 9434 62.14
trans 21206 30046 26643 18403 69.07
总计 949266 1408065 650859 301227 46.28

表3.2 方案4.1模型中节点的数量和比例

  比较表3.1和表3.2中的数据可以发现,使用方案4.1可以比方案2.1更节约内存。根据总计的结果,方案4.1与方案2.1相比节点项的个数减少了45.37%,上下文节点的个数减少了57.13%。同时我们也可以发现,二元上下文节点的个数减少了74.22%。二元上下文节点的大量减少,使得它在上下文节点中所占的比例也跟着减少。有两个文件“book1”和“pic”,其二元上下文比例降低到了三分之一以下。这时使用内存共用方案将不太合适。但是我们可以注意到,对于其它的大多数文件二元上下文仍然占有较大的比例。另外,使用内存共用方案时可以很方便的获取T(s)数据。所以使用内存共用方案还是可行的。

3.5 模型的规律

  同时使用区分削减方案、内存共用方案、CT结构建立起来的模型具有一定的规律。这些规律对于程序代码的编写以及变体方案的理解都具有很重要的意义。现在对这些规律归纳如下。

  规律一,在模型中不会出现零频符号和空上下文节点。使用区分削减方案可以确保模型中不会出现零频符号。使用CT结构可以确保模型中不会出现空上下文节点。不过我们注意到,当我们初始化模型时会建立一个空的0阶上下文节点。这时我们可以向0阶上下文节点中添加一个引导符号(Priming Symbol)来避免模型中出现空上下文节点。只要在解码时使用相同的引导符号,解码数据就不会出现错误。规律一意味着,对于每个上下文节点可以利用TNode.Count成员获取m(s)数据,可以利用TNode.Total成员获取T(s)数据。这两个数据在变体方案中将会被频繁的访问。

  规律二,同一分支中高1阶上下文中出现的符号数总是小于等于当前上下文中出现的符号数,即,m(sd+1) ≤m(sd) , si ∈ Zn。观察图3.2中的例子可以发现,添加符号时总是从0阶上下文s0一层一层逐步向上开始添加的。所以我们可以由此归纳出规律二。

  规律三,同一分支中高1阶上下文中出现的符号,在当前上下文中也全部都有出现,即,。这个规律同样通过观察图3.2中的例子可以归纳出来。推导规律三我们可以得出这样一个结论:在同一分支中,高阶上下文中出现的符号肯定会在低阶上下文中出现。另外,根据规律二和规律三我们可以发现。对于相差1阶的父子上下文sd+1和sd,如果m(sd+1) = m(sd),那么上下文sd+1中出现的符号和上下文sd中出现的符号将完全相同。这意味着在进行编码的时候。如果上下文sd+1已经完成编码,这时出现m(sd+1) = m(sd),那么可以跳过上下文sd不进行编码。如果上下文sd-1出现同样的情况,那么上下文sd-1也可以跳过。这样在编码的时候可以跳过一些上下文不进行编码,从而提高代码的效率。

  最后说明一点,以上归纳出来的规律仅适用于当前的模型。如果对模型的方案进行修改,比如修改区分削减方案或修改CT结构,那么以上规律和结论将不再成立。

  评论这张
 
阅读(986)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017