python写一个类600行代码_带你领略算法的魅力,一个600行代码的分词功能实现(一)...

为什么要说分词呢?其实这个话题挺大的。所以准备分几篇来写,这次先写第一篇。

写给别人看,也写给自己。毕竟,自己在思特奇也做了好久了,写点有意思的东西,结交一些有兴趣的朋友。

一是确实最近的一些实践给了自己很大的启发,另一方面,所谓互联网下半场的来临,那么,如果从一个增量的系统到一个存量的系统,那么数据挖掘就显得尤为重要了。怎么挖掘?或者说,怎么让计算机去帮我们挖掘呢?

不可否认,网上的一些大数据学习的资料,是翻译过来的,因为个人水平的不同,有时候很容易把人带到沟里去。尤其在牵扯到了一些算法的解释,更是莫名其妙,让不少初学者往往觉得这门课太难了。实际上,我可以说,它非常简单。简单到你几乎可以从你的生活中去恍然大悟。

经过持续的沉淀,我觉得从分词来切入是非常合适的。(这里特别感谢billing的王金山,感谢你的帮助)

我并不是想重复造轮子,而是通过造轮子,来解释一些基本的现象,以及一些算法为什么要这么用。如果你想做的更好,不停留在被动程序员的层次,那么希望你有兴趣读一下,或许对你有帮助,当然,也希望拍砖。

最近和一个朋友聊天,聊到了机器学习,说到大数据的应用,他说,网上说的这些名词,大多数都太空泛了。真正做到实践中去的,也就是几个大公司而已,中小公司基本没有这个实力。个人看数据挖掘,除了一些工具套路,实际上你真的会大数据吗?实际上,就算在大公司,目前的数据挖掘,也往往是刚起步而已。

和我说话的这个人,是今日头条的算法专家。

确实,怎么说是刚起步呢?

其实机器学习并不那么高深,所谓的机器大脑,目前阶段说穿了就一句话,"用概率学解释当前的事件是否可以映射成为另一个事件。

那么,这句话怎么理解?无论是什么学习,80%都离不开两个算法(实际是一个,另一个是这个的扩展),马尔科夫模型和贝叶斯算法。只要你能够沉淀一些这两个算法的知识和实践,当今大多开源机器学习,归类算法,推荐算法你都能玩的游刃有余。因为这些大部分都是以这两个为基础的变种。

什么东西都必须实践才能有沉淀,我一直秉持的这个观念。

分词和自然语言语音识别,现在取得了巨大的成就,基本上能把误差缩小到5%以内。甚至口音,方言,对于自然语言识别已经不是问题。分词而言,模糊的错误分词,对于识别也不再是难事。靠的是什么,靠的就是马科莫夫和贝叶斯。所以,我想从分词的角度,来阐释概率是如何计算出来的,以及显示和隐式到底是什么意思。

这不是什么了不得的技术,你我一样能轻松掌握,让我们从底层一步步来看看它是怎么实现的。

首先,要分词,先要有词库(废话)。

我们先不管词库是怎么来的(下几个帖子我会详细的解释,如何通过训练材料获得词库)。反正不是你一个个词敲击进去得到的。这样的方法简直是不可想象的。当然,你也可以这么做,不过你要花费很长的时间,还未必能达到你要的结果。现在,每天都会发生新的词汇,比如说"十动然拒"。你手动录入肯定是跟不上的。

一个真正的词库,实际一个txt足矣。

我的字库的组织方式是

一万条 4 m

一万次 16 m

一万步 30 m

一万盏 4 m

一万种 2 m

一万贯 4 m

一万遍 9 m

一万里 4 m

一万间 5 m

一行包含了三个元素,第一个是词本身,第二个是词在训练资料里出现的频率(词频),最后一个是词性标注(名词,动词,形容词,介词等等)

后两个参数我们先不用管(以后我会详细解释,为什么词典里需要后面两个参数,这两个参数是用于提炼一个句子中的关键词而是用的,也就是抽象一个句子的含义,我们需要后面两个参数),先以最简单的,只是单词为准。

我们有了词典,这里面大概有38万个中文词组。(目前以2016年6月份为准的中文词汇统计,差不多就这么多)

那么我们如何加载到内存里去效率最高呢?

或许,你会想,最简单的办法,给定一个句子,

比如:

"哪里见过你呀,朋友"

我可以for 38万次,用字符串比较看看是否每个词在这个句子里是包含的,直接用strstr()搞定。

是的,这样的办法很暴力,在服务器上,或许你还行,在一些运行效能低一些的机器上,可能就不行了。

有没有更好的办法?做到最高的判断效率?

当然有的,这就是算法的魅力了,先看看,我们怎么组织词典数据?

我们可以以树的形式记录词典,那么,怎么把38万个词变成一棵树呢?

我们知道,每个词,无论长短,它都是由一个个中文字符组成,我们把这个字作为一个个单独的关联元素。

那么我可以这样的组织数据

b比如,这几个词,"你" "你们" "你们的" "你的" "我" "他"

我们可以用上述方法无限类推,把38万的词,转换为一棵树。

这棵树和一般的树有些不一样,为了提高检索效率,我们必须把树种的子节点,变换为一个hash数组,这样,在给定任何一个字的基础上,我们都能快速获得这个字在所在层级的子节点位置。

我们可以给出节点的设定

//永久节点模型

struct _RuneLinkNode

{

CHashTable m_hmapRuneNextMap;

_Rune m_objRune;

char m_pWord[MAX_WORD_LENGTH];

int m_nPoolIndex;

char m_cUsed; //0为未使用,1为使用

};

最后一个叶子节点,也就是一个词的末尾,必须记录整个链条上的完整词汇,也就是m_pWord,便于抽取。过程节点,如果不是一个完整的词汇,则不必在记录这个词汇。

那么,给定一个句子,如可快读的遍历这棵树呢?

其实很简单,就和小孩子的积木形状填空游戏毫无二致。

还是那句话 "哪里见过你呀,朋友"

我们可以循环从这个字的第一个字开始,在树上寻找,如果找到这个字(哪),再获取这个字的下一个字(里),在这个字(哪)的下一个hashmap数组里去寻找(里),如果这个字(里)还有子节点,那么继续在这个子节点去寻找下一个字(见),直到找不到为止。

看上去可能有点绕口,那么程序上怎么实现呢?每次需要记录上一个字的位置,很烦的,因为我根本不知道一个词会有多少个字,比如"南无阿弥佗佛妈咪妈咪哄"这个词,我要记录多少次呀?是的,看上去很烦,但是,记得程序中有一个叫做递归的东西不?我们可以用一个递归函数,34行解决这个问题。

_RuneLinkNode* CWordBase::Set_HashMap_Word_Tree(_RuneLinkNode* pRuneNode, _Rune* pRune, int nLayer)

{

int nOfficeSet = pRuneNode->m_hmapRuneNextMap.Get_Hash_Box_Data((char* )pRune->m_szRune);

if(nOfficeSet > 0)

{

_RuneLinkNode* pCurrRuneNode = m_objNodePool.Get_NodeOffset_Ptr(nOfficeSet);

if(pCurrRuneNode->m_objRune == (*pRune))

{

//如果找到了,则返回当前节点

return pCurrRuneNode;

}

}

//如果没找到,则创建新的

_RuneLinkNode* pNode = m_objNodePool.Create(nLayer);

//printf("[CWordBase::Set_HashMap_Word_Tree]pNode=0x%08x.\n", pNode);

if(NULL == pNode)

{

printf("[CWordBase::Set_HashMap_Word_Tree]node pool is empty.\n");

return NULL;

}

int nNodeOffset = m_objNodePool.Get_Node_Offset(pNode);

pNode->m_objRune = (*pRune);

int nPos = pRuneNode->m_hmapRuneNextMap.Add_Hash_Data((char* )pRune->m_szRune, nNodeOffset);

if(-1 == nPos)

{

printf("[CWordBase::Set_HashMap_Word_Tree]tree node child is full.\n");

m_objNodePool.Delete(pNode);

return NULL;

}

return pNode;

}

这样,我们只需要按照肉眼的方式,读一遍句子,所有的词就被切分出来了。因为每层树的子节点都是hashmap,所以词的命中几乎和你读一遍句子for循环的次数是一致的,也就是说,"哪里见过你呀,朋友" 9个字我只需要9次循环就得到了全部的分词,比38万次强多了吧。分词结果是

[Cut]/哪里/见过/你/呀/,/朋友/

你看,机械分词就这么简单。那么,你可能会问。

分词是简单了,但是加载38万的词库,要消耗不少时间吧。

是的,38万个词,形成树,在我的测试服务器上,要消耗40秒左右。

我不可能为没次加载都付出40秒,启动太慢了,尤其在程序core掉的时候,有没有办法达到0秒启动呢?

当然,借助共享内存,我们可以把一棵树完整的压到共享内存中去,这样,当程序重启的时候,我只要得到共享内存的首地址,整个树就会瞬间还原了。怎么做呢?

树的主节点,可以是共享内存的开始节点。后面的叶子节点中的数据,我们不在用Node指针记录,直接用相对主节点的偏移量记录即可。使用的时候,我根据主节点的内存地址+偏移长度,直接还原成当前指针就可以了。所有的节点,都使用pool内存池的方式存储,用到添加节点的时候,从pool里面取得就行。

整个机械分词法+共享内存,也就500来行代码就搞定了,有兴趣可以看看我的实现。

完整代码,请看我的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值