数据挖掘入门与实战 公众号: datadw
一、前言
写这篇文时,突然想到一个问题,大家的词库都是从哪来的?
之所以会这么有些意外的问,是因为从没把词库当成个事儿:平时处理微博,就用程序跑一下微博语料获得微博词库;处理新闻,程序跑一下新闻语料获得新闻词库。甚至没有把跑出来的词库存下来的习惯,谁知道过两天是不是又出什么新词,与其用可能过时的,不如随手生成个新鲜出炉的。
好吧,我承认我这是在显摆。如果你也想和我一样,想要随用随丢,任性它一把,那随我来。
如果你只想要这样一个程序,可以直奔这里下载。
回复公众号"词库"获取。
如果你想亲手写一个,那也没什么,百来行代码的事儿。
好,咱们言归正传。
二、词库生成
1、算法分析,先来考虑以下几个问题
问:目标是从文本中抽取词语,是否可以考虑使用遗忘的方法呢?
答:可以,词语具备以相对稳定周期重复再现的特征,所以可以考虑使用遗忘的方法。这意味着,我们只需要找一种适当的方法,将句子划分成若干子串,这些子串即为“候选词”。在遗忘的作用下,如果“候选词”会周期性重现,那么它就会被保留在词库中,相反如果只是偶尔或随机出现,则会逐渐被遗忘掉。
问:那用什么方法来把句子划分成子串比较合适呢?
答:考察句中任意相邻的两个字,相邻两字有两种可能:要么同属于一个共同的词,要么是两个词的边界。我们都会有这样一种感觉,属于同一个词的相邻两字的“关系”肯定比属于不同词的相邻两字的“关系”要强烈一些。
数学中并不缺少刻划“关系”的模型,这里我们选择公式简单并且参数容易统计的一种:如果两个字共现的概率大于它们随机排列在一起的概率,那么我们认为这两个字有关,反之则无关。
如果相邻两字无关,就可以将两字中间断开。逐字扫描句子,如果相邻两字满足下面的公式,则将两字断开,如此可将句子切成若干子串,从而获得“候选词”集,判断公式如下图所示:
公式中所需的参数可以通过统计获得:遍历一次语料,即可获得公式中所需的“单字的频数”、“相邻两字共现的频数”,以及“所有单字的频数总和”。
问:如何计算遗忘剩余量?
答:使用牛顿冷却公式,各参数在遗忘算法中的含义,如下图所示:
牛顿冷却公式的详情说明,可以参考阮一峰老师的博文《
基于用户投票的排名算法(四):牛顿冷却定律
》。
问:参数中时间是用现实时间吗,遗忘系数取多少合适呢?
答:a、关于时间:
可以使用现实时间,遗忘的发生与现实同步。
也可以考虑用处理语料中对象的数量来代替,这样仅当有数据处理时,才会发生遗忘。比如按处理的字数为计时单位,人阅读的速度约每秒5至7个字,当然每个人的阅读速度并不相同,这里的参数值要求并不需要特别严格。
b、遗忘系数可以参考艾宾浩斯曲线中的实验值,如下图(来自互联网)
我们取6天记忆剩余量约为25.4%这个值,按每秒阅读7个字,将其代入牛顿冷却公式可以求得遗忘系数:
注意艾宾浩斯曲线中的每组数值代入公式,所得的系数并不相同,会对词库的最大有效容量产生影响。
2、算法的代码实现(C#版)
呼,算法分析完成,相比于上面的文档,还是写代码要容易的多。
2.1、候选词生成
[csharp]
view plain
copy
-
-
-
-
-
-
-
-
-
public
static
void
UpdateKeyWordColl(
string
text, MemoryBondColl<
string
> objCharBondColl, MemoryItemColl<
string
> objKeyWordColl,
bool
bUpdateCharBondColl =
true
,
bool
bUpdateKeyWordColl =
true
)
-
{
-
if
(String.IsNullOrEmpty(text))
return
;
-
-
StringBuilder buffer =
new
StringBuilder();
-
string
keyHead = text[0].ToString();
-
buffer.Append(keyHead);
-
for
(
int
k = 1; k
-
{
-
-
-
string
keyTail = text[k].ToString();
-
if
(bUpdateCharBondColl)
-
{
-
-
DictionaryDAL.UpdateMemoryBondColl<
string
>(keyHead, keyTail, objCharBondColl);
-
}
-
if
(bUpdateKeyWordColl)
-
{
-
-
if
(!DictionaryDAL.IsBondValid<
string
>(keyHead, keyTail, objCharBondColl ) )
-
{
-
-
string
keyword = buffer.ToString();
-
-
DictionaryDAL.UpdateMemoryItemColl<
string
>(keyword, objKeyWordColl);
-
-
buffer.Clear();
-
-
buffer.Append(keyTail);
-
}
-
else
-
{
-
-
buffer.Append(keyTail);
-
}
-
}
-
-
keyHead = keyTail;
-
}
-
}
2.2、相邻字统计
[csharp]
view plain
copy
-
-
-
-
-
-
-
public
static
void
UpdateCharBondColl(
string
text, MemoryBondColl<
string
> objCharBondColl)
-
{
-
if
(String.IsNullOrEmpty(text))
return
;
-
string
keyHead = text[0].ToString();
-
for
(
int
k = 1; k
-
{
-
string
keyTail = text[k].ToString();
-
-
DictionaryDAL.UpdateMemoryBondColl<
string
>(keyHead, keyTail, objCharBondColl);
-
keyHead = keyTail;
-
}
-
}
2.3、切片算法
[csharp]
view plain
copy
-
-
-
-
-
-
-
-
-
-
public
static
bool
IsBondValid
(T keyHead, T keyTail, MemoryBondColl
objMemoryBondColl )
-
{
-
-
if
(!objMemoryBondColl.Contains(keyHead) || !objMemoryBondColl.Contains(keyTail))
return
false
;
-
-
-
double
dHeadValidCount = CalcRemeberValue
(keyHead, objMemoryBondColl);
-
double
dTailValidCount = CalcRemeberValue
(keyTail, objMemoryBondColl);
-
-
double
dTotalValidCount = objMemoryBondColl.MinuteOffsetSize;
-
-
if
(dTotalValidCount <= 0)
return
false
;
-
-
-
MemoryItemColl
objLinkColl = objMemoryBondColl[keyHead].LinkColl;
-
if
(!objLinkColl.Contains(keyTail))
return
false
;
-
double
dShareValidCount = CalcRemeberValue
(keyTail, objLinkColl);
-
-
-
return
dShareValidCount / dHeadValidCount > dTailValidCount / dTotalValidCount;
-
-
}
2.4、牛顿冷却公式
[csharp]
view plain
copy
-
-
-
-
-
-
-
-
-
-
public
static
double
CalcNetonCooling(
double
parameter,
double
interval)
-
{
-
return
Math.Exp(-1 * parameter * interval);
-
}
2.5、遗忘是在词入库的时候计算的(其实算法核心仅此一行)
[csharp]
view plain
copy
-
-
-
-
-
-
-
public
static
void
UpdateMemoryItemColl
(T keyItem, MemoryItemColl
objMemoryItemColl)
-
{
-
-
if
(!objMemoryItemColl.Contains(keyItem))
-
{
-
-
-
-
MemoryItemMDL
mdl =
new
MemoryItemMDL
();
-
mdl.Key = keyItem;
-
mdl.TotalCount = 1;
-
mdl.ValidCount = 1;
-
mdl.ValidDegree = 1;
-
objMemoryItemColl.Add(mdl);
-
}
-
else
-
{
-
-
-
-
MemoryItemMDL
mdl = objMemoryItemColl[keyItem];
-
-
double
dRemeberValue = MemoryDAL.CalcRemeberValue(objMemoryItemColl.OffsetTotalCount - mdl.UpdateOffsetCount, objMemoryItemColl.MinuteOffsetSize);
-
mdl.TotalCount += 1;
-
-
mdl.ValidDegree = mdl.ValidDegree * ((mdl.ValidCount * dRemeberValue) / (mdl.ValidCount * dRemeberValue + 1)) + (1 - mdl.ValidCount * (1 - dRemeberValue)) * (1 / (mdl.ValidCount * dRemeberValue + 1));
-
mdl.ValidCount =mdl.ValidCount* dRemeberValue+ 1;
-
mdl.UpdateOffsetCount = objMemoryItemColl.OffsetTotalCount;
-
}
-
-
objMemoryItemColl.OffsetTotalCount += 1;
-
}
2.6、按词权重排序显示词库(词的权重将在后面的文章中详解)
[csharp]
view plain
copy
-
-
-
-
-
-
-
-
public
static
string
ShowKeyWordWeightColl(MemoryItemColl<
string
> objMemoryItemColl,
int
nKeyWordTopCount,
bool
bIsOnlyWord =
true
)
-
{
-
StringBuilder sb =
new
StringBuilder();
-
sb.AppendLine(String.Format(
" 【{0}】 | {1} | {2} | {3}"
,
"主词"
,
"遗忘词频"
,
"累计词频"
,
"词权值"
));
-
-
var tbuffer = from x
in
objMemoryItemColl
-
-
where !bIsOnlyWord || x.Key.Length > 1 && !Regex.IsMatch(x.Key,@
"\p{P}"
)
-
-
orderby x.ValidCount <= 0 ? 0 : (x.ValidCount )* (Math.Log(objMemoryItemColl.MinuteOffsetSize) - Math.Log(x.ValidCount)) descending
-
select x;
-
var buffer = (tbuffer).Take(nKeyWordTopCount);
-
sb.AppendLine(String.Format(
"================ 共{0} 个 ================== "
, buffer.Count()));
-
-
foreach
(var x
in
buffer)
-
{
-
sb.AppendLine(String.Format(
" 【{0}】 | {1} | {2} | {3}"
, x.Key, Math.Round(DictionaryDAL.CalcRemeberValue<
string
>(x.Key, objMemoryItemColl), 2), x.TotalCount, Math.Round((x.ValidCount <= 0 ? 0 : (x.ValidCount ) * (Math.Log(objMemoryItemColl.MinuteOffsetSize) - Math.Log(x.ValidCount))), 4)));
-
}
-
return
sb.ToString();
-
}
2.7、清理词频低于阀值的词
[csharp]
view plain
copy
-
-
-
-
-
-
-
public
static
void
ClearMemoryItemColl
(MemoryItemColl
objMemoryItemColl,
double
dMinValidValue=1.25)
-
{
-
-
for
(
int
k = objMemoryItemColl.Count - 1; k >= 0; k--)
-
{
-
-
-
-
-
if
(objMemoryItemColl[k].ValidCount
-
}
-
}
3、该算法生成词库的特点
3.1、无监督学习
3.2、O(N)级时间复杂度
3.3、训练、执行为同一过程,可无缝处理流式数据
3.4、未登录词、新词、登录词没有区别
3.5、领域自适应:领域变化时,词条、词频自适应的随之调整
3.6、算法中仅使用到频数这一语言的共性特征,无需对任何字符做特别处理,因此原理上跨语种。
三、词库成熟度
由于每个词都具备一个相对稳定的重现周期,不难证明,当训练语料达到一定规模后,在遗忘的作用下,每个词的词频在衰减和累加会达到平衡,也即衰减的速度与增加的速度基本一致。成熟的词库,词频的波动就会比较小,利用这个特征,我们可以衡量词库的成熟程度。
词频的这种波动也有一些妙用,比如:
微博中重复发的广告、
突然暴发的热点事件等情况,都会因词频的非正常变化而导致这些词的成熟度变低。
鉴于目前所用的模型,属于顺手写的,算不上满意,也没在这块花精力,这里就不再细讲。源代码中包含具体的方法,算是抛砖引玉,哪位兄弟找到更好的模型公式,记得跟俺说一声。
修改说明,上传的代码中,成熟度公式修改为:
[csharp]
view plain
copy
-
-
-
-
-
-
-
-
-
-
-
-
-
public
static
double
CalcValidDegree
(MemoryItemMDL
mdl,
double
dRemeberValue)
-
{
-
return
mdl.ValidDegree * dRemeberValue + Math.Abs(1 - mdl.ValidCount * (1 - dRemeberValue));
-
}
四、演示程序下载
回复公众号"词库"获取。
使用内附语料(在“可直接运行的演示程序”下可以找到)生成词库效果图如下:
数据挖掘入门与实战
搜索添加微信公众号:datadw
教你机器学习,教你数据挖掘
长按图片,识别二维码,点关注
公众号: weic2c
据分析入门与实战
长按图片,识别二维码,点关注