最近重读《集体智慧编程》,这本当年出版的介绍推荐系统的书,在当时看来很引领潮流,放眼现在已经成了各互联网公司必备的技术。
这次边阅读边尝试将书中的一些Python语言例子用C#来实现,利于自己理解,代码贴在文中方便各位园友学习。

由于本文可能涉及到的与原书版权问题,请第三方不要以任何形式转载,谢谢合作。

第二部分 聚类 - 发现群组

监督学习和无监督学习

利用样本输入和期望输出来学习如何预测的技术称为监督学习法。使用监督学习方法时,可以传入一组输入,应用程序可以根据此前学到的知识产生一个输出。
监督学习法包括如:神经网络、决策树、向量支持机及贝叶斯过滤等。
而这一部分介绍的聚类无监督学习的一种。无监督学习算法是在一组数据中找寻某种数据结构,如聚类算法,其目标是采集数据并从中找出不同的群组。
其他无监督学习方法还包括负矩阵因式分解和自组织映射。

单词向量

聚类算法所需的数据是一组有相同的数值型属性的数据项,我们的操作正是基于这些属性。

对博客用户分类

这个示例所使用的数据集是一组指定的词汇在排名前120的博客的RSS中出现的次数,根据这个来对这些博客进行分类。这个数据的一小部分如下表所示:

  "china" "kids" "music" "yahoo"
Gothamist 3 3 3 0
GigaOM 6 0 0 2
Quick Online Tips 0 2 2 22

根据单词出现的频度对博客聚类,可以分析出是否存在一类经常撰写相似主题的博客用户。
本文将略去构造这个数据源的过程,直接使用现成数据,可以在这里下载测试。这份文本中数据的格式与上面的表格差不多,只是以制表符作为分隔。后面所有代码都将基于这个格式的数据来实现。

这里稍微插一句构建测试数据的技巧,对于指定词汇的选择,可以使用一个范围如出现频率10%~50%进行过滤,这样可以去除像是the这样随处可见对聚类无意义的次,或是一些特别冷门的词,如合成词。排除这些干扰可以使后面的聚类结果更准确。

分级聚类

分级聚类通过连续不断地将最为相似的群组两两合并,来构造一个群组的层级结构。
具体方法是,先将单一元素,本例中就是博客,作为一个群组,在迭代过程中,分级聚类算法计算两个群组的距离(一开始时就是两个单一元素的距离),并将距离最近的两个群组合并为新群组,重复这个过程直到只剩一个群组。
按如上方式进行聚类,聚类的过程可以使用树状图来表示。树状图也可以体现构成聚类的元素之间间隔的远近,从而可以看出聚类中各元素间的相似程度,并以此指示聚类的紧密程度。

加载博客数据并进行聚类

下面的函数用于将上文提到的数据源加载到内存中用于聚类计算。

public Tuple<List<string>,List<string>,List<List<float>>> Readfile(string filename)
{List<string> lines = new List<string>();using (var fs = new FileStream(filename, FileMode.Open)){var sr = new StreamReader(fs);while (!sr.EndOfStream){var line = sr.ReadLine();if (!string.IsNullOrEmpty(line))lines.Add(line);}}if (lines.Count == 0)throw new Exception("文件为空");//第一行是列标题var colnames = lines[0].Trim().Split(new[] {'\t'}, StringSplitOptions.RemoveEmptyEntries).Skip(1).ToList();var rownames = new List<string>();var data = new List<List<float>>();foreach (var l in lines.Skip(1)){var p = l.Trim().Split(new[] {'\t'}, StringSplitOptions.RemoveEmptyEntries);// 每行第一列为行名rownames.Add(p[0]);// 剩余部分是行对应的数据data.Add(p.Skip(1).Select(float.Parse).ToList());}return Tuple.Create(rownames, colnames, data);
}

下一步来计算紧密度。由于一些博客比其他其他博客文章更多,或文章的长度比其他文章更长,这可能会导致这些博客比其他博客包含更多的词汇。我们使用皮尔逊相关度来确定这些博客的相关程度,由于皮尔逊相关度表示两组数据与某条直线的拟合程度,这样可以排除词汇量多少对相关性的影响(简单说,一个博客与另一个博客有差不多的相同词汇,但前者比后着的词汇数量更多,使用皮尔逊相关度计算两个博客的相关度依然会得出较高的结果)。这里的皮尔逊相关度计算函数接受两个博客词汇数列表作为参数,并返回这两个博客的相关度分值。

public float Person(List<float> v1,List<float> v2)
{//求和var sum1 = v1.Sum();var sum2 = v2.Sum();//求平方和var sum1Sq = v1.Sum(v => Math.Pow(v, 2));var sum2Sq = v2.Sum(v => Math.Pow(v, 2));//求乘积之和var pSum = v1.Select((v,i)=>v*v2[i]).Sum();//计算皮尔逊评价值var num = pSum - (sum1 * sum2 / v1.Count);var den = Math.Sqrt((sum1Sq - Math.Pow(sum1, 2) / v1.Count) * (sum2Sq - Math.Pow(sum2, 2) / v1.Count));if (den == 0) return 0;return 1 - num / (float)den;
}

由于在完全匹配的情况下,皮尔逊相关度的计算结果为1,而我们希望相关度高的两个元素“距离”更小,从而上面代码中使用1减去计算出的皮尔逊相关度。

接着我们构造一个数据结构来表示一个“聚类”。聚类可能是树中的叶节点,也可能事分支节点,对应我们的例子即可能是一个博客,也可能是几个博客聚类后的合并数据。

class BiCluster
{// 博客中词汇数量数组public List<float> Vec { get; set; } public BiCluster Left { get; set; }public BiCluster Right { get; set; }public int Id { get; set; }public float Distance { get; set; }
}

下面开始正式进入分类算法,分类算法以叶节点一级(即原始博客数据)聚类开始。函数的主循环中两两计算聚类相关度,以此找到最佳匹配。新生成的聚类的数据等于两个旧聚类求平均后的结果。然后重复这一过程直到只剩一个聚类。代码中一个优化的地方是保存每个配对的相关度计算结果,知道配对中的某一项被合并到另一个聚类中为止。

public BiCluster Hcluster(List<List<float>> rows, Func<List<float>, List<float>, float> distance)
{var distances = new Dictionary<long,float>();var currentclustid = -1;// 最开始的聚类就是数据集中的行var clust = rows.Select((data, i) => new BiCluster() {Vec = data, Id = i}).ToList();while (clust.Count>1){var lowestpair = Tuple.Create(0, 1);var closest = distance(clust[0].Vec, clust[1].Vec);//遍历每一个配对,寻找最小距离for (int i = 0; i < clust.Count; i++){for (int j = i+1; j < clust.Count; j++){//用distances缓存相关度计算值var key = ((long)clust[i].Id << 32) + clust[j].Id;if(!distances.ContainsKey(key))distances.Add(key, distance(clust[i].Vec,clust[j].Vec));var d = distances[key];if (d < closest){closest = d;lowestpair = Tuple.Create(i, j);}}}// 计算两个聚类的平均值var mergevec = clust[lowestpair.Item1].Vec.Select((v, i) => (v + clust[lowestpair.Item2].Vec[i])/2f).ToList();// 建立新聚类var newcluster = new BiCluster(){Vec = mergevec,Left = clust[lowestpair.Item1],Right = clust[lowestpair.Item2],Distance = closest,Id = currentclustid};//不在原始集合中,id为负数--currentclustid;var leftCluster = clust[lowestpair.Item1];var rightCluster = clust[lowestpair.Item2];clust.Remove(leftCluster);clust.Remove(rightCluster);clust.Add(newcluster);}return clust.FirstOrDefault();
}

在前面设计的存储聚类的数据结构BiCluster中保存了构成当前的聚类的两个原始聚类,可以使用这个信息,通过递归重建所有的中间聚类和叶节点。
下面的代码就可以测试聚类的生成。

Tester c = new Tester();
var datas = c.Readfile("blogdata.txt");
var clust = c.Hcluster(datas.Item3, c.Person);

可以使用下面的代码可视化的显示聚类的过程:

static void PrintClust(BiCluster clust, List<string> labels, int n = 0)
{// 缩进布局for (int i = 0; i < n; i++)Console.Write(" ");if (clust.Id < 0)                //分支Console.WriteLine("├");else Console.WriteLine($"{labels[clust.Id]}");++n;if (clust.Left != null) PrintClust(clust.Left,labels, n);if (clust.Right != null) PrintClust(clust.Right,labels, n);
}

制表符├表示接下来的是两个子聚类。使用如下测试代码可以看到运行的效果:

PrintClust(clust,datas.Item1);

列聚类

上面我们以行进行聚类得到了博客的聚类结果,如果反过来以列进行聚类可以得到单词的聚类结果,从而可以知道那些单词常常被放在一起使用。举另一个常见的例子,在消费者购物清单的聚类中,如果按行聚类可以将那些有相似消费习惯的用户划分到一起,而如果按列聚类则可以把用户常一起购买的商品集合计算出来以用于捆绑销售或相关商品推荐。
把上面计算改为列聚类最简单的方式就是将数据集转置,通过下面的函数可以简单完成:

public List<List<float>> RotatMatrix(List<List<float>> data)
{var newdata = new List<List<float>>();for (int i = 0; i < data[0].Count; i++){newdata.Add(data.Select(d=>d[i]).ToList());}return newdata;
}

然后测试代码也要进行调整:

Tester c = new Tester();
var datas = c.Readfile("blogdata.txt");
var rdata = c.RotatMatrix(datas.Item3);
var clust = c.Hcluster(rdata, c.Person);
PrintClust(clust,datas.Item2);

由于本身这种分级聚类算法的效率不高,移上列举类函数计算时间明显增加。

K值聚类

前文介绍的分级聚类有两个明显缺点,在没有额外处理的情况下,数据并没有真正拆分到不同组(只是以一棵二叉树的形式存在),且计算量非常大。计算量大的原因一是要计算每个项两两之间的关系,二是在项合并后要重新计算合并后项和其他项之间的关系。这导致在处理大规模数据时计算非常缓慢(如上面的列聚类)。
这一节介绍的K值聚类与分级聚类完全不同,其是通过传入的希望得到的聚类数量来根据数据的结构状态确定聚类的大小。
具体来说K值聚类的方法是,随机确定k个(按需求传入)中心位置(表示聚类中心),然后将各项分配到最邻近的中心点。分配完成后,将聚类的中心移动到分配给该聚类中心的所有项的中心位置,然后再次进行分配过程,直到分配结果不再发生变化为止。

public Dictionary<int,List<int>> Kcluster(List<List<float>> rows, Func<List<float>, List<float>, float> distance, int k = 4)
{// 确定每个点的最小值和最大值var ranges = new List<float[]>();for (var i = 0; i < rows[0].Count; i++){var min = rows.Min(r => r[i]);var max = rows.Max(r => r[i]);ranges.Add(new [] {min,max});}// 随机创建k个中心点var rnd = new Random();var clusters = new List<List<float>>();var rowLength = rows[0].Count;for (int j = 0; j < k; j++){var cluster = new List<float>();for (int i = 0; i < rowLength; i++){cluster.Add((float)rnd.NextDouble() * (ranges[i][1] - ranges[i][0]) + ranges[i][0]);}clusters.Add(cluster);}var lastmatches = new Dictionary<int, List<int>>(); ;for (int t = 1; t <= 100; t++){Console.WriteLine($"第{t}次迭代");var bestmatches = new Dictionary<int, List<int>>();for (int i = 0; i < k; i++){bestmatches.Add(i, new List<int>());}//在每一行中寻找距离最近的中心点for (int j = 0; j < rows.Count; j++){var row = rows[j];var bestmatch = 0;for (int i = 0; i < k; i++){var d = distance(clusters[i], row);if (d < distance(clusters[bestmatch], row))bestmatch = i;bestmatches[bestmatch].Add(j);}}// 如果结果与上一次相同,则整个过程结束if (CompareDic(bestmatches, lastmatches)) break;lastmatches = bestmatches;// 把中心点移到其所有成员的平均位置处for (int i = 0; i < k; i++){var avgs = ArrayList.Repeat(0.0f, rows[0].Count).Cast<float>().ToList();if (bestmatches[i].Count > 0){foreach (var rowid in bestmatches[i])for (int m = 0; m < rows[rowid].Count; m++)avgs[m] += rows[rowid][m];for (int j = 0; j < avgs.Count; j++)avgs[j] /= bestmatches[i].Count;clusters[i] = avgs;}}return bestmatches;}throw new Exception("超过最大迭代次数");
}

算法中用到的Dictionary比较方法如下:

private bool CompareDic(Dictionary<int, List<int>> first, Dictionary<int, List<int>> second)
{if (first == second) return true;if ((first == null) || (second == null)) return false;if (first.Count != second.Count) return false;foreach (var kvp in first){List<int> secondValue;if (!second.TryGetValue(kvp.Key, out secondValue)) return false;if (!kvp.Value.OrderBy(t => t).SequenceEqual(secondValue.OrderBy(t => t))) return false;}return true;
}

与分级聚类相比,这个算法产生最终结果所需要的迭代次数是非常少的。算法最终返回k组序列,其中每个序列表示一个聚类。
下面的代码可以测试上面算法:

可以明显感觉到,k值聚类算法比分级聚类算法快很多。

Tester c = new Tester();
var datas = c.Readfile("blogdata.txt");
var clust = c.Kcluster(datas.Item3, c.Person,k:10);
foreach (var ckvp in clust)
{Console.WriteLine($"聚类 {ckvp.Key} :{string.Join(",",ckvp.Value.Select(i=>datas.Item1[i]))}");
}

附:Tanimoto系数

对于数据类型是1和0的集合类型(如一个用户喜欢某些商品的一部分,对于喜欢的物品记录为1,不喜欢的物品记为0),判断相关性的最佳办法就是Tanimoto系数。Tanimoto系数定义很简单,就是交集与并集的比率。下面的函数就可以很简单的实现这个算法:

public float Tanimoto(HashSet<bool> v1, HashSet<bool> v2)
{return 1.0f - (float) v1.Intersect(v2).Count()/v1.Union(v2).Count();
}

参数v1和v2是两个集合,集合中元素为bool类型表示0和1。返回值为0.0到1.0之间值,0.0表示两个人喜欢的物品完全相同,1.0表示两个人喜欢的物品完全相同。

转载于:https://www.cnblogs.com/lsxqw2004/p/6017552.html

《集体智慧编程》读书笔记2相关推荐

  1. 全球通史读书笔记上(第六章——古代文明的新起)

    一.印度文明 1. 哈巴拉文化消失的原因 (1)被雅利安人所破坏 (2)奴隶主阶级的沉重剥削. (3)过度砍伐森林,水土流失,生态失衡 2. (1)印度的楼兰--摩亨约·达罗(或为最早期的印度帝国) ...

  2. 全球通史读书笔记上(第七章——战争的起源)

    一.梭伦改革 1.梭伦改革地点:希腊雅典.雅典在希腊文明史中扮演重要的角色.伯利克里:"雅典是全希腊的学校." 2.梭伦改革背景:公元前6世纪,雅典的奴隶制度确立.贵族成为特权阶层 ...

  3. 《全球通史》读书笔记2

    关键问题似乎在于,在技术变革和使之成为必需的社会变革之间,存在一个时间差.造成这个时间差的原因在于:技术变革能提高生产率和生活水平,所以很受欢迎,且很快便被采用:而社会变革则要求人类进行自我评估和自我 ...

  4. 《全球通史》读书笔记1

    这两天开始读斯塔夫理阿诺斯的<全球通史>第7版. 在推荐序里的一句话读了特别有感觉."--我们甚至依然在用别人的模式理解我们和整个世界的历史."我有时也在想,我们的历史 ...

  5. 读 L. S. Stavrianos 之 《全球通史:从史前到21世纪》

    Leften Stavros Stavrianos, 吴象婴, 梁赤民. 全球通史:从史前到21世纪:第7版新校本.上册. ISBN: 978-7-301-26938-1. Leften Stavro ...

  6. 读书笔记|如何让用户为你的产品尖叫

    文/PM十二   编辑/李老太.小太阳  Hi各位小伙伴,最近新认识的一位从事编辑的小伙伴推荐了<用户思维+:好产品让用户为自己尖叫>,趁着周末把它读完了,因此今天要分享的是一篇读书笔记. ...

  7. 《精通Unix下C语言与项目实践》读书笔记(16)

    <精通Unix下C语言编程与项目实践>读书笔记(new) 文章试读  不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八 ...

  8. 第一篇读书笔记,关于UML和模式应用(1)--书籍简介

    新添加了一个读书笔记分类,以后多写一些读书笔记吧.因为真的觉得自己技术太差了,写不出好文章了. 关于UML和模式应用(1)--书籍简介 Applying UML and patterns(Craig ...

  9. PMP读书笔记(第13章)

    大家好,我是烤鸭:     今天做一个PMP的读书笔记. 第十三章 项目相关方管理 项目相关方管理 项目相关方管理的核心概念 项目相关方管理的趋势和新兴实践 裁剪考虑因素 在敏捷或适应型环境中需要考虑 ...

  10. PMP读书笔记(第12章)

    大家好,我是烤鸭:     今天做一个PMP的读书笔记. 第十二章 项目采购管理 项目采购管理 项目采购管理的核心概念 项目采购管理的趋势和新兴实践 裁剪考虑因素 在敏捷或适应型环境中需要考虑的因素 ...

最新文章

  1. [转载]C# 二进制与十进制,十进制与十六进制相互转换
  2. Iframe 用法浅析
  3. java 使用new新建一个对象时的操作过程
  4. oracle 11g 忘记了sys,system,scott密码
  5. java实现三个数字的随机组合_JAVA编程实现随机生成指定长度的密码功能【大小写和数字组合】...
  6. Loj#2035-[SDOI2016]征途【斜率优化】
  7. android过滤html标签,Android中处理TextView带超链接HTML标签的点击事件处理方法
  8. 怎样高效阅读一份深度学习项目代码?
  9. Blizzard Transitions for Mac - 动态风雪过渡效果FCPX转场
  10. Java 笔试题---Java与编程模式--7月6日
  11. 项目“”受源代码管理,向源代码管理注册此项目时出错。。
  12. 入华五周年,微软亮AI、云计算成绩,制定“二五”新战略...
  13. 盘点国内外私募基金业绩报酬计提方式
  14. XGBOOST_航班延误预测
  15. 使用postfix和roundcube搭建webemail
  16. 万能五笔输入法弹窗_万能五笔输入法的广告怎么关闭
  17. 【过关斩将】高胜寒带你理清 “为什么从上家公司离职?”
  18. 既然Talk is cheap, 那么就用代码教你如何进行正交设计
  19. nginx参数sendfile
  20. Python pivot详解

热门文章

  1. git rebase 合并中间的提交
  2. java stream intermediateterminal方法
  3. python datetime datetime
  4. javascript Node对象
  5. WSGI Middleware
  6. Python Logging Loggers
  7. MySQL group-by-modifiers
  8. Pandas 文本数据方法 strip( ) lstrip( ) rstrip( )
  9. 怎么创建数据表的实体类和业务类_SSM搭建二手市场交易平台(二):数据表设计...
  10. 国二C语言大题评分,计算机二级C语言题型和评分标准