diff --git a/2022/07/03/hidden-blogs/index.html b/2022/07/03/hidden-blogs/index.html new file mode 100644 index 000000000..c2b9eedb1 --- /dev/null +++ b/2022/07/03/hidden-blogs/index.html @@ -0,0 +1,773 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 隐藏博客 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

隐藏博客

+ + + +
+ +
+
+ + +
+
+
+ + + + + +
+
+ + + + + + +
+
+
隐藏博客
+
https://zhangzhao219.github.io/2022/07/03/hidden-blogs/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/07/03/postgraduate-recommendation/index.html b/2022/07/03/postgraduate-recommendation/index.html new file mode 100644 index 000000000..6ba442f45 --- /dev/null +++ b/2022/07/03/postgraduate-recommendation/index.html @@ -0,0 +1,839 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2022保研经历 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

2022保研经历

+ + +
+ +

2022级推免基本告一段落,刚开始夏令营的时候其实还没有什么疫情,但是各大高校仍然基本选择了线上夏令营,因此造成了夏令营非常非常卷。我本人将比我们学校好的985基本全部报了一遍,总共报了20多个,最后入营的非常少。不幸中的万幸我以为根本不可能考虑我的中科院计算所在我没入营没联系老师的情况下仍然打电话叫我过去考试,最终拿到了offer。计算所offer确认的时间比较早,是9月10日,而经过夏令营入营惨痛的经历,我决定就不参加预推免了。

+ +

本人情况

+
    +
  • 某中流985大数据专业
  • +
  • rank:3/65,一等奖学金,优秀学生
  • +
  • 竞赛:天梯赛个人二等奖,程序设计竞赛校二,CCF250分;数学建模国赛省三,亚太杯二等奖,美赛H奖;大英赛三等奖;服创国二,计算机设计大赛省二
  • +
  • 科研经历:0论文,1国家级大创1校级大创(因为参与度不高全程夏令营都没有提及),跟本科老师做的项目(1专利1软著,论文正在写)
  • +
  • 英语:六级540,四级560
  • +
  • 最终去向:中科院计算所网数重点实验室学硕
  • +
  • 参加的夏令营(均为学硕):
    +南京大学计算机科学与技术系(笔试挂)
    +北京理工大学计算机学院(优营)
    +北京航空航天大学计算机学院(替补)
    +天津大学智能与计算学部(优营)
    +华东师范大学计算机科学与技术学院(优营)
    +华中科技大学计算机科学与技术学院(优营)
    +中山大学计算机学院(软件学院)(替补)
  • +
  • 没有入营但是去面试的:中科院计算所
  • +
  • 入营但是没有去面试的:东南大学计算机科学与工程学院
  • +
  • 没有参加预推免
  • +
+

入营经历

+

南京大学计算机科学与技术系(线上)

+

南京大学今年养了一个大鱼塘,就拿我们专业来说,65人的小专业前三名都通过了南大的初审。当时接到了南大的邮件激动坏了,然而南大先搞了一波笔试。。。

+
    +
  • 笔试:笔试的内容是408,27道选择题,一个小时,双机位监考。选择题有单选有多选,多选的选项超过4个。
  • +
+

我实在是太菜了,有一半的题里面的名词都没听说过。。。所以就挂了,也没有然后了。

+

北京理工大学计算机学院(线上)

+
    +
  • 宣讲:第一天上午是学院的宣讲,在宣讲的过程中所有的实验室都会拉一个群,下午是实验室的宣讲。不同实验室不一样,大部分也是老师宣讲,有一个实验室的老师直接让想来的在会议中作自我介绍,问问题(公开处刑)。
    +有一个印象最深的,一个中北大学的学生作了自我介绍,老师直接问“我有个顾虑:中北大学不太好,所以学生的质量可能也不太好,你来讲讲实验室录取你的理由”就特别直白。。
  • +
  • 面试:第二天分组面试,面试分组随机,一个人10分钟左右。自我介绍要使用PPT,可以全程使用英文,也可以先中文再英文(当然是用英文啊。。。混着说多麻烦)。自我介绍完问的问题也很迷,比如“自我介绍为什么用英文”(是你们要求的好吧??)“北理有你的学长学姐吗”等等,有一个和项目相关的,没有专业知识,基本就是在随便聊。。。
  • +
+

面试后就结束了,几周后公布了优营名单,一共三四百入营的,优营给了不到二百个,承诺优营一定录取。

+

北京航空航天大学计算机学院(线上)

+
    +
  • 宣讲:北航没有宣讲,给了一个百度网盘的链接,里面是宣讲的视频,可以自己下载看看。
  • +
  • 机试:北航的机试可以用CCF认证成绩抵,但是CCF证书上必须标明使用的是C /C++,ALL是不能抵的(所以我考的CCF就没什么用处了)机试是完全闭卷,两个小时两道题,一个是关于结构体排序的40分,一个是关于最小生成树的60分。有一个平台提交试题,但是只验证是否能编译通过,不能返回结果和得分。
  • +
  • 面试:机试后刷掉了一小批人,然后面试。面试20分钟,首先是英文自我介绍,然后是数学问题,专业问题,性格测试等。数学问题“请说一下积分和微分含义”,专业问题“满二叉树和完全二叉树的区别”“什么是大数据”“分布式系统主要有哪些方面的内容”“分布式与集群有什么区别”等等。当时好紧张,数学专业问题基本都没答对什么。。老师还一直在大数据的概念上给我扔问题。
  • +
+

北航最后拿到了候补,一共500个入营的,过了机试有400左右,优营给了110多个,候补给了100个。不过整个夏令营的阶段都没有北航的同学参与,而看去年的录取名单基本都是北航的。。不知道是什么原因

+

天津大学智能与计算学部(线上)

+
    +
  • 宣讲:学部整体宣讲+实验室宣讲,宣讲之后填志愿,根据志愿安排面试小组
  • +
  • 面试:没有专业知识,自我介绍后随便提两个问题就结束了
  • +
+

入营有五百左右,给了三百的优营,要自己联系老师,8月底联系不到的认为放弃优营资格。

+

华东师范大学计算机科学与技术学院(线上)

+
    +
  • 宣讲:上午是中目会议宣讲,宣讲过程中有签到,下午是实验室自由宣讲,要填报志愿决定面试小组
  • +
  • 机试:华师大的在线oj平台,acm难度,4道题400分,按通过的测试点给分。总共三个小时,可以查阅纸质材料。我只做出来了第一题,剩下的都是大模拟骗分,最终得了273分。平台上可以看到大家的平均成绩,160分左右,不能看到实时排名。
  • +
  • 面试:PPT中文自我介绍,专业知识问答,英文文献翻译,自由问答。专业知识包括“TCP与UDP的区别”“如何构造哈夫曼树”,自由问答基本都是项目相关,总体来说难度不大。
    +面试后老师打了电话询问能否确定报考华师大,我说不能确定,一个月后又打了电话,当时已经拿到计算所的offer了,就放弃了华师大,最终官网的优营名单中也没有我。入营的只有一百左右,给了五六十优营(应该是打电话后不放弃的都给了我觉得)。
  • +
+

华中科技大学计算机科学与技术学院(线上)

+
    +
  • 宣讲:上午分实验室B站宣讲
  • +
  • 面试:自我介绍+项目问题,没有专业知识,时间比较短
  • +
+

入营只有200左右,大多数给了优营,但是是唯一一个没有后续的学校,没人说过优营有什么用接下来干什么。。。

+

中山大学计算机学院(软件学院)(线上)

+

吐槽吐槽!!!!中山真的是太烦人了,就算过了也真心不想去。
+入营资格要一个一个电话确认,还没开始夏令营就开了三场会,一个面试环境检查会,一个笔试环境检查会,一个面试分组抽签会。更为奇葩的双机位要求:两个机位互相能看见,次机位看清电脑屏幕,主机位能看到脑袋+肩膀且能看到双手???你来教教我咋能主机位看到双手???我双手举起来编程吗???更为奇葩的机试监考,要共享屏幕到腾讯会议中。好家伙总共五百多人参加机试你找了五百多个研究生坐五百多个电脑前面开五百多个腾讯会议盯着我们???面试环境检查会都已经查看完了承诺书,正式面试还要再看一遍???程序无比繁琐,而且充斥着学校对学生的满满的不信任感!

+
    +
  • 宣讲:无数宣讲,还要签到
  • +
  • 机试:用的中山自己开发的Matrix平台,没有自动补全,三个小时10道题,根本就不是那种oj题,更像C++考试题,评测速度也慢。还有机试考察面向对象的内容不允许用Java不允许用Python奇葩不???没学过C++就直接踢一边了呗?
  • +
  • 面试:中文自我介绍,英文问答一个,然后随便问,一些项目相关的知识。最后有个老师问“看你一个本科生搞了这么多竞赛,是不是基础知识掌握的不好啊”然后让我结束会议了???无力吐槽了
  • +
+

五百多人给了300优营和100候补,还承诺优营一定录取,还说不搞预推免了。祝愿中山被鸽穿!

+

中科院计算所(线下)

+

中科院计算所本来是没有入营的(意料之中),看QQ群里面的报名号有六千多人报名,入营名单发了400多。但是实验室秘书有一天早上打电话过来希望我能去北京参加机试面试,难得的机会就过去了。在报名后和入营名单公布之前会在QQ群里面让选意向导师,实际上就是意向实验室,才会有实验室秘书联系你,所以要多关注群消息。

+
    +
  • 宣讲:B站整体宣讲,一个实验室20分钟左右,可线上可线下
  • +
  • 机试:宣讲的下午网数实验室组织了机试。两个小时六道题,题目打印好了发给你,在自己笔记本上做,做完后学长学姐用U盘拷贝走,不会的也可以写思路。大家都在一个屋子里面做题,有几个学长学姐在巡视,完全闭卷不允许参考任何资料。
  • +
  • 面试:安排的是一个人20分钟,但是普遍延后,我等了超出预定时间一个小时才轮到我。一个长条桌,有十多个老师在对面,英文自我介绍然后就是项目问题、性格测试和政治问题,没有专业知识,面试全程比较愉悦。
  • +
+

面试结束当天晚上就打来了电话并发送了拟录取的邮件。一个月后在官网公布了拟录取的名单,入营的优营与没入营的优营(比如我)各占一半,总共给了200优营,承诺一定录取。其中查了一下,网数实验室面试38进12。

+

经验总结

+
    +
  1. 机试能力相当重要,推荐PTA平台,《算法笔记》和《算法笔记上机训练指南》。我在大学期间将《算法笔记》刷了三遍,最后一遍是在天梯赛比赛前一个月与《算法笔记上机训练指南》一起刷完的,收获非常多。由于我们学院选拔ACM队只招大一的,大一当时学的不太好,我没有接触过ACM,但是大三的天梯赛我的分数也能超过一半的ACM队员。如果你是一个大一萌新,一定要尽早接触编程,最好能参加ACM集训体验一下,多练多刷题,毕竟这是互联网行业的敲门砖,硬实力的体现。
  2. +
  3. 英文能力很重要,至少能清晰表达自己的含义,自我介绍要背熟,中英文的都要准备,也要准备中英文的PPT。
  4. +
  5. 要多投学校,面试多了自然就不紧张了,而且该背的在面试期间也都背熟了。(如果计算所是第一个面试的我感觉我可能都紧张得说不出话来)。
  6. +
  7. 夏令营入营的门槛就是学校的title和个人的rank,竞赛什么的一点用都没有,论文不知道能不能有点用。
  8. +
  9. 夏令营是否要联系老师?我觉得如果你真的想去,可以联系联系,不想去的就不要联系了,免得到时候有心理负担。录取我的计算所我也是全程没有联系老师,甚至在拟录取后玩了一个月我才和研究生导师加上微信说了两句话。
  10. +
  11. 避坑:明显超发offer的:天津大学智能与计算学部和中山大学计算机学院(软件学院),千万不要堵在这两个学校上,会死得很惨的。
  12. +
+ + +
+ +
+
+ + + + + + +
+
+
2022保研经历
+
https://zhangzhao219.github.io/2022/07/03/postgraduate-recommendation/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/07/11/trip-to-qingdao/index.html b/2022/07/11/trip-to-qingdao/index.html new file mode 100644 index 000000000..11014b3a4 --- /dev/null +++ b/2022/07/11/trip-to-qingdao/index.html @@ -0,0 +1,1108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Trip To Qingdao - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Trip To Qingdao

+ + +
+ +

青岛旅行计划

+ +

防疫政策

+

理论上的防疫政策:低风险地区提前三天向酒店等报备,第一天和第三天两次核酸。

+

实际:基本只看“青岛一码通”的绿码和7天内核酸阴性报告(有的地方可能要48小时核酸阴性报告)

+

具体措施:

+
    +
  1. 在家做好核酸,时间越晚越好(当然上车前必须要出结果),带好电子版或者纸质版报告,提前申请“青岛一码通”。
  2. +
  3. Day0 从青岛北站出来应该有核酸检测的点位,如果没有就去台东北侧的“青岛市海慈医疗集团”(公众号:青岛市海慈医疗集团)做24小时核酸。
  4. +
  5. 保证 Day2 和 Day3 至少分别做一次核酸,青岛出结果比较慢,要第二天才能出。
  6. +
  7. 公交地铁景区基本都要看“青岛一码通”的绿码和7天内核酸阴性报告,不要下载“青岛地铁APP”(青岛地铁APP要核验山东省电子健康卡,而申请山东省电子健康卡需要“入鲁申报”,为了减少不必要的麻烦这个不做),公交可以在支付宝或者云闪付申请电子公交码,地铁直接在地铁站买票进站,景区提前预约
  8. +
  9. 要是“青岛一码通”变黄码就BBQ了,应该不会的。
  10. +
+

总计划

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
日期计划备注
Day0晚上到达青岛,做核酸、住酒店可以去旁边的丽达生活超市买一些水和吃的
Day1上午信号山公园、栈桥
Day1中午王姐烧烤午餐
Day1下午小青岛公园、鲁迅公园、小鱼山公园、青岛第一海水浴场、八大关风景区
Day1晚上台东步行街小吃晚餐、回酒店可以去大商场买一点点吃的和水果等
Day2上午小麦岛公园
Day2中午船歌鱼水饺午餐
Day2下午燕儿岛公园、奥帆中心、情人坝、五四广场、海天云上艺术中心看天气,太热了就先去海天云上艺术中心
Day2晚上探海岛海鲜自助(探鲜岛海鲜自助餐厅)晚餐、栈桥附近的夜景、回酒店
Day3上午去崂山风景区
Day3中午吃一些提前买的面包等,景区内应该也有一些吃的
Day3下午崂山风景区、回市区
Day3晚上前海沿晚餐、回酒店
Day4返程
+

备注:

+
    +
  1. 景点备选:鲁迅公园附近的青岛海底世界和海军博物馆(太热了可以去避避暑)(可能都预约不上的)
  2. +
  3. 美食备选:火车站附近:无名小吃(似乎关门了,推荐油焖大虾,扇贝)和白玉餐厅(鱿鱼、茄子);石老人海水浴场附近的马扎子烧烤;五四广场附近的开海;台东的湘西石锅鱼和大叹号韩式烤肉;双和园水饺;还可以去买活海鲜(团岛农贸市场、营口海鲜市场、埠西市场)。要 no 尝尝崂山可乐和崂山白花蛇草水。早上要吃点东西,面包或者出去买一点点早餐。
  4. +
  5. 预计支出:车票1800;吃饭128+100+158+178+150+…≈800;酒店200*4≈800;门票、交通≈500。总共3900。小荷包还有4405.73,应该可以cover全部支出
  6. +
  7. 实际支出:车票391.5+462.5+412.5+19.5+63+339.82+15=1703.82;酒店838;门票10+298+40;
  8. +
+

青岛景点总览图

+

jcdeWF.md.png

+

酒店附近地图

+

jqylcV.md.png

+

Day 1

+

信号山公园

+
+
    +
  • +

    交通方式:酒店——信号山公园,公交前往,37分钟,步行680米

    +

    jq66MV.md.png

    +

    jqg8nf.md.png

    +
  • +
  • +

    预约:已经预约好 7月24日 6:00-20:30,包括收费5元的旋转观景楼

    +
  • +
  • +

    时间:1个小时左右

    +
  • +
  • +

    简介:信号山公园位于青岛市中心,因曾在山顶建有为船只引航的信号台而得名。信号山海拔98米,山顶三幢红顶蘑菇楼尤为显眼,其中较高的一幢是旋转观景楼,在这里你可以360度俯看青岛“红瓦绿树,碧海蓝天”的景色。进入景区大门,南坡上有踏浪观景台,就在连心桥下面一点,是拍摄南边德国古堡式建筑迎宾馆的好位置。连心桥上一把把红色爱心造型的锁挂在绿色栏杆上,情侣们可以在此买一把同心锁把彼此的山盟海誓锁在信号山上,据说手拉手走过连心桥可以得到祝福,单身的话自个儿的左手拉右手一样很好!再往前,可以看看五龙潭喷泉等景点,周围四条小龙围着中间一条大龙,与信号山又叫五龙山对应,因为山周边有龙江路、龙华路、龙口路、龙山路、伏龙路五条带“龙”字的路而得此别名。最后到达山顶的旋转观景楼,登上楼上观景台观景,一幢幢掩映在绿树中红瓦黄墙的德式建筑令人惊叹。往西南看,近处有绿色钟楼屋顶的基督教堂在一片红屋顶中非常出挑。

    +
  • +
+

栈桥

+
+
    +
  • 交通方式:信号山公园——栈桥,步行前往,2公里路程,10分钟。
    +jq2aVO.md.png
  • +
  • 预约:无需预约,包括栈桥与回澜阁,一说回澜阁8:30-16:30开放
  • +
  • 时间:预计半小时左右
  • +
  • 简介:栈桥位于青岛中心城区的南部海滨,是一条440米长的海上长廊,从陆地延伸入海中。回澜阁里面有一块无字碑,这块石碑的来历至今众说纷纭。现在,阁内通过主题展陈的方式,全面展示青岛近现代历史、人文、民俗等独特城市风貌。栈桥两边有铁链护栏和莲花路灯,游人漫步于栈桥海滨,风平浪静时,可观看海鸥在此自由翱翔。走到桥的尽头还可远眺小青岛。岛上树影婆娑、绿荫成群,一座白灯塔亭亭玉立。涨潮时,惊涛拍打着防波堤,激起簇簇浪花,可驻足观看。退潮时,赭色岩礁和金色沙滩露出水面,可走下栈桥,漫步在海滩上赶海拾贝。
  • +
+

王姐烧烤

+
+
    +
  • +

    交通方式:栈桥——王姐烧烤,步行前往,1.2公里路程,16分钟。

    +

    jqRFeK.md.png

    +
  • +
  • +

    美团可以直接订座,重点菜:辣炒蛤蜊

    +
  • +
+
+ +
+

小青岛公园——鲁迅公园——小鱼山公园——青岛第一海水浴场

+
+
    +
  • +

    交通方式:步行,共3公里左右

    +

    j6slDJ.md.png

    +
  • +
  • +

    预约:小鱼山公园开放时间08:00-17:00,网上找不到预约入口

    +
  • +
  • +

    时间:2-3小时

    +
  • +
  • +

    简介:小青岛故称为“琴岛”,是青岛湾内的一座小岛,青岛这个城市的名称就来源于它。小青岛与陆地之间有长长的海堤相接,岛上矗立着德国人建于1898年的灯塔,是青岛的标志之一。小青岛面积很小,岛上绿树成荫,岛的四周礁石环绕,海水清澈、蔚蓝,岛上常能见到来垂钓的游客。坐在礁石上吹吹海风,赤脚踩踩海水,看看四周青岛湾边林立的高楼和红顶的小洋房,仿佛置身于海上花园。每当夕阳西下时景色尤其美,阳光把整个海湾都镀成了金色。小青岛的南侧有一尊姿态优美的琴女雕像,雕像周围是花坛,种植着五颜六色的鲜花。岛的较高处有当年德国人建造的灯塔,整个岛的海拔也不高,才17米,走到灯塔脚下不需要爬多少路。灯塔通体洁白,由大理石构筑,是海上过往船只进出胶州湾的重要航标。每当夜幕降临,灯塔与岛上的灯光倒映在海面上,像一匹飘动的彩绸,形成青岛的一大胜景“琴屿飘灯”,在这里拍摄夜景很不错。鲁迅公园是青岛市内一处对外开放的临海公园,海边有大片的红褐色礁石,景色很特别,常有不少新人在此拍摄婚纱照。沿着海边步道慢慢走、听听海浪拍壁之声,或是走上岩石高处的亭子远眺大海,很是惬意。公园的东部紧邻青岛海底世界,再往东走是第一海水浴场,沿途风光很美。小鱼山公园是青岛佳风景眺望点之一,一是因为它位于市中心,是青岛离海近的一座山,地理位置;二是因为它的海拔仅60米,爬山不累,登到山顶能看到“红瓦绿树,碧海蓝天”具青岛代表性的景色。

    +
  • +
+

八大关风景区

+
+
    +
  • +

    交通方式:步行,毗邻青岛第一海水浴场

    +
  • +
  • +

    景区图:

    +

    j6g5lV.md.png

    +
  • +
  • +

    预约:无需预约,内部场馆单独售票,营业时间:9:00-17:00;换票时间:9:00-15:00

    +
  • +
  • +

    时间:2小时

    +
  • +
  • +

    简介:八大关是青岛市区南部相交错的十几条马路的总称,它们大多取名于我国知名关隘的名称。这里环境清幽,春季有碧桃盛开、夏季有紫薇盛放,秋季可见银杏红枫夹道,还坐落着许多各国风格的别墅,是摄影胜地。在这里,你可以进入欧洲古堡风格的“花石楼”参观、登上楼顶遥望大海,或者外观开国元帅住过的日式洋楼“元帅楼”、流传着唯美爱情故事的丹麦建筑“公主楼”等经典别墅,让你仿佛身处欧洲的某个角落。

    +
  • +
+

台东步行街

+
+
    +
  • +

    交通方式:地铁,24分钟,步行248m

    +

    j6WRFH.md.png

    +
  • +
  • +

    预约:无需预约

    +
  • +
  • +

    时间:晚上

    +
  • +
  • +

    简介:“朝观壁画夜赏灯,购物休闲在台东”,台东步行街是青岛有名的街区,街内有国内外知名的沃尔玛、万达广场、百信鞋业、利群集团、苏宁电器、三联家电、亚泰电器、新华书店、医保城等各类业态的企业245家。步行街两侧的21座楼6万余平方米的墙面为统一彩绘,精心绘制了色彩斑斓、造型生动的大型壁画,形成了独特的彩色画廊,这是大型的手工彩绘一条街。台东三路经过精心的景观设置,夜景迷人。这里还有男士、女士特色广场,营造出优美的购物和休闲环境,使市民在购物消费的同时,还享受着文化特色的盛宴。

    +
  • +
  • +

    美食推荐(有人排队多的肯定好吃):

    +
      +
    • 一家烤猪蹄,好像是叫黄金猪蹄。
    • +
    • 一家烤冷面,旁边一个蜜雪冰城,对面一家章鱼小丸子。烤冷面、烤粉丝、还有对面的章鱼烧都很好。
    • +
    • 一家炸鸡腿,在一个路口上,然后附近有一个杨国福麻辣烫。
    • +
    • 有一家面包,就是三角形的,好像是叫三脚猫。
    • +
    • 买酱猪蹄带回去(周钦公流亭酱猪蹄)
    • +
    +
  • +
+
+ +
+

Day 2

+

小麦岛公园

+
+
    +
  • +

    交通方式:地铁+公交(打车)(或公交),58分钟,步行1.2公里

    +

    jqI21e.md.png

    +
  • +
  • +

    预约:无需预约

    +
  • +
  • +

    时间:1-2小时

    +
  • +
  • +

    简介:小麦岛公园位于崂山区麦岛路西50米,小麦岛属环形岛屿,有大片平坦宽广的绿化草地,远处就是湛蓝的海水,可在这里眺望到遥远的海岸线,一派海滨美景,非常适宜拍照。

    +
  • +
+

船歌鱼水饺

+
+

就在小麦岛公园的公交站旁边,逛后吃午餐。

+

重点菜:鲅鱼、墨鱼、三鲜、虾仁水饺,海菜凉粉

+
+ +
+

燕儿岛公园

+
+
    +
  • +

    交通方式:公交34分钟,步行883米

    +

    jcn8gS.md.png

    +
  • +
  • +

    预约:无需预约

    +
  • +
  • +

    时间:1-3小时

    +
  • +
  • +

    简介:燕儿岛山公园位于山东省青岛市南部,处在浮山湾东端,是一个突出海中的岬角。园内环境优美,集生态、景观、文化、健身、休闲等为一体,是市民休闲锻炼、观光游玩的好地方。公园里的海滨栈道是一大亮点,木栈道与阶梯相连,一边是大海,一边是峭壁,峭壁底下鲜花盛开,在这里拍照仿佛置身于美丽的垦丁,有着独特的韵味。登上阶梯高处的平台放眼望去,可以将整个大海纳入眼帘,景色十分迷人。

    +
  • +
+

奥帆中心——情人坝——五四广场

+
+
    +
  • +

    交通方式:步行,直线距离2公里左右

    +

    jcQYR0.md.png

    +
  • +
  • +

    预约:无需预约,奥帆中心其他景点待确定

    +
  • +
  • +

    时间:2-3小时

    +
  • +
  • +

    简介:青岛奥帆中心景区位于青岛市浮山湾畔,与青岛市标志性景点——五四广场近海相望,总占地面积约45公顷,是2008年北京第29届奥运会奥帆赛和第13届残奥会帆船比赛举办场地,奥帆中心景区依山面海,景色宜人,是全国唯一“国家滨海旅游休闲示范区”。青岛被誉为“帆船之都”,作为最能体现青岛城市特色和展示城市形象的景区,奥帆中心景区内不仅有飞帆渺渺的优雅,有青岛十大旧景代表燕岛秋潮,有青岛新时代景观鸥翔彩虹,更有众多惊险刺激的娱乐体验,是游客来青必看的景点。

    +
  • +
+

海天云上艺术中心

+
+
    +
  • +

    交通方式:公交或地铁,20分钟

    +

    jcQ7Wt.md.png

    +
  • +
  • +

    预约:已经预约好 7月25日 9:00-20:00,80F+81F联票

    +
  • +
  • +

    时间:没查到。。。

    +
  • +
  • +

    简介:海天中心城市观光厅是山东省超高层垂直建筑之上的高空观光平台。在这里,向西可揽胜八大关老城区红瓦绿树,向东承接新城区璀璨繁华,360°俯瞰壮美海景、山景、城景,全方位感受身处"天空之城"的独特体验。其内部设置的透明观景区、沉浸式体验区、多媒体展示区与空中水吧等多个功能空间,将内部游览体验与外部自然景观融为一体。站在369米之上的城市观光厅,可以看尽因海而生、向海而兴的魅力青岛在时间长河中的风貌变迁与发展动线。随着观光者的漫步,不同姿态的青岛都将尽收眼底。

    +
  • +
+

探海岛海鲜自助(探鲜岛海鲜自助餐厅)

+
+

回青岛站附近吃晚餐,美团可以订座,顺便可以游览栈桥附近的夜景。

+
+ +
+

Day 3

+

崂山风景区

+
    +
  • +

    交通方式:地铁接公交

    +
  • +
  • +

    预约:已经预约好 7月26日 6:00-12:00太清,12:01-17:30 华严和仰口

    +
  • +
  • +

    时间:一天

    +
  • +
  • +

    路线:大河东检票——第三站下车游览太清宫、太清索道——索道往返——走到垭口乘坐公交618路前往华严(或仰口)——景区游览车到仰口(或华严)——原路返回大河东(或者直接从仰口出去)

    +
  • +
  • +

    崂山风景区地图

    +

    jcaA8e.md.png

    +

    jcUjgJ.md.png

    +
  • +
+

前海沿

+

这个位置暂定,美团可以排队

+

jIirk9.md.png

+

重点菜:蒜蓉粉丝虾、手锤茄子卷饼

+
+ +
+ + +
+ +
+
+ + + + + + +
+
+
Trip To Qingdao
+
https://zhangzhao219.github.io/2022/07/11/trip-to-qingdao/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/07/12/Coursera/Mathematics-for-Machine-Learning-Linear-Algebra/index.html b/2022/07/12/Coursera/Mathematics-for-Machine-Learning-Linear-Algebra/index.html new file mode 100644 index 000000000..089f0cb2c --- /dev/null +++ b/2022/07/12/Coursera/Mathematics-for-Machine-Learning-Linear-Algebra/index.html @@ -0,0 +1,1299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mathematics for Machine Learning: Linear Algebra - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Mathematics for Machine Learning: Linear Algebra

+ + +
+ +

数学在机器学习领域的应用一:线性代数

+ +

开始学习

+

总是觉得自己数学有一点差,可能是因为上大学学习的时候题目做的比较少,我的脑子又不太灵光,因此一直不能很好的理解数学相关的一些公式、定理等,平时编程的时候尽量找简单的方法绕开复杂的数学公式。假期有时间了,试一下帝国理工的线性代数课程,注重记录,注重理解。这也是第一次看没有中文字幕的全英文课。加油!

+

课程简介

+

In this course on Linear Algebra we look at what linear algebra is and how it relates to vectors and matrices. Then we look through what vectors and matrices are and how to work with them, including the knotty problem of eigenvalues and eigenvectors, and how to use these to solve problems. Finally we look at how to use these to do fun things with datasets - like how to rotate images of faces and how to extract eigenvectors to look at how the Pagerank algorithm works.

+

Since we’re aiming at data-driven applications, we’ll be implementing some of these ideas in code, not just on pencil and paper. Towards the end of the course, you’ll write code blocks and encounter Jupyter notebooks in Python, but don’t worry, these will be quite short, focussed on the concepts, and will guide you through if you’ve not coded before.

+

At the end of this course you will have an intuitive understanding of vectors and matrices that will help you bridge the gap into linear algebra problems, and how to apply these concepts to machine learning.

+

什么是线性代数

+

Linear algebra is a mathematical system for manipulating vectors in the spaces described by vectors.

+

Linear algebra is linear, because it just takes input values, and multiplies them by constants, everything is linear.

+

Linear algebra is algebra, that is it’s a notation describing mathematical objects and a system of manipulating those notations.

+

How vectors are transformed by matrices is the heart of linear algebra.

+

为什么我们需要线性代数?

+
    +
  1. 让计算机快速求解多元方程组
    +例如:多元方程组,可以转换为,然后进行求解。
  2. +
  3. 为数据拟合方程
    +随着神经网络和机器学习的发展,并不仅仅是拟合方程,最好还能在已有方程曲线的前提下,找到最佳的拟合参数,从而更适用于当前的数据。描述一个方程的各种参数可以使用一个向量来表示,我们希望通过某种方式,数据科学或者机器学习的方式来找到最佳的拟合参数。
  4. +
+

向量(Vector)

+

在计算机科学中,向量被认为是描述一个物体的属性的集合。

+

向量的基本操作

+

向量有两种操作:向量与向量之间的加法,以及向量与标量之间的乘法。

+

向量与向量之间的加法满足结合律(associativity)。

+

向量与标量之间的乘法,要将标量与向量中的每一个属性相乘

+

向量的其他运算

+

如果不以坐标系的角度去观察向量,那么一个向量由两个属性构成:向量的方向和向量的模长

+

向量的模长指的是向量各组成成分的平方和开根号

+

向量的点乘指的是向量对应位置的数值相乘之和,满足交换律(commutative)

+

同时满足向量的加法分配律(distributive over addition),即

+

向量与标量相乘满足结合律和交换律,即

+

向量模长与点乘之间的关系:向量自身的点乘与模长的平方相等,即

+

向量的余弦定理:

+

向量投影(projection):

+

上的投影标量(scalar projection)=

+

上的投影向量(vector projection)= scalar projection * 单位向量 =

+

向量投影是一个标量,但是,如果需要投影向量的方向,直接与被投影的单位向量相乘即可。

+

向量的坐标系

+

两个不共线的向量可以确定一个坐标系(coordinate system)。要描述一个向量,首先要定义一个坐标系,决定坐标系的是基向量

+

基向量是维的向量集合,需要满足3个条件:

+
    +
  1. 维的向量彼此之间不线性相关,也就是线性独立的维向量。
  2. +
  3. 可以扩展到整个空间。
  4. +
  5. 空间是维的。
  6. +
+

虽然并不要求基向量正交,但是如果它们正交,会为解决数学问题带来很大的方便。

+

如果二维的基向量互相垂直,转换坐标系只需将向量投影到转换后的基向量,计算数值即可。

+

设原始坐标系,转换后的基向量

+

首先验证是否垂直,

+

然后将待转换的向量,对的投影为,这个投影除以的模长,即方向的投影为2个长度。同理,即方向的投影为0.5个长度。

+

从而得出,最终计算得

+

找到一个合适的坐标系,帮助我们解决数学问题,是非常重要的。

+

矩阵(Matrices)

+

矩阵与向量相乘,相当于将向量转换到不同的坐标系。

+

矩阵的乘法满足结合律,但是不满足交换律.

+

,相当于将转换到了

+

,相当于将转换到了

+

通过矩阵的转换实际上可以看作不同转换向量之间的和。

+

如果我们对做这个矩阵的变换,则可以推导:

+

+

+

.

+

单位矩阵(identity matrix)不对向量做任何变换

+

+

设单位矩阵为待求根,

+

根据逆矩阵的定义,

+

因此,即

+

通过初等行变换求解逆矩阵:

+

对于二维矩阵来说,它的逆矩阵是

+

二维行列式(determinant):

+

行列式为0的矩阵,维度不满足当前矩阵的维度,因此在矩阵操作前要首先检查行列式

+

矩阵的转置:,正交矩阵,则,且正交矩阵的行列式为-1或1。

+

爱因斯坦求和约定(Einstein summation convention)

+

,则

+

是由中的某一行与中的某一列相乘求和后填充的矩阵。

+

+

因此即为爱因斯坦求和约定的表示法。

+

矩阵坐标系的转换

+

设原始坐标系,现在有另外一个坐标系,坐标系在原始坐标系下基向量表示为

+

如果将坐标系下的向量转换到原始坐标系中,则为

+

反之,将原始坐标系中的向量转换到坐标系下,则

+

如果基向量是正交的,可以使用投影来实现坐标系的转换:

+

设原始坐标系,现在有另外一个坐标系,坐标系在原始坐标系下基向量表示为

+

则将坐标系下的向量转换到原始坐标系中,通过投影实现:

+

,因此在原始坐标系下的向量为

+

施密特正交化(Gram–Schmidt process)

+

正交的基向量会给我们解决问题带来很多的方便,需要一种方法将基向量转换为正交的基向量。

+

设原始的维基向量为

+

+

+

+

特征问题(Eigenproblems)

+

对特征向量的直观感受:在进行变换的时候方向仍然保持不变的向量。

+

为特征向量,为特征值。

+

求特征值,即的行列式为0

+

对角矩阵(diagonal matrix)会使矩阵的乘法变得更加容易,

+

因此可以通过特征值与特征向量的转换,将矩阵转化为对角矩阵,然后求矩阵的幂。

+

设特征向量,特征值的对角矩阵

+

矩阵

+

编程练习

+

判断一个矩阵是奇异矩阵(singular)还是非奇异矩阵

+
# GRADED FUNCTION
+import numpy as np
+
+# Our function will go through the matrix replacing each row in order turning it into echelon form.
+# If at any point it fails because it can't put a 1 in the leading diagonal,
+# we will return the value True, otherwise, we will return False.
+# There is no need to edit this function.
+def isSingular(A) :
+    B = np.array(A, dtype=np.float_) # Make B as a copy of A, since we're going to alter it's values.
+    try:
+        fixRowZero(B)
+        fixRowOne(B)
+        fixRowTwo(B)
+        fixRowThree(B)
+    except MatrixIsSingular:
+        return True
+    return False
+
+# This next line defines our error flag. For when things go wrong if the matrix is singular.
+# There is no need to edit this line.
+class MatrixIsSingular(Exception): pass
+
+# For Row Zero, all we require is the first element is equal to 1.
+# We'll divide the row by the value of A[0, 0].
+# This will get us in trouble though if A[0, 0] equals 0, so first we'll test for that,
+# and if this is true, we'll add one of the lower rows to the first one before the division.
+# We'll repeat the test going down each lower row until we can do the division.
+# There is no need to edit this function.
+def fixRowZero(A) :
+    if A[0,0] == 0 :
+        A[0] = A[0] + A[1]
+    if A[0,0] == 0 :
+        A[0] = A[0] + A[2]
+    if A[0,0] == 0 :
+        A[0] = A[0] + A[3]
+    if A[0,0] == 0 :
+        raise MatrixIsSingular()
+    A[0] = A[0] / A[0,0]
+    return A
+
+# First we'll set the sub-diagonal elements to zero, i.e. A[1,0].
+# Next we want the diagonal element to be equal to one.
+# We'll divide the row by the value of A[1, 1].
+# Again, we need to test if this is zero.
+# If so, we'll add a lower row and repeat setting the sub-diagonal elements to zero.
+# There is no need to edit this function.
+def fixRowOne(A) :
+    A[1] = A[1] - A[1,0] * A[0]
+    if A[1,1] == 0 :
+        A[1] = A[1] + A[2]
+        A[1] = A[1] - A[1,0] * A[0]
+    if A[1,1] == 0 :
+        A[1] = A[1] + A[3]
+        A[1] = A[1] - A[1,0] * A[0]
+    if A[1,1] == 0 :
+        raise MatrixIsSingular()
+    A[1] = A[1] / A[1,1]
+    return A
+
+# This is the first function that you should complete.
+# Follow the instructions inside the function at each comment.
+def fixRowTwo(A) :
+    # Insert code below to set the sub-diagonal elements of row two to zero (there are two of them).
+    A[2] = A[2] - A[2,0] * A[0]
+    A[2] = A[2] - A[2,1] * A[1]
+    # Next we'll test that the diagonal element is not zero.
+    if A[2,2] == 0 :
+        # Insert code below that adds a lower row to row 2.
+        A[2] = A[2] + A[3]
+        # Now repeat your code which sets the sub-diagonal elements to zero.
+        A[2] = A[2] - A[2,0] * A[0]
+        A[2] = A[2] - A[2,1] * A[1]
+    if A[2,2] == 0 :
+        raise MatrixIsSingular()
+    # Finally set the diagonal element to one by dividing the whole row by that element.
+    A[2] = A[2] / A[2,2]
+    return A
+
+# You should also complete this function
+# Follow the instructions inside the function at each comment.
+def fixRowThree(A) :
+    # Insert code below to set the sub-diagonal elements of row three to zero.
+    A[3] = A[3] - A[3,0] * A[0]
+    A[3] = A[3] - A[3,1] * A[1]
+    A[3] = A[3] - A[3,2] * A[2]
+    # Complete the if statement to test if the diagonal element is zero.
+    if A[3,3] == 0:
+        raise MatrixIsSingular()
+    # Transform the row to set the diagonal element to one.
+    A[3] = A[3] / A[3,3]
+    return A
+
A = np.array([
+        [2, 0, 0, 0],
+        [0, 3, 0, 0],
+        [0, 0, 4, 4],
+        [0, 0, 5, 5]
+    ], dtype=np.float_)
+isSingular(A)
+A = np.array([
+        [0, 7, -5, 3],
+        [2, 8, 0, 4],
+        [3, 12, 0, 5],
+        [1, 3, 1, 3]
+    ], dtype=np.float_)
+isSingular(A)
+fixRowZero(A)
+fixRowOne(A)
+fixRowTwo(A)
+fixRowThree(A)
+

施密特正交化

+
# GRADED FUNCTION
+import numpy as np
+import numpy.linalg as la
+
+verySmallNumber = 1e-14 # That's 1×10⁻¹⁴ = 0.00000000000001
+
+# Our first function will perform the Gram-Schmidt procedure for 4 basis vectors.
+# We'll take this list of vectors as the columns of a matrix, A.
+# We'll then go through the vectors one at a time and set them to be orthogonal
+# to all the vectors that came before it. Before normalising.
+# Follow the instructions inside the function at each comment.
+# You will be told where to add code to complete the function.
+def gsBasis4(A) :
+    B = np.array(A, dtype=np.float_) # Make B as a copy of A, since we're going to alter it's values.
+    # The zeroth column is easy, since it has no other vectors to make it normal to.
+    # All that needs to be done is to normalise it. I.e. divide by its modulus, or norm.
+    B[:, 0] = B[:, 0] / la.norm(B[:, 0])
+    # For the first column, we need to subtract any overlap with our new zeroth vector.
+    B[:, 1] = B[:, 1] - B[:, 1] @ B[:, 0] * B[:, 0]
+    # If there's anything left after that subtraction, then B[:, 1] is linearly independant of B[:, 0]
+    # If this is the case, we can normalise it. Otherwise we'll set that vector to zero.
+    if la.norm(B[:, 1]) > verySmallNumber :
+        B[:, 1] = B[:, 1] / la.norm(B[:, 1])
+    else :
+        B[:, 1] = np.zeros_like(B[:, 1])
+    # Now we need to repeat the process for column 2.
+    # Insert two lines of code, the first to subtract the overlap with the zeroth vector,
+    # and the second to subtract the overlap with the first.
+    B[:, 2] = B[:, 2] - B[:, 2] @ B[:, 0] * B[:, 0]
+    B[:, 2] = B[:, 2] - B[:, 2] @ B[:, 1] * B[:, 1]  
+    # Again we'll need to normalise our new vector.
+    # Copy and adapt the normalisation fragment from above to column 2.
+    if la.norm(B[:, 2]) > verySmallNumber :
+        B[:, 2] = B[:, 2] / la.norm(B[:, 2])
+    else :
+        B[:, 2] = np.zeros_like(B[:, 2])
+    # Finally, column three:
+    # Insert code to subtract the overlap with the first three vectors.
+    B[:, 3] = B[:, 3] - B[:, 3] @ B[:, 0] * B[:, 0]
+    B[:, 3] = B[:, 3] - B[:, 3] @ B[:, 1] * B[:, 1]   
+    B[:, 3] = B[:, 3] - B[:, 3] @ B[:, 2] * B[:, 2]  
+    # Now normalise if possible
+    if la.norm(B[:, 3]) > verySmallNumber :
+        B[:, 3] = B[:, 3] / la.norm(B[:, 3])
+    else :
+        B[:, 3] = np.zeros_like(B[:, 3])
+    # Finally, we return the result:
+    return B
+
+# The second part of this exercise will generalise the procedure.
+# Previously, we could only have four vectors, and there was a lot of repeating in the code.
+# We'll use a for-loop here to iterate the process for each vector.
+def gsBasis(A) :
+    B = np.array(A, dtype=np.float_) # Make B as a copy of A, since we're going to alter it's values.
+    # Loop over all vectors, starting with zero, label them with i
+    for i in range(B.shape[1]) :
+        # Inside that loop, loop over all previous vectors, j, to subtract.
+        for j in range(i) :
+            # Complete the code to subtract the overlap with previous vectors.
+            # you'll need the current vector B[:, i] and a previous vector B[:, j]
+            B[:, i] = B[:, i] - B[:, i] @ B[:, j] * B[:, j]
+        # Next insert code to do the normalisation test for B[:, i]
+        if la.norm(B[:, i]) > verySmallNumber :
+            B[:, i] = B[:, i] / la.norm(B[:, i])
+        else :
+                B[:, i] = np.zeros_like(B[:, i])
+    # Finally, we return the result:
+    return B
+
+# This function uses the Gram-schmidt process to calculate the dimension
+# spanned by a list of vectors.
+# Since each vector is normalised to one, or is zero,
+# the sum of all the norms will be the dimension.
+def dimensions(A) :
+    return np.sum(la.norm(gsBasis(A), axis=0))
+
V = np.array([[1,0,2,6],
+              [0,1,8,2],
+              [2,8,3,1],
+              [1,-6,2,3]], dtype=np.float_)
+gsBasis4(V)
+# Once you've done Gram-Schmidt once,
+# doing it again should give you the same result. Test this:
+U = gsBasis4(V)
+gsBasis4(U)
+# Try the general function too.
+gsBasis(V)
+# See what happens for non-square matrices
+A = np.array([[3,2,3],
+              [2,5,-1],
+              [2,4,8],
+              [12,2,1]], dtype=np.float_)
+gsBasis(A)
+dimensions(A)
+B = np.array([[6,2,1,7,5],
+              [2,8,5,-4,1],
+              [1,-6,3,2,8]], dtype=np.float_)
+gsBasis(B)
+dimensions(B)
+# Now let's see what happens when we have one vector that is a linear combination of the others.
+C = np.array([[1,0,2],
+              [0,1,-3],
+              [1,0,2]], dtype=np.float_)
+gsBasis(C)
+dimensions(C)
+

镜面投影

+
# PACKAGE
+# Run this cell first once to load the dependancies.
+import numpy as np
+from numpy.linalg import norm, inv
+from numpy import transpose
+from readonly.bearNecessities import *
+# GRADED FUNCTION
+# You should edit this cell.
+
+# In this function, you will return the transformation matrix T,
+# having built it out of an orthonormal basis set E that you create from Bear's Basis
+# and a transformation matrix in the mirror's coordinates TE.
+def build_reflection_matrix(bearBasis) : # The parameter bearBasis is a 2×2 matrix that is passed to the function.
+    # Use the gsBasis function on bearBasis to get the mirror's orthonormal basis.
+    E = gsBasis(bearBasis)
+    # Write a matrix in component form that performs the mirror's reflection in the mirror's basis.
+    # Recall, the mirror operates by negating the last component of a vector.
+    # Replace a,b,c,d with appropriate values
+    TE = np.array([[1, 0],
+                   [0, -1]])
+    # Combine the matrices E and TE to produce your transformation matrix.
+    T = E @ TE @ inv(E)
+    # Finally, we return the result. There is no need to change this line.
+    return T
+# First load Pyplot, a graph plotting library.
+%matplotlib inline
+import matplotlib.pyplot as plt
+
+# This is the matrix of Bear's basis vectors.
+# (When you've done the exercise once, see what happns when you change Bear's basis.)
+bearBasis = np.array(
+    [[1,   -1],
+     [1.5, 2]])
+# This line uses your code to build a transformation matrix for us to use.
+T = build_reflection_matrix(bearBasis)
+
+# Bear is drawn as a set of polygons, the vertices of which are placed as a matrix list of column vectors.
+# We have three of these non-square matrix lists: bear_white_fur, bear_black_fur, and bear_face.
+# We'll make new lists of vertices by applying the T matrix you've calculated.
+reflected_bear_white_fur = T @ bear_white_fur
+reflected_bear_black_fur = T @ bear_black_fur
+reflected_bear_face = T @ bear_face
+
+# This next line runs a code to set up the graphics environment.
+ax = draw_mirror(bearBasis)
+
+# We'll first plot Bear, his white fur, his black fur, and his face.
+ax.fill(bear_white_fur[0], bear_white_fur[1], color=bear_white, zorder=1)
+ax.fill(bear_black_fur[0], bear_black_fur[1], color=bear_black, zorder=2)
+ax.plot(bear_face[0], bear_face[1], color=bear_white, zorder=3)
+
+# Next we'll plot Bear's reflection.
+ax.fill(reflected_bear_white_fur[0], reflected_bear_white_fur[1], color=bear_white, zorder=1)
+ax.fill(reflected_bear_black_fur[0], reflected_bear_black_fur[1], color=bear_black, zorder=2)
+ax.plot(reflected_bear_face[0], reflected_bear_face[1], color=bear_white, zorder=3);
+
+

jhnVED.md.png

+

PageRank

+
# PACKAGE
+# Here are the imports again, just in case you need them.
+# There is no need to edit or submit this cell.
+import numpy as np
+import numpy.linalg as la
+from readonly.PageRankFunctions import *
+np.set_printoptions(suppress=True)
+# GRADED FUNCTION
+# Complete this function to provide the PageRank for an arbitrarily sized internet.
+# I.e. the principal eigenvector of the damped system, using the power iteration method.
+# (Normalisation doesn't matter here)
+# The functions inputs are the linkMatrix, and d the damping parameter - as defined in this worksheet.
+# (The damping parameter, d, will be set by the function - no need to set this yourself.)
+def pageRank(linkMatrix, d) :
+    n = linkMatrix.shape[0]
+    M = d * linkMatrix + (1-d)/n * np.ones([n, n])
+    r = 100 * np.ones(n) / n
+    lastR = r
+    r = M @ r
+    i = 0
+    while la.norm(lastR - r) > 0.01 :
+        lastR = r
+        r = M @ r
+        i += 1  
+    return r
+
# Use the following function to generate internets of different sizes.
+generate_internet(5)
+# Test your PageRank method against the built in "eig" method.
+# You should see yours is a lot faster for large internets
+L = generate_internet(10)
+pageRank(L, 1)
+# Do note, this is calculating the eigenvalues of the link matrix, L,
+# without any damping. It may give different results that your pageRank function.
+# If you wish, you could modify this cell to include damping.
+# (There is no credit for this though)
+eVals, eVecs = la.eig(L) # Gets the eigenvalues and vectors
+order = np.absolute(eVals).argsort()[::-1] # Orders them by their eigenvalues
+eVals = eVals[order]
+eVecs = eVecs[:,order]
+
+r = eVecs[:, 0]
+100 * np.real(r / np.sum(r))
+# You may wish to view the PageRank graphically.
+# This code will draw a bar chart, for each (numbered) website on the generated internet,
+# The height of each bar will be the score in the PageRank.
+# Run this code to see the PageRank for each internet you generate.
+# Hopefully you should see what you might expect
+# - there are a few clusters of important websites, but most on the internet are rubbish!
+%pylab notebook
+r = pageRank(generate_internet(100), 0.9)
+plt.bar(arange(r.shape[0]), r);
+

资料

+

Formula Sheet: Sheet summarising all the formulae covered in this course.

+ + +
+ +
+ + + +

Code and Notebooks

+ + +
+ +
+
+ + + + + + +
+
+
Mathematics for Machine Learning: Linear Algebra
+
https://zhangzhao219.github.io/2022/07/12/Coursera/Mathematics-for-Machine-Learning-Linear-Algebra/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月12日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/07/13/travel-list/index.html b/2022/07/13/travel-list/index.html new file mode 100644 index 000000000..6a36e6e5c --- /dev/null +++ b/2022/07/13/travel-list/index.html @@ -0,0 +1,783 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Travel List - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Travel List

+ + +
+ +

旅行物品清单

+ +

高铁新规

+

jqsL6K.md.png

+

必备物品

+
    +
  1. 身份证、学生证(本科的估计也行)、手机(足够流量)
  2. +
  3. 足量的口罩
  4. +
  5. 一点点现金
  6. +
+

生活用品

+
    +
  1. 手纸、面巾纸等、塑料袋
  2. +
  3. 洗漱包(牙具)、毛巾
  4. +
  5. 水杯(可选)
  6. +
  7. 雨伞(或雨衣)
  8. +
+

药品

+
    +
  1. 消炎药
  2. +
  3. 腹泻药
  4. +
  5. 感冒发烧药
  6. +
+

衣物

+
    +
  1. 2-3套换洗的内衣、袜子等
  2. +
  3. 应季适量外衣外裤
  4. +
  5. 被褥、蚊帐等(若目的地不提供)
  6. +
+

电子产品

+
    +
  1. 手机充电线(器)、充电宝、充电宝充电器
  2. +
  3. 笔记本电脑(充电器)、iPad(充电器)
  4. +
  5. 耳机
  6. +
  7. 电蚊香(液)
  8. +
  9. 剃须刀
  10. +
  11. 插排(若目的地不提供)
  12. +
+ + +
+ +
+
+ + + + + + +
+
+
Travel List
+
https://zhangzhao219.github.io/2022/07/13/travel-list/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/07/28/Coursera/Supervised-Machine-Learning-Regression-and-Classification/index.html b/2022/07/28/Coursera/Supervised-Machine-Learning-Regression-and-Classification/index.html new file mode 100644 index 000000000..0592eb661 --- /dev/null +++ b/2022/07/28/Coursera/Supervised-Machine-Learning-Regression-and-Classification/index.html @@ -0,0 +1,946 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Supervised Machine Learning: Regression and Classification - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Supervised Machine Learning: Regression and Classification

+ + +
+ +

机器学习-监督学习:回归和分类

+ +

开始学习

+

吴恩达的机器学习课程终于更新了!!!想当初应该是大二的时候,看了吴恩达的课程,对机器学习有了初步的了解。当时听的不是很明白,英语看不太懂,一些给了充分提示的代码也写不太好,也就是入了一个门而已。这次有一些时间,正好捡一捡机器学习的基础知识,推一推那些一直在调包的数学公式。注重记录!

+

课程简介

+

In the first course of the Machine Learning Specialization, you will:

+

• Build machine learning models in Python using popular machine learning libraries NumPy and scikit-learn.
+• Build and train supervised machine learning models for prediction and binary classification tasks, including linear regression and logistic regression

+

The Machine Learning Specialization is a foundational online program created in collaboration between DeepLearning.AI and Stanford Online. In this beginner-friendly program, you will learn the fundamentals of machine learning and how to use these techniques to build real-world AI applications.

+

This Specialization is taught by Andrew Ng, an AI visionary who has led critical research at Stanford University and groundbreaking work at Google Brain, Baidu, and Landing.AI to advance the AI field.

+

This 3-course Specialization is an updated and expanded version of Andrew’s pioneering Machine Learning course, rated 4.9 out of 5 and taken by over 4.8 million learners since it launched in 2012.

+

It provides a broad introduction to modern machine learning, including supervised learning (multiple linear regression, logistic regression, neural networks, and decision trees), unsupervised learning (clustering, dimensionality reduction, recommender systems), and some of the best practices used in Silicon Valley for artificial intelligence and machine learning innovation (evaluating and tuning models, taking a data-centric approach to improving performance, and more.)

+

By the end of this Specialization, you will have mastered key concepts and gained the practical know-how to quickly and powerfully apply machine learning to challenging real-world problems. If you’re looking to break into AI or build a career in machine learning, the new Machine Learning Specialization is the best place to start.

+

什么是机器学习

+
    +
  • 在谷歌、必应或百度上进行网络搜索会出现想要的答案。这是因为他们的机器学习软件已经解决了如何对网页进行排名。
  • +
  • 上传照片到Instagram或Snapchat,并且想标记我的朋友,让他们可以看到他们的照片。这些应用程序可以识别你照片中的朋友,并给他们贴上标签。
  • +
  • 刚刚在视频流服务上看完一部星球大战电影,流媒体服务可能会使用机器学习来推荐您可能喜欢的内容。
  • +
  • 用语音短信在手机上写短信时,手机会做出你希望的行为
  • +
  • 收到一封赢了一百万美元的电子邮件,你的电子邮件服务很可能会将其标记为垃圾邮件。
  • +
  • 除了消费者应用之外,人工智能也在迅速进入大公司和工业应用。机器学习已经有望优化风力涡轮机发电,开始进入医院,帮助医生做出准确的诊断,将计算机视觉应用到工厂中,以帮助检查生产线中的产品是否有任何缺陷。
  • +
  • 机器学习是一门让计算机在没有明确编程的情况下学习的科学-1950
  • +
+

监督学习

+

监督学习是学习从输入到输出标签的一个函数映射,主要特征是给予算法示例去学习,也就是从被给予的正确答案中学习。

+

监督学习的基本类型有两种:回归和分类

+

回归任务是在大量的数值空间中,对某一个具体数值进行预测

+

分类任务是在给定的数值空间中(如0和1),对某一个具体数据进行预测

+

符号表示方法

+

表示输入的变量或者特征

+

表示输出的实际目标变量,表示预测的变量

+

表示训练样本总数

+

表示一个训练样本,表示第个训练样本

+

线性回归的机器学习模型可以表示为:

+

损失函数

+

度量预测值与实际目标值之间的差异

+

线性回归中使用的平方损失函数:,将机器学习模型代入,则表示为

+

目标就是要找出最合适的,使得最小

+

使用梯度下降算法:

+

为学习率

+

梯度下降在更新的时候需要同时更新,因此在计算的过程中,首先要计算,然后再相减,保证同步更新。

+

具体计算:

+

+

+

+

+

学习率的选择:

+

如果学习率过小,梯度下降算法运行会比较慢

+

如果学习率过大,梯度下降算法可能运行过头,最终导致算法不能收敛

+

如果使用固定的学习率,梯度下降算法运行到局部最小值后不会再变化。因为到达局部最小值的附近后,梯度下降的每一步会变得更小,更新的值也会逐渐变小。

+

通过损失值随着迭代次数的变化可以看出一些错误:

+
    +
  1. 随着迭代次数增加,损失值波动上升下降——代码有问题或者学习率过大
  2. +
  3. 随着迭代次数增加,损失值一直上升——学习率过大或代码有问题(可能每一次的计算符号反了)
  4. +
  5. 如果很长时间不收敛,可能是学习率太小了
  6. +
+

将学习率调整足够小,损失值在每一次迭代的过程中都会减小

+

多元线性回归

+

表示第个特征,表示特征的数量

+

表示第个训练样本的全部特征,表示第个训练样本中的第个特征

+

+

,则

+

+

可以通过Numpy的向量化进行计算

+

特征缩放

+

当具有不同的值范围的不同特征时,可能会导致梯度下降算法运行较慢

+

需要对不同的特征重新缩放到相同或相似的范围

+

均值归一化:,可以缩放到的范围内

+

Z-score归一化:

+

逻辑回归(分类问题)

+

sigmoid函数:

+

逻辑回归:,用概率的形式表达:

+

不同的决策边界:

+

+

+

+

逻辑回归损失函数:

+

+

简化写法:

+

+

欠拟合和过拟合

+

欠拟合:函数对于训练集的拟合效果不好——高偏差(high bias)

+

过拟合:函数对于训练集的拟合效果好,对于测试集的效果不好——高方差(high variance)

+

避免过拟合的方法:

+
    +
  1. 收集更多的训练数据
  2. +
  3. 从全部的特征中挑选最相关的特征进行训练
  4. +
  5. 正则化——减小某一参数对拟合函数的影响
  6. +
+

正则化

+

通过将损失函数加上特别大的常数与某一参数的乘积,使得这个参数在优化的过程中变得非常小

+

例如回归问题:

+

+

由于不知道哪些特征是比较重要的,哪些特征不重要,因此加上参数平方求和的正则项,让优化算法自行选择。

+

对于线性回归来说:

+

+

+

进一步推导:

+

+

因此正则项的加入实际上相当于将参数略微减小

+

资料

+

第一周课件和代码

+

Notebooks Week 1

+ + +
+ +
+ + + +

第二周课件和代码

+

Notebooks Week 2

+ + +
+ +
+ + + +

第三周课件和代码

+

Notebooks Week 3

+ + +
+ +
+ + + +

作业代码

+

Exercise 1

+

Exercise 2

+ + +
+ +
+
+ + + + + + +
+
+
Supervised Machine Learning: Regression and Classification
+
https://zhangzhao219.github.io/2022/07/28/Coursera/Supervised-Machine-Learning-Regression-and-Classification/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/07/28/about-my-previous-love/index.html b/2022/07/28/about-my-previous-love/index.html new file mode 100644 index 000000000..4ef5f8e72 --- /dev/null +++ b/2022/07/28/about-my-previous-love/index.html @@ -0,0 +1,770 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + My Previous Love - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

My Previous Love

+ + +
+ +

记于2022年7月28日,已于2022年9月24日正式分手,公开于2023年11月19日

+ +

青岛的五天旅行结束了,251天后的初次见面,美好的时光总是短暂。

+

回家后心里一直不太舒服,一直在胡思乱想,想着想着有时还偷偷抹抹眼泪。父母也是真的了解我,虽然并没有表现出什么,一直在不断追问我怎么了。当然就算有明确的原因也不会说,对爸妈只能是报喜不报忧,何况我现在也不知道我为什么这样。

+

可能是不舍得吧,分开了251天,再次见面的时间只有短短的五天,下一次见面还不知道什么时候。

+

可能是由于毕业季的几乎分手吧,可能现在自己的信心没有那么足了。

+

可能是对自己未来的迷茫吧,本科取得了不错的成绩,研究生一切从头开始,不知该从何做起。

+

这一段时间,对我影响最大的就是那一次的几乎分手。女孩子真的需要陪伴,异地太久了,感情是真的会变淡的。而且之前并没有很明确的聊过未来的规划。可能随口的一句“杭州南京”,就成为了一道跨不过去的坎。

+

我出生在东北的一个小城,从小的梦想就是要走出去,给我自己,甚至给我的下一代创造一个更好的生活环境。高二那年清华暑校遇到全国的优秀学生,发现不同地区顶尖学生之间的差异居然也有如此之大,更加坚定了我走出去的决心。我也很庆幸在高考失利的情况下能选到一个好专业,在房价居高不下的大环境下,至少目前来看毕业后的薪资还是非常有竞争力的。

+

我很开心可以遇到我的女朋友,我们在一无所有的情况下愿意去尝试。我也从此有了另外的一个前进动力,从高考失利和大一的挫折中走了出来,拿下了年级排名和无数的竞赛奖项、荣誉称号,成功保研。保研的时候也没有选择华师大,想着自己应该获得更好的学历,以后赚更多的钱,才能和她一起有更好的生活。我按照我的道路一步一步在走。

+

然而由于我早去北京的提前异地,我们之间的沟通就少了许多。地理上的距离造成了心的距离,找到了一个很好的教师编职位后,她便产生了分手的想法。整个过程我甚至都是毫不知情的状态。虽然靠着一条时间轴挽回,但是我需要对自己做一个深刻的反思。我自认为我的爱没有变,但是异地半年多,确实很难将爱表达出来,同时也忽略了她的感受,我们之间的交流变得更少,最终导致了单方面无吵架的分手。

+

能有一个爱人时刻陪伴在身边,确实是一件非常美好的事情。才分开两天,五天的回忆一波一波涌上心头,真的很难受。想起她忘记带伞的时候,只能躲在小店内等待雨停,却无法等到一个送伞的我。异地恋真的难熬。然而这还不到一年的时间。最少需要三年才能奔现,要是找一份更高薪的工作,甚至需要五年的时间,我才能在合肥站稳脚跟,真正地和她在一起。“所以你就选定我了是嘛”“是的”“为什么呢”“。。。”是啊,为什么呢,我回答不上来。后来我也认真考虑了很久,我是一个纯理性思维的人,这一次我选择听从我的心。我相信我三年前的选择,不管是现在,三年后,三十年后,我还会作出同样的选择。

+

我是一个很坚定的人,我作出了选择,就会坚定的走下去。这几年我会尽全力维护这一段感情,改正掉我之前的错误,尽量多见面,尽量提升自己以后拿到更好的薪资,尽量多关心,多询问她的感受。三年前我还是一个懵懂无知的学生,我不知道三年后,甚至五年后我会成为什么样的人,但是我的爱是永远不变的。

+

如果她熬不住了,我会坦然接受。因为我知道,我才是那个最对不起她的人。长三角省会城市工作稳定,我又何德何能拴住她数年的时间,忍受着屏幕那边可有可无的关心,忍受着几个月甚至半年才有的一次短短几天的见面。

+

我真的希望最终我们可以幸福地走到一起。

+

为你,千千万万遍。

+ + +
+ +
+
+ + + + + + +
+
+
My Previous Love
+
https://zhangzhao219.github.io/2022/07/28/about-my-previous-love/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/07/31/Coursera/Mathematics-for-Machine-Learning-Multivariate-Calculus/index.html b/2022/07/31/Coursera/Mathematics-for-Machine-Learning-Multivariate-Calculus/index.html new file mode 100644 index 000000000..07d80e3d8 --- /dev/null +++ b/2022/07/31/Coursera/Mathematics-for-Machine-Learning-Multivariate-Calculus/index.html @@ -0,0 +1,929 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mathematics for Machine Learning: Multivariate Calculus - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Mathematics for Machine Learning: Multivariate Calculus

+ + +
+ +

数学在机器学习领域的应用二:多元微积分

+ +

课程简介

+

This course offers a brief introduction to the multivariate calculus required to build many common machine learning techniques. We start at the very beginning with a refresher on the “rise over run” formulation of a slope, before converting this to the formal definition of the gradient of a function. We then start to build up a set of tools for making calculus easier and faster. Next, we learn how to calculate vectors that point up hill on multidimensional surfaces and even put this into action using an interactive game. We take a look at how we can use calculus to build approximations to functions, as well as helping us to quantify how accurate we should expect those approximations to be. We also spend some time talking about where calculus comes up in the training of neural networks, before finally showing you how it is applied in linear regression models. This course is intended to offer an intuitive understanding of calculus, as well as the language necessary to look concepts up yourselves when you get stuck. Hopefully, without going into too much detail, you’ll still come away with the confidence to dive into some more focused machine learning courses in future.

+

函数

+

函数是从输入到输出的映射,选择函数来建模世界的过程是伟大天才的科学目的,微积分只是对这些函数如何相对于它们的输入变量如何变化的研究。

+

导数(derivative)

+

对于线性函数而言,斜率(梯度、gradient)=‘rise over run’,也就是任意取两点,方向的距离与方向的距离之比即为梯度。

+

对于梯度一直在变化的函数来说,设函数为,任意取两点

+

+

即,

+

导数的求和法则:

+

幂函数求导法则:令,则

+

不连续(discontinuity)的函数,例如,在处没有定义,导数处也没有定义.

+

例如这种函数,,这种类型的函数与导数始终相等,因此有两个特点:

+
    +
  1. 函数必须恒大于0或者恒小于0,如果函数改变符号,导数也会改变符号,会使得函数不改变符号,与定义不符
  2. +
  3. 函数是单调的,因为函数永远不可能达到其原来的值
  4. +
+

三角函数:

+

导数乘积法则:令,则

+

求导的链式法则:若,且,则

+

偏导数求导法则:

+

偏导数仍然遵循导数的求导法则

+

雅可比行列式(Jacobian)

+

设函数,它的雅可比行列式为

+

这样给予一组的值,可以快速得出函数在该点指向此函数最陡斜率方向的向量。

+

设函数,则它的雅可比行列式为

+

海森矩阵(The Hessian)

+

对雅可比行列式再求一次偏导数,构成的二阶偏导数矩阵为海森矩阵

+

设函数,它的雅可比行列式为,则海森矩阵为

+

雅可比行列式求得的值为0的情况下,首先求海森矩阵的行列式,如果行列式为正数,说明目前的点是一个极值点;然后看海森矩阵的第一个数字,如果第一个数字是正数,说明目前在极小值点,否则在极大值点;如果海森矩阵的行列式为负,说明目前的点是一个鞍点。

+

神经网络

+

最简单的神经网络:,其中,表示活动,表示权重,表示偏差,表示激活函数

+

输入可能不仅仅是一个,设输入的神经元有个,则

+

输出可能也不仅仅是一个,设输出的神经元有个,总体的神经网络表示为:

+

+

可以简化表示为:

+

如果神经网络不止一层,则可以表示为:

+

神经网络(分类任务)的损失函数为

+

泰勒展开式

+

泰勒展开式是对一个复杂函数的简化估计函数

+

+

+

+

+

+

(麦克劳林形式,需要知道零点)

+

泰勒形式:

+

+

+

+

(泰勒形式,知道任意一点即可)

+

二维泰勒展开

+

+

(零阶泰勒展开)
+(一阶泰勒展开-雅可比行列式)

+

(二阶泰勒展开-海森矩阵)

+

牛顿迭代法(Newton-Raphson)

+

迭代求解方程的近似根:

+

+

这种方法会存在一些问题,如果选取的点比较靠近函数的拐点,会得不到正确的结果,或者得到的结果并不是与选取的点最接近的。

+

梯度下降

+

如何使用梯度找到多元函数的最大值或者最小值

+

函数的梯度:,即为函数值增加最快的方向

+

如果希望找到最大值,将梯度与它的单位向量相乘,则

+

梯度下降:

+

拉格朗日乘子法(Lagrange multipliers)

+

计算函数在某些约束下的最大值或者最小值

+

+

为拉格朗日乘子

+

即:

+

多元微积分在回归问题中的应用

+

线性回归

+

设函数

+

计算平方误差:

+

求解使得误差最小:

+

则可以解得:

+

+

+

非线性回归

+
    +
  1. 梯度下降法
  2. +
  3. 泰勒展开式+海森矩阵
  4. +
  5. 莱文贝格-马夸特方法(Levenberg-Marquardt)
  6. +
  7. 高斯-牛顿迭代法 (Gauss-Newton iteration method)
  8. +
  9. 拟牛顿法(BFGS)
  10. +
+

资料

+

Formula Sheet: Sheet summarising all the formulae covered in this course.

+ + +
+ +
+ + + +

Code and Notebooks

+ + +
+ +
+
+ + + + + + +
+
+
Mathematics for Machine Learning: Multivariate Calculus
+
https://zhangzhao219.github.io/2022/07/31/Coursera/Mathematics-for-Machine-Learning-Multivariate-Calculus/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年7月31日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/01/Coursera/Advanced-Learning-Algorithms/index.html b/2022/08/01/Coursera/Advanced-Learning-Algorithms/index.html new file mode 100644 index 000000000..cb83501bf --- /dev/null +++ b/2022/08/01/Coursera/Advanced-Learning-Algorithms/index.html @@ -0,0 +1,988 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced Learning Algorithms - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Advanced Learning Algorithms

+ + +
+ +

机器学习-高级学习算法

+ +

课程简介

+

In the second course of the Machine Learning Specialization, you will:

+

• Build and train a neural network with TensorFlow to perform multi-class classification
+• Apply best practices for machine learning development so that your models generalize to data and tasks in the real world
+• Build and use decision trees and tree ensemble methods, including random forests and boosted trees

+

The Machine Learning Specialization is a foundational online program created in collaboration between DeepLearning.AI and Stanford Online. In this beginner-friendly program, you will learn the fundamentals of machine learning and how to use these techniques to build real-world AI applications.

+

This Specialization is taught by Andrew Ng, an AI visionary who has led critical research at Stanford University and groundbreaking work at Google Brain, Baidu, and Landing.AI to advance the AI field.

+

This 3-course Specialization is an updated and expanded version of Andrew’s pioneering Machine Learning course, rated 4.9 out of 5 and taken by over 4.8 million learners since it launched in 2012.

+

It provides a broad introduction to modern machine learning, including supervised learning (multiple linear regression, logistic regression, neural networks, and decision trees), unsupervised learning (clustering, dimensionality reduction, recommender systems), and some of the best practices used in Silicon Valley for artificial intelligence and machine learning innovation (evaluating and tuning models, taking a data-centric approach to improving performance, and more.)

+

By the end of this Specialization, you will have mastered key theoretical concepts and gained the practical know-how to quickly and powerfully apply machine learning to challenging real-world problems. If you’re looking to break into AI or build a career in machine learning, the new Machine Learning Specialization is the best place to start.

+

生物神经网络

+

生物神经元:通过树突接收到来自不同地方的输入,然后通过轴突将神经冲动传递出去。

+

但是目前对于人脑的运作方式了解的还不是很透彻。

+

Tensorflow搭建神经网络

+
    +
  1. 定义神经网络:
  2. +
+
model = Sequential(
+    [   
+        tf.keras.Input(shape=(400,)),    #specify input size
+        tf.keras.layers.Dense(25, activation='sigmoid'),
+        tf.keras.layers.Dense(15, activation='sigmoid'),
+        tf.keras.layers.Dense(1, activation='sigmoid')
+    ], name = "my_model" 
+)
+
    +
  1. 训练神经网络
  2. +
+
model.compile(
+    loss=tf.keras.losses.BinaryCrossentropy(),
+    optimizer=tf.keras.optimizers.Adam(0.001),
+)
+
+model.fit(
+    X,y,
+    epochs=20
+)
+
    +
  1. 预测
  2. +
+
prediction = model.predict(X[0].reshape(1,400))  # a zero
+print(f" predicting a zero: {prediction}")
+prediction = model.predict(X[500].reshape(1,400))  # a one
+print(f" predicting a one:  {prediction}")
+

Python搭建神经网络

+
    +
  1. 定义网络层
  2. +
+
def my_dense_v(A_in, W, b, g):
+    """
+    Computes dense layer
+    Args:
+      A_in (ndarray (m,n)) : Data, m examples, n features each
+      W    (ndarray (n,j)) : Weight matrix, n features per unit, j units
+      b    (ndarray (1,j)) : bias vector, j units  
+      g    activation function (e.g. sigmoid, relu..)
+    Returns
+      A_out (ndarray (m,j)) : m examples, j units
+    """
+    z = np.matmul(A_in,W)+b
+    A_out = g(z)
+    return(A_out)
+
    +
  1. 组合不同的层
  2. +
+
def my_sequential_v(X, W1, b1, W2, b2, W3, b3):
+    A1 = my_dense_v(X,  W1, b1, sigmoid)
+    A2 = my_dense_v(A1, W2, b2, sigmoid)
+    A3 = my_dense_v(A2, W3, b3, sigmoid)
+    return(A3)
+
    +
  1. 预测
  2. +
+
Prediction = my_sequential_v(X, W1_tmp, b1_tmp, W2_tmp, b2_tmp, W3_tmp, b3_tmp )
+Prediction.shape
+

通用人工智能(AGI)

+

人工智能(AI)可以分为两种,ANI和AGI:

+

ANI指在某一特定领域应用的人工智能,目前已经取得了很好的效果;

+

AGI指通用人工智能,人工智能可以做任何人类可以做到的事情。

+

鉴于对人脑的了解还不够,如果通过模拟人脑的方式达到通用人工智能比较困难。

+

不过目前有一些进展,让通用人工智能看到了一点点希望。

+

训练神经网络

+
    +
  1. 决定输入变量、模型的数学形式、参数以及最终输出的结果形式
  2. +
  3. 定义损失函数和代价函数(损失函数是针对一个训练样本而言的,代价函数是结合全部训练数据的损失函数得来的)
  4. +
  5. 在数据上使用某种方法(如梯度下降法)进行训练,从而使代价函数最小
  6. +
+

激活函数

+

如果不使用激活函数,那么不管多么复杂的神经网络都会退化成线性回归方法可以实现的效果。

+

Sigmoid激活函数:

+

ReLU激活函数:

+

如何选择输出层的激活函数:

+
    +
  1. 二分类问题,选择Sigmiod激活函数
  2. +
  3. 线性回归问题不使用激活函数,如果确保没有负数值出现,可以使用ReLU激活函数
  4. +
+

隐藏层中大多数使用ReLU激活函数而非Sigmoid激活函数

+
    +
  1. ReLU激活函数比Sigmoid激活函数计算更快
  2. +
  3. ReLU激活函数与x轴平行的部分更少,使用梯度下降算法运行更快
  4. +
+

多类别分类

+

多类别分类是指输出不止两种情况的分类问题,如对手写数字进行分类,输出的类别会有10个

+

可以使用Softmax回归算法:

+

+

+

损失函数:,也就是

+

多标签分类:可以看成很多多类别分类问题,也可以使用一个神经网络预测所有的类别

+

优化方法

+

Adam优化方法:自动调节学习率

+

如果梯度下降的方向一直是同一方向则增大学习率,让算法运行更快

+

如果梯度下降的方向一直在波动,则减小学习率。

+

机器学习问题诊断

+

如果发现训练好的模型在预测上存在很大的问题,可以从以下几个方面入手查找原因:

+
    +
  1. 采集更多的训练样本——高方差
  2. +
  3. 尝试减小特征数目——高方差
  4. +
  5. 尝试增加额外的特征——高偏差
  6. +
  7. 尝试增加一些其他多项式的特征,如等等——高偏差
  8. +
  9. 尝试增加或减少正则项——高方差、高偏差
  10. +
+

训练时对训练集进行划分,可以划分为训练集和测试集,如果希望使用交叉验证的方式,可以划分为训练集、验证集和测试集。通过测试集的表型评估模型的效果。模型的选择上,可以从多项式的次数从低到高依次进行选择,找出测试集误差最小的模型。

+

更大规模的神经网络的偏差往往更小

+

如果恰当选择正则化参数,更大规模的神经网络的表现比小规模的神经网络表现更好

+

偏差和方差

+

欠拟合:函数对于训练集的拟合效果不好——高偏差(high bias)

+

过拟合:函数对于训练集的拟合效果好,对于测试集的效果不好——高方差(high variance)

+

避免过拟合的方法:

+
    +
  1. 收集更多的训练数据
  2. +
  3. 从全部的特征中挑选最相关的特征进行训练
  4. +
  5. 正则化——减小某一参数对拟合函数的影响
  6. +
+

正则项参数对模型的影响

+
    +
  1. 太大的导致模型的训练集拟合效果不好——高偏差(high bias)
  2. +
  3. 太小的导致模型对于训练集的拟合效果好,对于测试集的效果不好——高方差(high variance)
  4. +
+

学习曲线:

+
    +
  1. 正常的学习曲线,随着训练集样本数量的增加,训练集的误差会逐渐增大,验证集的误差会逐渐减小,但是验证集的误差会始终大于训练集的误差
  2. +
  3. 如果一个模型偏差比较大,增加更多的训练数据不会帮助提升效果
  4. +
  5. 如果一个模型的方差比较大,可以考虑增加更多的训练数据
  6. +
+

评价分类(尤其针对分布不平衡的数据)

+

+

+

+

决策树

+

熵(Entropy)

+

信息增益

+
    +
  1. 在根结点处使用所有的数据示例
  2. +
  3. 对每一种可能的分类方式计算信息增益,选择信息增益最高的分类方式
  4. +
  5. 使用上一步选择的分类方式对数据进行划分,划分成为左子树和右子树
  6. +
  7. 重复上述的操作,直到①某一个节点仅有一种类别②决策树高度超过阈值③信息增益小于阈值
  8. +
+

如果一个决策结点有三个可选项,可以通过独热编码的方式将其转换为多个二分类形式。

+

如果变量是连续的数值,可以计算从那里开始划分的信息增益最高,从而转化为判断大小于的二分类形式。

+

决策树解决回归问题,则将熵替换为节点上数据的方差进行计算。

+

随机森林:

+
    +
  1. 有放回采样训练数据,并且分别使用采样后的训练数据训练决策树
  2. +
  3. 为了使决策树的决策结点不完全相同,每一次选取特征的时候只选取一部分子集的特征
  4. +
  5. 最后使用投票法确定最终的分类
  6. +
+

XGBoost:采样训练数据的时候更倾向于采样前面的树中被分类错误的数据

+

决策树更适用于结构化的数据,快速,但是不适用于其他类似于图片文本等的数据

+

神经网络适用于所有类型的数据,运行可能稍慢一些,可以迁移学习,更适合将不同的神经网络结合到一起。

+

资料

+

第一周课件和代码

+

Notebooks Week 1

+ + +
+ +
+ + + +

第二周课件和代码

+

Notebooks Week 2

+ + +
+ +
+ + + +

第三周课件

+ + +
+ +
+ + + +

第四周课件

+ + +
+ +
+ + + +

作业代码

+

Exercise 1

+

Exercise 2

+

Exercise 3

+

Exercise 4

+ + +
+ +
+
+ + + + + + +
+
+
Advanced Learning Algorithms
+
https://zhangzhao219.github.io/2022/08/01/Coursera/Advanced-Learning-Algorithms/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月1日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/04/Coursera/Unsupervised-Learning-Recommenders-Reinforcement-Learning/index.html b/2022/08/04/Coursera/Unsupervised-Learning-Recommenders-Reinforcement-Learning/index.html new file mode 100644 index 000000000..94f0f193c --- /dev/null +++ b/2022/08/04/Coursera/Unsupervised-Learning-Recommenders-Reinforcement-Learning/index.html @@ -0,0 +1,996 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unsupervised Learning, Recommenders, Reinforcement Learning - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Unsupervised Learning, Recommenders, Reinforcement Learning

+ + +
+ +

机器学习-无监督学习,推荐系统与强化学习

+ +

课程简介

+

In the third course of the Machine Learning Specialization, you will:

+

• Use unsupervised learning techniques for unsupervised learning: including clustering and anomaly detection.
+• Build recommender systems with a collaborative filtering approach and a content-based deep learning method.
+• Build a deep reinforcement learning model.

+

The Machine Learning Specialization is a foundational online program created in collaboration between DeepLearning.AI and Stanford Online. In this beginner-friendly program, you will learn the fundamentals of machine learning and how to use these techniques to build real-world AI applications.

+

This Specialization is taught by Andrew Ng, an AI visionary who has led critical research at Stanford University and groundbreaking work at Google Brain, Baidu, and Landing.AI to advance the AI field.

+

This 3-course Specialization is an updated and expanded version of Andrew’s pioneering Machine Learning course, rated 4.9 out of 5 and taken by over 4.8 million learners since it launched in 2012.

+

It provides a broad introduction to modern machine learning, including supervised learning (multiple linear regression, logistic regression, neural networks, and decision trees), unsupervised learning (clustering, dimensionality reduction, recommender systems), and some of the best practices used in Silicon Valley for artificial intelligence and machine learning innovation (evaluating and tuning models, taking a data-centric approach to improving performance, and more.)

+

By the end of this Specialization, you will have mastered key concepts and gained the practical know-how to quickly and powerfully apply machine learning to challenging real-world problems. If you’re looking to break into AI or build a career in machine learning, the new Machine Learning Specialization is the best place to start.

+

无监督学习

+

无监督学习是在没有标签的数据中自动寻找某些规律

+

聚类任务是典型的无监督学习任务,通过某些特征将相似的人或事物自动归为一类

+

无监督学习任务还有异常检测(找出一些不寻常的数据)和维度降低(使用更少的数字对数据进行压缩)

+

聚类

+

聚类是一种典型的无监督学习算法,不定义标签,让算法自己去寻找数据中有趣的特征

+

聚类可以在下面几个方面得到应用:

+
    +
  1. 找出比较相似的新闻
  2. +
  3. 对用户或者市场进行分析
  4. +
  5. 分析DNA
  6. +
  7. 分析宇宙数据
  8. +
+

K-means聚类步骤:

+
    +
  1. 随机初始化个中心点预先定义)
  2. +
  3. 计算其余的点与中心点的距离,与最近的中心点归为一类
  4. +
  5. 更改中心点为类别中所有点的平均值
  6. +
  7. 迭代上述步骤直到所有点的类别不再变化
  8. +
+

如何决定聚类的数量?Elbow method

+

多种聚类数量都尝试一下,找到“肘点”,也就是增加聚类数量后代价函数也不能明显减小的点

+

如何随机初始化最初的类别中心点?

+
    +
  1. 随机选择几个训练样本作为中心点
  2. +
  3. 随机选取中心点多次,运行一轮算法,寻找代价最小的作为初始化的中心点
  4. +
+

异常检测

+

已经拥有一些数据,增加一条数据,判断其是否符合已有的数据的特征,如果不符合则为异常数据

+

正态分布:

+

异常检测:,计算点的是否满足大于预先定义的阈值

+

实际应用中,可以找一些有标记的异常点,指导算法选取合适的阈值

+

在某种类别(异常)的数据量很少的情况下,且异常的种类较多,特征无法很好区分出来的时候,使用异常检测算法比较好。

+

推荐系统

+

场景:很多用户对电影进行打分,分数从0-5,如何向用户推荐合适的电影?

+

设用户的数量为,电影的数量为

+

如果用户对电影进行了打分,那么,反之

+

表示用户对电影打分的分数(0-5)

+

表示电影的特征数量(如浪漫程度、武打程度等等),则用户对应的特征向量为

+

表示的是用户打分的电影数目

+

预测用户对电影的打分:

+

代价函数:

+

对所有用户来说,是定值,忽略不计

+

协同过滤算法

+

前面是有特征,通过类似于线性回归的方式可以进行预测,但是如果没有特征应该怎么做呢?

+

已知,预测

+

代价函数:

+

将两个代价函数结合到一起:

+

如果评分是二值化的,则类似于线性回归与逻辑回归的区别:

+

+

+

+

如果一个人没有对任何电影进行评分,则选取其他所有人的评分平均值作为他的评分。

+

协同过滤算法的局限性:

+
    +
  1. 对于新加进来的事物不太好办,没有办法与其他的一起排名,且推荐后有一点讲不出道理
  2. +
  3. 不能使用一些已有的其他特征
  4. +
+

基于内容的过滤算法

+

协同过滤算法是基于用户的评分,根据比较相似的评分情况来进行推荐

+

基于内容的过滤算法是基于用户和物品的特征来寻找比较合适的匹配对

+

设用户对应的特征是,电影对应的特征是

+

比较两个特征之间相似度的方法是点乘,但是两者的维度不同,因此要对输入的特征增加几层神经网络,使其输出相同,再进行点乘。

+

通过神经网络后,的32维向量,的32维向量,

+

代价函数为::

+

大型推荐系统

+

检索和排序策略:

+
    +
  1. 检索策略会从大规模中选择可信的候选者,如对于电影来说找这个国家最流行的20个电影等等,然后汇总、去重
  2. +
  3. 然后对去重后的列表使用算法进行排序,按照排名的先后顺序向用户推荐。
  4. +
+

强化学习

+

强化学习不告诉应该怎么做,而是只告诉做什么,如果做的好有奖励,做的不好有惩罚,从而让算法自动向着奖赏最多的方向优化,最终学习出最好的结果。

+

目前的状态、动作、奖励、下一个状态,下一个状态的动作

+

每一个时间步后,会有一个权重,最终的返回值(Return)是权重与奖励的乘积

+

一般来说,权重按照幂的方式变化,如第一步是,第二步是,第步是

+

措施指的是在状态应该采取什么样的动作

+

强化学习的目标就是找到合适的措施从而最大化返回的奖励(Return)

+

马尔可夫决策过程:未来只取决于现在所处的位置,与之前是怎么到达现在这个位置的无关。

+

状态-动作方程:表示从状态开始进行动作,然后后面采取最优化的动作

+

因此,可以得出两个结论:

+
    +
  1. 从状态开始的最佳的返回奖励是
  2. +
  3. 从状态开始的最好的动作是能达到的动作
  4. +
+

贝尔曼方程:

+

在更为复杂的环境下,状态之间的转移可能并不是确定的,有一定的几率到达其他的状态

+

因此得到的返回奖励实际上是期望的返回奖励,即

+

状态空间可能是连续的,对于月球车来说,有方向(前后左右和旋转)和速度两种变量,因此

+

强化学习神经网络(DQN)

+

训练神经网络:输入是,输出目的是找到最合适的动作使得最大。其中,神经网络的最后一层输出的神经元数量可以修改为的数量,就可以对所有可能情况的进行同时训练。

+

训练步骤:

+
    +
  1. 随意进行一个动作,得到
  2. +
  3. 采集大量的
  4. +
  5. 使用作为输入,作为输出训练使得
  6. +
  7. ,重复上述步骤
  8. +
+

算法优化

+

-贪心策略:在DQN的第一步中,以的概率随意选取,以的概率选取能使最大化的

+

mini-batch:在只选取一部分进行训练

+

soft update:步骤中,并不直接修改,而是使用权重对新旧参数进行组合

+

强化学习的现状

+
    +
  1. 用于实验室模拟的效果比较好,实际中有些困难
  2. +
  3. 目前的应用比监督学习或者无监督学习要少很多
  4. +
  5. 在未来应用上还是有很大的潜力的
  6. +
+

资料

+

第一周课件

+ + +
+ +
+ + + +

第二周课件

+ + +
+ +
+ + + +

第三周课件和代码

+

Notebooks Week 3

+ + +
+ +
+ + + +

作业代码

+

Exercise 1

+

Exercise 2

+

Exercise 3

+

Exercise 4

+

Exercise 5

+ + +
+ +
+
+ + + + + + +
+
+
Unsupervised Learning, Recommenders, Reinforcement Learning
+
https://zhangzhao219.github.io/2022/08/04/Coursera/Unsupervised-Learning-Recommenders-Reinforcement-Learning/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月4日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/06/ndarray-axis/index.html b/2022/08/06/ndarray-axis/index.html new file mode 100644 index 000000000..72d42ffa4 --- /dev/null +++ b/2022/08/06/ndarray-axis/index.html @@ -0,0 +1,943 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Numpy中axis的理解 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Numpy中axis的理解

+ + +
+ +

Numpy是个好东西,但是ndarray的轴感觉弄不太明白。可能二维三维数组还好,要是再增加几维就无法在脑海中想象这个东西,对于一些有关轴的操作就稀里糊涂,只能一个个尝试。现在准备把它彻底弄明白!

+ +

思路

+

首先从二维入手,然后扩展到三维以及更高的维度(从特殊到一般),然后找出普遍的规律,再进行验证(从一般到特殊)

+

官方文档应该是最权威的,首先看官方文档是怎么说明的,然后查找一些资料,看看其他人是怎么理解的,最后总结出自己的一套规律

+
import numpy as np
+

ndarray.shape

+

感受一个ndarray,最简单的方法就是打印ndarray的shape。

+

官方文档里面是这样写的:

+

the dimensions of the array. This is a tuple of integers indicating the size of the array in each dimension. For a matrix with n rows and m columns, shape will be (n,m). The length of the shape tuple is therefore the number of axes, ndim.

+

只列举了矩阵的例子,尝试一下:

+
a1 = np.arange(15).reshape(3, 5)
+print(a1,'\n',a1.shape,'\n',a1.ndim)
+

输出结果:

+
[[ 0  1  2  3  4]
+ [ 5  6  7  8  9]
+ [10 11 12 13 14]] 
+ (3, 5) 
+ 2
+
    +
  1. reshape成什么样,最后打印出来的shape就会是什么样,这一点可以确定。
  2. +
  3. 官方文档里面写道“对于一个n行m列的矩阵来说,shape将会是(n,m)”。经验证,打印出来了一个3行5列的矩阵,shape是(3,5)。
  4. +
  5. 官方文档里面写道“shape元组的长度就是轴的数量,也就是ndim”。经验证,ndim=2
  6. +
+

简单推断:最开始有2个方括号,因此矩阵是2维的,且第1个方括号内部有3个“2级方括号”,每一个“2级方括号”内部都有5个元素,因此这个shape可能是从外向里数的。

+

尝试1维ndarray:

+
a2 = np.arange(15)
+print(a2,'\n',a2.shape,'\n',a2.ndim)
+

输出结果:

+
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14] 
+ (15,) 
+ 1
+
    +
  1. 打印ndim为1,最开始有1个方括号,因此数组是1维的。结论得到验证。
  2. +
  3. 打印shape为(15,)(一维元组),第1个方括号内部没有“2级方括号”shape从外向里数只有15。结论得到验证。
  4. +
+

尝试3维ndarray:

+
a3 = np.arange(24).reshape(3,2,4)
+print(a3,'\n',a3.shape,'\n',a3.ndim)
+

输出结果:

+
[[[ 0  1  2  3]
+  [ 4  5  6  7]]
+
+ [[ 8  9 10 11]
+  [12 13 14 15]]
+
+ [[16 17 18 19]
+  [20 21 22 23]]] 
+ (3, 2, 4) 
+ 3
+
    +
  1. 打印ndim为3,最开始有3个方括号,因此数组是3维的。结论得到验证。
  2. +
  3. 打印shape为(3, 2, 4),第1个方括号内部有3个“2级方括号”,“2级方括号”内部有2个“3级方括号”,“3级方括号”内部有4个元素。满足shape从外向里数,结论得到验证。
  4. +
+

尝试4维ndarray:

+
a4 = np.arange(24).reshape(3,2,1,4)
+print(a4,'\n',a4.shape,'\n',a4.ndim)
+

输出结果:

+
[[[[ 0  1  2  3]]
+
+  [[ 4  5  6  7]]]
+
+
+ [[[ 8  9 10 11]]
+
+  [[12 13 14 15]]]
+
+
+ [[[16 17 18 19]]
+
+  [[20 21 22 23]]]] 
+ (3, 2, 1, 4) 
+ 4
+
    +
  1. 打印ndim为4,最开始有4个方括号,因此数组是4维的。结论得到验证。
  2. +
  3. 打印shape为(3, 2, 1, 4),第1个方括号内部有3个“2级方括号”,“2级方括号”内部有2个“3级方括号”,“3级方括号”内部有1个“4级方括号”,“4级方括号”内部有4个元素。满足shape从外向里数,结论得到验证。
  4. +
  5. 有一个维度是1,也就是这个维度实际上并没有任何的作用。但是在实际中可能会有“凑维度”的操作,需要手动增加或者减少维度,会出现这种维度为1的情况。(增加维度使用reshape()实现,减小维度使用squeeze()实现)
  6. +
+

因此可以得出结论:对于给定的ndarray,判断ndim就是计数最前面有多少个相连的方括号,判断shape就是从外向内看,每一层分别有多少个“元素”。

+

也可以看出,数组超过4维后,肉眼就有些难以区分了。

+

索引

+

索引就是取数组中的某些元素,官方文档有下面的举例:

+
>>> a = np.arange(30).reshape(2, 3, 5)
+>>> a
+array([[[ 0,  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]]])
+>>> a[0, 2, :]
+array([10, 11, 12, 13, 14])
+>>> a[0, :, 3]
+array([ 3,  8, 13])
+

索引操作是与shape相对应的。如上述例子,a[0]即为取数组的第1个维度(2)的第1个元素,这样原来3维的数组就降到了2维;a[0, :]就是在a[0]的基础上取数组的第2个维度(3)的全部元素,数组的维度不变,还是2维;a[0, :, 3]就是在a[0, :]的基础上取数组的第3个维度(5)的第4个元素,即可得出上面的结果。

+

索引操作后的维度与索引的数量以及是否有“:”相关。如果索引的数量与ndim相同,则最后取出来的是一个数。如果数量不同或者有“:”(数量不同可以看成在后面补“:”),则最终取得的数组的维度与“:”对应的原数组的维度相同。

+

+

以numpy.sum为例:

+

官方文档:

+

Axis or axes along which a sum is performed. The default, axis=None, will sum all of the elements of the input array. If axis is negative it counts from the last to the first axis.

+

If axis is a tuple of ints, a sum is performed on all of the axes specified in the tuple instead of a single axis or all the axes as before.

+

以三维数组为例:

+
print('origin')
+print(a3,a3.shape)
+print('axis=0')
+print(a3.sum(axis=0),a3.sum(axis=0).shape)
+print('axis=1')
+print(a3.sum(axis=1),a3.sum(axis=1).shape)
+print('axis=2')
+print(a3.sum(axis=2),a3.sum(axis=2).shape)
+print('axis=(0,1)')
+print(a3.sum(axis=(0,1)),a3.sum(axis=(0,1)).shape)
+print('axis=(1,2)')
+print(a3.sum(axis=(1,2)),a3.sum(axis=(1,2)).shape)
+print('axis=(0,2)')
+print(a3.sum(axis=(0,2)),a3.sum(axis=(0,2)).shape)
+print('axis=(0,1,2)')
+print(a3.sum(axis=(0,1,2)),a3.sum(axis=(0,1,2)).shape)
+
origin
+[[[ 0  1  2  3]
+  [ 4  5  6  7]]
+
+ [[ 8  9 10 11]
+  [12 13 14 15]]
+
+ [[16 17 18 19]
+  [20 21 22 23]]] (3, 2, 4)
+axis=0
+[[24 27 30 33]
+ [36 39 42 45]] (2, 4)
+axis=1
+[[ 4  6  8 10]
+ [20 22 24 26]
+ [36 38 40 42]] (3, 4)
+axis=2
+[[ 6 22]
+ [38 54]
+ [70 86]] (3, 2)
+axis=(0,1)
+[60 66 72 78] (4,)
+axis=(1,2)
+[ 28  92 156] (3,)
+axis=(0,2)
+[114 162] (2,)
+axis=(0,1,2)
+276 ()
+

axis为多少,就是在这个维度上进行操作,最终的结果就是这个维度消失

+

不要从行列什么的去思考怎么变化,直接从shape的角度入手。设置axis为多少,这个维度就没有了!比如原来是(3,2,4)的维度,要是axis=0,第一个维度就没有了,加和得到的矩阵就是(2,4)。

+

如果希望保留维度,可以增加keepdims=True的选项,这样被操作的维度就会变为1而不是直接消失。

+
print('axis=(0,1)')
+print(a3.sum(axis=(0,1),keepdims=True),a3.sum(axis=(0,1),keepdims=True).shape)
+
axis=(0,1)
+[[[60 66 72 78]]] (1, 1, 4)
+

这样想应该会比较好理解,尤其是对于更高维的数组来说,行列的概念基本失效,从shape的角度思考会好。

+

np.concatenate

+

另外一个比较常用的操作是np.concatenate,可以将数组进行合并,在数据处理或者神经网络中很常用。

+

在np.concatenate上检验一下对于axis的理解:

+
ta = np.arange(24).reshape(3,2,4)
+tb = np.arange(24,36).reshape(3,1,4)
+print(ta,ta.shape)
+print(tb,tb.shape)
+
[[[ 0  1  2  3]
+  [ 4  5  6  7]]
+
+ [[ 8  9 10 11]
+  [12 13 14 15]]
+
+ [[16 17 18 19]
+  [20 21 22 23]]] (3, 2, 4)
+[[[24 25 26 27]]
+
+ [[28 29 30 31]]
+
+ [[32 33 34 35]]] (3, 1, 4)
+

两者合并,第2个维度不相同,应该是可以合并的,合并后的shape应该为(3,3,4)

+
print(np.concatenate((ta,tb),axis=1),np.concatenate((ta,tb),axis=1).shape)
+
[[[ 0  1  2  3]
+  [ 4  5  6  7]
+  [24 25 26 27]]
+
+ [[ 8  9 10 11]
+  [12 13 14 15]
+  [28 29 30 31]]
+
+ [[16 17 18 19]
+  [20 21 22 23]
+  [32 33 34 35]]] (3, 3, 4)
+

np.concatenate除了在待合并的axis上之外,必须具有相同的shape

+

之前的结论也得到了验证。

+

总结

+

我们处在三维空间中,二维和三维是比较直观的,可以在脑海中想象出来。因此我们会觉得axis的设计有些反直觉。以后应该从shape的角度去看待axis的设计思想,首先理解上比较直观,其次在更高维度的数组上也能合理的进行操作。不要去思考数组实际中应该是个什么样子,直接观察axis就足够了。

+

参考资料

+

Code

+

Numpy官方文档

+ + +
+ +
+
+ + + + + + +
+
+
Numpy中axis的理解
+
https://zhangzhao219.github.io/2022/08/06/ndarray-axis/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月6日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/07/lend-money/index.html b/2022/08/07/lend-money/index.html new file mode 100644 index 000000000..0b6009523 --- /dev/null +++ b/2022/08/07/lend-money/index.html @@ -0,0 +1,753 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 有关借钱的碎碎念 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

有关借钱的碎碎念

+ + +
+ + +

上大学之前,父母特意嘱咐借钱的问题,我当时觉得现在和以前不一样了,以前父母上学的时候可能零花钱限额比较严格,生活也不太好。现在大家基本都是独生子女,而且正规的借钱手段也不少,比如“花呗”等等。因此我不觉得有人会向我借钱,我自己也决定把不借给别人钱作为一条原则。大学四年风平浪静,然而今天下午就突然碰到了这个问题。

+

大学有三个室友,分别用QLD代替吧。Q保研了本校,L在深圳工作,D在二战考研。首先要说明的是,在我2年对他们的了解下,人品都没有问题。今天下午Q突然打微信电话给我,要借6000元钱。(可惜是微信电话没有留下录音证据)首先他说的是个人原因,明天就会还我,不好找父母要。再三追问下,他说是“类似于赌博的平台,自己是内部参加的,就是自己操作失误了,现在急需这些钱,明天肯定能赚回来”云云。他还说就算赚不回来明天也会找父母要还我。当然我非常相信他的人品,但是我感觉这个就是一个非常经典的骗局,我不想看到他越陷越深,就劝了他一阵,主要说的就是希望他能冷静下来好好想想。我劝不回来他,中间有一句话骗了他一下说我现在没有钱,也要找父母要。最终也没有借给他。后面打过电话后我也劝了几句话,但是他没有回复我。

+ +

我也想到了他会找我的另外的室友,我也知道这件事情知道的人越少越好,所以我找了和他关系更好一点的L。结果L一直都没有回复我,差不多半个多小时后,我们才交流了一下。L告诉我Q除了找我之外也找了L和D,他们俩一人借了3000元钱,算是Q借钱成功了。

+

我不知道D怎么想的,但是可能他家比较有钱,可能人家也不在乎这个。L和我交流了一下,大意就是他也知道十有八九是一个骗局,但是还是借给他了。

+ +

我本来还是挺理直气壮的,我认为我做的没什么问题,而且很遗憾没有把他们俩及时劝住。但是L一句话让我没话说了。

+

“如果他找其他人借,那事情会更难搞”

+

不是黑Q,主要是大学几年我们都看在眼里,确实没有什么很好的朋友。可能平时聊天最多的就是我们三个了。真的如果我们三个都不借给他,那他会怎么办呢?难道我们几个真的能劝住吗?而且已经毕业了我们又不在身边,谁知道他会做什么?高利贷?我不敢往下想了。都毕业了不可能找学校的任何人帮助他,又不知道他家庭的电话,打110也只能打到我自己家。所以是不是在充分了解这个人的情况下,劝说无效后借钱给他才是最好的帮助他的办法?

+

所以我一直到晚上一直都不太舒服,感觉自己一直坚守的底线被我自己动摇了。以后再遇到这样的事情怎么办?

+

还这么办!坚守底线!听对象的话!

+

当然我有十足的把握是了解Q的,才会这么纠结。一般的同学或者同事什么的肯定坚守底线的。

+

明天就出结果了,当然我还是认为99%是被骗了,现在又比较担心骗子还有后招,他会不会继续借钱。

+

后续:

+ +

果然是被骗了,傻孩子,咋办啊。。。。

+

后续更新:一直都没有理我,估计是认为我不够意思了。哎,心情复杂,没有什么办法,朋友没了就没了吧。

+ + +
+ +
+
+ + + + + + +
+
+
有关借钱的碎碎念
+
https://zhangzhao219.github.io/2022/08/07/lend-money/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月7日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/09/c-plus-basic/index.html b/2022/08/09/c-plus-basic/index.html new file mode 100644 index 000000000..650ca1406 --- /dev/null +++ b/2022/08/09/c-plus-basic/index.html @@ -0,0 +1,1823 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + C++ Primer - 第一部分 C++基础 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

C++ Primer - 第一部分 C++基础

+ + +
+ +

C++ Primer 阅读笔记 - 第一部分 C++基础

+ +

开始学习

+

大一学过C语言,当时学的不是很好,但是后面接触到算法竞赛的时候就慢慢补上来了,而且增加了一些C++特性以及STL标准模板库,也靠着半吊子C++拿了一些小奖,但是确实没有系统的学过C++。总之听说C++比较难,这次准备半系统性的学习一下。之前会的东西就做做题简单过一下,不会的重点看,尤其是指针和面向对象方面。希望以后能更加得心应手地使用C++,也为后面求职打打基础。

+

第1章 开始

+

注释

+
std::cout << "/*";
+std::cout << "*/";
+std::cout << /*  "*/" *.;
+std::cout << /* "*/" /* "/*" */;
+

前两行没问题,注释只有一边,编译运行顺利通过

+

第三行注释全,但是字符串不全,缺少右边的",编译运行不能通过

+

第四行两边分别有两组注释,且中间的字符串是全的,因此编译运行顺利通过

+

读取数量不定的输入

+
int sum = 0,value = 0;
+while(std::cin >> value){
+    sum += value;
+}
+std::cout << sum << std::endl;
+

读取数量不定的整数,将其加和。

+

std::cin属于一种 istream对象,将其作为条件时是检测流的状态,遇到文件结束符或者无效输入时会变为无效,从而退出循环。

+

在Ubuntu中输入 Ctrl+D来输入一个文件结束符。

+

类简介

+

定义好的头文件:

+
#ifndef SALESITEM_H
+// we're here only if SALESITEM_H has not yet been defined 
+#define SALESITEM_H
+
+// Definition of Sales_item class and related functions goes here
+#include <iostream>
+#include <string>
+
+class Sales_item {
+// these declarations are explained section 7.2.1, p. 270 
+// and in chapter 14, pages 557, 558, 561
+friend std::istream& operator>>(std::istream&, Sales_item&);
+friend std::ostream& operator<<(std::ostream&, const Sales_item&);
+friend bool operator<(const Sales_item&, const Sales_item&);
+friend bool 
+operator==(const Sales_item&, const Sales_item&);
+public:
+    // constructors are explained in section 7.1.4, pages 262 - 265
+    // default constructor needed to initialize members of built-in type
+    Sales_item() = default;
+    Sales_item(const std::string &book): bookNo(book) { }
+    Sales_item(std::istream &is) { is >> *this; }
+public:
+    // operations on Sales_item objects
+    // member binary operator: left-hand operand bound to implicit this pointer
+    Sales_item& operator+=(const Sales_item&);
+  
+    // operations on Sales_item objects
+    std::string isbn() const { return bookNo; }
+    double avg_price() const;
+// private members as before
+private:
+    std::string bookNo;      // implicitly initialized to the empty string
+    unsigned units_sold = 0; // explicitly initialized
+    double revenue = 0.0;
+};
+
+// used in chapter 10
+inline
+bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs) 
+{ return lhs.isbn() == rhs.isbn(); }
+
+// nonmember binary operator: must declare a parameter for each operand
+Sales_item operator+(const Sales_item&, const Sales_item&);
+
+inline bool 
+operator==(const Sales_item &lhs, const Sales_item &rhs)
+{
+    // must be made a friend of Sales_item
+    return lhs.units_sold == rhs.units_sold &&
+           lhs.revenue == rhs.revenue &&
+           lhs.isbn() == rhs.isbn();
+}
+
+inline bool 
+operator!=(const Sales_item &lhs, const Sales_item &rhs)
+{
+    return !(lhs == rhs); // != defined in terms of operator==
+}
+
+// assumes that both objects refer to the same ISBN
+Sales_item& Sales_item::operator+=(const Sales_item& rhs) 
+{
+    units_sold += rhs.units_sold; 
+    revenue += rhs.revenue; 
+    return *this;
+}
+
+// assumes that both objects refer to the same ISBN
+Sales_item 
+operator+(const Sales_item& lhs, const Sales_item& rhs) 
+{
+    Sales_item ret(lhs);  // copy (|lhs|) into a local object that we'll return
+    ret += rhs;           // add in the contents of (|rhs|) 
+    return ret;           // return (|ret|) by value
+}
+
+std::istream& 
+operator>>(std::istream& in, Sales_item& s)
+{
+    double price;
+    in >> s.bookNo >> s.units_sold >> price;
+    // check that the inputs succeeded
+    if (in)
+        s.revenue = s.units_sold * price;
+    else 
+        s = Sales_item();  // input failed: reset object to default state
+    return in;
+}
+
+std::ostream& 
+operator<<(std::ostream& out, const Sales_item& s)
+{
+    out << s.isbn() << " " << s.units_sold << " "
+        << s.revenue << " " << s.avg_price();
+    return out;
+}
+
+double Sales_item::avg_price() const
+{
+    if (units_sold) 
+        return revenue/units_sold; 
+    else 
+        return 0;
+}
+#endif
+
+

暂时不用怎么管,先试着使用:

+
    +
  1. 读取单价和数量,输出总价格
  2. +
+
Sales_item book; // 创建一个对象
+std::cin >> book;
+std::cout << book << std::endl;
+return 0;
+
0-201-70353-x 4 24.99
+> 0-201-70353-x 4 99.96 24.99
+
    +
  1. 对象相加,输出总价格和平均价格
  2. +
+
Sales_item book1,book2; // 创建一个对象
+std::cin >> book1 >> book2;
+std::cout << book1+book2 << std::endl;
+return 0;
+
0-201-70353-x 3 20.00
+0-201-70353-x 2 25.00
+> 0-201-70353-x 5 110 22
+
    +
  1. 增加成员函数,加和之前先判断两书的序列号是否相等
  2. +
+
Sales_item book1,book2; // 创建一个对象
+std::cin >> book1 >> book2;
+if(book1.isbn() == book2.isbn()){
+    std::cout << book1+book2 << std::endl;
+}
+else{
+    std::cerr << "Error!" << std::endl;
+}
+return 0;
+
0-201-70353-x 3 20.00
+0-201-70343-x 2 25.00
+> Error!
+
    +
  1. 读取销售记录,生成每本书的销售报告
  2. +
+
#include <bits/stdc++.h>
+#include "Sales_item.h"
+int main(void){
+    Sales_item total;
+    if(std::cin >> total){ // 读取第一条数据,确保有数据可以处理
+        Sales_item trans;
+        while(std::cin >> trans){
+            if(total.isbn() == trans.isbn()){
+                total += trans;
+            }
+            else{
+                std::cout << total << std::endl;
+                total = trans;
+            }
+        }
+        std::cout << total << std::endl; // 打印最后一本书
+    }
+    else{
+        std::cerr << "No data!" << std::endl;
+    }
+    return 0;
+}
+
0-201-70353-X 4 24.99
+0-201-82470-1 4 45.39
+0-201-88954-4 2 15.00 
+0-201-88954-4 5 12.00 
+0-201-88954-4 7 12.00 
+0-201-88954-4 2 12.00 
+0-399-82477-1 2 45.39
+0-399-82477-1 3 45.39
+0-201-78345-X 3 20.00
+0-201-78345-X 2 25.00
+> 
+0-201-70353-X 4 99.96 24.99
+0-201-82470-1 4 181.56 45.39
+0-201-88954-4 16 198 12.375
+0-399-82477-1 5 226.95 45.39
+0-201-78345-X 5 110 22
+

这个程序的局限性在于,必须是连号的输入,不连号的输入就失效了。

+

当然这个时候学到的还不多,后面会将这个程序继续完善。

+

第2章 变量和基本类型

+

整型可以分为带符号类型和无符号类型(在前面添加 unsigned

+

选择类型的原则:

+
    +
  1. 明确知道不可能为负值时,选用无符号类型
  2. +
  3. 整数运算使用int,超过范围了使用long long
  4. +
  5. 浮点数运算使用double
  6. +
  7. 不要在算术表达式中使用char或者bool
  8. +
  9. 不要混用无符号类型和带符号类型,因为带符号类型会自动转换为无符号类型,运算过程中出现负值即错误
  10. +
+

初始化

+

创建变量时赋予其一个初始值(赋值指的是将对象的当前值用一个新值来替代,含义不同)

+

初始化的4种方式:

+
    +
  1. int a = 0;
  2. +
  3. int a = {0};
  4. +
  5. int a{0}; // 列表初始化
  6. +
  7. int a(0);
  8. +
+

变量声明:“一个文件如果想使用别处定义的名字,必须包含对那个名字的声明”

+

与定义的区别在于不赋初值

+

extern int i;

+

作用域

+
    +
  1. 作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字
  2. +
  3. 允许在内层作用域中重新定义外层作用域已有的名字
  4. +
+

如:

+
int a = 0;
+int main(void){
+    std::cout << a << std::endl;
+    int a = 1;
+    std::cout << a << std::endl;
+    std::cout << ::a << std::endl; // 显式指定访问全局变量
+    return 0;
+}
+
0
+1
+0
+

引用

+

相当于为对象起一个另外的名字,通过 &符号来定义

+
int ival = 1024;
+int &refVal = ival;
+

引用必须初始化,因为引用需要和它的初始化对象一起绑定在一起,不能重新绑定到其他对象。

+

定义引用之后,对其进行的所有操作都是在它的绑定对象上进行的

+
refVal = 12;
+std::cout << refVal << std::endl;
+
12
+

引用本身不是一个对象,不能定义引用的引用

+

如下面的方式,实际上是绑定到了该引用对应的绑定对象上:

+
int &refVal2 = refVal;
+std::cout << refVal2 << std::endl;
+
12
+

引用的类型要与绑定的对象严格匹配

+

引用不能绑定到字面值上

+

指针

+

指针也实现了对其他对象的间接访问,但是指针本身也是一个对象,通过 *符号来定义

+
    +
  1. 指针存放某个对象的地址,如果获取这个地址,需要使用取地址符 &
  2. +
+
int ival = 42;
+int *p = &ival;
+

指针的类型也要与它所指向的对象严格匹配

+
    +
  1. 如果指针指向了一个对象,可以使用解引用符 *来访问这个对象
  2. +
+
int ival = 42;
+int *p = &ival;
+std::cout << *p << std::endl;
+
42
+

符号的多重含义:

+
int i = 42;
+int &r = i; // &随类型名出现,是声明的一部分,r是一个引用
+int *p; // *随类型名出现,是声明的一部分,p是一个指针
+p = &i; // &出现在表达式中,是一个取地址符
+*p = i; // *出现在表达式中,是一个解引用符
+int &r2 = *p; // r2是一个引用,*是一个解引用符
+std::cout << i << std::endl << r << std::endl << *p << std::endl << r2 << std::endl;
+
42
+42
+42
+42
+

空指针:int *p1 = nullptr

+

建议:初始化所有的指针

+

指针与引用不同,是可以赋值的。赋值的时候永远改变的是等号左侧的对象。

+

void*指针,可以用于存放任意类型对象的地址

+
double obj = 3.14;
+double *pd = &obj;
+void *pv = &obj;
+pv = pd;
+std::cout << *pv << std::endl;
+
error: ‘void*’ is not a pointer-to-object type
+

void*指针只能与其他指针作比较,作为函数的输入和输出,或者赋值给另外一个 void*指针。

+

甚至连访问对象都不可以

+

指向指针的指针

+
int ival = 1024;
+int *pi = &ival;
+int **ppi = π
+std::cout << ival << std::endl;
+std::cout << *pi << std::endl;
+std::cout << **ppi << std::endl;
+
1024
+1024
+1024
+

指向指针的引用

+
int i = 42;
+int *p;
+int *&r = p;
+r = &i; // p = &i;
+std::cout << i << std::endl;
+std::cout << *r << std::endl;
+std::cout << *p << std::endl;
+
42
+42
+42
+

阅读定义要从右往左,离变量名最近的符号对变量类型有最直接的影响

+

最近的是 &,因此 r是一个引用

+

然后是 *,说明 r引用的是一个指针

+

const

+

const对象一旦创建,值不可以再改变,因此在创建的时候必须初始化

+

const int a = 45;

+

只能在 const类型的对象上执行不改变其内容的操作

+

可以添加extern关键字,使const变量在文件间共享

+
extern const int bufSize = fcn(); // file.cpp定义并初始化了这个常量,可以被其他文件访问
+extern const int bufSize; // file.h 和上面的变量是同一个,只是一个声明,说明定义会在其他地方出现
+

const的引用是对常量的引用,不能改变引用的值,引用的时候也要添加 const限定符

+
const int ci = 1024;
+const int &r1 = ci;
+

初始化常量引用时可以使用任意的表达式,只要表达式的结果能转化成引用的类型即可

+
int i = 42;
+const int &r1 = i;
+const int &r2 = 42;
+const int &r3 = r1 * 2;
+std::cout << r1 << std::endl;
+std::cout << r2 << std::endl;
+std::cout << r3 << std::endl;
+i = 56;
+std::cout << r1 << std::endl;
+std::cout << r2 << std::endl;
+std::cout << r3 << std::endl;
+
42
+42
+84
+56
+42
+84
+

因此,对 const的引用可以并非一个 const的对象,不能通过这种引用改变被引用的对象的值,但是可以通过其他方式改变这个对象的值

+

指向常量的指针也不能用于改变其所指对象的值,且指向常量的指针所指的对象也不一定是一个常量

+
const double pi = 3.14;
+const double *cptr = &pi
+std::cout << *cptr << std::endl;
+double dval = 3.14;
+cptr = &dval;
+std::cout << *cptr << std::endl;
+
3.14
+3.14
+

const指针:将指针本身定义为常量,也就是指针所指的地址不变

+
int errNumb = 0;
+int *const curErr = &errNumb; // curErr将一直指向errNumb,不能改变
+const double pi = 3.14159;
+const double *const pip = &pi // 一个指向常量对象的常量指针
+*curErr = 56;
+std::cout << errNumb << std::endl;
+
56
+

指针所指的地址不变,但是如果指向的不是常量,还是可以改变指向的值的

+

顶层 const可以表示任意的对象是一个常量,底层 const与复合类型有关,指的是下一层对象是常量。

+

常量表达式:值不会改变且在编译过程就能得到计算结果的表达式

+

将变量声明为 constexpr来由编译器验证是否为一个常量表达式:constexpr int limit = mf + 1;

+

constexpr中如果声明了一个指针,那么一定是常量指针,即顶层 const

+

处理变量类型

+

类型别名的两种定义方式:

+
typedef double wages;
+using wages = double;
+

如果别名是一个复合类型,不能仅仅将其替换进行理解。

+

auto类型:将类型交给编译器自己去分析,一般会忽略掉顶层 const,如果需要保留要加 const auto进行推断

+

decltype类型指示符:通过表达式的类型推断出要定义的变量的类型

+
const int a = 0,b = 0;
+decltype(a+b) x = 0;
+std::cout << x << std::endl;
+
0
+

如果希望得到引用类型,可以添加两层括号,即 decltype((a+b))

+

自定义数据结构

+
struct Sales_data{
+    std::string bookNo;
+    unsigned units_sold = 0;
+    double revenue = 0.0;
+};
+

读取单价和数量,输出总价格

+
Sales_item book; // 创建一个对象
+std::cin >> book;
+std::cout << book << std::endl;
+return 0;
+
0-201-70353-x 4 24.99
+> 0-201-70353-x 4 99.96 24.99
+

头文件:包含只能被定义一起的实体

+

通过头文件保护符来确保不允许重复包含:

+
#ifndef SALES_DATA_H
+#define SALES_DATA_H
+#include <string>
+struct Sales_data{
+    std::string bookNo;
+    unsigned units_sold = 0;
+    double revenue = 0.0;
+};
+#endif
+

第3章 字符串、向量和数组

+

using声明:使用命名空间中的成员

+
using std::cin;
+

头文件不应包含 using声明

+

标准库类型string

+

定义和初始化

+
string s1;
+string s2(s1); // string s2 = s1;
+string s3("value"); // string s3 = "value";
+string s4(10,'c');
+cout << s1 << endl;
+cout << s2 << endl;
+cout << s3 << endl;
+cout << s4 << endl;
+

+
+value
+cccccccccc
+

初始化分为直接初始化和拷贝初始化,有 =的为拷贝初始化,一般只用于单个初始值的情况下

+

string对象的操作

+

输入输出与对整数等的操作相同

+

使用getline读入一整行(可以带空格)

+
string line;
+while(getline(cin,line)){
+    cout << line << endl;
+}
+return 0;
+
> fds fdsfdsf dsf
+fds fdsfdsf dsf
+> dsfdsfdsfds fdsfds 
+dsfdsfdsfds fdsfds
+

string.size()返回的是无符号整形数,不要去负数值混用

+

字面值不为字符串,不能将字面值相加,只能将字符串相加,如 "df"+"fdsfs"是不合法的

+

基于范围的 for语句:遍历给定序列中的每一个元素

+
string str("some string");
+for (auto c : str){
+    cout << c;
+}
+cout << endl;
+
some string
+

如果要改变字符,需要使用引用类型:

+
string str("some string");
+for (auto &c : str){
+    c = toupper(c);
+}
+cout << str << endl;
+
SOME STRING
+

标准库类型vector

+

vector属于一个类模板,模板不是类或者函数,但是可以看作编译器生成类或函数编写的一份说明,编译器根据模板创建类或函数的过程称为实例化

+

定义和初始化vector对象

+
vector<int> ivec;
+vector<int> ivec2(ivec);
+vector<int> ivec3 = ivec;
+vector<string> articles{"a","an","the"};
+vector<string> svec(10,"hi");
+for(auto i : svec){
+    cout << i << " ";
+}
+cout << endl;
+
hi hi hi hi hi hi hi hi hi hi
+

值初始化:只初始化 vector的大小,不赋值具体数值 vector<int> i(10)

+

其他vector操作

+

vector在设计上事先指定容量是不好的做法,比较适合运行时再添加具体的值

+

循环内部如果包含向 vector添加元素的语句,不能使用范围 for循环

+

不能用下标形式添加元素,也就是下标操作只能对确知已经存在的元素进行

+

迭代器

+
vector<int> vi = {1,2,4,5,7,8,9,5,6,4};
+for(auto it1 = vi.begin();it1 != vi.end();it1++){
+    *it1 *= 2;
+}
+for(auto it2 = vi.cbegin();it2 != vi.cend();it2++){
+    cout << *it2 << " ";
+}
+cout << endl;
+
2 4 8 10 14 16 18 10 12 8
+

数组

+

定义与初始化

+
int a2[] = {0,1,2};
+int a3[5] = {1,2,4}; // 多余的初始化成默认值
+char a4[8] = "Daniel"; // 至少是7,要有一个空字符
+for (auto i: a3){
+    cout << i << " ";
+}
+cout << endl;
+for (auto i: a4){
+    cout << i << " ";
+}
+cout << endl;
+
1 2 4 0 0 
+D a n i e l
+

数组不允许拷贝和赋值

+

复杂的数组声明:

+
int *ptrs[10]; // 含有10个整型指针的数组
+int arr[10];
+int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
+int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
+int *(&arry)[10] = ptrs; // arry是数组的引用,该数组含有10个指针
+

指针和数组

+

使用数组的时候编译器一般将其转化为指针

+
string nums[] = {"one","two","three"};
+string *p = &nums[0]; // p指向nums的第1个元素
+string *p2 = nums // 等价于上面的语句
+

使用 auto推断时会返回一个指针,但是只用 decltype推断的时候会返回数组

+

利用指针对数组可以起到迭代器的效果

+
int ia[] = {1,2,3,4,5,6,7,8,9};
+int *beg = begin(ia); // 指向ia的第一个元素
+int *last = end(ia); // 指向ia的最后一个元素的下一个位置
+for(auto it = beg;it != last;it++){
+    cout << *it << " ";
+}
+cout << endl;
+

C风格字符串

+

string转化为C风格字符串

+
string s("Hello World!");
+const char *str = s.c_str();
+cout << *str << endl;
+

使用数组初始化vector

+
int int_arr[] = {0,1,2,3,4,5};
+vector<int> ivec(begin(int_arr),end(int_arr));
+for(auto it : ivec){
+    cout << it << " ";
+}
+cout << endl;
+
0 1 2 3 4 5
+

指针和多维数组

+
int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
+int (*p)[4] = ia; // p指向含有4个整数的数组
+p = &ia[2]; // p 指向ia的尾元素
+for(auto p = ia;p != ia+3;++p){
+    for(auto q = *p;q != *p+4;q++){
+        cout << *q << " ";
+    }
+}
+cout << endl;
+for(auto p = begin(ia);p != end(ia);++p){
+    for(auto q = begin(*p);q != end(*p);q++){
+        cout << *q << " ";
+    }
+}
+cout << endl;
+

第4章 表达式

+

通俗的讲,左值就是能够出现在赋值符号左面的东西,而右值就是那些可以出现在赋值符号右面的东西.

+

左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象

+

右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。

+

当一个对象被用作右值的时候,使用的是对象的值(内容);当一个对象被用作左值的时候,用的是对象的身份(在内存中的位置)

+
    +
  1. 算术运算符的运算结果和求值对象都是右值
  2. +
+

m%n的符号与 m相同

+
    +
  1. 逻辑和关系运算符的运算结果和求值对象都是右值
  2. +
  3. 赋值运算符的左侧运算对象必须是一个可修改的左值,结果是他的左侧运算对象,并且是一个左值
  4. +
  5. 递增和递减运算符必须作用于左值运算对象,前置版本将对象本身作为左值返回,后置版本将对象原始值的副本作为右值返回
  6. +
  7. 箭头运算符作用于一个指针类型的运算对象,结果是一个左值
  8. +
  9. 点运算符的结果与成员所属的对象相同
  10. +
  11. 条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值,否则运算的结果是右值
  12. +
+

强制类型转换:

+
int i = 52;
+int j = 9;
+double slope = static_cast<double>(j) / i;
+cout << slope << endl;
+
0.173077
+

第5章 语句

+

switch语句:

+
int a;
+while(cin >> a){
+    switch(a){
+        case 0: cout << '1' << endl;break;
+        case 1: cout << '2' << endl;break;
+        case 2: cout << '3' << endl;break;
+        case 3: cout << '4' << endl;break;
+        case 4: cout << '5' << endl;break;
+        case 5: cout << '6' << endl;break;
+        case 6: cout << '7' << endl;break;
+        case 7: cout << '8' << endl;break;
+        case 8: cout << '9' << endl;break;
+        case 9: cout << '0' << endl;break;
+        default: cout << 'N' << endl;break;
+    }
+}
+
> 0
+1
+> 9
+0
+> 45
+N
+

try语句块和异常处理:

+

throw语句抛出异常:

+
int a = 1;
+throw runtime_error("fdsdfds");
+
terminate called after throwing an instance of 'std::runtime_error'
+  what():  fdsdfds
+Aborted
+

catch语句捕捉异常:

+
double m, n;
+cin >> m >> n;
+try {
+    if (n == 0)
+        throw - 1;  //抛出整型异常
+    else if (m == 0)
+        throw - 1.0;  //拋出 double 型异常
+    else
+        cout << m / n << endl;
+}
+catch (double d) {
+    cout << "catch (double)" << d << endl;
+}
+catch (...) {
+    cout << "catch (...)" << endl;
+}
+
> 0 6
+catch (double)-1
+> 6 0
+catch (...)
+

第6章 函数

+

局部静态对象:程序第一次经过时被初始化,直到程序终止时才被销毁。

+
int count_calls(){
+    static int ctr = -1;
+    return ++ctr;
+}
+
+int main(void){
+    for(int i = 0;i != 10; ++i){
+        cout << count_calls() << " ";
+    }
+    cout << endl;
+    return 0;
+}
+
0 1 2 3 4 5 6 7 8 9
+

函数声明(函数原型):在使用函数之前对函数的名字进行声明

+

函数声明可以忽略形参的名字,也可以加上形参的名字。

+

函数声明最好写在头文件中

+

分离式编译:编译和链接多个源文件

+

参数传递

+

指针形参:

+
void reset(int *ip){
+    *ip = 0;
+}
+
+int main(void){
+    int i = 42;
+    reset(&i);
+    cout << i << endl;
+    return 0;
+}
+
0
+

传引用参数:

+
void reset(int &i){
+    i = 0;
+}
+
+int main(void){
+    int i = 42;
+    reset(i);
+    cout << i << endl;
+    return 0;
+}
+
0
+

尽量使用引用形式从而避免拷贝

+

还可以通过引用形式返回一些额外信息。因为函数只能返回一个返回值,但是如果某个值是引用的形式传到函数中的,也会保留下修改后的值。

+

不修改的变量尽量使用常量引用

+

数组形参:

+
void Print(const int i[]){
+    cout << i[0] << endl;
+}
+
+void Print(const int *i){
+    cout << i[0] << endl;
+}
+
+void Print(const int i[10]){
+    cout << i[0] << endl;
+}
+
+int main(void){
+    int i = 5;
+    int j[2] = {6,7};
+    Print(&i);
+    Print(j);
+}
+
5
+6
+

数组不能直接进行传递,直接作为指针的形式传递,因此丢掉了数组大小的信息

+

可以使用指针的形式进行提示,也可以传入一个表示数组大小的参数。

+
void print(const int *beg,const int *end){
+    while(beg != end){
+        cout << *beg++ << " ";
+    }
+}
+
+void print(const int i[] ,size_t size){
+    for(size_t a=0;a<size;a++){
+        cout << i[a] << " ";
+    }
+}
+
+int main(void){
+    int i[10] = {6,7,5,4,7,8,9,6,5,4};
+    print(begin(i),end(i));
+    cout << endl;
+    return 0;
+}
+
6 7 5 4 7 8 9 6 5 4
+

数组引用形参:(缺点是只能作用于大小固定的数组)

+
void print(int (&arr)[10]){
+    for(auto elem : arr){
+        cout << elem << " ";
+    }
+}
+

含有可变形参的函数:

+
void error_msg(initializer_list<string> il){
+    for(auto beg = il.begin();beg != il.end();++beg){
+        cout << *beg << " ";
+    }
+    cout << endl;
+}
+
+int main(void){
+    error_msg({"a","b"});
+    error_msg({"a","b","c"});
+}
+
a b 
+a b c
+

函数的返回值

+

函数返回时不要返回局部对象的引用或指针

+

调用一个返回引用的函数会得到左值

+
char &get_val(string &str,string::size_type ix){
+    return str[ix];
+}
+
+int main(void){
+    string s("a value");
+    cout << s << endl;
+    get_val(s,0) = 'A';
+    cout << s << endl;
+    return 0;
+}
+
a value
+A value
+

返回值也可以是一个花括号包围起来的列表

+

函数重载

+

定义相同名称的函数,但是形参列表不同,可能是数量上的不同,也可能是类型上的不同。使得函数调用的时候根据不同的形参列表自动判断指定哪一个函数。

+

顶层 const不影响传入的参数

+

在不同的作用域中无法重载函数,会覆盖掉

+

特殊用途语言特性

+

默认实参:在函数的声明中给一个默认值,调用时可以覆盖掉,也可以不写以使用默认值。

+

内联函数:将函数在调用点展开,但是编译器不一定支持

+

constexpr函数:能用于常量表达式的函数,函数的返回值和所有形参的类型都要是字面值类型,函数体中有且只有一条 return语句。

+

assert表达式:用于调试的时候对程序进行检查 assert(s == "dfdsf");如果不满足条件程序会中断退出。

+

函数指针

+

完全不明白。。。没有示例程序看不懂

+

第7章 类

+

定义抽象数据类型

+

成员函数是类定义的一部分,通过特定的对象来调用。非成员函数就是普通的函数。

+

成员函数的声明必须在类的内部,定义可以在类的内部或者外部。非成员函数的声明和定义都在类的外部。

+

构造函数:控制对象的初始化过程

+

访问控制与封装:

+

定义在public说明符后的成员在整个程序内可被访问,public成员定义类的接口。

+

定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。

+

使用class和struct定义类的区别在于默认的访问权限不同,struct默认访问权限都是public的

+

友元:令其他类或成员成为访问它的非公有成员。但是友元只算一个权限控制,在类外一样要进行声明。

+

上述代码:

+
#include <bits/stdc++.h>
+using std::string;
+using std::vector;
+using std::cin;
+using std::cout;
+using std::endl;
+using std::begin;
+using std::end;
+using std::runtime_error;
+using std::initializer_list;
+using std::istream;
+using std::ostream;
+
+class Sales_data{
+friend Sales_data add(const Sales_data&,const Sales_data&);
+friend std::ostream &print(std::ostream&,const Sales_data&);
+friend std::istream &read(std::istream&,Sales_data&);
+public:
+    // 构造函数
+    Sales_data() = default; // 默认构造函数
+    // 构造函数初始值列表
+    Sales_data(const std::string &s):bookNo(s){ }
+    Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){ }
+    Sales_data(std::istream &);
+    // 常量成员函数
+    std::string isbn() const {
+        return bookNo;
+    }
+    Sales_data &combine(const Sales_data&);
+private:
+    double avg_price() const;
+    std::string bookNo;
+    unsigned units_sold = 0;
+    double revenue = 0.0;
+};
+Sales_data add(const Sales_data&,const Sales_data&);
+std::ostream &print(std::ostream&,const Sales_data&);
+std::istream &read(std::istream&,Sales_data&);
+
+
+// 在类的外部定义成员函数
+double Sales_data::avg_price() const {
+    if(units_sold){
+        return revenue / units_sold;
+    }
+    else{
+        return 0;
+    }
+}
+
+// 定义一个返回this对象的函数
+Sales_data& Sales_data::combine(const Sales_data &rhs){
+    units_sold += rhs.units_sold;
+    revenue += rhs.revenue;
+    return *this;
+}
+
+// 类相关的非成员函数
+istream &read(istream &is, Sales_data &item){
+    double price = 0;
+    is >> item.bookNo >> item.units_sold >> price;
+    item.revenue = price * item.units_sold;
+    return is;
+}
+
+ostream &print(ostream &os,const Sales_data &item){
+    os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();
+    return os;
+}
+
+Sales_data add(const Sales_data &lhs,const Sales_data &rhs){
+    Sales_data sum = lhs;
+    sum.combine(rhs);
+    return sum;
+}
+
+// 在类的外部定义构造函数
+Sales_data::Sales_data(std::istream &is){
+    read(is,*this);
+}
+
+int main(void){
+    return 0;
+}
+
+
+

类的其他特性

+
    +
  1. 定义一个类型成员:一般写在最开头的位置
  2. +
  3. 令成员作为内联函数:可以将 inline写在类内或者类外,一般写在类外
  4. +
  5. 重载成员函数
  6. +
  7. 可变数据成员:在 const里面也可以变化
  8. +
  9. 类数据成员的初始值:一个类里面由另外一个类提供初始值
  10. +
+
class Screen{
+    public:
+        typedef std::string::size_type pos; // 定义类型的成员
+        Screen() = default;
+        Screen(pos ht,pos wd,char c): height(ht), width(wd),contents(ht*wd,c){ }
+        char get() const {
+            return contents[cursor]; // 隐式内联函数
+        };
+        inline char get(pos ht,pos wd) const; // 显式内联函数
+        Screen &move(pos r, pos c); // 后面设置为内联函数
+        size_t some_member() const;
+        Screen &set(char);
+        Screen &set(pos,pos,char);
+        Screen &display(std::ostream &os){
+            do_display(os);
+            return *this;
+        }
+        const Screen &display(std::ostream &os) const {
+            do_display(os);
+            return *this;
+        }
+    private:
+        pos cursor = 0;
+        pos height = 0,width = 0;
+        std::string contents;
+        mutable size_t access_ctr = 0;
+        void do_display(std::ostream &os) const {
+            os << contents;
+        }
+};
+
+inline Screen &Screen::move(pos r,pos c){
+    pos row = r * width;
+    cursor = row + c;
+    return *this;
+}
+
+char Screen::get(pos r,pos c) const {
+    pos row = r * width;
+    return contents[row + c];
+}
+
+size_t Screen::some_member() const{
+    ++access_ctr;
+    return access_ctr;
+}
+
+inline Screen &Screen::set(char c){
+    contents[cursor] = c;
+    return *this;
+}
+
+inline Screen &Screen::set(pos r,pos col,char ch){
+    contents[r*width+col] = ch;
+    return *this;
+}
+
+// 类数据成员的初始值
+class Window_mgr{
+    private:
+        std::vector<Screen> screens{Screen(24,80,' ')};
+};
+
+int main(void){
+    Screen myscreen;
+    char ch = myscreen.get();
+    cout << myscreen.some_member() << endl;
+    ch = myscreen.get(0,0);
+    cout << myscreen.some_member() << endl;
+    myscreen.move(4,0);
+    myscreen.set('#');
+    cout << myscreen.get() << endl;
+    Screen myScreen(5,3,'!');
+    const Screen blank(5,3,'?');
+    myScreen.set(2,1,'#').display(cout);
+    cout << endl;
+    blank.display(cout);
+    cout << endl;
+    return 0;
+}
+
1
+2
+#
+!!!!!!!#!!!!!!!
+???????????????
+

类之间的友元关系:不存在传递性

+
class Screen{
+    friend class Window_mgr;
+
+// 类数据成员的初始值
+class Window_mgr{
+    public:
+        using ScreenIndex = std::vector<Screen>::size_type;
+        void clear(ScreenIndex);
+    private:
+        std::vector<Screen> screens{Screen(24,80,' ')};
+};
+
+void Window_mgr::clear(ScreenIndex i){
+    Screen &s = screens[i];
+    s.contents = string(s.height * s.width, ' ');
+}
+

类的作用域

+
class Screen{
+    friend class Window_mgr;
+    public:
+        typedef std::string::size_type pos; // 定义类型的成员
+        Screen() = default;
+        Screen(pos ht,pos wd,char c): height(ht), width(wd),contents(ht*wd,c){ }
+        char get() const {
+            return contents[cursor]; // 隐式内联函数
+        };
+        inline char get(pos ht,pos wd) const; // 显式内联函数
+        Screen &move(pos r, pos c); // 后面设置为内联函数
+        size_t some_member() const;
+        Screen &set(char);
+        Screen &set(pos,pos,char);
+        Screen &display(std::ostream &os){
+            do_display(os);
+            return *this;
+        }
+        const Screen &display(std::ostream &os) const {
+            do_display(os);
+            return *this;
+        }
+    private:
+        pos cursor = 0;
+        pos height = 0,width = 0;
+        std::string contents;
+        mutable size_t access_ctr = 0;
+        void do_display(std::ostream &os) const {
+            os << contents;
+        }
+};
+
+inline Screen &Screen::move(pos r,pos c){
+    pos row = r * width;
+    cursor = row + c;
+    return *this;
+}
+
+char Screen::get(pos r,pos c) const {
+    pos row = r * width;
+    return contents[row + c];
+}
+
+size_t Screen::some_member() const{
+    ++access_ctr;
+    return access_ctr;
+}
+
+inline Screen &Screen::set(char c){
+    contents[cursor] = c;
+    return *this;
+}
+
+inline Screen &Screen::set(pos r,pos col,char ch){
+    contents[r*width+col] = ch;
+    return *this;
+}
+
+// 类数据成员的初始值
+class Window_mgr{
+    public:
+        using ScreenIndex = std::vector<Screen>::size_type;
+        void clear(ScreenIndex);
+        ScreenIndex addScreen(const Screen&);
+    private:
+        std::vector<Screen> screens{Screen(24,80,' ')};
+};
+
+void Window_mgr::clear(ScreenIndex i){
+    Screen &s = screens[i];
+    s.contents = string(s.height * s.width, ' ');
+}
+
+Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s){
+    screens.push_back(s);
+    return screens.size() - 1;
+}
+
+int main(void){
+    Screen myscreen;
+    char ch = myscreen.get();
+    cout << myscreen.some_member() << endl;
+    ch = myscreen.get(0,0);
+    cout << myscreen.some_member() << endl;
+    myscreen.move(4,0);
+    myscreen.set('#');
+    cout << myscreen.get() << endl;
+    Screen myScreen(5,3,'!');
+    const Screen blank(5,3,'?');
+    myScreen.set(2,1,'#').display(cout);
+    cout << endl;
+    blank.display(cout);
+    cout << endl;
+    return 0;
+}
+

构造函数进阶

+

构造函数初始值列表:定义变量的时候习惯对其立即进行初始化,有时初始化的值是必不可少的,且要注意成员初始化的顺序。

+

委托构造函数:使用它所属类的其他构造函数执行它自己的初始化过程

+

默认构造函数

+

类类型转换

+

聚合类:就是比较简单的结构体

+

字面值常量类

+

类的静态成员

+

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据

+ + +
+ +
+
+ + + + + + +
+
+
C++ Primer - 第一部分 C++基础
+
https://zhangzhao219.github.io/2022/08/09/c-plus-basic/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月9日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/21/c-plus-stl/index.html b/2022/08/21/c-plus-stl/index.html new file mode 100644 index 000000000..728fa8ce7 --- /dev/null +++ b/2022/08/21/c-plus-stl/index.html @@ -0,0 +1,1145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + C++ Primer - 第二部分 C++标准库 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

C++ Primer - 第二部分 C++标准库

+ + +
+ +

C++ Primer - 第二部分 C++标准库

+ +

第8章 IO库

+

IO类

+

iostream定义了用于读写流的基本类型

+

fstream定义了读写命名文件的类型

+

sstream定义了读写内存 string对象的类型

+

用法都是完全相同的,得益于继承机制

+

流的状态:

+
auto old_state = cin.rdstate(); // 获取流的当前状态
+cin.clear(); // 将所有条件状态复位,将流的状态置为有效
+cin.setstate(old_state); // 根据给定的标志位对流进行复位
+

输出缓冲:每个输出流都管理一个缓冲区,保存程序读写的数据。

+

控制输出缓冲:

+
cout << "hi!" << endl; // 多输出一个换行符,然后刷新缓冲区
+cout << "hi!" << flush; // 输出后直接刷新缓冲区
+cout << "hi!" << ends; // 多输出一个空字符,然后刷新缓冲区
+cout << unitbuf; // 所有输出操作后都立即刷新缓冲区
+cout << nounitbuf; // 回到正常的缓冲方式
+

关联输入和输出流:如果某一个输入流和输出流关联,则从输入流读取的操作会对这个输出流进行刷新。

+

标准库将 coutcin关联在一起

+
cin.tie(&cerr); // 将cin和cerr关联在一起
+

文件输入输出

+
ifstream in("infile");
+ofstream output("outfile");
+string s;
+while(getline(in,s)){
+    output << s << endl;
+}
+return 0;
+

显式打开或者关闭文件流:

+
ofstream output; // 空文件流对象
+output.open("outfile"); // 调用open进行关联
+output.close(); // 关闭文件流
+

文件模式:

+
ofstream output("outfile");
+

这种方式其实隐含了以输出模式打开文件并进行截断,显式控制如下:

+
ofstream output("outfile",ofstream::out | ofstream::trunc);
+

为了保留之前的文件内容,需要显式指定 app模式

+
ofstream output("outfile",ofstream::out | ofstream::app);
+

string流

+
ifstream in("infile");
+ofstream output("outfile",ofstream::out | ofstream::app);
+string s;
+while(getline(in,s)){
+    string a,b,c;
+    istringstream s1(s);
+    ostringstream s2;
+    s1 >> a;
+    s2 << a;
+    s1 >> b;
+    s2 << b;
+    s1 >> c;
+    s2 << c;
+    cout << s2.str() << endl;
+}
+return 0;
+

第9章 顺序容器

+

一个容器就是一些特定类型对象的集合,顺序容器为程序员提供了控制元素存储和访问顺序的能力。

+

顺序容器种类

+

vector是可变大小数组,支持快速随机访问。但是在尾部之外的位置插入或者删除元素可能很慢。

+

deque是双端队列,支持快速随机访问,在头尾部插入或者删除元素的速度很快。

+

list是双向链表,只支持双向顺序访问,在 list中任意位置进行插入/删除的速度都很快

+

forward_list是单向链表,只支持单向顺序访问,在链表中任意位置进行插入/删除的速度都很快

+

array是固定大小的数组,支持快速随机访问,不能添加或者删除元素

+

string是与 vector相似的容器,专门用于保存字符,随机访问快,在尾部插入/删除的速度很快

+

顺序容器几乎可以保存任意类型的元素

+

各种迭代器:

+
auto it1 = a.begin(); // list<string>::iterator
+auto it2 = a.rbegin(); // list<string>::reverse_iterator
+auto it3 = a.cbegin(); // list<string>::const_iterator
+auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
+

元素的拷贝初始化:

+
list<string> a = {"Milton","SHakespeare","Austen"};
+list<string> a2(a);
+

array具有固定的大小,并且可以进行拷贝

+
array<int,10> ia1 = {0,1,2,3,4,5,6,7,8,9};
+array<int,10> ia2 = ia1;
+

使用 assign对不同但相容的类型进行赋值

+
list<string> names;
+vector<const char*> oldstyle;
+names.assign(oldstyle.cbegin(),oldstyle.cend());
+

添加元素三种方法:push_front()insert()push_back()

+

新标准对应了三种直接构造元素的方法:emplace_front()emplace()emplace_back()

+

更安全的访问元素的方法:svec.at(0)

+

改变容器大小并使用某个元素填充更大的部分:ilist.resize(15,-1)

+

管理容量的成员函数:

+
c.capacity(); // 不重新分配内存空间的话最多能保存多少元素
+c.reserve(n); // 分配至少能容纳n个元素的内存空间
+c.shrink_to_fit() // 请求将capacity()减小为size()一样的大小
+

数值转换:

+
int i = 42;
+string s = to_string(i);
+double d = stod(s);
+cout << s << " " << d << endl;
+
42 42
+

第10章 泛型算法

+

对于容器的其他操作,并没有通过定义成员函数的方式实现,而是定义一套泛型算法,实现了一些算法的公共接口。

+

在容器中对值进行查找使用 find,返回查找元素的指针的位置

+
auto result = find(vec.cbegin(),vec.cend(),val)
+

返回元素在容器中出现的次数:

+
vector<int> a;
+int temp;
+for(int i=0;i<10;i++){
+    cin >> temp;
+    a.push_back(temp);
+}
+cin >> temp;
+auto result = count(a.cbegin(),a.cend(),temp);
+cout << result << endl;
+

泛型算法本身不会执行容器的操作,只会运行于迭代器之上,执行迭代器的操作。

+

因此泛型算法永远不会改变底层容器的大小。

+

各种泛型算法

+

元素求和:

+
vector<int> a{1,2,3,4,5,6,7,8};
+int sum = accumulate(a.cbegin(),a.cend(),0);
+cout << sum << endl;
+
36
+

可以推广到字符串中用来连接字符串:

+
vector<int> a{1,2,3,4,5,6,7,8};
+vector<string> b{"df","fsfds","rte"};
+string sum = accumulate(b.cbegin(),b.cend(),string(""));
+cout << sum << endl;
+
dffsfdsrte
+

确定两个序列中保存的值是否相同(假定第二个序列至少与第一个序列一样长)

+
vector<int> a{1,2,3,4,5,6,7,8};
+vector<string> b{"df","fsfds","rte"};
+vector<string> c{"df","fsfds","rte","fdsf"};
+auto sum = equal(b.cbegin(),b.cend(),c.cbegin());
+cout << sum << endl;
+
1
+

使用 fillfill_n填充元素:

+
vector<int> a{1,2,3,4,5,6,7,8};
+fill(a.begin(),a.end(),0);
+for(auto i : a){
+    cout << i << " ";
+}
+cout << endl;
+fill_n(a.begin(),a.size(),1);
+for(auto i : a){
+    cout << i << " ";
+}
+cout << endl;
+
0 0 0 0 0 0 0 0 
+1 1 1 1 1 1 1 1
+

算法是不会检查写操作的,泛型算法也不能更改容器的大小。因此需要自行检查容器是否越界等问题。

+

安全的方式:插入迭代器

+
vector<int> a{1,2,3,4,5,6,7,8};
+vector<string> b{"df","fsfds","rte"};
+vector<string> c{"df","fsfds","rte","fdsf"};
+fill_n(back_inserter(a),10,1);
+for(auto i : a){
+    cout << i << " ";
+}
+cout << endl;
+
1 2 3 4 5 6 7 8 1 1 1 1 1 1 1 1 1 1
+

a1的内容拷贝到 a2copy(begin(a1),end(a1),a2)

+

对元素进行排序去重:

+
vector<string> d{"the","quick","red","fox","jumps","over","the","slow","red","turtle"};
+sort(d.begin(),d.end()); // 排序
+auto end_unique = unique(d.begin(),d.end()); // 将重复的移到末尾,并同时返回最后一个不重复的元素的后一位置
+d.erase(end_unique,d.end()); // 使用容器操作删除重复的元素(因为泛型算法无法改变容器大小)
+for(auto i : d){
+    cout << i << " ";
+}
+cout << endl;
+
fox jumps over quick red slow the turtle
+

lambda表达式:

+

一个 lambda表达式表示一个可调用的代码单元,可以理解为一个未命名的内联函数。

+
auto f = []{return 42;};
+cout << f() << endl;
+
42
+
int a = 0, b = 1;
+auto f6 = [&a, &b]{ return a + b; };
+cout << f6() << endl;
+
1
+

lambda表达式还有其他的一些用法。

+

其他迭代器

+

插入迭代器:

+

back_inserter:创建一个使用 push_back的迭代器

+

front_inserter:创建一个使用 push_front的迭代器

+

inserter:创建一个使用 insert的迭代器

+
list<int> lst = {1,2,3,4};
+list<int> lst2 = {5,6};
+list<int> lst3 = {9,10,11};
+list<int> lst4 = {12};
+copy(lst.cbegin(),lst.cend(),front_inserter(lst2));
+for(auto i : lst2){
+    cout << i << " ";
+}
+cout << endl;
+copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin()));
+for(auto i : lst3){
+    cout << i << " ";
+}
+cout << endl;
+copy(lst.cbegin(),lst.cend(),back_inserter(lst4));
+for(auto i : lst4){
+    cout << i << " ";
+}
+cout << endl;
+
4 3 2 1 5 6 
+1 2 3 4 9 10 11 
+12 1 2 3 4
+

流迭代器:

+
istream_iterator<int> in(cin),eof;
+cout << accumulate(in,eof,0) << endl;
+
> 2 1 4 5 6 7 8 9
+42
+
ostream_iterator<int> out(cout," ");
+for(int i=0;i<10;i++){
+    out = i;
+}
+cout << endl;
+
0 1 2 3 4 5 6 7 8 9
+

反向迭代器:

+
vector<int> vi{1,2,3,4,5,6,7,8,9,10};
+for(auto i = vi.crbegin();i != vi.crend();i++){
+    cout << *i << " ";
+}
+cout << endl;
+
10 9 8 7 6 5 4 3 2 1
+

要注意反向迭代器真的是反的。。。比如下面的例子:

+
string line = "first,middle,end";
+auto rcomma = find(line.crbegin(),line.crend(),',');
+cout << string(line.crbegin(),rcomma) << endl;
+cout << string(rcomma.base(),line.cend()) << endl;
+
dne
+end
+

第11章 关联容器

+

关联容器中的元素是按关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。

+

map是关键字-值对的结合

+
map<string,size_t> word_count;
+string word;
+while(cin >> word){
+    ++word_count[word];
+}
+for(const auto &w : word_count){
+    cout << w.first << " " << w.second << endl;
+}
+
> a b c d e d b c
+a 1
+b 2
+c 2
+d 2
+e 1
+

关联容器的元素都是根据关键字存储的,因此不支持位置相关的操作。

+

multimapmultiset允许相同关键字:

+
vector<int> vi{1,2,3,4,5,5,4,3,2,1};
+set<int> iset(vi.cbegin(),vi.cend());
+multiset<int> miset(vi.cbegin(),vi.cend());
+cout << iset.size() << " " << miset.size() << endl;
+

关联容器的迭代器:

+
vector<int> vi{1,2,3,4,5,5,4,3,2,1};
+set<int> iset(vi.cbegin(),vi.cend());
+for(auto set_it = iset.cbegin();set_it != iset.cend();set_it++){
+    cout << *set_it << " ";
+}
+cout << endl;
+
1 2 3 4 5
+

插入元素:

+
iset.insert(8);
+

查找元素的下标操作:

+
c[k]; // 如果没有会添加,并对值进行初始化
+c.at(k); // 如果没有会抛出异常
+

访问元素:findcount

+

multimap中查找元素:

+
multimap<string,int> mi{make_pair("as",1),make_pair("as",2),make_pair("ab",2),make_pair("ac",2),make_pair("ac",5)};
+for(auto pos = mi.equal_range("as");pos.first != pos.second;++pos.first){
+    cout << pos.first->second << " ";
+}
+cout << endl;
+

根据转换规则对文件内容进行转换:

+

转换规则:

+
brb be right back
+k okay?
+y why
+r are
+u you
+pic picture
+thk thanks!
+l8r later
+

文件内容:

+
where r u
+y dont u send me a pic 
+k thk l8r
+

转换代码:

+
// 实际的转换工作,生成转换文本
+const string & transform(const string &s, const map<string,string> &m){
+    auto map_it = m.find(s);
+    if (map_it != m.cend()){
+        return map_it->second;
+    }
+    else{
+        return s;
+    }
+}
+
+
+// 读入给定文件,建立转换映射
+map<string,string> buildMap(ifstream &map_file){
+    map<string,string> trans_map;
+    string key,value;
+    // 读取第一个单词存入key,剩余内容存入value
+    while(map_file >> key && getline(map_file,value)){
+        if(value.size() > 1){
+            trans_map[key] = value.substr(1);
+        }
+        else{
+            throw runtime_error("No rule for " + key);
+        }
+    }
+    return trans_map;
+}
+
+int main(void){
+    ifstream map_file("rules");
+    ifstream input("text");
+    auto trans_map = buildMap(map_file); // 保存转换规则
+    string text; // 保存输入中的每一行
+    while(getline(input,text)){
+        istringstream stream(text); // 读取每个单词
+        string word;
+        bool firstword = true; // 控制是否打印空格
+        while(stream >> word){
+            if(firstword){
+                firstword = false;
+            }
+            else{
+                cout << " ";
+            }
+            cout << transform(word,trans_map);
+        }
+        cout << endl;
+    }
+    return 0;
+}
+

输出:

+
where are you
+why dont you send me a picture
+okay? thanks! later
+

无序容器:不适用比较运算符来组织元素,而是使用哈希函数组织元素。

+

一般情况下的性能要比有序容器更好,但是不能按照顺序输出。

+

第12章 动态内存

+

动态内存与智能指针

+

前面都是静态对象,由程序自动分配内存并销毁。而动态对象需要被显式进行释放。

+

动态内存需要显式进行分配和释放,因此很容易忘记释放导致一些问题。因此定义了两种智能指针来管理这些动态对象,自动进行释放。

+
shared_ptr<string> p1; // 指向string的shared_ptr
+shared_ptr<list<int>> p2; // 指向int的list的shared_ptr
+

默认初始化的智能指针中保存着一个空指针。

+

最安全的分配和使用动态内存的方式是调用 make_shared的标准库函数。

+
shared_ptr<int> p3 = make_shared<int>(42);
+shared_ptr<string> p4 = make_shared<string>(10,'9');
+shared_ptr<int> p5 = make_shared<int>();
+

shared_ptr会自动记录有多少个其他 shared_ptr指向相同的对象,如果没有了,会自动销毁所管理的对象并自动释放相关联的内存。

+

离开作用域也会被销毁。如果返回这个指针,也不会被销毁(就是挺智能的)

+

直接管理内存:使用 newdelete

+
int *pi = new int;
+string *ps = new string(10,'9');
+const string *pcs = new const string;
+delete pi;
+delete ps;
+delete pcs;
+

delete不会抛出任何异常,尽管可能已经释放过了,甚至有可能都不是指针也会释放,会造成一些问题。

+

delete还可能会造成空悬指针,因此这个 delete只提供了有限的保护。

+

不要混用智能指针和普通指针,不要使用 get初始化另一个智能指针或者赋值。

+

unique_ptr“拥有”它所指向的对象,某个时刻只能由一个 unique_ptr指向一个给定对象。销毁指针就一起销毁了。

+

weak_ptr指向一个 shared_ptr管理的对象,不会改变 shared_ptr的计数,计数为 0后会自动释放。

+

动态数组

+

使用 new分配一个 int数组:

+
int *p = new int[42];
+

实际上并没有得到一个数组类型的对象,而是得到一个数据元素类型的指针。

+

动态分配并初始化数组:

+
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
+

动态分配的数组的大小可以为0,会返回一个类似于尾后迭代器的指针。

+

释放动态数组:delete [] p

+

智能指针管理动态数组:

+
unique_ptr<int[]> up(new int[10]);
+for(size_t i = 0;i != 10;++i){
+    up[i] = i;
+}
+up.release();
+

allocator将内存分配和对象构造分离开来,提供一种类型感知的内存分配方法。

+
int n = 5;
+allocator<string> alloc;
+auto const p = alloc.allocate(n);
+

这个 allocator为5个 string分配了内存

+

在内存中构造对象:

+
auto q = p;
+alloc.construct(q++,"hi");
+cout << *p << endl;
+
hi
+

案例:文本查询程序

+

在一个给定文件中查询单词,最终返回单词在文件中出现的次数及其所在行的列表。

+ + +
+ +
+
+ + + + + + +
+
+
C++ Primer - 第二部分 C++标准库
+
https://zhangzhao219.github.io/2022/08/21/c-plus-stl/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月21日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/26/diary/diary20220826/index.html b/2022/08/26/diary/diary20220826/index.html new file mode 100644 index 000000000..4c9d47277 --- /dev/null +++ b/2022/08/26/diary/diary20220826/index.html @@ -0,0 +1,750 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20220826 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20220826

+ + +
+ +

开学第一周,或者算是第二周,开学但是没有任何课程,也没有什么活动,开了一次班会,然后一个学院的开学典礼。新认识的人呢,也就一个室友+之前室友的同学,也就寥寥几人,同一个套间住着的人几乎都不认识。这几天就一直有些不太舒服,写些文字简单发泄一下。

+

总的来说,这里确实是一群学霸。首先可以拿我室友来说,早上7点起床,晚上11点左右睡觉,几乎每时每刻都在看论文做实验,甚至在看比赛的过程中间也会去看论文。他的目标就是要发文章,发一篇顶会文章,因此现在在努力完成这个目标。之后的方向他还没有想好,可能出国或者找音频算法相关的工作。其次是图书馆的同学们,才开学没有几天,图书馆就已经爆满了。大家都是思维缜密且有计划的人,昨天一窝蜂去抢机房,抢各种台式电脑去选课,选过课后去找相关的书籍,这在之前都是我的标准操作,在这里却被其他人不断模仿甚至比我做的更好。我有一种压力感,同时也有一种恐惧。

+

我的内心真的很脆弱。感觉其他人都还很适应的,我表面上也是这样,但是内心里已经稍稍有点崩溃了。我不禁回想我本科阶段,如果我高考真的考的好了,去了一些顶级985的学校,那么我是会坚持住学下去拼下去,还是会基本上崩溃掉,完全没有任何的竞争实力了呢?或许去了中南大学,并不是考的不好,而是帮我减轻了同龄人的压力。现在研究生的阶段,我是真真正正感受到了同龄人的压力。这么多优秀的人当中,我又能排到一个什么水平?如果真的在各个方面都比不上别人,我会不会崩溃呢?这些都是我现在所担心的。

+

其实换个角度来想,我没有必要去和任何人去比较。大家的人生道路都是不一样的,也无所谓好与不好,只是适不适合,以及过的是否开心罢了。对于我现在来说,虽然我知道不要去和其他人比较,总有人比你更强,比你过的更好。但是我还是时不时会看看想想别人现在在做什么,看看别人取得的成就,想想自己有没有可能赶得上甚至超过。这样就造成了现在每一天都非常不开心,学习也没有什么动力,学到后面甚至有一点混时间的感觉。这种想法困扰了我很长的一段时间,目前仍然在困扰着我。

+

我现在能做的,就是找准自己的目标,制定好计划,坚定不移地实施下去。至于我脆弱的内心,慢慢调解吧。没有人能帮助我,最后能靠得住的只有我自己。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20220826
+
https://zhangzhao219.github.io/2022/08/26/diary/diary20220826/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月26日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/26/zhangzhao-plan-1/index.html b/2022/08/26/zhangzhao-plan-1/index.html new file mode 100644 index 000000000..dd82425d8 --- /dev/null +++ b/2022/08/26/zhangzhao-plan-1/index.html @@ -0,0 +1,2329 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 学习计划 (2022年8月——2023年1月) - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

学习计划 (2022年8月——2023年1月)

+ + +
+ +

学习计划

+ +

2022年8月——2023年1月

+

总计划

+
+

1. C++初步了解与练习(2022年8月-2022年9月)#6f42c1

+
+
+

2. 面试算法题重做,加深理解,记录笔记(2022年9月-2022年10月)#777

+
+
+

3. 确定研究方向和实习面试岗位方向(2022年10月-2022年11月)#5cb85c

+
+
+

4. 实习面试(2022年12月-2023年1月)#d9534f

+
+
+

5. 上课+复习+考试(尽量考好一点)(2022年9月-2023年1月)#f0ad4e

+
+
+

6. 根据本科毕设尝试发论文(2022年9月-2022年10月)#428bca

+
+
+

7. 竞赛初步尝试(2022年9月-2023年1月)#3c4858

+
+
+

8. 其他计划(2022年9月-2023年1月)#ffc0cb

+
+

具体计划及实施

+

2022年8月26日前

+
    +
  1. Coursera上新版吴恩达机器学习课程并完成练习、获得证书、作笔记。
  2. +
  3. Coursera上数学在机器学习领域的应用课程并完成练习、获得证书、作笔记。
  4. +
  5. C++ Primer书籍阅读并做实验和笔记,完成前两大部分,共12章的内容。
  6. +
  7. 开学、选课、EMO
  8. +
+

2022年8月27日-2022年9月30日

+

计划安排

+
+

1. C++练习(在算法题中练习+额外知识补充)

+
+
+

2. Leetcode 101练习,加深理解,记录笔记

+
+
+

3. 上课(充分利用课堂时间弄懂,拒绝耽误课后太长时间)

+
+
+

4. 根据本科毕设尝试发论文(与师兄和导师了解目前情况)

+
+
+

5. 竞赛初步尝试(学习、练习为重点)

+
+

实施情况

+
2022年8月27日-2022年9月2日
+
    +
  • 8.27 +
      +
    • 申请到了一台Hax服务器,尝试着搭建VPN但是没搭建好
    • +
    • Leetcode 101 练习(第二章和第三章的一部分)
    • +
    • 第一次去体育馆打乒乓球
    • +
    +
  • +
  • 8.28 +
      +
    • 申请到了一台woiden服务器,没有再去配置什么东西。
    • +
    • Leetcode 101 练习(第三章的剩余部分和第四章)
    • +
    • 上课准备(课表、空教室、课程资料等)
    • +
    +
  • +
  • 8.29 +
      +
    • 上课
    • +
    • Leetcode 101 练习(第五章)
    • +
    • 课程内容复习
    • +
    +
  • +
  • 8.30 +
      +
    • Leetcode 101 练习(第六章第一部分)
    • +
    • 听科技前沿讲座
    • +
    +
  • +
  • 8.31 +
      +
    • 上课
    • +
    • Leetcode 101 练习(第六章第二部分)
    • +
    • 卜东波算法课oj六道题
    • +
    • 课程内容复习
    • +
    +
  • +
  • 9.1 +
      +
    • 上课
    • +
    • Leetcode 101 练习(第六章结束)
    • +
    • 课程内容复习
    • +
    +
  • +
  • 9.2 +
      +
    • 上课
    • +
    • Leetcode 101 练习(第七章第一部分)
    • +
    • 开发自动获取空教室的程序,在服务器上测试部署成功
    • +
    • 课程内容复习
    • +
    +
  • +
+

周总结(第2教学周):

+
    +
  1. 申请了两台服务器,尝试着搭建VPN等,没有搭建好。
  2. +
  3. Leetcode 101 练习(2、3、4、5、6、7)章,进度不错。
  4. +
  5. 上课+课程内容复习,由于课程内容都是概述性的,难度不大,整理笔记并复习完成
  6. +
  7. 坚持跳绳,去体育馆打了一次乒乓球
  8. +
  9. 听了一节科技前沿讲座
  10. +
  11. 完成了卜东波算法课oj六道题,比较简单
  12. +
  13. 开发自动获取空教室的程序并在服务器上测试部署成功,是有了服务器后的第2次尝试,比较简陋,后面尝试完善。
  14. +
+
2022年9月3日-2022年9月9日
+
    +
  • 9.3 +
      +
    • 复习+预习机器学习课程内容
    • +
    • 构思行为金融学论文结构
    • +
    • Leetcode 101 练习(第七章第二部分)
    • +
    +
  • +
  • 9.4 +
      +
    • 科技前沿讲座
    • +
    • 预习课程内容
    • +
    • Leetcode 101 练习(第七章第三部分)
    • +
    +
  • +
  • 9.5 +
      +
    • 上课+预习下一课
    • +
    • Leetcode 101 练习(第七章第四部分+第八章)
    • +
    • 科技前沿讲座
    • +
    • 玩一下Hax服务器
    • +
    +
  • +
  • 9.6 +
      +
    • 预习模式识别与机器学习课程,花费了大量的精力
    • +
    • Leetcode 101 练习(第九章)
    • +
    +
  • +
  • 9.7 +
      +
    • Leetcode 101 练习(第十章+第十一章第一部分)
    • +
    • 上课
    • +
    • 课程作业
    • +
    • 查看家庭金融的数据
    • +
    +
  • +
  • 9.8 +
      +
    • 预习信息检索,完成大概全部课程一半内容
    • +
    • 上课
    • +
    +
  • +
  • 9.9 +
      +
    • 上课
    • +
    • 听科技前沿讲座
    • +
    • 整理网上的课程资料
    • +
    • Leetcode 101 练习(第十章+第十一章第二部分)
    • +
    +
  • +
+

周总结(第3教学周):

+
    +
  1. Leetcode 101 练习(7、8、9、10)章,进度尚可。
  2. +
  3. 确定了选课,确定了全部课程的作业及考核要求,下周会重点准备
  4. +
  5. 上课+课程内容复习,由于课程内容难度比较大,重点整理了笔记
  6. +
  7. 在中特和自然辩证法课堂上预习后面的课程内容,效率比较高
  8. +
  9. 听了一大波科技前沿讲座,基本拿下这一个学分
  10. +
+
2022年9月10日-2022年9月16日
+
    +
  • 9.10 +
      +
    • Leetcode 101 练习(第十一章第三部分)
    • +
    • 浏览、思考中特和自然辩证法的论文
    • +
    • 打乒乓球、休息
    • +
    +
  • +
  • 9.11 +
      +
    • 预习模式识别课程
    • +
    • Leetcode 101 练习(第十一章第四部分)
    • +
    • 确定中特和自然辩证法的论文题目
    • +
    +
  • +
  • 9.12 +
      +
    • 预习模式识别课程,第3讲预习完成,但是一些算法细节不是很清晰
    • +
    • Leetcode 101 练习(第十一章第五部分)
    • +
    • 打乒乓球,参加趣味运动会
    • +
    +
  • +
  • 9.13 +
      +
    • 预习模式识别课程,基本预习完成,作业还剩最后一题
    • +
    • Leetcode 101 练习(第十二章)
    • +
    • 高级人工智能复习+预习
    • +
    +
  • +
  • 9.14 +
      +
    • 上课
    • +
    • 模式识别第三章作业完成
    • +
    • Leetcode 101 练习(第十三章)
    • +
    +
  • +
  • 9.15 +
      +
    • 上课
    • +
    • 预习高级人工智能和机器学习
    • +
    • 预习信息检索(已经大致预习完整一遍)
    • +
    +
  • +
  • 9.16 +
      +
    • 上课
    • +
    • Leetcode 101 练习(第十四章第一部分)
    • +
    • 预习机器学习
    • +
    +
  • +
+

周总结(第4教学周):

+
    +
  1. Leetcode 101 练习(11、12、13)章,进度偏慢。
  2. +
  3. 课程的作业及考核要求准备不多,下周会继续推进。
  4. +
  5. 上课+课程内容复习,由于课程内容难度比较大,目前课程的预习占用的时间过长,后面要尽量缩短
  6. +
  7. 基本可以结束第一阶段的刷题任务,后面转为常态化刷题,下周重点看机器学习竞赛的书籍,争取对竞赛流程有一个比较完整的了解。
  8. +
+
2022年9月17日-2022年9月23日
+
    +
  • 9.17 +
      +
    • 参加开学典礼
    • +
    • Leetcode 101 全部完成一遍(8.27-9.17)完成目标
    • +
    • 休息
    • +
    +
  • +
  • 9.18 +
      +
    • 信息检索大作业数据看懂
    • +
    • 模式识别预习第四章
    • +
    • 打球、休息
    • +
    +
  • +
  • 9.19 +
      +
    • 上课
    • +
    • 模式识别第四章预习完成,作业完成
    • +
    • 信息检索作业完成
    • +
    • 机器学习算法竞赛实战(第1、2章和第3章一部分)
    • +
    +
  • +
  • 9.20 +
      +
    • 机器学习算法竞赛实战(第3、4、5、6章)
    • +
    • 完善空教室发送程序(每一天中午发送第二天课表)
    • +
    +
  • +
  • 9.21 +
      +
    • 预习机器学习
    • +
    • 上课
    • +
    • 机器学习算法竞赛实战(第7章)
    • +
    +
  • +
  • 9.22 +
      +
    • 预习高级人工智能、机器学习
    • +
    • 上课
    • +
    +
  • +
  • 9.23 +
      +
    • 上课
    • +
    • 将文献管理软件从Mendeley转为Zotero,并开启了坚果云作为webdev。由于不太熟悉,耽误了一些时间。
    • +
    • 预习
    • +
    • 数据科学导论第一次作业完成
    • +
    +
  • +
+

周总结(第5教学周):

+
    +
  1. 正常上课,除高级人工智能外都能比较好的理解
  2. +
  3. 机器学习算法竞赛实战书籍完成前面的基础部分
  4. +
  5. 除高级人工智能外,其余课程的作业及大作业都已经发布,推进不多,要抽时间出来看
  6. +
  7. 一周没有碰算法题目,下周开始应该转入“常态化”刷题阶段,从比较重要的知识点专题开始,不能荒废掉。
  8. +
+
2022年9月24日-2022年9月30日
+
    +
  • 9.24 +
      +
    • 机器学习算法竞赛实战(第8、9章)
    • +
    • 信息检索大作业调研,确定下一步的方向
    • +
    • 休息
    • +
    +
  • +
  • 9.25 +
      +
    • 信息检索大作业取得初步成果
    • +
    • 打乒乓球,休息
    • +
    +
  • +
  • 9.26 +
      +
    • 上课
    • +
    • 机器学习算法竞赛实战(第10章)
    • +
    • 信息检索大作业
    • +
    +
  • +
  • 9.27 +
      +
    • 预习机器学习
    • +
    • 信息检索大作业
    • +
    +
  • +
  • 9.28 +
      +
    • 上课
    • +
    • 信息检索大作业
    • +
    • 炒股
    • +
    • 机器学习算法竞赛实战(第12、15章)
    • +
    +
  • +
  • 9.29 +
      +
    • 上课
    • +
    • 完成自然辩证法和新中特论文稿件
    • +
    • 提交目前已有的全部作业
    • +
    +
  • +
  • 9.30 +
      +
    • 上课
    • +
    • 炒股
    • +
    • 机器学习预习完成
    • +
    +
  • +
+

周总结(第6教学周):

+
    +
  1. 分手,影响非常大,睡不好觉,吃不好饭,学习也没有动力
  2. +
  3. 信息检索大作业跑通了目前最好的工具,但是效果并不好,还需要继续寻找原因
  4. +
  5. 机器学习算法竞赛实战的实战篇有点看不下去,先看完了所有的讲解部分,后面再专攻实战部分
  6. +
  7. 机器学习预习完成,大概熟悉了全部的内容
  8. +
  9. 完成自然辩证法和新中特的论文稿件,再改一版就可以完成作业了
  10. +
  11. 《行为金融学》的炒股取得了初步的成果,排名应该很高了
  12. +
+

月度完成情况总结

+
    +
  1. 上课+课程内容复习,基本完成度尚可,最后一周分手影响比较大,后面的信息检索内容理解的不太好
  2. +
  3. 科技前沿讲座听完,拿下学分
  4. +
  5. Leetcode 101 练习完成,基本上全部过了一遍,差不多找到了当时的感觉
  6. +
  7. 《机器学习算法竞赛实战》将理论部分看完,实战部分暂时还是不太好懂
  8. +
  9. 课程作业全部按时完成,准备了一些最后的大作业、考核论文等资料
  10. +
  11. 信息检索和机器学习预习完成,其他课程跟上了进度
  12. +
  13. 两台小服务器也是比较好申请的,目前还没必要使用。在服务器上写了两个脚本,然后用Github Action也跑通了
  14. +
  15. 坚持每周至少打一次乒乓球
  16. +
  17. 体验了炒股,目前的收益不错
  18. +
+

2022年10月1日-2022年10月30日

+

计划安排

+
+

1. 面试算法题重做(重点代码随想录),加深理解,记录笔记

+
+
+

2. 确定研究方向和实习面试岗位方向,首先确定到底是算法还是开发

+
+
+

3. 上课+预习+各种大作业+复习,拒绝耽误课后大量时间

+
+
+

4. 竞赛初步尝试(练习为重点)

+
+

实施情况

+
2022年10月1日-2022年10月7日
+
    +
  • 10.1 +
      +
    • 爬长城
    • +
    • 玩桌游
    • +
    +
  • +
  • 10.2 +
      +
    • 新中特论文基本写完
    • +
    • 思考行为金融学论文
    • +
    +
  • +
  • 10.3 +
      +
    • 行为金融学论文完成一半
    • +
    • Go语言入门
    • +
    +
  • +
  • 10.4 +
      +
    • 见初中同学
    • +
    • 打乒乓球
    • +
    +
  • +
  • 10.5 +
      +
    • Go语言入门
    • +
    • 基本完成行为金融学论文
    • +
    +
  • +
  • 10.6 +
      +
    • Go语言入门
    • +
    • 完成行为金融学论文和自然辩证法论文
    • +
    • 上课
    • +
    • 思考自己
    • +
    +
  • +
  • 10.7 +
      +
    • Go语言网上教程看了一遍
    • +
    • 上课
    • +
    • 代码随想录-动态规划专题基础问题(7道题)
    • +
    +
  • +
+

周总结(第7教学周):

+
    +
  1. 十一假期,渐渐从分手中走出来,见以前的老同学,开始努力学习
  2. +
  3. 完成行为金融学、自然辩证法和新中特的论文
  4. +
  5. Go语言开始入门学习,基本掌握了比较基础的用法,后面的一些高级功能还要继续研究
  6. +
  7. 开始常态化刷题阶段,从动态规划开始入手
  8. +
+
2022年10月8日-2022年10月15日
+
    +
  • 10.8 +
      +
    • 设置好了WSL2内部的代理
    • +
    • 代码随想录-01背包问题(4道题)
    • +
    • 自然辩证法论文和新中特论文添加参考文献
    • +
    • 确定高级人工智能大作业选题
    • +
    • 乒乓球、休息
    • +
    • 自然辩证法汇报总结完成
    • +
    +
  • +
  • 10.9 +
      +
    • 高级人工智能大作业跑了baseline出来
    • +
    • 代码随想录-完全背包问题(6道题)+股票买卖问题(1.5道题)
    • +
    • 打印处理好自然辩证法论文
    • +
    • 迷茫,各种搜索,各种emo
    • +
    • Go圣经第一章阅读练习
    • +
    +
  • +
  • 10.10 +
      +
    • 代码随想录-股票买卖问题(4.5道题)、子序列问题(3道题)
    • +
    • 上课
    • +
    • 拿到GPU,2080Ti*4,copy代码搭建好环境
    • +
    • Go圣经第二、三章阅读
    • +
    +
  • +
  • 10.11 +
      +
    • Go圣经第四章阅读
    • +
    • 代码随想录-子序列问题(9道题)(动态规划专题结束)
    • +
    • 跑通多分类Bert代码
    • +
    +
  • +
  • 10.12 +
      +
    • 完善多分类Bert代码并实际跑通
    • +
    • 上课
    • +
    • 模式识别第五章作业
    • +
    +
  • +
  • 10.13 +
      +
    • 完善多分类Bert代码,模板应该基本没问题了,最高性能能达到0.56
    • +
    • 上课
    • +
    • 代码随想录-数组问题(4道题)
    • +
    • 整理模式识别与机器学习上节课内容
    • +
    +
  • +
  • 10.14 +
      +
    • 代码随想录-数组问题完成
    • +
    • 信息检索第二次作业完成
    • +
    • 编写抢羽毛球场的脚本
    • +
    • 上课
    • +
    +
  • +
  • 10.15 +
      +
    • 修改信息检索作业并提交
    • +
    • 尝试多分类的trick(FGM、PGD、EMA)
    • +
    • 打乒乓球、休息
    • +
    +
  • +
+

周总结(第8教学周):

+
    +
  1. 开启常态化刷题阶段,完成动态规划部分和数组部分,刷过的题目基本都弄明白了
  2. +
  3. 高级人工智能大作业,跑通多分类代码并添加各种trick,目前稳步推进
  4. +
  5. 后端Go学习,感觉学的比较迷茫,没有在正确的学习道路上
  6. +
  7. 上课、完成作业
  8. +
  9. 自然辩证法和新中特基本完成
  10. +
  11. 其他杂活,设置的WSL2的内部代理,编写了抢羽毛球场的脚本等
  12. +
  13. 打乒乓球,遇到了高手,每周要坚持运动
  14. +
+
2022年10月16日-2022年10月23日
+
    +
  • 10.16 +
      +
    • 提交了伪标签和RDrop的数据文件,伪标签达到了0.60286913的得分,目前排名39
    • +
    • 代码随想录链表专题,7道题完成,完成度一般
    • +
    • 运行一些baseline,作为后面优化模型的依据
    • +
    • 打羽毛球
    • +
    +
  • +
  • 10.17 +
      +
    • 行为金融学买了15万股票,尝试多赚一些
    • +
    • 上课
    • +
    • 代码随想录哈希表专题,9道题完成,完成度一般
    • +
    • 完善多分类代码
    • +
    +
  • +
  • 10.18 +
      +
    • 卜东波算法课动态规划oj完成
    • +
    • 完善多分类代码,添加目前已知的全部trick
    • +
    • 代码随想录字符串专题7道题,最大的收获是弄懂了KMP算法
    • +
    • MixText初步尝试
    • +
    +
  • +
  • 10.19 +
      +
    • 小样本多分类开始整体实验,开始调节参数
    • +
    • 上课
    • +
    • 模式识别作业完成一半
    • +
    • 整理KMP算法
    • +
    • 彻底完成新中特的论文
    • +
    +
  • +
  • 10.20 +
      +
    • 小样本多分类跑出了单模型0.59的效果
    • +
    • 上课
    • +
    • 完成模式识别作业第六章作业
    • +
    • 代码随想录双指针专题和栈队列专题完成,总共8道题
    • +
    +
  • +
  • 10.21 +
      +
    • 上课
    • +
    • 代码随想录单调栈完成,总共6道题
    • +
    • 研究最新的mix方法,跑实验,翻译语料
    • +
    +
  • +
  • 10.22 +
      +
    • 上课
    • +
    • 跑实验,翻译语料
    • +
    • 理发、休息
    • +
    +
  • +
  • 10.23 +
      +
    • 研究生成式模型T5系列
    • +
    • 高级人工智能大作业打到第6名
    • +
    +
  • +
+

周总结(第9教学周):

+
    +
  1. 前半周代码随想录进度不错,链表、哈希表、字符串、栈队列、双指针、单调栈等等。确实对这种代码熟悉多了。
  2. +
  3. 高级人工智能大作业进展比较顺利,目前已经到第6名,也进行了一些其他的研究,但是能力有限没有什么进展。
  4. +
  5. 正常上课,但是感觉上课的知识没有完全弄明白,课后也不太想看了
  6. +
  7. 模式识别作业比较多,耗费的时间比较长,还好应该快结束了
  8. +
  9. 下周真的真的要开始学习后端了
  10. +
+
2022年10月24日-2022年10月30日
+
    +
  • 10.24 +
      +
    • 上课
    • +
    • 英文模型跑通
    • +
    • GPU 要回来了
    • +
    • 模式识别第七章作业完成
    • +
    +
  • +
  • 10.25 +
      +
    • 跑模型
    • +
    • 开始系统学习Go,以视频教程为基础,练习为主导。今天看完123章
    • +
    +
  • +
  • 10.26 +
      +
    • 跑模型
    • +
    • 上课
    • +
    • 看完45章go的视频
    • +
    +
  • +
  • 10.27 +
      +
    • 跑模型
    • +
    • 上课
    • +
    • 代码随想录贪心算法专题6道题,完成不太好
    • +
    +
  • +
  • 10.28 +
      +
    • 跑模型
    • +
    • 完成信息检索作业
    • +
    • 整理高级人工智能大作业代码
    • +
    • 讨论下一步的方向
    • +
    +
  • +
  • 10.29 +
      +
    • 跑模型
    • +
    • 整理代码
    • +
    • 看完第6章go的视频
    • +
    +
  • +
  • 10.30 +
      +
    • 跑模型,没有什么进展
    • +
    • 看完第7、8、9章go的视频
    • +
    +
  • +
+

周总结(第10教学周):

+
    +
  1. 上课,效果不太好,而且缺乏课后的复习,按时完成课后的作业
  2. +
  3. 开始学习Go,找了一套比较完整的视频。目前有了比较全的大概了解,但是缺乏实践。
  4. +
  5. 跑模型,但是没有什么进展,需要更换方法、更换思路
  6. +
  7. 代码随想录继续断断续续做着,做的不太好
  8. +
+

月度完成情况总结

+
    +
  1. 开启常态化刷题阶段,目前完成了代码随想录的动态规划大专题和数组、链表、哈希表、字符串、栈队列、双指针、单调栈等等一些小部分,开始做贪心算法部分,前面的还不错,后面有一点急躁,完成度不够好,还是要静下心来认真完成练习
  2. +
  3. 后端Go学习,有一些迷茫,刚开始看的比较高级的教程,然后发现看不太懂,现在找了一套比较详细的视频,希望可以尽快看懂上手。
  4. +
  5. 正常上课,但是上课有些懈怠,而且作业比较多,上过课后不太想复习,感觉上的课没有什么作用
  6. +
  7. 完成行为金融学、自然辩证法和新中特的论文,但是由于股票亏得太多,行为金融学的论文还是要重新写一下。三门课也已经或很快结课,自己学习的时间更多了。
  8. +
  9. 高级人工智能的大作业比预想中要好很多,但是后劲不足,现在已经没有什么办法了,可惜我又没有太多的时间去研究一些更好的方法
  10. +
  11. 多找人交流目前的想法,感觉好迷茫,有点不敢找那些应该对我有帮助的人。要克服社恐。
  12. +
  13. 坚持运动,坚持减肥,挺直腰杆,尽量不咬手指。
  14. +
+

2022年10月31日-2022年12月2日

+

计划安排

+
+

1. 面试算法题重做,加深理解,记录笔记,重点代码随想录一定要完成

+
+
+

2. 确定研究方向和实习面试岗位方向,多问问人,问问同龄人和学长学姐,克服社恐

+
+
+

3. 上课+复习+考试,应该只有信息检索(尽量考好一点)

+
+
+

4. 小样本分类竞赛,再多跑一跑,争取进前十

+
+
+

5. Go后端学习,不仅仅是Go这一门语言,对整个后端的知识要有体系,以面试为目的进行准备

+
+

实施情况

+
2022年10月31日-2022年11月6日
+
    +
  • 10.31 +
      +
    • 早上收结果跑代码
    • +
    • 上课
    • +
    • Go视频看完第10章,11章看完几乎一半
    • +
    +
  • +
  • 11.1 +
      +
    • 早上和上午收结果跑代码
    • +
    • Go视频看完第11章
    • +
    • IR大作业使用预训练模型跑通,效果非常好,但是应该是不能直接使用的
    • +
    +
  • +
  • 11.2 +
      +
    • 早上和上午收结果跑代码,终于突破0.63
    • +
    • Go视频看完第12章,做完了第一个小项目,比较简单
    • +
    • 上课
    • +
    • 晚上要到GPU继续跑代码
    • +
    +
  • +
  • 11.3 +
      +
    • 早上和上午收结果跑代码,没有进展
    • +
    • Go的第二个项目半独立完成,对于面向对象有点不太熟悉,不知道如何将代码分配到不同的文件中,后面根据视频再好好想一想
    • +
    • 上课
    • +
    • 晚上继续收结果提交
    • +
    +
  • +
  • 11.4 +
      +
    • 收取全部好模型的softmax,确定了半精度多模型集成的路线,正在进行最后实验
    • +
    • 上课
    • +
    • 完成数据科学导论的作业
    • +
    • 更换VPN服务器
    • +
    +
  • +
  • 11.5 +
      +
    • 早上去提交文件,跑新的伪标签的模型,突破0.64
    • +
    • 上山送水,打乒乓球,休息
    • +
    • 晚上收代码结果
    • +
    +
  • +
  • 11.6 +
      +
    • 早上提交文件,跑模型
    • +
    • 中午去聚餐
    • +
    • 复习《现代信息检索》,复习到第五章
    • +
    +
  • +
+

周总结(第11教学周):

+
    +
  1. 上课,信息检索课程即将结束考试,周日开始复习,高级人工智能听懂
  2. +
  3. 早起晚睡跑模型收结果,最后一周进行冲刺
  4. +
  5. Go的视频看到第12章,完成了两个比较简单的小项目,对于项目的组织流程不是很清晰,后面需要慢慢练习
  6. +
  7. 信息检索大作业终于跑通,效果比较好但是不能直接用,后面还是需要自己写代码
  8. +
  9. 完成数据科学导论的作业,更换了一个VPN服务器,后面再更换应该更容易了
  10. +
  11. 爬山送水,打乒乓球,聚餐休息
  12. +
+
2022年11月7日-2022年11月13日
+
    +
  • 11.7 +
      +
    • 早上收结果,最后一天冲刺
    • +
    • 上课
    • +
    • 下午复习信息检索,复习到第9章
    • +
    • Go视频看完第14章,文件和json部分
    • +
    +
  • +
  • 11.8 +
      +
    • 高级人工智能大作业完成最终数据的推理,明天提交后后天凌晨就可以知道结果了
    • +
    • 下午复习信息检索,基本捋完一遍,有的地方不是很明白,要看明天上课的情况再复习
    • +
    • Go视频看完第15章,16章看完大部分,但是有一点点不理解
    • +
    +
  • +
  • 11.9 +
      +
    • 上课
    • +
    • 信息检索测试和推理代码流程跑通
    • +
    • 信息检索总体复习
    • +
    • Go视频看完第16章
    • +
    +
  • +
  • 11.10 +
      +
    • Go视频看完第17章
    • +
    • 研究python终端美化的rich库,没有研究很明白
    • +
    • 信息检索重点内容复习完成
    • +
    • 上课,记笔记
    • +
    +
  • +
  • 11.11 +
      +
    • Go视频看完第17章,进入到最终的大项目和数据库部分
    • +
    • 信息检索题目做完,前阶段复习完成,明天开始进入后阶段复习
    • +
    • 上课
    • +
    +
  • +
  • 11.12 +
      +
    • 比赛进入复现阶段,重新走流程训练模型
    • +
    • 学习docker并打包
    • +
    • 打乒乓球,休息
    • +
    +
  • +
  • 11.13 +
      +
    • 完成模型的训练等流程,整理伪标签数据文件
    • +
    • 复习信息检索
    • +
    • Go视频知识部分结束
    • +
    +
  • +
+

周总结(第12教学周):

+
    +
  1. 小样本数据分类任务竞赛阶段完成,本来以为没戏了,又进了决赛(可能),正在复现代码流程
  2. +
  3. Go视频基本完成,最后的大项目还没开始
  4. +
  5. 复习信息检索,准备考试
  6. +
  7. 信息检索大作业的流程和代码跑通,但是效果不太好,正在找原因
  8. +
  9. 正常上课,但是还是只有高级人工智能的上课效果比较好,信息检索快考试了也有点听不进去
  10. +
  11. 要抓紧了,只剩一个半月的时间就要过年了,抓紧每一天,放松就放松,学习就学习,尽量让效率提升起来。
  12. +
+
2022年11月14日-2022年11月20日
+
    +
  • 11.14 +
      +
    • 信息检索考试
    • +
    • 提交竞赛的复现材料
    • +
    • 去一食堂三楼吃饭,打乒乓球
    • +
    +
  • +
  • 11.15 +
      +
    • Go最后项目视频,有些难度,进展不多
    • +
    • 整理竞赛代码并上传
    • +
    • 为邮箱生成了二次验证码
    • +
    • 研究K折不独立的问题
    • +
    +
  • +
  • 11.16 +
      +
    • 收取V100上的代码,删除全部不需要的代码
    • +
    • 上课
    • +
    • 完成数据科学导论的报告,应该不需要二次返工
    • +
    • Go最后项目视频继续学习
    • +
    +
  • +
  • 11.17 +
      +
    • 更新VPN服务器和免费订阅机场,估计可以稳了
    • +
    • Go最后项目视频继续学习
    • +
    • 上课,记笔记
    • +
    • 修改论文初稿
    • +
    +
  • +
  • 11.18 +
      +
    • Go最后项目视频继续学习
    • +
    • 上课
    • +
    • 修改论文,制作PPT图片
    • +
    +
  • +
  • 11.19 +
      +
    • 修改PPT和论文,制作讲稿,已经完成
    • +
    • Go最后项目学完做完
    • +
    • 打乒乓球,休息
    • +
    • 研究信息检索大作业
    • +
    +
  • +
  • 11.20 +
      +
    • 信息检索大作业实验基本完成,任务已经分配下去
    • +
    • 打羽毛球,休息
    • +
    +
  • +
+

周总结(第13教学周):

+
    +
  1. 考了第一个考试,考的感觉应该还可以,就是记忆的东西还是记得不太熟练,分数应该不差,也并没有耗费太多的时间复习
  2. +
  3. Go的项目做完,但是整体的架构还是不太会,语法上感觉还可以,类对象结构体什么的还是有点懵
  4. +
  5. 整理竞赛的代码,制作竞赛的论文和PPT,全部完成,做的应该还不错
  6. +
  7. 上课,没有什么作业
  8. +
  9. 完成了数据科学导论的读书报告,基本完成了信息检索的实验,并把信息检索的分工布置下去。
  10. +
  11. 打乒乓球和羽毛球,休息
  12. +
+
2022年11月21日-2022年11月27日
+
    +
  • 11.21 +
      +
    • 信息检索又跑了一波warmup的实验,编写了集成的代码,再做一个集成的实验就不做了
    • +
    • 催机器学习大作业的分工
    • +
    • 完成行为金融学的论文和数据科学导论的作业并提交,将数据科学导论的读书报告转换为PDF
    • +
    • 重新看了一遍Go的基础知识,开始做一些有难度的项目
    • +
    +
  • +
  • 11.22 +
      +
    • 第一个7天项目做完,开始还好,后面就有点看不懂了,有时间还要多看
    • +
    • 看了一些计算机网络的基础知识
    • +
    • 借着前面用的docker把流程重新走一遍
    • +
    +
  • +
  • 11.23 +
      +
    • 整理信息检索大作业的代码
    • +
    • 上课
    • +
    • 收拾寝室卫生
    • +
    +
  • +
  • 11.24 +
      +
    • 研究机器学习的大作业并搭建环境把代码跑起来
    • +
    • 上课
    • +
    +
  • +
  • 11.25 +
      +
    • 跑机器学习大作业的代码实验,预计要明天才能收取结果。
    • +
    • 完成机器学习大作业自己部分的写作
    • +
    • 整理机器学习的复习题
    • +
    • 上课
    • +
    +
  • +
  • 11.26 +
      +
    • 小样本分类任务竞赛答辩
    • +
    • 信息检索大作业报告基本完成
    • +
    • 打乒乓球,休息
    • +
    +
  • +
  • 11.27 +
      +
    • 信息检索大作业报告彻底完成,全部的大作业基本完成
    • +
    • 代码随想录-贪心算法完成,共12道题
    • +
    • 预习高级人工智能强化学习内容,复习概率图模型内容
    • +
    +
  • +
+

周总结(第14教学周):

+
    +
  1. 将全部的大作业完成并基本提交,后续基本不会再有作业的困扰了,只专注于复习就可以了
  2. +
  3. Go做了一个小小的网络框架项目,后面理解的有点不太好,感觉代码变多了就有点难读懂
  4. +
  5. 高级人工智能的大作业,也是竞赛的答辩完成,还不知道成绩,但是已经很不错了
  6. +
  7. 重启算法练习,效果还可以,敲起代码还是比较顺手的
  8. +
  9. 开始找八股文,先看了一点计算机网络
  10. +
+
2022年11月28日-2022年12月2日
+
    +
  • 11.28 +
      +
    • 代码随想录-二叉树算法15道题完成
    • +
    • 收拾东西准备回家
    • +
    +
  • +
  • 11.29 +
      +
    • 回家
    • +
    +
  • +
  • 11.30 +
      +
    • 回家休息
    • +
    • 上课
    • +
    +
  • +
  • 12.1 +
      +
    • 上课
    • +
    • 代码随想录-二叉树专题6道题
    • +
    +
  • +
  • 12.2 +
      +
    • 上课
    • +
    • 代码随想录-二叉树专题15道题
    • +
    +
  • +
+

周总结(第15教学周):

+
    +
  1. 上周开始重启算法练习,本周完成二叉树专题
  2. +
  3. 收拾东西回家,无心学习,无心上课,效率比较低
  4. +
+

月度完成情况总结

+
    +
  1. 完成小样本数据分类任务竞赛,以候补第5名的身份进入总决赛,同时学会了docker的部署使用。写论文制作PPT并答辩完成,还不知道成绩,估计最终也是第五名
  2. +
  3. 信息检索课程从复习到考试完成,并没有耽误特别长的时间,考的应该还可以,原题确实比较多
  4. +
  5. 完成所有的作业、大作业和课程论文等提交文件,后续就不会有这种东西占用时间了
  6. +
  7. Go视频看完,做了两个小项目和两个大项目,感觉没有架构体系的思想,代码有时候不知道应该怎么写
  8. +
  9. 正常上课,但是现在的内容都比较难,半懂不懂
  10. +
  11. 重启算法练习,完成二叉树专题,还有一个回溯算法的专题就将代码随想录完成一遍了
  12. +
  13. 找八股文,开始看一些计算机网络相关的内容
  14. +
  15. 在校坚持打乒乓球运动,换了几台VPN服务器,现在已经很熟练了,最后一周回家,本学期结束肯定在家中度过了。
  16. +
+

2022年12月3日-2023年1月1日

+

计划安排

+
+

1. 完成代码随想录,回顾前面做过的题目,做一些没有在讲解中的题目检验学习成果

+
+
+

2. 上课+复习+考试,不要耽误太长时间,但是要稳步推进复习

+
+
+

3. 后端知识学习,学习八股文的资料,对整个后端的知识要有体系,以面试为目的进行准备

+
+
+

4. 实习面试,克服恐惧,迎难而上

+
+

实施情况

+
2022年12月3日-2022年12月10日
+
    +
  • 12.3 +
      +
    • 代码随想录-回溯算法专题15道题
    • +
    • 修改完善机器学习大作业的论文
    • +
    +
  • +
  • 12.4 +
      +
    • 学习后端知识,以计算机网络和Redis为主
    • +
    +
  • +
  • 12.5 +
      +
    • 开始做分布式缓存的项目
    • +
    • 学习后端知识,以MySQL为主
    • +
    +
  • +
  • 12.6 +
      +
    • 大概完成分布式缓存的项目
    • +
    • 开始整理网上能搜集到的面试题
    • +
    • 学习后端知识,包括MySQL和计算机网络
    • +
    +
  • +
  • 12.7 +
      +
    • 上课
    • +
    • 完善简历
    • +
    • 编写面试测试代码
    • +
    • 复习八股文
    • +
    +
  • +
  • 12.8 +
      +
    • 上课
    • +
    • 复习八股文,基本看完
    • +
    • 完善简历
    • +
    • 找实习岗位
    • +
    • 整理面试题目
    • +
    +
  • +
  • 12.9 +
      +
    • 整理面试八股文
    • +
    • 复习go代码知识
    • +
    • 投实习
    • +
    • 复习操作系统和分布式相关
    • +
    +
  • +
  • 12.10 +
      +
    • 整理mysql和redis相关面试题目
    • +
    +
  • +
+

周总结(第16教学周):

+
    +
  1. 回家后效率降低,正常上课
  2. +
  3. 复习各种面试资料,差不多已经成体系了
  4. +
  5. 修改完善机器学习大作业的论文,大作业彻底结束
  6. +
  7. 投了十家单位的实习,目前还没有任何反馈
  8. +
  9. 完成代码随想录的算法题,尝试了一些SQL的题目
  10. +
+
2022年12月11日-2022年12月18日
+
    +
  • 12.11 +
      +
    • 高级人工智能的格子游戏问题
    • +
    • 上课
    • +
    • 整理计算机网络相关的面试题
    • +
    +
  • +
  • 12.12 +
      +
    • 高级人工智能简答题整理
    • +
    • 复习八股文
    • +
    • 整理模式识别的考题
    • +
    • 复习go资料
    • +
    +
  • +
  • 12.13 +
      +
    • 模式识别与机器学习复习:SVM部分和感知器部分
    • +
    • 准备开始学习6.824分布式系统MIT课程
    • +
    +
  • +
  • 12.14 +
      +
    • 模式识别KL变换和贝叶斯题目
    • +
    • 上课
    • +
    • 6.824 mapreduce论文阅读
    • +
    +
  • +
  • 12.15 +
      +
    • 模式识别概率图模型题目
    • +
    • 上课
    • +
    • 6.824 看完 Lec 1 视频
    • +
    • 初步整理高级人工智能最终提交材料
    • +
    +
  • +
  • 12.16 +
      +
    • 模式识别概率图条件独立性题目
    • +
    • 整理高级人工智能材料及开源代码
    • +
    • 上课
    • +
    • 整理互联网公司
    • +
    • Go Tour 基本弄懂
    • +
    +
  • +
  • 12.17 +
      +
    • 6.824 看完 Lec 2 视频
    • +
    • 初步弄懂6.824 Lab 1 内容
    • +
    • 阅读 6.824 Lec 3 GFS 论文
    • +
    +
  • +
  • 12.18 +
      +
    • 正态密度分布贝叶斯习题
    • +
    • 阅读 6.824 Lec 3 GFS 论文
    • +
    • 6.824 看完 Lec 3 视频
    • +
    +
  • +
+

周总结(第17教学周):

+
    +
  1. 回家后效率确实低,总有分心的东西,下周要准备面试了,要抓紧一些了
  2. +
  3. 6.824 完成了LEC 1 2 3,讲得确实不错,看了实验的要求,比较多但也基本弄懂,下周开始正式干
  4. +
  5. 上课,没有怎么听,讲的东西也都没什么意思,考试涉及的也比较少
  6. +
  7. 复习高级人工智能和模式识别,模式识别将计算题基本弄懂
  8. +
+
2022年12月19日-2022年12月25日
+
    +
  • 12.19 +
      +
    • 模式识别偏差方差和过拟合欠拟合问题
    • +
    • 初步实现 6.824 的 Lab 1 Map
    • +
    +
  • +
  • 12.20 +
      +
    • 初步实现 6.824 的 Lab 1 Reduce
    • +
    • 收到小红书的约面,要准备整理了
    • +
    +
  • +
  • 12.21 +
      +
    • 基本实现 6.824 的 Lab 1
    • +
    • 上课
    • +
    +
  • +
  • 12.22 +
      +
    • 复习机器学习复习题的单选题
    • +
    • 上课
    • +
    +
  • +
  • 12.23 +
      +
    • 复习机器学习复习题的多选题
    • +
    • 上课
    • +
    • 复习go并发代码写法
    • +
    • 复习算法题,链表还是不太行
    • +
    +
  • +
  • 12.24 +
      +
    • 做一些算法的大模拟题目
    • +
    • 复习八股相关
    • +
    • 大致浏览机器学习大题
    • +
    +
  • +
  • 12.25 +
      +
    • 准备实习面试
    • +
    +
  • +
+

周总结(第18教学周):

+
    +
  1. 基本完成6.824的Lab,容错机制还没有写好
  2. +
  3. 收到约面并准备实习面试
  4. +
  5. 上课,复习考试相关内容,复习的不多
  6. +
+
2022年12月26日-2023年1月1日
+
    +
  • 12.26 +
      +
    • 实习面试及复盘,添加操作系统面试准备题目
    • +
    +
  • +
  • 12.27 +
      +
    • 总结模式识别后面几章的考试重点
    • +
    +
  • +
  • 12.28 +
      +
    • 总结模式识别新划的重点
    • +
    • 上课
    • +
    • 复习高级人工智能逻辑相关的内容
    • +
    +
  • +
  • 12.29 +
      +
    • 整理复习模式识别相关内容
    • +
    • 整理复习高级人工智能相关内容
    • +
    • 上高级人工智能的最后一节课
    • +
    +
  • +
  • 12.30 +
      +
    • 上机器学习的最后一节课
    • +
    • 提交信息检索大作业
    • +
    • 获得华为云服务器
    • +
    • 总结高级人工智能选择题的部分
    • +
    • 做机器学习的大题
    • +
    +
  • +
  • 12.31 +
      +
    • 上模式识别的最后一节复习课
    • +
    • 公开信息检索代码
    • +
    +
  • +
  • 1.1 +
      +
    • 复习模式识别
    • +
    +
  • +
+

周总结(第19教学周):

+
    +
  1. 寒假期间的第一次实习面试,面试还可以,但是并没有后续,还是经历之类的不太匹配
  2. +
  3. 上课、总结考试重点、复习
  4. +
+

2023年1月2日-2023年2月21日

+

实施情况

+
2023年1月2日-2023年1月8日
+
    +
  • 1.2 +
      +
    • 复习高级人工智能
    • +
    +
  • +
  • 1.3 +
      +
    • 复习模式识别,整理资料以及完成推到题
    • +
    • 删除机器学习题库的答案
    • +
    +
  • +
  • 1.4 +
      +
    • 机器学习题库选择题
    • +
    • 试考两次
    • +
    • 模式识别整体过一遍
    • +
    • Prolog相关知识阅读整理
    • +
    +
  • +
  • 1.5 +
      +
    • 复习高级人工智能并考试
    • +
    • 过一遍机器学习的PPT
    • +
    +
  • +
  • 1.6 +
      +
    • 复习机器学习选择题
    • +
    • 复习机器学期和模式识别的大题
    • +
    • 做机器学习的下午试卷
    • +
    • 机器学习考试
    • +
    +
  • +
  • 1.7 +
      +
    • 模式识别考试
    • +
    • 字节青训笔试
    • +
    • 整理互联网公司
    • +
    +
  • +
  • 1.8 +
      +
    • 修改密码
    • +
    +
  • +
+

周总结(第20教学周):

+
    +
  1. 复习并考试
  2. +
  3. 参加字节后端青训的笔试,等待结果中
  4. +
  5. 整理各大互联网公司的招聘网站信息
  6. +
  7. 逐步开始修改密码,删除一些用不到的密码
  8. +
+
2022年1月9日-2023年1月15日
+
    +
  • 1.9
  • +
  • 1.10 +
      +
    • 6.824 VM-FT论文阅读,LEC 4 听了一点,Raft论文看了一点
    • +
    • 字节青训笔试通过并组队,假期有的忙了
    • +
    +
  • +
  • 1.11 +
      +
    • 6.824 LEC 4 听完,阅读Raft论文
    • +
    +
  • +
  • 1.12 +
      +
    • Raft论文阅读完成
    • +
    • 字节青训第一次直播,正在看任务细节
    • +
    +
  • +
  • 1.13 +
      +
    • 配置好华为云服务器的后端开发环境
    • +
    • 跑官方的Demo和github上面的一个做好的demo,基本跑通
    • +
    +
  • +
  • 1.14 +
      +
    • 字节青训Day 1 和 2 课程学习
    • +
    +
  • +
  • 1.15 +
      +
    • 爬取全部视频并编写合并音频和视频的代码,备后续学习使用
    • +
    • 准备Day 3的课前准备,学了一点Day 4的内容
    • +
    +
  • +
+
2022年1月16日-2023年1月23日
+
    +
  • 1.16 +
      +
    • 完成视频的转换及上传
    • +
    • 学习完成Day 4的内容
    • +
    +
  • +
  • 1.17 +
      +
    • 第一次直播课并作笔记,重点听了Gorm
    • +
    • 大致浏览Day 5的内容,现在应该作用不大
    • +
    • 抛弃V2ray,转为一元机场的clash,将Windows、Android、Linux全部配置好
    • +
    +
  • +
  • 1.18 +
      +
    • 开始独立开发抖音项目,尝试了视频流的推送模拟,效果尚可,同时逐步搭建起项目的整体框架
    • +
    +
  • +
  • 1.19 +
      +
    • 框架搭建完成,正式开始开发
    • +
    • 见到了真正的微服务,理解更深一层
    • +
    • 将昨天写的单体服务的代码移植到微服务中,可以跑通
    • +
    • 使用viper编写config模块,使用zap编写log模块
    • +
    • 搭建可以远程访问的mysql并创建即将使用到的表
    • +
    • 初步尝试grpc,安装proto并运行成功
    • +
    • 安装consul
    • +
    +
  • +
  • 1.20 +
      +
    • 1.1 Feed 接口全部完成并跑通,目前有slow SQL的问题
    • +
    +
  • +
  • 1.21 +
      +
    • 搭建其他接口的框架
    • +
    • 完成数据库表设计的写入
    • +
    • 完成RabbitMQ的配置
    • +
    +
  • +
  • 1.22 +
      +
    • 完成4个接口
    • +
    • 学习了jwt的token生成与鉴权等操作
    • +
    • 搭建好最后一个上传视频的接口
    • +
    +
  • +
  • 1.23 +
      +
    • 完成所有的接口
    • +
    +
  • +
+
2022年1月24日-2023年1月29日
+
    +
  • 1.24 +
      +
    • 开始写文档,绘制流程图
    • +
    • 注册七牛云OSS
    • +
    +
  • +
  • 1.25 +
      +
    • 重新修正代码中的错误,添加OSS并推送新的版本
    • +
    • 写用户注册登录部分的文档并画图
    • +
    +
  • +
  • 1.26 +
      +
    • 重构代码以支持微服务
    • +
    • 完成所有图片的初稿
    • +
    • 设计异步上传文件的消息队列并跑通demo
    • +
    +
  • +
  • 1.27 +
      +
    • 完成文件异步上传的队列
    • +
    • 尝试搭建consul,没有成功
    • +
    • 继续修改密码
    • +
    +
  • +
  • 1.28 +
      +
    • 学习完成6.824 的LEC 5 6 7
    • +
    • 回顾之前写的MapReduce代码
    • +
    • 参考队友的代码跑通consul
    • +
    +
  • +
  • 1.29 +
      +
    • 修改consul的代码更加规范
    • +
    • 添加gin的jwt中间件代码
    • +
    • 完成6.824 Lab1 的mapreduce的最后部分
    • +
    • 翻译6.824 Lab2 的指示
    • +
    • 看Lab 2A、B、C的代码运行和提示
    • +
    +
  • +
+
2022年1月30日-2023年2月21日
+
    +
  • 1.30 +
      +
    • 完成Lab 2A的一半内容
    • +
    +
  • +
  • 1.31 +
      +
    • 完成Lab2A
    • +
    • Lab2B做了一点点
    • +
    • 翻译完成Lab2D的内容
    • +
    • 更改密码,彻底完成
    • +
    +
  • +
  • 2.1 +
      +
    • 完成Lab 2B
    • +
    • 与队友联调consul和conga,彻底跑通并找到了代码与配置的错误
    • +
    +
  • +
  • 2.2 +
      +
    • 尝试Lab 2C,第一个测试点无法通过,可能是之前的代码的问题,无力解决了
    • +
    • 完成其他服务的整合与并行化操作,基本完成自己负责的抖音项目的部分
    • +
    • 修改文档图片
    • +
    +
  • +
  • 2.3 +
      +
    • 抖音自己负责的部分彻底跑通
    • +
    • 完成总体的文档与图片
    • +
    • 完成Consul和Kong的整理
    • +
    • 实现进程的优雅退出和Consul的注销操作
    • +
    +
  • +
  • 2.4 +
      +
    • 弄懂了条件变量的写法,成功应用到了抖音后端的退出上
    • +
    • 完善简历,找实习目标岗位
    • +
    +
  • +
  • 2.5 +
      +
    • 完善抖音项目与简历
    • +
    +
  • +
  • 2.6 +
      +
    • 投递简历并沟通
    • +
    +
  • +
  • 2.7 +
      +
    • 投递简历并沟通
    • +
    • 复习链表相关算法
    • +
    +
  • +
  • 2.8 +
      +
    • 收到百度的面试邀请
    • +
    • 复习准备面试
    • +
    +
  • +
  • 2.9 +
      +
    • 百度教育一面
    • +
    • metaApp笔试
    • +
    • 收到SmartX的面试邀请
    • +
    +
  • +
  • 2.10 +
      +
    • 收到MetaApp面试邀请
    • +
    • 完善面试知识点-Go和Consul
    • +
    +
  • +
  • 2.11 +
      +
    • 练习树相关算法题目
    • +
    • 完善面试知识点-Redis相关
    • +
    +
  • +
  • 2.12 +
      +
    • 练习双指针相关题目,大致捋一遍剑指Offer
    • +
    • 抖音项目联调、文档框架搭建
    • +
    • 完善面试知识点-MySQL相关
    • +
    +
  • +
  • 2.13 +
      +
    • MetaAPP一面
    • +
    • 找合适的实习岗位
    • +
    • 抖音项目图片文档
    • +
    +
  • +
  • 2.14 +
      +
    • 收到MetaAPP二面邀请
    • +
    • 找合适的实习岗位
    • +
    • SmartX一面
    • +
    • 整理Redis相关的部分和大数据相关的面试题目
    • +
    +
  • +
  • 2.15 +
      +
    • MetaAPP二面
    • +
    • 写抖音项目文档
    • +
    +
  • +
  • 2.16 +
      +
    • 写抖音项目文档
    • +
    +
  • +
  • 2.17 +
      +
    • 写抖音项目文档,基本完成自己的部分
    • +
    • 做抖音的demo视频
    • +
    • 收到第四范式一面邀请
    • +
    • 复习树相关算法题目
    • +
    +
  • +
  • 2.18 +
      +
    • 复习树相关算法题目
    • +
    • 复习链表相关算法题目
    • +
    • 回学校
    • +
    • 收拾学校东西
    • +
    +
  • +
  • 2.19 +
      +
    • 搞定排序相关的算法题以及堆相关的数据结构
    • +
    • 复习并查集,优化面试时候写的代码
    • +
    • 看掘金的技术文章,看到Day12
    • +
    +
  • +
  • 2.20 +
      +
    • 英语慕课第一周任务完成
    • +
    • 看掘金的技术文章,看到Day18
    • +
    • 上课
    • +
    • 收到MetaApp的offer
    • +
    • 完善抖音文档
    • +
    +
  • +
  • 2.21 +
      +
    • MetaApp入职准备
    • +
    • 完善抖音文档
    • +
    • 总结实习投递经历
    • +
    • 整理半年的计划并指定新学期的计划
    • +
    +
  • +
+

计划总结

+

回顾刚上研一定下的计划,看看完成情况:

+
+

1. C++初步了解与练习(2022年8月-2022年9月)

+
+

完成情况★★★

+

C++了解还是不太够,不过算法题使用C++过了一遍

+
+

2. 面试算法题重做,加深理解,记录笔记(2022年9月-2022年10月)

+
+

完成情况★★★★

+

算法题重做了大概两遍,第一遍是C++,第二遍是Go,现在有一些找到方法了

+
+

3. 确定研究方向和实习面试岗位方向(2022年10月-2022年11月)

+
+

完成情况★★★★★

+

确定了目前的目标是Go后端开发相关

+
+

4. 实习面试(2022年12月-2023年1月)

+
+

完成情况★★★★

+

实习面试推荐稍慢,不过最终还是成功找到理想的岗位

+
+

5. 上课+复习+考试(尽量考好一点)(2022年9月-2023年1月)

+
+

完成情况★★★★★

+

考试还不错,在周围人中间也算考的最好的了

+
+

6. 根据本科毕设尝试发论文(2022年9月-2022年10月)

+
+

完成情况★

+

不能找老师,暂时不要在论文上耽误时间,研二再看

+
+

7. 竞赛初步尝试(2022年9月-2023年1月)

+
+

完成情况★★★★★

+

在室友大佬的加持下还拿了一个比较好的奖项,又丰富了算法的简历

+ + +
+ +
+
+ + + + + + +
+
+
学习计划 (2022年8月——2023年1月)
+
https://zhangzhao219.github.io/2022/08/26/zhangzhao-plan-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月26日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/27/Leetcode/Leetcode-101/Leetcode-101-2/index.html b/2022/08/27/Leetcode/Leetcode-101/Leetcode-101-2/index.html new file mode 100644 index 000000000..71e526a69 --- /dev/null +++ b/2022/08/27/Leetcode/Leetcode-101/Leetcode-101-2/index.html @@ -0,0 +1,982 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第2章 贪心算法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第2章 贪心算法

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第2章 贪心算法

+ +

贪心算法

+

贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。

+

分配问题

+

Leetcode 455

+

有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子可以吃饱。

+
class Solution {
+public:
+    int findContentChildren(vector<int>& g, vector<int>& s) {
+        sort(g.begin(),g.end());
+        sort(s.begin(),s.end());
+        int childrenCount = 0;
+        for(size_t i = 0;i<s.size() && childrenCount < g.size();i++){
+            if(s[i] >= g[childrenCount]){
+                ++childrenCount;
+            }
+        }
+        return childrenCount;
+    }
+};
+

分析:用最小大小的饼干 (s)去满足最小饥饿度的孩子 (g),一直满足到饥饿度最大的孩子,相当于双指针的移动。

+

贪心策略是给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。

+

错误:忘记检查g是否越界,可能发生所有饼干都能满足所有孩子,然而饼干还剩着的情况。下标运算一定要确认是否越界。

+

Leetcode 135

+

一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比自己身旁的一个孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少个糖果。

+
class Solution {
+public:
+    int candy(vector<int>& ratings) {
+        vector<int> candyNum(ratings.size(),1);
+        for(size_t i = 0;i<ratings.size()-1;++i){
+            if(ratings[i+1] > ratings[i] && candyNum[i+1] <= candyNum[i]){
+                candyNum[i+1] = candyNum[i] + 1;
+            }
+        }
+        for(size_t i = ratings.size()-1;i>0;--i){
+            if(ratings[i-1] > ratings[i] && candyNum[i-1] <= candyNum[i]){
+                candyNum[i-1] = candyNum[i] + 1;
+            }
+        }
+        return accumulate(candyNum.cbegin(),candyNum.cend(),0);
+    }
+};
+

分析:首先至少有一个糖果分配好,然后从左向右扫一遍,如果右边的孩子评分高,则右边孩子的糖果=左边孩子的糖果+1,再从右往左扫一遍,如果左边的孩子评分高,则左边孩子的糖果=右边孩子的糖果+1。最后求和即可。

+

贪心策略:在每次遍历中,只考虑并更新相邻一侧的大小关系

+

错误:没有更新为相邻孩子+1,而是仅仅加了1,考虑不够完整。

+

区间问题

+

Leetcode 435

+

给定一个区间的集合 intervals ,返回 需要移除区间的最小数量,使剩余区间互不重叠。

+
class Solution {
+public:
+    static bool cmp(vector<int> &a,vector<int> &b){
+        return a[1] < b[1];
+    }
+    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
+        sort(intervals.begin(),intervals.end(),cmp);
+        int intervalsCount = 1;
+        int searchBack = intervals[0][1];
+        size_t n = intervals.size();
+        for(size_t i=0;i<n;++i){
+            if (intervals[i][0] >= searchBack){
+                intervalsCount += 1;
+                searchBack = intervals[i][1];
+            } 
+        }
+        return n - intervalsCount;
+    }
+};
+

分析:假设第一个区间是 kk的左边没有任何区间,因此使用其他任何一个区间,只要右端点小于 k的右端点就可以了。而且右端点向左移动,比 k更优。因此首个区间就是所有可以选择的区间中右端点最小的那个区间 。后面只要去寻找其中与首个区间不重合并且右端点最小的区间即可。

+

贪心策略:优先保留结尾小且不相交的区间

+

错误1:没想明白右端点的问题

+

错误2:函数要加 static(但是不太明白)

+

错误3:使用引用传参,防止拷贝浪费时间

+

建议:一些比如数组大小的数字提前计算出来,避免反复计算。

+

练习

+

Leetcode 605

+

有一个很长的花坛,一部分地块种植了花,另一部分却没有。花不能种植在相邻的地块上。 flowerbed 表示花坛,由若干 01 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false

+
class Solution {
+public:
+    bool canPlaceFlowers(vector<int>& flowerbed, int n) {
+        int flowerCount = 0;
+        size_t m = flowerbed.size();
+        if(m == 1){
+            if(flowerbed[0] == 0){
+                return true;
+            }
+            else if (flowerbed[0] == 1 && n == 1){
+                return false;
+            }
+        }
+        if(flowerbed[0] == 0 && flowerbed[1] == 0){
+            flowerbed[0] = 1;
+            flowerCount += 1;
+        }
+        if(flowerbed[m-1] == 0 && flowerbed[m-2] == 0){
+            flowerbed[m-1] = 1;
+            flowerCount += 1;
+        }
+        for(size_t i=1;i<m-1;i++){
+            if(flowerbed[i] == 0 && flowerbed[i-1] == 0 && flowerbed[i+1] == 0){
+                flowerbed[i] = 1;
+                flowerCount += 1;
+            }
+        }
+        return flowerCount >= n;
+    }
+};
+

分析:遍历即可,尤其注意开头部分和结尾部分。

+

错误:最后没有考虑等于条件也为 true

+

建议:判断太多,有更为简洁的解法,大致思路是计算相邻的 1之间能种多少个 0

+

Leetcode 452

+

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,不知道具体位置,但是知道一个位置的范围。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,射进了气球的位置范围后,该气球就会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。给你一个数组 points , 返回引爆所有气球所必须射出的最小弓箭数 。

+
class Solution {
+public:
+    static bool cmp(vector<int> &a,vector<int> &b){
+        return a[1] < b[1];
+    }
+    int findMinArrowShots(vector<vector<int>>& points) {
+        sort(points.begin(),points.end(),cmp);
+        int n = points.size();
+        int arrowCount = 1;
+        int endPoint = points[0][1];
+        for(size_t i=0;i<n;i++){
+            if(points[i][0] > endPoint){
+                ++arrowCount;
+                endPoint = points[i][1];
+            }
+        }
+        return arrowCount;
+    }
+};
+

分析:拿第一个气球来说,要是想射爆,最佳的方法就是射最右侧的位置,这样能射到的其他的气球数量也会增加,以此类推,构成贪心算法。

+

一遍AC

+

Leetcode 763

+

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

+
class Solution {
+public:
+    vector<int> partitionLabels(string s) {
+        vector<int> last(26,-1);
+        for(int i = s.size() - 1;i >= 0;i--){
+            if(last[s[i] - 'a'] == -1){
+                last[s[i] - 'a'] = i;
+            }
+        }
+        vector<int> result;
+        int start = 0;
+        int end = 0;
+        for(int i=0;i<s.size();i++){
+            end = max(end,last[s[i] - 'a']);
+            if(i == end){
+                result.push_back(end - start + 1);
+                start = i + 1;
+            }
+        }
+        return result;
+    }
+};
+

分析:首先得到字符出现的最后的下标位置,然后重新遍历字符串,得到每个字符最后出现的位置。一旦前面的所有字符都出现完了,就算一个区间。

+

上述做法使用贪心的思想寻找每个片段可能的最小结束下标,因此可以保证每个片段的长度一定是符合要求的最短长度,如果取更短的片段,则一定会出现同一个字母出现在多个片段中的情况。由于每次取的片段都是符合要求的最短的片段,因此得到的片段数也是最多的。

+

错误:思路有问题,没有做对

+

Leetcode 122

+

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。返回你能获得的最大利润 。

+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        int stockSum = 0;
+        for(size_t i=0;i<prices.size()-1;i++){
+            if(prices[i+1] > prices[i]){
+                stockSum = stockSum+prices[i+1]-prices[i];
+            }
+        }
+        return stockSum;
+    }
+};
+

分析:什么都不限制,涨了就卖就完事了,比较简单。贪心策略就是只要价格上涨就直接出售。

+

一遍AC

+

Leetcode 406

+

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面正好ki个身高大于或等于 hi 的人。

+

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

+
class Solution {
+public:
+    static bool cmp(vector<int> &a,vector<int> &b){
+        if(a[0] != b[0]){
+            return a[0] < b[0];
+        }
+        return a[1] > b[1];
+    }
+    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
+        vector<vector<int>> result(people.size());
+        sort(people.begin(),people.end(),cmp);
+        for(size_t i=0;i<people.size();++i){
+            int pos = people[i][1];
+            for(size_t j=0;j<result.size();j++){
+                if(result[j].empty()){
+                    pos--;
+                }
+                if(pos == -1){
+                    result[j] = people[i];
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+};
+

分析:将人员从低往高先排列,然后一个个进行插入。插入的人只会对后面的人有影响,因为后面的人的身高都会大于等于他。而对已经插入的人没有影响。因此插入的时候给后面的人要留出空位置,以便后面的人插入进去。如果身高相同,就去比较 kiki更小一点的,说明这个人在靠前一点,也就是最小的 ki前面是不会有相同身高的人的,由于相同身高也会算在内,因此要先插入大 ki

+

错误:思路有问题,没有做对

+

Leetcode 665

+

给你一个长度为 n 的整数数组 nums ,请你判断在最多改变 1 个元素的情况下,该数组能否变成一个非递减数列。

+
class Solution {
+public:
+    bool checkPossibility(vector<int>& nums) {
+        int count = 0;
+        for (int i = 1; i < nums.size(); i++) {
+            if (nums[i] < nums[i - 1]) {
+                if(i == 1 || nums[i] >= nums[i - 2]){
+                    nums[i - 1] = nums[i];
+                }
+                else{
+                    nums[i] = nums[i - 1];
+                }
+                ++count;
+            }
+        }
+        return count <= 1;
+    }
+};
+

分析:要多种情况一起考虑。。。。

+

错误:思路有问题,没有做对。另外不要去改 i+1啊。。判断什么修改什么好吧,要不就乱套了。

+

总结

+

贪心算法确实是比较好理解的,但是怎么贪心?什么时候贪心?这些问题都要去详细认真的思考,真正出题的时候不会如此直白,要多练多想。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第2章 贪心算法
+
https://zhangzhao219.github.io/2022/08/27/Leetcode/Leetcode-101/Leetcode-101-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月27日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-3/index.html b/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-3/index.html new file mode 100644 index 000000000..77319a68d --- /dev/null +++ b/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-3/index.html @@ -0,0 +1,1017 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第3章 双指针 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第3章 双指针

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第3章 双指针

+ +

双指针

+

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

+

若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

+

若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

+

Two Sum

+

Leetcode 167

+

在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。

+
class Solution {
+public:
+    vector<int> twoSum(vector<int>& numbers, int target) {
+        vector<int> result;
+        int left = 0;
+        int right = numbers.size() - 1;
+        while(left < right){
+            if(numbers[left] + numbers[right] < target){
+                ++left;
+            }
+            else if(numbers[left] + numbers[right] > target){
+                --right;
+            }
+            else{
+                result.push_back(left+1);
+                result.push_back(right+1);
+                break;
+            }
+        }
+        return result;
+    }
+};
+

分析:左右两个指针分别进行移动,加和小了就把左边的指针往右移动一下,加和大了就把右边的指针往左移动一下。这道题比较特殊,限定了一定有答案而且答案只会有一个,因此不需要添加任何其他的额外条件。

+

错误:没看清下标的表示方式,直接输出数组下标了。

+

归并两个有序数组

+

Leetcode 88

+

给定两个有序数组,把两个数组合并为一个。

+
class Solution {
+public:
+    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
+        if(n == 0){
+            return;
+        }
+        if(m == 0){
+            nums1 = nums2;
+            return;
+        }
+        int mi = m - 1;
+        int ni = n - 1;
+        int numIndex = m+n-1;
+        while(numIndex >= 0){
+            if(mi >= 0 && ni >= 0){
+                if(nums1[mi] > nums2[ni]){
+                    swap(nums1[mi],nums1[numIndex]);
+                    --mi;
+                }
+                else{
+                    swap(nums2[ni],nums1[numIndex]);
+                    --ni;  
+                }
+          
+            }
+            else if(mi == -1){
+                while(ni != -1){
+                    nums1[numIndex] = nums2[ni];
+                    --ni;
+                    --numIndex;
+                }
+                break;
+            }
+            --numIndex;
+        }
+
+    }
+};
+

分析:从后边开始安排数字,填充0的空位

+

错误:挺简单的一道题,首先是刚开始没有想到非常好的解法,看了答案后双指针又有一些问题。。真的是生疏了。

+

快慢指针

+

Leetcode 142

+

给定一个链表,如果有环路,找出环路的开始点。

+
class Solution {
+public:
+    ListNode *detectCycle(ListNode *head) {
+        ListNode* slow = head;
+        ListNode* fast = head;
+        do{
+            if(fast == nullptr || fast->next == nullptr){
+                return nullptr;
+            }
+            slow = slow->next;
+            fast = fast->next->next;
+        }while(slow != fast);
+        fast = head;
+        while(slow != fast){
+            slow = slow->next;
+            fast = fast->next;
+        }
+        return fast;
+    }
+};
+

分析:有一个通用的解法——快慢指针(Floyd判圈法)。给定两个指针,分别命名为slow和fast,起始位置在链表的开头。每次fast前进两步,slow前进一步。如果fast可以走到尽头,那么说明没有环路;如果fast可以无限走下去,那么说明一定有环路,且一定存在一个时刻slow 和fast 相遇。当slow和fast第一次相遇时,我们将fast重新移动到链表开头,并让slow和fast每次都前进一步。当slow和fast第二次相遇时,相遇的节点即为环路的开始点。

+

错误:算法忘记了,没有思路。

+

滑动窗口

+

Leetcode 76

+

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

+
class Solution {
+public:
+    string minWindow(string s, string t) {
+        size_t s_size = s.size();
+        size_t t_size = t.size();
+        map<char,int> mp1;
+        for(size_t i=0;i<t_size;i++){
+            if(mp1.count(t[i])){
+                mp1[t[i]] += 1;
+            }
+            else{
+                mp1[t[i]] = 1;
+            }
+        }
+        int left = 0,cnt = 0,min_l = 0,min_size = s_size+1;
+        for(int r=0;r<s_size;++r){
+            if(mp1.count(s[r])){
+                --mp1[s[r]];
+                if(mp1[s[r]] >= 0){
+                    ++cnt;
+                }
+                while(cnt == t_size){
+                    if(r - left + 1 < min_size){
+                        min_size = r - left + 1;
+                        min_l = left;
+                    }
+                    if(mp1.count(s[left]) && ++mp1[s[left]] > 0){   
+                        --cnt;
+                    } 
+                    ++left;
+                }
+            }
+        }
+        return min_size > s_size ? "" : s.substr(min_l,min_size);
+    }
+};
+

分析:滑动窗口典型题目

+

首先对子字符串进行计数,记录是否出现,以及出现的次数。然后采取滑动窗口的策略,两个指针都从左开始滑动,以右指针为基准构成外侧的大循环。右指针滑动的过程中,对之前的计数进行更改,滑动到了一个字符就减小1。等到0的时候,说明右指针滑动过了的字符串一定包含子字符串的全部字符,然后将左指针向右滑动来减小这个字符串的长度。左指针碰到了某个子字符串内部的字符,就会将计数+1,从而不满足这个字符串包含整个子字符串的要求,因此重新开始移动右字符串,以尝试再次包含整个子字符串。

+

错误:算法忘记了,没有思路。

+

练习

+

Leetcode 633

+

给定一个非负整数 c ,你要判断是否存在两个整数 ab,使得 a^2 + b^2 = c

+
class Solution {
+public:
+    bool judgeSquareSum(int c) {
+        long long left = 0;
+        long long right = sqrt(c);
+        while(left <= right){
+            if(left * left + right * right < c){
+                ++left;
+            }
+            else if(left * left + right * right > c){
+                --right;
+            }
+            else{
+                return true;
+            }
+        }
+        return false;
+    }
+};
+

分析:仍然是双指针的问题,多了一点点细节问题。

+

错误:left = right,right的范围考虑的不太好。

+

Leetcode 680

+

给你一个字符串 s最多可以从中删除一个字符。请你判断 s是否能成为回文字符串:如果能,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool judge(string &s,int left,int right){
+        while(left <= right){
+            if(s[left] == s[right]){
+                ++left;
+                --right;
+            }
+            else{
+                return false;
+            }
+        }
+        return true;
+    }
+    bool validPalindrome(string s) {
+        size_t s_size = s.size();
+        int left = 0;
+        int right = s_size - 1;
+        while(left <= right){
+            if(s[left] == s[right]){
+                ++left;
+                --right;
+            }
+            else{
+                return judge(s,left+1,right) || judge(s,left,right-1);
+            }
+        }
+        return true;
+    }
+};
+

分析:双指针移动就好

+

错误:没有考虑到删除一个字符后有两种情况,应该共同考虑而不是仅仅使用某一种情况进行判断。

+

Leetcode 524

+

给你一个字符串 s 和一个字符串数组 dictionary ,找出并返回 dictionary 中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。

+
class Solution {
+public:
+    static bool cmp(string &a,string &b){
+        if(a.size() != b.size()){
+            return a.size() > b.size();
+        }
+        return a < b;
+    }
+    string findLongestWord(string s, vector<string>& dictionary) {
+        sort(dictionary.begin(),dictionary.end(),cmp);
+        size_t s_size = s.size();
+        for(auto t : dictionary){
+            size_t t_size = t.size();
+            int si = 0,ti = 0;
+            while(si != s_size){
+                if(s[si] == t[ti]){
+                    ++ti;
+                }
+                ++si;
+                if(ti == t_size){
+                    return t;
+                }
+            }
+        }
+        return "";
+    }
+};
+

分析:先排序,然后双指针进行移动匹配,如果子字符串的指针移动到字符串的末尾了,说明已经匹配成功了,可以直接输出这个字符串。如果原始的字符串的指针移动到末尾了,说明没有匹配成功,因此转为匹配下一个字符串。

+

错误:题目要求的排序条件没有看好,返回了长度比较短的字符串。

+

Leetcode 340

+

给定一个字符串 s,找出至多包含 k个不同字符的最长子串 T

+

分析:还是滑动窗口的策略,以右边指针为基准,滑动一下就记录一下最长的长度,滑动到不满足条件了,就将左边的指针收回来,收到满足条件了就继续放右边的指针去滑动。

+
class Solution {
+public:
+    int lengthOfLongestSubstringKDistinct(string s, int k) {
+        size_t s_size - s.size();
+        map<char,int> mp;
+        int maxlen = 0;
+        int l = 0;
+        for(int r=0;r<s_size;r++){
+            if(mp.size() <= k){
+                ++mp[s[r]];
+            }
+            while(mp.size() > k){
+                if(--mp[s[l]] == 0){
+                    mp.erase(s[l]);
+                }
+                l++;
+            }
+            maxlen = max(maxlen,r-l+1);
+        }
+        return maxlen;
+    }
+};
+

错误:会员题,无法提交。

+

总结

+

双指针的题目还可以,感觉重要的是判断条件。滑动窗口的题目比较困难,可能也是做的题目比较少。后面还需要加强练习。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第3章 双指针
+
https://zhangzhao219.github.io/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-3/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-4/index.html b/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-4/index.html new file mode 100644 index 000000000..ad0d26b48 --- /dev/null +++ b/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-4/index.html @@ -0,0 +1,958 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第4章 二分查找 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第4章 二分查找

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第4章 二分查找

+ +

二分查找

+

二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。

+

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。

+

一点点细节小笔记

+
    +
  1. 最基本的二分查找算法:
  2. +
+

因为我们初始化 right = nums.length - 1,所以决定了我们的「搜索区间」是 [left, right],所以决定了 while (left <= right),同时也决定了 left = mid+1right = mid-1,因为我们只需找到一个 target 的索引即可,所以当 nums[mid] == target 时可以立即返回。

+
    +
  1. 寻找左侧边界的二分查找:
  2. +
+

因为我们初始化 right = nums.length,所以决定了我们的「搜索区间」是 [left, right),所以决定了 while (left < right),同时也决定了 left = mid + 1right = mid,因为我们需找到 target 的最左侧索引,所以当 nums[mid] == target 时不要立即返回,而要收紧右侧边界以锁定左侧边界。

+
    +
  1. 寻找右侧边界的二分查找:
  2. +
+

因为我们初始化 right = nums.length,所以决定了我们的「搜索区间」是 [left, right),所以决定了 while (left < right),同时也决定了 left = mid + 1right = mid,因为我们需找到 target 的最右侧索引,所以当 nums[mid] == target 时不要立即返回,而要收紧左侧边界以锁定右侧边界,又因为收紧左侧边界时必须 left = mid + 1,所以最后无论返回 left 还是 right,必须减一。

+

求开方

+

Leetcode 69

+

给定一个非负整数,求它的开方,向下取整。

+
class Solution {
+public:
+    int mySqrt(int x) {
+        long long left = 0;
+        long long right = sqrt(x) + 1;
+        while(left <= right){
+            long long mid = (right - left) / 2 + left;
+            if(mid * mid < x){
+                left = mid + 1;
+            }
+            else if(mid * mid > x){
+                right = mid - 1;
+            }
+            else{
+                return mid;
+            }
+        }
+        return left - 1;
+    }
+};
+

思路很简单,主要是细节问题,已经整理了笔记。

+

查找区间

+

Leetcode 34

+

给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。

+
class Solution {
+public:
+    vector<int> searchRange(vector<int>& nums, int target) {
+        vector<int> result;
+        int n = nums.size();
+        if (n == 0){
+            return vector<int>{-1, -1};
+        }
+        int left = 0;
+        int right = n;
+        while(left < right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] >= target){
+                right = mid;
+            }
+            else if(nums[mid] < target){
+                left = mid + 1;
+            }
+        }
+        if(right >= n || nums[right] != target){
+            return vector<int>{-1, -1};
+        }
+        else{
+            result.push_back(right);
+        }
+        left = 0;
+        right = n;
+        while(left < right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] > target){
+                right = mid;
+            }
+            else if(nums[mid] <= target){
+                left = mid + 1;
+            }
+        }
+        result.push_back(left - 1);
+        return result;
+    }
+};
+

分析:也是最基础的二分查找,实现了 upper_boundlower_bound两个函数。

+

错误:判断的时候忘记判断是否越界。

+

旋转数组查找数字

+

Leetcode 81

+

一个原本增序的数组被首尾相连后按某个位置断开(如[1,2,2,3,4,5] - [2,3,4,5,1,2],在第一位和第二位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个旋转数组中。

+
class Solution {
+public:
+    bool search(vector<int>& nums, int target) {
+        int n = nums.size();
+        int left = 0;
+        int right = n;
+        while(left < right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] == target){
+                return true;
+            }
+            else if(nums[mid] < nums[right-1]){
+                // 说明右端是排好序的
+                if(target >= nums[mid] && target <= nums[right-1]){
+                    left = mid + 1;
+                }
+                else{
+                    right = mid;
+                }
+            }
+            else if(nums[mid] > nums[right-1]){
+                // 说明左端是排好序的
+                if(target <= nums[mid] && target >= nums[left]){
+                    right = mid;
+                }
+                else{
+                    left = mid + 1;
+                }
+            }
+            else{
+                --right;
+            }
+        }
+        return false;
+    }
+};
+

分析:旋转数组是一类经典题目,需要抓住旋转后二分会有一个区间是单调的性质进行判断,从而对所查找的数字进行区间的锁定。

+

错误:条件考虑不全面,没有对旋转数组充分理解。

+

练习

+

Leetcode 154

+

寻找旋转排序数组中的最小值

+
class Solution {
+public:
+    int findMin(vector<int>& nums) {
+        int n = nums.size();
+        int left = 0;
+        int right = n;
+        int minnum = 10000;
+        while(left < right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] > nums[left]){
+                // 左边一定有序
+                minnum = min(minnum,nums[left]);
+                left = mid + 1;
+            }
+            else if(nums[mid] < nums[left]){
+                // 右边一定有序
+                minnum = min(minnum,nums[mid]);
+                right = mid;
+            }
+            else{
+                minnum = min(minnum,nums[mid]);
+                ++left;
+            }
+        }
+        return minnum;
+    }
+};
+

分析:比查找还要稍稍简单一点,只需要想好最小值可能出现的位置即可。

+

错误:相等的时候没有判断,会导致漏掉元素。

+

Leetcode 540

+

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。请你找出并返回只出现一次的那个数。

+
class Solution {
+public:
+    int singleNonDuplicate(vector<int>& nums) {
+        int n = nums.size();
+        int left = 0;
+        int right = n;
+        if(nums.size()==1){
+            return nums[0];
+        }
+        while(left < right){
+            int mid  = (right - left) / 2 + left;
+            if(mid % 2 == 0){
+                if(nums[mid] == nums[mid+1]){
+                    left = mid + 2;
+                }
+                else{
+                    right = mid;
+                }
+            }
+            else{
+                if(nums[mid] == nums[mid-1]){
+                    left = mid + 1;
+                }
+                else{
+                    right = mid;
+                }
+            }
+        }
+        return nums[left];
+    }
+};
+

分析:如果mid是偶数,则比较nums[mid]和nums[mid+1]是否相等;如果mid是奇数,则比较nums[mid−1]和nums[mid]是否相等。

+

错误:感觉需要判断很多条件?其实不用,只需要考虑长度为1的数组,然后根据下标寻找规律就可以。

+

Leetcode 4

+

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的中位数

+

分析:二分的解法太难了。。后续补充吧

+

错误:没有思路。。。

+

总结

+

二分查找是非常好的降低时间复杂度的方法之一,整体的思想不是很难,但是细节的部分需要多多注意。当然也有难题,还要多练习。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第4章 二分查找
+
https://zhangzhao219.github.io/2022/08/28/Leetcode/Leetcode-101/Leetcode-101-4/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/29/Leetcode/Leetcode-101/Leetcode-101-5/index.html b/2022/08/29/Leetcode/Leetcode-101/Leetcode-101-5/index.html new file mode 100644 index 000000000..a96096c21 --- /dev/null +++ b/2022/08/29/Leetcode/Leetcode-101/Leetcode-101-5/index.html @@ -0,0 +1,992 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第5章 排序算法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第5章 排序算法

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第5章 排序算法

+ +

排序算法

+

排序自然都有C++的STL搞定了,但是在实际中仍然需要这些排序算法,一方面夯实基础,另一方面有一些题目是从这些排序算法中引申出来的,掌握这些排序算法对于做题也会有很大的帮助。

+

常用排序算法

+

调用

+
int main(void){
+    vector<int> nums = {1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};
+    vector<int> temp(nums.size());
+    sort(nums.begin(), nums.end());
+    quick_sort(nums, 0, nums.size());
+    print(nums);
+    merge_sort(nums, 0, nums.size(), temp);
+    print(nums);
+    insertion_sort(nums, nums.size());
+    print(nums);
+    bubble_sort(nums, nums.size());
+    print(nums);
+    selection_sort(nums, nums.size());
+    print(nums);
+    return 0;
+}
+

快速排序

+
void quick_sort(vector<int> &nums,int left,int right){
+    int l = left;
+    int r = right;
+    if(left+1 >= right){
+        return;
+    }
+    int k = nums[left];
+    while(left+1 < right){
+        while(left+1 < right && nums[right-1] >= k){
+            --right;
+        }
+        nums[left] = nums[right-1];
+        while(left+1 < right && nums[left] < k){
+            ++left;
+        }
+        nums[right-1] = nums[left];
+    }
+    nums[left] = k;
+    quick_sort(nums,l,left);
+    quick_sort(nums,left+1,r);
+}
+

错误:while内部的 left < right的条件没有加,导致内部会出问题,而且也是要+1的

+

归并排序

+
void merge_sort(vector<int> &nums,int left,int right,vector<int> &temp){
+    if(left + 1 >= right){
+        return;
+    }
+    int mid = (right - left) / 2 + left;
+    merge_sort(nums,left,mid,temp);
+    merge_sort(nums,mid,right,temp);
+    int p = left;
+    int q = mid;
+    int i = left;
+    while(p < mid && q < right){
+        if(nums[p] <= nums[q]){
+            temp[i++] = nums[p++];
+        }
+        else{
+            temp[i++] = nums[q++];
+        }
+    }
+    while(p < mid){
+        temp[i++] = nums[p++];
+    }
+    while(q < right){
+        temp[i++] = nums[q++];
+    }
+    for(int j=left;j<right;++j){
+        nums[j] = temp[j];
+    }
+}
+

错误:应该是 left + 1 >= right,只剩下一个数字后就应该返回了。

+

插入排序

+
void insertion_sort(vector<int> &nums,int n){
+    for(int i=1;i<n;i++){
+        int a = i;
+        while(a - 1 >= 0 && nums[a] < nums[a-1]){
+            swap(nums[a],nums[a-1]);
+            --a;
+        }
+    }
+    return;
+}
+

冒泡排序

+
void bubble_sort(vector<int> &nums,int n){
+    for(int i=0;i<n-1;i++){
+        for(int j=0;j<n-i-1;j++){
+            if(nums[j] > nums[j+1]){
+                swap(nums[j],nums[j+1]);
+            }
+        }
+    }
+    return;
+}
+

选择排序

+
void selection_sort(vector<int> &nums,int n){
+    for(int i=0;i<n;i++){
+        int minnum = nums[i];
+        int minindex = i;
+        for(int j=i+1;j<n;j++){
+            if(nums[j] < minnum){
+                minnum = nums[j];
+                minindex = j;
+            }
+        }
+        swap(nums[i],nums[minindex]);
+    }
+    return;
+}
+

快速排序

+

Leetcode 215

+

在一个未排序的数组中,找到第 k大的数字

+
class Solution {
+public:
+    static void quick_selection(vector<int> &nums,int left,int right,int k){
+        int l = left;
+        int r = right;
+        int k2 = nums[left];
+        if(left + 1 > right){
+            return;
+        }
+        while(left+1 < right){
+            while(left+1 < right && nums[right-1] < k2){
+                --right;
+            }
+            nums[left] = nums[right-1];
+            while(left+1 < right && nums[left] >= k2){
+                ++left;
+            }
+            nums[right-1] = nums[left];
+        }
+        nums[left] = k2;
+        if(k <= left){
+            quick_selection(nums,l,left,k);
+        }
+        else{
+            quick_selection(nums,left+1,r,k);
+        }
+        return;
+    }
+    int findKthLargest(vector<int>& nums, int k) {
+        quick_selection(nums,0,nums.size(),k-1);
+        return nums[k-1];
+    }
+};
+

分析:与快速排序相同的思路,但是不需要对没有用的一侧进行快速排序,只需要对k在的区间一侧进行快速排序即可。

+

错误:开始快速排序有问题,然后k的值想不清楚造成错误。

+

桶排序

+

Leetcode 347

+
class Solution {
+public:
+    static bool cmp(pair<int,int> &a,pair<int,int> &b){
+        return a.first > b.first;
+    }
+    vector<int> topKFrequent(vector<int>& nums, int k) {
+        map<int,int> mp1,mp2;
+        for(auto i : nums){
+            ++mp1[i];
+        }
+        vector<pair<int,int>> pr;
+        vector<int> result;
+        for(auto i = mp1.cbegin();i != mp1.cend();++i){
+            pr.push_back(make_pair(i->second,i->first));
+        }
+        sort(pr.begin(),pr.end(),cmp);
+        for(auto i = pr.cbegin();i != pr.cend();++i){
+            if(k != 0){
+                result.push_back(i->second);
+            }
+            else{
+                break;
+            }
+            k--;
+        }
+        return result;
+    }
+};
+

分析:也是比较简单的一道题,通过这道题可以复习一下各种 STL数据结构,总也不用生疏了。

+

错误:STL有一些生疏,调了一段时间才调好。

+

练习

+

Leetcode 451

+

给定一个字符串 s ,根据字符出现的频率对其进行降序排序。一个字符出现的频率是它出现在字符串中的次数。返回已排序的字符串

+
class Solution {
+public:
+    static bool cmp(pair<char,int> &a,pair<char,int> &b){
+        return a.second > b.second;
+    }
+    string frequencySort(string s) {
+        string result = "";
+        map<char,int> mp;
+        for(auto i : s){
+            mp[i] += 1;
+        }
+        vector<pair<char,int>> pr;
+        for(auto i = mp.cbegin();i != mp.cend();i++){
+            pr.push_back(make_pair(i->first,i->second));
+        }
+        sort(pr.begin(),pr.end(),cmp);
+        for(auto i = pr.cbegin();i != pr.cend();i++){
+            for(int j=0;j<i->second;j++){
+                result += i->first;
+            }
+        }
+        return result;
+    }
+};
+

分析:桶排序的变形题,没有什么新意,还是数据结构

+

一遍AC

+

Leetcode 75

+
void sortColors(vector<int>& nums) {
+    int n = nums.size();
+    int p0 = 0;
+    int p1 = 0;
+    for(int i=0;i<n;++i){
+        if(nums[i] == 1){
+            swap(nums[i],nums[p1]);
+            ++p1;
+        }
+        else if(nums[i] == 0){
+            swap(nums[i],nums[p0]);
+            if(p0 < p1){
+                swap(nums[i],nums[p1]);
+            }
+            ++p0;
+            ++p1;
+        }
+    }
+}
+

分析:荷兰国旗问题,双指针一次遍历就可以得到三个数字的排序。

+

错误:想复杂了。

+

总结

+

排序算法基本都可以写,就是变形的题目还是有些不太熟练。还是要多多练习。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第5章 排序算法
+
https://zhangzhao219.github.io/2022/08/29/Leetcode/Leetcode-101/Leetcode-101-5/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月29日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/29/UCAS/information-retrieval/information-retrieval-0/index.html b/2022/08/29/UCAS/information-retrieval/information-retrieval-0/index.html new file mode 100644 index 000000000..30b994814 --- /dev/null +++ b/2022/08/29/UCAS/information-retrieval/information-retrieval-0/index.html @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第0讲 课程简介 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第0讲 课程简介

+ + +
+ +

《现代信息检索》课程笔记:第0讲 课程简介

+ +

第0讲 课程简介

+

什么是信息检索

+

信息检索应用例子的共同特征:

+

给定需求或者是对象,从信息库中找出与之最匹配的信息或对象。

+

数据形式是无固定结构的自由文本(谷歌搜索)或者结构化数据(京东商品)

+

信息检索的定义

+
    +
  1. 信息检索是给定用户需求返回满足该需求信息的一门学科。通常涉及信息的获取、存储、组织和访问。
  2. +
  3. 信息检索是从大规模非结构化数据(通常是文本)的集合(通常保存在计算机上)中找出满足用户信息需求的资料(通常是文档)的过程。
  4. +
  5. 信息检索是“找对象”的学科,即定义并计算某种匹配“相似度”的学科。
  6. +
+

信息检索与其他的学科关系密切,包括自然语言处理、数据挖掘和机器学习。

+

信息检索技术广泛应用于搜索、推荐、挖掘、舆情分析、情报处理和内容安全。

+

从信息规模上分类,信息检索可以分为:

+
    +
  1. 个人信息检索:个人相关信息的组织、整理、搜索等,包括桌面搜索、个人信息管理、个人数字记忆等
  2. +
  3. 企业级信息检索:在企业内容文档的组织、管理、搜索等。企业级信息检索是内容管理的重要组成部分。
  4. +
  5. Web信息检索:在超大规模数据集上的检索。
  6. +
+

为什么要学习信息检索

+
    +
  1. 用户国家、企业、个人等需要信息检索技术:互联网的信息量太大、噪音太多,寻找所需要的信息非常不容易。互联网的不只是搜索引擎才需要信息检索技术,电子商务、社交网、数字图书馆、大规模数据分析、金融证券行业等都需要信息检索技术。
  2. +
  3. 公司需要信息检索技术:搜索引擎改变了很多传统的生活方式,互联网五大盈利模式或多或少都依赖信息检索技术的支撑,目前搜索引擎公司甚至整个互联网正常运转的计算广告的核心技术是信息检索技术。
  4. +
  5. 应用需求:移动搜索、产品搜索、专利搜索、广告推荐、社会网络分析、消费行为分析、网络评论分析、SEO营销
  6. +
+

信息检索学科的特点

+
    +
  1. 应用性:目标非常实际,例如提升网络搜索引擎返回结果准确率、商品推荐转化率。
  2. +
  3. 经验性:理论上漂亮的方法并不一定有用,理论需要结合实践。
  4. +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第0讲 课程简介
+
https://zhangzhao219.github.io/2022/08/29/UCAS/information-retrieval/information-retrieval-0/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月29日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/29/UCAS/information-retrieval/information-retrieval-1/index.html b/2022/08/29/UCAS/information-retrieval/information-retrieval-1/index.html new file mode 100644 index 000000000..54add8a46 --- /dev/null +++ b/2022/08/29/UCAS/information-retrieval/information-retrieval-1/index.html @@ -0,0 +1,920 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第1讲 布尔检索 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第1讲 布尔检索

+ + +
+ +

《现代信息检索》课程笔记:第1讲 布尔检索

+ +

第1讲 布尔检索

+

信息检索概述

+

现在提到信息检索,通常会首先想到Web搜索,但是除此之外还有很多其它的搜索应用,如电子邮件搜索、笔记本电脑(桌面)搜索、知识库搜索、法律文献搜索等。

+

本课程主要关注文本检索,因为文本检索是最早的检索应用,也仍然是目前最主要的应用,且文本检索理论可以用于其他领域。

+

信息检索与数据库的区别主要在于数据的区别,信息检索关注的是非结构化的数据,而数据库关注的是结构化的数据。

+

数据库常常支持范围或者精确匹配查询。

+

非结构化数据通常指自由文本,允许关键词加上操作符号的查询和更复杂的概念性查询,经典的检索模型一般都针对自由文本进行处理。

+

信息检索的一些基本概念

+

文档集(Collection): 由固定数目的文档组成

+

目标:返回与用户需求相关的文档并辅助用户来完成某项任务

+

相关性(Relevance):主观的概念,反映对象的匹配程度不同,应用相关性不同。

+

检索效果的评价:准确率和召回率(准确率是自己的,召回率才是真正的)

+

布尔检索:针对布尔查询的检索,布尔查询是指利用 ANDOR或者 NOT操作符将词项连接起来的查询。

+

索引方法

+

需求:莎士比亚的哪部剧本包含Brutus及Caesar但是不包含Calpurnia

+

将需求表示为布尔表达式: Brutus AND Caesar AND NOT Calpurnia

+

暴力索引方法

+

从头到尾扫描所有剧本,对每部剧本判断它是否包含Brutus AND Caesar ,同时又不包含Calpurnia

+

暴力方法的优点:①实现简单②很容易支持文档动态变化

+

暴力方法的不足:

+
    +
  1. 速度超慢 (特别是大型文档集)
  2. +
  3. 处理NOT Calpurnia 并不容易(不到末尾不能停止判断)
  4. +
  5. 不太容易支持其他操作 (e.g., 寻找靠近countrymen的单词Romans)
  6. +
  7. 不支持检索结果的灵活排序 (排序时只返回较好的结果)
  8. +
+

倒排索引

+

关联矩阵:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Antony and CleopatraJulius CaesarThe TempestHamletOthelloMacbeth
Antony110001
Brutus110100
Caesar110111
Calpurnia010000
Cleopatra100000
mercy101111
worser101110
+

行表示单词,列表示文本,若文本中包含这个单词,则记录为1,反之记录为0

+

使用关联矩阵进行查询的时候,即将关联矩阵有关单词的行向量取出来后进行按位与或非操作即可。

+

但是这种词项-文档的关联矩阵将非常大,由于是 one-hot存储,矩阵高度稀疏,需要更好的表示方式,因此有了倒排索引。

+

对每个词项 t,记录所有包含 t的文档列表,每篇文档用一个唯一的 docID来表示,通常是正整数。

+

词典 ➡ 倒排记录(Posting)

+

Brutus ➡ 1 2 4 11 31 45 173

+

Calpurnia ➡ 1 2 4 5 6 16 57 132

+

Caesar ➡2 31 54 101

+

倒排索引的存储通常采用变长表方式

+
    +
  1. 磁盘上,顺序存储方式比较好,便于快速读取
  2. +
  3. 内存中,采用链表或者可变长数组方式,便于节省空间
  4. +
+

构建倒排索引的流程

+

文本预处理:

+
    +
  1. 词条化(Tokenization):将字符序列切分为词条
  2. +
  3. 规范化(Normalization):将文档和查询中的词项映射到相同的形式
  4. +
  5. 词干还原(Stemming):将同一词汇的不同形式还原到词根
  6. +
  7. 停用词去除(Stopwords removal):去除高频词项
  8. +
+

构建词条序列:<词条,docID> 类型的二元组

+

按词项排序:每个词项按 docID排序

+

某个词项在单篇文档中的多次出现会被合并

+

拆分成词典和倒排记录表两部分

+

每个词项出现的文档数目(doc.frequency, DF)会被加入

+

最终构成倒排索引:

+

v4BM1f.png

+

布尔查询的处理

+

对于布尔查询来说,对倒排记录表进行操作即可。

+

每个倒排记录表都有一个定位指针,两个指针同时从前往后扫描, 每次比较当前指针对应倒排记录,然后移动某个或两个指针。合并时间为两个表长之和的线性时间。时间复杂度为 O(m+n)

+

这也是倒排记录表按照 docID排序的关键原因!

+

查询处理中存在处理的顺序问题:n个词项的 AND我们希望查询的次数越少越好,因此要按照表从小到大(即 df从小到大)的顺序进行处理,每次从最小的开始合并(这样可以尽量提前结束合并)

+

按照直接加和的方式对 Ordf进行估计。

+

合并策略

+

每个布尔表达式都能转换成(合取范式)

+

获得每个词项的 df

+

通过将词项的 df相加,估计每个 OR表达式对应的倒排记录表的大小

+

按照上述估计从小到大依次处理每个 OR表达式

+

布尔检索的优点

+

构建简单,是构建信息检索系统的一种最简单方式

+
    +
  • 在30多年中是最主要的检索工具
  • +
  • 当前许多搜索系统仍然使用布尔检索模型
  • +
  • 有一些扩展的布尔操作符
  • +
  • 如果非常清楚想要查什么、能得到什么,很多专业人士喜欢使用布尔搜索
  • +
+

布尔检索的缺点

+
    +
  • 布尔查询构建复杂,不适合普通用户。构建不当,检索结果过多或者过少
  • +
  • 没有充分利用词项的频率信息。因为词通常出现的越多越好,需要利用词项在文档中的词项频率(term frequency, tf)信息
  • +
  • 不能对检索结果进行排序
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第1讲 布尔检索
+
https://zhangzhao219.github.io/2022/08/29/UCAS/information-retrieval/information-retrieval-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月29日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/30/Leetcode/Leetcode-101/Leetcode-101-6/index.html b/2022/08/30/Leetcode/Leetcode-101/Leetcode-101-6/index.html new file mode 100644 index 000000000..88d97ad4d --- /dev/null +++ b/2022/08/30/Leetcode/Leetcode-101/Leetcode-101-6/index.html @@ -0,0 +1,1382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第6章 搜索 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第6章 搜索

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第6章 搜索

+ +

搜索

+

深度优先搜索和广度优先搜索是两种最常见的优先搜索方法,它们被广泛地运用在图和树等结构中进行搜索。

+

深度优先搜索

+

Leetcode 695

+

岛屿是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直的四个方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。岛屿的面积是岛上值为 1 的单元格的数目。计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0

+
class Solution {
+public:
+    static int DFS(vector<vector<int>> & grid,int x,int y,int m,int n){
+        if(x < 0 || x >= m || y < 0 || y >= n || grid[x][y] == 0){
+            return 0;
+        }
+        grid[x][y] = 0;
+        return 1 + DFS(grid,x+1,y,m,n) + DFS(grid,x-1,y,m,n) + DFS(grid,x,y+1,m,n) + DFS(grid,x,y-1,m,n);
+    }
+    int maxAreaOfIsland(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        int maxarea = 0;
+        for(int i=0;i<m;++i){
+            for(int j=0;j<n;++j){
+                if(grid[i][j] == 1){
+                    maxarea = max(maxarea,DFS(grid,i,j,m,n));
+                }
+            }
+        }
+        return maxarea;
+    }
+};
+

分析:标准的DFS,重点要判断是否越界以及返回值的处理。

+

错误:基本思路是正确的,返回值的处理有问题,以及想的有些复杂。

+

Leetcode 547

+

n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中 省份 的数量。

+
class Solution {
+public:
+    static void DFS(vector<vector<int>>& isConnected, vector<bool>& visit,int x,int n){
+        if(x < 0 || x >= n || visit[x] == true){
+            return;
+        }
+        visit[x] = true;
+        for(int i=0;i<n;i++){
+            if(isConnected[x][i] == 1){
+                DFS(isConnected,visit,i,n);
+            }
+        }
+        return;
+    }
+    int findCircleNum(vector<vector<int>>& isConnected) {
+        int n = isConnected.size();
+        int sumCount = 0;
+        vector<bool> visit(n);
+        fill(visit.begin(),visit.end(),false);
+        for(int i=0;i<n;i++){
+            if(visit[i] == false){
+                DFS(isConnected,visit,i,n);
+                ++sumCount;
+            }
+        }
+        return sumCount;
+    }
+};
+

分析:还是比较基本的DFS,只不过是一个一维的DFS,比较简单

+

错误:开始的思路有一些偏差,后面纠正过来没什么问题了。

+

Leetcode 417

+

有一个 m × n 的矩形岛屿,与太平洋大西洋相邻。 太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heightsheights[r][c] 表示坐标 (r, c) 上单元格高于海平面的高度 。岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。返回网格坐标 result2D 列表 ,其中 result[i] = [r<sub>i</sub>, c<sub>i</sub>] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋

+
class Solution {
+public:
+    static void DFS(vector<vector<int>>& heights,vector<vector<bool>>& Ocean,int x,int y,int m,int n){
+        Ocean[x][y] = true;
+        if(x+1 < m && Ocean[x+1][y] == false && heights[x+1][y] >= heights[x][y]){
+            DFS(heights,Ocean,x+1,y,m,n);
+        }
+        if(x-1 >= 0 && Ocean[x-1][y] == false && heights[x-1][y] >= heights[x][y]){
+            DFS(heights,Ocean,x-1,y,m,n);
+        }
+        if(y+1 < n && Ocean[x][y+1] == false && heights[x][y+1] >= heights[x][y]){
+            DFS(heights,Ocean,x,y+1,m,n);
+        }
+        if(y-1 >= 0 && Ocean[x][y-1] == false && heights[x][y-1] >= heights[x][y]){
+            DFS(heights,Ocean,x,y-1,m,n);
+        }
+        return;
+    }
+    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
+        int m = heights.size();
+        int n = heights[0].size();
+        vector<vector<bool>> pOcean(m,vector<bool>(n,false));
+        vector<vector<bool>> aOcean(m,vector<bool>(n,false));
+        for(int i=0;i<m;++i){
+            if(pOcean[i][0] == false){
+                DFS(heights,pOcean,i,0,m,n);
+            }
+            if(aOcean[i][n-1] == false){
+                DFS(heights,aOcean,i,n-1,m,n);
+            }
+        }
+        for(int i=0;i<n;++i){
+            if(pOcean[0][i] == false){
+                DFS(heights,pOcean,0,i,m,n);
+            }
+            if(aOcean[m-1][i] == false){
+                DFS(heights,aOcean,m-1,i,m,n);
+            }
+        }
+        vector<vector<int>> result;
+        for(int i=0;i<m;++i){
+            for(int j=0;j<n;++j){
+                if(pOcean[i][j] == true && aOcean[i][j] == true){
+                    result.push_back(vector<int>{i,j});
+                }
+            }
+        }
+        return result;
+    }
+};
+

分析:仍然是比较普通的DFS,不过只要对周围的一圈进行DFS就足够了,不需要全部遍历。

+

错误:细节问题,写的时候一定好好检查 mn有没有用反。

+

回溯法

+

Leetcode 46

+

给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。

+
class Solution {
+public:
+    static void backtracking(vector<int>& nums,int level,vector<vector<int>>&result){
+        if(level == nums.size()-1){
+            result.push_back(nums);
+            return;
+        }
+        for(int i=level;i<nums.size();i++){
+            swap(nums[i],nums[level]);
+            backtracking(nums,level+1,result);
+            swap(nums[i],nums[level]);
+        }
+    }
+    vector<vector<int>> permute(vector<int>& nums) {
+        vector<vector<int>> result;
+        backtracking(nums,0,result);
+        return result;
+    }
+};
+

分析:对于每一个当前位置 i,我们可以将其于之后的任意位置交换,然后继续处理位置 i+1,直到处理到最后一位。为了防止我们每此遍历时都要新建一个子数组储存位置 i之前已经交换好的数字,我们可以利用回溯法,只对原数组进行修改,在递归完成后再修改回来。

+

错误:学习一下回溯法的基本框架。

+

Leetcode 77

+

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

+
class Solution {
+public:
+    static void backtracking(vector<vector<int>> &result,vector<int> &temp,int n,int level,int k){
+        if(temp.size() == k){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=level+1;i<=n;++i){
+            temp.push_back(i);
+            backtracking(result,temp,n,i,k);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> combine(int n, int k) {
+        vector<vector<int>> result;
+        vector<int> temp;
+        backtracking(result,temp,n,0,k);
+        return result;
+    }
+};
+

分析:类似于排列问题,也可以进行回溯。排列回溯的是交换的位置,而组合回溯的是是否把当前的数字加入结果中。

+

错误:需要有一个记录状态的数值,要不然就变成全排列了。

+

Leetcode 79

+

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

+
class Solution {
+public:
+    static void backtracking(vector<vector<char>>& board, string &word,int x,int y,int m,int n,vector<vector<bool>> &visited,bool &find,int level){
+        if(x < 0 || x >= m || y < 0 || y >= n){
+            return;
+        }
+        if(visited[x][y] == true || word[level] != board[x][y] || find == true){
+            return;
+        }
+        if(level == word.size() - 1){
+            find = true;
+            return;
+        }
+        visited[x][y] = true;
+        backtracking(board,word,x+1,y,m,n,visited,find,level+1);
+        backtracking(board,word,x-1,y,m,n,visited,find,level+1);
+        backtracking(board,word,x,y+1,m,n,visited,find,level+1);
+        backtracking(board,word,x,y-1,m,n,visited,find,level+1);
+        visited[x][y] = false;
+        return;
+    }
+    bool exist(vector<vector<char>>& board, string word) {
+        int m = board.size();
+        int n = board[0].size();
+        vector<vector<bool>> visited(m, vector<bool>(n, false));
+        bool find = false;
+        for(int i=0;i<m;++i){
+            for(int j=0;j<n;++j){
+                backtracking(board,word,i,j,m,n,visited,find,0);
+            }
+        }
+        return find;
+    }
+};
+

分析:典型回溯题,判断条件需要多一些

+

错误1:回溯法不要有返回值,都使用引用传参

+

错误2:判断条件:①是否越界②访问过③不匹配④已经确定对的了

+

Leetcode 51

+

n皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n ,返回所有不同的n皇后问题的解决方案。

+
class Solution {
+public:
+    void backtracking(vector<vector<string>> & result,vector<string> tempresult,vector<bool> &column,vector<bool> &ldiag,vector<bool> &rdiag,int n,int level){
+        if(level == n){
+            result.push_back(tempresult);
+            return;
+        }
+        for(int i=0;i<n;++i){
+            if (column[i] || ldiag[n-level+i-1] || rdiag[level+i]) {
+                continue;
+            }
+            tempresult[level][i] = 'Q';
+            column[i] = ldiag[n-level+i-1] = rdiag[level+i] = true;
+            backtracking(result,tempresult,column,ldiag,rdiag,n,level+1);
+            column[i] = ldiag[n-level+i-1] = rdiag[level+i] = false;
+            tempresult[level][i] = '.';
+        }
+        return;
+    }
+    vector<vector<string>> solveNQueens(int n) {
+        vector<vector<string>> result;
+        string tempstring = "";
+        for(int i=0;i<n;i++){
+            tempstring += ".";
+        }
+        vector<string> tempresult(n,tempstring);
+        vector<bool> column(n,false);
+        vector<bool> ldiag(2*n-1,false);
+        vector<bool> rdiag(2*n-1,false);
+        backtracking(result,tempresult,column,ldiag,rdiag,n,0);
+        return result;
+    }
+};
+

分析:最典型的回溯法之一。类似于在矩阵中寻找字符串,本题也是通过修改状态矩阵来进行回溯。不同的是,我们需要对每一行、列、左斜、右斜建立访问数组,来记录它们是否存在皇后。本题需要判断满足条件的结果中每一行或列有且仅有一个皇后。这是因为我们一共只有 n行和 n列。所以如果我们通过对每一行遍历来插入皇后,我们就不需要对行建立访问数组了。

+

错误:再理解吧。

+

广度优先搜索

+

Leetcode 934

+

在给定的二维二进制数组 A 中,存在两座岛。(岛是由四面相连的 1 形成的一个最大组。)现在,我们可以将 0 变为 1,以使两座岛连接起来,变成一座岛。返回必须翻转的 0 的最小数目。(可以保证答案至少是 1 。)

+
class Solution {
+public:
+    static void DFS(vector<vector<int>>& grid,queue<pair<int,int>> &points,int x,int y,int n){
+        if(x < 0 || x >= n || y < 0 || y >= n || grid[x][y] == 2){
+            return;
+        }
+        if(grid[x][y] == 0){
+            points.push({x,y});
+            return;
+        }
+        grid[x][y] = 2;
+        DFS(grid,points,x+1,y,n);
+        DFS(grid,points,x-1,y,n);
+        DFS(grid,points,x,y+1,n);
+        DFS(grid,points,x,y-1,n);
+        return;
+    }
+    int shortestBridge(vector<vector<int>>& grid) {
+        int n = grid.size();
+        queue<pair<int,int>> points;
+        bool find = false;
+        for(int i=0;i<n;++i){
+            if(find == true){
+                break;
+            }
+            for(int j=0;j<n;++j){
+                if(grid[i][j] == 1){
+                    find = true;
+                    DFS(grid,points,i,j,n);
+                    break;
+                }
+            }
+        }
+        int level = 0;
+        vector<vector<int>> d = {{0,1},{0,-1},{1,0},{-1,0}};
+        while(!points.empty()){
+            ++level;
+            int n_points = points.size();
+            while(n_points--){
+                auto [r,c] = points.front();
+                grid[r][c] = 2;
+                points.pop();
+                for(int k=0;k<4;k++){
+                    int x = r + d[k][0];
+                    int y = c + d[k][1];
+                    if(x >= 0 && y >= 0 && x < n && y < n){
+                        if(grid[x][y] == 1){
+                            return level;
+                        }
+                        else if(grid[x][y] == 0){
+                            grid[x][y] = 2;
+                            points.push({x,y});
+                        }
+                    }
+                }
+            }
+        }
+        return 0;
+    }
+};
+

分析:先通过任意搜索方法找到其中一个岛屿,然后利用广度优先搜索,查找其与另一个岛屿的最短距离

+

错误:BFS好久没有练习了,也是生疏了。

+

Leetcode 126

+

给定一个起始字符串和一个终止字符串,以及一个单词表,求是否可以将起始字符串每次改一个字符,直到改成终止字符串,且所有中间的修改过程表示的字符串都可以在单词表里找到。若存在,输出需要修改次数最少的所有更改方式。

+
class Solution {
+public:
+    vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &wordList) {
+        vector<vector<string>> res;
+        // 因为需要快速判断扩展出的单词是否在 wordList 里,因此需要将 wordList 存入哈希表,这里命名为「字典」
+        unordered_set<string> dict = {wordList.begin(), wordList.end()};
+        // 修改以后看一下,如果根本就不在 dict 里面,跳过
+        if (dict.find(endWord) == dict.end()) {
+            return res;
+        }
+        // 特殊用例处理
+        dict.erase(beginWord);
+
+        // 第 1 步:广度优先搜索建图
+        // 记录扩展出的单词是在第几次扩展的时候得到的,key:单词,value:在广度优先搜索的第几层
+        unordered_map<string, int> steps = {{beginWord, 0}};
+        // 记录了单词是从哪些单词扩展而来,key:单词,value:单词列表,这些单词可以变换到 key ,它们是一对多关系
+        unordered_map<string, set<string>> from = {{beginWord, {}}};
+        int step = 0;
+        bool found = false;
+        queue<string> q = queue<string>{{beginWord}};
+        int wordLen = beginWord.length();
+        while (!q.empty()) {
+            step++;
+            int size = q.size();
+            for (int i = 0; i < size; i++) {
+                const string currWord = move(q.front());
+                string nextWord = currWord;
+                q.pop();
+                // 将每一位替换成 26 个小写英文字母
+                for (int j = 0; j < wordLen; ++j) {
+                    const char origin = nextWord[j];
+                    for (char c = 'a'; c <= 'z'; ++c) {
+                        nextWord[j] = c;
+                        if (steps[nextWord] == step) {
+                            from[nextWord].insert(currWord);
+                        }
+                        if (dict.find(nextWord) == dict.end()) {
+                            continue;
+                        }
+                        // 如果从一个单词扩展出来的单词以前遍历过,距离一定更远,为了避免搜索到已经遍历到,且距离更远的单词,需要将它从 dict 中删除
+                        dict.erase(nextWord);
+                        // 这一层扩展出的单词进入队列
+                        q.push(nextWord);
+                        // 记录 nextWord 从 currWord 而来
+                        from[nextWord].insert(currWord);
+                        // 记录 nextWord 的 step
+                        steps[nextWord] = step;
+                        if (nextWord == endWord) {
+                            found = true;
+                        }
+                    }
+                    nextWord[j] = origin;
+                }
+            }
+            if (found) {
+                break;
+            }
+        }
+        // 第 2 步:回溯找到所有解,从 endWord 恢复到 beginWord ,所以每次尝试操作 path 列表的头部
+        if (found) {
+            vector<string> Path = {endWord};
+            backtrack(res, endWord, from, Path);
+        }
+        return res;
+    }
+
+    void backtrack(vector<vector<string>> &res, const string &Node, unordered_map<string, set<string>> &from,
+             vector<string> &path) {
+        if (from[Node].empty()) {
+            res.push_back({path.rbegin(), path.rend()});
+            return;
+        }
+        for (const string &Parent: from[Node]) {
+            path.push_back(Parent);
+            backtrack(res, Parent, from, path);
+            path.pop_back();
+        }
+    }
+};
+

分析:比较复杂的BFS+回溯法

+

错误:太复杂暂时还理解不了,慢慢来吧。。。

+

练习

+

Leetcode 130

+

给你一个 m x n 的矩阵 board ,由若干字符 'X''O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充。

+
class Solution {
+public:
+    static void DFS(vector<vector<char>>& board,int x,int y,int m,int n){
+        if(x < 0 || y < 0 || x >= m || y >= n){
+            return;
+        }
+        if(board[x][y] == 'X' || board[x][y] == 'A'){
+            return;
+        }
+        board[x][y] = 'A';
+        DFS(board,x+1,y,m,n);
+        DFS(board,x-1,y,m,n);
+        DFS(board,x,y-1,m,n);
+        DFS(board,x,y+1,m,n);
+        return;
+    }
+    void solve(vector<vector<char>>& board) {
+        int m = board.size();
+        int n = board[0].size();
+        for(int i=0;i<m;++i){
+            DFS(board,i,0,m,n);
+            DFS(board,i,n-1,m,n);
+        }
+        for(int i=0;i<n;++i){
+            DFS(board,0,i,m,n);
+            DFS(board,m-1,i,m,n);
+        }
+        for(int i=0;i<m;++i){
+            for(int j=0;j<n;++j){
+                if(board[i][j] == 'A'){
+                    board[i][j] = 'O';
+                }
+                else{
+                    board[i][j] = 'X';
+                }
+            }
+        }
+        return;
+    }
+};
+

分析:也是比较普通的DFS,注意记录是否访问过即可。

+

一遍AC

+

Leetcode 257

+

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

+
class Solution {
+public:
+    static void DFS(TreeNode* &root,vector<string> &paths,string temp){
+        if(root != nullptr){
+            temp += to_string(root->val);
+            if(root->left == nullptr && root->right == nullptr){
+                paths.push_back(temp);
+                return;
+            }
+            else{
+                temp += "->";
+                DFS(root->left,paths,temp);
+                DFS(root->right,paths,temp);
+            }
+        }
+    }
+    vector<string> binaryTreePaths(TreeNode* root) {
+        vector<string> paths;
+        DFS(root,paths,"");
+        return paths;
+    }
+};
+

分析:使用深度优先搜索。在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。如此,当遍历完整棵二叉树以后我们就得到了所有从根节点到叶子节点的路径。

+

错误:陷入回溯法的坑了。

+

Leetcode 47

+

给定一个可包含重复数字的序列 nums按任意顺序返回所有不重复的全排列。

+
class Solution {
+public:
+    static void backtracking(vector<int>& nums,vector<vector<int>> &result,int level){
+        if(level == nums.size() - 1){
+            result.push_back(nums);
+            return;
+        }
+        set<int> st;
+        for(int i=level;i<nums.size();++i){
+            if(st.find(nums[i]) == st.end()){
+                st.insert(nums[i]);
+                swap(nums[level],nums[i]);
+                backtracking(nums,result,level+1);
+                swap(nums[level],nums[i]);
+            }
+        }
+        return;
+    }
+    vector<vector<int>> permuteUnique(vector<int>& nums) {
+        vector<vector<int>> result;
+        backtracking(nums,result,0);
+        return result;
+    }
+};
+

分析:与全排列基本相同,添加一个set用于记录曾经交换过的数字,如果这个数字曾经交换过就不换了

+

错误:看了网上的思路。

+

Leetcode 40

+

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次

+
class Solution {
+public:
+    static void backtracking(vector<int>& candidates,vector<vector<int>> &result,vector<int> &path,vector<bool> &used,int level,int sum,int target){
+        if(sum > target){
+            return;
+        }
+        else if(sum == target){
+            result.push_back(path);
+            return;
+        }
+        else{
+            for(int i=level;i<candidates.size() && sum + candidates[i] <= target;++i){
+                if(i > 0 && candidates[i] == candidates[i-1] && used[i - 1] == false){
+                    continue;
+                }
+                sum += candidates[i];
+                path.push_back(candidates[i]);
+                used[i] = true;
+                backtracking(candidates,result,path,used,i+1,sum,target);
+                used[i] = false;
+                sum -= candidates[i];
+                path.pop_back();
+            }
+        }
+    }
+    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
+        vector<vector<int>> result;
+        vector<int> path;
+        vector<bool> used(candidates.size(),false);
+        sort(candidates.begin(),candidates.end());
+        backtracking(candidates,result,path,used,0,0,target);
+        return result;
+    }
+};
+

分析:还是组合数,但是数字内部有重复的,因此需要对同一树层上的“使用过”进行去重。

+

错误:没什么思路。

+

Leetcode 37

+

编写一个程序,通过填充空格来解决数独问题。

+
class Solution {
+public:
+    static bool isValid(int i,int j,char k,vector<vector<char>>& board){
+        set<char> st;
+        for(int x=0;x<9;x++){
+            st.insert(board[i][x]);
+            st.insert(board[x][j]);
+        }
+        int p = i / 3;
+        int q = j / 3;
+        for(int x = p*3;x < p*3+3;++x){
+            for(int y = q * 3;y < q*3+3;++y){
+                st.insert(board[x][y]);
+            }
+        }
+        if(st.find(k) == st.end()){
+            return true;
+        }
+        return false;
+    }
+    static bool backtracking(vector<vector<char>>& board){
+        for(int i=0;i<9;++i){
+            for(int j=0;j<9;++j){
+                if(board[i][j] != '.'){
+                    continue;
+                }
+                for(char k = '1';k <= '9';++k){
+                    if(isValid(i,j,k,board)){
+                        board[i][j] = k;
+                        if(backtracking(board) == true){
+                            return true;
+                        }
+                        board[i][j] = '.';
+                    }
+                }
+                return false;
+            }
+        }
+        return true;
+    }
+    void solveSudoku(vector<vector<char>>& board) {
+        bool judge = backtracking(board);
+        return;
+    }
+};
+

分析:二维的回溯问题,说白了就是去尝试填充每一个数字,合理就填上,不合理就删掉之前填充的重新进行尝试。

+

错误:看题解。

+

Leetcode 310

+

给你一棵包含 n 个节点的树,标记为 0n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 aibi 之间存在一条无向边。可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。

+
class Solution {
+public:
+    vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
+        int m = edges.size();
+        vector<int> result;
+        if(m == 0){
+            result.push_back(0);
+            return result;
+        }
+        vector<int> degree(n,0);
+        vector<vector<int>> tree(n);
+        for(int i=0;i<m;++i){
+            ++degree[edges[i][0]];
+            ++degree[edges[i][1]];
+            tree[edges[i][0]].push_back(edges[i][1]);
+            tree[edges[i][1]].push_back(edges[i][0]);
+        }
+        queue<int> q;
+        for(int i=0;i<n;++i){
+            if(degree[i] == 1){
+                q.push(i);
+            }
+        }
+        while(!q.empty()){
+            int size = q.size();
+            result.clear();
+            for(int i=0;i<size;++i){
+                int top = q.front();
+                result.push_back(top);
+                q.pop();
+                --degree[top];
+                for(int j=0;j<tree[top].size();++j){
+                    --degree[tree[top][j]];
+                    if(degree[tree[top][j]] == 1){
+                        q.push(tree[top][j]);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+};
+

分析:拓扑排序的思想,从多端同时BFS到中心点,直到到达最后一层,输出这一层的结点即为最小的高度。

+

错误:看了思路后自己实现,注意判断边界条件。

+

总结

+

深度优先、广度优先和回溯法,理解的还是并不是非常深入,今后还要多加练习。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第6章 搜索
+
https://zhangzhao219.github.io/2022/08/30/Leetcode/Leetcode-101/Leetcode-101-6/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月30日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/31/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-1/index.html b/2022/08/31/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-1/index.html new file mode 100644 index 000000000..cd48aa924 --- /dev/null +++ b/2022/08/31/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-1/index.html @@ -0,0 +1,971 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:模式识别与机器学习-第1章 概论 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:模式识别与机器学习-第1章 概论

+ + +
+ +

《模式识别与机器学习》课程笔记:第1章 概论

+ +
    +
  • 着重讲述模式识别与机器学习的基本概念,基本理论和方法,关键算法原理以及典型应用情况。
  • +
  • 注重理论与实践紧密结合 +
      +
    • 实例教学:通过实例讲述如何将所学知识运用到实际应用之中
    • +
    +
  • +
  • 尽量避免引用过多的、繁琐的数学推导
  • +
+

第1章 概论

+

什么是模式

+
    +
  • 广义地说,存在于时间和空间中可观察的物体,如果我们可以区别它们是否相同或是否相似,都可以称之为模式。
  • +
  • 模式所指的不是事物本身,而是从事物获得的信息,因此,模式往往表现为具有时间和空间分布的信息。
  • +
  • 模式的直观特性:①可观察性②可区分性③相似性
  • +
+

模式识别的目的:利用计算机对物理对象进行分类,在错误概率最小的条件下,使识别的结果尽量与客观物体相符合。

+

模式识别的数学化:Y= F(X)X的定义域取自特征集,Y的值域为类别的标号集,F是模式识别的判别方法。

+

机器学习:研究如何构造理论、算法和计算机系统,让机器通过从数据中学习后可以进行分类和识别事物、推理决策、预测未来等工作。

+

模式识别系统的目标

+

在特征空间和解释空间之间找到一种映射关系,这种映射也称之为假说。

+
    +
  • 特征空间:从模式得到的对分类有用的度量、属性或基元构成的空间。
  • +
  • 解释空间:c个类别的集合表示为Ω,称为解释空间。
  • +
+

机器学习的目标:针对某类任务 T,用 P衡量性能,根据经验 E来学习和自我完善,提高性能。

+

假说的两种获得方法:

+
    +
  • 监督学习、概念驱动或归纳假说:在特征空间中找到一个与解释空间的结构相对应的假说。在给定模式下假定一个解决方案,任何在训练集中接近目标的假说也都必须在“未知”的样本上得到近似的结果。 +
      +
    • 依靠已知所属类别的训练样本集,按它们特征向量的分布来确定假说(通常为一个判别函数),在判别函数确定之后能用它对未知的模式进行分类
    • +
    • 对分类的模式要有足够的先验知识,通常需要采集足够数量的具有典型性的样本进行训练。
    • +
    +
  • +
  • 非监督学习、数据驱动或演绎假说:在解释空间中找到一个与特征空间的结构相对应的假说。这种方法试图找到一种只以特征空间中的相似关系为基础的有效假说。 +
      +
    • 在没有先验知识的情况下,通常采用聚类分析方法,基于“物以类聚”的观点,用数学方法分析各特征向量之间的距离及分散情况;
    • +
    • 如果特征向量集聚集若干个群,可按群间距离远近把它们划分成类;
    • +
    • 这种按各类之间的亲疏程度的划分,若事先能知道应划分成几类,则可获得更好的分类结果。
    • +
    +
  • +
+

主要分类和学习方法

+

数据聚类

+
    +
  • 用某种相似性度量的方法将原始数据组织成有意义的和有用的各种数据集。
  • +
  • 是一种非监督学习的方法,解决方案是数据驱动的。
  • +
+

统计分类

+
    +
  • 基于概率统计模型得到各类别的特征向量的分布,以取得分类的方法。
  • +
  • 特征向量分布的获得是基于一个类别已知的训练样本集。
  • +
  • 是一种监督分类的方法,分类器是概念驱动的。
  • +
+

结构模式识别

+
    +
  • 该方法通过考虑识别对象的各部分之间的联系来达到识别分类的目的。
  • +
  • 识别采用结构匹配的形式,通过计算一个匹配程度值(matching score)来评估一个未知的对象或未知对象某些部分与某种典型模式的关系如何。
  • +
  • 当成功地制定出了一组可以描述对象部分之间关系的规则后,可以应用一种特殊的结构模式识别方法-句法模式识别,来检查一个模式基元的序列是否遵守某种规则,即句法规则或语法。
  • +
+

神经网络

+
    +
  • 神经网络是受人脑组织的生理学启发而创立的。
  • +
  • 由一系列互相联系的、相同的单元(神经元)组成。相互间的联系可以在不同的神经元之间传递增强或抑制信号。
  • +
  • 增强或抑制是通过调整神经元相互间联系的权重系数来(weight)实现。
  • +
  • 神经网络可以实现监督和非监督学习条件下的分类。
  • +
+

监督学习

+
    +
  • 监督学习是从有标记的训练数据来推断或建立一个模型,并依此模型推测新的实例。
  • +
  • 训练数据包括一套训练实例。在监督学习中,每个实例是由一个输入对象(通常为矢量)和一个期望的输出值(也称为监督信号)组成。
  • +
  • 一个最佳的模型将能够正确地决定那些看不见的实例的标签。常用于分类和回归。
  • +
+

无监督学习

+
    +
  • 无监督学习是我们不告诉计算机怎么做,而是让它自己去学习怎样做一些事情。
  • +
  • 无监督学习与监督学习的不同之处在于,事先没有任何训练样本,需要直接对数据进行建模,寻找数据的内在结构及规律,如类别和聚类。
  • +
  • 常用于聚类、概率密度估计。
  • +
+

半监督学习

+
    +
  • 半监督学习(Semi-supervised Learning)是模式识别和机器学习领域研究的重点问题,是监督学习与无监督学习相结合的一种学习方法。
  • +
  • 它主要考虑如何利用少量的标注样本和大量的未标注样本进行训练和分类的问题。
  • +
  • 半监督学习的主要算法有五类:基于概率的算法;在现有监督算法基础上改进的方法;直接依赖于聚类假设的方法;基于多视图的方法;基于图的方法。
  • +
+

强化学习

+
    +
  • 强化学习要解决的问题:一个能够感知环境的自治机器人,怎样通过学习选择能达到其目标的最优动作。
  • +
  • 机器人选择一个动作用于环境,环境接受该动作后状态发生变化,同时产生一个强化信号(奖或惩)反馈回来。
  • +
  • 机器人根据强化信号和环境当前状态再选择下一个动作,选择的原则是使受到正强化(奖)的概率增大。
  • +
+

集成学习

+
    +
  • 集成学习(Ensemble Learning)是机器学习中一类学习算法,指联合训练多个弱分类器并通过集成策略将弱分类器组合使用的方法。
  • +
  • 由于整合了多个分类器,这类算法通常在实践中会取得比单个若分类器更好的预测结果。
  • +
  • 常见的集成策略有:Boosting、Bagging、 Random subspace 、Stacking等。
  • +
  • 常见的算法主要有:决策树、随机森林、Adaboost、GBDT、DART等。
  • +
+

深度学习

+
    +
  • 深度学习的概念源于人工神经网络的研究,除输入层和输出层外,含多个隐藏层的神经网络就是一种深度学习结构。
  • +
  • 深度学习通过层次化模型结构可从低层原始特征中逐渐抽象出高层次的语义特征,以发现复杂、灵活、高效的特征表示。
  • +
  • 常见的深度学习模型有:卷积神经网络,递归神经网络,深度信任网络,自编码器,变分自编码器等。
  • +
+

元学习

+
    +
  • 元学习(Meta Learning)或者叫做“学会学习”(Learning to Learn),它是要“学会如何学习”,即利用以往的知识经验来指导新任务的学习,具有学会学习的能力。
  • +
  • 当前的机器学习模型往往只局限于从头训练已知任务并使用精调来学习新任务,耗时较长,且性能提升较为有限。
  • +
  • Meta Learning 就是研究如何让元模型记忆理解以往学习知识,使算法能在小样本训练的情况下完成新任务的学习。
  • +
+

多任务学习

+
    +
  • 多任务学习是指通过共享相关任务之间的表征,联合训练多个学习任务的学习范式。
  • +
  • 在通常的机器学习范式中,不同任务的学习过程往往分别处理,任务间的关系完全被割裂。而在多任务学习范式中,联系学习机制使不同任务的学习过程充分共享,可显著减少每个任务所需的训练样本。
  • +
  • 多任务学习的主要形式有:联合学习、自主学习和带有辅助任务的学习。
  • +
+

多标记学习

+
    +
  • 多标记学习问题为一种特殊的有监督分类问题,其所处理的数据集中的每个样本可同时存在多个真实类标。
  • +
  • 多标记学习主要用于处理多种标签的语义重叠,如预测歌曲的音乐流派,预测图书、商品的属性标签。
  • +
  • 多标记学习算法主要分为两类: +
      +
    • 问题转换法:把多标签问题转为其它学习场景,比如转为二分类、标签排序、多分类等。
    • +
    • 算法改编法:通过改编流行的学习算法去直接处理多标签数据,比如改编决策树、核技巧等。
    • +
    +
  • +
+

对抗学习

+
    +
  • 对抗学习是针对传统机器学习的一种攻击性方法,是机器学习和计算机安全领域都十分关注的交叉问题。
  • +
  • 对抗学习主要通过恶意输入来误导机器学习算法或模型使其得到错误结果,并在该过程中暴露机器学习算法存在的脆弱性,帮助设计适应复杂环境的鲁棒学习方法。
  • +
  • 常见的对抗学习方法主要有针对训练阶段的毒害式攻击以及针对测试阶段的躲避式攻击,常见的对抗学习场景主要有:垃圾邮件过滤、身份识别以及恶意软件检测等。
  • +
+

模式识别系统构成

+

模式识别系统与机器学习系统构成对比

+
v4Ot91.png
v4OUc6.png
+

模式识别系统组成单元

+
    +
  • 数据获取:用计算机可以运算的符号来表示所研究的对象 +
      +
    • 二维图像:文字、指纹、地图、照片等
    • +
    • 一维波形:脑电图、心电图、季节震动波形等
    • +
    • 物理参量和逻辑值:体温、化验数据、参量正常与否的描述
    • +
    +
  • +
  • 预处理单元:去噪声,提取有用信息,并对输入测量仪器或其它因素所造成的退化现象进行复原
  • +
  • 特征提取和选择:对原始数据进行变换,得到最能反映分类本质的特征 +
      +
    • 测量空间:原始数据组成的空间
    • +
    • 特征空间:分类识别赖以进行的空间
    • +
    • 模式表示:维数较高的测量空间->维数较低的特征空间
    • +
    +
  • +
  • 分类决策:在特征空间中用模式识别方法把被识别对象归为某一类别 +
      +
    • 基本做法:在样本训练集基础上确定某个判决规则,使得按这种规则对被识别对象进行分类所造成的错误识别率最小或引起的损失最小。
    • +
    +
  • +
+

机器学习系统组成单元

+
    +
  • 环境:是系统的工作对象(包括外界条件),代表信息来源。 +
      +
    • 信息水平:相对于执行环节要求而言,由学习环节消除差距
    • +
    • 信息质量:实例示教是否正确、实例次序是否合理等
    • +
    +
  • +
  • 知识库:存储学习到的知识 +
      +
    • 知识的表示要合理
    • +
    • 推理方法的实现不要太难
    • +
    • 存储的知识是否支持修改(更新)
    • +
    +
  • +
  • 学习环节:是系统的核心模块,是和外部环境的交互接口。 +
      +
    • 对环境提供的信息进行整理、分析、归纳或类比,生成新的知识单元,或修改知识库。
    • +
    • 接收从执行环节来的反馈信号,通过知识库修改,进一步改善执行环节的行为。
    • +
    +
  • +
  • 执行:根据知识库执行一系列任务 +
      +
    • 把执行结果或执行过程中获得的信息反馈给学习环节
    • +
    +
  • +
+

模式识别过程实例

+

在传送带上用光学传感器件对鱼按品种分类

+
    +
  1. 数据获取:架设一个摄像机,采集一些样本图像,获取样本数据
  2. +
  3. 预处理:去噪声,用一个分割操作把鱼和鱼之间以及鱼和背景之间分开
  4. +
  5. 特征提取和选择:对单个鱼的信息进行特征选择,从而通过测量某些特征来减少信息量
  6. +
  7. 分类决策:把特征送入决策分类器
  8. +
+

相关数学概念

+
    +
  • 随机向量及其分布 +
      +
    • 数学期望和方差
    • +
    • 协方差矩阵
    • +
    +
  • +
  • 正态分布 +
      +
    • 一维正态密度函数
    • +
    • 多维正态密度函数
    • +
    +
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:模式识别与机器学习-第1章 概论
+
https://zhangzhao219.github.io/2022/08/31/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年8月31日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/01/UCAS/advanced-ai/advanced-ai-1/index.html b/2022/09/01/UCAS/advanced-ai/advanced-ai-1/index.html new file mode 100644 index 000000000..fc1ddedc2 --- /dev/null +++ b/2022/09/01/UCAS/advanced-ai/advanced-ai-1/index.html @@ -0,0 +1,1228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第1讲 人工智能概述 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第1讲 人工智能概述

+ + +
+ +

《高级人工智能》课程笔记:第1讲 人工智能概述

+ +

首先讲授人工智能基础知识,进而分三个专题(联结主义、符号主义、行为主义)介绍人工智能的新进展。

+

第1讲 人工智能概述

+

智能和人工智能

+

智能:个体适应环境并能在不同环境中实现其目标的能力。

+

蕴含众多方面的能力

+
    +
  • 创造、推理、学习
  • +
  • 归纳、演绎、类比
  • +
  • 优化、规划、知识
  • +
  • 模式识别、问题求解
  • +
+

人工智能:

+
    +
  • 机器智能:使机器具备计算和“判别”的行为能力
  • +
  • 类脑智能:仿生智能,让机器像人或生物一样思考
  • +
  • 群体智能:社会智能的机器重现与利用、涌现智能
  • +
+

人工智能的发展历史

+

机械智能 ➡ 理性思考 ➡ 数理逻辑 ➡ 计算思维

+

萌芽期

+
    +
  • 机械自动化 +
      +
    • 希腊,蒸汽驱动的“会唱歌”的乌鸦
    • +
    • 中国,鲁班的“木鸢”,诸葛亮的“木牛流马”
    • +
    +
  • +
  • 逻辑推理 +
      +
    • 亚里士多德的“三段论”:从一般前提到具体论断
    • +
    +
  • +
+

孕育期(文艺复兴以来)

+
    +
  • 理性主义 +
      +
    • 笛卡尔:mind/body二象性,不相信机器会具有智能
    • +
    +
  • +
  • 数理逻辑学科 +
      +
    • 莱布尼茨:演算推论器,符号逻辑,提出将人的知识汇成“知识库”
    • +
    • 弗雷治:谓词演算
    • +
    +
  • +
  • 计算思维 +
      +
    • 巴贝奇:差分机
    • +
    • 图灵:图灵机
    • +
    +
  • +
+

形成期(1956年-1961年)

+
    +
  • 1956年,首次人工智能研讨会
  • +
  • IBM的西洋跳棋程序、文法体系、逻辑推理机、行动计划咨询系统、通用问题求解器
  • +
+

发展期(60年代)

+
    +
  • 研究领域拓展 +
      +
    • 问题求解、博弈、定理证明、程序设计、机器视觉、自然语言理解、知识表示、专家系统、神经网络、智能机器人……
    • +
    +
  • +
  • 1969年,第一届国际人工智能联合会议(IJCAI)
  • +
  • 1970年,《人工智能》国际杂志创刊,《Artificial Intelligence 》
  • +
+

寒冬期(60年代末到70年代初)

+
    +
  • 1966年,美国政府取消了机器翻译项目的所有投资
  • +
  • 英国政府取消了几乎所有人工智能研究投入
  • +
  • 神经网络的研究经费缩减到几乎没有
  • +
+

艰难前行(70年代)

+
    +
  • 弱方法:构建搜索机制,试图找出完全解 +
      +
    • 下棋:搜索解空间
    • +
    +
  • +
  • 强方法:构建领域知识库 +
      +
    • 专家系统:知识表示开始成为研究热点
    • +
    +
  • +
+

走向工业(80年代)

+
    +
  • 1982年,第一个商用专家系统RI
  • +
  • 1981年,日本启动“第五代计算机”计划,运行prolog语言的智能计算机
  • +
  • 美国、英国恢复对人工智能的投入
  • +
+

今天

+
    +
  • 大数据利用、计算能力提升、网络泛在化
  • +
  • 神经网络的复兴 +
      +
    • 多层感知机及其学习算法(BP算法)的提出
    • +
    • 隐马尔科夫模型(HMM)在语音识别上取得成功
    • +
    • 贝叶斯网络
    • +
    +
  • +
  • 专家系统逐渐成熟 +
      +
    • 知识发现、数据挖掘兴起
    • +
    +
  • +
  • 人工智能开始成为科学 +
      +
    • 学科边界开始明晰
    • +
    • 并开始借鉴其他学科的理论,如控制论、心里学、统计学
    • +
    +
  • +
+

人工智能:研究如何像人一样行动?

+

考试内容:图灵测试

+

Can Machine Think?

+

图灵测试:一个人(C)在完全不接触对方(A和B)的情况下,和对方进行一系列的问答,如果在相当长时间内,他无法根据这些问题判断对方是人(B)还是计算机(A),那么,就认为该计算机具有同人相当的智能(即计算机是能思维的)。

+

质疑:

+
    +
  • 图灵测试不是可构造的 +
      +
    • 例如:“完全不接触”的环境难以构建
    • +
    +
  • +
  • 图灵测试不是可重现的 +
      +
    • 例如:问题是开放的,答案正确性的判定是主观的
    • +
    +
  • +
  • 图灵测试无法进行数学分析 +
      +
    • 只是一种操作式测试,缺少形式化描述不严谨
    • +
    +
  • +
+

图灵预言:到2000年,机器可以做到5分钟内以30%的可能性让普通人分辨不出其是机器还是人。

+

图灵测试案例

+
    +
  • Master横空出世:Master在围棋对战网站上出现连胜30多场,才开始有人怀疑这是“机器人”。
  • +
  • 人工智能机器人Sophia:电视节目主持人查理•罗斯在节目《60分钟》中采访了Sophia机器人时,索菲亚不但对答如流,还与他开起了玩笑。
  • +
+

神经网络模拟器

+
    +
  • Snare:1951年由马文·明斯基提出,学习如何穿过迷宫
  • +
  • 他是多智能体的最早尝试者之一,使机器能基于过去行为的知识,预测其当前行为的结果
  • +
+

人工智能三大学派

+

达特茅斯会议:1956年在达特茅斯学院发起

+

发起人

+
    +
  • 约翰·麦卡锡(人工智能之父,Lisp语言发明者,1971年获图灵奖)
  • +
  • 马文·明斯基(1969年获图灵奖,首个获图灵奖的人工智能学者)
  • +
  • 克劳德·香农(信息论之父)
  • +
  • 纳撒尼尔·罗彻斯特(IBM 700系列计算机首席工程师,发明了首个汇编语言)
  • +
+

会议成就

+
    +
  • 首次提出了“人工智能”一词
  • +
  • 会议三大亮点 +
      +
    • 明斯基的Snare
    • +
    • 麦卡锡的𝛼-𝛽搜索法
    • +
    • 西蒙和纽厄尔的“逻辑理论家”
    • +
    +
  • +
+

并且出现了人工智能三大学派:

+
    +
  • 符号主义学派
  • +
  • 联结主义学派
  • +
  • 行为主义学派
  • +
+

符号主义学派(逻辑学派):规则驱动的确定性智能

+
    +
  • 认为“人的认知基元是符号,认知过程即符号操作过程
  • +
  • 认为人和计算机都是物理符号系统,可以用计算机来模拟人的智能行为
  • +
  • 认为人工智能的核心是知识表示、知识推理和知识运用
  • +
  • 代表人物 +
      +
    • 西蒙(1975年获图灵奖、1978年获诺贝尔经济学奖)
    • +
    • 纽厄尔
    • +
    +
  • +
+

衍生出:逻辑、专家系统、知识库

+

联结主义学派(仿生学派或生理学派):数据驱动的不确定性智能

+
    +
  • 认为人的思维基元是神经元,而不是符号处理过程
  • +
  • 认为人脑不同于电脑
  • +
  • 原理:神经网络及神经网络间的连接机制和学习算法
  • +
  • 代表人物 +
      +
    • 麦卡洛克(McCulloch)
    • +
    • 皮茨(Pitts)
    • +
    +
  • +
+

衍生出:人工神经网络、认知科学、类脑计算

+

行为主义学派(进化主义或控制论学派):交互驱动的涌现智能

+
    +
  • 认为智能取决于感知和行动
  • +
  • 主张利用机器对环境作用后的响应或反馈为原型来实现智能化
  • +
  • 认为人工智能可以像人类智能一样通过进化、学习来逐渐提高和增强
  • +
  • 代表人物:布鲁克斯
  • +
+

衍生出:控制论、多智能体、强化学习等

+

人工智能研究的课题

+

三大层次

+
    +
  • 基础理论:数学、思维科学、认知科学等
  • +
  • 原理技术:启发式搜索、演化计算
  • +
  • 工程应用:模式识别、计算机视觉、自然语言理解、问答系统
  • +
+

四大问题

+
    +
  • 知识科学、问题求解、机器学习、系统构成
  • +
+

人工智能之哲学基础

+

弱人工智能

+
    +
  • 机器表现得像具有智能一样
  • +
  • 图灵测试
  • +
+

强人工智能

+
    +
  • 机器实际具有智能
  • +
  • 机器具有自我意识吗?
  • +
  • 自由意志悖论 +
      +
    • 受物理法则严格支配的思想会是自由的吗?
    • +
    • 如果不能够说出我下一步会做什么,就说明我具有自由意志?
    • +
    +
  • +
+

人工智能恐慌

+
    +
  • 会不会造成人们失业? +
      +
    • 目前来看,人工智能技术带来的自动化,其创造的就业就会大于其减少的就业机会
    • +
    +
  • +
  • 对隐私权的侵害?
  • +
  • 是否导致可审计的丧失? +
      +
    • 例如:听从了医疗诊断专家系统的建议而带来的医疗事故,责任归谁?
    • +
    +
  • +
+

人工智能实现了会怎样?

+
    +
  • 人工智能的成功是否会意味着人类灭亡 +
      +
    • 人工演化取代自然选择
    • +
    • 机器智能一旦超过人类智能,他就能设计出更聪明的机器
    • +
    • 智力爆炸和技术奇点,人类时代的终结
    • +
    +
  • +
  • 怎么办? +
      +
    • 让机器保持可控
    • +
    • 使用人工智能拓展人类智能,将人工智能合并到人类智能中
    • +
    +
  • +
+

人工智能伦理

+
    +
  • 机器人三法则 +
      +
    • 第一法则:机器人不得伤害人类,或袖手旁观坐视人类受到伤害
    • +
    • 第二法则:除非违背第一法则,机器人必须服从人类的命令
    • +
    • 第三法则:在不违背第一及第二法则下,机器人必须保护自己
    • +
    +
  • +
+

人工智能的目标

+
    +
  • 近期目标 +
      +
    • 研究如何使机器做过去只有依靠人的智力才能完成的工作
    • +
    +
  • +
  • 远期目标 +
      +
    • 研究如何利用自动机模拟人的思维过程和智能行为,从而造出智能机器
    • +
    +
  • +
  • 终极目标 +
      +
    • 机器智能实现甚至超过生物智能
    • +
    +
  • +
+

“准人”水平的人工智能:手写识别、物体识别、语音识别、自然语言处理、词义消歧、机器翻译

+

“过人”水平的人工智能:游戏、双陆棋、国际象棋、桥牌、填词、拼字、七巧板、自动驾驶、智力竞赛问答、OCR字符识别

+

“许多尖端的人工智能由于应用广泛,已经不再被称为人工智能。因为,人们一旦觉得某些东西非常有用并广泛使用,就不再称之为人工智能了。”

+

人工智能案例实践

+
    +
  • 定理证明 +
      +
    • 50年代中期,西蒙和纽厄尔提出的“逻辑理论家”,证明了《数学原理》书中的38个定理
    • +
    • 1962年,改进后证明了书中全部52个定理,被认为是用计算机探讨人类智能的第一个真正成果
    • +
    +
  • +
  • 案例 +
      +
    • 四色定理 +
        +
      • 1852年提出,一直无人给出理论证明
      • +
      • 1976年6月,哈肯在伊利诺伊用两台计算机,用时1200个小时,通过100亿次判断,完成了证明,轰动世界
      • +
      +
    • +
    • 吴方法:吴文俊教授提出的“数学机器化”
    • +
    +
  • +
  • 通用问题求解器(GPS:General Problem Solver) +
      +
    • 1957年开始,纽厄尔等人开始研究不依赖于具体领域的通用解题程序
    • +
    • 模仿人类问题求解过程,第一个实现了“像人一样思考”的程序
    • +
    +
  • +
  • 专家系统 +
      +
    • 将领域专家的知识整理出来,让计算机利用这些知识求解专门领域的问题
    • +
    • DENDRAL:第一个专家系统,1968年问世,斯坦福大学完成,用于推断化学分子结构
    • +
    • MYCIN:著名的医疗诊断专家系统
    • +
    • RI:第一个商用专家系统,DEC公司于1982年正式使用
    • +
    +
  • +
  • 海湾战争中的专家系统 +
      +
    • 1991年的海湾战争,美国将专家系统用于后勤规划和运输日程安排
    • +
    • 涉及50000个车辆、货物和人,需要考虑起点、目的地、路径以及解决参数冲突问题
    • +
    • 该系统使一个计划可以在几个小时内产生,而旧方法需要几个星期
    • +
    +
  • +
  • 数字识别 +
      +
    • 清华大学智能技术与系统国家重点实验室采用神经元网络研制了数字识别系统
    • +
    • 用于2000年我国的人口普查
    • +
    • 错误率达到低于万分之一的水平
    • +
    +
  • +
  • 古籍数字化(OCR技术):《四库全书》
  • +
  • 国际象棋:IBM的“深蓝” +
      +
    • 1997年,IBM公司的“深蓝”在美国纽约公平大厦以3.5:2.5击败了国际象棋世界冠军卡斯帕罗夫
    • +
    +
  • +
  • 围棋 +
      +
    • AlphaGo: DeepMind +
        +
      • 使用深度学习技术(CNN:卷积神经网络)对棋局的局势进行估值
      • +
      • 在和其他围棋程序的对弈中取得99.8%的胜率
      • +
      • 和李世石的人机大战中以4:1取胜,在人机对战中60连胜,以3:0战胜柯洁
      • +
      +
    • +
    • AlphaGo背后的技术 +
        +
      • 深度学习(联结主义)+ 强化学习(行为主义)
      • +
      • 利用残差神经网络(ResNet)训练深度模型
      • +
      • 利用马尔科夫树搜索技术解决围棋的搜索空间爆炸问题
      • +
      • 采用**“自我对弈”**策略进行无人工标注的自我训练
      • +
      +
    • +
    +
  • +
  • 自动驾驶 +
      +
    • 在高速公路上,自动识别道路,自动躲避障碍物
    • +
    • 平均时速达到100公里/小时,最高速度可达150公里/小时
    • +
    • 从匹兹堡到圣地亚哥,98%的时间自动驾驶
    • +
    +
  • +
  • 自然语言处理 +
      +
    • 神经语言模型和词嵌入技术:word2vec
    • +
    • 机器翻译:统计机器翻译(SMT)到神经机器翻译(NMT)
    • +
    • 文本生成技术:给图像或视频加标题、聊天机器人、机器人写新闻报道、BERT和GPT-3
    • +
    +
  • +
  • 生成式预训练语言模型:GPT
  • +
  • IBM仿人脑芯片:TrueNorth +
      +
    • DARPA的研究项目SyNapse(自适应可塑可伸缩电子神经系统)的最新成果
    • +
    • 邮票大小、重量只有几克,集成54 亿个硅晶体管,内置4096 个内核,100 万个“神经元”、2.56 亿个“突触”,能力相当于一台超级计算机,功耗只有65 毫瓦
    • +
    • 目标:突破冯·诺依曼体系
    • +
    +
  • +
  • 脑科学 +
      +
    • 2013年1月,欧盟启动“人类大脑计划”
    • +
    • 2013年4月,奥巴马宣布启动“大脑基金计划”
    • +
    • 2014年,我国着手启动“脑科学计划”
    • +
    +
  • +
  • 互联网大脑:知识图谱+深度学习,利用网络大数据推断目标间的潜在关联关系等关系,为用户提供查询推荐、搜索导航等知识获取和深度理解功能。
  • +
  • 系统论 +
      +
    • 复杂自适应系统 +
        +
      • 1984年,美国圣塔菲研究所成立
      • +
      • 诺贝尔物理学将得主盖尔曼认为智能体现为个体的自适应能力,大量智能体(agent)积极地相互竞争和合作,在没有中央指挥的情况下,通过彼此相互作用和相互适应也能形成整体的有序状态
      • +
      +
    • +
    +
  • +
+

人工智能的今天

+
    +
  1. 自然语言理解(主战场之一):聊天机器人:小冰
  2. +
  3. 智能阅卷:安庆会考全学科智能阅卷
  4. +
  5. 考试机器人:美国华盛顿大学图灵中心和日本Todai高考机器人
  6. +
  7. 人工智能三级跳:运算智能(能存会算)➡感知智能(能听会说、能看会认)➡认知智能(能理解会思考)
  8. +
  9. 深度学习技术:DNN、RNN、CNN
  10. +
  11. 生物特征识别技术(刷脸、瞳仁、声纹……)
  12. +
  13. 中国创业公司:Face++
  14. +
+

人工智能的发展趋势

+
    +
  • 从“人机对抗”走向“人机协作” +
      +
    • AI 1.0 +
        +
      • 让机器在某些任务上“战胜”人
      • +
      +
    • +
    • AI 2.0:人本计算(human computation) +
        +
      • 让机器和人相互协作,完成更复杂的任务
      • +
      • 机器做机器擅长的:计算
      • +
      • 人做人擅长的:思考
      • +
      +
    • +
    +
  • +
  • 从单点智能走向网络智能 +
      +
    • AI 1.0 +
        +
      • 单个机器具备人的某些智能,例如:听、说、读、写、感知、认知……
      • +
      +
    • +
    • AI 2.0 +
        +
      • 借助互联网实现智能网络化
      • +
      +
    • +
    +
  • +
  • 从专用人工智能走向通用人工智能 +
      +
    • AI 1.0 +
        +
      • 在具体的任务上,让机器具备智能,例如:围棋、自动驾驶……
      • +
      +
    • +
    • AI 2.0 +
        +
      • 研究通用人工智能,包括探索智能形成的机制,AlphaGo到Master是一种初步尝试,让机器具备能够自我学习、形成概念的能力
      • +
      +
    • +
    +
  • +
+

人工智能是国家战略:2017年,国务院印发了《新一代人工智能发展规划》,人工智能成为国家战略,大数据在人工智能中将扮演越来越重要的角色。

+

人工智能经过60余年的发展取得了长足进步,近年来呈现出爆发之势,但总体上还处于初级阶段,通用智能之路任重道远。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第1讲 人工智能概述
+
https://zhangzhao219.github.io/2022/09/01/UCAS/advanced-ai/advanced-ai-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月1日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/02/Leetcode/Leetcode-101/Leetcode-101-7/index.html b/2022/09/02/Leetcode/Leetcode-101/Leetcode-101-7/index.html new file mode 100644 index 000000000..22d2fb203 --- /dev/null +++ b/2022/09/02/Leetcode/Leetcode-101/Leetcode-101-7/index.html @@ -0,0 +1,1477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第7章 动态规划 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第7章 动态规划

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第7章 动态规划

+ +

动态规划

+

动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。同时也可以对动态规划进行空间压缩,起到节省空间消耗的效果。

+

基本动态规划:一维

+

Leetcode 70

+

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

+
class Solution {
+public:
+    int climbStairs(int n) {
+        vector<int> num(n+1);
+        if(n <= 2){
+            return n;
+        }
+        else{
+            num[1] = 1;
+            num[2] = 2;
+            for(int i=3;i<=n;++i){
+                num[i] = num[i-1] + num[i-2];
+            }
+        }
+        return num[n];
+    }
+};
+

分析:num[i]表示在第 i阶的方法数,则到达第 i阶的方法是到达第 i-1阶的方法和到达第 i-2阶的方法数之和。因此 num[i] = num[i-1] + num[i-2]。判断边界条件即可。

+

一遍AC

+

Leetcode 198

+

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

+
class Solution {
+public:
+    int rob(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n+1);
+        if(n == 1){
+            return nums[0];
+        }
+        else if(n == 2){
+            return max(nums[0],nums[1]);
+        }
+        dp[1] = nums[0];
+        dp[2] = max(nums[0],nums[1]);
+        for(int i=3;i<=n;++i){
+            dp[i] = max(dp[i-1],dp[i-2] + nums[i-1]);
+        }
+        return dp[n];
+    }
+};
+

分析:定义一个数组 dpdp[i]表示抢劫到第i个房子时,可以抢劫的最大数量。我们考虑 dp[i],此时可以抢劫的最大数量有两种可能,一种是我们选择不抢劫这个房子,此时累计的金额即为 dp[i-1];另一种是我们选择抢劫这个房子,那么此前累计的最大金额只能是 dp[i-2]。因此本题的状态转移方程为 dp[i] = max(dp[i-1],nums[i-1] + dp[i-2])。然后判断边界条件即可。

+

一遍AC

+

Leetcode 413

+

给定一个数组,求这个数组中连续且等差的子数组一共有多少个

+
class Solution {
+public:
+    int numberOfArithmeticSlices(vector<int>& nums) {
+        int n = nums.size();
+        if(n == 1 || n == 2){
+            return 0;
+        }
+        vector<int>dp(n+1);
+        dp[0] = 0;
+        dp[1] = 0;
+        dp[2] = 0;
+        for(int i=2;i < n;++i){
+            if(nums[i] - nums[i-1] == nums[i-1] - nums[i-2]){
+                dp[i+1] = dp[i] + 1;
+            }
+        }
+        return accumulate(dp.begin(),dp.end(),0);
+    }
+};
+

分析:这道题略微特殊,因为要求是等差数列,可以很自然的想到子数组必定满足 num[i] - num[i-1] = num[i-1] - num[i-2]。然而由于我们对于 dp数组的定义通常为以 i结尾的,满足某些条件的子数组数量,而等差子数组可以在任意一个位置终结,因此此题在最后需要对 dp数组求和。

+

错误:最开始写的时候越界了

+

基本动态规划:二维

+

Leetcode 64

+

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

+
class Solution {
+public:
+    int minPathSum(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        vector<vector<int>> dp(m,vector<int>(n,0));
+        dp[0][0] = grid[0][0];
+        for(int i=1;i<m;++i){
+            dp[i][0] = dp[i-1][0] + grid[i][0];
+        }
+        for(int j=1;j<n;++j){
+            dp[0][j] = dp[0][j-1] + grid[0][j];
+        }
+        for(int i=1;i<m;++i){
+            for(int j=1;j<n;++j){
+                dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
+            }
+        }
+        return dp[m-1][n-1];
+    }
+};
+

分析:定义一个同样是二维的 dp数组,其中 dp[i][j]表示从左上角开始到 (i, j)位置的最优路径的数字和。因为每次只能向下或者向右移动,我们可以很容易得到状态转移方程 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j],其中 grid表示原数组。

+

错误:注意区间,开多大的 dp数组以及怎么进行状态转移,不要把自己转蒙。

+

Leetcode 542

+

给定一个由 01 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。两个相邻元素间的距离为 1

+
class Solution {
+public:
+    vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
+        int m = mat.size();
+        int n = mat[0].size();
+        vector<vector<int>> dp(m,vector<int>(n,0));
+        if(mat[0][0] == 0){
+            dp[0][0] = 0;
+        }
+        else{
+            dp[0][0] = 10002;
+        }
+
+        for(int i=1;i<m;++i){
+            if(mat[i][0] == 0){
+                dp[i][0] = 0;
+            }
+            else{
+                dp[i][0] = dp[i-1][0] + 1;
+            }
+        }
+        for(int j=1;j<n;++j){
+            if(mat[0][j] == 0){
+                dp[0][j] = 0;
+            }
+            else{
+                dp[0][j] = dp[0][j-1] + 1;
+            }
+        }
+        for(int i=1;i<m;++i){
+            for(int j=1;j<n;++j){
+                if(mat[i][j] == 0){
+                    dp[i][j] = 0;
+                }
+                else{
+                    dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + 1;
+                }
+            }
+        }
+        for(int i=m-2;i>=0;--i){
+            if(mat[i][n-1] == 0){
+                dp[i][n-1] = 0;
+            }
+            else{
+                dp[i][n-1] = min(dp[i][n-1],dp[i+1][n-1] + 1);
+            }
+        }
+        for(int j=n-2;j>=0;--j){
+            if(mat[m-1][j] == 0){
+                dp[m-1][j] = 0;
+            }
+            else{
+                dp[m-1][j] = min(dp[m-1][j],dp[m-1][j+1] + 1);
+            }
+        }
+        for(int i=m-2;i>=0;--i){
+            for(int j=n-2;j>=0;--j){
+                if(mat[i][j] != 0){
+                    dp[i][j] = min(dp[i][j],min(dp[i+1][j],dp[i][j+1])+1);
+                }
+            }
+        }
+        return dp;
+    }
+};
+

分析:从左上到右下进行一次动态搜索,再从右下到左上进行一次动态搜索。两次动态搜索即可完成四个方向上的查找。

+

错误:看了一下题解的思路,还是有点不敢想。另外要细心,注意越界!!!

+

Leetcode 221

+

在一个由 '0''1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

+
class Solution {
+public:
+    int maximalSquare(vector<vector<char>>& matrix) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        vector<vector<int>> dp(m+1,vector<int>(n+1,0));
+        int maxside = 0;
+        for(int i=1;i<=m;++i){
+            for(int j=1;j<=n;++j){
+                if(matrix[i-1][j-1] == '1'){
+                    dp[i][j] = min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j])) + 1;
+                }
+                maxside = max(maxside,dp[i][j]);
+            }
+        }
+        return maxside * maxside;
+    }
+};
+

分析:dp[i][j]表示以 (i, j)为右下角的全由 1构成的最大正方形边长。

+

错误:状态转移方程没有想太好。

+

分割类型题

+

对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。

+

Leetcode 279

+

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

+
class Solution {
+public:
+    int numSquares(int n) {
+        vector<int> dp(n+1,100000000);
+        dp[0] = 0;
+        for(int i=1;i<=n;++i){
+            for(int j=1;j*j<=i;++j){
+                dp[i] = min(dp[i],dp[i-j*j]+1);
+            }
+        }
+        return dp[n];
+    }
+};
+

分析:dp[i]表示数字 i最少可以由几个完全平方数相加构成。

+

错误:没有思路

+

Leetcode 91

+

输入是一个由数字组成的字符串,输出是满足条件的解码方式总数。

+
class Solution {
+public:
+    int numDecodings(string s) {
+        int n = s.size();
+        if(s[0] == '0'){
+            return 0;
+        }
+        if(n == 1){
+            return 1;
+        }
+        vector<int>dp(n+1,1);
+        int prev = s[0] - '0';
+        for(int i=2;i<=n;++i){
+            int cur = s[i-1] - '0';
+            if((prev == 0 || prev > 2) && cur == 0){
+                return 0;
+            }
+            if((prev == 1) || (prev == 2 && cur < 7)){
+                if(cur){
+                    dp[i] = dp[i-1] + dp[i-2];
+                }
+                else{
+                    dp[i] = dp[i-2];
+                }
+            }
+            else{
+                dp[i] = dp[i-1];
+            }
+            prev = cur;
+        }
+        return dp[n];
+    }
+};
+

分析:dp[i]表示以当前第i个位置上的数字为结尾的表示方法总数。dp[i]取决于两个数字,当前的数字和前一个数字。如果当前数字是 0,而前一个数字不是 1或者 2,说明这两个数字不可能构成字符,因此直接返回 0。如果前一个数字是 1,当前的数字是什么都行,或者前一个数字是 2,而当前的数字是 0-6的某一个数,说明这两个能构成一种组合。同时如果当前的数字不是 0,那么这个数字自己也能构成一种。如果前一个数字是其他,说明不能和当前的数字产生关系了,就只能是当前的数字自己了。

+

错误:不明白

+

Leetcode 139

+

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s

+
class Solution {
+public:
+    bool wordBreak(string s, vector<string>& wordDict) {
+        int n = s.size();
+        vector<bool> dp(n+1,false);
+        dp[0] = true;
+        for(int i=1;i<=n;++i){
+            for(const string & word : wordDict){
+                int len = word.size();
+                if(i >= len && s.substr(i-len,len) == word){
+                    dp[i] = dp[i] || dp[i-len];
+                }
+            }
+        }
+        return dp[n];
+    }
+};
+

分析:类似于完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置0,需要初始化值为真。

+

子序列问题

+

对于子序列问题,第一种动态规划方法是,定义一个 dp数组,其中 dp[i]表示以 i结尾的子序列的性质。在处理好每个位置后,统计一遍各个位置的结果即可得到题目要求的结果。第二种动态规划方法是,定义一个 dp数组,其中 dp[i]表示到位置 i为止的子序列的性质,并不必须以 i结尾。这样 dp数组的最后一位结果即为题目所求,不需要再对每个位置进行统计。

+

Leetcode 300

+

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

+
class Solution {
+public:
+    int lengthOfLIS(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n+1,1);
+        dp[0] = 0;
+        for(int i=1;i<=n;++i){
+            for(int j=0;j<i;j++){
+                if(nums[i-1] > nums[j]){
+                    dp[i] = max(dp[i],dp[j+1]+1);
+                }
+            }
+        }
+        return *max_element(dp.begin(),dp.end());
+    }
+};
+

分析: dp[i]表示以 i结尾的子序列的性质。简单动态规划即可。

+

错误:下标指代不清,初始化应该全部为1

+

Leetcode 1143

+

给定两个字符串 text1text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0

+
class Solution {
+public:
+    int longestCommonSubsequence(string text1, string text2) {
+        int m = text1.size();
+        int n = text2.size();
+        vector<vector<int>> dp(m+1,vector<int>(n+1,0));
+        for(int i=1;i<=m;++i){
+            for(int j=1;j<=n;++j){
+                if(text1[i-1] == text2[j-1]){
+                    dp[i][j] = dp[i-1][j-1] + 1;
+                }
+                else{
+                    dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

分析:建立一个二维数组 dp,其中 dp[i][j]表示到第一个字符串位置 i为止、到第二个字符串位置 j为止、最长的公共子序列长度。

+

错误:没想到是二维的动态规划。

+

背包问题

+

给定一个正整数数组,求是否可以把这个数组分成和相等的两部分。

+
class Solution {
+public:
+    bool canPartition(vector<int>& nums) {
+        int n = nums.size();
+        int sum = accumulate(nums.begin(),nums.end(),0);
+        if(sum % 2 == 1){
+            return false;
+        }
+        sum /= 2;
+        vector<vector<int>> dp(n+1,vector<int>(sum+1,false));
+        dp[0][0] = true;
+        for(int i=1;i<=n;++i){
+            for(int j=0;j<=sum;++j){
+                if(j < nums[i-1]){
+                    dp[i][j] = dp[i-1][j];
+                }
+                else{
+                    dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
+                }
+            }
+        }
+        return dp[n][sum];
+    }
+};
+

分析:背包问题,价值是一半,背包容量没有限制。比较重要的是 dp[0][0] =true,后续的判断都是从这个 true继承过来的。

+

错误:思路不够完善

+

Leetcode 474

+

给你一个二进制字符串数组 strs 和两个整数 mn ,请你找出并返回 strs 的最大子集的长度,该子集中最多m0n1

+
class Solution {
+public:
+    static vector<int> getzerosandones(string &str){
+        vector<int> result(2);
+        int n = str.size();
+        for(int i=0;i<n;++i){
+            if(str[i] == '0'){
+                ++result[0];
+            }
+            else{
+                ++result[1];
+            }
+        }
+        return result;
+    }
+    int findMaxForm(vector<string>& strs, int m, int n) {
+        int l = strs.size();
+        vector<vector<vector<int>>> dp(l+1,vector<vector<int>>(m+1,vector<int>(n+1,0)));
+        for(int i=1;i<=l;++i){
+            vector<int> && zerosones = getzerosandones(strs[i-1]);
+            int zero = zerosones[0];
+            int one = zerosones[1];
+            for(int j=0;j<=m;++j){
+                for(int k=0;k<=n;++k){
+                    dp[i][j][k] = dp[i-1][j][k];
+                    if(j >= zero && k >= one){
+                        dp[i][j][k] = max(dp[i][j][k],dp[i-1][j-zero][k-one]+1);
+                    }
+                }
+            }
+        }
+        return dp[l][m][n];
+    }
+};
+

分析:三维的背包问题,要同时考虑两个背包的容量。

+

错误:还是不理解

+

Leetcode 322

+

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1 ,可以认为每种硬币的数量是无限的。

+
class Solution {
+public:
+    int coinChange(vector<int>& coins, int amount) {
+        int n = coins.size();
+        vector<vector<int>> dp(n+1,vector<int>(amount+1,amount+1));
+        dp[0][0] = 0;
+        for(int i=0;i<=amount;++i){
+            for(int j=1;j<=n;++j){
+                if(coins[j-1] <= i){
+                    dp[j][i] = min(dp[j-1][i],dp[j][i-coins[j-1]]+1);
+                }
+                else{
+                    dp[j][i] = dp[j-1][i];
+                }
+            }
+        }
+        return dp[n][amount] == amount+1 ? -1 : dp[n][amount];
+    }
+};
+

分析:完全背包问题。

+

错误:就是不理解

+

字符串编辑

+

Leetcode 72

+

给定两个字符串,已知你可以删除、替换和插入任意字符串的任意字符,求最少编辑几步可以将两个字符串变成相同。

+
class Solution {
+public:
+    int minDistance(string word1, string word2) {
+        int m = word1.size();
+        int n = word2.size();
+        vector<vector<int>> dp(m+1,vector<int>(n+1,0));
+        for(int i=0;i<=m;++i){
+            dp[i][0] = i;
+        }
+        for(int j=0;j<=n;++j){
+            dp[0][j] = j;
+        }
+        for(int i=1;i<=m;++i){
+            for(int j=1;j<=n;++j){
+                if(word1[i-1] == word2[j-1]){
+                    dp[i][j] = dp[i-1][j-1];
+                }
+                else{
+                    dp[i][j] = min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j])) + 1;
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

分析:使用一个二维数组 dp[i][j],表示将第一个字符串到位置 i为止,和第二个字符串到位置 j为止,最多需要几步编辑。当第 i位和第 j位对应的字符相同时,dp[i][j]等于 dp[i-1][j-1];当二者对应的字符不同时,修改的消耗是 dp[i-1][j-1]+1,插入 i位置/删除 j位置的消耗是 dp[i][j-1] + 1,插入 j位置/删除 i位置的消耗是 dp[i-1][j] + 1

+

错误:初始化没有做好。

+

Leetcode 650

+

给定一个字母A,已知你可以每次选择复制全部字符,或者粘贴之前复制的字符,求最少需要几次操作可以把字符串延展到指定长度。

+
class Solution {
+public:
+    int minSteps(int n) {
+        vector<int> dp(n+1,0);
+        for(int i=2;i<=n;++i){
+            dp[i] = i;
+            for(int j=2;j * j <= i;++j){
+                if(i % j == 0){
+                    dp[i] = dp[j] + dp[i/j];
+                    break;
+                }
+            }
+        }
+        return dp[n];
+    }
+};
+

分析:我们使用一个一维数组dp,其中位置i表示延展到长度i的最少操作次数。对于每个位置j,如果j可以被i整除,那么长度i就可以由长度j操作得到,其操作次数等价于把一个长度为1的A延展到长度为i/j。因此我们可以得到递推公式dp[i] = dp[j] + dp[i/j]

+

错误:还是不会想。

+

Leetcode 10

+

给定一个字符串和一个正则表达式,求该字符串是否可以被匹配。

+
class Solution {
+public:
+    bool isMatch(string s, string p) {
+        int m = s.size();
+        int n = p.size();
+        vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
+        dp[0][0] = true;
+        for(int i=1;i<=n;++i){
+            if(p[i-1] == '*'){
+                dp[0][i] = dp[0][i-2];
+            }
+        }
+        for(int i=1;i<=m;++i){
+            for(int j=1;j<=n;++j){
+                if(p[j-1] == '.'){
+                    dp[i][j] = dp[i-1][j-1];
+                }
+                else if (p[j-1] != '*') {
+                    dp[i][j] = dp[i-1][j-1] && p[j-1] == s[i-1];
+                }
+                else if (p[j-2] != s[i-1] && p[j-2] != '.') {
+                    dp[i][j] = dp[i][j-2];
+                } 
+                else {
+                    dp[i][j] = dp[i][j-1] || dp[i-1][j] || dp[i][j-2];
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

分析:使用一个二维数组 dp,其中 dp[i][j]表示以 i截止的字符串是否可以被以 j截止的正则表达式匹配。

+

错误:没有思路

+

股票交易

+

Leetcode 121

+

给定一段时间内每天某只股票的固定价格,已知你只可以买卖各一次,求最大的收益。

+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        int sell = 0, buy = INT_MIN;
+        for (int i = 0; i < prices.size(); ++i) {
+            buy = max(buy, -prices[i]);
+            sell = max(sell, buy + prices[i]);
+        }
+        return sell;
+    }
+};
+

分析:遍历一次就行,记录一下最小的价格,然后遍历到每个价格的时候看看是不是比这个价格更大就行了。

+

错误:简单的问题也不会想了。。。

+

Leetcode 188

+

给定一段时间内每天某只股票的固定价格,已知你只可以买卖各 k次,且每次只能拥有一支股票,求最大的收益。

+

Leetcode 309

+

给定一段时间内每天某只股票的固定价格,已知每次卖出之后必须冷却一天,且每次只能拥有一支股票,求最大的收益。

+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        int n = prices.size();
+        if(n == 0){
+            return 0;
+        }
+        vector<int> buy(n),sell(n),s1(n),s2(n);
+        s1[0] = buy[0] = -prices[0];
+        sell[0] = s2[0] = 0;
+        for(int i=1;i<n;++i){
+            buy[i] = s2[i-1] - prices[i];
+            s1[i] = max(buy[i-1],s1[i-1]);
+            sell[i] = max(buy[i-1],s1[i-1]) + prices[i];
+            s2[i] = max(s2[i-1],sell[i-1]);
+        }
+        return max(sell[n-1],s2[n-1]);
+    }
+};
+

分析:状态机求解

+

错误:完全不懂

+

练习

+

Leetcode 213

+

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

+
class Solution {
+public:
+    int rob(vector<int>& nums) {
+        int n = nums.size();
+        if(n == 1){
+            return nums[0];
+        }
+        else if(n == 2){
+            return max(nums[0],nums[1]);
+        }
+        vector<int> dp(n+1);
+        int answer_a;
+        dp[0] = dp[1] = dp[2] = nums[0];
+        for(int i=3;i<n;++i){
+            dp[i] = max(dp[i-1],dp[i-2] + nums[i-1]);
+        }
+        answer_a = dp[n-1];
+        dp[0] = dp[1] = 0;
+        dp[2] = nums[1];
+        for(int i=3;i<=n;++i){
+            dp[i] = max(dp[i-1],dp[i-2] + nums[i-1]);
+        }
+        return max(answer_a,dp[n]);
+    }
+};
+

分析:分两种情况进行讨论,选第一个和不选第一个。

+

错误:看了一下思路,最后调通了

+

Leetcode 53

+

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

+
class Solution {
+public:
+    int maxSubArray(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n+1);
+        dp[0] = -20000;
+        for(int i=1;i<=n;++i){
+            dp[i] = max(nums[i-1],dp[i-1] + nums[i-1]);
+        }
+        return *max_element(dp.begin(),dp.end());
+    }
+};
+

分析:dp数组记录以当前位置为结尾的子数组的最大和,因此后面再加一位有两种可能,一是和这个一起,二是自己一组。最后取最大的部分即可。

+

错误:开始没想太懂,后来自己调通了。

+

Leetcode 343

+

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

+
class Solution {
+public:
+    int integerBreak(int n) {
+        vector<int> dp(n+1);
+        for(int i=2;i<=n;++i){
+            for(int j=1;j<i;++j){
+                dp[i] = max(dp[i],max(j*(i-j),j*dp[i-j]));
+            }
+        }
+        return dp[n];
+    }
+};
+

分析:对于正整数n,当n≥2时,可以拆分成至少两个正整数的和。令x是拆分出的第一个正整数,则剩下的部分是n-x,n−x可以不继续拆分,或者继续拆分成至少两个正整数的和。每个正整数对应的最大乘积取决于比它小的正整数对应的最大乘积。

+

错误:分割问题还是没有什么思路

+

Leetcode 583

+

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的 最小步数 。每步可以删除任意一个字符串中的一个字符。

+
class Solution {
+public:
+    int minDistance(string word1, string word2) {
+        int m = word1.size();
+        int n = word2.size();
+        vector<vector<int>> dp(m+1,vector<int>(n+1,0));
+        for(int i=0;i<=m;++i){
+            dp[i][0] = i;
+        }
+        for(int i=0;i<=n;++i){
+            dp[0][i] = i;
+        }
+        for(int i=1;i<=m;++i){
+            for(int j=1;j<=n;++j){
+                if(word1[i-1] == word2[j-1]){
+                    dp[i][j] = dp[i-1][j-1];
+                }
+                else{
+                    dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + 1;
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

分析:不相等的时候看两边的字符串,相等的时候看前一位

+

错误:字符相等的时候有些没想明白,后来调通了

+

Leetcode 646

+

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。现在,我们定义一种跟随关系,当且仅当 b < c 时,数对 (c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

+
class Solution {
+public:
+    static bool cmp(vector<int> &a,vector<int> &b){
+        return a[0] < b[0];
+    }
+    int findLongestChain(vector<vector<int>>& pairs) {
+        int n = pairs.size();
+        sort(pairs.begin(),pairs.end(),cmp);
+        vector<int> dp(n+1,1);
+        for(int i=1;i<=n;++i){
+            for(int j=0;j<i-1;++j){
+                if(pairs[i-1][0] > pairs[j][1]){
+                    dp[i] = max(dp[i],dp[j+1]+1);
+                }
+            }
+        }
+        return dp[n];
+    }
+};
+

分析:排序后进行动态规划即可

+

错误:排序有问题。

+

Leetcode 376

+

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。给你一个整数数组 nums ,返回 nums 中作为摆动序列的最长子序列的长度

+
class Solution {
+public:
+    int wiggleMaxLength(vector<int>& nums) {
+        int n = nums.size();
+        if(n == 1 || (n == 2 && nums[0] != nums[1])){
+            return n;
+        }
+        if(n == 2 && nums[0] == nums[1]){
+            return 1;
+        }
+        vector<int> up(n),down(n);
+        up[0] = down[0] = 1;
+        for(int i=1;i<n;++i){
+            if(nums[i] > nums[i-1]){
+                up[i] = max(up[i-1],down[i-1] + 1);
+                down[i] = down[i-1];
+            }
+            else if(nums[i] < nums[i-1]){
+                up[i] = up[i-1];
+                down[i] = max(down[i-1],up[i-1] + 1);
+            }
+            else{
+                up[i] = up[i-1];
+                down[i] = down[i-1];
+            }
+        }
+        return max(up[n-1],down[n-1]);
+    }
+};
+

分析:每当我们选择一个元素作为摆动序列的一部分时,这个元素要么是上升的,要么是下降的,这取决于前一个元素的大小。那么列出状态表达式为:up[i]表示以前 i个元素中的某一个为结尾的最长的「上升摆动序列」的长度。down[i]表示以前i个元素中的某一个为结尾的最长的「下降摆动序列」的长度。

+

错误:没有思路

+

Leetcode 494

+
class Solution {
+public:
+    int findTargetSumWays(vector<int>& nums, int target) {
+        int sum = 0;
+        for (int& num : nums) {
+            sum += num;
+        }
+        int diff = sum - target;
+        if (diff < 0 || diff % 2 != 0) {
+            return 0;
+        }
+        int n = nums.size(), neg = diff / 2;
+        vector<vector<int>> dp(n + 1, vector<int>(neg + 1));
+        dp[0][0] = 1;
+        for (int i = 1; i <= n; i++) {
+            int num = nums[i - 1];
+            for (int j = 0; j <= neg; j++) {
+                dp[i][j] = dp[i - 1][j];
+                if (j >= num) {
+                    dp[i][j] += dp[i - 1][j - num];
+                }
+            }
+        }
+        return dp[n][neg];
+    }
+};
+

分析:转化为0-1背包问题

+

错误:背包问题一直都不怎么理解,就先这样,后续再补充。

+

Leetcode 714

+

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

+
class Solution {
+public:
+    int maxProfit(vector<int>& prices, int fee) {
+        int n = prices.size();
+        vector<vector<int>> dp(n, vector<int>(2));
+        dp[0][0] = 0, dp[0][1] = -prices[0];
+        for (int i = 1; i < n; ++i) {
+            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
+            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
+        }
+        return dp[n - 1][0];
+    }
+};
+

分析:股票问题的变形,比较类似于状态机,不是很能想得到

+

错误:股票问题后面也要再做一做

+

总结

+

动态规划比较有难度,一是状态转移方程的写法,二是在实现状态转移中的各种细节。以后对于动态规划还要勤加练习,多练习思考方法。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第7章 动态规划
+
https://zhangzhao219.github.io/2022/09/02/Leetcode/Leetcode-101/Leetcode-101-7/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/02/UCAS/machine-learning/machine-learning-1/index.html b/2022/09/02/UCAS/machine-learning/machine-learning-1/index.html new file mode 100644 index 000000000..49783db8f --- /dev/null +++ b/2022/09/02/UCAS/machine-learning/machine-learning-1/index.html @@ -0,0 +1,848 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第1章 绪论 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第1章 绪论

+ + +
+ +

《机器学习》课程笔记:第1章 绪论

+ +
    +
  • 了解机器学习研究问题 +
      +
    • 有监督学习:分类、回归
    • +
    • 无监督学习:聚类、降维、特征提取等;
    • +
    +
  • +
  • 掌握基本的统计和优化方法 +
      +
    • 统计学习基础:最大似然估计、最小均方等;
    • +
    • 优化基础:梯度下降 、随机梯度下降等;
    • +
    +
  • +
  • 掌握机器学习的基础理论和算法 +
      +
    • Bayes、 SVM、鉴别分析、 logistic、决策树、感知机、多层感知机、 Adaboost、线性回归、kmeans、 PCA、 概率图模型、知识图谱、深度学习及前沿等;
    • +
    +
  • +
  • 能够针对任务设计机器学习方案
  • +
+

第1章 绪论

+

机器学习研究背景:人工智能

+

什么是人工智能?

+

“人工智能就是让机器来完成那些如果由来做则需要智能的事情的科学”;

+

“人工智能就是研究如何使计算机去做只有才能做的智能工作

+

“人工智能是研究使计算机来模拟人的某些思维过程和智能行为 (如学习、推理、思考、规划等)的学科 ”

+

图灵测试思考的问题:

+
    +
  • 人的智能非常复杂: 例如 直觉 、顿悟、理解,等等
  • +
  • 人的智能具有“人”性:例如 情绪、伪装、狡猾,等等;
  • +
  • 人的智能缺陷:不依赖于数学工具,无法实现高难度、大规模的运算;不依赖于词典和存储工具,信息的记忆量、精准性有限;
  • +
+

我们研究的是弱人工智能

+

人工智能的发展

+
    +
  • 孕育期(~1956):1950 年图灵测试
  • +
  • 推理期(1956~1965):1956 年逻辑理论家程序、 1960 年 Lisp 语言
  • +
  • 知识期(1965~1983):1965 年分子结构的专家系统 DENDRAL、1972年细菌感染专家系统MYCIN
  • +
  • 学习期(1983~2006):解决知识工程瓶颈, 统计机器学习主导
  • +
  • 黄金期(2006~):以深度学习为 代表的人工智能核心技术不断取得新突破
  • +
+

对人工智能的期望

+
    +
  • 在人工智能的第一波中,你必须成为一名程序员;
  • +
  • 在人工智能的第二次浪潮中,你必须是一名数据科学家;
  • +
  • 人工智能的第三次浪潮,你越道德越好。。。
  • +
+

人工智能创新发展引领新一轮产业变革之势,推动人类社会进入智能化时代,人工智能成为世界各国竞相战略布局的新高地,我国人工智能综合实力不断提升。

+

机器学习的发展

+

机器学习是一门人工智能的科学

+

“机器学习是一门人工智能的科学,该领域的主要研究对象是人工智能,特别是如何在经验学习中改善具体算法的性能 。 Langley(1996)“

+

“机器学习是对能通过经验自动改进的计算机算法的研究 。 Tom Mitchell (1997)“

+

“机器学习是用数据或以往的经验,以此优化计算机程序的性能标准”。 Alpaydin (2004)

+

机器学习发展时期

+

推理期➡知识期➡学科形成➡蓬勃发展期

+

应用领域

+
    +
  • 航空航天、军事、国防
  • +
  • 机器人、无人车、 NASA-JPL 火星机器人
  • +
  • 互联网应用
  • +
  • 信息安全
  • +
  • 生物信息学
  • +
  • 天气预报、地震预警、环境污染检测
  • +
  • 智能识别
  • +
  • 金融、经贸、管理 、 公共安全 、 医学 、 交通 、
  • +
+

机器学习研究意义

+
    +
  • 机器学习是人工智能的基石
  • +
  • 机器学习引领人工智能的前沿
  • +
  • 支持宽泛的学科领域
  • +
+

机器学习研究的问题

+

机器学习的一般过程

+

vIfz4K.png

+
    +
  1. 监督学习:学习输入 x到输出 y的映射,训练数据会有标签 y,分为回归问题和分类问题。
  2. +
  3. 无监督学习:学习数据之间的关联,训练数据是没有标签的,典型问题是聚类。
  4. +
  5. 强化学习:学习输入 x到输出 y的映射,不会提供标签,但是会给一个反馈表示目前的选择有多好。
  6. +
+

机器学习流程:

+
    +
  1. 收集数据
  2. +
  3. 选择模型(选择合适的模型,确定优化函数)
  4. +
  5. 训练模型:找到可以优化损失函数的合适的参数集
  6. +
  7. 应用训练好的模型
  8. +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第1章 绪论
+
https://zhangzhao219.github.io/2022/09/02/UCAS/machine-learning/machine-learning-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/02/UCAS/machine-learning/machine-learning-2/index.html b/2022/09/02/UCAS/machine-learning/machine-learning-2/index.html new file mode 100644 index 000000000..e26990a24 --- /dev/null +++ b/2022/09/02/UCAS/machine-learning/machine-learning-2/index.html @@ -0,0 +1,902 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第2章 贝叶斯学习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第2章 贝叶斯学习

+ + +
+ +

《机器学习》课程笔记:第2章 贝叶斯学习

+ +

第2章 贝叶斯学习

+

概述

+
    +
  1. 依赖先验的决策:
  2. +
+

某地全年365天,晴朗265天,非晴朗100天。判断明天天气如何?

+

,则:

+

,因此,明天晴天的概率更大。

+
    +
  1. 若增加可观测信息:晴朗(非晴朗)天气前一天特征(是否有晚霞)的统计。
  2. +
+

+

今天有晚霞,判断明天天气如何? 即计算

+

今天没有晚霞,判断明天天气如何? 即计算

+

利用贝叶斯决策原理:

+

+

+

的联合概率:

+

因此可以求得,则在前一天有晚霞的条件下晴天的概率要大于不是晴天的概率。

+

贝叶斯决策论

+

贝叶斯公式:

+

+

因此

+

贝叶斯决策:

+

基于观察特征、类别的贝叶斯公式:

+

+

也就是:

+

因此,即

+

如果存在两个变量进行决策,即计算,则可以转换为计算

+

更改为比值的形式:

+

可以定义类别相似性函数

+

分母都是相同的,因此可以将转化为

+

概率有很多都是的形式,因此可以将转化为,将乘积的形式转换为和的形式。

+

对于两变量决策问题来说,可以计算决策边界,绘制后可以直观看出边界的形状,可能是直线也可能是曲线,这样实现了贝叶斯决策方法。

+

贝叶斯分类器

+
    +
  • 朴素贝叶斯分类器:假设特征向量的各维属性独立;
  • +
  • 半朴素贝叶斯分类器:假设的各维属性存在依赖;
  • +
  • 正态分布的贝叶斯分类器:假设服从正态分布;
  • +
+

朴素贝叶斯分类器

+

采用了“属性条件独立性假设”

+

+

关键问题:由训练样本学习类别条件概率和类别先验概率

+

包括个属性和个类别,加上,共有个概率分布需要统计。

+

类别先验概率

+

类别概率密度

+

对于来说,若是离散的变量,则 ,其中表示中在第个属性上取值为的样本组成的集合。

+

是连续的变量,则 (由某一概率分布估计类别概率)

+

拉普拉斯平滑:避免因训练集样本不充分而导致概率估计值为零。

+

平滑后:为类别数;的可能取值个数。

+

正态分布的贝叶斯分类器

+

是连续的变量,则 (设置其为正态分布的概率密度)

+

多维正态分布的概率密度:

+

在每个维度上都是正态分布:

+

贝叶斯学习将公式化简为对数的形式:

+

不同的高斯参数情况:

+

:均为正态分布(当各个类别先验相等时,退化为最小距离分类器,退化为垂直平分面)

+

vL14KO.md.png

+

:各类分布都相同

+

vL1ORP.png

+

贝叶斯学习与参数估计问题

+

推导

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第2章 贝叶斯学习
+
https://zhangzhao219.github.io/2022/09/02/UCAS/machine-learning/machine-learning-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/04/UCAS/information-retrieval/information-retrieval-2/index.html b/2022/09/04/UCAS/information-retrieval/information-retrieval-2/index.html new file mode 100644 index 000000000..043a7f9f1 --- /dev/null +++ b/2022/09/04/UCAS/information-retrieval/information-retrieval-2/index.html @@ -0,0 +1,860 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第2讲 索引构建 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第2讲 索引构建

+ + +
+ +

《现代信息检索》课程笔记:第2讲 索引构建

+ +

语料通常很大,而服务器内存通常相对较小,因此需要在内存有限的情况下的索引构建策略。

+

第2讲 索引构建

+

词项:一个语料中不同的词的数量

+

词条:一个语料中所有词的数量(包括重复的)

+

基于排序的索引构建方法存在的问题

+

在构建索引时,每次解析一篇文档,因此对于每个词项而言,其倒排记录表不到最后一篇文档都是不完整的。

+

如果每个 (termID, docID)对占用 8个字节, 那么处理大规模语料需要大量的空间。

+

一般内存的容量比较小,没有办法将前面产生的倒排记录表全部放在内存中,需要在磁盘上存储中间结果。

+

内存和硬盘

+

内存的典型配置是几G ~ 几十G的内存或上百G或1-2T

+

磁盘空间通常有几T(小型服务器)或10T以上(磁盘阵列)

+

硬盘空间更大,但是在内存中访问数据会比从硬盘访问数据快很多(大概10倍以上的差距)

+

硬盘寻道时间是闲置时间:磁头在定位时不发生数据传输(假设使用的是机械硬盘)

+

因此一个大(连续)块的传输会比多个小块(非连续)的传输速度快

+

硬盘 I/O是基于块的:读写时是整块进行的。块大小:8KB到256KB不等

+

不能在硬盘上对倒排索引表进行排序,因为寻道的时间很慢,导致排序的时间也很慢。

+

BSBI算法

+

一种减少寻道操作的排序:Blocked sort-based Indexing

+

将所有记录划分为每个大小约为10M的块,收集每一块的倒排记录,排序,将倒排索引写入硬盘,最后将不同的分块合并为一个大的倒排索引。

+

关键决策:块的大小-块越大,最后的合并操作就越少

+

合并的过程中需要在磁盘中同时保存数据的两份拷贝(合并前与正在合并),因此磁盘空间要足够大。

+

vTB5VS.png

+

词项字符串的占用空间比较大,因此维护一个全局词典来将字符串映射到唯一的全局ID

+

合并的过程中,将每一个小块的一点点数据放入内存中进行排序,排序好了就放在写缓冲区中,写缓冲区满了就写回硬盘,直到排序完成。

+

可以将两两合并的方式优化为多项合并(multi-way merge):

+
    +
  • 从所有块同时读取,并且为每块保持一个读缓冲区(read buffer)
  • +
  • 为输出文件(即合并后的索引)保持一个写缓冲区(write buffer)
  • +
  • 维护一个待处理 termid的优先级队列(priority queue),每次迭代从队列中选取一个最小的未处理 termid
  • +
  • 合并不同块中所有的该 termid的倒排记录,并写入磁盘。
  • +
  • 因此每次迭代均处理较小规模的数据(一个词项的倒排记录)。
  • +
+

BSBI算法的问题:

+
    +
  • 假定词典可以在内存中放下
  • +
  • 通常需要一部词典(动态增长)来将 term映射成 termID。实际上倒排记录表可以直接采用 (term,docID)方式而不是
    +(termID,docID)方式,但是此时中间文件(即待合并的倒排记录表)将会变得很大(字符串比整型数空间消耗更大)
  • +
+

SPIMI算法

+

内存式单遍扫描索引构建算法:Single-pass in-memory indexing

+

关键思想:

+
    +
  • 对每个块都产生一个独立的词典(不需要在块之间进行 term-termID的映射)
  • +
  • 对倒排记录表不排序,按照它们出现的先后顺序排列,只对词典排序(实际上由于指针的存在,倒排记录表没有排序的必要)。
  • +
+

在扫描文档的同时,直接在内存中维护一个不断更新的倒排索引

+

因此对每个块生成一个完整的倒排索引,这些独立的索引最后合并成一个大索引

+

最终合并词典的过程中,需要进行词项字符串的比较,因为此时没有全局词典提供词项-整数ID的映射。

+

vTDP2R.png

+

BSBI算法和SPIMI算法的主要区别

+

BSBI算法:在分块索引阶段,BSBI算法维护一个全局Term (String) – Termid (int) 的映射表,局部索引为Termid及其倒排记录表,仍然按词典顺序排序。

+

SPIMI算法:分块索引阶段与BSBI算法不同在于建立局部词典和索引,无需全局词典。在合并阶段,将局部索引两两合并,最后产生全局词典建立Term – Termid的映射。

+

动态索引构建

+

实际中文档会增加、删除和修改,因此词典和倒排记录表必须要动态更新。

+

最简单的方法:主索引(Main index)+辅助索引(Auxiliary index)

+
    +
  • 在磁盘上维护一个大的主索引(Main index)
  • +
  • 新文档放入内存中较小的辅助索引中
  • +
  • 同时搜索两个索引,然后合并结果
  • +
  • 定期将辅助索引合并到主索引中
  • +
+

删除的处理:

+
    +
  • 采用无效位向量(Invalidation bit-vector)来表示删除的文档
  • +
  • 利用该维向量过滤返回的结果,以去掉已删除文档
  • +
+

问题:

+
    +
  • 合并过于频繁
  • +
  • 合并时如果正好在搜索,那么搜索的性能将很低
  • +
+

辅助索引方式: 每次合并都需要处理每个倒排记录,索引构建时间为,其中是所有倒排记录的个数

+

对数合并(Logarithmic merge):

+

对数合并算法能够缓解(随时间增长)索引合并的开销 → 用户并不感觉到响应时间上有明显延迟。

+
    +
  • 维护一系列索引,其中每个索引是前一个索引的两倍大小
  • +
  • 将最小的索引置于内存
  • +
  • 将其他更大的索引 置于磁盘
  • +
  • 如果 ,则将它作为 $I_0 $写到磁盘中(如果 $I_0 $不存在)
  • +
  • 或者和合并(如果已经存在),并将合并结果作为写到磁盘中(如果不存在),或者和合并(如果已经存在),依此类推
  • +
+

因此每次两两合并中两个索引的大小相同

+

索引数目的上界为 ,因此查询处理时需要合并个索引,因此每个倒排记录需要合并次,则索引构建时间为,时间复杂度相比较辅助索引方式小了一个数量级。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第2讲 索引构建
+
https://zhangzhao219.github.io/2022/09/04/UCAS/information-retrieval/information-retrieval-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月4日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-10/index.html b/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-10/index.html new file mode 100644 index 000000000..29764d770 --- /dev/null +++ b/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-10/index.html @@ -0,0 +1,936 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第10章 位运算 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第10章 位运算

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第10章 位运算

+ +

位运算

+

常用技巧

+

按位异或:x ^ 0s = x, x ^ 1s = ~x, x ^ x = 0

+

按位与:x & 0s = 0, x & 1s = x, x & x = x

+

按位或:x | 0s = x, x | 1s = 1s, x | x = x

+

n & (n - 1)可以去除 n的位级表示中最低的那一位,例如对于二进制表示 11110100,减去 1得到 11110011,这两个数按位与得到 11110000

+

n & (-n)可以得到n的位级表示中最低的那一位,例如对于二进制表示 11110100,取负得到 00001100,这两个数按位与得到 00000100

+

位运算基础问题

+

Leetcode 461

+

给定两个十进制数字,求它们二进制表示的汉明距离(Hamming distance,即不同位的个数)。

+
class Solution {
+public:
+    int hammingDistance(int x, int y) {
+        int diff = x ^ y;
+        int ans = 0;
+        while(diff){
+            ans += diff & 1;
+            diff >>= 1;
+        }
+        return ans;
+    }
+};
+

分析:将xy按位异或,则不同的位置为1,相同的位置为0。然后将得到的结果与1进行与操作,为0说明是0,为1说明是1,就计数了1。然后将这个结果逐步右移就可以看出下一位了。

+

错误:第一道题不太熟悉。

+

Leetcode 190

+

颠倒给定的 32 位无符号整数的二进制位

+
class Solution {
+public:
+    uint32_t reverseBits(uint32_t n) {
+        uint32_t ans = 0;
+        for(int i=0;i<32;++i){
+            ans <<= 1;
+            ans += n & 1;
+            n >>= 1;
+        }
+        return ans;
+    }
+};
+

分析:摆出一个0,然后左移,逐步加上n右移的数字。

+

错误:不太明白左右移这种东西

+

Leetcode 136

+

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

+
class Solution {
+public:
+    int singleNumber(vector<int>& nums) {
+        int ret = 0;
+        for (auto e: nums) ret ^= e;
+        return ret;
+    }
+};
+

分析:一个数字和 0进行按位异或会得到本身,一个数字和本身进行按位异或会得到0。因此在数组内部进行循环,两次的元素出现了一定会变为0,最后剩下的一个就是这个数字本身。

+

错误:不熟练

+

二进制特性

+

Leetcode 342

+

给定一个整数,判断它是否是4 的次方。

+
class Solution {
+public:
+    bool isPowerOfFour(int n) {
+        return n > 0 && !(n & (n - 1)) && (n & 1431655765);
+    }
+};
+

分析:首先我们考虑一个数字是不是2 的(整数)次方:如果一个数字n 是2 的整数次方,那么它的二进制一定是0…010…0 这样的形式;考虑到n - 1 的二进制是0…001…1,这两个数求按位与的结果一定是0。因此如果n & (n - 1) 为0,那么这个数是2 的次方。如果这个数也是4 的次方,那二进制表示中1 的位置必须为奇数位。我们可以把n 和二进制的10101…101(即十进制下的1431655765)做按位与,如果结果不为0,那么说明这个数是4的次方。

+

错误:不理解

+

Leetcode 318

+

给你一个字符串数组 words ,找出并返回 length(words[i]) * length(words[j]) 的最大值,并且这两个单词不含有公共字母。如果不存在这样的两个单词,返回 0

+
class Solution {
+public:
+    int maxProduct(vector<string>& words) {
+        unordered_map<int, int> hash;
+        int ans = 0;
+        for (const string & word : words) {
+            int mask = 0, size = word.size();
+            for (const char & c : word) {
+                mask |= 1 << (c - 'a');
+            }
+            hash[mask] = max(hash[mask], size);
+            for (const auto& [h_mask, h_len]: hash) {
+                if (!(mask & h_mask)) {
+                    ans = max(ans, size * h_len);
+                }
+            }
+        }
+        return ans;
+    }
+};
+

分析:怎样快速判断两个字母串是否含有重复数字呢?可以为每个字母串建立一个长度为26的二进制数字,每个位置表示是否存在该字母。如果两个字母串含有重复数字,那它们的二进制表示的按位与不为0

+

错误:看了思路后自己实现的。

+

Leetcode 338

+

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

+
class Solution {
+public:
+    vector<int> countBits(int n) {
+        vector<int> ans(n+1,0);
+        for (int i = 1; i <= num; ++i){
+            dp[i] = i & 1? dp[i-1] + 1: dp[i>>1];
+        }
+        return ans;
+    }
+};
+

分析:本题可以利用动态规划和位运算进行快速的求解。定义一个数组dp,其中dp[i] 表示数字i的二进制含有1 的个数。对于第i 个数字,如果它二进制的最后一位为1,那么它含有1 的个数
+则为dp[i-1] + 1;如果它二进制的最后一位为0,那么它含有1 的个数和其算术右移结果相同,即dp[i>>1]。

+

练习

+

Leetcode 268

+

给定一个包含 [0, n]n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

+
class Solution {
+public:
+    int missingNumber(vector<int>& nums) {
+        int n = nums.size();
+        int total = n * (n + 1) / 2;
+        int arrSum = 0;
+        for (int i = 0; i < n; i++) {
+            arrSum += nums[i];
+        }
+        return total - arrSum;
+    }
+};
+

分析:高斯求和后相减即可

+

Leetcode 693

+

给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。

+
class Solution {
+public:
+    bool hasAlternatingBits(int n) {
+        int pre = 0;
+        int sign = 0;
+        while(n){
+            int ans = n & 1;
+            if(sign == 1){
+                if(pre == ans){
+                    return false;
+                }
+            }
+            pre = ans;
+            sign = 1;
+            n >>= 1;
+        }
+        return true;
+    }
+};
+

分析:存储并判断即可

+

错误:有一点小问题,很快调通

+

Leetcode 476

+

给你一个整数 num ,输出它的补数。

+
class Solution {
+public:
+    int findComplement(int num) {
+        uint t = 1u << 31;
+        while (! (t & num)) {
+            num |= t;
+            t >>= 1;
+        }
+        return ~num;
+    }
+};
+

分析:前边补1,然后就可以直接取反了

+

错误:没有思路

+

Leetcode 260

+

给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

+
class Solution {
+public:
+    vector<int> singleNumber(vector<int>& nums) {
+        map<int,int> mp;
+        for(int i=0;i<nums.size();++i){
+            ++mp[nums[i]];
+        }
+        vector<int> result;
+        for(const auto &[a,b] : mp){
+            if(b == 1){
+                result.push_back(a);
+            }
+        }
+        return result;
+    }
+};
+

分析:哈希表算了。。。

+

一遍AC

+

总结

+

这东西和计组挺相关的,面试中应该不会怎么考察这种数学题,但不失为一种运算加速的好办法。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第10章 位运算
+
https://zhangzhao219.github.io/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-10/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月5日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-8/index.html b/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-8/index.html new file mode 100644 index 000000000..b4a3dbb57 --- /dev/null +++ b/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-8/index.html @@ -0,0 +1,850 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第8章 分治法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第8章 分治法

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第8章 分治法

+ +

分治法

+

顾名思义,分治问题由“分”(divide)和“治”(conquer)两部分组成,通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。我们在排序章节展示的归并排序就是典型的分治问题,其中“分”即为把大数组平均分成两个小数组,通过递归实现,最终我们会得到多个长度为1的子数组;“治”即为把已经排好序的两个小数组合成为一个排好序的大数组,从长度为1 的子数组开始,最终合成一个大数组。

+

表达式问题

+

Leetcode 241

+

给定一个只包含加、减和乘法的数学表达式,求通过加括号可以得到多少种不同的结果

+
class Solution {
+public:
+    vector<int> diffWaysToCompute(string expression) {
+        vector<int> ways;
+        for(int i=0;i<expression.size();++i){
+            char c = expression[i];
+            if(c == '+' || c == '-' || c == '*'){
+                vector<int> left = diffWaysToCompute(expression.substr(0,i));
+                vector<int> right = diffWaysToCompute(expression.substr(i+1));
+                for(const int &l : left){
+                    for(const int &r : right){
+                        if(c == '+'){
+                            ways.push_back(l+r);
+                        }
+                        else if(c == '-'){
+                            ways.push_back(l-r);
+                        }
+                        else{
+                            ways.push_back(l*r);
+                        }
+                    }
+                }
+            }
+        }
+        if (ways.empty()){
+            ways.push_back(stoi(expression));
+        }
+        return ways;
+    }
+};
+

分析:利用分治思想,我们可以把加括号转化为,对于每个运算符号,先执行处理两侧的数学表达式,再处理此运算符号。注意边界情况,即字符串内无运算符号,只有数字。

+

错误:想不通的

+

练习

+

Leetcode 932

+
class Solution {
+public:
+    vector<int> beautifulArray(int n) {
+        vector<int> ans;
+        if(n==1){
+            ans.push_back(1);
+            return ans;
+        }
+        int odd_num=(n+1)/2;
+        int even_num=n/2;
+        vector<int> left_arry=beautifulArray(odd_num);
+        vector<int> right_arry=beautifulArray(even_num);
+        //将左侧数组映射为奇数
+        for(auto &val:left_arry){
+            ans.push_back(val*2-1);
+        }
+        //将右侧数组映射为偶数
+        for(auto &val:right_arry){
+            ans.push_back(val*2);
+        }
+        return ans;
+    }
+};
+

分析:不懂

+

错误:不懂

+

Leetcode 312

+
class Solution {
+public:
+    int maxCoins(vector<int>& nums) {
+        int n = nums.size();
+        vector<vector<int>> rec(n + 2, vector<int>(n + 2));
+        vector<int> val(n + 2);
+        val[0] = val[n + 1] = 1;
+        for (int i = 1; i <= n; i++) {
+            val[i] = nums[i - 1];
+        }
+        for (int i = n - 1; i >= 0; i--) {
+            for (int j = i + 2; j <= n + 1; j++) {
+                for (int k = i + 1; k < j; k++) {
+                    int sum = val[i] * val[k] * val[j];
+                    sum += rec[i][k] + rec[k][j];
+                    rec[i][j] = max(rec[i][j], sum);
+                }
+            }
+        }
+        return rec[0][n + 1];
+    }
+};
+

分析:不懂

+

错误:不懂

+

总结

+

不懂不懂不懂啊啊啊啊啊

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第8章 分治法
+
https://zhangzhao219.github.io/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-8/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月5日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-9/index.html b/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-9/index.html new file mode 100644 index 000000000..c6281747e --- /dev/null +++ b/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-9/index.html @@ -0,0 +1,1195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第9章 数学问题 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第9章 数学问题

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第9章 数学问题

+ +

数学问题

+

公倍数与公因数

+

利用辗转相除法求得两个数的最大公因数,将两个数相乘再除以最大公因数即可得到最小公倍数

+
int gcd(int a, int b) {
+    return b == 0 ? a : gcd(b, a% b);
+}
+int lcm(int a, int b) {
+    return a * b / gcd(a, b);
+}
+

进一步也可以通过扩展欧几里得算法在求得 ab最大公因数的同时,也得到它们的系数 xy,从而使 ax + by = gcd(a, b)

+
int xGCD(int a, int b, int &x, int &y) {
+    if (!b) {
+        x = 1, y = 0;
+        return a;
+    }
+    int x1, y1, gcd = xGCD(b, a % b, x1, y1);
+    x = y1, y = x1 - (a / b) * y1;
+    return gcd;
+}
+

质数

+

Leetcode 204

+

给定整数 n ,返回所有小于非负整数 n 的质数的数量 。

+
class Solution {
+public:
+    int countPrimes(int n) {
+        if(n <= 2){
+            return 0;
+        }
+        vector<bool> nums(n,true);
+        for(int i=2;i<n;++i){
+            if(nums[i] == true){
+                for(int j=2*i;j<n;j += i){
+                    nums[j] = false;
+                }
+            }
+        }
+        return accumulate(nums.begin(),nums.end(),0) - 2;
+    }
+};
+

分析:使用埃拉托斯特尼筛法即可。

+

错误:有点忘记算法了。

+

数字处理

+

给定一个整数 num,将其转化为7进制,并以字符串形式输出。

+
class Solution {
+public:
+    string convertToBase7(int num) {
+        int sign = 0;
+        if(num < 0){
+            num = -num;
+            sign = 1;
+        }
+        if(num == 0){
+            return "0";
+        }
+        string result = "";
+        while(num/7){
+            char c = num%7 + '0';
+            result =  c + result;
+            num /= 7;
+        }
+        if(num != 0){
+            char b = '0' + num;
+            result =  b + result;
+        }
+        if(sign == 1){
+            return '-' + result;
+        }
+        return result;
+    }
+};
+

分析:直接进制转换就行,注意进制转换的时候用十进制进行过渡比较方便。

+

错误:磕磕绊绊调通了。

+

Leetcode 172

+

给定一个整数 n ,返回 n! 结果中尾随零的数量。

+
class Solution {
+public:
+    int trailingZeroes(int n) {
+        return n == 0? 0: n / 5 + trailingZeroes(n / 5);
+    }
+};
+

分析:每个尾部的0由2*5 = 10而来,因此我们可以把阶乘的每一个元素拆成质数相乘,统计有多少个2和5。明显的,质因子2的数量远多于质因子5的数量,因此我们可以只统计阶乘结果里有多少个质因子5。

+

错误:没想到这么好的思路

+

Leetcode 415

+

给定两个字符串形式的非负整数 num1num2 ,计算它们的和并同样以字符串形式返回。

+
class Solution {
+public:
+    string addStrings(string num1, string num2) {
+        int n1 = num1.size();
+        int n2 = num2.size();
+        --n1;
+        --n2;
+        string result = "";
+        int cnt = 0;
+        while(n1 >= 0 && n2 >= 0){
+            int temp = num1[n1] - '0' + num2[n2] - '0' + cnt;
+            if(temp >= 10){
+                cnt = 1;
+            }
+            else{
+                cnt = 0;
+            }
+            char c = temp%10 + '0';
+            result = c + result;
+            --n1;
+            --n2;
+        }
+        while(n1 >= 0){
+            int temp = num1[n1] - '0' + cnt;
+            if(temp >= 10){
+                cnt = 1;
+            }
+            else{
+                cnt = 0;
+            }
+            char c = temp%10 + '0';
+            result = c + result;
+            --n1;
+        }
+        while(n2 >= 0){
+            int temp = num2[n2] - '0' + cnt;
+            if(temp >= 10){
+                cnt = 1;
+            }
+            else{
+                cnt = 0;
+            }
+            char c = temp%10 + '0';
+            result = c + result;
+            --n2;
+        }
+        if(cnt == 1){
+            return '1' + result;
+        }
+        return result;
+    }
+};
+

分析:大数相加,没什么新的东西

+

一遍AC

+

Leetcode 326

+

给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool isPowerOfThree(int n) {
+        if(n == 1){
+            return true;
+        }
+        for(long long i=3;i<INT_MAX;i*=3){
+            if(i == n){
+                return true;
+            }
+        }
+        return false;
+    }
+};
+

分析:比较简单,有更好的解法,需要数学能力

+

错误:n=1没有考虑

+

随机与取样

+

Leetcode 384

+

给定一个数组,要求实现两个指令函数。第一个函数“shuffle”可以随机打乱这个数组,第二个函数“reset”可以恢复原来的顺序。

+
class Solution {
+public:
+    Solution(vector<int>& nums) {
+        this->nums = nums;
+        this->original.resize(nums.size());
+        copy(nums.begin(), nums.end(), original.begin());
+    }
+  
+    vector<int> reset() {
+        copy(original.begin(), original.end(), nums.begin());
+        return nums;
+    }
+  
+    vector<int> shuffle() {
+        if (nums.empty()) return {};
+        vector<int> shuffled(nums);
+        int n = nums.size();
+        for (int i = n - 1; i >= 0; --i) {
+            swap(shuffled[i], shuffled[rand() % (i + 1)]);
+        }
+        // 正向洗牌:
+        // for (int i = 0; i < n; ++i) {
+        // int pos = rand() % (n - i);
+        // swap(shuffled[i], shuffled[i+pos]);
+        // }
+        return shuffled;
+    }
+private:
+    vector<int> nums;
+    vector<int> original;
+};
+

分析:经典的Fisher-Yates洗牌算法,原理是通过随机交换位置来实现随机打乱,有正向和反向两种写法

+

错误:类什么的还是不太会写

+

Leetcode 528

+

给定一个数组,数组每个位置的值表示该位置的权重,要求按照权重的概率去随机采样。

+
class Solution {
+    vector<int> W;
+public:
+    Solution(vector<int>& w) {
+        partial_sum(w.begin(), w.end(), back_inserter(W));
+    }
+  
+    int pickIndex() {
+        int pos = rand() % W.back();
+        return upper_bound(W.begin(), W.end(), pos) - W.begin();
+    }
+};
+

分析:我们可以先使用 partial_sum求前缀和(即到每个位置为止之前所有数字的和),这个结果对于正整数数组是单调递增的。每当需要采样时,我们可以先随机产生一个数字,然后使用二分法查找其在前缀和中的位置,以模拟加权采样的过程。

+

错误:没思路

+

Leetcode 382

+

给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点被选中的概率一样 。

+
class Solution {
+    vector<int> arr;
+public:
+    Solution(ListNode* head) {
+        while (head) {
+            arr.emplace_back(head->val);
+            head = head->next;
+        }
+    }
+  
+    int getRandom() {
+        return arr[rand() % arr.size()];
+    }
+};
+

分析:用一个数组记录链表中的所有结点值,然后随机输出即可。

+

错误:思路简单就是不会写

+

练习

+

Leetcode 168

+

给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。

+
class Solution {
+public:
+    string convertToTitle(int columnNumber) {
+        string ans;
+        while (columnNumber > 0) {
+            int a0 = (columnNumber - 1) % 26 + 1;
+            ans += a0 - 1 + 'A';
+            columnNumber = (columnNumber - a0) / 26;
+        }
+        reverse(ans.begin(), ans.end());
+        return ans;
+    }
+};
+

分析:进制转换的变形题

+

错误:减法操作没想好

+

Leetcode 67

+

给你两个二进制字符串,返回它们的和(用二进制表示)。

+
class Solution {
+public:
+    string addBinary(string a, string b) {
+        int a_size = a.size();
+        int b_size = b.size();
+        --a_size;
+        --b_size;
+        int cnt = 0;
+        int sign;
+        string result = "";
+        while(a_size >= 0 && b_size >= 0){
+            sign = a[a_size] - '0' + b[b_size] - '0' + cnt;
+            if(sign == 0){
+                result = "0" + result;
+                cnt = 0;
+            }
+            else if(sign == 1){
+                result = "1" + result;
+                cnt = 0;
+            }
+            else if(sign == 2){
+                result = "0" + result;
+                cnt = 1;
+            }
+            else if(sign == 3){
+                result = "1" + result;
+                cnt = 1;
+            }
+            --a_size;
+            --b_size;
+        }
+        while(a_size >= 0){
+            sign = a[a_size] - '0' + cnt;
+            if(sign == 0){
+                result = "0" + result;
+                cnt = 0;
+            }
+            else if(sign == 1){
+                result = "1" + result;
+                cnt = 0;
+            }
+            else if(sign == 2){
+                result = "0" + result;
+                cnt = 1;
+            }
+            else if(sign == 3){
+                result = "1" + result;
+                cnt = 1;
+            }
+            --a_size;
+        }
+        while(b_size >= 0){
+            sign = b[b_size] - '0' + cnt;
+            if(sign == 0){
+                result = "0" + result;
+                cnt = 0;
+            }
+            else if(sign == 1){
+                result = "1" + result;
+                cnt = 0;
+            }
+            else if(sign == 2){
+                result = "0" + result;
+                cnt = 1;
+            }
+            else if(sign == 3){
+                result = "1" + result;
+                cnt = 1;
+            }
+            --b_size;
+        }
+        if(cnt == 1){
+            result = "1" + result;
+        }
+        return result;
+    }
+};
+

分析:还是大数加法

+

错误:忘记了,应该没什么错误

+

Leetcode 238

+

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

+
class Solution {
+public:
+    vector<int> productExceptSelf(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> left(n);
+        vector<int> right(n);
+        int start = 1;
+        left[0] = start;
+        for(int i=1;i<n;++i){
+            left[i] = start * nums[i-1];
+            start = left[i];
+        }
+        int end = 1;
+        right[n-1] = end;
+        for(int i=n-2;i>=0;--i){
+            right[i] = end * nums[i+1];
+            end = right[i];
+        }
+        vector<int> result(n);
+        for(int i=0;i<n;++i){
+            result[i] = left[i] * right[i];
+        }
+        return result;
+    }
+};
+

分析:前缀积+后缀积

+

错误:看了一下思路,后面自己想通了实现了

+

Leetcode 462

+

给你一个长度为 n 的整数数组 nums ,返回使所有数组元素相等需要的最少移动数。在一步操作中,你可以使数组中的一个元素加 1 或者减 1

+
class Solution {
+public:
+    int minMoves2(vector<int>& nums) {
+        int n = nums.size();
+        sort(nums.begin(),nums.end());
+        int num = nums[n/2];
+        int sum2 = 0;
+        for(int i=0;i<n;++i){
+            if(nums[i] > num){
+                sum2 += nums[i] - num;
+            }
+            else{
+                sum2 += num - nums[i];
+            }
+        }
+        return sum2;
+    }
+};
+

分析:如果仅仅考虑最大的数字和最小的数字,那么这个数字一定在这两个数字中间,去除掉后这个数字也一定在次大的和次小的数字之间。因此是中位数

+

错误:思路不对,开始想成平均数了

+

Leetcode 169

+

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

+
class Solution {
+public:
+    int majorityElement(vector<int>& nums) {
+        int candidate = -1;
+        int count = 0;
+        for (int num : nums) {
+            if (num == candidate)
+                ++count;
+            else if (--count < 0) {
+                candidate = num;
+                count = 1;
+            }
+        }
+        return candidate;
+    }
+};
+

分析:Boyer-Moore 算法:维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0;我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x:如果 x 与 candidate 相等,那么计数器 count 的值增加 1;如果 x 与 candidate 不等,那么计数器 count 的值减少 1。在遍历完成后,candidate 即为整个数组的众数。

+

错误:算法想的不太好,没有想到最优的解法。

+

Leetcode 470

+

给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数,试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。

+
class Solution {
+public:
+    int rand10() {
+        int row, col, idx;
+        do {
+            row = rand7();
+            col = rand7();
+            idx = col + (row - 1) * 7;
+        } while (idx > 40);
+        return 1 + (idx - 1) % 10;
+    }
+};
+

分析:调用两次rand7(),找到一些等概率的数字,然后拒绝掉另外的数字。

+

错误:想当然认为是直接乘法了。

+

Leetcode 202

+

编写一个算法来判断一个数 n 是不是快乐数。

+
class Solution {
+public:
+    bool isHappy(int n) {
+        int sum = 6;
+        while(sum--){
+            string s = to_string(n);
+            int t = 0;
+            for(int i=0;i<s.size();++i){
+                t += (s[i] - '0') * (s[i] - '0');
+            }
+            if(t == 1){
+                return true;
+            }
+            n = t;
+        }
+        return false;
+    }
+};
+

分析:看看会不会跳出循环

+

一遍AC,但是解法不够好,后面要用更好的方法进行尝试。

+

总结

+

数学问题需要有数学基础,一般面试中应该用的比较少,有些问题还是挺有意思的。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第9章 数学问题
+
https://zhangzhao219.github.io/2022/09/05/Leetcode/Leetcode-101/Leetcode-101-9/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月5日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/05/UCAS/information-retrieval/information-retrieval-3/index.html b/2022/09/05/UCAS/information-retrieval/information-retrieval-3/index.html new file mode 100644 index 000000000..8d95b40f4 --- /dev/null +++ b/2022/09/05/UCAS/information-retrieval/information-retrieval-3/index.html @@ -0,0 +1,892 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第3讲 索引压缩 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第3讲 索引压缩

+ + +
+ +

《现代信息检索》课程笔记:第3讲 索引压缩

+ +

第3讲 索引压缩

+

压缩

+

举例:将长编码串用短编码串来代替:111111111111111111➡18个1

+

为什么要压缩?

+
    +
  • 减少磁盘空间占用(节省开销)
  • +
  • 增加内存存储内容(加快速度)
  • +
  • 加快从磁盘到内存的数据传输速度(同样加快速度) +
      +
    • 读压缩数据到内存+在内存中解压,比直接读入未压缩数据到内存要快很多
    • +
    +
  • +
+

为什么在IR中需要压缩?

+
    +
  • 占用更少的硬盘空间 +
      +
    • 更经济,节省空间
    • +
    +
  • +
  • 将更多数据载入内存 +
      +
    • 加快处理速度(内存中读写很快)
    • +
    +
  • +
  • 减少从磁盘读入内存的时间 +
      +
    • 大型搜索引擎将相当比例的倒排记录表都放入内存(硬盘?)
    • +
    +
  • +
+

IR中压缩的两个基本要求:无损压缩和随机访问

+

压缩的一个基本问题:对齐,即建立不同压缩单元之间的分界标识

+

有损压缩:丢弃一些信息-很多常用的预处理步骤可以看成是有损压缩

+

无损压缩:所有信息都保留-索引压缩中通常都使用无损压缩

+

词项统计量

+

词典压缩中词典的大小即词汇表的大小是关键

+

词汇表大小会随着文档集的大小增长而增长,没有办法估计数量。

+

存在一个经验规律可以进行估计:

+

Heaps定律:,其中是词汇表大小, 是文档集的大小。参数的一个经典取值是:

+

Heaps定律在对数空间下是线性的。

+

在容许拼写错误或者对拼写错误自动纠错的情况下,Heaps定律的效果如何?

+
    +
  • 存在拼写错误:会增加词项数目
  • +
  • 自动纠错:总体词项数目趋于正常
  • +
  • 对效果有一定影响,但是除非存在大量拼写错误,否则不会有显著影响。
  • +
+

倒排记录表压缩中词项的分布情况是关键

+

我们还需要知道在文档集中有多少高频词项和低频词项

+

Zipf定律:第常见的词项的频率成正比

+

是语料中词项频率:词项在所有文档中出现的次数

+

实际统计中可以发现拟合度并不是很高,但是可以发现高频词项很少,低频罕见词项很多。

+

词典压缩

+

一般而言,相对于倒排记录表,词典所占空间较小。但是我们想将词典放入内存,另外满足一些特定领域特定应用的需要,如手机、机
+载计算机上的应用或要求快速启动等需求。因此,压缩词典也很重要。

+

定长数组方式下的词典存储:每个词项需要20(字符串)+4(词频)+4(指向倒排索引表的指针)=28个字节。

+

不足之处:

+
    +
  • 大量存储空间被浪费 +
      +
    • 即使是长度为1的词项,我们也分配20个字节,但是英语中每个词项的平均长度为8个字符
    • +
    +
  • +
  • 不能处理长度大于20字节的词项
  • +
+

将整部词典看成单一字符串:4(词频)+4(指向倒排索引表的指针)+3(指向字符串的指针,按照实际大小决定,例如8*400000个位置需要$log_2(8 * 400000)< 24 $位来表示)+8(每个字符串平均需要8个字节)=19个字节

+

按块存储,假设块大小k=4,此时每4个词项只需要保留1个词项指针,但是同时需要增加4个字节(比较短,1个字节就可以)来表示每个词项的长度,因此每4个词项需要3+4=7B,比之前的节省了12-7=5B

+

但是不采用块存储方式下的词项查找是典型的二叉查找,而采用块存储方式下的词项查找需要进行顺序查找,块如果太大会影响效率。

+

每个块当中,会有公共前缀,可以采用前端编码方式继续压缩。

+

哪些前缀应该用于前端编码?需要在哪些方面有所权衡?

+
    +
  • 同一个单词的不同形式适合使用这种前端编码
  • +
  • 没有什么公共前缀的话,压缩效果不太好,而且还会导致检索速度下降
  • +
+

倒排记录表压缩

+

倒排记录表空间远大于词典,压缩关键是对每条倒排记录进行压缩

+

关键思想:存储 docID间隔而不是 docID本身

+

设计一个变长编码(variable length encoding):可变长编码对于小间隔采用短编码而对于长间隔采用长编码

+

可变字节(VB)码:设定一个专用位 (高位) c作为延续位(continuation bit),如果间隔表示少于7比特,那么c置1,将间隔编入一个
+字节的后7位中;否则将高7位放入当前字节中,并将c置0,剩下的位数采用同样的方法进行处理,最后一个字节的c置1(表
+示结束)

+
    +
  • 除字节外,还可以采用不同的对齐单位:比如32位(word)、16位及4位(nibble)等等
  • +
  • 如果有很多很小的间隔,那么采用可变字节码会浪费很多空间,而此时采用4位为单位将会节省空间
  • +
+

一元码:将n表示成n个1和最后一个0

+

基于位的编码:

+

编码:(不考虑0)

+
    +
  • 将G (Gap, 间隔) 表示成长度(length)和偏移(offset)两部分
  • +
  • 偏移对应G的二进制编码,只不过将首部的1去掉(因为所有的编码第一位都是1)
  • +
  • 长度部分给出的是偏移的位数,采用一元编码
  • +
  • 手动计算的时候先计算偏移,再根据偏移计算长度
  • +
+

偏移部分是比特位,长度部分需要比特位,因此全部编码需要比特位。

+
    +
  • 编码是前缀无关的,也就是说一个合法的编码不会是任何一个其他的合法编码的前缀,也保证了解码的唯一性。
  • +
  • 编码在最优编码的2或3倍之内
  • +
  • 编码适用于任何分布,是通用性编码
  • +
  • 编码是无参数编码,不需要通过拟合得到参数
  • +
+

组变长整数编码:

+
    +
  • 按块存储,每块大小为5-17个字节,存放4个整数编码
  • +
  • 每块首字节:4个2位的二进制长度,
  • +
  • 使用 字节(在4–16之间)存放4个整数
  • +
+

Simple9编码:每块4字节,前4位标识块内结构,剩余28位存储若干个数字,每个数字占用相同的位数。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第3讲 索引压缩
+
https://zhangzhao219.github.io/2022/09/05/UCAS/information-retrieval/information-retrieval-3/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月5日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/06/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-2/index.html b/2022/09/06/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-2/index.html new file mode 100644 index 000000000..127eb323d --- /dev/null +++ b/2022/09/06/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-2/index.html @@ -0,0 +1,1083 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:模式识别与机器学习-第2章 统计判别 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:模式识别与机器学习-第2章 统计判别

+ + +
+ +

《模式识别与机器学习》课程笔记:第2章 统计判别

+ +

第2章 统计判别

+

统计学(statistics)是用以收集数据,分析数据和由数据得出结论的一组概念、原则和方法。

+

作为统计判别问题的模式分类

+
    +
  • 模式识别的目的就是要确定某一个给定的模式样本属于哪一类。
  • +
  • 可以通过对被识别对象的多次观察和测量,构成特征向量,并将其作为某一个判决规则的输入,按此规则来对样本进行分类。
  • +
  • 在获取模式的观测值时,有些事物具有确定的因果关系,即在一定的条件下,它必然会发生或必然不发生。 +
      +
    • 例如识别一块模板是不是直角三角形,只要凭“三条直线边闭合连线和一个直角”这个特征,测量它是否有三条直线边的闭合连线并有一个直角,就完全可以确定它是不是直角三角形。这种现象是确定性的现象。
    • +
    +
  • +
  • 但在现实世界中,由许多客观现象的发生,就每一次观察和测量来说,即使在基本条件保持不变的情况下也具有不确定性。
  • +
  • 只有在大量重复的观察下,其结果才能呈现出某种规律性,即对它们观察到的特征具有统计特性。
  • +
  • 特征值不再是一个确定的向量,而是一个随机向量
  • +
  • 此时,只能利用模式集的统计特性来分类,以使分类器发生错误的概率最小
  • +
+

给定观测值,判断其属于类还是类,作出某次判断时的错误率是:

+

+

最小化误差概率条件下,若,则;若,则

+

贝叶斯判别原则

+

两类模式集的分类:

+

目的:要确定是属于类还是类,要看是来自于类的概率大还是来自类的概率大。

+

根据概率判别规则,若,则;若,则

+

由贝叶斯定理,后验概率可由类别的先验概率的条件概率密度来计算,即:

+

,其中也称为似然函数。

+

与概率判别规则结合,则若,则;若,则

+

不等式转换一下:

+

,则

+

,则

+

其中,称为似然比,称为似然比的判决阈值

+

此判别称为贝叶斯判别。

+

贝叶斯判别的推广:

+
    +
  • 允许使用多于一个特征:标量、向量、多种特征向量
  • +
  • 允许多于两种类别状态的情形
  • +
  • 允许有其他行为而不仅仅是判定类别:如后验概率接近的情况下,如果拒绝判断的代价不大,可以拒绝判断。
  • +
+

可以通过引入一个更一般的损失函数来替代误差概率

+

朴素贝叶斯

+

特征是多维向量时,假设各个特征之间相互独立

+

+

贝叶斯最小风险判别

+

当考虑到对于某一类的错误判决要比对另一类的判决更为关键时,就需要把最小错误概率的贝叶斯判别做一些修正,提出条件平均风险

+

类问题,如果观察样本被判定属于类,则条件平均风险

+

为将本应属于类的模式判别成属于类的是非代价。

+

,即判别正确,得分,可以取负值或零,表示不失分。

+

,即判别错误,失分,应取正值。

+

意义:

+
    +
  • 对于自然属性是属于类的模式来说,它来自类的概率应为
  • +
  • 如果分类器判别是属于类,但它实际上来自类,也就是说分类器失败,这时为失分,对应的条件风险为后验概率进行的加权运算。
  • +
  • 由于模式的自然属性可能来自类中的任一类,因此可将观察样本指定为类的条件平均风险用的公式运算。
  • +
+

分类器对每一个模式种可能的类别可供选择,若对每一个计算出全部类别的平均风险值,并且将指定为是具有最小风险值的那一类,则这种分类器称为最小平均条件风险分类器。

+

按贝叶斯公式,最小平均条件风险可写成:

+

+

可以舍去公共项,则可以简化为:

+

+

也是贝叶斯分类器,只是它的判别方法不是按错误概率最小作为标准,而是按平均条件风险作为标准。

+

举例若

+

当分类器将判别为时:

+

当分类器将判别为时:

+

,则被判定为属于

+

此时:

+

即:

+

通常,因此

+

时,

+

左边为似然比:,右边为阈值

+

因此两类模式的贝叶斯判别条件为:

+
    +
  • ,则
  • +
  • ,则
  • +
  • ,则可以做任意判别。
  • +
+

通常,当判别正确时,不失分,可选常数

+

判别错误时,可选常数

+

此时:

+

对于类情况来说,若仍按判对失分为0,判错失分为1记,则

+

贝叶斯最小错误判别是计算得到某个类别的概率,而最小风险判别是计算得到某个类别后存在风险的概率。两者正好相反。

+

正态分布模式的贝叶斯分类器

+

出发点:当已知或者有理由设想类概率密度函数是多变量的正态分布时,贝叶斯分类器可以导出一些简单的判别函数。由于正态密度函数易于分析,且对许多重要的实际应用又是一种合适的模型,因此受到很大的重视。

+

种模式类别的多变量正态类密度函数:(参考数学推导

+

+

其中,每一类模式的分布密度都完全被其均值向量和协方差矩阵所规定

+

当协方差矩阵的全部非对角线上的元素都为零时,多变量正态类密度函数可简化为个单变量正态类密度函数的乘积,个单变量为互相独立的

+

已知类别的判别函数可写成如下形式:

+

可以取自然对数的形式以方便计算:

+

代入正态类密度函数,可以得到:

+

+

去掉与无关的项,最终可以得到:

+

即为正态分布模式的贝叶斯判别函数。

+

因此判别函数是一个超二次曲面,对于正态分布模式的贝叶斯分类器,两个模式类别之间用一个二次判别界面分开,就可以求得最优的分类效果。

+

当M=2且类模式都是正态分布的情况

+
    +
  1. 时:
  2. +
+

两类模式的正态分布:表示为表示为两类的判别函数对应为:

+

+

+

判别界面的二次型方程,即两类模式可用二次判别界面分开。

+

是二维时,判别界面为二次曲线,如椭圆,圆,抛物线或双曲线等

+
    +
  1. 时:
  2. +
+

+

为对称矩阵,上式可简化为:

+

+

由此可导出类别间的判别界面为:

+

判别界面为的线性函数,为一超平面。

+

是二维时,判别界面为一直线

+

决策边界的特征:

+
    +
  • 如果两种分布的协方差矩阵相等并且与单位阵成比例,且先验概率相等。则决策边界垂直于两个中心的连线。
  • +
  • 协方差矩阵相等,判决边界同样是超平面。随着先验概率的改变,判决边界也随之改变;对于差别较大的离散先验概率而言,判决边界不会落于中心点之间。
  • +
+

贝叶斯分类规则是基于统计概念的。如果只有少数模式样本,一般较难获得最优的结果。

+

实际代码编写

+
defBayesian(data,label,P):
+    if data.shape[0] != label.shape[0]: # 如果数据和标签的数量不相同
+        print('Error!')
+        sys.exit()
+    M = data[0].shape[0] # 获取数据的维度
+    data_list = [[],[]] # 将不同类别的数据分开存储
+    data_list[0] = np.array([data[i] for i inrange(len(label)) if label[i] ==0])
+    data_list[1] = np.array([data[i] for i inrange(len(label)) if label[i] ==1])
+    # 计算均值向量
+    m0 = np.sum(data_list[0],axis=0) / data_list[0].shape[0]
+    m1 = np.sum(data_list[1],axis=0) / data_list[1].shape[0]
+    # 计算协方差矩阵
+    C0 = np.sum(np.array([np.dot((data_list[0][i] - m0).reshape(-1,1), \
+        (data_list[0][i] - m0).reshape(1,-1)) for i inrange(data_list[0].shape[0])]),axis=0) / data_list[0].shape[0]
+    C1 = np.sum(np.array([np.dot((data_list[1][i] - m1).reshape(-1,1),\
+        (data_list[1][i] - m1).reshape(1,-1)) for i inrange(data_list[1].shape[0])]),axis=0) / data_list[1].shape[0]
+    return np.dot(m0-m1,np.linalg.inv(C0)),np.log(P[0]) - np.log(P[1]) +0.5* (np.dot(np.dot(m1.reshape(1,-1),\
+        np.linalg.inv(C0)),m1.reshape(-1,1)) - np.dot(np.dot(m0.reshape(1,-1),np.linalg.inv(C0)),m0.reshape(-1,1)))
+

均值向量和协方差矩阵的参数估计

+

在贝叶斯分类器中,构造分类器需要知道类概率密度函数,如果按先验知识已知其分布,则只需知道分布的参数即可。(例如:类概率密度是正态分布,它完全由其均值向量和协方差矩阵所确定)。

+

对均值向量和协方差矩阵的估计即为贝叶斯分类器中的一种参数估计问题。

+

参数估计的两种方式:

+
    +
  • 将参数作为非随机变量来处理,例如矩估计就是一种非随机参数的估计。
  • +
  • 随机参数的估计,即把这些参数看成是随机变量,例如贝叶斯参数估计。
  • +
+

均值和协方差矩阵的非随机参数的估计

+

均值和协方差矩阵的估计量定义

+

设模式的类概率密度函数为,则其均值向量定义为:

+

,其中

+

若以样本的平均值作为均值向量的近似值,则均值估计量

+

,其中为样本的数目

+

协方差矩阵

+

其中的每个元素

+

其中,分别为的第个分量。

+

协方差矩阵写成向量形式为:,(后面这样算更简单一点)

+

协方差矩阵的估计量(当时)为:

+

均值和协方差矩阵估计量的迭代运算形式

+

假设已经计算了个样本的均值估计量,若再加上一个样本,其新的估计量为:

+

+

其中为从个样本计算得到的估计量。迭代的第一步应取

+

协方差矩阵估计量的迭代运算与上述相似:

+

均值向量和协方差矩阵的贝叶斯学习

+

将概率密度函数的参数估计量看成是随机变量,它可以是纯量、向量或矩阵。按这些估计量统计特性的先验知识,可以先粗略地预选出它们的密度函数。通过训练模式样本集,利用贝叶斯公式设计一个迭代运算过程求出参数的后验概率密度。当后验概率密度函数中的随机变量的确定性提高时,可获得较准确的估计量。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:模式识别与机器学习-第2章 统计判别
+
https://zhangzhao219.github.io/2022/09/06/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月6日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/07/Leetcode/Leetcode-101/Leetcode-101-11/index.html b/2022/09/07/Leetcode/Leetcode-101/Leetcode-101-11/index.html new file mode 100644 index 000000000..2cb90ceee --- /dev/null +++ b/2022/09/07/Leetcode/Leetcode-101/Leetcode-101-11/index.html @@ -0,0 +1,1529 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第11章 数据结构 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第11章 数据结构

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第11章 数据结构

+ +

数据结构

+

数组

+

Leetcode 448

+

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

+
class Solution {
+public:
+    vector<int> findDisappearedNumbers(vector<int>& nums) {
+        int n = nums.size();
+        vector<bool> vt(n+1,false);
+        for(int i=0;i<n;++i){
+            vt[nums[i]] = true;
+        }
+        vector<int> result;
+        for(int i=1;i<=n;++i){
+            if(vt[i] == false){
+                result.push_back(i);
+            }
+        }
+        return result;
+    }
+};
+

分析:扫一遍确认一下,再扫一遍找出结果。

+

一遍AC

+

Leetcode 48

+

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像原地顺时针旋转 90 度。

+
class Solution {
+public:
+    void rotate(vector<vector<int>>& matrix) {
+        int temp = 0, n = matrix.size()-1;
+        for (int i = 0; i <= n / 2; ++i) {
+            for (int j = i; j < n - i; ++j) {
+                temp = matrix[j][n-i];
+                matrix[j][n-i] = matrix[i][j];
+                matrix[i][j] = matrix[n-j][i];
+                matrix[n-j][i] = matrix[n-i][n-j];
+                matrix[n-i][n-j] = temp;
+            }
+        }
+    }
+};
+

分析:转转转

+

错误:没想到原地旋转的思路。

+

Leetcode 240

+

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:每行的元素从左到右升序排列,每列的元素从上到下升序排列。

+
class Solution {
+public:
+    bool searchMatrix(vector<vector<int>>& matrix, int target) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        int x = 0;
+        int y = n-1;
+        while(x >= 0 && x < m && y >= 0 && y < n){
+            if(matrix[x][y] == target){
+                return true;
+            }
+            else if(target < matrix[x][y]){
+                y -= 1;
+            }
+            else{
+                x += 1;
+            }
+        }
+        return false;
+    }
+};
+

分析:从右上角开始查找,若当前值大于待搜索值,我们向左移动一位;若当前值小于待搜索值,我们向下移动一位。如果最终移动到左下角时仍不等于待搜索值,则说明待搜索值不存在于矩阵中。

+

错误:找到思路后一遍AC

+

Leetcode 769

+

给定一个长度为 n 的整数数组 arr ,它表示在 [0, n - 1] 范围内的整数的排列。我们将 arr 分割成若干 (即分区),并对每个块单独排序。将它们连接起来后,使得连接的结果和按升序排序后的原数组相同。返回数组能分成的最多块数量。

+
class Solution {
+public:
+    int maxChunksToSorted(vector<int>& arr) {
+        int n = arr.size();
+        int result = 0;
+        int maxnum = 0;
+        for(int i=0;i<n;++i){
+            maxnum = max(maxnum,arr[i]);
+            if(maxnum == i){
+                ++result;
+            }
+        }
+        return result;
+    }
+};
+

分析:从左往右遍历,同时记录当前的最大值,每当当前最大值等于数组位置时,我们可以多一次分割。

+

错误:看了思路后实现的

+

栈和队列

+

Leetcode 232

+

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty

+
class MyQueue {
+    stack<int> st1;
+    stack<int> st2;
+public:
+    MyQueue() {
+
+    }
+  
+    void push(int x) {
+        st1.push(x);
+    }
+  
+    int pop() {
+        while(!st1.empty()){
+            st2.push(st1.top());
+            st1.pop();
+        }
+        int a = st2.top();
+        st2.pop();
+        while(!st2.empty()){
+            st1.push(st2.top());
+            st2.pop();
+        }
+        return a;
+    }
+  
+    int peek() {
+        while(!st1.empty()){
+            st2.push(st1.top());
+            st1.pop();
+        }
+        int a = st2.top();
+        while(!st2.empty()){
+            st1.push(st2.top());
+            st2.pop();
+        }
+        return a;
+    }
+  
+    bool empty() {
+        return st1.empty();
+    }
+};
+

分析:比较简单,也没有算法

+

错误:全局变量没定义好,返回值漏掉了,调通了。

+

Leetcode 155

+

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

+
class MinStack {
+    stack<int> s1;
+    stack<int> mins;
+public:
+    MinStack() {
+
+    }
+  
+    void push(int val) {
+        if(mins.empty() || val <= mins.top()){
+            mins.push(val);
+        }
+        s1.push(val);
+    }
+  
+    void pop() {
+        int a = s1.top();
+        s1.pop();
+        if(mins.top() == a){
+            mins.pop();
+        }
+    }
+  
+    int top() {
+        return s1.top();
+    }
+  
+    int getMin() {
+        return mins.top();
+    }
+};
+

分析:可以额外建立一个新栈,栈顶表示原栈里所有值的最小值。每当在原栈里插入一个数字时,若该数字小于等于新栈栈顶,则表示这个数字在原栈里是最小值,我们将其同时插入新栈内。每当从原栈里取出一个数字时,若该数字等于新栈栈顶,则表示这个数是原栈里的最小值之一,我们同时取出新栈栈顶的值。

+

错误:没有思路

+

Leetcode 20

+

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

+
class Solution {
+public:
+    bool isValid(string s) {
+        stack<char> st;
+        int n = s.size();
+        for(int i=0;i<n;++i){
+            if(s[i] == '(' || s[i] == '{' || s[i] == '['){
+                st.push(s[i]);
+            }
+            else{
+                if(st.empty()){
+                    return false;
+                }
+                else if(st.top() == '[' && s[i] == ']'){
+                    st.pop();
+                }
+                else if(st.top() == '(' && s[i] == ')'){
+                    st.pop();
+                }
+                else if(st.top() == '{' && s[i] == '}'){
+                    st.pop();
+                }
+                else{
+                    return false;
+                }
+            }
+        }
+        if(st.empty()){
+            return true;
+        }
+        return false;
+    }
+};
+

分析:用栈进行匹配即可

+

错误:没有考虑只有一个左括号的情况,改正后调通了

+

单调栈

+

Leetcode 739

+

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

+
class Solution {
+public:
+    vector<int> dailyTemperatures(vector<int>& temperatures) {
+        int n = temperatures.size();
+        vector<int> answer(n);
+        stack<int> s;
+        for(int i=0;i<n;++i){
+            while (!s.empty()) {
+                int pre_index = s.top();
+                if (temperatures[i] <= temperatures[pre_index]) {
+                    break;
+                }
+                s.pop();
+                answer[pre_index] = i - pre_index;
+            }
+            s.push(i);
+        }
+        return answer;
+    }
+};
+

分析:我们可以维持一个单调递减的栈,表示每天的温度;为了方便计算天数差,我们这里存放位置(即日期)而非温度本身。我们从左向右遍历温度数组,对于每个日期p,如果p的温度比栈顶存储位置q的温度高,则我们取出q,并记录q需要等待的天数为p-q;我们重复这一过程,直到p的温度小于等于栈顶存储位置的温度(或空栈)时,我们将p插入栈顶,然后考虑下一天。在这个过程中,栈内数组永远保持单调递减,避免了使用排序进行比较。最后若栈内剩余一些日期,则说明它们之后都没有出现更暖和的日期。

+

错误:感觉并不是非常理解。

+

优先队列

+

Leetcode 23

+

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

+
class Solution {
+public:
+    struct Comp{
+        bool operator()(ListNode* l1,ListNode* l2){
+            return l1->val > l2->val;
+        }
+    };
+    ListNode* mergeKLists(vector<ListNode*>& lists) {
+        if(lists.empty()){
+            return nullptr;
+        }
+        priority_queue<ListNode*,vector<ListNode*>,Comp> q;
+        for(ListNode* list:lists){
+            if(list){
+                q.push(list);
+            }
+        }
+        ListNode* dummy = new ListNode(0), *cur = dummy;
+        while (!q.empty()) {
+            cur->next = q.top();
+            q.pop();
+            cur = cur->next;
+            if (cur->next) {
+                q.push(cur->next);
+            }
+        }
+        return dummy->next;
+    }
+};
+

分析:即把所有的链表存储在一个优先队列中,每次提取所有链表头部节点值最小的那个节点,直到所有链表都被提取完为止。

+

错误:优先队列不是很熟悉

+

Leetcode 218

+

给定建筑物的起止位置和高度,返回建筑物轮廓(天际线)的拐点。

+

Hard难度,想不太明白,暂时不做了

+

分析:使用优先队列储存每个建筑物的高度和右端(这里使用pair,其默认比较函数是先比较第一个值,如果相等则再比较第二个值),从而获取目前会拔高天际线、且妨碍到前一个建筑物(的右端端点)的下一个建筑物。

+

错误:没有思路

+

双端队列

+

Leetcode 239

+

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

+
class Solution {
+public:
+    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
+        vector<int> result;
+        deque<int> dq;
+        int n = nums.size();
+        for(int i=0;i<n;++i){
+            if(!dq.empty() && nums[i] > nums[dq.back()]){
+                while(!dq.empty() && nums[dq.back()] < nums[i]){
+                    dq.pop_back();
+                }
+            }
+            dq.push_back(i);
+            if(i >= k-1){
+                result.push_back(nums[dq.front()]);
+                if(nums[i-k+1] == nums[dq.front()]){
+                    dq.pop_front();
+                }
+            }
+        }
+        return result;
+    }
+};
+

分析:利用双端队列进行操作:每当向右移动时,把窗口左端的值从队列左端剔除,把队列右边小于窗口右端的值全部剔除。这样双端队列的最左端永远是当前窗口内的最大值。

+

错误:理解了思路后调通了。

+

哈希表

+

Leetcode 1

+

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target的那两个整数,并返回它们的数组下标。

+
class Solution {
+public:
+    vector<int> twoSum(vector<int>& nums, int target) {
+        vector<int> result;
+        unordered_map<int, int> hash;
+        int n = nums.size();
+        for(int i=0;i<n;++i){
+            if(hash.count(target - nums[i])){
+                result.push_back(hash[target - nums[i]]);
+                result.push_back(i);
+                break;
+            }
+            hash[nums[i]] = i;
+        }
+        return result;
+    }
+};
+

分析:利用哈希表存储遍历过的值以及它们的位置,每次遍历到位置i 的时候,查找哈希表里是否存在target - nums[i],若存在,则说明这两个值的和为target。

+

一遍AC

+

Leetcode 128

+

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

+
class Solution {
+public:
+    int longestConsecutive(vector<int>& nums) {
+        unordered_set<int> hash;
+        for(const int & num:nums){
+            hash.insert(num);
+        }
+        int ans = 0;
+        while(!hash.empty()){
+            int cnt = *(hash.begin());
+            hash.erase(cnt);
+            int pre = cnt - 1;
+            int next = cnt + 1;
+            while(!hash.empty() && hash.count(pre)){
+                hash.erase(pre);
+                --pre;
+            }
+            while(!hash.empty() && hash.count(next)){
+                hash.erase(next);
+                ++next;
+            }
+            ans = max(ans,next-pre-1);
+        }
+        return ans;
+    }
+};
+

分析:把所有数字放到一个哈希表,然后不断地从哈希表中任意取一个值,并删除掉其之前之后的所有连续数字,然后更新目前的最长连续序列长度。重复这一过程,我们就可以找到所有的连续数字序列。

+

错误:看了思路后实现了

+

Leetcode 149

+

给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。

+
class Solution {
+public:
+    int maxPoints(vector<vector<int>>& points) {
+        unordered_map<double, int> hash; // <斜率, 点个数>
+        int max_count = 0, same = 1, same_y = 1;
+        for (int i = 0; i < points.size(); ++i) {
+            same = 1, same_y = 1;
+            for (int j = i + 1; j < points.size(); ++j) {
+                if (points[i][1] == points[j][1]) {
+                    ++same_y;
+                    if (points[i][0] == points[j][0]) {
+                        ++same;
+                    }
+                }
+                else {
+                    double dx = points[i][0] - points[j][0], dy = points[i][1] -
+                    points[j][1];
+                    ++hash[dx/dy];
+                }
+            }
+            max_count = max(max_count, same_y);
+            for (auto item : hash) {
+                max_count = max(max_count, same + item.second);
+            }
+            hash.clear();
+        }
+        return max_count;
+    }
+};
+

分析:对于每个点,我们对其它点建立哈希表,统计同一斜率的点一共有多少个。这里利用的原理是,一条线可以由一个点和斜率而唯一确定。另外也要考虑斜率不存在和重复坐标的情况。

+

错误:好麻烦先算了

+

多重集合和映射

+

Leetcode 332

+

给你一份航线列表 tickets ,其中 tickets[i] = [from<sub>i</sub>, to<sub>i</sub>] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

+
class Solution {
+public:
+    vector<string> findItinerary(vector<vector<string>>& tickets) {
+        vector<string> ans;
+        if (tickets.empty()) {
+            return ans;
+        }
+        unordered_map<string, multiset<string>> hash;
+        for (const auto & ticket: tickets) {
+            hash[ticket[0]].insert(ticket[1]);
+        }
+        stack<string> s;
+        s.push("JFK");
+        while (!s.empty()) {
+            string next = s.top();
+            if (hash[next].empty()) {
+                ans.push_back(next);
+                s.pop();
+            } 
+            else {
+                s.push(*hash[next].begin());
+                hash[next].erase(hash[next].begin());
+            }
+        }
+        reverse(ans.begin(), ans.end());
+        return ans;
+    }
+};
+

分析:本题可以先用哈希表记录起止机场,其中键是起始机场,值是一个多重集合,表示对应的终止机场。因为一个人可能坐过重复的线路,所以我们需要使用多重集合储存重复值。储存完成之后,我们可以利用栈来恢复从终点到起点飞行的顺序,再将结果逆序得到从起点到终点的顺序。

+

错误:多重集合的第一道题,也是唯一一道题,不是很明白

+

前缀和和积分图

+

Leetcode 303

+

设计一个数据结构,使得其能够快速查询给定数组中,任意两个位置间所有数字的和。

+
class NumArray {
+    vector<int> frontsum;
+public:
+    NumArray(vector<int>& nums) {
+        for(int i=0;i<nums.size();++i){
+            if(i == 0){
+                frontsum.push_back(nums[i]);
+            }
+            else{
+                frontsum.push_back(nums[i] + frontsum[i-1]);
+            }
+        }
+    }
+  
+    int sumRange(int left, int right) {
+        if(left == 0){
+            return frontsum[right];
+        }
+        return frontsum[right] - frontsum[left-1];
+    }
+};
+

分析:前缀和即可

+

一遍AC

+

Leetcode 304

+

设计一个数据结构,使得其能够快速查询给定矩阵中,任意两个位置包围的长方形中所有数字的和。

+
class NumMatrix {
+    vector<vector<int>> frontmatrix;
+public:
+    NumMatrix(vector<vector<int>>& matrix) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        for(int i=0;i<m;++i){
+            vector<int> temp;
+            for(int j=0;j<n;++j){
+                if(i == 0 && j == 0){
+                    temp.push_back(matrix[i][j]);
+                }
+                else if(i == 0){
+                    temp.push_back(matrix[i][j] + temp[j-1]);
+                }
+                else if(j == 0){
+                    temp.push_back(matrix[i][j] + frontmatrix[i-1][j]);
+                }
+                else{
+                    temp.push_back(matrix[i][j] + frontmatrix[i-1][j] + temp[j-1] - frontmatrix[i-1][j-1]);
+                }
+            }
+            frontmatrix.push_back(temp);
+        }
+    }
+  
+    int sumRegion(int row1, int col1, int row2, int col2) {
+        if(row1 == 0 && col1 == 0){
+            return frontmatrix[row2][col2];
+        }
+        else if(row1 == 0){
+            return frontmatrix[row2][col2]-frontmatrix[row2][col1-1];
+        }
+        else if(col1 == 0){
+            return frontmatrix[row2][col2]-frontmatrix[row1-1][col2];
+        }
+        return frontmatrix[row2][col2]-frontmatrix[row2][col1-1]-frontmatrix[row1-1][col2]+frontmatrix[row1-1][col1-1];
+    }
+};
+

分析:二维上的前缀和(积分图)即可

+

一遍AC

+

Leetcode 560

+

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。

+
class Solution {
+public:
+    int subarraySum(vector<int>& nums, int k) {
+        int count = 0, psum = 0;
+        unordered_map<int, int> hashmap;
+        hashmap[0] = 1; // 初始化很重要
+        for (int i: nums) {
+            psum += i;
+            count += hashmap[psum-k];
+            ++hashmap[psum];
+        }
+        return count;
+    }
+};
+

分析:本题同样是利用前缀和,不同的是这里我们使用一个哈希表 hashmap,其键是前缀和,而值是该前缀和出现的次数。在我们遍历到位置i 时,假设当前的前缀和是 psum ,那么 hashmap[psum-k]即为以当前位置结尾、满足条件的区间个数。

+

错误:直接使用前缀和会超时,然而这个短代码挺难理解的样子。

+

练习

+

Leetcode 566

+

在 MATLAB 中,有一个非常有用的函数 reshape ,它可以将一个 m x n 矩阵重塑为另一个大小不同(r x c)的新矩阵,但保留其原始数据。给你一个由二维数组 mat 表示的 m x n 矩阵,以及两个正整数 rc ,分别表示想要的重构的矩阵的行数和列数。重构后的矩阵需要将原始矩阵的所有元素以相同的行遍历顺序填充。如果具有给定参数的 reshape 操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。

+
class Solution {
+public:
+    vector<vector<int>> matrixReshape(vector<vector<int>>& mat, int r, int c) {
+        int m = mat.size();
+        int n = mat[0].size();
+        if(m*n != r*c){
+            return mat;
+        }
+        int rowindex = 0;
+        int colindex = 0;
+        vector<vector<int>> result(r,vector<int>(c));
+        for(int i=0;i<r;++i){
+            for(int j=0;j<c;++j){
+                result[i][j] = mat[rowindex][colindex];
+                ++colindex;
+                if(colindex == n){
+                    ++rowindex;
+                    colindex = 0;
+                }
+            }
+        }
+        return result;
+    }
+};
+

分析:很简单的小题,没有任何难度。

+

一遍AC

+

Leetcode 225

+

用两个队列实现一个栈

+
class MyStack {
+    queue<int> q1;
+    queue<int> q2;
+public:
+    MyStack() {
+
+    }
+  
+    void push(int x) {
+        q1.push(x);
+        return;
+    }
+  
+    int pop() {
+        while(q1.size() != 1){
+            q2.push(q1.front());
+            q1.pop();
+        }
+        int a = q1.front();
+        q1.pop();
+        while(!q2.empty()){
+            q1.push(q2.front());
+            q2.pop();
+        }
+        return a;
+    }
+  
+    int top() {
+        while(q1.size() != 1){
+            q2.push(q1.front());
+            q1.pop();
+        }
+        int a = q1.front();
+        q2.push(q1.front());
+        q1.pop();
+        while(!q2.empty()){
+            q1.push(q2.front());
+            q2.pop();
+        }
+        return a;
+    }
+  
+    bool empty() {
+        return q1.empty();
+    }
+};
+

分析:也是很简单的题,倒腾倒腾数字就行了

+

一遍AC

+

Leetcode 503

+

给定一个循环数组 numsnums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的下一个更大元素

+
class Solution {
+public:
+    vector<int> nextGreaterElements(vector<int>& nums) {
+        int n = nums.size();
+        stack<int> st;
+        vector<int> result(n,-1);
+        for(int i=0;i<2*n-1;++i){
+            while(!st.empty() && nums[i%n] > nums[st.top()]){
+                result[st.top()] = nums[i%n];
+                st.pop();
+            }
+            st.push(i%n);
+        }
+        return result;
+    }
+};
+

分析:使用单调栈解决本题。单调栈中保存的是下标,从栈底到栈顶的下标在数组 nums中对应的值是单调不升的。每次我们移动到数组中的一个新的位置 i,我们就将当前单调栈中所有对应值小于 nums[i]的下标弹出单调栈,这些值的下一个更大元素即为 nums[i]。随后我们将位置 i入栈。

+

错误:没有想到单调栈,看了一下思路后自己实现的。

+

Leetcode 217

+

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false

+
class Solution {
+public:
+    bool containsDuplicate(vector<int>& nums) {
+        unordered_map<int,int> mp;
+        int n = nums.size();
+        for(int i=0;i<n;++i){
+            if(mp.find(nums[i]) == mp.end()){
+                mp[nums[i]] = 1;
+            }
+            else{
+                return true;
+            }
+        }
+        return false;
+    }
+};
+

分析:非常简单的哈希表,没什么难度

+

错误:下标和数字插入看的不太对

+

Leetcode 697

+

给定一个非空且只包含非负数的整数数组 nums,数组的的定义是指数组里任一元素出现频数的最大值。你的任务是在 nums 中找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度。

+
class Solution {
+public:
+    int findShortestSubArray(vector<int>& nums) {
+        unordered_map<int,vector<int>> mp;
+        int n = nums.size();
+        for(int i=0;i<n;++i){
+            if(mp.find(nums[i]) == mp.end()){
+                mp[nums[i]].push_back(i);
+                mp[nums[i]].push_back(i);
+                mp[nums[i]].push_back(1);
+            }
+            else{
+                if(i < mp[nums[i]][0]){
+                    mp[nums[i]][0] = i;
+                }
+                if(i > mp[nums[i]][1]){
+                    mp[nums[i]][1] = i;
+                }
+                ++mp[nums[i]][2];
+            }
+        }
+        int maxnum = 0;
+        int result = n+1;
+        for(auto it = mp.cbegin();it != mp.cend();++it){
+            if(it->second[2] > maxnum){
+                maxnum = it->second[2];
+                result = it->second[1]-it->second[0]+1;
+            }
+            else if (it->second[2] == maxnum){
+                result = min(result,it->second[1]-it->second[0]+1);
+            }
+        }
+        return result;
+    }
+};
+

分析:比较简单的数据结构应用题

+

错误:语法问题,还有下标数字问题,后面自己调通

+

Leetcode 594

+

和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。现在,给你一个整数数组 nums ,请你在所有可能的子序列中找到最长的和谐子序列的长度。数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。

+
class Solution {
+public:
+    int findLHS(vector<int>& nums) {
+        int n = nums.size();
+        int ans = 0;
+        unordered_map<int,int> mp;
+        for(int i=0;i<n;++i){
+            if(mp.find(nums[i]) == mp.end()){
+                mp[nums[i]] = 1;
+            }
+            else{
+                ++mp[nums[i]];
+            }
+        }
+        for(auto it = mp.cbegin();it != mp.cend();++it){
+            if(mp.find(it->first-1) != mp.end()){
+                ans = max(ans,it->second + mp[it->first-1]);
+            }
+            if(mp.find(it->first+1) != mp.end()){
+                ans = max(ans,it->second + mp[it->first+1]);
+            }
+        }
+        return ans;
+    }
+};
+

分析:看起来挺像动态规划,实际上并不是,统计一下就好了

+

错误:还是map迭代器不太熟练,后面调通。

+

Leetcode 287

+

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

+
class Solution {
+public:
+    int findDuplicate(vector<int>& nums) {
+        int n = nums.size();
+        int len = nums.length;
+        for (int num : nums) {
+            int idx = Math.abs(num);
+            if (nums[idx] < 0) {
+                return idx;
+            }
+            nums[idx] = -nums[idx];
+        }
+        return len;
+    }
+};
+

分析:考虑到数组元素值的范围是 [1,n],但数组长度为 n+1,那么很显然在遍历数组的时候,我们将数组的值变为其对应的负数,那么再次遇到负数就得到了答案。

+

错误:上面不是最优解,没有想到最优解

+

Leetcode 313

+

超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。给你一个整数 n 和一个整数数组 primes ,返回第 n超级丑数 。题目数据保证第 n超级丑数32-bit 带符号整数范围内。

+
class Solution {
+public:
+    int nthSuperUglyNumber(int n, vector<int>& primes) {
+        vector<long> dp(n + 1);
+        int m = primes.size();
+        vector<int> pointers(m, 0);
+        vector<long> nums(m, 1);
+        for (int i = 1; i <= n; i++) {
+            long minNum = INT_MAX;
+            for (int j = 0; j < m; j++) {
+                minNum = min(minNum, nums[j]);
+            }
+            dp[i] = minNum;
+            for (int j = 0; j < m; j++) {
+                if (nums[j] == minNum) {
+                    pointers[j]++;
+                    nums[j] = dp[pointers[j]] * primes[j];
+                }
+            }
+        }
+        return dp[n];
+    }
+};
+

分析:动态规划,没有思路

+

错误:没有思路

+

Leetcode 870

+

给定两个大小相等的数组 nums1nums2nums1 相对于 nums优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。返回 nums1 任意排列,使其相对于 nums2 的优势最大化。

+
class Solution {
+public:
+    vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
+        sort(nums1.begin(),nums1.end());
+        vector<pair<int,int>> vt;
+        for(int i=0;i<nums2.size();i++){
+            vt.push_back(make_pair(nums2[i],i));
+        }
+        sort(vt.begin(),vt.end());
+        vector<int> ans(nums2.size());
+        int l1=0,r1=nums1.size()-1,l2=0,r2=nums2.size()-1;
+        while(r2>=0){
+            if(nums1[r1]>vt[r2].first){
+                ans[vt[r2].second]=nums1[r1];
+                r1--;
+            }
+            else{
+                 ans[vt[r2].second]=nums1[l1];
+                 l1++;
+            }
+            r2--;
+        }
+      
+        return ans;
+    }
+};
+

分析:田忌赛马,能打就打,打不过让最菜的送人头。

+

错误:没思路

+

Leetcode 307

+

线段树先算了

+

总结

+

数据结构是最最基础的算法,没有合适的数据结构就不可能有高效的算法。普通的数据结构掌握的还不错,但是有一些比较高级的数据结构练的比较少,掌握的不太好。今后要注重这些比较高级的数据结构,并尽量去在实际中应用。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第11章 数据结构
+
https://zhangzhao219.github.io/2022/09/07/Leetcode/Leetcode-101/Leetcode-101-11/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月7日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/08/UCAS/advanced-ai/advanced-ai-2/index.html b/2022/09/08/UCAS/advanced-ai/advanced-ai-2/index.html new file mode 100644 index 000000000..a8f5ecbe2 --- /dev/null +++ b/2022/09/08/UCAS/advanced-ai/advanced-ai-2/index.html @@ -0,0 +1,926 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第2讲 搜索 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第2讲 搜索

+ + +
+ +

《高级人工智能》课程笔记:第2讲 搜索

+ +

第2讲 搜索

+

搜索问题:有策略有规律的探索

+

搜索问题是对原问题的建模

+

搜索问题的构成:状态空间➡后继函数(状态转化为另一个状态,采取的动作,付出的代价)➡初始状态和目标测试

+

解是一个行动序列,将初始状态转换成目标状态

+

例1:罗马尼亚旅行:

+

vqG77Q.md.png

+
    +
  • ①状态空间:所有城市
  • +
  • ②后继函数:沿着道路从一个城市到达另外一个城市,损失函数是距离
  • +
  • ③初始状态:这个人现在在Arad
  • +
  • ④目标测试:目前是否到达了Bucharest
  • +
+

解:从Arad到Bucharest的最短路径

+

例2:吃豆子游戏

+

vqJwNj.png

+

状态空间包含了环境中的每一个细节:Agent,Ghost,大的豆子和小的豆子

+

搜索状态只保留行动需要的细节:

+

对于走到终点来说:

+
    +
  • ①状态空间:Agent的位置信息
  • +
  • ②后继函数:四个方向进行行走,更新位置信息
  • +
  • ③目标测试:是否到达了终点
  • +
+

对于吃掉所有豆子来说:

+
    +
  • ①状态空间:Agent的位置信息和每一个点的状态(豆子吃没吃掉)
  • +
  • ②后继函数:四个方向进行行走,更新位置信息,更新豆子的信息
  • +
  • ③目标测试:全部豆子是否都被吃光
  • +
+

状态数量计算:

+
    +
  • Agent的状态:120
  • +
  • 食物数量:30
  • +
  • 鬼魂的位置:12*12
  • +
  • 朝向:4
  • +
  • 世界状态:
  • +
  • 路线规划状态:120
  • +
  • “吃光豆子”状态:
  • +
+

例3:三个传教士和三个野人

+

状态空间:{(M, C, B)},表示河左岸的传教士数量、野人数量和船目前的方位

+

后继函数:{P01, P10, P02, P20, P11, Q01, Q10, Q02, Q20, Q11},P表示现在是从左岸到右岸,后面两个数字表示船上的传教士数量和野人数量

+

初始状态:(3, 3, 1)

+

目标状态:(0, 0, 0)

+

状态空间图:搜索问题的数学表示,在状态空间图中,每个状态只出现一次

+

搜索树:

+
    +
  • 根节点对应了初始状态
  • +
  • 子节点对应了父节点的后继
  • +
  • 节点显示状态,但对应的是到达这些状态的行动
  • +
  • 对大多数问题,实际上不会构建整个树,一般都会剪枝
  • +
+

状态空间图的每一个结点表示每一个状态

+

搜索树的每一个结点不表示状态,而是从初始状态到这个状态的一个路径(因此要尽量少构建搜索树的结点)

+

无信息搜索

+

基于搜索树的搜索:

+
    +
  • 扩展出潜在的行动 (tree nodes)
  • +
  • 维护所考虑行动的边缘(fringe)节点
  • +
  • 试图扩展尽可能少的树节点
  • +
+

搜索算法特性:

+
    +
  • 完备性: 当问题有解时,保证能找到一个解?
  • +
  • 最优性: 保证能找到最优解(最小耗散路径)?
  • +
  • 时间复杂度和空间复杂度?
  • +
+

所有搜索算法都是相同的,除了对边缘的处理策略

+

深度优先搜索

+
    +
  • 在找到目标之前,搜索到整个树左侧的一些子树
  • +
  • 可以遍历整个树
  • +
  • 分支因子为,最大深度为时间复杂度为空间复杂度为(因为只保留了路径上的结点)
  • +
  • 完备性:不完备。如果无穷大,无法在可以接受的时间内找到解
  • +
  • 不是最优的:只去找最左边的结点
  • +
+

广度优先搜索

+
    +
  • 在找到目标之前,搜索到全部更浅的结点
  • +
  • 分支因子为,最大深度为,解的深度为时间复杂度为空间复杂度为
  • +
  • 完备性:完备。因为如果解存在,一定是有限的
  • +
  • 只有所有的路径代价都相同时才是最优的
  • +
+

迭代深入搜索(Iterative Deepening)

+

结合DFS的空间优势与BFS的时间优势

+

深度优先按照层数进行约束,不要搜索到

+

通常绝大多数的节点都在底层,所以上层的节点生成多次影响不是很大

+

代价敏感搜索(Cost-Sensitive Search)

+

代价一致搜索(Uniform Cost Search):将之前的走过的路径的代价进行一个累加,然后寻找其代价最低的路径。

+

可以看成代价敏感搜索的一种实现。

+
    +
  • 在找到目标之前,搜索到比代价最小的方式更小代价的结点
  • +
  • 解的代价为,每条结点间连线的代价大概为时间复杂度为,空间复杂度为
  • +
  • 完备性:完备。前提是代价都是有限且都为正数。
  • +
  • 最优的
  • +
+

启发式搜索

+

启发策略:估计一个状态到目标距离的函数,问题给予算法的额外信息,为特定搜索问题而设计。

+

贪婪搜索

+

策略:扩展你认为最接近目标状态的节点

+

启发式:对每个状态估计到最近目标的距离(曼哈顿距离或者欧氏距离),只使用启发函数来评价节点

+

通常情况下最佳优先使你直接(或很快)到达目标,最坏情况类似DFS

+

A* 搜索

+

结合代价一致搜索和贪婪搜索

+

重点搜索评价函数:

+

表示路径的代价,或者称为后向的代价

+

表示前方距离目标的距离,或者称为前向的代价

+

A* 搜索将两个代价进行组合

+

A* 搜索结束条件是目标出列的时候,而不是目标入列的时候,因为目标入列的时候可能路径并不是最优的。

+

A*搜索不一定是最优的,启发函数要好好选择

+

启发函数可采纳的,那么,其中是到最近目标的真实耗散。(例如曼哈顿距离)

+

前提:启发函数可采纳的,那么A* 树搜索是最优的。

+
    +
  • 代价一致搜索在所有“方向”上等可能的扩展
  • +
  • A*搜索主要朝着目标扩展,而且能够保证最优性
  • +
+

对于解决难的搜索问题,大部分工作就是想出可采纳的启发函数。通常可采纳启发函数是松弛问题的解的耗散

+

A*图搜索与树搜索的区别在于图搜索不允许访问相同结点

+

图搜索中,如果启发函数是一致的,A* 搜索是最优的。

+

一致的:启发函数不仅仅要是可采纳的,同时在每一个局部的位置也要合理。

+

+

也就是:如果沿路径的节点估计耗散值单调递增,即,那么A*图搜索具备最优性。

+

通常,天然的可采纳启发函数是倾向于一致的,特别是从松弛问题中获得的启发函数

+

局部搜索

+

树搜索在边缘集合中保留未探索的替代路径(确保完备性)

+

局部搜索: 改进单一选项直到不能再改善为止

+

爬山法搜索

+

模拟退火搜索:避免局部极大(允许向山下移动)

+

遗传算法——自然选择

+
    +
  • 基于适应度函数,在每步中保留N个最好状态
  • +
  • 配对杂交操作
  • +
  • 产生可选的变异
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第2讲 搜索
+
https://zhangzhao219.github.io/2022/09/08/UCAS/advanced-ai/advanced-ai-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月8日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/08/UCAS/information-retrieval/information-retrieval-4/index.html b/2022/09/08/UCAS/information-retrieval/information-retrieval-4/index.html new file mode 100644 index 000000000..05e6d0018 --- /dev/null +++ b/2022/09/08/UCAS/information-retrieval/information-retrieval-4/index.html @@ -0,0 +1,923 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第4讲 通配查询与拼写矫正 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第4讲 通配查询与拼写矫正

+ + +
+ +

《现代信息检索》课程笔记:第4讲 通配查询与拼写矫正

+ +

第4讲 通配查询与拼写矫正

+

词典

+

词典是指存储词项词汇表的数据结构:作用:存储词项以及定位词项

+

词项词汇表指的是具体数据,而词典指的是数据结构

+

采用定长数组的词典结构对每个词项需要存储文档频率和指向倒排记录表的指针

+

词项定位(查词典):在词典中查找给定关键字

+

用于词项定位的数据结构:主要是哈希表和树

+

有些IR系统用哈希表,有些系统用树结构

+

采用哈希表或树的准则:

+
    +
  • 词项数目是否固定(词项数目是否持续增长)(固定采用哈希表更好,因为快,但是动态更新的代价比较高)
  • +
  • 词项的相对访问频率如何
  • +
  • 词项的数目有多少
  • +
+

哈希函数:输入词项,输出正整数(通常是地址)

+
    +
  • 每个词项通过哈希函数映射成一个整数
  • +
  • 尽可能避免冲突
  • +
  • 查询处理时: 对查询词项进行哈希,如果有冲突,则解决冲突,最后在定长数组中定位
  • +
  • 优点: +
      +
    • 在哈希表中的定位速度快于树中的定位速度
    • +
    • 查询时间是常数
    • +
    +
  • +
  • 缺点: +
      +
    • 无法处理词项的微小变形
    • +
    • 不支持前缀搜索
    • +
    • 如果词汇表不断增大,需要定期对所有词项重新哈希
    • +
    +
  • +
+

+

树可以支持前缀查找(相当于对词典再建一层索引)

+

最简单的树结构:二叉树,搜索速度略低于哈希表方式,时间复杂度为, 其中是词汇表大小,即所有词项的数目

+

仅仅对平衡树成立,使二叉树重新保持平衡开销很大

+

B-树:每个内部节点的子节点数目在之间,其中为合适的正整数

+

通配查询

+

通配查询:包含通配符的查询

+

mon*: 找出所有包含以mon开头的词项的文档

+

如果采用B-树词典结构,那么实现起来非常容易,只需要返回区间mon ≤ t < moo上的词项t

+

*mon: 找出所有包含以mon结尾的词项的文档

+

将所有的词项倒转过来,然后基于它们建一棵附加的树,返回区间nom ≤ t < non上的词项t

+

词项中间的*号处理:mnchen

+
    +
  • 在B-树中分别查找满足m*和 *nchen的词项集合,然后求交集(开销很大)
  • +
+

轮排索引:(主要思想:让星号出现在词汇的末尾)

+
    +
  • 将每个通配查询旋转,使*出现在末尾
  • +
  • 将每个旋转后的结果存放在词典中,即B-树中
  • +
+

轮排索引的查找过程:

+
    +
  • 将查询进行旋转,将通配符旋转到右部
  • +
  • 同以往一样查找B-树,得到匹配的所有词项,将这些词项对应的倒排记录表取出
  • +
+

相对于通常的B-树,轮排索引(轮排树)的空间要大4倍以上 (经验值)

+

k-gram索引:枚举一个词项中所有连读的k个字符构成k-gram

+
    +
  • 构建一个倒排索引,此时词典部分是所有的k-gram,倒排记录表部分是包含某个k-gram的所有词项
  • +
  • 相当于对词项再构建一个倒排索引(二级索引)
  • +
  • 比轮排索引空间开销要小
  • +
  • 但是可能返回一些伪正例,需要进行后过滤
  • +
+

k-gram存在两个倒排索引:

+
    +
  • 词典-文档的倒排索引基于词项返回文档
  • +
  • k-gram索引用于查找词项,即基于查询所包含的k-gram来查找所有的词项
  • +
+

k-gram索引 vs. 轮排索引

+
    +
  • k-gram索引的空间消耗小
  • +
  • 轮排索引不需要进行后过滤
  • +
+

拼写矫正

+

涉及的任务:拼写错误检测和拼写错误矫正(并不是先后的关系)

+

错误种类:非词汇错误(纠正的时候不需要考虑上下文)和真实词汇错误(纠正的时候需要考虑上下文)

+

两个主要用途

+
    +
  • 纠正待索引文档
  • +
  • 纠正用户的查询
  • +
+

非词汇拼写错误检测:词典中不存在的词均视为错误

+
    +
  • 一般来说,词典越大越好
  • +
  • Web很大,但是充满了拼写错误,因此并不是一个很好的词典
  • +
+

非词汇拼写错误矫正:

+
    +
  • 产生候选:与错误书写的单词相似的真实词汇
  • +
  • 选择最好的候选词:最短加权编辑距离和最高噪声通道概率
  • +
  • 候选集:找到发音相似的候选词、找到拼写相似的候选词、将 w 也包括在候选集里
  • +
+

词独立法:

+
    +
  • 词典中不存在的词均视为错误
  • +
  • 只检查每个单词本身的拼写错误
  • +
  • 但是如果某个单词拼写错误后变成另外一个单词,则无法查出
  • +
+

采用拼写噪声通道模型:通过贝叶斯定理求解:

+

正确拼写为,错误拼写为,则

+

可以通过文档进行估计

+
    +
  • 拼写相近的词:Damerau-Levenshtein编辑距离(插入、删除、替换、两个相邻字母的替换) +
      +
    • 80% 的拼写错误到正确拼写的编辑距离 = 1,几乎所有拼写错误到正确拼写的编辑距离 <= 2
    • +
    +
  • +
+

产生候选词的方法:

+
    +
  1. 遍历词典,计算每一个词的编辑距离
  2. +
  3. 生成所有编辑距离 ≤ k (例如, k = 1 或 2)的词,然后与词典取交集
  4. +
  5. 建立一个字符k-gram索引,从词典中找到共享最多k-grams的词项(例如,基于Jaccard系数计算)
  6. +
  7. 使用Levenshtein 有限状态转换机快速计算
  8. +
  9. 预先计算一个词项到可能的 正确词项/拼写错误的映射表
  10. +
+

语言模型

+

若有包含个词条的大文本语料,则是词频。(一元先验概率)

+

通道模型概率-计算错误概率:混淆“矩阵”(计算一个字符变为另一个字符的概率如何)

+
    +
  • 混淆矩阵构建也可以考虑键盘的邻近型
  • +
+

然后可以计算噪声通道模型

+

计算的过程中可以添加加一概率平滑:上述混淆矩阵的例子很难避免某种操作样本数为0,要避免这种概率为0的情况

+

真实词汇错误的纠正通常需要考虑上下文

+

上下文敏感法:

+
    +
  • 纠错时要考虑周围的单词
  • +
  • 产生候选:与错误书写的单词相似的真实词汇 +
      +
    • 找到发音相似的候选词
    • +
    • 找到拼写相似的候选词
    • +
    • 选择最好的候选词:最短加权编辑距离、最高噪声通道概率
    • +
    +
  • +
+

真实词汇拼写矫正的噪声通道:二元语言模型,将一元模型与二元模型插值

+
    +
  • 给定句子,为每个词产生一个候选词集合,最后选择序列使得概率最大
  • +
+

通道模型的改进:

+
    +
  • 为概率增加一个权重
  • +
  • 允许更丰富的编辑操作
  • +
  • 将发音融入到通道模型中
  • +
  • 将设备融入到通道模型中
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第4讲 通配查询与拼写矫正
+
https://zhangzhao219.github.io/2022/09/08/UCAS/information-retrieval/information-retrieval-4/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月8日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-3/index.html b/2022/09/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-3/index.html new file mode 100644 index 000000000..75d9629db --- /dev/null +++ b/2022/09/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-3/index.html @@ -0,0 +1,1260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:模式识别与机器学习-第3章 判别函数 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:模式识别与机器学习-第3章 判别函数

+ + +
+ +

《模式识别与机器学习》课程笔记:第3章 判别函数

+ +

第3章 判别函数

+

线性判别函数

+

模式识别系统的主要作用:判别各个模式(也称样本)所属的类别

+

模式分类若可用任一个线性函数来划分,则这些模式就称为线性可分的,否则就是非线性可分的。

+

一旦线性函数的系数被确定,这些函数就可用作模式分类的基础。

+

对一个两类问题的判别,就是将模式划分成两类

+

vOaAmt.md.png

+

这两类可以通过一个直线方程来划分

+

,则,若,则

+

称为决策面/判别界面方程**(判别函数和判别界面是否等于0要注意)**

+

用判别函数进行模式分类依赖的两个因素:

+
    +
  • 判别函数的几何性质:线性的(一条直线)和非线性的函数(曲线、折线等)。 +
      +
    • 线性判别函数建立起来比较简单(实际应用较多);
    • +
    • 非线性判别函数建立起来比较复杂。
    • +
    +
  • +
  • 判别函数的形式确定后,主要就是确定判别函数的系数问题,只要被研究的模式是可分的,就能用给定的模式样本集来确定判别函数的系数。
  • +
+

一个维线性判别函数的一般形式:

+

+

权向量(参数向量):

+

维线性判别函数也可以表示为

+

增广模式向量:,增广权向量:

+

多类情况1:用线性判别函数将属于类的模式与不属于类的模式分开,称为 两分法,即把类多类问题分成个两类问题,因此共有个判别函数。会存在分类失败的问题:

+

vOdk34.png

+

多类情况2:采用每对划分,即 两分法,此时一个判别界面只能分开两种类别,但不能把它与其余所有的界面分开。

+

判别函数为,若 ,则

+

因此要分开类模式,共需个判别函数。也会存在不确定区域,即分类失败。

+

多类情况1和多类情况2的比较

+
    +
  • 对于类模式的分类,多类情况1需要个判别函数,而多类情况2需个判别函数,当较大时,后者需要更多的判别式
  • +
  • 采用多类情况1时,每一个判别函数都要把一种类别的模式与其余种类别的模式分开,而不是将一种类别的模式仅与另一种类别的模式分开。
  • +
  • 由于一种模式的分布要比种模式的分布更为聚集,因此多类情况2对模式是线性可分的可能性比多类情况1更大一些。
  • +
+

多类情况3:没有不确定区域的 两分法

+

,此时,对类情况应有个判别函数。

+

广义线性判别函数

+

线性判别函数简单,容易实现,而非线性判别函数复杂,不容易实现。

+

若能将非线性判别函数转换为线性判别函数,则有利于模式分类的实现。

+

设有一个训练用的模式集,在模式空间中线性不可分,但在模式空间中线性可分,其中的各个分量是的单值实函数,的维数高于的维数,即若取,则分类界面在中是线性的,在中是非线性的,此时只要将模式进行非线性变换,使之变换后得到维数更高的模式,就可以用线性判别函数来进行分类。

+

一个非线性判别函数可如下表示:,其中是模式的单值实函数。

+

若定义成广义形式:

+

此时有:。其中

+

非线性判别函数已被变换成广义线性,因此只讨论线性判别函数不会失去一般性意义。

+

是模式的二次多项式函数时:

+

+

式中各项的组成应包含的各个分量的二次项、一次项和常数项,其中平方项个,二次项个,一次项个,常数项1个,其总项数为:
+

+

是模式次多项式函数,总项数为

+

分段线性判别函数

+
    +
  • 线性判别函数在进行分类决策时是最简单有效的,但在实际应用中,常常会出现不能用线性判别函数直接进行分类的情况。
  • +
  • 采用广义线性判别函数的概念,可以通过增加维数来得到线性判别,但维数的大量增加会使在低维空间里在解析和计算上行得通的方法在高维空间遇到困难,增加计算的复杂性。
  • +
  • 引入分段线性判别函数的判别过程,它比一般的线性判别函数的错误率小,但又比非线性判别函数简单。
  • +
+

也就是说,可以使用一个二次判别函数进行分类的地方,也可以使用一个分段线性判别函数来逼近这个二次曲线。

+

可以采用最小距离分类的方法,只有在类别密集地分布在其均值附近时才有效。

+

对于各类交错分布的情况,若再用每类一个均值代表点产生最小距离分类器,就会产生很明显的错误率。在这种情况下,可以运用聚类方法将一些类分解成若干个子类,再用最小距离分类。

+
    +
  • 寻找交遇区—找到互为最小距离的原型对,组成“交遇区”。
  • +
  • 用局部训练模式产生分段线性判别函数并迭代优化决策面。
  • +
  • 撤走已分类正确的样本,从剩下的样本集合中,寻找交遇区,产生分段线性判别函数。
  • +
+

模式空间和权空间

+

模式空间:

+

对一个线性方程,它在三维空间中是一个平面方程式,是方程的系数。

+

向量作为该平面的法线向量,则该线性方程决定的平面通过原点且与垂直

+

是二维的增广向量,为非增广的权向量,它与直线AB垂直

+

模式空间即为增广向量决定的平面或非增广向量决定的直线。

+

权空间:

+

若将方程绘在权向量的三维空间中,则为方程的系数

+

Fisher线性判别

+
    +
  • 应用统计方法解决模式识别问题时,一再碰到的问题之一就是维数问题。
  • +
  • 在低维空间里解析上或计算上行得通的方法,在高维空间里往往行不通。
  • +
  • 因此,降低维数有时就会成为处理实际问题的关键。
  • +
+

问题描述:

+
    +
  • 考虑把维空间的样本投影到一条直线上,形成一维空间,即把维数压缩到一维。
  • +
  • 然而,即使样本在维空间里形成若干紧凑的互相分得开的集群,当把它们投影到一条直线上时,也可能会是几类样本混在一起而变得无法识别。
  • +
  • 但是,在一般情况下,总可以找到某个方向,使在这个方向的直线上,样本的投影能分得开。
  • +
+

Fisher判别方法所要解决的基本问题:如何根据实际情况找到一条最好的、最易于分类的投影线。

+

维空间到一维空间的一般数学变换方法:

+

假设有一集合包含维样本,其中个属于类的样本记为子集个属于类的样本记为子集,若对的分量做线性组合可得标量:,这样便得到个一维样本组成的集合,并可分为两个子集

+

实际上,的值是无关紧要的,它仅是乘上一个比例因子,重要的是选择的方向。的方向不同,将使样本投影后的可分离程度不同,从而直接影响分类效果。因此,上述寻找最佳投影方向的问题,在数学上就是寻找最好的变换向量的问题。

+

Fisher准则函数中的基本参量:

+

空间:

+

各类样本的均值向量

+

样本类内离散度矩阵:

+

总样本类内离散度矩阵:(对称半正定矩阵)

+

样本类间离散度矩阵:(对称半正定矩阵)

+

在一维空间:

+

各类样本的均值:

+

样本类内离散度:

+

总样本类内离散度:

+

我们希望投影后,在一维空间中各类样本尽可能分得开些,即希望两类均值之差越大越好,同时希望各类样本内部尽量密集,即希望类内离散度越小越好。

+

Fisher准则函数:将其推导为的显函数:

+

然后使用Lagrange乘数法求解,最终解得

+

事实上,Fisher的降维就相当于找一个线性判别函数。投影后的变化得来的,就相当于线性判别。

+

多类情形:

+

类间散度矩阵与两类情形略有不同:原来度量的是两个均值点的散列情况,现在度量的是每类均值点相对于样本中心的散列情况

+

推导可得:

+

感知器算法

+

一旦判别函数的形式确定下来,不管它是线性的还是非线性的,剩下的问题就是如何确定它的系数。在模式识别中,系数确定的一个主要方法就是通过对已知样本的训练和学习来得到。感知器算法就是通过训练样本模式的迭代和学习,产生线性(或广义线性)可分的模式判别函数。

+

基本思想:采用感知器算法能通过对训练模式样本集的“学习”得到判别函数的系数。不需要对各类别中模式的统计性质做任何假设,因此称为确定性的方法。

+

感知器作为人工神经网络中最基本的单元,由多个输入和一个输出组成。

+

已知两个训练模式集分别属于类和类,权向量的初始值为,可任意取值。

+

,若

+

次的训练步骤为:

+

,则分类器对第个模式做了错误分类,此时应校正权向量,使得,其中为一个校正增量。

+

,则分类器对第个模式做了错误分类,此时应校正权向量,使得,其中为一个校正增量。

+

若以上情况不符合,则表明该模式样本在第次中分类正确,因此权向量不变

+
    +
  • 对正确分类的模式则“赏”,实际上是“不罚”,即权向量不变。
  • +
  • 对错误分类的模式则“罚”,使加上一个正比于的分量。
  • +
  • 当用全部模式样本训练过一轮以后,只要有一个模式是判别错误的,则需要进行下一轮迭代,即用全部模式样本再训练一次。
  • +
  • 如此不断反复直到全部模式样本进行训练都能得到正确的分类结果为止。
  • +
+

感知器算法的收敛性:只要模式类别是线性可分的,就可以在有限的迭代步数里求出权向量。

+

采用感知器算法的多类模式的分类

+

采用多类情况3,将感知器算法推广到多类模式。

+

多类情况3:对类模式存在个判别函数,若, 则

+

设有种模式类别,若在训练过程的第次迭代时,一个属于类的模式样本送入分类器,则应先计算出个判别函数:。若的条件成立,则权向量不变,即

+

若其中第个权向量使得,则相应的权向量应做调整,即

+

+

+

+

其中是一个正常数。权向量的初始值可视情况任意选择。

+

这里的分类算法都是通过模式样本来确定判别函数的系数,但一个分类器的判断性能最终要受并未用于训练的那些未知样本来检验。要使一个分类器设计完善,必须采用有代表性的训练数据,它能够合理反映模式数据的整体。

+

要获得一个判别性能好的线性分类器,直观上训练样本越多越好,但实际上能收集到的样本数目会受到客观条件的限制,且过多的训练样本在训练阶段会使计算机需要较长的运算时间。一般来说,合适的样本数目可如下估计:若是模式的维数,令,则通常选用的训练样本数目约为的10~20倍。

+

感知器算法的解与初值的选择和迭代过程中误分类点的选择顺序有关。

+

可训练的确定性分类器的迭代算法

+

梯度法

+

设函数 是向量 的函数, 则 的梯度定义为

+

+

导出的一般关系式是一个正的比例因子(步长)

+

梯度是一个向量,它的最重要性质就是指出了函数在其自变量增加时最大增长率的方向。负梯度指出的最陡下降方向,利用这个性质可以设计一个迭代方案来寻找函数的最小值。

+

定义一个对错误分类敏感的准则函数。先任选一个初始权向量,计算准则函数的梯度,然后从出发,在最陡方向(梯度方向)上移动某一距离得到下一个权向量

+

C值的选择是很重要的。若C值太小,则收敛太慢;若C值太大,则搜索可能过头,引起发散。

+

固定增量的逐次调整算法

+

设取准则函数为:

+

的微分式:,其中

+

则由梯度法中的关系有:

+

其中是训练模式样本,是指第次迭代。

+

若模式是线性可分的,选择合适的准则函数,算法就能给出解。若模式不是线性可分的,算法的结果就会来回摆动,得不到收敛。

+

最小平方误差(LMSE)算法

+

感知器算法只是当被分模式可用一个特定的判别界面分开时才收敛,在不可分情况下,只要计算程序不终止,它就始终不收敛。即使在模式可分的情况下,也很难事先算出达到收敛时所需要的迭代次数。这样,在模式分类过程中,有时候会出现一次又一次迭代却不见收敛的情况,白白浪费时间。为此需要知道:发生迟迟不见收敛的情况时,到底是由于收敛速度过慢造成的呢,还是由于所给的训练样本集不是线性可分造成的呢?

+

最小平方误差(LMSE)算法,除了对可分模式是收敛的以外,对于类别不可分的情况也能指出来。

+

求两类问题的解相当于求一组线性不等式的解,因此,若给出分别属于的两个模式样本的训练样本集,即可求出其权向量的解。

+

设两类模式的训练样本总数为,写成增广形式,则有不等式组

+

+

+

H-K算法:

+

模式类别可分性的判别:

+

当不等式组有解时,该算法对收敛,可求得解

+
    +
  • ,即,有解。
  • +
  • ,此时隐含的条件,有解。若继续进行迭代,可使
  • +
  • 的全部分量停止变为正值(但不是全部为零),表明该模式类别线性不可分。因此,若没有一个分量为正值,则不会再变化,所以不能求得解。
  • +
+

固定增量算法与LMSE算法的比较:

+
    +
  • 固定增量算法:实现相对简单,可直接引伸到多类模式的分类情况,但未提供模式线性可分的测试特征;
  • +
  • LMSE算法:相对复杂,需要对求逆(维数高时求逆比较困难),但对两类情况,提供了线性可分的测试特征。
  • +
+

势函数法-一种确定性的非线性分类算法

+

用势函数的概念来确定判别函数划分类别界面

+

基本思想:

+
    +
  • 假设要划分属于两种类别的模式样本,这些样本可看成是分布在维模式空间中的点
  • +
  • 把属于的点比拟为某种能源点,在点上,电位达到峰值。
  • +
  • 随着与该点距离的增大,电位分布迅速减小,即把样本附近空间点上的电位分布,看成是一个势函数
  • +
  • 对于属于的样本集群,其附近空间会形成一个“高地”,这些样本点所处的位置就是“山头”。
  • +
  • 同理,用电位的几何分布来看待属于的模式样本,在其附近空间就形成“凹地”。
  • +
  • 只要在两类电位分布之间选择合适的等高线,就可以认为是模式分类的判别函数。
  • +
+

判别函数的产生

+

模式分类的判别函数可由分布在模式空间中的许多样本向量的势函数产生。任意一个样本所产生的势函数以表征,则判别函数可由势函数序列来构成,序列中的这些势函数相应于在训练过程中输入机器的训练模式样本。在训练状态,模式样本逐个输入分类器,分类器就连续计算相应的势函数,在第步迭代时的积累位势决定于在该步前所有的单独势函数的累加。以表示积累位势函数,若加入的训练样本是错误分类,则积累函数需要修改,若是正确分类,则不变。

+

从势函数可以看出,积累位势起着判别函数的作用:

+
    +
  • 属于时,
  • +
  • 属于时,,则积累位势不做任何修改就可用作判别函数。
  • +
+

由于一个模式样本的错误分类可造成积累位势在训练时的变化,因此势函数算法提供了确定两类判别函数的迭代过程。

+

判别函数表达式:取,则有

+

势函数的选择

+

选择势函数的条件:一般来说,若两个维向量的函数同时满足下列三个条件,则可作为势函数。

+
    +
  • ,并且当且仅当时达到最大值;
  • +
  • 当向量的距离趋于无穷时,趋于零;
  • +
  • 是光滑函数,且是之间距离的单调下降函数。
  • +
+

第一类势函数:可用对称的有限多项式展开:

+

在模式定义域内为正交函数集。

+

将这类势函数代入判别函数:,其中

+

因此,积累位势可写成可用迭代式求得。

+

第二类势函数:选择双变量的对称函数作为势函数,即,并且它可展开成无穷级数。

+

例如:

+

+

是正常数

+

+

用第二类势函数,当训练样本维数和数目都较高时,需要计算和存储的指数项较多。

+

因为势函数由许多新项组成,因此有很强的分类能力。

+

决策树简介

+

决策树,或称多级分类器,是模式识别中进行分类的一种有效方法,对于多类或多峰分布问题,这种方法尤为方便。利用树分类器可以把一个复杂的多类别分类问题,转化为若干个简单的分类问题来解决。它不是企图用一种算法、一个决策规则去把多个类别一次分开,而是采用分级的形式,使分类问题逐步得到解决。

+

一般来讲,一个决策树由一个根节点,一组非终止节点和一些终止节点组成,可对标以各种类别标签,有时不同的终止节点上可以出现相同的类别标签。

+

如果用表示决策树,则一个决策树对应于特征空间的一种划分,它把特征空间分成若干个区域,在每个区域中,某类的样本占优势,因此可以标出该类样本的类别标签。

+

决策树的一种简单形式是二叉树,它是指除叶结点外,树的每个节点仅分为两个分支,即每个非终止节点都有且仅有两个子节点

+

二叉树结构分类器可以把一个复杂的多类别分类问题转化为多级多个两类问题来解决,在每个非终止节点都把样本集分成左右两个子集。分成的每一部分仍然可能包含多个类别的样本,可以把每一部分再分成两个子集,如此下去,直至分成的每一部分只包含同一类别的样本,或某一类样本占优势为止。

+

二叉树结构分类器概念简单、直观、便于解释,而且在各个节点上可以选择不同的特征和采用不同的决策规则,因此设计方法灵活多样,便于利用先验知识来获得一个较好的分类器。

+

在设计一个决策树时,主要应解决以下几个问题:

+
    +
  • 选择一个合适的树结构,即合理安排树的节点和分支;
  • +
  • 确定在每个非终止节点上要使用的特征;
  • +
  • 在每个非终止节点上选择合适的决策规则。
  • +
+

把一个多类别分类问题转化为两类问题的形式是多种多样的,因此,对应的二叉树的结构也是各不相同的。通常的目的是要找一个最优的决策树。一个性能良好的决策树结构应该具有小的错误率和低的决策代价。但是由于很难把错误率的解析表达式和树的结构联系起来,而且在每个节点上所采用的决策规则也仅仅是在该节点上所采用的特征观测值的函数,因此,即使每个节点上的性能都达到最优,也不能说整个决策树的性能达到最优。在实际问题中,人们往往提出其它一些优化准则,例如极小化整个树的节点数目,或从根节点到叶结点的最大路经长度,或从根节点到叶结点的平均路经长度等,然后采用动态规划的方法,力争设计出能满足某种准则的“最优”决策树。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:模式识别与机器学习-第3章 判别函数
+
https://zhangzhao219.github.io/2022/09/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-3/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/12/UCAS/information-retrieval/information-retrieval-5/index.html b/2022/09/12/UCAS/information-retrieval/information-retrieval-5/index.html new file mode 100644 index 000000000..6e589a4af --- /dev/null +++ b/2022/09/12/UCAS/information-retrieval/information-retrieval-5/index.html @@ -0,0 +1,847 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第5讲 文档评分、词项权重计算及向量空间模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第5讲 文档评分、词项权重计算及向量空间模型

+ + +
+ +

《现代信息检索》课程笔记:第5讲 文档评分、词项权重计算及向量空间模型

+ +

第5讲 文档评分、词项权重计算及向量空间模型

+

布尔检索

+

布尔检索的优点:

+
    +
  • 对自身需求和文档集性质非常了解的专家而言,布尔查询是不错的选择
  • +
  • 对应用开发来说也非常简单,很容易就可以返回1000多条结果
  • +
+

布尔检索的不足:

+
    +
  • 对大多数用户来说不方便
  • +
  • 大部分用户不能撰写布尔查询或者他们认为需要大量训练才能撰写出合适的布尔查询
  • +
  • 大部分用户不愿意逐条浏览1000多条结果,特别是对Web搜索
  • +
  • 布尔查询常常会导致过少(=0)或者过多(>1000)的结果
  • +
+

在布尔检索中,需要大量技巧来生成一个可以获得合适规模结果的查询

+

排序式检索

+

排序式检索会对查询和文档的匹配程度进行排序,即给出一个查询和文档匹配评分

+

自由文本查询:与布尔查询不同,在排序式检索应用中,用户查询通常都是一个或几个关键字

+

排序式检索可以解决返回结果过少或过多的问题,可以把相关的结果排在前面

+

希望文档集中相关度高的文档排名高于相关度低的文档:对每个查询-文档对赋一个[0, 1]之间的分值,度量了文档和查询的匹配程度

+

Jaccard系数:计算两个集合重合度的常用方法,也就是计算查询文档之间的词项重合度——交集/并集

+

Jaccard系数的不足:

+
    +
  • 不考虑词项频率 ,即词项在文档中的出现次数
  • +
  • 一般而言,罕见词比高频词的信息量更大,Jaccard系数没有考虑这个信息
  • +
  • 没有仔细考虑文档的长度因素
  • +
+

词项频率

+

查询-文档匹配评分计算:

+

从单词项查询(查询只包含一个词项)开始,若该词项不出现在文档当中,该文档得分应该为0,该词项在文档中出现越多,则得分越高。

+

即为词项频率 (term frequency,TF)评分

+

词袋(Bag of words)模型:不考虑词在文档中出现的顺序

+

利用tf来计算文档评分的方法:采用原始的tf值(raw tf)

+

但是原始tf不太合适:某个词项在A文档中出现十次,即tf = 10,在B文档中tf = 1,那么A比B更相关,但是相关度不会相差10倍。

+

替代原始tf的方法:对数词频

+

tf-idf权重计算

+

罕见词项比常见词所蕴含的信息更多

+

考虑查询中某个词项,它在整个文档集中非常罕见,但是某篇包含该词项的文档很可能相关,因此需要提高权重

+

常见词项的信息量不如罕见词,一篇包含该词项的文档当然比不包含该词项的文档的相关度要高,但是,这些词对于相关度而言并不是非常强的指示词。

+

文档频率(Document frequency, df):出现词项的文档数目

+
    +
  • 对于罕见词项我们希望赋予高权重
  • +
  • 对于常见词我们希望赋予正的低权重
  • +
+

idf 权重

+

是出现词项的文档数目

+

是和词项的信息量成反比的一个值

+

于是可以定义词项t的idf权重(逆文档频率):,其中是文档集中文档的数目

+

是反映词项的信息量的一个指标,是一种全局性指标,反应的是词项在全局的区别性。

+

对于单词项查询,idf对文档排序没有任何影响,idf 会影响至少包含2个词项的查询的文档排序结果

+

词项的tf-idf权重是tf权重和idf权重的乘积:

+

tf-idf权重:

+
    +
  • 随着词项频率的增大而增大(局部信息)
  • +
  • 随着词项罕见度的增加而增大(全局信息)
  • +
+

向量空间模型

+

二值-tfidf矩阵

+

文档表示成向量:每篇文档表示成一个基于tfidf权重的实值向量 ∈ R|V|。有一个|V|维实值空间,空间的每一维都对应词项,文档都是该空间下的一个点或者向量。

+

查询看成向量:

+
    +
  • 关键思路1:对于查询做同样的处理,即将查询表示成同一高维空间的向量
  • +
  • 关键思路2:按照文档对查询的邻近程度排序,邻近度 = 相似度,邻近度≈ 距离的反面
  • +
+

向量空间下相似度:利用余弦相似度

+

文档长度归一化:一个向量可以通过除以它的长度进行归一化处理(防止长度影响)

+

问题:

+

余弦归一化倾向于短文档,即对短文档产生的归一化因子太大,而平均而言对长文档产生的归一化因子太小,因此余弦归一化对长文档的惩罚过重,实际上长文档中虽然词频较高,但也会包含较多的信息。

+

可以先找到一个支点(pivot,平衡点),然后通过这个支点对余弦归一化操作进行线性调整。因此短文档的相似度降低,而长文档的相似度增大,可以去除原来余弦归一化偏向短文档的问题

+

回转归一化:基本思想是旋转归一化曲线,使得两条曲线尽量重合

+

向量空间模型小结:

+
    +
  • 将查询表示成tf-idf权重向量
  • +
  • 将每篇文档表示成同一空间下的 tf-idf权重向量
  • +
  • 计算两个向量之间的某种相似度(如余弦相似度)
  • +
  • 按照相似度大小将文档排序
  • +
  • 将前K(如K =10)篇文档返回给用户
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第5讲 文档评分、词项权重计算及向量空间模型
+
https://zhangzhao219.github.io/2022/09/12/UCAS/information-retrieval/information-retrieval-5/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月12日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/13/Leetcode/Leetcode-101/Leetcode-101-12/index.html b/2022/09/13/Leetcode/Leetcode-101/Leetcode-101-12/index.html new file mode 100644 index 000000000..9f2941820 --- /dev/null +++ b/2022/09/13/Leetcode/Leetcode-101/Leetcode-101-12/index.html @@ -0,0 +1,1020 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第12章 字符串 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第12章 字符串

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第12章 字符串

+ +

字符串

+

字符串比较

+

Leetcode 242

+

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的字母异位词。注意:st 中每个字符出现的次数都相同,则称 st互为字母异位词。

+
class Solution {
+public:
+    bool isAnagram(string s, string t) {
+        sort(s.begin(),s.end());
+        sort(t.begin(),t.end());
+        if(s == t){
+            return true;
+        }
+        return false;
+    }
+};
+

分析:哈希表或者直接排序

+

一遍AC

+

Leetcode 205

+

给定两个字符串 st ,判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

+
class Solution {
+public:
+    bool isIsomorphic(string s, string t) {
+        unordered_map<char,char> mp1;
+        unordered_map<char,char> mp2;
+        for(int i=0;i<s.size();++i){
+            if(mp1.find(s[i]) == mp1.cend()){
+                mp1[s[i]] = t[i];
+            }
+            if(mp2.find(t[i]) == mp2.cend()){
+                mp2[t[i]] = s[i];
+            }
+            if(mp1[s[i]] != t[i] || mp2[t[i]] != s[i]){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

分析:通过字典比较即可

+

错误:开始想用统计的方法去做,后面用字符字典的方式也有一些小错误,应该是比较两遍的。

+

Leetcode 647

+

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

+
class Solution {
+public:
+    int countSubstrings(string s) {
+        int countsum = 0;
+        int n = s.size();
+        for(int i=0;i<n;++i){
+            countsum += 1;
+            int leftindex = i-1;
+            int rightindex = i+1;
+            while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){
+                ++countsum;
+                --leftindex;
+                ++rightindex;
+            }
+        }
+        for(int i=0;i<n-1;++i){
+            if(s[i] == s[i+1]){
+                ++countsum;
+                int leftindex = i-1;
+                int rightindex = i+2;
+                while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){
+                    ++countsum;
+                    --leftindex;
+                    ++rightindex;
+                }
+            }
+        }
+        return countsum;
+    }
+};
+

分析:遍历扩展即可,注意分两种情况讨论一下

+

一遍AC

+

Leetcode 696

+

给定一个字符串 s,统计并返回具有相同数量 01 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。重复出现(不同位置)的子串也要统计它们出现的次数。

+
class Solution {
+public:
+    int countBinarySubstrings(string s) {
+        int n = s.size();
+        int countsum = 0;
+        for(int i=0;i<n-1;++i){
+            if(s[i] != s[i+1]){
+                ++countsum;
+                int leftindex = i-1;
+                int rightindex = i+2;
+                while(leftindex >= 0 && rightindex < n && s[leftindex] == s[leftindex+1] && s[rightindex] == s[rightindex-1]){
+                    ++countsum;
+                    --leftindex;
+                    ++rightindex;
+                }
+            }
+        }
+        return countsum;
+    }
+};
+

分析:和上一道题目相同,甚至只考虑一种情况就可以了,比上一道题目还要简单一点。

+

一遍AC

+

字符串理解

+

Leetcode 227

+

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。整数除法仅保留整数部分。

+
class Solution {
+public:
+    int calculate(string s) {
+        vector<int> stk;
+        char preSign = '+';
+        int num = 0;
+        int n = s.length();
+        for (int i = 0; i < n; ++i) {
+            if (isdigit(s[i])) {
+                num = num * 10 + int(s[i] - '0');
+            }
+            if (!isdigit(s[i]) && s[i] != ' ' || i == n - 1) {
+                switch (preSign) {
+                    case '+':
+                        stk.push_back(num);
+                        break;
+                    case '-':
+                        stk.push_back(-num);
+                        break;
+                    case '*':
+                        stk.back() *= num;
+                        break;
+                    default:
+                        stk.back() /= num;
+                }
+                preSign = s[i];
+                num = 0;
+            }
+        }
+        return accumulate(stk.begin(), stk.end(), 0);
+    }
+};
+

分析:栈和字符串的应用

+

错误:最后的运算顺序有问题,没有能自己实现。

+

字符串匹配

+

Leetcode 28

+

给你两个字符串 haystackneedle,请你在 haystack字符串中找出 needle字符串的第一个匹配项的下标(下标从 0开始)。如果 needle不是 haystack的一部分,则返回 -1

+
class Solution {
+public:
+    int strStr(string haystack, string needle) {
+        int m = haystack.size();
+        int n = needle.size();
+        for(int i=0;i<m-n+1;++i){
+            if(haystack.substr(i,n) == needle){
+                return i;
+            }
+        }
+        return -1;
+    }
+};
+

分析:可以使用KMP算法,但是不会,简单一点就直接字符串匹配即可。

+

一遍AC

+

练习

+

Leetcode 409

+

给定一个包含大写字母和小写字母的字符串 s ,返回通过这些字母构造成的最长的回文串。在构造过程中,请注意区分大小写 。比如 "Aa" 不能当做一个回文字符串。

+
class Solution {
+public:
+    int longestPalindrome(string s) {
+        unordered_map<char,int> mp;
+        int n = s.size();
+        for(int i=0;i<s.size();++i){
+            if(mp.find(s[i]) == mp.cend()){
+                mp[s[i]] = 1;
+            }
+            else{
+                ++mp[s[i]];
+            }
+        }
+        int ans = 0;
+        int sign = 0;
+        for(auto it : mp){
+            if(it.second % 2 == 0){
+                ans += it.second;
+            }
+            else{
+                if(sign == 0){
+                    ans += it.second;
+                    sign = 1;
+                }
+                else{
+                    ans = ans + it.second / 2 * 2;
+                }
+            }
+        }
+        return ans;
+    }
+};
+

分析:统计数数即可

+

一遍AC

+

Leetcode 3

+

给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

+
class Solution {
+public:
+    int lengthOfLongestSubstring(string s) {
+        map<char,int> mp;
+        int n = s.size();
+        int right = 0;
+        int maxlen = 0;
+        int left = 0;
+        while(left < n){
+            while(right < n && (mp.find(s[right]) == mp.cend() || mp[s[right]] == 0)){
+                ++mp[s[right]];
+                ++right;
+            }
+            maxlen = max(maxlen,right-left);
+            --mp[s[left]];
+            ++left;
+        }
+        return maxlen;
+    }
+};
+

分析:滑动窗口经典算法

+

错误:与或非的括号忘记添加了

+

Leetcode 772

+

付费题目

+

Leetcode 5

+

给你一个字符串 s,找到 s 中最长的回文子串。

+
class Solution {
+public:
+    string longestPalindrome(string s) {
+        int countsum = 1;
+        string result = s.substr(0,1);
+        int n = s.size();
+        for(int i=0;i<n;++i){
+            int temp = 1;
+            int leftindex = i-1;
+            int rightindex = i+1;
+            while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){
+                temp += 2;
+                --leftindex;
+                ++rightindex;
+            }
+            if(temp > countsum){
+                countsum = temp;
+                result = s.substr(leftindex+1,rightindex-1-(leftindex+1)+1);
+            }
+        }
+        for(int i=0;i<n-1;++i){
+            if(s[i] == s[i+1]){
+                int temp = 2;
+                int leftindex = i-1;
+                int rightindex = i+2;
+                while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){
+                    temp += 2;
+                    --leftindex;
+                    ++rightindex;
+                }
+                if(temp > countsum){
+                    countsum = temp;
+                    result = s.substr(leftindex+1,rightindex-1-(leftindex+1)+1);
+                }
+            }
+        }
+        return result;
+    }
+};
+

分析:还是这种题,都第三道了

+

错误:开始有些索引没考虑好错了一些,后来调通了。

+

总结

+

字符串还可以,主要是熟悉一下字符串的处理过程,其余的知识点其他的数据结构中都有。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第12章 字符串
+
https://zhangzhao219.github.io/2022/09/13/Leetcode/Leetcode-101/Leetcode-101-12/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/13/UCAS/advanced-ai/advanced-ai-3/index.html b/2022/09/13/UCAS/advanced-ai/advanced-ai-3/index.html new file mode 100644 index 000000000..959b0d120 --- /dev/null +++ b/2022/09/13/UCAS/advanced-ai/advanced-ai-3/index.html @@ -0,0 +1,1129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第3讲 人工神经网络 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第3讲 人工神经网络

+ + +
+ +

《高级人工智能》课程笔记:第3讲 人工神经网络

+ +

第3讲 人工神经网络

+

联结主义学派:又称仿生学派或生理学派

+
    +
  • 认为人的思维基元是神经元,而不是符号处理过程
  • +
  • 认为人脑不同于电脑
  • +
+

核心:智能的本质是联接机制。

+

原理:神经网络及神经网络间的连接机制和学习算法

+

什么是神经网络

+
    +
  • 所谓的人工神经网络就是基于模仿生物大脑的结构和功能而构成的一种信息处理系统(计算机)。
  • +
  • 个体单元相互连接形成多种类型结构的图 +
      +
    • 循环、非循环
    • +
    • 有向、无向
    • +
    +
  • +
  • 自底向上(Bottom-Up)AI +
      +
    • 起源于生物神经系统
    • +
    • 从结构模拟到功能模拟
    • +
    +
  • +
+

发展历史

+
    +
  • 1940年代 +
      +
    • 心理学家McCulloch和数学家Pitts建立了阈值加权和模型(1943)
    • +
    • 心理学家Hebb提出神经元之间突触联系是可变(可学习)的假说——Hebb学习律(1949)
    • +
    +
  • +
  • 1950年代、1960年代 +
      +
    • 提出并完善了单级感知器(Perceptron)
    • +
    • 代表性人物:Marvin Minsky,Frank Rosenblatt,Bernard Widrow
    • +
    +
  • +
  • 1980年代 +
      +
    • J.Hopfield提出Hopfield网络(1984)
    • +
    • Hinton、Sejnowsky、Rumelhart等人提出了著名的Boltzmann机(1985)
    • +
    • Rumelhart等提出多层网络的学习算法—BP算法(1986)
    • +
    +
  • +
  • 2000年代 +
      +
    • Hinton et al. Deep Neural Networks (2007)
    • +
    +
  • +
+

生物学启示

+

生物神经元

+
    +
  • 神经元组成:细胞体,轴突,树突,突触
  • +
  • 神经元之间通过突触两两相连。信息的传递发生在突触。
  • +
  • 突触记录了神经元间联系的强弱。
  • +
  • 只有达到一定的兴奋程度,神经元才向外界传输信息。
  • +
+

神经元特性

+
    +
  • 信息以预知的确定方向传递:一个神经元的树突-细胞体-轴突-突触-另一个神经元树突
  • +
  • 时空整合性 +
      +
    • 对不同时间通过同一突触传入的信息具有时间整合功能
    • +
    • 对同一时间通过不同突触传入的信息具有空间整合功能
    • +
    +
  • +
+

工作状态

+
    +
  • 兴奋状态,对输入信息整合后使细胞膜电位升高,当高于动作电位的阈值时,产生神经冲动,并由轴突输出。
  • +
  • 抑制状态,对输入信息整合后使细胞膜电位降低,当低于动作电位的阈值时,无神经冲动产生。
  • +
+

结构的可塑性:神经元之间的柔性连接:突触的信息传递特性是可变的——学习记忆的基础

+

神经元模型

+

从生物学结构到数学模型

+

人工神经元

+

vxtgc4.md.png

+

为激活函数,为组合函数

+

组合函数:

+

权重和:

+

+

径向距离:

+

+

激活函数

+

vxtH3D.md.png

+

人工神经网络(ANN)

+
    +
  • 多个人工神经元按照特定的网络结构联接在一起,就构成了一个人工神经网络。
  • +
  • 神经网络的目标就是将输入转换成有意义的输出。
  • +
+

生物系统中的学习:

+
    +
  • 自适应学习:适应的目标是基于对环境信息的响应获得更好的状态
  • +
  • 在神经层面上,通过突触强度的改变实现学习:消除某些突触,建立一些新的突触
  • +
  • Hebb学习律:神经元同时激活,突触强度增加,异步激活,突触强度减弱
  • +
  • 学习律符合能量最小原则:保持突触强度需要能量,所以在需要的地方保持,在不需要的地方不保持。
  • +
+

ANN的学习规则:能量最小

+

对人工神经网络,需要确定合适的能量定义;可以使用数学上的优化技术来发现如何改变神经元间的联接权重。

+

两个主要问题:结构和学习方法

+

ANN结构

+
    +
  • 前馈结构:没有循环,静态的
  • +
  • 反馈/循环结构:有循环,动态的
  • +
+

ANN的学习方法:通过神经网络所在环境的模拟过程,调整网络中的自由参数。

+

学习策略:Error Correction:最小化实际输出与期望输出之间的误差,属于监督学习。

+

多层感知机

+

vzp70x.md.png

+

感知机实质上是一种神经元模型

+

阈值激活函数:

+

判别规则:

+

输入空间中

+
    +
  • 样本是空间中的一个点
  • +
  • 权向量是一个超平面
  • +
  • 超平面一边对应,另一边对应
  • +
+

单层感知机学习:用现在的权重进行分类,如果分类正确,权重不改变;如果分类错误,用分类错误的样本调整权重

+

感知机收敛定理:若训练数据集是线性可分的,则感知机模型收敛。

+

感知机存在的问题:如果存在噪声,或样本不是线性可分的,不会收敛。(例如不能处理异或操作),且泛化性比较差。

+

多层感知机:三层可以学习全部连续的函数,四层就可以学习全部的函数。层间神经元全连接,层内神经元不连接。

+

学习方法:反向传播

+
    +
  • 输入数据从输入层经过隐藏层传递到输出层
  • +
  • 误差信息从输出层反向传播,通过隐藏层传递到输入层
  • +
+

全局误差度量:(最小平方误差)

+

权值更新规则采用梯度下降的方法:

+

vzCYi8.md.png

+

vzCtJS.md.png

+

vzCdMj.md.png

+

vzCwss.md.png

+

误差反向传播:

+

vzC0Ln.md.png

+

实际应用中要对数据进行归一化,并且选择合适的学习率

+

优点:

+
    +
  • 很强的表达能力
  • +
  • 容易执行
  • +
+

缺点:

+
    +
  • 收敛速度慢(采用Newton法)
  • +
  • 过拟合(Over-fitting)(加正则化项,约束权值的平滑性;采用更少(但足够数量)的隐层神经元)
  • +
  • 局部极小(尝试不同的初始化,增加扰动)
  • +
+

多层感知机解决了一般性学习问题,并且与生物系统相联系。

+

层数增加使用BP算法会存在梯度消失的问题:在后面的几层,误差反向传播后可能变得非常小,权重不太好更新。

+

采用sigmoid函数,多个相乘使得传递过来的残差会越来越小。

+

深度学习

+

时代背景:数据爆炸、计算性能提升

+

传统机器学习解决问题的思路:

+
    +
  • 良好的特征表达,对最终算法的准确性起了非常关键的作用,而且系统主要的计算和测试工作都耗在这一大部分。
  • +
  • 但实际中一般都是人工完成的。
  • +
+

使用深度学习去自动学习特征!

+

人脑视觉机理

+
    +
  • “视觉系统的信息处理”:可视皮层是分级的
  • +
  • 神经-中枢-大脑的工作过程,或许是一个不断迭代、不断抽象的过程。
  • +
  • 关键词:一个是抽象,一个是迭代。
  • +
  • 从原始信号,做低级抽象,逐渐向高级抽象迭代。人类的逻辑思维,经常使用高度抽象的概念。
  • +
+

为什么使用深度学习?

+
    +
  • 深层结构能够有效被表达 +
      +
    • 对相同的函数需要更少的计算单元
    • +
    • 深层网络结构中,高层可以综合应用低层信息
    • +
    +
  • +
  • 深层结构可产生层次化特征表达 +
      +
    • 可解释性,更具有语义化信息
    • +
    +
  • +
  • 多层隐变量允许统计上的组合共享
  • +
  • 深层结构有效(vision, audio, NLP等)!
  • +
+

深层 vs 浅层神经网络

+
    +
  • 多隐层的人工神经网络具有优异的特征学习能力,学习得到的特征对数据有更本质的刻画,从而有利于可视化或分类 +
      +
    • 深层网络结构中,高层可以综合应用低层信息
    • +
    • 低层关注“局部”,高层关注“全局”、更具有语义化
    • +
    +
  • +
  • “深度模型”是手段,“特征学习”是目的。 +
      +
    • 强调了模型结构的深度,通常有5层、6层,甚至10多层的隐层节点;
    • +
    • 明确突出了特征学习的重要性,也就是说,通过逐层特征变换,将样本在原空间的特征表示变换到一个新特征空间,从而使分类或预测更加容易。
    • +
    +
  • +
  • 与人工规则构造特征的方法相比,利用大数据来学习特征,更能够刻画数据的丰富内在信息。
  • +
+

BP算法的问题:

+
    +
  • 需要带标签训练数据 +
      +
    • 几乎所有的数据是无标签的
    • +
    • 人脑可以从无标签数据中学习
    • +
    +
  • +
  • 局部极小 +
      +
    • 对深层网络远离了最优解
    • +
    +
  • +
  • 梯度消失
  • +
+

Deep learning训练:

+

自下向上的非监督学习(greedy layer-wise training)

+
    +
  • 把网络逐层进行预训练,或者找一个足够好的初始权重。
  • +
+

自顶向下的监督学习

+
    +
  • 就是通过带标签的数据去训练,误差自顶向下传输,对网络进行微调
  • +
  • 微调特征(中间层),使得与问题更相关。
  • +
+

对输入的结构建模:建立产生输入的生成式模型,调整参数使得生成式模型的概率最大。

+

Deep Learning的常用模型

+

AutoEncoder自动编码器

+

学习过程:无标签数据,用非监督学习学习特征

+
    +
  • 将input输入一个encoder编码器,就会得到一个code,这个code也就是输入的一个表示
  • +
  • 增加一个decoder解码器
  • +
  • 通过调整encoder和decoder的参数,使得重构误差最小,这样就得到输入input信号的一个表示了,也就是编码code。
  • +
  • 输入无标签数据,误差的来源就是直接重构后与原输入相比得到。
  • +
+

利用人工神经网络本身的层次结构特点

+
    +
  • 如果给定一个神经网络,假设其输出与输入是相同的,然后训练调整其参数,得到每一层中的权重。
  • +
  • 自然地,就得到了输入I的几种不同表示(每一层代表一种表示),这些表示就是特征。
  • +
+

自动编码器就是一种尽可能复现输入信号的神经网络。

+

为了实现这种复现,自动编码器就必须捕捉可以代表输入数据的最重要的因素

+

网络结构

+
    +
  • 三层结构:输入层,隐藏层,输出层
  • +
  • 限定神经元的数量 +
      +
    • 输入层神经元数=输出层神经元数
    • +
    • 隐层神经元数量<输入层神经元数量
    • +
    • 意义:迫使隐藏层节点学习得到输入数据的压缩表示方法
    • +
    +
  • +
+

自动编码器可以只训练单组参数,不需要关心另一半的参数。

+

Deep结构——逐层训练

+
    +
  • 自编码器“栈化”
  • +
  • 通过编码器产生特征,然后训练下一层。得到第一层的code,重构误差最小让我们相信这个code就是原输入信号的良好表达了,或者牵
    +强点说,它和原信号是一模一样的(表达不一样,反映的是一个东西)。将第一层输出的code当成第二层的输入信号,同样最小化重构误差,就会得到第二层的参数,并且得到第二层输入的code,也就是原输入信息的第二个表达了。其他层也以同样的方法进行。
  • +
+

监督学习

+
    +
  • Deep结构,每一层都会得到原始输入的不同层次的表达。
  • +
  • 有监督微调 +
      +
    • 为了实现分类,可以在AutoEncoder的最顶的编码层添加一个分类器(例如Logistic回归、SVM等),然后通过标准的多层神经网络的监督训练方法(梯度下降法)去训练。
    • +
    +
  • +
  • 最后层的特征code输入到分类器中,基于有标签样本,通过监督学习对网络进行微调 +
      +
    • 只调整分类器
    • +
    • 通过有标签样本,微调整个系统
    • +
    +
  • +
+

两隐层自编码网络MNIST手写数字识别:

+

训练一个包含两个隐含层的栈式自编码网络,用来进行MNIST手写数字分类

+
    +
  1. 用原始输入训练第一个自编码器,学习得到原始输入的一阶特征表示
  2. +
  3. 把上一层的一阶特征作为另一个稀疏自编码器的输入,使用它们来学习二阶特征
  4. +
  5. 将二阶特征作为softmax分类器的输入,训练得到一个能将二阶特征映射到数字标签的模型
  6. +
  7. 将这三层结合起来构成一个栈式自编码网络,通过反向传播算法(BP)同时调整所有层的参数以改善学习结果(称为整体微调finetuning)
  8. +
+

栈式自编码器神经网络

+
    +
  • 栈式自编码神经网络具有强大的表达能力及深度神经网络的所有优点。
  • +
  • 通常能够获取到输入的“层次型分组”或者“部分-整体分解”结构。 +
      +
    • 学习方式:前层的输出作为下一层输入的方式依次训练。
    • +
    • 如果网络的输入数据是图像,网络的第一层会学习如何去识别边,第二层一般会学习如何去组合边,从而构成轮廓、角等。更高层会学习如何去组合更形象且有意义的特征。
    • +
    • 如果输入数据集包含人脸图像,更高层会学习如何识别或组合眼睛、鼻子、嘴等人脸器官。
    • +
    +
  • +
+

Deep Belief Networks(DBN)和Deep Boltzmann Machine(DBM)

+

Hopfield Network

+

结构:

+
    +
  • 单层全互连、对称权值的反馈网络
  • +
  • 状态:-1(0),+1
  • +
+

vzFHYV.png

+

Hopfield网络按动力学方式运行,其工作过程为状态的演化过程,即从初始状态按能量减小的方向进行演化,直到达到稳定状态。稳定状态即为网络的输出。

+

二值随机神经元(Bernoulli variables):以一定的概率产生1

+

波尔兹曼机(Boltzmann Machine):

+
    +
  • 结构类似于Hopfield 网络,但它是具有隐单元的反馈互联网络
  • +
  • 遵循波尔兹曼分布,学习数据的固有内在表示。
  • +
  • 结构:一个可见层+一个隐层,层内有连接
  • +
+

BM基本原理:

+
    +
  1. Hopfield网络的神经元的结构功能及其在网络中的地位是一样的。但BM中一部分神经元与外部相连,可以起到网络的输入、输出功能,或者严格地说可以受到外部条件的约束。另一部分神经元则不与外部相连,因而属于隐单元
  2. +
  3. 每个神经元只取1或0这两种状态:状态1代表该神经元处于接通状态,状态0代表该神经元处于断开状态
  4. +
+

缺点:网络结构复杂、训练代价大、局部极小

+

受限波尔兹曼机(Restricted Boltzmann Machines):

+
    +
  • 通过输入数据集学习概率分布的随机生成神经网络
  • +
  • 结构:一个可见层+一个隐层,层内无连接
  • +
  • RBM中,隐单元在给定可视单元情况下,条件独立
  • +
+

Deep Belief Networks:

+
    +
  • 概率生成模型
  • +
  • 深层结构——多层
  • +
  • 非监督的预学习提供了网络好的初始化
  • +
  • 监督微调(fine-tuning)
  • +
+

Deep Boltzmann Machines:

+
    +
  • 所有层间无向连接 +
      +
    • 同层神经元间无连接
    • +
    +
  • +
  • 高层表示由无标注数据建立
  • +
  • 标注数据仅用来微调网络
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第3讲 人工神经网络
+
https://zhangzhao219.github.io/2022/09/13/UCAS/advanced-ai/advanced-ai-3/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/14/Leetcode/Leetcode-101/Leetcode-101-13/index.html b/2022/09/14/Leetcode/Leetcode-101/Leetcode-101-13/index.html new file mode 100644 index 000000000..edb373d1c --- /dev/null +++ b/2022/09/14/Leetcode/Leetcode-101/Leetcode-101-13/index.html @@ -0,0 +1,986 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第13章 链表 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第13章 链表

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第13章 链表

+ +

链表

+

(单)链表是由节点和指针构成的数据结构,每个节点存有一个值,和一个指向下一个节点的指针,因此很多链表问题可以用递归来处理。不同于数组,链表并不能直接获取任意节点的值,必须要通过指针找到该节点后才能获取其值。同理,在未遍历到链表结尾时,我们也无法知道链表的长度,除非依赖其他数据结构储存长度。

+

链表的基本操作

+

Leetcode 206

+

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

+
class Solution {
+public:
+    ListNode* reverseList(ListNode* head) {
+        ListNode* p = nullptr;
+        ListNode* q = head;
+        while(q){
+            ListNode* r = q->next;
+            q->next = p;
+            p = q;
+            q = r;
+        }
+        return p;
+    }
+};
+

分析:两种方式,迭代法和递归法反转链表。

+

错误:算法忘记了,稍稍看了一眼后明白了

+

Leetcode 21

+

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

+
class Solution {
+public:
+    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
+        ListNode* result = new ListNode(-1);
+        ListNode* head = result;
+        while(list1 != nullptr && list2 != nullptr){
+            if(list1->val > list2->val){              
+                head->next = list2;
+                list2 = list2->next;
+            }
+            else{
+                head->next = list1;
+                list1 = list1->next;
+            }
+            head = head->next;
+        }
+        if(list1 != nullptr){
+            head->next = list1;
+        }
+        else{
+            head->next = list2;
+        }
+        return result->next;
+    }
+};
+

分析:按照顺序一点一点合并即可,前面设置一个头结点,后面把它扔掉返回。

+

错误:链表操作忘记了

+

Leetcode 24

+

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

+
class Solution {
+public:
+    ListNode* swapPairs(ListNode* head) {
+        ListNode* pre = new ListNode(-1);
+        pre->next = head;
+        head = pre;
+        while(pre->next != nullptr && pre->next->next != nullptr){
+            ListNode* p = pre->next;
+            ListNode* q = p->next;
+            pre->next = q;
+            p->next = q->next;
+            q->next = p;
+            pre = p;
+        }
+        return head->next;
+    }
+};
+

分析:链表操作

+

错误:已经不熟练了,不知道什么时候加结点什么的。

+

其它链表技巧

+

Leetcode 160

+

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

+
class Solution {
+public:
+    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
+        if(headA == nullptr || headB == nullptr){
+            return nullptr;
+        }
+        ListNode* pa = headA;
+        ListNode* pb = headB;
+        while(pa != pb){
+            if(pa == nullptr){
+                pa = headB;
+                pb = pb->next;
+            }
+            else if(pb == nullptr){
+                pb = headA;
+                pa = pa->next;
+            }
+            else{
+                pa = pa->next;
+                pb = pb->next;
+            }
+        }
+        return pa;
+    }
+};
+

分析:当链表headA和headB都不为空时,创建两个指针pA和pB,初始时分别指向两个链表的头节点headA和headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:每步操作需要同时更新指针pA和pB。如果指针pA不为空,则将指针pA移到下一个节点;如果指针 pB不为空,则将指针pB移到下一个节点。如果指针pA为空,则将指针pA移到链表headB的头节点;如果指针pB为空,则将指针pB移到链表headA的头节点。当指针pA和pB指向同一个节点或者都为空时,返回它们指向的节点或者null。

+

错误:不会做

+

Leetcode 234

+

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool isPalindrome(ListNode* head) {
+        vector<int> vt;
+        while(head != nullptr){
+            vt.push_back(head->val);
+            head = head->next;
+        }
+        int n = vt.size();
+        for(int i=0;i<n/2;++i){
+            if(vt[i] != vt[n-i-1]){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

分析:复制到数组中判断

+

一遍AC

+

练习

+

Leetcode 83

+

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。

+
class Solution {
+public:
+    ListNode* deleteDuplicates(ListNode* head) {
+        ListNode* p = head;
+        if(p == nullptr){
+            return nullptr;
+        }
+        while(p->next != nullptr){
+            ListNode* q = p->next;
+            if(p->val == q->val){
+                p->next = q->next;
+                q = p->next;
+            }
+            else{
+                q = q->next;
+                p = p->next;
+            }
+        }
+        return head;
+    }
+};
+

分析:遍历判断即可

+

错误:没有考虑链表中没有结点的情况。

+

Leetcode 328

+

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。

+
class Solution {
+public:
+    ListNode* oddEvenList(ListNode* head) {
+        if(head == nullptr){
+            return head;
+        }
+        ListNode* odd = head;
+        ListNode* even = head->next;
+        ListNode* evenhead = even;
+        while(even != nullptr && even->next != nullptr){
+            odd->next = even->next;
+            even->next = even->next->next;
+            odd = odd->next;
+            even = even->next;
+        }
+        odd->next = evenhead;
+        return head;
+    }
+};
+

分析:单独存储奇偶结点即可。

+

错误:还是不熟练

+

Leetcode 19

+

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

+
class Solution {
+public:
+    ListNode* removeNthFromEnd(ListNode* head, int n) {
+        ListNode* p = head;
+        int sum = 0;
+        while(p != nullptr){
+            ++sum;
+            p = p->next;
+        }
+        p = head;
+        int num = sum - n;
+        if(num == 0){
+            return head->next;
+        }
+        ListNode* pre = new ListNode(-1);
+        pre->next = p;
+        for(int i=0;i<num;++i){
+            pre = p;
+            p = p->next;
+        }
+        pre->next = p->next;
+        return head;
+    }
+};
+

分析:先数一遍一共有多少个结点,然后再遍历一遍删掉即可。

+

一遍AC

+

Leetcode 148

+

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

+
class Solution {
+public:
+    ListNode* sortList(ListNode* head) {
+        if(head == nullptr){
+            return head;
+        }
+        vector<int> result;
+        ListNode* p = head;
+        while(head != nullptr){
+            result.push_back(head->val);
+            head = head->next;
+        }
+        sort(result.begin(),result.end());
+        head = p;
+        int index = 0;
+        while(head != nullptr){
+            head->val = result[index++];
+            head = head->next;
+        }
+        return p;
+    }
+};
+

分析:可以用一些比较高大上的链表排序方法,也可以耍赖,直接读入数组中排序即可。

+

一遍AC

+

总结

+

链表不难,就是太容易忘记了,后面要经常复习。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第13章 链表
+
https://zhangzhao219.github.io/2022/09/14/Leetcode/Leetcode-101/Leetcode-101-13/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/14/UCAS/information-retrieval/information-retrieval-6/index.html b/2022/09/14/UCAS/information-retrieval/information-retrieval-6/index.html new file mode 100644 index 000000000..a98ff4a06 --- /dev/null +++ b/2022/09/14/UCAS/information-retrieval/information-retrieval-6/index.html @@ -0,0 +1,838 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第6讲 概率检索模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第6讲 概率检索模型

+ + +
+ +

《现代信息检索》课程笔记:第6讲 概率检索模型

+ +

第6讲 概率检索模型

+

向量空间模型的优缺点

+

优点:

+
    +
  • 简洁直观,可以应用到很多其他领域(文本分类、生物信息学)。
  • +
  • 支持部分匹配和近似匹配,结果可以排序
  • +
  • 检索效果不错
  • +
+

缺点:

+
    +
  • 理论上不够严谨,往往基于直觉的经验性公式
  • +
  • 词项之间的独立性假设与实际不符:实际上,词项的出现之间是有关系的,并不是完全独立的。
  • +
+

基本概率统计知识

+

检索系统中,给定查询,计算每个文档的相关度

+

检索系统对用户查询的理解是非确定的(uncertain),对返回结果的猜测也是非确定的

+

而概率理论为非确定推理提供了坚实的理论基础,可以计算文档和查询相关的可能性

+

概率检索模型是通过概率的方法将查询和文档联系起来

+

定义3个随机变量R、Q、D:相关度R={0,1},查询Q可以是q1,q2,…中的一个查询,文档D可以是d1,d2,…中的一篇文档,则可以通过计算条件概率P(R=1|Q=q,D=d)来度量文档和查询的相关度。

+

概率排序原理(PRP):

+
    +
  • 如果文档按照与查询的相关概率大小返回,那么该返回结果是所有可能获得结果中效果最好的。
  • +
  • 如果文档按照与查询的相关概率大小返回,而这些相关概率又能够基于已知数据进行尽可能精确的估计,那么该返回结果是所有基于已知数据获得的可能的结果中效果最好的。
  • +
+

Logistic回归模型

+

回归分析:回归分析是处理变量之间相关关系的一种工具,回归的结果可以用于预测或者分类

+

一元线性回归:根据观测点,拟合出一条直线,使得某种损失 (如离差平方和)最小

+

Logistic回归是一种非线性回归,可以转化成线性回归来实现。

+

基本思想:为了求Q和D相关的概率P(R=1|Q,D),通过定义多个特征函数fi(Q,D),认为P(R=1|Q,D)是这些函数的组合。

+

求解和使用过程:通过训练集合拟和得到相应系数 ,对于新的文档,代入公式计算得到概率P

+

优缺点:

+
    +
  • 优点:直接引入数学工具,形式简洁。
  • +
  • 缺点:特征选择非常困难,实验中效果一般。 +
      +
    • 以文档为样本(Pointwise)训练模型,无法解决不同查询之间的差异
    • +
    +
  • +
+

BIM模型

+

二值独立概率模型

+

BIM模型通过贝叶斯公式对所求条件概率P(R=1|Q,D)展开进行计算,是一种生成式(generative)模型

+

对每个Q定义排序(Ranking)函数RSV(Q,D)

+

其中,P(D|R=1)、P(D|R=0)分别表示在相关和不相关情况下生成文档D的概率。Ranking函数显然是随着P(R=1|D)的增长而增长。

+

两种常用的文档生成的总体分布:多元贝努利分布和多项式分布

+

BIM中P(D|R=1)或P(D|R=0)的计算:类比M次独立试验

+

BIM模型公式的推导:pi qi参数的计算,RSJ权重

+

BIM计算过程:目标是求排序函数P(D|R=1)/P(D|R=0)

+
    +
  • 首先估计或计算每个term分别在相关文档和不相关文档中的出现概率pi=P(t|R=1)及qi=P(t|R=0)
  • +
  • 然后根据独立性假设,将P(D|R=1)/P(D|R=0) 转化为pi和qi的某种组合,将pi和qi代入即可求解。
  • +
+

优点:

+
    +
  • BIM模型建立在数学基础上,理论性较强
  • +
+

缺点:

+
    +
  • 需要估计参数
  • +
  • 原始的BIM没有考虑TF、文档长度因素
  • +
  • BIM中同样存在词项独立性假设
  • +
  • BIM实质上是一个idf权重公式,仅考虑了全局信息,缺少局部信息。因此需要和TF权重配合使用
  • +
+

BM25模型

+

二重泊松分布

+

泊松分布是一个经典的随机分布:分布公式参数:均值 λ,分布形式随参数取值变化

+

关于文本中词频分布的一个经典结论:在高质量精英文档集(Elite Set)中:均值较高,接近正态分布;在整个语料中:均值低,接近指数分布

+

优点:

+
    +
  • 一定程度上的理论化模型
  • +
  • 基于二重泊松假设——适用于绝大多数文本语料上的IR检索应用
  • +
  • 实验证明有效
  • +
+

缺点:

+
    +
  • 待调参数多且参数敏感性高
  • +
  • 必须去停用词
  • +
+

BM25被视为现实应用中最好的IR模型之一。即便现在基于BERT预训练语言模型的方法可以获得更好的效果,仍然需要使用BM25进行无监督过滤来保证检索精度。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第6讲 概率检索模型
+
https://zhangzhao219.github.io/2022/09/14/UCAS/information-retrieval/information-retrieval-6/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/15/UCAS/machine-learning/machine-learning-3/index.html b/2022/09/15/UCAS/machine-learning/machine-learning-3/index.html new file mode 100644 index 000000000..cf3ac446e --- /dev/null +++ b/2022/09/15/UCAS/machine-learning/machine-learning-3/index.html @@ -0,0 +1,943 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第3章 线性分类 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第3章 线性分类

+ + +
+ +

《机器学习》课程笔记:第3章 线性分类

+ +

第3章 线性分类

+

基础知识

+

个数组成的有序数组, 称为一个维向量

+

向量空间:所有分量为实数的维向量构成的集合称为一个维向量空间,又称线性空间。

+

超平面表达式:

+

线性判别函数表达式:

+

线性函数刻画了样本到超平面的距离

+

相似性测度:

+
    +
  • Minkovski Metric 闵氏距离(p-范数)
  • +
  • 欧氏距离(p=2)(2-范数)
  • +
  • 城市块(p=1)、曼哈顿距离(1-范数)
  • +
  • Chobychev 距离(p=inf)
  • +
  • 平方距离\马氏距离
  • +
  • 余弦相似性
  • +
+

常用的统计量:

+
    +
  • 类均值向量
  • +
  • 总均值向量
  • +
  • 类内散度矩阵
  • +
  • 总类内离散度矩阵
  • +
  • 类间散度矩阵
  • +
+

分类问题

+
    +
  1. 定义:根据给定的训练集,其中,要求寻找上的决策函数
  2. +
  3. 评估方法 +
      +
    1. 留出法数据集分成两类,交叉验证。
    2. +
    3. 交叉验证法数据集分成类,其中类做测试,类做训练;进行次实验取平均。
    4. +
    5. 自助法次随机取一个样本, 共个样本,放入中;由训练,测试。
    6. +
    +
  4. +
  5. 性能评价 +
      +
    1. 错误率与精度:
    2. +
    3. 查准率、查全率与F1
    4. +
    5. ROC 与AUC
    6. +
    7. 代价敏感错误率与代价曲线
    8. +
    +
  6. +
  7. 比较检验 +
      +
    1. 假设检验
    2. +
    3. 交叉验证检验
    4. +
    5. McNemar检验
    6. +
    7. Friedman检验与Nemenyi检验
    8. +
    +
  8. +
+

线性分类问题

+
    +
  1. 线性分类器描述: +
      +
    1. 线性判别函数:
    2. +
    3. 分类界为超平面:
    4. +
    +
  2. +
  3. 线性分类器的任务:通过已知的训练样本集, 构造线性判别函数
  4. +
  5. 线性可分性
  6. +
+

线性决策的多分类问题:

+

类问题,需要至少预先训练多少个二分类器?

+

需要训练好个分类器(所有可能的分类器),然后采用二叉树比对测试。

+

根据最大相似性决定类别。

+

感知机

+

基本知识:

+
    +
  1. 神经网络形成阶段(1943-1958),开拓性的贡献
  2. +
  3. 线性分类: +
      +
    1. 决策函数:
    2. +
    3. 增广表示:,其中
    4. +
    5. 决策超平面:
    6. +
    7. 分类判别:根据是否大于0进行判断
    8. +
    9. 决策函数几何含义:刻画了样本到超平面的距离
    10. +
    11. 验证函数:
    12. +
    +
  4. +
  5. 优化方法:梯度下降 +
      +
    1. 随机梯度下降:
    2. +
    +
  6. +
+

感知机结构

+

vz4erd.md.png

+

感知机学习准则:目标:最小化错分样本的误差代价。

+

代价函数(错分样本的误差函数):(只统计错分的样本,是错分的样本到超平面的距离之和)

+

的含义:错分样本到分类超平面误差距离的总和

+

感知机优化:Batch Perception和Online Perception

+

误差修正基本规则:

+
    +
  1. 固定增量的感知机修正:若训练样本是线性可分,则感知器训练算法在有限次迭代后可以收敛到正确的解向量
  2. +
  3. 增量自适应调整:当错分样本的正确标签为,修正;当错分样本的正确标签为,修正
  4. +
+

线性鉴别分析

+

基本思想:求线性变换,使得样本集${x_i} {y_i} $后,类别间距大,类内间距小。

+

目标函数:

+

样本投影后的类别间距离: ; 其中, 表示第 类样本投影后的均值

+

样本投影后的类别内距离:投影后的各类样本方差

+

计算:

+

logistic 模型

+

基本思想:假设likelihood ratio的对数为线性判别函数

+

+

两类问题:

+

+

学习目标:

+

标签 类, 越大, 越小,标签 类, 越大, 越小。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第3章 线性分类
+
https://zhangzhao219.github.io/2022/09/15/UCAS/machine-learning/machine-learning-3/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月15日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/15/UCAS/machine-learning/machine-learning-4/index.html b/2022/09/15/UCAS/machine-learning/machine-learning-4/index.html new file mode 100644 index 000000000..1388e14e0 --- /dev/null +++ b/2022/09/15/UCAS/machine-learning/machine-learning-4/index.html @@ -0,0 +1,1071 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第4章 非线性分类 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第4章 非线性分类

+ + +
+ +

《机器学习》课程笔记:第4章 非线性分类

+ +

第4章 非线性分类

+

概述

+

非线性问题:对于线性不可分数据,采用非线性决策的方法

+

线性扩展的思想:线性扩展模型,核函数方法

+

非线性的思想:最近邻、决策树、神经网络、集成学习

+

决策树

+

决策问题一定是一个二判决问题

+

样本根据问题一定可以分成两部分,两部分之间没有交集,两部分的并集包括所有的情况

+

决策树的目标:在树结构上,根据节点的判断,搜索类别。

+

树结构的优点:可以不必测试所有特征和区域。

+

问题数

+
    +
  1. 离散值情况:以特征或特征的可能离散值作为问题:
  2. +
+

设属性的可能离散取值个数为

+
    +
  • 方法1:每个特征可以作为候选问题,例如ID3、C4.5,属性产生的候选问题数为(切分太快,容易过拟合)
  • +
  • 方法2:每个特征的每个离散值作为候选问题,例如CART,属性产生的候选问题数为
  • +
+
    +
  1. 连续值情况:以每个维度的样本特征值作为问题
  2. +
+

属性上出现的样本特征值个数为

+

方法:每个特征上的样本特征值作为候选问题,属性产生的候选问题数为

+

无论特征值是连续还是离散,确定每个属性所产生的候选问题,候选的问题总数为

+

划分(问题)选择

+

非纯度(Impurity Measure)需要满足两条性质:

+
    +
  • IM最大值时,各类别概率相等
  • +
  • IM最小时为0,只有一类(期望的目标)
  • +
+

非纯度的熵度量(C4.5):

+

非纯度的基尼度量(CART):

+

划分目标:选择最大减少类别非纯度的问题作为划分节点。

+

+

基于非纯度变化量的三个指标:

+
    +
  • 信息增益(ID3):越大越好
  • +
  • 增益率(C4.5):越大越好
  • +
  • 基尼指数:越小越好
  • +
+

信息增益(熵度量):是问题导致的决策划分数目

+

倾向于选择划分集合个数多的节点。区间划分的越细,区间内纯度越高,极端情况每个区间只有一个样本,则熵为0。

+

增益率(信息增益与数据集关于问题的熵值之比)

+

+

增益率改善信息增益:对划分集合个数少的属性有所偏好,越小则越小

+

基尼指数(基尼度量):

+

节点类别设置:叶子节点纯度达到预设阈值后,停止划分,并对叶子节点进行类别设置。(按概率最大的类别设定)

+

决策树生成

+

决策树生成过程

+

从顶向下(不断增加一个节点)

+
    +
  • 准则:所有划分中选择一个使(非纯度减少量)最大的划分为节点,加入决策树。
  • +
  • 贪婪学习:根据划分准则,在问题集上进行划分,直到Impurity不能再改善,或达到较小的改善。
  • +
  • 停止规则:设定阈值
  • +
+

ID3 决策树:属性特征作为结点问题,划分选择实际是特征选择过程,最大化信息增益作为划分选择依据

+

C4.5 决策树:属性特征作为结点问题,划分选择实际是特征选择过程,最大化信息增益率作为划分选择依据

+

CART 决策树:属性特征离散值作为结点问题,本质是二叉树,最小化基尼指数作为划分选择依据

+

连续值二叉决策树

+

剪枝处理

+

ID3、C4.5决策树剪枝

+
    +
  • 代价函数
  • +
  • 剪枝算法
  • +
+

泛化性能评估法

+

最近邻方法

+

最近邻法

+

原理:将样本分类为离之最近的样本类别

+

类判别函数:

+

决策规则:

+

最近邻分类隐含的决策边界是非线性的

+

k-近邻法

+

原理:将样本分给个近邻中类别样本个数最多的类

+

个近邻中属于的样本数

+

判别函数:

+

决策规则:

+

误差讨论

+

近邻法的缺点:

+
    +
  • 存储量大:训练样本需要存到内存
  • +
  • 计算量大:每次决策都要计算所有样本的相似性
  • +
+

近邻法的快速算法

+

快速算法一:快速搜索近邻法(不减少的情况下怎么样才能更快)

+

原理:将样本分成不相交的子集,基于子集的搜索

+
    +
  1. 样本分级分层为多个子集
  2. +
  3. 逐层搜出一个最优子集
  4. +
  5. 在最后的子集中局部找最近样本点
  6. +
+

规则1-找最近子集:如果的距离 > 当前最近子集距离,则被忽略。

+

规则2-找最近样本:如果的距离>已存在的最近点,则样本被忽略。

+

k 近邻快速搜索推广:子集搜索过程与最近邻一致,样本搜索时,存有个最近距离值。

+

快速算法二:剪辑近邻法

+

原理:通过剪掉边界样本(错误分类样本),缩减样本规模

+

剪辑规则:两分剪辑近邻法

+
    +
  • 将训练样本集,分成两个子集
  • +
  • 做分类参考,对进行剪辑(错分样本被剪掉)
  • +
  • 剪辑后的作为最终的训练集训练近邻分类器
  • +
+

快速算法三:压缩近邻法

+

原理:去掉中心附近样本,保留错误样本,在剪辑基础上进行压缩

+

基本思想:分类中通常被正确分类的样本,较少支持决策,将常分误的样本保留。

+

压缩规则:

+
    +
  1. 初始化,训练集分为中仅个样本;
  2. +
  3. 作为训练,分类中第个样本;如果错误,将该样本放入
  4. +
  5. 对每一个样本重复2
  6. +
  7. 直到无错分样本,或为空
  8. +
  9. 中样本放弃,是最终压缩样本集
  10. +
+

拒绝决策近邻法

+

原理:对于与各类别相似度较低的样本,不做判断

+

优点:在样本压缩时,给可是可非的样本机会。

+
    +
  • 算法1:可拒绝的k近邻法(分类决策)-k近邻中,各类样本的个数小于 , 拒绝分类
  • +
  • 算法2:可拒绝的编辑近邻法(样本压缩)-与编辑近邻法比较的不同之处:除保留正确分类样本外,还保留了拒绝样本。
  • +
+

集成学习

+

结合策略

+

原理:不同的分类器对样本有不同的鉴别力;综合优势,使错误率最小。

+

问题描述:已知一组训练分类器,分类器的类别后验为,其中为索引类别,为索引分类器.

+

目标是对进行分类,求

+

概率分布相似性的计算:

+
    +
  1. 期望之间的相似度:
  2. +
  3. 在每个维度上的log比值:
  4. +
  5. 内积运算:
  6. +
+

几种集成学习准则

+

Geometric Average Rule

+
    +
  • 目标函数:最小化KL平均
  • +
  • 集成方法:
  • +
  • 决策规则:
  • +
+

Arithmetic Average Rule

+
    +
  • 目标函数:最小化Alternative KL平均
  • +
  • 集成方法:
  • +
  • 决策规则:
  • +
+

Majority Voting Rule

+
    +
  • 原理:对两类问题,多个分类器进行决策投票,票数过半的类别为样本最终标签。
  • +
  • 基分类器要求相互独立且正确率p>50%,且最好具有多样性
  • +
+

Bagging和随机森林

+

Bagging:通过随机采样,训练分类器,保证分类器的差异。从训练集中不断随机抽取样本构造分类器,分类时通过投票进行类别判断。

+

随机森林:多决策树的Bagging;决策树随机属性选择;从训练集中不断随机构造决策树分类器,分类时通过投票进行类别判断。

+

随机森林较一般Bagging效果好

+

Boosting: AdaBoost

+

Boosting原理:一系列弱分类器,在不同子集上学习,得到增强分类器。

+

AdaBoost加权分类器

+

AdaBoost 目标函数

+

非线性SVM

+

SVM 原理

+

两个核心思想

+
    +
  • 最大间隔:找到最大间隔分类超平面;
  • +
  • 核函数方法:样本升维映射到高维空间后,采用线性决策。升维映射由核技巧实现。
  • +
+

数学问题

+

KKT:任何目标函数有解的充要条件

+

一个原始问题总有它的对偶问题

+

对于特殊的凸优化来说,原始问题的对偶问题是,两个函数的极值相等,也就是最优解是相等的

+

如果原始问题和它的对偶问题都满足KKT条件,对于条件好的凸优化,可以构造的关系,从而将不好求解的原始问题转化为好求的对偶问题

+

最大间隔

+

目标:找到最大间隔分类超平面(类别集合到分类超平面的最小距离最大化)

+

函数间隔:给定的训练数据集和超平面

+
    +
  • 超平面关于样本的函数间隔定义为
  • +
  • 超平面关于训练数据集的函数间隔定义为,即所有样本点的函数间隔的最小值。
  • +
  • 存在问题:只要成比例的改变,函数间隔会相应变化。
  • +
+

几何间隔:给定的训练数据集和超平面

+
    +
  • 超平面关于样本的几何间隔定义为
  • +
  • 超平面关于训练数据集的几何间隔定义为,即所有样本点的几何间隔的最小值。
  • +
  • 成比例的改变,几何间隔不会相应变化。
  • +
+

最大几何间隔等价的问题:

+

函数间隔的取值并不影响最优化问题的解。

+

支撑向量(SV):支撑最小距离最大化的样本

+

支撑超平面:通过支持向量,平行于分类面的超平面

+

间隔:支撑向量到分类面的距离

+

支持向量机学习的基本想法是求解能够正确划分训练数据集并且几何间隔最大的分离超平面。

+

对偶问题

+

+

问题的求解

+

根据KKT条件成立求解

+

核函数方法

+

避免直接求非线性映射,由核函数替代内积运算

+

SVM 算法

+

硬间隔SVM

+

软间隔SVM

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第4章 非线性分类
+
https://zhangzhao219.github.io/2022/09/15/UCAS/machine-learning/machine-learning-4/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月15日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/16/Leetcode/Leetcode-101/Leetcode-101-14/index.html b/2022/09/16/Leetcode/Leetcode-101/Leetcode-101-14/index.html new file mode 100644 index 000000000..93a8e1021 --- /dev/null +++ b/2022/09/16/Leetcode/Leetcode-101/Leetcode-101-14/index.html @@ -0,0 +1,1556 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第14章 树 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第14章 树

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第14章 树

+ +

+

树的递归

+

Leetcode 104

+

给定一个二叉树,找出其最大深度。

+
class Solution {
+public:
+    static int DFS(TreeNode* &root,int sum){
+        if(root == nullptr){
+            return sum;
+        }
+        return max(DFS(root->left,sum+1),DFS(root->right,sum+1));
+    }
+    int maxDepth(TreeNode* root) {
+        return DFS(root,0);
+    }
+};
+

分析:递归计算最大高度即可

+

错误:开始递归写的有问题,变成引用传参了,后面改对后调通。

+

Leetcode 110

+

给定一个二叉树,判断它是否是高度平衡的二叉树。

+
class Solution {
+public:
+    static int DFS(TreeNode* &root){
+        if(root == nullptr){
+            return 0;
+        }
+        int left = DFS(root->left);
+        int right = DFS(root->right);
+        if(left == -1 || right == -1 || abs(left - right) > 1){
+            return -1;
+        }
+        return max(left,right)+1;
+    }
+    bool isBalanced(TreeNode* root) {
+        return DFS(root) != -1;
+    }
+};
+

分析:解法类似于求树的最大深度,但有两个不同的地方:一是我们需要先处理子树的深度再进行比较,二是如果我们在处理子树时发现其已经不平衡了,则可以返回一个-1,使得所有其长辈节点可以避免多余的判断

+

错误:思路不对,看了解析

+

Leetcode 543

+

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

+
class Solution {
+public:
+    static int DFS(TreeNode* &root,int &maxsum){
+        if(root == nullptr){
+            return 0;
+        }
+        int left = DFS(root->left,maxsum);
+        int right = DFS(root->right,maxsum);
+        maxsum = max(maxsum,left+right+1);
+        return max(left,right)+1;
+    }
+    int diameterOfBinaryTree(TreeNode* root) {
+        int maxsum = 0;
+        int a = DFS(root,maxsum);
+        return maxsum-1;
+    }
+};
+

分析:还是递归,要留两个变量进行记录

+

错误:没看解析调通,但是自己想的挺艰难的。

+

Leetcode 437

+

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

+
class Solution {
+public:
+    static long long DFS(TreeNode* &root, long long sum){
+        if(root == nullptr){
+            return 0;
+        }
+        long long count;
+        if(root->val == sum){
+            count = 1;
+        }
+        else{
+            count = 0;
+        }
+        return count + DFS(root->left,sum-root->val) + DFS(root->right,sum-root->val);
+    }
+    int pathSum(TreeNode* root, int targetSum) {
+        if(root == nullptr){
+            return 0;
+        }
+        return DFS(root,targetSum)+pathSum(root->left,targetSum)+pathSum(root->right,targetSum);
+    }
+};
+

分析:递归每个节点时,需要分情况考虑:(1)如果选取该节点加入路径,则之后必须继续加入连续节点,或停止加入节点(2)如果不选取该节点加入路径,则对其左右节点进行重新进行考虑。因此一个方便的方法是我们创建一个辅函数,专门用来计算连续加入节点的路径。

+

错误:两层的递归有点做不了

+

Leetcode 101

+

给你一个二叉树的根节点 root , 检查它是否轴对称。

+
class Solution {
+public:
+    static bool DFS(TreeNode* &left,TreeNode* &right){
+        if(left == nullptr && right != nullptr){
+            return false;
+        }
+        if(left != nullptr && right == nullptr){
+            return false;
+        }
+        if(left == nullptr && right == nullptr){
+            return true;
+        }
+        if(left->val != right->val){
+            return false;
+        }
+        return DFS(left->left,right->right) && DFS(left->right,right->left);
+    }
+    bool isSymmetric(TreeNode* root) {
+        if(root == nullptr){
+            return true;
+        }
+        return DFS(root->left,root->right);
+    }
+};
+

分析:判断一个树是否对称等价于判断左右子树是否对称。笔者一般习惯将判断两个子树是否相等或对称类型的题的解法叫做“四步法”:(1)如果两个子树都为空指针,则它们相等或对称(2)如果两个子树只有一个为空指针,则它们不相等或不对称(3)如果两个子树根节点的值不相等,则它们不相等或不对称(4)根据相等或对称要求,进行递归处理。

+

错误:不明白

+

Leetcode 1110

+

给出二叉树的根节点 root,树上每个节点都有一个不同的值。如果节点值在 to_delete 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。返回森林中的每棵树。你可以按任意顺序组织答案。

+
class Solution {
+public:
+    void DFS(TreeNode* &root, vector<int>& to_delete,vector<TreeNode*>& result){
+        if(root == nullptr){
+            return;
+        }
+        DFS(root->left,to_delete,result);
+        DFS(root->right,to_delete,result);
+        auto it = find(to_delete.begin(),to_delete.end(),root->val);
+        if(it != to_delete.end()){
+            if(root->left != nullptr){
+                result.push_back(root->left);
+            }
+            if(root->right != nullptr){
+                result.push_back(root->right);
+            }
+            root->left = nullptr;
+            root->right = nullptr;
+            root = nullptr;
+        }
+        return;
+    }
+    vector<TreeNode*> delNodes(TreeNode* root, vector<int>& to_delete) {
+        vector<TreeNode*> result;
+        DFS(root,to_delete,result);
+        if(root != nullptr){
+            result.push_back(root);
+        }
+        return result;
+    }
+};
+

分析:遍历,然后置为空指针就好

+

错误:开始的判断条件不太够,后来自己调通。

+

层次遍历

+

Leetcode 637

+

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10<sup>-5</sup> 以内的答案可以被接受。

+
class Solution {
+public:
+    vector<double> averageOfLevels(TreeNode* root) {
+        vector<double> result;
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int num = 0;
+            double sum = 0.0;
+            int nowsize = q.size();
+            while(nowsize--){
+                TreeNode* t = q.front();
+                q.pop();
+                num += 1;
+                sum += t->val;
+                if(t->left != nullptr){
+                    q.push(t->left);
+                }
+                if(t->right != nullptr){
+                    q.push(t->right);
+                }
+            }
+            result.push_back(sum/num);
+        }
+        return result;
+    }
+};
+

分析:层序遍历即可

+

一遍AC

+

前中后序遍历

+

Leetcode 105

+

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的 先序遍历inorder 是同一棵树的 中序遍历 ,请构造二叉树并返回其根节点。

+
class Solution {
+private:
+    unordered_map<int, int> index;
+public:
+    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
+        if (preorder_left > preorder_right) {
+            return nullptr;
+        }
+  
+        // 前序遍历中的第一个节点就是根节点
+        int preorder_root = preorder_left;
+        // 在中序遍历中定位根节点
+        int inorder_root = index[preorder[preorder_root]];
+  
+        // 先把根节点建立出来
+        TreeNode* root = new TreeNode(preorder[preorder_root]);
+        // 得到左子树中的节点数目
+        int size_left_subtree = inorder_root - inorder_left;
+        // 递归地构造左子树,并连接到根节点
+        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
+        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
+        // 递归地构造右子树,并连接到根节点
+        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
+        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
+        return root;
+    }
+    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
+        int n = preorder.size();
+        // 构造哈希映射,帮助我们快速定位根节点
+        for (int i = 0; i < n; ++i) {
+            index[inorder[i]] = i;
+        }
+        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
+    }
+};
+

分析:很老的题,好好判断,数据结构设计对即可

+

错误:太久远了忘记怎么判断了

+

Leetcode 144

+

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

+
class Solution {
+public:
+    static void dfs(TreeNode* &root,vector<int> &result){
+        if(root == nullptr){
+            return;
+        }
+        result.push_back(root->val);
+        dfs(root->left,result);
+        dfs(root->right,result);
+    }
+    vector<int> preorderTraversal(TreeNode* root) {
+        vector<int> result;
+        dfs(root,result);
+        return result;
+    }
+};
+

分析:递归遍历即可

+

一遍AC

+

二叉查找树

+

Leetcode 99

+

给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。 请在不改变其结构的情况下,恢复这棵树。

+
class Solution {
+public:
+    void inorder(TreeNode* root, TreeNode*& mistake1, TreeNode*& mistake2, TreeNode*& prev) {
+        if (!root) {
+            return;
+        }
+        if (root->left) {
+            inorder(root->left, mistake1, mistake2, prev);
+        }
+        if (prev && root->val < prev->val) {
+            if (!mistake1) {
+                mistake1 = prev;
+                mistake2 = root;
+            }
+            else {
+                mistake2 = root;
+            }
+            cout << mistake1->val;
+            cout << mistake2->val;
+        }
+        prev = root;
+        if (root->right) {
+            inorder(root->right, mistake1, mistake2, prev);
+        }
+    }
+    void recoverTree(TreeNode* root) {
+        TreeNode *mistake1 = nullptr, *mistake2 = nullptr, *prev = nullptr;
+        inorder(root, mistake1, mistake2, prev);
+        if (mistake1 && mistake2) {
+            int temp = mistake1->val;
+            mistake1->val = mistake2->val;
+            mistake2->val = temp;
+        }
+    }
+};
+

分析:我们可以使用中序遍历这个二叉查找树,同时设置一个prev 指针,记录当前节点中序遍历时的前节点。如果当前节点大于prev 节点的值,说明需要调整次序。有一个技巧是如果遍历整个序列过程中只出现了一次次序错误,说明就是这两个相邻节点需要被交换;如果出现了两次次序错误,那就需要交换这两个节点。

+

错误:没有思路

+

Leetcode 669

+

给你二叉搜索树的根节点 root ,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在 [low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案

+
class Solution {
+public:
+    TreeNode* trimBST(TreeNode* root, int low, int high) {
+        if(root == nullptr){
+            return root;
+        }
+        if(root->val > high){
+            return trimBST(root->left,low,high);
+        }
+        if(root->val < low){
+            return trimBST(root->right,low,high);
+        }
+        root->left = trimBST(root->left, low, high);
+        root->right = trimBST(root->right, low, high);
+        return root;
+    }
+};
+

分析:利用二叉查找树的大小关系递归进行树的处理。

+

错误:看了解析

+

字典树

+

Leetcode 208

+

尝试建立一个字典树,支持快速插入单词、查找单词、查找单词前缀的功能。

+
class Trie {
+private:
+    vector<Trie*> children;
+    bool isEnd;
+
+    Trie* searchPrefix(string prefix) {
+        Trie* node = this;
+        for (char ch : prefix) {
+            ch -= 'a';
+            if (node->children[ch] == nullptr) {
+                return nullptr;
+            }
+            node = node->children[ch];
+        }
+        return node;
+    }
+public:
+    Trie() : children(26), isEnd(false) {}
+    void insert(string word) {
+        Trie* node = this;
+        for (char ch : word) {
+            ch -= 'a';
+            if (node->children[ch] == nullptr) {
+                node->children[ch] = new Trie();
+            }
+            node = node->children[ch];
+        }
+        node->isEnd = true;
+    }
+
+    bool search(string word) {
+        Trie* node = this->searchPrefix(word);
+        return node != nullptr && node->isEnd;
+    }
+
+    bool startsWith(string prefix) {
+        return this->searchPrefix(prefix) != nullptr;
+    }
+};
+

分析:字典树的典型实现方法

+

错误:没做过,尝试理解

+

练习

+

Leetcode 226

+

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

+
class Solution {
+public:
+    TreeNode* invertTree(TreeNode* root) {
+        if(root == nullptr){
+            return nullptr;
+        }
+        TreeNode* left = invertTree(root->left);
+        TreeNode* right = invertTree(root->right);
+        root->left = right;
+        root->right = left;
+        return root;
+    }
+};
+

分析:递归反转即可

+

错误:翻转值是不对的,需要反转结点

+

Leetcode 617

+

给你两棵二叉树: root1root2 。想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。返回合并后的二叉树。

+
class Solution {
+public:
+    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
+        if (t1 == nullptr) {
+            return t2;
+        }
+        if (t2 == nullptr) {
+            return t1;
+        }
+        auto merged = new TreeNode(t1->val + t2->val);
+        merged->left = mergeTrees(t1->left, t2->left);
+        merged->right = mergeTrees(t1->right, t2->right);
+        return merged;
+    }
+};
+

分析:递归处理即可

+

错误:自己尝试的方法有问题,不太明白错在哪

+

Leetcode 572

+

给你两棵二叉树 rootsubRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

+
class Solution {
+public:
+    static bool check(TreeNode* root, TreeNode* subRoot){
+        if(root == nullptr && subRoot == nullptr){
+            return true;
+        }
+        if(root == nullptr && subRoot != nullptr){
+            return false;
+        }
+        if(root != nullptr && subRoot == nullptr){
+            return false;
+        }
+        if(root->val != subRoot->val){
+            return false;
+        }
+        return check(root->left,subRoot->left) && check(root->right,subRoot->right);
+    }
+    static bool DFS(TreeNode* root, TreeNode* subRoot){
+        if(root == nullptr){
+            return false;
+        }
+        return check(root,subRoot) || DFS(root->left,subRoot) || DFS(root->right,subRoot);
+    }
+    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
+        bool judge = DFS(root,subRoot);
+        return judge;
+    }
+};
+

分析:递归判断即可

+

错误:自己写了前半部分,看了一眼后写了后半部分

+

Leetcode 404

+

给定二叉树的根节点 root ,返回所有左叶子之和。

+
class Solution {
+public:
+    bool isLeafNode(TreeNode* node) {
+        return !node->left && !node->right;
+    }
+
+    int dfs(TreeNode* node) {
+        int ans = 0;
+        if (node->left) {
+            ans += isLeafNode(node->left) ? node->left->val : dfs(node->left);
+        }
+        if (node->right && !isLeafNode(node->right)) {
+            ans += dfs(node->right);
+        }
+        return ans;
+    }
+    int sumOfLeftLeaves(TreeNode* root) {
+        return dfs(root);
+    }
+};
+

分析:递归判断结点

+

错误:没有思路

+

Leetcode 513

+

给定一个二叉树的 根节点 root,请找出该二叉树的最底层最左边节点的值。

+
class Solution {
+public:
+    int findBottomLeftValue(TreeNode* root) {
+        queue<TreeNode*> q;
+        q.push(root);
+        int result = root->val;
+        while(!q.empty()){
+            int tempsize = q.size();
+            int sign = 0;
+            while(tempsize--){
+                TreeNode* t = q.front();
+                q.pop();
+                if(sign == 0){
+                    result = t->val;
+                    sign = 1;
+                }
+                if(t->left != nullptr){
+                    q.push(t->left);
+                }
+                if(t->right != nullptr){
+                    q.push(t->right);
+                }
+            }
+        }
+        return result;
+    }
+};
+

分析:广度优先遍历即可

+

一遍AC

+

Leetcode 538

+

给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

+
class Solution {
+public:
+    void DFS(TreeNode* root,int &sum){
+        if(root == nullptr){
+            return;
+        }
+        DFS(root->right,sum);
+        root->val = root->val + sum;
+        sum = root->val;
+        DFS(root->left,sum);
+        return;
+    }
+    TreeNode* convertBST(TreeNode* root) {
+        int sum = 0;
+        DFS(root,sum);
+        return root;
+    }
+};
+

分析:反向的中序遍历

+

错误:开始顺序弄反,后面修正了

+

Leetcode 235

+

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

+
class Solution {
+public:
+    vector<TreeNode*> getPath(TreeNode* root, TreeNode* target) {
+        vector<TreeNode*> path;
+        TreeNode* node = root;
+        while (node != target) {
+            path.push_back(node);
+            if (target->val < node->val) {
+                node = node->left;
+            }
+            else {
+                node = node->right;
+            }
+        }
+        path.push_back(node);
+        return path;
+    }
+    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
+        vector<TreeNode*> path_p = getPath(root, p);
+        vector<TreeNode*> path_q = getPath(root, q);
+        TreeNode* ancestor;
+        for (int i = 0; i < path_p.size() && i < path_q.size(); ++i) {
+            if (path_p[i] == path_q[i]) {
+                ancestor = path_p[i];
+            }
+            else {
+                break;
+            }
+        }
+        return ancestor;
+    }
+};
+

分析:从根节点开始遍历;如果当前节点就是p,那么成功地找到了节点;如果当前节点的值大于p的值,说明p应该在当前节点的左子树,因此将当前节点移动到它的左子节点;如果当前节点的值小于p的值,说明p应该在当前节点的右子树,因此将当前节点移动到它的右子节点。对于节点q同理。在寻找节点的过程中,我们可以顺便记录经过的节点,这样就得到了从根节点到被寻找节点的路径。

+

错误:没有思路

+

Leetcode 530

+

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值

+
class Solution {
+public:
+    void DFS(TreeNode* &root,vector<int>& result){
+        if(root == nullptr){
+            return;
+        }
+        DFS(root->left,result);
+        result.push_back(root->val);
+        DFS(root->right,result);
+    }
+    int getMinimumDifference(TreeNode* root) {
+        vector<int> result;
+        DFS(root,result);
+        int minval = 100000;
+        for(int i=0;i<result.size()-1;++i){
+            if(result[i+1]-result[i] < minval){
+                minval = result[i+1]-result[i];
+            }
+        }
+        return minval;
+    }
+};
+

分析:中序遍历存在数组内部,然后遍历判断即可

+

一遍AC

+

Leetcode 889

+

给定两个整数数组,preorderpostorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。

+
class Solution {
+    int preIdx = 0, postIdx = 0;
+public:
+    TreeNode* constructFromPrePost(vector<int>& preorder, vector<int>& postorder) {
+        TreeNode *node = new TreeNode(preorder[preIdx++]);
+        if(node->val != postorder[postIdx]){
+            node->left = constructFromPrePost(preorder, postorder);
+        }
+
+        if(node->val != postorder[postIdx]){
+            node->right = constructFromPrePost(preorder, postorder);
+        }
+        postIdx++;
+        return node;
+    }
+};
+

分析:利用前序遍历来构建Tree,然后通过后续遍历来检验当前树是否构建完毕

+

错误:思路不对

+

Leetcode 106

+

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

+
class Solution {
+public:
+    TreeNode* DFS(vector<int>& inorder, int inleft,int inright,vector<int>& postorder,int postleft,int postright){
+        if(inleft > inright){
+            return nullptr;
+        }
+        TreeNode* root = new TreeNode(postorder[postright]);
+        int k;
+        for(k=inleft;k<=inright;++k){
+            if(inorder[k] == postorder[postright]){
+                break;
+            }
+        }
+        int rightsize = inright - k;
+        root->left = DFS(inorder,inleft,k-1,postorder,postleft,postright-rightsize-1);
+        root->right = DFS(inorder,k+1,inright,postorder,postright-rightsize,postright-1);
+        return root;
+    }
+    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
+        int n = inorder.size();
+        TreeNode* root = DFS(inorder,0,n-1,postorder,0,n-1);
+        return root;
+    }
+};
+

分析:与前面的题目相同

+

一遍AC

+

Leetcode 94

+

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

+
class Solution {
+public:
+    static void dfs(TreeNode* &root,vector<int> &result){
+        if(root == nullptr){
+            return;
+        }
+        dfs(root->left,result);
+        result.push_back(root->val);
+        dfs(root->right,result);
+    }
+    vector<int> inorderTraversal(TreeNode* root) {
+        vector<int> result;
+        dfs(root,result);
+        return result;
+    }
+};
+

分析:普通递归

+

一遍AC

+

Leetcode 145

+

给你一棵二叉树的根节点 root ,返回其节点值的后序遍历。

+
class Solution {
+public:
+    static void dfs(TreeNode* &root,vector<int> &result){
+        if(root == nullptr){
+            return;
+        }
+        dfs(root->left,result);
+        dfs(root->right,result);
+        result.push_back(root->val);
+    }
+    vector<int> postorderTraversal(TreeNode* root) {
+        vector<int> result;
+        dfs(root,result);
+        return result;
+    }
+};
+

分析:普通递归

+

一遍AC

+

Leetcode 236

+

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

+
class Solution {
+public:
+    TreeNode* ans;
+    bool dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
+        if (root == nullptr) return false;
+        bool lson = dfs(root->left, p, q);
+        bool rson = dfs(root->right, p, q);
+        if ((lson && rson) || ((root->val == p->val || root->val == q->val) && (lson || rson))) {
+            ans = root;
+        } 
+        return lson || rson || (root->val == p->val || root->val == q->val);
+    }
+    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
+        dfs(root, p, q);
+        return ans;
+    }
+};
+

分析:不太明白

+

错误:不太明白

+

Leetcode 109

+

给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。

+
class Solution {
+public:
+    ListNode* getMedian(ListNode* left, ListNode* right) {
+        ListNode* fast = left;
+        ListNode* slow = left;
+        while (fast != right && fast->next != right) {
+            fast = fast->next;
+            fast = fast->next;
+            slow = slow->next;
+        }
+        return slow;
+    }
+
+    TreeNode* buildTree(ListNode* left, ListNode* right) {
+        if (left == right) {
+            return nullptr;
+        }
+        ListNode* mid = getMedian(left, right);
+        TreeNode* root = new TreeNode(mid->val);
+        root->left = buildTree(left, mid);
+        root->right = buildTree(mid->next, right);
+        return root;
+    }
+    TreeNode* sortedListToBST(ListNode* head) {
+        return buildTree(head, nullptr);
+    }
+};
+

分析:每一次找中位数,然后递归构造两边就可以了

+

错误:以为要调整平衡,没有思路

+

Leetcode 897

+

给你一棵二叉搜索树的 root ,请你 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。

+
class Solution {
+public:
+    void inorder(TreeNode *node, vector<int> &res) {
+        if (node == nullptr) {
+            return;
+        }
+        inorder(node->left, res);
+        res.push_back(node->val);
+        inorder(node->right, res);
+    }
+    TreeNode* increasingBST(TreeNode* root) {
+        vector<int> res;
+        inorder(root, res);
+
+        TreeNode *dummyNode = new TreeNode(-1);
+        TreeNode *currNode = dummyNode;
+        for (int value : res) {
+            currNode->right = new TreeNode(value);
+            currNode = currNode->right;
+        }
+        return dummyNode->right;
+    }
+};
+

分析:遍历建树就可以,注意不要在函数中建树,原因没明白

+

错误:在函数中建树不行

+

Leetcode 653

+

给定一个二叉搜索树 root 和一个目标结果 k,如果二叉搜索树中存在两个元素且它们的和等于给定的目标结果,则返回 true

+
class Solution {
+public:
+    void DFS(TreeNode* root,vector<int> &result){
+        if(root == nullptr){
+            return;
+        }
+        DFS(root->left,result);
+        result.push_back(root->val);
+        DFS(root->right,result);
+        return;
+    }
+    bool findTarget(TreeNode* root, int k) {
+        vector<int> result;
+        DFS(root,result);
+        int left = 0;
+        int right = result.size()-1;
+        while(left < right){
+            if(result[left] + result[right] == k){
+                return true;
+            }
+            else if(result[left] + result[right] < k){
+                ++left;
+            }
+            else{
+                --right;
+            }
+        }
+        return false;
+    }   
+};
+

分析:读出来二分就可以了

+

一遍AC

+

Leetcode 450

+

给定一个二叉搜索树的根节点root和一个值key,删除二叉搜索树中的key对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

+
class Solution {
+public:
+    TreeNode* deleteNode(TreeNode* root, int key) {
+        if (root == nullptr) {
+            return nullptr;
+        }
+        if (root->val > key) {
+            root->left = deleteNode(root->left, key);
+            return root;
+        }
+        if (root->val < key) {
+            root->right = deleteNode(root->right, key);
+            return root;
+        }
+        if (root->val == key) {
+            if (!root->left && !root->right) {
+                return nullptr;
+            }
+            if (!root->right) {
+                return root->left;
+            }
+            if (!root->left) {
+                return root->right;
+            }
+            TreeNode *successor = root->right;
+            while (successor->left) {
+                successor = successor->left;
+            }
+            root->right = deleteNode(root->right, successor->val);
+            successor->right = root->right;
+            successor->left = root->left;
+            return successor;
+        }
+        return root;
+    }
+};
+

分析:解析

+

错误:不明白应该怎么调整

+

总结

+

看起来树的题目并没有特别复杂的。主要的难度在于递归的思路,想明白后就简单了。另外就是各种边界条件的判断,也要多想多练。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第14章 树
+
https://zhangzhao219.github.io/2022/09/16/Leetcode/Leetcode-101/Leetcode-101-14/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月16日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/17/Leetcode/Leetcode-101/Leetcode-101-15/index.html b/2022/09/17/Leetcode/Leetcode-101/Leetcode-101-15/index.html new file mode 100644 index 000000000..95be04026 --- /dev/null +++ b/2022/09/17/Leetcode/Leetcode-101/Leetcode-101-15/index.html @@ -0,0 +1,928 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第15章 图 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第15章 图

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第15章 图

+ +

+

二分图

+

二分图算法也称为染色法,是一种广度优先搜索。如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么图为二分。

+

Leetcode 785

+

判断一个图是不是二分图

+
class Solution {
+private:
+    static constexpr int UNCOLORED = 0;
+    static constexpr int RED = 1;
+    static constexpr int GREEN = 2;
+    vector<int> color;
+public:
+    bool isBipartite(vector<vector<int>>& graph) {
+        int n = graph.size();
+        vector<int> color(n, UNCOLORED);
+        for (int i = 0; i < n; ++i) {
+            if (color[i] == UNCOLORED) {
+                queue<int> q;
+                q.push(i);
+                color[i] = RED;
+                while (!q.empty()) {
+                    int node = q.front();
+                    int cNei = (color[node] == RED ? GREEN : RED);
+                    q.pop();
+                    for (int neighbor: graph[node]) {
+                        if (color[neighbor] == UNCOLORED) {
+                            q.push(neighbor);
+                            color[neighbor] = cNei;
+                        }
+                        else if (color[neighbor] != cNei) {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+        return true;
+    }
+};
+

分析:广度优先遍历,需要判断

+

错误:想简单了

+

拓扑排序

+

拓扑排序(topological sort)是一种常见的,对有向无环图排序的算法。给定有向无环图中的N个节点,我们把它们排序成一个线性序列;若原图中节点i指向节点j,则排序结果中i一定在j之前。拓扑排序的结果不是唯一的,只要满足以上条件即可。

+

Leetcode 210

+

给定N个课程和这些课程的前置必修课,求可以一次性上完所有课的顺序。

+
class Solution {
+public:
+    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
+        vector<int> result;
+        vector<int> indegree(numCourses,0);
+        vector<vector<int>> graph(numCourses,vector<int>(numCourses,0));
+        int m = prerequisites.size();
+        for(int i=0;i<m;++i){
+            graph[prerequisites[i][1]][prerequisites[i][0]] = 1;
+            ++indegree[prerequisites[i][0]];
+        }
+        while(1){
+            if(result.size() == numCourses){
+                return result;
+            }
+            int sign = 0;
+            for(int i=0;i<numCourses;++i){
+                if(indegree[i] == 0){
+                    indegree[i] = -1;
+                    result.push_back(i);
+                    sign = 1;
+                    for(int j=0;j<numCourses;++j){
+                        if(graph[i][j] == 1){
+                            graph[i][j] = 0;
+                            --indegree[j];
+                        }
+                    }
+                }
+            }
+            if(sign == 0){
+                break;
+            }
+        }
+        result.clear();
+        return result;
+    }
+};
+

分析:经典拓扑排序

+

错误:有一点小错误,基本一遍AC

+

练习

+

Leetcode 1059

+

付费题目

+

Leetcode 1135

+

付费题目

+

Leetcode 882

+

经典的节点最短距离问题

+
class Solution {
+public:
+    int reachableNodes(vector<vector<int>>& edges, int maxMoves, int n) {
+        // 先构建图
+        vector<vector<pair<int, int>>> graph(n);
+        for (vector<int>& edge : edges)
+        {
+            int s = edge[0];
+            int e = edge[1];
+            int cnt = edge[2];
+            graph[s].emplace_back(e, cnt);
+            graph[e].emplace_back(s, cnt);
+        }
+
+        // 保持一个从起点到当前点的距离
+        unordered_map<int, int> distances;
+        distances[0] = 0;
+        for (int i = 1; i < n; ++i)
+        {
+            distances[i] = maxMoves + 1;
+        }
+
+        // 点到点的“额外扩展距离”,最大是cnt
+        // 二维变一维 int<<32 + int
+        unordered_map<long, int> extras;
+
+        // 结果记录
+        int res = 0;
+
+        // 从起点到改点的距离的小顶堆
+        priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int, int>>> q;
+        q.push({0, 0});
+
+        while (!q.empty())
+        {
+            int dist = q.top().first;
+            int curr = q.top().second;
+            q.pop();
+          
+            // 忽略更大的距离
+            if (dist > distances[curr])
+            {
+                continue;
+            }
+
+            distances[curr] = dist;
+            ++res;
+
+            for (auto& pair : graph[curr])
+            {
+                int next = pair.first;
+                int cnt = pair.second;
+                // 这里取最小的距离, 取(cnt和 maxMoves-dist)的最小值
+                extras[((long)curr << 32) + next] = min(cnt, maxMoves - dist);
+
+                // 计算基于当前点到下一个结点的距离,额外走一步,如果找到更小,则插入队列里
+                int dist2 = dist + cnt + 1;
+                if (dist2 < distances[next])
+                {
+                    q.emplace(dist2, next);
+                    distances[next] = dist2;
+                }
+            }
+        }
+
+        // 最后加上“额外扩展距离”
+        for (vector<int>& edge : edges)
+        {
+            int s = edge[0];
+            int e = edge[1];
+            res += min(edge[2], extras[((long)s<< 32) +e] + extras[((long)e<<32) +s]);
+        }
+
+        return res;
+    }
+};
+

总结

+

各种高级用法,还比较简单,但是应该不是很常见

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第15章 图
+
https://zhangzhao219.github.io/2022/09/17/Leetcode/Leetcode-101/Leetcode-101-15/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月17日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/17/UCAS/information-retrieval/information-retrieval-7/index.html b/2022/09/17/UCAS/information-retrieval/information-retrieval-7/index.html new file mode 100644 index 000000000..a1092c3d7 --- /dev/null +++ b/2022/09/17/UCAS/information-retrieval/information-retrieval-7/index.html @@ -0,0 +1,853 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第7讲 基于语言建模的IR模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第7讲 基于语言建模的IR模型

+ + +
+ +

《现代信息检索》课程笔记:第7讲 基于语言建模的IR模型

+ +

第7讲 基于语言建模的IR模型

+

语言模型

+

统计语言模型(Statistical Language Modeling,SLM)

+

SLM广泛使用于语音识别和统计机器翻译领域,利用概率统计理论研究语言。

+

规则方法:词、句、篇章的生成比如满足某些规则,不满足该规则就不应存在。

+

统计方法:任何语言片断都有存在的可能,只是可能性大小不同

+

对于n-gram,n越大,则模型越复杂,估计的参数(即估计的概率)也越多。当然,当数据量足够大的情况下,模型阶数越高越对片段概率的计算也越准确。

+

理论上说,在数据充足的情况下,利用更多的历史高阶的模型更准确,但是总计算量也越大

+

数据规模总是有限的,即用于训练模型参数的语料存在稀疏性 (Data Sparseness ,即某参数在训练语料中没有出现问题。

+

数据稀疏性导致零概率问题,但是训练集上不出现的事件并不代表在新的语料上不出现。

+

SLM的一个重要工作就是进行平滑重新分配概率,即使没出现的事件也会赋予一个概率。

+

基于统计建模的IR模型

+
    +
  • 查询似然模型:把相关度看成是每篇文档对应的语言下生成该查询的可能性
  • +
  • 翻译模型:假设查询经过某个噪声信道变形成某篇文章,则由文档还原成该查询的概率翻译模型可以视为相关度
  • +
  • KL距离模型 :查询对应某种语言,每篇文档对应某种语言,查询语言和文档语言的KL距离作为相关度度量
  • +
+

总体分布&抽样

+

文档的模型风格实际上是某种总体分布

+

(待评分)文档和查询都是该总体分布下的一个抽样样本实例

+

根据文档,估计文档的模型,即求出该总体分布(一般假设某种总体分布,然后求出其参数),然后计算该总体分布下抽样出查询的概率

+

查询似然模型(Query Likelihood Model)

+

文本生成的多项式模型

+

数据平滑的一般形式

+

其它SLMIR 模型

+
    +
  • 查询似然类:文档建模、计算查询的似然、基本QLM 模型、翻译模型等
  • +
  • 文档似然类:查询建模、计算文档的似然、BIM模型、相关性模型等
  • +
  • 模型比较类:文档建模、查询建模,计算两个模型的距离,KL距离模型
  • +
+

基于翻译模型的IR模型:

+

基本的QLM模型不能解决词语失配(word mismatch)问题,即查询中的用词和文档中的用词不一致

+

翻译概率P(qi|wj)在计算时可以将词项之间的关系融入。

+
    +
  • 基于词典来计算(人工或者自动构造的同义词/近义词/翻译词典)
  • +
  • 基于语料库来计算(标题、摘要vs. 文本;文档锚文本vs. 文档)
  • +
+

KL距离(相对熵)模型

+

统计语言建模IR模型优缺点

+

优点:

+
    +
  • 理论上具有解释性,有扩展空间
  • +
  • 有些模型虽然计算上仍然依赖于term 独立性假设,
  • +
  • 但是模型本身并不依赖于 term 独立性假设。
  • +
+

缺点:数据稀疏性,需要参数估计

+

SLMIR模型讨论

+
    +
  • SLMIR中有一些东西和VSM一样
  • +
  • 词项频率直接在模型中使用 +
      +
    • 但是在SLMIR 中没有进行放缩变化
    • +
    +
  • +
  • 本质上概率表示已经进行了长度归一化 +
      +
    • VSM中的余弦归一化也做了类似工作
    • +
    +
  • +
  • 文档中的词项频率和文档集频率混合以后和idf的效果相当 +
      +
    • 那些文档集中比较罕见,但是某些文档中比较普遍的词项将对排序起更重要的影响。
    • +
    +
  • +
+

SLMIR vs. VSM :

+

共性:

+
    +
  • 模型中都直接使用了词项频率
  • +
  • 本质上概率表示已经进行了长度归一化
  • +
  • 文档中词项频率和文档集频率混合以后和idf的效果相当
  • +
+

不同:

+
    +
  • SLMIR:基于概率论
  • +
  • VSM:基于相似度,一个线性代数中的概念
  • +
  • 文档集频率、文档概率、词项频率、归一化等计算细节
  • +
+

基于统计建模的IR模型 : 假设

+
    +
  • 简化假设:查询和文档是同一类对象,与实际并不相符 +
      +
    • 已经出现了一些不采用上述假设的SLMIR模型
    • +
    • VSM也基于同一假设
    • +
    +
  • +
  • 简化假设:词项之间是独立的 +
      +
    • VSM 中也采用了词项独立性假设
    • +
    +
  • +
  • 比向量空间中的假设表述更清晰
  • +
  • SLMIR比VSM 具有更好的理论基础,但是纯语言模型的效果会大大低于经过精心调参的向量模型的效果。
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第7讲 基于语言建模的IR模型
+
https://zhangzhao219.github.io/2022/09/17/UCAS/information-retrieval/information-retrieval-7/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月17日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/18/Leetcode/Leetcode-101/Leetcode-101-16/index.html b/2022/09/18/Leetcode/Leetcode-101/Leetcode-101-16/index.html new file mode 100644 index 000000000..65384ec63 --- /dev/null +++ b/2022/09/18/Leetcode/Leetcode-101/Leetcode-101-16/index.html @@ -0,0 +1,977 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode 刷题笔记-Leetcode 101 第16章 复杂数据结构 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode 刷题笔记-Leetcode 101 第16章 复杂数据结构

+ + +
+ +

Leetcode 刷题笔记-Leetcode 101 第16章 复杂数据结构

+ +

复杂数据结构

+

并查集

+

并查集(union-find, 或disjoint set)可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。假设存在n个节点,我们先将所有节点的父亲标为自己;每次要连接节点i和j时,我们可以将i的父亲标为j;每次要查询两个节点是否相连时,我们可以查找i和j的祖先是否最终为同一个人。

+

Leetcode 684

+

在无向图找出一条边,移除它之后该图能够成为一棵树(即无向无环图)。如果有多个解,返回在原数组中位置最靠后的那条边。

+
class Solution {
+public:
+    int Find(vector<int>& parent, int index) {
+        if (parent[index] != index) {
+            parent[index] = Find(parent, parent[index]);
+        }
+        return parent[index];
+    }
+
+    void Union(vector<int>& parent, int index1, int index2) {
+        parent[Find(parent, index1)] = Find(parent, index2);
+    }
+
+    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
+        int n = edges.size();
+        vector<int> parent(n + 1);
+        for (int i = 1; i <= n; ++i) {
+            parent[i] = i;
+        }
+        for (auto& edge: edges) {
+            int node1 = edge[0], node2 = edge[1];
+            if (Find(parent, node1) != Find(parent, node2)) {
+                Union(parent, node1, node2);
+            } else {
+                return edge;
+            }
+        }
+        return vector<int>{};
+    }
+};
+

分析:在一棵树中,边的数量比节点的数量少1。如果一棵树有n个节点,则这棵树有n−1条边。这道题中的图在树的基础上多了一条附加的边,因此边的数量也是n。树是一个连通且无环的无向图,在树中多了一条附加的边之后就会出现环,因此附加的边即为导致环出现的边。可以通过并查集寻找附加的边。初始时,每个节点都属于不同的连通分量。遍历每一条边,判断这条边连接的两个顶点是否属于相同的连通分量。如果两个顶点属于不同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间不连通,因此当前的边不会导致环出现,合并这两个顶点的连通分量。如果两个顶点属于相同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,将当前的边作为答案返回。

+

错误:不知道怎么使用并查集

+

复合数据结构

+

Leetcode 146

+

请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。

+
class LRUCache {
+public:
+    //定义双链表
+    struct Node{
+        int key,value;
+        Node* left ,*right;
+        Node(int _key,int _value): key(_key),value(_value),left(NULL),right(NULL){}
+    }*L,*R;//双链表的最左和最右节点,不存贮值。
+    int n;
+    unordered_map<int,Node*>hash;
+
+    void remove(Node* p)
+    {
+        p->right->left = p->left;
+        p->left->right = p->right;
+    }
+    void insert(Node *p)
+    {
+        p->right = L->right;
+        p->left = L;
+        L->right->left = p;
+        L->right = p;
+    }
+    LRUCache(int capacity) {
+        n = capacity;
+        L = new Node(-1,-1),R = new Node(-1,-1);
+        L->right = R;
+        R->left = L;  
+    }
+  
+    int get(int key) {
+        if(hash.count(key) == 0) return -1; //不存在关键字 key 
+        auto p = hash[key];
+        remove(p);
+        insert(p);//将当前节点放在双链表的第一位
+        return p->value;
+    }
+  
+    void put(int key, int value) {
+        if(hash.count(key)) //如果key存在,则修改对应的value
+        {
+            auto p = hash[key];
+            p->value = value;
+            remove(p);
+            insert(p);
+        }
+        else 
+        {
+            if(hash.size() == n) //如果缓存已满,则删除双链表最右侧的节点
+            {
+                auto  p = R->left;
+                remove(p);
+                hash.erase(p->key); //更新哈希表
+                delete p; //释放内存
+            }
+            //否则,插入(key, value)
+            auto p = new Node(key,value);
+            hash[key] = p;
+            insert(p);
+        }
+    }
+};
+

分析:采用一个链表 list<pair<int, int>>来储存信息的 keyvalue,链表的链接顺序即为最近使用的新旧顺序,最新的信息在链表头节点。同时我们需要一个嵌套着链表的迭代器的 unordered_map<int, list<pair<int, int>>::iterator>进行快速搜索,存迭代器的原因是方便调用链表的 splice函数来直接更新查找成功(cash hit)时的信息,即把迭代器对应的节点移动为链表的头节点。

+

错误:不明白

+

练习

+

Leetcode 1135

+

付费题目

+

Leetcode 380

+

设计一个插入、删除和随机取值均为时间复杂度的数据结构

+
class RandomizedSet {
+private:
+    vector<int> nums;
+    unordered_map<int, int> indices;
+public:
+    RandomizedSet() {
+        srand((unsigned)time(NULL));
+    }
+  
+    bool insert(int val) {
+        if (indices.count(val)) {
+            return false;
+        }
+        int index = nums.size();
+        nums.emplace_back(val);
+        indices[val] = index;
+        return true;
+    }
+  
+    bool remove(int val) {
+        if (!indices.count(val)) {
+            return false;
+        }
+        int index = indices[val];
+        int last = nums.back();
+        nums[index] = last;
+        indices[last] = index;
+        nums.pop_back();
+        indices.erase(val);
+        return true;
+    }   
+  
+    int getRandom() {
+        return nums[rand()%nums.size()];
+    }
+};
+

分析:变长数组 + 哈希表可以实现

+

错误:随机数不太会,剩下的自己实现了

+

Leetcode 432

+

设计一个increaseKey,decreaseKey,getMaxKey,getMinKey 均为时间复杂度的数据结构。

+
class AllOne {
+    list<pair<unordered_set<string>, int>> lst;
+    unordered_map<string, list<pair<unordered_set<string>, int>>::iterator> nodes;
+
+public:
+    AllOne() {}
+
+    void inc(string key) {
+        if (nodes.count(key)) {
+            auto cur = nodes[key], nxt = next(cur);
+            if (nxt == lst.end() || nxt->second > cur->second + 1) {
+                unordered_set<string> s({key});
+                nodes[key] = lst.emplace(nxt, s, cur->second + 1);
+            } else {
+                nxt->first.emplace(key);
+                nodes[key] = nxt;
+            }
+            cur->first.erase(key);
+            if (cur->first.empty()) {
+                lst.erase(cur);
+            }
+        } else { // key 不在链表中
+            if (lst.empty() || lst.begin()->second > 1) {
+                unordered_set<string> s({key});
+                lst.emplace_front(s, 1);
+            } else {
+                lst.begin()->first.emplace(key);
+            }
+            nodes[key] = lst.begin();
+        }
+    }
+
+    void dec(string key) {
+        auto cur = nodes[key];
+        if (cur->second == 1) { // key 仅出现一次,将其移出 nodes
+            nodes.erase(key);
+        } else {
+            auto pre = prev(cur);
+            if (cur == lst.begin() || pre->second < cur->second - 1) {
+                unordered_set<string> s({key});
+                nodes[key] = lst.emplace(cur, s, cur->second - 1);
+            } else {
+                pre->first.emplace(key);
+                nodes[key] = pre;
+            }
+        }
+        cur->first.erase(key);
+        if (cur->first.empty()) {
+            lst.erase(cur);
+        }
+    }
+
+    string getMaxKey() {
+        return lst.empty() ? "" : *lst.rbegin()->first.begin();
+    }
+
+    string getMinKey() {
+        return lst.empty() ? "" : *lst.begin()->first.begin();
+    }
+};
+

分析:双向链表+哈希表

+

错误:好难

+

Leetcode 716

+

付费题目

+

总结

+

基本上都是要自己写数据结构的题目,应该也不是很常见了。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode 刷题笔记-Leetcode 101 第16章 复杂数据结构
+
https://zhangzhao219.github.io/2022/09/18/Leetcode/Leetcode-101/Leetcode-101-16/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月18日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/18/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-4/index.html b/2022/09/18/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-4/index.html new file mode 100644 index 000000000..1d044bc33 --- /dev/null +++ b/2022/09/18/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-4/index.html @@ -0,0 +1,997 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:模式识别与机器学习-第4章 特征选择和提取 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:模式识别与机器学习-第4章 特征选择和提取

+ + +
+ +

《模式识别与机器学习》课程笔记:第4章 特征选择和提取

+ +

第4章 特征选择和提取

+

特征选择和提取是模式识别中的一个关键问题,前面讨论分类器设计的时候,一直假定已给出了特征向量维数确定的样本集,其中各样本的每一维都是该样本的一个特征;这些特征的选择是很重要的,它强烈地影响到分类器的设计及其性能;假若对不同的类别,这些特征的差别很大,则比较容易设计出具有较好性能的分类器。

+

例如,描述人可以用好多特征,如肤色,体重,身高等,但是如果要判断软件工程师,显然编程这个特征比较有判别性;如果要判断是不是篮球员,则体重、身高有很强的判别性。

+

特征选择和提取是构造模式识别系统时的一个重要课题。在很多实际问题中,往往不容易找到那些最重要的特征,或受客观条件的限制,不能对它们进行有效的测量;因此在测量时,由于人们心理上的作用,只要条件许可总希望把特征取得多一些;另外,由于客观上的需要,为了突出某些有用信息,抑制无用信息,有意加上一些比值、指数或对数等组合计算特征;如果将数目很多的测量值不做分析,全部直接用作分类特征,不但耗时,而且会影响到分类的效果,产生“特征维数灾难”问题。

+

为了设计出效果好的分类器,通常需要对原始的测量值集合进行分析,经过选择或变换处理,组成有效的识别特征;在保证一定分类精度的前提下,减少特征维数,即进行“降维”处理,使分类器实现快速、准确和高效的分类。为达到上述目的,关键是所提供的识别特征应具有很好的可分性,使分类器容易判别。为此,需对特征进行选择:

+
    +
  • 应去掉模棱两可、不易判别的特征;
  • +
  • 所提供的特征不要重复,即去掉那些相关性强且没有增加更多分类信息的特征。
  • +
+

特征选择和提取这一任务应在设计分类器之前进行;

+

xpsWjI.png

+

所谓特征选择,就是从个度量值集合中,按某一准则选取出供分类用的子集,作为降维(维,)的分类特征;

+

所谓特征提取,就是使通过某种变换,产生个特征 ,作为新的分类特征(或称为二次特征);

+

其目的都是为了在尽可能保留识别信息的前提下,降低特征空间的维数,以达到有效的分类效果。

+

模式类别可分性的测度

+

距离和散布矩阵:

+
    +
  • 点到点之间的距离:,其中, 维向量, 其第 个分量分别是
  • +
  • 点到点集之间的距离:点到点集之间的距离为
  • +
+

类内距离:维空间中同一类内各模式样本点集,其内部各点的均方距离为,其中

+

类内散布矩阵:考虑一类内模式点集,其类内散布矩阵为:,其中

+

对属于同一类的模式样本,类内散布矩阵表示各样本点围绕其均值周围的散布情况。

+

在考虑有两个以上的类别,如集合时,类间距离对类别的可分性起着重要作用,此时应计算

+

为简化起见,常用两类样本各自质心间的距离作为类间距离,并假设两类样本出现的概率相等,则

+

其中为两类模式样本集各自的均值向量,的第个分量,为维数。

+

两类模式的类间散布矩阵:

+

对三个以上的类别,类间散布矩阵常写成,其中,为多类模式(如共有类)分布的总体均值向量,即

+

多类情况的类内散布矩阵可写成各类的类内散布矩阵的先验概率的加权和,即,其中是第类的协方差矩阵。

+

有时,用多类模式总体分布的散布矩阵来反映其可分性,即:,其中为多类模式分布的总体均值向量。

+

,即总体散布矩阵是各类类内散布矩阵与类间散布矩阵之和。

+

特征选择

+

设有个可用作分类的测量值,为了在不降低(或尽量不降低)分类精度的前提下,减小特征空间的维数以减少计算量,需从中直接选出个作为分类的特征。

+

个测量值中选出个特征,一共有种可能的选法,需寻找一种简便的可分性准则,间接判断每一种子集的优劣。

+

对于独立特征的选择准则:类别可分性准则应具有这样的特点,即不同类别模式特征的均值向量之间的距离应最大,而属于同一类的模式特征,其方差之和应最小。假设各原始特征测量值是统计独立的,此时,只需对训练样本的个测量值独立地进行分析,从中选出个最好的作为分类特征即可。

+

对于 两类训练样本,假设其均值向量为 维方向的分量为 ,方差为 ,定义可分性准则函数,则为正值。 值越大,表示测度值的第个分量对分离 类越有效。将按大小排队, 选出最大的个对应测度值作为分类特征,即达到特征选择的目的。

+

上述基于距离测度的可分性准则,其适用范围与模式特征的分布有关。假若类概率密度函数不是或不近似正态分布,均值和方差就不足以用来估计类别的可分性,此时该准则函数不完全适用。

+

一般特征的散布矩阵准则:

+
    +
  • 类内:
  • +
  • 类间:
  • +
+

直观上,类间离散度越大且类内离散度越小,则可分性越好。因此,可推导出散布矩阵准则采用如下形式:

+
    +
  • 行列式形式:
  • +
  • 迹形式:
  • +
+

其中, 是矩阵 的特征值。使 最大的子集可作为选择的分类特征。

+

离散K-L变换(Karhunen-Loeve变换(卡洛南-洛伊变换))

+

前面讨论的特征选择是在一定准则下,从个特征中选出个来反映原有模式。这种简单删掉某个特征的做法并不十分理想,因为一般来说,原来的个数据各自在不同程度上反映了识别对象的某些特征,简单地删去某些特征可能会丢失较多的有用信息。如果将原来的特征做正交变换,获得的每个数据都是原来个数据的线性组合,然后从新的数据中选出少数几个,使其尽可能多地反映各类模式之间的差异,而这些特征间又尽可能相互独立,则比单纯的选择方法更灵活、更有效。

+

K-L变换就是一种适用于任意概率密度函数的正交变换。

+

离散的有限K-L展开

+

离散的有限K-L展开式的形式:

+

设一连续的随机实函数,则 可用已知的正交函数集 的线性组合来展开,即:。式中,为展开式的随机系数,为一连续的正交函数,它应满足:,其中的共轭复数式。

+

将上式写成离散的正交函数形式,使连续随机函数和连续正交函数在区间内被等间隔采样为个离散点,即:

+

+

写成向量形式:

+

将展开式写成离散形式:,其中为展开式中随机系数的向量形式维矩阵,其中,每一列为正交函数集中的一个函数,小括号内的序号为正交函数的采样点次序。因此,实质上是由向量组成的正交变换矩阵,
+它将变换成

+

对各个模式类别,正交函数都是相同的,但其展开系数向量则因类别的不同模式分布而异。

+

K-L展开式的根本性质是将随机向量展开为另一组正交向量的线性和,且其展开式系数(即系数向量的各个分量)具有不同的性质。

+

正交向量集的确定:

+

设随机向量的总体自相关矩阵为,则,要求系数向量的各个不同分量应统计独立,则应使,其中为对角形矩阵,其互相关成分均为0

+

因为是实对称矩阵,其不同特征值对应的特征向量应正交,即:

+

K-L展开式系数的计算步骤:

+
    +
  1. 求随机向量的自相关矩阵:
  2. +
  3. 求出矩阵的特征值和对应的特征向量,得矩阵:
  4. +
  5. 计算展开式系数:
  6. +
+

按K-L展开式选择特征

+

K-L展开式用于特征选择相当于一种线性变换。若从个特征向量中取出个组成变换矩阵,即,此时是一个维矩阵,维向量,经过变换,即得到降维为的新向量。

+

结论

+

从K-L展开式的性质和按最小均方差的准则来选择特征,应使。由于,故应使。基于这一条件,在将整体模式进行K-L变换之前,应先将其均值作为新坐标轴的原点,采用协方差矩阵或自相关矩阵来计算特征值。如果,则只能得到“次最佳”的结果。

+

将K-L展开式系数(亦即变换后的特征)用表示,写成向量形式:,此时变换矩阵个特征向量组成。为使误差最小,不采用的特征向量,其对应的特征值应尽可能小。因此,将特征值按大小次序标号,即。若首先采用前面的个特征向量,便可使变换误差最小。此时的变换矩阵为

+

K-L变换是在均方误差最小的意义下获得数据压缩(降维)的最佳变换,且不受模式分布的限制。对于一种类别的模式特征提取,它不存在特征分类问题,只是实现用低维的个特征来表示原来高维的个特征,使其误差最小,亦即使其整个模式分布结构尽可能保持不变。

+

通过K-L变换能获得互不相关的新特征。若采用较大特征值对应的特征向量组成变换矩阵,则能对应地保留原模式中方差最大的特征成分,所以K-L变换起到了减小相关性、突出差异性的效果。在此情况下,K-L变换也称为主成分变换(PCA变换)。

+

需要指出的是,采用K-L变换作为模式分类的特征提取时,要特别注意保留不同类别的模式分类鉴别信息,仅单纯考虑尽可能代表原来模式的主成分,有时并不一定有利于分类的鉴别。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:模式识别与机器学习-第4章 特征选择和提取
+
https://zhangzhao219.github.io/2022/09/18/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-4/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月18日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/19/UCAS/machine-learning/machine-learning-competition-basic/index.html b/2022/09/19/UCAS/machine-learning/machine-learning-competition-basic/index.html new file mode 100644 index 000000000..42e1cc9e0 --- /dev/null +++ b/2022/09/19/UCAS/machine-learning/machine-learning-competition-basic/index.html @@ -0,0 +1,1605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 机器学习算法竞赛实战-基础篇 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

机器学习算法竞赛实战-基础篇

+ + +
+ +

机器学习算法竞赛实战-基础篇

+ +

开始学习

+

一直想学,前面看过觉得太难,这回一定要坚持看完!

+

第1章 初见竞赛

+

参考资料:《机器学习算法竞赛实战》学习笔记1.竞赛简介

+

竞赛流程

+

x9y9YD.md.png

+

问题建模

+

分析数据进而抽象出建模目标和方案。自行利用主办方提供的数据构造训练集与测试集。

+

数据探索

+

EDA(探索性数据分析),Exploratory Data Analysis。在大致了解问题建模方式后,需结合对赛题背景业务的理解去看数据长什么样子、数据是否和描述相符、包含哪些信息等。首先需要对数据有清晰认知,主要是宽表中各个字段的取值含义、范围和数据结构等。然后更深层次地结合标签分析特征的分布状态、训练集与测试集的同分布情况、特征之间的业务关联以及隐含信息表征等。

+

特征工程

+

Feature Engineering。特征决定机器学习预测效果上限,算法不断逼近这个上限。最费时的模块。

+

模型训练

+

选模型、调参数

+

模型融合

+

找找队友,看看Code

+

第2章 问题建模

+

参考资料:《机器学习算法竞赛实战》学习笔记2.问题建模

+

赛题理解

+

从直观上梳理问题,分析问题可解的方法、赛题背景等

+

业务理解:从个人生活的直观角度对业务进行分析

+

数据理解:在问题建模阶段,只需对数据做基本的分析。可以将数据理解分为数据基础层和数据描述层两个部分。主办方提供的原始数据质量良莠不齐,往往需要对原始数据进行清洗、加工和计算等处理。

+
    +
  • 数据基础层:重点关注每个数据字段的来源、生产过程、取数逻辑、计算逻辑等,了解这些才能正确理解、选取并使用每一个原始字段,从而加工计算得出所需的更多衍生字段,数据最终通常以数据表格形式呈现。
  • +
  • 数据描述层:主要是在处理好的数据基础层上进行统计分析和概括描述,该层重点在于尽可能地通过一些简单统计量(如均值、最值、分布、增幅、趋势等)来概括整体数据的状况。具体使用哪些统计量依据数据呈现的具体情况而定。例如,对于时间序列问题,可以统计其增幅、趋势和周期;对于常规的数值特征,则可以观察期均值、最值和方差等统计量;对于存在多类别的样本集合,则可以使用分布、分位点等进行描述。
  • +
+

评价指标

+

分类指标:

+
    +
  1. 错误率(error rate)与精度(accuracy)
  2. +
+

错误率:分类错误的样本数占样本总数的比例

+

精度:分类正确的样本数占样本总数的比例

+

精度=1-错误率

+
    +
  1. 查准率/准确率(precision)、查全率/召回率(recall)
  2. +
+

查准率P反映真实情况与预测结果都为正例的样例在预测结果为正例的样例中的占比

+

查全率R反映真实情况与预测结果都为正例的样例在真实情况为正例的样例中的占比

+

(查准率与查全率是一对矛盾的度量,一般来讲,查准率高时,查全率偏低;查全率高时,查准率偏低)

+

,在查准率与查全率之间取得一个平衡

+
# 构建一个计算准确率、召回率和F1-score的评价代码
+y_train = np.array([1,0,1,0,1,0,1,0,1,1])
+y_pred = np.array([1,1,1,1,0,0,0,0,1,0])
+precision = precision_score(y_train,y_pred) #准确率
+recall = recall_score(y_train,y_pred) #召回率
+f1 = f1_score(y_train,y_pred) #f1度量
+print(precision,recall,f1)
+
0.6 0.5 0.5454545454545454
+
    +
  1. ROC与AUC
  2. +
+

先根据学习器的预测结果对样例进行排序,按此顺序逐个把样例作为正例进行预测,每次计算出“真正例率”(True Positive Rate,简称TPR)和“假正例率”(False Positive Rate,简称FPR),分别以他们为纵、横轴作图,就得到了ROC曲线。

+

,真正例率TPR反映真正例在实际情况为正例的样例中的占比

+

,假正例率FPR反映假正例在实际情况为反例的样例中的占比

+

ROC曲线对正负样本的数量和分布不敏感。

+

AUC定义为ROC下方的面积,在互联网的搜索、推荐和广告的排序业务中都极为常见。AUC作为一个数值,其值越大就代表分类器的效果越好。

+

值得一提的还有AUC的排序特性。相对于准确率、召回率等指标,AUC指标本身和模型预测的概率绝对值无关,它只关注样本间的排序效果,因此特别适合用作排序相关问题建模的评价指标。AUC是一个概率值,我们随机挑选一个正样本与一个负样本,由当前分类算法根据计算出的分数将这个正样本排在负样本前面的概率就是AUC值。

+

为什么AUC与模型预测的分数值无关是个很好的特性?假设采用的是准确率等指标,而模型预测的分数是个概率值,那么必须选择一个阈值来决定把哪些样本预测为1,哪些预测为0。阈值的选择不同,准确率的值就会不同。而AUC可以直接使用模型预测分数本身,参考的是相对顺序。在竞赛中,省去了参赛者试探阈值的麻烦。

+
    +
  1. 对数损失
  2. +
+

对数损失可用于评价分类器的概率输出。对数损失通过惩罚错误的分类来实现对分类器的准确度的量化。最小化对数损失基本等价于最大化分类器的准确度。为了计算对数损失,分类器必须提供概率结果,即把输入样本喂入模型后,预测得到每个类别的概率值(0~1),而不只是预测最可能的类别。

+

AUC与对数损失的区别

+

对数损失主要评价模型预测的概率是否足够准确,更关注和观察数据的吻合程度;AUC评价的则是模型把正样本排列到前面的能力。两者侧重不同,故应用不同。对于广告CTR问题,如果考虑广告排序效果,则可以选择AUC,这样不会受极端值影响。此外,对数损失反映了评价偏差,更偏向于将样本数量多的那类划分准确。由于使用AUC或对数损失可以避免把预测概率转换成类别的麻烦,在各种数据竞赛的分类问题中,AUC和对数损失基本是最常见的模型评价指标。

+

回归指标:

+
    +
  1. 平均绝对误差,又称L1范数损失
  2. +
+

MAE不是二阶连续可微的,其二阶导数总为0。

+
    +
  1. 均方误差,又称L2范数损失
  2. +
+

MSE的量纲与数据标签不一致,为了保证量纲的一致性,通常需要对均方误差进行开方(均方根误差RMSE)

+

平均绝对误差MAE与均方误差MSE的区别

+

均方误差对误差(真实值-预测值)取了平方,若误差>1,则均方误差会进一步增大误差。如果数据中存在异常点,那误差值就会很大,而误差的平方则会远大于误差的绝对值。因此,相对于使用平均绝对误差计算损失,使用均方误差的模型会赋予异常点更大的权重。简而言之,均方误差对异常值更加敏感

+

为什么在XGBoost里通常选择Huber损失替换MAE?

+

由于MAE不是连续可导的(0处不可导),所以需要使用可导目标函数来逼近平均绝对误差。而对于均方误差MSE,梯度又会随着损失的减小而减小,使预测结果更加精确。在这种情况下,Huber损失就非常有用,它会由于梯度的减小而落在最小值附近。比起均方误差MSE,Huber损失对异常点更加健壮。因此,Huber损失结合了MAE和MSE的优点。但是Huber损失可能需要我们不断调整超参数delta。

+
    +
  1. 平均绝对百分比误差
  2. +
+

MAPE与MAE一样,不存在二阶导数。但不用于MAE,平均绝对百分比误差MAPE除了考虑预测值与真实值的误差,还考虑了误差与真实值之间的比例。因此真实值越大,误差会越小。

+

样本选择

+

主办方提供的数据往往令人脑壳疼,主要是以下四个原因:

+
    +
  • 数据集过大严重影响了模型性能:过大的数据集会严重影响各种特征工程和建模方式的快速验证 +
      +
    • 对数据进行采样处理,然后在小数据集上建模分析
    • +
    • 特定业务场景下,可以过滤一些对建模没有意义的数据
    • +
    +
  • +
  • 噪声和异常数据导致准确率不够 +
      +
    • 采集数据时操作不当导致信息表征出现错误
    • +
    • 数据本身的特性存在合理范围内的抖动导致噪声与异常-看是否能够解码出正确数据
    • +
    • 数据噪声的存在具有两面性,噪声的存在会导致数据的质量变低,影响模型效果;另一方面,可以通过在训练集中引入噪声数据的方法使模型健壮性更强。
    • +
    • 当处理噪声数据时,首先考虑是否为采集错误导致的,再去权衡模型的泛化性和当前效果。有时去噪会导致模型泛化性能变差。要去噪,首先要识别出噪声,然后采取直接过滤或者修改噪声数据等多种办法,噪声数据可能是特征值不对,比如特征值缺失、超出特征值域范围等;也可能是标注不对,比如二分类问题的正样本标注成了负样本。
    • +
    +
  • +
  • 样本数据冗余或不相关数据没有给模型带来收益 +
      +
    • 数据中存在的冗余不仅会影响模型性能,更会引入噪声与异常。数据冗余的一个典型解决方案就是进行特征选择。
    • +
    +
  • +
  • 正负样本分布不均衡导致数据存在倾斜-进行数据采样
  • +
+

问题1:在数据量非常大的情况下,为了降低成本,如何提高模型的训练速度?

+
    +
  • 方法1:简单随机抽样,分为有放回与无放回
  • +
  • 方法2:分层采样-按照规定的比例从不同类别中随机抽取样本
  • +
+

问题2:针对正负样本分布不均衡的问题,如何通过数据采样解决这类问题?

+
    +
  • 方法1:评分加权处理 +
      +
    • 分布不均衡的问题包括欺诈交易识别和垃圾邮件识别等,其正负样本的数据分布差距极大。考虑正负样本的重要性,在模型训练以及评价的时候可以设计相应的得分权重,使得模型能够学习到需要获得关注的部分。
    • +
    • 此方法的具体操作步骤是:首先遍历所有样本,根据样本是否满足某个要求来给予其权重。
    • +
    • 加权的直观含义从业务上理解就是认为一个正样本的价值大于多个负样本的,因此希望模型在训练的时候能够更多地从正样本身上学到关键信息,当它学得不好的时候,就要对它加大惩罚力度。
    • +
    +
  • +
  • 方法2:欠采样 +
      +
    • 从数量较多的一类样本中随机选取一部分并剔除,使得最终样本的目标类别不太失衡。常用方法有随机欠采样和Tomek Links,其中Tomek Links先是找出两个各相指标都非常接近的相反类样本,然后删除这类样本中标签(label)占比高的,这类算法能够为分类器提供一个非常好的决策边界。
    • +
    +
  • +
  • 方法3:过采样 +
      +
    • 主要是对样本较少的类别进行重新组合,构造新样本。常用的方法有随机过采样和SMOTE算法。SMOTE算法并不是简单地复制已有的数据,而是在原有数据的基础上通过算法产生新生数据。
    • +
    +
  • +
+

思考:在什么场景下需要处理样本的不均衡问题?

+
    +
  • 如果竞赛任务对于召回有特别大的需求,即对每个正样本的预测都远远比负样本的预测更重要,那么这时候假如不做任何处理,对结果影响较大
  • +
  • 如果竞赛的评价指标是AUC,那么在实战中会发现这时处理样本不均衡问题与否对于结果的差别不太大。(但细微提升也是好的)
  • +
  • 如果在竞赛任务中正负样本同等重要,即预测正确一个正样本与预测准确一个负样本是同等重要的,那么不做处理问题也不大
  • +
+

线下评估

+

由于需要数据集对模型的效果进行线下验证,所以需要考虑如何对数据进行划分,构建合适的线下验证集。针对不同类型的问题,需要不同的线下验证方式。

+

书中将这些问题大致分为强时序性与弱时序性两类,然后以此确定线下验证方式。

+
    +
  • 强时序性问题:对于含有明显时间序列因素的赛题,可看作强时序性问题,即线上数据的时间都在离线数据集之后。因此要将最接近测试集的数据作为验证集对模型的效果进行评估(采用时间上最接近测试集的数据做验证集,且验证集的时间分布在训练集之后)
  • +
  • 弱时序性问题:这类问题的验证方式主要为K折交叉验证(K-fold Cross Validation)
  • +
+

定义:先将总数据集D划分为k个大小相似的互斥子集,每个子集都尽可能保持数据分布的一致性(即从D中分层采样得到)。然后每次用K-1个子集的并集作为训练集,余下的自己作为测试集。这样可以获得K组训练/测试集,从而可进行k次训练和测试,最终返回这k个测试结果的均值。

+

注意:

+
    +
  • 交叉验证法评估结果的稳定性和保真性很依赖K的取值,K通常取10,常用有5,20等
  • +
  • 给定k值,仍有多种划分方式。故通常要随机使用不同的划分重复p次,最终的评估结果是这p次k折交叉验证结果的均值,常见有10次10折交叉验证
  • +
+

以下为交叉验证代码,其中参数NFOLDS用来控制折数**(未实际验证)**

+
from sklearn.model_selection import KFold
+NFOLDS = 5 #五折交叉验证
+folds = KFold (n_split = NFOLDS,shuffle=True,random_state=2021)#random_state只要是一个固定的值就可以了,不一定是2021
+for trn_idx,val_idx in folds.split(X_train,y_train):
+	train_df,train_label = X_train.iloc[trn_idx, :],y_train[trn_idx]
+	valid_df,valid_label = X_train.iloc[val_idx, :],y_train[val_idx]
+

参数random_state默认设置为None,这意为着每次进行KFold(…, shuffle=True)时,打散都是不同的。

+

为了保证结果的可重复性(在相同的平台上),应该给random_state设定一个固定的值。

+

第3章 数据探索

+

参考资料:《机器学习算法竞赛实战》学习笔记3.数据探索

+

如何确保自己准备好竞赛使用的算法模型?如何为数据集选择最合适的算法?如何定义可用于算法模型的特征变量?数据探索可以帮助回答以上三点。

+

一般而言,数据探索可以分为三个部分:

+
    +
  1. 首先是赛前数据探索,帮助我们对数据有个整体性的认识,并发现数据中存在的问题,比如缺失值、异常值和数据冗余等
  2. +
  3. 其次是竞赛中的数据探索,通过分析数据发现变量的特点,帮助提取有价值的特征,这里可以从单变量、多变量和变量分布进行分析
  4. +
  5. 最后是模型的分析,可以分为重要性分析和结果误差分析,帮助我们从结果发现问题,并进一步优化
  6. +
+

数据初探

+

赛前数据探索,主要包含分析思路、分析方法和明确目的。

+
    +
  1. 分析思路
  2. +
+

在实际竞赛中,最好使用多种探索思路和方法来探索每个变量并比较结果。在完全理解数据集后,就可以进入数据预处理阶段和特征提取阶段了,以便根据所期望的业务结果转换数据集。此步骤的目标是确信数据集已准备好应用于机器学习算法。

+
    +
  1. 分析方法
  2. +
+

数据探索的分析主要采用以下方法:

+
    +
  • 单变量可视化分析:提供原始数据集中每个字段的摘要统计信息
  • +
  • 多变量可视化分析:用来了解不同变量之间的交互关系
  • +
  • 降维分析:有助于发现数据中特征变量之间方差最大的字段,并可以在保留最大信息量的同时减少数据维度。
  • +
+

可以检查每个变量的分布,定义一些丢失值,最终找到替换它们的可能方法。

+
    +
  1. 明确目的
  2. +
+

在竞赛中跳过数据探索阶段可能会导致数据倾斜、出现异常值和过多的缺失值,产生以下糟糕结果:

+
    +
  • 生成不准确的模型
  • +
  • 在错误的数据上生成精确的模型
  • +
  • 为模型选择错误的变量
  • +
  • 资源的低效利用,包括模型的重建
  • +
+

数据探索阶段必须要明确:

+
    +
  1. 数据集基本情况:比如数据有多大,每个字段各是什么类型
  2. +
  3. 重复值、缺失值和异常值:去除重复值,缺失值是否严重,缺失值是否有特殊含义,如何发现异常值
  4. +
  5. 特征之间是否冗余:可以通过特征间相似性特征来找出冗余特征
  6. +
  7. 是否存在时间信息:当存在时间信息时,通常要进行相关性、趋势性、周期性和异常点的分析,同时有可能涉及潜在的数据穿越问题
  8. +
  9. 标签分布:对于分类问题,是否存在类别分布不均衡。对于回归问题,是否存在异常值,整体分布如何,是否需要进行目标转换
  10. +
  11. 训练集与测试集的分布:是否有很多在测试集中存在的特征字段在训练集中没有
  12. +
  13. 单变量/多变量分布:熟悉特征的分布情况,以及特征和标签的关系
  14. +
+

数据探索最基本的步骤之一是获取对数据的基本描述,通过获取对数据的基本描述从而获得对数据的基本感觉。以下方法有助于我们认识数据:

+
    +
  • DataFrame.describe():查看数据的基本分布,具体是对每列数据进行统计,统计值包含频次、均值、方差、最小值、分位数、最大值等。
  • +
  • DataFrame.head(n):可以直接加载数据集的前n行,n默认为5
  • +
  • DataFrame.shape:得到数据集的行列情况
  • +
  • DataFrame.info():可以快速获得对数据集的简单描述,比如每个变量的类型、数据集的大小和缺失值情况。
  • +
+

下面通过一段代码展示nunique和缺失值的情况:

+
stats = []
+for col in train.columns:
+    stats.append((col, train[col].nunique(), train[col].isnull().sum() * 100 / train.shape[0], train[col].value_counts(normalize=True, dropna=False).values[0] * 100, train[col].dtype))
+stats_df = pd.DataFrame(stats, columns=['Feature', 'Unique_values', 'Percentage of missing values', 'Percentage of values in the biggest category', 'type'])
+stats_df.sort_values('Percentage of missing values', ascending=False)[:10]
+

xCG6L4.md.png

+

上图展示了经过上述代码生成的数据基本信息,我们从中找到特殊变量进行细致分析,这里选择nunique值低和缺失值多的变量进行观察。一般而言,nunique为1是不具备任何意义的,表示所有值都一样,不存在区分性,需要进行删除。可以发现有些变量的缺失值很多,比如缺失比例达到95%以上,我们可以考虑将其删除。

+

用柱状图的形式可以更加直观地展示变量的缺失值分布情况,以下为变量缺失值可视化图的具体生成代码:

+
missing = train.isnull().sum()
+missing = missing[missing > 0]
+missing.sort_values(inplace=True)
+missing.plot.bar()
+

变量分析

+

单变量分析

+

单变量可以分为标签、连续型和类别型

+
    +
  1. 标签
  2. +
+

标签是最重要的变量,首先应当观察标签的分布情况。对于房屋价格预测,其标签SalePrice为连续型变量。

+

通过可视化的方式观察SalePrice的分布情况

+
sns.distplot(train['SalePrice'], color='b', bins=100, hist_kws={'alpha': 0.4})
+

可见,SalePrice呈偏离正态分布,属于向右倾斜类型,存在峰值状态,一些异常值在500000以上。我们最终会想办法去掉这些异常值,得出能够让算法模型很好学习的、符合正态分布的变量。

+

xCJw0H.png

+

下面对SalePrice进行对数转换,并生成可视化图

+
sns.distplot(np.log(train['SalePrice']), color='b', bins=100, hist_kws={'alpha': 0.4})
+

xCJsht.png

+

可以看出 ,对数转换后的标签的分布为正态分布形式,比较适合算法模型学习。

+
    +
  1. 连续型
  2. +
+

类似于标签的查看方式,这里主要使用直方图这种可视化方式观察值的分布、每个值出现的频率等。以下为连续型变量的分布可视化的生成代码:

+
df_num = train.select_dtypes(include = ['float64', 'int64'])
+df_num.hist(figsize=(16, 20), bins=50, xlabelsize=8, ylabelsize=8)
+

xCYZEd.md.png

+

实际中要对全部的变量进行查看,分析每一个变量的分布情况。

+

接着进行更加科学的分析,首先是相关性分析。相关性分析只能比较数值间特征,所以对于字母或字符串特征,需要先进行编码,并将其转换为数值,然后再看有什么关联。在实际竞赛中,相关性分析可以很好地过滤掉与标签没有直接关系的特征。

+

正相关和负相关

+
    +
  • 正相关:如果一个特征增加导致另一个特征增加,则它们呈正相关。值1表示完全正相关 +
      +
    • 多重线性:现在假设特征A和特征B完全正相关,这意味着这两个特征值包含高度相似的信息,信息几乎没有或完全没有差异。这称为多重线性,因为两个特征包含几乎相同的信息。
    • +
    +
  • +
  • 负相关:如果一个特征增加导致另一个特征减少,则它们呈负相关。值-1表示完全负相关
  • +
+

在搭建或训练模型时,如果同时使用这两个特征,可能其中一个会是多余的。我们应尽量消除冗余特征,因为它会使训练时间变长,同时影响其他优势

+

以下代码为生成有关SalePrice的相似性矩阵图

+
corrmat = train.corr()
+f, ax = plt.subplots(figsize=(20, 9))
+sns.heatmap(corrmat, vmax=0.8, square=True)
+

xCY4KO.md.png

+

从生成的相似性矩阵中,可以找出与房价相关性最强的变量,其中OverallQual(总评价)、GarageCars(车库)、TotalBsmtSF(地下室面积)、GrLivArea(生活面积)等特征与SalePrice呈正相关

+

从相似性矩阵中,我们还能发现变量之间的关系,如何利用相似性矩阵进行分析就成为了关键

+
    +
  1. 类别型
  2. +
+

数据探索的目的是为了帮助我们了解数据并且构建有效特征。

+

比如,我们找到了与标签有着强相关的特征,那么就可以围绕着这个强相关特征进行一系列的扩展,具体可以进行交叉组合,比如强相关加弱相关、强相关加强相关等组合,挖掘更高维度的潜在信息。

+

首先,观察类别型变量的基本分布情况,即观察每个属性的频次。根据频次,我们不仅可以发现热点属性和极少出现的属性,还可以进一步分析出现这些情况的原因,比如淘宝网的女性用户多于男性,主要是因为平台在服饰和美妆业务方面拥有强大的影响力。这是从业务角度考虑,自然也有可能是数据采样的原因。

+

对部分类别变量的分布进行可视化展示

+
df_not_num = train.select_dtypes(include = ['O'])
+fig, axes = plt.subplots(round(len(df_not_num.columns) / 3), 3, figsize=(12, 30))
+for i, ax in enumerate(fig.axes):
+    if i < len(df_not_num.columns):
+        ax.set_xticklabels(ax.xaxis.get_majorticklabels(), rotation=45)
+        sns.countplot(x=df_not_num.columns[i], alpha=0.7, data=df_not_num, ax=ax)
+fig.tight_layout()
+

xCU8bD.md.png

+

多变量分析

+

单变量分析太过于单一,不足以挖掘变量之间的内在联系,获取更加细粒度的信息,所以有必要进行多变量分析。分析特征变量与特征变量之间的关系有助于构建更好的特征,同时降低构建冗余特征的概率值。

+

此处选用本赛题中需要特别关注的特征变量进行分析

+

从相似性矩阵中,我们已知房屋评价与SalePrice呈正相关。进一步扩展分析,通过可视化来考虑房屋评价和房屋位置是否存在某种联系。

+
plt.style.use('seaborn-white')
+type_cluster = train.groupby(['Neighborhood','OverallQual']).size()
+type_cluster.unstack().plot(kind='bar',stacked=True, colormap= 'PuBu', figsize=(13,11),  grid=False)
+plt.xlabel('OverallQual', fontsize=16)
+plt.show()
+

xCaFJA.md.png

+

上图为不同房屋位置的评价分布条状图,我们可发现颜色越深代表评价越高。NoRidge、NridgHt和StoneBr都有不错的评价

+

再进一步看看不同位置房屋的SalePrice

+
var = 'Neighborhood'
+data = pd.concat([train['SalePrice'], train[var]], axis=1)
+f, ax = plt.subplots(figsize=(26, 12))
+fig = sns.boxplot(x=var, y="SalePrice", data=data)
+

xCalJs.md.png

+

高评价位置对应高SalePrice,说明房屋位置评价与房屋售价有比较强的相关性。除了通过这样的分析证明原始特征与SalePrice强相关外,还可以通过分析来构建新的特征。

+

既然房屋位置和房屋评价的组合能够出现更高售价的房屋,那么我们可以构造这两个类别特征的交叉组合特征来进行更细致的描述,也可以构造这个组合特征下的房屋均价等。

+

模型分析

+

学习曲线

+

学习曲线是机器学习中被广泛使用的效果评估工具,能够反映训练集和验证集在训练迭代中的分数变化情况,帮助我们快速了解模型的学习效果。我们可以通过学习曲线来观察模型是否过拟合,通过判断拟合程度来确定如何改进模型

+

学习曲线广泛应用于机器学习中的模型评估,模型会随着训练迭代逐步学习(优化其内部参数),例如神经网络模型。这时用于评估学习的指标可能会最大化(分类准确率)或者最小化(回归误差),这也意味着得分越高(低)表示学习到的信息越多(少)。

+

以下是学习曲线图中观察到的一些常见形状

+
    +
  1. 欠拟合学习模型
  2. +
+

欠拟合是指模型无法学习到训练集中数据所展现的信息,这里可以通过训练损失的学习曲线来确定是否发生欠拟合。在通常情况下,欠拟合学习曲线可能是一条平坦的线或者有着相对较高的损失,也就表明该模型根本无法学习训练集

+
    +
  1. 过拟合学习模型
  2. +
+

过拟合是指模型对训练集学习得很好,包括统计噪声或训练集中的随机波动。过拟合的问题在于,模型对于训练数据的专业化程度越高,对新数据的泛化能力就越差,这会导致泛化误差增加。泛化误差的增加可以通过模型在验证集上的表现来衡量。如果模型的容量超出了问题所需的容量,而灵活性又过多,则会经常发生这种情况。如果模型训练时间过长,也会发生过拟合。

+

特征重要性分析

+

通过模型训练可以得到特征重要性。对于树模型(如LightGBM和XGBoost),通过计算特征的信息增益或分裂次数得到特征的重要性得分。对于模型LR和SVM,则是使用特征系数作为特征重要性得分,例如LR(逻辑回归),每个特征各对应一个特征系数w,w越大,那么改特征对模型预测结果的影响就会越大,就可以认为该特征越重要。我们假定特征性得分和特征系数w都是在衡量特征在模型中的重要性,都可以起到特征选择的作用。

+

误差分析

+

误差分析是通过模型预测结果来发现问题的关键。

+

一般而言,回归问题中看预测结果的分布,分类问题中看混淆矩阵等。

+

在真实问题中,误差分析会更加细致。比如,在进行一个用户违约预估的二分类任务中,验证集结果中有200个错误分类样本,进一步分析发现有70%的错误分类样本是由于大量特征缺失而导致的误判,这时就需要调整,既可以通过挖掘更多能够描述这些误判样本的特征信息帮助增强模型的预测能力,还可以在模型训练中赋予这些误判样本更高的权重。

+

第4章 特征工程

+

参考资料:《机器学习算法竞赛实战》学习笔记4.特征工程

+

数据预处理

+

尽量得到标准、干净、连续的数据,供数据统计、数据挖掘等使用,视情况尝试对缺失值进行处理,比如是否要填充,填充什么。此外,有些竞赛提供的数据集以及对应的存储方式可能使得需要占用超过参赛者本身硬件条件的内存,故有必要进行一定的内存优化,这也有助于在有限的内存空间对更大的数据集进行操作。

+

缺失值处理

+

除了XGBoost和LightGBM等算法在训练时可以直接处理缺失值以外,其他很多例如LR、DNN、CNN、RNN等都并不能对缺失值进行直接处理。故而在数据准备阶段,要比构建算法阶段花更多时间,因为像填补缺失值这样的操作需要细致处理。

+
    +
  1. 区分缺失值
  2. +
+

首先,需找到缺失值表现形式。除了None、NA和NaN外,还有例如-1或-999来填充的缺失值。还有一种看上去像缺失值,但实际上有实际意义的业务,此时需特殊对待。例如没有填“婚姻状态”的用户可能是对自己隐私比较敏感,应为其单独设为一个分类;没有“驾龄”可能是没有车,为其填充0比较合适。

+
    +
  1. 处理方法
  2. +
+

数据缺失可以分为类别特征的缺失和数值特征的缺失两种。

+
    +
  • 对于类别特征,通常会填充一个新类别,如0,-1等。
  • +
  • 对于数值特征,可以均值填充(但对异常值较为敏感),中位数填充(对异常值不敏感)。填充时一定要考虑所选择的填充方法会不会影响数据的准确性。
  • +
+

填充方法总结如下:

+
    +
  • 类别特征:可选择最常见的一类填充方法,即众数;或直接填一个新类别
  • +
  • 数值特征:可填平均数、中位数、最大最小值等,具体情况具体分析
  • +
  • 有序数据(如时间序列):可填充相邻值next或previous
  • +
  • 模型预测填充:普通的填充仅是一个结果的常态,并未考虑其他特征间相互作用的影响,可以对含有缺失值的那一列进行建模并预测其中缺失值的结果。方法虽然复杂但随后得到的结果直觉上比直接填充要好。
  • +
+

异常值处理

+

实际数据中可能会发现某个或某些字段(特征)根据某个变量(如时间序列问题中的时间)排序后,经观察存在一些数值远高于或低于其一定范围内的其他数值。还有些不合理的存在,这些都可以视作异常值,他们可能会给算法性能带来负面影响。

+
    +
  1. 寻找异常值
  2. +
+

首先,找到异常值,总结了两种方法:

+
    +
  • 通过可视化分析。简单使用散点图(Matplotlib),严重偏离密集区域的点都可当作异常值来处理
  • +
  • 通过简单的统计分析,即根据基本的统计方法来判断数据是否异常,例如四分位数间距、极差、均差、标准差等,这种方法适合于挖掘单变量的数值型数据。(seaborn库的箱型图)
  • +
+
    +
  1. 处理异常值
  2. +
+
    +
  • 删除含有异常值的记录。优点:可消除含有异常值的样本带来的不确定性。缺点:减少了样本量
  • +
  • 视为缺失值。优点:将异常值集中化为一类,增加了数据的可用性。缺点:将异常值和缺失值混为一谈,会影响数据的准确性、
  • +
  • 平均值(中位数修正)。用对应同类别的数值使用平均值修正该异常值。优缺点同“视为缺失值”
  • +
  • 不处理。直接在有异常值的数据集上进行数据挖掘。这就听天由命看异常值来源了。
  • +
+

离散型异常值(离散属性定义范围以外的所有值均为异常值)、知识型异常值(如大学生脱发情况:从无)等,都可以当做类别缺失值来处理。

+
    +
  1. 优化内存
  2. +
+

数据集太大而自己的硬件条件有限就有可能会因为内存不够导致代码出现memory error,介绍Python的内存回收机制和数值类型优化这两种有助于优化内存的常见方法。

+
    +
  • 内存回收机制:在Python的内存回收机制中,gc模块主要运用“引用计数”来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记清除”来解决容器对象可能产生的循坏引用问题,通过“隔代回收”以空间换取时间来进一步提高垃圾回收的效率。一般来讲,在我们删除一些变量时,使用gc.collect()来释放内存。(慎用)
  • +
  • 数值类型优化。竞赛中常使用的数据保存格式是csv以及txt,在进行处理时,需要将其读取为表格型数据,即DataFrame格式。需要利用pandas进行操作。pandas可以在底层将数值型数据表示成NumPy数组,并使其在内存中连续存储。这种存储方式不仅消耗的空间较少,还使我们能够快速访问数据。
  • +
+

我们可以用np.iinfo类来确认每一个int型子类型的最大值和最小值

+
import numpy as np
+np.iinfo(np.int8).min
+np.iinfo(np.int8).max
+
    +
  • 对于类别型的变量,若其编码ID的数字较大、极不连续且种类较少,则可以从0开始编码(自然数编码),这样可以减少变量的内存占用。
  • +
  • 对于数值型的变量,常常由于存在浮点数使得内存占用过多,可以考虑先将其最小值和最大值归一化,然后再乘以100、1000等,再取整,节省内存空间。
  • +
+

特征变换

+

连续变量无量纲化

+

无量纲化指的是将不同规格的数据转换到同一规格。常见无量纲化方法有标准化和区间缩放法。标准化的前提是特征值服从正态分布,标准化后,特征值服从标准正态分布。区间缩放法利用了边界信息,将特征的取值区间缩放到某个特定的范围,例如[0,1]

+

单特征转换是构建一些模型(如线性回归、KNN、神经网络)的关键,对于决策树相关模型并无影响。还有些纯粹的工程原因,即在进行回归预测时,对目标取对数处理,不仅可以缩小数据范围,而且压缩变量尺度使数据更平稳。

+

然而,数据要求不仅是通过参数化方法施加的。如果特征没有被规范化,例如当一个特征的分布位于0附近且范围不超过(-1,1),而另一个特征的分布范围在数十万数量级时,会导致分布于0附近的特征变得完全无用。

+
    +
  • 标准化:最简单的转换是标准化(零-均值规范化)。标准化需要计算特征的均值和标准差。
  • +
  • 区间缩放:区间缩放思路有很多种,常见的一种使利用最大最小值进行缩放。
    +2.2 连续变量数据变换
    +1.log变换
    +进行log变换可以将倾斜数据变得接近正态分布,因为大多数机器学习模型不能很好地处理非正态分布数据,比如右倾数据。可以应用log(x+1)变换来修正倾斜,其中+1的目的是防止数据等于0,同时保证x都是正的。取对数不会改变数据的性质和相关关系,但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。
  • +
+

扩展:cbox-cox变换,一种自动寻找最佳正态分布变换函数的方法。

+

连续变量数据变换

+

log变换可以将倾斜数据变得接近正态分布。

+

离散化后的特征对异常数据有很强的健壮性,更便于探索数据的相关性。常用的离散化分为无监督和有监督两种。

+

无监督的离散化分桶操作可以将连续变量离散化,同时使数据平滑,即降低噪声的影响。一般分为等频和等距两种分桶方式。

+
    +
  • 等频:区间的边界值要经过选择,使得每个区间包含数量大致相等的变量实例。这种分桶方式可以将数据变成均匀分布。
  • +
  • 等距:将实例从最小到最大值,均分为N等份,每份的间距是相等的。这里只考虑边界,每等份的实例数量可能不等。等距可以保持数据原有的分布,并且区间越多,对数据原貌保持得越好。
  • +
+

有监督的离散化对目标有很好的区分能力,常用的是使用树模型返回叶子节点来进行离散化。如在GBDT+LR经典模型中,就是先使用GBDT来将连续值转化为离散值。具体方法:用训练集中的所有连续值和标签输出来训练LightGBM,共训练两棵决策树,第一棵有4个叶子节点,第二棵树有3个叶子节点。如果某一个样本落在第一棵树的第三个叶子节点上,落在第二棵树的第一个叶子节点上,那么它的编码就是0010100,一共7个离散特征,其中会有两个取值为1的位置,分别对应每棵树中样本落点的位置。最终我们会获得num_trees*num_leaves维特征。

+

类别特征转换

+

在实际数据中,特征并不总是数值,还有可能是类别。对于离散型的类别特征进行编码,一般分为两种情况:自然数编码(特征有意义)和独热(one-hot)编码(特征无意义)。

+

自然数编码:一列有意义的类别特征(即有顺序关系)可以使用自然数进行编码,利用自然数的大小关系可以保留其顺序关系。以下是两种自然数编码的常用方式:

+

①调用sklearn中函数:

+
from sklearn import preprocessing
+from f in columns:
+	le = preprocessing.LableEncoder()
+	le.fit(data[f})
+

②自定义实现(速度快)

+
for f in columns:
+	data[f] = data[f].fillna(-999)
+	data[f] = data[f].map(dict(zip(data[f].unique(),range(0,data[f].nunique()))))
+

独热编码:当类别特征没有意义(即无顺序关系)时,需要使用独热编码。例如,红>蓝>绿不代表任何东西,进行独热编码后,每个特征的取值对应一维特征,最后得到一个样本数×类别数大小的0~1矩阵。可直接调用sklearn中API进行生成(或者是使用哑变量的方式)

+

不规则特征变换

+

不规则特征可能包含样本的很多信息,比如身份证号,各段表示不同的信息。一般不会提供这种比较敏感的信息。

+

特征提取

+

机器学习模型很难识别复杂的模式,特别是很难学习到不同特征组合交叉的信息,所以我们可以基于对数据集的直觉分析和业务理解创建一些特征来帮助模型有效学习。下面我们将介绍结构化数据的特征提取方式。

+

(结构化数据由明确定义的数据类型组成,非结构化数据由音频、视频和图片等不容易搜索的数据组成。)

+

类别相关的统计特征

+

类别特征又可以称为离散特征,除了每个类别属性的特定含义外,还可以构造连续型的统计特征,以挖掘更多有价值的信息,比如构造目标编码、count、nunique和ratio等特征。另外,也可以通过类别特征间的交叉组合构造更加细粒度的特征。

+
    +
  1. 目标编码
  2. +
+

目标编码可以理解为用目标变量(标签)的统计量对类别特征进行编码,即根据目标变量进行有监督的特征构造。如果是分类问题,可以统计目标均值、中位数和最值。目标编码的方式可以很好地替代类别特征,或者作为新特征。

+

使用目标变量时,非常重要的一点是不能泄露任何验证集的信息。所有基于目标编码的特征都应该在训练集上计算,测试集则由完整的训练集来构造。更严格一点,我们在统计训练集的特征时,需要采用K折交叉统计法构造目标编码特征,从而最大限度地防止信息泄露。如用五折交叉统计构造特征时,我们将样本划分为五份,对于其中每一份数据,我们都将用另外四份来计算每个类别取值对应目标变量的频次、比例或者均值,简单来说就是未知的数据(一份)在已知的数据(四份)里面取特征。

+

目标编码方法对于基数较低的类别特征通常很有效,但对于基数较高的类别特征,可能会有过拟合的风险。因为会存在一些类别出现频次非常低,统计出来的结果不具有代表性。一般会加入平滑性来降低过拟合风险。在处置妥当的情况下,无论是线性模型,还是非线性模型,目标编程都是最佳的编码方式和特征构造方式。

+
    +
  1. count nunique ratio
  2. +
+

count:计数特征,用于统计类别特征的出现频次

+

nunique和ratio常常会涉及多个类别特征的联合构造。例如在广告点击率预测问题中,对于用户ID和广告ID,使用nunique可以反映用户对广告的兴趣宽度,也就是统计用户ID看过几种广告ID;使用ratio可以反映用户对某类广告的偏好程度,即统计用户ID点击某类广告ID的频次占用户点击所有广告ID频次的比例。

+
    +
  1. 类别特征之间交叉组合
  2. +
+

交叉组合能够描述更细粒度的内容。对类别特征进行交叉组合在竞赛中是一项非常重要的工作,这样可以进行很好的非线性特征拟合。如用户年龄和用户性别可以组合成“年龄_性别”这样的新特征。一般我们可以对两个类别或三个类别特征进行组合,也称作二阶组合或三阶组合。简单来讲,就是对两个类别特征进行笛卡尔积的操作,产生新的类别特征。

+

并非所有组合都是需要考虑的,我们会从两个方面进行分析。

+
    +
  • 业务逻辑方面:比如用户操作系统版本与用户所在城市的组合是没有实际意义的。
  • +
  • 类别特征的基数:如果基数过大,那么可能导致很多类别只会出现一次,在一轮训练中,每个类别只会被训练一次,显然特征对应权重的置信度是很低的。
  • +
+

数值相关的统计特征

+

这里所说的数值特征,我们认为是连续的。数值特征的大小是有意义的,通常不需要处理就可以直接“喂”给模型进行训练。除了之前对数值特征进行各种变换外,还存在一些其他常见的数值特征构造方式。

+
    +
  • 数值特征之间的交叉组合:一般对数值特征进行加减乘除等算术操作类的交叉组合。这需要我们结合业务理解和数据分析进行构造。
  • +
  • 类别特征和数值特征之间的交叉组合:除了类别特征之间和数值特征之间的交叉组合外,还可以构造类别特征与数值特征之间的交叉组合。这类特征通常是在类别特征的某个类别中计算数值特征的一些统计量,比如均值、中位数和最值等。
  • +
  • 按行统计相关特征:行统计在构造时会包含更多的列,直接对多列进行统计。
  • +
+

时间特征

+

在实际数据中,通常给出的时间特征是时间戳属性,所以首先需要将其分离成多个维度,比如年月日小时分钟秒钟。如果你的数据源来自于不同的地理数据源,还需要利用时区将数据标准化。除了分离出来的基本时间特征外,还可以构造时间差特征,即计算出各个样本的时间与未来某一个时间的数值差距,这样这个差距是UTC的时间差,从而将时间特征转换为连续值,比如用户首次行为日期与用户注册日期的时间差、用户当前行为与用户上次行为的时间差等。

+

多值特征

+

在竞赛中,可能会遇到某一列特征中每行都包含多个属性的情况,这就是多值特征。例如广告大赛中的兴趣类目,其中包含5个兴趣特征组,每个兴趣特征组都包含若干个兴趣ID。对于多值特征,通常可以进行稀疏化或者向量化的处理,这种操作一般出现在自然语言处理中,比如文本分词后使用TF-IDF(词频-逆文档频率)、LDA(隐含狄利克雷分布)、NMF(非负矩阵分解)等方式进行处理,这里则可以将多值特征看作文本分词后的结果,然后做相同的处理。

+

对多值特征最基本的处理办法是完全展开,即把这列特征所包含的n个属性展开成n维稀疏矩阵。使用sklearn中的CountVectorizer函数,可以方便地将多值特征展开,只考虑每个属性在这个特征的出现频次。

+

还有一种情况,比如在广告算法大赛中,需要根据用户的历史点击行为预测用户的属性标签。这时候用户的点击序列尤为重要,当我们构造好用户对应的历史点击序列后,除了使用上述的TF-IDF等方法外,还可以提取点击序列中商品或广告的嵌入表示,比如用Word2Vec、DeepWalk等方法获取embedding向量表示。因为要提取用户单个特征,所以可以对序列中的嵌入向量进行聚合统计,这种方法本质上是假设用户点击过的商品或广告等同重要,是一种比较粗糙的处理方式。我们可以引入时间衰减因素,或者使用序列模型,如RNN、LSTN、GRU,套用NLP的方法进行求解。

+

特征选择

+

当我们添加新特征时,需要验证它是否确实能够提高模型预测的准确度,以确定不是加入了无用的特征,因为这样只会增加算法运算的复杂度,这时候就要通过特征选择算法自动选择出特征集中的最优子集,帮助模型提供更好的性能。特征选择算法用于从数据中识别并删除不需要、不相关以及冗余特征。这些特征可能会降低模型的准确度和性能,特征选择的方法主要有先验的特征关联性分析以及后验的特征重要性分析。、

+

特征关联性分析

+

特征关联性分析是使用统计量来为特征之间的相关性进行评分。特征按照分数进行排序,要么保留,要么从数据集中删除。关联性分析方法通常是针对单变量的,并独立考虑特征或者因变量。常见的特征关联性分析方法有皮尔逊相关系数、卡方检验、互信息法和信息增益等。这些方法速度快、使用方便,但是忽略了特征之间的关系,以及特征和模型之间的关系。

+
    +
  1. 皮尔逊相关系数
  2. +
+

不仅可以衡量变量之间的线性相关性,解决共线变量问题,还可以衡量特征与标签的相关性。共线变量是指变量之间存在高度相关关系,这会降低模型的学习可用性,可解释性以及测试集的泛化性能。但这三个特性都是我们想增加的,所以删除共线变量是一个有价值的步骤。我们将为删除共线变量建立一个基本的阈值(根据想要保留的特征数量决定)。

+

下面代码用于解决特征与标签不具有相关性的问题,根据皮尔逊相关系数的计算提取top300的相似特征:

+
def feature_select_pearson(train,features):
+	featureSelect = features[:]
+	#进行皮尔逊相关性计算
+	corr=[]
+	for feat in featureSelect:
+		corr.append(abs(train[[feat,'target']].fillna(0).corr().values[0][1]))
+	se = pd.Series(corr,index=featureSelect).sort_values(ascending=False)
+	feature_select = se[:300}.index.tolist()
+	#返回特征选择后的训练集
+	return train[feature_select]
+
    +
  1. 卡方检验
  2. +
+

用于检验特征变量与因变量的相关性。对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验恰好可以进行独立性检验,所以使用与特征选择。如果检验结果是某个特征与标签独立,则可以去除该特征。

+

    +
  1. 互信息法
  2. +
+

互信息是对一个联合分布中两个变量之间相互影响关系的度量,也可以用于评价两个变量之间的相关性。互信息法之所以能够用于特征选择,可以从两个角度进行解释:基于KL散度和基于信息增益。互信息越大说明两个变量相关性越高。

+

但是想把互信息直接用于特征选择其实不太方便,由于:

+
    +
  • 它不属于度量方式,也没有办法归一化,无法对不同数据集上的结果进行比较
  • +
  • 对于连续变量的计算不是很方便(X和Y都是集合,xi和y都是离散的取值),通常连续变量需要先离散化,而互信息的结果对离散化的方式很敏感。
  • +
+

特征重要性分析

+

在实际竞赛中,经常用到的一种特征选择方法是基于树模型评估特征的重要性分数。特征的重要性分数越高,说明特征在模型中被用来构建决策树的次数越多。这里我们以XGBoost为例来介绍树模型评估特征重要性的三种计算方法(weight、gain和cover)。(LightGBM也可以返回特征重要性)

+
    +
  • weight计算方式:该方式比较简单,计算特征在所有树中被选为分裂特征的次数,并将以此作为评估特征重要性的依据
  • +
+
params ={
+	'max_depth':10,
+	'subsample':1,
+	'verbose_eval':True,
+	'seed':12,
+	'objective':'binary:logistic'
+}
+xgtrain = xgb.DMatrix(x,label=y)
+bst = xgb.train(params,xgtrain,numm_boost_round=10)
+importance = bst.get_score(fmap='',importance_type='weight')
+
    +
  • gain计算方式:gain表示平均增益。在进行特征重要性评估时,使用gain表示特征在所有树中作为分裂节点的信息增益之和再除以该特征出现的频次。
  • +
+
importance =bst.get_score(fmap='',importance_type='gain')
+
    +
  • cover计算方式:cover是特征对每棵树的覆盖率,即特征被分到该节点的样本的二阶导数之和,而特征度量的标准就是平均覆盖率值。
  • +
+
importance = bst.get_score(fmap='',importance_type='cover')
+

技巧:虽然特征重要性可以帮助我们快速分析特征在模型训练过程中的重要性,但不能将其当做绝对的参考依据。一般而言,只要特征不会导致过拟合,我们就可以选择重要性高的特征进行分析和扩展,对于重要性低的特征,可以考虑将之从特征集中移除,然后观察线下效果,再做进一步判断。

+

封装方法

+

可以将一组特征的选择视作一个搜索问题,在这个问题中,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集,搜索过程可以是系统性的,比如最佳优先搜索;也可以是随机的,比如随机爬山算法,或者启发式方法,比如通过向前和向后搜索来添加和删除特征(类似前剪枝和后剪枝算法)。这种方法比较耗时。

+
    +
  • 启发式方法:分为前向搜索和后向搜索。前向搜索是每次增量地从剩余未选中的特征中选出一个并将其加入特征集中,待特征集中的特征数量达到初设阈值时,意味着贪心选出了错误率最小的特征子集。既然有增量加,就会有增量减,后者称为后向搜索,即从特征全集开始,每次删除其中的一个特征并评价,知道特征集中的特征数量达到初设阈值,就选出了最佳的特征子集 +
      +
    • 因为启发式方法会导致局部最优,所以加入模拟退火方式进行改善,这种方式不会因为新加入的特征不能改善效果而舍弃该特征,而是对其添加权重后放入已选特征集。这种启发式方法是很耗时间耗资源的。
    • +
    +
  • +
  • 递归消除特征法:用一个基模型来进行多轮训练,每轮训练都会先消除若干权值系数的特征,再基于新特征集进行下一轮训练。可以使用feature_selection库的RFE类来进行特征选择
  • +
+
from sklearn.feature_selection import RFE
+from sklearn.linear_model import LogisticRegression
+#递归消除特征法,返回特征选择后的数据
+#参数estimator为基模型
+#参数n_feature_to_select 为选择的特征个数
+RFE(estimator=LogisticRegression(),n_features_to_select=2).fit_transform(data,target)
+

技巧:在使用封装方法进行特征选择时,用全量数据训练并不是最明智的选择。应先对大数据进行采样,再对小数据使用封装方法

+

以上三种特征选择方法按需使用或组合使用,建议优先考虑特征重要性,其次是特征关联性。

+

此外还有null importance。其思想:将构建好的特征和正确的标签喂给树模型得到一个特征重要性分数,再将特征和打乱后的标签喂给树模型得到一个特征重要性分数,然后对比两个分数,如果前者没有超过后者,那么这个特征就是一个无用的特征。

+

第5章 模型训练

+

参考资料 :《机器学习算法竞赛实战》整理 | 五、模型训练

+

线性模型

+

Lasso回归

+

Lasso回归是对普通的线性回归采用L1正则化进行优化,通过惩罚或限制估计值的绝对值之和,可以使某些系数为零,从而达到特征稀疏化和特征选择的效果。当我们需要一些自动的特征、变量选择,或者处理高度相关的预测因素时,很方便。

+
from sklearn.linear_model import Lasso
+lasso_model = Lasso(alpha = 0.1, normalize = True)
+

只保留不相关的特征,其他为0,可能会导致信息损失

+

Ridge回归

+

Ridge回归是对普通的线性回归采用L2正则化进行优化,对特征的权重系数设置了惩罚项。

+
from sklearn.linear_model import Ridge
+ridge_model = Ridge(alpha = 0.05, normalize = True)
+

不会减少特征数量,不利于特征缩减。

+

两者合并:Elastic Net Regression

+

树模型

+

本节将介绍竞赛中常见的树模型,这些模型简单易用,能够带来高收益。

+

可将树模型分为随机森林(Random Forest, RF)和梯度提升树(GBDT), 这两者最大的差异是前者并行、后者串行。在梯度提升树部分我们将介绍如今竞赛中大火的三种树模型: XGBoost、 LightGBM 和CatBoost。能够灵活运用这三种模型是竞赛中的必备技能。接下来将详细介绍各种树模型的数学形式、优缺点、使用细节和应用场景。

+

随机森林

+

随机森林就是通过集成学习的思想将多个决策树集成在一起,各个决策树之间没有任何关联。随机森林算法对多个决策树的结果进行投票得到最终结果,也是最简单的bagging思想 。

+

随机森林的优点:

+
    +
  • 不仅可以解决分类和回归问题,还可以同时处理类别特征和数值特征;
  • +
  • 不容易过拟合,通过平均决策树的方式,降低过拟合的风险;
  • +
  • 非常稳定,即使数据集中出现了一个新的数据点,整个算法也不会受到过多影响,新的数据点只会影响到一棵决策树,很难对所有决策树都产生影响。
  • +
+

很多缺点都是相对而言的:

+
    +
  • 随机森林算法虽然比决策树算法更复杂,计算成本更高,但是其拥有天然的并行特性,在分布式环境下可以很快地训练。
  • +
  • 梯度提升树需要不断地训练残差,进行所以结果准确度更高,但是随机森林更不容易过拟合,更加稳定,这也是因为其Bagging的特性。
  • +
+
from sklearn.ensemble import RandomForestClassifier
+rf = RandomForestClassifier(max_ features=' auto', oob_ score=True, random state=1, n_ jobs=-1)
+

梯度提升树

+

梯度提升树(GBDT)是基于Boosting改进而得的,在Boosting算法中,一系列基学习器都需要串行生成,每次学习一棵树,学习目标是上棵树的残差。和AdaBoost 一样,梯度提升树也是基于梯度下降函数。梯度提升树算法已被证明是Boosting算法集合中最成熟的算法之一,它的特点是估计方差增加,对数据中的噪声更敏感(这两个问题都可以通过使用子采样来减弱),以及由于非并行操作而导致计算成本显著,因此要比随机森林慢很多。

+

梯度提升树是XGBoost、LightGBM和CatBoost的基础。

+

XGBoost

+
    +
  • 采用稀疏感知算法,XGBoost可以利用稀疏矩阵,节省内存(不需要密集矩阵)和节省计算时间(零值以特殊方式处理)。
  • +
  • 近似树学习(加权分位数略图),这类学习方式能得到近似的结果,但比完整的分支切割探索要省很多时间。
  • +
  • 在一台机器上进行并行计算(在搜索最佳分割阶段使用多线程),在多台机器上进行类似的分布式计算。
  • +
  • 利用名为核外计算的优化方法,解决在磁盘读取数据时间过长的问题。将数据集分成多个块存放在磁盘中,使用一个独立的线程专门从磁盘读取数据并加载到内存中,这样一来,从磁盘读取数据和在内存中完成数据计算就能并行运行。
  • +
  • XGBoost还可以有效地处理缺失值,训练时对缺失值自动学习切分方向。基本思路是在每次的切分中,让缺失值分别被切分到决策树的左节点和右节点,然后通过计算增益得分选择增益大的切分方向进行分裂,最后针对每个特征的缺失值,都会学习到一个最优的默认切分方向。
  • +
+
import xgboost as xgb
+params = {'eta': 0.01, ' max depth': 11, 'objective': 'reg:linear', 'eval_ metric': 'rmse' }
+dtrain = xgb.DMatrix(data=X_train, label=y_train)
+dtest = xgb.DMatrix(data=X_valid, label=y_valid)
+watchlist = [(train.data, 'train'), (valid_data, 'valid_ data')]model=xgb. train(params, train_data,num_boost_round=20000,evals=watchlist,early_stopping_rounds=200,verbose_eval=500)y_pred = model. predict(xgb .DMatrix(X_test), ntree_limit=model.best_ntree_limit)
+

LightGBM

+

LightGBM是微软的一个团队在Github上开发的一个开源项目,高性能的LightGBM算法具有分布式和可以快速处理大量数据的特点。LightGBM虽然基于决策树和XGBoost而生,但它还遵循其他不同的策略。

+

XGBoost使用决策树对一个变量进行拆分,并在该变量上探索不同的切割点(按级别划分的树生长策略),而LightGBM则专注于按叶子节点进行拆分,以便获得更好的拟合(这是按叶划分的树生长策略)。这使得LightGBM能够快速获得很好的数据拟合,并生成能够替代XGBoost的解决方案。从算法上讲,XGBoost将决策树所进行的分割结构作为一个图来计算,使用广度优先搜索(BFS),而LightGBM使用的是深度优先搜索(DFS)。

+

主要特点

+
    +
  • 比XGBoost准确性更高,训练时间更短。
  • +
  • 支持并行树增强,即使在大型数据集上也能提供比 XGBoost更好的训练速度。
  • +
  • 通过使用直方图算法将连续特征提取为离散特征,实现了惊人的快速训练速度和较低的内存使用率。
  • +
  • 通过使用按叶分割而不是按级别分割来获得更高精度,加快目标函数收敛过程,并在非常复杂的树中捕获训练数据的底层模式。使用num_leaves和max_depth超参数控制过拟合。
  • +
+
import lightgbm as lgb
+params = {'num_leaves': 54, 'objective': 'regression', 'max_depth': 18,'learning_rate': 0.01, 'boosting': 'gbdt', 'metric': 'rmse', 'lambda_11': 0.1}
+model = lgb.LGBMRegressor(**params, n_estimators = 20000, nthread = 4, n_jobs = -1)
+model.fit(x_train, y_train, eval_set=[(X_train, y_train), (X_valid, y_valid)], eval_metric='rmse', verbose=1000, early_stopping_rounds=200)
+y_pred= model.predict(X_test, num_iteration=model.best_iteration_)
+

CatBoost

+

CatBoost是由俄罗斯搜索引擎Yandex在2017年7月开源的一个GBM算法,它最强大的点是能够采用将独热编码和平均编码混合的策略来处理类别特征。

+

CatBoost用来对类别特征进行编码的方法并不是新方法,是均值编码,该方法已经成为一种特征工程方法,被广泛应用于各种数据科学竞赛中,如Kaggle。

+

均值编码,也称为似然编码、影响编码或目标编码,可将标签转换为基于它们的数字,并与目标变量相关联。如果是回归问题,则基于级别典型的平均目标值转换标签;如果是分类问题,则仅给定标签的目标分类概率(目标概率取决于每个类别值)。均值编码可能看起来只是一个简单而聪明的特征工程技巧,但实际上它也有副作用,主要是过拟合,因为会把目标信息带入预测中。

+

主要特点

+
    +
  • 支持类别特征,因此我们不需要预处理类别特征(例如通过label encoding或独热编码)。事实上,CatBoost文档中讲到不要在预处理期间使用独热编码,因为“这会影响训练速度和结果质量”。
  • +
  • 提出了一种全新的梯度提升机制(Ordered Boosting),不仅可以减少过拟合的风险,也大大提高了准确性。
  • +
  • 支持开箱即用的GPU训练(只需设置task_type=“GPU”)。
  • +
  • 训练中使用了组合类别特征,利用了特征之间的联系,极大丰富了特征维度。
  • +
  • 在树分裂选择节点的时候能够将所有类别特征之间的组合考虑进来,即能够对两个类别特征进行组合。
  • +
  • 目前还支持输入文本特征,因此不需要像以前那样先进行烦琐的操作获得标准化输入,再喂给模型。
  • +
+
from catboost import CatBoostRegressor
+params = {'learning_rate': 0.02, 'depth': 13,'bootstrap_type': 'Bernoulli', 'od_type': 'Iter', 'od_wait': 50, 'random_seed': 11}
+model = CatBoostRegressor(iterations=20000, eval_metric='RMSE', **params)
+model.fit(X_train, y_train, eval_set=(X_valid, y_valid), cat_features=[], use_best_model=True, verbose=False)
+y_pred = model.predict(X_test)
+

模型深入对比

+

每类树模型都其与众不同的地方,接下来将从决策树的生长策略、梯度偏差、类别特征处理和参数对比四个方面深入理解这些树模型,帮助参赛者更好地将它们应用到竞赛中。
+XGBoost,LightGBM 和 CatBoost是三个非常核心的树模型,本节将对它们进行分析,因为三者之间有着千丝万缕的关系,只有厘清其中的关系,才能更好地运用这三个模型。

+
    +
  1. 决策树生长策略
  2. +
+
    +
  • XGBoost使用的是Level-wise按层生长,可以同时分裂同一层的叶子,从而进行多线程优化,不容易过拟合,但很多叶子节点的分裂增益较低,会影响性能。
  • +
  • LightGBM使用的是Leaf-wise分裂方式,每次都从当前叶子中选择增益最大的结点进行分裂,循环迭代,但会生长出非常深的决策树,从而导致过拟合,这时可以调整参数max_depth来防止过拟合。
  • +
  • CatBoost 使用的是oblivious-tree(对称树),这种方式使得节点是镜像生长的。相对于传统的生长策略,oblivious-tree能够简单拟合方案,快速生成模型,这种树结构起到了正则化的作用,因此并不容易过拟合。
  • +
+
    +
  1. 梯度偏差(Gradient bias)
  2. +
+
    +
  • XGBoost和LightGBM中的提升树算法都是有偏梯度估计,在梯度估计中使用的数据与目前建立的模型所使用的数据是相同的,这样会导致数据发生泄漏,从而产生过拟合。
  • +
  • CatBoost改进了提升树算法,将原来的有偏梯度估计转换为了无偏梯度估计。具体做法是利用所有训练集(除第i条)建立模型,然后使用第1条到第i-1条数据来建一个修正树M,累加到原来的模型上。
  • +
+
    +
  1. 类别特征处理
  2. +
+
    +
  • XGBoost并不能处理类别特征,因此需要我们根据数据实际情况进行独热编码、count编码和目标编码。
  • +
  • LightGBM 直接支持类别特征,不需要独热展开。这里使用many-vs-many的切分方式来处理类别特征,并且可以把搜索最佳分割点的时间复杂度控制在线性级别,和原来one-vs-other方式的时间复杂度几乎一致。该算法先按照每个类别对应的标签均值(即avg(y)=Sum(y)/Count(y))进行排序,然后根据排序结果依次枚举最优分割点。和数值型特征的切分方式不同,它是将某一类别当作一类,然后将其余所有类别作为一类。
  • +
  • CatBoost在处理类别特征方面做了更细致的操作。或许在使用LightGBM时,还需要对类别特征进行更多的编码方式,但对于CatBoost,则可以选择不进行多余的编码方式。具体实现流程是首先对输入的样本集随机排序,然后针对类别特征中的某个取值,在将每个样本的该特征转换为数值型时,都基于排在该样本之前的类别标签取均值。对所有的类别特征值结果都进行如式(5-10)所示的运算,使之转化为数值结果,
  • +
+
    +
  1. 参数对比
  2. +
+

xPuQVP.png

+

神经网络

+

随着拥有数据量的增加,神经网络战胜传统机器学习模型的可能性也会加大。

+
    +
  • 多层感知机:含有多个隐藏层的神经网络
  • +
  • 卷积神经网络 :广泛应用于计算机视觉领域
  • +
  • 循环神经网络:更擅长对序列数据进行建模处理
  • +
+

实战案例(未实际运行)

+
#接第5章实战案例代码,构造训练集和测试集
+x_train = data[:ntrain][all_cols]
+x_test = data[ntrain:][all_cols]
+#对售价进行log处理
+y_train = np.log1p(data[data.SalePrice.notnull()]['SalePrice'].values)
+

XGBoost:使用比较常规的五折交叉验证

+
import xgboost as xgb
+from sklearn.model_selection import KFold
+kf = KFold(n_splits=5,shuffle=True,random_state=2020)
+for i,(train_index,valid_index)in enumerate(kf.split(x_train,y_train)):
+    trn_x,trn_y,val_x,val_y = x_train.iloc[train_index],y_train[train_index],x_train.iloc[valid_index],y_train[valid_index]
+    params ={'eta':0.01,'max_depth':11,'objective':'reg:linear','eval_metric':'mae'}
+    dtrain = xgb.DMatrix(data=trn_x,label=trn_y)
+    dtest = xgb.DMatrix(data=val_x,label=val_y)
+    watchlist =[(dtrain,'train'),(dtest,'valid_data')]
+    model=xgb.train(params,dtrain,num_boost_round=20000,evals=watchlist,early_stopping_rounds=200,verbose_eval=500)
+

多层感知机:要确保数据中没有缺失值,并且要对数据进行归一化处理。

+
from sklearn. model_selection import train_test_split
+from sklearn.preprocessing import StandardScaler
+x_train = x_train. fillna(0)
+x_train = StandardScaler(). fit_transform(x_train)
+trn_x, val_x, trny, val_y = train_test_split(x_train, y_train, random_state=2020)
+def create_mlp(shape):
+    x_input = Input((shape, ))
+    X = Dropout(0.2)(BatchNormalization()(
+        Dense(256, activation=' relu')(X_input)))
+    X = Dropout(0.2)(BatchNormalization()(Dense(128, activation=' relu')(X)))
+    X = Dropout(0.2)(BatchNormalization()(Dense(64, activation=' relu')(X)))
+    X = Dense(1)(X)
+    model = Model(inputs=X_input, outputs=X)
+    model. compile(optimizer=' adam', loss=' mse', metrics=[' mae'])
+    return modelmlp_model = create_mlp(trn_x. shape[1])
+mlp_model.fit(x=trn_x, y=trn_y, validation_data=(val_x, val_y), epochs=30, batch_size=16)
+

第6章 模型融合

+

参考资料:《机器学习算法竞赛实战》整理 | 六、模型融合

+

本章将向大家介绍在算法竞赛中提分的关键步骤,这也是最后阶段的惯用方法,即模型融合(或者集成学习),通过结合不同子模型的长处进行模型融合,当然这是在理想状态下。

+

本章主要分为构建多样性、训练过程融合和训练结果融合三部分。

+

模型融合常常是竞赛取得胜利的关键,相比之下具有差异性的模型融合往往能给结果带来很大提升。了解的模型融合方法越多,最后取胜的概率就会越高。

+

本章从这三个部分介绍不同模型融合方法的应用场景,同时给出使用技巧和应用代码。

+

构建多样性

+

介绍三种模型融合中构建多样性的方式,分别是特征多样性、样本多样性和模型多样性。其中多样性是指子模型之间存在着差异,可以通过降低子模型融合的同质性来构建多样性,好的多样性有助于模型融合效果的提升。

+

特征多样性

+

构建多个有差异的特征集并分别建立模型,可使特征存在于不同的超空间(hyperspace),从而建立的多个模型有不同的泛化误差,最终模型融合时可以起到互补的效果。在竞赛中,队友之间的特征集往往是不一样的,在分数差异不大的情况下,直接进行模型融合基本会获得不错的收益。

+

另外,像随机森林中的max_features,XGBoost中的colsample_bytree 和LightGBM中的feature_fraction都是用来对训练集中的特征进行采样的,其实本质上就是构建特征的多样性。

+

样本多样性

+

样本多样性也是竞赛中常见的一种模型融合方式,这里的多样性主要来自不同的样本集。

+

具体做法是将数据集切分成多份,然后分别建立模型。我们知道很多树模型在训练的时候会进行采样(sampling),主要目的是防止过拟合,从而提升预测的准确性。

+

有时候将数据集切分成多份并不是随机进行的,而是根据具体的赛题数据进行切分,需要考虑如何切分可以构建最大限度的数据差异性,并用切分后的数据分别训练模型。

+

例如,在天池“全球城市计算AI挑战赛”中,竞赛训练集包含从2019年1月1日到1月25日共25天的地铁刷卡数据记录,要求预测1月26日每个地铁站点每十分钟的平均出入客流量(2019年1月26日是周六)。显然,工作日和周末的客流量分布具有很大差异,这时会面临一个问题,若只保留周末的数据进行训练,则会浪费掉很多数据;若一周的数据全部保留,则会对工作日的数据产生一定影响。这时候就可以尝试构建两组有差异性的样本分别训练模型,即整体数据保留为一组,周末数据为一组。当然,模型融合后的分数会有很大提升。

+

模型多样性

+

不同模型对数据的表达能力是不同的,比如FM能够学习到特征之间的交叉信息,并且记忆性较强;树模型可以很好地处理连续特征和离散特征(如LightGBM 和CatBoost),并且对异常值也具有很好的健壮性。把这两类在数据假设、表征能力方面有差异的模型融合起来肯定会达到一定的效果。

+

对于竞赛而言,传统的树模型(XGBoost,LightGBM、CatBoost)和神经网络都需要尝试一遍,然后将尝试过的模型作为具有差异性的模型融合在一起。

+

还有很多其他构建多样性的方法,比如训练目标多样性、参数多样性和损失函数选择的多样性等,这些都能产生非常好的效果。

+

训练过程融合

+

模型融合的方式有两种,第一种是训练过程融合,比如我们了解到的随机森林和XGBoost,基于这两种模型在训练中构造多个决策树进行融合,这里的多个决策树可以看作多个弱学习器。其中随机森林通过Bagging的方式进行融合,XGBoost通过Boosting的方式进行融合。

+

Bagging

+

Bagging的思想很简单,即从训练集中有放回地取出数据(Bootstrapping),这些数据构成样本集,这也保证了训练集的规模不变,然后用样本集训练弱分类器。重复上述过程多次,取平均值或者采用投票机制得到模型融合的最终结果。

+

当在不同的样本集上训练模型时,Bagging通过减小误差之间的差来减少分类器的方差,因此Bagging可以降低过拟合的风险。Bagging算法的效率在于训练数据的不同,各模型之间存在着很大的差异,并且在加权融合的过程中可以使训练数据的错误相互抵消。

+

Boosting

+

Boosting的思想其实并不难理解,首先训练一个弱分类器,并把这个弱分类器分错类的样本记录下来,同时给予这个弱分类器一定的权重;然后建立一个新的弱分类器,新的弱分类器基于前面记录的错误样本进行训练,同样,我们也给予这个分类器一个权重。重复上面的过程,直到弱分类器的性能达到某一指标,例如当再建立的新弱分类器并不会使准确率显著提升时,就停止选代。最后,把这些弱分类器各自乘上相应的权重并全部加起来,就得到了最后的强分类器。其实,基于Boosting的算法是比较多的,有Adaboost、LightGBM、XGBoost和CatBoost等。

+

训练结果融合

+

模型融合的第二种方式是训练结果融合,主要分为加权法、Stacking和Blending,这些方法都可以有效地提高模型的整体预测能力,在竞赛中也是参赛者必须要掌握的方法。

+

加权法

+

加权法对于一系列任务(比如分类和回归)和评价指标(如AUC,MSE 或 Logloss)都是很有效的,比如我们有10个算法模型并都预测到了结果,直接对这10个结果取平均值或者给予每个算法不同的权重,即得到了融合结果。加权法通常还能减少过拟合,因为每个模型的结果可能存在一定的噪声,加权法能够平滑噪声,提高模型的泛化性。

+
    +
  1. +

    分类问题:对于分类问题,需要注意不同分类器的输出结果范围一致,因为输出的预测结果可以是0/1值,也可以是介于0和1之间的概率。另外,投票法(Voting)也是一种特殊的加权法。

    +
  2. +
  3. +

    回归问题:对于回归问题,使用加权法会非常简单。这里主要介绍算术平均和几何平均。

    +
  4. +
+
    +
  • 在2019腾讯广告算法大赛中,选择几何平均的效果远远好于选择算术平均,这是由于评分规则是平均绝对百分比误差(SMAPE),此时如果选择算术平均则会使模型融合的结果偏大,这不符合平均绝对百分比误差的直觉,越小的值对评分影响越大,算术平均会导致出现更大的误差,所以选择几何平均,能够使结果偏向小值。
  • +
+

算术平均:基于算术平均数的集成方法在算法中是用得最多的,因为它不仅简单,而且基本每次使用该算法都有较大概率能获得很好的效果。

+

几何平均:根据很多参赛选手的分享,基于几何平均数的加权法在算法中使用得还不是很多,但在实际情况中,有时候基于几何平均数的模型融合效果要稍好于基于算术平均数的效果。

+
    +
  1. 排序问题
  2. +
+

一般推荐问题中的主要任务是对推荐结果进行排序,常见的评价指标有mAP(mean Average Precision),NDCG(Normalized Discounted Cumulative Gain),MRR(Mean Reciprocal Rank)和AUC,这里主要介绍MRR和AUC。

+

MRR:给定推荐结果q,如果q在推荐序列中的位置是r,那么MRR(q)就是1/r。可以看出,如果向用户推荐的产品在推荐序列中命中,那么命中的位置越靠前,得分也就越高。显然,排序结果在前在后的重要性是不一样的,因此我们不仅要进行加权融合,还需要让结果偏向小值。这时候就要对结果进行转换,然后再用加权法进行融合,一般而言使用的转换方式是log变换。
+其基本思路如下:首先,输人三个预测结果文件,每个预测结果文件都包含M条记录,每条记录各对应N个预测结果,最终输出三个预测结果文件的整合结果。统计三个预测结果文件中记录的所有推荐商品(共N个商品)出现的位置,例如商品A,在第一份文件中的推荐位置是1,在第二个文件的推荐位置是3,在第三个文件中未出现,此时我们计算商品A的得分为log1+log3+log(N+1),此处我们用N+1来表示未出现,即在N个推荐商品中是找不到商品A的,所以只能是N+1。对每条记录中的商品按计算得分由小到大排序,取前N个作为这条记录的最终推荐结果。

+

AUC:作为排序指标,一般使用排序均值的融合思路,使用相对顺序来代替原先的概率值。很多以AUC为指标的比赛均取得了非常不错的成绩。使用过程如下:对每个分类器中分类的概率进行排序,然后用每个样本排序之后得到的排名值(rank)作为新的结果。对每个分类器的排名值求算术平均值作为最终结果。

+

Stacking 融合

+

使用加权法进行融合虽然简单,但需要人工来确定权重,因此可以考虑更加智能的方式,通过新的模型来学习每个分类器的权重。这里我们假设有两层分类器,如果在第一层中某个特定的基分类器错误地学习了特征空间的某个区域,则这种错误的学习行为可能会被第二层分类器检测到,这与其他分类器的学习行为一样,可以纠正不恰当的训练。上述过程便是Stacking融合的基本思想。

+

这里需要注意两点:第一,构建的新模型一般是简单模型,比如逻辑回归这样的线性模型;第二,使用多个模型进行Stacking融合会有比较好的结果。

+

Stacking融合使用基模型的预测结果作为第二层模型的输入。然而,我们不能简单地使用完整的训练集数据来训练基模型,这会产生基分类器在预测时就已经“看到”测试集的风险,因此在提供预测结果时出现过度拟合问题。所以我们应该使用Out-of-Fold的方式进行预测,也就是通过K折交叉验证的方式来预测结果。这里我们将Stacking融合分为训练阶段和测试阶段两部分,将并以流程图的形式展示每部分的具体操作。如图6.2所示为训练阶段。

+

特征加权的线性堆叠,可参考相应论文“Feature-Weighted Linear Stacking two layer stacking",其实就是对传统的Stacking融合方法在深度上进行扩展。通过传统的Stacking融合方法得到概率值,再将此值与基础特征集进行拼接,重新组成新的特征集,进行新一轮训练。

+

Blending 融合

+

不同于Stacking融合使用K折交叉验证方式得到预测结果,Blending融合是建立一个Holdout集,将不相交的数据集用于不同层的训练,这样可以在很大程度上降低过拟合的风险。

+

假设构造两层Blending,训练集等分为两部分(train_one和train_two),测试集为test。第一层用train_one训练多个模型,将train_two和test的预测结果合并到原始特征集合中,作为第二层的特征集。第二层用train_two的特征集和标签训练新的模型,然后对test预测得到最终的融合结果。

+

实战案例

+

以stacking为例。选择ExtraTreesRegressor、RandomForestRegressor、Ridge、Lasso作为基学习器,Ridge为最终分类器。

+

依然采用5折交叉验证

+
kf = KFold(n_splits=5, shuffle=True, random_state=2020)
+

然后构建一个sklearn中模型的功能类,初始化参数然后训练和预测。这段代码可复用性很高,建议完善、储存。

+
class SklearnWrapper(object):
+    def __init__(self, clf, seed=0, params=None):
+        params['random_state'] = seed
+        self.clf = clf(**params)
+
+    def train(self, x_train, y_train):
+        self.clf.fit(x_train, y_train)
+
+    def predict(self, x):
+        return self.clf.predict(x)
+

封装交叉验证函数。可复用性也很高。

+
def get_oof(clf):
+    oof_train = np.zeros((x_train.shape[0],))
+    oof_test = np.zeros((x_test.shape[0],))
+    oof_test_skf = np.empty((5, x_test.shape[0]))
+  
+    for i, (train_index, valid_index) in enumerate(kf.split(x_train, y_train)):
+        trn_x, trn_y, val_x, val_y = x_train.iloc[train_index], y_train[train_index],\
+            x_train.iloc[valid_index], y_train[valid_index]
+        clf.train(trn_x, trn_y)
+
+        oof_train[valid_index] = clf.predict(val_x)
+        oof_test_skf[i, :] = clf.predict(x_test)
+
+    oof_test[:] = oof_test_skf.mean(axis=0)
+    return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)
+

预测四个模型的验证集结果和测试集结果。并辅助最后一步的stacking融合操作:

+
et_params = {
+   'n_estimators': 100,
+    'max_features': 0.5,
+    'max_depth': 12,
+    'min_samples_leaf': 2,
+}
+rf_params = {
+    'n_estimators': 100,
+    'max_features': 0.2,
+    'max_depth': 12,
+    'min_samples_leaf': 2,
+}
+rd_params={'alpha': 10}
+ls_params={ 'alpha': 0.005}
+et = SklearnWrapper(clf=ExtraTreesRegressor, seed=2020, params=et_params)
+rf = SklearnWrapper(clf=RandomForestRegressor, seed=2020, params=rf_params)
+rd = SklearnWrapper(clf=Ridge, seed=2020, params=rd_params)
+ls = SklearnWrapper(clf=Lasso, seed=2020, params=ls_params)
+
+et_oof_train, et_oof_test = get_oof(et)
+rf_oof_train, rf_oof_test = get_oof(rf)
+rd_oof_train, rd_oof_test = get_oof(rd)
+ls_oof_train, ls_oof_test = get_oof(ls)
+

最后就是stacking部分,使用ridge模型。

+
def stack_model(oof_1, oof_2, oof_3, oof_4, predictions_1, predictions_2, predictions_3, predictions_4, y):
+    train_stack = np.hstack([oof_1, oof_2, oof_3, oof_4])
+    test_stack = np.hstack([predictions_1, predictions_2, predictions_3, predictions_4])
+  
+    oof = np.zeros((train_stack.shape[0],))
+    predictions = np.zeros((test_stack.shape[0],))
+    scores = []
+
+    for fold_, (trn_idx, val_idx) in enumerate(kf.split(train_stack, y)): 
+        trn_data, trn_y = train_stack[trn_idx], y[trn_idx]
+        val_data, val_y = train_stack[val_idx], y[val_idx]
+      
+        clf = Ridge(random_state=2020)
+        clf.fit(trn_data, trn_y)
+
+        oof[val_idx] = clf.predict(val_data)
+        predictions += clf.predict(test_stack) / 5
+      
+        score_single = sqrt(mean_squared_error(val_y, oof[val_idx]))
+        scores.append(score_single)
+        print(f'{fold_+1}/{5}', score_single)
+    print('mean: ',np.mean(scores))
+   
+    return oof, predictions
+
+oof_stack , predictions_stack  = stack_model(et_oof_train, rf_oof_train, rd_oof_train, ls_oof_train, et_oof_test, rf_oof_test, rd_oof_test,ls_oof_test, y_train)
+

实际运行后发现,基分类器的分类效果差别很大,且最终融合后的模型效果确实要比基分类器的模型效果好很多。

+ + + +
+ +
+
+ + + + + + +
+
+
机器学习算法竞赛实战-基础篇
+
https://zhangzhao219.github.io/2022/09/19/UCAS/machine-learning/machine-learning-competition-basic/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月19日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/20/UCAS/machine-learning/machine-learning-competition-people/index.html b/2022/09/20/UCAS/machine-learning/machine-learning-competition-people/index.html new file mode 100644 index 000000000..2ec1c1b83 --- /dev/null +++ b/2022/09/20/UCAS/machine-learning/machine-learning-competition-people/index.html @@ -0,0 +1,860 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 机器学习算法竞赛实战-用户画像 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

机器学习算法竞赛实战-用户画像

+ + +
+ +

机器学习算法竞赛实战-用户画像

+ +

第7章 用户画像

+

参考资料:《机器学习算法竞赛实战》整理 | 七、用户画像

+

用户:产品的使用者

+

数据收集方为了推广产品同时持续维护和改善用户体验需要对由用户操作而产生的数据进行挖掘,以期从中发现群体乃至个体的行为偏好,形成数据层面上的所谓画像。

+

用户画像

+

用于商业分析和数据挖掘的用户画像。基于给定的数据对用户属性及行为进行描述,然后提取用户的个性化指标,再以此分析可能存在的群体共性,并落地应用到各种业务场景中。

+

标签系统

+

核心就是给用户打标签,用来分析社会属性、社会习惯、生活习惯、消费行为。

+

标签分类方式

+

通过分析一个用户的特征来展示标签分类方式:

+

xiDaxH.md.png

+

多渠道获取标签

+

标签获取方式也可以看作特征获取方式

+

事实类:直接来自原始数据,比如性别、年龄、会员等级。也可以进行简单统计,比如用户行为次数、消费总额。

+

规则类:由运营人员和数据人员经过共同协商设定。例如,地域属性、家庭类型、年龄层等。所用技术知识:数理统计类,如基础统计、数值分层、概率分布、均值分析、方差分析等。

+

模型类:经过机器学习和深度学习等模型处理后,二次加工生成的洞察性标签。比如预测用户状态、预测用户信用分、划分兴趣人群和对评论文本进行分类。特点:综合程度高、复杂,依托数学建模,多种算法组合。

+

标签体系框架

+

xifcBF.md.png

+

用户画像数据特征

+

常见的数据形式

+
    +
  • 数值型变量
  • +
  • 类别型变量
  • +
  • 多值型变量:用户在某个维度具有多个取值的变量
  • +
  • 文本型变量:利用文本记录的变量。需要NLP知识,例如jieba中文分词工具
  • +
+

文本挖掘算法

+

LSA:非概率主题模型,与词向量有关,主要用于文档的话题分析。其核心思想是通过矩阵分解的方式发现文档和词之间基于话题的语义关系。

+

具体:将文档集表示为词-文档矩阵,对矩阵进行SVD(奇异值分解),从而得到话题向量以及文档在话题向量的表示。

+

举例:2020腾讯广告大赛,首先构造用户点击的广告素材id序列(creative_id),然后进行TF-IDF计算,最后经过SVD得到结果。

+

(代码与书中不同,未验证)

+
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
+# 稀疏特征降维 TruncatedSVD
+from sklearn.decomposition import TruncatedSVD
+from sklearn.pipeline import Pipeline
+# 提取用户点击序列
+docs = data_df.groupby(['user_id'])['creative_id'].agg(lambda x:"".join(x)).reset_index()['creative_id']
+# tf-idf
+tfd = TfidfVectorizer()
+svd = TruncatedSVD(n_components=100, n_iter=50, random_state=2020)
+

PLSA:PLSA(概率潜在语义分析)模型是为了克服LSA模型潜在存在的一些缺点而提出的。通过一个生成模型来为LSA赋予概率意义上的解释。该模型假设每一篇文档都包含一系列可能的潜在话题,文档中的每一个词都不是凭空产生的,而是在潜在话题的指引下通过一定的概率生成的。

+

LDA:LDA(潜在狄利克雷分布)是一种概率主题模型,与词向量无关,可以将文档集中的每篇文档的主题以概率分布的形式给出。通过分析一批文档集,抽取出他们的主题分布,就可以根据主题进行聚类或分类。同时,它是一种典型的词袋模型,即一篇文档由一组相互独立的词构成,词和词之间没有先后顺序。

+

神奇的嵌入表示

+

word2Vec:可调用gensim包,参数:窗口大小、模型类型选择、生成词向量长度

+

对于Skip-Gram和CBOW:

+
    +
  • CBOW在训练时比Skip-Gram快
  • +
  • CBOW可以更好地表示常见单词
  • +
  • Skip-Gram在少量的训练集中可以表示稀有单词或短语
  • +
+

DeepWalk

+

对于Word2Vec的衍生Item2Vec以及更多图嵌入方法,比如LINE、Node2Vec和SDNE

+

相似度计算方法

+
    +
  • 欧式距离
  • +
  • 余弦相似度
  • +
  • Jaccard相似度
  • +
+

用户画像的应用

+

用户分析

+
    +
  1. 京东JDATA平台2019年的“用户对品类下店铺的购买预测”
  2. +
  3. 腾讯广告“2020腾讯广告大赛”
  4. +
+

精准营销

+
    +
  1. 2018科大讯飞AI营销算法大赛
  2. +
  3. 2018腾讯广告算法大赛
  4. +
+

风控领域

+
    +
  • DF竞赛平台的“消费者人群画像-信用智能评分”
  • +
  • 拍拍贷“第四届魔镜杯大赛”
  • +
+

特点:

+
    +
  • 业务对模型解释性比较高,对时效性有一定要求,需要权衡模型复杂度和精度,并且适当优化算法内核
  • +
  • 业务模型多样,需要紧密结合业务
  • +
  • 负样本极少,均衡学习算法
  • +
+

第8章 实战案例

+

参考资料:《机器学习算法竞赛实战》整理 | 八、实战案例:Elo Merchant Category Recommendation

+

赛题理解

+

Imagine being hungry in an unfamiliar part of town and getting restaurant recommendations served up, based on your personal preferences, at just the right moment. The recommendation comes with an attached discount from your credit card provider for a local place around the corner!

+

Right now, Elo, one of the largest payment brands in Brazil, has built partnerships with merchants in order to offer promotions or discounts to cardholders. But do these promotions work for either the consumer or the merchant? Do customers enjoy their experience? Do merchants see repeat business? Personalization is key.

+

Elo has built machine learning models to understand the most important aspects and preferences in their customers’ lifecycle, from food to shopping. But so far none of them is specifically tailored for an individual or profile. This is where you come in.

+

In this competition, Kagglers will develop algorithms to identify and serve the most relevant opportunities to individuals, by uncovering signal in customer loyalty. Your input will improve customers’ lives and help Elo reduce unwanted campaigns, to create the right experience for customers.

+

赛题数据

+
    +
  • train.csv 训练数据集,包括 first_active_month,card_id,feature_1,feature_2,feature_3,target字段
  • +
  • test.csv 测试数据集,包括 first_active_month,card_id,feature_1,feature_2,feature_3字段
  • +
+

first_active_month表示的是信用卡产生第一笔交易的时间,feature是信用卡类型的脱敏特征。最后一列 target是要预测的数值

+

historical_transactions.csv 信用卡在给定商家的历史交易记录,文件比较大,基本都是一些脱敏的特征

+

merchants.csv所有商家的附加信息

+

new_merchant_transactions.csv two months’ worth of data for each card_id containing ALL purchases that card_id made at merchant_ids that were not visited in the historical data .(每张信用卡在新商家的购物数据)

+

评价指标使用RMSE

+ + +
+ +
+
+ + + + + + +
+
+
机器学习算法竞赛实战-用户画像
+
https://zhangzhao219.github.io/2022/09/20/UCAS/machine-learning/machine-learning-competition-people/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月20日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/21/UCAS/information-retrieval/information-retrieval-8/index.html b/2022/09/21/UCAS/information-retrieval/information-retrieval-8/index.html new file mode 100644 index 000000000..f3babf343 --- /dev/null +++ b/2022/09/21/UCAS/information-retrieval/information-retrieval-8/index.html @@ -0,0 +1,866 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第8讲 检索评价 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第8讲 检索评价

+ + +
+ +

《现代信息检索》课程笔记:第8讲 检索评价

+ +

第8讲 检索评价

+

检索评价

+
    +
  • 通过评估可以评价不同技术的优劣,不同因素对系统的影响,从而促进本领域研究水平的不断提高
  • +
  • 信息检索系统的目标是较少消耗情况下尽快、全面返回准确的结果。
  • +
  • 计算机应用学科偏重于研究“更好的”方法/算法/模型,需要一种公平可靠的方法和指标体系进行评价
  • +
+

评价什么?

+
    +
  • 效率:时间开销、空间开销、响应速度
  • +
  • 效果: +
      +
    • 返回的文档中有多少相关文档
    • +
    • 所有相关文档中返回了多少
    • +
    • 返回得靠不靠前
    • +
    +
  • +
  • 其他指标:覆盖率、访问量、数据更新速度
  • +
+

使用相同的文档集合,相同的查询主题集合,相同的评价指标,对不同的检索系统进行比较。

+

评价指标:某个或某几个可衡量、可比较的值

+

评价过程:设计上保证公平、合理

+

IR中评价的难点:相关性(Relevance)是一个主观概念,文档相关性依赖于查询(数据标记工作量庞大)

+

评价指标

+
    +
  1. 对单个查询进行评估的指标:在单个查询上检索系统的得分
  2. +
+

召回率(Recall):返回的相关结果数占实际相关结果总数的比率

+

正确率(Precision):返回的结果中真正相关结果的比率

+

虽然Precision和Recall都很重要,但是不同的应用、不用的用户可能会对两者的要求不一样。

+
    +
  • 垃圾邮件过滤:宁愿漏掉一些垃圾邮件,但是尽量少将正常邮件判定成垃圾邮件。
  • +
  • 有些用户希望返回的结果全一点,他有时间挑选;有些用户希望返回结果准一点,他不需要结果很全就能完成任务。
  • +
+

问题①:召回率难以计算:

+

对于大规模语料集合,列举每个查询的所有相关文档是不可能的事情,因此,这种情况几乎不可能准确地计算召回率可以采用Pooling方法,或者不考虑召回

+

缓冲池(Pooling)方法:对多个检索系统的Top k个结果组成的集合(并集)进行人工标注,标注出的相关文档集合作为整个相关文档集合。这种做法被验证是可行的(可以比较不同系统的相对效果),在TREC会议中被广泛采用。

+

问题②:两个指标需要融成一个指标,或者只采用单一指标

+

F值(F-measure):召回率R和正确率P的调和平均值

+

Fβ:表示召回率的重要程度是正确率的β(>=0)倍,β>1更重视召回率, β<1更重视正确率

+

E(Effectiveness)值:召回率R和正确率P的加权平均值,b>1表示更重视P

+

精确率是所有判定中正确的比率,一般不使用这一评价指标

+
    +
  • 由于和查询相关毕竟占文档集的极少数,所以即使什么都不返回,可能对大部分查询来说可以得到 99.99%以上的精确率
  • +
  • 信息检索用户希望找到某些文档并且能够容忍结果中有一定的不相关性,返回一些即使不好的文档也比不返回任何文档强
  • +
+

问题③:两个指标都是基于(无序)集合进行计算,并没有考虑(排)序的作用

+

R-Precision:检索结果中,在所有相关文档总数位置上的准确率,如某个查询的相关文档总数为80,则计算检索结果中在前80篇文档的正确率。

+

正确率-召回率 曲线:检索结果以排序方式排列,用户不可能马上看到全部文档,因此,在用户观察的过程中,正确率和召回率在不断变化。

+

在上面的曲线对应的系统结果更好,也就是线下的面积(AUC)

+

P-R 曲线的插值问题:利用存在的召回率点对不存在的召回率点进行插值

+

优点:

+
    +
  • 简单直观
  • +
  • 既考虑了检索结果的覆盖度,又考虑了检索结果的排序情况
  • +
+

缺点:单个查询的P-R曲线虽然直观,但是难以明确表示两个查询的检索结果的优劣

+

基于P-R曲线的单一指标:P-R曲线上P=R的那个点(Break Point)

+

平均正确率(Average Precision, AP):对不同召回率点上的正确率进行平均

+

不考虑召回率的指标:

+

Precision@N:在第N个位置上的正确率,对于搜索引擎,大量统计数据表明,大部分搜索引擎用户只关注前一、两页的结果,

+
    +
  1. 对多个查询进行评估的指标:在多个查询上检索系统的得分
  2. +
+

平均的求法:

+
    +
  • 宏平均(Macro Average): 对每个查询求出某个指标,然后对这些指标进行算术平均
  • +
  • 微平均(Micro Average): 将所有查询视为一个查询,将各种情况的文档总数求和,然后进行指标的计算
  • +
  • 宏平均对所有查询一视同仁,微平均受返回相关文档数目比较大的查询影响
  • +
+

MAP(Mean AP):对所有查询的AP求宏平均

+

整个IR系统的P-R曲线:

+

在每个召回率点上,对所有的查询在此点上的正确率进行算术平均,得到系统在该点上的正确率的平均值。

+

两个检索系统可以通过P-R曲线进行比较。位置在上面的曲线代表的系统性能占优。

+

MRR(Mean Reciprocal Rank): 对于某些IR系统(如问答系统或主页发现系统),只关心第一个标准答案返回的位置(Rank),越前越好,这个位置的倒数称为RR,对问题集合求平均,则得到MRR

+

Bpref:在相关性判断不完全的情况下,计算在进行了相关性判断的文档集合中,在判断到相关文档前,需要判断的不相关文档的篇数。

+

相关性判断完全的情况下,利用Bpref和MAP进行评价的结果很一致,但是相关性判断不完全的情况下,Bpref更鲁棒

+

GMAP:几何平均值

+

NDCG:对于返回结果,相关度级别越高的结果越多越好,相关度级别越高的结果越靠前越好。

+

优点:

+
    +
  • 图形直观,易解释
  • +
  • 支持非二值的相关度定义,比P-R曲线更精确
  • +
  • 能够反映用户的行为特征(如:用户的持续性)
  • +
+

缺点:

+
    +
  • 相关度的定义难以一致
  • +
  • 需要参数设定
  • +
+

现有评价体系远没有达到完美程度

+
    +
  • 对评价的评价研究
  • +
  • 指标的相关属性(公正性、敏感性)的研究
  • +
  • 新的指标的提出(新特点、新领域)
  • +
  • 指标的计算(比如Pooling方法中如何降低人工代价?查询集或文档集合发生变化怎么办?)
  • +
+

相关评测

+

TREC

+

总目标:支持在信息检索领域的基础研究,提供对大规模文本检索方法的评估办法

+
    +
  1. 鼓励对基于大测试集合的信息检索方法的研究
  2. +
  3. 提供一个可以用来交流研究思想的论坛,增进工业界、学术界和政府部门之间的互相了解;
  4. +
  5. 示范信息检索理论在解决实际问题方面的重大进步,提高信息检索技术从理论走向商业应用的速度;
  6. +
  7. 为工业界和学术界提高评估技术的可用性,并开发新的更为适用的评估技术。
  8. +
+

实验设计

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第8讲 检索评价
+
https://zhangzhao219.github.io/2022/09/21/UCAS/information-retrieval/information-retrieval-8/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月21日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/22/UCAS/advanced-ai/advanced-ai-4/index.html b/2022/09/22/UCAS/advanced-ai/advanced-ai-4/index.html new file mode 100644 index 000000000..f934c83e2 --- /dev/null +++ b/2022/09/22/UCAS/advanced-ai/advanced-ai-4/index.html @@ -0,0 +1,867 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第4讲 图像数据的深度学习模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第4讲 图像数据的深度学习模型

+ + +
+ +

《高级人工智能》课程笔记:第4讲 图像数据的深度学习模型

+ +

第4讲 图像数据的深度学习模型

+

卷积神经网络

+

计算机视觉需要应用大量的图像数据

+

卷积神经网络是一种特殊的深层神经网络模型

+
    +
  • 它的神经元间的连接是非全连接的
  • +
  • 同一层中某些神经元之间的连接的权重是共享的(即相同的)。
  • +
+

20世纪60年代,Hubel和Wiesel研究猫脑皮层

+
    +
  • 用于局部敏感和方向选择的神经元,其独特的网络结构可以有效地降低反馈神经网络的复杂性
  • +
  • 基于猫的初级视皮层(VI区)的研究:简单细胞和复杂细胞
  • +
  • 两层神经网络模拟初级视皮层中的简单细胞和复杂细胞 +
      +
    • 每层的神经元被组织成二维平面
    • +
    • “简单细胞”层提取其输入中的局部特征
    • +
    • “复杂细胞”层组合“简单细胞”层中相应的子区域,使得整个网络对局部变换具有一定的不变性。
    • +
    +
  • +
+

局部连接

+

局部感知野:图像的空间联系也是局部的像素联系较为紧密,而距离较远的像素相关性则较弱,减少了需要训练的权值数目

+

参数共享:图像的一部分的统计特性与其他部分是一样的。在输入的不同位置检测同一种特征具有平移不变性

+

一维、二维、三维卷积

+

其中三维卷积:假设输入数据的大小为a1×a2×a3,过滤器大小为f,即过滤器维度为f×f×f。三维卷积最终的输出为(a1−f+1)×(a2−f+1)×(a3−f+1)。

+

多卷积核:

+
    +
  • 每个卷积核都会将图像生成为另一幅图像。
  • +
  • 两个卷积核就可以生成两幅图像,这两幅图像可以看做是一张图像的不同的通道。
  • +
+

边缘检测示例:卷积运算是输入图像与过滤器(也叫核)进行的运算,得到输出图像。卷积核与图像对应的位置相乘求和得到一个新值。

+

假定要识别图像中的特定曲线,也就是说,对这种曲线有很高的输出,对其他形状则输出很低,这也就像是神经元的激活。

+

Padding:边缘不填充

+
    +
  • 随着不断卷积,图像会变得越来越小,有时你可不想让它变小
  • +
  • 最角落的点只被使用了一次,这意味着在下传的过程中丢掉了图像边缘位置的信息。
  • +
+

卷积步长:卷积中的步幅是另一个构建卷积神经网络的基本操作

+

输入与输出的尺寸关系:

+

单层卷积网络:每一个卷积核的输出对应一个实数b(偏差),然后在进行激活函数的非线性转换得到输出

+

Pooling池化:

+

通过卷积获得了特征之后,下一步利用这些特征去做分类。

+
    +
  • 使用卷积时是利用了图像的“静态”特征
  • +
  • Pooling对不同位置的特征进行聚合统计
  • +
+

池化层中没有需要学习的参数,所以通常不把池化层当做独立的一层来看。

+

池化层是一般不会设置padding,即一般padding为0。

+

fitter为2,stride为2是最常见的参数设置,尺寸图像缩小为原来的一半。

+

卷积时用的尺寸计算公式同样适用于池化层。

+

CNN

+

CNN基本结构:卷积层和子采样层

+

卷积神经网络是一个多层的神经网络

+
    +
  • 每层由多个二维平面组成
  • +
  • 每个平面由多个独立神经元组成
  • +
+

CNN训练过程

+

监督训练:Bp算法

+

向前传播

+
    +
  • 从样本集中取一个样本,将输入网络
  • +
  • 计算相应的实际输出
  • +
+

反向传播

+
    +
  • 计算实际输出与相应的理想输出的差
  • +
  • 按极小化误差的方法反向传播调整权矩阵
  • +
  • 代价函数 +
      +
    • 最小化平方误差(MSE),最小化相对熵(Relative Entropy)
    • +
    +
  • +
  • 反向传播主要考虑三个方面: +
      +
    • 输出层,代价函数的确定及求导
    • +
    • Pooling,数据的下采样及残差的上采样
    • +
    • 卷积层,数据的卷积运算及残差的反卷积运算
    • +
    +
  • +
+

卷积网络的核心思想:将局部感受野、权值共享以及时间或空间亚采样这三种结构思想结合起来获得了某种程度的位移、尺度、形变不变性。

+

层间联系和空域信息的紧密关系,使其适于图像处理和理解:图像和网络的拓扑结构能很好的吻合

+

避免了显式的特征抽取,而隐式地从训练数据中进行学习:特征提取和模式分类同时进行,并同时在训练中产生;权重共享可以减少网络的训练参数,使神经网络结构变得更简单,适应性更强。

+

CNN的改进:

+

Rectified linear function:加速收敛和稀疏化

+

dropout:将隐层节点以一定概率清0

+

局部对比归一

+

非线性变换、池化

+

残差网络(Residual Networks(ResNets))

+
    +
  • 因为残差网络很容易学习恒等式函数,所以随着网络加深,至少不会让网络变差。
  • +
  • 学习结果对网络权重的波动变化更敏感
  • +
+

图像数据应用

+
    +
  • 目标定位
  • +
  • 特征点检测
  • +
  • 目标检测
  • +
  • 人脸识别
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第4讲 图像数据的深度学习模型
+
https://zhangzhao219.github.io/2022/09/22/UCAS/advanced-ai/advanced-ai-4/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月22日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/22/UCAS/machine-learning/machine-learning-5/index.html b/2022/09/22/UCAS/machine-learning/machine-learning-5/index.html new file mode 100644 index 000000000..470d819a4 --- /dev/null +++ b/2022/09/22/UCAS/machine-learning/machine-learning-5/index.html @@ -0,0 +1,818 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第5章 回归分析 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第5章 回归分析

+ + +
+ +

《机器学习》课程笔记:第5章 回归分析

+ +

第5章 回归分析

+

概述

+

回归问题:

+

根据给定的训练集,其中(预测的结果是连续函数值)

+

要求寻找上的决策函数

+

性能评价:

+

均方误差:

+

泛化误差可分解为偏差、方差和噪声之和

+

线性回归原理:使用线性函数来预测数据的分布

+

最小二乘估计

+

目标函数:最小误差平方和

+

+

求解:

+

最大似然估计

+

正态分布假设的似然函数

+

误差服从正态分布:

+

似然函数:,可以转换为对数的形式

+

高斯误差的最大似然估计=最小二乘估计

+

优化学习:梯度下降方法

+

最大后验估计

+

正态分布的先验似然函数:

+

最大后验估计目标函数:

+

高斯分布的最大后验估计 = 正则化最小二乘估计

+

正则化最小二乘估计解:

+

正则项解决过拟合问题

+

扩展的非线性模型

+

线性基函数回归

+

线性回归:

+

扩展的非线性回归:

+

基函数形式:多项式函数、高斯分布函数、sigmoid类型的函数、tanh类型的函数

+

多项式回归:

+

误差分析

+

正则项对Bias和Variance的影响

+

参数估计

+

最小二乘估计是无偏估计

+

正则化最小二乘估计是有偏估计

+

使得参数估计更加稳定

+

相当于增加正则项

+

相当于加入白噪声

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第5章 回归分析
+
https://zhangzhao219.github.io/2022/09/22/UCAS/machine-learning/machine-learning-5/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月22日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/24/UCAS/information-retrieval/information-retrieval-9/index.html b/2022/09/24/UCAS/information-retrieval/information-retrieval-9/index.html new file mode 100644 index 000000000..a601ed3bf --- /dev/null +++ b/2022/09/24/UCAS/information-retrieval/information-retrieval-9/index.html @@ -0,0 +1,851 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第9讲 完整搜索系统中的评分计算 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第9讲 完整搜索系统中的评分计算

+ + +
+ +

《现代信息检索》课程笔记:第9讲 完整搜索系统中的评分计算

+ +

第9讲 完整搜索系统中的评分计算

+

不排序的问题严重性

+
    +
  • 用户只希望看到一些而不是成千上万的结果
  • +
  • 很难构造只产生一些结果的查询
  • +
  • 即使是专家也很难
  • +
  • 排序能够将成千上万条结果缩减至几条结果,因此非常重要
  • +
+

排序的重要性:

+
    +
  • 摘要阅读(Viewing abstracts):用户更可能阅读第一页的结果的摘要
  • +
  • 点击(Clicking):点击的分布甚至更有偏向性 +
      +
    • 一半情况下,用户点击排名最高的页面
    • +
    • 即使排名最高的页面不如排名第二的页面相关,仍然有接近30%的用户会点击它。
    • +
    +
  • +
  • 正确排序相当重要
  • +
  • 排对最高的页面非常重要
  • +
+

结果排序的实现

+

倒排索引中的词项频率存储

+
    +
  • 每条倒排记录中,除了docIDd 还要存储tft,d
  • +
  • 通常存储的是原始的整数词频,而不是对数词频对应的实数值 +
      +
    • 这是因为实数值不易压缩
    • +
    +
  • +
  • 对tf采用一元码编码效率很高
  • +
  • 总体而言,额外存储tf所需要的开销不是很大:采用位编码压缩方式,每条倒排记录增加不到一个字节的存储量
  • +
  • 或者在可变字节码方式下每条倒排记录额外需要一个字节即可
  • +
+

两种常见的评分累加算法:

+

以词项为单位(term-at-a-time, TAAT),首先获得词项t的posting list,然后累加得分

+

以文档为单位的计算,首先获得包含查询词的所有文档,将这些文档按照静态评分排序,然后依次累加得分

+

精确top K检索及其加速办法:

+

目标:从文档集的所有文档中找出K个离查询最近的文档

+

步骤:对每个文档评分(余弦相似度),按照评分高低排序,选出前K个结果

+

加速方法:

+

快速计算余弦:不考虑查询词项的权重

+

堆法N中选K:不对所有文档进行排序,只需要挑出最高的K个结果

+

提前终止计算:得到了top K结果,不需要再进行后续计算

+

精确topK检索的问题:仍然无法避免大量文档参与计算

+

非精确topK检索:非精确topK的结果如果和精确topK的结果相似度相差不大,应该也能让用户满意

+

找一个文档集合A,K<|A|<<N,利用A中的top K结果代替整个文档集的top K结果

+

方法一:索引去除

+

从查询词的角度:只考虑那些包含高idf查询词项的文档

+

从文档的角度:只考虑那些包含多个查询词项的文档

+

仅考虑高idf词项、仅考虑包含多个词项的文档

+

方法二:胜者表

+

对每个词项t,预先计算出其倒排记录表中权重最高的r篇文档,如果采用tfidf机制,即tf最高的r篇

+

方法三:静态质量得分排序方式

+

为每篇文档赋予一个与查询无关的[0,1]之间的值,记为g(d),例如Pagerank

+

最终文档排名基于g(d)和相关度的线性组合

+

目标是找net-score最高的top K文档

+

方法四:影响度(Impact)排序

+

提前结束法:

+

遍历倒排记录表时,可以在如下情况之一发生时停止:

+
    +
  • 遍历了固定的文档数目r
  • +
  • wft,d 低于某个预定的阈值
  • +
  • 将每个词项的结果集合合并
  • +
  • 仅计算合并集合中文档的得分
  • +
+

将词项按照idf排序:

+
    +
  • 对于多词项组成的查询,按照idf从大到小扫描词项
  • +
  • 在此过程中,会不断更新文档的得分(即本词项的贡献),如果文档得分基本不变的话,停止
  • +
  • 可以应用于余弦相似度或者其他组合得分
  • +
+

方法五: 簇剪枝

+

随机选 篇文档作为先导者,对于其他文档,计算和它最近的先导者

+

非docID的倒排记录表排序方法

+

与查询无关的一种反映结果好坏程度的指标

+

以文档为单位(Document-at-a-time)的处理、以词项为单位(Term-at-a-time)的处理方式

+

WAND(Weak AND) 评分算法

+
    +
  • 实验表明, WAND 可以降低 90% 以上的评分计算开支
  • +
  • WAND并非仅仅适用于cosine评分排序
  • +
  • WAND 及其不同的改进版能够满足安全排序(Safe Ranking, 即精确排序)
  • +
+

完整的搜索系统

+

多层次索引基本思路:

+
    +
  • 建立多层索引,每层对应索引词项的重要性
  • +
  • 查询处理过程中,从最高层索引开始
  • +
  • 如果最高层索引已经返回至少k (比如, k = 100)个结果,那么停止处理并将结果返回给用户
  • +
  • 如果结果 < k 篇文档,那么从下一层继续处理,直至索引用完或者返回至少k 个结果为止
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第9讲 完整搜索系统中的评分计算
+
https://zhangzhao219.github.io/2022/09/24/UCAS/information-retrieval/information-retrieval-9/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月24日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/24/UCAS/machine-learning/machine-learning-competition-time/index.html b/2022/09/24/UCAS/machine-learning/machine-learning-competition-time/index.html new file mode 100644 index 000000000..2629f097d --- /dev/null +++ b/2022/09/24/UCAS/machine-learning/machine-learning-competition-time/index.html @@ -0,0 +1,799 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 机器学习算法竞赛实战-时间序列 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

机器学习算法竞赛实战-时间序列

+ + +
+ +

机器学习算法竞赛实战-时间序列

+ +

第9章 时间序列

+

什么是时间序列

+

时间序列是按时间顺序索引(或列出或图示)的一系列数据点。组成时间序列的数据由相对确定的时间戳组成。

+

对时间序列的分析基于以下假设:数据文件中标签的数据值表示以等间隔时间进行的连续测量值。假设数据存在相关性,然后通过建模找到对应的相关性,并利用它预测未来的数据走向。

+

可以从变量角度将这些问题归纳为单变量时间序列和多变量时间序列

+

可以从预测目标角度将这些问题归纳为单步预测和多步预测

+

单变量时间序列仅具有单个时间相关变量,所以仅受时间因素的影响。这类问题重点在于分析数据的变化特点,受相关性、趋势性、周期性和循环性等因素的影响。

+

多变量时间序列具有多个时间相关变量,除了受时间因素的影响,还受其他变量的影响。需要考虑更多的因素,挑战也更大。

+

单步预测问题比较基础,仅在训练集的时间基础上添加一个时间单位便可以作为测试集

+

多步预测问题比较复杂,是在训练集的时间基础上添加多个时间单位作为测试集

+

交叉验证的时候为了保留时间相关性,需要采用滚动交叉验证的方式:

+
    +
  • 首先使用初始时间到t时刻的数据来训练模型
  • +
  • 然后用从t到t+n时刻的数据进行线下验证,并计算评价指标的分数
  • +
  • 将训练样本扩展到t+n时刻,用从t+n到t+2n时刻的数据进行验证
  • +
  • 不断重复,直到达到最后一个可用的标签值
  • +
+

基本规则方法

+

加权平均:离当前时间点越近的数据的重要性越高

+

指数平滑:将每个时间单位的权重按照指数级进行衰减(指数平滑像是拥有无限记忆且权值呈指数级递减的移动平均法)

+

时间序列模式

+

趋势性:在很长一段时间内呈现的数据持续上升或持续下降的变动

+

周期性:在一段时间序列内重复出现的波动,是各种因素综合影响的结果。

+

相关性:在某一段序列往往存在正相关或负相关,前后时间点会有很大的关联

+

随机性:除了上述三种模式外的随机扰动

+

特征提取方式

+

历史平移:直接将历史记录作为特征

+

窗口统计:从多个序列单位中提取特征

+

序列熵特征:描述序列的确定性和不确定性

+

还有时间特征与统计特征

+

模型的多样性

+

传统的时序模型:ARIMA(差分自回归滑动平均模型)

+

树模型:对时间序列进行平稳性调整

+

深度学习模型

+
    +
  • 卷积神经网络
  • +
  • 长短期记忆网络
  • +
+

第10章 实战案例

+

第11章 实战案例

+ + +
+ +
+
+ + + + + + +
+
+
机器学习算法竞赛实战-时间序列
+
https://zhangzhao219.github.io/2022/09/24/UCAS/machine-learning/machine-learning-competition-time/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月24日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/25/UCAS/machine-learning/machine-learning-6/index.html b/2022/09/25/UCAS/machine-learning/machine-learning-6/index.html new file mode 100644 index 000000000..8326164ea --- /dev/null +++ b/2022/09/25/UCAS/machine-learning/machine-learning-6/index.html @@ -0,0 +1,877 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第6章 聚类分析 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第6章 聚类分析

+ + +
+ +

《机器学习》课程笔记:第6章 聚类分析

+ +

第6章 聚类分析

+

概述

+

聚类是无监督机器学习问题

+
    +
  • 目标:感知样本间的相似度,进行类别归纳
  • +
  • 聚类研究的重要应用:1. 潜在类别预测,2. 数据压缩
  • +
  • 既可以作为一个单独过程,用于寻找数据内在的分布结构,也可以作为分类、稀疏表示等其他学习任务的前驱过程。
  • +
+

影响聚类结果的因素:

+
    +
  1. 属性选择导致不同结果
  2. +
  3. 相似性度量是判断样本间、类别间的相似的标准
  4. +
  5. 聚类规则是样本聚集条件,例如,近邻、损失函数
  6. +
+

相似性度量

+

样本-样本:(向量相似性)

+

+

+

+

+

样本-集合:

+
    +
  1. 集合为离散点集
  2. +
+

到集合最远点距离:

+

到集合最近点距离:

+

到集合平均点距离:

+
    +
  1. 集合为连续区域
  2. +
+

集合为平面:

+

集合为圆:

+

集合-集合:(类间距离)

+

集合间最远点距离:

+

集合间最近点距离:

+

集合间所有点平均距离:

+

集合表征点间距离(如平均值):

+

集合内样本间距离(类内距离):

+

性能度量

+

聚类性能的外部指标指通过已知类簇划分,对聚类结果进行评价;判别同类别样本对标签一致与否,避免相同类簇划分,不同标签名称导致的不一致。

+

Jaccard系数、FM系数和Rand系数

+

聚类性能的内部指标:没有已知的类簇划分进行参考,通过聚类具有的类内相似和类间相异的特点进行评价。

+

DB指数:,越小越好

+

Dunn指数:,越大越好

+

序贯方法

+

基本思想:逐一比较单个样本与类簇的相似性,有相似类则归类,无相似类则建立新类。

+

优点:一种简单的,快速算法

+

相似性的关键度量:类别相似性:样本—类簇(样本—集合)。

+

缺点:所有样本过滤一遍后才知道类别总数,而先出现的样本不能找到(后出现的)合适类别

+

改进算法:采用两个阶段,类别确定、分类。

+

两阶段序贯方法:

+

缺点:以上两种方法依赖于阈值

+

改进方法:弱化阈值作用,采用两个阈值,形成灰色带。

+

双阈值序贯方法

+

三种算法缺点:

+
    +
  1. 当类别一旦产生,不可变,尽管后来类簇增加,类别很相近也无法合并。
  2. +
  3. 敏感于样本顺序,样本类别未必是最合适的。
  4. +
+

增强算法

+

增强处理1:对类别集合进行合并操作

+

增强处理2:对样本类别重置

+

层次聚类

+

基本思想:

+

聚类嵌套定义:是样本集上的两种聚类划分,如果中所有的类簇都是中类簇的子集,则称嵌套在内,记作

+

层次聚类策略:类簇之间(依据相似性)不断合并、或不断的分化, 直到满足聚类停止条件。

+

自底向上/归并算法:

+

次迭代:计算所有两个类簇的相似性,归并最相似的两个类簇,更新类别划分

+

缺点:没有归并的类簇间相似性,被重复计算

+

基于矩阵的归并算法

+

利用矩阵记录类簇间的相似性

+
    +
  • 删除对应合并的两行和列
  • +
  • 增加一行和列: 新类簇与其他类簇的相似度
  • +
+

优点:不必重新计算“没有合并的类簇间”的相似性

+

分化算法:过程与归并相反

+

次迭代:在所有类簇的所有划分中,计算所有两个类簇相似性,选择最不相似的类簇集合划分,更新类别划分

+

缺点:没有划分的类簇间相似性,被重复计算

+

如何确定聚类个数?

+

K均值聚类

+

Kmeans:将样本分给最近的类心,然后重新调整类心;通过多次迭代,逐步进行类别划分。

+

最优准则:最小化误差平方和 是第个类簇的样本。

+

一般方法:最近类心原则,批量划分后修正类心

+

改进方法:单个划分最优原则,单个划分后修正类心

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第6章 聚类分析
+
https://zhangzhao219.github.io/2022/09/25/UCAS/machine-learning/machine-learning-6/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月25日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/25/diary/diary20220925/index.html b/2022/09/25/diary/diary20220925/index.html new file mode 100644 index 000000000..c81cb6170 --- /dev/null +++ b/2022/09/25/diary/diary20220925/index.html @@ -0,0 +1,766 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20220925 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20220925

+ + +
+ +

公开于2023年11月19日

+ +

四年相识、三年相恋、抵不过些许距离。

+

并没有表现得太过于悲伤,甚至都没有留下眼泪。可能是因为从日常的点点滴滴中已经知道这个结果了,最后的两三个月完全就是在硬撑着,我一厢情愿地在努力,但是她的心里早就已经有了答案。

+

相识的第一天,2018年9月24日,中秋节。两个人走进教室,拿出简历,面试。面试后一起下楼,简单的说了第一句打招呼的话语,分开。那是第一次见面,内心里有一种悸动,真的似乎有点喜欢。此时的我,刚刚经历了高考的失利,急于在这个看起来与我的能力并不匹配的学校中证明我自己。去竞选班干部,去参加各种学生组织,去认识更多的人,同时也不再压抑内心的感情,积极去找寻自己的爱情。当初对爱情只是懵懂,被拒绝了一次,拒绝了别人一次,有点怕了。有时候我也毫不掩饰我对她的喜欢,去车站接,送奶茶,约出来走走等等。就这样默默暗恋了一年。

+

第二年的中秋节,2019年9月13日,我终于鼓起勇气,约出来转到了湖大再转回来,说出了压在心底一年的话。这样就收获了我的初恋。当时的我,并不优秀,对未来一片迷茫,不知道四年大学毕业后要去到哪里。“我们在一无所有的情况下选择去尝试”,我同时也坚定了要共度一生的想法,想要给她今后一个更好的生活,于是我努力学习,从一个将将摸到保研边的中等生,逐渐变成了一个强者,拿下了很好的成绩排名,拿到了学校里面的绝大部分奖项,拿到了国家奖学金,成功保研。因为有了动力,一切都变得理所应当,再苦再累真的值得。

+

我们之间的感情没有那么多的激情燃烧,更多的是平淡。我尽量在她需要我的时候出现在她的身边,平时四周转一转,一起去图书馆学习,感冒了送她去医院,脚伤了每天接送,中午晚上点好饭送到身边。我很享受这种平淡的生活,因为我已经认准了她,什么东西都不能减少我对她的爱。我也认为她是和我一样性格的人,有自己的个性,有上进心进取心,不安于现状希望改变。就这样过了两年的美好时光,我们走入了大四的毕业季。

+

大四开始的我,松弛了下来,暂时与紧张的学习生活说了再见,开始无底线的放松。而她却要每天准备考研,还有两节课要上。而且由于搬校区的缘故,我见到她并不是很容易了。在这期间有了一些她不怎么讲话的迹象,甚至在我离开长沙和她吃的最后一顿饭上也是心不在焉。我把它归结为考研焦虑,并没有太过在意。也还是因为我对这段感情太有信心了吧,我相信时间距离都不是问题,我们只要努力把自己变得更好,总有一天会克服种种困难生活在一起衣食无忧。这也导致了大四下学期去实习的时候有点忽略了对她的关心,感觉是因为都忙,说的话也变少了。这种下了分手的种子。

+

6月正式本科毕业,2022年6月21日,突然的完全不理我,突然的提出分手,我直接崩溃掉。原来她并没有任何的信心,只是我自己自作多情罢了。原来这半年我基本不知道任何有关她的生活,我不知道她实习的工作怎么样,不知道她去面试了教师岗位,不知道她成功考上教师编制。我终于发现了这个问题,但是事实上已经晚了。虽然这一次分手我用回忆挽回,但是并没有打消她的念头,也并没有增加很多她对我的爱。而且由于距离,也阻隔了表达爱的方式。就好像“inception”一样,动了念头就很难再忘记掉了。

+

然后是短短四天的青岛旅行,差不多一年以来的首次见面。尤其是最后一天的晚上,最后一次吃饭基本上全程都在看手机。虽然是在修朋友圈的照片,但是我当然也是有一点点不高兴的。从上次几乎分手后我就十分在乎她的感受,但是我从来都没有勇气当面问出这些话语。这样过了两个月,我不断询问她的感受,不断讲给她我现在的想法。然而一切都是没有作用的。不爱了真的就不爱了。2022年9月24日,正式分手。我拼了命的想要挽回,我真的放不下,也不可能放得下三年的感情,换回来的仅仅是“不甜”、“不爱了”如此冰冷的字眼。我也并没有像我想象中的那么悲伤绝望,甚至一滴眼泪都没有落下。也许是因为早已经知道了这个结果吧。但是还是一夜没有睡着觉,真的无法接受这个冰冷的事实。

+

人,真的是会变的,会根据环境而变化。上大学的时候我们周围什么都没有,只有彼此。而步入社会,找到了稳定的工作,接触了各种各样的有趣的人,就会重新审视自己之前的生活,自己之前爱过的人。“我想换人了”“我倾向于比较条件,你的条件不如我”“及时止损”如此冰冷的话语,真的很难相信是从她的聊天框里面弹出来的。或许她发现自己面前存在着无数种可能性,为什么还要等着可能一年仅能见几次面,至少还要等上三年的远方的人呢?总之她不再怀念我们共度的三年时光了,毅然放手投入了新生活的怀抱,只能留下我在这里独自悲伤。

+

所以什么是爱情?我这几天不断在问自己这个问题。我一直认为爱情是一份承诺,是能克服重重困难一起走下去的勇气。现在我觉得这个想法确实太过于理想化了。可能我自己是这种想法,但是我不能要求别人有完全相同的想法。女孩子可能需要的并不是这种承诺,也不愿意有勇气,更愿意的是就在此时此刻,能有一个人在身边照顾她,关心她,两个人在一起的样子才是爱情。爱情也不可能没有物质需求,如果没有面包,只有爱情 ,那么这段爱情能撑到什么时候呢?如果能有一个人在身边照顾她,不愁吃穿,稳定工作,未来立刻触手可及,有人会不希望过上这种生活吗?可能以前觉得,两个人向着一个目标而努力,最终实现了理想,爱情自然修成了正果。但是如果不努力就能得到爱情,还努力做什么呢?为什么还要体验那种拼搏痛苦的生活,为什么不能躺在现实中直接享受呢?我这个人,对待每一件事情都很认真,对待每一个人也很认真,过于认真就过于理想化,理想化的目标,我能坚持但是并不能保证别人也坚持。世界是很残酷的,人也是很残酷的,坚持初心的人真的很少。

+

我的第一段恋爱之旅就这样结束了。我不恨她,她没有什么错误,也从来没有对我做出过任何的承诺,也没有做任何对不起我的事情。只能说,我们的爱情观确实不一致。好的恋爱让我们都成长了很多,学会更好地爱自己、爱他人。如果我还能有下一段爱情,我会更加谨慎地做出选择,没有结果,或者是短期内看不到结果的爱情,我宁愿不要,也不会去轻易去做出承诺,即使我知道我的承诺我一定坚持。

+

我不能这样悲伤下去,我要抬头向前看。虽然可能以后都不会有合适的人,合适的爱情,但,还是要过好每一天,珍惜自己现在的生活。最近纠结于这段感情,对父母疏远了一些,但其实他们才是这个世界上真的真的无条件爱我的人,我又有什么理由不爱他们呢?

+

放下过去,原谅自己,弥补过错,重新开始。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20220925
+
https://zhangzhao219.github.io/2022/09/25/diary/diary20220925/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月25日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/27/UCAS/information-retrieval/information-retrieval-10/index.html b/2022/09/27/UCAS/information-retrieval/information-retrieval-10/index.html new file mode 100644 index 000000000..191cb8bdd --- /dev/null +++ b/2022/09/27/UCAS/information-retrieval/information-retrieval-10/index.html @@ -0,0 +1,874 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第10讲 相关反馈及查询扩展 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第10讲 相关反馈及查询扩展

+ + +
+ +

《现代信息检索》课程笔记:第10讲 相关反馈及查询扩展

+ +

第10讲 相关反馈及查询扩展

+

动机

+

考虑查询q: [aircraft] . . .

+

某篇文档 d 包含“plane”, 但是不包含 “aircraft”

+

显然对于查询q,一个简单的IR系统不会返回文档d,即使d是和q最相关的文档

+

提高召回率的方法:

+

局部(local)方法:对用户查询进行局部的即时的分析

+

全局(Global)方法: 进行一次性的全局分析(比如分析整个文档集)来产生同/近义词词典

+

关于相关反馈和查询扩展:

+

相关反馈的本质是将检索返回的文档的相关性判定(不同的判定来源:人工或非人工)作为返回信息,希望提升检索效果(召回率和正确率)。

+

相关反馈常常用于查询扩展,所以提到相关反馈往往默认为有查询扩展

+

而查询扩展的最初含义是对查询进行扩充,近年来越来越向查询重构偏移,即现在的查询扩展是指对原有查询进行修改。

+
    +
  • 基于相关反馈(局部方法的代表)进行查询扩展/重构
  • +
  • 基于本讲的全局方法进行查询扩展/重构
  • +
  • 局部和全局方法相结合的方法
  • +
+

相关反馈基础

+

相关反馈的基本思想

+
    +
  • 用户提交一个(简短的)查询
  • +
  • 搜索引擎返回一系列文档
  • +
  • 用户或系统将部分返回文档标记为相关的,将部分文档标记为不相关的
  • +
  • 搜索引擎根据标记结果计算得到信息需求的一个新查询表示。当然我们希望该表示好于初始的查询表示
  • +
  • 搜索引擎对新查询进行处理,返回新结果,会有更高的召回率
  • +
+

显式相关反馈:用户显式参加交互过程

+

隐式相关反馈:系统跟踪用户的行为来推测返回文档的相关性,从而进行反馈。

+

伪相关反馈或盲相关反馈:没有用户参与,系统直接假设返回文档的前k篇是相关的,然后进行反馈。

+

相关反馈详细介绍

+

相关反馈中的核心概念:矩心

+

矩心是一系列点的中心

+

Rocchio算法是向量空间模型中相关反馈的实现方式

+

相关反馈中的假设:

+

假设 A1: 对于某初始查询,用户知道在文档集中使用哪些词项来表达

+

假设A2: 相关文档中出现的词项类似 (因此,可以基于相关反馈,从一篇相关文档跳到另一篇相关文档)

+

相关反馈的评价:

+

基于存留文档集(residual collection):用户没有判断的文档集

+

一轮相关反馈往往非常有用,相对一轮相关反馈,两轮相关反馈效果的提高有限。

+

用户相关反馈存在的问题:

+
    +
  • 用户相关反馈开销很大 +
      +
    • 相关反馈生成的新查询往往很长
    • +
    • 长查询的处理开销很大
    • +
    +
  • +
  • 用户不愿意提供显式的相关反馈
  • +
  • 很难理解,为什么会返回(应用相关反馈之后)某篇特定文档
  • +
  • Excite搜索引擎曾经提供完整的相关反馈功能,但是后来废弃了这一功能
  • +
+

隐式相关反馈

+

通过观察用户对当前检索结果采取的行为来给出对检索结果的相关性判定。

+

判定不一定很准确,但是省却了用户的显式参与过程。

+

用户行为种类:鼠标键盘动作和用户眼球动作

+

隐式相关反馈小结:

+

优点:

+
    +
  • 不需要用户显式参与,减轻用户负担
  • +
  • 用户行为某种程度上反映用户的兴趣,具有可行性
  • +
+

缺点:

+
    +
  • 对行为分析有较高要求
  • +
  • 准确度不一定能保证
  • +
  • 某些情况下需要增加额外设备
  • +
+

伪相关反馈

+

伪相关反馈对于真实相关反馈的人工部分进行自动化

+

伪相关反馈算法:对于用户查询返回有序的检索结果,假定前 k 篇文档是相关的进行相关反馈 (如 Rocchio)

+

优点:

+
    +
  • 不用考虑用户的因素,处理简单
  • +
  • 很多实验也取得了较好效果
  • +
+

缺点:

+
    +
  • 没有通过用户判断,所以准确率难以保证
  • +
  • 不是所有的查询都会提高效果
  • +
+

相关反馈小结:

+
    +
  • 文档选择:从检索结果中选择相关或不相关文档。用户显式/隐式,或者系统假设。
  • +
  • 词项选择:从相关不相关文档中选择需要处理的词项
  • +
  • 查询扩展/重构:修改原始查询
  • +
+

查询扩展

+

查询扩展是另一种提高召回率的方法

+

使用 “全局查询扩展” 来指那些 “查询重构(query reformulation)的全局方法”

+

在全局查询扩展中,查询基于一些全局的资源(同义词或近义词)进行修改,这些资源是与查询无关的

+

查询扩展的方法

+
    +
  • 基于相关反馈的查询扩展
  • +
  • 人工词典法:通过人工构建的同(近)义词词典 (人工编辑人员维护的词典,如 PubMed)来扩展原始查询
  • +
  • 自动词典法:自动导出的同(近)义词词典 (比如,基于词语的共现统计信息)
  • +
  • 其他外部资源法:比如基于查询日志挖掘出查询等价类(Web上很普遍,比如上面的 “palm” 例子)
  • +
+

交互式查询扩展 (Interactive QE):用户通常很懒,用户提交的扩展词项并不一定有用

+

基于词项相似度的查询扩展:

+

基于候选词和原始查询词项共现 (co-occurrences)的查询扩展

+

查询扩展的优点:

+
    +
  • 通常可以检索到更多的相关文档
  • +
  • 统计测试表明MAP显著提高
  • +
  • 在伪相关反馈的应用场景下,如果反馈集文档质量很差,会严重降低检索效果
  • +
  • 可能会产生查询漂移
  • +
  • 对于某些查询任务,例如主页搜索,由于相关文档总数非常少,查询扩展通常无效
  • +
+

使用外部资源进行查询扩展(External QE)

+

选择性查询扩展(Selective QE)

+
    +
  • 在伪相关反馈应用场景,如果预测反馈集质量很低,则不再执行QE
  • +
  • 适用于对排名靠前文档查准率(early precision)有要求的任务
  • +
+

搜索引擎中的查询扩展主要依赖的资源:查询日志

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第10讲 相关反馈及查询扩展
+
https://zhangzhao219.github.io/2022/09/27/UCAS/information-retrieval/information-retrieval-10/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月27日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/28/UCAS/advanced-ai/advanced-ai-5/index.html b/2022/09/28/UCAS/advanced-ai/advanced-ai-5/index.html new file mode 100644 index 000000000..1c713bd28 --- /dev/null +++ b/2022/09/28/UCAS/advanced-ai/advanced-ai-5/index.html @@ -0,0 +1,806 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第5讲 序列数据的深度学习模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第5讲 序列数据的深度学习模型

+ + +
+ +

《高级人工智能》课程笔记:第5讲 序列数据的深度学习模型

+ +

第5讲 序列数据的深度学习模型

+

循环神经网络

+

序列数据建模:

+
    +
  • 学习序列数据,常需要转换输入序列到不同领域的输出序列
  • +
  • 如果没有分离的目标序列,可以通过预测输入序列中的下一项来得到“教师信号”
  • +
  • 预测序列的下一项,模糊了监督学习与非监督学习的差别
  • +
+

为什么不使用标准的神经网络?

+
    +
  • 输入和输出数据在不同例子中可以有不同的长度
  • +
  • 不共享从文本的不同位置上学到的特征
  • +
+

RNN的特点:

+
    +
  • 隐藏状态可以高效存储过去的很多信息
  • +
  • 非线性的状态转移可以允许通过很复杂的方式更新他们的隐藏状态
  • +
+

一般来说,RNN每一时间的输入和输出是不一样的

+

序列学习:对于序列数据是将序列项依次传入,每个序列项再对应不同的输出

+

时序展开:在RNN中每一个时间步骤用到的参数都是一样的

+

RNN可看作权值共享的多层、前向网络,训练权值约束的前向网络

+

Back Propagation Through Time:前向传播和反向传播

+

示例:

+

语言模型

+

新序列采样

+

字符级别的语言模型

+

序列生成

+

长序列的循环神经网络

+

训练长序列 (100 time steps) RNN中,梯度很容易膨胀或消散

+

即使好的初始化,也难以检测当前目标输出对很多步之前的输入的依赖关系

+

GRU

+

LSTM:

+

解决了RNN长期(like hundreds of time steps)记忆的问题

+

LSTM是一个存储单元,使用logistic和linear单元执行乘法运算

+

记忆单元:存储RNN的长期信息

+

LSTM vs GRU

+

GRU是更加简单的模型,更容易创建一个更大的网络,而且它只有两个门,在计算性上也运行得更快,可以扩大模型的规模。

+

LSTM更加强大和灵活,有三个门而不是两个。

+

双向循环神经网络(Bidirectional RNN)

+

深层循环神经网络(Deep RNNs)

+

序列模型

+

机器翻译

+

图片说明

+

使用集束搜索(Beam search algorithm)而不使用贪心搜索

+

改进集束搜索(Refinements to Beam Search),序列长度归一化

+

注意力模型

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第5讲 序列数据的深度学习模型
+
https://zhangzhao219.github.io/2022/09/28/UCAS/advanced-ai/advanced-ai-5/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/28/UCAS/machine-learning/machine-learning-competition-advertisement/index.html b/2022/09/28/UCAS/machine-learning/machine-learning-competition-advertisement/index.html new file mode 100644 index 000000000..a1ba1d157 --- /dev/null +++ b/2022/09/28/UCAS/machine-learning/machine-learning-competition-advertisement/index.html @@ -0,0 +1,809 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 机器学习算法竞赛实战-计算广告 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

机器学习算法竞赛实战-计算广告

+ + +
+ +

机器学习算法竞赛实战-计算广告

+ +

第12章 计算广告

+

什么是计算广告

+

计算广告是指借助大数据的分析建模,使得广告能够覆盖广泛区域和实现消费者的多跨度精准曝光,让同一份广告尽可能接触到更多有效的流量和更多对广告感兴趣的人,从而用同样低的成本,让广告的效果尽可能更好,使产品和服务获得更多商业上的成功。

+

主要问题

+

如何协调广告主、平台和消费者三方之间的利益

+

计算广告系统架构

+

在线投放引擎:

+
    +
  • 广告检索:Web端发来广告请求时,系统根据该广告位的页面标签或者用户标签从广告索引中查找符合条件的广告。
  • +
  • 广告排序:当出现多个广告主抢夺一个广告位的情况时,需要对投放各个广告可能会产生的效益分别进行预估,对广告进行排序
  • +
+

分布式计算平台:

+
    +
  • 行为定向:挖掘广告投放日志中的用户行为属性
  • +
  • 点击率建模:在分布式计算平台上训练并得到点击率模型的参数和相应特征,用以辅助广告投放系统进行决策
  • +
+

流式计算平台:

+
    +
  • 实时受众定向:将最近一段短时间内发生的用户行为和广告投放日志及时地加工成实时用户标签,用以辅助广告检索模块。
  • +
  • 实时点击反馈:实时反馈用户行为和广告投放日志的变化,主要生成实时点击率相关特征,用以辅助广告检索模块。
  • +
+

广告类型

+

合约广告:包括CPT广告和定向广告。CPT广告指的是按照时间成本计算,广告主以固定的价格买断一段时间内的广告位来展示自己的广告;定向广告指的是广告主选择自己要投放的兴趣标签,然后算法为其匹配相应的受众人群并进行广告投放。

+

竞价广告:采用“价高者得”的方案来决策每次展示哪个广告,使得媒体主可以实时对不同广告进行比价,从而最大化收益。

+

程序化交易广告:广告主可以实时地在每一次广告展示中选择自己的目标受众,并且参与竞价。

+

广告召回

+

根据用户或商品属性以及页面上下文属性从广告索引中检索符合投放条件的候选广告。

+

广告召回模块

+

布尔表达式召回:根据广告主设置的定向标签组合成布尔表达式。

+

向量检索召回:通过传统的Word2Vec方式获取广告的向量表示,然后通过相似度计算对受众人群进行召回;或者通过深度学习模型获取广告的向量表示。

+

基于TDM(深度树匹配模型)的召回:基于深度学习的大规模推荐系统算法框架。

+

目前的找回策略大多是多路召回与权重检索相结合。

+

DSSM语义召回

+

为用户侧特征和广告侧特征构建不同的塔,在经过多层全连接后,计算相似度并进行广告检索。

+

广泛应用于搜索、推荐等领域的召回和排序问题中。

+

广告排序

+

对广告召回模块送来的广告候选集计算值,并按照所得值的大小倒排序。

+

点击率预估:向用户投放一个广告,然后预测用户点击广告的概率

+

特征处理:特征交叉组合、连续值特征的处理、点击率平滑、向量化表示

+

常见模型:

+
    +
  • FM:隐向量学习提升模型表达
  • +
  • Wide&Deep:记忆性与泛化性的信息互补
  • +
  • DeepFM:在FM基础上引入神经网络隐式高阶交叉信息
  • +
  • DIN:融合Attention机制的深度学习模型
  • +
+

广告竞价

+

在广告竞拍机制中,广告的实际曝光量取决于广告的流量覆盖大小和在竞争广告中的相对竞争力水平,其中前者取决于广告的人群定向(匹配对应特征的用户数量)、广告素材尺寸(匹配的广告位)以及投放时段、预算等设置项;影响后者的因素主要有出价、广告质量、以及对用户体验的控制策略等。

+

第13章 实战案例

+

第14章 实战案例

+ + +
+ +
+
+ + + + + + +
+
+
机器学习算法竞赛实战-计算广告
+
https://zhangzhao219.github.io/2022/09/28/UCAS/machine-learning/machine-learning-competition-advertisement/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/28/UCAS/machine-learning/machine-learning-competition-nlp/index.html b/2022/09/28/UCAS/machine-learning/machine-learning-competition-nlp/index.html new file mode 100644 index 000000000..7939c2873 --- /dev/null +++ b/2022/09/28/UCAS/machine-learning/machine-learning-competition-nlp/index.html @@ -0,0 +1,782 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 机器学习算法竞赛实战-自然语言处理 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

机器学习算法竞赛实战-自然语言处理

+ + +
+ +

机器学习算法竞赛实战-自然语言处理

+ +

第15章 自然语言处理

+

自然语言处理的发展历程

+
    +
  1. 1950年到1970年:基于经验、规则的阶段
  2. +
  3. 1970年到2008年:基于统计方法的阶段
  4. +
  5. 2008年至今:基于深度学习技术的阶段
  6. +
+

自然语言处理的常见场景

+
    +
  1. 分类、回归任务
  2. +
  3. 信息检索、文本匹配等任务
  4. +
  5. 序列对序列、序列标注
  6. +
  7. 机器阅读
  8. +
+

自然语言处理的常见技术

+
    +
  1. 基于词袋模型、TF-IDF的特征提取
  2. +
  3. N-Gram模型
  4. +
  5. 词嵌入模型
  6. +
  7. 上下文相关预训练模型
  8. +
  9. 常用的深度学习模型结构:TextCNN、BiLSTM+Attention、DPCNN
  10. +
+

第16章 实战案例

+ + +
+ +
+
+ + + + + + +
+
+
机器学习算法竞赛实战-自然语言处理
+
https://zhangzhao219.github.io/2022/09/28/UCAS/machine-learning/machine-learning-competition-nlp/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/30/UCAS/information-retrieval/information-retrieval-11/index.html b/2022/09/30/UCAS/information-retrieval/information-retrieval-11/index.html new file mode 100644 index 000000000..efbd09fea --- /dev/null +++ b/2022/09/30/UCAS/information-retrieval/information-retrieval-11/index.html @@ -0,0 +1,836 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第11讲 文本分类 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第11讲 文本分类

+ + +
+ +

《现代信息检索》课程笔记:第11讲 文本分类

+ +

第11讲 文本分类

+

常设查询(Standing Queries)

+

从检索到文本分类:假设某用户有一个经常关注的信息需求,用户会经常输入这个查询来寻找关于这个主题的新内容,关注于浏览新内容,此时排序问题变成了一个分类问题(相关 vs. 不相关)

+

需要构建分类函数

+

人工分类

+

专家分类一般都是准确的

+

当数据规模不大、标注者人数较少时,分类一致

+

当数据规模变大,人工分类困难且代价昂贵

+

人工编写的基于规则的分类器

+

新闻机构,情报机构等使用的一个技术,广泛部署于政府和企业

+

供应商提供“ IDE”来编写此类规则,商业系统具有复杂的查询语言

+

如果领域专家花时间精心完善规则,则准确性会很高,但是建立和维护这些规则非常昂贵

+

有监督学习

+

监督学习分类器可以使用各种特征

+

词袋模型

+

仅使用词项特征,使用文本中的所有词项

+

特征选择的意义

+
    +
  • 文本语料具有大量的词项/特征
  • +
  • 特征选择可以使得某些分类器可用
  • +
  • 减少训练时间
  • +
  • 使运行时模型更小,更快
  • +
  • 可以提高模型泛化能力
  • +
+

最简单的特征选择方法:

+
    +
  • 仅使用最常见词项
  • +
  • 没有特别的(理论)依据
  • +
  • 但是很好理解: +
      +
    • 这些词的概率可以被很好地估计(因为词频高),并且最常被用作相关性的证据
    • +
    • 在实际应用中,词频特征选择往往能达到一些更高的方法的90%的性能
    • +
    +
  • +
+

更聪明的特征选择方法:卡方(chi-square)等

+

朴素贝叶斯分类器

+

朴素贝叶斯分类的目标是寻找具有最大后验概率的类别

+

对数计算:通过取对数将原来的乘积计算变成求和计算

+

参数估计:极大似然估计

+

避免零概率:加一平滑

+

朴素贝叶斯对于训练集的大小和测试文档的大小而言是线性的,在某种意义上是最优的。

+
    +
  • 相对于其他很多更复杂的学习方法,朴素贝叶斯对不相关特征更具鲁棒性
  • +
  • 相对于其他很多更复杂的学习方法,朴素贝叶斯对概念漂移更鲁棒(概念漂移是指类别的定义随时间变化)
  • +
  • 当有很多同等重要的特征时,该方法优于决策树类方法
  • +
  • 如果满足独立性假设,那么朴素贝叶斯是最优的
  • +
  • 速度非常快、存储开销少
  • +
+

分类结果的评价:评估必须在独立于训练数据的测试数据上完成

+

评价指标:正确率(Precision),召回率(Recall),F1,分类准确率r/n ,其中 n 是所有测试文档的数量,r是正确分类的测试文档数量

+

向量空间分类

+

训练集包含一系列文档,每篇都标记着它的类别

+

在向量空间分类中,该集合对应着空间中一系列标记的点或向量。

+

利用Rocchio方法进行向量空间分类

+

基本思想:计算每个类的中心向量(所有文档向量的算术平均),将每篇测试文档分到离它最近的那个中心向量

+

Rocchio简单地将每个类别表示成其中心向量,分类基于文档向量到原型的相似度或聚类来进行,并不保证分类结果与训练集一致,即得到分类器后,不能保证训练集中的文档能否正确分类。

+

很多情况下,Rocchio的效果不如朴素贝叶斯:Rocchio算法不能正确处理非凸、多模式类别问题

+

kNN分类器

+

将每篇测试文档分给训练集中离它最近的那篇文档所属的类别。

+
    +
  • 不需要训练过程,但是文档的线性预处理过程和朴素贝叶斯的训练开销相当。对于训练集来说我们一般都要进行预处理,因此现实当中
    +kNN的训练时间是线性的。
  • +
  • 当训练集非常大的时候,kNN分类的精度很高
  • +
  • 如果训练集很小, kNN可能效果很差。
  • +
  • kNN倾向于大类,可以将相似度考虑在内来缓解这个问题。
  • +
+

线性分类器

+

线性分类器计算特征值的一个线性加权和

+

很多常用的文本分类器都是线性分类器:朴素贝叶斯、Rocchio、logistic回归、线性SVM等等

+

不同的方法选择超平面的策略不同,造成了在测试文档分类性能的巨大差异

+

不能通过更强大的非线性分类器来获得更好的分类性能

+

不存在某个学习方法对于任何分类问题都最优

+

kNN高方差低偏差,而朴素贝叶斯分类器低方差高偏差

+

单标签问题:类别之间互斥,每篇文档属于且仅属于某一个类

+

多标签分类问题:一篇文档可以属于0、1或更多个类,针对某个类的决策并不影响其他类别上的决策

+

对于给定的分类问题,要考虑很多因素从而选择合适的分类器算法。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第11讲 文本分类
+
https://zhangzhao219.github.io/2022/09/30/UCAS/information-retrieval/information-retrieval-11/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月30日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/09/30/UCAS/machine-learning/machine-learning-7/index.html b/2022/09/30/UCAS/machine-learning/machine-learning-7/index.html new file mode 100644 index 000000000..3d155b924 --- /dev/null +++ b/2022/09/30/UCAS/machine-learning/machine-learning-7/index.html @@ -0,0 +1,911 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第7章 降维与特征选择 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第7章 降维与特征选择

+ + +
+ +

《机器学习》课程笔记:第7章 降维与特征选择

+ +

第7章 降维与特征选择

+

概述

+

机器学习算法的有效性和计算复杂度是敏感于数据的特征表达和维度。

+

特征降维的意义:

+

数据压缩:简化数据表示,加快数据通信传输、节省存储资源、…

+

学习算法效率:

+
    +
  • 计算上,简化计算,加快速度
  • +
  • 性能上,提升精确度
  • +
  • 可理解性,发现数据的潜在本质特征
  • +
+

特征选择:从D个特征中选择d个,来表达模式

+

特征提取:采用特征变换的方法,生成d个新的特征

+

特征选择

+

特征选择框架

+

特征选择问题:从D维特征中选择d维(d<D)特征子集

+
    +
  • 使数据的压缩率高
  • +
  • 使学习机预测性能最佳
  • +
  • 使学习机学习速度加快
  • +
+

特征选择的处理过程:

+

xFNOw8.md.png

+

特征子集生成

+

特征子集生成问题:D维特征中,选择d维(d<D)特征子集,子集个数为

+
    +
  1. 穷举(最优子集搜索):计算特征的所有可能组合,并逐一评价。
  2. +
  3. 单独最优特征组合:对每个特征分别评估,找前d个单独最优特征。优点:算法简单,缺点:没有考虑特征之间的关系,存在特征冗余
  4. +
  5. SFS(Sequential forward selection, 前向序贯):每次加入一个特征,该特征使得新的特征组合最优。
  6. +
  7. GSFS (广义SFS):每次加入k个特征,使加入特征后的组合最优。
  8. +
  9. SBS(Sequential backward selection, 后向序贯):每次减掉一个特征,使剩余特征组合最优。
  10. +
  11. GSBS(广义SBS):每次减k个特征,使剩余特征组合最优。
  12. +
  13. L-R 法(增加L个,减R个)每次增加L个再减R个(L > R),或减R个增加L个(L < R)
  14. +
  15. 广义的L-R(ZL , ZR):增L和减R分Z步进行
  16. +
+

特征评价准则

+
    +
  1. 可分性度量:在选择的特征集下,采用类别可分性的程度,评价特征选择的好与坏。常用于Filter框架下。
  2. +
  3. 学习算法精度的度量:在选择的特征集下,通过学习算法的精确度,评价特征选择的好与坏。常用于wrapper框架下。
  4. +
+

基于距离的可分性判据:

+

通常依赖于类内类间的距离度量,前提是数据具有类别标签。可分性评估是在选择的特征子集维度上计算数据统计量。

+

距离的可分性判据的特点:

+
    +
  • 容易理解和实现
  • +
  • 与错误率无直接关系,不敏感于数据交叠情况
  • +
  • 常用于Filter特征选择框架下
  • +
+

基于概率分布的可分性判据:从类别概率密度的角度,讨论两个类别的交叠程度

+

常见的概率距离准则:

+

熵可分性判据:

+

特征选择方法

+

Filter 方法:

+

不依赖于学习算法(如分类器)的结果,直接由数据构建评估函数,对选择的特征子集进行评估。

+

通常方法:根据特征评价准则进行评估,选择最优的特征子集。

+

评价准则:距离准则、概率可分、熵可分准则。

+

优点:计算复杂度低,效率高。

+

缺点:选择的特征之间存在冗余信息。

+

Wrapper 方法:

+

原理:通过学习算法(如分类器),对选择的特征子集进行评估。

+

优点:选择的特征可以支持学习算法。

+

缺点:算法的计算复杂度高。

+

Embedded 方法:

+

原理:特征选择过程在学习算法中完成,目标是完成学习过程。

+

特点:不是专门的特征选择过程

+

缺点:计算复杂度高。

+

特征提取

+

优点:

+
    +
  • 数据更紧致的压缩
  • +
  • 优化预测性能
  • +
  • 加快学习速度
  • +
+

不同的应用问题会有不同的特征提取研究问题

+

线性变换

+

特征提取目标:学习变换矩阵

+

给定 , 通过某种降维准则, 学习变换矩阵

+

两种降维表示途径:

+
    +
  • 投影:
  • +
  • 矩阵分解:低秩表示:
  • +
+

主成分分析PCA

+

目标函数:均方误差最小原则(求最优重构子空间)

+

s.t.

+

+

最小误差等价于最大投影

+

求解目标函数:

+

特征值的意义:样本在w方向的投影平均值(或和)最大

+

PCA算法流程:

+
    +
  1. 标准化样本
  2. +
  3. 求样本的协方差矩阵特征值,并降排序对应非零特征向量
  4. +
  5. 变换矩阵
  6. +
  7. 降维表示
  8. +
+

线性鉴别分析LDA

+

PCA能保证类别区分的有效性,LDA特征的优点:类内最小、类间最大。

+

特征方向的提取:

+

非线性变换

+

核主成分分析KPCA

+
    +
  1. 求核矩阵的特征值,对应特征向量的问题:
  2. +
  3. 核矩阵的特征值降序,前个特征值对应特征向量
  4. +
  5. 高维空间中的投影方向$w_i=\Phi \boldsymbol{\alpha}_i \boldsymbol{\Lambda}=\left(\boldsymbol{\alpha}_1, \boldsymbol{\alpha}_2, \ldots, \boldsymbol{\alpha}_d\right)W=\Phi \boldsymbol{\Lambda}$
  6. +
  7. 降维表示 +
      +
    1. 训练集低维表示:
    2. +
    3. 新样本的低维表示:
    4. +
    5. 其中
    6. +
    +
  8. +
+

局部线性变换LLE

+

LLE方法是一种流形学习,保持样本间的局部线性关系,整体实现非线性映射。

+

非负矩阵分解

+

基本思想:通过矩阵分解,进行数据降维;分解后的矩阵为非负矩阵

+

不同的目标函数情况:

+
    +
  1. 范数误差最小
  2. +
  3. KL误差
  4. +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第7章 降维与特征选择
+
https://zhangzhao219.github.io/2022/09/30/UCAS/machine-learning/machine-learning-7/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年9月30日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/02/UCAS/information-retrieval/information-retrieval-12/index.html b/2022/10/02/UCAS/information-retrieval/information-retrieval-12/index.html new file mode 100644 index 000000000..6d5218fe9 --- /dev/null +++ b/2022/10/02/UCAS/information-retrieval/information-retrieval-12/index.html @@ -0,0 +1,822 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第12讲 支持向量机和排序学习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第12讲 支持向量机和排序学习

+ + +
+ +

《现代信息检索》课程笔记:第12讲 支持向量机和排序学习

+ +

第12讲 支持向量机和排序学习

+

支持向量机

+

线性可分情况下,不仅要区分开,而且要使得区分间隔最大

+

最优超平面)是使得两类的分类间隔(Margin)最大的超平面,即每类中离超平面最近的样本到超平面的距离最大。距离这个最优超平面最近的样本被称为支持向量。

+

求解最优超平面就相当于,在上述约束条件下,求2/||W||的最大值 ,即以下损失函数最小值

+

二次优化问题可以采用Lagrange方法求解

+

非线性可分情况下的处理

+

广义最优分类面方法:在线性不可分的情况下,就是某些训练样本不能满足约束条件,因此可以在条件中增加一个松弛项ζ(发音Zeta,也称
+引入Soft Margin,软边界),变换约束条件。

+

变换到高维空间的支持向量机

+
    +
  • SVM训练相对较慢,分类速度一般。但是分类效果较好。
  • +
  • 在面对非线性可分情况时,可以引入松弛变量进行处理或者通过空间变换到另一个线性可分空间进行处理。
  • +
  • SVM有很多实现工具,SMO/SVM light/SVM torch/LibSVM等等
  • +
+

为什么要使间隔最大化?

+
    +
  • 分界面附近的点代表了不确定的分类决策,分类器会以两边各50%的概率做出决策
  • +
  • 具有很大分类间隔的分类器不会做出确定性很低的决策,它给出了一个分类的安全间隔
  • +
  • 度量中的微小错误和文档中的轻微变化不会导致错误分类
  • +
  • SVM 分类器:在决策面周围有大的间隔
  • +
  • 与放置(无穷的)决策超平面相比,如果要在类别间放置一个宽间隔,那么选择会少很多
  • +
  • 减少记忆容量、增加测试文档分类泛化能力
  • +
+

SVM用于支持多类问题:结构化SVM

+

排序学习

+

基于布尔权重的学习

+
    +
  • 词项权重(如tfidf)的目标是为了度量词项的重要性 +
      +
    • 将一篇文档中所有词项的权重加起来便可以计算文档和查询的相关度,基于该相关度可以对所有文档排序
    • +
    +
  • +
  • 上述过程可以想象成一个文本分类问题 +
      +
    • 词项权重可以从已判定的训练集合中学习得到
    • +
    +
  • +
  • 上述研究方法被归入一类称为机器学习的相关度或排序学习
  • +
+

权重学习主要方法:

+

给定训练样例集合,每个样例表示为三元组<q, d, R(d,q)>

+

从上述样例中学习权重,使得学到的评分接近训练集中的相关性判定结果。

+

基于实数权重的学习

+

评分函数是两个因子的线性组合:

+
    +
  • 查询和文档的向量空间相似度评分
  • +
  • 查询词项在文档中存在的最小窗口宽度
  • +
+

我们的一个因子取决于查询词项在文档中的词袋统计量,另一个因子取决于邻近度权重

+

基于机器学习的检索结果排序

+

基于序回归的排序学习

+

将IR排序问题看成序回归

+

对于同一查询,文档之间可以按照相对得分排序即可,并不一定要求每篇文档有一个全局的绝对得分。因此,只需要一个排序,而不要得到相关度的绝对得分,问题空间可以减小。

+

排序SVM的构建

+
    +
  • 给定一些已经判定的查询
  • +
  • 对训练集中的每条查询q, 我们都有针对该查询的一系列文档集合,这些文档已经由人工按照其与查询的相关度排序
  • +
  • 对每个文档、查询对,构造特征向量 ψj = ψ(dj , q),这里的特征可以采用前面讨论的特征
  • +
  • 对于两篇文档di 和dj ,可以计算特征向量之间的差异向量
  • +
+

排序学习总结

+

排序学习算法现在一般分为以下三类

+
    +
  • Pointwise (即本讲介绍的权重学习方法):每个文档是一个训练样本,预测文档相关/不相关
  • +
  • Pairwise (即本讲介绍的序回归方法):文档对构成一个训练样本,预测一个文档相关性是否高于另一个文档
  • +
  • Listwise(基于列表的排序学习,未介绍):一个文档排序列表构成一个训练样本,预测最优排序
  • +
+

虽然近年来基于深度学习和大规模预训练语言模型的方法已成功应用于IR,排序学习仍然是一种整合不同文本特征的有效方法。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第12讲 支持向量机和排序学习
+
https://zhangzhao219.github.io/2022/10/02/UCAS/information-retrieval/information-retrieval-12/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/02/diary/diary20221002/index.html b/2022/10/02/diary/diary20221002/index.html new file mode 100644 index 000000000..df1e38514 --- /dev/null +++ b/2022/10/02/diary/diary20221002/index.html @@ -0,0 +1,749 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20221002 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20221002

+ + +
+ +

在下一段感情之前,必须要将这一段感情的问题想清楚,不能上头!

+ +

首先,我要好好剖析一下自己的性格。我的性格在我看来是存在很大的缺陷的,但是其实我自己并不是很了解自己。这次要好好想想。

+
    +
  1. 敏感。太敏感了,而且想法也特别多。一旦有一件事情和我想象的不一样,我的脑海中就会涌现出很多很多的想法,有时候会自我否定,有时候会怎么想也想不明白,想想就很难受,难受就无法静下心来做事。
  2. +
  3. 注重过去。我能清晰记得过去印象比较深刻的所有事情,如果是我自身经历的事情,我的记忆力会超级超级好。我有时还喜欢记日记,还喜欢留存着过去的一些东西,不愿意从过去中走出来。我不想让自己改变,我喜欢停留在过去的思想,经历着过去一样经历的事情。
  4. +
  5. 认真。在做自己喜欢的事情或者做一些我认为非常有意义的事情时会非常非常认真,非常在意,在意到别人似乎无法理解的程度。
  6. +
  7. 恋爱脑。真的好想好想谈恋爱,好想为了一个人,为了一个目标去奋斗,仅仅为了我自己去奋斗始终感觉动力真的不足。
  8. +
  9. 自卑。身高自卑、长相自卑、学习自卑,总是感觉自己什么东西都比不上别人。
  10. +
+

人,都是会变的。虽然我不愿意承认我自己在变化,但是事实是所有人都在改变,包括我自己在内,区别在于大家在潜移默化中接受了自己的变化,而我总愿意回头去看,不承认自己已经改变了的事实。我的前女友也在变。可能大一大二的时候,她只是需要一个人陪在她的身边,帮助她度过这些单调无聊的生活,但是大三开始,免不得要为自己以后的前程考虑。似乎用这种眼光来看,我们两个并不合适。但是我仍然纠结于自己之前的感情,认为我们还是和之前相同的,但是实际上已经不一样了。

+

我以为平平淡淡就是美,就是真爱,但是我还年轻啊,为什么不更有激情一点,聊天更有意思一点?恋爱脑最近还有点想大学同学。拜托?人家和你根本就不是同一路人,城市都不在同一个,怎么谈?人家会答应一个异地的矮子吗?

+

和gxgg聊了关于他女朋友的事情,我觉得他现在的状态也不是很好的状态,说不定保了研就基本告别了。我不知道为什么我们在感情生活中如此悲惨。是不是就是因为我们太在意了?是不是,没那么在意会好很多?

+

表白是水到渠成,不是破釜沉舟不要相信自己的朴实,别人不信没有用的,多看看别人的经验

+

不咬手指,前期用指甲刀修,尽量不撕嘴皮,把眼皮养好,这样太难受了

+

不要胡思乱想,好好学习!好好学习后端!

+

20221009

+

没写完,在下面继续写吧

+

一焦虑就咬手指,控制不住,毕竟都差不多二十年的毛病了。今天下午又开始emo,感觉计算机现在就不应该学,到处都是会计算机的,互联网大厂又基本上没有国家的支持,以后是不是会非常非常难啊。。。我一直在踏踏实实的学习,希望能学以致用,以后过的轻松一点,换一个城市生活。但是现在我感觉自己没有什么大的变化,环境却翻天覆地的变了。早出生几年,房子也不贵,学个计算机现在就已经财务自由了,基本都能做到OG级别了。可是我还有三年啊。。。这三年会变成什么样子啊。。。真的不敢想象。是不是还会经历战争?小时候还知道“和平和发展”是世界的主题。然而现在一切都变了。疫情、动乱、封锁。真的不知道我自己的努力方向在哪里,真的好想好想预测一下三年后究竟是个什么样子。。。

+

太焦虑了,太焦虑了。不学习感觉过的不踏实,学习又焦虑效率不太高。

+

可能什么时候真正把咬手指戒掉了,什么时候我才是一个正常的状态吧。。。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20221002
+
https://zhangzhao219.github.io/2022/10/02/diary/diary20221002/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/03/Go/go-basic-ms/index.html b/2022/10/03/Go/go-basic-ms/index.html new file mode 100644 index 000000000..08bd7639e --- /dev/null +++ b/2022/10/03/Go/go-basic-ms/index.html @@ -0,0 +1,3155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go基础学习(微软教程) - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go基础学习(微软教程)

+ + +
+ +

Go基础学习(微软教程)

+ +

什么是 Go?

+

Go 语言表现力强,且简单明了。 它在设计时考虑了惯用语言,这使程序员能够高效地编写高效且可靠的代码。 以 Go 语言编写的程序可以在 Unix 系统上运行,例如 Linux 和 macOS,还有 Windows。 Go 语言之所以值得注意,部分原因在于它独特的并发机制,使得编写可同时利用多个内核的程序非常容易。 它主要是一种强化静态类型的语言,这意味着变量类型在编译时是已知的。 不过,它确实具有一些动态类型化功能。

+

下面是 Go 编程语言的基本原理优势:

+
    +
  • Go 许可证是完全开放源代码的。
  • +
  • Go 程序编译为单独的二进制文件,这样更易于共享和分发。
  • +
  • 交叉编译到各种平台和操作系统
  • +
  • Go 语言致力于使语言变得简单,并用更少的代码行执行更多操作。
  • +
  • 并发是头等概念,使任何函数可以作为轻量级线程运行,而程序员只需少量工作。
  • +
  • Go 语言提供自动内存管理,包括垃圾回收。
  • +
  • 编译和执行速度很快。
  • +
  • Go 语言需要使用所有代码,否则会引发错误。
  • +
  • 有一种官方格式设置可帮助保持项目之间的一致性。
  • +
  • Go 语言具有大量全面标准库,并且可以在不使用第三方依赖项的情况下生成多个应用程序。
  • +
  • Go 保证语言与以前版本的后向兼容性。
  • +
+

安装Go

+

如果不想在本地安装 Go,可以使用 Go Playground。 Go Playground 是一款 Web 服务,可在浏览器中运行 Go 应用程序。

+

本地安装包下载地址

+
wget https://go.dev/dl/go1.19.1.linux-amd64.tar.gz // 版本号可能改变
+

提取本地安装包

+
sudo tar -C /usr/local -xzf go1.19.1.linux-amd64.tar.gz
+

编辑配置文件,添加到环境变量:

+
vim ~/.bashrc
+export PATH=$PATH:/usr/local/go/bin
+source ~/.bashrc
+

确认是否已经安装好:

+
go version
+
go version go1.19.1 linux/amd64
+

配置Go工作区

+

Go 在组织项目文件方面与其他编程语言不同。 首先,Go 是在工作区的概念下工作的。 工作区就是应用程序源代码所在的位置。 所有 Go 项目共享同一个工作区。 不过,从版本 1.11 开始,Go 已开始更改此方法。 你尚且不必担心,因为我们将在下一个模块中介绍工作区。 现在,Go 工作区位于 $HOME/go,但如果需要,可以为所有项目设置其他位置。

+

若要将工作区设置为其他位置,可以使用 $GOPATH 环境变量。 在处理更复杂的项目时,此环境变量有助于避免将来出现问题。

+
export GOPATH=/mnt/d/Programming_Design/Go
+

Go 工作区文件夹

+

每个 Go 工作区都包含三个基本文件夹:

+
    +
  • bin :包含应用程序中的可执行文件。
  • +
  • src :包括位于工作站中的所有应用程序源代码。
  • +
  • pkg :包含可用库的已编译版本。 编译器可以链接这些库,而无需重新编译它们。
  • +
+

例如,工作站文件夹结构树可能与下面的示例类似:

+
bin/
+    hello
+    coolapp
+pkg/
+    github.com/gorilla/
+        mux.a
+src/
+    github.com/golang/example/
+        .git/
+    hello/
+        hello.go
+

VSCode Go 插件

+

在安装插件之前要先更改go的源

+
The "gopls" command is not available. Run "go install -v golang.org/x/tools/gopls@latest" to install.
+

然后点击上边的窗口的install All,即可完成插件的安装

+

第一个Go应用

+

文件夹组织形式:

+

xQJIts.png

+
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello World!")
+}
+
+

运行:

+
go run main.go
+
Hello World!
+

只生成二进制文件但是不运行:

+
go build main.go
+

代码解释

+

我们在 package main 语句中告诉 Go,我们将要创建的应用是一个可执行程序(可以运行的文件)。 我们的“Hello World!”应用是 main 包的一部分。

+

包是一组常用的源代码文件。 每个可执行应用都具有此第一行,即使项目或文件具有不同的名称。

+

import 语句使你的程序可以访问其他包中的其他代码。 在本例中,fmt 为标准库包。

+

你需要此 import 语句,因为你将在此程序的稍后部分使用此包中的函数将消息打印到屏幕上。 可以在程序中包含你想要或需要的任意数量的 import 语句。 但是,Go 在这方面是惯用的。 如果导入包,但不使用包中的相应函数,应用将不会进行编译Visual Studio Code 的一大功能是,当你保存文件时,它会自动删除程序中未使用的导入

+

VSCode 是自动帮助我们删除的,但是和Python什么的不一样,如果多了冗余的包程序是无法运行的

+
# command-line-arguments
+./main.go:4:8: imported and not used: "math"
+

func 语句是用于声明函数的保留字。 第一个函数名为“main”,因为它是程序的起始点。 整个 package main 中只能有一个 main() 函数(在第一行中定义的那个)。 在 main() 函数中,你调用了 fmt 包中的 Println 函数。 你发送了你希望在屏幕上看到的文本消息。

+

声明和使用变量

+

声明变量

+
var firstName string            // 声明单一变量
+var secondName, lastName string // 如果多个变量的类型相同,可以用逗号分隔一起声明多个变量
+var age int
+
+// 也可以在一个括号内批量声明变量
+var (
+	thirdName, fourthName string
+	secondage             int
+)
+

(VSCode会自动进行格式化,完全不用担心格式的问题)

+

初始化变量

+

可以在声明的时候直接对变量进行初始化,会自动对变量的类型进行推断,不用显式指定类型

+
var (
+    firstName string = "John"
+    lastName  string = "Doe"
+    age       int    = 32
+)
+

等价于下面的写法:

+
var (
+    firstName, lastName, age = "John", "Doe", 32
+)
+

在main函数内部声明+初始化(更加常用)

+
package main
+
+import "fmt"
+
+func main() {
+	firstName, lastName := "John", "Doe"
+	age := 32
+	fmt.Println(firstName, lastName, age)
+}
+
John Doe 32
+

请注意,在定义变量名称后,需要在此处加入一个冒号等于号 (:=) 和相应的值。 使用冒号等于号时, 要声明的变量必须是新变量 。 如果使用冒号等于号并已经声明该变量,将不会对程序进行编译。

+

声明常量

+

用于声明常量的关键字是 const

+
const HTTPStatusOK = 200
+
+const (
+	StatusOK              = 0
+	StatusConnectionReset = 1
+	StatusOtherError      = 2
+)
+

常量和变量之间既有相似之处,也有一些重要差异。 例如,你可以在不使用常量的情况下声明常量。 你不会收到错误消息。 不能使用冒号等于号来声明常量。 如果采用这种方式,Go 会发出警告。

+

在 Go 中,当你(在函数内部)声明一个变量但不使用它时,Go 会抛出错误,而不是像某些其他编程语言一样抛出警告。

+

基本数据类型

+

整数数字

+

一般来说,定义整数类型的关键字是 int。 但 Go 还提供了 int8int16int32int64 类型,其大小分别为 8、16、32 或 64 位的整数。 使用 32 位操作系统时,如果只是使用 int,则大小通常为 32 位。 在 64 位系统上,int 大小通常为 64 位。 但是,此行为可能因计算机而不同。 可以使用 uint。 但是,只有在出于某种原因需要将值表示为无符号数字的情况下,才使用此类型。 此外,Go 还提供 uint8uint16uint32uint64 类型。

+
var integer8 int8 = 127
+var integer16 int16 = 32767
+var integer32 int32 = 2147483647
+var integer64 int64 = 9223372036854775807
+

不能进行隐式转换,如果两个变量的类型不同,需要进行强制转换,否则编译不能通过。

+

浮点数字

+

Go 提供两种浮点数大小的数据类型:float32float64。 如果需要存储较大的数字,则可以使用这些类型,这些类型无法适应前面提到的任何一个整数类型。 这两种类型的区别是它们可以容纳的最大位数。

+
var float32 float32 = 2147483647
+var float64 float64 = 9223372036854775807
+fmt.Println(float32, float64)
+

可以使用 math 包中提供的 math.MaxFloat32math.MaxFloat64 常量来查找这两种类型的限制。

+
package main
+
+import (
+	"fmt"
+	"math"
+)
+
+func main() {
+	fmt.Println(math.MaxFloat32, math.MaxFloat64)
+}
+
3.4028234663852886e+38 1.7976931348623157e+308
+

布尔型

+

布尔类型仅可能有两个值:truefalse。 你可以使用关键字 bool 声明布尔类型。 Go 不同于其他编程语言,在 Go 中,你不能将布尔类型隐式转换为 0 或 1。

+
var featureFlag bool = true
+

字符串

+

最后,让我们看一下编程语言中最常见的数据类型:string。 在 Go 中,关键字 string 用于表示字符串数据类型。 若要初始化字符串变量,你需要在双引号(")中定义值。 单引号(')用于单个字符(以及 runes,正如我们在上一节所述)。

+
var firstName string = "John"
+lastName := "Doe"
+fmt.Println(firstName, lastName)
+
John Doe
+

默认值

+

到目前为止,几乎每次声明变量时,都使用值对其进行了初始化。 但与在其他编程语言中不同的是,在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。 此功能非常方便,因为在使用之前,你无需检查变量是否已初始化。

+

下面列出了我们目前浏览过类型的几个默认值:

+
    +
  • int 类型的 0(及其所有子类型,如 int64
  • +
  • float32float64 类型的 +0.000000e+000
  • +
  • bool 类型的 false
  • +
  • string 类型的空值
  • +
+

类型转换

+

Go 中隐式强制转换不起作用。 接下来,需要显式强制转换。 Go 提供了将一种数据类型转换为另一种数据类型的一些本机方法。

+

一种方法是对每个类型使用内置函数,如下所示:

+
var integer16 int16 = 127
+var integer32 int32 = 32767
+fmt.Println(int32(integer16) + integer32)
+

Go 的另一种转换方法是使用 strconv 包。 将 stringint

+
package main
+
+import (
+	"fmt"
+	"strconv"
+)
+
+func main() {
+	i, _ := strconv.Atoi("-42")
+	s := strconv.Itoa(-42)
+	fmt.Println(i, s)
+}
+
-42 -42
+

有一个下划线 (_) 用作变量的名称。 在 Go 中(或Python中),这意味着我们不会使用该变量的值,而是要将其忽略。

+

创建函数

+

在 Go 中,函数允许你将一组可以从应用程序的其他部分调用的语句组合在一起。 你可以使用函数来组织代码并使其更易于阅读,而不是创建包含许多语句的程序。 更具可读性的代码也更易于维护。

+

与之交互的函数是 main() 函数。 Go 中的所有可执行程序都具有此函数,因为它是程序的起点。 你的程序中只能有一个 main() 函数。

+

命令行参数

+
package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+)
+
+func main() {
+	number1, _ := strconv.Atoi(os.Args[1])
+	number2, _ := strconv.Atoi(os.Args[2])
+	fmt.Println("Sum:", number1+number2)
+}
+

os.Args 变量包含传递给程序的每个命令行参数。 由于这些值的类型为 string,因此需要将它们转换为 int 以进行求和。

+
> go run main.go 3 5
+Sum: 8
+

自定义函数

+

使用 func 关键字来定义函数,然后为其指定名称。 在命名后,指定函数的参数列表。 你可以指定零个或多个参数。 你还可以定义函数的返回类型,该函数也可以是零个或多个。 (我们将在下一节中讨论如何返回多个值)。在定义所有这些值之后,你可以编写函数的正文内容。

+
package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+)
+
+func main() {
+	sum := sum(os.Args[1], os.Args[2])
+	fmt.Println("Sum:", sum)
+}
+
+func sum(number1 string, number2 string) int {
+	int1, _ := strconv.Atoi(number1)
+	int2, _ := strconv.Atoi(number2)
+	return int1 + int2
+}
+

此代码创建一个名为 sum 的函数,该函数采用两个 string 参数,并将它们强制转换为 int,然后返回求和所得的结果。 定义返回类型时,函数需要返回该类型的值。

+

在 Go 中,你还可以为函数的返回值设置名称,将其当作一个变量。

+
func sum(number1 string, number2 string) (result int) {
+	int1, _ := strconv.Atoi(number1)
+	int2, _ := strconv.Atoi(number2)
+	result = int1 + int2
+	return
+}
+

返回多个值

+
package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+)
+
+func main() {
+	sum, mul := calc(os.Args[1], os.Args[2])
+	fmt.Println("Sum:", sum)
+	fmt.Println("Mul:", mul)
+}
+
+func calc(number1 string, number2 string) (sum int, mul int) {
+	int1, _ := strconv.Atoi(number1)
+	int2, _ := strconv.Atoi(number2)
+	sum = int1 + int2
+	mul = int1 * int2
+	return
+}
+
> go run main.go 3 5
+Sum: 8
+Mul: 15
+

更改函数参数值(指针)

+

将值传递给函数时,该函数中的每个更改都不会影响调用方。 Go 是“按值传递”编程语言。 每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。 在函数中对该变量所做的更改都不会影响你向函数发送的更改。

+

指针是包含另一个变量的内存地址的变量。 当你发送指向某个函数的指针时,不会传递值,而是传递地址内存。 因此,对该变量所做的每个更改都会影响调用方。

+

在 Go 中,有两个运算符可用于处理指针:

+
    +
  • & 运算符使用其后对象的地址。
  • +
  • * 运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。
  • +
+
package main
+
+import "fmt"
+
+func main() {
+	firstName := "John"
+	updateName(&firstName)
+	fmt.Println(firstName)
+}
+
+func updateName(name *string) {
+	*name = "David"
+}
+
+

首先要做的就是修改函数的签名,以指明你要接收指针。 为此,请将参数类型从 string 更改为 *string。 (后者仍是字符串,但现在它是指向字符串 的 指针。)然后,将新值分配给该变量时,需要在该变量的左侧添加星号 (*) 以暂停该变量的值。 调用 updateName 函数时,系统不会发送值,而是发送变量的内存地址。 这就是前面的代码在变量左侧带有 & 符号的原因。

+

了解包

+

Go 包与其他编程语言中的库或模块类似。 你可以打包代码,并在其他位置重复使用它。 包的源代码可以分布在多个 .go 文件中。 到目前为止,我们已编写 main 包,并对其他本地包进行了一些引用。

+

main 包

+

你可能注意到,在 Go 中,甚至最直接的程序都是包的一部分。 通常情况下,默认包是 main 包,即目前为止一直使用的包。 如果程序是 main 包的一部分,Go 会生成二进制文件。 运行该文件时,它将调用 main() 函数。

+

换句话说,当你使用 main 包时,程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时,Go 不会生成二进制文件。 它生成包存档文件(扩展名为“.a”的文件)。

+

在 Go 中,包名称需遵循约定。 包使用其导入路径的最后一部分作为名称。 例如,Go 标准库包含名为 math/cmplx 的包,该包提供用于处理复数的有用代码。 此包的导入路径为 math/cmplx,导入包的方式如下所示:

+
import "math/cmplx"
+

创建包

+

在名为 calculator 的目录中 创建名为 sum.go 的文件。 树目录应如下列目录所示:

+

xQUdgI.png

+

用包的名称初始化 sum.go 文件:

+
package calculator
+

你现在可以开始编写包的函数和变量。 不同于其他编程语言,Go 不会提供 publicprivate 关键字,以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则:

+
    +
  • 如需将某些内容设为专用内容,请以小写字母开始。
  • +
  • 如需将某些内容设为公共内容,请以大写字母开始。
  • +
+

接下来,让我们将以下代码添加到我们要创建的计算器包:

+
package calculator
+
+var logMessage = "[LOG]"
+
+// Version of the calculator
+var Version = "1.0"
+
+func internalSum(number int) int {
+    return number - 1
+}
+
+// Sum two integer numbers
+func Sum(number1, number2 int) int {
+    return number1 + number2
+}
+

让我们看一下该代码中的一些事项:

+
    +
  • 只能从包内调用 logMessage 变量。
  • +
  • 可以从任何位置访问 Version 变量。 建议你添加注释来描述此变量的用途。 (此描述适用于包的任何用户。)
  • +
  • 只能从包内调用 internalSum 函数。
  • +
  • 可以从任何位置访问 Sum 函数。 建议你添加注释来描述此函数的用途。
  • +
+

若要确认一切正常,可在 calculator 目录中运行 go build 命令。 如果执行此操作,请注意系统不会生成可执行的二进制文件。

+

创建模块

+

你已将计算器功能放入包中。 现在可以将包放到模块中。 Go 模块通常包含可提供相关功能的包。 包的模块还指定了 Go 运行你组合在一起的代码所需的上下文。 此上下文信息包括编写代码时所用的 Go 版本。

+

此外,模块还有助于其他开发人员引用代码的特定版本,并更轻松地处理依赖项。 另一个优点是,我们的程序源代码无需严格存在于 $GOPATH/src 目录中。 如果释放该限制,则可以更方便地在其他项目中同时使用不同包版本。

+

(下面与教程不同,自己探索出了一个可用不报错的方法)

+

VSCode GOPATH设置:"go.gopath": "/mnt/d/Programming_Design/Go"

+

首先设置 go env -w GO111MODULE=on

+

如果 helloworld要引用 calculator,则文件夹的组织形式如下:

+

xlDdl4.png

+

$GOPATH/src/calculator创建 go.mod文件,其中文件第一行与文件夹同名

+
module calculator
+
+go 1.19
+

$GOPATH/src/helloworld创建 go.mod文件,其中文件第一行与文件夹同名,下面要写好版本号和包的路径

+
module helloworld
+
+go 1.19
+
+require "calculator" v1.0.0
+replace "calculator" => "../calculator"
+

然后可以导入这个包并运行主文件

+
package main
+
+import (
+	"calculator"
+	"fmt"
+)
+
+func main() {
+	total := calculator.Sum(3, 5)
+	fmt.Println(total)
+	fmt.Println("Version: ", calculator.Version)
+}
+
+
8
+Version:  1.0
+

引用外部(第三方)包

+

有时,程序需要引用其他开发人员编写的包。

+

测试后不是很明白,基本上是在主文件和 .mod文件中写入包的名称和版本即可。然后根据控制台的输出将包安装好即可使用

+

main.go

+
package main
+
+import (
+	"calculator"
+	"fmt"
+
+	"rsc.io/quote"
+)
+
+func main() {
+	total := calculator.Sum(3, 5)
+	fmt.Println(total)
+	fmt.Println("Version: ", calculator.Version)
+	fmt.Println(quote.Hello())
+}
+

go.mod

+
module helloworld
+
+go 1.19
+
+require (
+	calculator v1.0.0
+	rsc.io/quote v1.5.2
+)
+
+require (
+	golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
+	rsc.io/sampler v1.3.0 // indirect
+)
+
+replace calculator => ../calculator
+

输出:

+
8
+Version:  1.0
+Ahoy, world!
+

使用控制流

+

if 语句的语法

+

与其他编程语言不同的是,在 Go 中,你不需要在条件中使用括号。 else 子句可选。 但是,大括号仍然是必需的。 此外,为了减少行,Go 不支持三元 if 语句,因此每次都需要编写完整的 if 语句。

+
package main
+
+import "fmt"
+
+func givemeanumber() int {
+	return -1
+}
+
+func main() {
+	if num := givemeanumber(); num < 0 {
+		fmt.Println(num, "is negative")
+	} else if num < 10 {
+		fmt.Println(num, "has only one digit")
+	} else {
+		fmt.Println(num, "has multiple digits")
+	}
+}
+

其中,有一个在 Go 中常见的约定进行高效编程的方式 if num := givemeanumber(); num < 0,同时接收函数的返回值,但是不重复进行接收,然后使用到if语句中进行判断。当然这个 num变量在 if的外部是无法使用的。

+

使用 switch 语句控制流

+

像其他编程语言一样,Go 支持 switch 语句。 可以使用 switch 语句来避免链接多个 if 语句。 使用 switch 语句,就不需维护和读取包含多个 if 语句的代码。 这些语句还可以让复杂的条件更易于构造。 请参阅以下部分的 switch 语句。

+

普通的switch语句:

+
package main
+
+import (
+	"fmt"
+	"math/rand"
+	"time"
+)
+
+func main() {
+	sec := time.Now().Unix()
+	rand.Seed(sec)
+	i := rand.Int31n(10)
+
+	switch i {
+	case 0:
+		fmt.Print("zero...")
+	case 1:
+		fmt.Print("one...")
+	case 2:
+		fmt.Print("two...")
+	default:
+		fmt.Print("no match...")
+	}
+
+	fmt.Println("ok")
+}
+

有时,多个表达式仅与一个 case 语句匹配。 在 Go 中,如果希望 case 语句包含多个表达式,请使用逗号 (,) 来分隔表达式。 此方法可避免代码重复。

+
package main
+
+import "fmt"
+
+func location(city string) (string, string) {
+	var region string
+	var continent string
+	switch city {
+	case "Delhi", "Hyderabad", "Mumbai", "Chennai", "Kochi":
+		region, continent = "India", "Asia"
+	case "Lafayette", "Louisville", "Boulder":
+		region, continent = "Colorado", "USA"
+	case "Irvine", "Los Angeles", "San Diego":
+		region, continent = "California", "USA"
+	default:
+		region, continent = "Unknown", "Unknown"
+	}
+	return region, continent
+}
+func main() {
+	region, continent := location("Irvine")
+	fmt.Printf("John works in %s, %s\n", region, continent)
+}
+
+
John works in California, USA
+

case 语句的表达式中包含的值对应于 switch 语句验证的变量的数据类型。

+

调用函数

+

switch 还可以调用函数。 在该函数中,可以针对可能的返回值编写 case 语句。

+

第一种是在switch上调用函数,对返回值进行判断

+
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	switch time.Now().Weekday().String() {
+	case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
+		fmt.Println("It's time to learn some Go.")
+	default:
+		fmt.Println("It's weekend, time to rest!")
+	}
+
+	fmt.Println(time.Now().Weekday().String())
+}
+
+
It's time to learn some Go.
+Wednesday
+

第二种是在case上调用函数

+
package main
+
+import (
+	"fmt"
+	"regexp"
+)
+
+func main() {
+	var email = regexp.MustCompile(`^[^@]+@[^@.]+\.[^@.]+`)
+	var phone = regexp.MustCompile(`^[(]?[0-9][0-9][0-9][). \-]*[0-9][0-9][0-9][.\-]?[0-9][0-9][0-9][0-9]`)
+
+	contact := "foo@bar.com"
+
+	switch {
+	case email.MatchString(contact):
+		fmt.Println(contact, "is an email")
+	case phone.MatchString(contact):
+		fmt.Println(contact, "is a phone number")
+	default:
+		fmt.Println(contact, "is not recognized")
+	}
+}
+
+
foo@bar.com is an email
+

上面的 switch 语句中省略了条件,就像在 if 语句中那样。 此模式类似于比较 true 值,就像强制 switch 语句一直运行一样。

+

一个条件 switch 块比一长串的 ifelse if 语句更易于维护。

+

使逻辑进入到下一个 case

+

在某些编程语言中,你会在每个 case 语句末尾写一个 break 关键字。 但在 Go 中,当逻辑进入某个 case 时,它会退出 switch 块,除非你显式停止它。 若要使逻辑进入到下一个紧邻的 case,请使用 fallthrough 关键字。

+
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	switch num := 15; {
+	case num < 50:
+		fmt.Printf("%d is less than 50\n", num)
+		fallthrough
+	case num > 100:
+		fmt.Printf("%d is greater than 100\n", num)
+		fallthrough
+	case num < 200:
+		fmt.Printf("%d is less than 200\n", num)
+	}
+}
+
+
15 is less than 50
+15 is greater than 100
+15 is less than 200
+

请注意,由于 num 为 15(小于 50),因此它与第一个 case 匹配。 但是,num 不大于 100。 由于第一个 case 语句包含 fallthrough 关键字,因此逻辑会立即转到下一个 case 语句,而不会对该 case 进行验证。 因此,在使用 fallthrough 关键字时必须谨慎。 该代码产生的行为可能不是你想要的。

+

for 表达式

+

另一个常用控制流是循环。 Go 只使用一个循环构造,即 for 循环。 但是,你可以通过多种方式表示循环。

+
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	sum := 0
+	for i := 1; i <= 100; i++ {
+		sum += i
+	}
+	fmt.Println("sum of 1..100 is", sum)
+}
+
+
sum of 1..100 is 5050
+

空预处理语句和后处理语句

+
package main
+
+import (
+	"fmt"
+	"math/rand"
+	"time"
+)
+
+func main() {
+	var num int64
+	rand.Seed(time.Now().Unix())
+	for num != 5 {
+		num = rand.Int63n(15)
+		fmt.Println(num)
+	}
+}
+

只要 num 变量保存的值与 5 不同,程序就会输出一个随机数。

+

无限循环和 break 语句

+

可以在 Go 中编写的另一种循环模式是无限循环。 在这种情况下,你不编写条件表达式,也不编写预处理语句或后处理语句, 而是采取退出循环的方式进行编写。 否则,逻辑永远都不会退出。 若要使逻辑退出循环,请使用 break 关键字。

+
package main
+
+import (
+	"fmt"
+	"math/rand"
+	"time"
+)
+
+func main() {
+	var num int32
+	sec := time.Now().Unix()
+	rand.Seed(sec)
+
+	for {
+		fmt.Print("Writing inside the loop...")
+		if num = rand.Int31n(10); num == 5 {
+			fmt.Println("finish!")
+			break
+		}
+		fmt.Println(num)
+	}
+}
+
+

在 Go 中,可以使用 continue 关键字跳过循环的当前迭代。 例如,可以使用此关键字在循环继续之前运行验证。 也可以在编写无限循环并需要等待资源变得可用时使用它。

+
package main
+
+import "fmt"
+
+func main() {
+	sum := 0
+	for num := 1; num <= 100; num++ {
+		if num%5 == 0 {
+			continue
+		}
+		sum += num
+	}
+	fmt.Println("The sum of 1 to 100, but excluding numbers divisible by 5, is", sum)
+}
+
+
The sum of 1 to 100, but excluding numbers divisible by 5, is 4000
+

使用 defer、panic 和 recover 函数进行控制

+

defer 函数

+

在 Go 中,defer 语句会推迟函数(包括任何参数)的运行,直到包含 defer 语句的函数完成。 通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟某个函数的运行。

+

可以根据需要推迟任意多个函数。 defer 语句按逆序运行,先运行最后一个,最后运行第一个。

+
package main
+
+import "fmt"
+
+func main() {
+	for i := 1; i <= 4; i++ {
+		defer fmt.Println("deferred", -i)
+		fmt.Println("regular", i)
+	}
+}
+
+
regular 1
+regular 2
+regular 3
+regular 4
+deferred -4
+deferred -3
+deferred -2
+deferred -1
+

在此示例中,请注意,每次推迟 fmt.Println("deferred", -i) 时,都会存储 i 的值,并会将其运行任务添加到队列中。 在 main() 函数输出完 regular 值后,所有推迟的调用都会运行。 这就是你看到输出采用逆序(后进先出)的原因。

+

defer 函数的一个典型用例是在使用完文件后将其关闭。

+
package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+)
+
+func main() {
+	newfile, error := os.Create("learnGo.txt")
+	if error != nil {
+		fmt.Println("Error: Could not create file.")
+		return
+	}
+	defer newfile.Close()
+
+	if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
+		fmt.Println("Error: Could not write to file.")
+		return
+	}
+
+	newfile.Sync()
+}
+
+

创建或打开某个文件后,可以推迟 .Close() 函数的执行,以免在你完成后忘记关闭该文件。

+

panic 函数

+

运行时错误会使 Go 程序崩溃,例如尝试通过使用超出范围的索引或取消引用 nil 指针来访问数组。 你也可以强制程序崩溃。

+

内置 panic() 函数可以停止 Go 程序中的正常控制流。 当你使用 panic 调用时,任何延迟的函数调用都将正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。 此消息包含错误信息和堆栈跟踪,有助于诊断问题的根本原因。

+

调用 panic() 函数时,可以添加任何值作为参数。 通常,你会发送一条错误消息,说明为什么会进入紧急状态。

+

例如,下面的代码将 panicdefer 函数组合在一起。 尝试运行此代码以了解控制流的中断。 请注意,清理过程仍会运行。

+
package main
+
+import "fmt"
+
+func highlow(high int, low int) {
+	if high < low {
+		fmt.Println("Panic!")
+		panic("highlow() low greater than high")
+	}
+	defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
+	fmt.Println("Call: highlow(", high, ",", low, ")")
+
+	highlow(high, low+1)
+}
+
+func main() {
+	highlow(2, 0)
+	fmt.Println("Program finished successfully!")
+}
+
+
Call: highlow( 2 , 0 )
+Call: highlow( 2 , 1 )
+Call: highlow( 2 , 2 )
+Panic!
+Deferred: highlow( 2 , 2 )
+Deferred: highlow( 2 , 1 )
+Deferred: highlow( 2 , 0 )
+panic: highlow() low greater than high
+
+goroutine 1 [running]:
+main.highlow(0x4b8018?, 0xc000012018?)
+        /mnt/d/Programming_Design/Go/src/helloworld/main.go:8 +0x285
+main.highlow(0x2, 0x2)
+        /mnt/d/Programming_Design/Go/src/helloworld/main.go:13 +0x211
+main.highlow(0x2, 0x1)
+        /mnt/d/Programming_Design/Go/src/helloworld/main.go:13 +0x211
+main.highlow(0x2, 0x0)
+        /mnt/d/Programming_Design/Go/src/helloworld/main.go:13 +0x211
+main.main()
+        /mnt/d/Programming_Design/Go/src/helloworld/main.go:17 +0x25
+exit status 2
+

下面是运行代码时会发生的情况:

+
    +
  1. 一切正常运行。 程序将输出传递到 highlow() 函数中的高值和低值。
  2. +
  3. 如果 low 的值大于 high 的值,则程序会崩溃。 会显示“Panic!”消息。 此时,控制流中断,所有推迟的函数都开始输出“Deferred...”消息。
  4. +
  5. 程序崩溃,并显示完整的堆栈跟踪。 不会显示“Program finished successfully!”消息。
  6. +
+

recover 函数

+

有时,你可能想要避免程序崩溃,改为在内部报告错误。 或者,你可能想要先清理混乱情况,然后再让程序崩溃。 例如,你可能想要关闭与某个资源的连接,以免出现更多问题。

+

Go 提供内置 recover() 函数,让你可以在程序崩溃之后重新获得控制权。 你只会在你同时调用 defer 的函数中调用 recover。 如果调用 recover() 函数,则在正常运行的情况下,它会返回 nil,没有任何其他作用。

+

尝试修改前面的代码中的 main 函数,以添加对 recover() 的调用,如下所示:

+
package main
+
+import "fmt"
+
+func highlow(high int, low int) {
+	if high < low {
+		fmt.Println("Panic!")
+		panic("highlow() low greater than high")
+	}
+	defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
+	fmt.Println("Call: highlow(", high, ",", low, ")")
+
+	highlow(high, low+1)
+}
+
+func main() {
+	defer func() {
+		handler := recover()
+		if handler != nil {
+			fmt.Println("main(): recover", handler)
+		}
+	}()
+
+	highlow(2, 0)
+	fmt.Println("Program finished successfully!")
+}
+
+
Call: highlow( 2 , 0 )
+Call: highlow( 2 , 1 )
+Call: highlow( 2 , 2 )
+Panic!
+Deferred: highlow( 2 , 2 )
+Deferred: highlow( 2 , 1 )
+Deferred: highlow( 2 , 0 )
+main(): recover highlow() low greater than high
+

main() 函数中,你会将一个可以调用 recover() 函数的匿名函数推迟。 当程序处于紧急状态时,对 recover() 的调用无法返回 nil。 你可以在此处执行一些操作来清理混乱,但在本例中,你只是简单地输出一些内容。

+

panicrecover 函数的组合是 Go 处理异常的惯用方式。 其他编程语言使用 try/catch 块。 Go 首选此处所述的方法。

+

练习 - 在 Go 中使用控制流

+

编写 FizzBuzz 程序

+

首先,编写一个用于输出数字(1 到 100)的程序,其中有以下变化:

+
    +
  • 如果数字可被 3 整除,则输出 Fizz
  • +
  • 如果数字可被 5 整除,则输出 Buzz
  • +
  • 如果数字可同时被 3 和 5 整除,则输出 FizzBuzz
  • +
  • 如果前面的情况都不符合,则输出该数字。
  • +
+

尝试使用 switch 语句。

+
package main
+
+import (
+    "fmt"
+    "strconv"
+)
+
+func fizzbuzz(num int) string {
+    switch {
+    case num%15 == 0:
+        return "FizzBuzz"
+    case num%3 == 0:
+        return "Fizz"
+    case num%5 == 0:
+        return "Buzz"
+    }
+    return strconv.Itoa(num)
+}
+
+func main() {
+    for num := 1; num <= 100; num++ {
+        fmt.Println(fizzbuzz(num))
+    }
+}
+

查找质数

+

编写一个程序来查找小于 20 的所有质数。 质数是大于 1 的任意数字,只能被它自己和 1 整除。 “整除”表示经过除法运算后没有余数。 与大多数编程语言一样,Go 还提供了一种方法来检查除法运算是否产生余数。 我们可以使用模数 %(百分号)运算符。

+

在本练习中,你将更新一个名为 findprimes 的函数,以检查数值是否为质数。 该函数有一个整数参数,并返回一个布尔值。 函数通过检查是否有余数来测试输入数字是否为质数。 如果数字为质数,则该函数返回 true。

+
package main
+
+import "fmt"
+
+func findprimes(number int) bool {
+	for i := 2; i < number; i++ {
+		if number%i == 0 {
+			return false
+		}
+	}
+
+	if number > 1 {
+		return true
+	} else {
+		return false
+	}
+}
+
+func main() {
+	fmt.Println("Prime numbers less than 20:")
+
+	for number := 1; number < 20; number++ {
+		if findprimes(number) {
+			fmt.Printf("%v ", number)
+		}
+	}
+	fmt.Println()
+}
+
Prime numbers less than 20:
+2 3 5 7 11 13 17 19
+

要求用户输入一个数字,如果该数字为负数,则进入紧急状态

+

编写一个要求用户输入一个数字的程序。 在开始时使用以下代码片段:

+

此程序要求用户输入一个数字,然后将其输出。 修改示例代码,使之符合以下要求:

+
    +
  • 持续要求用户输入一个整数。 此循环的退出条件应该是用户输入了一个负数。
  • +
  • 当用户输入负数时,让程序崩溃。 然后输出堆栈跟踪错误。
  • +
  • 如果数字为 0,则输出“0 is neither negative nor positive”。 继续要求用户输入数字。
  • +
  • 如果数字为正数,则输出“You entered: X”(其中的 X 为输入的数字)。 继续要求用户输入数字。
  • +
+
package main
+
+import "fmt"
+
+func main() {
+	val := 0
+	for {
+		fmt.Print("Enter number: ")
+		fmt.Scanf("%d", &val)
+		if val < 0 {
+			panic("Negative!")
+		} else if val == 0 {
+			fmt.Println("0 is neither negative nor positive")
+		} else {
+			fmt.Printf("You entered: %d\n", val)
+		}
+	}
+}
+

使用数组

+

Go 中的数组是一种特定类型且长度固定的数据结构。 它们可具有零个或多个元素,你必须在声明或初始化它们时定义大小。 此外,它们一旦创建,就无法调整大小。 鉴于这些原因,数组在 Go 程序中并不常用,但它们是切片和映射的基础。

+

声明数组

+

要在 Go 中声明数组,必须定义其元素的数据类型以及该数组可容纳的元素数目。 然后,可采用下标表示法访问数组中的每个元素,其中第一个元素是 0,最后一个元素是数组长度减去 1(长度 - 1)。

+
package main
+
+import "fmt"
+
+func main() {
+	var a [3]int
+	a[1] = 10
+	fmt.Println(a[0])
+	fmt.Println(a[1])
+	fmt.Println(a[len(a)-1])
+}
+
0
+10
+0
+

已声明的数组访问其元素时不会遇到错误。 默认情况下,Go 会用默认数据类型初始化每个元素。 这样的话,int 的默认值为零。 不过,你可为特定位置分配值。 这就是为什么你会看到 a[1] = 10。 你可采用上述表示法来访问该元素。 另请注意,为了打印出第一个元素,我们使用了 a[0]。 为了打印出最后一个元素,我们使用了 a[len(a)-1]len 函数是 Go 中的内置函数,用于获取数组、切片或映射中的元素数。

+

初始化数组

+

声明数组时,还可使用非默认值来初始化数组。

+
package main
+
+import "fmt"
+
+func main() {
+	cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}
+	fmt.Println("Cities:", cities)
+}
+
Cities: [New York Paris Berlin Madrid ]
+

数组中的省略号

+

如果你不知道你将需要多少个位置,但知道你将具有多少数据,那么还有一种声明和初始化数组的方法是使用省略号 (...)

+
package main
+
+import "fmt"
+
+func main() {
+	cities := [...]string{"New York", "Paris", "Berlin", "Madrid"}
+	fmt.Println("Cities:", cities)
+}
+

另一种有趣的数组初始化方法是使用省略号并仅为最新的位置指定值。

+
package main
+
+import "fmt"
+
+func main() {
+	numbers := [...]int{99: -1}
+	fmt.Println("First Position:", numbers[0])
+	fmt.Println("Last Position:", numbers[99])
+	fmt.Println("Length:", len(numbers))
+}
+
+
First Position: 0
+Last Position: -1
+Length: 100
+

注意数组的长度是 100,因为你为第 99 个位置指定了一个值。

+

多维数组

+

如果需要处理复杂数据结构,请记住 Go 支持多维数组。

+
package main
+
+import "fmt"
+
+func main() {
+	var twoD [3][5]int
+	for i := 0; i < 3; i++ {
+		for j := 0; j < 5; j++ {
+			twoD[i][j] = (i + 1) * (j + 1)
+		}
+		fmt.Println("Row", i, twoD[i])
+	}
+	fmt.Println("\nAll at once:", twoD)
+}
+
+
Row 0 [1 2 3 4 5]
+Row 1 [2 4 6 8 10]
+Row 2 [3 6 9 12 15]
+
+All at once: [[1 2 3 4 5] [2 4 6 8 10] [3 6 9 12 15]]
+

了解切片

+

与数组一样,切片也是 Go 中的一种数据类型,它表示一系列类型相同的元素。 不过,与数组更重要的区别是切片的大小是动态的,不是固定的。

+

切片是数组或另一个切片之上的数据结构。 我们将源数组或切片称为基础数组。 通过切片,可访问整个基础数组,也可仅访问部分元素。

+

切片只有 3 个组件:

+
    +
  • 指向基础数组中第一个可访问元素的指针 。 此元素不一定是数组的第一个元素 array[0]
  • +
  • 切片的长度 。 切片中的元素数目。
  • +
  • 切片的容量 。 切片开头与基础数组结束之间的元素数目。
  • +
+

声明和初始化切片

+

要声明切片,可采用与声明数组相同的方式操作。

+
package main
+
+import "fmt"
+
+func main() {
+	months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
+	fmt.Println(months)
+	fmt.Println("Length:", len(months))
+	fmt.Println("Capacity:", cap(months))
+}
+
[January February March April May June July August September October November December]
+Length: 12
+Capacity: 12
+

切片项

+

Go 支持切片运算符 s[i:p],其中:

+
    +
  • s 表示数组。
  • +
  • i 表示指向要添加到新切片的基础数组(或另一个切片)的第一个元素的指针。 变量 i 对应于数组 array[i] 中索引位置 i 处的元素。 请记住,此元素不一定是基础数组的第一个元素 array[0]
  • +
  • p 表示创建新切片时要使用的基础数组中的元素数目。 变量 p 对应于可用于新切片的基础数组中的最后一个元素。 可在位置 array[i+1] 找到基础数组中位置 p 处的元素。 请注意,此元素不一定是基础数组的最后一个元素 array[len(array)-1]
  • +
+
package main
+
+import "fmt"
+
+func main() {
+	months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
+	quarter1 := months[0:3]
+	quarter2 := months[3:6]
+	quarter3 := months[6:9]
+	quarter4 := months[9:12]
+	fmt.Println(quarter1, len(quarter1), cap(quarter1))
+	fmt.Println(quarter2, len(quarter2), cap(quarter2))
+	fmt.Println(quarter3, len(quarter3), cap(quarter3))
+	fmt.Println(quarter4, len(quarter4), cap(quarter4))
+}
+
[January February March] 3 12
+[April May June] 3 9
+[July August September] 3 6
+[October November December] 3 3
+

注意,切片的长度不变,但容量不同。 我们来了解 quarter2 切片。 声明此切片时,你指出希望切片从位置编号 3 开始,最后一个元素位于位置编号 6。 切片长度为 3 个元素,但容量为 9,原因是基础数组有更多元素或位置可供使用,但对切片而言不可见。

+

切片容量仅指出切片可扩展的程度。 因此可从 quarter2 创建扩展切片

+
package main
+
+import "fmt"
+
+func main() {
+	months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
+	quarter2 := months[3:6]
+	quarter2Extended := quarter2[:4]
+	fmt.Println(quarter2, len(quarter2), cap(quarter2))
+	fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
+}
+
[April May June] 3 9
+[April May June July] 4 9
+

追加项

+

切片与数组之间有何不同? 第一个区别是切片的大小不是固定的,而是动态的。 创建切片后,可向其添加更多元素,这样切片就会扩展。

+

Go 提供了内置函数 append(slice, element),便于你向切片添加元素。 将要修改的切片和要追加的元素作为值发送给该函数。 然后,append 函数会返回一个新的切片,将其存储在变量中。 对于要更改的切片,变量可能相同。

+
package main
+
+import "fmt"
+
+func main() {
+	var numbers []int
+	for i := 0; i < 10; i++ {
+		numbers = append(numbers, i)
+		fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
+	}
+}
+
0       cap=1   [0]
+1       cap=2   [0 1]
+2       cap=4   [0 1 2]
+3       cap=4   [0 1 2 3]
+4       cap=8   [0 1 2 3 4]
+5       cap=8   [0 1 2 3 4 5]
+6       cap=8   [0 1 2 3 4 5 6]
+7       cap=8   [0 1 2 3 4 5 6 7]
+8       cap=16  [0 1 2 3 4 5 6 7 8]
+9       cap=16  [0 1 2 3 4 5 6 7 8 9]
+

当切片容量不足以容纳更多元素时,Go 的容量将翻倍。 它将新建一个具有新容量的基础数组。 无需执行任何操作即可使容量增加。 Go 会自动扩充容量。

+

删除项

+

Go 没有内置函数用于从切片中删除元素。 可使用上述切片运算符 s[i:p] 来新建一个仅包含所需元素的切片。

+
package main
+
+import "fmt"
+
+func main() {
+	letters := []string{"A", "B", "C", "D", "E"}
+	remove := 2
+
+	if remove < len(letters) {
+
+		fmt.Println("Before", letters, "Remove ", letters[remove])
+
+		letters = append(letters[:remove], letters[remove+1:]...)
+
+		fmt.Println("After", letters)
+	}
+
+}
+
Before [A B C D E] Remove  C
+After [A B D E]
+

创建切片的副本

+

Go 具有内置函数 copy(dst, src []Type) 用于创建切片的副本。

+

更改切片中的元素时,基础数组将随之更改。

+
package main
+
+import "fmt"
+
+func main() {
+	letters := []string{"A", "B", "C", "D", "E"}
+	fmt.Println("Before", letters)
+
+	slice1 := letters[0:2]
+	slice2 := letters[1:4]
+
+	slice1[1] = "Z"
+
+	fmt.Println("After", letters)
+	fmt.Println("Slice2", slice2)
+}
+
+
Before [A B C D E]
+After [A Z C D E]
+Slice2 [Z C D]
+

创建副本则不会产生影响

+
package main
+
+import "fmt"
+
+func main() {
+	letters := []string{"A", "B", "C", "D", "E"}
+	fmt.Println("Before", letters)
+
+	slice1 := letters[0:2]
+
+	slice2 := make([]string, 3)
+	copy(slice2, letters[1:4])
+
+	slice1[1] = "Z"
+
+	fmt.Println("After", letters)
+	fmt.Println("Slice2", slice2)
+}
+
Before [A B C D E]
+After [A Z C D E]
+Slice2 [B C D]
+

使用映射

+

Go 中的映射是一个哈希表,是键值对的集合。 映射中所有的键都必须具有相同的类型,它们的值也是如此。 不过,可对键和值使用不同的类型。 例如,键可以是数字,值可以是字符串。 若要访问映射中的特定项,可引用该项的键。

+

声明和初始化映射

+

若要声明映射,需要使用 map 关键字。 然后,定义键和值类型,如下所示:map[T]T

+
package main
+
+import "fmt"
+
+func main() {
+	studentsAge := map[string]int{
+		"john": 32,
+		"bob":  31,
+	}
+	fmt.Println(studentsAge)
+}
+
+
map[bob:31 john:32]
+

如果不想使用项来初始化映射,可使用内置函数 make() 在上一部分创建切片。

+

添加项

+

要添加项,无需像对切片一样使用内置函数。 映射更加简单。 你只需定义键和值即可。 如果没有键值对,则该项会添加到映射中。

+
package main
+
+import "fmt"
+
+func main() {
+	studentsAge := make(map[string]int)
+	studentsAge["john"] = 32
+	studentsAge["bob"] = 31
+	fmt.Println(studentsAge)
+}
+

访问项

+

若要访问映射中的项,可使用常用的下标表示法 m[key]

+

在映射中使用下标表示法时,即使映射中没有键,你也总会获得默认值的响应。

+
package main
+
+import "fmt"
+
+func main() {
+	studentsAge := make(map[string]int)
+	studentsAge["john"] = 32
+	studentsAge["bob"] = 31
+	fmt.Println("Bob's age is", studentsAge["bob"])
+	fmt.Println("Christy's age is", studentsAge["christy"])
+}
+
Bob's age is 31
+Christy's age is 0
+

在很多情况下,访问映射中没有的项时 Go 不会返回错误,这是正常的。 但有时需要知道某个项是否存在。 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。

+
package main
+
+import "fmt"
+
+func main() {
+	studentsAge := make(map[string]int)
+	studentsAge["john"] = 32
+	studentsAge["bob"] = 31
+
+	age, exist := studentsAge["christy"]
+	if exist {
+		fmt.Println("Christy's age is", age)
+	} else {
+		fmt.Println("Christy's age couldn't be found")
+	}
+}
+
+
Christy's age couldn't be found
+

删除项

+

若要从映射中删除项,请使用内置函数 delete()

+
package main
+
+import "fmt"
+
+func main() {
+	studentsAge := make(map[string]int)
+	studentsAge["john"] = 32
+	studentsAge["bob"] = 31
+	delete(studentsAge, "bob")
+	delete(studentsAge, "christy")
+	fmt.Println(studentsAge)
+}
+
+
map[john:32]
+

如果你尝试删除不存在的项,Go 不会执行 panic

+

映射中的循环

+

最后,让我们看看如何在映射中进行循环来以编程方式访问其所有的项。 为此,可使用基于范围的循环

+
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	studentsAge := make(map[string]int)
+	studentsAge["john"] = 32
+	studentsAge["bob"] = 31
+	for name, age := range studentsAge {
+		fmt.Printf("%s\t%d\n", name, age)
+	}
+}
+
+
john    32
+bob     31
+

range 会首先生成项的键,然后再生成该项的值。 可使用 _ 变量忽略其中任何一个

+

使用结构

+

有时,你需要在一个结构中表示字段的集合。在 Go 中,可使用结构将可能构成记录的不同字段组合在一起。Go 中的结构也是一种数据结构,它可包含零个或多个任意类型的字段,并将它们表示为单个实体。

+

声明和初始化结构

+

若要声明结构,需要使用 struct 关键字,还要使用希望新的数据类型具有的字段及其类型的列表。

+

若要访问结构的各个字段,可使用点表示法 (.) 做到这一点

+

可使用 & 运算符生成指向结构的指针以修改结构中的项

+
package main
+
+import "fmt"
+
+type Employee struct {
+	ID        int
+	FirstName string
+	LastName  string
+	Address   string
+}
+
+func main() {
+	employee := Employee{LastName: "Doe", FirstName: "John"}
+	fmt.Println(employee)
+	employeeCopy := &employee
+	employeeCopy.FirstName = "David"
+	fmt.Println(employee)
+}
+
{0 John Doe }
+{0 David Doe }
+

结构嵌入

+

通过 Go 中的结构,可将某结构嵌入到另一结构中。

+
package main
+
+import "fmt"
+
+type Person struct {
+	ID        int
+	FirstName string
+	LastName  string
+	Address   string
+}
+
+type Employee struct {
+	Person
+	ManagerID int
+}
+
+type Contractor struct {
+	Person
+	CompanyID int
+}
+
+func main() {
+	employee := Employee{
+		Person: Person{
+			FirstName: "John",
+		},
+	}
+	employee.LastName = "Doe"
+	employee.ManagerID = 2
+	fmt.Println(employee)
+}
+
+
{{0 John Doe } 2}
+

用 JSON 编码和解码结构

+

最后,可使用结构来对 JSON 中的数据进行编码和解码。 Go 对 JSON 格式提供很好的支持,该格式已包含在标准库包中。

+

你还可执行一些操作,例如重命名结构中字段的名称。 例如,假设你不希望 JSON 输出显示 FirstName 而只显示 name,或者忽略空字段, 可使用如下例所示的字段标记:

+

然后,若要将结构编码为 JSON,请使用 json.Marshal 函数。 若要将 JSON 字符串解码为数据结构,请使用 json.Unmarshal 函数。 下例将所有内容组合在一起,将员工数组编码为 JSON,并将输出解码为新的变量:

+
package main
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type Person struct {
+	ID        int
+	FirstName string `json:"name"`
+	LastName  string
+	Address   string `json:"address,omitempty"`
+}
+
+type Employee struct {
+	Person
+	ManagerID int
+}
+
+type Contractor struct {
+	Person
+	CompanyID int
+}
+
+func main() {
+	employees := []Employee{
+		{
+			Person: Person{
+				LastName: "Doe", FirstName: "John",
+			},
+		},
+		{
+			Person: Person{
+				LastName: "Campbell", FirstName: "David",
+			},
+		},
+	}
+
+	data, _ := json.Marshal(employees)
+	fmt.Printf("%s\n", data)
+
+	var decoded []Employee
+	json.Unmarshal(data, &decoded)
+	fmt.Printf("%v\n", decoded)
+}
+
[{"ID":0,"name":"John","LastName":"Doe","ManagerID":0},{"ID":0,"name":"David","LastName":"Campbell","ManagerID":0}]
+[{{0 John Doe } 0} {{0 David Campbell } 0}]
+

练习 - 数据类型

+

编写一个程序来计算斐波纳契数列

+

在这第一个挑战中,你将编写一个程序来计算某个数字的斐波纳契数列。 这是在学习新语言时要编码的一个典型的编程练习。 你将编写一个函数,它返回一个包含按斐波纳契数列排列的所有数字的切片,而这些数字是通过根据用户输入的大于 2 的数字计算得到的。 让我们假设小于 2 的数字会导致错误,并返回一个 nil 切片。

+

请记住,斐波那契数列是一个数字列表,其中每个数字是前两个斐波那契数字之和。 例如,数字 6 的序列是 1,1,2,3,5,8,数字 7 的序列是 1,1,2,3,5,8,13,数字 8 的序列是 1,1,2,3,5,8,13,21,以此类推。

+
package main
+
+import "fmt"
+
+func fibonacci(n int) []int {
+	if n < 2 {
+		return make([]int, 0)
+	}
+
+	nums := make([]int, n)
+	nums[0], nums[1] = 1, 1
+
+	for i := 2; i < n; i++ {
+		nums[i] = nums[i-1] + nums[i-2]
+	}
+
+	return nums
+}
+
+func main() {
+	var num int
+
+	fmt.Print("What's the Fibonacci sequence you want? ")
+	fmt.Scanln(&num)
+	fmt.Println("The Fibonacci sequence is:", fibonacci(num))
+}
+
+

创建罗马数字转换器

+

编写一个程序来转换罗马数字(例如将 MCLX 转换成 1,160)。 使用映射加载要用于将字符串字符转换为数字的基本罗马数字。 例如,M 将是映射中的键,其值将为 1000。 使用以下字符串字符映射表列表:

+
    +
  • M => 1000
  • +
  • D => 500
  • +
  • C => 100
  • +
  • L => 50
  • +
  • X => 10
  • +
  • V => 5
  • +
  • I => 1
  • +
+

如果用户输入的字母与上述列表中的不同,则打印一个错误。

+

请记住在有些情况下,较小的数字会排在较大的数字前面,因此不能仅仅将数字相加。 例如,数字 MCM 应打印为 1,900

+
package main
+
+import (
+	"fmt"
+)
+
+func romanToArabic(numeral string) int {
+	romanMap := map[rune]int{
+		'M': 1000,
+		'D': 500,
+		'C': 100,
+		'L': 50,
+		'X': 10,
+		'V': 5,
+		'I': 1,
+	}
+
+	arabicVals := make([]int, len(numeral)+1)
+
+	for index, digit := range numeral {
+		if val, present := romanMap[digit]; present {
+			arabicVals[index] = val
+		} else {
+			fmt.Printf("Error: The roman numeral %s has a bad digit: %c\n", numeral, digit)
+			return 0
+		}
+	}
+
+	total := 0
+
+	for index := 0; index < len(numeral); index++ {
+		if arabicVals[index] < arabicVals[index+1] {
+			arabicVals[index] = -arabicVals[index]
+		}
+		total += arabicVals[index]
+	}
+
+	return total
+}
+func main() {
+	fmt.Println("MCLX is", romanToArabic("MCLX"))
+	fmt.Println("MCMXCIX is ", romanToArabic("MCMXCIX"))
+	fmt.Println("MCMZ is", romanToArabic("MCMZ"))
+}
+
+

如何在 Go 中处理错误

+

编写程序时,需要考虑程序失败的各种方式,并且需要管理失败。 无需让用户看到冗长而混乱的堆栈跟踪错误。 让他们看到有关错误的有意义的信息更好。 正如你所看到的,Go 具有 panicrecover 之类的内置函数来管理程序中的异常或意外行为。 但错误是已知的失败,你的程序应该可以处理它们。

+

Go 的错误处理方法只是一种只需要 ifreturn 语句的控制流机制。 例如,在调用函数以从 employee 对象获取信息时,可能需要了解该员工是否存在。 Go 处理此类预期错误的一贯方法如下所示:

+
employee, err := getInformation(1000)
+if err != nil {
+    // Something is wrong. Do something.
+}
+

注意 getInformation 函数返回了 employee 结构,还返回了错误作为第二个值。 该错误可能为 nil。 如果错误为 nil,则表示成功。 如果错误不是 nil,则表示失败。 非 nil 错误附带一条错误消息,你可以打印该错误消息,也可以记录该消息(更可取)。

+

错误处理策略

+

当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。

+

你可能还需要在传播错误之前添加更多信息。 为此,可以使用 fmt.Errorf() 函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误,如下所示:

+
func getInformation(id int) (*Employee, error) {
+    employee, err := apiCallEmployee(1000)
+    if err != nil {
+        return nil, fmt.Errorf("got an error when getting the employee information: %v", err)
+    }
+    return employee, nil
+}
+

另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟

+
func getInformation(id int) (*Employee, error) {
+    for tries := 0; tries < 3; tries++ {
+        employee, err := apiCallEmployee(1000)
+        if err == nil {
+            return employee, nil
+        }
+
+        fmt.Println("Server is not responding, retrying ...")
+        time.Sleep(time.Second * 2)
+    }
+
+    return nil, fmt.Errorf("server has failed to respond to get the employee information")
+}
+

创建可重用的错误

+

有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在 Go 中,你可以使用 errors.New() 函数创建错误并在若干部分中重复使用这些错误,如下所示:

+
var ErrNotFound = errors.New("Employee not found!")
+
+func getInformation(id int) (*Employee, error) {
+    if id != 1001 {
+        return nil, ErrNotFound
+    }
+
+    employee := Employee{LastName: "Doe", FirstName: "John"}
+    return &employee, nil
+}
+

最后,如果你具有错误变量,则在处理调用方函数中的错误时可以更具体。 errors.Is() 函数允许你比较获得的错误的类型

+
employee, err := getInformation(1000)
+if errors.Is(err, ErrNotFound) {
+    fmt.Printf("NOT FOUND: %v\n", err)
+} else {
+    fmt.Print(employee)
+}
+

用于错误处理的推荐做法

+

在 Go 中处理错误时,请记住下面一些推荐做法:

+
    +
  • 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
  • +
  • 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
  • +
  • 创建尽可能多的可重用错误变量。
  • +
  • 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
  • +
  • 在记录错误时记录尽可能多的详细信息(我们将在下一部分介绍记录方法),并打印出最终用户能够理解的错误。
  • +
+

如何在 Go 中记录

+

日志在程序中发挥着重要作用,因为它们是在出现问题时你可以检查的信息源。 通常,发生错误时,最终用户只会看到一条消息,指示程序出现问题。 从开发人员的角度来看,我们需要简单错误消息以外的更多信息。 这主要是因为我们想要再现该问题以编写适当的修补程序。

+

log

+

对于初学者,Go 提供了一个用于处理日志的简单标准包。 可以像使用 fmt 包一样使用此包。 该标准包不提供日志级别,且不允许为每个包配置单独的记录器。 如果需要编写更复杂的日志记录配置,可以使用记录框架执行此操作。

+
package main
+
+import (
+	"log"
+)
+
+func main() {
+	log.Print("Hey, I'm a log!")
+}
+
+
2022/10/05 15:37:16 Hey, I'm a log!
+

默认情况下,log.Print() 函数将日期和时间添加为日志消息的前缀。 你可以通过使用 fmt.Print() 获得相同的行为,但使用 log 包还能执行其他操作,例如将日志发送到文件。 稍后我们将详细介绍 log 包功能。

+

你可以使用 log.Fatal() 函数记录错误并结束程序,就像使用 os.Exit(1) 一样。

+
package main
+
+import (
+	"fmt"
+	"log"
+)
+
+func main() {
+	log.Fatal("Hey, I'm an error log!")
+	fmt.Print("Can you see me?")
+}
+
+
2022/10/05 15:38:56 Hey, I'm an error log!
+exit status 1
+

使用 log.Panic() 函数时会出现类似行为,但是还会获取错误堆栈跟踪。

+

另一重要函数是 log.SetPrefix()。 可使用它向程序的日志消息添加前缀。

+
package main
+
+import (
+	"log"
+)
+
+func main() {
+	log.SetPrefix("main(): ")
+	log.Print("Hey, I'm a log!")
+	log.Fatal("Hey, I'm an error log!")
+}
+
+
main(): 2022/10/05 15:40:36 Hey, I'm a log!
+main(): 2022/10/05 15:40:36 Hey, I'm an error log!
+exit status 1
+

记录到文件

+

除了将日志打印到控制台之外,你可能还希望将日志发送到文件,以便稍后或实时处理这些日志。

+
package main
+
+import (
+	"log"
+	"os"
+)
+
+func main() {
+	file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	defer file.Close()
+
+	log.SetOutput(file)
+	log.Print("Hey, I'm a log!")
+}
+

最后,可能有 log 包中的函数不足以处理问题的情况。 你可能会发现,使用记录框架而不编写自己的库很有用。 Go 的几个记录框架有 LogruszerologzapApex

+

在 Go 中使用方法

+

面向对象编程 (OOP) 是一种广受欢迎的编程模式,大部分编程语言都支持(至少部分支持)。 Go 是其中一种语言,但它并不完全支持所有 OOP 原则。

+

Go 中的方法是一种特殊类型的函数,但存在一个简单的区别:你必须在函数名称之前加入一个额外的参数。 此附加参数称为 接收方

+

如你希望分组函数并将其绑定到自定义类型,则方法非常有用。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。

+

声明方法

+

到目前为止,你仅将结构用作可在 Go 中创建的另一种自定义类型。 在此模块中你将了解到,通过添加方法你可以将行为添加到你所创建的结构中。

+

在声明方法之前,必须先创建结构。 假设你想要创建一个几何包,并决定创建一个名为 triangle 的三角形结构作为此程序包的一个组成部分。 然后,你需要使用一种方法来计算此三角形的周长。 你可以在 Go 中将其表示为:

+
type triangle struct {
+	size int
+}
+
+func (t triangle) perimeter() int {
+	return t.size * 3
+}
+

结构看起来像普通结构,但 perimeter() 函数在函数名称之前有一个类型 triangle 的额外参数。 也就是说,在使用结构时,你可以按如下方式调用函数:

+
func main() {
+	t := triangle{3}
+	fmt.Println("Perimeter:", t.perimeter())
+}
+

如果尝试按平常的方式调用 perimeter() 函数,则此函数将无法正常工作,因为此函数的签名表明它需要接收方。 正因如此,调用此方法的唯一方式是先声明一个结构,获取此方法的访问权限。 这也意味着,只要此方法属于不同的结构,你甚至可以为其指定相同的名称。 例如,你可以使用 perimeter() 函数声明一个 square 结构,具体如下所示:

+
package main
+
+import "fmt"
+
+type triangle struct {
+	size int
+}
+
+type square struct {
+	size int
+}
+
+func (t triangle) perimeter() int {
+	return t.size * 3
+}
+
+func (s square) perimeter() int {
+	return s.size * 4
+}
+
+func main() {
+	t := triangle{3}
+	s := square{4}
+	fmt.Println("Perimeter (triangle):", t.perimeter())
+	fmt.Println("Perimeter (square):", s.perimeter())
+}
+
+
Perimeter (triangle): 9
+Perimeter (square): 16
+

通过对 perimeter() 函数的两次调用,编译器将根据接收方类型来确定要调用的函数。 这有助于在各程序包之间保持函数的一致性和名称的简短,并避免将包名称作为前缀。

+

方法中的指针

+

有时,方法需要更新变量,或者,如果参数太大,则可能需要避免复制它。 在遇到此类情况时,你需要使用指针传递变量的地址。 在之前的模块中,当我们在讨论指针时提到,每次在 Go 中调用函数时,Go 都会复制每个参数值以便使用。

+

如果你需要更新方法中的接收方变量,也会执行相同的行为。 例如,假设你要创建一个新方法以使三角形的大小增加一倍。 你需要在接收方变量中使用指针,具体如下所示:

+
func (t *triangle) doubleSize() {
+	t.size *= 2
+}
+

如果方法仅可访问接收方的信息,则不需要在接收方变量中使用指针。 但是,依据 Go 的约定,如果结构的任何方法具有指针接收方,则此结构的所有方法都必须具有指针接收方,即使某个方法不需要也是如此。

+

声明其他类型的方法

+

方法的一个关键方面在于,需要为任何类型定义方法,而不只是针对自定义类型(如结构)进行定义。 但是,你不能通过属于其他包的类型来定义结构。 因此,不能在基本类型(如 string)上创建方法。

+

尽管如此,你仍然可以利用一点技巧,基于基本类型创建自定义类型,然后将其用作基本类型。 例如,假设你要创建一个方法,以将字符串从小写字母转换为大写字母。 你可以按如下所示写入方法:

+
package main
+
+import (
+	"fmt"
+	"strings"
+)
+
+type upperstring string
+
+func (s upperstring) Upper() string {
+	return strings.ToUpper(string(s))
+}
+
+func main() {
+	s := upperstring("Learning Go!")
+	fmt.Println(s)
+	fmt.Println(s.Upper())
+}
+
+

嵌入方法

+

在之前的模块中,您已了解到可以在一个结构中使用属性,并将同一属性嵌入另一个结构中。 也就是说,可以重用来自一个结构的属性,以避免出现重复并保持代码库的一致性。 类似的观点也适用于方法。 即使接收方不同,也可以调用已嵌入结构的方法。

+

例如,假设你想要创建一个带有逻辑的新三角形结构,以加入颜色。 此外,你还希望继续使用之前声明的三角形结构。 然后,你可以初始化 coloredTriangle 结构,并从 triangle 结构调用 perimeter() 方法(甚至访问其字段)

+
package main
+
+import "fmt"
+
+type triangle struct {
+	size int
+}
+
+type coloredTriangle struct {
+	triangle
+	color string
+}
+
+func (t triangle) perimeter() int {
+	return t.size * 3
+}
+
+func main() {
+	t := coloredTriangle{triangle{3}, "blue"}
+	fmt.Println("Size:", t.size)
+	fmt.Println("Perimeter", t.perimeter())
+}
+
+
Size: 3
+Perimeter 9
+

重载方法

+

让我们回到之前讨论过的 triangle 示例。 如果要在 coloredTriangle 结构中更改 perimeter() 方法的实现,会发生什么情况? 不能存在两个同名的函数。 但是,因为方法需要额外参数(接收方),所以,你可以使用一个同名的方法,只要此方法专门用于要使用的接收方即可。 这就是重载方法的方式。

+

如果你仍需要从 triangle 结构调用 perimeter() 方法,则可通过对其进行显示访问来执行此操作

+
package main
+
+import "fmt"
+
+type triangle struct {
+	size int
+}
+
+type coloredTriangle struct {
+	triangle
+	color string
+}
+
+func (t coloredTriangle) perimeter() int {
+	return t.size * 3 * 2
+}
+
+func (t triangle) perimeter() int {
+	return t.size * 3
+}
+
+func main() {
+	t := coloredTriangle{triangle{3}, "blue"}
+	fmt.Println("Size:", t.size)
+	fmt.Println("Perimeter (colored)", t.perimeter())
+	fmt.Println("Perimeter (normal)", t.triangle.perimeter())
+}
+
+

方法中的封装

+

“封装”表示对象的发送方(客户端)无法访问某个方法。 通常,在其他编程语言中,你会将 privatepublic 关键字放在方法名称之前。 在 Go 中,只需使用大写标识符,即可公开方法,使用非大写的标识符将方法设为私有方法

+

Go 中的封装仅在程序包之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。

+
package geometry
+
+type Triangle struct {
+	size int
+}
+
+func (t *Triangle) doubleSize() {
+	t.size *= 2
+}
+
+func (t *Triangle) SetSize(size int) {
+	t.size = size
+}
+
+func (t *Triangle) Perimeter() int {
+	t.doubleSize()
+	return t.size * 3
+}
+
package main
+
+import (
+	"fmt"
+	"geometry"
+)
+
+func main() {
+	t := geometry.Triangle{}
+	t.SetSize(3)
+	fmt.Println("Perimeter", t.Perimeter())
+}
+
+

在 Go 中使用接口

+

Go 中的接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。 在你使用接口时,你的基本代码将变得更加灵活、适应性更强,因为你编写的代码未绑定到特定的实现。 因此,你可以快速扩展程序的功能。

+

与其他编程语言中的接口不同,Go 中的接口是满足隐式实现的。 Go 并不提供用于实现接口的关键字,因此,如果你之前使用的是其他编程语言中的接口,但不熟悉 Go,那么此概念可能会造成混淆。

+

声明接口

+

Go 中的接口是一种抽象类型,只包括具体类型必须拥有或实现的方法。 正因如此,我们说接口类似于蓝图。

+

假设你希望在几何包中创建一个接口来指示形状必须实现的方法。 你可以按如下所示定义接口:

+
type Shape interface {
+    Perimeter() float64
+    Area() float64
+}
+

Shape 接口表示你想要考虑 Shape 的任何类型都需要同时具有 Perimeter()Area() 方法。 例如,在创建 Square 结构时,它必须实现两种方法,而不是仅实现一种。 另外,请注意接口不包含这些方法的实现细节(例如,用于计算某个形状的周长和面积)。 接口仅表示一种协定。 三角形、圆圈和正方形等形状有不同的计算面积和周长方式。

+

实现接口

+

正如上文所讨论的内容,你没有用于实现接口的关键字。 当 Go 中的接口具有接口所需的所有方法时,则满足按类型的隐式实现。

+

让我们创建一个 Square 结构,此结构具有 Shape 接口中的两个方法

+
type Square struct {
+	size float64
+}
+
+func (s Square) Area() float64 {
+	return s.size * s.size
+}
+
+func (s Square) Perimeter() float64 {
+	return s.size * 4
+}
+

请注意 Square 结构的方法签名与 Shape 接口的签名的匹配方式。

+
func main() {
+	var s Shape = Square{3}
+	fmt.Printf("%T\n", s)
+	fmt.Println("Area: ", s.Area())
+	fmt.Println("Perimeter:", s.Perimeter())
+}
+
main.Square
+Area:  9
+Perimeter: 12
+

此时,无论你是否使用接口,都没有任何区别。 接下来,让我们创建另一种类型,如 Circle,然后进行相同的操作:

+
package main
+
+import (
+	"fmt"
+	"math"
+)
+
+type Shape interface {
+	Perimeter() float64
+	Area() float64
+}
+
+type Square struct {
+	size float64
+}
+
+func (s Square) Area() float64 {
+	return s.size * s.size
+}
+
+func (s Square) Perimeter() float64 {
+	return s.size * 4
+}
+
+type Circle struct {
+	radius float64
+}
+
+func (c Circle) Area() float64 {
+	return math.Pi * c.radius * c.radius
+}
+
+func (c Circle) Perimeter() float64 {
+	return 2 * math.Pi * c.radius
+}
+
+func printInformation(s Shape) {
+	fmt.Printf("%T\n", s)
+	fmt.Println("Area: ", s.Area())
+	fmt.Println("Perimeter:", s.Perimeter())
+	fmt.Println()
+}
+
+func main() {
+	var s Shape = Square{3}
+	printInformation(s)
+
+	c := Circle{6}
+	printInformation(c)
+}
+
+
main.Square
+Area:  9
+Perimeter: 12
+
+main.Circle
+Area:  113.09733552923255
+Perimeter: 37.69911184307752
+

使用接口的优点在于,对于 Shape的每个新类型或实现,printInformation 函数都不需要更改。 正如之前所述,当你使用接口时,代码会变得更灵活、更容易扩展。

+

扩展现有实现

+

假设你具有以下代码,并且希望通过编写负责处理某些数据的 Writer 方法的自定义实现来扩展其功能。

+

通过使用以下代码,你可以创建一个程序,此程序使用 GitHub API 从 Microsoft 获取三个存储库:

+
package main
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+)
+
+func main() {
+	resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
+	if err != nil {
+		fmt.Println("Error:", err)
+		os.Exit(1)
+	}
+
+	io.Copy(os.Stdout, resp.Body)
+}
+

改写后:

+
package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+)
+
+type GitHubResponse []struct {
+	FullName string `json:"full_name"`
+}
+
+type customWriter struct{}
+
+func (w customWriter) Write(p []byte) (n int, err error) {
+	var resp GitHubResponse
+	json.Unmarshal(p, &resp)
+	for _, r := range resp {
+		fmt.Println(r.FullName)
+	}
+	return len(p), nil
+}
+
+func main() {
+	resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
+	if err != nil {
+		fmt.Println("Error:", err)
+		os.Exit(1)
+	}
+
+	writer := customWriter{}
+	io.Copy(writer, resp.Body)
+}
+
+

编写自定义服务器 API

+

最后,我们一起来探讨接口的另一种用例,如果你要创建服务器 API,你可能会发现此用例非常实用。 编写 Web 服务器的常用方式是使用 net/http 程序包中的 http.Handler 接口

+
package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+)
+
+// 创建 float32 类型的自定义类型,然后编写 String() 方法的自定义实现
+type dollars float32
+
+func (d dollars) String() string {
+	return fmt.Sprintf("$%.2f", d)
+}
+
+// 写入 http.Handler 可使用的 ServeHTTP 方法的实现。
+
+type database map[string]dollars
+
+// 通过使用 database 类型作为接收方来写入 ServeHTTP 方法。
+func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	for item, price := range db {
+		fmt.Fprintf(w, "%s: %s\n", item, price)
+	}
+}
+
+// 在 main() 函数中,我们将 database 类型实例化,并使用一些值对其进行初始化。 我们使用 http.ListenAndServe 函数启动了 HTTP 服务器,在其中定义了服务器地址,包括要使用的端口和实现 ServerHTTP 方法自定义版本的 db 对象。
+func main() {
+	db := database{"Go T-Shirt": 25, "Go Jacket": 55}
+	log.Fatal(http.ListenAndServe("localhost:8000", db))
+}
+

练习 - 方法和接口

+

创建用于管理在线商店的程序包

+

编写一个程序,此程序使用自定义程序包来管理在线商店的帐户。 你的挑战包括以下四个要素:

+
    +
  1. 创建一个名为 Account 的自定义类型,此类型包含帐户所有者的名字和姓氏。 此类型还必须加入 ChangeName 的功能。
  2. +
  3. 创建另一个名为 Employee 的自定义类型,此类型包含用于将贷方数额存储为类型 float64 并嵌入 Account 对象的变量。 类型还必须包含 AddCreditsRemoveCreditsCheckCredits 的功能。 你需要展示你可以通过 Employee 对象更改帐户名称。
  4. +
  5. 将字符串方法写入 Account 对象,以便按包含名字和姓氏的格式打印 Employee 名称。
  6. +
  7. 最后,编写使用已创建程序包的程序,并测试此挑战中列出的所有功能。 也就是说,主程序应更改名称、打印名称、添加贷方、删除贷方以及检查余额。
  8. +
+
package main
+
+import (
+	"errors"
+	"fmt"
+)
+
+type Account struct {
+	firstname string
+	lastname  string
+}
+
+func (a *Account) ChangeName(afterfirstname string) {
+	a.firstname = afterfirstname
+}
+
+type Employee struct {
+	Account
+	credit float64
+}
+
+func (e Employee) String() string {
+	return fmt.Sprintf("Firstname:%s,Lastname:%s,Credit:%.2f\n", e.firstname, e.lastname, e.credit)
+}
+
+func CreateEmployee(firstname, lastname string, credit float64) (*Employee, error) {
+	return &Employee{Account{firstname, lastname}, credit}, nil
+}
+
+func (e *Employee) AddCredits(amount float64) (float64, error) {
+	if amount > 0.0 {
+		e.credit += amount
+		return e.credit, nil
+	}
+	return 0.0, errors.New("invalid amount")
+}
+
+func (e *Employee) RemoveCredits(amount float64) (float64, error) {
+	if e.credit-amount < 0 {
+		return 0.0, errors.New("too much")
+	}
+	if amount < 0 {
+		return 0.0, errors.New("invalid amount")
+	}
+	e.credit -= amount
+	return e.credit, nil
+}
+
+func (e Employee) CheckCredits() float64 {
+	return e.credit
+}
+
+func main() {
+	bruce, _ := CreateEmployee("Bruce", "Lee", 500)
+	fmt.Println(bruce.CheckCredits())
+	credits, err := bruce.AddCredits(250)
+	if err != nil {
+		fmt.Println("Error:", err)
+	} else {
+		fmt.Println("New Credits Balance = ", credits)
+	}
+
+	_, err = bruce.RemoveCredits(2500)
+	if err != nil {
+		fmt.Println("Can't withdraw or overdrawn!", err)
+	}
+
+	bruce.ChangeName("Mark")
+
+	fmt.Println(bruce)
+}
+
+

goroutine(轻量线程)

+

并发是独立活动的组合,就像 Web 服务器虽然同时处理多个用户请求,但它是自主运行的。 并发在当今的许多程序中都存在。 Web 服务器就是一个例子,但你也能看到,在批量处理大量数据时也需要使用并发。

+

Go 有两种编写并发程序的样式。 一种是在其他语言中通过线程实现的传统样式。

+

Go 实现并发的方法

+

通常,编写并发程序时最大的问题是在进程之间共享数据。 Go 采用不同于其他编程语言的通信方式,因为 Go 是通过 channel 来回传递数据的。 这意味着只有一个活动 (goroutine) 有权访问数据,设计上不存在争用条件。 学完本模块中的 goroutine 和 channel 之后,你将更好地理解 Go 的并发方法。

+

可以使用下面的标语来概括 Go 的方法:“不是通过共享内存通信,而是通过通信共享内存。”

+

Goroutine

+

goroutine 是轻量线程中的并发活动,而不是在操作系统中进行的传统活动。 假设你有一个写入输出的程序和另一个计算两个数字相加的函数。 一个并发程序可以有数个 goroutine 同时调用这两个函数。

+

我们可以说,程序执行的第一个 goroutine 是 main() 函数。 如果要创建其他 goroutine,则必须在调用该函数之前使用 go 关键字

+
func main(){
+    login()
+    go launch()
+}
+

许多程序喜欢使用匿名函数来创建 goroutine

+
func main(){
+    login()
+    go func() {
+        launch()
+    }()
+}
+

编写并发程序

+

由于我们只想将重点放在并发部分,因此我们使用现有程序来检查 API 终结点是否响应。

+

串行程序:

+
package main
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+)
+
+func main() {
+	start := time.Now()
+
+	apis := []string{
+		"https://management.azure.com",
+		"https://dev.azure.com",
+		"https://api.github.com",
+		"https://outlook.office.com/",
+		"https://api.somewhereintheinternet.com/",
+		"https://graph.microsoft.com",
+	}
+
+	for _, api := range apis {
+		_, err := http.Get(api)
+		if err != nil {
+			fmt.Printf("ERROR: %s is down!\n", api)
+			continue
+		}
+
+		fmt.Printf("SUCCESS: %s is up and running!\n", api)
+	}
+
+	elapsed := time.Since(start)
+	fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
+}
+
+
SUCCESS: https://management.azure.com is up and running!
+SUCCESS: https://dev.azure.com is up and running!
+SUCCESS: https://api.github.com is up and running!
+SUCCESS: https://outlook.office.com/ is up and running!
+ERROR: https://api.somewhereintheinternet.com/ is down!
+SUCCESS: https://graph.microsoft.com is up and running!
+Done! It took 5.163787068 seconds!
+

同时检查所有站点?我们需要并发运行的代码部分是向站点进行 HTTP 调用的部分。 换句话说,我们需要为程序要检查的每个 API 创建一个 goroutine。为了创建 goroutine,我们需要在调用函数前使用 go 关键字。

+

首先创建一个新函数:

+
func checkAPI(api string) {
+    _, err := http.Get(api)
+    if err != nil {
+        fmt.Printf("ERROR: %s is down!\n", api)
+        return
+    }
+
+    fmt.Printf("SUCCESS: %s is up and running!\n", api)
+}
+

修改 main() 函数中的代码,为每个 API 创建一个 goroutine

+
for _, api := range apis {
+	go checkAPI(api)
+}
+
Done! It took 3.42e-05 seconds!
+

即使看起来 checkAPI 函数没有运行,它实际上是在运行。 它只是没有时间完成。

+

添加 time.Sleep(3 * time.Second)

+
ERROR: https://api.somewhereintheinternet.com/ is down!
+SUCCESS: https://api.github.com is up and running!
+SUCCESS: https://management.azure.com is up and running!
+SUCCESS: https://dev.azure.com is up and running!
+SUCCESS: https://outlook.office.com/ is up and running!
+SUCCESS: https://graph.microsoft.com is up and running!
+Done! It took 3.001536063 seconds!
+

将 channel 用作通信机制

+

Go 中的 channel 是 goroutine 之间的通信机制。 这就是为什么我们之前说过 Go 实现并发的方式是:“不是通过共享内存通信,而是通过通信共享内存。”需要将值从一个 goroutine 发送到另一个时,可以使用通道。

+

Channel 语法

+

由于 channel 是发送和接收数据的通信机制,因此它也有类型之分。 这意味着你只能发送 channel 支持的数据类型。 除使用关键字 chan 作为 channel 的数据类型外,还需指定将通过 channel 传递的数据类型,如 int 类型。

+

每次声明一个 channel 或希望在函数中指定一个 channel 作为参数时,都需要使用 chan <type>,如 chan int。 要创建 channel,需使用内置的 make() 函数,如下所示:

+
ch := make(chan int)
+

一个 channel 可以执行两项操作:发送数据和接收数据。 若要指定 channel 具有的操作类型,需要使用 channel 运算符 <-。 此外,在 channel 中发送数据和接收数据属于阻止操作。

+

如果希望 channel 仅发送数据,则必须在 channel 之后使用 <- 运算符。 如果希望 channel 接收数据,则必须在 channel 之前使用 <- 运算符

+
ch <- x // sends (or write) x through channel ch
+x = <-ch // x receives (or reads) data sent to the channel ch
+<-ch // receives data, but the result is discarded
+

可在 channel 中执行的另一项操作是关闭 channel

+
close(ch)
+

关闭 channel 时,你希望数据将不再在该 channel 中发送。 如果试图将数据发送到已关闭的 channel,则程序将发生严重错误。 如果试图通过已关闭的 channel 接收数据,则可以读取发送的所有数据。 随后的每次“读取”都将返回一个零值。

+

使用 channel 来删除睡眠功能并稍做清理:

+
package main
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// 通过 channel 发送该消息,而不是在 checkAPI 函数中打印结果
+func checkAPI(api string, ch chan string) {
+	_, err := http.Get(api)
+	if err != nil {
+		ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
+		return
+	}
+	ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
+}
+
+func main() {
+	// 创建channel
+	ch := make(chan string)
+
+	start := time.Now()
+
+	apis := []string{
+		"https://management.azure.com",
+		"https://dev.azure.com",
+		"https://api.github.com",
+		"https://outlook.office.com/",
+		"https://api.somewhereintheinternet.com/",
+		"https://graph.microsoft.com",
+	}
+
+	for _, api := range apis {
+		go checkAPI(api, ch)
+	}
+	fmt.Print(<-ch)
+
+	elapsed := time.Since(start)
+	fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
+}
+
+
ERROR: https://api.somewhereintheinternet.com/ is down!
+Done! It took 0.088759104 seconds!
+

但是事实上并没有实现功能

+

无缓冲 channel

+

使用 make() 函数创建 channel 时,会创建一个无缓冲 channel,这是默认行为。 无缓冲 channel 会阻止发送操作,直到有人准备好接收数据。 这就是为什么我们之前说发送和接收都属于阻止操作。 这也是上面的程序在收到第一条消息后立即停止的原因。

+

我们可以说 fmt.Print(<-ch) 会阻止程序,因为它从 channel 读取,并等待一些数据到达。 一旦有任何数据到达,它就会继续下一行,然后程序完成。

+

其他 goroutine 发生了什么? 它们仍在运行,但都没有在侦听。 而且,由于程序提前完成,一些 goroutine 无法发送数据。

+

读取数据和接收数据都属于阻止操作

+

要解决此问题,只需更改循环的代码,然后只接收确定要发送的数据

+
package main
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// 通过 channel 发送该消息,而不是在 checkAPI 函数中打印结果
+func checkAPI(api string, ch chan string) {
+	_, err := http.Get(api)
+	if err != nil {
+		ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
+		return
+	}
+	ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
+}
+
+func main() {
+	// 创建channel
+	ch := make(chan string)
+
+	start := time.Now()
+
+	apis := []string{
+		"https://management.azure.com",
+		"https://dev.azure.com",
+		"https://api.github.com",
+		"https://outlook.office.com/",
+		"https://api.somewhereintheinternet.com/",
+		"https://graph.microsoft.com",
+	}
+
+	for _, api := range apis {
+		go checkAPI(api, ch)
+	}
+	for i := 0; i < len(apis); i++ {
+		fmt.Print(<-ch)
+	}
+
+	elapsed := time.Since(start)
+	fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
+}
+
+
ERROR: https://api.somewhereintheinternet.com/ is down!
+SUCCESS: https://api.github.com is up and running!
+SUCCESS: https://management.azure.com is up and running!
+SUCCESS: https://graph.microsoft.com is up and running!
+SUCCESS: https://dev.azure.com is up and running!
+SUCCESS: https://outlook.office.com/ is up and running!
+Done! It took 1.029620196 seconds!
+

无缓冲 channel 在同步发送和接收操作。 即使使用并发,通信也是同步的。

+

有缓冲 channel

+

默认情况下 channel 是无缓冲行为。 这意味着只有存在接收操作时,它们才接受发送操作。 否则,程序将永久被阻止等待。

+

有时需要在 goroutine 之间进行此类同步。 但是,有时你可能只需要实现并发,而不需要限制 goroutine 之间的通信方式。

+

有缓冲 channel 在不阻止程序的情况下发送和接收数据,因为有缓冲 channel 的行为类似于队列。 创建 channel 时,可以限制此队列的大小

+
package main
+
+import (
+	"fmt"
+)
+
+func send(ch chan string, message string) {
+	ch <- message
+}
+
+func main() {
+	size := 4
+	ch := make(chan string, size)
+	send(ch, "one")
+	send(ch, "two")
+	send(ch, "three")
+	send(ch, "four")
+	fmt.Println("All data sent to the channel ...")
+
+	for i := 0; i < size; i++ {
+		fmt.Println(<-ch)
+	}
+
+	fmt.Println("Done!")
+}
+
+
All data sent to the channel ...
+one
+two
+three
+four
+Done!
+

channel 与 goroutine 有着紧密的联系。 如果没有另一个 goroutine 从 channel 接收数据,则整个程序可能会永久处于被阻止状态。

+
func main() {
+    size := 2
+    ch := make(chan string, size)
+    send(ch, "one")
+    send(ch, "two")
+    go send(ch, "three")
+    go send(ch, "four")
+    fmt.Println("All data sent to the channel ...")
+
+    for i := 0; i < 4; i++ {
+        fmt.Println(<-ch)
+    }
+
+    fmt.Println("Done!")
+}
+

无缓冲 channel 与有缓冲 channel

+

现在,你可能想知道何时使用这两种类型。 这完全取决于你希望 goroutine 之间的通信如何进行。 无缓冲 channel 同步通信。 它们保证每次发送数据时,程序都会被阻止,直到有人从 channel 中读取数据。

+

相反,有缓冲 channel 将发送和接收操作解耦。 它们不会阻止程序,但你必须小心使用,因为可能最终会导致死锁(如前文所述)。 使用无缓冲 channel 时,可以控制可并发运行的 goroutine 的数量。 例如,你可能要对 API 进行调用,并且想要控制每秒执行的调用次数。 否则,你可能会被阻止。

+

Channel 方向

+

Go 中 channel 的一个有趣特性是,在使用 channel 作为函数的参数时,可以指定 channel 是要发送数据还是接收数据。 随着程序的增长,可能会使用大量的函数,这时候,最好记录每个 channel 的意图,以便正确使用它们。 或者,你要编写一个库,并希望将 channel 公开为只读,以保持数据一致性。

+

要定义 channel 的方向,可以使用与读取或接收数据时类似的方式进行定义。 但是你在函数参数中声明 channel 时执行此操作。 将 channel 类型定义为函数中的参数的语法如下所示:

+
chan<- int // it's a channel to only send data
+<-chan int // it's a channel to only receive data
+

通过仅接收的 channel 发送数据时,在编译程序时会出现错误。

+

让我们使用以下程序作为两个函数的示例,一个函数用于读取数据,另一个函数用于发送数据:

+
package main
+
+import "fmt"
+
+func send(ch chan<- string, message string) {
+    fmt.Printf("Sending: %#v\n", message)
+    ch <- message
+}
+
+func read(ch <-chan string) {
+    fmt.Printf("Receiving: %#v\n", <-ch)
+}
+
+func main() {
+    ch := make(chan string, 1)
+    send(ch, "Hello World!")
+    read(ch)
+}
+

运行程序时,将看到以下输出:

+
Sending: "Hello World!"
+Receiving: "Hello World!"
+

程序阐明每个函数中每个 channel 的意图。 如果试图使用一个 channel 在一个仅用于接收数据的 channel 中发送数据,将会出现编译错误。 例如,尝试执行如下所示的操作:

+
func read(ch <-chan string) {
+    fmt.Printf("Receiving: %#v\n", <-ch)
+    ch <- "Bye!"
+}
+

运行程序时,将看到以下错误:

+
# command-line-arguments
+./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)
+

编译错误总比误用 channel 好。

+

多路复用

+

最后,让我们讨论一个关于如何在使用 select 关键字的同时与多个 channel 交互的简短主题。 有时,在使用多个 channel 时,需要等待事件发生。 例如,当程序正在处理的数据中出现异常时,可以包含一些逻辑来取消操作。

+

select 语句的工作方式类似于 switch 语句,但它适用于 channel。 它会阻止程序的执行,直到它收到要处理的事件。 如果它收到多个事件,则会随机选择一个。

+

select 语句的一个重要方面是,它在处理事件后完成执行。 如果要等待更多事件发生,则可能需要使用循环。

+
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func process(ch chan string) {
+	time.Sleep(3 * time.Second)
+	ch <- "Done processing!"
+}
+
+func replicate(ch chan string) {
+	time.Sleep(1 * time.Second)
+	ch <- "Done replicating!"
+}
+
+func main() {
+	ch1 := make(chan string)
+	ch2 := make(chan string)
+	go process(ch1)
+	go replicate(ch2)
+
+	for i := 0; i < 2; i++ {
+		select {
+		case process := <-ch1:
+			fmt.Println(process)
+		case replicate := <-ch2:
+			fmt.Println(replicate)
+		}
+	}
+}
+
+
Done replicating!
+Done processing!
+

请注意,replicate 函数先完成。 这就是你在终端中先看到其输出的原因。 main 函数存在一个循环,因为 select 语句在收到事件后立即结束,但我们仍在等待 process 函数完成。

+

练习 - 利用并发方法更快地计算斐波纳契数

+

实现并发的改进版本。 完成此操作需要几秒钟的时间(不超过 15 秒),应使用有缓冲 channel。

+
package main
+
+import (
+	"fmt"
+	"math/rand"
+	"time"
+)
+
+func fib(number float64, ch chan string) {
+	x, y := 1.0, 1.0
+	for i := 0; i < int(number); i++ {
+		x, y = y, x+y
+	}
+
+	r := rand.Intn(3)
+	time.Sleep(time.Duration(r) * time.Second)
+
+	ch <- fmt.Sprintf("Fib(%v): %v\n", number, x)
+}
+
+func main() {
+	ch := make(chan string, 15)
+
+	start := time.Now()
+
+	for i := 1; i < 15; i++ {
+		go fib(float64(i), ch)
+	}
+
+	for i := 1; i < 15; i++ {
+		fmt.Printf(<-ch)
+	}
+
+	elapsed := time.Since(start)
+	fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
+}
+
+

编写一个新版本以计算斐波纳契数,直到用户使用 fmt.Scanf() 函数在终端中输入 quit。 如果用户按 Enter,则应计算新的斐波纳契数。

+

使用两个无缓冲 channel:一个用于计算斐波纳契数,另一个用于等待用户的“退出”消息。 你需要使用 select 语句。

+
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+var quit = make(chan bool)
+
+func fib(c chan int) {
+	x, y := 1, 1
+
+	for {
+		select {
+		case c <- x:
+			x, y = y, x+y
+		case <-quit:
+			fmt.Println("Done calculating Fibonacci!")
+			return
+		}
+	}
+}
+
+func main() {
+	start := time.Now()
+
+	command := ""
+	data := make(chan int)
+
+	go fib(data)
+
+	for {
+		num := <-data
+		fmt.Println(num)
+		fmt.Scanf("%s", &command)
+		if command == "quit" {
+			quit <- true
+			break
+		}
+	}
+
+	time.Sleep(1 * time.Second)
+
+	elapsed := time.Since(start)
+	fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
+}
+
+ + +
+ +
+
+ + + + + + +
+
+
Go基础学习(微软教程)
+
https://zhangzhao219.github.io/2022/10/03/Go/go-basic-ms/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/05/UCAS/machine-learning/machine-learning-8/index.html b/2022/10/05/UCAS/machine-learning/machine-learning-8/index.html new file mode 100644 index 000000000..7dd0e70ba --- /dev/null +++ b/2022/10/05/UCAS/machine-learning/machine-learning-8/index.html @@ -0,0 +1,819 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第8章 信息论模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第8章 信息论模型

+ + +
+ +

《机器学习》课程笔记:第8章 信息论模型

+ +

第8章 信息论模型

+

熵、最大熵

+

信息量(信息增益量)定义:

+

信息量性质:概率越小的状态,信息量越大

+

信息熵定义:信息量在全部数值域上的概率平均值

+
    +
  • 离散熵:
  • +
  • 微分熵:(微分熵不是严格意义的信息熵)
  • +
+

微分熵性质:平移不变、尺度变化,且可以是负值

+

当根据不完整的信息作为依据进行推断时,应该由满足分布限制条件的具有最大熵的概率分布推得。

+

最大微分熵问题:

+

已知均值和方差,高斯分布的微分熵最大

+

互信息

+

条件信息量:

+

条件熵:

+
    +
  • 给定的条件熵:
  • +
  • 给定的条件熵:
  • +
+

联合熵:

+
    +
  • 联合概率密度:
  • +
  • 联合信息量:
  • +
  • 联合微分熵:
  • +
+

互信息:信息熵与条件熵的差:

+

互信息性质:非负性、对称性、不变性

+

相对熵是衡量两个分布的平均信息差异

+

+

相对熵和互信息之间的关系:

+

信息论优化模型

+

最大熵模型:最大化 , 求取类别后验概率分布 , 用于分类、预测等

+

最大互信息模型: 最大化 ; 最大化

+

最小互信息模型:最小化 ; 最小化 , 独立分析

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第8章 信息论模型
+
https://zhangzhao219.github.io/2022/10/05/UCAS/machine-learning/machine-learning-8/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月5日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/07/UCAS/information-retrieval/information-retrieval-13/index.html b/2022/10/07/UCAS/information-retrieval/information-retrieval-13/index.html new file mode 100644 index 000000000..cbb55f1d6 --- /dev/null +++ b/2022/10/07/UCAS/information-retrieval/information-retrieval-13/index.html @@ -0,0 +1,848 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第13讲 决策树与面向文档的机器学习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第13讲 决策树与面向文档的机器学习

+ + +
+ +

《现代信息检索》课程笔记:第13讲 决策树与面向文档的机器学习

+ +

第13讲 决策树与面向文档的机器学习

+

面向文本分类的决策树

+
    +
  • 树结构:内部节点由词项作为标记
  • +
  • 分支标记:词项权重的“测试” (test),或仅仅是出现/不出现
  • +
  • 叶节点标记:类别
  • +
  • 分类器 +
      +
    • 分类器通过“测试”后的降序树对文档进行分类
    • +
    • 然后将叶节点的标签分配给文档
    • +
    +
  • +
  • 大多数决策树都是二叉树
  • +
+

决策树的学习

+

学习一个序列的特征测试,典型的做法是由上到下的贪心搜索,每一步选择具有最高信息收益的未使用特征

+

叶节点标记:yes/no 类别标记,或连续值

+

如果有个特征,决策树的节点数量上限是(太大了,会有计算开支等方面的问题)

+

我们可以通过在每个节点上递归选择最佳拆分特征,以贪心的方式创建树

+

属性选择基本思想:(理想情况下)一个好的特征能够把所有样本划分成“全部正样本”和“全部负样本”两个子集

+

利用信息论:

+

信息熵(Entropy):考虑每个节点的类分解

+

信息增益

+

对每个节点,我们选择使信息增益最大的特征f

+

数值特征 (例如tf-idf):通常使用二元的切分 (f < t), t怎样确定?

+

穷尽式(搜索):评估观察值之间的每个分割点的信息增益。

+
    +
  • 慢;通过优化计数方法可以稍微提高效率
  • +
+

分箱(Discretize into bins)

+
    +
  • 将所有的数值切分到k个箱中
  • +
  • (连续的数值)特征被离散化
  • +
  • 分箱操作可以基于整个语料集的统计
  • +
+

(树的构建)什么时候停止?

+
    +
  • 当一个节点的所有样本都属于同一个类别
  • +
  • 当树的深度d达到一个固定阈值
  • +
  • 如果没有合适的属性可以拆分中区分具有统计意义的类(例如,使用卡方检验或Fisher精确检验),则停止构建。
  • +
  • 最常用/最佳方法:使用单独的验证数据 +
      +
    • 构建一个较大的树(可以给树的深度设定阈值)
    • +
    • 自下而上的修剪未能(显著)改善验证数据分类性能的节点
    • +
    +
  • +
+

面向文本的决策树学习

+

宏平均:计算每个类别的性能指标,然后取平均值

+

微平均:收集所有类别的决策(分类)结果,计算列联表,评价

+

判别式 (discriminative) 分类方法: Logistic Regression (逻辑回归) 与 Support vector machines (支持向量机)

+

Ensemble 方法

+

随机森林 (Random Forests)

+

从原始数据集重复采样(bootstrap采样),在采样数据上构建K个树,p=特征数量

+
    +
  • 获得K个大小为N的bootstrap采样,其中N是原始数据集大小
  • +
  • 通过在每个节点的p个特征中随机选择m个,并选择最佳特征来扩展每个决策树。
  • +
  • m的典型取值: sqrt(p)
  • +
  • 预测(Runtime):汇总树的预测(最受欢迎的投票)以产生最终分类
  • +
+

原则:我们希望在不同的学习器(learner)之间进行投票,因此我们不希望这些模型过于相似。这两个标准确保了各个树的多样性

+

优点:

+
    +
  • 在实践中非常流行,有段时间是密集数据(dense data)上最流行的分类器(<=几千个特征)
  • +
  • 容易实现 (训练多个树).
  • +
  • 容易并行化 (但并不意味着效率高)。适合用于 MapReduce.
  • +
+

缺点:

+
    +
  • 现在和一些新方法相比准确度并不高 – Gradient-boosted trees (特征少) 与 深度神经网络(视觉, 语音, 语言, …)通常更好
  • +
  • 需要多次遍历数据 – 至少是树的最大深度 (虽然远小于boosted trees )
  • +
  • 容易过拟合 – 需要权衡准确度(accuracy)与拟合度(fit)
  • +
+

Boosted Decision Trees (BDT, 增强决策树)

+
    +
  • 一个比随机森林(RF)提出更晚的算法
  • +
  • 与独立训练树的RF不同,BDT的树通过增强(boosting)的方式依次(sequetially)训练树: +
      +
    • 每个树都在加权数据上训练,通过加权强调之前(训练)
      +的树错误标记的样本
    • +
    +
  • +
  • 这两个方法都能够通过训练产生高质量模型
  • +
  • 但是BDT通常都更适用于中等数量特征的数据集
  • +
+

随机森林(RF) vs 增强树(BDT)

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第13讲 决策树与面向文档的机器学习
+
https://zhangzhao219.github.io/2022/10/07/UCAS/information-retrieval/information-retrieval-13/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月7日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/09/Go/go-basic-1/index.html b/2022/10/09/Go/go-basic-1/index.html new file mode 100644 index 000000000..ca3cf1388 --- /dev/null +++ b/2022/10/09/Go/go-basic-1/index.html @@ -0,0 +1,1135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go语言圣经-入门 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go语言圣经-入门

+ + +
+ +

Go语言圣经-入门

+ +

命令行参数

+

os 包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从 os 包的 Args 变量获取;os 包外部使用 os.Args 访问该变量。

+

os.Args 的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数。

+
package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	var s, sep string
+	for i := 1; i < len(os.Args); i++ {
+		s += sep + os.Args[i]
+		sep = " "
+	}
+	fmt.Println(s)
+}
+
> go run main.go 4 2 5 4 1
+4 2 5 4 1
+

for 循环的另一种形式,在某种数据类型的区间(range)上遍历

+
func main() {
+	var s, sep string
+	for _, j := range os.Args[1:] {
+		s += sep + j
+		sep = " "
+	}
+	fmt.Println(s)
+}
+

使用 strings 包的 Join 函数:

+
func main() {
+	fmt.Println(strings.Join(os.Args[1:], " "))
+}
+

查找重复的行

+
// Dup1 prints the text of each line that appears more than
+// once in the standard input, preceded by its count.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+func main() {
+	counts := make(map[string]int)
+	input := bufio.NewScanner(os.Stdin)
+	for input.Scan() {
+		counts[input.Text()]++
+	}
+	// NOTE: ignoring potential errors from input.Err()
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+
> abc
+> abc
+> def
+> efd
+> efd
+2       abc
+2       efd
+

Printf的多种转换形式:

+
%d          十进制整数
+%x, %o, %b  十六进制,八进制,二进制整数。
+%f, %g, %e  浮点数: 3.141593 3.141592653589793 3.141593e+00
+%t          布尔:true或false
+%c          字符(rune) (Unicode码点)
+%s          字符串
+%q          带双引号的字符串"abc"或带单引号的字符'c'
+%v          变量的自然形式(natural format)
+%T          变量的类型
+%%          字面上的百分号标志(无操作数)
+

ln 结尾的格式化函数,则遵循 Println 的方式,以跟 %v 差不多的方式格式化参数,并在最后添加一个换行符。

+

第二个版本,可以接收文件并判断重复行

+
// Dup2 prints the count and text of lines that appear more than once
+// in the input.  It reads from stdin or from a list of named files.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+func main() {
+	counts := make(map[string]int)
+	files := os.Args[1:]
+	if len(files) == 0 {
+		countLines(os.Stdin, counts)
+	} else {
+		for _, arg := range files {
+			f, err := os.Open(arg)
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
+				continue
+			}
+			countLines(f, counts)
+			f.Close()
+		}
+	}
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+func countLines(f *os.File, counts map[string]int) {
+	input := bufio.NewScanner(f)
+	for input.Scan() {
+		counts[input.Text()]++
+	}
+	// NOTE: ignoring potential errors from input.Err()
+}
+
+

map 是一个由 make 函数创建的数据结构的引用。map 作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对 map 底层数据结构的任何修改,调用者函数都可以通过持有的 map 引用看到。

+
package main
+
+import (
+	"fmt"
+	"os"
+	"strings"
+)
+
+func main() {
+	counts := make(map[string]int)
+	for _, filename := range os.Args[1:] {
+		data, err := os.ReadFile(filename)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
+			continue
+		}
+		for _, line := range strings.Split(string(data), "\n") {
+			counts[line]++
+		}
+	}
+	for line, n := range counts {
+		if n > 1 {
+			fmt.Printf("%d\t%s\n", n, line)
+		}
+	}
+}
+
+

引入了 ReadFile 函数(来自于 os包),其读取指定文件的全部内容,strings.Split 函数把字符串分割成子串的切片。

+

GIF动画

+
// Lissajous generates GIF animations of random Lissajous figures.
+package main
+
+import (
+	"image"
+	"image/color"
+	"image/gif"
+	"io"
+	"math"
+	"math/rand"
+	"os"
+	"time"
+)
+
+var palette = []color.Color{color.White, color.Black}
+
+const (
+	whiteIndex = 0 // first color in palette
+	blackIndex = 1 // next color in palette
+)
+
+func main() {
+	// The sequence of images is deterministic unless we seed
+	// the pseudo-random number generator using the current time.
+	// Thanks to Randall McPherson for pointing out the omission.
+	rand.Seed(time.Now().UTC().UnixNano())
+	lissajous(os.Stdout)
+}
+
+func lissajous(out io.Writer) {
+	const (
+		cycles  = 5     // number of complete x oscillator revolutions
+		res     = 0.001 // angular resolution
+		size    = 100   // image canvas covers [-size..+size]
+		nframes = 64    // number of animation frames
+		delay   = 8     // delay between frames in 10ms units
+	)
+
+	freq := rand.Float64() * 3.0 // relative frequency of y oscillator
+	anim := gif.GIF{LoopCount: nframes}
+	phase := 0.0 // phase difference
+	for i := 0; i < nframes; i++ {
+		rect := image.Rect(0, 0, 2*size+1, 2*size+1)
+		img := image.NewPaletted(rect, palette)
+		for t := 0.0; t < cycles*2*math.Pi; t += res {
+			x := math.Sin(t)
+			y := math.Sin(t*freq + phase)
+			img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
+				blackIndex)
+		}
+		phase += 0.1
+		anim.Delay = append(anim.Delay, delay)
+		anim.Image = append(anim.Image, img)
+	}
+	gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
+}
+
+

获取URL

+
// Fetch prints the content found at a URL.
+package main
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+)
+
+func main() {
+	for _, url := range os.Args[1:] {
+		resp, err := http.Get(url)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
+			os.Exit(1)
+		}
+		b, err := io.ReadAll(resp.Body)
+		resp.Body.Close()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
+			os.Exit(1)
+		}
+		fmt.Printf("%s", b)
+	}
+}
+
+

改进:

+

避免申请缓冲区、url参数没有 http:// 前缀、打印出HTTP协议的状态码

+
// Fetch prints the content found at a URL.
+package main
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"strings"
+)
+
+func main() {
+	for _, url := range os.Args[1:] {
+		if !strings.HasPrefix(url, "http://") {
+			url = "http://" + url
+		}
+		resp, err := http.Get(url)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
+			os.Exit(1)
+		}
+		_, err = io.Copy(os.Stdout, resp.Body)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
+			os.Exit(1)
+		}
+		fmt.Println(resp.Status)
+		resp.Body.Close()
+	}
+}
+
+

并发获取多个URL

+
// Fetchall fetches URLs in parallel and reports their times and sizes.
+package main
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"time"
+)
+
+func main() {
+	start := time.Now()
+	ch := make(chan string)
+	for _, url := range os.Args[1:] {
+		go fetch(url, ch) // start a goroutine
+	}
+	for range os.Args[1:] {
+		fmt.Println(<-ch) // receive from channel ch
+	}
+	fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
+}
+
+func fetch(url string, ch chan<- string) {
+	start := time.Now()
+	resp, err := http.Get(url)
+	if err != nil {
+		ch <- fmt.Sprint(err) // send to channel ch
+		return
+	}
+	nbytes, err := io.Copy(io.Discard, resp.Body)
+	resp.Body.Close() // don't leak resources
+	if err != nil {
+		ch <- fmt.Sprintf("while reading %s: %v", url, err)
+		return
+	}
+	secs := time.Since(start).Seconds()
+	ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
+}
+
+

goroutine是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数。

+

Web服务

+
// Server1 is a minimal "echo" server.
+package main
+
+import (
+    "fmt"
+    "log"
+    "net/http"
+)
+
+func main() {
+    http.HandleFunc("/", handler) // each request calls handler
+    log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+// handler echoes the Path component of the request URL r.
+func handler(w http.ResponseWriter, r *http.Request) {
+    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
+}
+

为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算

+
// Server2 is a minimal "echo" and counter server.
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"sync"
+)
+
+var mu sync.Mutex
+var count int
+
+func main() {
+	http.HandleFunc("/", handler)
+	http.HandleFunc("/count", counter)
+	log.Fatal(http.ListenAndServe("localhost:8000", nil))
+}
+
+// handler echoes the Path component of the requested URL.
+func handler(w http.ResponseWriter, r *http.Request) {
+	mu.Lock()
+	count++
+	mu.Unlock()
+	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
+}
+
+// counter echoes the number of calls so far.
+func counter(w http.ResponseWriter, r *http.Request) {
+	mu.Lock()
+	fmt.Fprintf(w, "Count %d\n", count)
+	mu.Unlock()
+}
+
+

handler函数会把请求的http头和请求的form数据都打印出来,这样可以使检查和调试这个服务更为方便

+
// handler echoes the HTTP request.
+func handler(w http.ResponseWriter, r *http.Request) {
+    fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
+    for k, v := range r.Header {
+        fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
+    }
+    fmt.Fprintf(w, "Host = %q\n", r.Host)
+    fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
+    if err := r.ParseForm(); err != nil {
+        log.Print(err)
+    }
+    for k, v := range r.Form {
+        fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
+    }
+}
+ + +
+ +
+
+ + + + + + +
+
+
Go语言圣经-入门
+
https://zhangzhao219.github.io/2022/10/09/Go/go-basic-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月9日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/10/Go/go-basic-2-3/index.html b/2022/10/10/Go/go-basic-2-3/index.html new file mode 100644 index 000000000..b9e249bb1 --- /dev/null +++ b/2022/10/10/Go/go-basic-2-3/index.html @@ -0,0 +1,817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go语言圣经-程序结构-基础数据类型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go语言圣经-程序结构-基础数据类型

+ + +
+ +

Go语言圣经-程序结构-基础数据类型

+ +

var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

+

简短变量声明语句中必须至少要声明一个新的变量

+

如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是 *int,指针被称之为“指向int类型的指针”。

+

指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。

+
// Echo4 prints its command-line arguments.
+package main
+
+import (
+    "flag"
+    "fmt"
+    "strings"
+)
+
+var n = flag.Bool("n", false, "omit trailing newline")
+var sep = flag.String("s", " ", "separator")
+
+func main() {
+    flag.Parse()
+    fmt.Print(strings.Join(flag.Args(), *sep))
+    if !*n {
+        fmt.Println()
+    }
+}
+

程序中的 sepn变量分别是指向对应命令行标志参数变量的指针,因此必须用 *sep*n形式的指针语法间接引用它们。

+

另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为 *T

+
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
+fmt.Println(*p) // "0"
+*p = 2          // 设置 int 匿名变量的值为 2
+fmt.Println(*p) // "2"
+

下面的两个newInt函数有着相同的行为:

+
func newInt() *int {
+    return new(int)
+}
+
+func newInt() *int {
+    var dummy int
+    return &dummy
+}
+

变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

+

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

+
type 类型名字 底层类型
+

在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的

+

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。

+

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

+

bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。

+

strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

+

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。

+

将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”)

+

如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数

+

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

+
type Weekday int
+
+const (
+    Sunday Weekday = iota
+    Monday
+    Tuesday
+    Wednesday
+    Thursday
+    Friday
+    Saturday
+)
+ + +
+ +
+
+ + + + + + +
+
+
Go语言圣经-程序结构-基础数据类型
+
https://zhangzhao219.github.io/2022/10/10/Go/go-basic-2-3/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月10日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/11/Go/go-basic-4/index.html b/2022/10/11/Go/go-basic-4/index.html new file mode 100644 index 000000000..2668453d8 --- /dev/null +++ b/2022/10/11/Go/go-basic-4/index.html @@ -0,0 +1,823 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go语言圣经-复合数据类型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go语言圣经-复合数据类型

+ + +
+ +

Go语言圣经-复合数据类型

+ +

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。

+

一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0

+

如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。

+

一个slice可以用来模拟一个stack

+

最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:

+
stack = append(stack, v) // push v
+

stack的顶部位置对应slice的最后一个元素:

+
top := stack[len(stack)-1] // top of stack
+

通过收缩stack可以弹出栈顶的元素

+
stack = stack[:len(stack)-1] // pop
+

要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:

+
func remove(slice []int, i int) []int {
+    copy(slice[i:], slice[i+1:])
+    return slice[:len(slice)-1]
+}
+
+func main() {
+    s := []int{5, 6, 7, 8, 9}
+    fmt.Println(remove(s, 2)) // "[5 6 8 9]"
+}
+

如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:

+
func remove(slice []int, i int) []int {
+    slice[i] = slice[len(slice)-1]
+    return slice[:len(slice)-1]
+}
+
+func main() {
+    s := []int{5, 6, 7, 8, 9}
+    fmt.Println(remove(s, 2)) // "[5 6 9 8]
+}
+

JSON

+
type Movie struct {
+    Title  string
+    Year   int  `json:"released"`
+    Color  bool `json:"color,omitempty"`
+    Actors []string
+}
+
+var movies = []Movie{
+    {Title: "Casablanca", Year: 1942, Color: false,
+        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
+    {Title: "Cool Hand Luke", Year: 1967, Color: true,
+        Actors: []string{"Paul Newman"}},
+    {Title: "Bullitt", Year: 1968, Color: true,
+        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
+    // ...
+}
+
+

这样的数据结构特别适合JSON格式,并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:

+
data, err := json.Marshal(movies)
+if err != nil {
+    log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+

另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

+
data, err := json.MarshalIndent(movies, "", "    ")
+if err != nil {
+    log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+

编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫unmarshaling,通过json.Unmarshal函数完成。

+
var titles []struct{ Title string }
+if err := json.Unmarshal(data, &titles); err != nil {
+    log.Fatalf("JSON unmarshaling failed: %s", err)
+}
+fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
+

文本和HTML模板

+ + +
+ +
+
+ + + + + + +
+
+
Go语言圣经-复合数据类型
+
https://zhangzhao219.github.io/2022/10/11/Go/go-basic-4/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/12/UCAS/information-retrieval/information-retrieval-14/index.html b/2022/10/12/UCAS/information-retrieval/information-retrieval-14/index.html new file mode 100644 index 000000000..2d0d910c6 --- /dev/null +++ b/2022/10/12/UCAS/information-retrieval/information-retrieval-14/index.html @@ -0,0 +1,817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第14讲 面向信息检索的分布式词项表示 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第14讲 面向信息检索的分布式词项表示

+ + +
+ +

《现代信息检索》课程笔记:第14讲 面向信息检索的分布式词项表示

+ +

第14讲 面向信息检索的分布式词项表示

+

怎样更鲁棒的匹配用户搜索意图?

+

查询扩展/Query expansion:

+
    +
  • 相关反馈/Relevance feedback 能够通过向查询中添加扩展词,从而对捕捉用户搜索意图有所帮助
  • +
  • 也可以利用 词项相似度/word similarities 信息: +
      +
    • 基于人工 同义词表/thesaurus of synonyms 的查询扩展
    • +
    • 词项相似度指标 +
        +
      • 基于大规模文档语料计算
      • +
      • 基于查询日志挖掘(Web上的常见做法)计算
      • +
      +
    • +
    +
  • +
+

文档扩展/Document expansion:

+

使用锚文本/anchor text可以通过提供人工创作的同义词(即锚文本)来解决此问题,但不适用于新的或不太受欢迎的网页(注:链接稀疏,锚文本少)或无超链接的语料

+

基于查询日志的查询扩展

+

不考虑上下文语境的查询扩展可能会导致问题

+

从查询日志学习考虑上下文语境的查询重写:识别同一用户基于同一信息需求的多次查询请求

+

自动同义词库生成

+
    +
  • 尝试通过分析文档集来自动生成同义词库
  • +
  • 基本概念:两个词之间的相似性
  • +
  • 假设1:如果两个单词与相似单词同时出现,则它们是相似的。
  • +
  • 假设2:如果两个单词与同一个词在给定的语法关系中出现,则它们是相似的。
  • +
  • 基于共现的相似度更鲁棒,基于语法关系的相似度更准确。
  • +
+

表示词项之间的关系

+

使用词项的标准符号编码,每个词项都是一个维度

+

不同的词项没有内在的相似性

+

基于分布式相似度的表示:用相邻词项的意义来表示一个词项

+

解决方案:低维向量

+

基本思想: 将“大部分的”重要信息存储在一个维度固定的低维向量中 - 即“密集向量”

+

传统方法:潜在语义索引/分析

+

使用奇异值分解(Singular Value Decomposition,SVD)–或只是随机映射(random projection)以找到低维基向量或正交向量

+

神经嵌入

+

词项的意义由向量表示:为每个词类构建一个密集向量,该向量应当能够准确的预测其上下文词项

+

学习神经词嵌入:基本思路

+
    +
  • 定义一个向量表示的模型,该模型预测一个中心词 wt 的 上下文词(或者反过来)
  • +
  • 同时也有一个损失函数
  • +
  • 不断调整(所有词的)向量表示使得损失最小化
  • +
  • 最终得到每个词的低维密集向量表示
  • +
+

思路:直接基于预测能力学习低维词向量

+

Word2Vec包含一组算法预测每个词的上下文(或者反过来)

+

神经网络的优化:(求导的)链式法则

+

Word2vec里的线性关系

+

Word2vec的向量表示非常善于对相似性和相似性的维度编码!

+

仅通过在嵌入空间中进行向量减法就可以很好地解决类比测试相似度的问题

+

Dual Embedding Space Model (DESM)

+

一种简单的利用词嵌入的检索模型

+

文档由其词项嵌入的中心向量表示

+

查询-文档相似度:查询词向量与文档向量的平均相似度

+

DESM 是一个弱排序模型,但是具有发现微妙相似性/关联性的能力

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第14讲 面向信息检索的分布式词项表示
+
https://zhangzhao219.github.io/2022/10/12/UCAS/information-retrieval/information-retrieval-14/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月12日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/12/UCAS/machine-learning/machine-learning-9/index.html b/2022/10/12/UCAS/machine-learning/machine-learning-9/index.html new file mode 100644 index 000000000..2975ca4fb --- /dev/null +++ b/2022/10/12/UCAS/machine-learning/machine-learning-9/index.html @@ -0,0 +1,950 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第9章 概率图模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第9章 概率图模型

+ + +
+ +

《机器学习》课程笔记:第9章 概率图模型

+ +

第9章 概率图模型

+

有向图模型:贝叶斯网络

+

图结构:有向无环图

+

结点:一个或一组随机变量

+

边:随机变量之间的单向、直接影响

+

联合概率分布分解形式:,其中, 所有父结点构成的集合

+

条件独立性 D-分离准则(D-separation criterion):判断贝叶斯网络结点之间的条件独立性。

+

贝叶斯网络的全局马尔科夫性:给定结点集合A,B,C,若A到B中结点的所有无向路径都是被C阻塞的(blocked),则称A和B被C D-分离(D-separated),即A和B关于C条件独立。

+

若一条无向路径包含结点x满足以下条件之一,则称其是阻塞的:

+
    +
  • x 是tail-to-tail 或head-to-tail 结点,并且x包含在C中。
  • +
  • x 是head-to-head 结点,并且x(及x 的任意后代均)不包含在C中。
  • +
+

贝叶斯网络的局部马尔科夫性:

+
    +
  • 给定某变量的父结点,则该变量条件独立于所有其他非其后代结点。
  • +
  • 给定某变量的马尔可夫毯(父结点,子结点,子结点的父结点),则该变量条件独立于其他变量。
  • +
+

无向图模型:马尔可夫随机场

+

图结构:无向图

+

结点:一个或一组随机变量。

+

边:随机变量之间的相互依赖(非“因果关系”)。

+

团:对于图中的结点子集,若其中任意两个节点之间都有连边,则称该结点子集为一个团(clique)。

+

极大团:若在团中加入其他任意一个结点都不再形成团,则称该团为极大团(maximal clique)。

+

分解形式:

+

其中, 为团集合, 为团 对应的变量集合, 为定义在团 上的非负势函数,是归一化因子

+

条件独立性:

+

马尔可夫随机场的全局马尔科夫性:给定结点集合A,B,C,若从A中的结点到B中结点必须经过C中的结点,则称A和B被C分离,即A和B关于C条件独立。

+

局部马尔科夫性:给定某变量的马尔可夫毯(邻接变量),则该变量条件独立于其他变量。

+

成对马尔科夫性:给定其他所有变量,两个非相邻变量条件独立。 if

+

学习与推断

+

基本定义

+

推断:已知联合概率分布 ,估计 ,其中 是集合 的子集。 是问题变量, 是证据变量。

+

学习:从观测数据 中学习联合概率分布 ,寻找最符合观测数据的概率图模型。

+

推断:已知联合概率分布 ,估计,其中

+

枚举 : 假设 个变量,每个变量的取值个数的期望是 ,则时间复杂度为

+

推断的核心问题 : 如何高效地计算边际分布

+

推断方法

+

精确推断:计算的精确值。

+

变量消去(variable elimination)

+

思路:利用图模型的紧凑概率分布形式来削减计算量。

+

优点:简单直观,代数上的消去对应图中结点的消去。

+

缺点:针对不同证据变量会造成大量冗余计算。

+

信念传播(belief propagation)

+

思路:将变量消去过程中产生的中间结果视为可复用的消息,避免重复计算。

+

消息传递仅在邻接变量之间发生,与边的方向性无关。

+

树结构:有向树=无向树

+

树结构上的消息传递:

+

消息计算公式:

+

边际分布:

+

二次扫描算法:

+
    +
  • 指定一个根结点,从所有叶结点开始向根节点传递消息,直到根结点收到所有邻接结点的消息。
  • +
  • 从根结点开始向叶结点传递消息,直到所有叶结点均收到消息。
  • +
+

近似推断

+

近似推断:在较低的时间复杂度下获得原问题的近似解。通过采样一组服从特定分布的样本,来近似原始分布,适用范围更广,操作性更强。

+

前向采样(forward sampling)

+

思路:依据贝叶斯网络的(条件)概率直接采样。采样后,进行需要的概率统计。

+

缺点:对于小概率事件采样困难,可能经过很多次采样也无法获得足够多的样本

+

仅适用于贝叶斯网络,不适用于马尔可夫随机场。

+

吉布斯采样(Gibbs sampling)

+

思路:直接依照条件概率采样。

+

马尔可夫毯的性质:

+

优点:

+
    +
  • 直接从采样,解决小概率事件采样难的问题
  • +
  • 同时适用于贝叶斯网络和马尔可夫随机场
  • +
  • 简单易推导,时间复杂度低。
  • +
+

实例模型

+

隐马尔可夫模型

+

隐马尔可夫模型是关于时序的概率模型,是最简单的动态贝叶斯网络模型。

+

状态变量 表示第 时刻的系统状态,观测变量 表示第 时刻的观测值。

+

观测变量仅依赖于当前时刻的状态变量,当前状态仅依赖于前一时刻的状态。状态集合 ,观测值集合

+

联合概率:

+

状态转移矩阵,其中表示 时刻处于状态 的条件下, 时刻转移到状态 的概率

+

观测概率矩阵,其中表示 时刻处于状态 的条件下观测到 的概率

+

初始状态概率向量 ,其中表示系统初始状态为的概率。

+

生成过程:

+

给定 ,生成观测序列

+
    +
  1. 设置,并根据初始状态概率生成初始状态
  2. +
  3. 根据和观测概率矩阵B生成
  4. +
  5. 根据和状态转移矩阵A 生成
  6. +
  7. ,则设置,并转到第(2)步;否则,停止。
  8. +
+

三个基本问题

+
    +
  • 概率计算问题:给定模型和观测序列,计算在模型下观测到的概率。(评估模型与观测序列之间的匹配程度)
  • +
  • 直接计算法:给定模型和观测序列,求使得最大的状态观测序列。(根据观测序列推测状态序列)
  • +
  • 学习问题:给定观测序列,调整模型参数,使得该序列出现的概率最大。(训练模型使其更好地描述观测序列)
  • +
+

条件随机场

+

条件随机场(Conditional Random Field) 是给定随机变量的条件下,随机变量的马尔可夫随机场。中的随机变量构成的无向图,图中每个变量在给定的条件下都满足马尔可夫性:

+

线性链条件随机场(linear-chain CRF)是随机变量为线性链时的条件随机场

+

是观测序列。 是标记序列(或称状态序列 ),在给定的条件下,的条件分布构成条件随机场。

+

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第9章 概率图模型
+
https://zhangzhao219.github.io/2022/10/12/UCAS/machine-learning/machine-learning-9/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月12日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/13/UCAS/advanced-ai/advanced-ai-7/index.html b/2022/10/13/UCAS/advanced-ai/advanced-ai-7/index.html new file mode 100644 index 000000000..a39840596 --- /dev/null +++ b/2022/10/13/UCAS/advanced-ai/advanced-ai-7/index.html @@ -0,0 +1,854 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第7讲 图卷积神经网络 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第7讲 图卷积神经网络

+ + +
+ +

《高级人工智能》课程笔记:第7讲 图卷积神经网络

+ +

第7讲 图卷积神经网络

+

卷积神经网络在欧式数据(图像、文本、声音和视频等)上获得了巨大的成功,广泛应用于图像分类、目标检测、机器翻译等

+

卷积神经网络可以学习局部小结构,使用局部的卷积核,然后形成多维的模式

+

卷积如何迁移到非欧空间上去?

+

卷积是在函数和函数上的数学运算,从而得到函数

+

连续形式:

+

离散形式:

+

在图上定义卷积的方法:

+

谱方法:在谱空间中定义卷积:

+
    +
  • 通过图傅里叶变换和卷积原理定义卷积 +
      +
    • 图数据符合幂律分布,造成了极大的挑战
    • +
    +
  • +
  • 主要挑战是在谱空间定义的卷积在结点空间并没有局部化
  • +
+

空间方法:在向量空间中定义卷积

+
    +
  • 卷积被定义为目标结点到它的所有邻居的一个加权平均函数
  • +
  • 主要挑战是邻域的大小在结点之间差异很大,可能服从幂律分布
  • +
+

谱方法

+

定义一个图(结点、边、邻接矩阵)

+

图上的每个结点上都有维的特征,因此是结点的特征矩阵,每一列是结点的一个信号

+

图的拉普拉斯算子:,其中

+

归一化的拉普拉斯算子:

+

的傅里叶变换:

+

的正交特征向量是,对应的非负特征值是,可以对拉普拉斯矩阵进行分解:

+

对于一个信号的图傅里叶变换为

+

两个信号的卷积的傅里叶变换是两个信号的傅里叶变换的逐点相乘,卷积核就是

+

xd8xQU.md.png

+

图卷积神经网络:

+

xdGCw9.md.png

+

缺点:

+
    +
  • 需要拉普拉斯矩阵的特征分解,特征向量不太好获得
  • +
  • 计算成本高,傅里叶乘法的时间复杂度是
  • +
  • 在结点空间上不是局部化的(操作的是全局信号)
  • +
+

ChebyNet:参数化-将参数的数量从n降为K

+

xdJCjS.md.png

+

优点:

+
    +
  • 不再需要特征分解
  • +
  • 时间复杂度从下降到
  • +
  • 卷积在结点空间上是局部化的(卷积严格定位在半径为 K 的球中)
  • +
+

Graph wavelet neural network:图小波神经网络

+

将傅里叶基换为小波基:稀疏、局部化、计算代价低

+

空间方法

+

方法类比卷积:

+
    +
  1. 对于每个节点,根据某些邻近度指标选择固定数量的结点作为其相邻结点
  2. +
  3. 根据邻近度指标给邻居排序
  4. +
  5. 共享参数
  6. +
+

GraphSAGE:从每个结点开始随机游走,采样个结点,不用临近度指标判断。然后通过聚合函数进行参数共享

+

图卷积网络(GCN):通过归一化的拉普拉斯矩阵从不固定数量的邻居结点中聚合信息,通过特征变换共享参数

+

GAT:Graph Attention Network:通过注意力机制学习聚合矩阵

+

MoNet:空间方法的一般意义框架:所有的空间方法都是定义多个核函数,来测量目标结点和其他结点之间的相似度

+

谱方法与空间方法的关系

+

谱方法是空间方法的特例

+
    +
  • 谱方法通过特别的空间变换定义核函数
  • +
  • 空间方法直接定义核函数
  • +
+

图池化

+

图粗化:将结点进行聚类,每一类作为一个超级结点

+

结点选择:学习一个评价标准去挑选比较重要的结点

+

图神经网络的表达能力

+

图神经网络在结点分类、链接预测、图分类上取得了巨大的成功,但是图神经网络的设计大多基于直觉、启发式方法或者实验试错,缺少对于图神经网络的理论理解。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第7讲 图卷积神经网络
+
https://zhangzhao219.github.io/2022/10/13/UCAS/advanced-ai/advanced-ai-7/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/13/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-5/index.html b/2022/10/13/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-5/index.html new file mode 100644 index 000000000..5360b4673 --- /dev/null +++ b/2022/10/13/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-5/index.html @@ -0,0 +1,833 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:模式识别与机器学习-第5章 统计机器学习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:模式识别与机器学习-第5章 统计机器学习

+ + +
+ +

《模式识别与机器学习》课程笔记:第5章 统计机器学习

+ +

第5章 统计机器学习

+

机器学习简介

+

桑克(R.Shank)“一台计算机若不会学习,就不能说它具有智能。”

+

机器学习更强调面向算法,而统计学更偏重于面向模型。换而言之,机器学习强调算法的结果要好,所以机器学习很关注损失函数。而统计学要先扔出来一大堆模型假设,然后站在模型上面通过严格的数学推导做出结果。

+

统计机器学习:是基于数据构建概率统计模型并运用模型对数据进行预测分析的一门学科。

+

机器学习的学习过程:

+
    +
  • 经验(E):训练数据
  • +
  • 模型(T):需要学习的目标函数
  • +
  • 学习算法:怎么样从经验中推断出模型
  • +
  • 评价(P):测试数据
  • +
+

机器学习的特点:

+
    +
  • 数据大量且廉价,知识昂贵而稀少
  • +
  • 数据产生过程的细节是未知的,但是数据产生的过程不是完全随机的。
  • +
  • 通过利用数据中的某些模式或规律从数据中学习模型:反推数据生成路径。
  • +
  • 模型通常不是完整过程的精确复制品,而是一种良好且有用的近似。
  • +
  • 模型可以描述从数据中获取知识,或对预测将来(具有预测性),或者两者兼而有之。
  • +
  • 几乎所有的科学都关注于用模型拟合数据:推理。
  • +
+

机器学习方法分类:

+
    +
  • 有监督学习:有标记数据 e.g. Fisher,、感知器算法、线性判别分析
  • +
  • 无监督学习:无标注数据,降维方法K-L
  • +
  • 半监督学习:无标注数据+有标注数据
  • +
  • 多任务学习:共享相关任务之间的表征
  • +
  • 迁移学习:训练数据与测试数据不是同分布的
  • +
  • 强化学习:间接的标注数据(状态和对应的reward)
  • +
  • 主动学习:主动选择训练数据
  • +
  • 自监督学习:从无标注数据提取监督信号。
  • +
+

自监督学习是自主监督学习。它提取并使用自然可用的相关上下文和嵌入式元数据作为监督信号。

+

统计机器学习

+

框架

+

输入训练样本,目标是损失函数期望风险最小化

+

期望风险最小化:

+

经验风险最小化:(导致过拟合)

+

结构风险最小化:

+

过拟合及正则化

+

怎么样在测试数据上预测得好?

+

两方面:

+
    +
  • 模型对训练数据拟合得好-需要复杂的模型
  • +
  • 模型具有一定的能力来容忍测试数据的不同行为-需要稳定的模型
  • +
+

正则项:在原来的经验损失函数中添加一个惩罚项,不鼓励复杂的模型

+

泛化能力分析

+

偏差-方差分解:expected loss=bias2+variance+noise

+

偏差:度量了模型的期望预测和真实结果的偏离程度

+

方差:刻画了数据扰动所造成的影响

+

噪声:与f相互独立,刻画了问题的难易程度

+

由正则化参数控制的偏差和方差对模型复杂性的依赖性说明:

+

大的值将权重参数拉至零导致较大偏差,较小的值允许对噪声进行微调,从而导致较大的方差

+
    +
  • 简单模型:低方差、高偏差
  • +
  • 复杂模型:高方差、低偏差
  • +
+

对模型复杂度问题的深刻理解:

+
    +
  • 非常灵活的模型具有低偏差和高方差。
  • +
  • 相对刚性的模型有大的偏差和低的方差。
  • +
  • 具有最佳预测能力的模型是使得偏差和方差之间最佳平衡的模型。
  • +
  • 偏差-方差分解的实际应用价值有限: +
      +
    • 偏差和方差无法计算,因为它依赖于了解x和y的真实分布。
    • +
    • 偏差-方差分解基于数据集集合的平均值,而实际上我们只有单个观测数据集。
    • +
    +
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:模式识别与机器学习-第5章 统计机器学习
+
https://zhangzhao219.github.io/2022/10/13/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-5/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/17/UCAS/information-retrieval/information-retrieval-15/index.html b/2022/10/17/UCAS/information-retrieval/information-retrieval-15/index.html new file mode 100644 index 000000000..283608ec7 --- /dev/null +++ b/2022/10/17/UCAS/information-retrieval/information-retrieval-15/index.html @@ -0,0 +1,824 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第15讲 基于深度神经网络的IR模型 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第15讲 基于深度神经网络的IR模型

+ + +
+ +

《现代信息检索》课程笔记:第15讲 基于深度神经网络的IR模型

+ +

第15讲 基于深度神经网络的IR模型

+

深度神经网络基础

+

最简单的神经网络-神经元

+

激活函数:主要作用是引入非线性,增强网络的表示能力。

+

最简单的多层神经网络-多层感知机

+

Softmax归一化是在使用神经网络进行分类时常用的方法,对于分类问题,通常需要给出可能属于每一个类别的概率,即需要输出介于 0 和 1 之间,且加和为 1

+

参数的学习

+

正则化

+

卷积神经网络

+

循环神经网络

+

Neural IR Model

+

Neural IR 模型分类

+

Representation based:学习文本的分布式表示 在高维空间匹配

+
    +
  • 词表示:one hot → distributed
  • +
  • 句子表示:bag of words → distributed
  • +
  • 匹配能力取决于学习文本表示的算法能力
  • +
  • 代表模型:DSSM, CDSSM
  • +
+

Matching function:文本之间先进行交互匹配,再对匹配信号进行融合

+
    +
  • 输入:比较底层的输入
  • +
  • 匹配函数:cosine, dot product → NN
  • +
  • 优点:可以考虑更加丰富的匹配信号, 如软匹配 (soft matching)
  • +
  • 代表模型:MatchPyramid , DRMM, K NRM, PACRR, NPRF
  • +
+

Combination of both: 既考虑 Representation 又考虑 Matching function

+
    +
  • 代表模型:Duet
  • +
+

DSSM:Deep Structured Semantic Models

+

word hashing: Bag of letter trigrams representation

+

模型:DNN学习查询,文本的语义表示, cosine相似度作为匹配评分

+

MatchPyramid:

+

考虑各种层次的匹配信号,包括单词层次、短语层次以及句子层次等等;

+

在图像领域,基于 CNN 特征提取的图像金字塔被证明是有效的

+

DRMM:相比普通的文本匹配任务,检索任务更需要关注相关性匹配

+

通过显式地对精确匹配信号,查询词重要度以及多样匹配要求进行建模,得到的模型更加适合于检索任务。

+

DRMM是第一个在 TREC 数据集能够取得比传统检索模型更好效果的基于 DNN 模型

+

DRMM的设计思路在一定程度上借鉴了传统的 TF-IDF

+

K-NRM:使用kernel pooling 技术提取多层次的软匹配 (soft match)特征

+

PACRR:通过将具有不同大小(k= lg 卷积核的卷积层作用于查询与文档间的单词-单词相似度矩阵,来对 k gram 匹配信息进行建模。

+

DUET:Representation与Matching function 的方法是互补的

+

SNRM:监督学习得到文本稀疏表示,解决效率问题

+

NPRF:将反馈文档视为原始查询的扩充表示,通过增强与查询相关的信息匹配信号获得更好的交互矩阵

+

总结与展望

+
    +
  • 基于DNN 的检索模型的研究虽然目前取得了一定的成果,但还有许多问题没有解决 +
      +
    • 尚未得到明显优于传统模型(如BM25+QE )的结果
    • +
    • 很多论文回避了与传统PRF 模型的比较
    • +
    +
  • +
  • CNN、统计直方图:有用; RNN :没有效果
  • +
  • 长文本IR 应用中往往 DNN 方法效果有限
  • +
  • 但是在商品推荐、基于title 的检索、 microblog retrieval 等短文本应用中效果不错
  • +
  • 通过CNN 等方法提取的特征 Vs 基于信息理论进行概率估计得到的特征
  • +
  • 很多在NLP 领域证明非常有效的方法,在 IR 领域尚未发挥威力
  • +
+

BERT

+

基于BERT的检索模型

+

稠密向量检索模型

+

直接改变了原有第一阶段的检索模式,通过BERT等预训练语言模型,将查询和文档都映射到语义空间中,编码成单个稠密向量表示,用ANN 算法来进行检索。在一定程度上缓解了词汇不匹配问题,并将检索阶段的匹配效果推到了一个新的台阶

+

模型框架:一般采用双塔结构对查询和文档单独地编码得到二者独立的表达,从而使文档可以进行离线索引。

+

RepBERT:平均词项表示作为文档的单个向量

+

ANCE:利用k-1的模型来检索得到top-n文档并随机采样负样本,每隔一段时间都需要对训练数据中的负样本进行更新,因此该方法的训练代价较大。

+

RocketQA:与ANCE相比,做了额外的denoised操作;

+

TCT-ColBERT:通过蒸馏技术,将ColBERT的强建模能力蒸馏到类似于RepBERT这样的双塔架构上去

+

Condenser:为了将更完整的序列信息压缩到CLS 位置上

+

DAR:通过插值、扰动的方式在文档表示层面进行数据增强

+

JPQ:除了直接采用乘积量化(Product Quantization, PQ )方法来压缩向量外,将乘积量化后的文档d†表示用于模型训练,通过排序排序训练目标来优化 PQ 的聚类中心表示

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第15讲 基于深度神经网络的IR模型
+
https://zhangzhao219.github.io/2022/10/17/UCAS/information-retrieval/information-retrieval-15/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月17日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/17/future-research/index.html b/2022/10/17/future-research/index.html new file mode 100644 index 000000000..beb296865 --- /dev/null +++ b/2022/10/17/future-research/index.html @@ -0,0 +1,790 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 未来方向的调研 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

未来方向的调研

+ + +
+ +

对自己未来方向的调研

+ +

已经工作

+

冯律

+

赵海宇

+

陈建宏

+

霍腾飞

+

汪嘉富

+

者佳赟

+

学长学姐

+

谢瑞峰

+

2022.10.16 要失业了,商汤都要裁员了,NLP还是可以的

+

谢慧萱

+

张亚强

+

申瑞红

+

阎旭

+

葛璐豪

+

同届

+

白庚淮

+

陈国鑫

+

邹文浩

+

高野淇

+

李翀

+

2022.10.10

+

目前先学算法,然后找算法实习,也是学一些开发

+

实验室的学长:有个拿了农行offer,带我的学长快手转正了

+

都有暑期实习:2个字节算法,1个字节产品,2个快手算法,还有个好像去电信了

+

我可能就没什么发论文的想法,打打算法比赛,学学开发,找个大厂实习,然后毕业银行、国企,或者如果机会好,再考虑

+

我学长他们没论文,也能找到算法岗

+

如果不是上海广州,我觉得,得学开发吧,毕竟其他城市可能算法岗更少

+

于书懿

+

李一鸣

+

算法岗不动摇

+

胡坤霖

+

王菡

+

王子潇

+

2022.10.15

+

我觉得,现在能做的就两点 1. 多卷一点 2. 降低预期

+

准备下一个实习去开发

+

开发的话,就那些技术,我觉得方向上不会错太多,不像算法,跳坑里就出不来了

+

我最近在做一个自己想的项目,学了学web前端,之后再学学后端,把项目做出来,然后争取寒假前找个实习

+

前端是 vue,后端现在想的是 node + mogoDB + SQL,但后端还没咋研究,不好说

+

我本来想的是,去微软苏州,或者先去微软北京再rebase苏州,我实在不想加班

+

而且微软苏州人多,也买得起房,但现在进微软也好难的,不知道2年后咋样

+

我选这个导师,也是考虑到他和微软有合作,但进来才知道他合作的都是MSRA,每年全国就要几个人,都是博士打架。。。

+

八股内容很多,感觉很难搞,写程序的时候,经常觉得底层懂得太少,包的源码也看不懂

+

挺想再学学计网,偏实践的,还想做csapp

+

学弟学妹

+

陈才

+

2022.10.1

+

今年秋招的算法岗比开发岗还惨,搞算法投开发岗直接简历挂

+

赛道还是挺重要的,要是搞算法没发好一点的论文应该就直接寄了,还是趁早转赛道

+

可以去搞大数据嘛

+

去年秋招和今年秋招比是一个天上一个地下,今年阿里腾讯秋招hc大概只有几百个,而且大部分都要留给实习生,基本等于不招人。其他大厂的hc也基本缩减50%以上

+

大厂hc少导致一些中厂的简历也是暴涨,我知道的网易的java岗有10w份简历。还有小红书也是几万几万的简历

+

今年字节卡简历很严重,实习成了进大厂的捷径

+

2022.10.8

+

web3这条赛道挺好

+

江灵杰

+

2022.10.11

+

看到知乎上好多ict的学长去lab实习的,感觉学长也可以考虑下这个,不过好像准备硕士直接工作的话,去做一下面向业务的岗好像帮助要大点

+

毕江源

+ + +
+ +
+
+ + + + + + +
+
+
未来方向的调研
+
https://zhangzhao219.github.io/2022/10/17/future-research/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月17日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/18/UCAS/machine-learning/machine-learning-10/index.html b/2022/10/18/UCAS/machine-learning/machine-learning-10/index.html new file mode 100644 index 000000000..5e8c7af8f --- /dev/null +++ b/2022/10/18/UCAS/machine-learning/machine-learning-10/index.html @@ -0,0 +1,975 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-第10章 神经网络与深度学习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-第10章 神经网络与深度学习

+ + +
+ +

《机器学习》课程笔记:第10章 神经网络与深度学习

+ +

第10章 神经网络与深度学习

+

概述

+

背景与现状

+

ANN到DL的技术发展

+
    +
  • ANN始于1890年:开始于美国心理学家W.James对于人脑结构与功能的研究。
  • +
  • M-P模型 (1943 年):神经科学家麦卡洛克和数学家皮兹建立了神经网络和数学模型(MP模型),人工神经网络的大门由此开启。
  • +
  • Hebb学习规则(1949年):加拿大著名心理学家唐纳德·赫布提出了Hebb学习规则,这是一种无监督的学习规则。 Hebb学习规则表明了网络是可以学习的,这启发了后面一系列关于神经网络的研究工作。
  • +
  • 感知机(1958 年):心理学家Frank Rosenblatt受到Hebb思想的启发提出了感知机。感知机是最早的人工神经网络,也是具有学习功能M-P模型。整个1958 年-1969年期间,有许多科学家和学者都投入到了感知机研究。但是由于当时的计算水平相对落后,计算也显得很吃力。
  • +
  • 1969年进入冰河期:马文明斯基在发表《 Perceptrons 》时,证明了感知器的致命弱点:不能够解决异或问题。
  • +
  • 神经网络(1986 年)BP 算法:Rumelhar和Hinton提出了反向传播算法(BP 算法),是一种监督学习算法,解决了两层神经网络计算的复杂性。
  • +
  • 卷积神经网络(1989年):1989年, LeCun发明了卷积神经网络LeNet,并将其用于数字识别,且取得了较好的成绩,不过当时并没有引起足够的注意。
  • +
  • RNN模型:递归(recurrent)的现代定义由Jordan(1986 年),随后Elman(1990 年)的RNN网络。
  • +
  • LSTM模型(1997年):LSTM的提出,尽管该模型在序列建模上的特性非常突出,但由于正处于ANN 的下坡期,也没有引起足够的重视。
  • +
  • 深层信度网络(2006 年):2006DL元年,Hinton提出了深层网络训练中梯度消失问题的解决方案: 无监督预训练对权值进行初始化,并
    +进行有监督训练微调 。但是由于没有特别有效的实验验证,该论文并没有引起重视。
  • +
  • ReLU激活函数(2011 年):该激活函数能够有效的抑制梯度消失问题。
  • +
  • 语音识别突破(2011 年):微软首次将DL 应用在语音识别上,取得了重大突破。
  • +
  • ImageNet竞赛夺冠(2012 年):Hinton团队首次参加ImageNet图像识别比赛,其通过构建的AlexNet网络一举夺得冠军。
  • +
  • AlphaGo (强化学习):2016年 3 月人工智能围棋比赛,谷歌( Google )旗下 DeepMind 公司的戴维 · 西尔弗、艾佳 · 黄和戴密斯 · 哈萨比斯与他们的团队开发的 AlphaGo 战胜了世界围棋冠军、职业九段选手李世石,并以 4:1 的总比分获胜。
  • +
  • 深度学习的技术突破:生成对抗、注意力机制、预训练模型
  • +
+

DL在AI的成功应用

+

语音识别

+

2009年, Hinton把深层神经网络介绍给做语音识别的学者们。2010年,语音识别就产生了巨大突破。本质上是把传统的混合高斯模型(GMM)替换成了
+深度神经网络(DNN)模型,但相对识别错误率一下降低20%多,这个改进幅度超过了过去很多年的总和。这里的关键是把原来模型中通过 GMM 建模的手工特征换成了通过 DNN 进行更加复杂的特征学习。

+

在此之后,在深度学习框架下,人们还在不断利用更好的模型和更多的训练数据进一步改进结果。现在语音识别已经真正变得比较成熟,并且被广泛商用,目前所有的商用语音识别算法没有一个不是基于深度学习的。

+

计算视觉:通过组合低层特征形成更加抽象的高层特征

+

DL在图像识别

+

Yann LeCun早在1989年就开始了卷积神经网络的研究,取得了在一些小规模(手写字)的图像识别的成果,但在像素丰富的图片上迟迟没有突破,直到2012年Hinton和他学生在ImageNet上的突破,使识别精度提高了一大步;截至2015年最好的模型ResNet

+

2012年 Google Brain 用 16000 个 CPU 核的计算平台训练 10 亿神经元的深度网络,无外界干涉下自动识别了“Cat”

+

2014年香港中文大学教授汤晓鸥研究组DeepID的深度学习模型,在 LFW 数据库上获得了99.15%的识别率,人用肉眼在LFW上的识别率为97.52%,深度学习在学术研究层面上已经超过了人用肉眼的识别 。

+

自然语言处理

+

词向量表示学习

+

词向量是指通过对大量文本的无监督学习,根据前后文自动学习到每个词的紧凑向量表达,包括NNML 、 Word2Vector 、预训练模型等。

+

预训练模型:ELMo、 GPT和BERT 等,全线提升自然语言领域的多项任务的Baseline

+

递归神经网络 RNN:文本的各个词之间是有顺序的,RNN能更好的挖掘和利用这个性质,在自然语言各个领域都在尝试进行中。 已经有BPTT 、 LSTM等。

+

神经网络模型概述

+

神经网络模型学习框架

+

xuQS9e.md.png

+

损失函数:

+

平方损失:

+

交叉熵损失:

+

单个神经元模型:

+

xuQHPS.md.png

+

单个神经元模型:

+
    +
  • 感知机
  • +
  • 最小方差回归
  • +
  • Logistic模型
  • +
+

多层感知机

+

卷积网络

+

核函数网络:单隐层神经网络、非线性体现在径向基核函数

+
    +
  • 径向基网络
  • +
  • 支持向量机
  • +
+

自组织映射

+

RBM

+
    +
  • 同层神经元间无连接,并彼此相互独立
  • +
  • 是一个无向图(权值对称),即连接可看作双向的
  • +
  • 层为隐层,层为可见层
  • +
+

递归网络

+

深度网络模型概述

+

深度前馈网络

+

常见的结构:

+
    +
  • 全连接网络DFL
  • +
  • 预训练+全连接网络 Au+FL
  • +
  • 卷积+全连接网络 CNN+FL
  • +
  • CNN + FL+ ReLu + Tricks
  • +
+

递归神经网络

+

常见的结构:

+
    +
  • Bi结构
  • +
  • Deep结构
  • +
  • CNN+RNN结构
  • +
+

生成对抗网络(GAN)

+

两个网络博弈:G(Generator)和D(Discriminator)

+
    +
  • G是一个生成图片的网络,它接收一个随机的噪声z,通过这个噪声生成图片,记做G(z)。
  • +
  • D是一个判别网络,判别一张图片是不是“真实的”。它输入一张图片x,输出D(x)代表x为真实图片的概率,如果为1,就代表100%是真实的图片,而输出为0,就代表不可能是真实的图片。
  • +
+

深度强化学习

+

强化学习:学习目标:策略概率

+

值函数网络:Deep Q-Learning

+

策略网络:Deep Policy Network

+

多层感知机

+

含有数据输入层、1个以上隐藏层、 1个输出层;各层神经元全连接,同一层神经元之间无连接。

+

xu1LBn.md.png

+

多层感知机的运算:

+

xu3VN6.md.png

+

激活函数(包括硬门限阈值函数),是导致网络运算非线性的直接原因。

+

问题描述

+

学习问题:

+

学习目标:调整神经元连接权重值,使得平均误差能量最小。

+

两种方法:批量学习和在线学习。

+

目标:最小化损失函数

+

批量学习(Batch Learning)

+
    +
  • N个样本(一个batch)
  • +
  • 随机采样 batch 训练样本集
  • +
  • Batch by Batch 调整权值
  • +
  • 优点:梯度向量形式固定,有利于并行处理
  • +
  • 缺点:需要内存资源大
  • +
+

在线学习(Online Learning):sample by sample 调整权值

+

xu8Hwn.png
+优点:容易执行、存储量小、有效解决大规模和困难模式的分类。

+

缺点:学习过程随机、不稳定。

+

BP基本思想

+

两个方向的信号流、两个方向的函数运算

+

函数信号:计算输出函数信号

+

误差信号:计算梯度向量

+

数据前馈运算

+

xuGRB9.md.png

+

梯度反馈运算

+

xuGhA1.md.png

+

BP 算法小结

+
    +
  1. 数据初始化
  2. +
  3. Epoch 采样
  4. +
  5. 前向计算
  6. +
  7. 反向梯度计算
  8. +
  9. 求参数梯度
  10. +
  11. 迭代
  12. +
+

激活函数

+

异或问题

+

改善性能的试探法

+

函数逼近

+

卷积网络

+

卷积层:卷积层具有局部连接和权重共享特点。

+

一维、二维卷积

+

卷积层的输出尺度

+

卷积层的参数个数

+

子采样层:每个通道,通过下采样,缩减尺度。

+

典型实例:LeNet-5

+

Recurrent 网络

+

四种基本递归结构

+
    +
  1. 输入-输出递归模型(NARX 模型)
  2. +
  3. 状态空间模型
  4. +
  5. 递归多层感知机
  6. +
  7. 二阶网络
  8. +
+

通用逼近定理:如果网络具有充分多的隐藏神经元,任意的非线性动态系统可以由递归神经网络以期望的精度来逼近,对于状态空间的紧致性没有限制。

+

计算能力

+

Recurrent 网络

+

RNN分回合训练

+

RNN连续训练

+

RNN长期依赖

+

RNN扩展的递归结构

+

前沿概述

+

深度学习

+

深层结构:神经网络 + 深层结构 + 优化 + 计算资源 + 人工智能应用

+

梯度消失:解决梯度消失

+
    +
  • 前馈网络:自编码、ReLU 激活函数
  • +
  • Recurrent 网络:二次优化、非线性逐次状态估计、ReLU 激活函数
  • +
+

视觉识别

+

自然语言处理

+

生成对抗学习

+

生成对抗模型原理

+

生成器(Generator):尽可能去学习真实样本的分布,迷惑鉴别器。

+

鉴别器(Discriminator):尽可能的正确判断输入数据是来自真实数据还是来自生成器。

+

损失函数:

+

训练过程:生成器与鉴别器交替训练,互相提升各自的生成能力和鉴别能力,最终寻找二者之间的一个纳什均衡。

+

强化学习

+

马尔科夫决策过程:

+

智能体环境交互-智能体的目标是最大化将来的期望累积奖励

+

知识图谱

+

背景

+

知识图谱的概念最早出现于Google公司的知识图谱项目,体现在使用Google搜索引擎时,出现于搜索结果右侧的相关知识展示。

+

截止到2016 年底,Google知识图谱中的知识数量已经达到了600亿条,关于1500个类别的5.7亿个实体,以及它们之间的3.5万种关系。

+

实体、关系和事实:

+
    +
  • 实体(entity):现实世界中可区分、可识别的事物或概念。
  • +
  • 关系(relation):实体和实体之间的语义关联。
  • +
  • 事实(fact): (head entity, relation, tail entity) 三元组形式。
  • +
+

狭义知识图谱

+

狭义知识图谱:具有图结构的三元组知识库。

+

节点:实体。 边:事实(由头实体指向尾实体)。 边的类型:关系。

+

链接预测、三元组分类:知识图谱上的链接预测

+

分布式知识表示方法分类:

+
    +
  • 位移距离模型 (translational distance models):采用基于距离的打分函数来衡量三元组成立的可能性。
  • +
  • 语义匹配模型 (semantic matching models):采用基于相似度的打分函数来衡量三元组成立的可能性。 +
      +
    • 简单匹配模型:RESCAL及其变种-将头实体和尾实体的表示进行组合后再与关系的表示进行匹配
    • +
    • 复杂匹配模型:深度神经网络-利用较为复杂的神经网络结构完成实体和关系的语义匹配
    • +
    +
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-第10章 神经网络与深度学习
+
https://zhangzhao219.github.io/2022/10/18/UCAS/machine-learning/machine-learning-10/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月18日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/19/KMP/index.html b/2022/10/19/KMP/index.html new file mode 100644 index 000000000..68187f270 --- /dev/null +++ b/2022/10/19/KMP/index.html @@ -0,0 +1,1029 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KMP算法详解 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

KMP算法详解

+ + +
+ +

KMP算法详解

+ +

KMP算法详解

+

一直都没弄明白,也没下决心去弄明白。昨天感觉基本上差不多了,整理一下,再加深一下印象。

+

问题

+

给你两个字符串 haystackneedle,请你在 haystack字符串中找出 needle字符串的第一个匹配项的下标(下标从 0开始)。如果 needle不是 haystack的一部分,则返回 -1

+

AC代码:

+
func strStr(haystack string, needle string) int {
+    needlelen := len(needle)
+    haystacklen := len(haystack)
+    next := make([]int,needlelen)
+    next[0] = 0
+    j := 0
+    for i:=1;i<needlelen;i++{
+        for j > 0 && needle[i] != needle[j]{
+            j = next[j-1]
+        }
+        if needle[i] == needle[j]{
+            j++
+        }
+        next[i] = j
+    }
+    j = 0
+    for i:=0;i<haystacklen;i++{
+        for j > 0 && needle[j] != haystack[i]{
+            j = next[j-1]
+        }
+        if needle[j] == haystack[i]{
+            j++
+        }
+        if j == needlelen{
+            return i-j+1;
+        }
+    }
+    return -1
+}
+

简介

+

判断一个字符串(模式串)是不是另外一个字符串(文本串)的子串,怎么做?

+

最容易想到的:暴力匹配。

+

比如有下面的两个字符串:

+

abacacac

+

开始肯定是第一个 a开始和 ac进行匹配,匹配失败了,然后从 b再开始匹配。最坏情况,每一个都要判断到匹配字符串的最后一个字符,两层循环,时间复杂度很容易想到就是

+

但是事实上,如果从人工匹配的角度来看,我们都知道 b不可能匹配成功,让你用肉眼匹配,傻子才会去看 b。但是计算机程序为了全部判断还是要去尝试一下。

+

那么怎么把这种无效的匹配让开呢?直观上可能想到,我判断第一个能不能匹配上不就行了,应该能降低时间复杂度?

+

那么再举一个例子:aaaaaaaaaaab,时间复杂度一样是

+

所以不仅仅要看第一个,看第一个也无法完全抹去无效的匹配。这时候需要一种高效的匹配算法,核心思想就是在匹配的过程中要记录,匹配失败后从第一个可能成功的地方开始即可,不要做无效工作。

+

因此就有了超难理解的KMP算法以及各种比KMP还要复杂的算法。这里就先好好的讲一下KMP,希望以后可以真正理解,抬手就来。

+

概念

+

前缀表:记录下标 i之前(包括 i)的字符串中,有多大长度的相同前缀后缀。

+

前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串

+

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

+

啥意思?举例子就好了

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模式串下标0123456789101112
字符串abcdabcabcdab
前缀表0000123123456
+

怎么算的?

+

下标为 0,字符串为 a,前缀为空(因为不包含最后一个字符,因此字符就没了),后缀为空(因为不包含第一个字符,因此字符就没了),因此相同前缀后缀长度为0(因为都是空串)

+

下标为 1,字符串为 ab,前缀为 a,后缀为 b,因此相同前缀后缀长度为 0

+

下标为 2,字符串为 abc,前缀为 ab,后缀为 bc,因此相同前缀后缀长度为 0

+

下标为 3,字符串为 abcd,前缀为 abc,后缀为 bcd,因此相同前缀后缀长度为 0

+

下标为 4,字符串为 abcda,前缀为 abcd,后缀为 bcda,因此相同前缀后缀长度为 1,也就是 a

+

下标为 5,字符串为 abcdab,前缀为 abcda,后缀为 bcdab,因此相同前缀后缀长度为 2,也就是 ab

+

下标为 6,字符串为 abcdabc,前缀为 abcdab,后缀为 bcdabc,因此相同前缀后缀长度为 3,也就是 abc

+

下标为 7,字符串为 abcdabca,前缀为 abcdabc,后缀为 bcdabca,因此相同前缀后缀长度为 1,也就是 a

+

+

下标为 12,字符串为 abcdabcabcdab,前缀为 abcdabcabcda,后缀为 bcdabcabcdab,因此相同前缀后缀长度为 6,也就是 abcdab

+

人工计算还是挺好算的,用眼睛看看简单算算就行了。网上有些资料是从 -1开始,然后右移一位,我认为不好理解,不如保留前缀表的本意

+

所以算来算去,前缀表有什么用处呢?

+

前缀表可以帮助我们在匹配不成功的时候找到前面最佳的重新开始的位置,从而保证我们只遍历文本串一次就能判断模式串与文本串是否匹配。(废话)

+

先举例:后面的 i指文本串的下标,j指模式串的下标。(文本串下标保证递增,绝对不回退)

+ + + + + + + + + + + + + + + + + + + + + + + +
文本串下标012345
字符串acbaba
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模式串下标01234
字符串acbac
前缀表00012
+

开始匹配,ij匹配的很顺利,转眼就到了 i=j=4,然后发现糟了,匹配不上了,现在 j要回退,找到重新开始匹配的位置。

+

j退到哪里呢?因为 j是没有匹配上的,而 j-1如果有意义(j≠0),一定是能匹配上的!(为什么?因为只有匹配上了 j才会移动,j移动过的位置一定是之前匹配好了的)

+

那么 j-1是匹配上的又说明了什么呢?说明对于 0~j-1的字符串,如果有相同的前缀后缀,一定也是能和i-1匹配的上的,因此就不需要回退超过前缀的位置!

+

还是上面的例子,模式串的 j-1前缀表的值是 1,说明 j-1位置的 a在模式串的前面也出现过,就是模式串 0位置的 a。由于 j-1是和 i-1匹配上了的,因此 j=0i-1也是匹配上了的,不需要再去看模式串 0的位置,只需要看0的后一个位置 1i是否能匹配上就好了!

+

流程步骤:

+
    +
  1. ij匹配不上了,隐含条件是 i-1j-1是可以匹配的
  2. +
  3. 看一下 j-1后缀的相同长度的前缀长度,也就是 next[j-1]的值
  4. +
  5. 回退 jnext[j-1]的位置,隐含了这一步将相同长度的前缀绕过
  6. +
+

然后 j=next[j-1]后就去判断 ji是不是相同就好了,很不幸的是,还是不相同,i指向的是 bj指向的是 c

+

那么没办法,留着这个前缀也无法匹配了,只好再次回退,这一退就退到 j=0了,但是还是不相等。

+

j=0就没有办法再次回退了,只好 i++,舍弃这一个部分的文本串,开始新的文本串。

+

到这里应该明白了前缀表的作用了,字面上很难理解,跟着流程走一遍就明白它的思想了,确实精妙

+

字符串匹配

+

所以在已知 next数组的前提下,这个字符串匹配的代码就很简单了。虽然简单,但是结构一点都不可以修改,循环和顺序都是精心设计的。

+
j = 0
+for i:=0;i<haystacklen;i++{
+    for j > 0 && needle[j] != haystack[i]{ // 匹配不上就一直回退,j=0说明真的匹配不上了,跳出来i++
+        j = next[j-1]
+    }
+    // j=0也会跳到这里尝试一下
+    if needle[j] == haystack[i]{ // 匹配上的就能j++去看模式串的下一个字符了,然后进入下一个循环i++,判断文本串的下一个字符能不能和模式串的这个字符匹配上
+        j++
+    }
+}
+

还有一个问题,next数组怎么求?

+

next数组

+

首先要明确一点,next数组是针对模式串而言的,与文本串半毛钱关系没有

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模式串下标0123456
字符串abaabae
前缀表0011230
+

其实思想和匹配是相同的,不同的地方在于上面的是用模式串和文本串进行匹配,这里是用自己和自己进行匹配,匹配的过程中看看能匹配上多少,就能得出 next数组的数值了

+

next[0]=0,初始化毫无争议,因为空串一定是0

+

指针 i同样一直向前,指针 j会进行回退,因为 next[0]确定了,因此直接初始化i=1

+

最开始,j指向的是 0的位置,就在这里等着到底哪个 i能和我这个可爱的 j匹配上

+

到了 i=2,匹配上了!这时候 j不满足了,是不是 i+1也能和 j+1匹配上呢?所以就 j++,尝试匹配下一个

+

要是匹配不上了怎么办呢?比如 j=3,i=6匹配不上了,也隐含了条件,就是 j=2是能和 i-1匹配上的(要是匹配不上j也不可能不等于0

+

那么j=2时候的相同长度的前后缀在哪里呢?因为如果相同也不需要去看了,所以更新j=next[j-1]就可以了,和上面的字符串的匹配思想是完全相同的。

+

如果还是匹配不上,那么j只好乖乖变为0,等待着下一个能匹配上的将j+1

+

代码如下:

+
next[0] = 0 // 初始化
+j := 0 // j指向首位
+for i:=1;i<needlelen;i++{ // 遍历模式串,不回退
+    for j > 0 && needle[i] != needle[j]{
+        j = next[j-1] // 匹配不上了,绕过已知的相同长度的前后缀,直到变为j=0的初始状态
+    }
+    // 如果j=0还是有一次判断的机会的
+    if needle[i] == needle[j]{ // 匹配上了将j解放出来,+1再试试
+        j++
+    }
+    next[i] = j // 赋值next数组
+}
+

时间复杂度分析

+

n为文本串长度,m为模式串长度

+

在匹配的过程中,根据前缀表不断调整匹配的位置,如果没有一个字符能匹配的上,时间复杂度就是文本串的指针从头移到尾,也就是

+

如果能匹配上一些字符,回退的次数也不可能超过 n次。因此时间复杂度是

+

生成 next数组,不会比匹配的时间复杂度高(因为如果模式串比文本串还要长,根本就不需要匹配了)

+

所以从平方级别的时间复杂度直接降到了线性的时间复杂度。

+

总结

+

看过很多遍,应该也曾经懂过,就是从来没有整理过,因此可能也没有真正懂过。

+

希望这次能真真正正懂了,后面忘记了再来看看这篇文章,希望能快一些想起来。

+ + +
+ +
+
+ + + + + + +
+
+
KMP算法详解
+
https://zhangzhao219.github.io/2022/10/19/KMP/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月19日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/20/UCAS/advanced-ai/advanced-ai-8/index.html b/2022/10/20/UCAS/advanced-ai/advanced-ai-8/index.html new file mode 100644 index 000000000..68369e514 --- /dev/null +++ b/2022/10/20/UCAS/advanced-ai/advanced-ai-8/index.html @@ -0,0 +1,1507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第8讲 逻辑 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第8讲 逻辑

+ + +
+ +

《高级人工智能》课程笔记:第8讲 逻辑

+ +

第8讲 逻辑

+

什么是“数理逻辑”?

+
    +
  • 一种“算法”:输入+输出,不仅要得到算法,还要证明是正确的
  • +
  • 一种“关于证明、推理等思考活动”的算法
  • +
+

一个算法,以任何作为输入,输出的都是正确答案

+

输入:

+
    +
  • 知识库:任意的问题假设,前提条件等
  • +
  • 查询:想要解决的问题
  • +
+

输出答案:该查询在此知识库上的正确答案

+

如果有上面的算法,那么所有难题都能得到解决

+

如果有这样的一种“终极算法”,首先要将自然语言表达的知识库和查询表示成形式语言表达的知识库和查询,然后通过自动的知识推理,得到形式语言表达的答案

+

逻辑的研究内容

+

解决如下问题:

+
    +
  • 关于逻辑的形式语言是什么
  • +
  • 在形式语言上的自动推理的算法是什么 +
      +
    • 该算法复杂度如何,是否可以更高效
    • +
    +
  • +
  • 自动推理的算法是否正确 +
      +
    • 算法正确性的严格证明
    • +
    +
  • +
+

研究形式化定义的sentences之间的关系

+

x6PrGD.md.png

+

左侧是语义的蕴含关系(逻辑推导),,从知识库出发一定正确的知识

+

右侧是语法的演绎关系(形式推演),,通过算法可以从知识库推出的

+

如果左侧的是右侧的子集,说明正确的结论都在算法推导的里面,那么说明这个算法是完备的,但是有一些结论可能算法计算出来是错误的

+

如果右侧的是左侧的子集,说明算法推出来的结论都是正确的,因此算法是可靠的,但是有可能有一些正确的结论算法算不出来

+

如果兼具完备性和可靠性,那么证明这个算法是正确的。

+

语义

+

如果的条件下是 true,那么称是句子的一个 model,句子的所有model的集合是

+

KB指的是一些句子的集合

+

:在任意的条件下(一个真值指派)只要成立,一定成立,称为 KB蕴含

+

因此完全等价(当且仅当)(是不可满足的)

+

命题逻辑

+

语法(逻辑推导)

+

命题是一种声明,要么是真的,要么是假的,不存在第三种可能

+

命题逻辑通常不考虑时间

+

原子命题指的是最小的命题,用大写字母表达

+

文字是原子命题,或者是原子命题的否

+

一个句子是一个原子句或者复杂句

+

一个原子句表示为:

+

复杂句有五种表示形式,与复杂句之间的真值表:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
falsefalsetruefalsefalsetruetrue
falsetruetruefalsetruetruefalse
truefalsefalsefalsetruefalsefalse
truetruefalsetruetruetruetrue
+

连接词和集合之间的联系:

+

+

+

两个句子是逻辑等价的-两个句子的model相同: 当且仅当

+

定理:

+

+

KB: 满足命题逻辑语法的sentence的集合

+

假设:这组sentence中,一共有n个原子命题

+

真值指派(truth assignment):对每个原子名字赋值

+

一共有种真值指派,其中:使得KB中的每个sentence都为真的真值指派,就是KB的model

+

在此基础上,在命题逻辑中,我们可以明确的定义

+
    +
  • 如果一个句子在任意的model下面都为true,则这个句子是永真的
  • +
  • 演绎定理: 当且仅当是永真的
  • +
  • 如果一个句子在某些model下为真,则称这个句子是可满足的
  • +
  • 如果一个句子在任何model下都为假,则称这个句子是不可满足的
  • +
  • 当且仅当是不可满足的
  • +
+

蕴含,不是连接词:描述的是蕴含的一种关系,有了知识表示后,额外推出其他的知识

+

命题逻辑里面的连接词,用于知识表示(实际上是可以替代的,但是引入这个符号进行知识表示比较方便)

+

形式推演

+

推出:,通过算法可以从知识库推出的

+

共有两套规则(11条规则和归结原理)

+

11条形式推演规则:(不需要背诵)

+

形式可推演性:A在命题逻辑中由形式可推演的,记作,当且仅当能由(有限次使用)命题逻辑的形式推演规则生成

+

句子可以通过规则从KB中得出,记作

+

可靠性:任意时刻当时,同时成立,那么说是可靠的

+

完备性:任意时刻当时,同时成立,那么说是完备的

+

归结原理

+

合取范式:子句(文字和析取符号)的合取形式,子句内部是没有合取的(CNF)转换为合取范式是多项式时间复杂度的

+

归结原理:互补文字可以互相消去(但是每一次只能消去一对)

+

归结原理是既可靠又完备的

+

证明:若当且仅当,其中仅使用归结法则获得新子句

+

使用上述证明来证明知识库可以推出某个子句

+

证明:归结原理既可靠又完备

+

在研究可靠性与完备性问题时,应当把语法层面的知识理解为Groundtruth

+

因此可靠性可以大概表述为:语义上推演得到的知识在语法上正确。因此要证明归结原理的可靠性,即证明

+

xqkuwj.md.png

+

使用真值表进行证明即可

+

完备性可以大概表述为:如果语法上能够推理得到的,那么语义上正确。

+

即证明:如果,则

+

RC(S):对S内部的全部子句进行归结后的集合。

+

完备性证明

+

等价于永假,等价于是不可满足的。

+

等价于可以归结出空子句,即RC(S)包含空子句

+

则只需要证明:如果是不可满足的,则RC(S)包含空子句

+

等价于证明逆否命题:如果RC(S)不包含空子句,则是可满足的

+

证明:针对S中的原子命题,我们构造如下的model:

+

首先,因为RC(S)中不包含空集,即RC(S)中不包含永假的子句。

+

, 顺序指派的真值:

+

如果RC(S)中包含一个子句,此子句包含,且此子句的其它文字都已经被指派为False(在之前的步骤中进行的)或不包含其它文字,则把指派为False;否则,把指派为True

+

我们用反证法证明:这个真值指派使得RC(S)中的子句都为真

+

假设,在此过程的第i步,我们这样来指派使得某个子句C为False,且假设这是首次出现False的子句;此时,子句C只能是如下两种形式之一:或者

+

显然,如果RC(S)中只包含以上两个子句之一,子句C是不会在此真值指派中为False的。因此, RC(S)此时应该同时包含了以上两个子句。

+

以上两个子句显然是满足归结条件的,也就是说,它归结后的子句也应该在RC(S)中;同时,该子句已经被指派为False了;这与我们之前的假设矛盾。

+

因此这个真值指派使得RC(S)中的子句都为真,进而S是可满足的。

+

可以转换为搜索问题,如何使用A*搜索实现呢?

+

Modus Ponens规则

+

以限制知识库里面的句子形式为代价,获得时间复杂度上的提升

+

上述提到的归结原理具有完备性,这是很好的性质,对于许多现实世界的应用,如果添加一些限制,可以实现更高效的推理。为了换取更好的inference的时间效率,缩小命题逻辑的表达范围,得到适用于Horn Form的Modus Ponens规则,是另外一种形式的归结原理。

+

KB为Definite clause的合取形式:

+

xq1HJS.png

+

Definite clause: 每一个句子有且只有一个文字是正文字的析取形式

+

只有两种形式:①原子命题②命题的合取另外一个命题

+

Horn clause: 每一个句子最多一个文字是正文字的析取形式

+

PPT例子:KB是全部句子的情况下是否能推出Q

+

前向推理:从条件出发去推结论

+

前向推理是数据驱动的,可能推出一些结论与我们要推出的结论是无关的

+

后向推理:从结论返回推出条件

+

后向推理是目的驱动的,找为了推出这个结论所需要的条件,因此通常情况下后向推理比前向推理好,但是也存在某种情况前向推理比后向推理好

+

(全连接神经网络)

+
Modus Ponens规则证明
+

证明是可靠的,即证明

+

通过真值表进行证明即可

+

证明是完备的:

+

。此时,中仅包含definite子句,仅使用Modus Ponens规则,且是一个正文字

+

证明:RC(KB)是KB中原始的句子和通过Modus Ponens推出的句子的全部集合

+
    +
  1. 构造如下的真值指派:对于任意的symbol a,a指派为True当且仅当
  2. +
+

(如果一个正文字在中,就设为True,不在就设置为False)

+
    +
  1. 接下来证明:在下,为真。
  2. +
+

反证:若此时为False,那么:必存在一个definite子句,在下为False。

+

若该子句为 也就是说,在m中,均为True,且为False。根据1中的定义, ,又根据Modus Ponens规则,根据1中的定义,在中, 为True。推出矛盾。

+

若该子句为,在下为为False,则,矛盾

+
    +
  1. ,根据蕴含的定义:在中,为真;则根据1中的定义,,也就是说:
  2. +
+

命题逻辑的缺点:能表达的东西比较有限。

+

一阶谓词逻辑

+

语法和语义

+

命题逻辑假设世界上都是事实(fact),一阶谓词逻辑认为世界上还包括对象、关系和函数等等。

+

基本元素:

+
    +
  1. 常量和变量
  2. +
  3. 谓词(哥哥、大于等)Predicates
  4. +
  5. 函数
  6. +
  7. 连接词(与命题逻辑的连接词完全相同)
  8. +
  9. 为真当且仅当指向现实世界中的同一个对象
  10. +
  11. 量词:全称量词和存在量词
  12. +
+

简单句与复杂句

+

简单句:

+

或常量或变量

+

嵌套函数会造成很大的问题。命题逻辑的算法一定会停止(decidable可判定的),但是由于嵌套函数的存在,谓词逻辑只是半可判定的。

+

复杂句:使用连接词对简单句进行连接构成复杂句

+

量词

+

在谓词逻辑中,要将每一个符号指派到现实世界中,将常量转化为对象、将谓词转化为关系、将函数符号转化为真正的函数

+

量词与变量是对应的,有变量一定要有量词来量化

+

全称量词:变量所有实例的合取形式

+

+

错误的形式:

+

存在量词:变量所有实例的析取形式

+

+

错误的形式:

+

量词的属性关系

+

两种量词之间可以相互转换

+

一阶谓词的形式推演(命题化)

+

全称实例化:实例化全称量词蕴含的每一个实例

+

注意在实例化的过程中,第n次循环只能嵌套n次函数

+

因此算法可能不会停止,为semi-decidable的

+

存在实例化:赋予一个新的常量符号

+

一阶谓词逻辑的归结原理

+

去掉存在量词和存在量词修饰的变量,使得句子里面的每一个变量都是全称量词修饰的变量,且为合取范式

+

合一算子:替换后等价的替换方式(只能将常量赋值给变量)

+

归结原理:

+

尤其注意要赋值

+

归结原理既完备又可靠,证明比较复杂不讲

+

归结策略

+

可能有很多的归结策略,选择哪种方式进行归结呢?

+

没有一种归结策略适用于全部情况

+

广度优先策略:扩展所有可能的情况然后归结

+

优点:

+
    +
  • 当问题有解时保证能找到最短归结路径。
  • +
  • 是一种完备的归结策略。
  • +
+

缺点:

+
    +
  • 归结出了许多无用的子句
  • +
  • 既浪费时间,又浪费空间
  • +
+

广度优先对大问题的归结容易产生组合爆炸,但对小问题却仍是一种比较好的归结策略。

+

常用的归结策略可分为两大类:

+
    +
  • 删除策略是通过删除某些无用的子句来缩小归结范围
  • +
  • 限制策略是通过对参加归结的子句进行某些限制,来减少归结的盲目性,以尽快得到空子句。
  • +
+
删除策略
+

删除法主要想法是:把子句集中无用的子句删除掉,这就会缩小搜索范围,减少比较次数,从而提高归结效率。

+

删除纯文字:

+
    +
  • 如果某文字在子句集中不存在可与其互补的文字,则称该文字为纯文字。
  • +
  • 在归结过程中,纯文字不可能被消除,用包含纯文字的子句进行归结也不可能得到空子句
  • +
  • 对子句集而言,删除包含纯文字的子句,是不影响其不可满足性的。
  • +
+

重言式删除法:

+
    +
  • 如果一个子句中包含有互补的文字对,则称该子句为重言式。
  • +
  • 重言式是真值为真的子句。对一个子句集来说,不管是增加还是删除一个真值为真的子句,都不会影响该子句集的不可满足性。
  • +
  • 因此,可从子句集中删去重言式。
  • +
+
限制策略
+

限制策略要慎重,防止可以得到空子句但是限制后就得不到空子句了

+

支持集策略:每一次参加归结的两个亲本子句中,至少应该有一个是由目标公式的否定所得到的子句或它们的后裔。(就是别自己本身进行归结,带上一起归结)

+

支持集策略是完备的,即当子句集为不可满足时,则由支持集策略一定能够归结出一个空子句。

+
    +
  • 可以把支持集策略看成是在广度优先策略中引入了某种限制条件,这种限制条件代表一种启发信息,因而有较高的效率
  • +
  • 支持集策略限制了子句集元素的剧增,但会增加空子句所在的深度(结果可能不是最优)。
  • +
  • 支持集策略具有逆向推理的含义,由于进行归结的亲本子句中至少有一个与目标子句有关,因此推理过程可以看作是沿目标、子目标的方向前进的。
  • +
+

单文字子句策略:每次参加归结的两个亲本子句中至少有一个子句是单文字子句

+

采用单文字子句策略,归结式包含的文字数将少于其非单文字亲本子句中的文字数,这将有利于向空子句的方向发展,因此会有较高的归结效率。

+

单文字子句策略是不完备的,即当子句集为不可满足时,用这种策略不一定能归结出空子句。原因: 没有可用的单文字字句

+

祖先过滤策略:满足以下两个条件中的任意一个就可进行归结:

+
    +
  • 两个亲本子句中至少有一个是初始子句集中的子句。
  • +
  • 如果两个亲本子句都不是初始子句集中的子句,则一个子句应该是另一个子句的先辈子句。
  • +
+

祖先过滤策略是完备的

+

Generalized Modus Ponens(前见推理)

+

+

GMP的可靠性证明:将量词去掉变量替换为,使用命题逻辑的Modus Ponens证明即可

+

同样有前向推理和后向推理,同样是半可判定的

+

但是,如果仅包含一阶谓词的definite子句且没有函数,那么是decidable的(也叫Datalog)

+

模糊计算

+

清晰的概念:对象是否属于这个概念是明确的。

+

模糊性的概念:对象从属的界限是模糊的,随判断人的思维而定

+

取得精确数据不可能或很困难,也没有必要获取精确数据

+

要使计算机能够模仿人脑,对复杂系统进行识别和判断,出路何在?

+

1965年扎德(Zadeh)教授开创了对“模糊数学”的研究。他认为数学是可以模糊的,主张从精度方面“后退”一步。他提出用隶属函数使模糊概念数学化。

+

模糊集的定义

+

是给定论域,是把任意映射为上某个实值的函数,即,则称为定义在上的一个隶属函数,由(对所有)所构成的集合称为上的一个模糊集,称为的隶属度。

+

模糊集完全是由隶属函数来刻画的,中的每一个元素都映射为上的一个值

+

的值表示隶属于的程度,其值越大,表示隶属于的程度越高。当仅取时,模糊集便退化为一个普通集合。

+

模糊性:事件发生的程度,而不是一个事件是否发生

+

随机性:描述事件发生的不确定性,即一个事件发生与否

+

模糊集的表示

+

离散且为有限论域的表示方法

+

设论域为离散论域,则其模糊集可表示为:

+

为了能够表示出论域中的元素与其隶属度之间的对应关系,扎德引入了一种模糊集的表示方式:先为论域中的每个元素都标上其隶属度,然后再用“+”号把它们连接起来,即,其中的隶属度;“”不是相除关系,只是一个记号;“+”也不是算术意义上的加,只是一个连接符号。

+

连续论域的表示方法:如果论域是连续的,则其模糊集可用一个实函数来表示。

+

模糊集的运算

+

分别是上的两个模糊集,对任意,都有成立,则称等于,记为

+

分别是上的两个模糊集,对任意,都有成立,则称包含,记为

+

分别是上的两个模糊集,则分别称为的并集、交集,它们的隶属函数分别为:

+

+

+

上的模糊集,称的补集,其隶属函数为:

+

两个模糊集之间的运算实际上就是逐点对隶属函数作相应的运算

+

模糊关系

+

经典集合的关系:

+

笛卡尔积:设是两个普通集合,的笛卡尔乘积为

+

的关系上的一个子集,即,记为

+

对于中的元素,若,则称有关系;若,则称没有关系

+

模糊集合的关系:在二元关系上定义隶属度函数

+

上的模糊集,则称

+

+

的笛卡尔乘积,它是上的一个模糊集

+

上的一个元模糊关系是指以为论域的一个模糊集,记为

+

+

分别是上的两个模糊关系,则的合成是从的一个模糊关系,记为。其隶属函数为,其中其中,分别表示取最小和取最大

+

模糊逻辑

+

模糊逻辑:定义模糊谓词、模糊量词、模糊修饰语等

+

模糊谓词:设为模糊谓词,即U中的一个模糊关系,则模糊命题可表示为,其中的模糊谓词可以是大、小、年轻、年老、冷、暖、长、短等。

+

模糊量词:模糊逻辑中使用的模糊量词,如极少、很少、几个、少数、多数、大多数、几乎所有等。

+

模糊修饰语:

+

是模糊修饰语,是变量,是模糊谓词,则模糊命题可表示为为,模糊修饰语也称为程度词,常用的程度词有“很”、“非常”、“有些”、“绝对”等。

+

模糊修饰语的四种主要运算:

+
    +
  1. 求补:表示否定,如“不”、“非”等,其隶属函数的表示为:
  2. +
  3. 集中:表示“很”、“非常”等,其效果是减少隶属函数的值:
  4. +
  5. 扩张:表示“有些”、“稍微”等,其效果是增加隶属函数的值:
  6. +
  7. 加强对比:表示“明确”、“确定”等,其效果是增加0.5以上隶属函数的值,减少0.5以下隶属函数的值:
  8. +
+

演化计算

+

演化计算(Evolutionary Computation, EC):

+
    +
  • 在基因和种群层次上模拟自然界生物进化过程与机制的问题求解技术和计算模型。
  • +
  • 思想源于生物遗传学和适者生存的自然规律
  • +
  • 基于达尔文(Darwin)的进化论和孟德尔(Mendel)的遗传变异理论 +
      +
    • 达尔文的自然选择学说是一种被人们广泛接受的生物进化学说: +
        +
      • 生物要生存下去,就必须进行生存斗争。
      • +
      • 具有有利变异的个体容易存活下来,并且有更多的机会将有利变异传给后代;具有不利变异的个体就容易被淘汰,产生后代的机会也少的多。
      • +
      • 适者生存,不适者淘汰:自然选择。
      • +
      • 遗传和变异是决定生物进化的内在因素。(相对稳定+新的物种)
      • +
      +
    • +
    +
  • +
+

典型代表:

+
    +
  • 遗传算法(Genetic Algorithm, GA)
  • +
  • 进化策略(Evolutionary Strategy, ES)
  • +
  • 进化规划(Evolutionary Programming, EP)
  • +
  • 遗传规划(Genetic Programming, GP)
  • +
+

演化计算:一种模拟自然界生物进化过程与机制进行问题求解的自组织、自适应的随机搜索技术。

+

演化规则:“物竞天择、适者生存”

+

演化操作:繁殖(Reproduction)、变异(Mutation)、竞争(Competition)、选择(Selection)

+

遗传算法

+

遗传算法的基本思想是从初始种群出发,采用优胜劣汰、适者生存的自然法则选择个体,并通过杂交、变异来产生新一代种群,如此逐代进化,直到满足目标为止

+

基本概念:

+
    +
  • 种群(Population):多个备选解的集合。
  • +
  • 个体(Individual):种群中的单个元素,通常由一个用于描述其基本遗传结构的数据结构来表示。例如,长度为L的0、1串。
  • +
  • 适应度(Fitness)函数:用来对种群中各个个体的环境适应性进行度量的函数,函数值是遗传算法实现优胜劣汰的主要依据
  • +
  • 遗传操作(Genetic Operator):作用于种群而产生新的种群的操作。选择(Selection)、交叉(Cross-over)、变异(Mutation)
  • +
+

遗传算法主要由染色体编码、初始种群设定、适应度函数设定、遗传操作设计等几大部分所组成,

+

算法基本步骤:

+
    +
  1. 选择编码策略,将问题搜索空间中每个可能的点用相应的编码策略表示出来,即形成染色体;
  2. +
  3. 定义遗传策略,包括种群规模N,交叉、变异方法,以及选择概率Pr、交叉概率Pc、变异概率Pm等遗传参数;
  4. +
  5. 令t=0,随机选择N个染色体初始化种群P(0);
  6. +
  7. 定义适应度函数f;
  8. +
  9. 计算P(t)中每个染色体的适应值;
  10. +
  11. t=t+1;
  12. +
  13. 运用选择算子,从P(t-1)中得到P(t);
  14. +
  15. 对P(t)中的每个染色体,按概率Pc参与交叉;
  16. +
  17. 对染色体中的基因,以概率Pm参与变异运算;
  18. +
  19. 判断群体性能是否满足预先设定的终止标准,若不满足返回(5)。
  20. +
+

遗传编码

+

二进制编码

+

二进制编码是将原问题的结构变换为染色体的位串结构。假设某一参数的取值范围是。用长度为的二进制编码串来表示该参数,将等分成个子部分,记每一个等分的长度为

+

优点:易于理解和实现,可表示的模式数最多

+

缺点:海明悬崖。当算法从7改进到8时,就必须改变所有的位

+

格雷编码

+

要求两个连续整数的编码之间只能有一个码位不同,其余码位都是完全相同的。有效地解决了海明悬崖问题。

+

基本原理:

+
    +
  • 二进制码->格雷码(编码):从最右边一位起,依次将每一位与左边一位异或,作为对应格雷码该位的值,最左边一位不变;
  • +
  • 格雷码->二进制码(解码):从左边第二位起,将每位与左边一位解码后的值异或,作为该位解码后的值,最左边一位依然不变。
  • +
+

符号编码

+

个体染色体编码串中的基因值取自一个无数值含义,而只有代码含义的符号集。

+

适应度函数

+

适应度函数是一个用于对个体的适应性进行度量的函数。个体的适应度值越大,它被遗传到下一代种群中的概率越大

+

常用的适应度函数

+
    +
  • 原始适应度函数:直接将待求解问题的目标函数定义为遗传算法的适应度函数。 +
      +
    • 例如:求最大值时,即为的原始适应度函数。
    • +
    • 优点:能够直接反映出待求解问题的最初求解目标
    • +
    • 缺点:有可能出现适应度值为负的情况
    • +
    +
  • +
  • 标准适应度函数 +
      +
    • 在遗传算法中,一般要求适应度函数非负,并其适应度值越大越好。这就往往需要对原始适应函数进行某种变换,将其转换为标准的度量方式,以满足进化操作的要求,这样所得到的适应度函数被称为标准适应度函数
    • +
    +
  • +
+

基本遗传操作

+

选择(selection)操作:根据选择概率按某种策略从当前种群中挑选出一定数目的个体,使它们能够有更多的机会被遗传到下一代中

+
    +
  • 比例选择:各个个体被选中的概率与其适应度大小成正比。
  • +
  • 轮盘赌选择:个体被选中的概率取决于该个体的相对适应度。,其中,是个体的相对适应度,即个体被选中的概率,是个体的原始适应度。
  • +
+

交叉(crossover)操作:按照某种方式对选择的父代个体的染色体的部分基因进行交配重组,从而形成新的个体。

+

二进制交叉:二进制编码情况下所采用的交叉操作

+
    +
  • 单点交叉:先在两个父代个体的编码串中随机设定一个交叉点,然后对这两个父代个体交叉点前面或后面部分的基因进行交换,并生成子代中的两个新的个体。
  • +
  • 两点交叉:先在两个父代个体的编码串中随机设定两个交叉点,然后再按这两个交叉点进行部分基因交换,生成子代中的两个新的个体。
  • +
  • 均匀交叉:先随机生成一个与父串具有相同长度的二进制串(交叉模版),然后再利用该模版对两个父串进行交叉,即将模版中1对应的位进行交换,而0对应的位不交换,依此生成子代中的两个新的个体。
  • +
+

实值交叉:在实数编码情况下所采用的交叉操作,主要包括离散交叉和算术交叉

+
    +
  • 部分离散交叉:先在两个父代个体的编码向量中随机选择一部分分量,然后对这部分分量进行交换,生成子代中的两个新的个体。
  • +
  • 整体交叉:对两个父代个体的编码向量中的所有分量,都以的概率进行交换,从而生成子代中的两个新的个体。
  • +
+

变异(Mutation)操作:对选中个体的染色体中的某些基因进行变动,以形成新的个体。遗传算法中的变异操作增加了算法的局部随机搜索能力,从而可以维持种群的多样性。

+
    +
  • 二进制变异:先随机地产生一个变异位,然后将该变异位置上的基因值由“0”变为“1”,或由“1”变为“0”,产生一个新的个体。
  • +
  • 实值变异:用另外一个在规定范围内的随机实数去替换原变异位置上的基因值,产生一个新的个体。 +
      +
    • 基于次序的变异:先随机地产生两个变异位置,然后交换这两个变异位置上的基因。
    • +
    +
  • +
+

精英主义 (Elitism)

+

仅仅从产生的子代中选择基因去构造新的种群可能会丢失掉上一代种群中的很多信息。也就是说当利用交叉和变异产生新的一代时,我们有很大的可能把在某个中间步骤中得到的最优解丢失。

+

使用精英主义(Elitism)方法,在每一次产生新的一代时,我们首先把当前最优解原封不动的复制到新的一代中,其他步骤不变。这样任何时刻产生的一个最优解都可以存活到遗传算法结束。

+

遗传算法特点

+
    +
  • 自组织、自适应和自学习性—概率转移准则,非确定性规则 +
      +
    • 确定进化方案后,算法将利用进化过程中得到的信息自行组织搜索;基于自然的选择策略,优胜劣汰;
    • +
    • 遗传算法很快就能找到良好的解,即使是在很复杂的解空间中 +
        +
      • 采用随机方法进行最优解搜索,选择体现了向最优解迫近
      • +
      • 交叉体现了最优解的产生,变异体现了全局最优解的复盖
      • +
      +
    • +
    +
  • +
  • 本质并行性—群体搜索 +
      +
    • 算法本身非常适合大规模并行,各种群分别独立进化,不需要相互间交换信息
    • +
    • 可以同时搜索解空间的多个区域并相互间交流信息,使得演化计算能以较少的计算获得较大的收益。
    • +
    +
  • +
  • 不需要其他知识,只需要影响搜索方向的目标函数和相应的适应度函数 +
      +
    • 对待求解问题的指标函数没有什么特殊的要求,如不要求连续性、导数存在、单峰值等假设
    • +
    • 容易形成通用算法程序
    • +
    • 遗传算法不能解决那些“大海捞针”的问题,所谓“大海捞针”问题就是没有一个确切的适应度函数表征个体好坏的问题,遗传算法对这类问题无法找到收敛的路径。
    • +
    +
  • +
  • 理论上证明算法的收敛性很困难
  • +
  • 多用于解决实际问题
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第8讲 逻辑
+
https://zhangzhao219.github.io/2022/10/20/UCAS/advanced-ai/advanced-ai-8/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月20日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/22/UCAS/information-retrieval/information-retrieval-16/index.html b/2022/10/22/UCAS/information-retrieval/information-retrieval-16/index.html new file mode 100644 index 000000000..a86ed1ade --- /dev/null +++ b/2022/10/22/UCAS/information-retrieval/information-retrieval-16/index.html @@ -0,0 +1,834 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第16讲 Web搜索 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第16讲 Web搜索

+ + +
+ +

《现代信息检索》课程笔记:第16讲 Web搜索

+ +

第16讲 Web搜索

+

互联网上的搜索

+

搜索是Web上使用最多的应用之一

+

没有搜索引擎,Web甚至无法运转

+
    +
  • 没有搜索,很难找到所需的内容
  • +
  • 没有搜索,在Web上创建内容也就缺了动机 +
      +
    • 如果没人看为什么要发布内容?
    • +
    • 如果没有任何回报为什么要发布内容?
    • +
    +
  • +
  • Web运转必须要有人买单 +
      +
    • 服务器、Web 基础设施、内容创建过程等需要费用支持
    • +
    • 这些费用的相当大一部分都是通过搜索广告支付
    • +
    • 可以说,搜索为Web 买单
    • +
    +
  • +
+

兴趣聚合:具有相同兴趣的人,即使所处地理位置分散,也可以通过Web找到对方。

+

搜索引擎是实现兴趣聚合的关键事物

+

在Web上,搜索不仅仅是一个好的特点

+

Web是一个充满噪声数据且组织失调的集合体→大量的重复需要检测

+

用户可以(某种意义上)无控制和无限制地发布内容→大量作弊内容需要检测

+

互联网广告

+

传统广告:品牌广告、直接营销、

+

传统广告的不足:

+
    +
  • 广告投放场地或媒介相对有限:报纸、电视、杂志、橱窗、公汽、电梯等
  • +
  • 广告场地的费用一般不菲:CCTV 标王
  • +
  • 很难进行个性化
  • +
  • 投放效果取决于广告商的智慧
  • +
  • 投放效果很难度量
  • +
+

互联网广告的优点:

+
    +
  • 无限机会
  • +
  • 无限创意
  • +
  • 完全可以个性化处理
  • +
  • 每次点击花费的代价很低
  • +
  • 定量度量程度高
  • +
+

互联网广告的主要形式:图片广告、文本广告、搜索广告、网页广告、

+

第一代搜索广告:Goto

+

第二代搜索广告:Google

+

如何对广告排序?

+
    +
  • 简单的方法:按照类似 Goto 的方式,即按照投标价格排序
  • +
  • 替代方法:按照投标价格和相关性排序(相关度度量的关键指标:点击率)
  • +
+

Web查询“长尾”现象:基于AOL查询频次的统计、基于查询频次的流量统计

+

长尾效应的解释

+
    +
  • 大多数用户搜索“常见”查询;一小部分用户搜索“罕见”查询
  • +
  • 大量用户使用“常见”查询;同时大量用户也会使用一些“罕见”查询
  • +
+

重复检测

+
    +
  • Web上充斥重复内容
  • +
  • 相对其它文档集合,Web 上的重复内容更多
  • +
  • 完全重复:易剔除,比如采用哈希指纹的方法
  • +
  • 近似重复:Web上存在大量近似重复,很难剔除
  • +
  • 对用户而言,如果搜索结果中存在不少几乎相同的页面,那么体验非常不好
  • +
  • 边缘相关度(Marginal relevance) 为 0 :如果一篇高度相关的文档出现在另一篇高度近似的文档之后,那么该文档变得不相关
  • +
  • 必须要去除这些近似重复
  • +
+

近似重复的检测:采用编辑距离指标计算页面之间的相似度

+

将每篇文档表示成一个shingle 集合

+

每个shingle 是一个基于词语的 n-gram

+

使用shingle 来计算文档之间的语法相似度

+

两个文档的相似度定义为它们的shingle 集合的Jaccard距离

+

每篇文档的shingle的个数非常大

+

为提高效率,接下来我们使用文档的梗概来表示文档,它由文档的shingle集合中精巧挑选出的子集构成

+

高效的近似重复检测:局部敏感哈希或排序

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第16讲 Web搜索
+
https://zhangzhao219.github.io/2022/10/22/UCAS/information-retrieval/information-retrieval-16/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月22日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/25/UCAS/information-retrieval/information-retrieval-17/index.html b/2022/10/25/UCAS/information-retrieval/information-retrieval-17/index.html new file mode 100644 index 000000000..00b327403 --- /dev/null +++ b/2022/10/25/UCAS/information-retrieval/information-retrieval-17/index.html @@ -0,0 +1,848 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第17讲 信息采集 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第17讲 信息采集

+ + +
+ +

《现代信息检索》课程笔记:第17讲 信息采集

+ +

第17讲 信息采集

+

一个简单的采集器

+

基本的采集过程

+
    +
  • 初始化采集URL 种子队列
  • +
  • 重复如下过程 +
      +
    • 从队列中取出URL
    • +
    • 下载并分析网页
    • +
    • 从网页中抽取更多的URL
    • +
    • 将这些URL 放到队列中
    • +
    +
  • +
+

上述简单采集器的问题:

+
    +
  • 规模问题:必须要分布式处理
  • +
  • 我们不可能索引所有网页,必须要从中选择部分网页,如何选择?
  • +
  • 重复网页:必须要集成重复检测功能
  • +
  • 作弊网页和采集器陷阱:必须要集成作弊网页检测功能
  • +
  • 礼貌性问题:对同一网站的访问按遵照协议规定,并且访问的间隔必须要足够
  • +
  • 新鲜度问题:必须要定期更新或者重采 +
      +
    • 由于Web 的规模巨大,我们只能对一个小的网页子集频繁重采
    • +
    • 同样,这也存在一个选择或者优先级问题
    • +
    +
  • +
+

采集器必须做到

+
    +
  • 礼貌性 +
      +
    • 不要高频率采集某个网站
    • +
    • 仅仅采集robots.txt 所规定的可以采集的网页
    • +
    +
  • +
  • 鲁棒性 +
      +
    • 能够处理采集器陷阱、重复页面、超大页面、超大网站、动态页面等问题v
    • +
    +
  • +
+

任意一个采集器应该做到:

+
    +
  • 能够进行分布式处理
  • +
  • 支持规模的扩展:能够通过增加机器支持更高的采集速度
  • +
  • 优先采集高质量网页
  • +
  • 能够持续运行:对已采集网页进行更新
  • +
+

一个真实的采集器

+

待采集URL池:

+
    +
  • 待采集URL池是一个数据结构,它存放并管理那些已经看到但是还没有采集的URL集合
  • +
  • 可能包含来自同一主机的不同页面
  • +
  • 必要要避免在同一时间采集这些来自同一主机的页面
  • +
  • 必须要保证采集线程任务饱和
  • +
+

基本的采集架构

+

URL规范化

+

内容重复判别

+
    +
  • 对每个抓取的页面,判断它是否已在索引当中
  • +
  • 可以采用文档指纹或者shingle 的方法判别
  • +
  • 忽略那些已经在索引中的重复页面
  • +
+

分布式采集

+
    +
  • 运行多个采集线程,这些线程可以分布在不同节点上 +
      +
    • 这些节点往往在地理上分散在不同位置
    • +
    +
  • +
  • 将采集的主机分配到不同节点上
  • +
+

分布式采集器

+

待采集URL池 : 主要考虑两点

+
    +
  • 礼貌性: 不要非常频繁第访问某个 Web 服务器 +
      +
    • 比如,可以在两次服务器访问之间设置一个时间间隔
    • +
    +
  • +
  • 新鲜度: 对某些网站的采集频率如新闻网站要高于其他网站
  • +
+

采集器陷阱

+
    +
  • 一些恶意的服务器可以产生无穷的链接网页序列
  • +
  • 一些复杂的采集器陷阱产生的页面不能简单地判断为动态页面
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第17讲 信息采集
+
https://zhangzhao219.github.io/2022/10/25/UCAS/information-retrieval/information-retrieval-17/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月25日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/10/28/UCAS/information-retrieval/information-retrieval-18/index.html b/2022/10/28/UCAS/information-retrieval/information-retrieval-18/index.html new file mode 100644 index 000000000..74819c766 --- /dev/null +++ b/2022/10/28/UCAS/information-retrieval/information-retrieval-18/index.html @@ -0,0 +1,874 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-第18讲 链接分析 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-第18讲 链接分析

+ + +
+ +

《现代信息检索》课程笔记:第18讲 链接分析

+ +

第18讲 链接分析

+

链接无处不在

+
    +
  • 真实性和权威性的有效来源 +
      +
    • 垃圾邮件-哪些电子邮件帐户是垃圾邮件发送者?
    • +
    • host质量-哪些 host 质量不好?
    • +
    • 电话呼叫记录
    • +
    +
  • +
  • 好节点、坏节点和未知节点 +
      +
    • 好节点不会指向坏节点 +
        +
      • 如果一个节点指向了坏节点,那么这个节点是坏节点
      • +
      • 如果一个好节点指向这个节点,那么这个节点是好节点
      • +
      +
    • +
    • 所有其他貌似合理的组合
    • +
    +
  • +
+

为什么我们对链接分析感兴趣?

+

链接分析对目前为止的完全基于文本的IR任务进行了补充

+
    +
  • (文档)评分和排序
  • +
  • 基于链接的聚类-来自链接的主题结构
  • +
  • 链接作为分类特征-彼此链接的文档可能是同一主题
  • +
  • 爬虫-根据已看到的链接,我们下一步要爬取哪里?
  • +
+

Web可以看成一个有向图

+
    +
  • 假设1: 超链接代表了某种质量认可信号
  • +
  • 假设2: 锚文本描述了文档d2 的内容
  • +
+

对锚文本构建索引

+
    +
  • 因此,锚文本往往比网页本身更能揭示网页的内容
  • +
  • 在计算过程中,锚文本应该被赋予比文档中文本更高的权重
  • +
+

PageRank背后的假设

+
    +
  • 假设1:Web 上的链接是网页质量的标志-链出网页的作者认为链向的网页具有很高的质量
  • +
  • 假设2:锚文本能够描述链向网页的内容
  • +
+

Google炸弹:指由于人为恶意构造锚文本而导致的结果很差的搜索。用户群体有意创建链接误导搜索引擎

+

锚文本索引:将从指向文档D的链接的锚文本(也可能包含锚文本附近的文本)包含在D的索引中

+

有时会产生低于期望的效果,例如:垃圾邮件过滤应用全然失败

+

可以根据锚页面网站的权威性对锚文本进行加权

+

链接服务器:低成本地获取所有链接信息

+
    +
  • 支持网络图上的快速查询
  • +
  • 将映射存储在内存中
  • +
  • 应用:链接分析、网络图分析、爬虫控制
  • +
+

Boldi and Vigna:基本目标-维护内存中的节点邻接表

+

邻接表压缩中利用到的属性:

+
    +
  • 相似度(邻接表之间)
  • +
  • 位置(一个页面中的许多链接都连接到“附近”的页面)
  • +
  • 在已排序的邻接表中使用间隔编码
  • +
  • gap value的分布
  • +
+

间隔编码

+

给出整数x,y,z 的已排序列表,用 x y-x z-y 来对 x,y,z 进行表示

+

使用编码来压缩整数

+

BV算法的主要优势

+
    +
  • 仅依赖于位置的规范顺序 +
      +
    • 字典顺序对web十分适用
    • +
    +
  • +
  • 邻接查询可以被非常高效地回答 +
      +
    • 要获取外部邻居,需要回溯到链的原型
    • +
    • 在实践中,这条链通常很短(因为相似性主要基于host 内部)
    • +
    • 编码过程中也可以明确限制链的长度
    • +
    +
  • +
  • 易于实现one pass 算法 +
      +
    • 顺序读取,不需要无限缓冲。读取复杂度与网页数量是线性关系
    • +
    +
  • +
+

引用分析

+

引用分析:科技文献中的引用分析

+

另一个应用:引用频率可以用度量一篇文档的影响度

+

更好的度量方法:对不同网页来的引用频率进行加权

+

PageRank

+
    +
  • 一个网页如果它的入链越多,那么它也越重要(PageRank 越高)
  • +
  • 一个网页如果被越重要的网页所指向,那么它也越重要(PageRank 越高 )
  • +
+

原始PageRank的一个不足:图中存在一个循环通路,每次迭代,该循环通路中的每个节点的 PageRank不断增加,但是它们并不指出去,即不将PageRank分配给其他节点!

+

改进的PageRank公式:随机冲浪或随机游走(Random Walk)模型

+

HITS: Hub节点&Authority节点

+

每个网页计算两个值:

+

Hub:作为目录型或导航型网页的权重

+

Authority:作为权威型网页的权重

+

一个网页被越重要的导航型网页指向越多,那么它的Authority越大;

+

一个网页指向的高重要度权威型网页越多,那么它的Hub越大。

+

HITS算法也是收敛的,也可以通过迭代的方式计算。

+

HITS算法的实际计算过程

+
    +
  • 首先进行Web 搜索;
  • +
  • 搜索的结果称为根集(从搜索结果中选择一部分排名靠前的网页作为根集,也叫做种子集合)
  • +
  • 将所有链向种子集合和种子集合链出的网页加入到种子集合;
  • +
  • 新的更大的集合称为基本集
  • +
  • 最后,在基本集上计算每个网页的hub值和authority值(该基本集可以看成一个小的Web图)。
  • +
+

PageRank vs. HITS

+

网页的PageRank 与查询主题无关,可以事先算好,因此适合于大型搜索引擎的应用。

+

HITS算法的计算与查询主题相关,检索之后再进行计算,因此,不适合于大型搜索引擎。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-第18讲 链接分析
+
https://zhangzhao219.github.io/2022/10/28/UCAS/information-retrieval/information-retrieval-18/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年10月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/11/02/Go/Go-Project-Family-Ledger/index.html b/2022/11/02/Go/Go-Project-Family-Ledger/index.html new file mode 100644 index 000000000..c254c2eb0 --- /dev/null +++ b/2022/11/02/Go/Go-Project-Family-Ledger/index.html @@ -0,0 +1,1278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go项目-家庭收支记账软件 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go项目-家庭收支记账软件

+ + +
+ +

Go项目-家庭收支记账软件

+ +

项目开发流程

+

xHqQNd.md.png

+

项目需求说明

+
    +
  1. 模拟实现基于文本界面的《家庭记账软件》
  2. +
  3. 软件能够记录家庭的收入、支出,并能够打印收支明细表
  4. +
+

项目代码编写

+

主菜单的设计

+
func main() {
+	// 声明一个变量保存用户的输入
+	key := ""
+
+	// 声明一个变量,控制是否退出for循环
+	loop := true
+
+	// 显示主菜单
+	for loop {
+		fmt.Println("---------------------家庭收支记账软件---------------------")
+		fmt.Println("                       1 收支明细")
+		fmt.Println("                       2 登记收入")
+		fmt.Println("                       3 登记输出")
+		fmt.Println("                       4 退出软件")
+		fmt.Print("请选择(1-4):")
+		// 接收用户的输入
+		fmt.Scanln(&key)
+		// 对用户的输入进行判断
+		switch key {
+		case "1":
+			fmt.Println("---------------------当前收支明细记录---------------------")
+		case "2":
+		case "3":
+			fmt.Println("登记支出------")
+		case "4":
+			loop = false
+		default:
+			fmt.Println("请输入正确的选项------")
+		}
+	}
+	fmt.Println("-------------------退出家庭收支记账软件-------------------")
+}
+

没啥有意思的,基础编程,效果如下:

+

xHOuYd.png

+

显示明细与登记输入

+
case "1":
+	fmt.Println("---------------------当前收支明细记录---------------------")
+	fmt.Println(details)
+case "2":
+	fmt.Println("-------------------------登记收入-------------------------")
+	fmt.Print("本次收入金额:")
+	fmt.Scanln(&money)
+	fmt.Print("本次收入说明:")
+	fmt.Scanln(&note)
+	balance += money
+	details += fmt.Sprintf("收  入\t%v\t%v\t%v\n", balance, money, note)
+	fmt.Println("收入登记成功!")
+

其中明细是用字符串拼接实现的,实际中应该是要操作数据库的

+

登记支出

+
case "3":
+	fmt.Println("-------------------------登记支出-------------------------")
+	fmt.Print("本次支出金额:")
+	fmt.Scanln(&money)
+	if money > balance {
+		fmt.Println("余额的金额不足!")
+		break
+	} else if money <= 0 {
+		fmt.Println("支出金额应为正数!")
+	}
+	fmt.Print("本次支出说明:")
+	fmt.Scanln(&note)
+	balance -= money
+	details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", balance+money, money, balance, note)
+	fmt.Println("支出登记成功!")
+

注意支出的金额要小于账户余额,也要注意收入和支出的时候用户输入的数字需要为正数。

+

完善代码

+

退出时增加确认条件

+
case "4":
+	var choice byte
+	for {
+		fmt.Print("确定退出?(y/n):")
+		fmt.Scanf("%c\n", &choice)
+		if choice == 'y' {
+			loop = false
+			break
+		} else if choice == 'n' {
+			break
+		} else {
+			fmt.Println("输入有误!!请重新输入")
+		}
+	}
+

注意scanf字符的时候与C语言是差不多的,需要考虑回车符号

+

没有记录时不输出收支详情字符串

+
// 判断当前是否有输入或者输出的记录
+flag := false
+

也没啥好说的,加个标志位,有记录的时候将这个标志位改掉就可以了

+

面向对象

+

将上面的面向过程的代码修改成面向对象的代码

+

主要思想:将记账软件的功能封装到结构体中,然后调用这个结构体的方法完成功能。

+

定义结构体

+
// 定义结构体
+type FamilyAccount struct {
+	// 声明一个变量保存用户的输入
+	key string
+	// 声明一个变量,控制是否退出for循环
+	loop bool
+	// 定义账户的初始值
+	balance float64
+	// 定义每次收支的金额和说明
+	money float64
+	note  string
+	// 收支的详情使用字符串来记录
+	// 当有记录时对这个字符串进行拼接
+	details string
+	// 判断当前是否有输入或者输出的记录
+	flag bool
+}
+

注意定义结构体的时候不能进行初始化

+

工厂模式返回结构体的指针

+
// 编写一个工厂模式的构造方法,返回结构体的指针
+func NewFamilyAcount() *FamilyAccount {
+	return &FamilyAccount{
+		key:     "",
+		loop:    true,
+		balance: 10000.0,
+		money:   0.0,
+		note:    "",
+		details: "收  支\t收支前账户余额\t收支金额\t收支后账户余额\t说  明\n",
+		flag:    false,
+	}
+}
+

注意如果结构体是私有的是一定要有的,公开的也可以有,以后就要记得一定要有这样的一个方法

+

编写各种方法

+

简单改造一下面向过程的代码即可完成面向对象的效果

+

将显示明细写成一个方法

+
func (fa *FamilyAccount) showDetails() {
+	if !fa.flag {
+		fmt.Println("当前没有任何收支记录!")
+	} else {
+		fmt.Println("---------------------当前收支明细记录---------------------")
+		fmt.Println(fa.details)
+	}
+}
+

将登记收入写成一个方法

+
func (fa *FamilyAccount) income() {
+	fmt.Println("-------------------------登记收入-------------------------")
+	fmt.Print("本次收入金额:")
+	fmt.Scanln(&fa.money)
+	// 收入金额不能是负数
+	if fa.money <= 0 {
+		fmt.Println("收入金额应为正数!")
+		return
+	}
+	fmt.Print("本次收入说明:")
+	fmt.Scanln(&fa.note)
+	fa.balance += fa.money
+	fa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance-fa.money, fa.money, fa.balance, fa.note)
+	fmt.Println("收入登记成功!")
+	fa.flag = true
+}
+

将登记支出写成一个方法

+
func (fa *FamilyAccount) pay() {
+	fmt.Println("-------------------------登记支出-------------------------")
+	fmt.Print("本次支出金额:")
+	fmt.Scanln(&fa.money)
+	if fa.money > fa.balance {
+		fmt.Println("余额的金额不足!")
+		return
+	} else if fa.money <= 0 {
+		fmt.Println("支出金额应为正数!")
+	}
+	fmt.Print("本次支出说明:")
+	fmt.Scanln(&fa.note)
+	fa.balance -= fa.money
+	fa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance+fa.money, fa.money, fa.balance, fa.note)
+	fmt.Println("支出登记成功!")
+	fa.flag = true
+}
+

将退出系统写成一个方法

+
func (fa *FamilyAccount) exit() {
+	var choice byte
+	for {
+		fmt.Print("确定退出?(y/n):")
+		fmt.Scanf("%c\n", &choice)
+		if choice == 'y' {
+			fa.loop = false
+			break
+		} else if choice == 'n' {
+			break
+		} else {
+			fmt.Println("输入有误!!请重新输入")
+		}
+	}
+}
+

显示主菜单

+
func (fa *FamilyAccount) MainMenu() {
+	// 显示主菜单
+	for fa.loop {
+		fmt.Println("\n---------------------家庭收支记账软件---------------------")
+		fmt.Println("                       1 收支明细")
+		fmt.Println("                       2 登记收入")
+		fmt.Println("                       3 登记输出")
+		fmt.Println("                       4 退出软件")
+		fmt.Print("请选择(1-4):")
+		// 接收用户的输入
+		fmt.Scanln(&fa.key)
+		// 对用户的输入进行判断
+		switch fa.key {
+		case "1":
+			fa.showDetails()
+		case "2":
+			fa.income()
+		case "3":
+			fa.pay()
+		case "4":
+			fa.exit()
+		default:
+			fmt.Println("请输入正确的选项------")
+		}
+	}
+}
+

主函数中进行调用

+
func main() {
+	utils.NewFamilyAcount().MainMenu()
+}
+

增加扩展功能

+

添加一个用户登录的功能,即只有将用户名和密码输入正确后才能打开软件,否则无法看到主界面

+

实现:在显示菜单之前增加一个无限循环要求用户输入用户名和密码,只有输入正确才能退出循环

+
for {
+	var username, password string
+	fmt.Print("请输入用户名:")
+	fmt.Scanln(&username)
+	fmt.Print("请输入密码:")
+	fmt.Scanln(&password)
+	if fa.login(username, password) {
+		break
+	} else {
+		fmt.Println("用户名或密码错误!")
+	}
+}
+

用户登录的方法

+
func (fa *FamilyAccount) login(username string, password string) bool {
+	if (username == fa.username) && (password == fa.password) {
+		return true
+	}
+	return false
+}
+

完整源代码

+

面向过程的代码

+
package main
+
+import "fmt"
+
+func main() {
+	// 声明一个变量保存用户的输入
+	key := ""
+
+	// 声明一个变量,控制是否退出for循环
+	loop := true
+
+	// 定义账户的初始值
+	balance := 10000.0
+
+	// 定义每次收支的金额和说明
+	var money float64
+	var note string
+
+	// 收支的详情使用字符串来记录
+	// 当有记录时对这个字符串进行拼接
+	details := "收  支\t收支前账户余额\t收支金额\t收支后账户余额\t说  明\n"
+	// 判断当前是否有输入或者输出的记录
+	flag := false
+
+	// 显示主菜单
+	for loop {
+		fmt.Println("\n---------------------家庭收支记账软件---------------------")
+		fmt.Println("                       1 收支明细")
+		fmt.Println("                       2 登记收入")
+		fmt.Println("                       3 登记输出")
+		fmt.Println("                       4 退出软件")
+		fmt.Print("请选择(1-4):")
+		// 接收用户的输入
+		fmt.Scanln(&key)
+		// 对用户的输入进行判断
+		switch key {
+		case "1":
+			if !flag {
+				fmt.Println("当前没有任何收支记录!")
+			} else {
+				fmt.Println("---------------------当前收支明细记录---------------------")
+				fmt.Println(details)
+			}
+		case "2":
+			fmt.Println("-------------------------登记收入-------------------------")
+			fmt.Print("本次收入金额:")
+			fmt.Scanln(&money)
+			// 收入金额不能是负数
+			if money <= 0 {
+				fmt.Println("收入金额应为正数!")
+				break
+			}
+			fmt.Print("本次收入说明:")
+			fmt.Scanln(&note)
+			balance += money
+			details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", balance-money, money, balance, note)
+			fmt.Println("收入登记成功!")
+			flag = true
+		case "3":
+			fmt.Println("-------------------------登记支出-------------------------")
+			fmt.Print("本次支出金额:")
+			fmt.Scanln(&money)
+			if money > balance {
+				fmt.Println("余额的金额不足!")
+				break
+			} else if money <= 0 {
+				fmt.Println("支出金额应为正数!")
+			}
+			fmt.Print("本次支出说明:")
+			fmt.Scanln(&note)
+			balance -= money
+			details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", balance+money, money, balance, note)
+			fmt.Println("支出登记成功!")
+			flag = true
+		case "4":
+			var choice byte
+			for {
+				fmt.Print("确定退出?(y/n):")
+				fmt.Scanf("%c\n", &choice)
+				if choice == 'y' {
+					loop = false
+					break
+				} else if choice == 'n' {
+					break
+				} else {
+					fmt.Println("输入有误!!请重新输入")
+				}
+			}
+		default:
+			fmt.Println("请输入正确的选项------")
+		}
+	}
+	fmt.Println("-------------------退出家庭收支记账软件-------------------")
+}
+
+

面向对象的代码

+

.
+├── Family-Ledger
+│ ├── main
+│ │ └── main.go
+│ └── utils
+│ └── familyAccount.go

+

main.go

+
package main
+
+import (
+	"Go-Projects/Family-Ledger/utils"
+)
+
+func main() {
+	utils.NewFamilyAcount().MainMenu()
+}
+
+

familyAccount.go

+
package utils
+
+import "fmt"
+
+// 定义结构体
+type FamilyAccount struct {
+	// 用户名和密码
+	username string
+	password string
+	// 声明一个变量保存用户的输入
+	key string
+	// 声明一个变量,控制是否退出for循环
+	loop bool
+	// 定义账户的初始值
+	balance float64
+	// 定义每次收支的金额和说明
+	money float64
+	note  string
+	// 收支的详情使用字符串来记录
+	// 当有记录时对这个字符串进行拼接
+	details string
+	// 判断当前是否有输入或者输出的记录
+	flag bool
+}
+
+// 编写一个工厂模式的构造方法,返回结构体的指针
+func NewFamilyAcount() *FamilyAccount {
+	return &FamilyAccount{
+		username: "admin",
+		password: "password",
+		key:      "",
+		loop:     true,
+		balance:  10000.0,
+		money:    0.0,
+		note:     "",
+		details:  "收  支\t收支前账户余额\t收支金额\t收支后账户余额\t说  明\n",
+		flag:     false,
+	}
+}
+
+// 给结构体绑定相应的方法
+
+// 将显示明细写成一个方法
+func (fa *FamilyAccount) showDetails() {
+	if !fa.flag {
+		fmt.Println("当前没有任何收支记录!")
+	} else {
+		fmt.Println("---------------------当前收支明细记录---------------------")
+		fmt.Println(fa.details)
+	}
+}
+
+// 将登记收入写成一个方法
+func (fa *FamilyAccount) income() {
+	fmt.Println("-------------------------登记收入-------------------------")
+	fmt.Print("本次收入金额:")
+	fmt.Scanln(&fa.money)
+	// 收入金额不能是负数
+	if fa.money <= 0 {
+		fmt.Println("收入金额应为正数!")
+		return
+	}
+	fmt.Print("本次收入说明:")
+	fmt.Scanln(&fa.note)
+	fa.balance += fa.money
+	fa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance-fa.money, fa.money, fa.balance, fa.note)
+	fmt.Println("收入登记成功!")
+	fa.flag = true
+}
+
+// 将登记支出写成一个方法
+func (fa *FamilyAccount) pay() {
+	fmt.Println("-------------------------登记支出-------------------------")
+	fmt.Print("本次支出金额:")
+	fmt.Scanln(&fa.money)
+	if fa.money > fa.balance {
+		fmt.Println("余额的金额不足!")
+		return
+	} else if fa.money <= 0 {
+		fmt.Println("支出金额应为正数!")
+	}
+	fmt.Print("本次支出说明:")
+	fmt.Scanln(&fa.note)
+	fa.balance -= fa.money
+	fa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance+fa.money, fa.money, fa.balance, fa.note)
+	fmt.Println("支出登记成功!")
+	fa.flag = true
+}
+
+// 将退出系统写成一个方法
+func (fa *FamilyAccount) exit() {
+	var choice byte
+	for {
+		fmt.Print("确定退出?(y/n):")
+		fmt.Scanf("%c\n", &choice)
+		if choice == 'y' {
+			fa.loop = false
+			break
+		} else if choice == 'n' {
+			break
+		} else {
+			fmt.Println("输入有误!!请重新输入")
+		}
+	}
+}
+
+// 用户登录的功能
+func (fa *FamilyAccount) login(username string, password string) bool {
+	if (username == fa.username) && (password == fa.password) {
+		return true
+	}
+	return false
+}
+
+// 显示主菜单
+func (fa *FamilyAccount) MainMenu() {
+	for {
+		var username, password string
+		fmt.Print("请输入用户名:")
+		fmt.Scanln(&username)
+		fmt.Print("请输入密码:")
+		fmt.Scanln(&password)
+		if fa.login(username, password) {
+			break
+		} else {
+			fmt.Println("用户名或密码错误!")
+		}
+	}
+	// 显示主菜单
+	for fa.loop {
+		fmt.Println("\n---------------------家庭收支记账软件---------------------")
+		fmt.Println("                       1 收支明细")
+		fmt.Println("                       2 登记收入")
+		fmt.Println("                       3 登记输出")
+		fmt.Println("                       4 退出软件")
+		fmt.Print("请选择(1-4):")
+		// 接收用户的输入
+		fmt.Scanln(&fa.key)
+		// 对用户的输入进行判断
+		switch fa.key {
+		case "1":
+			fa.showDetails()
+		case "2":
+			fa.income()
+		case "3":
+			fa.pay()
+		case "4":
+			fa.exit()
+		default:
+			fmt.Println("请输入正确的选项------")
+		}
+	}
+}
+
+ + +
+ +
+
+ + + + + + +
+
+
Go项目-家庭收支记账软件
+
https://zhangzhao219.github.io/2022/11/02/Go/Go-Project-Family-Ledger/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年11月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/11/03/Go/Go-Project-Customer-Management-System/index.html b/2022/11/03/Go/Go-Project-Customer-Management-System/index.html new file mode 100644 index 000000000..beeabfc8e --- /dev/null +++ b/2022/11/03/Go/Go-Project-Customer-Management-System/index.html @@ -0,0 +1,1316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go项目-客户信息管理系统 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go项目-客户信息管理系统

+ + +
+ +

Go项目-客户信息管理系统

+ +

项目开发流程

+

xHx5Pe.md.png

+

项目需求说明

+
    +
  1. 模拟实现基于文本界面的《客户信息管理软件》。
  2. +
  3. 该软件能够实现对客户对象的插入、修改和删除(用切片实现),并能够打印客户明细表
  4. +
+

项目代码编写

+

编写Customer.go

+

主要是用于表示一个客户的信息,包含结构体以及在其他地方如果调用它的工厂模式的方法

+
package model
+
+import "fmt"
+
+// 定义Customer结构体,表示一个客户信息
+type Customer struct {
+	Id     int
+	Name   string
+	Gender string
+	Age    int
+	Phone  string
+	Email  string
+}
+
+// 工厂模式返回Customer的结构体,在CustomerService里面使用
+// 感觉就是新建一个Customer的实例
+func NewCustomer(id int, name string, gender string, age int, phone string, email string) *Customer {
+	return &Customer{
+		Id:     id,
+		Name:   name,
+		Gender: gender,
+		Age:    age,
+		Phone:  phone,
+		Email:  email,
+	}
+}
+
+func (cu *Customer) GetInfo() string {
+	return fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v", cu.Id, cu.Name, cu.Gender, cu.Age, cu.Phone, cu.Email)
+}
+

以及如果我们要返回一个客户的信息,操作也是在Customer的实例上进行的,因此后面的方法也要写在这个结构体的下面

+

完成对Customer结构体的操作的代码在CustomerService里面,定义另外一个结构体,里面包含一个切片,存储全部实例化的Customer

+
// 完成对Customer的操作,包括增删改查
+type CustomerService struct {
+	// 存储当前的客户
+	Customers []model.Customer
+	// 声明一个字段,表示当前切片含有多少个客户
+	CustomerNum int
+}
+

主界面 customerView.go

+

主菜单:

+
func (cv *CustomerView) MainMenu() {
+	for {
+		var username, password string
+		fmt.Print("请输入用户名:")
+		fmt.Scanln(&username)
+		fmt.Print("请输入密码:")
+		fmt.Scanln(&password)
+		if cv.login(username, password) {
+			break
+		} else {
+			fmt.Println("用户名或密码错误!")
+		}
+	}
+	// 显示主菜单
+	for cv.loop {
+		fmt.Println("\n---------------------客户信息管理软件---------------------")
+		fmt.Println("                         1 添加客户")
+		fmt.Println("                         2 修改客户")
+		fmt.Println("                         3 删除客户")
+		fmt.Println("                         4 客户列表")
+		fmt.Println("                         5 退    出")
+		fmt.Print("请选择(1-5):")
+		// 接收用户的输入
+		fmt.Scanln(&cv.key)
+		// 对用户的输入进行判断
+		switch cv.key {
+		case "1":
+			cv.addCustomer()
+		case "2":
+			cv.changeCustomer()
+		case "3":
+			cv.deleteCustomer()
+		case "4":
+			cv.showCustomer()
+		case "5":
+			cv.exit()
+		default:
+			fmt.Println("请输入正确的选项------")
+		}
+	}
+}
+

主菜单里面有的变量是需要定义在结构体中的

+
type CustomerView struct {
+	key             string                   // 接收用户输入
+	loop            bool                     // 表示是否循环的显示主菜单
+	username        string                   // 用户的用户名
+	password        string                   // 用户的密码
+	customerService *service.CustomerService // 获取用户服务
+}
+

同时也编写一个工厂模式的方法,方便main.go文件进行调用

+
func NewCustomerView() *CustomerView {
+	return &CustomerView{
+		key:             "",
+		loop:            true,
+		username:        "admin",
+		password:        "password",
+		customerService: service.NewCustomerService(),
+	}
+}
+

main.go:

+
package main
+
+import (
+	"Go-Projects/Customer-Management-System/view"
+)
+
+func main() {
+	view.NewCustomerView().MainMenu()
+}
+
+

完成增删改查的功能

+

要注意,全部的功能实现细节都应该是在customerService里面进行编写的,customerView.go 文件只负责调用,并对返回的结果进行判断等。

+

首先要对CustomerService进行初始化,也是相当于工厂模式了

+
// 初始化CustomerService
+func NewCustomerService() *CustomerService {
+	customerService := &CustomerService{} // 初始化
+	customerService.CustomerNum = 0
+	return customerService
+}
+

展示客户列表

+
func (cv *CustomerView) showCustomer() {
+	if cv.customerService.CustomerNum == 0 {
+		fmt.Println("没有客户!")
+		return
+	}
+	fmt.Println("\n-------------------------客户列表-------------------------")
+	fmt.Println("编号\t姓名\t性别\t年龄\t电话\t电子邮件")
+	for _, eachCustomer := range cv.customerService.ShowCustomerSlice() {
+		fmt.Println(eachCustomer.GetInfo())
+	}
+}
+
func (cs *CustomerService) ShowCustomerSlice() []model.Customer {
+	return cs.Customers
+}
+

添加客户

+

对切片增加一个客户的实例,然后将记录的数量+1

+
func (cv *CustomerView) addCustomer() {
+	id := cv.customerService.CustomerNum + 1
+	var name, gender, phone, email string
+	var age int
+	fmt.Print("请输入姓名:")
+	fmt.Scanln(&name)
+	fmt.Print("请输入性别:")
+	fmt.Scanln(&gender)
+	fmt.Print("请输入年龄:")
+	fmt.Scanln(&age)
+	fmt.Print("请输入电话:")
+	fmt.Scanln(&phone)
+	fmt.Print("请输入电子邮件:")
+	fmt.Scanln(&email)
+
+	if cv.customerService.AddCustomer(*model.NewCustomer(id, name, gender, age, phone, email)) {
+		fmt.Println("-------------------------添加成功-------------------------")
+	} else {
+		fmt.Println("-------------------------添加失败-------------------------")
+	}
+}
+
func (cs *CustomerService) AddCustomer(customer model.Customer) bool {
+	cs.Customers = append(cs.Customers, customer)
+	cs.CustomerNum += 1
+	return true
+}
+

删除客户

+

根据客户的ID寻找客户在切片中的位置,然后将它删除即可。

+
func (cv *CustomerView) changeCustomer() {
+	var id int
+	fmt.Print("请输入修改的ID号:")
+	fmt.Scanln(&id)
+	if cv.customerService.ChangeCustomer(id) {
+		fmt.Println("-------------------------修改成功-------------------------")
+	} else {
+		fmt.Println("-------------------------添加失败-------------------------")
+	}
+}
+
func (cs *CustomerService) DeleteCustomer(id int) bool {
+	for index, cus := range cs.Customers {
+		if cus.Id == id {
+			cs.Customers = append(cs.Customers[:index], cs.Customers[index+1:]...)
+			cs.CustomerNum -= 1
+			return true
+		}
+	}
+	return false
+}
+

修改客户

+

根据客户的ID寻找客户在切片中的位置,然后修改需要修改的字段即可。

+
func (cv *CustomerView) changeCustomer() {
+	var id int
+	fmt.Print("请输入修改的ID号:")
+	fmt.Scanln(&id)
+	if cv.customerService.ChangeCustomer(id) {
+		fmt.Println("-------------------------修改成功-------------------------")
+	} else {
+		fmt.Println("-------------------------添加失败-------------------------")
+	}
+}
+
func (cs *CustomerService) ChangeCustomer(id int) bool {
+
+	reader := bufio.NewReader(os.Stdin) // 标准输入输出
+
+	for index, cus := range cs.Customers {
+		if cus.Id == id {
+
+			fmt.Printf("请输入修改的姓名(%v):", cus.Name)
+			name, _ := reader.ReadString('\n')
+			name = strings.TrimSpace(name)
+			if len(name) != 0 {
+				cs.Customers[index].Name = name
+			}
+
+			fmt.Printf("请输入修改的性别(%v):", cus.Gender)
+			gender, _ := reader.ReadString('\n')
+			gender = strings.TrimSpace(gender)
+			if len(gender) != 0 {
+				cs.Customers[index].Gender = gender
+			}
+
+			fmt.Printf("请输入修改的年龄(%v):", cus.Age)
+			age, _ := reader.ReadString('\n')
+			age = strings.TrimSpace(age)
+			if len(age) != 0 {
+				t, _ := strconv.ParseInt(age, 10, 64)
+				cs.Customers[index].Age = int(t)
+			}
+
+			fmt.Printf("请输入修改的电话(%v):", cus.Phone)
+			phone, _ := reader.ReadString('\n')
+			phone = strings.TrimSpace(phone)
+			if len(phone) != 0 {
+				cs.Customers[index].Phone = phone
+			}
+
+			fmt.Printf("请输入修改的电子邮件(%v):", cus.Email)
+			email, _ := reader.ReadString('\n')
+			email = strings.TrimSpace(email)
+			if len(email) != 0 {
+				cs.Customers[index].Email = email
+			}
+
+			return true
+		}
+	}
+	return false
+}
+

修改的时候回车表示对这个字段不修改,因此要调一个reader的包来完成这个工作,自己无法作出这种判断。

+

完整源代码

+

.
+├── Customer-Management-System
+│ ├── main
+│ │ └── main.go
+│ ├── model
+│ │ └── customer.go
+│ ├── service
+│ │ └── customerService.go
+│ └── view
+│ └── customerView.go

+

main.go

+
package main
+
+import (
+	"Go-Projects/Customer-Management-System/view"
+)
+
+func main() {
+	view.NewCustomerView().MainMenu()
+}
+
+

customer.go

+
package model
+
+import "fmt"
+
+// 定义Customer结构体,表示一个客户信息
+type Customer struct {
+	Id     int
+	Name   string
+	Gender string
+	Age    int
+	Phone  string
+	Email  string
+}
+
+// 工厂模式返回Customer的结构体,在CustomerService里面使用
+// 感觉就是新建一个Customer的实例
+func NewCustomer(id int, name string, gender string, age int, phone string, email string) *Customer {
+	return &Customer{
+		Id:     id,
+		Name:   name,
+		Gender: gender,
+		Age:    age,
+		Phone:  phone,
+		Email:  email,
+	}
+}
+
+func (cu *Customer) GetInfo() string {
+	return fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v", cu.Id, cu.Name, cu.Gender, cu.Age, cu.Phone, cu.Email)
+}
+
+

customerService.go

+
package service
+
+import (
+	"Go-Projects/Customer-Management-System/model"
+	"bufio"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+)
+
+// 完成对Customer的操作,包括增删改查
+type CustomerService struct {
+	// 存储当前的客户
+	Customers []model.Customer
+	// 声明一个字段,表示当前切片含有多少个客户
+	CustomerNum int
+}
+
+// 初始化CustomerService
+func NewCustomerService() *CustomerService {
+	customerService := &CustomerService{} // 初始化
+	customerService.CustomerNum = 0
+	return customerService
+}
+
+func (cs *CustomerService) ShowCustomerSlice() []model.Customer {
+	return cs.Customers
+}
+
+func (cs *CustomerService) AddCustomer(customer model.Customer) bool {
+	cs.Customers = append(cs.Customers, customer)
+	cs.CustomerNum += 1
+	return true
+}
+
+func (cs *CustomerService) DeleteCustomer(id int) bool {
+	for index, cus := range cs.Customers {
+		if cus.Id == id {
+			cs.Customers = append(cs.Customers[:index], cs.Customers[index+1:]...)
+			cs.CustomerNum -= 1
+			return true
+		}
+	}
+	return false
+}
+
+func (cs *CustomerService) ChangeCustomer(id int) bool {
+
+	reader := bufio.NewReader(os.Stdin) // 标准输入输出
+
+	for index, cus := range cs.Customers {
+		if cus.Id == id {
+
+			fmt.Printf("请输入修改的姓名(%v):", cus.Name)
+			name, _ := reader.ReadString('\n')
+			name = strings.TrimSpace(name)
+			if len(name) != 0 {
+				cs.Customers[index].Name = name
+			}
+
+			fmt.Printf("请输入修改的性别(%v):", cus.Gender)
+			gender, _ := reader.ReadString('\n')
+			gender = strings.TrimSpace(gender)
+			if len(gender) != 0 {
+				cs.Customers[index].Gender = gender
+			}
+
+			fmt.Printf("请输入修改的年龄(%v):", cus.Age)
+			age, _ := reader.ReadString('\n')
+			age = strings.TrimSpace(age)
+			if len(age) != 0 {
+				t, _ := strconv.ParseInt(age, 10, 64)
+				cs.Customers[index].Age = int(t)
+			}
+
+			fmt.Printf("请输入修改的电话(%v):", cus.Phone)
+			phone, _ := reader.ReadString('\n')
+			phone = strings.TrimSpace(phone)
+			if len(phone) != 0 {
+				cs.Customers[index].Phone = phone
+			}
+
+			fmt.Printf("请输入修改的电子邮件(%v):", cus.Email)
+			email, _ := reader.ReadString('\n')
+			email = strings.TrimSpace(email)
+			if len(email) != 0 {
+				cs.Customers[index].Email = email
+			}
+
+			return true
+		}
+	}
+	return false
+}
+
+

customerView.go

+
package view
+
+import (
+	"Go-Projects/Customer-Management-System/model"
+	"Go-Projects/Customer-Management-System/service"
+	"fmt"
+)
+
+type CustomerView struct {
+	key             string                   // 接收用户输入
+	loop            bool                     // 表示是否循环的显示主菜单
+	username        string                   // 用户的用户名
+	password        string                   // 用户的密码
+	customerService *service.CustomerService // 获取用户服务
+}
+
+func NewCustomerView() *CustomerView {
+	return &CustomerView{
+		key:             "",
+		loop:            true,
+		username:        "admin",
+		password:        "password",
+		customerService: service.NewCustomerService(),
+	}
+}
+
+func (cv *CustomerView) login(username, password string) bool {
+	if username == cv.username && password == cv.password {
+		return true
+	}
+	return false
+}
+
+func (cv *CustomerView) addCustomer() {
+	id := cv.customerService.CustomerNum + 1
+	var name, gender, phone, email string
+	var age int
+	fmt.Print("请输入姓名:")
+	fmt.Scanln(&name)
+	fmt.Print("请输入性别:")
+	fmt.Scanln(&gender)
+	fmt.Print("请输入年龄:")
+	fmt.Scanln(&age)
+	fmt.Print("请输入电话:")
+	fmt.Scanln(&phone)
+	fmt.Print("请输入电子邮件:")
+	fmt.Scanln(&email)
+
+	if cv.customerService.AddCustomer(*model.NewCustomer(id, name, gender, age, phone, email)) {
+		fmt.Println("-------------------------添加成功-------------------------")
+	} else {
+		fmt.Println("-------------------------添加失败-------------------------")
+	}
+}
+
+func (cv *CustomerView) changeCustomer() {
+	var id int
+	fmt.Print("请输入修改的ID号:")
+	fmt.Scanln(&id)
+	if cv.customerService.ChangeCustomer(id) {
+		fmt.Println("-------------------------修改成功-------------------------")
+	} else {
+		fmt.Println("-------------------------添加失败-------------------------")
+	}
+}
+
+func (cv *CustomerView) deleteCustomer() {
+	var id int
+	fmt.Print("请输入删除的ID号:")
+	fmt.Scanln(&id)
+	if cv.customerService.DeleteCustomer(id) {
+		fmt.Println("-------------------------删除成功-------------------------")
+	} else {
+		fmt.Println("-------------------------删除失败-------------------------")
+	}
+}
+
+func (cv *CustomerView) showCustomer() {
+	if cv.customerService.CustomerNum == 0 {
+		fmt.Println("没有客户!")
+		return
+	}
+	fmt.Println("\n-------------------------客户列表-------------------------")
+	fmt.Println("编号\t姓名\t性别\t年龄\t电话\t电子邮件")
+	for _, eachCustomer := range cv.customerService.ShowCustomerSlice() {
+		fmt.Println(eachCustomer.GetInfo())
+	}
+}
+
+func (cv *CustomerView) exit() {
+	var choice byte
+	for {
+		fmt.Print("确定退出?(y/n):")
+		fmt.Scanf("%c\n", &choice)
+		if choice == 'y' {
+			cv.loop = false
+			break
+		} else if choice == 'n' {
+			break
+		} else {
+			fmt.Println("输入有误!!请重新输入")
+		}
+	}
+}
+
+func (cv *CustomerView) MainMenu() {
+	for {
+		var username, password string
+		fmt.Print("请输入用户名:")
+		fmt.Scanln(&username)
+		fmt.Print("请输入密码:")
+		fmt.Scanln(&password)
+		if cv.login(username, password) {
+			break
+		} else {
+			fmt.Println("用户名或密码错误!")
+		}
+	}
+	// 显示主菜单
+	for cv.loop {
+		fmt.Println("\n---------------------客户信息管理软件---------------------")
+		fmt.Println("                         1 添加客户")
+		fmt.Println("                         2 修改客户")
+		fmt.Println("                         3 删除客户")
+		fmt.Println("                         4 客户列表")
+		fmt.Println("                         5 退    出")
+		fmt.Print("请选择(1-5):")
+		// 接收用户的输入
+		fmt.Scanln(&cv.key)
+		// 对用户的输入进行判断
+		switch cv.key {
+		case "1":
+			cv.addCustomer()
+		case "2":
+			cv.changeCustomer()
+		case "3":
+			cv.deleteCustomer()
+		case "4":
+			cv.showCustomer()
+		case "5":
+			cv.exit()
+		default:
+			fmt.Println("请输入正确的选项------")
+		}
+	}
+}
+
+ + +
+ +
+
+ + + + + + +
+
+
Go项目-客户信息管理系统
+
https://zhangzhao219.github.io/2022/11/03/Go/Go-Project-Customer-Management-System/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年11月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/11/09/UCAS/information-retrieval/information-retrieval-final/index.html b/2022/11/09/UCAS/information-retrieval/information-retrieval-final/index.html new file mode 100644 index 000000000..9787d9cc8 --- /dev/null +++ b/2022/11/09/UCAS/information-retrieval/information-retrieval-final/index.html @@ -0,0 +1,1247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:现代信息检索-期末复习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:现代信息检索-期末复习

+ + +
+ +

《现代信息检索》期末复习

+ +

考试主要涉及概念上的问题,可能没有特别复杂的计算的内容

+

第1讲 布尔检索

+

倒排索引基本结构:

+

对每个词项t,记录所有包含t的文档列表。每篇文档用一个唯一的docID来表示,通常是正整数,如1,2,3…

+

为什么要用倒排索引:

+

当用户发起查询时(假设查询为一个关键词),搜索引擎会扫描索引库中的所有文档,找出所有包含关键词的文档,这样依次从文档中去查找是否含有关键词的方法叫做正向索引 。

+

为了增加效率, 搜索引擎会把正向索引变为倒排索引即把“文档→单词”的形式变为“单词→文档”的形式 。

+

倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。

+

布尔查询的处理优化:

+
    +
  • 每个布尔表达式都能转换成合取范式
  • +
  • 获得每个词项的df
  • +
  • 通过将词项的df相加,估计每个OR表达式对应的倒排记录表的大小
  • +
  • 按照上述估计从小到大依次处理每个OR表达式
  • +
+

问题:什么是倒排索引?为什么说倒排索引能加快检索的速度?假设“信息”、“检索”在倒排索引中是两个独立的term,试说明检索短语“信息检索”的基本流程。

+

答案:倒排索引指的是从词项到文档的一种索引结构。由于它直接可以从查询词定位到文档,所以能够大大加快检索的速度。检索短语“信息检索”的基本流程:从词典中分别查找到“信息”和“检索”这两个词,分别返回它们的倒排记录表,然后求这两个表的交集,在求交集时要考虑它们在文档中的位置相对关系。

+

词条 :一段文本中有效词的子序列,其中每个子序列称为一个词条。

+

词条类 :相同词条构成的集合。

+

词项 :一个词项指的是在信息检索系统词典中所包含的某个可能经过归一化处理的词条类。(词项集合和词条集合可以完全不同,比如可以采用某一个分类体系中的类别标签作为词项。当然,在实际的信息检索系统中,词项往往和词条密切相关)

+

注意:①文档-词项关联矩阵只包含01②要按字典序进行排序

+

zC3hFS.png

+

第2讲 索引构建

+

基于排序的索引构建方法存在的问题

+

在构建索引时,每次解析一篇文档,因此对于每个词项而言,其倒排记录表不到最后一篇文档都是不完整的。

+

如果每个 (termID, docID)对占用 8个字节, 那么处理大规模语料需要大量的空间。

+

一般内存的容量比较小,没有办法将前面产生的倒排记录表全部放在内存中,需要在磁盘上存储中间结果。

+

BSBI算法

+

一种减少寻道操作的排序:Blocked sort-based Indexing

+

将所有记录划分为每个大小约为10M的块,收集每一块的倒排记录,排序,将倒排索引写入硬盘,最后将不同的分块合并为一个大的倒排索引。

+

SPIMI算法

+

内存式单遍扫描索引构建算法:Single-pass in-memory indexing

+

关键思想:

+
    +
  • 对每个块都产生一个独立的词典(不需要在块之间进行 term-termID的映射)
  • +
  • 对倒排记录表不排序,按照它们出现的先后顺序排列,只对词典排序(实际上由于指针的存在,倒排记录表没有排序的必要)。
  • +
+

在扫描文档的同时,直接在内存中维护一个不断更新的倒排索引

+

因此对每个块生成一个完整的倒排索引,这些独立的索引最后合并成一个大索引

+

最终合并词典的过程中,需要进行词项字符串的比较,因为此时没有全局词典提供词项-整数ID的映射。

+

BSBI算法和SPIMI算法的主要区别

+

BSBI算法:在分块索引阶段,BSBI算法维护一个全局Term (String) – Termid (int) 的映射表,局部索引为Termid及其倒排记录表,仍然按词典顺序排序。

+

SPIMI算法:分块索引阶段与BSBI算法不同在于建立局部词典和索引,无需全局词典。在合并阶段,将局部索引两两合并,最后产生全局词典建立Term – Termid的映射。

+

使用文本预处理步骤可以大大减小系统所需要存储的倒排记录表的数目,从而提高索引构建和检索的速度

+

第3讲 索引压缩

+

有损压缩:丢弃一些信息-很多常用的预处理步骤可以看成是有损压缩

+

无损压缩:所有信息都保留-索引压缩中通常都使用无损压缩

+

词典压缩

+

定长数组方式下的词典存储:每个词项需要20(字符串)+4(词频)+4(指向倒排索引表的指针)=28个字节。

+

将整部词典看成单一字符串:4(词频)+4(指向倒排索引表的指针)+3(指向字符串的指针,按照实际大小决定,例如8*400000个位置需要$log_2(8 * 400000)< 24 $位来表示)+8(每个字符串平均需要8个字节)=19个字节

+

按块存储,假设块大小k=4,此时每4个词项只需要保留1个词项指针,但是同时需要增加4个字节(比较短,1个字节就可以)来表示每个词项的长度,因此每4个词项需要3+4=7B,比之前的节省了12-7=5B

+

前端编码:每个块当中 (k = 4)会有公共前缀,可以采用前端编码方式继续压缩

+

如果使用词干还原,由于将同一词汇的不同形式还原到词根,因此前端编码的压缩效果有限

+

倒排记录表压缩

+

倒排记录表的压缩:两种经典编码VB和γ编码(注意对gap进行编码,第一个id,后面都是gap

+

可变字节(VB)码:设定一个专用位 (高位) c作为延续位(continuation bit),如果间隔表示少于7比特,那么c置1,将间隔编入一个
+字节的后7位中;否则将高7位放入当前字节中,并将c置0,剩下的位数采用同样的方法进行处理,最后一个字节的c置1(表
+示结束)

+

编码

+
    +
  • 将G (Gap, 间隔) 表示成长度(length)和偏移(offset)两部分
  • +
  • 偏移对应G的二进制编码,只不过将首部的1去掉(因为所有的编码第一位都是1)
  • +
  • 长度部分给出的是偏移的位数,采用一元编码
  • +
  • 手动计算的时候先计算偏移,再根据偏移计算长度
  • +
+

zC8kTK.png

+

第4讲 拼写矫正

+

通道模型:

+

若有包含个词条的大文本语料,则是词频。(一元先验概率)

+

通道模型概率-计算错误概率:混淆“矩阵”(计算一个字符变为另一个字符的概率如何)

+

轮排索引:(主要思想:让星号出现在词汇的末尾)

+
    +
  • 将每个通配查询旋转,使*出现在末尾
  • +
  • 将每个旋转后的结果存放在词典中,即B-树中
  • +
+

轮排索引的查找过程:

+
    +
  • 将查询进行旋转,将通配符旋转到右部
  • +
  • 同以往一样查找B-树,得到匹配的所有词项,将这些词项对应的倒排记录表取出
  • +
+

相对于通常的B-树,轮排索引(轮排树)的空间要大4倍以上 (经验值)

+

k-gram索引:枚举一个词项中所有连读的k个字符构成k-gram(在首尾添加k-1个首尾符号)

+
    +
  • 构建一个倒排索引,此时词典部分是所有的k-gram,倒排记录表部分是包含某个k-gram的所有词项
  • +
  • 相当于对词项再构建一个倒排索引(二级索引)
  • +
  • 比轮排索引空间开销要小
  • +
  • 但是可能返回一些伪正例,需要进行后过滤
  • +
+

zC8KOI.png

+

k-gram索引 vs. 轮排索引

+
    +
  • k-gram索引的空间消耗小
  • +
  • 轮排索引不需要进行后过滤
  • +
+

第5讲 TF-IDF

+

tf-idf词频及log词频

+

TF是词项t的词项频率,是与文档相关的一个量,可以认为是文档内代表度的一个量,也可以认为是一种局部信息。

+

IDF是反映词项t的信息量的一个指标,是一种全局性指标,反应的是词项在全局的区别性,可视为一种词项全局信息量的指标。

+

向量空间模型基本思想:把查询和文本表示成向量(早期表示成TF-IDF权重)

+

向量空间模型的不同实现方案(不用背表,但是有很多情况,要看好题)(比如有时候idf不用算):

+

z9prND.md.png

+

注意:看好题目,不说对数、归一化什么的就不要做

+

zC8Jfg.png

+

第6讲 概率检索模型

+

主要是BM25模型的基本概念,IDF是怎么计算的,以及它的基本假设,伯努利分布

+

BIM的基本假设,BM25的二重泊松分布,考虑了哪些因素,如长度归一等等。

+

参考资料

+

以往的向量空间模型是将query和文档使用向量表示然后计算其内容相似性来进行相关性估计的,而概率检索模型是一种直接对用户需求进行相关性的建模方法,一个query进来,将所有的文档分为两类-相关文档、不相关文档,这样就转为了一个相关性的分类问题。

+

对于某个文档来说,表示该文档属于相关文档的概率,则表示该文档属于不相关文档的概率,如果query属于相关文档的概率大于不相关文档,则认为这个文档是与用户查询相关的。

+

使用贝叶斯公式转换一下,则在搜索排序过程中不需要真正的分类,只需要保证相关性由高到底排序即可,所以只需要降序即可,
+这样就最终转为计算的值即可。

+

二值独立概率模型BIM

+

为了能够使得上述两个计算因子可行,二元独立模型做出了两个假设

+
    +
  1. 二元假设
  2. +
+

类似于布尔模型中的文档表示方法,一篇文档在由特征(或者单词)进行表示的时候,以特征(或者单词)出现和不出现两种情况来表示,不考虑词频等其他因素。

+
    +
  1. 词汇独立性假设
  2. +
+

指文档里出现的单词之间没有任何关联,任意一个单词在文档的分布概率不依赖于其他单词是否出现。因为词汇之间没有关联,所以可以将文档概率转换为单词概率的乘积。

+

上述提到的文档D表示为,用来表示第个单词在相关文档出现的概率,则在已知相关文档集合的情况下,观察到D的概率为:

+

+

同理在不相关文档中出现的概率为

+

可以推导出:

+

设文档统计量如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
相关文档不相关文档文档数量
文档数量
+

则可以得出(加1平滑):

+

因此最终的公式为:

+

+

其代表的含义是:对于同时出现在用户查询Q和文档D中的单词,累加每个单词的估值,其和就是文档D和查询的相关性度量。

+

在不确定哪些文档是相关的,哪些文档是不相关的的时候,可以给公式的估算因子直接赋予固定值,则该公式将会退化为IDF因子。

+

优点:BIM模型建立在数学基础上,理论性较强

+

缺点:

+
    +
  • 需要估计参数
  • +
  • 原始的BIM没有考虑TF、文档长度因素
  • +
  • BIM中同样存在词项独立性假设
  • +
  • BIM实质上是一个idf权重公式,仅考虑了全局信息,缺少局部信息。因此需要和TF权重配合使用
  • +
+

BM25模型

+

BM25模型计算公式其实融合了4个考虑因素:IDF因子,文档长度因子,文档词频和查询词频。并对3个自由调节因子进行权值的调整。

+

IDF因子:设BIM模型中的相关文档数量为0,则退化为

+

查询权重:,考虑查询词频

+

TF权重(基于二重泊松分布):,考虑文档中词频和文档长度

+

最终形式为三项相乘

+

例题:

+

zCMO8s.png

+

zCMxK0.png

+

优点:

+
    +
  • 一定程度上的理论化模型
  • +
  • 基于二重泊松假设——适用于绝大多数文本语料上的IR检索应用
  • +
  • 实验证明有效
  • +
+

缺点:

+
    +
  • 待调参数多且参数敏感性高
  • +
  • 必须去停用词
  • +
+

问题:BM25和向量空间模型(VSM)为何需要长度归一?语言模型为何需要平滑处理?两个问题之间有何联系?

+

答案:由于长文挡中词项反复出现的可能性大,包含更多的不同词项,所以词项频率和词汇量可能更大。这显然是不公平的。长度归一化,可以使长文档和短文档的向量中的权重都处于同一数量级。平滑处理是为了解决数据稀疏引起的0概率问题。两者都是常见的数据预处理方法,提高了数据质量,为了保证模型的鲁棒性。

+

第7讲 语言建模的检索模型

+

流行的是基于多项式分布,对于生成模型的计算有零概率的问题,需要进行平滑,基本概念要知道

+

zC8stU.png

+

第8讲 信息检索的评价

+

指标计算,如正确率召回率等等,F1,未插值的AP

+

题目:什么是非插值的MAP?为什么说它在引入序的作用的同时考虑了召回率?

+

答案:单个查询的非插值MAP指的是所有相关文档(不论是否在结果出现,若不出现就假定出现在无穷远处)在结果出现位置上的正确率的算术平均值。系统的非插值MAP是所有查询上的非插值AP的算术平均值。从非插值AP的定义看,一方面,如果出现在结果中的相关文档越多,求和结果也越大,那么非插值AP值也越大。另一方面,如果相关文档在结果中出现位置越靠前,那么非插值AP值也越大。因此,可以认为非插值MAP同时考底了召回率和序的作用。

+

Bpref:在相关性判断不完全的情况下,计算在进行了相关性判断的文档集合中,在判断到相关文档前,需要判断的不相关文档的篇数。相关性判断完全的情况下,利用Bpref和MAP进行评价的结果很一致,但是相关性判断不完全的情况下,Bpref更鲁棒。

+

NDCG:每个文档不仅仅只有相关和不相关两种情况,而是有相关度级别,比如0,1,2,3。我们可以假设,对于返回结果:相关度级别越高的结果越多越好,相关度级别越高的结果越靠前越好

+

优点:

+
    +
  • 图形直观,易解释
  • +
  • 支持非二值的相关度定义,比P-R曲线更精确
  • +
  • 能够反映用户的行为特征(如:用户的持续性persistence)
  • +
+

缺点:

+
    +
  • 相关度的定义难以一致
  • +
  • 需要参数设定
  • +
+

zCKQeO.png

+

zC8476.png

+

zC8g1J.png

+

zC8fn1.png

+

第9讲 完整搜索系统中的评分计算

+

考试基本不涉及

+

第10讲 查询扩展

+

相关反馈的本质是将检索返回的文档的相关性判定(不同的判定来源:人工或非人工)作为返回信息,希望提升检索效果(召回率和正确率)

+

反馈信息的来源:显式(用户点击)、隐式(用户行为等)、伪相关反馈(返回的前几个结果算相关)

+

Rocchio算法

+

查询扩展的最初含义是对查询进行扩充,近年来越来越向查询重构偏移,即现在的查询扩展是指对原有查询进行修改。

+

通过在查询中加入同义或者相关的词项来提高检索结果。

+

相关词项的来源: 人工编辑的同义词词典、自动构造的同义词词典、查询日志等等。

+

查询扩展和相关反馈对检索效果的提升是非常有用的经验性的方法

+

问题:什么是伪相关反馈?为什么说有时候伪相关反馈会降低某个查询的检索效果?

+

答案:伪相关反馈指的是系统对上次返回的检索结采进行“伪”判定(比如假设前几个结果是相关的),然后根据这个结果进行反馈。伪相关反馈依赖于上次检索的结果,那么在上次检索结果不可靠情况下,假设成立的可能性很小,此时就进行伪相关反馈反而可能降低后一次检索的效果。

+

注意:负权重要记为0,同时也要进行排序

+

zC8jBt.png

+

第11、12、13讲 文本分类

+

问题:文本分类当中,什么是宏平均?什么是微平均?为什么说微平均计算时易受大类影响?

+

答案:宏平均指的是在每个类别上分类效果的平均值,也即将每个类别看成一个单位。而微平均是将所有类别看成一个类别后求到的效果值,即将每篇文档看成一个单位。由于微平均将文档看成单位,而大类文档数目较多,因此它在计算时易受大类影响。

+

朴素贝叶斯(线性分类器)

+

使用log将乘积计算变为求和计算

+

最大似然估计(零概率的情况下怎么进行加一平滑)

+

Rocchio分类(线性分类器)

+

计算每个类的中心向量(所有文档向量的算术平均)

+

将每篇测试文档分到离它最近的那个中心向量

+

Rocchio分类器是要训练的

+

KNN(非线性分类器)

+

kNN分类决策取决于k个邻居类中的多数类

+

类别之间的分类面是分段线性的

+

kNN分类器几乎不需要训练

+

但是像kNN这种非线性学习方法在某些情况下也会产生一个线性分类器

+

SVM

+

SVM分线性SVM和非线性SVM,SVM本身是一个线性决策,但是核函数可以是线性或非线性的

+

算法本身是转化成一个线性公式,但是最终得到的是一个非线性的决策面,只不过把样本投射到高维空间里面

+

问题:总结SVM中处理线性不可分数据的方法,给出其基本原理。

+
    +
  • 广义最优分类面:在条件中增加一个松弛项,容纳少量线性不可分的噪声样本。
  • +
  • 核函数:从低维空间非线性映射到线性可分的高维空间。
  • +
+

问题:什么是核函数?它的作用是什么?为什么核函数的引入常常称为核技巧?

+

答案:核函数是满足若干性质的相似度计算函数。它的主要作用是计算两个对象的相似度,具体地说,它可以基于原始空间上的点来定义映射后空间上的内积函数。核函数避免知道空间映射的具体函数形式,能够直接基于核函数进行映射后的对象相似度计算,所以它的引入常常称为核技巧。

+

偏差和方差

+

对于像Rocchio和NB一样的线性方法来说,对于非线性问题它们的偏差就比较大

+

像kNN一样的非线性方法的偏差较小,方差较大

+

如果拥有的训练数据非常少,而又要训练出一个基于监督学习的分类器,应该采用具有高偏差的分类器,在这种情况下NB能取得较好的结果,诸如kNN的低偏差模型大概是不可取的。

+

分类题目

+

zCGEBq.png

+

zCGn4U.png

+

第12讲 排序学习

+

现有检索排序算法存在哪些问题,怎么改进?

+

很多传统的IR权重计算机制中都包含了基本指标的非线性缩放过程(比如词项频率或idf 的对数权重计算)。目前为止,机器学习非常擅长特征线性组合(或者其他类似的受限模型)中的权重优化,但是并不擅长基本指标的非线性缩放。这个领域仍然需要人工的特征工程方法。

+

基于布尔权重的学习

+

给定训练样例集合,每个样例表示为三元组,相关或者不相关

+

从上述样例中学习权重,使得学到的评分接近训练集中的相关性判定结果。

+

基于实数权重的学习(pointwise)

+

设置评分函数是两个因子的线性组合:查询和文档的向量空间相似度评分和查询词项在文档中存在的最小窗口宽度

+

相关记为1,不相关记为0,我们的目标是寻找一个评分函数,该函数能够组合特征因子的值,并尽量接近0或1,希望该函数的结果尽量与训练集上的结果保持一致

+

基于序回归的排序学习(pairwise)

+

为什么将IR排序问题看成一个序回归问题?

+
    +
  • 对于同一查询,文档之间可以按照相对得分排序即可,并不一定要求每篇文档有一个全局的绝对得分
  • +
  • 因此,只需要一个排序,而不要得到相关度的绝对得分,问题空间可以减小
  • +
  • pairwise 方法相对 pointwise 方法对噪声标注更敏感,即一个错误标注会引起多个 doc pair 标注错误。
  • +
+

方法:

+
    +
  • 给定一些已经判定的查询
  • +
  • 对训练集中的每条查询, 我们都有针对该查询的一系列文档集合,这些文档已经由人工按照其与查询的相关度排序
  • +
  • 对每个文档、查询对,构造特征向量,这里的特征可以采用前面的特征
  • +
  • 对于两篇文档,可以计算特征向量之间的差异向量
  • +
  • 依据假设,中的一个更相关
  • +
  • 如果更相关,记为(在检索结果中,应该出现在前面), 那么分配给向量的类别为,否则为
  • +
  • 学习的目标是建立一个分类器,满足:
  • +
+

第14、15讲

+

词项表示:通过分析文档集来自动生成同义词库-基于共现的同义词库

+

词嵌入:得到每个词的低维密集向量表示

+

Neural IR 模型分类

+

Representation based(基于表示学习的模型):学习文本的分布式表示,在高维空间匹配

+
    +
  • 词表示:one hot → distributed
  • +
  • 句子表示:bag of words → distributed
  • +
  • 匹配能力取决于学习文本表示的算法能力
  • +
  • 代表模型:DSSM, CDSSM
  • +
+

Matching function(基于交互匹配的模型):文本之间先进行交互匹配,再对匹配信号进行融合

+
    +
  • 输入:比较底层的输入
  • +
  • 匹配函数:cosine, dot product → NN
  • +
  • 优点:可以考虑更加丰富的匹配信号, 如软匹配 (soft matching)
  • +
  • 代表模型:MatchPyramid , DRMM, K NRM, PACRR, NPRF
  • +
+

Combination of both: 既考虑 Representation 又考虑 Matching function

+

BERT在检索应用中的特点:

+
    +
  1. 在高频查询上,BM25的偏差比BERT更大,导致BM25的效果不好
  2. +
  3. BERT可以检索到更稀有的词项
  4. +
  5. 在长查询上,BERT的表现不如BM25更好
  6. +
+

问题:简述BERT的基本结构?如何预训练一个BERT(涉及什么任务)?

+

BERT的基本结构:

+
    +
  1. 词向量
  2. +
  3. 多层Transformer Encoder结构:包括自注意力和Feed-Forward
  4. +
  5. 任务特定的输出层
  6. +
+

BERT的训练任务有两类:

+
    +
  1. masked language model 随机掩盖掉一些单词,然后通过上下文预测该单词。BERT中有15%的wordpiece token会被随机掩盖,这15%的token中80%用[MASK]这个token来代替,10%用随机的一个词来替换,10%保持这个词不变。这种设计使得模型具有捕捉上下文关系的能力,同时能够有利于token-level tasks,例如序列标注等。
  2. +
  3. next sentence prediction 语料中50%的句子,选择其相应的下一句一起形成上下句,作为正样本;其余50%的句子Embedding随机选择一句非下一句一起形成上下句,作为负样本。这种设定,有利于sentence-level tasks,例如问答,注意:作者特意说了语料的选取很关键,要选用document-level的而不是sentence-level的,这样可以具备抽象连续长序列特征的能力。
  4. +
+

第16讲 Web搜索

+

Google次高竞标价格拍卖机制:

+

zS0Wkt.png

+

bid:每个广告商为每次点击给出的最大投标价格

+

CTR:一旦被显示后被点击的比率

+

ad rank=bid × CTR:这种做法可以在广告商愿意支付的价钱和广告的相关度高低之间进行平衡。

+

排名第1的C,需要支付的价格是它的下一位的

+

排名第2的B,需要支付的价格是它的下一位的

+

这样做避免了“保底”行为的产生,可以使收益更大化。

+

第17讲 爬虫

+

采集器必须做到

+
    +
  • 礼貌性 +
      +
    • 不要高频率采集某个网站
    • +
    • 仅仅采集robots.txt所规定的可以采集的网页 +
        +
      • robots.txt协议不让采集,不过写程序还是可以采集到的,但是不能这样做,一定要遵守协议
      • +
      +
    • +
    +
  • +
  • 鲁棒性 +
      +
    • 能够处理采集器陷阱、重复页面、超大页面、超大网站、动态页面等问题
    • +
    +
  • +
+

第18讲 链接分析

+

锚文本是人为创建的超链接,可以理解为质量认证的信号。

+

BV算法-邻接表压缩的经典算法

+

邻接表:一个节点的邻居集合,可以视为一个结点(URL)所有指向它的页面的集合

+

假设每个URL由一个整数表示,对于40亿页的网站,每个结点需要32位甚至64位,存储开销非常大

+

BV算法可以降低到平均3位

+

压缩中使用到的属性:

+
    +
  • 相似度(邻接表之间)
  • +
  • 位置(一个页面中的许多链接都连接到“附近”的页面)-将所有URL按照字母顺序排序,同一个网站的页面的链接相似
  • +
  • 在已排序的邻接表中使用间隔编码
  • +
  • gap value 的分布
  • +
+

BV算法主要思想:由于模板的缘故,一个节点的邻接列表类似于字典顺序中的7个先前的URL之一,根据这7个中的之一表示邻接表,否则重新编码。

+

BV算法的主要优势

+
    +
  • 仅依赖于位置的规范顺序-字典顺序对web十分适用
  • +
  • 邻接查询可以被非常高效地回答
  • +
  • 易于实现one-pass算法 +
      +
    • 顺序读取,不需要无限缓冲。读取复杂度与网页数量是线性关系
    • +
    +
  • +
+

PageRank

+

起源 : 引用分析

+

特点:

+
    +
  1. 一个网页如果它的入链越多,那么它也越重要(PageRank越高)
  2. +
  3. 一个网页如果被越重要的网页所指向,那么它也越重要(PageRank越高)
  4. +
+

PageRank背后的假设:

+
    +
  1. Web 上的链接是网页质量的标志-链出网页的作者认为链向的网页具有很高的质量
  2. +
  3. 锚文本能够描述链向网页的内容
  4. +
+

PageRank的计算:迭代法计算

+

如果存在循环通路,需要虚拟一个结点,或者以一定的概率选取一个其他结点到达

+

HITS: Hub节点&Authority节点

+

每个网页计算两个值:

+
    +
  • Hub:目录型或导航型网页的权重
  • +
  • Authority:权威型网页的权重
  • +
+

计算方法:

+

,其中是所有链接到的页面

+

,其中是所有页面链接到的页面

+
    +
  • 一个网页被越重要的导航型网页指向越多,那么它的Authority越大
  • +
  • 一个网页指向的高重要度权威型网页越多,那么它的Hub越大
  • +
+

实际计算过程:

+
    +
  1. 首先进行Web 搜索,搜索的结果称为根集(从搜索结果中选择一部分排名靠前的网页作为根集,也叫做种子集合)
  2. +
  3. 将所有链向种子集合和种子集合链出的网页加入到种子集合,新的更大的集合称为基本集
  4. +
  5. 最后,在基本集上计算每个网页的hub值和authority值
  6. +
+

PageRank vs. HITS

+

PageRank算法是Google提出的一个链接分析的算法,它可以根据节点之间的链接关系计算出每个节点的重要性,反映的是“越多越重要的节点指向该节点则该节点越重要”这个事实。

+

HITS是IBM提出的另一种链接分析算法,它根据节点之间的链接关系对每个节点计算出两个值:权威度(authority值)和导航度(hub值).

+

相同点:两者都是基于链接分析的排序算法,并且在算法中两者都利用了特征向量作为理论基础和收敛性依据。

+

不同点:网页的PageRank是一种静态评分,与查询主题无关,可以事先算好,因此适合于大型搜索引擎;HITS算法的计算与查询主题相关,检索之后再进行计算,因此不适合于大型搜索引擎。

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:现代信息检索-期末复习
+
https://zhangzhao219.github.io/2022/11/09/UCAS/information-retrieval/information-retrieval-final/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年11月9日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/11/11/Go/Go-Project-Mass-Communication-System/index.html b/2022/11/11/Go/Go-Project-Mass-Communication-System/index.html new file mode 100644 index 000000000..6443c1df6 --- /dev/null +++ b/2022/11/11/Go/Go-Project-Mass-Communication-System/index.html @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go项目-海量用户通讯系统 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go项目-海量用户通讯系统

+ + +
+ +

Go项目-海量用户通讯系统

+ +

项目开发流程

+
    +
  1. 实现客户端登录菜单以及简单的用户登录逻辑
  2. +
  3. 实现用户登录(与服务器端进行通信验证用户的信息)
  4. +
  5. 客户端可以发送消息的长度,服务器端可以接收消息的长度
  6. +
  7. 客户端可以发送消息本身,服务器端可以接收消息
  8. +
  9. 改进服务器端和客户端的结构,更易读
  10. +
  11. 增加数据库验证,增加一层models,同时实现用户的注册和登录
  12. +
  13. 维护用户在线列表
  14. +
  15. 客户端发送消息
  16. +
  17. 服务器端转发消息
  18. +
+

项目需求说明

+

用户注册、用户登录、显示在线用户列表、群聊(广播)、点对点聊天、离线留言

+

项目代码编写

+

实现客户端登录菜单以及简单的用户登录逻辑

+
package main
+
+import "fmt"
+
+// 定义两个变量,一个表示用户ID,一个表示用户密码
+var userId int
+var userPwd string
+
+func main() {
+
+	// 接收用户的选择
+	var key int
+	// 判断是否还能继续显示菜单
+	var loop = true
+	// 循环展示菜单
+	for loop {
+		fmt.Println("---------------欢迎登录多人聊天系统---------------")
+		fmt.Println("---------------   1 登录聊天室")
+		fmt.Println("---------------    2 注册用户")
+		fmt.Println("---------------    3 退出系统")
+		fmt.Println("请选择(1-3):")
+		fmt.Scanln(&key)
+		switch key {
+		case 1:
+			fmt.Println("登录聊天室")
+			loop = false
+		case 2:
+			fmt.Println("注册用户")
+		case 3:
+			fmt.Println("退出系统")
+			loop = false
+		default:
+			fmt.Println("输入有误,请重新输入")
+		}
+	}
+	if key == 1 {
+		fmt.Println("请输入用户ID")
+		fmt.Scanln(&userId)
+		fmt.Println("请输入用户密码")
+		fmt.Scanln(&userPwd)
+		// 先把登录的函数写在另外一个文件
+		err := login(userId, userPwd)
+		if err != nil {
+			fmt.Println("登录失败")
+		} else {
+			fmt.Println("登录成功")
+		}
+	} else if key == 2 {
+		fmt.Println("进行用户注册的逻辑")
+	}
+}
+

登录逻辑的判断首先写在另外的文件中,后续再进行修改

+
package main
+
+import "fmt"
+
+func login(userId int, userPwd string) (err error) {
+	fmt.Printf("userId=%d, userPed=%s\n", userId, userPwd)
+	return nil
+}
+

注意这种在同一个包下引用函数的方式需要在src文件夹之外进行编译,然后手动运行

+

实现用户登录(与服务器端进行通信验证用户的信息)

+

重点是如何发送包以及如何对包进行校验,同时要保证多线程

+

zESgG6.md.png

+

消息长度的发送与接收

+

要对发送的消息进行序列化等操作,首先定义好处理这些数据的结构体

+
package message
+
+// 确定消息类型
+const (
+	LoginMesType    = "LoginMes"
+	LoginResMesType = "LoginResMes"
+)
+
+type Message struct {
+	Type string `json:"type"` // 消息类型
+	Data string `json:"data"` // 消息内容
+}
+
+// 定义两个消息,后面需要再增加
+type LoginMes struct {
+	UserId   int    `json:"userId"`   // 用户Id
+	UserPwd  string `json:"userPwd"`  // 用户密码
+	UserName string `json:"userName"` // 用户名
+}
+
+type LoginResMes struct {
+	Code  int    `json:"code"`  // 返回的状态码 500 表示用户未注册,200 表示成功
+	Error string `json:"error"` // 返回错误信息
+}
+

客户端发送消息(消息的长度)

+
package main
+
+import (
+	"Go-Projects/Mass-Communication-System/common/message"
+	"encoding/binary"
+	"encoding/json"
+	"fmt"
+	"net"
+)
+
+func login(userId int, userPwd string) (err error) {
+	// fmt.Printf("userId=%d, userPed=%s\n", userId, userPwd)
+	// return nil
+	// 连接到服务器端
+	conn, err := net.Dial("tcp", "localhost:8889")
+	if err != nil {
+		fmt.Println("net.Dial err=", err)
+		return
+	}
+	defer conn.Close()
+	// 准备通过conn发送消息给服务
+	var mes message.Message
+	mes.Type = message.LoginMesType
+	// 创建一个LoginMes结构体
+	var loginMes message.LoginMes
+	loginMes.UserId = userId
+	loginMes.UserPwd = userPwd
+	// 将loginMes序列化
+	data, err := json.Marshal(loginMes)
+	if err != nil {
+		fmt.Println("json Marshal err=", err)
+		return
+	}
+	mes.Data = string(data)
+	// 将mes进行序列化
+	data, err = json.Marshal(mes)
+	if err != nil {
+		fmt.Println("json Marshal err=", err)
+		return
+	}
+	// data为发送的消息
+	// 先把data的长度发送给服务器
+	var pkgLen = uint32(len(data))
+	var buf [4]byte
+	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
+	//  发送长度
+	n, err := conn.Write(buf[:4])
+	if n != 4 || err != nil {
+		fmt.Println("conn.Write err=", err)
+		return
+	}
+	fmt.Println("客户端发送的消息长度为", len(data))
+	fmt.Println("客户端发送的消息内容为", string(data))
+	return
+}
+
+

服务器端接收消息

+
package main
+
+import (
+	"fmt"
+	"net"
+)
+
+func process(conn net.Conn) {
+	// 延时关闭连接
+	defer conn.Close()
+	// 读取客户端发送的信息
+	for {
+		buf := make([]byte, 1024*4)
+		fmt.Println("等待读取客户端发送的数据.....")
+		n, err := conn.Read(buf[:4])
+		if n != 4 || err != nil {
+			fmt.Println("conn.Read err=", err)
+			return
+		}
+		fmt.Println("读到的长度为", buf[:4])
+	}
+
+}
+
+func main() {
+	fmt.Println("服务器在8889端口监听.....")
+	listen, err := net.Listen("tcp", "localhost:8889")
+	defer listen.Close()
+	if err != nil {
+		fmt.Println("net.Listen err=", err)
+		return
+	}
+	// 一旦监听成功,等待客户端连接服务器
+	for {
+		fmt.Println("等待客户端连接服务器.....")
+		conn, err := listen.Accept()
+		if err != nil {
+			fmt.Println("listen.Accept err=", err)
+		}
+		// 一旦连接成功,则启动一个协程和客户端保持通讯
+		go process(conn)
+	}
+}
+
+

客户端发送消息本身,服务器端进行接收

+

将服务器端的消息接收封装成一个函数

+
func readPkg(conn net.Conn) (mes message.Message, err error) {
+	buf := make([]byte, 1024*4)
+	fmt.Println("等待读取客户端发送的数据.....")
+	_, err = conn.Read(buf[:4])
+	if err != nil {
+		fmt.Println("conn.Read err=", err)
+		return
+	}
+	// fmt.Println("读到的长度为", buf[:4])
+	// 转换为一个uint32类型
+	var pkgLen = binary.BigEndian.Uint32(buf[0:4])
+	//  发送长度
+	n, err := conn.Read(buf[:pkgLen])
+	if n != int(pkgLen) || err != nil {
+		fmt.Println("conn.Read err=", err)
+		return
+	}
+	// 把pkgLen反序列化成message
+	err = json.Unmarshal(buf[:pkgLen], &mes)
+	if err != nil {
+		fmt.Println("json.Unmarshal err=", err)
+		return
+	}
+	return
+}
+

客户端发送消息

+
// 发送消息本身
+_, err = conn.Write(data)
+if err != nil {
+	fmt.Println("conn.Write err=", err)
+	return
+}
+

完成登录的验证功能(相当于服务器发送消息,客户端接收)

+

服务器端封装一个发送消息的函数

+
func writePkg(conn net.Conn, data []byte) (err error) {
+	// 先发送一个长度
+	var pkgLen = uint32(len(data))
+	var buf [4]byte
+	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
+	//  发送长度
+	_, err = conn.Write(buf[:4])
+	if err != nil {
+		fmt.Println("conn.Write err=", err)
+		return
+	}
+
+	//发送data本身
+	n, err := conn.Write(data)
+	if n != int(pkgLen) || err != nil {
+		fmt.Println("conn.Write err=", err)
+		return
+	}
+	return
+}
+

将这种请求通用化,为后面的其他消息做准备

+
// 编写serverProcessLogin函数,专门处理登录的请求
+func serverProcessLogin(conn net.Conn, mes *message.Message) (err error) {
+	// 从mes中取出data,并反序列化
+	var loginMes message.LoginMes
+	err = json.Unmarshal([]byte(mes.Data), &loginMes)
+	if err != nil {
+		fmt.Println("json.Unmarshal error, err=", err)
+		return
+	}
+	// 先声明一个resMes
+	var resMes message.Message
+	resMes.Type = message.LoginResMesType
+	// 声明一个LoginResMes
+	var loginResMes message.LoginResMes
+	// 如果用户的id为100,密码为123456,认为合法,否则不合法
+	if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
+		//合法
+		loginResMes.Code = 200
+	} else {
+		//不合法
+		loginResMes.Code = 500
+		loginResMes.Error = "该用户不存在,请注册再使用..."
+	}
+	// 将loginResMes序列化
+	data, err := json.Marshal(loginResMes)
+	if err != nil {
+		fmt.Println("json.Marshal error, err=", err)
+		return
+	}
+	// 将data赋值给resMes
+	resMes.Data = string(data)
+	// 对resMes进行序列化,准备发送
+	data, err = json.Marshal(resMes)
+	if err != nil {
+		fmt.Println("json.Marshal error, err=", err)
+		return
+	}
+	// 发送data,封装到writePkg函数
+	err = writePkg(conn, data)
+	return
+}
+
+// 根据客户端发送消息种类不同,决定调用哪个函数来实现
+func serverProcessMes(conn net.Conn, mes *message.Message) (err error) {
+	switch mes.Type {
+	case message.LoginMesType:
+		// 处理登录的逻辑
+		err = serverProcessLogin(conn, mes)
+	case message.RegisterMesType:
+		// 处理注册的逻辑
+	default:
+		fmt.Println("消息类型不存在,无法处理")
+	}
+	return
+}
+

客户端对消息进行处理

+
// 处理服务器端返回的消息
+mes, err = readPkg(conn)
+if err != nil {
+	fmt.Println("readPkg(conn) error, err=", err)
+	return
+}
+// 将mes的data部分反序列化
+var loginResMes message.LoginResMes
+err = json.Unmarshal([]byte(mes.Data), &loginResMes)
+if loginResMes.Code == 200 {
+	fmt.Println("登录成功")
+} else if loginResMes.Code == 500 {
+	fmt.Println(loginResMes.Error)
+}
+

改进服务器端和客户端的结构,更易读

+

zEDSBt.png

+

改进主要是将前面编写的函数封装进方法之中,减少不同函数之间参数的传递,通过结构体直接调用即可

+

客户端的改进增加了一个与服务器端保持联系的函数

+
// 和服务器端保持通讯
+func serverProcessMes(conn net.Conn) {
+	tf := &utils.Transfer{
+		Conn: conn,
+	}
+	for {
+		fmt.Println("客户端正在等待读取服务器发送的消息")
+		mes, err := tf.ReadPkg()
+		if err != nil {
+			fmt.Println("tf.ReadPkg err=", err)
+			return
+		}
+		// 如果读取到消息,下一步进行处理
+		fmt.Println(mes)
+	}
+}
+

增加数据库验证,增加一层models,同时实现用户的注册和登录

+

MVC开发模式,增加models,从而从数据库中进行读取和接收,验证用户的有效性

+

models层

+
package model
+
+import (
+	"Go-Projects/Mass-Communication-System/common/message"
+	"encoding/json"
+	"fmt"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+// 使用工厂模式创建一个UserDao的实例
+func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
+	userDao = &UserDao{
+		pool: pool,
+	}
+	return
+}
+
+// 在服务器启动后初始化一个userDao实例
+var (
+	MyUserDao *UserDao
+)
+
+// 定义一个userDao的结构体
+type UserDao struct {
+	pool *redis.Pool
+}
+
+// 根据用户id返回user实例
+func (ud *UserDao) getUserById(conn redis.Conn, id int) (user *User, err error) {
+	res, err := redis.String(conn.Do("HGET", "users", id))
+	if err != nil {
+		if err == redis.ErrNil {
+			err = ERROR_USER_NOTEXISTS
+		}
+		return
+	}
+
+	user = &User{}
+
+	// 把res反序列化成User实例
+	err = json.Unmarshal([]byte(res), user)
+	if err != nil {
+		fmt.Println("json.Unmarshal err=", err)
+		return
+	}
+	return
+}
+
+// 完成登录的校验
+func (ud *UserDao) Login(userId int, userPwd string) (user *User, err error) {
+	conn := ud.pool.Get()
+	defer conn.Close()
+	user, err = ud.getUserById(conn, userId)
+	if err != nil {
+		return
+	}
+	if user.UserPwd != userPwd {
+		err = ERROR_USER_PWD
+		return
+	}
+	return
+}
+
+// 注册
+func (ud *UserDao) Register(user *message.User) (err error) {
+	conn := ud.pool.Get()
+	defer conn.Close()
+	_, err = ud.getUserById(conn, user.UserId)
+	if err == nil {
+		err = ERROR_USER_EXISTS
+		return
+	}
+	// 说明该用户还没有注册过,则可以完成注册
+	data, err := json.Marshal(user)
+	if err != nil {
+		return
+	}
+	_, err = conn.Do("HSET", "users", user.UserId, string(data))
+	if err != nil {
+		fmt.Println("保存注册用户错误,err=", err)
+		return
+	}
+	return
+}
+
+

处理注册的请求

+
// 编写ServerProcessRegister函数,专门处理注册的请求
+func (u *UserProcess) ServerProcessRegister(mes *message.Message) (err error) {
+	// 从mes中取出data,并反序列化
+	var registerMes message.RegisterMes
+	err = json.Unmarshal([]byte(mes.Data), &registerMes)
+	if err != nil {
+		fmt.Println("json.Unmarshal error, err=", err)
+		return
+	}
+	// 先声明一个resMes
+	var resMes message.Message
+	resMes.Type = message.RegisterResMesType
+	// 声明一个RegisterResMes
+	var registerResMes message.RegisterResMes
+
+	err = model.MyUserDao.Register(&registerMes.User)
+
+	if err != nil {
+		if err == model.ERROR_USER_EXISTS {
+			registerResMes.Code = 505
+			registerResMes.Error = err.Error()
+		} else {
+			registerResMes.Code = 506
+			registerResMes.Error = "注册发生未知错误"
+		}
+
+	} else {
+		registerResMes.Code = 200
+	}
+
+	// 将loginResMes序列化
+	data, err := json.Marshal(registerResMes)
+	if err != nil {
+		fmt.Println("json.Marshal error, err=", err)
+		return
+	}
+	// 将data赋值给resMes
+	resMes.Data = string(data)
+	// 对resMes进行序列化,准备发送
+	data, err = json.Marshal(resMes)
+	if err != nil {
+		fmt.Println("json.Marshal error, err=", err)
+		return
+	}
+	// 发送data,封装到writePkg函数
+	tf := &utils.Transfer{
+		Conn: u.Conn,
+	}
+	err = tf.WritePkg(data)
+	return
+}
+

维护用户在线列表

+

完成对当前在线用户的增删改查

+
package process2
+
+import "fmt"
+
+// 在服务器端实例只有一个,在很多的地方都会使用到
+
+var (
+	userMgr *UserMgr
+)
+
+type UserMgr struct {
+	onlineUsers map[int]*UserProcess
+}
+
+// 完成对userMgr初始化工作
+func init() {
+	userMgr = &UserMgr{
+		onlineUsers: make(map[int]*UserProcess, 1024),
+	}
+}
+
+// 完成对onlineUsers的增删改查
+
+func (um *UserMgr) AddOnlineUser(up *UserProcess) {
+	um.onlineUsers[up.UserId] = up
+}
+
+func (um *UserMgr) DelOnlineUser(userId int) {
+	delete(um.onlineUsers, userId)
+}
+
+func (um *UserMgr) GetAllOnlineUser() map[int]*UserProcess {
+	return um.onlineUsers
+}
+
+func (um *UserMgr) GetOnlineUserById(userId int) (up *UserProcess, err error) {
+	up, ok := um.onlineUsers[userId]
+	if !ok {
+		err = fmt.Errorf("用户%d不存在", userId)
+		return
+	}
+	return
+}
+
+

显示当前在线用户列表

+
// 因为用户登录成功,要将用户放入全局变量中以返回列表
+u.UserId = loginMes.UserId
+userMgr.AddOnlineUser(u)
+// 将当前在线用户的id放入到loginResMes.UsersIds
+for id := range userMgr.onlineUsers {
+	loginResMes.UsersIds = append(loginResMes.UsersIds, id)
+}
+fmt.Println(user, "登录成功")
+
// 显示当前在线用户列表
+fmt.Println("当前在线用户列表如下:")
+for _, v := range loginResMes.UsersIds {
+	fmt.Println("用户id,\t", v)
+}
+

服务器端对用户列表进行处理

+
// 通知所有用户在线
+func (u *UserProcess) NotifyOthersOnlineUser(userId int) {
+	for id, up := range userMgr.onlineUsers {
+		if id == userId {
+			continue
+		}
+		up.NotifyMeOnline(userId)
+	}
+
+}
+
+func (u *UserProcess) NotifyMeOnline(userId int) {
+	var mes message.Message
+	mes.Type = message.NotifyUserStatusMesType
+	var notifyUserStatusMes message.NotifyUserStatusMes
+	notifyUserStatusMes.UserId = userId
+	notifyUserStatusMes.Status = message.UserOnline
+
+	data, err := json.Marshal(notifyUserStatusMes)
+	if err != nil {
+		fmt.Println("json.Marshal err=", err)
+		return
+	}
+	mes.Data = string(data)
+
+	data, err = json.Marshal(mes)
+	if err != nil {
+		fmt.Println("json.Marshal err=", err)
+		return
+	}
+	tf := &utils.Transfer{
+		Conn: u.Conn,
+	}
+	err = tf.WritePkg(data)
+	if err != nil {
+		fmt.Println("NotifyMeOnline err=", err)
+	}
+}
+

客户端对用户列表进行处理

+
package process
+
+import (
+	"Go-Projects/Mass-Communication-System/common/message"
+	"fmt"
+)
+
+// 客户端要维护的map
+var onlineUsers map[int]*message.User = make(map[int]*message.User, 10)
+
+// 在客户端显示当前在线的用户
+
+func outputOnlineUser() {
+	fmt.Println("当前在线用户列表")
+	for id, user := range onlineUsers {
+		fmt.Println(id, user)
+	}
+}
+
+// 处理返回的NotifyUserStatusMes
+func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
+
+	user, ok := onlineUsers[notifyUserStatusMes.UserId]
+	if !ok {
+		user = &message.User{
+			UserId: notifyUserStatusMes.UserId,
+		}
+	}
+	user.UserStatus = notifyUserStatusMes.Status
+	onlineUsers[notifyUserStatusMes.UserId] = user
+	outputOnlineUser()
+}
+
+

客户端显示用户列表

+
// 显示当前在线用户列表
+fmt.Println("当前在线用户列表如下:")
+for _, v := range loginResMes.UsersIds {
+	fmt.Println("用户id,\t", v)
+	user := &message.User{
+		UserId:     v,
+		UserStatus: message.UserOnline,
+	}
+	onlineUsers[v] = user
+}
+

客户端发送消息

+

直接调用前面写好的就行,代码很少了

+
package process
+
+import (
+	"Go-Projects/Mass-Communication-System/client/utils"
+	"Go-Projects/Mass-Communication-System/common/message"
+	"encoding/json"
+	"fmt"
+)
+
+type SmsProecss struct {
+}
+
+func (sp *SmsProecss) SendGroupSms(content string) (err error) {
+	var mes message.Message
+	mes.Type = message.SmsMesType
+
+	var smsMes message.SmsMes
+	smsMes.Content = content
+	smsMes.UserId = CurUser.UserId
+	smsMes.UserStatus = CurUser.UserStatus
+
+	data, err := json.Marshal(smsMes)
+	if err != nil {
+		fmt.Println("json.Marshal err=", err)
+		return
+	}
+	mes.Data = string(data)
+	data, err = json.Marshal(mes)
+	if err != nil {
+		fmt.Println("json.Marshal err=", err)
+		return
+	}
+	tf := &utils.Transfer{
+		Conn: CurUser.Conn,
+	}
+	err = tf.WritePkg(data)
+	if err != nil {
+		fmt.Println("tf.WritePkg err=", err)
+		return
+	}
+	return
+}
+
+

服务器端转发消息

+

也是和上面的差不多

+
package process2
+
+import (
+	"Go-Projects/Mass-Communication-System/common/message"
+	"Go-Projects/Mass-Communication-System/server/utils"
+	"encoding/json"
+	"fmt"
+	"net"
+)
+
+type SmsProecss struct {
+}
+
+func (sp *SmsProecss) SendGroupSms(mes *message.Message) (err error) {
+
+	var smsMes message.SmsMes
+	err = json.Unmarshal([]byte(mes.Data), &smsMes)
+	if err != nil {
+		fmt.Println("json.Unmarshal err=", err)
+		return
+	}
+
+	data, err := json.Marshal(mes)
+	if err != nil {
+		fmt.Println("json.Marshal err=", err)
+		return
+	}
+
+	for id, up := range userMgr.onlineUsers {
+		if id == smsMes.UserId {
+			continue
+		}
+		sp.SendMesToEachOnlineUser(data, up.Conn)
+	}
+	return
+}
+
+func (sp *SmsProecss) SendMesToEachOnlineUser(data []byte, conn net.Conn) (err error) {
+
+	tf := &utils.Transfer{
+		Conn: conn,
+	}
+	err = tf.WritePkg(data)
+	if err != nil {
+		fmt.Println("tf.WritePkg err=", err)
+		return
+	}
+	return
+}
+
+ + +
+ +
+
+ + + + + + +
+
+
Go项目-海量用户通讯系统
+
https://zhangzhao219.github.io/2022/11/11/Go/Go-Project-Mass-Communication-System/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年11月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/11/21/Go/Go-Project-Gee/index.html b/2022/11/21/Go/Go-Project-Gee/index.html new file mode 100644 index 000000000..df6118637 --- /dev/null +++ b/2022/11/21/Go/Go-Project-Gee/index.html @@ -0,0 +1,1577 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go项目-Gee Web框架 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go项目-Gee Web框架

+ + +
+ +

Go项目-Gee Web框架

+ +

完成的功能

+
    +
  • 简单介绍 net/http库以及 http.Handler接口
  • +
  • 路由(router)独立出来,方便之后增强。
  • +
  • 设计 上下文(Context),封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。
  • +
  • 使用 Trie 树实现动态路由(dynamic route)解析。
  • +
  • 实现路由分组控制(Route Group Control)
  • +
  • 设计并实现 Web 框架的中间件(Middlewares)机制。
  • +
  • 实现通用的 Logger中间件,能够记录请求到响应所花费的时间,
  • +
  • 实现静态资源服务(Static Resource)。
  • +
  • 支持HTML模板渲染。
  • +
  • 实现错误处理机制。
  • +
+

http.Handler

+

Go语言内置了 net/http库,封装了HTTP网络编程的基础的接口,使用这个库:

+
package main
+
+import (
+	"fmt"
+	"net/http"
+)
+
+func main() {
+	// 设置两个路由
+	http.HandleFunc("/", indexHandler)
+	http.HandleFunc("/hello", helloHandler)
+	// 启动Web服务,在9999端口进行监听,处理所有的HTTP请求的实例
+	http.ListenAndServe("localhost:9999", nil)
+	// 最后的nil即为实现框架的入口
+}
+
+// 根路由
+func indexHandler(w http.ResponseWriter, req *http.Request) {
+	fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
+}
+
+// hello路由
+func helloHandler(w http.ResponseWriter, req *http.Request) {
+	for k, v := range req.Header {
+		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
+	}
+}
+

使用curl进行测试:

+
> curl http://localhost:9999/
+URL.Path="/"
+> curl http://localhost:9999/hello
+Header["User-Agent"] = ["curl/7.68.0"]
+Header["Accept"] = ["*/*"]
+

其中代码的nil就是一个接口,需要实现方法 ServeHTTP ,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。

+

拦截一下请求进行尝试

+
package main
+
+import (
+	"fmt"
+	"net/http"
+)
+
+// 定义一个空结构体,因为后面实现的是一个方法,比如在一个结构体的基础上进行实现
+type Engine struct{}
+
+// 实现ServeHTTP方法
+func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	switch req.URL.Path {
+	case "/":
+		fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
+	case "/hello":
+		for k, v := range req.Header {
+			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
+		}
+	default:
+		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
+	}
+}
+
+func main() {
+	engine := &Engine{}
+	// 多设置一个路由
+	http.HandleFunc("/hi", indexHandler)
+	// 启动Web服务,在9999端口进行监听,处理所有的HTTP请求的实例
+	http.ListenAndServe("localhost:9999", engine)
+	// 最后的nil即为实现框架的入口
+}
+
+// 根路由
+func indexHandler(w http.ResponseWriter, req *http.Request) {
+	fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
+}
+
+

测试:

+
> curl http://localhost:9999/hello
+Header["User-Agent"] = ["curl/7.68.0"]
+Header["Accept"] = ["*/*"]
+> curl http://localhost:9999/
+URL.Path="/"
+> curl http://localhost:9999/hi
+404 NOT FOUND: /hi
+

因此就将所有的HTTP请求转向了自己的处理逻辑,代码的运行结果与之前的是一致的。

+

我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

+

因此就可以从这里入手完成这个Web框架,最终的代码结构是这样的

+
.
+├── gee
+│   └── gee.go
+└── main.go
+

main.go:

+

使用 New()创建 gee 的实例,使用 GET()方法添加路由,最后使用 Run()启动Web服务。

+
package main
+
+import (
+	"Go-Projects/Gee/gee"
+	"fmt"
+	"net/http"
+)
+
+func main() {
+	r := gee.New()
+	r.Get("/", func(w http.ResponseWriter, req *http.Request) {
+		fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
+	})
+	r.Get("/hello", func(w http.ResponseWriter, req *http.Request) {
+		for k, v := range req.Header {
+			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
+		}
+	})
+	r.Run("localhost:9999")
+}
+

gee.go

+
package gee
+
+import (
+	"fmt"
+	"net/http"
+)
+
+// 定义一个普遍使用的函数类型,避免后面再次定义
+type HandlerFunc func(http.ResponseWriter, *http.Request)
+
+// 定义路由表
+type Engine struct {
+	router map[string]HandlerFunc
+}
+
+// 工厂模式的构造方法,返回一个实例
+func New() *Engine {
+	return &Engine{
+		router: make(map[string]HandlerFunc),
+	}
+}
+
+// 将路由添加到路由表中
+func (engine *Engine) addRoute(method, pattern string, handler HandlerFunc) {
+	key := method + "-" + pattern
+	engine.router[key] = handler
+}
+
+// 实现GET方法
+func (engine *Engine) GET(pattern string, handler HandlerFunc) {
+	engine.addRoute("GET", pattern, handler)
+}
+
+// 实现POST方法
+func (engine *Engine) POST(pattern string, handler HandlerFunc) {
+	engine.addRoute("POST", pattern, handler)
+}
+
+// 实现Run方法
+func (engine *Engine) Run(addr string) (err error) {
+	return http.ListenAndServe(addr, engine)
+}
+
+// 完成统一的控制入口方法ServeHTTP
+func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	key := req.Method + "-" + req.URL.Path
+	if handler, ok := engine.router[key]; ok {
+		handler(w, req)
+	} else {
+		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
+	}
+}
+
+

测试后的效果和之前完全相同。

+

整个 Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。

+

上下文Context

+

最终调用的效果:

+
package main
+
+import (
+	"Go-Projects/Gee/gee"
+	"net/http"
+)
+
+func main() {
+	r := gee.New()
+	r.GET("/", func(c *gee.Context) {
+		c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
+	})
+	r.GET("/hello", func(c *gee.Context) {
+		c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
+	})
+	r.POST("/login", func(c *gee.Context) {
+		c.JSON(http.StatusOK, gee.H{
+			"username": c.PostForm("username"),
+			"password": c.PostForm("password"),
+		})
+	})
+	r.Run("localhost:9999")
+}
+
+
    +
  • Handler的参数变成成了 gee.Context,提供了查询Query/PostForm参数的功能。
  • +
  • gee.Context封装了 HTML/String/JSON函数,能够快速构造HTTP响应。
  • +
+
    +
  1. 对Web服务来说,无非是根据请求 *http.Request,构造响应 http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
  2. +
  3. 针对使用场景,封装 *http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由 /hello/:name,参数 :name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。
  4. +
+

context.go

+
package gee
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// 给map[string]interface{}起了一个别名gee.H,构建JSON数据时,显得更简洁。
+type H map[string]interface{}
+
+type Context struct {
+	// 原始的两个参数
+	Writer http.ResponseWriter
+	Req    *http.Request
+	// 请求信息
+	Path   string
+	Method string
+	// 响应信息
+	StatusCode int
+}
+
+// 创建一个Context实例
+func newContext(w http.ResponseWriter, req *http.Request) *Context {
+	return &Context{
+		Writer: w,
+		Req:    req,
+		Path:   req.URL.Path,
+		Method: req.Method,
+	}
+}
+
+// 根据key返回用户输入的value,属于POST方法的工具
+func (c *Context) PostForm(key string) string {
+	return c.Req.FormValue(key)
+}
+
+// 根据key返回用户输入的value,属于GET方法的工具
+func (c *Context) Query(key string) string {
+	return c.Req.URL.Query().Get(key)
+}
+
+// 写入状态码并更改Context的状态码
+func (c *Context) Status(code int) {
+	c.StatusCode = code
+	c.Writer.WriteHeader(code)
+}
+
+// 帮助下面的方法快速构造响应
+func (c *Context) SetHeader(key, value string) {
+	c.Writer.Header().Set(key, value)
+}
+
+// 构造字符串类型的响应
+func (c *Context) String(code int, format string, values ...interface{}) {
+	c.SetHeader("Content-Type", "text/plain")
+	c.Status(code)
+	c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
+}
+
+// 构造JSON类型的响应
+func (c *Context) JSON(code int, obj interface{}) {
+	c.SetHeader("Content-Type", "application/json")
+	c.Status(code)
+	encoder := json.NewEncoder(c.Writer) // 流数据构造json
+	if err := encoder.Encode(obj); err != nil {
+		http.Error(c.Writer, err.Error(), 500)
+	}
+}
+
+// 构造data类型的响应
+func (c *Context) Data(code int, data []byte) {
+	c.Status(code)
+	c.Writer.Write(data)
+}
+
+// 构造HTML类型的响应
+func (c *Context) HTML(code int, html string) {
+	c.SetHeader("Content-Type", "text/html")
+	c.Status(code)
+	c.Writer.Write([]byte(html))
+}
+
+

将和路由相关的方法和结构提取出来,放到了一个新的文件中 router.go,方便我下一次对 router 的功能进行增强,

+
package gee
+
+import (
+	"log"
+	"net/http"
+)
+
+type router struct {
+	handlers map[string]HandlerFunc
+}
+
+func newRouter() *router {
+	return &router{
+		handlers: make(map[string]HandlerFunc),
+	}
+}
+
+// 将路由添加到路由表中
+func (r *router) addRoute(method, pattern string, handler HandlerFunc) {
+	log.Printf("Route %4s - %s", method, pattern)
+	key := method + "-" + pattern
+	r.handlers[key] = handler
+}
+
+// 路由处理
+func (r *router) handle(c *Context) {
+	key := c.Method + "-" + c.Path
+	if handler, ok := r.handlers[key]; ok {
+		handler(c)
+	} else {
+		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
+	}
+}
+
+

调整主框架入口gee.go

+
package gee
+
+import (
+	"net/http"
+)
+
+// 定义一个普遍使用的函数类型,避免后面再次定义
+type HandlerFunc func(*Context)
+
+// 定义路由表
+type Engine struct {
+	router *router
+}
+
+// 工厂模式的构造方法,返回一个实例
+func New() *Engine {
+	return &Engine{
+		router: newRouter(),
+	}
+}
+
+// 实现GET方法
+func (engine *Engine) GET(pattern string, handler HandlerFunc) {
+	engine.router.addRoute("GET", pattern, handler)
+}
+
+// 实现POST方法
+func (engine *Engine) POST(pattern string, handler HandlerFunc) {
+	engine.router.addRoute("POST", pattern, handler)
+}
+
+// 实现Run方法
+func (engine *Engine) Run(addr string) (err error) {
+	return http.ListenAndServe(addr, engine)
+}
+
+// 完成统一的控制入口方法ServeHTTP
+func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	c := newContext(w, req)
+	engine.router.handle(c)
+}
+
+

测试代码

+

启动程序后:

+
2022/11/21 21:05:40 Route  GET - /
+2022/11/21 21:05:40 Route  GET - /hello
+2022/11/21 21:05:40 Route POST - /login
+
> curl -i http://localhost:9999/
+HTTP/1.1 200 OK
+Content-Type: text/html
+Date: Mon, 21 Nov 2022 13:05:47 GMT
+Content-Length: 19
+
+<h1>Hello Gee</h1>
+> curl "http://localhost:9999/hello?name=geektutu"
+hello geektutu, you're at /hello
+> curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'
+{"password":"1234","username":"geektutu"}
+> curl "http://localhost:9999/xxx"
+404 NOT FOUND: /xxx
+

前缀树路由

+

之前,我们用了一个非常简单的 map结构存储了路由表,使用 map存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。那如果我们想支持类似于 /hello/:name这样的动态路由怎么办呢?

+

实现动态路由最常用的数据结构,被称为前缀树(Trie树),每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。

+

首先设计树节点上应该存储哪些信息

+
type node struct {
+	pattern  string  // 待匹配路由,例如 /p/:lang
+	part     string  // 路由中的一部分,例如 :lang
+	children []*node // 子节点,例如 [doc, tutorial, intro]
+	isWild   bool    // 是否精确匹配,part 含有 : 或 * 时为true
+}
+
+

将匹配的逻辑,包装为一个辅助函数:

+
// 查找第一个匹配的节点,用于插入
+func (n *node) matchChild(part string) *node {
+	for _, child := range n.children {
+		if child.part == part || n.isWild {
+			return child
+		}
+	}
+	return nil
+}
+
+// 查找全部匹配的节点,用于查找
+func (n *node) matchChildren(part string) []*node {
+	nodes := make([]*node, 0)
+	for _, child := range n.children {
+		if child.part == part || n.isWild {
+			nodes = append(nodes, child)
+		}
+	}
+	return nodes
+}
+

实现节点的递归插入和查找

+
// 插入节点
+func (n *node) insert(pattern string, parts []string, height int) {
+	// 到达高度了就停止
+	if len(parts) == height {
+		n.pattern = pattern
+		return
+	}
+	part := parts[height]       // 获取当前的规则
+	child := n.matchChild(part) // 尝试用当前的规则进行匹配
+	// 如果没有匹配成功,就新建一个节点,并加入到当前节点的孩子们中去
+	if child == nil {
+		child = &node{
+			part:   part,
+			isWild: part[0] == ':' || part[0] == '*',
+		}
+		n.children = append(n.children, child)
+	}
+	// 递归进行插入
+	child.insert(pattern, parts, height+1)
+}
+
+// 查询节点
+func (n *node) search(parts []string, height int) *node {
+	if len(parts) == height || strings.HasPrefix(n.part, "*") {
+		if n.pattern == "" {
+			return nil
+		}
+		return n
+	}
+	part := parts[height]             // 获取当前的规则
+	children := n.matchChildren(part) // 尝试用当前的规则进行匹配
+	// 遍历所有当前匹配的节点进行递归匹配
+	for _, child := range children {
+		result := child.search(parts, height+1)
+		if result != nil {
+			return result
+		}
+	}
+	return nil
+}
+

使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中解析了 :*两种匹配符的参数,返回一个 map 。

+
// 将字符串解析成一个切片
+func parsePattern(pattern string) []string {
+	vs := strings.Split(pattern, "/")
+	parts := make([]string, 0)
+	for _, item := range vs {
+		if item != "" {
+			parts = append(parts, item)
+			if item[0] == '*' {
+				break
+			}
+		}
+	}
+	return parts
+}
+
+// 将路由添加到路由表中
+func (r *router) addRoute(method, pattern string, handler HandlerFunc) {
+	parts := parsePattern(pattern)
+	key := method + "-" + pattern
+	// 先看看是不是Get或者Post方法
+	_, ok := r.roots[method]
+	if !ok {
+		r.roots[method] = &node{}
+	}
+	r.roots[method].insert(pattern, parts, 0)
+	r.handlers[key] = handler
+}
+
+// 从路由表中查找路由
+func (r *router) getRoute(method, path string) (*node, map[string]string) {
+	searchParts := parsePattern(path)
+	params := make(map[string]string)
+	root, ok := r.roots[method]
+	if !ok {
+		return nil, nil
+	}
+	n := root.search(searchParts, 0)
+	if n != nil {
+		parts := parsePattern(n.pattern)
+		for index, part := range parts {
+			if part[0] == ':' {
+				params[part[1:]] = searchParts[index]
+			}
+			if part[0] == '*' && len(part) > 1 {
+				params[part[1:]] = strings.Join(searchParts[index:], "/")
+				break
+			}
+		}
+		return n, params
+	}
+	return nil, nil
+}
+

对 Context 对象增加一个属性和方法,来提供对路由参数的访问。我们将解析后的参数存储到 Params

+

更改路由处理的方法

+
// 路由处理
+func (r *router) handle(c *Context) {
+	n, params := r.getRoute(c.Method, c.Path)
+	if n != nil {
+		c.Params = params
+		key := c.Method + "-" + n.pattern
+		r.handlers[key](c)
+	} else {
+		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
+	}
+}
+

测试:

+
> curl "http://localhost:9999/hello/geektutu"
+hello geektutu, you're at /hello/geektutu
+> curl "http://localhost:9999/assets/css/geektutu.css"
+{"filepath":"css/geektutu.css"}
+

分组控制Group

+

真实的业务场景中,往往某一组路由需要相似的处理。例如:

+
    +
  • /post开头的路由匿名可访问。
  • +
  • /admin开头的路由需要鉴权。
  • +
  • /api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。
  • +
+

一个 Group 对象需要具备哪些属性呢?

+

首先是前缀(prefix),比如 /,或者 /api

+

要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;

+

中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。

+

还需要有访问 Router的能力

+
// 分组路由
+type RouterGroup struct {
+	prefix      string
+	middlewares []HandlerFunc
+	parent      *RouterGroup
+	engine      *Engine
+}
+

Engine作为最顶层的分组,也就是说 Engine拥有 RouterGroup所有的能力。

+
// 扩展Engine
+type Engine struct {
+	*RouterGroup
+	router *router
+	groups []*RouterGroup
+}
+

更改下面的其他Engine方法即可

+
// 工厂模式的构造方法,返回一个实例
+func New() *Engine {
+	engine := &Engine{
+		router: newRouter(),
+	}
+	engine.RouterGroup = &RouterGroup{
+		engine: engine,
+	}
+	engine.groups = []*RouterGroup{engine.RouterGroup}
+	return engine
+}
+

增加一个Group的方法,创建一个新的RouterGroup

+
// 创建一个新的RouterGroup
+func (group *RouterGroup) Group(prefix string) *RouterGroup {
+	engine := group.engine
+	newGroup := &RouterGroup{
+		prefix: group.prefix + prefix,
+		parent: group,
+		engine: engine,
+	}
+	engine.groups = append(engine.groups, newGroup)
+	return newGroup
+}
+

后面的Get方法和Post方法就都换成RouterGroup的方法就可以了

+

测试:

+
> curl "http://localhost:9999/v1/hello?name=geektutu"
+hello geektutu, you're at /v1/hello
+> curl "http://localhost:9999/v2/hello/geektutu"
+hello geektutu, you're at /v2/hello/geektutu
+> curl "http://localhost:9999/index"
+<h1>Index Page</h1>
+

中间件Middleware

+

中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2个比较关键的点:

+
    +
  • 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
  • +
  • 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。
  • +
+

Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是 Context对象。插入点是框架接收到请求初始化 Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对 Context进行二次加工。另外通过调用 (*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,c.Next()表示等待执行其他的中间件或用户的 Handler

+
package gee
+
+import (
+	"log"
+	"time"
+)
+
+func Logger() HandlerFunc {
+	return func(c *Context) {
+		t := time.Now()                                                            // 开始计时
+		c.Next()                                                                   // 等待用户自己的Handler处理结束
+		log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t)) // 打印时间
+	}
+}
+
+

中间件是应用在 RouterGroup上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。

+

Context添加了2个参数,定义了 Next方法:

+
func (c *Context) Next() {
+	c.index++
+	s := len(c.handlers)
+	for ; c.index < s; c.index++ {
+		c.handlers[c.index](c)
+	}
+}
+

index是记录当前执行到第几个中间件,当在中间件中调用 Next方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在 Next方法之后定义的部分。

+

定义 Use函数,将中间件应用到某个 Group 。

+
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
+	group.middlewares = append(group.middlewares, middlewares...)
+}
+

ServeHTTP 函数也有变化,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 c.handlers

+
// 完成统一的控制入口方法ServeHTTP
+func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	var middlewares []HandlerFunc
+	for _, group := range engine.groups {
+		if strings.HasPrefix(req.URL.Path, group.prefix) {
+			middlewares = append(middlewares, group.middlewares...)
+		}
+	}
+	c := newContext(w, req)
+	c.handlers = middlewares
+	engine.router.handle(c)
+}
+

handle 函数中,将从路由匹配得到的 Handler 添加到 c.handlers列表中,执行 c.Next()

+
// 路由处理
+func (r *router) handle(c *Context) {
+	n, params := r.getRoute(c.Method, c.Path)
+	if n != nil {
+		key := c.Method + "-" + n.pattern
+		c.Params = params
+		c.handlers = append(c.handlers, r.handlers[key])
+	} else {
+		c.handlers = append(c.handlers, func(c *Context) {
+			c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
+		})
+	}
+	c.Next()
+}
+

测试:

+
> go run Go-Projects/Gee
+2022/11/22 15:45:00 Route  GET - /
+2022/11/22 15:45:00 Route  GET - /v2/hello/:name
+>
+2022/11/22 15:45:11 [200] / in 3µs
+>
+2022/11/22 15:45:25 [500] /v2/hello/geektutu in 39.4µs for group v2
+2022/11/22 15:45:25 [500] /v2/hello/geektutu in 77.6µs
+
> curl http://localhost:9999/
+<h1>Hello Gee</h1>
+> curl http://localhost:9999/v2/hello/geektutu
+{"message":"Internal Server Error"}
+

模板(HTML Template)

+

Web 框架如何支持服务端渲染的场景

+

解析请求的地址,映射到服务器上文件的真实地址:

+
// 解析请求的地址,映射到服务器上文件的真实地址
+func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
+	absolutePath := path.Join(group.prefix, relativePath)
+	fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
+	return func(c *Context) {
+		file := c.Param("filepath")
+		// Check if file exists and/or if we have permission to access it
+		if _, err := fs.Open(file); err != nil {
+			c.Status(http.StatusNotFound)
+			return
+		}
+
+		fileServer.ServeHTTP(c.Writer, c.Req)
+	}
+}
+
+func (group *RouterGroup) Static(relativePath string, root string) {
+	handler := group.createStaticHandler(relativePath, http.Dir(root))
+	urlPattern := path.Join(relativePath, "/*filepath")
+	// Register GET handlers
+	group.GET(urlPattern, handler)
+}
+

HTML 模板渲染

+
// 扩展Engine
+type Engine struct {
+	*RouterGroup
+	router        *router
+	groups        []*RouterGroup
+	htmlTemplates *template.Template // 模板渲染
+	funcMap       template.FuncMap   // 模板渲染
+}
+
+func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
+	engine.funcMap = funcMap
+}
+
+func (engine *Engine) LoadHTMLGlob(pattern string) {
+	engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(pattern))
+}
+

对原来的 (*Context).HTML()方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。

+
// 构造HTML类型的响应
+func (c *Context) HTML(code int, name string, data interface{}) {
+	c.SetHeader("Content-Type", "text/html")
+	c.Status(code)
+	if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
+		c.Fail(500, err.Error())
+	}
+}
+

进行测试:

+
package main
+
+import (
+	"Go-Projects/Gee/gee"
+	"fmt"
+	"html/template"
+	"net/http"
+	"time"
+)
+
+type student struct {
+	Name string
+	Age  int8
+}
+
+func FormatAsDate(t time.Time) string {
+	year, month, day := t.Date()
+	return fmt.Sprintf("%d-%02d-%02d", year, month, day)
+}
+
+func main() {
+	r := gee.New()
+	r.Use(gee.Logger())
+	r.SetFuncMap(template.FuncMap{
+		"FormatAsDate": FormatAsDate,
+	})
+	r.LoadHTMLGlob("Gee/templates/*")
+	r.Static("/assets", "./static")
+
+	stu1 := &student{Name: "Geektutu", Age: 20}
+	stu2 := &student{Name: "Jack", Age: 22}
+	r.GET("/", func(c *gee.Context) {
+		c.HTML(http.StatusOK, "css.tmpl", nil)
+	})
+	r.GET("/students", func(c *gee.Context) {
+		c.HTML(http.StatusOK, "arr.tmpl", gee.H{
+			"title":  "gee",
+			"stuArr": [2]*student{stu1, stu2},
+		})
+	})
+
+	r.GET("/date", func(c *gee.Context) {
+		c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{
+			"title": "gee",
+			"now":   time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC),
+		})
+	})
+
+	r.Run("localhost:9999")
+}
+
+

错误恢复(Panic Recover)

+

错误处理也可以作为一个中间件,增强 gee 框架的能力

+
package gee
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"runtime"
+	"strings"
+)
+
+// print stack trace for debug
+func trace(message string) string {
+	var pcs [32]uintptr
+	n := runtime.Callers(3, pcs[:]) // skip first 3 caller
+
+	var str strings.Builder
+	str.WriteString(message + "\nTraceback:")
+	for _, pc := range pcs[:n] {
+		fn := runtime.FuncForPC(pc)
+		file, line := fn.FileLine(pc)
+		str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))
+	}
+	return str.String()
+}
+
+func Recovery() HandlerFunc {
+	return func(c *Context) {
+		defer func() {
+			if err := recover(); err != nil {
+				message := fmt.Sprintf("%s", err)
+				log.Printf("%s\n\n", trace(message))
+				c.Fail(http.StatusInternalServerError, "Internal Server Error")
+			}
+		}()
+
+		c.Next()
+	}
+}
+
+ + +
+ +
+
+ + + + + + +
+
+
Go项目-Gee Web框架
+
https://zhangzhao219.github.io/2022/11/21/Go/Go-Project-Gee/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年11月21日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/11/24/UCAS/advanced-ai/advanced-ai-13/index.html b/2022/11/24/UCAS/advanced-ai/advanced-ai-13/index.html new file mode 100644 index 000000000..81d320952 --- /dev/null +++ b/2022/11/24/UCAS/advanced-ai/advanced-ai-13/index.html @@ -0,0 +1,988 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第13讲 群体智能 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第13讲 群体智能

+ + +
+ +

《高级人工智能》课程笔记:第13讲 群体智能

+ +

第13讲 群体智能

+

群体智能

+
    +
  • 群体智能指的是无智能或者仅具有相对简单智能的主体通过合作涌现出更高智能行为的特性 +
      +
    • 其中的个体并非绝对的无智能或只具有简单智能,而是相对于群体表现出来的智能而言是简单的。
    • +
    +
  • +
  • 单个复杂个体可以实现的功能,同样可以由大量简单的个体通过群体合作实现,后者的优势在于它更健壮、灵活和经济。
  • +
  • 群体智能利用群体优势,在没有中心控制的条件下,寻找解决复杂问题的新思路
  • +
+

集群智能:众多无智能的个体,通过相互之间的简单合作所表现出来的智能行为

+

博弈:具备一定智能的理性个体,按照某种机制行动,在群体层面体现出的智能

+

众包:设计合适的机制,激励个体参与,从而实现单个个体不具备的社会智能

+

集群智能

+

分布式 、 自组织的(自然/人造)系统表现出的一种群体智能

+

集群智能系统一般由一群简单的智能体构成,智能体按照简单的规则彼此进行局部交互,智能体也可以环境交互

+

灵感通常来自生物系统(蚁群、鸟群、兽群、粒子群)

+

特点:

+
    +
  • 分布式:无中心控制
  • +
  • 随机性:非确定性
  • +
  • 自适应:个体根据环境进行策略调整
  • +
  • 正反馈:个体好的尝试会对个体产生正反馈
  • +
  • 自发涌现:会在群体层面涌现出一种智能
  • +
+

蚁群优化算法

+

一种解空间搜索方法,适用于在图上寻找最优路径

+

算法形式化:

+
    +
  • 每个蚂蚁对应一个计算智能体
  • +
  • 蚂蚁依概率选择候选位置进行移动
  • +
  • 在经过的路径上留下“信息素”
  • +
  • “信息素”随时间挥发
  • +
  • “信息素”浓度大的路径在后续的选择中会以更高的概率被选取
  • +
+

TSP问题蚁群算法流程

+

蚁群大小:一般情况下,蚁群中的蚂蚁个数不超过TSP图中节点的个数

+

终止条件:

+
    +
  • 设定迭代轮数
  • +
  • 设定最优解连续保持不变的迭代轮数
  • +
+

思想:局部随机搜索+自增强

+

缺点:

+
    +
  • 收敛速度慢
  • +
  • 易于陷入局部最优
  • +
  • 对于解空间为连续的优化问题不适用
  • +
+

粒子群优化算法

+
    +
  • 粒子群优化算法是一种基于种群寻优的启发式搜索算法 。在 1995年由Kennedy和Eberhart首先提出来的。
  • +
  • 它的主要启发来源于对鸟群群体运动行为的研究。我们经常可以观察到鸟群表现出来的同步性,虽然每只鸟的运动行为都是互相独立的,但是在整个鸟群的飞行过程中却表现出了高度一致性的复杂行为,并且可以自适应的调整飞行的状态和轨迹。
  • +
  • 鸟群具有这样的复杂飞行行为的原因,可能是因为每只鸟在飞行过程中都遵循了一定的行为规则,并能够掌握邻域内其它鸟的飞行信息。
  • +
  • 粒子群优化算法借鉴了这样的思想,每个粒子代表待求解问题搜索解空间中的一个潜在解,它相当于一只鸟,“飞行信息”包括粒子当前的位置和速度两个状态量。
  • +
  • 每个粒子都可以获得其邻域内其它个体的信息,对所经过的位置进行评价,并根据这些信息和位置速度更新规则,改变自身的两个状态量,在“飞行”过程中传递信息和互相学习,去更好地适应环境。
  • +
  • 随着这一过程的不断进行,粒子群最终能够找到问题的近似最优解。
  • +
+

是一种随机优化方法,通过粒子群在解空间中进行搜索,寻找最优解(适应度最大的解)

+

构成要素

+
    +
  • 粒子群: +
      +
    • 每个粒子对应所求解问题的一个可行解
    • +
    • 粒子通过其位置和速度表示 +
        +
      • 粒子在第轮的位置:
      • +
      • 粒子在第轮的速度:
      • +
      +
    • +
    +
  • +
  • 记录: +
      +
    • :粒子的历史最好位置
    • +
    • :全局历史最好位置
    • +
    +
  • +
  • 计算适应度的函数-适应度:
  • +
+

算法过程描述

+
    +
  • 初始化 +
      +
    • 初始化粒子群:每个粒子的位置和速度,即
    • +
    • +
    +
  • +
  • 循环执行如下三步直至满足结束条件 +
      +
    • 计算每个粒子的适应度:
    • +
    • 更新每个粒子历史最好适应度及其相应的位置,更新当前全局最好适应度及其相应的位置
    • +
    • 更新每个粒子的速度和位置 +
        +
      • +
      • +
      +
    • +
    +
  • +
+

粒子速度更新公式:

+
    +
  1. 惯性项:保持原速度不变的倾向
  2. +
  3. 记忆项:回到历史最好位置的倾向
  4. +
  5. 社会项:走向粒子群全局最好位置的倾向
  6. +
  7. 权重参数:一般取值为2
  8. +
  9. 随机参数:0和1之间的随机数
  10. +
+

算法终止条件:

+
    +
  • 迭代的轮数
  • +
  • 最佳位置连续未更新的轮数
  • +
  • 适应度函数的值到达预期要求
  • +
+

速度更新参数:又称加速度参数,用来控制粒子当前最优位置和粒子群当前最优位置对粒子飞行速度的影响

+
    +
  • :每个微粒执行局部搜索;
  • +
  • :微粒群转化为一个随机爬山法
  • +
  • :微粒逐渐移向的加权均值
  • +
  • :算法比较适合于单峰优化问题
  • +
  • :算法比较适合于多峰优化问题
  • +
+

惯性权重:速度冲量导致微粒按照先前速度方向继续移动。提出一个惯性权重来控制先前微粒速度的影响

+

粒子群优化算法和遗传算法相比

+
    +
  • 遗传算法强调“适者生存”,不好的个体在竞争中被淘汰; PSO 强调“协同合作”,不好的个体通过学习向好的方向转变。
  • +
  • 遗传算法中最好的个体通过产生更多的后代来传播基因;PSO 中的最好个体通过吸引其它个体向它靠近来施加影响。
  • +
  • 遗传算法的选择概率只与上一代群体相关,而与历史无关,群体的信息变化过程是一个Markov链过程;而PSO中的个体除了有位置和速度外,还有着过去的历史信息(pBest, gBest)。
  • +
+

优点:

+
    +
  • 易于实现
  • +
  • 可调参数较少
  • +
  • 所需种群或微粒群规模较小
  • +
  • 计算效率高,收敛速度快
  • +
+

缺点:和其它演化计算算法类似,不保证收敛到全局最优解

+

粒子群优化算法代码

+
import numpy as np
+
+def cal(x):
+    return x*x*x-5*x*x-2*x+3
+
+x_min = -2
+x_max = 5
+
+p_num = 1000
+
+g_best_max = 1
+g_best_max_i = 0
+
+g_best_min = 1
+g_best_min_i = 0
+
+x_MAX = (x_max - x_min) * np.random.random_sample((p_num,)) + x_min
+v_MAX = (x_max - x_min) * np.random.random_sample((p_num,)) + x_min
+
+x_MIN = (x_max - x_min) * np.random.random_sample((p_num,)) + x_min
+v_MIN = (x_max - x_min) * np.random.random_sample((p_num,)) + x_min
+
+p_best_max = np.ones_like(x_MAX)
+p_best_min = np.ones_like(x_MAX)
+
+for i in range(1,10000):
+
+    f_max = cal(x_MAX)
+    f_min = cal(x_MIN)
+
+    t_max = cal(p_best_max)
+    t_min = cal(p_best_min)
+
+    p_best_max = np.where(t_max > f_max, p_best_max, x_MAX)
+    p_best_min = np.where(t_min < f_min, p_best_min, x_MIN)
+
+
+    if np.max(f_max) > g_best_max:
+        g_best_max = np.max(f_max)
+        g_best_max_i = x_MAX[np.argmax(f_max)]
+
+    if np.min(f_min) < g_best_min:
+        g_best_min = np.min(f_min)
+        g_best_min_i = x_MIN[np.argmin(f_min)]
+
+    v_MAX = v_MAX + (p_best_max - x_MAX) + (g_best_max - x_MAX)
+    x_MAX = x_MAX + v_MAX
+
+    x_MAX = np.where(x_MAX > x_max,x_max,x_MAX)
+    x_MAX = np.where(x_MAX < x_min,x_min,x_MAX)
+
+    v_MIN = v_MIN + (p_best_min - x_MIN) + (g_best_min - x_MIN)
+    x_MIN = x_MIN + v_MIN
+
+    x_MIN = np.where(x_MIN > x_max,x_max,x_MIN)
+    x_MIN = np.where(x_MIN < x_min,x_min,x_MIN)
+
+print(g_best_max_i)
+print(g_best_min_i)
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第13讲 群体智能
+
https://zhangzhao219.github.io/2022/11/24/UCAS/advanced-ai/advanced-ai-13/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年11月24日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/01/UCAS/advanced-ai/advanced-ai-14/index.html b/2022/12/01/UCAS/advanced-ai/advanced-ai-14/index.html new file mode 100644 index 000000000..7a825b1ac --- /dev/null +++ b/2022/12/01/UCAS/advanced-ai/advanced-ai-14/index.html @@ -0,0 +1,985 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-第14讲 强化学习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-第14讲 强化学习

+ + +
+ +

《高级人工智能》课程笔记:第14讲 强化学习

+ +

第14讲 强化学习

+

强化学习

+

目标:学习从环境状态到行为的映射(即策略),智能体选择能够获得环境最大奖赏的行为,使得外部环境对学习系统在某种意义下的评价为最佳。

+

区别于监督学习:监督学习是从标注中学习;强化学习是从交互中学习

+

两种反馈

+

评价性反馈

+
    +
  • 当智能体采取某个行为时,对该行为给出一个评价,但并不知道哪个行为是最好的
  • +
  • 强化学习经常面临的是评价性反馈
  • +
+

指导性反馈

+
    +
  • 直接给出某个状态下的正确或最好行为
  • +
  • 独立于智能体当前采取的行为
  • +
  • 监督学习使用的是指导性反馈
  • +
+

强化学习的两大特性

+

试错搜索和延迟奖励,用于判断某一问题是否适用于强化学习求解。

+

强化学习需要应对的挑战

+

利用和探索之间的矛盾

+

强化学习的要素

+

主体:智能体和环境-状态、行为和奖励

+

要素:

+
    +
  • 策略:状态到行为的映射,包括确定策略和随机策略两种
  • +
  • 奖励:关于状态和行为的函数,通常具有一定的不确定性
  • +
  • 价值:累积奖励或长期目标
  • +
  • 环境模型:刻画环境对行为的反馈
  • +
+

强化学习发展历程

+
    +
  • 1911年,Thorndike 提出效果律(Law of effect),从心理学的角度探讨了强化思想:动物感到舒服的行为会被强化,动物感到不舒服的行为会被弱化
  • +
  • 1954年,马文 · 明斯基(Marvin Minsky)在其博士论文中实现了计算上的试错学习
  • +
  • 1957年,Bellman提出求解最优控制问题的动态规划方法,并提出了最优控制问题的随机离散版本,即著名的马尔科夫决策过程
  • +
  • 1960年,Howard提出马尔科夫决策过程的策略迭代方法
  • +
  • 1961年,明斯基在其论文“Steps toward artificial intelligence”中首次使用“Reinforcement learning”一词
  • +
  • 1989年,Watkins提出了Q-learning,将动态规划、时序差分、蒙特卡洛模拟三条线结合在了一起
  • +
  • 1992年,Tesauro 将强化学习成功应用到西洋双陆棋
  • +
  • 2015年,强化学习和深度学习结合: AlphaGo
  • +
  • 2017年,AlphaGo Zero
  • +
+

多臂赌博机

+

一台赌博机有多个摇臂 ,每个摇臂摇出的奖励(reward)大小不确定 ,玩家希望摇固定次数的臂所获得的期望累积奖励最大

+

问题形式化

+

行为:摇哪个臂

+

奖励:每次摇臂获得的奖金

+

表示第轮的行为,表示第轮获得的奖励

+

轮采取行为的期望奖励为:

+

假如摇臂次, 那么按照什么策略摇臂,才能使期望累积奖励最大呢?

+

已知时, 每次都选择最大的(贪心策略)

+

但是一般情况下,对于玩家而言是未知的或具有不确定性,玩家在第轮时只能依赖于当时对的估值进行选择,此时,贪心策略是在第轮 选择最大的

+

利用和探索

+

利用:

+
    +
  • 按照贪心策略进行选择,即选择最大的行为
  • +
  • 优点:最大化即时奖励
  • +
  • 缺点:由于只是对的估计,估计的不确定性导致按照贪心策略选择的行为不一定是最大的行为
  • +
+

探索:

+
    +
  • 选择贪心策略之外的行为
  • +
  • 缺点:短期奖励会比较低
  • +
  • 优点:长期奖励会比较高 ,通过探索可以找出奖励更大的行为,供后续选择
  • +
+

每步选择在“利用”和“探索”中二选一

+

如何平衡“利用”和“探索” 是关键

+

贪心策略形式化地表示为:,当有多个行为的同时为最大时,随机选择一个

+

贪心策略:

+
    +
  • 以概率按照贪心策略进行行为选择(Exploitation)
  • +
  • 以概率在所有行为中随机选择一个(Exploration)
  • +
  • 的取值取决于的方差,方差越大取值应越大
  • +
+

行为估值方法

+

根据历史观测样本的均值对进行估计

+

约定:

+
    +
  • 当分母等于0时,
  • +
  • 当分母趋于无穷大时,收敛到
  • +
+

行为估值时,一个行为被选择了次后的估值记为,该估值方式需要记录个奖励值

+

乐观初值法

+

行为的初始估值

+
    +
  • 前述贪心策略中,每个行为的初始估值为0
  • +
  • 每个行为的初始估值可以帮助我们引入先验知识
  • +
  • 初始估值还可以帮助我们平衡exploitation 和 exploration
  • +
+

乐观初值法:Optimistic Initial Values

+
    +
  • 为每个行为赋一个高的初始估值
  • +
  • 好处:初期每个行为都有较大机会被explore
  • +
+

小结

+
    +
  • 多臂赌博机是强化学习的一个简化场景,行为和状态之间没有关联关系
  • +
  • 扩展情形 +
      +
    • 有上下文的多臂赌博机 +
        +
      • 存在多个多臂赌博机,状态表示赌博机
      • +
      • 学习状态到行为的映射
      • +
      • 但行为不改变状态
      • +
      +
    • +
    +
  • +
  • 更一般的情形 +
      +
    • 马尔科夫决策过程
    • +
    +
  • +
+

马尔科夫决策过程

+
    +
  • 常用于建模序列化决策过程
  • +
  • 行为不仅获得即时奖励,还能改变状态,从而影响长期奖励
  • +
  • 学习状态到行为的映射-策略 +
      +
    • 多臂赌博机学习
    • +
    • MDP学习
    • +
    +
  • +
+

奖励设置

+
    +
  • 设置奖励是希望智能体能达到我们期望的目标 +
      +
    • 下围棋 +
        +
      • 目标:赢棋
      • +
      • 奖励需要是能够实现赢棋这一目标才合适 +
          +
        • 吃子多少?占领棋盘的中心?
        • +
        +
      • +
      +
    • +
    • 迷宫 +
        +
      • 目标:尽快走出去
      • +
      • 奖励:每走一步,奖励为-1(相当于惩罚)
      • +
      +
    • +
    • 垃圾回收机器人 +
        +
      • 目标:在尽可能少的人工干预的情况下回收尽可能多的垃圾
      • +
      • 奖励:回收一个垃圾奖励+1 (等待和主动寻找获得奖励的概率不同),人工干预一次奖励-3
      • +
      +
    • +
    +
  • +
+

贝尔曼方程的作用

+

贝尔曼方程定义了状态估值函数的依赖关系

+
    +
  • 给定策略下,每个状态的估值视为一个变量
  • +
  • 所有状态(假如有个)的估值根据贝尔曼方程形成了一个具有个方程和个变量的线性方程组
  • +
  • 求解该方程组即可得到该策略下每个状态的估值
  • +
+

寻找最优策略

+
    +
  • 基于状态估值函数的贝尔曼最优性方程 +
      +
    • 第一步:求解状态估值函数的贝尔曼最优性方程得到最优策略对应的状态估值函数
    • +
    • 第二步:根据状态估值函数的贝尔曼最优性方程,进行一步搜索找到每个状态下的最优行为 +
        +
      • 注意:最优策略可以存在多个
      • +
      • 贝尔曼最优性方程的优势,可以采用贪心局部搜索即可得到全局最优解
      • +
      +
    • +
    +
  • +
  • 基于行为估值函数的贝尔曼最优性方程 +
      +
    • 直接得到最优策略
    • +
    +
  • +
+

寻找最优策略小结

+

求解贝尔曼最优性方程寻找最优策略的局限性

+
    +
  • 需要知道环境模型
  • +
  • 需要高昂的计算代价和内存(存放估值函数)
  • +
  • 依赖于马尔科夫性
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-第14讲 强化学习
+
https://zhangzhao219.github.io/2022/12/01/UCAS/advanced-ai/advanced-ai-14/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月1日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/02/Interview/Interview-Questions-algorithm/index.html b/2022/12/02/Interview/Interview-Questions-algorithm/index.html new file mode 100644 index 000000000..c46b31ac4 --- /dev/null +++ b/2022/12/02/Interview/Interview-Questions-algorithm/index.html @@ -0,0 +1,918 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 算法面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

算法面试题准备

+ + +
+ +

算法面试题准备

+ +

排序

+
void mergesort(vector<int> & nums, int start, int end){
+    if(start >= end){
+        return;
+    }
+    int mid = (end - start) / 2 + start;
+    mergesort(nums, start, mid);
+    mergesort(nums, mid+1, end);
+    vector<int> temp;
+    int l1start = start;
+    int l1end = mid;
+    int l2start = mid + 1;
+    int l2end = end;
+    while(l1start <= l1end || l2start <= l2end){
+        if(l1start > l1end){
+            temp.push_back(nums[l2start]);
+            l2start += 1;
+        } else if(l2start > l2end){
+            temp.push_back(nums[l1start]);
+            l1start += 1;
+        } else{
+            if(nums[l1start] < nums[l2start]){
+                temp.push_back(nums[l1start]);
+                l1start += 1;
+            } else{
+                temp.push_back(nums[l2start]);
+                l2start += 1;
+            }
+        }
+    }
+    for(int i=0;i<temp.size();i++){
+        nums[start+i] = temp[i];
+    }
+}
+
+void quicksort(vector<int> &nums, int start, int end){
+    if(start >= end){
+        return;
+    }
+    int x = nums[start];
+    int left = start;
+    int right = end;
+    while(left < right){
+        while(left < right && nums[right] >= x){
+            right--;
+        }
+        nums[left] = nums[right];
+        while(left < right && nums[left] < x){
+            left++;
+        }
+        nums[right] = nums[left];
+    }
+    nums[left] = x;
+    quicksort(nums, start, left-1);
+    quicksort(nums, left+1, end);
+}
+

计算某个数字的平方根和立方根

+
double binarysearch(double x, double threshold){
+    double left = 0;
+    double right = x+1;
+    while(right - left >= threshold){
+        double mid = (left + right) / 2.0;
+        if(mid * mid < x){
+            left = mid;
+        } else{
+            right = mid;
+        }
+    }
+    return left;
+}
+
+double gdtosqrt(double x, double threshold){
+    // y^2 = x -> x - y^2 = 0
+    // min (x - y^2)^2
+    // x^2 - 2 * x * y^2 + y^4
+    // 对y求导
+    // -4*x * y + 4 * y ^ 3 
+
+    double now = x;
+    double pre = x + threshold*2;
+    double lr = 0.00001;
+    while(abs(now-pre) >= threshold){
+        pre = now;
+        now = now - lr * (-4 * x * now + 4 * now * now * now);
+    }
+    return now;
+}
+
+double gdtocube(double x, double threshold){
+    // y^3 = x -> x - y^3 = 0
+    // min (x - y^3)^2
+    // x^2 - 2 * x * y^3 + y^6
+    // 对y求导
+    // -6*x * y^2 + 6 * y ^ 5
+
+    double now = x;
+    double pre = x + threshold*2;
+    double lr = 0.00001;
+    while(abs(now-pre) >= threshold){
+        pre = now;
+        now = now - lr * (-6 * x * now * now + 6 * now * now * now * now * now);
+    }
+  
+    return now;
+}
+
+int main(){
+    double a = 1;
+    double threshold = 0.00000000001;
+    double res = binarysearch(a, threshold);
+    cout << res << endl;
+    double res2 = gdtosqrt(a, threshold);
+    cout << res2 << endl;
+    double res3 = gdtocube(a, threshold);
+    cout << res3 << endl;
+    return 0;
+

跑不通调整学习率,大数学习率要小,小数学习率要大

+

MultiHeadAttention

+
class MultiHeadAttention(nn.Module):
+    def __init__(self, hidden_size, num_heads):
+        super(MultiHeadAttention, self).__init__()
+        self.num_heads = num_heads
+        self.head_dim = hidden_size // num_heads
+
+        self.q_linear = nn.Linear(hidden_size, hidden_size)
+        self.k_linear = nn.Linear(hidden_size, hidden_size)
+        self.v_linear = nn.Linear(hidden_size, hidden_size)
+
+        self.o_linear = nn.Linear(hidden_size, hidden_size)
+  
+    def forward(self, hidden_state, attention_mask=None):
+        batch_size = hidden_state.size()[0]
+
+        query = self.q_linear(hidden_state)
+        key = self.k_linear(hidden_state)
+        value = self.v_linear = nn.Linear(hidden_state)
+
+        query = self.split_head(query)
+        key = self.split_head(key)
+        value = self.split_head(value)
+
+        attention_scores = torch.matmul(query, key.transpose(-1,-2)) / torch.sqrt(torch.tensor(self.head_dim))
+
+        if attention_mask != None:
+            attention_scores += attention_mask * -1e9
+      
+        attention_probs = torch.softmax(attention_scores, dim=-1)
+
+        output = torch.matmul(attention_probs, value)
+
+        output = output.transpose(-1,-2).contiguous().view(batch_size, -1, self.head_dim * self.num_heads)
+
+        output = self.o_linear(output)
+
+        return output
+
+    def split_head(self, x):
+        batch_size = x.size()[0]
+        return x.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1,2)
+

Softmax和CrossEntropy

+
import numpy as np
+import torch
+import torch.nn as nn
+
+nn.Softmax()
+
+def softmax(logits):
+    max_logits = np.max(logits)
+    exp_logits = np.exp(logits - max_logits)
+    return exp_logits / np.sum(exp_logits)
+
+print(softmax([1,2,3,4,5,6]))
+
+class Our_CrossEntropy(torch.nn.Module):
+
+    def __init__(self):
+        super(Our_CrossEntropy,self).__init__()
+  
+    def forward(self, x ,y):
+        P_i = torch.nn.functional.softmax(x, dim=1)
+        y = torch.nn.functional.one_hot(y)
+        loss = y*torch.log(P_i + 0.0000001)
+        loss = -torch.mean(torch.sum(loss, dim=1),dim = 0)
+        return loss
+
+ + +
+ +
+
+ + + + + + +
+
+
算法面试题准备
+
https://zhangzhao219.github.io/2022/12/02/Interview/Interview-Questions-algorithm/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/02/Interview/Interview-Questions-go/index.html b/2022/12/02/Interview/Interview-Questions-go/index.html new file mode 100644 index 000000000..63cfc3369 --- /dev/null +++ b/2022/12/02/Interview/Interview-Questions-go/index.html @@ -0,0 +1,905 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go面试题准备

+ + +
+ +

Go面试题准备

+ +

使用Go的好处

+
    +
  1. 针对并发进行了优化,并且在规模上运行良好
  2. +
  3. 具有单一的标准代码格式与自带的代码格式化工具,比其他语言更具可读性。
  4. +
  5. 垃圾收集比其他语言更有效。
  6. +
+

make和new关键字

+

make 关键字的主要作用是创建 slice、map 和 Channel 等内置的数据结构

+

new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针

+

Go的GC机制

+

垃圾回收算法

+

最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),Go 语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。

+

标记清除的执行过程可以分成标记和清除两个阶段:

+
    +
  • 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象;
  • +
  • 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表。
  • +
+

标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束之后,用户程序才可以继续执行。为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法。

+

三色标记法

+

三色标记算法将程序中的对象分成白色、黑色和灰色三类。

+
    +
  • 白色:不确定对象。
  • +
  • 灰色:存活对象,子对象待处理。
  • +
  • 黑色:存活对象。
  • +
+

标记开始时,所有对象加入白色集合(这一步需 STW)。首先将根对象标记为灰色,加入灰色集合,垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。重复这个过程,直到灰色集合为空为止

+

标记阶段结束。那么白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。

+

问题

+

三色标记法因为多了一个白色的状态来存放不确定对象,所以后续的标记阶段可以并发地执行。当然并发执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。所以三色标记法是一个 false negative(假阴性)的算法。

+

三色标记法并发执行仍存在一个问题,此时用户从已经被标记成黑色的对象新建了引用指向了白色对象,白色不可达对象将被收集

+

为了解决这个问题,Go 使用了内存屏障技术,当对象新增或更新时,会将其着色为灰色。这样即使与用户程序并发执行,对象的引用发生改变时,垃圾收集器也能正确处理。

+

总结

+

一次完整的 GC 分为四个阶段:

+
    +
  • 1)标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier)
  • +
  • 2)使用三色标记法标记(Marking, 并发)
  • +
  • 3)标记结束(Mark Termination,需 STW),关闭写屏障。
  • +
  • 4)清理(Sweeping, 并发)
  • +
+

GMP模型(实习-百度-Go后端开发-2023.02.09)

+

进程:进程是系统进行资源分配的基本单位,有独立的内存空间。

+

线程:线程是CPU调度的基本单位,线程依附于进程存在,每个线程会共享父进程的资源。

+

协程: 协程是一种用户态的轻量级线程, 协程的调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核的开销。

+

Goroutine 非常轻量

+
    +
  • 上下文切换代价小: Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器的刷新;
  • +
  • 内存占用少: 线程栈空间通常是 2M,Goroutine 栈空间最小 2K;
  • +
  • 创建和销毁小:goroutine的创建和销毁是由运行环境(runtime)完成的
  • +
+

Goroutine的并发编程模型基于GMP模型:

+

G: 表示goroutine,每个goroutine都有自己的栈空间,定时器,初始化的栈空间在2k左右,空间会随着需求增长。

+

M: 内核线程,每个m对象对应一个内核线程,p对象需要挂载到m上才能运行,每个m都有一个g0协程负责进行调度

+

P: 代表调度器,表示执行 Go 代码所需的资源,负责调度goroutine,维护一个本地goroutine队列,M从P上获得goroutine并执行。

+
    +
  • 全局队列(Global Queue):存放等待运行的 G。
  • +
  • P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
  • +
  • P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
  • +
+

线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

+

调度器的设计策略

+

复用线程:避免频繁的创建、销毁线程,而是对线程的复用。

+

1)work stealing 机制

+

当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,或者从全局队列中获取G。

+

2)hand off 机制

+
    +
  • 由于channel、原子、互斥量操作调用导致的阻塞:调度器将把当前阻塞的goroutine切换出去,重新调度其它goroutine
  • +
  • 由于网络请求和IO操作导致goroutine阻塞:Go提供了网络轮询器(NetPoller)来处理网络请求和IO操作的问题,后台通过epoll(Linux)实现IO多路复用,G被移到了NetPoller上进行异步的网络系统调用,M可以执行P中其它的goroutine,网络系统调用完成后,G被移回到了P的本地队列中
  • +
  • 调用一些系统方法时发生阻塞:goroutine导致M阻塞,此时调度器将M和P分离,分离后的P和新的M绑定,继续调度全局队列中的goroutine,阻塞的系统调用完成后,可以移回本地队列并再次由P执行
  • +
  • goroutine中执行sleep操作导致M被阻塞:go程序后台有一个监控线程sysmon,它监控哪些长时间运行的G任务,然后设置可以抢占的标识符,别的goroutine可以抢先进来执行
  • +
+

多个P同时去全局队列中请求Goroutine(实习-百度-Go后端开发-2023.02.09)

+

为了保证多个P之间任务的平衡,所有M共享P全局队列,为保证数据竞争问题,需要加锁处理

+

锁相关

+

基本概念

+

Go的代码库中为开发人员提供了一下两种锁:

+
    +
  1. 互斥锁 sync.Mutex
  2. +
  3. 读写锁 sync.RWMutex
  4. +
+

第一个互斥锁指的是在Go编程中,同一资源的锁定对各个协程是相互排斥的,当其中一个协程获取到该锁时,其它协程只能等待,直到这个获取锁的协程释放锁之后,其它的协程才能获取。

+

第二个读写锁依赖于互斥锁的实现,这个指的是当多个协程对某一个资源都是只读操作,那么多个协程可以获取该资源的读锁,并且互相不影响,但当有协程要修改该资源时就必须获取写锁,如果获取写锁时,已经有其它协程获取了读写或者写锁,那么此次获取失败,也就是说读写互斥,读读共享,写写互斥。

+

乐观锁与悲观锁

+

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

+

乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

+

悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

+

实现方式

+

乐观锁的实现方式主要有两种:CAS机制和版本号机制

+

如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。

+

许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

+

atomic 包可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作,这个包应用的便是乐观锁的原理。

+

版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。

+
    +
  • 当某个线程查询数据时,将该数据的版本号一起查出来;
  • +
  • 当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
  • +
+

Golang中的sync包,提供了各种锁,如果使用了这个包,基本上就以悲观锁的工作模式了。

+

CAS的缺点

+
    +
  1. ABA问题:已经更改过但是无法检测出来
  2. +
  3. 高竞争下的开销问题 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。
  4. +
  5. CAS只能保证单个变量(或者说单个内存值)操作的原子性
  6. +
+

乐观锁与悲观锁对比

+

乐观锁没有加锁和解除锁的步骤,直觉上会快一些;但是乐观锁这么做的前提是总认为不会发生并发,如果并发发生的概率很大,重试的次数会增加,这种情况下乐观锁的性能就差很多了。

+

悲观锁有加锁和解除锁的步骤,直觉上会慢一些;但是当有很多进程或者线程对同一个数值进行修改时,能避免大量的重试过程,这种情况下悲观锁的性能相对就很高了。

+
    +
  • 功能限制:与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
  • +
+

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。

+

再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

+
    +
  • 竞争激烈程度:如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
  • +
+

当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。

+

当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

+

Channel

+

channel 是一个通道,用于端到端的数据传输,这有点像我们平常使用的消息队列,只不过 channel 的发送方和接受方是 goroutine 对象,属于内存级别的通信。

+

无缓冲的 channel,一旦有 goroutine 往 channel 发送数据,那么当前的 goroutine 会被阻塞住,直到有其他的 goroutine 消费了 channel 里的数据,才能继续运行。

+

有缓冲的 channel,第二个参数表示 channel 可缓冲数据的容量。只要当前 channel 里的元素总数不大于这个可缓冲容量,则当前的 goroutine 就不会被阻塞住。

+

底层

+

channel 创建后返回了 hchan 结构体,包括:

+
qcount   uint   // channel 里的元素计数
+ dataqsiz uint   // 可以缓冲的数量,如 ch := make(chan int, 10)。 此处的 10 即 dataqsiz
+ elemsize uint16 // 要发送或接收的数据类型大小
+ buf      unsafe.Pointer // 当 channel 设置了缓冲数量时,该 buf 指向一个存储缓冲数据的区域,该区域是一个循环队列的数据结构
+ closed   uint32 // 关闭状态
+ sendx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已发送数据的索引位置
+ recvx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已接收数据的索引位置
+ recvq    waitq // 想读取数据但又被阻塞住的 goroutine 队列
+ sendq    waitq // 想发送数据但又被阻塞住的 goroutine 队列
+
+ lock mutex
+ ...
+

channel 先写再读

+

G1往channel写数据,G1 暂时被挂在 sendq 队列里,然后 G1 调用了 gopark 休眠了起来

+

G2从channel读数据,发现 sendq 等待队列里有 goroutine 存在,于是直接从 G1 copy 数据过来,并且会对 G1 设置 goready 函数,这样下次调度发生时, G1 就可以继续运行,并且会从等待队列里移除掉。

+

channel 先读再写

+

G1 暂时被挂在了 recvq 队列,然后休眠起来。

+

G2 在写数据时,发现 recvq 队列有 goroutine 存在,于是直接将数据发送给 G1。同时设置 G1 goready 函数,等待下次调度运行。

+

channel和mutex的选择

+

面对一个并发问题的时候,应当选择合适的并发方式:channel还是mutex。 选择的依据是他们的能力/特性:channel的能力是让数据流动起来,擅长的是数据流动的场景:

+
    +
  1. 传递数据的所有权,即把某个数据发送给其他协程
  2. +
  3. 分发任务,每个任务都是一个数据
  4. +
  5. 交流异步结果,结果是一个数据
  6. +
+

mutex的能力是数据不动,某段时间只给一个协程访问数据的权限擅长数据位置固定的场景:

+
    +
  1. 缓存
  2. +
  3. 状态,我们银行例子中的 map就是一种状态
  4. +
+

Go 内存逃逸,栈和堆是否都进行垃圾回收(实习-百度-Go后端开发-2023.02.09)

+

内存逃逸:在一段程序中,每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等,这些内存会由编译器在栈中进行分配,每一个函数都会分配一个栈桢,在函数运行结束后进行销毁,但是有些变量我们想在函数运行结束后仍然使用它,那么就需要把这个变量在堆上分配,这种从"栈"上逃逸到"堆"上的现象就成为内存逃逸。

+

Go语言中堆内存的分配与释放完全不需要我们去管了,Go语言引入了 GC机制,GC机制会对位于堆上的对象进行自动管理,当某个对象不可达时(即没有其对象引用它时),他将会被回收并被重用。

+

为了减少 GC造成的压力,Go语言引入了逃逸分析,也就是想法设法尽量减少在堆上的内存分配,可以在栈中分配的变量尽量留在栈中。

+

逃逸分析就是指程序在编译阶段根据代码中的数据流,对代码中哪些变量需要在栈中分配,哪些变量需要在堆上分配进行静态分析的方法。堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。所以逃逸分析更做到更好内存分配,提高程序的运行速度。

+

逃逸分析原理:

+
    +
  • 指向栈对象的指针不能存储在堆中
  • +
  • 指向栈对象的指针不能超过该对象的存活期,也就说指针不能在栈对象被销毁后依旧存活。(例子:声明的函数返回并销毁了对象的栈帧,或者它在循环迭代中被重复用于逻辑上不同的变量)
  • +
+

对逃逸做一个总结:

+
    +
  • 逃逸分析在编译阶段确定哪些变量可以分配在栈中,哪些变量分配在堆上
  • +
  • 逃逸分析减轻了 GC压力,提高程序的运行速度
  • +
  • 栈上内存使用完毕不需要 GC处理,堆上内存使用完毕会交给 GC处理
  • +
  • 函数传参时对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能
  • +
  • 根据代码具体分析,尽量减少逃逸代码,减轻 GC压力,提高性能
  • +
+ + +
+ +
+
+ + + + + + +
+
+
Go面试题准备
+
https://zhangzhao219.github.io/2022/12/02/Interview/Interview-Questions-go/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/02/Interview/Interview-Questions-mysql/index.html b/2022/12/02/Interview/Interview-Questions-mysql/index.html new file mode 100644 index 000000000..c65bc7a01 --- /dev/null +++ b/2022/12/02/Interview/Interview-Questions-mysql/index.html @@ -0,0 +1,883 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MySQL面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MySQL面试题准备

+ + +
+ +

MySQL面试题准备

+ +

Mysql语句执行流程(实习-百度-Go后端开发-2023.02.09)

+

img

+
    +
  • 建立连接,查询缓存(如果缓存开启)
  • +
  • 解析器根据这条 SQL 语句的语法、词法进行检查,如果没有错误的话会按关键词拆分成一个个节点,最终形成一棵解析树
  • +
  • 预处理器会检查 SQL 语句的语义,检查 SQL 语句是否有歧义、字段等是否存在,形成一棵新的解析树
  • +
  • 查询优化器拿到这个解析树生成的各种执行计划,经过逻辑查询优化、物理查询优化(索引等)后得到一个开销最小的执行计划
  • +
  • 执行引擎拿到这份执行计划调用存储引擎的接口
  • +
  • 存储引擎根据执行计划进行数据查询,查询会查询调用操作系统中文件系统的一些接口,完成数据查询,最后返回给客户端
  • +
+

Mysql的索引底层实现(实习-MetaAPP-Go后端开发-2023.02.13)(实习-SmartX-Go后端开发-2023.02.14)

+

MySQL 常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引

+

B+Tree 索引类型是 MySQL 存储引擎采用最多的索引类型。

+
    +
  • B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。
  • +
  • 对于有 N 个叶子节点的 B+Tree,其搜索复杂度为 O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN)
  • +
  • Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。但是 Hash 表不适合做范围查询,它更适合做等值的查询
  • +
+

聚簇索引和非聚簇索引(实习-MetaAPP-Go后端开发-2023.02.13)

+
    +
  • 聚簇索引 ,又叫主键索引,每个表只有一个主键索引,叶子节点保存主键的值和数据
  • +
  • 非聚簇索引 ,又叫辅助索引,叶子节点保存索引字段的值和主键的值
  • +
+

前缀索引和覆盖索引

+
    +
  1. 前缀索引:对于列的值较长,比如BLOB、TEXT、VARCHAR,就必须建立前缀索引,即将值的前一部分作为索引。这样既可以节约空间,又可以提高查询效率。但无法使用前缀索引做 ORDER BY 和 GROUP BY,也无法使用前缀索引做覆盖扫描。
  2. +
  3. 覆盖索引:select的数据列从索引中就能获得,不必再从数据表中读取。如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫 做覆盖索引。当发起一个被索引覆盖的查询(也叫作索引覆盖查询)时,在EXPLAIN的Extra列可以看到“Using index”的信息
  4. +
+

索引的缺点

+
    +
  • 需要占用物理空间,数量越大,占用空间越大;
  • +
  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
  • +
  • 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
  • +
+

对于多列索引,哪些情况下能用到索引,哪些情况用不到索引

+
    +
  • like以%开头
  • +
  • or查询,必须左右字段都是索引,否则索引失效
  • +
  • 联合索引,遵从最左匹配原则,如果不是使用第一列索引,索引失效
  • +
  • 数据出现隐形转换,如varchar字段没加单引号,自动转为int类型,会使索引失效
  • +
  • 索引字段使用not、<>、!=,索引失效
  • +
  • 索引字段使用函数,索引无效
  • +
+

Mysql的ACID

+
    +
  • 原子性(Atomicity) :一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
  • +
  • 一致性(Consistency) :是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
  • +
  • 隔离性(Isolation) :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
  • +
  • 持久性(Durability) :事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
  • +
+

InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

+
    +
  • 持久性是通过 redo log (重做日志)来保证的;
  • +
  • 原子性是通过 undo log(回滚日志) 来保证的;
  • +
  • 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
  • +
  • 一致性则是通过持久性+原子性+隔离性来保证;
  • +
+

binlog是二进制文件,记录了对数据库执行更改的所有操作,不包括 select、show,因为这两个操作没有对数据本身做修改。但是若操作了数据,但是数据没有发生变化,也会记录到 binlog。常用来数据恢复,数据备份。

+

MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

+

并行事务的问题

+

脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。

+

不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。

+

幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。

+

四种隔离级别

+
    +
  • 读未提交:指一个事务还没提交时,它做的变更就能被其他事务看到;
  • +
  • 读提交:指一个事务提交之后,它做的变更才能被其他事务看到;
  • +
  • 可重复读:指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的, MySQL InnoDB 引擎的默认隔离级别 ;
  • +
  • 串行化:会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
  • +
+

Mysql 在可重复读的隔离级别下基本不会有幻读的情况,但是在特殊的情况下也可能会有。

+

范式(2022.12.26 小红书 Golang开发实习生)

+

第一范式:列不可再分:每一列属性都是不可再分的属性值,确保每一列的原子性

+

第二范式:数据库表中的每个实例或行必须可以被惟一地区分,属性完全依赖于主键

+

第三范式:数据不能存在传递关系,即每个属性都跟主键有直接关系而不是间接关系。

+

范式化的优缺点

+

优点:

+
    +
  • 减少数据冗余
  • +
  • 表中重复数据较少,更新操作比较快
  • +
  • 范式化的表通常比反范式化的表小
  • +
+

缺点:

+
    +
  • 在查询的时候通常需要很多的关联,降低性能
  • +
  • 增加了索引优化的难度
  • +
+

消息队列(实习-MetaAPP-Go后端开发-2023.02.13)(实习-SmartX-Go后端开发-2023.02.14)

+

解耦:一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的

+

异步:将数据放到消息队列中,直接对用户返回响应,后续再等待慢慢写入

+

削峰:从消息队列到数据库的流速固定,不受外界突然的高请求数量的干扰

+

缺点:

+

系统可用性降低:系统引入的外部依赖越多,越容易挂掉。

+

系统复杂度提高

+

一致性问题:不知道后续对数据库的操作是否完成

+

Innodb 和 MyISAM 的区别

+
    +
  1. Innodb 支持事务。MyISAM 不支持
  2. +
  3. Innodb 支持外键。MyISAM 不支持
  4. +
  5. Innodb 主键索引的叶子节点是数据文件,辅助索引的叶子节点是主键的值。MyISAM 的主键索引和辅助索引,叶子节点都是数据文件的指针
  6. +
  7. Innodb 不保存表的行数,执行 select count(*) from tb需要全表扫描。MyISAM 用一个变量保存了整个表的行数,执行上述语句只需要读取该变量,速度很快
  8. +
  9. Innodb 所有的表在磁盘上保存在一个文件中。MyISAM 存储成三个文件。
  10. +
  11. Innodb 需要更多的内存和存储。MyISAM 可被压缩,存储空间较小。
  12. +
  13. Innodb 移植方案拷贝文件、备份 binlog,或者用 mysqldump,移植较困难。MyISAM 数据以文件形式存储,在备份和回复时可以单独针对表进行操作
  14. +
  15. Innodb 支持行锁、表锁。MyISAM 支持表锁
  16. +
  17. Innodb 在5.7版本之前不支持全文索引。MyISAM 支持全文索引
  18. +
+

慢查询

+
    +
  1. 没有设置索引,或查询没有用到索引
  2. +
  3. I/O吞吐量过小
  4. +
  5. 内存不足
  6. +
  7. 网络速度慢
  8. +
  9. 查询的数据量过大
  10. +
  11. 锁或者死锁
  12. +
  13. 返回了不必要的行或列
  14. +
  15. 查询语句存在问题,需要优化
  16. +
+

解决方法:

+
    +
  1. 把数据、日志、索引放到不同的I/O设备上,增加读取速度
  2. +
  3. 纵向、横向分割表,减少表的尺寸
  4. +
  5. 升级硬件
  6. +
  7. 根据查询条件,建立索引,索引优化
  8. +
  9. 提高网速
  10. +
  11. 扩大服务器内存
  12. +
  13. 分库分表
  14. +
+

SQL语句优化

+
    +
  1. 避免使用select *:多查出来的数据增加数据传输的时间;不会走覆盖索引,会出现大量的回表操作
  2. +
  3. 能用union all的时候,尽量不用union(去重耽误时间)
  4. +
  5. 用小表的数据集驱动大表的数据集
  6. +
  7. 批量插入数据,只需要远程请求一次数据库
  8. +
  9. 多用limit
  10. +
  11. in中的值不要太多
  12. +
  13. 增量查询
  14. +
  15. 对查询接口做分页处理(先找到上次分页最大的id,然后利用id上的索引查询)
  16. +
  17. 用连接查询代替子查询
  18. +
  19. join表的数量不应该过多,join要注意顺序,尽量使用inner join
  20. +
  21. 控制索引数量
  22. +
  23. 选择合理的索引数量
  24. +
  25. 在group by前面先用where缩小数据量
  26. +
  27. 索引优化
  28. +
+ + +
+ +
+
+ + + + + + +
+
+
MySQL面试题准备
+
https://zhangzhao219.github.io/2022/12/02/Interview/Interview-Questions-mysql/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/02/Interview/Interview-Questions-network/index.html b/2022/12/02/Interview/Interview-Questions-network/index.html new file mode 100644 index 000000000..047fd9246 --- /dev/null +++ b/2022/12/02/Interview/Interview-Questions-network/index.html @@ -0,0 +1,959 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 计算机网络面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

计算机网络面试题准备

+ + +
+ +

计算机网络面试题准备

+ +

输入URL到返回页面的全过程(2022.12.26 小红书 Golang开发实习生)

+

基础版本

+

浏览器根据请求的 URL 交给 DNS 域名解析,找到真实 IP ,向服务器发起请求;

+

服务器交给后台处理完成后返回数据,浏览器接收⽂件( HTML、JS、CSS 、图像等);

+

浏览器对加载到的资源( HTML、JS、CSS 等)进行语法解析,建立相应的内部数据结构 (如 HTML 的 DOM);

+

载⼊解析到的资源⽂件,渲染页面,完成。

+

详细版

+
    +
  • 在浏览器地址栏输⼊URL
  • +
  • 浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤 +
      +
    • 如果资源未缓存,发起新请求
    • +
    • 如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进⾏验证。
    • +
    • 检验新鲜通常有两个HTTP头进⾏控制 Expires 和 Cache-Control: +
        +
      • HTTP1.0提供 Expires,值为⼀个绝对时间表示缓存新鲜日期
      • +
      • HTTP1.1增加了Cache-Control: max-age=time,值为以秒为单位的最大新鲜时间
      • +
      +
    • +
    +
  • +
  • 浏览器解析URL获取协议,主机,端口,path
  • +
  • 浏览器组装⼀个HTTP(GET)请求报⽂
  • +
  • 浏览器获取主机 ip 地址,过程如下: +
      +
    • 浏览器缓存
    • +
    • 本机缓存
    • +
    • hosts文件
    • +
    • 路由器缓存
    • +
    • ISP DNS缓存
    • +
    • DNS递归查询(可能存在负载均衡导致每次IP不⼀样)
    • +
    +
  • +
  • 打开⼀个socket与⽬标IP地址,端口建立TCP链接,三次握手如下: +
      +
    • 客户端发送⼀个TCP的SYN=1,Seq=X的包到服务器端口
    • +
    • 服务器发回SYN=1, ACK=X+1, Seq=Y的响应包
    • +
    • 客户端发送ACK=Y+1, Seq=Z
    • +
    +
  • +
  • TCP链接建立后发送HTTP请求
  • +
  • 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使⽤HTTP Host头部判断请求的服务程序
  • +
  • 服务器检查HTTP请求头是否包含缓存验证信息,如果验证缓存新鲜,返回304等对应状态码
  • +
  • 处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作
  • +
  • 服务器将响应报⽂通过TCP连接发送回浏览器
  • +
  • 浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重⽤,关闭TCP连接的四次握⼿如下: +
      +
    • 主动方发送Fin=1, Ack=Z, Seq= X报文
    • +
    • 被动方发送ACK=X+1, Seq=Z报文
    • +
    • 被动方发送Fin=1, ACK=X, Seq=Y报文
    • +
    • 主动方发送ACK=Y, Seq=X报文
    • +
    +
  • +
  • 浏览器检查响应状态吗:是否为1XX,3XX, 4XX, 5XX,这些情况处理与2XX不同
  • +
  • 如果资源可缓存,进行缓存
  • +
  • 对响应进行解码(例如gzip压缩)
  • +
  • 根据资源类型决定如何处理(假设资源为HTML文档)
  • +
  • 解析HTML⽂档,构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序
  • +
  • 显示页面(HTML解析过程中会逐步显示页面)
  • +
+

详细简版

+
    +
  • 从浏览器接收 url 到开启网络请求线程(这⼀部分可以展开浏览器的机制以及进程与线程之间的关系)
  • +
  • 开启网络线程到发出⼀个完整的 HTTP 请求(这⼀部分涉及到dns查询, TCP/IP 请求,五层因特网协议栈等知识)
  • +
  • 从服务器接收到请求到对应后台接收到请求(这⼀部分可能涉及到负载均衡,安全拦截以及后台内部的处理等等)
  • +
  • 后台和前台的 HTTP 交互(这⼀部分包括 HTTP 头部、响应码、报文结构、 cookie 等知 识,可以提下静态资源的 cookie 优化,以及编码解码,如 gzip 压缩等)
  • +
  • 单独拎出来的缓存问题, HTTP 的缓存(这部分包括http缓存头部, ETag , catchcontrol 等)
  • +
  • 浏览器接收到 HTTP 数据包后的解析流程(解析 html、 词法分析然后解析成 dom 树、解析 css ⽣成 css 规则树、合并成 render 树,然后 layout 、 painting 渲染、复合图层的合成、 GPU 绘制、外链资源的处理、 loaded 和 DOMContentLoaded 等)
  • +
  • CSS 的可视化格式模型(元素的渲染规则,如包含块,控制框, BFC , IFC 等概念)
  • +
  • JS 引擎解析过程( JS 的解释阶段,预处理阶段,执⾏阶段生成执行上下文, VO ,作用域链、回收机制等等)
  • +
  • 其它(可以拓展不同的知识模块,如跨域,web安全, hybrid 模式等等内容)
  • +
+

计算机网络分层

+

OSI七层模型从上到下依次为:

+
    +
  • 应用层:为应用程序提供网络服务;
  • +
  • 表示层:数据格式转换、数据压缩和数据加密;
  • +
  • 会话层:建立、断开和维护通信链接;
  • +
  • 传输层:为上层协议提供端到端的可靠传输;
  • +
  • 网络层:寻址和路由;
  • +
  • 数据链路层:定义通过通信媒介互连的设备之间传输的规范;
  • +
  • 物理层:利用物理传输介质为数据链路层提供物理连接。
  • +
+

TCP/IP:应用层、传输层、网络层、数据链路层

+

TCP

+

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

+
    +
  • 面向连接 :一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
  • +
  • 可靠的 :无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
  • +
  • 字节流 :用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
  • +
+

TCP和UDP的区别如下:

+
    +
  • TCP是面向有连接型,UDP是面向无连接型;
  • +
  • TCP是一对一传输,UDP支持一对一、一对多、多对一和多对多的交互通信;
  • +
  • TCP是面向字节流的,即把应用层传来的报文看成字节流,将字节流拆分成大小不等的数据块,并添加TCP首部;UDP是面向报文的,对应用层传下来的报文不拆分也不合并,仅添加UDP首部;
  • +
  • TCP支持传输可靠性的多种措施,包括保证包的传输顺序、重发机制、流量控制和拥塞控制;UDP仅提供最基本的数据传输能力。
  • +
+

TCP对应的典型的应用层协议:

+
    +
  • FTP:文件传输协议;
  • +
  • SSH:远程登录协议;
  • +
  • HTTP:web服务器传输超文本到本地浏览器的超文本传输协议。
  • +
+

UDP对应的典型的应用层协议:

+
    +
  • DNS:域名解析协议;
  • +
  • TFTP:简单文件传输协议;
  • +
  • SNMP:简单网络管理协议。
  • +
+

TCP三次握手(2022.12.26 小红书 Golang开发实习生)

+

img

+

TCP四次挥手(2022.12.26 小红书 Golang开发实习生)

+

img

+

在四次挥手中的TIME_WAIT状态等待2*MSL时间

+

客户端在发送完给服务端的回执报文后没有立刻进入CLOSED状态,而是进入TIME-WAIT状态,然后等待2*MSL(最长报文段寿命)的时间后才进入CLOSED状态,原因有以下两点:

+
    +
  • 客户端发送给服务端回执后,有可能这个回执报文在传输途中丢失等原因,服务端并没有收到,此时服务端会再次向客户端发送FIN=1的断开请求报文,如果客户端没有等待2*MSL时间而直接进入了CLOSED状态,客户端就会收不到服务端再次发送的断开连接的请求报文,导致服务端无法进入CLOSED状态;
  • +
  • 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
  • +
+

TCP其他相关

+

ARQ协议

+

ARQ协议,即自动重传请求(Automatic Repeat-reQuest),意思是如果发送方在发送后一段时间之内没有收到确认回执,它通常会重新发送。ARQ协议包括停止等待ARQ协议和连续ARQ协议。

+

(1)停止等待ARQ协议

+

停止等待ARQ协议是指,在停止等待中如果接收端没有收到发送端发来的分组,接收端就不会给发送端发送确认回执,此时发送端会重新发送之前的报文分组。发送端会维护一个超时计时器,超时时间会设置的比数据在传输往返过程的时间要长一些。

+

(2)连续ARQ协议

+

连续ARQ协议是指,发送端维护一个“窗口”,“窗口”内可以有多个分组,窗口的大小就是窗口中分组的个数,凡是位于“窗口”内的分组可以连续发送出去而不必等待接收端返回的确认回执,对按序到达的最后一个分组,接收端会向发送端发送确认回执,如果有分组没有正确到达,会返回最后一个正确达到的分组序号,该序号后面的分组会重新发送给接收端。

+

在连续ARQ协议中,发送端会维护一块发送端的数据缓存,“窗口”里的分组都会在这个缓存中,当需要重新发送“窗口”中的分组报文时,便会从缓存里读取分组并发送。

+

连续 ARQ 协议可提高信道利用率。

+

TCP的流量控制

+

流量控制是为了控制发送端发送数据的速率,保证接收端能将本应接收的所有报文分组接收成功,否则会触发自动重传机制造成网络流量的浪费。

+

流量控制的具体操作是:接收端会通知发送端自己能接收的数据大小,于是发送端会发送不超过这个数据量的数据,这个大小被称为“窗口”的大小,在TCP首部中专门有一个字段表示“窗口”的大小,该值越大代表网络的吞吐量越高。

+

TCP的拥塞控制

+

计算机网络都处在一个共享的环境,在通信开始时如果立即把大量数据注入到网络,可能会引起网络阻塞,甚至带来网络瘫痪。TCP为了防止该问题的出现,采用了拥塞控制的策略

+

常见的拥塞控制策略有慢启动、拥塞避免、快重传与快恢复

+
    +
  • TCP 在刚建立连接完成后,首先是有个慢启动的过程,当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1
  • +
  • 当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法:每当收到一个 ACK 时,cwnd 增加 1/cwnd,变成了线性增长
  • +
  • 当网络出现拥塞,也就是会发生数据包重传,cwnd = cwnd/2 ,也就是设置为原来的一半; ssthresh = cwnd
  • +
  • 快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕 +
      +
    • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
    • +
    • 重传丢失的数据包;
    • +
    • 如果再收到重复的 ACK,那么 cwnd 增加 1;
    • +
    • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
    • +
    +
  • +
+

TCP粘包

+

如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况。

+
    +
  1. TCP 是基于字节流的,虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块,但是 TCP 把这些数据块仅仅看成一连串无结构的字节流,没有边界;
  2. +
  3. 从 TCP 的帧结构也可以看出,在 TCP 的首部没有表示数据长度的字段。
  4. +
+

基于上面两点,在使用 TCP 传输数据时,才有粘包或者拆包现象发生的可能。一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。

+

粘包如何产生的

+
    +
  1. 发送方产生粘包
  2. +
+

采用 TCP 协议传输数据的客户端与服务器经常是保持一个长连接的状态(一次连接发一次数据不存在粘包),双方在连接不断开的情况下,可以一直传输数据。但当发送的数据包过于的小时,那么 TCP 协议默认的会启用 Nagle 算法,将这些较小的数据包进行合并发送(缓冲区数据发送是一个堆压的过程);这个合并过程就是在发送缓冲区中进行的,也就是说数据发送出来它已经是粘包的状态了。

+

一句话:要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。

+
    +
  1. 接收方产生粘包
  2. +
+

接收方采用 TCP 协议接收数据时的过程是这样的:数据到接收方,从网络模型的下方传递至传输层,传输层的 TCP 协议处理是将其放置接收缓冲区,然后由应用层来主动获取(C 语言用 recv、read 等函数);这时会出现一个问题,就是我们在程序中调用的读取数据函数不能及时的把缓冲区中的数据拿出来,而下一个数据又到来并有一部分放入的缓冲区末尾,等我们读取数据时就是一个粘包(放数据的速度 > 应用层拿数据速度)。

+

一句话:接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

+

如何避免粘包

+
    +
  • 在每个包的末尾加上特殊字符,用以区分连续的两个包;
  • +
  • 在报文首部添加包的长度。
  • +
+

DNS

+

浏览器输入地址,然后浏览器这个进程去调操作系统某个库里的gethostbyname函数(例如,Linux GNU glibc标准库的gethostbyname函数),然后呢这个函数通过网卡给DNS服务器发UDP请求,接收结果,然后将结果给返回给浏览器。

+

为什么域名解析用UDP协议?

+

UDP的DNS协议只要一个请求、一个应答就好了。UDP协议传输内容不能超过512字节。不过客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。

+

为什么区域传送用TCP协议?

+

因为TCP协议可靠性好啊!你要从主DNS上复制内容啊,你用不可靠的UDP? TCP协议传输的内容大

+

分级查询

+
    +
  1. 先在本机的DNS里头查,如果有就直接返回了。
  2. +
  3. 本机DNS里头发现没有,就去根服务器里查。根服务器发现这个域名是属于 com域,因此根域DNS服务器会返回它所管理的 com域中的DNS 服务器的IP地址,意思是“虽然我不知道你要查的那个域名的地址,但你可以去 com域问问看”
  4. +
  5. 本机的DNS接到又会向 com域的DNS服务器发送查询消息。com 域中也没有 www.tmall.com这个域名的信息,和刚才一样,com域服务器会返回它下面的 tmall.com域的DNS服务器的IP地址。
  6. +
+

HTTP

+

HTTP 与 HTTPS 有哪些区别?

+
    +
  • HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
  • +
  • HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
  • +
  • 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
  • +
  • HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
  • +
+

为什么说http协议是无状态协议?怎么解决Http协议无状态协议?

+

http协议是一种无状态协议,协议自身不对请求和响应之间的通信状态进行保存,即对发送过来的请求和响应都不做持久化处理,把http协议设计的如此简单是为了更快地处理大量事务。

+

为了解决http协议不能保存通信状态的问题,引入了Cookie状态管理。Cookie技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。Cookie会根据从服务端发送的响应报文的一个叫Set-Cookie的首部字段,通知客户端保存Cookie。当下次客户端再往该服务端发送请求时,客户端会自动在请求报文中加入Cookie值发送出去,服务端发现客户端发来的Cookie后,会检查是哪一个客户端发来的连接请求,对比服务器上的记录,最后得到之前的状态信息。

+

常见的http动词有哪些?

+
    +
  • GET: 从服务器获取资源
  • +
  • POST: 在服务器新建资源(每一次都会封装报文)
  • +
  • PUT: 在服务器更新资源(幂等)
  • +
  • DELETE: 在服务器删除资源
  • +
  • HEAD: 获取资源的元数据
  • +
  • OPTIONAL: 查询对指定的资源支持的方法
  • +
+

常见的http返回码

+
    +
  • 200:请求被正常处理
  • +
  • 204:请求被受理但没有资源可以返回
  • +
  • 206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
  • +
  • 301:永久性重定向
  • +
  • 302:临时重定向
  • +
  • 303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
  • +
  • 304:发送附带条件的请求时,条件不满足时返回,与重定向无关
  • +
  • 307:临时重定向,与302类似,只是强制要求使用POST方法
  • +
  • 400:请求报文语法有误,服务器无法识别
  • +
  • 401:请求需要认证
  • +
  • 403:请求的对应资源禁止被访问
  • +
  • 404:服务器无法找到对应资源
  • +
  • 500:服务器内部错误
  • +
  • 503:服务器正忙
  • +
+ + +
+ +
+
+ + + + + + +
+
+
计算机网络面试题准备
+
https://zhangzhao219.github.io/2022/12/02/Interview/Interview-Questions-network/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/02/Interview/Interview-Questions-project/index.html b/2022/12/02/Interview/Interview-Questions-project/index.html new file mode 100644 index 000000000..2f3e3aeff --- /dev/null +++ b/2022/12/02/Interview/Interview-Questions-project/index.html @@ -0,0 +1,1882 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 面试项目准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

面试项目准备

+ + +
+ +

面试项目准备

+ +

自我介绍

+

您好,我是张兆,就读于中国科学院计算技术研究所,是预计25年夏季毕业的硕士研究生。我本科来自于中南大学计算机学院,是通过推荐免试的方式进入到中科院计算所就读研究生的。我在本科期间获得了国家奖学金和省级优秀毕业生的荣誉称号,同时拿过一些程序设计竞赛、数学建模和英语竞赛的奖项。

+
    +
  • 本科期间我在中南大学可视化实验室参与无线电信号的视觉摘要可视化设计这个项目,获得了一篇软著和一篇专利,并参与发表了可视化顶会的论文。
  • +
  • 保研后到现在我先后尝试了四份实习工作,分别是在商汤科技、MetaApp、微软STCA和蚂蚁集团,其中商汤科技是作为算法研究实习生进行篮球项目的算法研究工作,MetaApp是一家游戏公司,它与Meta没有任何关系,我在这里加入了推荐和广告部门做了一些高并发广告服务的维护和开发。微软STCA是在做Bing News的新闻分类与推荐工作。在蚂蚁集团是作为蚂蚁星计划的候选人,在隐语团队做一些垂直领域大模型相关的工作。
  • +
  • 我在实验室的研究方向是立场检测,我尝试着将立场检测与大模型进行结合,充分利用大模型的zero-shot能力产生知识,并且使用生成式模型Bart与原型聚类对比学习相结合,在zero-shot与cross-target的两个立场检测任务的标准数据集上取得了SOTA效果。我的一作论文已经在NAACL 2024上发表。同时我与清华大学KEG实验室一起合作探索大型语言模型对于小规模的模型的知识蒸馏过程,我们设计了ARTE的框架,通过知识提取、偏好收集和偏好对齐三个步骤,让教师模型根据学生模型的偏好来生成为知识蒸馏量身定制的训练数据,我们的大量实验证明了方法的有效性和泛化性。目前作为第一作者已经行文投稿至NeurIPS 2024。
  • +
  • 除此之外我在其他时间参加了一些算法竞赛,参加的所有比赛均拿到了名次,按照时间先后顺序,包括CCF大数据与计算智能大赛全国总决赛三等奖(5/1624),ChatGLM金融大模型挑战赛的季军(9/2200),百度搜索创新大赛的优秀奖(4-10/220),CodeQwen代码大模型比赛的季军(3/600),以及WSDM Cup 2024 的冠军(这个是与对话式多文档问答大模型相关的比赛)。我们团队一共两人,得到WSDM Cup的冠军后我们受邀在WSDM会议上进行了汇报并整理冠军方案发表了一篇WSDM Workshop的论文。
  • +
+

我的主要经历大概这样,有哪些经历您比较感兴趣我可以更加详细的介绍一下。

+

蚂蚁实习

+

简历内容

+

目标:基于Qwen进行医疗行业垂直领域大模型建设,包括医疗领域数据增量预训练、监督微调、强化学习等大模型全链路方法,同时增加检索增强、密态隐私安全等相关特色能力;

+

方法:收集爬取4.5B医学领域开源数据和中英文通用领域开源数据,并使用 PPLMinHash等方法对数据进行清洗、过滤等,随后在Qwen2系列LLM上进行继续预训练尝试;在SFT阶段针对医疗任务的指令特点,设计了一种基于多轮对话的指令扩充方法,增强模型指令能力;在SFT的基础上,设计奖励模型数据获取流程,对PPO、DPO及变体等进行了简单尝试;

+

目前成果:设计了完备的多维度模型评测流程,与其他著名开源医疗模型相比胜和率平均达到 64.8% ,选择题准确率超出 35% ,同时在三项中文医疗领域著名算法竞赛上取得 第一名

+

背景

+

隐语团队要打造大模型密算平台,其一要吸引外部客户在密算平台上训练模型,所以需要向外展示隐语团队有较强的打造垂域模型的能力。其二医疗作为目前是受众最多的垂域领域之一,结合密算平台推出隐语自研的安全医疗大模型,可以和医疗机构合作达到落地的目的。

+

垂域能力打造:在安全框架下针对特定医疗领域达到或超过蚂蚁医疗大模型的能力。

+

Why:垂域模型需要有自主的技术,选择医疗领域考虑是受众最多的垂域领域之一。

+

Todo:

+
    +
  1. 数据收集和处理: +
      +
    1. 收集相关开源数据,确保数据覆盖各个医疗子领域,以便全面训练。
    2. +
    +
  2. +
  3. 模型训练 +
      +
    1. 在通用开源模型的基础之上进行增量预训练。
    2. +
    3. 使用指令数据等对模型进行监督微调,增强模型在下游任务上的能力。
    4. +
    5. 采用强化学习方法增强用户体验,使模型能力符合真实医疗场景需求。
    6. +
    +
  4. +
  5. 评估和验证 +
      +
    1. 与目前已有开源医疗大模型进行对比。
    2. +
    3. 与闭源通用大模型和闭源医疗大模型进行对比。
    4. +
    5. 在实际应用场景中验证效果。
    6. +
    +
  6. +
  7. 天池相关医疗大模型领域竞赛打榜 +
      +
    1. 取得前排名次,展示医疗垂域相关模型算法能力。
    2. +
    +
  8. +
+

目前结论

+
    +
  1. GPT评测与顺序和长度强相关,经过实验,放在前面的回复更容易让GPT认为这个回复更好,且两个完全相同的回答GPT也认为靠前的回答更好一下而不是输出“一样好”。因此至少在GPT-3.5下,不能输入两个回答然后让GPT去评测。现在修改为给答案和标准答案然后让GPT去打分,打分后再离线进行比较,同时增加答案与标准答案之间的RougeL分数并完善准确率评测,目前从三个角度对模型进行评测。
  2. +
  3. 其他的开源大模型对选择题支持不太好
  4. +
  5. 训练后的模型相比于其他开源模型在各项指标上都要更好(和HuatuoGPT Ⅱ差距较小)。对比Qwen2-7B-Instruct我们的模型在RougeL评测上表现更好,但是在GPT评测上不如原始的Qwen2-7B-Instruct,主要原因认为是在SFT的过程中训练的数据答案比较短,而前期实验GPT的评测更倾向于较长的回复,因此在GPT评测指标下差距较大。
  6. +
  7. 推理时不同Temperature对结果影响不大
  8. +
  9. 单instruction在对话上不如不加,但是选择题有提升
  10. +
  11. 多instruction有提升
  12. +
  13. base post pretrain对话上总体有提升,选择题也有提升
  14. +
  15. 日常数据对选择题提升很大
  16. +
  17. sft在12000step的性能最好
  18. +
  19. 选择题数据多加一些对其他的性能没有很大影响,且选择题提升很大
  20. +
+

继续预训练

+

继续预训练:共2.7B token,其中通用语料约1.4B token,医疗领域约1.3B token

+

+英文数据:

+

pmc_llama_instructions row 513999 tokens:151723250

+

pubmed abstracts 5.4B token row:112165 tokens:29505846

+

共1.8B token

+

pk2omUs.md.png

+

SFT:SFT 训练数据(100w条左右)

+
    +
  • 22w huatuo_sft/train_data_22w.jsonl
  • +
  • 15w huatuo_wiki/train_data_15w.jsonl
  • +
  • 15w MedDiag/train_data_15w.jsonl
  • +
  • 7w CMeKG_CMedQA/train_data.jsonl
  • +
  • 15w CMeKG_CMedQA/train_data_150k.jsonl
  • +
  • 13w ChatMed/train_data_13w.jsonl
  • +
  • 8.26w PromptCBLUE_v2/train_data.jsonl
  • +
  • 5.44w CMExam/train_data_cot.jsonl
  • +
+

混入30%的通用数据(medical_and_daily)

+
    +
  • all_medical_data 随机挑选70%
  • +
  • alpaca-zh 4.9w
  • +
  • BelleGroup___multiturn_chat_0.8_m 10w
  • +
  • BelleGroup___train_0.5_m_cn 15w
  • +
+

PPL

+
    +
  • +

    困惑度(perplexity ppl)基本内容

    +
      +
    • 用来评价语言模型好坏或文本质量的指标。
    • +
    • 基本思想:给测试集的句子赋予较高概率值的语言模型较好,当语言模型训练完之后,测试集中的句子都是正常的句子,那么训练好的模型就是在测试集上的概率越高越好。,其中,W=w1w2w3…wN表示一个句子。
    • +
    +
  • +
  • +

    基本解释:困惑度越小,说明文本语义等内容越流畅。挑选困惑度较小的数据进行训练

    +
  • +
+

MinHash

+

用文档中所有词最小的K个哈希值做特征集合来表征这篇文档,然后基于特征集合的Jaccard距离计算文档之间的相似度。适合海量文档,是一种大规模文本去重算法。代表是GPT-3和Gopher.

+

SFT

+

指令扩充

+

pk2TSLF.md.png

+

针对医疗任务的指令特点,设计了一种基于多轮对话的指令扩充方法

+

原始指令:

+

请你基于患者当前及历史的问题给出回复,说话方式要像医生,在必要时如果无法明确诊断患者的疾病,可以询问患者更多的信息。但请切记,不要重复之前轮次的询问。\n

+

共分为三个部分:

+
    +
  1. 基本说明:请你基于患者当前及历史的问题给出回复,说话方式要像医生
  2. +
  3. 补充说明: 在必要时如果无法明确诊断患者的疾病,可以询问患者更多的信息。 (针对多轮对话医疗问答的特点,模型并不一定马上给出正确回复,可以进行适当的追问)
  4. +
  5. 限制:但请切记,不要重复之前轮次的询问。
  6. +
+

基本说明:上述的原始指令+约70条英文指令,共n条

+

补充说明+限制:共m条(当前使用m=2)

+

多轮对话的优势:

+
    +
  1. 减少模型无法理解指令从而输出不相关内容的现象
  2. +
  3. 种子数据较少但是排列组合比较多样,增强模型输出的随机性
  4. +
  5. 避免模型输出过长
  6. +
+

DPO

+

获取rm数据

+
    +
  1. 在sft数据中随机采样6w条数据
  2. +
  3. 设置temprature为1.0,将第一步的数据使用sft模型推理五遍,得到五个不同的推理结果。
  4. +
  5. 使用qwen2-72b-instruct,glm4-9b, yi1.5-34b 分别对第二步得到的数据进行评判,从五个结果中选出最好的回答和最差的回答。
  6. +
  7. 构建RM数据使用的原始数据从SFT数据里面选择或者不从SFT数据里面选择,使用SFT的不同ckpt进行选择还是使用通用大模型生成chosen的答案
  8. +
+

评测

+

pk2o5M8.md.png

+

目标:在有限的资源条件下尽量全面评测自研医疗大模型的能力

+

模型:与下面开源医疗大模型进行对比(模型开源能跑通且较新)

+
    +
  1. **Zhongjing **https://github.com/SupritYoung/Zhongjing
  2. +
  3. **HuatuoGPT-II **https://github.com/FreedomIntelligence/HuatuoGPT-II
  4. +
  5. **WiNGPT2 **https://github.com/winninghealth/WiNGPT2
  6. +
  7. **ChiMed-GPT **https://github.com/synlp/ChiMed-GPT
  8. +
+

数据:评测数据分为单轮对话、多轮对话和选择题三种类别

+
    +
  1. 单轮对话:Huatuo26M-test 和 webMedQA,各随机抽取500条数据,共1000条数据
  2. +
  3. 多轮对话:CMtMedQA,抽取150组对话,共517条数据
  4. +
  5. 选择题:C-Eval, CMMLU, CMExam, CMB, 2023_Pharmacist_Licensure_Examination,共抽取1356道题目
  6. +
  7. 医学名词解释:medtiku网站,共抽取1000道题目
  8. +
+

评测流程:

+
    +
  1. 对于单选类题目,尽量提取开源医疗大模型输出的答案部分,直接与自研大模型比较准确率
  2. +
  3. 对于对话问答类题目,从两个角度进行评测: +
      +
    1. 计算自研大模型与开源医疗大模型在GPT-3.5下相比的胜和率(通过分别令GPT3.5打分的形式进行)
    2. +
    3. 计算自研医疗大模型输出答案与标准答案的Rouge-L分数
    4. +
    +
  4. +
  5. 对于医学名词解释类题目,计算自研医疗大模型输出答案与标准答案的Rouge-L分数
  6. +
+

知识蒸馏论文

+

简历内容

+

动机:为了保证隐私安全,需要部署本地小型LLM,且我们希望其拥有大型LLM的能力,因此需要对大型LLM进行 知识蒸馏 ,将大型LLM的语言理解能力迁移到小型LLM上;

+

受教育学中响应式教学的启发,创新性地考虑 学生模型的反馈 ,提出ARTE框架,让 大型LLM(教师模型)根据小型LLM(学生模型)的偏好来生成为知识蒸馏量身定制的训练数据

+

ARTE框架通过 知识提取偏好收集偏好对齐的三个步骤,经过Llama-3-70B对Gemma-2B的知识蒸馏实验,在BIG-Bench-Hard评测标准上面相比于其他方法提升超过 3%

+

小型LLM在其他的数据集的微调效果展示了 学生模型的良好泛化性能 ;经过对齐后的大型LLM在其他任务方面与其他相似尺寸的小型LLM层面展示了 教师模型的良好泛化性能

+

简介

+

大型语言模型在自然语言处理的很多任务中都表现的比较出色。其中很大一部分原因是因为参数量比较大,模型的能力比较强,但是在处理隐私敏感数据时,为了保证隐私数据不被泄露,我们可能会需要在自己的设备上本地部署 LLM,而不能使用其他服务器或者是提供的api这种。 但是我们自己有的一般是小型设备,不太能直接部署大规模的 LLM。因此我们需要一种方法将大规模的模型的能力迁移到小规模的模型上,也就是一个知识蒸馏的过程。

+

目前已经有很多工作来探究这种知识蒸馏的过程,有些是采用COT的方式让大模型生成一个回答问题的过程从而让小模型进行模仿。让大模型生成小模型的训练数据并不困难,难点是保证数据的多样性和高质量。有些研究通过一些相似性度量方式(如ROUGE-L等)找出冗余的数据从而保证数据的多样性,phi模型是希望找到一些比较类似于教科书的数据,还有人认为生成数据的时候对于不同的任务应该采取不同的策略。这些工作都是聚焦于训练数据本身,而并没有考虑如果有了学生模型的反馈,对大模型的数据进行更有针对性的提取,会对这个教师模型生成数据的过程产生促进作用。这个就类似于教学法中的响应式教学,老师就是大规模的大模型,学生就是小规模的大模型,一个老师应该根据学生的反馈来调整自己的教学内容。

+

我们提出了ARTE的框架,让教师模型根据学生模型的偏好来生成为知识蒸馏量身定制的训练数据。主要有三个步骤

+
    +
  1. 知识提取:使用自己编写的种子问题作为prompt对教师模型进行推理,令教师模型草拟生成一些问题和答案
  2. +
  3. 偏好收集:在验证集上进行1-shot的上下文学习推理来收集上面生成的问题和答案的偏好,也就是哪些问题和答案比较好,哪些问题和答案不太好,就可以从提取出最具辨别力的top k的问题和答案对。
  4. +
  5. 偏好对齐:通过直接偏好优化 (DPO)让教师模型的偏好与学生模型保持一致
  6. +
+

使用对齐的教师模型重新生成问题和答案,这个训练数据就是为学生模型量身定制的,使用它们来微调学生模型,在下游任务上取得的效果更好。

+

我们的知识蒸馏方法在BBH数据集上面要比不采用我们这个方法提升3个点以上,我们同时也测试了这个方法的泛化性能。

+
    +
  1. 我们将在BBH上面设计的偏好微调出来的学生模型直接迁移到其他的数据集上面进行微调、例如PIQA、ARC、GSM8K等,验证了这个框架在增强学生模型学习能力上面的有效性
  2. +
  3. 我们探究了对齐偏好后的教师模型的泛化性能,效果也是很不错的 +
      +
    1. 任务层面的泛化性:在BBH上面设计的对齐教师模型可以迁移到其他的数据集上,例如生成PIQA、ARC等数据集更偏好的数据
    2. +
    3. 学生模型层面的泛化性:这个对齐的教师模型生成的数据也可以用来微调其他种类或者其他尺寸的小型大模型
    4. +
    +
  4. +
+

我们的贡献主要有三个方面:

+
    +
  1. 受教育学响应式教学的启发,我们提出了一种新的框架,使教师语言模型与学生语言模型的偏好保持一致,为学生模型生成量身定制的训练数据。
  2. +
  3. 在领域内和领域外的推理基准数据集中的大量实验表明,我们的框架生成的带有定制化的训练示例的微调学生模型大大优于现有的指令调优数据集。
  4. +
  5. 我们还研究了对齐教师模型的泛化,包括跨任务的泛化和跨学生模型的泛化。结果表明对齐的教师模型可以针对不同的推理任务和具有相似参数能力的不同学生模型生成量身定制的训练数据,从而完成知识蒸馏的过程。
  6. +
+

方法

+

我们的主实验是在BBH上面进行的,这个数据集总共有27个子任务

+

知识提取

+

使用自己编写的种子问题作为prompt对教师模型进行推理,令教师模型草拟生成一些问题和答案

+

首先是第一个知识提取的步骤,我们使用三个种子问题来构建一个问题生成提示作为Prompt对教师模型进行推理,引导教师模型集思广益,生成多个问题,在temperature=1的情况下多次进行推理,最终在每一个任务上得到大概250个问题。

+

然后,对于每个问题我们让教师模型生成一个经过思考后的答案,我们使用不同的Prompt推理让其进行生成,例如直接一句简单的Prompt然后回答、详细解释后回答,先说一下一些基本原理然后回答,step by step等,因为我们认为对于不同的问题和不同的模型,最好的推理Prompt可能是不同的。例如给定一个相同的数学问题,更强大的语言模型可能更喜欢纯粹基于数学符号的基本原理,因为它有这个能力,而小模型可能更喜欢将数学符号和自然语言组合在一起的一个推理过程。我们人为的设计了4个Prompt来生成答案。

+

最后,我们将每个问题和相应的回答两两结合起来,这样我们获取了task * 250 * 4=1000个问题回答对作为最初的数据集,也就是从教师模型中提取知识的过程。

+

偏好收集

+

在验证集上进行1-shot的上下文学习推理来收集上面生成的问题和答案的偏好,也就是哪些问题和答案比较好,哪些问题和答案不太好,就可以从提取出最具辨别力的top k的问题和答案对。

+

第二个步骤是偏好收集。在这一步中,我们的目标是收集学生模型在问题和答案两个层面的偏好。也就是说我们的目标是确定哪个问题或回答更有可能被学生模型接受。有研究指出,语言模型在上下文学习期间也会进行上下文示例的梯度下降。因此我们认为小模型对于这些问题和回答的偏好可以通过上下文学习的方式来获得,这样就不需要对模型进行微调,可以比较快速获取结果。我们具体的做法是从big-bench里面抽取了一个验证集,然后通过上下文学习的方式验证上面的问题和答案的有效性。也就是把上面的问题和答案对分别作为1-shot,加入到上面的有答案的验证集的前面,然后将temperature置为1推理100次,统计这100次中回答正确的次数。由于上面我们生成数据的时候对于每一个问题有不同的回答方式,这里我们对每一种回答方式都进行统计,最终对于每一个回答都可以获得一个分数,就是答案层面的一个偏好。然后一个问题的所有四个回答的分数的平均值就作为这个问题的分数,就是问题层面的一个偏好。

+

(IRT theory 待补充)不用全部的BBH数据,用十条数据测一下就行了

+

偏好对齐

+

通过直接偏好优化 (DPO)让教师模型的偏好与学生模型保持一致

+

在收集了问题和回答的偏好分数后,我们的目标是使教师模型与学生模型的偏好保持一致,也就是我们调整教师模型之后,教师模型能够有为目标任务生成量身定制的训练数据的能力。我们不仅要生成量身定制的回答,还要生成量身定制的问题。我们的实现方式是采用直接偏好优化的方式(DPO)。这种方式比较简单且稳定。

+
    +
  • 对于问题级别的偏好优化,我们在上面收集的分数最高的25个问题和分数最低的25个问题中进行随机抽取排列组合,分别作为DPO训练数据的chosen和rejected的答案。最终我们对于每个任务抽取了50个,得到50*task个训练数据对
  • +
  • 对于回答级别的偏好优化,我们选择上面每个问题的偏好得分最高的回答作为chosen,选择偏好分数最低的回答作为rejected。最终我们对于每个任务抽取了250个,得到250*task个训练数据对
  • +
+

最后,我们将问题和回答的DPO数据混合在一起进行教师模型的DPO,以使教师模型与学生模型的偏好保持一致。

+

最终数据生成

+

经过上面的三个步骤后,我们就获得了经过对齐的教师模型。那么我们使用对齐的教师模型来重新生成问题和答案,这个训练数据就是为学生模型量身定制的,使用它们来sft微调学生模型,然后在下游任务上进行测试。

+

实验效果

+

实验就是在没有相关benchmark数据的情况下,用什么数据来微调小模型才能让这个小模型在benchmark上面表现得更好

+

首先我们整个过程都是在BBH上面进行的,我们将BBH的23个子任务分成了四大部分的任务,分别是逻辑推理、常识推理、世界知识和数学能力。我们使用多种数据对小模型进行微调,每个数据都是250*27=6750条左右,从而证明我们方法的有效性。我们的大模型是Llama-70b-instruct,小模型是gemma-2b,DPO学习率1e-7,SFT学习率2e-5。

+

其他的指令微调数据集包括:

+
    +
  • GPT-4-LLM:使用Self-Instruct方法从GPT-4中蒸馏出来的指令微调数据集
  • +
  • Tulu-v2:多个高质量之指令微调数据集的混合,包括FLAN、OpenAssistant等
  • +
  • OpenOrca:使用GPT-4或3.5对Orca数据生成解释进行增强
  • +
  • WizardLM-Evol-Instruct:通过Evol-Instruct从GPT-4中蒸馏出来的指令微调数据集
  • +
+

除了用上面的流程生成的数据之外,我们也使用了一些其他的数据,分别在0-shot和3-shot两种实验设置下进行了实验。

+
    +
  • 其他比较有名的指令微调数据集
  • +
  • 原始的数据集:就是我们上面的第一阶段的数据,但是我们筛选了dpo chosen的数据,只用这些在验证集上面得分比较高的数据进行微调
  • +
  • DPO仅回答:只有答案由对齐的教师模型生成,问题是从原始数据中得来的。
  • +
  • DPO仅问题:只有问题由对齐的教师模型生成,答案是从原始数据中得来的。
  • +
+

我们的效果比其他的都好,同时我们发现问题的质量比回答的质量起着更重要的作用。

+

然后我们探究了我们的框架的泛化性能

+
    +
  • 我们将在BBH上面设计的偏好微调出来的学生模型直接迁移到其他的数据集上面进行推理、例如PIQA、ARC、GSM8K等,验证了这个框架在增强学生模型学习能力上面的有效性,超越了用原始数据的方法1.5个点左右。
  • +
  • 我们探究了对齐偏好后的教师模型的泛化性能,效果也是很不错的 +
      +
    1. 任务层面的泛化性:在BBH上面设计的对齐教师模型可以迁移到其他的数据集上,我们调整了一下Prompt让它生成PIQA、ARC等数据集的训练数据,这些数据是整个过程中都没有见过的。然后生成偏好的数据一样可以达到很好的效果,证明了通过偏好对齐,对齐的教师模型对学生模型的偏好有了更深入的理解,这有助于完成没有见过的任务。同时与原始教师模型相比,这种对教师模型的理解使得更容易将对齐教师模型的具体能力提炼到学生模型中。
    2. +
    3. 学生模型层面的泛化性:这个对齐的教师模型生成的数据也可以用来微调其他种类或者其他尺寸的小型大模型,我们在Gemma-7B、CodeGemma 2B和Qwen 1.8B上面进行了实验,结果表明,在Qwen1.5-1.8B和CodeGemma-2B中性能更好一些,但是在Gemma-7B 中,并没有更好。这表明具有相似参数容量的语言模型具有相似的训练数据的偏好。参数量不相似的模型的偏好是不一样的。
    4. +
    +
  • +
+

附录

+

Limitations

+

尽管使用的大多数数据都是由教师模型自动生成的,但仍然需要一些手动工作来构造提示并收集偏好分数。

+

主要有两个方面:

+
    +
  1. 在答案生成中,为了生成多样化和高质量的答案,需要一套精心设计的Prompt。在这项工作中,我们自己使用不同的推理技术为每项任务手工制作了系统提示。最近,有研究提出了CoT-Decoding,以揭示语言模型中问题的推理过程,而无需人工设计,后面会继续探索。
  2. +
  3. 其次,在偏好集合中,需要一组由问题和答案组成的问答对来充当验证集和偏好集。在这些标记的问答对上收集偏好分数,以衡量学生模型对草稿问题和理由的偏好。在这项工作中,我们只是将原始 Big-Bench 数据集中的数据重用为验证集。未来,我们将探索通过学生模型的内部状态直接衡量偏好的可能性
  4. +
+

为小模型生成答案的一些启示

+

通过分析小模型的偏好的答案,我们有下面的一些启示:

+
    +
  1. +

    答案越详细并不一定意味着小语言模型的性能越好。我们发现,答案的长度与偏好分数之间没有显着的线性相关性,即小语言模型在1-shot上下文学习中的准确性。具有完整而简洁的推理步骤的基本原理更有利于小语言模型的学习。原因可能有两方面:

    +
      +
    1. 首先,当输入上下文太长时,语言模型可能会丢失信息。对于容量有限的语言模型,当示例理由太长时,小语言模型可能会迷失在叙述中,忘记要解决的问题。
    2. +
    3. 在长篇细致的理论中,教师模型可以多次重复相同的步骤,例如在解决数学问题时。对于容量有限的语言模型,此重复步骤可能会导致小型语言模型陷入循环并无限重复同一步骤。
    4. +
    +
  2. +
  3. +

    尽管同一任务中的不同问题更喜欢不同的推理策略,但在监督微调中,小语言模型更喜欢为一个任务使用一致的推理策略。

    +
  4. +
+

我们还有一个问题可以进行探索,就是在偏好对齐后用大模型生成回答数据的时候,用哪个Prompt是更好的。(因为我们上面设计了四种Prompt对一个问题来生成答案)

+

训练语料库的多样性对于语言模型的预训练阶段至关重要,为了研究训练数据集中推理策略多样性的影响,我们在 Big-Bench-Hard 上使用四个不同的训练数据集对 Gemma-2B 进行了微调。

+
    +
  1. Randomly Selected:从不同的推理策略中随机选择每个问题的答案。
  2. +
  3. Most Preferred:根据上面测试的最高偏好分数选择每个问题的答案。
  4. +
  5. Task Consistent:一个任务选一个任务的Prompt,平均得分最高的
  6. +
  7. Aligned Teacher:最朴素的,第三个Prompt最简单,希望微调后的模型不要太多的Prompt工程就可以生成更好的数据
  8. +
+

结果是使用Most Preferred数据集微调的 Gemma-2B 的性能与Randomly Selected数据集相似,而Task Consistent和Aligned Teacher数据集的性能都优于其他两个数据集。这说明在微调阶段,特别是当我们尝试增强小语言模型的一两个特定能力时,一致的推理策略更有益。这背后的原因是,当推理策略对于一项任务来说过于多样化时,小语言模型可能会感到困惑,而一致的推理策略为小语言模型提供了明确的指导,以模仿特定的能力增强。

+

微软实习

+

简历内容

+

使用停留时间的用户历史行为特征从输入侧模型侧两个角度优化Unium特征向量,解决用户误点击及停留时间过长等问题,同时增强模型的鲁棒性。线上AUC+0.51%;

+

利用GPT和Ada2 Embedding升级原始多级主题模型,不依赖于人工标注,并保证对其他市场和其他主题的 泛化能力 。线下精确率在英语市场提升1.9%,在非英语市场提升7.4%;

+

分析线上用于推荐的特征,将线上用于对Top 30 Bing首页新闻进行CTR预估的简单DNN模型升级为以DeepFM为基础的推荐模型,完善模型训练流程并上线,线上AUC+0.37%;

+

针对线上问题,如Linkoff点击率过高、新闻文章质量、不同市场之间效果差异较大等,在GPT能力加持下训练深度学习模型进行解决,满足需求同时确保线上指标不变或有增益。

+

用户停留时间推荐

+

如何利用用户停留时间实现更为高效的推荐?

+

为什么要使用用户停留时间的特征?因为只看用户是否点击是不够的。

+
    +
  • 用户可能会因为“误点击”而迅速离开新闻内容页面。
  • +
  • 用户点击行为并不总是他们兴趣的全面体现。例如,他们可能会被新闻封面或标题所吸引,但在点击后很快意识到内容与他们的兴趣不一致而离开。
  • +
+

用户停留时间可以帮助过滤掉不相关的新闻,并有助于在用户建模中准确衡量特定点击行为的相关性。

+

但是有下面的问题:

+
    +
  • 仅仅依靠停留时间作为新闻质量和积极/消极用户行为的绝对衡量标准,并不能解释单个用户的多样化和个性化阅读习惯,短暂的停留时间不能最终解释为对内容不感兴趣或缺乏参与度。
  • +
  • 需要考虑现实场景中数据收集的潜在延迟等问题,可能有一些数据收集不到,因此模型的鲁棒性是需要特殊考虑的
  • +
+

对停留时间进行分析:

+
    +
  • 停留时间为0的点击文章很多,呈长尾分布
  • +
  • 用户的点击主要集中在5到200s之间,停留时间过长可能反映了用户在没有实质性阅读的情况下无意停留在页面上。
  • +
+

之前的版本:新闻表示+用户表示 + 多头注意力 双塔结构

+
    +
  • 新闻的表示:使用Ada Embedding获取
  • +
  • 用户的表示:用户点击的H篇文章的embedding(H,emb_size)作为用户的特征输入
  • +
+

pkWtgaj.md.png

+

DweW(停留时间权重)

+

之前的工作将停留时间作为一个过滤条件,过滤掉停留时间比较短的用户点击行为。但是这种方法没有考虑在现实场景下面的收集用户点击时间延迟的问题,以及用户的个性化问题(如某些用户就是停留时间比较短)。首先将用户的停留时间进行离散化,按照0和5为阈值,筛选出停留时间大于0的用户点击行为和停留时间大于5的点击行为。然后使用用户表示方法送入相同的多头注意力层和Attention pool层获取语义信息。

+

考虑到每一个用户都有独一无二的阅读习惯,我们引入了一个阅读偏好网络。将用户的停留时间编码进矩阵,过Attention层,获取大于0和大于5的两个部分的权重,使用门控网络将这些向量合并,形成一个更细微的用户兴趣表示。

+

DweA(停留时间Attention)

+

之前的工作过于依赖停留时间,且没有考虑语义和停留时间的交互过程。DweA将用户的停留时间编码进矩阵,拼接到原始的嵌入维度中(Q和K),使模型能够权衡被点击新闻的语义内容和停留时间。

+

随机抽取4条在同一会话中出现但未被该用户点击的新闻作为负样本。此外,我们扰乱新闻的顺序,以避免可能的位置偏见。

+

数据集:

+
    +
  • 一天数据集训练,一天数据集测试
  • +
  • 五千多万用户+五百多万新闻+389亿曝光+2亿点击
  • +
  • Adam,lr=10-3,batch 32 dropout 0.2
  • +
  • 选择用户的最后50个行为,10个头,维度为20
  • +
+

至关重要的是,我们的方法对用户停留时间信息表现出鲁棒性,即使在停留时间数据完全缺失的极端情况下,也能保持推荐高质量内容的能力。

+

TERA

+

主题模型可以推荐给用户他们感兴趣的东西,主要有两点问题

+
    +
  • 需要保持迅速扩展topic的能力,新topic过来后要尽快使用
  • +
  • 需要获得多语言多主题的大量数据来训练模型,依赖人工标注代价太大了
  • +
+

通过多个角度利用GPT的能力:作为先知(标注数据)和编码器(获取ada2 embedding)

+

之前是一个分级的model,第一级13个,第二级400,第三级10w

+

现在一共4000topic左右

+

在Ada2 Embedding上面搭建的LightGBM

+

pkWt0RP.md.png

+

模型结构:title embedding和body embedding各1536维直接取平均,query(也就是主题)直接送给ada2 embedding,也是1536维,两个向量逐个元素相乘成为1536维,query与title body 和刚才平均的进行点乘作为剩下的三维,1539维输入到LightGBM里面进行打分

+
    +
  • 使用元素相乘和点乘:这是因为输入文本的更高相似度被期望转化为它们的嵌入的更高余弦相似度,这相当于它们的点积,而点积又是它们的按元素的积之和。因此比直接连接两个向量要更好一些
  • +
+

评价:主要关注精确率,不关心召回(召回也很难算)

+

一周的数据,70w行,来自11个市场的7个语言,4k多主题

+

从已有的topic model上面拿数据,然后用GPT标,0.77是对的,0.23是错的,很多主题没有出现过,因此采样了一些,保证主题覆盖率的同时增加负样本的数量。同时也采样了一些正样本,让GPT给10个主题这样的方式

+

最后50对50的数据,8:2分训练集和验证集

+

测试集构造:为了包括全部的文档类别,用GPT Embedding进行聚类然后随机采样

+

7天的数据,还包括了2个非英语市场,一个市场聚100类然后取10个,是用GPT-4标注的

+

精确率在英语市场上提升1.9%,在非英语市场上提升7.4%

+

特征

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
featureexample valuepossible valuesdescription
AbstractLength102
AbstractWordCount18
BrandAuthority800
ImageScore1419822
Score1473
timeSlot18
TitleLength140
TitleWordCount23
TrendingScore0
IsLocalContent0
delayMinutes1939current datetime minus “DateCreated” in IntAttributes
isWeekend71-7day of week
Clicks50total clicks in the past 5 minutes
Clicks100total clicks in the past 10 minutes
Clicks300total clicks in the past 30 minutes
CTR510ctr in the past 5 minutes
CTR1010ctr in the past 10 minutes
CTR3010ctr in the past30 minutes
Udi[0-67]element wide product between user’s unium_v4 and item’s unium_v4 vector
Cosine[0-10]cosine similarity of unium_v4 vectors between user’s pass 10 clicks and the item
dotProd[0-10]dot product of unium_v4 vectors between user’s pass 10 clicks and the item
neastCosine1339050largest cosine-similarity
neastCosinePos6index of the largest consine similarity in user’s click history
neastDotprod921662largest dot product
neastDotprodPos2index of largest dot product in user’s click history
vertType1
Locale[0-54]0,1one-hot encoding of locale
Product[1-5]0,1one hot encoding of locale
vertType[0-18]0,1one-hot encoding of vertical type
ContentType[0-3]0,1one-hot encoding of ContentType
+

CTR预估

+

训练参数:hidden_units 512 256 64,batch_size 4096 Adamw lr 5e-2

+

这是一个名为 FM_Layer的因子分解机(Factorization Machine)层的实现。因子分解机是一种用于处理稀疏数据的机器学习算法,它可以捕获特征之间的交互关系。

+

__init__方法中,初始化了一个线性层和一个嵌入层。线性层用于处理输入特征的线性关系,嵌入层用于将输入特征映射到一个低维空间,以便处理特征之间的交互关系。

+

forward方法中,首先计算了线性项,然后计算了交互项。交互项的计算使用了因子分解机的一个重要特性,即通过嵌入向量的内积来模拟特征之间的交互。具体来说,交互项的计算公式为:(sum(v*x))^2 - sum((v*x)^2),其中 v是嵌入向量,x是输入特征。这个公式可以有效地计算所有特征对的交互,而且计算复杂度是线性的,这是因子分解机的一个重要优点。

+

最后,将线性项和交互项相加,得到最终的输出。

+
class FM_Layer(nn.Module):
+    def __init__(self, n_features, k):
+        super(FM_Layer, self).__init__()
+        self.linear = nn.Linear(n_features, 1)
+        self.embedding = nn.Embedding(n_features, k)
+  
+    def forward(self, x):
+        linear_term = self.linear(x)
+        interaction_term = torch.sum(self.embedding(x), dim=1) ** 2 - torch.sum(self.embedding(x) ** 2, dim=1)
+        return linear_term + interaction_term
+

FM模型的二次项等价化简过程如下:

+

+

FM部分结构图如下:

+

img

+

下图为DNN部分的结构图:

+

img

+

上面分别介绍了FM和DNN,下面把他们融合起来。典型网络融合有两种方式,一种是并行结构,一种是串行结构,DeepFM采用的是并行的方式。

+

forward方法中,模型首先通过输入层处理输入数据,然后将稀疏特征进行嵌入,接着将嵌入的稀疏特征和密集特征拼接在一起。如果启用了批量归一化,那么会对输入进行归一化。最后,模型将处理过的输入通过全连接层和逻辑回归层,然后将两者的输出相加,得到最终的预测结果。

+

img

+

与 Wide&Deep 的异同:

+

相同点:都是线性模型与深度模型的结合,低阶与高阶特征交互的融合。

+

不同点:DeepFM 两个部分共享输入,而 Wide&Deep 的 wide 侧是稀疏输入,deep 侧是稠密输入;DeepFM 无需加入人工特征,可端到端的学习,线上部署更方便,Wide&Deep 则需要在输入上加入人工特征提升模型表达能力。

+

DeepFM 优缺点:

+

优点:

+
    +
  1. 两部分联合训练,无需加入人工特征,更易部署;
  2. +
  3. 结构简单,复杂度低,两部分共享输入,共享信息,可更精确的训练学习。
  4. +
+

缺点:

+

1 将类别特征对应的稠密向量拼接作为输入,然后对元素进行两两交叉。这样导致模型无法意识到域的概念,FM 与 Deep 两部分都不会考虑到域,属于同一个域的元素应该对应同样的计算。

+
sparse_features = ['IsLocalContent', 'isWeekend', 'vertType', 'timeSlot', 'HeadLineWholePageOrder', 'WholePageOrder', 'hostAuthority0', 'hostAuthority1', 'hostAuthority2', 'hostAuthority3', 'hostAuthority4', 'hostAuthority5', 'hostAuthority6', 'hostAuthority7', 'hostAuthority8', 'hostAuthority9', 'hostAuthority10', 'minAuthority', 'avgAuthority', 'Product_OHIndex_0', 'Product_OHIndex_1', 'Product_OHIndex_2', 'Product_OHIndex_3', 'ContentType_OHIndex_0', 'ContentType_OHIndex_1', 'ContentType_OHIndex_2', 'ContentType_OHIndex_3', 'vertType_0', 'vertType_1', 'vertType_2', 'vertType_3', 'vertType_4', 'vertType_5', 'vertType_6', 'vertType_7', 'vertType_8', 'vertType_9', 'vertType_10', 'vertType_11', 'vertType_12', 'vertType_13', 'vertType_14', 'vertType_15', 'vertType_16', 'vertType_17', 'vertType_18', 'blingMatchCount', 'userBlingCtrCount', 'userBlingNegCount', 'IsMale', 'IsFemale', 'NoGender', 'IndexedAgeIntValue', 'IsSemiFresh']
+
class DeepFM(nn.Module):
+    def __init__(self, 
+                 feature_config,
+                 hidden_units=[512,64],
+                 output_dim=1,
+                 emb_size=10,
+                 embedding_cols=230,
+                 bucket_cols=54,
+                 dropout=0.0,
+                 activation='relu',
+                 use_senet=False,
+                 sparse_batchnorm=True,
+                 mlp_normalization='batch',
+                 non_blocking=False,
+                 device='cuda'):
+  
+        super(DeepFM, self).__init__()
+        self.device = device
+        self.non_blocking = non_blocking
+        self.sparse_batchnorm = sparse_batchnorm
+        self.bucket_feat = ['IsLocalContent', 'isWeekend', 'vertType', 'timeSlot', 'HeadLineWholePageOrder', 'WholePageOrder', 'hostAuthority0', 'hostAuthority1', 'hostAuthority2', 'hostAuthority3', 'hostAuthority4', 'hostAuthority5', 'hostAuthority6', 'hostAuthority7', 'hostAuthority8', 'hostAuthority9', 'hostAuthority10', 'minAuthority', 'avgAuthority', 'Product_OHIndex_0', 'Product_OHIndex_1', 'Product_OHIndex_2', 'Product_OHIndex_3', 'ContentType_OHIndex_0', 'ContentType_OHIndex_1', 'ContentType_OHIndex_2', 'ContentType_OHIndex_3', 'vertType_0', 'vertType_1', 'vertType_2', 'vertType_3', 'vertType_4', 'vertType_5', 'vertType_6', 'vertType_7', 'vertType_8', 'vertType_9', 'vertType_10', 'vertType_11', 'vertType_12', 'vertType_13', 'vertType_14', 'vertType_15', 'vertType_16', 'vertType_17', 'vertType_18', 'blingMatchCount', 'userBlingCtrCount', 'userBlingNegCount', 'IsMale', 'IsFemale', 'NoGender', 'IndexedAgeIntValue', 'IsSemiFresh']
+        self.bucket_size = [2, 8, 19, 24, 16, 16, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 11, 129, 129, 2, 2, 2, 11, 2]
+  
+        self.embedding_dim = emb_size
+        self.use_senet = use_senet
+  
+
+        input_size = embedding_cols + bucket_cols * self.embedding_dim
+  
+        if sparse_batchnorm:
+            self.sparse_norm = nn.BatchNorm1d(embedding_cols + bucket_cols * self.embedding_dim)
+            self.sparse_norm_wide = nn.BatchNorm1d((embedding_cols + bucket_cols) * 1 )
+  
+        # Deep
+        self.dnn = MLP_Layer(input_dim=input_size, 
+                             hidden_units=hidden_units, 
+                             output_dim=1,
+                             dropout=dropout,
+                             activation=activation,
+                             bn_name=mlp_normalization)
+
+        self.fc = nn.Linear(hidden_units[-1] + 1, 1, bias=True)
+  
+  
+        self.emb_layer = nn.ModuleList([nn.Embedding(self.bucket_size[i], self.embedding_dim) for i, _ in enumerate(self.bucket_feat)])
+
+        self.input_layer = CustomInputLayer(feature_config,device)
+  
+        # self.alpha = nn.Parameter(torch.FloatTensor([0.05]))
+  
+        # Wide
+        self.lr_layer = LogisticRegression_Layer(input_size, use_bias=False)
+        self.fm_layer = FM_Layer(embedding_cols + bucket_cols, 5)
+  
+        self.initialize()
+
+  
+    def initialize(self):
+        def init_weights(m):
+            if isinstance(m, (nn.Linear, nn.Embedding)):
+                nn.init.xavier_uniform_(m.weight)
+                if isinstance(m, (nn.Linear)) and m.bias is not None:
+                    m.bias.data.fill_(0.01)
+        self.apply(init_weights)
+  
+    def forward(self, X):
+        sparse_feature, dense_feature = self.input_layer(X)
+
+        # wide_input = torch.cat([sparse_feature, dense_feature], -1)
+  
+        sparse_embedding = [layer(sparse_feature[:,i]) for i, layer in enumerate(self.emb_layer)]
+
+        input = torch.cat(sparse_embedding + [dense_feature], -1)
+  
+        if self.sparse_batchnorm:
+            input = self.sparse_norm(input)
+  
+        lr_logit = self.lr_layer(input)
+        #print(lr_logit)
+        # input = torch.cat((input, dense_feature_log), -1)
+  
+        return self.dnn(input) + lr_logit
+

立场检测NAACL论文

+

简历内容

+

立场检测任务:从某人的言论中判断他对某个目标的立场(支持、反对、中立),相比于普通的情感分析的难点在于 目标(较短)不同的相同言论(较长)的立场标签可能完全相反

+

充分利用大模型的语言理解能力,对言论与目标的关系进行 显式深度分析 ;使用生成式小模型(BART)微调立场标签的 生成 ,并创新性应用原型聚类对比学习的训练策略增强语义理解;

+

提出的立场检测方法在Zero-Shot与Cross-Target两种类型的立场检测任务上分别超越前SOTA **2.5%**及 10-15% 。消融实验证明了上述每一个模块的有效性。论文地址

+

任务介绍

+

立场检测任务是指从某人的言论(也就是文本text)中判断他对某个目标(也就是target)的立场。这个言论是发表在一些公开场合的文字,如Twitter,Facebook或者辩论会上面。需要判断的立场一般分三种,分别是支持、反对和中立,表示这个人发表的言论text对于target的一个立场态度,(支持target,反对target,或者仅仅是对target的客观评价,并没有情感倾向),也有的数据集可能会包括“不相关”的第四个立场,也就是这个言论与这个target完全没有关系。这个任务类似于文本分类任务中的情感分析的任务,不同的是target很重要,比如美国总统大选的时候,target一般就指Trump和Biden,如果一个人发表了一大段支持特朗普的言论,则对于拜登的立场就会是完全相反的。target的变化在数据集中可能仅仅是一个单词的变化,结果会导致立场标签完全不同,也就是立场检测相比于普通的情感分析的难点所在。

+

任务分类

+

根据target在测试集中是否可见,立场检测可分为三类: 一、训练集与测试集有相同的target;二、target在测试集中不存在,但是测试集中有相似的target(也就是cross-target的分类);三、target在测试集中不存在,也没有相似的target存在(也就是立场检测中的zero-shot)。我这篇论文主要是做zero-shot的任务,因为后两个任务要比第一个任务更为困难一些。

+

详细说明

+

LLM驱动的知识

+

之前的工作普遍希望使用各种方法扩充target的信息,如在wiki上面查找target的相关知识并拼接一起输入到模型等,或者是在模型结构上对target语义进行更为详尽的提取。但是很少有工作对target与text之间进行显式的分析。我们有了大模型,这个问题正好可以使用大模型来解决,可以利用类似于ChatGPT等大模型的语言理解能力,去显式提取text与target之间的关系,也就是增加额外的“LLM驱动的知识”。这个知识与之前相比更加具体,语义更加丰富。

+

我们具体的做法是将text与target一起送给GPT-3.5,让它分析关键词、修辞手法、隐藏情感,最后说一下对这个立场的看法,但是并不允许直接给出表明立场的词汇。提取关键词的目的是让模型更为关注这些关键词语,修辞手法和隐藏情感的目的是找到一些表达立场的证据,如果这个是一个讽刺的言论,那么立场应该是完全相反的。最后只让它分析立场而并不是直接给出立场,因为我们前期实验发现让大模型直接给出立场的效果并不好,在VAST数据集上面只有69左右,因此不让其给出立场词汇是会防止其误导模型,有助于最后的立场检测效果。

+

我们后面做了一些消融实验证明了这里面的每一个部分对于我们整个模型的效果都是起促进作用的。如果不加这些LLM驱动的知识的话效果大概是不到75的样子,像之前的工作添加wiki的知识的效果大概是76多,我们添加关键词的效果可以到达75多,修辞手法和隐藏情感大概76多,加上分析立场的文字后大概是77多,如果一起加上的效果最终是79.6

+

BART

+

因为立场标签也是有语义的,为了弥补这个知识与立场标签之间的语义的gap,我们使用了双向自回归语言模型-BART作为我们的主干网络,因此我们把这个分类问题转换成了一个立场标签的生成问题,输入是文本和LLM驱动的知识,输出是利用了丰富的语义进行解码的立场标签的文字。

+

我们后面做了消融实验证明了BART的有效性,我们分别实验了将上面的LLM驱动的知识直接使用LLM进行分析,以及使用BERT或者BART作为主干网络,最终效果最好的是BART

+
    +
  • BART的预训练目标就是对于被破坏的文章优化一个重构损失函数,实际上就是针对Decoder的预测结果和目标文章(label)的交叉熵损失。亮点是它允许接受任意破坏形式的文章
  • +
  • 单Token级别的掩码(Token Masking),这个和BERT一样,按一定比例随机采样Token进行[MASK];
  • +
  • Token级别的丢失(Token Deletion),直接随机删掉文章中的一些单词(注意作者没有提到deletion的Token是否可以连续,因此猜测这种方式同样也包含了连续几个Token被delete的情况),这和MASK可不一样,MASK的话模型至少知道原Token在哪,而直接deletion的话,模型不仅需要知道原Token是啥,还需要知道原Token都在哪些位置,这无形就提高了模型的预测能力。
  • +
  • 片段级别的Token MASK(Text Infilling),这个在上文已经解释过了,这里作者给出了片段选取的依据,即通过均值λ为3的泊松分布对span text进行采样,比如有长度为0的,长度为3的,长度为5的等等片段,将它们分别只用一个MASK替换。作者也提到这种方式和SpanBERT的思路类似,区别在于SpanBERT使用的是clamped geometric分布采样,且采出来的片段长度是多少,就用相同数量的MASK替换,可见BART实际上对这种方式的学习更加苛刻。作者也说了,text-infilling是为了让模型学习到一个MASK中原本是有多少个Token,其它们是啥。
  • +
  • 句序打乱(Sentence Permutation),将完整的句子在原文中的顺序打乱,让模型学习它们原来应该在什么位置。
  • +
  • 文章旋转(Document Rotation),其实就是随机在文章中取一个词,并以该词为基点将文章旋转,然后再让模型学习原文的起始Token是啥。说实话,这个操作挺骚气的,虽然看上去很牛的样子,但是还没怎么领会其中的奥妙,我觉得Sentence Permutation已经包含了这个功能了。
  • +
+

img

+

原型聚类对比学习

+

之前的工作一般使用对比学习的方式,也就是将立场标签相同的文本在向量空间中拉近,将立场标签不同的文本在向量空间中推远,这样最终立场标签的表示应该会形成类别数量的簇。

+

但是虽然标签相同的文本,在语义上可能是不太相同的,将其强行归为一个类别并不是很合理。因此为了更好的align立场的表示与标签的语义,我们在上述框架的基础上进一步采用原型聚类对比学习的方式,与普通对比学习的区别在于我们并不强制相同类别的标签一定将彼此拉近,而是采用多个原型来对分类特征空间进行建模,然后优化对比损失,使得立场标签被相应标签的最近的原型吸引,并被其他立场的原型排斥。这样最终的立场标签的表示会更为鲁棒,会形成原型类别的簇,立场的表示与标签的语义就会更为统一。

+

具体的做法是在最初对每个类别都初始化一个原型,对比学习的时候将样本归到最相近的原型中,这也会有一个问题,也就是之前的归类更多的原型后面的相似度越来越高导致所有样本都聚类到这个原型上面,可能导致我们最终只有一个原型起作用。因此我们训练5个epoch后使用Kmeans对最近的1000个类别向量重新设定类别中心来缓解这个问题。实验表明原型的数量为3的时候效果最好,总体上大概有1-2个点的提升。

+
class MCContrastiveLoss(torch.nn.Module):
+    def __init__(self, training_steps, seed, bert_config):
+        super(MCContrastiveLoss, self).__init__()
+        self.prototype_contrastive_class = bert_config["prototype_contrastive_class"]
+        self.prototype_contrastive_num = bert_config["prototype_contrastive_num"]
+        self.encoder_label_pos = bert_config["encoder_label_pos"]
+        self.decoder_label_pos = bert_config["decoder_label_pos"]
+        self.label_id_list = bert_config["label_id_list"]
+        self.prototype_contrastive_project_dimension = bert_config[
+            "prototype_contrastive_project_dimension"
+        ]
+        self.prototype_contrastive_temperature = bert_config[
+            "prototype_contrastive_temperature"
+        ]
+        self.prototype_contrastive_ema = bert_config["prototype_contrastive_ema"]
+        self.loss_combination_ratio = bert_config["loss_combination_ratio"]
+        self.kmeans_step = bert_config["kmeans_epoch"] * training_steps
+
+        self.kmeans = KMeans(
+            n_clusters=self.prototype_contrastive_num, random_state=seed, n_init="auto"
+        )
+        self.kmeans_deque = [
+            deque(maxlen=bert_config["kmeans_deque"])
+            for i in range(self.prototype_contrastive_class)
+        ]
+
+        self.project = nn.Linear(768, self.prototype_contrastive_project_dimension)
+        self.anchor = torch.randn(
+            (
+                self.prototype_contrastive_class,
+                self.prototype_contrastive_num,
+                self.prototype_contrastive_project_dimension,
+            ),
+            dtype=torch.float32,
+        )
+        self.anchor = F.normalize(self.anchor, p=2, dim=-1)
+
+    def forward(self, outputs, labels):
+        # judge when to apply kmeans
+        self.kmeans_step -= 1
+
+        # to gpu
+        self.project = self.project.to(labels.device)
+        self.anchor = self.anchor.to(labels.device)
+
+        # predict label now
+        predict_label = (
+            outputs.logits[:, self.decoder_label_pos, self.label_id_list]
+            .squeeze(1)
+            .detach()
+        )
+        # transform predict label to one-hot
+        predict_label_index = F.one_hot(
+            torch.max(predict_label, dim=1)[1],
+            num_classes=self.prototype_contrastive_class,
+        )  # (64, 3)
+
+        # transform actual label to one hot
+        contrastive_mask = F.one_hot(
+            labels, num_classes=self.prototype_contrastive_class
+        )  # (64, 3)
+
+        # project encoder_last_hidden_state into smaller dimension
+        contrastive_vector = F.normalize(
+            self.project(
+                outputs.encoder_last_hidden_state[:, self.encoder_label_pos, :].squeeze(
+                    1
+                )
+            ),
+            p=2,
+            dim=-1,
+        )  # (64, 128)
+
+        for i in range(self.prototype_contrastive_class):
+            # only extract same label both label and predict
+            temp_mask = contrastive_mask[:, i] * predict_label_index[:, i]
+            if torch.count_nonzero(temp_mask) != 0:
+                # contrastive_vector for class i
+                contrastive_vector_class = contrastive_vector[temp_mask.bool()].detach()
+
+                self.kmeans_deque[i].extend(contrastive_vector_class)
+
+                if self.kmeans_step == 0:
+                    now_deque_tensor = torch.stack(list(self.kmeans_deque[i]))
+                    self.kmeans.fit(now_deque_tensor.cpu().numpy().astype("float32"))
+                    cls_centers = torch.tensor(self.kmeans.cluster_centers_)
+                    cls_centers = F.normalize(cls_centers, p=2, dim=-1)
+                    self.anchor[i] = cls_centers
+
+                # choose the most similar one
+                max_index = torch.max(
+                    torch.einsum("bd,nd->bn", contrastive_vector_class, self.anchor[i]),
+                    dim=1,
+                )[1]
+                # print(max_index)
+
+                for j in range(self.prototype_contrastive_num):
+                    proto_index = (max_index == j).nonzero()
+                    if proto_index.size(0) == 0:
+                        continue
+                    cur_mean_vector = torch.mean(
+                        contrastive_vector_class[proto_index], dim=0
+                    )
+
+                    self.anchor[i][j] = self.anchor[i][
+                        j
+                    ].data * self.prototype_contrastive_ema + cur_mean_vector * (
+                        1 - self.prototype_contrastive_ema
+                    )
+
+                    self.anchor[i][j] = F.normalize(self.anchor[i][j].data, p=2, dim=-1)
+
+        feat_sim_mat = torch.max(
+            torch.matmul(self.anchor, contrastive_vector.T).permute(2, 0, 1), dim=-1
+        )[0]
+        feat_sim_mat = torch.div(feat_sim_mat, self.prototype_contrastive_temperature)
+
+        logits_max, _ = torch.max(feat_sim_mat, dim=1, keepdim=True)
+        feat_sim_mat = feat_sim_mat - logits_max.detach()
+
+        # compute log_prob
+        exp_logits = torch.exp(feat_sim_mat)
+        log_prob = feat_sim_mat - torch.log(1e-7 + exp_logits.sum(1, keepdim=True))
+        # print(log_prob)
+        # print(log_prob.shape)
+        contrastive_loss = -(
+            (contrastive_mask * log_prob).sum(1) / (contrastive_mask.sum(1) + 1e-7)
+        ).mean()
+        # print(contrastive_loss)
+
+        return outputs.loss + contrastive_loss * self.loss_combination_ratio
+

数据集

+

我们在P-Stance和VAST两个数据集上面取得了SOTA的效果,实验表明在zero-shot(VAST数据集)与cross-target(P-Stance数据集)两种类型的立场检测任务上分别超越前SOTA 2.5% 及10-15%。

+

WSDM Cup 2024

+

简历内容

+

比赛目标:在给定上下文对话和最多5篇 参考文档 (小红书文本笔记)的基础上,根据用户的最终问题生成正确的回答,评价指标主要为ROUGE-L,要求模型总参数量不超过14B;

+

方案简介:以SOLAR 10.7B大模型为基座进行LoRA 微调 ,结合Prompt设计、多轮对话训练方式、混合训练、不相关参考文档的联合过滤、生成式任务的模型集成等策略;

+

比赛结果:在三个评价指标上分别领先亚军网易互娱与季军华为2012实验室 1.6%0.9%2.3% 获得冠军,受邀投稿并在WSDM 2024会议进行汇报。Github仓库Star 140+

+

介绍

+

WSDM Cup 2024是一个解决多文档对话式问答的比赛。

+

对话式问答是根据对话中识别的用户意图生成正确且有意义的答案,(在现代搜索引擎和对话系统中发挥着至关重要的作用)。 目前的挑战是训练模型的数据中可能不包含时效性高的信息,因为在语言模型在训练阶段是无法获得当下或者是训练时间之后的知识。 尽管可以通过检索增强的方式为模型提供多个相关文档作为上下文信息,但模型仍然面临着无法从长文本中获取有效信息,被大量无关信息输入误导的风险。本次比赛就是鼓励对这种对话式多文档问答类问题进行进一步的探索。

+

本次比赛提供的数据集是json的格式,其中训练集包括12557 条数据,验证集包括1794条数据,测试集包括3588条数据。所有数据中都包含uuid,历史的问答对,最多5篇小红书的参考文档和用户的问题。数据就类似于一个人在询问如何将iPhone中的照片导入到电脑中,然后回答说可以通过这样的方式,需要一个USB线,然后用户说如果我没有这根线该怎么办。然后会给几篇小红书上面的参考文档,我们的目标就是生成最终的答案,尽量与标准答案越相似越好。其中训练集包括主办方给出的回答的标准答案,验证集和测试集需要我们自己生成答案提交到竞赛平台上面进行评测。评价标准有三个,包括字符级别的rouge l,文字级别的rouge l和keywords recall(关键词的召回率),这个关键词也是数据集中的预先标注的,不过在训练、验证和测试三个数据集中都不会公开给我们,仅用于评测。参赛模型的参数量要求不允许超过14B

+

最近,ChatGPT 等大语言模型在多项自然语言处理任务上表现出了 SOTA 性能,我们希望通过利用大模型的理解和推理能力来探索这一问题。 我们做了很多的尝试,最work的方案是首先根据任务微调 LLM,然后设计了一种混合训练策略,以充分利用无标注的验证集数据和测试集数据。并使用一个文本嵌入模型来过滤潜在的不相关文档,最后设计和比较了几种模型集成的方法,拿到了这个比赛的冠军。

+

LLM

+

首先使用LLM进行微调的时候,由于我们的输入有很多信息,包括历史对话、参考文档和问题等。我们仔细设计了三者的输入顺序,最终确定了历史对话、问题、参考文档的格式。首先历史对话与问题是相关的,我们发现在问题中会存在一些代词,可能要去历史对话中找答案,且问题有可能很简短,实际上的信息都保存在历史的对话中。因此历史对话与问题是要连在一起输入的。其次有研究表明,在RAG中与问题越接近的文档,模型的关注度会越高,而我们参考的小红书的文档顺序是直接从搜索引擎中得来的,因此最相关的文档排序靠前,也就与问题更为接近。经过实验我们的这种组织方式效果是最好的。我们并没有仔细设计prompt,因为我们前期做了一些简单的尝试发现Prompt对于最终结果的影响并不是很大。

+

然后我们对大模型进行微调。虽然我们发现历史的答案与最终的答案并不是很相似,最终的答案的长度要大于历史的答案,但是我们经过实验后还是采用多轮对话的方式进行训练。我们猜测多轮对话的模式可以使大模型更加关注上下文信息,帮助解决我们上面提到的最终问题中可能存在的指代消解。

+
    +
  • 将一条多轮对话数据,拆分成多条数据
  • +
  • 将一条多轮对话数据拼接之后,输入模型,并行计算每个位置的loss,只有Assistant部分的loss参与权重更新。
  • +
+

为什么Work?答案在于因果语言模型的attention mask。以GPT为代表的Causal Language Model(因果语言模型),这种模型的attention mask是一个对角掩码矩阵,每个token在编码的时候,只能看到它之前的token,看不到它之后的token。

+

最后我们对目前比较流行的多个开源且参数量小于14B的大模型进行了尝试,发现SOLAR的模型表现最好。它是两个Llama结构(使用了mistral的权重)拼接在一起后使用高质量的训练数据进行再次预训练后得到的大模型,因此最后我们使用SOLAR大模型进行微调。

+

SOLAR模型:

+

Depthwise scaling的过程如下:

+
    +
  1. 从32层的基础模型开始,我们将该模型复制一份以进行后续修改。
  2. +
+

然后,从原始模型中移除最后m层,并从其复制体中移除最初的m层,这样就形成了两个各有(n-m)层的独特模型。

+
    +
  1. 将这两个模型拼接在一起,构建一个具有s = 2·(n−m)层的扩展模型。
  2. +
+

在SOLAR 10.7B-Instruct中,由于基础模型n为32层,并且考虑到硬件限制以及扩展模型效率(模型参数量介于70亿至130亿之间),SOLAR 10.7B-Instruct设置s为48层。因此,需要移除的中间层数目m计算得出为8层(即m=8)。

+

+
    +
  1. 然后进行预训练+SFT+DPO(最后使用了数学的数据集)
  2. +
+

混合训练

+

在大模型的训练或微调中,高质量的数据要比数量更为重要。但是由于小红书的数据比较特殊,我们无法找到很相似的数据进行数据增强。不过我们在评测测试集的效果的时候,验证集是可以使用的。因此我们使用我们第一阶段的最好的模型对验证集进行推理,推理得到的结果加入到训练数据中再次进行训练,也就是训练数据从12000条增长到了14000条。一方面可以被视为对域内无标签数据的知识蒸馏过程,另一方面,因为我们只为最终答案生成伪标签,历史问题的答案仍然是官方标注的,这有利于多轮对话的训练模式。实验结果表明这样训练会将各个指标提升半个点到一个点。不过我们不会进一步加入测试数据集的伪标签,因为它可能会过度校正模型,实验结果表明这样反而会降低最终的模型性能。

+

不相关文档过滤

+

其次我们发现有一些不相关的文档,一种不相关的文档是这个文档可能是视频或者图片的形式,因此提取出来的文字就只有对问题的重复,因此对于回答这个问题来说并没有任何的帮助。另一种不相关的文档是真正的不相关的文档,描述的内容与问题或者是历史的对话都是完全不相关的。因此在不存在真实答案的情况下量化相关性就显得至关重要,相似度太高的和相似度太低的文档是都要进行剔除的。

+

我们从语义和词汇的角度使用了多种方式进行联合判断,如计算单词或字符级的 ROUGE-L ,也可以被视为词汇相关性标准。或者使用文本嵌入模型计算文档与相应问题(或与对话历史问答一起)之间的余弦相似度,或者我们对每个评价指标都设置了一个较高的和一个较低的阈值,如果计算的分数不在这个阈值的区间范围内我们就将其进行剔除。最终,我们在第二阶段过滤掉了 193 个噪声文档,分数有一点点的提升。一方面是这个参考文档本身就比较短,我们将全部的数据拼接在一起也不会超过3072个token,另一方面我们也发现大模型是有一定的过滤不相关文档的能力的。

+

此外,之前的工作表明,将重要的信息放在大模型的开头或结尾处有利于其更好地利用有效信息,因此,我们也尝试了对输入数据的参考文档进行顺序调整。 然而,我们发现原数据中参考文档的索引(即其出现顺序)和官方标注的答案中其出现的相对顺序之间存在很强的相关性,相关实验也表明,对参考文档重新排序可能会导致严重的性能下降,因此我们实际上并没有对文档的顺序进行调整。

+

模型集成

+

模型集成已被证明在判别类任务中是有效的,但是,很少有工作在生成任务上对其进行探索。在这项工作中,我们希望找到一种方法可以近似评估不同模型生成答案的质量,然后从中选择选择最好的一个作为最终结果。我们想象这样一个现实场景,假设有 N 位候选者都提出了自己的方案,那么最终的方案应该是获得赞成最多的方案。因此,我们最终选择的答案应该是与最多候选模型达成一致的代表。具体地,假设给定一个测试样本,我们有M个候选答案进行集成,对于每个候选r_i,我们计算r_i和r_j之间的相关性分数,将它们加在一起作为r_i的质量分数q_i。这个质量分数我们也尝试了一些,例如上文提到的余弦相似度,rouge l等。虽然使用我们上面的方案就已经超出第二名很多了,我们通过这种方式进行集成也可以显著提升最终模型的效果,且集成的数量越多,效果提升就越明显。最终我们训练出了8个比较好的单模型一起集成。

+

ChatGLM金融大模型

+

简历内容

+

赛题目标:以ChatGLM2-6B大模型为中心制作一个问答系统,根据上市公司的原始PDF年报,回答用户的 金融相关问题 。问题包括基本查询、统计分析、联合对比、开放性问题等;

+

将原始金融年报的文字与表格提取到数据库中,利用通用模型的上下文多轮对话能力提取问题的关键部分。针对不同的问题类型,首先微调LoRA权重进行问题分类,对于查询统计对比类问题,使用 NL2SQL (微调的LoRA权重)+正则匹配+查表的联合方法解决;对于开放性问题使用 多路检索召回 +Prompt设计解决,最终实现了完整的金融年报问答系统,得分85.89。

+

任务是以ChatGLM2-6B模型为中心制作一个问答系统,根据上市公司的原始PDF年报,回答用户的金融相关的问题。上市公司每一年都会发布一个公开的金融年报,格式基本相同,且大部分是表格形式的数据,一个年报的总页数大概在400页左右。主办方提供了1万多篇年报文档和五千的待回答的问题,所有问题都没有答案,只能在天池的线上系统进行评测。评测指标:首先要包括最重要的答案数字,占0.25分,但是如果数字答案不对直接为0分,其次查找答案中的关键词,满分为0.25分,没有关键词会按照缺失比例扣分,最后的0.5分是根据语句的相似度匹配进行计算给出的分数。

+

问题介绍

+

问题类型有三种,实际上是五种问题:

+
    +
  1. 数据基本查询:参赛者需要利用提供的ChatGLM2-6B开源模型和上市公司年报原始数据,并以此为基础创建信息问答系统。系统需能够解决基本查询,如:某公司2021年的研发费用是多少?等问题。
  2. +
  3. 数据统计分析查询:在初级阶段的基础上,参赛者需要进行金融数据的统计分析和关联指标查询。系统需基于各类指标,提供问题和答案,如:某公司2021年研发费用增长率为多少?等问题。决赛增加了一些难度更大的问题,如上海研发费用增长率排名前10的公司是哪些,需要对多个年报文档进行联合查询。
  4. +
  5. 开放性问题:如某公司2021年主要研发项目是否涉及国家创新领域,如新能源技术、人工智能等?或者询问一些年报中提到的概念,如什么是研发费用等。前面的问题是需要参考年报进行回答的,后面的问题是纯开放性问题,年报中也不包含任何的相关信息。
  6. +
+

年报数据处理 todo

+

方案介绍 – PDF2TXT工具优化

+
    +
  1. 解决处理非封闭表格数据使用原有lines方式获取表格缺失数据问题? (兼容考虑第一行和第一列可能是合并单元格的情况)
    +通过page.rects分析表格线条是否完整封闭,如果非封闭的表格,使用明确的水平线和垂直线获取表格数据;如果是封闭的表格使用原有的line的方式获取表格数据
  2. +
  3. 解决虽然是封闭的表格,但是使用lines的方式获取表格没有被正常提取的情况,缺失部分列的情况?
    +使用原有的方式获取到表格后,对表格的宽带进行判断,如果小于页面宽度的60%,会基于explicit指定水平线的方式进行指定位置之下的表格进行获取,获取后判断是否替换当前的表格
  4. +
  5. 对文本数据进行优化处理(主要用于解决完全无框的数据或其他表格行数据粘连的情况)
    +对现有的文本进行处理,主要处理思想为对文本间距超过10个像素的同行数据追加时增加空格;对于换行数据,判断是否为段落, 符合段落条件的合并为一个一行数据
  6. +
  7. 对行列数据的进一步处理(如判断数据合并,删除空行空列,判断删除错位的空列等)
  8. +
  9. 对页眉数据的进一步优化,增加判断是否存在页眉线,如果存在页眉线以上数据作为页眉处理
  10. +
+

年报数据主要包括公司的基础信息,例如公司名称、注册地址等,财务的数据,主要都是表格,利润表负债表等等,以及综合信息部分,包括财务指标业务概要等

+

有人开源了处理pdf文件的代码,基于pdfplumber将pdf年报提取成为txt文件,表格也作为一行一行的文字存储在txt文件中。

+

有一些表格txt提取的不太好,我们使用html文件进行了二次提取,有几个年报是图片形式的,我们使用paddleocr进行提取。最终将提取到的表格存储在csv文件中,文字以txt的形式保存

+

同时将提供的计算公式引入,将计算的字段名称直接作为已知条件插入到数据库中

+

问题分类

+

对问题进行结构化解析,提取公司名称、年份和问题的关键词(研发费用等词语),模拟多轮对话的方式,使用In Context Learning的关键词抽取方案,无需微调,保留大模型的通用能力,拓展性和灵活性更好。

+

然后将问题与关键词一起输入到大模型中,训练一个Lora对问题进行分类(训练大模型做选择题),我们人工对给定的问题进行标注,将其分为基本统计题目,计算题目,SQL题目,根据文档的开放性问题和纯开放性问题。由于问题的特征都比较明显,因此这个Lora是非常好训练的,准确率可以达到99%之上。

+

NL2SQL+正则匹配解决统计分析问题

+

基本统计题目和计算题目直接通过关键词与csv文件中的标题字段名等进行匹配,定位到正确的关键词,通过向量相似度和编辑距离进行匹配(金融特有词汇使用向量语义匹配的效果不是很好)

+

答案通过规则的方式进行生成(也就是只需要找到正确的数字)

+

训练一个LoRA解决复杂的SQL问题(哪家在上海注册的上市公司,2020年营业收入最高?金额是?)

+

提供的数据量不够,大模型对微调数据质量要求非常高,重要性在数据量之上。使用GPT-4+人工修改的方式进行数据集的扩充。

+

生成SQL后实际进行运行,运行不通重新进行生成(通过分词和 sqlparse 校对 SQL ,并编写算法)

+

多路检索召回解决根据文档的开放性问题

+

基于向量语义匹配的广度优先搜索

+

首先语义匹配数据库字段,再语义匹配文档树中章节标题节点,最后深度向量语义检索文档树子叶,或全文向量语义检索,保证系统的泛化和可靠

+

如果未能精准匹配到章节标题,扩大检索范围,检索faiss向量数据库,召回top-n条文本块。

+

将文档解析成文档树,根据提取出来的关键词,通过BM25与向量检索两者融合选出相关的top5文档块,输入到大模型的上下文中作为prompt让大模型直接进行回答

+

设计Prompt解决纯开放性问题

+

简单设计了一个prompt让大模型回答纯开放性问题

+

CodeQwen

+

简历内容

+

使用大规模公开的代码数据集提升通用模型的代码能力,同时 利用GPT-4扩充 “代码修复”任务相关数据,并参考测试集的Prompt****组织方式 ,对Qwen1.8B模型进行参数高效微调;

+

采用多任务学习的微调方式,添加额外语义相关任务进行 联合训练 ,显著提升代码能力,同时探索最佳训练推理参数。初赛Pass@1为0.2594(第一名),复赛Pass@1为0.3001。

+

介绍

+

任务:使用Qwen系列的大模型生成可以实际运行的实际代码,探索Qwen大模型的代码生成的能力

+

初赛任务:只允许使用Qwen 1.8B模型,在MFTCoder训练框架上进行数据收集与微调,评测指标是pass@1,使用humanevalpack和mbpp的benchmark进行评测。

+

评测指标:humanevalpack是人工编写的164个问题的代码解决方案,每一个问题都用六种编程语言实现。这些代码会被组织成三种任务,包括代码编写、代码解释和代码修复,这个比赛在评测的时候只评测代码编写和代码修复的任务。mbpp是python代码生成任务,数据量要稍大一些。评测指标使用pass@1,就是让模型生成一个解决方案,然后将代码放到可以实际运行的环境中进行编译执行。如果这个代码可以通过所有的测试样例,证明这个解决方案正确;反之如果不能通过任何一个测试样例则认为这个解决方案是错误的。它不单单关注代码的语法是否正确,更重视代码能否正确执行功能,这种评估方式更符合实际编程工作中的要求。
+数据集:数据集是可以自行收集的,比赛中给出了建议使用的CodeExercise-Python-27k和Evol-Instruction-66k,但是不允许使用humanevalpack或者mbpp这些测试数据进行训练。

+

复赛任务:使用Qwen-72B进行微调,不需要自行操作,使用阿里平台提供的api进行在线训练,我们只能上传数据和模型的参数,具体的训练方式是不可见的,训练结束之后我们可以通过api自行进行代码生成效果的测试。提供的验证集A榜是Leetcode上面的50道问题,验证集B榜是更大规模的Leetcode的数据。数据限定问题都只是中文的语言描述,最终只会要求生成Python的代码。

+

初赛的亮点:数据组织与生成、多任务学习、参数尝试

+
    +
  1. 数据组织与生成
    +数据生成:我们在huggingface上面找了很多相关的代码数据,针对测试集的问题,从这些数据集中过滤出仅为英文的数据。由于Python的数据集比较多,其它语言的数据集比较少,我们使用GPT-4让其将Python语言的数据翻译成其他语言的数据,然后利用主办方提供的评测镜像判断其是否能正确运行,仅保留可以正确运行的生成的数据。这样就构造了humanevalpack的代码生成任务和mbpp的任务所需要的数据。由于代码修复的数据集几乎没有,我们在第一步生成的数据的基础上让GPT-4去在代码中产生与humanevalpack数据集类似的bug,这样构造了代码修复的数据。
  2. +
+

数据组织:按照humanevalpack和mbpp的prompt的组织方法,将不同数据集的组织形式更改成与他们尽量相似的形式,使得训练过程与测试过程尽量相似。

+
    +
  1. 多任务学习:主办方提供了MFTCoder的大模型多任务学习的框架帮助我们进行训练。我们参考了微软的PHI-1的训练方式,这个小模型在humaneval上能够达到51%的准确率
    +PHI-1使用的数据集有三种,第一个是从stackoverflow中提取的代码-语言数据集,包括Python代码和相关的自然语言的注释,大概有6B的token,第二个是教材数据集,是Python教材的自然语言文本和相关的代码片段,大概有1B的token,最后是Python编程练习和相应的解决方案,大约180M个token。PHI-1首先在前两个数据集上面进行预训练,最后在最后一个数据集上面进行微调,最终达到了对一个小模型而言非常好的效果。
  2. +
+

MFTCoder是一个多任务的学习框架,可以接受不同组织形式的输入同时进行训练,得到多个loss的汇总结果。借助PHI-1的思想,我们除了让其通过写代码的方式进行训练之外,也让其学习对代码进行解释。因为在代码数据中会存在一些注释,正常在我们写代码的时候注释会给我们很大的帮助。因此除了写代码的任务之外,我们还加入了对代码进行解释的任务,实验效果表明确实会增强写代码的效果。

+

为了解决数据量不平衡的问题

+
    +
  • MFTCoder会确保在单个epoch内所有任务的每一个样本都被使用且只使用一次。
  • +
  • 为了避免模型偏向具有较多数据的任务,我们在损失计算过程中引入了权重分配策略。 +
      +
    • 一种基于任务样本数量
    • +
    • 一种基于纳入loss计算的的有效Tokens数量。(样本数量与有效Tokens数量具有极端差异的任务(例如"是"或"否"回答的二元分类任务或单项选择考试任务)时可能表现不佳)
    • +
    +
  • +
  • 为了解决任务难易不一的问题,借鉴了Focal Loss的思想,并将其纳入到MFTCoder中。实现了两个不同层次的Focal Loss函数,以适应不同的细粒度。一个在样本级别操作,另一个在任务级别操作
  • +
  • 为了解决收敛速度不一致的问题,借鉴了FAMO方法的思想,并创新地将其应用于计算validation loss。首先,我们假设每个任务(以索引i表示)都有自己的原始损失函数Li(θ)。在第t次迭代中,我们根据对应任务的validation loss的梯度来更新每个任务的权重,目标是最大化收敛速度最慢的任务的权重w_i,为了确保任务以相似的速度收敛,我们引入了一种动态平衡机制。在每次迭代中,我们根据任务的validation loss梯度更新任务特定的权重。该方法旨在给予收敛速度较慢的任务更多的关注,使其对整体优化过程产生更大的影响
  • +
+
    +
  1. 参数尝试:我们尝试了许多不同的参数组合,如Lora参数、学习率、不同的ckpt等。最终取了评测最好的效果提交。learn rate 1e-4, batch size 4, max length 4096, epoch 3,选最好的epoch提交
  2. +
+

复赛:
+复赛的时候是通过api的方式提交进行训练,我们并没有在训练技巧上面的发挥空间,只能在训练数据和训练参数上面简单尝试。在72B模型上,我们测试zero-shot能力已经达到了0.5,由于模型已经具备较强的推理能力,想要在特定任务上获得更好的模型效果,需要加入特定任务的高质量的数据集。因此我们使用了公开的Leetcode中文数据集(由于事先告知全部为中文Python评测数据),同时也加入了代码解释的任务。由于已知测试集都是中文数据,因此并没有加入英文的数据进行训练。同时我们制作了本地的单样例的评测脚本,使用Leetcode上面公开的测试样例进行测试,与线上结果相差10个点左右(50道题中本地测试要比线上测试多正确5道题目)。最终版本模型加入了测试集A榜的数据进行训练,最终模型在B榜的效果也不错,在10支决赛队伍中排名第5。

+

训练参数:直接zero-shot的方式性能有0.5,尝试过增大lora_rank,增大epoch,效果下降。

+

百度搜索

+

简历内容

+

使用Baichuan2-7B大模型进行LoRA微调,配合数据增强、风格适应、NEFTune、探索最佳推理参数、集成学习等策略,完成对多文档搜索摘要进行组织的任务,性能达到0.5032。

+

介绍

+

任务:针对用户查询检索返回的不超过5个的文档,利用生成模型进行组织,生成一个正确、语义通顺、满足用户需求的答案。
+评价指标:BLEU-4与ROUGE-L,BLEU-4侧重于衡量答案的准确性和精确匹配程度,类似于精确度Precision,ROUGE-L更侧重于衡量答案的信息完整性和涵盖程度,更类似于召回率Recall。
+数据集:百度提供的数据集,训练集、验证集、测试集分别包含8000条、1000条和1000条查询及从网页搜索结果中抽取的摘要,除了测试集外都包含了人工撰写的答案。
+限制条件:模型可以在单卡A100使用最多40G显存完成推理,允许使用外部数据集但不允许使用api,如chatgpt等直接进行回答。

+

我们选择了合适的大模型基座,基于LoRA进行微调,对于训练、推理参数进行了调优,并尝试了噪声微调、知识蒸馏、等方式进一步提升模型性能。

+

模型选择:我们进行了一些实验进行基座模型的选择,包括不同模型尺寸的Baichuan2,Qwen,ChatGLM-3,Lingowhale等尝试进行相同配置下的训练与推理,最后发现Baichuan2-7B和Qwen-14B的效果比较好。此外我们利用Baichuan2-7B模型比较了Base和Chat权重对于模型性能的影响,发现Base权重要好于Chat权重,可能是由于我们的任务形式相对较为单一,可能并不需要Chat模型多轮对话、理解问题意图的能力。

+

性能调优:我们主要在Baichuan2-7B上对训练和推理时的一些重要参数进行选择,训练时需要考虑的主要是LoRA的秩的大小,它决定了可训练参数的量,但是我们实际发现一些比较常用的秩,比如8和16的微调效果都差不多。在推理参数上,我们发现温度系数对于模型效果的影响十分显著,较大的温度系数使得预测输出分布更为均匀,更多的词有机会被输出,继而生成答案也更加丰富多样,但同时也有偏离主题的风险,而较小的温度系数使得预测输出分布更加尖锐,模型输出的准确性和真实性更高一些,实验发现0.7左右的温度系数可以取得较好的表现。此外,topp和topk等采样参数也会在一定程度上影响推理表现,但是我们发现没有温度系数影响的更为明显,因此我们维持了默认参数。

+

训练的时候打乱检索摘要能提升两个点 不打乱的话loss降的很快

+

推理的时候我们还采取了风格适应的策略,在prompt里面给几个例子让它照着生成,并且在推理结束之后让模型自行反思推理的质量。

+

之后,我们尝试纳入更多的训练数据来提升模型的表现,主要想法是搜集一些问答数据集,然后利用一个在原训练集训练好的大语言模型产生回答作为伪标签,得到的问题-摘要-伪标签对作为新数据与原训练集合并,然后指导一个模型从零开始训练。在这里,我们称产生伪标签的模型为教师模型,而从零开始训练的模型为学生模型,当教师模型与学生模型相同时,相当于一个同样容量的模型在自举地生成数据,即Bootstrap,当教师模型比学生模型能力更强、容量更大时,相当于通过蒸馏,即Distillation的方式在两个模型之间传递知识。

+

对于上述训练流程,最重要的是如何选择与所提供数据集分布较为一致的数据集,经过调研,我们选择了百度整理的WebQA中文问答数据集作为额外的训练集,选择它的一个重要原因是相较于其它数据集,WebQA对于每个问题都有多个从百度知道等渠道返回的检索摘要,虽然数据集当中也有标注的问题答案,但是考虑到其可能与本次竞赛人工撰写答案存在分布不一致的情况,因此我们舍弃了这些答案,而使用前述的伪标签生成的方式重新生成答案。此外,我们也对数据集进行了简单的自动化清洗,比如限制问题对应的检索摘要数目大于等于3条小于等于5条,并去掉了其中总字数过短的样本。之后,我们利用合并后的数据集对于不同设置下的模型进行重新训练,使用自举,也就是保持师生一致的情况下,模型性能的提升较小,而采用性能较好、参数量更大的教师模型向性能稍差、参数量更小的学生模型蒸馏知识时,则可以较为显著地提升学生模型的性能,但是其性能相较于规模更大的教师模型还有一定差距。因此,我猜想如果我们获得了性能更好、但是由于参数量过大而无法在40G A100 GPU 上完成推理的模型,是否也可以通过这样蒸馏的方式获得一个效果稍差一点、但是参数尺寸符合参赛要求的模型呢。

+

之后,我们使用了一系列技巧尝试进一步提升模型的性能,首先尝试的是NEFTune,其想法来源于Noisy Embeddings Improve Instruction Finetuning这篇文章。主要方法较为简单,就是在训练过程中对于输入指令的问题部分还及多条检索摘要部分的嵌入表示随机添加均匀采样的噪声,希望以此提升模型的鲁棒性,缓解过拟合,同时,也有研究表明其可以使得生成结果包含额外的细节,增强答案的丰富性。实验发现,NEFTune可以较大地提升7B模型的表现,但是对于13B模型性能提升则相对较少,除此之外可以看出,这种方法对于ROUGE-L,也就是召回,提升更为明显,说明模型生成的答案更多地包含了之前所没有的细节。

+

最后我们采取了一系列的集成策略,首先是由于我们将temperature设置的很高,所以我们获取结果的时候推理多次,然后取这个结果中与原始的材料重合度最高的作为我们的最终答案。

+

小样本文本分类竞赛

+

简历内容

+

赛题目标:对小样本训练数据的中文专利文本进行分类,数据包括标题、发明人和专利摘要,共36个数值类别(类别标签 无语义信息 ),训练集: 测试集≈1: 20,评价指标为F1-Score;

+

提出基于类别动态阈值伪标签尾部类别数据增强的小样本文本分类方法,采用恰当的Prompt组织策略,使用ERNIE-3.0中文预训练模型微调文本分类任务,利用无标签的大量测试集数据标注 伪标签 、采用多语言回译等文本数据增强方法并加入对抗训练策略提升模型的鲁棒性,在A榜测试集分数为0.6516,在B榜测试集分数为0.5955。解决方案在Github开源

+

数据及评价标准

+

数据是专利文本数据,包括专利名称、专利发明单位(一般是企业)和专利的摘要,最后有一个专利的类别标签。专利的类别标签共有36个,是数字形式的,也就是说从标签本身是无法获取标签的语义信息的。训练集数据总共900条左右(带标签),测试集(不带标签)A、B榜数据分别都是两万条左右。在比赛的前两个月是可以获得训练集和A榜测试集的,其中A榜测试集的预测标签每一天可以在网站上提交三次,网站会返回这个测试集的得分。评价标准是宏平均的F1-Score,即求出每一个类别的F1-Score后取平均(也就是除以36)。最后一天开放B榜的数据,只能提交一次且无法看到自己的成绩,比赛结束后统一公布B榜成绩,按照B榜成绩决定团队的名次。

+

主要方法

+

预训练模型+伪标签+尾部类别数据增强+对抗训练+模型集成

+

预训练模型

+

尝试了一些huggingface上面的预训练模型,包括bert-chinese、mengzi、macbert、roberta-wwm等等,最终我们发现使用百度的ernie作为主干效果最好。

+

伪标签

+

因为我们的训练数据比较少,因此在训练集上训练第一个模型后,我们对测试集进行了标签的预测,根据预测的softmax分数打了测试集的伪标签。

+

在长尾识别任务下,尾部类别虽然召回率Recall较低,但却有着较高的精确度Precision,而头部类别则相反。这启发我们根据标签分布情况及模型预测情况为每一个类别设置不同的阈值:头部类别较多,模型学习情况较好,应为其设置较高的阈值,保证其精确度;尾部类别较少,模型学习情况较差,应为其设置较低的阈值,从而获得较高的召回率。

+

对于每一类别,我们先得到预测标签为该类的所有记录,之后将这些记录按照对于该类预测概率由大到小进行排序,进一步地,我们求其分位数(即前大的预测概率)作为该类别的阈值,同时,若求得的阈值小于一个预先给定的固定阈值,则将其重新置为。利用分位数筛选伪标签可以充分地考虑到各个类别的学习情况(头部类别学习情况较好,通常模型预测的置信度较高,而尾部类别则相反),同时固定阈值γ作为下限,使得尾部类别的筛选不至于混入过多的噪声样本。

+

这种方式的伪标签产生流程可以迭代进行,即训练得到一个较好的模型A,利用其产生的伪标签再训练一个模型B,再利用模型B对无标签测试数据打伪标签,并由此再训练一个新的模型C。在这个过程中我们会将标准定的越来越严格。最终的训练数据大概在1万5左右。

+

提升大概是从0.55-0.62左右

+

尾部类别数据增强

+

为了缓解类别不平衡的问题,我们对尾部类别(类别12、22、32、35)应用两类数据增强方法。

+
    +
  1. 回译:我们利用谷歌翻译接口,将中文翻译为英法德日韩5种语言,再翻译回中文,得到新的样本。
  2. +
  3. 简单文本数据增强:由4种不同的方法组成 +
      +
    1. 同义词替换(从句子中随机选择个非停用词,随机选择它们的同义词进行替换)
    2. +
    3. 随机插入(从句子中随机选择一个非停用词的单词,随机选择它的一个近义词并将它插入在句子的任意位置。并将此过程重复次)
    4. +
    5. 随机替换(随机选择句子中的两个单次并将它们交换位置。将此过程重复次)
    6. +
    7. 随机删除(对句子中的每一个单词,都以一给定概率判定此单词是否被删除)。
    8. +
    +
  4. +
+

提升大概是0.62-0.63左右

+

其他的数据增强方法:

+
    +
  1. Paraphrasing:对句子中的词、短语、句子结构做一些更改,保留原始的语义 +
      +
    1. Thesaurus:利用词典、知识图谱等外部数据,随机将非停用词替换成同义词或上位词,如果增加多样性的话还可以替换成相同词性的其他词
    2. +
    3. Semantic Embeddings:利用语义向量,将词或短语替换成相近的(不一定是同义词)。由于每个词都有语义表示,可替换的范围更大。而上一种方法只能替换图谱里的
    4. +
    5. MLMs:利用BERT等模型,随机mask掉一些成分后生成新的
    6. +
    7. Rules:利用一些规则,例如缩写、动词变位、否定等,对句子一些成分进行改写,比如把 is not 变成 isn’t
    8. +
    9. Machine Translation:分为两种,Back-translation指把句子翻译成其他语言再翻译回来,Unidirectional Translation指在跨语言任务中,把句子翻译成其他语言
    10. +
    11. Model Generation:利用Seq2Seq模型生成语义一致的句子
    12. +
    +
  2. +
  3. Noising:在保证label不变的同时,增加一些离散或连续的噪声,对语义的影响不大 +
      +
    1. Swapping:除了交换词之外,在分类任务中也可以交换instance或者sentence
    2. +
    3. Deletion:可以根据tf-idf等词的重要程度进行删除
    4. +
    5. Insertion:可以把同义词随机插入句子中
    6. +
    7. Substitution:把一些词随机替换成其他词(非同义),模拟misspelling的场景。为了避免改变label,可以使用label-independent的词,或者利用训练数据中的其他句子
    8. +
    9. Mixup:这个方法最近两年比较火,把句子表示和标签分别以一定权重融合,引入连续噪声,可以生成不同label之间的数据,但可解释性较差
    10. +
    +
  4. +
  5. Sampling:旨在根据目前的数据分布选取新的样本,会生成更多样的数据 +
      +
    1. Rules:用规则定义新的样本和label,比如把句子中的主谓进行变换
    2. +
    3. Seq2Seq Models:根据输入和label生成新的句子,比如在NLI任务中,有研究者先为每个label(entailment,contradiction,neutral)训一个生成模型,再给定新的句子,生成对应label的。对比之下,paraphrasing主要是根据当前训练样本进行复述
    4. +
    5. Language Models:给定label,利用语言模型生成样本,有些研究会加个判别模型过滤
    6. +
    7. Self-training:先有监督训练一个模型,再给无监督数据打一些标签,有点蒸馏的感觉
    8. +
    +
  6. +
+

对抗训练

+

对抗训练是一种引入噪声的训练方式,可以对参数进行正则化,提升模型鲁棒性和泛化能力。

+

对抗训练的假设是:给输入加上扰动之后,输出分布和原Y的分布一致

+

往增大损失的方向增加扰动

+

在计算对抗扰动时虽然计算了梯度,但不对参数进行更新, 因为当前得到的对抗扰动是对旧参数最优的

+

用一句话形容对抗训练的思路,就是 在输入上进行梯度上升(增大loss),在参数上进行梯度下降(减小loss)实际的做法是在embedding table上进行梯度上升

+

接下来介绍不同的方法,后续方法优化的主要方向有两点:得到更优的扰动 & 提升训练速度

+

FGM

+

对于每个x:(输入的梯度是g)

+
    +
  1. 计算x的前向loss、反向传播得到梯度
  2. +
  3. 根据embedding矩阵的梯度计算出r,并加到当前embedding上,相当于x+r
  4. +
  5. 计算x+r的前向loss,反向传播得到对抗的梯度,累加到(1)的梯度上
  6. +
  7. 将embedding恢复为(1)时的值
  8. +
  9. 根据(3)的梯度对参数进行更新
  10. +
+

PGD小步走多走几步

+

模型集成

+

训练模型的时候采用了5折交叉验证的方式,因此实际上每一次训练后我们可以得到6个推理结果,分别是五个小模型的结果和最终投票的结果。但是我们在实际提交的时候发现,并不总是最终投票的结果的得分最高,有可能是其中的某一折或某几折的效果更好。因此通过前期多种模型的线上提交返回的F1分数,我们最终选择了效果较好的9个模型作为参赛系统的最终结果。9个模型的具体参数细节各不相同,训练数据也采用了不同阶段得到的伪标签。同时为了满足模型的总大小不超过2G的要求,我们将单精度模型转换为半精度,实验表明这种转换对于最终的推理结果影响不大。最后我们通过投票法将9个模型集成在一起得到最终的推理结果。

+

最终A榜成绩是0.65159296/0.65780034,评测后的B榜成绩是0.59547145/0.61387745

+

商汤科技

+

简历内容

+

项目目标:进行场馆级别多相机跟踪与动作捕捉研究:通过相机架设、目标感知、多视角多目标匹配、三维关键点重建、动作分析等模块,最终实现篮球场景下的动作分析和技术统计;

+

辅助完善多相机匹配模块,在多相机感知的基础上利用多种特征、考虑时间连续性与相机的空间连续性,优化层级式匈牙利匹配算法,最终离线性能达到 **99.5%**以上

+

独立设计实现相机架设优化算法模块,满足全链路中 不同模块的需求 ,与原始的相机架设相比性能提升 50%以上 ;并设计小工具辅助相机架设算法在实际场景中进行验证,效果良好。

+

整体介绍

+

项目的整体目标是完成球类运动(主要是篮球)的动作分析和技术统计,动作分析包括球员动作的标准程度等,技术统计包括球员得分、球员跑动距离、球速统计等等。背景是选择一个篮球场馆,然后架设多台相机,通过相机实时拍摄到的视频流进行实时的效果输出。

+

项目整体分为五大模块:

+
    +
  1. 相机架设和标定:这个模块是在做后面模块后添加的,因为我们发现相机的架设位置也可以影响后面模块的性能。
  2. +
  3. 感知:通过深度学习的方法,尽量准确的识别,检测目标,最终得到目标表征,具体来说是球员的检测框、球员身体的2D的21个关键点和模型根据这个检测框提取出来的一个向量形式的特征。
  4. +
  5. 匹配:通过感知得到的各种信息,在时间和空间上将同一个人匹配在一起。在时间上是这一帧与下一帧的球员进行匹配,空间上是在场地四周架设的相机之间进行匹配,一个匹配序列称为一个tracklet,最终得到每一个球员的tracklet。
  6. +
  7. 重建:利用相机,感知的2D关键点和匹配的信息,计算出球员的3D关键点的信息,也就是得到一个球员在3D世界中的动作,在电脑上观看就是火柴人在录像中打球的模拟视频。
  8. +
  9. 动作分析:对人体关键点进行分析,获得球员的动作信息,例如球员的投篮一瞬间的手臂的夹角,球员的脚有没有踩到边界之外等等。这个也是传统方法,对上一步的3D关键点进行进一步的分析。
  10. +
+

除此之外,在感知的层面还需要对篮球单独跑一个模型进行检测,同时项目后期我们加入了人脸的检测,在感知到的人体的框中切割出人脸的部分并进行人脸识别,后面准备也添加进去作为一种特征。我们在算法研究的时候是采集了小部分的数据,包括三个场馆的不同数量的球员的打球的视频,请外部团队进行标注后供我们进行算法研究。后期我们在实际的篮球场馆中进行验证,主要是根据实际情况对一些超参数进行调整。最终的效果达到了验收的标准。

+

匹配模块

+

上一层感知模块的输出包括人体的检测框,深度学习模型提取出来的人体的唯一特征向量,和检测出来的人体的21个关键点(平面关键点)。关键点的信息没有使用,主要是使用了检测框和提取出来的特征向量。同时也通过相机的内外参计算检测框的底边中点在真实世界中的位置,作为另外一个特征。

+

运用信息及评价标准:(归一化到0-1)

+
    +
  • 2D检测框:IOU,交集/并集,两个框越接近IOU越大
  • +
  • 特征:归一化的向量,使用cos相似度进行度量(1-cos)/ 2
  • +
  • Homo点:使用欧氏距离,1-exp的方式
  • +
+

算法流程:第一张图片有几个检测框就认为有几个tracklet,然后外层循环遍历全部帧,内层循环遍历全部相机,将一帧、一个相机中的检测框与这些tracklet进行匈牙利匹配。匹配的过程中会包含一个阈值,因此会有匹配不上的,或者前期有可能检测框的数量多于tracklet的数量。产生了这些情况就新建一个tracklet。最终选择匹配到的数量最多的球员数量的tracklet作为该球员的tracklet。

+

我们将我们使用到的特征进行了分类:

+
    +
  • 长时匹配(跟踪):使用长时不变的特征保证长时性能,利用tracklet前T-1帧,特征归一化后的算术平均值
  • +
  • 短时匹配(跟踪):使用短时不变,相似性高的特征保证短时性能,利用tracklet第T-1帧匹配的bbox框,homo_point
  • +
+

同时探究了空间连续性对相机匹配顺序带来的影响,因为一个相机对于它的相邻的相机的重合部分更高,用两个重合度更高的相机进行匹配的准确率也会更高。

+

核心代码:

+

这段代码的主要目的是预测视频中物体的轨迹。具体来说,它首先加载视频数据和特征点,然后对每一帧进行处理。在处理每一帧时,它会提取出人脸、特征点、轨迹等信息,并根据输入的匹配函数进行匹配。匹配结果会被存储在 new数组中,最后将预测结果用于评估。

+

以下是代码的主要步骤:

+
    +
  1. 加载视频数据和特征点:使用 np.load()函数加载 homopathtrackpathnorm_feature_pathid_feature_path中的文件,分别得到特征点矩阵 track_np1norm_feature_np1id_feature_np1id_feature_path
  2. +
  3. 初始化变量:定义一些变量,如时间戳 T1、最大跟踪数 max_track_num、布尔值 humannum等。
  4. +
  5. 遍历每一帧:对于视频中的每一帧,首先获取当前的时间戳 frame,然后根据 framenumcamnum计算出当前帧的最大跟踪数 max_track_num
  6. +
  7. 提取人脸、特征点和轨迹信息:对于每一帧,首先计算当前帧的人脸特征向量 featurelist,然后提取特征点 featurelist和轨迹信息 tracklist
  8. +
  9. 匹配过程:对于每一帧,首先计算单应性矩阵 homomat,然后根据输入的匹配函数 match_function进行匹配。匹配结果会被存储在 new数组中。
  10. +
  11. 更新变量:在每一帧处理完成后,更新相关变量,如 countcount2等。
  12. +
  13. 保存匹配结果:将预测结果 new保存到文件中,以便后续评估。
  14. +
+

相机架设模块

+

在原始的相机架设方案中,相机的架设一般都是纯人为架设的,通俗来说,就是简单观察一下球场,让相机尽量的平均分布一下,相机的位置和朝向等全凭借着在场人员的经验进行实施。整个的过程没有任何数学上的依据,比较简单粗暴。

+

我们将相机架设的问题看成一个最优化问题,最优化问题主要需要明确下面的四个问题:设计变量、目标函数、约束函数和优化方法

+

设计变量即是相机的架设可变的量,也就是相机的架设相关参数。

+

对于单个相机来说,分为外参和内参的变化。

+
    +
  • 相机的内参在确定相机的型号后使用焦距进行控制,相当于只有1个变量
  • +
  • 相机的外参有6个变量,包括相机架设的空间位置,以及相机的架设角度
  • +
+

目标函数是变量到评价标准之间的映射模型,使用其来评估相机架设的好坏,可以从感知,匹配以及重建,或者功能的好坏等方面来进行评估。

+

首先对于感知来说,对于人体的大小和人脸的大小有要求,也就是适合感知的区域要尽可能大。因此需要满足人体和人脸大小的有效区域尽可能大,同时要考虑是否能够覆盖全场。其次对于匹配来说,对于人体大小和人体拍摄视角有一定的要求,这些会影响特征提取,框的重叠程度,homo点的误差等。同时对于相机之间的空间连续性有一定的要求,相机拍到的非目标区域要尽可能少,目标区域要尽可能大,使得相机的拍摄范围内基本都是我们关注的球员,减少其他场外人员的干扰。最后考虑重建的方面,同一个区域是否有多个相机可以看到,如果没有几个相机拍摄到同一个位置就很难进行重建。并且相机和相机,相机和目标之间的角度关系是否合适。最终希望适合重建的区域要尽可能多,适合重建的相机也要尽可能多。

+

约束函数也就是相机架设的物理限制,比如说相机架设的高度,架设的数目等等。

+
    +
  • 具体的架设环境决定:相机只能架设在球场的边缘,因此中一定有一个的搜索空间比较小;相机只能架设在已有的墙上,已有的屋顶上,会对产生一些约束。除此之外还会有客户对于架设提出的约束要求,以及在实际架设相机时,架设的困难和容易的程度。
  • +
  • 对称性约束是比较符合常理认知的一种约束条件。如果在一个角落架设了一台相机,很自然在另外一个对称的角落也会架设另外一台相机;如果架设了一个照射半场的相机,很自然也应该架设另外一台照射另外一个半场的相机。
  • +
+

定义了优化问题之后,可以使用一些优化方法来进行优化。我们采用的是遗传算法进行优化。

+

可视化论文

+

一种展示中长期信号时变特征的新的视觉抽象方法,负责数据处理、时间片划分算法的损失函数并设计算法效果评价标准,发表二作专利、一作软著并协助撰写部分论文。论文地址

+

项目目标:设计一种新的视觉抽象方法,可以在有限屏幕空间内显示中长期无线电信号的时变特征。

+

承担工作:设计了系统核心部分的时间片划分算法及其对应的损失函数,创新了无线电信号的度量方法。

+

项目背景:如何在有限屏幕空间内显示中长期无线电信号的时变特征,无线电信号只有0和1两个数值

+

视觉抽象设计:采用五种基本图形编码无线电信号,菱形表示短联,正方形表示长联,空白表示中断,长方形+三角形表示先出联后消失,X形表示中间中断两边出联。异常使用红线表示,横竖两种红线表示不同的异常种类

+

时间分割算法:平均分算法+微调 优化目标

+

优化目标:

+
    +
  • 数据分布和空间分布的相似性:将视觉的编码通过面积的方式转化为01信号,与实际的01数据取差的绝对值
  • +
  • 占空比的差异:无线电信号出现的时间比例:一个信号的占空比与划分的视觉编码的平均占空比的差值
  • +
  • 时间片的变异系数:标准差/均值,测量时间片对应真实时间差异的离散程度
  • +
+

开发

+

简历内容

+

参与推荐广告、激励视频广告、用户特征等后端服务研发;使用Hertz、Kitex等新框架升级旧系统架构,引入泛型等新特性降低服务资源消耗3-4%;协助周末假期等高峰期的故障排除。

+

字节跳动青训营 抖音项目 Redis和MQ相关

+

由于服务端直接与数据库交互导致响应客户端时间过慢,因此在完成点赞模块基本功能的基础上,增加缓存减少数据获取的时间从而减少响应时间,使用具有高性能特点的Redis作为缓存数据库。考虑到实际情况下,用户在客户端刷视频时,使用率最频繁的是点赞、取消赞功能,因此在变更点赞状态时,直接更新缓存中的数据进行返回响应,提高用户刷视频的流畅度。同时查询点赞数量、获取点赞列表、判断是否点赞、点赞总数、被点赞总数都可根据缓存中的已有数据进行性能优化

+

在大量用户同时使用客户端的情况下,会出现请求数量过大导致数据库压力过大、处理能力下降甚至是宕机的问题。因此,设计在服务端与数据库操作间加入消息队列。计划使用具有较高可靠性和稳定性的rabbitMQ作为消息队列。当需要对数据库进行操作时,将数据库操作放入消息队列,服务端取出消息队列中的信息,在对数据库操作后再取出下一个信息进行处理,从而避免数据库在一段时间接收大量响应出现异常,同时为了避免数据库操作失败,设置了update失败重试机制。

+

最初的设计是客户端在发起请求后,需要在Mysql的likes表中添加或者更新相应点赞关系的信息,响应时间较久,影响客户体验。

+

优化方案:考虑将这些信息存进缓存中,当点赞或者取消赞的时候,只需要添加或者删除对应的信息即可,响应时间更快,再采用往rabbitmq中传递需要修改数据库的关键信息,通过协程的方式修改数据库。

+

设计两种Redis存储信息内容:

+
    +
  • 第一种key(userid)-set(videoId):存储用户点赞的视频id;
  • +
  • 第二种key(videoid)-set(userid):存储给对应视频点赞的用户id;
  • +
+

当进行点赞或者取消赞的操作时,同时维护这两种数据结构。这边考虑到脏读的情况,所以每个key首次加载的过程中,加入永不读取或者更新的值“-1”。

+

使用缓存进行性能优化提升后,需要解决一个新的问题,即数据的脏读问题。在多个用户进行删除、读取评论操作时,若某一个操作恰好将缓存中的数据清空,而此时数据库还未来得及对数据做出同样的操作(脏数据),此时,读取数据的用户在读取不到缓存中信息的情况下,很可能将数据库中的脏数据重新写入缓存,此时就会出现大量用户读取到脏数据且无法恢复正确值的情况。

+

针对上述情况,计划对缓存中的键值设置一个固定的、不会被删除的id值。经过项目组成员讨论一致决定,使用-1作为首次添加新key时对应的预设value值。

+ + +
+ +
+
+ + + + + + +
+
+
面试项目准备
+
https://zhangzhao219.github.io/2022/12/02/Interview/Interview-Questions-project/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/02/Interview/Interview-Questions-redis/index.html b/2022/12/02/Interview/Interview-Questions-redis/index.html new file mode 100644 index 000000000..efa54f075 --- /dev/null +++ b/2022/12/02/Interview/Interview-Questions-redis/index.html @@ -0,0 +1,903 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Redis面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Redis面试题准备

+ + +
+ +

Redis面试题准备

+ +

关系型数据库和非关系数据库的优缺点

+

关系型数据库

+

优点:

+
    +
  • 二维表格,容易理解
  • +
  • 操作方便
  • +
  • 易于维护
  • +
  • 支持SQL
  • +
+

缺点:

+
    +
  • 读写性能较差
  • +
  • 固定的表结构,不够灵活
  • +
  • 应对高并发场景,磁盘I/O存在瓶颈
  • +
  • 海量数据的读写性能差
  • +
+

非关系型数据库

+

优点:

+
    +
  • 不需要SQL解析,读写性能高
  • +
  • 可以使用硬盘或者内存作为载体,速度快
  • +
  • 基于键值对,数据没有耦合性,方便扩展
  • +
  • 部署简单
  • +
+

缺点:

+
    +
  • 不支持SQL,增加了学习成本
  • +
  • 没有事务
  • +
+

Redis底层数据结构及适用场景(实习-百度-Go后端开发-2023.02.09)

+

String

+

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M

+
    +
  • SDS 不仅可以保存文本数据,还可以保存二进制数据 。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
  • +
  • SDS 获取字符串长度的时间复杂度是 O(1) 。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)
  • +
  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出 。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
  • +
+

应用场景:

+
    +
  • 缓存对象
  • +
  • 常规计数:比如计算访问次数、点赞、转发、库存数量等等
  • +
  • 分布式锁:SET 命令有个 NX 参数可以实现「key不存在才插入」
  • +
  • 共享Session信息:无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息
  • +
+

List

+

List 列表是简单的字符串列表, 按照插入顺序排序 ,可以从头部或尾部向 List 列表添加元素。

+

列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。

+

List 类型的底层数据结构是由双向链表或压缩列表实现的,在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现

+

压缩列表:由一个连续内存组成的顺序型数据结构,一个压缩列表可以包含任意多个节点,每个节点上可以保存一个字节数组或整数值。

+

quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

+

应用场景:消息队列

+

Hash

+

Hash 是一个键值对(key - value)集合

+

Hash 类型的底层数据结构是由压缩列表或哈希表实现的,在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现

+

listpack 也叫 紧凑列表 ,它的特点就是用一块连续的内存空间来紧凑地保存数据,同时为了节省内存空间,listpack 列表项使用了多种编码方式,来表示不同长度的数据,这些数据包括整数和字符串。

+

它最大的改进就是每个listpack节点中,不再保存前一个节点的长度了,所以也就不存在出现连锁更新的情况了。

+

应用场景:

+
    +
  • 缓存对象:Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
  • +
  • 购物车:以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素
  • +
+

Set

+

Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。

+

一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。

+

Set 类型和 List 类型的区别如下:

+
    +
  • List 可以存储重复元素,Set 只能存储非重复元素;
  • +
  • List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。
  • +
+

Set 类型的底层数据结构是由哈希表或整数集合实现的

+

应用场景:

+

Set 类型比较适合用来数据去重和保障数据的唯一性,还可以用来统计多个集合的交集、错集和并集等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。

+
    +
  • 点赞:Set 类型可以保证一个用户只能点一个赞
  • +
  • 共同关注:Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等
  • +
  • 抽奖活动:存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。
  • +
+

ZSet

+

Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。

+

有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。

+

Zset 类型的底层数据结构是由压缩列表或跳表实现的,在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现

+

应用场景:

+
    +
  • 排行榜:例如学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等
  • +
  • 电话、姓名排序
  • +
+

BitMap

+

Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行 0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

+

由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用 二值统计的场景

+

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

+

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

+

应用场景:

+
    +
  • 签到统计:在签到打卡的场景中,我们只用记录签到(1)或未签到(0)
  • +
  • 判断用户登录状态
  • +
  • 连续签到用户总数
  • +
+

HyperLogLog

+

统计一个集合中不重复的元素个数,但是并不准确,不过非常节省空间

+

应用场景:百万级网页 UV 计数

+

GEO

+

Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。

+

GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

+

应用场景:叫车等需要地理位置的场景

+

Stream

+

Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。

+

应用场景:消息队列

+

redis线程模型

+

Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的 ,这也是我们常说 Redis 是单线程的原因。

+

但是, Redis 程序并不是单线程的 ,Redis 在启动的时候,是会 启动后台线程 (BIO)的:

+
    +
  • Redis 在 2.6 版本 ,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
  • +
  • Redis 在 4.0 版本之后 ,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。
  • +
+

虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是 在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上

+

Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会 额外创建 6 个线程这里的线程数不包括主线程 ):

+
    +
  • Redis-server : Redis的主线程,主要负责执行命令;
  • +
  • bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
  • +
  • io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。
  • +
+

redis 中的 SDS 和 C 语言中的字符串

+

SDS比C语言的字符串多了一个SDSHDR表头。里面存放free(空闲空间)、len(已用空间)、buf(缓冲区)

+

优点:

+
    +
  1. 获取字符串长度更快。C语言获取字符串长度需要遍历整个字符串,时间复杂度为O(N),SDS的表头len成员存放了已使用空间,获取字符串长度复杂度为O(1)
  2. +
  3. 杜绝缓冲区溢出。C语言字符串本身不记录自身长度和空闲空间,容易造成缓冲区溢出。SDS表头free成员存放了空闲空间,拼接字符串前会先通过free字段检测剩余空间是否能满足,如果空间不够就会扩容
  4. +
  5. 减少内存分配次数。C语言对字符串进行增长或缩短操作,都需要重新分配内存。SDS使用了空间预分配和惰性空间释放策略,减少了内存分配次数
  6. +
  7. 二进制安全。C语言字符串遇0则止,会对文件进行截断。SDS判断字符串是否结尾的依据是表头的len成员,不会改变二进制文件。
  8. +
+

redis 中的字典

+

根据键值对的键计算哈希值和索引值,然后根据索引值,将包含键值对的哈希节点放到哈希数组的指定索引上

+

如何解决冲突:redis采用链地址法解决键冲突。每个哈希节点有一个next指针,多个哈希节点通过next指针构成一个单向链表。总是将最新的节点添加到表头

+

扩容:redis的扩容通过rehash(重新散列)实现,为字典ht[1]分配空间,ht[1]的大小为第一个大于等于ht[0].used * 2 的 2n

+

redis 缓存穿透,缓存击穿,缓存雪崩,热点数据集中失效

+
    +
  1. 缓存穿透:在缓存层和数据库层,都没有找到数据。解决方案一:把空对象缓存起来,并设置合理的过期时间。解决方案二:使用布隆过滤器
  2. +
  3. 缓存击穿:缓存中的数据在某个时刻批量过期,导致大部分的请求都会直接落在数据库上。解决方案一:针对不同类型的数据,设置不同的过期时间。解决方案二:使用分布式锁。
  4. +
  5. 缓存雪崩:缓存在某一时刻集中失效,或者缓存系统出现故障,所有的并发流量会直接到达数据库。解决方案一:保证redis的高可用,使用集群部署。解决方案二:限流降级
  6. +
  7. 热点数据集中失效 :类似于缓存击穿。热点数据同时失效,造成都去查数据库。解决方案:利用集群部署,保证redis的高可用。
  8. +
+

redis 的持久化

+
    +
  • RDB:将某一时刻的内存快照,以二进制的方式写入磁盘;如果追求备份的速度,可以忽略部分数据丢失,推荐使用RDB
  • +
  • AOF:将redis的操作日志以追加的方式写入文件;如果追求数据的完整性,可以接受牺牲备份的速度,推荐使用AOF
  • +
+

redis 如何处理事务

+

redis使用MULTI、EXEC、DISCARD、WATCH四个事务命令

+

使用MULTI开启事务,客户端可以向服务器发送任意多个命令,这些命令不会立马执行,而是被放到一个队列中,当调用EXEC命令时,所有队列中的命令才会被执行。如果在执行事务的过程发生异常,而没有执行EXEC,那么事务中的所有命令都不会被执行。至于在执行EXEC命令之后发生了错误,及时事务中某个命令发生了错误,其他事务也会正常执行,没有回滚操作

+

通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务

+

WATCH命令可以为redis提供CAS行为

+

redis 速度

+
    +
  1. 采用多路复用IO阻塞机制
  2. +
  3. 数据结构简单,操作节省时间
  4. +
  5. 运行在内存中
  6. +
+

redis 是单线程为什么还那么快?

+
    +
  1. 采用了非阻塞IO多路复用机制
  2. +
  3. 单线程操作,避免了频繁的上下文切换
  4. +
  5. 运行在内存中
  6. +
+

redis 集群

+

主从模式:主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。

+

哨兵模式:在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。为了解决这个问题,Redis 增加了哨兵模式,因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。

+

cluster集群模式:当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群 方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

+ + +
+ +
+
+ + + + + + +
+
+
Redis面试题准备
+
https://zhangzhao219.github.io/2022/12/02/Interview/Interview-Questions-redis/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/02/diary/diary20221202/index.html b/2022/12/02/diary/diary20221202/index.html new file mode 100644 index 000000000..ba3d79365 --- /dev/null +++ b/2022/12/02/diary/diary20221202/index.html @@ -0,0 +1,736 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20221202 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20221202

+ + +
+ +

2022年12月2日

+ +

最近心情不太好,自己也不怎么在状态,想写点什么东西又感觉写不出来,就随便写写吧。

+

首先是一句关心,真的当场吓到我了。现在想想,前女友上一次的关心已经都想不起来是什么时候了。最近一年一直都是我在说我到了之类的话,一丁点的关心都没有。所以我真的早就应该意识到,早就没有了当初的感觉,真的不应该弄得如此狼狈,好聚好散,哪有那么多的舍不得,只不过是自己骗自己罢了。

+

但是也就是这么一句关心吧,有点开始想入非非了。我应该清醒一下,以我的条件根本不可能的嘛,人家大美女,数学竞赛一等奖,家里有钱有势,我又有什么?就只能尽量当个朋友吧,毕竟现在能和我说话的女生也不多了。可能有个饭局,就尽量表现得好一点,留下点好印象,不要减分就可以了。

+

人家就是客气客气罢了,早有意思也不能拖到现在了,还是自己想太多了。

+

然后呢,按照自己的计划,现在应该开始找实习了,身边的人确实也都已经开始找了。但是自己又不会太多的东西,很不敢开始下一步,这几天一直迷茫,用一些其他的游戏视频等让自己暂时开心,开心后又是不断的悲伤。上课现在也是听不太明白,考试也没有什么着落,每天过的都不怎么开心。

+

无人可以倾诉,只能自己慢慢调整。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20221202
+
https://zhangzhao219.github.io/2022/12/02/diary/diary20221202/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/05/Go/Go-Project-Geecache/index.html b/2022/12/05/Go/Go-Project-Geecache/index.html new file mode 100644 index 000000000..85ea138d7 --- /dev/null +++ b/2022/12/05/Go/Go-Project-Geecache/index.html @@ -0,0 +1,1282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go项目-分布式缓存GeeCache - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go项目-分布式缓存GeeCache

+ + +
+ +

Go项目-分布式缓存GeeCache

+ +

GeeCache

+

完成的功能

+
    +
  • 单机缓存和基于 HTTP 的分布式缓存
  • +
  • 最近最少访问(Least Recently Used, LRU) 缓存策略
  • +
  • 使用 Go 锁机制防止缓存击穿
  • +
  • 使用一致性哈希选择节点,实现负载均衡
  • +
+

LRU 缓存淘汰策略

+

FIFO:先淘汰缓存中最早添加的记录

+

LFU:淘汰缓存中访问频率最低的记录,需要维护一个访问频率的表

+

LRU:最近最少使用,认为如果数据最近被访问过,那么将来被访问的概率也会更高。维护一个队列,如果一条记录被访问,则移动到队列尾端,这样保证队首一定是最近最少访问的数据

+
package lru
+
+import "container/list"
+
+// LRU cache 结构体
+type Cache struct {
+	maxBytes  int64                         // 允许使用的最大内存
+	nbytes    int64                         // 当前已使用的内存
+	ll        *list.List                    // cache链表
+	cache     map[string]*list.Element      // 查找键值对的字典
+	OnEvicted func(key string, value Value) // 某条记录被移除时的回调函数
+}
+
+// 双向链表节点的数据类型
+// 主要目的是为了删除节点后能从字典中删除该键值对
+type entry struct {
+	key   string
+	value Value
+}
+
+// 值的类型可以是任意的,定义一个空接口,实现Len()方法返回值的占用空间大小
+
+// Len the number of cache entries
+func (c *Cache) Len() int {
+	return c.ll.Len()
+}
+
+type Value interface {
+	Len() int // 包含一个方法返回值占用的内存大小
+}
+
+// 工厂模式,返回实例化的cache
+func New(maxBytes int64, onEvicted func(string, Value)) *Cache {
+	return &Cache{
+		maxBytes:  maxBytes,
+		ll:        list.New(),
+		cache:     make(map[string]*list.Element),
+		OnEvicted: onEvicted,
+	}
+}
+
+// 查找功能,在字典中进行查找,然后移动到队尾(Front)
+func (c *Cache) Get(key string) (value Value, ok bool) {
+	if ele, ok := c.cache[key]; ok {
+		c.ll.MoveToFront(ele)
+		kv := ele.Value.(*entry)
+		return kv.value, true
+	}
+	return
+}
+
+// LRU删除策略:从队首(Back)拿到节点,然后将其删除
+func (c *Cache) RemoveOldest() {
+	ele := c.ll.Back()
+	if ele != nil {
+		c.ll.Remove(ele)
+		kv := ele.Value.(*entry)
+		delete(c.cache, kv.key)
+		c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) // 更新当前已经使用的内存
+		if c.OnEvicted != nil {
+			c.OnEvicted(kv.key, kv.value)
+		}
+	}
+}
+
+// 新增节点/修改节点
+func (c *Cache) Add(key string, value Value) {
+	// 如果在链表中找到则将其更新,同时更新占用的空间大小等,并移动到队列尾端
+	if ele, ok := c.cache[key]; ok {
+		c.ll.MoveToFront(ele)
+		kv := ele.Value.(*entry)
+		c.nbytes += int64(value.Len()) - int64(kv.value.Len())
+		kv.value = value
+	} else { // 如果找不到则直接插入
+		ele := c.ll.PushFront(&entry{key, value})
+		c.cache[key] = ele
+		c.nbytes += int64(len(key)) + int64(value.Len())
+	}
+	// 如果占用空间超过了链表的最大空间,则删除掉队首的节点
+	for c.maxBytes != 0 && c.maxBytes < c.nbytes {
+		c.RemoveOldest()
+	}
+}
+
+

单机并发缓存

+

多个协程(goroutine)同时读写同一个变量,在并发度较高的情况下,会发生冲突。确保一次只有一个协程(goroutine)可以访问该变量以避免冲突,这称之为 互斥,互斥锁可以解决这个问题。

+

当一个协程调用了 Lock() 方法时,其他协程被阻塞了,直到 Unlock()调用将锁释放。因此被包裹部分的代码就能够避免冲突,实现互斥。

+

抽象了一个只读数据结构 ByteView 用来表示缓存值:

+
package geecache
+
+// 只读数据结构用来表示缓存值
+type ByteView struct {
+	b []byte
+}
+
+// 返回缓存值的长度
+func (v ByteView) Len() int {
+	return len(v.b)
+}
+
+// 返回拷贝从而防止这个值被外部操作修改
+func (v ByteView) ByteSlice() []byte {
+	return cloneBytes(v.b)
+}
+
+// 将数据作为一个字符串进行返回
+func (v ByteView) String() string {
+	return string(v.b)
+}
+
+func cloneBytes(b []byte) []byte {
+	c := make([]byte, len(b))
+	copy(c, b)
+	return c
+}
+
+

为 lru.Cache 添加并发特性(加锁):

+
package geecache
+
+import (
+	"Go-Projects/GeeCache/lru"
+	"sync"
+)
+
+type cache struct {
+	mu         sync.Mutex
+	lru        *lru.Cache
+	cacheBytes int64
+}
+
+func (c *cache) add(key string, value ByteView) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	// 延迟初始化
+	if c.lru == nil {
+		c.lru = lru.New(c.cacheBytes, nil)
+	}
+	c.lru.Add(key, value)
+}
+
+func (c *cache) get(key string) (value ByteView, ok bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if c.lru == nil {
+		return
+	}
+
+	if v, ok := c.lru.Get(key); ok {
+		return v.(ByteView), ok
+	}
+
+	return
+}
+
+

Group 是 GeeCache 最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。

+

在缓存不存在时,调用这个函数,得到源数据:

+
type Getter interface {
+	Get(key string) ([]byte, error)
+}
+
+// 定义函数类型 GetterFunc,并实现 Getter 接口的 Get 方法
+type GetterFunc func(key string) ([]byte, error)
+
+func (f GetterFunc) Get(key string) ([]byte, error) {
+	return f(key)
+}
+

核心Group:

+
// 缓存的命名空间
+type Group struct {
+	name      string // 每个Group拥有一个唯一的名称
+	getter    Getter // 缓存未命中时的回溯
+	mainCache cache  // 并发缓存
+}
+
+var (
+	mu     sync.RWMutex
+	groups = make(map[string]*Group)
+)
+
+// 创建Group实例,并且将group的名称存在全局变量中
+func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
+	if getter == nil {
+		panic("nil Getter")
+	}
+	mu.Lock()
+	defer mu.Unlock()
+	g := &Group{
+		name:      name,
+		getter:    getter,
+		mainCache: cache{cacheBytes: cacheBytes},
+	}
+	groups[name] = g
+	return g
+}
+
+// 获取指定的group
+func GetGroup(name string) *Group {
+	mu.RLock()
+	g := groups[name]
+	mu.RUnlock()
+	return g
+}
+

Group的Get方法,完成对缓存的查找以及未命中后的回调操作

+
// 找到缓存值
+func (g *Group) Get(key string) (ByteView, error) {
+	// 如果没有键则报错
+	if key == "" {
+		return ByteView{}, fmt.Errorf("key is required")
+	}
+	// 从 mainCache 中查找缓存,如果存在则返回缓存值
+	if v, ok := g.mainCache.get(key); ok {
+		log.Println("[GeeCache] hit")
+		return v, nil
+	}
+
+	return g.load(key)
+}
+
+// 缓存不存在,则调用 load 方法
+func (g *Group) load(key string) (value ByteView, err error) {
+	return g.getLocally(key)
+}
+
+// getLocally 调用用户回调函数 g.getter.Get() 获取源数据,并且将源数据添加到缓存 mainCache 中
+func (g *Group) getLocally(key string) (ByteView, error) {
+	bytes, err := g.getter.Get(key)
+	if err != nil {
+		return ByteView{}, err
+
+	}
+	value := ByteView{b: cloneBytes(bytes)}
+	g.populateCache(key, value)
+	return value, nil
+}
+
+func (g *Group) populateCache(key string, value ByteView) {
+	g.mainCache.add(key, value)
+}
+
+

HTTP 服务端

+

分布式缓存需要实现节点间通信,建立基于 HTTP 的通信机制是比较常见和简单的做法。如果一个节点启动了 HTTP 服务,那么这个节点就可以被其他节点访问。

+

承载节点间 HTTP 通信的核心数据结构:

+
package geecache
+
+const defaultBasePath = "/_geecache/"
+
+type HTTPPool struct {
+	self     string // 记录自己的地址,包括主机名/IP 和端口
+	basePath string // 节点间通讯地址的前缀
+}
+
+// 返回HTTP实例
+func NewHTTPPool(self string) *HTTPPool {
+	return &HTTPPool{
+		self:     self,
+		basePath: defaultBasePath,
+	}
+}
+

实现最为核心的 ServeHTTP 方法:

+
// 使用服务器登录
+func (p *HTTPPool) Log(format string, v ...interface{}) {
+	log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...))
+}
+
+// 处理HTTP请求
+func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// 判断访问路径的前缀是否是 basePath,不是返回错误
+	if !strings.HasPrefix(r.URL.Path, p.basePath) {
+		panic("HTTPPool serving unexpected path: " + r.URL.Path)
+	}
+	p.Log("%s %s", r.Method, r.URL.Path)
+	// /<basepath>/<groupname>/<key> required
+	parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2)
+	if len(parts) != 2 {
+		http.Error(w, "bad request", http.StatusBadRequest)
+		return
+	}
+
+	groupName := parts[0]
+	key := parts[1]
+	// 通过 groupname 得到 group 实例,再使用 group.Get(key) 获取缓存数据
+	group := GetGroup(groupName)
+	if group == nil {
+		http.Error(w, "no such group: "+groupName, http.StatusNotFound)
+		return
+	}
+
+	view, err := group.Get(key)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/octet-stream")
+	// 将缓存值作为 httpResponse 的 body 返回
+	w.Write(view.ByteSlice())
+}
+

一致性哈希

+

一致性哈希算法将 key 映射到 2^32 的空间中,将这个数字首尾相连,形成一个环

+
package consistenthash
+
+import (
+	"hash/crc32"
+	"sort"
+	"strconv"
+)
+
+// 定义了函数类型 Hash,采取依赖注入的方式,允许用于替换成自定义的 Hash 函数
+type Hash func(data []byte) uint32
+
+// 一致性哈希算法的主数据结构
+type Map struct {
+	hash     Hash
+	replicas int            // 虚拟节点倍数
+	keys     []int          // 哈希环
+	hashMap  map[int]string // 虚拟节点与真实节点的映射表
+}
+
+// 允许自定义虚拟节点倍数和 Hash 函数
+func New(replicas int, fn Hash) *Map {
+	m := &Map{
+		replicas: replicas,
+		hash:     fn,
+		hashMap:  make(map[int]string),
+	}
+	if m.hash == nil {
+		m.hash = crc32.ChecksumIEEE
+	}
+	return m
+}
+
+// 实现添加真实节点/机器的 Add() 方法
+func (m *Map) Add(keys ...string) {
+	for _, key := range keys {
+		for i := 0; i < m.replicas; i++ {
+			hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
+			m.keys = append(m.keys, hash)
+			m.hashMap[hash] = key
+		}
+	}
+	sort.Ints(m.keys)
+}
+
+// 实现选择节点的 Get() 方法
+func (m *Map) Get(key string) string {
+	if len(m.keys) == 0 {
+		return ""
+	}
+
+	hash := int(m.hash([]byte(key))) // 计算 key 的哈希值
+	// 顺时针找到第一个匹配的虚拟节点的下标 idx
+	idx := sort.Search(len(m.keys), func(i int) bool {
+		return m.keys[i] >= hash
+	})
+
+	return m.hashMap[m.keys[idx%len(m.keys)]]
+}
+
+

分布式节点

+

抽象 PeerPicker

+
package geecache
+
+// PeerPicker 的 PickPeer() 方法用于根据传入的 key 选择相应节点 PeerGetter
+type PeerPicker interface {
+	PickPeer(key string) (peer PeerGetter, ok bool)
+}
+
+// 接口 PeerGetter 的 Get() 方法用于从对应 group 查找缓存值
+type PeerGetter interface {
+	Get(group string, key string) ([]byte, error)
+}
+
+

节点选择与 HTTP 客户端

+
const (
+	defaultBasePath = "/_geecache/"
+	defaultReplicas = 50
+)
+
+type HTTPPool struct {
+	self        string                 // 记录自己的地址,包括主机名/IP 和端口
+	basePath    string                 // 节点间通讯地址的前缀
+	mu          sync.Mutex             // 锁
+	peers       *consistenthash.Map    // 新增成员变量 peers,类型是一致性哈希算法的 Map,用来根据具体的 key 选择节点
+	httpGetters map[string]*httpGetter // 映射远程节点与对应的 httpGetter
+}
+
+// 实现 PeerGetter 接口
+type httpGetter struct {
+	baseURL string
+}
+
// 使用 http.Get() 方式获取返回值,并转换为 []bytes 类型
+func (h *httpGetter) Get(group string, key string) ([]byte, error) {
+	u := fmt.Sprintf(
+		"%v%v/%v",
+		h.baseURL,
+		url.QueryEscape(group),
+		url.QueryEscape(key),
+	)
+	res, err := http.Get(u)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+
+	if res.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("server returned: %v", res.Status)
+	}
+
+	bytes, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return nil, fmt.Errorf("reading response body: %v", err)
+	}
+
+	return bytes, nil
+}
+

实现 PeerPicker 接口

+
// Set() 方法实例化了一致性哈希算法,并且添加了传入的节点,为每一个节点创建了一个 HTTP 客户端 httpGetter
+func (p *HTTPPool) Set(peers ...string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	p.peers = consistenthash.New(defaultReplicas, nil)
+	p.peers.Add(peers...)
+	p.httpGetters = make(map[string]*httpGetter, len(peers))
+	for _, peer := range peers {
+		p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath}
+	}
+}
+
+// PickerPeer() 包装了一致性哈希算法的 Get() 方法,根据具体的 key,选择节点,返回节点对应的 HTTP 客户端
+func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	if peer := p.peers.Get(key); peer != "" && peer != p.self {
+		p.Log("Pick peer %s", peer)
+		return p.httpGetters[peer], true
+	}
+	return nil, false
+}
+

修改主方法

+
// 将 实现了 PeerPicker 接口的 HTTPPool 注入到 Group 中
+func (g *Group) RegisterPeers(peers PeerPicker) {
+	if g.peers != nil {
+		panic("RegisterPeerPicker called more than once")
+	}
+	g.peers = peers
+}
+
+// 使用实现了 PeerGetter 接口的 httpGetter 从访问远程节点,获取缓存值
+func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) {
+	bytes, err := peer.Get(g.name, key)
+	if err != nil {
+		return ByteView{}, err
+	}
+	return ByteView{b: bytes}, nil
+}
+
+// 缓存不存在,则调用 load 方法
+// 若非本机节点,则调用 getFromPeer() 从远程获取。若是本机节点或失败,则回退到 getLocally()
+func (g *Group) load(key string) (value ByteView, err error) {
+
+	if g.peers != nil {
+		if peer, ok := g.peers.PickPeer(key); ok {
+			if value, err = g.getFromPeer(peer, key); err == nil {
+				return value, nil
+			}
+			log.Println("[GeeCache] Failed to get from peer", err)
+		}
+	}
+
+	return g.getLocally(key)
+}
+

防止缓存击穿

+

并发了 N 个请求,假设对数据库的访问没有做任何限制的,很可能向数据库也发起 N 次请求,容易导致缓存击穿和穿透。针对相同的 key,如何做到只向远端节点发起一次请求呢?

+

实现了一个名为 singleflight 的 package 来解决这个问题

+
package singleflight
+
+import "sync"
+
+// call 代表正在进行中,或已经结束的请求。使用 sync.WaitGroup 锁避免重入
+type call struct {
+	wg  sync.WaitGroup
+	val interface{}
+	err error
+}
+
+// Group 是 singleflight 的主数据结构,管理不同 key 的请求(call)
+type Group struct {
+	mu sync.Mutex // protects m
+	m  map[string]*call
+}
+
+

实现 Do 方法

+
// 针对相同的 key,无论 Do 被调用多少次,函数 fn 都只会被调用一次,等待 fn 调用结束了,返回返回值或错误。
+func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
+	g.mu.Lock()
+	if g.m == nil {
+		g.m = make(map[string]*call)
+	}
+	if c, ok := g.m[key]; ok {
+		g.mu.Unlock()
+		c.wg.Wait()         // 如果请求正在进行中,则等待
+		return c.val, c.err // 请求结束,返回结果
+	}
+	c := new(call)
+	c.wg.Add(1)  // 发起请求前加锁
+	g.m[key] = c // 添加到 g.m,表明 key 已经有对应的请求在处理
+	g.mu.Unlock()
+
+	c.val, c.err = fn() // 调用 fn,发起请求
+	c.wg.Done()         // 请求结束
+
+	g.mu.Lock()
+	delete(g.m, key) // 更新 g.m
+	g.mu.Unlock()
+
+	return c.val, c.err // 返回结果
+}
+ + +
+ +
+
+ + + + + + +
+
+
Go项目-分布式缓存GeeCache
+
https://zhangzhao219.github.io/2022/12/05/Go/Go-Project-Geecache/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月5日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/11/UCAS/advanced-ai/advanced-ai-final/index.html b/2022/12/11/UCAS/advanced-ai/advanced-ai-final/index.html new file mode 100644 index 000000000..afa96ece9 --- /dev/null +++ b/2022/12/11/UCAS/advanced-ai/advanced-ai-final/index.html @@ -0,0 +1,1278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:高级人工智能-期末复习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:高级人工智能-期末复习

+ + +
+ +

《高级人工智能》期末复习

+ +

概述部分

+

人工智能的三大主义:行为主义、联结主义、符号主义

+

pSpBeD1.md.png

+

图灵测试是做什么的?给几个论断,哪些是哪些不是?

+

图灵测试:一个人(C)在完全不接触对方(A和B)的情况下,和对方进行一系列的问答,如果在相当长时间内,他无法根据这些问题判断对方是人(B)还是计算机(A),那么,就认为该计算机具有同人相当的智能(即计算机是能思维的)。

+

pSpBZuR.md.png

+

搜索和优化部分

+

选择题

+

g(x)为从根节点到x节点的代价总和

+

h(x)为从x节点到目标节点的估计代价总和

+

代价一致搜索 f(x) = g(x)

+
    +
  • 完备性:肯定能找到最优解
  • +
  • 最优性:找到的解花费最小
  • +
  • 比A*慢一些
  • +
  • 广度优先搜索是代价一致搜索的特例
  • +
+

贪婪搜索 f(x) = h(x)

+
    +
  • 不完备
  • +
  • 不保证能找到最优解
  • +
  • 深度优先搜索是贪婪搜索的特例
  • +
+

A*搜索 f(x) = g(x) + h(x)

+
    +
  • 启发函数可采纳的,那么,其中是到最近目标的真实耗散。
  • +
  • 启发函数可采纳的,那么A* 树搜索是最优的
  • +
  • A*图搜索与树搜索的区别在于图搜索不允许访问相同结点
  • +
  • 一致的:启发函数不仅仅要是可采纳的,沿路径的节点估计耗散值单调递增。
  • +
  • 图搜索中,如果启发函数是一致的,A* 搜索是最优的。
  • +
+

pSpsd56.md.png

+

遗传算法

+

pSpy9sJ.md.png

+

简答题

+

蚁群优化算法和粒子群优化算法是群体智能优化算法的两个代表,请从蚁群优化算法和粒子群优化算法中任选一个阐述其基本原理、算法过程及适用范围。

+

粒子群优化算法

+

基本原理:

+

粒子群优化算法中的每个粒子模拟一只鸟,代表待求解问题搜索解空间中的一个潜在解,“飞行信息”包括粒子当前的位置和速度两个状态量。每个粒子都可以获得其邻域内其它个体的信息,对所经过的位置进行评价,并根据这些信息和位置速度更新规则,改变自身的两个状态量,随着这一过程的不断进行,粒子群最终能够找到问题的近似最优解。

+

算法过程:

+
    +
  • 初始化 +
      +
    • 初始化粒子群:每个粒子的位置和速度,即
    • +
    • +
    +
  • +
  • 循环执行如下三步直至满足结束条件 +
      +
    • 计算每个粒子的适应度:
    • +
    • 更新每个粒子历史最好适应度及其相应的位置,更新当前全局最好适应度及其相应的位置
    • +
    • 更新每个粒子的速度和位置 +
        +
      • +
      • +
      +
    • +
    +
  • +
+

适用范围:适用于求解连续解空间的优化问题

+

蚁群优化算法

+

基本原理:

+

蚁群算法是一种用来寻找优化路径的概率型算法。用蚂蚁的行走路径表示待优化问题的可行解,整个蚂蚁群体的所有路径构成待优化问题的解空间。路径较短的蚂蚁释放的信息素量较多,随着时间的推进,较短的路径上累积的信息素浓度逐渐增高,选择该路径的蚂蚁个数也愈来愈多。最终,整个蚂蚁会在正反馈的作用下集中到最佳的路径上,此时对应的便是待优化问题的最优解。

+

算法过程:

+
    +
  • 首先将只蚂蚁随机放置在个城市,位于城市的第只蚂蚁选择下一个城市的概率为:
  • +
+

+

其中表示边上的信息素浓度,是根据距离定义的启发信息,反映了信息素与启发信息的相对重要性

+
    +
  • 当所有蚂蚁完成周游后,按以下公式进行信息素更新:
  • +
+

+

+

+

其中: 为常数, 表示第只蚂蚁在本轮迭代中走过的路径,为路径长度,为小于1的常数,反映信息素挥发速度

+

适用范围:适用于求解离散解空间的优化问题,适用于在图上寻找最优路径

+

应用题

+

A*树搜索的最优性条件

+
    +
  • 启发函数可采纳的,那么,其中是到最近目标的真实耗散。
  • +
  • 启发函数可采纳的,那么A* 树搜索是最优的
  • +
+

A*图搜索的最优性条件

+
    +
  • 一致的:启发函数不仅仅要是可采纳的,沿路径的节点估计耗散值单调递增。
  • +
  • 图搜索中,如果启发函数是一致的,A* 搜索是最优的。
  • +
+

pSPRY7D.md.jpg

+

传教士和野人问题通常描述如下:三个传教士和三个野人在河的一边,还有一条能载一个人或者两个人的船,找到一个方法让所有的人都渡到河的另一岸,要求在任何地方野人数都不能多于传教士的人数(可以只有野人没有传教士)。

+

(1) 精确地形式化该问题,只描述确保该问题有解所必须的特性,画出该问题的完全状态图

+

pSPRNAe.md.jpg

+

(2) 用一个合适的算法实现和最优地求解该问题,检查重复状态是个好主意吗?

+

采用先深搜索、先广搜索以及图搜索都可以,注意检查重复状态,重复状态的检测避免程序陷入死循环。

+

(3) 这个问题的状态空间如此简单,你认为为什么人们求解他却很困难?

+

虽然状态空间比较简单,但是要检测重复状态是一个困难:另外,在当前状态选取下一个合法状态,要能够不漏举所有合法状态也存在困难,当在某个状态无下一个合法状态时,需要回溯,这些都使得人为求解它变得困难

+

逻辑部分

+

选择题

+

pSpsjiV.md.pngpSpyldI.md.pngpSpyGJf.md.png

+

简答题

+

命题逻辑

+

已知知识库里包含如下的句子:

+

请用归结原理证明该知识库蕴含如下的句子:$\neg A \land \neg B $

+

Forward chain 证明7<3+9

+

pSPdsmR.md.jpg

+

kb中所有句子都为definite子句,请构造一种真值指派使得kb中所有子句为真

+

将所有的原子命题指派为True即可。

+
    +
  1. 由于是definite子句,不可能包含负文字,只能包含正文字,因此单独的文字一定为正文字,也就一定为True
  2. +
  3. 由于是definite子句,每一个非文字的子句中一定有一个文字是正文字,且子句内部一定使用析取符号连接,因此正文字一定为True,子句也一定为True
  4. +
  5. 综上,所有子句都为True
  6. +
+

pSPdafU.md.jpg

+

归结原理及证明:

+

pSPdwpF.md.jpgpSPdB6J.md.jpg

+

设计一个可靠但不完备的规则

+
    +
  • 知识库中是全部有理数的集合
  • +
  • 算法:,为全部自然数的集合
  • +
  • 因此算法是可靠的,但是并不完备,因为算法无法计算出任何的小数
  • +
+

描述语义蕴含、的作用

+
    +
  • 语义蕴含指的是有了知识表示后,额外推出其他的知识
  • +
  • 是命题逻辑里面的连接词,用于知识表示(实际上是可以替代的,但是引入这个符号进行知识表示比较方便)
  • +
+

设计A*启发式函数来使归结次数最少

+

构想一个A启发式函数,使得A归结结果为最优,并证明

+

h(n)为集合中的最短子句的长度

+

一阶谓词逻辑

+

胜者为王,败者为寇

+

不到长城非好汉,到了长城就是好汉;两个句子是否语义等价,并证明

+

成绩好的人都很刻苦,刻苦的人,一定成绩好;两个句子是否语义等价,并证明

+

理发师只给不给自己理发的人理发

+

pSPd0l4.md.jpg

+

将如下的一阶谓词逻辑的句子转化为合取范式:(不需要包含存在量词)

+

构造一个一阶谓词逻辑的知识库和句子,使得的归结过程永远不会停止。

+

pSPdUYT.md.jpg

+

模糊逻辑

+

(刻画模糊量词、模糊修饰词等)

+

很少有成绩好的学生特别贪玩

+
    +
  • 模糊谓词:贪玩、成绩好
  • +
  • 模糊修饰词:很、特别
  • +
  • 模糊量词:很少
  • +
+

很少有成绩好的学生特别喜欢玩游戏

+
    +
  • 模糊谓词:贪玩、喜欢玩游戏
  • +
  • 模糊修饰词:很、特别
  • +
  • 模糊量词:很少
  • +
+

Prolog

+

普通编程的步骤:了解问题-收集条件-寻找解决方法-编程解决-将问题数据化-用程序运行数据-debug

+

逻辑编程的步骤:了解问题-收集条件-不寻找解决方法-将条件写进KB-将问题转换为fact-问query-寻找错误的事实

+

C :- A,B 如果AB,则implyC(definite 子句)

+

[E | L]:将list拆解成第一个是E,后面的剩下

+

trace 和 notrace是debug的过程

+

DFS+backward chaining

+

不教程序怎么算,只列出事实

+

Prolog缺点:

+
    +
  • 不做occur check,因此有些事实是错的但是也可能推导出来,也就是不sound
  • +
  • DFS可能造成无穷递归,对的也导不出来,不complete。与语句编写的顺序也有关系
  • +
+

深度学习部分

+

选择题

+

pSpsvGT.md.pngpSpsxRU.pngpSpyEi6.md.png

+

GNN

+

谱方法:在谱空间中定义卷积:

+
    +
  • 通过图傅里叶变换和卷积原理定义卷积 +
      +
    • 图数据符合幂律分布,造成了极大的挑战
    • +
    +
  • +
  • 主要挑战是在谱空间定义的卷积在结点空间并没有局部化
  • +
+

空间方法:在向量空间中定义卷积

+
    +
  • 卷积被定义为目标结点到它的所有邻居的一个加权平均函数
  • +
  • 主要挑战是邻域的大小在结点之间差异很大,可能服从幂律分布
  • +
+

谱方法是空间方法的特例

+
    +
  • 谱方法通过特别的空间变换定义核函数
  • +
  • 空间方法直接定义核函数
  • +
+

聚合,更新是什么?

+

图神经网络的框架:聚合邻居节点的信息从而更新中心节点的表示

+

GraphSAGE:从每个结点开始随机游走,采样个结点,不用临近度指标判断。然后通过聚合函数进行参数共享

+

图卷积网络(GCN):通过归一化的拉普拉斯矩阵从不固定数量的邻居结点中聚合信息,通过特征变换共享参数

+

应用题

+

证明感知机不能表示异或逻辑

+

异或的逻辑为:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
000
101
011
110
+

两个变量的感知机模型为

+

代入上面的异或逻辑:

+
    +
  1. ,则
  2. +
  3. ,则
  4. +
  5. ,则
  6. +
  7. ,根据上面三个式子是明显不可满足的
  8. +
+

因此感知机不能表示异或逻辑

+

设计用于异或问题的二层感知机

+

z4tj3V.md.jpg

+

(以下简答题目答案来源于shmily

+

描述BP算法

+

BP算法由正向传播与反向传播两个过程组成。正向传播时,输入由输入层经过隐藏层到输出层;反向传播时,输出结果与真实结果通过损失函数计算误差,误差信号再沿相反方向传播至输入层,获得各层各单元的误差信号(梯度),并将其作为修正权值的依据。通过梯度下降算法更新权值,使得网络的整体误差迭代减小。

+

试论述在深度神经网络中BP算法遇到的困难,并说明为什么会出现“梯度消失”问题

+

当网络变深时,BP算法会遇到梯度消失或者梯度爆炸的现象,此时浅层的神经元几乎接受不到来自输出层的误差信号或者误差太大,无法更新其参数或参数剧烈波动。

+

根据链式求导法则,浅层参数的梯度来源于深层参数梯度的乘积。由于中间梯度矩阵的范数可能远小于1,再加上许多激活函数的导数小于1,随着传播层数的增多,误差信号反向传播的过程中以指数形式衰减,当传播到浅层时便出现了梯度消失现象。

+

简述对抗式生成网络(GAN)的基本原理及其学习算法

+

GAN的思想来源于博弈论当中的均衡理论,其由生成器G与判别器D构成。生成器G希望生成更接近于真实分布的数据,判别器则希望尽可能分辨所给数据是由生成器生成的还是从真实分布中采样的。

+

GAN的学习算法交替地更新判别器D与生成器G:

+

首先训练判别器D,

+
    +
  1. 从真实分布采样数据
  2. +
  3. 从高斯分布采样数据,送入生成器G生成数据
  4. +
  5. 最小化损失函数
  6. +
+

接着训练生成器G,

+
    +
  1. 从高斯分布采样数据,送入生成器G生成数据
  2. +
  3. 最小化损失函数
  4. +
+

重复进行以上各步骤直至收敛。

+

描述ResNet(ResNet的原理和结构图)

+

ResNet由如下多个Residual Block堆叠构成

+

pSPRJ0O.png

+

残差网络容易优化恒等式函数,学习优化残差映射比原始映射更加容易,随着网络加深,网络至少不会变得更差,有效地缓解了梯度消失等现象;此外,残差连接隐式地扩展了模型的特征空间,可以看作一种模型集成。

+

利用RNN构建一个翻译器

+

采用编码器-解码器结构,二者都是RNN网络,示意图如下:

+

pSPRGnK.png

+

其中,编码器RNN接受输入(原文token) ,并通过RNN结构编码隐藏状态。编码器编码完成后所有隐藏状态聚合为背景向量

+

解码器的RNN同样编码隐藏状态,并将编码的隐藏状态映射到预测结果,计算间的损失来完成模型的训练

+

预测时,通过自回归与束搜索的方式得到翻译序列。

+

强化学习部分

+

选择题

+

强化学习基础

+

多臂赌博机:

+

一台赌博机有多个摇臂,每个摇臂摇出的奖励大小不确定,玩家希望摇固定次数的臂所获得的期望累积奖励最大

+

优化目标:期望累计奖励最大化

+

探索和利用的关系:

+
    +
  • 利用:按照贪心策略进行选择,最大化即时奖励
  • +
  • 探索:选择贪心策略之外的行为,短期奖励会比较低,长期奖励会比较高
  • +
+

策略:

+
    +
  • 贪心策略
  • +
  • 贪心策略 +
      +
    • 以概率按照贪心策略进行行为选择(利用)
    • +
    • 以概率在所有行为中随机选择一个(探索)
    • +
    +
  • +
  • 乐观初值法:未发生之前,保持乐观的心态。每次摇完臂都会失望,所以下次会换个臂摇,鼓励探索
  • +
  • UCB行为选择策略:对Qt(a)做估计,但因为估不准(估不准与之前尝试的次数有关,尝试次数越多估的越准),所以对它做一个上界
  • +
+

pSpWfmD.md.png

+

马尔可夫状态过程的要素:

+
    +
  • 智能体(Agent)和环境(Environment)按照离散的时间步进行交互
  • +
  • 智能体的状态S、智能体采取的行为A、获得的奖励R
  • +
+

pSpfnhR.md.png

+

奖励假设:最终目标是通过最大化累积的Reward实现的

+

策略学习方法:

+
    +
  • 动态规划 +
      +
    • 策略迭代:从初始策略开始,迭代进行策略估值和策略提升,最终得到最优策略 +
        +
      • 策略估值:解给定的策略下的值函数,也就是预测当前策略下所能拿到的值函数问题。
      • +
      • 策略提升:根据当前策略的估值函数,寻找更优的策略(如果存在)
      • +
      +
    • +
    • 估值迭代:值迭代算法是策略评估过程只进行一次迭代的策略迭代算法,从初始状态估值开始,进行估值迭代,找到最优状态估值,按照贪心方式得到最优策略
    • +
    • 从运算量角度看,值迭代方法中策略评估只需要一次迭代,需要的运算量更小,应该比策略迭代更快收敛。但是,通常在策略提升中间插入需要多次迭代的策略评估的算法,收敛的更快!这可能与值迭代算法的终止条件有关。值迭代算法的终止条件对象为值函数,策略迭代算法的终止条件对象为策略,结合之前gridworld中观察的现象(策略可能比值函数收敛的更快),所以策略迭代可能比值迭代更快收敛。
    • +
    +
  • +
  • 蒙特卡洛:(通过采样的方式,最后用样本的平均值作估值,是一种从经验中获得的方法) +
      +
    • 从真实或者模拟的经验中计算状态(行动估值函数)不需要关于环境的完整模型
    • +
    • 直接根据真实经验或模拟经验计算状态估值函数
    • +
    • 不同状态的估值在计算时是独立的,不依赖于“自举”方法
    • +
    +
  • +
  • 时序差分:非平稳情形下的蒙特卡洛方法(恒定步长)
  • +
  • 参数近似
  • +
+

pSpyJW8.md.png

+

博弈部分

+

博弈的要素

+
    +
  • 局中人:在博弈中有权决定自己行动方案的博弈参加者
  • +
  • 重要假设:局中人是自私的理性人
  • +
  • 策略:博弈中可供局中人选择的行动方案
  • +
  • 效用函数:对每个参与博弈的局中人,都有一个相应的效用函数,每个局中人的目的都是最大化自己的效用
  • +
+

剪刀石头布:所有玩家的收益之和为0-零和博弈

+

最佳应对:针对局中人2的策略t,若局中人1用策略s产生的收益大于或等于其任何其他策略,则称策略s是局中人1对局中人2的策略t的最佳应对

+

纳什均衡:如果一个局势下,每个局中人的策略都是相对其他局中人当前策略的最佳应对,则称该局势是一个纳什均衡

+

帕累托最优:对于一组策略选择(局势)若不存在其他策略选择使所有参与者得到至少和目前一样高的回报,且至少一个参与者会得到严格较高的回报,则这组策略选择为帕累托最优。(“不可能再改善某些人的境况,而不使任何其他人受损。”)

+

社会最优:使参与者的回报之和最大的策略选择,社会最优的结果一定也是帕累托最优的结果

+

pSpytSS.md.png

+

应用案例:

+
    +
  • 首价密封报价拍卖 +
      +
    • 纳什均衡:每个竞拍者的报价低于其对商品的估价
    • +
    • 最优报价低于估价,竞拍者越多,报价越接近于估价
    • +
    +
  • +
  • 次价密封报价拍卖 +
      +
    • 纳什均衡:每个竞拍者会倾向于采用其对商品的估价进行报价
    • +
    +
  • +
+

讨价的对象是双方对商品估价之差

+

pSpyFd1.md.png

+

maxmin策略:最大化自己最坏情况时的效用

+
    +
  • 最小化损失,控制风险
  • +
  • 预防其它局中人的不理性给自己带来损失
  • +
+

minmax策略:最小化对手的最大效用

+

零和博弈情况下:

+
    +
  • minmax和maxmin是对偶的
  • +
  • minmax策略和maxmin策略等价于纳什均衡策略
  • +
+

pSpyNQg.md.png

+

匹配市场:

+
    +
  • 完全匹配:对于两类节点集合大小一样的二部图,选择数目和节点个数一样的边,使得每类节点中的任意一个节点在另一类节点中都有唯一的对应者
  • +
  • 最优匹配:效用最大的匹配,最优匹配对于个体而言不一定最优,甚至是最差的
  • +
+

市场结清价格:给定买方报价的情况下,如果卖方的某种价格使得对应的买方偏好图中存在完全匹配,则称卖方的这组价格为市场结清价格。市场结清价格总是存在,且使得买卖双方总效用最优。

+

pSpyaLj.md.png

+

议价权:

+

不稳定边:对于结局中未参与配对的边,如果边的两个端点获得的收益之和小于1,则称这条边为不稳定边,不稳定边的存在意味着其两个端点可以通过改变报价而改变结局

+

稳定结局:如果一个结局中不存在不稳定边,则称该结局为稳定结局

+

纳什议价解:

+
    +
  • A的备选项收益为
  • +
  • B的备选项收益为
  • +
  • 分配剩余价值
  • +
  • 纳什议价解:一人一半就好 +
      +
    • A的收益是
    • +
    • B的收益是
    • +
    +
  • +
+

均衡结局:给定一个结局,如果结局中的任意一个参与配对的边都满足纳什议价解的条件,则称该结局是均衡结局

+

均衡结局一定是稳定结局

+

pSpykIx.md.png

+

因果学习

+

画一个图,什么什么路径,上课那种,阻断、D分离

+

pSp5cUe.md.png

+

后门准则:Z满足关于(X,Y)的后门准则

+
    +
  • Z阻断了X与Y之间的每条含有指向X的路径(后门路径)
  • +
  • Z中没有X的后代节点
  • +
+

pSp5g4H.md.png

+

应用题:格子游戏

+

zf5LVA.md.png

+

zf71aj.md.jpg

+

zf7lZQ.md.jpg

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:高级人工智能-期末复习
+
https://zhangzhao219.github.io/2022/12/11/UCAS/advanced-ai/advanced-ai-final/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/11/UCAS/machine-learning/machine-learning-final/index.html b/2022/12/11/UCAS/machine-learning/machine-learning-final/index.html new file mode 100644 index 000000000..2931745c2 --- /dev/null +++ b/2022/12/11/UCAS/machine-learning/machine-learning-final/index.html @@ -0,0 +1,1095 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:机器学习-期末复习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:机器学习-期末复习

+ + +
+ +

《机器学习》期末复习

+ +

选择题

+

各种分类

+

监督学习:贝叶斯分类器、支持向量机、Logistic回归、决策树、线性回归、最大熵、CRF

+

无监督学习:主成分分析、K-Means、高斯混合聚类、层次聚类

+

线性分类方法:感知机、线性鉴别分析、最小距离分类器

+

非线性分类方法:决策树、最近邻、集成学习、核SVM

+

线性分类器最佳准则:感知准则函数、支持向量机、Fisher准则

+

生成式模型:朴素贝叶斯、隐马尔可夫模型、高斯混合模型

+

判别式模型:支持向量机、线性分类器、神经网络、线性判别分析

+

回归

+

Logistic回归使用最大似然估计

+

回归问题和分类问题的区别:前者预测函数值为连续值,后者为离散值

+

最小二乘回归方法的等效回归方法:线性均值和正态误差的最大似然回归

+

正则化的回归分析,可以避免过拟合

+

假如使用一个较复杂的回归模型来拟合样本数据,使用岭回归,调试正则化参数λ,来降低模型复杂度。若λ较大时,偏差增大,方差减小

+

在线性回归中使用正则项,你发现解的不少coefficient都是0,这个正则项可能是L0-norm或L1-norm

+

LR模型的损失函数是交叉熵

+

在Logistic Regression 中,如果同时加入L1和L2范数,可以做特征选择,并在一定程度上防止过拟合

+

逻辑斯蒂回归没有利用回归的思想

+

共轭分布

+

二项式分布的共轭分布是Beta分布

+

多项式分布的共轭分布是Dirichlet分布

+

贝叶斯

+
    +
  • 以贝叶斯定理为基础
  • +
  • 可以解决有监督学习的问题
  • +
  • 可以用极大似然估计法解贝叶斯分类器
  • +
+

朴素贝叶斯分类器的特点是假设样本各维属性独立

+

最大似然估计没有考虑先验分布

+

对于正态密度的贝叶斯分类器,各类协方差矩阵相同时,决策函数为线性决策函数

+

下面关于贝叶斯分类器描述错误:是基于后验概率,推导出先验概率

+

朴素贝叶斯模型属于生成式模型

+

贝叶斯分类器参数估计的准则:最大高斯后验、最大beta后验、极大似然

+

错误:以贝叶斯估计的角度来看朴素贝叶斯时,其没有估计联合概率

+

以下模型中属于贝叶斯网络的有( BD )

+

A.马尔可夫随机场

+

B.隐马尔可夫模型

+

C.条件随机场

+

D.朴素贝叶斯分类器

+

SVM

+

支持向量机属于判别式模型

+

SVM的原理:最大间隔分类

+

SVM的算法性能取决于:核函数的选择、核函数的参数、软间隔参数C

+

支持向量机的对偶问题是凸二次优化

+

支撑向量:最大间隔支撑面上的向量

+

避免直接的复杂非线性变换,采用线性手段实现非线性学习的方法是:核函数方法

+

软间隔SVM的阈值趋于无穷:只要最佳分类超平面存在,它就能将所有数据全部正确分类

+

核函数并不是把特征映射到的空间维度越高越好

+

如果SVM模型欠拟合, 以下方法哪些可以改进模型:增大惩罚参数C的值,增大核系数(gamma参数)

+

聚类

+

密度聚类方法充分考虑了样本间的密度可达关系

+

混合高斯聚类使用了EM算法

+

k-means算法初始值不同,最终结果可能不同

+

k-means不适合处理非凸型数据

+

以下可用于聚类性能测量的评估方法:Jaccard系数、FM指数、Rand指数、DB指数

+

降维

+

主成分分析方法是一种降维方法

+

PCA在做降维处理时,优先选取中心化样本的协方差矩阵的最大特征值对应特征向量

+

可以用于特征降维的:SVD、PCA和LDA

+

不可以用于特征降维的:蒙特卡洛方法

+

特征降维带来的好处:节省数据通信开销、节省数据存储资源、加快模型计算速度

+

决策树

+

关于决策树节点划分指标描述正确的是信息增益越大越好

+

决策树不受数据归一化影响,SVM、神经网络、Logistic回归都会受影响

+

增加决策树的深度可能导致随机森林模型过拟合数据

+

我们想在大数据集上训练决策树, 为了使用较少时间, 我们可以减少树的深度,减少树的数量

+

集成学习

+

Bootstrap数据:有放回地从总共N个样本中抽样n个样本

+

集成学习中基分类器多样,差异大,学习效率通常越好,每个基分类器的正确率的最低要求50%以上

+

Bagging方法的特点:构造训练集时采用Bootstraping的方式

+

Boosting方法的特点:预测结果时,分类器的比重不同

+

随机森林方法属于Bagging方法

+

Adaboost算法:

+
    +
  • 是弱分类器的线性组合
  • +
  • 提升树是以分类树或者回归树为基本分类器的提升办法
  • +
  • 该算法实际上是前向分步算法的一个实现,在这个方法里,模型是加法模型,损失函数是指数损失,算法是前向分步算法。
  • +
+

Adaboost方法中,需要迭代调整的两个重要参数是:样本权重和分类器权重

+

深度学习

+

以下关于深度网络训练的说法正确的:

+
    +
  • 训练过程需要用到梯度,梯度衡量了损失函数相对于模型参数的变化率
  • +
  • 损失函数衡量了模型预测结果与真实值之间的差异
  • +
  • 训练过程基于一种叫做反向传播的技术
  • +
+

在训练神经网络时,如果出现训练error过高,增加训练数据不能大幅度降低训练error

+

Tanh可以导致梯度消失

+

ReLU在神经网络中引入了非线性

+

关于CNN,Pooling层用于减少图片的空间分辨率

+

卷积神经网络可以有多个卷积核,可以不同大小

+

GRU和LSTM的说法正确的是:GRU的参数比LSTM的参数少

+

与普通反向传播不同的是,BPTT会在每个时间步长内叠加所有对应权重的梯度

+

在RNN中,梯度裁剪可以较好地处理梯度爆炸问题

+

循环神经网络有反馈连接并常被用来处理序列数据

+

过拟合和欠拟合

+

数据增强会增加模型的欠拟合风险

+

过拟合现象中训练样本的测试误差最小,测试样本的正确识别率却很低

+

过拟合:训练误差小,测试误差大

+

容易引起过拟合:SVM算法中使用高斯核代替线性核

+

不容易引起过拟合:增加训练集量、减少神经网络隐藏层节点数、删除稀疏的特征

+

神经网络处理过拟合:Dropout、Batch Normalization、regularization

+

概率图模型

+

在HMM中,如果已知观察序列和产生观察序列的状态序列,那么可用极大似然估计直接进行参数估计

+

解决隐马模型中预测问题的算法是维特比算法

+

其他

+

K-NN最近邻方法在什么情况下效果好:样本较少但典型性较好

+

以下可行的最近邻分类的加速方案:分层搜索和训练样本缩减

+

线性鉴别分析:找到一个投影方向,使得类内距离最小,类间距离最大

+

KL散度是根据类概率密度构造的可分性判据

+

最大似然估计没有考虑先验分布

+

多层感知机方法中,可用作神经元的非线性激活函数:logistic 函数

+

在有限支撑集上,均匀分布的熵最大

+

已知均值和方差,高斯分布的熵最大

+

受限玻尔兹曼机属于概率图模型

+

余弦距离会侧重考虑向量的方向

+

除了EM算法,梯度下降也可求混合高斯模型的参数

+

下列哪个不属于常用的文本分类的特征选择算法(D)

+

A. 卡方检验值

+

B. 互信息

+

C. 信息增益

+

D. 主成分分析

+

解决样本类别不平衡的手段:欠采样、过采样、使用focal loss

+

对于k折交叉验证, 以下对k的说法正确的是:

+

A.k越大, 不一定越好, 选择大的k会加大评估时间

+

B.选择更大的k, 就会有更小的bias ,因为训练集更加接近总数据集

+

C.在选择k时, 要最小化数据集之间的方差

+

下列选项中,关于KNN算法说法不正确的是(D)

+

A.能找出与待测样本相近的K个样本

+

B.可以使用欧氏距离度量相似度

+

C.实现过程相对简单,但是可解释性不强

+

D.效率很高

+

73.关于特征预处理,下列说法中错误的是(B )

+

A.包含标准化和归一化

+

B.标准化在任何场景下受异常值的影响都很小

+

C.归一化利用了样本中的最大值和最小值

+

D.标准化实际上是将数据在样本的标准差上做了等比例的缩放操作

+

交叉验证不能够提升模型的准确率

+

76.EM算法(Expectation Maximization Algorithm)是机器学习领域的一个经典算法,下面关于EM算法的说法中不正确的有:(A)

+

A.EM算法属于一种分类算法

+

B.EM算法可用于隐马尔科夫模型的参数估计

+

C.EM算法可以分为E-step和M-step两步

+

D.EM算法可用于从不完整的数据中计算最大似然估计

+

将一个k分类问题分解成一对一问题时总共需要k(k-1)/2个分类器

+

在有限支撑集上,下面分布中熵最大的是均匀分布

+

在机器学习中,当模型的参数量大于样本量时参数估计使用梯度下降法

+
    +
  1. GRU和LSTM的说法正确的是(D)
  2. +
+

A. GRU通过output gate控制memory;

+

B. LSTM对memory不做控制,直接传递给下一个unit

+

C. GRU不对上一时刻的信息做任何控制;

+

D. GRU的参数比LSTM的参数少;

+

以下哪些算法, 可以用神经网络去构造( BD )

+

A.KNN

+

B.Logistic回归

+

C.决策树

+

D.最小二乘估计

+

简答题

+

原题目

+

试阐述LDA(线性鉴别分析)的分类思想

+

给定训练样例集,设法将样例投影到一条直线上,使得同类样例的投影点尽可能接近,异类样例的投影点尽可能远离;

+

在对新样本进行分类时,将其投影到同样的这条直线上,再根据投影点的位置来判断新样本的类别。

+

请简要介绍SVM的设计思想

+

答案:SVM是一个分类算法,它的目标为确定一个分类超平面,从而将不同类别的数据分隔开达到分类的目标。

+

当训练数据线性可分时,通过硬间隔最大化,学习一个线性的分类器,即线性可分支持向量机,又称为硬间隔支持向量机;

+

当训练数据近似线性可分时,通过软间隔最大化,也学习一个线性的分类器,即线性支持向量机,又称为软间隔支持向量机;

+

当训练数据线性不可分时,通过使用核技巧及软间隔最大化,学习非线性支持向量机。

+

试分析SVM对噪声敏感的原因

+

给定训练集,SVM最优决策边界由支持向量决定。

+

当增加噪声时,那么该噪声有极高的可能是含噪声训练集的一个支持向量,这意味着决策边界需要变。

+

简要介绍在深度神经网络中引入非线性激活函数的作用

+

不引入非线性激活函数的情况下,不管神经网络有多少层其输出都是输入的线性组合,与没有隐藏层的效果相当

+

在数据处理时,为什么通常要进行标准化处理

+

在实际问题中,我们使用的样本通常是多维数据,每一维对应一个特征,这些特征的量纲和数量级都是不一样的

+

这时需要对数据进行标准化处理,试所有的特征具有同样的尺度

+

试述将线性函数用作神经元激活函数的缺陷

+

如果单用线性函数作为激活函数,无论多少层的神经网络会退化成一个线性回归,不能处理非线性分类任务。

+

试述学习率的取值对神经网络训练的影响

+

如果学习率太低,每次下降的很慢,使得迭代次数非常多。

+

如果学习率太高,在后面迭代时会出现震荡现象,在最小值附近来回波动。

+

神经网络为什么会产生梯度消失,有什么解决方案

+

前面层上的梯度是来自于后面层上梯度的乘积。当存在过多的层次时,且激活函数的梯度小于1时,就会使前面层的梯度变得很小,更新速度过慢,导致梯度消失。

+

一种解决方案是使用Relu激活函数替换sigmoid,relu函数的梯度不会随着x的增大而变小,sigmoid在x取值较大时梯度趋近于0。

+

卷积核尺度和参数的计算

+

对3个32×32的特征图进行卷积层操作,卷积核10个5×5,Stride是1,pad为2,输出特征图的尺度是多少?卷积层的参数是多少?写出公式和结果。

+

输出尺度:(N+2P-F)/stride+1

+

卷积层的参数:(F×F×n+1)×N

+

答案:输出尺度( 32+2×2-5)/1+1 = 32

+

卷积层的参数 (5×5×3+1)×10=760

+

试析随机森林为何比决策树Bagging集成的训练速度更快

+

随机森林是Bagging算法的一个扩展变体,以决策树为基学习器构建Bagging集成,

+

Bagging在选择划分属性时需要考察结点的所有属性,而随机森林只需随机地考察一个属性子集

+

所以随机森林比决策树Bagging训练速度更快,泛化能力越强。

+

请给出L1范数和L2范数的计算方法及他们的使用场景。

+

L1范数为向量各个元素绝对值之和可以使权值稀疏,方便特征提取。

+

L2 范数为向量各个元素平方和的1/2次方可以防止过拟合,提升模型的泛化能力。

+

试述为什么基于L1范数可以进行特征选择。

+

基于L1范数的特征选择:不能直接设置最终选择特征的个数k;通过设置正则化系数λ来隐式控制k;

+

λ值越大,模型越关注稀疏性,得到的非零系数个数越少;

+

反之,非零稀疏个数越多;

+

可以设置一个选择特征个数的上限,通过设置不同λ值,得到满足要求的特征。

+

从有条件极值问题的角度来看,L1范数相当于将模型界空间限制在了L1-ball上,目标函数的等高线有很大的概率与坐标轴和边相交,这样的解具有稀疏性。

+

请指出数据聚类存在哪些挑战性问题

+
    +
  • 能够处理高维数据:在高维空间聚类更具挑战性,随着维数的增加,具有相同距离的两个样本其相似程度可以相差很远。对于高维稀疏数据,这一点更突出。
  • +
  • 对噪声鲁棒:在实际中,绝大多数样本集都包含噪声、空缺、部分未知属性、孤立点、甚至错误数据。
  • +
  • 具有约束的聚类:在实际应用中,通常需要在某种约束条件下进行聚类,既满足约束条件,以希望有高聚类精度,是一个挑战性问题。
  • +
  • 对初始输入参数鲁棒:具有自适应的簇数判定能力,对初始聚类中心鲁棒。
  • +
  • 能够解决用户的问题:聚类结果能被用户所理解,并能带来经济效益,特别是在数据挖掘领域。
  • +
+

描述主成分分析的主要步骤

+
    +
  1. 数据标准化
  2. +
  3. 计算协方差矩阵,求协方差的特征值和特征向量。
  4. +
  5. 将特征值按照从大到小的顺序排序,选择其中最大的k个,然后将其对应的k个特征向量分别作为列向量组成特征向量矩阵。
  6. +
  7. 将样本点投影到选取的特征向量上。
  8. +
+

请描述机器学习中的分类任务

+

根据给定的训练集,其中,要求寻找上的决策函数

+

请给出你对泛化误差的理解

+

泛化误差 = 偏差+方差+噪声

+

偏差:度量了学习算法的期望预测与真实结果的偏离程度,刻画了学习算法本身的拟合能力

+

方差:度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响

+

噪声:表达了在当前任务上任何学习算法所能达到的期望泛化误差的下界,即刻画了学习问题本身的难度

+

模型评估过程中,欠拟合和过拟合现象是什么。

+

过拟合是指模型对于训练数据拟合呈过当的情况,反映到评估指标上,就是模型在训练集上的表现很好,但在测试集和新数据上的表现较差。

+

欠拟合是模型在训练和预测时表现都不好的情况。

+

说出几种降低过拟合和欠拟合的方法。

+

降低过拟合:

+
    +
  1. 从数据入手,获得更多的训练数据。使用更多的训练数据是解决过拟合问题最高效的手段,因为更多的样本能够让模型学习到更多更高效的特征。当然,直接增加实验数据一般是很困难的,但是可以通过一定的规则来扩充训练数据。比如在图像分类的问题上,可以通过图像的平移、旋转、缩放等方式扩充数据,更进一步地,可以使用生成式对抗网络来合成大量的新训练数据。
  2. +
  3. 降低模型复杂度。在数据较少时,模型过于复杂是产生过拟合的主要因素,适当降低模型复杂度可以避免模型拟合过多的采样噪声。例如,在神经网络模型中减少网络层数、神经元个数等;在决策树模型中降低树的深度、进行剪枝等。
  4. +
  5. 正则化方法。给模型的参数加上一定的正则约束,比如将权值的大小加入到损失函数中。
  6. +
  7. 集成学习方法。集成学习是把多个模型集成在一起,来降低单一模型的过拟合风险,如Bagging方法。
  8. +
+

降低欠拟合:

+
    +
  1. 添加新特征。当特征不足或者现特征与样本标签的相关性不强时,模型容易出现欠拟合。通过挖掘“上下文特征”“ ID 类特征”“组合特征”等新的特征,往往能够取得更好的效果。
  2. +
  3. 增加模型复杂度。简单模型的学习能力较差,通过增加模型的复杂度可以便模型拥高更强的拟合能力。例如,在线性模型中添加高次项,在神经网络模型中增加网络层数或神经元个数等。
  4. +
  5. 减小正则化系数。正则化是用来防止过拟合的,但当模型出现欠拟合现象时,则需要针对性地减小正则化系数。
  6. +
+

K均值算法的优缺点是什么,如何对其调优。

+

K均值算法缺点:例如受初值和离群点的影响每次的结果不稳定、结果通常不是全局最优而是局部最优解、无法很好地解决数据簇分布差别比较大的情况、不太适用于离散分类等。

+

K均值聚类的优点:主要体现在对于大数据集,K均值聚类算法相对是高效的,计算复杂度是 O(NKt) 接近于线性,其中N是数据对象的数目,K是聚类的簇数,t 是迭代的轮数。

+

调优方法:数据归一化,离群点预处理,采用核函数,合理选择K值。

+

请简述relu激活函数的优缺点

+

优点:

+
    +
  1. 从计算的角度上,Sigmoid与Tanh激活函数均需要计算指数,复杂度高。而ReLU 只需要一个阈值即可得到激活值。
  2. +
  3. ReLU的非饱和性可以有效地解决梯度消失的问题。
  4. +
  5. ReLU的单侧抑制提供了网络的稀疏表达能力。
  6. +
+

缺点:

+

在较大学习率设置下Relu可能会出现大量神经元死亡问题。后面神经元方向传播梯度为正,且学习率较大,Relu的梯度为1,梯度下降此时会导致该神经元的参数为负值,可能之后不会再被激活,造成神经元死亡。

+

补充题目

+

生成式模型和判别式模型的区别

+

生成模型估计的是联合概率分布,然后求出条件概率分布P(Y|X)作为预测的模型,即生成模型:P(Y|X)= P(X,Y)/ P(X)。

+

生成方法关心的是给定输入x产生输出y的生成关系。

+

判别模型估计的是条件概率分布,有数据直接学得决策函数P(X)或者条件概率分布P(Y|X)作为预测的模型。

+

判别式方法关心的是给定输入X,应该预测什么样的输出Y

+

逻辑回归和线性回归的异同

+

不同之处:

+
    +
  1. 逻辑回归解决的是分类问题,因此因变量是离散的;而线性回归解决的是回归问题,因此因变量是连续的。这是两者最本质的区别
  2. +
  3. 在自变量和超参数确定的情况下逻辑回归可看作广义的线性模型在因变量下服从二元分布的一个特殊情况
  4. +
  5. 使用最小二乘法求解线性回归时我们认为因变量服从正态分布
  6. +
+

相同之处:

+
    +
  1. 二者在求解超参数的过程中都使用梯度下降的方法
  2. +
  3. 二者都使用了极大似然估计对训练样本进行建模
  4. +
+

距离函数的四个基本性质

+
    +
  1. 非负性:
  2. +
  3. 同一性:
  4. +
  5. 对称性:
  6. +
  7. 直递性:
  8. +
+

随机变量x的支撑集(也就是非零值域)定义为[a,b],没有别的限制加在x上,该随机变量的最大熵分布是什么

+

根据最大熵模型, 推导出x概率密度函数是一个常函数,所以最大熵分布为均匀分布。

+

随机变量x的给定均值和方差限制在x上,该随机变量的最大熵分布是什么

+

根据最大熵模型推导出x概率密度函数是一个高斯分布 。

+

计算题

+

概率图

+

写出概率图模型联合分布的因子分解式

+

无向图看团,有向图看条件概率

+

贝叶斯网络计算概率

+

HMM

+

前向算法

+

后向算法

+

维特比解码

+

聚类

+

Kmeans:

+
    +
  • 确定初始中心点
  • +
  • 计算聚类结果
  • +
  • 根据结果更新中心点
  • +
+

层次聚类自底向上:初始每一个点为一类,逐步合并更新中心即可,注意更新的时候要使用原始的点重新进行计算

+

贝叶斯

+

贝叶斯最小错误分类

+

贝叶斯最小风险

+

决策树

+
    +
  • ID3:最大信息增益:根据类别计算经验熵,然后按照特征对类别算条件熵,两者相减,取比较大的特征作为划分的节点
  • +
  • C4.5:最大信息增益比:在ID3计算后的基础上除以每一个特征的经验熵
  • +
  • CART:最小基尼指数:外层是特征比例,内层是特征内部的类别比例
  • +
+

Maximum Likelihood

+

抛一枚硬币问题,观察数据情况是:一枚硬币包括正反两面,共抛了30次,其中12次是正面,18次是反面。采用Maximum Likelihood方法,估计正面出
+现的概率和反面出现的概率。

+

pS96XQO.md.png

+

Fisher

+

设计题

+

10万张图片分类,说明模型结构和训练方法

+

在机器学习中常常采用基于数据驱动的方法进行图像分类。所谓基于数据驱动的方法,就是给计算机很多数据,然后实现学习算法,让计算机学习到每个类的外形的方法。基于这种方法的完整流程如下

+
    +
  1. 输入:输入是包含 N 个图像的集合,每个图像的标签是 K 种分类标签中的一种。这个集合称为训练集。
  2. +
  3. 学习:这一步的任务是使用训练集来学习每个类到底长什么样。一般该步骤叫做训练分类器或者学习一个模型。
  4. +
  5. 评价:让分类器来预测它未曾见过的图像的分类标签,并以此来评价分类器的质量。我们会把分类器预测的标签和图像真正的分类标签对比。毫无疑问,分类器预测的分类标签和图像真正的分类标签如果一致,那就是好事,这样的情况越多越好。
  6. +
+

[pS.md.png

+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:机器学习-期末复习
+
https://zhangzhao219.github.io/2022/12/11/UCAS/machine-learning/machine-learning-final/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-final/index.html b/2022/12/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-final/index.html new file mode 100644 index 000000000..2b79373bb --- /dev/null +++ b/2022/12/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-final/index.html @@ -0,0 +1,1119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研究生课程:模式识别与机器学习-期末复习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研究生课程:模式识别与机器学习-期末复习

+ + +
+ +

《模式识别与机器学习》期末复习

+ +

第1章 引言

+

模式识别:利用计算机对物理对象进行分类,在错误概率最小的条件下,使识别的结果尽量与客观物体相符合

+

在特征空间和解释空间之间找到一种映射关系:

+

机器学习:利用大量的训练数据,获得产生数据的模式或预测

+

第2章 统计判别

+

贝叶斯

+

pSirOKS.md.png

+

作为统计判别问题的模式分类

+

zIkB5j.md.jpgzIk0aQ.md.jpgzIkUr8.md.jpg

+

正态分布模式的贝叶斯分类器

+

zbf478.md.jpgzbfh0f.md.jpg

+

第3章 判别函数

+

线性判别函数

+

什么是线性判别函数?

+

统计模式识别中用以对模式进行分类的一种最简单的判别函数称为线性判别函数。线性判别函数的一般形式是,其中是特征向量的增广形式,是权重系数。根据的取值进行分类,这个函数在几何上一般表现为直线(高维空间的超平面),所以称之为线性判别函数。

+

为什么需要非线性判别函数?

+

对于复杂的实际应用,线性分类器往往无法满足要求,不同类别的样本之间并不总是线性可分的,比如著名的异或问题,这就需要寻找能够实现非线性分类的判别函数分类器。

+

多类情况:

+
    +
  • 多类情况1:用M个判别函数将属于这一类的和不属于这一类的分开,也就是分类成功只能有一个大于0的
  • +
  • 多类情况2:用M*(M-1)/2个判别函数,两两进行分类,只有这一类关于其他所有类的判别函数都大于0时才算分类成功
  • +
  • 多类情况3:M个判别函数,没有不确定区域的多类情况2,判别函数比较大小即可
  • +
+

权重分量数量计算:的维度,为多项式次数。

+

Fisher线性判别

+

pSPdNkV.md.jpg

+

感知器

+

z5CLon.md.jpgzIkaqS.md.jpg

+

多类情况增广向量不需要变为负数,要求这个类别的比其他的类别都要大,否则这个类别+样本,其他的类别-样本

+

H-K算法可以发现类别不可分的情况

+

第4章 特征选择和提取

+

K-L变换

+

zIkwVg.md.jpg

+

第5章 统计机器学习基础

+

期望风险:机器学习算法的目标就是降低式所示的期望泛化误差(这个数据量被称为风险),选择期望风险最小的模型。

+

经验风险:用训练集的分布代替真实情况下的数据分布,最小化训练集上的期望损失

+

结构风险:在经验风险最小化的基础上再引入参数的正则化来限制模型能力,使其不要过度地最小化经验风险

+

偏差方差和噪声

+

简述偏差方差分解及其推导过程,并说明偏差、方差和噪声三部分的内在含义

+

pSpCJmD.md.pngzqsmin.md.jpg

+

过拟合和欠拟合

+

pSi6VhR.md.pngpSi6QBD.md.png

+

过拟合:当学习器把训练样本学的“太好”了的时候,很可能已经把训练样本自身的一些特点当作了所有潜在样本都会具有的一般性质,在训练集上效果好。但是在测试集上效果差,这样就会导致模型的泛化性能下降。

+

欠拟合:模型尚未学习到数据的真实结构。在训练集和验证集上的性能都很差。

+

如何判断一个模型处在过拟合状态还是欠拟合状态?

+
    +
  • 欠拟合情况 :随着训练样本数增大,训练集得分和验证集得分相差不大,并且得分都不高。
  • +
  • 过拟合情况 :随着训练样本数增大,训练集得分上升的同时和验证集得分下降。
  • +
+

给出3种减轻模型过拟合的方法:

+

过拟合:

+
    +
  • 获得更多数据
  • +
  • 降低模型复杂度
  • +
  • 特征选择
  • +
  • 早停
  • +
  • 正则化
  • +
  • 添加噪声
  • +
+

欠拟合:

+
    +
  • 增加特征数
  • +
  • 增加模型复杂度
  • +
  • 减小正则化参数
  • +
+

假设某研究者在 ImageNet 数据上使用线性支持向量机 Linear SVM 来做文本分类的任务,请说明在如下情况下分别如何操作才能得到更好的结果, 并说明原因。

+
    +
  • 训练误差5%,验证误差10%,测试误差10% +
      +
    • 训练、验证和测试误差都很大,模型处于欠拟合状态,可以选择将正则化参数C值适当调大,增大模型的复杂度
    • +
    +
  • +
  • 训练误差1%,验证误差10%,测试误差10% +
      +
    • 训练误差比较小,验证和测试误差比较大,模型处于过拟合状态,可以选择进行数据增强、或者将C值适当调小,增加模型泛化能力
    • +
    +
  • +
  • 训练误差1%,验证误差3%,测试误差10% +
      +
    • 训练和验证误差比较小,测试误差比较大,说明训练数据和测试数据的分布差别比较大,可以重新采样或者shuffle数据
    • +
    +
  • +
+

如果使用SVM做二分类问题得到如下结果,分别应该采取什么措施以取得更好的结果?并说明原因。

+
    +
  • 训练集的分类准确率90%,验证集的分类准确率90%,测试集的分类准确率88% +
      +
    • 训练、验证和测试准确率都很低,模型处于欠拟合状态,可以选择将正则化参数C值适当调大,增大模型的复杂度
    • +
    +
  • +
  • 训练集的分类准确率98%,验证集的分类准确率90%,测试集的分类准确率88% +
      +
    • 训练准确率比较高,验证和测试准确率比较低,模型处于过拟合状态,可以选择进行数据增强、或者将C值适当调小,增加模型泛化能力
    • +
    +
  • +
+

如果使用逻辑回归算法做二分类问题得到如下结果,分别应该采取什么措施以取得更好的结果?并说明理由。

+
    +
  • 训练集的分类准确率85%,验证集的分类准确率80%,测试集的分类准确率75% +
      +
    • 训练、验证和测试准确率都很低,模型处于欠拟合状态,可以选择增加训练特征,使用更多的训练参数
    • +
    +
  • +
  • 训练集的分类准确率99%,验证集的分类准确率80%,测试集的分类准确率78% +
      +
    • 训练准确率比较高,验证和测试准确率比较低,模型处于过拟合状态,可以选择减少训练特征,添加正则项,增加数据量等等
    • +
    +
  • +
+

第6章 有监督学习方法

+

pSiybnS.md.pngpSiyXkj.md.pngpSiyjts.md.png

+

公式推导相关

+

2018-2019

+

pSit5Af.md.pngpSitgcd.md.jpgpSCUIW4.md.pngpSiNZE6.md.pngpSiNm4O.md.pngpSiN1KA.md.pngpSiNUPS.md.png

+

2021-2022

+

pSCdBvj.md.png

+

pSitc1H.md.jpg

+

第7章 支持向量机

+

pSi6S10.md.pngpSi6pcV.md.pngpSi6Y9I.md.png

+

径向基函数(RBF)gamma和C的影响:

+
    +
  • 参数gamma定义了单个训练样本的影响大小,值越小影响越大,值越大影响越小。参数gamma可以看作被模型选中作为支持向量的样本的影响半径的倒数。gamma越大半径越窄,因此如果欠拟合需要增大gamma,分的更准
  • +
  • 参数C在误分类样本和分界面之间进行权衡。低的C值使分界面平滑,而高的C值通过增加模型自由度以选择更多支持向量来确保所有样本都被正确分类。因此如果欠拟合要增大C
  • +
+

最小化VC维h等价于最大化间隔,使分类器的复杂度小!

+

简述SVM算法的原理

+

z5CjJ0.md.jpgz5CXiq.md.jpg

+

第8章 聚类

+

pSi6s4s.md.png

+

K均值:CE

+

密度:AF

+

高斯混合:BD

+

Kmeans:Kmeans的判别界面应该是簇的中垂线

+
    +
  • 一种经典的聚类算法,简单、快速
  • +
  • 假定簇为球形且每个簇的概率相等
  • +
  • 能处理大规模数据,可扩展型好
  • +
  • 当簇接近高斯分布时,效果较好
  • +
  • 当簇具有不同的尺寸、密度、非球形,Kmeans可能得不到理想的聚类结果
  • +
  • 硬划分数据点到簇,当数据上出现一些小的扰动,可能导致一个点划分到另外的簇
  • +
+

K-Means与GMM

+

K-Means

+
    +
  • 损失函数:最小化平方距离的和
  • +
  • 样本点硬划分到某个簇
  • +
  • 假定样本属于每个簇的概率相等,且为球形簇
  • +
+

GMM

+
    +
  • 最小化负对数似然
  • +
  • 点到簇的从属关系为软分配
  • +
  • 可以被用于椭球形簇,且各个簇概率不同
  • +
+

层次聚类:最小距离层次聚类可以做同心圆相关聚类

+
    +
  • 对噪声和离群点敏感
  • +
  • 比较难处理不同尺寸的簇和凸的簇
  • +
  • 成链,误把大簇分裂
  • +
+

DBSCAN

+
    +
  • 各种大小、各种形状的簇,不需要明确簇的数量
  • +
  • 具有一定的抗噪音特性
  • +
  • 参数选择比较困难
  • +
  • 不适合密度差异较大的数据集
  • +
  • 时间慢
  • +
+

pSCaYhF.md.png

+

第9章 降维

+

PCA的优化目标:

+
    +
  • 最大化映射后的样本方差角度
  • +
  • 最小重建误差角度
  • +
+

第10章 半监督学习

+

基本假设

+

平滑假设:如果高密度区域中两个点距离较近, 那么对应的输出也应该接近

+

聚类假设:如果两个点在同一个簇,那么它们很有可能属于同一个类别

+
    +
  • 等价形式:低密度分割(决策边界应该在低密度区域)
  • +
+

流形假设:输入空间由所有数据点所在的多个低维流形构成,位于同一流形上的数据点具有相同的标签,流形上距离近的点的标签相似

+

具体算法

+

自我训练算法:假设输出的高度置信的预测是正确的

+

协同训练:假设特征可分裂单独对于训练一个好的分类器是充分的,在给定类别后是条件独立的

+

生成式模型:假设所有数据(带标签&不带标签)都由一个潜在的模型生成(GMM,HMM,朴素贝叶斯)

+

半监督支持向量机:假设来自不同类别的无标记数据之间会被较大的间隔隔开

+
    +
  • C2很小表达对未标注样本错分的容忍度比较大,很大表示不容忍错分,每一个未标注样本也要分对
  • +
+

基于干扰的半监督:基于连续性假设:考虑对输入稍加改变,得到其增广表示,模型对的预测和对原始数据点的预测相似。

+

基于图的半监督学习:假设在所有数据点(标注数据和无标注数据)定义一个相似性图,相似的数据点之间存在边,边的权重表示两个数据点之间的相似程度,相似图中“紧密”连接的点趋向于有相同的标签

+

第11章 概率图模型

+

贝叶斯球:

+

pSSLlbq.md.png

+

z7Vp0P.md.jpg
+z7VSmt.md.jpg

+

HMM

+

zoUqK0.md.jpg

+

前向算法

+

zoUjVU.md.jpg
+zoUObT.md.jpg

+

维特比算法

+

zoULrV.md.jpgzoUvaF.md.jpg

+

第12章 集成学习

+

bagging

+

降低模型的方差,偏差不变

+

原理:通过对训练样本进行bootstrap采样(有放回的随机采样),然后训练多个模型,最后对多个模型作平均,得到最后的融合模型。

+

Bagging适合对偏差低、方差高的模型进行融合,如决策树、神经网络等

+

boosting

+

降低模型的偏差,方差不变

+

原理:每次迭代顺序的把一些模型加进去,最后一些子模型的加权平均是我们最后的集成模型

+

Adaboost

+

Adaboost:在弱学习器失败的样本上,学习第二个弱学习器

+

开始初始化的时候每个样本的权重相同

+

分对的样本,其权重除以,权重减小

+

分错的样本,其权重乘以,权重增大

+

最后对模型进行加权融合

+

Adaboost 原理:先从初始训练集训练出一个学习器,再根据基学习器的表现来对训练样本分布进行调整,使得先前基学习器做错的训练样本在后续得到更多的关注,然后基于调整后的样本分布来训练下一个基学习器;如此重复进行,直到基学习器达到事先指定的值T,最终将这T个基学习器进行加权结合。

+

Adaboost 损失函数:使用指数损失函数

+

Adaboost算法流程:

+

pSigIpR.md.png

+

为什么AdaBoost经常可以在训练误差为0后继续训练还可能带来测试误差的持续下降?

+

在训练误差下降到接近0的时候,更多的训练,会增加分类器的分类margin,这个过程也能够防止测试误差的上升,随着Margin的变大,测试误差会逐渐收敛。

+

AdaBoost优缺点:

+

优点:实现快速简单、灵活、通用性高

+

缺点:AdaBoost性能取决于数据和弱学习器,如果弱分类器过于复杂,可能会产生过拟合情况,如果弱分类器太弱有可能造成欠拟合,还容易收到均匀噪声的影响。

+

第13章 深度学习

+

神经元的结构

+

pSpSD6U.md.png

+

激活函数

+

Sigmoid函数:

+

在早期的神经网络中较为普遍,逐渐被更简单的ReLU函数取代

+

容易导致梯度消失问题:

+
    +
  • 导数最大值为0.25:反向传播时,返回网络的误差将会在每一层收缩至少75%
  • +
  • 尾部是饱和的,对应的梯度接近0,导致消失梯度问题
  • +
+

Tanh函数:形状和sigmoid函数的形状很像,但tanh函数在坐标系的原点上对称:使用tanh激活函数收敛会更快,减轻消失梯度的现象

+

ReLU函数:

+
    +
  • 计算量小,不涉及除法
  • +
  • 一部分神经元的输出为0:网络稀疏,减少了参数的相互依存关系,缓解过拟合
  • +
  • 时,导数为,解决了梯度消失问题,收敛速度更快
  • +
  • 时,导数为,无法更新权重
  • +
+

神经网络

+

pSpSLtI.md.pngpSpSOht.md.png

+

梯度消失和梯度爆炸

+

梯度爆炸:梯度值超出范围:无穷大值

+

对学习率敏感

+
    +
  • 学习率较大-更大的权重-更大的梯度
  • +
  • 学习率太小-模型训练没有进展
  • +
  • 可能需要在训练期间大幅改变学习率
  • +
+

梯度消失:梯度值趋近0

+

无论如何选择学习率,训练都没有进展

+

只有顶层训练有效,底层训练基本无效,使网络更深可能并没有更好

+

模型的深度增加,梯度会逐渐消失:

+
    +
  • 将sigmoid激活函数换成其他的激活函数
  • +
  • Resnet:通过跳接的方式,优化残差映射比直接优化原始映射更容易,带有集成学习的思想
  • +
  • Batch normalization,还能带来正则化的效果
  • +
+

其他技巧:

+
    +
  • batch normalization 会使得我们的训练对好多因素(学习率、初始化)的要求没有那么高
  • +
  • 参数初始化,或者采用预训练网络作初始化或者核初始化
  • +
  • mini-batch的梯度下降
  • +
  • 动量法梯度下降:移动量不仅与梯度有关,还与前一时刻的移动量有关。
  • +
  • Adam:同时利用一阶动量和二阶动量进行优化
  • +
+

过拟合

+
    +
  • 早停
  • +
  • 正则:L1正则能让系数=0,L2可以让参数趋向于变小,对整体的影响就变小了,相当于参数变简单了
  • +
  • Dropout:随机删除一部分神经元(可视为一种集成学习)
  • +
  • 数据增强:增加训练样本集尽可能让他多样化,也可以增加模型的泛化能力
  • +
+

卷积神经网络

+
    +
  • 局部连接:我们认为是一个模式在一个比较小的范围内,而不是要看全局,有些模式比整个图像小得多,神经元不需要看整幅图像就能发现模式,与一个小区域连接所需的参数更少
  • +
  • 权值共享:同一个模式会在图像中不同的区域出现,不同位置可能都有这样的模式,这样做可以使得模型的参数变少
  • +
  • 池化:对像素进行下采样不影响目标,通过下采样可以让图像更小,网络处理图像需要的参数更少
  • +
+ + +
+ +
+
+ + + + + + +
+
+
研究生课程:模式识别与机器学习-期末复习
+
https://zhangzhao219.github.io/2022/12/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-final/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/13/6.824/Distributed-Systems-MIT-6.824-LEC-1/index.html b/2022/12/13/6.824/Distributed-Systems-MIT-6.824-LEC-1/index.html new file mode 100644 index 000000000..a4fb0de2b --- /dev/null +++ b/2022/12/13/6.824/Distributed-Systems-MIT-6.824-LEC-1/index.html @@ -0,0 +1,961 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-LEC 1 Introduction - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-LEC 1 Introduction

+ + +
+ +

MIT-6.824(Spring 2022)LEC 1 Introduction

+ +

MapReduce论文阅读

+

参考翻译

+ + +
+ +
+ + + +

方法提出

+
    +
  • 大量的数据分布在不同的机器上,为了能让某些算法在可以接受的时间内完成,需要将算法分配到不同的机器上一起并行运行
  • +
  • 受Lisp的启发,我们发现大多数的操作都可以分为两个部分,map和reduce +
      +
    • 首先将输入中的逻辑记录应用map操作转化为过渡的键值对
    • +
    • 然后将相同的键对应的值应用reduce操作,从而合并上一步产生的过渡数据
    • +
    +
  • +
+

编程模型

+

(WordCount)

+
map(String key, String value):
+// key: document name
+// value: document contents
+    for each word w in value:
+        EmitIntermediate(w, "1");
+reduce(String key, Iterator values):
+// key: a word
+// values: a list of counts
+    int result = 0;
+    for each v in values:
+        result += ParseInt(v);
+    Emit(AsString(result));
+

map函数输出每个单词和计数的数量,reduce汇总其中某个特定单词的数量并输出。

+
    +
  • 分布式查找:map函数匹配到了就直接输出,reduce函数不发挥作用
  • +
  • 计数URL访问频率:map函数对网页的日志进行处理并输出中间键值对,reduce函数再进行汇总处理
  • +
  • 反转网页-链接图:map函数在source网页中寻找target URL,输出 <target, source>键值对,reduce函数对目标URL汇总source并输出
  • +
  • 节点的主干词向量
  • +
  • 倒排索引
  • +
  • 分布式排序:map从每一条记录中提取键,reduce输出所有的键值对(后面详细说明)
  • +
+

Google对MapReduce的一种实现

+

zIslPU.md.png

+

如上图所示,map的过程是在多机器上调用的,其中分配的过程是自动化的,共分配了个节点进行。reduce过程是通过用户指定的节点数量,通过某种方法(如计算哈希值等)分配台机器进行。

+

其中有一个master节点,这个节点负责将任务进行分配,有些机器进行map操作,有些机器进行reduce操作等。

+

被分配到map任务的节点读取输入,将处理好的内容写入缓存,周期性的存入硬盘。存入时直接分为部分,并将数据存放的位置告知master

+

当一个节点被master通知要进行reduce时,通过RPC的方式从硬盘中读取数据到缓存中,进行处理并排序,保证相同的key出现在相同的位置

+

最终输出的时的文件,但是并不需要用户进行手动合并,因为这些文件通常是作为下一阶段的输入。

+

Master数据结构

+

对于每一个map任务或者reduce任务,都要保存任务的状态(已经完成或者未完成)以及工作节点的信息

+

对于每一个完成后的map任务,还要保存完成后的中间数据的位置和大小等信息

+

容错机制

+

机器太多了肯定有的机器会失效

+

Worker失效:Master会定期ping每一个Worker,如果没有得到响应,将这个节点标记为失效

+
    +
  • 如果节点的任务正在进行,将分配给它的任务还原到初始状态,给没有失效的节点去完成
  • +
  • 如果节点的任务已经完成,对于map任务要重做,因为无法访问这个节点的存储。对于reduce来说不需要,因为已经输出到文件了
  • +
  • map任务重做时会通知所有的reduce任务的节点
  • +
+

Master失效:Master的数据要经常备份,且由于只有一个Master,不太可能失效(因为被保护好了?),因此如果Master失效了会终止整个任务

+

故障时处理的机制:用户提供的Map和Reduce操作是输入确定性函数时,分布式的计算要保证任何情况下的输出都要一致没有错误.

+

使用map和reduce的原子提交特点来实现。map和reduce操作都写入临时文件中,完成操作后通知Master节点。如果Master节点被通知了另外一次,则直接忽略掉。reduce操作结束后将临时文件重命名为最终输出的文件,重命名操作也是原子性,最终只会有一个符合条件的文件名。

+

存储位置

+

尽量存储在本地的硬盘中,通过GFS把每个文件按64MB一个块,并在不同的机器上存储三份冗余的数据。

+

任务粒度

+

理想情况下都应该比物理节点数量大得多,在每台机器都执行大量的不同任务能够提高集群的动态的负载均衡能力,并且能够加快故障恢复的速度。

+

在我们的具体实现中对的取值有一定的限制,因为master必须执行)次调度,并且在内存中保存个状态(一个字节一个状态)

+

值通常由用户指定,实际使用中选择合适的值,以使得每一个独立任务都是处理大约的输入数据

+

MapReduce的合适执行比例:,使用台机器节点

+

备份任务

+

在运算过程中,如果有一台机器花了很长的时间才完成最后几个Map或Reduce任务,会导致MapReduce操作总的执行时间超过预期。

+

当一个MapReduce操作接近完成的时候,master会调度备用任务进程来一起执行最后的任务,谁完成了整个任务都算完成。

+

任务细节

+

在具体的实现上,对上面描述的简单mapreduce过程可以进行优化

+
    +
  1. reduce前需要先分配map的结果,使用哈希函数的方式分配的比较均衡,但是可能有一些场景下需要将特定的键值对分配到一起,因此用户可以传入自定义的类似于哈希的函数进行分配
  2. +
  3. 确保在给定的分区中,键值对数据的处理顺序是按照键进行排序后的。排序后对后面的任务都有利
  4. +
  5. Map函数产生的中间key值的重复数据会占很大的比重(成千上万个<the,1>),因此允许用户指定一个可选的combiner函数,combiner函数首先在本地将这些记录进行一次合并,然后将合并的结果再通过网络发送出去。一般情况下,Combiner和Reduce函数相同。区别在于输出到最终文件还是中间文件。
  6. +
  7. MapReduce支持不同的格式的输入数据,如文本或者键值对等,同时提供Reader接口使用户可以自定义输出,只要保证输入是可以分割的就可以
  8. +
  9. 某些情况下,在Map或Reduce操作过程中增加辅助的输出文件会比较省事。(但是这里不支持?)
  10. +
  11. 用户程序中的bug导致Map或者Reduce函数在处理某些记录的时候会崩溃掉。这个bug可能很难找。因此提供了一种执行模式,在这种模式下,为了保证保证整个处理能继续进行,MapReduce会检测哪些记录导致确定性的crash,并且跳过这些记录不处理。
  12. +
  13. 在远程分布式节点上调试程序非常困难,因此开发了一套MapReduce库的本地实现版本,可以调试使用
  14. +
  15. master使用嵌入式的HTTP服务器(如Jetty)显示一组状态信息页面,用户可以监控各种执行状态
  16. +
  17. MapReduce库使用计数器统计不同事件发生次数。比如,用户可能想统计已经处理了多少个单词、已经索引的多少篇German文档等等。可以用于MapReduce操作的完整性检查。
  18. +
+

实验表现

+
    +
  • 在大约1TB的数据中进行特定的模式匹配(从海量数据中抽取感兴趣的数据)
  • +
  • 对大约1TB的数据进行排序(对数据的形式进行转换)
  • +
+

应用

+
    +
  1. 大规模机器学习问题
  2. +
  3. Google News和Froogle产品的集群问题
  4. +
  5. 从公众查询产品(比如Google的Zeitgeist)的报告中抽取数据。
  6. +
  7. 从大量的新应用和新产品的网页中提取有用信息(比如,从大量的位置搜索网页中抽取地理位置信息)。
  8. +
  9. 大规模的图形计算。
  10. +
+

MapReduce的成功取决于采用MapReduce库能够在不到半个小时时间内写出一个简单的程序,这个简单的程序能够在上千台机器的组成的集群上做大规模并发处理,极大的加快了开发和原形设计的周期。另外,采用MapReduce库,可以让完全没有分布式和/或并行系统开发经验的程序员很容易的利用大量的资源,开发出分布式和/或并行处理的应用。

+

结论

+

MapReduce的成功有几个方面:

+
    +
  1. MapReduce封装了并行处理、容错处理、数据本地化优化、负载均衡等等技术难点的细节,使得MapReduce库易于使用。
  2. +
  3. 大量不同类型的问题都可以通过MapReduce简单解决。
  4. +
  5. 实现了在数千台计算机组成的大型集群上灵活部署运行的MapReduce,使得有效利用这些计算资源变得非常简单,适合用来解决其他需要大量计算的问题。
  6. +
+

从MapReduce开发过程中也学到了不少东西。

+
    +
  1. 使用固定的编程模式使得并行和分布式计算非常容易,也易于构造容错的计算环境;
  2. +
  3. 网络带宽是稀有资源。大量的系统优化是针对减少网络传输量为目的的:本地优化策略使大量的数据从本地磁盘读取,中间文件写入本地磁盘、并且只写一份中间文件也节约了网络带宽
  4. +
  5. 备份服务器执行相同的任务可以减少性能缓慢的机器带来的负面影响(硬件配置的不平衡),同时解决了由于机器失效导致的数据丢失问题。
  6. +
+

LEC 1

+

什么是分布式系统

+
    +
  • 多个计算机通过网络连接,因此只能通过发送和接收数据包的形式进行交互,不能共享内存等等。
  • +
  • 支持应用程序的基础设施主干架构
  • +
+

分布式系统的作用

+
    +
  • 连接物理上分离的机器-允许用户之间的数据共享
  • +
  • 通过并行提升性能
  • +
  • 容错机制-挂掉的机器不能影响服务
  • +
  • 通过将程序分布在不同的机器上获得安全性(例如一台机器只用于登录服务的验证)
  • +
+

分布式系统的发展历程

+
    +
  • 起始于局域网出现(AFS)-DNS、Email
  • +
  • 数据中心(大量数据)和大型网站(大量用户)
  • +
  • 云计算
  • +
  • 很难跟上时代发展节奏,一直在不断努力
  • +
+

分布式系统的挑战

+
    +
  • 很多并行的部分
  • +
  • 容错机制
  • +
  • 很难实现分布式的性能优势
  • +
+

判断系统是否正常工作非常困难,例如两台机器间的网络挂掉,两边都认为对方挂掉了,因此对外提供了两份服务。

+

课程关注的内容

+

课程不关注应用程序,只关注基础设施,也就是支撑这些应用程序正确工作的部分。

+

关注的三个方面:存储、计算和通信

+

抽象:分布式系统的抽象与单机系统的抽象基本相同

+

重点内容

+

容错机制

+
    +
  • 可用性:使系统高可用的技术,某个节点挂掉仍然可以正常工作 +
      +
    • 关键:复制
    • +
    +
  • +
  • 可恢复性:挂掉的机器重启后还能回到分布式系统中继续工作 +
      +
    • 关键:日志或事务
    • +
    +
  • +
+

一致性:分布式系统与单机的行为相同

+

性能:不同类型的一致性和容错机制与性能相关

+
    +
  • 吞吐量
  • +
  • 低延迟:某些很慢的机器会拖慢整个程序的运行过程
  • +
+

实现细节:如何实现并发、远程过程调用等等

+

MapReduce

+

背景

+

在Google早期的数据中心,有一个搜索引擎,需要构建万维网的倒排索引,允许用户上网查询。

+

在这个过程中处理TB级别的数据需要耗费几个小时。

+

为每一个应用都编写一个这种系统很困难,因此提出了MapReduce,使得构建不同应用的分布式程序比较轻松

+

不过这些应用必须要能分成map和reduce两个部分,然后放到MapReduce框架下运行,不需要再关注其他细节(如容错机制等等)

+

框架图

+

zIslPU.md.png

+
    +
  1. Map操作统计所有的输入文件,不同机器节点之间没有通信
  2. +
  3. Shuffle:从每个Map获取输出,按照键进行排序(最难的操作)
  4. +
  5. 在键相同的字段上运行Reduce
  6. +
+

主要的网络通信在于传输map产生的中间文件给reduce使用

+

容错机制

+

如果一个机器在一定的时间内没有对Coordinator作出响应,就认为这个机器已经挂掉了,因此Coordinator会重新安排其他机器重启它的任务。

+

map和reduce任务可能会运行两次,例如Coordinator认为这个机器挂掉了,把它的任务分配给别人了,但是实际上这个机器并没有挂掉。最终使用重命名操作的原子性确保只存储一个结果。

+

Coordinator会挂掉吗?挂掉了整个任务就都要重新跑了,一般不会挂掉。

+

一些机器可能会运行很慢从而拖累整个任务的进程。当整个任务快要结束的时候,会复制任务到其他的空闲节点上一起做,谁先做完取谁的。

+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-LEC 1 Introduction
+
https://zhangzhao219.github.io/2022/12/13/6.824/Distributed-Systems-MIT-6.824-LEC-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/15/6.824/Distributed-Systems-MIT-6.824-LEC-2/index.html b/2022/12/15/6.824/Distributed-Systems-MIT-6.824-LEC-2/index.html new file mode 100644 index 000000000..babd82ad2 --- /dev/null +++ b/2022/12/15/6.824/Distributed-Systems-MIT-6.824-LEC-2/index.html @@ -0,0 +1,1213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-LEC 2 RPC and Threads - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-LEC 2 RPC and Threads

+ + +
+ +

MIT-6.824(Spring 2022)LEC 2 RPC and Threads

+ +

Go快速入门

+

How do Go channels work? How does Go make sure they are synchronized between the many possible goroutines?

+

https://golang.org/src/runtime/chan.go

+

At a high level, a chan is a struct holding a buffer and a lock. Sending on a channel involves acquiring the lock, waiting (perhaps releasing the CPU) until some thread is receiving, and handing off the message. Receiving involves acquiring the lock and waiting for a sender. You could implement your own channels with Go sync.Mutex and sync.Cond.

+

LEC 2

+

为什么使用Go?

+
    +
  • 对线程和RPC有很好的支持(更适合分布式编程)
  • +
  • 垃圾收集器,不需要用户自己释放内存
  • +
  • 简单易学
  • +
  • 自带编译器,不是 Python 那样的解释型语言
  • +
+

线程

+

在一个进程中并行运行多个线程

+

线程原语:开启线程、退出线程(隐式)、停止线程(挂在一边不懂)、恢复线程

+

为什么需要线程?

+

支持并发

+
    +
  • 输入/输出并发
  • +
  • 多核并行
  • +
  • 方便(例如定期执行后台活动等)
  • +
+

数量可以不考虑,按照需求创建线程即可

+

线程编程挑战

+
    +
  • 竞争情况(同时对某一个变量进行写操作) +
      +
    • 可能大多数情况运行都很好,但是确实在某些条件下得不到想要的结果
    • +
    • 解决的两种方法 +
        +
      • 避免共享变量(channels)go推荐使用
      • +
      • 使用锁(mutex)
      • +
      +
    • +
    +
  • +
  • 协调问题:一个线程必须等待另一个线程完成后才能继续进行 +
      +
    • channels
    • +
    • condition variables
    • +
    +
  • +
  • 死锁问题:两边都在等待对方
  • +
+

Go应对挑战的机制

+

channels和condition variables

+
    +
  • 如果不共享内存,只想让线程互相进行通信,则应该使用channels
  • +
  • 如果需要共享内存,应该使用锁和condition variables
  • +
+

条件变量和channel实例

+

分配条件变量并且和锁关联,不满足条件进入睡眠状态,并释放关联的锁。

+

在goroutine运行的最后唤醒睡眠状态的线程,重新进行判断

+
package main
+
+import "sync"
+import "time"
+import "math/rand"
+
+func main() {
+	rand.Seed(time.Now().UnixNano())
+
+	count := 0
+	finished := 0
+	var mu sync.Mutex
+	cond := sync.NewCond(&mu)
+
+	for i := 0; i < 10; i++ {
+		go func() {
+			vote := requestVote()
+			mu.Lock()
+			defer mu.Unlock()
+			if vote {
+				count++
+			}
+			finished++
+			cond.Broadcast()
+		}()
+	}
+
+	mu.Lock()
+	for count < 5 && finished != 10 {
+		cond.Wait()
+	}
+	if count >= 5 {
+		println("received 5+ votes!")
+	} else {
+		println("lost")
+	}
+	mu.Unlock()
+}
+
+func requestVote() bool {
+	time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
+	return rand.Int() % 2 == 0
+}
+
+
package main
+
+import "time"
+import "math/rand"
+
+func main() {
+	rand.Seed(time.Now().UnixNano())
+
+	count := 0
+	ch := make(chan bool)
+	for i := 0; i < 10; i++ {
+		go func() {
+			ch <- requestVote()
+		}()
+	}
+	for i := 0; i < 10; i++ {
+		v := <-ch
+		if v {
+			count += 1
+		}
+	}
+	if count >= 5 {
+		println("received 5+ votes!")
+	} else {
+		println("lost")
+	}
+}
+
+func requestVote() bool {
+	time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
+	return rand.Int()%2 == 0
+}
+
+

go tour 爬虫练习

+
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+//
+// Several solutions to the crawler exercise from the Go tutorial
+// https://tour.golang.org/concurrency/10
+//
+
+//
+// Serial crawler
+//
+
+func Serial(url string, fetcher Fetcher, fetched map[string]bool) {
+	if fetched[url] {
+		return
+	}
+	fetched[url] = true
+	urls, err := fetcher.Fetch(url)
+	if err != nil {
+		return
+	}
+	for _, u := range urls {
+		Serial(u, fetcher, fetched)
+	}
+	return
+}
+
+//
+// Concurrent crawler with shared state and Mutex
+//
+
+type fetchState struct {
+	mu      sync.Mutex
+	fetched map[string]bool
+}
+
+func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {
+	f.mu.Lock()
+	already := f.fetched[url]
+	f.fetched[url] = true
+	f.mu.Unlock()
+
+	if already {
+		return
+	}
+
+	urls, err := fetcher.Fetch(url)
+	if err != nil {
+		return
+	}
+	var done sync.WaitGroup
+	for _, u := range urls {
+		done.Add(1)
+		go func(u string) {
+			defer done.Done()
+			ConcurrentMutex(u, fetcher, f)
+		}(u)
+	}
+	done.Wait()
+	return
+}
+
+func makeState() *fetchState {
+	f := &fetchState{}
+	f.fetched = make(map[string]bool)
+	return f
+}
+
+//
+// Concurrent crawler with channels
+//
+
+func worker(url string, ch chan []string, fetcher Fetcher) {
+	urls, err := fetcher.Fetch(url)
+	if err != nil {
+		ch <- []string{}
+	} else {
+		ch <- urls
+	}
+}
+
+func coordinator(ch chan []string, fetcher Fetcher) {
+	n := 1
+	fetched := make(map[string]bool)
+	for urls := range ch {
+		for _, u := range urls {
+			if fetched[u] == false {
+				fetched[u] = true
+				n += 1
+				go worker(u, ch, fetcher)
+			}
+		}
+		n -= 1
+		if n == 0 {
+			break
+		}
+	}
+}
+
+func ConcurrentChannel(url string, fetcher Fetcher) {
+	ch := make(chan []string)
+	go func() {
+		ch <- []string{url}
+	}()
+	coordinator(ch, fetcher)
+}
+
+//
+// main
+//
+
+func main() {
+	fmt.Printf("=== Serial===\n")
+	Serial("http://golang.org/", fetcher, make(map[string]bool))
+
+	fmt.Printf("=== ConcurrentMutex ===\n")
+	ConcurrentMutex("http://golang.org/", fetcher, makeState())
+
+	fmt.Printf("=== ConcurrentChannel ===\n")
+	ConcurrentChannel("http://golang.org/", fetcher)
+}
+
+//
+// Fetcher
+//
+
+type Fetcher interface {
+	// Fetch returns a slice of URLs found on the page.
+	Fetch(url string) (urls []string, err error)
+}
+
+// fakeFetcher is Fetcher that returns canned results.
+type fakeFetcher map[string]*fakeResult
+
+type fakeResult struct {
+	body string
+	urls []string
+}
+
+func (f fakeFetcher) Fetch(url string) ([]string, error) {
+	if res, ok := f[url]; ok {
+		fmt.Printf("found:   %s\n", url)
+		return res.urls, nil
+	}
+	fmt.Printf("missing: %s\n", url)
+	return nil, fmt.Errorf("not found: %s", url)
+}
+
+// fetcher is a populated fakeFetcher.
+var fetcher = fakeFetcher{
+	"http://golang.org/": &fakeResult{
+		"The Go Programming Language",
+		[]string{
+			"http://golang.org/pkg/",
+			"http://golang.org/cmd/",
+		},
+	},
+	"http://golang.org/pkg/": &fakeResult{
+		"Packages",
+		[]string{
+			"http://golang.org/",
+			"http://golang.org/cmd/",
+			"http://golang.org/pkg/fmt/",
+			"http://golang.org/pkg/os/",
+		},
+	},
+	"http://golang.org/pkg/fmt/": &fakeResult{
+		"Package fmt",
+		[]string{
+			"http://golang.org/",
+			"http://golang.org/pkg/",
+		},
+	},
+	"http://golang.org/pkg/os/": &fakeResult{
+		"Package os",
+		[]string{
+			"http://golang.org/",
+			"http://golang.org/pkg/",
+		},
+	},
+}
+

RPC-远程过程调用

+

RPC:在客户端上调用在服务器端实现的函数-传递参数并返回结果

+

实际过程:

+
    +
  • 在客户端上调用stub过程:构建一个消息,包括调用哪个函数,函数的参数,参数类型等等。
  • +
  • 通过网络发送给服务器上对应的stub
  • +
  • 在服务器上调用函数
  • +
  • 返回给服务器的stub
  • +
  • 返回给客户端的stub(这个期间一直在等待)
  • +
  • 返回结果
  • +
+

示例

+
package main
+
+import (
+	"fmt"
+	"log"
+	"net"
+	"net/rpc"
+	"sync"
+)
+
+//
+// Common RPC request/reply definitions
+//
+
+type PutArgs struct {
+	Key   string
+	Value string
+}
+
+type PutReply struct {
+}
+
+type GetArgs struct {
+	Key string
+}
+
+type GetReply struct {
+	Value string
+}
+
+//
+// Client
+//
+
+func connect() *rpc.Client {
+	client, err := rpc.Dial("tcp", ":1234")
+	if err != nil {
+		log.Fatal("dialing:", err)
+	}
+	return client
+}
+
+func get(key string) string {
+	client := connect()
+	args := GetArgs{"subject"}
+	reply := GetReply{}
+	err := client.Call("KV.Get", &args, &reply)
+	if err != nil {
+		log.Fatal("error:", err)
+	}
+	client.Close()
+	return reply.Value
+}
+
+func put(key string, val string) {
+	client := connect()
+	args := PutArgs{"subject", "6.824"}
+	reply := PutReply{}
+	err := client.Call("KV.Put", &args, &reply)
+	if err != nil {
+		log.Fatal("error:", err)
+	}
+	client.Close()
+}
+
+//
+// Server
+//
+
+type KV struct {
+	mu   sync.Mutex
+	data map[string]string
+}
+
+func server() {
+	kv := new(KV)
+	kv.data = map[string]string{}
+	rpcs := rpc.NewServer()
+	rpcs.Register(kv)
+	l, e := net.Listen("tcp", ":1234")
+	if e != nil {
+		log.Fatal("listen error:", e)
+	}
+	go func() {
+		for {
+			conn, err := l.Accept()
+			if err == nil {
+				go rpcs.ServeConn(conn)
+			} else {
+				break
+			}
+		}
+		l.Close()
+	}()
+}
+
+func (kv *KV) Get(args *GetArgs, reply *GetReply) error {
+	kv.mu.Lock()
+	defer kv.mu.Unlock()
+
+	reply.Value = kv.data[args.Key]
+
+	return nil
+}
+
+func (kv *KV) Put(args *PutArgs, reply *PutReply) error {
+	kv.mu.Lock()
+	defer kv.mu.Unlock()
+
+	kv.data[args.Key] = args.Value
+
+	return nil
+}
+
+//
+// main
+//
+
+func main() {
+	server()
+
+	put("subject", "6.824")
+	fmt.Printf("Put(subject, 6.824) done\n")
+	fmt.Printf("get(subject) -> %s\n", get("subject"))
+}
+

RPC失败

+
    +
  • 至少一次:失败后(没有接到服务器的响应)会自动重试 +
      +
    • 有可能多次执行
    • +
    +
  • +
  • 最多一次:服务器端实现过滤重复,确保最多只能执行一次(Go的RPC实现)
  • +
  • 正好一次:很难实现
  • +
+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-LEC 2 RPC and Threads
+
https://zhangzhao219.github.io/2022/12/15/6.824/Distributed-Systems-MIT-6.824-LEC-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月15日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/15/6.824/Distributed-Systems-MIT-6.824-Lab-1/index.html b/2022/12/15/6.824/Distributed-Systems-MIT-6.824-Lab-1/index.html new file mode 100644 index 000000000..b277367c5 --- /dev/null +++ b/2022/12/15/6.824/Distributed-Systems-MIT-6.824-Lab-1/index.html @@ -0,0 +1,1694 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-Lab 1 MapReduce - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-Lab 1 MapReduce

+ + +
+ +

MIT-6.824(Spring 2022)Lab 1 MapReduce

+ +

6.824 Lab 1: MapReduce

+

简介

+

构建一个MapReduce系统

+
    +
  1. 实现一个worker进程,调用Map和Reduce函数、处理读写文件,
  2. +
  3. 实现coordinator进程,向worker分发任务并提供容错机制。
  4. +
+

准备开始

+

src/main/mrsequential.go 中提供了串行的mapreduce程序,在单进程里面直接顺序执行Map操作和Reduce操作

+

同时提供了一些MapReduce的应用程序:

+

mrapps/wc.go:WordCount程序

+

mrapps/indexer.go:text-indexer

+

按照如下的方式运行串行的mapreduce程序:

+
cd src/main
+go build -race -buildmode=plugin ../mrapps/wc.go
+rm mr-out*
+go run -race mrsequential.go wc.so pg*.txt
+more mr-out-0
+

输出的文件中是对文件的WordCount结果

+

代码理解

+

插件模式编译

+

参考资料

+

Go是静态编译型语言,在编译时就将所有引用的包(库)全部加载打包到最终的可执行程序(或库文件)中,因此并不能在运行时动态加载其他共享库。Go Plugin提供了这样一种方式,能够让你在运行时动态加载外部功能。

+
    +
  • 可插拔:有了Plugin,我的程序可以根据需要随时替换其中某些部件而不用修改我的程序;
  • +
  • 动态加载的需要:有些模块只有在运行时才能确定,需要动态加载外部的功能模块;
  • +
  • 独立开发:Plugin 可以和主程序独立建设,主程序只需要制定好框架,实现默认(模版)功能。Plugin 可根据用户需求随时自行扩展开发,运行时随意替换,提高了程序的可定制性;
  • +
+

type Plugin即Golang加载的插件,与之有关的两个方法:

+
    +
  • Open: 根据参数path提供的插件路径加载这个插件,并返回插件这个插件结构的指针*Plugin
  • +
  • Lookup: *Plugin的惟一方法,通过名称symName在插件中寻找对应的变量或方法,以Symbol的形式返回
  • +
+

因此这一行命令将 wc.go文件编译成了一个插件 wc.so(默认文件名),从而可以插入到MapReduce主程序中运行。

+

wc.go-Map函数

+
// The map function is called once for each file of input. The first
+// argument is the name of the input file, and the second is the
+// file's complete contents. You should ignore the input file name,
+// and look only at the contents argument. The return value is a slice
+// of key/value pairs.
+func Map(filename string, contents string) []mr.KeyValue {
+	// function to detect word separators.
+	ff := func(r rune) bool { return !unicode.IsLetter(r) }
+
+	// split contents into an array of words.
+	words := strings.FieldsFunc(contents, ff)
+
+	kva := []mr.KeyValue{}
+	for _, w := range words {
+		kv := mr.KeyValue{w, "1"}
+		kva = append(kva, kv)
+	}
+	return kva
+}
+

对每一个传进来的字符串,通过 strings.FieldsFunc函数找到字符串的分割点,分割成单独的单词,构造成KeyValue结构体并合并成切片返回

+

wc.go-Reduce函数

+
// The reduce function is called once for each key generated by the
+// map tasks, with a list of all the values created for that key by
+// any map task.
+func Reduce(key string, values []string) string {
+	// return the number of occurrences of this word.
+	return strconv.Itoa(len(values))
+}
+

直接以字符串的形式返回values的长度

+

串行MapReduce运行

+

导入插件

+
// load the application Map and Reduce functions
+// from a plugin file, e.g. ../mrapps/wc.so
+func loadPlugin(filename string) (func(string, string) []mr.KeyValue, func(string, []string) string) {
+	p, err := plugin.Open(filename)
+	if err != nil {
+		log.Fatalf("cannot load plugin %v", filename)
+	}
+	xmapf, err := p.Lookup("Map")
+	if err != nil {
+		log.Fatalf("cannot find Map in %v", filename)
+	}
+	mapf := xmapf.(func(string, string) []mr.KeyValue)
+	xreducef, err := p.Lookup("Reduce")
+	if err != nil {
+		log.Fatalf("cannot find Reduce in %v", filename)
+	}
+	reducef := xreducef.(func(string, []string) string)
+
+	return mapf, reducef
+}
+

从编译好的*.so文件中查找Map函数和Reduce函数,通过函数的返回值类型进行类型推断,最终返回两个函数通过主函数里面的变量进行接收

+
mapf, reducef := loadPlugin(os.Args[1])
+

打开文件,进行Map操作

+
       //
+// read each input file,
+// pass it to Map,
+// accumulate the intermediate Map output.
+//
+intermediate := []mr.KeyValue{}
+for _, filename := range os.Args[2:] {
+	file, err := os.Open(filename)
+	if err != nil {
+		log.Fatalf("cannot open %v", filename)
+	}
+	content, err := ioutil.ReadAll(file)
+	if err != nil {
+		log.Fatalf("cannot read %v", filename)
+	}
+	file.Close()
+	kva := mapf(filename, string(content))
+	intermediate = append(intermediate, kva...)
+}
+

pg-*.txt会匹配到所有满足条件的文件,将文件逐个打开,读取文件内容,通过Map函数处理成中间数据格式,存入中间变量intermediate

+

排序

+

对中间变量的切片按照键的字典序进行排序

+
sort.Sort(ByKey(intermediate))
+// for sorting by key.
+func (a ByKey) Len() int           { return len(a) }
+func (a ByKey) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key }
+

这里是通过实现Sort的接口实现了自定义排序

+

统计Reduce

+
//
+// call Reduce on each distinct key in intermediate[],
+// and print the result to mr-out-0.
+//
+i := 0
+for i < len(intermediate) {
+	j := i + 1
+	for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
+		j++
+	}
+	values := []string{}
+	for k := i; k < j; k++ {
+		values = append(values, intermediate[k].Value)
+	}
+	output := reducef(intermediate[i].Key, values)
+
+	// this is the correct format for each line of Reduce output.
+	fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)
+
+	i = j
+}
+

由于已经排好顺序了,从左到右遍历一遍就可以统计每一个键出现的数量,然后输出到文件即可。

+

我的工作

+

实现一个分布式MapReduce,由coordinator和worker两个程序组成。

+

coordinator进程只有一个,worker进程有一个或多个并行执行。

+

worker进程将通过RPC与coordinator进程进行通信。每个worker进程将向coordinator进程请求任务,从一个或多个文件中读取任务的输入,执行任务,并将任务的输出写入一个或更多个文件。

+

coordinator进程应该注意到一个worker进程是否没有在合理的时间内完成其任务(10秒),并将相同的任务交给另一个worker进程。

+

coordinator和worker的“main”函数在 main/mrcordinator.gomain/mrworker.go

+

实现应该在 mr/coordinator.gomr/worker.gomr/rpc.go中。

+

测试运行:

+
go build -race -buildmode=plugin ../mrapps/wc.go
+rm mr-out*
+go run -race mrcoordinator.go pg-*.txt
+
+go run -race mrworker.go wc.so
+go run -race mrworker.go wc.so
+

测试脚本:

+
bash test-mr.sh
+

代码理解

+

待补充的代码提供了一个RPC的示例

+

启动Worker后,会调用CallExample()函数

+
// example function to show how to make an RPC call to the coordinator.
+//
+// the RPC argument and reply types are defined in rpc.go.
+func CallExample() {
+
+	// declare an argument structure.
+	args := ExampleArgs{}
+
+	// fill in the argument(s).
+	args.X = 99
+
+	// declare a reply structure.
+	reply := ExampleReply{}
+
+	// send the RPC request, wait for the reply.
+	// the "Coordinator.Example" tells the
+	// receiving server that we'd like to call
+	// the Example() method of struct Coordinator.
+	ok := call("Coordinator.Example", &args, &reply)
+	if ok {
+		// reply.Y should be 100.
+		fmt.Printf("reply.Y %v\n", reply.Y)
+	} else {
+		fmt.Printf("call failed!\n")
+	}
+}
+

函数构建了RPC的结构体,然后调用call函数并接收响应

+

在这里体现了RPC的核心思想:在这里看起来就是调用的本地函数call,但是实际上call内部是与coordinator进行通信,然后在远程得到返回值后返回给reply结构体,因此为“远程过程调用”

+

call函数:

+
// send an RPC request to the coordinator, wait for the response.
+// usually returns true.
+// returns false if something goes wrong.
+func call(rpcname string, args interface{}, reply interface{}) bool {
+	// c, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
+	sockname := coordinatorSock()
+	c, err := rpc.DialHTTP("unix", sockname)
+	if err != nil {
+		log.Fatal("dialing:", err)
+	}
+	defer c.Close()
+
+	err = c.Call(rpcname, args, reply)
+	if err == nil {
+		return true
+	}
+
+	fmt.Println(err)
+	return false
+}
+

注意coordinatorSock()方法,会获取一个临时文件,通信是通过这个临时文件进行的。

+

在coordinator.go内部,RPC指定的方法"Coordinator.Example":

+
// an example RPC handler.
+//
+// the RPC argument and reply types are defined in rpc.go.
+func (c *Coordinator) Example(args *ExampleArgs, reply *ExampleReply) error {
+	reply.Y = args.X + 1
+	return nil
+}
+

因此返回的结构体中reply.Y的值就为100

+

在启动Worker前要先启动Coordinator,启动后首先创建一个Coordinator结构:

+
// create a Coordinator.
+// main/mrcoordinator.go calls this function.
+// nReduce is the number of reduce tasks to use.
+func MakeCoordinator(files []string, nReduce int) *Coordinator {
+	c := Coordinator{}
+
+	// Your code here.
+
+	c.server()
+	return &c
+}
+

其中调用server方法,监听Worker的RPC:

+
// start a thread that listens for RPCs from worker.go
+func (c *Coordinator) server() {
+	rpc.Register(c)
+	rpc.HandleHTTP()
+	//l, e := net.Listen("tcp", ":1234")
+	sockname := coordinatorSock()
+	os.Remove(sockname)
+	l, e := net.Listen("unix", sockname)
+	if e != nil {
+		log.Fatal("listen error:", e)
+	}
+	go http.Serve(l, nil)
+}
+

Coordinator会不断检测Done方法的返回值,一旦为true,Coordinator就会退出:

+
// main/mrcoordinator.go calls Done() periodically to find out
+// if the entire job has finished.
+func (c *Coordinator) Done() bool {
+	ret := false
+
+	// Your code here.
+
+	return ret
+}
+

Map简单实现

+

首先考虑简单一些,不考虑并行、容错处理等,先把整个的流程跑通。

+

首先跑通Map流程

+

Coordinator的数据结构:

+
type Coordinator struct {
+	// Your definitions here.
+	MapTask    []MapTaskInformation    // Map任务列表
+	ReduceTask []ReduceTaskInformation // Reduce任务列表
+}
+

内部有两个切片,分别对应Map的任务列表和Reduce的任务列表。

+

两个任务列表是在Coordinator启动的时候就设置好:

+
// create a Coordinator.
+// main/mrcoordinator.go calls this function.
+// nReduce is the number of reduce tasks to use.
+func MakeCoordinator(files []string, nReduce int) *Coordinator {
+
+	mapTaskSlice := []MapTaskInformation{}
+
+	for id, fileName := range files {
+		mapTaskSlice = append(mapTaskSlice, MapTaskInformation{
+			Id:                   id + 1,
+			State:                0,
+			NReduce:              nReduce,
+			OriginFileName:       fileName,
+			IntermediateFileName: "mr-" + strconv.Itoa(id+1) + "-",
+		})
+	}
+
+	reduceTaskSlice := []ReduceTaskInformation{}
+
+	for i := 0; i < nReduce; i++ {
+		reduceTaskSlice = append(reduceTaskSlice, ReduceTaskInformation{
+			Id:             i + 1,
+			State:          0,
+			OriginFileName: "mr-0-" + strconv.Itoa(i+1),
+			OutputFileName: "mr-" + strconv.Itoa(i+1),
+		})
+	}
+
+	c := Coordinator{
+		MapTask:    mapTaskSlice,
+		ReduceTask: reduceTaskSlice,
+	}
+
+	// Your code here.
+
+	c.server()
+	return &c
+}
+

其中为Map和Reduce暂时设计的数据结构:

+
type MapTaskInformation struct {
+	Id                   int    // 任务唯一编码
+	State                int    // 0表示未开始,1表示正在进行,2表示已经完成
+	NReduce              int    // 分成Reduce任务的数量
+	OriginFileName       string // 原始文件名称
+	IntermediateFileName string // Map任务完成后的文件名称(中间文件)
+}
+type ReduceTaskInformation struct {
+	Id             int    // 任务唯一编码
+	State          int    // 0表示未开始,1表示正在进行,2表示已经完成
+	OriginFileName string // Reduce的初始文件名称(中间文件)
+	OutputFileName string // Reduce任务完成后的最终文件名称
+}
+

Worker启动时,通过RPC向Coordinator要一个任务

+
// main/mrworker.go calls this function.
+func Worker(mapf func(string, string) []KeyValue,
+	reducef func(string, []string) string) {
+
+	args := TaskInformation{}
+	reply := TaskInformation{}
+
+	ok := call("Coordinator.AsssignTask", &args, &reply)
+

Coordinator会遍历自己内部的所有任务列表,找到第一个还没有完成的任务分配给这个Worker:

+
// 分配任务
+func (c *Coordinator) AsssignTask(args *TaskInformation, reply *TaskInformation) error {
+	isMapfinished := true
+	//遍历所有的Map任务信息,将未开始的分配给这个节点
+	for i, mapTask := range c.MapTask {
+		if mapTask.State == 0 {
+			isMapfinished = false
+			reply.Id = mapTask.Id
+			reply.TaskType = "map"
+			reply.InputFileName = mapTask.OriginFileName
+			reply.OutputFileName = mapTask.IntermediateFileName
+			reply.NReduce = mapTask.NReduce
+			c.MapTask[i].State = 1
+			return nil
+		} else if mapTask.State == 1 {
+			isMapfinished = false
+		}
+	}
+	// 如果所有的Map任务都完成了,就遍历Reduce任务
+	if isMapfinished {
+		for _, reduceTask := range c.ReduceTask {
+			if reduceTask.State == 0 {
+				return nil
+			}
+		}
+	}
+	return nil
+}
+

Worker接收到任务后使用插件中的Map函数进行处理,并将成功完成任务的消息通过RPC的方式返回给Coordinator

+
if ok {
+	fmt.Println("Call Success!")
+	if reply.TaskType == "map" {
+		fmt.Printf("Map Task!\n")
+		intermediate := []KeyValue{}
+		file, err := os.Open(reply.InputFileName)
+		if err != nil {
+			log.Fatalf("cannot open %v", reply.InputFileName)
+		}
+		content, err := io.ReadAll(file)
+		if err != nil {
+			log.Fatalf("cannot read %v", reply.InputFileName)
+		}
+		file.Close()
+		kva := mapf(reply.InputFileName, string(content))
+		intermediate = append(intermediate, kva...)
+
+		// 排序
+		sort.Sort(ByKey(intermediate))
+
+		fmt.Println(intermediate)
+		args = reply
+		call("Coordinator.TaskFinish", &args, &reply)
+

Coordinator接收消息,将自己内部的任务状态修改,后续就不会再将这个任务分配给Worker了。

+
// 接收任务已经完成的信息
+func (c *Coordinator) TaskFinish(args *TaskInformation, reply *TaskInformation) error {
+	if args.TaskType == "map" {
+		c.MapTask[args.Id-1].State = 2
+	} else if args.TaskType == "reduce" {
+		c.ReduceTask[args.Id-1].State = 2
+	}
+	return nil
+}
+

问题:

+
    +
  1. Worker要任务的时候Coordinator去列表中遍历是不是有点太傻了,有更好的办法吗?比如Coordinator维护未完成的和已完成的任务列表,然后动态更新?
  2. +
  3. 定义的struct数据结构不一定合理,还要看后面怎么用
  4. +
  5. RPC传递的数据结构不是很合理,而且有大量的冗余,比如后面的消息args和reply几乎完全相同,后面需要修改
  6. +
+

Reduce简单实现

+

首先在Worker的主函数增加一层循环,从而使Worker不断请求任务,由Coordinator按需分配

+

首先要构造中间文件,也就是map结束后的文件需要存起来,然后才能用reduce去处理

+
// 循环创建NReduce个文件准备保存
+encoderList := make([]*json.Encoder, 0)
+for i := 0; i < reply.NReduce; i++ {
+	fileName := reply.OutputFileName + strconv.FormatInt(int64(i+1), 10)
+	tempFile, err := os.Create(fileName)
+	if err != nil {
+		log.Fatalf("cannot create %v", fileName)
+	}
+	defer tempFile.Close()
+	encoderList = append(encoderList, json.NewEncoder(tempFile))
+}
+for i, v := range intermediate {
+	encoderList[ihash(v.Key)%reply.NReduce].Encode(&intermediate[i])
+}
+

map在保存的时候要直接分成NReduce的文件,文件的内容是由哈希函数对键进行映射后得到的,保证键大致平均分到NReduce个节点上

+

保存文件的时候使用的是json的格式,保存的过程有些慢,需要对整个map的结果全部遍历一遍,后续可以考虑并行处理?

+

Reduce内容:

+
} else if reply.TaskType == "reduce" {
+
+	ofile, _ := os.Create(reply.OutputFileName)
+
+	fmt.Printf("Reduce Task!\n")
+	kva := make([]KeyValue, 0)
+	for p := 1; p <= 8; p++ {
+		filename := strings.Replace(reply.InputFileName, "*", strconv.FormatInt(int64(p), 10), 1)
+		fmt.Println(filename)
+		file, err := os.Open(filename)
+		if err != nil {
+			log.Fatalf("cannot open %v", filename)
+		}
+		dec := json.NewDecoder(file)
+		for {
+			var kv KeyValue
+			if err := dec.Decode(&kv); err != nil {
+				break
+			}
+			kva = append(kva, kv)
+		}
+	}
+	// 排序
+	sort.Sort(ByKey(kva))
+	//
+	// call Reduce on each distinct key in intermediate[],
+	// and print the result to mr-out-0.
+	//
+	i := 0
+	for i < len(kva) {
+		j := i + 1
+		for j < len(kva) && kva[j].Key == kva[i].Key {
+			j++
+		}
+		values := []string{}
+		for k := i; k < j; k++ {
+			values = append(values, kva[k].Value)
+		}
+		output := reducef(kva[i].Key, values)
+
+		// this is the correct format for each line of Reduce output.
+		fmt.Fprintf(ofile, "%v %v\n", kva[i].Key, output)
+
+		i = j
+	}
+

循环读取map保存下来的内容,这里写死了,后面需要调整。

+

读取内容后汇总并排序,排序后直接使用串行的Reduce代码即可

+

对于Coordinator,将Reduce的内容添加进去即可:

+
// 如果所有的Map任务都完成了,就遍历Reduce任务
+if isMapfinished {
+	for i, reduceTask := range c.ReduceTask {
+		if reduceTask.State == 0 {
+			reply.Id = reduceTask.Id
+			reply.TaskType = "reduce"
+			reply.InputFileName = reduceTask.OriginFileName
+			reply.OutputFileName = reduceTask.OutputFileName
+			mu.Lock()
+			c.ReduceTask[i].State = 1
+			mu.Unlock()
+			return nil
+		}
+	}
+
+}
+

Reduce结束后需要告知主Coordinator在无限循环的Done(),返回True让其退出:

+
// main/mrcoordinator.go calls Done() periodically to find out
+// if the entire job has finished.
+func (c *Coordinator) Done() bool {
+	ret := true
+	mu.Lock()
+	// Your code here.
+	for _, v := range c.ReduceTask {
+		if v.State != 2 {
+			ret = false
+			break
+		}
+	}
+	mu.Unlock()
+	return ret
+}
+

中间添加了锁,但是添加的有些问题,后面需要调整。

+

到这里的代码除了异常处理外已经都能测试通过了,只不过是有data race问题

+

问题:

+
    +
  1. Worker的无限循环退不出去,需要Coordinator通过RPC的方式告知才可以
  2. +
  3. Reduce的遍历文件写死了,需要动态变化去判断
  4. +
  5. Coordinator存在data race问题,是循环遍历任务和对任务的完成情况进行更改后两者的锁加的不太好导致的,需要对数据结构进行修改
  6. +
  7. 没有异常处理,不能处理有Worker异常退出的情况,实际测试中陷入了死循环,需要进行调整
  8. +
+

问题及解决

+

首先将Coordinator对于任务的数据结构更改,内部维护三个双向链表,分别表示未开始的任务,正在进行的任务和已经结束的任务,链表外面使用map的数据结构,从而支持快速查找。在生成任务的时候自动赋值一个全局唯一的id。

+

数据结构中要包括全部的信息,主要变化部分是对输入和输出的信息,将Map的输入、输出和Reduce的输出都在初始化的时候直接写在结构体中,避免后续进行多次判断和修改。

+

结构体:

+
// Coordinator存储的主要信息,包括Map和Reduce两部分任务的信息以及工作节点的信息
+type Coordinator struct {
+	UniqueIdSlice     []*list.Element // 通过任务Id找到任务信息的切片,相当于一个Map
+	MapTaskNum        int             // map任务总数量
+	ReduceTaskNum     int             // reduce任务总数量
+	WorkerNum         int             // 目前正在工作的节点数量
+	MapTask                           // Map任务信息链表
+	ReduceTask                        // Reduce任务信息链表
+	WorkerInformation                 // Worker的信息
+}
+
+// Map任务信息链表,包括三个链表,分别表示未开始、正在进行和已经完成的任务
+type MapTask struct {
+	MapListReady    *list.List // 未开始的Map任务
+	MapListRunning  *list.List // 正在进行的Map任务
+	MapListComplete *list.List // 已经完成的Map任务
+}
+
+// Reduce任务信息链表,包括三个链表,分别表示未开始、正在进行和已经完成的任务
+type ReduceTask struct {
+	ReduceListReady    *list.List // 未开始的Reduce任务
+	ReduceListRunning  *list.List // 正在进行的Reduce任务
+	ReduceListComplete *list.List // 已经完成的Reduce任务
+}
+
+// Map任务具体信息
+type MapTaskInformation struct {
+	Id                   int      // 任务唯一编码
+	OriginFileName       string   // 原始文件名称
+	IntermediateFileName []string // Map任务完成后中间文件列表
+}
+
+// Reduce任务具体信息
+type ReduceTaskInformation struct {
+	Id                   int      // 任务唯一编码
+	IntermediateFileName []string // Reduce的初始中间文件列表(从Map处获得)
+	OutputFileName       string   // Reduce任务完成后的最终文件名称
+}
+

Worker中分为几个步骤:

+
    +
  1. 告知Coordinator自己已经上线
  2. +
  3. 向Coordinator请求任务
  4. +
  5. 向Coordinator返回自己的Map任务已经完成
  6. +
  7. 向Coordinator返回自己的Reduce任务已经完成
  8. +
  9. 向Coordinator返回自己退出的消息
  10. +
+

主程序如下:

+
// main/mrworker.go 调用的函数
+func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) {
+
+	// 1. 告知Coordinator自己已经上线
+	args := WorkerArgs{TaskType: "None"}
+	reply := WorkerReply{TaskType: "None"}
+	call("Coordinator.WorkerOnline", &args, &reply)
+
+	// 无限循环向Coordinator请求任务
+	for {
+		// 2. 向Coordinator请求任务
+		args = WorkerArgs{TaskType: "None"}
+		reply = WorkerReply{TaskType: "None"}
+		ok := call("Coordinator.AsssignTask", &args, &reply)
+
+		if ok {
+
+			fmt.Println("Call Success!")
+
+			if reply.TaskType == "map" {
+
+				fmt.Printf("Map Task!\n")
+
+				// 读取文件,调用map函数进行处理
+				intermediate := []KeyValue{}
+				file, err := os.Open(reply.MapInput)
+				if err != nil {
+					log.Fatalf("cannot open %v", reply.MapInput)
+				}
+				content, err := io.ReadAll(file)
+				if err != nil {
+					log.Fatalf("cannot read %v", reply.MapInput)
+				}
+				file.Close()
+				kva := mapf(reply.MapInput, string(content))
+				intermediate = append(intermediate, kva...)
+
+				// 循环创建NReduce个文件准备保存
+				encoderList := make([]*json.Encoder, 0)
+				for _, fileName := range reply.MapOutput {
+					tempFile, err := os.Create(fileName)
+					if err != nil {
+						log.Fatalf("cannot create %v", fileName)
+					}
+					defer tempFile.Close()
+					encoderList = append(encoderList, json.NewEncoder(tempFile))
+				}
+				// 将map后的结果存入文件中(最费时间)
+				for i, v := range intermediate {
+					encoderList[ihash(v.Key)%len(reply.MapOutput)].Encode(&intermediate[i])
+				}
+
+				// 3. 向Coordinator返回自己的Map任务已经完成
+				args.TaskType = "map"
+				args.Taskid = reply.Id
+				call("Coordinator.TaskFinish", &args, &reply)
+
+			} else if reply.TaskType == "reduce" {
+
+				fmt.Printf("Reduce Task!\n")
+
+				// 创建输出文件
+				ofile, _ := os.Create(reply.ReduceOutput)
+
+				// 遍历输入文件,汇总Map产生的所有结果
+				kva := make([]KeyValue, 0)
+				for _, filename := range reply.ReduceInput {
+					// fmt.Println(filename)
+					file, err := os.Open(filename)
+					if err != nil {
+						log.Fatalf("cannot open %v", filename)
+					}
+					dec := json.NewDecoder(file)
+					for {
+						var kv KeyValue
+						if err := dec.Decode(&kv); err != nil {
+							break
+						}
+						kva = append(kva, kv)
+					}
+				}
+
+				// 排序
+				sort.Sort(ByKey(kva))
+
+				// 在已经排好序的键值对上进行统计,并写入到文件中
+				i := 0
+				for i < len(kva) {
+					j := i + 1
+					for j < len(kva) && kva[j].Key == kva[i].Key {
+						j++
+					}
+					values := []string{}
+					for k := i; k < j; k++ {
+						values = append(values, kva[k].Value)
+					}
+					output := reducef(kva[i].Key, values)
+
+					fmt.Fprintf(ofile, "%v %v\n", kva[i].Key, output)
+
+					i = j
+				}
+
+				// 4. 向Coordinator返回自己的Reduce任务已经完成
+				args.Taskid = reply.Id
+				args.TaskType = "reduce"
+				call("Coordinator.TaskFinish", &args, &reply)
+
+			} else if reply.TaskType == "finish" {
+
+				// 5. 向Coordinator返回自己退出的消息
+				call("Coordinator.WorkerFinish", &args, &reply)
+				fmt.Printf("Bye!\n")
+				return
+			}
+		} else {
+			fmt.Printf("Call failed!\n")
+		}
+
+		// 间隔1秒请求一次
+		time.Sleep(time.Second)
+	}
+}
+

其中将RPC的发送和接收的结构体更改的更为合理:

+
// Worker向Coordinator传递的信息
+type WorkerArgs struct {
+	Id       int    // Worker的唯一ID
+	Taskid   int    // 任务全局唯一ID
+	TaskType string // 任务类型
+}
+
+// Coordinator向Worker传递的信息
+type WorkerReply struct {
+	Id           int      // 任务id
+	TaskType     string   // 任务类型
+	MapInput     string   // Map任务的输入
+	MapOutput    []string // Map任务的输出
+	ReduceInput  []string // Reduce任务的输入
+	ReduceOutput string   // Reduce任务的输出
+}
+
    +
  • 告知Coordinator自己已经上线:
  • +
+
// Worker告知Coordinator自己上线了
+func (c *Coordinator) WorkerOnline(args *WorkerArgs, reply *WorkerReply) error {
+	mu.Lock()
+	if c.WorkerNum == -1 {
+		c.WorkerNum = 0
+	}
+	c.WorkerNum += 1
+	mu.Unlock()
+	return nil
+}
+

这里暂时比较简单,后续需要进行处理,以进行异常处理

+
    +
  • 向Coordinator请求任务:
  • +
+
// Worker向Coordinator请求任务
+func (c *Coordinator) AsssignTask(args *WorkerArgs, reply *WorkerReply) error {
+
+	mu.Lock()
+
+	// 首先查看map任务是否已经全部完成,如果全部完成了就去完成Reduce任务,如果也全部完成了就发送Worker可以退出的消息
+	// 判断方式:通过完成链表的节点数量与初始化时侯计算的数量是否相同
+
+	if c.MapListComplete.Len() != c.MapTaskNum {
+
+		// 分配map任务
+
+		if c.MapListReady.Len() == 0 {
+
+			// 没有没开始的Map任务
+			reply.TaskType = "waiting"
+
+		} else {
+
+			// 将一个未完成的任务从未开始的链表中取出,插入到正在进行的链表里面
+			e := c.MapListReady.Front()
+			c.MapListReady.Remove(e)
+			c.MapListRunning.PushBack(e)
+
+			// 构建返回消息,告知Worker这个任务的信息
+			reply.TaskType = "map"
+			value := e.Value.(MapTaskInformation)
+			reply.Id = value.Id
+			reply.MapInput = value.OriginFileName
+			reply.MapOutput = value.IntermediateFileName
+		}
+	} else if c.ReduceListComplete.Len() != c.ReduceTaskNum {
+
+		// 分配reduce任务
+
+		if c.ReduceListReady.Len() == 0 {
+			// 没有没开始的Reduce任务
+			reply.TaskType = "waiting"
+
+		} else {
+
+			// 将一个未完成的任务从未开始的链表中取出,插入到正在进行的链表里面
+			e := c.ReduceListReady.Front()
+			c.ReduceListReady.Remove(e)
+			c.ReduceListRunning.PushBack(e)
+
+			// 构建返回消息,告知Worker这个任务的信息
+			reply.TaskType = "reduce"
+			value := e.Value.(ReduceTaskInformation)
+			reply.Id = value.Id
+			reply.ReduceInput = value.IntermediateFileName
+			reply.ReduceOutput = value.OutputFileName
+
+		}
+	} else {
+
+		//告知Worker已经没有任务了,可以退出了
+		reply.TaskType = "finish"
+	}
+
+	mu.Unlock()
+
+	return nil
+}
+

收到请求后操作全局链表,构建消息并返回即可

+
    +
  • 向Coordinator返回自己的任务已经完成
  • +
+
// Worker告知Coordinator刚才分配的任务已经完成
+func (c *Coordinator) TaskFinish(args *WorkerArgs, reply *WorkerReply) error {
+
+	mu.Lock()
+
+	// 将节点从正在进行的链表中取出,插入到已经完成的链表中
+	if args.TaskType == "map" {
+
+		// 操作节点
+		e := c.UniqueIdSlice[args.Taskid]
+		c.MapListRunning.Remove(e)
+		c.MapListComplete.PushBack(e)
+
+		// 如果是Map任务,需要将产生的nReduce个中间文件分配给Reduce节点
+		for _, file := range e.Value.(MapTaskInformation).IntermediateFileName {
+
+			// 计算是哪个Reduce节点
+			reduceTaskNum, err := strconv.Atoi(strings.Split(file, "-")[2])
+			if err != nil {
+				log.Fatalf("cannot parseInt %v", file)
+			}
+
+			// 将产生的nReduce个中间文件分配给Reduce节点(需要重新构建节点)
+			value := c.UniqueIdSlice[reduceTaskNum].Value
+			tempSlice := append(value.(ReduceTaskInformation).IntermediateFileName, file)
+			c.UniqueIdSlice[reduceTaskNum].Value = ReduceTaskInformation{
+				Id:                   value.(ReduceTaskInformation).Id,
+				IntermediateFileName: tempSlice,
+				OutputFileName:       value.(ReduceTaskInformation).OutputFileName,
+			}
+		}
+	} else if args.TaskType == "reduce" {
+
+		// 操作节点
+		e := c.ReduceListRunning.Remove(c.UniqueIdSlice[args.Taskid])
+		c.ReduceListComplete.PushBack(e)
+	}
+	mu.Unlock()
+	return nil
+}
+

对于Map任务需要传递Map输出,Reduce输入的文件信息,将结构体填充完整

+
    +
  • 向Coordinator返回自己退出的消息
  • +
+
// Worker告知Coordinator自己退出了
+func (c *Coordinator) WorkerFinish(args *WorkerArgs, reply *WorkerReply) error {
+
+	mu.Lock()
+
+	// 退出时将Coordinator内部存储的Worker数量-1
+	c.WorkerNum -= 1
+
+	mu.Unlock()
+
+	return nil
+}
+

将全局的WorkerNum减去1,后续需要进行处理。

+

经测试,除异常检测完已经都能顺利pass,多次运行的结果也完全相同

+

有一个小问题是它的脚本给的超时时间不够,调大一些后才能顺利运行,后续可以进行更改。

+

异常处理

+

原文与异常处理相关的部分:

+

The coordinator should notice if a worker hasn’t completed its task in a reasonable amount of time (for this lab, use ten seconds), and give the same task to a different worker.

+

The best you can do is have the coordinator wait for some amount of time, and then give up and re-issue the task to a different worker. For this lab, have the coordinator wait for ten seconds; after that the coordinator should assume the worker has died (of course, it might not have).

+

To test crash recovery, you can use the mrapps/crash.go application plugin. It randomly exits in the Map and Reduce functions.

+

可以先查看crash.go,看看是如何模拟线程崩溃的:

+
func maybeCrash() {
+	max := big.NewInt(1000)
+	rr, _ := crand.Int(crand.Reader, max)
+	if rr.Int64() < 330 {
+		// crash!
+		os.Exit(1)
+	} else if rr.Int64() < 660 {
+		// delay for a while.
+		maxms := big.NewInt(10 * 1000)
+		ms, _ := crand.Int(crand.Reader, maxms)
+		time.Sleep(time.Duration(ms.Int64()) * time.Millisecond)
+	}
+}
+

阅读代码,可以发现这个设置是有1/3的概率直接崩溃掉,有2/3的概率线程睡眠不到10s,模拟的环境还是比较简单的。

+

实现:

+

Worker部分:

+

Worker上线后,由Coordinator为其分配一个ID,随后在Worker的每一个rpc请求中都带有这个ID

+
WorkerID := reply.WorkerID
+

Worker上线后每5秒发送心跳信号给Coordinator,表明自己在线

+
// 心跳信号
+go func() {
+	for {
+		args := WorkerArgs{TaskType: "None"}
+		args.Id = WorkerID
+		reply := WorkerReply{TaskType: "None"}
+		time.Sleep(time.Second * 5)
+		call("Coordinator.WorkerAlive", &args, &reply)
+	}
+}()
+

Coordinator部分:

+

维护一个切片结构体,索引表示Worker的ID,结构体内部包括任务ID和上一次心跳信号的时间

+
type HeartBeat struct {
+	WorkID int
+	Time   int64
+}
+var WorkerList []HeartBeat
+

接收到Worker上线的RPC后,记录当前的时间戳,记录任务ID为-1,即表示这个索引ID已经分配给Worker了

+
// 分配任务ID并记录时间
+WorkerList = append(WorkerList, HeartBeat{
+	WorkID: -1,
+	Time:   time.Now().Unix(),
+})
+reply.WorkerID = len(WorkerList)
+

接收心跳信号后更新切片结构体

+
// Coordinator接收心跳信号
+func (c *Coordinator) WorkerAlive(args *WorkerArgs, reply *WorkerReply) error {
+	mu.Lock()
+	WorkerList[args.Id-1].Time = time.Now().Unix()
+	fmt.Printf("接收到%d心跳信号\n", args.Id-1)
+	mu.Unlock()
+	return nil
+}
+

分配任务后在切片结构体内更新任务ID信息

+
WorkerList[args.Id-1].WorkID = value.Id
+

开启协程每10秒检查切片结构体的时间戳,如果时间戳与当前时间间隔大于10秒,将任务的状态更改为未完成,重新分配。

+
// Worker信息存储
+WorkerList = make([]HeartBeat, 0)
+// 每间隔10秒进行验证
+go func() {
+	for {
+		time.Sleep(10 * time.Second)
+		mu.Lock()
+		for i := 0; i < len(WorkerList); i++ {
+			if WorkerList[i].WorkID != -1 && time.Now().Unix()-WorkerList[i].Time > 10 {
+				fmt.Printf("%d心跳信号过期\n", i)
+				e2 := *(c.UniqueIdSlice[WorkerList[i].WorkID])
+
+				// 这里不太懂为什么要这样写
+				if WorkerList[i].WorkID < c.MapTaskNum {
+					c.MapListRunning.Remove(&e2)
+					c.MapListReady.PushBack(e2.Value)
+				} else {
+					c.ReduceListRunning.Remove(&e2)
+					c.ReduceListReady.PushBack(e2.Value)
+				}
+
+				c.WorkerNum -= 1
+				WorkerList[i].WorkID = -1
+			}
+		}
+		mu.Unlock()
+	}
+}()
+

结束

+

至此,可以单独通过全部的test,但是仍然存在一些问题

+
    +
  • 代码可读性不高,不够规范,自己都看不太明白
  • +
  • 第一个wc的test和第二个index的test结合在一起通不过,但是可以单独通过两个test
  • +
  • 最后异常检测的时候会有worker退不出去
  • +
  • 代码运行时间整体比较长,不能满足脚本的运行时间
  • +
  • 加锁的地方考虑的比较少,有点过于简单粗暴了
  • +
+

总之基本功能已经没有什么问题了,以后有时间再进行重构。

+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-Lab 1 MapReduce
+
https://zhangzhao219.github.io/2022/12/15/6.824/Distributed-Systems-MIT-6.824-Lab-1/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月15日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/16/Company/index.html b/2022/12/16/Company/index.html new file mode 100644 index 000000000..05d381e4e --- /dev/null +++ b/2022/12/16/Company/index.html @@ -0,0 +1,1162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 互联网公司整理 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

互联网公司整理

+ + +
+ +

互联网公司整理

+ +

腾讯

+

岗位投递 | 腾讯校招:https://join.qq.com/post.html

+

字节跳动

+

字节跳动校园招聘:https://jobs.bytedance.com/campus/position

+

阿里

+

阿里巴巴集团校园招聘:https://talent.alibaba.com/campus/home?lang=zh

+

百度

+

百度校园招聘:https://talent.baidu.com/jobs/list

+

京东

+

京东校招:https://campus.jd.com/#/jobs

+

网易

+

网易校园招聘:https://campus.163.com/app/index

+

快手

+

快手校招 - 首页:https://campus.kuaishou.cn/recruit/campus/e/#/campus/index

+

美团

+

校园招聘 | 美团招聘:https://zhaopin.meituan.com/web/campus

+

滴滴

+

滴滴招聘:https://talent.didiglobal.com/

+

新浪

+

新浪集团 | 校园招聘:https://career.sina.com.cn/campus-recruitment/#/

+

搜狐

+

搜狐 - 校园招聘:https://app.mokahr.com/campus_apply/sohu/5682#/

+

360

+

360校园招聘:https://360campus.zhiye.com/

+

北京

+

一线互联网

+
    +
  • 百度(总部)
  • +
  • 阿里(北京)
  • +
  • 腾讯(北京)
  • +
  • 字节跳动(总部)
  • +
+

外企

+
    +
  • 微软(北京)微软中国主要就是北京和苏州
  • +
  • Hulu(北京)美国的视频网站,听说福利待遇超级棒
  • +
  • Airbnb(北京)房屋租赁平台
  • +
  • Grab(北京)东南亚第一大出行 App
  • +
  • 印象笔记(北京)evernote在中国的独立品牌
  • +
  • FreeWheel(北京)美国最大的视频广告管理和投放平台
  • +
  • amazon(北京)全球最大的电商平台
  • +
+

二线互联网

+
    +
  • 美团点评(总部)
  • +
  • 京东(总部)
  • +
  • 网易(北京)
  • +
  • 滴滴出行(总部)
  • +
  • 新浪(总部)
  • +
  • 快手(总部)
  • +
  • 搜狐(总部)
  • +
  • 搜狗(总部)
  • +
  • 360(总部)
  • +
+

硬件巨头 (有软件/互联网业务)

+
    +
  • 华为(北京)
  • +
  • 联想(总部)
  • +
  • 小米(总部)后序要搬到武汉,互联网业务也是小米重头
  • +
+

三线互联网

+
    +
  • 爱奇艺(总部)
  • +
  • 去哪儿网(总部)
  • +
  • 知乎(总部)
  • +
  • 豆瓣(总部)
  • +
  • 当当网(总部)
  • +
  • 完美世界(总部)游戏公司
  • +
  • 昆仑万维(总部)游戏公司
  • +
  • 58同城(总部)
  • +
  • 陌陌(总部)
  • +
  • 金山软件(北京)包括金山办公软件
  • +
  • 用友网络科技(总部)企业服务ERP提供商
  • +
  • 映客直播(总部)
  • +
  • 猎豹移动(总部)
  • +
  • 一点资讯(总部)
  • +
  • 国双(总部)企业级大数据和人工智能解决方案提供商
  • +
+

明星创业公司

+

可以发现北京一堆在线教育的公司,可能教育要紧盯了政策变化,所以都要在北京吧

+
    +
  • 好未来(总部)在线教育
  • +
  • 猿辅导(总部)在线教育
  • +
  • 跟谁学(总部)在线教育
  • +
  • 作业帮(总部)在线教育
  • +
  • VIPKID(总部)在线教育
  • +
  • 雪球(总部)股市资讯
  • +
  • 唱吧(总部)
  • +
  • 每日优鲜(总部)让每个人随时随地享受食物的美好
  • +
  • 微店(总部)
  • +
  • 罗辑思维(总部)得到APP
  • +
  • 值得买科技(总部)让每一次消费产生幸福感
  • +
  • 拉勾网(总部)互联网招聘
  • +
+

AI独角兽公司

+
    +
  • 商汤科技(总部)专注于计算机视觉和深度学习
  • +
  • 旷视科技(总部)人工智能产品和解决方案公司
  • +
  • 第四范式(总部)人工智能技术与服务提供商
  • +
  • 地平线机器人(总部)边缘人工智能芯片的全球领导者
  • +
  • 寒武纪(总部)全球智能芯片领域的先行者
  • +
+

互联网媒体

+
    +
  • 央视网
  • +
  • 搜房网
  • +
  • 易车网
  • +
  • 链家网
  • +
  • 自如网
  • +
  • 汽车之家
  • +
+

上海

+

一线互联网

+
    +
  • 百度(上海)
  • +
  • 阿里(上海)
  • +
  • 腾讯(上海)
  • +
  • 字节跳动(上海)
  • +
  • 蚂蚁金服(上海)
  • +
+

外企IT/互联网/硬件

+
    +
  • 互联网 +
      +
    • Google(上海)
    • +
    • 微软(上海)
    • +
    • LeetCode/力扣(上海)
    • +
    • unity(上海)游戏引擎
    • +
    • SAP(上海)主要产品是ERP
    • +
    • PayPal(上海)在线支付鼻祖
    • +
    • eBay(上海)电子商务公司
    • +
    +
  • +
  • 偏硬件 +
      +
    • IBM(上海)
    • +
    • Tesla(上海)特斯拉
    • +
    • Cisco(上海)思科
    • +
    • Intel(上海)
    • +
    • AMD(上海)半导体产品领域
    • +
    • EMC(上海)易安信是美国信息存储资讯科技公司
    • +
    • NVIDIA(上海)英伟达是GPU(图形处理器)的发明者,人工智能计算的引领者
    • +
    +
  • +
+

二线互联网

+
    +
  • 拼多多(总部)
  • +
  • 饿了么(总部)阿里旗下。
  • +
  • 哈啰出行(总部)阿里旗下
  • +
  • 盒马(总部)阿里旗下
  • +
  • 哔哩哔哩(总部)
  • +
  • 阅文集团(总部)腾讯旗下
  • +
  • 爱奇艺(上海)百度旗下
  • +
  • 携程(总部)
  • +
  • 京东(上海)
  • +
  • 网易(上海)
  • +
  • 美团点评(上海)
  • +
  • 唯品会(上海)
  • +
+

硬件巨头 (有软件/互联网业务)

+

华为(上海)

+

三线互联网

+
    +
  • PPTV(总部)
  • +
  • 微盟(总部)企业云端商业及营销解决方案提供商
  • +
  • 喜马拉雅(总部)
  • +
  • 陆金所(总部)全球领先的线上财富管理平台
  • +
  • 口碑(上海)阿里旗下。
  • +
  • 三七互娱(上海)
  • +
  • 趣头条(总部)
  • +
  • 巨人网络(总部)游戏公司
  • +
  • 盛大网络(总部)游戏公司
  • +
  • UCloud(总部)云服务提供商
  • +
  • 达达集团(总部)本地即时零售与配送平台
  • +
  • 众安保险(总部)在线财产保险
  • +
  • 触宝(总部)触宝输入法等多款APP
  • +
  • 平安系列
  • +
+

明星创业公司

+
    +
  • 小红书(总部)
  • +
  • 叮咚买菜(总部)
  • +
  • 蔚来汽车(总部)
  • +
  • 七牛云(总部)
  • +
  • 得物App(总部)品潮流尖货装备交易、球鞋潮品鉴别查验、互动潮流社区
  • +
  • 收钱吧(总部)开创了中国移动支付市场“一站式收款”
  • +
  • 蜻蜓FM(总部)音频内容聚合平台
  • +
  • 流利说(总部)在线教育
  • +
  • Soul(总部)社交软件
  • +
  • 美味不用等(总部)智慧餐饮服务商
  • +
  • 微鲸科技(总部)专注于智能家居领域
  • +
  • 途虎养车(总部)
  • +
  • 米哈游(总部)游戏公司
  • +
  • 莉莉丝游戏(总部)游戏公司
  • +
  • 樊登读书(总部)在线教育
  • +
+

AI独角兽公司

+
    +
  • 依图科技(总部)和旷视,商汤对标,都是做安防视觉
  • +
  • 深兰科技(总部)致力于人工智能基础研究和应用开发
  • +
+

其他行业,涉及互联网

+
    +
  • 花旗、摩根大通等一些列金融巨头
  • +
  • 百姓网
  • +
  • 找钢网
  • +
  • 安居客
  • +
  • 前程无忧
  • +
  • 东方财富
  • +
  • 三大电信运营商:中国移动、中国电信、中国联通
  • +
  • 沪江英语
  • +
  • 各大银行
  • +
+

深圳

+

一线互联网

+
    +
  • 腾讯(总部深圳)
  • +
  • 百度(深圳)
  • +
  • 阿里(深圳)
  • +
  • 字节跳动(深圳)
  • +
+

硬件巨头 (有软件/互联网业务)

+
    +
  • 华为(总部深圳)
  • +
  • 中兴(总部深圳)
  • +
  • 海能达(总部深圳)
  • +
  • oppo(总部深圳)
  • +
  • vivo(总部深圳)
  • +
  • 深信服(总部深圳)
  • +
  • 大疆(总部深圳,无人机巨头)
  • +
  • 一加手机(总部深圳)
  • +
  • 柔宇科技(最近口碑急转直下)
  • +
+

二线大厂

+
    +
  • 快手(深圳)
  • +
  • 京东(深圳)
  • +
  • 顺丰(总部深圳)
  • +
+

三线大厂

+
    +
  • 富途证券(2020年成功赴美上市,主要经营港股美股)
  • +
  • 微众银行(总部深圳)
  • +
  • 招银科技(总部深圳)
  • +
  • 平安系列(平安科技、平安寿险、平安产险、平安金融、平安好医生等)
  • +
  • Shopee(东南亚最大的电商平台,最近发展势头非常强劲)
  • +
  • 有赞(深圳)
  • +
  • 迅雷(总部深圳)
  • +
  • 金蝶(总部深圳)
  • +
  • 随手记(总部深圳)
  • +
+

AI独角兽公司

+
    +
  • 商汤科技(人工智能领域的独角兽)
  • +
  • 追一科技(一家企业级智能服务AI公司)
  • +
  • 超多维科技 (计算机视觉、裸眼3D)
  • +
  • 优必选科技 (智能机器人、人脸识别)
  • +
+

明星创业公司

+
    +
  • 丰巢科技(让生活更简单)
  • +
  • 人人都是产品经理(全球领先的产品经理和运营人 学习、交流、分享平台)
  • +
  • 大丰收(综合农业互联网服务平台)
  • +
  • 小鹅通(专注新教育的技术服务商)
  • +
  • 货拉拉(拉货就找货拉拉)
  • +
  • 编程猫(少儿编程教育头部企业)
  • +
  • HelloTalk(全球最大的语言学习社交社区)
  • +
  • 大宇无限( 拥有SnapTube, Lark Player 等多款广受海外新兴市场用户欢迎的产品)
  • +
  • 知识星球(深圳大成天下公司出品)
  • +
  • XMind(隶属深圳市爱思软件技术有限公司,思维导图软件)
  • +
  • 小赢科技(以技术重塑人类的金融体验)
  • +
+

其他行业(有软件/互联网业务)

+
    +
  • 三大电信运营商:中国移动、中国电信、中国联通
  • +
  • 房产企业:恒大(暴雷)、万科
  • +
  • 中信深圳
  • +
  • 广发证券,深交所
  • +
  • 珍爱网(珍爱网是国内知名的婚恋服务网站之一)
  • +
+

广州

+

一线互联网

+
    +
  • 微信(总部) 有点难进!
  • +
  • 字节跳动(广州)
  • +
+

二线

+
    +
  • 网易(总部)主要是游戏
  • +
+

三线

+
    +
  • 唯品会(总部)
  • +
  • 欢聚时代(总部)旗下YY,虎牙,YY最近被浑水做空,不知百度还要不要收购了
  • +
  • 酷狗音乐(总部)
  • +
  • UC浏览器(总部)现在隶属阿里创始人何小鹏现在搞小鹏汽车
  • +
  • 荔枝FM(总部)用户可以在手机上开设自己的电台和录制节目
  • +
  • 映客直播(总部)股票已经跌成渣了
  • +
  • 爱范儿(总部)
  • +
  • 三七互娱(总部)游戏公司
  • +
  • 君海游戏(总部)游戏公司
  • +
  • 4399游戏(总部)游戏公司
  • +
  • 多益网络(总部)游戏公司
  • +
+

硬件巨头 (有软件/互联网业务)

+
    +
  • 小鹏汽车(总部)新能源汽车小霸王
  • +
+

创业公司

+
    +
  • 妈妈网(总部)母婴行业互联网公司
  • +
  • 云徙科技(总部)数字商业云服务提供商
  • +
  • Fordeal(总部)中东领先跨境电商平台
  • +
  • Mobvista(总部)移动数字营销
  • +
  • 久邦GOMO(总部)游戏
  • +
  • 深海游戏(总部)游戏
  • +
+

国企

+
    +
  • 中国电信广州研发(听说没有996)
  • +
+

成都

+

一线互联网

+
    +
  • 腾讯(成都) 游戏,王者荣耀就在成都!
  • +
  • 阿里(成都)
  • +
  • 蚂蚁金服(成都)
  • +
  • 字节跳动(成都)
  • +
+

硬件巨头 (有软件/互联网业务)

+
    +
  • 华为(成都)
  • +
  • OPPO(成都)
  • +
+

二线互联网

+
    +
  • 京东(成都)
  • +
  • 美团(成都)
  • +
  • 滴滴(成都)
  • +
+

三线互联网

+
    +
  • 完美世界 (成都)游戏
  • +
  • 聚美优品 (成都)
  • +
  • 陌陌 (成都)
  • +
  • 爱奇艺(成都)
  • +
+

外企互联网

+
    +
  • NAVER China (成都)搜索引擎公司,主要针对韩国市场
  • +
+

创业公司

+
    +
  • tap4fun(总部)游戏
  • +
  • 趣乐多(总部)游戏
  • +
  • 天上友嘉(总部)游戏
  • +
  • 三七互娱(成都)游戏
  • +
  • 咕咚(总部)智能运动
  • +
  • 百词斩(总部)在线教育
  • +
  • 晓多科技(总部)AI方向
  • +
  • 萌想科技(总部)实习僧
  • +
  • Camera360(总部)移动影像社区
  • +
  • 医联 (总部)医疗解决方案提供商
  • +
  • 小明太极 (总部)原创漫画文娱内容网站以及相关APP
  • +
  • 小鸡叫叫(总部)致力于儿童教育的智慧解决方案
  • +
+

AI独角兽公司

+
    +
  • 科大讯飞(成都)
  • +
  • 商汤(成都)
  • +
+

杭州

+

一线互联网

+
    +
  • 阿里巴巴(总部)
  • +
  • 蚂蚁金服(总部)阿里旗下
  • +
  • 阿里云(总部)阿里旗下
  • +
  • 网易(杭州) 网易云音乐
  • +
  • 字节跳动(杭州)抖音分部
  • +
+

外企

+
    +
  • ZOOM (杭州研发中心)全球知名云视频会议服务提供商
  • +
  • infosys(杭州)印度公司,据说工资相对不高
  • +
  • 思科(杭州)
  • +
+

二线互联网

+
    +
  • 滴滴(杭州)
  • +
  • 快手(杭州)
  • +
+

硬件巨头 (有软件/互联网业务)

+
    +
  • 海康威视(总部)安防三巨头
  • +
  • 浙江大华(总部)安防三巨头
  • +
  • 杭州宇视(总部) 安防三巨头
  • +
  • 萤石
  • +
  • 华为(杭州)
  • +
  • vivo(杭州)
  • +
  • oppo(杭州)
  • +
  • 魅族(杭州)
  • +
+

三线互联网

+
    +
  • 蘑菇街(总部)女性消费者的电子商务网站
  • +
  • 有赞(总部)帮助商家进行网上开店、社交营销
  • +
  • 菜鸟网络(杭州)
  • +
  • 花瓣网(总部)图片素材领导者
  • +
  • 兑吧(总部)用户运营服务平台
  • +
  • 同花顺(总部)网上股票证券交易分析软件
  • +
  • 51信用卡(总部)信用卡管理
  • +
  • 虾米(总部)已被阿里收购
  • +
  • 曹操出行(总部)
  • +
  • 口碑网 (总部)
  • +
+

AI独角兽公司

+
    +
  • 旷视科技(杭州)
  • +
  • 商汤(杭州)
  • +
+

创业公司

+
    +
  • e签宝(总部)做电子签名
  • +
  • 婚礼纪(总部)好多结婚的朋友都用
  • +
  • 大搜车(总部)中国领先的汽车交易服务供应商
  • +
  • 二更(总部)自媒体
  • +
  • 丁香园(总部)
  • +
+ + +
+ +
+
+ + + + + + +
+
+
互联网公司整理
+
https://zhangzhao219.github.io/2022/12/16/Company/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月16日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/17/6.824/Distributed-Systems-MIT-6.824-LEC-3/index.html b/2022/12/17/6.824/Distributed-Systems-MIT-6.824-LEC-3/index.html new file mode 100644 index 000000000..04e9d2bf7 --- /dev/null +++ b/2022/12/17/6.824/Distributed-Systems-MIT-6.824-LEC-3/index.html @@ -0,0 +1,1029 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-LEC 3 GFS - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-LEC 3 GFS

+ + +
+ +

MIT-6.824(Spring 2022)LEC 3 GFS

+ +

GFS论文阅读

+

参考资料(感谢Alex!这篇论文翻译得非常有质量!)

+ + +
+ +
+ + + +

摘要

+

Google GFS文件系统是一个面向大规模数据密集型应用的、可伸缩的分布式文件系统。GFS运行在廉价的普遍硬件设备上,但是依然了提供容错机制,为大量客户提供了高性能的服务。

+

GFS的设计目标与许多传统的分布式文件系统有很多相同之处,不过还是以我们对自己的应用的负载情况和技术环境的分析为基础进行设计,和早期的分布式文件系统有明显的不同。

+

GFS完全满足了我们对存储的需求。GFS作为存储平台已经被广泛的部署在Google内部,存储我们的服务产生和处理的数据,同时还用于那些需要大规模数据集的研究和开发工作。目前为止,最大的一个集群利用数千台机器的数千个硬盘,提供了数百TB的存储空间,同时为数百个客户机服务。

+

在本论文中,我们展示能够支持分布式应用的文件系统接口扩展,讨论我们设计的许多方面,最后列出了小规模性能测试以及真实生产系统中性能的相关数据。

+

1. 简介

+

GFS与传统的分布式文件系统有着很多相同的设计目标,比如,性能、可伸缩性、可靠性以及可用性。但是,我们的设计还基于我们对我们自己的应用的负载情况和技术环境的观察的影响,和早期文件系统的假设都有明显的不同。

+

所以我们重新审视了传统文件系统在设计上的折衷选择,衍生出了完全不同的设计思路。

+

首先,组件失效被认为是常态事件,而不是意外事件。GFS组件的数量和质量导致在任何给定时间内都有可能发生某些组件无法工作,且某些组件无法从它们目前的失效状态中恢复。因此,持续的监控、错误侦测、容错以及自动恢复的机制必须集成在GFS中。

+

其次,我们的文件非常巨大,GB的文件非常普遍。当我们经常需要处理快速增长的、并且由数亿个对象构成的、数以TB的数据集时,采用管理数亿个KB大小的小文件的方式是非常不明智的。因此,设计的假设条件和参数,比如I/O操作和Block的尺寸等都需要重新考虑。

+

第三,绝大部分文件的修改是采用在文件尾部追加数据,而不是覆盖原有数据的方式。一旦写完之后,对文件的操作就只有读,而且通常是按顺序读。对于这种针对海量文件的访问模式,客户端对数据块缓存是没有意义的,数据的追加操作是性能优化和原子性保证的主要考量因素。

+

第四,应用程序和文件系统API的协同设计提高了整个系统的灵活性。比如,我们放松了对GFS一致性模型的要求,这样就减轻了文件系统对应用程序的苛刻要求,大大简化了GFS的设计。我们引入了原子性的记录追加操作,从而保证多个客户端能够同时进行追加操作,不需要额外的同步操作来保证数据的一致性。

+

2. 设计概述

+

2.1 设计预期

+
    +
  • 系统的组件失效是一种常态。系统必须持续监控自身的状态,迅速侦测、容忍并恢复失效的组件。
  • +
  • 系统要能存储一定数量的大文件。系统也必须支持小文件,但是不需要针对小文件做专门的优化。
  • +
  • 系统的工作负载主要由两种读操作组成:大规模的流式读取和小规模的随机读取。 +
      +
    • 大规模的流式读取通常一次读取1MB甚至更多的数据。来自同一个客户机的连续操作通常是读取同一个文件中连续的一个区域。
    • +
    • 小规模的随机读取通常是在文件某个随机的位置读取几个KB数据。通常的做法是把小规模的随机读取操作合并并排序,之后按顺序批量读取。
    • +
    +
  • +
  • 系统的工作负载还包括许多大规模的、顺序的、数据追加方式的写操作。每次写入的数据的大小和大规模读类似。数据一旦被写入后,文件就很少会被修改了。
  • +
  • 系统必须高效的、行为定义明确的。实现多客户端并行追加数据到同一个文件里的功能。
  • +
  • 高性能的稳定网络带宽远比低延迟重要。我们的目标程序绝大部分要求能够高速率的、大批量的处理数据,极少有程序对单一的读写操作有严格的响应时间要求。
  • +
+

2.2 接口

+

GFS提供了一套类似传统文件系统的API接口函数,虽然并不是严格按照POSIX等标准API的形式实现的。文件以分层目录的形式组织,用路径名来标识。支持常用的操作如创建新文件、删除文件、打开文件、关闭文件、读和写文件。

+

另外,GFS提供了快照记录追加操作。快照以很低的成本创建一个文件或者目录树的拷贝。记录追加操作允许多个客户端同时对一个文件进行数据追加操作,同时保证每个客户端的追加操作都是原子性的。多个客户端可以在不需要额外的同步锁定的情况下,同时对一个文件追加数据。这些类型的文件对于构建大型分布应用是非常重要的。

+

2.3 架构

+

zbm43T.md.png

+

一个GFS集群包含一个单独的Master节点 (alex注:这里的一个单独的Master节点的含义是GFS系统中只存在一个逻辑上的Master组件。后面还会提到Master节点复制,因此,为了理解方便,我们把Master节点视为一个逻辑上的概念,一个逻辑的Master节点包括两台物理主机,即两台Master服务器)、 和多台Chunk服务器,并且同时被多个客户端访问。Chunk服务器和客户端也可以放在同一台机器上。

+

GFS存储的文件都被分割成固定大小的Chunk。在Chunk创建的时候,Master服务器会给每个Chunk分配一个唯一不变的64位的Chunk标识。Chunk服务器把Chunk以linux文件的形式保存在本地硬盘上,并且根据指定的Chunk标识和字节范围来读写块数据。出于可靠性的考虑,每个块都会复制到多个块服务器上,默认是3份。

+

Master节点管理所有的文件系统元数据。这些元数据包括名字空间、访问控制信息、文件和Chunk的映射信息、以及当前Chunk的位置信息。Master节点还管理着系统范围内的活动,比如,Chunk租用管理、孤儿Chunk的回收、以及Chunk在Chunk服务器之间的迁移。Master节点使用心跳信息周期地和每个Chunk服务器通讯,发送指令到各个Chunk服务器并接收Chunk服务器的状态信息。

+

GFS客户端代码以库的形式链接到客户程序里。客户端代码实现了GFS文件系统的API接口函数、应用程序与Master节点和Chunk服务器通讯、以及对数据进行读写操作。客户端和Master节点的通信只获取元数据,所有的数据操作都是由客户端直接和Chunk服务器进行交互的。

+

无论是客户端还是Chunk服务器都不需要缓存文件数据。客户端缓存数据几乎没有什么用处,因为大部分程序要么以流的方式读取一个巨大文件,要么工作集太大根本无法被缓存。无需考虑缓存相关的问题也简化了客户端和整个系统的设计和实现。(不过,客户端会缓存元数据。)Chunk服务器不需要缓存文件数据的原因是,Chunk以本地文件的方式保存,Linux操作系统的文件系统缓存会把经常访问的数据缓存在内存中。

+

2.4 单一Master节点

+

单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策。不过我们必须减少对Master节点的读写,避免Master节点成为系统的瓶颈。客户端并不通过Master节点读写文件数据。而是向Master节点询问它应该联系的Chunk服务器。客户端将这些元数据信息缓存一段时间,后续的操作将直接和Chunk服务器进行数据读写操作。

+

一次简单读取的流程:首先,客户端把文件名和程序指定的字节偏移,根据固定的Chunk大小,转换成文件的Chunk索引。然后,它把文件名和Chunk索引发送给Master节点。Master节点将相应的Chunk标识和副本的位置信息发还给客户端。客户端用文件名和Chunk索引作为key缓存这些信息。之后客户端发送请求到其中的一个(一般是最近的)副本处。请求信息包含了Chunk的标识和字节范围。在对这个Chunk的后续读取操作中,客户端不必再和Master节点通讯了,除非缓存的元数据信息过期或者文件被重新打开。实际上,客户端通常会在一次请求中查询多个Chunk信息,Master节点的回应也可能包含了紧跟着这些被请求的Chunk后面的Chunk的信息。在实际应用中,这些额外的信息避免了客户端和Master节点未来可能会发生的几次通讯。

+

2.5 Chunk尺寸

+

Chunk的大小是关键的设计参数之一。我们选择了64MB,远远大于一般文件系统的Block size。每个Chunk的副本都以普通Linux文件的形式保存在Chunk服务器上,只有在需要的时候才扩大。惰性空间分配策略避免了因内部碎片造成的空间浪费,内部碎片或许是对选择这么大的Chunk尺寸最具争议的一点。

+

选择较大的Chunk尺寸有几个重要的优点。首先,它减少了客户端和Master节点通讯的需求,一次和Master节点的通信就可以获取Chunk的位置信息,之后就可以对同一个Chunk进行多次的读写操作。即使是小规模的随机读取,客户端可以轻松的缓存一个数TB的工作数据集所有的Chunk位置信息。其次,采用较大的Chunk尺寸,客户端能够对一个块进行多次操作,这样就可以通过与Chunk服务器保持较长时间的TCP连接来减少网络负载。第三,选用较大的Chunk尺寸减少了Master节点需要保存的元数据的数量。

+

另一方面,即使配合惰性空间分配,采用较大的Chunk尺寸也有其缺陷。小文件包含较少的Chunk,甚至只有一个Chunk。当有许多的客户端对同一个小文件进行多次的访问时,存储这些Chunk的Chunk服务器就会变成热点。在实际应用中,热点不是主要问题。

+

然而,当我们第一次把GFS用于批处理队列系统的时候,热点的问题还是产生了:一个可执行文件在GFS上保存为single-chunk文件,之后这个可执行文件在数百台机器上同时启动。存放这个可执行文件的几个Chunk服务器被数百个客户端的并发请求访问导致系统局部过载。我们通过将这个文件复制更多份,并错开批处理队列系统程序的启动时间的方法解决了这个问题。一个可能的长效解决方案是,在这种的情况下,允许客户端从其它客户端读取数据。

+

2.6 元数据

+

Master服务器(alex注:注意逻辑的Master节点和物理的Master服务器的区别。后续我们谈的是每个Master服务器的行为,如存储、内存等等,因此我们将全部使用物理名称)存储3种主要类型的元数据,包括:文件和Chunk的命名空间、文件和Chunk的对应关系、每个Chunk副本的存放地点。所有的元数据都保存在Master服务器的内存中。前两种类型的元数据(命名空间、文件和Chunk的对应关系)同时也会以记录变更日志的方式记录在操作系统的系统日志文件中,日志文件存储在本地磁盘上,同时日志会被复制到其它的远程Master服务器上。采用保存变更日志的方式,我们能够简单可靠的更新Master服务器的状态,并且不用担心Master服务器崩溃导致数据不一致的风险。Master服务器不会持久保存Chunk位置信息。Master服务器在启动时,或者有新的Chunk服务器加入时,向各个Chunk服务器轮询它们所存储的Chunk的信息。

+

2.6.1 内存中的数据结构

+

因为元数据保存在内存中,所以Master服务器可以在后台简单而高效的周期性扫描自己保存的全部状态信息。这种周期性的状态扫描也用于实现Chunk垃圾收集、在Chunk服务器失效的时重新复制数据、通过Chunk的迁移实现跨Chunk服务器的负载均衡以及磁盘使用状况统计等功能。

+

将元数据全部保存在内存中的方法的问题:Chunk的数量以及整个系统的承载能力都受限于Master服务器所拥有的内存大小。但是在实际应用中,这并不是一个严重的问题。Master服务器只需要不到64个字节的元数据就能够管理一个64MB的Chunk。每个文件的在命名空间中的数据大小通常在64字节以下,因为保存的文件名是用前缀压缩算法压缩过的。

+

即便是需要支持更大的文件系统,为Master服务器增加额外内存的费用是很少的,增强了系统的简洁性、可靠性、高性能和灵活性。

+

2.6.2 Chunk位置信息

+

Master服务器并不保存持久化保存哪个Chunk服务器存有指定Chunk的副本的信息。Master服务器只是在启动的时候轮询Chunk服务器以获取这些信息。Master服务器能够保证它持有的信息始终是最新的,因为它控制了所有的Chunk位置的分配,而且通过周期性的心跳信息监控Chunk服务器的状态。

+

最初设计时,我们试图把Chunk的位置信息持久的保存在Master服务器上,但是后来我们发现在启动的时候轮询Chunk服务器,之后定期轮询更新的方式更简单。这种设计简化了在有Chunk服务器加入集群、离开集群、更名、失效、以及重启的时候,Master服务器和Chunk服务器数据同步的问题。

+

可以从另外一个角度去理解这个设计决策:只有Chunk服务器才能最终确定一个Chunk是否在它的硬盘上。我们从没有考虑过在Master服务器上维护一个这些信息的全局视图,因为Chunk服务器的错误可能会导致Chunk自动消失(比如,硬盘损坏了或者无法访问了),亦或者操作人员可能会重命名一个Chunk服务器。

+

2.6.3 操作日志

+

操作日志包含了关键的元数据变更历史记录。这对GFS非常重要。这不仅仅是因为操作日志是元数据唯一的持久化存储记录,它也作为判断同步操作顺序的逻辑时间基线 (alex注:也就是通过逻辑日志的序号作为操作发生的逻辑时间,类似于事务系统中的LSN) 。文件和Chunk,连同它们的版本,都由它们创建的逻辑时间唯一的、永久的标识。

+

操作日志非常重要,我们必须确保日志文件的完整,确保只有在元数据的变化被持久化后,日志才对客户端是可见的。否则,即使Chunk本身没有出现任何问题,我们仍有可能丢失整个文件系统,或者丢失客户端最近的操作。所以,我们会把日志复制到多台远程机器,并且只有把相应的日志记录写入到本地以及远程机器的硬盘后,才会响应客户端的操作请求。Master服务器会收集多个日志记录后批量处理,以减少写入磁盘和复制对系统整体性能的影响。

+

Master服务器在恢复时,通过重演操作日志把文件系统恢复到最近的状态。为了缩短Master启动的时间,我们必须使日志足够小 (alex注:即重演系统操作的日志量尽量的少)。 Master服务器在日志增长到一定量时对系统状态做一次Checkpoint (alex注:Checkpoint是一种行为,一种对数据库状态作一次快照的行为), 将所有的状态数据写入一个Checkpoint文件 (alex注:并删除之前的日志文件)。 在灾难恢复的时候,Master服务器就通过从磁盘上读取这个Checkpoint文件,以及重演Checkpoint之后的有限个日志文件就能够恢复系统。Checkpoint文件以压缩B-树形势的数据结构存储,可以直接映射到内存,在用于命名空间查询时无需额外的解析。这大大提高了恢复速度,增强了可用性。

+

由于创建一个Checkpoint文件需要一定的时间,所以Master服务器的内部状态被组织为一种格式,这种格式要确保在Checkpoint过程中不会阻塞正在进行的修改操作。Master服务器使用独立的线程切换到新的日志文件和创建新的Checkpoint文件。新的Checkpoint文件包括切换前所有的修改。对于一个包含数百万个文件的集群,创建一个Checkpoint文件需要1分钟左右的时间。创建完成后,Checkpoint文件会被写入在本地和远程的硬盘里。

+

Master服务器恢复只需要最新的Checkpoint文件和后续的日志文件。旧的Checkpoint文件和日志文件可以被删除,但是为了应对灾难性的故障 (alex注:catastrophes,数据备份相关文档中经常会遇到这个词,表示一种超出预期范围的灾难性事件), 我们通常会多保存一些历史文件。Checkpoint失败不会对正确性产生任何影响,因为恢复功能的代码可以检测并跳过没有完成的Checkpoint文件。

+

2.7 一致性模型

+

GFS支持一个宽松的一致性模型,这个模型能够很好的支撑我们的高度分布的应用,同时还保持了相对简单且容易实现的优点。本节我们讨论GFS的一致性的保障机制,以及对应用程序的意义。我们也着重描述了GFS如何管理这些一致性保障机制。

+

2.7.1 GFS一致性保障机制

+

文件命名空间的修改(例如,文件创建)是原子性的。它们仅由Master节点的控制:命名空间锁提供了原子性和正确性的保障;Master节点的操作日志定义了这些操作在全局的顺序。

+

数据修改后文件region (alex注:region这个词用中文非常难以表达,我认为应该是修改操作所涉及的文件中的某个范围) 的状态取决于操作的类型、成功与否、以及是否同步修改。如果所有客户端,无论从哪个副本读取,读到的数据都一样,那么我们认为文件region是“一致的”;如果对文件的数据修改之后,region是一致的,并且客户端能够看到写入操作全部的内容,那么这个region是“已定义的”。当一个数据修改操作成功执行,并且没有受到同时执行的其它写入操作的干扰,那么影响的region就是已定义的(隐含了一致性):所有的客户端都可以看到写入的内容。并行修改操作成功完成之后,region处于一致的、未定义的状态:所有的客户端看到同样的数据,但是无法读到任何一次写入操作写入的数据。通常情况下,文件region内包含了来自多个修改操作的、混杂的数据片段。失败的修改操作导致一个region处于不一致状态(同时也是未定义的):不同的客户在不同的时间会看到不同的数据。

+

数据修改操作分为写入或者记录追加两种。写入操作把数据写在应用程序指定的文件偏移位置上。即使有多个修改操作并行执行时,记录追加操作至少可以把数据原子性的追加到文件中一次,但是偏移位置是由GFS选择的 (alex注:这句话有点费解,其含义是所有的追加写入都会成功,但是有可能被执行了多次,而且每次追加的文件偏移量由GFS自己计算) 。(相比而言,通常说的追加操作写的偏移位置是文件的尾部。)GFS返回给客户端一个偏移量,表示了包含了写入记录的、已定义的region的起点。另外,GFS可能会在文件中间插入填充数据或者重复记录。这些数据占据的文件region被认定是不一致的,这些数据通常比用户数据小的多。

+

经过了一系列的成功的修改操作之后,GFS确保被修改的文件region是已定义的,并且包含最后一次修改操作写入的数据。GFS通过以下措施确保上述行为:(a) 对Chunk的所有副本的修改操作顺序一致(3.1章),(b)使用Chunk的版本号来检测副本是否因为它所在的Chunk服务器宕机(4.5章)而错过了修改操作而导致其失效。失效的副本不会再进行任何修改操作,Master服务器也不再返回这个Chunk副本的位置信息给客户端。它们会被垃圾收集系统尽快回收。

+

由于Chunk位置信息会被客户端缓存,所以在信息刷新前,客户端有可能从一个失效的副本读取了数据。在缓存的超时时间和文件下一次被打开的时间之间存在一个时间窗,文件再次被打开后会清除缓存中与该文件有关的所有Chunk位置信息。而且,由于我们的文件大多数都是只进行追加操作的,所以,一个失效的副本通常返回一个提前结束的Chunk而不是过期的数据。当一个Reader (alex注:本文中将用到两个专有名词,Reader和Writer,分别表示执行GFS读取和写入操作的程序) 重新尝试并联络Master服务器时,它就会立刻得到最新的Chunk位置信息。

+

即使在修改操作成功执行很长时间之后,组件的失效也可能损坏或者删除数据。GFS通过Master服务器和所有Chunk服务器的定期“握手”来找到失效的Chunk服务器,并且使用Checksum来校验数据是否损坏(5.2章)。一旦发现问题,数据要尽快利用有效的副本进行恢复(4.3章)。只有当一个Chunk的所有副本在GFS检测到错误并采取应对措施之前全部丢失,这个Chunk才会不可逆转的丢失。在一般情况下GFS的反应时间 (alex注:指Master节点检测到错误并采取应对措施) 是几分钟。即使在这种情况下,Chunk也只是不可用了,而不是损坏了:应用程序会收到明确的错误信息而不是损坏的数据。

+

2.7.2 程序的实现

+

使用GFS的应用程序可以利用一些简单技术实现这个宽松的一致性模型,这些技术也用来实现一些其它的目标功能,包括:尽量采用追加写入而不是覆盖,Checkpoint,自验证的写入操作,自标识的记录。

+

在实际应用中,我们所有的应用程序对文件的写入操作都是尽量采用数据追加方式,而不是覆盖方式。一种典型的应用,应用程序从头到尾写入数据,生成了一个文件。写入所有数据之后,应用程序自动将文件改名为一个永久保存的文件名,或者周期性的作Checkpoint,记录成功写入了多少数据。Checkpoint文件可以包含程序级别的校验和。Readers仅校验并处理上个Checkpoint之后产生的文件region,这些文件region的状态一定是已定义的。这个方法满足了我们一致性和并发处理的要求。追加写入比随机位置写入更加有效率,对应用程序的失败处理更具有弹性。Checkpoint可以让Writer以渐进的方式重新开始,并且可以防止Reader处理已经被成功写入,但是从应用程序的角度来看还并未完成的数据。

+

我们再来分析另一种典型的应用。许多应用程序并行的追加数据到同一个文件,比如进行结果的合并或者是一个生产者-消费者队列。记录追加方式的“至少一次追加”的特性保证了Writer的输出。Readers使用下面的方法来处理偶然性的填充数据和重复内容。Writers在每条写入的记录中都包含了额外的信息,例如Checksum,用来验证它的有效性。Reader可以利用Checksum识别和抛弃额外的填充数据和记录片段。如果应用不能容忍偶尔的重复内容(比如,如果这些重复数据触发了非幂等操作),可以用记录的唯一标识符来过滤它们,这些唯一标识符通常用于命名程序中处理的实体对象,例如web文档。这些记录I/O功能 (alex注:These functionalities for record I/O) (除了剔除重复数据)都包含在我们的程序共享的库中,并且适用于Google内部的其它的文件接口实现。所以,相同序列的记录,加上一些偶尔出现的重复数据,都被分发到Reader了。

+

3. 系统交互

+

我们在设计这个系统时,一个重要的原则是最小化所有操作和Master节点的交互。带着这样的设计理念,我们现在描述一下客户机、Master服务器和Chunk服务器如何进行交互,以实现数据修改操作、原子的记录追加操作以及快照功能。

+

3.1 租约(lease)和变更顺序

+

(alex注:lease是数据库中的一个术语)

+

变更是一个会改变Chunk内容或者元数据的操作,比如写入操作或者记录追加操作。变更操作会在Chunk的所有副本上执行。我们使用租约(lease)机制来保持多个副本间变更顺序的一致性。Master节点为Chunk的一个副本建立一个租约,我们把这个副本叫做主Chunk。主Chunk对Chunk的所有更改操作进行序列化。所有的副本都遵从这个序列进行修改操作。因此,修改操作全局的顺序首先由Master节点选择的租约的顺序决定,然后由租约中主Chunk分配的序列号决定。

+

设计租约机制的目的是为了最小化Master节点的管理负担。租约的初始超时设置为60秒。不过,只要Chunk被修改了,主Chunk就可以申请更长的租期,通常会得到Master节点的确认并收到租约延长的时间。这些租约延长请求和批准的信息通常都是附加在Master节点和Chunk服务器之间的心跳消息中来传递。有时Master节点会试图提前取消租约(例如,Master节点想取消在一个已经被改名的文件上的修改操作)。即使Master节点和主Chunk失去联系,它仍然可以安全地在旧的租约到期后和另外一个Chunk副本签订新的租约。

+

在图中,依据步骤编号,展现写入操作的控制流程。

+

zbbzEn.png

+
    +
  1. 客户机向Master节点询问哪一个Chunk服务器持有当前的租约,以及其它副本的位置。如果没有一个Chunk持有租约,Master节点就选择其中一个副本建立一个租约(这个步骤在图上没有显示)。
  2. +
  3. Master节点将主Chunk的标识符以及其它副本(又称为secondary副本、二级副本)的位置返回给客户机。客户机缓存这些数据以便后续的操作。只有在主Chunk不可用,或者主Chunk回复信息表明它已不再持有租约的时候,客户机才需要重新跟Master节点联系。
  4. +
  5. 客户机把数据推送到所有的副本上。客户机可以以任意的顺序推送数据。Chunk服务器接收到数据并保存在它的内部LRU缓存中,一直到数据被使用或者过期交换出去。由于数据流的网络传输负载非常高,通过分离数据流和控制流,我们可以基于网络拓扑情况对数据流进行规划,提高系统性能,而不用去理会哪个Chunk服务器保存了主Chunk。3.2章节会进一步讨论这点。
  6. +
  7. 当所有的副本都确认接收到了数据,客户机发送写请求到主Chunk服务器。这个请求标识了早前推送到所有副本的数据。主Chunk为接收到的所有操作分配连续的序列号,这些操作可能来自不同的客户机,序列号保证了操作顺序执行。它以序列号的顺序把操作应用到它自己的本地状态中 (alex注:也就是在本地执行这些操作,这句话按字面翻译有点费解,也许应该翻译为“它顺序执行这些操作,并更新自己的状态”)
  8. +
  9. 主Chunk把写请求传递到所有的二级副本。每个二级副本依照主Chunk分配的序列号以相同的顺序执行这些操作。
  10. +
  11. 所有的二级副本回复主Chunk,它们已经完成了操作。
  12. +
  13. 主Chunk服务器 (alex注:即主Chunk所在的Chunk服务器) 回复客户机。任何副本产生的任何错误都会返回给客户机。在出现错误的情况下,写入操作可能在主Chunk和一些二级副本执行成功。(如果操作在主Chunk上失败了,操作就不会被分配序列号,也不会被传递。)客户端的请求被确认为失败,被修改的region处于不一致的状态。我们的客户机代码通过重复执行失败的操作来处理这样的错误。在从头开始重复执行之前,客户机会先从步骤(3)到步骤(7)做几次尝试。
  14. +
+

如果应用程序一次写入的数据量很大,或者数据跨越了多个Chunk,GFS客户机代码会把它们分成多个写操作。这些操作都遵循前面描述的控制流程,但是可能会被其它客户机上同时进行的操作打断或者覆盖。因此,共享的文件region的尾部可能包含来自不同客户机的数据片段,尽管如此,由于这些分解后的写入操作在所有的副本上都以相同的顺序执行完成,Chunk的所有副本都是一致的。这使文件region处于2.7节描述的一致的、但是未定义的状态。

+

3.2 数据流

+

为了提高网络效率,我们采取了把数据流和控制流分开的措施。在控制流从客户机到主Chunk、然后再到所有二级副本的同时,数据以管道的方式,顺序的沿着一个精心选择的Chunk服务器链推送。我们的目标是充分利用每台机器的带宽,避免网络瓶颈和高延时的连接,最小化推送所有数据的延时。

+

为了充分利用每台机器的带宽,数据沿着一个Chunk服务器链顺序的推送,而不是以其它拓扑形式分散推送(例如,树型拓扑结构)。线性推送模式下,每台机器所有的出口带宽都用于以最快的速度传输数据,而不是在多个接受者之间分配带宽。

+

为了尽可能的避免出现网络瓶颈和高延迟的链接(eg,inter-switch最有可能出现类似问题),每台机器都尽量的在网络拓扑中选择一台还没有接收到数据的、离自己最近的机器作为目标推送数据。假设客户机把数据从Chunk服务器S1推送到S4。它把数据推送到最近的Chunk服务器S1。S1把数据推送到S2,因为S2和S4中最接近的机器是S2。同样的,S2把数据传递给S3和S4之间更近的机器,依次类推推送下去。我们的网络拓扑非常简单,通过IP地址就可以计算出节点的“距离”。

+

最后,我们利用基于TCP连接的、管道式数据推送方式来最小化延迟。Chunk服务器接收到数据后,马上开始向前推送。管道方式的数据推送对我们帮助很大,因为我们采用全双工的交换网络。接收到数据后立刻向前推送不会降低接收的速度。在没有网络拥塞的情况下,传送B字节的数据到R个副本的理想时间是 B/T+RL ,T是网络的吞吐量,L是在两台机器数据传输的延迟。通常情况下,我们的网络连接速度是100Mbps(T),L将远小于1ms。因此,1MB的数据在理想情况下80ms左右就能分发出去。

+

3.3 原子的记录追加

+

GFS提供了一种原子的数据追加操作–记录追加。传统方式的写入操作,客户程序会指定数据写入的偏移量。对同一个region的并行写入操作不是串行的:region尾部可能会包含多个不同客户机写入的数据片段。使用记录追加,客户机只需要指定要写入的数据。GFS保证至少有一次原子的写入操作成功执行(即写入一个顺序的byte流),写入的数据追加到GFS指定的偏移位置上,之后GFS返回这个偏移量给客户机。这类似于在Unix操作系统编程环境中,对以O_APPEND模式打开的文件,多个并发写操作在没有竞态条件时的行为。

+

记录追加在我们的分布应用中非常频繁的使用,在这些分布式应用中,通常有很多的客户机并行地对同一个文件追加写入数据。如果我们采用传统方式的文件写入操作,客户机需要额外的复杂、昂贵的同步机制,例如使用一个分布式的锁管理器。在我们的工作中,这样的文件通常用于多个生产者/单一消费者的队列系统,或者是合并了来自多个客户机的数据的结果文件。

+

记录追加是一种修改操作,它也遵循3.1节描述的控制流程,除了在主Chunk有些额外的控制逻辑。客户机把数据推送给文件最后一个Chunk的所有副本,之后发送请求给主Chunk。主Chunk会检查这次记录追加操作是否会使Chunk超过最大尺寸(64MB)。如果超过了最大尺寸,主Chunk首先将当前Chunk填充到最大尺寸,之后通知所有二级副本做同样的操作,然后回复客户机要求其对下一个Chunk重新进行记录追加操作。(记录追加的数据大小严格控制在Chunk最大尺寸的1/4,这样即使在最坏情况下,数据碎片的数量仍然在可控的范围。)通常情况下追加的记录不超过Chunk的最大尺寸,主Chunk把数据追加到自己的副本内,然后通知二级副本把数据写在跟主Chunk一样的位置上,最后回复客户机操作成功。

+

如果记录追加操作在任何一个副本上失败了,客户端就需要重新进行操作。重新进行记录追加的结果是,同一个Chunk的不同副本可能包含不同的数据–重复包含一个记录全部或者部分的数据。GFS并不保证Chunk的所有副本在字节级别是完全一致的。它只保证数据作为一个整体原子的被至少写入一次。这个特性可以通过简单观察推导出来:如果操作成功执行,数据一定已经写入到Chunk的所有副本的相同偏移位置上。这之后,所有的副本至少都到了记录尾部的长度,任何后续的记录都会追加到更大的偏移地址,或者是不同的Chunk上,即使其它的Chunk副本被Master节点选为了主Chunk。就我们的一致性保障模型而言,记录追加操作成功写入数据的region是已定义的(因此也是一致的),反之则是不一致的(因此也就是未定义的)。正如我们在2.7.2节讨论的,我们的程序可以处理不一致的区域。

+

3.4 快照

+

(alex注:这一节非常难以理解,总的来说依次讲述了什么是快照、快照使用的COW技术、快照如何不干扰当前操作)

+

快照操作几乎可以瞬间完成对一个文件或者目录树(“源”)做一个拷贝,并且几乎不会对正在进行的其它操作造成任何干扰。我们的用户可以使用快照迅速的创建一个巨大的数据集的分支拷贝(而且经常是递归的拷贝拷贝),或者是在做实验性的数据操作之前,使用快照操作备份当前状态,这样之后就可以轻松的提交或者回滚到备份时的状态。

+

就像AFS (alex注:AFS,即Andrew File System,一种分布式文件系统), 我们用标准的copy-on-write技术实现快照。当Master节点收到一个快照请求,它首先取消作快照的文件的所有Chunk的租约。这个措施保证了后续对这些Chunk的写操作都必须与Master交互交互以找到租约持有者。这就给Master节点一个率先创建Chunk的新拷贝的机会。

+

租约取消或者过期之后,Master节点把这个操作以日志的方式记录到硬盘上。然后,Master节点通过复制源文件或者目录的元数据的方式,把这条日志记录的变化反映到保存在内存的状态中。新创建的快照文件和源文件指向完全相同的Chunk地址。

+

在快照操作之后,当客户机第一次想写入数据到Chunk C,它首先会发送一个请求到Master节点查询当前的租约持有者。Master节点注意到Chunke C的引用计数超过了1 (alex注:不太明白为什么会大于1.难道是Snapshot没有释放引用计数?) 。Master节点不会马上回复客户机的请求,而是选择一个新的Chunk句柄C 。之后,Master节点要求每个拥有Chunk C当前副本的Chunk服务器创建一个叫做C的新Chunk。通过在源Chunk所在Chunk服务器上创建新的Chunk,我们确保数据在本地而不是通过网络复制(我们的硬盘比我们的100Mb以太网大约快3倍)。从这点来讲,请求的处理方式和任何其它Chunk没什么不同:Master节点确保新Chunk C`的一个副本拥有租约,之后回复客户机,客户机得到回复后就可以正常的写这个Chunk,而不必理会它是从一个已存在的Chunk克隆出来的。

+

4. Master节点的操作

+

Master节点执行所有的名称空间操作。此外,它还管理着整个系统里所有Chunk的副本:它决定Chunk的存储位置,创建新Chunk和它的副本,协调各种各样的系统活动以保证Chunk被完全复制,在所有的Chunk服务器之间的进行负载均衡,回收不再使用的存储空间。

+

4.1 名称空间管理和锁

+

Master节点的很多操作会花费很长的时间:比如,快照操作必须取消Chunk服务器上快照所涉及的所有的Chunk的租约。我们不希望在这些操作的运行时,延缓了其它的Master节点的操作。因此,我们允许多个操作同时进行,使用名称空间的region上的锁来保证执行的正确顺序。

+

不同于许多传统文件系统,GFS没有针对每个目录实现能够列出目录下所有文件的数据结构。GFS也不支持文件或者目录的链接(即Unix术语中的硬链接或者符号链接)。在逻辑上,GFS的名称空间就是一个全路径和元数据映射关系的查找表。利用前缀压缩,这个表可以高效的存储在内存中。在存储名称空间的树型结构上,每个节点(绝对路径的文件名或绝对路径的目录名)都有一个关联的读写锁。

+

每个Master节点的操作在开始之前都要获得一系列的锁。通常情况下,如果一个操作涉及/d1/d2/…/dn/leaf,那么操作首先要获得目录/d1,/d1/d2,…,/d1/d2/…/dn的读锁,以及/d1/d2/…/dn/leaf的读写锁。注意,根据操作的不同,leaf可以是一个文件,也可以是一个目录。

+

我们演示一下在/home/user被快照到/save/user的时候,锁机制如何防止创建文件/home/user/foo。快照操作获取/home和/save的读取锁,以及/home/user和/save/user的写入锁。文件创建操作获得/home和/home/user的读取锁,以及/home/user/foo的写入锁。这两个操作要顺序执行,因为它们试图获取的/home/user的锁是相互冲突。文件创建操作不需要获取父目录的写入锁,因为这里没有”目录”,或者类似inode等用来禁止修改的数据结构。文件名的读取锁足以防止父目录被删除。

+

采用这种锁方案的优点是支持对同一目录的并行操作。比如,可以在同一个目录下同时创建多个文件:每一个操作都获取一个目录名的上的读取锁和文件名上的写入锁。目录名的读取锁足以的防止目录被删除、改名以及被快照。文件名的写入锁序列化文件创建操作,确保不会多次创建同名的文件。

+

因为名称空间可能有很多节点,读写锁采用惰性分配策略,在不再使用的时候立刻被删除。同样,锁的获取也要依据一个全局一致的顺序来避免死锁:首先按名称空间的层次排序,在同一个层次内按字典顺序排序。

+

4.2 副本的位置

+

GFS集群是高度分布的多层布局结构,而不是平面结构。典型的拓扑结构是有数百个Chunk服务器安装在许多机架上。Chunk服务器被来自同一或者不同机架上的数百个客户机轮流访问。不同机架上的两台机器间的通讯可能跨越一个或多个网络交换机。另外,机架的出入带宽可能比机架内所有机器加和在一起的带宽要小。多层分布架构对数据的灵活性、可靠性以及可用性方面提出特有的挑战。

+

Chunk副本位置选择的策略服务两大目标:最大化数据可靠性和可用性,最大化网络带宽利用率。为了实现这两个目的,仅仅是在多台机器上分别存储这些副本是不够的,这只能预防硬盘损坏或者机器失效带来的影响,以及最大化每台机器的网络带宽利用率。我们必须在多个机架间分布储存Chunk的副本。这保证Chunk的一些副本在整个机架被破坏或掉线(比如,共享资源,如电源或者网络交换机造成的问题)的情况下依然存在且保持可用状态。这还意味着在网络流量方面,尤其是针对Chunk的读操作,能够有效利用多个机架的整合带宽。另一方面,写操作必须和多个机架上的设备进行网络通信,但是这个代价是我们愿意付出的。

+

4.3 创建,重新复制,重新负载均衡

+

Chunk的副本有三个用途:Chunk创建,重新复制和重新负载均衡。

+

当Master节点创建一个Chunk时,它会选择在哪里放置初始的空的副本。Master节点会考虑几个因素。(1)我们希望在低于平均硬盘使用率的Chunk服务器上存储新的副本。这样的做法最终能够平衡Chunk服务器之间的硬盘使用率。(2)我们希望限制在每个Chunk服务器上”最近”的Chunk创建操作的次数。虽然创建操作本身是廉价的,但是创建操作也意味着随之会有大量的写入数据的操作,因为Chunk在Writer真正写入数据的时候才被创建,而在我们的”追加一次,读取多次”的工作模式下,Chunk一旦写入成功之后就会变为只读的了。(3)如上所述,我们希望把Chunk的副本分布在多个机架之间。

+

当Chunk的有效副本数量少于用户指定的复制因数的时候,Master节点会重新复制它。这可能是由几个原因引起的:一个Chunk服务器不可用了,Chunk服务器报告它所存储的一个副本损坏了,Chunk服务器的一个磁盘因为错误不可用了,或者Chunk副本的复制因数提高了。每个需要被重新复制的Chunk都会根据几个因素进行排序。一个因素是Chunk现有副本数量和复制因数相差多少。例如,丢失两个副本的Chunk比丢失一个副本的Chunk有更高的优先级。另外,我们优先重新复制活跃(live)文件的Chunk而不是最近刚被删除的文件的Chunk(查看4.4节)。最后,为了最小化失效的Chunk对正在运行的应用程序的影响,我们提高会阻塞客户机程序处理流程的Chunk的优先级。

+

Master节点选择优先级最高的Chunk,然后命令某个Chunk服务器直接从可用的副本”克隆”一个副本出来。选择新副本的位置的策略和创建时类似:平衡硬盘使用率、限制同一台Chunk服务器上的正在进行的克隆操作的数量、在机架间分布副本。为了防止克隆产生的网络流量大大超过客户机的流量,Master节点对整个集群和每个Chunk服务器上的同时进行的克隆操作的数量都进行了限制。另外,Chunk服务器通过调节它对源Chunk服务器读请求的频率来限制它用于克隆操作的带宽。

+

最后,Master服务器周期性地对副本进行重新负载均衡:它检查当前的副本分布情况,然后移动副本以便更好的利用硬盘空间、更有效的进行负载均衡。而且在这个过程中,Master服务器逐渐的填满一个新的Chunk服务器,而不是在短时间内用新的Chunk填满它,以至于过载。新副本的存储位置选择策略和上面讨论的相同。另外,Master节点必须选择哪个副本要被移走。通常情况,Master节点移走那些剩余空间低于平均值的Chunk服务器上的副本,从而平衡系统整体的硬盘使用率。

+

4.4 垃圾回收

+

GFS在文件删除后不会立刻回收可用的物理空间。GFS空间回收采用惰性的策略,只在文件和Chunk级的常规垃圾收集时进行。我们发现这个方法使系统更简单、更可靠。

+

4.4.1 机制

+

当一个文件被应用程序删除时,Master节点像对待其它修改操作一样,立刻把删除操作以日志的方式记录下来。但是,Master节点并不马上回收资源,而是把文件名改为一个包含删除时间戳的、隐藏的名字。当Master节点对文件系统命名空间做常规扫描的时候,它会删除所有三天前的隐藏文件(这个时间间隔是可以设置的)。直到文件被真正删除,它们仍旧可以用新的特殊的名字读取,也可以通过把隐藏文件改名为正常显示的文件名的方式“反删除”。当隐藏文件被从名称空间中删除,Master服务器内存中保存的这个文件的相关元数据才会被删除。这也有效的切断了文件和它包含的所有Chunk的连接 (alex注:原文是This effectively severs its links to all its chunks)

+

在对Chunk名字空间做类似的常规扫描时,Master节点找到孤儿Chunk(不被任何文件包含的Chunk)并删除它们的元数据。Chunk服务器在和Master节点交互的心跳信息中,报告它拥有的Chunk子集的信息,Master节点回复Chunk服务器哪些Chunk在Master节点保存的元数据中已经不存在了。Chunk服务器可以任意删除这些Chunk的副本。

+

4.4.2 讨论

+

虽然分布式垃圾回收在编程语言领域是一个需要复杂的方案才能解决的难题,但是在GFS系统中是非常简单的。我们可以轻易的得到Chunk的所有引用:它们都只存储在Master服务器上的文件到块的映射表中。我们也可以很轻易的得到所有Chunk的副本:它们都以Linux文件的形式存储在Chunk服务器的指定目录下。所有Master节点不能识别的副本都是”垃圾”。

+

垃圾回收在空间回收方面相比直接删除有几个优势。首先,对于组件失效是常态的大规模分布式系统,垃圾回收方式简单可靠。Chunk可能在某些Chunk服务器创建成功,某些Chunk服务器上创建失败,失败的副本处于无法被Master节点识别的状态。副本删除消息可能丢失,Master节点必须重新发送失败的删除消息,包括自身的和Chunk服务器的 (alex注:自身的指删除metadata的消息) 。垃圾回收提供了一致的、可靠的清除无用副本的方法。第二,垃圾回收把存储空间的回收操作合并到Master节点规律性的后台活动中,比如,例行扫描和与Chunk服务器握手等。因此,操作被批量的执行,开销会被分散。另外,垃圾回收在Master节点相对空闲的时候完成。这样Master节点就可以给那些需要快速反应的客户机请求提供更快捷的响应。第三,延缓存储空间回收为意外的、不可逆转的删除操作提供了安全保障。

+

根据我们的使用经验,延迟回收空间的主要问题是,延迟回收会阻碍用户调优存储空间的使用,特别是当存储空间比较紧缺的时候。当应用程序重复创建和删除临时文件时,释放的存储空间不能马上重用。我们通过显式的再次删除一个已经被删除的文件的方式加速空间回收的速度。我们允许用户为命名空间的不同部分设定不同的复制和回收策略。例如,用户可以指定某些目录树下面的文件不做复制,删除的文件被即时的、不可恢复的从文件系统移除。

+

4.5 过期失效的副本检测

+

当Chunk服务器失效时,Chunk的副本有可能因错失了一些修改操作而过期失效。Master节点保存了每个Chunk的版本号,用来区分当前的副本和过期副本。

+

无论何时,只要Master节点和Chunk签订一个新的租约,它就增加Chunk的版本号,然后通知最新的副本。Master节点和这些副本都把新的版本号记录在它们持久化存储的状态信息中。这个动作发生在任何客户机得到通知以前,因此也是对这个Chunk开始写之前。如果某个副本所在的Chunk服务器正好处于失效状态,那么副本的版本号就不会被增加。Master节点在这个Chunk服务器重新启动,并且向Master节点报告它拥有的Chunk的集合以及相应的版本号的时候,就会检测出它包含过期的Chunk。如果Master节点看到一个比它记录的版本号更高的版本号,Master节点会认为它和Chunk服务器签订租约的操作失败了,因此会选择更高的版本号作为当前的版本号。

+

Master节点在例行的垃圾回收过程中移除所有的过期失效副本。在此之前,Master节点在回复客户机的Chunk信息请求的时候,简单的认为那些过期的块根本就不存在。另外一重保障措施是,Master节点在通知客户机哪个Chunk服务器持有租约、或者指示Chunk服务器从哪个Chunk服务器进行克隆时,消息中都附带了Chunk的版本号。客户机或者Chunk服务器在执行操作时都会验证版本号以确保总是访问当前版本的数据。

+

5. 容错和诊断

+

我们在设计GFS时遇到的最大挑战之一是如何处理频繁发生的组件失效。组件的数量和质量让这些问题出现的频率远远超过一般系统意外发生的频率:我们不能完全依赖机器的稳定性,也不能完全相信硬盘的可靠性。组件的失效可能造成系统不可用,更糟糕的是,还可能产生不完整的数据。我们讨论我们如何面对这些挑战,以及当组件失效不可避免的发生时,用GFS自带工具诊断系统故障。

+

5.1 高可用性

+

在GFS集群的数百个服务器之中,在任何给定的时间必定会有些服务器是不可用的。我们使用两条简单但是有效的策略保证整个系统的高可用性:快速恢复和复制。

+

5.1.1 快速恢复

+

不管Master服务器和Chunk服务器是如何关闭的,它们都被设计为可以在数秒钟内恢复它们的状态并重新启动。事实上,我们并不区分正常关闭和异常关闭;通常,我们通过直接kill掉进程来关闭服务器。客户机和其它的服务器会感觉到系统有点颠簸 (alex注:a minor hiccup) ,正在发出的请求会超时,需要重新连接到重启后的服务器,然后重试这个请求。

+

5.1.2 Chunk复制

+

正如之前讨论的,每个Chunk都被复制到不同机架上的不同的Chunk服务器上。用户可以为文件命名空间的不同部分设定不同的复制级别。缺省是3。当有Chunk服务器离线了,或者通过Chksum校验(参考5.2节)发现了已经损坏的数据,Master节点通过克隆已有的副本保证每个Chunk都被完整复制 (alex注:即每个Chunk都有复制因子制定的个数个副本,缺省是3)。 虽然Chunk复制策略对我们非常有效,但是我们也在寻找其它形式的跨服务器的冗余解决方案,比如使用奇偶校验、或者Erasure codes (alex注:Erasure codes用来解决链接层中不相关的错误,以及网络拥塞和buffer限制造成的丢包错误) 来解决我们日益增长的只读存储需求。我们的系统主要的工作负载是追加方式的写入和读取操作,很少有随机的写入操作,因此,我们认为在我们这个高度解耦合的系统架构下实现这些复杂的冗余方案很有挑战性,但并非不可实现。

+

5.1.3 Master服务器的复制

+

为了保证Master服务器的可靠性,Master服务器的状态也要复制。Master服务器所有的操作日志和checkpoint文件都被复制到多台机器上。对Master服务器状态的修改操作能够提交成功的前提是,操作日志写入到Master服务器的备节点和本机的磁盘。简单说来,一个Master服务进程负责所有的修改操作,包括后台的服务,比如垃圾回收等改变系统内部状态活动。当它失效的时候,几乎可以立刻重新启动。如果Master进程所在的机器或者磁盘失效了,处于GFS系统外部的监控进程会在其它的存有完整操作日志的机器上启动一个新的Master进程。客户端使用规范的名字访问Master(比如gfs-test)节点,这个名字类似DNS别名,因此也就可以在Master进程转到别的机器上执行时,通过更改别名的实际指向访问新的Master节点。

+

此外,GFS中还有些“影子”Master服务器,这些“影子”服务器在“主”Master服务器宕机的时候提供文件系统的只读访问。它们是影子,而不是镜像,所以它们的数据可能比“主”Master服务器更新要慢,通常是不到1秒。对于那些不经常改变的文件、或者那些允许获取的数据有少量过期的应用程序,“影子”Master服务器能够提高读取的效率。事实上,因为文件内容是从Chunk服务器上读取的,因此,应用程序不会发现过期的文件内容。在这个短暂的时间窗内,过期的可能是文件的元数据,比如目录的内容或者访问控制信息。

+

“影子”Master服务器为了保持自身状态是最新的,它会读取一份当前正在进行的操作的日志副本,并且依照和主Master服务器完全相同的顺序来更改内部的数据结构。和主Master服务器一样,“影子”Master服务器在启动的时候也会从Chunk服务器轮询数据(之后定期拉数据),数据中包括了Chunk副本的位置信息;“影子”Master服务器也会定期和Chunk服务器“握手”来确定它们的状态。在主Master服务器因创建和删除副本导致副本位置信息更新时,“影子”Master服务器才和主Master服务器通信来更新自身状态。

+

5.2 数据完整性

+

每个Chunk服务器都使用Checksum来检查保存的数据是否损坏。考虑到一个GFS集群通常都有好几百台机器、几千块硬盘,磁盘损坏导致数据在读写过程中损坏或者丢失是非常常见的(第7节讲了一个原因)。我们可以通过别的Chunk副本来解决数据损坏问题,但是跨越Chunk服务器比较副本来检查数据是否损坏很不实际。另外,GFS允许有歧义的副本存在:GFS修改操作的语义,特别是早先讨论过的原子纪录追加的操作,并不保证副本完全相同 (alex注:副本不是byte-wise完全一致的) 。因此,每个Chunk服务器必须独立维护Checksum来校验自己的副本的完整性。

+

我们把每个Chunk都分成64KB大小的块。每个块都对应一个32位的Checksum。和其它元数据一样,Checksum与其它的用户数据是分开的,并且保存在内存和硬盘上,同时也记录操作日志。

+

对于读操作来说,在把数据返回给客户端或者其它的Chunk服务器之前,Chunk服务器会校验读取操作涉及的范围内的块的Checksum。因此Chunk服务器不会把错误数据传递到其它的机器上。如果发生某个块的Checksum不正确,Chunk服务器返回给请求者一个错误信息,并且通知Master服务器这个错误。作为回应,请求者应当从其它副本读取数据,Master服务器也会从其它副本克隆数据进行恢复。当一个新的副本就绪后,Master服务器通知副本错误的Chunk服务器删掉错误的副本。

+

Checksum对读操作的性能影响很小,可以基于几个原因来分析一下。因为大部分的读操作都至少要读取几个块,而我们只需要读取一小部分额外的相关数据进行校验。GFS客户端代码通过每次把读取操作都对齐在Checksum block的边界上,进一步减少了这些额外的读取操作的负面影响。另外,在Chunk服务器上,Chunksum的查找和比较不需要I/O操作,Checksum的计算可以和I/O操作同时进行。

+

Checksum的计算针对在Chunk尾部的追加写入操作作了高度优化(与之对应的是覆盖现有数据的写入操作),因为这类操作在我们的工作中占了很大比例。我们只增量更新最后一个不完整的块的Checksum,并且用所有的追加来的新Checksum块来计算新的Checksum。即使是最后一个不完整的Checksum块已经损坏了,而且我们不能够马上检查出来,由于新的Checksum和已有数据不吻合,在下次对这个块进行读取操作的时候,会检查出数据已经损坏了。

+

相比之下,如果写操作覆盖已经存在的一个范围内的Chunk,我们必须读取和校验被覆盖的第一个和最后一个块,然后再执行写操作;操作完成之后再重新计算和写入新的Checksum。如果我们不校验第一个和最后一个被写的块,那么新的Checksum可能会隐藏没有被覆盖区域内的数据错误。

+

在Chunk服务器空闲的时候,它会扫描和校验每个不活动的Chunk的内容。这使得我们能够发现很少被读取的Chunk是否完整。一旦发现有Chunk的数据损坏,Master可以创建一个新的、正确的副本,然后把损坏的副本删除掉。这个机制也避免了非活动的、已损坏的Chunk欺骗Master节点,使Master节点认为它们已经有了足够多的副本了。

+

5.3 诊断工具

+

详尽的、深入细节的诊断日志,在问题隔离、调试、以及性能分析等方面给我们带来无法估量的帮助,同时也只需要很小的开销。没有日志的帮助,我们很难理解短暂的、不重复的机器之间的消息交互。GFS的服务器会产生大量的日志,记录了大量关键的事件(比如,Chunk服务器启动和关闭)以及所有的RPC的请求和回复。这些诊断日志可以随意删除,对系统的正确运行不造成任何影响。然而,我们在存储空间允许的情况下会尽量的保存这些日志。

+

RPC日志包含了网络上发生的所有请求和响应的详细记录,但是不包括读写的文件数据。通过匹配请求与回应,以及收集不同机器上的RPC日志记录,我们可以重演所有的消息交互来诊断问题。日志还用来跟踪负载测试和性能分析。

+

日志对性能的影响很小(远小于它带来的好处),因为这些日志的写入方式是顺序的、异步的。最近发生的事件日志保存在内存中,可用于持续不断的在线监控。

+

7. 经验

+

在建造和部署GFS的过程中,我们经历了各种各样的问题,有些是操作上的,有些是技术上的。

+

起初,GFS被设想为我们的生产系统的后端文件系统。随着时间推移,在GFS的使用中逐步的增加了对研究和开发任务的支持。我们开始增加一些小的功能,比如权限和配额,到了现在,GFS已经初步支持了这些功能。虽然我们生产系统是严格受控的,但是用户层却不总是这样的。需要更多的基础架构来防止用户间的相互干扰。

+

我们最大的问题是磁盘以及和Linux相关的问题。很多磁盘都声称它们支持某个范围内的Linux IDE硬盘驱动程序,但是实际应用中反映出来的情况却不是这样,它们只支持最新的驱动。因为协议版本很接近,所以大部分磁盘都可以用,但是偶尔也会有由于协议不匹配,导致驱动和内核对于驱动器的状态判断失误。这会导致数据因为内核中的问题意外的被破坏了。这个问题促使我们使用Checksum来校验数据,同时我们也修改内核来处理这些因为协议不匹配带来的问题。

+

较早的时候,我们在使用Linux 2.2内核时遇到了些问题,主要是fsync()的效率问题。它的效率与文件的大小而不是文件修改部分的大小有关。这在我们的操作日志文件过大时给出了难题,尤其是在我们尚未实现Checkpoint的时候。我们费了很大的力气用同步写来解决这个问题,但是最后还是移植到了Linux2.4内核上。

+

另一个和Linux相关的问题是单个读写锁的问题,也就是说,在某一个地址空间的任意一个线程都必须在从磁盘page in(读锁)的时候先hold住,或者在mmap()调用(写锁)的时候改写地址空间。我们发现即使我们的系统负载很轻的情况下也会有偶尔的超时,我们花费了很多的精力去查找资源的瓶颈或者硬件的问题。最后我们终于发现这个单个锁在磁盘线程交换以前映射的数据到磁盘的时候,锁住了当前的网络线程,阻止它把新数据映射到内存。由于我们的性能主要受限于网络接口,而不是内存copy的带宽,因此,我们用pread()替代mmap(),用了一个额外的copy动作来解决这个问题。

+

尽管偶尔还是有其它的问题,Linux的开放源代码还是使我们能够快速探究和理解系统的行为。在适当的时候,我们会改进内核并且和公开源码组织共享这些改动。

+

9. 结束语

+

Google文件系统展示了一个使用普通硬件支持大规模数据处理的系统的特质。虽然一些设计要点都是针对我们的特殊的需要定制的,但是还是有很多特性适用于类似规模的和成本的数据处理任务。

+

首先,我们根据我们当前的和可预期的将来的应用规模和技术环境来评估传统的文件系统的特性。我们的评估结果将我们引导到一个使用完全不同于传统的设计思路上。根据我们的设计思路,我们认为组件失效是常态而不是异常,针对采用追加方式(有可能是并发追加)写入、然后再读取(通常序列化读取)的大文件进行优化,以及扩展标准文件系统接口、放松接口限制来改进整个系统。

+

我们系统通过持续监控,复制关键数据,快速和自动恢复提供灾难冗余。Chunk复制使得我们可以对Chunk服务器的失效进行容错。高频率的组件失效要求系统具备在线修复机制,能够周期性的、透明的修复损坏的数据,也能够第一时间重新建立丢失的副本。此外,我们使用Checksum在磁盘或者IDE子系统级别检测数据损坏,在这样磁盘数量惊人的大系统中,损坏率是相当高的。

+

我们的设计保证了在有大量的并发读写操作时能够提供很高的合计吞吐量。我们通过分离控制流和数据流来实现这个目标,控制流在Master服务器处理,而数据流在Chunk服务器和客户端处理。当一般的操作涉及到Master服务器时,由于GFS选择的Chunk尺寸较大 (alex注:从而减小了元数据的大小), 以及通过Chunk Lease将控制权限移交给主副本,这些措施将Master服务器的负担降到最低。这使得一个简单、中心的Master不会成为成为瓶颈。我们相信我们对网络协议栈的优化可以提升当前对于每客户端的写入吞吐量限制。

+

GFS成功的实现了我们对存储的需求,在Google内部,无论是作为研究和开发的存储平台,还是作为生产系统的数据处理平台,都得到了广泛的应用。它是我们持续创新和处理整个WEB范围内的难题的一个重要工具。

+

LEC 3

+

存储系统

+

存储系统是容错系统的基础构件

+

如果可以建立一个持久的存储系统,应用程序不需要特殊对自己的状态进行保存,因为存储系统已经存好了,从而简化了应用程序的设计。

+

因此存储系统本身必须有很高的容错性能,设计这个并不容易。

+
    +
  • 高性能:需要跨服务器对数据分片
  • +
  • 多服务器:出错概率非常大
  • +
  • 容错机制:复制数据到其他的机器上
  • +
  • 多份数据:带来数据的不一致性
  • +
  • 强一致性:持久性协议,写入存储系统会降低性能
  • +
+

因此形成了一个环,主要矛盾是一致性和性能之间的矛盾

+

一致性

+

理想情况下的一致性:分布式系统与单机系统在表现上完全相同

+

然而在实际情况下很难实现

+
    +
  1. 并发问题:
  2. +
+

两个线程为同一个变量写入了不同的值,此时有两个线程读取。

+

此时读取的值应该是中的任意一个,而的值应该与相同,才是我们希望看到的结果。

+
    +
  1. 故障问题
  2. +
+

解决故障一般是通过使用复制数据到其他机器上的方式。

+

一个很烂的服务器之间复制数据的方案:客户端写入数据的时候,同时向两个服务器写入数据,不需要服务器之间同步。

+

此时两个线程为同一个变量写入了不同的值,两个线程读取不一定读出什么。

+

GFS

+

相当于一个分布式系统的案例研究,包括了高性能、复制和容错、一致性等等主题

+

GFS是第一个在上千台计算机上构建的分布式系统,后续的HDFS等都受到了GFS的启发。

+

两个非标准做法:

+
    +
  • 单一的master节点
  • +
  • 存在不一致的地方
  • +
+

关键属性

+
    +
  • 大数据集:可能是从整个互联网上爬取的数据集
  • +
  • 快速:自动分片到多个磁盘
  • +
  • 全局共享:所有应用程序都看到的是相同的文件系统
  • +
  • 容错:自动容错性(或者容错能力很强)
  • +
+

设计

+

zbm43T.md.png

+

Master

+
    +
  • 文件名到chunk的对应关系(存在日志中-持久存储)
  • +
  • 对每一个chunk有一个版本号(持久存储,恢复时候才能找到正确的版本)和服务器列表
  • +
  • 日志(首先的操作,建立持久存储)+checkpoints(持久存储)
  • +
+

读取文件

+
    +
  1. 客户端发送消息
  2. +
  3. master通过消息返回chunk信息等
  4. +
  5. 客户端缓存信息(在一段时间内不需要再次与master进行通信)
  6. +
  7. 客户端从信息中的最近的服务器中读取文件
  8. +
  9. 最近的服务器检查版本号,无误后发送数据
  10. +
+

写文件(追加操作)

+

zbbzEn.png

+
    +
  1. 客户端发送消息
  2. +
  3. master增加版本号,选取primary服务器,服务器增加版本号(持久存储),发送租约,返回消息给客户端
  4. +
  5. 客户端发送写数据的请求给最近的服务器,然后服务器之间通过网络传递数据
  6. +
  7. 客户端给primary服务器端发送写数据的消息
  8. +
  9. primary服务器检查版本号和租约,无误后写入数据,发送写数据的信息给其他的服务器
  10. +
  11. 其他服务器反馈写成功的消息给primary服务器
  12. +
  13. 服务器反馈成功消息给客户端
  14. +
+

如果中间过程有错误,客户端一般会重试,希望下一次可以正常运行(也就是最少一次)

+

这可能会造成在一个磁盘中有两份数据的拷贝。会有id和checksum协助控制不会将相同的数据读取两次。

+

一致性

+

一个服务器暂时挂掉了,导致版本号没有更新,同时一个客户端的版本号也是一个老版本号,结果正好匹配到了这个刚刚挂掉的服务器,最终导致读取的数据和期望的不同。

+

通过租约机制确保只会存在一个primary服务器,不会产生“脑裂”现象

+

获得强一致性?更新所有的除primary外的其他服务器或者全部都不更新,GFS没有实现这个。

+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-LEC 3 GFS
+
https://zhangzhao219.github.io/2022/12/17/6.824/Distributed-Systems-MIT-6.824-LEC-3/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月17日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/18/6.824/Distributed-Systems-MIT-6.824-LEC-4/index.html b/2022/12/18/6.824/Distributed-Systems-MIT-6.824-LEC-4/index.html new file mode 100644 index 000000000..8040b17fe --- /dev/null +++ b/2022/12/18/6.824/Distributed-Systems-MIT-6.824-LEC-4/index.html @@ -0,0 +1,931 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-LEC 4 Primary-Backup Replication - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-LEC 4 Primary-Backup Replication

+ + +
+ +

MIT-6.824(Spring 2022)LEC 4 Primary-Backup Replication

+ +

Fault-Tolerant Virtual Machines 论文阅读

+

参考翻译

+ + +
+ +
+ + + +

摘要

+

通过提供故障容错性的虚拟机,我们实现了一个商业化的企业级系统,建立在复制一个主虚拟机的执行过程到另一个服务器上的备份虚拟机的基础上。系统很容易使用,同时保证了应用的性能仅有少于10%的降低。另外,为了让主VM和二级VM的执行活动保持一致,对于几个实际的应用而言,需要的数据带宽少于20Mbit/s,这也允许实现更长距离的故障容错的可能性。一种容易使用,在故障后自动恢复备份的商业化系统,在复制VM执行之前需要额外的组件。我们已经设计并且实现了这些额外的组件,并且解决了在支持VM运行企业级应用的时候,遇到的许多实际问题。

+

1. 简介

+

一个实现故障容忍服务器的常见方法是主备机制,主服务器失败的同时另外一个备份服务器立即进行接管,此时对于外部客户端而言,故障就相当于被隐藏了起来,并且不会造成数据丢失。因此在任何时间,备份服务器的状态必须和主服务器几乎保持一致,在备份服务器上复制状态的一种方法是将主服务器的所有状态,包括CPU、memory、IO设备,连续地送给备份服务器。然而,这种发送状态的方法,尤其是涉及到内存中的变更,其需要的带宽非常大。

+

另一种可以用更少带宽复制服务器的方法类似于状态机。这种思路是将服务器建模为确定性的状态机,他们从相同的初始状态开始,并且确保以相同的顺序接收相同的输入请求,这样就能保持同步。因为大多数服务器或服务有一些不确定性的操作,因此必须使用额外的协调机制来确保主备同步。然而,需要保持主备一致性的额外信息数目,远远少于正在变更的主服务器上状态(主要是内存更新)的数目。

+

实现协调机制来确保物理服务器的确定性操作是困难的,尤其随着处理器频率增长。反之,一个运行在管理程序(hypervisor)上的VM,是一个实现状态机方法的很好的平台。 一个VM可以被当作一个定义好的状态机,它的操作是机器被虚拟化的操作(包括它所有的设备) 。和物理服务器一样,VM有相同的非确定性操作(例如读取时钟或发送中断),因此为了保持同步,额外的信息必须被发送给备份服务器。管理程序(hypervisor)有VM的全权控制权利,包括处理所有输入,因此它能够获得所有与主VM上的非确定性操作有关的必要信息,并且能正确地重放这些操作。

+

因此,这个状态机方法可以通过商业化软件上的VM来实现,它不需要硬件更改,允许在最新的微处理器上立刻实现故障容错。另外,状态机方法需要的低带宽允许了主备服务器能更好地进行物理分隔。例如,被复制的VM可以运行在横跨一个学校的物理机器上,相比于运行在同一建筑内的VM而言,可以提供更多的可靠性。

+

我们在VMware vSphere 4.0平台上使用主备机制实现了故障容忍的VMs,VMware vSphere实现了一个完整的x86虚拟机,所以我们自动地能够为任何x86操作系统和应用提供故障容忍。这种允许我们记录一个主服务器执行,并确保备份服务器一致执行的基础技术是确定性重放。VMware vSphere Fault Tolerance(FT)是基于确定性重放(Deterministic Replay) 的,但是为了建立一个完整的故障容忍系统,还增加了必要的额外协议和功能。除了提供硬件故障容忍,我们的系统在一次失败后,通过在局部集群中任何可接受的服务器上开始一个新的备份虚拟机,进行自动地存储备份。目前确定性重放和VMare FT的产品版本只支持单处理器的VMs。多处理器VM的操作记录和重放还在开发中,因为每个共享内存的操作都是一个非确定性的操作,因此还有重要的性能问题待解决。

+

Bressoud和Schneider描述了一个针对HP PA-RISC平台的故障容忍VMs的原型实现。我们的方法是相似的,但是出于性能原因,以及在调查了许多可替代设计后,我们已经做了一些基础性的改变。另外,为了建立一个完整的系统,而这个系统是有效的并且能够被正在运行企业级应用的客户使用,我们已经设计并实现了系统中许多额外的组件,可以处理许多实际问题。与大多数其他实际系统讨论的类似, 我们只尝试应付fail-stop的故障 ,这是一种服务器故障,可以在故障服务器造成一次不正确的外部可见行为之前被检测。( Hades注:fail-stop故障指的是,如果某些东西出现故障,只是单纯的停止运行,而不是运算出错误结果。比如电源线、服务器风扇导致CPU过热停止运行、网络等故障

+

2. 基本的FT设计

+

+

图1展示了我们系统在故障容忍VMs的基本步骤。对于一个给定的VM,我们希望提供故障容忍(主VM),我们在一个完全不同的物理机器上运行一个备份VM,保持和主VM同步并且执行一致,虽然存在短时间的滞后。我们说这两个VMs是虚拟的步调一致。VMs的虚拟磁盘是在一个共享存储中的(例如一个Fibre Channel或者iSCSI磁盘阵列),因此可以接受主备服务器的输入和输出。(我们将在4.1节中讨论带有分隔的非共享虚拟磁盘的主备VM的设计)只有主VM会说明它在网络中的存在,因此所有网络输入都会来到主VM上。相似地,所有其他输入(例如键盘和鼠标)也只会来到主VM上。

+

所有主VM接收到的输入都会通过名为logging channel的网络连接,被发送到备份VM上。对于几个工作负载而言,主要的输入途径是网络和磁盘。为了保证备份VM和主VM使用相同的方式执行非确定性操作,下面2.1节讨论的额外的信息也需要发送。最终备份VM总是执行和主VM一致的操作。然而,备份VM的输出会被管理程序扔掉,因此只有主VM产生实际输出,并被返回给客户端。和2.2节中描述的一样,为了确保主VM失败后没有数据丢失,主备VM遵循一个具体的协议,包括备份VM明确的确认信息。

+

为了检测主或备份虚拟机是否失败,我们的系统既使用相关服务器间的心跳机制,同时也监测 logging channel 上的流量。另外,我们我们必须确保只有主或备份VM执行操作,即使存在脑裂(split brain)的场景(在这种场景中主备服务器互相之间会失去通信)。

+

2.1 确定性重放的实现

+

正如我们已经提到的,复制服务器(或者VM)的操作可以被建模为确定性状态机的复制。如果两个确定性的状态机以相同的初始状态开始,并且以相同的顺序提供确切的输入,它们将经历相同的状态序列并且产生相同的输出。一个虚拟机有很宽泛的输入,包括到来的网络包,磁盘读,以及来自键盘和鼠标的输入。非确定性事件(例如虚拟中断)和非确定性操作(例如处理器的时钟周期计数器)也会影响虚拟机的状态。这显示了对于正在运行任何操作系统和工作负载的任何虚拟机而言,复制执行有 三个挑战

+
    +
  1. 为了保证一个备份虚拟机的确定性执行,正确地得到所有输入以及非确定性执行是必要的。
  2. +
  3. 正确地将输入与非确定性执行应用到备份虚拟机上。
  4. +
  5. 以一种不会引起性能退化的方式执行。
  6. +
+

另外,许多在x86处理器上的复杂操作还未被定义,因此会引起非确定性以及副作用。捕获这些未定义的操作并且重放它们产生相同的状态是一个额外的挑战。

+

针对在VMare vSphere平台上的x86虚拟机,VMware确定性地重放恰好提供了这个功能。确定性重放记录了 VM 的输入以及与 VM执行相关的所有可能的不确定性的日志条目流,这些条目会被写入日志文件。在读取日志文件中的条目后,VM 操作会被精确地重放。 对于非确定性操作,为了允许操作以相同的状态变化和输出再现,需要记录足够的信息。 对于非确定性事件,例如定时器或 IO 完成中断,事件发生的确切指令也会被记录下来。 在重播期间,事件被传递在指令流中的同一位置。 VMware 确定性重放采用各种技术,实现了高效的事件记录和事件传递机制,包括使用AMD和英特尔联合开发的硬件性能计数器。

+

Bressoud 和 Schneider提到将VM执行切分成不同的epoch,其中非确定性事件,例如中断仅在一个epoch结束时传递。 epoch的概念似乎被用作批处理机制,因为在它发生的确切指令处单独传递每个中断的成本太高。然而,我们的事件传递机制足够高效,以至于 VMware确定性重放不需要使用epochs。 每次中断在发生时被记录,并且在重放时有效地传递到适当的指令处。

+

2.2 FT协议

+

对于 VMware FT而言,我们使用确定性重放来生成必要的日志条目来记录主VM的执行情况,但是不是将日志条目写入磁盘,而是通过日志通道将它们发送到备份VM。备份 VM 实时重放日志条目,因此与主 VM 的执行保持一致。 然而,我们必须在日志通道中使用严格的 FT 协议以增强日志条目,从而确保我们实现故障容忍。 我们的基本要求如下:

+

输出要求 :如果备份VM在主VM发生故障后接管,那么备份VM将继续以一种与主虚拟机发送到外部世界的所有输出完全一致的方式执行。

+

请注意,在发生故障转移后(即备份 VM 需要在主VM故障后接管),备份VM开始执行的方式可能与主 VM 相当不同,因为在执行期间发生了许多非确定性事件。但是,只要备份VM满足输出要求,在故障转移到备份 VM期间 没有外部可见状态或数据的丢失 ,客户端将注意到他们的服务没有中断或不一致。

+

可以通过延迟任何外部输出(通常是网络数据包)直到备份VM 已收到重放的所有信息来确保输出要求,这些信息允许它至少执行到该输出操作的点。一个必要条件是备份 VM 必须接收到输出操作之前生成的所有日志条目。这些日志条目将允许它执行到最后一个日志条目的点。但是,假设失败是在主VM执行输出操作后立即发生。备份 VM 必须知道它必须继续重播到输出操作点,并且到那时只能“上线”(停止重播并作为主VM接管,如2.3 节所述)。如果备份将在输出操作之前的最后一个日志条目点上线,一些非确定性事件(例如计时器传递给 VM 的中断)可能会在执行输出操作之前改变其执行路径。

+

给定上述的限制,强制满足输入要求的最容易的方式是在每个输出操作时创建一个特殊的日志条目。然后,输出要求一定被下面特殊的规则限制:

+

输出规则 :主VM可能不发送一个输出到外部世界,直到备份VM已收到并确认与产生输出的操作相关的日志条目。

+

如果备份 VM 已收到所有日志条目,包括生成输出操作的日志条目,然后备份 VM 将能够准确地重现主 VM在输出点的状态,所以如果主VM死了, 备份将正确地达到一个与输出一致的状态 。相反,如果备份VM在没有收到所有必要的日志条目的情况下接管,那么它的状态可能会 迅速分歧 ,以至于与主服务器的输出不一致。输出规则在某些方面类似于 [11] 中描述的方法,其中“外部同步” IO 实际上可以被缓存,只要它在下一次外部通信之前确实被写入磁盘了。

+

请注意,输出规则没有说明关于停止主VM执行的任何事。我们只需要延迟输出发送,但 VM 本身可以继续执行。由于操作系统通过异步中断来指示完成,因此可以执行非阻塞的网络和磁盘输出,VM可以轻松地继续执行并且不一定会立即受到输出延迟的影响。相比之下,以前的工作 [3, 9] 通常必须在执行输出之前完全停止主VM,直到备份 VM 已确认来自主 VM 的所有必要信息。

+

+

作为一个例子,我们在图2中展示了 FT 协议的需求。该图显示了一个主VM和备份VM上的事件时间线。从主线到备份线的箭头表示日志条目的传输,从备份线路到主线路的箭头表示确认。有关异步事件、输入和输出操作的信息必须作为日志条目发送到备份VM并确认。如图所示,到外部世界的输出被延迟,直到主VM收到来自备份 VM 的确认,它已经收到与输出操作相关的日志条目。鉴于遵循输出规则,备份VM将能够以这样一种状态接管,即与主VM最后的输出一致。

+

我们不能保证一旦出现故障转移情况,所有输出都准确地产生一次。当主VM打算发送输出时, 没有使用两阶段提交事务 ,备份VM无法确定主VM是在发送它的最后一个输出之前还是之后立即崩溃。 幸运的是,网络基础设施(包括常用的TCP)旨在处理丢失的数据包和相同(重复)的数据包 。 请注意传入到主VM的数据包也可能在其故障的期间丢失,因此不会被传递给备份VM。 但是,传入的数据包可能会由于与服务器故障无关的任何原因被丢弃,因此网络基础设施、操作系统和应用程序都被写入,以确保他们可以弥补丢失的数据包。

+

2.3 检测与故障响应

+

如上所述,如果另一个 VM 出现故障,主备VMs必须快速响应。如果备份VM出现故障,主VM将上线,即离开记录模式(因此停止发送条目到日志通道)并开始正常执行。如果主VM失败,备份VM应该同样上线(go live),但过程更为复杂。由于其执行的滞后,备份 VM 可能会有许多它已收到并确认,但尚未消耗的日志条目,因为备份 VM 尚未达到执行的适当点。 备份VM必须继续重放日志条目,直到它消耗了最后一个日志条目 。此时,备份 VM 将停止重放模式并开始作为正常VM执行。本质上备份VM被提升为主VM(现在缺少备份VM)。由于它不再是备份 VM,当操作系统执行输出操作时,新的主VM现在将向外部世界生产输出。在过渡到正常模式期间,可能会有一些特定设备的操作需要允许正确地发送输出。特别是, 出于联网目的,VMware FT 自动在网络上通告新的主VM的MAC 地址,以便物理网络交换机知道新的主 VM 所在的服务器 。此外,新提升的主VM可能需要重做一些磁盘 IO(如第 3.4 节所述)。

+

有许多可能的方法来尝试检测主备VMs的故障。VMware FT在运行容错VMs的服务器之间使用 UDP心跳 ,来检测服务器何时崩溃。此外,VMware FT 监控日志流量,包括从主到备的发送以及从备到主的确认。因为定时器中断,日志流量应该是有规律的,并且永远不会停止。因此,在日志条目或确认流中的中断可能表明VM故障。如果心跳或记录流量已停止超过特定超时时间(大约几秒钟),就可能发生故障了。

+

但是,任何此类故障检测方法都容易受到脑裂(split brain)问题的影响 。如果备份服务器停止接收来自主服务器的心跳,这可能表明主服务器出现故障,或者可能只是意味着所有仍在运行的服务器之间的网络连接丢失。如果备份VM随后上线,而主VM也仍然在运行,对于与VM通信的客户端而言可能会有数据损坏以及其他问题。因此,我们必须确保当检测到故障时,主VM和备份VM只有一个在线。为了避免脑裂问题,我们利用共享存储,来存储VM的虚拟磁盘。 当任一主或备份VM想要上线时,它会在共享存储中执行一个原子性的测试设置操作 。 如果操作成功,VM 被允许上线。 如果操作失败,那么另一个 VM 一定已经上线,所以当前虚拟机实际上停止了自己(“自杀”)。 如果尝试执行此原子操作时,VM 无法访问共享存储,然后它只是等待,直到可以访问。 注意如果由于存储网络上的某些故障而无法访问共享存储时,那么虚拟机可能无法做有用的工作,因为虚拟磁盘在同样的共享存储中,因此,为了解决脑裂问题而使用共享存储不会引入任何额外的不可接受性。( Hades注:使用共享存储这种解决方案本身使得主备又得以通信了,只不过是通过信号量,而非socket。

+

这个设计的一个最终方面是一旦故障发生并且一个VM已经上线,VMware FT自动地通过在另一个主机上开始一个新的备份VM,来恢复备份。虽然这个过程不能覆盖过去大部分的工作,但是对于故障容忍的VM有用,它是基础,需要仔细设计。

+

3. FT的实际执行

+

第二节描述了我们基础的设计以及FT协议。然而,为了创建一个有用的、健壮的以及自动化的系统,有许多其他组件必须设计实现。

+

3.1 启动与重启 FT VMs

+

一个必须被设计的最大的额外组件是这种机制,即 启动一个拥有和主VM状态一样的备份VM 。当故障发生后重启一个备份VM时,这个机制也将变得很有用。因此,这个机制一定可用于一个处于任意状态的正在运行中的主VM。此外,我们希望该机制不会显著地中断主VM的执行,因为这会影响 VM 的任何当前客户端。

+

对于 VMware FT而言,我们调整了VMware vSphere上现有的 VMotion 功能。 VMware VMotion [10] 允许以最少中断的方式,将正在运行的 VM 从一台服务器迁移到另一台服务器,VM的暂停时间通常不到一秒钟。我们创建了一个VMotion的修改形式,可在远程服务器上创建准确的 VM 运行副本,但不会破坏本地服务器的虚拟机。也就是说,我们修改后的 FT VMotion 将VM克隆到远程主机上而不是迁移它。 FT VMotion还设置了一个日志记录通道,并导致源VM作为主VM进入日志记录模式,而目的VM 作为备份进入重放模式。像平常的VMotion一样,FT VMotion 通常会中断主VM的执行不到一秒。因此,启用 FT在正在运行的 VM 上是一个简单的、无中断的操作。

+

启动备份 VM 的另一个方面是选择一个运行它的服务器。容错 VM 在服务器集群中运行,可以访问共享存储,因此所有 VM通常可以运行在集群上的任何服务器中。这种灵活性允许VMware vSphere恢复FT冗余,即使一个或多个服务器失效。 VMware vSphere 实现了一种集群服务,用于维护管理以及资源信息 。 当发生故障并且主VM 现在需要一个新的备份 VM 来重新建立冗余时,主 VM 通知集群服务它需要一个新的备份。 集群服务基于资源利用率以及其他约束,决定运行备份VM最好的服务器,并调用 FT VMotion 以创建新的备份 VM。 结果是 VMware FT通常可以在几分钟内重新建立VM冗余,在一个故障容忍VM的执行上,所有这些都没有任何明显的中断。

+

3.2 管理日志通道

+

+

在管理日志通道上的流量时,有许多有趣的实现细节。在我们的实现中,管理程序为主备 VM 的日志记录条目维持了一个大的 缓冲区 。当主 VM 执行时,它生成日志条目到缓冲区中,类似地,备份VM从它的日志缓冲区中消耗日志条目。主日志缓冲区的内容会被尽快刷新到日志记录通道,这些日志条目一到日志通道,就会被读取到备份的日志缓冲区。备份每次从网络上读取一些日志条目到它的日志缓冲区时,都会发送确认返回给主VM。这些确认允许 VMware FT 确定一个被输入规则延迟的输出何时可以被发送。图3说明了这个过程。

+

如果备份 VM 在需要读取下一个日志条目时,遇到空的日志缓冲区,它将停止执行直到有新的日志条目可用。由于备份 VM 是不与外部通信的,此暂停不会影响任何VM 的客户端。同样地,当主VM需要写入一个日志条目时,如果主VM遇到一个完整的日志缓冲区,它必须停止执行,直到可以刷新日志条目。这种执行的停止是一种自然的流控制机制,当主VM生产日志条目太快了,它会减慢主VM。但是,此暂停可能会影响VM的客户端,因为主 VM 将完全停止并且无响应,直到它可以记录其条目并继续执行。因此,我们的实现必须设计为尽量减少主日志缓冲区填满的可能性。

+

主日志缓冲区可能填满的原因之一是备份 VM 执行速度太慢,因此消耗日志条目太慢。 一般来说,备份VM必须能够以与正在记录执行的主VM大致相同的速度重放执行 。幸运的是,在 VMware 确定性重放中,记录和重放的开销大致相同。然而,如果由于其他VMs,托管备份 VM 的服务器负载很重(因此过度使用资源),备份VM 可能无法获得足够的 CPU 和内存资源,来与主 VM 一样快地执行,尽管备份管理程序的VM调度器已经尽了最大努力。

+

如果日志缓冲区填满,除了避免意外暂停,还有另一个原因是我们不希望滞后变得太大。如果主VM出现故障,备份VM必须通过重放它在上线和开始与外部世界交流之前已经确认的所有日志条目来“赶上”。完成重放的时间基本上是失败点的执行延迟时间,所以 备份上线的时间大约等于故障检测时间加上当前执行时差 。因此,我们不希望执行滞后时间太大(超过一秒),因为这将显著地增加故障转移时间。

+

因此,我们有一个额外的机制减慢主VM,以防止备份 VM 获取太滞后了。在我们的发送和确认日志条目的协议中,我们发送附加信息来确定主备VM之间的实时执行滞后。通常执行滞后小于 100 毫秒。 如果备份 VM 有一个显著的执行滞后(例如,超过 1 秒),VMware FT 通过通知调度程序给它稍微少一点的CPU(最初只是百分之几)来减慢主 VM 。我们使用一个缓慢的反馈循环,这将尝试逐步确定适当的 CPU 限制,将允许主备 VM同步执行。如果备份 VM 继续滞后,我们继续逐步降低主VM的 CPU 限制。反之,如果备份VM赶上,我们逐渐增加主VM的 CPU 限制,直到备份虚拟机恢复轻微的滞后。

+

请注意,主VM的这种减速很少见,通常只在系统处于低压力时发生。第 5 节的所有性能编号包括任何此类放缓的成本。

+

3.3 FT VMs上的操作

+

另一个实际问题是处理各种控制操作,它们可以应用于主 VM 。例如,如果主VM明确关闭电源,备份 VM 也应该停止,而不是尝试上线。 再举一个例子,任何主VM上的资源管理更改(例如增加 CPU 份额)应该 也适用于备份。 对于此类操作,为了影响备份进行合适的操作,特殊的控制条目通过日志通道从主发送到备份。

+

一般来说,VM 上的大部分操作都应该仅在主 VM 上初始化。 VMware FT 然后发送任何必要的控制条目以造成备份VM上适当的更改。 唯一可以独立在主VM和备份VM上完成的操作是 VMotion。 那即,主VM和备份VM可以独立被 VMotioned到其他主机。 请注意,VMware FT 确保两个 VM 都不会移动到另一个 VM 所在的服务器,因为这种场景将不再提供故障容忍。

+

主VM的VMotion增加了比普通VM更多的复杂性,因为备份VM一定会与源主VM失去连接以及在适当的时间重连。备份VM的VMotion有一个相似的问题,但是只增加了一个额外的复杂性。对于一个正常的VMotion而言,我们需要当VMotion上最后的切换发生时,所有的磁盘IO停止(或完成)。对于一个主VM而言,这种停顿是容易应付的,通过等待直到物理IO完成并将这些完成信息发送给VM。然而,对于一个备份VM而言,没有容易的方式来使得所有IO在任何需要的时刻完成,因为备用VM必须重放主VM的执行过程,并在相同的执行点完成IO。主VM可能正运行在一个工作负载上,在正常执行过程中总是有磁盘IO。VMware FT有一个独一无二的方法来解决这个问题。当一个备份VM是在VMotion最后的切换点时,它需要通过日志通道来告知主VM临时停止所有IO。备份VM的IO将自然地被停止在一个单独的执行点,因为它需要重放主VM的停止操作的过程。

+

3.4 磁盘IO的实现问题

+

有许多与磁盘IO相关的微小的实现问题。首先,假设磁盘操作是非阻塞的,因此访问相同磁盘位置的并行、同时执行的磁盘操作将引起非确定性。此外,我们的磁盘 IO 实现使用DMA 直接from/to虚拟机的内存,所以同时访问相同内存页的磁盘操作也可能导致不确定性。我们的解决方案是 经常检测任何此类 IO 竞争 (很少见),以及强制此类竞争磁盘操作在主备VM上按顺序执行。

+

第二,通过 VM 中的应用程序(或操作系统)时,磁盘操作与内存访问也会存在竞争,因为磁盘操作通过 DMA 直接访问 VM 的内存。例如,如果一个VM 中的应用程序/操作系统正在读取内存块,同时对该块进行磁盘读取。( Hades注:这里的意思应该是,该块内存作为DMA操作的目的地址。 )这个情况也不太可能发生,但如果它发生,我们必须检测它并处理它。一种解决方案是临时设置页保护,在作为磁盘操作目标的页面上。如果VM 碰巧访问一个页,同时该页面也是磁盘操作的目标,页保护将导致一个陷阱( Hades注:trap,陷入系统调用 ),VM将暂停直到磁盘操作完成。 因为改变页上的MMU 保护是一项昂贵的操作,所以我们选择使用 弹跳缓冲区(Bounce Buffer) 代替 。bounce buffer是临时缓冲区,与正在被磁盘操作访问的内存大小相同。磁盘读取操作被修改为读取指定数据到bounce buffer,并在在IO完成时将数据复制到内存中。相似地,对于磁盘写操作,首先将要发送的数据复制到bounce buffer,磁盘写入修改为向bounce buffer写入数据。bounce buffer的使用会减慢磁盘操作,但我们还没有看到它会导致任何明显的性能损失。( Hades注:bounce buffer存在的意义是在内存访问这个操作之前加了一个拦截器,其最本质的意义是为了supervisor监控DMA操作,使得数据从bounce buffer拷贝到到内存和系统中断这两个步骤,能够同时在backup VM上被复制, 否则网卡直接将网络数据包DMA到Primary虚机中这个操作是无法通过log channel进行复制的

+

第三,有一些与故障发生并且备份VM接管时,主VM未完成的磁盘 IO 相关的问题。对于新上线的主VM,没有办法确定磁盘IO是有问题的还是成功完成了。另外,由于磁盘IO没有从外部发布到备用VM上,而是通过主备传递,因此对于继续运行的新上任的主VM来说,将没有明确的IO完成信息,最终将导致VM上的操作系统开始中止或者重调度程序。我们能够发送一个错误完成,表示每个IO失败,因为即使IO成功完成了,它可以接受返回一个错误。然而,操作系统可能不能对这些来自本地磁盘的错误有很好的响应。反之,我们在备份VM上线的过程中,重新发送这些悬挂着的IO。因为我们已经限制了所有的竞争和所有的直接指定内存和磁盘的IO,这些磁盘操作可以被重新发送,即使它们已经成功完成了(即他们是幂等的)。

+

3.5 网络IO的实现问题

+

VMware vSphere针对VM网络提供了很多性能优化。一些优化是基于管理程序(supervisor) 异步更新虚拟机的网络设备状态 。例如,当VM正在执行时,接收缓冲区可以由管理程序直接更新。不幸的是这些对 VM 状态的 异步更新会增加不确定性 。除非我们可以保证所有更新都发生在主备指令流上的同一点,否则备份VM的执行可能与主VM的执行不同。

+

对于FT而言,网络仿真代码的最大变化是禁用异步网络优化。异步更新带有传入数据包的VM环形缓冲区的代码已被修改,以强制管理程序捕获到操作系统,它可以在其中记录更新然后将它们应用到 VM。同样,异步地将数据包从传输队列中拉出也被修改了,取而代之的是通过管理程序traps来完成传输(如下所述)。

+

网络设备异步更新的消除结合第 2.2 节中描述的发送数据包的延迟带来了一些网络性能的挑战。我们采取了两种方法在运行 FT 时提高 VM 的网络性能。第一,我们实施了集群优化以减少 VM 的陷阱和中断。当 VM 以足够的比特率流式传输数据时,管理程序可以对每组数据包做一个传输trap,在最好的情况下零trap,因为它可以传输所接收新数据包的一部分数据包。同样地,通过仅对于一组数据包发布中断,管理程序可以将接收包的中断数量减少。

+

我们对网络的第二个性能优化涉及 减少传输数据包的延迟 。如前所述,管理程序必须延迟所有发送的包直到它得到备份VM对于某些日志条目的确认。减少发送延迟的关键在于减少发送/接收备份VM信息的所需时间。我们的主要优化包括 保证收发信息在无需任何线程上下文切换的情形下就可以被执行 。VMware vSphere管理程序允许函数被注册到TCP栈中,只要TCP数据被接收到了,函数就会被一个延期执行的上下文调用(和Linux中的tasklet类似)。这允许我们快速处理备份VM上任何即将到来的日志消息,以及主VM接收的任何确认消息,而不需要任何线程上下文的切换。另外,当主VM有一个包要寄出去时,我们强制一次相关输出日志条目的日志刷出(正如2.2节中所描述的),通过调度一个延迟执行的上下文来执行这次刷出。

+

4. 替代设计

+

在我们VMware FT的实现中,我们已经探索了许多有趣的替代设计。在这节中,我们探索一些替代设计。

+

4.1 共享 vs. 非共享磁盘

+

在我们默认的设计中,主备VM共享相同的虚拟磁盘。因此,如果一次故障转移发生,共享磁盘的内容自然是正确、可接受的。必要地,对于主备VM来说,共享磁盘被认为是外部的,因此任何共享磁盘的写入被认为是一次与外部世界的沟通。因此,只有主VM做这种实际的磁盘写入,并且为了遵循输出规则,这种写入必须被延迟。

+

+

对于主备VM而言,一种可替代的选择是分隔的虚拟磁盘。在这种设计中,备份VM要执行所有虚拟磁盘的写入操作。而且这样做的话自然要保持它的虚拟磁盘内容与主VM虚拟磁盘内容一致。图4阐述了这种配置。在非共享磁盘的情况下,虚拟磁盘必须被认为是每个VM的内部状态。因此,依据输出规则, 主VM的磁盘写入不必延迟 。在共享存储不能被主备VM接受的情况下,非共享的设计是相当有用的。这种情况可能是由于共享存储不可接受或者太昂贵,或者由于运行主备VM的服务器相隔太远(“长距离FT”)。非共享设计的一个缺点是在首次启动故障容错时,虚拟磁盘的两个复制必须以相同的方式进行显示同步。另外,发生故障后磁盘 可能会不同步 ,因此当在一次失败后备份VM重启的时候,他们必须再显式地同步。FT VMotion必须不止同步主备VM的运行状态,还要同步他们的磁盘状态。

+

在这种非共享磁盘的配置中,他们也能应付脑裂场景。在这种场景中,系统能够 使用一些其他的外部决策者 ,例如所有服务器可以沟通的一个第三方服务。如果服务器是超过两个节点的集群的一部分,这个系统能够基于集群关系使用一种majority算法。在这个例子中,一个VM能够被允许上线,如果它正在一个服务器上运行,这个服务器是包含大多数原始节点的正在通信的子集群的一部分。

+

4.2 在备份VM上执行磁盘读

+

在我们默认的设计中,备份的VM从不会从它自己的虚拟磁盘上读取(无论共享还是非共享)。 因为磁盘读取被认为是一个输入 ,它是自然地通过日志通道将磁盘读取的结果发送到备份VM上。

+

一种替代的设计是 让备份VM执行磁盘读取 ,因此消除了磁盘读取的日志。对于大多数时候都做磁盘读取的工作负载而言,这种方法可以很好地降低日志通道上的流量。然而,这种方法有很多小问题。它可能会减慢备份VM的执行速度,因为备份VM必须执行所有的磁盘读取,当到达VM执行中主VM已经完成的位置时,如果备份上的磁盘读取还没完成就必须等待。

+

同样地, 为了处理失败的磁盘读取操作,必须做一些额外的工作 。如果一个主VM的磁盘读取成功了,但是相应的备份VM磁盘读取失败了,备份VM的磁盘读取必须重试直到成功。因为备份VM必须获得和主VM一样的数据到内存中。相反地,如果一个主VM的磁盘读取失败了,目标内存的内容必须通过日志通道发送给备份服务器,因此内存的内容将被破坏,不能被备份VM成功的磁盘读取复制。

+

最后,如果这种磁盘读取被用于共享磁盘配置的话,还有一个小问题。如果主VM做了一次对具体磁盘位置的读取,然后紧跟相同磁盘位置的写入,然后这个磁盘写必须被延迟到备份VM已经执行了第一次磁盘读取。这种依赖可以被检测和正确处理,但是需要增加实现上额外的复杂性。

+

在5.1节中,对于实际的应用而言,我们给出一些性能结果以表示在备份VM上执行磁盘读取会造成一些轻微的吞吐量减少(1-4%),因此在日志通道的带宽被限制的情况下,在备份VM上执行磁盘读取可能是有用的。

+

5. 性能评估

+

在这节中,我们做了一次VMware FT性能的基础评估,针对许多应用负载以及网络基准。为了得到这些结果,我们在一样的服务器上运行主备VM,每个都带9个Intel Xeon 2.8Ghz CPUs and 8Gbytes of RAM。服务器间通过10 Gbit/s的交换机连接,但是在所有的例子中都能看到被使用的网络带宽远远少于1Gbit/s。从一个通过标准的4Gbit/s的光纤通道网络连接的EMC Clariion中,服务器可以连接他们的共享虚拟磁盘。客户端通过1 Gbit/s的网络来驱动一些连接服务器的工作负载。

+

我们评估性能结果的应用如下所示。SPECJbb2005是工业标准的Java应用基准,非常耗费CPU和内存,但是IO非常少。Kernel Compile是一种运行Linux核编译的工作负载。由于许多编译过程的创建和毁灭,这个工作负载做很多磁盘读取和写入,是非常耗费CPU和MMU的。Oracle Swingbench是被Swingbench OLTP工作负载(在线事务处理)驱动的一个Oracle 11g的数据库。这个工作负载做连续的磁盘和网络IO,有80个同时在线的数据库会话。MS-SQL DVD Store是一种工作负载,运行了一个Microsoft SQL Server 2005的数据库,有60个同时在线的客户端。

+

5.1 基本性能结果(Basic Performance Results)

+

+

表 1 列出了基本的性能结果。对于每个应用程序,第二列给出了应用程序的性能比例,运行服务器工作负载的虚拟机上启用和未启用FT的情况。性能比小于 1 表示带FT的工作负载更慢。显然,这些有代表性的工作负载上启用FT 的开销小于10%。 SPECJbb2005 完全受计算限制,没有空闲时间,但其表现性能良好,因为它具有最小的除定时器中断以外的不确定性事件。另一个工作负载做磁盘 IO 有一些空闲时间,所以一些FT 开销可能被 FT虚拟机的空闲时间更少的真实情况隐藏。然而,一般的结论是VMware FT 能够支持故障容忍VM,并且具备相当低的性能开销。

+

在表的第三列中,我们给出了当应用程序正在运行时,在日志通道上发送数据的平均带宽。对于这些应用程序,日志带宽相当合理,1 Gbit/s的网络就能满足 。事实上,低带宽要求表明多个 FT 工作负载可以共享相同的 1 Gbit/s网络,同时没有任何负面的性能影响。

+

对于运行常见操作系统的 VM,例如Linux 和 Windows,我们发现当操作系统空闲时,通常的日志记录带宽为 0.5-1.5 Mbits/sec。"空闲"带宽主要是记录定时器中断发送的结果。对于具有活动中工作负载的 VM而言,日志带宽由网络和必须发送到备份的磁盘输入主导—网络收到的数据包和从磁盘读取的磁盘块。因此,对于非常高的网络接收或者磁盘读取带宽的应用而言,日志带宽高于表1中的测量值。对于这类应用而言,日志通道的带宽可能是瓶颈,特别是日志通道还有其他使用时。

+

对于许多实际应用程序而言, 日志记录所需的带宽相对较低,这使得基于重放的故障容忍对于使用非共享磁盘的长距离配置非常有吸引力 。对于远距离配置而言,其主备VM可能相隔1-100公里,光纤可以轻松地支持延迟小于 10 毫秒的100-1000 Mbit/s带宽。对于表 1 中的应用而言,主备之间的额外往返延迟,可能会导致网络和磁盘输出最多延迟 20 毫秒。远距离配置仅适用于这类应用程序:他的客户端可以容忍每个请求的额外延迟。

+

对于两个最占用磁盘空间的应用程序,我们测量了在备份 VM上执行磁盘读取(如第 4.2 节所述)与通过日志记录通道发送磁盘读取数据相比,对于性能的影响。对于 Oracle Swingbench来说,在备份VM上执行磁盘读取时的吞吐量降低约 4%;对于 MS-SQL DVD 存储,吞吐量约降低 1%。同时,Oracle Swingbench的日志带宽从 12 Mbits/sec 降低到 3 Mbits/sec,MS-SQL DVD 存储从 18 Mbits/sec 降低到 8 Mbits/sec。显然,对于具有更大磁盘读取带宽的应用程序,带宽可能会节省很多。如第 4.2 节所述,预计在备份 VM 上执行磁盘读取时,性能可能会更差。但是,对于日志通道的带宽是有限的(例如,远程配置)情况下,在备份 VM 上执行磁盘读取可能有用。

+

5.2 网络基准测试(Network Benchmarks)

+

出于多种原因。网络基准测试对我们的系统来说非常具有挑战性。第一,高速网络会有一个非常高的中断率,这需要以非常高的速度记录和重放异步事件。 第二,以高速率接收数据包的基准将导致高速率的日志流量,因为所有这些数据包必须通过日志通道发送到备份。第三,发送数据包的基准测试将受制于输出规则,延迟网络数据包的发送直到已收到来自备份VM的确认。 此延迟会增加对客户端测量的延迟。这种延迟还可能会降低到客户端的网络带宽,因为网络协议(如 TCP)由于往返延迟增加,可能不得不降低网络传输速率。

+

+

表 2 给出了我们通过标准的netperf 基准测试,多次测量的结果。在所有这些测量中,客户端 VM 和主 VM 通过 1 Gbit/s 网络连接。前两行给出了主备主机间通过1 Gbit/s 的日志通道连接时,发送和接收的性能。第三行和第四行给出当主备服务器通过10 Gbit/s的日志通道连接时,发送和接收的性能,不仅带宽更高,延迟也低于 1 Gbit/s。作为一个粗略的测量,在1 Gbit/s 网络连接的管理程序之间, ping 时间约为 150 微秒,而对于 10 Gbit/s 连接,ping时间大约需要 90 微秒。

+

未启用 FT 时,主 VM 对于接收和发送,可以实现接近 (940 Mbit/s) 1 Gbit/s 的线路传输速率。当为接收工作负载启用 FT 时,日志记录带宽非常大,因为所有传入的网络数据包必须在日志通道上发送。因此,日志记录通道可能成为瓶颈,正如1 Gbit/s 日志网络的结果。对于 10 Gbit/s 的日志网络,影响则小了很多。当为上传工作负载启用 FT 时,上传数据包的数据不会记录,但仍必须记录网络中断。日志带宽要低得多,因此可实现的网络上传带宽高于网络接收带宽。 总的来说,我们看到 FT 在非常高的上传和接收速率情况下,可以显著地限制网络带宽,但仍然可以实现很高的速率

+

7. 结论与今后的工作

+

我们在VMware vSphere 中设计并实施了一个高效完整的系统(FT) ,用于为服务器上运行的虚拟机提供容错。我们的设计基于复制主VM中的执行,再通过另一台主机上的备份VM执行VMware确定性重放。如果运行主 VM的服务器出现故障,备份 VM 能立即接管且不会中断或丢失数据。

+

总体而言,在商业硬件上运行VMware FT时,故障容错VM的性能非常出色,并且对于某些典型应用程序,其开销低于 10%。大多数 VMware FT 的性能成本来自于使用 VMware 确定性重放来保持主备VM同步。因此,VMware FT 的低开销源自 VMware 确定性重放的效率。此外,保持主备同步所需的日志带宽非常小,通常小于 20 Mbit/s。因为日志带宽在大多数情况下很小,主备相隔很长的距离(1-100公里)似乎也是可行的实施配置。因此,VMware FT 可用于这种场景:可以防止整个站点发生故障的灾难。值得注意的是,日志流通常是可压缩的,因此简单的压缩技术可以显著地减少日志带宽,虽然有少量额外的 CPU 开销。

+

我们对 VMware FT 的结果表明, 一个高效的故障容错VM的实现可以建立在确定性重放的基础上这样的系统可以透明地为运行任何操作系统和应用的虚拟机提供容错能力,仅会带来极小的开销 。然而,对客户有用的故障容错VM系统而言,它必须还具有强大、易于使用和高度自动化的特点。一个可用的系统除了复制虚拟机执行之外,还需要许多其他组件。特别是VMware FT 故障后自动地恢复冗余,通过在本地集群中找到合适的服务器并在其上创建一个新的备份VM。通过解决所有必要的问题,我们已经展示了一个在客户的数据中心可用于实际应用的系统。

+

通过确定性重放实现容错的权衡之一是当前确定性重放仅针对单处理器VM 。然而,单处理器虚拟机足够应付各种各样的工作负载,特别是因为物理处理器不断变得更加强大。此外,许多工作负载可以通过使用许多单处理器的虚拟机来扩展,而不是通过使用一个更大的多处理器虚拟机来扩展。多处理器 VM 的高性能重放是一种活跃的研究领域,并且可以潜在地被微处理器中的一些额外硬件支持。一个有趣的方向可能是扩展事务内存模型以促进多处理器重放。

+

将来,我们也有兴趣扩展我们的系统处理部分硬件故障。通过部分硬件故障,我们的意思是服务器上功能或冗余的部分丢失,不会导致损坏或丢失数据。一个例子是到 VM所有网络连接的丢失,或在物理服务器中备用电源丢失。如果在运行主 VM 的服务器上发生部分硬件故障,在许多情况下(但不是all) 故障转移到备份 VM 将是有利的。这样的故障转移对于关键VM而言,可以立即恢复完整服务,并确保虚拟机从可能不可靠的服务器上快速地移走。

+

LEC 4

+

故障

+

我们希望复制方案可以处理的故障:

+
    +
  1. 只处理Fail-Stop类型的故障,也就是基础设施的故障导致计算机不能正常运行的类型的失败。因此失败是一瞬间发生的,这样的失败也不会产生一些奇怪的结果。 +
      +
    1. 排除了逻辑错误(也就是代码错误)
    2. +
    3. 排除了配置错误
    4. +
    5. 排除了恶意错误(不能处理黑客、攻击者模拟出来的错误等)
    6. +
    +
  2. +
  3. 可能处理的:比如地震等,但是我们不关注,因为主从机器都在一个机房中
  4. +
+

挑战

+

如果发生了故障,主机器真的挂掉了吗?

+

在分布式系统中,没有办法区分网络分区和机器故障的区别,因此很有可能主机器并没有挂掉,有一些客户端还能访问主机器,但是从机器和主机器之间的网络有问题,无法互相访问到,所以从机器认为主机器已经挂掉了。因此不能有两个主机器同时存在的情况,也就是脑裂问题。

+

如何保持主从同步?

+

如果主机器挂了,从机器要从主机器挂掉的地方直接开始,这就意味着从机器的状态与主机器的状态相同,都是最新的。从客户端的角度感知不到这种变化。

+

非常困难:

+
    +
  • 我们在主机器上的所有改变都要按照相同的顺序应用到从机器上
  • +
  • 解决非确定性问题,也就是相同的更改在两台机器上作的改变必须相同
  • +
  • 故障转移:要弄明白主机器在挂掉之前有没有发送过数据包,再发送一次是否可行(或者是如果所有机器都挂掉了,回来之后哪个机器上有最新的状态呢?)
  • +
+

两种主从复制方法

+
    +
  • 状态转移:客户端与主机器进行交互,主机器更新状态,每隔一段时间有一个检查点,将状态传给从机器。因此一旦主机器有了状态的改变,这个状态就要马上传递给从机器。
  • +
  • 状态机复制:不发送状态给从机器,而是将对主机器进行更改的操作发送给从机器。
  • +
+

两种方法都是目前流行的方法,状态转移的缺点是如果一个操作生成了很多状态,这个传输的数据量非常大,因此如果只发送操作过去就很轻松。

+

复制操作的级别

+

应用级别:文件追加写入,需要在应用程序上进行修改

+

机器级别:寄存器指令级别的复制,只有x86指令,不涉及应用程序方面的更改,可以使用虚拟机实现,从而不用再硬件级别上实现。

+

VM-FT

+

利用虚拟化技术,使得复制操作对应用程序是透明的,应用程序认为仅有一台服务器,并且也同时提供了很强的一致性。

+

概览

+

虚拟机监控器(hypervisor):在实际硬件上运行,虚拟出多个虚拟的硬件

+

任何我们看到的外部事件实际上都经过了hypervisor,例如一个外部中断,hypervisor会先观察到并决定什么时候传递给虚拟机

+

多个hypervisor之间通过logging channel进行通信,从而进行操作的精确复制

+

pSnSrQO.md.png

+

storage server可以对谁当主机器进行仲裁

+

如果主机器和从机器不能相互通信,但是都能看到storage server,两台机器都会进行test-and-set操作,比较早的那一个就会成为主机器。

+

设计

+

目标:多台虚拟机对外表现为单一的机器

+

问题:差异来源导致两台机器表现不一样

+

非确定性指令:

+
    +
  • 获取时间的指令
  • +
  • 数据的输入顺序需要相同
  • +
  • 中断指令的顺序需要相同
  • +
  • 多核——这篇论文中不允许
  • +
+

中断

+

确定性指令不需要通过logging channel进行通信

+

中断发生后,会传递给从机器中断发生的前一个指令号,但是从机器并不会马上去执行,而是缓存下来,等到下一条中断指令传递过来之后,再执行前一条指令。这样会落后一条指令

+

非确定性指令

+

在机器启动之前会遍历全部的指令,确保浏览到全部的非确定性指令,不会直接执行,而会交给hypervisor进行控制。hypervisor执行的时候会额外记录下这些指令操作后的对应结果。传递的时候会同时对结果进行传递,这样从机器不需要真正去执行,直接修改结果就可以。

+

性能

+

指令级别的复制会付出性能的代价

+

论文的实验表明带宽会降低大概30%左右,由于主机器接收来自客户端的输入,然后传递给从机器,这个过程中主机器必须等待,才能将响应传递给客户端。

+

因此状态机复制的方法并不常用的原因之一是性能会下降。

+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-LEC 4 Primary-Backup Replication
+
https://zhangzhao219.github.io/2022/12/18/6.824/Distributed-Systems-MIT-6.824-LEC-4/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月18日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/26/Interview/Interview-Questions-os/index.html b/2022/12/26/Interview/Interview-Questions-os/index.html new file mode 100644 index 000000000..eef35d43b --- /dev/null +++ b/2022/12/26/Interview/Interview-Questions-os/index.html @@ -0,0 +1,791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 操作系统面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

操作系统面试题准备

+ + +
+ +

操作系统面试题准备

+ +

基本特征

+

并发

+

并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。

+

并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。

+

操作系统通过引入进程和线程,使得程序能够并发运行。

+

共享

+

共享是指系统中的资源可以被多个并发进程共同使用。

+

有两种共享方式:互斥共享和同时共享。

+

互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。

+

虚拟

+

虚拟技术把一个物理实体转换为多个逻辑实体。

+

主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。

+

多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换。

+

虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。

+

异步

+

异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。

+

基本功能

+
    +
  • 进程管理:进程控制、进程同步、进程通信、死锁处理、处理机调度等。
  • +
  • 内存管理:内存分配、地址映射、内存保护与共享、虚拟内存等。
  • +
  • 文件管理:文件存储空间的管理、目录管理、文件读写管理和保护等。
  • +
  • 设备管理:完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。主要包括缓冲管理、设备分配、设备处理、虛拟设备等。
  • +
+

宏内核和微内核

+

宏内核

+

宏内核是将操作系统功能作为一个紧密结合的整体放到内核。

+

由于各模块共享信息,因此有很高的性能。

+

微内核

+

由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。

+

在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。

+

因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。

+

中断

+

外中断:由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

+

异常:由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。

+

陷入:在用户程序中使用系统调用。

+

用户态、系统调用、内核态

+

用户态

+

当进程执行用户自己的代码时,则该进程处于用户态。用户态运行的进程可以直接读取用户程序的数据,但是,这时cpu访问资源受限。

+

内核态

+

内核态(kernel mode):当进程执行系统内核代码时,则该进程处于内核态。系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制,这时cpu可以访问计算机的所有资源

+

用户态与内核态的转换

+

系统调用

+

用户态进程通过系统调用申请使用系统态级别的资源,并由操作系统程序代为完成。

+

在运行的用户态程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成*。

+

外围设备的中断

+

当外围设备接收到用户请求后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令,转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如需要进行硬盘读写操作时,系统会切换到硬盘读写的中断处理程序

+

异常

+

当CPU在执行用户态程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

+

死锁的必要条件(2022.12.26 小红书 Golang开发实习生)

+
    +
  • 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
  • +
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
  • +
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  • +
  • 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
  • +
+ + +
+ +
+
+ + + + + + +
+
+
操作系统面试题准备
+
https://zhangzhao219.github.io/2022/12/26/Interview/Interview-Questions-os/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月26日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/27/UCAS/exam-final-summary/index.html b/2022/12/27/UCAS/exam-final-summary/index.html new file mode 100644 index 000000000..01fd1ba6c --- /dev/null +++ b/2022/12/27/UCAS/exam-final-summary/index.html @@ -0,0 +1,883 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 研一上学期闭卷三科目考试重点 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

研一上学期闭卷三科目考试重点

+ + +
+ +

研一上学期闭卷三科目考试重点

+ +

模式识别与机器学习

+

25道选择题,5道大题(简答题与计算题结合在一起)

+

2022.12.27 二班苏荔老师

+

非监督学习:不考死记硬背的概念,看一下作业题的例子,比如给一些具体样本分布的图,如果使用KMeans聚类会怎么样,Kmeans的判别界面应该是一个中垂线,两个簇的中垂线,如果不是大小均匀的球体可能KMeans就不适合,如果是有疏有密的,一个椭球形的,是不是考虑基于混合高斯的模型,或者簇是一个不规则的形状,比如是字母S或者C型,甚至有的簇是一个同心圆的,最适合的聚类算法应该是什么样的,可以考虑一下基于密度的聚类是不是可以,基于密度的聚类有没有不适合的情况,包括基于层次聚类会遇到什么问题,结合一些实际的例子请大家分析一下。

+

降维:PCA每年都考,可能和前面讲K-L变换的知识结合起来,比如PCA为什么要先做零均值的平移,特征值的选择,为什么先做倒序的排序,选排在前面的特征值最大的几个特征值对应的特征向量来构成对应的变换核,基本原理希望掌握。后面的非线性降维,核PCA的主要思想是什么,先升维再降维,基于流形的非线性降维方法,说出一些经典方法的名字就行,不可能考太细。(前面讲了一些降维,这里不可能考大题,可以看看Fisher线性判别怎么做降维,选择最佳投影方向等等)

+

半监督学习:半监督学习是基于一些假设的,最基本的假设有哪些,不同的半监督算法各自基于什么假设?比如半监督的SVM应该是基于低密度分割的假设,半监督SVM里面有一些比较关键的参数,C1,C2等等,分别表达什么意思,C2很小表达对未标注样本错分的容忍度比较大,很大表示不容忍错分,每一个未标注样本也要分对,但是所有的样本都必须要严格分对这种也可能有问题,过拟合等等,过拟合怎么办呢?可以把模型参数调简单点,怎么让模型参数调简单?不同的模型方法不一样,比如半监督SVM就可以把C2调小一点,或者把C1调小也行,如果是神经网络可以让结构简单一点,层数和节点变少参数就变少,欠拟合怎么办,如何让模型变复杂,除了从模型参数上调,增加数据样本是不是一定有用?

+

概率图模型每年都会有大题,要么是考贝叶斯球判断条件独立性,或者大题问维特比解码,一般只考前向的推导。

+

集成学习只考经典的bagging和boosting两种,其他的不考,考的时候从基本原理出发,比如bagging的基本原理是降低方差,但是不能改变偏差,boosting主要是降低偏差,考的时候可能会给实际的例子去判断哪种更适用,比如模型的单个基学习器都是偏差比较高的情况,要把多个集成起来增加学习能力,到底是用boosting还是bagging,如果选择boosting,不同的基学习器的权重是完全一样的吗,谁的权重大,或者是boosting作迭代训练的时候,训练样本每一次的权重在迭代的时候是不是都要改变,为什么要让分错的样本权重更大,分对的样本下一次权重就调小,要知道这些基本的调节权重的原则。

+

人工神经网络只考基本原理和卷积神经网络,可能让你设计一个人工神经网络的结构,和前面几章的内容有结合,比如线性分类器,非线性分类器,两类分类的,多类分类的,都怎么来设计,能画出大概的结构,写出能用什么(激励?)函数就可以。卷积神经网络主要是考察基本原理,比如为什么卷积神经网络能够进一步降低(深度,参数量?)但是又不会降低性能。可能是基于局部连接和参数重用,池化等等技术,这几个技术的基本的动机是什么,是因为有一些很有判别力的局部模式,他是之前局部出现,并且能够在不同的位置出现,还有有的时候下采样也不会影响这种局部模式的识别。可能会问一些深度神经网络训练的时候遇到一些问题怎么解决,比如说层数很深的时候,会遇到梯度消失、梯度爆炸的问题,遇到问题怎么办呢,激活函数从sigmoid换成relu为什么这样能解决这个问题,比如说使用batch normalization也可以缓解梯度消失的问题,甚至还能带来一些正则化的效果,或者是残差网络的技术,将前层的信息引入进来,可能还带有一些集成学习的思想,把这些基本的原理说清楚。人工神经网络的训练可能也会遇到过拟合,模型的参数可能就过于复杂,除了简化模型的结构之外,还有什么其他的技术,比如是不是可以考虑添加正则化项,正则化也分为L1和L2,L1正则能让系数=0也能做特征选择,L2可以让参数趋向于变小,对整体的影响就变小了,相当于参数变简单了,也能防止过拟合,包括做数据增强,增加训练样本集尽可能让他多样化,也是可以增加模型的泛化能力,还有做梯度下降的时候收敛速度变慢怎么办,陷入局部极值点怎么办,一般是这种实际一些的问题

+

2022.12.28 三班卿来云老师

+

不考:

+

势函数、决策树、贝叶斯参数估计、后向算法、马尔科夫随机场、SMO、拉普拉斯平滑

+

没有证明题,但是会有一点公式推导

+

EM算法不作为重点考察,最多知道概念是拿来干什么的就行,是对含有隐含变量的模型做优化,不会考很细节的

+

零碎考点:

+

判别器有讲过贝叶斯风险,是每年都会考的,感知器算法、朴素贝叶斯、Fisher判别分析,都有可能考到的,K-L变换作特征提取或者后面的PCA,还有像LR,SVM,还有线性回归,另外就是机器学习一般的概念,过拟合、欠拟合应该怎么办,怎么判断模型处于哪种状态,正则的概念,可能放到多种模型中,都要能理解。

+

降维需要掌握的知识点:PCA是怎么做的,Fisher判别分析要会

+

Fisher判别分析是要考的但是不考计算,除了两类也要看一下多类的

+

深度学习有一道大题,偏简答性质的

+

HMM有计算题,判断独立性是选择题

+

概率图掌握贝叶斯球就可以,概率的分布表示还是要的

+

多类LR不需要特别掌握,知道有这回事就行,比如用softmax函数做最后一层就可以

+

多类分类问题在SVM或者LR可以转化为两两的分类问题,算术题,转成多少个需要知道

+

支持向量考的挺多的,给一个图,哪些点是支持向量,或者自己画图

+

偏差方差分解具体的证明不考,但是要知道泛化误差是三部分,会判断模型什么时候偏差比较大,什么时候方差比较大,应该怎么办

+

高斯判别分析今年没有大题,Fisher判别分析多看看

+

SVM软间隔硬间隔都会有

+

一般对线性回归、LR、SVM公式推导方面严一点,比如损失函数是怎么推导来的,极大似然估计是怎么回事,MAP估计是怎么样,这些基本概念需要掌握,SVM的模型推导可能少一些。SVM更多理解margin、支持向量、不同的C的影响等等、核函数,RBF核模型复杂度的参数是核函数的宽度,多项式核模型复杂度还有多项式的项数。

+

类内类间散度矩阵应该怎么算是要求掌握的,与Fisher是什么关系,但是不会考察具体的数值计算

+

多类感知器不考,感知器是有的

+

PCA没有计算,需要知道PCA的计算过程,另外目的和达到的效果也需要知道。首先要减均值,然后再算协方差矩阵作矩阵分解,或者是作SVD分解都可以,测试的过程也一样,目的是去掉各个维度之间的相关性

+

bagging知道功效和怎么做就行,是多个模型做平均,目的是偏差不变,降低模型的方差,boosting是降低模型的偏差,方差不变

+

聚类:比如像K均值,GMM,DBSCAN这三类聚类方法需要掌握他的特点,我们什么时候用哪种方法来做,选择题

+

降维:今年考的不是很多,稍微看一看就行

+

半监督学习:不会考的特别细,但是半监督学习是基于一些假设的,最基本的假设有哪些,不同的半监督算法各自基于什么假设?

+

概率图模型一方面考对于有向图模型,判断给定观测到某些节点的情况下,这个变量和那个变量是否独立,可以通过贝叶斯球的方式做,需要记忆贝叶斯球的规则,根据一个图模型把联合分布写出来,HMM要求掌握前向算法求观测概率和利用维特比算法求给定一个观测后最可能的隐含状态的序列

+

集成学习主要是bagging和boosting,要知道bagging的原理是通过对训练样本进行bootstrap采样,然后训练多个模型,最后对多个模型作平均,得到最后的融合模型。它的好处是可以降低模型的方差,偏差不变。boosting对于Adaboost的过程,比如一开始初始化的时候每个样本的权重相同,经过一轮迭代之后哪些样本的权重会增加,哪些样本的权重会减小,最后模型是怎么融合的,没有计算题,知道Adabooost的过程就可以。对于boosting一般的原理也要知道,每次迭代顺序的把一些模型加进去,最后一些子模型的加权平均是我们最后的集成模型,boosting的好处是降低模型的偏差,方差不变。

+

深度学习:知道神经元的结构(线性组合+激活函数),一个神经元可以起到一个什么样的作用,神经网络是把神经元组合起来,卷积神经网络为什么要局部连接,为什么要权重共享,这样做的基于对数据的假设是什么,局部连接我们认为是一个模式在一个比较小的范围内,而不是要看全局,权值共享是说不同位置可能都有这样的模式,这样做可以使得模型的参数变少,另外多层感知器很多层并不能带来性能提升,现在的模型采用哪些技术使得训练深度模型成为可能?比如说激活函数,sigmoid容易梯度消失,使用relu使得梯度消失的问题会减弱,这样网络层数可以更深,另外batch normalization会使得我们的训练会和好多因素(学习率、初始化)的要求没有那么高,这样也是一种技术,另外采用预训练网络作初始化也是一种方式,或者核初始化等等,也可以让模型的层数更深。另外Resnet,或者叫skip-connect,通过跳接的方式使得梯度消失的问题能减弱,使得模型可以很深很深甚至上千层。神经网络设计的时候也讲了一些其他的技术。不要求全部掌握,至少知道几点。为什么要用mini-batch的梯度下降,随机的梯度下降,有什么样的好处或者特点等等。

+

2022.12.31

+

第一章:模式识别和机器学习我们并不是很区分它们,可以看成一个问题的两个方面

+

第二章:统计判别,主要是讲了错误率最小,错误率最小对应到分类问题等价于后验概率最大,后验概率怎么算需要大家一定掌握,后面也把风险带进来

+

第三章:判别函数,作判别的时候一种方式可以使用生成式分类器,高斯分布的贝叶斯分类器采用的实际上是生成式分类器,指的是我们的联合分布可以由先验和似然相乘得到,有了联合分布可以从联合分布进行采样从而得到新的数据,也就是我们知道数据的产生过程,因此叫做生成式分类器。朴素贝叶斯,高斯判别分析,概率图模型,HMM都属于生成式分类器。好处是因为我们对数据的产生过程有比较强的假设,如果我们的数据符合这个假设,通常用很少量的数据就能得到很好的模型,或者说收敛的比较快,缺点是如果数据不符合这个假设,模型的性能就很不好。另外是判别式分类器,就是直接对后验概率进行建模,或者是定义一个判别函数,根据判别函数的值判断是哪一个类别。像逻辑斯蒂回归,决策树,SVM,深度学习都是判别式的。

+

线性判别函数如何将多类分类任务转化成两类分类任务或者是直接用多类分类任务去做。线性判别函数处理的都是线性可分的问题,如果不是线性可分的需要用广义的线性判别函数,或者是分段线性判别函数实现。模式空间和权空间只是一些基本概念。重点掌握Fisher线性判别,协方差矩阵、类内类间,向量等都是怎么定义的,Fisher线性判别的准则是什么,都需要掌握,两类的情况,多类的情况,大家都去想想,感知器算法需要重点掌握,它是线性判别函数,只能在线性可分的时候可以使用,感知器多类也需要大家掌握。

+

第四章:特征选择是很重要的内容,但是不作为重点考察。特征提取重点掌握K-L变换,比较简单,实际也比较有用,PCA实际上和K-L变换就是一回事。需要知道K-L变换的目的和过程,做了K-L变换之后能达到一个什么样的效果。

+

第五章:期望风险:在没有见过的数据上的误差,也就是期望风险。结构风险:正则项,L1正则和L2正则,泛化误差的分解,证明过程不要求,结论要知道,用VC维计算泛化误差的上界,基本概念比较多

+

第六章:线性回归的损失函数,目标函数包括两部分,L1损失或者L2损失,负log似然损失,需要掌握似然是怎么定义的,由负log似然损失怎么推到了L2损失,L2损失的问题是对噪声比较敏感,因为是平方的关系,预测差距比较大的时候损失也比较大,可以采用L1损失,但是是不连续的,因此一般采用Huber损失,实际上是L1损失和L2损失的分段的情况。正则可以是L1正则或者L2正则,目的是为了限制模型的复杂度,L2会使得w的绝对值会缩小,也叫权重缩减,但是一般不为0。L1会为0,可以起到特征选择的作用。然后讲了逻辑斯蒂回归,是一个分类问题,直接计算后验概率,多类分类需要softmax函数,损失也是负log似然,只是称为交叉熵损失,推导的时候都是从负log似然推导过来的。然后生成式分类器,高斯判别分析和朴素贝叶斯,两种分类器的先验是相同的,两类是伯努利分布,多类是多项式分布,两者不同的地方是类条件的定义不一样,高斯是在给定类别的条件下是高斯分布,朴素贝叶斯是独立的,

+

第七章:SVM,考察的重点,需要掌握线性的SVM,核化的SVM,回归了解一下就行,首先要知道间隔的概念,硬间隔和软间隔的目标函数分别是什么,可以写成损失+正则的情况,合页损失+正则,什么样的样本是支持向量,了解原问题和对偶问题,核化也是很重要的,后面的PCA,包括逻辑斯蒂回归都可以核化,但是模型比较复杂,大家用的比较少,线性核,多项式核的复杂度体现在基数,RBF核的复杂度是核函数的宽度,软间隔的复杂度还有松弛因子C,概念需要掌握

+

第八章:聚类:重点掌握K均值,GMM,DBSCAN,知道聚类的过程,对什么样的数据比较适合,K均值每一个类的都是高斯,方差相同等等,GMM更宽泛,不球形,DBSCAN不要求每一类的具体形式,找密度最大的作为中心,还可以处理带有噪声的情况。

+

第九章降维:PCA就是K-L变换,目标是重构误差最小,MDS不作重点考察,非线性降维知道一些常用的非线性降维方法的做法就可以了

+

第十章半监督学习:重点掌握三种基本假设,半监督学习算法了解就行,想一下每一种算法的背后是基于哪些假设在做

+

第十一章概率图模型,考试时候主要是考察有向的图模型,无向图模型了解一下就行,给定一个概率图模型,能不能写出概率分布来,根据有向图模型,判断给定观测到某些节点的情况下,这个变量和那个变量是否独立,可以通过贝叶斯球的方式做,需要记忆贝叶斯球的规则,缺失的边实际上也蕴含了独立的关系,HMM要求掌握前向算法求观测概率和利用维特比算法求给定一个观测后最可能的隐含状态的序列。HMM的模型参数学习,全监督的用极大似然估计,隐含状态不知道用EM算法。

+

第十二章集成学习主要是bagging和boosting,要知道bagging的原理是通过对训练样本进行bootstrap采样,然后训练多个模型,最后对多个模型作平均或者投票,得到最后的融合模型。可以并行训练,它的好处是可以降低模型的方差,偏差不变。基学习器的选择可以选方差比较大的模型。比如随机森林里面选取的决策树就是选层数比较深或者叶子节点比较多的。或者多个神经网络进行。boosting是多个基学习器采用顺序的方式进行训练,不能并行训练,每一个新加入的学习器希望能纠正前面的学习器的一些错误,实现的时候每个基学习器都是一样的,融合的方式是加权平均,权重和每个基学习器是相关的。boosting的好处是降低模型的偏差,方差不变。因此基学习器可以比较简单,决策树就树比较浅。重点考察Adaboost的过程,需要掌握。

+

第十三章深度学习:知道神经元的结构(多个输入线性组合+激活函数得到输出),与人类神经元比较像,线性回归实际上也可以用一个神经元来表示,相当于激活函数是一个线性的激活函数(恒等映射函数)。逻辑斯蒂回归就相当于一个神经元+sigmoid函数,SVM也可以写成这样的形式。多层叠加就可以实现非常复杂的非线性函数的拟合。80年代层数不深,当时的训练方法有一些问题,sigmoid或者tanh会产生梯度消失的问题,relu可以解决梯度消失,计算简单。梯度消失是梯度传几层就没了,一个原因是激活函数,另外的原因是乘很多次,也会没了。神经网络之间的连接方式:全连接,每一个神经元都和其他的神经元相连。为了简化,图像可以采用卷积神经网络,通常只是局部连接,为什么要局部连接,因为图像里面一些局部的输入就可以判断模式的存在。为什么要权重共享,权值共享是说不同位置可能都有这样的模式,这样做可以使得模型的参数变少,计算量并不会减少。数据不满足这种特点不能使用卷积做。另外多层感知器很多层并不能带来性能提升,现在的模型采用哪些技术使得训练深度模型成为可能?Resnet,或者叫skip-connect,通过跳接的方式使得梯度消失的问题能减弱,使得模型可以很深很深甚至上千层。另外batch normalization相当于做一个标准化,使得输入的范围的差异不至于这么大。相当于在神经网络里面增加了一个层,学习这个层的参数。会使得模型训练更快更稳定,性能更好。dropout使得某些输出是0,不往下传,相当于集成学习的高效的实现方式,主要使用在全连接层里面。另外讲了一些神经网络的训练技巧,基本算法是梯度下降,梯度爆炸就是乘多了很大很大,梯度的计算首先是利用网络结构进行反向传播,批处理梯度下降是所有的样本一起,算一次很慢,一般不采用,随机梯度下降是每一个只选取一个样本,比较快,但是受单个样本的影响比较大。梯度的方向可能是锯齿的形状。通常使用小批量梯度下降,两者取中间。走一步的步长叫做学习率,下降的方法不一定是梯度,因为鞍点可能不动,平滑的区域也不怎么动,因此考虑动量法,进行自适应调整。参数初始化也比较重要。另外关于抗过拟合的问题,每一层的节点很多,模型就非常复杂,需要抗过拟合。及早停止,监控验证集上的误差,正则,数据增广,收集更多的训练样本,但是收集样本需要费用,可以采用一些图像处理的方法。还有dropout。

+

非线性降维基本掌握流程就行

+

大题没有画模型结构图

+

RNN不作为重点考察内容

+

极大似然:给一个应用场景,写似然的式子,概率分布都写的,无非是取一个log然后对所有的样本求和

+

SVM二维计算量不大,侧重于概念,考了很多选择题

+

考试不涉及求矩阵的逆、特征值的具体计算

+

没有问概念的简答题

+

选择题重点:过拟合欠拟合、偏差方差、概率图、深度学习等等

+

MAP的概念需要知道,大题里面没有

+

LDA需要知道

+

K-L变换需要掌握过程,能达到什么目的,但是没有数值计算

+

逻辑斯蒂回归需要知道正则,损失函数的推导,实际上是负log似然,关于似然的计算,这个过程需要大家知道。

+

极大似然或贝叶斯求参数今年没有

+

线性回归逻辑斯蒂回归损失函数的推导需要掌握,+正则

+

Adaboost知道过程就行

+

核函数表示两个样本的相似度,常用的核函数,比如多项式核,RBF核,还有核函数控制模型复杂度的超参数,就差不多了

+

DBSCAN具体过程不考,需要知道对哪些数据合适

+

EM算法不考察

+

泛化误差不会考具体的公式,知道泛化误差的上界由两部分构成,一部分是和训练误差有关系,另外一部分是模型复杂度(VC维)有关系,是一个正关系还是反关系就行了

+

贝叶斯估计、高斯分布参数求解不作为大题考试内容,MLE的计算过程希望大家了解,考试可能不考

+

高级人工智能

+

20道选择题

+

3道简答题

+

3道综合应用题

+

2022.11.17 罗平老师

+

确定性的知识:

+

命题逻辑:语法和语义,蕴含和形式推演

+

三种形式推演的系统:

+
    +
  • 11条规则
  • +
  • 归结原理:完备性和可靠性,计算机实现是一个搜索问题,联系前面的搜索算法考察
  • +
  • Modus Ponens规则:完备性和可靠性,Forward Chaining和Backward Chaining
  • +
+

一阶谓词逻辑:与命题逻辑对应复习,不考证明

+

Logic Programming:一种新的编程的思路

+
    +
  • Logic Programming与正常的编程有什么差异?选择题
  • +
  • Prolog要能读得懂 +
      +
    • Prolog有时候会推出错误的答案,实现的时候并不可靠
    • +
    • 有时候正确的答案也推不出来,也不完备
    • +
    +
  • +
+

不确定性的知识:

+

模糊集合之间的运算,交并补、模糊关系、模糊关系的合成,用模糊逻辑表示自然语言

+

模糊逻辑比一阶谓词逻辑多了模糊谓词、模糊量词和模糊修饰词

+

2022.12.29

+

深度学习部分:

+

受限玻尔兹曼机原理理解就可以了

+

卷积神经网络、循环神经网络用的比较多,对于具体模型来说,要了解模型的原理,为什么采用这种结构就可以

+

更倾向于概念

+

不会考公式,梯度下降应该熟练掌握的

+

综合应用题分三个小问题,每一个是一个方面,各自独立

+

A*树搜索和图搜索的最优性证明最好是了解一下

+

简答题是搜索方面的,搜索这些算法相应的原理了解一下就可以了

+

简答题不是要求证明的,没有证明题

+

简答题是单个题目

+

没有考公式推导

+

野人传教士问题,实际上是考你搜索问题的形式化描述,形式化描述了解的话应该是没问题的

+

对于GAN,基本概念和原理掌握,考试掌握基本原理就可以了

+

对于启发式搜索,主要是设计一个合适的启发式函数(可采纳性和一致性),针对实际问题用松弛问题的解来作为启发式函数就可以

+

综合应用题是神经网络相关

+

综合应用题有要求画神经网络结构的,说明具体采用的算法

+

选择题都是一些基本概念

+

机器学习

+

单选30题,每题1分

+

多选15题,每题1分

+

简答3题,每题5分

+

计算3题,每题10分

+

设计1题,每题10分

+ + +
+ +
+
+ + + + + + +
+
+
研一上学期闭卷三科目考试重点
+
https://zhangzhao219.github.io/2022/12/27/UCAS/exam-final-summary/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月27日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/12/31/diary/diary20221231/index.html b/2022/12/31/diary/diary20221231/index.html new file mode 100644 index 000000000..a93b9a5d5 --- /dev/null +++ b/2022/12/31/diary/diary20221231/index.html @@ -0,0 +1,768 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2022年终总结 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

2022年终总结

+ + +
+ +

2022年终总结

+ +

到了一年的末尾,伴着客厅的电视声音和窗外若有若无的鞭炮声,还是要写一点总结。

+

我本想用“高开低走”来对这一年做一个精炼的总结,虽然说目前确实是“低”的状态,可是年初似乎也并没有什么“高”的事情,故这个词语还是不怎么恰当。

+

回想去年的这个时候,应该是在科一招和两位同学一起跑赛车吧,当时虽然屋子里面很冷,心是火热的,幻想着这样的生活可以一直持续下去。今天屋子里面还是很冷,不同的是心也很冷,目前过的不怎么样,也看不到什么未来。

+

再回想几年前,已经想不起来什么印象深刻的事情了,可能大多数都是在准备考试吧hh。

+

现在我自己的状态,或许和2018年初是相同的,又或许是2019年,又或许不同,只是我自己的内心深处偏要找一个相同的历史时刻才能让我自己获得某种慰藉。

+

我不知道应该写些什么关于今年的事情,写一写可能又写到了感情生活上,而这是我现在最不愿触及的部分之一。

+

突然想起了五年前看到过的一篇文章,翻出来,最后就用它做一个总结吧:

+

小时候,过年是头等大事。我们家的人不多,但是和父母一起,准备小零食,准备年夜饭,包饺子,看春晚。年少时的我总觉得,日子一天天过去,没有开端也没有终结。

+

那时我总以为,过完了今天,明天还是一样的会来,过完了今年,还有明天这个时候的“今年”。可曾经那个心心念念的过年,曾经的那个“今年”,都像天上的云彩和海上的浪花一样,早已不知所踪。

+

人不能两次踏进同一条河流,也不能,重新过一遍2022。

+

季节流转,日升月落,星移斗转,世事如白衣苍狗。这一年有多少遗憾和侥幸,有多少悲恼和欣欢,多少披星染雾的启程和多少戴月荷锄的归途。新的一年终将随着初生的太阳喷薄而出,我们如同站在两个世界的边缘,愧疚地送别过去,紧张地等候未来。

+

我不愿意用一句“新年新气象”,就将过去一年的得失通通扫净,尽管它们终将消失在记忆的犄角旮旯。

+

新的一年,不是一切归零的重新开局,也不是一成不变的延续。

+

回头再看看2022,我们有伤感的时候,有无奈的时候,有纠结的时候,也有骄傲的时候。总结过去,才能展望未来。

+

2023,不是新的开始,而是新的征程。

+ + +
+ +
+
+ + + + + + +
+
+
2022年终总结
+
https://zhangzhao219.github.io/2022/12/31/diary/diary20221231/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2022年12月31日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/10/6.824/Distributed-Systems-MIT-6.824-LEC-5/index.html b/2023/01/10/6.824/Distributed-Systems-MIT-6.824-LEC-5/index.html new file mode 100644 index 000000000..eb3921587 --- /dev/null +++ b/2023/01/10/6.824/Distributed-Systems-MIT-6.824-LEC-5/index.html @@ -0,0 +1,1525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-LEC 5 Fault Tolerance-Raft-1 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-LEC 5 Fault Tolerance-Raft-1

+ + +
+ +

MIT-6.824(Spring 2022)LEC 5 Fault Tolerance-Raft-1

+ +

In Search of an Understandable Consensus Algorithm (Raft) 论文阅读

+

参考翻译

+

参考总结

+ + +
+ +
+ + + +

摘要

+

一致性算法,或者说 共识算法 ,让⼀组机器像⼀个整体⼀样工作,即使其中⼀些机器出现故障也能够继续工作。

+

Raft 是⼀种为了管理复制日志的⼀致性算法。

+

它将⼀致性算法分解成了几个关键模块:领导人选举、日志复制和安全性。同时它通过更强的⼀致性来 减少状态机的数量

+

总之,对比传统的一致性算法 Paxos,Raft 更清晰易懂,易于实现。

+

1. 简介

+

一致性算法允许多台机器作为一个集群协同工作,并且在其中的某几台机器出故障时集群仍然能正常工作。正因为如此,一致性算法在建立可靠的大规模软件系统方面发挥了重要作用。在过去十年中,Paxos 主导了关于一致性算法的讨论:大多数一致性的实现都是基于 Paxos 或受其影响,Paxos 已经成为教授学生关于一致性知识的主要工具。然而尽管很多人一直在努力尝试使 Paxos 更易懂,Paxos 还是太难理解了。此外,Paxos 的架构需要复杂的改变来支持实际系统。

+

我们开始着手寻找一个新的一致性算法,希望可以为系统开发和教学提供更好的基础。 我们的方法是不寻常的,因为我们的主要目标是可理解性。在该算法的设计中,重要的不仅是如何让算法起作用,还要清晰地知道该算法为什么会起作用。这项工作的结果是一个称为 Raft 的一致性算法。在设计 Raft 时,我们使用了特定的技术来提高它的可理解性,包括:

+
    +
  • 分解(Raft 分离出三个关键点:leader election、log replication、safety)
  • +
  • 减少状态空间(相比于 Paxos,Raft 降低了不确定性的程度和服务器之间的不一致)
  • +
+

一项针对 2 所大学共 43 名学生的用户研究表明,Raft 比 Paxos 更容易理解:在学习两种算法后,其中 33 名学生能够更好地回答 Raft 的相关问题。

+

Raft 在许多方面类似于现有的公式算法,但它有几个新特性:

+
    +
  • Strong leader(强领导性):相比于其他算法,Raft 使用了更强的领导形式。比如,日志条目只能从 leader 流向 follower(集群中除 leader 外其他的服务器)。这在使 Raft 更易懂的同时简化了日志复制的管理流程。
  • +
  • Leader election(领导选举):Raft 使用随机计时器来进行领导选举。任何一致性算法都需要心跳机制,Raft 只需要在这个基础上,添加少量机制,就可以简单快速地解决冲突。
  • +
  • Membership changes(成员变更):Raft 在更改集群中服务器集的机制中使用了 联合一致性 的方法。在联合一致性下,在集群配置的转换过程中,新旧两种配置大多数是重叠的,这使得集群在配置更改期间可以继续正常运行。
  • +
+

我们认为 Raft 跟 Paxos 以及其他一致性算法相比是更优的,这不仅体现在教学方面,还体现在工程实现方面。

+
    +
  • 它比其他算法更简单且更易于理解
  • +
  • 它被描述得十分详细足以满足实际系统的需要
  • +
  • 它有多个开源实现,并被多家公司使用
  • +
  • 它的安全性已被正式规定和验证
  • +
  • 它的效率与其他算法相当
  • +
+

2. 复制状态机

+

一致性算法基于复制状态机

+

一致性算法一般都是在 复制状态机 的背景下实现的。在这种方法下,一组服务器在的状态机计算相同状态的相同副本,即使某些服务器崩溃,它们也可以继续运行。

+

复制状态机是用来解决分布式系统中的各种容错问题。比如说,具有单个 leader 的大规模的系统,如 GFS,HDFS 和 RAMCloud ,他们通常都使用单独的复制状态机来管理 leader election 和保存 leader 崩溃后重新选举所需的配置信息。像 Chubby 和 ZooKeeper 都是复制状态机。

+

复制状态机通常都是使用日志复制(log replication)来实现。

+

+

如图:每个服务器都保存着一份拥有一系列命令的日志,然后服务器上的状态机会按顺序执行日志中的命令。每一份日志中命令相同并且顺序也相同,因此每个状态机可以处理相同的命令序列。所以状态机是可确定的,每个状态机都执行相同的状态和相同的输出序列。

+

一致性算法的主要工作就是保证复制日志(replicated log)的一致性 。每台服务器上的一致性模块接收来自客户端的命令,并将这些命令添加到其日志当中。一致性模块与其他服务器上的一致性模块进行通信,以确保每台服务器上最终以相同的顺序包含相同的命令,即使部分服务器崩溃了,这个条件也可以满足。一旦命令被正确复制,每台服务器上的状态机就会按日志顺序处理它们,并将输出返回给客户端。这样就形成了高可用的复制状态机。

+

适用于实际系统的 一致性算法通常都包含以下几点特征

+
    +
  • 安全性:非拜占庭错误(出现故障(crash 或 fail-stop,即不响应)但不会伪造信息)情况下,绝不会返回错误的结果
  • +
  • 可用性:只要大多数机器(过半)正常就可保证可用。假设服务器崩溃了,一小段时间后,它们很可能会根据已经稳定存储的状态来进行恢复,并重新加入集群。
  • +
  • 不依赖时序保证一致性:错误的时钟和极端消息延迟在最坏的情况下会产生影响可用性的一系列问题。
  • +
  • 在通常情况下,只要集群中大部分(过半)服务器已经响应了单轮远程过程调用(RPC),命令就可以被视为完成, 小部分慢节点不影响整体性能
  • +
+

3. Paxos 算法的问题

+

在过去的十年间,Leslie Lamport 的 Paxos 协议 几乎成为一致性的同义词。它是课堂上被教授最多的一致性协议,大多数一致性的实现也是以它为起点。Paxos 首先定义了能在单个决策问题(例如单个复制日志条目)上达成一致性的协议。我们将这个子集称为 single-decree Paxos 。然后 Paxos 组合该协议的多个实例去实现一系列决策,比如日志(multi-Paxos)。Paxos 保证了安全性和活性,它也支持改变集群中的成员,它的安全性也已经被论证了,并且大多数情况下都是高效的。

+

美中不足的是,Paxos 有两个严重的缺点:

+

Paxos 非常难理解

+

众所周知,Paxos 非常晦涩难懂,除非下了很大的功夫,很少有人能够成功理解它。因此,尽管目前已经有几个尝试希望将 Paxos 解释得通俗易懂一些,而且这些解释都集中在 single-decree Paxos,但是它们还是很难懂。在对 NSDI 2012 参会者的非正式调查中,我们发现很少人会喜欢 Paxos,即使是经验丰富的研究人员。我们自己也一直在跟 Paxos 作斗争,我们也无法完全理解整个 Paxos 协议,直到阅读了几个更简单的描述和自己设计了替代 Paxos 的协议,我们才对 Paxos 有了比较深刻的理解。但这个过程,花了将近一年。我们推测 Paxos 这么晦涩难懂,主要是因为作者选择了 Single-decree Paxos 来作为基础。Single-decree Paxso 非常搞人:它分为两个阶段,但是并没有对这两个阶段进行简单直观的说明,而且这两个阶段也不能分开了单独理解,所以使用者将就很难理解为什么该算法能起作用。Multi-Paxos 的合成规则又增加了许多复杂性。我们相信,对多个决定(日志,并非单个日志条目)达成一致性的总体问题可以用其他更直接和更明显的方式进行分解。

+

Paxos 没有为实际实现提供一个良好的基础

+

其中一个原因是没有广泛认同的针对 Multi-Paxos 的算法。Lamport 的描述主要是针对 signle-decree Paxos 的,他描述了针对 multi-Paxos 的可能方法,但缺少了很多细节。目前已经有人在尝试具体化和优化 Paxos,但是这些尝试都互不相同并且它们跟 Lamport 描述的也不尽相同。虽然像 Chubby 这样的系统已经实现了类 Paxos 算法,但是他们并没有透露出很多的实现细节。

+

此外,Paxos 的架构对于构建实际系统来说其实是一个糟糕的设计,这是 single-decree Paxos 分解的另一个结果。举个例子,这对于独立选择地日志条目的集合,然后再将它们合并到顺序日志当中没有任何好处,这只会增加复杂性。围绕日志来设计系统是更加简单和高效的方法,其中新条目按受约束的顺序依次附加。另外一个问题是 Paxos 在其核心使用了 对称对等方法 (尽管它最终表明了这会被用作一种性能优化的弱领导模式)。这在只有一个决策的情况下是有意义的,但是尽管如此,还是很少有实际系统采用了这种方法。如果有一系列的决策需要制定,更简单和更快速的方法应该是首先选择一个 leader,然后由 leader 去协调这些决策。

+

因此,按照 Paxos 来实现的实际系统往往跟 Paxos 相差很大。几乎所有的实现都是从 Paxos 开始,然后在实现的过程中发现了一系列的难题,在解决难题的过程中,开发出了跟 Paxos 完全不一样的架构。这样既费时又容易出错,而且 Paxos 本身的晦涩难懂又使得问题变得更加严重。Paxos 公式可能是证明其正确性的一个很好的公式,但真正的实现与 Paxos 又相差很大,这证明了它其实没有什么价值。来自 Chubby 作者的评论非常典型:在 Paxos 算法描述和现实实现系统之间有着巨大的鸿沟,如果一直按照 Paxos 算法走下去,最终的系统往往会建立在一个还未被证明的协议之上。

+

综合上述问题,我们觉得 Paxos 在教学端和系统构建端都没有提供一个良好的基础。考虑到共识性在大规模软件系统中的重要性,我们决定去尝试一下看看能不能设计一个替代 Paxos 并且具有更好特性的共识算法。

+

Raft 算法就是尝试克服以上缺点,替代 Paxos 的一致性算法。

+

4. 为了可理解性的设计

+

设计 Raft 的初衷:

+
    +
  • 提供⼀个完整的实际的系统实现基础:大大减少开发者的工作
  • +
  • 任何情况下都是安全的
  • +
  • 大多数的情况下都是可用的
  • +
  • 大部分操作必须是高效的
  • +
  • 可理解性:(最重要、最大挑战)保证大多数人都可以容易理解。
  • +
  • 能够让人形成直观的认识:使系统的构建者能够在现实中进行必然的扩展。
  • +
+

在设计 Raft 算法的过程中,很多情况下我们需要在多个备选方案下做出抉择。在这种情况下,我们往往会基于可理解性来进行抉择:

+
    +
  • 解释各个备选方案的难度有多大?例如,它的状态空间有多复杂?它是否具有难以理解的含义?
  • +
  • 对于一个读者来说,完成理解这个方案和方案中的各种含义是否简单?
  • +
+

我们意识到这一的分析具有高度的主观性。所以我们采取了两种通用的措施来解决这个问题。

+
    +
  1. 第一个措施就是众所周知的 问题分解 :只要有可能,我们就将问题划分成几个相对独立地解决、解释和理解的子问题。例如,Raft 算法被我们划分成 leader 选举、日志复制、安全性和成员变更几个部分。
  2. +
  3. 第二个措施是 通过减少状态的数量来简化状态空间 ,尽可能地使系统变得更加连贯和尽可能地消除不确定性。很明显的一个例子就是,所有的日志都是不允许有空挡的,并且 Raft 限制了日志之间可能不一样的方式。尽管在大多数情况下我们都极力去消除不确定性,但是在某些情况下不确定性却可以提高可理解性。一个重要的例子就是随机化方法,它们虽然引入了不确定性,但是它们往往能够通过以类似的方式处理所有可能的选择来减少状态空间(随便选,没关系)。所以我们使用了随机化来简化 Raft 中的 leader election 算法。
  4. +
+

5. Raft 一致性算法

+

Raft 是一种用来管理第2节中提到的复制日志(replicated log)的算法

+

Raft算法的关键特性:

+

pSn0Ve0.png

+

Raft算法的简略版:

+

pSnwXLt.jpg

+

Raft 选举一个 Leader ,给予管理所有复制日志的权限,由此实现一致性。

+

Leader 从客户接受指令,写入日志,复制到其他 Backup Server 上,在保证安全性时通知其他 Server 根据日志执行指令更新状态机。

+

Leader 大大简化了对复制日志的管理。leader 可以自行决定新日志写入位置,数据都从 Leader 流向其他 Server。当 Leader 宕机,从其他 Server 中选举一个新 Leader。

+

Raft 将一致性问题分解为 三个子问题

+
    +
  • Leader election(领导选举):一个 leader 倒下之后,一定会有一个新的 leader 站起来。
  • +
  • Log replication(日志复制):leader 必须接收来自客户端的日志条目然后复制到集群中的其他节点,并且强制其他节点的日志和自己的保持一致。
  • +
  • Safety(安全性):Raft 中安全性的关键是状态机的安全性:只要有任何服务器节点将一个特定的日志条目应用到它的状态机中,那么其他服务器节点就不能在同一个日志索引位置上存储另外一条不同的指令。此处还涉及一个额外的选举机制上的限制。
  • +
+

5.0 Raft算法的关键特性与简略说明

+

State(状态)

+

所有服务器上持久存在的:

+

(在响应RPCs之前已在稳定存储上进行更新)

+ + + + + + + + + + + + + + + + + + + + + +
状态变量说明
currentTerm服务器最后⼀次知道的最新的任期号(初始化为 0,持续递增)
votedFor在当前任期获得选票的候选人的id(如果没有则为 null)
log[]日志条目集;每⼀个条目包含⼀个用户状态机执行的指令,和收到时的任期号
+

所有服务器上经常变的:

+ + + + + + + + + + + + + + + + + +
状态变量说明
commitIndex已知的最大的已经被提交的日志条目的索引值
lastApplied最后被应用到状态机的日志条目索引值(初始化为 0,持续递增)
+

在leader里面经常改变的:

+

(选举后重新初始化)

+ + + + + + + + + + + + + + + + + +
状态变量说明
nextIndex[]对于每⼀个服务器,需要发送给他的下⼀个日志条目的索引值(初始化为领导人最后索引值加1)
matchIndex[]对于每⼀个服务器,已经复制给他的日志的最高索引值
+

AppendEntries RPC(追加待同步日志 RPC)

+

由 Leader 负责调用来复制日志(5.3);也会用作心跳机制(5.2)

+

传入参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态变量说明
termLeader的任期号
leaderIdLeader的 id,以便于跟随者重定向请求
prevLogIndex新的日志条目紧随之前的索引值
prevLogTermprevLogIndex 条目的任期号
entries[]准备存储的日志条目(表示心跳时为空;⼀次性发送多个是为了提高效率)
leaderCommitLeader已经提交的日志的索引值
+

返回值:

+ + + + + + + + + + + + + + + + + +
状态变量说明
term当前的任期号,用于Leader去更新自己
success跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真
+

接收者实现:

+
    +
  1. 如果 term < currentTerm 就返回 false (5.1 节)
  2. +
  3. 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false (5.3 节)
  4. +
  5. 如果现有的日志条目和新的产⽣冲突(索引值相同但是任期号不同),删除现有的和之后所有的条目 (5.3 节)
  6. +
  7. 追加日志中尚未存在的任何新条目
  8. +
  9. 如果 leaderCommit > commitIndex ,令 commitIndex = min(leaderCommit, 新日志条目索引)
  10. +
+

RequestVote RPC(请求投票 RPC)

+

由候选人调用用来征集选票(5.2 节)

+

传入参数

+ + + + + + + + + + + + + + + + + + + + + + + + + +
状态变量说明
term候选人的任期号
candidateId请求选票的候选人的 Id
lastLogIndex候选人的最后日志条目的索引值
lastLogTerm候选人最后日志条目的任期号
+

返回值

+ + + + + + + + + + + + + + + + + +
状态变量说明
term当前任期号,以便于候选人去更新自己的任期号
voteGranted候选人赢得了此张选票时为 true
+

接收者实现:

+
    +
  1. 如果 term < currentTerm 返回 false (5.2 节)
  2. +
  3. 如果 votedFornull 或者为 candidateId,并且候选人的日志至少和接受者一样新,那么就给它投票(5.2 节,5.4 节)
  4. +
+

Rules for Servers(服务器的规则)

+

所有服务器

+
    +
  • 如果 commitIndex > lastApplied,那么就将 lastApplied 加一,并把 log[lastApplied] 应用到状态机中(5.3 节)
  • +
  • 如果接收到的 RPC 请求或响应中,任期号 T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为 Follower(5.1 节)
  • +
+

Followers(跟随者)(5.2 节):

+
    +
  • 响应来自候选人和 Leader 的 RPC 请求
  • +
  • 如果选举超时,都没有收到现任 Leader 的 AppendEntries RPC,也没有给候选人投票:则自己转变成候选人。
  • +
+

Candidates(候选人)(5.2 节):

+
    +
  • 在转变成候选人后就立即开始选举过程 +
      +
    • 自增当前的任期号(currentTerm
    • +
    • 给自己投票
    • +
    • 重置选举超时计时器
    • +
    • 发送 RequestVote RPC 给其他所有服务器
    • +
    +
  • +
  • 如果接收到大多数服务器的选票,那么就变成 Leader
  • +
  • 如果接收到来自新的 Leader 的 AppendEntries RPC,转变成 follower
  • +
  • 如果选举过程超时,再次发起一轮选举
  • +
+

Leader(领导人):

+
    +
  • 一旦成为Leader:发送初始的空 AppendEntries RPCs(心跳)给每个服务器;在空闲期间重复发送,防止选举超时(5.2 节)
  • +
  • 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节)
  • +
  • 如果一个 follower 最后日志条目的索引值 index ≥ nextIndex,那么:使用 AppendEntries RPC 发送从 nextIndex 开始的所有日志条目: +
      +
    • 如果成功:更新相应跟随者的 nextIndexmatchIndex
    • +
    • 如果 AppendEntries 因为日志不一致而失败,减少 nextIndex 并重试
    • +
    +
  • +
  • 如果存在一个满足 N > commitIndex 的 N,并且大多数的 matchIndex[i] ≥ N 成立,并且 log[N].term == currentTerm 成立,那么令 commitIndex = N (5.3 和 5.4 节)
  • +
+

关键特性

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性解释
选举安全对于一个给定的任期号,最多只会有一个 Leader 被选举出来(5.2 节)
Leader 只追加Leader 绝对不会删除或者覆盖自己的日志,只会增加(5.3 节)
日志匹配特性如果两个日志在相同的索引位置的日志条目的任期号相同,那么我们就认为这个日志从头到这个索引位置之间全部完全相同(5.3 节)
领导人完全特性如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有领导人中(5.4 节)
状态机安全特性如果一个 Leader 已经在给定的索引值位置的日志条目应用到状态机中,那么其他任何的服务器在这个索引位置不会提交一个不同的日志(5.4.3 节)
+

5.1 Raft 基础

+

一个 Raft 集群通常包含 5 个节点,能容忍 2 个节点宕机。

+

Raft 集群的服务器都处于三个状态之一:

+
    +
  • Leader :只有一个,响应所有客户端请求
  • +
  • Follower :其余都是,不发送只响应 Leader 或 Candidate 的请求。若客户向其请求,会重定向到 Leader。
  • +
  • Candidate :选举新 Leader 时使用(5.2)
  • +
+

+

服务器状态。Follower 只响应来自其他服务器的请求。如果 Follower 接收不到消息,那么他就会变成 Candidate 并发起一次选举。获得集群中大多数选票的 Candidate 将成为 Leader。在一个任期内,Leader 保持身份直到自己宕机。

+

Raft 把时间分割成任意长度的 任期(term) ,用 连续递增整数编号 ,任期开始即选举。Raft 保证一个任期只有一个 Leader。在某些情况下,一次选举无法选出 leader,这个时候这个任期会以没有 leader 而结束。同时一个新的任期(包含一次新的选举)会很快重新开始。

+

+

时间被划分成一个个的任期(term),每个任期开始都是一次选举。在选举成功后,领导人会管理整个集群直到任期结束。有时候选举会失败,那么这个任期就会没有领导人而结束。任期之间的切换可以在不同的时间不同的服务器上观察到。

+

任期编号在 Raft 算法中充当逻辑时钟,每个节点都储存当前任期号, 节点之间通信会交换任期号 ,当一个节点:

+
    +
  • 当前任期号比其他节点小,更新自己的任期号
  • +
  • Leader 或 Candidate 发现自己任期号过期,立即转为 Follower(也就是放弃成为Leader的机会)
  • +
  • 收到过期的任期号请求,拒绝请求。
  • +
+

节点之间通信使用远程过程调用(RPCs) ,包含两种(第7节还增加了第三种传送快照的):

+
    +
  • 请求投票(RequestVote) RPCs:Candidate 在选举期间发起(5.2)
  • +
  • 追加条目(AppendEntries)RPCs:Leader 发起,用于复制日志和心跳(5.3)
  • +
+

当节点没有及时的收到 RPC 的响应时,会进行重试,而且节点之间都是以并行的方式发送 RPC 请求,以此来获得更好的性能。

+

5.2 Leader 选举

+
    +
  • 服务器启动时所有节点都是 Follower 。 +
      +
    • Follower 一段时间没接收到消息即 选举超时 ,发起新选举。
    • +
    • Leader 周期性发送 心跳包(不含日志的 AE RPC) 给所有 Follower 来维持自己地位。
    • +
    • Follower 只要能收到 Leader 或 Candidate 的 RPC 就保持当前状态。
    • +
    +
  • +
  • 开始选举 。Follower 自增 term(任期号)并转为 Candidate,并行向其他节点发送 RV RPC 等待给自己投票。 +
      +
    • 等待时 收到 Leader 的心跳 ,且心跳中的任期不小于自己的当前任期,则自己变为 Follower。若小于自己的任期,则拒绝并保持 Candidate。
    • +
    • 如果同时出现多个 Candidate,选票可能被瓜分, 没有人得到多数选票 。则等待超时后重新选举。
    • +
    • Raft 使用 随机选举超时时间 (例如 150-300 毫秒)防止多次无人上任。每个节点 开始选举时重制超时时间 。可以让多数情况只有一个节点超时,进入下一轮赢得选举。
    • +
    +
  • +
  • 获得多数选票的 Candidate 变为 Leader 。 +
      +
    • 每个节点在一个任期内,按先来先服务(5.4节还有额外限制) 最多为一个 Candidate 投票
    • +
    • 成为 Leader 后向其他节点发送心跳建立权威。
    • +
    +
  • +
+

5.3 日志复制

+

5.3.1 Leader 日志复制流程

+
    +
  • 把客户端请求指令追加到日志,然后并行发 AE RPC 给其他节点让其追加日志。
  • +
  • 在日志被其他节点安全复制后(多数节点已复制),Leader 应用该指令到状态机并返回结果给客户端。
  • +
  • 如果中途出现问题,Leader 会不断重复 AE RPC(甚至已回复客户端后)直到所有 Follower 都追加了该日志。
  • +
+

5.3.2 日志提交

+
    +
  • 一条日志包含当前任期号一条指令 ,也都有一个整数索引来表明它在日志中的位置。
  • +
  • Leader 决定什么时候能把日志安全应用到状态机,这样的日志条目为 已提交committed )。Raft 保证所有已提交日志都是持久化并最终被所有状态机执行。
  • +
  • Leader 把日志设为已提交后,还需要 通知 Follower 应用日志到状态机 ,这个通知通过下一次 AE RPC(也许是心跳)附加 commitIndex
  • +
  • 日志条目复制到大多数节点上时,就是 已提交 ,且 Leader 中当前条目 之前的日志也都已提交 ,包括其他 Leader 创建的条目(5.4)。Leader 记录最大已提交索引 leaderCommit,并放进所有 AE PRCs,其他节点由此得知 Leader 已提交位置,并按日志顺序应用到自己的状态机。
  • +
+

+

日志由序号标记的条目组成。每个条目都包含创建时的任期号和一个状态机需要执行的指令。一个条目当可以安全的被应用到状态机中去的时候,就认为是可以提交了。

+

5.3.3 日志一致性

+

这样 Raft 能维持 日志的一致性日志匹配特性):

+
    +
  • 在不同的日志中的两个条目拥有 相同的索引和任期号 ,那么他们 存储了相同的指令
  • +
  • 在不同的日志中的两个条目拥有 相同的索引和任期号 ,那么他们 之前的所有日志条目也全部相同
  • +
  • 追加日志的一致性检查 :每次新条目 AE RPC 给 Follower,如果上一条索引任期不一致,则拒收新条目。所以 一旦 AE RPC 返回成功,说明 Follower 所有日志和 Leader 相同
  • +
+

5.3.4 日志不一致情况

+

正常情况下一致性检查不会失败,能一直保持一致。 但是 Leader 在未完全复制日志时宕机会使日志不一致 。例如 Follower 可能没有新 Leader 有的条目,也可能有新 Leader 没有的条目,或者都有,如下图。

+

+

当一个领导人成功当选时,跟随者可能是任何情况(a-f)。每一个盒子表示是一个日志条目;里面的数字表示任期号。跟随者可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。

+

例如,场景 f 可能会这样发生:f 对应的服务器在任期2的时候是 Leader,它追加了一些日志条目到自己的日志中,一条日志还没提交就宕机了,但是它很快就恢复重启了,然后再在任期3重新被选举为 Leader,又追加了一些日志条目到自己的日志中,在这些任期2和任期3的日志还没有被提交之前,该服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。

+

5.3.5 不一致的恢复

+

Raft 中处理这种不一致方法是, Leader 强制 Follower 复制自己的日志,即覆盖 Follower 中所有冲突日志 (安全性在5.4)。

+

Leader 找到最后和 Follower 一致的地方,删除 Follower 之后的冲突日志,发送自己的日志附加给 Follower。这些操作 在 AE RPCs 一致性检查时完成

+
    +
  • Leader 对每个 Follower 维护下一个需发送的条目索引 nextIndex,在刚上任时初始化为最新日志索引+1。
  • +
  • Follower 日志不一致则拒绝 AE PRC ,Leader 减小 nextIndex 重试直到成功 ,Follower 删除冲突日志并追加 Leader 日志。日志即保持一致。
    +这里可以优化,Follower 拒绝时返回冲突任期号的最早地址,Leader 下次就可以越过同任期号的冲突。但是此优化不一定有必要,因为实际很少发生。
  • +
+

所以 Leader 无需特殊操作就能恢复一致性 ,Leader 也从不会覆盖删除自己的日志(图3 Leader 只追加特性)。

+

日志复制机制展示了一致性特征:

+
    +
  • 只要大部分的机器是工作的就能正常复制日志和应用保证可用性;
  • +
  • 一条指令大多数节点可一轮 RPC 完成,小部分慢节点不影响整体性能
  • +
+

5.4 安全性

+

目前为止所讨论的机制并不能充分地保证每一个状态机会按相同的顺序执行相同的指令。比如说,一个 follower 可能会进入不可用状态,在此期间,leader 可能提交了若干的日志条目, 然后这个 follower 可能被选举为新的 leader 并且用新的日志条目去覆盖这些日志条目 。这样就会造成不同的状态机执行不同的指令的情况。

+

故需 增加选举限制 ,保证图 3 中的领导人完整性,即 Leader 一定包含所有已提交日志条目

+

5.4.1 选举限制

+

某些一致性算法中需要额外复杂机制把缺少的日志传给 Leader。但是 Raft 保证 Leader 本来就有所有日志,所有日志都是单向从 Leader 传出去。

+

Raft 在等待投票时,RV PRC 包含 Candidate 的日志信息, 投票人会拒绝日志没有自己新的 Candidate 的投票请求

+

投票人 比较最后一条日志的索引值和任期号

+
    +
  • 任期号不同,则任期号大的比较新
  • +
  • 任期号相同,索引值大的(日志较长的)比较新
  • +
+

5.4.2 提交之前任期内的日志条目

+

(本小节是一种错误情况)

+

前面介绍,一旦当前任期内的某个日志条目以及存储到过半的服务器节点上,Leader 就知道此日志在自己任期已提交。

+

Leader 可能在提交之前崩溃 ,新 Leader 不知道保存在多数节点的的条目是否提交。例如下图,存在多数节点的老日志仍可能被覆盖。

+

+
    +
  • 在(a)中,S1是Leader,复制了索引位置2的日志条目给S2,这时还没过半。
  • +
  • 在(b)中,S1宕机了,然后S5在任期3中通过S3、S4和它自己的投票赢得了选举,然后从客户端接收了一条不一样的日志条目放在了索引位置2上面。
  • +
  • 在©中,S5宕机了,S1重启,此时S1和S2都可能成为leader,假如 S1贏得选举,然后 S1继续复制它之前在任期2中放在索引2上的日志条目。此时,来自任期2的那条日志已经被复制到了集群中过半的节点上了,但是它还没被提交。
  • +
  • 情况一,在(d)中,假如S1在提交日志之前宕机了,然后S5重启,这个时候S5最后的日志条目上的任期号比S2、S3和S4都大,所以它可以获得到S2、S3、S4和自己的投票成功当选leader。S5当选leader后,它就继续复制在任期3期间存储在索引位置2上的日志条目,那么该日志条目就会覆盖之前引复制在节点S1、S2、S3索引2处的日志中 。
  • +
  • 情况二, 在(e)中,如果在宕机之前,S1在自己任期内复制了日志条目到人多数机器上。那么S5就不可能贏得选举,这种情况下,之前的所有日志也被提交了。
  • +
+

所以 Raft 对日志提交条件增加一个额外限制Leader 在当前任期至少有一条日志被提交 (即超过半数节点复制),如图 8 中的(e)所示。而©中并没有提交4任期的日志。

+

所以新上任的 Leader 在接受客户写入命令前先提交一个 no-op(空命令),携带自己任期号的日志复制到多数节点,这样能保证选举限制成立。

+

5.4.3 安全性证明

+

img

+

假设:

+

假设任期 T 的 leaderT 在任期内提交了一个日志条目,但是该日志条目没有存在未来某些任期的 leader 中,假设 U 是大于 T 的没有存储该日志条目的最小任期号,处在任期 U 的 leader 称为 leaderU。

+

反证法论证:

+
    +
  1. 因为 leader 从来不删除或重写自己的日志条目,所以如果一个已提交的日志要做到不存在未来的 leaderU 中的话,那么它只可能在 leaderU 选举的过程中被丢失。
  2. +
  3. leaderT 将该日志复制给了集群中过半的节点,leaderU 从集群中过半的节点得到了投票。因此,至少有一个节点(这里称它为 voter)同时接收了来自 leaderT 的日志条目并且给 leaderU 投票了。
  4. +
  5. voter 必然在给 leaderU 投票之前就已经接收了这个已经提交的日志条目了。否则,它就会拒绝来自 leaderT 的 AppendEntries RPC 请求,因为如果它在给 leaderU 投票之后再接收条目的话,那么它的当前任期号会比 T 大。
    +译者注:因为要举行 Leader election 的话需要开一轮新的任期,这个时候前一轮任期已经结束了。我们这里假设了 T < U,上述所说的已提交日志条目是在任期 T 中的,如果 voter 先投票的话,那么就说明它已经进入了任期 U 了,而 U > T,voter 是不可能接受 leaderT 的 AppendEntries 请求的。
  6. +
  7. 而且,voter 在给 leaderU 投票的时候,它依旧保有该日志条目,因为任何 U、T 之间的 leader 都包含该日志条目(因为我们前面假设了 U 是大于 T 的没有存储该日志条目的最小任期号),而且 leader 从来不会删除条目,并且 follower 只有再跟 leader 冲突的时候才会删除条目。
  8. +
  9. 该投票者把自己的选票投给 leaderU 的时候,leaderU 的日志至少跟 voter 一样新(可以更新),这就导致了以下的两个矛盾之一了。
  10. +
  11. 第一个矛盾:如果 voter 和 leaderU 最后一个日志条目的任期号相同的话,那么 leaderU 的日志至少和 voter 的一样长,所以 leaderU 的日志一定包含 voter 日志中的所有日志条目。 这是一个矛盾,因为 voter 包含了该已提交的日志条目,所以 leaderU 必定也包含该日志条目,而前面我们假设了 leaderU 是不包含的,这就产生了矛盾。
  12. +
  13. 第二个矛盾:如果不是上面描述的情况的话,那么 leaderU 最后一个日志条目的任期号必然需要比 voter 的更大。此外,它还比 T 要大,因为 voter 拥有在任期号为 T 提交的日志条目,所以 voter 最后一个日志条目的任期号至少为 T。创建了 leaderU 的最后一个日志条目的之前的 leader 一定已经包含了该已被提交的日志条目(因为我们上面假设了 leaderU 是第一个没有该日志条目的 leader)。所以,根据日志匹配特性,leaderU 一定也包含了该已被提交的日志条目,这样也产生了矛盾
  14. +
  15. 上述讨论就证明了假设是不成立的。因此,所有比 T 大的任期的 leader 一定包含了任期 T 中提交的所有日志条目。
  16. +
  17. 日志匹配特性保证了未来的 leader 也会包含被间接提交的日志条目,如图中的索引 2。
  18. +
+

通过 leader 的完整性特性,我们就可以证明状态机安全特性了,即如果某个节点已经将某个给定的索引处的日志条目应用到自己的状态机里了,那么其他的节点就不会在相同的索引处应用一个不同的日志条目。在一个节点应用一个日志条目到自己的状态机中时,它的日志和 leader 的日志从开始到该日志条目都是相同的,并且该日志条目必须被提交。现在考虑一个最小的任期号,在该任期中任意节点应用了一个给定的最小索引上面的日志条目,那么 Log 的完整性特性就会保证该任期之后的所有 leader 将存储相同的日志条目,因此在后面的任期中应用该索引上的日志条目的节点会应用相同的值。所以,状态机安全特性是可以得到保证的。

+

因为 Raft 要求服务器节点按照日志索引顺序应用日志条目,再加上状态机安全特性,这样就意味着我们可以保证所有的服务器都会按照相同的顺序应用相同的日志条目到自己的状态机中了。

+

5.5 Follower 和 Candidate 崩溃

+

前面都是讨论 Leader 崩溃,Follower和 Candidate 崩溃后的处理方式简单的多,Raft 只需要不断重试发送 RPCs 即可,崩溃重启后再执行 RPC。

+

Raft 的 RPCs 都是幂等的,重试不会产生问题。如果 Follower 发现 AE RPC 中的日志已经有了,它直接忽略这个请求。

+

5.6 时间和可用性

+

Raft 的要求之一就是 安全性不能依赖时间 :整个系统不能因为某些事件运行的比预期快一点或者慢一点就产生了错误的结果。

+

但可用性不可避免要依赖时间,最关键在于 Leader 选举,需要满足如下时间要求:

+

broadcastTime<<electionTimeout<<MTB

+
    +
  • 广播时间(broadcastTime):一个节点并行发送 RPCs 给其他节点并接收响应的平均时间 +
      +
    • 应比选举超时时间小一个数量级才能保证稳定的心跳。
    • +
    • 广播时间大约是 0.5 毫秒到 20 毫秒,取决于存储的技术
    • +
    +
  • +
  • 选举超时时间(electionTimeout):选举超时时间限制。随机化使之难以瓜分选票。 +
      +
    • 只有选举超时时间是我们自己选择的 。可能需要在 10 毫秒到 500 毫秒之间。
    • +
    • 应比平均故障时间小几个数量级。Leader 崩溃后系统将在一个选举超时时间中不可用,此情况应很少出现。
    • +
    +
  • +
  • 平均故障间隔时间(MTBF):一个节点两次故障之间的平均时间。 +
      +
    • 大多数服务器平均故障时间在几个月甚至更长,很容易满足时间的需求。
    • +
    +
  • +
+

6. 集群成员变化

+

到目前为止,我们都假设集群的配置(参与共识算法的服务器节点集合)是固定不变的。但是在实际情况中,我们有时候是需要去改变集群配置的,比如说在服务器崩溃的时候去更换服务器或者是更改副本的数量。尽管可以通过下线整个集群,更新所有配置,然后重启整个集群的方式来实现这个需求,但是这会导致集群在更改过程中是不可用的。另外,如果这个过程中存在一些操作需要人工干预,那么就会有操作失误的风险。为了避免这些问题,我们决定将配置变更自动化并将其纳入到 Raft 的共识算法中来。

+

6.1 两阶段提交:Joint Consensus

+

为了让配置修改机制安全,在转换的过程中同一个任期里 不能够存在两个 Leader 同时当选 。问题在于, 一次性自动的转换所有服务器是不可能的 ,任何切换方法都是不安全的,所以在转换期间 整个集群可能分裂成两个独立的多数

+

+

直接从一种配置转到新的配置是十分不安全的,因为各个机器可能在不同时候进行转换。在中间位置 Server1 可以通过自身和 Server2 的选票成为 leader(满足旧配置下收到大多数选票的原则);Server3 可以通过自身和 Server4、Server5 的选票成为 leader(满足新配置线,即集群有 5 个节点的情况下的收到大多数选票的原则);此时整个集群可能在同一任期中出现了两个 leader,这和 Raft 协议是违背的。

+

为了保证安全性,配置更改必须使用 两阶段方法 。有些系统在第一阶段停掉旧的配置,集群就不能处理客户端请求;然后在第二阶段在启用新的配置。

+

在 Raft 中,集群先切换到一个过渡性配置,我们称之为 Joint Consensus联合共识 );一旦联合共识被提交,那么系统就切换到新的配置上。

+

Joint Consensus 是老配置和新配置的结合:

+
    +
  • 日志条目被复制给集群中新、老配置的所有服务器。
  • +
  • 新、旧配置的服务器都可以成为领导人。
  • +
  • 达成一致(针对选举和提交)需要 分别在两种配置上获得大多数的支持
  • +
+

Joint Consensus 允许独立的服务器在不影响安全性的前提下,在不同的时间进行配置转换,还可以让集群在配置转换的过程中依然响应客户端的请求。

+

6.2 实现细节

+

集群配置在复制日志中以特殊的日志条目来存储和通信。下图展示了配置转换的过程:

+
    +
  • 当一个 Leader 接收到一个改变配置从 C-old 到 C-new 的请求,他将创建联合共识的配置(图中的 C-old,new)并存储为一个新日志条目。
  • +
  • Leader 将 C-old,new 发给所有 Follower 进行复制。 +
      +
    • 如果 C-old,new 被半数以上节点同步,则此配置已提交,之后遵循 Raft 安全性机制, 只有拥有 C-old,new 日志条目的服务器才有可能被选为新 Leader
    • +
    • 如果半数同步前 Leader 崩溃,新 Leader 可能有 C-old,new 也可能没有,若没有则退回老配置重试更新即可
    • +
    • 在这一时期,C-new 不会有任何影响。
    • +
    +
  • +
  • C-old,new 已提交后,C-old 已不会产生影响,Leader 再创建和提交 C-new 就是安全的了。
  • +
+

在整个过程中 没有哪个时候让 C-old 和 C-new 同时产生影响 ,保证了安全性。

+

img

+

6.3 问题讨论

+
    +
  • 没有存储任何的日志条目新节点加入,复制日志条目需要时间,此时无法作为提交和选举决策的节点。 +
      +
    • 新节点设置保护期,此期间以没有投票权身份加入到集群中来,不参加选举投票和日志提交决策,直到日志同步完毕。
    • +
    +
  • +
  • Leader 不是新配置成员。 +
      +
    • Leader 在 提交了 C-new 日志之后主动退位 (回到 Follower 状态)。并且在 复制提交 C-new 时自己不算半数之一
    • +
    +
  • +
  • 被移除的服务器未关闭,可能会扰乱集群。因为它们不再收到心跳,就会一直超时发起带有新任期号的选举。 +
      +
    • 集群中节点在 未达到选举超时时间前,不响应 RV RPC 。即如果当前 Leader 能够在超时时间内发送心跳,Follwer 就能确认当前 Leader 存在而不响应新的投票请求。
    • +
    +
  • +
+

7. 日志压缩

+

7.1 快照基本思路

+

日志不能无限增长, Snapshotting快照 )是最简单的压缩方法。在快照系统中,整个系统的状态都以快照的形式写入到稳定的持久化存储中,那个时间点之前的日志全部丢弃。

+

增量压缩 ,例如日志清理或者日志结构合并树也可行,这些方法每次只对一小部分数据进行操作,分散了负载压力。首先,选择一个已经积累的大量已经被删除或者被覆盖对象的区域,然后重写那个区域还活跃的对象,之后释放那个区域。

+

增量压缩需要增加复杂的机制来实现,而快照总是简单操作整个数据集合,简化了这个问题。日志清除方法需要修改 Raft,但是 状态机可以使用和快照相同的接口实现 LSM tree(日志结构合并树)

+

+

上图展示了 Raft 中快照的基本思路:

+
    +
  • 每个服务器独立的创建快照 ,只包括已经被提交的日志。大部分由状态机将当前状态写入到快照中,也包括少量元数据:
  • +
  • lastIncludedIndex:被快照取代的最后的条目在日志中的索引值(状态机最后应用的日志)
  • +
  • lastIncludedTerm:该条目的任期号
  • +
+

保留这些数据是为了支持快照后第一个 AE RPC 时的一致性检查,因为这个条目需要前一日志条目的索引值和任期号。

+
    +
  • 为了支持集群成员更新(第 6 节),快照中也将 最后的集群配置作为最后一个状态条目存下来 。一旦服务器完成一次快照,他就可以删除最后索引位置之前的所有日志和快照了。
  • +
+

7.2 InstallSnapshot RPC

+

Leader 必须偶尔 通过 RPC 发送快照给一些落后的 Follower 。一般发生于当 Leader 已经删除下一条需要发送给某 Follower 的日志条目的时候。例如一个运行非常缓慢的 Follower 或者新加入集群的服务器(第 6 节),这时让这个 Follower 更新到最新的状态的方式就是通过网络把快照发送给他们。

+

当 Follower 接收到 IS RPC 时,自己决定对于已经存在的日志该如何处理。

+
    +
  • 通常快照会 包含没有在接收者日志中存在的信息 。此时跟随者 丢弃其整个日志,全部被快照取代 ,即使包含与快照冲突的未提交条目。
  • +
  • 如果接收到的 快照是自己日志的前面部分 (由于网络重传或者错误),那么被快照包含的条目将会被全部删除,但是 快照后面的条目仍然有效,必须保留
  • +
+

pSu1L80.png

+

由 Leader 调用,将快照的分块发送给 Follower。Leader 总是按顺序发送分块。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数解释
term领导人的任期号
leaderId领导人的 Id,以便于跟随者重定向请求
lastIncludedIndex快照中包含的最后日志条目的索引值
lastIncludedTerm快照中包含的最后日志条目的任期号
offset分块在快照中的字节偏移量
data[]原始数据
done如果这是最后一个分块则为 true
+ + + + + + + + + + + + + +
返回结果解释
term当前任期号(currentTerm),便于领导人更新自己
+

接收者实现

+
    +
  1. 如果 term < currentTerm 就立即回复
  2. +
  3. 如果是第一个分块(offset = 0)就创建一个新的快照
  4. +
  5. 在指定偏移量写入数据
  6. +
  7. 如果 done = false,则继续等待更多的数据
  8. +
  9. 保存快照文件,丢弃具有较小索引的任何现有或部分快照
  10. +
  11. 如果现存的日志条目与快照中最后包含的日志条目具有相同的索引值和任期号,则保留其后的日志条目并进行回复
  12. +
  13. 丢弃整个日志
  14. +
  15. 使用快照重置状态机(并加载快照的集群配置)
  16. +
+

7.3 问题讨论

+

这种快照的方式背离了 Raft 的强 Leader 原则,因为 Follower 可以在 Leader 不知情情况下创建快照,但是这是值得的。Leader 的存在,是为了解决在达成一致性的时候的冲突,创建快照的时候一致性已经达成,不存在冲突了,所以没有 Leader 也是可以的。数据依然是从 Leader 传给 Follower,只是Follower 可以重新组织他们的数据。

+

而只有 Leader 创建快照,发送给所有的 Follower 的方案有三个问题:

+
    +
  • 浪费网络带宽并且延缓了快照处理的时间,Follower 已有快照所需信息显然更经济。
  • +
  • Leader 的实现会更加复杂。例如需要发送快照的同时并行的将新的日志条目发送给跟随者,这样才不会阻塞新的客户端请求。
  • +
+

还有两个问题影响快照性能:

+
    +
  • 什么时候应该创建快照?过于频繁会浪费大量的磁盘带宽和其他资源;频率太低要承受耗尽存储容量的风险,也增加了从日志重建的时间。 +
      +
    • 日志大小达到一个固定大小的时候就创建一次快照 。如果这个阈值设置的显著大于期望的快照的大小,那么快照对磁盘压力的影响就会很小了。
    • +
    +
  • +
  • 写入快照需要花费显著的一段时间,并且我们还不希望影响到正常操作,如何处理? +
      +
    • 写时复制的技术 ,这样新的更新就可以被接收而不影响到快照。具有函数式数据结构的状态机天然支持这样的功能。另外,操作系统的写时复制技术的支持(如 Linux 上的 fork)可以被用来创建完整的状态机的内存快照(我们的实现就是这样的)。
    • +
    +
  • +
+

8. 客户端交互

+

这一节将介绍客户端是如何和 Raft 进行交互的,包括:

+
    +
  • 客户端如何发现 Leader
  • +
  • Raft 如何支持线性化语义
  • +
+

这些问题对于所有基于一致性的系统都存在,并且 Raft 的解决方案和其他的也差不多。

+
    +
  • 客户端发送所有请求都要给 Leader 。 +
      +
    • 第一次通信会 随机联系一个节点 ,如果不是 Leader ,会被拒绝并提供最近接收的 Leader 信息(AE RPC 包含 Leader 地址),即 重定向
      +如果 Leader 宕机,请求超时,客户重试即可。
    • +
    +
  • +
  • Raft 的目标是要实现线性化语义 (每次操作立即执行,在调用和收到回复之间只执行一次) +
      +
    • 若 Leader 提交了客户端的操作日志,在回复客户端之前宕机,客户端重试。此时该指令可能执行两次。
      +解决方案是 客户端对每条指令赋予唯一序列号,状态机接受的序列号被执行的指令直接返回结果
    • +
    +
  • +
  • 只读操作可以不需要记录日志,但是旧 Leader 响应客户端时可能已经卸任,此时返回的是脏数据。需要两个额外机制 保证不返回脏数据
  • +
+
    +
  1. Leader 必须有关于被提交日志的最新信息,刚上任时可能不知道哪些已提交,所以需要提交一个 no-op(空命令) 日志条目。
  2. +
  3. Leader 在响应选举请求前,检查自己是否已被卸任。只需要和集群中大多数节点交换一次心跳信息即可。
  4. +
+

可选项: Leader 可以通过心跳机制实现租约机制 ,但是这种方法依赖时间来保证安全性(假设时间误差是有界的)。

+

9. 算法实现与评估

+

10. 相关工作

+

11. 结论

+

算法的设计通常以正确性、效率和简洁性为主要目标。虽然这些都是有价值的目标,但我们相信可理解性同样重要。在开发人员将算法转化为实际实现之前,其他任何目标都不能实现,而实际实现将不可避免地偏离和扩展发布的形式。除非开发人员对算法有深刻的理解,并能对算法有直观的认识,否则他们很难在实现中保留算法理想的特性。

+

在本文中,我们讨论了分布式共识的问题,在这个问题上,一个被广泛接受但难以理解的算法:Paxos,多年来一直让学生和开发人员非常挣扎。我们开发了一种新的算法:Raft,我们已经证明它比 Paxos 更容易理解。我们也相信 Raft 会为系统建设提供更好的基础。将可理解性作为主要设计目标改变了我们处理 Raft 设计的方式。随着设计的进展,我们发现自己反复使用了一些技术,比如分解问题和简化状态空间。这些技术不仅提高了 Raft 的可理解性,而且使我们更容易证实它的正确性。

+

LEC 5

+

模式

+

前面的系统都有单点故障:例如Coordinator、Master等等。因为要避免脑裂问题,因此并不设计成分布式的。

+

这种在一般情况下是没有问题的,出错的概率很小,即使出错了也可以在很短的时间内恢复回来。

+

Raft协议就是处理这种类型的问题,不允许单点故障产生,即使产生了也会更快恢复。

+

客户端访问两台服务器,一台得到了响应,另一台没有得到响应,如果另一台服务器挂掉了最好,但是如果仅仅是网络不通,会造成网络分区的问题,也就是脑裂,导致服务器不一致。因此前面的方案中都使用单点服务器的方式。

+

网络分区问题

+

处理原则:少数服从多数

+

客户端的操作需要在大多数服务器都成功,否则一直等待恢复,这样可以实现强一致性

+

大多数:全部服务器,无论是开机的还是停机的,需要获得一半以上的服务器同意

+

两种前协议:Paxos和View-stamped replication

+

Raft

+

构建复制状态机

+

pSUHKcd.md.png

+

步骤:

+
    +
  1. 客户端发送操作给Leader的K/V服务器
  2. +
  3. K/V服务器将操作传递给Raft
  4. +
  5. Raft写入日志
  6. +
  7. Raft与其他服务器通信传送日志
  8. +
  9. 其他服务器发送响应给Leader
  10. +
  11. Leader提交操作(其他的Followers需要等到下一次交互才确认前面的操作并提交)
  12. +
  13. 操作按照顺序传送到K/V服务器
  14. +
  15. K/V服务器执行操作
  16. +
  17. Leader返回操作结果给客户端
  18. +
+

如果失败,需要选举新的Leader,重试操作

+

日志

+

为什么需要日志?

+

K/V服务器是保留操作表的,为什么还需要日志呢?

+
    +
  • 重传:Leader发送的时候可能会丢失,因此Leader必须保留所有的日志条目从而具有重传的能力
  • +
  • 顺序:每一个操作需要按照顺序传送,日志可以非常方便做到
  • +
  • 持久化:服务器都有可能挂掉,因此需要持久化保留所有的操作
  • +
  • 空间:需要空间进行一些试探性的操作,日志可以很方便做到
  • +
+

最终需要保证日志在所有的服务器上都是相同的

+

日志基本结构

+

日志条目包括序号、操作和Leader的任期(隐含表示了这个日志条目是哪个Leader追加的)

+

选举Leader

+

Follower如果接收不到Leader发送的周期性的心跳信号,就认为Leader挂掉了,开始选举Leader

+

具体实施:Follower自己有计时器,如果在一段的时间之内既没有接收到新的日志条目,也没有接收到Leader的心跳信号,则认为选举超时,开始进行选举。

+
    +
  1. 增加任期号,并给自己投票
  2. +
  3. 联系其他的服务器(包括Follower和Leader)
  4. +
  5. 收到大多数的选票,成为Leader
  6. +
+

此时新的Leader的任期号要大于原来的Leader的任期号,如果此时客户端与旧的Leader进行交互,Leader给新的Leader发送了增加日志的请求,会被拒绝,发送给旧的Leader自己的任期号。旧的Leader发现任期号比自己大,不会再成为Leader。从而避免了脑裂的问题。

+

挑战:两个Follower几乎同时发起选举,选不出Leader(分裂选举)

+

因此设置选举超时时间,但是是随机的,如果选不出Leader,经过一段时间后就不会同时开始选举Leader,就可以最终选出Leader了。

+

选举超时时间

+
    +
  • 不能小于心跳信号的间隔时间
  • +
  • 三到四次RPC的时间
  • +
  • 随机值越大停机的时间越长,越小可能仍然会选举失败
  • +
  • 250-300ms
  • +
+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-LEC 5 Fault Tolerance-Raft-1
+
https://zhangzhao219.github.io/2023/01/10/6.824/Distributed-Systems-MIT-6.824-LEC-5/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月10日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/13/Software-Commands/index.html b/2023/01/13/Software-Commands/index.html new file mode 100644 index 000000000..bec3a8f73 --- /dev/null +++ b/2023/01/13/Software-Commands/index.html @@ -0,0 +1,1259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 常用软件常用命令 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

常用软件常用命令

+ + +
+ +

常用软件常用命令

+ +

产生随机字符串

+
head -c 32 /dev/random | base64
+

sudo免密码设置

+
sudo visudo
+

压缩相关

+
tar -cvf temp.tar temp.txt temp/
+tar -xvf temp.tar -C temp/
+tar cvzf - pic | split -b 10m -d - pic
+cat pic* > pic.tar.gz
+tar xvzf pic.tar.gz
+

Conda

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
描述命令
查看都有什么环境conda info --envs
查看当前环境有什么包conda list
创建环境conda create -n env_name python=version package_names
安装包conda install name=version
离线安装包conda install --use-local name
导出当前环境conda env export > name.yaml
导出base环境需要更换名称conda create -n new_name --clone base
复制后导入conda env create -f name.yaml
删除环境下的某个包conda env remove -n your_env_name package_name
删除环境conda env remove --name your_env_name
打包环境conda pack -n my_env -o out_name.tar.gz
利用打包的环境复现tar -xzf my_env.tar.gz -C my_env && source my_env/bin/activate
+

Anaconda环境离线迁移移植

+

Linux 系统安装

+
sudo apt update
+sudo apt install vim
+sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
+sudo vim /etc/apt/sources.list
+sudo apt update
+sudo apt upgrade
+sudo apt install python3-pip
+
+
+sudo mkdir ~/.pip
+cd ~/.pip
+sudo vim pip.conf
+# [global]
+# index-url=https://mirrors.aliyun.com/pypi/simple/ 
+# https://pypi.org/simple
+
+
+vim ~/.bashrc
+export PATH=~/mypath/bin:$PATH
+source ~/.bashrc
+

添加%zhangzhao ALL=(ALL:ALL) NOPASSWD: ALL 到最后一行 , 后 ctrl+o, 回车 , ctrl+x 退出

+

VSCode

+

How can I install vscode-server in linux offline

+

科学上网相关

+

V2ray安装

+

安装说明:https://www.v2fly.org/guide/install.html

+

脚本下载地址:https://github.com/v2fly/fhs-install-v2ray

+

执行脚本前首先临时更改hosts,避免github连接不上的情况

+

参考仓库:https://github.com/521xueweihan/GitHub520

+

执行脚本下载安装V2ray

+
bash install-release.sh
+

安装后的文件位置:

+
installed: /usr/local/bin/v2ray
+installed: /usr/local/share/v2ray/geoip.dat
+installed: /usr/local/share/v2ray/geosite.dat
+installed: /usr/local/etc/v2ray/config.json
+installed: /var/log/v2ray/
+installed: /var/log/v2ray/access.log
+installed: /var/log/v2ray/error.log
+installed: /etc/systemd/system/v2ray.service
+installed: /etc/systemd/system/v2ray@.service
+

按照上面的配置文件路径写入配置文件(可以直接从Windows客户端中copy过来)

+

V2ray命令:

+
# 启动V2ray
+systemctl start v2ray
+# 检查V2ray状态
+systemctl status v2ray
+# 设置V2ray开机自启动
+systemctl enable v2ray
+

测试:

+
curl -x socks5://127.0.0.1:10808 https://www.google.com -v
+

clash安装

+

下载地址:https://github.com/Dreamacro/clash/releases/tag/v1.12.0

+

解压并赋予权限:

+
gzip -d clash-linux-amd64-v1.11.4.gz
+chmod a+x clash-linux
+

有的代理服务商会直接给出配置文件config.yaml,如果没有,可以将订阅链接直接粘贴在浏览器网址栏,然后搜索,会直接下载下来文件或者展示出配置文件,如果搜索到的是一大堆字符则需要在订阅链接的后面添加 &flag=clash ,然后会下载下来一个文件,将其更名为config.yaml即可

+

然后替换~/.config/clash下自动生成的config.yaml,删除Country.mmdb文件,然后再次执行 ./clash-linux

+

即可以使用

+

释放9090端口后可以通过Web端查看:http://clash.razord.top

+

(WSL2 git push时候可能会遇到错误,解决方法:将下述代码粘贴到~/.ssh/config文件中)

+
Host github.com
+Hostname ssh.github.com
+Port 443
+

proxychains安装

+

安装proxychains从而避免全局代理

+
apt install proxychains4
+

配置文件:(/etc/proxychains.conf)

+
# proxychains.conf  VER 4.x
+#
+#        HTTP, SOCKS4a, SOCKS5 tunneling proxifier with DNS.
+
+
+# The option below identifies how the ProxyList is treated.
+# only one option should be uncommented at time,
+# otherwise the last appearing option will be accepted
+#
+# dynamic_chain
+#
+# Dynamic - Each connection will be done via chained proxies
+# all proxies chained in the order as they appear in the list
+# at least one proxy must be online to play in chain
+# (dead proxies are skipped)
+# otherwise EINTR is returned to the app
+#
+strict_chain
+#
+# Strict - Each connection will be done via chained proxies
+# all proxies chained in the order as they appear in the list
+# all proxies must be online to play in chain
+# otherwise EINTR is returned to the app
+#
+#round_robin_chain
+#
+# Round Robin - Each connection will be done via chained proxies
+# of chain_len length
+# all proxies chained in the order as they appear in the list
+# at least one proxy must be online to play in chain
+# (dead proxies are skipped).
+# the start of the current proxy chain is the proxy after the last
+# proxy in the previously invoked proxy chain.
+# if the end of the proxy chain is reached while looking for proxies
+# start at the beginning again.
+# otherwise EINTR is returned to the app
+# These semantics are not guaranteed in a multithreaded environment.
+#
+#random_chain
+#
+# Random - Each connection will be done via random proxy
+# (or proxy chain, see  chain_len) from the list.
+# this option is good to test your IDS :)
+
+# Make sense only if random_chain or round_robin_chain
+#chain_len = 2
+
+# Quiet mode (no output from library)
+# quiet_mode
+
+# Proxy DNS requests - no leak for DNS data
+proxy_dns
+
+# set the class A subnet number to use for the internal remote DNS mapping
+# we use the reserved 224.x.x.x range by default,
+# if the proxified app does a DNS request, we will return an IP from that range.
+# on further accesses to this ip we will send the saved DNS name to the proxy.
+# in case some control-freak app checks the returned ip, and denies to 
+# connect, you can use another subnet, e.g. 10.x.x.x or 127.x.x.x.
+# of course you should make sure that the proxified app does not need
+# *real* access to this subnet. 
+# i.e. dont use the same subnet then in the localnet section
+#remote_dns_subnet 127 
+#remote_dns_subnet 10
+remote_dns_subnet 224
+
+# Some timeouts in milliseconds
+tcp_read_time_out 15000
+tcp_connect_time_out 8000
+
+### Examples for localnet exclusion
+## localnet ranges will *not* use a proxy to connect.
+## Exclude connections to 192.168.1.0/24 with port 80
+# localnet 192.168.1.0:80/255.255.255.0
+
+## Exclude connections to 192.168.100.0/24
+# localnet 192.168.100.0/255.255.255.0
+
+## Exclude connections to ANYwhere with port 80
+# localnet 0.0.0.0:80/0.0.0.0
+
+## RFC5735 Loopback address range
+## if you enable this, you have to make sure remote_dns_subnet is not 127
+## you'll need to enable it if you want to use an application that 
+## connects to localhost.
+# localnet 127.0.0.0/255.0.0.0
+
+## RFC1918 Private Address Ranges
+# localnet 10.0.0.0/255.0.0.0
+# localnet 172.16.0.0/255.240.0.0
+# localnet 192.168.0.0/255.255.0.0
+
+# ProxyList format
+#       type  ip  port [user pass]
+#       (values separated by 'tab' or 'blank')
+#
+#       only numeric ipv4 addresses are valid
+#
+#
+#        Examples:
+#
+#            	socks5	192.168.67.78	1080	lamer	secret
+#		http	192.168.89.3	8080	justu	hidden
+#	 	socks4	192.168.1.49	1080
+#	        http	192.168.39.93	8080
+#
+#
+#       proxy types: http, socks4, socks5
+#        ( auth types supported: "basic"-http  "user/pass"-socks )
+#
+[ProxyList]
+# add proxy here ...
+# meanwile
+# defaults set to "tor"
+socks5 127.0.0.1 10808
+http 127.0.0.1 10809
+

需要走代理的命令在命令开头添加proxychains即可

+

全局代理:(似乎对软件内部,例如go没有作用)

+
export http_proxy=http://127.0.0.1:10809
+export https_proxy=https://127.0.0.1:10809
+

Go安装

+

下载安装

+

网站说明:https://golang.google.cn/doc/install

+
wget https://golang.google.cn/dl/go1.19.5.linux-amd64.tar.gz
+

删除旧版本并解压安装包:

+
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.5.linux-amd64.tar.gz
+

编辑配置文件,增加环境变量:

+
vim ~/.bashrc
+export PATH=$PATH:/usr/local/go/bin
+source ~/.bashrc
+

验证安装:

+
go version
+

配置

+

查看配置:

+
go env
+

修改配置:

+
go env -w GO111MODULE=on
+go env -w GOPROXY=https://goproxy.io,direct
+

拉取私有仓库的包

+
go env -w GOPRIVATE=gitlab.appshahe.com
+

除配置私有仓库的地址外,还需要将走http或者https的协议转到git协议上

+

具体命令:

+
git config --global url."git@gitlab.appshahe.com:".insteadOf "https://gitlab.appshahe.com/"
+git config --global url."git@gitlab.appshahe.com:".insteadOf "http://gitlab.appshahe.com/"
+

具体的更改会体现在 ~/.gitconfig里面

+

MySQL

+

docker安装直接可以远程访问,不需要任何配置操作

+
apt install mysql-server
+

运行mysql服务并查看是否正在运行

+
service mysql start
+service mysql status
+

刚开始安装不能使用用户名和密码访问,需要更换为原来的密码验证方式

+
mysql
+ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';
+FLUSH PRIVILEGES;
+

创建数据库:

+
CREATE DATABASE simpledy
+

增加远程访问的用户

+

取消bind-address=127.0.0.1

+
vim /etc/mysql/mysql.conf.d/mysqld.cnf
+

密码生成为随机字符串:

+
head -c 8 /dev/random | base64
+

创建用户:

+
CREATE USER 'dymysql'@'%' IDENTIFIED BY 'gxnw21XxRhY';
+

更改密码验证方式:

+
ALTER USER 'dymysql'@'%' IDENTIFIED WITH mysql_native_password BY 'gxnw21XxRhY';
+

授予用户某个数据库的全部权限:

+
GRANT ALL PRIVILEGES ON `simpledy`.* TO `dymysql`@`%` WITH GRANT OPTION;
+

撤销某个用户对某个数据库的全部权限:

+
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'dymysql';
+

刷新缓存:

+
FLUSH PRIVILEGES;
+

展示某个用户的权限:

+
SHOW GRANTS FOR 'dymysql'@'%';
+

查看已有用户以及是否可以远程登录:

+
select host,user,plugin from mysql.user;
+

Redis

+

docker安装配置成功

+
apt install redis-server
+

运行并查看是否正在运行

+
service redis-server start
+service redis-server status
+

设置redis密码

+

打开redis配置文件 /etc/redis/redis.conf

+

找到requirepass,修改即可

+

配置 Redis 远程访问

+

默认情况下,Redis 不允许远程连接。只能从127.0.0.1(localhost)连接 Redis 服务器

+

打开redis配置文件 /etc/redis/redis.conf

+

注释掉 bind 127.0.0.1 ::1

+

关闭保护模式 protected-mode no

+

重启Redis服务:service redis-server restart

+

(注意WSL的ip要到WSL里面去看)

+

RabbitMQ

+

官网安装脚本:https://www.rabbitmq.com/install-debian.html#apt-cloudsmith

+

注意修改apt-get为apt,将软件源设置为对应版本(如Ubuntu22.04为jammy)

+

查看安装状态:

+
service rabbitmq-server status
+

打开管理界面:

+
rabbitmq-plugins enable rabbitmq_management
+

通过http://localhost:15672/#/进行查看

+

默认的guest用户,密码为guest,具有超级管理员权限,无法远程登录

+

创建用户并设置密码:

+
add_user root QxdkQeiIUNY
+

管理用户角色:

+
    +
  • 超级管理员(administrator):可登陆管理控制台(启用management plugin的情况下),可查看所有的信息,并且可以对用户,策略(policy)进行操作。
  • +
  • 监控者(monitoring):可登陆管理控制台(启用management plugin的情况下),同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)。
  • +
  • 策略制定者(policymaker):可登陆管理控制台(启用management plugin的情况下), 同时可以对policy进行管理。但无法查看节点的相关信息。
  • +
  • 普通管理者(management):仅可登陆管理控制台(启用management plugin的情况下),无法看到节点信息,也无法对策略进行管理。
  • +
  • 其他:无法登陆管理控制台,通常就是普通的生产者和消费者。(最后项目中使用的)
  • +
+
rabbitmqctl set_user_tags root administrator
+

查看当前的用户及角色:

+
rabbitmqctl list_users
+

不需要开启远程连接,自动支持

+

然后进入到管理页面中,对virtual hosts进行设置(相当于数据库中的db)

+

然后即可使用程序等跑通

+

FFmpeg

+
apt install ffmpeg
+

Nginx

+
apt install nginx
+

配置文件:/etc/nginx/nginx.conf

+

增加mp4支持:

+
apt install nginx-extras
+

vsftpd

+
apt install vsftpd
+

Protobuf

+

下载protobuf官方的protoc工具(tar.gz版本

+

编译安装:

+
# 安装需要的工具包
+apt install autoconf automake libtool curl make g++ unzip
+# 解压安装包
+tar xvf protobuf-21.12.tar.gz && cd protobuf-21.12
+# 编译安装
+./autogen.sh
+./configure
+make && make install
+ldconfig
+# 验证安装
+protoc --version
+

安装go语言插件:

+
go get -u google.golang.org/protobuf/cmd/protoc-gen-go
+go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
+

将执行文件添加到环境变量中:

+
export PATH=$PATH:/root/go/bin
+

执行:

+
protoc --go_out=. video.proto
+

Consul

+

下载地址:https://developer.hashicorp.com/consul/downloads

+

解压后直接执行即可

+

Docker

+

官网安装方法

+

核心思想:

+
    +
  1. Add Docker’s official GPG key
  2. +
+
root@hecs-296470:/etc/apt/keyrings# cd /etc/apt/keyrings
+root@hecs-296470:/etc/apt/keyrings# ls
+docker.gpg
+
    +
  1. 添加可以下载docker的源
  2. +
+
root@hecs-296470:/etc/apt/sources.list.d# ls
+docker.list
+root@hecs-296470:/etc/apt/sources.list.d# cat docker.list
+deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu   jammy stable
+root@hecs-296470:/etc/apt/sources.list.d#
+
    +
  1. 安装docker
  2. +
+
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+

换源:

+
vim /etc/docker/daemon.json
+

写入源:

+
{
+    "registry-mirrors": [
+	"https://hub-mirror.c.163.com",
+	"https://ustc-edu-cn.mirror.aliyuncs.com",
+	"https://ghcr.io",
+	"https://mirror.baidubce.com"
+    ]
+}
+

重启docker:

+
systemctl daemon-reload
+systemctl restart docker
+

环境相关

+

准备重装编程环境,以Docker为基础,既能开发,又能方便部署,同时不损害原有的其他环境

+

但是Docker Desktop坑点太多,且占用资源巨大,因此不安装Windows环境下面的Docker,而是在WSL内部安装Docker,VSCode通过SSH方式跨过WSL访问容器。

+

Docker安装

+

与上面的Docker安装基本相同,不过注意每一次重启WSL的时候要手动重启Docker,否则无法使用Docker

+
service docker start
+

网络桥接

+

由于WSL的ip会总变化,这里准备配桥接模式,我的理解是WSL与主机的地位相同,在内网中都有自己的ip,这样无论是互相访问还是访问外网都没有什么问题。

+

参考资料:

+

官方文档

+

WSL2 网络的最终解决方案

+

WSL2 静态IP(固定IP)不需要自动化脚本的设置方案

+

常用命令

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
描述命令
查询容器sudo docker ps -a
删除容器sudo docker rm 容器ID
查询镜像sudo docker images
删除镜像sudo docker rmi 镜像ID(要先删除掉容器才能删除掉镜像)
拉取镜像sudo docker pull python:3.8.13(去dockerhub上找合适的版本)
根据镜像启动容器并挂载数据docker run -v 绝对路径:/mnt --gpus all --shm-size=6g -it python:3.8.13 /bin/bash
启动已经停止的容器sudo docker start ID
进入某个容器的终端sudo docker exec -it ID /bin/bash
将容器转为镜像并上传到dockerhub-登录docker login
将容器转为镜像并上传到dockerhub-提交sudo docker commit 容器ID zhangzhao219/仓库名(也是将容器写回到镜像中的操作)
将容器转为镜像并上传到dockerhub-打标签sudo docker tag zhangzhao219/仓库名 zhangzhao219/TAG
将容器转为镜像并上传到dockerhub-上传sudo docker push zhangzhao219/仓库名:TAG
导出容器到文件sudo docker export -o *.tar 容器ID
从文件导入容器(会直接变为镜像)sudo docker import IR.tar 名称
+ + +
+ +
+
+ + + + + + +
+
+
常用软件常用命令
+
https://zhangzhao219.github.io/2023/01/13/Software-Commands/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/15/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day01/index.html b/2023/01/15/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day01/index.html new file mode 100644 index 000000000..cf82bfd6e --- /dev/null +++ b/2023/01/15/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day01/index.html @@ -0,0 +1,1733 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 语言基础 - 基础语法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go 语言基础 - 基础语法

+ + +
+ +

Go 语言基础 - 基础语法

+ +

Go 语言基础 - 基础语法

+

概述

+

本节课程主要分为四个方面:

+
    +
  1. Go 语言简介
  2. +
  3. Go 语言开发入门,包括开发环境配置、基础语法、标准库
  4. +
  5. Go 实战,包括三个实战项目
  6. +
+

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

+

课前

+

安装 Go 语言

+
    +
  1. 访问 go.dev/ ,点击 Download ,下载对应平台安装包,安装即可
  2. +
  3. 如果无法访问上述网址,可以改为访问 studygolang.com/dl 下载安装
  4. +
  5. 如果访问 github 速度比较慢,建议配置 go mod proxy,参考 goproxy.cn/ 里面的描述配置,下载第三方依赖包的速度可以大大加快
  6. +
+

配置 Go 语言开发环境

+

可以选择安装 VS Code , 或者 Goland ,对于 VS Code,需要安装 Go 插件

+

下载课程示例代码

+
    +
  1. Windows 平台建议安装 git,其它系统自带,安装教程
  2. +
  3. 打开 github.com/wangkechun/… 克隆课程示例项目
  4. +
  5. 进入课程示例项目代码目录,运行 go run example/01-hello/main.go 如果正确输出 hello world,则说明环境配置正确
  6. +
+

学习 Go 语言基础语法

+

空余时间阅读 Go语言圣经(中文版)

+

课程笔记

+

课程链接:

+ +

Go语言的优势

+
    +
  1. 高性能、高并发:不需要另外的库对并发进行支持
  2. +
  3. 语法简单、学习曲线平缓:一周时间即可上手
  4. +
  5. 丰富的标准库:与Python一样有大量的标准库,非常稳定
  6. +
  7. 完善的工具链:保证代码正确稳定运行
  8. +
  9. 静态链接:只需要编译后的一个文件就可以运行
  10. +
  11. 快速编译:静态语言几乎最快的编译速度
  12. +
  13. 跨平台:几乎支持所有设备
  14. +
  15. 垃圾回收:无需考虑内存的分配释放
  16. +
+

基础语法

+

1. Hello World

+
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	fmt.Println("hello world")
+}
+
+

2. 变量

+

注意常量没有类型,会根据使用的上下文自动推断类型

+
package main
+
+import (
+	"fmt"
+	"math"
+)
+
+func main() {
+
+	var a = "initial"
+
+	var b, c int = 1, 2
+
+	var d = true
+
+	var e float64
+
+	f := float32(e)
+
+	g := a + "foo"
+	fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
+	fmt.Println(g)                // initialapple
+
+	const s string = "constant"
+	const h = 500000000
+	const i = 3e20 / h
+	fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
+}
+
+

3. 循环

+
package main
+
+import "fmt"
+
+func main() {
+
+	i := 1
+	for {
+		fmt.Println("loop")
+		break
+	}
+	for j := 7; j < 9; j++ {
+		fmt.Println(j)
+	}
+
+	for n := 0; n < 5; n++ {
+		if n%2 == 0 {
+			continue
+		}
+		fmt.Println(n)
+	}
+	for i <= 3 {
+		fmt.Println(i)
+		i = i + 1
+	}
+}
+
+

4. if else

+
package main
+
+import "fmt"
+
+func main() {
+
+	if 7%2 == 0 {
+		fmt.Println("7 is even")
+	} else {
+		fmt.Println("7 is odd")
+	}
+
+	if 8%4 == 0 {
+		fmt.Println("8 is divisible by 4")
+	}
+
+	if num := 9; num < 0 {
+		fmt.Println(num, "is negative")
+	} else if num < 10 {
+		fmt.Println(num, "has 1 digit")
+	} else {
+		fmt.Println(num, "has multiple digits")
+	}
+}
+
+

5. switch

+

默认不需要添加break

+

可以使用任意的变量类型

+
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+
+	a := 2
+	switch a {
+	case 1:
+		fmt.Println("one")
+	case 2:
+		fmt.Println("two")
+	case 3:
+		fmt.Println("three")
+	case 4, 5:
+		fmt.Println("four or five")
+	default:
+		fmt.Println("other")
+	}
+
+	t := time.Now()
+	switch {
+	case t.Hour() < 12:
+		fmt.Println("It's before noon")
+	default:
+		fmt.Println("It's after noon")
+	}
+}
+
+

6. 数组

+

真实场景下很少用,一般使用切片

+
package main
+
+import "fmt"
+
+func main() {
+
+	var a [5]int
+	a[4] = 100
+	fmt.Println("get:", a[2])
+	fmt.Println("len:", len(a))
+
+	b := [5]int{1, 2, 3, 4, 5}
+	fmt.Println(b)
+
+	var twoD [2][3]int
+	for i := 0; i < 2; i++ {
+		for j := 0; j < 3; j++ {
+			twoD[i][j] = i + j
+		}
+	}
+	fmt.Println("2d: ", twoD)
+}
+
+

7. 切片

+
package main
+
+import "fmt"
+
+func main() {
+
+	s := make([]string, 3)
+	s[0] = "a"
+	s[1] = "b"
+	s[2] = "c"
+	fmt.Println("get:", s[2])   // c
+	fmt.Println("len:", len(s)) // 3
+
+	s = append(s, "d")
+	s = append(s, "e", "f")
+	fmt.Println(s) // [a b c d e f]
+
+	c := make([]string, len(s))
+	copy(c, s)
+	fmt.Println(c) // [a b c d e f]
+
+	fmt.Println(s[2:5]) // [c d e]
+	fmt.Println(s[:5])  // [a b c d e]
+	fmt.Println(s[2:])  // [c d e f]
+
+	good := []string{"g", "o", "o", "d"}
+	fmt.Println(good) // [g o o d]
+}
+
+

8. map

+

实际中使用最频繁,完全无序

+
package main
+
+import "fmt"
+
+func main() {
+	m := make(map[string]int)
+	m["one"] = 1
+	m["two"] = 2
+	fmt.Println(m)           // map[one:1 two:2]
+	fmt.Println(len(m))      // 2
+	fmt.Println(m["one"])    // 1
+	fmt.Println(m["unknow"]) // 0
+
+	r, ok := m["unknow"]
+	fmt.Println(r, ok) // 0 false
+
+	delete(m, "one")
+
+	m2 := map[string]int{"one": 1, "two": 2}
+	var m3 = map[string]int{"one": 1, "two": 2}
+	fmt.Println(m2, m3)
+}
+
+

9. range

+
package main
+
+import "fmt"
+
+func main() {
+	nums := []int{2, 3, 4}
+	sum := 0
+	for i, num := range nums {
+		sum += num
+		if num == 2 {
+			fmt.Println("index:", i, "num:", num) // index: 0 num: 2
+		}
+	}
+	fmt.Println(sum) // 9
+
+	m := map[string]string{"a": "A", "b": "B"}
+	for k, v := range m {
+		fmt.Println(k, v) // b 8; a A
+	}
+	for k := range m {
+		fmt.Println("key", k) // key a; key b
+	}
+}
+
+

10. 函数

+

一般返回两个值,第一个值是真正需要的,第二个值是错误信息

+
package main
+
+import "fmt"
+
+func add(a int, b int) int {
+	return a + b
+}
+
+func add2(a, b int) int {
+	return a + b
+}
+
+func exists(m map[string]string, k string) (v string, ok bool) {
+	v, ok = m[k]
+	return v, ok
+}
+
+func main() {
+	res := add(1, 2)
+	fmt.Println(res) // 3
+
+	v, ok := exists(map[string]string{"a": "A"}, "a")
+	fmt.Println(v, ok) // A True
+}
+
+

11. 指针

+

对传入的参数进行修改

+

功能比较有限,不如C++丰富

+
package main
+
+import "fmt"
+
+func add2(n int) {
+	n += 2
+}
+
+func add2ptr(n *int) {
+	*n += 2
+}
+
+func main() {
+	n := 5
+	add2(n)
+	fmt.Println(n) // 5
+	add2ptr(&n)
+	fmt.Println(n) // 7
+}
+
+

12. 结构体

+

传入指针避免传递的开销过大,同时也可以对结构体进行修改

+
package main
+
+import "fmt"
+
+type user struct {
+	name     string
+	password string
+}
+
+func main() {
+	a := user{name: "wang", password: "1024"}
+	b := user{"wang", "1024"}
+	c := user{name: "wang"}
+	c.password = "1024"
+	var d user
+	d.name = "wang"
+	d.password = "1024"
+
+	fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
+	fmt.Println(checkPassword(a, "haha"))   // false
+	fmt.Println(checkPassword2(&a, "haha")) // false
+}
+
+func checkPassword(u user, password string) bool {
+	return u.password == password
+}
+
+func checkPassword2(u *user, password string) bool {
+	return u.password == password
+}
+
+

13. 结构体方法

+

相当于一个类成员函数

+

带指针就能对结构体进行修改

+
package main
+
+import "fmt"
+
+type user struct {
+	name     string
+	password string
+}
+
+func (u user) checkPassword(password string) bool {
+	return u.password == password
+}
+
+func (u *user) resetPassword(password string) {
+	u.password = password
+}
+
+func main() {
+	a := user{name: "wang", password: "1024"}
+	a.resetPassword("2048")
+	fmt.Println(a.checkPassword("2048")) // true
+}
+
+

14. 错误处理

+
package main
+
+import (
+	"errors"
+	"fmt"
+)
+
+type user struct {
+	name     string
+	password string
+}
+
+func findUser(users []user, name string) (v *user, err error) {
+	for _, u := range users {
+		if u.name == name {
+			return &u, nil
+		}
+	}
+	return nil, errors.New("not found")
+}
+
+func main() {
+	u, err := findUser([]user{{"wang", "1024"}}, "wang")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(u.name) // wang
+
+	if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
+		fmt.Println(err) // not found
+		return
+	} else {
+		fmt.Println(u.name)
+	}
+}
+
+

15. 字符串操作

+
package main
+
+import (
+	"fmt"
+	"strings"
+)
+
+func main() {
+	a := "hello"
+	fmt.Println(strings.Contains(a, "ll"))                // true
+	fmt.Println(strings.Count(a, "l"))                    // 2
+	fmt.Println(strings.HasPrefix(a, "he"))               // true
+	fmt.Println(strings.HasSuffix(a, "llo"))              // true
+	fmt.Println(strings.Index(a, "ll"))                   // 2
+	fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
+	fmt.Println(strings.Repeat(a, 2))                     // hellohello
+	fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
+	fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
+	fmt.Println(strings.ToLower(a))                       // hello
+	fmt.Println(strings.ToUpper(a))                       // HELLO
+	fmt.Println(len(a))                                   // 5
+	b := "你好"
+	fmt.Println(len(b)) // 6
+}
+
+

16. 字符串格式化

+

+和#号可以打印更为详细的信息

+
package main
+
+import "fmt"
+
+type point struct {
+	x, y int
+}
+
+func main() {
+	s := "hello"
+	n := 123
+	p := point{1, 2}
+	fmt.Println(s, n) // hello 123
+	fmt.Println(p)    // {1 2}
+
+	fmt.Printf("s=%v\n", s)  // s=hello
+	fmt.Printf("n=%v\n", n)  // n=123
+	fmt.Printf("p=%v\n", p)  // p={1 2}
+	fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
+	fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
+
+	f := 3.141592653
+	fmt.Println(f)          // 3.141592653
+	fmt.Printf("%.2f\n", f) // 3.14
+}
+
+

17. json

+

注意结构体要保证大写,小写传参的问题使用反射解决

+
package main
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type userInfo struct {
+	Name  string
+	Age   int `json:"age"`
+	Hobby []string
+}
+
+func main() {
+	a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
+	buf, err := json.Marshal(a)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(buf)         // [123 34 78 97...]
+	fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
+
+	buf, err = json.MarshalIndent(a, "", "\t")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(string(buf))
+
+	var b userInfo
+	err = json.Unmarshal(buf, &b)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
+}
+
+

18. 时间处理

+
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	now := time.Now()
+	fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
+	t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
+	t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
+	fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
+	fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
+	fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
+	diff := t2.Sub(t)
+	fmt.Println(diff)                           // 1h5m0s
+	fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
+	t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(t3 == t)    // true
+	fmt.Println(now.Unix()) // 1648738080
+}
+
+

19. 数字解析

+
package main
+
+import (
+	"fmt"
+	"strconv"
+)
+
+func main() {
+	f, _ := strconv.ParseFloat("1.234", 64)
+	fmt.Println(f) // 1.234
+
+	n, _ := strconv.ParseInt("111", 10, 64)
+	fmt.Println(n) // 111
+
+	n, _ = strconv.ParseInt("0x1000", 0, 64)
+	fmt.Println(n) // 4096
+
+	n2, _ := strconv.Atoi("123")
+	fmt.Println(n2) // 123
+
+	n2, err := strconv.Atoi("AAA")
+	fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
+}
+
+

20. 进程信息

+
package main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+)
+
+func main() {
+	// go run example/20-env/main.go a b c d
+	fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
+	fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
+	fmt.Println(os.Setenv("AA", "BB"))
+
+	buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(string(buf)) // 127.0.0.1       localhost
+}
+
+

实战案例

+

猜谜游戏

+
    +
  1. 生成随机数之前需要生成不同的随机种子,否则每一次运行都会输出相同的数字,一般使用时间戳来初始化
  2. +
+
rand.Seed(time.Now().UnixNano())
+secretNumber := rand.Intn(maxNum)
+
    +
  1. bufio.NewReader读取输入并对输入进行处理,是工程中比较常用的做法。注意读取后需要对字符串进行处理
  2. +
+
reader := bufio.NewReader(os.Stdin)
+input, err := reader.ReadString('\n')
+input = strings.Trim(input, "\r\n")
+

最终代码:

+
package main
+
+import (
+	"bufio"
+	"fmt"
+	"math/rand"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func main() {
+	maxNum := 100
+	rand.Seed(time.Now().UnixNano())
+	secretNumber := rand.Intn(maxNum)
+	// fmt.Println("The secret number is ", secretNumber)
+
+	fmt.Println("Please input your guess")
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		input, err := reader.ReadString('\n')
+		if err != nil {
+			fmt.Println("An error occured while reading input. Please try again", err)
+			continue
+		}
+		input = strings.Trim(input, "\r\n")
+
+		guess, err := strconv.Atoi(input)
+		if err != nil {
+			fmt.Println("Invalid input. Please enter an integer value")
+			continue
+		}
+		fmt.Println("You guess is", guess)
+		if guess > secretNumber {
+			fmt.Println("Your guess is bigger than the secret number. Please try again")
+		} else if guess < secretNumber {
+			fmt.Println("Your guess is smaller than the secret number. Please try again")
+		} else {
+			fmt.Println("Correct, you Legend!")
+			break
+		}
+	}
+}
+
+

命令行在线词典

+

抓包:找到翻译网站,提交一个翻译后去控制台抓包,然后copy as cURL,可以将这个请求转到本地进行运行

+

为了方便,可以将这个请求copy到一些在线将请求转换为Go代码的网站,最终得到可以直接运行的代码,运行代码获得与网页返回相同的结果。

+

将请求的部分单独提取出来,通过用户的输入进行序列化

+

解析返回的响应,进行反序列化提取真正需要的部分

+

返回的响应也使用在线工具转换为go代码,减少工作量

+

最后通过命令行读入即可

+

最终代码:

+
package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+)
+
+type DictRequest struct {
+	TransType string `json:"trans_type"`
+	Source    string `json:"source"`
+	UserID    string `json:"user_id"`
+}
+
+type DictResponse struct {
+	Rc   int `json:"rc"`
+	Wiki struct {
+		KnownInLaguages int `json:"known_in_laguages"`
+		Description     struct {
+			Source string      `json:"source"`
+			Target interface{} `json:"target"`
+		} `json:"description"`
+		ID   string `json:"id"`
+		Item struct {
+			Source string `json:"source"`
+			Target string `json:"target"`
+		} `json:"item"`
+		ImageURL  string `json:"image_url"`
+		IsSubject string `json:"is_subject"`
+		Sitelink  string `json:"sitelink"`
+	} `json:"wiki"`
+	Dictionary struct {
+		Prons struct {
+			EnUs string `json:"en-us"`
+			En   string `json:"en"`
+		} `json:"prons"`
+		Explanations []string      `json:"explanations"`
+		Synonym      []string      `json:"synonym"`
+		Antonym      []string      `json:"antonym"`
+		WqxExample   [][]string    `json:"wqx_example"`
+		Entry        string        `json:"entry"`
+		Type         string        `json:"type"`
+		Related      []interface{} `json:"related"`
+		Source       string        `json:"source"`
+	} `json:"dictionary"`
+}
+
+func query(word string) {
+	client := &http.Client{}
+	request := DictRequest{TransType: "en2zh", Source: word}
+	buf, err := json.Marshal(request)
+	if err != nil {
+		log.Fatal(err)
+	}
+	var data = bytes.NewReader(buf)
+	req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
+	if err != nil {
+		log.Fatal(err)
+	}
+	req.Header.Set("Authority", "api.interpreter.caiyunai.com")
+	req.Header.Set("Accept", "application/json, text/plain, */*")
+	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")
+	req.Header.Set("App-Name", "xy")
+	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
+	req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
+	req.Header.Set("Os-Type", "web")
+	req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
+	req.Header.Set("Sec-Ch-Ua", "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"")
+	req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
+	req.Header.Set("Sec-Ch-Ua-Platform", "\"Windows\"")
+	req.Header.Set("Sec-Fetch-Dest", "empty")
+	req.Header.Set("Sec-Fetch-Mode", "cors")
+	req.Header.Set("Sec-Fetch-Site", "cross-site")
+	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76")
+	req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
+	resp, err := client.Do(req)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer resp.Body.Close()
+	bodyText, err := io.ReadAll(resp.Body)
+	if err != nil {
+		log.Fatal(err)
+	}
+	if resp.StatusCode != 200 {
+		log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
+	}
+	var dictResponse DictResponse
+	err = json.Unmarshal(bodyText, &dictResponse)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
+	for _, item := range dictResponse.Dictionary.Explanations {
+		fmt.Println(item)
+	}
+}
+
+func main() {
+	if len(os.Args) != 2 {
+		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
+example: simpleDict hello
+		`)
+		os.Exit(1)
+	}
+	word := os.Args[1]
+	query(word)
+}
+
+

Socks5代理服务器

+

正常浏览器访问一个网站,先和对方的网站建立TCP连接,然后正常发起HTTP请求,服务器返回响应

+

如果设置了代理服务器,浏览器要先和代理服务器建立TCP连接,然后代理服务器再去和真正的网站建立TCP连接,可以分为4个阶段:

+
    +
  1. 协商(握手):用户的浏览器会向Socks5服务器发起请求,发送一个报文,这个报文里面包括协议版本号,支持的认证的种类等,代理服务器会从里面选择一个它自己支持的认证方式,返回给浏览器,如果返回00表示不需要认证。
  2. +
  3. 认证:(这个代理不加密,认证步骤跳过)
  4. +
  5. 请求:认证通过之后,浏览器会向Socks5服务器发送下一个报文,包括协议的版本号,请求的类型,一般是Connection请求,代表浏览器命令代理服务器要和某个域名,某个端口建立连接。代理服务器收到后会去和真正的网站后端服务器建立TCP连接,然后返回一个报文告诉用户浏览器已经成功建立连接了。
  6. +
  7. Relay:浏览器正常发送请求,代理服务器收到请求后将请求转发到真正的服务器上,将返回的响应转发到浏览器。代理服务器并不关注流量的类别,可以是TCP或者HTTP
  8. +
+

实现流程:

+
    +
  1. 实现TCP echo server,就是发送什么就回复什么,用来测试server写的是否正确(使用 nc 127.0.0.1 1080)进行测试
  2. +
  3. 实现协商阶段:测试时可以使用 curl --socks5 127.0.0.1:1080 -v http://www.qq.com进行测试,但是仅为协商,因此不会成功,但是服务器端会有正确的输出。
  4. +
  5. 实现请求阶段
  6. +
  7. 实现Relay阶段
  8. +
+

最终代码:

+
package main
+
+import (
+	"bufio"
+	"context"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"net"
+)
+
+const socks5Ver = 0x05
+const cmdBind = 0x01
+const atypIPV4 = 0x01
+const atypeHOST = 0x03
+const atypeIPV6 = 0x04
+
+func main() {
+	server, err := net.Listen("tcp", "127.0.0.1:1080")
+	if err != nil {
+		panic(err)
+	}
+	for {
+		client, err := server.Accept()
+		if err != nil {
+			log.Printf("Accept failed %v", err)
+			continue
+		}
+		go process(client)
+	}
+}
+
+func process(conn net.Conn) {
+	defer conn.Close()
+	reader := bufio.NewReader(conn)
+	err := auth(reader, conn)
+	if err != nil {
+		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
+		return
+	}
+	err = connect(reader, conn)
+	if err != nil {
+		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
+		return
+	}
+}
+
+func auth(reader *bufio.Reader, conn net.Conn) (err error) {
+	// +----+----------+----------+
+	// |VER | NMETHODS | METHODS  |
+	// +----+----------+----------+
+	// | 1  |    1     | 1 to 255 |
+	// +----+----------+----------+
+	// VER: 协议版本,socks5为0x05
+	// NMETHODS: 支持认证的方法数量
+	// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
+	// X’00’ NO AUTHENTICATION REQUIRED
+	// X’02’ USERNAME/PASSWORD
+
+	ver, err := reader.ReadByte()
+	if err != nil {
+		return fmt.Errorf("read ver failed:%w", err)
+	}
+	if ver != socks5Ver {
+		return fmt.Errorf("not supported ver:%v", ver)
+	}
+	methodSize, err := reader.ReadByte()
+	if err != nil {
+		return fmt.Errorf("read methodSize failed:%w", err)
+	}
+	method := make([]byte, methodSize)
+	_, err = io.ReadFull(reader, method)
+	if err != nil {
+		return fmt.Errorf("read method failed:%w", err)
+	}
+
+	// +----+--------+
+	// |VER | METHOD |
+	// +----+--------+
+	// | 1  |   1    |
+	// +----+--------+
+	_, err = conn.Write([]byte{socks5Ver, 0x00})
+	if err != nil {
+		return fmt.Errorf("write failed:%w", err)
+	}
+	return nil
+}
+
+func connect(reader *bufio.Reader, conn net.Conn) (err error) {
+	// +----+-----+-------+------+----------+----------+
+	// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+	// +----+-----+-------+------+----------+----------+
+	// | 1  |  1  | X'00' |  1   | Variable |    2     |
+	// +----+-----+-------+------+----------+----------+
+	// VER 版本号,socks5的值为0x05
+	// CMD 0x01表示CONNECT请求
+	// RSV 保留字段,值为0x00
+	// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
+	//   0x01表示IPv4地址,DST.ADDR为4个字节
+	//   0x03表示域名,DST.ADDR是一个可变长度的域名
+	// DST.ADDR 一个可变长度的值
+	// DST.PORT 目标端口,固定2个字节
+
+	buf := make([]byte, 4)
+	_, err = io.ReadFull(reader, buf)
+	if err != nil {
+		return fmt.Errorf("read header failed:%w", err)
+	}
+	ver, cmd, atyp := buf[0], buf[1], buf[3]
+	if ver != socks5Ver {
+		return fmt.Errorf("not supported ver:%v", ver)
+	}
+	if cmd != cmdBind {
+		return fmt.Errorf("not supported cmd:%v", ver)
+	}
+	addr := ""
+	switch atyp {
+	case atypIPV4:
+		_, err = io.ReadFull(reader, buf)
+		if err != nil {
+			return fmt.Errorf("read atyp failed:%w", err)
+		}
+		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
+	case atypeHOST:
+		hostSize, err := reader.ReadByte()
+		if err != nil {
+			return fmt.Errorf("read hostSize failed:%w", err)
+		}
+		host := make([]byte, hostSize)
+		_, err = io.ReadFull(reader, host)
+		if err != nil {
+			return fmt.Errorf("read host failed:%w", err)
+		}
+		addr = string(host)
+	case atypeIPV6:
+		return errors.New("IPv6: no supported yet")
+	default:
+		return errors.New("invalid atyp")
+	}
+	_, err = io.ReadFull(reader, buf[:2])
+	if err != nil {
+		return fmt.Errorf("read port failed:%w", err)
+	}
+	port := binary.BigEndian.Uint16(buf[:2])
+
+	dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
+	if err != nil {
+		return fmt.Errorf("dial dst failed:%w", err)
+	}
+	defer dest.Close()
+	log.Println("dial", addr, port)
+
+	// +----+-----+-------+------+----------+----------+
+	// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+	// +----+-----+-------+------+----------+----------+
+	// | 1  |  1  | X'00' |  1   | Variable |    2     |
+	// +----+-----+-------+------+----------+----------+
+	// VER socks版本,这里为0x05
+	// REP Relay field,内容取值如下 X’00’ succeeded
+	// RSV 保留字段
+	// ATYPE 地址类型
+	// BND.ADDR 服务绑定的地址
+	// BND.PORT 服务绑定的端口DST.PORT
+	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
+	if err != nil {
+		return fmt.Errorf("write failed: %w", err)
+	}
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	go func() {
+		_, _ = io.Copy(dest, reader)
+		cancel()
+	}()
+	go func() {
+		_, _ = io.Copy(conn, dest)
+		cancel()
+	}()
+
+	<-ctx.Done()
+	return nil
+}
+
+

课后

+

Go 语言学习路线图

+ + +
+ +
+
+ + + + + + +
+
+
Go 语言基础 - 基础语法
+
https://zhangzhao219.github.io/2023/01/15/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day01/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月15日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/16/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day02/index.html b/2023/01/16/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day02/index.html new file mode 100644 index 000000000..2e2ba837c --- /dev/null +++ b/2023/01/16/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day02/index.html @@ -0,0 +1,1040 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 语言进阶 - 工程进阶 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go 语言进阶 - 工程进阶

+ + +
+ +

Go 语言进阶 - 工程进阶

+ +

Go 语言进阶 - 工程进阶

+

概述

+

本节课程主要分为四个方面:

+
    +
  1. 并发编程
  2. +
  3. 依赖管理
  4. +
  5. 单元测试
  6. +
  7. 项目实战
  8. +
+

详述

+
    +
  • 罗列课程中涉及到的概念和相关资料,对于不熟悉的知识点,希望同学们可以提前查询预习,届时跟上直播课程进度。
  • +
  • 【必须】课程内容相关代码链接:github.com/Moonlight-Z…
  • +
+

并发编程

+ +

属于编程进阶内容,考虑到工程项目的可用性和可靠性,工程实践中经常会用到。

+

依赖管理

+ +

了解Go依赖管理演进的历程,通过课程学习以及课后实践能能够熟练使用go module 管理依赖。

+

单元测试

+ +

项目实战

+

需求模型来源

+

青训营话题页forum.juejin.cn/youthcamp/p…

+

需求

+
    +
  1. 实现一个展示话题(标题,文字描述)和回帖列表的后端http接口;
  2. +
  3. 本地文件存储数据
  4. +
+

组件及技术点

+ +

课程笔记

+

课程链接:

+ +

语言进阶

+

Go可以充分发挥多核的优势,高效运行

+

线程:内核态,比较重量级

+

协程:用户态,线程可以跑多个协程,比较轻量

+

Goroutine

+

快速打印:

+
func hello(i int) {
+	println("hello goroutine : " + fmt.Sprint(i))
+}
+
+func HelloGoRoutine() {
+	for i := 0; i < 5; i++ {
+		go func(j int) {
+			hello(j)
+		}(i)
+	}
+	time.Sleep(time.Second)
+}
+

最后是使用time.sleep进行阻塞,防止在协程未运行结束前主线程先运行结束了。

+

Channel

+

协程通过通信来共享内存

+
func CalSquare() {
+	src := make(chan int)
+	dest := make(chan int, 3)
+	go func() {
+		defer close(src)
+		for i := 0; i < 10; i++ {
+			src <- i
+		}
+	}()
+	go func() {
+		defer close(dest)
+		for i := range src {
+			dest <- i * i
+		}
+	}()
+	for i := range dest {
+		//复杂操作
+		println(i)
+	}
+}
+
+

+
var (
+	x    int64
+	lock sync.Mutex
+)
+
+func addWithLock() {
+	for i := 0; i < 2000; i++ {
+		lock.Lock()
+		x += 1
+		lock.Unlock()
+	}
+}
+func addWithoutLock() {
+	for i := 0; i < 2000; i++ {
+		x += 1
+	}
+}
+
+func Add() {
+	x = 0
+	for i := 0; i < 5; i++ {
+		go addWithoutLock()
+	}
+	time.Sleep(time.Second)
+	println("WithoutLock:", x)
+	x = 0
+	for i := 0; i < 5; i++ {
+		go addWithLock()
+	}
+	time.Sleep(time.Second)
+	println("WithLock:", x)
+}
+

WaitGroup并发同步

+
func ManyGoWait() {
+	var wg sync.WaitGroup
+	wg.Add(5)
+	for i := 0; i < 5; i++ {
+		go func(j int) {
+			defer wg.Done()
+			hello(j)
+		}(i)
+	}
+	wg.Wait()
+}
+

依赖管理

+

GOPATH:环境变量,项目代码直接依赖src下的代码,go get下载最新的包到src目录下

+

Go Vendor:增加vendor文件,存放依赖包的副本,优先从vendor文件里面查找,但是仍然无法控制依赖的版本

+

Go Module:go.mod:依赖管理基本单元、原生库、单元依赖

+

测试

+

单元测试

+
    +
  • 所有测试文件以_test.go结尾
  • +
  • func TestXxx(*testing.T)
  • +
  • 初始化逻辑放到TestMain中
  • +
+
func HelloTom() string {
+	return "Tom"
+}
+
+func TestHelloTom(t *testing.T) {
+	output := HelloTom()
+	expectOutput := "Tom"
+	assert.Equal(t, expectOutput, output)
+}
+

添加–cover参数可以评价测试代码的覆盖率

+

Mock测试

+

一些函数对本地的数据库、文件等有强依赖,在测试的同时找到这些依赖要求过高

+

可以使用Mock进行测试,在函数执行的时候替换成另外一个函数(打桩),从而规避掉对本地其他的强依赖

+
func ReadFirstLine() string {
+	open, err := os.Open("log")
+	defer open.Close()
+	if err != nil {
+		return ""
+	}
+	scanner := bufio.NewScanner(open)
+	for scanner.Scan() {
+		return scanner.Text()
+	}
+	return ""
+}
+
+func ProcessFirstLine() string {
+	line := ReadFirstLine()
+	destLine := strings.ReplaceAll(line, "11", "00")
+	return destLine
+}
+
+func TestProcessFirstLine(t *testing.T) {
+	firstLine := ProcessFirstLine()
+	assert.Equal(t, "line00", firstLine)
+}
+
+func TestProcessFirstLineWithMock(t *testing.T) {
+	monkey.Patch(ReadFirstLine, func() string {
+		return "line110"
+	})
+	defer monkey.Unpatch(ReadFirstLine)
+	line := ProcessFirstLine()
+	assert.Equal(t, "line000", line)
+}
+

基准测试

+

对函数的运行时间进行测试:go test -bench=.

+
var ServerIndex [10]int
+
+func InitServerIndex() {
+	for i := 0; i < 10; i++ {
+		ServerIndex[i] = i+100
+	}
+}
+
+func Select() int {
+	return ServerIndex[rand.Intn(10)]
+}
+
+func FastSelect() int {
+	return ServerIndex[fastrand.Intn(10)]
+}
+
+func BenchmarkSelect(b *testing.B) {
+	InitServerIndex()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		Select()
+	}
+}
+func BenchmarkSelectParallel(b *testing.B) {
+	InitServerIndex()
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			Select()
+		}
+	})
+}
+func BenchmarkFastSelectParallel(b *testing.B) {
+	InitServerIndex()
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			FastSelect()
+		}
+	})
+}
+

项目实战:社区话题页面

+

需求

+
    +
  1. 实现一个展示话题(标题,文字描述)和回帖列表的后端http接口;
  2. +
  3. 本地文件存储数据
  4. +
+

分层结构

+
    +
  1. 数据层:数据Model,处理外部数据的增删改查
  2. +
  3. 逻辑层:业务Entity,处理核心业务逻辑输出
  4. +
  5. 视图层:视图View,处理和外部的交互逻辑
  6. +
+

组件及技术点

+ +

具体逻辑见代码

+

课后实践

+
    +
  1. 支持对话题发布回帖。
  2. +
  3. 回帖id生成需要保证不重复、唯一性。
  4. +
  5. 新加回帖追加到本地文件,同时需要更新索引,注意Map的并发安全问题
  6. +
+ + +
+ +
+
+ + + + + + +
+
+
Go 语言进阶 - 工程进阶
+
https://zhangzhao219.github.io/2023/01/16/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day02/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月16日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/17/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day03/index.html b/2023/01/17/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day03/index.html new file mode 100644 index 000000000..a6bf4a494 --- /dev/null +++ b/2023/01/17/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day03/index.html @@ -0,0 +1,853 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 框架三件套详解(Web/RPC/ORM) - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go 框架三件套详解(Web/RPC/ORM)

+ + +
+ +

Go 框架三件套详解(Web/RPC/ORM)

+ +

Go 框架三件套详解(Web/RPC/ORM)

+

环境搭建部分

+

搭建课程所需要的开发环境以及安装需要用到的软件。

+

学习如何安装 Docker/Postman/Git/Golang

+
    +
  • 安装 Minikube 或 Docker Desktop 用于使用 Docker 安装教程 +
      +
    • 可以使用 Minikube 或者使用 Docker Desktop 启动 Docker
    • +
    +
  • +
  • 安装 Postman(使用更新的Apifox替代)
  • +
  • 安装 Git 安装教程
  • +
  • 安装 Go(Golang >= 1.15) 安装教程
  • +
+

框架体验部分

+

提前体验一下课程涉及的 HTTP/RPC/ORM 框架

+

HTTP 框架 Hertz 初体验

+

通过阅读 www.cloudwego.io/zh/docs/her… 尝试运行 Hertz 的示例代码(Hertz 框架地址: github.com/cloudwego/h…

+
    +
  1. 首先安装命令行工具hz:go install github.com/cloudwego/hertz/cmd/hz@latest
  2. +
  3. 生成代码 hz new -module github.com/cloudwego/hertz-examples
  4. +
  5. 整理 & 拉取依赖 go mod tidy
  6. +
  7. 编译并启动 go build -o hertz_demo && ./hertz_demo
  8. +
+

RPC 框架 Kitex 初体验

+

通过阅读 www.cloudwego.io/zh/docs/kit… 尝试运行 Kitex 的示例代码(KItex 框架地址: github.com/cloudwego/k…

+
    +
  1. 安装 kitex:go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
  2. +
  3. 安装 thriftgo:go install github.com/cloudwego/thriftgo@latest
  4. +
  5. 克隆该示例仓库到本地 git clone https://github.com/cloudwego/kitex-examples.git
  6. +
  7. 进入示例仓库的 hello 目录 cd kitex-examples/hello
  8. +
  9. 运行 server go run .
  10. +
  11. 运行 client 另起一个终端后,go run ./client
  12. +
+

ORM 框架 Gorm 初体验

+

通过阅读 gorm.cn/docs/#Insta… 尝试运行 Gorm 的示例代码(Gorm 框架地址: github.com/go-gorm/gor…

+
go get -u gorm.io/gorm
+go get -u gorm.io/driver/sqlite
+

其它知识

+
    +
  • 了解一下什么IDL以及IDL的语法
  • +
  • 了解一下什么是 opentracing 以及 etcd
  • +
+

Etcd 与 Opentracing 是什么

+
    +
  • 了解 etcd 是什么 参考文档 +
      +
    • etcd是一种高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以在网络分区期间优雅地处理领导人选举,并且可以容忍机器故障,即使在领导人节点中也是如此。
    • +
    +
  • +
  • 了解 opentracing 是什么 参考文档 +
      +
    • OpenTracing是一种分布式系统链路跟踪的设计原则、规范、标准。
    • +
    +
  • +
+

IDL 是什么

+
    +
  • 了解 IDL 是什么 zh.m.wikipedia.org/zh-hans/%E6… +
      +
    • 接口描述语言 (Interface description language,缩写 IDL ),是用来描述软件组件介面 “介面 (程式设计)”)的一种计算机语言。IDL通过一种独立于编程语言的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Java写成。
    • +
    • IDL通常用于远程调用软件。在这种情况下,一般是由远程客户终端调用不同操作系统上的对象组件,并且这些对象组件可能是由不同计算机语言编写的。IDL建立起了两个不同操作系统间通信的桥梁。
    • +
    +
  • +
  • Thrift IDL 语法 thrift.apache.org/docs/idl
  • +
  • proto3 IDL 语法 developers.google.com/protocol-bu…
  • +
+

课程笔记

+

直播链接:https://live.juejin.cn/4354/9899243

+

课程目标

+
    +
  1. 将前面几节课所学到的知识应用到项目中。
  2. +
  3. 掌握 Hertz/Kitex/Gorm 的基本用法。
  4. +
  5. 通过学习实战案例,可以使用 Hertz/Kitex/Gorm 完成日常后端开发任务
  6. +
+

三件套介绍

+

Gorm是一个已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。

+

Kitex是字节内部的Golang微服务RPC框架,具有高性能、强可扩展的主要特点,支持多协议并且拥有丰富的开源扩展。

+

Hertz是字节内部的HTTP框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用性、高性能、高扩展性特点。

+

Gorm的基本使用

+

CRUD

+

pS1dwvD.pngpS1wJsg.pngpS10Mm4.pngpS10sht.pngpS1bm36.png

+

其他操作

+

pS1bDEj.pngpS1bo5R.pngpS1bb26.pngpS1qixf.png

+

Gorm拥有丰富的扩展生态,可以使用代码生成工具、分片库方案、手动索引、乐观锁、读写分离、OpenTelemetry 扩展等等

+

Kitex

+

Hertz

+ + +
+ +
+
+ + + + + + +
+
+
Go 框架三件套详解(Web/RPC/ORM)
+
https://zhangzhao219.github.io/2023/01/17/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day03/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月17日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/19/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day04/index.html b/2023/01/19/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day04/index.html new file mode 100644 index 000000000..0fdafd58c --- /dev/null +++ b/2023/01/19/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day04/index.html @@ -0,0 +1,1217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 高质量编程与性能调优 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go 高质量编程与性能调优

+ + +
+ +

Go 高质量编程与性能调优

+ +

Go 高质量编程与性能调优

+

课程概述

+
    +
  • 介绍编码规范,帮助大家写出高质量程序
  • +
  • 介绍 Go 语言的性能优化建议,分析对比不同方式对性能的影响和背后的原理
  • +
  • 讲解常用性能分析工具 pprof 的使用和工作原理,熟悉排查程序性能问题的基本流程
  • +
  • 分析性能调优实际案例,介绍实际性能调优时的工作内容
  • +
+

课前

+
    +
  • 课程内容概要
  • +
+

image.png

+

实践准备 (必须)

+ +

推荐阅读

+ +

课程笔记

+ +

高质量编程

+

简介

+
    +
  • 编写的代码能够达到正确可靠、简洁清晰、无性能隐患的目标就能称之为高质量代码
  • +
  • 实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的
  • +
  • 高质量的编程需要注意以下原则:简单性、可读性、生产力
  • +
+

常见编码规范

+
代码格式
+
    +
  • 使用 gofmt 自动格式化代码,保证所有的 Go 代码与官方推荐格式保持一致
  • +
+

总结

+
    +
  • 提升可读性,风格一致的代码更容易维护、需要更少的学习成本、团队合作成本,同时可以降低 Review 成本
  • +
+
注释
+ +

总结

+
    +
  • 代码是最好的注释
  • +
  • 注释应该提供代码未表达出的上下文信息
  • +
+
命名规范
+
    +
  • +

    variable

    +
      +
    • 简洁胜于冗长
    • +
    • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
    • +
    • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • +
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
    • +
    +
  • +
  • +

    function

    +
      +
    • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
    • +
    • 函数名尽量简短
    • +
    • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
    • +
    • 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息
    • +
    +
  • +
  • +

    package

    +
      +
    • 只由小写字母组成。不包含大写字母和下划线等字符
    • +
    • 简短并包含一定的上下文信息。例如 schema、task 等
    • +
    • 不要与标准库同名。例如不要使用 sync 或者 strings
    • +
    +
  • +
+

总结

+
    +
  • 关于命名的大多数规范核心在于考虑上下文
  • +
  • 人们在阅读理解代码的时候也可以看成是计算机运行程序,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本
  • +
+
控制流程
+
    +
  • +

    避免嵌套,保持正常流程清晰

    +
  • +
  • +

    如果两个分支中都包含 return 语句,则可以去除冗余的 else

    +
  • +
  • +

    尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性

    + +
  • +
+

总结

+
    +
  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  • +
  • 提高代码的可读性
  • +
+
错误和异常处理
+
    +
  • +

    简单错误处理

    +
      +
    • 优先使用 errors.New 来创建匿名变量来直接表示该错误。有格式化需求时使用 fmt.Errorf
    • +
    • github.com/golang/go/b…
    • +
    +
  • +
  • +

    错误的 Wrap 和 Unwrap

    +
      +
    • 在 fmt.Errorf 中使用 %w 关键字来将一个错误 wrap 至其错误链中
    • +
    • github.com/golang/go/b…
    • +
    • Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Iserrors.As 、errors.Unwrap 以及 fmt.Errorf 的 %w。如果项目运行在小于 Go1.13 的版本中,导入 golang.org/x/xerrors 来使用。以下语法均已 Go1.13 作为标准。
    • +
    +
  • +
  • +

    错误判定

    + +
  • +
  • +

    panic

    +
      +
    • 不建议在业务代码中使用 panic
    • +
    • 如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃
    • +
    • 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
    • +
    • github.com/Shopify/sar…
    • +
    +
  • +
  • +

    recover

    +
      +
    • recover 只能在被 defer 的函数中使用,嵌套无法生效,只在当前 goroutine 生效
    • +
    • github.com/golang/go/b…
    • +
    • 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈。
    • +
    • github.com/golang/webs…
    • +
    +
  • +
+

总结

+
    +
  • panic 用于真正异常的情况
  • +
  • error 尽可能提供简明的上下文信息,方便定位问题
  • +
  • recover 生效范围,在当前 goroutine 的被 defer 的函数中生效
  • +
+

性能优化建议

+
    +
  • +

    在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率

    +
  • +
  • +

    性能对比测试代码,可参考 github.com/RaymondCode…

    +
  • +
  • +
    slice 预分配内存
    +
      +
    • 在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时
    • +
    • 原理 +
        +
      • ueokande.github.io/go-slice-tr…
      • +
      • 切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最大长度)
      • +
      • 切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的
      • +
      • 切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景: +
          +
        • 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
        • +
        • 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
        • +
        +
      • +
      • 因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能
      • +
      +
    • +
    • 另一个陷阱:大内存得不到释放 +
        +
      • 在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组
      • +
      • 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
      • +
      • 推荐的做法,使用 copy 替代 re-slice
      • +
      +
    • +
    +
  • +
  • +
    map 预分配内存
    +
      +
    • 原理 +
        +
      • 不断向 map 中添加元素的操作会触发 map 的扩容
      • +
      • 根据实际需求提前预估好需要的空间
      • +
      • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
      • +
      +
    • +
    +
  • +
  • +
    使用 strings.Builder
    +
      +
    • 常见的字符串拼接方式
      +* +
        +
      • strings.Builder
      • +
      • bytes.Buffer
      • +
      +
    • +
    • strings.Builder 最快,bytes.Buffer 较快,+ 最慢
    • +
    • 原理 +
        +
      • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
      • +
      • strings.Builder,bytes.Buffer 的内存是以倍数申请的
      • +
      • strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回
      • +
      +
    • +
    +
  • +
  • +
    使用空结构体节省内存
    +
      +
    • 空结构体不占据内存空间,可作为占位符使用
    • +
    • 比如实现简单的 Set +
        +
      • Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。对于集合场景,只需要用到 map 的键而不需要值
      • +
      +
    • +
    +
  • +
  • +
    使用 atomic 包
    +
      +
    • 原理 +
        +
      • 锁的实现是通过操作系统来实现,属于系统调用,atomic 操作是通过硬件实现的,效率比锁高很多
      • +
      • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
      • +
      • 对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface{}
      • +
      +
    • +
    +
  • +
+
总结
+
    +
  • 避免常见的性能陷阱可以保证大部分程序的性能
  • +
  • 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能
  • +
+

性能调优实战

+

性能调优简介

+
    +
  • 性能调优原则 +
      +
    • 要依靠数据不是猜测
    • +
    • 要定位最大瓶颈而不是细枝末节
    • +
    • 不要过早优化
    • +
    • 不要过度优化
    • +
    +
  • +
+

性能分析工具

+

性能调优的核心是性能瓶颈的分析,对于 Go 应用程序,最方便的就是 pprof 工具

+ +

性能调优案例

+
    +
  • +
    基本概念
    +
      +
    • 服务:能单独部署,承载一定功能的程序
    • +
    • 依赖:Service A 的功能实现依赖 Service B 的响应结果,称为 Service A 依赖 Service B
    • +
    • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
    • +
    • 基础库:公共的工具包、中间件
    • +
    +
  • +
  • +
    业务优化
    +
      +
    • 流程 +
        +
      • 建立服务性能评估手段
      • +
      • 分析性能数据,定位性能瓶颈
      • +
      • 重点优化项改造
      • +
      • 优化效果验证
      • +
      +
    • +
    • 建立压测评估链路 +
        +
      • 服务性能评估
      • +
      • 构造请求流量
      • +
      • 压测范围
      • +
      • 性能数据采集
      • +
      +
    • +
    • 分析性能火焰图,定位性能瓶颈 +
        +
      • pprof 火焰图
      • +
      +
    • +
    • 重点优化项分析 +
        +
      • 规范组件库使用
      • +
      • 高并发场景优化
      • +
      • 增加代码检查规则避免增量劣化出现
      • +
      • 优化正确性验证
      • +
      +
    • +
    • 上线验证评估 +
        +
      • 逐步放量,避免出现问题
      • +
      +
    • +
    • 进一步优化,服务整体链路分析 +
        +
      • 规范上游服务调用接口,明确场景需求
      • +
      • 分析业务流程,通过业务流程优化提升服务性能
      • +
      +
    • +
    +
  • +
  • +
    基础库优化
    +
      +
    • 适应范围更广,覆盖更多服务
    • +
    • AB 实验 SDK 的优化 +
        +
      • 分析基础库核心逻辑和性能瓶颈
      • +
      • 完善改造方案,按需获取,序列化协议优化
      • +
      • 内部压测验证
      • +
      • 推广业务服务落地验证
      • +
      +
    • +
    +
  • +
  • +
    Go 语言优化
    +
      +
    • 适应范围最广,Go 服务都有收益
    • +
    • 优化方式 +
        +
      • 优化内存分配策略
      • +
      • 优化代码编译流程,生成更高效的程序
      • +
      • 内部压测验证
      • +
      • 推广业务服务落地验证
      • +
      +
    • +
    +
  • +
+

课后

+
    +
  • 了解下其他语言的编码规范,是否和 Go 语言编码规范有相通之处,注重理解哪些共同点
  • +
  • 编码规范或者性能优化建议大部分是通用的,有没有方式能够自动化对代码进行检测?
  • +
  • github.com/golang/go/t… 中选择感兴趣的包,看看官方代码是如何编写的
  • +
  • 使用 Go 进行并发编程时有哪些性能陷阱或者优化手段?
  • +
  • 在真实的线上环境中,每个场景或者服务遇到的性能问题也是各种各样,搜索下知名公司的官方公众号或者博客,里面有哪些性能优化的案例?比如 eng.uber.com/category/os…
  • +
  • Go 语言本身在持续更新迭代,每个版本在性能上有哪些重要的优化点?
  • +
+ + +
+ +
+
+ + + + + + +
+
+
Go 高质量编程与性能调优
+
https://zhangzhao219.github.io/2023/01/19/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day04/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月19日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/20/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day05/index.html b/2023/01/20/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day05/index.html new file mode 100644 index 000000000..662349e39 --- /dev/null +++ b/2023/01/20/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day05/index.html @@ -0,0 +1,1208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 语言内存管理详解 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Go 语言内存管理详解

+ + +
+ +

Go 语言内存管理详解

+ +

Go 语言内存管理详解

+

本节课程主要分为四个方面:

+
    +
  1. 自动内存管理
  2. +
  3. Go 内存管理及优化
  4. +
  5. 编译器和静态分析
  6. +
  7. Go 编译器优化
  8. +
+

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

+

课前

+

自动内存管理

+
    +
  • +

    Auto memory management: 自动内存管理

    +
  • +
  • +

    Grabage collction: 垃圾回收

    +
  • +
  • +

    Mutator: 业务线程

    +
  • +
  • +

    Collector: GC 线程

    +
  • +
  • +

    Concurrent GC: 并发 GC

    +
  • +
  • +

    Parallel GC: 并行 GC

    +
  • +
  • +

    Tracing garbage collection: 追踪垃圾回收

    +
      +
    • Copying GC: 复制对象 GC
    • +
    • Mark-sweep GC: 标记-清理 GC
    • +
    • Mark-compact GC: 标记-压缩 GC
    • +
    +
  • +
  • +

    Reference counting: 引用计数

    +
  • +
  • +

    Generational GC: 分代 GC

    +
      +
    • Young generation: 年轻代
    • +
    • Old generation: 老年代
    • +
    +
  • +
+

Go 内存管理及优化

+
    +
  • TCMalloc
  • +
  • mmap() 系统调用
  • +
  • scan object 和 noscan object
  • +
  • mspan, mcache, mentral
  • +
  • Bump-pointer object allocation: 指针碰撞风格的对象分配
  • +
+

编译器和静态分析

+
    +
  • 词法分析
  • +
  • 语法分析
  • +
  • 语义分析
  • +
  • Intermediate representation (IR) 中间表示
  • +
  • 代码优化
  • +
  • 代码生成
  • +
  • Control flow: 控制流
  • +
  • Data flow: 数据流
  • +
  • Intra-procedural analysis 过程内分析
  • +
  • Inter-procedural analysis: 过程间分析
  • +
+

Go 编译器优化

+
    +
  • Function inlining: 函数内联
  • +
  • Escape analysis: 逃逸分析
  • +
+

课中

+ +

引言

+
    +
  • +

    什么是性能优化?

    +
      +
    • 提升软件系统处理能力减少不必要的消耗 ,充分发掘计算机算力
    • +
    +
  • +
  • +

    为什么要做性能优化?

    +
      +
    • 用户体验:带来用户体验的提升 —— 让刷抖音更丝滑,让双十一购物不再卡顿
    • +
    • 资源高效利用:降低成本,提高效率 —— 很小的优化乘以海量机器会是显著的性能提升和成本节约
    • +
    +
  • +
  • +

    性能优化

    +
      +
    • 业务层优化 +
        +
      • 针对特定场景,具体问题,具体分析
      • +
      • 容易获得较大性能收益
      • +
      +
    • +
    • 语言运行时优化 +
        +
      • 解决更通用的性能问题
      • +
      • 考虑更多场景
      • +
      • Tradeoffs
      • +
      +
    • +
    • 数据驱动 +
        +
      • 自动化性能分析工具 —— pprof
      • +
      • 依靠数据而非猜测
      • +
      • 首先优化最大瓶颈
      • +
      +
    • +
    +
  • +
  • +

    软件质量

    +
      +
    • 保证接口稳定的前提下改进实现
    • +
    +

    +
  • +
  • +

    测试驱动

    +
  • +
  • +

    通过清晰的文档告诉用户这一项优化 做了什么没做什么能达到怎样的效果

    +
  • +
  • +

    隔离,优化代码用选项和原先的路径隔离,保证优化未启用时的行为同以前一致

    +
  • +
  • +

    可观测、可灰度、可回滚

    +
  • +
+

自动内存管理

+

基本概念

+
    +
  • +

    自动内存管理:由程序语言的运行时系统管理动态内存

    +
  • +
  • +

    避免手动内存管理,专注于实现业务逻辑

    +
  • +
  • +

    保证内存使用的正确性安全性 : double-free problem, use-after-free problem

    +
  • +
  • +

    三个任务

    +
      +
    • 为新对象分配空间
    • +
    • 找到存活对象
    • +
    • 回收死亡对象的内存空间
    • +
    +
  • +
  • +

    概念
    +Mutator: 业务线程,分配新对象,修改对象指向关系
    +Collector: GC 线程,找到存活对象,回收死亡对象的内存空间

    +
  • +
+

+

Serial GC: 只有一个 collector

+

+

Parallel GC: 并行 GC,支持多个 collectors 同时回收的 GC 算法

+

+

Concurrent GC: 并发 GC,支持 mutator(s) 和 collector(s) 同时执行的 GC 算法

+

+

Collectors 必须感知对象指向关系的改变!

+

+

追踪垃圾回收

+
    +
  • Tracing garbage collection: 追踪垃圾回收 +
      +
    • 被回收的条件:不可达对象
    • +
    • 过程 +
        +
      • 标记根对象 (GC roots): 静态变量、全局变量、常量、线程栈等
      • +
      • 标记:找到所有可达对象
      • +
      • 清理:回收所有不可达对象占据的内存空间 +
          +
        • Copying GC: 将存活对象从一块内存空间复制到另外一块内存空间,原先的空间可以直接进行对象分配
        • +
        • +
        • Mark-sweep GC: 将死亡对象所在内存块标记为可分配,使用 free list 管理可分配的空间
        • +
        • +
        • Mark-compact GC: 将存活对象复制到同一块内存区域的开头
        • +
        • +
        +
      • +
      +
    • +
    +
  • +
+

引用计数

+
    +
  • +

    每个对象都有一个与之关联的引用数目

    +
  • +
  • +

    对象存活的条件:当且仅当引用数大于 0

    +
  • +
  • +

    优点

    +
      +
    • 内存管理的操作被 平摊到程序运行中 :指针传递的过程中进行引用计数的增减
    • +
    • 不需要了解 runtime 的细节:因为不需要标记 GC roots,因此不需要知道哪里是全局变量、线程栈等
    • +
    +
  • +
  • +

    缺点

    +
      +
    • 开销大,因为对象可能会被多线程访问,对引用计数的修改需要原子****操作保证原子性和可见性
    • +
    • 无法回收环形数据结构
    • +
    • 每个对象都引入额外存储空间存储引用计数
    • +
    • 虽然引用计数的操作被平摊到程序运行过程中,但是回收大的数据结构依然可能引发暂停
    • +
    +
  • +
  • +

    说明

    +
      +
    • 以上我们所讲述的技术的缺点并非是无法解决的问题。学术界和工业界在一直在致力于解决自动内存管理技术的不足之处。例如,最新的 PLDI’22 的文章 Low-Latency, High-Throughput Garbage Collection 感兴趣的同学可以阅读。
    • +
    +
  • +
+

Go 内存管理及优化

+

Go 内存管理

+
    +
  • +

    TCMalloc: TC is short for thread caching

    +
  • +
  • +

    目标:为对象在 heap 上分配内存

    +
  • +
  • +

    提前将内存分块

    +
      +
    • 调用系统调用 mmap() 向 OS 申请一大块内存,例如 4 MB
    • +
    • 先将内存划分成大块,例如 8 KB,称作 mspan
    • +
    • 再将大块继续划分成特定大小的小块,用于对象分配
    • +
    • noscan mspan: 分配不包含指针的对象 —— GC 不需要扫描
    • +
    • scan mspan: 分配包含指针的对象 —— GC 需要扫描
    • +
    +
  • +
+

+
    +
  • +

    对象分配:根据对象的大小,选择最合适的块返回

    +
  • +
  • +

    内存缓存

    +
      +
    • Go 内存管理构成了多级缓存机制,从 OS 分配得的内存被内存管理回收后,也不会立刻归还给 OS,而是在 Go runtime 内部先缓存起来,从而避免频繁向 OS 申请内存。内存分配的路线图如下。
    • +
    +
  • +
+

+

Go 内存管理的问题

+

mspan, mcache 和 mcentral 构成了内存管理的多级缓存机制。

+
    +
  • 对象分配是非常高频的操作:每秒分配 GB 级别的内存
  • +
  • 线上 profiling 发现,Go 的内存分配占用很多 CPU
  • +
+

+

可以看到,用于分配对象的函数 mallocgc() 占用 CPU 较高

+
    +
  • 小对象分配占大多数
  • +
+

+

横轴是对象大小,纵轴是数目,可以看到绝大多数对象都小于 80 B。因此 优化小对象分配是关键

+

字节跳动的优化方案

+
    +
  • Balanced GC
  • +
  • 核心:将 noscan 对象在 per-g allocation buffer (GAB) 上分配,并使用移动对象 GC 管理这部分内存,提高对象分配和回收效率
  • +
+

+
    +
  • 每个 g 会附加一个较大的 allocation buffer (例如 1 KB) 用来分配小于 128 B 的 noscan 小对象
  • +
  • bump pointer 风格的对象分配。示意如下。
  • +
+
if g.ab.end - g.ab.top < size {
+    // Allocate a new allocation buffer
+}
+addr := g.ab.top
+g.ab.top += size
+return addr
+
    +
  • 分配对象时,根据对象大小移动 top 指针并返回,快速完成一次对象分配
  • +
  • 同原先调用 mallocgc() 进行对象分配的方式相比,balanced GC 缩短了对象分配的路径,减少了对象分配执行的指令数目,降低 CPU 使用
  • +
+

从 Go runtime 内存管理模块的角度看,一个 allocation buffer 其实是一个大对象。本质上 balanced GC 是 将多次小对象的分配合并成一次大对象的分配 。因此,当 GAB 中哪怕只有一个小对象存活时,Go runtime 也会认为整个大对象(即 GAB)存活。为此,balanced GC 会根据 GC 策略, 将 GAB 中存活的对象移动到另外的 GAB 中 ,从而压缩并清理 GAB 的内存空间,原先的 GAB 空间由于不再有存活对象,可以全部释放,如下图所示。

+

+

上图上方是两个 GAB,其中虚线表示 GAB 中对象的分界线。黑色表示 GAB 中存活的对象,白色表示死掉的对象。由于 GAB 中有存活对象,整个 GAB 无法被回收。

+

Balanced GC 会将 GAB 中存活的对象移动到下面的 GAB 中,这样原先的两个 GABs 就可以被释放,压缩并清理 GAB 的内存空间。

+

Balanced GC 只负责 noscan 对象的分配和移动,对象的标记和回收依然依赖 Go GC 本身,并和 Go GC 保持兼容。

+

编译器和静态分析

+
    +
  • 编译器的结构
  • +
+

+
    +
  • 静态分析: 不执行代码 ,推导程序的行为,分析程序的性质。
  • +
  • 控制流:程序的执行流程
  • +
  • 数据流:数据在控制流上的传递
  • +
+

+

上图的程序转换成控制流图 (control-flow graph)

+

+
    +
  • 通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties) ,这些事实可以帮助我们做编译优化。 +
      +
    • 例如上面的程序。我们通过分析数据流和控制流,知道这个程序始终返回 4。编译器可以根据这个结果做出优化。
    • +
    +
  • +
+

+
    +
  • Intra-procedural analysis: 函数内分析:在函数内进行控制流和数据流的分析
  • +
  • Inter-procedural analysis: 函数间分析:除了函数内的分析,还需要考虑跨函数的数据流和控制流,例如参数传递,函数返回值等
  • +
+

Go 编译器优化

+

目的

+
    +
  • 用户无感知,重新编译即可获得性能收益
  • +
  • 通用的优化手段
  • +
+

现状

+
    +
  • 采用的优化较少
  • +
  • 追求编译时间短,因此没有进行复杂的代码分析和优化
  • +
+

思路

+
    +
  • 面向后端长期执行的任务
  • +
  • 用适当增加编译时间换取更高性能的代码
  • +
+

函数内联

+
    +
  • +

    定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定

    +
  • +
  • +

    优点

    +
      +
    • 消除调用开销
    • +
    • 将过程间分析的问题转换为过程内分析,帮助其他分析
    • +
    +
  • +
  • +

    缺点

    +
      +
    • 函数体变大
    • +
    • 编译生成的 Go 镜像文件变大
    • +
    +
  • +
  • +

    函数内联在大多数情况下是正向优化,即多内联,会提升性能

    +
  • +
  • +

    采取一定的策略决定是否内联

    +
      +
    • 调用和被调用函数的规模
    • +
    +
  • +
  • +

    Go 内联的限制

    +
      +
    • 语言特性:interface, defer 等等,限制了内联优化
    • +
    • 内联策略非常保守
    • +
    +
  • +
  • +

    字节跳动的优化方案

    +
      +
    • 修改了内联策略,让更多函数被内联
    • +
    • 增加了其他优化的机会:逃逸分析
    • +
    +
  • +
  • +

    开销

    +
      +
    • Go 镜像大小略有增加
    • +
    • 编译时间增加
    • +
    • 运行时栈扩展开销增加
    • +
    +
  • +
+

逃逸分析

+
    +
  • +

    定义:分析代码中指针的动态作用域,即指针在何处可以被访问

    +
  • +
  • +

    大致思路

    +
      +
    • 从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s: +
        +
      • 作为参数传递给其他函数;
      • +
      • 传递给全局变量;
      • +
      • 传递给其他的 goroutine;
      • +
      • 传递给已逃逸的指针指向的对象;
      • +
      +
    • +
    • 则指针 p 逃逸出 s,反之则没有逃逸出 s.
    • +
    +
  • +
  • +

    优化:未逃逸出当前函数的指针指向的对象可以在栈上分配

    +
      +
    • 对象在栈上分配和回收很快:移动 sp 即可完成内存的分配和回收;
    • +
    • 减少在堆上分配对象,降低 GC 负担。
    • +
    +
  • +
+

课后

+
    +
  1. 从业务层和语言运行时层进行优化分别有什么特点?
  2. +
  3. 从软件工程的角度出发,为了保证语言 SDK 的可维护性和可拓展性,在进行运行时优化时需要注意什么?
  4. +
  5. 自动内存管理技术从大类上分为哪两种,每一种技术的特点以及优缺点有哪些?
  6. +
  7. 什么是分代假说?分代 GC 的初衷是为了解决什么样的问题?
  8. +
  9. Go 是如何管理和组织内存的?
  10. +
  11. 为什么采用 bump-pointer 的方式分配内存会很快?
  12. +
  13. 为什么我们需要在编译器优化中进行静态代码分析?
  14. +
  15. 函数内联是什么,这项优化的优缺点是什么?
  16. +
  17. 什么是逃逸分析?逃逸分析是如何提升代码性能的?
  18. +
+ + +
+ +
+
+ + + + + + +
+
+
Go 语言内存管理详解
+
https://zhangzhao219.github.io/2023/01/20/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day05/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月20日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Users/index.html b/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Users/index.html new file mode 100644 index 000000000..1131c1fa6 --- /dev/null +++ b/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Users/index.html @@ -0,0 +1,859 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 简易抖音项目-用户模块 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

简易抖音项目-用户模块

+ + +
+ +

简易抖音项目-用户模块

+ +

简易抖音项目-用户模块设计说明

+

需求分析

+

用户模块包括用户注册、用户登录和用户信息三个部分。

+

1. 用户注册接口 /douyin/user/register/

+

新用户注册时提供用户名,密码,昵称即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token.

+

接口定义:

+
service UserRegister {
+    rpc UserRegister (douyin_user_register_request) returns (douyin_user_register_response) {}
+}
+
+message douyin_user_register_request{
+    string username = 1; // 注册用户名,最长32个字符
+    string password = 2; // 密码,最长32个字符
+}
+
+message douyin_user_register_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    int64 user_id = 3; // 用户id
+    string token = 4; // 用户鉴权token
+}
+

2. 用户登录接口 /douyin/user/login/

+

通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token

+

接口定义:

+
service UserLogin {
+    rpc UserLogin (douyin_user_login_request) returns (douyin_user_login_response) {}
+}
+
+message douyin_user_login_request{
+    string username = 1; // 登录用户名
+    string password = 2; // 登录密码
+}
+
+message douyin_user_login_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    int64 user_id = 3; // 用户id
+    string token = 4; // 用户鉴权token
+}
+

3. 用户信息接口 /douyin/user/

+

获取登录用户的 id、昵称,如果实现社交部分的功能,还会返回关注数和粉丝数。

+

接口定义:

+
service UserInfo {
+    rpc UserInfo (douyin_user_request) returns (douyin_user_response) {}
+}
+
+message douyin_user_request{
+    int64 user_id = 1; // 用户id
+    string token = 2; // 用户鉴权token
+}
+
+message douyin_user_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    User user = 3; // 用户信息
+}
+

整体架构设计

+

pSsNise.png

+

返回的状态码(虽然客户端并没有逻辑进行处理):

+
    +
  • 注册时用户已经存在,状态码为1
  • +
  • 用户不存在,状态码为2
  • +
  • 登录时用户存在但是密码错误,状态码为3
  • +
+

详细设计

+

用户注册

+

pSsl600.png

+
    +
  1. DY-api.UserRegister处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserRegister
  2. +
  3. 服务端根据用户名查询数据库,如果发现重名用户名,则直接返回错误
  4. +
  5. 未发现重名用户名,则通过md5加盐(用户名)对密码进行加密,加密后插入数据库,数据库返回唯一自增ID
  6. +
  7. 服务端返回成功响应给DY-api.UserRegister
  8. +
  9. DY-api.UserRegister利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
  10. +
+

用户登录

+

pSs80pD.png

+
    +
  1. DY-api.UserLogin处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserLogin
  2. +
  3. 服务端根据用户名查询数据库,如果未发现相同用户名,则直接返回错误,否则返回通过用户名查询出来的用户id和密码
  4. +
  5. 对用户输入的密码进行md5加盐(用户名)加密,与上一步返回的密码进行比较,如果不匹配直接返回错误
  6. +
  7. 密码匹配,则服务端返回成功响应给DY-api.UserLogin
  8. +
  9. DY-api.UserLogin利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
  10. +
+

用户信息

+

pSst2rQ.png

+
    +
  1. DY-api.UserInfo处理请求,将请求中带有的id字段传递到服务端DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList
  2. +
  3. 并行请求三个服务,其中DY-srv.UserInfo根据id字段查询数据库,如果id有效,则返回用户姓名,否则返回错误
  4. +
  5. 等待三个服务全部成功返回后,填充响应中的User的五个字段 +
      +
    1. id与name字段通过DY-srv.UserInfo的响应直接获取
    2. +
    3. followcount通过获取DY-srv.GetFollowList返回的切片长度获取
    4. +
    5. followercount通过获取DY-srv.GetFollowerList返回的切片长度获取
    6. +
    7. 通过Token获取当前的登录用户id,在DY-srv.GetFollowerList切片内部查询,如果查询到为True,否则为False
    8. +
    +
  6. +
  7. 构建响应结构体并返回给客户端
  8. +
+ + +
+ +
+
+ + + + + + +
+
+
简易抖音项目-用户模块
+
https://zhangzhao219.github.io/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Users/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月24日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Videos/index.html b/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Videos/index.html new file mode 100644 index 000000000..2981331f5 --- /dev/null +++ b/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Videos/index.html @@ -0,0 +1,870 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 简易抖音项目-视频模块 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

简易抖音项目-视频模块

+ + +
+ +

简易抖音项目-视频模块

+ +

简易抖音项目-视频模块设计说明

+

需求分析

+

视频模块包括包括视频Feed流获取、视频投稿和获取用户投稿列表三个模块

+

1. 视频流接口 /douyin/feed/

+

不限制登录状态,返回按投稿时间倒序的视频列表,视频数由服务端控制,单次最多30个。

+

接口定义:

+
service Feed {
+    rpc Feed (douyin_feed_request) returns (douyin_feed_response) {}
+}
+
+message douyin_feed_request{
+    int64 latest_time = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间
+    string token = 2;  // 可选参数,登录用户设置
+}
+
+message douyin_feed_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    repeated Video video_list = 3; // 视频列表
+    int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
+}
+

2. 发布列表 /douyin/publish/list/

+

登录用户的视频发布列表,直接列出用户所有投稿过的视频。

+

接口定义:

+
service PublishList {
+    rpc PublishList (douyin_publish_list_request) returns (douyin_publish_list_response) {}
+}
+
+message douyin_publish_list_request{
+    int64 user_id = 1; // 用户id
+    string token = 2; // 用户鉴权token
+}
+
+message douyin_publish_list_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    repeated Video video_list = 3; // 用户发布的视频列表
+}
+

3. 视频投稿 /douyin/publish/action/

+

登录用户选择视频上传。

+

接口定义:

+
service PublishAction {
+    rpc PublishAction (douyin_publish_action_request) returns (douyin_publish_action_response) {}
+}
+
+message douyin_publish_action_request{
+    string token = 1; // 用户鉴权token
+    bytes data = 2; // 视频数据
+    string title = 3; // 视频标题
+}
+
+message douyin_publish_action_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+}
+

整体架构设计

+

pSodXSP.png

+

返回的状态码(虽然客户端并没有逻辑进行处理):

+
    +
  • 用户不存在,状态码为2
  • +
  • 应该携带Token但是没有携带,状态码为4
  • +
  • 备份文件夹操作失败,状态码为5
  • +
  • 无法写入视频文件,状态码为6
  • +
  • 无法写入图片文件,状态码为7
  • +
  • 无法上传文件到OSS,状态码为8
  • +
+

详细设计

+

视频流接口

+

pSowAS0.png

+
    +
  1. DY-api.Feed处理请求,准备请求服务
  2. +
  3. 首先请求DY-srv.Feed服务,根据时间戳查询数据库,查询出不超过时间戳的前30个视频,查询后返回视频列表
  4. +
  5. 随后并行请求视频列表中的每一个视频(即最大并发数为30)
  6. +
  7. 对每一个视频,根据前一个服务响应的作者的id并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录Author响应相关的5个字段
  8. +
  9. 对每一个视频,根据视频id并行请求DY-srv.和DY-srv.,对于每个视频 +
      +
    1. commentCount通过获取DY-srv.返回的切片长度获取
    2. +
    3. favoriteCount通过获取DY-srv.返回的切片长度获取
    4. +
    5. 通过Token获取当前的登录用户id,在DY-srv.切片内部查询,如果查询到为True,否则为False
    6. +
    +
  10. +
  11. 等待全部的视频返回响应后,构建响应结构体并返回给客户端
  12. +
+

发布列表

+

pSow3Sx.png

+
    +
  1. DY-api.PublishList处理请求,准备请求服务
  2. +
  3. 首先请求DY-srv.PublishList服务,根据id查询数据库,如果id在数据库中不存在,则直接返回错误,然后根据用户id查询发布的视频列表并返回
  4. +
  5. 随后并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录User响应相关的5个字段
  6. +
  7. 对每一个视频,根据视频id并行请求DY-srv.和DY-srv.,对于每个视频 +
      +
    1. commentCount通过获取DY-srv.返回的切片长度获取
    2. +
    3. favoriteCount通过获取DY-srv.返回的切片长度获取
    4. +
    5. 通过Token获取当前的登录用户id,在DY-srv.切片内部查询,如果查询到为True,否则为False
    6. +
    +
  8. +
  9. 等待全部的视频返回响应后,构建响应结构体并返回给客户端
  10. +
+

视频投稿

+

pSsa6xK.png

+
    +
  1. DY-api.PublishAction处理请求,将请求中的字段传递到服务端DY-srv.PublishAction
  2. +
  3. 服务端从Token中获取id信息,如果无法获取id,直接返回错误
  4. +
  5. 服务端根据id信息查询数据库,获取用户信息,如果id并不存在于数据库,则直接返回错误
  6. +
  7. 服务端判断本地存放视频与图片文件的文件夹是否存在,如果不存在则创建文件夹
  8. +
  9. 服务端将接收到的请求中的字节流写入文件,并调用ffmpeg对视频的第一帧进行截图作为封面,同样写入图片文件
  10. +
  11. 服务端将文件上传信息传递给消息队列,直接返回成功响应给客户端
  12. +
  13. 消息队列接收到消息后并行上传视频和图片文件,两者都上传成功后将视频信息写入数据库
  14. +
+ + +
+ +
+
+ + + + + + +
+
+
简易抖音项目-视频模块
+
https://zhangzhao219.github.io/2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Videos/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月24日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/28/6.824/Distributed-Systems-MIT-6.824-LEC-7/index.html b/2023/01/28/6.824/Distributed-Systems-MIT-6.824-LEC-7/index.html new file mode 100644 index 000000000..a1935de45 --- /dev/null +++ b/2023/01/28/6.824/Distributed-Systems-MIT-6.824-LEC-7/index.html @@ -0,0 +1,820 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-LEC 7 Fault Tolerance-Raft-2 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-LEC 7 Fault Tolerance-Raft-2

+ + +
+ +

MIT-6.824(Spring 2022)LEC 7 Fault Tolerance-Raft-2

+ +

Raft

+

Leader选举规则

+
    +
  1. 获得大多数的投票
  2. +
  3. 至少是最新的-最后一个term相同就可以给选票
  4. +
  5. 任期号相同则最长的一个获得Leader
  6. +
  7. 如果存在最后一个term号比当前发起选举的Candidate大,则Candidate自动放弃选举
  8. +
+

日志追赶

+
    +
  1. Leader发送心跳信号,连带自己的前一个term和前一个日志达到的索引号
  2. +
  3. follower查看自己的前一个term,如果小于Leader的term,返回不允许追加的信息,因为自己落后了
  4. +
  5. Leader的nextIndex减1,然后与Follower反复迭代,直到找到了两者第一个相同的位置
  6. +
  7. 然后Leader更新自己与这个Follower的matchIndex。可以认为nextIndex是乐观的,从最后一个开始往前遍历,而matchIndex是悲观的,最开始的时候直接设置为0
  8. +
  9. Leader与Follower进行日志同步
  10. +
+

日志擦除可能会带来一些问题,论文中的Figure 8 说明了这个问题,因此需要有日志提交条件的额外限制Leader 在当前任期至少有一条日志被提交

+

前面的协议中一直是减1操作,因此如果Follower落后过多,通信开销会很大

+

追赶更快的优化算法:并不按照索引后退,而是按照term后退,然后再扫描相同的位置

+

此时Follower并不只是拒绝,而是返回前一个term以及这个term开始的索引

+

持久化

+

重启机器会发生什么?

+
    +
  • 看成一台新机器加入,可能会复制大量的日志
  • +
  • 从自己的最后的持久化状态开始追赶
  • +
+

需要持久化什么信息?我们应该尽量不保存信息,因为需要存入磁盘,开销很大,只需要保留必要的信息

+
    +
  • 投票的信息
  • +
  • 日志信息:承诺Leader这些条目都是已经提交过的
  • +
  • 当前的term:term不可以下降,需要监控term上升,获得自己的投票信息
  • +
+

服务恢复

+
    +
  • 根据全部日志重建状态,一定会获得与之前完全相同的状态,太长了可能开销过大
  • +
  • 周期性进行快照操作,持久化到磁盘上,可以通过快照对日志进行裁剪,开销不会过大
  • +
+

状态机通过apply channel获得一个快照,然后使用它来进行恢复

+

使用Raft

+

步骤:

+
    +
  1. 客户端发送操作给Leader的K/V服务器
  2. +
  3. K/V服务器将操作传递给Raft
  4. +
  5. Raft写入日志
  6. +
  7. Raft与其他服务器通信传送日志
  8. +
  9. 其他服务器发送响应给Leader
  10. +
  11. Leader提交操作(其他的Followers需要等到下一次交互才确认前面的操作并提交)
  12. +
  13. 操作按照顺序传送到K/V服务器
  14. +
  15. K/V服务器执行操作
  16. +
  17. Leader返回操作结果给客户端
  18. +
+

客户端也需要保存Raft的Leader和Follower的信息,可以切换它的通信对象

+

客户端如果没有接收到服务器的响应会进行重试,而服务器可鞥已经执行过这些操作了,因此需要对这些重复的操作进行检测。

+

一种实现方法:客户端的每一个操作都带有一个id,通过id对重复的操作进行过滤

+

正确性

+

模糊定义:多台机器的行为如同单独的一台机器一样

+

精确定义:

+

线性一致性:

+
    +
  • 有一个整体的顺序,操作按照顺序逐步执行
  • +
  • 实时匹配
  • +
  • 读取操作应该始终返回最后一次写入的值
  • +
+

查看历史操作,即使是并行的程序是否可以在一台机器上执行相同的结果,从而判断是否满足线性一致性。

+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-LEC 7 Fault Tolerance-Raft-2
+
https://zhangzhao219.github.io/2023/01/28/6.824/Distributed-Systems-MIT-6.824-LEC-7/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/29/6.824/Distributed-Systems-MIT-6.824-Lab-2/index.html b/2023/01/29/6.824/Distributed-Systems-MIT-6.824-Lab-2/index.html new file mode 100644 index 000000000..94bc5f519 --- /dev/null +++ b/2023/01/29/6.824/Distributed-Systems-MIT-6.824-Lab-2/index.html @@ -0,0 +1,1773 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MIT-6.824 Distributed Systems-Lab 2 Raft - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MIT-6.824 Distributed Systems-Lab 2 Raft

+ + +
+ +

MIT-6.824(Spring 2022)Lab 2 Raft

+ +

6.824 Lab 2: Raft

+

简介

+

https://raft.github.io/

+

这是构建容错k/v存储系统的一系列实验室中的第一个。这个实验室将实现复制状态机协议Raft。

+

复制服务通过在多个副本服务器上存储其状态(即数据)的完整副本来实现容错。复制允许服务继续运行,即使某些服务器出现故障(崩溃或网络问题)。挑战在于,故障可能会导致复制副本保存不同的数据副本。

+

Raft将客户端请求组织成一个序列,称为日志,并确保所有副本服务器都看到相同的日志。每个副本按日志顺序执行客户端请求,并将它们应用于服务状态的本地副本。由于所有活动副本都看到相同的日志内容,因此它们都以相同的顺序执行相同的请求,从而继续具有相同的服务状态。如果服务器出现故障,但稍后恢复,Raft会负责更新其日志。只要有大多数服务器处于活动状态,并且可以相互通信,Raft就会继续运行。如果没有这样的大多数,Raft将会暂时停机,但一旦大多数服务器能够再次通信,Raft就会恢复原来的状态。

+

在这个实验中,将会把Raft实现为一个Go对象类型,并实现相关的方法,这意味着要在更大的服务中将Raft用作模块。一组Raft实例通过RPC相互通信,以维护复制的日志。Raft接口将支持无限序列的编号命令,也称为日志条目。条目用索引编号进行编号。具有给定索引的日志条目最终会被提交。此时,Raft应该将日志条目发送到更大的服务以供其执行。

+

您应该遵循扩展的Raft论文中的设计,尤其是图2。您将实现本文中的大部分内容,包括保存持久状态,并在节点发生故障后重新启动后读取该状态。不实现第6节提到的集群成员资格更改。

+

最具挑战性的部分可能不是实现解决方案,而是调试解决方案。为了帮助解决这一挑战,您可能需要花时间思考如何使实现更易于调试。

+

我们还提供了一个Raft交互的图表,可以帮助阐明Raft代码如何与上面的层进行交互。

+

pSdAYtS.jpg

+

参考资料

+

Students' Guide to Raft

+

(几年前编写,特别是2D部分已经发生了变化)

+

背景

+

Raft 是一种共识算法,旨在轻松理解。它与Paxos的容错和性能相当。不同的是,它被分解成相对独立的子问题,它干净地解决了所有主要部分的实际系统需求。我们希望Raft可供更广泛的受众使用,并且这些更广泛的受众将是能够开发各种更高质量的基于共识的系统。

+

可视化网站

+

与所有分布式共识协议一样,细节很难理解。在没有故障的稳定状态下,Raft 的行为易于理解,并且可以直观地解释。例如,从可视化中很容易看出, 假设没有失败,最终将选出Leader,并且最终,发送给Leader的所有操作都将由Follower按照顺序正确执行。但是,当消息延迟,网络分区或者服务故障,细节变得至关重要。特别是,我们可能一遍又一遍地重复许多错误,仅仅是由于阅读论文时的误解或疏忽。这个问题并非Raft所独有。

+

实现Raft

+

Raft 的最终指南在 Raft 论文的图 2 中。这个图片指定在Raft服务器之间交换的每个RPC的行为, 给出服务器必须维护的各种不变量,并指定何时应执行某些操作。我们将在本文的其余部分大量讨论图 2。它需要一字不差地遵循。

+

图 2 定义了每个服务器在各种状态下应该对每个传入的 RPC应该做什么,以及何时发生某些其他事情(例如就像在日志中应用条目是安全的一样)。图 2 非常精确,每一条语句在规范术语中,它应该被视为必须,而不是应该。例如,您可以合理地重置一台服务器的选举计时器,只要您收到或RPC,都表明其他服务器要么认为它是Leader,或者是努力成为Leader。直觉上,这意味着我们不应该干扰。但是,如果您仔细阅读图 2,它会说:如果选举超时过去而没有收到当前Leader的RPC或投票给其他的服务器,则转换为Candidate。

+

事实证明,区别很重要,因为前一种实现在某些情况下,可能导致活性显著降低。

+

细节的重要性

+

考虑一个例子。Raft论文在许多地方提到了心跳RPC。具体来说,领导者将偶尔(每个检测信号间隔至少一次)向所有服务器发送 RPC,以防止它们启动新的选举。如果领导者没有要发送到特定对等方的新条目, RPC 不包含任何条目,被视为心跳。

+

我们的许多学生认为心跳在某种程度上是“特别的”,当服务器收到心跳时,它应该以不同的方式对待它。特别是,许多人会只在收到心跳时重置他们的选举计时器,然后返回成功,而不执行图2中指定的任何检查。这是极其危险的。通过接受 RPC, Follower隐式地告诉Leader他们的日志与Leader匹配并包括参数中包含的内容。收到回复后,领导可能错误地确定某个条目已被复制到大多数服务器,并开始提交它。

+

许多人遇到的另一个问题是在收到心跳时,他们会截断Follower的记录,然后添加参数中包含的日志条目。这也是不正确的。图 2说明,如果现有条目与新条目冲突(相同的索引但 不同的任期),删除现有条目及其后面的所有条目。

+

这里的如果至关重要。如果Follower拥有Leader的所有条目,Follower不得截断其日志。必须保留Leader发送的条目之后的任何元素。这是因为我们可能从Leader收到过期的RPC,截断日志将意味着“收回”我们可能已经告诉Leader的我们的日志。

+

调试Raft

+

在调试时,Raft通常有四个主要的错误来源: 活锁、不正确或不完整的 RPC 处理程序、未能遵循规则和术语混淆。死锁也是一个常见问题,但它们通常可以通过记录所有锁和解锁来调试,并且弄清楚你正在占有哪些锁且没有释放。

+

活锁

+

当系统活锁时,系统中的每个节点都在执行一些东西,但总的来说,你的节点没有取得进展。一个活锁场景特别频繁出现:没有领导人被选举出来,或者一个领导者被选举出来后另一个节点马上开始选举,迫使最近当选的领导人立即退位。

+

出现这种情况的原因有很多:

+

确保在图 2说明的时候准确重置选举计时器。具体来说,有三种情况:

+
    +
  • 从当前Leader那里获得 RPC (如果参数中的任期已过时,则不应重置计时器)
  • +
  • 正在开始选举
  • +
  • 向其他服务器投票。
  • +
+

最后一种情况在不可靠的网络中尤其重要,其中Follower可能有不同的日志,在这些情况下, 只有少量的服务器使得大多数服务器都愿意投票支持。如果每当有人要求您投票给他们时都重置选举计时器,会使日志过时的服务器同样有可能向前迈进

+

事实上,因为很少的服务器有足够的最新的日志,这些服务器不太可能在足够和平的情况下进行选举。如果您遵循图 2,具有最新日志的服务器不会被过时的服务器选举打断,因此更有可能完成选举并成为Leader。

+

按照图 2 的说明操作了解何时应开始选举。 特别要注意的是,如果您是Candidate,但选举计时器触发,应该开始另一次选举。这对于避免由于 RPC 延迟或丢弃而导致系统停止非常重要。

+

在处理传入的 RPC 之前 ,请确保遵循“服务器规则”中的第二条规则。第二条规则规定:如果 RPC 请求或响应包含术语set ,则转换为Follower

+

例如,如果您已经在当前任期内投票,并且传入的RPC有一个更高的任期号,你应该首先下台并采用他们的任期(从而重置),然后处理RPC,处理的过程中就会进行投票

+

不正确的 RPC 处理程序

+

尽管图 2 准确地说明了每个 RPC 处理程序应该执行的操作, 一些细节仍然很容易被忽略。

+

如果步骤显示“回复错误”,这意味着您应该立即回复,不要执行任何后续步骤。

+

如果你得到一个指向日志末尾的RPC,应该像确实有该条目,但该任期不匹配处理这个。

+

如果领导者没有发送任何条目,RPC处理程序的检查 2 应执行。

+

#5 是必要的, 并且需要使用最后一个新条目的索引进行计算。 这是因为日志中可能存在与领导者日志不同的条目。因为 #3 规定您只有在有冲突的条目情况下才会截断日志,这些条目不会被删除,如果超出领导发送给您的条目,您可能会应用不正确的条目。

+

实施“最新日志”检查非常重要。只是检查长度!

+

不遵守规则

+

虽然 Raft 论文非常明确地说明了如何实现每个 RPC 处理程序,它还留下了许多规则的实现和未指定的不变量。这些列在“服务器规则”中 图 2 右侧的块。虽然其中一些是不言自明的,也有一些需要非常小心地设计,以免违反规则:

+

如果在执行过程中的任何时候应用特定的日志条目。请务必确保仅由一个实体完成此应用程序。具体来说,您需要有一个专门的 “应用器”,或者锁定这些应用,以便其他一些例程不会同时检测到需要应用条目。

+

确保定期更新,或更新后进行检查。例如,如果您在发送给同行的同时进行检查,您可能需要等到下一个条目追加到日志中后再应用您刚刚发送并得到确认的条目。

+

如果领导者发出 RPC,并且被拒绝,但不是因为日志不一致(这只有在我们的任期中才会发生),那么您应该立即下台并且不更新。

+

领导者不允许更新到上一任期(或就此而言,未来任期)的某个地方。因此特别需要检查。这是因为如果这不是他们目前的任期,Raft 领导者无法确定条目是否实际提交(并且将来永远不会更改)。

+

一个常见的问题来源是nextIndex和matchIndex之间的区别。特别是,你可能会观察到matchIndex = nextIndex - 1,而干脆不实现matchIndex。这是不安全的。虽然nextIndex和matchIndex通常在同一时间被更新为类似的值(具体来说,nextIndex = matchIndex + 1),但两者的作用完全不同。它通常是相当乐观的(我们分享一切),并且只在消极的反应中向后移动。例如,当一个领导者刚刚当选时,nextIndex被设置为日志末尾的索引指数。在某种程度上,nextIndex是用于性能的–你只需要将这些东西发送给这个对等体。

+

matchIndex是用于安全的。MatchIndex不能被设置为一个太高的值,因为这可能会导致commitIndex被向前移动得太远。这就是为什么matchIndex被初始化为-1(也就是说,我们不同意任何前缀),并且只在跟随者肯定地确认AppendEntries RPC时才更新。

+

任期混淆

+

任期混淆是指服务器被来自旧任期的RPC所迷惑。一般来说,在收到RPC时,这不是一个问题,因为图2中的规则确切地说明了当你看到一个旧任期时你应该做什么。然而,图2一般没有讨论当你收到旧的RPC回复时你应该做什么。根据经验,我们发现到目前为止,最简单的做法是首先记录回复中的任期(它可能比你当前的任期高),然后将当前任期与你在原始RPC中发送的任期进行比较。如果两者不同,就放弃回复并返回。只有当这两个任期相同时,你才应该继续处理回复。

+

一个相关但不完全相同的问题是,预设你的状态在你发送RPC和你收到回复之间没有变化。这方面的一个很好的例子是,当你收到RPC的响应时,设置matchIndex = nextIndex - 1,或者matchIndex = len(log)。这并不安全,因为这两个值都可能在你发送RPC后被更新。相反,正确的做法是将 matchIndex 更新为你最初在 RPC 中发送的参数中 prevLogIndex + len( entries[]) 。

+

Raft的结构

+

一个Raft实例必须处理外部事件的到来(Start()调用、AppendEntries和RequestVote RPC以及RPC回复),它必须执行定期任务(选举和心跳)。有许多方法可以构造Raft代码来管理这些活动,下面是一些想法。

+
    +
  • 每个Raft实例都有一组状态(日志、当前索引、&c) 必须根据在goroutine并行同时发生的事件进行更新。Go文档指出,goroutine可以使用共享数据结构和锁直接执行更新操作,或者通过在channel上传递消息。经验表明,对于Raft使用共享数据和锁是最简单的。
  • +
  • Raft实例有两个时间驱动的活动:Leader必须发送心跳信号,如果距离上一次接收到心跳信号的时间太长,其他人必须开始选举。每一个活动最好单独启动一个专门的长时间运行的goroutine,而不是将多个活动组合成一个单独的goroutine
  • +
  • 选举超时的管理是很头痛的。最简单的方法是在Raft数据结构中包括上一次Follower接收到Leader消息的时间,然后让负责选举的goroutine定期检查这个时间是否超时。使用time.Sleep()和一个小常量参数驱动定期检查是很容易的。不要使用time.Ticker和time.Timer,它们很难正确使用。
  • +
  • 需要有一个单独的长时间运行的goroutine在applyCh上按顺序提交日志条目。它必须是单独的,因为在applyCh上发送可以被阻止;而且必须是单个
    +goroutine,否则很难确保发送日志是按照日志顺序的。advance commitIndex的代码需要kick apply goroutine;使用sync.Cond可能最简单。
  • +
  • 每个RPC应该以自己的方式发送(并处理其回复)自己的goroutine,原因有两个:这样无法访问的服务器不会延迟大多数回复的收集,而且心跳信号和
    +选举计时器可以一直计时。如果RPC应答处理在同一个goroutine中就很容易做到,而不是通过channel发送回复的信息。
  • +
  • 请记住,网络可能会延迟RPC和RPC响应,而且如果发送并行的RPC,网络可以对请求和答复进行重新排序。图2很好地指出了RPC处理程序必须对此小心(例如,RPC处理程序应该忽略具有旧日志条目的RPC)。图2并不总是明确说明RPC响应的处理过程。Leader在处理RPC响应时必须小心,它必须检查自从发送RPC之后日志条目没有改变,并且必须考虑对同一Follower的并发的RPC改变了Leader的状态(例如nextIndex)。
  • +
+

Raft中的锁

+
    +
  1. 当有多个goroutine使用的数据时,且至少有一个goroutine可以修改数据,那么goroutine应该使用锁防止同时使用数据。Go race检测器非常擅长检测违反此规则的情况。
  2. +
  3. 每当代码对共享数据进行一系列修改时,如果其他goroutine查看了数据,可能会出错,因此在整个过程中都应该使用锁。
  4. +
  5. 每当代码对共享数据进行一系列读取时(或读取和写入),如果另一个goroutine在中途修改数据,则会发生错误。因此在整个过程中都应该使用锁。真正的Raft代码需要使用很长代码的锁,例如,一个Raft RPC处理程序可能需要在整个处理过程都要加锁。
  6. +
  7. 在做一些可能会等待的事情的时候都加锁是个坏主意,例如:读取Go channel,在channel上发送,等待计时器、调用time.Sleep()或发送RPC并等待回复。一个原因是你可能想让其他的goroutine在等待期间照常执行。另一个原因是避免死锁。想象两个服务器在保持锁的同时彼此发送RPC;两个RPC
    +处理程序需要接收对方的锁;两个RPC处理程序都不能完成,因为它需要等待的RPC调用所持有的锁。等待的代码应该首先释放锁。如果这不方便,有时创建一个单独的goroutine来执行等待是很有用的。
  8. +
  9. 要小心扔掉和重新获取锁的情况。一个可能出现这种情况的地方是避免带锁等待。例如,下面的发送投票RPC的代码是不正确的:
  10. +
+
rf.mu.Lock()
+ rf.currentTerm += 1
+ rf.state = Candidate
+ for <each peer> {
+   go func() {
+     rf.mu.Lock()
+     args.Term = rf.currentTerm
+     rf.mu.Unlock()
+     Call("Raft.RequestVote", &args, ...)
+     // handle the reply...
+   } ()
+ }
+ rf.mu.Unlock()
+

这个代码在单独的goroutine中发送每个RPC。这是不正确的,因为如果周围的代码是决定成为Candidate,args.Term可能与rf.currentTerm不同。当周围的代码创建goroutine和当goroutine读取rf.currentTerm时可能过去了很多的时间,这台服务器也可能不再是Candidate。一种方法是当外部代码持有锁的时候创建rf.currentTerm的副本从而让goroutine去使用。同样的,在调用之后的回复处理代码重新获取锁后必须重新检查所有相关的假设,例如,它应该检查自从决定成为Candidate后rf.currentTerm没有再次改变。

+

一种方法是从没有锁的代码开始,然后仔细考虑需要在哪里添加锁以变得正确。另一个更务实的方法从观察开始,如果没有并发性(没有同时执行goroutine)则根本不需要锁。但是当RPC系统创建goroutine以执行RPC处理程序时,以及
+因为您需要在单独的goroutine中发送RPC以避免等待,并发性就有了。可以通过识别所有goroutine开始的位置(RPC处理程序、在Make()中创建的后台goroutine,&c),并且在每个goroutine开始的时候获得锁,只有当goroutine
+完全完成并返回的时候才释放锁,从而消除并发性。这个锁定协议确保任何重要的事情都不会并行执行;锁确保每个goroutine在其他goroutine执行之前完成,没有并行执行,很难违反规则1、2、3或5。如果每个goroutine的代码正确,在使用锁抑制并发时仍然是正确的。

+

然而,规则4可能是一个问题。所以下一步是找到代码等待的位置,然后根据需求添加锁释放和重新获取(或goroutine的创建),记得小心重新建立和重新获取后的情况。

+

代码相关

+

框架代码:src/raft/raft.go

+

测试代码:src/raft/test_test.go,运行go test即可

+

通过在src/raft/raft.go中增加代码实现Raft,必须遵循下面的接口:

+
// create a new Raft server instance:
+rf := Make(peers, me, persister, applyCh)
+
+// start agreement on a new log entry:
+rf.Start(command interface{}) (index, term, isleader)
+
+// ask a Raft for its current term, and whether it thinks it is leader
+rf.GetState() (term, isLeader)
+
+// each time a new entry is committed to the log, each Raft peer
+// should send an ApplyMsg to the service (or tester).
+type ApplyMsg
+

服务调用 Make(peers, me, ...)创建一个 Raft peer。peers 参数是所有 Raft peers(包括这一个)的网络标识符数组,用于 RPC。me参数是网络标识符数组中,属于这个peer的网络标识符的下标。Start(command) 要求 Raft 启动处理,将命令追加到日志副本中。Start()应立即返回,无需等待日志追加完成。该服务希望你将每个新的日志条目,封装为 ApplyMsg,发送给Make函数中的 applyCh参数(这是一个channel)。

+

raft.go包含发送 RPC sendRequestVote()和处理传入 RPC RequestVote()的样例代码。您的 Raft peers 应该使用 labrpc Go 包(源代码在 src/labrpc)交换 RPC。测试代码可以告诉 labrpc 延迟 RPC请求,重新排列它们,并丢弃它们以模拟各种网络故障。Raft 实例必须仅与 RPC 交互;例如,不允许它们使用共享的 Go 变量或文件进行通信。

+

后续的实验也在此实验上进行构建。

+

参考翻译:https://zhuanlan.zhihu.com/p/248686289

+

Part 2A:选举Leader

+

指导

+

实现Raft算法中的Leader选举和心跳机制(AppendEntries RPC 且没有日志条目)。确保只有一个Leader被选中,且若无错误该Leader会一直唯一存在,当该Leader下线或发生其他错误导致发出的数据无法被成功接收,则会产生新的Leader来替代。

+
    +
  1. 运行 go test -run 2A 来验证代码的正确性
  2. +
  3. 参考论文的Figure 2实现,需要关注发送和接收RequestVote RPCs,与选举相关的服务器的规则,和与选举相关的服务器的状态
  4. +
  5. raft.go中添加Figure 2的Leader选举的状态,同时也需要定义一个结构体保留日志条目的信息
  6. +
  7. 填充 RequestVoteArgsRequestVoteReply结构。修改 Make()以创建一个后台 go 协程,该协程将在一段时间未从其他 peers 那里听到请求投票 RPC 时,发送 RequestVote RPC 来定期启动 Leader 选举。这样,如果已经有一个 Leader,或者自己成为 Leader,其他 peers 就会知道谁是Leader。实现 RequestVote() RPC 函数,以便服务器投票给别人。
  8. +
  9. 为了实现心跳检测,请提前定义 AppendEntries RPC 结构(尽管您可能还不需要所有参数),并让 Leader 定期发送它们。AppendEntries RPC 函数需要重置选举超时时间,以便其他服务器已当选时,不会以 Leader 的身份继续运行。
  10. +
  11. 确保不同 Peers 不会在同一时间选举超时,否则所有 Peers 将只为自己投票,没有人会成为 Leader。
  12. +
  13. 测试要求 Leader 发送心跳检测 RPC 的频率不超过 10 次/秒。
  14. +
  15. 测试要求您的 Raft 在旧 Leader 失败后5秒内选出新 Leader(如果大多数同行仍然可以沟通)。但是,请记住,在发生分裂投票的情况下(如果数据包丢失或候选人不幸地选择相同的随机回票时间,则可能发生),领导人选举可能需要多轮投票。您必须选择足够短的选举超时(心跳间隔也是如此),确保即使选举需要多次轮断,也能在5秒内完成。
  16. +
  17. 论文第 5.2 节提到选举超时应该在 150 到 300 毫秒范围内。只有当 Leader 发送一次心跳包的远小于 150 毫秒,这种范围才有意义。由于测试将您发送心跳包的频率限制在 10 次/秒内(译者注:也就是大于 100 毫秒),因此您必须使用比论文 150 到 300 毫秒更大的选举超时时间,但请不要太大,因为那可能导致无法在 5 秒内选出 Leader。
  18. +
  19. Go 的 rand 很有用。
  20. +
  21. 您将需要定期执行某些操作,或在一段时间后做些什么。最简单的方法是新起一个协程,在协程的循环中调用time.Sleep()。不要使用 time.Timertime.Ticker,这两个并不好用,容易出错。
  22. +
  23. 如果代码在通过测试时遇到问题,请再次阅读论文的 Figure 2 ;Leader 选举的逻辑分布在Figure 2 的多个部分。
  24. +
  25. 别忘了实现 GetState()
  26. +
  27. 测试调用您的 Raft 的 rf.Kill()时,您可以先调用 rf.killed()再检查是否 Kill()。您可能希望在所有循环中执行此功能,以避免已经死亡的 Raft 实例打印令人困惑的信息。
  28. +
  29. 调试代码的一个好方法,就是在 Peer 发送或收到消息时打印自己的状态,并在测试时运行 go test -run 2A > out,将日志收集到文件中。然后,通过研究 out 文件,可以确定实现中不正确的地方。您可能会喜欢用 util.go中的 Dprintf函数来调试,其可以在不同情况下打开和关闭日志。
  30. +
  31. Go RPC 仅发送以大写字母为首的结构体字段(译者注:可导出的字段)。子结构体还必须具有大写字段名称(例如数组中的日志记录字段)。labgob包会警告您这一点,不要忽略警告。
  32. +
  33. go test -race测试你的代码,并修复它报告的任何问题。
  34. +
+

输出应该如下面所示:

+
$ go test -run 2A
+Test (2A): initial election ...
+  ... Passed --   3.5  3   58   16840    0
+Test (2A): election after network failure ...
+  ... Passed --   5.4  3  118   25269    0
+Test (2A): multiple elections ...
+  ... Passed --   7.3  7  624  138014    0
+PASS
+ok  	6.824/raft	16.265s
+$
+

每一个“通过”的测试用例会输出五个数字;他们分别是

+
    +
  1. 测试所用的时间(单位:秒)
  2. +
  3. Raft Peer 的数量(通常为 3 或 5)
  4. +
  5. 测试期间发送 RPC 的次数
  6. +
  7. RPC 消息中的字节总数
  8. +
  9. Raft 确定并提交的日志条目数。
  10. +
+

实现

+

参考资料

+

定义 global.go

+

首先需要对代码中不完整的结构体进行填充,论文中的Figure 2有的字段一定保留,其他的字段看情况保留

+

首先定义服务器的状态,用字符串常量表示:

+
// 定义Peer的状态
+type State string
+
+const (
+	Follower  State = "follower"
+	Candidate State = "candidate"
+	Leader    State = "leader"
+)
+

然后定义Raft结构体:

+
type Raft struct {
+	mu        sync.Mutex          // Lock to protect shared access to this peer's state
+	peers     []*labrpc.ClientEnd // RPC end points of all peers
+	persister *Persister          // Object to hold this peer's persisted state
+	me        int                 // this peer's index into peers[]
+	dead      int32               // set by Kill()
+
+	// Your data here (2A, 2B, 2C).
+	// Look at the paper's Figure 2 for a description of what
+	// state a Raft server must maintain.
+
+	// 在所有peer上面的持久性的状态
+	// 在对RPC进行响应之后要在稳定存储上更新
+	currentTerm int // this peer 看到的最新的任期号
+	votedFor    int // 在当前任期获得选票的Candidate的id(如果没有则为-1)
+
+	log []LogEntry // 日志信息
+
+	// 在所有peer上面的变化的状态
+	commitIndex int // 已知的已经被提交的日志条目的最大索引值
+	lastApplied int // 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增)
+
+	// 在Leader上面的变化的状态
+	// 每一次选举后都要重新进行初始化
+	nextIndex  []int // 对于每⼀个服务器,需要发送给他的下⼀个日志条目的索引值(初始化为Leader最后索引值加1)
+	matchIndex []int // 对于每⼀个服务器,已经复制给他的日志的最高索引值
+
+	// 与时间相关的变量
+	electTimeout     int64 // 选举超时时间
+	randomTimeout    int64 // 随机时间
+	heartBeatTimeout int64 // 心跳周期
+
+	// 当前状态
+	state        State // 当前Peer所处的状态(Leader、Candidate或Follower)
+	majorityVote int   // 成为Leader需要获得的最少票数
+	lastReceive  int64
+}
+

其中多定义了6个变量,3个变量与时间相关,分别表示选举超时时间、随机的时间上限和Leader发送心跳的周期时间

+
// 与时间相关的变量
+electTimeout     int64 // 选举超时时间
+randomTimeout    int64 // 随机时间
+heartBeatTimeout int64 // 心跳周期
+

最后3个变量,第1个表示服务器当前所处的状态,第2个表示成为Leader需要获得的最少票数,这个值提前计算出来,最后一个值表示最后一次接收到Leader的心跳信号的时间

+
// 当前状态
+state        State // 当前Peer所处的状态(Leader、Candidate或Follower)
+majorityVote int   // 成为Leader需要获得的最少票数
+lastReceive  int64 // 最后一次接收到Leader的心跳信号的时间
+

工具 util.go

+

服务器不同状态之间的转换比较频繁,因此可以将这些服务器状态转换的代码提取出来编写成工具函数,方便后续直接调用

+
// 转为Leader
+func (rf *Raft) toLeader() {
+	DPrintf("[%d]: convert from [%s] to [%s], term [%d]", rf.me, rf.state, Leader, rf.currentTerm)
+	rf.state = Leader
+	// rf.lastReceive = time.Now().Unix()
+}
+
+// 转为Follower
+func (rf *Raft) toFollower(newTerm int) {
+	DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Follower)
+	rf.state = Follower
+	rf.currentTerm = newTerm
+	rf.votedFor = -1
+	rf.lastReceive = time.Now().Unix()
+}
+
+// 转为Candidate
+func (rf *Raft) toCandidate() {
+	DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Candidate)
+	rf.state = Candidate
+	rf.currentTerm += 1
+	rf.votedFor = rf.me
+	// rf.lastReceive = time.Now().Unix()
+}
+
    +
  1. 转为Leader只需更新自己的状态即可,不需要对其他值做任何的操作。
  2. +
  3. 转为Follower除更新自己的状态之外,要更新自己的任期(因为变为Follower就是因为自己的任期落后),然后要初始化自己的投票状态,并且这个变化的过程隐含了从Leader那里收到心跳包,因此要更新自己的时间。
  4. +
  5. 转为Follower除更新自己的状态之外,要将自己的任期+1(因为变为Candidate是因为接收不到Leader的心跳信息了,认为Leader已经挂了,这个任期不能再用了),然后要初始化自己的投票投给自己。
  6. +
+

然后补充一个预定义的获取服务器状态的方法

+
// return currentTerm and whether this server
+// believes it is the leader.
+func (rf *Raft) GetState() (int, bool) {
+
+	var term int
+	var isleader bool
+	// Your code here (2A).
+	rf.mu.Lock()
+	defer rf.mu.Unlock()
+	isleader = false
+	term = rf.currentTerm
+	if rf.state == Leader {
+		isleader = true
+	}
+	return term, isleader
+}
+

请求投票RPC requestVote.go

+

结构体定义完全按照论文即可,目前不需要其他字段

+
// example RequestVote RPC arguments structure.
+// field names must start with capital letters!
+type RequestVoteArgs struct {
+	// Your data here (2A, 2B).
+	Term         int // Candidate的任期号
+	CandidateId  int // Candidate的 Id
+	LastLogIndex int // Candidate最后一条日志条目的索引
+	LastLogTerm  int // Candidate最后一条日志条目的任期
+}
+
+// example RequestVote RPC reply structure.
+// field names must start with capital letters!
+type RequestVoteReply struct {
+	// Your data here (2A).
+	Term        int  // 当前的任期,接收到了之后Candidate可以更新自己
+	VoteGranted bool // 是否给这个Candidate投票
+}
+

核心RPC:

+
// example RequestVote RPC handler.
+func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
+	// Your code here (2A, 2B).
+	// RPC 请求不一定在什么时候应用,因此必须加锁
+	rf.mu.Lock()
+	defer rf.mu.Unlock()
+	DPrintf("[%d]: received vote request from [%d]", rf.me, args.CandidateId)
+
+	reply.VoteGranted = false
+
+	// 如果参数的任期号还没有我的大,不投票,直接默认值返回即可
+	if args.Term < rf.currentTerm {
+		// 响应中包含当前自己的任期号
+		reply.Term = rf.currentTerm
+		return
+	}
+	// 如果参数的任期号比我的大,则我在这个任期内就只能是它的Follower,则更改我的任期号,而且在这个任期内我要投票给它
+	if args.Term > rf.currentTerm {
+		rf.toFollower(args.Term)
+	}
+	reply.Term = rf.currentTerm // 注意这里任期号已经变化了,因此要重新赋值
+	DPrintf("[%d]: status: term [%d], state [%s], vote for [%d]", rf.me, rf.currentTerm, rf.state, rf.votedFor)
+	// 如果参数的任期号和我的相同,则任期号不变,需要通过日志确定是否投票给它
+	// 这里论文要求的 rf.VotedFor == args.CandidateId 不是很明白
+	if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
+		// Todo:判断日志是否至少更新才可以投票
+		rf.votedFor = args.CandidateId
+		rf.lastReceive = time.Now().Unix() // 更新时间,上面操作相当于与可能的Leader通信过了
+		reply.VoteGranted = true
+		DPrintf("[%d]: voted to [%d]", rf.me, args.CandidateId)
+	}
+}
+

核心就是计算返回的reply中的两个值,第一个是是否投票,第二个是当前服务器的任期号。其中任期号一定要小心,可能服务器自己的状态改变后任期号会随之改变,因此一定要及时更新。

+
    +
  1. 如果请求我投票的任期号还没有我的大,不投票,直接默认值返回即可
  2. +
+
if args.Term < rf.currentTerm {
+	// 响应中包含当前自己的任期号
+	reply.Term = rf.currentTerm
+	return
+}
+
    +
  1. 如果参数的任期号比我的大,则我在这个任期内就只能是它的Follower,则更改我的任期号,而且在这个任期内我要投票给它
  2. +
+
if args.Term > rf.currentTerm {
+	rf.toFollower(args.Term)
+}
+

(这个结构不返回,投票的逻辑在下一个结构)

+
    +
  1. 如果参数的任期号和我的相同,则任期号不变,需要通过日志确定是否投票给它
  2. +
+

rf.votedFor == -1 承接上面的投票逻辑,把情况2的票投了

+

rf.VotedFor == args.CandidateId 在后面要加上对于日志的判断,这里仅仅是简单投票给它

+
if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
+	// Todo:判断日志是否至少更新才可以投票
+	rf.votedFor = args.CandidateId
+	rf.lastReceive = time.Now().Unix() // 更新时间,上面操作相当于与可能的Leader通信过了
+	reply.VoteGranted = true
+	DPrintf("[%d]: voted to [%d]", rf.me, args.CandidateId)
+}
+

在调用的时候,Candidate请求每一台服务器投票给它,如果得到的响应说我的任期号比你还大,也就是上面的情况2,也自动放弃Candidate的地位成为Follower。否则这个Candidate就会得到自己的票。

+
// 向每一个Peer请求投票
+func (rf *Raft) requestVoteToPeer(index int, args *RequestVoteArgs, votesSum *int, votesGet *int, cond *sync.Cond) {
+
+	reply := RequestVoteReply{}
+	ok := rf.sendRequestVote(index, args, &reply)
+	rf.mu.Lock()
+	defer rf.mu.Unlock()
+	defer cond.Broadcast()
+	*votesSum += 1
+	if !ok {
+		return
+	}
+	if reply.Term > rf.currentTerm {
+		rf.toFollower(reply.Term)
+		// } else if reply.VoteGranted && reply.Term == rf.currentTerm {
+	} else if reply.VoteGranted {
+		*votesGet += 1
+	}
+}
+

追加日志RPC appendEntries.go

+

结构体定义完全按照论文即可,目前不需要其他字段

+
type AppendEntriesArgs struct {
+	// Your data here (2A, 2B).
+	Term         int        // Leader的任期号
+	LeaderId     int        // Follower可以通过这个LeaderId重定向客户端
+	PrevLogIndex int        // 新的日志条目紧随之前的索引值
+	PrevLogTerm  int        // PrevLogIndex日志条目的任期
+	Entries      []LogEntry // 存储的日志条目,如果是心跳包则为空
+	LeaderCommit int        // Leader的提交索引
+}
+
+type AppendEntriesReply struct {
+	// Your data here (2A).
+	Term    int  // 当前的任期,接收到了之后Leader可以更新自己
+	Success bool // Follower包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真
+}
+

这个RPC既作为日志更新的来源,在没有日志携带的时候也作为心跳包用于维持Leader的地位

+
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
+	// Your code here (2A, 2B).
+	// RPC 请求不一定在什么时候应用,因此必须加锁
+	rf.mu.Lock()
+	defer rf.mu.Unlock()
+	// 更新至少为当前的任期
+	reply.Term = rf.currentTerm
+	reply.Success = false
+	// 如果Leader的任期还没有我的大,则直接拒绝请求
+	if args.Term < rf.currentTerm {
+		return
+	}
+	// 如果Leader的任期比我的大,则我转为这个任期的Follower
+	if args.Term >= rf.currentTerm || rf.state == Candidate {
+		rf.toFollower(args.Term)
+	}
+	// 如果Leader的任期和我的相同,则操作日志
+	// Todo:日志操作
+	rf.lastReceive = time.Now().Unix()
+	reply.Term = rf.currentTerm
+	reply.Success = true
+}
+

核心也是计算返回的reply中的两个值,第一个是是否更新成功,第二个是当前服务器的任期号。其中任期号一定要小心,可能服务器自己的状态改变后任期号会随之改变,因此一定要及时更新。

+
    +
  1. 如果Leader的任期还没有我的大,则直接拒绝请求
  2. +
+
if args.Term < rf.currentTerm {
+	return
+}
+
    +
  1. 如果Leader的任期比我的大,则如果我是Candidate,放弃Candidate的地位,转为这个任期的Follower
  2. +
+
// 如果Leader的任期比我的大,则我转为这个任期的Follower
+if args.Term >= rf.currentTerm || rf.state == Candidate {
+	rf.toFollower(args.Term)
+}
+

(同时要对我自己的日志进行更新,目前还没有实现)

+
    +
  1. 如果Leader的任期和我的相同,则操作日志(这里没有操作)
  2. +
  3. 更新服务器的时间和返回的参数
  4. +
+
rf.lastReceive = time.Now().Unix()
+reply.Term = rf.currentTerm
+reply.Success = true
+

主要是要对服务器的收到Leader的请求的时间进行更新,从而避免Follower转为Candidate,在Leader存在的情况下发起选举

+

在调用的时候,Leader向其他的每一台服务器发送这个RPC,如果得到的响应说我的任期号比你还大,也就是上面的情况2,也自动放弃Leader的地位成为Follower。

+
// 向指定的Peer增加日志条目或者发送心跳包
+func (rf *Raft) appendEntriesToPeer(index int, args *AppendEntriesArgs) {
+	reply := AppendEntriesReply{}
+	if ok := rf.sendAppendEntries(index, args, &reply); ok {
+		rf.mu.Lock()
+		defer rf.mu.Unlock()
+		// Todo:log相关
+		// 如果响应的任期比Leader更大了,说明Leader需要退位成Follower了
+		if reply.Term > rf.currentTerm {
+			rf.toFollower(reply.Term)
+		}
+	}
+}
+
+

主函数 raft.go

+

初始化

+

每一台服务器初始化的时候都是一个Follower,任期号为0

+

除此之外还要设置选举超时时间,心跳发送时间等

+

以及根据服务器的数量计算好需要多少张选票才能达成共识

+

然后直接开始选举

+
rf.toFollower(0)
+
+rf.electTimeout = 200     // 初始化选举超时时间
+rf.heartBeatTimeout = 100 // 初始化心跳间隔时间
+rf.randomTimeout = 100    // 设置随机时间的最大范围
+
+// 初始化成为Leader需要得到的票数
+if len(rf.peers)%2 == 0 {
+	rf.majorityVote = len(rf.peers)/2 + 1
+} else {
+	rf.majorityVote = (len(rf.peers) + 1) / 2
+}
+
+// start ticker goroutine to start elections
+go rf.leaderElection()
+

所有的协程都不设置退出条件,因此内部要么是无限循环,要么是有状态变量等进行控制

+

选举Leader

+

选举Leader是一个无限循环,在每一次循环的时候记录当前的时间后进行睡眠(固定时间+随机时间),然后在循环内部进行判断,如果上一次循环到这里的实时时间比上一次接收到心跳包的时间还大,说明在睡眠时间内一直没有接收到心跳包,则认为超时,此时就要放弃自己的Follower身份,转为Candidate开始竞选。

+
// The ticker go routine starts a new election if this peer hasn't received
+// heartsbeats recently.
+func (rf *Raft) leaderElection() {
+
+	lastElectTime := time.Now().Unix()
+
+	for !rf.killed() {
+
+		// Your code here to check if a leader election should
+		// be started and to randomize sleeping time using
+		// time.Sleep().
+
+		time.Sleep(time.Duration(rf.electTimeout+rand.Int63n(rf.randomTimeout)) * time.Millisecond)
+
+		rf.mu.Lock()
+		// lastStartTime := startTime
+
+		// 如果上一次循环到这里的实时时间比上一次接收到心跳包的时间还大,说明在睡眠时间内一直没有接收到心跳包,则认为超时
+		if lastElectTime > rf.lastReceive {
+			//DPrintf("[%d]: current state is [%s].", rf.me, rf.state)
+			if rf.state != Leader {
+				DPrintf("[%d]: is not leader, start election.", rf.me)
+				rf.tryLeader()
+			}
+		}
+		lastElectTime = time.Now().Unix() // 更新“上一次”的时间
+		rf.mu.Unlock()
+	}
+}
+

然后在 rf.tryLeader()中,首先将服务器的状态转为Candidate,然后构建请求,向其他的peer发送请求投票的RPC,收到响应后对收到的投票进行统计。如果得到了大多数的选票,则这个Candidate可以转为Leader,同时向其他的服务器发送心跳包说明自己已经成为了Leader,其他的peer需要放弃竞选。

+
func (rf *Raft) tryLeader() {
+	rf.toCandidate()
+
+	votesSum := 1                // 总共的票的数量
+	votesGet := 1                // 收到的票数,自己首先给自己投票
+	cond := sync.NewCond(&rf.mu) // 条件变量,控制投票结果的返回
+	args := RequestVoteArgs{
+		Term:        rf.currentTerm,
+		CandidateId: rf.me,
+	}
+	for i := 0; i < len(rf.peers); i++ {
+		if i != rf.me {
+			go rf.requestVoteToPeer(i, &args, &votesSum, &votesGet, cond)
+		}
+	}
+	// 等待票数统计完毕并判断是否能成为Leader
+	go func() {
+		rf.mu.Lock()
+		defer rf.mu.Unlock()
+
+		for votesGet < rf.majorityVote && votesSum < len(rf.peers) && rf.state == Candidate {
+			cond.Wait()
+		}
+		if votesGet >= rf.majorityVote && rf.state == Candidate {
+			rf.toLeader()
+			// 发送心跳包
+			go rf.logReplication()
+		}
+	}()
+}
+

内部的协程同步使用状态变量控制(虽然不明白为什么使用WaitGroup不可以实现功能)

+

心跳包发送

+

心跳包发送(或与日志更新一起)是只有Leader才可以发起的动作。

+

注意定时发起请求即可

+
// Leader定时发送更新log的请求,同时也作为心跳包
+func (rf *Raft) logReplication() {
+	for !rf.killed() {
+		rf.mu.Lock()
+		if rf.state == Leader {
+			args := AppendEntriesArgs{
+				Term:     rf.currentTerm,
+				LeaderId: rf.me,
+			}
+			for i := 0; i < len(rf.peers); i++ {
+				if i != rf.me {
+					go rf.appendEntriesToPeer(i, &args)
+				}
+			}
+		}
+		rf.mu.Unlock()
+		time.Sleep(time.Duration(rf.heartBeatTimeout) * time.Millisecond)
+	}
+}
+

运行结果

+

目前最快的结果:

+
Test (2A): initial election ...
+  ... Passed --   3.0  3   72   18660    0
+Test (2A): election after network failure ...
+  ... Passed --   4.9  3  166   31952    0
+Test (2A): multiple elections ...
+  ... Passed --   5.3  7  522  111880    0
+PASS
+ok      6.824/raft      13.335s
+

运行10次后均成功

+

Part 2B:日志

+

指导

+

完善 Leader 和 Follower 的代码,使他们可以追加新的日志条目,并通过 go test -run 2B

+
    +
  • 你的第一个目标应该是通过 TestBasicAgree2B()。首先实现 Start(),然后按照 Figure 2,实现 RPC 函数 AppendEntries来收发新的日志条目。通过 applyCh发送每一个新提交的日志条目。
  • +
  • 您需要实现选举限制(论文第 5.4.1 节)。
  • +
  • 在早期的 2B 实验中,测试中未能达成协议的解决办法是:即使领导人还活着,也举行重复的选举。在选举计时器中找到并修复这个 bug ,或在赢得选举后不要立即发送心跳包。
  • +
  • 您的代码可能需要循环检测变量。不要让这些循环不间断连续执行,这将使您的服务运行变慢,最终导致测试失败。使用Go的条件变量或在循环中插入 time.Sleep(10 * time.Millisecond)
  • +
+

如果运行太慢,可能会没法通过接下来的测试。您可以使用 time命令检查您的解决方案使用了多少实时时间和CPU时间。这是典型的输出:

+
$ time go test -run 2B
+Test (2B): basic agreement ...
+  ... Passed --   0.9  3   16    4572    3
+Test (2B): RPC byte count ...
+  ... Passed --   1.7  3   48  114536   11
+Test (2B): agreement after follower reconnects ...
+  ... Passed --   3.6  3   78   22131    7
+Test (2B): no agreement if too many followers disconnect ...
+  ... Passed --   3.8  5  172   40935    3
+Test (2B): concurrent Start()s ...
+  ... Passed --   1.1  3   24    7379    6
+Test (2B): rejoin of partitioned leader ...
+  ... Passed --   5.1  3  152   37021    4
+Test (2B): leader backs up quickly over incorrect follower logs ...
+  ... Passed --  17.2  5 2080 1587388  102
+Test (2B): RPC counts aren't too high ...
+  ... Passed --   2.2  3   60   20119   12
+PASS
+ok  	6.824/raft	35.557s
+
+real	0m35.899s
+user	0m2.556s
+sys	0m1.458s
+$
+

“ok 6.824/raft 35.557s” 意味着 Go 运行 2B 的测试所用的实时时间为 35.557 秒。“user 0m2.556s” 表示代码运行了 2.556 秒的 CPU 时间,或实际运行(而不是等待或睡眠)所花费的时间。如果测试 2B 使用超过 1 分钟的实时时间,或超过 5 秒的 CPU 时间,则以后的实验可能会遇到麻烦。检查睡眠时间、等待 RPC 超时所花费的时间、没有睡眠或等待地检查条件或channel信息的循环、或发送大量 RPC 的地方。

+

实现

+

参考资料

+

2A完善 util.go

+

无论是转为Leader、Follower或者转为Candidate,实际上都可以看成是有一个隐含存在的Leader告诉他们这样做的,因此都要同步更新自己的选举超时时间,防止在有Leader的时候就已经超时,导致Leader的存在时间过短。

+
// 转为Leader
+func (rf *Raft) toLeader() {
+	DPrintf("[%d]: convert from [%s] to [%s], term [%d]", rf.me, rf.state, Leader, rf.currentTerm)
+	rf.state = Leader
+	rf.lastReceive = time.Now().Unix()
+	// 选举为Leader后重新对所有的peer进行初始化
+	for i := 0; i < len(rf.peers); i++ {
+		rf.nextIndex[i] = len(rf.log)
+		rf.matchIndex[i] = -1
+	}
+}
+
+// 转为Follower
+func (rf *Raft) toFollower(newTerm int) {
+	DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Follower)
+	rf.state = Follower
+	rf.currentTerm = newTerm
+	rf.votedFor = -1
+	rf.lastReceive = time.Now().Unix()
+}
+
+// 转为Candidate
+func (rf *Raft) toCandidate() {
+	DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Candidate)
+	rf.state = Candidate
+	rf.currentTerm += 1
+	rf.votedFor = rf.me
+	rf.lastReceive = time.Now().Unix()
+}
+

结构体字段理解

+

首先要注意由于论文中的索引是从1开始计算的,而计算机上切片的索引是从0开始算的,因此论文说明的初始化为0的地方都要初始化为-1

+

nextIndex[]:leader要发送给follower的下一条log entry(各follower不同),follower与leader一致的时候只发最新一条log,有不一致的时候,nextIndex要减,一次发多条log。把不一致的部分都修正过来。

+

matchIndex[]:已知follower上,从0开始有多少条连续的log entry与leader一致。即: 有多少条log entry已经被成功replicate到follower上了。如果过半数,就可以增加commitIndex, apply到状态机, 答复客户端操作成功了

+

commitIndex: 已知被提交的最高日志项对应的index。当日志项被提交(committed)了,意味着该日志项已经成功复制到了集群中的多数派server上,属于“集体记忆”了。如果当前的leader宕机再次发生选举,只有拥有完整已提交日志的server才能够获得多数派选票,才能被选举为leader。根据Leader完整性(Leader Completeness),如果一个日志项在某个term被提交了,则该Entry会存在于所有更高term的leader日志中。

+

lastApplied: 应用(apply)给状态机的最高日志项的index,也就是上层应用“消费”到Raft日志项的最新index。Leader使用nextIndex和matchIndex两个数组来维护集群中其它server的日志状态。

+

其他结构体字段:

+
    +
  • applyCh: 由实验提供,通过该channel将ApplyMsg发送给上层应用。
  • +
  • moreApply: 示意有更多的日志项已经被提交,可以apply。
  • +
  • applyCond: apply时用于多goroutine之间同步的Condition。
  • +
+

Start函数

+

Start函数是raft顶层的服务最开始调用的类似初始化的函数

+

如果server不是leader则返回false。如果是leader的话,那么将command组装成LogEntry后追加到自己的日志中。此处要同时更新leader自己的matchIndex(由于自己就是Leader,自己肯定与自己一致)和nextIndex(如果自己是Follower,这条日志肯定就不能改了)

+
func (rf *Raft) Start(command interface{}) (int, int, bool) {
+	index := -1
+	term := -1
+	isLeader := false
+
+	// Your code here (2B).
+	if !rf.killed() {
+		rf.mu.Lock()
+		defer rf.mu.Unlock()
+		if rf.state == Leader {
+			isLeader = true
+			// 只有是Leader才可以接收日志信息
+			// 添加日志信息
+			rf.log = append(rf.log, LogEntry{
+				Term:    rf.currentTerm,
+				Command: command,
+			})
+			index = len(rf.log) - 1
+			term = rf.currentTerm
+			rf.matchIndex[rf.me] = index    // 已经复制给他的日志的最高索引值
+			rf.nextIndex[rf.me] = index + 1 // 需要发送给他的下⼀个日志条目的索引值
+		}
+		// 论文与代码起始位置索引不同
+		index += 1
+	}
+
+	return index, term, isLeader
+}
+

两个RPC的新增字段

+

请求投票RPC:新增了最后一个日志项的信息

+
    +
  • LastLogIndex 是 candidate 最后一个日志项的 index
  • +
  • LastLogTerm 是 candidate 最后一个日志项的 term
  • +
+

新增日志RPC:(只有Leader才可能发出)

+
    +
  • Entries[]: 发送给对应server的新日志,如果是心跳则为空。这里要发送给对应server日志的index,是从nextIndex到最后一个日志项的index,注意也可能为空。
  • +
  • PrevLogIndex: 紧跟在新日志之前的日志项的index,是leader认为follower当前可能已经同步到了的最高日志项的index。对于第i个server,就是nextIndex[i] - 1。
  • +
  • PrevLogTerm: prevLogIndex对应日志项的term。
  • +
  • LeaderCommit: leader已经提交的commit index。用于通知follower更新自己的commit index。
  • +
+

AppendEntryReply结构体新增了XTerm、XIndex和XLen几个变量用于nextIndex的快速回退。

+

论文中的nextIndex在AppendEntry RPC返回不匹配后,默认只是回退一个日志项(nextIndex[i]=PrevLogIndex)。如果follower能够返回更多信息,那么leader可以根据这些信息使对应server的nextIndex快速回退,减少AppendEntry RPC通信不匹配的次数,从而加快同步日志的步伐。这几个变量的具体含义:

+
    +
  • XLen: 当前follower所拥有的的日志长度。
  • +
  • XTerm: 当前follower的日志中,PrevLogIndex所对应日志项的term。可能为空。
  • +
  • XIndex: 当前follower的日志中,拥有XTerm的日志项的最低index,可能为空。
  • +
+

主函数 Make

+

make()函数中除做一些初始化的工作之外,新增了将已经被提交的日志项返回给上层应用的goroutine

+
// 初始化日志相关
+rf.log = make([]LogEntry, 0)
+rf.commitIndex = -1
+rf.lastApplied = -1
+rf.nextIndex = make([]int, len(peers))
+rf.matchIndex = make([]int, len(peers))
+
+rf.applyCh = applyCh
+rf.moreApply = false
+rf.applyCond = sync.NewCond(&rf.mu)
+
+go rf.appMsgApplier()
+

这个新增的goroutine无限循环判断rf.moreApply字段,一旦发现为真,则触发返回的操作,返回新的提交过的日志给上层应用

+
func (rf *Raft) sendApplyMsg() {
+	rf.moreApply = true
+	rf.applyCond.Broadcast()
+}
+
+func (rf *Raft) appMsgApplier() {
+	for {
+		rf.mu.Lock()
+		// 等待这个字段为真才可以继续
+		for !rf.moreApply {
+			rf.applyCond.Wait()
+		}
+		rf.moreApply = false
+
+		commitIndex := rf.commitIndex
+		lastApplied := rf.lastApplied
+		entries := rf.log
+		rf.mu.Unlock()
+		// 发送已经提交但是还没有返回的日志字段
+		for i := lastApplied + 1; i <= commitIndex; i++ {
+			msg := ApplyMsg{
+				CommandValid: true,
+				Command:      entries[i].Command,
+				CommandIndex: i + 1,
+			}
+			DPrintf("[%d]: apply index %d - 1", rf.me, msg.CommandIndex)
+			rf.applyCh <- msg
+			// 及时加锁更新,否则可能会变化
+			rf.mu.Lock()
+			rf.lastApplied = i
+			rf.mu.Unlock()
+		}
+
+	}
+}
+

返回给上层应用的情况两种:

+
    +
  • Leader在将日志项复制到多数派后更新commitIndex的同时,要调用sendApplyMsg()
  • +
  • Follower在AppendEntry RPC收到LeaderCommit的更新时,也要调用sendApplyMsg()
  • +
+

选举限制

+

在前面选举Leader时,并没有对日志做限制,在这里需要补充日志层面的选举限制

+

首先要在请求投票的结构体中附带自己最后一条日志的信息

+
// Candidate最后一条日志的信息
+lastLogIndex := len(rf.log) - 1
+lastLogTerm := -1
+// 如果日志为空需要添加判断
+if lastLogIndex != -1 {
+	lastLogTerm = rf.log[lastLogIndex].Term
+}
+args := RequestVoteArgs{
+	Term:         rf.currentTerm,
+	CandidateId:  rf.me,
+	LastLogIndex: lastLogIndex,
+	LastLogTerm:  lastLogTerm,
+}
+

然后严格按照论文说明对请求投票的双方进行判断即可:

+

总体原则:candidate的log是否至少和接受者的log一样新

+
    +
  1. 我的log长度为0,那我肯定投票给他了 len(rf.log) ==0
  2. +
  3. candidate的最后的log的任期比我的最后的log的任期大 args.LastLogTerm > rf.log[len(rf.log)-1].Term
  4. +
  5. candidate的最后的log的任期和我的最后的log的任期相同 args.LastLogTerm == rf.log[len(rf.log)-1].Term,但是它的日志长度比我长或一样(它先请求我投票,那么我就投票给他吧)args.LastLogIndex >=len(rf.log)-1
  6. +
+
// 是否没投票或者投给的是这个candidate
+if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
+	// candidate的log是否至少和接受者的log一样新
+	// 1. 我的log长度为0,那我肯定投票给他了
+	// 2. candidate的最后的log的任期比我的最后的log的任期大
+	// 3. candidate的最后的log的任期和我的最后的log的任期相同,但是它的日志长度比我长
+	if len(rf.log) == 0 || (args.LastLogTerm > rf.log[len(rf.log)-1].Term) ||
+		(args.LastLogTerm == rf.log[len(rf.log)-1].Term && args.LastLogIndex >= len(rf.log)-1) {
+		rf.votedFor = args.CandidateId
+		rf.lastReceive = time.Now().Unix() // 更新时间,上面操作相当于与可能的Leader通信过了
+		reply.VoteGranted = true
+		DPrintf("[%d]: voted to [%d]", rf.me, args.CandidateId)
+	}
+}
+

日志复制

+

前期准备(构建请求)

+
// 找到日志的同步位置
+prevLogIndex := rf.nextIndex[index] - 1
+prevLogTerm := -1
+if prevLogIndex != -1 {
+	prevLogTerm = rf.log[prevLogIndex].Term
+}
+// 找到要发送的日志
+var entries []LogEntry
+if len(rf.log)-1 >= rf.nextIndex[index] {
+	entries = rf.log[rf.nextIndex[index]:]
+}
+// 补充结构体
+args := AppendEntriesArgs{
+	Term:         rf.currentTerm,
+	LeaderId:     rf.me,
+	LeaderCommit: rf.commitIndex,
+	PrevLogIndex: prevLogIndex,
+	PrevLogTerm:  prevLogTerm,
+	Entries:      entries,
+}
+

论文的日志匹配性质:

+
    +
  • 如果来自不同日志的两个日志项有相同的index和term,那么它们存储了相同的command。
  • +
  • 如果来自不同日志的两个日志项有相同的index和term,那么它们前面的日志完全相同。
  • +
+

因此只需要判断PrevLogIndex和PrevLogTerm与follower的日志匹配的程度即可,这里只是Leader猜测一下,真正的判断在接收到RPC后完成

+

Follower处理请求

+

在处理AppendEntry RPC的代码中,新增了日志匹配的逻辑。

+

如果日志在prevLogIndex处不包含term为prevLogTerm的日志项,那么返回false,(需要回退才能找到对应的位置)。

+
    +
  • 接收者的日志没有index为prevLogIndex的日志项
  • +
  • 有对应index的日志项但是term不匹配。
  • +
+

回退的逻辑:

+
    +
  1. 记录Follower的日志的长度
  2. +
  3. 找到prevLogIndex的索引位置的任期号并记录任期(一定比prevLogTerm更小)
  4. +
  5. 往回遍历日志,找到第一个是上一步记录的任期的索引,那么这个位置之前一定是与Leader相同的日志,记录索引
  6. +
+
// Reply false if log doesn’t contain an entry at prevLogIndex
+// whose term matches prevLogTerm (§5.3)
+if args.PrevLogIndex >= len(rf.log) || (args.PrevLogIndex >= 0 && rf.log[args.PrevLogIndex].Term != args.PrevLogTerm) {
+	reply.Term = rf.currentTerm
+	// 回退
+	reply.XLen = len(rf.log)
+	if args.PrevLogIndex >= 0 && args.PrevLogIndex < len(rf.log) {
+		reply.XTerm = rf.log[args.PrevLogIndex].Term
+		for i := args.PrevLogIndex; i >= 0; i-- {
+			if rf.log[i].Term == reply.XTerm {
+				reply.XIndex = i
+			} else {
+				break
+			}
+		}
+	}
+	return
+}
+

此外还要注意prevLogIndex可能为-1,意味着日志全都没有匹配上,或者leader此刻还没有日志,此时接收者就要完全服从。

+

接下来是PreLogIndex与PrevLogTerm匹配到的情况,还要额外检查新同步过来的日志和已存在的日志是否存在冲突。如果一个已经存在的日志项和新的日志项冲突(相同index但是不同term),那么要删除这个冲突的日志项及其往后的日志。

+
// If an existing entry conflicts with a new one (same index
+// but different terms), delete the existing entry and all that
+// follow it (§5.3)
+misMatchIndex := -1
+for i := range args.Entries {
+	if args.PrevLogIndex+1+i >= len(rf.log) || rf.log[args.PrevLogIndex+1+i].Term != args.Entries[i].Term {
+		misMatchIndex = i
+		break
+	}
+}
+

将新的日志项追加到日志中

+
// Append any new entries not already in the log
+if misMatchIndex != -1 {
+	rf.log = append(rf.log[:args.PrevLogIndex+1+misMatchIndex], args.Entries[misMatchIndex:]...)
+}
+

最后根据论文,如果 leaderCommit > commitIndex,说明follower的commitIndex也需要更新。为了防止越界,commitIndex取 min(leaderCommit, index of last new entry)。同时要向上层应用发回响应。

+
// If leaderCommit > commitIndex, set commitIndex = min(leaderCommit, index of last new entry)
+if args.LeaderCommit > rf.commitIndex {
+	newEntryIndex := len(rf.log) - 1
+	if args.LeaderCommit >= newEntryIndex {
+		rf.commitIndex = newEntryIndex
+	} else {
+		rf.commitIndex = args.LeaderCommit
+	}
+	DPrintf("[%d]: commit index [%d]", rf.me, rf.commitIndex)
+	rf.sendApplyMsg()
+}
+

Leader处理响应

+

由于RPC在网络中可能乱序或者延迟,我们要确保当前RPC发送时的term、当前接收时的currentTerm以及RPC的reply.term三者一致,丢弃过去term的RPC,避免对当前currentTerm产生错误的影响。

+
reply.Term == rf.currentTerm && rf.currentTerm == args.Term
+

当reply.Success为true,说明follower包含了匹配prevLogIndex和prevLogTerm的日志项,更新nextIndex[serverTo]和matchIndex[serverTo]。这里只能用prevLogIndex和entries来更新,而不能用nextIndex及len(log),因为后两者可能已经被别的RPC更新了,进而导致数据不一致。

+

由于matchIndex发生了变化,我们要检查是否更新commitIndex。根据论文,如果存在一个N,这个N大于commitIndex,多数派的matchIndex[i]都大于等于N,并且log[N].term等于currentTerm,那么更新commitIndex为N。这里必须注意,日志提交是有限制的,Raft从不提交过去term的日志项,即使已经复制达到了多数派。如果要更新commitIndex为N,那么N所对应的日志项的term必须是当前currentTerm。

+

在检查是否更新commitIndex的实现上,我们将matchIndex复制到了matches数组中,通过sort升序排序以方便遍历。然后对matches数组进行遍历,找到大多数都提交的索引位置,随后调用sendApplyMsg(),通知有更多的日志项已经被提交,上层应用可以应用。

+
if reply.Success {
+
+	// 更新服务器的状态
+	rf.nextIndex[index] = prevLogIndex + len(entries) + 1
+	rf.matchIndex[index] = prevLogIndex + len(entries)
+
+	// If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm:
+	// set commitIndex = N
+	matches := make([]int, len(rf.peers))
+	copy(matches, rf.matchIndex)
+	sort.Ints(matches)
+
+	for i := rf.majorityVote - 1; i >= 0 && matches[i] > rf.commitIndex; i-- {
+		if rf.log[matches[i]].Term == rf.currentTerm {
+			rf.commitIndex = matches[i]
+			DPrintf("[%d]: commit index [%d]", rf.me, rf.commitIndex)
+			rf.sendApplyMsg()
+			break
+		}
+	}
+}
+

当reply.Success为false,说明follower的日志不包含在prevLogIndex处并匹配prevLogTerm的日志项,要将nextIndex缩减。此处更新不宜采用自减的方式更新,因为RPC可能会重发,正确的方式是 rf.nextIndex[serverTo] = prevLogIndex

+

在AppendEntryReply中增加了几个变量,以使nextIndex能够快速回退(back up)。如果接下来要尝试匹配的prevLogIndex比follower当前所拥有的的日志长度(XLen)还要大,那么显然直接从XLen尝试匹配即可。如果接下来要尝试匹配的prevLogIndex在XLen以内,因为我们已经知道了follower的日志从XIndex到当前prevLogIndex的日志项的term都是XTerm,那么我们可以直接在leader侧遍历匹配一遍,而无需多次往返RPC通信。

+
} else {
+	// In Test (2C): Figure 8 (unreliable), the AppendEntry RPCs are reordered
+	// So rf.nextIndex[index]-- would be wrong
+	rf.nextIndex[index] = prevLogIndex
+	// 如果接下来要尝试匹配的prevLogIndex比follower当前所拥有的的日志长度(XLen)还要大,那么显然直接从XLen尝试匹配即可。
+	if rf.nextIndex[index]-1 >= reply.XLen {
+		rf.nextIndex[index] = reply.XLen
+	} else {
+		// 如果接下来要尝试匹配的prevLogIndex在XLen以内,因为我们已经知道了follower的日志从XIndex到当前prevLogIndex的日志项的term都是XTerm,那么我们可以直接在leader侧遍历匹配一遍,而无需多次往返RPC通信
+		for i := rf.nextIndex[index] - 1; i >= reply.XIndex; i-- {
+			if rf.log[i].Term != reply.XTerm {
+				rf.nextIndex[index] -= 1
+			} else {
+				break
+			}
+		}
+	}
+}
+

运行结果

+
Test (2B): basic agreement ...
+  ... Passed --   1.3  3   16    4546    3
+Test (2B): RPC byte count ...
+  ... Passed --   2.7  3   48  114510   11
+Test (2B): agreement after follower reconnects ...
+  ... Passed --   7.1  3  116   31767    8
+Test (2B): no agreement if too many followers disconnect ...
+  ... Passed --   4.1  5  160   37664    3
+Test (2B): concurrent Start()s ...
+  ... Passed --   1.2  3   12    3466    6
+Test (2B): rejoin of partitioned leader ...
+  ... Passed --   5.6  3  166   40233    4
+Test (2B): leader backs up quickly over incorrect follower logs ...
+  ... Passed --  34.1  5 2352 2038228  102
+Test (2B): RPC counts aren't too high ...
+  ... Passed --   2.5  3   42   12630   12
+PASS
+ok      6.824/raft      58.652s
+
+real    0m59.353s
+user    0m1.744s
+sys     0m1.630s
+

Part 2C:持久性

+

指导

+

如果基于 Raft 的服务器重新启动,它应该在中断的地方恢复服务。这要求 Raft 在重启后,依旧能确保数据持久化。本文的Figure 2 提到的那些状态应该被持久化。

+

真正的实现会在每次 persistent state 被修改时写磁盘,并在重新启动后从磁盘读取状态。您不需要使用磁盘,而应该通过 Persister 对象保存和恢复 persistent state (请参阅 persister.go)。调用 Raft.Make()时会提供一个 Persister, 其可能会包含 Raft 最近的 persistent state(也可能没有) 。Raft 应从 Persister 初始化其状态(对应方法 ReadRaftState()),并在每次 president state 更改后使用 Persister 保存(对应方法 SaveRaftState())。

+

完善 raft.go中的 persist()readPerisit()函数,实现保存和读取 persistent state。你可能需要使用 labgob encoder 来编码(或者说序列化)persistent state,让 Persister来存储二进制流。欢迎查看 persist()readPerisit()的注释了解更多。labgob很像 go 的 gob,只是会在序列化非导出字段时报错。实现完“ 在每次 persistent state 改变时调用 presist()”后,应通过其余测试。

+

您可能想优化为一次性保存多条日志。查看论文第7页的顶部到第 8 页顶部(用灰色线标记的地方)。论文没有描述清楚细节,你需要自己多考虑一下。 6.824 Raft 的讲座或许也能提供一些帮助。

+

您的代码应通过所有 2C 测试:

+
$ go test -run 2C
+Test (2C): basic persistence ...
+  ... Passed --   5.0  3   86   22849    6
+Test (2C): more persistence ...
+  ... Passed --  17.6  5  952  218854   16
+Test (2C): partitioned leader and one follower crash, leader restarts ...
+  ... Passed --   2.0  3   34    8937    4
+Test (2C): Figure 8 ...
+  ... Passed --  31.2  5  580  130675   32
+Test (2C): unreliable agreement ...
+  ... Passed --   1.7  5 1044  366392  246
+Test (2C): Figure 8 (unreliable) ...
+  ... Passed --  33.6  5 10700 33695245  308
+Test (2C): churn ...
+  ... Passed --  16.1  5 8864 44771259 1544
+Test (2C): unreliable churn ...
+  ... Passed --  16.5  5 4220 6414632  906
+PASS
+ok  	6.824/raft	123.564s
+$
+

最好能多次运行:for i in {0..10}; do go test; done

+

实现

+

Part 2D:日志压缩

+

指导

+

就目前情况而言,重新启动的服务器会重放完整的Raft日志,以恢复其状态。然而,对于长期运行的服务来说,永远记住完整的Raft日志是不现实的。相反,您将修改Raft以与持久存储其状态的“快照”的服务协作,此时Raft将丢弃快照之前的日志条目。其结果是持久数据量更少,重启速度更快。然而,现在有可能一个追随者远远落后,以至于领导者放弃了需要追赶的日志条目;然后领导者必须发送快照以及快照时开始的日志。

+

您的Raft必须提供以下函数 Snapshot(index int, snapshot []byte),服务可以使用其状态的序列化快照调用该函数。

+

在Lab 2D中,测试代码定期调用 Snapshot()。在Lab 3中,您将编写一个k/v服务器调用 Snapshot();快照将包含k/v对的完整表。服务层对每个对等方(而不仅仅是Leader)调用 Snapshot()

+

index参数指示快照中包括的最高日志条目。raft应该在这个参数之前丢弃其日志条目。您需要修改Raft代码以只存储日志尾部。

+

您需要实现论文中讨论的 InstallSnapshot RPC,该RPC允许raft的Leader告诉落后的Raft服务器用快照替换其状态。您可能需要考虑 InstallSnapshot应该如何与图2中的状态和规则交互。

+

当Follower的Raft代码接收到 InstallSnapshot RPC时,它可以使用 applyCh将快照发送到 ApplyMsg中的服务。ApplyMsg结构定义已经包含了您需要的字段(并且是测试代码期望的)。请注意,这些快照只会增加服务的状态,而不会导致服务向后移动。

+

如果服务器崩溃,它必须从持久数据重新启动。您的Raft应该保持Raft状态和相应的快照。使用 persister.SaveStateAndSnapshot(),它对于Raft状态和相应的快照有单独的参数。如果没有快照,则传递nil作为快照参数。

+

当服务器重新启动时,应用程序层读取持久化快照并恢复其保存状态。

+

以前,建议您实现一个名为 CondInstallSnapshot的函数,以避免在 applyCh上发送的快照和日志条目需要协调。这个残留的API接口仍然存在,但不希望实现它:相反,我们建议您只需将其返回true。

+

任务:实现 Snapshot()InstallSnapshot RPC,以及对Raft的更改以支持这些(例如,使用修剪日志的操作)。

+

提示:

+
    +
  1. 修改代码以便能够存储从某个索引X开始的日志部分是一个好的开始。最初,您可以将X设置为零并运行2B/2C测试。然后使用 Snapshot(index)放弃索引之前的日志,并将X设置为索引。如果一切顺利,您现在应该通过第一个2D测试。
  2. +
  3. 您将无法将日志存储在Go切片中,并将Go切片索引与Raft日志索引互换使用;您需要以一种方式对切片进行索引,以说明日志中被丢弃的部分。
  4. +
  5. 下一步:如果Leader没有更新Follower所需的日志条目,则让Leader发送 InstallSnapshot RPC
  6. +
  7. 在单个 InstallSnapshot RPC中发送整个快照。不要实现图13的用于分割快照的偏移机制。
  8. +
  9. Raft必须以允许Go垃圾收集器释放和重新使用内存的方式丢弃旧日志条目;这要求对丢弃的日志条目没有可访问的引用(指针)。
  10. +
  11. 即使日志被修剪,您的实现仍然需要在 AppendEntries RPC中的新条目之前正确发送条目的术语和索引;这可能需要保存和引用最新快照的 lastIncludedTerm/lastIncludedIndex(请考虑是否应持久化)。
  12. +
  13. 在不检测竞争的情况下,全套Lab 2测试(2A+2B+2C+2D)所需的合理时间是6分钟的实时时间和1分钟的CPU时间。使用–race运行时,大约需要10分钟的实时时间和2分钟的CPU时间。
  14. +
+

输出示例:

+
$ go test -run 2D
+Test (2D): snapshots basic ...
+  ... Passed --  11.6  3  176   61716  192
+Test (2D): install snapshots (disconnect) ...
+  ... Passed --  64.2  3  878  320610  336
+Test (2D): install snapshots (disconnect+unreliable) ...
+  ... Passed --  81.1  3 1059  375850  341
+Test (2D): install snapshots (crash) ...
+  ... Passed --  53.5  3  601  256638  339
+Test (2D): install snapshots (unreliable+crash) ...
+  ... Passed --  63.5  3  687  288294  336
+Test (2D): crash and restart all servers ...
+  ... Passed --  19.5  3  268   81352   58
+PASS
+ok      6.824/raft      293.456s
+

实现

+ + +
+ +
+
+ + + + + + +
+
+
MIT-6.824 Distributed Systems-Lab 2 Raft
+
https://zhangzhao219.github.io/2023/01/29/6.824/Distributed-Systems-MIT-6.824-Lab-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月29日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/29/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day06/index.html b/2023/01/29/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day06/index.html new file mode 100644 index 000000000..069a7848d --- /dev/null +++ b/2023/01/29/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day06/index.html @@ -0,0 +1,1136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 【实践课】规则引擎设计与实现 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

【实践课】规则引擎设计与实现

+ + +
+ +

【实践课】规则引擎设计与实现

+ +

【实践课】规则引擎设计与实现

+

一、概述

+

1.1 前言

+

规则引擎是一种嵌入在应用服务中的组件,可以将灵活多变的业务决策从服务代码中分离出来。通过使用预定义的语义模块来编写业务逻辑规则。在执行时接受数据输入、解释业务规则,并做出决策。规则引擎能大大提高系统的灵活性和扩展性。

+

在字节跳动,规则引擎已经在风控识别、活动运营、配置下发等场景得到了广泛的应用。开发人员可以将业务逻辑与服务代码解耦,实现灵活、高效的业务策略发布。目前公司内部基于规则引擎的动态决策系统已经承接了千万级别QPS的决策请求。

+

规则引擎的实现需要在满足大容量、高请求、低延迟的基础上尽可能做到简单易上手。本次课程将会带领大家实现一个简单版的规则引擎。

+

1.2 课程目标

+
    +
  • 了解规则引擎的组成部分和应用场景。
  • +
  • 学习并掌握规则引擎的设计与实现原理。
  • +
  • 明确一个规则引擎的设计目标,并完成各部分的设计与实现步骤拆解。
  • +
  • 动手实现规则引擎项目,完成预定目标。
  • +
  • [课外扩展] 结合其他课程,完成一个在线 规则引擎 服务。
  • +
+

1.3 课程重难点

+

重点

+
    +
  • 规则引擎的设计 。明确设计目标、完成步骤拆解、完成各部分状态机的详细设计
  • +
  • 规则引擎的实现。基于项目工程完成词法分析、语法分析、抽象语法树的执行功能
  • +
+

难点

+
    +
  • 规则引擎的核心原理(理论)。词法分析、语法分析、类型检查、语法树执行
  • +
+

主要涉及到编译原理的部分

+

二、课前预习

+

课前必看!!!

+

本部分是需要大家在上课之前了解的内容,主要是一些基本的概念和原理。

+

在这门课程之前你可能根本没有听说过规则引擎这个东西,当然也可能是浅浅的大概知道这是个什么东西,或者是个规则引擎方面的资深专家(还没毕业,五年工作经验那种🐶,如果是这样请赶紧找我内推)。都没有关系,这门课包教包会!!!(学不会的下课后可以找我们运营人员联系我一对一教学)

+

当然,这门课程还是有一定的门槛的,这也就是我为什么要说这么多一定要让你仔细看看这部分的原因。经过实验,课程的内容如果只依赖于课上老师的讲解,只能做到:能听懂,能跟上,来不及思考。要想能够理解掌握这部分内容,能跟别人battle下,再向自己的知识山峰上加那么一块小石头,得好好预习。

+

开始之前先百度或者Google一下 “规则引擎”简单浏览下哈,📪📪📪另外掘金app上面也有许多不错的文章。可以先浏览看看。

+

2.1 数据结构基础

+

数据结构得学过吧,考多少分?😁

+

这块的预习目标呢,包括以下几个部分

+
    +
  • 精通常用数据结构:数组、结构体、指针、队列、二叉树 等等等,课本上有的都得看看
  • +
  • 熟练掌握二叉树的各种遍历方式:前中后序遍历,层序遍历,打印二叉树,有时间可以自己写几个小demo,当然最基础的是需要知道各种遍历方式的区别
  • +
+

2.2 Go语言基础

+
    +
  • 掌握Go语言的基础语法,能读懂项目代码
  • +
+

是的,就这一个要求,其实学完青训营的前几节课就可以达到了

+

2.3 编译原理基础

+

编译原理被誉为"程序员的三大浪漫"之一,足以可见这块知识的深度与广度,我们这次课程也是简单的介绍一下与规则引擎相关的概念。

+

那么可能会有疑问了,不是讲规则引擎么?为啥还得学编译原理?

+

规则引擎的本质呢就是我们自己定义一套语法,然后去解析用这套语法写的表达式,然后根据解析的内容执行表达式。这个过程其实就是编译和执行的过程。

+

因此呢需要自行了解以下的内容

+
    +
  • 编译的概念: +
      +
    • 编译的过程发生了什么?
    • +
    • 一般分为哪几个步骤,每个步骤的中间结果是什么?
    • +
    +
  • +
  • 词法分析: +
      +
    • 词法如何表示?| 正则文法
    • +
    • 词法分析阶段的输出是什么
    • +
    • 词法分析阶段是怎么做的?
    • +
    • 词法分析可能会产生什么问题?
    • +
    • 如何解决词法分析过程中产生的问题?| 左递归问题怎么解决
    • +
    +
  • +
  • 语法分析 +
      +
    • 语法如何表示?上下文无关语法、巴克斯范式怎么理解
    • +
    • 语法分析阶段的输出是什么? 一般怎么表示
    • +
    • 语法分析有哪些方式?什么是递归下降算法?
    • +
    +
  • +
  • 抽象语法树 +
      +
    • 抽象语法树是什么?
    • +
    • 抽象语法树如何执行?
    • +
    +
  • +
  • 类型检查 +
      +
    • 类型检查怎么做?有哪些方式?
    • +
    • 类型检查什么时候做?有什么区别?
    • +
    +
  • +
+

2.4 环境搭建

+

课程之前,大家需要根据项目工程,来完成环境的搭建和Demo的运行

+

项目地址:

+

github.com/qimengxingy…

+

相信大家已经完成了Go环境的搭建,项目工程依赖了hertz框架,如果在之前的课程中完成了项目环境搭建可以直接复用。

+

项目环境:

+
    +
  • go语言环境搭建
  • +
+

www.runoob.com/go/go-envir…

+
    +
  • 需要安装docker环境
  • +
+

www.runoob.com/docker/wind…

+
    +
  • 安装docker-compose工具
  • +
+

www.runoob.com/docker/dock…

+

项目clone到本地后,可以执行测试脚本来测试环境的可用性。如果有错误欢迎百度和Google解决

+
git clone https://github.com/qimengxingyuan/young_engine.git
+chmod a+x ./setup.sh
+./setup.sh
+

脚本执行成功,则环境可以支持项目的执行

+

项目说明:

+

本项目是一个简单的规则引擎的实现,详细目录可以参考README.md

+

项目实现时间有限,没有做比较完备的测试,如果在demo执行的过程中出现某些bug或者执行异常可以直接在github提交issue或者修复后提起PR

+

juejin.cn/post/711798…

+

三、课中知识点补充

+

3.1 什么是编译

+

编译的过程就是 把某种语言的源程序, 在不改变语义的条件下 ,转换成另一种语言程序(目标语言程序)

+

+
    +
  • 如果源代码编译后要在操作系统上运行,那目标代码就是汇编/机器代码。
  • +
  • 如果编译后是在虚拟机里执行,那目标代码就可以不是汇编代码,而是一种解释器可以理解的中间形式的代码即可。
  • +
+

解释型语言和编译型语言

+
    +
  • 有的语言提前把代码一次性转换完毕,这种就是编译型语言,用的转换工具就叫编译器,比如C、C++、Go。一次编译可重复执行 +
      +
    • 编译后产物不能跨平台,不同系统对可执行文件的要求不同。.exe
    • +
    • 特殊的,c、c++、汇编、源代码也不能跨平台
    • +
    +
  • +
  • 有的语言则可以一边执行一边转化,用到哪里了就转哪里,这种就是解释性语言,用的转化工具叫虚拟机或者解释器,比如java python、javascript
  • +
+

+

关于 Java Python .

+
    +
  • Java既有编译又有解释。但是编译并没有直接编译成机器码,而是编译成字节码,然后再放到虚拟机中执行。
  • +
  • Python执行过程也是经过两个阶段,先编译成字节码 .pyc 再放到虚拟机中去执行
  • +
+

JVM 和 Python解释器 | 为什么一个叫虚拟机一个叫解释器

+
    +
  1. “虚拟机”对二进制字节码进行解释,而“解释器”是对程序文本进行解释。
  2. +
  3. 从历史上看,Java 是为解释二进制字节码而设计和实现的,而 Python 最初是为解释程序文本而设计和实现的。因此,“Java 虚拟机”这个术语在 Java 社区中具有历史意义并且非常成熟,“Python 解释器”这个术语在 Python 社区中具有历史意义并且非常成熟。人们倾向于延长传统并使用很久以前使用的相同术语。
  4. +
  5. 对于 Java,二进制字节码解释是程序执行的主要方式,而 JIT 编译只是一种可选的和透明的优化。而对于 Python,目前,程序文本解释是 Python 程序执行的主要方式,而编译成 Python VM 字节码只是一种可选的透明优化。
  6. +
+

3.2 词法分析

+

把源代码字符串转换为词法单元(Token)的这个过程。

+

确定的有限自动机 DFA | Deterministic Finite Automaton

+

+

确定的有限自动机就是一个状态机,它的状态数量是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换。

+

3.3 语法分析

+

词法分析是识别一个个的单词,而语法分析就是在词法分析的基础上识别出程序的语法结构。这个结构是一个树状结构。这棵树叫做抽象语法树(Abstract Syntax Tree,AST)。树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫“语法”。每个节点还可以有下级节点。

+

Token -> AST

+

上下文无关语法 Context-Free Grammar

+

语言句子无需考虑上下文,就可以判断正确性

+
+
...
+a = 0;
+...
+这是一个赋值语句,无论此语句的前后是什么代码,此语句所代表的操作是确定的。即给变量a赋值等于0
+
+
+

编程语言为什么不用人类的语言(自然语言),而是用上下文无关的文法呢? 因为

+
    +
  1. 便于设计编译器。 客观上技术目前无法实现,如果使用了上下文相关文法,那就是真正实现了人工智能,NLP领域将会有重大突破。
  2. +
  3. 便于代码开发维护。 如果开发出来的代码像高考的语文阅读理解一样,每个人都有不同的理解,那么,到底哪个才是作者真正想要表达的?如果人类都确定不了含义,那计算机同样也确定不了,最终结果就是错误执行或无法执行。
  4. +
  5. 汇编语言/机器语言是上下文无关的。CPU执行指令时,读到哪条执行哪条。如果CPU需要考虑上下文,来决定一个语句到底要做什么,那么CPU执行一条语句会比现在慢千倍万倍。考虑上下文的事情,完全可以用户在编程的时候用算法实现。既然机器语言是上下文无关的,那高级语言也基本上是上下文无关的,可能有某些个别语法为了方便使用,设计成了上下文相关的,比如脚本语言的弱类型。在便于使用的同时,增加了解析器的复杂度。
  6. +
+

上下文无关语法G:终结符集合T + 非终结符集合N + 产生式集合P + 起始符号S

+

G由T、N、S和P组成,由语法G推导出来的所有句子的集合称为G语言!

+

终结符: 组成串的基本符号。可以理解为词法分析器产生的token集合。比如 + Id ( )

+

非终结符: 表示token的的集合的语法变量。比如 stmt varDecl 等等

+
start:blockStmts ;               //起始
+block : '{' blockStmts '}' ;      //语句块
+blockStmts : stmt* ;              //语句块中的语句
+stmt = varDecl | expStmt | returnStmt | block;   //语句
+varDecl : type Id varInitializer? ';' ;         //变量声明
+type : Int | Long ;                              //类型
+varInitializer : '=' exp ;                       //变量初始化
+expStmt : exp ';' ;                              //表达式语句
+returnStmt : Return exp ';' ;                    //return语句
+exp : add ;                                      //表达式   
+add : add '+' mul | mul;                         //加法表达式
+mul : mul '*' pri | pri;                         //乘法表达式
+pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式 
+

产生式:表示形式,S : AB ,就是说S的含义可以用语法AB进行表达

+
S : AB
+A : aA | ε
+B : b | bB
+

展开(expand):将P(A->u )应用到符号串vAw中,得到新串vu **w

+

折叠(reduce):将P(A->uu )应用到符号串vuu w中,得到新串vAw

+

推导(derivate):符号串u 应用一系列产生式,变成符号串v ,则u =>v:S => ab | b | bb

+

巴科斯范式

+

BNF是描述上下文无关理论的一种具体方法,通过BNF可以实现上下文无关文法的具体化、公式化、科学化,是实现代码解析的必要条件。

+
<expr> ::= <expr> + <term>
+         | <expr> - <term>
+         | <term>
+
+<term> ::= <term> * <factor>
+         | <term> / <factor>
+         | <factor>
+
+<factor> ::= ( <expr> )
+           | Num
+
+

BNF本质上就是树形分解,分解成一棵抽象语法树

+
+
    +
  • 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数。
  • +
  • 叶子节点叫做 终结符 ,非叶子节点叫做 非终结符
  • +
+

递归下降算法 Recursive Descent Parsing

+

基本思路就是按照语法规则去匹配 Token 串。比如说,变量声明语句的规则如下:

+
varDecl : types Id varInitializer? ';' ;        //变量声明
+varInitializer : '=' exp ;                       //变量初始化
+exp : add ;                                      //表达式   
+add : add '+' mul | mul;                         //加法表达式
+mul : mul '*' pri | pri;                         //乘法表达式
+pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式
+

如果写成产生式格式,是下面这样:

+
varDecl -> types Id varInitializer ';' 
+varInitializer -> '=' exp      
+varInitializer -> ε
+exp -> add
+add -> add + mul
+add -> mul
+mul -> mul * pri
+mul -> pri
+pri -> IntLiteral
+pri -> Id
+pri -> ( exp )
+

而基于这个规则做解析的算法如下:

+
匹配一个数据类型(types)
+匹配一个标识符(Id),作为变量名称
+匹配初始化部分(varInitializer),而这会导致下降一层,使用一个新的语法规则:
+   匹配一个等号
+   匹配一个表达式(在这个步骤会导致多层下降:exp->add->mul->pri->IntLiteral)
+   创建一个varInitializer对应的AST节点并返回
+如果没有成功地匹配初始化部分,则回溯,匹配ε,也就是没有初始化部分。
+匹配一个分号   
+创建一个varDecl对应的AST节点并返回
+

+
+

int a = 2

+
+
    +
  • 对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。
  • +
  • 在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。
  • +
  • 如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。这种回退过程,叫做回溯(Backtracking)。
  • +
+

四、课后作业

+

4.1 实现一个在线规则引擎

+

课上我们重点讲了规则引擎的设计和实现,结合前面课程的内容课后实现一个在线版本的规则引擎

+

4.1.1 项目要求

+

使用Hertz框架开发一个HTTP服务,服务使用mysql,支持表达式的增删查改和编译执行。

+

并实现以下接口

+
直接表达式执行
+

请求参数为待执行的表达式和表达式中参数的值,并输出编译结果

+

实时编译并执行结果,不需要写入DB中

+
    +
  • POST api/engine/run
  • +
  • Request
  • +
+
{
+    "exp": "uid == 12345 && did > 0",
+    "params": {
+        "uid": 123456,
+        "did": 0
+    }
+}
+
    +
  • Response
  • +
+
{
+    "code": 0,
+    "message": "success",
+    "data": {  // 执行结果
+        "result": true
+    }
+}
+
新增表达式
+

新增一条表达式到DB中,并返回表达式在DB中的ID

+

需要检测表达式 是否已经存在 ,如果已经存在,直接返回表达式的ID

+

需要检测表达式是否合法(编译是否通过) ,如果编译失败,返回错误码 20001和编译错误

+
    +
  • POST api/engine/exp/new
  • +
  • Request
  • +
+
{
+    "exp": "uid == 12345 && did > 0",
+}
+
    +
  • Response
  • +
+
{
+    "code": 0,
+    "message": "success",
+    "data": {  // 表达式ID
+        "id": 1
+    }
+}
+
+// 编译失败时
+{
+    "code": -1,
+    "message": "compile error: xxxxx", // 编译失败的信息
+    "data": {  // 表达式ID
+        "id": 0
+    }
+}
+
查询表达式
+

查询数据库中所有的表达式

+
    +
  • GET api/engine/exp/list
  • +
  • Response
  • +
+
{
+    "code": 0,
+    "message": "success",
+    "data": [  
+        {
+            "id": 1,
+            "exp": "uid > 0"
+        }
+    ]
+}
+
删除表达式
+

根据ID删除表达式,表达式不存在时返回错误码 20002 , 和错误信息

+

删除成功返回被删除的表达式信息

+
    +
  • DELETE api/engine/exp/:id
  • +
  • Response
  • +
+
// 删除成功时
+{
+    "code": 0,
+    "message": "success",
+    "data": {  // 表达式ID
+        "id": 1,
+        "exp": "uid > 0"
+    }
+}
+
+// 删除失败时
+{
+    "code": -1,
+    "message": "exp id 1 not exist", //查询失败的信息
+    "data": {}
+}
+
执行表达式
+

根据表达式的ID,查询出表达式内容,并编译执行。表达式不存在时返回错误码 20002 , 和错误信息

+
    +
  • POST api/engine/exp/run
  • +
  • Request
  • +
+
{
+    "exp_id": 1,
+    "parmas": {
+        "uid": 123456,
+        "did": 0
+    }
+}
+
    +
  • Response
  • +
+
{
+    "code": 0,
+    "message": "success",
+    "data": {  // 执行结果
+        "result": true
+    }
+}
+
+// 表达式不存在时
+{
+    "code": -1,
+    "message": "exp id 1 not exist", //查询失败的信息
+    "data": {}
+}
+ + +
+ +
+
+ + + + + + +
+
+
【实践课】规则引擎设计与实现
+
https://zhangzhao219.github.io/2023/01/29/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day06/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月29日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/30/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day07/index.html b/2023/01/30/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day07/index.html new file mode 100644 index 000000000..3b43379a8 --- /dev/null +++ b/2023/01/30/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day07/index.html @@ -0,0 +1,1141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 架构初探 - 谁动了我的蛋糕 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

架构初探 - 谁动了我的蛋糕

+ + +
+ +

架构初探 - 谁动了我的蛋糕

+ +

架构初探 - 谁动了我的蛋糕

+

使用指南

+

为了帮助同学们更好地理解本课程,我为大家准备了本学员手册。它包含以下几大模块内容:

+
    +
  • 课程目标,本课程主要框架的简单介绍,便于同学们抓住课程的框架结构,把握听课节奏;
  • +
  • 课前,本课程的重要前置知识点,便于同学们在听课过程中快速理解、跟紧思路;
  • +
  • 课中,本课程各章节涉及的关键概念和知识点,帮助同学们加深核心内容的理解和认识;
  • +
  • 课后,本课程的内容提炼,便于同学们总结课程要点,争取达到举一反三的效果。
  • +
+

课程目标

+

本课程的包含以下四个方面:

+
    +
  • 什么是架构 +
      +
    • 围绕架构的定义和演进两部分内容展开
    • +
    +
  • +
  • 企业级后端架构剖析 +
      +
    • 详细介绍企业级后端架构的形态
    • +
    +
  • +
  • 企业级后端架构的挑战 +
      +
    • 企业级架构都面临着哪些挑战,如何解决
    • +
    +
  • +
  • 后端架构实战 +
      +
    • 结合前三部分的知识点,以第三部分中的一个挑战为例,讲解如何做架构设计
    • +
    +
  • +
+

课前

+

什么是架构

+

常见软件架构:

+
    +
  • 单机
  • +
  • 单体
  • +
  • 垂直应用
  • +
  • SOA (Service Oriented Architecture)
  • +
  • 微服务 (Microservice)
  • +
+

一些小问题:

+
    +
  • 如何给架构下定义?
  • +
  • 架构的重要性?
  • +
  • 架构演进的初衷?
  • +
  • 架构演进的思路?
  • +
+

企业级后端架构剖析

+
    +
  • 云计算 +
      +
    • 基础 +
        +
      • 虚拟化
      • +
      • 编排
      • +
      +
    • +
    • 架构 +
        +
      • IaaS
      • +
      • SaaS
      • +
      • PaaS
      • +
      • FaaS
      • +
      +
    • +
    +
  • +
  • 云原生 +
      +
    • 弹性资源 +
        +
      • 计算资源
      • +
      • 存储资源
      • +
      +
    • +
    • 微服务架构 +
        +
      • 通信协议
      • +
      • 中间件
      • +
      +
    • +
    • DevOps +
        +
      • 软件生命周期
      • +
      +
    • +
    • 服务网格
    • +
    +
  • +
+

企业级后端架构的挑战

+
    +
  • 离线任务
  • +
  • 在线任务
  • +
  • IO 密集型
  • +
  • CPU 密集型
  • +
  • 服务治理
  • +
  • IPC (Inter-Process Communication)
  • +
  • RPC (Remote Procedure Call)
  • +
+

后端架构实战

+
    +
  • 负载均衡 Load Balancing
  • +
  • 服务发现 Service Discovery
  • +
  • 服务注册 Service Registry
  • +
  • 宿主机 Host
  • +
  • 容器 Container
  • +
  • 时序数据 Time Series
  • +
  • 一致性哈希 Consistent Hash
  • +
+

课前思考题

+
    +
  1. 软件架构演进至今都有哪些形态?它们分别解决了什么问题?仍然存在什么问题?
  2. +
  3. 云计算有哪些基础技术?云计算服务的形态又有哪些?
  4. +
  5. 云原生是什么?它跟云计算的关系是?
  6. +
  7. 云原生的代表技术有哪些?
  8. +
  9. 企业级后端架构面临的挑战有哪些?
  10. +
+

课中

+

什么是架构

+

架构定义

+

Q:如何给架构下定义?

+

A:架构,又称软件架构:

+
    +
  • 是有关软件整体结构与组件的抽象描述
  • +
  • 用于指导软件系统各个方面的设计
  • +
+

Q:架构的重要性?

+

A:那盖房子来做举例子。

+

我们都知道,地基对于一栋楼房的主要性,架构对于一个软件的重要性也是类似的:

+
    +
  • 架构没设计好,软件容易崩,用户体验上不去。最终要么重构,要么放弃
  • +
  • 架构设计好了,软件的稳定性上去了,用户体验高了,口碑一点点就打造出来了
  • +
  • 良好的架构基础,也为软件的未来发展提供了更多的可能。为用户赋能,实现自身价值
  • +
+

单机架构

+

All in one,所有的东西都在一个进程里,部署在一个机器上。

+

优点:

+
    +
  • 简单
  • +
+

缺点:

+
    +
  • 运维需要停服,用户体验较差
  • +
  • 承载能力有限。了解下 c10k 问题
  • +
+

单体架构

+

在单机架构的基础上,将进程部署到多个机器上。

+

优点:

+
    +
  • 具备水平扩容能力
  • +
  • 运维不需要停服
  • +
+

缺点:

+
    +
  • 后端进程职责太多,越来越臃肿
  • +
  • 爆炸半径较大,进程中一个很小的模块出现问题,都可能导致整个进程崩溃
  • +
+

垂直应用架构

+

在单机架构基础上,将进程按照某种依据切分开。比如,A 软件和 B 软件的后端原先采用单机架构部署,那就是一个进程部署在多个机器上;如果用垂直应用架构,可以将 A 和 B 的后端拆分为 A、B 两个进程,然后再按照单体模式的思路,部署在多个机器上。

+

优点:

+
    +
  • 一定程度上减少了后端进程职责
  • +
  • 一定程度上缩小爆炸半径
  • +
+

缺点:

+
    +
  • 没有根本解决单体架构的问题
  • +
+

SOA (面向服务架构)

+

SOA 架构中,服务为一等公民,将进程按照不同的功能单元进行抽象,拆分为『服务』。有了服务之后,SOA 还为服务之间的通信定义了标准,保证各个服务之间通讯体验的一致性。

+

优点:

+
    +
  • 各服务的职责更清晰
  • +
  • 运维粒度减小到服务,爆炸半径可控
  • +
+

缺点:

+
    +
  • ESB (企业服务总线) 往往需要一整套解决方案
  • +
+

微服务

+

在 SOA 架构中,ESB 起到了至关重要的作用。但从架构拓扑来看,它更像是一个集中式的模块。有一个 SOA 分布式演进的分支,最终的形态便是微服务。

+

优点:

+
    +
  • 兼具 SOA 解决的问题
  • +
  • 服务间的通信更敏捷、灵活
  • +
+

缺点:

+
    +
  • 运维成本
  • +
+

小结

+
    +
  • 架构演进的初衷:满足软件迭代诉求,提高迭代效率
  • +
  • 架构演进的思路:垂直切分——分布式,水平切分——分层/模块化
  • +
+

企业级后端架构剖析

+

云计算

+

云计算基础:

+
    +
  • 虚拟化技术 +
      +
    • 硬件层面(VM 虚拟机)- KVM/Xen/VMware
    • +
    • 操作系统层面(Container 容器)- LCX/Docker/Kata Container
    • +
    • 网络层面 - Linux Bridge/Open v Switch
    • +
    +
  • +
  • 编排方案 +
      +
    • VM - OpenStack/VMWare Workstation
    • +
    • Container - Kubernetes/Docker Swarm
    • +
    +
  • +
+

云计算架构:

+
    +
  • 云服务 +
      +
    • IaaS - 云基础设施,对底层硬件资源池的抽象
    • +
    • PaaS - 基于资源池抽象,对上层提供的弹性资源平台
    • +
    • SaaS - 基于弹性资源平台构建的云服务
    • +
    • FaaS - 更轻量级的函数服务。好比 LeetCode 等 OJ,刷题时只需要实现函数,不需要关注输入输出流
    • +
    +
  • +
  • 云部署模式(拓展) +
      +
    • 私有云 - 企业自用
    • +
    • 公有云 - AWS/Azure/Google Cloud/Huawei
    • +
    • 混合云
    • +
    +
  • +
+

云原生

+

云原生,实际是云原生(计算)的简称,它是云计算发展到现在的一种形态。

+

云原生技术为组织(公司)在公有云、自由云、混合云等新型的动态环境中,构建和运行可弹性拓展的应用提供了可能。 它的代表技术:

+
    +
  • 弹性资源
  • +
  • 微服务架构
  • +
  • DevOps
  • +
  • 服务网格
  • +
+
弹性资源
+

基于虚拟化技术,提供的可以快速扩缩容的能力。可以分为弹性计算资源和弹性存储资源两个方面。

+

弹性计算资源:

+
    +
  • 计算资源调度 +
      +
    • 在线计算 - 互联网后端服务
    • +
    • 离线计算 - 大数据分析。Map-Reduce/Spark/Flinnk
    • +
    +
  • +
  • 消息队列 +
      +
    • 在线队列 - 削峰、解耦
    • +
    • 离线队列 - 结合数据分析的一整套方案,如 ELK
    • +
    +
  • +
+

弹性存储资源:

+
    +
  • 经典存储 +
      +
    • 对象存储 - 视频、图片等。结合 CDN 等技术,可以为应用提供丰富的多媒体能力
    • +
    • 大数据存储 - 应用日志、用户数据等。结合数据挖掘、机器学习等技术,提高应用的体验
    • +
    +
  • +
  • 关系型数据库
  • +
  • 元数据 +
      +
    • 服务发现
    • +
    +
  • +
  • NoSQL +
      +
    • KV 存储 - Redis
    • +
    • 文档存储 - Mongo
    • +
    +
  • +
+

在云原生的大背景下,不论是计算资源还是存储资源,他们都像是服务一样供用户使用。

+
微服务架构
+

微服务架构下,服务之间的通讯标准是基于协议而不是 ESB 的。

+
    +
  • HTTP - H1/H2
  • +
  • RPC - Apache Thrift/gRPC
  • +
+

如何在 HTTP 和 RPC 之间选择?

+
    +
  • 性能 - RPC 协议往往具备较好的压缩率,性能较高。如 Thrift, Protocol Buffers
  • +
  • 服务治理 - RPC 中间件往往集成了丰富的服务治理能力。如 熔断、降级、超时等
  • +
  • 可解释性 - HTTP 通信的协议往往首选 JSON,可解释性、可调试性更好
  • +
+
服务网格
+

什么是服务网格?

+
    +
  • 微服务之间通讯的中间层
  • +
  • 一个高性能的 4 层网络代理
  • +
  • 将流量层面的逻辑与业务进程解耦
  • +
+

没有什么是加一层代理解决不了的问题,服务网格相比较于 RPC/HTTP 框架:

+
    +
  • 实现了异构系统治理体验的统一化
  • +
  • 服务网格的数据平面代理与业务进程采取进程间通信的模式,使得流量相关的逻辑(包含治理)与业务进程解耦,生命周期也更容易管理
  • +
+

企业级后端架构的挑战

+

挑战

+

基础设施层面:

+

Q:我们总说,云是弹性的,也就是说,在用户的角度,云提供的资源是无限的。然而,云背后的物理资源是有限的。在企业级后端架构里,云如何解决近乎无限的弹性资源和有限的物理资源之间的矛盾?

+

Q:闲事的资源就这么空着呢?如何提高资源利用率,提高物理资源的价值转换率?

+

用户层面:

+

Q:上了云原生微服务后,服务之间的通信开销较大,应该如何做成本优化?

+

Q:微服务看起来没有那么美好,抖动导致的运维成本较高,如何解决?

+

Q:异构的物理环境应该对用户是透明的,如何屏蔽这些细节?

+

离在线资源并池

+

考虑到在线业务的 潮汐性 ,物理资源的用量不是一成不变的。离在线资源并池,可以:

+
    +
  • 提高物理资源利用率
  • +
  • 提供更多的弹性资源
  • +
+

image.png

+

微服务亲合性部署

+

微服务之间的通信成本较高,是否可以:

+
    +
  • 形态上是微服务架构
  • +
  • 通信上是单体架构
  • +
+

亲合性部署,通过将微服务调用形态与资源调度系统结合,将一些调用关系紧密、通信量大的服务部署在同一个机器上,并且使用 IPC 代替 RPC 的方式,降低网络通信带来的开销

+

流量治理

+

Q:微服务之间的通信流量为什么需要治理?

+

Q:都有哪些常用的治理手段?

+

Q:微服务中心件和服务网格在其中扮演着怎样的角色?

+

屏蔽异构环境的算力差异

+

Q:基础设施层往往是个复杂的异构环境,比如,有些机器的 CPU 是英特尔的,而有些是 AMD 的。就算是同一个品牌,也可能是不同代际。如何将这些差异屏蔽掉,使用户尽可能不感知呢?

+

Q:什么情况下,我们觉得,服务需要扩容了?异构环境会对这个评判标准产生怎样的影响?

+

后端架构实战

+

问题

+

如何设计一个根据主机层面的资源信息,实时进行流量调度的系统,打平不同宿主机异构环境的算力差异。

+

关键点:

+
    +
  • 紧急回滚能力
  • +
  • 大规模
  • +
  • 极端场景
  • +
+

image.png

+

课后

+
课后作业-兰师傅蛋糕房要支持线上售卖了!请帮忙做整套系统的架构设计
+

设计需求:

+
    +
  1. 多端支持 +
      +
    1. 微信/支付宝小程序
    2. +
    3. App
    4. +
    5. 网页
    6. +
    +
  2. +
  3. 使用云原生基础设施
  4. +
  5. 用户画像很重要
  6. +
  7. 积极参加妇女节/光棍节等活动
  8. +
+

注意: 不需要考虑与做蛋糕相关服务的交互

+

尾声

+
    +
  1. 没有最好的架构,只有最合适的架构
  2. +
  3. 做架构设计 +
      +
    1. 先从需求出发。要满足什么样的需求?预期规模有多大?
    2. +
    3. 做足够的业界调研。业界对于类似的需求是怎么做的?有无成熟的方案可以借鉴?直接拿来用有什么问题?
    4. +
    5. 技术选型。涉及的技术组件是自研,还是使用开源的?
    6. +
    7. 异常情况。任何时候,都不能做『输入合法』的假设。容灾能力一定要有
    8. +
    +
  4. +
  5. 学好架构,是工程师成长的一个重要标志
  6. +
+

参考文献

+ + + +
+ +
+
+ + + + + + +
+
+
架构初探 - 谁动了我的蛋糕
+
https://zhangzhao219.github.io/2023/01/30/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day07/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月30日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/01/31/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day08/index.html b/2023/01/31/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day08/index.html new file mode 100644 index 000000000..6de05ce27 --- /dev/null +++ b/2023/01/31/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day08/index.html @@ -0,0 +1,1107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分布式理论 - 现代架构基石 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

分布式理论 - 现代架构基石

+ + +
+ +

分布式理论 - 现代架构基石

+ +

分布式理论 - 现代架构基石

+

概述

+

本节课程主要分为6个方面:

+
    +
  1. 概述
  2. +
  3. 系统模型
  4. +
  5. 理论基础
  6. +
  7. 分布式事务
  8. +
  9. 共识协议
  10. +
  11. 分布式实践
  12. +
+

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

+

课前 (必须)

+

概述

+
    +
  • 什么是分布式?
  • +
  • Why-How-What
  • +
  • 常见的分布式系统
  • +
+

系统模型

+
    +
  • 故障模型
  • +
  • 拜占庭将军问题
  • +
  • 共识和一致性
  • +
  • 时间和事件顺序
  • +
+

理论基础

+
    +
  • CAP理论
  • +
  • ACID理论
  • +
  • BASE理论
  • +
+

分布式事务

+
    +
  • 两阶段提交
  • +
  • 三阶段提交
  • +
  • MVCC
  • +
+

共识协议

+
    +
  • Quorum NWR模型
  • +
  • RAFT协议
  • +
  • Paxos协议
  • +
+

分布式实践

+
    +
  • MapReduce
  • +
  • 分布式KV
  • +
+

课中

+

概述

+
    +
  • 什么是分布式? +
      +
    • 分布式系统定义:跨多个节点的计算机程序的集合
    • +
    • 使用分布式系统的五大优势:去中心化、低成本、弹性、资源共享、可靠性高
    • +
    • 分布式系统的挑战:故障、网络、环境、安全
    • +
    +
  • +
  • Why-How-What +
      +
    • 使用者视角:大规模计算存储的述求
    • +
    • 学习者视角:后端开发必备技能
    • +
    +
  • +
  • 常见的分布式系统 +
      +
    • 分布式存储:GFS、Ceph、HDFS、Zookeeper
    • +
    • 分布式数据库:Spanner、TiDB、HBase、MangoDB
    • +
    • 分布式计算:Hadoop、YARN、Spark
    • +
    +
  • +
+

系统模型

+

故障模型

+
    +
  • 六种故障模型,从处理的难易程度分类 +
      +
    • Byzantine failure:节点可以任意篡改发送给其他节点的数据,是最难处理的故障
    • +
    • Authentication detectable byzantine failure (ADB):节点可以篡改数据,但不能伪造其他节点的数据
    • +
    • Performance failure:节点未在特定时间段内收到数据,即时间太早或太晚
    • +
    • Omission failure:节点收到数据的时间无限晚,即收不到数据
    • +
    • Crash failure:节点停止响应,持续性的故障
    • +
    • Fail-stop failure:错误可检测,是最容易处理的故障
    • +
    +
  • +
  • 故障模型举例,按照模型分类 +
      +
    • 磁盘、主板、交换机、网络分区、cpu、内存、线缆、电源等故障详细说明
    • +
    +
  • +
+

拜占庭将军问题

+
    +
  • 两将军问题 +
      +
    • 定义: +
        +
      • 两支军队的将军只能派信使穿越敌方领土互相通信,以此约定进攻时间。该问题希望求解如何在两名将军派出的任何信使都可能被俘虏的情况下,就进攻时间达成共识
      • +
      +
    • +
    • 结论: +
        +
      • 两将军问题是被证实无解的电脑通信问题,两支军队理论上永远无法达成共识
      • +
      +
    • +
    • TCP是两将军问题的一个工程解
    • +
    +
  • +
  • 三将军问题: +
      +
    • 两个“忠将”A和B,一个“叛徒”C,互相传递消息,消息可能丢失,也可能被篡改,当有一个将军是“叛徒”(即出现拜占庭故障)时,整个系统无法达成一致。
    • +
    • 由于“叛徒”C的存在,将军A和将军B获得不同的信息。这样将军A获得2票进攻1票撤退的信息,将军B获得1票进攻2票撤退的信息,产生了不一致
    • +
    +
  • +
  • 四将军问题: +
      +
    • 将军D作为消息分发中枢,约定如果没收到消息则执行撤退
    • +
    • 步骤: +
        +
      • 如果D为“叛徒”,ABC无论收到任何消息,总能达成一致
      • +
      • D为“忠将”,ABC有2人将D的消息进行正确的传递,同样能保证最终决策符合大多数。
      • +
      +
    • +
    • 进而能够证明,当有3m+1个将军,m个“叛徒”时,可以进行m轮协商,最终达成一致
    • +
    +
  • +
+

共识和一致性

+
    +
  • 不同客户端A和B看到客户端C写入,因为时机的不同,产生数据读取的偏差。引导出最终一致性的详细说明
  • +
  • 要保证所有客户端看到相同的值,需要多节点进行“协商”,达成共识,来保证线性一致性
  • +
  • 一致性和可用性是对矛盾
  • +
+

时间和事件顺序

+
    +
  • 1978年Leslie Lamport发表《Time, Clocks, and the Ordering of Events in a Distributed System》 +
      +
    • 定义了计算机系统中的时间和事件顺序,引入happened before和并发的定义,可以以此对分布式系统中的事件进行推导
    • +
    • 根据上述推导,创造了Lamport逻辑时钟的概念,这个概念在分布式理论中具有革命性的意义,帮助我们在一系列分布式事件当中梳理出逻辑的先后关系。利用逻辑时钟,我们可以对整个系统中的事件进行全序排序
    • +
    +
  • +
+

理论基础

+

CAP理论

+
    +
  • CAP的定义,分别代表一致性、可用性、分区容错性。三者无法同时达到
  • +
  • CAP诞生了三类系统: +
      +
    • CA系统:传统数据库的代表
    • +
    • AP系统:放弃强一致性,保证高可用,不少nosql存储系统采用
    • +
    • CP系统:放弃可用性,保证数据一致性
    • +
    +
  • +
  • 举例说明两个分布式进程之间同步数据,当出现故障的时候,如何选择不同的CAP系统,以及带来的影响 +
      +
    • CP系统:故障发生时,为了避免读到不一致的数据,可能拒绝访问
    • +
    • AP系统:故障发生时,为了保证可用性,允许不同进程读到不同的数据
    • +
    +
  • +
  • 针对故障场景,可以通过故障转移的方式,做一个相对较优的解决方式: +
      +
    • 允许一个进程作为Master,其他进程作为Backup,当故障时将请求转移给Backup进行处理
    • +
    +
  • +
+

ACID理论

+
    +
  • ACID理论是针对CA系统而言的,通常在数据库中具有广泛意义
  • +
  • 事务是数据库系统中非常重要的概念,它是数据库管理系统执行过程中的一个逻辑单元,它能够保证一个事务中的所有操作要么全部执行,要么全都不执行
  • +
  • 数据库事务拥有四个特性ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)
  • +
+

+

BASE理论

+
    +
  • BASE理论是针对AP系统而言的,其来源于对大型互联网分布式实践的总结 +
      +
    • Basically Available(基本可用):假设系统,出现了不可预知的故障,但还是能用
    • +
    • Soft state(软状态):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性
    • +
    • Eventually consistent(最终一致性):数据最终一定能够达到一致的状态
    • +
    +
  • +
+

分布式事务

+

二阶段提交

+
    +
  • 定义: +
      +
    • 二阶段提交(Two-phase Commit):为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种演算法。
    • +
    +
  • +
  • 三个假设: +
      +
    • 协调者和参与者进行通信
    • +
    • 预写式日志被保持在可靠的存储设备上
    • +
    • 所有节点不会永久性损坏,即使损坏后仍然可以恢复
    • +
    +
  • +
  • 正常流程:Prepare阶段和Commit阶段
  • +
  • 异常流程:Prepare阶段失败 -> 回滚;协调者宕机 -> 重新启用新的协调者;双故障重启 -> 数据库管理员介入
  • +
  • 两阶段提交需解决的问题: +
      +
    • 性能问题:需要多次网络通信,资源需要等待并锁定
    • +
    • 新协调者:如何确定状态选出新协调者
    • +
    • Commit阶段网络分区带来的数据不一致:非所有节点都收到Commit请求
    • +
    +
  • +
  • 两个思考: +
      +
    • 日志被保存在「可靠」的存储设备上。如何保证这一点?
    • +
    • 参与者Commit了,但Ack信息协调者没收到。怎么办?
    • +
    +
  • +
+

三阶段提交

+
    +
  • 针对两阶段提交的补充,将两阶段提交中的Prepare阶段,拆成两部分:CanCommit和PreCommit机制
  • +
  • CanCommit阶段:询问是否可以执行;PreCommit阶段:重新确认是否可以执行
  • +
  • DoCommit阶段:向所有人提交事务
  • +
+

MVCC

+
    +
  • MVCC:多版本并发控制的方法。维持一个数据的多个版本使读写操作没有冲突。所以既不会阻塞写,也不阻塞读。提高并发性能的同时也解决了脏读的问题。
  • +
  • 悲观锁和乐观锁 +
      +
    • 悲观锁:操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据
    • +
    • 乐观锁:不会上锁,只是在执行更新时判断别人是否修改数据,只有冲突时才放弃操作
    • +
    +
  • +
  • 版本的选取:使用物理时钟或逻辑时钟 +
      +
    • 物理时钟:提供TrueTime API,有Master节点维持一个绝对时间,保证各个服务器之间时钟误差控制在ϵ内,通常ϵ<7ms。
    • +
    • 逻辑时钟:中心化授时的方式–时间戳预言机(TSO),好处是无需硬件的支持
    • +
    +
  • +
+

共识协议

+

Quorum NWR模型

+
    +
  • 三要素: +
      +
    • N:在分布式存储系统中,有多少份备份数据
    • +
    • W:代表一次成功的更新操作要求至少有w份数据写入成功
    • +
    • R: 代表一次成功的读数据操作要求至少有R份数据成功读取
    • +
    • 为了保证强一致性,需要保证 W+R>N
    • +
    +
  • +
  • Quorum NWR模型将CAP的选择交给用户,是一种简化版的一致性模型
  • +
  • 引起的并发更新问题 +
      +
    • 如果允许数据被覆盖,则并发更新容易引起一致性问题
    • +
    +
  • +
+

RAFT协议

+
    +
  • 概述 +
      +
    • Raft协议是一种分布式一致性算法(共识算法),即使出现部分节点故障,网络延时等情况,也不影响各节点,进而提高系统的整体可用性。Raft是使用较为广泛的分布式协议。
    • +
    +
  • +
  • 三种角色 +
      +
    • Leader - 领导者:Leader 负责处理所有的客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后,通知Follower提交日志
    • +
    • Follower - 跟随者:接受并持久化Leader同步的日志,在Leader告知日志可以提交后,提交日志
    • +
    • Candidate - 备选者:Leader选举过程中的临时角色。向其他节点发送请求投票信息
    • +
    +
  • +
  • 四种定义: +
      +
    • Log(日志):节点之间同步的信息,以只追加写的方式进行同步,解决了数据被覆盖的问题
    • +
    • Term(任期号):单调递增,每个Term内最多只有一个Leader
    • +
    • Committed:日志被复制到多数派节点,即可认为已经被提交
    • +
    • Applied:日志被应用到本地状态机:执行了log中命令,修改了内存状态
    • +
    +
  • +
  • 状态转移:
  • +
  • Leader选举过程: +
      +
    • 初始全部为Follower
    • +
    • Current Term + 1
    • +
    • 选举自己
    • +
    • 向其它参与者发起RequestVote请求,retry直到 +
        +
      • 收到多数派请求,成为Leader,并发送心跳
      • +
      • 收到其它Leader的请求,转为Follower,更新自己的Term
      • +
      • 收到部分,但未达到多数派,选举超时,随机timeout开始下一轮
      • +
      +
    • +
    +
  • +
  • Log Replication过程: +
      +
    • 新Leader产生,Leader和Follower不同步,Leader强制覆盖Followers的不同步的日志
    • +
    +
  • +
  • 切主:当Leader出现问题时,就需要进行重新选举 +
      +
    • Leader发现失去Follower的响应,失去Leader身份
    • +
    • 两个Follower之间一段时间未收到心跳,重新进行选举,选出新的Leader,此时发生了切主
    • +
    • Leader自杀重启,以Follower的身份加入进来
    • +
    +
  • +
  • Stale读: +
      +
    • 发生Leader切换,old leader收到了读请求。如果直接响应,可能会有Stale Read
    • +
    +
  • +
+

Paxos协议

+
    +
  • Paxos算法与RAFT算法区别: +
      +
    • Multi-Paxos 可以并发修改日志,而Raft写日志操作必须是连续的
    • +
    • Multi-Paxos 可以随机选主,不必最新最全的节点当选Leader
    • +
    +
  • +
  • 优劣势 +
      +
    • 优势:写入并发性能高,所有节点都能写
    • +
    • 劣势:没有一个节点有完整的最新的数据,恢复流程复杂,需要同步历史记录
    • +
    +
  • +
+

分布式实践

+

MapReduce

+
    +
  • 设计一个简易的MapReduce系统,思考如何应对故障?
  • +
+

分布式KV

+
    +
  • 设计一个简易的分布式键值系统,要求具备弹性的能力和达成线性一致
  • +
+

课后

+
    +
  1. 分布式系统有哪些优势和挑战?
  2. +
  3. 两将军问题为什么理论上永远达不成共识?
  4. +
  5. 为什么TCP采用三次握手?而不是两次和四次?
  6. +
  7. 为什么在4将军问题中,增加1轮协商就可以对抗拜占庭故障?
  8. +
  9. 什么是最终一致性?什么是线性一致性?
  10. +
  11. CAP理论中,请举例说明可用性和一致性的矛盾?
  12. +
  13. 数据库里的一致性和分布式系统中的一致性有什么区别?
  14. +
  15. 两阶段提交中,什么场景需要数据库管理员介入?
  16. +
  17. 三阶段提交缓和两阶段提交的哪两个问题?
  18. +
  19. 什么场景适合乐观锁?什么场景适合悲观锁?
  20. +
  21. 在共识协议中,为什么说允许数据被覆盖会带来数据一致性问题?
  22. +
  23. RAFT协议中,Leader写成功日志Log20但未同步给Followers后宕机,Follower重新选举后产生一条新日志Log20,这时Leader重启,整个系统发现两种不一样的Log20的记录,请问如何区分并拒掉前面的Log20?
  24. +
  25. RAFT协议中,Stale读是如何产生的?该如何解决Stale读的问题?
  26. +
+ + +
+ +
+
+ + + + + + +
+
+
分布式理论 - 现代架构基石
+
https://zhangzhao219.github.io/2023/01/31/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day08/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年1月31日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/02/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day09/index.html b/2023/02/02/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day09/index.html new file mode 100644 index 000000000..b610f859c --- /dev/null +++ b/2023/02/02/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day09/index.html @@ -0,0 +1,1186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 微服务框架 - 不变的基建 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

微服务框架 - 不变的基建

+ + +
+ +

微服务框架 - 不变的基建

+ +

微服务框架 - 不变的基建

+

概述

+

本课程内容主要分为以下4个方面:

+
    +
  • +

    微服务架构介绍

    +
      +
    • 微服务架构的背景由来、架构概览、基本要素
    • +
    +
  • +
  • +

    微服务架构原理及特征

    +
      +
    • 微服务架构的基本组件、工作原理、流量特征
    • +
    +
  • +
  • +

    核心服务治理功能

    +
      +
    • 核心的服务治理功能,包括流量治理、服务均衡、稳定性治理
    • +
    +
  • +
  • +

    字节跳动服务治理实践

    +
      +
    • 字节跳动在微服务架构稳定性治理中,对请求重试策略的探索及实践
    • +
    +
  • +
+

为了帮助大家更好地预习及理解本节课程,该学员手册列出了课前、课中、及课后这三个阶段所涉及到的专业内容大纲,其中课前部分供同学们提前预习参考,课中部分给出了课程大纲,帮助同学们整理思路,课后部分列出一些扩展性的问题让同学们进一步延伸思考。

+

课前

+

微服务架构介绍

+
    +
  • +

    系统架构的演进历史

    +
      +
    • 单体架构
    • +
    • 垂直应用架构
    • +
    • 分布式架构
    • +
    • SOA架构
    • +
    • 微服务架构
    • +
    +
  • +
  • +

    微服务架构的三大要素

    +
      +
    • 服务治理
    • +
    • 可观测性
    • +
    • 安全
    • +
    +
  • +
+

微服务架构原理及特征

+
    +
  • +

    微服务架构中的基本概念及组件

    +
      +
    • 服务、实例…
    • +
    +
  • +
  • +

    服务间通信

    +
      +
    • RPC、HTTP
    • +
    +
  • +
  • +

    服务注册及服务发现

    +
  • +
+

核心服务治理功能

+
    +
  • +

    服务发布

    +
      +
    • 蓝绿部署
    • +
    • 灰度发布(金丝雀发布)
    • +
    +
  • +
  • +

    流量治理

    +
  • +
  • +

    负载均衡

    +
      +
    • Round Robin
    • +
    • Ring Hash
    • +
    • Random
    • +
    +
  • +
  • +

    稳定性治理

    +
      +
    • 限流
    • +
    • 熔断
    • +
    • 过载保护
    • +
    • 降级
    • +
    +
  • +
+

字节跳动服务治理实践

+
    +
  • 请求重试的意义
  • +
  • 请求重试的难点
  • +
+

课中

+

微服务架构介绍

+

系统架构的演进历史

+
    +
  • +

    单体架构

    +
      +
    • All in one process
    • +
    +
  • +
  • +

    垂直应用架构

    +
      +
    • 按照业务线垂直划分
    • +
    +
  • +
  • +

    分布式架构

    +
      +
    • 抽出与业务无关的公共模块
    • +
    +
  • +
  • +

    SOA架构

    +
      +
    • 面向服务
    • +
    +
  • +
  • +

    微服务架构

    +
      +
    • 彻底的服务化
    • +
    +
  • +
+

微服务架构概览

+
    +
  • 网关
  • +
  • 服务配置和治理
  • +
  • 链路追踪和监控
  • +
+

微服务架构的三大要素

+
    +
  • +

    服务治理(本课程内容)

    +
      +
    • 服务注册
    • +
    • 服务发现
    • +
    • 负载均衡
    • +
    • 扩缩容
    • +
    • 流量治理
    • +
    • 稳定性治理
    • +
    +
  • +
  • +

    可观测性

    +
      +
    • 日志采集
    • +
    • 日志分析
    • +
    • 监控打点
    • +
    • 监控大盘
    • +
    • 异常报警
    • +
    • 链路追踪
    • +
    +
  • +
  • +

    安全

    +
      +
    • 身份验证
    • +
    • 认证授权
    • +
    • 访问令牌
    • +
    • 审计
    • +
    • 传输加密
    • +
    • 黑产攻击
    • +
    +
  • +
+

微服务架构原理及特征

+

微服务架构中的基本概念及组件

+
    +
  • +

    服务

    +
      +
    • 一组具有相同逻辑的运行实体
    • +
    +
  • +
  • +

    实例

    +
      +
    • 一个服务中的每个运行实体
    • +
    +
  • +
  • +

    实例与进程的关系

    +
      +
    • 没有必然对应关系,一般一对一或者一对多
    • +
    +
  • +
  • +

    常见的实例承载形式

    +
      +
    • 进程、VM、k8s pod…
    • +
    +
  • +
+

服务间通信

+
    +
  • 微服务之间通过网络进行通信
  • +
  • 常见的通信协议包括 HTTP、RPC
  • +
+

服务注册及服务发现

+
    +
  • +

    基本问题

    +
      +
    • 服务间调用中,如何指定下游服务实例的地址?
    • +
    +
  • +
  • +

    简单方案

    +
      +
    • 直接指定 ip:port? +
        +
      • 没有任何动态能力
      • +
      • 有多个实例下游实例怎么办?
      • +
      +
    • +
    • 使用 DNS? +
        +
      • 本地 DNS 存在缓存,导致延迟
      • +
      • DNS 没有负载均衡
      • +
      • 不支持服务探活检查
      • +
      • DNS 不能指定端口
      • +
      +
    • +
    +
  • +
  • +

    服务注册发现

    +
      +
    • 新增一个统一的服务注册中心,用于存储服务名到服务实例之间的映射关系
    • +
    • 旧服务实例下线前,从服务注册中心删除该实例,下线流量
    • +
    • 新服务实例上线后,在服务注册中心注册该实例,上线流量
    • +
    +
  • +
  • +

    微服务流量特征

    +
      +
    • 统一网关入口
    • +
    • 外网通信多数采用 HTTP,内网通信多数采用 RPC(Thrift, gRPC)
    • +
    +
  • +
+

核心服务治理功能

+

服务发布

+
    +
  • +

    何为服务发布

    +
      +
    • 让一个服务升级运行新的代码的过程
    • +
    +
  • +
  • +

    服务发布难点

    +
      +
    • 服务不可用
    • +
    • 服务抖动
    • +
    • 服务回滚
    • +
    +
  • +
  • +

    蓝绿部署

    +
      +
    • 将服务分成两个部分,分别先后发布
    • +
    • 简单、稳定
    • +
    • 但需要两倍资源
    • +
    +
  • +
  • +

    灰度发布(金丝雀发布)

    +
      +
    • 先发布少部分实例,接着逐步增加发布比例
    • +
    • 不需要增加资源
    • +
    • 回滚难度大,基础设施要求高
    • +
    +
  • +
+

流量治理

+
    +
  • +

    流量控制

    +
      +
    • 在微服务架构中,可以从各个维度对端到端的流量在链路上进行精确控制
    • +
    +
  • +
  • +

    控制维度

    +
      +
    • 地区维度
    • +
    • 集群维度
    • +
    • 实例维度
    • +
    • 请求维度
    • +
    +
  • +
+

负载均衡

+
    +
  • Round Robin
  • +
  • Random
  • +
  • Ring Hash
  • +
  • Least Request
  • +
+

稳定性治理

+
    +
  • +

    限流

    +
      +
    • 限制服务处理的最大 QPS,拒绝过多请求
    • +
    +
  • +
  • +

    熔断

    +
      +
    • 中断请求路径,增加冷却时间从而让故障实例尝试恢复
    • +
    +
  • +
  • +

    过载保护

    +
      +
    • 在负载高的实例中,主动拒绝一部分请求,防止实例被打挂
    • +
    +
  • +
  • +

    降级

    +
      +
    • 服务处理能力不足时,拒绝低级别的请求,只响应线上高优请求
    • +
    +
  • +
+

字节跳动服务治理实践

+
    +
  • +

    请求重试的意义

    +
      +
    • 本地函数调用 +
        +
      • 通常没有重试意义
      • +
      +
    • +
    • 远程函数调用 +
        +
      • 网络抖动、下游负载高、下游机器宕机…
      • +
      • 重试是有意义的,可以避免偶发性的错误,提高 SLA
      • +
      +
    • +
    • 重试的意义 +
        +
      • 降低错误率
      • +
      • 降低长尾延时
      • +
      • 容忍暂时性错误
      • +
      • 避开下游故障实例
      • +
      +
    • +
    +
  • +
  • +

    请求重试的难点

    +
      +
    • 幂等性 +
        +
      • POST 请求可以重试吗?
      • +
      +
    • +
    • 重试风暴 +
        +
      • 随着调用链路的增加,重试次数呈指数级上升
      • +
      +
    • +
    • 超时设置 +
        +
      • 假设调用时间一共1s,经过多少时间开始重试?
      • +
      +
    • +
    +
  • +
  • +

    重试策略

    +
      +
    • 限制重试比例 +
        +
      • 设定一个重试比例阈值(例如 1%),重试次数占所有请求比例不超过该阈值
      • +
      +
    • +
    • 防止链路重试 +
        +
      • 返回特殊的 status code,表示“请求失败,但别重试”
      • +
      +
    • +
    • Hedged Requests +
        +
      • 对于可能超时(或延时高)的请求,重新向另一个下游实例发送一个相同的请求,并等待先到达的响应
      • +
      +
    • +
    +
  • +
  • +

    重试效果验证

    +
      +
    • 字节跳动重试组件能够极大限制重试发生的链路放大效应
    • +
    +
  • +
+

课后

+
    +
  1. 结合 CAP 等原理,思考微服务架构有哪些缺陷?
  2. +
  3. 微服务是否拆分得越“微”越好?为什么?
  4. +
  5. Service Mesh 这一架构是为了解决微服务架构的什么问题?
  6. +
  7. 有没有可能有这样一种架构,从开发上线运维体验上是微服务,但实际运行又类似单体服务?
  8. +
+

参考文献

+
    +
  1. A Design Analysis of Cloud-based Microservices Architecture at Netflix
  2. +
  3. 字节跳动微服务架构体系演进
  4. +
  5. 微服务架构的一知半解
  6. +
+ + +
+ +
+
+ + + + + + +
+
+
微服务框架 - 不变的基建
+
https://zhangzhao219.github.io/2023/02/02/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day09/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/03/Backend/Consul_kong/index.html b/2023/02/03/Backend/Consul_kong/index.html new file mode 100644 index 000000000..3e0c56716 --- /dev/null +++ b/2023/02/03/Backend/Consul_kong/index.html @@ -0,0 +1,838 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Consul与Kong联合配置理解 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Consul与Kong联合配置理解

+ + +
+ +

Consul与Kong联合配置理解

+ +

Consul与Kong联合配置理解

+

Consul介绍(实习-百度-Go后端开发-2023.02.09)

+

Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案,Consul的方案更“一站式”,内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如ZooKeeper等)。使用起来也较 为简单。Consul使用Go语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与Docker等轻量级容器可无缝配合 。

+

consul主要由server和client两种组件组成。

+

server负责核心数据的存储和处理请求,server可以部署多个实例(通常推荐3-5个),server只有一个leader实例,就是主节点,主节点是通过选举产生的,主节点负责数据的写入处理,同时将数据同步至其他server节点

+

client负责跟server通信,处理转发服务注册、服务发现请求到server节点,client还负责服务的健康检查,client节点可以部署多个实例,甚至每个微服务节点都部署一个client实例。

+

以开发模式启动consul,同时具备server和client的功能,不需要单独部署server和client

+

consul健康检查机制制运行在consul client中,会定期的根据服务健康检查配置,去检测服务是否正常,如果服务异常,就将服务的实例标记为不用, 如果恢复了,就标记为可用。

+
    +
  • 基于http请求:定时以GET请求方式,请求指定url,http请求返回状态码200表示正常,其他状态代表异常。
  • +
  • 基于tcp请求:基于tcp请求方式,就是定时向指定的地址,建立tcp链接,连接成功就代表服务正常,否则就代表异常。
  • +
  • 基于grpc请求:如果微服务是基于grpc协议,可以使用grpc协议监测服务是否正常。
  • +
  • 基于命令:consul支持定期执行一个命令或脚本,来检测服务是否正常,consul通过监测命令退出状态判断服务是否正常,命令退出状态0代表正常,其他代表异常。
  • +
  • 基于TTL(服务主动向consul报告自己的健康状况):一个健康的APP可以周期性的将状态put到HTTP端
  • +
+

Kong介绍

+

Kong是一款基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,由Mashape公司开源的API Gateway项目。Kong是基于NGINX和Apache Cassandra或PostgreSQL构建的,能提供易于使用的RESTful API来操作和配置API管理系统,所以它可以水平扩展多个Kong服务器,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。

+

Konga是可以管理Kong的图形界面,带来的一个最大的便利就是可以很好地通过UI观察到现在kong的所有的配置,并且可以对于管理kong节点情况进行查看、监控和预警。

+

传统架构

+

微服务架构是由多个服务端和多个api端组成,客户端发起请求,需要单独的api进行接收和路由转发,然后通过与不同的服务端建立连接从而获得服务。

+

这个过程中需要在程序中记忆大量的端口,且一旦有节点失效,整个服务都将不可用。

+

Consul+Kong架构

+

pSsIlXF.png

+

Kong

+
    +
  • 将不同api的ip和端口配置到Kong中(如果与Consul结合,直接配置consul_api服务名称.service.consul即可)
  • +
  • 在Kong中设置路由匹配规则
  • +
  • 客户端的请求首先发送给Kong,由Kong进行路由规则的匹配,随后转发到不同的api上
  • +
  • 客户端在请求的时候的ip地址和端口号使用任意一台api的ip地址和端口号即可,所有日志都会发送到该台服务器上,实际请求的日志会转发到其他的api上
  • +
+

Consul

+
    +
  • 一个服务下面可以启动多个实例,收到请求会平均发送给每一个实例
  • +
  • 服务发现:请求服务时只需得知服务名称、consul的ip与端口号即可,无需知道服务具体细节
  • +
  • 健康检查:服务注册后consul每间隔一段时间发送响应给服务的实例,确认在线情况
  • +
  • 服务注册:服务向consul报告自己的ip和端口号
  • +
+

Consul代码示例

+

api端

+

服务注册:

+
registerClient := consul.NewRegistryClient(global.GlobalConfig.Consul.Address, global.GlobalConfig.Consul.Port)
+err = registerClient.Register(global.GlobalConfig.MainServer.Address, global.GlobalConfig.MainServer.Port, "video-api", []string{"api", "video"})
+

健康检查(注意是HTTP类型的):

+
check := &api.AgentServiceCheck{
+	HTTP:                           "http://" + address + ":" + port + "/health",
+	Timeout:                        "5s",
+	Interval:                       "5s",
+	DeregisterCriticalServiceAfter: "10s",
+}
+

连接服务端:

+
conn, err = grpc.Dial(
+	"consul://"+global.GlobalConfig.Consul.Address+":"+global.GlobalConfig.Consul.Port+"/"+name+"?wait=14s",
+	grpc.WithTransportCredentials(
+		insecure.NewCredentials(),
+	),
+	grpc.WithDefaultCallOptions(
+		grpc.MaxCallRecvMsgSize(1024*1024*size),
+		grpc.MaxCallSendMsgSize(1024*1024*size),
+	),
+	grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
+)
+

服务端

+

注意监听的时候要监听内网地址

+
lis, err := net.Listen("tcp", global.GlobalConfig.Address.In+":"+port)
+

服务注册:

+
register_client := consul.NewRegistryClient(global.GlobalConfig.Consul.Address, global.GlobalConfig.Consul.Port)
+register_client.Register(global.GlobalConfig.Address.Out, port, name, []string{"srv", "video"})
+

健康检查(注意是GRPC类型的):

+
grpc_health_v1.RegisterHealthServer(s, health.NewServer())
+
check := &api.AgentServiceCheck{
+	GRPC:                           address + ":" + port,
+	Timeout:                        "5s",
+	Interval:                       "5s",
+	DeregisterCriticalServiceAfter: "10s",
+}
+

连接api端:

+
s := grpc.NewServer()
+ + +
+ +
+
+ + + + + + +
+
+
Consul与Kong联合配置理解
+
https://zhangzhao219.github.io/2023/02/03/Backend/Consul_kong/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day10/index.html b/2023/02/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day10/index.html new file mode 100644 index 000000000..0d96025c1 --- /dev/null +++ b/2023/02/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day10/index.html @@ -0,0 +1,894 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 【实践课】手把手教你做系统设计 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

【实践课】手把手教你做系统设计

+ + +
+ +

【实践课】手把手教你做系统设计

+ +

【实践课】手把手教你做系统设计

+

手把手教你做系统设计之秒杀系统

+

概述

+

本节课程主要分为四个方面:

+
    +
  1. 系统设计方法论
  2. +
  3. 电商秒杀业务介绍
  4. +
  5. 课程实践
  6. +
  7. 课程总结
  8. +
+

课前部分主要罗列课程中涉及到的中间件和相关背景知识。对于使用到的中间件,同学们需要体验了解概念,安装并正确使用。课中部分会详细讲解系统设计的方法论和秒杀系统实践,帮助同学们入门系统设计。课后部分会做一些总结,梳理核心思想和重点。

+

课前 (必须)

+

了解基本的电商概念和流程

+
    +
  • 电商平台业务
  • +
  • 秒杀业务特点
  • +
+

MySQL

+
    +
  • 安装MySQL,推荐使用MySQL8及以上版本
  • +
  • 熟悉ddl,dml等基础语法
  • +
  • 了解sql优化
  • +
+

Redis

+
    +
  • 安装Redis,推荐最新版本
  • +
  • 了解Redis的基本数据类型和使用场景
  • +
  • 熟悉常用命令
  • +
  • 了解Lua脚本的使用
  • +
  • 了解Redis分布式锁
  • +
+

RocketMQ

+
    +
  • 安装RocketMQ,推荐最新版本
  • +
  • 了解RocketMQ的基础概念和架构
  • +
  • 了解MQ的使用场景
  • +
  • 了解生产者如何保证消息的可靠性发送
  • +
  • 了解消费者如何保证幂等
  • +
  • 了解消费者pull和push模式的区别
  • +
+

OpenResty

+
    +
  • 安装OpenResty,推荐最新版本
  • +
  • 了解Nginx的基础概念和使用
  • +
  • 了解Lua脚本的语法
  • +
+

Linux

+
    +
  • 熟悉常用命令
  • +
  • 熟悉进程和线程
  • +
  • 了解Linux调优
  • +
+

Java

+
    +
  • 按照JDK,推荐JDK11
  • +
  • 熟悉Java基础语法和lambda表达式
  • +
  • 熟悉idea的使用
  • +
  • 了解并发编程
  • +
  • 了解springboot框架的使用
  • +
  • 了解maven的使用
  • +
+

Jmeter

+
    +
  • 安装Jmeter
  • +
  • 了解使用Jmeter压测
  • +
+

课中

+

引言

+
    +
  • 为什么要做系统设计 +
      +
    • 个人?
    • +
    • 工作?
    • +
    +
  • +
  • 系统设计的概念是什么
  • +
  • 如何做系统设计 +
      +
    • 4S分析法
    • +
    +
  • +
  • 如何分析系统瓶颈和优化 +
      +
    • 火焰图分析
    • +
    • 链路分析
    • +
    • 全链路压测
    • +
    +
  • +
  • 如何验证系统的可用性和稳定性 +
      +
    • 链路梳理
    • +
    • 可观测性
    • +
    • 全链路测试
    • +
    • 稳定性控制
    • +
    • 容灾演练
    • +
    +
  • +
+

电商和秒杀

+

基本概念

+
    +
  • Spu
  • +
  • Sku
  • +
  • 秒杀业务的特点
  • +
+

秒杀的挑战

+
    +
  • 资源有限性
  • +
  • 反欺诈
  • +
  • 高性能
  • +
  • 防止超卖
  • +
  • 流量管控
  • +
  • 扩展性
  • +
  • 鲁棒性
  • +
+

设计秒杀系统

+

4S分析

+
    +
  • 场景
  • +
  • 存储
  • +
  • 功能
  • +
  • 扩展
  • +
+

系统架构图

+

+

实践

+

秒杀流程

+

+

总结

+

高性能系统的通用设计思想

+

课后

+
    +
  • 秒杀课程的总结
  • +
  • 秒杀系统的扩展
  • +
+ + +
+ +
+
+ + + + + + +
+
+
【实践课】手把手教你做系统设计
+
https://zhangzhao219.github.io/2023/02/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day10/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/04/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day11/index.html b/2023/02/04/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day11/index.html new file mode 100644 index 000000000..439e62146 --- /dev/null +++ b/2023/02/04/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day11/index.html @@ -0,0 +1,784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 黑灰产监控与防御 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

黑灰产监控与防御

+ + +
+ +

黑灰产监控与防御

+ +

黑灰产监控与防御

+

1、概述

+

企业的信息安全体系是非常庞大的,任何一个环节都可能会出现安全风险。其中,黑灰产是安全人员最为关注的一个风险来源,也是历年来导致企业和用户损失最大的因素。

+

如果某个平台或者业务被黑灰产盯上,可能是因为这个业务存在安全隐患被黑灰产利用,也可能只是被黑灰产当做牟利的垫脚石。对黑灰产的监控和防御,就是要了解他们的意图、手段和行为模式,避免被黑灰产攻击或者利用。

+

本次可能会给大家简单介绍国内黑灰产的情况,挑选了几种比较经典的黑产作弊手段进行详细分析,希望能帮助大家对黑灰产这个群体有一定的了解,提升各位的安全意识,在日后的工作和生活中,多一些安全角度的思考。

+

2、课前预习

+

本次课程偏科普性质,但内容不是大家在网络上可以随便看到的,课前可以阅读一些国内黑灰产的调研报告

+

推荐 Freebuf 黑镜调查系列 ,其中部分内容是讲师参与调查编写,不一定权威,但内容和数据都比较真实

+

3、思考

+
    +
  • 身边是否有一些事情是可能与黑产有关的,如何辨别?
  • +
  • 你当前所学习和研究的技术,是否存在一些公开的安全问题,比如漏洞或者设计缺陷?如何避免他人利用这些问题来攻击你?
  • +
  • 如果无法避免被攻击,如何将损失降低到最小?
  • +
+

4、相关阅读

+

关于业务风控

+

《风控要略 互联网业务反欺诈之路》讲师参与编写

+

《互联网平台智能风控实战》

+

关于安全攻防

+

《白帽子讲web安全》

+

《Web安全深度剖析》

+

《Web安全机器学习入门》

+

上述几本都是入门级的书,挑一本即可

+

《 SQL注入攻击与防御》数据库安全进阶

+

《 linux服务器安全攻防》 主机安全进阶

+

关于安全体系建设

+

《互联网企业安全高级指南》

+

《大型互联网企业安全架构》

+ + +
+ +
+
+ + + + + + +
+
+
黑灰产监控与防御
+
https://zhangzhao219.github.io/2023/02/04/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day11/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月4日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/06/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day12/index.html b/2023/02/06/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day12/index.html new file mode 100644 index 000000000..533d08dd1 --- /dev/null +++ b/2023/02/06/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day12/index.html @@ -0,0 +1,960 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分布式定时任务 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

分布式定时任务

+ + +
+ +

分布式定时任务

+ +

分布式定时任务

+

概述

+

本节课程主要分为五个方面:

+
    +
  1. 分布式定时任务整体架构
  2. +
  3. 控制台Admin详细设计
  4. +
  5. 触发器Trigger详细设计
  6. +
  7. 调度器Scheduler详细设计
  8. +
  9. 执行器Executor详细设计
  10. +
+

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

+

课前

+

分布式定时任务发展历史

+
    +
  • Linux命令-CronJob
  • +
  • 单机定时任务-Timer、Ticker
  • +
  • 单机定时任务-ScheduledExecutorService
  • +
  • 任务调度- Quartz
  • +
  • 分布式定时任务
  • +
+

分布式定时任务核心架构

+
    +
  • 控制台Admin
  • +
  • 触发器Trigger
  • +
  • 调度器Scheduler
  • +
  • 执行器Executor
  • +
+

知识点扩充

+
    +
  • 时间轮
  • +
  • 延时消息
  • +
  • 离线计算引擎 Hive
  • +
  • 实时计算引擎 Flink
  • +
+

课中

+

前言

+
    +
  • +

    每年春节抖音都会有很多有意思的玩法,如果同学们是字节的后端同学,怎么设计今年春节集卡瓜分20亿的技术方案?

    +
  • +
  • +

    业务流程

    +
      +
    • 定时扫描抖音用户集卡状态
    • +
    • 汇总计算用户的瓜分金额
    • +
    • 定时开奖
    • +
    +
  • +
  • +

    技术体量

    +
      +
    • 亿级用户规模
    • +
    • 十亿级资金规模
    • +
    • 百万级读写QPS
    • +
    +
  • +
  • +

    方案引出

    +
      +
    • 自动化 + 定时执行 + 海量数据 + 高效稳定 = 分布式定时任务
    • +
    +
  • +
+

发展历程

+
    +
  • +

    发展历史

    +
  • +
  • Linux命令-CronJob
  • +
+

+
    +
  • 单机定时任务-Timer、Ticker
  • +
+

+
    +
  • 单机定时任务-ScheduledExecutorService
  • +
+

+
    +
  • 任务调度- Quartz
  • +
+

+
    +
  • 分布式定时任务
  • +
+

+
    +
  • +

    概述

    +
      +
    • 定义 +
        +
      • 定时任务是指系统为了自动完成特定任务,实时、延时、周期性完成任务调度的过程。
      • +
      • 分布式定时任务是把分散的、可靠性差的定时任务纳入统一的 平台 ,并实现集群管理调度和分布式部署的一种定时任务的管理方式。
      • +
      +
    • +
    • 特点
    • +
    • 执行模式 +
        +
      • 单机任务
      • +
      • 广播任务
      • +
      • Map任务
      • +
      • MapReduce任务
      • +
      +
    • +
    • 现状 +
        +
      • 业内流行框架| | Xxl-job | SchedulerX | TCT | Elastic-job | Saturn |
        +| ---------- | ---------- | ---------- | ---- | ----------- | ------ |
        +| 来源公司 | 美团点评 | 阿里巴巴 | 腾讯 | 当当网 | 唯品会 |
        +| 是否开源 | 是 | 否 | 否 | 是 | 是 |
        +| 任务编排 | 子任务依赖 | 支持 | 支持 | 不支持 | 不支持 |
        +| 任务分片 | 支持 | 支持 | 支持 | 支持 | 支持 |
        +| 高可用 | 支持 | 支持 | 支持 | 支持 | 支持 |
        +| 故障转移 | 支持 | 支持 | 支持 | 支持 | 支持 |
        +| 可视化运维 | 支持 | 支持 | 支持 | 支持 | 支持 |
      • +
      • 美团点评Xxl-job
      • +
      • 阿里巴巴SchedulerX
      • +
      • 腾讯TCT
      • +
      +
    • +
    +
  • +
  • +

    关联方案

    +
      +
    • 单机定时任务
    • +
    • 大数据处理引擎
    • +
    +
  • +
+

实现原理

+
    +
  • +

    整体架构

    +
      +
    • 核心架构
    • +
    +
  • +
+

+
    +
  • 数据流
  • +
+

+
    +
  • 功能架构
  • +
+

+

控制台Admin

+

+

触发器Trigger

+

方案一:腾讯字节方案

+

+

方案二:Quartz方案——时间轮

+

+

调度器Scheduler

+

资源来源

+
    +
  • 业务系统
  • +
  • 定时任务平台
  • +
+

执行器Executor

+

+

业务应用

+
    +
  • 业务应用 +
      +
    • 所有需要定时、延时、周期性执行任务的业务场景,都可以考虑使用分布式定时任务
    • +
    +
  • +
  • 知识面扩充 +
      +
    • 分布式定时任务
    • +
    • 单机定时任务
    • +
    • 延时消息
    • +
    • 离线计算引擎Hive
    • +
    • 实时计算引擎Flink
    • +
    +
  • +
+

课后

+
    +
  1. 分布式定时任务可以帮助我们处理哪些业务场景?
  2. +
  3. 春节集卡瓜分20亿的玩法,发奖金额计算、实时开奖两个阶段分别用到分布式定时任务什么执行模式?
  4. +
  5. 有了分布式定时任务,单机定时任务还有适用场景么?
  6. +
  7. 时间轮这种数据结构,在定时/延时场景相比其他数据结构有哪些优势?
  8. +
  9. 分布式定时任务的调度中心怎么判断一台执行器的机器处于可被调度状态?
  10. +
  11. 你能想到哪些业务场景,实时计算引擎优于分布式定时任务?
  12. +
+ + +
+ +
+
+ + + + + + +
+
+
分布式定时任务
+
https://zhangzhao219.github.io/2023/02/06/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day12/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月6日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/07/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day13/index.html b/2023/02/07/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day13/index.html new file mode 100644 index 000000000..797180502 --- /dev/null +++ b/2023/02/07/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day13/index.html @@ -0,0 +1,815 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 消息队列原理与实战 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

消息队列原理与实战

+ + +
+ +

消息队列原理与实战

+ +

消息队列原理与实战

+

概述

+

本节课程主要分为五个方面:

+
    +
  1. 消息队列的前世今生
  2. +
  3. 消息队列-Kafka
  4. +
  5. 消息队列-BMQ
  6. +
  7. 消息队列-RocketMQ
  8. +
  9. 最佳实践
  10. +
+

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

+

课前

+

消息队列的前世

+
    +
  • 消息队列应用场景
  • +
  • 消息队列的发展历史
  • +
+

常见消息队列

+
    +
  • Kafka使用场景、架构、高级特性
  • +
  • Pulsar使用场景、架构、高级特性
  • +
  • Rocket使用场景、架构、高级特性
  • +
+

课中

+

消息队列是什么

+
    +
  • 解耦
  • +
  • 削峰
  • +
  • 异步
  • +
  • 日志处理
  • +
+

消息队列的前世今生

+

消息队列-Kafka

+

kafka使用场景,业务日志、用户行为数据、Metrics数据

+

基本概念,Producer、Cluster、Consumer、Topic、Partition

+

数据迁移、Offset、Partition选主

+

一条消息从生产到消费是如何处理的,Producer端逻辑、Broker端逻辑、Consumer端逻辑

+

消息队列-BMQ

+

Kafka在使用中遇到问题

+

BMQ架构

+

BMQ各模块是如何工作的,Broker、Proxy、HDFS、MetaStorage

+

BMQ多机房容灾

+

消息队列-RocketMQ

+

RocketMQ使用场景

+

RocketMQ和Kafka对比

+

RocketMQ架构介绍,Producer、Broker、Nameserver、Consumer

+

一条消息从生产到消费是如何处理的,Producer端逻辑、Broker端逻辑、Consumer端逻辑

+

消息队列在字节

+

一些最佳实践的场景,包括数据展示

+

课后

+
    +
  1. 消息队列的应用场景有哪些?
  2. +
  3. Kafka的哪些Feature让其可以支撑大吞吐写入的场景?
  4. +
  5. Kafka Consumer Rebalance的流程简述?
  6. +
  7. BMQ相比较Kafka有哪些优势?
  8. +
  9. RocketMQ有哪些特有的Feature?
  10. +
  11. RocketMQ事务消息处理流程简述?
  12. +
  13. 你认为MQ后面应该如何发展?(开放题)
  14. +
+ + +
+ +
+
+ + + + + + +
+
+
消息队列原理与实战
+
https://zhangzhao219.github.io/2023/02/07/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day13/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月7日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/08/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day14/index.html b/2023/02/08/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day14/index.html new file mode 100644 index 000000000..fd3c377f6 --- /dev/null +++ b/2023/02/08/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day14/index.html @@ -0,0 +1,1151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RPC 原理与实现 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

RPC 原理与实现

+ + +
+ +

RPC 原理与实现

+ +

RPC 原理与实践

+

概述

+

本节课程主要分为四个方面:

+
    +
  1. RPC 相关的基本概念
  2. +
  3. RPC 框架的分层设计
  4. +
  5. 衡量 RPC 框架的一些核心指标
  6. +
  7. 字节内部 RPC 框架 Kitex 实践分享
  8. +
+

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;

+

课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;

+

课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

+

课前

+

RPC 的基本概念

+
    +
  • +

    RPC的概念模型:User、User-Stub、RPC-Runtime、Server-Stub、Server

    + +
  • +
  • +

    IDL(Interface Definition Language) 文件

    +
      +
    • Thrift
    • +
    • Protobuf
    • +
    +
  • +
  • +

    生成代码

    +
  • +
  • +

    编解码(序列化/反序列化)

    +
  • +
  • +

    通信协议

    +
      +
    • 应用层协议
    • +
    +
  • +
  • +

    网络通信

    +
      +
    • IO 网络模型 +
        +
      • blocking IO
      • +
      • unblocking IO
      • +
      • IO multiplexing
      • +
      • signal driven IO
      • +
      • asynchronous IO
      • +
      +
    • +
    • 传输层协议 +
        +
      • TCP
      • +
      • UDP
      • +
      +
    • +
    +
  • +
+

RPC 框架分层设计

+
    +
  • +

    编解码层

    +
      +
    • 数据格式: +
        +
      • 语言特定格式
      • +
      • 文本格式
      • +
      • 二进制编码 +
          +
        • TLV 编码:Thrift 使用 TLV 编码
        • +
        • Varint 编码:Protobuf 使用 Varint 编码
        • +
        +
      • +
      +
    • +
    • 选项: +
        +
      • 兼容性
      • +
      • 通用型
      • +
      • 性能
      • +
      +
    • +
    +
  • +
  • +

    传输协议层

    +
      +
    • 消息切分 +
        +
      • 特殊结束符
      • +
      • 变长协议:length+body
      • +
      +
    • +
    • 协议构造 +
        +
      • 以 Thrift 的 THeader 协议为例讲解
      • +
      +
    • +
    +
  • +
  • +

    网络通信层

    +
      +
    • 网络库
    • +
    • 核心指标 +
        +
      • 吞吐高
      • +
      • 延迟低
      • +
      +
    • +
    +
  • +
+

RPC 框架的核心指标

+
    +
  • +

    稳定性

    +
      +
    • 保障策略 +
        +
      • 熔断
      • +
      • 限流
      • +
      • 超时
      • +
      +
    • +
    • 请求成功率 +
        +
      • 负载均衡
      • +
      • 重试
      • +
      +
    • +
    • 长尾请求 +
        +
      • BackupRequest
      • +
      +
    • +
    +
  • +
  • +

    易用性

    +
      +
    • 开箱即用
    • +
    • 周边工具
    • +
    +
  • +
  • +

    扩展性

    +
  • +
  • +

    观测性

    +
      +
    • Log
    • +
    • Metric
    • +
    • Tracing
    • +
    • 内置观测性服务
    • +
    +
  • +
  • +

    高性能

    +
  • +
+

字节内部 Kitex 实践分享

+
    +
  • +

    Kitex 整体架构

    +
  • +
  • +

    自研网络库 Netpoll

    +
  • +
  • +

    性能优化

    +
      +
    • 网络库优化
    • +
    • 编解码优化
    • +
    +
  • +
  • +

    合并部署

    +
  • +
+

课中

+

基本概念

+
    +
  • +

    相比本地函数调用,RPC调用需要解决的问题

    +
      +
    • 函数映射
    • +
    • 数据转换成字节流
    • +
    • 网络传输
    • +
    +
  • +
  • +

    一次 RPC 的完整过程

    +
  • +
  • +

    RPC 带来的问题将由 RPC 框架来解决

    +
      +
    • 服务宕机如何感知?
    • +
    • 遇到网络异常应该如何应对?
    • +
    • 请求量暴增怎么处理?
    • +
    +
  • +
+

RPC 框架分层设计

+

+

编解码层

+
    +
  • +

    数据格式

    +
      +
    • 语言特定格式:例如 java.io.Serializable
    • +
    • 文本格式:例如 JSON、XML、CSV 等
    • +
    • 二进制编码:常见有 Thrift 的 BinaryProtocol,Protobuf,实现可以有多种形式,例如 TLV 编码 和 Varint 编码
    • +
    +
  • +
  • +

    选型考察点

    +
      +
    • 兼容性
    • +
    • 通用型
    • +
    • 性能 +
        +
      • 空间开销
      • +
      • 时间开销
      • +
      +
    • +
    +
  • +
  • +

    生成代码和编解码层相互依赖,框架的编解码应当具备扩展任意编解码协议的能力

    +
  • +
+

+

协议层

+
    +
  • 以 Thrift 的 THeader 协议为例
  • +
+

+
-   LENGTH 字段 32bits,包括数据包剩余部分的字节大小,不包含 LENGTH 自身长度
+-   HEADER MAGIC 字段16bits,值为:0x1000,用于标识 协议版本信息,协议解析的时候可以快速校验
+-   FLAGS 字段 16bits,为预留字段,暂未使用,默认值为 0x0000
+-   SEQUENCE NUMBER 字段 32bits,表示数据包的 seqId,可用于多路复用,最好确保单个连接内递增
+-   HEADER SIZE 字段 16bits,等于头部长度字节数/4,头部长度计算从第14个字节开始计算,一直到 PAYLOAD 前(备注:header 的最大长度为 64K)
+-   PROTOCOL ID 字段 uint8 编码,取值有: - ProtocolIDBinary = 0 - ProtocolIDCompact = 2
+-   NUM TRANSFORMS 字段 uint8 编码,表示 TRANSFORM 个数
+-   TRANSFORM ID 字段 uint8 编码,表示压缩方式 zlib or snappy
+-   INFO ID 字段 uint8 编码,具体取值参考下文,用于传递一些定制的 meta 信息
+-   PAYLOAD 消息内容
+
+
    +
  • 协议解析
  • +
+

+

网络通信层

+

+
    +
  • 阻塞 IO 下,耗费一个线程去阻塞在 read(fd) 去等待用足够多的数据可读并返回。
  • +
  • 非阻塞 IO 下,不停对所有 fds 轮询 read(fd) ,如果读取到 n <= 0 则下一个循环继续轮询。
  • +
+

第一种方式浪费线程(会占用内存和上下文切换开销),第二种方式浪费 CPU 做大量无效工作。而基于 IO 多路复用系统调用实现的 Poll 的意义在于将可读/可写状态通知和实际文件操作分开,并支持多个文件描述符通过一个系统调用监听以提升性能。

+

网络库的核心功能就是去同时监听大量的文件描述符的状态变化(通过操作系统调用),并对于不同状态变更,高效,安全地进行对应的文件操作。

+

RPC 框架核心指标

+

稳定性

+
    +
  • 保障策略 +
      +
    • 熔断
    • +
    • 限流
    • +
    • 超时控制
    • +
    +
  • +
+

+

从某种程度上讲超时、限流和熔断也是一种服务降级的手段 。

+
    +
  • +

    请求成功率

    +
      +
    • 负载均衡
    • +
    • 重试
    • +
    +
  • +
  • +

    长尾请求

    +
      +
    • BackupRequest
    • +
    +
  • +
+

+

易用性

+
    +
  • +

    开箱即用

    +
      +
    • 合理的默认参数选项、丰富的文档
    • +
    +
  • +
  • +

    周边工具

    +
      +
    • 生成代码工具、脚手架工具
    • +
    +
  • +
+

扩展性

+
    +
  • Middleware:middleware 会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等
  • +
  • Option:作为初始化参数
  • +
  • 核心层是支持扩展的:编解码、协议、网络传输层
  • +
  • 代码生成工具也支持插件扩展
  • +
+

+

观测性

+
    +
  • 三件套:Log、Metric 和 Tracing
  • +
+

+
    +
  • 内置观测性服务,用于观察框架内部状态 +
      +
    • 当前环境变量
    • +
    • 配置参数
    • +
    • 缓存信息
    • +
    • 内置 pprof 服务用于排查问题
    • +
    +
  • +
+

高性能

+
    +
  • 连接池和多路复用:复用连接,减少频繁建联带来的开销
  • +
  • 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap’n Proto 等
  • +
  • 高性能网络库:Netpoll 和 Netty 等
  • +
+

字节内部 Kitex 实践分享

+
    +
  1. +

    框架文档 Kitex

    +
  2. +
  3. +

    自研网络库 Netpoll,背景:
    +a. 原生库无法感知连接状态

    +

    b. 原生库存在 goroutine 暴涨的风险

    +
  4. +
  5. +

    扩展性:支持多协议,也支持灵活的自定义协议扩展

    +
  6. +
  7. +

    性能优化,参考 字节跳动 Go RPC 框架 KiteX 性能优化实践
    +a. 网络优化

    +
      +
    • i. 调度优化
    • +
    • ii. LinkBuffer 减少内存拷贝,从而减少 GC
    • +
    • iii. 引入内存池和对象池
    • +
    +

    b. 编解码优化

    +
      +
    • i. Codegen:预计算提前分配内存,inline,SIMD等
    • +
    • ii. JIT:无生产代码,将编译过程移到了程序的加载(或首次解析)阶段,可以一次性编译生成对应的 codec 并高效执行
    • +
    +
  8. +
  9. +

    合并部署
    +a. 微服务过微,引入的额外的传输和序列化开销越来越大

    +

    b. 将强依赖的服务统计部署,有效减少资源消耗

    +
  10. +
+

课后

+
    +
  1. 行业内各个流行的 RPC 框架的优劣对比
  2. +
  3. 从第三章节 RPC 的核心指标来看,Kitex 还有哪些功能是欠缺或者需要加强的?
  4. +
  5. 了解微服务的新趋势 ServiceMesh,以及 RPC 框架和 ServiceMesh 的关系
  6. +
  7. 关于 RPC 框架,业界有哪些新的趋势和概念?
  8. +
  9. Netpoll 的优势在哪?相比其他高性能网络库例如 Netty 还有什么不足?
  10. +
  11. Flatbuffer 和 Cap’n Proto 等编解码协议为什么高性能?
  12. +
+

参考文献

+
    +
  1. 官方文档 Kitex Netpoll
  2. +
  3. 字节跳动 Go RPC 框架 KiteX 性能优化实践_架构_字节跳动技术团队_InfoQ精选文章
  4. +
  5. 字节跳动微服务架构体系演进_架构_字节跳动技术团队_InfoQ精选文章
  6. +
+ + +
+ +
+
+ + + + + + +
+
+
RPC 原理与实现
+
https://zhangzhao219.github.io/2023/02/08/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day14/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月8日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/10/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day15/index.html b/2023/02/10/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day15/index.html new file mode 100644 index 000000000..26f9df06a --- /dev/null +++ b/2023/02/10/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day15/index.html @@ -0,0 +1,839 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 带你认识存储的本质 - 状态 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

带你认识存储的本质 - 状态

+ + +
+ +

带你认识存储的本质 - 状态

+ +

带你认识存储的本质 - 状态

+

课程概述

+

存储系统和数据库系统往往是后端服务的最后一环,提供数据存储、查询能力。本课程会先用模拟案例导入,向学员介绍存储系统、数据库系统的特点,然后解析多个主流产品,最后分享存储和数据库结合新技术演进的方向。本节课程主要包含以下内容:

+
    +
  1. 模拟案例
  2. +
  3. 存储 & 数据库简介
  4. +
  5. 主流产品剖析
  6. +
  7. 新技术演进
  8. +
+

课前材料 (必须)

+

跟存储 & 数据库系统相关的材料很多,涵盖开源项目、博客、论文等。下面提供部分资料作为参考

+
    +
  1. The Google File System
  2. +
+

static.googleusercontent.com/media/resea…

+

作为各种开源分布式文件系统的鼻祖,GFS论文里面提到的架构非常经典,值得一学。

+
    +
  1. The Linux Programming Interface(第13章 FILE I/O BUFFERING)
  2. +
+

本书介绍了很多Linux内核子系统的实现,其中第13章着重讲了单机的文件IO。学习完Linux中的文件IO栈,对单机存储系统会有更深的认识。

+

课程详情

+

经典案例

+

通过一个模拟案例,描述了数据是怎么产生,在后端系统里怎么流通,最后怎么写入到存储/数据库系统。

+

存储 & 数据库简介

+
    +
  • 存储系统概览 +
      +
    • 存储系统特点
    • +
    • 存储器层级结构
    • +
    • 单机存储栈
    • +
    • RAID技术
    • +
    +
  • +
  • 数据库系统概览 +
      +
    • 关系型数据库特点
    • +
    • 非关系型数据库特点
    • +
    • 数据库 vs 经典存储
    • +
    • 数据库使用方式
    • +
    +
  • +
+

主流产品剖析

+
    +
  • 单机存储产品 +
      +
    • 单机文件系统
    • +
    • 单机key-value存储
    • +
    +
  • +
  • 分布式存储产品 +
      +
    • HDFS
    • +
    • Ceph
    • +
    +
  • +
  • 单机数据库产品 +
      +
    • 关系型数据库 —— PG、MySQL
    • +
    • 非关系型数据库 —— ES、MongoDB、Redis
    • +
    • Elasticsearch使用案例
    • +
    +
  • +
  • 分布式数据库产品 +
      +
    • 问题与挑战
    • +
    • 解决方案
    • +
    +
  • +
+

新技术演进

+
    +
  • SPDK
  • +
  • 人工智能
  • +
  • 新硬件加速
  • +
+

课后思考

+
    +
  1. 写入存储系统的粒度太大,会不会导致数据原子性问题?例如一次性写100MB,如果系统突然crash,会不会只有一部分数据持久化了,另一部分丢失了?如果要解决原子性问题,一般会设计什么机制?
  2. +
  3. 在从应用程序到存储介质的链路上,无论读还是写,数据可能要被拷贝好几次,这几次拷贝能不能去掉?如果我们去掉大部分拷贝操作,会有什么副作用,要怎么缓解副作用?
  4. +
  5. 一个关系型数据库大概率是会被并发访问的,如果要保证并发安全,除了在行数据上加悲观锁还有其他方式吗?
  6. +
  7. 在数据库领域,把数据按行存和按列存各有好处,你能从性能优先的角度设计出一种混合存储格式吗?
  8. +
+ + +
+ +
+
+ + + + + + +
+
+
带你认识存储的本质 - 状态
+
https://zhangzhao219.github.io/2023/02/10/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day15/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月10日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/11/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day16/index.html b/2023/02/11/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day16/index.html new file mode 100644 index 000000000..ada925dcc --- /dev/null +++ b/2023/02/11/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day16/index.html @@ -0,0 +1,856 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MySQL - 深入理解RDBMS - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

MySQL - 深入理解RDBMS

+ + +
+ +

MySQL - 深入理解RDBMS

+ +

MySQL - 深入理解 RDBMS

+

课程概述

+

RDBMS(关系型数据库)是目前使用最为广泛的数据库之一,同时也是整个信息化时代的基石。本节课程通过生活中常见的场景向大家介绍RDBMS的作用、发展历程及其核心技术,最后以字节为例,展示了RDBMS的企业级实践。本节课程主要包含以下内容:

+
    +
  1. 经典案例
  2. +
  3. 发展历史
  4. +
  5. 关键技术
  6. +
  7. 企业实践
  8. +
+

课前材料

+

RDBMS有相关的数据和材料都非常多,这里主要给大家提供几篇经典论文,从经典的论文中,能够更有效的帮助大家理解RDBMS。

+
    +
  1. A Relational Model of Data for Large Shared Data Banks
  2. +
+

暂时无法在飞书文档外展示此内容

+

这篇论文是RDBMS的奠基之作,由RDBMS之父E.F.Codd博士于1970年发表。在这篇论文中,E.F.Codd首次提出了用于管理数据的关系模型,并将数据独立于硬件来存储,用户使用一个非过程语言来访问数据。

+
    +
  1. Readings in Database Systems(Fifth Edition)
  2. +
+

暂时无法在飞书文档外展示此内容

+

这本书被称为数据库领域的“红宝书”,由著名的图灵奖获得者,数据库领域专家,Michael Stonebraker撰写。其中介绍了数据库的基本概念,传统的RDBMS以及新的数据库架构等等,是一本非常棒的数据库领域入门文章。

+

课程详情

+

经典案例

+

通过抖音红包雨的案例,介绍 RDBMS 中 ACID 的概念:

+
    +
  • 原子性( Atomicity ):事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生。
  • +
  • 一致性( Consistency ):数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
  • +
  • 隔离性( Isolation ):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
  • +
  • 持久性( Durability ):在事务完成以后,该事务所对数据库所做的更改便持久的保存在数据库之中,并不会被回滚。
  • +
+

发展历史

+

数据库发展最初过程中,诞生过3种数据模型,最终关系型模型成为了应用最为广泛的数据库模型。

+
    +
  • 网状模型:用有向图表示实体和实体之间的联系的数据结构模型称为网状数据模型。
  • +
  • 层次模型:层次数据模型是用树状<层次>结构来组织数据的数据模型。
  • +
  • 关系模型:使用表格表示实体和实体之间关系的数据模型称之为关系数据模型。
  • +
+ + + + + + + + + + + + + + + + + + + + + + + +
网状模型层次模型关系模型
优势能直接描述现实世界 存取效率较高结构简单 查询效率高 可以提供较好的完整性支持实体及实体间的的联系都通过二维表结构表示 可以方便的表示M:N关系 数据访问路径对用户透明
劣势结构复杂 用户不易使用 访问程序设计复杂无法表示M:N的关系 插入、删除限制多 遍历子节点必须经过父节点 访问程序设计复杂关联查询效率不够高 关系必须规范化
+

关键技术

+

SQL 执行流程

+

在SQL执行过程中,需要经历SQL引擎、存储引擎、以及事务引擎等模块。而其中SQL引擎又分为Parser、Optimizer、Executor几个部分:

+

+

SQL 引擎

+

SQL引擎包括了:

+
    +
  • Paser:经过词法分析、语法分析生成语法树,然后对语法树进行合法性校验。
  • +
  • Optimizer:根据Parser产生的语法树,根据规则或者代价产生执行计划树。
  • +
  • Executor:根据计划树进行执行,常见的执行方式是火山模型。
  • +
+

存储引擎

+

存储引擎负责了数据的底层存储、管理和访问工作。各大RDBMS存储引擎的设计都有不少的差异,这里选择MySQL的InnoDB存储引擎来向大家做一个介绍:

+
    +
  • Buffer Pool:存储引擎位于内存中的重要结构,用于缓存数据,减少磁盘IO的开销。
  • +
  • Page:数据存储的最基本单位,一般为16KB。
  • +
  • B+u Tree:InnoDB中最常用的索引结构。
  • +
+

事务引擎

+

事务引擎实现了数据库的ACID能力,这里还是以MySQL的InnoDB为例来介绍数据库内部是通过哪些技术来实现ACID:

+
    +
  • Atomicity:InnoDB中通过undo日志实现了数据库的原子性,通过Undo Log,数据库可以回滚到事务开始的状态;
  • +
  • Isolation:通过Undo Log实现MVCC(多版本并发控制),降低读写冲突。
  • +
  • Durability:通过Redo Log(一种WAL实现方式)来保证事务在提交后一定能持久化到磁盘中。
  • +
  • Consistency:一致性本质上是一种业务层的限制。
  • +
+

企业实践

+

字节中是国内数据规模最大的互联网公司之一,公司内部有成千上万套RDBMS系统。这一章节还是以红包雨为案例,展示了字节是如何解决大流量、流量突增、高可靠等问题的。

+

课后大作业

+
    +
  1. WAL 日志到底是如何保证数据的持久化,宕机后数据不丢失的?相比于其他方案,WAL 日志都有什么优势?
  2. +
  3. 除了 Undo Log 之外,是否还有其他方案可以实现 MVCC?
  4. +
  5. 基于代价的优化器一般需要考虑哪些代价?
  6. +
  7. 执行器的执行模型,除了本课中提到的火山模型是否还有其他模型?相比于火山模型有什么优劣势?
  8. +
  9. InnoDB 的 B+ Tree 是怎么实现的?
  10. +
  11. InnoDB 的 buffer pool 是怎么实现页面管理和淘汰的?
  12. +
+ + +
+ +
+
+ + + + + + +
+
+
MySQL - 深入理解RDBMS
+
https://zhangzhao219.github.io/2023/02/11/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day16/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/13/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day17/index.html b/2023/02/13/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day17/index.html new file mode 100644 index 000000000..0e9f1bd28 --- /dev/null +++ b/2023/02/13/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day17/index.html @@ -0,0 +1,811 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Redis - 大厂程序员是怎么用的 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Redis - 大厂程序员是怎么用的

+ + +
+ +

Redis - 大厂程序员是怎么用的

+ +

Redis - 大厂程序员是怎么用的

+

1.概述

+

本节课程主要分为三个方面:

+
    +
  1. 为什么需要Redis,Redis的基本工作原理
  2. +
  3. Redis应用案例
  4. +
  5. 在字节跳动,使用Redis有哪些注意事项
  6. +
+

2.课前(必须)

+

2.1 安装Golang开发环境

+ +

2.2 安装Redis

+ +

2.3 熟悉Redis基本操作

+

2.3.1 熟悉以下命令的操作

+
    +
  • GET/SET/DEL/INCR/SETNX
  • +
  • HSET/HGET/HINCRBY
  • +
  • LPUSH/RPOP/LRANGE
  • +
  • ZADD/ZRANGEBYSCORE/ZREVRANGE/ZINCRBY/ZSCORE
  • +
+

2.3.2 了解pipelining概念

+

2.4 复习数据结构

+
    +
  • 链表/FIFO
  • +
  • Hash Tale
  • +
  • Skip List
  • +
+

3.课中

+

3.1 Redis基本工作原理

+
    +
  • Redis实现数据持久化的原理:AOF/RDB
  • +
  • Redis单线程处理命令的概念
  • +
+

3.2 Redis应用案例

+
    +
  • 掘金连续签到,需要了解GET/SET,Key过期
  • +
  • 掘金用户计数,使用到HASH
  • +
  • 排行榜ZSET
  • +
  • 使用SETNX实现分布式锁
  • +
+

3.3 在字节跳动,使用Redis有哪些注意事项

+
    +
  • 大Key:Value大于10KB就是大Key,使用大Key将导致Redis系统不稳定
  • +
  • 热Key:一个Key的QPS特别高,将导致Redis实例出现负载突增,负责均衡流量不均的情况。导致单实例故障
  • +
  • 慢查询:大Key、热Kye的读写;一次操作过多的Key(mset/hmset/sadd/zadd)
  • +
  • 导致缓存穿透、缓存雪崩的场景及避免方案
  • +
+ + +
+ +
+
+ + + + + + +
+
+
Redis - 大厂程序员是怎么用的
+
https://zhangzhao219.github.io/2023/02/13/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day17/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/14/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day18/index.html b/2023/02/14/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day18/index.html new file mode 100644 index 000000000..b4206c894 --- /dev/null +++ b/2023/02/14/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day18/index.html @@ -0,0 +1,1362 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CIickHouse - 你没有见过的列存储 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

CIickHouse - 你没有见过的列存储

+ + +
+ +

CIickHouse - 你没有见过的列存储

+ +

ClickHouse - 你没有见过的列存储

+

概述

+

本节课程分为四个部分

+
    +
  1. 数据库基本概念
  2. +
  3. 列式存储
  4. +
  5. ClickHouse存储设计
  6. +
  7. ClickHouse典型应用场景
  8. +
+

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

+

课前 (必须)

+

数据库基本概念

+
    +
  1. 数据库
  2. +
  3. DBMS:数据库管理系统
  4. +
  5. OLTP 数据库 OLTP(Online transactional processing)
  6. +
  7. OLAP 数据库:OLAP (Online analytical processing)
  8. +
  9. SQL (Structured Query Language)
  10. +
  11. 词法分析
  12. +
  13. 语法分析
  14. +
  15. AST (Abstract syntax tree)
  16. +
+

列式存储

+
    +
  1. 行式存储
  2. +
  3. 列式存储
  4. +
  5. 数据压缩
    +a. LZ4
    +b. Run-length encoding
    +c. Delta encoding
  6. +
  7. 延迟物化
    +a. 物化
    +b. Cpu cache
    +c. 内存带宽
  8. +
  9. 向量化
    +a. SIMD (single instruction multiple data)
    +b. SSE指令集
    +c. AVX指令集
  10. +
+

ClickHouse存储设计

+
    +
  1. Shard key
  2. +
  3. 索引
    +a. 哈希索引
    +b. B-Tree
    +c. B+Tree
    +d. LSM-Tree
  4. +
+

ClickHouse典型应用场景

+
    +
  1. Kafka
  2. +
  3. Spark
  4. +
  5. Hdfs
  6. +
  7. Bitmap
  8. +
  9. 字典编码
  10. +
+

课中

+

数据库基本概念

+

数据库是什么

+

数据库是结构化信息或数据的有序集合,一般以电子形式存储在计算机系统中。通常由数据库管理系统 (DBMS) 来控制。在现实中,数据、DBMS 及关联应用一起被称为数据库系统,通常简称为数据库。

+

一个简单的例子

+
    +
  1. 数据解析整理成有序集合
  2. +
+

+
    +
  1. 数据的写入和读取,可以通过查询语言获取想要的信息
  2. +
+

+

数据库的类型

+
    +
  1. 数据库有很多种,至于各种数据库孰优孰劣,主要取决于企业希望如何使用数据。
  2. +
  3. 关系数据库:关系型数据库是把数据以表的形式进行储存,然后再各个表之间建立关系,通过这些表之间的关系来操作不同表之间的数据。
  4. +
  5. 非关系数据库 NoSQL 或非关系数据库,支持存储和操作非结构化及半结构化数据。相比于关系型数据库,NoSQL没有固定的表结构,且数据之间不存在表与表之间的关系,数据之间可以是独立的。NoSQL的关键是它们放弃了传统关系型数据库的强事务保证和关系模型,通过所谓最终一致性和非关系数据模型(例如键值对,图,文档)来提高Web应用所注重的高可用性和可扩展性。
  6. +
  7. 单机数据库:在一台计算机上完成数据的存储和查询的数据库系统。
  8. +
  9. 分布式数据库 分布式数据库由位于不同站点的两个或多个文件组成。数据库可以存储在多台计算机上,位于同一个物理位置,或分散在不同的网络上。
  10. +
  11. OLTP 数据库 OLTP(Online transactional processing)数据库是一种高速分析数据库,专为多个用户执行大量事务而设计。
  12. +
  13. OLAP 数据库:OLAP (Online analytical processing) 数据库旨在同时分析多个数据维度,帮助团队更好地理解其数据中的复杂关系
  14. +
+

OLAP数据库

+
    +
  1. 大量数据的读写,PB级别的存储
  2. +
  3. 多维分析,复杂的聚合函数
  4. +
+

+

+
    +
  1. 离线/实时分析,对查询速度有要求
  2. +
+

SQL

+
    +
  1. 一种编程语言,目前几乎所有的关系数据库都使用 SQL (Structured Query Language ) 编程语言来查询、操作和定义数据,进行数据访问控制。
  2. +
  3. SQL的结构
  4. +
+

查询包含一系列含有最终结果的字段, 紧跟 SELECT关键词。星号(“*”)也可以用来指定查询应当返回查询表所有字段,可选的关键词和子句包括:

+
    +
  • FROM子句指定了选择的数据表。FROM子句也可以包含 JOIN 二层子句来为数据表的连接设置规则。
  • +
  • WHERE子句后接一个比较谓词以限制返回的行。WHERE子句仅保留返回结果里使得比较谓词的值为True的行。
  • +
  • GROUP BY子句用于将若干含有相同值的行合并。 GROUP BY通常与SQL聚合函数连用,或者用于清除数据重复的行。GROUP BY子句要用在 WHERE子句之后。
  • +
  • HAVING子句后接一个谓词来过滤从 GROUP BY子句中获得的结果,由于其作用于 GROUP BY子句之上,所以聚合函数也可以放到其谓词中。
  • +
  • ORDER BY子句指明将哪个字段用作排序关键字,以及排序顺序(升序/降序),如果无此子句,那么返回结果的顺序不能保证有序。
  • +
+

+
    +
  1. SQL的用途
    +a. 定义数据模型
  2. +
+
CREATE TABLE default.test_insert_local
+(
+   `p_date` Date,
+   `id` Int32
+)
+ENGINE = MergeTree
+PARTITION BY p_date
+ORDER BY id
+SETTINGS index_granularity = 8192
+复制代码
+

b. 读写数据库数据

+
insert into default.test_insert_local values ('2022-01-01', 1);
+
+select count() from default.test_insert_local;
+复制代码
+
    +
  1. SQL的优点
  2. +
+
    +
  • 标准化,ISO和ANSI是长期建立使用的SQL数据库标准
  • +
  • 高度非过程化,用SQL进行数据操作,用户只需提出“做什么”,而不必指明“怎么做”,因此用户无须了解存取路径,存取路径的选择以及SQL语句的操作过程由系统自动完成。这不但大大减轻了用户负担,而且有利于提高数据独立性。
  • +
  • 以同一种语法结构提供两种使用方式,用户可以在终端上直接输入SQL命令对数据库进行操作。作为嵌入式语言,SQL语句能够嵌入到高级语言(如C、C#、JAVA)程序中,供程序员设计程序时使用。而在两种不同的使用方式下,SQL的语法结构基本上是一致的。
  • +
  • 语言简洁,易学易用:SQL功能极强,但由于设计巧妙,语言十分简洁,完成数据定义、数据操纵、数据控制的核心功能只用了9个动词:CREATE、ALTER、DROP、SELECT、INSERT、UPDATE、DELETE、GRANT、REVOKE。且SQL语言语法简单,接近英语口语,因此容易学习,也容易使用。
  • +
+

数据库的架构

+

+
    +
  1. Client
  2. +
  3. Parser
    +词法分析,语法分析,生成AST树 (Abstract syntax tree)
  4. +
+

+
    +
  1. Analyzer
    +变量绑定、类型推导、语义检查、安全、权限检查、完整性检查等,为生成计划做准备
  2. +
  3. Analyzer
    +变量绑定、类型推导、语义检查、安全、权限检查、完整性检查等,为生成计划做准备
  4. +
  5. Optimizer
  6. +
+
    +
  • 为查询生成性能最优的执行计划
  • +
  • 进行代价评估
  • +
  • Executor 将执行计划翻译成可执行的物理计划
  • +
  • Storage engine
    +a. 管理内存数据结构【index、内存数据、缓存(Query cache、Data cache、Index cache)】
    +b. 管理磁盘数据【磁盘数据的文件格式、磁盘数据的增删查改】
    +c. 读写算子【数据写入逻辑、数据读取逻辑】
  • +
+

一个sql的执行流程

+

+

+

+

设计数据库存储的要点

+
    +
  1. 性能瓶颈在哪里:数据选择、数据读取、构造内存数据、计算
  2. +
  3. 选择什么样的数据格式:是否可以并发处理、是否可以构建索引、行存,列存 或者 行列混合存储
  4. +
  5. 选择什么样的索引:读写的方式:读多写少、读少写多、点查场景、分析型场景
  6. +
+

列式存储

+

什么是列存

+
    +
  1. 行存的存储
  2. +
+

+
    +
  1. 列存的存储
  2. +
+

+

列存的优点

+

a. 数据压缩

+
    +
  • 数据压缩可以使读的数据量更少,在IO密集型计算中获得大的性能优势
  • +
  • 相同类型压缩效率更高
  • +
  • 排序之后压缩效率更高
  • +
  • 可以针对不同类型使用不同的压缩算法
  • +
  • 几种常见的压缩算法
  • +
+

【LZ4】

+
输入:abcde_bcdefgh_abcdefghxxxxxxx
+
+输出:abcde_(5,4)fgh_(14,5)fghxxxxxxx
+复制代码
+

(5,4) 代表向前5个byte,匹配到的内容长度有4,即"bcde"是一个重复

+

重复项越多或者越长,压缩率就会越高

+

【Run-length encoding】

+
输入:WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWBWWWWWWWWWWWWWW
+
+输出:12W1B12W3B24W1B14W
+复制代码
+
+

压缩重复的数据

+
+

【Delta encoding】

+
输入:105, 135, 112, 135, 143, 147
+
+输出:105(base),30, -23, 23, 8, 4
+复制代码
+

将数据存储为连续数据之间的差异,而不是直接存储数据本身

+

b. 数据处理

+

【查询优化】1.可以选择特定的列做计算而不是读所有列 2.对聚合计算友好

+

+

【延迟物化】

+
    +
  • 物化:将列数据转换为可以被计算或者输出的行数据或者内存数据结果的过程,物化后的数据通常可以用来做数据过滤,聚合计算,Join
  • +
+

image.png

+
    +
  • 延迟物化:尽可能推迟物化操作的发生
  • +
+

image.png

+
    +
  • 缓存友好
  • +
  • CPU / 内存带宽友好
  • +
  • 可以利用到执行计划和算子的优化,例如filter
  • +
  • 保留直接在压缩列做计算的机会
  • +
+

【向量化】

+
    +
  • SIMD
    +single instruction multiple data,对于现代多核CPU,其都有能力用一条指令执行多条数据
    +对于代码
  • +
+
for (size_t i = 0; i < 100; ++i) 
+ c[i] = a[i] + b[i];
+复制代码
+

非向量化执行

+
c[0] = a[0] + b[0]; 
+c[1] = a[1] + b[1];
+... ... 
+复制代码
+

如果这时候CPU也可以并行的计算我们写的代码,那么理论上我们的处理速度就会是之前代码的100倍,幸运的是SIMD指令就是完成这样的工作的,用SIMD指令完成这样代码设计和执行就叫做向量化

+

image.png

+
    +
  • 执行模型
    +数据需要按批读取
    +函数的调用需要明确数据类型
  • +
+

+
    +
  • 列存数据库适合设计出这样的执行模型,从而使用向量化技术
  • +
+

列存 VS 行存

+

image.png

+

ClickHouse的存储设计

+

ClickHouse的架构

+
    +
  1. 架构图
  2. +
+

+
    +
  1. 表定义和结构
  2. +
+

+
    +
  1. 集群架构
  2. +
+

+

ClickHouse的存储架构

+
    +
  1. 数据结构
  2. +
+

a.文件组织

+

+

b.文件内容

+
+

对于表

+
+
CREATE TABLE test.test_insert_local
+(
+    `p_date` Date,
+    `id` Int32
+)
+ENGINE = MergeTree
+PARTITION BY p_date
+ORDER BY id
+SETTINGS index_granularity = 8192
+复制代码
+
+

它的文件组织

+
+
├── 20220101_1_1_0
+│   ├── checksums.txt
+│   ├── columns.txt
+│   ├── count.txt
+│   ├── data.bin
+│   ├── data.mrk3
+│   ├── default_compression_codec.txt
+│   ├── minmax_p_date.idx
+│   ├── partition.dat
+│   ├── primary.idx
+│   └── versions.txt
+├── 20220102_2_2_0
+│   ├── checksums.txt
+│   ├── columns.txt
+│   ├── count.txt
+│   ├── data.bin
+│   ├── data.mrk3
+│   ├── default_compression_codec.txt
+│   ├── minmax_p_date.idx
+│   ├── partition.dat
+│   ├── primary.idx
+│   └── versions.txt
+├── detached
+└── format_version.txt
+复制代码
+

c. part和partition

+
    +
  • part是物理文件夹的名字
  • +
  • partition是逻辑结构
  • +
+

+

d. part和column

+
    +
  • 每个column都是一个文件
  • +
  • 所有的column文件都在自己的part文件夹下
  • +
+

e. column和index

+
    +
  • 一个part有一个主键索引
  • +
  • 每个column都有列索引
  • +
+

索引设计

+
    +
  1. 主键索引
  2. +
+
CREATE TABLE hits_UserID_URL
+(
+    `UserID` UInt32,
+    `URL` String,
+    `EventTime` DateTime
+)
+ENGINE = MergeTree
+PRIMARY KEY (UserID, URL)
+ORDER BY (UserID, URL, EventTime)
+SETTINGS index_granularity = 8192, index_granularity_bytes = 0;
+复制代码
+
    +
  1. 数据按照主键顺序一次排序
    +UserID首先做排序,然后是URL,最后是EventTime
  2. +
+

+
    +
  1. 数据被组织成granule
  2. +
+
    +
  • granule是引擎做数据处理的最小数据单位,引擎读数据的时候不是按照一行一行读取的,而是最少读取一个granule
  • +
  • 方便构建稀疏索引
  • +
  • 方便并行计算
  • +
+

+
    +
  1. 每个granule都对应primary.idx里面的一行
  2. +
+

+
    +
  1. 默认每8192行记录主键的一行值,primary.idx需要被全部加载到内存里面
  2. +
+

+
    +
  1. 每个主键的一行数据被称为一个mark
  2. +
+

+
    +
  1. 每个列都有这样一个mark文件,mark文件存储所有granule在物理文件里面的地址,每一列都有一个mark文件
  2. +
+

+
    +
  1. mark文件里面的每一行存储两个地址
  2. +
+
    +
  • 第一个地址称为block_offset,用于定位一个granule的压缩数据在物理文件中的位置,压缩数据会以一个block为单位解压到内存中。
  • +
  • 第二个地址称为granule_offset,用于定位一个granule在解压之后的block中的位置。
  • +
+

索引的缺陷和优化

+
    +
  1. 缺陷:数据按照key的顺序做排序,因此只有第一个key的过滤效果好,后面的key过滤效果依赖第一个key的基数大小
  2. +
+

+
    +
  1. 二级索引
  2. +
+
    +
  • 在URL列上构建二级索引
  • +
+

+
    +
  1. 构建多个主键索引
  2. +
+
    +
  • 再建一个表(数据需要同步两份,查询需要用户判断查哪张表)
  • +
+

+
    +
  • 建一个物化视图(数据自动同步到隐式表,查询需要用户判断查哪张表)
  • +
+

+
    +
  • 使用Projection(数据自动同步到隐式表,查询自动路由到最优的表)
  • +
+

+

数据合并

+
    +
  • 一个part内的数据是有序的
  • +
+

+
    +
  • 不同part之间的数据是无序的
  • +
+

+
    +
  • 数据合并是将多个part合并成一起的过程
  • +
+

+
    +
  • part的合并发生在一个分区内
  • +
+

+
    +
  • 数据的可见性
    +数据合并过程中,未被合并的数据对查询可见
    +数据合并完成后,新part可见,被合并的part被标记删除
  • +
+

+

数据查询

+
    +
  1. 对于查询
  2. +
+
SELECT
+    URL,
+    count(URL) AS Count
+FROM hits_UserID_URL
+WHERE UserID = 749927693
+GROUP BY URL
+ORDER BY Count DESC
+LIMIT 10
+复制代码
+
    +
  1. 通过主键找到需要读的mark
  2. +
  3. 切分marks,然后并发的调度reader
  4. +
+

+
    +
  1. Reader 通过mark block_offset得到需要读的数据文件的偏移量
  2. +
  3. Reader 通过mark granule_offset得到解压之后数据的偏移量
  4. +
+

+
    +
  1. 构建列式filter做数据过滤
  2. +
+

+

ClickHouse的典型使用场景

+

大宽表存储和查询

+
    +
  1. 动态表结构
  2. +
+
CREATE TABLE test_multi_columns
+(
+    `p_date` Date,
+    `id` Int32,
+    `map_a` Map(String, Int32)
+)
+ENGINE = MergeTree
+PARTITION BY p_date
+ORDER BY map_a
+复制代码
+

+
    +
  1. +

    map中的每个key都是一列

    +
  2. +
  3. +

    map中的每一列都可以单独的查询

    +
  4. +
  5. +

    使用方式同普通列,可以做任何计算

    +
  6. +
  7. +

    大宽表查询

    +
  8. +
+

可以建非常多的列查询的时候引擎可以快速选择需要的列,查询的时候引擎可以快速选择需要的列

+

+

离线数据分析

+
    +
  1. 数据导入
  2. +
+

+
+

数据可以通过spark生成clickhouse格式的文件

+

导入到hdfs上由hive2ch导入工具完成数据导入

+

数据直接导入到各个物理节点

+
+
    +
  1. 数据按列导入
  2. +
+

+

保证查询可以及时访问已有数据

+

可以按需加载需要的列

+

实时数据分析

+

+
    +
  1. 数据可以被立刻查询
  2. +
  3. 使用memory table减少parts数量
  4. +
+
    +
  • 数据先缓存在内存中
  • +
  • 到达一定阈值再写到磁盘
  • +
+

+

复杂类型查询

+
    +
  1. bitmap索引
  2. +
+
    +
  • 构建
  • +
+

+
    +
  • 查询
    +
  • +
+
    +
  1. bitmap64类型
  2. +
+
select countDistinct(uid)
+from user_detial
+where tag_id = 'a' and uid in 
+(
+    select uid from user_detail
+    wherer tag_id = 'b'
+)  
+复制代码
+

+
    +
  1. lowcardinality
  2. +
+
    +
  • 对于低基数列使用字典编码
  • +
  • 减少数据存储和读写的IO使用
  • +
  • 可以做运行时的压缩数据过滤
  • +
+

+

课后

+
    +
  1. 列存和行存的差别是什么,使用场景有什么不同
  2. +
  3. 列存的优点有哪些
  4. +
  5. 列存的缺点有哪些
  6. +
  7. 列存适合什么样的索引
  8. +
  9. ClickHouse的列存是什么样的存储架构
  10. +
  11. ClickHouse的索引是怎么设计的
  12. +
  13. ClickHouse的查询是怎么使用索引的
  14. +
+ + +
+ +
+
+ + + + + + +
+
+
CIickHouse - 你没有见过的列存储
+
https://zhangzhao219.github.io/2023/02/14/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day18/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/14/Interview/Interview-Questions-bigdata/index.html b/2023/02/14/Interview/Interview-Questions-bigdata/index.html new file mode 100644 index 000000000..e0d6b75f3 --- /dev/null +++ b/2023/02/14/Interview/Interview-Questions-bigdata/index.html @@ -0,0 +1,806 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 面试大数据题目准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

面试大数据题目准备

+ + +
+ +

面试大数据题目准备

+ +

解题技巧

+
    +
  1. 哈希函数可以把数据按照种类均匀分流
  2. +
  3. 布隆过滤器用于集合的建立与查询,并可以节省大量空间
  4. +
  5. 一致性哈希解决数据服务器的负载管理问题
  6. +
  7. 利用并查集结构做岛问题的并行计算
  8. +
  9. 位图解决某一范围上数字的出现情况,并可以节省大量空间
  10. +
  11. 利用分段统计思想、并进一步节省大量空间
  12. +
  13. 利用堆、外排序来做多个处理单元的结果合并
  14. +
+

统计40亿个整数中出现次数最多的数,只有1G内存,磁盘无限制

+

分析

+

正常来说,我们一般会想到用哈希表来统计词频,也就是 HashMap<Integer, Integer>,其中,key 表示数字,value 表示这个数字的出现次数。但在 JavaInteger 类型的占用空间是 4 Byte,我们暂时不考虑其他的占用空间,这样一个数的 key + value 的占用空间就是 8 Byte。假设最差情况,40 亿个数字,出现最多次数的数只出现了两次,也就是说我们需要保存 40 亿 - 1 个数,40 亿大约是 2 ^32。所以我们可以计算一下大概要花费的空间:2^32 * 8 Byte = 2^35 Byte = 32 GB。所以说这种方式肯定不符合题目的条件的。

+

思路

+

我们可以使用哈希函数对 40 亿个数字进行分类这种方式 。具体步骤如下:

+

(1)在磁盘上申请 100 个文件(具体申请多少个文件视情况而定)

+

(2)对 40 亿个数依次进行遍历,对遍历到的数字用哈希函数计算出一个哈希值,然后再模除文件的个数,也就是 % 100 ,这样就能均匀的分配到 100 个文件中(相等的数他们的哈希值肯定是一样的,所以一样的数肯定能保存到一个文件中)

+

(3)依次对 100 个文件进行统计,统计每个文件中出现最多的数字,如何统计呢?我们可以使用哈希表进行统计,key 还是数字,value 还是出现次数。正常情况下,文件中的数大概是 40 亿 / 100 个,也就是只占用 32 GB / 100 = 0.32 GB,内存足够。

+

(4)再用一个哈希表保存每个文件出现最多的数字和出现次数(也就是只保存 100 个数)。然后再依次遍历哈希表,找出现次数最大值即可。

+

注意:假设出现最差情况,一个数出现了 40 亿 - 1 次,这意味着一个文件中保持了将近 40 亿个数,也就是 32 GB 左右,但是也没事。我们统计的时候是分批读入数字,并且哈希表肯定不会溢出,因为你保存的本来就是 数字 + 出现次数 ,假设 2 出现了 40 亿次,我们在哈希表中就记录 2 : 40 亿 不就行了吗,也就是只记录一个数就行了。

+
+

假设有40亿个整数,统计0~42亿范围中没出现过的数

+

32位无符号整数的范围是 0~4294967295,现在有一个正好包含40亿个无符号整数的文件, 所以在整个范围中必然存在没出现过的数。 可以使用最多1GB 的内存,怎么找到所有未出现过的数 ?

+

进阶:内存限制为 10MB, 但是只用找到一个没出现过的数即可

+

分析

+

正常来说,我们想到的是范围多大就开多大的数组,只要出现过就在数组对应的下标标记,最后我们遍历数组,没有标记过的位置就是范围内没出现过的数。一个 int 类型在 Java 中是 4 个字节,计算空间占用:42 亿 * 4 Byte,大概为 16 GB,不符合题意。

+

思路

+

我们可以使用位图的方式来做,也就是 bitMap。具体步骤如下:

+

(1)我们用 1 bit 来表示数字是否出现过,也就是说我们可以开 int[ 42 亿 / 8] 这么大的数组(一个 Byte = 8 bit),这里的 42 亿是范围,其实意思就是说我们的思路没有变化,只是换了一种数据的存储方式。空间占用:42 亿 Byte / 8,大概是 500 多 MB,满足条件。

+

(2)然后遍历这 40 亿个数,依次在对应的 bit 位上标为 1。

+

(3)最后统计我们开的数组,bit 位上为 0 的就是没出现过的。

+

进阶

+

我们可以使用范围划分的方式 。具体步骤如下:

+

(1)10 MB 大概可以申请 int[2621440] 这么多数组空间,计算方式:10 MB / 4Byte = 2621440

+

(2)划分份数:42 亿 / 2621440 大概等于 1638,假设这个数组名为 arr,arr[0] 就代表 0 ~ 1637 这个范围出现的数字的个数,同理 arr[1] 就代表 1638~ (1638+ 1638- 1) 这个范围出现的数字的个数。也就是类似于词频统计。

+

(3)依次遍历这 40 亿个数,arr[每个数 / 1638]++,也就是说 0 就是 arr[0]++,1638就是 arr[1]++,其实思路就是把范围划分成 1638份,每份统计对应的数字个数,如果对应的数字个数小于 1638,说明在这个范围内肯定有数字没出现过

+

(4)重复此过程,将范围逐渐缩小即可求出答案。

+

注意:这里是因为能使用的空间比较大,所以只需一次划分即可。

+

再进阶

+

只能使用有限几个变量,如何做

+

二分法,一直二分下去。

+

有一个包含100亿个URL的大文件,每个URL占用64B,找出所有重复的URL

+

思路

+

第一种办法:使用哈希函数进行分类。

+

(1)申请 N 个磁盘文件

+

(2)遍历所有的 URL,为每个 URL 计算哈希值,然后再模除 N,就能均匀地分类到对应的文件中,重复的 URL 一定会在同一个文件中。

+

(3)依次统计每一个文件中重复的 URL,然后再汇总各个文件的信息即可。

+

第二种办法:使用布隆过滤器。会有失误率,不过概率很低。

+

(1)首先申请一个足够大的 bitMap,便建立边检查是否重复

+

(2)遍历所有的 URL,遍历到一个 URL,就通过某种哈希函数计算它的哈希值,然后再模除 bitMap 的位数。

+

(3)如果发现对应的槽位已经为 1 (初始都是为 0,一个 bit 位就两种状态,0 和 1),就说明这个 URL 是重复的。

+

(4)如果发现对应的槽位为 0,说明不重复,将槽位置为 1。

+

可以发现,如果重复的 URL 一定能找出来,但是可能不重复的 URL 也被找出来了(这也就是失误率,因为哈希冲突),如果可以允许有一定的失误率就可以使用这种办法。

+

百亿数据量中找到热门 Top 100

+

思路

+

使用分类 + 大根堆方式

+

(1)通过哈希函数将海量数据分类到一个个小文件中

+

(2)依次对每个小文件建立大根堆,规则就是 URL 出现次数最多的就在大根堆的顶部

+

(3)取每个小文件出现次数最多的 URL,也就是刚刚建立的大根堆顶部元素,再建立一个大根堆

+

(4)这样建立出来的大根堆就是 Top 100 了

+

40亿个数中只出现了两次的数

+

32位无符号整数的范围是0~4294967295,现在有40亿个无符号整数,可以使用最多1GB的内存,找出所有出现了两次的数。

+

思路

+

第一种方式:哈希函数。

+

(1)通过哈希函数对 40 亿个数进行分流,分到一个个小文件中。

+

(2)依次用哈希表统计每个小文件中只出现了两次的数。

+

(3)然后再汇总各个文件中只出现了两次的数即可。

+

第二种方式:bitMap

+

(1)正常来说我们都是用 1 bit 来表示对应的数字是否出现过,对应这个题目,我们可以变化一下。

+

(2)用 2 bit 来表示对应的数字,00 代表这个数没有出现过,01 代表这个数出现过一次,10 代表这个数出现过两次,11 代表这个数出现了两次以上。

+

(3)容量大概使用:40 亿大概就是 2 ^32^ ,所以就是 2^32^ * 2 bit / 8 ,大概就是 1 GB 左右。

+

10G的文件,文件中每个数都是无符号整数,是无序的,只给5G内存,输出一个新文件,使其变成有序的

+

小根堆方式。

+ + +
+ +
+
+ + + + + + +
+
+
面试大数据题目准备
+
https://zhangzhao219.github.io/2023/02/14/Interview/Interview-Questions-bigdata/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/19/diary/Happy-Birthday-2023/index.html b/2023/02/19/diary/Happy-Birthday-2023/index.html new file mode 100644 index 000000000..f2dbff47f --- /dev/null +++ b/2023/02/19/diary/Happy-Birthday-2023/index.html @@ -0,0 +1,748 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 23岁的自己,生日快乐! - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

23岁的自己,生日快乐!

+ + +
+ +

23岁的自己,生日快乐!

+ +

也许今天你很迷茫,不知道应该做一些什么事情

+

也许今天你很失落,努力了两周的结果是从头再来

+

也许今天你很懊恼,后悔自己之前的选择不够合适

+

也许今天你很伤心,并不会有人记得你的生日

+

但是今天是你的生日呀

+

在这个并不算很特殊的日子里,也值得你对自己说一声

+

张兆,生日快乐!

+ + +
+ +
+
+ + + + + + +
+
+
23岁的自己,生日快乐!
+
https://zhangzhao219.github.io/2023/02/19/diary/Happy-Birthday-2023/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月19日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/21/Interview/Interview-Internship-Experience-Backend/index.html b/2023/02/21/Interview/Interview-Internship-Experience-Backend/index.html new file mode 100644 index 000000000..fc63066ca --- /dev/null +++ b/2023/02/21/Interview/Interview-Internship-Experience-Backend/index.html @@ -0,0 +1,1188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 后端实习面试经历 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

后端实习面试经历

+ + +
+ +

后端实习面试经历

+ +

前言

+

第一次自己找实习,借助各公司官网和第三方APP,在形式不好的情况下最终上岸MetaApp,公司虽然不大,但是岗位很符合预期。

+

这样我就有了一次算法的实习经历,研二前加上一次开发的实习经历,之后的选择应该会更多了吧

+

第一阶段(2022年12月-2023年1月)

+

开始的时候还是低估了互联网寒冬,想当初在商汤算法岗实习的时候,一半以上的人都是实习生,篮球的组也基本都是一两个研究员在指导很多实习生。

+

因此当时感觉不会很难,而且有过算法实习和一般的基础知识+好学历应该会比较简单(好天真…)

+

第一阶段主要聚焦在大中厂,在boss和实习僧上沟通了滴滴、VMWare、小红书、小米、bilibili,再加上一个量化的岗位,只有小红书得到了一面的机会。

+

总体面试考的题目都比较基础,但是算法题是没见过的“搜索二叉树转双向链表”,没有意识到题目的含义是更改指针的指向。

+

总之一面之后就没有消息了。

+

这里开始有一点点担心了,但是随后的期末考试也没有时间去想,我自己还是不能完全放下期末考试。

+

第二阶段(2023年1月-2023年2月)

+

期末考试结束后,参加字节跳动青训营,做了一个抖音的项目,简单完善了一下简历,继续投递

+

这回就不光盯着头部大厂了,只要是评价还可以的公司就直接投,一个公司也不仅仅投递一个岗位

+

日程大概如下(主要是有回应的):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
时间
2023.02.06滴滴投递1Momenta投递图森未来投递
2023.02.07滴滴简历挂Momenta转岗百度投递1
2023.02.08好未来投递Momenta简历挂百度一面通知
2023.02.09SmartX一面通知MetaApp笔试百度一面
2023.02.10滴滴投递2MetaApp一面通知
2023.02.13蔚来投递MetaApp一面
2023.02.14SmartX一面MetaApp二面通知百度一面挂
2023.02.15SmartX感谢信MetaApp二面
2023.02.16
2023.02.17第四范式一面通知图森未来感谢信
2023.02.20MetaApp Offer
+

总的来说面试机会还是太少,有多方面的原因:

+
    +
  • 简历没有技能模块,比较吃亏
  • +
  • 学校还是不够强
  • +
  • 面试太少,经验不足
  • +
  • 岗位招聘实习生比较少
  • +
+

感想

+

感觉一切事情都是有关联的,而且都是一个接着一个,很少有空闲时间,也没有很多事情冲突的时间

+

12月看到了字节跳动青训营的通知,期末考试结束当天正好是青训营的笔试

+

12月底从北大学长那里搞了一台免费的华为云一年的服务器,然后青训营正好使用上

+

青训营需要下载飞书,然后入职后的工作软件也正是飞书

+

开学前两个星期面试,正好在开学后面试完成,不用到处找空教室面试(也找不到)

+

有一种感觉,所有的事情都是有人安排好的,虽然这些事情之间并没有太多的关联,但是在我自己的视角看来就是一个接着另外一个安排好的

+

我也不知道这对我来说是好是坏,是幸运还是不幸

+

考到的算法题目

+

LRU(实习-MetaAPP-Go后端开发-2023.02.15)

+
package main
+
+import "fmt"
+import "container/list"
+
+type node struct{
+	key int
+	value int 
+}
+
+type Cache struct{
+	cache map[int]*list.Element
+	nodelist *list.List
+}
+
+func NewCache() *Cache{
+	return &Cache{
+		cache : make(map[int]*list.Element,0),
+		nodelist : list.New(),
+	}
+}
+
+func (c *Cache) put(k,v int){
+	n,ok := c.cache[k]
+	if !ok{
+		c.cache[k] = c.nodelist.PushFront(&node{k,v})
+	} else{
+		n.Value.(*node).value = v
+		c.nodelist.MoveToFront(n)
+	}
+}
+
+func (c *Cache) get(k int){
+	n,ok := c.cache[k]
+	if !ok{
+		fmt.Println("-1")
+	} else{
+		c.nodelist.MoveToFront(n)
+		fmt.Println(n.Value.(*node).value)
+	}
+}
+
+func main() {
+	c := NewCache()
+	c.put(1,2)
+	c.put(2,3)
+	c.put(1,4)
+	var key int
+	for i:=0;i<3;i++{
+		fmt.Scanf("%d",&key)
+		c.get(key)
+	}
+}
+

Go多线程

+
package main
+
+import "fmt"
+
+func putNum(n int, putNumChan chan int) {
+	for i := 1; i <= 2000; i++ {
+		putNumChan <- i
+	}
+	close(putNumChan)
+}
+
+func dealNum(x int) int {
+	res := 0
+	for i := 1; i <= x; i++ {
+		res += i
+	}
+	return res
+}
+
+func add(putNumChan chan int, resChan chan map[int]int, exitChan chan bool) {
+	for {
+		x, ok := <-putNumChan
+		if !ok {
+			exitChan <- true
+			return
+		}
+		a := dealNum(x)
+		m := make(map[int]int, 0)
+		m[x] = a
+		resChan <- m
+	}
+}
+
+func output(resChan chan map[int]int, exitChan chan bool) {
+	for i := 0; i < 8; i++ {
+		<-exitChan
+	}
+	close(exitChan)
+	close(resChan)
+}
+
+func main() {
+	n := 2000
+	putNumChan := make(chan int)
+	go putNum(n, putNumChan)
+	resChan := make(chan map[int]int)
+	exitChan := make(chan bool)
+	for i := 0; i < 8; i++ {
+		go add(putNumChan, resChan, exitChan)
+	}
+	go output(resChan, exitChan)
+	for v := range resChan {
+		fmt.Println(v)
+	}
+}
+
+

删除链表倒数第n个节点(实习-Momenta-C++开发-2022.12.02)

+

https://leetcode.cn/problems/remove-nth-node-from-end-of-list/

+
func removeNthFromEnd(head *ListNode, n int) *ListNode {
+	var dummy = &ListNode{}
+	dummy.Next = head
+
+	count := 0
+	for head != nil {
+		count += 1
+		head = head.Next
+	}
+	head = dummy.Next
+	pre := dummy
+	count -= n
+	for count != 0 {
+		pre = head
+		count -= 1
+		head = head.Next
+	}
+	pre.Next = head.Next
+	return dummy.Next
+}
+

反转链表(实习-MetaAPP-Go后端开发-2023.02.15)

+

https://leetcode.cn/problems/reverse-linked-list/

+
func reverseList(head *ListNode) *ListNode {
+	if head == nil {
+		return head
+	}
+	dummy := &ListNode{}
+	dummy.Next = head
+	post := head.Next
+	head.Next = nil
+	for post != nil {
+		dummy.Next = post
+		post = post.Next
+		dummy.Next.Next = head
+		head = dummy.Next
+	}
+	return dummy.Next
+}
+

搜索二叉树转双向链表(2022.12.26 小红书 Golang开发实习生)

+
package main
+
+import "fmt"
+import "container/list"
+
+type TreeNode struct{
+	Val int 
+	Left *TreeNode
+	Right *TreeNode
+}
+
+func convert(head *TreeNode, listhead *list.List){
+	if head == nil{
+		return 
+	}
+	convert(head.Left,listhead)
+	listhead.PushBack(head.Val)
+	fmt.Println(head.Val)
+	convert(head.Right,listhead)
+}
+
+func main() {
+	testlist := list.New()
+	head := &TreeNode{
+		Val:2,
+		Left:nil,
+		Right:nil,
+	}
+	head.Left = &TreeNode{
+		Val:1,
+		Left:nil,
+		Right:nil,
+	}
+	head.Right = &TreeNode{
+		Val:3,
+		Left:nil,
+		Right:nil,
+	}
+	convert(head,testlist)
+}
+

并查集(实习-百度-Go后端开发-2023.02.09)

+
/**
+  编程:输入一多行数据,每一行代表两个数有关系,将有关系的数在同一行输出。
+  输入:
+  1    2
+  3    4
+  1    3
+  5    6
+  5    7
+  输出(不要求有序):
+  1 2 3 4
+  5 6 7
+*/
+
+package main
+
+import "fmt"
+
+type UFS struct {
+	father []int
+}
+
+func NewUFS(size int) *UFS {
+	return &UFS{
+		father: make([]int, size),
+	}
+}
+
+func (u *UFS) Init() {
+	for i := 0; i < len(u.father); i++ {
+		u.father[i] = i
+	}
+}
+
+func (u *UFS) FindFather(x int) int {
+	a := x
+	for x != u.father[x] {
+		x = u.father[x]
+	}
+	for a != u.father[a] {
+		z := a
+		a = u.father[a]
+		u.father[z] = x
+	}
+	return x
+}
+
+func (u *UFS) Union(a, b int) {
+	faA := u.FindFather(a)
+	faB := u.FindFather(b)
+	if faA != faB {
+		u.father[faA] = faB
+	}
+}
+
+func (u *UFS) Print() {
+	fathermap := make(map[int][]int, 0)
+	for i := 0; i < len(u.father); i++ {
+		father := u.FindFather(i)
+		_, ok := fathermap[father]
+		if ok {
+			fathermap[father] = append(fathermap[father], i)
+		} else {
+			fathermap[father] = make([]int, 1)
+			fathermap[father][0] = i
+		}
+	}
+	for k, v := range fathermap {
+		fmt.Println(k, v)
+	}
+}
+
+func main() {
+	u := NewUFS(10)
+	u.Init()
+	u.Union(0, 1)
+	u.Union(2, 3)
+	u.Union(3, 5)
+	u.Union(7, 5)
+	u.Union(4, 6)
+	u.Print()
+}
+
+

全排列(实习-MetaAPP-Go后端开发-2023.02.13)

+
package main
+
+import "fmt"
+
+// 没有重复元素的数组,打印全排列组合
+
+var arr []int
+var vis []bool
+
+func Printarr(temp []int) {
+	for i := 0; i < len(temp); i++ {
+		fmt.Printf("%d ", temp[i])
+	}
+	fmt.Println()
+}
+
+func backtracking(arr []int, temp []int, start int) {
+	if start == len(arr) {
+		Printarr(temp)
+	}
+	for i := 0; i < len(arr); i++ {
+		if !vis[i] {
+			vis[i] = true
+			temp = append(temp, arr[i])
+			backtracking(arr, temp, start+1)
+			temp = temp[:len(temp)-1]
+			vis[i] = false
+		}
+	}
+}
+
+func main() {
+	arr = []int{1, 2, 3}
+	temp := make([]int, 0)
+	vis = make([]bool, len(arr))
+	backtracking(arr, temp, 0)
+}
+

比较谁先结束(实习-SmartX-Go后端开发-2023.02.14)

+
package main
+
+// 1. 只能编辑 foo 函数
+// 2. foo 必须要调用 slow 函数
+// 3. foo 函数在 ctx 超时后必须立刻返回
+// 4. 如果 slow 结束的比 ctx 快,也立刻返回
+
+import (
+	"context"
+	"fmt"
+	"math/rand"
+	"time"
+)
+
+func main() {
+	rand.Seed(time.Now().UnixNano())
+	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(ctx, time.Second)
+	defer cancel()
+
+	foo(ctx)
+}
+
+func foo(ctx context.Context) {
+	go func() {
+		slow()
+	}()
+	<-ctx.Done()
+}
+
+func slow() {
+	n := rand.Intn(3)
+	fmt.Printf("sleep %ds\n", n)
+	time.Sleep(time.Duration(n) * time.Second)
+}
+
+ + +
+ +
+
+ + + + + + +
+
+
后端实习面试经历
+
https://zhangzhao219.github.io/2023/02/21/Interview/Interview-Internship-Experience-Backend/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月21日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/21/zhangzhao-plan-2/index.html b/2023/02/21/zhangzhao-plan-2/index.html new file mode 100644 index 000000000..59fdece0b --- /dev/null +++ b/2023/02/21/zhangzhao-plan-2/index.html @@ -0,0 +1,1814 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 学习计划(2023年2月——2023年7月) - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

学习计划(2023年2月——2023年7月)

+ + +
+ +

学习计划

+ +

2023年02月——2023年07月

+

总计划

+
+

1. 后端实习(2023年02月-2023年07月)#6f42c1

+
+
+

2. 后端知识学习,记录技术总结笔记(2023年02月-2023年07月)#777

+
+
+

3. 算法题目重做(2023年05月-2022年07月)#5cb85c

+
+
+

4. 抖音项目完善(2023年02月-2023年03月)#d9534f

+
+
+

5. 抖音前端找demo修改实现(2023年03月-2023年04月)#f0ad4e

+
+
+

6. 找合适的后端项目并开发(2023年05月-2023年07月)#428bca

+
+
+

7. 开源初步尝试(2023年02月-2023年07月)#3c4858

+
+
+

8. 其他计划(2023年02月-2023年07月)#ffc0cb

+
+

具体计划及实施

+

2023年02月22日-2023年03月03日

+

计划安排

+
+

1. 后端实习,尽快熟悉上手

+
+
+

2. 后端知识学习,主要是部门的技术,不仅要会用,更要多看底层原理,多记录笔记

+
+
+

3. 抖音项目完善,先把两位队友的代码看懂,找一找优化方向

+
+
+

4. 开源初步尝试,了解开源组织贡献方法等(如GSOC等)

+
+

实施情况

+
    +
  • 02.22 +
      +
    • 编写抢课脚本
    • +
    • 研究docker,基本功能已经可以跑通
    • +
    • 打球,准备实习用品
    • +
    +
  • +
  • 02.23 +
      +
    • 第一天实习,谈判失败,需要租房
    • +
    • 搭建台式机的开发环境
    • +
    +
  • +
  • 02.24 +
      +
    • 完成分论的论文
    • +
    • Hertz联合thrift跑一个demo
    • +
    +
  • +
  • 02.25 +
      +
    • 租房
    • +
    • 工位走线,安装软件等
    • +
    • 收拾东西
    • +
    +
  • +
  • 02.26 +
      +
    • 农大一日游
    • +
    +
  • +
  • 02.27 +
      +
    • 完成go环境的配置
    • +
    • 拿到所有代码的权限
    • +
    • 代码的框架大致看懂
    • +
    • 完成大部分英语慕课视频任务
    • +
    +
  • +
  • 02.28 +
      +
    • 项目的流程基本看懂,细节和结构体的含义还要继续看
    • +
    • 完成英语慕课视频任务,复制英语慕课题目相关课文
    • +
    +
  • +
  • 03.01 +
      +
    • 看完一半多的step的具体细节,redis相关还不怎么熟悉,其他还好
    • +
    • 完成英语慕课的一个阅读题目
    • +
    +
  • +
  • 03.02 +
      +
    • 看完全部step的具体细节,研究Prometheus和Thrift
    • +
    • 完成英语慕课的第一个Quiz
    • +
    +
  • +
  • 03.03 +
      +
    • 再次捋一遍全部的step,item和adsitem的区别也弄懂了
    • +
    • 研究Kafka和Thrift并跑通示例demo
    • +
    • 团建
    • +
    +
  • +
+

月度完成情况总结

+
    +
  1. 学校课程相关:完成分论的论文,英语慕课跟上进度,编写了选课脚本并确定选课,没有遇到导师的干扰
  2. +
  3. 生活相关:租房,收拾东西搬入出租屋,合租体验还可以
  4. +
  5. 实习相关:搭建好了实习需要的物理环境和代码运行环境,项目的流程和代码基本弄懂,学习Prometheus、Thrift、Kafka等新工具的使用
  6. +
  7. 娱乐相关:打球、团建、农大一日游
  8. +
  9. 其他:跑通docker的基本功能并在公司的电脑上搭建好docker环境,青训营答辩结束,等待成绩中
  10. +
+

2023年03月04日-2023年03月31日

+

计划安排

+
+

1. 后端实习,尽快熟悉上手

+
+
+

2. 后端知识学习,主要是部门的技术,不仅要会用,更要多看底层原理,多记录笔记

+
+
+

3. 抖音项目完善,先把两位队友的代码看懂,找一找优化方向,整理青训营的资料,找一个比较完备的前端准备开始优化项目

+
+
+

4. 开源初步尝试,了解开源组织贡献方法等(如GSOC等)

+
+

实施情况

+
2023年03月04日-2022年03月10日
+
    +
  • 03.04 +
      +
    • 智能计算系统第二章作业完成,尝试三四五章的作业
    • +
    • 整理一周的学习笔记到博客上
    • +
    • 回学校
    • +
    • 听青训营项目的答辩
    • +
    +
  • +
  • 03.05 +
      +
    • 学校图书馆再次对代码进行复盘
    • +
    • 回出租屋,休息
    • +
    +
  • +
  • 03.06 +
      +
    • 串讲完成,细节获得了认可,但是业务还需要多熟悉
    • +
    • 开发第一个小需求,基本就是抄代码,基本完成等待资源进行测试
    • +
    • 研究Goland环境,基本弄明白了
    • +
    +
  • +
  • 03.07 +
      +
    • 放弃WSL的Go环境,在Windows下面安装,配合GoLand使用起来比较顺手
    • +
    • 看懂第一个需求的上下文并开发完成,申请各种权限并体验使用
    • +
    • 整理汇总青训营大项目的资料
    • +
    +
  • +
  • 03.08 +
      +
    • 第一个需求开发测试完成,预发版也没有问题,等待合并
    • +
    • 学会了整个的开发流程
    • +
    +
  • +
  • 03.09 +
      +
    • 根据元铭的建议修改代码,修改bug且使得代码结构变量等更为清晰合理,学到了很多
    • +
    • 完成“打压”的小需求并在pre环境测试通过
    • +
    • 学习user_feature服务的代码逻辑
    • +
    • 上线user_feature_upgrade的需求并开AB实验
    • +
    +
  • +
  • 03.10 +
      +
    • 修改“打压”的小需求的具体代码并正式上线,学习开启潘多拉的看板
    • +
    • 看user_feature服务与redis存储相关的代码,尝试寻找优化点
    • +
    • 深入学习反射相关内容
    • +
    +
  • +
+

周总结:

+
    +
  1. 学校课程相关:智能计算系统课程作业搞懂,基本慢慢抄就可以,英语慕课跟上了进度
  2. +
  3. 实习相关:串讲还可以,但是可能业务还是不是很熟悉,完成了两个小需求,Mentor对代码质量的要求比较高,以后写代码的时候要多注意。熟悉了整个的开发的流程
  4. +
  5. 学习反射相关的内容,感觉闲着的时候不知道应该干什么,要好好想想给自己找些事情做
  6. +
+
2023年03月11日-2022年03月17日
+
    +
  • 03.11 +
      +
    • 生病,剪头,休息
    • +
    +
  • +
  • 03.12 +
      +
    • 看通论相关的内容,大致思考PPT的制作
    • +
    • 休息
    • +
    +
  • +
  • 03.13 +
      +
    • 完成英语慕课视频及习题
    • +
    • 看优惠券相关代码
    • +
    • 开发优化消息队列相关需求
    • +
    +
  • +
  • 03.14 +
      +
    • 减少最终可以返回的最多的广告数量,从而控制MQ的消息大小,开发测试完成
    • +
    • 查看MQ消费消息的下游服务,查看哪些结构体字段是不需要的,并测试实现
    • +
    +
  • +
  • 03.15 +
      +
    • 通过查看下游服务,判断哪些结构体字段是下游服务不需要的,直接传递空值过去节省MQ资源
    • +
    • 了解泛型相关知识
    • +
    • http错误处理开发完成,未测试
    • +
    • 继续阅读coupon_rec_sort的代码并作笔记
    • +
    +
  • +
  • 03.16 +
      +
    • 节约MQ与http错误在灰度上进行实验,没有报错,10条广告可以将大小降低40%左右,32条广告可以将大小降低60%左右
    • +
    • 继续阅读coupon_rec_sort的代码并作笔记
    • +
    • 看trace_log相关的部分,基本弄懂且没有找到错误,但是错误确实存在,还需要分析
    • +
    +
  • +
  • 03.17 +
      +
    • 回学校,回家
    • +
    +
  • +
+

周总结:

+
    +
  1. 甲流或者新冠,虽然发烧只烧了一天,但是出租屋空调比较干,难受了比较长的一段时间,现在也还咳嗽,对正常生活造成了一些影响
  2. +
  3. 学校完成了英语慕课和习题,接到了通论的任务并分配下去
  4. +
  5. 完成实习的任务
  6. +
+
2023年03月18日-2022年03月24日
+
    +
  • 03.18 +
      +
    • 完成通论的PPT
    • +
    • 休息
    • +
    +
  • +
  • 03.19 +
      +
    • 回出租屋
    • +
    • 记日记
    • +
    +
  • +
  • 03.20 +
      +
    • 找到tracer_log的bug并修改完成,效果显著
    • +
    +
  • +
  • 03.21 +
      +
    • 接手从头开始写的服务和Hertz的优化方向
    • +
    +
  • +
  • 03.22 +
      +
    • 完成部分广告走推荐自动化配置的需求的主体代码并在本地测试
    • +
    • ads_sort上游服务修改
    • +
    • 将Nacos迁移到Hertz的Hook中进行实验
    • +
    +
  • +
  • 03.23 +
      +
    • rec_sort上游服务修改
    • +
    • 学习Hertz优雅退出的代码
    • +
    +
  • +
  • 03.24 +
      +
    • new_game_sort上游服务修改
    • +
    • 学习Hertz接收信号退出的代码并测试,同时了解Linux信号的知识
    • +
    • 搭建Nacos服务并跑通Hertz集成的代码
    • +
    • 删除ads_sort之前的实验代码并上线
    • +
    • 提交完成第3章的智能计算系统实验
    • +
    +
  • +
+

周总结:

+
    +
  1. 学校完成英语慕课、通论PPT和智能计算系统的实验
  2. +
  3. 实习 +
      +
    1. 接了一个从头开始写的服务,全部写完并修改3个上游的服务,等待联调
    2. +
    3. 删除之前的工作的实验代码并正式上线
    4. +
    5. 开始研究Hertz的代码,重点放在优雅上线和优雅退出的部分,源代码也基本看懂
    6. +
    +
  4. +
+
2023年03月25日-2022年03月31日
+
    +
  • 03.25 +
      +
    • 整理智能计算系统后续实验,基本都看不太懂,后面要慢慢学习
    • +
    • 接姥姥,转北京
    • +
    +
  • +
  • 03.26 +
      +
    • 转北京
    • +
    • 完成分论和通论的论文
    • +
    • 记日记
    • +
    +
  • +
  • 03.27 +
      +
    • 继续看Hertz的新框架,实现了优雅退出的功能,正在看服务发现和注册的内容
    • +
    • 和后端联调,太慢了
    • +
    +
  • +
  • 03.28 +
      +
    • 在user feature上实现了Hertz在K8s上的优雅退出
    • +
    • 完成ads_sdk的两个小需求:按照条件过滤和与ads_sort一样减小消息结构体
    • +
    • 继续阅读Nacos的服务发现和注册的部分
    • +
    +
  • +
  • 03.29 +
      +
    • 部分广告走推荐自动化配置 服务上线,但是需求没清晰,需要添加功能
    • +
    • 修改之前ads_sort小结构体的bug并上线
    • +
    • 更改rec_sort服务并增加监听
    • +
    • 编写脚本,迁移前面的Redis数据到新的Redis中
    • +
    +
  • +
  • 03.30 +
      +
    • 对接并编写部分广告走推荐自动化配置的修改操作
    • +
    • rec_sort上线有两个bug,第一个是redis的监听的问题,第二个是并发的map读写问题
    • +
    • 预编译语句查数据库有bug,需要将holo的$符号更改为?的符号
    • +
    +
  • +
  • 03.31 +
      +
    • 部分广告走推荐自动化配置的服务并debug,已经可以跑通,但是数据库的部分需要修改
    • +
    +
  • +
+

周总结:

+
    +
  1. 实习任务比较多,怎么才能写好有一些难度,学到了一些知识
  2. +
  3. 带着姥姥转北京,人太多了体验不太好
  4. +
  5. 完成一些作业
  6. +
+

月度完成情况总结

+

基本就是做作业,完成实习任务,上手还是有点慢,没学习什么其他的知识,感觉时间不够而且晚上回家什么都不想干。时间还是要挤的,还是要多学一些其他的东西。

+

2023年04月01日-2023年04月30日

+

计划安排

+
+

1. 后端实习,可以用chatgpt等工具辅助阅读代码,效率要提上来

+
+
+

2. 后端知识学习,在实际项目中体验新技术的使用,同时关注学习底层原理

+
+
+

3. 抖音项目完善,先把两位队友的代码看懂,找一找优化方向,用Hertz框架进行重写,整理青训营的资料,找一个比较完备的前端准备开始优化

+
+
+

4. 开源初步尝试,多关注关注群消息和一些时间节点等

+
+
+

5. 完成学校大作业等杂事

+
+

实施情况

+
2023年04月01日-2022年04月07日
+
    +
  • 04.01 +
      +
    • 完成SQL代码的优化,同时简化逻辑
    • +
    • 在本地windows上使用docker搭建redis服务,测试通过
    • +
    • 讨论文本数据挖掘和情感计算的大作业的完成方式
    • +
    +
  • +
  • 04.02 +
      +
    • 找大作业的文本情感相关的数据集
    • +
    • 在服务器上使用docker搭建mysql和redis
    • +
    • 完成通论的个人报告,与小组的PPT一起提交
    • +
    +
  • +
  • 04.03 +
      +
    • 完成config服务修改的需求,与运营等对接,应该没有问题了
    • +
    • ads_sdk服务修改上线测试
    • +
    • 文本情感分类的基本代码抄完跑通,尽快完善做完
    • +
    +
  • +
  • 04.04 +
      +
    • 开减小消息体的实验,对自动化配置的需求
    • +
    • 准备将beego框架迁移到hertz
    • +
    • 继续做情感分类
    • +
    +
  • +
  • 04.05 +
      +
    • 情感分类自己负责的部分基本写完并跑通,创建实验报告的文档
    • +
    • 休息
    • +
    +
  • +
  • 04.06 +
      +
    • 完成框架迁移的讨论和实际实现,测试配置服务
    • +
    • 通过docker安装nacos、Promethus和Grafana
    • +
    +
  • +
  • 04.07 +
      +
    • 完成框架迁移的测试并在灰度上线,完成配置服务的上线
    • +
    • 通过docker部署flask服务,完成音频部分的处理和展示
    • +
    +
  • +
+

周总结:

+
    +
  • config服务进行SQL代码的优化,测试后成功上线
  • +
  • 更改ads_sdk的两个小需求并做实验,将ads_sdk的框架从beego迁移到Hertz上并在灰度进行测试
  • +
  • 讨论文本数据挖掘和情感计算的大作业的完成方式、找大作业的文本情感相关的数据集、完成自己负责的情感分类的部分,创建实验报告的文档
  • +
  • 完成通论的个人报告,与小组的PPT一起提交
  • +
  • 使用docker搭建mysql、redis、nacos、Promethus和Grafana,同时通过docker提供服务的方式搭建Flask服务做了文本数据挖掘的一个简单的前后端
  • +
+
2023年04月08日-2022年04月14日
+
    +
  • 04.08 +
      +
    • 测试大作业的前后端交互,已经跑通提供服务
    • +
    • 完成智能计算系统第4章实验
    • +
    +
  • +
  • 04.09 +
      +
    • 完成智能计算系统第5章实验
    • +
    • ms找到了一个很好的bert,转换参数也还可以,有一些问题需要解决
    • +
    +
  • +
  • 04.10 +
      +
    • 完成ads_sort框架迁移的代码编写,还没有测试
    • +
    • 完成三个定时任务的编写并测试完成,Redis中以后应该没有冗余数据了
    • +
    • 完成英语慕课及题目
    • +
    +
  • +
  • 04.11 +
      +
    • 不断修改定时任务的代码使其更为规范
    • +
    • ads_sdk上线,没什么问题
    • +
    • 完成ms与pytorch模型参数的对应工作,大作业的全流程跑通
    • +
    +
  • +
  • 04.12 +
      +
    • 阅读Hertz框架启动的源码
    • +
    • 将ads_sort迁移到Hertz框架上
    • +
    • 完成代码的合并,已经可以流畅跑通整个流程
    • +
    +
  • +
  • 04.13 +
      +
    • 完成config的代码第一次优化
    • +
    • ads_sort发灰度
    • +
    • 完成智能计算系统第七章实验
    • +
    +
  • +
  • 04.14 +
      +
    • ads_sort灰度确认没问题
    • +
    • 完成config的代码第二次优化
    • +
    • 接手更改泛型的任务
    • +
    +
  • +
+

周总结:

+
    +
  1. 作业:完成大作业的基本全部内容,最终的结果也都基本跑通,同时完成智能计算系统的四、五、七章作业,完成英语慕课题目同时总结词汇题目
  2. +
  3. 实习写了一些定时任务,迁移了两个框架,mentor对代码质量的要求比较高,下周准备接手目前的难点,优化泛型
  4. +
  5. docker现在基本使用没有问题,后续再测试一下准备全量换docker
  6. +
+
2023年04月15日-2022年04月22日
+
    +
  • 04.15 +
      +
    • 编写情感计算算法部分的文档
    • +
    • 将情感识别的系统端到端跑通,并提取测试样例
    • +
    • 英语慕课词汇题目整理
    • +
    • 打乒乓球,休息
    • +
    +
  • +
  • 04.16 +
      +
    • 完成情感计算的绝大部分文档,同时加入系统部分的内容
    • +
    • 准备文本数据挖掘文档的模板
    • +
    • 英语慕课词汇题目整理
    • +
    +
  • +
  • 04.17 +
      +
    • 看懂泛型和修改思路
    • +
    • rec_ads_config服务修改代码后上线
    • +
    +
  • +
  • 04.18 +
      +
    • 修改泛型完成,在预发上测试没问题
    • +
    • 完成大作业的数据填充和一部分文字
    • +
    +
  • +
  • 04.19 +
      +
    • 泛型测试+debug
    • +
    • 完成情感计算的文档,完善文本数据挖掘的文档
    • +
    +
  • +
  • 04.20 +
      +
    • 泛型debug完成
    • +
    • 完成文本数据挖掘的文档
    • +
    +
  • +
  • 04.21 +
      +
    • ads_sdk优化代码,准备周日上线
    • +
    • 开始优化ads_sort的代码
    • +
    • 完成智能计算系统第6章实验
    • +
    +
  • +
  • 04.22 +
      +
    • 整理英语慕课题目
    • +
    • 看电影,休息
    • +
    +
  • +
+

周总结:

+
    +
  1. 作业:提前一周完成大作业,包括代码与文档,目前应该就没有什么事情了,补了智能计算系统的一个实验,也是分分钟抄完拿下,完成英语慕课的题目
  2. +
  3. 实习主要是读懂泛型和修改服务到泛型的库上面,感觉做的东西不多,debug用了很长时间
  4. +
+
2023年04月23日-2022年04月30日
+
    +
  • 04.23 +
      +
    • 更改ads_sort泛型,预发上测试没有报错
    • +
    • 完善智能计算系统的作业并提交
    • +
    +
  • +
  • 04.24 +
      +
    • 完成英语慕课全部内容
    • +
    • 完成go dockerfile的探索
    • +
    +
  • +
  • 04.25 +
      +
    • ads_sort灰度测试,修了一个小bug,其他没什么问题
    • +
    • 将pulsar迁移到ads_sort上,已经开发完,有一个小bug需要看原理修一下
    • +
    • VSCode docker测试通过,编写批量下载gitlab的脚本
    • +
    +
  • +
  • 04.26 +
      +
    • ads_sdk开实验测试是否在cacheBid前添加过滤条件的效果
    • +
    • ads_sdk泛型全量,没有报错
    • +
    +
  • +
  • 04.27 +
      +
    • 减少Redis存储的两个实验均全量
    • +
    • 针对ads_sdk调用user feature超时的问题,更改ads_sdk的user feature调用为单独的http连接,效果不算显著
    • +
    • 测试ads_sdk全量后的时间、cpu等的效果,优化有一定的效果
    • +
    • 整理代码仓库,边缘仓库已经都整理完
    • +
    +
  • +
  • 04.28 +
      +
    • 整理全部的代码仓库
    • +
    • 看DDIA
    • +
    • 整理智能计算系统的实验
    • +
    +
  • +
  • 04.29 +
      +
    • 视频、逛超市、聚餐、休息
    • +
    +
  • +
  • 04.30 +
      +
    • 看两个课程的PPT
    • +
    +
  • +
+

周总结:

+
    +
  1. 实习,完成全部任务
  2. +
  3. docker copy代码准备完成,开始浏览学期学过的内容
  4. +
+

月度完成情况总结

+
    +
  1. 除智能计算系统的报告外,完成学校的全部作业
  2. +
  3. 完成实习的任务
  4. +
  5. 学了一些docker,目前基本使用没有问题了
  6. +
+

2023年05月01日-2023年06月02日

+

计划安排

+

实施情况

+
2023年05月01日-2022年05月06日
+
    +
  • 05.01 +
      +
    • 串门,打乒乓球
    • +
    +
  • +
  • 05.02 +
      +
    • 整理智能计算系统实验代码
    • +
    +
  • +
  • 05.03 +
      +
    • 调研开源活动
    • +
    • 调研大模型最新进展,看了一篇半论文,对大模型有了一个初步的了解
    • +
    +
  • +
  • 05.04 +
      +
    • ads_sort RocketMQ 转 Pulsar 灰度测试,配置Prometheus看板
    • +
    • 学习探究使用RocksDB,一个文件系统的数据库,目前已经可以跑通
    • +
    • 继续调研开源活动,感觉不太敢尝试
    • +
    +
  • +
  • 05.05 +
      +
    • 一些实验全量发线上等
    • +
    • 思考过滤优化的方法
    • +
    +
  • +
  • 05.06 +
      +
    • 智能计算系统综合实验拿到81分,似乎评测平台有问题无法拿到90分
    • +
    • 继续尝试思考过滤优化的方法,目前确定了Bitmap+MultiFilter两种思路
    • +
    +
  • +
+

周总结:

+
    +
  1. 整理智能计算系统实验代码,完成综合实验但是最高只有81分,理论上应该能到90分,还要继续多尝试
  2. +
  3. 调研开源活动和大模型的最新进展,看了两篇大模型相关的论文,对大模型有了初步的了解
  4. +
  5. 实习将之前做的一些实验全量,思考Filter的优化方法
  6. +
  7. 串门、打乒乓球、休息
  8. +
+
2023年05月07日-2022年05月13日
+
    +
  • 05.07 +
      +
    • 整理智能计算系统的笔记资料和代码资料并打印
    • +
    • 打乒乓球、休息
    • +
    +
  • +
  • 05.08 +
      +
    • Pulsar切回RocketMQ
    • +
    • 开始更改Filter
    • +
    • 编写智能计算系统的提交脚本
    • +
    +
  • +
  • 05.09 +
      +
    • 更改Filter完成并在预发上测试通过
    • +
    • RocketMQ切回Pulsar
    • +
    +
  • +
  • 05.10 +
      +
    • 第二次优化Filter
    • +
    • Pulsar+Sentinel熔断
    • +
    +
  • +
  • 05.11 +
      +
    • Sentinel熔断做完发版
    • +
    • 优化ads_sort并收集效果
    • +
    • 开始看aml代码,看完一个仓库
    • +
    +
  • +
  • 05.12 +
      +
    • 看完大部分的aml代码
    • +
    +
  • +
  • 05.13 +
      +
    • 回学校,找考试材料
    • +
    • 大概看一遍文本数据挖掘的教材,看了半本
    • +
    +
  • +
+

周总结:

+
    +
  1. 实习:优化Filter,时间和空间都有效果,折腾Pulsar的使用,看aml的代码,大概的流程基本看完
  2. +
  3. 上课:智能计算系统编写提交脚本,打印完成全部资料;文本数据挖掘看了一半的教材,回学校找到了开卷考试需要携带的书
  4. +
  5. 打乒乓球、休息
  6. +
+
2023年05月14日-2022年05月20日
+
    +
  • 05.14 +
      +
    • 看大模型论文
    • +
    • 完成英语慕课的试考,背单词
    • +
    • 用update镜像重新尝试yolo,效果与之前相同
    • +
    +
  • +
  • 05.15 +
      +
    • 继续看mms代码
    • +
    • 背英语单词
    • +
    +
  • +
  • 05.16 +
      +
    • 看mms代码完成,画了一个流程图
    • +
    • 了解目前的模型训练测试流程
    • +
    • 整理情感计算提交材料,背英语单词
    • +
    • 尝试搭建GPT-4服务,不成功
    • +
    • 准备使用不背单词APP辅助背单词
    • +
    +
  • +
  • 05.17 +
      +
    • 学习Redis基础知识
    • +
    • 找到Config服务的bug,准备明天修复
    • +
    • 背单词
    • +
    +
  • +
  • 05.18 +
      +
    • 学习Redis基础知识
    • +
    • 报警加完并验证
    • +
    • 文本数据挖掘书看完
    • +
    +
  • +
  • 05.19 +
      +
    • 学习Redis基础知识
    • +
    • 大概完成打压投诉率高的游戏的需求
    • +
    • 智能计算系统最后一节课
    • +
    +
  • +
  • 05.20 +
      +
    • 整理完成慕课词汇题目
    • +
    • 智能计算系统书看完
    • +
    +
  • +
+

周总结:

+
    +
  1. 作业考试:英语慕课完成试考,背单词坚持一周,整理词汇题目;文本数据挖掘和智能计算系统大致看完教材,智能计算系统上了最后一节课;整理了情感计算的提交材料
  2. +
  3. 实习:看完mms的代码,了解了目前的模型的训练测试流程,Config服务加一个报警,学习了一些Redis的基础知识,完成打压投诉率高的游戏的需求
  4. +
  5. 尝试搭建GPT-4服务,未搭建成功,看完上周剩下的大模型的论文
  6. +
+
2023年05月21日-2022年05月27日
+
    +
  • 05.21 +
      +
    • 修补上周需求的bug并在预发上进行测试
    • +
    • 看比赛、休息
    • +
    +
  • +
  • 05.22 +
      +
    • 完成需求,还有另一半需要做,基本做完但是测试没效果
    • +
    • 了解阿里云Kafka并申请权限
    • +
    • 推荐自动化配置完成手动修改
    • +
    +
  • +
  • 05.23 +
      +
    • 完成另一半需求并上线
    • +
    • 报警上线
    • +
    • Kafka开发完成,待明天测试上线
    • +
    +
  • +
  • 05.24 +
      +
    • 学习Kafka和Redis基础知识
    • +
    • ads_sort切Kafka完成
    • +
    +
  • +
  • 05.25 +
      +
    • 学习Kafka和Redis基础知识
    • +
    • Kafka上线成功,测试通过
    • +
    +
  • +
  • 05.26 +
      +
    • 智能计算系统考试
    • +
    +
  • +
  • 05.27 +
      +
    • 整理两个大作业的提交
    • +
    • 英语慕课考试
    • +
    +
  • +
+

周总结:

+
    +
  1. 智能计算系统、英语慕课考试结束,大作业完成最终梳理
  2. +
  3. 实习切换Kafka但是没有应用,完成了打压投诉率高的需求,加了非主包的报警,学习Kafka和Redis的基础知识
  4. +
+
2023年05月28日-2022年06月02日
+
    +
  • 05.28 +
      +
    • 复习文本数据挖掘后面部分
    • +
    • 打乒乓球、休息
    • +
    +
  • +
  • 05.29 +
      +
    • 投诉率打压更改位置后上线
    • +
    • Redis书籍看完,准备开始看K8S的书
    • +
    • 完成代码仓库的克隆并推送
    • +
    +
  • +
  • 05.30 +
      +
    • 打压需求加Redis监听
    • +
    • mms小需求发版
    • +
    • K8s基础知识学习
    • +
    • 复习文本数据挖掘
    • +
    +
  • +
  • 05.31 +
      +
    • 文本数据挖掘考试
    • +
    • 练习乒乓球
    • +
    +
  • +
  • 06.01 +
      +
    • 迁sdk的Pulsar为Kafka
    • +
    • 阅读K8s、Rust书籍
    • +
    +
  • +
  • 06.02 +
      +
    • 迁sdk的Pulsar为Kafka,找bug重新观察实验
    • +
    +
  • +
+

周总结:

+
    +
  1. 学校:复习文本数据挖掘并考试
  2. +
  3. 实习:基本在自己学习,大致看了一遍Redis的基础知识,看了一些K8s的知识,看了一点Rust语言;其他完成了一些小需求,将Pulsar切为Kafka并上线测试。同时完成代码仓库的克隆,目前在逐步更新中
  4. +
  5. 运动:练习乒乓球,准备打比赛
  6. +
+

月度完成情况总结

+

2023年06月03日-2023年07月07日

+

计划安排

+

实施情况

+
2023年06月03日-2022年06月09日
+
    +
  • 06.03 +
      +
    • 乒乓球院赛
    • +
    +
  • +
  • 06.04 +
      +
    • 休息
    • +
    +
  • +
  • 06.05 +
      +
    • ads_sort Kafka上线开实验
    • +
    • Sentinel config从Nacos迁移到Redis
    • +
    +
  • +
  • 06.06 +
      +
    • Sentinel config从Nacos迁移到Redis 上线
    • +
    • 排查Redis占用过高的问题
    • +
    • 迁移Kafka确认应该没有问题
    • +
    • 学习K8s书籍
    • +
    +
  • +
  • 06.07 +
      +
    • 学习集群Redis
    • +
    • 学习飞书机器人使用方法
    • +
    • 抢到课程
    • +
    • ads_sdk加Sentinel,更改更准确的Prometheus看板
    • +
    • 完成科普翻译初稿
    • +
    +
  • +
  • 06.08 +
      +
    • ads_sdk加Sentinel上线,更改更准确的Prometheus看板完成
    • +
    • 预发Redis打数据迁移到定时任务中
    • +
    • 完成科普翻译
    • +
    +
  • +
  • 06.09 +
      +
    • 学习位运算、算法等知识
    • +
    • Sentinel迁移到Hook中调用
    • +
    +
  • +
+

周总结:

+
    +
  1. 学校:选课抢课,参加了科普翻译的比赛,借助工具完成了翻译
  2. +
  3. 实习:基本在自己学习,看了Redis的集群版知识,看了一些K8s的知识,看了一点Rust语言,学习了一些算法的知识;两个服务切Kafka都没什么问题了,将Sentinel的配置迁移到Redis中,并在ads_sdk中新增加Sentinel;排查了Redis占用过高的问题;学了一点飞书机器人的使用方法
  4. +
  5. 运动:乒乓球院赛男子双打冠军
  6. +
+
2023年06月10日-2022年06月16日
+
    +
  • 06.10 +
      +
    • 排查帖子服务问题
    • +
    • 练习链表Leetcode题目
    • +
    +
  • +
  • 06.11 +
      +
    • 聚餐
    • +
    • 打乒乓球
    • +
    +
  • +
  • 06.12 +
      +
    • Kafka正式上线
    • +
    • ads_sort优化深拷贝重新编写,修改原来的问题
    • +
    +
  • +
  • 06.13 +
      +
    • 翻译强化学习论文初稿完成
    • +
    +
  • +
  • 06.14 +
      +
    • 优化服务小地方
    • +
    • 完成强化学习论文报告
    • +
    +
  • +
  • 06.15 +
      +
    • ads_sdk与user_feature联动完成Kitex的调用,nacos还有一些问题
    • +
    • 简单撰写金融选修课的报告
    • +
    +
  • +
  • 06.16 +
      +
    • 研究Kitex未果
    • +
    • 大致完成金融选修课的报告
    • +
    +
  • +
+

周总结:

+
    +
  1. 上课:基本完成两个选修课的大作业,下班时间基本用在了这些上面
  2. +
  3. 实习没有什么任务,研究了一下Kitex的使用,服务端还是会报一点点错误
  4. +
+
2023年06月17日-2022年06月23日
+
    +
  • 06.17 +
      +
    • 回学校练习乒乓球
    • +
    • 金融选修课报告完善
    • +
    +
  • +
  • 06.18 +
      +
    • 乒乓球比赛
    • +
    • 金融选修课报告基本完成
    • +
    +
  • +
  • 06.19 +
      +
    • 完成Kitex实现,发灰度进行测试
    • +
    • 提交金融选修课报告,完成自我鉴定
    • +
    +
  • +
  • 06.20 +
      +
    • Kitex排查问题
    • +
    • 继续刷链表的题目
    • +
    +
  • +
  • 06.21 +
      +
    • 添加Sentinel限流
    • +
    • 排查线上问题
    • +
    • 继续做链表的题目
    • +
    +
  • +
  • 06.22 +
      +
    • 链表题目基本完成
    • +
    +
  • +
  • 06.23 +
      +
    • 做了一部分双指针的题目
    • +
    +
  • +
+

周总结:

+
    +
  1. 上课:彻底完成全部课程内容
  2. +
  3. 实习:没有什么任务,开始刷题,刷完了链表相关的题目和一部分双指针的题目
  4. +
  5. 乒乓球校赛,输得很惨
  6. +
+
2023年06月24日-2022年06月30日
+
    +
  • 06.24 +
      +
    • 刷剧休息
    • +
    +
  • +
  • 06.25 +
      +
    • 整理交接材料
    • +
    +
  • +
  • 06.26 +
      +
    • 整理交接材料
    • +
    • 研究Kitex错误
    • +
    • 刷题
    • +
    +
  • +
  • 06.27 +
      +
    • 整理交接材料
    • +
    • 研究Kitex错误
    • +
    +
  • +
  • 06.28 +
      +
    • 刷题,完成配置需求
    • +
    +
  • +
  • 06.29 +
      +
    • 刷题
    • +
    • 收拾物品
    • +
    +
  • +
  • 06.30 +
      +
    • 结束实习
    • +
    • 回学校
    • +
    +
  • +
+

周总结:

+

整理交接材料,结束实习回学校

+
2023年07月01日-2022年07月07日
+
    +
  • 07.01 +
      +
    • 回家
    • +
    +
  • +
  • 07.02 +
      +
    • 搭建Docker开发环境,完成Go、Python、Leetcode环境的搭建
    • +
    +
  • +
  • 07.03 +
      +
    • 搭建Docker开发环境,完成Anaconda、Hexo、Rust环境的搭建
    • +
    +
  • +
  • 07.04 +
      +
    • 整理Github仓库和研一的脚本
    • +
    • 复习前面学过的Rust,看到第四章所有权结束
    • +
    +
  • +
  • 07.05 +
      +
    • Rust看到第八章,看不下去了
    • +
    +
  • +
+

计划总结

+

基本就是实习,没受到重视,除了实习之外也没有干什么其他的事情

+ + +
+ +
+
+ + + + + + +
+
+
学习计划(2023年2月——2023年7月)
+
https://zhangzhao219.github.io/2023/02/21/zhangzhao-plan-2/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月21日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/02/24/Backend/HertzAndThrift/index.html b/2023/02/24/Backend/HertzAndThrift/index.html new file mode 100644 index 000000000..7b7a4ad4d --- /dev/null +++ b/2023/02/24/Backend/HertzAndThrift/index.html @@ -0,0 +1,1183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hertz和Thrift简单示例 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Hertz和Thrift简单示例

+ + +
+ +

Hertz和Thrift简单示例

+ +

Hertz

+

Hertz 是字节跳动服务框架团队研发的超大规模的企业级微服务 HTTP 框架,具有高易用性、易扩展、低时延等特点。

+

官方文档:https://www.cloudwego.io/zh/docs/hertz/

+

基本使用:

+

定义路由:

+
func main() {
+	h := server.Default(server.WithHostPorts("127.0.0.1:50000"))
+	h.GET("/ping", router.Deal)
+	h.Spin()
+}
+

路由的Handler:

+

其中与其他框架最大的不同点是将Context分成了两个部分:

+

两个上下文主要有两点区别:

+
    +
  1. 生命周期不同。RequestContext 的生命周期局限于一次 http 请求之内,而 context.Context 会在 RPC Client 或者日志、Tracing 等组件间传递,其生命周期可能是链路级别的;
  2. +
  3. 协程安全性。RequestContext 协程不安全,不适合异步传递,但可以通过 Copy()方法获取一个协程安全的副本,而 context.Context 本身就是协程安全的。
  4. +
+
func Deal(c context.Context, ctx *app.RequestContext) {
+	ctx.JSON(consts.StatusOK, utils.H{"message": res})
+}
+

Thrift

+

Thrift是一个 轻量级跨语言远程服务调用框架,最初由 Facebook开发,后面进入 Apache开源项目。它通过自身的 IDL 中间语言 , 并借助代码生成引擎生成各种主流语言的 RPC 服务端 /客户端模板代码。

+

官方安装:https://thrift.apache.org/docs/BuildingFromSource.html

+

网上资料:

+

注意安装的thrift的版本与go的插件版本一定要相同!

+

安装go插件:

+
go get github.com/apache/thrift/lib/go/thrift
+

首先安装依赖:

+
apt install libboost-dev libboost-test-dev libboost-program-options-dev libboost-filesystem-dev libboost-thread-dev libevent-dev automake libtool flex bison pkg-config g++ libssl-dev
+

安装Thrift:

+
git clone https://github.com/apache/thrift
+cd thrift
+./bootstrap.sh
+./configure --without-qt4 --wihout-qt5
+make
+make install
+

编译使用:

+
thrift -r --gen go compute.thrift
+

Thrift文件定义

+
namespace go compute
+
+service MulRange {
+    string BigRange(1:i64 max)
+}
+

客户端

+
func Deal(c context.Context, ctx *app.RequestContext) {
+
+	transportFactory := thrift.NewTTransportFactory()
+
+	protocolFactory := thrift.NewTBinaryProtocolFactoryConf(nil)
+	addr := "127.0.0.1:9999"
+
+	cfg := &thrift.TConfiguration{}
+
+	// 建立和服务器的连接socket,通过socket建立Transport
+	var transport thrift.TTransport
+	transport = thrift.NewTSocketConf(addr, cfg)
+	transport, _ = transportFactory.GetTransport(transport)
+	defer transport.Close()
+
+	// 打开Transport,与服务器进行连接
+	transport.Open()
+
+	iprot := protocolFactory.GetProtocol(transport)
+	oprot := protocolFactory.GetProtocol(transport)
+
+	client := compute.NewMulRangeClient(thrift.NewTStandardClient(iprot, oprot))
+
+	num, _ := client.BigRange(context.Background(), 10)
+	fmt.Println(num)
+
+	ctx.JSON(consts.StatusOK, utils.H{"message": num})
+}
+

服务端

+
// 尽量一个struct对应一个service
+type mulrangeThrift struct {
+}
+
+func (m *mulrangeThrift) BigRange(_ context.Context, max int64) (string, error) {
+
+	result := max + 1253
+	return strconv.FormatInt(result, 10), nil
+}
+
+func main() {
+	// 创建服务器
+	serverTransport, _ := thrift.NewTServerSocket(net.JoinHostPort("127.0.0.1", "9999"))
+
+	// 创建二进制协议
+	transportFactory := thrift.NewTTransportFactory()
+	protocolFactory := thrift.NewTBinaryProtocolFactoryConf(nil)
+
+	mulrangeProcessor := compute.NewMulRangeProcessor(new(mulrangeThrift))
+
+	// 启动服务器
+	server := thrift.NewTSimpleServer4(mulrangeProcessor, serverTransport, transportFactory, protocolFactory)
+	server.Serve()
+
+	// 退出时停止服务器
+	defer server.Stop()
+}
+

Thrift深入学习

+

参考资料:https://juejin.cn/post/6844903622380093447

+

Thrift是一个 轻量级跨语言远程服务调用框架,最初由 Facebook开发,后面进入 Apache开源项目。它通过自身的 IDL 中间语言 , 并借助代码生成引擎生成各种主流语言的 RPC 服务端 /客户端模板代码。

+

Thrift的特性

+

(一) 开发速度快

+

通过编写 RPC接口 Thrift IDL文件,利用编译生成器自动生成 服务端骨架 (Skeletons)和 客户端桩 (Stubs)。从而省去开发者自定义维护接口编解码消息传输服务器多线程模型等基础工作。

+
    +
  • 服务端:只需要按照服务骨架接口 ,编写好具体的 业务处理程序 (Handler)即实现类即可。
  • +
  • 客户端:只需要拷贝 IDL定义好的客户端桩服务对象 ,然后就像调用本地对象的方法一样调用远端服务。
  • +
+

(二) 接口维护简单

+

通过维护 Thrift格式的IDL( 接口描述语言 )文件(注意写好注释),即可作为给 Client使用的接口文档使用,也自动生成接口代码,始终保持代码和文档的一致性。且 Thrift协议可灵活支持接口可扩展性

+

(三) 学习成本低

+

因为其来自 Google Protobuf开发团队,所以其 IDL文件风格类似 Google Protobuf,且更加 易读易懂 ;特别是 RPC服务接口的风格就像写一个面向对象Class一样简单。

+

初学者只需参照:thrift.apache.org/,一个多小时就可以理解 Thrift IDL文件的语法使用。

+

(四) 多语言/跨语言支持

+

Thrift支持 C++JavaPythonPHPRubyErlangPerlHaskellC#CocoaJavaScriptNode.jsSmalltalk等多种语言,即可生成上述语言的服务器端客户端程序

+

对于我们经常使用的 JavaPHPPythonC++支持良好,虽然对 iOS环境的 Objective-C(Cocoa)支持稍逊,但也完全满足我们的使用要求。

+

(五) 稳定/广泛使用

+

Thrift在很多开源项目中已经被验证是稳定高效的,例如 CassandraHadoopHBase等;国外在 Facebook中有广泛使用,国内包括百度、美团小米、和饿了么等公司。

+

数据类型

+
    +
  • 基本类型 +
      +
    • bool : 布尔值
    • +
    • byte : 8位有符号整数
    • +
    • i16 : 16位有符号整数
    • +
    • i32 : 32位有符号整数
    • +
    • i64 : 64位有符号整数
    • +
    • double : 64位浮点数
    • +
    • string : UTF-8编码的字符串
    • +
    • binary : 二进制串
    • +
    +
  • +
  • 结构体类型 +
      +
    • struct : 定义的结构体对象
    • +
    +
  • +
  • 容器类型 +
      +
    • list : 有序元素列表
    • +
    • set : 无序无重复元素集合
    • +
    • map : 有序的key/value集合
    • +
    +
  • +
  • 异常类型 +
      +
    • exception : 异常类型
    • +
    +
  • +
  • 服务类型 +
      +
    • service : 具体对应服务的类
    • +
    +
  • +
+

Thrift协议

+

Thrift可以让用户选择客户端服务端之间传输通信协议的类别,在传输协议上总体划分为 文本 (text)和 二进制 (binary)传输协议。为 节约带宽提高传输效率 ,一般情况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这需要根据项目/产品中的实际需求。常用协议有以下几种:

+
    +
  • TBinaryProtocol:二进制编码格式进行数据传输
  • +
  • TCompactProtocol:高效率的、密集二进制编码格式进行数据传输
  • +
  • TJSONProtocol: 使用 JSON文本的数据编码协议进行数据传输
  • +
  • TSimpleJSONProtocol:只提供 JSON只写的协议,适用于通过脚本语言解析
  • +
+

Thrift与Protobuf的区别

+

Thrift和Protobuf的最大不同,在于Thrift提供了完整的RPC支持,包含了Server/Client,而Protobuf只包括了stub的生成器和格式定义。

+

Thrift示例

+

thrift语法

+

User.thrift

+
namespace go Sample
+
+struct User {
+    1:required i32 id;
+    2:required string name;
+    3:required string avatar;
+    4:required string address;
+    5:required string mobile;
+}
+
+struct UserList {
+    1:required list<User> userList;
+    2:required i32 page;
+    3:required i32 limit;
+}
+

Service.thrift

+
include "User.thrift"
+
+namespace go Sample
+
+typedef map<string, string> Data
+
+struct Response {
+    1:required i32 errCode; //错误码
+    2:required string errMsg; //错误信息
+    3:required Data data;
+}
+
+//定义服务
+service Greeter {
+    Response SayHello(
+        1:required User.User user
+    )
+
+    Response GetUser(
+        1:required i32 uid
+    )
+}
+
    +
  1. +
    文件引入
    +
  2. +
+

thrift支持引入另一个thrift文件:

+
include "User.thrift"
+

注意:

+

include 引入文件的使用,字段必须带文件名前缀:

+
1:required User.User user
+

不能直接写 User user,这样会提示找不到 User定义。

+

编译时只编译引用了其他文件的thrift文件即可:

+
thrift -r --gen go Service.thrift
+
    +
  1. +
    定义命名空间或者包名
    +
  2. +
+
namespace go Sample
+namespace php Sample
+

需要支持多个语言,则需要定义多行。

+

命名空间或者包名是多层级,使用 .号隔开。例如golang对于 Sample.Model会生成目录 Sample/Model,包名是 Model

+
    +
  1. +
    Field
    +
  2. +
+
struct User {
+    1:required i32 id = 0;
+    2:optional string name;
+}
+

字段选项 支持 requiredoptional两种。

+

一旦一个参数设置为 required,未来就一定不能删除或者改为 optional,否则就会出现版本不兼容问题,老客户端访问新服务会出现参数错误。不确定的情况可以都使用 optional

+
    +
  1. +
    类型定义
    +
  2. +
  3. 基本类型
  4. +
+
bool:布尔值(truefalse)
+byte8位有符号整数
+i1616位有符号整数
+i3232位有符号整数
+i6464位有符号整数
+double64位浮点数
+string:使用UTF-8编码编码的文本字符串
+
    +
  1. 容器类型
  2. +
+
list<t1>:一系列t1类型的元素组成的有序列表,元素可以重复
+set<t1>:一些t1类型的元素组成的无序集合,元素唯一不重复
+map<t1,t2>:key/value对,key唯一
+
    +
  1. 类型别名
  2. +
+
typedef map<string, string> Data
+
    +
  1. 枚举类型
  2. +
+
enum TweetType {
+    TWEET,
+    RETWEET = 2,
+    DM = 0xa,
+    REPLY
+}
+

默认从0开始赋值,枚举值可以赋予某个常量,允许常量是十六进制整数。末尾没有逗号。

+

不支持枚举类嵌套,枚举常量必须是32位正整数。

+

对于go,会生成 TweetType_开头的常量。

+
    +
  1. 常量类型
  2. +
+

Thrift允许用户定义常量,复杂的类型和结构体可以使用JSON形式表示:

+
const i32 INT_CONST = 1234
+const map<string,string> MAP_CONST = {"hello": "world", "goodnight": "moon"}
+
    +
  1. 异常类型
  2. +
+
exception BizException {
+    1:required i32 code
+    2:required string msg
+}
+
    +
  1. 结构体
  2. +
+

结构体可以包含其他结构体,但不支持继承结构体。

+
struct Response {
+    1:required i32 errCode; //错误码
+    2:required string errMsg; //错误信息
+    3:required Data data;
+}
+
    +
  1. 服务
  2. +
+

Thrift编译器会根据选择的目标语言为server产生服务接口代码,为client产生桩(stub)代码。

+

在go里是 interfaceservice里定义的方法必须由服务端实现。

+
service Greeter {
+    Response SayHello(
+        1:required User.User user
+    )
+}
+

参数是user,返回值是Response类型

+

服务端代码

+

服务端主要完成4个部分的工作:

+
    +
  • Create a transport
  • +
  • Create input/output protocols for the transport
  • +
  • Create a processor based on the input/output protocols
  • +
  • Wait for incoming connections and hand them off to the processor
  • +
+

服务端最终要创建这样的一个server

+
func NewTSimpleServerFactory6(processorFactory TProcessorFactory, serverTransport TServerTransport, inputTransportFactory TTransportFactory, outputTransportFactory TTransportFactory, inputProtocolFactory TProtocolFactory, outputProtocolFactory TProtocolFactory) *TSimpleServer {
+    return &TSimpleServer{
+        processorFactory:       processorFactory,
+        serverTransport:        serverTransport,
+        inputTransportFactory:  inputTransportFactory,
+        outputTransportFactory: outputTransportFactory,
+        inputProtocolFactory:   inputProtocolFactory,
+        outputProtocolFactory:  outputProtocolFactory,
+    }
+}
+

说明:

+
    +
  • 需要至少指定2个字段(processorFactory和serverTransport)
  • +
  • 常用是指定4个字段(包括TransportFactory和ProtocolFactory),默认input与output使用的协议相同
  • +
+
server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
+err = server.Serve()
+
    +
  1. +
    processor:thrift定义服务的处理函数
    +
  2. +
+
// 定义服务
+type Greeter struct {
+}
+handler := &Greeter{}
+processor := Sample.NewGreeterProcessor(handler)
+
    +
  1. +
    serverTransport:在指定的端口上创建一个socket连接
    +
  2. +
+
var transport thrift.TServerTransport
+transport, err = thrift.NewTServerSocket(*addr)
+
    +
  1. +
    transportFactory
    +
  2. +
+

不同类型可选

+
//buffered
+var transportFactory thrift.TTransportFactory
+if *buffered {
+    transportFactory = thrift.NewTBufferedTransportFactory(8192)
+} else {
+    transportFactory = thrift.NewTTransportFactory()
+}
+
+//framed
+if *framed {
+    transportFactory = thrift.NewTFramedTransportFactory(transportFactory)
+}
+
    +
  1. +
    ProtocolFactory
    +
  2. +
+

不同类型可选

+
var protocolFactory thrift.TProtocolFactory
+switch *protocol {
+case "compact":
+    protocolFactory = thrift.NewTCompactProtocolFactory()
+case "simplejson":
+    protocolFactory = thrift.NewTSimpleJSONProtocolFactory()
+case "json":
+    protocolFactory = thrift.NewTJSONProtocolFactory()
+case "binary", "":
+    protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()
+

客户端代码

+

客户端定义好client后直接调用方法即可,如下所示:

+
client := GetClient()
+rep, err := client.GetUser(ctx, 100)
+rep, err := client.SayHello(ctx, &Sample.User{
+     Name:    "thrift",
+     Address: "address",
+ })
+
    +
  1. +
    定义client
    +
  2. +
+
iprot := protocolFactory.GetProtocol(transport)
+oprot := protocolFactory.GetProtocol(transport)
+client := Sample.NewGreeterClient(thrift.NewTStandardClient(iprot, oprot))
+

涉及到protocolFactory与transport

+
    +
  1. +
    protocolFactory
    +
  2. +
+
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
+iprot := protocolFactory.GetProtocol(transport)
+oprot := protocolFactory.GetProtocol(transport)
+

注意要与服务端定义的protocolFactory要一致

+
    +
  1. +
    transport
    +
  2. +
+

创建socket连接:

+
var transport thrift.TTransport
+var err error
+transport, err = thrift.NewTSocket(addr)
+

注意要提前进行类型定义,否则后面类型不匹配

+

定义transportFactory:

+
transportFactory := thrift.NewTTransportFactory()
+transport, err = transportFactory.GetTransport(transport)
+transport.Open()
+

注意transportFactory的类型要与服务端相同

+

其他

+

可以添加key同时使用SSL进行Socket连接从而确保安全性

+ + +
+ +
+
+ + + + + + +
+
+
Hertz和Thrift简单示例
+
https://zhangzhao219.github.io/2023/02/24/Backend/HertzAndThrift/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年2月24日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/03/02/Backend/Prometheus/index.html b/2023/03/02/Backend/Prometheus/index.html new file mode 100644 index 000000000..c0202ff7f --- /dev/null +++ b/2023/03/02/Backend/Prometheus/index.html @@ -0,0 +1,850 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prometheus简单示例 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Prometheus简单示例

+ + +
+ +

Prometheus简单示例

+ +

Prometheus

+

Prometheus 是一款基于时序数据库的开源监控告警系统。Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。

+

示例

+

下载安装启动

+
wget https://github.com/prometheus/prometheus/releases/download/v2.37.6/prometheus-2.37.6.linux-amd64.tar.gz
+tar xvfz prometheus-2.37.6.linux-amd64.tar.gz 
+cd prometheus-2.37.6.linux-amd64/
+./prometheus --config.file=prometheus.yml
+

此时打开http://localhost:9090/即可以看到监控界面

+

Go客户端编写

+
package main
+
+import (
+    "net/http"
+
+    "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+func main() {
+    //提供 /metrics HTTP 端点
+    http.Handle("/metrics", promhttp.Handler())
+    //端口号
+    http.ListenAndServe(":2112", nil)
+}
+

运行后访问http://localhost:2112/metrics可以看到采集的指标数据

+

注册自定义应用程序指定指标:

+
package main
+
+import (
+    "net/http"
+    "time"
+
+    "github.com/prometheus/client_golang/prometheus"
+    "github.com/prometheus/client_golang/prometheus/promauto"
+    "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+func recordMetrics() {
+    //每2秒,计数器增加1。
+    go func() {
+        for {
+            opsProcessed.Inc()
+            time.Sleep(2 * time.Second)
+        }
+    }()
+}
+
+// 公开了 myapp_processed_ops_total 计数器
+var (
+    opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
+        Name: "myapp_processed_ops_total",
+        Help: "The total number of processed events",
+    })
+)
+
+func main() {
+    recordMetrics()
+
+    http.Handle("/metrics", promhttp.Handler())
+    http.ListenAndServe(":2112", nil)
+}
+

运行后访问http://localhost:2112/metrics可以看到自定义的指标,每2秒,计数器增加1

+

服务端看板

+

可以修改配置文件:prometheus.yml

+
# my global config
+global:
+  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
+  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
+  # scrape_timeout is set to the global default (10s).
+
+# Alertmanager configuration
+alerting:
+  alertmanagers:
+    - static_configs:
+        - targets:
+          # - alertmanager:9093
+
+# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
+rule_files:
+  # - "first_rules.yml"
+  # - "second_rules.yml"
+
+# A scrape configuration containing exactly one endpoint to scrape:
+# Here it's Prometheus itself.
+scrape_configs:
+  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
+  - job_name: "prometheus"
+
+    # metrics_path defaults to '/metrics'
+    # scheme defaults to 'http'.
+
+    static_configs:
+      - targets: ["localhost:2112"]
+

将最后的targets修改成客户端启动的端口即可

+ + +
+ +
+
+ + + + + + +
+
+
Prometheus简单示例
+
https://zhangzhao219.github.io/2023/03/02/Backend/Prometheus/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年3月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/03/03/Backend/Kafka/index.html b/2023/03/03/Backend/Kafka/index.html new file mode 100644 index 000000000..5f995db2d --- /dev/null +++ b/2023/03/03/Backend/Kafka/index.html @@ -0,0 +1,849 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kafka简单示例 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Kafka简单示例

+ + +
+ +

Kafka简单示例

+ +

Kafka

+

安装

+

首先需要安装Java

+
sudo apt install openjdk-11-jdk
+

下载安装kafka

+
wget https://dlcdn.apache.org/kafka/3.4.0/kafka_2.13-3.4.0.tgz
+tar -xzf kafka_2.13-3.4.0.tgz
+cd kafka_2.13-3.4.0
+

启动zookeeper和kafka:

+
bin/zookeeper-server-start.sh config/zookeeper.properties
+bin/kafka-server-start.sh config/server.properties
+

go连接Kafka使用

+

生产者

+

使用给定代理地址和配置创建一个同步生产者

+
// 使用给定代理地址和配置创建一个同步生产者
+SyncProducer, err := sarama.NewSyncProducer(
+    []string{conn},
+    config,
+)
+

config可以自由配置:

+
config := sarama.NewConfig()
+// 等待服务器所有副本都保存成功后的响应
+config.Producer.RequiredAcks = sarama.WaitForAll
+// 随机的分区类型:返回一个分区器,该分区器每次选择一个随机分区
+config.Producer.Partitioner = sarama.NewRandomPartitioner
+// 是否等待成功和失败后的响应
+config.Producer.Return.Successes = true
+

构建发送的消息:

+
// 构建发送的消息
+msg := &sarama.ProducerMessage{
+    Topic: topic,
+    Key:   sarama.StringEncoder(time.Now().String()),
+    Value: sarama.StringEncoder(content),
+}
+

生产者发送消息:

+
// SendMessage:该方法是生产者生产给定的消息
+// 生产成功的时候返回该消息的分区和所在的偏移量
+// 生产失败的时候返回error
+partition, offset, err := SyncProducer.SendMessage(msg)
+

消费者

+

创建一个消费者的实例

+
config := sarama.NewConfig()
+consumer, err := sarama.NewConsumer(c.Node, config)
+

查询这个 topic 有多少分区

+
partitions, err := consumer.Partitions(c.Topic)
+

每个分区开一个 goroutine 来消费

+
wg.Add(len(partitions))
+// 然后每个分区开一个 goroutine 来消费
+for _, partitionId := range partitions {
+    //不开异步会导致一个消费完才会消费另外一个
+    go c.consumeByPartition(consumer, c.Topic, partitionId, &wg)
+}
+

消费

+
partitionConsumer, err := consumer.ConsumePartition(topic, partitionId, sarama.OffsetNewest)
+// 然后可以通过partitionConsumer.Messages()打印得到的消息
+

主函数

+
func main() {
+    Conn := "127.0.0.1:9092"
+    topic := "test_log"
+
+    var wg sync.WaitGroup
+    wg.Add(2)
+
+    // 消费者
+    go func() {
+        defer wg.Done()
+        // 初始化consumer
+        var kafkaConsumer = consumer.KafkaConsumer{
+            Node:  []string{Conn},
+            Topic: topic,
+        }
+        // 消费
+        go kafkaConsumer.Consume()
+    }()
+
+    // 生产者
+    go func() {
+        defer wg.Done()
+
+        index := 0
+        for {
+            // 生产者发送消息
+            _, err := producer.Send(Conn, topic, fmt.Sprintf("lox_%d", index))
+            if err != nil {
+                log.Print("测试失败:" + err.Error())
+                return
+            }
+            index++
+            time.Sleep(1 * time.Second)
+        }
+    }()
+    wg.Wait()
+}
+ + +
+ +
+
+ + + + + + +
+
+
Kafka简单示例
+
https://zhangzhao219.github.io/2023/03/03/Backend/Kafka/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年3月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/03/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project/index.html b/2023/03/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project/index.html new file mode 100644 index 000000000..6d9b5bad6 --- /dev/null +++ b/2023/03/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project/index.html @@ -0,0 +1,1993 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 字节跳动青训营-抖音项目 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

字节跳动青训营-抖音项目

+ + +
+ +

字节跳动青训营-抖音项目

+ +

一、项目介绍

+

“NoBugVideo”基于Gin Web框架,采用微服务架构,使用Kong集成Consul做服务发现,实现了“抖音”的基本功能,包括视频流的推送、视频投稿、用户的注册与登录,以及社交(用户之间的关注与聊天)与互动(用户对视频点赞及评论)等功能。

+

项目服务地址:http://124.221.120.88:8000

+

Github地址:https://github.com/xu-jq/simple-DY

+

我们团队实现了包括基础功能在内的两大方向:互动方向社交方向 ,根据项目考核的4个标准,自评如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
评价项实现情况
功能实现微服务与其他资源能够正常运行,完全实现文档中定义的全部接口,边界情况处理良好
代码质量项目结构清晰,包划分合理,代码符合编码规范
服务性能数据表设置了合理的索引,代码中尽量使用并行处理提高性能
安全可靠通过GORM框架防止SQL注入,通过JWT进行用户的认证,防止越权
+

二、项目分工

+ + + + + + + + + + + + + + + + + + + + + +
团队成员主要贡献
@汪辉开发社交模块,搭建Kong集成Consul做服务发现
@许珺琪开发用户互动相关模块包括点赞评论等相关接口、搭建redis服务
@张兆开发用户模块与视频模块相关接口、搭建MySQL、RabbitMQ等服务
+

三、项目实现

+

3.1 技术选型与相关开发文档

+

抖音上线于2016年9月26日,一开始是定位于专注于新生代的音乐创意短视频App,视频时常限制在15s内。年轻人比较爱赶新潮,乐于尝试新鲜事物,通过清晰明确定位在“潮流”“炫酷”“技术流”的方式,抖音吸引了第一批忠实粉丝。当产品功能逐渐完善后,抖音在运营方面开始发力,用户迎来大幅增长。抖音的主力用户群体年龄段上升,已经从早期的18岁到24岁,上升到了25岁到30岁用户。随着用户的快速增长,在内容层面也向着更加主流化、多元化的方向转变。

+

架构方面比较常见的有三种:

+
    +
  1. 单体应用
  2. +
+

所有的模块打包到一起部署运行,在开发小型项目上有独特优势:易于调试、部署,运维方便。缺点是容错性低,不可靠。只能通过运行更多的服务器水平扩展, 而不同的应用服务对资源的需求不同,且不可持续发展。

+
    +
  1. SOA面向服务架构
  2. +
+

面向服务架构是一种设计方法,设计上通常是自上而下的,服务间松散耦合。ESB集成不同协议的服务,做消息的转化、解释、路由从而联通各个服务,解决企业通信问题,服务松耦合、可扩展。缺点是SOA更多的面向企业服务,服务拆分粒度很大,更多的是为了复用。

+
    +
  1. 微服务
  2. +
+

微服务是去中心化的SOA的扩展,强调服务彻底的组件化,一个组件就是一个产品,服务切分力度更小,设计上更多的是自下而上的。服务间通过轻量级的协议进行通信,并根据服务本身需要独立化部署。从产品视角出发,更多聚焦可扩展性,兼顾可维护性。

+

综合上述几种服务的对比,我们最终选择了微服务架构,并使用下面的技术栈:

+
    +
  • 分布式中间件:Consul
  • +
  • 网关:Kong
  • +
  • 数据库:MySQL
  • +
  • orm框架:GORM
  • +
  • 缓存:Redis
  • +
  • 消息队列:RabbitMQ
  • +
  • 对象存储:七牛云对象存储Kodo
  • +
  • Web框架:Gin
  • +
  • RPC 框架:GRPC
  • +
  • 数据传输协议:protobuf
  • +
  • 用户鉴权中间件:JWT
  • +
  • 配置文件:viper
  • +
+

3.1.1 需求分析

+
一、用户模块
+

用户模块包括用户注册、用户登录和用户信息三个部分。

+
    +
  1. +
    用户注册接口 POST-/douyin/user/register/
    +
  2. +
+

新用户注册时提供用户名,密码,昵称即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token。

+

接口定义:

+
message douyin_user_register_request{
+    string username = 1; // 注册用户名,最长32个字符
+    string password = 2; // 密码,最长32个字符
+}
+
+message douyin_user_register_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    int64 user_id = 3; // 用户id
+    string token = 4; // 用户鉴权token
+}
+
    +
  1. +
    用户登录接口 POST-/douyin/user/login/
    +
  2. +
+

通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token

+

接口定义:

+
message douyin_user_login_request{
+    string username = 1; // 登录用户名
+    string password = 2; // 登录密码
+}
+
+message douyin_user_login_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    int64 user_id = 3; // 用户id
+    string token = 4; // 用户鉴权token
+}
+
    +
  1. +
    用户信息接口 GET-/douyin/user/
    +
  2. +
+

获取登录用户的 id、昵称,如果实现社交部分的功能,还会返回关注数和粉丝数。

+

接口定义:

+
message douyin_user_request{
+    int64 user_id = 1; // 用户id
+    string token = 2; // 用户鉴权token
+}
+
+message douyin_user_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    User user = 3; // 用户信息
+}
+
二、视频模块
+

视频模块包括包括视频Feed流获取、视频投稿和获取用户投稿列表三个模块

+
    +
  1. +
    视频流接口 GET-/douyin/feed/
    +
  2. +
+

不限制登录状态,返回按投稿时间倒序的视频列表,视频数由服务端控制,单次最多30个。

+

接口定义:

+
message douyin_feed_request{
+    int64 latest_time = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间
+    string token = 2;  // 可选参数,登录用户设置
+}
+
+message douyin_feed_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    repeated Video video_list = 3; // 视频列表
+    int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
+}
+
    +
  1. +
    发布列表接口 GET-/douyin/publish/list/
    +
  2. +
+

登录用户的视频发布列表,直接列出用户所有投稿过的视频。

+

接口定义:

+
message douyin_publish_list_request{
+    int64 user_id = 1; // 用户id
+    string token = 2; // 用户鉴权token
+}
+
+message douyin_publish_list_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+    repeated Video video_list = 3; // 用户发布的视频列表
+}
+
    +
  1. +
    视频投稿接口 POST-/douyin/publish/action/
    +
  2. +
+

登录用户选择视频上传。

+

接口定义:

+
message douyin_publish_action_request{
+    string token = 1; // 用户鉴权token
+    bytes data = 2; // 视频数据
+    string title = 3; // 视频标题
+}
+
+message douyin_publish_action_response{
+    int32 status_code = 1; // 状态码,0-成功,其他值-失败
+    string status_msg = 2; // 返回状态描述
+}
+
三、点赞模块
+
    +
  1. +
    点赞操作接口 POST-/douyin/favorite/action/
    +
  2. +
+

登录用户对视频进行点赞与取消点赞操作。

+

接口定义:

+
message douyin_favorite_action_request {
+   string token = 1; // 用户鉴权token
+   int64 video_id = 2; // 视频id
+   int32 action_type = 3; // 1-点赞,2-取消点赞
+}
+
+message douyin_favorite_action_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+}
+
    +
  1. +
    点赞列表接口 GET-/douyin/favorite/list/
    +
  2. +
+

登录用户的所有点赞视频。

+

接口定义:

+
message douyin_favorite_list_request {
+   int64 user_id = 1; // 用户id
+   string token = 2; // 用户鉴权token
+}
+
+message douyin_favorite_list_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+   repeated Video video_list = 3; // 用户点赞视频列表
+}
+
四、评论模块
+
    +
  1. +
    评论操作接口 POST-/douyin/comment/action/
    +
  2. +
+

登录用户对视频进行评论。

+

接口定义:

+
message douyin_comment_action_request {
+   string token = 1; // 用户鉴权token
+   int64 video_id = 2; // 视频id
+   int32 action_type = 3; // 1-发布评论,2-删除评论
+   string comment_text = 4; // 用户填写的评论内容,在action_type=1的时候使用
+   int64 comment_id = 5; // 要删除的评论id,在action_type=2的时候使用
+}
+
+message douyin_comment_action_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+   Comment comment = 3; // 评论成功返回评论内容,不需要重新拉取整个列表
+}
+
    +
  1. +
    视频评论列表接口 GET-/douyin/comment/list/
    +
  2. +
+

查看视频的所有评论,按发布时间倒序。

+

接口定义:

+
message douyin_comment_list_request {
+   string token = 1; // 用户鉴权token
+   int64 video_id = 2; // 视频id
+}
+
+message douyin_comment_list_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+   repeated Comment comment_list = 3; // 评论列表
+}
+
五、关注模块
+
    +
  1. +
    关注操作接口 POST-/douyin/relation/action/
    +
  2. +
+

登录用户对其他用户进行关注或取消关注。实现用户之间的关注关系维护,登录用户能够关注或取关其他用户,同时自己能够看到自己关注过的所有用户列表,以及所有关注自己的用户列表。

+

接口定义:

+
message douyin_favorite_list_request {
+   int64 user_id = 1; // 用户id
+   string token = 2; // 用户鉴权token
+}
+
+message douyin_favorite_list_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+   repeated Video video_list = 3; // 用户点赞视频列表
+}
+
    +
  1. +
    用户关注列表 GET-/douyin/relatioin/follow/list/
    +
  2. +
+

登录用户关注的所有用户列表。

+
message douyin_favorite_list_request {
+   int64 user_id = 1; // 用户id
+   string token = 2; // 用户鉴权token
+}
+
+message douyin_favorite_list_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+   repeated Video video_list = 3; // 用户点赞视频列表
+}
+
    +
  1. +
    用户粉丝列表 GET-/douyin/relation/follower/list/
    +
  2. +
+

所有关注登录用户的粉丝列表。

+
message douyin_favorite_list_request {
+   int64 user_id = 1; // 用户id
+   string token = 2; // 用户鉴权token
+}
+
+message douyin_favorite_list_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+   repeated Video video_list = 3; // 用户点赞视频列表
+}
+
    +
  1. +
    用户好友列表 GET-/douyin/relation/friend/list/
    +
  2. +
+

互相关注的用户列表。

+
message douyin_favorite_list_request {
+   int64 user_id = 1; // 用户id
+   string token = 2; // 用户鉴权token
+}
+
+message douyin_favorite_list_response {
+   int32 status_code = 1; // 状态码,0-成功,其他值-失败
+   string status_msg = 2; // 返回状态描述
+   repeated Video video_list = 3; // 用户点赞视频列表
+}
+
六、消息模块
+

客户端通过定时轮询服务端接口查询消息记录

+
    +
  1. +
    聊天记录 GET-/douyin/message/chat/
    +
  2. +
+

当前登录用户和其他指定用户的聊天消息记录

+
message douyin_message_chat_request{
+    required string token=1;//用户鉴权token
+    required int64 to_user_id=2;//对方用户id
+    required int64 pre_msg_time=3;//上次最新消息的时间
+}
+
+message douyin_message_chat_response {
+    required int:32 status_code=1;//状态码,g-成功,其他值-失败
+    optional string status._msg=2;//返回状态描述
+    repeated Message message_list=3;//消息列表
+}
+message Message{
+    required int64 id=1;//消息id
+    required int64 to_user_id=2;//该消息接收者的d
+    required int64 from_user_id=3;//该消息发送者的id
+    required string content=4;//消息内容
+    optional int64 create_time=5;//消息创建时间
+}
+
    +
  1. +
    消息操作 POST-/douyin/message/action/
    +
  2. +
+

登录用户对消息的相关操作,目前只支持消息发送

+
message douyin_relation_action_request{
+    required string token=1;//用户鉴权token
+    required int64 to_user_id=2;//对方用户id
+    required int32 action_type=3;//1-发送消息
+    required string content=4;//消息内容
+}
+message douyin_relation_action_response{
+    required int32 status._code=1;//状态码,g-成功,其他值-失败
+    optional string status_msg=2;//返回状态描述
+}
+

3.2 架构设计

+

+

运行流程:

+
    +
  1. 后端服务启动,根据注册中心consul的地址(1.1.1.1:8500),将自己注册到注册中心 。
  2. +
  3. 客户端访问域名,根据解析找到kong网关地址(2.2.2.2:8000)。
  4. +
  5. kong网关根据客户端传过来的服务名匹配到对应的Routes,再根据Routes找到对应的Service details 。
  6. +
  7. 然后拿着Service details里面配置Host,去找consul地址(1.1.1.1:8600)。
  8. +
  9. 根据名称查询consul的dns表,进而找到对应的ip+端口 。
  10. +
  11. 找到对应的服务,然后通信。
  12. +
+

3.2.1 用户模块

+
1. 整体架构设计
+

+
2. 详细设计
+
2.1. 用户注册
+

+

用户注册的逻辑比较简单,请求的参数中只包含用户的用户名与密码,不支持手机注册以及各种验证码操作。因此用户的唯一识别信息为用户名。如果数据库中存在相同的用户名,则认为这个用户已经存在,拒绝注册;否则则允许用户注册,并在数据库中分配给这个用户唯一的id。最后调用JWT生成Token返回响应,作为在Token生效期间的用户的唯一标识符。

+

用户注册流程:

+
    +
  1. DY-api.UserRegister处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserRegister
  2. +
  3. 服务端根据用户名查询数据库,如果发现重名用户名,则直接返回错误
  4. +
  5. 未发现重名用户名,则通过md5加盐(用户名)对密码进行加密,加密后插入数据库,数据库返回唯一自增ID
  6. +
  7. 服务端返回成功响应给DY-api.UserRegister
  8. +
  9. DY-api.UserRegister利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
  10. +
+
2.2. 用户登录
+

+

用户登录请求的参数中只包含用户的用户名与密码,不支持手机登录以及各种验证码操作。因此用户的唯一识别信息为用户名。如果数据库中不存在相同的用户名,则认为这个用户不存在,拒绝登录;否则则允许用户登录,并返回数据库中这个用户的唯一id。同时调用JWT生成Token返回响应,作为在Token生效期间的用户的唯一标识符。

+

用户登录流程:

+
    +
  1. DY-api.UserLogin处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserLogin
  2. +
  3. 服务端根据用户名查询数据库,如果未发现相同用户名,则直接返回错误,否则返回通过用户名查询出来的用户id和密码
  4. +
  5. 对用户输入的密码进行md5加盐(用户名)加密,与上一步返回的密码进行比较,如果不匹配直接返回错误
  6. +
  7. 密码匹配,则服务端返回成功响应给DY-api.UserLogin
  8. +
  9. DY-api.UserLogin利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
  10. +
+
2.3. 用户信息
+

+

用户信息请求的参数包括要请求的用户的id和当前登录的用户的Token。返回的用户信息应该包括用户的名称,用户的关注人数和粉丝人数,以及用户与当前登录用户的关注关系。因此除了调用DY-api.UserInfo获取用户的基本信息之外,还需要调用DY-srv.GetFollowList与DY-srv.GetFollowerList获取用户的关注人和用户的粉丝列表。两个Count数值可以通过查看切片的大小获得,关注关系需要遍历切片进行搜索。

+

在对不同的服务进行调用的时候采取并行调用的方式,服务全部返回后在api层进行拼接,从而提高效率。

+

用户信息流程:

+
    +
  1. DY-api.UserInfo处理请求,将请求中带有的id字段传递到服务端DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList
  2. +
  3. 并行请求三个服务,其中DY-srv.UserInfo根据id字段查询数据库,如果id有效,则返回用户姓名,否则返回错误
  4. +
  5. 等待三个服务全部成功返回后,填充响应中的User的五个字段 +
      +
    1. id与name字段通过DY-srv.UserInfo的响应直接获取
    2. +
    3. followcount通过获取DY-srv.GetFollowList返回的切片长度获取
    4. +
    5. followercount通过获取DY-srv.GetFollowerList返回的切片长度获取
    6. +
    7. 通过Token获取当前的登录用户id,在DY-srv.GetFollowerList切片内部查询,如果查询到为True,否则为False
    8. +
    +
  6. +
  7. 构建响应结构体并返回给客户端
  8. +
+

3.2.2 视频模块

+
1. 整体架构设计
+

img

+
2. 详细设计
+
2.1. 视频流
+

+

获取视频流的请求参数包括视频的最新时间和当前用户的Token信息。如果当前用户在登录的状态下请求视频流,则通过最新时间在数据库中查询前30个视频的信息,包括视频本身的id和作者的id。获得最多30个视频的信息后,创建多个协程(视频数量个数),根据每个id获得视频的其他信息,如作者的详细信息,视频的点赞数量,作者的关注人数等等。在调用不同服务的时候也采用并行调用的方式。最后对返回的全部响应进行组织返回。

+

如果用户没有登录,则Token信息为空,那么返回的响应中缺少一些交互信息,如当前用户是否已经对当前视频点赞等等。

+

获取视频流流程:

+
    +
  1. +

    DY-api.Feed处理请求,准备请求服务

    +
  2. +
  3. +

    首先请求DY-srv.Feed服务,根据时间戳查询数据库,查询出不超过时间戳的前30个视频,查询后返回视频列表

    +
  4. +
  5. +

    随后并行请求视频列表中的每一个视频(即最大并发数为30)

    +
  6. +
  7. +

    对每一个视频,根据前一个服务响应的作者的id并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录Author响应相关的5个字段

    +
  8. +
  9. +

    对每一个视频,根据视频id并行请求DY-srv.douyinCommentList和DY-srv.douyinLikeVideo,对于每个视频

    +
      +
    1. commentCount通过获取DY-srv.douyinCommentList返回的切片长度获取
    2. +
    3. favoriteCount通过获取DY-srv.douyinLikeVideo返回的切片长度获取
    4. +
    5. 通过Token获取当前的登录用户id,在DY-srv.douyinLikeVideo切片内部查询,如果查询到为True,否则为False
    6. +
    +
  10. +
  11. +

    等待全部的视频返回响应后,构建响应结构体并返回给客户端

    +
  12. +
+
2.2. 发布列表
+

+

获取用户视频发布列表的请求参数包括用户的id和当前用户的Token信息。两者不一定是相同的用户,因为用户在观看视频的同时点击用户头像即可以看到这个视频作者的信息和作者的视频发布列表。

+

如果当前用户是查看自己的视频发布列表,则通过用户的id在数据库中查询发布的视频的信息。获得最多视频的信息后,创建多个协程(视频数量个数),根据每个id获得视频的其他信息,如视频的点赞数量,作者的关注人数等等。在调用不同服务的时候也采用并行调用的方式。最后对返回的全部响应进行组织返回。

+

如果Token信息为空,则当前场景是用户查看其他用户的发布视频列表。那么返回的响应中缺少一些交互信息,如当前用户是否已经对当前作者的视频点赞等等。

+

获取视频发布列表流程:

+
    +
  1. +

    DY-api.PublishList处理请求,准备请求服务

    +
  2. +
  3. +

    首先请求DY-srv.PublishList服务,根据id查询数据库,如果id在数据库中不存在,则直接返回错误,然后根据用户id查询发布的视频列表并返回

    +
  4. +
  5. +

    随后并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录User响应相关的5个字段

    +
  6. +
  7. +

    对每一个视频,根据视频id并行请求DY-srv.douyinCommentList和DY-srv.douyinLikeVideo,对于每个视频

    +
      +
    1. commentCount通过获取DY-srv.douyinCommentList返回的切片长度获取
    2. +
    3. favoriteCount通过获取DY-srv.douyinLikeVideo返回的切片长度获取
    4. +
    5. 通过Token获取当前的登录用户id,在DY-srv.douyinLikeVideo切片内部查询,如果查询到为True,否则为False
    6. +
    +
  8. +
  9. +

    等待全部的视频返回响应后,构建响应结构体并返回给客户端

    +
  10. +
+
2.3. 视频投稿
+

+

视频投稿的请求参数中包括用户的Token,上传的视频流数据以及视频的标题。其中视频流是用户从本地上传得到的,视频的标题是用户自行输入得到的。上传视频必须是在登录的状态下,因此必须包含用户的Token信息。获得参数后,根据Token信息解析出当前用户的id,然后根据用户id判断是否存在这个用户的文件夹。如果不存在文件夹则新建用户文件夹。创建文件夹后将视频流写入这个文件夹下的视频文件,同时调用ffmpeg对视频的封面进行截取从而获得视频的首图。确认视频文件与图片文件都保存在本地后,构建返回的响应,并将上传文件的消息推送到消息队列中,此时消息队列将视频文件和图片文件异步上传到对象存储当中,上传结束后将视频信息写入数据库,在下次请求视频流的过程中就可以请求到这个视频了。

+

其中使用RabbitMQ进行异步处理,在服务器带宽有限的情况下,上传视频对用户来说基本无感,增加了用户的体验。且上传到对象存储后视频和图片的展示和下载速度也会更快,方便用户查看视频。

+

视频投稿流程:

+
    +
  1. DY-api.PublishAction处理请求,将请求中的字段传递到服务端DY-srv.PublishAction
  2. +
  3. 服务端从Token中获取id信息,如果无法获取id,直接返回错误
  4. +
  5. 服务端根据id信息查询数据库,获取用户信息,如果id并不存在于数据库,则直接返回错误
  6. +
  7. 服务端判断本地存放视频与图片文件的文件夹是否存在,如果不存在则创建文件夹
  8. +
  9. 服务端将接收到的请求中的字节流写入文件,并调用ffmpeg对视频的第一帧进行截图作为封面,同样写入图片文件
  10. +
  11. 服务端将文件上传信息传递给消息队列,直接返回成功响应给客户端
  12. +
  13. 消息队列接收到消息后并行上传视频和图片文件,两者都上传成功后将视频信息写入数据库
  14. +
+

3.2.3 点赞模块

+
1. 整体架构设计
+

img

+
2. 详细设计
+
2.1 点赞操作
+

点赞操作分为对未点赞的视频点赞以及对已点赞的视频取消点赞。点赞操作接口的请求参数包括,用户token;视频id;操作类型(1–点赞,2–取消点赞)。通过解析用户token可获得用户id。构建一个redis集合,将用户已经点赞的视频将其按照k-v形式存入redis。

+

2.1.1 对视频点赞

+

当请求参数操作类型的值为1时,即为点赞操作,点赞操作是要对用户未点赞的视频进行点赞,首先在redis集合中查询该用户是否对此视频点赞过,若点赞过则返回视频已点赞,若未点赞,则将该条点赞记录先插入redis再插入数据库中,最后返回成功的响应码。

+

2.1.2 对视频取消点赞

+

当请求参数操作类型的值为2时,即为取消点赞操作,取消点赞操作是要对用户点赞的视频进行取消,首先在redis集合中查询该用户是否对此视频点赞过,若未点赞过则返回视频暂未点赞,若点赞了,则将该条点赞记录先从redis中删除再从数据库中删除,最后返回成功的响应码。

+
2.2 喜欢列表
+

喜欢列表接口的请求参数为用户id和用户token,先根据token验证用户身份与登录状态,若成功,则根据用户id查询用户的喜欢列表,将喜欢列表封装进响应结构体中,返回参数中还需要视频相关信息,通过调用视频服务接口,获取视频相关信息,并封装到响应结构体中,最终将响应结构体返回。

+

3.2.4 评论模块

+
1. 整体架构设计
+

img

+
2. 详细设计
+
2.1 评论操作
+

评论操作分为发表评论和删除评论,评论操作接口的请求参数包括用户token,视频id,操作类型(1–发表评论,2–删除评论),评论内容(发表评论时),评论id(删除评论时)。首先根据token验证用户身份与登录状态,若成功,则解析token获取用户id。

+

2.1.1 发表评论

+

当操作类型等于1时,表示是发表评论,将对应评论内容,用户id,视频id,添加进数据库,并且将评论列表封装进响应结构体,同时调用社交服务,获取对应的用户信息,将用户信息也封装进响应结构体,最后将其返回。

+

2.1.2 删除评论

+

当操作类型等于2时,表示是删除评论,将评论id对应的数据从数据库中删除,并返回删除成功的信息。

+
2.2 评论列表
+

评论列表接口的请求参数为视频id和用户token,先根据token验证用户身份与登录状态,若成功,则根据视频id查询视频的评论列表,将评论列表封装进响应结构体中,返回参数中还需要用户相关信息,通过调用社交服务接口,获取用户相关信息,并封装到响应结构体中,最终将响应结构体返回。

+

3.2.5 社交模块

+

社交模块的整体设计如下图:

+

+

其中 social-api程序是使用Gin框架搭建的Web服务。主要接受url请求,通过路由绑定handler处理函数,添加授权中间件。social-api部署了多个,并将自己注册在Consule服务上,支持负载均衡,并通过服务发现调用gRPC服务。

+

social-srv是业务处理代码,主要和MySQL数据库打交道。social-srv可以部署在多个不同服务器上,并将自己注册到Consul上来实现负载均衡,提供被其他服务发现。

+

详细设计:

+
    +
  1. 关注模块
  2. +
+

关注接口的请求参数为用户ID和被关注的用户ID,先根据token验证用户身份与登录状态,若成功,则向数据库插入数据,同时互相关注的用户会成为朋友,在朋友界面显示朋友列表,并展现最近的一条消息。用户也可以在信息详情页面来查看关注的用户和粉丝。

+
    +
  1. 消息模块
  2. +
+

通过用户ID和朋友ID可以新增一条消息。使用定时调用接口的方式来获取消息。

+

3.3 数据库设计

+

+

3.3.1 videos表

+

字段如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称类型说明
idbigint视频唯一id,自增主键
author_idbigint视频作者id
file_namevarchar文件名称
publish_timebigint发布时间
titlevarchar视频标题
+

索引设置:

+
    +
  1. 视频唯一id的自增主键索引
  2. +
  3. 发布时间的索引,用户在数据库中查询指定时间范围的视频
  4. +
  5. 作者id的索引,用于查询指定作者的视频列表
  6. +
+

3.3.2 users表

+ + + + + + + + + + + + + + + + + + + + + + + + + +
名称类型说明
idbigint用户id,自增主键
namevarchar用户名
passwordvarchar用户密码
+

索引设置:

+
    +
  1. 用户id的自增主键索引
  2. +
  3. 用户名与密码的联合索引,用于在数据库中匹配用户
  4. +
+

3.3.3 comments表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称类型说明
idbigint评论唯一id,自增主键
user_idbigint评论发布者的id
video_idbigint评论发布位置的视频id
comment_textvarchar评论内容
create_timedatetime评论创建时间
+

索引设置:

+
    +
  1. 评论id的自增主键索引
  2. +
  3. 视频id的索引,用于在数据库中查询某条视频对应的评论内容
  4. +
+

3.3.4 follows表

+ + + + + + + + + + + + + + + + + + + + + + + + + +
名称类型说明
idbigint关注关系id,自增主键
user_idbigint用户id
follower_idbigint关注的用户id
+

索引设置:

+
    +
  1. 关注关系id的自增主键索引
  2. +
  3. 用户id和关注的用户id的联合索引,用于在数据库中查询两个用户之间的关注关系
  4. +
  5. 关注的用户id索引,用于在数据库中查询用户的关注关系
  6. +
+

3.3.5 likes表

+ + + + + + + + + + + + + + + + + + + + + + + + + +
名称类型说明
idbigint喜欢关系id,自增主键
user_idbigint点赞用户的id
video_idbigint被点赞的视频id
+

索引设置:

+
    +
  1. 喜欢关系id的自增主键索引
  2. +
  3. 用户和点赞视频的联合索引,用于在数据库中查询某个用户是否对某个视频点赞
  4. +
  5. 用户id索引,用于在数据库中查询某个用户的点赞的视频的id
  6. +
  7. 视频id索引,用于在数据库中查询某个视频的点赞用户的id
  8. +
+

3.3.6 messages表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称类型说明
idbigint消息唯一id,自增主键
user_idbigint发送消息的用户id
to_user_idbigint接收消息的用户id
sent_timedatetime消息发送时间
contentvarchar消息内容
+

索引设置:

+
    +
  1. 消息id的自增主键索引
  2. +
  3. 发送用户id索引,用于查询数据库中指定的用户发送的消息
  4. +
  5. 接收用户id索引,用于查询数据库中指定的用户接收的消息
  6. +
+

3.4 项目代码介绍

+

后端项目总体分为两个大部分:

+
    +
  1. web项目(simple-DY/DY-api/):使用Gin框架来获取用户请求,连接GRPC远程调用服务,最后返回数据。
  2. +
  3. service项目(simple-DY/DY-srvs/):GRPC编写的微服务。
  4. +
+

项目的总体结构如下所示:

+
├── simple-DY
+│   ├── db.sql        // 数据库初始化文件
+│   ├── DY-api           // web项目
+│   │   ├── interact-web    // 互动模块
+│   │   ├── social-web      // 社交模块
+│   │   └── video-web       // 视频模块
+│   ├── DY-srvs          // service项目
+│   │   ├── interact-srv    // 互动模块
+│   │   ├── social-srv      // 社交模块
+│   │   └── video-srv       // 视频模块
+│   ├── go.mod
+│   ├── go.sum
+│   └── README.md
+

3.4.1 video服务(包括视频模块和用户模块)

+
    +
  1. api层
  2. +
+

代码结构:

+
video-web
+├── api
+│   ├── base.go
+│   ├── feed.go
+│   ├── info.go
+│   ├── otherapi.go
+│   ├── publishaction.go
+│   ├── publishlist.go
+│   ├── userinfo.go
+│   ├── userlogin.go
+│   └── userregister.go
+├── config
+│   └── config.go
+├── config-debug.yaml
+├── config-pro.yaml
+├── global
+│   └── global.go
+├── initialize
+│   ├── config.go
+│   ├── logger.go
+│   ├── router.go
+│   ├── srv_conn.go
+│   └── validator.go
+├── logs
+│   └── video-web.log
+├── main.go
+├── middlewares
+│   ├── cors.go
+│   └── jwt.go
+├── models
+│   ├── base.go
+│   ├── jwt.go
+│   ├── other.go
+│   ├── request.go
+│   └── response.go
+├── proto
+│   ├── simpledy_grpc.pb.go
+│   ├── simpledy.pb.go
+│   └── simpledy.proto
+├── README.md
+└── utils
+    └── consul
+        └── register.go
+

详细说明:

+
    +
  • api:编写路由的Handler处理函数
  • +
  • config:读取yaml文件时的接收结构体
  • +
  • *.yaml:配置文件 +
      +
    • config-debug.yaml:线下开发使用的配置文件
    • +
    • config-pro.yaml: 线上配置文件
    • +
    +
  • +
  • global:存放全局变量,例如config信息,连接信息等
  • +
  • initialize:初始化程序代码 +
      +
    • config.go:读取配置文件
    • +
    • logger.go:日志配置
    • +
    • router.go:gin路由
    • +
    • srv_conn.go:连接微服务
    • +
    • validator.go:翻译器
    • +
    +
  • +
  • logs:日志文件
  • +
  • main.go:主程序入口
  • +
  • middlewares:gin的自定义中间件 +
      +
    • cors.go:跨域中间件
    • +
    • jwt.go:JWT中间件
    • +
    +
  • +
  • models:用户请求参数的结构体
  • +
  • proto:编写和生成proto文件
  • +
  • README.md:说明文件
  • +
  • utils:工具类 +
      +
    • consul:调用consul api进行服务注册发现等操作
    • +
    +
  • +
+
    +
  1. srv层
  2. +
+

代码结构:

+
.
+├── config
+│   └── config.go
+├── config-debug.yaml
+├── config-pro.yaml
+├── global
+│   └── global.go
+├── handler
+│   ├── base.go
+│   ├── feed.go
+│   ├── publishaction.go
+│   ├── publishlist.go
+│   ├── userinfo.go
+│   ├── userlogin.go
+│   ├── userregister.go
+│   └── videoinfo.go
+├── initialize
+│   ├── config.go
+│   ├── db.go
+│   ├── handler.go
+│   └── logger.go
+├── logs
+│   └── video-srv.log
+├── main.go
+├── models
+│   ├── base.go
+│   └── db.go
+├── proto
+│   ├── simpledy_grpc.pb.go
+│   ├── simpledy.pb.go
+│   └── simpledy.proto
+├── README.md
+└── utils
+    ├── backup
+    │   └── backup.go
+    ├── consul
+    │   └── register.go
+    ├── dao
+    │   ├── followdao.go
+    │   ├── userdao.go
+    │   └── videodao.go
+    ├── ffmpeg
+    │   └── extractFirstFrame.go
+    ├── freeport
+    │   └── port.go
+    ├── jwt
+    │   └── token.go
+    ├── md5salt
+    │   └── md5.go
+    ├── oss
+    │   └── upload.go
+    └── rabbitmq
+        ├── base.go
+        ├── consumer.go
+        └── producer.go
+

详细说明:

+
    +
  • config:读取yaml文件时的接收结构体
  • +
  • *.yaml:配置文件 +
      +
    • config-debug.yaml:线下开发使用的配置文件
    • +
    • config-pro.yaml: 线上配置文件
    • +
    +
  • +
  • global:存放全局变量,例如config信息,连接信息等
  • +
  • handler:主要的逻辑代码,proto的service的实现类
  • +
  • initialize:初始化程序代码 +
      +
    • config.go:读取配置文件
    • +
    • db.go:数据库全局连接
    • +
    • handler.go:监听客户端连接
    • +
    • logger.go:日志配置
    • +
    +
  • +
  • logs:日志文件
  • +
  • main.go:主程序入口
  • +
  • models:用户请求参数的结构体
  • +
  • proto:编写和生成proto文件
  • +
  • README.md:说明文件
  • +
  • utils:工具类 +
      +
    • backup:备份用户上传的视频和图片文件
    • +
    • consul:调用consul api进行服务注册发现等操作
    • +
    • dao:数据库相关操作
    • +
    • ffmpeg:视频首页截图
    • +
    • freeport:获取空闲网络端口
    • +
    • jwt:鉴权Token的生成与解析
    • +
    • md5salt:密码加密存储
    • +
    • oss:七牛云对象存储相关操作
    • +
    • rabbitmq:消息队列相关操作
    • +
    +
  • +
+

3.4.2 interact服务(包括点赞模块和评论模块)

+
    +
  1. api层
  2. +
+
interact-web
+├── api
+│   ├── base.go
+│   ├── comment.go
+│   └── like.go
+├── config
+│   └── config.go
+├── global
+│   └── global.go
+├── initialize
+│   ├── config.go
+│   ├── logger.go
+│   ├── router.go
+│   ├── srv_conn.go
+│   └── validator.go
+├── main.go
+├── middlewares
+│   ├── cors.go
+│   └── jwt.go
+├── models
+│   └── request.go
+├── proto
+│   ├── simpledy_grpc.pb.go
+│   ├── simpledy.pb.go
+│   └── simpledy.proto
+├── router
+│   ├── comment.go
+│   └── like.go
+└── utils
+    └── register
+        └── consul
+            └── register.go
+
    +
  1. srv层
  2. +
+
interact-srv
+├── build.sh
+├── config
+│   └── config.go
+├── global
+│   └── global.go
+├── handler
+│   └── interact.go
+├── initalize
+│   ├── config.go
+│   ├── db.go
+│   ├── logger.go
+│   ├── rdb.go
+│   └── srvs_conn.go
+├── main.go
+├── model
+│   ├── base.go
+│   ├── comment.go
+│   ├── like.go
+│   └── video.go
+├── proto
+│   ├── simpledy_grpc.pb.go
+│   ├── simpledy.pb.go
+│   └── simpledy.proto
+└── utils
+    ├── addr.go
+    ├── jwt
+    │   └── token.go
+    ├── key
+    │   └── key.go
+    └── register
+        └── consul
+            └── register.go
+

3.4.3 social服务(包括关注模块和消息模块)

+
    +
  1. api层
  2. +
+
social-web
+├── api
+│   ├── base.go
+│   ├── message.go
+│   └── relation.go
+├── config
+│   └── config.go
+├── config-debug.yaml
+├── config-pro.yaml
+├── forms
+│   ├── message.go
+│   └── relation.go
+├── global
+│   └── global.go
+├── initialize
+│   ├── config.go
+│   ├── logger.go
+│   ├── router.go
+│   ├── srv_conn.go
+│   └── validator.go
+├── main.go
+├── middlewares
+│   ├── cors.go
+│   └── jwt.go
+├── models
+│   └── request.go
+├── proto
+│   ├── simpledy_grpc.pb.go
+│   ├── simpledy.pb.go
+│   └── simpledy.proto
+├── router
+│   ├── message.go
+│   └── relation.go
+└── utils
+    ├── addr.go
+    └── register
+        └── consul
+            └── register.go
+
    +
  1. srv层
  2. +
+
social-srv
+├── build.sh
+├── config
+│   └── config.go
+├── config-debug.yaml
+├── config-pro.yaml
+├── global
+│   └── global.go
+├── handler
+│   └── social.go
+├── initialize
+│   ├── config.go
+│   ├── db.go
+│   └── logger.go
+├── main.go
+├── model
+│   └── base.go
+└── proto
+    ├── simpledy_grpc.pb.go
+    ├── simpledy.pb.go
+    └── simpledy.proto
+

四、测试结果

+

4.1 功能测试

+

通过Apifox的自动化测试,构建不同实际使用中可能遇到的情况,对接口进行充分测试。

+

1. 用户注册接口 /douyin/user/register/

+

需要对如下的用例进行测试:

+
    +
  1. 注册不存在的用户名-返回成功响应
  2. +
  3. 注册已经存在的用户名-返回失败响应
  4. +
+

测试结果:

+

img

+

2. 用户登录接口 /douyin/user/login/

+

需要对如下的用例进行测试:

+
    +
  1. 登录已经存在的用户名且密码正确-返回成功响应
  2. +
  3. 登录不存在的用户名-返回失败响应
  4. +
  5. 登录已经存在的用户名,但是密码错误-返回失败响应
  6. +
+

测试结果:

+

img

+

3. 用户信息接口 /douyin/user/

+

需要对如下的用例进行测试:

+
    +
  1. 用户id存在且Token正确-返回成功响应
  2. +
  3. 用户id存在但Token为空或不正确-返回成功响应(但是没有是否关注与是否点赞等关系信息)
  4. +
  5. 用户id不存在-返回失败响应
  6. +
+

测试结果:

+

img

+

4. 视频流接口 /douyin/feed/

+

需要对如下的用例进行测试:

+
    +
  1. 未登录用户请求视频流(包括Token错误的情况)-返回成功响应(但是缺少是否对视频点赞等关系信息)
  2. +
  3. 登录用户请求视频流-返回完整的成功响应
  4. +
+

测试结果:

+

img

+

5. 发布列表接口 /douyin/publish/list/

+

需要对如下的用例进行测试:

+
    +
  1. 用户id存在且Token正确-返回成功响应
  2. +
  3. 用户id存在但Token为空或不正确-返回成功响应(但是没有是否点赞等关系信息)
  4. +
  5. 用户id不存在-返回失败响应
  6. +
+

测试结果:

+

img

+

6. 视频投稿接口 /douyin/publish/action/

+

需要对如下的用例进行测试:

+
    +
  1. 正常上传视频-返回成功响应
  2. +
  3. Token为空或Token不正确-返回错误响应
  4. +
+

测试结果:

+

img

+

7. 社交模块

+

img

+

8. 互动模块

+

+

4.2 性能测试

+

+

五、其他资料

+

接口文档(旧版)

+

汇报文档

+

课程汇总

+

抖音项目方案说明

+

极简抖音App使用说明

+

青训营大项目答疑

+

六、项目总结与反思

+

1. 目前仍存在的问题

+
    +
  • 在视频模块中,上传视频的大小有限制,如果超过了限制会返回网络错误,无法将视频字节流传递到服务器端。
  • +
  • 观看视频时,一个服务器的宽带顶不住,有点卡。
  • +
  • 获取消息的API由于是定时查询,消息会重叠。
  • +
  • 若出现对短时间内一个视频进行大量点赞操作,写入数据库操作会太频繁,可以考虑将点赞记录进行定期写入数据库。
  • +
+

2. 已识别出的优化项

+
    +
  • 视频模块中可以对用户的视频习惯进行分类,每一次获取视频流的时候对用户进行视频推荐
  • +
  • 用户模块可以增加邮箱或手机号等验证方式,并添加密码找回的功能,增加安全性
  • +
  • 粉丝列表、用户的聊天记录、关注列表和朋友列表可以使用Redis的List数据结构来存储,来降低MySQL的压力
  • +
  • 用户聊天的消息推送可以使用websocket长连接来避免每次建立链接释放链接所消耗的资源。
  • +
  • 用户聊天的消息推送可以使用MQ消息队列来实现,不查表可以减低MySQL压力和消息的实时性。
  • +
  • 点赞功能将点赞记录存在redis中,减少数据库查询压力。
  • +
+

3. 架构演进的可能性

+
    +
  • 微服务基本根据路由进行拆分,拆分不够合理,服务之间耦合的地方稍多。后续可以将微服务进行进一步拆分,真正做到将所有的功能打包成独立的单元。
  • +
  • 可以从微服务架构演进为Serverless。Serverless是一种构建和管理基于微服务架构的完整流程,允许你在服务部署级别而不是服务器部署级别来管理你的应用部署。它与传统架构的不同之处在于,完全由第三方管理,由事件触发,存在于无状态(Stateless)、暂存(可能只存在于一次调用的过程中)计算容器内。构建无服务器应用程序意味着开发者可以专注在产品代码上,而无须管理和操作云端或本地的服务器或运行时。Serverless真正做到了部署应用无需涉及基础设施的建设,自动构建、部署和启动服务。
  • +
+

4. 项目过程中的反思与总结

+

在参加青训营期间,官方提供了全面的课程,涵盖了创作技巧、内容制作、问题分析等多个方面。这些课程不仅提供了实用的知识和技能,还可以让我们更好地理解抖音平台和用户需求。抖音青训营项目还提供了多种资源支持,包括专业导师、团队合作等。这些资源可以帮助我们更好地实践和落地自己的创意。

+

回顾整个项目的过程,我们团队做了如下总结:

+
    +
  • 在代码编写的过程中,保持良好的编码规范不仅对自己以后复习代码节省时间,同事对代码的理解也会更方便。
  • +
  • 在实践中学习新的知识和技能。
  • +
  • 好记性不如烂笔头。伴学笔记的习惯值得我们继续保持。
  • +
  • 在协作开发中,团队的活力来源于不断的交流。通过交流和合作,我们学到了很多新的创作思路和理念。
  • +
+

七、参考资料

+

https://grpc.io/

+

https://www.jianshu.com/p/4e4ff6be6af9

+

https://www.apifox.cn/apidoc/shared-09d88f32-0b6c-4157-9d07-a36d32d7a75c/api-50707523

+

https://juejin.cn/post/7174037539345399839

+

https://blog.csdn.net/cc18868876837/article/details/90672971

+

https://www.woshipm.com/evaluating/1552722.html

+ + +
+ +
+
+ + + + + + +
+
+
字节跳动青训营-抖音项目
+
https://zhangzhao219.github.io/2023/03/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年3月3日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/03/19/diary/diary20230319/index.html b/2023/03/19/diary/diary20230319/index.html new file mode 100644 index 000000000..ececf9039 --- /dev/null +++ b/2023/03/19/diary/diary20230319/index.html @@ -0,0 +1,736 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20230319 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20230319

+ + +
+ +

2023年3月19日,周日

+ +

回了一次家,但是实际上回家并不是我先提出的(虽然打过了回家的招呼),主要是张曦元提出的(虽然她说了后直接撤回了,我再问才告诉我)。

+

虽然我的感情生活并不是很顺利,但是似乎从来不缺少聊的比较好的女同学。大一的时候是这样,研一的时候也是这样。这次实在是太热情了,我也是完全想不到,一个之前几乎没有说过话的女生,也仅仅在班里不到一年的时间,而且还是一个绝对的大美女,对我还如此感兴趣。甚至在没有怎么访问我的空间的情况下知道我的一些小事,还有我的程序设计竞赛的奖项,这个我自己从网络上都搜索不到。

+

感觉她对我来说是一个黑盒子,但是她已经得知我的很多事情了,但是我们都避开了个人感情方面,甚至她对我们共同同学的谈论兴致也不是很高。最让我惊奇的一点是上车后几乎没有看过手机,这个我觉得实在是太出乎意料了,这个评价一个人是否对你有兴趣是很关键的一个点(前女友就是这样引起我的注意的)。本来也是想问问杨青默的,可是似乎并没有给我这个机会。

+

很热情,说了很多东西,但是感觉有点缺少感觉,似乎只是很好的朋友关系,但是为什么突然就变成很好的朋友了呢?为什么初次见面的时候她完全了解我,但是我却连她本科去了哪里都不知道。反复想请我吃饭,但是我一直在拒绝,也是我不太敢吧。我还是没有从上一段感情中走出来,这种过分的热情让我暂时无法承受。

+

我甚至问了问chatgpt,它的回答和我想得差不多,就慢慢来慢慢培养,平时若有若无关心一下,主要看她的反应。虽然是个大美女,但是看起来她的社交圈也不是非常广泛的样子,可以慢慢来,毕竟之后能创造见面的机会还有很多,我也可以稍微主动一些,请她到望京附近转转之类的。

+

虽然矮,但是并不能成为自卑的理由,还是要多学知识,多看书,争取能配得上人家。慢慢加油吧,你已经不是情窦初开的小孩子了。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20230319
+
https://zhangzhao219.github.io/2023/03/19/diary/diary20230319/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年3月19日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/03/26/diary/diary20230326/index.html b/2023/03/26/diary/diary20230326/index.html new file mode 100644 index 000000000..b62a89086 --- /dev/null +++ b/2023/03/26/diary/diary20230326/index.html @@ -0,0 +1,734 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20230326 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20230326

+ + +
+ +

2023年3月26日,周日

+ +

最重要的,感情,一周没有任何交流,尝试着发了一条朋友圈,晚上点左右发的,结果第二天上午才点赞,不知道什么原因,下周要不要再主动一点还有点犹豫。

+

亲情要多交流,要始终铭记这些人是世界上唯一无条件对你好的人。

+

最近事情有点多,每天下班后还是要学些知识,上班没什么事情的时候也要多看书,学技术,不要发呆

+

尽量控制住自己的坏毛病。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20230326
+
https://zhangzhao219.github.io/2023/03/26/diary/diary20230326/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年3月26日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/05/02/diary/diary20230502/index.html b/2023/05/02/diary/diary20230502/index.html new file mode 100644 index 000000000..0fa1a3f6e --- /dev/null +++ b/2023/05/02/diary/diary20230502/index.html @@ -0,0 +1,743 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20230502 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20230502

+ + +
+ +

2023年5月2日,周二

+ +

干什么事情都没有动力,学习也不知道学什么,玩也不知道去哪,打球也略显尴尬,聊天也不知道找谁,刷剧也没有看下去的动力。

+

不管了,好久没有刷剧了,先刷一刷比较火的悬疑剧吧

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20230502
+
https://zhangzhao219.github.io/2023/05/02/diary/diary20230502/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年5月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/07/13/Interview/Interview-Questions-Deep-Learning/index.html b/2023/07/13/Interview/Interview-Questions-Deep-Learning/index.html new file mode 100644 index 000000000..332f41d01 --- /dev/null +++ b/2023/07/13/Interview/Interview-Questions-Deep-Learning/index.html @@ -0,0 +1,1112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 深度学习面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

深度学习面试题准备

+ + +
+ +

深度学习面试题准备

+ +

机器学习算法优缺点

+

逻辑回归

+

优点:

+
    +
  1. 简单易用:逻辑回归算法的输入输出都是数值,它的实现方法和解释方法都很直观。
  2. +
  3. 计算简单:逻辑回归模型的计算比较简单,易于实现,在大数据量下仍然能较快地计算出结果。
  4. +
  5. 容易推导:逻辑回归模型可以用统计方法很容易推导出来。
  6. +
  7. 效果良好:逻辑回归模型在许多应用场景中效果很好,比如二分类问题。
  8. +
+

缺点:

+
    +
  1. 线性假设:逻辑回归假设数据具有线性关系,而很多分类问题并不是线性可分的,不适用于非线性问题。
  2. +
  3. 数据不平衡:逻辑回归不能很好处理数据不平衡的情况。
  4. +
  5. 对异常值敏感:逻辑回归对于异常值非常敏感,如果数据中存在异常值,逻辑回归的结果将会受到影响。
  6. +
  7. 容易欠拟合:如果样本量不够大,逻辑回归容易欠拟合,导致分类结果不准确。
  8. +
+

适用场景:

+
    +
  1. 二分类问题:当问题是一个二分类问题,即将样本划分为两类的问题时,逻辑回归是一个不错的选择。例如,预测一个人是否有疾病、预测一个电子邮件是否是垃圾邮件等。
  2. +
  3. 变量解释性强:逻辑回归可以给出对预测变量贡献的说明,即它们对预测结果的贡献程度,这使得逻辑回归变量具有很好的可解释性。
  4. +
  5. 处理大数据量:逻辑回归能够处理大数据量,并且不需要很高的计算能力。
  6. +
  7. 离散变量:逻辑回归可以处理离散和连续变量,因此非常适合处理含有离散变量的数据集。
  8. +
  9. 线性可分:逻辑回归是一种线性分类算法,因此它在数据线性可分的情况下表现很好。
  10. +
+

决策树

+

优点:

+
    +
  1. 实现简单、复杂度低:决策树的构建和预测的计算复杂度较低,特别是当决策树的深度不是特别深时。
  2. +
  3. 数据的类型不限:决策树分类算法对数据的类型没有严格的限制,既可以处理离散型变量,也可以处理连续型变量。
  4. +
  5. 可解释性高:决策树模型具有很高的可解释性,模型的每一个决策点都可以解释为一个特征或一个特征的一个特定值,更易于人们理解。
  6. +
  7. 对数据准备要求较少:不需要进行特征标准化或数据转换便可输入模型进行训练。
  8. +
+

缺点:

+
    +
  1. 容易过拟合:决策树算法容易被过度训练,从而导致模型过于复杂,在预测新样本时出现高误差。
  2. +
  3. 特征不平衡问题:决策树对不同特征数量的敏感,如果一个特征的数量比其他特征高得多,决策树就会偏向于这个特征。
  4. +
  5. 忽略了数据不平衡:决策树算法不能很好地处理类别不平衡问题,即某一类别的样本数量远小于其他类别的样本数量。
  6. +
  7. 剪枝麻烦:决策树算法需要对树进行剪枝,以防止过拟合。但是,如果剪枝不当,容易导致模型准确性降低。
  8. +
+

适用场景:

+
    +
  1. 对离散特征数据进行分类
  2. +
  3. 处理特征关系非线性的数据分类问题
  4. +
  5. 用于处理大量的特征数据
  6. +
  7. 解决二分类和多分类问题
  8. +
  9. 适用于缺失数据的分类问题。
  10. +
+

支持向量机

+

优点:

+
    +
  1. 强大的泛化能力:SVM能够很好地处理高维数据,并且能够很好地抵抗噪声的影响。
  2. +
  3. 可以处理非线性问题:SVM支持使用核函数,可以处理非线性的分类问题。
  4. +
  5. 对离群点不敏感:SVM通过求解最大间隔来分类,对离群点不敏感。
  6. +
+

缺点:

+
    +
  1. 计算复杂度高:计算最大间隔的复杂度很高,特别是对于大型数据集,可能需要大量的时间和计算资源。
  2. +
  3. 参数调整困难:SVM需要对许多参数进行调整,并且很难找到合适的参数。
  4. +
  5. 不适用于大数据集:SVM不适用于大数据集,因为其计算复杂度高。
  6. +
  7. 对缺失数据敏感:SVM对缺失数据敏感,因此在使用SVM之前需要处理缺失的数据。
  8. +
+

适用场景:

+
    +
  1. 处理二分类问题:SVM是一种非常有效的二分类模型,特别是在数据点数量较少时。
  2. +
  3. 处理高维数据:SVM是一种比较好的高维数据分类模型。
  4. +
  5. 处理非线性分类问题:SVM可以使用核函数进行非线性映射,然后再在高维空间中进行线性分类,因此可以解决非线性分类问题。
  6. +
  7. 处理数据有噪音的分类问题:SVM可以通过引入惩罚项来解决数据有噪音的分类问题。
  8. +
+

K近邻法

+

优点:

+
    +
  1. 实现简单:KNN算法的实现非常简单,因为它没有任何训练过程,只需要记住训练数据的样本。
  2. +
  3. 适用于小数据集:KNN算法在数据量较小的情况下表现良好。
  4. +
  5. 精度高:KNN算法在多数情况下具有很高的分类精度。
  6. +
  7. 多分类能力:KNN算法可以实现多分类任务。
  8. +
+

缺点:

+
    +
  1. 计算复杂度高:KNN算法的计算复杂度随着数据量的增加而增加。
  2. +
  3. 对离群值敏感:KNN算法对离群值非常敏感,在计算距离时可能会影响分类结果。
  4. +
  5. 对特征数据类型敏感:KNN算法对特征数据类型敏感,不能对具有不同数量级的特征值的样本进行分类。
  6. +
  7. 对不平衡数据集敏感:KNN算法对不平衡数据集敏感,在不平衡数据集中可能会得到不准确的分类结果。
  8. +
+

适用场景:

+
    +
  1. 非常简单的分类问题:KNN算法比较适合简单的分类问题,这些问题没有太多的特征和复杂的规则。
  2. +
  3. 对于非线性数据的分类:KNN算法在非线性数据的分类方面表现不错,因为它对每个样本点周围的数据点进行分类,所以能够适应非线性的数据。
  4. +
  5. 缺失数据的分类:KNN算法不需要计算出特征的全部值,所以对于缺失数据也能进行分类。
  6. +
  7. 对于高维数据的分类:KNN算法可以在高维数据空间中进行分类,因为它不需要进行高维特征空间的映射。
  8. +
+

朴素贝叶斯

+

优点:

+
    +
  1. 朴素贝叶斯分类算法的计算量比较小,存储资源低,适合在硬件资源有限的环境中使用。
  2. +
  3. 对缺失数据不太敏感,算法也比较简单,易于理解和实现。
  4. +
  5. 可以处理多分类问题。
  6. +
  7. 对于输入数据的准备没有特别严格的要求,可以处理连续性和离散性数据。
  8. +
+

缺点:

+
    +
  1. 该模型的假设是属性之间相互独立,这在实际应用中往往是不成立的。
  2. +
  3. 对输入数据的表达形式很敏感。
  4. +
  5. 对于参数估计,需要大量的样本数据。
  6. +
+

适用场景:

+
    +
  1. 大数据集:朴素贝叶斯算法的计算代价比较低,适用于处理大数据集。
  2. +
  3. 分类任务:朴素贝叶斯算法主要用于分类任务,特别是二分类任务。
  4. +
  5. 简单的特征:朴素贝叶斯算法假设特征之间是独立的,如果特征简单,朴素贝叶斯算法的表现很好。
  6. +
  7. 分类基于统计:朴素贝叶斯算法是基于统计学的,它适用于基于统计学的分类任务。
  8. +
  9. 数据缺失:朴素贝叶斯算法对数据缺失较强,适用于数据缺失的分类任务。
  10. +
+

神经网络

+

优点:

+
    +
  1. 模型灵活:神经网络模型可以模拟人类的大脑,并且可以通过不断学习来解决复杂的问题;
  2. +
  3. 强大的预测能力:神经网络分类算法具有强大的预测能力,可以提取数据的隐含信息;
  4. +
  5. 可以处理大量的特征:神经网络分类算法可以处理大量的特征,并且能够高效地处理结构化和非结构化的数据;
  6. +
  7. 处理非线性关系:神经网络分类算法可以通过多层神经元,表示复杂的非线性关系;
  8. +
+

缺点:

+
    +
  1. 数据偏差问题:如果训练数据有偏差,神经网络分类算法的预测结果会受到影响;
  2. +
  3. 过拟合问题:神经网络分类算法容易对训练数据进行过度拟合,从而影响对新数据的预测的准确率;
  4. +
  5. 训练复杂度高:神经网络分类算法的训练复杂度高,对于大规模数据集,需要大量的时间和计算资源;
  6. +
  7. 训练比较耗时:神经网络分类算法需要训练大量的数据,因此训练时间比较长。
  8. +
+

适用场景:

+
    +
  1. 适用于处理非线性问题,例如图像分类、语音识别、文本分类等。
  2. +
  3. 可以学习和抽象高维数据的复杂关系,并在大量训练数据的情况下表现得非常出色。
  4. +
  5. 适用于处理非结构化数据,例如图像和语音。
  6. +
+

自回归语言模型与自编码语言模型

+

自回归语言模型,是通过上文一步一步预测下文,不能看见未来信息的模型。像坚持只用单向Transformer的GPT就是典型的自回归语言模型

+

自编码语言模型是类似于bert 这种,使用了mask LM,可以使用上下文语境信息进行预测。这也是为什么bert是双向的原因。

+

自回归语言模型没能自然的同时获取单词的上下文信息(ELMo把两个方向的LSTM做concat是一个很好的尝试,但是效果并不是太好);

+

自编码语言模型能很自然的把上下文信息融合到模型中(Bert中的每个Transformer都能看到整句话的所有单词,等价于双向语言模型),但在Fine-tune阶段,模型是看不到[mask]标记的,所以这就会带来一定的误差。

+

XLNet的思路采用的是自回归语言模型,根据上文来预测下一个单词,但是在上文中添加了下文信息,这样就既解决了[mask]带来的两阶段不一致问题和无法同时引入上下文信息的问题。实际上是通过排列组合的方式将一部分下文单词放到上文单词的位置,但实际形式还是一个从左到右预测的自回归语言模型

+

优化器

+

SGD

+
    +
  • 批梯度下降(Batch gradient descent):遍历全部数据集算一次损失函数,计算量开销大,计算速度慢,不支持在线学习。
  • +
  • 随机梯度下降(Stochastic gradient descent,SGD) : 每看一个数据就算一下损失函数,然后求梯度更新参数。这个方法速度比较快,但是收敛性能不太好。
  • +
  • 批量随机梯度下降(Min-batch SGD) :用一些小样本来近似全部的,其本质就是既然1个样本的近似不一定准,那就用更大的30个或50个样本来近似。将样本分成m个mini-batch,每个mini-batch包含n个样本。
  • +
+

使用小批量梯度下降的优点是:

+
    +
  1. 可以减少参数更新的波动,最终得到效果更好和更稳定的收敛。
  2. +
  3. 还可以使用最新的深层学习库中通用的矩阵优化方法,使计算小批量数据的梯度更加高效。
  4. +
  5. 通常来说,小批量样本的大小范围是从50到256,可以根据实际问题而有所不同。
  6. +
  7. 在训练神经网络时,通常都会选择小批量梯度下降算法。
  8. +
+

SGD方法中的高方差振荡使得网络很难稳定收敛 ,所以有研究者提出了一种称为 动量(Momentum)的技术 ,通过优化相关方向的训练和弱化无关方向的振荡,来加速SGD训练。在动量学习算法中,每一步走多远不仅依赖于本次的梯度的大小还取决于过去的速度。速度v是累积各轮训练参的梯度。

+

Adam算法

+

RMSprop将学习率分解成一个平方梯度的指数衰减的平均。 Adam中动量直接并入了梯度一阶矩(指数加权)的估计。其次,相比于缺少修正因子导致二阶矩估计可能在训练初期具有很高偏置的RMSProp,Adam还包括偏置修正,修正从原点初始化的 一阶矩(动量项) 和(非中心的) 二阶矩估计

+

本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳

+
import torch
+
+def adam_update(parameters, gradients, m, v, t, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
+    for param, grad in zip(parameters, gradients):
+        m[param] = beta1 * m[param] + (1 - beta1) * grad
+        v[param] = beta2 * v[param] + (1 - beta2) * (grad ** 2)
+        m_corrected = m[param] / (1 - beta1 ** t)
+        v_corrected = v[param] / (1 - beta2 ** t)
+        param_update = lr * m_corrected / (np.sqrt(v_corrected) + epsilon)
+        param -= param_update
+

Adam与SGD的区别

+

SGD缺点是其更新方向完全依赖于当前batch计算出的梯度,因而十分不稳定。

+

Adam的优点主要在于:

+
    +
  • 考虑历史步中的梯度更新信息,能够降低梯度更新噪声。
  • +
  • 经过偏差校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
  • +
+

但是Adam也有其自身问题:可能会对前期出现的特征过拟合,后期才出现的特征很难纠正前期的拟合效果。二者似乎都没法很好避免局部最优问题。不收敛、无法达到全局最优。

+

其他

+

Nesterov梯度加速法 ,通过使网络更新与误差函数的斜率相适应,并依次加速SGD,也可根据每个参数的重要性来调整和更新对应参数,以执行更大或更小的更新幅度。

+

AdaDelta方法是AdaGrad的延伸方法,它倾向于解决其学习率衰减的问题。Adadelta不是累积所有之前的平方梯度,而是将累积之前梯度的窗口限制到某个固定大小w。

+

Adagrad方法是通过参数来调整合适的学习率η,对稀疏参数进行大幅更新和对频繁参数进行小幅更新。因此,Adagrad方法非常适合处理稀疏数据。

+

过拟合

+

一般定义:模型在训练集上的表现很好,但在测试集和新数据上的表现很差。

+

出现的原因:

+
    +
  • 模型复杂度过高,参数过多
  • +
  • 训练数据比较小
  • +
  • 训练集和测试集分布不一致
  • +
  • 样本里面的噪声数据干扰过大,导致模型过分记住了噪声特征。
  • +
+

解决的方法:

+
    +
  • 降低模型复杂度
  • +
  • 数据增强
  • +
  • 正则化: +
      +
    • L1 惩罚权重绝对值, 生成简单、可解释的模型
    • +
    • L2 惩罚权重平方和, 能够学习复杂数据模式
    • +
    • dropout
    • +
    +
  • +
  • 早停
  • +
+

BN与LN

+

将这些输入值进行标准化,降低scale的差异至同一个范围内。这样做的好处在于一方面提高梯度的收敛程度, 加快模型的训练速度 ;另一方面使得每一层可以尽量面对同一特征分布的输入值,减少了变化带来的不确定性,也降低了对后层网路的影响,各层网路变得相对独立, 缓解了训练中的梯度消失问题

+

训练时,均值、方差分别是该批次内数据相应维度的均值与方差;推理时,均值、方差是基于所有批次的期望计算所得。其中在推理时所用的均值和方差是通过移动平均计算得到的,可以减少存储每个batch均值方差的内存。

+

LN层与BN相比,只考虑单个sample内的统计变量,因此也不用使用BN实现中的running mean, running var.,LN也完全不用考虑输入batch_size的问题。

+

为什么transformer中不使用BN归一化

+

解释一:CV和NLP数据特性的不同,对于NLP数据,前向和反向传播中,batch统计量及其梯度都不太稳定,一个Batch中每个句子对应位置的分量不一定有意义。

+

解释二:要能在某个维度做独立同分布假设,才能合理归一化。对于CV来说,batch之间的图像是独立的,可以使用BN,而对于自然语言的token,相互是具有较强的关联性,不是相互独立的。

+

BN是对每个特征在batch_size上求的均值和方差,如果BN应用到NLP任务中,对应的是对每一个单词作处理,也就是说,现在的每一个单词是对应到了MLP中的每一个特征,也就是默认了在同一个位置的单词对应的是同一种特征,比如:“我/爱/中国/共产党”和“今天/天气/真/不错”

+

如果使用BN,代表着认为 "我"和“今天”是对应的同一个维度特征,这样才可以去做BN。但是每个单词表达的特征是不一样的,所以按照位置对单词特征进行缩放,是违背直觉的。

+

layer-norm 做的是针对每一个样本,做特征的缩放。也就是,它认为“我/爱/中国/共产党”这四个词在同一个特征之下,所以基于此而做归一化。

+

梯度消失和爆炸

+

梯度消失的原因:主要是是网络层较深,其次是采用了不合适的损失函数,会使得靠近输入层的参数更新缓慢。导致在训练时,只等价于后面几层的浅层网络的学习。

+

梯度爆炸的原因:一般出现在深层网络和权值初始化值太大的情况下。在深层神经网络或循环神经网络中,误差的梯度可在更新中累积相乘。如果网络层之间的梯度值大于 1.0,那么重复相乘会导致梯度呈指数级增长,梯度变的非常大,然后导致网络权重的大幅更新,并因此使网络变得不稳定。

+

梯度爆炸会使得在训练过程中,权重的值变得非常大,以至于溢出,导致模型损失变成 NaN等等。

+

解决方法:梯度剪切,对梯度设定阈值;权重正则化;batch normalization;残差网络的捷径(shortcut);

+

防止梯度爆炸:

+
    +
  1. 梯度剪切:更新梯度时,梯度超过某个阈值,就将其强制限制在这个范围内
  2. +
  3. 权重正则化:L1正则和L2正则
  4. +
+

防止梯度消失:

+
    +
  1. 合理的激活函数(如ReLU)+权重初始化
  2. +
  3. Batch Normalization:应用于每层激活函数之前
  4. +
  5. 残差网络
  6. +
+

以上问题可以拓展到具体的模型上,比如问BERT是如何防止梯度消失的,就可以从残差网络等方面回答

+

Bert与GPT区别

+
    +
  1. 模型不同-单双向
  2. +
  3. 预训练任务不同
  4. +
  5. 使用方法区别
  6. +
  7. GPT是单向模型,无法利用上下文信息,只能利用上文;Bert是双向模型。
  8. +
  9. GPT是基于自回归模型,可以应用在NLU和NLG两大任务,而原生的BERT采用的基于自编码模型,只能完成NLU任务,无法直接应用在文本生成上面。
  10. +
  11. 同等参数规模下,BERT的效果要好于GPT。
  12. +
+

LSTM的优缺点

+

优点:

+
    +
  1. 解决梯度消失问题:传统的RNN在处理长序列时容易出现梯度消失的问题,导致难以训练。LSTM引入了门控机制,可以有效地缓解梯度消失问题,从而能够处理更长的序列数据。
  2. +
  3. 捕捉长期依赖关系:LSTM通过细胞状态和门控机制,能够更好地捕捉序列数据中的长期依赖关系。相比传统的RNN,LSTM有更好的记忆性能,可以在处理序列数据时保留较远的上下文信息。
  4. +
  5. 可以学习到时序特征:LSTM具有对时间的敏感性,能够学习到时序数据中的模式和特征。这使得LSTM在时间序列预测、信号处理等任务中具有优势。
  6. +
+

缺点:

+
    +
  1. 计算复杂度高:相比传统的RNN,LSTM的计算复杂度更高。由于引入了门控机制和长期记忆机制,LSTM需要更多的参数和计算量。
  2. +
  3. 难以解释:LSTM的复杂性使得其内部运行机制不太直观,难以解释网络的决策过程。这对于某些应用场景,如金融领域或医疗领域,可能带来一定的困扰。
  4. +
  5. 需要大量数据进行训练:LSTM有更多的参数需要训练,因此需要更多的数据来避免过拟合。如果训练数据不足,LSTM可能面临泛化能力不足的问题。
  6. +
+

Dropout

+

Dropout可以作为训练深度神经网络的一种trick供选择。在每个训练批次中,在前向传播的时候,让某个神经元的激活值以一定的概率p停止工作,这样可以使模型泛化性更强,因为它不会太依赖某些局部的特征

+
    +
  1. 取平均的作用:相当于对很多个不同的神经网络取平均
  2. +
  3. 减少神经元之间复杂的共适应关系:因为dropout程序导致两个神经元不一定每次都在一个dropout网络中出现。这样权值的更新不再依赖于有固定关系的隐含节点的共同作用,阻止了某些特征仅仅在其它特定特征下才有效果的情况 。迫使网络去学习更加鲁棒的特征 ,这些特征在其它的神经元的随机子集中也存在。
  4. +
+

L1 L2 正则化

+

L1正则化是指在损失函数中加入权值向量w的绝对值之和,即各个元素的绝对值之和,使权重稀疏,可以进行特征选择

+

L2正则化指在损失函数中加入权值向量w的平方和,使权重平滑

+

L1范数MAE 与 L2范数 MSE作为损失函数的对比:

+

MAE相比MSE,鲁棒性更强。MSE对误差取了平方,如果数据存在异常值,误差会被放大。所以,MAE对于异常值比MSE更稳定。

+

然而MAE存在一个严重的问题(特别是对于神经网络):更新的梯度始终相同,也就是说,即使对于很小的损失值,梯度也很大。这样不利于模型的学习。为了解决这个缺陷,我们可以使用变化的学习率,在损失接近最小值时降低学习率。

+

而MSE在这种情况下的表现就很好,即便使用固定的学习率也可以有效收敛。MSE损失的梯度随损失增大而增大,而损失趋于0时则会减小。这使得在训练结束时,使用MSE模型的结果会更精确。

+

Word2Vec

+

Word2Vec是轻量级的神经网络,其模型仅仅包括输入层、隐藏层和输出层,模型框架根据输入输出的不同,主要包括CBOW和Skip-gram模型。 CBOW的方式是在知道词的上下文的情况下预测当前词,而Skip-gram是在知道了词的情况下,对词的上下文进行预测。

+

Word2Vec提出两种加快训练速度的方式,一种是Hierarchical softmax,另一种是Negative Sampling

+

在进行最优化的求解过程中:从隐藏层到输出的Softmax层的计算量很大,因为要计算所有词的Softmax概率,再去找概率最大的值。

+

Hierarchical softmax相当于将线性的Softmax转换为哈夫曼树,从而将时间复杂度降低到log级别

+

无需计算词表中所有单词的softmax并选择最大的作为输出,只需遍历树的深度个节点,即可找到softmax值最大的词作为输出

+

Negative Sampling

+
    +
  1. 针对softmax运算导致的每次梯度计算开销过大,将softmax函数调整为sigmoid函数,当然对应的含义也由给定中心词,每个词作为背景词的概率,变成了给定中心词,每个词出现在背景窗口中的概率
  2. +
  3. 进行负采样,引入负样本,随机选择一小部分的 negative words,比如选 10个 negative words 来更新对应的权重参数
  4. +
+

Word2vec 的优缺点

+

优点:

+
    +
  1. 由于 Word2vec 会考虑上下文,跟之前的方法相比,效果要更好
  2. +
  3. 比之前的Embedding方法维度更少,所以速度更快
  4. +
  5. 通用性很强,可以用在各种 NLP 任务中
  6. +
+

缺点:

+
    +
  1. 由于词和向量是一对一的关系,所以多义词的问题无法解决。
  2. +
  3. Word2vec 是一种静态的方式,虽然通用性强,但是无法针对特定任务做动态优化
  4. +
+

Softmax

+

下溢出与上溢出

+
    +
  • 如果都是一个非常大的负数,则下溢出,分母为0,结果未定义
  • +
  • 如果都是一个非常大的正数,则上溢出,结果未定义
  • +
+

解决方式:将全部的分量减去最大值

+
    +
  • 当分量都比较小的时候,减去后至少有一个为0,因此分母至少有一个为1,解决了下溢出的问题
  • +
  • 当分量都比较大的时候,相当于分子分母同时除以一个非常大的数,解决了上溢出的问题
  • +
+

带权重交叉熵与Focal Loss

+

加权交叉熵思想是用一个系数描述样本在loss中的重要性。对于小数目样本,加强它对loss的贡献,对于大数目的样本减少它对loss的贡献。带权重的交叉熵在正样本的判别上加了一个w系数,w需要事先根据数据集计算。也就是权重参数是不变的

+

focal loss的设计很巧妙,就是在cross entropy的基础上加上权重,让模型注重学习难以学习的样本,训练数据不均衡中占比较少的样本,相对放大对难分类样本的梯度,相对降低对易分类样本的梯度,并在一定程度上解决类别不均衡问题。

+

focal loss相比交叉熵多了一个,对于分类准确的样本,参数趋近于0

+

相比交叉熵损失,focal loss对于分类不准确的样本,损失没有改变,对于分类准确的样本,损失会变小。 整体而言,相当于增加了分类不准确样本在损失函数中的权重。

+

对抗训练

+

对抗训练是一种引入噪声的训练方式,可以对参数进行正则化,提升模型鲁棒性和泛化能力。

+

对抗训练的假设是:给输入加上扰动之后,输出分布和原Y的分布一致

+

往增大损失的方向增加扰动

+

在计算对抗扰动时虽然计算了梯度,但不对参数进行更新, 因为当前得到的对抗扰动是对旧参数最优的

+

用一句话形容对抗训练的思路,就是 在输入上进行梯度上升(增大loss),在参数上进行梯度下降(减小loss) 。由于输入会进行embedding lookup,所以 实际的做法是在embedding table上进行梯度上升

+

接下来介绍不同的方法,后续方法优化的主要方向有两点:得到更优的扰动 & 提升训练速度

+

FGM

+

对于每个x:(输入的梯度是g)

+
    +
  1. 计算x的前向loss、反向传播得到梯度
  2. +
  3. 根据embedding矩阵的梯度计算出r,并加到当前embedding上,相当于x+r
  4. +
  5. 计算x+r的前向loss,反向传播得到对抗的梯度,累加到(1)的梯度上
  6. +
  7. 将embedding恢复为(1)时的值
  8. +
  9. 根据(3)的梯度对参数进行更新
  10. +
+

PGD小步走多走几步

+

NLP

+

N-Gram

+

N-Gram是一种基于统计语言模型的算法。它的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。

+

每一个字节片段称为gram,对所有gram的出现频度进行统计,并且按照事先设定好的阈值进行过滤,形成关键gram列表,也就是这个文本的向量特征空间,列表中的每一种gram就是一个特征向量维度。

+

该模型基于这样一种假设,第N个词的出现只与前面N-1个词相关,而与其它任何词都不相关,整句的概率就是各个词出现概率的乘积。这些概率可以通过直接从语料中统计N个词同时出现的次数得到。常用的是二元的Bi-Gram和三元的Tri-Gram。

+

模型通过训练语句对指数级语义相关的句子进行建模。

+

(1)每个单词的分布式表示

+

(2)单词序列的概率函数。

+

(3)泛化(Generalization)是指从未出现的单词序列,可以通过类似的词的组成的已经出现的句子来获得较高的概率。

+

语言模型与其他学习问题的最基本的问题就是维度爆炸

+

两者的含义基本相同,但是NNLM使用了神经网络模型

+

Seq2Seq

+

编码器+解码器的结构:

+

编码器处理输入序列中的每一项,将捕获的信息编译成一个向量(输入序列的编码)

+

解码器接收编码器处理后的上下文,逐项生成输出序列

+

应用:阅读理解,文本摘要,闲聊系统,看图说话

+

添加Attention机制后:

+

编码器向解码器传递更多的数据,编码器不仅仅传递编码阶段的最后一个隐藏状态,而是将所有的隐藏状态传递给解码器

+

注意解码器在产生输出之前的额外的步骤,为了聚焦于与该解码时间步骤相关的输入部分,解码的每一时刻都通过编码器隐藏状态与编码器当前隐藏状态的相关性,对不同的编码器隐藏状态进行打分,打分后的编码器隐藏状态加权相加,并与当前的隐藏状态相结合,再进行最后的输出运算

+

文本生成的评价标准:

+

BLEU:比较候选译文与参考译文的n-gram重合程度

+

BLEU专注于召回率(关注有多少个参考译句中的n-gram出现在了输出之中),而非精度(候选译文的n-gram有没有在参考译文中出现过)

+

ROUGE-N:将BLEU的精确率优化为召回率

+

ROUGE-L:将BLEU的n-gram优化为公共子序列(公共子序列不一定连续)

+

ROUGE-W:在ROUGE-L的基础上对连续性添加一个权重

+

ROUGE-S:对n-gram进行统计,但是允许跳词

+

METEOR:考虑了基于整个语料库上的准确率和召回率,包括同义词匹配与同型词匹配

+

Seq2Seq模型输入的方式:

+

①将前一时刻的输出作为下一时刻的输入

+

缺点:如果预测错了,后面都是错的,错误会一直累积

+

②以正确的作为输入

+

缺点:测试时候不知道输入,因此存在训练测试偏差

+

③Curriculum Learning

+

使用概率p决定是①还是②

+

开始训练时候②概率较大,随着训练时间减小

+

Beam Search:

+

贪心的方法:一个步骤一个步骤去看,可能不太准确

+

Beam search对每一个单词的预测概率进行搜索,生成多个候选输出序列

+

ELMo(Embeddings from Language Model)

+

一词多义的现象——应用同一词向量不合适

+

基于双向两层的LSTM,训练动态词表征

+

双向的循环神经网络能更好地学习词语间的上下文关系

+

两层的循环神经网络能学习到更深层次的语义表征。

+

低层能够提取语法等方面的初级信息

+

高层擅长于捕捉语义等高级特征

+

对原始输入进行字符级别的卷积,能更好的抓取字词的内部信息

+

核心:基于语言模型的思路,利用上下文信息去建模某一单词

+

多轮对话

+
    +
  • 将一条多轮对话数据,拆分成多条数据
  • +
  • 将一条多轮对话数据拼接之后,输入模型,并行计算每个位置的loss,只有Assistant部分的loss参与权重更新。
  • +
+

为什么Work?

+

答案在于因果语言模型的attention mask。以GPT为代表的Causal Language Model(因果语言模型),这种模型的attention mask是一个对角掩码矩阵,每个token在编码的时候,只能看到它之前的token,看不到它之后的token。

+ + +
+ +
+
+ + + + + + +
+
+
深度学习面试题准备
+
https://zhangzhao219.github.io/2023/07/13/Interview/Interview-Questions-Deep-Learning/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年7月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/07/14/Interview/Interview-Questions-Transformer/index.html b/2023/07/14/Interview/Interview-Questions-Transformer/index.html new file mode 100644 index 000000000..c321e58cf --- /dev/null +++ b/2023/07/14/Interview/Interview-Questions-Transformer/index.html @@ -0,0 +1,964 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Transformer面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Transformer面试题准备

+ + +
+ +

Transformer面试题准备

+ +

Attention

+

引入Attention机制的原因

+
    +
  • 参数少:相对于RNN、CNN复杂度小,参数小
  • +
  • 速度快:解决了RNN不能并行计算的问题
  • +
  • 效果好:解决了长距离的信息会被弱化问题,Attention 是挑重点,就算文本比较长,也能从中间抓住重点,不丢失重要的信息。
  • +
+

分类

+

计算区域

+
    +
  1. Soft Attention,这是比较常见的Attention方式,对所有key求权重概率,每个key都有一个对应的权重,是一种全局的计算方式(也可以叫Global Attention)。这种方式比较理性,参考了所有key的内容,再进行加权。但是计算量可能会比较大一些。
  2. +
  3. Hard Attention,这种方式是直接精准定位到某个key,其余key就都不管了,相当于这个key的概率是1,其余key的概率全部是0。因此这种对齐方式要求很高,要求一步到位,如果没有正确对齐,会带来很大的影响。另一方面,因为不可导,一般需要用强化学习的方法进行训练。(或者使用gumbel softmax之类的)
  4. +
  5. Local Attention,这种方式其实是以上两种方式的一个折中,对一个窗口区域进行计算。先用Hard方式定位到某个地方,以这个点为中心可以得到一个窗口区域,在这个小区域内用Soft方式来算Attention。
  6. +
+

所用信息

+

假设我们要对一段原文计算Attention,这里原文指的是我们要做attention的文本,那么所用信息包括内部信息和外部信息,内部信息指的是原文本身的信息,而外部信息指的是除原文以外的额外信息。

+

General Attention,这种方式利用到了外部信息,常用于需要构建两段文本关系的任务,query一般包含了额外信息,根据外部query对原文进行对齐。比如在阅读理解任务中,需要构建问题和文章的关联,假设现在baseline是,对问题计算出一个问题向量q,把这个q和所有的文章词向量拼接起来,输入到LSTM中进行建模。那么在这个模型中,文章所有词向量共享同一个问题向量,现在我们想让文章每一步的词向量都有一个不同的问题向量,也就是,在每一步使用文章在该步下的词向量对问题来算attention,这里问题属于原文,文章词向量就属于外部信息。

+

Local Attention,这种方式只使用内部信息,key和value以及query只和输入原文有关,在self attention中,key=value=query。既然没有外部信息,那么在原文中的每个词可以跟该句子中的所有词进行Attention计算,相当于寻找原文内部的关系。还是举阅读理解任务的例子,上面的baseline中提到,对问题计算出一个向量q,那么这里也可以用上attention,只用问题自身的信息去做attention,而不引入文章信息。

+

结构层次

+

结构方面根据是否划分层次关系,分为单层attention,多层attention和多头attention:

+
    +
  1. 单层Attention,这是比较普遍的做法,用一个query对一段原文进行一次attention。
  2. +
  3. 多层Attention,一般用于文本具有层次关系的模型,假设我们把一个document划分成多个句子,在第一层,我们分别对每个句子使用attention计算出一个句向量(也就是单层attention);在第二层,我们对所有句向量再做attention计算出一个文档向量(也是一个单层attention),最后再用这个文档向量去做任务。
  4. +
  5. 多头Attention,这是Attention is All You Need中提到的multi-head attention,用到了多个query对一段原文进行了多次attention,每个query都关注到原文的不同部分,相当于重复做多次单层attention,最后再把这些结果拼接起来。
  6. +
+

https://www.nowcoder.com/discuss/387725948110602240?sourceSSR=search

+

Scale 的作用:矩阵点乘可能会导致 数值指数级增加 ,从而 使得 softmax 的梯度非常小 ,所以使用d_k进行缩放来避免这个问题

+

Softmax 的作用:Softmax 将其归一化至 (0,1)区间便于后续与V相乘,同时也起到以对梯度进行缩放的作用(防负数以及过大的结果导致梯度问题)

+

pCTK839.png

+

GQA

+
    +
  • MHA(Multi Head Attention) 中,每个头有自己单独的 key-value 对;
  • +
  • MQA(Multi Query Attention) 中只会有一组 key-value 对;
  • +
  • GQA(Grouped Query Attention) 中,会对 attention 进行分组操作,query 被分为 N 组,每个组共享一个 Key 和 Value 矩阵。
  • +
+

+

GQA-N 是指具有 N 组的 Grouped Query Attention。GQA-1具有单个组,因此具有单个Key 和 Value,等效于MQA。而GQA-H具有与头数相等的组,等效于MHA。

+

在基于 Multi-head 多头结构变为 Grouped-query 分组结构的时候,也是采用跟上图一样的方法,对每一组的 key-value 对进行 mean pool 的操作进行参数融合。 融合后的模型能力更综合,精度比 Multi-query 好,同时速度比 Multi-head 快

+

代码实现:https://dongnian.icu/llm_interview_note/#/02.%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/MHA_MQA_GQA/MHA_MQA_GQA

+

Transformer

+

img

+
    +
  • 输入:假设输入序列长度为T,则Encoder输入的维度为[batch_size, T],经过embedding层、position encoding等流程后,生成[batch_size, T, D]的数据,D表示词嵌入模型隐层维度;
  • +
  • 位置编码的维度与词嵌入的维度相同,用正弦函数表示出二进制那样的交替。position encoding是与embedding直接相加的,但是由于position encoding的有效字段在前面,embedding的有效字段在后面,因此相当于隐式concat
  • +
  • Encoder:这个数据会经过N个模块,每个模块的结构都是相同的,为Multi-head Attention->Add->LayerNorm->Feed Forward->Add->LayerNorm。Multi-head Attention在T这个维度上,计算每两个位置元素的Attention值,汇聚再次得到每个位置的Embedding,输出维度仍然为[batch_size, T, D]。Add层将Multi-head Attention的输出结果和输入结果相加,类似于一个残差网络。Feed Forward会用一个比较大的中间层维度将上一层的隐藏维度扩大,然后再缩小,如用一个全连接从[batch_size, T, D]变为[batch_size, T, 4*D],再变回到[batch_size, T, D],主要为了增加模型容量。最终经过N个模块,Encoder的输出维度仍然为[batch_size, T, D]。
  • +
  • Transformer里加了残差连接,所以模型输入的信息(位置信息)可以有效地传播到其它层。
  • +
  • 在Transformer模型中,LN(Layer Normalization)层是一种用于规范化输入向量的技术,它可以提高模型的训练效率和稳定性。在LN层中,对于每个输入向量的每个维度,都会计算该维度上的均值和方差,然后对该维度上的值进行规范化(即将其减去均值并除以标准差)。这样可以使得模型输入的分布更加稳定,从而提高模型的训练效果。在Transformer模型中,LN层通常被应用在每个子层(如Multi-Head Attention和Feedforward子层)的输出之后。Batchnorm为批归一化,对相同批次中所有样本的同一位置特征做归一化,而layernorm是对某个样本的所有位置特征做归一化。
  • +
  • Decoder:Decoder的输入也经过类似的变换得到[batch_size, T’, D],T’是Decoder输入长度。之后会进入多个相同结果的模块,每个模块为Self Multi-head Attention->Add->LayerNorm->Cross Multi-head Attention->Add->LayerNorm->Feed Forward->Add Norm。Self Multi-head Attention,表示Decoder序列上的元素内部做Attention,和Encoder是一样的。Cross Multi-head Attention,是Decoder每个位置和Encoder各个位置进行Attention,类似于传统的seq2seq中的Attention,用来进行Decoder和Encoder的对齐。
  • +
  • 在解码器中,自注意力层只允许关注已输出位置的信息。实现方法是在自注意力层的softmax之前进行mask,将未输出位置的信息设为极小值。每个解码器组件将在“encoder-decoder attention”层中使用编码器传过来的K和V,这有助于解码器将注意力集中在输入序列中的适当位置
  • +
+

img

+

最后我们再来整体看一下 Transformer:

+
    +
  • 首先输入数据生成词的嵌入式向量表示(Embedding),生成位置编码(Positional Encoding,简称 PE)。
  • +
  • 进入 Encoders 部分。先进入多头注意力层(Multi-Head Attention),是自注意力处理,然后进入全连接层(又叫前馈神经网络层),每层都有 ResNet、Add & Norm。
  • +
  • 每一个 Encoder 的输入,都来自前一个 Encoder 的输出,但是第一个 Encoder 的输入就是 Embedding + PE。
  • +
  • 进入 Decoders 部分。先进入第一个多头注意力层(是 Masked 自注意力层),再进入第二个多头注意力层(是 Encoder-Decoder 注意力层),每层都有 ResNet、Add & Norm。
  • +
  • 每一个 Decoder 都有两部分输入。
  • +
  • Decoder 的第一层(Masked 多头自注意力层)的输入,都来自前一个 Decoder 的输出,但是第一个 Decoder 是不经过第一层的(因为经过算出来也是 0)。
  • +
  • Decoder 的第二层(Encoder-Decoder 注意力层)的输入,Q 都来自该 Decoder 的第一层,且每个 Decoder 的这一层的 K、V 都是一样的,均来自最后一个 Encoder。
  • +
  • 最后经过 Linear、Softmax 归一化。
  • +
+

其他优化细节:

+
    +
  • Label Smoothing :基本原理是提高了模型学习的不确定性,让模型在每次输出时即使单个词的概率分数再高也能“考虑”输出其他词,最终起到了提高模型学习能力的效果。这也是分类问题中常用的优化技巧。
  • +
+

+

Residual Dropout :对于残差连接的 当前层输出和上一层输出相加后再正则化这一组操作,对其来自上一层的输出(不包括当前层的输出)和残差连接后的结果均进行 Dropout。

+

一些面试题目

+

代码详解

+

复杂度

+

时间复杂度

+
    +
  1. 输入序列首先经过线性变换得到QKV矩阵,为[N, d] * [d, d] =
  2. +
  3. QKV矩阵相乘,为[N, d] * [d, N],为;然后是[N, N] * [N, d]也为
  4. +
  5. Softmax 为
  6. +
+

空间复杂度

+

建立矩阵进行变换,空间复杂度来源于建立的矩阵

+
    +
  1. Layer Normalization :每个Self-Attention块和MLP块各有一个Layer Normalization,包含两个可训练参数:仿射变换中的weight跟bias,形状都是d。因此,两个Layer Normalization的参数量为
  2. +
  3. 词嵌入矩阵:词嵌入矩阵的维度通常等于隐藏层维度d,词表大小为V,还要再加位置编码。因此,词嵌入矩阵的参数量为,然后要过Layer Normalization,就是
  4. +
  5. QKV矩阵,每个Self-Attention块包含四个主要的权重矩阵(Q、K、V、O)(多头注意力拼接在一起后还需要一个O矩阵进行变换从而对齐到输出)和它们对应的偏置项。每个权重矩阵的形状为[d, d],其中d是隐藏层维度。因此,每个Self-Attention块的参数量为(权重矩阵和偏置项)。然后要过Layer Normalization,再加
  6. +
  7. MLP:MLP块由两个线性层组成,第一个线性层将维度从d映射到4d,第二个线性层再将维度从4d映射回d。因此,MLP块的参数量为(两个权重矩阵和偏置项)。然后要过Layer Normalization,再加
  8. +
  9. 最后如果是Bert加一层全连接层,参数量
  10. +
+

Transformer-XL

+

Transformer模型可以学习输入到文本的长距离依赖关系和全局特性,但是在预测时候会受到训练时所设定的最大长度的限制(直接截长补短)

+

缺点:语义残破,文本非常长

+

Transformer-XL:通过引入循环的机制(RNN)与相对位置编码,解决了Transformer长度限制的问题

+

Vanilla Transformer:基于Transformer的语言模型

+

将原来的句子进行切片,上下文有限且计算速度非常慢

+

Transformer-XL:

+

①循环机制:分成子句,在训练和预测时候,依次将每个子句传入Transformer模型,并将每个子句在Transformer中各层的输出传递给下一个子句,可以捕获的最大依赖项增加了N倍

+

②相对位置编码:由于是分段计算的,因此如果对每个段直接使用Transformer中的位置编码,会出现问题,相同相对位置将具有相同的位置编码

+

Bert

+

结构

+

与Transformer的Encoder基本相同,其中输入层略有不同,被改造成[CLS]+句子A(+[SEP]+句子B+[SEP])

+
    +
  • [CLS]: 代表的是分类任务的特殊token,它的输出就是模型的pooler output
  • +
  • [SEP]:分隔符
  • +
  • 句子A以及句子B是模型的输入文本,其中句子B可以为空,则输入变为[CLS]+句子A
  • +
+

因为transformer无法获得字的位置信息,BERT和transformer一样也加入了 绝对位置 position encoding,但是和transformer不同的是,BERT使用的不是transformer对应的函数型(functional)的encoding方式,而是直接采用类似word embedding的方式(Parametric),直接获得position embedding。

+

因为我们对输入进行了改造,使得模型可能有多个句子Segment的输入,所以我们也需要加入segment的embedding,例如 [CLS], A_1, A_2, A_3,[SEP], B_1, B_2, B_3, [SEP] 对应的segment的输入是 [0,0,0,0,1,1,1,1], 然后再根据segment id进行embedding_lookup得到segment embedding。

+

因此输入层为三个embedding相加(position embedding + segment embedding + token embedding)

+

一个transformer的encoder单元由一个multi-head-Attention + Layer Normalization + feedforword + Layer Normalization 叠加产生,BERT的每一层由一个这样的encoder单元构成。在比较大的BERT模型中,有24层encoder,每层有16个Attention,词向量的维度是1024。在比较小的BERT模型中,有12层encoder,每层有12个Attention,每个head的神经元个数是64,12个head总的神经元的个数即为768,因此词向量维度是768。在所有情况下,将feed-forward/filter 的大小设置为 4H(H为词向量的维度),即H = 768时为3072,H = 1024时为4096。

+

Pre-training

+

Mask Language Model(MLM)

+

BERT第一次采用了mask language model(MLM)任务,这就类似于完形填空(Cloze task)。

+

具体的做法: 我们会随机mask输入的几个词,然后预测这个词。但是这样子做的坏处是因为fine-tuning阶段中并没有[MASK] token,所以导致了pre-training 和 fine-tuning的不匹配的情况。所以为了减轻这个问题,文章中采用的做法是:

+

对于要MASK 15%的tokens,

+
    +
  • 80%的情况是替换成[MASK]
  • +
  • 10%的情况是替换为随机的token
  • +
  • 10%的情况是保持不变
  • +
+

Next sentence order

+

为了适配下游任务,使得模型懂得句子之间的关系,BERT加了一个新的训练任务,预测两个句子是不是下一句的关系。

+

具体来说:50%的概率,句子A和句子B是来自同一个文档的上下句,50%的概率,句子A和句子B不是同一个文档的上下句,具体的做法就是,采用从其他的文档(document)中,加入新的连续句子(segments)作为句子B。

+

预处理:subword

+

一般的词表示方法不能解决OOV

+

subword的粒度在词与字符之间,能较好的平衡OOV问题

+

Byte Pair Encoding:准备足够大的训练语料并确定期望的subword词表大小,将单词拆分为字符序列并在末尾添加后缀并统计单词频率,统计每一个字节对的出现频率,选择最高频率的合并成新的subword

+

BERT的缺点

+
    +
  1. MASK标记在实际预测中不会出现,训练时用过多[MASK]影响模型表现;
  2. +
  3. 每个batch只有15%的token被预测,所以BERT收敛得比left-to-right模型要慢(它们会预测每个token);
  4. +
  5. BERT对硬件资源的消耗巨大。
  6. +
  7. BERT在分词后做[MASK]会产生的其他问题,为了解决OOV则通常会把一个词切分成更细粒度的WordPiece。BERT在pre-training的时候是随机mask这些WordPiece的,这就可能出现只mask一个词的一部分的情况
  8. +
+

Bert参数量计算

+

https://zhuanlan.zhihu.com/p/452369195

+

Attention结构有什么优点

+
    +
  1. 一步到位捕捉全局与局部的联系:一步到位灵活地捕捉全局与局部的relevance信息,而且不存在信息的链式传递,可以很好的处理长距离依赖关系。Attention函数是将序列中的每个元素与其他元素的对比,每两个元素间(Query与Key)的距离都是1。而RNNs通过一步步递推得到长期依赖关系好的多,越长的序列RNN能捕捉到的长期依赖关系就越弱。
  2. +
  3. 并行计算减少模型训练时间:Attention机制每一步计算不依赖于上一步的计算结果,可以像CNN一样并行处理。但CNN每次只能捕捉局部信息,再通过层叠来扩大视野获取全局的联系。
  4. +
  5. 模型复杂度小,参数少:模型复杂度是与CNN和RNN同条件下相比较的。
  6. +
+

position embedding的作用是什么

+

对子中token次序信息进行编码

+

Residual Connection的作用是什么

+

减缓梯度衰减,加快收敛

+

加入LayerNorm层有什么好处

+

当使用梯度下降法做优化时,随着网络深度的增加,数据的分布会不断发生变化,加入Layer Normalization可以提高数据特征分布的稳定性,从而加速模型的收敛速度

+

加入Next Sentence Prediction任务的目的是什么

+

获得句子级的语义表征,对问答、推理、句⼦关系类的NLP任务帮助非常大。

+

Mask-LM的样本中,选中的词在10%的概率不做Mask保持真实的词的原因是什么

+

给模型一定的bias,相当于是额外的奖励,将模型对于词的表征能够拉向词的真实表征

+

Mask-LM的样本中,选中的词在10%的概率下不做mask,而是被随机替换成为一个其他词的目的是什么

+

因为模型不知道哪些词是被mask的,哪些词是mask了之后又被替换成了一个其他的词,这会迫使模型尽量在每一个词上都学习到一个全局语境下的表征,因而也能够让BERT获得更好的语境相关的词向量,提升模型的鲁棒性。

+

为什么即便数量很小,基于BERT做微调也能取得很好的泛化效果

+

这个问题最直观的解释是BERT提供了好的起点,模型的训练是站在巨人的肩膀上。但更恰当的解释是用BERT初始化模型,相当于提供了一种正则化,即便数据量很少也不容易过拟合。有了BERT做初始化,用少量的高质量数据可以训练出比大量劣质数据更好的模型。

+

Bert的CLS

+

与文本中已有的其它字/词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个字/词的语义信息,因此用来做分类任务

+

Bert衍生

+

AlBert

+
    +
  1. 提高参数的利用率 +
      +
    1. 矩阵分解-对Embedding进行降维
    2. +
    3. 权重共享-多个层使用相同的参数
    4. +
    +
  2. +
  3. 预训练策略 SOP 替代 NSP-不从不相关的句子中生成,而是将原来的两句话翻转
  4. +
  5. 模型增大,参数量增加
  6. +
  7. 移除dropout
  8. +
+

几种减少内存的方法,提升速度

+
    +
  • ①对Embedding进行因式分解
  • +
  • ②跨层的参数共享
  • +
  • ③句间连贯:负样本是正样本的顺序反转得来的
  • +
  • ④移除dropout
  • +
  • ⑤Segments-Pair
  • +
  • ⑥Masked-ngram-LM
  • +
+

主要通过参数共享减少参数量

+

RoBERTa

+

核心思想:通过更好地训练BERT可以达到超过其他新的预训练语言模型的效果

+

核心改动

+
    +
  1. 更大的Batch Size( 最大的 Batch Size 达到了 32K)
  2. +
  3. 去掉Next Sentence Prediction(在建模时需要注意这一点)
  4. +
  5. 采用更大的预训练语料(超过100G)
  6. +
  7. Dynamic Masking(BERT在训练时可能会固定地把一个地方 Mask几遍),每次向模型输入一个序列时都会生成新的掩码模式
  8. +
+

XLNet

+

XLNet专注于预训练阶段。在预训练阶段,它提出了一个新的目标,称为重排列语言建模

+

Bert的痛点:独立性假设:Bert假设不同[mask]相互独立,忽略了[mask]之间的相关性;Pre-train阶段和Fine-tune阶段数据分布不一致

+

排列组合获取上下文信息

+

双流注意力

+

XLNet:自回归(AR)语言模型 + 自动编码器(AE)语言模型

+

自回归(AR)语言模型:希望通过已知的前半句预测后面的词或字

+
    +
  • 优点:计算效率比较高
  • +
  • 缺点:只能编码单向语义
  • +
+

自动编码器语言模型:Mask

+
    +
  • 优势:可以从向前和向后的方向看到上下文
  • +
  • 缺点:预训练与调优的差异,且假设预测的词在给定未屏蔽的词的情况下彼此独立
  • +
+

XLNet:结合两种语言模型,用上下文预测,随机打乱顺序

+

双流自注意力机制:一个流包含了位置信息和内容信息,另外一个流仅包含位置信息

+

ERNIE

+

Bert聚焦在针对字或者英文Word粒度的完形填空学习上面,没有充分利用训练数据当中的词法结构,语法结构以及语义信息去学习建模

+

ERNIE直接对先验语义知识单元进行建模,增强了模型语义表示能力

+
    +
  • Mask的方式有所升级:在BERT的训练中,是以字符为单位进行训练的,而ERNIE则将MASK分为了3个级别: 字符级、实体级、短语级 总共整理了7个任务,7个任务分位3类,一类是词法层的任务,一类是语法类的任务,一类是语义类的任务,词法层的任务。知识融合和MLM任务。利用短语和实体级别的mask方式,来融合外部知识
  • +
  • 添加更多优质中文语料。加入了百度百科、百度新闻、百度贴吧等中文语料,使得在中文NLP任务上效果更好
  • +
  • DLM。对Dialog的角色,进行了Dialog embedding,从而加强模型在Dialog上的效果(如百度贴吧)。
  • +
+ + +
+ +
+
+ + + + + + +
+
+
Transformer面试题准备
+
https://zhangzhao219.github.io/2023/07/14/Interview/Interview-Questions-Transformer/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年7月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/07/29/Interview/Interview-Questions-LLM/index.html b/2023/07/29/Interview/Interview-Questions-LLM/index.html new file mode 100644 index 000000000..dd9cf797e --- /dev/null +++ b/2023/07/29/Interview/Interview-Questions-LLM/index.html @@ -0,0 +1,1343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LLM面试题准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

LLM面试题准备

+ + +
+ +

LLM面试题准备

+ +

GPT系列

+

GPT-1

+

动机:虽然无标注的文本很多,但是在下游任务上,有标注的文本很少。

+

GPT提出了一种方法:采用语言模型的方式在无标注文本下进行预训练,之后再在各个下游任务上进行微调。

+

模型主要是三个方向的贡献:

+
    +
  1. 如何在没有标注的数据集上进行预训练
  2. +
  3. 如何做微调
  4. +
  5. 如何在每个子任务上表示其输入
  6. +
+

预训练:输入含有大量token的语料库,GPT使用一个语言模型来极大化这个似然函数。具体的说,语言模型就是给定第 到第个词,预测第个词出现的概率。其中被称为滑动窗口,当的值被设置的很大的时候,模型将会看到更多的上文,当的值被设计的很小时,模型将会看到更少的上文。

+

+

语言模型的损失函数其实是一个乘法规则,因为有log所以变成加法

+

作者选择transformer的decoder作为骨干模型。

+

img

+

当输入是时,将这些词通过映射矩阵转化为词嵌入,再加上位置嵌入,再通过transformer块对其进行更新,最后输入到全连接层,得到最终的预测值。

+

而训练的过程其实非常的简单,就是将句子n个词的词向量(第一个为 <SOS>)加上Positional Encoding后输入到前面提到的Transfromer中,n个输出分别预测该位置的下一个词(<SOS>预测句子中的第一个词,最后一个词的预测结果不用于语言模型的训练)。

+

微调:当预训练后,作者将预训练好的参数直接迁移到下游任务中来。下游任务数据集中的每一个数据含有一系列的token:,标签为。将这些数据喂到预训练好参数的transformer decoder中,将得到的结果用softmax进行分类,得到最后的结果。

+

img

+

将预测值和真实值进行比对,得到有监督部分的损失函数,如下所示:

+

img

+

将预训练时的损失函数和有监督的损失函数加在一起,可以取得更好的效果

+

+

下游任务的损失=有监督的损失+预训练的损失

+

如何把NLP里面很不一样的子任务表示成一个我们想要的形式(表示成一个序列+对应的标签)

+

img

+
    +
  • classification分类任务:一段文字,之后输出这段文字的标签。start+文本+extract输入到transformer_block中,之后将得到的结果输入到线性分类器中,得到最终的结果。
  • +
  • entailment蕴含任务:给出两段文字,之后输出这两段文字是否是相互关联的。start+文本1+delim+文本2+extract输入到transformer_block中,之后将得到的结果输入到线性分类器中,得到最终的结果。
  • +
  • similarity相似任务:给出两段文字,之后输出这两段文字是否相关。start+文本1+delim+文本2;start+文本2+delim+文本1,将以上两个标签输入到transformer_block中,之后将得到的结果输入到线性分类器中,得到最终的结果。
  • +
  • multiple choice多项选择任务:给出一个问题和几个候选选项,之后挑选出正确的答案。start+文本+delim+候选选项1,……start+文本+delim+候选选项n,分别输入到transformer_block中,之后将得到的结果输入到线性分类器
  • +
+

GPT-2

+

GPT2不仅仅使用一个更大的数据集,使用更大的模型去学习,还提出了一个新的更难的任务,zero-shot零样本学习,即将预训练好的模型,直接接诸多的下游任务,不再进行微调操作,在多个任务下都可以取得很好的效果。

+

这两个模型的区别可以概括为:

+
    +
  1. 从数据量上,GPT 使用了约 5GB 数据,而 GPT2 利用了 40GB,并且质量更高;
  2. +
  3. 从模型的规模和参数量上说,GPT 有 1.17 亿的参数量,而 GPT2 使用了更深的网络结构,更高的隐藏层维度,参数量达到了15亿;
  4. +
  5. 模型结构方面 +
      +
    1. 后置层归一化( post-norm )改为前置层归一化( pre-norm )。
    2. +
    3. 在模型最后一个自注意力层之后,额外增加一个层归一化
    4. +
    5. 调整参数的初始化方式,按残差层个数进行缩放,缩放比例为
    6. +
    7. 输入序列的最大长度从 512 扩充到 1024,词表大小也增加
    8. +
    +
  6. +
  7. 训练任务方面,GPT2 放弃了 GPT 第二阶段的有监督训练,完全使用无监督任务进行语言模型训练。完全不需要去定义这个模型应该做什么任务,因为很多标签所蕴含的信息,就存在于语料当中。
  8. +
+

关于 post-norm 和 pre-norm,两者的主要区别在于,post-norm 将 transformer 中每一个 block 的层归一化放在了残差层之后,而 pre-norm 将层归一化放在了每个 block 的输入位置,GPT-2 进行上述模型调整的主要原因在于,随着模型层数不断增加,梯度消失和梯度爆炸的风险越来越大,这些调整能够 减少预训练过程中各层之间的方差变化,使梯度更加稳定 。如下图所示:

+

Pre Norm结构无形地增加了模型的宽度而降低了模型的深度,而我们知道深度通常比宽度更重要,所以是无形之中的降低深度导致最终效果变差了。Pre Norm结构会过度倾向于恒等分支(bottom layers),从而使得Pre Norm倾向于退化(degradation)为一个“浅而宽”的模型,最终不如同一深度的Post Norm

+

+

GPT-3

+

GPT3的参数量进一步的增大,并且使用few-shot learning的方法,取得了很好的效果。

+

GPT3特别大,在计算子任务的时候无法计算梯度,性能非常好。

+

最近一些年来,大家都使用预训练好的语言模型,之后再进行微调,这其实是有问题的:

+
    +
  1. 微调需要对每一个任务有一个任务相关的数据集以及和任务相关的微调。
  2. +
  3. 需要一个大的数据集,需要对其进行标号,当一个样本没有出现在数据分布的时候,泛化性不见得比小模型要好,
  4. +
+

GPT-3提出了一种in-context learning的方法,就是给出任务的描述和一些参考案例的情况下,模型能根据当前任务描述、参数案例明白到当前的语境,即使在下游任务和预训练的数据分布不一致情况下,模型也能表现很好。注意的是,GPT并没有利用实例进行Fine-tune,而是让案例作为一种输入的指导,帮助模型更好的完成任务。

+

在模型结构上,GPT-3 延续使用 GPT 模型结构,但是引入了 Sparse Transformer 中的 sparse attention 模块(稀疏注意力)。

+

sparse attention 与传统 self-attention(称为 dense attention) 的区别在于:

+

dense attention:每个 token 之间两两计算 attention,复杂度 O(n²)

+

sparse attention:每个 token 只与其他 token 的一个子集计算 attention,复杂度 O(n*logn),具体来说,sparse attention 除了相对距离不超过 k 以及相对距离为 k,2k,3k,… 的 token,其他所有 token 的注意力都设为 0

+

使用 sparse attention 的好处主要有以下两点:

+
    +
  1. 减少注意力层的计算复杂度 ,节约显存和耗时,从而能够处理更长的输入序列;
  2. +
  3. 具有“局部紧密相关和远程稀疏相关”的特性 ,对于距离较近的上下文关注更多,对于距离较远的上下文关注较少;
  4. +
+

GPT3是一个1750亿参数的模型,评估用的是三种方法:

+
    +
  1. Few-shot learning,对每个子任务,提供大概10-100个训练样本:在预训练和真正翻译的样本之间,插入多个样本做指导。好比说在预训练好的结果和所要执行的任务之间,给多个例子,告诉模型应该如何工作。
  2. +
  3. one-shot,也就是每一个类别只有一个样本:在预训练和真正翻译的样本之间,插入一个样本做指导。好比说在预训练好的结果和所要执行的任务之间,给一个例子,告诉模型英语翻译为法语,应该这么翻译。
  4. +
  5. zero-shot,一个样本都不提供,直接让其进行测试:先给出任务的描述,之后给出一个测试数据对其进行测试,直接让预训练好的模型去进行任务测试。
  6. +
+

img

+

整体来看,GPT-3 相比于 GPT-2 有如下几点区别:

+
    +
  1. 效果上 ,超出 GPT-2 非常多,能生成人类难以区分的新闻文章;
  2. +
  3. 主推 few-shot ,相比于 GPT-2 的 zero-shot,具有很强的创新性;
  4. +
  5. 模型结构略微变化,采用 sparse attention 模块;
  6. +
  7. 海量训练语料 45TB(清洗后 570GB),相比于 GPT-2 的 40GB;
  8. +
  9. 海量模型参数 ,最大模型为 1750 亿,GPT-2 最大为 15 亿参数;
  10. +
+

GPT缺点

+
    +
  1. 当生成文本长度较长时 ,GPT-3 还是会出现各种问题,比如重复生成一段话,前后矛盾,逻辑衔接不好等等;
  2. +
  3. 模型和结构的局限性 ,对于某一些任务,比如填空类型的文本任务,使用单向的自回归语言模型确实存在一定的局限性,这时候如果同时考虑上文和下文的话,效果很可能会更好一些;
  4. +
  5. 预训练语言模型的通病,在训练时,语料中所有的词都被同等看待,对于一些虚词或无意义的词同样需要花费很多计算量去学习, 无法区分学习重点
  6. +
  7. 样本有效性或者利用率过低 ,训一个模型几乎要把整个互联网上的文本数据全都用起来,这与我们人类学习时所需要的成本存在非常大的差异,这方面也是未来人工智能研究的重点;
  8. +
  9. 有一个不太确定的点是,模型到底是在“ 学习 ”还是在“ 记忆 ”?我们当然希望它能够学习,但是在使用数据量如此大的情况下,很难去判断它到底是什么样的;
  10. +
  11. 众所周知,GPT-3 的训练和使用成本都太大了;
  12. +
  13. GPT-3 跟很多深度学习模型一样,都是不可解释的,没办法知道模型内部到底是如何作出一系列决策的;
  14. +
  15. 模型最终呈现的效果取决于训练数据,这会导致模型会出现各种各样的“ 偏见 ”;
  16. +
+

InstructGPT

+

GPT-3 虽然在各大 NLP 任务以及文本生成的能力上令人惊艳,但是他仍然还是会生成一些带有偏见的,不真实的,有害的造成负面社会影响的信息,而且很多时候,他并不按人类喜欢的表达方式去说话。在这个背景下,OpenAI 提出了一个概念“Alignment”,意思是模型输出与人类真实意图对齐,符合人类偏好。因此,为了让模型输出与用户意图更加 “align”,就有了 InstructGPT 这个工作。

+

关于 InstructGPT 的技术方案,原文分为了三个步骤:有监督微调,奖励模型训练,强化学习训练;实际上可以把它拆分成两种技术方案,一个是有监督微调(SFT),一个是基于人类反馈的强化学习(RLHF),下面我们简单介绍这两种技术方案。

+

SFT(Supervised Fine-Tuning)

+

+

本质上来说,SFT 可以理解为人工标注了一批数据,然后去微调 GPT-3。

+

这里标注的数据与 GPT-3 之前用来做下游任务使用的 few-shot 格式,有非常本质的区别。

+

GPT-3 中的 few-shot 对于同一个下游任务,通常采用固定的任务描述方式,而且需要人去探索哪一种任务表述方式更好。显然这种模式与真实场景下用户的使用方式存在较大的 gap,用户在向 GPT-3 提问时才不会采用某种固定的任务表述,而是随心所欲地以自己的说话习惯去表达某个需求。

+

InstructGPT 在 SFT 中标注的数据,正是为了消除这种模型预测与用户表达习惯之间的 gap。在标注过程中,他们从 GPT-3 的用户真实请求中采样大量下游任务的描述,然后让标注人员对任务描述进行续写,从而得到该问题的高质量回答。这里用户真实请求又被称为某个任务的指令,即 InstructGPT 的核心思想“基于人类反馈的指令微调”。

+

RLHF(Reinforcement Learning from Human Feedback)

+

+

基于 SFT 得到的模型被用于后续的 RLHF 做进一步的模型优化。

+

如上图所示,以摘要生成任务为例,详细展示了如何基于人类反馈进行强化学习,最终训练完成得到 InstructGPT 模型。主要分为三步:

+
    +
  1. 收集人类反馈 :使用初始化模型对一个样本生成多个不同摘要,人工对多个摘要按效果进行排序,得到一批排好序的摘要样本;
  2. +
  3. 训练奖励模型 :使用第1步得到的样本集,训练一个模型,该模型输入为一篇文章和对应的一个摘要,模型输出为该摘要的得分;
  4. +
  5. 训练策略模型 :使用初始化的策略模型生成一篇文章的摘要,然后使用奖励模型对该摘要打分,再使用打分值借助 PPO 算法重新优化策略模型;
  6. +
+

直接偏好优化(DPO)

+

+

直接偏好优化 (DPO) 是一种微调大型语言模型 (LLM)以符合人类偏好的新颖方法。与涉及来自人类反馈的复杂强化学习 (RLHF) 的传统方法不同, DPO简化了流程。它的工作原理是创建人类偏好对的数据集,每个偏好对都包含一个提示和两种可能的完成方式——一种是首选,一种是不受欢迎。然后对LLM进行微调,以最大限度地提高生成首选完成的可能性,并最大限度地减少生成不受欢迎的完成的可能性。

+

与 RLHF 相比,DPO 具有多项优势:

+
    +
  • 简单性: DPO更容易实施和训练,使其更易于使用。
  • +
  • 稳定性: 不易陷入局部最优,保证训练过程更加可靠。
  • +
  • 效率:与 RLHF 相比, DPO 需要更少的计算资源和数据,使其计算量轻。
  • +
  • 有效性: 实验结果表明,DPO在情感控制、摘要和对话生成等任务中可以优于 RLHF 。
  • +
+

InstructGPT 总结

+

总的来说,InstructGPT 相对于之前的 GPT 系列,有以下几点值得注意:

+
    +
  1. 解决 GPT-3 的输出与人类意图之间的 Align 问题;
  2. +
  3. 让具备丰富世界知识的大模型,学习“人类偏好”;
  4. +
  5. 标注人员明显感觉 InstructGPT 的输出比 GPT-3 的输出更好,更可靠;
  6. +
  7. InstructGPT 在真实性,丰富度上表现更好;
  8. +
  9. InstructGPT 对有害结果的生成控制的更好,但是对于“偏见”没有明显改善;
  10. +
  11. 基于指令微调后,在公开任务测试集上的表现仍然良好;
  12. +
  13. InstructGPT 有令人意外的泛化性,在缺乏人类指令数据的任务上也表现很好;
  14. +
+

GPT-4

+
    +
  • GPT-4可以接受包含文本和图像的提示,但是输出只能是文本
  • +
  • 支持更长的上下文窗口
  • +
  • 扮演一个角色,风格可控
  • +
  • 预测很自信,但是RLHF后自信会减少
  • +
  • 改善幻觉、安全等局限性
  • +
  • 用小模型预测大模型的loss非常稳定
  • +
+

思维链CoT

+

单纯的扩大LLM模型的参数量无法让模型在算术推理/常识推理/符号推理等推理任务上取得理想的效果。 如何提升LLM在这些推理任务上性能呢?首次提出思维链(Chain-of-Throught,CoT)的概念,思维链就是一系列中间的推理步骤。

+

在问LLM问题前,手工在prompt里面加入一些 包含思维过程(Chain of thought)的问答示例 ,就可以让LLM在推理任务上大幅提升。CoT的方法,就是在 In-Context-Learning 的范式中,增加了对推理的示范,从而希望LLM在给出答案的时候,也像模像样地进行推理。

+

思维链提示作为一种促进语言模型推理的方法,有几个吸引人的特性。

+
    +
  1. 首先,思维链,在原则上,允许模型将多步骤问题分解成中间步骤,这意味着额外的计算可以分配给需要更多推理步骤的问题。
  2. +
  3. 第二,一个思维链为模型的行为提供了一个可解释的窗口,表明它是如何得到一个特定的答案的,并提供了调试推理路径出错的机会(尽管完整地描述支持一个答案的模型的计算仍然是一个开放的问题)。
  4. +
  5. 第三,思维链推理可以用于诸如数学应用题、常识推理和符号操作等任务,并且可能(至少在原则上)适用于任何人类可以通过语言解决的任务。
  6. +
  7. 最后,思想链推理可以很容易地在足够大的现成语言模型中得到,只需将思想链序列的示例包含到小样本提示的范例中即可。
  8. +
+

Zero-shot CoT

+

大模型,尤其是足够大的模型,可能不需要你写一堆CoT来作为prompt了,它自己可能就会推理了,秘诀就是加上一句咒语:“Let’s think step by step.”

+

具体则是需要LLM两次生成:

+
    +
  1. 先使用 “Let’s think step by step.” 让模型自己给出推理过程
  2. +
  3. 把原始问题以及给出的推理过程再合在一起,让模型抽取出最终答案。
  4. +
+
    +
  • Zero-shot CoT和Few-shot CoT在常识推理问题(CommonsenseQA)上,并没有太大的提升(相比于数学推理)。很多时候CoT给不出正确的答案,但是推理过程却是合理且灵活的。Zero-shot CoT在多项选择时,倾向于给出多个答案,很难只给出一个答案。
  • +
  • 在数学推理问题上,CoT能有显著的提升,但是Zero-shot CoT和Few-shot CoT犯错误时的特点很不一样:Zero-shot方法在推出正确答案后,可能会继续“画蛇添足”,导致最终错误;另外,Zero-shot有时候干脆不推理,直接重复题目。Few-shot方法则是在生成的推理过程中包含三元运算的时候很容易出错,例如(3+2)*4
  • +
+

Auto CoT

+

能不能利用 Zero-shot CoT 来让 LLM 产生很多带有推理的QA pair,然后把这些QA pair加入到prompt中,构成ICL的上文,再让LLM进行推理。

+

一大堆的待测试的问题 (没有标注,不知道正确答案和推理过程),我们要怎么利用 LLM 和这么一个无标注问题集合,在不进行手工编写CoT的情况下,提升LLM回答这些模型的质量。

+

作者的基本思路是这样的:

+
    +
  • 给定待测试的问题q,从无标注问题集合中,采样一批问题;
  • +
  • 使用 GPT-3 作为产生推理过程的工具,即直接使用 “Let’s think step by step.” 咒语,来对这一批采样的问题产生推理过程;
  • +
  • 把产生的这些问题和推理过程,构成In-Context-Learning的上文加入到prompt中,再让LLM对问题q进行回答。
  • +
+

关键就在于这个采样过程,作者分别先测试了两种简单的采样过程:

+
    +
  1. 随机采样,Random-Q-CoT
  2. +
  3. 基于跟待测试q的相似度进行采样,Retrieval-Q-CoT
  4. +
+

实验发现,居然随机采样还要更好一些。经过探究,作者发现GPT-3自动产生推理过程是有一定比例出错的,而 出错的问题也容易聚集 ,因此基于相似度搜索的时候,容易导致采样出一批错误的示范,而随机采样的方法,则可能避免聚集性地出错。基于这样的考虑,作者设计了基于多样性的采样方法,先试用SentenceBERT对所有问题进行聚类,然后从每个cluster中进行采样

+

Auto的方法居然可以比Manual更好。其实有一种解释,Manual方法其实给多个任务都使用的是同一套模板,比方6个数学任务里面5个都使用的同一套示例(为了省力,同时Manual-CoT的论文也不是为了刷榜,而是为了揭示这么一个现象,所以CoT没有进行仔细调优),而Auto-CoT则是每个任务都会有自己的一套示例产生,毕竟问题集合不一样,聚类的结果也会不一样。

+

LLaMA

+

给定一个目标性能水平,首选的模型不是训练速度最快的,而是推理速度最快的,尽管训练一个大的模型以达到一定的性能水平可能更便宜,但训练时间较长的小模型最终会在推理中更便宜。

+

代码详解

+

tokenizer使用的是BPE算法

+

img

+

+

结构修改

+
    +
  • Pre-normalization (借鉴于GPT-3)作者选择在transformer的入口处使用normalization,而不是在出口,该技巧称为pre-normalization,作者认为这样做有助于提升训练的稳定性。使用的normalization为RMS Norm +
      +
    • 相同的深度条件下,Post-Norm的效果要优于Pre-Norm,因为Pre-Norm实际上相当于通过了一个更宽的网络而非更深的网络,所以在同等深度下,Pre-Norm的实际效果相当于一个更浅却更宽的网络。然而在LLaMA中却采用了Pre-Norm,或许是因为模型够深,而Pre-Norm的恒等分支更加明显,有利于梯度的传播。
    • +
    • RMS Norm 在梯度下降时令损失更加平滑,与layerNorm相比,RMS Norm的主要区别在于去掉了减去均值的部分(re-centering),只保留方差部分(re-scaling)
    • +
    +
  • +
  • SwiGLU 激活函数 (借鉴于PaLM)x * sigmoid(x) GLU(x) = x ⊗ σ(g(x)),相较于ReLU函数,SiLU函数可能会更适合一些需要保留更多输入信息的场景。
  • +
  • Rotary Embeddings (借鉴于GPTNeo):作者在模型的每一层transformer结构中都加入RoPE了,采用绝对位置编码的形式,实现相对位置编码
  • +
+

一般认为,Post-Norm在残差之后做归一化,对参数正则化的效果更强,进而模型的收敛性也会更好;而Pre-Norm有一部分参数直接加在了后面,没有对这部分参数进行正则化,可以在反向时防止梯度爆炸或者梯度消失,大模型的训练难度大,因而使用Pre-Norm较多

+

加速技巧

+
    +
  • 使用xformer中高效的multi-head attention,以减小内存和加速
  • +
  • 基于checkpointing技术,减少了反向传播过程中的激活函数重计算,减少了在check point的后向传递中重新计算的激活量,在实现上,通过手动实现transformer层的后向函数来进行操作。为了充分受益于这种优化,还通过如Korthikanti等人(2022)中采用的方法,进行使用模型和序列并行来减少模型的内存使用。
  • +
  • 尽可能的使网络中GPU间的计算和通信重合(这里没说明是如何实现的?)
  • +
+

LLaMA 2

+

与Llama 1相比,主要的架构差异包括增加的上下文长度和分组查询注意力(GQA)

+
    +
  • 上下文长度: Llama 2 的上下文窗口从 2048 个标记扩展到 4096 个字符。 越长上下文窗口使模型能够处理更多信息,这对于支持聊天应用程序中较长的历史记录、各种摘要任务以及理解较长的文档。多个评测结果表示较长的上下文模型在各种通用任务上保持了强大的性能。
  • +
  • Grouped-Query Attention 分组查询注意力:(1)自回归解码的标准做法是缓存序列中先前标记的键 (K) 和值 (V) 对,从而加快注意力计算速度。 然而,随着上下文窗口或批量大小的增加,多头注意力 (MHA) 模型中与 KV 缓存大小相关的内存成本显着增长。 对于较大的模型,KV 缓存大小成为瓶颈,键和值投影可以在多个头之间共享,而不会大幅降低性能。 GQA将查询头分成G组,每个组共享一个Key 和 Value 矩阵。GQA-G是指具有G组的grouped-query attention。GQA-1具有单个组,因此具有单个Key 和 Value,等效于MQA。而GQA-H具有与头数相等的组,等效于MHA。
  • +
+

Llama 2-Chat的训练过程:该过程始于使用公开可用的在线资源对Llama 2进行 预训练 。随后,我们通过有监督的微调创建Llama 2-Chat的初始版本。随后,我们使用强化学习与人类反馈( RLHF )方法,具体包括拒绝抽样和近端策略优化(PPO),对模型进行迭代优化。在RLHF阶段,迭代奖励建模数据的积累与模型改进密切相关,以确保奖励模型保持在分布内。

+

Mistral 7B

+

Sliding Window Attention ,attention 中的操作数量与序列长度呈二次关系,通过Sliding Window Attention,可减少计算,但是会牺牲一点的效果。

+

做法如下,第2层中的位置4的隐藏状态,关注来自前一层中位置在4- W和4之间的所有隐藏状态,下图中w=3

+

+

img

+

Rolling Buffer Cache, 显存消耗与序列长度呈二次关系。当长度比较长时,显存的消耗是比较多的

+

Rolling Buffer Cache使用的是LRU算法,选择最久未使用的数据予以淘汰,相当于缓存最新数据。

+

img

+

缓存预处理

+

img

+

在生成序列时,由于每个标记的生成都依赖于前一个标记,因此需要逐个预测。但是,在开始生成之前,提示信息是已知的,我们可以预先将提示信息填充到(k, v)缓存中。

+

具体而言,首先将已知的提示信息按照选定的块大小进行分段,并将每一小段的数据预处理后填充到滚动缓冲区缓存中。在生成新标记的过程中,模型会根据当前时刻的输入以及缓存中的历史信息来计算注意力权重,并更新隐藏状态。这样,在生成长序列时,通过预先填充和分块技术,可以有效地利用已知信息并保持内存使用量的可控性,同时确保模型能充分考虑整个上下文信息进行预测。

+

Qwen

+

Pretraining Data

+

去重:标准化后进行完全匹配重复数据删除,以及使用 MinHash 和 LSH 算法进行模糊重复数据删除

+

过滤质量低的数据:过滤低质量的数据,采用了规则型和基于机器学习的方法的组合。多个模型对内容进行评分,包括语言模型,文本质量评分模型以及用于识别潜在的攻击性或不适当内容的模型。 人工从各种来源中对文本进行抽样并审阅,以确保其质量

+

高质量指令:由于多任务指令可以增强他们的零样本和少样本性能,预训练过程中加入了高质量的指令数据。

+

Tokenizer

+

使用基于bytepair encoding (BPE) 的tiktoken算法,其相当于BPE tokenizer分词更快。首先使用cl100k作为base token,针对连续数字分会拆为单独的数字,最终 词典大小为152K

+

编码压缩率越小,则传递的信息就更多,每种语言100万个文档语料库来测试和比较不同模型的编码压缩率,

+

可看到qwen编码压缩率是比较低的

+

Architecture

+

Embedding和output 投影层:解开输入嵌入和输出投影的权重,这一决定是为了以内存成本为代价获得更好的性能

+

位置嵌入:RoPE,选择使用FP32精度的逆频率矩阵,而不是BF16或FP16,以优先考虑模型性能并获得更高的准确性。

+

激活函数:SwiGLU

+

normal方法:RMSNorm,前馈网络(FFN)的维度从隐藏大小的4倍减少到隐藏大小的83倍

+

content长度:

+

长度外推,在QKV注意力层中添加bias以增强模型的外推能力。下图可看到加上了bias,长度大于1024效果下降不是很多。

+

NTK-aware interpolation,动态 NTK-aware 插值,则每个块比例不同:高频部分外推,低频部分内插。

+

LogN-Scaling,q和v乘以一个系数,context length和training length的长度关系,来保持注意力的稳定。保证注意力的熵在上下文长度增加时也保持稳定,同时能提升外推表现。

+

window attention,将注意力限制在有限的上下文窗口内,防止模型关注距离太远的标记。基于这一发现,我们为每个层分配不同的窗口大小,对较低层使用较短的窗口,对较高层使用较长的窗口。

+

Baichuan

+

Pre-training

+

来源收集数据,包括常规互联网网页、书籍、研究论文、代码库等,以构建一个广泛的世界知识体系。

+

去重,构建了一个大规模的重复数据删除和聚类系统,支持LSH类似特征和稠密嵌入特征。最终只保留原始数据的31.68%的数据进行训练。

+

Tokenizer

+

字节对编码(BPE), 不对输入文本应用任何规范化,也不添加虚拟前缀 。将数字拆分为单独的数字,处理额外空格的代码数据,向分词器添加仅空格标记,最大标记长度设置为32,以处理长中文词组。

+

Architecture

+

位置嵌入:RoPE

+

激活函数:SwiGLU

+

注意力层:xFormers减少内存。

+

normal方法:RMSNorm,并且 规范化输出嵌入lm_head

+

norm_head loss

+

最大z损失 ,在训练过程中,发现LLM的logits可能变得非常大。添加了一个最大z损失来规范化logits。其中z是最大logit值,这有助于稳定训练,并使推理更加稳健地适应超参数。

+

img

+

大模型基础知识

+

为何Decoder only结构

+
    +
  1. Encoder的低秩问题 :Encoder的双向注意力会存在低秩问题,这可能会削弱模型表达能力,就生成任务而言,引入双向注意力并无实质好处。
  2. +
  3. 更好的Zero-Shot性能、更适合于大语料自监督学习 :decoder-only 模型在没有任何 tuning 数据的情况下、zero-shot 表现最好,而 encoder-decoder 则需要在一定量的标注数据上做 multitask finetuning 才能激发最佳性能。
  4. +
  5. 效率问题 :decoder-only支持一直复用KV-Cache,对多轮对话更友好,因为每个Token的表示之和它之前的输入有关,而encoder-decoder和PrefixLM就难以做到。
  6. +
+

LLMs复读机问题

+

LLMs复读机问题(LLMs Parroting Problem)是指大型语言模型在生成文本时过度依赖输入文本的复制,而缺乏创造性和独特性。当面对一个问题或指令时,模型可能会简单地复制输入文本的一部分或全部内容,并将其作为生成的输出,而不是提供有意义或新颖的回应。

+
    +
  1. 数据偏差 :大型语言模型通常是通过预训练阶段使用大规模无标签数据进行训练的。如果训练数据中存在大量的重复文本或者某些特定的句子或短语出现频率较高,模型在生成文本时可能会倾向于复制这些常见的模式。
  2. +
  3. 训练目标的限制 :大型语言模型的训练通常是基于自监督学习的方法,通过预测下一个词或掩盖词来学习语言模型。这样的训练目标可能使得模型更倾向于生成与输入相似的文本,导致复读机问题的出现。
  4. +
  5. 缺乏多样性的训练数据 :虽然大型语言模型可以处理大规模的数据,但如果训练数据中缺乏多样性的语言表达和语境,模型可能无法学习到足够的多样性和创造性,导致复读机问题的出现。
  6. +
  7. 模型结构和参数设置 :大型语言模型的结构和参数设置也可能对复读机问题产生影响。例如,模型的注意力机制和生成策略可能导致模型更倾向于复制输入的文本。
  8. +
+

为了缓解LLMs复读机问题,可以尝试以下方法:

+
    +
  1. 多样性训练数据 :在训练阶段,使用多样性的语料库来训练模型,避免数据偏差和重复文本的问题。这可以包括从不同领域、不同来源和不同风格的文本中获取数据。
  2. +
  3. 引入噪声 :在生成文本时,引入一些随机性或噪声,例如通过采样不同的词或短语,或者引入随机的变换操作,以增加生成文本的多样性。这可以通过在生成过程中对模型的输出进行采样或添加随机性来实现。
  4. +
  5. 温度参数调整 :温度参数是用来控制生成文本的多样性的一个参数。通过调整温度参数的值,可以控制生成文本的独创性和多样性。较高的温度值会增加随机性,从而减少复读机问题的出现。
  6. +
  7. Beam搜索调整 :在生成文本时,可以调整Beam搜索算法的参数。Beam搜索是一种常用的生成策略,它在生成过程中维护了一个候选序列的集合。通过调整Beam大小和搜索宽度,可以控制生成文本的多样性和创造性。
  8. +
  9. 后处理和过滤 :对生成的文本进行后处理和过滤,去除重复的句子或短语,以提高生成文本的质量和多样性。可以使用文本相似度计算方法或规则来检测和去除重复的文本。
  10. +
  11. 人工干预和控制 :对于关键任务或敏感场景,可以引入人工干预和控制机制,对生成的文本进行审查和筛选,确保生成结果的准确性和多样性。
  12. +
+

Tokenizer

+

BPE

+

BPE,即字节对编码。其核心思想在于将 最常出现的子词对合并,直到词汇表达到预定的大小时停止

+

BPE是一种基于数据压缩算法的分词方法。它通过不断地合并出现频率最高的字符或者字符组合,来构建一个词表。具体来说,BPE的运算过程如下:

+
    +
  1. 将所有单词按照字符分解为字母序列。例如:“hello”会被分解为[“h”,“e”,“l”,“l”,“o”]。
  2. +
  3. 统计每个字母序列出现的频率,将频率最高的序列合并为一个新序列。
  4. +
  5. 重复第二步,直到达到预定的词表大小或者无法再合并。
  6. +
+

WordPiece

+

wordpiece算法可以看作是BPE的变种。不同的是,WordPiece基于概率生成新的subword而不是下一最高频字节对。WordPiece算法也是每次从词表中选出两个子词合并成新的子词。BPE选择频数最高的相邻子词合并,而 WordPiece选择使得语言模型概率最大的相邻子词加入词表 。即它每次合并的两个字符串A和B,应该具有最大的值。合并AB之后,所有原来切成A+B两个tokens的就只保留AB一个token。

+

Unigram

+

与BPE或者WordPiece不同,Unigram的算法思想是 从一个巨大的词汇表出发 ,再 逐渐删除trim down其中的词汇 ,直到size满足预定义。

+

初始的词汇表可以 采用所有预分词器分出来的词,再加上所有高频的子串

+

每次从词汇表中删除词汇的原则是使预定义的损失最小

+

SentencePiece

+

SentencePiece,顾名思义,它是 把一个句子看作一个整体,再拆成片段 ,而没有保留天然的词语的概念。一般地,它 把空格space也当作一种特殊字符来处理,再用BPE或者Unigram算法来构造词汇表

+

遇到OOV(Out Of Vocabulary)怎么做?

+

也就是词汇表外的词。在NLP中,通常会预先构建一个词汇表,包含所有模型能够识别的词。然而,总会有一些词没有出现在预先构建的词汇表中,这些词就是 OOV。传统的处理方式往往是将这些 OOV 映射到一个特殊的符号,如 UnKnow,但这种方式无法充分利用 OOV 中的信息。例如,对于词汇表中没有的词 “unhappiness”,如果直接映射为UnKnow ,则模型就无法理解它的含义。

+

WordPiece/Byte Pair Encoding (BPE) 等基于子词的分词方法提供了一种解决 OOV 问题的方式。现在更多的语言大模型选择基于BPE的方式,只不过BERT时代更多还是WordPiece。BPE 通过将词分解为更小的单元(子词或字符),可以有效地处理词汇表外的词。对于上面的 “unhappiness” 例子,即使 “unhappiness” 本身不在词汇表中,但是它可以被分解为 “un”、“happiness” 等子词,而这些子词可能在词汇表中。这样,模型就可以通过这些子词来理解 “unhappiness” 的含义。另一方面就是,BPE本身的语义粒度也很合适,一个token不会太大,也不会小到损失连接信息(如一个字母)。

+

LLM长文本

+

理论上来说,LLMs(大型语言模型)可以处理任意长度的输入句子,但实际上存在一些限制和挑战 。下面是一些相关的考虑因素:

+
    +
  1. 计算资源 :生成长句子需要更多的计算资源,包括内存和计算时间。由于LLMs通常是基于神经网络的模型,计算长句子可能会导致内存不足或计算时间过长的问题。
  2. +
  3. 模型训练和推理 :训练和推理长句子可能会面临一些挑战。在训练阶段,处理长句子可能会导致梯度消失或梯度爆炸的问题,影响模型的收敛性和训练效果。在推理阶段,生成长句子可能会增加模型的错误率和生成时间。
  4. +
  5. 上下文建模 :LLMs是基于上下文建模的模型,长句子的上下文可能会更加复杂和深层。模型需要能够捕捉长句子中的语义和语法结构,以生成准确和连贯的文本。
  6. +
+

要让大模型处理更长的文本,可以考虑以下几个方法:

+
    +
  1. 分块处理 :将长文本分割成较短的片段,然后逐个片段输入模型进行处理。这样可以避免长文本对模型内存和计算资源的压力。在处理分块文本时,可以使用重叠的方式,即将相邻片段的一部分重叠,以保持上下文的连贯性。
  2. +
  3. 层次建模 :通过引入层次结构,将长文本划分为更小的单元。例如,可以将文本分为段落、句子或子句等层次,然后逐层输入模型进行处理。这样可以减少每个单元的长度,提高模型处理长文本的能力。
  4. +
  5. 部分生成 :如果只需要模型生成文本的一部分,而不是整个文本,可以只输入部分文本作为上下文,然后让模型生成所需的部分。例如,输入前一部分文本,让模型生成后续的内容。
  6. +
  7. 注意力机制 :注意力机制可以帮助模型关注输入中的重要部分,可以用于处理长文本时的上下文建模。通过引入注意力机制,模型可以更好地捕捉长文本中的关键信息。
  8. +
  9. 模型结构优化 :通过优化模型结构和参数设置,可以提高模型处理长文本的能力。例如,可以增加模型的层数或参数量,以增加模型的表达能力。还可以使用更高效的模型架构,如Transformer等,以提高长文本的处理效率。
  10. +
+

长度外推

+

大模型在训练时和预测时的输入长度不一致,导致模型的泛化能力下降的问题 。在目前的大模型中,一般指的是超出预训练设置的上下文长度时,依旧保持良好推理效果的能力。

+
    +
  1. 进制表示
  2. +
+

我们将整数n以一个三维向量[a,b,c]来输入,a,b,c分别是n的百位、十位、个位。这样,我们既缩小了数字的跨度,又没有缩小相邻数字的差距,代价了增加了输入的维度——刚好,神经网络擅长处理高维数据。

+

如果想要进一步缩小数字的跨度,我们还可以进一步缩小进制的基数,如使用8进制、6进制甚至2进制,代价是进一步增加输入的维度。

+
    +
  1. 直接外推
  2. +
+

简单来说,假如原来位置编码用三维向量表示,那外插就是直接增加一维。

+

可以提前预留多几维,训练阶段设为0,推理阶段直接改为其他数字,这就是外推(Extrapolation)。

+

然而,训练阶段预留的维度一直是0,如果推理阶段改为其他数字,效果不见得会好,因为模型对没被训练过的情况不一定具有适应能力。也就是说, 由于某些维度的训练数据不充分,所以直接进行外推通常会导致模型的性能严重下降

+
    +
  1. 线性插值
  2. +
+

就是将2000以内压缩到1000以内,比如通过除以2,1749就变成了874.5,然后转为三维向量[8,7,4.5]输入到原来的模型中。从绝对数值来看,新的[7,4,9]实际上对应的是1498,是原本对应的2倍,映射方式不一致;从相对数值来看,原本相邻数字的差距为1,现在是0.5,最后一个维度更加“拥挤”。所以,做了内插修改后,通常都需要微调训练,以便模型重新适应拥挤的映射关系。

+

不过,内插方案也不尽完美,当处理范围进一步增大时,相邻差异则更小,并且这个相邻差异变小集中在个位数,剩下的百位、十位,还是保留了相邻差异为1。换句话说, 内插方法使得不同维度的分布情况不一样,每个维度变得不对等起来,模型进一步学习难度也更大

+
    +
  1. 进制转换
  2. +
+

有没有不用新增维度,又能保持相邻差距的方案呢? 进制转换 !三个数字的10进制编码可以表示0~999,如果是16进制呢?它最大可以表示163−1=4095>1999。所以,只需要转到16进制,如1749变为[6,13,5],那么三维向量就可以覆盖目标范围,代价是每个维度的数字从0~9变为0~15。

+

这个进制转换的思想,实际上就对应着 NTK-aware scaled RoPE!

+

长度外推需要关注的两个点:

+
    +
  1. 预测时位置编码的外推 :没见过的就无法保证很好的泛化,不仅学习式位置编码如此;像正弦位置编码、RoPE也有这样的问题,它们自身虽然不用学习,但是会影响上层参数的学习;
  2. +
  3. 预测时序列更长,导致注意力相比训练时更分散 :序列长度增大意味着attention分布的熵增大了,注意力更分散了;
  4. +
+

大模型训练共同点

+

llama2、qwen和baichuan2的论文 都提到使用RoPE位置嵌入、SwiGLU激活函数、RMSNorm方法。并且都在尽可能实现更长长度的预测

+

大模型训练不同点

+

数据上,qwen和baichuan2 去重上做了许多工作 。并且qwen在数据质量上做了两方面工作首先 过滤低质量语料 ,其次加入高质量指令提高预训练效果。

+

模型结构上:都在更长预测上下文长度进行提升,只是每个模型使用方式不一样。llama2使用 GQA ,Mistral 使用 Sliding Window Attention 和 Rolling Buffer Cache。 qwen在 QKV注意力层中添加bias以增强模型的外推能力、NTK-aware interpolation、LogN-Scaling和window attention

+

另外,baichuan2使用规范化输出嵌入lm_head和最大z损失提升模型稳定性。qwen在 核心的矩阵计算中使用FP32换取更好效果

+

大模型推理

+
    +
  • Temperature:temperature参数控制生成语言模型中生成文本的随机性和创造性,调整模型的softmax输出层中预测词的概率;当temperature较高时,会更平均地分配概率给各个token,这导致生成的文本更具随机性和多样性;temperature较低接近0时,会倾向于选择概率最高的token,从而使生成的文本更加确定和集中。注:temperature=1时表示不使用此方式。
  • +
  • top-p 是一个用于控制生成文本多样性的参数,也被称为"nucleus sampling"。这个参数的全名是"top probability",通常用一个介于 0 到 1 之间的值来表示生成下一个token时,在概率分布中选择的最高概率的累积阈值
  • +
  • top_k 用于在生成下一个token时,限制模型只能考虑前k个概率最高的token,这个策略可以降低模型生成无意义或重复的输出的概率,同时提高模型的生成速度和效率。
  • +
  • repetition_penalty 的目标是在这个概率分布中对先前生成过的token,又重复的生成了该token进行惩罚(降低概率),以减少生成文本中的重复性
  • +
  • do_sample 对模型计算出来的概率要不要进行多项式采样(从一个具有多个可能结果的离散概率分布中进行随机抽样)。首先,根据概率分布对应的概率,为每个可能结果分配一个抽样概率。这些抽样概率之和必须为1。然后,在进行一次抽样时,会根据这些抽样概率来选择一个结果。具体地,会生成一个随机数,然后根据抽样概率选择结果。抽样概率越高的结果,被选中的概率也就越大。最终,被选中的结果就是这次抽样的输出。在多项式采样中, 概率高的结果更有可能被选中,但不同于确定性的选择,每个结果仍然有一定的概率被选中 。这使得模型在生成文本时具有一定的随机性,但又受到概率的控制,以便生成更加多样且符合概率分布的文本。
  • +
  • num_beams参数是用于束搜索(beam search)算法的,其用途是控制生成的多个候选句子的数量,该参数控制的是每个生成步要保留的生成结果的数量,用于在生成过程中增加多样性或生成多个可能的结果。
  • +
  • length_penalty 在束搜索的生成中,候选序列的得分通过对数似然估计计算得到,即得分是负对数似然。
  • +
+

联合采样:先top-k,k大一点,然后top-p,最后用temperature进行归一化

+

ChatGLM

+

GLM

+

预训练

+

+

GLM 将 NLU 任务制定为包含任务描述的完形填空问题,这些问题可以通过自回归生成来回答。

+

多任务预训练

+

在前面的部分中,GLM掩蔽短跨度,并适用于NLU任务。然而,我们有兴趣预训练一个单一模型,可以处理NLU和文本生成。我们研究了一个多任务预训练设置,其中第二个目标是与空白填充目标联合优化的长文本生成任务。我们考虑以下两个目标:

+
    +
  • 文档级别。我们随机抽样一个跨度,其长度从原始长度的50%到100%的均匀分布中抽样。该目标旨在进行长文本生成。
  • +
  • 句子级别。我们限制掩蔽跨度必须是完整的句子。我们随机抽样多个跨度(句子)以覆盖15%的原始令牌。此目标旨在进行序列到序列任务,其预测通常为完整的句子或段落。
  • +
+

模型架构

+
    +
  • Layer Normalization的顺序和残差连接被重新排列
  • +
  • 用于输出标记预测的单个线性层
  • +
  • 用GeLU替换Relu
  • +
+

2D位置编码

+

自回归空白填充任务的挑战之一是如何对位置信息进行编码。Transformers依靠位置编码来注入令牌的绝对位置和相对位置。GLM提出了2D位置编码来应对这一挑战。具体来说,每个令牌都使用两个位置id进行编码。第一个位置id表示mask文本Xcorrupt中的位置。对于掩码跨度,它是相应[MASK]标记的位置。第二个位置id表示跨度内的位置。对于A部分中的标记,它们的第二个位置id为0。对于B部分中的标记,它们的范围从1到span的长度。通过可学习embedding将两个位置id投影到两个向量中,这两个向量都被添加到输入token embedding中。GLM的编码方法确保模型在重建它们时,不知道掩蔽跨度的长度。

+

微调GLM

+

对于分类任务:将NLU分类任务重新表述为空白填充的生成任务

+

对于生成任务:给定的上下文构成输入的A部分,并在末尾附加一个掩码标记。该模型自回归地生成B部分的文本

+

GLM和其他预训练模型之间的差异

+

与BERT比较:BERT无法捕获掩码令牌的相互依赖性。BERT的另一个缺点是它不能正确地填充多个令牌的空白。为了推断长度为l的答案的概率,BERT需要执行l个连续预测。如果长度l未知,可能需要枚举所有可能的长度,因为BERT需要根据长度更改[MASK]令牌的数量。

+

与XLNet比较:GLM和XLNet都是用自回归目标预训练的,但它们之间有两个不同之处。首先,XLNet使用mask前的原始位置编码。在推理过程中,需要知道或枚举答案的长度,这与BERT的问题相同。其次,为了避免Transformer内部的信息泄漏,XLNet使用two-stream自注意机制,而不是right-shift。这使得预训练的时间成本翻倍。

+

与T5比较:T5提出了一个类似的空白填充目标来预训练编码器-解码器Transformer。T5对编码器和解码器使用独立的位置编码,并依赖于多个 sentinel tokens来区分掩码跨度。在下游任务中,只使用一个 sentinel tokens,导致模型容量的浪费和预训练和微调之间的不一致。此外,T5总是按照从左到右的固定顺序预测跨度。因此,在参数和数据更少的情况下,GLM在NLU和seq2seq任务上的性能明显优于T5。

+

与UniLM比较:UniLM通过改变双向、单向和交叉注意之间的注意掩码,将不同的预训练目标组合在自动编码框架下。然而,UniLM总是用[MASK]令牌替换掩码跨度,这限制了它建模掩码跨度与其上下文之间依赖关系的能力。GLM输入前一个令牌并自回归生成下一个令牌。对下游生成任务的UniLM进行微调也依赖于掩码语言建模,效率较低。UniLMv2 (Bao等人,2020)对生成任务采用部分自回归建模,同时对NLU任务采用自编码目标。相反,GLM将NLU和生成任务与自回归预训练相结合。

+

ChatGLM

+

ChatGLM 参考了 ChatGPT 的设计思路,在千亿基座模型 GLM-130B 中注入了代码预训练,通过有监督微调(Supervised Fine-Tuning)等技术实现人类意图对齐。ChatGLM 当前版本模型的能力提升主要来源于独特的千亿基座模型 GLM-130B。

+
    +
  • ChatGLM采用的是编码器-解码器架构,ChatGPT采用的是仅解码器架构
  • +
  • ChatGLM是基于 Base 模型进行有监督微调(SFT)训练而来。而ChatGPT是基于人工反馈的强化学习(RLHF)训练而来
  • +
  • ChatGLM 模型参数量仅62亿,而ChatGPT无论是GPT3.5还是GPT4都是上千亿级规模的参数量
  • +
+

具体来说,ChatGLM-6B 有如下特点:

+
    +
  • 充分的中英双语预训练 : ChatGLM-6B 在 1:1 比例的中英语料上训练了 1T 的 token 量,兼具双语能力。
  • +
  • 优化的模型架构和大小 : 吸取 GLM-130B 训练经验,修正了二维 RoPE 位置编码实现,使用传统FFN结构。6B(62亿)的参数大小,也使得研究者和个人开发者自己微调和部署 ChatGLM-6B 成为可能。
  • +
  • 较低的部署门槛 : FP16 半精度下,ChatGLM-6B 需要至少 13GB 的显存进行推理,结合模型量化技术,这一需求可以进一步降低到 10GB(INT8) 和 6GB(INT4), 使得 ChatGLM-6B 可以部署在消费级显卡上。
  • +
  • 更长的序列长度 : 相比 GLM-10B(序列长度1024),ChatGLM-6B 序列长度达 2048,支持更长对话和应用。
  • +
  • 人类意图对齐训练 : 使用了监督微调(Supervised Fine-Tuning)、反馈自助(Feedback Bootstrap)、人类反馈强化学习(Reinforcement Learning from Human Feedback) 等方式,使模型初具理解人类指令意图的能力。输出格式为 markdown,方便展示。
  • +
+

因此,ChatGLM-6B 具备了一定条件下较好的对话与问答能力。当然,ChatGLM-6B 也有相当多已知的局限和不足:

+
    +
  • 模型容量较小 : 6B 的小容量,决定了其相对较弱的模型记忆和语言能力。在面对许多事实性知识任务时,ChatGLM-6B 可能会生成不正确的信息;她也不擅长逻辑类问题(如数学、编程)的解答。
  • +
  • 可能会产生有害说明或有偏见的内容 :ChatGLM-6B 只是一个初步与人类意图对齐的语言模型,可能会生成有害、有偏见的内容。
  • +
  • 较弱的多轮对话能力 :ChatGLM-6B 的上下文理解能力还不够充分,在面对长答案生成,以及多轮对话的场景时,可能会出现上下文丢失和理解错误的情况。
  • +
  • 英文能力不足 :训练时使用的指示大部分都是中文的,只有一小部分指示是英文的。因此在使用英文指示时,回复的质量可能不如中文指示的回复,甚至与中文指示下的回复矛盾。
  • +
  • 易被误导 :ChatGLM-6B 的“自我认知”可能存在问题,很容易被误导并产生错误的言论。例如当前版本模型在被误导的情况下,会在自我认知上发生偏差。即使该模型经过了1万亿标识符(token)左右的双语预训练,并且进行了指令微调和人类反馈强化学习(RLHF),但是因为模型容量较小,所以在某些指示下可能会产生有误导性的内容。
  • +
+

微调方法

+

Freeze方法

+

Freeze方法,即参数冻结,对原始模型部分参数进行冻结操作,仅训练部分参数。

+

大模型的Prompt构造方式严重影响下游任务的效果。比如:GPT-3采用人工构造的模版来做上下文学习(in context learning),但人工设计的模版的变化特别敏感,加一个词或者少一个词,或者变动位置都会造成比较大的变化。

+

Prefix-Tuning

+

Prefix Tuning,在输入token之前构造一段任务相关的virtual tokens作为Prefix,然后训练的时候只更新Prefix部分的参数,而PLM中的其他部分参数固定。

+

img

+

针对不同的模型结构,需要构造不同的Prefix。

+
    +
  • 针对自回归架构模型:在句子前面添加前缀,得到 z = [PREFIX; x; y],合适的上文能够在固定 LM 的情况下去引导生成下文(比如:GPT3的上下文学习)。
  • +
  • 针对编码器-解码器架构模型:Encoder和Decoder都增加了前缀,得到 z = [PREFIX; x; PREFIX0; y]。Encoder端增加前缀是为了引导输入部分的编码,Decoder 端增加前缀是为了引导后续token的生成。
  • +
+

为了防止直接更新Prefix的参数导致训练不稳定和性能下降的情况,在Prefix层前面加了MLP结构,训练完成后,只保留Prefix的参数。除此之外,通过消融实验证实,只调整embedding层的表现力不够,将导致性能显著下降,因此,在每层都加了prompt的参数。

+

Prompt-Tuning

+

Prompt Tuning方法可以看作是Prefix Tuning的简化版本,它给每个任务定义了自己的Prompt,然后拼接到数据上作为输入,但 只在输入层加入prompt tokens ,并且不需要加入 MLP 进行调整来解决难训练的问题。

+

与输出相关的tokens组成的上下文信息即可理解为是一个prompt。Prompt通常是一种短文本字符串,用于指导语言模型生成响应。Prompt提供上下文和任务相关信息,以帮助模型更好地理解要求,并生成正确的输出。例如,在问答任务中,prompt可能包含问题或话题的描述,以帮助模型生成正确的答案。Prompt通常是人类设计的,以帮助模型更好地理解特定任务或领域。

+

简单总结就是说Prompt就是利用语言模型的生成能力帮我们完成任务。而Prompt-tuning的目的就是设计更加精巧的prompt,然后让模型输出我们想要的内容。

+

以句子的情感分类为例,基于prompt方式让模型做情感分类任务的做法通常是在句子前面加入前缀“该句子的情感是”即可。本质上BERT这样的模型是一种生成模型,是无法完成特定任务的。它只是一个提取文本特征的通用模型。当你在句子前加入“该句子的情感是”这样的前缀,你实际上是将情感分类任务转换为一个“填空”任务。这是因为,在训练过程中,BERT可以学习到这个前缀与句子情感之间的关联。例如,它可以学习到“该句子的情感是积极的”和“该句子的情感是消极的”之间的差异。

+

P-Tuning

+

主要针对NLU任务

+

P-tuning v1 微调方法是将 Prompt 加入到微调过程中, 只对 Prompt 部分的参数进行训练,而语言模型的参数固定不变

+

img

+

P-Tuning方法将Prompt转换为可以学习的Embedding层,并用MLP+LSTM的方式来对Prompt Embedding进行一层处理。相比Prefix Tuning,P-Tuning加入的可微的virtual token,但仅限于输入层,没有在每一层都加;另外,virtual token的位置也不一定是前缀,插入的位置是可选的。这里的出发点实际是把传统人工设计模版中的真实token替换成可微的virtual token。

+

V2

+

之前的Prompt Tuning和P-Tuning等方法存在两个主要的问题:

+

第一,缺乏模型参数规模和任务通用性。

+
    +
  • 缺乏规模通用性:Prompt Tuning论文中表明当模型规模超过100亿个参数时,提示优化可以与全量微调相媲美。但是对于那些较小的模型(从100M到1B),提示优化和全量微调的表现有很大差异,这大大限制了提示优化的适用性。
  • +
  • 缺乏任务普遍性:尽管Prompt Tuning和P-tuning在一些 NLU 基准测试中表现出优势,但提示调优对硬序列标记任务(即序列标注)的有效性尚未得到验证。
  • +
+

第二,缺少深度提示优化,在Prompt Tuning和P-tuning中,连续提示只被插入transformer第一层的输入embedding序列中,在接下来的transformer层中,插入连续提示的位置的embedding是由之前的transformer层计算出来的,这可能导致两个可能的优化挑战。

+
    +
  • 由于序列长度的限制,可调参数的数量是有限的。
  • +
  • 输入embedding对模型预测只有相对间接的影响。
  • +
+

P-tuning v2 微调方法是 P-tuning v1 微调方法的改进版,同时借鉴了 prefix-tuning 微调的方法。如下图所示:

+

+

与 P-tuning v1 微调方法相比,P-tuning v2 微调方法采用了 prefix-tuning 的做法,在输入前面的每一层都加入可微调的参数。在 prefix 部分,每一层的 transformer 的 embedding 输入都需要被微调,而 P-tuning v1 只在第一层进行微调。同时,对于 prefix 部分,每一层 transformer 的输入不是从上一层输出,而是随机初始化的 embedding 作为输入。

+

P-Tuning v2方法在每一层都加入了Prompts tokens作为输入,而不是仅仅加在输入层,这带来两个方面的好处:

+
    +
  • 更多可学习的参数(从P-tuning和Prompt Tuning的0.01%增加到0.1%-3%),同时也足够参数高效。
  • +
  • 加入到更深层结构中的Prompt能给模型预测带来更直接的影响。
  • +
+

和prefix tuning的区别在于P-Tuning V2每一层的prompt是独立的,并不是由上一层计算得来

+

具体做法基本同Prefix Tuning,可以看作是将文本生成的Prefix Tuning技术适配到NLU任务中,然后做了一些改进:

+
    +
  • 移除重参数化的编码器 。以前的方法利用重参数化功能来提高训练速度和鲁棒性(如:Prefix Tuning中的MLP、P-Tuning中的LSTM))。在 P-tuning v2 中,作者发现重参数化的改进很小,尤其是对于较小的模型,同时还会影响模型的表现。
  • +
  • 针对不同任务采用不同的提示长度 。提示长度在提示优化方法的超参数搜索中起着核心作用。在实验中,我们发现不同的理解任务通常用不同的提示长度来实现其最佳性能,这与Prefix-Tuning中的发现一致,不同的文本生成任务可能有不同的最佳提示长度。
  • +
  • 引入多任务学习 。先在多任务的Prompt上进行预训练,然后再适配下游任务。多任务学习对我们的方法来说是可选的,但可能是相当有帮助的。一方面,连续提示的随机惯性给优化带来了困难,这可以通过更多的训练数据或与任务相关的无监督预训练来缓解;另一方面,连续提示是跨任务和数据集的特定任务知识的完美载体。我们的实验表明,在一些困难的序列任务中,多任务学习可以作为P-tuning v2的有益补充。
  • +
  • 回归传统的分类标签范式,而不是映射器 。标签词映射器(Label Word Verbalizer)一直是提示优化的核心组成部分,它将one-hot类标签变成有意义的词,以利用预训练语言模型头。尽管它在few-shot设置中具有潜在的必要性,但在全数据监督设置中,Verbalizer并不是必须的。它阻碍了提示调优在我们需要无实际意义的标签和句子嵌入的场景中的应用。因此,P-Tuning v2回归传统的CLS标签分类范式,采用随机初始化的分类头(Classification Head)应用于tokens之上,以增强通用性,可以适配到序列标注任务。
  • +
+

Lora方法

+

LoRA(Low-Rank Adaptation of Large Language Models),直译为 大语言模型的低阶自适应 。LoRA 的基本原理 是冻结预训练好的模型权重参数,在冻结原模型参数的情况下,通过往模型中加入额外的网络层,并只训练这些新增的网络层参数 。由于这些新增参数数量较少,这样不仅 finetune 的成本显著下降,还能获得和全模型参数参与微调类似的效果。

+

Lora方法的核心是在大型语言模型上对指定参数增加额外的低秩矩阵,也就是在原始PLM旁边增加一个旁路,做一个降维再升维的操作。并在模型训练过程中,固定PLM的参数,只训练降维矩阵A与升维矩阵B。

+

原始论文加到Q和V上效果最好。

+

先降维再升维,两个低秩矩阵A和B,一个高斯初始化,一个零初始化,目的是最开始的时候不会给模型带来噪声

+

为秩,是调节学习率用的,

+

当我们第一次做实验时,我们会尽量把调得大些,例如32、64,并假设在这个秩下,低秩权重已经好了,因此这时我们设置,意味着我们假定LoRA低秩微调的效果和全参数微调持平。

+

那么接下来,我们肯定就要往小的进行尝试了。这时我们把固定住,意味着随着的减小,会越来越大,我们这样做的原因是:

+
    +
  • 越小时,低秩矩阵表示的信息精炼,但不全面。我们通过调大,来放大forward过程中新知识对模型的影响。
  • +
  • 越小时,低秩矩阵表示的信息精炼,噪声/冗余信息少,此时梯度下降的方向也更加确信,所以我们可以通过调大,适当增加梯度下降的步伐,也就相当于调整learning rate了
  • +
+

AdaLora

+

在使用LoRA微调时,对模型的不同模块使用相同的秩,显然是不合理的

+

LoRA微调过程中一直保持秩不变也是不合理的

+

AdaLoRA的总体改进目标:找到一种办法,让模型在微调过程中,去学习每个模块参数对训练结果(以loss衡量)的重要性。然后,根据重要性,动态地调整不同模块的秩。

+

LoRA是学习两个矩阵A和B,用来近似SVD分解的结果,而AdaLoRA是让模型去学习三个权重矩阵,直接近似真实的SVD分解结果

+

img

+
    +
  1. 首先,我们初始化三个矩阵。其中,矩阵比较特殊,其大部分元素为0,只有对角线上的个元素有值。所以实操中我们可将其视为长度为的向量,初始化时,我们将初始化为0,初始化为高斯随机矩阵 。这样做的目的和LoRA一样,都是为了避免引入噪声。
  2. +
  3. 正常做forward和backward,得到Loss和参数的梯度
  4. +
  5. 根据Loss和参数梯度 ,我们可以对图中所示的每个三元组计算重要性分数
  6. +
  7. 根据计算出来的重要性分数,我们将不重要的三元组挑选出来
  8. +
  9. 对于不重要的三元组,我们将其值置0 。这样,在下一次做forward时,这个三元组里对应的向量和向量相当于被mask掉了,对Loss没有贡献。也就起到了变秩的效果。
  10. +
  11. 使用(2)中计算出来的梯度,更新的参数。
  12. +
  13. 使用更新完毕的,开启新一轮forward和backward,重复上面步骤,随时动态更新参数的秩。
  14. +
+

LoRA中是让模型学习BA,去近似SVD分解的结果,但是在训练过程中,没有引入任何SVD分解相关的性质做约束,所以模型就可能学歪了(因此LoRA作者在文章中写了很多实验,证明学出来的BA在一定程度上能近似SVD分解,能取得较好的效果)。而AdaLoRA则是直接将这一束缚考虑到了Loss中。

+

主要优势

+
    +
  1. 预训练模型参数可以被共享,用于为不同的任务构建许多小的 LoRA 模块。冻结共享模型,并通过替换矩阵 A 和 B 可以有效地切换任务,从而显著降低存储需求和多个任务切换的成本。
  2. +
  3. 当使用自适应优化器时,由于不需要计算梯度以及保存太多模型参数,LoRA 使得微调效果更好,并将微调的硬件门槛降低了 3 倍。
  4. +
  5. 低秩分解采用线性设计的方式使得在部署时能够将可训练的参数矩阵与冻结的参数矩阵合并,与完全微调的方法相比,不引入推理延迟。
  6. +
  7. LoRA 与其它多种微调方法不冲突,可以与其它微调方法相结合。
  8. +
+

Adapter Tuning

+

Adapter Tuning 设计了Adapter结构 ,并将其嵌入Transformer的结构里面, 针对每一个Transformer层,增加了两个Adapter结构(分别是多头注意力的投影之后和第二个feed-forward层之后) 在训练时,固定住原来预训练模型的参数不变,只对新增的 Adapter 结构和 Layer Norm 层进行微调,从而保证了训练的高效性

+

每当出现新的下游任务,通过添加Adapter模块来产生一个易于扩展的下游模型,从而避免全量微调与灾难性遗忘的问题。

+

+

其他ChatGPT

+

ChatGPT 模型上基本上和之前 GPT-3 都没有太大变化,主要变化的是训练策略变了,用上了强化学习

+

监督调优模型

+

收集演示数据,用监督学习去训练生成规则(把一些问题写出答案,把问题和答案都丢给GPT去训练,这个是有监督的训练,已经有答案了,让AI一葫芦画瓢,这种方法可以引导AI往人类所期望的方向去做答)

+

但是,我们不可能人工穷举出所有可能的问题和答案,这个显然是不现实的,所以OpenAI只是提供了可能几万个这种有答案的数据,主要是为了让它在这个基础上进行泛化,然后提供一个方向上的引导,就是告诉模型,你就往这个方向上去答。

+

训练回报模型

+

让简化版的GPT监督训练之后变得更强,通过人工标注所有输出的优劣势

+

先让ChatGPT输出很多个答案,然后基于它所生成的答案给他排序,我们只需要人工标注哪个答案是最好的,所以OpenAI做了大量的这种标注,

+

使用 PPO 模型微调 SFT 模型

+

通过PPO强化学习算法,实现模型的自我优化,强化学习就是让AI在不断的试错过程中自我调整优化策略,然后最大化预期的长期奖励,简单来说,就是让AI自己去不断尝试,前两步学习的模型在强化学习这一步都能派上用场。

+

首先用监督版学习的ChatGPT来初始化PPO模型,让Reward模型去指导它,去给回答一个评分,然后AI就基于这个评分去调整自己的参数,试图在下一个回答中得到更高的分数,不断的重复这个过程,这个幼儿版的ChatGPT就成熟起来了,能够自我更新了。

+

T5和Bart

+

T5(Text-to-Text Transfer Transformer)和Bart(Bidirectional and Auto-Regressive Transformer)是两个常见的预训练模型,它们之间的区别如下:

+

T5是一种基于Transformer的通用文本生成模型。T5的训练目标是将不同的自然语言处理(NLP)任务统一为文本到文本的转换任务。它采用了编码器-解码器结构,通过输入一个自然语言文本,输出另一个相关的自然语言文本,可以应用于机器翻译、摘要生成、问题回答等多个NLP任务。

+

Span Masking

+

给定一系列的tokens , 我们迭代式的从中选择出tokens的子集 , 直到我们达到masking budget(例如,15%)。在每一次迭代中,我们首先从一个几何分布中采样出一个span length 。然后从中均匀地采样出一个starting point,作为该masked span的第一个token。

+

与BERT保持一致的是,本文作者在SpanBERT中也同样会将15%的tokens给mask了,其中80%的用[MASK]来进行代替,10%的用random tokens,另外10%的就使用它本身的tokens。但是,对于同一个masked span来说,它们所做的替换操作是一样的(即,要么全用[MASK]来代替,要么全用sampled tokens来进行代替)。

+
    +
  • Transformer Encoder-Decoder 模型;
  • +
  • BERT-style 式的破坏方法;
  • +
  • Replace Span 的破坏策略;
  • +
  • 15 %的破坏比;
  • +
  • 3 的破坏时小段长度。
  • +
+

Bart是建立在T5模型基础上的一个变种,它专注于生成式任务。Bart模型使用了自回归解码器,通过训练一个自编码器来重构原始文本,同时采用了标准的语言模型预训练目标,从而使得生成的文本更加流畅和连贯。Bart的主要应用领域包括文本生成、摘要生成、对话系统等。

+

在任务类型上,T5更加通用,适用于多种NLP任务的文本转换,而Bart则更加专注于生成式任务,并且在生成文本的质量和连贯性上有所优化。

+

关于Bart的DAE(Denoising AutoEncoder)任务,它是Bart模型的一种预训练目标。DAE任务要求模型从输入的有噪声的文本中恢复原始的无噪声文本。通过在训练过程中向输入文本中添加噪声,并要求模型重建无噪声的文本,Bart可以学习到更好的文本表示和重构能力,从而提高生成文本的质量和准确性。

+

BART的预训练任务是 将带噪声的输入还原 。如下图所示,输入为ABCDE,在AB中插入一个span长度为0的mask,再将CD替换为mask,最终得到加噪输入的A_B_E。模型的目标是将其还原为ABCDE。

+

BART最终使用Text Infilling + Sentence permutation,其中Text Infilling起到了最主要的作用,其实就是Span级别的mask,只不过这里允许span长度为0,span的长度服从泊松分布,lambda = 3,总共mask30%的字符。Sentence permutation提升不大,之所以使用是作者假设模型规模提升后这个任务会有用。

+
    +
  • Token Masking : 就是BERT的方法----随机将token替换成[MASK]
  • +
  • Token Deletion : 随机删去token
  • +
  • Text Infilling : 随机将一段连续的token(称作span)替换成一个[MASK],span的长度服从3 的泊松分布。注意span长度为0就相当于插入一个[MASK]。
  • +
  • Sentence Permutation : 将一个document的句子打乱
  • +
  • Document Rotation : 从document序列中随机选择一个token,然后使得该token作为document的开头
  • +
+

PALM

+

模型使用的是标准的Transformer结构中的Decoder

+

PaLM做了以下修改:

+
    +
  • 使用了SwiGLU激活函数
  • +
  • 将Decoder的公式并行化
  • +
  • K、V是共享的,并且维度变成[1,h]。Q任然是[k, h]。这样做可以减少自回归解码的时间,同时对模型性能不会有太大的影响。这是因为标准multi-headed attention,在解码的过程中对硬件加速的效率利用率比较低,因为K,V不共享,每次输出只能输出一个token。
  • +
  • RoPE旋转位置编码对于长文本具有更好的性能
  • +
  • 共享输入&输出Embedding矩阵
  • +
  • 在mlp、normlayer等算法中,都不使用bias。对于大模型,可以提高训练稳定性。
  • +
  • 使用SentencePiece进行tokenization
  • +
+ + +
+ +
+
+ + + + + + +
+
+
LLM面试题准备
+
https://zhangzhao219.github.io/2023/07/29/Interview/Interview-Questions-LLM/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年7月29日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/08/08/Interview/Interview-Internship-Experience-Algorithm/index.html b/2023/08/08/Interview/Interview-Internship-Experience-Algorithm/index.html new file mode 100644 index 000000000..0066dad12 --- /dev/null +++ b/2023/08/08/Interview/Interview-Internship-Experience-Algorithm/index.html @@ -0,0 +1,1243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 算法实习面试经历 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

算法实习面试经历

+ + +
+ +

算法实习面试经历

+

字节跳动-NLP自然语言处理实习生-电商业务

+

渠道:师兄组内内推+官网投递

+

https://jobs.bytedance.com/campus/position/7225875094580119864/detail

+

2023-07-13 17:31 官网投递

+

2023-07-14 11:33 约一面

+

2023-07-19 11:00-12:00 1/2

+

第一次正式面算法岗,经历、问题等回答的都不算太好

+

Transformer的mask回答的不好,如何使用、mask的形状等等。

+

算法题1:一个已经排好序的数组,给定一个目标值,求是否存在连续的子数组使得数组内部元素的和与目标值相等。滑动窗口实现,直接过

+

算法题2:两个硬币有8种序列,两个人提前一人选择一种序列,然后抛这个硬币N次(N>3),从头开始匹配,先匹配到谁选择的序列就算谁赢,在已知两个人选择的序列的情况下求两个人赢的概率。

+

模拟的方式做了一半超时,正规的解法应该是用马尔可夫的方法,没弄明白。

+
def get_all_possible_list(possibilities, temparr, n):
+    if len(temparr) == n:
+        possibilities.append(temparr.copy())
+        return
+    for coin in [0, 1]:
+        temparr.append(coin)
+        get_all_possible_list(possibilities, temparr, n)
+        temparr.pop()
+
+
+def judge(arr1, arr2):
+    if len(arr1) != len(arr2):
+        return False
+    for i in range(0, len(arr1)):
+        if arr1[i] != arr2[i]:
+            return False
+    return True
+
+
+n = 5  # 第三方扔硬币的次数
+a = [0, 0, 1]  # 第一个人选择的结果
+b = [0, 1, 0]  # 第二个人选择的结果
+possibilities = []
+temparr = []
+get_all_possible_list(possibilities, temparr, n)
+# print(possibilities)
+all = len(possibilities)
+k = len(a)
+a_win = 0
+b_win = 0
+for arr in possibilities:
+    for index in range(0, n - k + 1):
+        if judge(arr[index : index + k], a):
+            a_win += 1
+            break
+        if judge(arr[index : index + k], b):
+            b_win += 1
+            break
+print(a_win / all)  # 0.34375
+print(b_win / all)  # 0.21875
+

2023-07-19 14:25 约二面

+

2023-07-20 14:00-15:00 2/2

+

项目回答的还可以,出了一些数学题等

+

有一个均匀分布的随机数发生器,如何产生正态分布的数字

+

算法题:平面上有若干点,求最多可以连成多少条线

+

应该通过斜率和截距唯一表示这个直线,同时要考虑斜率为无穷大的情况,也就是除数为0。

+

提示下才写出来,也没有考虑除数为0的条件。

+

2023-07-24 16:19 感谢信

+

排序挂,对方24届苏州大学NLP,五段经历,其中包括三篇论文

+

字节跳动-NLP算法实习生-房产

+

渠道:微信公众号内推 https://mp.weixin.qq.com/s/y8zwSY0OYKZ5HfqqUoiLsw

+

https://jobs.bytedance.com/campus/position/7259286117455235388/detail

+

2023-07-25 18:15 邮件投递

+

2023-07-26 16:52 约一面

+

2023-07-28 17:00-17:40 1/3

+

看了之前的面评,基础知识没有什么好问的,让手推Transformer的Attention的细节,也问了一些mask相关的内容

+

算法题:https://leetcode.cn/problems/two-sum/

+

秒杀

+

2023-07-31 14:53 约二面

+

2023-07-31 17:00-18:30 2/3

+

简历上面的全部项目都讲了一遍,大概一个小时

+

算法题:手写IOU,思路错误基本没有写

+

概率题:圆上任取三点,能构成钝角三角形的概率

+

2023-08-04 19:13 约三面

+

2023-08-07 19:00-19:40 3/3

+

讲解印象最深的一篇论文-InstructGPT

+

讲解GPT的发展过程1-2-3-InstructGPT-4

+

没有算法题及概率题

+

2023-08-07 19:49 确认不推进

+

阿里-夸克搜索-大模型算法工程师实习生

+

渠道:boss直聘(对方主动联系)

+

https://www.zhipin.com/job_detail/679790c8cb4e151e1X1y3dS4FFtX.html

+

2023-07-26 09:30 投递+约面

+

2023-07-26 20:00-21:00 1/2

+

电话面试

+

项目问题

+

基础知识

+

对大模型的了解程度

+

算法题:排列数和组合数,快速排序和组合排序与树的关系

+

2023-08-23 09:51 约二面

+

约当天电话面试,拒绝后一面面试官联系我挽留

+

百度-内容解析与理解及资源与用商垂类搜索组-机器学习实习研发工程师

+

渠道:boss直聘

+

https://www.zhipin.com/job_detail/faa8bd495d022e241Xx80967F1dU.html

+

2023-08-01 17:30 投递

+

2023-08-03 15:52 约一面

+

2023-08-04 15:00-15:45 1/2

+

没有问项目

+

对大模型怎么看

+

抽取网页的标题和正文怎么抽

+

算法题:https://leetcode.cn/problems/longest-substring-without-repeating-characters/

+

基本秒杀,但是还可以优化一个if,没有再继续做

+

2023-08-04 16:43 约二面

+

2023-08-07 17:00-17:25 2/2

+

讲解Transformer的结构

+

没有算法题

+

2023-08-08 12:30 oc

+

最多只能拖两天

+

2023-08-10 19:00 offer

+

2023-08-16 15:40 鸽Offer

+

微信拒绝:

+

您好!我是下周一准备入职的张兆。

+

很抱歉,之前可能刚刚回所低估了实验室的任务我今天接到导师的通知,从下周(甚至这周末)开始要有一个新项目需要我来主做,可能无法离开计算所保证实习时间去实习了。

+

我不得不临时再拒绝您提供的百度的实习offer,给您造成的不便请您谅解,顺祝您工作顺利,也希望我们将来还有机会继续合作共事。

+

回复:

+

这样啊,那很遗憾

+

理解,这边就先结束流程了哈

+

微软-STCA-Feeds组算法

+

渠道:boss直聘

+

https://www.zhipin.com/job_detail/473e81a25baa5aaa1HR609u_FVtQ.html

+

2023-08-02 15:40 投递

+

2023-08-04 10:33 约一面

+

2023-08-08 16:00-17:00 1/2

+

不开摄像头

+

文本分类的机器学习算法有哪些,各自的优缺点和适用数据

+

机器学习过拟合欠拟合的解决方法

+

正则化为什么会有用

+

算法题:

+
    +
  1. https://leetcode.cn/problems/symmetric-tree/ 基本没问题,要求用非递归做,做不出来
  2. +
  3. https://leetcode.cn/problems/house-robber-ii/ 基本没问题
  4. +
+

对比赛不关心,只关心实习

+

2023-08-09 16:10 约二面

+

2023-08-10 14:00 2/2

+

Bert和Transformer的结构

+

矩阵转换 transpose和view的用法

+

梯度下降法求平方根,调通一半吧

+

合并 K 个升序链表

+

2023-08-16 10:46 oc

+

最多考虑到2023-08-17中午

+

2023-08-16 15:23 offer

+

腾讯-AILab

+

2023-08-14 17:19 投递

+

智能生成算法平台组-应用研究实习生

+

2023-08-15 15:30 约一面

+

2023-08-18 20:00 1/

+

GPT、RLHF、PPO的细节

+

GPT4的置信度的图片

+

Transformer的Self Attention的机制

+

Decoder的mask的原理,mask和后面置0有什么区别

+

做题:求三次方根

+

深圳的部门,去不了直接挂

+

2023-08-21 09:20 约一面

+

约2023-08-22 10:00

+

不打电话,直接微信通知,应该是AI平台部

+

拒绝

+

腾讯-微信搜索

+

2024-03-18 19:30 捞约一面

+

2024-03-26 15:00 1/n

+

微软实习一段一段说,不太问细节,主要可能没说太清楚,她可能没理解好

+

百度搜索比赛任务细节,不算搜索的,没讲完

+

基础知识:交叉熵损失函数,transformer结构,beam search,temperature,topk,topp,前序中序后序遍历二叉树的原理,rope编码简单原理

+

算法题:归并排序,白板写不需要运行

+

2024-03-27 10:31 约二面

+

2024-03-28 14:30 2/n

+

微软实习第一个特征算法

+

NAACL论文,主要是大模型方面

+

没有算法题

+

2024-04-01 16:34 约面委会

+

2024-04-08 19:00-20:00 3/n

+

NAACL论文问了很多,质疑创新点

+

微软实习质疑底层特征不能更改

+

不开摄像头,共享屏幕讲论文

+

微信底层开发平台人员面试

+

美团-到家事业群-搜索推荐算法

+

2024-03-26 18:41 投递

+

2024-03-29 14:56 通知笔试

+

2024-03-30 19:00-21:00 笔试

+

5道编程题,100%,100%,60%,13.33%,0%

+

2024-04-01 10:00 通知选择一面时间

+

2024-04-08 11:00-12:00 1/

+

NAACL论文,数据集,结合多轮对话场景如何判断立场转变

+

微软实习没有问太多

+

反转链表,力扣模式

+

2024-04-08 16:51 通知选择二面时间

+

2024-04-15 17:00-17:45 2/

+

实验室干了啥,有啥帮助

+

手撕二叉树的层序遍历

+

为什么从微软离职,微软实习

+

大模型没问比赛,问了做这些比赛有什么收获,有什么成长

+

倾向于广告还是倾向于大模型

+

2024-04-19 10:00 发现回到人才库

+

发邮件及公众号询问

+

2024-04-19 11:37 OC

+

入职时间暂定6月3日,一周下正式的offer

+

2024-04-22 16:27 正式Offer

+

48小时之内选择接受还是拒绝

+

2024-04-24 16:19 拒绝Offer

+

阿里-淘天集团-算法技术-商品推荐算法

+

2024-03-28 19:04 投递

+

2024-03-29 16:20 通知笔试及测评

+

2024-03-29 19:00-19:40 自由测评

+

分析题11题、数学统计题11题、图形题11题、性格测试51题

+

2024-04-03 19:00-20:40 笔试

+

9道单选,6道不定项选择题,三道编程只做出来一个

+

2024-04-07 10:00 约一面

+

2024-04-09 17:00-18:00 1/2

+

微软实习,扩展SSB问题

+

WSDM Cup的比赛

+

CodeQwen比赛

+

softmax公式,数值过小怎么办

+

梯度和导数的区别

+

力扣原题:322 零钱兑换,初始条件有点问题

+

说非常满意,会有二面的

+

2024-04-11 14:10 约二面

+

先拒了一个时间,然后约了第二个时间

+

2024-04-16 11:30-12:00 2/2

+

最近公共父节点及变形(如果结点不一定存在应该怎么办),询问时间复杂度

+

有一个小bug,但是做的也很快

+

问WSDM比赛,没问完,出了一个对用户评论进行总结的场景题目

+

2024-04-23 15:33 约HR面

+

2024-04-24 17:00-17:30 HR面

+

被发现看自我介绍了

+

实习公司的氛围,更喜欢哪个

+

自己的优缺点

+

有什么兴趣爱好

+

对部门的了解

+

反问转正率

+

直接更新到“等待面试结果”,应该是正向的

+

蚂蚁-蚂蚁星-算法工程师-自然语言处理

+

2024-04-02 09:29 发校园大使简历

+

一直没有消息,应该是简历挂了

+

2024-04-10 10:11 自行投递

+

2024-04-10 13:49 测评及笔试通知

+

2024-04-10 13:56-14:30 自由测评

+

2024-04-13 10:00-11:40 笔试

+

单选、不定项选择题

+

2024-04-17 20:24 约一面

+

2024-04-18 20:00-21:30 1/2

+

WSDM比赛

+

NAACL论文

+

CodeQwen比赛

+

ChatGLM比赛

+

全部组合 本地写算法题

+

说直接终面,二面免了

+

2024-04-19 11:30 约二面

+

2024-04-19 15:30-16:30 2/2

+

WSDM比赛

+

NAACL论文简要介绍

+

对大模型的理解

+

以后做什么方向

+

2024-04-23 13:44 约HR面

+

2024-04-23 17:00-17:30 HR面

+

自我介绍

+

怎么安排时间,动力在哪

+

与岗位最匹配的点

+

压力最大的时候

+

2024-04-24 15:50 自行询问后说已经在Offer审批流程中

+

2024-04-24 16:25 意向书到邮箱

+

2024-04-24 19:30 确认意向

+

字节跳动-抖音-NLP自然语言处理实习生

+

2024-04-12 14:24 通过北大论坛内推投递

+

中间各种催

+

2024-04-18 22:00 简历评估通过

+

没招了,就是不打电话

+

2024-04-23 20:17 约一面

+

算法实习生-风控

+

职位描述
+ByteIntern:面向2025届毕业生(2024年9月-2025年8月期间毕业),为符合岗位要求的同学提供转正机会。
+团队介绍:风控研发作为业务的风控技术中台主要帮助解决各个产品(包括抖音、头条等)的各种黑灰产作弊问题,包括内容、交易、流量、账号等方面的风险,结合各场景业务(抖音短视频、电商、直播、生活服务、用户增长等)特性,进行模型和算法创新,打造业界领先的风控算法体系。

+

1、负责字节各产品(包括抖音、头条等)各种作弊风险的发现与治理,包括内容、交易、流量、账号等方面的风险;
+2、通过指标监控归因,及时感知风险及业务的变化,主动识别潜在攻击,持续优化调整风控方案,并推动风控治理建设及业务模式调整;
+3、通过CV、NLP、表征学习、图模型、深度学习、迁移学习、多任务学习等技术,提升问题发现的效率,从而快速阻断风险,优化平台的各项生态指标;
+4、挖掘和分析海量内容和用户行为进行用户长短期画像建设,提升模型的精准性和召回面;
+5、结合各场景业务(抖音短视频、电商、直播、生活服务、用户增长等)特性,进行模型和算法创新,打造业界领先的风控算法体系。
+职位要求
+1、2025届本科及以上学历在读,计算机、机器学习和模式识别等相关专业优先;
+2、热爱计算机科学和互联网技术,对人工智能类产品有浓厚兴趣;
+3、具备强悍的编码能力,熟悉 Linux 开发环境,熟悉 C++ 和 Python 语言优先;
+4、有扎实的数据结构和算法功底,熟悉机器学习、自然语言处理、数据挖掘、分布式计算、计算机视觉、计算机图形中一项或多项;
+5、优秀的分析问题和解决问题的能力,对解决具有挑战性问题充满激情。
+6、每周可实习4天以上,实习时间3个月以上。

+

都不太建议去,先面面看

+

2024-04-25 11:00-12:00 1/

+

WSDM比赛

+

GPT为什么有效

+

大模型长度外推

+

大模型的词表扩充

+

大模型的微调方式

+

Transformer的结构

+

对于小样本数据如何作处理

+

代码:最长公共子序列

+

2024-05-07 17:52 感谢信

+

腾讯-CSIG-优图实验室

+

2024-04-18 09:41 捞约一面

+

2024-04-18 13:58 约一面

+

2024-04-18 15:00-16:00 1/4

+

WSDM比赛,对着PPT详细介绍

+

为什么大模型的参数量都是六七B这样,主要原因是模型的层数

+

NAACL论文的贡献

+

算法题

+

IP 地址排序
+给定一组 IP 地址字符串组成的数组, 将其从小到大排序
+如 [“10.3.72.160”, “10.3.71.106”, “10.3.71.102”, “10.3.69.108”] 排序后得到 [‘10.3.69.108’, ‘10.3.71.102’, ‘10.3.71.106’, ‘10.3.72.160’]

+

样例1:
+[输入]
+[“10.3.72.160”, “10.3.71.106”, “10.3.71.102”, “10.3.69.108”]
+[输出]
+[‘10.3.69.108’, ‘10.3.71.102’, ‘10.3.71.106’, ‘10.3.72.160’]

+
#include <bits/stdc++.h>
+using namespace std;
+
+vector<int> splitstring(string s){
+    vector<int> result;
+    int temp = 0;
+    for(int i=0;i<s.size();i++){
+        if(s[i] == '.'){
+            result.push_back(temp);
+            temp = 0;
+        } else{
+            temp = temp * 10 + (s[i] - '0');
+        }
+    }
+    result.push_back(temp);
+    return result;
+}
+
+bool cmp(string a, string b){
+    vector<int> a_vt = splitstring(a);
+    // for(int i=0;i<a_vt.size();i++){
+    //     cout << a_vt[i] << " ";
+    // }
+    // cout << endl;
+    vector<int> b_vt = splitstring(b);
+    // for(int i=0;i<b_vt.size();i++){
+    //     cout << a_vt[i] << " ";
+    // }
+    // cout << endl;
+    for(int i=0;i<4;i++){
+        if(a_vt[i] < b_vt[i]){
+            return true;
+        } else if (a_vt[i] > b_vt[i]){
+            return false;
+        }
+    }
+    return true;
+}
+
+int main() {
+    vector<string> vs;
+    // string s;
+    // while(cin >> s){
+    //     vs.push_back(s);
+    // }
+    vs.push_back("10.3.72.160");
+    vs.push_back("10.3.71.106");
+    vs.push_back("10.3.71.102");
+    vs.push_back("10.3.69.108");
+    sort(vs.begin(), vs.end(), cmp);
+    for(int i=0;i<vs.size();i++){
+        cout << vs[i] << endl;
+    }
+    return 0;
+}
+
+

数组中重复的数据

+

给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次。找到所有出现两次的元素。

+

你可以不用到任何额外空间并在O(n)时间复杂度内解决这个问题吗?

+

样例1:
+[输入]
+[4,3,2,7,8,2,3,1]
+[输出]
+[2,3]

+
#include <iostream>
+using namespace std;
+int main() {
+    //int a;
+    //cin >> a;
+    vector<int> vt;
+    vt = [4,3,2,7,8,2,3,1]
+    for(int i=0;i<vt.size();i++){
+        if(vt[vt[i] - 1] < 0){
+            print(vt[i]);
+        } else{
+            vt[vt[i] - 1] = - vt[vt[i] - 1]
+        }
+    }
+}
+

2024-04-18 16:00-16:40 2/4

+

CodeQwen比赛

+

算法题:

+

给定两个字符串s1和s2,需要删除一些字符使得两个字符串相等。每删除一个字符,需要花费对应字符的ascii值代价。求使得两个字符相等,需要删除的字符花费最小和。

+

例如: s1=‘sea’, s2=‘eat’
+在s1中删除s(115),在s2中删除t(116),两个字符串都等于 ea,代价和为 115 + 116 = 231

+
#include <bits/stdc++.h>
+using namespace std;
+int main() {
+    string s1 = "sea";
+    string s2 = "eat";
+    int m = s1.size();
+    int n = s2.size();
+    vector<vector<int> > dp(m+1,){vector<int>(n+1,0));
+    int tempsum = 0;
+    for(int i=0;i<=m;i++
+        dp[i][0] = tempsum;
+        if(i == m){
+            break;
+        }
+        tempsum += s1[i];
+    }
+    tempsum = 0;
+    for(int j=0;j<=n;j++){
+        dp[0][j] = tempsum;
+        if(j == n){
+            break;
+        }
+        tempsum += s2[j];
+    }
+    for(int i=1;i<=m;i++){
+        for(int j=1;j<=n;j++){
+            if(s1[i-1] == s2[j-1]){
+                dp[i][j] = dp[i-1][j-1];
+            } else{
+                dp[i][j] = min(dp[i-1][j] + s1[i-1], dp[i][j-1] + s2[j-1]);
+            }
+        }
+    }
+    for(int i=0;i<=m;i++){
+        for(int j=0;j<=n;j++){
+            cout << dp[i][j] << " ";
+        }
+        cout << endl;
+    }
+    cout << dp[m][n] << endl;
+    return 0;
+
+}
+

2024-04-18 19:42 直接更新下一面信息

+

2024-04-20 16:00-16:30 3/4

+

CodeQwen代码大模型

+

ChatGLM金融大模型

+

NAACL论文

+

对实习有什么预期

+

实习期间需要弄一个论文,以论文和项目的进展判断最终转正的薪资

+

2024-04-22 14:20 直接更新下一面信息

+

2024-04-22 19:30-20:00 4/4

+

WSDM Cup

+

NAACL 论文

+

2024-04-23 16:30 反馈

+

反馈后面面试不太好,研究深度不够

+

2024-04-24 19:21 挂

+

Shopee-算法-内容/推荐算法方向

+

2024-04-18 17:00 BOSS 捞简历

+

2024-04-19 16:20 自由选择面试时间

+

2024-04-23 15:00-16:00 1/2

+

WSDM Cup 有没有更改过模型,模型的集成策略

+

微软实习,各种业务问题,线上效果,优化策略

+

小样本分类竞赛的难点

+

机器学习树模型

+

算法题:判断链表是否有环,计算环的长度

+

2024-04-25 12:09 自由选择面试时间

+

2024-04-30 16:00-17:00 2/2

+

自选项目做介绍

+

先主动讲了WSDM Cup

+

然后让我讲小样本分类比赛

+

怎么通过计算机估算Π

+

两个业务,一个是视频生成文本然后提取特征,一个是纯搜广推,主要是混排策略

+

说会有HR面的

+

2024-04-30 19:01 自由选择面试时间

+

2024-05-08 15:00-15:30 HR面

+

在实习中对团队最有帮助的事情

+

未来规划,为什么不读博

+

不一定转正,和秋招是一起的,试用期可能会缩短

+

2024-05-21 11:28 OC

+

实习工资9000,周末双休,五道口附近

+

网易雷火投递

+

被推荐人是中科院计算所的硕士研究生,发表了NAACL 2024的一作文章,同时有NeurIPS 2024的一作文章在投;曾在商汤和微软实习,现在在蚂蚁集团作为蚂蚁星的候选人进行暑期实习;参加过五项算法竞赛,包括顶会竞赛WSDM Cup的冠军。

+

文本大模型算法工程师

+

(简答题)请简述你在文本大模型技术的研究或项目经历。

+

研究经历:

+
    +
  1. NeurIPS 2024 在投论文(第一作者):受教育学中响应式教学的启发,我们创新性地考虑学生模型的反馈 ,提出了ARTE框架,让大型LLM(教师模型)根据小型LLM(学生模型)的偏好来生成为知识蒸馏量身定制的训练数据 。我们在领域内和领域外的推理基准数据集中进行了大量实验,证明了我们的框架生成的带有定制化的训练示例的微调学生模型大大优于现有的指令调优数据集,且教师模型与学生模型都有很强大的泛化能力。
  2. +
  3. NAACL 2024 已发表论文:在立场检测细分任务上,创新性地利用文本大模型的语言理解能力,对言论与目标的关系进行显式深度分析,并与生成式小模型和原型聚类对比学习相结合,在Zero-Shot与Cross-Target两种类型的立场检测任务上达到了SOTA效果。
  4. +
+

实习经历:

+
    +
  1. 在蚂蚁集团以Qwen系列大模型为基座进行医疗行业垂直领域大模型建设,包括医疗领域数据增量预训练、监督微调、强化学习等大模型全链路方法。目前性能已经超出其他开源医疗模型。
  2. +
  3. 在微软以GPT Ada2 Embedding为原始输入优化文本分类、推荐等深度学习模型,性能超过原始模型。
  4. +
+

项目经历(竞赛经历):

+
    +
  1. WSDM Cup 2024:以SOLAR 10.7B大模型为基座进行LoRA微调 ,结合Prompt设计、多轮对话训练方式、混合训练、不相关参考文档的联合过滤、生成式任务的模型集成等策略,在对话式多文档问答任务中大幅度领先于其他参赛队伍得到冠军,并在信息检索顶级会议WSDM 2024上汇报方案。
  2. +
  3. 通义千问AI挑战赛CodeQwen能力算法赛道:使用公开的代码数据集,在初赛时对Qwen1.8B模型进行参数高效微调,复赛尝试了提升Qwen-72B的代码能力,最终成绩在562支参赛队伍中排名第3。
  4. +
  5. 2023百度搜索创新大赛搜索答案组织赛道:使用Baichuan2-7B大模型进行LoRA微调,配合数据增强、风格适应、NEFTune、探索最佳推理参数、集成学习等策略,完成对多文档搜索摘要进行组织的任务,最终成绩在220支参赛队伍中排名第4。
  6. +
  7. SMP 2023 ChatGLM金融大模型挑战赛:以ChatGLM2-6B大模型为中心,合理利用大模型的Zero-Shot能力与参数高效微调训练方法,制作了一个金融年报问答系统,最终在2294支参赛队伍中排名第6。
  8. +
+

(简答题)请罗列你玩过的游戏,并简述你对AIGC技术在游戏场景应用的思考。

+

我平时一般不玩游戏,不过过年的时候可能会玩一点和平精英,其他游戏就有点不太了解了。

+

下面是我对AIGC技术如果可能应用在和平精英中的一些思考:

+
    +
  1. 游戏地图相关:现在虽然有一些节日等的限时新地图,但是总体来说地图还是比较固定,可能不太能吸引老玩家。如果能利用AIGC技术,根据用户的兴趣生成地图、调整地图元素、优化地图环境细节等,应该可以增加游戏地图的吸引力。同时现在也有一些功能开放给用户可以自行设计地图场景等,如果能利用AIGC技术辅助用户,可以大大降低用户的上手难度,从而获得更高的用户活跃量。
  2. +
  3. 角色相关:现在的人物的服饰等装扮是游戏中的收费能力,如果能引入AIGC技术,根据用户的偏好自动生成独特的角色外观,可以增强用户的个性化体验,从而获得更高的收入。
  4. +
  5. 战斗相关:现在对于老用户来说,游戏中的一些NPC的特点还是比较明显的,比较影响游戏体验。如果能引入AIGC技术,使NPC具备更高的智能水平,能够根据用户的行为做出实时反应,可以增加游戏的难度和策略性,从而能更加吸引老用户,提升用户留存率。
  6. +
  7. 玩家相关:可以通过AIGC技术分析玩家的行为数据,优化游戏设计和运营策略,提升玩家留存率和满意度,同时利用AIGC技术实时监控和分析游戏数据,检测和防止作弊行为,维护游戏的公平和公正。
  8. +
+

人工智能算法工程师(NLP方向)

+

(简答题)最近看了什么paper?为什么要看这些paper?看完后有什么体会?

+

(简答题)最近跟进了哪些开源项目?为什么跟进这些项目?有进一步的优化建议吗?

+

(简答题)最近做了哪些算法应用实践?如果可以自由选择,想做什么样的应用实践?

+ + +
+ +
+
+ + + + + + +
+
+
算法实习面试经历
+
https://zhangzhao219.github.io/2023/08/08/Interview/Interview-Internship-Experience-Algorithm/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年8月8日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/08/12/Pytorch-distributed/index.html b/2023/08/12/Pytorch-distributed/index.html new file mode 100644 index 000000000..7f2ee6d34 --- /dev/null +++ b/2023/08/12/Pytorch-distributed/index.html @@ -0,0 +1,1429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pytorch分布式训练 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Pytorch分布式训练

+ + +
+ +

Pytorch分布式训练学习整理

+ +

参考资料

+

源码解析:PyTorch 源码解读之 DP & DDP:模型并行和分布式训练解析

+

简单小模型示例:pytorch中分布式训练DDP教程(新手快速入门!)

+

Pytorch - 弹性训练极简实现(附源码)

+

系列文章:【分布式训练】单机多卡的正确打开方式(一):理论基础

+

【分布式训练】基于PyTorch进行多GPU分布式模型训练(补充)

+

较新较详细的教程:torch分布式训练

+

博客:pytorch弹性分布式训练

+

模型并行(流水线)

+

把模型隔成不同的层,每一层都放到一块GPU上

+

(1)GPU利用度不够。

+

+

如图,阴影部分所表示的时间段里,总有GPU在空转。GPU的数量越多时,空置的比例接近1

+

(2)中间结果占据大量内存

+

在做backward计算梯度的过程中,我们需要用到每一层的中间结果z。假设我们的模型有L层,每一层的宽度为d,则对于每块GPU,不考虑其参数本身的存储,额外的空间复杂度为 。从这个复杂度可以看出,随着模型的增大,N,L,d三者的增加可能会平滑掉K增加带来的GPU内存收益。因此,这也是需要优化的地方。

+

Gpipe

+

流水线并行的核心思想是: 在模型并行的基础上,进一步引入数据并行的办法,即把原先的数据再划分成若干个batch,送入GPU进行训练 。未划分前的数据,叫 mini-batch 。在mini-batch上再划分的数据,叫 micro-batch

+

+

其中,第一个下标表示GPU编号,第二个下标表示micro-batch编号。假设我们将mini-batch划分为M个,则流水线并行下,bubble的时间复杂度为: 。Gpipe通过实验证明,当时,bubble产生的空转时间占比对最终训练时长影响是微小的,可以忽略不计。

+

将batch切好,并逐一送入GPU的过程,就像一个流水生产线一样(类似于CPU里的流水线),因此也被称为Pipeline Parallelism。

+

Gpipe采用用时间换空间的方法,几乎不存中间结果,等到backward的时候,再重新算一遍forward

+

每块GPU上只保存来自上一块的最后一层输入z,其余的中间结果我们算完就废。等到backward的时候再由保存下来的z重新进行forward来算出。

+

空间复杂度为

+

在实际应用中,流水线并行并不特别流行,主要原因是模型能否均匀切割,影响了整体计算效率,这就需要算法工程师做手调。

+

数据并行

+

数据并行的核心思想是: 在各个GPU上都拷贝一份完整模型,各自吃一份数据,算一份梯度,最后对梯度进行累加来更新整体模型 。理念不复杂,但到了大模型场景, 巨大的存储和GPU间的通讯量, 就是系统设计要考虑的重点了。在本文中,我们将递进介绍三种主流数据并行的实现方式:

+
    +
  • DP(Data Parallelism) :最早的数据并行模式,一般采用参数服务器(Parameters Server)这一编程框架。实际中多用于单机多卡
  • +
  • DDP(Distributed Data Parallelism) :分布式数据并行,采用Ring AllReduce的通讯方式,实际中多用于多机场景
  • +
  • ZeRO: 零冗余优化器。由微软推出并应用于其DeepSpeed框架中。严格来讲ZeRO采用数据并行+张量并行的方式,旨在降低存储。
  • +
+

DP

+

一个经典数据并行的过程如下:

+
    +
  • 若干块 计算GPU ,如图中GPU0~GPU2;1块 梯度收集GPU ,如图中AllReduce操作所在GPU。
  • +
  • 在每块计算GPU上都拷贝一份完整的模型参数。
  • +
  • 把一份数据X(例如一个batch)均匀分给不同的计算GPU。
  • +
  • 每块计算GPU做一轮FWD和BWD后,算得一份梯度G。
  • +
  • 每块计算GPU将自己的梯度push给梯度收集GPU,做聚合操作。这里的聚合操作一般指 梯度累加 。当然也支持用户自定义。
  • +
  • 梯度收集GPU聚合完毕后,计算GPU从它那pull下完整的梯度结果,用于更新模型参数W。更新完毕后,计算GPU上的模型参数依然保持一致。
  • +
  • 聚合再下发梯度的操作,称为AllReduce
  • +
+

流程

+

../_images/ps.svg

+

DP 基于单机多卡,所有设备都负责计算和训练网络,除此之外, device[0] (并非 GPU 真实标号而是输入参数 device_ids 首位) 还要负责整合梯度,更新参数。从图中我们可以看出,有三个主要过程:

+
    +
  • 过程一(图中红色部分):各卡分别计算损失和梯度
  • +
  • 过程二(图中蓝色部分):所有梯度整合到 device[0]
  • +
  • 过程三(图中绿色部分):device[0] 进行参数更新,其他卡拉取 device[0] 的参数进行更新
  • +
+

所有卡都并行运算(图中红色),将梯度收集到 device[0](图中浅蓝色)和 device[0] 分享模型参数给其他 GPU(图中绿色)三个主要过程。

+

更详细的流程如下图所示:

+

+

分析

+
    +
  • 存储开销大 。每块GPU上都存了一份完整的模型,造成冗余。
  • +
  • 通讯开销大 。Server需要和每一个Worker进行梯度传输。当Server和Worker不在一台机器上时,Server的带宽将会成为整个系统的计算效率瓶颈。
  • +
+

梯度异步更新:Worker并不会实际等到把聚合梯度拿回来,更新完参数W后再做计算。而是直接拿旧的W,吃新的数据,继续第11轮的计算。这样就保证在通讯的时间里,Worker也在马不停蹄做计算,提升计算通讯比。

+

但是模型收敛的速度不会变快,只是多用了一些数据

+

受通讯负载不均的影响, DP一般用于单机多卡场景

+

DDP

+

DDP作为一种更通用的解决方案出现了,既能多机,也能单机。DDP首先要解决的就是通讯问题:将Server上的通讯压力均衡转到各个Worker上。实现这一点后,可以进一步去Server,留Worker。

+

简介

+

随着大模型的出现,简单的数据并行已经无法满足需求,毕竟一个模型的大小就有可能超过显卡的显存,更不可能将其复制多份。因此需要让每一张卡仅负责模型的一部分计算,承载模型的一小部分。

+

使用DDP进行分布式训练有以下几个优势:

+
    +
  1. 加速训练:通过数据并行,DDP能够在多个设备或节点上同时处理不同批次的数据,从而加快训练速度。
  2. +
  3. 内存效率:DDP在每个设备上只保存模型的局部副本和相应的梯度,而不是整个模型的副本,这样可以节省内存。
  4. +
  5. 不需要额外的代码:在PyTorch中,使用DDP进行分布式训练几乎不需要修改您的原始模型和训练代码。
  6. +
+

流程:Ring All Reduce

+

Scatter Reduce过程:首先将参数分为份,相邻的GPU传递不同的参数,在传递次之后,可以得到每一份参数的累积(在不同的GPU上)。

+

动图

+

All Gather:得到每一份参数的累积之后,再做一次传递,同步到所有的GPU上。

+

动图

+

假设有个GPU, 传输总量是,每一次的通信上限是,则完成一次通信需要时间,那么总共需要花费时间,可以看到通信成本与GPU数量无关。

+

DP和DDP的总通讯量相同,但因负载不均的原因,DP需要耗费更多的时间搬运数据,但是DP不一定就比DDP差

+

代码

+

分析

+

DDP采用多进程控制多GPU,共同训练模型,一份代码会被pytorch自动分配到n个进程并在n个GPU上运行。 DDP运用Ring-Reduce通信算法在每个GPU间对梯度进行通讯,交换彼此的梯度,从而获得所有GPU的梯度。对比DP,不需要在进行模型本体的通信,因此可以加速训练。

+

需要注意以下几点:

+
    +
  1. 设置DistributedSampler来打乱数据,因为一个batch被分配到了好几个进程中,要确保不同的GPU拿到的不是同一份数据。
  2. +
  3. 要告诉每个进程自己的id,即使用哪一块GPU。
  4. +
  5. 如果需要做BatchNormalization,需要对数据进行同步。
  6. +
+

Torchrun使用及参数详解

+
核心概念
+
    +
  • rank:进程号,在多进程上下文中,我们通常假定rank 0是第一个进程或者主进程,其它进程分别具有1,2,3不同rank号,这样总共具有4个进程。
  • +
  • node:物理节点,可以是一个容器也可以是一台机器,节点内部可以有多个GPU;nnodes指物理节点数量, nproc_per_node指每个物理节点上面进程的数量
  • +
  • local_rank:指在一个node上进程的相对序号,local_rank在node之间相互独立
  • +
  • WORLD_SIZE:全局进程总个数,即在一个分布式任务中rank的数量
  • +
  • Group:进程组,一个分布式任务对应了一个进程组。只有用户需要创立多个进程组时才会用到group来管理,默认情况下只有一个group
  • +
  • backend:通信后端,可选的包括:nccl(NVIDIA推出)、gloo(Facebook推出)、mpi(OpenMPI)。一般建议GPU训练选择nccl,CPU训练选择gloo
  • +
  • master_addr与master_port:主节点的地址以及端口,供init_method 的tcp方式使用。 因为pytorch中网络通信建立是从机去连接主机,运行ddp只需要指定主节点的IP与端口,其它节点的IP不需要填写。
  • +
+

如下图所示,共有3个节点(机器),每个节点上有4个GPU,每台机器上起4个进程,每个进程占一块GPU,那么图中一共有12个rank,nproc_per_node=4,nnodes=3,每个节点都有一个对应的node_rank

+

在这里插入图片描述

+

rank与GPU之间没有必然的对应关系,一个rank可以包含多个GPU;一个GPU也可以为多个rank服务(多进程共享GPU),在torch的分布式训练中习惯默认一个rank对应着一个GPU,因此local_rank可以当作GPU号

+
简介
+

torchrun相当于原来的torch.distributed.launch,有一些额外增加的功能:

+
    +
  • 通过重启优雅处理某一个worker运行过程中的错误
  • +
  • worker的RANK和WORLD_SIZE都是被自动分配的
  • +
  • Node的数量允许从最小值到最大值中间弹性伸缩
  • +
+

torchrun命令与 python -m torch.distributed.run命令完全等同,为命令行命令

+
从旧版本迁移 --use_env
+

有一个参数 --use_env在目前版本的torchrun中是不存在的,因此需要做一点处理

+
    +
  1. 将原始指定的–local-rank参数修改为从环境变量中读取
  2. +
  3. 命令行不需要再次指定 --use_env参数
  4. +
+

旧版本代码:

+
$ python -m torch.distributed.launch --use-env train_script.py
+import argparse
+parser = argparse.ArgumentParser()
+parser.add_argument("--local-rank", type=int)
+args = parser.parse_args()
+
+local_rank = args.local_rank
+

新版本代码:

+
$ torchrun train_script.py
+import os
+local_rank = int(os.environ["LOCAL_RANK"])
+
命令行参数
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数名称含义示例
–nnodes节点数量,一个节点对应一个主机1或MIN_SIZE:MAX_SIZE(弹性训练)
–nproc-per-node一个节点中的进程数量,一般一个进程使用一个显卡,故也通常表述为一个节点中显卡的数量[auto, cpu, gpu, int]
–rdzv-backendrendezvous 后端c10d etcd
–rdzv-endpointrendezvous 后端地址<host>:<port>
–rdzv-id用户可以指定当前rendezvous的id,所有的node都要使用这同一个id
–rdzv-conf希望传入rendezvous的其他参数<key1>=<value1>
–standalone单节点多卡的默认配置,不需要再传入上述的rendezvous参数,默认为C10d TCP 29400(–master-addr等也会失效)选项
–max-restartsworker group重启的最大次数
–monitor-interval检测worker状态的时间间隔(以秒为单位)
–start-method创建子进程的方式{spawn,fork,forkserver}
–roleUser-defined role for the workers.
-m与python -m相同,将模块当作脚本运行选项
–no-python不使用python命令而是直接执行(如果这个文件并不是一个py文件会使用这个)
–run-path使用runpy.run_path执行文件
–log-dir日志文件存放目录
–redirects将控制台输出的日志信息重定向到日志目录中的文件[-r 3] 将所有worker的标准输出和标准错误进行重定向,[-r 0:1,1:2] 将rank 0的标准输出重定向,将rank 1的标准错误重定向
–tee除将日志输出到控制台外也输出到日志文件日志文件流
–node-rank多节点分布式训练的时候该节点的Rank
–master-addrmaster 节点的 IP 地址,也就是 rank=0 对应的主机地址
–master-portmaster 节点的端口号,用于通信
–local-addr本地节点的IP地址
+

torchrun主要是对多节点作了分布式的优化,从而可以满足容错性和弹性伸缩。如果是单节点就不需要很复杂。

+
环境变量
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称含义示例
LOCAL_RANKGPU在单节点中的序号01
RANKGPU在全部节点的序号01
GROUP_RANKworker组的rank00
ROLE_RANK相同ROLE的worker的rank01
LOCAL_WORLD_SIZE与–nproc-per-node相同22
WORLD_SIZEjob中worker的总数22
ROLE_WORLD_SIZE相同角色的worker的数量12
MASTER_ADDRrank为0的worker的地址127.0.0.1127.0.0.1
MASTER_PORTrank为0的worker的端口2950029500
TORCHELASTIC_RESTART_COUNT最近重启的worker组的数量00
TORCHELASTIC_MAX_RESTARTS配置的最大重启次数00
TORCHELASTIC_RUN_ID与–rdzv-id相同nonenone
PYTHON_EXEC执行这个脚本的python的位置没有没有
+

代码示例

+
import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from torch.utils.data import Dataset, DataLoader
+from torch.utils.data.distributed import DistributedSampler
+from torch.nn.parallel import DistributedDataParallel
+from torch.distributed import init_process_group, destroy_process_group
+import os
+import time
+
+class ToyModel(nn.Module):
+    def __init__(self):
+        super(ToyModel, self).__init__()
+        self.net1 = nn.Linear(10, 10)
+        self.relu = nn.ReLU()
+        self.net2 = nn.Linear(10, 5)
+
+    def forward(self, x):
+        return self.net2(self.relu(self.net1(x)))
+
+class MyTrainDataset(Dataset):
+    def __init__(self, size):
+        self.size = size
+        self.data = [(torch.rand(10), 0) for _ in range(size)]
+
+    def __len__(self):
+        return self.size
+  
+    def __getitem__(self, index):
+        return self.data[index]
+
+class Trainer:
+    def __init__(
+        self,
+        model: torch.nn.Module,
+        train_data: DataLoader,
+        optimizer: torch.optim.Optimizer,
+        save_every: int,
+        snapshot_path: str,
+    ) -> None:
+        self.gpu_id = int(os.environ["LOCAL_RANK"])
+        self.model = model.to(self.gpu_id)
+        self.train_data = train_data
+        self.optimizer = optimizer
+        self.save_every = save_every
+        self.epochs_run = 0
+        self.snapshot_path = snapshot_path
+        if os.path.exists(snapshot_path):
+            print("Loading snapshot")
+            self._load_snapshot(snapshot_path)
+
+        self.model = DistributedDataParallel(self.model, device_ids=[self.gpu_id])
+
+    def _load_snapshot(self, snapshot_path):
+        loc = f"cuda:{self.gpu_id}"
+        snapshot = torch.load(snapshot_path, map_location=loc)
+        self.model.load_state_dict(snapshot["MODEL_STATE"])
+        self.epochs_run = snapshot["EPOCHS_RUN"]
+        print(f"Resuming training from snapshot at Epoch {self.epochs_run}")
+
+    def _run_batch(self, source, targets):
+        self.optimizer.zero_grad()
+        output = self.model(source)
+        # print(output,targets)
+        loss = F.cross_entropy(output, targets)
+        print(f"[GPU{self.gpu_id}] Loss {loss.item()}")
+        loss.backward()
+        self.optimizer.step()
+
+    def _run_epoch(self, epoch):
+        b_sz = len(next(iter(self.train_data))[0])
+        print(f"[GPU{self.gpu_id}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")
+        self.train_data.sampler.set_epoch(epoch)
+        for source, targets in self.train_data:
+            source = source.to(self.gpu_id)
+            targets = targets.to(self.gpu_id)
+            self._run_batch(source, targets)
+
+    def _save_snapshot(self, epoch):
+        snapshot = {
+            "MODEL_STATE": self.model.module.state_dict(),
+            "EPOCHS_RUN": epoch,
+        }
+        torch.save(snapshot, self.snapshot_path)
+        print(f"Epoch {epoch} | Training snapshot saved at {self.snapshot_path}")
+
+    def train(self, max_epochs: int):
+        for epoch in range(self.epochs_run, max_epochs):
+            self._run_epoch(epoch)
+            if self.gpu_id == 0 and epoch % self.save_every == 0:
+                self._save_snapshot(epoch)
+            time.sleep(1)
+
+def ddp_setup():
+    init_process_group(backend="nccl")
+    print("Parameters")
+    print(f"LOCAL_RANK:{os.environ['LOCAL_RANK']}")
+    print(f"RANK:{os.environ['RANK']}")
+    print(f"GROUP_RANK:{os.environ['GROUP_RANK']}")
+    print(f"ROLE_RANK:{os.environ['ROLE_RANK']}")
+    print(f"LOCAL_WORLD_SIZE:{os.environ['LOCAL_WORLD_SIZE']}")
+    print(f"WORLD_SIZE:{os.environ['WORLD_SIZE']}")
+    print(f"ROLE_WORLD_SIZE:{os.environ['ROLE_WORLD_SIZE']}")
+    print(f"MASTER_ADDR:{os.environ['MASTER_ADDR']}")
+    print(f"MASTER_PORT:{os.environ['MASTER_PORT']}")
+    print("")
+    torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))
+
+def load_train_objs():
+    train_set = MyTrainDataset(2048)  # load your dataset
+    model = ToyModel()
+    optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
+    return train_set, model, optimizer
+
+def prepare_dataloader(dataset: Dataset, batch_size: int):
+    return DataLoader(
+        dataset,
+        batch_size=batch_size,
+        pin_memory=True,
+        shuffle=False,
+        sampler=DistributedSampler(dataset)
+    )
+
+def main(save_every: int, total_epochs: int, batch_size: int, snapshot_path: str = "snapshot.pt"):
+    ddp_setup()
+    dataset, model, optimizer = load_train_objs()
+    train_data = prepare_dataloader(dataset, batch_size)
+    trainer = Trainer(model, train_data, optimizer, save_every, snapshot_path)
+    trainer.train(total_epochs)
+    destroy_process_group()
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description='simple distributed training job')
+    parser.add_argument('--total_epochs', default=10, type=int, help='Total epochs to train the model')
+    parser.add_argument('--save_every', default=2, type=int, help='How often to save a snapshot')
+    parser.add_argument('--batch_size', default=512, type=int, help='Input batch size on each device (default: 32)')
+    args = parser.parse_args()
+  
+    main(args.save_every, args.total_epochs, args.batch_size)
+

与单卡有几点不同:

+
    +
  1. 初始化进程组:init_process_group(backend="nccl"),后端一般选择nccl
  2. +
  3. 分布式数据采样器:sampler=DistributedSampler(dataset)
  4. +
  5. 封装模型:self.model = DistributedDataParallel(self.model, device_ids=[self.gpu_id])
  6. +
  7. 启动torchrun脚本进行训练
  8. +
+

训练脚本:

+
    +
  1. 单机多卡
  2. +
+
torchrun \
+    --nnodes=1 \
+    --nproc_per_node=2 \
+	--master-addr=127.0.0.1 \
+	--master-port=29500 \
+	main.py
+
    +
  1. 多机多卡
  2. +
+
export NCCL_DEBUG=info
+export NCCL_SOCKET_IFNAME=bond0
+export NCCL_IB_DISABLE=1
+
+torchrun \
+    --nnodes=2 \
+    --nproc_per_node=2 \
+	--master-addr=10.208.58.27 \
+	--master-port=29602 \
+	--node-rank=0 \
+	main.py
+
export NCCL_DEBUG=info
+export NCCL_SOCKET_IFNAME=bond0
+export NCCL_IB_DISABLE=1
+
+torchrun \
+    --nnodes=2 \
+    --nproc_per_node=1 \
+	--master-addr=10.208.58.27 \
+	--master-port=29602 \
+	--node-rank=1 \
+	main.py
+

注意事项:

+
    +
  1. 多进程训练,也就是会同时运行多份代码,因此训练时候要想好GPU的序号等需要自己指定的变量
  2. +
  3. 数据是按照进程数量分的,比如总共2048条,如果三个进程就每一个进程683
  4. +
+

测试环境:

+

master:10.208.58.27 2*V100

+
Parameters
+LOCAL_RANK:0
+RANK:0
+GROUP_RANK:0
+ROLE_RANK:0
+LOCAL_WORLD_SIZE:2
+WORLD_SIZE:3
+ROLE_WORLD_SIZE:3
+MASTER_ADDR:10.208.58.27
+MASTER_PORT:29602
+
+Parameters
+LOCAL_RANK:1
+RANK:1
+GROUP_RANK:0
+ROLE_RANK:1
+LOCAL_WORLD_SIZE:2
+WORLD_SIZE:3
+ROLE_WORLD_SIZE:3
+MASTER_ADDR:10.208.58.27
+MASTER_PORT:29602
+

worker:1*A100

+
Parameters
+LOCAL_RANK:0
+RANK:2
+GROUP_RANK:1
+ROLE_RANK:2
+LOCAL_WORLD_SIZE:1
+WORLD_SIZE:3
+ROLE_WORLD_SIZE:3
+MASTER_ADDR:10.208.58.27
+MASTER_PORT:29602
+

ZeRO(零冗余优化)

+

数据并行中,每个GPU上都复制了一份完整模型,当模型变大时,很容易打爆GPU的显存

+

存储消耗

+

存储主要分为两大块:Model States和Residual States

+

Model States指和模型本身息息相关的,必须存储的内容,具体包括:

+
    +
  • optimizer states :Adam优化算法中的momentum和variance
  • +
  • gradients :模型梯度
  • +
  • parameters :模型参数W
  • +
+

Residual States指并非模型必须的,但在训练过程中会额外产生的内容,具体包括:

+
    +
  • activation :激活值。在流水线并行中我们曾详细介绍过。在backward过程中使用链式法则计算梯度时会用到。有了它算梯度会更快,但它不是必须存储的,因为可以通过重新做Forward来算它。
  • +
  • temporary buffers: 临时存储。例如把梯度发送到某块GPU上做加总聚合时产生的存储。
  • +
  • unusable fragment memory :碎片化的存储空间。虽然总存储空间是够的,但是如果取不到连续的存储空间,相关的请求也会被fail掉。对这类空间浪费可以通过内存整理来解决。
  • +
+

精度混合训练

+
    +
  • 存储一份fp32的parameter,momentum和variance(统称model states)
  • +
  • 在forward开始之前,额外开辟一块存储空间,将fp32 parameter减半到fp16 parameter。
  • +
  • 正常做forward和backward,在此之间产生的activation和gradients,都用fp16进行存储。
  • +
  • 用fp16 gradients去更新fp32下的model states。
  • +
  • 当模型收敛后,fp32的parameter就是最终的参数输出。
  • +
+

存储大小

+

img

+

其中很大的momentum和variance是Adam保存的,首先就优化他们

+

ZeRO-DP

+

优化状态分割

+

将optimizer state分成若干份,每块GPU上各自维护一份。这样就减少了相当一部分的显存开销。

+

+

得到G是与DP一样的通信,然后还要聚合W

+

显存和通讯量的情况如下:

+

img

+

优化状态与梯度分割

+

把梯度也拆开,每个GPU格子维护一块梯度。

+

img

+

此时,数据并行的整体流程如下:

+

对梯度做一次 Reduce-Scatter ,保证每个GPU上所维持的那块梯度是聚合梯度。例如对GPU1,它负责维护G1,因此其他的GPU只需要把G1对应位置的梯度发给GPU1做加总就可。

+

+

每块GPU用自己对应的O和G去更新相应的W。更新完毕后, 每块GPU维持了一块更新完毕的W 。同理,对W做一次 All-Gather ,将别的GPU算好的W同步到自己这来。单卡通讯量 Φ

+

img

+

优化状态、梯度与参数分割

+

每块GPU只维持对应的optimizer states,gradients和parameters

+
    +
  • 做forward时,对W做一次 All-Gather ,取回分布在别的GPU上的W,得到一份完整的W,立刻把不是自己维护的W抛弃。
  • +
  • 做backward时,对W做一次 All-Gather ,取回完整的W,**backward做完,立刻把不是自己维护的W抛弃。
  • +
  • 做完backward,算得一份完整的梯度G,对G做一次 Reduce-Scatter ,从别的GPU上聚合自己维护的那部分梯度,聚合操作结束后,立刻把不是自己维护的G抛弃
  • +
+

+

到这一步, 我们用1.5倍的通讯开销,换回近120倍的显存 。只要梯度计算和异步更新做的好,通讯时间大部分可以被计算时间隐藏,因此这样的额外通讯开销,也是划算的。

+

ZeRO VS 模型并行

+

ZeRO是模型并行的形式,数据并行的实质

+

模型并行,是指在forward和backward的过程中,我只需要用自己维护的那块W来计算就行。即 同样的输入X,每块GPU上各算模型的一部分,最后通过某些方式聚合结果

+

对ZeRO来说,它做forward和backward的时候,是需要把各GPU上维护的W聚合起来的,即本质上还是用完整的W进行计算。 它是不同的输入X,完整的参数W,最终再做聚合

+

ZeRO-Offload与ZeRO-Infinity

+

核心思想是: 显存不够,内存来凑

+

把要存储的大头卸载(offload)到CPU上,而把计算部分放到GPU上

+

ZeRO-Offload的做法是:

+
    +
  • forward和backward计算量高 ,因此和它们相关的部分,例如参数W(fp16),activation,就全放入GPU。
  • +
  • update的部分计算量低 ,因此和它相关的部分,全部放入CPU中。例如W(fp32),optimizer states(fp32)和gradients(fp16)等。
  • +
+

具体切分如下图:

+

+

ZeRO-infinity也是同理,它们在解决的事情都是:找个除GPU之外的地方,存数据。感兴趣的朋友可以深入研究,这里就不展开了。

+

张量模型并行

+

把模型的参数纵向切开,放到不同的GPU上进行独立计算,然后再做聚合。

+

假设现在W太大,导致单卡装不下。我们需要把W切开放到不同的卡上,则我们面临三个主要问题:

+
    +
  • 怎么切分W。
  • +
  • 切完W后,怎么做forward。
  • +
  • 做完forward后,怎么做backward,进而求出梯度,更新权重。
  • +
+

按行切分权重

+

forward

+

我们用N来表示GPU的数量。有几块GPU,就把W按行维度切成几份。下图展示了N=2时的切割方式:

+

+

W按照行维度切开后,X的维度和它不对齐了,这可怎么做矩阵乘法呢?很简单,再把X“按列切开”就行了,如下图所示:

+

+

backward

+

做完forward,取得预测值Y,进而可计算出损失L,接下来就能做backward了。我们重画一下forward的过程,并在其中加入backward的部分,整体流程图如下:

+

img

+

按列切分权重

+

forward

+

按列切分权重后,forward计算图如下:

+

+

backward

+

img

+

具体模型拆分方式:https://zhuanlan.zhihu.com/p/622212228

+

在实际应用中,对Transformer类的模型,采用最经典方法是张量模型并行 + 数据并行,并在数据并行中引入ZeRO做显存优化。具体的架构如下:

+

+

其中,node表示一台机器, 一般我们在同一台机器的GPU间做张量模型并行。在不同的机器上做数据并行 。图中颜色相同的部分,为一个数据并行组。凭直觉,我们可以知道这么设计大概率和两种并行方式的通讯量有关。具体来说, 它与TP和DP模式下每一层的通讯量有关,也与TP和DP的backward计算方式有关

+ + +
+ +
+
+ + + + + + +
+
+
Pytorch分布式训练
+
https://zhangzhao219.github.io/2023/08/12/Pytorch-distributed/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年8月12日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/08/15/zhangzhao-ict-work/index.html b/2023/08/15/zhangzhao-ict-work/index.html new file mode 100644 index 000000000..e6d62f0cd --- /dev/null +++ b/2023/08/15/zhangzhao-ict-work/index.html @@ -0,0 +1,1028 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ICT周报月报 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

ICT周报月报

+ + +
+ +

ICT周报月报

+ +

2023年07月15日-2023年08月15日

+

月报:

+
    +
  1. 调研了解大模型,阅读经典论文,包括但不限于GPT 1 2 3 4、GLM(ChatGLM)、 LLaMA 1 2等
  2. +
  3. 调研了解大模型的微调方法,包括Prompt Tuning、P-Tuning V 1 2、LoRA等
  4. +
  5. 在"TinyStories"数据集上实际运行LLaMA的预训练任务
  6. +
  7. 调研立场检测任务的定义、分类、系统架构、研究方法、应用领域等
  8. +
+

附上了 立场检测调研.docx 第一章内容

+

2023年08月16日-2023年09月15日

+

月报:

+
    +
  1. 根据修改意见修改张亚强师兄的论文《融合文档结构信息的篇章级事件表示学习方法》,并编写修改文档,与王苑铮师兄讨论并制作会议汇报PPT,修改论文后投稿《模式识别与人工智能》期刊。
  2. +
  3. 与邱鹏师兄沟通立场检测项目
  4. +
  5. 调研立场检测任务的新进展,阅读论文: +
      +
    1. MeLT: Message-Level Transformer with Masked Document Representations as Pre-Training for Stance Detection
    2. +
    3. Knowledge-enhanced Prompt-tuning for Stance Detection
    4. +
    5. Ladder-of-Thought: Using Knowledge as Steps to Elevate Stance Detection
    6. +
    +
  6. +
  7. 使用大模型的api,与huggingface模型进行比较,做实验复现论文Stance Detection With Supervised, Zero-Shot, and Few-Shot Applications结果
  8. +
  9. 跟进大模型的新进展
  10. +
+

2023年09月11日-2023年09月15日

+

周报:

+
    +
  1. +

    与邱鹏师兄沟通立场检测项目,还没有得到师兄的反馈

    +
  2. +
  3. +

    继续调研立场检测任务的新进展

    +
      +
    1. MeLT: Message-Level Transformer with Masked Document Representations as Pre-Training for Stance Detection +
        +
      1. 在Twitter数据集上做预训练,将word级别的mask更改为message级别的mask,对某个人的一些message进行随机mask(Bert的方式),让模型预测该位置的message。其中message的表示是word的表示取平均得来的。
      2. +
      3. 后续进行分类任务的微调
      4. +
      +
    2. +
    3. Knowledge-enhanced Prompt-tuning for Stance Detection +
        +
      1. 自动空间映射器:用SenticNet扩充词汇,自动选择相关的词语进行答案的映射
      2. +
      3. 背景知识 +
          +
        1. 将target送入ConceptGraph中获得target的背景知识
        2. +
        3. 使用neural topic model学习利用#符号的语义信息
        4. +
        +
      4. +
      5. 将上述的知识一起作为Prompt送入到预训练模型中进行微调,得到类别
      6. +
      +
    4. +
    5. Ladder-of-Thought: Using Knowledge as Steps to Elevate Stance Detection +
        +
      1. CoT利用的是大模型内部的知识,但是立场检测相关的知识大模型可能没有见过
      2. +
      3. 方法 +
          +
        1. 首先在Google上面搜到target的相关信息
        2. +
        3. 用Text,Target和上面搜集到的信息微调一个预训练模型,让其产生更好的外部信息 Generation Finetuning
        4. +
        5. 在上面的模型基础上,将text,target,和上面产生的外部信息连接在一起输入到模型中,以预测label为目标进行微调
        6. +
        +
      4. +
      +
    6. +
    +
  4. +
  5. +

    立场检测简单实验

    +
      +
    1. 数据集:关于特朗普竞选总统的数据集,分为三种立场,support, against和none
    2. +
    3. zero-shot方法:选取了huggingface的一个模型:MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli,不训练,直接对数据集进行三分类,最后的指标为 f1_score: 0.7555012224938875,precision: 0.6912751677852349,recall: 0.8328840970350404
    4. +
    5. prompt方法:输入到chatglm2中让其进行三分类,最后的指标为f1_score: 0.26476578411405294,precision: 0.5416666666666666,recall: 0.1752021563342318
    6. +
    +
  6. +
+

总结:

+
    +
  1. Prompt可以多尝试一下,有的模型少加一些提示词的效果会更好
  2. +
  3. 实验室不太方便提供够用的GPU
  4. +
+

2023年09月16日-2023年10月15日

+

月报:

+
    +
  1. 时序预测项目 +
      +
    1. 开会了解项目背景
    2. +
    3. 了解数据与模型
    4. +
    5. 实现在期货数据上的预测(根据前75%的数据预测后25%的数据)
    6. +
    +
  2. +
  3. 立场检测相关论文阅读: +
      +
    1. Few-Shot Stance Detection via Target-Aware Prompt Distillation
    2. +
    3. Don’t Stop Pretraining: Adapt Language Models to Domains and Tasks
    4. +
    5. To Pretrain or Not to Pretrain: Examining the Benefits of Pretraining on Resource Rich Tasks
    6. +
    7. JointCL: A Joint Contrastive Learning Framework for Zero-Shot Stance Detection
    8. +
    9. Exploiting Sentiment and Common Sense for Zero-shot Stance Detection
    10. +
    11. StanceReCL: Zero-Shot Stance Detection Based on Relevance and Contrastive Learning
    12. +
    13. Use of Large Language Models for Stance Classification
    14. +
    +
  4. +
  5. 立场检测项目 +
      +
    1. 从邱鹏师兄那里获得代码与论文,并实际运行代码跑通
    2. +
    3. 用大模型进行Prompt的立场检测实验,效果不好,且已经有论文研究过效果确实不好
    4. +
    5. 调研目前学术界的数据集并了解字段含义
    6. +
    +
  6. +
+

2023年09月18日-2023年09月22日

+

周报:

+
    +
  1. 项目:周末再问问
  2. +
  3. 论文阅读 +
      +
    1. Few-Shot Stance Detection via Target-Aware Prompt Distillation +
        +
      1. 动机:target通常是随时间变化的,对每一个target都获取充足的数据进行训练是很不现实的,立场检测方法需要获得few-shot的能力
      2. +
      3. 多目标训练:训练一个模型,可以准确预测不同的target的label
      4. +
      5. 方法: +
          +
        1. 设计三个Prompt,输入到Bert等模型的预训练任务中,让其预测label
        2. +
        3. 预测的时候不映射到具体的label的词语,而与target的表示向量进行结合,与label的表示向量计算损失
        4. +
        5. teacher-student model融合三个prompt的结果,迭代进行预测,对比真实标签与预测标签之间的差距。
        6. +
        +
      6. +
      +
    2. +
    3. Don’t Stop Pretraining: Adapt Language Models to Domains and Tasks +
        +
      1. 在专门的领域进行预训练可以获得直接使用预训练权重后进行微调的效果更好
      2. +
      3. DAPT:领域适用的预训练:用大规模的无标签数据继续进行预训练,再使用有标签的数据进行微调,效果优于直接使用有标签的数据进行微调的结果
      4. +
      5. TAPT:任务适用的预训练:用任务的数据进行预训练(无论是有标签的数据还是没有标签的数据),在更少的数据量上可以达到与DAPT相当的效果
      6. +
      7. 先进行DAPT再进行TAPT,效果更好
      8. +
      +
    4. +
    5. To Pretrain or Not to Pretrain: Examining the Benefits of Pretraining on Resource Rich Tasks +
        +
      1. 下游任务中训练样本的数量对预训练模型的影响有多大?
      2. +
      3. 对RoBERTa、LSTM 以及使用预训练模型的词向量层的 LSTM三种模型进行了系统的实验,情感分类任务
      4. +
      5. 对于文本分类任务来说,用百万级的数据训练后,微调 RoBERTa 的结果与 LSTM 的差距不足 1%
      6. +
      7. 小模型的推理时间有显著的优势
      8. +
      +
    6. +
    7. JointCL: A Joint Contrastive Learning Framework for Zero-Shot Stance Detection
    8. +
    9. Exploiting Sentiment and Common Sense for Zero-shot Stance Detection +
        +
      1. 一个没有出现过的target的信息是可以通过其他已知的target表示出来的,使用图自动编码的模块将target的普遍信息融合进立场检测的模型
      2. +
      3. (图理解不是很深,有待继续研究)
      4. +
      +
    10. +
    +
  4. +
  5. 立场检测继续实验 +
      +
    1. 上次的zero-shot的模型介绍: +
        +
      1. 使用NLI任务,基于Deberta,在多种数据集上面进行微调过。NLI为自然语言推理。它主要用来判断两个句子在语义上的关系,一般可以分为:Entailment(蕴含)、Contradiction(矛盾)、Neutral(中立)。
      2. +
      +
    2. +
    3. prompt方法: +
        +
      1. 原来的Prompt,输入到vicuna进行预测,f1_score: 0.4172461752433936,precision: 0.43103448275862066,recall: 0.40431266846361186
      2. +
      3. 减少prompt的字符,只提供一个示例,vicuna:f1_score: 0.47346938775510206,precision: 0.47802197802197804,recall: 0.46900269541778977
      4. +
      5. 减少prompt的字符,不提供示例,vicuna:f1_score: 0.4986263736263736,precision: 0.3345622119815668,recall: 0.9784366576819407
      6. +
      +
    4. +
    +
  6. +
+

2023年10月07日-2023年10月13日

+
    +
  • 时序预测项目 +
      +
    • 开会了解项目背景,与徐老师讨论后面计划
    • +
    • 数据:赌盘、期货和民调数据
    • +
    • 模型:两个文档中提到的模型
    • +
    • 进展 +
        +
      • 时间序列模型ARIMA(自回归差分移动平均模型) +
          +
        • AR部分(自回归模型)用于处理时间序列的自回归部分,它考虑了过去若干时期的观测值对当前值的影响
        • +
        • I部分(差分过程)用于使非平稳时间序列达到平稳,通过一阶或者二阶等差分处理,消除了时间序列中的趋势和季节性因素
        • +
        • MA部分(移动平均模型)用于处理时间序列的移动平均部分,它考虑了过去的预测误差对当前值的影响
        • +
        +
      • +
      • 实现在期货数据上的预测(根据前75%的数据预测后25%的数据)
      • +
      +
    • +
    +
  • +
  • 立场检测论文: +
      +
    • StanceReCL: Zero-Shot Stance Detection Based on Relevance and Contrastive Learning(邱鹏师兄的论文) +
        +
      • pPvYprT.md.png
      • +
      • 提出了几个概念:stance indicator(support,against和neutral),stance pattern(由stance indicator和target组成)
      • +
      • 两种表达:句子层面的表达([CLS]对应的最后一层的表示)与词语层面的表达(单个token的最后一层的隐藏状态)
      • +
      • 相关性的计算: +
          +
        • 上面的CLS与下面的CLS计算句子层面的相关性
        • +
        • 上面的stance indicator与下面的sentence中的最相关的词语计算相关性
        • +
        +
      • +
      • 句子层面计算对比学习的损失,然后与词语层面的损失加权重融合计算
      • +
      • 在数据集SEM-16、VAST、WT-WT上进行了实验,取得了SOTA效果
      • +
      +
    • +
    • Use of Large Language Models for Stance Classification(ICWSM 2024 (CCF B)) +
        +
      • 尝试了四种Prompt,用上下文包裹text和target,加一些few shot的例子,最后让其解释原因
      • +
      • 尝试了几种开源的大模型
      • +
      • 总结:大模型不太行,不如微调过的小模型,甚至不如zero-shot
      • +
      • 数据集:covid-lies、election2016、phemerumors、semeval2016、wtwt
      • +
      +
    • +
    +
  • +
  • 立场检测实验:在VAST数据集上跑通代码,并了解数据集的字段含义
  • +
+

2023年10月16日-2023年11月15日

+

2023年10月16日-2023年10月19日

+
    +
  • 时序预测项目 + +
  • +
  • 立场检测论文 +
      +
    • Zero-Shot and Few-Shot Stance Detection on Varied Topics via Conditional Generation +
        +
      • 第一个用生成的方式做立场检测的任务
      • +
      • Bart预训练模型
      • +
      • 将立场检测任务建模成生成任务,以生成对应的标签为目标
      • +
      • 多任务联合学习(预测target)
      • +
      • Unlikelihood training
      • +
      • 增加Wiki的知识
      • +
      +
    • +
    • Infusing Knowledge from Wikipedia to Enhance Stance Detection +
        +
      • 上面的增加wiki的知识的原始论文
      • +
      +
    • +
    +
  • +
  • 立场检测实验 +
      +
    • 生成任务+wiki知识,超过SOTA
    • +
    • 用大模型获取text与target的关联的知识 +
        +
      • 问题:大模型的利用不多,仅仅评估了Prompt能力,没有用大模型做知识增强的工作
      • +
      • 直接利用text与target的显式关系
      • +
      • 多个数据集进行评测,zero-shot,跨领域的新SOTA
      • +
      +
    • +
    +
  • +
+

2023年10月20日-2023年10月26日

+
    +
  • 时序预测项目 +
      +
    • 2020民调简单模型:平均误差6.01%
    • +
    • 2020民调简单模型+机构效应纠偏:平均误差6.01% -> 3.89%
    • +
    • 2020沉默螺旋效应不如直接归一化
    • +
    • 期货+民调简单融合模型:平均误差3.54%(期货平均误差6.3%+民调平均误差6.01%)
    • +
    • 整理2024年已有数据
    • +
    • 与徐老师讨论后续计划
    • +
    +
  • +
  • 立场检测 +
      +
    • 实验两个技巧:原型聚类对比学习与Text smoothing
    • +
    • 继续看论文
    • +
    +
  • +
+

2023年10月27日-2023年11月2日

+
    +
  • 时序预测项目 +
      +
    • 针对2020的预测结果,对2024的民调机构的机构效应进行分析总结,附件“2024机构效应分析.xlsx”
    • +
    • 2020民调模型+机构效应纠偏:平均误差7.313% -> 5.467%
    • +
    • 期货+民调融合模型:平均误差3.54% -> 3.11%
    • +
    • 整理本周数据,见附件的两个csv文件
    • +
    • 通过期货数据进行预测,误差在4%~6%之间波动,与前期在2020年数据上面的实验结果基本相同
    • +
    • 与徐老师讨论后续计划
    • +
    +
  • +
  • 立场检测 +
      +
    • 继续实验两个技巧:原型聚类对比学习与Text smoothing
    • +
    • 总结其他论文的写作
    • +
    +
  • +
+ + +
+ +
+
+ + + + + + +
+
+
ICT周报月报
+
https://zhangzhao219.github.io/2023/08/15/zhangzhao-ict-work/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年8月15日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/10/06/diary/diary20231006/index.html b/2023/10/06/diary/diary20231006/index.html new file mode 100644 index 000000000..b702e4284 --- /dev/null +++ b/2023/10/06/diary/diary20231006/index.html @@ -0,0 +1,734 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20231006 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20231006

+ + +
+ +

2023年10月6日,周五

+ +

从7月中旬到9月底,我经历了到目前为止最忙碌的一段时间。现在十一假期已经过去,回头想想,感觉实现的成果还是很有限,白天有些困倦,自己也越发迷茫起来。

+

硕士还是博士?硕士的工作准备足够吗?博士的论文可能吗?这个是摆在我面前的最急迫的选择了。可能一年前甚至半年前,我都会坚定选择硕士,但是按照目前看来,周围的绝大多数人都是向着博士的方向去准备的,就当比你强的人和比你弱的人都选择了博士的道路,无论之前有多么坚定硕士,也会发生一些动摇。可能我有一些家庭的因素,有一些自己的因素,希望我自己可以远离家乡,在另外的大城市定居。但是就当其他人没有任何这种想法的时候,你也会怀疑你自己的想法是否合理?其实历史都是相似的,想想你自己的高考之前,是不是也是这种想法?那么现在你又回到了这个地方,所以你的想法是否正确呢?你会不会走上高考之后沉沦的老路呢?这些问题都要一点一点想明白,不能放任自流,否则你自己的心态会崩溃的。现在你的状态就不怎么样,最多一个月的时间一定要把自己调整过来,后面要进入下一个阶段的考验了。

+

至于个人问题,虽然还是很向往的,但是暂时也没什么办法考虑。果然大学才是最好的时机,越往后认识的人就越少了。只可惜自己大学时候没有遇到相同等级的人,过于感情用事了。现在毕竟硬件条件有限,只能慢慢随缘,相信有缘的人一定会来到,没有缘分强求也没有什么用。

+

就这么多吧,自己的问题自己要慢慢克服,当下要先把每一天过好,争取每一天都有收获。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20231006
+
https://zhangzhao219.github.io/2023/10/06/diary/diary20231006/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年10月6日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/10/07/Stance-Detection/index.html b/2023/10/07/Stance-Detection/index.html new file mode 100644 index 000000000..460d322fd --- /dev/null +++ b/2023/10/07/Stance-Detection/index.html @@ -0,0 +1,1420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Stance Detection - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Stance Detection

+ + +
+ +

立场检测相关内容总结整理

+ +

数据集

+

SemEval 2016

+

论文:Stance and Sentiment in Tweets

+

数据集可视化:http://www.saifmohammad.com/WebPages/StanceDataset.htm

+

VAST

+

Zero-shot数据集

+

New data released in this submission. Short column descriptions

+
    +
  • author: username of the comment author
  • +
  • post: original comment, unprocessed
  • +
  • ori_topic: heuristically extracted topic
  • +
  • ori_id: id generated to link post and heuristically extracted topics
  • +
  • new_topic: updated topic from crowdsourced annotations
  • +
  • label: stance label, 0=con, 1=pro, 2=neutral
  • +
  • type_idx: type number, 1=HeurTopic, 2=CorrTopic, 3=ListTopic, 4=Synthetic neutral
  • +
  • new_id: unique id for every comment-topic-label pair
  • +
  • arc_id: id of the original article on NYT
  • +
  • text: sentence and word tokenized and lowercased text, with punctuation and stopwords removed
  • +
  • text_s: string version of text
  • +
  • topic: tokenized and lowercased version topic, with punctuation and stopwords removed
  • +
  • topic_str: string version of topic
  • +
  • seen?: indicator for zero-shot or few-shot example, 0=zero-shot, 1=few-shot
  • +
  • contains_topic?: indicator for whether topic is contained in the text, 0=no, 1=yes
  • +
  • change_lst: list of swapped words (unique to vast_test-sentswap.csv)
  • +
  • change_type: type of sentiment swapping
  • +
  • LexSim: a list of lexically similar training topics (if a zero-shot topic)
  • +
  • Qte: whether the example contains quotes (1=yes, 0=no)
  • +
  • Sarc: whether the example contains sarcasm (1=yes, 0=no)
  • +
  • Imp: whether the text contains the topic and the label is non-neutral (1=yes, 0=no)
  • +
  • mlS: whether there are other examples with the same document and different, non-neutral, stance labels (1=yes, 0=no)
  • +
  • mlT: whether there are other examples with the same document and different topics (1=yes, 0=no)
  • +
+

WT-WT

+

相关链接:https://github.com/BinLiang-NLP/TPDG

+

51284条英文Tweet

+

关于公司的兼并收购的信息,第一个金融领域的数据集

+

四个标签:

+
    +
  • Support:两个公司会合并成一个公司
  • +
  • Refute:对两个公司要合并成一个的消息表示怀疑
  • +
  • Comment:对合并消息的评论,中立态度
  • +
  • Unrelated:完全不相关
  • +
+

P-stance

+

21574条英文Tweet

+

对三个target(Donald Trump(7953),Joe Biden(7296),Bernie Sanders(6325))的立场

+

按照8:1:1进行划分

+

UKP

+

论文

+

2017

+

A Dataset for Multi-Target Stance Detection

+

时间:2017年4月

+

等级:EACL 2017

+

2020

+

Will-They-Won’t-They: A Very Large Dataset for Stance Detection on Twitter

+

时间:2020年5月1日

+

等级:ACL 2020

+

pPvtXj0.md.png

+

思想:

+
    +
  • 第一个金融领域的立场数据集,描述公司的兼并收购的信息
  • +
  • 首先爬取关于公司、兼并等内容的Tweet
  • +
  • 定义四个标签(support, refute, comment, unrelated),其中一个Tweet的不同的target可能会有不同的标签
  • +
  • 找人进行标注,评估了标注的质量,并与之前的数据集进行了对比
  • +
  • 对目前的一些模型进行了这个数据集上面的测试
  • +
+

Zero-Shot Stance Detection: A Dataset and Model using Generalized Topic Representations

+

时间:2020年10月7日

+

等级:EMNLP 2020(CCF B)

+

思想:提出了VAST数据集

+
    +
  • 纽约时报辩论区的评论内容
  • +
  • 选择了3365条评论,包括304个主题,找人工进行主题标注
  • +
  • 中立的立场很少,从支持与反对两种类别中选一些可能性较低的加入到中立标签中
  • +
+

同时提出了一个方法解决Zero-shot问题

+

pPz7FzQ.md.png

+
    +
  • 文档和主题联合输入
  • +
  • 对主题进行聚类,获取注意力
  • +
+

数据集:VAST

+

2021

+

Target-adaptive Graph for Cross-target Stance Detection

+

时间:2021年4月

+

等级:WWW 2021(CCF A)

+

tWT–WT: A Dataset to Assert the Role of Target Entities for Detecting Stance of Tweets

+

时间:2021年6月

+

等级:NAACL 2021(CCF B)

+

Adversarial Learning for Zero-Shot Stance Detection on Social Media

+

时间:2021年6月

+

等级:NAACL 2021(CCF B)

+

思想:使用对抗学习增强zero-shot的立场检测的效果

+

piUUZ6J.md.png

+
    +
  • 使用BiCond编码text
  • +
  • 将编码的向量进行正则化
  • +
  • 对立场进行分类
  • +
  • 对topic进行鉴别
  • +
  • 增加对抗训练的技巧
  • +
+

数据集:Sem-16

+

Stance Detection in COVID-19 Tweets

+

时间:2021年8月

+

等级:ACL 2021(CCF A)

+

思想:

+
    +
  • 构建了一个COVID-19数据集,包括四个target,例如关闭学校、居家、戴口罩等
  • +
  • 用无标签的数据做预训练
  • +
  • 对不同的监督学习方法进行了比较
  • +
+

数据集:自行构建的COVID-19数据集

+

Enhancing Zero-shot and Few-shot Stance Detection with Commonsense Knowledge Graph

+

时间:2021年8月

+

等级:ACL 2021 Findings (CCF A)

+

思想:topic在文本中是可以通过图推断出来的

+

piU6EUH.png

+
    +
  • 用Bert对文本和topic进行编码
  • +
  • 使用ConceptNet获取文本之间的关系
  • +
  • 进行立场分类检测
  • +
+

数据集:

+

MeLT: Message-Level Transformer with Masked Document Representations as Pre-Training for Stance Detection

+

时间:2021年09月16日

+

等级:EMNLP 2021 Findings

+

pPj2v6S.png

+

思想:

+
    +
  • 在Twitter数据集上做预训练,将word级别的mask更改为message级别的mask,message的表示是word的表示取平均,按照时间顺序进行排列,对某个人的一些message进行随机mask(Bert的方式),让模型预测该位置的message。
  • +
  • 后续进行分类任务的微调
  • +
+

数据集:SemEval 2016

+

P-Stance: A Large Dataset for Stance Detection in Political Domain

+

时间:2021年08月

+

等级:ACL 2021 Findings

+

思想:

+
    +
  • 现有数据集局限 +
      +
    • 明确提及的目标和可能暴露立场的表层词汇线索在数据中显式存在
    • +
    • 社交媒体的数据太短了,模型不需要理解就可以找出立场
    • +
    +
  • +
  • 通过#的标签收集三个总统候选人的Tweet,收集了2.8 million条数据 +
      +
    • 选取10-128长度的Tweet
    • +
    • 移除重复数据
    • +
    • 只保留英文数据
    • +
    • 减少到2 million,为PSTANCE-EXT数据
    • +
    • 每个人采样10000,共30000条数据构成最终的数据集
    • +
    • 人工标注,并去除I don’t know类别的数据
    • +
    +
  • +
  • 构建一个#词典,删除文本后面的#,同时更改内部的#为中性的标记,防止暴露立场信息
  • +
  • 微调BERTweet预测CLS进行分类任务
  • +
  • 可以进行跨目标的立场检测、跨主题的立场检测(在2016年的数据上训练,预测2020年的立场)
  • +
  • 采用半监督方法(UST)提升跨主题的立场检测性能(没详细介绍)
  • +
+

数据集:SemEval-2016、Multi-Target stance datasets

+

2022

+

Zero-Shot Stance Detection via Contrastive Learning

+

时间:2022年4月

+

等级:WWW 2022(CCF A)

+

pPxUfET.md.png

+

思想:

+
    +
  • 将数据分为两种类型: +
      +
    • target-invariant:即使目标或目标相关词被屏蔽,仍然可以识别上下文中表达的立场。
    • +
    • target-specific:如果目标和与目标相关的词语被屏蔽,则很难理解立场信息。
    • +
    +
  • +
  • 训练一个普通的立场检测模型,训练到过拟合
  • +
  • 用主题模型找到与target最相关的词语,然后将其MASK掉,用上面的模型进行预测。如果预测对了就是target-invariant,错了就是target-specific,加一个标签给这个数据
  • +
  • 重新训练主模型 +
      +
    • target-invariant与target-specific之间作对比学习
    • +
    • 不同的label之间做对比学习
    • +
    +
  • +
  • 数据集: VAST、SEM-16、WT-WT
  • +
+

Infusing Knowledge from Wikipedia to Enhance Stance Detection

+

时间:2022年5月

+

等级:ACL 2022 Workshop(WASSA)

+

思想:从Wikipedia上预先查询到target的相关知识,融合到模型中进行立场检测

+

数据集:P-Stance、COVID-19-Stance、VAST

+

Few-Shot Stance Detection via Target-Aware Prompt Distillation

+

时间:2022年6月27日

+

等级:SIGIR 2022(CCF A)

+

pPvJ5xP.md.png

+

思想:

+
    +
  • 动机:target通常是随时间变化的,对每一个target都获取充足的数据进行训练是很不现实的,立场检测方法需要获得few-shot的能力
  • +
+
    +
  1. 多目标训练:训练一个模型,可以准确预测不同的target的label
  2. +
  3. 设计三个Prompt,输入到Bert等模型的预训练任务中,让其预测label
  4. +
  5. 预测的时候不映射到具体的label的词语,而是提前通过预训练模型获取label的表示向量,最终将target的向量与label的向量相乘计算损失
  6. +
  7. teacher-student model融合三个prompt的结果,迭代进行预测,对比真实标签与预测标签之间的差距。
  8. +
+

数据集:SemEval-2016、UKP

+

JointCL: A Joint Contrastive Learning Framework for Zero-Shot Stance Detection

+

时间:2022年5月

+

等级:ACL 2022(CCF A)

+

思想:

+

pPzo3hF.md.png

+

图相关

+
    +
  • 一个没有出现过的target的信息是可以通过其他已知的target表示出来的(从target-aware的视角来看)
  • +
  • 提出了由立场对比学习与原型图网络对比学习。通过构建原形图,可以在未知target和已知target之间建立关系,从而用已学习到的信息表示未知target,从而提升对未知target的立场学习能力。
  • +
+

数据集:VAST、SEM-16、WT-WT

+

A Survey on Stance Detection for Mis- and Disinformation Identification

+

时间:2022年7月

+

等级:NAACL 2022 Findings(CCF B)

+

思想:虚假新闻的立场检测,一篇综述性质的文章

+

数据集:没有做实验,只是汇总之前人的数据、方法与结果

+

Enhancing Zero-Shot Stance Detection via Targeted Background Knowledge

+

时间:2022年7月

+

等级:SIGIR 2022(CCF A)

+

pPxUhUU.png

+

思想:

+
    +
  • 用相关信息进行增强 +
      +
    • 根据target在网络上爬取相关信息,找最相关的top k个主题
    • +
    • 用NLTK的工具提取关键词,找到爬取的信息中与关键词最相关的部分,作为额外知识
    • +
    +
  • +
  • 其他的模型训练非常普通
  • +
+

数据集:VAST、SEM-16、WT-WT

+

Exploiting Sentiment and Common Sense for Zero-shot Stance Detection

+

时间:2022年10月

+

等级:COLING 2022

+

思想:

+

pPzolkT.md.png

+

图相关

+
    +
  1. 使用图自动编码的模块将target的普遍信息融合进立场检测的模型
  2. +
  3. 立场检测是被情感词汇影响的,使用Bert单独提取文档中的情感的词汇。
  4. +
+

Generative Data Augmentation with Contrastive Learning for Zero-Shot Stance Detection

+

时间:2022年12月

+

等级:EMNLP 2022(CCF B)

+

思想:在看见过的target的基础之上生成没有看见过的target的数据

+

piU6WM6.md.png

+
    +
  • 使用GAN网络进行对抗生成
  • +
  • 添加对比学习的策略
  • +
  • 在立场检测任务上进行微调
  • +
+

数据集:VAST、Sem-16

+

How would Stance Detection Techniques Evolve after the Launch of ChatGPT?

+

时间:2022年12月30日

+

等级:Arxiv

+

思想:

+
    +
  • 加个Prompt的立场检测效果可以达到SOTA
  • +
  • 多轮对话理论上可以增强背景知识等
  • +
  • 没有和很多的SOTA进行比较,没啥说服力
  • +
+

数据集:P-Stance

+

2023

+

Hashtag-Guided Low-Resource Tweet Classification

+

时间:2023年2月20日

+

等级:WWW 2023(CCF A)

+

思想:

+

piSBw1U.png

+
    +
  • Hash Tag是很重要的
  • +
  • Tweet注意力模块:获取Tweet之间的相关性从而借鉴已有的标签
  • +
  • 实体注意力模块:实体图获取Tweet中的实体
  • +
  • 融合两个模块生成HashTag
  • +
  • 通过原始的Tweet与HashTag一起输入到预训练模型中进行训练
  • +
+

数据集:

+

Few-shot Learning for Cross-Target Stance Detection by Aggregating Multimodal Embeddings

+

时间:2023年3月31日

+

等级:IEEE Transactions on Computational Social Systems(CCF C)

+

思想:

+

pipi3YF.png

+
    +
  • 通过发Tweet的人之间的关系网络增强立场检测的效果
  • +
  • 包括Follower、Like和Friend的信息
  • +
+

数据集:P-Stance,额外找到了作者的关系信息

+

Investigating Chain-of-thought with ChatGPT for Stance Detection on Social Media

+

时间:2023年4月6日

+

等级:Arxiv

+

思想:通过思维链的方式,给一个例子帮助ChatGPT进行分析,在多个数据集上达到了SOTA(假)效果

+

pipF4D1.md.png

+

数据集:SEM-16、VAST、P-Stance

+

Claim Extraction and Dynamic Stance Detection in COVID-19 Tweets

+

时间:2023年4月

+

等级:WWW 2023 Companion

+

思想:

+
    +
  • 是否存在主张:作者是否在Tweet中提出了客观事实的主张?并进一步分析是否值得检查。 +
      +
    • 微调Bert系列的模型来完成
    • +
    +
  • +
  • 主张提取:识别Tweet中的哪些部分对应于事实主张,哪些部分对应于作者的评论 +
      +
    • 使用IOB2方式进行标注,也是微调Bert进行,尝试了多种模型结构
    • +
    +
  • +
  • 动态立场检测:识别作者对事实主张的立场。不过主张是上面识别出来的,因此变化很大,基本上之前都没有见过
  • +
  • 数据集:自行收集的COVID-19的数据集
  • +
+

Can ChatGPT Reproduce Human-Generated Labels? A Study of Social Computing Tasks

+

时间:2023年4月22日

+

等级:无

+

思想:

+
    +
  • 将一些NLP任务的数据集通过ChatGPT进行标注,标注后评估效果
  • +
  • 在立场检测的任务上面大概0.5-0.6左右
  • +
+

Examining Temporalities on Stance Detection Towards COVID-19 Vaccination

+

时间:2023年5月7日

+

等级:ICWSM Data Challenge

+

思想:

+
    +
  • 划分数据集是以时间顺序进行划分的,更接近于真实的情况
  • +
  • 用单语言的Bert和多语言的Bert进行测试
  • +
+

数据集:COVID数据集

+

Robust Integration of Contextual Information for Cross-Target Stance Detection

+

(Contextual information integration for stance detection via cross-attention)

+

时间:2023年5月25日

+

等级:SEM2023(Co-located with ACL 2023)

+

pPxUqDx.png

+

思想:

+
    +
  • 一个灵活的结合外部知识的方法 +
      +
    • 一个Input+Target的Encoder和另外一个Context的Encoder,相当于Cross Attention
    • +
    • 直接连接Context与Text,相当于Self Attention
    • +
    +
  • +
  • 尝试了多种获取外部知识的方法。例如ConceptNet、CauseNet、预训练模型等
  • +
  • 多个数据集测试效果
  • +
+

Guiding Computational Stance Detection with Expanded Stance Triangle Framework

+

时间:2023年5月31日

+

等级:ACL 2023

+

思想:

+

pipkSVP.md.png

+
    +
  • 从语言学的角度考虑立场检测,使用很早就提出过的立场检测三角形
  • +
  • 语言学看不太懂,效果也没有很SOTA
  • +
  • 感觉就是方法比较新颖
  • +
+

数据集:SEM-16、P-Stance、VAST、Tweet-COVID

+

Knowledge-enhanced Prompt-tuning for Stance Detection

+

时间:2023年6月

+

等级:2023 ACM Transactions on Asian and Low-Resource Language Information Processing(SCI 4区 CCF C)

+

pPjWQbQ.md.png

+

思想:

+
    +
  • 将立场检测的任务通过Bert中MASK的方式转换成一个MLM任务
  • +
  • 自动空间映射器:用SenticNet扩充词汇,自动选择相关的词语进行答案的映射(涉及一个树模型)
  • +
  • 背景知识 +
      +
    1. 将target送入ConceptGraph中获得target的背景知识
    2. +
    3. 使用neural topic model学习利用#符号的语义信息(涉及变分自编码器VAE)
    4. +
    +
  • +
  • 将上述的知识一起作为Prompt送入到预训练模型中进行微调,得到类别
  • +
+

数据集:SEM16、VAST、P-stance、自己的数据集(ISD)

+

Topic-Guided Sampling For Data-Efficient Multi-Domain Stance Detection

+

时间:2023年6月

+

等级:ACL 2023 Oral(CCF A)

+

思想:

+

pPz522d.png

+
    +
  • 适用于跨主题(领域)的立场检测
  • +
  • 方法 +
      +
    • 通过主题模型进行训练数据的采样
    • +
    • 将立场检测看成序列分类问题(d, t),加个Prompt
    • +
    • 对比学习计算损失
    • +
    +
  • +
+

数据集:16个benchmark数据集

+

Voting Booklet Bias: Stance Detection in Swiss Federal Communication

+

时间:2023年6月15日

+

等级:Arxiv

+

思想:

+
    +
  • 分析的目标是面向选民的小册子中的Topic的立场是否为中立的立场
  • +
  • 模型结构没有创新,评价了一些方法的性能
  • +
  • 这个任务与普通的立场检测任务不同
  • +
+

数据集:x-stance

+

C-STANCE: A Large Dataset for Chinese Zero-Shot Stance Detection

+

时间:2023年7月

+

等级:ACL 2023(CCF A)

+

思想:第一个中文的Zero-shot数据集

+
    +
  • 微博的数据
  • +
  • 人工进行标注
  • +
  • 在多个立场检测的领域,使用多种方法进行评测
  • +
+

数据集:C-STANCE

+

A New Direction in Stance Detection: Target-Stance Extraction in the Wild

+

时间:2023年7月

+

等级:ACL 2023(CCF A)

+

思想:

+

piUJzO1.png

+
    +
  • 在立场检测中,target可能是隐含在text中的,大规模标注target不太现实
  • +
  • 从文本中获取target-stance的对
  • +
  • Target Identification: +
      +
    • 训练一个分类器对target进行分类
    • +
    • 用BART对target进行生成,然后map到已知的target上面
    • +
    +
  • +
  • Stance Detection +
      +
    • 建立一个分类器,并用target预测作为辅助任务
    • +
    +
  • +
+

数据集:SemEval-2016、AM、COVID-19、P-Stance、自己构建的zero-shot数据集

+

Distilling Calibrated Knowledge for Stance Detection

+

时间:2023年7月

+

等级:ACL 2023 Findings(CCF A)

+

思想:与知识蒸馏等相关

+

数据集:AM、COVID-19、P-Stance

+

Target-Oriented Relation Alignment for Cross-Lingual Stance Detection

+

时间:2023年7月

+

等级:ACL 2023 Findings(CCF A)

+

思想:将单语言的立场检测迁移到多语言上

+

piUcLtJ.md.png

+
    +
  • 使用mBert获取文本的表示
  • +
  • 构建target的关系图
  • +
+

也是图相关的工作

+

数据集:X-Stance-all

+

Exploration of Contrastive Learning Strategies toward more Robust Stance Detection

+

时间:2023年7月

+

等级:ACL 2023 Workshop(WASSA)

+

思想:使用对比学习增强立场检测系统的鲁棒性

+
    +
  • 词表相似的句子也能通过对比学习获取语义从而发现他们之间的区别
  • +
  • 选择anchor的策略可以有多种方法
  • +
  • 使用拼写错误、同义重复和同义词替换三种策略来对数据集进行增强
  • +
  • 不同的构造方法(数据集中的所有topic或者一部分的topic)进行不同的数据集下的训练,仅考虑二分类
  • +
+

数据集:DebateForum (DF), SemEval2016 (SE) ,ARC, Perspectrum, FNC-1, KSD-Biden, KSD-Trump

+

Zero-Shot and Few-Shot Stance Detection on Varied Topics via Conditional Generation

+

时间:2023年7月

+

等级:ACL 2023(CCF A)

+

思想:

+
    +
  • 用生成的思想做立场检测的问题,使用BART作为基础架构
  • +
  • 使用联合训练,不仅生成标签,同时生成target
  • +
  • Unlikelihood Training 数据增强方法提升性能
  • +
  • 结合Wiki的知识(其他论文的工作)
  • +
+

数据集:VAST

+

Ladder-of-Thought: Using Knowledge as Steps to Elevate Stance Detection

+

时间:2023年8月31日

+

等级:Arxiv

+

pPvJTr8.md.png

+

思想:

+
    +
  • CoT利用的是大模型内部的知识,但是立场检测相关的知识大模型可能没有见过
  • +
  • 首先在Google上面搜到target的相关信息
  • +
  • 用Text,Target和上面搜集到的信息微调一个预训练模型,让其产生更好的外部信息 Generation Finetuning
  • +
  • 在上面的模型基础上,将text,target,和上面产生的外部信息连接在一起输入到模型中,以预测label为目标进行微调
  • +
+

数据集:VAST

+

Use of Large Language Models for Stance Classification

+

时间:2023年9月24日

+

等级:ICWSM 2024 (CCF B)

+

思想:

+
    +
  • 尝试了四种Prompt,用上下文包裹text和target,加一些few shot的例子,最后让其解释原因
  • +
  • 尝试了几种开源的大模型
  • +
  • 总结:大模型不太行,不如微调过的小模型,甚至不如zero-shot
  • +
+

数据集:covid-lies、election2016、phemerumors、semeval2016、wtwt

+

STANCE-C3: Domain-adaptive Cross-target Stance Detection via Contrastive Learning and Counterfactual Generation

+

时间:2023年9月26日

+

等级:无

+

思想:

+

pPzQSNn.md.png

+
    +
  • 跨领域的知识迁移
  • +
  • 反事实数据增强
  • +
+

StanceReCL: Zero-Shot Stance Detection Based on Relevance and Contrastive Learning

+

时间:2023年10月

+

等级:投稿 EMNLP 2023 没中

+

思想:

+

pPvYprT.md.png

+
    +
  • 提出了几个概念:stance indicator(support,against和neutral),stance pattern(由stance indicator和target组成)
  • +
  • 两种表达:句子层面的表达([CLS]对应的最后一层的表示)与词语层面的表达(单个token的最后一层的隐藏状态)
  • +
  • 相关性的计算: +
      +
    • 上面的CLS与下面的CLS计算句子层面的相关性
    • +
    • 上面的stance indicator与下面的sentence中的最相关的词语计算相关性
    • +
    +
  • +
  • 句子层面计算对比学习的损失,然后与词语层面的损失加权重融合计算
  • +
+

数据集:SEM-16、VAST、WT-WT

+

Stance Detection with Collaborative Role-Infused LLM-Based Agents

+

时间:2023年10月

+

等级:Arxiv

+

思想:多个LLM的Agent一起分析文本的各个方面,最后一正一反对立场进行推断,完全的Zero-shot

+

piUkrSe.png

+

数据集:Sem-16、WT-WT、VAST

+

TATA: Stance Detection via Topic-Agnostic and Topic-Aware Embeddings

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:

+

piUk7Os.png

+
    +
  • topic-aware/TAW embeddings and generalized topic-agnostic/TAG stance embeddings
  • +
  • 使用T5-Flan作为基座模型
  • +
  • 收集了一个新的数据集,包括相关的passage对与相关的topic对,Topic-Aware/TAW Dataset +
      +
    • 使用T5对topic进行预测从而做预训练任务
    • +
    • 使用MPNet LLM 识别其他数据集中相同的topic
    • +
    +
  • +
  • 用TAW Dataset对VAST数据集进行扩充
  • +
  • Topic-Aware/TAW Embedding Layer:对整个的text-topic对进行训练
  • +
  • Topic-Agnostic/TAG Embedding Layer:topic看不到
  • +
  • 后面加两个注意力层
  • +
+

数据集:VAST

+

Support or Refute: Analyzing the Stance of Evidence to Detect Out-of-Context Mis- and Disinformation

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:多模态的信息不匹配会造成误解

+

pFuEtgA.md.png

+

分别训练图片的立场检测分类器、文本的立场检测分类器,外加一些实体的知识进行识别

+

数据集:NewsCLIPpings

+

Why Should This Article Be Deleted? Transparent Stance Detection in Multilingual Wikipedia Editor Discussions

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:在文本审核中加入立场检测从而进行自动判断其是否应该被删除

+

pFlQIR1.png

+

数据集:提出了多语言的Wiki的审核数据集

+

ORCHID: A Chinese Debate Corpus for Target-Independent Stance Detection and Argumentative Dialogue Summarization

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:

+
    +
  • 提出中文的辩论的立场检测数据集,且是与目标无关的
  • +
  • 提出立场相关的摘要任务,可以提升摘要的效果
  • +
+

数据集:

+

Cross-Lingual Cross-Target Stance Detection with Dual Knowledge Distillation Framework

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:

+

pFlQoxx.png

+

pFlQ7M6.png

+
    +
  • 提出了新的跨语言cross target的立场检测任务
  • +
  • 一个跨语言的老师,一个跨target的老师
  • +
  • 大量的目标语言的无标签数据如何利用
  • +
  • 使用mBert作为跨语言的teacher,翻译prompt和label构建文本对,使得两个文本对的预测结果更为接近
  • +
  • 使用上面的跨语言的teacher作为跨target的encoder
  • +
  • 使用GAT等将target分类,然后做与类别相关的对比学习
  • +
  • 用无标签的目标语言数据+两个teacher的伪标签训练
  • +
+

数据集:X-Stance、Semeval-2016、R-ita、Czech

+

Identification of Multimodal Stance Towards Frames of Communication

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:文字和图片的多模态立场检测,主要的贡献是数据集

+
    +
  • 在疫苗场景下
  • +
  • 收集了关于疫苗或者新冠的Twitter的数据集,包括文字与图片数据
  • +
  • 选择了一些多模态的模型作为baseline,通过OCR等方式提取图片中的文字
  • +
  • 分一些图片与文字不吻合的情况
  • +
+

数据集:MMVAX-STANCE

+

From Values to Opinions: Predicting Human Behaviors and Stances Using Value-Injected Large Language Models

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:与价值观相关,不算立场检测任务

+
    +
  • 使用Argument Generation和Question Answering两种方法对LLM进行微调
  • +
+

数据集:非立场检测

+

Stance Detection on Social Media with Background Knowledge

+

时间:2023年12月

+

等级:EMNLP 2023(CCF B)

+

思想:补充两种知识增强立场检测的效果

+

pFlQbqO.png

+
    +
  • Episodic knowledge:情景知识,只能从背景知识中推断出来
  • +
  • discourse knowledge:口语知识,代号、hashtag等
  • +
  • 在网络上搜索最相关的top10的wiki知识
  • +
  • 通过主题模型和关键词检索最相关的部分、使用大模型进行过滤
  • +
  • 使用大模型对口语知识进行扩充
  • +
  • 既微调了小模型,也在大模型上面做zero-shot看效果
  • +
+

数据集:Sem-16、P-Stance、VAST

+

EZ-STANCE: A Large Dataset for Zero-Shot Stance Detection

+

时间:2023年12月

+

等级:EMNLP 2023 Findings

+

思想:提出了与VAST对标的EZ-Stance数据集

+

数据集:EZ-Stance

+

Multi-label and Multi-target Sampling of Machine Annotation for Computational Stance Detection

+

时间:2023年12月

+

等级:EMNLP 2023 Findings

+

思想:思维链等zero-shot来增强直接使用大模型进行立场检测的效果

+

pFlQvid.png

+

数据集:

+

Chain-of-Thought Embeddings for Stance Detection on Social Media

+

时间:2023年12月

+

等级:EMNLP 2023 Findings

+

思想:用大模型对立场进行预测,然后输入到Roberta中进行再次预测

+

piUklJU.md.png

+

数据集:Tweet-Stance、P-Stance

+

Toxicity, Morality, and Speech Act Guided Stance Detection

+

时间:2023年12月

+

等级:EMNLP 2023 Findings

+

思想:关注一些情绪倾向

+

pFllSzt.png

+

数据集:SemEval、P-Stance、Climate、COVID

+

Multilingual Coarse Political Stance Classification of Media. The Editorial Line of a ChatGPT and Bard Newspaper

+

时间:2023年12月

+

等级:EMNLP 2023 Findings

+

思想:使用大模型对人工编写的新闻的倾向进行判断,不算立场检测

+

数据集:与立场检测无关

+ + +
+ +
+
+ + + + + + +
+
+
Stance Detection
+
https://zhangzhao219.github.io/2023/10/07/Stance-Detection/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年10月7日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/10/28/diary/diary20231028/index.html b/2023/10/28/diary/diary20231028/index.html new file mode 100644 index 000000000..a978d6c4f --- /dev/null +++ b/2023/10/28/diary/diary20231028/index.html @@ -0,0 +1,735 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20231028 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20231028

+ + +
+ +

2023年10月28日,周六

+ +

鼓起勇气约了一次,然而也没说什么更进一步的,就是普通同学的感觉。临走时还偏要与我AA,硬撑着没有要

+

然后借比赛的幌子微信上主动找过两次,第一次还比较正常,第二次她说了好多,从比赛转到报告,又转到一点点生活(生活)。当然可能是我有想法所以我想的比较多,人家可能就是正常的一问。

+

然而一条谁看了都会点赞的朋友圈,对于一个从来都会给我所有的朋友圈点赞的女生,这一次居然没有点。很伤心。这是故意的?还是为了引起我的注意?我一个如此单纯的小男生,经不住如此的试探。

+

突然发现似乎每一次找她都是秒回,当然不排除都是正在看手机,但是我却从来都不敢快回复她。

+

下回吃饭(要是还有下次)多试探一下吧,还是别太主动了,毕竟没有什么女性朋友,不想再失去了。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20231028
+
https://zhangzhao219.github.io/2023/10/28/diary/diary20231028/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年10月28日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/11/16/diary/diary20231116/index.html b/2023/11/16/diary/diary20231116/index.html new file mode 100644 index 000000000..9f793e1d8 --- /dev/null +++ b/2023/11/16/diary/diary20231116/index.html @@ -0,0 +1,737 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20231116 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20231116

+ + +
+ +

2023年11月16日,周四

+ +

来北京整整两年了

+

两年前的此时,我正在长沙黄花机场附近的酒店标数据(哈哈哈哈)记得那个任务很突然,也不太好做。当时应该是没有做完吧,记得后面过来之后还做了一小会时间。当然这些都不重要。

+

当时是一个什么样的心情呢?对未来的憧憬?对大学三年多早已熟悉的长沙和学校的留恋?对周围优秀的同学和当时的女友的不舍?现在似乎早都忘记了。唯一记得的是,当时总体的心情还是比较愉悦的。如果给自己的心情评个分?1-10分,估计会给自己7分吧。

+

然后就来到了既熟悉后陌生的北京。坐着地铁转了大半圈,遇见了老师与师兄师姐,有幸与三个健谈的室友一起度过每天的时光,与之前的同学相遇交流成长,独自一人或者与三五好友一起吃吃玩玩,同时入门了人工智能与自然语言处理。这一段时间应该算是最开心的时光了吧。没有忧愁也没有烦恼,每天有规律的做着不是很繁忙的事情,有很好的一群人陪在你身边,远方有牵挂着你的女友与家人。一切事情在有条不紊的进行当中。

+

这样一直到了22年6月,开心的拿了一大堆的证书与奖品毕业的同时,几条QQ消息直接将我拉到谷底。原来我认为的“有感情基础,平稳期”就只是浮云罢了,原来人家根本就没把你当成可以走完一辈子的人。原来之前两年多三年的感情与行动全部付诸东流。我发疯了一样要挽回,虽然挽回了,但是也没有什么真正的作用。这一段感情最终还是在22年9月无疾而终了。

+

从这时开始,我便没有真正的快乐过。我不断的怀疑自己,认为自己是一个很无趣的人,自己无法与别人相处,自己的情商很低,自己的先天条件不足,身高太矮等等。一直到现在,我也没有停止过任何这种想法。总是觉得自己可能就这辈子就这样了。之后也不会遇到太多的女同学,遇到了也不会喜欢我,我又不会去主动喜欢人家。我只能将自己埋在学习中,像本科一样进行各种尝试。

+

写不下去了,不知道自己在说些啥,不知道自己今后的感情生活怎么办,什么都不知道。慢慢来吧,前方的道路从来都是黑暗的。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20231116
+
https://zhangzhao219.github.io/2023/11/16/diary/diary20231116/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年11月16日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/11/19/diary/diary20231119/index.html b/2023/11/19/diary/diary20231119/index.html new file mode 100644 index 000000000..4d754ab31 --- /dev/null +++ b/2023/11/19/diary/diary20231119/index.html @@ -0,0 +1,752 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20231119 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20231119

+ + +
+ +

2023年11月19日,周日

+ +

今天看了一些自己博客的文章,发现对外公开的居然全都是刷题或者学习的内容,对于外人来说是不是太枯燥了一些hh。

+

于是挑了几篇过了很长时间的,或者已经没有隐藏的必要的文章,放出来也可以让其他人对我有更多的了解。

+

当然没放出来的文章还有很多,没办法很多的内容利益相关,或者写的时候直呼其名,要是公开感觉对其他人不太好,后续我会慢慢调整一下。

+

这些文章基本都在Life的标签下。

+

文笔不好,请见谅。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20231119
+
https://zhangzhao219.github.io/2023/11/19/diary/diary20231119/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年11月19日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/11/21/diary/diary20231121/index.html b/2023/11/21/diary/diary20231121/index.html new file mode 100644 index 000000000..e3fa477ec --- /dev/null +++ b/2023/11/21/diary/diary20231121/index.html @@ -0,0 +1,754 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20231121 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20231121

+ + +
+ +

2023年11月21日,周二

+ +

又感冒了(or 发烧?)没什么区别吧,反正现在感冒必发烧。

+

我还记得上初中的时候,有一次去辽工大打篮球,碰见了有一段时间没有见面的小学同学。他当时问了我一句话:“你还像小时候那样总生病吗?“当时我很奇怪,因为在我的印象里面小学时候生病不算很多。这个小学同学我至今也没有再见过了,也没有联系方式,但是这一次见面我始终都会记得,可能就因为他问了我这一句话吧。

+

初中我已经不太记得了,但是高中确实一直在生病。几乎每一个月我都要感冒或者发烧一次。尤其是刚刚保送的一个月中,我还记得应该是周四的物理晚自习(当时物理老师给我的印象很恐怖),正好我也在生病,我就把卷子都扔给了我同桌,美美的回去休息了一个晚上,第二天就基本好的差不多了。从那之后我渐渐意识到,生病也并不是纯客观原因,其实自己的情绪、压力等主观因素才是生病的必要条件。

+

上了大学之后我的感冒的次数就少很多了,但是每次有一些让我非常伤心难过的事情的时候,总会发一次烧。发烧逐渐成为了我宣泄的一个出口。心情难过了,无处抱怨,用较高的体温促使自己休息一下,帮助自己放松心情缓解压力。

+

前一段刚刚发烧了一次,在床上躺了一天的同时出去吃了一些想吃的,完全没有看电脑。然而短暂的放松过后,自己的任务也并没有随之减轻,还是要一点一点继续推进。虽然发烧可以帮助我休息,但是实际上并没有对我的目标等起到任何的作用,短暂的麻痹过后还是要继续前行。可能我就是这样的人吧,目标很现实,丝毫不敢放松,完成一个目标后开心的同时又向着下一个目标推进,因此我现在过的并不快乐。

+

如果有一天,我能真正放下一切好好休息一下,才算与自己达成了和解,内心可能才能真正快乐一些?

+

写的稍微有点丧,心情不太好。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20231121
+
https://zhangzhao219.github.io/2023/11/21/diary/diary20231121/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年11月21日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/12/02/diary/diary20231202/index.html b/2023/12/02/diary/diary20231202/index.html new file mode 100644 index 000000000..58f111654 --- /dev/null +++ b/2023/12/02/diary/diary20231202/index.html @@ -0,0 +1,737 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 杂谈-20231202 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

杂谈-20231202

+ + +
+ +

2023年12月02日,周六

+ +

行了,今天基本摊牌了,估计大概率是不会再理我了,概率应该会到99.9%,就算理我估计也没有什么好结果。

+

最近的一段时间感觉自己一直被这件事情主导了自己的情绪,感觉上很不好,也不能再这么下去了。总共两个月吧,今天基本就算彻底放下了。

+

其实也还好,毕竟怎么看自己都是配不上人家的,对你热情一些已经很够意思了。

+

还是要找一个足够喜欢你的吧,真的不要再碰你喜欢的了,真的就没有什么好结果。

+

以后再说吧,我这种人真的很不适合谈恋爱,只适合好好过日子,就慢慢等待吧。

+

后面认真学习,好好吃饭,在这样下去自己的胃都受不了了。

+

慢慢与自己和解吧。

+ + +
+ +
+
+ + + + + + +
+
+
杂谈-20231202
+
https://zhangzhao219.github.io/2023/12/02/diary/diary20231202/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年12月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/12/18/diary/diary20231218/index.html b/2023/12/18/diary/diary20231218/index.html new file mode 100644 index 000000000..fb12c4cff --- /dev/null +++ b/2023/12/18/diary/diary20231218/index.html @@ -0,0 +1,737 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2023年终总结 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

2023年终总结

+ + +
+ +

2023年终总结

+ +

说是年终总结,然而今天才是12月18日,目前还有两项长期任务和两项短期任务压在肩上。当然年终总结嘛,我不会去絮絮叨叨那些小事,写的像那些杂谈一样。

+

如果这个时候回看2022的年终总结,是可以看哭的。可能对于其他人来讲我一直都是一个较为冷漠的态度,没有什么情绪上的波动,但是实际上我的内心的活动是非常丰富的,总是有时候想着想着就有些想不开,甚至躺在床上自己偷偷哭一场。

+

我很想把这个心理状态归结为ISFJ的普遍特征,最近刷小红书比较多,感觉对ISFJ的每一个特征都能对的上。然而我知道这只不过是自欺欺人罢了。ISFJ又怎样?ISFJ就要一直自己内耗下去吗?ISFJ就不配拥有快乐与幸福吗?

+

又回到了我最近一直在思考的问题,什么是快乐?我一直在对自己说,对别人说,我自己不快乐,然而我真的不快乐吗?

+

~我是一个可爱的分界线~

+

2023的年终总结,然而现在已经是2024年的1月7日了。这期间一直发烧不舒服,反复来反复去,包括现在感觉我又有一点不舒服。这个年终总结注定难产,一方面是没有心情去写,另一方面是也还没想好写点什么,自己对于自己还是认识不清。

+

想写一点就写一点,不写就抓紧把自己的任务完成吧,怎么说1月底有些东西要准备收尾了。

+ + +
+ +
+
+ + + + + + +
+
+
2023年终总结
+
https://zhangzhao219.github.io/2023/12/18/diary/diary20231218/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2023年12月18日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/06/Leetcode/programmercarl/programmercarl-ds/index.html b/2024/03/06/Leetcode/programmercarl/programmercarl-ds/index.html new file mode 100644 index 000000000..679d75a71 --- /dev/null +++ b/2024/03/06/Leetcode/programmercarl/programmercarl-ds/index.html @@ -0,0 +1,2725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-基本数据结构专题 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-基本数据结构专题

+ + +
+ +

代码随想录-基本数据结构专题

+ +

数组

+

数组理论基础

+

需要两点注意的是

+
    +
  • 数组下标都是从0开始的。
  • +
  • 数组内存空间的地址是连续的 +
      +
    • C++中二维数组是连续分布的
    • +
    • Java中二维数组不一定是连续分布的
    • +
    +
  • +
+

实际中一定要判断好自己的下标操作有没有越界!

+

704. 二分查找

+

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

+

模板题,烂熟于心了

+

第一种写法:定义 target 是在一个在左闭右闭的区间里, 也就是[left, right] (这个很重要非常重要)

+

区间的定义决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

+
    +
  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • +
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
  • +
+

第二种写法:定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。

+
    +
  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • +
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
  • +
+
func search(nums []int, target int) int {
+    lennums := len(nums)
+    l := 0
+    r := lennums
+    for l < r{
+        mid := l + (r-l)/2
+        if nums[mid] == target{
+            return mid
+        } else if nums[mid] < target{
+            l = mid + 1
+        } else{
+            r = mid
+        }
+    }
+    return -1
+}
+
class Solution {
+public:
+    int search(vector<int>& nums, int target) {
+        int left = 0;
+        int right = nums.size() - 1;
+        while(left <= right){
+            int mid = left + (right - left) / 2;
+            if (nums[mid] == target){
+                return mid;
+            } else if (nums[mid] < target){
+                left = mid + 1;
+            } else{
+                right = mid - 1;
+            }
+        }
+        return -1;
+    }
+};
+

35. 搜索插入位置

+

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

+

请必须使用时间复杂度为 O(log n) 的算法。

+
func searchInsert(nums []int, target int) int {
+    lennums := len(nums)
+    l := 0
+    r := lennums
+    for l < r{
+        mid := l + (r-l)/2
+        if nums[mid] == target{
+            return mid
+        } else if nums[mid] < target{
+            l = mid + 1
+        } else{
+            r = mid
+        }
+    }
+    return l
+}
+
class Solution {
+public:
+    int searchInsert(vector<int>& nums, int target) {
+        int left = 0;
+        int right = nums.size();
+        while(left < right){
+            int mid = left + (right - left) / 2;
+            if (nums[mid] == target){
+                return mid;
+            } else if (nums[mid] < target){
+                left = mid + 1;
+            } else{
+                right = mid;
+            }
+        }
+        return right;
+    }
+};
+

34. 在排序数组中查找元素的第一个和最后一个位置

+

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

+

如果数组中不存在目标值 target,返回 [-1, -1]。

+

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

+

注意判断下标!!!思路没有任何问题,判断下标即可!!!!

+
func searchRange(nums []int, target int) []int {
+    result := make([]int,2)
+    result[0] = lowerbound(nums,target)
+    result[1] = upperbound(nums,target)
+    return result
+}
+
+func lowerbound(nums []int, target int) int {
+    lennums := len(nums)
+    l := 0
+    r := lennums
+    for l < r{
+        mid := l + (r-l)/2
+        if nums[mid] == target{
+            r = mid
+        } else if nums[mid] < target{
+            l = mid + 1
+        } else{
+            r = mid
+        }
+    }
+    if l < lennums && nums[l] == target{
+        return l
+    }
+    return -1
+}
+
+func upperbound(nums []int, target int) int {
+    lennums := len(nums)
+    l := 0
+    r := lennums
+    for l < r{
+        mid := l + (r-l)/2
+        if nums[mid] == target{
+            l = mid + 1
+        } else if nums[mid] < target{
+            l = mid + 1
+        } else{
+            r = mid
+        }
+    }
+    if r-1 >= 0 && nums[r-1] == target{
+        return r-1
+    }
+    return -1
+}
+
class Solution {
+public:
+    int searchbegin(vector<int>& nums, int target){
+        int left = 0;
+        int right = nums.size();
+        while(left < right){
+            int mid = left + (right - left) / 2;
+            if (nums[mid] >= target){
+                right = mid;
+            } else{
+                left = mid + 1;
+            }
+        }
+        if ((left < nums.size()) && (nums[left] == target)){
+            return left;
+        }
+        return -1;
+    }
+    int searchend(vector<int>& nums, int target){
+        int left = 0;
+        int right = nums.size();
+        while(left < right){
+            int mid = left + (right - left) / 2;
+            if (nums[mid] <= target){
+                left = mid + 1;
+            } else{
+                right = mid;
+            }
+        }
+        if ((right-1 >= 0) && (nums[right-1] == target)){
+            return right-1;
+        }
+        return -1;
+    }
+    vector<int> searchRange(vector<int>& nums, int target) {
+        vector<int> result = {-1,-1};
+        result[0] = searchbegin(nums, target);
+        result[1] = searchend(nums, target);
+        return result;
+    }
+};
+

69. x 的平方根

+

给你一个非负整数 x ,计算并返回 x算术平方根

+

由于返回类型是整数,结果只保留 整数部分,小数部分将被 舍去 。

+
func mySqrt(x int) int {
+    if x == 1{
+        return 1
+    }
+    l := 1
+    r := x
+    for l < r{
+        mid := l + (r-l)/2
+        if mid*mid == x{
+            return mid
+        } else if mid*mid < x{
+            l = mid + 1
+        } else{
+            r = mid
+        }
+    }
+    return l-1
+}
+
class Solution {
+public:
+    int mySqrt(int x) {
+        if (x == 0 || x == 1){
+            return x;
+        }
+        int left = 0;
+        int right = x;
+        while(left < right){
+            int mid = left + (right - left) / 2;
+            if((long long)mid * mid == x){
+                return mid;
+            } else if ((long long)mid * mid < x){
+                left = mid + 1;
+            } else{
+                right = mid;
+            }
+        }
+        return left - 1;
+    }
+};
+

367. 有效的完全平方数

+

给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false

+
func isPerfectSquare(num int) bool {
+    if num == 1{
+        return true
+    }
+    l := 1
+    r := num
+    for l < r{
+        mid := l + (r-l)/2
+        if mid*mid == num{
+            return true
+        } else if mid*mid < num{
+            l = mid + 1
+        } else{
+            r = mid
+        }
+    }
+    return false
+}
+
class Solution {
+public:
+    bool isPerfectSquare(int num) {
+        if (num == 1){
+            return true;
+        }
+        int left = 0;
+        int right = num;
+        while(left < right){
+            int mid = left + (right - left) / 2;
+            if ((long long)mid * mid == num){
+                return true;
+            } else if ((long long)mid * mid < num){
+                left = mid + 1;
+            } else{
+                right = mid;
+            }
+        }
+        return false;
+    }
+};
+

27. 移除元素

+

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

+

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

+

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

+

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

+

定义快慢指针

+
    +
  • 快指针:寻找新数组的元素,新数组就是不含有目标元素的数组
  • +
  • 慢指针:指向更新 新数组下标的位置
  • +
+
func removeElement(nums []int, val int) int {
+    l := 0
+    numslen := len(nums)
+    for r := 0;r < numslen;r++{
+        if nums[r] != val{
+            nums[l] = nums[r]
+            l++
+        }
+    }
+    return l
+}
+
class Solution {
+public:
+    int removeElement(vector<int>& nums, int val) {
+        int left = 0;
+        for(int right = 0;right < nums.size(); right += 1){
+            if (nums[right] != val){
+                nums[left] = nums[right];
+                left += 1;
+            }
+        }
+        return left;
+    }
+};
+

26. 删除排序数组中的重复项

+

给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。

+

由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。

+

将最终结果插入 nums 的前 k 个位置后返回 k 。

+

不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

+

还是双指针,思路不太好,想了一小会

+
func removeDuplicates(nums []int) int {
+    l := 0
+    lennums := len(nums)
+    for r:=1;r<lennums;r++{
+        k := nums[l]
+        if nums[r] != k{
+            l++
+            nums[l] = nums[r]
+        }
+    }
+    return l+1
+}
+
class Solution {
+public:
+    int removeDuplicates(vector<int>& nums) {
+        int left = 0;
+        for(int right = 1;right < nums.size();right++){
+            if (nums[right] != nums[left]){
+                nums[++left] = nums[right];
+            }
+        }
+        return left + 1;
+    }
+};
+

283. 移动零

+

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

+

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

+
func moveZeroes(nums []int)  {
+    l := 0
+    numslen := len(nums)
+    for r := 0;r < numslen;r++{
+        if nums[r] != 0{
+            nums[l] = nums[r]
+            l++
+        }
+    }
+    for l != numslen{
+        nums[l] = 0
+        l++
+    }
+}
+
class Solution {
+public:
+    void moveZeroes(vector<int>& nums) {
+        int left = 0;
+        for(int right = 0;right < nums.size();right++){
+            if(nums[right] != 0){
+                nums[left] = nums[right];
+                left += 1;
+            }
+        }
+        for(;left < nums.size();left += 1){
+            nums[left] = 0;
+        }
+    }
+};
+

844. 比较含退格的字符串

+

给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。

+

注意:如果对空文本输入退格字符,文本继续为空。

+
func backspaceCompare(s string, t string) bool {
+    s1 := []rune(s)
+    l := 0
+    slen := len(s1)
+    for r:=0;r<slen;r++{
+        if s1[r] == '#'{
+            if l > 0{
+                l--
+            }
+        } else{
+            s1[l] = s1[r]
+            l++
+        }
+    }
+    s1 = s1[:l]
+    s2 := []rune(t)
+    l = 0
+    tlen := len(s2)
+    for r:=0;r<tlen;r++{
+        if s2[r] == '#'{
+            if l > 0{
+                l--
+            }
+        } else{
+            s2[l] = s2[r]
+            l++
+        }
+    }
+    s2 = s2[:l]
+	return string(s1) == string(s2)
+}
+
class Solution {
+public:
+    bool backspaceCompare(string s, string t) {
+        int slen = s.length() - 1;
+        int tlen = t.length() - 1;
+        int sskip = 0;
+        int tskip = 0;
+        while(slen >= 0 || tlen >= 0){
+            while(slen >= 0){
+                if (s[slen] == '#'){
+                    sskip += 1;
+                    slen -= 1;
+                } else if (sskip > 0){
+                    sskip -= 1;
+                    slen -= 1;
+                } else{
+                    break;
+                }
+            }
+            while(tlen >= 0){
+                if (t[tlen] == '#'){
+                    tskip += 1;
+                    tlen -= 1;
+                } else if (tskip > 0){
+                    tskip -= 1;
+                    tlen -= 1;
+                } else{
+                    break;
+                }
+            }
+            if (slen >= 0 && tlen >= 0){
+                if (s[slen] != t[tlen]){
+                    return false;
+                }
+            } else if (slen >= 0 || tlen >= 0){
+                return false;
+            }
+            slen -= 1;
+            tlen -= 1;
+
+        }
+        return true;
+    }
+};
+

977.有序数组的平方

+

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

+

实现的是从中间往两边进行遍历,也可以修改成从两边往中间遍历的代码

+
func sortedSquares(nums []int) []int {
+    lennums := len(nums)
+    minindex := 0
+    minnum := nums[0]*nums[0]
+    for i:=0;i<lennums;i++{
+        nums[i] = nums[i] * nums[i]
+        if nums[i] < minnum{
+            minnum = nums[i]
+            minindex = i
+        }
+    }
+    result := make([]int,lennums)
+    result[0] = nums[minindex]
+    l := minindex-1
+    r := minindex+1
+    i := 1
+    for l >= 0 && r < lennums{
+        if nums[l] < nums[r]{
+            result[i] = nums[l]
+            l--
+        } else{
+            result[i] = nums[r]
+            r++
+        }
+        i++
+    }
+    for l >= 0{
+        result[i] = nums[l]
+        l--
+        i++
+    }
+    for r < lennums{
+        result[i] = nums[r]
+        r++
+        i++
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<int> sortedSquares(vector<int>& nums) {
+        vector<int> result;
+        stack<int> temp;
+        int left = 0;
+        int right = nums.size() - 1;
+        while(left < right){
+            int a = nums[left] * nums[left];
+            int b = nums[right] * nums[right];
+            if (a < b){
+                temp.push(b);
+                right--;
+            } else{
+                temp.push(a);
+                left++;
+            }
+        }
+        result.push_back(nums[left] * nums[left]);
+        while(!temp.empty()){
+            result.push_back(temp.top());
+            temp.pop();
+        }
+        return result;
+    }
+};
+

209.长度最小的子数组

+

给定一个含有 n 个正整数的数组和一个正整数 target 。

+

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

+

滑动窗口典型题目

+
func minSubArrayLen(target int, nums []int) int {
+    lennums := len(nums)
+    l := 0
+    r := 0
+    minlength := lennums+2
+    nowsum := 0
+    allsum := 0
+    for r < lennums{
+        for r < lennums && nowsum < target{
+            nowsum += nums[r]
+            allsum += nums[r]
+            r++
+        }
+        minlength = min(minlength,r-l)
+        for nowsum >= target{
+            nowsum -= nums[l]
+            l++
+        }
+        minlength = min(minlength,r-l+1)
+    }
+
+    if allsum < target{
+        return 0
+    } 
+    return minlength
+}
+
+func min(a int,b int) int{
+    if a < b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int minSubArrayLen(int target, vector<int>& nums) {
+        int result = nums.size() + 1;
+        int left = 0;
+        int right = 0;
+        int tempsum = 0;
+        while(right < nums.size()){
+            while(right < nums.size() && tempsum < target){
+                tempsum += nums[right];
+                right += 1;
+            }
+            while(tempsum >= target){
+                tempsum -= nums[left];
+                left += 1;
+            }
+            result = min(right - left + 1, result);
+        }
+        if (result == nums.size() + 1){
+            return 0;
+        }
+        return result;
+    }
+};
+

904. 水果成篮

+

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。

+

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

+

你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
+你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
+一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
+给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

+
func totalFruit(fruits []int) int {
+    fruitmap := make(map[int]int)
+    lennums := len(fruits)
+    l := 0
+    r := 0
+    maxlength := 0
+    for r < lennums{
+        for r < lennums{
+            _, ok := fruitmap[fruits[r]]
+            if !ok && len(fruitmap) >= 2{
+                break
+            }
+            fruitmap[fruits[r]] += 1
+            r++
+        }
+        maxlength = max(maxlength,r-l)
+        if r < lennums{
+            for len(fruitmap) == 2{
+                if fruitmap[fruits[l]] == 1{
+                    delete(fruitmap,fruits[l])
+                } else{
+                    fruitmap[fruits[l]]--
+                }
+                l++
+            }
+        }
+
+    }
+    return maxlength
+}
+
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int totalFruit(vector<int>& fruits) {
+        int res = 0;
+        int left = 0;
+        map<int,int> mp;
+        for(int right=0;right < fruits.size();right += 1){
+            mp[fruits[right]]++;
+            while (mp.size() > 2){
+                mp[fruits[left]]--;
+                if (mp[fruits[left]] == 0){
+                    mp.erase(mp.find(fruits[left]));
+                }
+                left++;
+            }
+            res = max(res,right-left+1);
+        }
+        return res;
+    }
+};
+

76. 最小覆盖子串

+

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

+
class Solution {
+public:
+    string minWindow(string s, string t) {
+        map<char,int> mp;
+        int zero_count = 0;
+        for (int i=0;i<t.size();i++){
+            mp[t[i]] += 1;
+            if (mp[t[i]] == 1){
+                zero_count += 1;
+            } 
+        }
+        int min_begin = 0;
+        int min_end = s.size();
+        int left = 0;
+        bool istrue = false;
+        for(int right=0;right < s.size();right += 1){
+            
+            mp[s[right]]--;
+            if (mp[s[right]] == 0){
+                zero_count -= 1;
+            }
+            while(zero_count == 0){
+                istrue = true;
+                if (right - left < min_end - min_begin){
+                    min_end = right;
+                    min_begin = left;
+                }
+                if(mp[s[left]] == 0){
+                    zero_count = 1;
+                }
+                mp[s[left]] += 1;
+                left += 1;
+            }
+
+        }
+        if (istrue){
+            return s.substr(min_begin,min_end-min_begin+1);
+        }
+        return "";
+    }
+};
+

59. 螺旋矩阵II

+

给你一个正整数 n ,生成一个包含 1n^2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

+
func generateMatrix(n int) [][]int {
+    result := make([][]int,n)
+    for i,_ := range result{
+        result[i] = make([]int,n)
+    }
+    boolmatrix := make([][]bool,n)
+    for i,_ := range boolmatrix{
+        boolmatrix[i] = make([]bool,n)
+    }
+    result[0][0] = 1
+    boolmatrix[0][0] = true
+    resultindex := 1
+    i := 0
+    j := 0
+    for resultindex < n*n{
+        for resultindex < n*n && j+1 < n && boolmatrix[i][j+1] == false{
+            j++
+            result[i][j] = resultindex+1
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+        for resultindex < n*n && i+1 < n && boolmatrix[i+1][j] == false{
+            i++
+            result[i][j] = resultindex+1
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+        for resultindex < n*n && j-1 >= 0 && boolmatrix[i][j-1] == false{
+            j--
+            result[i][j] = resultindex+1
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+        for resultindex < n*n && i-1 >= 0 && boolmatrix[i-1][j] == false{
+            i--
+            result[i][j] = resultindex+1
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<vector<int>> generateMatrix(int n) {
+        vector<vector<int> > res(n, vector<int>(n));
+        int num = 1;
+        int x = 0;
+        int y = -1;
+        while(num <= n * n){
+            while(num <= n*n && y+1 < n && res[x][y+1] == 0){
+                y += 1;
+                res[x][y] = num;
+                num += 1;
+                
+            }
+            while(num <= n*n && x+1 < n && res[x+1][y] == 0){
+                x += 1;
+                res[x][y] = num;
+                num += 1;
+            }
+            while(num <= n*n && y-1 >= 0 && res[x][y-1] == 0){
+                y -= 1;
+                res[x][y] = num;
+                num += 1;
+            }
+            while(num <= n*n && x-1 >= 0 && res[x-1][y] == 0){
+                x -= 1;
+                res[x][y] = num;
+                num += 1;
+            }
+        }
+        return res;
+    }
+};
+

54. 螺旋矩阵

+

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

+
func spiralOrder(matrix [][]int) []int {
+    m := len(matrix)
+    n := len(matrix[0])
+    result := make([]int,m*n)
+    boolmatrix := make([][]bool,m)
+    for i,_ := range boolmatrix{
+        boolmatrix[i] = make([]bool,n)
+    }
+    result[0] = matrix[0][0]
+    boolmatrix[0][0] = true
+    resultindex := 1
+    i := 0
+    j := 0
+    for resultindex < m*n{
+        for resultindex < m*n && j+1 < n && boolmatrix[i][j+1] == false{
+            j++
+            result[resultindex] = matrix[i][j]
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+        for resultindex < m*n && i+1 < m && boolmatrix[i+1][j] == false{
+            i++
+            result[resultindex] = matrix[i][j]
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+        for resultindex < m*n && j-1 >= 0 && boolmatrix[i][j-1] == false{
+            j--
+            result[resultindex] = matrix[i][j]
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+        for resultindex < m*n && i-1 >= 0 && boolmatrix[i-1][j] == false{
+            i--
+            result[resultindex] = matrix[i][j]
+            boolmatrix[i][j] = true
+            resultindex++
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<int> spiralOrder(vector<vector<int>>& matrix) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        vector<int> res(m*n);
+        vector<vector<bool> > judge(m, vector<bool>(n));
+        int x = 0;
+        int y = -1;
+        int index = 0;
+        while(index < m * n){
+            while(index < m * n && y+1 < n && judge[x][y+1] == false){
+                y += 1;
+                judge[x][y] = true;
+                res[index] = matrix[x][y];
+                index += 1;
+            }
+            while(index < m * n && x+1 < m && judge[x+1][y] == false){
+                x += 1;
+                judge[x][y] = true;
+                res[index] = matrix[x][y];
+                index += 1;
+            }
+            while(index < m * n && y-1 >= 0 && judge[x][y-1] == false){
+                y -= 1;
+                judge[x][y] = true;
+                res[index] = matrix[x][y];
+                index += 1;
+            }
+            while(index < m * n && x-1 >= 0 && judge[x-1][y] == false){
+                x -= 1;
+                judge[x][y] = true;
+                res[index] = matrix[x][y];
+                index += 1;
+            }
+        }
+        return res;
+    }
+};
+

链表

+

链表理论基础

+

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

+

链接的入口节点称为链表的头结点也就是head。

+

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。

+

循环链表:链表首尾相连,可以用来解决约瑟夫环问题

+

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。

+

链表是通过指针域的指针链接在内存中各个节点。

+

所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

+

链表的定义:

+
// 单链表
+struct ListNode {
+    int val;  // 节点上存储的元素
+    ListNode *next;  // 指向下一个节点的指针
+    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
+};
+

链表的操作

+

删除节点

+

删除D节点,如图所示:

+

链表-删除节点

+

只要将C节点的next指针 指向E节点就可以了。

+

那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。

+

是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。

+

其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

+

添加节点

+

如图所示:

+

链表-添加节点

+

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

+

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

+

203. 移除链表元素

+

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

+
/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ *     Val int
+ *     Next *ListNode
+ * }
+ */
+func removeElements(head *ListNode, val int) *ListNode {
+    dummy := &ListNode{Val: 0, Next: head}
+    p := dummy
+    q := head
+    for q != nil{
+        if q.Val == val{
+            p.Next = q.Next
+            q = p.Next
+        } else{
+            p = q
+            q = q.Next
+        }
+    }
+    return dummy.Next
+}
+
class Solution {
+public:
+    ListNode* removeElements(ListNode* head, int val) {
+        ListNode* dummy = new ListNode(-1);
+        dummy->next = head;
+        ListNode* p = dummy;
+        while(p->next != NULL){
+            if(p->next->val == val){
+                p->next = p->next->next;
+            } else{
+                p = p->next;
+            }
+        }
+        return dummy->next;
+    }
+};
+

707. 设计链表

+

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

+

在链表类中实现这些功能:

+

get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
+addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
+addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
+addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
+deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

+
type MyLinkedList struct {
+    dummy *Node
+}
+
+type Node struct {
+	Val  int
+	Next *Node
+}
+
+func Constructor() MyLinkedList {
+    rear := &Node{
+        Val : -1,
+        Next : nil,
+    }
+    rear.Next = nil
+    return MyLinkedList{rear}
+}
+
+func (this *MyLinkedList) Get(index int) int {
+    head := this.dummy.Next
+    for index != 0 && head != nil{
+        index--
+        head = head.Next
+    }
+    if head == nil{
+        return -1
+    }
+    return head.Val
+}
+
+
+func (this *MyLinkedList) AddAtHead(val int)  {
+    newnode := &Node{
+        Val : val,
+        Next : this.dummy.Next,
+    }
+    this.dummy.Next = newnode
+}
+
+
+func (this *MyLinkedList) AddAtTail(val int)  {
+    head := this.dummy
+    for head.Next != nil{
+        head = head.Next
+    }
+    head.Next = &Node{
+        Val : val,
+        Next : nil,
+    }
+}
+
+
+func (this *MyLinkedList) AddAtIndex(index int, val int)  {
+    newnode := &Node{
+        Val : val,
+        Next : nil,
+    }
+    if index <= 0{
+        newnode.Next = this.dummy.Next
+        this.dummy.Next = newnode
+    } else{
+        head := this.dummy.Next
+        for head != nil{
+            if index == 1{
+                newnode.Next = head.Next
+                head.Next = newnode
+            }
+            head = head.Next
+            index--
+        }
+    }
+}
+
+
+func (this *MyLinkedList) DeleteAtIndex(index int)  {
+    if index >= 0{
+        dummy := this.dummy
+        head := this.dummy.Next
+        for head != nil{
+            if index == 0{
+                dummy.Next = head.Next
+                return
+            }
+            dummy = head
+            head = head.Next
+            index--
+        }
+    }
+}
+
+
+/**
+ * Your MyLinkedList object will be instantiated and called as such:
+ * obj := Constructor();
+ * param_1 := obj.Get(index);
+ * obj.AddAtHead(val);
+ * obj.AddAtTail(val);
+ * obj.AddAtIndex(index,val);
+ * obj.DeleteAtIndex(index);
+ */
+
class MyLinkedList {
+public:
+    MyLinkedList() {
+        this->size = 0;
+        this->head = new ListNode(0);
+    }
+    
+    int get(int index) {
+        if(index >= this->size || index <= -1){
+            return -1;
+        }
+        ListNode* p = this->head;
+        while(index != 0){
+            index -= 1;
+            p = p->next;
+        }
+        return p->val;
+    }
+    
+    void addAtHead(int val) {
+        addAtIndex(0, val);
+    }
+    
+    void addAtTail(int val) {
+        addAtIndex(this->size, val);
+    }
+    
+    void addAtIndex(int index, int val) {
+        if(index > this->size || index <= -1){
+            return;
+        }
+        ListNode* dummy = new ListNode(-1);
+        ListNode* p = dummy;
+        dummy->next = this->head;
+        while(index != 0){
+            index -= 1;
+            p = p->next;
+        }
+        ListNode* newnode = new ListNode(val);
+        newnode->next = p->next;
+        p->next = newnode;
+        this->head = dummy->next;
+        this->size += 1;
+    }
+    
+    void deleteAtIndex(int index) {
+        if(index >= this->size || index <= -1){
+            return;
+        }
+        ListNode* dummy = new ListNode(-1);
+        ListNode* p = dummy;
+        dummy->next = this->head;
+        while(index != 0){
+            index -= 1;
+            p = p->next;
+        }
+        p->next = p->next->next;
+        this->head = dummy->next;
+        this->size -= 1;
+    }
+private:
+    int size;
+    ListNode *head;
+};
+

206. 反转链表

+

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

+
/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ *     Val int
+ *     Next *ListNode
+ * }
+ */
+func reverseList(head *ListNode) *ListNode {
+    var pre *ListNode
+    for head != nil{
+        temp := head.Next
+        head.Next = pre
+        pre = head
+        head = temp
+    }
+    return pre
+}
+
class Solution {
+public:
+    ListNode* reverseList(ListNode* head) {
+        ListNode* pre = NULL;
+        ListNode* p = head;
+        while(p != NULL){
+            ListNode* q = p->next;
+            p->next = pre;
+            pre = p;
+            p = q;
+        }
+        return pre;
+    }
+};
+

24. 两两交换链表中的节点

+

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

+
/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ *     Val int
+ *     Next *ListNode
+ * }
+ */
+func swapPairs(head *ListNode) *ListNode {
+    dummy := &ListNode{
+        Val : 0,
+        Next : head,
+    }
+    var pre *ListNode
+    pre = dummy
+    for head != nil && head.Next != nil{
+        temp := head.Next.Next
+        pre.Next = head.Next
+        head.Next = temp
+        pre.Next.Next = head
+        pre = head
+        head = temp
+    }
+    head = dummy.Next
+    return head
+}
+
class Solution {
+public:
+    ListNode* swapPairs(ListNode* head) {
+        ListNode* dummy = new ListNode(-1);
+        dummy->next = head;
+        ListNode* p = dummy;
+        while(p != NULL){
+            if (p->next != NULL && p->next->next != NULL){
+                ListNode* a = p->next;
+                ListNode* b = a->next;
+                ListNode* c = b->next;
+                p->next = b;
+                b->next = a;
+                a->next = c;
+                p = a;
+            } else{
+                break;
+            }
+        }
+        return dummy->next;
+    }
+};
+

19. 删除链表的倒数第N个节点

+

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

+

还可以使用快慢指针做这个题

+
/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ *     Val int
+ *     Next *ListNode
+ * }
+ */
+func removeNthFromEnd(head *ListNode, n int) *ListNode {
+    dummy := &ListNode{
+        Val:0,
+        Next: head,
+    }
+    count := 1
+    for head != nil{
+        head = head.Next
+        count++
+    }
+    var pre *ListNode
+    pre = dummy
+    head = dummy.Next
+    for count-n != 1{
+        count--
+        pre = head
+        head = head.Next
+    }
+    pre.Next = head.Next
+    return dummy.Next
+}
+
class Solution {
+public:
+    ListNode* removeNthFromEnd(ListNode* head, int n) {
+        ListNode* dummy = new ListNode(-1);
+        dummy->next = head;
+        ListNode* slow = dummy;
+        ListNode* fast = dummy;
+        while(n--){
+            fast = fast->next;
+        }
+        while(fast->next != NULL){
+            slow = slow->next;
+            fast = fast->next;
+        }
+        slow->next = slow->next->next;
+        return dummy->next;
+    }
+};
+

160. 链表相交

+

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

+

题目数据 保证 整个链式结构中不存在环。

+

注意,函数返回结果后,链表必须 保持其原始结构 。

+
/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ *     Val int
+ *     Next *ListNode
+ * }
+ */
+func getIntersectionNode(headA, headB *ListNode) *ListNode {
+    var p,q *ListNode
+    p = headA
+    q = headB
+    count := 60000
+    if p == nil || q == nil{
+        return nil
+    }
+    for p != q && count > 0{
+        p = p.Next
+        q = q.Next
+        if p == nil{
+            p = headB
+        }
+        if q == nil{
+            q = headA
+        }
+        count--
+    }
+    if count == 0{
+        return nil
+    }
+    return p
+}
+
class Solution {
+public:
+    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
+        ListNode* p = headA;
+        ListNode* q = headB;
+        int flag = 4;
+        while(flag > 0){
+            if(p == q){
+                return p;
+            }
+            if (p->next != NULL){
+                p = p->next;
+            } else{
+                p = headB;
+                flag -= 1;
+            }
+            if(q->next != NULL){
+                q = q->next;
+            } else{
+                q = headA;
+                flag -= 1;
+            }
+        }
+        return NULL;
+    }
+};
+

142. 环形链表II

+

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

+

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

+

不允许修改 链表。

+
/**
+ * Definition for singly-linked list.
+ * type ListNode struct {
+ *     Val int
+ *     Next *ListNode
+ * }
+ */
+func detectCycle(head *ListNode) *ListNode {
+    slow, fast := head, head
+    for fast != nil && fast.Next != nil {
+        slow = slow.Next
+        fast = fast.Next.Next
+        if slow == fast {
+            for slow != head {
+                slow = slow.Next
+                head = head.Next
+            }
+            return head
+        }
+    }
+    return nil
+}
+
class Solution {
+public:
+    ListNode *detectCycle(ListNode *head) {
+        if (head == NULL || head->next == NULL){
+            return NULL;
+        }
+        ListNode* slow = head;
+        ListNode* fast = head;
+        do{
+            if (slow == NULL || fast == NULL || fast->next == NULL){
+                return NULL;
+            }
+            slow = slow->next;
+            fast = fast->next->next;
+        }while(slow != fast);
+        slow = head;
+        while(slow != fast){
+            slow = slow->next;
+            fast = fast->next;
+        }
+        return slow;
+    }
+};
+

链表总结

+

img

+

哈希表

+

哈希表理论基础

+

其实直白来讲其实数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素

+

一般哈希表都是用来快速判断一个元素是否出现集合里。

+

242. 有效的字母异位词

+

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

+

注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

+
func isAnagram(s string, t string) bool {
+    alphabet := make([]int, 26)
+	sBytes := []byte(s)
+	tBytes := []byte(t)
+    lens := len(sBytes)
+    lent := len(tBytes)
+    if lens != lent{
+        return false
+    }
+    for i:=0;i<lens;i++{
+        alphabet[s[i]-'a']++
+        alphabet[t[i]-'a']--
+    }
+    for i:=0;i<26;i++{
+        if alphabet[i] != 0{
+            return false
+        }
+    }
+    return true
+}
+
class Solution {
+public:
+    bool isAnagram(string s, string t) {
+        unordered_map<char, int> mp;
+        for(int i=0;i<s.length();i++){
+            mp[s[i]] += 1;
+        }
+        for(int i=0;i<t.length();i++){
+            mp[t[i]] -= 1;
+        }
+        for(auto it = mp.begin();it != mp.end();it++){
+            if(it->second != 0){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

49. 字母异位词分组

+

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

+

字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。

+
func groupAnagrams(strs []string) [][]string {
+    mp := map[string][]string{}
+    for _, str := range strs {
+        s := []byte(str)
+        sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
+        sortedStr := string(s)
+        mp[sortedStr] = append(mp[sortedStr], str)
+    }
+    ans := make([][]string, 0, len(mp))
+    for _, v := range mp {
+        ans = append(ans, v)
+    }
+    return ans
+}
+
class Solution {
+public:
+
+    vector<vector<string>> groupAnagrams(vector<string>& strs) {
+        vector<vector<string> > result;
+        unordered_map<string, vector<string> > mp;
+        int arraylen = strs.size();
+        for(int i=0;i<arraylen;i++){
+            string t = strs[i];
+            sort(t.begin(),t.end());
+            mp[t].push_back(strs[i]);
+        }
+        for(auto it = mp.begin(); it != mp.end(); it++){
+            result.push_back(it->second);
+        }
+        return result;
+    }
+};
+

438. 找到字符串中所有字母异位词

+

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

+

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

+
func findAnagrams(s string, p string) []int {
+    alphabet := make([]int,26)
+    result := []int{}
+    lens := len(s)
+    lenp := len(p)
+    if lenp > lens{
+        return result
+    }
+    for i:=0;i<lenp;i++{
+        alphabet[p[i]-'a']++
+        alphabet[s[i]-'a']--
+    }
+    sign := 0
+    for i:=lenp;i<lens;i++{
+        sign = 0
+        for j:=0;j<26;j++{
+            if alphabet[j] != 0{
+                sign = 1
+                break
+            }
+        }
+        if sign == 0{
+            result = append(result,i-lenp)
+        }
+        alphabet[s[i]-'a']--
+        alphabet[s[i-lenp]-'a']++
+    }
+    sign = 0
+    for j:=0;j<26;j++{
+        if alphabet[j] != 0{
+            sign = 1
+            break
+        }
+    }
+    if sign == 0{
+        result = append(result,lens-lenp)
+    }
+    return result
+}
+
class Solution {
+public:
+    bool judgemp(unordered_map<char, int> mp){
+        for(auto it = mp.begin();it != mp.end();it++){
+            if(it->second != 0){
+                return false;
+            }
+        }
+        return true;
+    }
+    vector<int> findAnagrams(string s, string p) {
+        vector<int> result;
+        unordered_map<char, int> mp;
+        for(int i=0;i<p.length();i++){
+            mp[p[i]] += 1;
+        }
+        int left = 0;
+        for(int right=0;right < s.length();right++){
+            mp[s[right]] -= 1;
+            if(right - left != p.length()-1){
+                continue;
+            }
+            if(judgemp(mp)){
+                result.push_back(left);
+            }
+            mp[s[left]] += 1;
+            left += 1;
+        }
+        return result;
+    }
+};
+

349. 两个数组的交集

+

给定两个数组 nums1nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

+
func intersection(nums1 []int, nums2 []int) []int {
+    result := []int{}
+    nums := make([]int,1001)
+    for _,i := range nums1{
+        nums[i] = 1
+    }
+    for _,i := range nums2{
+        if nums[i] == 1{
+            nums[i] = 2
+            result = append(result,i)
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
+        vector<int> result;
+        unordered_map<int, bool> mp1;
+        unordered_map<int, bool> mp2;
+        for(int i=0;i<nums1.size();i++){
+            mp1[nums1[i]] = true;
+        }
+        for(int i=0;i<nums2.size();i++){
+            mp2[nums2[i]] = true;
+        }
+        for(auto it = mp1.begin();it != mp1.end(); it++){
+            if(mp2.find(it->first) != mp2.end()){
+                result.push_back(it->first);
+            }
+        }
+        return result;
+    }
+};
+

350. 两个数组的交集 II

+

给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。

+
func intersect(nums1 []int, nums2 []int) []int {
+    result := []int{}
+    nums11 := make([]int,1001)
+    nums22 := make([]int,1001)
+    for _,i := range nums1{
+        nums11[i]++
+    }
+    for _,i := range nums2{
+        nums22[i]++
+    }
+    for i,j := range nums11{
+        a := min(j,nums22[i])
+        for k:= 0;k<a;k++{
+            result = append(result,i)
+        }
+    }
+    return result
+}
+
+func min ( a int, b int) int {
+    if a < b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
+        vector<int> result;
+        unordered_map<int, int> mp1;
+        unordered_map<int, int> mp2;
+        for(int i=0;i<nums1.size();i++){
+            mp1[nums1[i]] += 1;
+        }
+        for(int i=0;i<nums2.size();i++){
+            mp2[nums2[i]] += 1;
+        }
+        for(auto it = mp1.begin();it != mp1.end(); it++){
+            if(mp2.find(it->first) != mp2.end()){
+                for(int i=0;i<min(mp2[it->first],it->second);i++){
+                    result.push_back(it->first);
+                }
+            }
+        }
+        return result;
+    }
+};
+

202. 快乐数

+

编写一个算法来判断一个数 n 是不是快乐数。

+

「快乐数」 定义为:

+

对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
+然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
+如果这个过程 结果为 1,那么这个数就是快乐数。
+如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

+
func isHappy(n int) bool {
+    for i:=0;i<6;i++{
+        result := 0
+        for n != 0{
+            result = result + (n%10) * (n%10)
+            n /= 10
+        }
+        if result == 1{
+            return true
+        }
+        n = result
+    }
+    return false
+}
+
class Solution {
+public:
+    bool isHappy(int n) {
+        unordered_map<int,bool> mp;
+        while(1){
+            if(mp.find(n) != mp.end()){
+                return false;
+            }
+            if(n == 1){
+                return true;
+            }
+            mp[n] = true;
+            int res = 0;
+            while(n != 0){
+                res += (n%10) * (n%10);
+                n = n/10;
+            }
+            n = res;
+        }
+        return true;
+    }
+};
+

1. 两数之和

+

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

+

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

+

你可以按任意顺序返回答案。

+
func twoSum(nums []int, target int) []int {
+    result := make([]int,2)
+    mapnum := make(map[int]int)
+    for i,n := range nums{
+        if j, ok := mapnum[target-n]; ok {
+            result[0] = j
+            result[1] = i
+            return result
+        } else{
+            mapnum[n] = i
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<int> twoSum(vector<int>& nums, int target) {
+        vector<int> result(2);
+        unordered_map<int, int> mp;
+        for(int i=0;i<nums.size();i++){
+            if(mp.find(target-nums[i]) != mp.end()){
+                result[0] = i;
+                result[1] = mp[target-nums[i]];
+            } else{
+                mp[nums[i]] = i;
+            }
+        }
+        return result;
+    }
+};
+

454. 四数相加II

+

给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

+

0 <= i, j, k, l < n,nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

+
func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int {
+    result := 0
+    numsmap := make(map[int]int)
+    for _,i := range nums1{
+        for _,j := range nums2{
+            numsmap[i+j]++
+        }
+    }
+    for _,i := range nums3{
+        for _,j := range nums4{
+            k,ok := numsmap[0-(i+j)]
+            if ok{
+                result += k
+            }
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
+        unordered_map <int, int> mp;
+        for(int i=0;i<nums1.size();i++){
+            for(int j=0;j<nums2.size();j++){
+                mp[nums1[i] + nums2[j]] += 1;
+            }
+        }
+        int count_result = 0;
+        for(int i=0;i<nums3.size();i++){
+            for(int j=0;j<nums4.size();j++){
+                if(mp.find(0-nums3[i]-nums4[j]) != mp.end()){
+                    count_result += mp[0-nums3[i]-nums4[j]];
+                }
+            }
+        }
+        return count_result;
+    }
+};
+

383. 赎金信

+

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

+

如果可以,返回 true ;否则返回 false 。

+

magazine 中的每个字符只能在 ransomNote 中使用一次。

+
func canConstruct(ransomNote string, magazine string) bool {
+    alphabet := make([]int,26)
+    for i:=0;i<len(magazine);i++{
+        alphabet[magazine[i]-'a']++
+    }
+    for i:=0;i<len(ransomNote);i++{
+        alphabet[ransomNote[i]-'a']--
+        if alphabet[ransomNote[i]-'a'] < 0{
+            return false
+        }
+    }
+    return true
+}
+
class Solution {
+public:
+    bool canConstruct(string ransomNote, string magazine) {
+        unordered_map<char,int> mp;
+        for(int i=0;i<ransomNote.size();i++){
+            mp[ransomNote[i]] += 1;
+        }
+        for(int i=0;i<magazine.size();i++){
+            mp[magazine[i]] -= 1;
+        }
+        for(auto it = mp.begin(); it != mp.end();it++){
+            if(it->second > 0){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

字符串

+

344. 反转字符串

+

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

+

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

+
func reverseString(s []byte)  {
+    for i, j := 0, len(s)-1; i < j; {
+		s[i], s[j] = s[j], s[i]
+		i++
+		j--
+	}
+}
+
class Solution {
+public:
+    void reverseString(vector<char>& s) {
+        int slen = s.size();
+        for(int left=0;left < slen/2;left++){
+            char t = s[left];
+            s[left] = s[slen-left-1];
+            s[slen-left-1] = t;
+        }
+
+    }
+};
+

541. 反转字符串II

+

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。

+

如果剩余字符少于 k 个,则将剩余字符全部反转。

+

如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

+
func reverseStr(s string, k int) string {
+    sbytes := []byte(s)
+    lens := len(s)
+    sign := 0
+    left := 1
+    right := len(s)
+    for i:=1;i<=lens;i++{
+        if i % k == 0{
+            if sign == 0{
+                sign = 1
+                right = i
+            } else{
+                sign = 0
+                x := left
+                y := right
+                for x < y{
+                    sbytes[x-1],sbytes[y-1] = sbytes[y-1],sbytes[x-1]
+                    x++
+                    y--
+                }
+                left = i+1
+            }
+        }
+    }
+    if sign == 0{
+        right = len(s)
+    }
+    x := left
+    y := right
+    for x < y{
+        sbytes[x-1],sbytes[y-1] = sbytes[y-1],sbytes[x-1]
+        x++
+        y--
+    }
+    return string(sbytes)
+}
+
class Solution {
+public:
+    string reversesmallstr(string s){
+        reverse(s.begin(), s.end()); 
+        return s;
+    }
+    string reverseStr(string s, int k) {
+        string result_str = "";
+        while(s.size() >= 2 * k){
+            result_str += reversesmallstr(s.substr(0,k));
+            s.erase(0,k);
+            result_str += s.substr(0,k);
+            s.erase(0,k);
+        }
+        if (s.size() >= k){
+            result_str += reversesmallstr(s.substr(0,k));
+            s.erase(0,k);
+            result_str += s;
+        } else{
+            result_str += reversesmallstr(s);
+        }
+        return result_str;
+    }
+};
+

剑指Offer 05. 替换空格

+

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

+
func replaceSpace(s string) string {
+    var newS []byte
+    for i := range s {
+        if s[i] == ' ' {
+            newS = append(newS, '%', '2', '0')
+        } else {
+            newS = append(newS, s[i]) 
+        }
+    }
+    return string(newS)
+}
+

151.翻转字符串里的单词

+

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

+

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

+

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

+

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

+
func reverseWords(s string) string {
+    var result string
+    var store []string
+    sbytes := []byte(s)
+    sign := 1
+    point := 0
+    for i:=0;i<len(s);i++{
+        if sbytes[i] == ' '{
+            if sign == 0{
+                store = append(store,s[point:i])
+                sign = 1
+            }
+        } else{
+            if sign == 1{
+                point = i
+                sign = 0
+            }
+        }
+    }
+    if sign == 0{
+        store = append(store,s[point:len(s)])
+    }
+    for i:=len(store)-1;i>=0;i--{
+        result += store[i]
+        if i != 0{
+            result += " "
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    string reverse_word(string s){
+        reverse(s.begin(), s.end());
+        return s;
+    }
+    string reverseWords(string s) {
+        string result_str = "";
+        int left = 0;
+        while(left < s.size()){
+            while(left < s.size() && s[left] == ' '){
+                left += 1;
+            }
+            int right = left;
+            while(right < s.size() && s[right] != ' '){
+                right += 1;
+            }
+            if(left == right){
+                break;
+            }
+            result_str = result_str + reverse_word(s.substr(left,right-left)) + " ";
+            left = right;
+        }
+        string result_str2 = result_str.substr(0,result_str.size()-1);
+
+        return reverse_word(result_str2);
+    }
+};
+

796. 旋转字符串

+

给定两个字符串, s 和 goal。如果在若干次旋转操作之后,s 能变成 goal ,那么返回 true 。

+

s 的 旋转操作 就是将 s 最左边的字符移动到最右边。

+

例如, 若 s = ‘abcde’,在旋转一次之后结果就是’bcdea’ 。

+
class Solution {
+public:
+    bool rotateString(string s, string goal) {
+        if (s.size() != goal.size()){
+            return false;
+        }
+        for(int left = 0;left < s.size();left++){
+            string comp = s.substr(1,s.size()-1) + s.substr(0,1);
+            s = comp;
+            if(s == goal){
+                return true;
+            }
+        }
+        return false;
+    }
+};
+

剑指Offer58-II. 左旋转字符串

+

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

+
func reverseLeftWords(s string, n int) string {
+    return s[n:]+s[:n]
+}
+

28. 实现 strStr()

+

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 。

+
func strStr(haystack string, needle string) int {
+    needlelen := len(needle)
+    haystacklen := len(haystack)
+    next := make([]int,needlelen)
+    next[0] = 0
+    j := 0
+    for i:=1;i<needlelen;i++{
+        for j > 0 && needle[i] != needle[j]{
+            j = next[j-1]
+        }
+        if needle[i] == needle[j]{
+            j++
+        }
+        next[i] = j
+    }
+    j = 0
+    for i:=0;i<haystacklen;i++{
+        for j > 0 && needle[j] != haystack[i]{
+            j = next[j-1]
+        }
+        if needle[j] == haystack[i]{
+            j++
+        }
+        if j == needlelen{
+            return i-j+1;
+        }
+    }
+    return -1
+}
+

459.重复的子字符串

+

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

+
func repeatedSubstringPattern(s string) bool {
+    lens := len(s)
+    next := make([]int,lens)
+    next[0] = 0
+    j := 0
+    for i:=1;i<lens;i++{
+        for j > 0 && s[i] != s[j]{
+            j = next[j-1]
+        }
+        if s[i] == s[j]{
+            j++
+        }
+        next[i] = j
+    }
+    if next[lens-1] != 0 && lens % (lens-next[lens-1]) == 0{
+        return true
+    }
+    return false
+}
+
class Solution {
+public:
+    bool repeatedSubstringPattern(string s) {
+        int slen = s.size();
+        if (slen == 1){
+            return false;
+        }
+        for(int i=1;i<slen / 2+1;i++){
+            if(slen % i != 0){
+                continue;
+            }
+            string t = s.substr(0,i);
+            bool flag = true;
+            for(int index=i;index < slen-i+1;index += i){
+                if(s.substr(index,i) != t){
+                    flag = false;
+                    break;
+                }
+            }
+            if(flag){
+                return true;
+            }
+        }
+        return false;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
代码随想录-基本数据结构专题
+
https://zhangzhao219.github.io/2024/03/06/Leetcode/programmercarl/programmercarl-ds/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月6日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/07/Leetcode/programmercarl/programmercarl-dp/index.html b/2024/03/07/Leetcode/programmercarl/programmercarl-dp/index.html new file mode 100644 index 000000000..f80184424 --- /dev/null +++ b/2024/03/07/Leetcode/programmercarl/programmercarl-dp/index.html @@ -0,0 +1,2997 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-动态规划专题 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-动态规划专题

+ + +
+ +

代码随想录-动态规划专题

+ +

动态规划专题

+

x3odjs.png

+

基础知识

+

动态规划中每一个状态一定是由上一个状态推导出来的, 这一点就区分于贪心 ,贪心没有状态推导,而是从局部直接选最优的

+

解决动态规划问题的五步曲:

+
    +
  1. 确定dp数组(dp table)以及下标的含义
  2. +
  3. 确定递推公式
  4. +
  5. dp数组如何初始化
  6. +
  7. 确定遍历顺序
  8. +
  9. 举例推导dp数组
  10. +
+

做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果

+

然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。

+

如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。

+

如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。

+

基础题目

+

509. 斐波那契数

+

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

+
F(0) = 0F(1) = 1
+F(n) = F(n - 1) + F(n - 2),其中 n > 1
+

给定 n ,请计算 F(n)

+
    +
  • 确定dp数组以及下标的含义:dp数组表示当前求得的数字,下标表示当前是第几个数字(dp[i]的定义为:第i个数的斐波那契数值是dp[i])
  • +
  • 确定递推公式:dp[i]=dp[i-1]+dp[i-2]
  • +
  • dp数组如何初始化:dp[0]=0, dp[1]=1
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:1, 1, 2, 3, 5, 8,…
  • +
+

因为每一个数字的求出只依赖于之前的两个数字,因此不需要存储整个数组,只需要存储前两个数字即可。

+
func fib(n int) int {
+	a := 0
+	b := 1
+	if n == 0 {
+		return a
+	} else if n == 1 {
+		return b
+	}
+	var c int
+	for i := 2; i <= n; i++ {
+		c = a + b
+		if i == n {
+			break
+		}
+		a, b = b, c
+	}
+	return c
+}
+
class Solution {
+public:
+    int fib(int n) {
+        if(n == 0 || n == 1){
+            return n;
+        }
+        vector<int> dp(n+1);
+        dp[0] = 0;
+        dp[1] = 1;
+        for(int i=2;i<=n;i++){
+            dp[i] = dp[i-1] + dp[i-2];
+        }
+        return dp[n];
+    }
+};
+

70. 爬楼梯

+

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

+

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

+
    +
  • 确定dp数组以及下标的含义:dp[i]的定义为:爬到第i个台阶的方法数目
  • +
  • 确定递推公式:爬到第i个台阶,可以是爬到第i-1个台阶后跨一步,也可以是爬到第i-2个台阶后跨两步,因此 dp[i]=dp[i-1]+dp[i-2]
  • +
  • dp数组如何初始化:dp[1]=1, dp[2]=2(注意 dp[0]是没有意义的,不要强行赋值进去)
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:1, 2, 3,…
  • +
+
func climbStairs(n int) int {
+    dp := make([]int, n+1)
+    dp[0] = 1
+    dp[1] = 1
+    for i:=2;i<=n;i++{
+        dp[i]=dp[i-1]+dp[i-2]
+    }
+    return dp[n]
+}
+
class Solution {
+public:
+    int climbStairs(int n) {
+        vector<int> dp(n+1);
+        dp[0] = 1;
+        dp[1] = 1;
+        for(int i=2;i<=n;i++){
+            dp[i] = dp[i-1] + dp[i-2];
+        }
+        return dp[n];
+    }
+};
+

746. 使用最小花费爬楼梯

+

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

+

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

+

请你计算并返回达到楼梯顶部的最低花费。

+
    +
  • 确定dp数组以及下标的含义:dp[i]的定义为:到达第i个台阶的最低花费
  • +
  • 确定递推公式:到达第i个台阶的最低花费,可以是到达第i-1个台阶后支付第i-1台阶后的花费,也可以是到达第i-2个台阶后支付第i-2台阶后的花费,两者需要进行比较取最小值,因此 dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]),这里认为第一步是不要花钱的,最后一步需要花钱。
  • +
  • dp数组如何初始化:dp[0]=0, dp[1]=0(因为第一步都不要钱)
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组
  • +
+
func minCostClimbingStairs(cost []int) int {
+    n := len(cost)
+    dp := make([]int,n+1)
+    dp[0],dp[1] = 0,0
+    for i:= 2;i<=n;i++{
+        dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
+    }
+    return dp[n]
+}
+
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
class Solution {
+public:
+    int minCostClimbingStairs(vector<int>& cost) {
+        int n = cost.size();
+        vector<int> dp(n+2);
+        dp[1] = 0;
+        dp[2] = 0;
+        for(int i=3;i<=n+1;i++){
+            dp[i] = min(dp[i-1] + cost[i-2], dp[i-2] + cost[i-3]);
+        }
+        return dp[n+1];
+    }
+};
+

62. 不同路径

+

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

+

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

+

问总共有多少条不同的路径?

+
    +
  • 确定dp数组以及下标的含义:dp[i][j]的定义为:到达第(i, j)格子的不同路径数量
  • +
  • 确定递推公式:到达第(i, j)格子的不同路径数量,是到达第(i-1, j)格子和到达第(i, j-1)格子的数量之和,因此 dp[i][j]=dp[i-1][j]+dp[i][j-1]
  • +
  • dp数组如何初始化:到达第一行和第一列的不同路径数量都为1
  • +
  • 确定遍历顺序:从上到下,从左到右遍历
  • +
  • 举例推导dp数组
  • +
+
func uniquePaths(m int, n int) int {
+    dp := make([][]int, m+1)
+	for i := 0; i < m+1; i++ {
+		dp[i] = make([]int, n+1)
+	}
+    for i:=1;i<m+1;i++{
+        for j:=1;j<n+1;j++{
+            if i == 1 || j == 1{
+                dp[i][j] = 1
+            } else{
+                dp[i][j] = dp[i-1][j] + dp[i][j-1]
+            }
+        }
+    }
+    return dp[m][n]
+}
+
class Solution {
+public:
+    int uniquePaths(int m, int n) {
+        vector<vector<int> > dp(m, vector<int>(n));
+        for(int i=0;i<m;i++){
+            dp[i][0] = 1;
+        }
+        for(int j=0;j<n;j++){
+            dp[0][j] = 1;
+        }
+        for(int i=1;i<m;i++){
+            for(int j=1;j<n;j++){
+                dp[i][j] = dp[i-1][j] + dp[i][j-1];
+            }
+        }
+        return dp[m-1][n-1];
+    }
+};
+

事实上使用一个一维数组就可以实现了,可以对代码的空间复杂度进行优化:

+
func uniquePaths(m int, n int) int {
+    dp := make([]int, n+1)
+	for i := 0; i < n+1; i++ {
+		dp[i] = 1
+	}
+    for i:=2;i<m+1;i++{
+        for j:=2;j<n+1;j++{
+            dp[j] += dp[j-1]
+        }
+    }
+    return dp[n]
+}
+

63. 不同路径Ⅱ

+

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

+

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

+

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

+

网格中的障碍物和空位置分别用 1 和 0 来表示。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j]的定义为:到达第(i, j)格子的不同路径数量
  • +
  • 确定递推公式:到达第(i, j)格子的不同路径数量,是到达第(i-1, j)格子和到达第(i, j-1)格子的数量之和,因此 dp[i][j]=dp[i-1][j]+dp[i][j-1]但是,若该格子是障碍物,到达这个格子的路径数量为0,因此直接置 dp[i][j]=0
  • +
  • dp数组如何初始化:初始化时候要考虑是否是障碍物,如果是障碍物,那么后面的格子肯定也是无法到达的,不能仅仅置为1
  • +
  • 确定遍历顺序:从上到下,从左到右遍历
  • +
  • 举例推导dp数组
  • +
+
class Solution {
+public:
+    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
+        int m = obstacleGrid.size();
+        int n = obstacleGrid[0].size();
+        vector<vector<int> > dp(m+1, vector<int>(n+1));
+        if(obstacleGrid[0][0] == 1){
+            return 0;
+        }
+        for(int i=0;i<m;i++){
+            dp[i][0] = 0;
+        }
+        for(int j=0;j<n;j++){
+            dp[0][j] = 0;
+        }
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(i == 1 && j == 1){
+                    dp[i][j] = 1;
+                    continue;
+                }
+                if(obstacleGrid[i-1][j-1] == 1){
+                    dp[i][j] = 0;
+                } else{
+                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

直接给出状态压缩后的写法:

+
func uniquePathsWithObstacles(obstacleGrid [][]int) int {
+    m := len(obstacleGrid)
+    n := len(obstacleGrid[0])
+    dp := make([]int, n)
+    var sign int = 0
+    for j:=0;j<n;j++{
+        if obstacleGrid[0][j] == 1{
+            sign = 1
+            dp[j] = 0
+        } else if sign == 1{
+            dp[j] = 0
+        } else{
+            dp[j] = 1
+        }
+    }
+    for i:=1;i<m;i++{
+        for j:=0;j<n;j++{
+            if obstacleGrid[i][j] == 1{
+                dp[j] = 0
+            } else{
+                if j != 0{
+                    dp[j] += dp[j-1]
+                }
+            }
+        }
+    }
+    return dp[n-1]
+}
+

343. 整数拆分

+

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

+

返回 你可以获得的最大乘积

+
    +
  • 确定dp数组以及下标的含义:分拆数字i,可以得到的最大乘积为dp[i]
  • +
  • 确定递推公式:从1遍历比i更小的数字j,有两种情况:①如果直接用这个数字,就是j*(i-j),②如果将j继续拆分,就是j*dp[i-j]。因此 dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
  • +
  • dp数组如何初始化:dp[0]dp[1]都没有意义,dp[2]=1
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组
  • +
+
func integerBreak(n int) int {
+    dp := make([]int,n+1)
+    dp[2] = 1
+    for i:=3;i<=n;i++{
+        for j:=1;j<i-1;j++{
+            dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
+        }
+    }
+    return dp[n]
+}
+func max(a,b int) int {
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int integerBreak(int n) {
+        vector<int> dp(n+1);
+        dp[1] = 1;
+        for(int i=2;i<=n;i++){
+            for(int j=i-1;j>=1;j--){
+                dp[i] = max(dp[i],max(j * dp[i-j], j*(i-j)));
+            }
+        }
+        return dp[n];
+    }
+};
+

96.不同的二叉搜索树

+

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

+
    +
  • 确定dp数组以及下标的含义:dp[i]表示有i个结点的互不相同的二叉搜索树的种类数
  • +
  • 确定递推公式:二叉搜索树的性质决定了根结点的值一定比左边结点的值大,一定比右边的值小,因此可以通过根结点将二叉搜索树分为左右的两个部分。而不管是左边的部分还是右边的部分,结点的值一定是连续的,就是在找结点数目更少的二叉搜索树的数量。因此分为左右两个部分分别寻找,然后相乘到一起即可。递推公式为:dp[i] = dp[i] + dp[j-1] * dp[i-j],j是遍历时候的标记。
  • +
  • dp数组如何初始化:dp[0]=1,dp[1]=1,感觉 dp[0]也是很难解释。。。
  • +
  • 确定遍历顺序:从前到后
  • +
  • 举例推导dp数组:推导后才发现之前的简单想法是错误的
  • +
+
func numTrees(n int) int {
+    dp := make([]int,n+1)
+    dp[0] = 1
+    dp[1] = 1
+    for i:=2;i<=n;i++{
+        for j:=1;j<=i;j++{
+            dp[i] = dp[i] + dp[j-1] * dp[i-j]
+        }
+    }
+    return dp[n]
+}
+
class Solution {
+public:
+    int numTrees(int n) {
+        vector<int> dp(n+1);
+        dp[0] = 1;
+        dp[1] = 1;
+        for(int i=2;i<=n;i++){
+            for(int j=i;j>=1;j--){
+                dp[i] = dp[i] + dp[j-1] * dp[i-j];
+            }
+        }
+        return dp[n];
+    }
+};
+

背包问题

+

01 背包解题方法

+

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。 每件物品只能用一次 ,求解将哪些物品装入背包里物品价值总和最大。

+
    +
  1. 确定dp数组以及下标的含义:对于背包问题,有一种写法是使用二维数组,即 dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
  2. +
  3. 确定递推公式
  4. +
+

再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

+

那么可以有两个方向推出来dp[i][j],

+
    +
  • 不放物品i :由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
  • +
  • 放物品i :由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
  • +
+

所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

+
    +
  1. dp数组如何初始化
  2. +
+

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

+

首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

+

x8hWVK.md.png

+

状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

+

dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

+

那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

+

当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量足够放编号0物品。

+
    +
  1. 确定遍历顺序
  2. +
+

要理解递归的本质和递推的方向

+

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。

+

dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向)

+

先遍历物品再遍历背包这个顺序更好理解,但是怎么遍历都可以

+

一维dp数组(滚动数组)

+

对于背包问题其实状态都是可以压缩的。

+

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

+

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

+

与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了 ,只用dp[j](一维数组,也可以理解是一个滚动数组)。

+

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

+
    +
  1. 确定dp数组的定义
  2. +
+

在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值最大可以为dp[j]。

+
    +
  1. 一维dp数组的递推公式
  2. +
+

dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢?

+

dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

+

dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

+

此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,

+

所以递归公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

+

可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。

+
    +
  1. 一维dp数组如何初始化
  2. +
+

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

+

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

+

那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?

+

看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

+

dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。

+

这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了

+

那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。

+
    +
  1. 一维dp数组遍历顺序
  2. +
+

和二维dp的写法中,遍历背包的顺序是不一样的!

+

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

+

倒序遍历是为了保证物品i只被放入一次! 但如果一旦正序遍历了,那么物品0就会被重复加入多次!

+

从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

+

为什么二维dp数组历的时候不用倒序呢? 因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

+

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

+

不可以!因为一维dp的写法,背包容量一定要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

+

416. 分割等和子集

+

给你一个 只包含正整数非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示是否能凑出总和为j的数值
  • +
  • 确定递推公式:判断当前的数值是否能凑出来,首先如果目前已有的数值都能凑出来,那么加上这个数字肯定也能凑出来。其次如果减掉这个数值的前一步能凑出来,那么这个更大的数值也能凑出来。因此 dp[j] = dp[j] || dp[j-nums[i]]
  • +
  • dp数组如何初始化:dp[0]=true
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从后到前进行填充,避免填充多个数字
  • +
  • 举例推导dp数组:
  • +
+
func canPartition(nums []int) bool {
+    var sum int = 0
+    for i:=0;i<len(nums);i++{
+        sum += nums[i]
+    }
+    if sum % 2 == 1{
+        return false
+    }
+    sum /= 2
+    dp := make([]bool,sum+1)
+    dp[0] = true
+    for i:=0;i<len(nums);i++{
+        for j:=sum;j>=nums[i];j--{
+            dp[j] = dp[j] || dp[j-nums[i]]
+        }
+        if dp[sum] == true{
+            return true
+        }
+    }
+    return dp[sum]
+}
+
class Solution {
+public:
+    bool canPartition(vector<int>& nums) {
+        int numsum = 0;
+        for(int i=0;i<nums.size();i++){
+            numsum += nums[i];
+        }
+        if(numsum % 2 != 0){
+            return false;
+        }
+        int target = numsum / 2;
+        vector<vector<int> > dp(nums.size(), vector<int>(target+1,0));
+        for(int i=0;i<=target;i++){
+            if(i >= nums[0]){
+                dp[0][i] = nums[0];
+            }
+        }
+        for(int i=1;i<nums.size();i++){
+            for(int j=0;j<=target;j++){
+                if(j < nums[i]){
+                    dp[i][j] = dp[i-1][j];
+                }
+                else{
+                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]] + nums[i]);
+                }
+            }
+        }
+        return dp[nums.size()-1][target] == target;
+    }
+};
+

1049. 最后一块石头的重量 II

+

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

+

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

+

如果 x == y,那么两块石头都会被完全粉碎;

+

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

+

最后,最多只会剩下一块石头。返回此石头最小的可能重量 。如果没有石头剩下,就返回 0。

+

分析:将石头分成重量大致相同的两堆,也就是看在不超过sum/2的情况下背包内最多能装多少石头,然后计算差值即可

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示是否能凑出总和为j的重量的石头
  • +
  • 确定递推公式:判断当前的数值是否能凑出来,首先如果目前已有的数值都能凑出来,那么加上这个数字肯定也能凑出来。其次如果减掉这个数值的前一步能凑出来,那么这个更大的数值也能凑出来。因此 dp[j] = dp[j] || dp[j-nums[i]]
  • +
  • dp数组如何初始化:dp[0]=true
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从后到前进行填充,避免填充多个数字
  • +
  • 举例推导dp数组:
  • +
+
func lastStoneWeightII(stones []int) int {
+    var sum1,sum int = 0,0
+    for i:=0;i<len(stones);i++{
+        sum1 += stones[i]
+    }
+    if sum1 % 2 == 1{
+        sum = (sum1 -1) / 2
+    } else{
+        sum = sum1 / 2
+    }
+    dp := make([]bool,sum+1)
+    dp[0] = true
+    for i:=0;i<len(stones);i++{
+        for j:=sum;j>=stones[i];j--{
+            dp[j] = dp[j] || dp[j-stones[i]]
+        }
+    }
+    var result int = 0
+    for j:=sum;j>=0;j--{
+        if dp[j] == true{
+            result = j
+            break
+        }
+    }
+    return sum1 - result * 2
+}
+
class Solution {
+public:
+    int lastStoneWeightII(vector<int>& stones) {
+        int numsum = 0;
+        for(int i=0;i<stones.size();i++){
+            numsum += stones[i];
+        }
+        int target = numsum;
+        numsum /= 2;
+        vector<vector<int> > dp(stones.size(),vector<int>(numsum+1, 0));
+        for(int i=0;i<=numsum;i++){
+            if(i >= stones[0]){
+                dp[0][i] = stones[0];
+            }
+        }
+        for(int i=1;i<stones.size();i++){
+            for(int j=0;j<=numsum;j++){
+                if(j < stones[i]){
+                    dp[i][j] = dp[i-1][j];
+                } else{
+                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-stones[i]] + stones[i]);
+                }
+            }
+        }
+        return abs(target-2*dp[stones.size()-1][numsum]);
+    }
+};
+

494. 目标和

+

给你一个整数数组 nums 和一个整数 target 。

+

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

+

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。

+

返回可以通过上述方法构造的、运算结果等于 target 的不同表达式的数目。

+

分析:假设加法的总和为x,那么减法对应的总和就是sum - x。所以我们要求的是 x - (sum - x) = S,x = (S + sum) / 2,此时问题就转化为,装满容量为x背包,有几种方法

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示装满容量为j的背包的方法数量
  • +
  • 确定递推公式:如果不选数字i,则方法数量为dp[j],如果选择数字i,则方法数量为dp[j-nums[i]],因此总共的方法数量应该是dp[j]+dp[j-nums[i]],因此递推公式为 dp[j] = dp[j]+dp[j-nums[i]]
  • +
  • dp数组如何初始化:dp[0]=1,装0件物品对应的方法数量为1,就是什么都不装
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从后到前进行填充,避免填充多个数字
  • +
  • 举例推导dp数组:
  • +
+
func findTargetSumWays(nums []int, target int) int {
+    var sum int = 0
+    for i:=0;i<len(nums);i++{
+        sum += nums[i]
+    }
+    x := target + sum
+    if x % 2 == 1 || x < 0{
+        return 0
+    }
+    x /= 2
+    dp := make([]int,x+1)
+    dp[0] = 1
+    for i:=0;i<len(nums);i++{
+        for j:=x;j>=nums[i];j--{
+            dp[j] += dp[j-nums[i]]
+        }
+    }
+    return dp[x]
+}
+
class Solution {
+public:
+    int findTargetSumWays(vector<int>& nums, int target) {
+        int sum = 0;
+        for (int i = 0; i < nums.size(); i++) sum += nums[i];
+        if (abs(target) > sum) return 0; // 此时没有方案
+        if ((target + sum) % 2 == 1) return 0; // 此时没有方案
+        int bagSize = (target + sum) / 2;
+        vector<int> dp(bagSize + 1, 0);
+        dp[0] = 1;
+        for (int i = 0; i < nums.size(); i++) {
+            for (int j = bagSize; j >= nums[i]; j--) {
+                dp[j] += dp[j - nums[i]];
+            }
+        }
+        return dp[bagSize];
+    }
+};
+

474.一和零

+

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

+

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

+

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

+

分析:相当于一个三维的0-1背包问题,可以通过压缩的方式压缩成二维背包,且需要对数据进行预处理

+
    +
  • 确定dp数组以及下标的含义:dp[j][k]表示最多有j个0和k个1的最大子集长度
  • +
  • 确定递推公式:dp[j][k] = max(dp[j][k],dp[j-strs[i]][k-strs[i]]+1)
  • +
  • dp数组如何初始化:dp[0][0]=0
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从后到前进行填充,避免填充多个数字
  • +
  • 举例推导dp数组:
  • +
+
func findMaxForm(strs []string, m int, n int) int {
+    dp := make([][]int, m+1)
+	for i := 0; i < m+1; i++ {
+		dp[i] = make([]int, n+1)
+	}
+    for _,str := range strs{
+        zero := strings.Count(str, "0")
+		one := len(str) - zero
+        for j:=m;j>=zero;j--{
+            for k:=n;k>=one;k--{
+                dp[j][k] = max(dp[j][k],dp[j-zero][k-one]+1)
+            }
+        }
+    }
+    return dp[m][n]
+}
+func max (a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int findMaxForm(vector<string>& strs, int m, int n) {
+        vector<vector<int> > dp(m+1, vector<int>(n+1,0));
+        for(int i=0;i<strs.size();i++){
+            string s = strs[i];
+            int zeronum = 0;
+            int onenum = 0;
+            for(int j=0;j<s.size();j++){
+                if(s[j] == '0'){
+                    zeronum += 1;
+                } else{
+                    onenum += 1;
+                }
+            }
+            for(int x=m;x >= zeronum;x--){
+                for(int y=n;y>=onenum;y--){
+                    dp[x][y] = max(dp[x][y],dp[x-zeronum][y-onenum]+1);
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

完全背包解题方法

+

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。 每件物品都有无限个(也就是可以放入背包多次) ,求解将哪些物品装入背包里物品价值总和最大。

+

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

+

01背包和完全背包唯一不同就是体现在遍历顺序上

+

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

+

而完全背包的物品是可以添加多次的,所以要从小到大去遍历

+

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序同样无所谓!

+

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

+

518. 零钱兑换 II

+

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

+

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

+

假设每一种面额的硬币有无限个。

+

题目数据保证结果符合 32 位带符号整数。

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示凑成金额j的组合数量
  • +
  • 确定递推公式:dp[j]有两种情况,第一种情况是加上这个硬币后无法凑出新的种类,因此维持不变。第二种情况是加上新的硬币后能凑出新的种类了,因此dp[j]=dp[j]+dp[j-coins[i]],因此递推公式为 dp[j]=dp[j]+dp[j-coins[i]]
  • +
  • dp数组如何初始化:dp[0]=1,凑成金额0的组合数量为1,不影响后面的递推,解释起来就是凑成金额0的组合数量只有一种,就是什么都不凑
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从前到后进行填充,从而允许多个选择,符合完全背包的条件。外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况是找组合数,而反向实际上是排列数,因此遍历的顺序不能反
  • +
  • 举例推导dp数组:
  • +
+
class Solution {
+public:
+    int change(int amount, vector<int>& coins) {
+        int n = coins.size();
+        vector<int> dp(amount+1,0);
+        dp[0] = 1;
+        for(int i=0;i<n;i++){
+            for(int j=coins[i];j<=amount;j++){
+                dp[j] += dp[j-coins[i]];
+            }
+        }
+        return dp[amount];
+    }
+};
+

377. 组合总和 Ⅳ

+

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

+

题目数据保证答案符合 32 位整数范围。

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示凑成j的排列数量
  • +
  • 确定递推公式:dp[j]有两种情况,第一种情况是加上这个数字后无法凑出新的排列,因此维持不变。第二种情况是加上新的数字后能凑出新的排列了,因此dp[j]=dp[j]+dp[j-nums[i]],因此递推公式为 dp[j]=dp[j]+dp[j-nums[i]]
  • +
  • dp数组如何初始化:dp[0]=1,凑成整数0的组合数量为1,不影响后面的递推
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从前到后进行填充,从而允许多个选择,符合完全背包的条件。外层for循环背包(目标整数),内层for遍历物品(数组)的情况是找排列数,而反向实际上是组合数,因此遍历的顺序不能反
  • +
  • 举例推导dp数组:
  • +
+
func combinationSum4(nums []int, target int) int {
+    coinlen := len(nums)
+    dp := make([]int,target+1)
+    dp[0] = 1
+    for j:=0;j<=target;j++{
+        for i:=0;i<coinlen;i++{
+            if j-nums[i] >= 0{
+                dp[j] += dp[j-nums[i]]
+            }
+        }
+    }
+    return dp[target]
+}
+
class Solution {
+public:
+    int combinationSum4(vector<int>& nums, int target) {
+        int n = nums.size();
+        vector<int> dp(target+1,0);
+        dp[0] = 1;
+        for(int i=0;i<=target;i++){
+            for(int j=0;j<n;j++){
+                if(i - nums[j] >= 0  && dp[i] < INT_MAX - dp[i - nums[j]] ){
+                    dp[i] = dp[i] + dp[i - nums[j]];
+                }
+            }
+        }
+        return dp[target];
+    }
+};
+

70. 爬楼梯

+

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

+

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

+

改为:一步一个台阶,两个台阶,三个台阶,…,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?

+

实际上就是一个完全背包问题

+

322. 零钱兑换

+

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

+

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

+

你可以认为每种硬币的数量是无限的。

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示能凑成j的最少的硬币个数
  • +
  • 确定递推公式:dp[j]有两种情况,第一种情况是加上这种硬币后无法凑出新的组合,因此维持不变。第二种情况是加上新的数字后能凑出新的组合了,因此dp[j]=dp[j-nums[i]]+1,要取两者的最小值,因此递推公式为 dp[j]=min(dp[j],dp[j-nums[i]]+1)
  • +
  • dp数组如何初始化:dp[0]=0,凑成整数0的最少硬币个数为0,不影响后面的递推。同时要将整个dp数组初始化一个比较大的数值,从而不影响min的判断
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从前到后进行填充,从而允许多个选择,符合完全背包的条件。
  • +
  • 举例推导dp数组:
  • +
+
func coinChange(coins []int, amount int) int {
+    coinlen := len(coins)
+    dp := make([]int,amount+1)
+    for i:=1;i<=amount;i++{
+        dp[i] = amount+1
+    }
+    for i:=0;i<coinlen;i++{
+        for j:=coins[i];j<=amount;j++{
+            dp[j] = min(dp[j],dp[j-coins[i]]+1)
+        }
+    }
+    if dp[amount] == amount+1{
+        return -1
+    }
+    return dp[amount]
+}
+func min(a int,b int) int{
+    if a > b{
+        return b
+    }
+    return a
+}
+
class Solution {
+public:
+    int coinChange(vector<int>& coins, int amount) {
+        int n = coins.size();
+        vector<int> dp(amount+1,INT_MAX);
+        dp[0] = 0;
+        for(int i=0;i<n;i++){
+            for(int j=coins[i];j<=amount;j++){
+                if (dp[j - coins[i]] != INT_MAX) {
+                    dp[j] = min(dp[j], dp[j-coins[i]]+1);
+                }
+            }
+        }
+        return dp[amount] == INT_MAX ? -1 : dp[amount];
+    }
+};
+

279. 完全平方数

+

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

+

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示和为j的完全平方数的最少数量
  • +
  • 确定递推公式:dp[j]有两种情况,第一种情况是不能凑出完全平方数。第二种情况是遍历比它小的数字,可以通过一些组合凑出完全平方数,最终取最少的数量,因此递推公式为 dp[j]=min(dp[j],dp[j-i*i]+1)
  • +
  • dp数组如何初始化:dp[0]=0,凑成整数0的完全平方数的最少数量为0,不影响后面的递推。同时要将整个dp数组初始化一个比较大的数值,从而不影响min的判断
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从前到后进行填充。
  • +
  • 举例推导dp数组:
  • +
+
func numSquares(n int) int {
+    dp := make([]int,n+1)
+    for i:=1;i<=n;i++{
+        dp[i] = n+1
+    }
+    dp[0] = 0
+    for j:=0;j<=n;j++{
+        for i:=1;i*i<=j;i++{
+            dp[j] = min(dp[j],dp[j-i*i]+1)
+        }
+    }
+    return dp[n]
+}
+
+func min(a int,b int) int{
+    if a > b{
+        return b
+    }
+    return a
+}
+
class Solution {
+public:
+    int numSquares(int n) {
+        vector<int> nums;
+        for(int i=1;i<=n;i++){
+            if(i * i <= n){
+                nums.push_back(i*i);
+            } else{
+                break;
+            }
+        }
+        int m = nums.size();
+        vector<int> dp(n+1, INT_MAX);
+        dp[0] = 0;
+        for(int i=0;i<m;i++){
+            for(int j=nums[i];j<=n;j++){
+                if(dp[j-nums[i]] == INT_MAX){
+                    continue;
+                }
+                dp[j] = min(dp[j],dp[j-nums[i]]+1);
+            }
+        }
+        return dp[n];
+    }
+};
+

139. 单词拆分

+

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s

+
    +
  • 确定dp数组以及下标的含义:dp[j]表示字符串s以下标j结尾是否能通过字符串列表拼接出来
  • +
  • 确定递推公式:如果当前减掉遍历的字符串的长度为真,且这个减掉的区间的字符串恰好等于这个字符串,那么dp[j]=true,否则dp[j]=false,要注意不要被后面没有匹配上的干扰了,因此 dp[i] = dp[i] || dp[i-len(wordDict[j])]
  • +
  • dp数组如何初始化:dp[0]=true,以0为结尾是一个空串,是可以拼接出来的,后续要用到这个真值进行递推。
  • +
  • 确定遍历顺序:遍历每一个数值,填充dp数组时要从前到后进行填充,以符合完全背包的条件。外层遍历字符串,内层遍历字典中的数值,相当于每多一个字符就把字典内全部的字符串拿过去尝试一下
  • +
  • 举例推导dp数组:
  • +
+
func wordBreak(s string, wordDict []string) bool {
+    lenword := len(wordDict)
+    lens := len(s)
+    dp := make([]bool,lens+1)
+    dp[0] = true
+    for i:=0;i<=lens;i++{
+        for j:=0;j<lenword;j++{
+            if i-len(wordDict[j]) >= 0{
+                if wordDict[j] == s[i-len(wordDict[j]):i]{
+                    dp[i] = dp[i] || dp[i-len(wordDict[j])]
+                }
+            }
+        }
+    }
+    return dp[lens]
+}
+
class Solution {
+public:
+    bool wordBreak(string s, vector<string>& wordDict) {
+        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
+        vector<bool> dp(s.size() + 1, false);
+        dp[0] = true;
+        for (int i = 1; i <= s.size(); i++) {   // 遍历背包
+            for (int j = 0; j < i; j++) {       // 遍历物品
+                string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
+                if (wordSet.find(word) != wordSet.end() && dp[j]) {
+                    dp[i] = true;
+                }
+            }
+        }
+        return dp[s.size()];
+    }
+};
+

多重背包

+

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

+

每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

+

背包问题总结

+

xJY2rR.md.png

+

背包递推公式

+

问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:

+ +

问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:

+ +

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:

+ +

问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:

+ +

遍历顺序

+
01背包
+

动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

+

动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

+

一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!

+
完全背包
+

说完01背包,再看看完全背包。

+

动态规划:关于完全背包,你该了解这些! (opens new window)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

+

但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。

+

如果求组合数就是外层for循环遍历物品,内层for遍历背包

+

如果求排列数就是外层for遍历背包,内层for循环遍历物品

+

相关题目如下:

+ +

如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:

+ +

对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了

+

总结

+

xJYoGD.png

+

打家劫舍

+

198. 打家劫舍

+

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

+

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

+
    +
  • 确定dp数组以及下标的含义:dp[i]表示偷到第i个房屋的最高金额
  • +
  • 确定递推公式:设当前是第i个房间,则如果偷第i个房间,则dp[i] = dp[i-2] + nums[i-1],如果不偷第i个房间,则dp[i] = dp[i-1]。因此递推公式为 dp[i]=max(dp[i-2] + nums[i-1], dp[i-1])
  • +
  • dp数组如何初始化:dp[0]=0,偷第0个房间得到的金额为0;dp[1] = nums[0],偷第1个房间得到的金额只能是第一个房间的金额
  • +
  • 确定遍历顺序:从前到后遍历即可。
  • +
  • 举例推导dp数组:
  • +
+
func rob(nums []int) int {
+    numslen := len(nums)
+    dp := make([]int,numslen+1)
+    dp[0] = 0
+    dp[1] = nums[0]
+    for i:=2;i<=numslen;i++{
+        dp[i]=max(dp[i-2] + nums[i-1], dp[i-1])
+    }
+    return dp[numslen]
+}
+
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int rob(vector<int>& nums) {
+        int n = nums.size();
+        if(n == 1){
+            return nums[0];
+        }
+        vector<int> dp(n,0);
+        dp[0] = nums[0];
+        dp[1] = max(nums[0], nums[1]);
+        for(int i=2;i<n;i++){
+            dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
+        }
+        return dp[n-1];
+    }
+};
+

213. 打家劫舍II

+

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

+

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

+

分两种情况讨论就好,第一种情况是偷第一间房间,那么最后一间房间就不能偷了,第二种情况是不偷第一件房间,那么就可以偷最后一间房间。分类讨论即可。

+
    +
  • 确定dp数组以及下标的含义:dp[i]表示偷到第i个房屋的最高金额
  • +
  • 确定递推公式:设当前是第i个房间,则如果偷第i个房间,则dp[i] = dp[i-2] + nums[i-1],如果不偷第i个房间,则dp[i] = dp[i-1]。因此递推公式为 dp[i]=max(dp[i-2] + nums[i-1], dp[i-1])
  • +
  • dp数组如何初始化:dp[0]=0,偷第0个房间得到的金额为0;dp[1] = nums[0],偷第1个房间得到的金额只能是第一个房间的金额
  • +
  • 确定遍历顺序:从前到后遍历即可。
  • +
  • 举例推导dp数组:
  • +
+
func rob(nums []int) int {
+    numslen := len(nums)
+    if numslen == 1{
+        return nums[0]
+    }
+    dp := make([]int,numslen)
+    dp[0] = 0
+    dp[1] = nums[0]
+    for i:=2;i<numslen;i++{
+        dp[i]=max(dp[i-2] + nums[i-1], dp[i-1])
+    }
+    a := dp[numslen-1]
+
+    dp2 := make([]int,numslen)
+    dp2[0] = 0
+    dp2[1] = nums[1]
+    for i:=2;i<numslen;i++{
+        dp2[i]=max(dp2[i-2] + nums[i], dp2[i-1])
+    }
+    b := dp2[numslen-1]
+  
+    return max(a,b)
+}
+
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int rob(vector<int>& nums) {
+        int n = nums.size();
+        if(n == 1){
+            return nums[0];
+        }
+        if(n == 2){
+            return max(nums[0], nums[1]);
+        }
+        if(n == 3){
+            return max(nums[0],max(nums[1], nums[2]));
+        }
+        vector<int> dp(n);
+        dp[0] = nums[0];
+        dp[1] = max(nums[0],nums[1]);
+        for(int i=2;i<n-1;i++){
+            dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
+        }
+        vector<int> dp2(n);
+        dp2[1] = nums[1];
+        dp2[2] = max(nums[1],nums[2]);
+        for(int i=3;i<n;i++){
+            dp2[i] = max(dp2[i-1], dp2[i-2] + nums[i]);
+        }
+        return max(dp[n-2], dp2[n-1]);
+    }
+};
+

337. 打家劫舍 III

+

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

+

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

+

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

+
    +
  1. 确定递归函数的参数和返回值
  2. +
+

这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

+
    +
  1. 确定终止条件
  2. +
+

在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回,这也相当于dp数组的初始化

+
    +
  1. 确定遍历顺序
  2. +
+

首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。

+

通过递归左节点,得到左节点偷与不偷的金钱。

+

通过递归右节点,得到右节点偷与不偷的金钱。

+
    +
  1. 确定单层递归的逻辑
  2. +
+

如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0];

+

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

+

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func rob(root *TreeNode) int {
+    res := DFS(root)
+    return max(res[0],res[1])
+}
+
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
+func DFS(root *TreeNode) []int{
+    if root == nil{
+        return []int{0,0}
+    }
+    // 后序遍历
+	left := DFS(root.Left)
+	right := DFS(root.Right)
+
+    // 考虑去偷当前的屋子
+	robCur := root.Val + left[0] + right[0]
+    // 考虑不去偷当前的屋子
+	notRobCur := max(left[0], left[1]) + max(right[0], right[1])
+
+    // 注意顺序:0:不偷,1:去偷
+	return []int{notRobCur, robCur}
+}
+
class Solution {
+public:
+    int rob(TreeNode* root) {
+        vector<int> result = robTree(root);
+        return max(result[0], result[1]);
+    }
+    vector<int> robTree(TreeNode* root){
+        if(root == NULL){
+            return vector<int>{0,0};
+        }
+        vector<int> left = robTree(root->left);
+        vector<int> right = robTree(root->right);
+        int val1 = root->val + left[0] + right[0];
+        int val2 = max(left[0], left[1]) + max(right[0], right[1]);
+        return vector<int>{val2, val1};
+    }
+};
+

股票问题

+

121. 买卖股票的最佳时机

+

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

+

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

+

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

+

贪心:取最左最小值,取最右最大值,那么得到的差值就是最大利润。

+
func maxProfit(prices []int) int {
+    pricelen := len(prices)
+    result := 0
+    minnum := prices[0]
+    for i:= 0;i<pricelen;i++{
+        minnum = min(minnum,prices[i])
+        result = max(result,prices[i] - minnum)
+    }
+    return result
+}
+
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
+func min(a int,b int) int{
+    if a < b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        vector<vector<int> > dp(prices.size(), vector<int>(2,0));
+        dp[0][0] = -prices[0];
+        dp[0][1] = 0;
+        for(int i=1;i<prices.size();i++){
+            dp[i][0] = max(dp[i-1][0], -prices[i]);
+            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
+        }
+        return max(dp[prices.size()-1][0], dp[prices.size()-1][1]);
+    }
+};
+

122. 买卖股票的最佳时机II

+

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

+

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

+

返回 你能获得的 最大 利润 。

+
    +
  • 确定dp数组以及下标的含义:dp[i][0]在第i天持有股票获得的最大利润,dp[i][1]在第i天不持有股票获得的最大利润
  • +
  • 确定递推公式: +
      +
    • 如果持有股票: +
        +
      • 第i-1天不持有股票,那么第i天是买入了,因此dp[i][0]=dp[i-1][1]-prices[i]
      • +
      • 第i-1天持有股票,那么获得的利润和前一天是相同的,因此dp[i][0]=dp[i-1][0]
      • +
      +
    • +
    • 如果不持有股票: +
        +
      • 第i-1天不持有股票,那么获得的利润和前一天是相同的,因此dp[i][1]=dp[i-1][1]
      • +
      • 第i-1天持有股票,那么第i天是卖出了,因此dp[i][1]=dp[i-1][0]+prices[i]
      • +
      +
    • +
    +
  • +
  • dp数组如何初始化:第0天不持有股票 dp[0][1]=0,第0天持有股票 dp[0][0]=-prices[0]
  • +
  • 确定遍历顺序:从前到后遍历即可。
  • +
  • 举例推导dp数组:
  • +
+
func maxProfit(prices []int) int {
+    lenprices := len(prices)
+    dp := make([][]int,lenprices+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,2)
+    }
+    dp[0][1] = 0
+    dp[0][0] = -prices[0]
+    for i:=1;i<lenprices;i++{
+        dp[i][0]=max(dp[i-1][1]-prices[i],dp[i-1][0])
+        dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])
+    }
+    return max(dp[lenprices-1][0],dp[lenprices-1][1])
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        int n = prices.size();
+        vector<vector<int> > dp(n,vector<int>(2,0));
+        dp[0][0] = -prices[0];
+        dp[0][1] = 0;
+        for(int i=1;i<n;i++){
+            dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
+            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
+        }
+        return dp[n-1][1];
+    }
+};
+

123. 买卖股票的最佳时机III

+

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

+

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

+

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

+
    +
  • 确定dp数组以及下标的含义:一天一共就有五个状态 +
      +
    • dp[i][0]表示第i天没有操作所能获取的最大利润
    • +
    • dp[i][1]表示第i天是第一次买入所能获取的最大利润
    • +
    • dp[i][2]表示第i天是第一次卖出所能获取的最大利润
    • +
    • dp[i][3]表示第i天是第二次买入所能获取的最大利润
    • +
    • dp[i][4]表示第i天是第二次卖出所能获取的最大利润
    • +
    +
  • +
  • 确定递推公式: +
      +
    • 第i天没有操作的状态一定沿用i-1天没有操作的状态,则dp[i][0]=dp[i-1][0]
    • +
    • 第i天第一次买入的状态 +
        +
      • 若第i天买入,前面一定没有买过,dp[i][1]=dp[i-1][0]-prices[i]
      • +
      • 若第i天不变,沿用上一天买入的状态,dp[i][1]=dp[i-1][1]
      • +
      +
    • +
    • 第i天第一次卖出的状态 +
        +
      • 若第i天卖出,前面一定会买入,dp[i][2]=dp[i-1][1]+prices[i]
      • +
      • 若第i天不变,沿用上一天第一次卖出的状态,dp[i][2]=dp[i-1][2]
      • +
      +
    • +
    • 第i天第二次买入的状态 +
        +
      • 若第i天第二次买入,前面一定会卖出,dp[i][3]=dp[i-1][2]-prices[i]
      • +
      • 若第i天不变,沿用上一天第二次买入的状态,dp[i][3]=dp[i-1][3]
      • +
      +
    • +
    • 第i天第二次卖出的状态 +
        +
      • 若第i天第二次卖出,前面一定会买入,dp[i][4]=dp[i-1][3]+prices[i]
      • +
      • 若第i天不变,沿用上一天第二次卖出的状态,dp[i][4]=dp[i-1][4]
      • +
      +
    • +
    +
  • +
  • dp数组如何初始化: +
      +
    • 第0天没有操作:dp[0][0] = 0
    • +
    • 第0天第一次买入:dp[0][1] = -prices[0]
    • +
    • 第0天第一次卖出:卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0,从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了,因此初始化dp[0][2] = 0
    • +
    • 第0天第二次买入:dp[0][3] = -prices[0]
    • +
    • 第0天第二次卖出:dp[0][4] = 0
    • +
    +
  • +
  • 确定遍历顺序:从前到后遍历即可。
  • +
  • 举例推导dp数组:
  • +
+
func maxProfit(prices []int) int {
+    lenprices := len(prices)
+    dp := make([][]int,lenprices+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,5)
+    }
+    dp[0][0] = 0
+    dp[0][1] = -prices[0]
+    dp[0][2] = 0
+    dp[0][3] = -prices[0]
+    dp[0][4] = 0
+    for i:=1;i<lenprices;i++{
+        dp[i][0]=dp[i-1][0]
+        dp[i][1]=max(dp[i-1][0]-prices[i],dp[i-1][1])
+        dp[i][2]=max(dp[i-1][1]+prices[i],dp[i-1][2])
+        dp[i][3]=max(dp[i-1][2]-prices[i],dp[i-1][3])
+        dp[i][4]=max(dp[i-1][3]+prices[i],dp[i-1][4])
+    }
+    return dp[lenprices-1][4]
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        int n = prices.size();
+        vector<vector<int> > dp(n,vector<int>(4,0));
+        // 第一次持有
+        // 第一次不持有
+        // 第二次持有
+        // 第二次不持有
+        dp[0][0] = -prices[0];
+        dp[0][2] = -prices[0];
+        for(int i=1;i<n;i++){
+            dp[i][0] = max(dp[i-1][0], - prices[i]);
+            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
+            dp[i][2] = max(dp[i-1][2], dp[i-1][1] - prices[i]);
+            dp[i][3] = max(dp[i-1][3], dp[i-1][2] + prices[i]);
+        }
+        return max(dp[n-1][1], dp[n-1][3]);
+    }
+};
+

188. 买卖股票的最佳时机IV

+

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

+

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

+
func maxProfit(k int, prices []int) int {
+    lenprices := len(prices)
+    dp := make([][]int,lenprices+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,k*2+1)
+    }
+    for i:=0;i<k*2+1;i++{
+        if i % 2 == 1{
+            dp[0][i] = -prices[0]
+        }
+    }
+    for i:=1;i<lenprices;i++{
+        dp[i][0]=dp[i-1][0]
+        for j:=1;j<=2*k;j++{
+            if j % 2 == 1{
+                dp[i][j] = max(dp[i-1][j-1]-prices[i],dp[i-1][j])
+            } else{
+                dp[i][j] = max(dp[i-1][j-1]+prices[i],dp[i-1][j])
+            }
+        }
+    }
+    return dp[lenprices-1][2*k]
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int maxProfit(int k, vector<int>& prices) {
+        int n = prices.size();
+        vector<vector<int> > dp(n,vector<int>(k*2,0));
+        for(int i=0;i<k*2;i+=2){
+            dp[0][i] = -prices[0];
+        }
+        for(int i=1;i<n;i++){
+            dp[i][0] = max(dp[i-1][0], - prices[i]);
+            for(int j=1;j<2*k;j++){
+                if(j % 2 == 0){
+                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-1] - prices[i]);
+                } else{
+                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-1] + prices[i]);
+                }
+            }
+        }
+        int maxprofit = 0;
+        for(int i=0;i<2*k;i++){
+            maxprofit = max(maxprofit, dp[n-1][i]);
+        }
+        return maxprofit;
+    }
+};
+

309. 最佳买卖股票时机含冷冻期

+

给定一个整数数组prices,其中第  prices[i] 表示第 i 天的股票价格 。

+

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

+

卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

+

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

+
    +
  • 确定dp数组以及下标的含义:一天一共有四个状态 +
      +
    • dp[i][0]表示第i天是买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
    • +
    • dp[i][1]表示第i天是两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
    • +
    • dp[i][2]表示第i天是今天卖出了股票
    • +
    • dp[i][3]表示第i天是冷冻期状态
    • +
    +
  • +
  • 确定递推公式: +
      +
    • 第i天是买入股票状态 +
        +
      • 状态不变,则dp[i][0]=dp[i-1][0]
      • +
      • 第i-1天是状态1,则dp[i][0]=dp[i-1][1]-prices[i]
      • +
      • 第i-1天是状态3,则dp[i][0]=dp[i-1][3]-prices[i]
      • +
      +
    • +
    • 第i天是两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态 +
        +
      • 状态不变,则dp[i][1]=dp[i-1][1]
      • +
      • 第i-1天是状态3,则dp[i][1]=dp[i-1][3]
      • +
      +
    • +
    • 第i天是今天卖出了股票的状态,那么第i-1天一定是状态0,dp[i][2]=dp[i-1][0]+prices[i]
    • +
    • 第i天是冷冻期状态,那么第i-1天一定是状态2,dp[i][3]=dp[i-1][2]
    • +
    +
  • +
  • dp数组如何初始化: +
      +
    • 状态0:dp[0][0] = -prices[0]
    • +
    • 状态1:dp[0][1] = 0
    • +
    • 状态2:dp[0][2] = 0
    • +
    • 状态3:dp[0][3] = 0
    • +
    +
  • +
  • 确定遍历顺序:从前到后遍历即可。
  • +
  • 举例推导dp数组:
  • +
+
func maxProfit(prices []int) int {
+    lenprices := len(prices)
+    dp := make([][]int,lenprices+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,4)
+    }
+    dp[0][0] = -prices[0]
+    dp[0][1] = 0
+    dp[0][2] = 0
+    dp[0][3] = 0
+    for i:=1;i<lenprices;i++{
+        dp[i][0]=max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i])
+        dp[i][1]=max(dp[i-1][1],dp[i-1][3])
+        dp[i][2]=dp[i-1][0]+prices[i]
+        dp[i][3]=dp[i-1][2]
+    }
+    return max(dp[lenprices-1][1],max(dp[lenprices-1][2],dp[lenprices-1][3]))
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        int n = prices.size();
+        vector<vector<int> > dp(n,vector<int>(4,0));
+        // 持有
+        // 不持有不是今天卖的
+        // 不持有是今天卖的
+        // 冷冻期
+        dp[0][0] = -prices[0];
+        for(int i=1;i<n;i++){
+            dp[i][0] = max(dp[i-1][0], max(dp[i-1][1], dp[i-1][3]) - prices[i]);
+            dp[i][1] = max(dp[i-1][1], dp[i-1][3]);
+            dp[i][2] = dp[i-1][0] + prices[i];
+            dp[i][3] = dp[i-1][2];
+        }
+        return max(dp[n-1][1], max(dp[n-1][2],dp[n-1][3]));
+    }
+};
+

714. 买卖股票的最佳时机含手续费

+

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

+

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

+

返回获得利润的最大值。

+

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

+

分析:卖出的时候扣除手续费即可。如果买入的时候扣除手续费,会导致买入的代价过大。卖出是获利的,卖出时候扣除手续费就可以了

+
    +
  • 确定dp数组以及下标的含义:dp[i][0]在第i天持有股票获得的最大利润,dp[i][1]在第i天不持有股票获得的最大利润
  • +
  • 确定递推公式: +
      +
    • 如果持有股票: +
        +
      • 第i-1天不持有股票,那么第i天是买入了,因此dp[i][0]=dp[i-1][1]-prices[i]
      • +
      • 第i-1天持有股票,那么获得的利润和前一天是相同的,因此dp[i][0]=dp[i-1][0]
      • +
      +
    • +
    • 如果不持有股票: +
        +
      • 第i-1天不持有股票,那么获得的利润和前一天是相同的,因此dp[i][1]=dp[i-1][1]
      • +
      • 第i-1天持有股票,那么第i天是卖出了,因此dp[i][1]=dp[i-1][0]+prices[i]-fee
      • +
      +
    • +
    +
  • +
  • dp数组如何初始化:第0天不持有股票 dp[0][1]=0,第0天持有股票 dp[0][0]=-prices[0]
  • +
  • 确定遍历顺序:从前到后遍历即可。
  • +
  • 举例推导dp数组:
  • +
+
func maxProfit(prices []int, fee int) int {
+    lenprices := len(prices)
+    dp := make([][]int,lenprices+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,2)
+    }
+    dp[0][1] = 0
+    dp[0][0] = -prices[0]
+    for i:=1;i<lenprices;i++{
+        dp[i][0]=max(dp[i-1][1]-prices[i],dp[i-1][0])
+        dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]-fee)
+    }
+    return max(dp[lenprices-1][0],dp[lenprices-1][1])
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int maxProfit(vector<int>& prices, int fee) {
+        int n = prices.size();
+        vector<vector<int> > dp(n,vector<int>(2,0));
+        dp[0][0] = -prices[0]-fee;
+        dp[0][1] = 0;
+        for(int i=1;i<n;i++){
+            dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]-fee);
+            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
+        }
+        return dp[n-1][1];
+    }
+};
+

股票问题总结

+

xY6wKx.md.png

+

子序列问题

+

300. 最长递增子序列

+

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

+

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

+
    +
  • 确定dp数组以及下标的含义:dp[i]的定义到第i个数字截止的最长严格递增子序列的长度
  • +
  • 确定递推公式:第i个数字截止的最长严格递增子序列的长度,是要遍历这个数字前面的数字,发现前面的数字j比这个数字小,即可以再长一位。即dp[i]=max(dp[i],dp[j]+1)
  • +
  • dp数组如何初始化:每一个数字的最长严格递增子序列最少是自己本身,因此要初始化为全1
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func lengthOfLIS(nums []int) int {
+    lennums := len(nums)
+    dp := make([]int,lennums)
+    for i,_ := range dp{
+        dp[i] = 1
+    }
+    var result int = 0
+    for i:=0;i<lennums;i++{
+        for j:=0;j<i;j++{
+            if nums[i] > nums[j]{
+                dp[i] = max(dp[i],dp[j]+1)
+            }
+        }
+        result = max(result,dp[i])
+    }
+    return result
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int lengthOfLIS(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n,1);
+        int result = 1;
+        for(int i=1;i<n;i++){
+            for(int j=0;j<i;j++){
+                if(nums[i] > nums[j]){
+                    dp[i] = max(dp[i],dp[j]+1);
+                }
+            }
+            result = max(result, dp[i]);
+        }
+        return result;
+    }
+};
+

674. 最长连续递增序列

+

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

+

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。

+
    +
  • 确定dp数组以及下标的含义:dp[i]的定义到第i个数字截止的最长连续递增的子序列的长度
  • +
  • 确定递推公式:如果第i个数字比第i-1个数字数值大,则dp[i]=dp[i-1]+1,否则不变。最终找到整个dp数组中的最大值
  • +
  • dp数组如何初始化:每一个数字的最长连续递增的子序列的长度最少是自己本身,因此要初始化为全1
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func findLengthOfLCIS(nums []int) int {
+    lennums := len(nums)
+    dp := make([]int,lennums)
+    for i,_ := range dp{
+        dp[i] = 1
+    }
+    var result int = 1
+    for i:=1;i<lennums;i++{
+        if nums[i] > nums[i-1]{
+            dp[i] = dp[i-1]+1
+        }
+        result = max(result,dp[i])
+    }
+    return result
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int findLengthOfLCIS(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n,1);
+        int result = 1;
+        for(int i=1;i<n;i++){
+            if(nums[i] > nums[i-1]){
+                dp[i] = dp[i-1] + 1;
+            }
+            result = max(result,dp[i]);
+        }
+        return result;
+    }
+};
+

718. 最长重复子数组

+

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度。

+

注意题目中说的子数组,其实就是连续子序列。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j]表示nums1到下标i-1,nums2到下标j-1的公共的长度最长的子数组的长度
  • +
  • 确定递推公式:如果nums[i-1] == nums[j-1],则说明长度可以再增长一位,dp[i][j] = dp[i-1][j-1] + 1,如果不相等,则不可以延续,保持为0
  • +
  • dp数组如何初始化:dp[i][0]和dp[0][j]均初始化为0
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func findLength(nums1 []int, nums2 []int) int {
+    nums1len := len(nums1)
+    nums2len := len(nums2)
+    dp := make([][]int,nums1len+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,nums2len+1)
+    }
+    var result int = 0
+    for i:=1;i<=nums1len;i++{
+        for j:=1;j<=nums2len;j++{
+            if nums1[i-1] == nums2[j-1]{
+                dp[i][j] = dp[i-1][j-1] + 1
+            }
+            result = max(result,dp[i][j])
+        }
+    }
+    return result
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int findLength(vector<int>& nums1, vector<int>& nums2) {
+        int m = nums1.size();
+        int n = nums2.size();
+        int result = 0;
+        vector<vector<int> > dp(m+1, vector<int>(n+1,0));
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(nums1[i-1] == nums2[j-1]){
+                    dp[i][j] = dp[i-1][j-1] + 1;
+                }
+                result = max(result, dp[i][j]);
+            }
+        }
+        return result;
+    }
+};
+

1143. 最长公共子序列

+

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

+

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

+

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。

+

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j]表示text1到下标i-1,text2到下标j-1的最长公共子序列的长度
  • +
  • 确定递推公式:如果text1[i-1] == text2[j-1],则说明长度可以再增长一位,dp[i][j] = dp[i-1][j-1] + 1,如果不相等,则保留前面的最长公共子序列的长度,即dp[i][j] = max(dp[i][j-1],dp[i-1][j]
  • +
  • dp数组如何初始化:dp[i][0]和dp[0][j]均初始化为0
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func longestCommonSubsequence(text1 string, text2 string) int {
+    text1len := len(text1)
+    text2len := len(text2)
+    dp := make([][]int,text1len+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,text2len+1)
+    }
+    for i:=1;i<=text1len;i++{
+        for j:=1;j<=text2len;j++{
+            if text1[i-1] == text2[j-1]{
+                dp[i][j] = dp[i-1][j-1] + 1
+            } else{
+                dp[i][j] = max(dp[i][j-1],dp[i-1][j])
+            }
+        }
+    }
+    return dp[text1len][text2len]
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int longestCommonSubsequence(string text1, string text2) {
+        int m = text1.size();
+        int n = text2.size();
+        vector<vector<int> > dp(m+1,vector<int>(n+1,0));
+        for(int i=1; i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(text1[i-1] == text2[j-1]){
+                    dp[i][j] = dp[i-1][j-1] + 1;
+                } else{
+                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

1035. 不相交的线

+

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

+

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:nums1[i] == nums2[j]

+

且绘制的直线不与任何其他连线(非水平线)相交。

+

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

+

以这种方法绘制线条,并返回可以绘制的最大连线数。

+

分析:直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。

+

本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

+
func maxUncrossedLines(nums1 []int, nums2 []int) int {
+    nums1len := len(nums1)
+    nums2len := len(nums2)
+    dp := make([][]int,nums1len+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,nums2len+1)
+    }
+    for i:=1;i<=nums1len;i++{
+        for j:=1;j<=nums2len;j++{
+            if nums1[i-1] == nums2[j-1]{
+                dp[i][j] = dp[i-1][j-1] + 1
+            } else{
+                dp[i][j] = max(dp[i][j-1],dp[i-1][j])
+            }
+        }
+    }
+    return dp[nums1len][nums2len]
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
+        int m = nums1.size();
+        int n = nums2.size();
+        vector<vector<int> > dp(m+1,vector<int>(n+1,0));
+        for(int i=1; i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(nums1[i-1] == nums2[j-1]){
+                    dp[i][j] = dp[i-1][j-1] + 1;
+                } else{
+                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

53. 最大子序和

+

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

+

子数组是数组中的一个连续部分。

+
    +
  • 确定dp数组以及下标的含义:dp[i]表示以第i个数字为结尾的连续子数组的最大和
  • +
  • 确定递推公式:dp[i]有两种情况,一种情况是自身就是具有最大和的连续子数组,另外一种情况是与dp[i-1]一起是具有最大和的连续子数组,因此dp[i]=max(dp[i-1]+nums[i],nums[i])
  • +
  • dp数组如何初始化:dp[0]=0
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func maxSubArray(nums []int) int {
+    lennums := len(nums)
+    dp := make([]int,lennums+1)
+    dp[0] = 0
+    var result int = nums[0]
+    for i:=1;i<=lennums;i++{
+        dp[i] = max(nums[i-1],dp[i-1]+nums[i-1])
+        result = max(result,dp[i])
+    }
+    return result
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int maxSubArray(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n,0);
+        dp[0] = nums[0];
+        int result = dp[0];
+        for(int i=1;i<n;i++){
+            dp[i] = max(nums[i], dp[i-1] + nums[i]);
+            result = max(result,dp[i]);
+        }
+        return result;
+    }
+};
+

918. 环形子数组的最大和

+

给定一个长度为 n环形整数数组 nums ,返回 * nums 的非空 子数组 的最大可能和 * 。

+

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n]nums[i] 的前一个元素是 nums[(i - 1 + n) % n]

+

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n

+
class Solution {
+public:
+    int maxSubarraySumCircular(vector<int>& nums) {
+        vector<int> dp(nums.size(),0);
+        vector<int> leftMax(nums.size(),0);
+        dp[0] = nums[0];
+        int maxlength = dp[0];
+        int leftSum = dp[0];
+        leftMax[0] = dp[0];
+        for(int i=1;i<nums.size();i++){
+            dp[i] = max(dp[i-1] + nums[i], nums[i]);
+            maxlength = max(maxlength, dp[i]);
+            leftSum += nums[i];
+            leftMax[i] = max(leftMax[i-1], leftSum);
+        }
+        int rightSum = 0;
+        for(int j=nums.size()-1;j>0;j--){
+            rightSum += nums[j];
+            maxlength = max(maxlength, rightSum + leftMax[j-1]);
+        }
+        return maxlength;
+    }
+};
+

392. 判断子序列

+

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

+

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
  • +
  • 确定递推公式:如果s[i-1] == t[j-1],则说明长度可以再增长一位,dp[i][j] = dp[i-1][j-1] + 1,如果不相等,则保留前面的最长公共子序列的长度,即dp[i][j] = max(dp[i][j-1],dp[i-1][j])
  • +
  • dp数组如何初始化:dp[i][0]和dp[0][j]均初始化为0
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func isSubsequence(s string, t string) bool {
+    slen := len(s)
+    tlen := len(t)
+    dp := make([][]int,slen+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,tlen+1)
+    }
+    for i:=1;i<=slen;i++{
+        for j:=1;j<=tlen;j++{
+            if s[i-1] == t[j-1]{
+                dp[i][j] = dp[i-1][j-1] + 1
+            } else{
+                dp[i][j] = max(dp[i][j-1],dp[i-1][j])
+            }
+        }
+    }
+    return dp[slen][tlen] == slen
+}
+func max(a int,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    bool isSubsequence(string s, string t) {
+        int m = s.size();
+        int n = t.size();
+        vector<vector<int> > dp(m+1, vector<int>(n+1,0));
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(s[i-1] == t[j-1]){
+                    dp[i][j] = dp[i-1][j-1] + 1;
+                } else{
+                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
+                }
+            }
+        }
+        if (dp[m][n] == m){
+            return true;
+        }
+        return false;
+    }
+};
+

115. 不同的子序列

+

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

+

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)

+

题目数据保证答案符合 32 位带符号整数范围。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j] 表示以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
  • +
  • 确定递推公式:如果s[i-1] == t[j-1],dp[i][j]可以由两部分组成:一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1],一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j]
  • +
  • dp数组如何初始化: +
      +
    • dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。
    • +
    • dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。那么dp[0][j]一定都是0,s如论如何也变成不了t。
    • +
    • dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。
    • +
    +
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func numDistinct(s string, t string) int {
+    slen := len(s)
+    tlen := len(t)
+    dp := make([][]int,slen+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,tlen+1)
+    }
+    for i:=0;i<=slen;i++{
+        dp[i][0] = 1
+    }
+    for j:=1;j<=tlen;j++{
+        dp[0][j] = 0
+    }
+    for i:=1;i<=slen;i++{
+        for j:=1;j<=tlen;j++{
+            if s[i-1] == t[j-1]{
+                dp[i][j] = dp[i-1][j-1]+dp[i-1][j]
+            } else{
+                dp[i][j] = dp[i-1][j]
+            }
+        }
+    }
+    return dp[slen][tlen]
+}
+
class Solution {
+public:
+    int numDistinct(string s, string t) {
+        vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
+        for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
+        for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
+        for (int i = 1; i <= s.size(); i++) {
+            for (int j = 1; j <= t.size(); j++) {
+                if (s[i - 1] == t[j - 1]) {
+                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
+                } else {
+                    dp[i][j] = dp[i - 1][j];
+                }
+            }
+        }
+        return dp[s.size()][t.size()];
+    }
+};
+

583. 两个字符串的删除操作

+

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的 最小步数

+

每步可以删除任意一个字符串中的一个字符。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j] 表示以i-1为结尾的word1子序列和以j-1为结尾的word2子序列,使两者相同的最小步数
  • +
  • 确定递推公式:如果word1[i-1] == word2[j-1],相同则步数一样,则dp[i][j]=dp[i-1][j-1]。当word1[i - 1] 与 word2[j - 1]不相等时,dp[i][j]至少需要删除掉两个字符中的某一个,因此dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1
  • +
  • dp数组如何初始化: +
      +
    • dp[i][0]:以i-1为结尾的word1随便删除元素,等于空字符串的最小步数。那么dp[i][0]一定都是i,全部都要删了
    • +
    • dp[0][j]:同理dp[0][j]=j
    • +
    +
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func minDistance(word1 string, word2 string) int {
+    word1len := len(word1)
+    word2len := len(word2)
+    dp := make([][]int,word1len+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,word2len+1)
+    }
+    for i:=0;i<=word1len;i++{
+        dp[i][0] = i
+    }
+    for j:=1;j<=word2len;j++{
+        dp[0][j] = j
+    }
+    for i:=1;i<=word1len;i++{
+        for j:=1;j<=word2len;j++{
+            if word1[i-1] == word2[j-1]{
+                dp[i][j] = dp[i-1][j-1]
+            } else{
+                dp[i][j] = min(dp[i-1][j],dp[i][j-1])+1
+            }
+        }
+    }
+    return dp[word1len][word2len]
+}
+
+func min(a int,b int) int{
+    if a < b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int minDistance(string word1, string word2) {
+        int m = word1.size();
+        int n = word2.size();
+        vector<vector<int> > dp(m+1, vector<int>(n+1,1));
+        for(int i=0;i<=m;i++){
+            for(int j=0;j<=n;j++){
+                if(i == 0){
+                    dp[i][j] = j;
+                }
+                if(j == 0){
+                    dp[i][j] = i;
+                }
+            }
+        }
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(word1[i-1] == word2[j-1]){
+                    dp[i][j] = dp[i-1][j-1];
+                } else{
+                    dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1;
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

72. 编辑距离

+

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

+

你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符

+
    +
  • 确定dp数组以及下标的含义:dp[i][j] 表示以i-1为结尾的word1子序列和以j-1为结尾的word2子序列,使两者相同的最小步数
  • +
  • 确定递推公式: +
      +
    • 如果word1[i-1] == word2[j-1],相同则步数一样,则dp[i][j]=dp[i-1][j-1]。
    • +
    • 当word1[i - 1] 与 word2[j - 1]不相等时: +
        +
      • 若删除或新增,则dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1
      • +
      • 若替换,则dp[i][j]=dp[i-1][j-1]+1
      • +
      +
    • +
    +
  • +
  • dp数组如何初始化: +
      +
    • dp[i][0]:以i-1为结尾的word1删除元素,等于空字符串的最小步数。那么dp[i][0]一定都是i,全部都要删了
    • +
    • dp[0][j]:同理dp[0][j]=j
    • +
    +
  • +
  • 确定遍历顺序:从前到后遍历
  • +
  • 举例推导dp数组:
  • +
+
func minDistance(word1 string, word2 string) int {
+    word1len := len(word1)
+    word2len := len(word2)
+    dp := make([][]int,word1len+1)
+    for i,_ := range dp{
+        dp[i] = make([]int,word2len+1)
+    }
+    for i:=0;i<=word1len;i++{
+        dp[i][0] = i
+    }
+    for j:=1;j<=word2len;j++{
+        dp[0][j] = j
+    }
+    for i:=1;i<=word1len;i++{
+        for j:=1;j<=word2len;j++{
+            if word1[i-1] == word2[j-1]{
+                dp[i][j] = dp[i-1][j-1]
+            } else{
+                dp[i][j] = min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1
+            }
+        }
+    }
+    return dp[word1len][word2len]
+}
+
+func min(a int,b int) int{
+    if a < b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int minDistance(string word1, string word2) {
+        int m = word1.size();
+        int n = word2.size();
+        vector<vector<int> > dp(m+1, vector<int>(n+1,1));
+        for(int i=0;i<=m;i++){
+            for(int j=0;j<=n;j++){
+                if(i == 0){
+                    dp[i][j] = j;
+                }
+                if(j == 0){
+                    dp[i][j] = i;
+                }
+            }
+        }
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(word1[i-1] == word2[j-1]){
+                    dp[i][j] = dp[i-1][j-1];
+                } else{
+                    dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

647. 回文子串

+

给你一个字符串 s ,请你统计并返回这个字符串中回文子串的数目。

+

回文字符串 是正着读和倒过来读一样的字符串。

+

子字符串 是字符串中的由连续字符组成的一个序列。

+

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串 +
      +
    • 注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分
    • +
    +
  • +
  • 确定递推公式: +
      +
    • s[i]!=s[j],dp[i][j]一定是false
    • +
    • s[i]==s[j] +
        +
      • 下标i与j相同,同一个字符,是回文子串
      • +
      • 下标i与j相差为1,也一定是回文子串
      • +
      • i与j相差大于1的时候,dp[i][j]=dp[i + 1][j - 1](也就是往回看)
      • +
      +
    • +
    +
  • +
  • dp数组如何初始化:均为false
  • +
  • 确定遍历顺序:会用到dp[i + 1][j - 1],因此要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的
  • +
  • 举例推导dp数组:
  • +
+
func countSubstrings(s string) int {
+    slen := len(s)
+    dp := make([][]bool,slen)
+    for i,_ := range dp{
+        dp[i] = make([]bool,slen)
+    }
+    var result int = 0
+    for i:=slen-1;i>=0;i--{
+        for j:=i;j<slen;j++{
+            if s[i] == s[j]{
+                if j - i <= 1{
+                    dp[i][j] = true
+                    result++
+                } else if dp[i+1][j-1]{
+                    dp[i][j] = true
+                    result++
+                }
+            }
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    int countSubstrings(string s) {
+        int n = s.size();
+        int result = 0;
+        vector<vector<bool> > dp(n, vector<bool>(n,false));
+        for(int i=n-1;i>=0;i--){
+            for(int j=i;j<n;j++){
+                if(s[i] == s[j]){
+                    if(j-i <= 1){
+                        result += 1;
+                        dp[i][j] = true;
+                    } else if(dp[i+1][j-1]){
+                        result += 1;
+                        dp[i][j] = true;
+                    }
+                }
+            }
+        }
+        return result;
+    }
+};
+

516. 最长回文子序列

+

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

+

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

+
    +
  • 确定dp数组以及下标的含义:dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的最长的回文子序列的长度 +
      +
    • 注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分
    • +
    +
  • +
  • 确定递推公式: +
      +
    • s[i]!=s[j],无法延续,因此dp[i][j]=max(dp[i+1][j],dp[i][j-1])
    • +
    • s[i]==s[j],可以延续,因此dp[i][j]=dp[i+1][j-1]+2
    • +
    +
  • +
  • dp数组如何初始化:不需要进行初始化
  • +
  • 确定遍历顺序:会用到dp[i + 1][j - 1],因此要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的
  • +
  • 举例推导dp数组:
  • +
+
func longestPalindromeSubseq(s string) int {
+    slen := len(s)
+    dp := make([][]int,slen)
+    for i,_ := range dp{
+        dp[i] = make([]int,slen)
+    }
+    for i:=slen-1;i>=0;i--{
+        for j:=i;j<slen;j++{
+            if j == i{
+                dp[i][j] = 1
+                continue
+            }
+            if s[i] == s[j]{
+                dp[i][j]=dp[i+1][j-1]+2
+            } else{
+                dp[i][j]=max(dp[i+1][j],dp[i][j-1])
+            }
+        }
+    }
+    return dp[0][slen-1]
+}
+func max(a int,b int)int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int longestPalindromeSubseq(string s) {
+        int n = s.size();
+        vector<vector<int> > dp(n, vector<int>(n,false));
+        for (int i = 0; i < n; i++){
+            dp[i][i] = 1;
+        }
+        for(int i=n-1;i>=0;i--){
+            for(int j=i+1;j<n;j++){
+                if (s[i] == s[j]) {
+                    dp[i][j] = dp[i + 1][j - 1] + 2;
+                } else {
+                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
+                }
+            }
+        }
+        return dp[0][n-1];
+    }
+};
+

动态规划总结

+

xY6yIe.png

+ + +
+ +
+
+ + + + + + +
+
+
代码随想录-动态规划专题
+
https://zhangzhao219.github.io/2024/03/07/Leetcode/programmercarl/programmercarl-dp/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月7日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/07/Leetcode/programmercarl/programmercarl-gt/index.html b/2024/03/07/Leetcode/programmercarl/programmercarl-gt/index.html new file mode 100644 index 000000000..9f2f347b1 --- /dev/null +++ b/2024/03/07/Leetcode/programmercarl/programmercarl-gt/index.html @@ -0,0 +1,1598 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-图论专题 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-图论专题

+ + +
+ +

代码随想录-图论专题

+ +

图论专题

+

dfs 与 bfs 区别

+
    +
  • +

    dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。

    +
  • +
  • +

    bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。

    +
  • +
+

深度优先搜索理论基础

+

回溯算法,其实就是dfs的过程,这里给出dfs的代码框架:

+
void dfs(参数) {
+    if (终止条件) {
+        存放结果;
+        return;
+    }
+
+    for (选择:本节点所连接的其他节点) {
+        处理节点;
+        dfs(图,选择的节点); // 递归
+        回溯,撤销处理结果
+    }
+}
+

可以发现dfs的代码框架和回溯算法的代码框架是差不多的。

+

深搜三部曲如下:

+
    +
  1. 确认递归函数,参数
  2. +
+
void dfs(参数)
+

通常我们递归的时候,我们递归搜索需要了解哪些参数,其实也可以在写递归函数的时候,发现需要什么参数,再去补充就可以。

+

一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。

+

例如这样:

+
vector<vector<int>> result; // 保存符合条件的所有路径
+vector<int> path; // 起点到终点的路径
+void dfs (图,目前搜索的节点) 
+
    +
  1. 确认终止条件
  2. +
+

终止条件很重要,写dfs的时候,之所以容易死循环,栈溢出等等这些问题,都是因为终止条件没有想清楚。

+
if (终止条件) {
+    存放结果;
+    return;
+}
+

终止添加不仅是结束本层递归,同时也是我们收获结果的时候。

+

另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。

+
    +
  1. 处理目前搜索节点出发的路径
  2. +
+

一般这里就是一个for循环的操作,去遍历 目前搜索节点 所能到的所有节点。

+
for (选择:本节点所连接的其他节点) {
+    处理节点;
+    dfs(图,选择的节点); // 递归
+    回溯,撤销处理结果
+}
+

广度优先搜索理论基础

+

广搜的搜索方式就适合于解决两个点之间的最短路径问题。

+

因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。

+

当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行

+

我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,那么用队列,还是用栈,甚至用数组,都是可以的。

+

用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针。

+

因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。

+

如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历。

+

因为栈是先进后出,加入元素和弹出元素的顺序改变了。

+

下面给出广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释)

+
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
+// grid 是地图,也就是一个二维数组
+// visited标记访问过的节点,不要重复访问
+// x,y 表示开始搜索节点的下标
+void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
+    queue<pair<int, int>> que; // 定义队列
+    que.push({x, y}); // 起始节点加入队列
+    visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
+    while(!que.empty()) { // 开始遍历队列里的元素
+        pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
+        int curx = cur.first;
+        int cury = cur.second; // 当前节点坐标
+        for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
+            int nextx = curx + dir[i][0];
+            int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
+            if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;  // 坐标越界了,直接跳过
+            if (!visited[nextx][nexty]) { // 如果节点没被访问过
+                que.push({nextx, nexty});  // 队列添加该节点为下一轮要遍历的节点
+                visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
+            }
+        }
+    }
+}
+

并查集理论基础

+

当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。

+

并查集主要有两个功能:

+
    +
  • 将两个元素添加到一个集合中。
  • +
  • 判断两个元素在不在同一个集合
  • +
+
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
+vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
+
+// 并查集初始化
+void init() {
+    for (int i = 0; i < n; ++i) {
+        father[i] = i;
+    }
+}
+// 并查集里寻根的过程
+int find(int u) {
+    return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
+}
+
+// 判断 u 和 v是否找到同一个根
+bool isSame(int u, int v) {
+    u = find(u);
+    v = find(v);
+    return u == v;
+}
+
+// 将v->u 这条边加入并查集
+void join(int u, int v) {
+    u = find(u); // 寻找u的根
+    v = find(v); // 寻找v的根
+    if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
+    father[v] = u;
+}
+

通过模板,我们可以知道,并查集主要有三个功能。

+
    +
  • 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
  • +
  • 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
  • +
  • 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
  • +
+

797. 所有可能的路径

+

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)

+

graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。

+
class Solution {
+private:
+    vector<vector<int> > result;
+    vector<int> path;
+public:
+    void DFS(vector<vector<int>>& graph, int node){
+        if(node == graph.size()-1){
+            result.push_back(path);
+            return;
+        }
+        for(int i=0;i<graph[node].size();i++){
+            path.push_back(graph[node][i]);
+            DFS(graph, graph[node][i]);
+            path.pop_back();
+        }
+    }
+    vector<vector<int> > allPathsSourceTarget(vector<vector<int>>& graph) {
+        path.push_back(0);
+        DFS(graph,0);
+        return result;
+    }
+};
+

200. 岛屿数量

+

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

+

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

+

此外,你可以假设该网格的四条边均被水包围。

+
class Solution {
+private:
+    vector<vector<bool> > isvisit;
+    int sumcount = 0;
+public:
+    void DFS(vector<vector<char>>& grid, int i, int j, int m, int n){
+        if(i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == '0' || isvisit[i][j] == true){
+            return;
+        }
+        isvisit[i][j] = true;
+        DFS(grid,i-1,j,m,n);
+        DFS(grid,i,j-1,m,n);
+        DFS(grid,i+1,j,m,n);
+        DFS(grid,i,j+1,m,n);
+    }
+    void BFS(vector<vector<char>>& grid, int i, int j, int m, int n){
+        queue<pair<int, int> > q;
+        q.push({i,j});
+        isvisit[i][j] = true;
+        while(!q.empty()){
+            pair<int, int> cur = q.front();
+            q.pop();
+            int x = cur.first;
+            int y = cur.second;
+            if(x-1 >= 0 && grid[x-1][y] == '1' && isvisit[x-1][y] == false){
+                q.push({x-1,y});
+                isvisit[x-1][y] = true;
+            }
+            if(x+1 < m && grid[x+1][y] == '1' && isvisit[x+1][y] == false){
+                q.push({x+1,y});
+                isvisit[x+1][y] = true;
+            }
+            if(y-1 >= 0 && grid[x][y-1] == '1' && isvisit[x][y-1] == false){
+                q.push({x,y-1});
+                isvisit[x][y-1] = true;
+            }
+            if(y+1 < n && grid[x][y+1] == '1' && isvisit[x][y+1] == false){
+                q.push({x,y+1});
+                isvisit[x][y+1] = true;
+            }
+        }
+        return;
+    }
+    int numIslands(vector<vector<char>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        for(int i=0;i<m;i++){
+            isvisit.push_back(vector<bool>(n));
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(isvisit[i][j] == true || grid[i][j] == '0'){
+                    continue;
+                }
+                BFS(grid, i,j,m,n);
+                sumcount += 1;
+            }
+        }
+        return sumcount;
+    }
+};
+

695. 岛屿的最大面积

+

给你一个大小为 m x n 的二进制矩阵 grid 。

+

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

+

岛屿的面积是岛上值为 1 的单元格的数目。

+

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

+
class Solution {
+private:
+    vector<vector<bool> > isvisit;
+    int result = 0;
+public:
+    void BFS(vector<vector<int>>& grid, int i, int j, int m, int n, int nowsum){
+        queue<pair<int,int> > q;
+        q.push({i,j});
+        isvisit[i][j] = true;
+        while(!q.empty()){
+            pair<int,int> cur = q.front();
+            nowsum += 1;
+            q.pop();
+            int x = cur.first;
+            int y = cur.second;
+            if(x-1 >= 0 && grid[x-1][y] == 1 && isvisit[x-1][y] == false){
+                isvisit[x-1][y] = true;
+                q.push({x-1,y});
+            }
+            if(x+1 < m && grid[x+1][y] == 1 && isvisit[x+1][y] == false){
+                isvisit[x+1][y] = true;
+                q.push({x+1,y});
+            }
+            if(y-1 >= 0 && grid[x][y-1] == 1 && isvisit[x][y-1] == false){
+                isvisit[x][y-1] = true;
+                q.push({x,y-1});
+            }
+            if(y+1 < n && grid[x][y+1] == 1 && isvisit[x][y+1] == false){
+                isvisit[x][y+1] = true;
+                q.push({x,y+1});
+            }
+        }
+        result = max(result,nowsum);
+    }
+    int maxAreaOfIsland(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        for(int i=0;i<m;i++){
+            isvisit.push_back(vector<bool>(n));
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(grid[i][j] == 1 && isvisit[i][j] == false){
+                    BFS(grid,i,j,m,n,0);
+                }
+            }
+        }
+        return result;
+    }
+};
+

1020. 飞地的数量

+

给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。

+

一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。

+

返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。

+
class Solution {
+private:
+    vector<vector<bool> > isvisit;
+    int result = 0;
+public:
+    void BFS(vector<vector<int>>& grid, int i, int j, int m, int n, int nowsum, int mode){
+        queue<pair<int,int> > q;
+        q.push({i,j});
+        isvisit[i][j] = true;
+        while(!q.empty()){
+            pair<int,int> cur = q.front();
+            nowsum += 1;
+            q.pop();
+            int x = cur.first;
+            int y = cur.second;
+            if(x-1 >= 0 && grid[x-1][y] == 1 && isvisit[x-1][y] == false){
+                isvisit[x-1][y] = true;
+                q.push({x-1,y});
+            }
+            if(x+1 < m && grid[x+1][y] == 1 && isvisit[x+1][y] == false){
+                isvisit[x+1][y] = true;
+                q.push({x+1,y});
+            }
+            if(y-1 >= 0 && grid[x][y-1] == 1 && isvisit[x][y-1] == false){
+                isvisit[x][y-1] = true;
+                q.push({x,y-1});
+            }
+            if(y+1 < n && grid[x][y+1] == 1 && isvisit[x][y+1] == false){
+                isvisit[x][y+1] = true;
+                q.push({x,y+1});
+            }
+        }
+        if(mode == 0){
+            return;
+        }
+
+        result  += nowsum;
+    }
+    int numEnclaves(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        for(int i=0;i<m;i++){
+            isvisit.push_back(vector<bool>(n));
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(i != 0 && j != 0 && i != m-1 && j != n-1){
+                    continue;
+                }
+                if(grid[i][j] == 1){
+                    BFS(grid,i,j,m,n,0,0);
+                }
+            }
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(i == 0 || j == 0 || i == m-1 || j == n-1){
+                    continue;
+                }
+                if(grid[i][j] == 1 && isvisit[i][j] == false){
+                    BFS(grid,i,j,m,n,0,1);
+                }
+            }
+        }
+        return result;
+    }
+};
+

1254. 统计封闭岛屿的数目

+

二维矩阵 grid 由 0 (土地)和 1 (水)组成。岛是由最大的4个方向连通的 0 组成的群,封闭岛是一个 完全 由1包围(左、上、右、下)的岛。

+

请返回 封闭岛屿 的数目。

+
class Solution {
+private:
+    vector<vector<bool> > isvisit;
+public:
+    void BFS(vector<vector<int>>& grid, int i, int j, int m, int n){
+        queue<pair<int,int> > q;
+        q.push({i,j});
+        isvisit[i][j] = true;
+        while(!q.empty()){
+            pair<int,int> cur = q.front();
+            q.pop();
+            int x = cur.first;
+            int y = cur.second;
+            if(x-1 >= 0 && grid[x-1][y] == 0 && isvisit[x-1][y] == false){
+                isvisit[x-1][y] = true;
+                q.push({x-1,y});
+            }
+            if(x+1 < m && grid[x+1][y] == 0 && isvisit[x+1][y] == false){
+                isvisit[x+1][y] = true;
+                q.push({x+1,y});
+            }
+            if(y-1 >= 0 && grid[x][y-1] == 0 && isvisit[x][y-1] == false){
+                isvisit[x][y-1] = true;
+                q.push({x,y-1});
+            }
+            if(y+1 < n && grid[x][y+1] == 0 && isvisit[x][y+1] == false){
+                isvisit[x][y+1] = true;
+                q.push({x,y+1});
+            }
+        }
+    }
+    int closedIsland(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        for(int i=0;i<m;i++){
+            isvisit.push_back(vector<bool>(n));
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(i != 0 && j != 0 && i != m-1 && j != n-1){
+                    continue;
+                }
+                if(grid[i][j] == 0){
+                    BFS(grid,i,j,m,n);
+                }
+            }
+        }
+        int result = 0;
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(grid[i][j] == 0 && isvisit[i][j] == false){
+                    BFS(grid,i,j,m,n);
+                    result += 1;
+                }
+            }
+        }
+        return result;
+    }
+};
+

130. 被围绕的区域

+

给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ ,找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。

+
class Solution {
+private:
+    vector<vector<bool> > isvisit;
+public:
+    void BFS(vector<vector<char>>& board, int i, int j, int m, int n){
+        queue<pair<int,int> > q;
+        q.push({i,j});
+        isvisit[i][j] = true;
+        while(!q.empty()){
+            pair<int,int> cur = q.front();
+            q.pop();
+            int x = cur.first;
+            int y = cur.second;
+            if(x-1 >= 0 && board[x-1][y] == 'O' && isvisit[x-1][y] == false){
+                isvisit[x-1][y] = true;
+                q.push({x-1,y});
+            }
+            if(x+1 < m && board[x+1][y] == 'O' && isvisit[x+1][y] == false){
+                isvisit[x+1][y] = true;
+                q.push({x+1,y});
+            }
+            if(y-1 >= 0 && board[x][y-1] == 'O' && isvisit[x][y-1] == false){
+                isvisit[x][y-1] = true;
+                q.push({x,y-1});
+            }
+            if(y+1 < n && board[x][y+1] == 'O' && isvisit[x][y+1] == false){
+                isvisit[x][y+1] = true;
+                q.push({x,y+1});
+            }
+        }
+    }
+    void solve(vector<vector<char>>& board) {
+        int m = board.size();
+        int n = board[0].size();
+        for(int i=0;i<m;i++){
+            isvisit.push_back(vector<bool>(n));
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(i != 0 && i != m-1 && j != 0 && j != n-1){
+                    continue;
+                }
+                if(board[i][j] == 'O' && isvisit[i][j] == false){
+                    BFS(board,i,j,m,n);
+                }
+            }
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(board[i][j] == 'O' && isvisit[i][j] == false){
+                    board[i][j] = 'X';
+                }
+            }
+        }
+        return;
+    }
+};
+

417. 太平洋大西洋水流问题

+

有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。

+

这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。

+

岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

+

返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。

+
class Solution {
+private:
+    vector<vector<bool> > visit_p;
+    vector<vector<bool> > visit_a;
+public:
+    void BFS(vector<vector<int> >& heights, vector<vector<bool> >& visit, int i, int j, int m, int n){
+        visit[i][j] = true;
+        queue<pair<int, int> > q;
+        q.push({i,j});
+        while(!q.empty()){
+            pair<int, int> cur = q.front();
+            q.pop();
+            int x = cur.first;
+            int y = cur.second;
+            if(x-1 >= 0 && visit[x-1][y] == false && heights[x-1][y] >= heights[x][y]){
+                visit[x-1][y] = true;
+                q.push({x-1,y});
+            }
+            if(x+1 < m && visit[x+1][y] == false && heights[x+1][y] >= heights[x][y]){
+                visit[x+1][y] = true;
+                q.push({x+1,y});
+            }
+            if(y-1 >= 0 && visit[x][y-1] == false && heights[x][y-1] >= heights[x][y]){
+                visit[x][y-1] = true;
+                q.push({x,y-1});
+            }
+            if(y+1 < n && visit[x][y+1] == false && heights[x][y+1] >= heights[x][y]){
+                visit[x][y+1] = true;
+                q.push({x,y+1});
+            }
+        }
+    }
+    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
+        int m = heights.size();
+        int n = heights[0].size();
+        for(int i=0;i<m;i++){
+            visit_p.push_back(vector<bool>(n));
+            visit_a.push_back(vector<bool>(n));
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(i == 0 || j == 0){
+                    BFS(heights, visit_p, i,j,m,n);
+                }
+                if(i == m-1 || j == n-1){
+                    BFS(heights, visit_a, i,j,m,n);
+                }
+            }
+        }
+        vector<vector<int> > result;
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(visit_p[i][j] == true && visit_a[i][j] == true){
+                    result.push_back(vector<int>{i,j});
+                }
+            }
+        }
+        return result;
+    }
+};
+

827. 最大人工岛

+

给你一个大小为 n x n 二进制矩阵 grid 。最多 只能将一格 0 变成 1 。

+

返回执行此操作后,grid 中最大的岛屿面积是多少?

+

岛屿 由一组上、下、左、右四个方向相连的 1 形成。

+
class Solution {
+private:
+    int numindex = 2;
+    unordered_map<int, int> mp;
+public:
+    void BFS(vector<vector<int>>& grid, int i, int j, int n, int numindex){
+        queue<pair<int, int> > q;
+        q.push({i,j});
+        int nowcount = 1;
+        grid[i][j] = numindex;
+        while(!q.empty()){
+            pair<int, int> cur = q.front();
+            q.pop();
+            int x = cur.first;
+            int y = cur.second;
+            if(x-1 >= 0 && grid[x-1][y] == 1){
+                q.push({x-1,y});
+                nowcount += 1;
+                grid[x-1][y] = numindex;
+            }
+            if(y-1 >= 0 && grid[x][y-1] == 1){
+                q.push({x,y-1});
+                nowcount += 1;
+                grid[x][y-1] = numindex;
+            }
+            if(x+1 < n && grid[x+1][y] == 1){
+                q.push({x+1,y});
+                nowcount += 1;
+                grid[x+1][y] = numindex;
+            }
+            if(y+1 < n && grid[x][y+1] == 1){
+                q.push({x,y+1});
+                nowcount += 1;
+                grid[x][y+1] = numindex;
+            }
+        }
+        mp[numindex] = nowcount;
+    }
+    int largestIsland(vector<vector<int>>& grid) {
+        int n = grid.size();
+        for(int i=0;i<n;i++){
+            for(int j=0;j<n;j++){
+                if(grid[i][j] == 1){
+                    BFS(grid,i,j,n,numindex);
+                    numindex += 1;
+                }
+            }
+        }
+        mp[0] = 0;
+        int maxarea = 0;
+        for(int i=0;i<n;i++){
+            for(int j=0;j<n;j++){
+                int area = 0;
+                if(grid[i][j] == 0){
+                    area += 1;
+                }
+                unordered_map<int,bool> mp2;
+                mp2[grid[i][j]] = true;
+                area += mp[grid[i][j]];
+                if(i-1 >= 0 && mp2.find(grid[i-1][j]) == mp2.end()){
+                    mp2[grid[i-1][j]] = true;
+                    area += mp[grid[i-1][j]];
+                }
+                if(j-1 >= 0 && mp2.find(grid[i][j-1]) == mp2.end()){
+                    mp2[grid[i][j-1]] = true;
+                    area += mp[grid[i][j-1]];
+                }
+                if(i+1 < n && mp2.find(grid[i+1][j]) == mp2.end()){
+                    mp2[grid[i+1][j]] = true;
+                    area += mp[grid[i+1][j]];
+                }
+                if(j+1 < n && mp2.find(grid[i][j+1]) == mp2.end()){
+                    mp2[grid[i][j+1]] = true;
+                    area += mp[grid[i][j+1]];
+                }
+                maxarea = max(maxarea, area);
+            }
+        }
+        return maxarea;
+    }
+};
+

127. 单词接龙

+

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:

+

序列中第一个单词是 beginWord 。

+

序列中最后一个单词是 endWord 。

+

每次转换只能改变一个字母。

+

转换过程中的中间单词必须是字典 wordList 中的单词。

+

给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。

+
class Solution {
+private:
+    int mincount = 0;
+public:
+    bool judgetwowords(string word1, string word2){
+        int sumcount = 0;
+        for(int i=0;i<word1.size();i++){
+            if(word1[i] != word2[i]){
+                sumcount += 1;
+            }
+            if(sumcount >= 2){
+                return false;
+            }
+        }
+        if(sumcount == 1){
+            return true;
+        }
+        return false;
+    }
+    int BFS(vector<vector<int> >& grid, int start, int end){
+        unordered_map<int, bool> visitMap;
+        visitMap[start] = true;
+        queue<int> q;
+        q.push(start);
+        int nowcount = 0;
+        while(!q.empty()){
+            int t = q.size();
+            nowcount += 1;
+            // cout << nowcount << " " << t << endl;
+            for(int j=0;j<t;j++){
+                int cur = q.front();
+                q.pop();
+                if(cur == end){
+                    return nowcount;
+                }
+                for(int i=0;i<grid[cur].size();i++){
+                    if(visitMap.find(grid[cur][i]) == visitMap.end()){
+                        visitMap[grid[cur][i]] = true;
+                        q.push(grid[cur][i]);
+                    }
+                }
+            }
+        }
+        return 0;
+    }
+    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
+        int sign = -1;
+        for(int i=0;i<wordList.size();i++){
+            if(wordList[i] == endWord){
+                sign = i+1;
+            }
+        }
+        if(sign == -1){
+            return 0;
+        }
+        vector<vector<int> > grid;
+        vector<int> tempgrid;
+        for(int i=0;i<wordList.size();i++){
+            if(judgetwowords(wordList[i], beginWord)){
+                // cout << i << "fgdgf" << endl;
+                tempgrid.push_back(i+1);
+            }
+        }
+        grid.push_back(tempgrid);
+        for(int i=0;i<wordList.size();i++){
+            vector<int> tempgrid2;
+            // tempgrid2.push_back(i+2);
+            for(int j=0;j<wordList.size();j++){
+                if(i+1 == j+1){
+                    continue;
+                }
+                if(judgetwowords(wordList[i], wordList[j])){
+                    tempgrid2.push_back(j+1);
+                }
+            }
+            grid.push_back(tempgrid2);
+        }
+        // for(int i=0;i<grid.size();i++){
+        //     for(int j=0;j<grid[i].size();j++){
+        //         cout << grid[i][j] << " ";
+        //     }
+        //     cout << endl;
+        // }
+        int mincount = BFS(grid, 0, sign);
+        return mincount;
+    }
+};
+

841. 钥匙和房间

+

有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,…,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。

+

在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i][j] 由 [0,1,…,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms[i][j] = v 可以打开编号为 v 的房间。

+

最初,除 0 号房间外的其余所有房间都被锁住。

+

你可以自由地在房间之间来回走动。

+

如果能进入每个房间返回 true,否则返回 false。

+
class Solution {
+public:
+    bool canVisitAllRooms(vector<vector<int>>& rooms) {
+        int lenrooms = rooms.size();
+        vector<bool> isvisit(lenrooms);
+        queue<int> q;
+        q.push(0);
+        isvisit[0] = true;
+        while(!q.empty()){
+            int t = q.front();
+            q.pop();
+            for(int i=0;i<rooms[t].size();i++){
+                if (isvisit[rooms[t][i]] == false){
+                    isvisit[rooms[t][i]] = true;
+                    q.push(rooms[t][i]);
+                }
+            }
+        }
+        for(int i=0;i<lenrooms;i++){
+            if(isvisit[i] == false){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

463. 岛屿的周长

+

给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。

+

网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。

+

岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

+
class Solution {
+public:
+    int islandPerimeter(vector<vector<int>>& grid) {
+        int result = 0;
+        int m = grid.size();
+        int n = grid[0].size();
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(grid[i][j] == 1){
+                    result += 4;
+                    if(i-1 >= 0 && grid[i-1][j] == 1){
+                        result -= 1;
+                    }
+                    if(j-1 >= 0 && grid[i][j-1] == 1){
+                        result -= 1;
+                    }
+                    if(i+1 < m && grid[i+1][j] == 1){
+                        result -= 1;
+                    }
+                    if(j+1 < n && grid[i][j+1] == 1){
+                        result -= 1;
+                    }
+                }
+            }
+        }
+        return result;
+    }
+};
+

1971. 寻找图中是否存在路径

+

有一个具有 n个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。

+

请你确定是否存在从顶点 start 开始,到顶点 end 结束的 有效路径 。

+

给你数组 edges 和整数 n、start和end,如果从 start 到 end 存在 有效路径 ,则返回 true,否则返回 false 。

+
class Solution {
+private:
+    vector<int> father;
+public:
+    int findfather(int a){
+        if(a == father[a]){
+            return a;
+        }
+        return father[a] = findfather(father[a]);
+    }
+    void join(int a, int b){
+        a = findfather(a);
+        b = findfather(b);
+        if(a == b){
+            return;
+        }
+        father[a] = b;
+    }
+    bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
+        for(int i=0;i<n;i++){
+            father.push_back(i);
+        }
+        for(int i=0;i<edges.size();i++){
+            join(edges[i][0], edges[i][1]);
+        }
+        if(findfather(source) == findfather(destination)){
+            return true;
+        }
+        return false;
+    }
+};
+

684. 冗余连接

+

树可以看成是一个连通且 无环 的 无向 图。

+

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。

+

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。

+
class Solution {
+private:
+    vector<int> father;
+public:
+    int findfather(int a){
+        return a == father[a] ? a : father[a] = findfather(father[a]);
+    }
+    void join(int a, int b){
+        a = findfather(a);
+        b = findfather(b);
+        if(a == b){
+            return;
+        }
+        father[a] = b;
+    }
+    bool issamefather(int a, int b){
+        a = findfather(a);
+        b = findfather(b);
+        if(a == b){
+            return true;
+        }
+        return false;
+    }
+    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
+        int n = edges.size();
+        for(int i=0;i<n+1;i++){
+            father.push_back(i);
+        }
+        for(int i=0;i<n;i++){
+            int x = edges[i][0];
+            int y = edges[i][1];
+            if(issamefather(x,y)){
+                return edges[i];
+            }
+            join(x,y);
+        }
+        return edges[0];
+    }
+};
+

685. 冗余连接II

+

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

+

输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。

+

结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。

+

返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

+

这个不会

+
+ + +
+ +
+
+ + + + + + +
+
+
代码随想录-图论专题
+
https://zhangzhao219.github.io/2024/03/07/Leetcode/programmercarl/programmercarl-gt/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月7日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/11/Leetcode/programmercarl/programmercarl-bt/index.html b/2024/03/11/Leetcode/programmercarl/programmercarl-bt/index.html new file mode 100644 index 000000000..78651b5ec --- /dev/null +++ b/2024/03/11/Leetcode/programmercarl/programmercarl-bt/index.html @@ -0,0 +1,3041 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-二叉树 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-二叉树

+ + +
+ +

代码随想录-二叉树

+ +

二叉树

+

理论基础

+
type TreeNode struct {
+    Val int
+    Left *TreeNode
+    Right *TreeNode
+}
+

二叉树的递归遍历

+

确定递归函数的参数和返回值 :因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void

+

确定终止条件 :在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return

+

确定单层递归的逻辑 :前序遍历是中左右的顺序,所以在单层递归的逻辑,是要先取中节点的数值

+
func preorderTraversal(root *TreeNode) (res []int) {
+    var traversal func(node *TreeNode)
+    traversal = func(node *TreeNode) {
+	if node == nil {
+            return
+	}
+	res = append(res,node.Val)
+	traversal(node.Left)
+	traversal(node.Right)
+    }
+    traversal(root)
+    return res
+}
+

二叉树的迭代遍历

+

将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。

+

如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。

+

前序遍历统一迭代法:

+
 /**
+ type Element struct {
+    // 元素保管的值
+    Value interface{}
+    // 内含隐藏或非导出字段
+}
+
+func (l *List) Back() *Element 
+前序遍历:中左右
+压栈顺序:右左中
+ **/
+func preorderTraversal(root *TreeNode) []int {
+	if root == nil {
+		return nil
+	}
+	var stack = list.New()//栈
+    res:=[]int{}//结果集
+    stack.PushBack(root)
+    var node *TreeNode
+    for stack.Len()>0{
+        e := stack.Back()
+        stack.Remove(e)//弹出元素
+        if e.Value==nil{// 如果为空,则表明是需要处理中间节点
+            e=stack.Back()//弹出元素(即中间节点)
+            stack.Remove(e)//删除中间节点
+            node=e.Value.(*TreeNode)
+            res=append(res,node.Val)//将中间节点加入到结果集中
+            continue//继续弹出栈中下一个节点
+        }
+        node = e.Value.(*TreeNode)
+        //压栈顺序:右左中
+        if node.Right!=nil{
+            stack.PushBack(node.Right)
+        }
+        if node.Left!=nil{
+            stack.PushBack(node.Left)
+        }
+        stack.PushBack(node)//中间节点压栈后再压入nil作为中间节点的标志符
+        stack.PushBack(nil)
+    }
+    return res
+
+}
+

中序遍历统一迭代法:

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+ //中序遍历:左中右
+ //压栈顺序:右中左
+func inorderTraversal(root *TreeNode) []int {
+    if root==nil{
+       return nil
+    }
+    stack:=list.New()//栈
+    res:=[]int{}//结果集
+    stack.PushBack(root)
+    var node *TreeNode
+    for stack.Len()>0{
+        e := stack.Back()
+        stack.Remove(e)
+        if e.Value==nil{// 如果为空,则表明是需要处理中间节点
+            e=stack.Back()//弹出元素(即中间节点)
+            stack.Remove(e)//删除中间节点
+            node=e.Value.(*TreeNode)
+            res=append(res,node.Val)//将中间节点加入到结果集中
+            continue//继续弹出栈中下一个节点
+        }
+        node = e.Value.(*TreeNode)
+        //压栈顺序:右中左
+        if node.Right!=nil{
+            stack.PushBack(node.Right)
+        }
+        stack.PushBack(node)//中间节点压栈后再压入nil作为中间节点的标志符
+        stack.PushBack(nil)
+        if node.Left!=nil{
+            stack.PushBack(node.Left)
+        }
+    }
+    return res
+}
+

后序遍历统一迭代法:

+
//后续遍历:左右中
+//压栈顺序:中右左
+func postorderTraversal(root *TreeNode) []int {
+	if root == nil {
+		return nil
+	}
+	var stack = list.New()//栈
+    res:=[]int{}//结果集
+    stack.PushBack(root)
+    var node *TreeNode
+    for stack.Len()>0{
+        e := stack.Back()
+        stack.Remove(e)
+        if e.Value==nil{// 如果为空,则表明是需要处理中间节点
+            e=stack.Back()//弹出元素(即中间节点)
+            stack.Remove(e)//删除中间节点
+            node=e.Value.(*TreeNode)
+            res=append(res,node.Val)//将中间节点加入到结果集中
+            continue//继续弹出栈中下一个节点
+        }
+        node = e.Value.(*TreeNode)
+        //压栈顺序:中右左
+        stack.PushBack(node)//中间节点压栈后再压入nil作为中间节点的标志符
+        stack.PushBack(nil)
+        if node.Right!=nil{
+            stack.PushBack(node.Right)
+        }
+        if node.Left!=nil{
+            stack.PushBack(node.Left)
+        }
+    }
+    return res
+}
+

二叉树层序遍历

+
/**
+102. 二叉树的层序遍历
+ */
+func levelOrder(root *TreeNode) [][]int {
+    res:=[][]int{}
+    if root==nil{//防止为空
+        return res
+    }
+    queue:=list.New()
+    queue.PushBack(root)
+    var tmpArr []int
+    for queue.Len()>0 {
+        length:=queue.Len()//保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数)
+        for i:=0;i<length;i++{
+            node:=queue.Remove(queue.Front()).(*TreeNode)//出队列
+            if node.Left!=nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right!=nil{
+                queue.PushBack(node.Right)
+            }
+            tmpArr=append(tmpArr,node.Val)//将值加入本层切片中
+        }
+        res=append(res,tmpArr)//放入结果集
+        tmpArr=[]int{}//清空层的数据
+    }
+    return res
+}
+

102. 二叉树的层序遍历

+

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func levelOrder(root *TreeNode) [][]int {
+    result := make([][]int,0)
+    if root == nil{
+        return result
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    for queue.Len() > 0{
+        length := queue.Len()
+        temp := make([]int,0)
+        for i:=0;i<length;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+            temp = append(temp,node.Val)
+        }
+        result = append(result,temp)
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<vector<int>> levelOrder(TreeNode* root) {
+        vector<vector<int> > result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            vector<int> temp;
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                temp.push_back(t->val);
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+            result.push_back(temp);
+        }
+        return result;
+    }
+};
+

107. 二叉树的层次遍历II

+

给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func levelOrderBottom(root *TreeNode) [][]int {
+    result := make([][]int,0)
+    if root == nil{
+        return result
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    for queue.Len() > 0{
+        templen := queue.Len()
+        tempArr := make([]int,0)
+        for i:=0;i<templen;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+            tempArr = append(tempArr,node.Val)
+        }
+        result = append([][]int{tempArr},result...)
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<vector<int>> levelOrderBottom(TreeNode* root) {
+        vector<vector<int> > result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            vector<int> temp;
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                temp.push_back(t->val);
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+            result.push_back(temp);
+        }
+        vector<vector<int> > acresult;
+        for(int i=result.size()-1;i>=0;i--){
+            acresult.push_back(result[i]);
+        }
+        return acresult;
+    }
+};
+

199. 二叉树的右视图

+

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func rightSideView(root *TreeNode) []int {
+    result := make([]int,0)
+    if root == nil{
+        return result
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    for queue.Len() > 0{
+        tempLen := queue.Len()
+        for i:=0;i<tempLen;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+            if i == tempLen-1{
+                result = append(result,node.Val)
+            }
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<int> rightSideView(TreeNode* root) {
+        vector<int> result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                if(i == s-1){
+                    result.push_back(t->val);
+                }
+
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+        }
+        return result;
+    }
+};
+

637. 二叉树的层平均值

+

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func averageOfLevels(root *TreeNode) []float64 {
+    result := make([]float64,0)
+    if root == nil{
+        return result
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    for queue.Len() > 0{
+        tempLen := queue.Len()
+        sum := 0
+        for i:=0;i<tempLen;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+            sum += node.Val
+        }
+        result = append(result,float64(sum)/float64(tempLen))
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<double> averageOfLevels(TreeNode* root) {
+        vector<double> result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            double nowsum = 0.0;
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                nowsum += (double)t->val;
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+            nowsum /= (double)s;
+            result.push_back(nowsum);
+        }
+        return result;
+    }
+};
+

429. N叉树的层序遍历

+

给定一个 N 叉树,返回其节点值的 层序遍历 。(即从左到右,逐层遍历)。

+

树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

+
/**
+ * Definition for a Node.
+ * type Node struct {
+ *     Val int
+ *     Children []*Node
+ * }
+ */
+
+func levelOrder(root *Node) [][]int {
+    result := make([][]int,0)
+    if root == nil{
+        return result
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    for queue.Len() > 0{
+        length := queue.Len()
+        temp := make([]int,0)
+        for i:=0;i<length;i++{
+            node := queue.Remove(queue.Front()).(*Node)
+            for j:=0;j<len(node.Children);j++{
+                if node.Children[j] != nil{
+                    queue.PushBack(node.Children[j])
+                }
+            }
+            temp = append(temp,node.Val)
+        }
+        result = append(result,temp)
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<vector<int>> levelOrder(Node* root) {
+        vector<vector<int> > result;
+        if(root == NULL){
+            return result;
+        }
+        queue<Node*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            vector<int> temp;
+            for(int i=0;i<s;i++){
+                Node* t = q.front();
+                q.pop();
+                temp.push_back(t->val);
+                for(int i=0;i<t->children.size();i++){
+                    if(t->children[i] != NULL){
+                        q.push(t->children[i]);
+                    }
+                }
+            }
+            result.push_back(temp);
+        }
+        return result;
+    }
+};
+

515. 在每个树行中找最大值

+

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func largestValues(root *TreeNode) []int {
+    result := make([]int,0)
+    if root == nil{
+        return result
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    for queue.Len() > 0{
+        maxVal := -1024*1024*1024*2
+        tempLen := queue.Len()
+        for i:=0;i<tempLen;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            maxVal = max(maxVal,node.Val)
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+        }
+        result = append(result,maxVal)
+    }
+    return result
+}
+
+func max(a,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    vector<int> largestValues(TreeNode* root) {
+        vector<int> result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            int maxnode = q.front()->val;
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                maxnode = max(maxnode,t->val);
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+            result.push_back(maxnode);
+        }
+        return result;
+    }
+};
+

116. 填充每个节点的下一个右侧节点指针

+

给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。

+

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

+

初始状态下,所有 next 指针都被设置为 NULL

+
/**
+ * Definition for a Node.
+ * type Node struct {
+ *     Val int
+ *     Left *Node
+ *     Right *Node
+ *     Next *Node
+ * }
+ */
+
+func connect(root *Node) *Node {
+    queue := []*Node{root}
+    if root == nil{
+        return root
+    }
+    for len(queue) > 0{
+        tempLen := len(queue)
+        for i:=0;i<tempLen;i++{
+            node := queue[0]
+            queue = queue[1:]
+            if i != tempLen-1{
+                node.Next = queue[0]  
+            }
+            if node.Left != nil{
+                queue = append(queue,node.Left)
+            }
+            if node.Right != nil{
+                queue = append(queue,node.Right)
+            }
+        }
+    }
+    return root
+}
+
class Solution {
+public:
+    Node* connect(Node* root) {
+        if(root == NULL){
+            return root;
+        }
+        queue<Node*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            Node* pre = NULL;
+            for(int i=0;i<s;i++){
+                Node* t = q.front();
+                if(pre != NULL){
+                    pre->next = q.front();
+                }
+                pre = t;
+                q.pop();
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+        }
+        return root;
+    }
+};
+

117. 填充每个节点的下一个右侧节点指针II

+

给定一个二叉树,填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

+

初始状态下,所有 next 指针都被设置为 NULL

+
/**
+ * Definition for a Node.
+ * type Node struct {
+ *     Val int
+ *     Left *Node
+ *     Right *Node
+ *     Next *Node
+ * }
+ */
+
+func connect(root *Node) *Node {
+	    queue := []*Node{root}
+    if root == nil{
+        return root
+    }
+    for len(queue) > 0{
+        tempLen := len(queue)
+        for i:=0;i<tempLen;i++{
+            node := queue[0]
+            queue = queue[1:]
+            if i != tempLen-1{
+                node.Next = queue[0]  
+            }
+            if node.Left != nil{
+                queue = append(queue,node.Left)
+            }
+            if node.Right != nil{
+                queue = append(queue,node.Right)
+            }
+        }
+    }
+    return root
+}
+
class Solution {
+public:
+    Node* connect(Node* root) {
+        if(root == NULL){
+            return root;
+        }
+        queue<Node*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            Node* pre = NULL;
+            for(int i=0;i<s;i++){
+                Node* t = q.front();
+                if(pre != NULL){
+                    pre->next = q.front();
+                }
+                pre = t;
+                q.pop();
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+        }
+        return root; 
+    }
+};
+

104. 二叉树的最大深度

+

给定一个二叉树,找出其最大深度。

+

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

+

说明: 叶子节点是指没有子节点的节点。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func maxDepth(root *TreeNode) int {
+    if root == nil{
+        return 0
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    sum := 0
+    for queue.Len() > 0{
+        sum += 1
+        tempLen := queue.Len()
+        for i:=0;i<tempLen;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+        }
+    }
+    return sum
+}
+
class Solution {
+public:
+    int maxDepth(TreeNode* root) {
+        int result = 0;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+            result += 1;
+        }
+        return result;
+    }
+};
+

111. 二叉树的最小深度

+

给定一个二叉树,找出其最小深度。

+

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

+

说明: 叶子节点是指没有子节点的节点。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func minDepth(root *TreeNode) int {
+    if root == nil{
+        return 0
+    }
+    queue := list.New()
+    queue.PushBack(root)
+    level := 0
+    for queue.Len() > 0{
+        level += 1
+        tempLen := queue.Len()
+        for i:=0;i<tempLen;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+            if node.Left == nil && node.Right == nil{
+                return level
+            }
+        }
+    }
+    return level
+}
+
class Solution {
+public:
+    int minDepth(TreeNode* root) {
+        int result = 0;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                if(t->left == NULL && t->right == NULL){
+                    return result + 1;
+                }
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+            result += 1;
+        }
+        return result;
+    }
+};
+

226. 翻转二叉树

+

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func invertTree(root *TreeNode) *TreeNode {
+    if root == nil{
+        return root
+    }
+    DFS(root)
+    return root
+}
+
+func DFS(root *TreeNode){
+    if root == nil{
+        return
+    }
+    node := root.Left
+    root.Left = root.Right
+    root.Right = node
+    DFS(root.Left)
+    DFS(root.Right)
+}
+
class Solution {
+public:
+    TreeNode* invertTree(TreeNode* root) {
+        if(root == NULL){
+            return root;
+        }
+        swap(root->left, root->right);
+        invertTree(root->left);
+        invertTree(root->right);
+        return root;
+    }
+};
+

101. 对称二叉树

+

给你一个二叉树的根节点 root , 检查它是否轴对称。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func isSymmetric(root *TreeNode) bool {
+    return isSymmetricJudge(root.Left,root.Right)
+}
+
+func isSymmetricJudge(root1 *TreeNode,root2 *TreeNode) bool{
+    if root1 == nil && root2 == nil{
+        return true
+    } else if root1 == nil{
+        return false
+    } else if root2 == nil{
+        return false
+    } else{
+        if root1.Val == root2.Val{
+            return isSymmetricJudge(root1.Left,root2.Right) && isSymmetricJudge(root1.Right,root2.Left)
+        } else{
+            return false
+        }
+    }
+    return false
+}
+
class Solution {
+public:
+    bool DFS(TreeNode* left, TreeNode* right){
+        if(left == NULL && right == NULL){
+            return true;
+        }
+        if(left == NULL){
+            return false;
+        }
+        if(right == NULL){
+            return false;
+        }
+        if(left->val != right->val){
+            return false;
+        }
+        return DFS(left->left, right->right) && DFS(right->left, left->right);
+    }
+    bool isSymmetric(TreeNode* root) {
+        if(root == NULL){
+            return true;
+        }
+        return DFS(root->left, root->right);
+    }
+};
+

100. 相同的树

+

给你两棵二叉树的根节点 pq ,编写一个函数来检验这两棵树是否相同。

+

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

+

递归判断即可,注意留下一个true的值方便后续使用

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func isSameTree(p *TreeNode, q *TreeNode) bool {
+    if p == nil && q == nil{
+        return true
+    } else if p == nil{
+        return false
+    } else if q == nil{
+        return false
+    } else{
+        if p.Val == q.Val{
+            return isSameTree(p.Left,q.Left) && isSameTree(p.Right,q.Right)
+        }
+        return false
+    }
+    return false
+}
+
class Solution {
+public:
+    bool isSameTree(TreeNode* p, TreeNode* q) {
+        if(p == NULL && q == NULL){
+            return true;
+        }
+        if(p == NULL){
+            return false;
+        }
+        if(q == NULL){
+            return false;
+        }
+        if(p->val != q->val){
+            return false;
+        }
+        return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
+    }
+};
+

572. 另一个树的子树

+

给你两棵二叉树 rootsubRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false

+

二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func isSubtree(root *TreeNode, subRoot *TreeNode) bool {
+    if root == nil && subRoot == nil{
+        return true
+    } else if root == nil || subRoot == nil{
+        return false
+    }
+    return isSameTree(root,subRoot) || isSubtree(root.Left,subRoot) || isSubtree(root.Right,subRoot)
+}
+
+func isSameTree(p *TreeNode, q *TreeNode) bool {
+    if p == nil && q == nil{
+        return true
+    } else if p == nil{
+        return false
+    } else if q == nil{
+        return false
+    } else{
+        if p.Val == q.Val{
+            return isSameTree(p.Left,q.Left) && isSameTree(p.Right,q.Right)
+        }
+        return false
+    }
+    return false
+}
+
class Solution {
+public:
+    bool isSameTree(TreeNode* p, TreeNode* q) {
+        if(p == NULL && q == NULL){
+            return true;
+        }
+        if(p == NULL){
+            return false;
+        }
+        if(q == NULL){
+            return false;
+        }
+        if(p->val != q->val){
+            return false;
+        }
+        return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
+    }
+
+    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
+        if(root == NULL && subRoot == NULL){
+            return true;
+        }
+        if(root == NULL || subRoot == NULL){
+            return false;
+        }
+        return isSameTree(root,subRoot) || isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
+    }
+};
+

222. 完全二叉树的节点个数

+

给你一棵完全二叉树的根节点 root ,求出该树的节点个数。

+

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。

+

完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。

+

对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。

+

对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func countNodes(root *TreeNode) int {
+    if root == nil {
+        return 0
+    }
+    leftH, rightH := 0, 0
+    leftNode := root.Left
+    rightNode := root.Right
+    for leftNode != nil {
+        leftNode = leftNode.Left
+        leftH++
+    }
+    for rightNode != nil {
+        rightNode = rightNode.Right
+        rightH++
+    }
+    if leftH == rightH {
+        return (2 << leftH) - 1
+    }
+    return countNodes(root.Left) + countNodes(root.Right) + 1
+}
+
class Solution {
+public:
+    int countNodes(TreeNode* root) {
+        if(root == NULL){
+            return 0;
+        }
+        int leftCount = 0;
+        int rightCount = 0;
+        TreeNode* leftt = root->left;
+        TreeNode* rightt = root->right;
+        while(leftt != NULL){
+            leftCount += 1;
+            leftt = leftt->left;
+        }
+        while(rightt != NULL){
+            rightCount += 1;
+            rightt = rightt->right;
+        }
+        if(leftCount == rightCount){
+            return (2 << leftCount) - 1;
+        }
+        return countNodes(root->left)  + countNodes(root->right) + 1;
+    }
+};
+

110. 平衡二叉树

+

给定一个二叉树,判断它是否是高度平衡的二叉树。

+

写一个函数计算递归的深度,如果差值大于1,则直接返回-1标志位作为不满足条件,否则返回最大的深度确保后面计算的是最大的。

+

如果是空结点,说明左右子树都是没有的,直接返回即可。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func isBalanced(root *TreeNode) bool {
+    a := DFS(root)
+    if a == -1{
+        return false
+    }
+    return true
+}
+
+func DFS(root *TreeNode) int {
+    if root == nil{
+        return 0
+    }
+    leftheight := DFS(root.Left)
+    if leftheight == -1{
+        return -1
+    }
+    rightheight := DFS(root.Right)
+    if rightheight == -1{
+        return -1
+    }
+    if leftheight - rightheight == -1 || leftheight - rightheight == 0 || leftheight - rightheight == 1{
+        return 1+max(leftheight,rightheight)
+    }
+    return -1
+}
+
+func max(a,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int getHeight(TreeNode* root){
+        if(root == NULL){
+            return 0;
+        }
+        int left = getHeight(root->left);
+        if(left == -1){
+            return -1;
+        }
+        int right = getHeight(root->right);
+        if(right == -1){
+            return -1;
+        }
+        if(abs(right - left) <= 1){
+            return 1 + max(left, right);
+        }
+        return -1;
+    }
+    bool isBalanced(TreeNode* root) {
+        int a = getHeight(root);
+        if(a == -1){
+            return false;
+        }
+        return true;
+    }
+};
+

257. 二叉树的所有路径

+

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

+

叶子节点 是指没有子节点的节点。

+

与前面不同的是判断的时候要判断左右孩子的结点,不能只判断自己本身。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func binaryTreePaths(root *TreeNode) []string {
+    result := make([]string,0)
+    tempstring := ""
+    DFS(root,tempstring,&result)
+    return result
+}
+
+func DFS(root *TreeNode, tempstring string, result *[]string ){
+    if root.Left == nil && root.Right == nil{
+        tempstring += fmt.Sprintf("%d", root.Val)
+        *result = append(*result,tempstring)
+        return
+    }
+    tempstring += fmt.Sprintf("%d->", root.Val)
+    if root.Left != nil{
+        DFS(root.Left,tempstring,result)
+    }
+    if root.Right != nil{
+        DFS(root.Right,tempstring,result)
+    } 
+}
+
class Solution {
+private:
+    vector<string> result;
+public:
+    void DFS(TreeNode* root, vector<int> temp){
+        temp.push_back(root->val);
+        if(root->left == NULL && root->right == NULL){
+            string s = "";
+            for(int i=0;i<temp.size();i++){
+                if(i != temp.size()-1){
+                    s = s + to_string(temp[i]) + "->";
+                }
+                else{
+                    s = s + to_string(temp[i]);
+                }
+            }
+            result.push_back(s);
+            return;
+        }
+        if(root->left != NULL){
+            DFS(root->left, temp);
+        }
+        if(root->right != NULL){
+            DFS(root->right, temp);
+        }
+        temp.pop_back();
+    }
+    vector<string> binaryTreePaths(TreeNode* root) {
+        if(root == NULL){
+            return result;
+        }
+        vector<int> temp;
+        DFS(root, temp);
+        return result;
+    }
+};
+

404. 左叶子之和

+

给定二叉树的根节点 root ,返回所有左叶子之和。

+

判断条件进行递归操作即可

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func sumOfLeftLeaves(root *TreeNode) int {
+    sum := 0
+    if root.Left == nil && root.Right == nil{
+        return sum
+    }
+    DFS(root,&sum)
+    return sum
+}
+
+func DFS(root *TreeNode, sum *int){
+    if root.Left == nil && root.Right == nil{
+        *sum += root.Val
+        return
+    }
+    if root.Right != nil && (root.Right.Left != nil || root.Right.Right != nil){
+        DFS(root.Right,sum)
+    }
+    if root.Left != nil{
+        DFS(root.Left,sum)
+    }
+}
+
class Solution {
+public:
+    int sumOfLeftLeaves(TreeNode* root) {
+        if(root == NULL){
+            return 0;
+        }
+        int sumleaves = 0;
+        if(root->left != NULL && root->left->left == NULL && root->left->right == NULL){
+            sumleaves = root->left->val;
+        }
+        return sumleaves + sumOfLeftLeaves(root->left) + sumOfLeftLeaves(root->right);
+    }
+};
+

513. 找树左下角的值

+

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。

+

假设二叉树中至少有一个节点。

+

层序遍历即可(不能用一般的深度优先遍历!)

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func findBottomLeftValue(root *TreeNode) int {
+    queue := list.New()
+    queue.PushBack(root)
+    result := 0
+    for queue.Len() != 0 {
+        len1 := queue.Len()
+        for i:=0;i<len1;i++{
+            node := queue.Remove(queue.Front()).(*TreeNode)
+            if i == 0{
+                result = node.Val
+            }
+            if node.Left != nil{
+                queue.PushBack(node.Left)
+            }
+            if node.Right != nil{
+                queue.PushBack(node.Right)
+            }
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    int findBottomLeftValue(TreeNode* root) {
+        if(root == NULL){
+            return 0;
+        }
+        int temp = root->val;
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            TreeNode* t = q.front();
+            q.pop();
+            temp = t->val;
+            if(t->right != NULL){
+                q.push(t->right);
+            }
+            if(t->left != NULL){
+                q.push(t->left);
+            }
+        }
+        return temp;
+    }
+};
+

112. 路径总和

+

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false

+

叶子节点 是指没有子节点的节点。

+

与找路径相同,递归寻找即可

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func hasPathSum(root *TreeNode, targetSum int) bool {
+    if root == nil{
+        return false
+    }
+    return DFS(root,0, targetSum)
+}
+
+func DFS(root *TreeNode,sum int,targetSum int) bool {
+    if root.Left == nil && root.Right == nil{
+        sum += root.Val
+        if sum == targetSum{
+            return true
+        }
+        return false
+    }
+    sum += root.Val
+    var a,b bool
+    if root.Left != nil{
+        a = DFS(root.Left,sum, targetSum)
+    } 
+    if root.Right != nil{
+        b = DFS(root.Right,sum, targetSum)
+    }
+    return a || b
+}
+
class Solution {
+private:
+    bool judge = false;
+public:
+    void DFS(TreeNode* root, int nowSum, int targetSum){
+        nowSum += root->val;
+        if(root->left == NULL && root->right == NULL){
+            if(nowSum == targetSum){
+                judge = true;
+            }
+            return;
+        }
+        if(root->left != NULL){
+            DFS(root->left, nowSum, targetSum);
+        }
+        if(root->right != NULL){
+            DFS(root->right, nowSum, targetSum);
+        }
+        nowSum -= root->val;
+    }
+    bool hasPathSum(TreeNode* root, int targetSum) {
+        if(root == NULL){
+            return false;
+        }
+        DFS(root,0, targetSum);
+        return judge;
+    }
+};
+

113. 路径总和 II

+

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

+

叶子节点 是指没有子节点的节点。

+

新建一个数组记录每个结点的父亲即可

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+
+var result [][]int
+var pre map[*TreeNode]*TreeNode
+var target int
+
+func pathSum(root *TreeNode, targetSum int) [][]int {
+    result = make([][]int,0)
+    if root == nil{
+        return result
+    }
+    pre = make(map[*TreeNode]*TreeNode,0)
+    target = targetSum
+    DFS(root,0)
+    return result
+}
+
+func DFS(root *TreeNode, temp int){
+    temp += root.Val
+    if root.Left == nil && root.Right == nil{
+        if temp == target{
+            t := root
+            temp2 := make([]int,1)
+            temp2[0] = root.Val
+            for {
+                n,ok := pre[t]
+                if ok{
+                    temp2 = append([]int{n.Val},temp2...)
+                    t = n
+                } else{
+                    break
+                }
+            }
+            result = append(result,temp2)
+        }
+    } else if root.Left == nil{
+        pre[root.Right] = root
+        DFS(root.Right,temp)
+    } else if root.Right == nil{
+        pre[root.Left] = root
+        DFS(root.Left,temp)
+    } else{
+        pre[root.Right] = root
+        pre[root.Left] = root
+        DFS(root.Left,temp)
+        DFS(root.Right,temp)
+    }
+}
+
class Solution {
+private:
+    vector<vector<int> > result;
+public:
+    void DFS(TreeNode* root, int nowSum, int targetSum, vector<int> path){
+        nowSum += root->val;
+        path.push_back(root->val);
+        if(root->left == NULL && root->right == NULL && nowSum == targetSum){
+            result.push_back(path);
+            return;
+        }
+        if(root->left != NULL){
+            DFS(root->left,nowSum,targetSum, path);
+        }
+        if(root->right != NULL){
+            DFS(root->right,nowSum,targetSum, path);
+        }
+        nowSum -= root->val;
+        path.pop_back();
+    }
+    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
+        if(root == NULL){
+            return result;
+        }
+        vector<int> path;
+        DFS(root,0,targetSum, path);
+        return result;
+    }
+};
+

106. 从中序与后序遍历序列构造二叉树

+

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func buildTree(inorder []int, postorder []int) *TreeNode {
+    if len(inorder) < 1{
+        return nil
+    }
+    nodeValue := postorder[len(postorder)-1]
+    interval := findIndex(inorder,nodeValue)
+    root := &TreeNode{
+        Val:nodeValue,
+        Left:buildTree(inorder[:interval],postorder[:interval]),
+        Right:buildTree(inorder[interval+1:],postorder[interval:len(postorder)-1]),
+    }
+    return root
+}
+
+func findIndex(inorder []int,nodeValue int) int {
+    index := 0
+    for i:=0;i<len(inorder);i++{
+        if inorder[i] == nodeValue{
+            index = i
+            break
+        }
+    }
+    return index
+}
+
class Solution {
+public:
+    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
+        int postordersize = postorder.size();
+        int rootval = postorder[postordersize-1];
+        TreeNode* root = new TreeNode(rootval);
+        vector<int> newinorderleft;
+        vector<int> newinorderright;
+        vector<int> newpostorderleft;
+        vector<int> newpostorderright;
+        int inordersign = -1;
+        for(int i=0;i<inorder.size();i++){
+            if(inorder[i] == rootval){
+                inordersign = i;
+                newpostorderright.push_back(postorder[i]);
+                continue;
+            }
+            if(inordersign == -1){
+                newinorderleft.push_back(inorder[i]);
+                newpostorderleft.push_back(postorder[i]);
+            } else{
+                newinorderright.push_back(inorder[i]);
+                if(i != inorder.size() -1){
+                    newpostorderright.push_back(postorder[i]);
+                }
+            }
+        }
+        if(newinorderleft.size() != 0){
+            root->left = buildTree(newinorderleft,newpostorderleft);
+        }
+        if(newinorderright.size() != 0){
+            root->right = buildTree(newinorderright,newpostorderright);
+        }
+        return root;
+    }
+};
+

105. 从前序与中序遍历序列构造二叉树

+

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的 先序遍历inorder 是同一棵树的 中序遍历 ,请构造二叉树并返回其根节点。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func buildTree(preorder []int, inorder []int) *TreeNode {
+    if len(inorder) < 1{
+        return nil
+    }
+    nodeValue := preorder[0]
+    interval := findIndex(inorder,nodeValue)
+    root := &TreeNode{
+        Val:nodeValue,
+        Left:buildTree(preorder[1:interval+1],inorder[:interval]),
+        Right:buildTree(preorder[interval+1:],inorder[interval+1:]),
+    }
+    return root
+}
+
+func findIndex(inorder []int,nodeValue int) int {
+    index := 0
+    for i:=0;i<len(inorder);i++{
+        if inorder[i] == nodeValue{
+            index = i
+            break
+        }
+    }
+    return index
+}
+
class Solution {
+public:
+    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
+        int rootval = preorder[0];
+        TreeNode* root = new TreeNode(rootval);
+        vector<int> newinorderleft;
+        vector<int> newinorderright;
+        vector<int> newpreorderleft;
+        vector<int> newpreorderright;
+        int inordersign = -1;
+        for(int i=0;i<inorder.size();i++){
+            if(inorder[i] == rootval){
+                inordersign = i;
+                newpreorderleft.push_back(preorder[i]);
+                continue;
+            }
+            if(inordersign == -1){
+                newinorderleft.push_back(inorder[i]);
+                if(i != 0){
+                    newpreorderleft.push_back(preorder[i]);
+                }
+            } else{
+                newinorderright.push_back(inorder[i]);
+                newpreorderright.push_back(preorder[i]);
+            }
+        }
+        if(newinorderleft.size() != 0){
+            root->left = buildTree(newpreorderleft,newinorderleft);
+        }
+        if(newinorderright.size() != 0){
+            root->right = buildTree(newpreorderright,newinorderright);
+        }
+        return root;
+    }
+};
+

654. 最大二叉树

+

给定一个不重复的整数数组 nums最大二叉树 可以用下面的算法从 nums 递归地构建:

+
    +
  1. 创建一个根节点,其值为 nums 中的最大值。
  2. +
  3. 递归地在最大值 左边子数组前缀上 构建左子树。
  4. +
  5. 递归地在最大值 右边子数组后缀上 构建右子树。
  6. +
+

返回 nums 构建的最大二叉树。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func constructMaximumBinaryTree(nums []int) *TreeNode {
+    if len(nums) < 1{
+        return nil
+    }
+    maxIndex := findMaxindex(nums)
+    root := &TreeNode{
+        Val:nums[maxIndex],
+        Left:constructMaximumBinaryTree(nums[:maxIndex]),
+        Right:constructMaximumBinaryTree(nums[maxIndex+1:]),
+    }
+    return root
+}
+
+func findMaxindex(nums []int) int{
+    maxValue := nums[0]
+    maxIndex := 0
+    for i:=1;i<len(nums);i++{
+        if nums[i] > maxValue{
+            maxValue = nums[i]
+            maxIndex = i
+        }
+    }
+    return maxIndex
+}
+
class Solution {
+public:
+    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
+        if(nums.size() == 0){
+            return NULL;
+        }
+        int maxindex = 0;
+        int maxnode = nums[0];
+        for(int i=0;i<nums.size();i++){
+            if(nums[i] > maxnode){
+                maxindex = i;
+                maxnode = nums[i];
+            }
+        }
+        TreeNode* root = new TreeNode(maxnode);
+        vector<int> leftnums;
+        vector<int> rightnums;
+        for(int i=0;i<maxindex;i++){
+            leftnums.push_back(nums[i]);
+        }
+        for(int i=maxindex+1;i<nums.size();i++){
+            rightnums.push_back(nums[i]);
+        }
+        root->left = constructMaximumBinaryTree(leftnums);
+        root->right = constructMaximumBinaryTree(rightnums);
+        return root;
+    }
+};
+

617. 合并二叉树

+

给你两棵二叉树: root1root2

+

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。

+

返回合并后的二叉树。

+

注意: 合并过程必须从两个树的根节点开始。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func mergeTrees(root1 *TreeNode, root2 *TreeNode) *TreeNode {
+    if root1 == nil {
+        return root2
+    }
+    if root2 == nil {
+        return root1
+    }
+    root1.Val += root2.Val
+    root1.Left = mergeTrees(root1.Left, root2.Left)
+    root1.Right = mergeTrees(root1.Right, root2.Right)
+    return root1
+}
+
class Solution { 
+public:
+    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
+        if(root1 == NULL){
+            return root2;
+        } else if(root2 == NULL){
+            return root1;
+        }
+        root1->val += root2->val;
+        root1->left = mergeTrees(root1->left, root2->left);
+        root1->right = mergeTrees(root1->right, root2->right);
+        return root1;
+    }
+};
+

700. 二叉搜索树中的搜索

+

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

+

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func searchBST(root *TreeNode, val int) *TreeNode {
+    if root == nil || root.Val == val{
+        return root
+    }   
+    if val < root.Val{
+        return searchBST(root.Left,val)
+    } else if val > root.Val{
+        return searchBST(root.Right,val)
+    }
+    return root
+}
+
class Solution {
+public:
+    TreeNode* searchBST(TreeNode* root, int val) {
+        if(root == NULL || root->val == val){
+            return root;
+        }
+        if(root->val > val){
+            return searchBST(root->left,val);
+        } else{
+            return searchBST(root->right,val);
+        }
+        return root;
+    }
+};
+

98. 验证二叉搜索树

+

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

+

有效 二叉搜索树定义如下:

+
    +
  • 节点的左子树只包含 小于 当前节点的数。
  • +
  • 节点的右子树只包含 大于 当前节点的数。
  • +
  • 所有左子树和右子树自身必须也是二叉搜索树。
  • +
+

不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func isValidBST(root *TreeNode) bool {
+    result := make([]int,0)
+    DFS(root,&result)
+    for i:=0;i<len(result)-1;i++{
+        if result[i+1] <= result[i]{
+            return false
+        }
+    }
+    return true
+}
+
+func DFS(root *TreeNode, result *[]int){
+    if root == nil{
+        return
+    }
+    DFS(root.Left,result)
+    *result = append(*result,root.Val)
+    DFS(root.Right,result)
+}
+
class Solution {
+private:
+    vector<int> result;
+public:
+    void inorder(TreeNode* root){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left);
+        result.push_back(root->val);
+        inorder(root->right);
+    }
+    bool isValidBST(TreeNode* root) {
+        inorder(root);
+        for(int i=0;i<result.size()-1;i++){
+            if(result[i] >= result[i+1]){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

530. 二叉搜索树的最小绝对差

+

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值

+

差值是一个正数,其数值等于两值之差的绝对值。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func getMinimumDifference(root *TreeNode) int {
+    result := make([]int,0)
+    DFS(root,&result)
+    minValue := 100000
+    for i:=0;i<len(result)-1;i++{
+        if result[i+1] - result[i] < minValue{
+            minValue = result[i+1] - result[i]
+        }
+    }
+    return minValue
+}
+
+func DFS(root *TreeNode, result *[]int){
+    if root == nil{
+        return
+    }
+    DFS(root.Left,result)
+    *result = append(*result,root.Val)
+    DFS(root.Right,result)
+}
+
class Solution {
+private:
+    vector<int> result;
+public:
+    void inorder(TreeNode* root){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left);
+        result.push_back(root->val);
+        inorder(root->right);
+    }
+    int getMinimumDifference(TreeNode* root) {
+        inorder(root);
+        int minresult = INT_MAX;
+        for(int i=0;i<result.size()-1;i++){
+            minresult = min(result[i+1] - result[i], minresult);
+        }
+        return minresult;
+    }
+};
+

501. 二叉搜索树中的众数

+

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。

+

如果树中有不止一个众数,可以按 任意顺序 返回。

+

假定 BST 满足如下定义:

+
    +
  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • +
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • +
  • 左子树和右子树都是二叉搜索树
  • +
+

双指针法通过一次遍历快速找出众数

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func findMode(root *TreeNode) []int {
+    result := make([]int,0)
+    result2 := make([]int,0)
+    DFS(root,&result)
+    times := -1
+    p := 0
+    for q:=0;q<=len(result);q++{
+        if q == len(result) || result[q] != result[p]{
+            if times < q-p{
+                times = q-p
+                result2 = make([]int,0)
+                result2 = append(result2,result[p])
+            } else if times == q-p{
+                result2 = append(result2,result[p]) 
+            }
+            p = q
+        }
+    }
+    return result2
+}
+
+func DFS(root *TreeNode, result *[]int){
+    if root == nil{
+        return
+    }
+    DFS(root.Left,result)
+    *result = append(*result,root.Val)
+    DFS(root.Right,result)
+}
+
class Solution {
+private:
+    unordered_map<int,int> mp;
+public:
+    void inorder(TreeNode* root){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left);
+        mp[root->val] += 1;
+        inorder(root->right);
+    }
+    vector<int> findMode(TreeNode* root) {
+        inorder(root);
+        int result = 0;
+        vector<int> res;
+        for(auto it = mp.begin();it != mp.end();it++){
+            if(it->second > result){
+                res.clear();
+                res.push_back(it->first);
+                result = it->second;
+            } else if (it->second == result){
+                res.push_back(it->first);
+            }
+        }
+        return res;
+    }
+};
+

236. 二叉树的最近公共祖先

+

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

+

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大( 一个节点也可以是它自己的祖先 )。”

+

通过递归的方式将节点一层一层传递回去

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
+    if root == p || root == q || root == nil{
+        return root
+    }
+    left := lowestCommonAncestor(root.Left,p,q)
+    right := lowestCommonAncestor(root.Right,p,q)
+    if left != nil && right != nil{
+        return root
+    } else if left == nil && right != nil{
+        return right
+    } else if left != nil && right == nil{
+        return left
+    }
+    return nil
+}
+
class Solution {
+public:
+    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
+        if (root == q || root == p || root == NULL) return root;
+        TreeNode* left = lowestCommonAncestor(root->left, p, q);
+        TreeNode* right = lowestCommonAncestor(root->right, p, q);
+        if (left != NULL && right != NULL){
+            return root;
+        } else if (left == NULL){
+            return right;
+        }
+        return left;
+    }
+};
+

235. 二叉搜索树的最近公共祖先

+

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

+

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大( 一个节点也可以是它自己的祖先 )。”

+

利用二叉搜索树的性质,总体思想与上题相同

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val   int
+ *     Left  *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+
+func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
+    if root.Val < p.Val && root.Val < q.Val{
+        return lowestCommonAncestor(root.Right,p,q)
+    } else if root.Val > p.Val && root.Val > q.Val{
+        return lowestCommonAncestor(root.Left,p,q)
+    }
+    return root
+}
+
class Solution {
+public:
+    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
+        if (root->val > p->val && root->val > q->val) {
+            return lowestCommonAncestor(root->left, p, q);
+        } else if (root->val < p->val && root->val < q->val) {
+            return lowestCommonAncestor(root->right, p, q);
+        } else return root;
+
+    }
+};
+

701. 二叉搜索树中的插入操作

+

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

+

注意 ,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func insertIntoBST(root *TreeNode, val int) *TreeNode {
+    if root == nil {
+        return &TreeNode{Val:val}
+    }
+    node := root
+    var pnode *TreeNode
+    for node != nil{
+        if node.Val < val{
+            pnode = node
+            node = node.Right
+        } else if node.Val > val{
+            pnode = node
+            node = node.Left
+        }
+    }
+    if val > pnode.Val {
+        pnode.Right = &TreeNode{Val: val}
+    } else {
+        pnode.Left = &TreeNode{Val: val}
+    }
+    return root
+}
+
class Solution {
+public:
+    TreeNode* insertIntoBST(TreeNode* root, int val) {
+        if(root == NULL){
+            TreeNode* node = new TreeNode(val);
+            return node;
+        }
+        if(root->val > val){
+            root->left = insertIntoBST(root->left, val);
+        } else{
+            root->right = insertIntoBST(root->right, val);
+        }
+        return root;
+    }
+};
+

450. 删除二叉搜索树中的节点

+

给定一个二叉搜索树的根节点 root 和一个值 key ,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

+

有以下五种情况:

+
    +
  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • +
  • 找到删除的节点 +
      +
    • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    • +
    • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    • +
    • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    • +
    • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
    • +
    +
  • +
+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func deleteOneNode(target *TreeNode) *TreeNode {
+	if target == nil {
+		return target
+	}
+	if target.Right == nil {
+		return target.Left
+	}
+	cur := target.Right
+	for cur.Left != nil {
+		cur = cur.Left
+	}
+	cur.Left = target.Left
+	return target.Right
+}
+func deleteNode(root *TreeNode, key int) *TreeNode {
+	// 特殊情况处理
+	if root == nil {
+		return root
+	}
+	cur := root
+	var pre *TreeNode
+	for cur != nil {
+		if cur.Val == key {
+			break
+		}
+		pre = cur
+		if cur.Val > key {
+			cur = cur.Left
+		} else {
+			cur = cur.Right
+		}
+	}
+	if pre == nil {
+		return deleteOneNode(cur)
+	}
+	// pre 要知道是删除左孩子还有右孩子
+	if pre.Left != nil && pre.Left.Val == key {
+		pre.Left = deleteOneNode(cur)
+	}
+	if pre.Right != nil && pre.Right.Val == key {
+		pre.Right = deleteOneNode(cur)
+	}
+	return root
+}
+
class Solution {
+public:
+    TreeNode* deleteNode(TreeNode* root, int key) {
+        if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了
+        if (root->val == key) {
+            // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
+            if (root->left == nullptr && root->right == nullptr) {
+                ///! 内存释放
+                delete root;
+                return nullptr;
+            }
+            // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
+            else if (root->left == nullptr) {
+                auto retNode = root->right;
+                ///! 内存释放
+                delete root;
+                return retNode;
+            }
+            // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
+            else if (root->right == nullptr) {
+                auto retNode = root->left;
+                ///! 内存释放
+                delete root;
+                return retNode;
+            }
+            // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
+            // 并返回删除节点右孩子为新的根节点。
+            else {
+                TreeNode* cur = root->right; // 找右子树最左面的节点
+                while(cur->left != nullptr) {
+                    cur = cur->left;
+                }
+                cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置
+                TreeNode* tmp = root;   // 把root节点保存一下,下面来删除
+                root = root->right;     // 返回旧root的右孩子作为新root
+                delete tmp;             // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)
+                return root;
+            }
+        }
+        if (root->val > key) root->left = deleteNode(root->left, key);
+        if (root->val < key) root->right = deleteNode(root->right, key);
+        return root;
+    }
+};
+

669. 修剪二叉搜索树

+

给你二叉搜索树的根节点 root ,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在 [low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案

+

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func trimBST(root *TreeNode, low int, high int) *TreeNode {
+    if root==nil{
+        return nil
+    }
+    if root.Val<low{//如果该节点值小于最小值,则该节点更换为该节点的右节点值,继续遍历
+        right:=trimBST(root.Right,low,high)
+        return right
+    }
+    if root.Val>high{//如果该节点的值大于最大值,则该节点更换为该节点的左节点值,继续遍历
+        left:=trimBST(root.Left,low,high)
+        return left
+    }
+    root.Left=trimBST(root.Left,low,high)
+    root.Right=trimBST(root.Right,low,high)
+    return root
+}
+
class Solution {
+public:
+    TreeNode* trimBST(TreeNode* root, int low, int high) {
+        if (root == nullptr ) return nullptr;
+        if (root->val < low) {
+            TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点
+            return right;
+        }
+        if (root->val > high) {
+            TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点
+            return left;
+        }
+        root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子
+        root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子
+        return root;
+    }
+};
+

108. 将有序数组转换为二叉搜索树

+

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

+

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 的二叉树。

+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func sortedArrayToBST(nums []int) *TreeNode {
+    if len(nums) == 0{
+        return nil
+    }
+    mid := len(nums) / 2
+    root := &TreeNode{
+        Val:nums[mid],
+        Left:sortedArrayToBST(nums[:mid]),
+        Right:sortedArrayToBST(nums[mid+1:]),
+    }
+    return root
+}
+
class Solution {
+public:
+    TreeNode* sortedArrayToBST(vector<int>& nums) {
+        if(nums.size() == 0){
+            return NULL;
+        }
+        int mid = nums.size() / 2;
+        TreeNode* root = new TreeNode(nums[mid]);
+        vector<int> leftnums;
+        vector<int> rightnums;
+        for(int i=0;i<mid;i++){
+            leftnums.push_back(nums[i]);
+        }
+        for(int i=mid+1;i<nums.size();i++){
+            rightnums.push_back(nums[i]);
+        }
+        root->left = sortedArrayToBST(leftnums);
+        root->right = sortedArrayToBST(rightnums);
+        return root;
+    }
+};
+

538. 把二叉搜索树转换为累加树

+

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

+

提醒一下,二叉搜索树满足下列约束条件:

+
    +
  • 节点的左子树仅包含键 小于 节点键的节点。
  • +
  • 节点的右子树仅包含键 大于 节点键的节点。
  • +
  • 左右子树也必须是二叉搜索树。
  • +
+
/**
+ * Definition for a binary tree node.
+ * type TreeNode struct {
+ *     Val int
+ *     Left *TreeNode
+ *     Right *TreeNode
+ * }
+ */
+func convertBST(root *TreeNode) *TreeNode {
+    sum := 0
+    DFS(root,&sum)
+    return root
+}
+
+func DFS(root *TreeNode, sum *int){
+    if root == nil{
+        return
+    }
+    DFS(root.Right,sum)
+    root.Val += *sum
+    *sum = root.Val
+    DFS(root.Left,sum)
+}
+
class Solution {
+private:
+    int nowsum = 0;
+public:
+    TreeNode* convertBST(TreeNode* root) {
+        if(root == NULL){
+            return NULL;
+        }
+        convertBST(root->right);
+        root->val += nowsum;
+        nowsum = root->val;
+        convertBST(root->left);
+        return root;
+    }
+};
+ +
+ +
+
+ + + + + + +
+
+
代码随想录-二叉树
+
https://zhangzhao219.github.io/2024/03/11/Leetcode/programmercarl/programmercarl-bt/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月11日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/13/Leetcode/programmercarl/programmercarl-bkt/index.html b/2024/03/13/Leetcode/programmercarl/programmercarl-bkt/index.html new file mode 100644 index 000000000..dfb7a4580 --- /dev/null +++ b/2024/03/13/Leetcode/programmercarl/programmercarl-bkt/index.html @@ -0,0 +1,1839 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-回溯算法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-回溯算法

+ + +
+ +

代码随想录-回溯算法

+ +

回溯

+

回溯算法理论基础

+

回溯法,一般可以解决如下几种问题:

+
    +
  • 组合问题:N个数里面按一定规则找出k个数的集合
  • +
  • 切割问题:一个字符串按一定规则有几种切割方式
  • +
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • +
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • +
  • 棋盘问题:N皇后,解数独等等
  • +
+

回溯法解决的问题都可以抽象为树形结构 ,是的,我指的是所有回溯法的问题都可以抽象为树形结构!

+

因为回溯法解决的都是在集合中递归查找子集, 集合的大小就构成了树的宽度,递归的深度,都构成的树的深度

+

递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

+

回溯算法模板:

+
void backtracking(参数) {
+    if (终止条件) {
+        存放结果;
+        return;
+    }
+
+    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
+        处理节点;
+        backtracking(路径,选择列表); // 递归
+        回溯,撤销处理结果
+    }
+}
+
+

77. 组合

+

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

+

你可以按 任何顺序 返回答案。

+
func combine(n int, k int) [][]int {
+    result := make([][]int,0)
+    temp := make([]int,0)
+    backtracking(n,k,1,temp,&result)
+    return result
+}
+
+func backtracking(n,k,s int,temp []int, result *[][]int){
+    if len(temp) == k{
+        b := make([]int, len(temp))
+	copy(b, temp)
+        *result = append(*result,b)
+        return
+    }
+    for i:=s;i<=n-k+len(temp)+1;i++{
+        temp = append(temp,i)
+        backtracking(n,k,i+1,temp,result)
+        temp = temp[:len(temp)-1]
+    }
+}
+
class Solution {
+private:
+    vector<vector<int> > result;
+    vector<int> temp;
+public:
+    void backtracking(int start, int end, int k){
+        if(temp.size() == k){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=start;i<=end;i++){
+            temp.push_back(i);
+            backtracking(i+1,end,k);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> combine(int n, int k) {
+        
+        backtracking(1,n,k);
+        return result;
+    }
+};
+

216. 组合总和III

+

找出所有相加之和为 nk 个数的组合,且满足下列条件:

+
    +
  • 只使用数字1到9
  • +
  • 每个数字 最多使用一次
  • +
+

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

+
func combinationSum3(k int, n int) [][]int {
+    result := make([][]int,0)
+    temp := make([]int,0)
+    backtracking(n,k,1,temp,&result)
+    return result
+}
+
+func backtracking(n,k,s int, temp []int,result *[][]int){
+    if len(temp) == k{
+        if sum(temp) == n{
+            t2 := make([]int,len(temp))
+            copy(t2,temp)
+            *result = append(*result,t2)
+        }
+        return
+    }
+    for i:=s;i<=9-k+len(temp)+1;i++{
+        temp = append(temp,i)
+        backtracking(n,k,i+1,temp,result)
+        temp = temp[:len(temp)-1]
+    }
+}
+
+func sum(temp []int) int{
+    res := 0
+    for i:=0;i<len(temp);i++{
+        res += temp[i]
+    }
+    return res
+}
+
class Solution {
+private:
+    vector<vector<int> > result;
+    vector<int> temp;
+public:
+    void backtracking(int start, int end, int k, int n){
+        if(temp.size() == k){
+            int tempsum = 0;
+            for(int i=0;i<k;i++){
+                tempsum += temp[i];
+            }
+            if(tempsum == n){
+                result.push_back(temp);
+                return;
+            }
+        } else if(temp.size() > k){
+            return;
+        }
+        for(int i=start;i<=end;i++){
+            temp.push_back(i);
+            backtracking(i+1,9,k,n);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> combinationSum3(int k, int n) {
+        backtracking(1,9,k,n);
+        return result;
+    }
+};
+

17. 电话号码的字母组合

+

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

+

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

+
func letterCombinations(digits string) []string {
+    alphamap := make(map[byte][]string,0)
+    alphamap['2'] = []string{"a","b","c"}
+    alphamap['3'] = []string{"d","e","f"}
+    alphamap['4'] = []string{"g","h","i"}
+    alphamap['5'] = []string{"j","k","l"}
+    alphamap['6'] = []string{"m","n","o"}
+    alphamap['7'] = []string{"p","q","r","s"}
+    alphamap['8'] = []string{"t","u","v"}
+    alphamap['9'] = []string{"w","x","y","z"}
+    result := make([]string,0)
+    n := len(digits)
+    temp := ""
+    backtracking(n,0,digits,alphamap,temp,&result)
+    if len(result) == 1 && result[0] == ""{
+        return make([]string,0)
+    }
+    return result
+}
+
+func backtracking(n,s int,digits string,alphamap map[byte][]string,temp string ,result *[]string){
+    if len(temp) == n{
+        *result = append(*result,temp)
+        return
+    }
+    index := digits[s]
+    letter := alphamap[index]
+    for i:=0;i<len(letter);i++{
+        temp += letter[i]
+        backtracking(n,s+1,digits,alphamap,temp,result)
+        temp = temp[:len(temp)-1]
+    }
+}
+
class Solution {
+private:
+    map<int, vector<string> > mp;
+    vector<string> result;
+    vector<string> temp;
+public:
+    void backtracking(string digits, int start, int end){
+        if(temp.size() == end){
+            string t;
+            for(int i=0;i<end;i++){
+                t += temp[i];
+            }
+            result.push_back(t);
+            return;
+        }
+        for(int i=start;i<end;i++){
+            int index = digits[i] - '0';
+            vector<string> worddict = mp[index];
+            for(int j=0;j<worddict.size();j++){
+                temp.push_back(worddict[j]);
+                backtracking(digits,i+1,digits.size());
+                temp.pop_back();
+            }
+        }
+    }
+    vector<string> letterCombinations(string digits) {
+        if(digits.size() == 0){
+            return result;
+        }
+        mp[2] = vector<string>{"a","b","c"};
+        mp[3] = vector<string>{"d","e","f"};
+        mp[4] = vector<string>{"g","h","i"};
+        mp[5] = vector<string>{"j","k","l"};
+        mp[6] = vector<string>{"m","n","o"};
+        mp[7] = vector<string>{"p","q","r","s"};
+        mp[8] = vector<string>{"t","u","v"};
+        mp[9] = vector<string>{"w","x","y","z"};
+        backtracking(digits,0,digits.size());
+        return result;
+    }
+};
+

39. 组合总和

+

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

+

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

+

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

+
func combinationSum(candidates []int, target int) [][]int {
+    result := make([][]int,0)
+    temp := make([]int,0)
+    backtracking(0,candidates,target,temp,&result)
+    return result
+}
+
+func backtracking(s int,candidates []int, target int, temp []int,result *[][]int){
+    if sum(temp) == target{
+        t2 := make([]int,len(temp))
+        copy(t2,temp)
+        *result = append(*result,t2)
+        return
+    } else if sum(temp) > target{
+        return
+    }
+    for i:=s;i<len(candidates);i++{
+        temp = append(temp,candidates[i])
+        backtracking(i,candidates,target,temp,result)
+        temp = temp[:len(temp)-1]
+    }
+}
+
+func sum(temp []int) int{
+    res := 0
+    for i:=0;i<len(temp);i++{
+        res += temp[i]
+    }
+    return res
+}
+
class Solution {
+private:
+    vector<vector<int> > result;
+    vector<int> temp;
+public:
+    void backtracking(vector<int>& candidates, int target, int start, int end){
+        int countsum = 0;
+        for(int i=0;i<temp.size();i++){
+            countsum += temp[i];
+        }
+        if(countsum == target){
+            result.push_back(temp);
+            return;
+        } else if(countsum > target){
+            return;
+        }
+        for(int i=start;i<=end;i++){
+            temp.push_back(candidates[i]);
+            backtracking(candidates,target,i,candidates.size()-1);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
+        backtracking(candidates, target,0,candidates.size()-1);
+        return result;
+    }
+};
+

40. 组合总和II

+

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

+

candidates 中的每个数字在每个组合中只能使用 一次

+

注意: 解集不能包含重复的组合。

+

注意要去重!

+
func combinationSum2(candidates []int, target int) [][]int {
+    sort.Slice(candidates,func(i,j int) bool{
+        return candidates[i] < candidates[j]
+    })
+    result := make([][]int,0)
+    temp := make([]int,0)
+    backtracking(0,0,candidates,target,temp,&result)
+    return result
+}
+
+func backtracking(s,sum int,candidates []int, target int, temp []int,result *[][]int){
+    if sum == target{
+        t2 := make([]int,len(temp))
+        copy(t2,temp)
+        *result = append(*result,t2)
+        return
+    } else if sum > target{
+        return
+    }
+    for i:=s;i<len(candidates);i++{
+        if i>s&&candidates[i]==candidates[i-1]{
+            continue
+        }
+        temp = append(temp,candidates[i])
+        sum += candidates[i]
+        backtracking(i+1,sum,candidates,target,temp,result)
+        temp = temp[:len(temp)-1]
+        sum -= candidates[i]
+    }
+}
+
class Solution {
+private:
+    vector<vector<int> > result;
+    vector<int> temp;
+public:
+    void backtracking(vector<int>& candidates, int target, int start, int end){
+        int countsum = 0;
+        for(int i=0;i<temp.size();i++){
+            countsum += temp[i];
+        }
+        if(countsum == target){
+            result.push_back(temp);
+            return;
+        } else if(countsum > target){
+            return;
+        }
+        for(int i=start;i<=end;i++){
+            if (i > start && candidates[i] == candidates[i - 1]) {
+                continue;
+            }
+            temp.push_back(candidates[i]);
+            backtracking(candidates,target,i+1,candidates.size()-1);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
+        sort(candidates.begin(), candidates.end());
+        backtracking(candidates, target,0,candidates.size()-1);
+        return result;
+    }
+};
+

131. 分割回文串

+

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

+

回文串 是正着读和反着读都一样的字符串。

+
func partition(s string) [][]string {
+    result := make([][]string,0)
+    temp := make([]string,0)
+    backtracking(s,0,temp,&result)
+    return result
+}
+
+func backtracking(s string, start int, temp []string,result *[][]string){
+    if start == len(s){
+        t2 := make([]string,len(temp))
+        copy(t2,temp)
+        *result = append(*result,t2)
+        return
+    }
+    for i:=start;i<len(s);i++{
+        if judge(s,start,i){
+            temp = append(temp,s[start:i+1])
+        } else{
+            continue
+        }
+        backtracking(s,i+1,temp,result)
+        temp = temp[:len(temp)-1]
+    }
+}
+
+func judge(s string,start,end int)bool {
+    for start < end{
+        if s[start] != s[end]{
+            return false
+        }
+        start += 1
+        end -= 1
+    }
+    return true
+}
+
class Solution {
+public:
+    vector<vector<string> > result;
+    vector<string> path;
+    bool judgepa(string s, int startIndex, int endIndex){
+        while(startIndex < endIndex){
+            if(s[startIndex] != s[endIndex]){
+                return false;
+            }
+            startIndex += 1;
+            endIndex -= 1;
+        }
+        return true;
+    }
+    void backtracking(string s, int startIndex){
+        if(startIndex >= s.size()){
+            result.push_back(path);
+            return;
+        }
+        for(int i = startIndex;i<s.size();i++){
+            if(judgepa(s,startIndex,i)){
+                string str = s.substr(startIndex, i-startIndex+1);
+                path.push_back(str);
+            } else{
+                continue;
+            }
+            backtracking(s,i+1);
+            path.pop_back();
+        }
+
+    }
+    vector<vector<string>> partition(string s) {
+        backtracking(s,0);
+        return result;
+    }
+};
+

93. 复原IP地址

+

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

+
    +
  • 例如:"0.1.2.201" "192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1"无效 IP 地址。
  • +
+

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的 有效 IP 地址 ,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

+
func restoreIpAddresses(s string) []string {
+    result := make([]string,0)
+    temp := make([]string,0)
+    backtracking(s,0,temp,&result)
+    return result
+}
+
+func backtracking(s string, start int, temp []string,result *[]string){
+    if start == len(s) && len(temp) == 4{
+        t2 := temp[0] + "." + temp[1] + "." + temp[2] + "." + temp[3]
+        *result = append(*result,t2)
+        return
+    }
+    for i:=start;i<len(s);i++{
+        temp = append(temp,s[start:i+1])
+        if i-start+1<=3&&len(temp)<=4&&judge(s,start,i){
+            backtracking(s,i+1,temp,result)
+        } else{
+            continue
+        }
+        temp = temp[:len(temp)-1]
+    }
+}
+
+func judge(s string,start,end int)bool {
+    checkInt,_:=strconv.Atoi(s[start:end+1])
+	if end-start+1>1&&s[start]=='0'{//对于前导 0的IP(特别注意s[startIndex]=='0'的判断,不应该写成s[startIndex]==0,因为s截取出来不是数字)
+		return false
+	}
+	if checkInt>255{
+		return false
+	}
+	return true
+}
+
class Solution {
+public:
+    vector<string> result;
+    vector<string> temp;
+    bool judgenum(string r){
+        if(r.size() <= 0 || r.size() > 3){
+            return false;
+        }
+        int num = stoi(r);
+        if(num > 255){
+            return false;
+        }
+        if(num != 0 && r[0] == '0'){
+            return false;
+        }
+        if(num == 0 && r.size() != 1){
+            return false;
+        }
+        return true;
+    }
+    void backtracking(string s, int startIndex){
+        if(startIndex >= s.size() && temp.size() == 4){
+            string r = "";
+            for(int i=0;i<3;i++){
+                r += temp[i];
+                r += ".";
+            }
+            r += temp[3];
+            result.push_back(r);
+            return;
+        } else if(temp.size() > 4 || startIndex >= s.size()){
+            return;
+        }
+        for(int i=startIndex;i<s.size();i++){
+            string r = s.substr(startIndex,i-startIndex+1);
+            if(judgenum(r)){
+                temp.push_back(r);
+            } else{
+                continue;
+            }
+            backtracking(s,i+1);
+            temp.pop_back();
+        }
+    }
+    vector<string> restoreIpAddresses(string s) {
+        backtracking(s,0);
+        return result;
+    }
+};
+

78. 子集

+

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

+

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

+
func subsets(nums []int) [][]int {
+    result := make([][]int,0)
+    temp := make([]int,0)
+    for n:=0;n<=len(nums);n++{
+        backtracking(0,n,temp,&result,nums)
+    }
+    return result
+}
+
+func backtracking(s,n int,temp []int,result *[][]int,nums []int){
+    if len(temp) == n{
+        t2 := make([]int,n)
+        copy(t2,temp)
+        *result = append(*result,t2)
+        return
+    }
+    for i:=s;i<len(nums);i++{
+        temp = append(temp,nums[i])
+        backtracking(i+1,n,temp,result,nums)
+        temp = temp[:len(temp)-1]
+    }
+}
+
class Solution {
+public:
+    vector<vector<int> > result;
+    vector<int> temp;
+    void backtracking(vector<int> nums, int startIndex, int maxIndex){
+        if(temp.size() == maxIndex){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=startIndex;i<nums.size();i++){
+            temp.push_back(nums[i]);
+            backtracking(nums, i+1, maxIndex);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> subsets(vector<int>& nums) {
+        for(int i=0;i<=nums.size();i++){
+            backtracking(nums,0,i);
+        }
+        return result;
+    }
+};
+

90. 子集 II

+

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

+

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

+
func subsetsWithDup(nums []int) [][]int {
+    sort.Slice(nums,func(i,j int) bool{
+        return nums[i] < nums[j]
+    })
+    result := make([][]int,0)
+    temp := make([]int,0)
+    for n:=0;n<=len(nums);n++{
+        backtracking(0,n,temp,&result,nums)
+    }
+    return result
+}
+
+func backtracking(s,n int,temp []int,result *[][]int,nums []int){
+    if len(temp) == n{
+        t2 := make([]int,n)
+        copy(t2,temp)
+        *result = append(*result,t2)
+        return
+    }
+    for i:=s;i<len(nums);i++{
+        if i > s && nums[i] == nums[i-1]{
+            continue
+        }
+        temp = append(temp,nums[i])
+        backtracking(i+1,n,temp,result,nums)
+        temp = temp[:len(temp)-1]
+    }
+}
+
class Solution {
+public:
+    vector<vector<int> > result;
+    vector<int> temp;
+    void backtracking(vector<int> nums, int startIndex){
+        result.push_back(temp);
+        for(int i=startIndex;i<nums.size();i++){
+            if(i > startIndex && nums[i] == nums[i-1]){
+                continue;
+            }
+            temp.push_back(nums[i]);
+            backtracking(nums, i+1);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
+        sort(nums.begin(), nums.end());
+        backtracking(nums,0);
+        return result;
+    }
+};
+

491. 递增子序列

+

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

+

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

+
func findSubsequences(nums []int) [][]int {
+    result := make([][]int,0)
+    temp := make([]int,0)
+    backtracking(0,temp,&result,nums)
+    return result
+}
+
+func backtracking(s int ,temp []int,result *[][]int,nums []int){
+    if len(temp) > 1{
+        t2 := make([]int,len(temp))
+        copy(t2,temp)
+        *result = append(*result,t2)
+    }
+    history:=[201]int{}
+    for i:=s;i<len(nums);i++{
+        if len(temp)>0 && nums[i]<temp[len(temp)-1]||history[nums[i] + 100]==1{
+            continue
+        }
+        history[nums[i] + 100]=1
+        temp = append(temp,nums[i])
+        backtracking(i+1,temp,result,nums)
+        temp = temp[:len(temp)-1]
+    }
+}
+
class Solution {
+public:
+    vector<vector<int> > result;
+    vector<int> temp;
+    bool judge(vector<int> t){
+        for(int i=0;i<t.size()-1;i++){
+            if(t[i+1] <= t[i]){
+                return false;
+            }
+        }
+        return true;
+    }
+    void backtracking(vector<int>& nums, int startIndex) {
+        if (temp.size() > 1) {
+            result.push_back(temp);
+        }
+        unordered_set<int> uset; 
+        for (int i = startIndex; i < nums.size(); i++) {
+            if ((!temp.empty() && nums[i] < temp.back())
+                    || uset.find(nums[i]) != uset.end()) {
+                    continue;
+            }
+            uset.insert(nums[i]);
+            temp.push_back(nums[i]);
+            backtracking(nums, i + 1);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> findSubsequences(vector<int>& nums) {
+        backtracking(nums,0);
+        return result;
+    }
+};
+

46. 全排列

+

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

+
func permute(nums []int) [][]int {
+    result := make([][]int,0)
+    temp := make([]int,0)
+    used := make([]bool,len(nums))
+    backtracking(0,nums,temp,&result,used)
+    return result
+}
+
+func backtracking(s int, nums,temp []int, result *[][]int,used []bool){
+    if len(temp) == len(nums){
+        b := make([]int, len(temp))
+	    copy(b, temp)
+        *result = append(*result,b)
+        return
+    }
+    for i:=0;i<len(nums);i++{
+        if used[i] == true{
+            continue
+        }
+        temp = append(temp,nums[i])
+        used[i] = true
+        backtracking(i+1,nums,temp,result,used)
+        temp = temp[:len(temp)-1]
+        used[i] = false
+    }
+}
+
class Solution {
+public:
+    vector<vector<int> > result;
+    vector<int> temp;
+    void backtracking(vector<int>& nums, int startIndex, vector<bool> used){
+        if(temp.size() == nums.size()){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=0;i<nums.size();i++){
+            if(used[i] == true){
+                continue;
+            }
+            used[i] = true;
+            temp.push_back(nums[i]);
+            backtracking(nums,i+1, used);
+            temp.pop_back();
+            used[i] = false;
+        }
+    }
+    vector<vector<int>> permute(vector<int>& nums) {
+        vector<bool> used(nums.size(), false);
+        backtracking(nums,0, used);
+        return result;
+    }
+};
+

47. 全排列 II

+

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

+

used[i - 1] == true,说明同一树枝nums[i - 1]使用过

+

used[i - 1] == false,说明同一树层nums[i - 1]使用过

+

如果同一树层nums[i - 1]使用过则直接跳过

+
func permuteUnique(nums []int) [][]int {
+    sort.Slice(nums,func(i,j int) bool {
+        return nums[i] < nums[j]
+    })
+    result := make([][]int,0)
+    temp := make([]int,0)
+    used := make([]bool,len(nums))
+    backtracking(0,nums,temp,&result,used)
+    return result
+}
+
+func backtracking(s int, nums,temp []int, result *[][]int,used []bool){
+    if len(temp) == len(nums){
+        b := make([]int, len(temp))
+	    copy(b, temp)
+        *result = append(*result,b)
+        return
+    }
+    for i:=0;i<len(nums);i++{
+        if i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false || used[i] == true{
+            continue
+        }
+        temp = append(temp,nums[i])
+        used[i] = true
+        backtracking(i+1,nums,temp,result,used)
+        temp = temp[:len(temp)-1]
+        used[i] = false
+    }
+}
+
class Solution {
+public:
+    vector<vector<int> > result;
+    vector<int> temp;
+    void backtracking(vector<int>& nums, vector<bool> used){
+        if(temp.size() == nums.size()){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=0;i<nums.size();i++){
+            if((i > 0 && nums[i] == nums[i-1] && used[i-1] == false) || used[i] == true){
+                continue;
+            }
+            used[i] = true;
+            temp.push_back(nums[i]);
+            backtracking(nums, used);
+            temp.pop_back();
+            used[i] = false;
+        }
+    }
+    vector<vector<int>> permuteUnique(vector<int>& nums) {
+        vector<bool> used(nums.size(), false);
+        sort(nums.begin(), nums.end());
+        backtracking(nums, used);
+        return result;
+    }
+};
+

332. 重新安排行程

+

给你一份航线列表 tickets ,其中 tickets[i] = [from<sub>i</sub>, to<sub>i</sub>] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

+

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

+
    +
  • 例如,行程 ["JFK", "LGA"]["JFK", "LGB"] 相比就更小,排序更靠前。
  • +
+

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

+
type pair struct {
+	target  string
+	visited bool
+}
+type pairs []*pair
+
+func (p pairs) Len() int {
+	return len(p)
+}
+func (p pairs) Swap(i, j int) {
+	p[i], p[j] = p[j], p[i]
+}
+func (p pairs) Less(i, j int) bool {
+	return p[i].target < p[j].target
+}
+
+func findItinerary(tickets [][]string) []string {
+	result := []string{}
+	// map[出发机场] pair{目的地,是否被访问过}
+	targets := make(map[string]pairs)
+	for _, ticket := range tickets {
+		if targets[ticket[0]] == nil {
+			targets[ticket[0]] = make(pairs, 0)
+		}
+		targets[ticket[0]] = append(targets[ticket[0]], &pair{target: ticket[1], visited: false})
+	}
+	for k, _ := range targets {
+		sort.Sort(targets[k])
+	}
+	result = append(result, "JFK")
+	var backtracking func() bool
+	backtracking = func() bool {
+		if len(tickets)+1 == len(result) {
+			return true
+		}
+		// 取出起飞航班对应的目的地
+		for _, pair := range targets[result[len(result)-1]] {
+			if pair.visited == false {
+				result = append(result, pair.target)
+				pair.visited = true
+				if backtracking() {
+					return true
+				}
+				result = result[:len(result)-1]
+				pair.visited = false
+			}
+		}
+		return false
+	}
+
+	backtracking()
+
+	return result
+}
+
class Solution {
+public:
+    unordered_map<string, map<string, int>> targets;
+    bool backtracking(int ticketNum, vector<string>& result) {
+        if (result.size() == ticketNum + 1) {
+            return true;
+        }
+        for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
+            if (target.second > 0 ) { // 记录到达机场是否飞过了
+                result.push_back(target.first);
+                target.second--;
+                if (backtracking(ticketNum, result)) return true;
+                result.pop_back();
+                target.second++;
+            }
+        }
+        return false;
+    }
+    vector<string> findItinerary(vector<vector<string>>& tickets) {
+        targets.clear();
+        vector<string> result;
+        for (const vector<string>& vec : tickets) {
+            targets[vec[0]][vec[1]]++; // 记录映射关系
+        }
+        result.push_back("JFK"); // 起始机场
+        backtracking(tickets.size(), result);
+        return result;
+    }
+};
+

51. N皇后

+

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

+

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

+

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

+

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

+
func solveNQueens(n int) [][]string {
+    result := make([][]string,0)
+    temp := make([][]string,n)
+    for i:=0;i<n;i++{
+        temp[i] = make([]string,n)
+    }
+    for i:=0;i<n;i++{
+        for j:=0;j<n;j++{
+            temp[i][j] = "."
+        }
+    }
+    backtracking(0,temp,&result)
+    return result
+}
+
+func backtracking(s int,temp [][]string,result *[][]string){
+    if s == len(temp){
+        t2 := make([]string,0)
+        for i:=0;i<len(temp);i++{
+            s2 := ""
+            for j:=0;j<len(temp);j++{
+                s2 += temp[i][j]
+            }
+            t2 = append(t2,s2)
+        }
+        *result = append(*result,t2)
+        return
+    }
+    for i:=0;i<len(temp);i++{
+        if judge(temp,s,i) == false{
+            continue
+        }
+        temp[s][i] = "Q"
+        backtracking(s+1,temp,result)
+        temp[s][i] = "."
+    }
+}
+
+func judge(temp [][]string,p,q int) bool {
+    n := len(temp)
+    for i:=0;i<n;i++{
+        if temp[i][q] == "Q"{
+            return false
+        }
+    }
+    i := p
+    j := q 
+    for i >= 0 && i < n && j >= 0 && j < n{
+        if temp[i][j] == "Q"{
+            return false
+        }
+        i--
+        j--
+    }
+    i = p
+    j = q 
+    for i >= 0 && i < n && j >= 0 && j < n{
+        if temp[i][j] == "Q"{
+            return false
+        }
+        i--
+        j++
+    }
+    return true
+}
+
class Solution {
+public:
+    vector<vector<string> > result;
+    vector<int> temp;
+    bool judge(int now, int end){
+        int now_size = temp.size();
+        for(int i=0;i<now_size;i++){
+            if(now == temp[i]){
+                return false;
+            }
+        }
+        int x = now_size - 1;
+        int y = now - 1;
+        while(x >= 0 && y >= 0){
+            if(temp[x] == y){
+                return false;
+            }
+            x -= 1;
+            y -= 1;
+        }
+        x = now_size - 1;
+        y = now + 1;
+        while(x >= 0 && y < end){
+            if(temp[x] == y){
+                return false;
+            }
+            x -= 1;
+            y += 1;
+        }
+        return true;
+    }
+    void backtracking(int start, int end){
+        if(start == end){
+            vector<string> s;
+            for(int i=0;i<end;i++){
+                int a = temp[i];
+                string temp_s = "";
+                for(int j=0;j<end;j++){
+                    if(j == a){
+                        temp_s += "Q";
+                    } else{
+                        temp_s += ".";
+                    }
+                }
+                s.push_back(temp_s);
+            }
+            result.push_back(s);
+            return;
+        }
+        for(int i=0;i<end;i++){
+            if(judge(i,end)){
+                temp.push_back(i);
+                backtracking(start+1, end);
+                temp.pop_back();
+            }
+        }
+    }
+    vector<vector<string> > solveNQueens(int n) {
+        backtracking(0,n);
+        return result;
+    }
+};
+

37. 解数独

+

编写一个程序,通过填充空格来解决数独问题。

+

数独的解法需 遵循如下规则

+
    +
  1. 数字 1-9 在每一行只能出现一次。
  2. +
  3. 数字 1-9 在每一列只能出现一次。
  4. +
  5. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
  6. +
+

数独部分空格内已填入了数字,空白格用 '.' 表示。

+
func solveSudoku(board [][]byte)  {
+    // numstring := []byte{"0","1","2","3","4","5","6","7","8","9"}
+    numbyte := []byte{'1','2','3','4','5','6','7','8','9'}
+    backtracking(numbyte,&board)
+}
+
+func backtracking(numbyte []byte, board *[][]byte) bool {
+    for i:=0;i<9;i++{
+        for j:=0;j<9;j++{
+            if (*board)[i][j] != '.'{
+                continue
+            }
+            for k:=0;k<9;k++{
+                if judge(*board,i,j,numbyte[k]) == false{
+                    continue
+                }
+                (*board)[i][j] = numbyte[k]
+                if backtracking(numbyte,board) == true{
+                    return true
+                }
+                (*board)[i][j] = '.'
+            }
+            return false
+        }
+    }
+    return true
+}
+
+func judge(board [][]byte, p,q int,c byte) bool {
+    for i:=0;i<9;i++{
+        if board[p][i] == c || board[i][q] == c{
+            return false
+        }
+    }
+    p = p / 3
+    q = q / 3
+    for i:=p*3;i<(p+1)*3;i++{
+        for j := q*3;j<(q+1)*3;j++{
+            if board[i][j] == c{
+                return false
+            }
+        }
+    }
+    return true
+}
+
class Solution {
+public:
+    bool backtracking(vector<vector<char>>& board) {
+        for (int i = 0; i < board.size(); i++) {
+            for (int j = 0; j < board[0].size(); j++) {
+                if (board[i][j] == '.') {
+                    for (char k = '1'; k <= '9'; k++) {
+                        if (isValid(i, j, k, board)) {
+                            board[i][j] = k;
+                            if (backtracking(board)) return true;
+                            board[i][j] = '.';
+                        }
+                    }
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+    bool isValid(int row, int col, char val, vector<vector<char>>& board) {
+        for (int i = 0; i < 9; i++) {
+            if (board[row][i] == val) {
+                return false;
+            }
+        }
+        for (int j = 0; j < 9; j++) {
+            if (board[j][col] == val) {
+                return false;
+            }
+        }
+        int startRow = (row / 3) * 3;
+        int startCol = (col / 3) * 3;
+        for (int i = startRow; i < startRow + 3; i++) {
+            for (int j = startCol; j < startCol + 3; j++) {
+                if (board[i][j] == val ) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+    void solveSudoku(vector<vector<char>>& board) {
+        backtracking(board);
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
代码随想录-回溯算法
+
https://zhangzhao219.github.io/2024/03/13/Leetcode/programmercarl/programmercarl-bkt/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/13/Leetcode/programmercarl/programmercarl-sq/index.html b/2024/03/13/Leetcode/programmercarl/programmercarl-sq/index.html new file mode 100644 index 000000000..fa2ce5a7c --- /dev/null +++ b/2024/03/13/Leetcode/programmercarl/programmercarl-sq/index.html @@ -0,0 +1,1197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-栈与队列 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-栈与队列

+ + +
+ +

代码随想录-栈与队列

+ +

栈与队列

+

232. 用栈实现队列

+

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

+

实现 MyQueue 类:

+

void push(int x) 将元素 x 推到队列的末尾

+

int pop() 从队列的开头移除并返回元素

+

int peek() 返回队列开头的元素

+

boolean empty() 如果队列为空,返回 true ;否则,返回 false

+

说明:你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

+
type MyQueue struct {
+    stack []int
+    back  []int
+}
+
+
+func Constructor() MyQueue {
+    return MyQueue{
+        stack: make([]int, 0),
+        back:  make([]int, 0),
+    }
+}
+
+
+func (this *MyQueue) Push(x int)  {
+    for len(this.back) != 0{
+        val := this.back[len(this.back)-1]
+        this.stack = append(this.stack,val)
+        this.back = this.back[:len(this.back)-1]
+    }
+    this.stack = append(this.stack,x)
+}
+
+
+func (this *MyQueue) Pop() int {
+    for len(this.stack) != 0{
+        val := this.stack[len(this.stack)-1]
+        this.back = append(this.back,val)
+        this.stack = this.stack[:len(this.stack)-1]
+    }
+    val := this.back[len(this.back)-1]
+    this.back = this.back[:len(this.back)-1]
+    return val
+}
+
+
+func (this *MyQueue) Peek() int {
+    for len(this.stack) != 0{
+        val := this.stack[len(this.stack)-1]
+        this.back = append(this.back,val)
+        this.stack = this.stack[:len(this.stack)-1]
+    }
+    return this.back[len(this.back)-1]
+}
+
+
+func (this *MyQueue) Empty() bool {
+    return len(this.stack) == 0 && len(this.back) == 0
+}
+
+
+/**
+ * Your MyQueue object will be instantiated and called as such:
+ * obj := Constructor();
+ * obj.Push(x);
+ * param_2 := obj.Pop();
+ * param_3 := obj.Peek();
+ * param_4 := obj.Empty();
+ */
+
class MyQueue {
+public:
+    stack<int> st1;
+    stack<int> st2;
+    MyQueue() {
+
+    }
+    
+    void push(int x) {
+        while(!st2.empty()){
+            int a = st2.top();
+            st2.pop();
+            st1.push(a);
+        }
+        st1.push(x);
+    }
+    
+    int pop() {
+        while(!st1.empty()){
+            int a = st1.top();
+            st1.pop();
+            st2.push(a);
+        }
+        int res = st2.top();
+        st2.pop();
+        return res;
+    }
+    
+    int peek() {
+        while(!st1.empty()){
+            int a = st1.top();
+            st1.pop();
+            st2.push(a);
+        }
+        int res = st2.top();
+        return res;
+    }
+    
+    bool empty() {
+        return st1.size() == 0 && st2.size() == 0;
+    }
+};
+

225. 用队列实现栈

+

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

+

实现 MyStack 类:

+

void push(int x) 将元素 x 压入栈顶。

+

int pop() 移除并返回栈顶元素。

+

int top() 返回栈顶元素。

+

boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

+

注意:你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

+
type MyStack struct {
+    queue1 []int
+    queue2 []int
+}
+
+
+func Constructor() MyStack {
+    return MyStack{
+        queue1 : make([]int,0),
+        queue2 : make([]int,0),
+    }   
+}
+
+
+func (this *MyStack) Push(x int)  {
+    this.queue1 = append(this.queue1,x)
+}
+
+
+func (this *MyStack) Pop() int {
+    for len(this.queue1) != 1{
+        this.queue2 = append(this.queue2,this.queue1[0])
+        this.queue1 = this.queue1[1:]
+    }
+    val := this.queue1[0]
+    this.queue1 = []int{}
+    this.queue1,this.queue2 = this.queue2,this.queue1
+    return val
+}
+
+
+func (this *MyStack) Top() int {
+    for len(this.queue1) != 1{
+        this.queue2 = append(this.queue2,this.queue1[0])
+        this.queue1 = this.queue1[1:]
+    }
+    this.queue2 = append(this.queue2,this.queue1[0])
+    val := this.queue1[0]
+    this.queue1 = []int{}
+    this.queue1,this.queue2 = this.queue2,this.queue1
+    return val
+}
+
+
+func (this *MyStack) Empty() bool {
+    return len(this.queue1) == 0
+}
+
+
+/**
+ * Your MyStack object will be instantiated and called as such:
+ * obj := Constructor();
+ * obj.Push(x);
+ * param_2 := obj.Pop();
+ * param_3 := obj.Top();
+ * param_4 := obj.Empty();
+ */
+
class MyStack {
+public:
+    queue<int> q1;
+    queue<int> q2;
+    MyStack() {
+
+    }
+    
+    void push(int x) {
+        q1.push(x);
+    }
+    
+    int pop() {
+        while(q1.size() != 1){
+            q2.push(q1.front());
+            q1.pop();
+        }
+        int a = q1.front();
+        q1.pop();
+        while(!q2.empty()){
+            q1.push(q2.front());
+            q2.pop();
+        }
+        return a;
+    }
+    
+    int top() {
+        while(q1.size() != 1){
+            q2.push(q1.front());
+            q1.pop();
+        }
+        int a = q1.front();
+        q2.push(a);
+        q1.pop();
+        while(!q2.empty()){
+            q1.push(q2.front());
+            q2.pop();
+        }
+        return a;
+    }
+    
+    bool empty() {
+        return q1.empty();
+    }
+};
+

20. 有效的括号

+

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

+

有效字符串需满足:

+
    +
  1. 左括号必须用相同类型的右括号闭合。
  2. +
  3. 左括号必须以正确的顺序闭合。
  4. +
  5. 每个右括号都有一个对应的相同类型的左括号。
  6. +
+
func isValid(s string) bool {
+    result := make([]byte,0)
+    for i:=0;i<len(s);i++{
+        if s[i] == '(' || s[i] == '{' || s[i] == '['{
+            result = append(result,s[i])
+        } else{
+            if len(result) == 0{
+                return false
+            }
+            if s[i] == ')' && result[len(result)-1] == '('{
+                result = result[:len(result)-1]
+            } else if s[i] == '}' && result[len(result)-1] == '{'{
+                result = result[:len(result)-1]
+            } else if s[i] == ']' && result[len(result)-1] == '['{
+                result = result[:len(result)-1]
+            } else{
+                return false
+            }
+        }
+    }
+    return len(result) == 0
+}
+
class Solution {
+public:
+    bool isValid(string s) {
+        stack<char> st;
+        for(int i=0;i<s.size();i++){
+            char a = s[i];
+            if(a == '[' || a == '(' || a == '{'){
+                st.push(a);
+            } else{
+                if(st.empty()){
+                    return false;
+                }
+                if(a == ']' && st.top() == '['){
+                    st.pop();
+                } else if (a == '}' && st.top() == '{'){
+                    st.pop();
+                } else if (a == ')' && st.top() == '('){
+                    st.pop();
+                } else{
+                    return false;
+                }
+            }
+        }
+        return st.empty();
+    }
+};
+

1047. 删除字符串中的所有相邻重复项

+

给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

+

在 S 上反复执行重复项删除操作,直到无法继续删除。

+

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

+
func removeDuplicates(s string) string {
+    result := make([]byte,0)
+    for i := 0;i<len(s);i++{
+        if len(result) == 0 || s[i] != result[len(result)-1]{
+            result = append(result,s[i])
+        } else{
+            result = result[:len(result)-1]
+        }
+    }
+    return string(result)
+}
+
class Solution {
+public:
+    string removeDuplicates(string s) {
+        stack<char> st;
+        for(int i=0;i<s.size();i++){
+            char a = s[i];
+            if(st.empty()){
+                st.push(a);
+                continue;
+            }
+            if(st.top() == a){
+                st.pop();
+            } else{
+                st.push(a);
+            }
+        }
+        vector<char> c;
+        while(!st.empty()){
+            c.push_back(st.top());
+            st.pop();
+        }
+        string s2 = "";
+        for(int i=c.size()-1;i>=0;i--){
+            s2 += c[i];
+        }
+        return s2;
+    }
+};
+

150. 逆波兰表达式求值

+

根据 逆波兰表示法,求表达式的值。

+

有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

+

注意 两个整数之间的除法只保留整数部分。

+

可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

+
func evalRPN(tokens []string) int {
+    l := len(tokens)
+    numstack := make([]int,0)
+    for i:=0;i<l;i++{
+        if len(tokens[i]) == 1{
+            if tokens[i] == "+"{
+                b := numstack[len(numstack)-1]
+                a := numstack[len(numstack)-2]
+                numstack[len(numstack)-2] = a+b
+                numstack = numstack[:len(numstack)-1]
+            } else if tokens[i] == "-"{
+                b := numstack[len(numstack)-1]
+                a := numstack[len(numstack)-2]
+                numstack[len(numstack)-2] = a-b
+                numstack = numstack[:len(numstack)-1]
+
+            } else if tokens[i] == "*"{
+                b := numstack[len(numstack)-1]
+                a := numstack[len(numstack)-2]
+                numstack[len(numstack)-2] = a*b
+                numstack = numstack[:len(numstack)-1]
+
+            } else if tokens[i] == "/"{
+                b := numstack[len(numstack)-1]
+                a := numstack[len(numstack)-2]
+                numstack[len(numstack)-2] = a/b
+                numstack = numstack[:len(numstack)-1]
+            } else{
+                numstack = append(numstack,change(tokens[i]))
+            }
+        } else{
+            numstack = append(numstack,change(tokens[i]))
+        }
+    }
+    return numstack[0]
+}
+
+func change(s string) int{
+    sum := 0
+    sign := 1
+    lens := len(s)
+    for i:=lens-1;i>=0;i--{
+        if s[i] == '-'{
+            sum = -sum
+        } else{
+            c := int(s[i] - '0')
+            sum += sign * c
+            sign *= 10
+        }
+    }
+    return sum
+}
+
class Solution {
+public:
+    int evalRPN(vector<string>& tokens) {
+        stack<int> st;
+        for(int i=0;i<tokens.size();i++){
+            string s = tokens[i];
+            if(s == "+" || s == "-" || s == "*" || s == "/"){
+                int a = st.top();
+                st.pop();
+                int b = st.top();
+                st.pop();
+                if(s == "+"){
+                    st.push(a+b);
+                } else if (s == "-"){
+                    st.push(b - a);
+                } else if(s == "*"){
+                    st.push(a * b);
+                } else{
+                    st.push(b/a);
+                }
+            } else{
+                st.push(atoi(s.c_str()));
+            }
+        }
+        return st.top();
+    }
+};
+

347. 前K个高频元素

+

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

+
func topKFrequent(nums []int, k int) []int {
+    ans:=[]int{}
+    map_num:=map[int]int{}
+    for _,item:=range nums {
+        map_num[item]++
+    }
+    for key,_:=range map_num{
+        ans=append(ans,key)
+    }
+    //核心思想:排序
+    //可以不用包函数,自己实现快排
+    sort.Slice(ans,func (a,b int)bool{
+        return map_num[ans[a]]>map_num[ans[b]]
+    })
+    return ans[:k]
+}
+
class Solution {
+public:
+    // 小顶堆
+    class mycomparison {
+    public:
+        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
+            return lhs.second > rhs.second;
+        }
+    };
+    vector<int> topKFrequent(vector<int>& nums, int k) {
+        // 要统计元素出现频率
+        unordered_map<int, int> map; // map<nums[i],对应出现的次数>
+        for (int i = 0; i < nums.size(); i++) {
+            map[nums[i]]++;
+        }
+
+        // 对频率排序
+        // 定义一个小顶堆,大小为k
+        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
+
+        // 用固定大小为k的小顶堆,扫描所有频率的数值
+        for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
+            pri_que.push(*it);
+            if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
+                pri_que.pop();
+            }
+        }
+
+        // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
+        vector<int> result(k);
+        for (int i = k - 1; i >= 0; i--) {
+            result[i] = pri_que.top().first;
+            pri_que.pop();
+        }
+        return result;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
代码随想录-栈与队列
+
https://zhangzhao219.github.io/2024/03/13/Leetcode/programmercarl/programmercarl-sq/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月13日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/14/Leetcode/programmercarl/programmercarl-gd/index.html b/2024/03/14/Leetcode/programmercarl/programmercarl-gd/index.html new file mode 100644 index 000000000..618cbd895 --- /dev/null +++ b/2024/03/14/Leetcode/programmercarl/programmercarl-gd/index.html @@ -0,0 +1,1532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-贪心 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-贪心

+ + +
+ +

代码随想录-贪心

+ +

贪心

+

455. 分发饼干

+

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

+

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

+

贪心策略:对于每个孩子,找到能满足他的最小饼干,并分配给他。从胃口最大的孩子开始,从最大的饼干开始。

+
func findContentChildren(g []int, s []int) int {
+    sort.Slice(g, func(i, j int) bool {return g[i] > g[j]})
+    sort.Slice(s, func(i, j int) bool {return s[i] > s[j]})
+    leng := len(g)
+    lens := len(s)
+    si := 0
+    gi := 0
+    childsum := 0
+    for{
+        if (si == lens) || (gi == leng){
+            break
+        }
+        if s[si] >= g[gi]{
+            childsum++
+            si++
+        }
+        gi++
+    }
+    return childsum
+}
+
class Solution {
+public:
+    int findContentChildren(vector<int>& g, vector<int>& s) {
+        sort(g.begin(), g.end());
+        sort(s.begin(), s.end());
+        int gindex = 0;
+        int sindex = 0;
+        while(gindex != g.size() && sindex != s.size()){
+            if(s[sindex] >= g[gindex]){
+                gindex += 1;
+                sindex += 1;
+            } else{
+                sindex += 1;
+            }
+        }
+        return gindex;
+    }
+};
+

376. 摆动序列

+

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

+

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

+

相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
+子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

+

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

+

贪心策略:统计整个序列的峰值,但是序列的两个端点不好统计,需要进行特殊判断。

+

这里的技巧是结尾直接加上,开头的判断融入到循环内部

+
func wiggleMaxLength(nums []int) int {
+    lennums := len(nums)
+    if lennums == 1{
+        return 1
+    } else if lennums == 2{
+        if nums[1] != nums[0]{
+            return 2
+        }
+        return 1
+    } else{
+        result := 1
+        predif := 0
+        for i:=0;i<=lennums-2;i++{
+            curdif := nums[i+1] - nums[i]
+            if (predif <= 0 && curdif > 0) || (predif >= 0 && curdif < 0){
+                result++
+                predif = curdif
+            }
+        }
+        if result == 2 && nums[0] == nums[lennums-1]{
+            return 1
+        }
+        return result
+    }
+}
+
class Solution {
+public:
+    int wiggleMaxLength(vector<int>& nums) {
+        vector<vector<int> > dp(nums.size(), vector<int>(2,1));
+        dp[0][0] = 1;
+        dp[0][1] = 1;
+        for(int i=1;i<nums.size();i++){
+            for(int j=0;j<i;j++){
+                if(nums[j] < nums[i]){
+                    dp[i][0] = max(dp[i][0],dp[j][1]+1);
+                }
+                if(nums[j] > nums[i]){
+                    dp[i][1] = max(dp[i][1], dp[j][0] + 1);
+                }
+            }
+        }
+        return max(dp[nums.size()-1][0], dp[nums.size()-1][1]);
+    }
+};
+

55. 跳跃游戏

+

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

+

数组中的每个元素代表你在该位置可以跳跃的最大长度。

+

判断你是否能够到达最后一个下标。

+

贪心策略:跳几步不重要,重要的是跳一次的覆盖范围。只要覆盖的范围能超过最后一个下标即可

+

这里的更新注意是遍历的范围变化

+
func canJump(nums []int) bool {
+    lennums := len(nums)
+    result := 0
+    for i:=0;i<=result;i++{
+        result = max(result,i+nums[i])
+        if result >= lennums - 1{
+            return true
+        }
+    }
+    return false
+}
+
+func max(a,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    bool canJump(vector<int>& nums) {
+        vector<bool> judge(nums.size(),false);
+        judge[0] = true;
+        for(int i=0;i<nums.size();i++){
+            if(judge[i] == true){
+                for(int j=i;j<=i+nums[i];j++){
+                    if(j < nums.size()){
+                        judge[j] = true;
+                        if(judge[nums.size()-1] == true){
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return judge[nums.size()-1];
+    }
+};
+

45. 跳跃游戏II

+

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。

+

数组中的每个元素代表你在该位置可以跳跃的最大长度。

+

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

+

假设你总是可以到达数组的最后一个位置。

+

贪心策略:记录当前的覆盖范围和下一步的覆盖范围,覆盖范围超过了最后的下标就可以

+

代码不会写。。。。

+
func jump(nums []int) int {
+    lennums := len(nums)
+    curDistance := 0;    // 当前覆盖的最远距离下标
+    ans := 0;            // 记录走的最大步数
+    nextDistance := 0;   // 下一步覆盖的最远距离下标
+    for i := 0; i < lennums - 1; i++ { // 注意这里是小于nums.size() - 1,这是关键所在
+        nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖的最远距离下标
+        if i == curDistance {                 // 遇到当前覆盖的最远距离下标
+            curDistance = nextDistance;         // 更新当前覆盖的最远距离下标
+            ans++;
+        }
+    }
+    return ans;
+}
+func max(a,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int jump(vector<int>& nums) {
+        int curDistance = 0;    // 当前覆盖的最远距离下标
+        int ans = 0;            // 记录走的最大步数
+        int nextDistance = 0;   // 下一步覆盖的最远距离下标
+        for (int i = 0; i < nums.size() - 1; i++) { // 注意这里是小于nums.size() - 1,这是关键所在
+            nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖的最远距离下标
+            if (i == curDistance) {                 // 遇到当前覆盖的最远距离下标
+                curDistance = nextDistance;         // 更新当前覆盖的最远距离下标
+                ans++;
+            }
+        }
+        return ans;
+    }
+};
+

1005. K次取反后最大化的数组和

+

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

+
    +
  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]
  • +
+

重复这个过程恰好 k 次。可以多次选择同一个下标 i

+

以这种方式修改数组后,返回数组 可能的最大和

+

贪心策略:计数数组中有多少负值,从负值最大的开始向最小的逐渐变为相反数。如果负数都变为正数了,就对最小的正数进行重复改变。

+

落实到代码上,对数组进行排序,然后一边计数一边加和即可。

+
func largestSumAfterKNegations(nums []int, k int) int {
+    sort.Slice(nums, func(i, j int) bool {return nums[i] < nums[j]})
+
+    minnum := 1000
+    lennum := len(nums)
+    sum := 0
+    for i:=0;i<lennum;i++{
+        if nums[i] < 0{
+            if k != 0{
+                k--
+                nums[i] = -nums[i]
+            }
+        }
+        sum += nums[i]
+        minnum = min(minnum,nums[i])
+    }
+    if k % 2 == 1{
+        sum -= 2 * minnum
+    }
+    return sum
+  
+}
+func min(a,b int) int {
+    if a <b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int largestSumAfterKNegations(vector<int>& nums, int k) {
+        int maxsum = 0;
+        sort(nums.begin(),nums.end());
+        for(int i=0;i<nums.size() && k > 0;i++,k--){
+            if(nums[i] < 0){
+                nums[i] = -nums[i];
+            } else{
+                break;
+            }
+        }
+        for(int i=0;i<nums.size();i++){
+            maxsum += nums[i];
+        }
+        if(k == 0 || k % 2 == 0){
+            return maxsum;
+        }
+        int minvalue = nums[0];
+        for(int i=0;i<nums.size();i++){
+            minvalue = min(minvalue, nums[i]);
+        }
+        return maxsum - 2 * minvalue;
+    }
+};
+

134. 加油站

+

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

+

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

+

给定两个整数数组 gascost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

+

贪心策略:首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。每个加油站的剩余量rest[i]为gas[i] - cost[i]。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。

+
func canCompleteCircuit(gas []int, cost []int) int {
+    lengas := len(gas)
+    totalgas := 0
+    totalcost := 0
+    start := 0
+    sum := 0
+    for i:=0;i<lengas;i++{
+        totalgas += gas[i]
+        totalcost += cost[i]
+        sum += gas[i] - cost[i]
+        if sum < 0{
+            start = i+1
+            sum = 0
+        }
+    }
+    if totalcost > totalgas{
+        return -1
+    }
+    return start
+}
+
class Solution {
+public:
+    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
+        int curSum = 0;
+        int totalSum = 0;
+        int start = 0;
+        for (int i = 0; i < gas.size(); i++) {
+            curSum += gas[i] - cost[i];
+            totalSum += gas[i] - cost[i];
+            if (curSum < 0) {   // 当前累加rest[i]和 curSum一旦小于0
+                start = i + 1;  // 起始位置更新为i+1
+                curSum = 0;     // curSum从0开始
+            }
+        }
+        if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
+        return start;
+    }
+};
+

135. 分发糖果

+

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

+

你需要按照以下要求,给这些孩子分发糖果:

+
    +
  • 每个孩子至少分配到 1 个糖果。
  • +
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。
  • +
+

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

+

贪心策略:从左到右遍历一遍,从右到左遍历一遍,每一次只看一侧的孩子就可以

+

需要注意在右往左遍历的时候不要毁掉左往右遍历好的

+
func candy(ratings []int) int {
+    lenratings := len(ratings)
+    result := make([]int,lenratings)
+    for i:=0;i<lenratings-1;i++{
+        if ratings[i+1] > ratings[i]{
+            result[i+1] = result[i] + 1
+        }
+    }
+    for i:=lenratings-1;i>0;i--{
+        if ratings[i-1] > ratings[i] && result[i-1] <= result[i]{
+            result[i-1] = result[i] + 1
+        }
+    }
+    sum := 0
+    for i:=0;i<lenratings;i++{
+        sum += result[i]
+    }
+    return sum + lenratings
+}
+
class Solution {
+public:
+    int candy(vector<int>& ratings) {
+        vector<int> temp(ratings.size(),1);
+        for(int i=0;i<ratings.size();i++){
+            if(i == 0){
+                continue;
+            }
+            if(ratings[i] > ratings[i-1]){
+                temp[i] = temp[i-1] + 1;
+            }
+        }
+        for(int i=ratings.size()-1;i>=0;i--){
+            if(i == ratings.size()-1){
+                continue;
+            }
+            if(ratings[i] > ratings[i+1]){
+                temp[i] = max(temp[i], temp[i+1] + 1);
+            }
+        }
+        int totalsum = 0;
+        for(int i=0;i<temp.size();i++){
+            totalsum += temp[i];
+        }
+        return totalsum;
+    }
+};
+

860. 柠檬水找零

+

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

+

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

+

注意,一开始你手头没有任何零钱。

+

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

+

贪心策略:直接模拟就好了,没有什么意思

+
func lemonadeChange(bills []int) bool {
+    bill10 := 0
+    bill5 := 0
+    for i:=0;i<len(bills);i++{
+        if bills[i] == 5{
+            bill5 += 1
+        } else if bills[i] == 10{
+            bill10 += 1
+            if bill5 > 0{
+                bill5 -= 1
+            } else{
+                return false
+            }
+        } else{
+            if bill10 >= 1 && bill5 >= 1{
+                bill5 -= 1
+                bill10 -= 1
+            } else if bill5 >= 3{
+                bill5 -= 3
+            } else{
+                return false
+            }
+        }
+    }
+    return true
+}
+
class Solution {
+public:
+    bool lemonadeChange(vector<int>& bills) {
+        map<int,int> mp;
+        mp[5] = 0;
+        mp[10] = 0;
+        for(int i=0;i<bills.size();i++){
+            if(bills[i] == 5){
+                mp[5] += 1;
+            } else if (bills[i] == 10){
+                if(mp[5] <= 0){
+                    return false;
+                }
+                mp[5] -= 1;
+                mp[10] += 1;
+            } else{
+                if(mp[10] >= 1 && mp[5] >= 1){
+                    mp[5] -= 1;
+                    mp[10] -= 1;
+                } else if(mp[5] >= 3){
+                    mp[5] -= 3;
+                } else{
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+};
+

406. 根据身高重建队列

+

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki个身高大于或等于 hi 的人。

+

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

+

贪心策略:身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。然后只需要按照k为下标重新插入队列就可以了

+
func reconstructQueue(people [][]int) [][]int {
+    sort.Slice(
+        people, func(i,j int) bool{
+            if people[i][0] == people[j][0]{
+                return people[i][1] < people[j][1]
+            }
+            return people[i][0] > people[j][0]
+        })
+    result := make([][]int,0)
+    for _,info := range people{
+        result = append(result,info)
+        copy(result[info[1] +1:], result[info[1]:])
+        result[info[1]] = info
+    }
+    return result
+}
+
class Solution {
+public:
+    static bool cmp(const vector<int>& a, const vector<int>& b) {
+        if (a[0] == b[0]) return a[1] < b[1];
+        return a[0] > b[0];
+    }
+    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
+        sort (people.begin(), people.end(), cmp);
+        vector<vector<int>> que;
+        for (int i = 0; i < people.size(); i++) {
+            int position = people[i][1];
+            que.insert(que.begin() + position, people[i]);
+        }
+        return que;
+    }
+};
+

452. 用最少数量的箭引爆气球

+

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中 points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

+

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstartxend, 且满足 xstart ≤ x ≤ xend, 则该气球会被 引爆。 可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

+

给你一个数组 points , 返回引爆所有气球所必须射出的 最小 弓箭数 。

+

贪心策略:按照最小末尾坐标排序,射出的箭在保证将最末尾的气球引爆的同时最好引爆更多的气球。

+
func findMinArrowShots(points [][]int) int {
+    lenpoints := len(points)
+    sort.Slice(
+        points,func(i,j int) bool{
+            if points[i][1] == points[j][1]{
+                return points[i][0] < points[j][0]
+            }
+            return points[i][1] < points[j][1]
+        })
+    count := 0
+    start := 0
+    for start < lenpoints{
+        count += 1
+        arrow := points[start][1]
+        for i:=start;i<lenpoints;i++{
+            start = i+1
+            if points[i][0] <= arrow && points[i][1] >= arrow{
+                continue
+            } else{
+                start -= 1
+                break
+            }
+        }
+        if start == lenpoints{
+            break
+        }
+    }
+    return count
+}
+
class Solution {
+public:
+    static bool cmp(const vector<int>& a, const vector<int>& b){
+        return a[0] < b[0];
+    }
+    int findMinArrowShots(vector<vector<int>>& points) {
+        sort(points.begin(), points.end(), cmp);
+        int result = 1;
+        for(int i=1;i<points.size();i++){
+            if(points[i][0] > points[i-1][1]){
+                result += 1;
+            } else{
+                points[i][1] = min(points[i-1][1], points[i][1]);
+            }
+        }
+        return result;
+    }
+};
+

435. 无重叠区间

+

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回需要移除区间的最小数量,使剩余区间互不重叠。

+

贪心策略:与上一道题相同,仍然是对后面的值进行排序,然后从头到尾遍历去除即可。

+
func eraseOverlapIntervals(intervals [][]int) int {
+    leni := len(intervals)
+    sort.Slice(
+        intervals,func(i,j int) bool{
+            if intervals[i][1] == intervals[j][1]{
+                return intervals[i][0] < intervals[j][0]
+            }
+            return intervals[i][1] < intervals[j][1]
+        })
+    count := 0
+    min1 := intervals[0][1]
+    for i:=1;i<leni;i++{
+        if intervals[i][0] >= min1{
+            min1 = intervals[i][1]
+        } else{
+            count += 1
+        }
+    }
+    return count
+}
+
class Solution {
+public:
+    static bool cmp(const vector<int>& a, const vector<int>& b){
+        if(a[1] == b[1]){
+            return a[0] < b[0];
+        }
+        return a[1] < b[1];
+    }
+    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
+        int result = 0;
+        sort(intervals.begin(), intervals.end(), cmp);
+        int nowend = intervals[0][1];
+        for(int i=1;i<intervals.size();i++){
+            if(intervals[i][0] < nowend){
+                result += 1;
+            } else{
+                nowend = intervals[i][1];
+            }
+        }
+        return result;
+    }
+};
+

763. 划分字母区间

+

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

+

贪心策略:与上一题的贪心策略也是相同的,只是多了一步字母到区间的转换

+

注意这道题的区别是看0位置进行排序,因为是一个包括的区间,与前面只看后面1位置不相同

+
func partitionLabels(s string) []int {
+    result := make([][]int,26)
+    for i:=0;i<26;i++{
+        result[i] = make([]int,2)
+        result[i][0] = 1000
+        result[i][1] = 1000
+    }
+    s1 := []rune(s)
+    lens := len(s1)
+    for i:=0;i<lens;i++{
+        index := int(s1[i]) - 97
+        if result[index][0] == 1000{
+            result[index][0] = i  
+        }
+        result[index][1] = i
+    }
+    sort.Slice(
+        result,func(i,j int)bool{
+            if result[i][0] == result[j][0]{
+                return result[i][1] < result[j][1]
+            } 
+            return result[i][0] < result[j][0]
+        })
+    submit := make([]int,0)
+    startmin := result[0][0]
+    endmax := result[0][1]
+    for i:=1;i<26;i++{
+        if result[i][0] > endmax{
+            submit = append(submit,endmax-startmin+1)
+            endmax = result[i][1]
+            startmin = result[i][0]
+        } else{
+            startmin = min(startmin,result[i][0])
+            endmax = max(endmax,result[i][1])
+        }
+    }
+    if startmin != 1000{
+        submit = append(submit,endmax-startmin+1)
+    }
+    return submit
+}
+
+func min(a,b int) int{
+    if a < b{
+        return a
+    }
+    return b
+}
+
+func max(a,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    vector<int> partitionLabels(string s) {
+        int hash[27] = {0};
+        for (int i = 0; i < s.size(); i++) { 
+            hash[s[i] - 'a'] = i;
+        }
+        vector<int> result;
+        int left = 0;
+        int right = 0;
+        for (int i = 0; i < s.size(); i++) {
+            right = max(right, hash[s[i] - 'a']); 
+            if (i == right) {
+                result.push_back(right - left + 1);
+                left = i + 1;
+            }
+        }
+        return result;
+    }
+};
+

56. 合并区间

+

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

+

贪心策略:相当于上一道题的简化版

+
func merge(intervals [][]int) [][]int {
+    leni := len(intervals)
+    sort.Slice(
+        intervals,func(i,j int) bool {
+            if intervals[i][0] == intervals[j][0]{
+                return intervals[i][1] < intervals[j][1]
+            }
+            return intervals[i][0] < intervals[j][0]
+        })
+    result := make([][]int,0)
+    minstart := intervals[0][0]
+    maxend := intervals[0][1]
+    for i:=1;i<leni;i++{
+        if intervals[i][0] > maxend{
+            temp := []int{minstart,maxend}
+            result = append(result,temp)
+            minstart = intervals[i][0]
+            maxend = intervals[i][1]
+        } else{
+            minstart = min(minstart,intervals[i][0])
+            maxend = max(maxend,intervals[i][1])
+        }
+    }
+    temp := []int{minstart,maxend}
+    result = append(result,temp)
+    return result
+} 
+
+func min(a,b int) int{
+    if a < b{
+        return a
+    }
+    return b
+}
+func max(a,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    static bool cmp(const vector<int>& a, const vector<int>& b){
+        if(a[0] == b[0]){
+            return a[1] < b[1];
+        }
+        return a[0] < b[0];
+    }
+    vector<vector<int>> merge(vector<vector<int>>& intervals) {
+        vector<vector<int> > result;
+        sort(intervals.begin(), intervals.end(), cmp);
+        int nowstart = intervals[0][0];
+        int nowend = intervals[0][1];
+        for(int i=1;i<intervals.size();i++){
+            if(intervals[i][0] <= nowend){
+                nowstart = min(nowstart,intervals[i][0]);
+                nowend = max(nowend,intervals[i][1]);
+            } else{
+                result.push_back(vector<int>{nowstart, nowend});
+                nowstart = intervals[i][0];
+                nowend = intervals[i][1];
+            }
+        }
+        result.push_back(vector<int>{nowstart, nowend});
+        return result;
+    }
+};
+

738. 单调递增的数字

+

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

+

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

+

贪心策略:局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]–,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数

+

从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。

+

因此应该从后向前遍历

+
func monotoneIncreasingDigits(N int) int {
+    s := strconv.Itoa(N)//将数字转为字符串,方便使用下标
+    ss := []byte(s)//将字符串转为byte数组,方便更改。
+    n := len(ss)
+    if n <= 1 {
+        return N
+    }
+    for i:=n-1 ; i>0; i-- {
+        if ss[i-1] > ss[i] {//前一个大于后一位,前一位减1,后面的全部置为9
+            ss[i-1] -= 1
+            for j := i ; j < n; j++ {//后面的全部置为9
+                ss[j] = '9'
+            }
+        } 
+    }
+    res, _ := strconv.Atoi(string(ss))
+    return res 
+}
+
class Solution {
+public:
+    int monotoneIncreasingDigits(int n) {
+        string strNum = to_string(n);
+        int flag = strNum.size();
+        for (int i = strNum.size() - 1; i > 0; i--) {
+            if (strNum[i - 1] > strNum[i] ) {
+                flag = i;
+                strNum[i - 1]--;
+            }
+        }
+        for (int i = flag; i < strNum.size(); i++) {
+            strNum[i] = '9';
+        }
+        return stoi(strNum);
+    }
+};
+

968. 监控二叉树

+

给定一个二叉树,我们在树的节点上安装摄像头。

+

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

+

计算监控树的所有节点所需的最小摄像头数量。

+

局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!

+

确定遍历顺序

+

在二叉树中如何从低向上推导呢?

+

可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。

+

如何隔两个节点放一个摄像头

+

每个节点可能有如下三种状态:

+
    +
  • 该节点无覆盖
  • +
  • 本节点有摄像头
  • +
  • 本节点有覆盖
  • +
+

为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。

+

那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。

+

所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了

+

后续待补充

+
const inf = math.MaxInt64 / 2
+
+func minCameraCover(root *TreeNode) int {
+    var dfs func(*TreeNode) (a, b, c int)
+    dfs = func(node *TreeNode) (a, b, c int) {
+        if node == nil {
+            return inf, 0, 0
+        }
+        lefta, leftb, leftc := dfs(node.Left)
+        righta, rightb, rightc := dfs(node.Right)
+        a = leftc + rightc + 1
+        b = min(a, min(lefta+rightb, righta+leftb))
+        c = min(a, leftb+rightb)
+        return
+    }
+    _, ans, _ := dfs(root)
+    return ans
+}
+
+func min(a, b int) int {
+    if a <= b {
+        return a
+    }
+    return b
+}
+
class Solution {
+private:
+    int result;
+    int traversal(TreeNode* cur) {
+        if (cur == NULL) return 2;
+        int left = traversal(cur->left);    // 左
+        int right = traversal(cur->right);  // 右
+        if (left == 2 && right == 2) return 0;
+        else if (left == 0 || right == 0) {
+            result++;
+            return 1;
+        } else return 2;
+    }
+public:
+    int minCameraCover(TreeNode* root) {
+        result = 0;
+        if (traversal(root) == 0) { // root 无覆盖
+            result++;
+        }
+        return result;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
代码随想录-贪心
+
https://zhangzhao219.github.io/2024/03/14/Leetcode/programmercarl/programmercarl-gd/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/14/Leetcode/programmercarl/programmercarl-tp/index.html b/2024/03/14/Leetcode/programmercarl/programmercarl-tp/index.html new file mode 100644 index 000000000..fc50bca58 --- /dev/null +++ b/2024/03/14/Leetcode/programmercarl/programmercarl-tp/index.html @@ -0,0 +1,927 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-双指针法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-双指针法

+ + +
+ +

代码随想录-双指针法

+ +

双指针法

+

15. 三数之和

+

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

+

注意:答案中不可以包含重复的三元组。

+
func threeSum(nums []int) [][]int {
+    result := make([][]int, 0)
+    sort.Slice(nums,func(i,j int) bool {return nums[i] < nums[j]})
+    lennums := len(nums)
+    for i :=0;i<lennums;i++ {
+        if i > 0 && nums[i] == nums[i-1]{
+            continue
+        }
+        left := i+1
+        right := lennums-1
+        for left < right{
+            if nums[i] + nums[left] + nums[right] == 0{
+                result = append(result, []int{nums[i],nums[left],nums[right]})
+                for left < right && nums[left] == nums[left+1]{
+                    left++
+                }
+                for left < right && nums[right] == nums[right-1]{
+                    right--
+                }
+                left++
+                right--
+            } else if nums[i] + nums[left] + nums[right] < 0{
+                left++
+            } else{
+                right--
+            }
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<vector<int>> threeSum(vector<int>& nums) {
+        vector<vector<int> > result;
+        sort(nums.begin(), nums.end());
+        for(int i=0;i<nums.size();i++){
+            int j = i + 1;
+            int k = nums.size() - 1;
+            if(i != 0 && nums[i] == nums[i-1]){
+                continue;
+            }
+            while(j < k){
+                if(nums[i] + nums[j] + nums[k] == 0){
+                    result.push_back(vector<int>{nums[i], nums[j], nums[k]});
+                    while(j < k && nums[j] == nums[j+1]){
+                        j++;
+                    }
+                    while(j < k && nums[k] == nums[k-1]){
+                        k--;
+                    }
+                    j++;
+                    k--;
+                } else if(nums[i] + nums[j] + nums[k] > 0){
+                    k--;
+                } else{
+                    j++;
+                }
+            }
+        }
+        return result;
+    }
+};
+

18. 四数之和

+

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

+

0 <= a, b, c, d < n,a、b、c 和 d 互不相同,nums[a] + nums[b] + nums[c] + nums[d] == target

+

你可以按 任意顺序 返回答案 。

+
func fourSum(nums []int, target int) [][]int {
+    sort.Slice(nums,func(a,b int)bool{return nums[a] < nums[b]})
+    lennums := len(nums)
+    result := make([][]int,0)
+    for i:=0;i<=lennums-4;i++{
+        if i > 0 && nums[i] == nums[i-1]{
+            continue
+        }
+        for j:=i;j<lennums;j++{
+            if j < lennums - 1 && nums[j] == nums[j+1]{
+                continue
+            }
+            left := i+1
+            right := j-1
+            for left < right{
+                if nums[i] + nums[j] + nums[left] + nums[right] == target{
+                    result = append(result,[]int{nums[i],nums[j],nums[left],nums[right]})
+                    for left < right && nums[left] == nums[left+1]{
+                        left++
+                    }
+                    for left < right && nums[right] == nums[right-1]{
+                        right--
+                    }
+                    left++
+                    right--
+                } else if nums[i] + nums[j] + nums[left] + nums[right] < target{
+                    left++
+                } else{
+                    right--
+                }
+            }
+        }
+    }
+
+    return result
+}
+
class Solution {
+public:
+    vector<vector<int>> fourSum(vector<int>& nums, int target) {
+        vector<vector<int> > result;
+        sort(nums.begin(), nums.end());
+        for(int i=0;i<nums.size();i++){
+            if(i > nums.size()-4){
+                break;
+            }
+            if(i > 0 && nums[i] == nums[i-1]){
+                continue;
+            }
+            for(int l=i+3;l<nums.size();l++){
+                if(l < nums.size()-1 && nums[l] == nums[l+1]){
+                    continue;
+                }
+                int j = i + 1;
+                int k = l - 1;
+
+                while(j < k){
+                    if((long long)nums[i] + nums[j] + nums[k] + nums[l] == (long long)target){
+                        result.push_back(vector<int>{nums[i], nums[j], nums[k], nums[l]});
+                        while(j < k && nums[j] == nums[j+1]){
+                            j++;
+                        }
+                        while(j < k && nums[k] == nums[k-1]){
+                            k--;
+                        }
+                        j++;
+                        k--;
+                    } else if((long long)nums[i] + nums[j] + nums[k] + nums[l] > (long long)target){
+                        k--;
+                    } else{
+                        j++;
+                    }
+                }
+            }
+        }
+        return result;
+    }
+};
+

总结

+

数组篇

+

数组:就移除个元素很难么? (opens new window)中,原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。

+

一些同学可能会写出如下代码(伪代码):

+
for (int i = 0; i < array.size(); i++) {
+    if (array[i] == target) {
+        array.erase(i);
+    }
+}
+

这个代码看上去好像是O(n)的时间复杂度,其实是O(n^2)的时间复杂度,因为erase操作也是O(n)的操作。

+

所以此时使用双指针法才展现出效率的优势:通过两个指针在一个for循环下完成两个for循环的工作。

+

字符串篇

+

字符串:这道题目,使用库函数一行代码搞定 (opens new window)中讲解了反转字符串,注意这里强调要原地反转,要不然就失去了题目的意义。

+

使用双指针法, 定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。 ,时间复杂度是O(n)。

+

替换空格 (opens new window)中介绍使用双指针填充字符串的方法,如果想把这道题目做到极致,就不要只用额外的辅助空间了!

+

思路就是首先扩充数组到每个空格替换成"%20"之后的大小。然后双指针从后向前替换空格。

+

有同学问了,为什么要从后向前填充,从前向后填充不行么?

+

从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。

+

其实很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

+

那么在字符串:花式反转还不够! (opens new window)中,我们使用双指针法,用O(n)的时间复杂度完成字符串删除类的操作,因为题目要删除冗余空格。

+

在删除冗余空格的过程中,如果不注意代码效率,很容易写成了O(n^2)的时间复杂度。其实使用双指针法O(n)就可以搞定。

+

主要还是大家用erase用的比较随意,一定要注意for循环下用erase的情况,一般可以用双指针写效率更高!

+

链表篇

+

翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。

+

链表:听说过两天反转链表又写不出来了? (opens new window)中,讲如何使用双指针法来翻转链表,只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。

+

思路还是很简单的,代码也不长,但是想在白纸上一次性写出bugfree的代码,并不是容易的事情。

+

在链表中求环,应该是双指针在链表里最经典的应用,在链表:环找到了,那入口呢? (opens new window)中讲解了如何通过双指针判断是否有环,而且还要找到环的入口。

+

使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

+

那么找到环的入口,其实需要点简单的数学推理,我在文章中把找环的入口清清楚楚的推理的一遍,如果对找环入口不够清楚的同学建议自己看一看链表:环找到了,那入口呢? (opens new window)

+

N数之和篇

+

哈希表:解决了两数之和,那么能解决三数之和么? (opens new window)中,讲到使用哈希法可以解决1.两数之和的问题

+

其实使用双指针也可以解决1.两数之和的问题,只不过1.两数之和求的是两个元素的下标,没法用双指针,如果改成求具体两个元素的数值就可以了,大家可以尝试用双指针做一个leetcode上两数之和的题目,就可以体会到我说的意思了。

+

使用了哈希法解决了两数之和,但是哈希法并不使用于三数之和!

+

使用哈希法的过程中要把符合条件的三元组放进vector中,然后在去去重,这样是非常费时的,很容易超时,也是三数之和通过率如此之低的根源所在。

+

去重的过程不好处理,有很多小细节,如果在面试中很难想到位。

+

时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。

+

所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。

+

只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。

+

双指针法:一样的道理,能解决四数之和 (opens new window)中,讲到了四数之和,其实思路是一样的,在三数之和的基础上再套一层for循环,依然是使用双指针法。

+

对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。

+

同样的道理,五数之和,n数之和都是在这个基础上累加。

+ + +
+ +
+
+ + + + + + +
+
+
代码随想录-双指针法
+
https://zhangzhao219.github.io/2024/03/14/Leetcode/programmercarl/programmercarl-tp/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/03/15/Leetcode/programmercarl/programmercarl-ms/index.html b/2024/03/15/Leetcode/programmercarl/programmercarl-ms/index.html new file mode 100644 index 000000000..facc9eb48 --- /dev/null +++ b/2024/03/15/Leetcode/programmercarl/programmercarl-ms/index.html @@ -0,0 +1,1050 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 代码随想录-单调栈 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

代码随想录-单调栈

+ + +
+ +

代码随想录-单调栈

+ +

单调栈

+

739. 每日温度

+

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

+
func dailyTemperatures(temperatures []int) []int {
+    l := len(temperatures)
+    result := make([]int,l)
+    stack := make([]int,0)
+    for i:=0;i<l;i++{
+        if len(stack) == 0 || temperatures[i] <= temperatures[stack[len(stack)-1]]{
+            stack = append(stack,i)
+        } else{
+            for len(stack) > 0 && temperatures[i] > temperatures[stack[len(stack)-1]]{
+                result[stack[len(stack)-1]] = i - stack[len(stack)-1]
+                stack = stack[:len(stack)-1]
+            }
+            stack = append(stack,i)
+        }
+    }
+    return result
+}
+
class Solution {
+public:
+    vector<int> dailyTemperatures(vector<int>& temperatures) {
+        stack<int> st;
+        vector<int> result(temperatures.size(),0);
+        for(int i=0;i<temperatures.size();i++){
+            while(!st.empty() && temperatures[st.top()] < temperatures[i]){
+                result[st.top()] = i - st.top();
+                st.pop();
+            }
+            st.push(i);
+        }
+        return result;
+    }
+};
+

496.下一个更大元素 I

+

nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。

+

给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。

+

对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。

+

返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。

+
func nextGreaterElement(nums1 []int, nums2 []int) []int {
+    lennums1 := len(nums1)
+    ans := make([]int,lennums1)
+    nummap := make(map[int] int)
+    stack := make([]int,0)
+    for i:=0;i<len(nums2);i++{
+        if len(stack) == 0 || nums2[i] <= stack[len(stack)-1]{
+            stack = append(stack,nums2[i])
+        } else{
+            for len(stack) > 0 && nums2[i] > stack[len(stack)-1]{
+                nummap[stack[len(stack)-1]] = nums2[i]
+                stack = stack[:len(stack)-1]
+            }
+            stack = append(stack,nums2[i])
+        }
+    }
+    for i:=0;i<lennums1;i++{
+        a,ok := nummap[nums1[i]]
+        if ok{
+            ans[i] = a
+        } else{
+            ans[i] = -1
+        }
+    }
+    return ans
+}
+
class Solution {
+public:
+    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
+        vector<int> result(nums1.size(),-1);
+        stack<int> st;
+        unordered_map<int, int> mp;
+        for(int i=0;i<nums1.size();i++){
+            mp[nums1[i]] = i;
+        }
+        for(int i=0;i<nums2.size();i++){
+            while(!st.empty() && st.top() < nums2[i]){
+                if(mp.find(st.top()) != mp.end()){
+                    result[mp[st.top()]] = nums2[i];
+                }
+                st.pop();
+            }
+            st.push(nums2[i]);
+        }
+        return result;
+    }
+};
+

503. 下一个更大元素II

+

给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。

+

数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。

+
func nextGreaterElements(nums []int) []int {
+    lennums1 := len(nums)
+    for i:=0;i<lennums1-1;i++{
+        nums = append(nums,nums[i])
+    }
+    lennums := len(nums)
+    result := make([]int,lennums)
+    for i:=0;i<lennums;i++{
+        result[i] = -1
+    }
+    stack := make([]int,0)
+    for i:=0;i<lennums;i++{
+        if len(stack) == 0 || nums[i] <= nums[stack[len(stack)-1]]{
+            stack = append(stack,i)
+        } else{
+            for len(stack) > 0 &&  nums[i] > nums[stack[len(stack)-1]]{
+                result[stack[len(stack)-1]] = nums[i]
+                stack = stack[:len(stack)-1]
+            }
+            stack = append(stack,i)
+        }
+    }
+    return result[:lennums1]
+}
+
class Solution {
+public:
+    vector<int> nextGreaterElements(vector<int>& nums) {
+        vector<int> numsall(nums.size()*2);
+        for(int i=0;i<numsall.size();i++){
+            numsall[i] = nums[i%nums.size()];
+        }
+        stack<int> st;
+        vector<int> result(nums.size(),-1);
+        for(int i=0;i<numsall.size();i++){
+            while(!st.empty() && numsall[st.top()] < numsall[i]){
+                if(result[st.top()%nums.size()] == -1){
+                    result[st.top()%nums.size()] = numsall[i];
+                }
+                st.pop();
+            }
+            st.push(i);
+        }
+        return result;
+    }
+};
+

42. 接雨水

+

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

+
func trap(height []int) int {
+    result := 0
+    lenh := len(height)
+    stack := make([]int,0)
+    stack = append(stack,0)
+    for i:=1;i<lenh;i++{
+        if height[i] < height[stack[len(stack)-1]]{
+            stack = append(stack,i)
+        } else if height[i] == height[stack[len(stack)-1]]{
+            stack[len(stack)-1] = i
+        } else{
+            for len(stack) > 0 && height[i] > height[stack[len(stack)-1]]{
+                mid := stack[len(stack)-1]
+                stack = stack[:len(stack)-1]
+                if len(stack) != 0{
+                    h := min(height[stack[len(stack)-1]], height[i]) - height[mid];
+                    w := i - stack[len(stack)-1] - 1;
+                    result += h * w;
+                }
+            }
+            stack = append(stack,i)
+        }
+    }
+    return result
+}
+
+func min(a,b int) int {
+    if a < b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int trap(vector<int>& height) {
+        stack<int> st;
+        st.push(0);
+        int sum = 0;
+        for (int i = 1; i < height.size(); i++) {
+            while (!st.empty() && height[i] > height[st.top()]) {
+                int mid = st.top();
+                st.pop();
+                if (!st.empty()) {
+                    int h = min(height[st.top()], height[i]) - height[mid];
+                    int w = i - st.top() - 1;
+                    sum += h * w;
+                }
+            }
+            st.push(i);
+        }
+        return sum;
+    }
+};
+

84. 柱状图中最大的矩形

+
func largestRectangleArea(heights []int) int {
+    maxarea := 0
+    stack := []int{-1}
+    heights=append(heights,0)
+    l := len(heights)
+    for i:=0;i<l;i++{
+        for len(stack)>1 && heights[i] < heights[stack[len(stack)-1]]{
+            cur := stack[len(stack)-1]
+            stack = stack[:len(stack)-1]
+            left := stack[len(stack)-1]
+            h := heights[cur]
+            w := i - left - 1
+            maxarea = max(maxarea,h*w)
+        }
+        stack = append(stack,i)
+    }
+    return maxarea
+}
+
+func max(a,b int) int{
+    if a > b{
+        return a
+    }
+    return b
+}
+
class Solution {
+public:
+    int largestRectangleArea(vector<int>& heights) {
+        stack<int> st;
+        heights.insert(heights.begin(), 0);
+        heights.push_back(0); 
+        st.push(0);
+        int sum = 0;
+        for (int i = 1; i < heights.size(); i++) {
+            while (heights[i] < heights[st.top()]) {
+                int mid = st.top();
+                st.pop();
+                int w = i - st.top() - 1;
+                int h = heights[mid];
+                sum = max(sum, w * h);
+            }
+            st.push(i);
+        }
+        return sum;
+    }
+};
+

239. 滑动窗口最大值

+

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

+

返回 滑动窗口中的最大值 。

+
func maxSlidingWindow(nums []int, k int) []int {
+    lennum := len(nums)
+    queue := make([]int,0)
+    for i:=0;i<k-1;i++{
+        if len(queue) == 0 || nums[i] < nums[queue[len(queue)-1]]{
+            queue = append(queue,i)
+        } else{
+            for len(queue) > 0 && nums[i] >= nums[queue[len(queue)-1]]{
+                queue = queue[:len(queue)-1]
+            }
+            queue = append(queue,i)
+        }
+    }
+    result := make([]int,0)
+    for i:=k-1;i<lennum;i++{
+        if len(queue) == 0 || nums[i] < nums[queue[len(queue)-1]]{
+            queue = append(queue,i)
+        } else{
+            for len(queue) > 0 && nums[i] >= nums[queue[len(queue)-1]]{
+                queue = queue[:len(queue)-1]
+            }
+            queue = append(queue,i)
+        }
+        result = append(result,nums[queue[0]])
+        if i-(k-1) == queue[0]{
+            queue = queue[1:]
+        } 
+    }
+    return result
+}
+
class Solution {
+private:
+class MyQueue { //单调队列(从大到小)
+public:
+    deque<int> que; // 使用deque来实现单调队列
+    // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
+    // 同时pop之前判断队列当前是否为空。
+    void pop(int value) {
+        if (!que.empty() && value == que.front()) {
+            que.pop_front();
+        }
+    }
+    // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
+    // 这样就保持了队列里的数值是单调从大到小的了。
+    void push(int value) {
+        while (!que.empty() && value > que.back()) {
+            que.pop_back();
+        }
+        que.push_back(value);
+
+    }
+    // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
+    int front() {
+        return que.front();
+    }
+};
+public:
+    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
+        MyQueue que;
+        vector<int> result;
+        for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
+            que.push(nums[i]);
+        }
+        result.push_back(que.front()); // result 记录前k的元素的最大值
+        for (int i = k; i < nums.size(); i++) {
+            que.pop(nums[i - k]); // 滑动窗口移除最前面元素
+            que.push(nums[i]); // 滑动窗口前加入最后面的元素
+            result.push_back(que.front()); // 记录对应的最大值
+        }
+        return result;
+    }
+};
+ +
+ +
+
+ + + + + + +
+
+
代码随想录-单调栈
+
https://zhangzhao219.github.io/2024/03/15/Leetcode/programmercarl/programmercarl-ms/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年3月15日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/04/02/Leetcode/Leetcode-ds/index.html b/2024/04/02/Leetcode/Leetcode-ds/index.html new file mode 100644 index 000000000..f2b524531 --- /dev/null +++ b/2024/04/02/Leetcode/Leetcode-ds/index.html @@ -0,0 +1,1313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-基本数据结构 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-基本数据结构

+ + +
+ +

Leetcode-基本数据结构

+ +

数组

+

7. 整数反转

+

给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。

+

如果反转后整数超过 32 位的有符号整数的范围 [−2<sup>31</sup>,  2<sup>31 </sup>− 1] ,就返回 0。

+

假设环境不允许存储 64 位整数(有符号或无符号)。

+
class Solution {
+public:
+    int reverse(int x) {
+        int rev = 0;
+        while (x != 0) {
+            if (rev < INT_MIN / 10 || rev > INT_MAX / 10) {
+                return 0;
+            }
+            int digit = x % 10;
+            x /= 10;
+            rev = rev * 10 + digit;
+        }
+        return rev;
+    }
+};
+

215. 数组中的第K个最大元素

+

给定整数数组 nums 和整数 k,请返回数组中第 <strong>k</strong> 个最大的元素。

+

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

+

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

+
class Solution {
+public:
+    void quicksort(vector<int>& nums, int start, int end, int k){
+        if(start >= end){
+            return;
+        }
+        int x = nums[start];
+        int left = start;
+        int right = end;
+        while(left < right){
+            while(left < right && nums[right] <= x){
+                right--;
+            }
+            nums[left] = nums[right];
+            while(left < right && nums[left] > x){
+                left++;
+            }
+            nums[right] = nums[left];
+        }
+        nums[left] = x;
+        // start    left    end   
+        // k
+        if(k == left + 1){
+            return;
+        } else if(k < left+1){
+            quicksort(nums, start, left-1,k);
+        } else{
+            quicksort(nums,left+1,end,k);
+        }
+    }
+    int findKthLargest(vector<int>& nums, int k) {
+        quicksort(nums, 0, nums.size()-1,k);
+        return nums[k-1];
+    }
+};
+

347. 前K个高频元素

+

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

+
class Solution {
+public:
+    void quicksort(vector<pair<int,int> >& nums, int start, int end, int k){
+        if(start >= end){
+            return;
+        }
+        pair<int,int> x = nums[start];
+        int left = start;
+        int right = end;
+        while(left < right){
+            while(left < right && nums[right].second <= x.second){
+                right--;
+            }
+            swap(nums[left],nums[right]);
+            while(left < right && nums[left].second > x.second){
+                left++;
+            }
+            swap(nums[left],nums[right]);
+        }
+        nums[left].first = x.first;
+        nums[left].second = x.second;
+        // start  left  end;
+        if(k == left){
+            return;
+        } else if(k > left){
+            quicksort(nums, left+1, end,k);
+        } else{
+            quicksort(nums, start, left-1,k);
+        }
+
+    }
+    vector<int> topKFrequent(vector<int>& nums, int k) {
+        vector<pair<int, int> > result;
+        unordered_map<int, int> mp;
+        for(int i=0;i<nums.size();i++){
+            mp[nums[i]]++;
+        }
+        for(auto it=mp.begin();it!= mp.end();it++){
+            result.push_back({it->first, it->second});
+        }
+        quicksort(result, 0, result.size()-1,k);
+        vector<int> res;
+        for(int i=0;i<k;i++){
+            res.push_back(result[i].first);
+        }
+        return res;
+    }
+};
+

32. 最长有效括号

+

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

+
class Solution {
+public:
+    int longestValidParentheses(string s) {
+        vector<bool> visit(s.size(),false);
+        stack<pair<char,int> > st;
+        for(int i=0;i<s.size();i++){
+            if(st.empty() || s[i] == '(' || (s[i] == ')' && st.top().first == ')')){
+                st.push({s[i],i});
+                continue;
+            }
+            for(int j=st.top().second;j<=i;j++){
+                visit[j] = true;
+            }
+            st.pop();
+        }
+        int maxlength = 0;
+        int index = 0;
+        while(index < visit.size()){
+            int counttemp = 0;
+            while(index < visit.size() && visit[index]){
+                counttemp += 1;
+                index += 1;
+            }
+            maxlength = max(maxlength, counttemp);
+            while(index < visit.size() && !visit[index]){
+                index += 1;
+            }
+        }
+        return maxlength;
+    }
+};
+

2570. 合并两个二维数组 - 求和法

+

给你两个 二维 整数数组 nums1nums2.

+
    +
  • nums1[i] = [id<sub>i</sub>, val<sub>i</sub>] 表示编号为 id<sub>i</sub> 的数字对应的值等于 val<sub>i</sub>
  • +
  • nums2[i] = [id<sub>i</sub>, val<sub>i</sub>] 表示编号为 id<sub>i</sub> 的数字对应的值等于 val<sub>i</sub>
  • +
+

每个数组都包含 互不相同 的 id ,并按 id 以 递增 顺序排列。

+

请你将两个数组合并为一个按 id 以递增顺序排列的数组,并符合下述条件:

+
    +
  • 只有在两个数组中至少出现过一次的 id 才能包含在结果数组内。
  • +
  • 每个 id 在结果数组中 只能出现一次 ,并且其对应的值等于两个数组中该 id 所对应的值求和。如果某个数组中不存在该 id ,则认为其对应的值等于 0
  • +
+

返回结果数组。返回的数组需要按 id 以递增顺序排列。

+
class Solution {
+public:
+    static bool cmp(const vector<int> &a, const vector<int> &b){
+        return a[0] < b[0];
+    }
+    vector<vector<int>> mergeArrays(vector<vector<int>>& nums1, vector<vector<int>>& nums2) {
+        unordered_map<int, int> mp;
+        for(int i=0;i<nums1.size();i++){
+            if(mp.find(nums1[i][0]) != mp.end()){
+                mp[nums1[i][0]] += nums1[i][1];
+            } else{
+                mp[nums1[i][0]] = nums1[i][1];
+            }
+        }
+        for(int i=0;i<nums2.size();i++){
+            if(mp.find(nums2[i][0]) != mp.end()){
+                mp[nums2[i][0]] += nums2[i][1];
+            } else{
+                mp[nums2[i][0]] = nums2[i][1];
+            }
+        }
+        vector<vector<int> > result;
+        for(auto it = mp.begin(); it != mp.end();it++){
+            result.push_back(vector<int> {it->first, it->second});
+        }
+        sort(result.begin(), result.end(), cmp);
+        return result;
+    }
+};
+

33. 搜索旋转排序数组

+

整数数组 nums 按升序排列,数组中的值 互不相同

+

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

+

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

+

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

+
class Solution {
+public:
+    int search(vector<int>& nums, int target) {
+        int n = nums.size();
+        int left = 0;
+        int right = n-1;
+        while(left <= right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] == target){
+                return mid;
+            }
+            if(nums[0] <= nums[mid]){
+                if(nums[0] <= target && target < nums[mid]){
+                    right = mid - 1;
+                } else{
+                    left = mid + 1;
+                }
+            } else{
+                if(nums[mid] < target && target <= nums[n-1]){
+                    left = mid + 1;
+                } else{
+                    right = mid - 1;
+                }
+            }
+        }
+        return -1;
+    }
+};
+

48. 旋转图像

+

给定一个 *n * × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

+

你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。**请不要 **使用另一个矩阵来旋转图像。

+
class Solution {
+public:
+    void rotate(vector<vector<int>>& matrix) {
+        int n = matrix.size();
+        for(int i=0;i< n / 2;i++){
+            for(int j=0;j<(n+1) / 2;j++){
+                int temp = matrix[i][j];
+                matrix[i][j] = matrix[n-j-1][i];
+                matrix[n-j-1][i] = matrix[n-i-1][n-j-1];
+                matrix[n-i-1][n-j-1] = matrix[j][n-i-1];
+                matrix[j][n-i-1] = temp;
+            }
+        }
+    }
+};
+

75. 颜色分类

+

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

+

我们使用整数 012 分别表示红色、白色和蓝色。

+

必须在不使用库内置的 sort 函数的情况下解决这个问题。

+
class Solution {
+public:
+    void sortColors(vector<int>& nums) {
+        int left = 0;
+        for(int i=0;i<nums.size();i++){
+            if(nums[i] == 0){
+                swap(nums[i], nums[left]);
+                left += 1;
+            }
+        }
+        for(int i=left;i<nums.size();i++){
+            if(nums[i] == 1){
+                swap(nums[i], nums[left]);
+                left += 1;
+            }
+        }
+    }
+};
+

73. 矩阵置零

+

给定一个 <em>m</em> x <em>n</em> 的矩阵,如果一个元素为 0 ** ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。**

+
class Solution {
+public:
+    void setZeroes(vector<vector<int>>& matrix) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        vector<bool> rowvector(m,false);
+        vector<bool> colvector(n,false);
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(matrix[i][j] == 0){
+                    rowvector[i] = true;
+                    colvector[j] = true;
+                }
+            }
+        }
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(rowvector[i] == true || colvector[j] == true){
+                    matrix[i][j] = 0;
+                }
+            }
+        }
+        return;
+    }
+};
+

118. 杨辉三角

+

给定一个非负整数 numRows 生成「杨辉三角」的前 *numRows *行。

+

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

+
class Solution {
+public:
+    vector<vector<int>> generate(int numRows) {
+        vector<vector<int> > result;
+        vector<int> temp;
+        temp.push_back(1);
+        result.push_back(temp);
+        if(numRows == 1){
+            return result;
+        }
+        for(int i=2;i<=numRows;i++){
+            vector<int> t;
+            t.push_back(1);
+            for(int j=1;j<i-1;j++){
+                t.push_back(result[i-2][j-1] + result[i-2][j]);
+            }
+            t.push_back(1);
+            result.push_back(t);
+        }
+        return result;
+    }
+};
+

153. 寻找旋转排序数组中的最小值

+

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:* 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]

+
    +
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
  • +
+

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

+

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

+

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

+
class Solution {
+public:
+    int findMin(vector<int>& nums) {
+        int n = nums.size();
+        int left = 0;
+        int right = n-1;
+        while(left < right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] < nums[right]){
+                right = mid;
+            } else {
+                left = mid + 1;
+            }
+        }
+        return nums[left];
+    }
+};
+

链表

+

2. 两数相加

+

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

+

请你将两个数相加,并以相同形式返回一个表示和的链表。

+

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

+
class Solution {
+public:
+    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
+        int cur = 0;
+        ListNode* head = new ListNode(-1);
+        ListNode* l3 = head;
+        while(l1 != NULL || l2 != NULL){
+            int target;
+            if(l1 == NULL){
+                target = l2->val + cur;
+                l2 = l2->next;
+            } else if(l2 == NULL){
+                target = l1->val + cur;
+                l1 = l1->next;
+            } else{
+                target = l1->val + l2->val + cur;
+                l1 = l1->next;
+                l2 = l2->next;
+            }
+            if (target >= 10){
+                cur = 1;
+                target -= 10;
+            } else{
+                cur = 0;
+            }
+            l3->next = new ListNode(target);
+            l3 = l3->next;
+        }
+        if(cur == 1){
+            l3->next = new ListNode(1);
+        }
+        return head->next;
+    }
+};
+

21. 合并两个有序链表

+

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

+
class Solution {
+public:
+    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
+        ListNode* dummy = new ListNode(0);
+        ListNode* head = dummy;
+        while(list1 != NULL && list2 != NULL){
+            if(list1->val < list2->val){
+                dummy->next = list1;
+                dummy = dummy->next;
+                list1 = list1->next;
+            } else{
+                dummy->next = list2;
+                dummy = dummy->next;
+                list2 = list2->next;
+            }
+        }
+        if(list1 != NULL){
+            dummy->next = list1;
+        }
+        if(list2 != NULL){
+            dummy->next = list2;
+        }
+        return head->next;
+    }
+};
+

哈希表

+

字符串

+

6. Z 字形变换

+

将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

+

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

+
P   A   H   N
+A P L S I I G
+Y   I   R
+

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"

+

请你实现这个将字符串进行指定行数变换的函数:

+
string convert(string s, int numRows);
+
class Solution {
+public:
+    string convert(string s, int numRows) {
+        if(numRows == 1){
+            return s;
+        }
+        vector<queue<char> > vt(numRows);
+        int nowindex = 0;
+        int reverse = false;
+        for(int i=0;i<s.size();i++){
+            vt[nowindex].push(s[i]);
+            if(nowindex == 0 && reverse == true){
+                reverse = false;
+                nowindex += 1;
+            } else if (nowindex == numRows-1 && reverse == false){
+                reverse = true;
+                nowindex -= 1;
+            } else {
+                if(reverse == false){
+                    nowindex += 1;
+                } else{
+                    nowindex -= 1;
+                }
+            }
+        }
+        string resultstring = "";
+        for(int i=0;i<vt.size();i++){
+            while(!vt[i].empty()){
+                resultstring += vt[i].front();
+                vt[i].pop();
+            }
+        }
+        return resultstring;
+    }
+};
+

8. 字符串转换整数 (atoi)

+

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

+

函数 myAtoi(string s) 的算法如下:

+
    +
  1. 读入字符串并丢弃无用的前导空格
  2. +
  3. 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
  4. +
  5. 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
  6. +
  7. 将前面步骤读入的这些数字转换为整数(即,“123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
  8. +
  9. 如果整数数超过 32 位有符号整数范围 [−2<sup>31</sup>,  2<sup>31 </sup>− 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2<sup>31</sup> 的整数应该被固定为 −2<sup>31</sup> ,大于 2<sup>31 </sup>− 1 的整数应该被固定为 2<sup>31 </sup>− 1
  10. +
  11. 返回整数作为最终结果。
  12. +
+

注意:

+
    +
  • 本题中的空白字符只包括空格字符 ' '
  • +
  • 除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。
  • +
+
class Solution {
+public:
+    int myAtoi(string s) {
+        int res = 0;
+        bool havenumsign = false;
+        bool negativesign = false;
+        bool fakebigsign = false;
+        for(int i=0;i<s.size();i++){
+            if(s[i] == ' ' && havenumsign == false){
+                continue;
+            }
+            if(s[i] == '-' && havenumsign == false){
+                negativesign = true;
+                havenumsign = true;
+                continue;
+            }
+            if(s[i] == '+' && havenumsign == false){
+                havenumsign = true;
+                continue;
+            }
+            if(s[i] >= '0' && s[i] <= '9'){
+                havenumsign = true;
+                if(res > (INT_MAX - s[i] + '0') / 10){
+                    res = INT_MAX;
+                    fakebigsign = true;
+                    break;
+                }
+                res = res * 10 - '0' + s[i];
+                continue;
+            }
+            if(havenumsign == true){
+                break;
+            }
+            break;
+        }
+        if (negativesign){
+            if(res == INT_MAX && fakebigsign){
+                return INT_MIN;
+            }
+            return -res;
+        }
+        return res;
+    }
+};
+

12. 整数转罗马数字

+

罗马数字包含以下七种字符: IVXLCDM

+
字符          数值
+I             1
+V             5
+X             10
+L             50
+C             100
+D             500
+M             1000
+

例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II

+

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

+
    +
  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
  • +
  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
  • +
  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
  • +
+

给你一个整数,将其转为罗马数字。

+
class Solution
+{
+public:
+    string intToRoman(int num)
+    {
+        pair<int, string> valueSymbols[] = {
+            {1000, "M"},
+            {900, "CM"},
+            {500, "D"},
+            {400, "CD"},
+            {100, "C"},
+            {90, "XC"},
+            {50, "L"},
+            {40, "XL"},
+            {10, "X"},
+            {9, "IX"},
+            {5, "V"},
+            {4, "IV"},
+            {1, "I"},
+        };
+        string roman;
+        for (auto &[value, symbol] : valueSymbols)
+        {
+            while (num >= value)
+            {
+                num -= value;
+                roman += symbol;
+            }
+            if (num == 0)
+            {
+                break;
+            }
+        }
+        return roman;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-基本数据结构
+
https://zhangzhao219.github.io/2024/04/02/Leetcode/Leetcode-ds/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年4月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/04/02/Leetcode/Leetcode-tp/index.html b/2024/04/02/Leetcode/Leetcode-tp/index.html new file mode 100644 index 000000000..090f4277e --- /dev/null +++ b/2024/04/02/Leetcode/Leetcode-tp/index.html @@ -0,0 +1,995 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-双指针法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-双指针法

+ + +
+ +

Leetcode-双指针法

+ +

双指针法

+

3. 无重复字符的最长子串

+

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

+
class Solution {
+public:
+    int lengthOfLongestSubstring(string s) {
+        int result = 0;
+        unordered_set<char> st;
+        int left = 0;
+        for(int right=0; right<s.size(); right++){
+            while(st.find(s[right]) != st.end()){
+                st.erase(s[left]);
+                left++;
+            }
+            st.insert(s[right]);
+            result = max(result, right - left + 1);
+        }
+        return result;
+    }
+};
+

4. 寻找两个正序数组的中位数

+

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

+

算法的时间复杂度应该为 O(log (m+n))

+
class Solution {
+public:
+    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
+        int m = nums1.size();
+        int n = nums2.size();
+        bool next;
+        int sign = (m + n) / 2; 
+        if((m + n) % 2 == 1){
+            next = false;
+        } else{
+            sign -= 1;
+            next = true;
+        }
+        int res1 = 0;
+        int res2 = 0;
+        int globalindex = 0;
+        int nums1index = 0;
+        int nums2index = 0;
+        while(globalindex <= sign){
+            if(nums1index < m && nums2index < n){
+                if(nums1[nums1index] < nums2[nums2index]){
+                    res1 = nums1[nums1index];
+                    nums1index += 1;
+                } else{
+                    res1 = nums2[nums2index];
+                    nums2index += 1;
+                }
+            } else if (nums1index < m){
+                res1 = nums1[nums1index];
+                nums1index += 1;
+            } else {
+                res1 = nums2[nums2index];
+                nums2index += 1;
+            }
+            globalindex += 1;
+        }
+        if(next == false){
+            return res1;
+        }
+        if(nums1index < m && nums2index < n){
+            if(nums1[nums1index] < nums2[nums2index]){
+                res2 = nums1[nums1index];
+            } else{
+                res2 = nums2[nums2index];
+            }
+        } else if (nums1index < m){
+            res2 = nums1[nums1index];
+        } else {
+            res2 = nums2[nums2index];
+        }
+        return ((double)res1 + (double)res2) / 2.0;
+    }
+};
+

5. 最长回文子串

+

给你一个字符串 s,找到 s 中最长的回文子串。

+

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

+
class Solution {
+public:
+    string longestPalindrome(string s) {
+        vector<int> result = {0,0};
+        for(int i=0;i<s.size()-1;i++){
+            int left = i;
+            int right = i;
+            while(left >= 0 && right < s.size()){
+                if(s[left] == s[right]){
+                    if(result[1] - result[0] < right - left){
+                        result[1] = right;
+                        result[0] = left;
+                    }
+                    right += 1;
+                    left -= 1;
+                } else{
+                    break;
+                }
+            }
+            left = i;
+            right = i+1;
+            while(left >= 0 && right < s.size()){
+                if(s[left] == s[right]){
+                    if(result[1] - result[0] < right - left){
+                        result[1] = right;
+                        result[0] = left;
+                    }
+                    right += 1;
+                    left -= 1;
+                } else{
+                    break;
+                }
+            }
+        }
+        return s.substr(result[0], result[1]-result[0] + 1);
+    }
+};
+

9. 回文数

+

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false

+

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

+
    +
  • 例如,121 是回文,而 123 不是。
  • +
+
class Solution {
+public:
+    bool isPalindrome(int x) {
+        string s = to_string(x);
+        int left = 0;
+        int right = s.size() - 1;
+        while(left < right){
+            if(s[left] != s[right]){
+                return false;
+            }
+            left += 1;
+            right -= 1;
+        }
+        return true;
+    }
+};
+

11. 盛最多水的容器

+

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

+

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

+

返回容器可以储存的最大水量。

+

说明: 你不能倾斜容器。

+
class Solution {
+public:
+    int maxArea(vector<int>& height) {
+        int maxarea = 0;
+        int left = 0;
+        int right = height.size() - 1;
+        while(left < right) {
+            if(height[left] < height[right]){
+                maxarea = max(maxarea, (right - left) * height[left]);
+                left += 1;
+            } else{
+                maxarea = max(maxarea, (right - left) * height[right]);
+                right -= 1;
+            }
+        }
+        return maxarea;
+    }
+};
+

88. 合并两个有序数组

+

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

+

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

+

注意: 最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n

+
class Solution {
+public:
+    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
+        int index = m + n - 1;
+        m -= 1;
+        n -= 1;
+        while(index >= 0){
+            if(m < 0){
+                nums1[index] = nums2[n];
+                n--;
+            } else if (n < 0){
+                nums1[index] = nums1[m];
+                m--;
+            } else{
+                if(nums1[m] > nums2[n]){
+                    nums1[index] = nums1[m];
+                    m--;
+                } else{
+                    nums1[index] = nums2[n];
+                    n--;
+                }
+            }
+            index--;
+        }
+        return;
+    }
+};
+

23. 合并 K 个升序链表

+

给你一个链表数组,每个链表都已经按升序排列。

+

请你将所有链表合并到一个升序链表中,返回合并后的链表。

+
class Solution {
+public:
+    ListNode* merge(vector<ListNode*>& lists, int start, int end){
+        if(start > end){
+            return NULL;
+        }
+        if(start == end){
+            return lists[start];
+        }
+        int mid = (end - start) / 2 + start;
+        ListNode* first = merge(lists, start, mid);
+        ListNode* second = merge(lists, mid + 1, end);
+        if(first == NULL){
+            return second;
+        }
+        if(second == NULL){
+            return first;
+        }
+        ListNode* head = new ListNode(0);
+        ListNode* p = head;
+        while(first != NULL && second != NULL){
+            if(first->val < second->val){
+                p->next = first;
+                first = first->next;
+            } else{
+                p->next = second;
+                second = second->next;
+            }
+            p = p->next;
+        }
+        if(first != NULL){
+            p->next = first;
+        } else{
+            p->next = second;
+        }
+        return head->next;
+    }
+    ListNode* mergeKLists(vector<ListNode*>& lists) {
+        return merge(lists, 0, lists.size()-1);
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-双指针法
+
https://zhangzhao219.github.io/2024/04/02/Leetcode/Leetcode-tp/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年4月2日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/04/14/Leetcode/Leetcode-bkt/index.html b/2024/04/14/Leetcode/Leetcode-bkt/index.html new file mode 100644 index 000000000..34f0b1adc --- /dev/null +++ b/2024/04/14/Leetcode/Leetcode-bkt/index.html @@ -0,0 +1,865 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-回溯算法 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-回溯算法

+ + +
+ +

Leetcode-回溯算法

+ +

回溯

+

22. 括号生成

+

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合。

+
class Solution {
+public:
+    vector<string> result;
+    void backtracking(vector<string> temp, int n, int nowleft, int nowright){
+        if(temp.size() == n * 2){
+            string res = "";
+            for(int i=0;i<temp.size();i++){
+                res += temp[i];
+            }
+            result.push_back(res);
+            return;
+        }
+        if (nowleft < n){
+            temp.push_back("(");
+            backtracking(temp,n,nowleft+1,nowright);
+            temp.pop_back();
+        }
+        if(nowright < n && nowright < nowleft){
+            temp.push_back(")");
+            backtracking(temp,n,nowleft,nowright+1);
+            temp.pop_back();
+        }
+    }
+    vector<string> generateParenthesis(int n) {
+        vector<string> temp;
+        backtracking(temp,n,0,0);
+        return result;
+    }
+};
+

31. 下一个排列

+

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

+
    +
  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]
  • +
+

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

+
    +
  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • +
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • +
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
  • +
+

给你一个整数数组 nums ,找出 nums 的下一个排列。

+

必须原地修改,只允许使用额外常数空间。

+
class Solution {
+public:
+    void nextPermutation(vector<int>& nums) {
+        int n = nums.size();
+        int index = n-1;
+        for(;index>=1;index--){
+            if(nums[index] > nums[index-1]){
+                break;
+            }
+        }
+        if(index != 0){
+            index -= 1;
+            for(int i=n-1;i>=0;i--){
+                if(nums[i] > nums[index]){
+                    swap(nums[index], nums[i]);
+                    reverse(nums.begin()+index+1,nums.end());
+                    return;
+                }
+            }
+        }
+        else{
+            reverse(nums.begin(), nums.end());
+        }
+    }
+};
+

79. 单词搜索

+

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

+

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

+
class Solution {
+public:
+    bool result = false;
+    void DFS(int i, int j, int m, int n,int now, vector<vector<char>>& board,vector<vector<bool> > &visited,  string word){
+        if(i < 0 || j < 0 || i >= m || j >= n || visited[i][j] == true || board[i][j] != word[now]){
+            return;
+        }
+        if(now == word.size() - 1){
+            result = true;
+            return;
+        }
+        visited[i][j] = true;
+        DFS(i+1,j,m,n,now+1,board,visited, word);
+        DFS(i-1,j,m,n,now+1,board,visited, word);
+        DFS(i,j+1,m,n,now+1,board,visited, word);
+        DFS(i,j-1,m,n,now+1,board,visited, word);
+        visited[i][j] = false;
+    }
+    bool exist(vector<vector<char>>& board, string word) {
+        int m = board.size();
+        int n = board[0].size();
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                vector<vector<bool> > visited(m, vector<bool>(n, false));
+                DFS(i,j,m,n,0,board,visited, word);
+                if(result){
+                    return result;
+                }
+            }
+        }
+        return result;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-回溯算法
+
https://zhangzhao219.github.io/2024/04/14/Leetcode/Leetcode-bkt/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年4月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/04/14/Leetcode/Leetcode-bt/index.html b/2024/04/14/Leetcode/Leetcode-bt/index.html new file mode 100644 index 000000000..77ee70870 --- /dev/null +++ b/2024/04/14/Leetcode/Leetcode-bt/index.html @@ -0,0 +1,807 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-二叉树 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-二叉树

+ + +
+ +

Leetcode-二叉树

+ +

二叉树

+

94. 二叉树的中序遍历

+

给定一个二叉树的根节点 root ,返回 它的 中序 遍历

+
class Solution {
+public:
+    vector<int> result;
+    void inorder(TreeNode* root){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left);
+        result.push_back(root->val);
+        inorder(root->right);
+    }
+    vector<int> inorderTraversal(TreeNode* root) {
+        inorder(root);
+        return result;
+    }
+};
+

103. 二叉树的锯齿形层序遍历

+

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

+
class Solution {
+public:
+    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
+        vector<vector<int> > result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        int sign = 0;
+        while(!q.empty()){
+            int t = q.size();
+            vector<int> temp;
+            for(int i=0;i<t;i++){
+                TreeNode* x = q.front();
+                q.pop();
+                temp.push_back(x->val);
+                if(x->left != NULL){
+                    q.push(x->left);
+                }
+                if(x->right != NULL){
+                    q.push(x->right);
+                }
+            }
+            if(sign == 1){
+                reverse(temp.begin(), temp.end());
+            }
+            result.push_back(temp);
+            sign = 1 - sign;
+        }
+        return result;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-二叉树
+
https://zhangzhao219.github.io/2024/04/14/Leetcode/Leetcode-bt/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年4月14日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/04/20/Interview/Interview-Questions-HR/index.html b/2024/04/20/Interview/Interview-Questions-HR/index.html new file mode 100644 index 000000000..05d1003eb --- /dev/null +++ b/2024/04/20/Interview/Interview-Questions-HR/index.html @@ -0,0 +1,867 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HR面试准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

HR面试准备

+ + +
+ +

HR面试准备

+

自我介绍

+

您好,我是张兆,就读于中国科学院计算技术研究所,是预计25年夏季毕业的硕士研究生。我本科来自于中南大学计算机学院,是通过推荐免试的方式进入到中科院计算所就读研究生的。我在本科期间获得了国家奖学金和省级优秀毕业生的荣誉称号,同时拿过一些程序设计竞赛、数学建模和英语竞赛的奖项。在研究生期间连续获得了两年的中国科学院大学三好学生。

+

在科研经历方面,我一共发表过5篇论文,涉及信息检索、大模型、可视化、数学建模等多个领域。在实习经历方面,我先后尝试过四份实习工作,分别是在商汤科技、MetaApp、微软STCA和蚂蚁集团,主要进行算法相关的研究工程等相关工作,涉及到传统算法、后端开发、自然语言处理、推荐等多种技术领域。除此之外我在其他业余时间参加了一些NLP、大模型或搜索等相关的算法竞赛,参加的所有比赛都拿到了比较好的名次。

+

我认为我拥有较强的学习能力,善于学习新事物、适应新环境。对待工作比较踏实认真,团队合作能力强。我的专业知识和实习工作经验,和这个岗位要求也比较相符,希望能够得到一个施展才华的空间。

+

我的自我介绍就到这里,谢谢。

+

知识经验

+

实习期间的收获

+

怎样开展工作

+

首先听取领导的指示和要求,然后就有关情况进行了解和熟悉,接下来制定一份近期的工作计划并报领导批准,最后根据计划开展工作。

+

通过之前的了解,我知道了你们要招聘的这个岗位,主要负责xx方面的内容,需要xx方面的能力(贴合岗位JD),我是xx专业的,有理论知识背景,之前在xx公司作为实习生在xx岗位主要负责xx方面的内容,取得了xx成果,解决了xx问题,跟贵公司要招聘的岗位相似度比较高,然后本人比较擅长xx(个人能力内容),我有信心可以胜任这个岗位。最后,我还拥有xx特色或某方面的经验(个人比较有差异性的优势),应该会比一般的应聘者更适合这个岗位。

+

对这项工作有哪些可预见的困难

+

问题八:对这项工作,你有哪些可预见的困难?
+【思路】:①不宜直接说出具体的困难,否则可能令对方怀疑应聘者不行。②可以尝试迂回战术,说出应聘者对困难所持有的态度。
+【参考答案】:工作中出现一些困难是正常的,也是难免的,但是只要有坚韧不拔的毅力、良好的合作精神以及事前周密而充分的准备,任何困难都是可以克服的。

+

遇到最大的困难是什么,如何解决

+

我之前在公司时,领导给我安排了一个从未接手过的任务,写一个品牌宣传文案,我用了一晚上的时间拆解其他人的宣传文案,然后按领导要求的写了出来,当投放出去的时候,获得了XX的效果,通过这次面对困难的经历,我除了收获成绩以外,还收获了如何写品牌宣传文案的底层逻辑。

+

面试时候有那些表现好的地方,哪些表现不好的地方

+

你觉得自己在面试中有什么表现好的地方,有什么表现差的地方(这个问题要注意回答,这个问题大概率表明你的评级有弹性空间,后续可能会用来argue薪资)

+

更看重公司的哪个方面

+

你更看重工作那个方面(薪资,发展空间,工作氛围灯)
+(这个问题中,主要要说清楚base地等硬性需求,防止被改base,其他自由发挥)

+

发现自己不适合怎么办

+

要表明自己是非常喜欢这份工作,才来公司面试的,如果在入职后是公司这边发现这份工作不适合自己,那希望公司要提出来,自己会去自查后提升能力,或端正态度来好这份工作,如果是自己发现不适合,自己也会自查,找到自己觉得不合适的原因,进行自我完善,我相信我的能力这边匹配JD岗位的话我是不会觉得不合适的。

+

任职意愿

+

选择我们公司的原因

+

你为什么选择我们公司?
+【思路】:①面试官试图从中了解你求职的动机、愿望以及对此项工作的态度。②建议从行业、企业和岗位这三个角度来回答
+【参考答案】:我十分看好贵公司所在的行业,我认为贵公司十分重视人才,而且这项工作很适合我,相信自己一定能做好。

+

面试官通过这个问题主要想考察你是否对公司和岗位感兴趣,有了解,把你招进来是否具备稳定性,主要突出公司知名度+工作内容+匹配度。

+

首先,贵公司在行业中有着xxX的成绩,同时我了解贵公司在XX方向近期有拓展的规划,这部分也是我非常感兴趣的地方。
+其次,该岗位和我的职业规划比较匹配,我理解后续我的工作内容是做XX方向的工作,这部分内容是我后续想尝试的一个方向。
+最后,我认为我目前的能力和该岗位的招聘标准比较契合,之前我做过XXX的工作,相关的工作经验能够在贵司复用,同时也能帮助自己在这一方向完成进一步的能力深造。
+所以我想加入贵公司。

+

·想测试你来公司面试前有没有做过调查研究,是不是海投;可以从岗位性质或者公司的角度回答。
+【回答公式】
+·万с真能鮒譞蕹餓胪坰卦疫由:发展平台、管理、产品实力、业界口碑
+【机智回答】
+据我了解,贵公司很注重企业文化,特别注重员工个人能力的培养。我想,在.一个学习氛围良好的企业工作,自身将会受到很好的影响,也能做到为公司提供最大化的价值服务。另外,公司岗位性质也是我人比较喜欢的,人们都说兴趣是最好的老师,我想这更有利于自身能力的发挥。

+

录用你的原因

+

问题六:我我们为什么要录用你?
+【思路】:招聘单位一般会录用这样的招聘者:基本符合条件、对这份工作感兴趣、有足够信心。
+【参考答案】:我符合贵公司的招聘条件,凭我目前掌握的技能、高度的责任感和良好的适应能力以及学习能力,完全能胜任这份工作。我十分希望能为贵公司服务,如果贵公司给我这个机会,我一定能成为贵公司的栋娆贑壞婚梁!

+

问题十三:你是应届毕业生,缺乏经验,如何胜任这项工作?
+思路】:①如果招聘单位对应届生提出这个问题,说明招聘单位并不在乎“经验”,关键看应聘者怎样回答,②对这个问题的回答最好体现出应聘者的诚恳、机智果敢及敬业。
+【参考答案】:作为应届毕业生,在工作经验方面的确会有所欠缺,因此读书期间我一直利用各种机会在这个行业做兼职。发现实际工作远比书本知识丰富、复杂。但我有较强的责任心、适应能力和学习能力,请贵公司放心,!学校所学及兼职的工作经验使我一定能胜任这个职位。

+

在来应聘这个岗位之前,我通过XX的方式对行业和岗位进行了一定的了解,同时针对这个岗位,我发现之前的工作/实习经验也有一些可迁移的地方,且我自己是一个具备很强学习能力和思考能力的人,相信我能够在日后快速上手并融入工作,为公司创造价值。

+

我认为我的核心竞争力是有0-1搭建账号的丰富经验。我擅长以结果为导向的数据分析、内容选题,个人账号在1个月发布x篇笔记,其中爆款x篇,共获赞万,涨粉1K+个,通过不断参考竞品、优化封面和内容、完善用户画像,提升转化率xx%。根据岗位JD介绍,我深信我能很好的将这些能力迁移到工作中。

+

如何处理工作中的压力

+

你会如何处理工作中的压力?(压力测试题)6、
+【回答思路】
+这个问题主要是想考察你在面对工作中压力的抗压能力,时决断能力、应变能力怎么样,所以阐述实例时一定要注意要有正向的证明。【机智回答】
+我相信压力是工作中的一部分,并且难以完全避免。关键在于找到压力的来源,并学会如何将其转化为动力。
+个人来说,我处理压力的方式是对压力来源进行详细的拆解和分析。通过对工作进行总结和复盘,我可以更清楚地了解自己的优势和劣势,并利用碎片化的时间来提升自己的短板,从而将压力转变为动力。当我面临一时无法解决源头问题的情况时,我不会过度纠结或困扰自己。相反,我会转移注意力,放松心情,并思结或困扰自己。相反,我会转移注意力,放松心情,并寻求他人的倾诉以缓解压力。与他人交流可以帮助我获得新的思路和解决问题的方向,同时也能得到情感上的支持。总的来说,我认为将压力转化为动力的关键在于积极应对并努力解决问题,在面对无解或困境时保持冷静,并适当转移注意力和寻求他人的帮助。这样的处理方式有助于提升个人的应变能力和团队协作的效果。非常感谢您给我这次机会分享我的经验。

+

离职原因

+

在上一段实习中,我对于自己负责的这个部分已经非常熟悉,同时对于整个组接触到的整个的算法工程的流程也有了一定的认知和了解,Leader对我的能力也有一定的认可,如果继续实习下去可能对于我自身来说就不会有特别大的进步或者学习更大量的东西了,因此在完成手头工作后就离开了上个实习单位。

+

职业规划

+

问题十七:你最近3~5年的职业规划是什
+么?
+【思路】:①考察应聘者职业规划和他们公司的发展是否匹配。②考察应聘者对未来是否有清晰的规划。【参考答案】:第一阶段:,熟悉业务,提高自己的专业和技能水平,让自己胜任这个岗位。第二阶段:稳定提升,除了做好本职工作,持续学习行业知识,同时对公司各部门有全面的了解。第三阶段:不断学习、积极探索。横向拓宽自己的业务能力,寻求更多的发展机会,为公司带来更大的价值。

+

(回答公式】
+近期规划(1-3年),适应位+提升岗位技能+积累工作经验远期规划(三到五年),在岗位上独当一面+在行业内立足【机智回答一】
+未来1年,我会尽快适应新环境的工作节奏,熟悉相关业务和流程,并不断提高自己专业上和技能上的水平。
+未来3年,我希望跟随公司的发展,在自己的业务能力和管理水平上再上一个台阶,比如带领一个小团队共同做好项目,用自己的影响力帮助他人。
+未来5年,我会根据环境的变化,工作内容的变化,以及我自身能力的变化,不断进调整。
+【机智回答二)
+未来1年,我会尽快适应新环境的工作节奏,熟悉相关业务和流程,并不断提高自己专业上和技能上的水平。
+未来3年,我希望跟随公司的发展,在自己的业务能力和管理水平上再上一个台阶,比如带领一个小团队共同做好项目,用自己的影响力帮助他人。
+未来5年,我会根据环境的变化,工作内容的变化,以及我自身能力的变化,不断进调整

+

应届生:
+。我是xx届刚毕业的应届生,目前需要解决的问题就是工作经验不足,面对这个问题,我首先要做的就是尽快了解行业和岗位的发展趋势,尽快熟悉和上手自己的工作,努力提升自己的xxx能力和xxx能力(你求职的岗位所需的能力),在工作中多总结复盘,对于不懂的地方和不熟悉的地方及时反馈寻求帮助,拓宽自己的能力范围和视野。在熟悉自己的工作之后,我不会局限于自己眼前的小事,会针对性提升工作中需要的xx能力,形成自己的方法论以更高效地完成自己的工作,为公司创造更多的价值。在过后的2-3年中,我希望能够在xx岗位上独当一面(你求职的岗位),独立负责xxx项日。
+。职场人:
+首先我要将我的职业规划分为短期和长期两个方面:
+短期规划:刚进入公司首先要做的是熟悉公司的制度和xx岗位(你求职的岗位)的工作流程,尽快融入到公司的业务之中,因为我之前有过xx的工作经验,学到了xx技能和xx技能(你的岗位所需技能),在接下来的工作中我也会精进这些技能,不断提升自己的业务能力。
+长期规划:在接下来的1-2年内,我会不断深耕行业的专业领域,(提升自己的专业能力和岗位所需的能力)未来三年,行业也会不断变化,我会及时关注行业的发展趋势和市场的发展变化,我会根据行业的大环境不断积累岗位知识提升自身的专业水平,能够带领团队完整完成一个项目,为公司承担更多责任。

+

如何看待加班

+

问题十四:你怎么看待加班?
+【思路】:实际上很多公司问这个问题,并不证明一定要加班,只是想测试你是否愿意为公司奉献。
+【参考答案】:如果是工作需要我会义不容辞加班,我现在单身,没有任何家庭负担,可以全身心投入工作。但同时我也会提高工作效率,减少不必要的加班。

+

高情商回答话术:
+。注意:回答要给自己留有余地,不要完全拒绝也不要太快接受(回答太积极可能会给自己留下大坑哦)
+(明确自己的态度,拒绝无效加班)首先,有些项目会出现紧急情况需要加班,这点我是可以接受的,有些情况确实是不可抗因素,这种时候需要的话我会积极配合。
+(导致加班的原因+解决方室),其次,如果出现了需要加班的情况,我会分析加班的原因,如果是因为刚入职还没熟悉导致加班,我会及时请教前辈和领导,在空闲时间弥补自己的不足,提高工作效率,做到按时按量完成工作。但是我不会特意为了“表现自己”而加班,我认为高效率完成工作是对自己和公司的负责,高效率的产出才不会浪费时间。
+(打探公司的真实情况,如果“卷加班”是企业文化就赶紧跑路吧!!)既然说到了加班的问题,我想了解一下我这个岗位的架构组成和工作分配,加班频率高吗?一般是什么情况需要加班呢?

+

为什么尝试过开发又回到算法了

+

问题十五:你为什么转行?
+【思路】:①从行业-转行动机(离开老行业和选择新行业的原因)。②从岗位-新旧岗位(可以从个人兴趣&能力&新旧岗位匹配度等方向说明选择新岗位的原因)。③从规划-未来1-3年的职业规划【参考答案】:行业:政策影响老行业-新行业的优势平时对这个行业关注比较多;岗位:对应聘岗位的认识,以及选择这个岗位的原因。;规划:从短期和长期来说,最后一般都是称为某领域专家or走管理路线。

+

在过去的工作中,我是做XX行业的XX岗位,但是在工作过程中我发现,这个行业的增长到达了瓶颈,同时我在岗的方向也做到了一定的稳定程度,缺少一些可以下钻的方向,后来接触到了XX行业,发现这个行业具备一定的增长空间,同时在学习了解的过程中,我也发现了我目前岗位和该岗位在工作技能上的可迁移性,认为自己的一些历史经验能够在这个行业做到差异化的应用,同时还能收获到新的思考,所以我选择转澛浰羰両寑线殄。

+

性格与价值观

+

性格怎么样,如何评价自己

+

我认为自己有很强的责任感和高要求的工作态度,这也得到了前任领导的认可。在工作中,我总是尽职尽责,不轻易放弃,努力追求卓越的工作表现。这一点也得到了团队成员的认可和肯定。同时,我在为人处世方面也被朋友和同事认为很随和。整体而言,我的人际关系良好,能够和不同的人群建立良好的合作关系。

+

优点和缺点

+

问题三:谈谈你的优点
+【思路】:①这是面试常见问题,看似简单反而很容易陷入自嗨,最后反而导致HR不满意,这里需要注意的是所回答的点一定要和你应聘的岗位相匹配,一定一定要相匹配!不然这个不匹配的优点在HR看来就不是优点。②回答一定要符合STAR法则。③一定要有例子!!!不然怎么证明这的确是你的优点。
+【参考答案】: 我认为我最大的优点是执行能力强,具体来说就是面对一个任务,只要我确定了目标和实现路径,会立马执行,然后在过程中进行优化迭代。比如在XX时候,领导下发了一个紧急重要并且我完全没有做过的项目,我在做了任务的分析和背景调查后,当天就做出来完成这个目标的To-do list,第二天就马上开始逐一执行,及时根据进度、反馈调整,最后成功在任务前一天提前完成了任务,所以执行力强是我认为自己的优点。同时我觉得对于我应聘的岗位,这个优点可以让我在工作中及时验证用户反馈,帮助团队目标的实现。

+

回答思路:
+这里主要考察面试者对工作技能的理解程度,说2点即可,最好是展示这些优点是怎么体现出来的。
+参考模板:
+我认为我的优点是执行能力强和多任务并行能力强关于执行能力,对于工作中有明确目标和路径的事情,我都能很快推进项目的落地,比如XX。
+关于多任务并行能力,我在工作中经常承接多个项目,不同的项目我都能按照优先级合理推进项目的落地,比如XX。

+

问题四:谈谈你的缺点
+【思路】:①不宜说自己没缺点。②不宜把那些明显的优点说成缺点。③不宜说出严重影响所应聘工作的缺点。④不宜说出令人不放心、不舒服的缺点。⑤可以说出一些对于说应聘工作“无关紧要”的缺点,甚至是一些表面看起来是去缺点,从工作的角度看却是优点的缺点。
+【参考答案】:我的缺点在于不是这个专业or行业的,同时工作经验比较少,所具备的理论知识和实践能力有限。但我已经开始在网上学习这方面的知识,包括网上看视频学习,看别人的知识点总结以及看有关xx的书籍。我相信我很快能够具备这方面的知识,并且能很快上手工作,为公司创造更大的效益。

+

.你觉得自己最大的缺点是什么?
+回答思路:
+这里不要说与面试岗位冲突的缺点,要尽量挑选与岗位弱相关但是可以改进和优化的缺点进行回复,参考模板:
+我认为自己最大的缺点是执行过程中有些完美主义。在推进项目的过程中,我会关注很多细节,在细节方向上不断进行修改,但实际有些事情是已经可以先推进落地来满足业务需求了。所以之后的工作中,我希望自己能够抓住项目核心快速迭代,保证MVP版本优先上线,提升推进的效率,

+

优点.
+做事有条理:我做事很有计划性,每天都会按要做的事优先级来列个清单,我会把每件事整理得非常有条理。并且我也有很明确的职业规划,大学期间我学习了xxx技能,在学校参加过xxx活动,学到了xxx能力(阐述和岗位相关的技能/能力)

+

事整理得非常有条理。并且我也有很明确的职业规划,大学期间我学习了xxx技能,在学校参加过xxx活动,学到了xxx能力(阐述和岗位相关的技能/能力)
+时间观念很强:我是一个凡事都要做好计划的人,并且也会根据时间更合理规划。在面试之前我会做好面试准备和路线规划,同时在平时工作中,我也会时刻做好规划,防止不必要的情况发生。
+责任心强:我是一个责任感很强的人。一旦确定了目标,我会用最大的努力去完成,我认为做事情就一定要有始有终,在今后对待工作我也是一样的态度,半途而废这个词不存在在我的字典里。
+沟通能力强:我是个善于沟通的人,在工作的各种场合都会很好的听他人的想法,同时也乐于表达自己的观点,在学校参加过辩论比赛和演讲比赛,在工作中敢于表达自己也是对工作负责的表现。
+缺点
+性格急躁:如果在工作上没能及时的达到转化会焦虑,有一点结果导向。但是这种情绪往往会推进我优化工作流程,直到预期的结果出现。我也在不断学习调整,能够将这种心态更多的转化为动力。
+不懂得拒绝:(比如别人拜托你做不属于你的活)但现在我会努力改进工作处理的优先级顺序,在时间宽松的情况下再去帮忙,这种情况已经很少了。
+不够自信:我性格比较内向,缺乏自信,在一些大的场合会害怕不敢发言,为了解决这个情况,我看了很多的演讲视频,平时会经常锻炼自己的表达能力,也会找身边的朋友同事练习,在不断的尝试之后现在也比较自信了。

+

成功经历和失败经历

+

问题十八:谈谈你的一次成功经历【思路】:①举一个你最后把握的例子,把来龙去脉说清楚,而不要说了很多却没有重点。②切记夸张,把别人的功劳说成自己的,很多主管为确保要用的人是最合适的,会打电话向你前任主管征询对你的看法及意见。【参考答案】:通过努力,每完成一个工作目标,都是一种成就感。例如,在短短的5天内,完成资质增项并受到了老板的夸奖…

+

问题五:谈谈你的一次失败经历
+【思路】:①不宜说自己没有失败的经历②不宜把那些明显的成功说成是失败。③所谈经历的结果应是失败的。④宜说明失败之前自己曾信心百倍、尽心尽力。⑤说明仅仅是外在客观原因导致失败。⑥失败后自己很快振作起来,以更加饱满的热情面对以后的工作。
+【参考答案】:曾经接触过一个客户,原本就有耳闻他们以挑剔出名,所以事前准备功夫做得十分充足,也投入了相当多的时间与精力,最后客户虽然并没有照单全收,但是接受的程度已经出乎我们意料之外了。原以为从此可以合作愉快,却得知客户最后因为预算关系选择了另外一家代理商,之前的努力因而付诸流水。尽管如此,我还是从这次的经验学到很多,如对该产业的了解,整个team的默契也更好了

+

业余爱好

+

我平时喜欢一些球类运动,经常约朋友一起打乒乓球和羽毛球等,如果是自己休闲的时候喜欢听一些音乐。

+

几个词语描述一下自己

+

我认为我是一个积极进取、坚持不懈、具有团队合作精神的人。在上学期间,我始终保持着对学习的热情和好奇心,不断提升自己的专业技能和综合素质。在项目、参加竞赛或者是实习工作中,我面临过很多挑战和困难,不过从来没有放弃过,也因此取得了很多的成绩。同时,我也注重与他人的交流和合作,相信通过共同努力可以实现更好的成果。

+

为人处事

+

家庭情况

+

我的家庭比较温馨和睦,我的父亲是初中数学教师,我的母亲是三甲医院的护士,工作都比较稳定,也都准备步入退休生活了。我的父母从小就很注重对我的教育,我的父母同时也是很开明的家长,我们家的沟通是平等友好的。在选择的就业方向与岗位这块,我父母也支持我的自由选择。在未来的职业生涯中,我也将努力工作,回报我的父母,为家庭做出更多的贡献。

+

单独带团队怎么做

+

我一般会设立目标,首先是把整个项目的目标拆解成可执行的小目标,然后设置好小目标的完成时间,设置完成后,我会召集团队成员,让大家聊聊对这个项目的想法,之后,整合大家的想法,现场一起把项目再拆一遍,然后让大家领取自己擅长的部分去做,和大家商量一个大概的截止时间。在做的过程中,我会和每一个执行的同事及时沟通,了解情况,完成一个小阶段,我也会组织会议,一起讨论下一步怎么更好的执行,以此流程进行,直到项目完成为止。

+

与怎样的上级共事

+

作为一个刚步入社会的新人,熟悉环境、适应环境应该是我的首要任务,我就不对工作环境提出更多的要求了,我只希望能够发挥自己的能力、专长,快速熟悉并独立完成工作。希望上级在我的工作中能多指导、多帮助,这样我也能立即纠正自己的错误,更快地成长和进步,为公司的发展贡献更多的力量。

+

与上级意见不一致怎么办

+

对于非原则性问题,就类似于项目研究等方向技术路径之类的,我会服从,并在执行过程中进行完善。然后,在执行上级的要求的同时,我也会找适当的机会给领导提出我的建议,交换一下看法。如果是涉及原则性问题或者涉及公司利益的重大问题,我希望能向更高层领导反映并交流看法。

+

佩服的人

+ + +
+ +
+
+ + + + + + +
+
+
HR面试准备
+
https://zhangzhao219.github.io/2024/04/20/Interview/Interview-Questions-HR/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年4月20日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/04/30/Leetcode/Leetcode-dp/index.html b/2024/04/30/Leetcode/Leetcode-dp/index.html new file mode 100644 index 000000000..cb446fe93 --- /dev/null +++ b/2024/04/30/Leetcode/Leetcode-dp/index.html @@ -0,0 +1,772 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-动态规划 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-动态规划

+ + +
+ +

Leetcode-动态规划

+ +

动态规划

+

64. 最小路径和

+

给定一个包含非负整数的 <em>m</em> x <em>n</em> 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

+

说明: 每次只能向下或者向右移动一步。

+
class Solution {
+public:
+    int minPathSum(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        vector<vector<int> > dp(m, vector<int>(n,0));
+        dp[0][0] = grid[0][0];
+        for(int i=1;i<m;i++){
+            dp[i][0] = dp[i-1][0] + grid[i][0];
+        }
+        for(int j=1;j<n;j++){
+            dp[0][j] = dp[0][j-1] + grid[0][j];
+        }
+        for(int i=1;i<m;i++){
+            for(int j=1;j<n;j++){
+                dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
+            }
+        }
+        return dp[m-1][n-1];
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-动态规划
+
https://zhangzhao219.github.io/2024/04/30/Leetcode/Leetcode-dp/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年4月30日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/05/22/Interview/Interview-Questions-SR/index.html b/2024/05/22/Interview/Interview-Questions-SR/index.html new file mode 100644 index 000000000..84adc88b6 --- /dev/null +++ b/2024/05/22/Interview/Interview-Questions-SR/index.html @@ -0,0 +1,867 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搜推面试题目准备 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

搜推面试题目准备

+ + +
+ +

搜推面试题目准备

+ +

推荐系统经典文章

+

FM:作为逻辑回归模型的改进版,拟解决在稀疏数据的场景下模型参数难以训练的问题。并且考虑了特征的二阶交叉,弥补了逻辑回归表达能力差的缺陷。

+
    +
  • FM在线性模型的基础上添加了一个多项式,用于描述特征之间的二阶交叉。n表示一个样本的特征的个数(类别特征 onehot 之后的维度),两两交互可得到 n(n−1)/2 个交叉项;多项式要学习的参数即为 n(n−1)/2 个 w系数。
  • +
  • 对每个特征分量引入k维辅助向量,每个特征对应一个总共n个向量,然后利用向量内积的结果来表示原来的组合参数w,要学习的参数变成了元素个数为n*k的V矩阵
  • +
  • 引入辅助向量削弱了参数间的独立性,化简之后,FM的复杂度从n^2k降到线性的nk,更利于上线使用
  • +
+

优点:

+
    +
  1. 将二阶交叉特征考虑进来,提高模型的表达能力;
  2. +
  3. 引入隐向量,缓解了数据稀疏带来的参数难训练问题;
  4. +
  5. 模型复杂度保持为线性,并且改进为高阶特征组合时,仍为线性复杂度,有利于上线应用。
  6. +
+

缺点:

+
    +
  1. 虽然考虑了特征的交叉,但是表达能力仍然有限,不及深度模型;
  2. +
  3. 同一特征与不同特征组合使用的都是同一隐向量,违反了特征与不同特征组合可发挥不同重要性的事实。
  4. +
+

FFM 是 FM 的改进版,作者引入 field 的概念,把相同性质的特征 (经过 onehot 编码的类别特征) 归于同一个 field,同一特征与属于不同域的特征作交互时,具有不同的隐向量表示。

+

FFM 将隐向量进一步细分,每个特征具有多个隐向量 (等于 field 的数目)。模型参数量为 1+n+n(F−1)k , F为 field 数。公式不可化简,复杂度为n^2k ,随着特征数n 平方级增长。

+

优点:

+
    +
  1. 引入 field 域的概念,让某一特征与不同特征做交互时,可发挥不同的重要性,提升模型表达能力;
  2. +
  3. 可解释性强,可提供某些特征组合的重要性。
  4. +
+

缺点:

+
    +
  1. 复杂度高,不适用于特征数较多的场景。
  2. +
+

Wide&Deep:一个线性模型与深度模型结合的产物

+

img

+

左边(Wide Models)为拆解出来线性模型,右边(Deep Models)为深度模型。未激活的线性模型输出与深度模型输出相加,再进行激活即得到总体模型的输出。

+

Wide 部分:Dense Features + Sparse Features(onehot 处理)+ 特征组合

+

Deep 部分:Dense Embeddings (Sparse Features 进行 onehot + embedding 处理)

+

优点:

+
    +
  1. 结构简单,复杂度低,目前在工业界仍有广泛应用;
  2. +
  3. 线性模型与深度模型优势互补,分别提取低阶与高阶特征交互信息,兼顾记忆能力与泛化能力;
  4. +
  5. 线性部分为广义线性模型,可灵活替换为其他算法,比如 FM,提升 wide 部分提取信息的能力。
  6. +
+

缺点:

+
    +
  1. 深度模型可自适应的进行高阶特征交互,但这是隐式的构造特征组合,可解释性差;
  2. +
  3. 深度模型仍需要人工特征来提升模型效果,只是需求量没有线性模型大。
  4. +
+

DCN 是基于 Wide&Deep 的改进版,它把 wide 侧的 LR 换成了 cross layer,可显式的构造有限阶特征组合,并且具有较低的复杂度。

+

img

+

优点:

+
    +
  1. 引入 cross layer 显示的构造有限阶特征组合,无需特征工程,可端到端训练;
  2. +
  3. cross layer 具有线性复杂度,可累加多层构造高阶特征交互,并且因为其类似残差连接的计算方式,使其累加多层也不会产生梯度消失问题;
  4. +
  5. 跟 deepfm 相同,两个分支共享输入,可更精确的训练学习。
  6. +
+

缺点:

+
    +
  1. cross layer 是以 bit-wise 方式构造特征组合的,最小粒度是特征向量中的每个元素,这样导致 DCN 不会考虑域的概念,属于同一特征的各个元素应同等对待;
  2. +
+

xDeepFM 是 Wide & Deep 的改进版,在此基础上添加了 CIN 层显式的构造有限阶特征组合。

+

+

上图为 xDeepFM 的总体结构,有三个分支:Linear(稀疏的01向量作为输入)、DNN(经过embedding的稠密向量作为输入)、CIN(压缩感知层)。

+

xDeepFM 如果去掉 CIN 分支,就等同于 Wide & Deep。

+

优点:

+

使用 vector-wise 的方式,通过特征的元素积来进行特征交互,将一个特征域的元素整体考虑,比 bit-wise 方式更 make sence 一些;

+

缺点:

+

CIN 层的复杂度通常比较大,它并不具有像 DCN 的 cross layer 那样线性复杂度,它的复杂度通常是平方级的,因为需要计算两个特征矩阵中特征的两两交互,这就给模型上线带来压力。

+

FNN 采用的是串行拼接的结合方式,将 DNN 接在 FM 层后方,以减轻全连接层构造隐式特征的工作。

+

FNN 并不是端到端的训练方式,而是两阶段的训练方式。阶段一训练一个 FM,阶段二训练一个带嵌入层的 DNN。

+

阶段一: 先使用带标签的训练集有监督的训练 FM 模型,训练完成后,会得到每个特征对应的隐向量。若输入特征维度为 n,隐向量维度为 k,则隐向量矩阵 W 的形状就为 [n, k]。

+

阶段二: 然后用隐向量矩阵 W 初始化 DNN 的嵌入层,然后再有监督训练 DNN 即可。DNN 的输入包含了 FM 学到的先验知识,可减轻 DNN 的学习压力。

+

优点:

+
    +
  1. 将 FM 学习得到的隐向量作为 DNN 的输入,隐向量包含了 FM 习得的先验知识,可减轻 DNN 的学习压力;
  2. +
  3. FM 只考虑到了二阶特征交互,忽略了高阶特征,后面接 DNN 可弥补该缺陷,提升模型表达能力。
  4. +
+

缺点:

+
    +
  1. 采用两阶段、非端到端的训练方式,不利于模型的线上部署;
  2. +
  3. 将 FM 的隐向量直接拼接作为 DNN 的输入,忽略了 field 的概念;
  4. +
  5. FNN 未考虑低阶特征组合,低阶、高阶特征是同等重要的。
  6. +
+

PNN 通过引入特征交互层 Product Layer,显式的对特征进行交互,以提升模型的表达能力。Product Layer层为特征交互层,由 z 和 p 两部分组成,其中 z 为上层的输出结果,p 为上层输出的特征交互结果,低维与高维特征的直接拼接。

+

优点:

+
    +
  1. 显式的进行特征交互,提高模型表达能力;
  2. +
  3. 以 field 为粒度进行特征交互,保留的域的概念;
  4. +
  5. 同时保留了低维与高维特征
  6. +
+

缺点:

+
    +
  1. 外积交互方式参数量较大,随着特征维度平方级增长;
  2. +
+

DIN:

+

Base 模型的做法是将用户点击的商品序列,简单的进行 SUM Pooling,然后将聚合得到的 embedding 向量,作为用户的兴趣表示。

+

这种做法的缺陷也很明显,简单的累加无法突出某些商品的重要性。对于与候选商品具有强关联性的 item,应该给予更大的权重,让其在提取用户兴趣时发挥更大的作用。

+

DIN 便是采用这种方式,引入 Activation Unit 为每个商品计算一个重要性权重,再 Pooling 得到兴趣表示。模型结构如下:

+

+

主要关注 Activation Unit 内的权重计算方式,该单元的输入为:用户点击的商品(Inputs from User)、候选商品(Inputs from Ad)。

+

优点:

+
    +
  1. 引入 Attention 机制,更精准的提取用户兴趣;
  2. +
  3. 引入 Dice 激活函数与,并优化了稀疏场景中的 L2 正则方式。
  4. +
+

缺点:

+
    +
  1. 没有考虑用户点击商品的相对位置信息,后续的 DIEN 也是针对这点进行了改进。
  2. +
+

DIEN:针对行为的时间顺序进行建模,挖掘用户的兴趣及兴趣变化趋势。

+

优点:

+
    +
  1. 引入 GRU 层,挖掘用户兴趣的同时,引入了行为发生的时序信息;
  2. +
  3. 引入 GRU 与 Attention 融合层,挖掘用户的兴趣变化趋势。
  4. +
+

缺点:

+
    +
  1. GRU 层难以训练充分,模型并行性较差,给模型上线带来压力;
  2. +
  3. 模型训练复杂度随着行为序列长度的增加而增长。
  4. +
+

DSIN:将行为序列划分为多个 Session,然后针对每个 Session 去挖掘用户的兴趣以及兴趣变化趋势。

+

优点:

+
    +
  1. 以会话粒度进行兴趣的提取,提取结果更精确;
  2. +
  3. 利用 Transformer Encoder 挖掘用户兴趣,学习能力更强;
  4. +
  5. 引入 双向 LSTM,挖掘用户的兴趣变化趋势;
  6. +
  7. 引入 Attention 机制,突出关键行为的重要性。
  8. +
+

缺点:

+
    +
  1. 引入 Encoder 单元,双层的LSTM,训练复杂度大,给模型上线带来压力。
  2. +
+ + +
+ +
+
+ + + + + + +
+
+
搜推面试题目准备
+
https://zhangzhao219.github.io/2024/05/22/Interview/Interview-Questions-SR/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年5月22日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/06/01/LLM-RL/index.html b/2024/06/01/LLM-RL/index.html new file mode 100644 index 000000000..51eb36820 --- /dev/null +++ b/2024/06/01/LLM-RL/index.html @@ -0,0 +1,935 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LLM强化学习 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

LLM强化学习

+ + +
+ +

LLM强化学习相关学习资料

+ +

相关链接

+

图解大模型RLHF系列之:人人都能看懂的PPO原理与源码解读:https://zhuanlan.zhihu.com/p/677607581

+

原理解析:https://blog.csdn.net/v_JULY_v/article/details/134242910

+

KL散度详解:https://blog.csdn.net/Rocky6688/article/details/103470437

+

简要版本解析:https://blog.csdn.net/u014386899/article/details/136633074

+

代码解读:https://zhuanlan.zhihu.com/p/696044978

+

强化学习

+

+

NLP中的强化学习

+

+
    +
  • :模型根据上文,产生一个token
  • +
  • :即时收益,指语言模型当下产生token的收益
  • +
  • :实际期望总收益(即时+未来),指对语言模型“当下产生token ,一直到整个response生产结束”的后期收益预估。因为当下语言模型还没产出后的token,所以我们只是对它之后一系列动作的收益做了估计,因而称为“期望总收益”。
  • +
+

+

在RLHF-PPO阶段,一共有四个主要模型 ,分别是:

+
    +
  • Actor Model:演员模型 ,这就是我们想要训练的目标语言模型
  • +
  • Critic Model:评论家模型 ,它的作用是预估总收益
  • +
  • Reward Model:奖励模型 ,它的作用是计算即时收益
  • +
  • Reference Model:参考模型 ,它的作用是在RLHF阶段给语言模型增加一些“约束”,防止语言模型训歪(朝不受控制的方向更新,效果可能越来越差)
  • +
+

Actor/Critic Model在RLHF阶段是需要训练的(图中给这两个模型加了粗边,就是表示这个含义);而Reward/Reference Model参数冻结的。

+

Critic/Reward/Reference Model共同组成了一个“奖励-loss”计算体系(我自己命名的,为了方便理解),我们综合它们的结果计算loss,用于更新Actor和Critic Model

+

Actor Model (演员模型)

+

Actor就是我们想要训练的目标语言模型。我们一般用SFT阶段产出的SFT模型来对它做初始化。

+

我们的最终目的是让Actor模型能产生符合人类喜好的response。所以我们的策略是,先喂给Actor一条prompt (这里假设batch_size = 1,所以是1条prompt),让它生成对应的response。然后,我们再将“prompt + response"送入我们的“奖励-loss”计算体系中去算得最后的loss,用于更新actor。

+

Reference Model(参考模型)

+

Reference Model(以下简称Ref模型)一般也用SFT阶段得到的SFT模型做初始化,在训练过程中,它的参数是冻结的。 Ref模型的主要作用是防止Actor”训歪”

+

我们希望训练出来的Actor模型既能达到符合人类喜好的目的,又尽量让它和SFT模型不要差异太大 。因此我们使用KL散度来衡量输出分布的相似度

+

img

+
    +
  • 对Actor模型 ,我们喂给它一个prompt,它正常输出对应的response。那么response中每一个token肯定有它对应的log_prob结果,我们把这样的结果记为log_probs
  • +
  • 对Ref模型 ,我们把Actor生成的"prompt + response"喂给它,那么它同样能给出每个token的log_prob结果,我们记其为ref_log_probs
  • +
  • 那么这两个模型的输出分布相似度就可以用 ref_log_probs - log_probs 来衡量,就是KL散度的公式 +
      +
    • ref_log_probs越高,说明Ref模型对Actor模型输出的肯定性越大。即Ref模型也认为Actor模型较Ref模型没有训歪
    • +
    +
  • +
+

Critic Model(评论家模型)

+

Critic Model用于预测期望总收益,和Actor模型一样,它需要做参数更新

+

时刻,我们给不出客观存在的总收益,我们只能训练一个模型去预测它。

+

在RLHF中,我们不仅要训练模型生成符合人类喜好的内容的能力(Actor),也要提升模型对人类喜好量化判断的能力(Critic)

+

deepspeed-chat采用了Reward模型作为它的初始化,可以简单理解成,Reward/Critic模型和Actor模型的架构是很相似的(毕竟输入都一样),同时,它在最后一层增加了一个Value Head层,该层是个简单的线形层,用于将原始输出结果映射成单一的值。

+

Reward Model(奖励模型)

+

Reward Model用于计算生成token的即时收益,它就是RW阶段所训练的奖励模型,在RLHF过程中,它的参数是冻结的。

+

Reward模型是站在上帝视角的。这个上帝视角有两层含义:

+
    +
  • 第一点,Reward模型是经过和“估算收益”相关的训练的,因此在RLHF阶段它可以直接被当作一个能产生客观值的模型。
  • +
  • 第二点,Reward模型代表的含义就是“即时收益”,你的token已经产生,因此即时收益自然可以立刻算出。
  • +
+

reward是对actor模型进行了某一个action之后的直接打分;而critic则是对这个actor模型的整体预估得分。每次actor模型更新后,critic模型都要对这个新的actor模型重新打分,所以critic模型也要更新参数。critic模型对actor模型的整体预估得分,是根据reward模型的每一次实时打分来预估的。当critic模型的预估得分达到了一定的基准,就代表actor模型训练完成。

+

RLHF-PPO的训练过程

+
    +
  • 第一步,我们准备一个batch的prompts
  • +
  • 第二步,我们将这个batch的prompts喂给Actor模型,让它生成对应的responses
  • +
  • 第三步,我们把prompt+responses喂给我们的Critic/Reward/Reference模型,让它生成用于计算actor/critic loss的数据,按照强化学习的术语,我们称这些数据为经验(experiences)。
  • +
  • 第四步,我们根据这些经验,实际计算出actor/critic loss,然后更新Actor和Critic模型
  • +
+

+

Loss

+

Actor Loss

+

直观设计

+
    +
  • Actor接收到当前上文,产出token ,概率是
  • +
  • Critic model 根据,产出对总收益的预测
  • +
  • actor loss = +
      +
    • 时,意味着Critic对Actor当前采取的动作给了正向反馈,因此我们就需要在训练迭代中提高,这样就能达到减小loss的作用。
    • +
    • 时,意味着Critic对Actor当前采取的动作给了负向反馈,因此我们就需要在训练迭代中降低,这样就能到达到减小loss的作用。
    • +
    +
  • +
+

引入优势

+

如果Critic对的总收益预测为,但实际执行后的总收益是 ,我们就定义优势为:

+

,替换上面的

+

actor loss =

+

本来是即时收益,但是可以调整一下:(是最后一个时刻)

+
    +
  • 时,我们更加关心Actor是否有在Ref的约束下生产token
  • +
  • 时,我们不仅关心Actor是否遵从了Ref的约束,也关心真正的即时收益
  • +
+

+

为什么只有最后一个时刻的被纳入了考量呢?这是因为在Reward模型训练阶段,就是用这个位置的的 来表示对完整的prompt + response的奖励预测(但不妨碍你理解成是执行完的即时奖励),然后用这个指标来做模型eval的(但是Reward训练阶段算loss时,还是考虑了response部分所有token输出的reward值)。所以到了RLHF的场景下,其余时刻的即时奖励,我们就用“Actor是否遵循了Ref的约束”来进行评价。

+

改造优势

+

+

+

新引入的也是一个常量,可将其理解为权衡因子,直觉上看它控制了在计算当前优势时对未来优势的考量。

+

对于最后一个时刻,它的未来收益和未来优势都是0,也就是,这是可以直接算出来的。而有了 ,我们可以通过动态规划的方法,把所有时刻的优势算出来

+

重复使用

+

太慢了,所以一个batch的经验值将被用于n次模型更新

+

1个batch的经验值被使用ppo_epochs次,在这ppo_epochs中,Actor是不吃任何新数据,不做任何交互的,所以我们只能让Actor“模拟”一下和环境交互的过程,吐出一些新数据出来。

+

还是保证新的数据和旧的差不多,还是使用KL散度

+

actor loss =

+

在Actor想通过模拟交互的方式,使用一个batch的经验值更新自己时,它需要收到真正吃到batch的那个时刻的Actor的约束,这样才能在有效利用batch,提升训练速度的基础上,保持训练的稳定。

+

设置一个范围,差距太大就不要更新了

+

Critic Loss

+
    +
  • :Critic对时刻的总收益的预估,这个总收益包含即时和未来的概念(预估收益)
  • +
  • :Reward计算出的即时收益,Critic预测出的及之后时候的收益的折现,这是比更接近时刻真值总收益的一个值(实际收益)
  • +
+

第一想法:Critic loss =$ (𝑅_𝑡+ \gamma ∗𝑉_{𝑡+1}-V_t)^2$

+

实际收益优化:

+

预估收益优化:类比于Actor,Critic模型在ppo_epochs的过程中也是不断更新的。所以这个可以理解成是 ,也就是真正吃了batch,参与产出经验的那个时候的Critic产出的收益预测结果。

+

用老设计了了一个变动范围,然后用这个变动范围去约束新

+

最终我们就取实际收益和预估收益的MSE做为loss就好,这里注意,计算实际收益时都是老Critic(真正吃了batch的那个)产出的结果,而预估收益是随着ppo_epochs而变动的。

+

DPO

+

DPO通过简单的分类目标直接优化最满足偏好的策略,而没有明确的奖励函数或RL

+

DPO的本质在于增加了被首选的response相对不被首选的response的对数概率,但它包含了一个动态的、每个示例的重要性权重,以防止设计的概率比让模型的能力退化。

+

img

+

变种

+

IPO相当于在DPO的损失函数上添加了一个正则项,从而可以使得不使用early stopping技巧就可以使模型收敛。

+

KTO定义的损失函数只需要将样本标注为"好(good)“或"坏(bad)”,从而使得获取标注样本的成本更低。(就是不需要一对一对标注了)

+

CPO在训练期间不需要加载参考策略模型。通过省略内存的参考模型,CPO提高了操作效率,与DPO相比,能够以更低的成本训练更大的模型。

+

ORPO整合SFT和DPO,且不需要额外的参考模型

+

SimPO 包含两个主要组件:(1)在长度上归一化的奖励,其计算方式是使用策略模型的奖励中所有 token 的平均对数概率;(2)目标奖励差额,用以确保获胜和失败响应之间的奖励差超过这个差额。

+

SimPO 不需要参考模型,性能却明显优于 DPO 及其最新变体,且不会显著增加响应长度

+ + +
+ +
+
+ + + + + + +
+
+
LLM强化学习
+
https://zhangzhao219.github.io/2024/06/01/LLM-RL/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年6月1日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/06/01/Leetcode/Leetcode-gt/index.html b/2024/06/01/Leetcode/Leetcode-gt/index.html new file mode 100644 index 000000000..cc735b255 --- /dev/null +++ b/2024/06/01/Leetcode/Leetcode-gt/index.html @@ -0,0 +1,791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-图论 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-图论

+ + +
+ +

Leetcode-图论

+ +

图论

+

207. 课程表

+

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

+

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a<sub>i</sub>, b<sub>i</sub>] ,表示如果要学习课程 a<sub>i</sub>必须 先学习课程 b<sub>i</sub> ~ ~ 。

+
    +
  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1
  • +
+

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
+        vector<int> totalnum(numCourses,0);
+        vector<vector<int> > matrix(numCourses, vector<int>(0,0));
+        for(int i=0;i<prerequisites.size();i++){
+            totalnum[prerequisites[i][0]] += 1;
+            matrix[prerequisites[i][1]].push_back(prerequisites[i][0]);
+        }
+        bool judge = true;
+        while(judge){
+            judge = false;
+            for(int i=0;i<numCourses;i++){
+                if(totalnum[i] == 0){
+                    judge = true;
+                    for(int j=0;j<matrix[i].size();j++){
+                        totalnum[matrix[i][j]] -= 1;
+                    }
+                    totalnum[i] = -1;
+                }
+            }
+        }
+        for(int i=0;i<numCourses;i++){
+            if(totalnum[i] != -1){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-图论
+
https://zhangzhao219.github.io/2024/06/01/Leetcode/Leetcode-gt/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年6月1日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/06/01/Leetcode/Leetcode-hot200/index.html b/2024/06/01/Leetcode/Leetcode-hot200/index.html new file mode 100644 index 000000000..592c54cbc --- /dev/null +++ b/2024/06/01/Leetcode/Leetcode-hot200/index.html @@ -0,0 +1,3750 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-Hot 100 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-Hot 100

+ + +
+ +

Leetcode-Hot 100

+ +

哈希

+

1. 两数之和

+

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 **和为目标值 **target 的那 两个 整数,并返回它们的数组下标。

+

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

+

你可以按任意顺序返回答案。

+
class Solution {
+public:
+    vector<int> twoSum(vector<int>& nums, int target) {
+        vector<int> result(2,0);
+        map<int,int> mp;
+        for(int i=0;i<nums.size();i++){
+            if(mp.find(target - nums[i]) != mp.end()){
+                result[0] = mp[target - nums[i]];
+                result[1] = i;
+                break;
+            }
+            mp[nums[i]] = i;
+        }
+        return result;
+    }
+};
+

49. 字母异位词分组

+

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

+

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

+
class Solution {
+public:
+    vector<vector<string>> groupAnagrams(vector<string>& strs) {
+        vector<vector<string> > result;
+        unordered_map<string, vector<string> > mp;
+        for(int i=0;i<strs.size();i++){
+            string t = strs[i];
+            sort(t.begin(),t.end());
+            mp[t].push_back(strs[i]);
+        }
+        for (auto it = mp.begin();it != mp.end();it++){
+            result.push_back(it->second);
+        }
+        return result;
+    }
+};
+

128. 最长连续序列

+

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

+

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

+
class Solution {
+public:
+    int longestConsecutive(vector<int>& nums) {
+        unordered_map<int, bool> mp;
+        if(nums.size() == 0){
+            return 0;
+        }
+        for(int i=0;i<nums.size();i++){
+            mp[nums[i]] = true;
+        }
+        int maxcount = 0;
+        for(int i=0;i<nums.size();i++){
+            int tempcount = 0;
+            int num = nums[i];
+            if(mp.find(num-1) != mp.end()){
+                continue;
+            }
+            while(mp.find(num) != mp.end()){
+                num += 1;
+                tempcount += 1;
+            }
+            maxcount = max(maxcount, tempcount);
+        }
+        return maxcount;
+    }
+};
+

双指针

+

283. 移动零

+

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

+

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

+
class Solution {
+public:
+    void moveZeroes(vector<int>& nums) {
+        int left = 0;
+        for(int right=0; right < nums.size(); right++){
+            if(nums[right] != 0){
+                nums[left] = nums[right];
+                left += 1;
+            }
+        }
+        while(left < nums.size()){
+            nums[left] = 0;
+            left += 1;
+        }
+    }
+};
+

11. 盛最多水的容器

+

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

+

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

+

返回容器可以储存的最大水量。

+

说明: 你不能倾斜容器。

+
class Solution {
+public:
+    int maxArea(vector<int>& height) {
+        int result = 0;
+        int left = 0;
+        int right = height.size()-1;
+        while(left < right){
+            result = max(result, (right - left) * min(height[left], height[right]));
+            cout << left << " " << right << " " << endl;
+            if(height[left] < height[right]){
+                left += 1;
+            } else{
+                right -= 1;
+            }
+        }
+        return result;
+    }
+};
+

15. 三数之和

+

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

+

你返回所有和为 0 且不重复的三元组。

+

注意: 答案中不可以包含重复的三元组。

+
class Solution {
+public:
+    vector<vector<int>> threeSum(vector<int>& nums) {
+        vector<vector<int>> result;
+        sort(nums.begin(), nums.end());
+        for(int start = 0; start < nums.size()-2;start += 1){
+            if(start > 0 && nums[start] == nums[start - 1]){
+                continue;
+            }
+            int left = start + 1;
+            int right = nums.size() - 1;
+            while(left < right){
+                int res = nums[start] + nums[left] + nums[right];
+                if(res == 0){
+                    result.push_back(vector<int> {nums[start], nums[left], nums[right]});
+                    while(left < right && nums[right] == nums[right-1]){
+                        right -= 1;
+                    }
+                    while(left < right && nums[left] == nums[left + 1]){
+                        left += 1;
+                    }
+                    left += 1;
+                    right -= 1;
+                } else if (res < 0){
+                    left += 1;
+                } else{
+                    right -= 1;
+                }
+            }
+        }
+        return result;
+    }
+};
+

42. 接雨水

+

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

+
class Solution {
+public:
+    int trap(vector<int>& height) {
+        int result = 0;
+        int leftMax = 0;
+        int rightMax = 0;
+        int left = 0;
+        int right = height.size() - 1;
+        while(left < right){
+            leftMax = max(leftMax, height[left]);
+            rightMax = max(rightMax, height[right]);
+            if(height[left] < height[right]){
+                result += leftMax - height[left];
+                left += 1;
+            } else{
+                result += rightMax - height[right];
+                right -= 1;
+            }
+        }
+        return result;
+    }
+};
+

滑动窗口

+

3. 无重复字符的最长子串

+

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

+
class Solution {
+public:
+    int lengthOfLongestSubstring(string s) {
+        int n = s.size();
+        int result = 0;
+        int right = 0;
+        unordered_set<char> st;
+        for(int left = 0;left < n;left++){
+            while(right < n && !st.count(s[right])){
+                st.insert(s[right]);
+                right++;
+            }
+            result = max(result, right - left);
+            st.erase(s[left]);
+        }
+        return result;
+    }
+};
+

438. 找到字符串中所有字母异位词

+

给定两个字符串 sp,找到 s 中所有 p 的 **异位词 **的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

+

**异位词 **指由相同字母重排列形成的字符串(包括相同的字符串)。

+
class Solution {
+public:
+    vector<int> findAnagrams(string s, string p) {
+        vector<int> schar(26, 0);
+        for(int i=0;i<p.size();i++){
+            schar[p[i]-'a'] += 1;
+        }
+        vector<int> result;
+        if(p.size() > s.size()){
+            return result;
+        }
+        int left = 0;
+        for(int right = 0;right < s.size();right++){
+            schar[s[right]-'a'] -= 1;
+            if (right < p.size()-1){
+                continue;
+            }
+            bool flag = false;
+            for(int i=0;i<26;i++){
+                if(schar[i] != 0){
+                    flag = true;
+                    break;
+                }
+            }
+            if(flag == false){
+                result.push_back(left);
+            }
+            schar[s[left]-'a'] += 1;
+            left += 1;
+        }
+        return result;
+    }
+};
+

子串

+

560. 和为 K 的子数组

+

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数。

+

子数组是数组中元素的连续非空序列。

+
class Solution {
+public:
+    int subarraySum(vector<int>& nums, int k) {
+        int n = nums.size();
+        vector<int> pre(n+1,0);
+        for(int i=1;i<=n;i++){
+            pre[i] = nums[i-1] + pre[i-1];
+        }
+        for(int i=0;i<=n;i++){
+            cout << pre[i] << " ";
+        }
+        cout << endl;
+        unordered_map<int, int> cnt;
+        int result = 0;
+        for(int i=0; i<=n; i++){
+            if (cnt.contains(pre[i] - k)){
+                result += cnt[pre[i] - k];
+            }
+            cnt[pre[i]] += 1;
+        }
+        return result;
+    }
+};
+

239. 滑动窗口最大值

+

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

+

返回 *滑动窗口中的最大值 * 。

+
class Solution {
+public:
+    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
+        deque<int> st;
+        vector<int> result;
+        int n = nums.size();
+        for(int i=0;i<n;i++){
+            if(!st.empty() && st.front() <= i-k){
+                st.pop_front();
+            }
+            while(!st.empty() && nums[st.back()] < nums[i]){
+                st.pop_back();
+            }
+            st.push_back(i);
+            if(i >= k-1){
+                result.push_back(nums[st.front()]);
+            }
+        }
+        return result;
+    }
+};
+

76. 最小覆盖子串

+

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

+

注意:

+
    +
  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • +
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。
  • +
+
class Solution {
+private:
+    unordered_map<char, int> map_window, map_base;
+    bool check() {
+        for (auto &it : map_base) { // 遍历字符串t的哈希表!!不要遍历错了!!
+            // 只要出现窗口内的某字符数量<字符串t的同一字符数量,则说明“窗口字符串”未覆盖“字符串t”
+            if (map_window[it.first] < it.second) {
+                return false;
+            }
+        }
+        return true;
+    }
+public:
+    string minWindow(string s, string t) {
+        // 初始化固定“字符串t”的字符频率哈希表
+        for (char c : t) {
+            map_base[c]++;
+        }
+        int left = 0;
+        int current_min_length = INT_MAX; // 因为后续下相当于取min,所以这里取MAX
+        int res_start_index = -1;
+        for (int right = 0; right < s.size(); right++) {
+            map_window[s[right]]++; // 更新“当前窗口字符串”的哈希表(right右移,添加字符)
+            while (check()) { // 如果覆盖
+                if (right - left + 1 < current_min_length) { // 窗口大小 < 当前覆盖子串最小长度
+                    current_min_length = right - left + 1; // 更新当前覆盖子串的最小长度
+                    res_start_index = left; // 记录该覆盖子串的起始索引。最后直接通过起始索引+最小长度来求结果,避免这里重复的拷贝复制
+                }
+                map_window[s[left]]--; // 更新“当前窗口字符串”的哈希表(left右移,删除字符)
+                left++;
+            }
+        }
+        // 别漏了无结果的情况,返回空串
+        return res_start_index == -1 ? "" : s.substr(res_start_index, current_min_length);
+    }
+};
+

普通数组

+

53. 最大子数组和

+

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

+

**子数组 **是数组中的一个连续部分。

+
class Solution {
+public:
+    int maxSubArray(vector<int>& nums) {
+        int result = nums[0];
+        int n = nums.size();
+        vector<int> dp(n, 0);
+        dp[0] = nums[0];
+        for(int i=1;i<n;i++){
+            dp[i] = max(nums[i], nums[i] + dp[i-1]);
+            result = max(result, dp[i]);
+        }
+        return result;
+    }
+};
+

56. 合并区间

+

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [start<sub>i</sub>, end<sub>i</sub>] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

+
class Solution {
+public:
+    static bool cmp(vector<int> &a, vector<int> &b){
+        if(a[0] == b[0]){
+            return a[1] < b[1];
+        }
+        return a[0] < b[0];
+    }
+    vector<vector<int>> merge(vector<vector<int>>& intervals) {
+        vector<vector<int> > result;
+        sort(intervals.begin(), intervals.end(), cmp);
+        bool now = false;
+        int start = intervals[0][0];
+        int end = intervals[0][1];
+        for(int i=0;i<intervals.size();i++){
+            if(intervals[i][0] > end){
+                result.push_back(vector<int>{start, end});
+                start = intervals[i][0];
+                end = intervals[i][1];
+            } else{
+                start = min(start, intervals[i][0]);
+                end = max(end, intervals[i][1]);
+            }
+        }
+        result.push_back(vector<int>{start, end});
+        return result;
+    }
+};
+

189. 轮转数组

+

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

+
class Solution {
+public:
+    void reverse(vector<int> & nums, int start, int end){
+        while(start < end){
+            int temp = nums[start];
+            nums[start] = nums[end];
+            nums[end] = temp;
+            start += 1;
+            end -= 1;
+        }
+    }
+    void rotate(vector<int>& nums, int k) {
+        k = k % nums.size();
+        reverse(nums,0, nums.size()-1);
+        reverse(nums,0,k-1);
+        reverse(nums,k,nums.size()-1);
+    }
+};
+

238. 除自身以外数组的乘积

+

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积

+

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

+

不要使用除法, 且在 O(<em>n</em>) 时间复杂度内完成此题。

+
class Solution {
+public:
+    vector<int> productExceptSelf(vector<int>& nums) {
+        vector<int> pre(nums.size()+1,1);
+        vector<int> back(nums.size()+1,1);
+        for(int i=0;i<nums.size();i++){
+            pre[i+1] = nums[i] * pre[i];
+        }
+        for(int i=nums.size();i>0;i--){
+            back[i-1] = nums[i-1] * back[i];
+        }
+        vector<int> result(nums.size(),0);
+        for(int i=0;i<nums.size()+1;i++){
+            cout << pre[i] << " " << back[i] << endl;
+        }
+        for(int i=0;i<nums.size();i++){
+            result[i] = pre[i] * back[i+1];
+        }
+        return result;
+    }
+};
+

41. 缺失的第一个正数

+

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

+

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

+
class Solution {
+public:
+    int firstMissingPositive(vector<int>& nums) {
+        int n = nums.size();
+        for(int i=0;i<n;i++){
+            if(nums[i] <= 0){
+                nums[i] = n + 1;
+            }
+        }
+        for(int i=0;i<n;i++){
+            int num = abs(nums[i]);
+            if(num <= n){
+                nums[num-1] = -abs(nums[num-1]);
+            }
+        }
+        for(int i=0;i<n;i++){
+            if(nums[i] > 0){
+                return i+1;
+            }
+        }
+        return n+1;
+    }
+};
+

矩阵

+

73. 矩阵置零

+

给定一个 <em>m</em> x <em>n</em> 的矩阵,如果一个元素为0 ,则将其所在行和列的所有元素都设为0 。请使用 原地 算法

+
class Solution {
+public:
+    void setZeroes(vector<vector<int>>& matrix) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        bool firstrow = false;
+        bool firstcol = false;
+        for(int i=0;i<m;i++){
+            if(matrix[i][0] == 0){
+                firstcol = true;
+                break;
+            }
+        }
+        for(int j=0;j<n;j++){
+            if(matrix[0][j] == 0){
+                firstrow = true;
+                break;
+            }
+        }
+        for(int i=1;i<m;i++){
+            for(int j=1;j<n;j++){
+                if(matrix[i][j] == 0){
+                    matrix[0][j] = 0;
+                    matrix[i][0] = 0;
+                }
+            }
+        }
+        for(int i=1;i<m;i++){
+            for(int j=1;j<n;j++){
+                if(matrix[i][0] == 0 || matrix[0][j] == 0){
+                    matrix[i][j] = 0;
+                }
+            }
+        }
+        if(firstcol == true){
+            for(int i=0;i<m;i++){
+                matrix[i][0] = 0;
+            }
+        }
+        if(firstrow == true){
+            for(int j=0;j<n;j++){
+                matrix[0][j] = 0;
+            }
+        }
+    }
+};
+

54. 螺旋矩阵

+

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

+
class Solution {
+public:
+    vector<int> spiralOrder(vector<vector<int>>& matrix) {
+        vector<int> result;
+        int m = matrix.size();
+        int n = matrix[0].size();
+        vector<vector<bool> > visit(m, vector<bool>(n, false));
+        vector<vector<int> > directions{
+            {0,1},{1,0},{0,-1},{-1,0}
+        };
+        int xIndex = 0;
+        int yIndex = 0;
+        result.push_back(matrix[xIndex][yIndex]);
+        visit[xIndex][yIndex] = true;
+        while(result.size() != m*n){
+            for(int i=0;i<directions.size();i++){
+                while(xIndex + directions[i][0] >= 0 && xIndex + directions[i][0] < m && yIndex + directions[i][1] >= 0 && yIndex + directions[i][1] < n && visit[xIndex + directions[i][0]][yIndex + directions[i][1]] == false){
+                    xIndex = xIndex + directions[i][0];
+                    yIndex = yIndex + directions[i][1];
+                    result.push_back(matrix[xIndex][yIndex]);
+                    visit[xIndex][yIndex] = true;
+                }
+            }
+        }
+        return result;
+    }
+};
+

48. 旋转图像

+

给定一个 *n * × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

+

你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。**请不要 **使用另一个矩阵来旋转图像。

+
class Solution {
+public:
+    void rotate(vector<vector<int>>& matrix) {
+        int n = matrix.size();
+        for(int i=0;i<(n+1)/2;i++){
+            for(int j=0;j<n/2;j++){
+                int temp = matrix[n-j-1][i];
+                matrix[n-j-1][i] = matrix[n-i-1][n-j-1];
+                matrix[n-i-1][n-j-1] = matrix[j][n-i-1];
+                matrix[j][n-i-1] = matrix[i][j];
+                matrix[i][j] = temp;
+            }
+        }
+    }
+};
+

240. 搜索二维矩阵 II

+

编写一个高效的算法来搜索 <em>m</em> x <em>n</em> 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

+
    +
  • 每行的元素从左到右升序排列。
  • +
  • 每列的元素从上到下升序排列。
  • +
+
class Solution {
+public:
+    bool searchMatrix(vector<vector<int>>& matrix, int target) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        int x = 0;
+        int y = n-1;
+        while(x < m && y >= 0){
+            if (matrix[x][y] == target){
+                return true;
+            }
+            if(target < matrix[x][y]){
+                y -= 1;
+            }
+            else{
+                x += 1;
+            }
+        }
+        return false;
+    }
+};
+

链表

+

160. 相交链表

+

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

+
class Solution {
+public:
+    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
+        ListNode* p = headA;
+        ListNode* q = headB;
+        while(p != q){
+            if(p == NULL){
+                p = headB;
+            } else{
+                p = p->next;
+            }
+            if(q == NULL){
+                q = headA;
+            } else{
+                q = q->next;
+            }
+        }
+        return p;
+    }
+};
+

206. 反转链表

+

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

+
class Solution {
+public:
+    ListNode* reverseList(ListNode* head) {
+        ListNode* pre = NULL;
+        while(head != NULL){
+            ListNode* q = head->next;
+            head->next = pre;
+            pre = head;
+            head = q;
+        }
+        return pre;
+    }
+};
+

234. 回文链表

+

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool isPalindrome(ListNode* head) {
+        ListNode* pre = NULL;
+        ListNode* slow = head;
+        ListNode* fast = head;
+        bool flag = false;
+        while(fast != NULL){
+            if(fast->next == NULL){
+                fast = fast->next;
+                flag = true;
+            } else{
+                fast = fast->next->next;
+            }
+            ListNode* q = slow->next;
+            slow->next = pre;
+            pre = slow;
+            slow = q;
+            if(flag){
+                pre = pre->next;
+            }
+
+        }
+        while(pre != NULL && slow != NULL){
+            if(pre->val != slow->val){
+                return false;
+            }
+            pre = pre->next;
+            slow = slow->next;
+        }
+        return true;
+
+    }
+};
+

141. 环形链表

+

给你一个链表的头节点 head ,判断链表中是否有环。

+

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 **注意:pos 不作为参数进行传递 ** 。仅仅是为了标识链表的实际情况。

+

如果链表中存在环 ,则返回 true 。 否则,返回 false

+
class Solution {
+public:
+    bool hasCycle(ListNode *head) {
+        if(head == NULL){
+            return false;
+        }
+        ListNode* slow = head;
+        ListNode* fast = head->next;
+        while(fast != NULL && fast->next != NULL){
+            slow = slow->next;
+            fast = fast->next->next;
+            if(slow == fast){
+                return true;
+            }
+        } 
+        return false;
+    }
+};
+

142. 环形链表 II

+

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

+

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置( 索引从 0 开始 )。如果 pos-1,则在该链表中没有环。 注意:pos 不作为参数进行传递 ,仅仅是为了标识链表的实际情况。

+

**不允许修改 **链表。

+
class Solution {
+public:
+    ListNode *detectCycle(ListNode *head) {
+        if(head == NULL){
+            return NULL;
+        }
+        ListNode* slow = head;
+        ListNode* fast = head;
+        bool flag = false;
+        while(fast != NULL && fast->next != NULL){
+            slow = slow->next;
+            fast = fast->next->next;
+            if(slow == fast){
+                flag = true;
+                break;
+            }
+        }
+        if(flag == false){
+            return NULL;
+        }
+        fast = head;
+        while(slow != fast){
+            slow = slow->next;
+            fast = fast->next;
+        }
+        return slow;
+    }
+};
+

21. 合并两个有序链表

+

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

+
class Solution {
+public:
+    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
+        ListNode* dummy = new ListNode(-1);
+        ListNode* head = dummy;
+        while(list1 != NULL && list2 != NULL){
+            if(list1->val < list2->val){
+                dummy->next = list1;
+                list1 = list1->next;
+                dummy = dummy->next;
+            } else{
+                dummy->next = list2;
+                list2 = list2->next;
+                dummy = dummy->next;
+            }
+        }
+        if(list1 != NULL){
+            dummy->next = list1;
+        } else{
+            dummy->next = list2;
+        }
+        return head->next;
+    }
+};
+

2. 两数相加

+

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

+

请你将两个数相加,并以相同形式返回一个表示和的链表。

+

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

+
class Solution {
+public:
+    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
+        ListNode* dummy = new ListNode(-1);
+        ListNode* l3 = dummy;
+        int cur = 0;
+        int sum = 0;
+        while(l1 != NULL || l2 != NULL){
+            if(l1 != NULL && l2 != NULL){
+                sum = l1->val + l2->val + cur;
+                l1 = l1->next;
+                l2 = l2->next;
+            } else if (l1 != NULL){
+                sum = l1->val + cur;
+                l1 = l1->next;
+            } else if(l2 != NULL){
+                sum = l2->val + cur;
+                l2 = l2->next;
+            }
+            if(sum >= 10){
+                sum -= 10;
+                cur = 1;
+            } else{
+                cur = 0;
+            }
+            l3->next = new ListNode(sum);
+            l3 = l3->next;
+        }
+        if (cur == 1){
+            l3->next = new ListNode(1);
+        }
+        return dummy->next;
+    }
+};
+

19. 删除链表的倒数第 N 个结点

+

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

+
class Solution {
+public:
+    ListNode* removeNthFromEnd(ListNode* head, int n) {
+        ListNode* dummy = new ListNode(-1);
+        dummy->next = head;
+        ListNode* pre = dummy;;
+        ListNode* slow = head;
+        ListNode* fast = head;
+        while(n--){
+            fast = fast->next;
+        }
+        while(fast != NULL){
+            pre = slow;
+            slow = slow->next;
+            fast = fast->next;
+        }
+        pre->next = pre->next->next;
+        return dummy->next;
+    }
+};
+

24. 两两交换链表中的节点

+

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

+
class Solution {
+public:
+    ListNode* reverseList(ListNode* head){
+        ListNode* pre = NULL;
+        while(head != NULL){
+            ListNode* q = head->next;
+            head->next = pre;
+            pre = head;
+            head = q;
+        }
+        return pre;
+    }
+    ListNode* swapPairs(ListNode* head) {
+        ListNode* dummy = new ListNode(-1);
+        ListNode* realhead = dummy;
+        while(head != NULL && head->next != NULL){
+            ListNode* q = head->next->next;
+            head->next->next = NULL;
+            dummy->next = reverseList(head);
+            dummy = dummy->next->next;
+            head = q;
+        }
+        if(head != NULL){
+            dummy->next = head;
+        }
+        return realhead->next;
+    }
+};
+

25. K 个一组翻转链表

+

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

+

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

+

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

+
class Solution {
+public:
+    ListNode* reverseList(ListNode* head){
+        ListNode* pre = NULL;
+        while(head != NULL){
+            ListNode* q = head->next;
+            head->next = pre;
+            pre = head;
+            head = q;
+        }
+        return pre;
+    }
+    ListNode* reverseKGroup(ListNode* head, int k) {
+        ListNode* dummy = new ListNode(-1);
+        ListNode* realhead = dummy;
+  
+        while(1){
+            bool sign = false;
+            ListNode* p = head;
+            ListNode* pre = NULL;
+            for(int i=0;i<k;i++){
+                if(p == NULL){
+                    sign = true;
+                    break;
+                }
+                pre = p;
+                p = p->next;
+            }
+            if(sign){
+                break;
+            }
+            pre->next = NULL;
+            dummy->next = reverseList(head);
+            for(int i=0;i<k;i++){
+                dummy = dummy->next;
+            }
+            head = p;
+        }
+        if(head != NULL){
+            dummy->next = head;
+        }
+        return realhead->next;
+    }
+};
+

138. 随机链表的复制

+

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

+

构造这个链表的 深拷贝 。 深拷贝应该正好由 n全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。 **复制链表中的指针都不应指向原链表中的节点 ** 。

+

例如,如果原链表中有 XY 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 xy ,同样有 x.random --> y

+

返回复制链表的头节点。

+

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

+
    +
  • val:一个表示 Node.val 的整数。
  • +
  • random_index:随机指针指向的节点索引(范围从 0n-1);如果不指向任何节点,则为 null
  • +
+

你的代码 接受原链表的头节点 head 作为传入参数。

+
class Solution {
+public:
+    Node* copyRandomList(Node* head) {
+        if(head == NULL){
+            return NULL;
+        }
+        Node* cur = head;
+        unordered_map<Node*, Node*> map;
+        // 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
+        while(cur != NULL) {
+            map[cur] = new Node(cur->val);
+            cur = cur->next;
+        }
+        cur = head;
+        // 4. 构建新链表的 next 和 random 指向
+        while(cur != NULL) {
+            map[cur]->next = map[cur->next];
+            map[cur]->random = map[cur->random];
+            cur = cur->next;
+        }
+        // 5. 返回新链表的头节点
+        return map[head];
+    }
+};
+

148. 排序链表

+

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

+
class Solution {
+public:
+    ListNode* mergesort(ListNode* head){
+        if(head == NULL || head->next == NULL){
+            return head;
+        }
+        ListNode* slow = head;
+        ListNode* fast = head->next;
+        while(fast != NULL && fast->next != NULL){
+            slow = slow->next;
+            fast = fast->next->next;
+        }
+  
+        ListNode* right = mergesort(slow->next);
+        if(slow != NULL){
+            slow->next = NULL;
+        }
+        ListNode* left = mergesort(head);
+        ListNode* dummy = new ListNode(-1);
+        ListNode* p = dummy;
+        while(left != NULL || right != NULL){
+            if(left == NULL){
+                p->next = right;
+                right = right->next;
+            } else if(right == NULL){
+                p->next = left;
+                left = left->next;
+            } else{
+                if(left->val < right->val){
+                    p->next = left;
+                    left = left->next;
+                } else{
+                    p->next = right;
+                    right = right->next;
+                }
+            }
+            p = p->next;
+        }
+        return dummy->next;
+    }
+    ListNode* sortList(ListNode* head) {
+        return mergesort(head);
+    }
+};
+

23. 合并 K 个升序链表

+

给你一个链表数组,每个链表都已经按升序排列。

+

请你将所有链表合并到一个升序链表中,返回合并后的链表。

+
class Solution {
+public:
+    ListNode* merge(vector<ListNode*>& lists, int start, int end){
+        if(start == end){
+            return lists[start];
+        }
+        int mid = (end - start) / 2 + start;
+        ListNode* left = merge(lists, start, mid);
+        ListNode* right = merge(lists, mid+1, end);
+        ListNode* dummy = new ListNode(-1);
+        ListNode* p = dummy;
+        while(left != NULL || right != NULL){
+            if(left == NULL){
+                dummy->next = right;
+                right = right->next;
+            } else if(right == NULL){
+                dummy->next = left;
+                left = left->next;
+            } else{
+                if(left->val < right -> val){
+                    dummy->next = left;
+                    left = left->next;
+                } else{
+                    dummy->next = right;
+                    right = right->next;
+                }
+            }
+            dummy = dummy->next;
+        }
+        return p->next;
+    }
+    ListNode* mergeKLists(vector<ListNode*>& lists) {
+        if(lists.size() == 0){
+            return NULL;
+        }
+        return merge(lists, 0, lists.size()-1);
+    }
+};
+

146. LRU 缓存

+

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

+

实现 LRUCache 类:

+
    +
  • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
  • +
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • +
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
  • +
+

函数 getput 必须以 O(1) 的平均时间复杂度运行。

+
struct DLinkedNode {
+    int key, value;
+    DLinkedNode* prev;
+    DLinkedNode* next;
+    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
+    DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
+};
+
+class LRUCache {
+private:
+    unordered_map<int, DLinkedNode*> cache;
+    DLinkedNode* head;
+    DLinkedNode* tail;
+    int size;
+    int capacity;
+public:
+    LRUCache(int _capacity): capacity(_capacity), size(0) {
+        // 使用伪头部和伪尾部节点
+        head = new DLinkedNode();
+        tail = new DLinkedNode();
+        head->next = tail;
+        tail->prev = head;
+    }
+  
+    int get(int key) {
+        if (!cache.count(key)) {
+            return -1;
+        }
+        // 如果 key 存在,先通过哈希表定位,再移到头部
+        DLinkedNode* node = cache[key];
+        moveToHead(node);
+        return node->value;
+    }
+  
+    void put(int key, int value) {
+        if (!cache.count(key)) {
+            // 如果 key 不存在,创建一个新的节点
+            DLinkedNode* node = new DLinkedNode(key, value);
+            // 添加进哈希表
+            cache[key] = node;
+            // 添加至双向链表的头部
+            addToHead(node);
+            ++size;
+            if (size > capacity) {
+                // 如果超出容量,删除双向链表的尾部节点
+                DLinkedNode* removed = removeTail();
+                // 删除哈希表中对应的项
+                cache.erase(removed->key);
+                // 防止内存泄漏
+                delete removed;
+                --size;
+            }
+        }
+        else {
+            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
+            DLinkedNode* node = cache[key];
+            node->value = value;
+            moveToHead(node);
+        }
+    }
+    void addToHead(DLinkedNode* node) {
+        node->prev = head;
+        node->next = head->next;
+        head->next->prev = node;
+        head->next = node;
+    }
+  
+    void removeNode(DLinkedNode* node) {
+        node->prev->next = node->next;
+        node->next->prev = node->prev;
+    }
+
+    void moveToHead(DLinkedNode* node) {
+        removeNode(node);
+        addToHead(node);
+    }
+
+    DLinkedNode* removeTail() {
+        DLinkedNode* node = tail->prev;
+        removeNode(node);
+        return node;
+    }
+};
+

二叉树

+

94. 二叉树的中序遍历

+

给定一个二叉树的根节点 root ,返回 它的 中序 遍历

+
class Solution {
+public:
+    void inorder(TreeNode* root, vector<int> & result){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left, result);
+        result.push_back(root->val);
+        inorder(root->right, result);
+    }
+    vector<int> inorderTraversal(TreeNode* root) {
+        vector<int> result;
+        inorder(root, result);
+        return result;
+    }
+};
+

104. 二叉树的最大深度

+

给定一个二叉树 root ,返回其最大深度。

+

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

+
class Solution {
+public:
+    int max_depth = 0;
+    void DFS(TreeNode* root, int depth){
+        if(root == NULL){
+            max_depth = max(max_depth, depth);
+            return;
+        }
+        DFS(root->left, depth+1);
+        DFS(root->right, depth+1);
+    }
+    int maxDepth(TreeNode* root) {
+        DFS(root,0);
+        return max_depth;
+    }
+};
+

226. 翻转二叉树

+

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

+
class Solution {
+public:
+    TreeNode* invertTree(TreeNode* root) {
+        if(root == NULL){
+            return NULL;
+        }
+        swap(root->left, root->right);
+        invertTree(root->left);
+        invertTree(root->right);
+        return root;
+    }
+};
+

101. 对称二叉树

+

给你一个二叉树的根节点 root , 检查它是否轴对称。

+
class Solution {
+public:
+    bool issame(TreeNode* left, TreeNode* right){
+        if(left == NULL && right == NULL){
+            return true;
+        } else if(left == NULL){
+            return false;
+        } else if(right == NULL){
+            return false;
+        } else{
+            if(left->val != right->val){
+                return false;
+            }
+        }
+        return issame(left->right, right->left) && issame(right->right, left->left);
+    }
+    bool isSymmetric(TreeNode* root) {
+        if(root == NULL){
+            return true;
+        }
+        return issame(root->left, root->right);
+    }
+};
+

543. 二叉树的直径

+

给你一棵二叉树的根节点,返回该树的 直径

+

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root

+

两节点之间路径的 长度 由它们之间边数表示。

+
class Solution {
+public:
+    int maxlength = 0;
+    int DFS(TreeNode* root, int nowlength){
+        if(root == NULL){
+            return 0;
+        }
+        int leftlength = DFS(root->left, nowlength+1);
+        int rightlength = DFS(root->right, nowlength+1);
+        maxlength = max(maxlength, leftlength + rightlength + 1);
+        return max(leftlength, rightlength) + 1;
+    }
+    int diameterOfBinaryTree(TreeNode* root) {
+        DFS(root,0);
+        return maxlength-1;
+    }
+};
+

102. 二叉树的层序遍历

+

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

+
class Solution {
+public:
+    vector<vector<int>> levelOrder(TreeNode* root) {
+        vector<vector<int>> result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode* > q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            vector<int> temp;
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                temp.push_back(t->val);
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+            }
+            result.push_back(temp);
+        }
+        return result;
+    }
+};
+

108. 将有序数组转换为二叉搜索树

+

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。

+
class Solution {
+public:
+    TreeNode* sortedArrayToBST(vector<int>& nums) {
+        if(nums.size() == 0){
+            return NULL;
+        }
+        int start = 0;
+        int end = nums.size();
+        int mid = (start + end) / 2;
+        vector<int> numsleft;
+        vector<int> numsright;
+        for(int i=start;i<end;i++){
+            if(i < mid){
+                numsleft.push_back(nums[i]);
+            } else if(i > mid){
+                numsright.push_back(nums[i]);
+            }
+        }
+        TreeNode* left = sortedArrayToBST(numsleft);
+        TreeNode* root = new TreeNode(nums[mid]);
+        TreeNode* right = sortedArrayToBST(numsright);
+        root->left = left;
+        root->right = right;
+        return root;
+  
+    }
+};
+

98. 验证二叉搜索树

+

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

+

有效 二叉搜索树定义如下:

+
    +
  • 节点的左子树只包含** 小于 **当前节点的数。
  • +
  • 节点的右子树只包含 大于 当前节点的数。
  • +
  • 所有左子树和右子树自身必须也是二叉搜索树。
  • +
+
class Solution {
+public:
+    vector<int> result;
+    void inorder(TreeNode* root){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left);
+        result.push_back(root->val);
+        inorder(root->right);
+    }
+    bool isValidBST(TreeNode* root) {
+        inorder(root);
+        for(int i=0;i<result.size()-1;i++){
+            if(result[i] < result[i+1]){
+                continue;
+            }
+            return false;
+        }
+        return true;
+    }
+};
+

230. 二叉搜索树中第K小的元素

+

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

+
class Solution {
+public:
+    int num = 0;
+    int result = 0;
+    void inorder(TreeNode* root,int k){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left, k);
+        num += 1;
+        if(num == k){
+            result = root->val;
+        }
+        inorder(root->right, k);
+    }
+   
+    int kthSmallest(TreeNode* root, int k) {
+        inorder(root, k);
+        return result;
+    }
+};
+

199. 二叉树的右视图

+

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

+
class Solution {
+public:
+    vector<int> rightSideView(TreeNode* root) {
+        vector<int> result;
+        if(root == NULL){
+            return result;
+        }
+        queue<TreeNode*> q;
+        q.push(root);
+        while(!q.empty()){
+            int s = q.size();
+            for(int i=0;i<s;i++){
+                TreeNode* t = q.front();
+                q.pop();
+                if(t->left != NULL){
+                    q.push(t->left);
+                }
+                if(t->right != NULL){
+                    q.push(t->right);
+                }
+                if(i == s-1){
+                    result.push_back(t->val);
+                }
+            }
+        }
+        return result;
+    }
+};
+

114. 二叉树展开为链表

+

给你二叉树的根结点 root ,请你将它展开为一个单链表:

+
    +
  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • +
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。
  • +
+
class Solution {
+public:
+    void flatten(TreeNode* root) {
+        if (root == NULL){
+            return;
+        }
+        flatten(root->left);
+        flatten(root->right);
+        if (root->left != NULL) {
+            auto pre = root->left;
+            while (pre->right != NULL){
+                pre = pre->right;
+            }
+            pre->right = root->right;
+            root->right = root->left;
+            root->left = NULL;
+        }
+        root = root->right;
+        return;
+    }
+};
+

105. 从前序与中序遍历序列构造二叉树

+

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的 先序遍历inorder 是同一棵树的 中序遍历 ,请构造二叉树并返回其根节点。

+
class Solution {
+public:
+    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
+        if(preorder.size() == 0){
+            return NULL;
+        }
+        int val = preorder[0];
+        TreeNode* root = new TreeNode(val);
+        int k;
+        vector<int> leftpreorder, leftinorder, rightpreorder, rightinorder;
+        for(int i=0;i<inorder.size();i++){
+            if(inorder[i] == val){
+                k = i;
+                break;
+            }
+        }
+        for(int i=1;i<preorder.size();i++){
+            if(i <= k){
+                leftpreorder.push_back(preorder[i]);
+            } else{
+                rightpreorder.push_back(preorder[i]);
+            }
+        }
+        for(int i=0;i<inorder.size();i++){
+            if(i < k){
+                leftinorder.push_back(inorder[i]);
+            } else if (i > k){
+                rightinorder.push_back(inorder[i]);
+            }
+        }
+        root->left = buildTree(leftpreorder, leftinorder);
+        root->right = buildTree(rightpreorder, rightinorder);
+        return root;
+    }
+};
+

437. 路径总和 III

+

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。

+

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

+
class Solution {
+public:
+    int DFS(TreeNode* root, long long targetSum){
+        if(root == NULL){
+            return 0;
+        }
+        int res = 0;
+        if(root->val == targetSum){
+            res += 1;
+        }
+        res += DFS(root->left, targetSum - root->val);
+        res += DFS(root->right, targetSum - root->val);
+        return res;
+    }
+    int pathSum(TreeNode* root, int targetSum) {
+        if(root == NULL){
+            return 0;
+        }
+        int nownum = DFS(root, (long long)targetSum);
+        nownum += pathSum(root->left, targetSum);
+        nownum += pathSum(root->right, targetSum);
+        return nownum;
+    }
+};
+

236. 二叉树的最近公共祖先

+

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

+

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大( 一个节点也可以是它自己的祖先 )。”

+
class Solution {
+public:
+    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
+        if(root == NULL){
+            return root;
+        }
+        if(root == p || root == q){
+            return root;
+        }
+        TreeNode* left = lowestCommonAncestor(root->left,p,q);
+        TreeNode* right = lowestCommonAncestor(root->right,p,q);
+        if(left != NULL && right != NULL){
+            return root;
+        }
+        if(left == NULL){
+            return right;
+        }
+        return left;
+    }
+};
+

124. 二叉树中的最大路径和

+

二叉树中的** 路径** 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径** 至少包含一个 **节点,且不一定经过根节点。

+

路径和 是路径中各节点值的总和。

+

给你一个二叉树的根节点 root ,返回其 最大路径和

+
class Solution {
+private:
+    int maxSum  = INT_MIN;
+public:
+  
+    int maxGain(TreeNode* node){
+        if(node == NULL){
+            return 0;
+        }
+        int leftGain  = max(maxGain(node->left), 0);
+        int rightresult = max(maxGain(node->right), 0);
+        int priceNewpath = node->val + leftGain  + rightresult;
+
+        // 更新答案
+        maxSum  = max(maxSum , priceNewpath);
+        return node->val + max(leftGain, rightresult);
+    }
+    int maxPathSum(TreeNode* root) {
+        maxGain(root);
+        return maxSum ;
+    }
+};
+

图论

+

200. 岛屿数量

+

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

+

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

+

此外,你可以假设该网格的四条边均被水包围。

+
class Solution {
+public:
+    void DFS(vector<vector<char>>& grid, vector<vector<bool> > & visited, int i, int j, int m, int n){
+        if(i < 0 || i >= m || j < 0 || j >= n || visited[i][j] == true || grid[i][j] == '0'){
+            return;
+        }
+        visited[i][j] = true;
+        DFS(grid, visited, i-1, j, m, n);
+        DFS(grid, visited, i+1, j, m, n);
+        DFS(grid, visited, i, j-1, m, n);
+        DFS(grid, visited, i, j+1, m, n);
+    }
+    int numIslands(vector<vector<char>>& grid) {
+        int sum = 0;
+        int m = grid.size();
+        int n = grid[0].size();
+        vector<vector<bool> > visited(m, vector<bool>(n, false));
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(visited[i][j] == false && grid[i][j] == '1'){
+                    DFS(grid, visited, i, j, m, n);
+                    sum += 1;
+                }
+            }
+        }
+        return sum;
+
+    }
+};
+

994. 腐烂的橘子

+

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

+
    +
  • 0 代表空单元格;
  • +
  • 1 代表新鲜橘子;
  • +
  • 2 代表腐烂的橘子。
  • +
+

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

+

返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1

+
class Solution {
+public:
+    int orangesRotting(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        vector<vector<bool>> visited(m, vector<bool>(n, false));
+        int minpath = 0;
+        queue<pair<int, int> > q;
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(grid[i][j] == 2){
+                    visited[i][j] = true;
+                    q.push({i,j});
+                }
+            }
+        }
+        while(!q.empty()){
+            minpath += 1;
+            int s = q.size();
+            for(int i=0;i<s;i++){
+                pair<int, int> p = q.front();
+                q.pop();
+                int pi = p.first;
+                int pj = p.second;
+                if(pi-1 >= 0 && visited[pi-1][pj] == false && grid[pi-1][pj] == 1){
+                    visited[pi-1][pj] = true;
+                    grid[pi-1][pj] = 2;
+                    q.push({pi-1,pj});
+                }
+                if(pj-1 >= 0 && visited[pi][pj-1] == false && grid[pi][pj-1] == 1){
+                    visited[pi][pj-1] = true;
+                    grid[pi][pj-1] = 2;
+                    q.push({pi,pj-1});
+                }
+                if(pi+1 < m && visited[pi+1][pj] == false && grid[pi+1][pj] == 1){
+                    visited[pi+1][pj] = true;
+                    grid[pi+1][pj] = 2;
+                    q.push({pi+1,pj});
+                }
+                if(pj+1 < n && visited[pi][pj+1] == false && grid[pi][pj+1] == 1){
+                    visited[pi][pj+1] = true;
+                    grid[pi][pj+1] = 2;
+                    q.push({pi,pj+1});
+                }
+            }
+        }
+
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(grid[i][j] == 1){
+                    return -1;
+                }
+            }
+        }
+        if(minpath == 0){
+            return 0;
+        }
+        return minpath-1;
+    }
+};
+

207. 课程表

+

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

+

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a<sub>i</sub>, b<sub>i</sub>] ,表示如果要学习课程 a<sub>i</sub>必须 先学习课程 b<sub>i</sub> ~ ~ 。

+
    +
  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1
  • +
+

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
+        vector<vector<int> > table(numCourses, vector<int>(0,0));
+        vector<int> judge(numCourses,0);
+        vector<bool> visit(numCourses,false);
+        for(int i=0;i<prerequisites.size();i++){
+            int pre = prerequisites[i][1];
+            int post = prerequisites[i][0];
+            table[pre].push_back(post);
+            judge[post] += 1;
+        }
+  
+        while(1){
+            bool modify = false;
+            for(int i=0;i<numCourses;i++){
+                if(visit[i] == false && judge[i] == 0){
+                    modify = true;
+                    visit[i] = true;
+                    for(int j=0;j<table[i].size();j++){
+                        judge[table[i][j]] -= 1;
+                    }
+                }
+            }
+            if(modify == false){
+                break;
+            }
+        }
+        for(int i=0;i<numCourses;i++){
+            if(judge[i] != 0){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

208. 实现 Trie (前缀树)

+

Trie (发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

+

请你实现 Trie 类:

+
    +
  • Trie() 初始化前缀树对象。
  • +
  • void insert(String word) 向前缀树中插入字符串 word
  • +
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • +
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false
  • +
+
class Trie {
+private:
+    bool isEnd;
+    Trie* next[26];
+public:
+    Trie() {
+        isEnd = false;
+        memset(next, 0, sizeof(next));
+    }
+  
+    void insert(string word) {
+        Trie* node = this;
+        for (char c : word) {
+            if (node->next[c-'a'] == NULL) {
+                node->next[c-'a'] = new Trie();
+            }
+            node = node->next[c-'a'];
+        }
+        node->isEnd = true;
+    }
+  
+    bool search(string word) {
+        Trie* node = this;
+        for (char c : word) {
+            node = node->next[c - 'a'];
+            if (node == NULL) {
+                return false;
+            }
+        }
+        return node->isEnd;
+    }
+  
+    bool startsWith(string prefix) {
+        Trie* node = this;
+        for (char c : prefix) {
+            node = node->next[c - 'a'];
+            if (node == NULL) {
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

回溯

+

46. 全排列

+

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

+
class Solution {
+private:
+    vector<vector<int> > result;
+public:
+    void backtracking(vector<int>& nums, vector<bool>& visited, vector<int>& temp){
+        if(temp.size() == nums.size()){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=0;i<nums.size();i++){
+            if(visited[i] == true){
+                continue;
+            }
+            visited[i] = true;
+            temp.push_back(nums[i]);
+            backtracking(nums, visited, temp);
+            temp.pop_back();
+            visited[i] = false;
+        }
+    }
+    vector<vector<int>> permute(vector<int>& nums) {
+        vector<int> temp;
+        vector<bool> visited(nums.size(), false);
+        backtracking(nums, visited, temp);
+        return result;
+    }
+};
+

78. 子集

+

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

+

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

+
class Solution {
+private:
+    vector<vector<int> > result;
+public:
+    void backtracking(vector<int>& nums, vector<int>& temp, int start){
+        result.push_back(temp);
+        if(start >= nums.size()){
+            return;
+        }
+        for(int i=start;i<nums.size();i++){
+            temp.push_back(nums[i]);
+            backtracking(nums, temp, i+1);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> subsets(vector<int>& nums) {
+        vector<int> temp;
+        backtracking(nums, temp, 0);
+        return result;
+    }
+};
+

17. 电话号码的字母组合

+

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

+

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

+
class Solution {
+private:
+    vector<string> result;
+    vector<char> temp;
+public:
+    void backtracking(string digits, map<char, vector<char> > &mp, int start){
+        if(start == digits.size()){
+            string t = "";
+            for(int i=0;i<temp.size();i++){
+                t += temp[i];
+            }
+            result.push_back(t);
+            return;
+        }
+        vector<char> now = mp[digits[start]];
+        for(int i=0;i<now.size();i++){
+            temp.push_back(now[i]);
+            backtracking(digits, mp, start+1);
+            temp.pop_back();
+        }
+    }
+    vector<string> letterCombinations(string digits) {
+        if(digits == ""){
+            return result;
+        }
+        map<char, vector<char> > mp;
+        mp['2'] = vector<char>{'a', 'b', 'c'};
+        mp['3'] = vector<char>{'d', 'e', 'f'};
+        mp['4'] = vector<char>{'g', 'h', 'i'};
+        mp['5'] = vector<char>{'j', 'k', 'l'};
+        mp['6'] = vector<char>{'m', 'n', 'o'};
+        mp['7'] = vector<char>{'p', 'q', 'r', 's'};
+        mp['8'] = vector<char>{'t', 'u', 'v'};
+        mp['9'] = vector<char>{'w', 'x', 'y', 'z'};
+        backtracking(digits, mp, 0);
+        return result;
+    }
+};
+

39. 组合总和

+

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

+

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

+

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

+
class Solution {
+private:
+    vector<vector<int>> result;
+    vector<int> temp;
+public:
+    void backtracking(vector<int>& candidates, int target, int nowsum, int start){
+        if(nowsum > target){
+            return;
+        }
+        if(nowsum == target){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=start;i<candidates.size();i++){
+            temp.push_back(candidates[i]);
+            backtracking(candidates, target, nowsum+candidates[i], i);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
+        backtracking(candidates, target, 0, 0);
+        return result;
+    }
+};
+

22. 括号生成

+

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的 **括号组合。

+
class Solution {
+private:
+    vector<string> result;
+    vector<char> temp;
+public:
+    void backtracking(int n, int left){
+        if(temp.size() == 2 * n && left == 0){
+            string res = "";
+            for(int i=0;i<temp.size();i++){
+                res += temp[i];
+            }
+            result.push_back(res);
+            return;
+        }
+        if(temp.size() == 2*n){
+            return;
+        }
+        if(left > n){
+            return;
+        }
+        if(left == 0){
+            temp.push_back('(');
+            backtracking(n, 1);
+            temp.pop_back();
+        } else{
+            temp.push_back('(');
+            backtracking(n, left+1);
+            temp.pop_back();
+            temp.push_back(')');
+            backtracking(n, left-1);
+            temp.pop_back();
+        }
+    }
+    vector<string> generateParenthesis(int n) {
+        backtracking(n,0);
+        return result;
+    }
+};
+

79. 单词搜索

+

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

+

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

+
class Solution {
+private:
+    bool sign = false;
+    string temp = "";
+public:
+    void backtracking(vector<vector<char>>& board, vector<vector<bool> > &visited, string word, int i, int j, int m, int n, int now){
+        if(temp == word){
+            sign = true;
+            return;
+        }
+        if(now >= word.size()){
+            return;
+        }
+        if(i < 0 || i >= m || j < 0 || j >= n){
+            return;
+        }
+        if(visited[i][j] == true){
+            return;
+        }
+        if(board[i][j] != word[now]){
+            return;
+        }
+        visited[i][j] = true;
+        string t = temp;
+        temp += board[i][j];
+        backtracking(board, visited, word, i+1, j, m, n, now+1);
+        backtracking(board, visited, word, i-1, j, m, n, now+1);
+        backtracking(board, visited, word, i, j+1, m, n, now+1);
+        backtracking(board, visited, word, i, j-1, m, n, now+1);
+        temp = t;
+        visited[i][j] = false;
+    }
+    bool exist(vector<vector<char>>& board, string word) {
+        int m = board.size();
+        int n = board[0].size();
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                vector<vector<bool> > visited(m, vector<bool>(n,false));
+                temp = "";
+                backtracking(board, visited, word, i, j, m, n, 0);
+                if(sign == true){
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+};
+

131. 分割回文串

+

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

+
class Solution {
+private:
+    vector<vector<string>> result;
+    vector<string> temp;
+public:
+    bool judge(string t){
+        int start = 0;
+        int end = t.size() - 1;
+        while(start < end){
+            if(t[start] != t[end]){
+                return false;
+            }
+            start += 1;
+            end -= 1;
+        }
+        return true;
+    }
+    void backtracking(string s, int start){
+        if(start == s.size()){
+            result.push_back(temp);
+            return;
+        }
+        for(int i=1; i<=s.size()-start;i++){
+            string t = s.substr(start,i);
+            if(judge(t)){
+                temp.push_back(t);
+                backtracking(s,start+i);
+                temp.pop_back();
+            }
+        }
+    }
+    vector<vector<string> > partition(string s) {
+        backtracking(s,0);
+        return result;
+    }
+};
+

51. N 皇后

+

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

+

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

+

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

+

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

+
class Solution {
+private:
+    vector<vector<string>> result;
+public:
+    bool judge(vector<vector<char> > &board, int i, int j, int n){
+        for(int p=0;p<i;p++){
+            if(board[p][j] == 'Q'){
+                return false;
+            }
+        }
+        int l = i-1;
+        int r = j-1;
+        while(l >= 0 && r >= 0){
+            if(board[l][r] == 'Q'){
+                return false;
+            }
+            l--;
+            r--;
+        }
+        l = i-1;
+        r = j+1;
+        while(l >= 0 && r < n){
+            if(board[l][r] == 'Q'){
+                return false;
+            }
+            l--;
+            r++;
+        }
+        return true;
+    }
+    void backtracking(vector<vector<char> > &board, int start, int n){
+        if(start == n){
+            vector<string> vt;
+            for(int i=0;i<n;i++){
+                string temp = "";
+                for(int j=0;j<n;j++){
+                    temp += board[i][j];
+                }
+                vt.push_back(temp);
+            }
+            result.push_back(vt);
+            return;
+        }
+        for(int i=0;i<n;i++){
+            board[start][i] = 'Q';
+            if(judge(board,start,i,n)){
+                backtracking(board,start+1,n);
+            }
+            board[start][i] = '.';
+        }
+    }
+    vector<vector<string> > solveNQueens(int n) {
+        vector<vector<char> > board(n, vector<char>(n, '.'));
+        backtracking(board,0,n);
+        return result; 
+    }
+};
+

二分查找

+

35. 搜索插入位置

+

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

+

请必须使用时间复杂度为 O(log n) 的算法。

+
class Solution {
+public:
+    int searchInsert(vector<int>& nums, int target) {
+        int left = 0;
+        int right = nums.size() - 1;
+        while(left <= right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] == target){
+                return mid;
+            } else if (nums[mid] < target){
+                left = mid + 1;
+            } else{
+                right = mid - 1;
+            }
+        }
+        return left;
+    }
+};
+

74. 搜索二维矩阵

+

给你一个满足下述两条属性的 m x n 整数矩阵:

+
    +
  • 每行中的整数从左到右按非严格递增顺序排列。
  • +
  • 每行的第一个整数大于前一行的最后一个整数。
  • +
+

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool searchMatrix(vector<vector<int>>& matrix, int target) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        int i = 0;
+        int j = n - 1;
+        while(i <= m-1 && j >= 0){
+            if(matrix[i][j] == target){
+                return true;
+            } else if(matrix[i][j] < target){
+                i += 1;
+            } else{
+                j -= 1;
+            }
+        }
+        return false;
+    }
+};
+

34. 在排序数组中查找元素的第一个和最后一个位置

+

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

+

如果数组中不存在目标值 target,返回 [-1, -1]

+

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

+
class Solution {
+public:
+    int lowerbound(vector<int>& nums, int target){
+        int left = 0;
+        int leftBorder = -2;
+        int right = nums.size() - 1;
+        while(left <= right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] >= target){
+                right = mid - 1;
+                leftBorder = right;
+            } else{
+                left = mid + 1;
+            }
+        }
+        return leftBorder;
+    }
+    int upperbound(vector<int>& nums, int target){
+        int left = 0;
+        int rightBorder = -2;
+        int right = nums.size() - 1;
+        while(left <= right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] > target){
+                right = mid - 1;
+            } else{
+                left = mid + 1;
+                rightBorder = left;
+            }
+        }
+        return rightBorder;
+    }
+    vector<int> searchRange(vector<int>& nums, int target) {
+        int leftBorder = lowerbound(nums, target);
+        int rightBorder = upperbound(nums,target);
+        if (leftBorder == -2 || rightBorder == -2){
+            return vector<int>{-1,-1};
+        }
+        if (rightBorder - leftBorder > 1){
+            return vector<int>{leftBorder + 1, rightBorder - 1};
+        }
+        return vector<int>{-1, -1};
+    }
+};
+

33. 搜索旋转排序数组

+

整数数组 nums 按升序排列,数组中的值 互不相同

+

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

+

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

+

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

+
class Solution {
+public:
+    int search(vector<int>& nums, int target) {
+        int left = 0;
+        int right = nums.size() - 1;
+        while(left <= right){
+            int mid = (right - left) / 2 + left;
+            if(nums[mid] == target){
+                return mid;
+            }
+            if(nums[0] <= nums[mid]){
+                if(nums[0] <= target && target <= nums[mid]){
+                    right = mid - 1;
+                } else{
+                    left = mid + 1;
+                }
+            } else{
+                if(nums[mid] <= target && target <= nums[nums.size()-1]){
+                    left = mid + 1;
+                } else{
+                    right = mid - 1;
+                }
+            }
+
+        }
+        return -1;
+    }
+};
+

153. 寻找旋转排序数组中的最小值

+

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

+
    +
  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • +
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
  • +
+

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

+

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

+

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

+
class Solution {
+public:
+    int findMin(vector<int>& nums) {
+        int left = 0;
+        int right = nums.size() - 1;
+        while(left < right){
+            int mid = (right - left) / 2 + left;
+            if(nums[right] > nums[mid]){
+                right = mid;
+            } else{
+                left = mid + 1;
+            }
+        }
+        return nums[left];
+    }
+};
+

4. 寻找两个正序数组的中位数

+

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

+

算法的时间复杂度应该为 O(log (m+n))

+
class Solution {
+public:
+    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
+        int m = nums1.size();
+        int n = nums2.size();
+        bool next;
+        int sign = (m + n) / 2; 
+        if((m + n) % 2 == 1){
+            next = false;
+        } else{
+            sign -= 1;
+            next = true;
+        }
+        int res1 = 0;
+        int res2 = 0;
+        int globalindex = 0;
+        int nums1index = 0;
+        int nums2index = 0;
+        while(globalindex <= sign){
+            if(nums1index < m && nums2index < n){
+                if(nums1[nums1index] < nums2[nums2index]){
+                    res1 = nums1[nums1index];
+                    nums1index += 1;
+                } else{
+                    res1 = nums2[nums2index];
+                    nums2index += 1;
+                }
+            } else if (nums1index < m){
+                res1 = nums1[nums1index];
+                nums1index += 1;
+            } else {
+                res1 = nums2[nums2index];
+                nums2index += 1;
+            }
+            globalindex += 1;
+        }
+        if(next == false){
+            return res1;
+        }
+        if(nums1index < m && nums2index < n){
+            if(nums1[nums1index] < nums2[nums2index]){
+                res2 = nums1[nums1index];
+            } else{
+                res2 = nums2[nums2index];
+            }
+        } else if (nums1index < m){
+            res2 = nums1[nums1index];
+        } else {
+            res2 = nums2[nums2index];
+        }
+        return ((double)res1 + (double)res2) / 2.0;
+    }
+};
+

+

20. 有效的括号

+

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

+

有效字符串需满足:

+
    +
  1. 左括号必须用相同类型的右括号闭合。
  2. +
  3. 左括号必须以正确的顺序闭合。
  4. +
  5. 每个右括号都有一个对应的相同类型的左括号。
  6. +
+
class Solution {
+public:
+    bool isValid(string s) {
+        stack<char> st;
+        for(int i=0;i<s.size();i++){
+            if(s[i] == '(' || s[i] == '[' || s[i] == '{'){
+                st.push(s[i]);
+            } else{
+                if(st.size() == 0){
+                    return false;
+                }
+                if(s[i] == ')'){
+                    if(st.top() != '('){
+                        return false;
+                    } else{
+                        st.pop();
+                    }
+                } else if (s[i] == ']'){
+                    if(st.top() != '['){
+                        return false;
+                    } else{
+                        st.pop();
+                    }
+                } else if (s[i] == '}'){
+                    if(st.top() != '{'){
+                        return false;
+                    } else{
+                        st.pop();
+                    }
+                }
+            }
+        }
+        return st.size() == 0;
+    }
+};
+

155. 最小栈

+

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

+

实现 MinStack 类:

+
    +
  • MinStack() 初始化堆栈对象。
  • +
  • void push(int val) 将元素val推入堆栈。
  • +
  • void pop() 删除堆栈顶部的元素。
  • +
  • int top() 获取堆栈顶部的元素。
  • +
  • int getMin() 获取堆栈中的最小元素。
  • +
+
class MinStack {
+private:
+    stack<int> st1;
+    stack<int> st2;
+public:
+    MinStack() {
+    }
+  
+    void push(int val) {
+        st1.push(val);
+        if(st2.empty() || val < st2.top()){
+            st2.push(val);
+        } else{
+            st2.push(st2.top());
+        }
+    }
+  
+    void pop() {
+        st1.pop();
+        st2.pop();
+    }   
+  
+    int top() {
+        return st1.top();
+    }
+  
+    int getMin() {
+        return st2.top();
+    }
+};
+

394. 字符串解码

+

给定一个经过编码的字符串,返回它解码后的字符串。

+

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

+

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

+

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a2[4] 的输入。

+
class Solution {
+public:
+    string decodeString(string s) {
+        stack<char> st;
+        for(int i=0;i<s.size();i++){
+            if(s[i] == ']'){
+                vector<char> tempchar;
+                vector<int> tempnum;
+                while(st.top() != '['){
+                    tempchar.push_back(st.top());
+                    st.pop();
+                }
+                st.pop();
+                while(!st.empty() && st.top() >= '0' && st.top() <= '9'){
+                    tempnum.push_back(st.top() - '0');
+                    st.pop();
+                }
+                int num = 0;
+                for(int j=tempnum.size()-1;j>=0;j--){
+                    num = num * 10 + tempnum[j];
+                }
+                for(int j=0;j<num;j++){
+                    for(int k=tempchar.size()-1;k>=0;k--){
+                        st.push(tempchar[k]);
+                    }
+                }
+            }else{
+                st.push(s[i]);
+            }
+        }
+        string res;
+        while(!st.empty()){
+            res += st.top();
+            st.pop();
+        }
+        reverse(res.begin(), res.end());
+        return res;
+    }
+};
+

739. 每日温度

+

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

+
class Solution {
+public:
+    vector<int> dailyTemperatures(vector<int>& temperatures) {
+        int n = temperatures.size();
+        vector<int> result(n,0);
+        stack<int> st;
+        for(int i=0;i<temperatures.size();i++){
+            while(!st.empty() && temperatures[st.top()] < temperatures[i]){
+                result[st.top()] = i - st.top();
+                st.pop();
+            }
+            st.push(i);
+        }
+        return result;
+    }
+};
+

84. 柱状图中最大的矩形

+

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

+

求在该柱状图中,能够勾勒出来的矩形的最大面积。

+
class Solution {
+public:
+    int largestRectangleArea(vector<int>& heights) {
+        int result = 0;
+        int n = heights.size();
+        if(n == 1){
+            return heights[0];
+        }
+        vector<int> minLeftIndex(n,0);
+        vector<int> minRightIndex(n,0);
+        minLeftIndex[0] = -1;
+        for(int i=1;i<n;i++){
+            int t = i - 1;
+            while (t >= 0 && heights[t] >= heights[i]){
+                t = minLeftIndex[t];
+            }
+            minLeftIndex[i] = t;
+        }
+        minRightIndex[n-1] = n;
+        for(int i=n-2;i>=0;i--){
+            int t = i + 1;
+            while (t < n && heights[t] >= heights[i]){
+                t = minRightIndex[t];
+            }
+            minRightIndex[i] = t;
+        }
+        for(int i=0;i<n;i++){
+            result = max(result, heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1));
+        }
+        return result;
+    }
+};
+

+

215.数组中的第K个最大元素

+

给定整数数组 nums 和整数 k,请返回数组中第 <strong>k</strong> 个最大的元素。

+

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

+

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

+
class Solution {
+public:
+    void quicksort(vector<int>& nums, int i, int j, int k){
+        if(i >= j){
+            return;
+        }
+        int start = i;
+        int end = j;
+        int x = nums[start];
+        while(start < end){
+            while(start < end && nums[end] < x){
+                end--;
+            }
+            nums[start] = nums[end];
+            while(start < end && nums[start] >= x){
+                start++;
+            }
+            nums[end] = nums[start];
+        }
+        nums[start] = x;
+        if(k-1 == start){
+            return;
+        } else if(k-1 < start){
+            quicksort(nums, i, start-1,k);
+        } else{
+            quicksort(nums,start+1,j,k);
+        }
+    }
+    int findKthLargest(vector<int>& nums, int k) {
+        quicksort(nums,0,nums.size()-1,k);
+        return nums[k-1];
+    }
+};
+

347. 前 K 个高频元素

+

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

+
class Solution {
+public:
+    static bool cmp(int a, int b){
+        return a > b;
+    }
+    vector<int> topKFrequent(vector<int>& nums, int k) {
+        unordered_map<int, int> mp;
+        for(int i=0;i<nums.size();i++){
+            mp[nums[i]] += 1;
+        }
+        vector<int> temp;
+        for(auto it = mp.begin(); it != mp.end();it++){
+            temp.push_back(it->second);
+        }
+        sort(temp.begin(), temp.end(), cmp);
+        int sign = temp[k-1];
+        vector<int> result;
+        for(auto it = mp.begin(); it != mp.end();it++){
+            if(it->second >= sign){
+                result.push_back(it->first);
+            }
+        }
+        return result;
+    }
+};
+

295. 数据流的中位数

+

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

+
    +
  • 例如 arr = [2,3,4] 的中位数是 3
  • +
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5
  • +
+

实现 MedianFinder 类:

+
    +
  • MedianFinder() 初始化 MedianFinder 对象。
  • +
  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
  • +
  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10<sup>-5</sup> 以内的答案将被接受。
  • +
+
class MedianFinder {
+public:
+    priority_queue<int, vector<int>, greater<int>> A; // 小顶堆,保存较大的一半
+    priority_queue<int, vector<int>, less<int>> B; // 大顶堆,保存较小的一半
+
+    MedianFinder() {
+
+    }
+    void addNum(int num) {
+        if (A.size() != B.size()) {
+            A.push(num);
+            B.push(A.top());
+            A.pop();
+        } else {
+            B.push(num);
+            A.push(B.top());
+            B.pop();
+        }
+
+    }
+  
+    double findMedian() {
+        return A.size() != B.size() ? A.top() : (A.top() + B.top()) / 2.0;
+    }
+};
+

贪心算法

+

121. 买卖股票的最佳时机

+

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

+

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

+

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

+
class Solution {
+public:
+    int maxProfit(vector<int>& prices) {
+        int minprice = INT_MAX, maxprofit = 0;
+        for(int i=0;i<prices.size();i++){
+            maxprofit = max(maxprofit, prices[i] - minprice);
+            minprice = min(prices[i], minprice);
+        }
+        return maxprofit;
+    }
+};
+

55. 跳跃游戏

+

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

+

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

+
class Solution {
+public:
+    bool canJump(vector<int>& nums) {
+        int n = nums.size();
+        int result = 0;
+        for(int i=0;i<nums.size();i++){
+            if (i <= result){
+                result = max(result, i + nums[i]);
+                if(result >= n - 1){
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+};
+

45. 跳跃游戏 II

+

给定一个长度为 n0 索引整数数组 nums。初始位置为 nums[0]

+

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

+
    +
  • 0 <= j <= nums[i]
  • +
  • i + j < n
  • +
+

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

+
class Solution {
+public:
+    int jump(vector<int>& nums) {
+        int n = nums.size();
+        int result = n+1;
+        vector<int> dp(n+1,n+1);
+        dp[0] = 0;
+        for(int i=1;i<n;i++){
+            for(int j=0;j<i;j++){
+                if(j + nums[j] >= i){
+                    dp[i] = min(dp[i], dp[j] + 1);
+                }
+            }
+        }
+        return dp[n-1];
+    }
+};
+

763. 划分字母区间

+

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

+

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

+

返回一个表示每个字符串片段的长度的列表。

+
class Solution {
+public:
+    vector<int> partitionLabels(string s) {
+        vector<int> result;
+        int n = s.size();
+        vector<int> alphabet(26,0);
+        for(int i=0;i<s.size();i++){
+            alphabet[s[i] - 'a'] = i;
+        }
+        int start = 0;
+        int end = 0;
+        for(int i=0;i<s.size();i++){
+            end = max(end, alphabet[s[i] - 'a']);
+            if(i == end){
+                result.push_back(end - start + 1);
+                start = end + 1;
+            }
+        }
+        return result;
+    }
+};
+

动态规划

+

70. 爬楼梯

+

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

+

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

+
class Solution {
+public:
+    int climbStairs(int n) {
+        if(n == 1 || n == 2){
+            return n;
+        }
+        vector<int> dp(n+1,0);
+        dp[1] = 1;
+        dp[2] = 2;
+        for(int i=3;i<=n;i++){
+            dp[i] = dp[i-1] + dp[i-2];
+        }
+        return dp[n];
+    }
+};
+

118. 杨辉三角

+

给定一个非负整数 numRows 生成「杨辉三角」的前 *numRows *行。

+
class Solution {
+public:
+    vector<vector<int>> generate(int numRows) {
+        vector<vector<int>> result;
+        for(int i=1;i<=numRows;i++){
+            vector<int> temp;
+            for(int j=0;j<i;j++){
+                if(j == 0 || j == i-1){
+                    temp.push_back(1);
+                } else{
+                    vector<int> last = result[result.size()-1];
+                    temp.push_back(last[j-1] + last[j]);
+                }
+            }
+            result.push_back(temp);
+        }
+        return result;
+    }
+};
+

198. 打家劫舍

+

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

+

给定一个代表每个房屋存放金额的非负整数数组,计算你 ** 不触动警报装置的情况下 ** ,一夜之内能够偷窃到的最高金额。

+
class Solution {
+public:
+    int rob(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n+1);
+        if(n == 1){
+            return nums[0];
+        }
+        if(n == 2){
+            return max(nums[0], nums[1]);
+        }
+        dp[1] = nums[0];
+        dp[2] = max(nums[0], nums[1]);
+        for(int i=3;i<=n;i++){
+            dp[i] = max(dp[i-1], dp[i-2] + nums[i-1]);
+        }
+        return dp[n];
+    }
+};
+

279. 完全平方数

+

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

+

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

+
class Solution {
+public:
+    int numSquares(int n) {
+        vector<int> dp(n+1,n+1);
+        for(int i=1;i<=n;i++){
+            if(i*i < n){
+                dp[i*i] = 1;
+            } else if(i*i == n){
+                return 1;
+            } else{
+                break;
+            }
+        }
+        for(int i=1;i<=n;i++){
+            for(int j=i;j>=1;j--){
+                dp[i] = min(dp[i], dp[i-j]+ dp[j]);
+            }
+        }
+        return dp[n];
+    }
+};
+

322. 零钱兑换

+

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

+

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

+

你可以认为每种硬币的数量是无限的。

+
class Solution {
+public:
+    int coinChange(vector<int>& coins, int amount) {
+        vector<int> dp(amount+1,amount+1);
+        dp[0] = 0;
+        for(int i=0;i<coins.size();i++){
+            for(int j=0;j<=amount;j++){
+                if(j >= coins[i]){
+                    dp[j] = min(dp[j], dp[j-coins[i]] + 1);
+                }
+    
+            }
+        }
+        return dp[amount] == amount+1 ? -1:dp[amount];
+    }
+};
+

139. 单词拆分

+

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

+

注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

+
class Solution {
+public:
+    bool wordBreak(string s, vector<string>& wordDict) {
+        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
+        vector<bool> dp(s.size()+1,false);
+        dp[0] = true;
+        for(int i=0;i<=s.size();i++){
+            for(int j=0;j<i;j++){
+                string word = s.substr(j,i-j);
+                if(wordSet.find(word) != wordSet.end() && dp[j] == true){
+                    dp[i] = true;
+                }
+            }
+        }
+        return dp[s.size()] == true;
+    }
+};
+

300. 最长递增子序列

+

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

+

**子序列 **是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

+
class Solution {
+public:
+    int lengthOfLIS(vector<int>& nums) {
+        int n = nums.size();
+        vector<int> dp(n,1);
+        int result = 0;
+        dp[0] = 1;
+        for(int i=0;i<n;i++){
+            for(int j=0;j<i;j++){
+                if(nums[j] < nums[i]){
+                    dp[i] = max(dp[i], dp[j] + 1);
+                }
+            }
+            result = max(result, dp[i]);
+        }
+        return result;
+    }
+};
+

152. 乘积最大子数组

+

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

+

测试用例的答案是一个 32-位 整数。

+
class Solution {
+public:
+    int maxProduct(vector<int>& nums) {
+        double maxF = nums[0], minF = nums[0], ans = nums[0];
+        for (int i = 1; i < nums.size(); ++i) {
+            double mx = maxF, mn = minF;
+            maxF = max(mx * nums[i], max(static_cast<double>(nums[i]), mn * nums[i]));
+            minF = min(mn * nums[i], min(static_cast<double>(nums[i]), mx * nums[i]));
+            ans = max(maxF, ans);
+        }
+        return static_cast<int>(ans);
+    }
+};
+

416. 分割等和子集

+

给你一个 **只包含正整数 **的 **非空 **数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

+
class Solution {
+public:
+    bool canPartition(vector<int>& nums) {
+        int sum = 0;
+        for(int num : nums){
+            sum += num;
+        }
+        if(sum % 2 == 1){
+            return false;
+        }
+        sum /= 2;
+        vector<bool> dp(sum+1,false);
+        dp[0] = true;
+        for(int i=0;i<nums.size();i++){
+            for(int j=sum;j>=nums[i];j--){
+                dp[j] = dp[j] || dp[j-nums[i]];
+            }
+        }
+        return dp[sum];
+    }
+};
+

32. 最长有效括号(*)

+

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

+
class Solution {
+public:
+    int longestValidParentheses(string s) {
+        stack<int> st;
+        int ans = 0;
+        int start = 0;
+        for(int i=0;i<s.size();i++){
+            if(s[i] == '('){
+                st.push(i);
+            } else{
+                if(!st.empty()){
+                    st.pop();
+                    if (st.empty()) {
+                        ans = max(ans, i - start + 1);
+                    }
+                    else {
+                        ans = max(ans, i - st.top());
+                    }
+
+                } else{
+                    start = i + 1;
+                }
+            }
+        }
+        return ans;
+    }
+};
+

多维动态规划

+

62. 不同路径

+

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

+

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

+

问总共有多少条不同的路径?

+
class Solution {
+public:
+    int uniquePaths(int m, int n) {
+        vector<vector<int> > dp(m+1, vector<int>(n+1,0));
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(i == 1 && j == 1){
+                    dp[i][j] = 1;
+                } else{
+                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
+                }
+    
+            }
+        }
+        return dp[m][n];
+    }
+};
+

64. 最小路径和

+

给定一个包含非负整数的 <em>m</em> x <em>n</em> 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

+

说明: 每次只能向下或者向右移动一步。

+
class Solution {
+public:
+    int minPathSum(vector<vector<int>>& grid) {
+        int m = grid.size();
+        int n = grid[0].size();
+        vector<vector<int> > dp(m, vector<int>(n,0));
+        for(int i=0;i<m;i++){
+            for(int j=0;j<n;j++){
+                if(i == 0 && j == 0){
+                    dp[i][j] = grid[i][j];
+                } else if (i == 0){
+                    dp[i][j] = grid[i][j] + dp[i][j-1];
+                } else if (j == 0){
+                    dp[i][j] = grid[i][j] + dp[i-1][j];
+                } else{
+                    dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
+                }
+            }
+        }
+        return dp[m-1][n-1];
+    }
+};
+

5. 最长回文子串

+

给你一个字符串 s,找到 s 中最长的 回文 子串。

+
class Solution {
+public:
+    string longestPalindrome(string s) {
+        string result = "";
+        int longresult = 1;
+        result += s[0];
+        int l = s.size();
+        for(int i=0;i<l-1;i++){
+            int start = i;
+            int end = i + 1;
+            while(start >= 0 && end < l && s[start] == s[end]){
+                start--;
+                end++;
+            }
+            start++;
+            end--;
+            if(end-start+1 > longresult){
+                longresult = end-start+1;
+                result = s.substr(start,longresult);
+            }
+        }
+        for(int i=1;i<l-1;i++){
+            int start = i - 1;
+            int end = i + 1;
+            while(start >= 0 && end < l && s[start] == s[end]){
+                start--;
+                end++;
+            }
+            start++;
+            end--;
+            if(end-start+1 > longresult){
+                longresult = end-start+1;
+                result = s.substr(start,longresult);
+            }
+        }
+        return result;
+    }
+};
+

1143. 最长公共子序列

+

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

+

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

+
    +
  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
  • +
+

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

+
class Solution {
+public:
+    int longestCommonSubsequence(string text1, string text2) {
+        int m = text1.size();
+        int n = text2.size();
+        vector<vector<int> > dp(m+1, vector<int>(n+1,0));
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(text1[i-1] == text2[j-1]){
+                    dp[i][j] = dp[i-1][j-1] + 1;
+                } else{
+                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
+                }
+            }
+        }
+        return dp[m][n];
+    }
+};
+

72. 编辑距离

+

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

+

你可以对一个单词进行如下三种操作:

+
    +
  • 插入一个字符
  • +
  • 删除一个字符
  • +
  • 替换一个字符
  • +
+
class Solution {
+public:
+    int minDistance(string word1, string word2) {
+        int s1 = word1.size();
+        int s2 = word2.size();
+        vector<vector<int> > dp(s1+1, vector<int>(s2+1,0));
+        for(int i=0;i<=s1;i++){
+            dp[i][0] = i;
+        }
+        for(int j=0;j<=s2;j++){
+            dp[0][j] = j;
+        }
+        for(int i=1;i<=s1;i++){
+            for(int j=1;j<=s2;j++){
+                if(word1[i-1] == word2[j-1]){
+                    dp[i][j] = dp[i-1][j-1];
+                } else{
+                    dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
+                }
+            }
+        }
+        return dp[s1][s2];
+    }
+};
+

技巧(*)

+

136. 只出现一次的数字

+

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

+

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

+
class Solution {
+public:
+    int singleNumber(vector<int>& nums) {
+        int ret = 0;
+        for (auto e: nums) ret ^= e;
+        return ret;
+    }
+};
+

169. 多数元素

+

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

+

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

+
class Solution {
+public:
+    int majorityElement(vector<int>& nums) {
+        int x = 0, votes = 0;
+        for (int num : nums){
+            if (votes == 0) x = num;
+            votes += num == x ? 1 : -1;
+        }
+        return x;
+    }
+};
+

75. 颜色分类

+

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

+

我们使用整数 012 分别表示红色、白色和蓝色。

+

必须在不使用库内置的 sort 函数的情况下解决这个问题。

+
class Solution {
+public:
+    void sortColors(vector<int>& nums) {
+        int left = 0;
+        for(int right = 0;right < nums.size();right++){
+            if(nums[right] == 0){
+                swap(nums[left], nums[right]);
+                left += 1;
+            }
+        }
+        left = nums.size() - 1;
+        for(int right = nums.size()-1;right >= 0; right--){
+            if(nums[right] == 2){
+                swap(nums[left], nums[right]);
+                left -= 1;
+            }
+        }
+    }
+};
+

31. 下一个排列

+

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

+
    +
  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]
  • +
+

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

+
    +
  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • +
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • +
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
  • +
+

给你一个整数数组 nums ,找出 nums 的下一个排列。

+

必须** 原地 **修改,只允许使用额外常数空间。

+
class Solution {
+public:
+    void nextPermutation(vector<int>& nums) {
+        int n = nums.size();
+        int sign = -1;
+        for(int i=n-1;i>0;i--){
+            if(nums[i-1] < nums[i]){
+                sign = i-1;
+                break;
+            }
+        }
+        if(sign == -1){
+            reverse(nums.begin(),nums.end());
+            return;
+        }
+        int sign2 = 0;
+        for(int i=n-1;i>0;i--){
+            if(nums[i] > nums[sign]){
+                sign2 = i;
+                break;
+            }
+        }
+        swap(nums[sign], nums[sign2]);
+        reverse(nums.begin()+sign+1,nums.end());
+        return;
+    }
+};
+

287. 寻找重复数

+

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。

+

假设 nums 只有 一个重复的整数 ,返回 这个重复的数

+

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

+
class Solution {
+public:
+    int findDuplicate(vector<int>& nums) {
+        int fast = 0, slow = 0;
+        while(true){
+            fast = nums[nums[fast]];
+            slow = nums[slow];
+            if(fast == slow)
+                break;
+        }
+        int finder = 0;
+        while(true){
+            finder = nums[finder];
+            slow = nums[slow];
+            if(slow == finder)
+                break;  
+        }
+        return slow;
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-Hot 100
+
https://zhangzhao219.github.io/2024/06/01/Leetcode/Leetcode-hot200/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年6月1日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/06/01/Leetcode/Leetcode-sq/index.html b/2024/06/01/Leetcode/Leetcode-sq/index.html new file mode 100644 index 000000000..f85d241a2 --- /dev/null +++ b/2024/06/01/Leetcode/Leetcode-sq/index.html @@ -0,0 +1,798 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-栈与队列 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-栈与队列

+ + +
+ +

Leetcode-栈与队列

+ +

栈与队列

+

295. 数据流的中位数

+

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

+
    +
  • 例如 arr = [2,3,4] 的中位数是 3
  • +
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5
  • +
+

实现 MedianFinder 类:

+
    +
  • MedianFinder() 初始化 MedianFinder 对象。
  • +
  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
  • +
  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10<sup>-5</sup> 以内的答案将被接受。
  • +
+
class MedianFinder {
+public:
+    priority_queue<int, vector<int>, less<int>> queMin;
+    priority_queue<int, vector<int>, greater<int>> queMax;
+
+    MedianFinder() {
+      
+    }
+  
+    void addNum(int num) {
+        if (queMin.empty() || num <= queMin.top()) {
+            queMin.push(num);
+            if (queMax.size() + 1 < queMin.size()) {
+                queMax.push(queMin.top());
+                queMin.pop();
+            }
+        } else {
+            queMax.push(num);
+            if (queMax.size() > queMin.size()) {
+                queMin.push(queMax.top());
+                queMax.pop();
+            }
+        }
+    }
+  
+    double findMedian() {
+        if (queMin.size() > queMax.size()) {
+            return queMin.top();
+        }
+        return (queMin.top() + queMax.top()) / 2.0; 
+    }
+};
+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-栈与队列
+
https://zhangzhao219.github.io/2024/06/01/Leetcode/Leetcode-sq/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年6月1日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/07/12/Leetcode/Leetcode-interview/index.html b/2024/07/12/Leetcode/Leetcode-interview/index.html new file mode 100644 index 000000000..6ef2e7198 --- /dev/null +++ b/2024/07/12/Leetcode/Leetcode-interview/index.html @@ -0,0 +1,2631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leetcode-Interview - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

Leetcode-Interview

+ + +
+ +

Leetcode-Interview

+ +

面试题 01

+

面试题 01.01. 判定字符是否唯一

+

实现一个算法,确定一个字符串 s 的所有字符是否全都不同。

+
class Solution {
+public:
+    bool isUnique(string astr) {
+        vector<int> visited(26, false);
+        for(int i=0;i<astr.size();i++){
+            int t = astr[i] - 'a';
+            if(visited[t] == true){
+                return false;
+            }
+            visited[t] = true;
+        }
+        return true;
+    }
+};
+

面试题 01.02. 判定是否互为字符重排

+

给定两个由小写字母组成的字符串 s1s2,请编写一个程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。

+
class Solution {
+public:
+    bool CheckPermutation(string s1, string s2) {
+        vector<int> visited(26, 0);
+        for(int i=0;i<s1.size();i++){
+            visited[s1[i] - 'a'] += 1;
+        }
+        for(int i=0;i<s2.size();i++){
+            visited[s2[i] - 'a'] -= 1;
+        }
+        for(int i=0;i<26;i++){
+            if(visited[i] != 0){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

面试题 01.03. URL化

+

URL化。编写一种方法,将字符串中的空格全部替换为 %20。假定该字符串尾部有足够的空间存放新增字符,并且知道字符串的“真实”长度。(注:用 Java实现的话,请使用字符数组实现,以便直接在数组上操作。)

+
class Solution {
+public:
+    string replaceSpaces(string S, int length) {
+        string res = "";
+        for(int i=0;i<length;i++){
+            if(S[i] == ' '){
+                res += "%20";
+            } else{
+                res += S[i];
+            }
+        }
+        return res;
+    }
+};
+

面试题 01.04. 回文排列

+

给定一个字符串,编写一个函数判定其是否为某个回文串的排列之一。

+

回文串是指正反两个方向都一样的单词或短语。排列是指字母的重新排列。

+

回文串不一定是字典当中的单词。

+
class Solution {
+public:
+    bool canPermutePalindrome(string s) {
+        set<char> st;
+        for(int i=0;i<s.size();i++){
+            if(st.count(s[i]) == 0){
+                st.insert(s[i]);
+            } else{
+                st.erase(s[i]);
+            }
+        }
+        return st.size() == 1 || st.size() == 0;
+    }
+};
+

面试题 01.05. 一次编辑

+

字符串有三种编辑操作:插入一个英文字符、删除一个英文字符或者替换一个英文字符。 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。

+
class Solution {
+public:
+    bool oneEditAway(string first, string second) {
+        int m = first.size();
+        int n = second.size();
+        vector<vector<int> > dp(m+1, vector<int>(n+1, 0));
+        for(int i=0;i<=m;i++){
+            dp[i][0] = i;
+        }
+        for(int j=0;j<=n;j++){
+            dp[0][j] = j;
+        }
+        for(int i=1;i<=m;i++){
+            for(int j=1;j<=n;j++){
+                if(first[i-1] == second[j-1]){
+                    dp[i][j] = dp[i-1][j-1];
+                }else{
+                    dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
+                }
+            }
+        }
+        return dp[m][n] == 0 || dp[m][n] == 1;
+    }
+};
+

面试题 01.06. 字符串压缩

+

字符串压缩。利用字符重复出现的次数,编写一种方法,实现基本的字符串压缩功能。比如,字符串 aabcccccaaa会变为 a2b1c5a3。若“压缩”后的字符串没有变短,则返回原先的字符串。你可以假设字符串中只包含大小写英文字母(a至z)。

+
class Solution {
+public:
+    string compressString(string S) {
+        if(S.size() == 0){
+            return S;
+        }
+        string res = "";
+        int count = 1;
+        char temp = S[0];
+        for(int i=1;i<S.size();i++){
+            if(S[i] == temp){
+                count += 1;
+            } else{
+                res += temp;
+                string tempcount = "";
+                while(count != 0){
+                    tempcount += count%10 + '0';
+                    count /= 10;
+                }
+                reverse(tempcount.begin(), tempcount.end());
+                res += tempcount;
+            
+                count = 1;
+                temp = S[i];
+            }
+        }
+        res += temp;
+        string tempcount2 = "";
+        while(count != 0){
+            tempcount2 += count%10 + '0';
+            count /= 10;
+        }
+        reverse(tempcount2.begin(), tempcount2.end());
+        res += tempcount2;
+        if(res.size() < S.size()){
+            return res;
+        }
+        return S;
+    }
+};
+

面试题 01.07. 旋转矩阵

+

给你一幅由 N × N 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。

+

不占用额外内存空间能否做到?

+
class Solution {
+public:
+    void rotate(vector<vector<int>>& matrix) {
+        int n = matrix.size();
+        for(int i=0;i<(n+1)/2;i++){
+            for(int j=0;j<n/2;j++){
+                int temp = matrix[i][j];
+                matrix[i][j] = matrix[n-j-1][i];
+                matrix[n-j-1][i] = matrix[n-i-1][n-j-1];
+                matrix[n-i-1][n-j-1] = matrix[j][n-i-1];
+                matrix[j][n-i-1] = temp;
+            }
+        }
+    }
+};
+

面试题 01.08. 零矩阵

+

编写一种算法,若M × N矩阵中某个元素为0,则将其所在的行与列清零。

+
class Solution {
+public:
+    void setZeroes(vector<vector<int>>& matrix) {
+        int m = matrix.size();
+        int n = matrix[0].size();
+        bool rowflag = false;
+        bool colflag = false;
+        for(int i=0;i<m;i++){
+            if(matrix[i][0] == 0){
+                colflag = true;
+                break;
+            }
+        }
+        for(int j=0;j<n;j++){
+            if(matrix[0][j] == 0){
+                rowflag = true;
+                break;
+            }
+        }
+        for(int i=1;i<m;i++){
+            for(int j=1;j<n;j++){
+                if(matrix[i][j] == 0){
+                    matrix[0][j] = 0;
+                    matrix[i][0] = 0;
+                }
+            }
+        }
+        for(int i=1;i<m;i++){
+            for(int j=1;j<n;j++){
+                if(matrix[0][j] == 0 || matrix[i][0] == 0){
+                    matrix[i][j] = 0;
+                }
+            }
+        }
+        if(rowflag){
+            for(int j=0;j<n;j++){
+                matrix[0][j] = 0;
+            }
+        }
+        if(colflag){
+            for(int i=0;i<m;i++){
+                matrix[i][0] = 0;
+            }
+        }
+    }
+};
+

面试题 01.09. 字符串轮转

+

字符串轮转。给定两个字符串 s1s2,请编写代码检查 s2是否为 s1旋转而成(比如,waterbottleerbottlewat旋转后的字符串)。

+

示例1:

+
 输入:s1 = "waterbottle", s2 = "erbottlewat"
+ 输出:True
+
+

示例2:

+
 输入:s1 = "aa", s2 = "aba"
+ 输出:False
+
+
class Solution {
+public:
+    bool isFlipedString(string s1, string s2) {
+        if(s1.size() != s2.size()){
+            return false;
+        }
+        if(s1 == ""){
+            return true;
+        }
+        int l = s1.size();
+        s1 = s1 + s1;
+        for(int i=0;i<s1.size();i++){
+            if(s1.substr(i,l) == s2){
+                return true;
+            }
+        }
+        return false;
+    }
+};
+

面试题 02

+

面试题 02.01. 移除重复节点

+

编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。

+
class Solution {
+public:
+    ListNode* removeDuplicateNodes(ListNode* head) {
+        ListNode* t = head;
+        ListNode* p = head;
+        if(p == NULL){
+            return p;
+        }
+        set<int> st;
+        st.insert(p->val);
+        while(p->next != NULL){
+            ListNode* q = p->next;
+            if(st.count(q->val) != 0){
+                p->next = p->next->next;
+            } else{
+                p = p->next;
+                st.insert(q->val);
+            }
+        }
+        return t;
+    }
+};
+

面试题 02.02. 返回倒数第 k 个节点

+

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

+
class Solution {
+public:
+    int kthToLast(ListNode* head, int k) {
+        ListNode* slow = head;
+        ListNode* fast = head;
+        for(int i=0;i<k;i++){
+            fast = fast->next;
+        }
+        while(fast != NULL){
+            slow = slow->next;
+            fast = fast->next;
+        }
+        return slow->val;
+    }
+};
+

面试题 02.03. 删除中间节点

+

若链表中的某个节点,既不是链表头节点,也不是链表尾节点,则称其为该链表的「中间节点」。

+

假定已知链表的某一个中间节点,请实现一种算法,将该节点从链表中删除。

+

例如,传入节点 c(位于单向链表 a->b->c->d->e->f 中),将其删除后,剩余链表为 a->b->d->e->f

+

示例:

+
输入:节点 5 (位于单向链表 4->5->1->9 中)
+输出:不返回任何数据,从链表中删除传入的节点 5,使链表变为 4->1->9
+
+
class Solution {
+public:
+    void deleteNode(ListNode* node) {
+        *node = *node->next;
+    }
+};
+

面试题 02.04. 分割链表

+

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

+

你不需要 保留 每个分区中各节点的初始相对位置。

+
class Solution {
+public:
+    ListNode* partition(ListNode* head, int x) {
+        ListNode* p = head;
+        ListNode* q = head;
+        while(q != NULL){
+            if(q->val < x){
+                swap(p->val,q->val);
+                p = p->next;
+            }
+            q = q->next;
+        }
+        return head;
+    }
+};
+

面试题 02.05. 链表求和

+

给定两个用链表表示的整数,每个节点包含一个数位。

+

这些数位是反向存放的,也就是个位排在链表首部。

+

编写函数对这两个整数求和,并用链表形式返回结果。

+
class Solution {
+public:
+    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
+        ListNode* dummy = new ListNode(-1);
+        ListNode* l3 = dummy;
+        int cnt = 0;
+        int temp = 0;
+        while(l1 != NULL || l2 != NULL){
+            if(l1 == NULL){
+                temp = l2->val + cnt;
+                l2 = l2->next;
+            } else if(l2 == NULL){
+                temp = l1->val + cnt;
+                l1 = l1->next;
+            } else{
+                temp = l1->val + l2->val + cnt;
+                l1 = l1->next;
+                l2 = l2->next;
+            }
+            if(temp >= 10){
+                temp = temp - 10;
+                cnt = 1;
+            } else{
+                cnt = 0;
+            }
+            l3->next = new ListNode(temp);
+            l3 = l3->next;
+        }
+        if(cnt == 1){
+            l3->next = new ListNode(1);
+        }
+        return dummy->next;
+    }
+};
+

面试题 02.06. 回文链表

+

编写一个函数,检查输入的链表是否是回文的。

+
class Solution {
+public:
+    bool isPalindrome(ListNode* head) {
+        vector<int> result;
+        while(head != NULL){
+            result.push_back(head->val);
+            head = head->next;
+        }
+        int left = 0;
+        int right = result.size()-1;
+        while(left < right){
+            if(result[left] != result[right]){
+                return false;
+            }
+            left++;
+            right--;
+        }
+        return true;
+    }
+};
+

面试题 02.07. 链表相交

+

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

+

图示两个链表在节点 c1 开始相交**:**

+

+

题目数据 保证 整个链式结构中不存在环。

+

注意 ,函数返回结果后,链表必须 保持其原始结构

+
class Solution {
+public:
+    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
+        ListNode* p = headA;
+        ListNode* q = headB;
+        while(p != q){
+            if(p == NULL){
+                p = headB;
+            } else{
+                p = p->next;
+            }
+            if(q == NULL){
+                q = headA;
+            } else{
+                q = q->next;
+            }
+        }
+        return p;
+    }
+};
+

面试题 02.08. 环路检测

+

给定一个链表,如果它是有环链表,实现一个算法返回环路的 开头节点。若环不存在,请返回 null

+

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos-1,则在该链表中没有环。 注意:pos 不作为参数进行传递 ,仅仅是为了标识链表的实际情况。

+
class Solution {
+public:
+    ListNode *detectCycle(ListNode *head) {
+        ListNode* slow = head;
+        ListNode* fast = head;
+        while(fast != NULL && fast->next != NULL){
+            slow = slow->next;
+            fast = fast->next->next;
+            if(slow == fast){
+                break;
+            }
+        }
+        if(fast == NULL || fast->next == NULL){
+            return NULL;
+        }
+        fast = head;
+        while(slow != fast){
+            slow = slow->next;
+            fast = fast->next;
+        }
+        return slow;
+    }
+};
+

面试题 03

+

面试题 03.01. 三合一

+

三合一。描述如何只用一个数组来实现三个栈。

+

你应该实现 push(stackNum, value)pop(stackNum)isEmpty(stackNum)peek(stackNum)方法。stackNum表示栈下标,value表示压入的值。

+

构造函数会传入一个 stackSize参数,代表每个栈的大小。

+
class TripleInOne {
+private:
+    vector<int> stackown;
+    vector<int> nowtop;
+public:
+    TripleInOne(int stackSize) {
+        for(int i=0;i<stackSize*3;i++){
+            stackown.push_back(0);
+        }
+        nowtop.push_back(0);
+        nowtop.push_back(1);
+        nowtop.push_back(2);
+    }
+  
+    void push(int stackNum, int value) {
+        if(nowtop[stackNum] / 3 == stackown.size() / 3){
+            return;
+        }
+        stackown[nowtop[stackNum]] = value;
+        nowtop[stackNum] += 3;
+    }
+  
+    int pop(int stackNum) {
+        if(nowtop[stackNum] == stackNum){
+            return -1;
+        }
+        int t = stackown[nowtop[stackNum]-3];
+        nowtop[stackNum] -= 3;
+        return t;
+    }
+  
+    int peek(int stackNum) {
+        if(nowtop[stackNum] == stackNum){
+            return -1;
+        }
+        return stackown[nowtop[stackNum]-3];
+    }
+  
+    bool isEmpty(int stackNum) {
+        if(nowtop[stackNum] == stackNum){
+            return true;
+        }
+        return false;
+    }
+};
+
+/**
+ * Your TripleInOne object will be instantiated and called as such:
+ * TripleInOne* obj = new TripleInOne(stackSize);
+ * obj->push(stackNum,value);
+ * int param_2 = obj->pop(stackNum);
+ * int param_3 = obj->peek(stackNum);
+ * bool param_4 = obj->isEmpty(stackNum);
+ */
+

面试题 03.02. 栈的最小值

+

请设计一个栈,除了常规栈支持的pop与push函数以外,还支持min函数,该函数返回栈元素中的最小值。执行push、pop和min操作的时间复杂度必须为O(1)。

+
class MinStack {
+private:
+    stack<int> st;
+    stack<int> minst;
+public:
+    /** initialize your data structure here. */
+    MinStack() {
+        minst.push(INT_MAX);
+    }
+  
+    void push(int x) {
+        st.push(x);
+        if(x < minst.top()){
+            minst.push(x);
+        }else{
+            minst.push(minst.top());
+        }
+    }
+  
+    void pop() {
+        st.pop();
+        minst.pop();
+    }
+  
+    int top() {
+        return st.top();
+    }
+  
+    int getMin() {
+        return minst.top();
+    }
+};
+

面试题 03.03. 堆盘子

+

堆盘子。设想有一堆盘子,堆太高可能会倒下来。因此,在现实生活中,盘子堆到一定高度时,我们就会另外堆一堆盘子。请实现数据结构 SetOfStacks,模拟这种行为。SetOfStacks应该由多个栈组成,并且在前一个栈填满时新建一个栈。此外,SetOfStacks.push()SetOfStacks.pop()应该与普通栈的操作方法相同(也就是说,pop()返回的值,应该跟只有一个栈时的情况一样)。 进阶:实现一个 popAt(int index)方法,根据指定的子栈,执行pop操作。

+

当某个栈为空时,应当删除该栈。当栈中没有元素或不存在该栈时,poppopAt 应返回 -1.

+
class StackOfPlates {
+private:
+    vector<stack<int> > vt;
+    int capacity;
+public:
+    StackOfPlates(int cap) {
+        capacity = cap;
+    }
+  
+    void push(int val) {
+        if (capacity == 0){
+            return;
+        }
+        if(vt.empty()){
+            stack<int> st;
+            st.push(val);
+            vt.push_back(st);
+            return;
+        }
+        int l = vt.size()-1;
+        if(vt[l].size() == capacity){
+            stack<int> st;
+            st.push(val);
+            vt.push_back(st);
+            return;
+        }
+        vt[l].push(val);
+    }
+  
+    int pop() {
+        if (capacity == 0 || vt.empty()) {
+            return -1;
+        }
+        int l = vt.size()-1;
+        int t = vt[l].top();
+        vt[l].pop();
+        if(vt[l].empty()){
+            vt.pop_back();
+        }
+        return t;
+    }
+  
+    int popAt(int index) {
+        if (capacity == 0 || index >= vt.size()){
+            return -1;
+        }
+        int res = vt[index].top();
+        vt[index].pop();
+        if (vt[index].empty()) {
+            // 如果当前栈空,最后删除当前栈
+            vt.erase(vt.begin() + index);
+        }
+        return res;
+    }
+};
+

面试题 03.04. 化栈为队

+

实现一个MyQueue类,该类用两个栈来实现一个队列。

+
class MyQueue {
+private:
+    stack<int> st1;
+    stack<int> st2;
+public:
+    /** Initialize your data structure here. */
+    MyQueue() {
+
+    }
+  
+    /** Push element x to the back of queue. */
+    void push(int x) {
+        while(!st2.empty()){
+            st1.push(st2.top());
+            st2.pop();
+        }
+        st1.push(x);
+    }
+  
+    /** Removes the element from in front of queue and returns that element. */
+    int pop() {
+        while(!st1.empty()){
+            st2.push(st1.top());
+            st1.pop();
+        }
+        int t = st2.top();
+        st2.pop();
+        return t;
+    }
+  
+    /** Get the front element. */
+    int peek() {
+        while(!st1.empty()){
+            st2.push(st1.top());
+            st1.pop();
+        }
+        return st2.top();
+    }
+  
+    /** Returns whether the queue is empty. */
+    bool empty() {
+        return st1.empty() && st2.empty();
+    }
+};
+

面试题 03.05. 栈排序

+

栈排序。 编写程序,对栈进行排序使最小元素位于栈顶。最多只能使用一个其他的临时栈存放数据,但不得将元素复制到别的数据结构(如数组)中。该栈支持如下操作:pushpoppeekisEmpty。当栈为空时,peek 返回 -1。

+
class SortedStack {
+private:
+    stack<int> st1;
+    stack<int> st2;
+public:
+    SortedStack() {
+
+    }
+  
+    void push(int val) {
+        while(!st2.empty() && st2.top() > val){
+            st1.push(st2.top());
+            st2.pop();
+        }
+        while(!st1.empty() && st1.top() < val){
+            st2.push(st1.top());
+            st1.pop();
+        }
+        st1.push(val);
+    }
+  
+    void pop() {
+        while(!st2.empty()){
+            st1.push(st2.top());
+            st2.pop();
+        }
+        if(st1.size() != 0){
+            st1.pop();
+        }
+    }
+  
+    int peek() {
+        while(!st2.empty()){
+            st1.push(st2.top());
+            st2.pop();
+        }
+        if(!st1.empty()){
+            return st1.top();
+        }
+        return -1;
+    }
+  
+    bool isEmpty() {
+        return st1.empty() && st2.empty();
+    }
+};
+

面试题 03.06. 动物收容所

+

动物收容所。有家动物收容所只收容狗与猫,且严格遵守“先进先出”的原则。在收养该收容所的动物时,收养人只能收养所有动物中“最老”(由其进入收容所的时间长短而定)的动物,或者可以挑选猫或狗(同时必须收养此类动物中“最老”的)。换言之,收养人不能自由挑选想收养的对象。请创建适用于这个系统的数据结构,实现各种操作方法,比如 enqueuedequeueAnydequeueDogdequeueCat。允许使用Java内置的LinkedList数据结构。

+

enqueue方法有一个 animal参数,animal[0]代表动物编号,animal[1]代表动物种类,其中 0 代表猫,1 代表狗。

+

dequeue*方法返回一个列表 [动物编号, 动物种类],若没有可以收养的动物,则返回 [-1,-1]

+
class AnimalShelf {
+private:
+    queue<int> cats, dogs;
+public:
+    AnimalShelf() {
+    
+    }
+  
+    void enqueue(vector<int> animal) {
+        int id = animal[0];
+        int type = animal[1];
+        if (type == 1){
+            dogs.push(id);
+        }
+        else{
+            cats.push(id);
+        }
+    }
+  
+    vector<int> dequeueAny() {
+        if(cats.empty()){
+            return dequeueDog();
+        }
+        if(dogs.empty()){
+            return dequeueCat();
+        }
+        if(cats.front() < dogs.front()){
+            return dequeueCat();
+        }
+        return dequeueDog();
+    }
+  
+    vector<int> dequeueDog() {
+        if(dogs.empty()){
+            return {-1, -1};
+        }
+        int id = dogs.front();
+        dogs.pop();
+        return {id, 1};
+    }
+  
+    vector<int> dequeueCat() {
+        if(cats.empty()){
+            return {-1, -1};
+        }
+        int id = cats.front();
+        cats.pop();
+        return {id, 0};
+    }
+};
+

面试题 04

+

面试题 04.01. 节点间通路

+

节点间通路。给定有向图,设计一个算法,找出两个节点之间是否存在一条路径。

+
class Solution {
+public:
+    bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
+        if(n == 0){
+            return false;
+        }
+        vector<vector<int> > g(n);
+        for(int i=0;i<graph.size();i++){
+            g[graph[i][0]].push_back(graph[i][1]);
+        }
+        vector<bool> vis(n, false);
+        queue<int> q;
+        q.push(start);
+        vis[start] = true;
+        while(!q.empty()){
+            int cur = q.front();
+            q.pop();
+            for(int i=0;i<g[cur].size();i++){
+                if(g[cur][i] == target){
+                    return true;
+                }
+                if(!vis[g[cur][i]] && g[cur][i] != cur){
+                    q.push(g[cur][i]);
+                    vis[g[cur][i]] = true;
+                }
+            }
+        }
+        return false;
+    }
+};
+

面试题 04.02. 最小高度树

+

给定一个有序整数数组,元素各不相同且按升序排列,编写一个算法,创建一棵高度最小的二叉搜索树。

+
class Solution {
+public:
+    TreeNode* sortedArrayToBST(vector<int>& nums) {
+        if(nums.size() == 0){
+            return NULL;
+        }
+        int mid = nums.size() / 2;
+        TreeNode* root = new TreeNode(nums[mid]);
+        vector<int> templeft;
+        vector<int> tempright;
+        for(int i=0;i<nums.size();i++){
+            if(i < mid){
+                templeft.push_back(nums[i]);
+            } else if(i > mid){
+                tempright.push_back(nums[i]);
+            }
+        }
+        root->left = sortedArrayToBST(templeft);
+        root->right = sortedArrayToBST(tempright);
+        return root;
+    }
+};
+

面试题 04.03. 特定深度节点链表

+

给定一棵二叉树,设计一个算法,创建含有某一深度上所有节点的链表(比如,若一棵树的深度为 D,则会创建出 D 个链表)。返回一个包含所有深度的链表的数组。

+
class Solution {
+public:
+    vector<ListNode*> listOfDepth(TreeNode* tree) {
+        vector<ListNode*> vt;
+        queue<TreeNode*> q;
+        if(tree == NULL){
+            return vt;
+        }
+        q.push(tree);
+        while(!q.empty()){
+            int t = q.size();
+            ListNode* n = new ListNode(-1);
+            ListNode* nhead = n;
+            for(int i=0;i<t;i++){
+                TreeNode* a = q.front();
+                q.pop();
+                n->next = new ListNode(a->val);
+                n = n->next;
+                if(a->left != NULL){
+                    q.push(a->left);
+                }
+                if(a->right != NULL){
+                    q.push(a->right);
+                }
+            }
+            vt.push_back(nhead->next);
+        }
+        return vt;
+    }
+};
+

面试题 04.04. 检查平衡性

+

实现一个函数,检查二叉树是否平衡。在这个问题中,平衡树的定义如下:任意一个节点,其两棵子树的高度差不超过 1。

+
class Solution {
+public:
+    int balance(TreeNode* root){
+        if(root == NULL){
+            return 0;
+        }
+        int left = balance(root->left);
+        int right = balance(root->right);
+        return max(left, right) + 1;
+    }
+    bool isBalanced(TreeNode* root) {
+        if(root == NULL){
+            return true;
+        }
+        int left = balance(root->left);
+        int right = balance(root->right);
+        if(abs(left-right) > 1){
+            return false;
+        }
+        return isBalanced(root->left) && isBalanced(root->right);
+    }
+};
+

面试题 04.05. 合法二叉搜索树

+

实现一个函数,检查一棵二叉树是否为二叉搜索树。

+
class Solution {
+private:
+    vector<int> result;
+public:
+    void inorder(TreeNode* root){
+        if(root == NULL){
+            return;
+        }
+        inorder(root->left);
+        result.push_back(root->val);
+        inorder(root->right);
+    }
+    bool isValidBST(TreeNode* root) {
+        if(root == NULL){
+            return true;
+        }
+        inorder(root);
+        for(int i=0;i<result.size()-1;i++){
+            if(result[i+1] <= result[i]){
+                return false;
+            }
+        }
+        return true;
+    }
+};
+

面试题 04.06. 后继者

+

设计一个算法,找出二叉搜索树中指定节点的“下一个”节点(也即中序后继)。

+

如果指定节点没有对应的“下一个”节点,则返回 null

+
class Solution {
+public:
+    TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
+        stack<TreeNode*> st;
+        TreeNode *prev = NULL, *curr = root;
+        while (!st.empty() || curr != NULL) {
+            while (curr != NULL) {
+                st.emplace(curr);
+                curr = curr->left;
+            }
+            curr = st.top();
+            st.pop();
+            if (prev == p) {
+                return curr;
+            }
+            prev = curr;
+            curr = curr->right;
+        }
+        return NULL;
+    }
+};
+

面试题 04.08. 首个共同祖先

+

设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索树。

+
class Solution {
+public:
+    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
+        if(root == NULL || root == p || root == q){
+            return root;
+        }
+        TreeNode* left = lowestCommonAncestor(root->left, p, q);
+        TreeNode* right = lowestCommonAncestor(root->right, p, q);
+        if(left != NULL && right != NULL){
+            return root;
+        }
+        if(left == NULL){
+            return right;
+        }
+        return left;
+    }
+};
+

面试题 04.09. 二叉搜索树序列

+

从左向右遍历一个数组,通过不断将其中的元素插入树中可以逐步地生成一棵二叉搜索树。

+

给定一个由不同节点组成的二叉搜索树 root,输出所有可能生成此树的数组。

+
class Solution {
+private:
+    vector<vector<int>> res;
+    vector<int> tmp;
+public:
+    void dfs(deque<TreeNode*> dq){
+        if (dq.empty())
+        {
+            res.push_back(tmp);     //得到一种合法序列
+            return;
+        }
+
+        int size = dq.size();
+        while (size -- )            //二叉树的层序遍历
+        {
+            TreeNode* node = dq.front();
+            dq.pop_front();
+
+            tmp.push_back(node->val);
+
+            if (node->left) dq.push_back(node->left);   //左右子树入队
+            if (node->right) dq.push_back(node->right);
+
+            dfs(dq);
+
+            if (node->right) dq.pop_back();             //左右子树出队
+            if (node->left) dq.pop_back();
+
+            dq.push_back(node);
+            tmp.pop_back();
+        }
+    }
+
+    vector<vector<int>> BSTSequences(TreeNode* root) {
+        if (root == nullptr) return {{}};
+        deque<TreeNode*> dq;
+        dq.push_back(root);
+        dfs(dq);
+
+        return res;
+    }
+};
+

面试题 04.10. 检查子树

+

检查子树。你有两棵非常大的二叉树:T1,有几万个节点;T2,有几万个节点。设计一个算法,判断 T2 是否为 T1 的子树。

+

如果 T1 有这么一个节点 n,其子树与 T2 一模一样,则 T2 为 T1 的子树,也就是说,从节点 n 处把树砍断,得到的树与 T2 完全相同。

+
class Solution {
+public:
+    bool isSame(TreeNode* t1, TreeNode* t2){
+        if(t1 == NULL && t2 == NULL){
+            return true;
+        }
+        if(t1 == NULL || t2 == NULL){
+            return false;
+        }
+        if(t1->val != t2->val){
+            return false;
+        }
+        return isSame(t1->left, t2->left) && isSame(t1->right, t2->right);
+    }
+    bool checkSubTree(TreeNode* t1, TreeNode* t2) {
+        if(t1 == NULL && t2 == NULL){
+            return true;
+        }
+        if(t1 == NULL || t2 == NULL){
+            return false;
+        }
+        if(t1->val == t2->val && isSame(t1, t2)){
+            return true;
+        }
+        return checkSubTree(t1->left, t2) || checkSubTree(t1->right, t2);
+    }
+};
+

面试题 04.12. 求和路径

+

给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。

+
class Solution {
+private:
+    int res;
+public:
+    void DFS(TreeNode* root, int sum){
+        if(root == NULL){
+            return;
+        }
+        if(sum == root->val){
+            res += 1;
+        }
+        DFS(root->left, sum-root->val);
+        DFS(root->right, sum-root->val);
+    }
+    void DFS1(TreeNode* root, int sum){
+        if(root == NULL){
+            return;
+        }
+        DFS(root, sum);
+        DFS1(root->left, sum);
+        DFS1(root->right, sum);
+    }
+    int pathSum(TreeNode* root, int sum) {
+        res = 0;
+        DFS1(root, sum);
+        return res;
+    }
+};
+

面试题 05

+

面试题 05.01. 插入

+

给定两个整型数字 NM,以及表示比特位置的 iji <= j,且从 0 位开始计算)。

+

编写一种方法,使 M 对应的二进制数字插入 N 对应的二进制数字的第 i ~ j 位区域,不足之处用 0 补齐。具体插入过程如图所示。

+
    +
  • 先将 N的第 i ~ j位全部置零;
  • +
  • 再将 M左移 i位,使之对齐上一步中 N置零的位,直接相加即可。
  • +
+
class Solution {
+public:
+    int insertBits(int N, int M, int i, int j) {
+        for (int k = i; k <= j; ++ k){
+               //举例说明: (1 << 3) 表示 00001000,取反后得 11110111
+            // N &= (11110111) 表示将 N 的第3位置零了
+            N &= ~(1 << k);
+        }
+        return N + (M << i);
+    }
+};
+

面试题 05.02. 二进制数转字符串

+

二进制数转字符串。给定一个介于0和1之间的实数(如0.72),类型为double,打印它的二进制表达式。如果该数字无法精确地用32位以内的二进制表示,则打印“ERROR”。

+
class Solution {
+public:
+    string printBin(double num) {
+        string res = "0.";
+        while (res.size() <= 32 && num != 0) {
+            num *= 2;
+            int digit = num;
+            res.push_back(digit + '0');
+            num -= digit;
+        }
+        return res.size() <= 32 ? res : "ERROR";
+    }
+};
+

面试题 05.03. 翻转数位

+

给定一个32位整数 num,你可以将一个数位从0变为1。请编写一个程序,找出你能够获得的最长的一串1的长度。

+

暴力思路:

+
    +
  1. 将32位整数转化为0序列和1序列长度的数组:例如 11011101111被转化为[0(0), 4(1), 1(0), 3(1), 1(0), 2(1)];即num(从右往左为)) 0个0, 4个1, 1个0, 3个1, 1个0, 2个1 组成。该数组为从0开始的交替序列
  2. +
  3. 有了该序列之后,对其进行遍历,对每一个0序列如果长度为1,则length = left + right + 1 把左边和右边的1长度相加再加1。如果0序列长度大于1,则length = max(left, right) + 1 不能连接,左右1长度最大 + 1
  4. +
  5. 遍历过程中更新最大值
  6. +
+

思路优化:

+
    +
  1. 遍历每一位,用previous和current记录之前1的数量和现在1的数量,遇到0就更新previous,current = 0;
  2. +
  3. 用一个变量储存最大长度,每轮遍历进行更新:length = max(previous + current + 1, length);
  4. +
  5. 有个问题是遇到连续的0,这样正确吗?书上的做法是再判断一位,如果 (num & 2) == 0,将previous置为0;这样做好像没有必要,因为遇到第一个0时,current = 0;遇到第二个0时, previous = current = 0, current 被重新置为0,此时length = previous + current + 1,表达式仍然正确;
  6. +
  7. 在c++中 >> 右移为逻辑右移,左边补符号位,如果是负数,左边补1,正数左边补0,使用 while (num > 0) 时负数会死循环:可以转化为无符号数,左移时补0, while 循环正确退出;或者使用for循环,循环32次
  8. +
+
class Solution {
+public:
+    int reverseBits(int num) {
+        if (~num == 0) return 32;
+        int previous = 0;
+        int current = 0;
+        int length = 0;
+        for (int i = 0; i < 32; i++) {
+            if (num & 1) {
+                current++;
+            } else {
+                previous = current;
+                current = 0;
+            }
+            length = max(length, previous + current + 1);
+            num >>= 1;
+        }
+        return length;
+
+    }
+};
+

面试题 05.04. 下一个数

+

下一个数。给定一个正整数,找出与其二进制表达式中1的个数相同且大小最接近的那两个数(一个略大,一个略小)。

+
class Solution {
+public:
+    int f(int num) {
+        int lb  = num & -num;
+        int r = lb + num;
+        //if(r==0)return 0; 这里是对small没有10的判断,但是也会影响large
+        return (num ^r)/lb >> 2 | r;
+    }
+    vector<int> findClosedNumbers(int num) {
+        if (num == INT_MAX) return {-1,-1};
+        if (num==1)return {2,-1};
+        return {f(num), ~f(~num)};  //large和small
+    }
+};
+

面试题 05.06. 整数转换

+

整数转换。编写一个函数,确定需要改变几个位才能将整数A转成整数B。

+

不断对 c 进行移位操作,然后检查最低有效位。

+
class Solution {
+public:
+    int convertInteger(int A, int B) {
+        int res = 0;
+        for (unsigned c = A ^ B; c != 0; c = c >> 1)
+            res += c & 1; // 数一数 c 中有几个 1
+        return res;
+    }
+};
+

面试题 05.07. 配对交换

+

配对交换。编写程序,交换某个整数的奇数位和偶数位,尽量使用较少的指令(也就是说,位0与位1交换,位2与位3交换,以此类推)。

+
class Solution {
+public:
+    int exchangeBits(int num) {
+        int pre=1;
+        int res=0;
+        while(pre<=num)
+        {
+            res|=(pre&num)<<1;
+            res|=((pre<<1)&num)>>1;
+            pre<<=2;
+        }
+        return res;
+    }
+};
+

面试题 05.08. 绘制直线

+

已知一个由像素点组成的单色屏幕,每行均有 w 个像素点,所有像素点初始为 0,左上角位置为 (0,0)

+

现将每行的像素点按照「每 32 个像素点」为一组存放在一个 int 中,再依次存入长度为 length 的一维数组中。

+

我们将在屏幕上绘制一条从点 (x1,y) 到点 (x2,y) 的直线(即像素点修改为 1),请返回绘制过后的数组。

+
class Solution {
+public:
+    vector<int> drawLine(int length, int w, int x1, int x2, int y) {
+        // 注意直线的坐标
+        // 实际上需要特殊处理的 只有直线所在的那一行
+        // 结果数组从 像素点(0, 0)开始计算
+        int per_row = w / 32;
+        vector<int> res(length, 0);
+        for(int i = x1; i <= x2 && i < length * 32; i++)
+            res[y * per_row + i / 32] |= (1 << 31 - i % 32);        // 从左到右,先高后低
+
+        return res;
+    }
+};
+

面试题 08

+

面试题 08.01. 三步问题

+

三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。

+
class Solution {
+public:
+    int waysToStep(int n) {
+        if(n == 1 || n == 2){
+            return n;
+        }
+        if(n == 3){
+            return 4;
+        }
+        vector<int> dp(n+1, 0);
+        dp[0] = 0;
+        dp[1] = 1;
+        dp[2] = 2;
+        dp[3] = 4;
+        for(int i=4;i<=n;i++){
+            dp[i] = (dp[i-1] + dp[i-2]) % 1000000007 +dp[i-3];
+            dp[i] %= 1000000007;
+        }
+        return dp[n];
+    }
+};
+

面试题 08.02. 迷路的机器人

+

设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种算法,寻找机器人从左上角移动到右下角的路径。

+
class Solution {
+public:
+    bool DFS(vector<vector<int>>& obstacleGrid, vector<vector<int>>& result, int i, int j, int m, int n){
+        if(obstacleGrid[i][j] == 1){
+            return false;
+        }
+        if(i == m-1 && j == n-1){
+            result.push_back({i,j});
+            return true;
+        }
+        result.push_back({i,j});
+        if(i+1 < m && DFS(obstacleGrid, result, i+1,j,m,n)){
+            return true;
+        }
+        if(j+1 < n && DFS(obstacleGrid, result, i,j+1,m,n)){
+            return true;
+        }
+        result.pop_back();
+        obstacleGrid[i][j] = 1;
+        return false;
+    }
+    vector<vector<int>> pathWithObstacles(vector<vector<int>>& obstacleGrid) {
+        vector<vector<int> > result;
+        int m = obstacleGrid.size();
+        int n = obstacleGrid[0].size();
+        DFS(obstacleGrid, result, 0,0,m,n);
+        return result;
+    }
+};
+

面试题 08.03. 魔术索引

+

魔术索引。 在数组 A[0...n-1]中,有所谓的魔术索引,满足条件 A[i] = i。给定一个有序整数数组,编写一种方法找出魔术索引,若有的话,在数组A中找出一个魔术索引,如果没有,则返回-1。若有多个魔术索引,返回索引值最小的一个。

+
class Solution {
+public:
+    int findMagicIndex(vector<int>& nums) {
+        for(int i = 0; i<nums.size(); i++){
+            if(i == nums[i]) return i;
+        }
+        return -1;
+    }
+};
+

面试题 08.04. 幂集

+

幂集。编写一种方法,返回某集合的所有子集。集合中 不包含重复的元素

+

说明:解集不能包含重复的子集。

+
class Solution {
+public:
+    void backtracking(vector<vector<int> > &result, vector<int>& temp, vector<int>& nums, int start){
+        result.push_back(temp);
+        for(int i=start;i<nums.size();i++){
+            temp.push_back(nums[i]);
+            backtracking(result, temp, nums, i+1);
+            temp.pop_back();
+        }
+    }
+    vector<vector<int>> subsets(vector<int>& nums) {
+        vector<vector<int> > result;
+        vector<int> temp;
+        backtracking(result, temp, nums, 0);
+        return result;
+    }
+};
+

面试题 08.05. 递归乘法

+

递归乘法。 写一个递归函数,不使用 * 运算符, 实现两个正整数的相乘。可以使用加号、减号、位移,但要吝啬一些。

+
class Solution {
+public:
+    int multiply(int A, int B) {
+        if(B){//B非0才计算
+            //从0阶(A*(B&1)*2^0)开始,每次算当前阶(A*(B&1)*2^n)的乘法并累加起来,算到B为0为止。
+            if(B&1){//如果B的最后一位是1
+                //把B的阶放到A上去,递归算B的倒数第2位和A的乘法,然后求和+(1*A=A)。
+                return multiply((long long)A<<1,B>>1)+A;
+            }else{
+                //把B的阶放到A上去,递归算B的倒数第2位和A的乘法,然后求和+(0*A=0)。
+                return multiply((long long)A<<1,B>>1);
+            }
+        }
+        // B为0结果当然是0
+        return 0;
+    }
+};
+

面试题 08.06. 汉诺塔问题

+

在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
+(1) 每次只能移动一个盘子;
+(2) 盘子只能从柱子顶端滑出移到下一根柱子;
+(3) 盘子只能叠在比它大的盘子上。

+

请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。

+

你需要原地修改栈。

+
class Solution {
+public:
+    void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
+        int n = A.size();
+        move(n, A, B, C);
+    }
+    void move(int n, vector<int>& A, vector<int>& B, vector<int>& C){
+        if (n == 1){
+            C.push_back(A.back());
+            A.pop_back();
+            return;
+        }
+
+        move(n-1, A, C, B);    // 将A上面n-1个通过C移到B
+        C.push_back(A.back());  // 将A最后一个移到C
+        A.pop_back();          // 这时,A空了
+        move(n-1, B, A, C);     // 将B上面n-1个通过空的A移到C
+    }
+};
+

面试题 08.07. 无重复字符串的排列组合

+

无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。

+
class Solution {
+public:
+    void backtracking(vector<string> & result, string S, vector<char> &temp, vector<bool> &visited){
+        if(temp.size() == S.size()){
+            string t = "";
+            for(int i=0;i<temp.size();i++){
+                t += temp[i];
+            }
+            result.push_back(t);
+            return;
+        }
+        for(int i=0;i<S.size();i++){
+            if (visited[i] == false){
+                visited[i] = true;
+                temp.push_back(S[i]);
+                backtracking(result, S, temp, visited);
+                temp.pop_back();
+                visited[i] = false;
+            }
+
+        }
+    }
+    vector<string> permutation(string S) {
+        vector<string> result;
+        vector<char> temp;
+        vector<bool> visited(S.size(), false);
+        backtracking(result, S, temp, visited);
+        return result;
+    }
+};
+

面试题 08.08. 有重复字符串的排列组合

+

有重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合。

+
class Solution {
+public:
+    void backtracking(vector<string> & result, string S, vector<char> &temp, vector<bool> &visited){
+        if(temp.size() == S.size()){
+            string t = "";
+            for(int i=0;i<temp.size();i++){
+                t += temp[i];
+            }
+            result.push_back(t);
+            return;
+        }
+        for(int i=0;i<S.size();i++){
+            if(i > 0 && S[i] == S[i-1] && visited[i-1] == false){
+                continue;
+            }
+            if (visited[i] == false){
+                visited[i] = true;
+                temp.push_back(S[i]);
+                backtracking(result, S, temp, visited);
+                temp.pop_back();
+                visited[i] = false;
+            }
+
+        }
+    }
+    vector<string> permutation(string S) {
+        sort(S.begin(), S.end());
+        vector<string> result;
+        vector<char> temp;
+        vector<bool> visited(S.size(), false);
+        backtracking(result, S, temp, visited);
+        return result;
+    }
+};
+

面试题 08.09. 括号

+

括号。设计一种算法,打印n对括号的所有合法的(例如,开闭一一对应)组合。

+

说明:解集不能包含重复的子集。

+
class Solution {
+public:
+    void backtracking(vector<string> &result, vector<char> &temp, int nowleft, int n){
+        if(temp.size() == n * 2){
+            if(nowleft == 0){
+                string t = "";
+                for(int i=0;i<temp.size();i++){
+                    t += temp[i];
+                }
+                result.push_back(t);
+            }
+            return;
+        }
+        temp.push_back('(');
+        backtracking(result, temp, nowleft+1, n);
+        temp.pop_back();
+        if(nowleft > 0){
+            temp.push_back(')');
+            backtracking(result, temp, nowleft-1, n);
+            temp.pop_back();
+        }
+    }
+    vector<string> generateParenthesis(int n) {
+        vector<string> result;
+        vector<char> temp;
+        backtracking(result, temp, 0, n);
+        return result;
+    }
+};
+

面试题 08.10. 颜色填充

+

编写函数,实现许多图片编辑软件都支持的「颜色填充」功能。

+

待填充的图像用二维数组 image 表示,元素为初始颜色值。初始坐标点的行坐标为 sr 列坐标为 sc。需要填充的新颜色为 newColor

+

「周围区域」是指颜色相同且在上、下、左、右四个方向上存在相连情况的若干元素。

+

请用新颜色填充初始坐标点的周围区域,并返回填充后的图像。

+
class Solution {
+public:
+    vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
+        int m = image.size();
+        int n = image[0].size();
+        vector<vector<bool> > visited(m, vector<bool>(n, false));
+        queue<pair<int, int> > q;
+        q.push({sr, sc});
+        visited[sr][sc] = true;
+        int sign = image[sr][sc];
+        image[sr][sc] = newColor;
+        vector<vector<int> > direction = {
+            {0,-1},
+            {0,1},
+            {-1,0},
+            {1,0},
+        };
+        while(!q.empty()){
+            pair<int, int> p = q.front();
+            q.pop();
+            for(int i=0;i<direction.size();i++){
+                for(int j=0;j<direction[0].size();j++){
+                    int x = p.first + direction[i][0];
+                    int y = p.second + direction[i][1];
+                    if(x >= 0 && x < m && y >= 0 && y < n && image[x][y] == sign && visited[x][y] == false){
+                        q.push({x,y});
+                        image[x][y] = newColor;
+                        visited[x][y] = true;
+                    }
+                }
+            }
+        }
+        return image;
+    }
+};
+

面试题 08.11. 硬币

+

硬币。给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)

+
class Solution {
+public:
+    int waysToChange(int n) {
+        vector<int> coins = {25,10,5,1};
+        vector<int> dp(n+1, 0);
+        dp[0] = 1;
+        for(int i=0;i<coins.size();i++){
+            for(int j=coins[i];j<=n;j++){
+                dp[j] = (dp[j] + dp[j-coins[i]]) % 1000000007;
+            }
+        }
+        return dp[n];
+    }
+};
+

面试题 08.12. 八皇后

+

设计一种算法,打印 N 皇后在 N × N 棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。这里的“对角线”指的是所有的对角线,不只是平分整个棋盘的那两条对角线。

+
class Solution {
+public:
+    bool check(vector<vector<char> > &temp, int nowindex){
+        int m = temp.size();
+        int n = temp[0].size();
+        for(int i=0;i<m-1;i++){
+            if(temp[i][nowindex] == 'Q'){
+                return false;
+            }
+        }
+        int x = m-1-1;
+        int y = nowindex-1;
+        while(x >= 0 && y >= 0){
+            if(temp[x][y] == 'Q'){
+                return false;
+            }
+            x--;
+            y--;
+        }
+        x = m-1-1;
+        y = nowindex+1;
+        while(x >= 0 && y < n){
+            if(temp[x][y] == 'Q'){
+                return false;
+            }
+            x--;
+            y++;
+        }
+        return true;
+    }
+    void backtracking(vector<vector<string> > &result, vector<vector<char> > &temp, int n, int now){
+        if(now == n){
+            vector<string> t;
+            for(int i=0;i<temp.size();i++){
+                string tempstring = "";
+                for(int j=0;j<temp[i].size();j++){
+                    tempstring += temp[i][j];
+                }
+                t.push_back(tempstring);
+            }
+            result.push_back(t);
+            return;
+        }
+
+        for(int i=0;i<n;i++){
+            vector<char> vt;
+            for(int j=0;j<n;j++){
+                vt.push_back('.');
+            }
+            vt[i] = 'Q';
+            temp.push_back(vt);
+            if(check(temp, i)){
+                backtracking(result, temp, n, now+1);
+            }
+            temp.pop_back();
+        }
+    }
+    vector<vector<string>> solveNQueens(int n) {
+        vector<vector<string> > result;
+        vector<vector<char> > temp;
+        backtracking(result, temp, n, 0);
+        return result;
+    }
+};
+

面试题 08.13. 堆箱子

+

堆箱子。给你一堆n个箱子,箱子宽 wi、深 di、高 hi。箱子不能翻转,将箱子堆起来时,下面箱子的宽度、高度和深度必须大于上面的箱子。实现一种方法,搭出最高的一堆箱子。箱堆的高度为每个箱子高度的总和。

+

输入使用数组 [wi, di, hi]表示每个箱子。

+
class Solution {
+public:
+    int pileBox(vector<vector<int>>& box) {
+        sort(box.begin(), box.end());
+        vector<int> dp(box.size(), 0);
+        for (int i = 0; i < box.size(); ++i) {
+            dp[i] = box[i][2];
+        }
+        for (int i = 1; i < box.size(); ++i) {
+            for (int j = 0; j < i; ++j) {
+                if (box[i][0] > box[j][0] && box[i][1] > box[j][1] && box[i][2] > box[j][2])
+                dp[i] = max(dp[i], dp[j] + box[i][2]);
+            }
+        }
+        return *max_element(dp.begin(), dp.end());
+    }
+};
+

面试题 08.14. 布尔运算

+

给定一个布尔表达式和一个期望的布尔结果 result,布尔表达式由 0 (false)、1 (true)、& (AND)、 | (OR) 和 ^ (XOR) 符号组成。实现一个函数,算出有几种可使该表达式得出 result 值的括号方法。

+
    +
  • dp[i][j][0]代表第i个字符到第j个字符,result=0的可能性个数
  • +
  • dp[i][j][1]代表第i个字符到第j个字符,result=1的可能性个数
  • +
+

然后枚举中间断点就行啦

+
class Solution {
+public:
+    int countEval(string s, int result) {
+        int n = s.length();
+        vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>(2, 0)));
+
+        for (int i=0; i<n; i+=2){
+            int tmp = 0;
+            if (s[i] == '1') tmp = 1;
+            dp[i][i][0] = 1-tmp;
+            dp[i][i][1] = tmp;
+        }
+
+        for (int step=0; step<n; step+=2){
+            for (int i=0; i+step<n; i+=2){
+                for (int j=i+1; j<i+step; j+=2){
+                    int left0 = dp[i][j-1][0], left1 = dp[i][j-1][1];
+                    int right0 = dp[j+1][i+step][0], right1 = dp[j+1][i+step][1];
+                    // cout<<i<<" "<<i+step<<" "<<left0<<" "<<left1<<" "<<right0<<" "<<right1;
+                    if (s[j]=='&'){
+                        dp[i][i+step][0] += left0*(right0+right1)+left1*right0;
+                        dp[i][i+step][1] += left1*right1;
+                    }
+                    else if (s[j]=='|'){
+                        dp[i][i+step][0] += left0*right0;
+                        dp[i][i+step][1] += left0*right1 + left1*(right0+right1);
+                    }
+                    else{//s[j]=='^'
+                        dp[i][i+step][0] += left0*right0 + left1*right1;
+                        dp[i][i+step][1] += left0*right1 + left1*right0;
+                    }
+                    // cout<<" dp[i][i+step][0]:"<<dp[i][i+step][0]<<" dp[i][i+step][1]:"<<dp[i][i+step][1]<<endl;
+                }
+            }
+        }
+        return dp[0][n-1][result];
+    }
+};
+

面试题 10

+

面试题 10.01. 合并排序的数组

+

给定两个排序后的数组 A 和 B,其中 A 的末端有足够的缓冲空间容纳 B。 编写一个方法,将 B 合并入 A 并排序。

+

初始化 A 和 B 的元素数量分别为 mn

+
class Solution {
+public:
+    void merge(vector<int>& A, int m, vector<int>& B, int n) {
+        int actualindex = m + n - 1;
+        int Aindex = m - 1;
+        int Bindex = n - 1;
+        while(Aindex >= 0 || Bindex >= 0){
+            if(Aindex < 0){
+                A[actualindex] = B[Bindex];
+                Bindex--;
+            } else if(Bindex < 0){
+                A[actualindex] = A[Aindex];
+                Aindex--;
+            } else{
+                if(A[Aindex] < B[Bindex]){
+                    A[actualindex] = B[Bindex];
+                    Bindex--;
+                } else{
+                    A[actualindex] = A[Aindex];
+                    Aindex--;
+                }
+            }
+            actualindex--;
+        }
+    }
+};
+

面试题 10.02. 变位词组

+

编写一种方法,对字符串数组进行排序,将所有变位词组合在一起。变位词是指字母相同,但排列不同的字符串。

+
class Solution {
+public:
+    vector<vector<string>> groupAnagrams(vector<string>& strs) {
+        unordered_map<string, vector<string> > mp;
+        for(int i=0;i<strs.size();i++){
+            string t = strs[i];
+            sort(t.begin(), t.end());
+            mp[t].push_back(strs[i]);
+        }
+        vector<vector<string> > result;
+        for(auto it = mp.begin(); it != mp.end();it++){
+            result.push_back(it->second);
+        }
+        return result;
+    }
+};
+

面试题 10.03. 搜索旋转数组

+

搜索旋转数组。给定一个排序后的数组,包含n个整数,但这个数组已被旋转过很多次了,次数不详。请编写代码找出数组中的某个元素,假设数组元素原先是按升序排列的。若有多个相同元素,返回索引值最小的一个。

+
class Solution {
+public:
+    int search(vector<int>& arr, int target) {
+        int left = 0;
+        int right = arr.size() - 1;
+        if (right == -1)
+            return -1;
+        while (left < right) {                                         // 循环结束条件left==right
+            int mid = left + (right - left) / 2;
+            if (arr[left] < arr[mid]) {                              // 如果左值小于中值,说明左边区间升序             
+                if (arr[left] <= target && target <= arr[mid]) {     // 如果目标在左边的升序区间中,右边界移动到mid
+                    right = mid;                                     
+                } else {                                               // 否则目标在右半边,左边界移动到mid+1
+                    left = mid + 1;                                  
+                }
+            } else if (arr[left] > arr[mid]) {                       // 如果左值大于中值,说明左边不是升序,右半边升序
+                if (arr[left] <= target || target <= arr[mid]) {     // 如果目标在左边,右边界移动到mid
+                    right = mid;                                     
+                } else {                                               // 否则目标在右半边,左边界移动到mid+1
+                    left = mid + 1;                                  
+                }
+            } else if (arr[left] == arr[mid]) {                      // 如果左值等于中值,可能是已经找到了目标,也可能是遇到了重复值
+                if (arr[left] != target) {                            // 如果左值不等于目标,说明还没找到,需要逐一清理重复值。
+                    left++;
+                } else {                                               // 如果左值等于目标,说明已经找到最左边的目标值 
+                    right = left;                                      // 将右边界移动到left,循环结束
+                }
+            }
+        }
+        return (arr[left] == target) ? left : -1;                     // 返回left,或者-1
+    }
+};
+

面试题 10.05. 稀疏数组搜索

+

稀疏数组搜索。有个排好序的字符串数组,其中散布着一些空字符串,编写一种方法,找出给定字符串的位置。

+
int findString(char** words, int wordsSize, char* s){
+    int left = 0, right = wordsSize-1, mid;
+    while(left<right){
+        mid = (left + right) / 2;
+        if (*words[mid] == NULL) {//如果中间为空,则由二分查找变为线性遍历
+            if(strcmp(words[left],s)) left++;//从左至右扫描
+            else return left; 
+        }
+        else if (strcmp(words[mid],s) > 0) right = mid - 1;
+        else if (strcmp(words[mid],s) < 0) left = mid + 1;
+        else return mid;
+    }
+    if (strcmp(words[left],s) == 0) return left;
+    else return -1;
+
+}
+

面试题 10.09. 排序矩阵查找

+

给定M×N矩阵,每一行、每一列都按升序排列,请编写代码找出某元素。

+
class Solution {
+public:
+    bool searchMatrix(vector<vector<int>>& matrix, int target) {
+        int m = matrix.size();
+        if(m == 0){
+            return false;
+        }
+        int n = matrix[0].size();
+        int x = 0;
+        int y = n - 1;
+        while(x < m && y >= 0){
+            if(matrix[x][y] == target){
+                return true;
+            } else if(matrix[x][y] > target){
+                y--;
+            } else{
+                x++;
+            }
+        }
+        return false;
+    }
+};
+

面试题 10.10. 数字流的秩

+

假设你正在读取一串整数。每隔一段时间,你希望能找出数字 x 的秩(小于或等于 x 的值的个数)。请实现数据结构和算法来支持这些操作,也就是说:

+

实现 track(int x) 方法,每读入一个数字都会调用该方法;

+

实现 getRankOfNumber(int x) 方法,返回小于或等于 x 的值的个数。

+

树状数组的模板题目

+
class StreamRank {
+private:
+    int a[50010];
+public:
+    StreamRank() {
+        for (int i=1;i<=50001;++i) a[i]=0;
+    }
+  
+    void track(int x) {
+        ++x;
+        for (int i=x;i<=50001;i+=i&(-i)) a[i]++;
+    }
+  
+    int getRankOfNumber(int x) {
+        ++x;
+        int sum=0;
+        for (int i=x;i;i-=i&(-i)) sum+=a[i];
+        return sum;
+    }
+};
+
+/**
+ * Your StreamRank object will be instantiated and called as such:
+ * StreamRank* obj = new StreamRank();
+ * obj->track(x);
+ * int param_2 = obj->getRankOfNumber(x);
+ */
+

面试题 10.11. 峰与谷

+

在一个整数数组中,“峰”是大于或等于相邻整数的元素,相应地,“谷”是小于或等于相邻整数的元素。例如,在数组{5, 8, 4, 2, 3, 4, 6}中,{8, 6}是峰, {5, 2}是谷。现在给定一个整数数组,将该数组按峰与谷的交替顺序排序。

+
class Solution {
+public:
+    void wiggleSort(vector<int>& nums) {
+        vector<int> temp=nums;
+        sort(temp.begin(), temp.end());
+        int Left=0, Right=nums.size()-1;
+        int index=0;
+        while(index < nums.size()){
+            nums[index++] = temp[Left++];
+            if(index < nums.size()){
+                nums[index++] = temp[Right--];
+            }
+        }
+        return;
+    }
+};
+

面试题 16

+

面试题 16.01. 交换数字

+

编写一个函数,不用临时变量,直接交换 numbers = [a, b]ab的值。

+

面试题 16.02. 单词频率

+

设计一个方法,找出任意指定单词在一本书中的出现频率。

+

你的实现应该支持如下操作:

+
    +
  • WordsFrequency(book)构造函数,参数为字符串数组构成的一本书
  • +
  • get(word)查询指定单词在书中出现的频率
  • +
+

面试题 16.03. 交点

+

给定两条线段(表示为起点 start = {X1, Y1}和终点 end = {X2, Y2}),如果它们有交点,请计算其交点,没有交点则返回空值。

+

要求浮点型误差不超过 10^-6。若有多个交点(线段重叠)则返回 X 值最小的点,X 坐标相同则返回 Y 值最小的点。

+

面试题 16.04. 井字游戏

+

设计一个算法,判断玩家是否赢了井字游戏。输入是一个 N x N 的数组棋盘,由字符" ",“X"和"O"组成,其中字符” "代表一个空位。

+

以下是井字游戏的规则:

+
    +
  • 玩家轮流将字符放入空位(" ")中。
  • +
  • 第一个玩家总是放字符"O",且第二个玩家总是放字符"X"。
  • +
  • "X"和"O"只允许放置在空位中,不允许对已放有字符的位置进行填充。
  • +
  • 当有N个相同(且非空)的字符填充任何行、列或对角线时,游戏结束,对应该字符的玩家获胜。
  • +
  • 当所有位置非空时,也算为游戏结束。
  • +
  • 如果游戏结束,玩家不允许再放置字符。
  • +
+

如果游戏存在获胜者,就返回该游戏的获胜者使用的字符(“X"或"O”);如果游戏以平局结束,则返回 “Draw”;如果仍会有行动(游戏未结束),则返回 “Pending”。

+

面试题 16.05. 阶乘尾数

+

设计一个算法,算出 n 阶乘有多少个尾随零。

+

面试题 16.06. 最小差

+

给定两个整数数组 ab,计算具有最小差绝对值的一对数值(每个数组中取一个值),并返回该对数值的差

+

面试题 16.07. 最大数值

+

编写一个方法,找出两个数字 ab中最大的那一个。不得使用if-else或其他比较运算符。

+

面试题 16.08. 整数的英语表示

+

给定一个整数,打印该整数的英文描述。

+

面试题 16.09. 运算

+

请实现整数数字的乘法、减法和除法运算,运算结果均为整数数字,程序中只允许使用加法运算符和逻辑运算符,允许程序中出现正负常数,不允许使用位运算。

+

你的实现应该支持如下操作:

+
    +
  • Operations() 构造函数
  • +
  • minus(a, b) 减法,返回 a - b
  • +
  • multiply(a, b) 乘法,返回 a * b
  • +
  • divide(a, b) 除法,返回 a / b
  • +
+

面试题 16.10. 生存人数

+

给定 N 个人的出生年份和死亡年份,第 i 个人的出生年份为 birth[i],死亡年份为 death[i],实现一个方法以计算生存人数最多的年份。

+

你可以假设所有人都出生于 1900 年至 2000 年(含 1900 和 2000 )之间。如果一个人在某一年的任意时期处于生存状态,那么他应该被纳入那一年的统计中。例如,生于 1908 年、死于 1909 年的人应当被列入 1908 年和 1909 年的计数。

+

如果有多个年份生存人数相同且均为最大值,输出其中最小的年份。

+

面试题 16.11. 跳水板

+

你正在使用一堆木板建造跳水板。有两种类型的木板,其中长度较短的木板长度为 shorter,长度较长的木板长度为 longer。你必须正好使用 k块木板。编写一个方法,生成跳水板所有可能的长度。

+

返回的长度需要从小到大排列。

+

面试题 16.13. 平分正方形

+

给定两个正方形及一个二维平面。请找出将这两个正方形分割成两半的一条直线。假设正方形顶边和底边与 x 轴平行。

+

每个正方形的数据 square包含3个数值,正方形的左下顶点坐标 [X,Y] = [square[0],square[1]],以及正方形的边长 square[2]。所求直线穿过两个正方形会形成4个交点,请返回4个交点形成线段的两端点坐标(两个端点即为4个交点中距离最远的2个点,这2个点所连成的线段一定会穿过另外2个交点)。2个端点坐标 [X<sub>1</sub>,Y<sub>1</sub>][X<sub>2</sub>,Y<sub>2</sub>]的返回格式为 {X<sub>1</sub>,Y<sub>1</sub>,X<sub>2</sub>,Y<sub>2</sub>},要求若 X<sub>1</sub> != X<sub>2</sub>,需保证 X<sub>1</sub> < X<sub>2</sub>,否则需保证 Y<sub>1</sub> <= Y<sub>2</sub>

+

若同时有多条直线满足要求,则选择斜率最大的一条计算并返回(与Y轴平行的直线视为斜率无穷大)。

+

面试题 16.14. 最佳直线

+

给定一个二维平面及平面上的 N 个点列表 Points,其中第 i个点的坐标为 Points[i]=[X<sub>i</sub>,Y<sub>i</sub>]。请找出一条直线,其通过的点的数目最多。

+

设穿过最多点的直线所穿过的全部点编号从小到大排序的列表为 S,你仅需返回 [S[0],S[1]]作为答案,若有多条直线穿过了相同数量的点,则选择 S[0]值较小的直线返回,S[0]相同则选择 S[1]值较小的直线返回。

+

面试题 16.15. 珠玑妙算

+

珠玑妙算游戏(the game of master mind)的玩法如下。

+

计算机有4个槽,每个槽放一个球,颜色可能是红色(R)、黄色(Y)、绿色(G)或蓝色(B)。例如,计算机可能有RGGB 4种(槽1为红色,槽2、3为绿色,槽4为蓝色)。作为用户,你试图猜出颜色组合。打个比方,你可能会猜YRGB。要是猜对某个槽的颜色,则算一次“猜中”;要是只猜对颜色但槽位猜错了,则算一次“伪猜中”。注意,“猜中”不能算入“伪猜中”。

+

给定一种颜色组合 solution和一个猜测 guess,编写一个方法,返回猜中和伪猜中的次数 answer,其中 answer[0]为猜中的次数,answer[1]为伪猜中的次数。

+

面试题 16.16. 部分排序

+

给定一个整数数组,编写一个函数,找出索引 mn,只要将索引区间 [m,n]的元素排好序,整个数组就是有序的。注意:n-m尽量最小,也就是说,找出符合条件的最短序列。函数返回值为 [m,n],若不存在这样的 mn(例如整个数组是有序的),请返回 [-1,-1]

+

面试题 16.17. 连续数列

+

给定一个整数数组,找出总和最大的连续数列,并返回总和。

+

面试题 16.18. 模式匹配

+

你有两个字符串,即 patternvaluepattern字符串由字母 "a""b"组成,用于描述字符串中的模式。例如,字符串 "catcatgocatgo"匹配模式 "aabab"(其中 "cat""a""go""b"),该字符串也匹配像 "a""ab""b"这样的模式。但需注意 "a""b"不能同时表示相同的字符串。编写一个方法判断 value字符串是否匹配 pattern字符串。

+

面试题 16.19. 水域大小

+

你有一个用于表示一片土地的整数矩阵 land,该矩阵中每个点的值代表对应地点的海拔高度。若值为0则表示水域。由垂直、水平或对角连接的水域为池塘。池塘的大小是指相连接的水域的个数。编写一个方法来计算矩阵中所有池塘的大小,返回值需要从小到大排序。

+

面试题 16.20. T9键盘

+

在老式手机上,用户通过数字键盘输入,手机将提供与这些数字相匹配的单词列表。每个数字映射到0至4个字母。给定一个数字序列,实现一个算法来返回匹配单词的列表。你会得到一张含有有效单词的列表。映射如下图所示:

+

+

面试题 16.21. 交换和

+

给定两个整数数组,请交换一对数值(每个数组中取一个数值),使得两个数组所有元素的和相等。

+

返回一个数组,第一个元素是第一个数组中要交换的元素,第二个元素是第二个数组中要交换的元素。若有多个答案,返回任意一个均可。若无满足条件的数值,返回空数组。

+

面试题 16.22. 兰顿蚂蚁

+

一只蚂蚁坐在由白色和黑色方格构成的无限网格上。开始时,网格全白,蚂蚁面向右侧。每行走一步,蚂蚁执行以下操作。

+

(1) 如果在白色方格上,则翻转方格的颜色,向右(顺时针)转 90 度,并向前移动一个单位。
+(2) 如果在黑色方格上,则翻转方格的颜色,向左(逆时针方向)转 90 度,并向前移动一个单位。

+

编写程序来模拟蚂蚁执行的前 K 个动作,并返回最终的网格。

+

网格由数组表示,每个元素是一个字符串,代表网格中的一行,黑色方格由 'X' 表示,白色方格由 '_' 表示,蚂蚁所在的位置由 'L', 'U', 'R', 'D' 表示,分别表示蚂蚁 左、上、右、下 的朝向。只需要返回能够包含蚂蚁走过的所有方格的最小矩形。

+

面试题 16.24. 数对和

+

设计一个算法,找出数组中两数之和为指定值的所有整数对。一个数只能属于一个数对。

+

面试题 16.25. LRU 缓存

+

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

+

它应该支持以下操作: 获取数据 get 和 写入数据 put

+

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
+写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

+

面试题 16.26. 计算器

+

给定一个包含正整数、加(+)、减(-)、乘(*)、除(/)的算数表达式(括号除外),计算其结果。

+

表达式仅包含非负整数,+-*/ 四种运算符和空格  。 整数除法仅保留整数部分。

+

面试题 17

+

面试题 17.01. 不用加号的加法

+

设计一个函数把两个数字相加。不得使用 + 或者其他算术运算符。

+

面试题 17.04. 消失的数字

+

数组 nums包含从 0n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?

+

面试题 17.05. 字母与数字

+

给定一个放有字母和数字的数组,找到最长的子数组,且包含的字母和数字的个数相同。

+

返回该子数组,若存在多个最长子数组,返回左端点下标值最小的子数组。若不存在这样的数组,返回一个空数组。

+

面试题 17.06. 2出现的次数

+

编写一个方法,计算从 0 到 n (含 n) 中数字 2 出现的次数。

+

面试题 17.07. 婴儿名字

+

每年,政府都会公布一万个最常见的婴儿名字和它们出现的频率,也就是同名婴儿的数量。有些名字有多种拼法,例如,John 和 Jon 本质上是相同的名字,但被当成了两个名字公布出来。给定两个列表,一个是名字及对应的频率,另一个是本质相同的名字对。设计一个算法打印出每个真实名字的实际频率。注意,如果 John 和 Jon 是相同的,并且 Jon 和 Johnny 相同,则 John 与 Johnny 也相同,即它们有传递和对称性。

+

在结果列表中,选择** 字典序最小 **的名字作为真实名字。

+

面试题 17.08. 马戏团人塔

+

有个马戏团正在设计叠罗汉的表演节目,一个人要站在另一人的肩膀上。出于实际和美观的考虑,在上面的人要比下面的人矮一点且轻一点。已知马戏团每个人的身高和体重,请编写代码计算叠罗汉最多能叠几个人。

+

面试题 17.09. 第 k 个数

+

有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。

+

面试题 17.10. 主要元素

+

数组中占比超过一半的元素称之为主要元素。给你一个** 整数 **数组,找出其中的主要元素。若没有,返回 -1 。请设计时间复杂度为 O(N) 、空间复杂度为 O(1) 的解决方案。

+

面试题 17.11. 单词距离

+

有个内含单词的超大文本文件,给定任意两个 不同的单词,找出在这个文件中这两个单词的最短距离(相隔单词数)。如果寻找过程在这个文件中会重复多次,而每次寻找的单词不同,你能对此优化吗?

+

面试题 17.12. BiNode

+

二叉树数据结构 TreeNode可用来表示单向链表(其中 left置空,right为下一个链表节点)。实现一个方法,把二叉搜索树转换为单向链表,要求依然符合二叉搜索树的性质,转换操作应是原址的,也就是在原始的二叉搜索树上直接修改。

+

返回转换后的单向链表的头节点。

+

面试题 17.13. 恢复空格

+

哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子 "I reset the computer. It still didn’t boot!"已经变成了 "iresetthecomputeritstilldidntboot"。在处理标点符号和大小写之前,你得先把它断成词语。当然了,你有一本厚厚的词典 dictionary,不过,有些词没在词典里。假设文章用 sentence表示,设计一个算法,把文章断开,要求未识别的字符最少,返回未识别的字符数。

+

面试题 17.14. 最小K个数

+

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

+

面试题 17.15. 最长单词

+

给定一组单词 words,编写一个程序,找出其中的最长单词,且该单词由这组单词中的其他单词组合而成。若有多个长度相同的结果,返回其中字典序最小的一项,若没有符合要求的单词则返回空字符串。

+

面试题 17.16. 按摩师

+

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

+

面试题 17.17. 多次搜索

+

给定一个较长字符串 big和一个包含较短字符串的数组 smalls,设计一个方法,根据 smalls中的每一个较短字符串,对 big进行搜索。输出 smalls中的字符串在 big里出现的所有位置 positions,其中 positions[i]smalls[i]出现的所有位置。

+

面试题 17.18. 最短超串

+

假设你有两个数组,一个长一个短,短的元素均不相同。找到长数组中包含短数组所有的元素的最短子数组,其出现顺序无关紧要。

+

返回最短子数组的左端点和右端点,如有多个满足条件的子数组,返回左端点最小的一个。若不存在,返回空数组。

+

面试题 17.19. 消失的两个数字

+

给定一个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O(N) 时间内只用 O(1) 的空间找到它们吗?

+

以任意顺序返回这两个数字均可。

+

面试题 17.20. 连续中值

+

随机产生数字并传递给一个方法。你能否完成这个方法,在每次产生新值时,寻找当前所有值的中间值(中位数)并保存。

+

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

+

例如,

+

[2,3,4] 的中位数是 3

+

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

+

设计一个支持以下两种操作的数据结构:

+
    +
  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • +
  • double findMedian() - 返回目前所有元素的中位数。
  • +
+

面试题 17.21. 直方图的水量

+

给定一个直方图(也称柱状图),假设有人从上面源源不断地倒水,最后直方图能存多少水量?直方图的宽度为 1。

+

+

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的直方图,在这种情况下,可以接 6 个单位的水(蓝色部分表示水)。

+

面试题 17.22. 单词转换

+

给定字典中的两个词,长度相等。写一个方法,把一个词转换成另一个词, 但是一次只能改变一个字符。每一步得到的新词都必须能在字典中找到。

+

编写一个程序,返回一个可能的转换序列。如有多个可能的转换序列,你可以返回任何一个。

+

面试题 17.23. 最大黑方阵

+

给定一个方阵,其中每个单元(像素)非黑即白。设计一个算法,找出 4 条边皆为黑色像素的最大子方阵。

+

返回一个数组 [r, c, size] ,其中 r, c 分别代表子方阵左上角的行号和列号,size 是子方阵的边长。若有多个满足条件的子方阵,返回 r 最小的,若 r 相同,返回 c 最小的子方阵。若无满足条件的子方阵,返回空数组。

+

面试题 17.24. 最大子矩阵

+

给定一个正整数、负整数和 0 组成的 N × M 矩阵,编写代码找出元素总和最大的子矩阵。

+

返回一个数组 [r1, c1, r2, c2],其中 r1, c1 分别代表子矩阵左上角的行号和列号,r2, c2 分别代表右下角的行号和列号。若有多个满足条件的子矩阵,返回任意一个均可。

+

面试题 17.25. 单词矩阵

+

给定一份单词的清单,设计一个算法,创建由字母组成的面积最大的矩形,其中每一行组成一个单词(自左向右),每一列也组成一个单词(自上而下)。不要求这些单词在清单里连续出现,但要求所有行等长,所有列等高。

+

如果有多个面积最大的矩形,输出任意一个均可。一个单词可以重复使用。

+

面试题 17.26. 稀疏相似度

+

两个(具有不同单词的)文档的交集(intersection)中元素的个数除以并集(union)中元素的个数,就是这两个文档的相似度。例如,{1, 5, 3} 和 {1, 7, 2, 3} 的相似度是 0.4,其中,交集的元素有 2 个,并集的元素有 5 个。给定一系列的长篇文档,每个文档元素各不相同,并与一个 ID 相关联。它们的相似度非常“稀疏”,也就是说任选 2 个文档,相似度都很接近 0。请设计一个算法返回每对文档的 ID 及其相似度。只需输出相似度大于 0 的组合。请忽略空文档。为简单起见,可以假定每个文档由一个含有不同整数的数组表示。

+

输入为一个二维数组 docsdocs[i] 表示 id 为 i 的文档。返回一个数组,其中每个元素是一个字符串,代表每对相似度大于 0 的文档,其格式为 {id1},{id2}: {similarity},其中 id1 为两个文档中较小的 id,similarity 为相似度,精确到小数点后 4 位。以任意顺序返回数组均可。

+ + +
+ +
+
+ + + + + + +
+
+
Leetcode-Interview
+
https://zhangzhao219.github.io/2024/07/12/Leetcode/Leetcode-interview/
+
+
+ +
+
作者
+
Zhang Zhao
+
+ + +
+
发布于
+
2024年7月12日
+
+ + + +
+
许可协议
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/404.html b/404.html new file mode 100644 index 000000000..43027927c --- /dev/null +++ b/404.html @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 页面不存在 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/about/index.html b/about/index.html new file mode 100644 index 000000000..d0b33e9e6 --- /dev/null +++ b/about/index.html @@ -0,0 +1,581 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + About Me - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+ avatar +
+ +
+
+
+ + +
+
+
Zostanzo's Blog
+
A Lazy Programmer
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+

个人信息

+

教育经历

+
    +
  • 2022.09-2025.07 中国科学院计算技术研究所 网络数据科学与技术重点实验室 学术型硕士
  • +
  • 2018.09—2022.06 中南大学 计算机学院 本科
  • +
+

学术论文

+
    +
  • (xxxx 2024)
  • +
  • Li Y, Zhang Z. The First Place Solution of WSDM Cup 2024: Leveraging Large Language Models for Conversational Multi-Doc QA[J]. arXiv preprint arXiv:2402.18385, 2024. (WSDM 2024)
  • +
  • Zhao Zhang, Yiming Li, Jin Zhang, and Hui Xu. 2024. LLM-Driven Knowledge Injection Advances Zero-Shot and Cross-Target Stance Detection. In Proceedings of the 2024 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (Volume 2: Short Papers), pages 371–378, Mexico City, Mexico. Association for Computational Linguistics. (NAACL 2024)
  • +
  • Y. Zhao et al., “ASTF: Visual Abstractions of Time-Varying Patterns in Radio Signals,” in IEEE Transactions on Visualization and Computer Graphics, vol. 29, no. 1, pp. 214-224, Jan. 2023, doi: 10.1109/TVCG.2022.3209469. (IEEE VIS 2022)
  • +
  • Wangbao Zhou, Zonglin Jiang, Ying Zhang, and Zhao Zhang. 2021. Laser Marking Hatch Contour Generation Algorithm. In 2021 2nd International Conference on Artificial Intelligence and Information Systems (ICAIIS 2021). Association for Computing Machinery, New York, NY, USA, Article 16, 1–4. https://doi.org/10.1145/3469213.3469229 (ICAIIS 2021)
  • +
  • W. Zhou et al, “Decomposition rate and interaction of fungi: A random perturbation differential equation approach,” IOP Conference Series.Earth and Environmental Science, vol. 784, (1), 2021. Available: https://www.proquest.com/scholarly-journals/decomposition-rate-interaction-fungi-random/docview/2535567007/se-2. DOI: https://doi.org/10.1088/1755-1315/784/1/012045. (EEWRC 2022)
  • +
+

主要获奖

+

竞赛获奖

+
    +
  • xxxx
  • +
  • xxxx
  • +
  • CHIP2023-PromptCBLUE-参数高效微调赛道 打榜赛 1/607
  • +
  • WSDM 2024 Cup Conversational Multi-Doc QA 冠军
  • +
  • 通义千问AI挑战赛CodeQwen能力算法赛道季军
  • +
  • 第二届百度搜索创新大赛全国总决赛优秀奖(4-10/220)
  • +
  • “华为杯”第二十届中国研究生数学建模竞赛二等奖
  • +
  • SMP 2023 ChatGLM金融大模型挑战赛 季军 6/2294
  • +
  • 2022 CCF-BDCI大数据与计算智能大赛 小样本数据分类任务 全国总决赛三等奖 5/1426
  • +
  • “中国高校计算机大赛-团体程序设计天梯赛”全国总决赛个人二等奖
  • +
  • 中国大学生服务外包创新创业大赛全国总决赛二等奖
  • +
  • 中国大学生计算机设计大赛中南地区赛二等奖
  • +
  • 全国大学生英语竞赛国家级三等奖
  • +
  • 中南大学大学生程序设计竞赛二等奖
  • +
  • 全国大学生数学建模竞赛湖南省三等奖
  • +
  • 亚太地区大学生数学建模竞赛二等奖
  • +
  • Mathematical Contest In Modeling Honorable Mention
  • +
+

体育获奖

+
    +
  • 中国科学院大学计算机科学与技术学院乒乓球男子双打冠军
  • +
+

荣誉称号

+
    +
  • 中国科学院大学三好学生
  • +
  • 本科生国家奖学金
  • +
  • 湖南省普通高等学校优秀毕业生
  • +
  • 中南大学优秀学生
  • +
  • 中南大学一等奖学金
  • +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/07/index.html b/archives/2022/07/index.html new file mode 100644 index 000000000..9aee773f0 --- /dev/null +++ b/archives/2022/07/index.html @@ -0,0 +1,433 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/08/index.html b/archives/2022/08/index.html new file mode 100644 index 000000000..54698da44 --- /dev/null +++ b/archives/2022/08/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/08/page/2/index.html b/archives/2022/08/page/2/index.html new file mode 100644 index 000000000..3495fca69 --- /dev/null +++ b/archives/2022/08/page/2/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/09/index.html b/archives/2022/09/index.html new file mode 100644 index 000000000..17b6689b8 --- /dev/null +++ b/archives/2022/09/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/09/page/2/index.html b/archives/2022/09/page/2/index.html new file mode 100644 index 000000000..45aca6c67 --- /dev/null +++ b/archives/2022/09/page/2/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/09/page/3/index.html b/archives/2022/09/page/3/index.html new file mode 100644 index 000000000..e310eec75 --- /dev/null +++ b/archives/2022/09/page/3/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/09/page/4/index.html b/archives/2022/09/page/4/index.html new file mode 100644 index 000000000..f15de58d4 --- /dev/null +++ b/archives/2022/09/page/4/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/09/page/5/index.html b/archives/2022/09/page/5/index.html new file mode 100644 index 000000000..7ebd38988 --- /dev/null +++ b/archives/2022/09/page/5/index.html @@ -0,0 +1,397 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/10/index.html b/archives/2022/10/index.html new file mode 100644 index 000000000..06d719781 --- /dev/null +++ b/archives/2022/10/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/10/page/2/index.html b/archives/2022/10/page/2/index.html new file mode 100644 index 000000000..0d83f28c9 --- /dev/null +++ b/archives/2022/10/page/2/index.html @@ -0,0 +1,439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/11/index.html b/archives/2022/11/index.html new file mode 100644 index 000000000..44a937d0c --- /dev/null +++ b/archives/2022/11/index.html @@ -0,0 +1,421 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/12/index.html b/archives/2022/12/index.html new file mode 100644 index 000000000..18d3002a0 --- /dev/null +++ b/archives/2022/12/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/12/page/2/index.html b/archives/2022/12/page/2/index.html new file mode 100644 index 000000000..fb9f38b13 --- /dev/null +++ b/archives/2022/12/page/2/index.html @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/index.html b/archives/2022/index.html new file mode 100644 index 000000000..4a6541154 --- /dev/null +++ b/archives/2022/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/10/index.html b/archives/2022/page/10/index.html new file mode 100644 index 000000000..f906f2c62 --- /dev/null +++ b/archives/2022/page/10/index.html @@ -0,0 +1,445 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/2/index.html b/archives/2022/page/2/index.html new file mode 100644 index 000000000..af485eab6 --- /dev/null +++ b/archives/2022/page/2/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/3/index.html b/archives/2022/page/3/index.html new file mode 100644 index 000000000..d18ae4532 --- /dev/null +++ b/archives/2022/page/3/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/4/index.html b/archives/2022/page/4/index.html new file mode 100644 index 000000000..cd69cca23 --- /dev/null +++ b/archives/2022/page/4/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/5/index.html b/archives/2022/page/5/index.html new file mode 100644 index 000000000..f2c18bbc6 --- /dev/null +++ b/archives/2022/page/5/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/6/index.html b/archives/2022/page/6/index.html new file mode 100644 index 000000000..e1488ec26 --- /dev/null +++ b/archives/2022/page/6/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/7/index.html b/archives/2022/page/7/index.html new file mode 100644 index 000000000..e6f80827c --- /dev/null +++ b/archives/2022/page/7/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/8/index.html b/archives/2022/page/8/index.html new file mode 100644 index 000000000..5949a8446 --- /dev/null +++ b/archives/2022/page/8/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/9/index.html b/archives/2022/page/9/index.html new file mode 100644 index 000000000..cba6fe4b9 --- /dev/null +++ b/archives/2022/page/9/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/01/index.html b/archives/2023/01/index.html new file mode 100644 index 000000000..e19a79919 --- /dev/null +++ b/archives/2023/01/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/01/page/2/index.html b/archives/2023/01/page/2/index.html new file mode 100644 index 000000000..f89367a8a --- /dev/null +++ b/archives/2023/01/page/2/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/02/index.html b/archives/2023/02/index.html new file mode 100644 index 000000000..2eab7ad95 --- /dev/null +++ b/archives/2023/02/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/02/page/2/index.html b/archives/2023/02/page/2/index.html new file mode 100644 index 000000000..dd3869f3e --- /dev/null +++ b/archives/2023/02/page/2/index.html @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html new file mode 100644 index 000000000..2e9c7cffc --- /dev/null +++ b/archives/2023/03/index.html @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/05/index.html b/archives/2023/05/index.html new file mode 100644 index 000000000..e38993833 --- /dev/null +++ b/archives/2023/05/index.html @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 142 篇文章

+
+ + + + +

2023

+ + + +
杂谈-20230502
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/08/index.html b/archives/2023/08/index.html new file mode 100644 index 000000000..6f5091eaf --- /dev/null +++ b/archives/2023/08/index.html @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 142 篇文章

+
+ + + + +

2023

+ + + +
Pytorch分布式训练
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/10/index.html b/archives/2023/10/index.html new file mode 100644 index 000000000..72b23a7b4 --- /dev/null +++ b/archives/2023/10/index.html @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 142 篇文章

+
+ + + + +

2023

+ + + +
Stance Detection
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html new file mode 100644 index 000000000..9d09e9c79 --- /dev/null +++ b/archives/2023/11/index.html @@ -0,0 +1,397 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 142 篇文章

+
+ + + + +

2023

+ + + +
杂谈-20231121
+
+ + + + +
杂谈-20231119
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/index.html b/archives/2023/index.html new file mode 100644 index 000000000..276737e19 --- /dev/null +++ b/archives/2023/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html new file mode 100644 index 000000000..00a73a88c --- /dev/null +++ b/archives/2023/page/2/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/page/3/index.html b/archives/2023/page/3/index.html new file mode 100644 index 000000000..541907f9d --- /dev/null +++ b/archives/2023/page/3/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/page/4/index.html b/archives/2023/page/4/index.html new file mode 100644 index 000000000..344f52af0 --- /dev/null +++ b/archives/2023/page/4/index.html @@ -0,0 +1,421 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/04/index.html b/archives/2024/04/index.html new file mode 100644 index 000000000..37582ce77 --- /dev/null +++ b/archives/2024/04/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/06/index.html b/archives/2024/06/index.html new file mode 100644 index 000000000..b28997907 --- /dev/null +++ b/archives/2024/06/index.html @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/index.html b/archives/2024/index.html new file mode 100644 index 000000000..42a15aba5 --- /dev/null +++ b/archives/2024/index.html @@ -0,0 +1,433 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 000000000..34149deb5 --- /dev/null +++ b/archives/index.html @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/10/index.html b/archives/page/10/index.html new file mode 100644 index 000000000..3170a7d7b --- /dev/null +++ b/archives/page/10/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/11/index.html b/archives/page/11/index.html new file mode 100644 index 000000000..bd9449c0f --- /dev/null +++ b/archives/page/11/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/12/index.html b/archives/page/12/index.html new file mode 100644 index 000000000..f2a7ae692 --- /dev/null +++ b/archives/page/12/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/13/index.html b/archives/page/13/index.html new file mode 100644 index 000000000..b1d3e268b --- /dev/null +++ b/archives/page/13/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/14/index.html b/archives/page/14/index.html new file mode 100644 index 000000000..925796234 --- /dev/null +++ b/archives/page/14/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/15/index.html b/archives/page/15/index.html new file mode 100644 index 000000000..6e2696da3 --- /dev/null +++ b/archives/page/15/index.html @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 142 篇文章

+
+ + + + +

2022

+ + + +
2022保研经历
+
+ + + + +
隐藏博客
+
+ +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/2/index.html b/archives/page/2/index.html new file mode 100644 index 000000000..9c6f6cc5c --- /dev/null +++ b/archives/page/2/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/3/index.html b/archives/page/3/index.html new file mode 100644 index 000000000..e352051a9 --- /dev/null +++ b/archives/page/3/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/4/index.html b/archives/page/4/index.html new file mode 100644 index 000000000..314c39a23 --- /dev/null +++ b/archives/page/4/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/5/index.html b/archives/page/5/index.html new file mode 100644 index 000000000..b017cf82f --- /dev/null +++ b/archives/page/5/index.html @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/6/index.html b/archives/page/6/index.html new file mode 100644 index 000000000..430c24c47 --- /dev/null +++ b/archives/page/6/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/7/index.html b/archives/page/7/index.html new file mode 100644 index 000000000..792124bc9 --- /dev/null +++ b/archives/page/7/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/8/index.html b/archives/page/8/index.html new file mode 100644 index 000000000..afeb284f0 --- /dev/null +++ b/archives/page/8/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/9/index.html b/archives/page/9/index.html new file mode 100644 index 000000000..c019d0c4a --- /dev/null +++ b/archives/page/9/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Experience/index.html b/categories/Experience/index.html new file mode 100644 index 000000000..018c5a3a7 --- /dev/null +++ b/categories/Experience/index.html @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Experience - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2022

+ + + +
2022保研经历
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Life/index.html b/categories/Life/index.html new file mode 100644 index 000000000..2e33fe979 --- /dev/null +++ b/categories/Life/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Life - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/index.html b/categories/Study/index.html new file mode 100644 index 000000000..c1037d4ed --- /dev/null +++ b/categories/Study/index.html @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/10/index.html b/categories/Study/page/10/index.html new file mode 100644 index 000000000..f6f639230 --- /dev/null +++ b/categories/Study/page/10/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/11/index.html b/categories/Study/page/11/index.html new file mode 100644 index 000000000..4ef8bb32f --- /dev/null +++ b/categories/Study/page/11/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/12/index.html b/categories/Study/page/12/index.html new file mode 100644 index 000000000..52c5267f0 --- /dev/null +++ b/categories/Study/page/12/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/13/index.html b/categories/Study/page/13/index.html new file mode 100644 index 000000000..d0f9828e8 --- /dev/null +++ b/categories/Study/page/13/index.html @@ -0,0 +1,445 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/2/index.html b/categories/Study/page/2/index.html new file mode 100644 index 000000000..31280ea24 --- /dev/null +++ b/categories/Study/page/2/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/3/index.html b/categories/Study/page/3/index.html new file mode 100644 index 000000000..db9cdbee1 --- /dev/null +++ b/categories/Study/page/3/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/4/index.html b/categories/Study/page/4/index.html new file mode 100644 index 000000000..928a891f7 --- /dev/null +++ b/categories/Study/page/4/index.html @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/5/index.html b/categories/Study/page/5/index.html new file mode 100644 index 000000000..cf3dbe8e6 --- /dev/null +++ b/categories/Study/page/5/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/6/index.html b/categories/Study/page/6/index.html new file mode 100644 index 000000000..9a892d260 --- /dev/null +++ b/categories/Study/page/6/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/7/index.html b/categories/Study/page/7/index.html new file mode 100644 index 000000000..62ffca291 --- /dev/null +++ b/categories/Study/page/7/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/8/index.html b/categories/Study/page/8/index.html new file mode 100644 index 000000000..8918519f7 --- /dev/null +++ b/categories/Study/page/8/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Study/page/9/index.html b/categories/Study/page/9/index.html new file mode 100644 index 000000000..335475ae8 --- /dev/null +++ b/categories/Study/page/9/index.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Study - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Tools/index.html b/categories/Tools/index.html new file mode 100644 index 000000000..2d7c6e8f5 --- /dev/null +++ b/categories/Tools/index.html @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Tools - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
常用软件常用命令
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Travel/index.html b/categories/Travel/index.html new file mode 100644 index 000000000..49b16417d --- /dev/null +++ b/categories/Travel/index.html @@ -0,0 +1,397 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Travel - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 2 篇文章

+
+ + + + +

2022

+ + + +
Travel List
+
+ + + + +
Trip To Qingdao
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 000000000..42476a086 --- /dev/null +++ b/categories/index.html @@ -0,0 +1,759 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/gitalk.css b/css/gitalk.css new file mode 100644 index 000000000..a268f1d28 --- /dev/null +++ b/css/gitalk.css @@ -0,0 +1,546 @@ +@font-face { + font-family: octicons-link; + src: url(data:font/woff;charset=utf-8;base64,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) format('woff'); +} +/* variables */ +/* functions & mixins */ +/* variables - calculated */ +/* styles */ +.gt-container { + -webkit-box-sizing: border-box; + box-sizing: border-box; + font-size: 16px; + /* loader */ + /* error */ + /* initing */ + /* no int */ + /* link */ + /* meta */ + /* popup */ + /* header */ + /* comments */ + /* comment */ +} +.gt-container * { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.gt-container a { + color: #6190e8; +} +.gt-container a:hover { + color: #81a6ed; + border-color: #81a6ed; +} +.gt-container a.is--active { + color: #333; + cursor: default !important; +} +.gt-container a.is--active:hover { + color: #333; +} +.gt-container .hide { + display: none !important; +} +.gt-container .gt-svg { + display: inline-block; + width: 1em; + height: 1em; + vertical-align: sub; +} +.gt-container .gt-svg svg { + width: 100%; + height: 100%; + fill: #6190e8; +} +.gt-container .gt-ico { + display: inline-block; +} +.gt-container .gt-ico-text { + margin-left: 0.3125em; +} +.gt-container .gt-ico-github { + width: 100%; + height: 100%; +} +.gt-container .gt-ico-github .gt-svg { + width: 100%; + height: 100%; +} +.gt-container .gt-ico-github svg { + fill: inherit; +} +.gt-container .gt-spinner { + position: relative; +} +.gt-container .gt-spinner::before { + content: ''; + -webkit-box-sizing: border-box; + box-sizing: border-box; + position: absolute; + top: 3px; + width: 0.75em; + height: 0.75em; + margin-top: -0.1875em; + margin-left: -0.375em; + border-radius: 50%; + border: 1px solid #fff; + border-top-color: #6190e8; + -webkit-animation: gt-kf-rotate 0.6s linear infinite; + animation: gt-kf-rotate 0.6s linear infinite; +} +.gt-container .gt-loader { + position: relative; + border: 1px solid #999; + -webkit-animation: ease gt-kf-rotate 1.5s infinite; + animation: ease gt-kf-rotate 1.5s infinite; + display: inline-block; + font-style: normal; + width: 1.75em; + height: 1.75em; + line-height: 1.75em; + border-radius: 50%; +} +.gt-container .gt-loader:before { + content: ''; + position: absolute; + display: block; + top: 0; + left: 50%; + margin-top: -0.1875em; + margin-left: -0.1875em; + width: 0.375em; + height: 0.375em; + background-color: #999; + border-radius: 50%; +} +.gt-container .gt-avatar { + display: inline-block; + width: 3.125em; + height: 3.125em; +} +@media (max-width: 479px) { + .gt-container .gt-avatar { + width: 2em; + height: 2em; + } +} +.gt-container .gt-avatar img { + width: 100%; + height: auto; + border-radius: 3px; +} +.gt-container .gt-avatar-github { + width: 3em; + height: 3em; +} +@media (max-width: 479px) { + .gt-container .gt-avatar-github { + width: 1.875em; + height: 1.875em; + } +} +.gt-container .gt-btn { + padding: 0.75em 1.25em; + display: inline-block; + line-height: 1; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + border: 1px solid #6190e8; + border-radius: 5px; + background-color: #6190e8; + color: #fff; + outline: none; + font-size: 0.75em; +} +.gt-container .gt-btn-text { + font-weight: 400; +} +.gt-container .gt-btn-loading { + position: relative; + margin-left: 0.5em; + display: inline-block; + width: 0.75em; + height: 1em; + vertical-align: top; +} +.gt-container .gt-btn.is--disable { + cursor: not-allowed; + opacity: 0.5; +} +.gt-container .gt-btn-login { + margin-right: 0; +} +.gt-container .gt-btn-preview { + background-color: #fff; + color: #6190e8; +} +.gt-container .gt-btn-preview:hover { + background-color: #f2f2f2; + border-color: #81a6ed; +} +.gt-container .gt-btn-public:hover { + background-color: #81a6ed; + border-color: #81a6ed; +} +.gt-container .gt-error { + text-align: center; + margin: 0.625em; + color: #ff3860; +} +.gt-container .gt-initing { + padding: 1.25em 0; + text-align: center; +} +.gt-container .gt-initing-text { + margin: 0.625em auto; + font-size: 92%; +} +.gt-container .gt-no-init { + padding: 1.25em 0; + text-align: center; +} +.gt-container .gt-link { + border-bottom: 1px dotted #6190e8; +} +.gt-container .gt-link-counts, +.gt-container .gt-link-project { + text-decoration: none; +} +.gt-container .gt-meta { + margin: 1.25em 0; + padding: 1em 0; + position: relative; + border-bottom: 1px solid #e9e9e9; + font-size: 1em; + position: relative; + z-index: 10; +} +.gt-container .gt-meta:before, +.gt-container .gt-meta:after { + content: " "; + display: table; +} +.gt-container .gt-meta:after { + clear: both; +} +.gt-container .gt-counts { + margin: 0 0.625em 0 0; +} +.gt-container .gt-user { + float: right; + margin: 0; + font-size: 92%; +} +.gt-container .gt-user-pic { + width: 16px; + height: 16px; + vertical-align: top; + margin-right: 0.5em; +} +.gt-container .gt-user-inner { + display: inline-block; + cursor: pointer; +} +.gt-container .gt-user .gt-ico { + margin: 0 0 0 0.3125em; +} +.gt-container .gt-user .gt-ico svg { + fill: inherit; +} +.gt-container .gt-user .is--poping .gt-ico svg { + fill: #6190e8; +} +.gt-container .gt-version { + color: #a1a1a1; + margin-left: 0.375em; +} +.gt-container .gt-copyright { + margin: 0 0.9375em 0.5em; + border-top: 1px solid #e9e9e9; + padding-top: 0.5em; +} +.gt-container .gt-popup { + position: absolute; + right: 0; + top: 2.375em; + background: #fff; + display: inline-block; + border: 1px solid #e9e9e9; + padding: 0.625em 0; + font-size: 0.875em; + letter-spacing: 0.5px; +} +.gt-container .gt-popup .gt-action { + cursor: pointer; + display: block; + margin: 0.5em 0; + padding: 0 1.125em; + position: relative; + text-decoration: none; +} +.gt-container .gt-popup .gt-action.is--active:before { + content: ''; + width: 0.25em; + height: 0.25em; + background: #6190e8; + position: absolute; + left: 0.5em; + top: 0.4375em; +} +.gt-container .gt-header { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} +.gt-container .gt-header-comment { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + margin-left: 1.25em; +} +@media (max-width: 479px) { + .gt-container .gt-header-comment { + margin-left: 0.875em; + } +} +.gt-container .gt-header-textarea { + padding: 0.75em; + display: block; + -webkit-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + min-height: 5.125em; + max-height: 15em; + border-radius: 5px; + border: 1px solid rgba(0,0,0,0.1); + font-size: 0.875em; + word-wrap: break-word; + resize: vertical; + background-color: #f6f6f6; + outline: none; + -webkit-transition: all 0.25s ease; + transition: all 0.25s ease; +} +.gt-container .gt-header-textarea:hover { + background-color: #fbfbfb; +} +.gt-container .gt-header-preview { + padding: 0.75em; + border-radius: 5px; + border: 1px solid rgba(0,0,0,0.1); + background-color: #f6f6f6; +} +.gt-container .gt-header-controls { + position: relative; + margin: 0.75em 0 0; +} +.gt-container .gt-header-controls:before, +.gt-container .gt-header-controls:after { + content: " "; + display: table; +} +.gt-container .gt-header-controls:after { + clear: both; +} +@media (max-width: 479px) { + .gt-container .gt-header-controls { + margin: 0; + } +} +.gt-container .gt-header-controls-tip { + font-size: 0.875em; + color: #6190e8; + text-decoration: none; + vertical-align: sub; +} +@media (max-width: 479px) { + .gt-container .gt-header-controls-tip { + display: none; + } +} +.gt-container .gt-header-controls .gt-btn { + float: right; + margin-left: 1.25em; +} +@media (max-width: 479px) { + .gt-container .gt-header-controls .gt-btn { + float: none; + width: 100%; + margin: 0.75em 0 0; + } +} +.gt-container:after { + content: ''; + position: fixed; + bottom: 100%; + left: 0; + right: 0; + top: 0; + opacity: 0; +} +.gt-container.gt-input-focused { + position: relative; +} +.gt-container.gt-input-focused:after { + content: ''; + position: fixed; + bottom: 0%; + left: 0; + right: 0; + top: 0; + background: #000; + opacity: 0.6; + -webkit-transition: opacity 0.3s, bottom 0s; + transition: opacity 0.3s, bottom 0s; + z-index: 9999; +} +.gt-container.gt-input-focused .gt-header-comment { + z-index: 10000; +} +.gt-container .gt-comments { + padding-top: 1.25em; +} +.gt-container .gt-comments-null { + text-align: center; +} +.gt-container .gt-comments-controls { + margin: 1.25em 0; + text-align: center; +} +.gt-container .gt-comment { + position: relative; + padding: 0.625em 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} +.gt-container .gt-comment-content { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + margin-left: 1.25em; + padding: 0.75em 1em; + background-color: #f9f9f9; + overflow: auto; + -webkit-transition: all ease 0.25s; + transition: all ease 0.25s; +} +.gt-container .gt-comment-content:hover { + -webkit-box-shadow: 0 0.625em 3.75em 0 #f4f4f4; + box-shadow: 0 0.625em 3.75em 0 #f4f4f4; +} +@media (max-width: 479px) { + .gt-container .gt-comment-content { + margin-left: 0.875em; + padding: 0.625em 0.75em; + } +} +.gt-container .gt-comment-header { + margin-bottom: 0.5em; + font-size: 0.875em; + position: relative; +} +.gt-container .gt-comment-block-1 { + float: right; + height: 1.375em; + width: 2em; +} +.gt-container .gt-comment-block-2 { + float: right; + height: 1.375em; + width: 4em; +} +.gt-container .gt-comment-username { + font-weight: 500; + color: #6190e8; + text-decoration: none; +} +.gt-container .gt-comment-username:hover { + text-decoration: underline; +} +.gt-container .gt-comment-text { + margin-left: 0.5em; + color: #a1a1a1; +} +.gt-container .gt-comment-date { + margin-left: 0.5em; + color: #a1a1a1; +} +.gt-container .gt-comment-like, +.gt-container .gt-comment-edit, +.gt-container .gt-comment-reply { + position: absolute; + height: 1.375em; +} +.gt-container .gt-comment-like:hover, +.gt-container .gt-comment-edit:hover, +.gt-container .gt-comment-reply:hover { + cursor: pointer; +} +.gt-container .gt-comment-like { + top: 0; + right: 2em; +} +.gt-container .gt-comment-edit, +.gt-container .gt-comment-reply { + top: 0; + right: 0; +} +.gt-container .gt-comment-body { + color: #333 !important; +} +.gt-container .gt-comment-body .email-hidden-toggle a { + display: inline-block; + height: 12px; + padding: 0 9px; + font-size: 12px; + font-weight: 600; + line-height: 6px; + color: #444d56; + text-decoration: none; + vertical-align: middle; + background: #dfe2e5; + border-radius: 1px; +} +.gt-container .gt-comment-body .email-hidden-toggle a:hover { + background-color: #c6cbd1; +} +.gt-container .gt-comment-body .email-hidden-reply { + display: none; + white-space: pre-wrap; +} +.gt-container .gt-comment-body .email-hidden-reply .email-signature-reply { + padding: 0 15px; + margin: 15px 0; + color: #586069; + border-left: 4px solid #dfe2e5; +} +.gt-container .gt-comment-body .email-hidden-reply.expanded { + display: block; +} +.gt-container .gt-comment-admin .gt-comment-content { + background-color: #f6f9fe; +} +@-webkit-keyframes gt-kf-rotate { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes gt-kf-rotate { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/css/hbe.style.css b/css/hbe.style.css new file mode 100644 index 000000000..060f1f83b --- /dev/null +++ b/css/hbe.style.css @@ -0,0 +1,749 @@ +.hbe, +.hbe:after, +.hbe:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.hbe-container{ + margin: 0 auto; + overflow: hidden; +} +.hbe-content { + text-align: center; + font-size: 150%; + padding: 1em 0; +} + +.hbe-input { + position: relative; + z-index: 1; + display: inline-block; + margin: 1em; + width: 80%; + min-width: 200px; + vertical-align: top; +} + +.hbe-input-field { + line-height: normal; + font-size: 100%; + margin: 0; + position: relative; + display: block; + float: right; + padding: 0.8em; + width: 60%; + border: none; + border-radius: 0; + background: #f0f0f0; + color: #aaa; + font-weight: 400; + font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; + -webkit-appearance: none; /* for box shadows to show on iOS */ +} + +.hbe-input-field:focus { + outline: none; +} + +.hbe-input-label { + display: inline-block; + float: right; + padding: 0 1em; + width: 40%; + color: #696969; + font-weight: bold; + font-size: 70.25%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.hbe-input-label-content { + position: relative; + display: block; + padding: 1.6em 0; + width: 100%; +} + +.hbe-graphic { + position: absolute; + top: 0; + left: 0; + fill: none; +} + +/* hbe button in post page */ +.hbe-button { + width: 130px; + height: 40px; + background: linear-gradient(to bottom, #4eb5e5 0%,#389ed5 100%); /* W3C */ + border: none; + border-radius: 5px; + position: relative; + border-bottom: 4px solid #2b8bc6; + color: #fbfbfb; + font-weight: 600; + font-family: 'Open Sans', sans-serif; + text-shadow: 1px 1px 1px rgba(0,0,0,.4); + font-size: 15px; + text-align: left; + text-indent: 5px; + box-shadow: 0px 3px 0px 0px rgba(0,0,0,.2); + cursor: pointer; + + display: block; + margin: 0 auto; + margin-bottom: 20px; +} + +.hbe-button:active { + box-shadow: 0px 2px 0px 0px rgba(0,0,0,.2); + top: 1px; +} + +.hbe-button:after { + content: ""; + width: 0; + height: 0; + display: block; + border-top: 20px solid #187dbc; + border-bottom: 20px solid #187dbc; + border-left: 16px solid transparent; + border-right: 20px solid #187dbc; + position: absolute; + opacity: 0.6; + right: 0; + top: 0; + border-radius: 0 5px 5px 0; +} +/* hbe button in post page */ + +/* default theme {{{ */ +.hbe-input-default { + overflow: hidden; +} + +.hbe-input-field-default { + width: 100%; + background: transparent; + padding: 0.5em; + margin-bottom: 2em; + color: #f9f7f6; + z-index: 100; + opacity: 0; +} + +.hbe-input-label-default { + width: 100%; + position: absolute; + text-align: left; + padding: 0.5em 0; + pointer-events: none; + font-size: 1em; +} + +.hbe-input-label-default::before, +.hbe-input-label-default::after { + content: ''; + position: absolute; + width: 100%; + left: 0; +} + +.hbe-input-label-default::before { + height: 100%; + background: #666666; + top: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; +} + +.hbe-input-label-default::after { + height: 2px; + background: #666666; + top: 100%; + -webkit-transition: opacity 0.2s; + transition: opacity 0.2s; +} + +.hbe-input-label-content-default { + padding: 0; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + -webkit-transition: -webkit-transform 0.2s, color 0.2s; + transition: transform 0.2s, color 0.2s; +} + +.hbe-input-field-default:focus, +.hbe-input--filled .hbe-input-field-default { + opacity: 1; + -webkit-transition: opacity 0s 0.2s; + transition: opacity 0s 0.2s; +} + +.hbe-input-label-default::before, +.hbe-input-label-default::after, +.hbe-input-label-content-default, +.hbe-input-field-default:focus, +.hbe-input--filled .hbe-input-field-default { + -webkit-transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); + transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); +} + +.hbe-input-field-default:focus + .hbe-input-label-default::before, +.hbe-input--filled .hbe-input-label-default::before { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} + +.hbe-input-field-default:focus + .hbe-input-label-default::after, +.hbe-input--filled .hbe-input-label-default::after { + opacity: 0; +} + +.hbe-input-field-default:focus + .hbe-input-label-default .hbe-input-label-content-default, +.hbe-input--filled .hbe-input-label-default .hbe-input-label-content-default { + color: #555555; + -webkit-transform: translate3d(0, 2.1em, 0) scale3d(0.65, 0.65, 1); + transform: translate3d(0, 2.1em, 0) scale3d(0.65, 0.65, 1); +} +/* default theme }}} */ + +/* up theme {{{ */ +.hbe-input-up { + overflow: hidden; + padding-top: 2em; +} + +.hbe-input-field-up { + width: 100%; + background: transparent; + opacity: 0; + padding: 0.35em; + z-index: 100; + color: #837482; +} + +.hbe-input-label-up { + width: 100%; + bottom: 0; + position: absolute; + pointer-events: none; + text-align: left; + color: #8E9191; + padding: 0 0.5em; +} + +.hbe-input-label-up::before { + content: ''; + position: absolute; + width: 100%; + height: 4em; + top: 100%; + left: 0; + background: #fff; + border-top: 4px solid #9B9F9F; + -webkit-transform: translate3d(0, -3px, 0); + transform: translate3d(0, -3px, 0); + -webkit-transition: -webkit-transform 0.4s; + transition: transform 0.4s; + -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); + transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); +} + +.hbe-input-label-content-up { + padding: 0.5em 0; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transition: -webkit-transform 0.4s, color 0.4s; + transition: transform 0.4s, color 0.4s; + -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); + transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); +} + +.hbe-input-field-up:focus, +.input--filled .hbe-input-field-up { + cursor: text; + opacity: 1; + -webkit-transition: opacity 0s 0.4s; + transition: opacity 0s 0.4s; +} + +.hbe-input-field-up:focus + .hbe-input-label-up::before, +.input--filled .hbe-input-label-up::before { + -webkit-transition-delay: 0.05s; + transition-delay: 0.05s; + -webkit-transform: translate3d(0, -3.3em, 0); + transform: translate3d(0, -3.3em, 0); +} + +.hbe-input-field-up:focus + .hbe-input-label-up .hbe-input-label-content-up, +.input--filled .hbe-input-label-content-up { + color: #6B6E6E; + -webkit-transform: translate3d(0, -3.3em, 0) scale3d(0.81, 0.81, 1); + transform: translate3d(0, -3.3em, 0) scale3d(0.81, 0.81, 1); +} +/* up theme }}} */ + +/* wave theme {{{ */ +.hbe-input-wave { + overflow: hidden; + padding-top: 1em; +} + +.hbe-input-field-wave { + padding: 0.5em 0em 0.25em; + width: 100%; + background: transparent; + color: #9da8b2; + font-size: 1.25em; +} + +.hbe-input-label-wave { + position: absolute; + top: 0.95em; + font-size: 0.85em; + left: 0; + display: block; + width: 100%; + text-align: left; + padding: 0em; + pointer-events: none; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + -webkit-transition: -webkit-transform 0.2s 0.15s, color 1s; + transition: transform 0.2s 0.15s, color 1s; + -webkit-transition-timing-function: ease-out; + transition-timing-function: ease-out; +} + +.hbe-graphic-wave { + stroke: #92989e; + pointer-events: none; + -webkit-transition: -webkit-transform 0.7s, stroke 0.7s; + transition: transform 0.7s, stroke 0.7s; + -webkit-transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); + transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); +} + +.hbe-input-field-wave:focus + .hbe-input-label-wave, +.input--filled .hbe-input-label-wave { + color: #333; + -webkit-transform: translate3d(0, -1.25em, 0) scale3d(0.75, 0.75, 1); + transform: translate3d(0, -1.25em, 0) scale3d(0.75, 0.75, 1); +} + +.hbe-input-field-wave:focus ~ .hbe-graphic-wave, +.input--filled .graphic-wave { + stroke: #333; + -webkit-transform: translate3d(-66.6%, 0, 0); + transform: translate3d(-66.6%, 0, 0); +} +/* wave theme }}} */ + +/* flip theme {{{ */ +.hbe-input-field-flip { + width: 100%; + background-color: #d0d1d0; + border: 2px solid transparent; + -webkit-transition: background-color 0.25s, border-color 0.25s; + transition: background-color 0.25s, border-color 0.25s; +} + +.hbe-input-label-flip { + width: 100%; + text-align: left; + position: absolute; + bottom: 100%; + pointer-events: none; + overflow: hidden; + padding: 0 1.25em; + -webkit-transform: translate3d(0, 3em, 0); + transform: translate3d(0, 3em, 0); + -webkit-transition: -webkit-transform 0.25s; + transition: transform 0.25s ; + -webkit-transition-timing-function: ease-in-out; + transition-timing-function: ease-in-out; +} + +.hbe-input-label-content-flip { + color: #8B8C8B; + padding: 0.25em 0; + -webkit-transition: -webkit-transform 0.25s; + transition: transform 0.25s; + -webkit-transition-timing-function: ease-in-out; + transition-timing-function: ease-in-out; +} + +.hbe-input-label-content-flip::after { + content: attr(data-content); + position: absolute; + font-weight: 800; + bottom: 100%; + left: 0; + height: 100%; + width: 100%; + color: #666666; + padding: 0.25em 0; + letter-spacing: 1px; + font-size: 1em; +} + +.hbe-input-field-flip:focus + .hbe-input-label-flip, +.input--filled .hbe-input-label-flip { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} + +.hbe-input-field-flip:focus + .hbe-input-label-flip .hbe-input-label-content-flip, +.input--filled .hbe-input-label-content-flip { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); +} + +.hbe-input-field-flip:focus + .hbe-input-field-flip, +.input--filled .hbe-input-field-flip { + background-color: transparent; + border-color: #666666; +} +/* flip theme }}} */ + +/* xray theme {{{ */ +.hbe-input-xray { + overflow: hidden; + padding-bottom: 2.5em; +} + +.hbe-input-field-xray { + padding: 0; + margin-top: 1.2em; + width: 100%; + background: transparent; + color: #84AF9B ; + font-size: 1.55em; +} + +.hbe-input-label-xray { + position: absolute; + top: 2em; + left: 0; + display: block; + width: 100%; + text-align: left; + padding: 0em; + letter-spacing: 1px; + color: #84AF9B ; + pointer-events: none; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + -webkit-transition: -webkit-transform 0.2s 0.1s, color 0.3s; + transition: transform 0.2s 0.1s, color 0.3s; + -webkit-transition-timing-function: ease-out; + transition-timing-function: ease-out; +} + +.hbe-graphic-xray { + stroke: #84AF9B ; + pointer-events: none; + stroke-width: 2px; + top: 1.25em; + bottom: 0px; + height: 3.275em; + -webkit-transition: -webkit-transform 0.7s, stroke 0.7s; + transition: transform 0.7s, stroke 0.7s; + -webkit-transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); + transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); +} + +.hbe-input-field-xray:focus + .hbe-input-label-xray, +.input--filled .hbe-input-label-xray { + color: #84AF9B ; + -webkit-transform: translate3d(0, 3.5em, 0) scale3d(0.85, 0.85, 1); + transform: translate3d(0, 3.5em, 0) scale3d(0.85, 0.85, 1); +} + +.hbe-input-field-xray:focus ~ .hbe-graphic-xray, +.input--filled .graphic-xray { + stroke: #84AF9B ; + -webkit-transform: translate3d(-66.6%, 0, 0); + transform: translate3d(-66.6%, 0, 0); +} +/* xray theme }}} */ + +/* blink theme {{{ */ +.hbe-input-blink { + padding-top: 1em; +} + +.hbe-input-field-blink { + width: 100%; + padding: 0.8em 0.5em; + background: transparent; + border: 2px solid; + color: #8781bd; + -webkit-transition: border-color 0.25s; + transition: border-color 0.25s; +} + +.hbe-input-label-blink { + width: 100%; + position: absolute; + top: 0; + text-align: left; + overflow: hidden; + padding: 0; + pointer-events: none; + -webkit-transform: translate3d(0, 3em, 0); + transform: translate3d(0, 3em, 0); +} + +.hbe-input-label-content-blink { + padding: 0 1em; + font-weight: 400; + color: #b5b5b5; +} + +.hbe-input-label-content-blink::after { + content: attr(data-content); + position: absolute; + top: -200%; + left: 0; + color: #8781bd ; + font-weight: 800; +} + +.hbe-input-field-blink:focus, +.input--filled .hbe-input-field-blink { + border-color: #8781bd ; +} + +.hbe-input-field-blink:focus + .hbe-input-label-blink, +.input--filled .hbe-input-label-blink { + -webkit-animation: anim-blink-1 0.25s forwards; + animation: anim-blink-1 0.25s forwards; +} + +.hbe-input-field-blink:focus + .hbe-input-label-blink .hbe-input-label-content-blink, +.input--filled .hbe-input-label-content-blink { + -webkit-animation: anim-blink-2 0.25s forwards ease-in; + animation: anim-blink-2 0.25s forwards ease-in; +} + +@-webkit-keyframes anim-blink-1 { + 0%, 70% { + -webkit-transform: translate3d(0, 3em, 0); + transform: translate3d(0, 3em, 0); + } + 71%, 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@-webkit-keyframes anim-blink-2 { + 0% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 70%, 71% { + -webkit-transform: translate3d(0, 125%, 0); + transform: translate3d(0, 125%, 0); + opacity: 0; + -webkit-animation-timing-function: ease-out; + } + 100% { + color: transparent; + -webkit-transform: translate3d(0, 200%, 0); + transform: translate3d(0, 200%, 0); + } +} + +@keyframes anim-blink-1 { + 0%, 70% { + -webkit-transform: translate3d(0, 3em, 0); + transform: translate3d(0, 3em, 0); + } + 71%, 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes anim-blink-2 { + 0% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 70%, 71% { + -webkit-transform: translate3d(0, 125%, 0); + transform: translate3d(0, 125%, 0); + opacity: 0; + -webkit-animation-timing-function: ease-out; + } + 100% { + color: transparent; + -webkit-transform: translate3d(0, 200%, 0); + transform: translate3d(0, 200%, 0); + } +} +/* blink theme }}} */ + +/* surge theme {{{ */ +.hbe-input-surge { + overflow: hidden; + padding-bottom: 1em; +} + +.hbe-input-field-surge { + padding: 0.25em 0.5em; + margin-top: 1.25em; + width: 100%; + background: transparent; + color: #D0D0D0; + font-size: 1.55em; + opacity: 0; +} + +.hbe-input-label-surge { + width: 100%; + text-align: left; + position: absolute; + top: 1em; + pointer-events: none; + overflow: hidden; + padding: 0 0.25em; + -webkit-transform: translate3d(1em, 2.75em, 0); + transform: translate3d(1em, 2.75em, 0); + -webkit-transition: -webkit-transform 0.3s; + transition: transform 0.3s; +} + +.hbe-input-label-content-surge { + color: #A4A5A6; + padding: 0.4em 0 0.25em; + -webkit-transition: -webkit-transform 0.3s; + transition: transform 0.3s; +} + +.hbe-input-label-content-surge::after { + content: attr(data-content); + position: absolute; + font-weight: 800; + top: 100%; + left: 0; + height: 100%; + width: 100%; + color: #2C3E50; + padding: 0.25em 0; + letter-spacing: 1px; + font-size: 0.85em; +} + +.hbe-graphic-surge { + fill: #2C3E50; + pointer-events: none; + top: 1em; + bottom: 0px; + height: 4.5em; + z-index: -1; + -webkit-transition: -webkit-transform 0.7s, fill 0.7s; + transition: transform 0.7s, fill 0.7s; + -webkit-transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); + transition-timing-function: cubic-bezier(0, 0.25, 0.5, 1); +} + +.hbe-input-field-surge:focus, +.input--filled .hbe-input-field-surge { + -webkit-transition: opacity 0s 0.35s; + transition: opacity 0s 0.35s; + opacity: 1; +} + +.hbe-input-field-surge:focus + .hbe-input-label-surge, +.input--filled .hbe-input-label-surge { + -webkit-transition-delay: 0.15s; + transition-delay: 0.15s; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} + +.hbe-input-field-surge:focus + .hbe-input-label-surge .hbe-input-label-content-surge, +.input--filled .hbe-input-label-content-surge { + -webkit-transition-delay: 0.15s; + transition-delay: 0.15s; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); +} + +.hbe-input-field-surge:focus ~ .hbe-graphic-surge, +.input--filled .graphic-surge { + fill: #2C3E50; + -webkit-transform: translate3d(-66.6%, 0, 0); + transform: translate3d(-66.6%, 0, 0); +} +/* surge theme }}} */ + +/* shrink theme {{{ */ +.hbe-input-field-shrink { + width: 100%; + background: transparent; + padding: 0.5em 0; + margin-bottom: 2em; + color: #2C3E50; +} + +.hbe-input-label-shrink { + width: 100%; + position: absolute; + text-align: left; + font-size: 1em; + padding: 10px 0 5px; + pointer-events: none; +} + +.hbe-input-label-shrink::after { + content: ''; + position: absolute; + width: 100%; + height: 7px; + background: #B7C3AC; + left: 0; + top: 100%; + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transition: -webkit-transform 0.3s, background-color 0.3s; + transition: transform 0.3s, background-color 0.3s; +} + +.hbe-input-label-content-shrink { + padding: 0; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + -webkit-transition: -webkit-transform 0.3s, color 0.3s; + transition: transform 0.3s, color 0.3s; +} + +.hbe-input-field-shrink:focus + .hbe-input-label-shrink::after, +.input--filled .hbe-input-label-shrink::after { + background: #84AF9B; + -webkit-transform: scale3d(1, 0.25, 1); + transform: scale3d(1, 0.25, 1); +} + +.hbe-input-field-shrink:focus + .hbe-input-label-shrink .hbe-input-label-content-shrink, +.input--filled .hbe-input-label-shrink .hbe-input-label-content-shrink { + color: #84AF9B; + -webkit-transform: translate3d(0, 2em, 0) scale3d(0.655, 0.655, 1); + transform: translate3d(0, 2em, 0) scale3d(0.655, 0.655, 1); +} +/* shrink theme }}} */ diff --git a/css/highlight-dark.css b/css/highlight-dark.css new file mode 100644 index 000000000..13d6ac816 --- /dev/null +++ b/css/highlight-dark.css @@ -0,0 +1,62 @@ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em +} +code.hljs { + padding: 3px 5px +} +/* + +Dark style from softwaremaniacs.org (c) Ivan Sagalaev + +*/ +.hljs { + color: #ddd; + background: #303030 +} +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-section, +.hljs-link { + color: white +} +.hljs-subst { + /* default */ + +} +.hljs-string, +.hljs-title, +.hljs-name, +.hljs-type, +.hljs-attribute, +.hljs-symbol, +.hljs-bullet, +.hljs-built_in, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #d88 +} +.hljs-comment, +.hljs-quote, +.hljs-deletion, +.hljs-meta { + color: #979797 +} +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-title, +.hljs-section, +.hljs-doctag, +.hljs-type, +.hljs-name, +.hljs-strong { + font-weight: bold +} +.hljs-emphasis { + font-style: italic +} diff --git a/css/highlight.css b/css/highlight.css new file mode 100644 index 000000000..acd5468e9 --- /dev/null +++ b/css/highlight.css @@ -0,0 +1,118 @@ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em +} +code.hljs { + padding: 3px 5px +} +/*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/ +.hljs { + color: #24292e; + background: #ffffff +} +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + /* prettylights-syntax-keyword */ + color: #d73a49 +} +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + /* prettylights-syntax-entity */ + color: #6f42c1 +} +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-variable, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id { + /* prettylights-syntax-constant */ + color: #005cc5 +} +.hljs-regexp, +.hljs-string, +.hljs-meta .hljs-string { + /* prettylights-syntax-string */ + color: #032f62 +} +.hljs-built_in, +.hljs-symbol { + /* prettylights-syntax-variable */ + color: #e36209 +} +.hljs-comment, +.hljs-code, +.hljs-formula { + /* prettylights-syntax-comment */ + color: #6a737d +} +.hljs-name, +.hljs-quote, +.hljs-selector-tag, +.hljs-selector-pseudo { + /* prettylights-syntax-entity-tag */ + color: #22863a +} +.hljs-subst { + /* prettylights-syntax-storage-modifier-import */ + color: #24292e +} +.hljs-section { + /* prettylights-syntax-markup-heading */ + color: #005cc5; + font-weight: bold +} +.hljs-bullet { + /* prettylights-syntax-markup-list */ + color: #735c0f +} +.hljs-emphasis { + /* prettylights-syntax-markup-italic */ + color: #24292e; + font-style: italic +} +.hljs-strong { + /* prettylights-syntax-markup-bold */ + color: #24292e; + font-weight: bold +} +.hljs-addition { + /* prettylights-syntax-markup-inserted */ + color: #22863a; + background-color: #f0fff4 +} +.hljs-deletion { + /* prettylights-syntax-markup-deleted */ + color: #b31d28; + background-color: #ffeef0 +} +.hljs-char.escape_, +.hljs-link, +.hljs-params, +.hljs-property, +.hljs-punctuation, +.hljs-tag { + /* purposely ignored */ + +} diff --git a/css/main.css b/css/main.css new file mode 100644 index 000000000..820431828 --- /dev/null +++ b/css/main.css @@ -0,0 +1,2238 @@ +.anchorjs-link { + text-decoration: none !important; + transition: opacity 0.2s ease-in-out; +} +.markdown-body h1:hover > .anchorjs-link, +h2:hover > .anchorjs-link, +h3:hover > .anchorjs-link, +h4:hover > .anchorjs-link, +h5:hover > .anchorjs-link, +h6:hover > .anchorjs-link { + opacity: 1; +} +.banner { + height: 100%; + position: relative; + overflow: hidden; + cursor: default; +} +.banner .mask { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.3); +} +.banner[parallax="true"] { + will-change: transform; + -webkit-transform-style: preserve-3d; + -webkit-backface-visibility: hidden; + transition: transform 0.05s ease-out; +} +@media (max-width: 100vh) { + .header-inner { + max-height: 100vw; + } + #board { + margin-top: -1rem !important; + } +} +@media (max-width: 79.99vh) { + .scroll-down-bar { + display: none; + } +} +#board { + position: relative; + margin-top: -2rem; + padding: 3rem 0; + background-color: var(--board-bg-color); + transition: background-color 0.2s ease-in-out; + border-radius: 0.5rem; + z-index: 3; + -webkit-box-shadow: 0 12px 15px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); + box-shadow: 0 12px 15px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); +} +.code-widget { + display: inline-block; + background-color: transparent; + font-size: 0.75rem; + line-height: 1; + font-weight: bold; + padding: 0.3rem 0.1rem 0.1rem 0.1rem; + position: absolute; + right: 0.45rem; + top: 0.15rem; + z-index: 1; +} +.code-widget-light { + color: #999; +} +.code-widget-dark { + color: #bababa; +} +.copy-btn { + cursor: pointer; + user-select: none; + -webkit-appearance: none; + outline: none; +} +.copy-btn > i { + font-size: 0.75rem !important; + font-weight: 400; + margin-right: 0.15rem; + opacity: 0; + transition: opacity 0.2s ease-in-out; +} +.markdown-body pre:hover > .copy-btn > i { + opacity: 0.9; +} +.markdown-body pre:hover > .copy-btn, +.markdown-body pre:not(:hover) > .copy-btn { + outline: none; +} +.license-box { + background-color: rgba(27,31,35,0.05); + transition: background-color 0.2s ease-in-out; + border-radius: 4px; + font-size: 0.9rem; + overflow: hidden; + padding: 1.25rem; + position: relative; + z-index: 1; +} +.license-box .license-icon { + position: absolute; + top: 50%; + left: 100%; +} +.license-box .license-icon::after { + content: "\e8e4"; + font-size: 12.5rem; + line-height: 1; + opacity: 0.1; + position: relative; + left: -0.85em; + bottom: 0.5em; + z-index: -1; +} +.license-box .license-title { + margin-bottom: 1rem; +} +.license-box .license-title div:nth-child(1) { + line-height: 1.2; + margin-bottom: 0.25rem; +} +.license-box .license-title div:nth-child(2) { + color: var(--sec-text-color); + font-size: 0.8rem; +} +.license-box .license-meta { + align-items: center; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} +.license-box .license-meta .license-meta-item { + align-items: center; + justify-content: center; + margin-right: 1.5rem; +} +.license-box .license-meta .license-meta-item div:nth-child(1) { + color: var(--sec-text-color); + font-size: 0.8rem; + font-weight: normal; +} +.license-box .license-meta .license-meta-item i.iconfont { + font-size: 1rem; +} +@media (max-width: 575px) and (min-width: 425px) { + .license-box .license-meta .license-meta-item { + display: flex; + justify-content: flex-start; + flex-wrap: wrap; + font-size: 0.8rem; + flex: 0 0 50%; + max-width: 50%; + margin-right: 0; + } + .license-box .license-meta .license-meta-item div:nth-child(1) { + margin-right: 0.5rem; + } + .license-box .license-meta .license-meta-date { + order: -1; + } +} +@media (max-width: 424px) { + .license-box::after { + top: -65px; + } + .license-box .license-meta { + flex-direction: column; + align-items: flex-start; + } + .license-box .license-meta .license-meta-item { + display: flex; + flex-wrap: wrap; + font-size: 0.8rem; + } + .license-box .license-meta .license-meta-item div:nth-child(1) { + margin-right: 0.5rem; + } +} +.footer-inner { + padding: 3rem 0 1rem 0; + text-align: center; +} +.footer-inner > div:not(:first-child) { + margin: 0.25rem 0; + font-size: 0.85rem; +} +.footer-inner .statistics { + display: flex; + flex-direction: row; + justify-content: center; +} +.footer-inner .statistics > span { + flex: 1; + margin: 0 0.25rem; +} +.footer-inner .statistics > *:nth-last-child(2):first-child { + text-align: right; +} +.footer-inner .statistics > *:nth-last-child(2):first-child ~ * { + text-align: left; +} +.footer-inner .beian { + display: flex; + flex-direction: row; + justify-content: center; +} +.footer-inner .beian > * { + margin: 0 0.25rem; +} +.footer-inner .beian-police { + position: relative; + overflow: hidden; + display: inline-flex; + align-items: center; + justify-content: left; +} +.footer-inner .beian-police img { + margin-right: 3px; + width: 1rem; + height: 1rem; + margin-bottom: 0.1rem; +} +@media (max-width: 424px) { + .footer-inner .statistics { + flex-direction: column; + } + .footer-inner .statistics > *:nth-last-child(2):first-child { + text-align: center; + } + .footer-inner .statistics > *:nth-last-child(2):first-child ~ * { + text-align: center; + } + .footer-inner .beian { + flex-direction: column; + } + .footer-inner .beian .beian-police { + justify-content: center; + } + .footer-inner .beian > *:nth-last-child(2):first-child { + text-align: center; + } + .footer-inner .beian > *:nth-last-child(2):first-child ~ * { + text-align: center; + } +} +sup > a::before, +.footnote-text::before { + display: block; + content: ""; + margin-top: -5rem; + height: 5rem; + width: 1px; + visibility: hidden; +} +sup > a::before, +.footnote-text::before { + display: inline-block; +} +.footnote-item::before { + display: block; + content: ""; + margin-top: -5rem; + height: 5rem; + width: 1px; + visibility: hidden; +} +.footnote-list ol { + list-style-type: none; + counter-reset: sectioncounter; + padding-left: 0.5rem; + font-size: 0.95rem; +} +.footnote-list ol li:before { + font-family: "Helvetica Neue", monospace, "Monaco"; + content: "[" counter(sectioncounter) "]"; + counter-increment: sectioncounter; +} +.footnote-list ol li+li { + margin-top: 0.5rem; +} +.footnote-text { + padding-left: 0.5em; +} +.navbar { + background-color: transparent; + font-size: 0.875rem; + box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12); + -webkit-box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12); +} +.navbar .navbar-brand { + color: var(--navbar-text-color); +} +.navbar .navbar-toggler .animated-icon span { + background-color: var(--navbar-text-color); +} +.navbar .nav-item .nav-link { + display: block; + color: var(--navbar-text-color); + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; +} +.navbar .nav-item .nav-link:hover { + color: var(--link-hover-color); +} +.navbar .nav-item .nav-link:focus { + color: var(--navbar-text-color); +} +.navbar .nav-item .nav-link i { + font-size: 0.875rem; +} +.navbar .nav-item .nav-link i:only-child { + margin: 0 0.2rem; +} +.navbar .navbar-toggler { + border-width: 0; + outline: 0; +} +.navbar.scrolling-navbar { + will-change: background, padding; + -webkit-transition: background 0.5s ease-in-out, padding 0.5s ease-in-out; + transition: background 0.5s ease-in-out, padding 0.5s ease-in-out; +} +@media (min-width: 600px) { + .navbar.scrolling-navbar { + padding-top: 12px; + padding-bottom: 12px; + } + .navbar.scrolling-navbar .navbar-nav > li { + -webkit-transition-duration: 1s; + transition-duration: 1s; + } +} +.navbar.scrolling-navbar.top-nav-collapse { + padding-top: 5px; + padding-bottom: 5px; +} +.navbar .dropdown-menu { + font-size: 0.875rem; + color: var(--navbar-text-color); + background-color: rgba(0,0,0,0.3); + border: none; + min-width: 8rem; + -webkit-transition: background 0.5s ease-in-out, padding 0.5s ease-in-out; + transition: background 0.5s ease-in-out, padding 0.5s ease-in-out; +} +@media (max-width: 991.98px) { + .navbar .dropdown-menu { + text-align: center; + } +} +.navbar .dropdown-item { + color: var(--navbar-text-color); +} +.navbar .dropdown-item:hover, +.navbar .dropdown-item:focus { + color: var(--link-hover-color); + background-color: rgba(0,0,0,0.1); +} +@media (min-width: 992px) { + .navbar .dropdown:hover > .dropdown-menu { + display: block; + } + .navbar .dropdown > .dropdown-toggle:active { + pointer-events: none; + } + .navbar .dropdown-menu { + top: 95%; + } +} +.navbar .animated-icon { + width: 30px; + height: 20px; + position: relative; + margin: 0; + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + -webkit-transition: 0.5s ease-in-out; + -moz-transition: 0.5s ease-in-out; + -o-transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; + cursor: pointer; +} +.navbar .animated-icon span { + display: block; + position: absolute; + height: 3px; + width: 100%; + border-radius: 9px; + opacity: 1; + left: 0; + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + -webkit-transition: 0.25s ease-in-out; + -moz-transition: 0.25s ease-in-out; + -o-transition: 0.25s ease-in-out; + transition: 0.25s ease-in-out; + background: #fff; +} +.navbar .animated-icon span:nth-child(1) { + top: 0; +} +.navbar .animated-icon span:nth-child(2) { + top: 10px; +} +.navbar .animated-icon span:nth-child(3) { + top: 20px; +} +.navbar .animated-icon.open span:nth-child(1) { + top: 11px; + -webkit-transform: rotate(135deg); + -moz-transform: rotate(135deg); + -o-transform: rotate(135deg); + transform: rotate(135deg); +} +.navbar .animated-icon.open span:nth-child(2) { + opacity: 0; + left: -60px; +} +.navbar .animated-icon.open span:nth-child(3) { + top: 11px; + -webkit-transform: rotate(-135deg); + -moz-transform: rotate(-135deg); + -o-transform: rotate(-135deg); + transform: rotate(-135deg); +} +.navbar .dropdown-collapse, +.top-nav-collapse, +.navbar-col-show { + background-color: var(--navbar-bg-color); +} +@media (max-width: 767px) { + .navbar { + font-size: 1rem; + line-height: 2.5rem; + } +} +.banner-text { + color: var(--subtitle-color); + max-width: calc(960px - 6rem); + width: 80%; + overflow-wrap: break-word; +} +.banner-text .typed-cursor { + margin: 0 0.2rem; +} +@media (max-width: 767px) { + #subtitle, + .typed-cursor { + font-size: 1.5rem; + } +} +@media (max-width: 575px) { + .banner-text { + font-size: 0.9rem; + } + #subtitle, + .typed-cursor { + font-size: 1.35rem; + } +} +.modal-dialog .modal-content { + background-color: var(--board-bg-color); + border: 0; + border-radius: 0.125rem; + -webkit-box-shadow: 0 5px 11px 0 rgba(0,0,0,0.18), 0 4px 15px 0 rgba(0,0,0,0.15); + box-shadow: 0 5px 11px 0 rgba(0,0,0,0.18), 0 4px 15px 0 rgba(0,0,0,0.15); +} +.modal-dialog .modal-content .modal-header { + border-bottom-color: var(--line-color); + transition: border-bottom-color 0.2s ease-in-out; +} +.close { + color: var(--text-color); +} +.close:hover { + color: var(--link-hover-color); +} +.close:focus { + outline: 0; +} +.modal-dialog .modal-content .modal-header { + border-top-left-radius: 0.125rem; + border-top-right-radius: 0.125rem; + border-bottom: 1px solid #dee2e6; +} +.md-form { + position: relative; + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} +.md-form input[type] { + -webkit-box-sizing: content-box; + box-sizing: content-box; + background-color: transparent; + border: none; + border-bottom: 1px solid #ced4da; + border-radius: 0; + outline: none; + -webkit-box-shadow: none; + box-shadow: none; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; +} +.md-form input[type]:focus:not([readonly]) { + border-bottom: 1px solid #4285f4; + -webkit-box-shadow: 0 1px 0 0 #4285f4; + box-shadow: 0 1px 0 0 #4285f4; +} +.md-form input[type]:focus:not([readonly]) + label { + color: #4285f4; +} +.md-form input[type].valid, +.md-form input[type]:focus.valid { + border-bottom: 1px solid #00c851; + -webkit-box-shadow: 0 1px 0 0 #00c851; + box-shadow: 0 1px 0 0 #00c851; +} +.md-form input[type].valid + label, +.md-form input[type]:focus.valid + label { + color: #00c851; +} +.md-form input[type].invalid, +.md-form input[type]:focus.invalid { + border-bottom: 1px solid #f44336; + -webkit-box-shadow: 0 1px 0 0 #f44336; + box-shadow: 0 1px 0 0 #f44336; +} +.md-form input[type].invalid + label, +.md-form input[type]:focus.invalid + label { + color: #f44336; +} +.md-form input[type].validate { + margin-bottom: 2.5rem; +} +.md-form input[type].form-control { + height: auto; + padding: 0.6rem 0 0.4rem 0; + margin: 0 0 0.5rem 0; + color: var(--text-color); + background-color: transparent; + border-radius: 0; +} +.md-form label { + font-size: 0.8rem; + position: absolute; + top: -1rem; + left: 0; + color: #757575; + cursor: text; + transition: color 0.2s ease-out; +} +.modal-open[style] { + padding-right: 0 !important; + overflow: auto; +} +.modal-open[style] #navbar[style] { + padding-right: 1rem !important; +} +#nprogress .bar { + height: 3px !important; + background-color: #29d !important; +} +#nprogress .peg { + box-shadow: 0 0 14px #29d, 0 0 8px #29d !important; +} +@media (max-width: 575px) { + #nprogress .bar { + display: none; + } +} +.noscript-warning { + background-color: #f55; + color: #fff; + font-family: sans-serif; + font-size: 1rem; + font-weight: bold; + position: fixed; + left: 0; + bottom: 0; + text-align: center; + width: 100%; + z-index: 99; +} +.pagination { + margin-top: 3rem; + justify-content: center; +} +.pagination .space { + align-self: flex-end; +} +.pagination .page-number, +.pagination .current, +.pagination .extend { + outline: 0; + border: 0; + background-color: transparent; + font-size: 0.9rem; + padding: 0.5rem 0.75rem; + line-height: 1.25; + border-radius: 0.125rem; +} +.pagination .page-number { + margin: 0 0.05rem; +} +.pagination .page-number:hover, +.pagination .current { + transition: background-color 0.2s ease-in-out; + background-color: var(--link-hover-bg-color); +} +.qr-trigger { + cursor: pointer; + position: relative; +} +.qr-trigger:hover .qr-img { + display: block; + transition: all 0.3s; +} +.qr-img { + max-width: 12rem; + position: absolute; + right: -5.25rem; + z-index: 99; + display: none; + border-radius: 0.2rem; + background-color: transparent; + box-shadow: 0 0 20px -5px rgba(158,158,158,0.2); +} +.scroll-down-bar { + position: absolute; + width: 100%; + height: 6rem; + text-align: center; + cursor: pointer; + bottom: 0; +} +.scroll-down-bar i.iconfont { + font-size: 2rem; + font-weight: bold; + display: inline-block; + position: relative; + padding-top: 2rem; + color: var(--subtitle-color); + transform: translateZ(0); + animation: scroll-down 1.5s infinite; +} +#scroll-top-button { + position: fixed; + z-index: 99; + background: var(--board-bg-color); + transition: background-color 0.2s ease-in-out, bottom 0.3s ease; + border-radius: 4px; + min-width: 40px; + min-height: 40px; + bottom: -60px; + outline: none; + display: flex; + display: -webkit-flex; + align-items: center; + box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12); +} +#scroll-top-button i { + font-size: 32px; + margin: auto; + color: var(--sec-text-color); +} +#scroll-top-button:hover i, +#scroll-top-button:active i { + animation-name: scroll-top; + animation-duration: 1s; + animation-delay: 0.1s; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + animation-fill-mode: forwards; + animation-direction: alternate; +} +#local-search-result .search-list-title { + border-left: 3px solid #0d47a1; +} +#local-search-result .search-list-content { + padding: 0 1.25rem; +} +#local-search-result .search-word { + color: #ff4500; +} +#toc { + visibility: hidden; +} +.toc-header { + margin-bottom: 0.5rem; + font-weight: bold; + line-height: 1.2; +} +.toc-header, +.toc-header > i { + font-size: 1.25rem; +} +.toc-body { + max-height: 75vh; + overflow-y: auto; + overflow: -moz-scrollbars-none; + -ms-overflow-style: none; +} +.toc-body ol { + list-style: none; + padding-inline-start: 1rem; +} +.toc-body::-webkit-scrollbar { + display: none; +} +.tocbot-list { + position: relative; +} +.tocbot-list ol { + list-style: none; + padding-left: 1rem; +} +.tocbot-list a { + font-size: 0.95rem; +} +.tocbot-link { + color: var(--text-color); +} +.tocbot-active-link { + font-weight: bold; + color: var(--link-hover-color); +} +.tocbot-is-collapsed { + max-height: 0; +} +.tocbot-is-collapsible { + overflow: hidden; + transition: all 0.3s ease-in-out; +} +.toc-list-item { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.toc-list-item.is-active-li::before { + height: 1rem; + margin: 0.25rem 0; + visibility: visible; +} +.toc-list-item::before { + width: 0.15rem; + height: 0.2rem; + position: absolute; + left: 0.25rem; + content: ""; + border-radius: 2px; + margin: 0.65rem 0; + background: var(--link-hover-color); + visibility: hidden; + transition: height 0.1s ease-in-out, margin 0.1s ease-in-out, visibility 0.1s ease-in-out; +} +.sidebar { + position: -webkit-sticky; + position: sticky; + top: 2rem; + padding: 3rem 0; +} +html { + font-size: 16px; + letter-spacing: 0.02em; +} +html, +body { + height: 100%; + font-family: var(--font-family-sans-serif); + overflow-wrap: break-word; +} +body { + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; + background-color: var(--body-bg-color); + color: var(--text-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +body a { + color: var(--text-color); + text-decoration: none; + cursor: pointer; + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; +} +body a:hover { + color: var(--link-hover-color); + text-decoration: none; + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; +} +code { + color: inherit; +} +table { + font-size: inherit; + color: var(--post-text-color); +} +img[lazyload] { + object-fit: cover; +} +*[align="left"] { + text-align: left; +} +*[align="center"] { + text-align: center; +} +*[align="right"] { + text-align: right; +} +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-color); + border-radius: 6px; +} +::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-hover-color); +} +::-webkit-scrollbar-corner { + background-color: transparent; +} +label { + margin-bottom: 0; +} +i.iconfont { + font-size: 1em; + line-height: 1; +} +:root { + --color-mode: "light"; + --body-bg-color: #eee; + --board-bg-color: #fff; + --text-color: #3c4858; + --sec-text-color: #718096; + --post-text-color: #2c3e50; + --post-heading-color: #1a202c; + --post-link-color: #0366d6; + --link-hover-color: #30a9de; + --link-hover-bg-color: #f8f9fa; + --line-color: #eaecef; + --navbar-bg-color: #2f4154; + --navbar-text-color: #fff; + --subtitle-color: #fff; + --scrollbar-color: #c4c6c9; + --scrollbar-hover-color: #a6a6a6; + --button-bg-color: transparent; + --button-hover-bg-color: #f2f3f5; + --highlight-bg-color: #f6f8fa; + --inlinecode-bg-color: rgba(175,184,193,0.2); + --fold-title-color: #3c4858; + --fold-border-color: #eaecef; +} +@media (prefers-color-scheme: dark) { + :root { + --color-mode: "dark"; + } + :root:not([data-user-color-scheme]) { + --body-bg-color: #181c27; + --board-bg-color: #252d38; + --text-color: #c4c6c9; + --sec-text-color: #a7a9ad; + --post-text-color: #c4c6c9; + --post-heading-color: #c4c6c9; + --post-link-color: #1589e9; + --link-hover-color: #30a9de; + --link-hover-bg-color: #364151; + --line-color: #435266; + --navbar-bg-color: #1f3144; + --navbar-text-color: #d0d0d0; + --subtitle-color: #d0d0d0; + --scrollbar-color: #687582; + --scrollbar-hover-color: #9da8b3; + --button-bg-color: transparent; + --button-hover-bg-color: #46647e; + --highlight-bg-color: #303030; + --inlinecode-bg-color: rgba(99,110,123,0.4); + --fold-title-color: #c4c6c9; + --fold-border-color: #435266; + } + :root:not([data-user-color-scheme]) img { + -webkit-filter: brightness(0.9); + filter: brightness(0.9); + transition: filter 0.2s ease-in-out; + } + :root:not([data-user-color-scheme]) .license-box { + background-color: rgba(62,75,94,0.35); + transition: background-color 0.2s ease-in-out; + } + :root:not([data-user-color-scheme]) .gt-comment-admin .gt-comment-content { + background-color: transparent; + transition: background-color 0.2s ease-in-out; + } +} +@media not print { + [data-user-color-scheme="dark"] { + --body-bg-color: #181c27; + --board-bg-color: #252d38; + --text-color: #c4c6c9; + --sec-text-color: #a7a9ad; + --post-text-color: #c4c6c9; + --post-heading-color: #c4c6c9; + --post-link-color: #1589e9; + --link-hover-color: #30a9de; + --link-hover-bg-color: #364151; + --line-color: #435266; + --navbar-bg-color: #1f3144; + --navbar-text-color: #d0d0d0; + --subtitle-color: #d0d0d0; + --scrollbar-color: #687582; + --scrollbar-hover-color: #9da8b3; + --button-bg-color: transparent; + --button-hover-bg-color: #46647e; + --highlight-bg-color: #303030; + --inlinecode-bg-color: rgba(99,110,123,0.4); + --fold-title-color: #c4c6c9; + --fold-border-color: #435266; + } + [data-user-color-scheme="dark"] img { + -webkit-filter: brightness(0.9); + filter: brightness(0.9); + transition: filter 0.2s ease-in-out; + } + [data-user-color-scheme="dark"] .license-box { + background-color: rgba(62,75,94,0.35); + transition: background-color 0.2s ease-in-out; + } + [data-user-color-scheme="dark"] .gt-comment-admin .gt-comment-content { + background-color: transparent; + transition: background-color 0.2s ease-in-out; + } +} +@media print { + :root { + --color-mode: "light"; + } +} +.fade-in-up { + -webkit-animation-name: fade-in-up; + animation-name: fade-in-up; +} +.hidden-mobile { + display: block; +} +.visible-mobile { + display: none; +} +@media (max-width: 575px) { + .hidden-mobile { + display: none; + } + .visible-mobile { + display: block; + } +} +.nomargin-x { + margin-left: 0 !important; + margin-right: 0 !important; +} +.nopadding-x { + padding-left: 0 !important; + padding-right: 0 !important; +} +@media (max-width: 767px) { + .nopadding-x-md { + padding-left: 0 !important; + padding-right: 0 !important; + } +} +.flex-center { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + height: 100%; +} +.hover-with-bg { + display: inline-block; + line-height: 1; +} +.hover-with-bg:hover { + background-color: var(--link-hover-bg-color); + transition-duration: 0.2s; + transition-timing-function: ease-in-out; + border-radius: 0.2rem; +} +@-moz-keyframes fade-in-up { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@-webkit-keyframes fade-in-up { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@-o-keyframes fade-in-up { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@keyframes fade-in-up { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@-moz-keyframes scroll-down { + 0% { + opacity: 0.8; + top: 0; + } + 50% { + opacity: 0.4; + top: -1em; + } + 100% { + opacity: 0.8; + top: 0; + } +} +@-webkit-keyframes scroll-down { + 0% { + opacity: 0.8; + top: 0; + } + 50% { + opacity: 0.4; + top: -1em; + } + 100% { + opacity: 0.8; + top: 0; + } +} +@-o-keyframes scroll-down { + 0% { + opacity: 0.8; + top: 0; + } + 50% { + opacity: 0.4; + top: -1em; + } + 100% { + opacity: 0.8; + top: 0; + } +} +@keyframes scroll-down { + 0% { + opacity: 0.8; + top: 0; + } + 50% { + opacity: 0.4; + top: -1em; + } + 100% { + opacity: 0.8; + top: 0; + } +} +@-moz-keyframes scroll-top { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 50% { + -webkit-transform: translateY(-0.35rem); + transform: translateY(-0.35rem); + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@-webkit-keyframes scroll-top { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 50% { + -webkit-transform: translateY(-0.35rem); + transform: translateY(-0.35rem); + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@-o-keyframes scroll-top { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 50% { + -webkit-transform: translateY(-0.35rem); + transform: translateY(-0.35rem); + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes scroll-top { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 50% { + -webkit-transform: translateY(-0.35rem); + transform: translateY(-0.35rem); + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@media print { + header, + footer, + .side-col, + #scroll-top-button, + .post-prevnext, + #comments { + display: none !important; + } + .markdown-body a:not([href^='#']):not([href^='javascript:']):not(.print-no-link)::after { + content: ' (' attr(href) ')'; + font-size: 0.8rem; + color: var(--post-text-color); + opacity: 0.8; + } + .markdown-body > h1, + .markdown-body h2 { + border-bottom-color: transparent !important; + } + .markdown-body > h1, + .markdown-body h2, + .markdown-body h3, + .markdown-body h4, + .markdown-body h5, + .markdown-body h6 { + margin-top: 1.25em !important; + margin-bottom: 0.25em !important; + } + .markdown-body [data-anchorjs-icon]::after { + display: none; + } + .markdown-body figure.highlight table, + .markdown-body figure.highlight tbody, + .markdown-body figure.highlight tr, + .markdown-body figure.highlight td.code, + .markdown-body figure.highlight td.code pre { + width: 100% !important; + display: block !important; + } + .markdown-body figure.highlight pre > code { + white-space: pre-wrap; + } + .markdown-body figure.highlight .gutter, + .markdown-body figure.highlight .code-widget { + display: none !important; + } + .post-metas a { + text-decoration: none; + } +} +@media not print { + #seo-header { + display: none; + } +} +.index-card { + margin-bottom: 2.5rem; +} +.index-img img { + display: block; + width: 100%; + height: 10rem; + object-fit: cover; + box-shadow: 0 5px 11px 0 rgba(0,0,0,0.18), 0 4px 15px 0 rgba(0,0,0,0.15); + border-radius: 0.25rem; + background-color: transparent; +} +.index-info { + display: flex; + flex-direction: column; + justify-content: space-between; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.index-header { + color: var(--text-color); + font-size: 1.5rem; + font-weight: bold; + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 0.25rem; +} +.index-header .index-pin { + color: var(--text-color); + font-size: 1.5rem; + margin-right: 0.15rem; +} +.index-btm { + color: var(--sec-text-color); +} +.index-btm a { + color: var(--sec-text-color); +} +.index-excerpt { + color: var(--sec-text-color); + margin: 0.5rem 0; + height: calc(1.4rem * 3); + overflow: hidden; + display: flex; +} +.index-excerpt > div { + width: 100%; + line-height: 1.4rem; + word-break: break-word; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; +} +.index-excerpt__noimg { + height: auto; + max-height: calc(1.4rem * 3); +} +@media (max-width: 767px) { + .index-info { + padding-top: 1.25rem; + } + .index-header { + font-size: 1.25rem; + white-space: normal; + overflow: hidden; + word-break: break-word; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + .index-header .index-pin { + font-size: 1.25rem; + } + .index-excerpt { + height: auto; + max-height: calc(1.4rem * 3); + margin: 0.25rem 0; + } +} +#valine.v[data-class=v] .status-bar, +#valine.v[data-class=v] .veditor, +#valine.v[data-class=v] .vinput, +#valine.v[data-class=v] .vbtn, +#valine.v[data-class=v] p, +#valine.v[data-class=v] pre code { + color: var(--text-color); +} +#valine.v[data-class=v] .vinput::placeholder { + color: var(--sec-text-color); +} +#valine.v[data-class=v] .vicon { + fill: var(--text-color); +} +.gt-container .gt-comment-content:hover { + -webkit-box-shadow: none; + box-shadow: none; +} +.gt-container .gt-comment-body { + color: var(--text-color) !important; + transition: color 0.2s ease-in-out; +} +#remark-km423lmfdslkm34-back { + z-index: 1030; +} +#remark-km423lmfdslkm34-node { + z-index: 1031; +} +.markdown-body .highlight pre, +.markdown-body pre { + padding: 1.45rem 1rem; +} +.markdown-body pre code.hljs { + padding: 0; +} +.markdown-body pre[class*="language-"] { + padding-top: 1.45rem; + padding-bottom: 1.45rem; + padding-right: 1rem; + line-height: 1.5; + margin-bottom: 1rem; +} +.markdown-body .code-wrapper { + position: relative; + border-radius: 4px; + margin-bottom: 1rem; +} +.markdown-body .hljs, +.markdown-body .highlight pre, +.markdown-body .code-wrapper pre, +.markdown-body figure.highlight td.gutter { + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; + background-color: var(--highlight-bg-color); +} +pre[class*=language-].line-numbers { + position: initial; +} +figure { + margin: 1rem 0; +} +figure.highlight { + position: relative; +} +figure.highlight table { + border: 0; + margin: 0; + width: auto; + border-radius: 4px; +} +figure.highlight td { + border: 0; + padding: 0; +} +figure.highlight tr { + border: 0; +} +figure.highlight td.code { + width: 100%; +} +figure.highlight td.gutter { + display: table-cell; + position: -webkit-sticky; + position: sticky; + left: 0; + z-index: 1; +} +figure.highlight td.gutter pre { + text-align: right; + padding: 0 0.75rem; + border-radius: initial; + border-right: 1px solid #999; +} +figure.highlight td.gutter pre span.line { + color: #999; +} +figure.highlight td.code > pre { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.markdown-body { + font-size: 1rem; + line-height: 1.6; + font-family: var(--font-family-sans-serif); + margin-bottom: 2rem; + color: var(--post-text-color); +} +.markdown-body > h1, +.markdown-body h2 { + border-bottom-color: var(--line-color); +} +.markdown-body > h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + color: var(--post-heading-color); + transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; + font-weight: bold; + margin-bottom: 0.75em; + margin-top: 2em; +} +.markdown-body > h1::before, +.markdown-body h2::before, +.markdown-body h3::before, +.markdown-body h4::before, +.markdown-body h5::before, +.markdown-body h6::before { + display: block; + content: ""; + margin-top: -5rem; + height: 5rem; + width: 1px; + visibility: hidden; +} +.markdown-body > h1:focus, +.markdown-body h2:focus, +.markdown-body h3:focus, +.markdown-body h4:focus, +.markdown-body h5:focus, +.markdown-body h6:focus { + outline: none; +} +.markdown-body a { + color: var(--post-link-color); +} +.markdown-body strong { + font-weight: bold; +} +.markdown-body code { + tab-size: 4; + background-color: var(--inlinecode-bg-color); + transition: background-color 0.2s ease-in-out; +} +.markdown-body table tr { + background-color: var(--board-bg-color); + transition: background-color 0.2s ease-in-out; +} +.markdown-body table tr:nth-child(2n) { + background-color: var(--board-bg-color); + transition: background-color 0.2s ease-in-out; +} +.markdown-body table th, +.markdown-body table td { + border-color: var(--line-color); + transition: border-color 0.2s ease-in-out; +} +.markdown-body pre { + font-size: 85% !important; +} +.markdown-body pre .mermaid { + text-align: center; +} +.markdown-body pre .mermaid > svg { + min-width: 100%; +} +.markdown-body p > img, +.markdown-body p > a > img, +.markdown-body figure > img, +.markdown-body figure > a > img { + max-width: 90%; + margin: 1.5rem auto; + display: block; + box-shadow: 0 5px 11px 0 rgba(0,0,0,0.18), 0 4px 15px 0 rgba(0,0,0,0.15); + border-radius: 4px; + background-color: transparent; +} +.markdown-body blockquote { + color: var(--sec-text-color); +} +.markdown-body details { + cursor: pointer; +} +.markdown-body details summary { + outline: none; +} +hr, +.markdown-body hr { + background-color: initial; + border-top: 1px solid var(--line-color); + transition: border-top-color 0.2s ease-in-out; +} +.markdown-body hr { + height: 0; + margin: 2rem 0; +} +.markdown-body figcaption.image-caption { + font-size: 0.8rem; + color: var(--post-text-color); + opacity: 0.65; + line-height: 1; + margin: -0.75rem auto 2rem; + text-align: center; +} +.markdown-body figcaption:not(.image-caption) { + display: none; +} +.post-content, +post-custom { + box-sizing: border-box; + padding-left: 10%; + padding-right: 10%; +} +@media (max-width: 767px) { + .post-content, + post-custom { + padding-left: 2rem; + padding-right: 2rem; + } +} +@media (max-width: 424px) { + .post-content, + post-custom { + padding-left: 1rem; + padding-right: 1rem; + } + .anchorjs-link-left { + opacity: 0 !important; + } +} +.page-content strong, +.post-content strong { + font-weight: bold; +} +.page-content > *:nth-child(2), +.post-content > *:nth-child(2) { + margin-top: 0; +} +.page-content img, +.post-content img { + object-fit: cover; + max-width: 100%; +} +@media (max-width: 767px) { + .page-content, + .post-content { + overflow-x: hidden; + } +} +.post-metas { + display: flex; + flex-wrap: wrap; + font-size: 0.9rem; +} +.post-meta > *:not(.hover-with-bg) { + margin-right: 0.2rem; +} +.post-prevnext { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + font-size: 0.9rem; + margin-left: -0.35rem; + margin-right: -0.35rem; +} +.post-prevnext .post-prev, +.post-prevnext .post-next { + display: flex; + padding-left: 0; + padding-right: 0; +} +.post-prevnext .post-prev i, +.post-prevnext .post-next i { + font-size: 1.5rem; +} +.post-prevnext .post-prev a, +.post-prevnext .post-next a { + display: flex; + align-items: center; +} +.post-prevnext .post-prev .hidden-mobile, +.post-prevnext .post-next .hidden-mobile { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + text-overflow: ellipsis; + overflow: hidden; +} +@media (max-width: 575px) { + .post-prevnext .post-prev .hidden-mobile, + .post-prevnext .post-next .hidden-mobile { + display: none; + } +} +.post-prevnext .post-prev:hover i, +.post-prevnext .post-prev:active i, +.post-prevnext .post-next:hover i, +.post-prevnext .post-next:active i { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-delay: 0.1s; + animation-delay: 0.1s; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; + -webkit-animation-direction: alternate; + animation-direction: alternate; +} +.post-prevnext .post-prev:hover i, +.post-prevnext .post-prev:active i { + -webkit-animation-name: post-prev-anim; + animation-name: post-prev-anim; +} +.post-prevnext .post-next:hover i, +.post-prevnext .post-next:active i { + -webkit-animation-name: post-next-anim; + animation-name: post-next-anim; +} +.post-prevnext .post-next { + justify-content: flex-end; +} +.post-prevnext .fa-chevron-left { + margin-right: 0.5rem; +} +.post-prevnext .fa-chevron-right { + margin-left: 0.5rem; +} +#seo-header { + color: var(--post-heading-color); + font-weight: bold; + margin-top: 0.5em; + margin-bottom: 0.75em; + border-bottom-color: var(--line-color); + border-bottom-style: solid; + border-bottom-width: 2px; + line-height: 1.5; +} +.custom, +#comments { + margin-top: 2rem; +} +#comments noscript { + display: block; + text-align: center; + padding: 2rem 0; +} +.visitors { + font-size: 0.8em; + padding: 0.45rem; + float: right; +} +a.fancybox:hover { + text-decoration: none; +} +mjx-container, +.mjx-container { + overflow-x: auto; + overflow-y: hidden !important; + padding: 0.5em 0; +} +mjx-container:focus, +.mjx-container:focus, +mjx-container svg:focus, +.mjx-container svg:focus { + outline: none; +} +.mjx-char { + line-height: 1; +} +.katex-block { + overflow-x: auto; +} +.katex, +.mjx-mrow { + white-space: pre-wrap !important; +} +.footnote-ref [class*=hint--][aria-label]:after { + max-width: 12rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +@-moz-keyframes post-prev-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(-0.35rem); + transform: translateX(-0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@-webkit-keyframes post-prev-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(-0.35rem); + transform: translateX(-0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@-o-keyframes post-prev-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(-0.35rem); + transform: translateX(-0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes post-prev-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(-0.35rem); + transform: translateX(-0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@-moz-keyframes post-next-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(0.35rem); + transform: translateX(0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@-webkit-keyframes post-next-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(0.35rem); + transform: translateX(0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@-o-keyframes post-next-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(0.35rem); + transform: translateX(0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes post-next-anim { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(0.35rem); + transform: translateX(0.35rem); + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +.fold { + margin: 1rem 0; + border: 0.5px solid var(--fold-border-color); + position: relative; + clear: both; + border-radius: 0.125rem; +} +.fold .fold-title { + color: var(--fold-title-color); + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + font-weight: bold; + border-radius: 0.125rem; +} +.fold .fold-title:not(.collapsed) > .fold-arrow { + transform: rotate(90deg); + transform-origin: center center; +} +.fold .fold-title .fold-arrow { + display: inline-block; + margin-right: 0.35rem; + transition: transform 0.3s ease-out; +} +.fold .fold-content { + padding: 1rem 1rem; +} +.fold .fold-content > *:last-child { + margin-bottom: 0; +} +.fold-default, +.fold-secondary { + background-color: rgba(187,187,187,0.25); +} +.fold-primary { + background-color: rgba(183,160,224,0.25); +} +.fold-info { + background-color: rgba(160,197,228,0.25); +} +.fold-success { + background-color: rgba(174,220,174,0.25); +} +.fold-warning { + background-color: rgba(248,214,166,0.25); +} +.fold-danger { + background-color: rgba(236,169,167,0.25); +} +.fold-light { + background-color: rgba(254,254,254,0.25); +} +.note { + padding: 0.75rem; + border-left: 0.35rem solid; + border-radius: 0.25rem; + margin: 1.5rem 0; + color: var(--text-color); + transition: color 0.2s ease-in-out; + font-size: 0.9rem; +} +.note a { + color: var(--text-color); + transition: color 0.2s ease-in-out; +} +.note *:last-child { + margin-bottom: 0; +} +.note-default, +.note-secondary { + background-color: rgba(187,187,187,0.25); + border-color: #777; +} +.note-primary { + background-color: rgba(183,160,224,0.25); + border-color: #6f42c1; +} +.note-success { + background-color: rgba(174,220,174,0.25); + border-color: #5cb85c; +} +.note-danger { + background-color: rgba(236,169,167,0.25); + border-color: #d9534f; +} +.note-warning { + background-color: rgba(248,214,166,0.25); + border-color: #f0ad4e; +} +.note-info { + background-color: rgba(160,197,228,0.25); + border-color: #428bca; +} +.note-light { + background-color: rgba(254,254,254,0.25); + border-color: #0f0f0f; +} +.label { + display: inline; + border-radius: 3px; + font-size: 85%; + margin: 0; + padding: 0.2em 0.4em; + color: var(--text-color); + transition: color 0.2s ease-in-out; +} +.label-default, +.label-secondary { + background-color: rgba(187,187,187,0.25); +} +.label-primary { + background-color: rgba(183,160,224,0.25); +} +.label-info { + background-color: rgba(160,197,228,0.25); +} +.label-success { + background-color: rgba(174,220,174,0.25); +} +.label-warning { + background-color: rgba(248,214,166,0.25); +} +.label-danger { + background-color: rgba(236,169,167,0.25); +} +.markdown-body .btn { + border: 1px solid var(--line-color); + background-color: var(--button-bg-color); + color: var(--text-color); + transition: color 0.2s ease-in-out, background 0.2s ease-in-out, border-color 0.2s ease-in-out; + border-radius: 0.25rem; + display: inline-block; + font-size: 0.875em; + line-height: 2; + padding: 0 0.75rem; + margin-bottom: 1rem; +} +.markdown-body .btn:hover { + background-color: var(--button-hover-bg-color); + text-decoration: none; +} +.group-image-container { + margin: 1.5rem auto; +} +.group-image-container img { + margin: 0 auto; + border-radius: 3px; + background-color: transparent; + box-shadow: 0 3px 9px 0 rgba(0,0,0,0.15), 0 3px 9px 0 rgba(0,0,0,0.15); +} +.group-image-row { + margin-bottom: 0.5rem; + display: flex; + justify-content: center; +} +.group-image-wrap { + flex: 1; + display: flex; + justify-content: center; +} +.group-image-wrap:not(:last-child) { + margin-right: 0.25rem; +} +input[type=checkbox] { + margin: 0 0.2em 0.2em 0; + vertical-align: middle; +} +.list-group a ~ p.h5 { + margin-top: 1rem; +} +.list-group-item { + display: flex; + background-color: transparent; + border: 0; +} +.list-group-item time { + flex: 0 0 5rem; +} +.list-group-item .list-group-item-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +@media (max-width: 575px) { + .list-group-item { + font-size: 0.95rem; + padding: 0.5rem 0.75rem; + } + .list-group-item time { + flex: 0 0 4rem; + } +} +.list-group-item-action { + color: var(--text-color); +} +.list-group-item-action:focus, +.list-group-item-action:hover { + color: var(--link-hover-color); + background-color: var(--link-hover-bg-color); +} +.about-avatar { + position: relative; + margin: -8rem auto 1rem; + width: 10rem; + height: 10rem; + z-index: 3; +} +.about-avatar img { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: transparent; + object-fit: cover; + box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12); +} +.about-info > div { + margin-bottom: 0.5rem; +} +.about-name { + font-size: 1.75rem; + font-weight: bold; +} +.about-intro { + font-size: 1rem; +} +.about-icons > a:not(:last-child) { + margin-right: 0.5rem; +} +.about-icons > a > i { + font-size: 1.5rem; +} +.category-bar .category-list { + max-height: 85vh; + overflow-y: auto; + overflow-x: hidden; +} +.category-bar .category-list::-webkit-scrollbar { + display: none; +} +.category-bar .category-list > .category-sub > a { + font-weight: bold; + font-size: 1.2rem; +} +.category-bar .category-list .category-item-action i { + margin: 0; +} +.category-bar .category-list .category-subitem.list-group-item { + padding-left: 0.5rem; + padding-right: 0; +} +.category-bar .category-list .category-collapse .category-post-list { + margin-top: 0.25rem; + margin-bottom: 0.5rem; +} +.category-bar .category-list .category-collapse .category-post { + font-size: 0.9rem; + line-height: 1.75; +} +.category-bar .category-list .category-item-action:hover { + background-color: initial; +} +.category-bar .list-group-item { + padding: 0; +} +.category-bar .list-group-item.active { + color: var(--link-hover-color); + background-color: initial; + font-weight: bold; + font-family: "iconfont"; + font-style: normal; + -webkit-font-smoothing: antialiased; +} +.category-bar .list-group-item.active::before { + content: "\e61f"; + font-weight: initial; + margin-right: 0.25rem; +} +.category-bar .list-group-count { + margin-left: 0.2rem; + margin-right: 0.2rem; + font-size: 0.9em; +} +.category-bar .list-group-item-action:focus, +.category-bar .list-group-item-action:hover { + background-color: initial; +} +.category-chains { + display: flex; + flex-wrap: wrap; +} +.category-chains > *:not(:last-child) { + margin-right: 1em; +} +.category:not(:last-child) { + margin-bottom: 1rem; +} +.category .category-item, +.category .category-subitem { + font-weight: bold; + display: flex; + align-items: center; +} +.category .category-item { + font-size: 1.25rem; +} +.category .category-subitem { + font-size: 1.1rem; +} +.category .category-collapse { + padding-left: 1.25rem; + width: 100%; +} +.category .category-count { + font-size: 0.9rem; + font-weight: initial; + min-width: 1.3em; + line-height: 1.3em; + display: flex; + align-items: center; +} +.category .category-count i { + padding-right: 0.25rem; +} +.category .category-count span { + width: 2rem; +} +.category .category-post { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.category .category-item-action:not(.collapsed) > i { + transform: rotate(90deg); + transform-origin: center center; +} +.category .category-item-action i { + transition: transform 0.3s ease-out; + display: inline-block; + margin-left: 0.25rem; +} +.category .category-item-action .category:hover { + z-index: 1; + color: var(--link-hover-color); + text-decoration: none; + background-color: var(--link-hover-bg-color); +} +.category .row { + margin-left: 0; + margin-right: 0; +} +.tagcloud { + padding: 1rem 5%; +} +.tagcloud a { + display: inline-block; + padding: 0.5rem; +} +.tagcloud a:hover { + color: var(--link-hover-color) !important; +} +.links .card { + box-shadow: none; + min-width: 33%; + background-color: transparent; + border: 0; +} +.links .card-body { + margin: 1rem 0; + padding: 1rem; + border-radius: 0.3rem; + display: block; + width: 100%; + height: 100%; +} +.links .card-body:hover .link-avatar { + transform: scale(1.1); +} +.links .card-content { + display: flex; + flex-wrap: nowrap; + width: 100%; + height: 3.5rem; +} +.link-avatar { + flex: none; + width: 3rem; + height: 3rem; + margin-right: 0.75rem; + object-fit: cover; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.link-avatar img { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: transparent; + object-fit: cover; +} +.link-text { + flex: 1; + display: grid; + flex-direction: column; + line-height: 1.5; +} +.link-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-color); + font-weight: bold; +} +.link-intro { + max-height: 2rem; + font-size: 0.85rem; + line-height: 1.2; + color: var(--sec-text-color); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + text-overflow: ellipsis; + overflow: hidden; +} +@media (max-width: 767px) { + .links { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + .links .card { + padding-left: 2rem; + padding-right: 2rem; + } +} +@media (min-width: 768px) { + .link-text:only-child { + margin-left: 1rem; + } +} diff --git a/file/6.824/gfs.pdf b/file/6.824/gfs.pdf new file mode 100644 index 000000000..969fcc0b6 Binary files /dev/null and b/file/6.824/gfs.pdf differ diff --git a/file/6.824/mapreduce.pdf b/file/6.824/mapreduce.pdf new file mode 100644 index 000000000..bb87240ba Binary files /dev/null and b/file/6.824/mapreduce.pdf differ diff --git a/file/6.824/raft-extended.pdf b/file/6.824/raft-extended.pdf new file mode 100644 index 000000000..8e6388651 Binary files /dev/null and b/file/6.824/raft-extended.pdf differ diff --git a/file/6.824/vm-ft.pdf b/file/6.824/vm-ft.pdf new file mode 100644 index 000000000..008c0c6d8 Binary files /dev/null and b/file/6.824/vm-ft.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W1.pdf b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W1.pdf new file mode 100644 index 000000000..3bb909a8a Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W1.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W2.pdf b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W2.pdf new file mode 100644 index 000000000..630c0fb41 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W2.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W3.pdf b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W3.pdf new file mode 100644 index 000000000..48af80c78 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W3.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W4.pdf b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W4.pdf new file mode 100644 index 000000000..18475b97c Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/C2_W4.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W1.zip b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W1.zip new file mode 100644 index 000000000..2b2c3dd3e Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W1.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W2.zip b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W2.zip new file mode 100644 index 000000000..adafca3ec Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W2.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W3.zip b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W3.zip new file mode 100644 index 000000000..2bdb7ea38 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W3.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W4.zip b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W4.zip new file mode 100644 index 000000000..2a6da51a5 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Exercises-W4.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Notebooks-W1.zip b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Notebooks-W1.zip new file mode 100644 index 000000000..3dd2b55a5 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Notebooks-W1.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Notebooks-W2.zip b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Notebooks-W2.zip new file mode 100644 index 000000000..033141861 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Advanced-Learning-Algorithms/Notebooks-W2.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W1.pdf b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W1.pdf new file mode 100644 index 000000000..a075d7454 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W1.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W2.pdf b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W2.pdf new file mode 100644 index 000000000..5038c41d5 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W2.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W3.pdf b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W3.pdf new file mode 100644 index 000000000..9a6ef71a3 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/C1_W3.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Exercises-W2.zip b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Exercises-W2.zip new file mode 100644 index 000000000..1bed411f2 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Exercises-W2.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Exercises-W3.zip b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Exercises-W3.zip new file mode 100644 index 000000000..add60517e Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Exercises-W3.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W1.zip b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W1.zip new file mode 100644 index 000000000..d67769556 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W1.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W2.zip b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W2.zip new file mode 100644 index 000000000..18504f95f Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W2.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W3.zip b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W3.zip new file mode 100644 index 000000000..d8718f476 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Supervised-Machine-Learning-Regression-and-Classification/Notebooks-W3.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W1.pdf b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W1.pdf new file mode 100644 index 000000000..fd66a1c11 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W1.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W2.pdf b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W2.pdf new file mode 100644 index 000000000..df4753e40 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W2.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W3.pdf b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W3.pdf new file mode 100644 index 000000000..3d3174e20 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/C3_W3.pdf differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W1-1.zip b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W1-1.zip new file mode 100644 index 000000000..ca89e74c8 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W1-1.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W1-2.zip b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W1-2.zip new file mode 100644 index 000000000..0b34c2cb9 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W1-2.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W2-1.zip b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W2-1.zip new file mode 100644 index 000000000..6705de07f Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W2-1.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W2-2.zip b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W2-2.zip new file mode 100644 index 000000000..4cdc44372 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W2-2.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W3.zip b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W3.zip new file mode 100644 index 000000000..e35375435 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Exercises-W3.zip differ diff --git a/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Notebooks-W3.zip b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Notebooks-W3.zip new file mode 100644 index 000000000..d6c746f07 Binary files /dev/null and b/file/Coursera/Machine-Learning-Specialization/Unsupervised-Learning-Recommenders-Reinforcement-Learning/Notebooks-W3.zip differ diff --git a/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Linear-Algebra/Formula-Sheet.pdf b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Linear-Algebra/Formula-Sheet.pdf new file mode 100644 index 000000000..774c04f93 Binary files /dev/null and b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Linear-Algebra/Formula-Sheet.pdf differ diff --git a/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Linear-Algebra/Notebooks.zip b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Linear-Algebra/Notebooks.zip new file mode 100644 index 000000000..5f8d10085 Binary files /dev/null and b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Linear-Algebra/Notebooks.zip differ diff --git a/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Multivariate-Calculus/Formula-Sheet.pdf b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Multivariate-Calculus/Formula-Sheet.pdf new file mode 100644 index 000000000..5638ac71e Binary files /dev/null and b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Multivariate-Calculus/Formula-Sheet.pdf differ diff --git a/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Multivariate-Calculus/Notebooks.zip b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Multivariate-Calculus/Notebooks.zip new file mode 100644 index 000000000..48a055943 Binary files /dev/null and b/file/Coursera/Mathematics-for-Machine-Learning-Specialization/Mathematics-for-Machine-Learning-Multivariate-Calculus/Notebooks.zip differ diff --git a/file/Machine-Learning-Competition/basic.ipynb b/file/Machine-Learning-Competition/basic.ipynb new file mode 100644 index 000000000..7fdae1d24 --- /dev/null +++ b/file/Machine-Learning-Competition/basic.ipynb @@ -0,0 +1,1619 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 问题建模" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import warnings\n", + "warnings.filterwarnings('ignore') # 忽略警告提示\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import tsfresh as tsf\n", + "\n", + "import scipy.stats as st\n", + "from math import sqrt\n", + "\n", + "import lightgbm as lgb\n", + "import xgboost as xgb\n", + "\n", + "import seaborn as sns\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import colors \n", + "from matplotlib.ticker import PercentFormatter \n", + "\n", + "from sklearn.model_selection import KFold,cross_val_score\n", + "from sklearn.preprocessing import OneHotEncoder\n", + "from sklearn.metrics import mean_squared_error,precision_score,recall_score,f1_score\n", + "from sklearn.linear_model import Ridge, RidgeCV, ElasticNet, LassoCV, LassoLarsCV,Lasso\n", + "from sklearn.ensemble import ExtraTreesRegressor,RandomForestRegressor\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.6 0.5 0.5454545454545454\n" + ] + } + ], + "source": [ + "# 构建一个计算准确率、召回率和F1-score的评价代码\n", + "y_train = np.array([1,0,1,0,1,0,1,0,1,1])\n", + "y_pred = np.array([1,1,1,1,0,0,0,0,1,0])\n", + "precision = precision_score(y_train,y_pred) #准确率\n", + "recall = recall_score(y_train,y_pred) #召回率\n", + "f1 = f1_score(y_train,y_pred) #f1度量\n", + "print(precision,recall,f1)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "train = pd.read_csv('train.csv')\n", + "test = pd.read_csv('test.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
IdMSSubClassLotFrontageLotAreaOverallQualOverallCondYearBuiltYearRemodAddMasVnrAreaBsmtFinSF1...WoodDeckSFOpenPorchSFEnclosedPorch3SsnPorchScreenPorchPoolAreaMiscValMoSoldYrSoldSalePrice
count1460.0000001460.0000001201.0000001460.0000001460.0000001460.0000001460.0000001460.0000001452.0000001460.000000...1460.0000001460.0000001460.0000001460.0000001460.0000001460.0000001460.0000001460.0000001460.0000001460.000000
mean730.50000056.89726070.04995810516.8280826.0993155.5753421971.2678081984.865753103.685262443.639726...94.24452146.66027421.9541103.40958915.0609592.75890443.4890416.3219182007.815753180921.195890
std421.61000942.30057124.2847529981.2649321.3829971.11279930.20290420.645407181.066207456.098091...125.33879466.25602861.11914929.31733155.75741540.177307496.1230242.7036261.32809579442.502883
min1.00000020.00000021.0000001300.0000001.0000001.0000001872.0000001950.0000000.0000000.000000...0.0000000.0000000.0000000.0000000.0000000.0000000.0000001.0000002006.00000034900.000000
25%365.75000020.00000059.0000007553.5000005.0000005.0000001954.0000001967.0000000.0000000.000000...0.0000000.0000000.0000000.0000000.0000000.0000000.0000005.0000002007.000000129975.000000
50%730.50000050.00000069.0000009478.5000006.0000005.0000001973.0000001994.0000000.000000383.500000...0.00000025.0000000.0000000.0000000.0000000.0000000.0000006.0000002008.000000163000.000000
75%1095.25000070.00000080.00000011601.5000007.0000006.0000002000.0000002004.000000166.000000712.250000...168.00000068.0000000.0000000.0000000.0000000.0000000.0000008.0000002009.000000214000.000000
max1460.000000190.000000313.000000215245.00000010.0000009.0000002010.0000002010.0000001600.0000005644.000000...857.000000547.000000552.000000508.000000480.000000738.00000015500.00000012.0000002010.000000755000.000000
\n", + "

8 rows × 38 columns

\n", + "
" + ], + "text/plain": [ + " Id MSSubClass LotFrontage LotArea OverallQual \\\n", + "count 1460.000000 1460.000000 1201.000000 1460.000000 1460.000000 \n", + "mean 730.500000 56.897260 70.049958 10516.828082 6.099315 \n", + "std 421.610009 42.300571 24.284752 9981.264932 1.382997 \n", + "min 1.000000 20.000000 21.000000 1300.000000 1.000000 \n", + "25% 365.750000 20.000000 59.000000 7553.500000 5.000000 \n", + "50% 730.500000 50.000000 69.000000 9478.500000 6.000000 \n", + "75% 1095.250000 70.000000 80.000000 11601.500000 7.000000 \n", + "max 1460.000000 190.000000 313.000000 215245.000000 10.000000 \n", + "\n", + " OverallCond YearBuilt YearRemodAdd MasVnrArea BsmtFinSF1 ... \\\n", + "count 1460.000000 1460.000000 1460.000000 1452.000000 1460.000000 ... \n", + "mean 5.575342 1971.267808 1984.865753 103.685262 443.639726 ... \n", + "std 1.112799 30.202904 20.645407 181.066207 456.098091 ... \n", + "min 1.000000 1872.000000 1950.000000 0.000000 0.000000 ... \n", + "25% 5.000000 1954.000000 1967.000000 0.000000 0.000000 ... \n", + "50% 5.000000 1973.000000 1994.000000 0.000000 383.500000 ... \n", + "75% 6.000000 2000.000000 2004.000000 166.000000 712.250000 ... \n", + "max 9.000000 2010.000000 2010.000000 1600.000000 5644.000000 ... \n", + "\n", + " WoodDeckSF OpenPorchSF EnclosedPorch 3SsnPorch ScreenPorch \\\n", + "count 1460.000000 1460.000000 1460.000000 1460.000000 1460.000000 \n", + "mean 94.244521 46.660274 21.954110 3.409589 15.060959 \n", + "std 125.338794 66.256028 61.119149 29.317331 55.757415 \n", + "min 0.000000 0.000000 0.000000 0.000000 0.000000 \n", + "25% 0.000000 0.000000 0.000000 0.000000 0.000000 \n", + "50% 0.000000 25.000000 0.000000 0.000000 0.000000 \n", + "75% 168.000000 68.000000 0.000000 0.000000 0.000000 \n", + "max 857.000000 547.000000 552.000000 508.000000 480.000000 \n", + "\n", + " PoolArea MiscVal MoSold YrSold SalePrice \n", + "count 1460.000000 1460.000000 1460.000000 1460.000000 1460.000000 \n", + "mean 2.758904 43.489041 6.321918 2007.815753 180921.195890 \n", + "std 40.177307 496.123024 2.703626 1.328095 79442.502883 \n", + "min 0.000000 0.000000 1.000000 2006.000000 34900.000000 \n", + "25% 0.000000 0.000000 5.000000 2007.000000 129975.000000 \n", + "50% 0.000000 0.000000 6.000000 2008.000000 163000.000000 \n", + "75% 0.000000 0.000000 8.000000 2009.000000 214000.000000 \n", + "max 738.000000 15500.000000 12.000000 2010.000000 755000.000000 \n", + "\n", + "[8 rows x 38 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "all_data = pd.concat((train,test))\n", + "all_data = pd.get_dummies(all_data) # 将所有非数值变量均变成哑变量(即onehot形式)\n", + "# 填充缺失值\n", + "all_data = all_data.fillna(all_data.mean())\n", + "# 数据切分\n", + "X_train = all_data[:train.shape[0]]\n", + "X_test = all_data[train.shape[0]:]\n", + "y = train.SalePrice" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "KFold(n_splits=5, random_state=2020, shuffle=True)\n", + "Training until validation scores don't improve for 100 rounds\n", + "[100]\ttraining's rmse: 34583.7\tvalid_1's rmse: 33466.3\n", + "[200]\ttraining's rmse: 22770.9\tvalid_1's rmse: 18964.6\n", + "[300]\ttraining's rmse: 19899.1\tvalid_1's rmse: 16299.2\n", + "[400]\ttraining's rmse: 18740.3\tvalid_1's rmse: 15997.4\n", + "Early stopping, best iteration is:\n", + "[397]\ttraining's rmse: 18766.3\tvalid_1's rmse: 15991.7\n", + "Training until validation scores don't improve for 100 rounds\n", + "[100]\ttraining's rmse: 34408.6\tvalid_1's rmse: 33487\n", + "[200]\ttraining's rmse: 21861.1\tvalid_1's rmse: 20431.9\n", + "[300]\ttraining's rmse: 19073.3\tvalid_1's rmse: 17497.6\n", + "[400]\ttraining's rmse: 18061.7\tvalid_1's rmse: 16475.3\n", + "[500]\ttraining's rmse: 17425.2\tvalid_1's rmse: 15887.5\n", + "[600]\ttraining's rmse: 16919\tvalid_1's rmse: 15448.9\n", + "[700]\ttraining's rmse: 16482.9\tvalid_1's rmse: 15124.6\n", + "[800]\ttraining's rmse: 16052.4\tvalid_1's rmse: 14837.6\n", + "[900]\ttraining's rmse: 15635.8\tvalid_1's rmse: 14584.8\n", + "[1000]\ttraining's rmse: 15248.4\tvalid_1's rmse: 14389.8\n", + "Did not meet early stopping. Best iteration is:\n", + "[1000]\ttraining's rmse: 15248.4\tvalid_1's rmse: 14389.8\n", + "Training until validation scores don't improve for 100 rounds\n", + "[100]\ttraining's rmse: 34681\tvalid_1's rmse: 30578.1\n", + "[200]\ttraining's rmse: 21186.4\tvalid_1's rmse: 18514.1\n", + "[300]\ttraining's rmse: 18144.9\tvalid_1's rmse: 15676.8\n", + "[400]\ttraining's rmse: 17123.1\tvalid_1's rmse: 14870.1\n", + "[500]\ttraining's rmse: 16486.3\tvalid_1's rmse: 14573\n", + "[600]\ttraining's rmse: 15969.8\tvalid_1's rmse: 14371.3\n", + "[700]\ttraining's rmse: 15515.7\tvalid_1's rmse: 14233.4\n", + "[800]\ttraining's rmse: 15111.7\tvalid_1's rmse: 14129.4\n", + "[900]\ttraining's rmse: 14743.5\tvalid_1's rmse: 14067\n", + "[1000]\ttraining's rmse: 14406.9\tvalid_1's rmse: 14018.9\n", + "Did not meet early stopping. Best iteration is:\n", + "[1000]\ttraining's rmse: 14406.9\tvalid_1's rmse: 14018.9\n", + "Training until validation scores don't improve for 100 rounds\n", + "[100]\ttraining's rmse: 35136.1\tvalid_1's rmse: 26771\n", + "[200]\ttraining's rmse: 22449.9\tvalid_1's rmse: 13128.1\n", + "[300]\ttraining's rmse: 19477.6\tvalid_1's rmse: 10981.8\n", + "[400]\ttraining's rmse: 18333.7\tvalid_1's rmse: 10996.4\n", + "Early stopping, best iteration is:\n", + "[324]\ttraining's rmse: 19134.4\tvalid_1's rmse: 10884.3\n", + "Training until validation scores don't improve for 100 rounds\n", + "[100]\ttraining's rmse: 31888.3\tvalid_1's rmse: 44443.1\n", + "[200]\ttraining's rmse: 18512.4\tvalid_1's rmse: 33127.2\n", + "[300]\ttraining's rmse: 15478.7\tvalid_1's rmse: 30536.4\n", + "[400]\ttraining's rmse: 14465.8\tvalid_1's rmse: 29392\n", + "[500]\ttraining's rmse: 13900.5\tvalid_1's rmse: 28676.1\n", + "[600]\ttraining's rmse: 13482.8\tvalid_1's rmse: 28147.7\n", + "[700]\ttraining's rmse: 13058.7\tvalid_1's rmse: 27645.6\n", + "[800]\ttraining's rmse: 12661.8\tvalid_1's rmse: 27189.3\n", + "[900]\ttraining's rmse: 12340.6\tvalid_1's rmse: 26840.6\n", + "[1000]\ttraining's rmse: 12048\tvalid_1's rmse: 26524.1\n", + "Did not meet early stopping. Best iteration is:\n", + "[1000]\ttraining's rmse: 12048\tvalid_1's rmse: 26524.1\n" + ] + } + ], + "source": [ + "params = {'num_leaves': 63,\n", + " 'min_child_samples': 50,\n", + " 'objective': 'regression',\n", + " 'learning_rate': 0.01,\n", + " 'boosting_type': 'gbdt',\n", + " 'metric': 'rmse',\n", + " 'verbose': -1,\n", + " } \n", + "folds = KFold(n_splits=5, shuffle=True, random_state=2020)\n", + "print(folds)\n", + "for trn_idx, val_idx in folds.split(X_train, y):\n", + " trn_df, trn_label = X_train.iloc[trn_idx, :], y[trn_idx]\n", + " val_df, val_label = X_train.iloc[val_idx, :], y[val_idx]\n", + " dtrn = lgb.Dataset(trn_df, label = trn_label)\n", + " dval = lgb.Dataset(val_df, label = val_label) \n", + " bst = lgb.train(params,dtrn, \n", + " num_boost_round=1000,\n", + " valid_sets=[dtrn, dval],\n", + " early_stopping_rounds=100, verbose_eval=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 结果分析" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def rmse_cv(model):\n", + " rmse= np.sqrt(-cross_val_score(model, X_train, y, scoring=\"neg_mean_squared_error\", cv = 5))\n", + " return(rmse)\n", + "model_ridge = Ridge()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "alphas = [0.05, 0.1, 0.3, 1, 3, 5, 10, 15, 30, 50, 75]\n", + "cv_ridge = [rmse_cv(Ridge(alpha = alpha)).mean() for alpha in alphas]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'rmse')" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEWCAYAAABrDZDcAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAAnQUlEQVR4nO3dd3yV9f3+8debvQl7hxmWICssxT2KONCKrVssSmtrra1atyCuqq1Vf04cuHCjyCo4QHGhAjIyWLJn2AGSkPX+/XEObb4xYICc3Cc51/PxyCPnHrlzwQlcucfnvs3dERGR2FUh6AAiIhIsFYGISIxTEYiIxDgVgYhIjFMRiIjEOBWBiEiMUxGIFGJmbmYdwq+fM7O7i7PuEXyfy8zs4yPNKVJSTOMIpDwys+nA9+5+T6H5Q4HngZbunnuQr3Ugwd1XFOP7FGtdM2sDrAIqH+z7igRFewRSXr0KXG5mVmj+FcB4/Wcs8j8qAimvJgINgBMOzDCzesA5wCQz+9bMdpnZJjN7ysyqFLURM3vFzO4vMH1L+Gs2mtnvCq17tpn9aGbpZrbOzEYXWDw7/HmXme01s4FmNtzMvirw9ceZ2Q9mtjv8+bgCyz43s/vM7Gsz22NmH5tZwyP/6xH5HxWBlEvungm8C1xZYPZvgCXAXuCvQENgIHAa8Mdf2qaZDQZuBs4AEoDTC62yL/z94oCzgevM7PzwshPDn+PcvZa7f1to2/WBqcCThArsMWCqmTUosNqlwNVAY6BKOIvIUSuTRWBmL5tZmpklldD24sO/YaWaWUr4eK6Ufa8Cw8ysWnj6SuBVd5/n7nPcPdfdVxM6Z3BSMbb3G2Ccuye5+z5gdMGF7v65uy9293x3XwS8VcztQqg4lrv76+FcbxEqrXMLrDPO3ZcVKLmexdy2yCGVySIAXgEGl+D2XgMedfcuQD8grQS3LQFx96+AbcD5Ztae0Hv7ppl1NLMpZrbZzNKBBwntHfyS5sC6AtNrCi40s/5mNsvMtprZbuAPxdzugW2vKTRvDdCiwPTmAq8zgFrF3LbIIZXJInD32cCOgvPMrL2ZTTezeWb2pZl1Ls62zKwrUMndPwlve6+7Z5R8agnIa4T2BC4HZrj7FuBZQr9tJ7h7HeAOoPBJ5aJsAloVmI4vtPxNYBLQyt3rAs8V2O4vXZ63EWhdaF48sKEYuUSOSpksgoMYC/zZ3fsQOnb6TDG/riOhE3gfhE/0PWpmFSOWUkrba4SO5V9L6FARQG0gHdgb/oXhumJu611guJl1NbMawKhCy2sDO9w9y8z6ETqmf8BWIB9od5BtTwM6mtmlZlbJzH4LdAWmFDObyBErF0VgZrWA44D3zGwBoWO+zcLLfm1mSUV8zAh/eSVCV5bcDPQl9A91eGn/GSQywucAvgFqEvptHULv9aXAHuAF4J1ibus/wOPATGBF+HNBfwTGmNke4B5CxXHgazOAB4Cvw1crDSi07e2Ermi6CdgO/B04x923FfOPKnLEyuyAsvAJ3Snu3s3M6gBL3b3ZEWxnAPCwu58Unr4CGODufyrRwCIiUapc7BG4ezqwyswuArCQHsX88h+AODNrFJ4+FUiJQEwRkahUJovAzN4CvgU6mdl6MxsBXAaMMLOFQDIwtDjbcvc8QocKPjOzxYRO7r0QmeQiItGnzB4aEhGRklEm9whERKTkVAo6wOFq2LCht2nTJugYIiJlyrx587a5e6OilpW5ImjTpg1z584NOoaISJliZoVHrv+XDg2JiMQ4FYGISIxTEYiIxDgVgYhIjFMRiIjEOBWBiEiMUxGIiMQ4FYGISJTbn5vHc1/8xPy1OyOy/TI3oExEJJbMWpLGmCkprNq2j+tObk/v+Hol/j1UBCIiUWjl1r3cNyWFWUu30q5RTV79XT9O6ljkHSKOmopARCSK7N2fy/+buZyXv1pF1UoVuevsLlw5sA1VKkXuSL6KQEQkCuTnOxMXbOCh/yxh6579XNSnJbcM7kTj2tUi/r1VBCIiAVu0fhejJyUzf+0uerSK44UrE+nZKq7Uvr+KQEQkINv27uefM5byztx1NKhZlUeHHcuFvVtSoYKVag4VgYhIKcvJy+e1b9fw+KfLyMzO45pBbfnzaQnUqVY5kDwqAhGRUvTV8m2MnpzMirS9nNixEfec05UOjWsFmklFICJSCtbtyOD+qSnMSN5CfP0avHhlIqd1aYxZ6R4GKoqKQEQkgjKz83j28xU8N3slFc245VedGDGoLdUqVww62n+pCEREIsDdmbJoEw9NS2Xj7iyG9mzObWd1plnd6kFH+xkVgYhICUvZmM7oycl8v2oHxzSvwxOX9KJvm/pBxzooFYGISAnZuS+bxz5Zxvjv1lC3emUevKA7v+3bioqlfDno4VIRiIgcpbx8583v1/Kvj5eyJyuXKwe24a+nd6RujWAuBz1cKgIRkaPw3crtjJ6cQuqmdAa2a8Co87rSuWmdoGMdFhWBiMgR2Lgrk4f+s4TJCzfSIq46z1zWm7O6NY2Ky0EPl4pAROQwZOXk8cLslTzz+U/ku3Pj6Qn8/sT2VK8SPZeDHi4VgYhIMbg7H6ds4f6pKazbkcmQ7k25Y0gXWtarEXS0o6YiEBH5BSvS9nDv5BS+XL6Njk1q8eY1/TmuQ8OgY5UYFYGIyEGkZ+XwxKfLefWb1dSoUpHR53bl8gGtqVSxfD3uXUUgIlJIfr7z/rz1PDJjCdv3ZXNJv3huOqMjDWpVDTpaRKgIREQKmL92J6MnJbNo/W4SW9fjlav70a1F3aBjRZSKQEQESEvP4uHpS5kwfz1N6lTliYt7cl6P5mXyctDDpSIQkZiWnZvPuK9X8eRny8nJc/54cnv+dEoHalaNnf8eY+dPKiJSyKyladw3OYWV2/ZxepfG3HV2V9o0rBl0rFKnIhCRmLN62z7um5LCZ0vSaNewJuOu7sspnRoHHSswKgIRiRn79ufy1KwVvPTlKqpUqsAdQzoz/Li2VKlUvi4HPVwqAhEp99ydiQs28I//LGFL+n6G9WnJ3wd3onHtakFHiwoRKwIzawW8BjQBHBjr7k8UWseAJ4AhQAYw3N3nRyqTiMSexet3M3pyMvPW7KRHy7o8d3kfesXXCzpWVInkHkEucJO7zzez2sA8M/vE3VMKrHMWkBD+6A88G/4sInJUtu/dzz8/XsrbP6yjQc0qPDLsWIb1bkmFKH9ITBAiVgTuvgnYFH69x8xSgRZAwSIYCrzm7g7MMbM4M2sW/loRkcOWk5fPG3PW8Ngny8jMzmPE8W254fQE6lQrGw+JCUKpnCMwszZAL+C7QotaAOsKTK8Pz/s/RWBmI4GRAPHx8RHLKSJl29crtnHv5GSWbdnLCQkNGXVuVzo0rh10rKgX8SIws1rABOBGd08/km24+1hgLEBiYqKXYDwRKQfW7cjggampTE/eTHz9Goy9og9ndG0SE6OCS0JEi8DMKhMqgfHu/kERq2wAWhWYbhmeJyLyizKz83j2i594/oufqGDGLb/qxIhBbalWuew+JCYIkbxqyICXgFR3f+wgq00CrjeztwmdJN6t8wMi8kvcnWmLN/PA1BQ27s7ivB7NuX1IZ5rVrR50tDIpknsExwNXAIvNbEF43h1APIC7PwdMI3Tp6ApCl49eHcE8IlIOLNmczuhJycxZuYMuzerw+MW96Ne2ftCxyrRIXjX0FXDIA3Thq4X+FKkMIlJ+7MrI5t+fLOP1OWuoW70y95/fjUv6xVNRl4MeNY0sFpGolpfvvPX9Wv718VJ2Z+ZwxYDW/PWMjsTVqBJ0tHJDRSAiUev7VTsYPSmZlE3pDGhXn1HnHkOXZnWCjlXuqAhEJOps2p3JQ9OWMGnhRprXrcbTl/ZmSPemuhw0QlQEIhI1snLyeOmrVTw1cwX57txwWgLXndSe6lV0OWgkqQhEJHDuzqepadw3JYW1OzI4q1tT7hjShVb1awQdLSaoCEQkUCvS9jJmSgqzl20loXEtxl/Tn+M7NAw6VkxREYhIINKzcnjy0+W88s1qqlepyKhzu3L5gNZUrhjbD4kJgopAREpVfr7z/vz1PDJ9Cdv3ZXNx31bcfGYnGtSqGnS0mKUiEJFS8+PanYyenMLCdbvoHR/HuOH96N6ybtCxYp6KQEQiLm1PFo9MX8r789bTuHZVHv9tT4b2bK7LQaOEikBEIiY7N59Xv1nNE58tZ39uHted3J4/ndKBWlX1X0800bshIhHx+dI0xkxJYeXWfZzWuTF3ndOVtg1rBh1LiqAiEJEStXrbPu6fmsKnqWm0bViTccP7ckrnxkHHkkNQEYhIidi3P5enZ63gxS9XUbmicftZnbn6+LZUqaTLQaOdikBEjoq7M2nhRh6clsqW9P38uncLbhvcmcZ1qgUdTYpJRSAiRyxpw25GT0pm7pqddG9Rl2cu60Of1vWCjiWHSUUgIodt+979/PPjZbz9w1rq16jCIxcey7A+Lamgh8SUSSoCESm23Lx83pizhsc+WUZGdh6/O74tN5yWQN3qlYOOJkdBRSAixfLNim3cOzmFpVv2MKhDQ0ad25WEJrWDjiUlQEUgIoe0fmcGD05LZdrizbSsV53nr+jDmV2baFRwOaIiEJEiZeXk8dwXP/Hs5z9RwYybzujItSe2o1plPSSmvFERiMj/4e78J2kzD0xNZcOuTM7t0Zzbz+pM87jqQUeTCFERiMh/rdq2j7snJvHVim10blqbd0YOoH+7BkHHkghTEYgIWTl5PPt56DBQ1UoVGDP0GC7tF08lPSQmJqgIRGLc7GVbueejJFZvz2Boz+bceXYXGtfWqOBYoiIQiVFb0rO4b0oKUxZtol3Dmrwxoj+DEvSs4FikIhCJMXn5zuvfruafHy8jOy+fv53Rkd+f1I6qlXQ1UKxSEYjEkEXrd3Hnh0ks3rCbExIact/QbrTRMwJinopAJAbszszhXx8v5fU5a2hUqypPXdqLs7s306AwAVQEIuXagVtE3zcllR379nPVwDbcdGZHalfTvYHkf1QEIuXUyq17ufujJL5esZ0eLevyytV96daibtCxJAqpCETKmaycPJ75/Cee+/wnqlauwH3nd+PSfvFU1C2i5SBUBCLlyBfhMQFrtmdwfs/m3KExAVIMESsCM3sZOAdIc/duRSw/GfgIWBWe9YG7j4lUHpHybEt6FmOmpDA1PCZg/DX9Ob6DxgRI8URyj+AV4CngtUOs86W7nxPBDCLlWm5ePq/PWcO/NCZAjkLEisDdZ5tZm0htXyTWLVi3izs/XEzyxnRO6tiIMUOPoXUDjQmQwxf0OYKBZrYQ2Ajc7O7JRa1kZiOBkQDx8fGlGE8k+uzOzOHRGUsY/91aGteuytOX9mZI96YaEyBHLMgimA+0dve9ZjYEmAgkFLWiu48FxgIkJiZ6qSUUiSLuzkcLNnL/1NCYgOHHteFvZ2hMgBy9wIrA3dMLvJ5mZs+YWUN33xZUJpFo9dPWvdw9MYlvftpOj1ZxGhMgJSqwIjCzpsAWd3cz6wdUALYHlUckGmXl5PH0rBU8/8VKqlWuwP3nd+MSjQmQEhbJy0ffAk4GGprZemAUUBnA3Z8DhgHXmVkukAlc7O467CMS9vnSNO75KJm1OzK4oFcL7hjShUa1qwYdS8qhSF41dMkvLH+K0OWlIlLA5t2h5wRMXbyJdo1q8uY1/TlOYwIkgoK+akhEwnLz8nnt2zX86+Ol5OY7N5/ZkWtP1JgAiTwVgUgU+HHtTu78MImUTemc3KkRY87rRnyDGkHHkhihIhAJ0O6MHB6ZsYQ3vw+NCXj2st4M7qYxAVK6VAQiAXB3Ji7YwANTU9mxL5urj2vL387sSK2q+icppU8/dSKlbEVaaEzAtyu307NVHK9c3U9jAiRQKgKRUpKVk8dTM1fw/OyfqF65Ig9c0I1L+sZTQWMCJGAqApFSMGtpGqPCYwJ+3asFd5zdhYa1NCZAooOKQCSCNu/OYsyUZKYt3kz7RjV589r+HNdeYwIkuqgIRCIgNy+fV75Zzb8/WUZuvnPLrzpx7QntqFKpQtDRRH5GRSBSwuaHxwSkbkrnlE6NuFdjAiTKFbsIzGwQkODu48ysEVDL3Vf90teJxIrdGTk8PGMJb32/lia1q/Hc5b351TEaEyDRr1hFYGajgESgEzCO0M3j3gCOj1w0kbLB3fnwx9CYgF2ZOYw4vi03nqExAVJ2FPcn9QKgF6GHyeDuG82sdsRSiZQRK9L2cNfEJOas3EGv+DheP787XZvXCTqWyGEpbhFkh58b4ABmpgejSkzLzM7jqVnLGTt7JTWqVOLBC7pzcd9WGhMgZVJxi+BdM3seiDOza4HfAS9ELpZI9Jq1JI17JiWxbkcmv+4dek6AxgRIWVasInD3f5rZGUA6ofME97j7JxFNJhJlNu3OZMzkFP6TtJkOjWvx9sgBDGjXIOhYIketuCeLawIz3f0TM+sEdDKzyu6eE9l4IsHTmAAp74p7aGg2cIKZ1QOmA3OB3wKXRSqYSDSYt2Ynd03835iAMUO70aq+xgRI+VLcIjB3zzCzEcCz7v6ImS2IYC6RQO3KyObh6Ut56/u1NKurMQFSvhW7CMxsIKE9gBHheXp+npQ77s4H8zfw4LTQmIBrT2jLjad3pKbGBEg5Vtyf7huB24EP3T3ZzNoBsyKWSiQAy7eExgR8t2oHvePjeOOC7nRppjEBUv4V96qhL4AvCkyvBG6IVCiR0pSZncf/mxkaE1CzaiUe+nV3fpuoMQESO4p71VAicAfQpuDXuPuxkYklUjpmLtnCPR8ls35nJhf2bskdQzrTQGMCJMYU99DQeOAWYDGQH7k4IqVj465M7p2czIzkLSQ0rsU7IwfQX2MCJEYVtwi2uvukiCYRKQU5efm88vVq/v3pMvLd+fvgTlwzSGMCJLYVtwhGmdmLwGfA/gMz3f2DiKQSiYB5a3Zw54dJLNm8h9M6N2b0ecdoTIAIxS+Cq4HOhG4/feDQkAMqAol6O/dl8/D0Jbz9wzqa1a3G81f04cyuTTQmQCSsuEXQ1907RTSJSAlzdyaExwTszsxh5Int+MtpCRoTIFJIcf9FfGNmXd09JaJpRErIsvCYgO9X7aBP63rcf343jQkQOYhfLAIL7T+fBFxmZqsInSMwwHX5qESbzOw8npy5nBdmr6RWtUo8fGF3LuqjMQEih/KLRRB+IE1jIKEU8ogcsW9+2sZtExazdkcGw/q05PazNCZApDiKe2hoAtDY3X+IZBiRI7F3fy4PTUtl/HdradOghp4TIHKYilsE/QkdGloD7EOHhiRKzF62lds/WMzG3ZlcM6gtN53ZiepVdD9EkcNR3CL41eFu2MxeBs4B0ty9WxHLDXgCGAJkAMPdff7hfh+JTbszc3hgagrvzl1P+0Y1mXDdcfSOrxd0LJEyqbg3nVtzBNt+BXgKeO0gy88idN4hgdAex7PhzyKHNHPJFu74IIm0PVlcd3J7/nJaAtUqay9A5EhF7IJqd59tZm0OscpQ4DV3d2COmcWZWTN33xSpTFK27crI5t7JKXz44wY6NanN2Cv7cGzLuKBjiZR5QY6saQGsKzC9PjzvZ0VgZiOBkQDx8fGlEk6iy/Skzdw1MYldGdnccFoC15/SQfcHEikhZWKIpbuPBcYCJCYmesBxpBRt37ufeyYlM3XRJro2q8Orv+vLMc3rBh1LpFwJsgg2AK0KTLcMzxPB3ZmyaBOjJiWzJyuHm8/syO9Pak/litoLEClpQRbBJOB6M3ub0Eni3To/IABpe7K4e2ISM5K30KNlXR69aAAdm9QOOpZIuRWxIjCzt4CTgYZmth4YRejupbj7c8A0QpeOriB0+ejVkcoiZYO78+GPG7h3cgqZOXncdlZnrhnUlkraCxCJqEheNXTJLyx34E+R+v5StmzencWdHy7msyVp9I6P45FhPejQuFbQsURiQpk4WSzll7vz3tz13Dc1hZy8fO4+pyvDj2tDRd0kTqTUqAgkMBt2ZXLbhEV8uXwb/dvW5+ELj6VNw5pBxxKJOSoCKXX5+c6b36/loWmpOHDf0GO4rH9r3SpaJCAqAilVa7dncOuERXy7cjuDOjTkoV9313ODRQKmIpBSkZ/vvPrtah6ZvpRKFYx//Lo7v+3bSs8NFokCKgKJuJVb93LrhEX8sHonJ3dqxIMXdKd5XPWgY4lImIpAIiYv33npq5X86+NlVK1UgX9e1IMLe7fQXoBIlFERSEQs37KHW95fxIJ1uzi9SxMeuKAbTepUCzqWiBRBRSAlKjcvn+dnr+SJT5dTs2pFnri4J+f1aK69AJEopiKQEpO6KZ1b3l9I0oZ0hnRvyr3ndaNRbT08XiTaqQjkqGXn5vPM5yt4etYK6lavzDOX9WZI92ZBxxKRYlIRyFFJ2rCbm99byJLNexjaszmjzj2G+jWrBB1LRA6DikCOyP7cPJ78bDnPfbGSBjWr8MKViZzRtUnQsUTkCKgI5LD9uHYnf39/EcvT9jKsT0vuPrsrdWtUDjqWiBwhFYEUW1ZOHo99sowXv1xJkzrVGHd1X07p1DjoWCJylFQEUixzV+/g7+8vYuW2fVzSL57bh3SmTjXtBYiUByoCOaSM7FwenbGUV75ZTYu46oy/pj/Hd2gYdCwRKUEqAjmob3/azq0TFrF2RwZXDmzNrYM7U7OqfmREyhv9q5af2bs/l3/8J5U35qyldYMavDNyAP3bNQg6lohEiIpA/o/Zy7Zy+weL2bg7kxGD2nLzmZ2oXqVi0LFEJIJUBAJAelYOD0xJ5Z2562jXqCbv/2EgfVrXDzqWiJQCFYEwc8kW7vggibQ9WfzhpPbceHoC1SprL0AkVqgIYtiujGzGTE7hgx830LFJLZ6/4nh6tIoLOpaIlDIVQYyakbyZuyYmsWNfNn8+tQPXn9qBqpW0FyASi1QEMWb73v2MnpzC5IUb6dqsDuOG96Vbi7pBxxKRAKkIYoS7M3XxJkZ9lEx6Vg5/O6Mj153cnsoVKwQdTUQCpiKIAVv37OfuiUlMT97MsS3rMn5Yfzo3rRN0LBGJEiqCcszdmbhgA/dOTiEjO49bB3fm2hPaUkl7ASJSgIqgnNqxL5vbJizi45Qt9IqP49FhPejQuFbQsUQkCqkIyqHZy7Zy03sL2Z2Rwx1DOjNiUDsqVtDD40WkaCqCciQrJ49Hpi/l5a9XkdC4Fq9e3Y+uzXUuQEQOTUVQTizZnM6Nby9gyeY9XDWwNbcP6aLRwSJSLBE9a2hmg81sqZmtMLPbilg+3My2mtmC8Mc1kcxTHrk7475exXlPfc22vfsZN7wv9w7tphIQkWKL2B6BmVUEngbOANYDP5jZJHdPKbTqO+5+faRylGdpe7K45b1FfLFsK6d2bswjw46lYa2qQccSkTImkoeG+gEr3H0lgJm9DQwFCheBHIFPUrZw64RF7Nufy33nd+Py/vGY6YSwiBy+SBZBC2Bdgen1QP8i1rvQzE4ElgF/dfd1RawjYRnZudw/NZU3v1tL12Z1ePKSnnRoXDvoWCJShgV9sngy8Ja77zez3wOvAqcWXsnMRgIjAeLj40s3YRRJ2rCbG97+kZVb9zHyxHbcdGZH3ShORI5aJItgA9CqwHTL8Lz/cvftBSZfBB4pakPuPhYYC5CYmOglGzP65eU7L3y5kn99vJQGNavqAfIiUqIiWQQ/AAlm1pZQAVwMXFpwBTNr5u6bwpPnAakRzFMmbdyVyd/eXcCclTs4q1tTHrygO/VqVgk6loiUIxErAnfPNbPrgRlAReBld082szHAXHefBNxgZucBucAOYHik8pRFUxZt5I4PFpOb7zxy4bFclNhSJ4RFpMSZe9k60pKYmOhz584NOkZE7cnKYfSkFCbMX0+PVnE88duetGlYM+hYIlKGmdk8d08salnQJ4ulkHlrdvLXdxawfmcGN5zagT+flqBnBohIRKkIokRevvPUzBU8OXM5TetU453fD6Rvm/pBxxKRGKAiiALb9u7nxrcX8NWKbQzt2Zz7zu9GnWqVg44lIjFCRRCw71ft4Po357M7M4eHL+zObxJb6YSwiJQqFUFA8vOdsV+u5NEZS2lVrzqv6JbRIhIQFUEAdmVkc/N7C/k0NY0h3Zvy8IXHUluHgkQkICqCUrZw3S7+OH4+aXuyGHVuV4Yf10aHgkQkUCqCUuLuvD5nDfdPSaVR7aq8+/uB9IqvF3QsEREVQWnYuz+X2yYsYsqiTZzSqRGP/aanbhMhIlFDRRBhSzan88c35rN6+z7+PrgTfzixPRX0IHkRiSIqggh6b+467v4oidrVKvPmtQMY0K5B0JFERH5GRRABmdl53PNREu/NW89x7RvwxMW9aFRbj5AUkeikIihhK7fu5Y/j57N0yx5uOLUDfzm9IxV1KEhEopiKoARNWbSRW99fRJVKFRg3vC8nd2ocdCQRkV+kIigB+3PzeHBqKq9+u4be8XE8dWlvmsdVDzqWiEixqAiO0rodGVz/5nwWrt/NNYPacutZnXXbaBEpU1QER+Gz1C387d2F5Oc7z13eh8HdmgYdSUTksKkIjkB+vvPkzOU8/ulyjmleh2cu603rBnqCmIiUTSqCw5SRncvN7y1k2uLNXNi7JQ9c0I1qlSsGHUtE5IipCA7Dhl2ZXPvqXJZsTufOIV245oS2umGciJR5KoJimrdmB79/fR77c/J5aXhfTtGloSJSTqgIiuG9ueu488MkmsdV4+2RiXRoXDvoSCIiJUZFcAh5+c5D01J58atVHN+hAU9f2pu4GrprqIiULyqCg0jPyuHPb/7IF8u2ctXA1tx1TleNDxCRcklFUIS09CyuGvcDy7fs4YELunFZ/9ZBRxIRiRgVQSGrtu3jipe+Y8e+bF4a3peTOjYKOpKISESpCApYtH4XV4/7AQfeunYAPVrFBR1JRCTiVARhs5dt5Q9vzKN+zSq89rt+tGtUK+hIIiKlQkUAfLRgAze9u5CEJrV59eq+NK5TLehIIiKlJuaL4MUvV3L/1FQGtKvP2CsTqVOtctCRRERKVcwWQX6+8/D0JTw/eyVDujflsd/01D2DRCQmxWQRuDv3TErijTlruWJAa0afd4weJykiMSvmisDdeXBaKm/MWcsfTmrPrYM76cZxIhLTIjpU1swGm9lSM1thZrcVsbyqmb0TXv6dmbWJZB6AJz5bzgtfruKqga1VAiIiRLAIzKwi8DRwFtAVuMTMuhZabQSw0907AP8GHo5UHoDx363h8U+Xc1Gflow69xiVgIgIkd0j6AescPeV7p4NvA0MLbTOUODV8Ov3gdMsQv87/7B6B6M+SuaUTo34x4XHUkHnBEREgMgWQQtgXYHp9eF5Ra7j7rnAbqBB4Q2Z2Ugzm2tmc7du3XpEYWpUqcjA9g14/OJeOjEsIlJAmbidpruPdfdEd09s1OjI7v1zTPO6vD6iP3Wra5yAiEhBkSyCDUCrAtMtw/OKXMfMKgF1ge0RzCQiIoVEsgh+ABLMrK2ZVQEuBiYVWmcScFX49TBgprt7BDOJiEghERtH4O65ZnY9MAOoCLzs7slmNgaY6+6TgJeA181sBbCDUFmIiEgpiuiAMnefBkwrNO+eAq+zgIsimUFERA6tTJwsFhGRyFERiIjEOBWBiEiMUxGIiMQ4K2tXa5rZVmDNEX55Q2BbCcaJhGjPGO35QBlLQrTng+jPGG35Wrt7kSNyy1wRHA0zm+vuiUHnOJRozxjt+UAZS0K054Pozxjt+QrSoSERkRinIhARiXGxVgRjgw5QDNGeMdrzgTKWhGjPB9GfMdrz/VdMnSMQEZGfi7U9AhERKURFICIS42KmCMxssJktNbMVZnZb0HkAzOxlM0szs6QC8+qb2Sdmtjz8uV6A+VqZ2SwzSzGzZDP7SxRmrGZm35vZwnDGe8Pz25rZd+H3+53wrdADY2YVzexHM5sSpflWm9liM1tgZnPD86LpfY4zs/fNbImZpZrZwCjL1yn8d3fgI93MboymjIcSE0VgZhWBp4GzgK7AJWbWNdhUALwCDC407zbgM3dPAD4LTwclF7jJ3bsCA4A/hf/eoinjfuBUd+8B9AQGm9kA4GHg3+7eAdgJjAguIgB/AVILTEdbPoBT3L1ngWvfo+l9fgKY7u6dgR6E/i6jJp+7Lw3/3fUE+gAZwIfRlPGQ3L3cfwADgRkFpm8Hbg86VzhLGyCpwPRSoFn4dTNgadAZC2T7CDgjWjMCNYD5QH9CIzorFfX+B5CrJaH/BE4FpgAWTfnCGVYDDQvNi4r3mdCTC1cRvrgl2vIVkfdM4Otozlj4Iyb2CIAWwLoC0+vD86JRE3ffFH69GWgSZJgDzKwN0Av4jijLGD7ssgBIAz4BfgJ2uXtueJWg3+/Hgb8D+eHpBkRXPgAHPjazeWY2MjwvWt7ntsBWYFz48NqLZlYzivIVdjHwVvh1tGb8P2KlCMokD/0aEfj1vWZWC5gA3Oju6QWXRUNGd8/z0C55S6Af0DnIPAWZ2TlAmrvPCzrLLxjk7r0JHT79k5mdWHBhwO9zJaA38Ky79wL2UegQSzT8HAKEz/WcB7xXeFm0ZCxKrBTBBqBVgemW4XnRaIuZNQMIf04LMoyZVSZUAuPd/YPw7KjKeIC77wJmETrUEmdmB57AF+T7fTxwnpmtBt4mdHjoCaInHwDuviH8OY3Qse1+RM/7vB5Y7+7fhaffJ1QM0ZKvoLOA+e6+JTwdjRl/JlaK4AcgIXylRhVCu26TAs50MJOAq8KvryJ0XD4QZmaEniud6u6PFVgUTRkbmVlc+HV1QucwUgkVwrDwaoFldPfb3b2lu7ch9HM3090vi5Z8AGZW08xqH3hN6Bh3ElHyPrv7ZmCdmXUKzzoNSCFK8hVyCf87LATRmfHngj5JUVofwBBgGaHjx3cGnSec6S1gE5BD6LeeEYSOH38GLAc+BeoHmG8QoV3ZRcCC8MeQKMt4LPBjOGMScE94fjvge2AFod30qlHwfp8MTIm2fOEsC8MfyQf+fUTZ+9wTmBt+nycC9aIpXzhjTWA7ULfAvKjKeLAP3WJCRCTGxcqhIREROQgVgYhIjFMRiIjEOBWBiEiMUxGIiMQ4FYHIYQjfpbPh0a4jEk1UBCIiMU5FIHIQZjYxfBO25AI3YjuwrE343vjjw/fHf9/MahRY5c9mNj98j//O4a/pZ2bfhm+c9k2BkbIigVIRiBzc79y9D5AI3GBmDQot7wQ84+5dgHTgjwWWbfPQTdyeBW4Oz1sCnOChG6fdAzwY0fQixaQiEDm4G8xsITCH0E0LEwotX+fuX4dfv0HolhwHHLhB3zxCz5yA0H3137PQE+n+DRwTidAih0tFIFIEMzsZOB0Y6KGnn/0IVCu0WuH7sxSc3h/+nEfoNsoA9wGz3L0bcG4R2xMJhIpApGh1gZ3unhE+xj+giHXizWxg+PWlwFfF2OaB200PL5GUIiVARSBStOlAJTNLBf5B6PBQYUsJPcQlldDdMJ/9hW0+AjxkZj/yv70EkcDp7qMiRyD86M4p4cM8ImWa9ghERGKc9ghERGKc9ghERGKcikBEJMapCEREYpyKQEQkxqkIRERi3P8H7x6HF9chM3oAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "cv_ridge = pd.Series(cv_ridge, index = alphas)\n", + "cv_ridge.plot(title = \"Validation\")\n", + "plt.xlabel(\"alpha\")\n", + "plt.ylabel(\"rmse\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4.047404508915239e-09" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cv_ridge.min()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "524.7581840311993" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_lasso = LassoCV(alphas = [1, 0.1, 0.001, 0.0005]).fit(X_train, y)\n", + "rmse_cv(model_lasso).mean()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lasso picked 269 variables and eliminated the other 21 variables\n" + ] + } + ], + "source": [ + "coef = pd.Series(model_lasso.coef_, index = X_train.columns)\n", + "print(\"Lasso picked \" + str(sum(coef != 0)) + \" variables and eliminated the other \" + str(sum(coef == 0)) + \" variables\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Coefficients in the Lasso Model')" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "imp_coef = pd.concat([coef.sort_values().head(10),coef.sort_values().tail(10)])\n", + "imp_coef.plot(kind = \"barh\")\n", + "plt.title(\"Coefficients in the Lasso Model\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# 接下来观察真实结果与预测结果的残差\n", + "preds = pd.DataFrame({\"preds\":model_lasso.predict(X_train), \"true\":y})\n", + "preds[\"residuals\"] = preds[\"true\"] - preds[\"preds\"]\n", + "preds.plot(x = \"preds\", y = \"residuals\",kind = \"scatter\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "train = pd.concat([train,preds], axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "x = [1,2,4,8]\n", + "ts = pd.Series(x) #数据x假设已经获取\n", + "ae = tsf.feature_extraction.feature_calculators.absolute_sum_of_changes(ts)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ2I9AAAAAAA0IlIDwAAAAAAnYj0AAAAAADQiUgPAAAAAACdiPQAAAAAANCJSA8AAAAAAJ10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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import seaborn as sns\n", + "\n", + "var = 'LotShape'\n", + "data = pd.concat([train['residuals'], train[var]], axis=1)\n", + "f, ax = plt.subplots(figsize=(26, 12))\n", + "fig = sns.boxplot(x=var, y=\"residuals\", data=data)\n", + "#fig.axis(ymin=0, ymax=15);" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[19:28:41] WARNING: ../src/learner.cc:627: \n", + "Parameters: { \"silent\" } might not be used.\n", + "\n", + " This could be a false alarm, with some parameters getting used by language bindings but\n", + " then being mistakenly passed down to XGBoost core, or some parameter actually being used\n", + " but getting flagged wrongly here. Please open an issue if you find any such cases.\n", + "\n", + "\n", + "[19:28:42] WARNING: ../src/learner.cc:627: \n", + "Parameters: { \"silent\" } might not be used.\n", + "\n", + " This could be a false alarm, with some parameters getting used by language bindings but\n", + " then being mistakenly passed down to XGBoost core, or some parameter actually being used\n", + " but getting flagged wrongly here. Please open an issue if you find any such cases.\n", + "\n", + "\n", + "[19:28:42] WARNING: ../src/learner.cc:627: \n", + "Parameters: { \"silent\" } might not be used.\n", + "\n", + " This could be a false alarm, with some parameters getting used by language bindings but\n", + " then being mistakenly passed down to XGBoost core, or some parameter actually being used\n", + " but getting flagged wrongly here. Please open an issue if you find any such cases.\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dtrain = xgb.DMatrix(X_train, label = y)\n", + "dtest = xgb.DMatrix(X_test)\n", + "\n", + "params = {\"max_depth\":2, \"eta\":0.1, 'silent': True}\n", + "model = xgb.cv(params, dtrain, num_boost_round=500, early_stopping_rounds=100)\n", + "model.loc[30:,[\"test-rmse-mean\", \"train-rmse-mean\"]].plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 数据探索" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
FeatureUnique_valuesPercentage of missing valuesPercentage of values in the biggest categorytype
72PoolQC399.52054899.520548object
74MiscFeature496.30137096.301370object
6Alley293.76712393.767123object
73Fence480.75342580.753425object
57FireplaceQu547.26027447.260274object
3LotFrontage11017.73972617.739726float64
63GarageQual55.54794589.794521object
58GarageType65.54794559.589041object
60GarageFinish35.54794541.438356object
64GarageCond55.54794590.821918object
\n", + "
" + ], + "text/plain": [ + " Feature Unique_values Percentage of missing values \\\n", + "72 PoolQC 3 99.520548 \n", + "74 MiscFeature 4 96.301370 \n", + "6 Alley 2 93.767123 \n", + "73 Fence 4 80.753425 \n", + "57 FireplaceQu 5 47.260274 \n", + "3 LotFrontage 110 17.739726 \n", + "63 GarageQual 5 5.547945 \n", + "58 GarageType 6 5.547945 \n", + "60 GarageFinish 3 5.547945 \n", + "64 GarageCond 5 5.547945 \n", + "\n", + " Percentage of values in the biggest category type \n", + "72 99.520548 object \n", + "74 96.301370 object \n", + "6 93.767123 object \n", + "73 80.753425 object \n", + "57 47.260274 object \n", + "3 17.739726 float64 \n", + "63 89.794521 object \n", + "58 59.589041 object \n", + "60 41.438356 object \n", + "64 90.821918 object " + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stats = []\n", + "for col in train.columns:\n", + " stats.append((col, train[col].nunique(), train[col].isnull().sum() * 100 / train.shape[0], train[col].value_counts(normalize=True, dropna=False).values[0] * 100, train[col].dtype))\n", + "stats_df = pd.DataFrame(stats, columns=['Feature', 'Unique_values', 'Percentage of missing values', 'Percentage of values in the biggest category', 'type'])\n", + "stats_df.sort_values('Percentage of missing values', ascending=False)[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "missing = train.isnull().sum()\n", + "missing = missing[missing > 0]\n", + "missing.sort_values(inplace=True)\n", + "missing.plot.bar()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "count 1460.000000\n", + "mean 180921.195890\n", + "std 79442.502883\n", + "min 34900.000000\n", + "25% 129975.000000\n", + "50% 163000.000000\n", + "75% 214000.000000\n", + "max 755000.000000\n", + "Name: SalePrice, dtype: float64" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train['SalePrice'].describe()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEGCAYAAABo25JHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAAnHUlEQVR4nO3deZhT5dnH8e/NsMviwojIriCKC4oD7gVbimhFq6IVcWtVtHVpa/tWrVZ9te2r1lq74ILWaq3FhbpQl+K+lm1EEREXBBWowoAiIAIC9/vHk+nEMTOTYXJykpzf57rOleTkSXITJrnz7ObuiIhIcjWLOwAREYmXEoGISMIpEYiIJJwSgYhIwikRiIgkXPO4A2isTp06ea9eveIOQ0SkqLz88svL3L08031Flwh69epFZWVl3GGIiBQVM3u/rvvUNCQiknBKBCIiCadEICKScEoEIiIJp0QgIpJwSgQiIgkXWSIws9vMbKmZvd5AuUFmtsHMRkUVi4iI1C3KGsHtwIj6CphZGXA18HiEcYiISD0iSwTu/jzwcQPFzgX+ASyNKg4REalfbDOLzawrcBRwMDCogbJjgbEAPXr0iD44Sazx42uujx0bXxwi+RRnZ/H1wAXuvqmhgu4+3t0r3L2ivDzjUhkiIrKZ4lxrqAK428wAOgGHmdkGd38wxphERBIntkTg7r2rr5vZ7cDDSgIiIvkXWSIwswnAUKCTmS0CLgNaALj7TVG9roiINE5kicDdRzei7KlRxSEiIvXTzGKRiI0f/+XRSCKFRolARCThlAhERBJOiUBEJOGKbs9ikVzRLGKRQDUCEZGEUyIQEUk4JQKROmjYpySFEoGISMIpEYiIJJwSgYhIwikRiIgknBKBiEjCKRGIiCScEoGISMIpEYiIJJwSgYhIwmnROZEGaHE6KXWqEYiIJJwSgYhIwikRiIgkXGSJwMxuM7OlZvZ6HfePMbPXzGy2mf3bzAZEFYuIiNQtyhrB7cCIeu5fAAxx992BKwEt+CsiEoPIRg25+/Nm1que+/+ddnMq0C2qWEREpG6F0kdwGvBYXXea2VgzqzSzyqqqqjyGJSJS+mJPBGZ2MCERXFBXGXcf7+4V7l5RXl6ev+BERBIg1gllZrYHcCtwqLsvjzMWEZGkiq1GYGY9gPuBk9z97bjiEBFJushqBGY2ARgKdDKzRcBlQAsAd78JuBTYBrjBzAA2uHtFVPGIiEhmUY4aGt3A/acDp0f1+iIikp3YO4tFRCReSgQiIgmnRCAiknBKBCIiCadEICKScEoEIiIJp0QgIpJwSgQiIgmnRCAiknBKBCIiCRfr6qMihWzuXJgzBzZuhF12gd12izsikWgoEYjU8sUX8Le/wdSp0Lw5NGsGTz8NXbrAfvvBAO2uLSVGiUCklvvuC0ngsMPC0awZzJwZzu+zD4wfDyefHHeUIrmjRCCSprISnnsOvvlNOPLImvODBsHOO8Ojj8Ipp8DChfDzn0NYQb3xxo8Pl2PHNj1mkaZSZ7FIyrp14Vd/r15w1FFfvb99e3jsMTjxRLjkEjjrLNiwIe9hiuScagQiKX/5C6xYAaeeCmVlmcu0bAl//Sv06AG//jUsXgz33ANbbJHPSEVyS4lABFi/Hv7v/2CHHUITUH3M4Fe/gu7d4eyzYcgQmDgx1CTqU90cJFJo1DQkAkyaBB98EDqHs233P+ssePBBeOedMJLojjvAPdIwRSKhRCBCaO7ZfnvYddfGPW7kSHj1Vdh999Ck9I1vwFtvRRGhSHSUCCTxVq0KncBjxoShoo3Vuzc8/zzcdBO88grssQdcdhmsXZv7WEWiEFkiMLPbzGypmb1ex/1mZn8ws3lm9pqZDYwqFpH6zJgRRv80Zm7A+PE1B4QEcuaZ8OabMGoUXHFFSAivvRZNzCK5FGWN4HZgRD33Hwr0TR1jgRsjjEWkTjNmhDb+XCwh0bkz3HUXPPEEfPYZHHAAvJ7xp5BI4YgsEbj788DH9RQ5EvirB1OBLc2sS1TxiGSyciUsWABHH53b5x02DKZPh759Q5PRu+/m9vlFcinOPoKuwMK024tS577CzMaaWaWZVVZVVeUlOEmGOXPCSJ/DD8/9c3ftCpMnw1ZbwbhxsHx57l9DJBeKorPY3ce7e4W7V5SXl8cdjpSQ2bNhyy1hr72ief7ycjj33NAHceedGl4qhSnORLAY6J52u1vqnEhebNgQagS77bb5awZlY9tt4ZhjwrLWL70U3euIbK44E8Ek4OTU6KF9gU/d/cMY45GEeffdMMRz992b9jy1RxBlctBBob/ggQc0rFQKT5TDRycAU4B+ZrbIzE4zs7PM7KxUkUeB+cA84BbgB1HFIpLJ3Llh2GdDS0rkQrNmoVawenXY20CkkES21pC7j27gfgfOjur1RRry9tvQsye0bp2f1+vdO8wteOIJGDoU2rbNz+uKNKQoOotFcm3VqjBsNB+1gXQjR8KaNfDCC+F2Ns1KIlFTIpBEevFF2LQJ+vXL7+v26AE77RQ2v9m0Kb+vLVIXJQJJpKefDvsR77hj/l976NAwp2D27Py/tkgmSgSSSM8+G9rsW7bM/2vvuWeYZPbMM/l/bZFMtDGNJM7q1WGV0EMOie416mvzLysLaxA98gh8/DFsvXV0cYhkQzUCSZxp02Djxniahartu2+YZTxtWnwxiFRTIpDEeemlMJM4zkRQXg59+sDUqVp2QuKnRCCJ8+KLYTZxmzbxxrHvvvDRR/Dee/HGIaJEIImyYQNMmRLa6OO2996hv+Dll+OORJJOiUASZfbs0Fl84IFxRxJmFvfvDzNnqnlI4qVRQ5IoU6aEy/33h8cfrzmfq5m9jX2egQNDcnr//dy8vsjmUI1AEmXatLAsdM+ecUcSDBgQFqRT85DESYlAEmX6dNhnn2j3H2iMLbaAXXZR85DES4lAEmPFCnjzTRg8OO5IvmzgQFi2LCQDkTgoEUhiVFaGy332iTeO2vbcMzQPTZwYdySSVEoEkhjVs3gHDYo3jtratQuroE6cqOYhiYcSgSTGtGnhC3fLLeOO5Kv23hvmzYNZs+KORJIoq0RgZveb2bfMTIlDipJ7TUdxIapuHrrvvrgjkSTK9ov9BuAE4B0zu8rM8rydh0jTfPABLFlSuImgffuwT4GahyQOWSUCd3/S3ccAA4H3gCfN7N9m9l0zaxFlgCK5UN0/UGgjhtKNGhX2UZ4zJ+5IJGmybuoxs22AU4HTgVeA3xMSwxORRCaSQ9OnQ6tWYfP4pohyf+GjjgrzGzZ39JD2PpbNlW0fwQPAC0BbYKS7H+Hu97j7uUC7eh43wszeMrN5ZnZhhvt7mNkzZvaKmb1mZodt7j9EpD7TpoXx+nHsSJat7baDgw6Cf/wj7kgkabKtEdzi7v3d/f/c/UMAM2sF4O4VmR5gZmXAOOBQoD8w2sz61yp2CXCvu+8FHE/oixDJqQ0bwhIOhdwsVG3UKHj99TDxTSRfsk0Ev8xwbkoDjxkMzHP3+e6+HrgbOLJWGQc6pK53BP6TZTwiWXv9dfj888LtKE539NHhUrUCyad6E4GZbWdmewNtzGwvMxuYOoYSmonq0xVYmHZ7UepcusuBE81sEfAocG4dcYw1s0ozq6yqqmrgZUW+rBg6iqt17Qr77adZxpJfDdUIDgGuBboB1wG/TR3nAz/PweuPBm53927AYcCdmeYquPt4d69w94ry8vIcvKwkyfTp0KkT7LBD3JFkZ9QoePXVMMFMJB/qTQTufoe7Hwyc6u4Hpx1HuPv9DTz3YqB72u1uqXPpTgPuTb3WFKA10KlR/wKRBkybFmoDhbLiaEOOOSZcanKZ5Eu9G9OY2Ynu/jegl5mdX/t+d7+unofPAPqaWW9CAjieMCkt3QfAN4DbzWwXQiJQ2480WvqwybFja66vWgVvvAHHHpv/mBor/d+w334wYQJcdFF88UhyNNQ0tEXqsh3QPsNRJ3ffAJwDTAbmEkYHzTGzK8zsiFSxnwBnmNksYAKh5qF5lZIzlZVhpm4xdBSnO+GEsHPZ7NlxRyJJUG+NwN1vTl3+7+Y8ubs/SugETj93adr1N4AC2EZcSlUxdRSnO+44+NGP4K674Kqr4o5GSl22E8quMbMOZtbCzJ4ysyozOzHq4ESaavp06NMHtt467kgaZ9ttYfhw+PvfYdOmuKORUpftPILh7r4SOJyw1lAf4H+iCkokV6ZNK75moWonnQQLF8Izz8QdiZS6bBNBdRPSt4D73P3TiOIRyZlFi+A//yneRPDtb4e9E267Le5IpNTV20eQ5mEzexP4HPi+mZUDa6MLS6TpfpmaD/+ftPnqxbQoW5s2MGYM3Hor/OlPsNVWcUckpSrbZagvBPYHKtz9C+AzvrpchEhBWbAAmjeHbt3ijmTzfe97sG5dGEoqEpXG7Di2M/AdMzsZGAUMjyYkkdxYsAC6d4cWRbxjxl57hVVTb7xRG9ZIdLIdNXQnYamJA4FBqSPjqqMihWDDBnj/fejdO+5ImsYMzj47LJz3/PNxRyOlKts+ggqgvyZ7SbGYMwfWr69JBMXUN1Db8cfDT38a+gmGDIk7GilF2TYNvQ5sF2UgIrlUPZGs2GsEAG3bwmmnwQMPhFqOSK5lmwg6AW+Y2WQzm1R9RBmYSFNMmwbt2oVVR0vBueeGZqLf/jbuSKQUZds0dHmUQYjk2tSp0KtX8aw42pAePeDEE8NQ0l/8ArQau+RStsNHnyPMKG6Ruj4DmBlhXCKbbeVKmDu3NJqF0l1wAaxdC7//fdyRSKnJdtTQGcBE4ObUqa7AgxHFJNIkM2aEoZallgh23hmOOip0Gq9cGXc0Ukqy7SM4m7BK6EoAd38H2DaqoESaorqjuFevWMOIxEUXwaefwk03xR2JlJJsE8G61Ab0AJhZc8LG8yIFZ9o06NcPttii4bLFpqIChg2D666Dzz+POxopFdkmgufM7OeETey/CdwH/DO6sEQ2j3txrziajYsvhiVL4JZb4o5ESkW2o4YuJOwvPBs4k7DZzK1RBSWyuT7+OHxJFnsiqJ4Al77tZrW334addgqjh5o1g5Yt8xublJ5sRw1tInQO/8DdR7n7LZplLIVowYJwWeyJoCEjR4YO4+eeizsSKQX1JgILLjezZcBbwFup3ckure9xInFZsABat4Y99og7kmjttBPssgtMnhxWJxVpioZqBD8mjBYa5O5bu/vWwD7AAWb248ijE2mkBQvCap3FvOJotkaOhFWr4Nln445Eil1DieAkYLS7L6g+4e7zgROBkxt6cjMbYWZvmdk8M7uwjjLHmdkbZjbHzP7emOBF0m3cCB98UFrNQuPH1xy17bgj9O8fagVrtU2UNEFDiaCFuy+rfdLdq4B6f3OZWRkwDjgU6A+MNrP+tcr0BS4CDnD3XYEfZR+6yJctXgxffFFaiaAhRxwBn32mfY2laRpKBOs38z6AwcA8d5+fmoNwN1/d1ewMYJy7fwLg7ksbeE6ROiWlozhd796w++7w+OOaVyCbr6FEMMDMVmY4VgG7N/DYrsDCtNuLUufS7QTsZGYvmdlUMxuR6YnMbKyZVZpZZVVVVQMvK0n17rvQoQP07Bl3JNGoq4lo5EhYswaefjr/MUlpqDcRuHuZu3fIcLR391x0xzUH+gJDgdHALWa2ZYY4xrt7hbtXlGvZRanDvHnQp0/prDiarZ49YcAAePLJkBBEGqsxexY31mKge9rtbqlz6RYBk9z9i1SH9NuExCDSKJ98AsuXhw7UJDr88JAEnnoq7kikGEWZCGYAfc2st5m1BI4Ham9m8yChNoCZdSI0Fc2PMCYpUe++Gy779Ik3jrj06BE2un/yyZAURRojskTg7huAc4DJwFzgXnefY2ZXmNkRqWKTgeVm9gbwDPA/7r48qpikdM2bB61aQffuDZctVSNHhmGk110XdyRSbLJda2izuPujhHWJ0s9dmnbdgfNTh8hmmzcvLDtdVhZ3JPHp2hX23huuvx5+9CPYZpu4I5JiEWXTkEherFoFixYlt1ko3eGHh3kF114bdyRSTJQIpOhNnRqWn1YigO23h+OOg3HjYMWKuoeciqRTIpCi9+KLYcjoDjvEHUlhuPDCUEu68ca4I5FioUQgRe/FF6Fbt7DqqMCee8KIEWGT+/UNzf8XQYlAitwXX4QdydQs9GUXXBA26Jkype4y9S1oJ8miRCBFbdas0Dma1IlkdRkyBAYPDmsQbdwYdzRS6JQIpKi9+GK4VI3gy8xCX8GyZTBzZtzRSKGLdB6BSNTuuAM6dYKttqo5p6aO4MgjoXPnsF+Be/LWYJLsqUYgRWvjxrCR+847xx1JYWrWDA45BBYuhCeeiDsaKWRKBFK0Zs4MC6316xd3JIVr8GDYcku46qq4I5FCpkQgRat6/X3VCOrWogUMGxZ2MJs+Pe5opFApEUjReuqpMJO2Q4e4IylsBx0UagXXXBN3JFKolAikKK1bF0YMqTbQsNat4Qc/gPvvh3feiTsaKURKBFKUpk4Ne/QqEWTnvPOgZUstRieZKRFIUXrqqTAqpq/2s8vKQw/BPvvAbbfBRx/FHY0UGiUCKUpPPQUVFdC2bdyRFI9hw8KQ2z/8Ie5IpNAoEUjRWbUqjID5xjfijqS4dO4ctrO84QZYuTLuaKSQKBFI0XnhBdiwAb7+9bgjKT6HHAKffgq33BJ3JFJIlAik6Dz1VNif+IAD4o6k+PTqBQcfDL/7XUimIqBEIEXoscfC2Pg2beKOpDhdcAEsXqwJZlJDiUCKyvz5MHdu2JtXNs/w4TBgQFiietOm7B6jfQtKW6SJwMxGmNlbZjbPzC6sp9wxZuZmVhFlPFL8HnkkXH7rW/HGUczM4Gc/gw8/hNmz445GCkFkicDMyoBxwKFAf2C0mfXPUK498ENgWlSxSOl4+OGwyJz2H2ia446DbbYJS1SLRFkjGAzMc/f57r4euBs4MkO5K4GrgbURxiIlYPVqePZZ1QZyoXnzMK/g3Xdh3ry4o5G4RZkIugIL024vSp37LzMbCHR390fqeyIzG2tmlWZWWVVVlftIpSg89ljYjH3kyLgjKW7V7f0HHADt2tU0t0lyxdZZbGbNgOuAnzRU1t3Hu3uFu1eUl5dHH5wUpIkTYdttw4ghabpWrULH8Rtv1L/JvZS+KBPBYqB72u1uqXPV2gO7Ac+a2XvAvsAkdRhLJmvWhF+uRx8NZWVxR1M6hg6F9u3hssvijkTiFGUimAH0NbPeZtYSOB6YVH2nu3/q7p3cvZe79wKmAke4e2WEMUmRmjwZPvsMRo2KO5LSUl0reOKJMJJIQ0STKbJE4O4bgHOAycBc4F53n2NmV5jZEVG9rpSme+8Nm9QPGRJ3JKVnyJBQK3j44bgjkbg0j/LJ3f1R4NFa5y6to+zQKGOR4rViBTz4IJx2WhjtIrnVqlVYg2jiRG1ck1SaWSwF7957Ye3asCWlmi6iMWRIeH8feADc445G8k2JQAreX/4S9ibu2TPuSEpXy5Zw5JFhXsE//hF3NJJvSgRS0ObODdtS7rdfWBpBorP//tC1a1iUbt26uKORfFIikIL2xz+GNux99407ktLXrFkYlTV/Powbl7lM9WQ0NdGVFiUCKVgffwx33AFjxoT2a4le//6h4/jKK2H58rijkXxRIpCCdeutYSLZD38YdyTJcu21YStLTTJLDiUCKUhr1sB114XtKPfYI+5okmW33eD73w97G1dqemciaFS2FKQ//QmWLAlj26VxctF+/6tfwf33w5lnwjQtEF/yVCOQgrNyJVx9NYwYAQceGHc0ydSxI1x/PcycWXfHsZQOJQIpOJdeCp98Ar/8ZdyRJNuxx4ZkfMkl4f9DSpeahqSgzJwZhoyedRbsvXfmMhq6mB9moTaw664wYULoN9BcjtKkGoEUjHXr4PTTobwcfv3ruKMRgB12CP0Fs2aFiX1SmpQIpGBcdBG88grcfDNsuWXc0Ui1H/4w7BF9zz1qIipVahqSvElv0hk79svnXnkFbroJzjknrHkjhaOsDE49NUwy++tf4bzz1ERUalQjkNgtWAB//nNYRuI3v4k7GsmkvByOOSZsa/nCC3FHI7mmRCCxWrYsdEh27AgPPQStW8cdkdTla1+DXXYJczuqquKORnJJiUBis2JFmDi2cWNoEtp227gjkvqYwcknh8s77oBNm+KOSHJFiUAi0dAKlevXh6aGpUvDUNEuXTI/RitdFpatt4bjjw87mV17bdzRSK4oEUjeuYelC55+Gk46Cfr1izsiaYx994WBA+Hii2HGjLijkVxQIpC8e+QRuP12uPzysOGMFBczOPHEsGvc6NGwalXcEUlTRTp81MxGAL8HyoBb3f2qWvefD5wObACqgO+5+/tRxiT5VbtZZ8YM+Oc/w6/K7baLJybJTn1NcltsAXfdFfY6Hj4cvvvdcL56WHBDz9lQOcmvyGoEZlYGjAMOBfoDo82sf61irwAV7r4HMBG4Jqp4JH5LlsCdd8KOO4YmIY1FL24HHgi/+EWYcfzvf8cdjTRFlE1Dg4F57j7f3dcDdwNfmirk7s+4+5rUzalAtwjjkRh98UXYaKZ587CMRHNNZSwJl1wS+njuugs++CDuaGRzRZkIugIL024vSp2ry2nAY5nuMLOxZlZpZpVVGsBclO6/P3xRnHJKGHkipaF5czjjDGjfPswMX7bsq2W0z3HhK4jOYjM7EagAMs4rdffx7l7h7hXl5eX5DU6abNasMELo61+HAQPijkZyrX37MAT4009D5/HGjXFHJI0VZSJYDHRPu90tde5LzGwYcDFwhLuvizAeicHnn8Pf/w7dusHRR8cdjUSlVy844QR48smwFpF73BFJY0TZUjsD6GtmvQkJ4HjghPQCZrYXcDMwwt2XRhiLxOTBB8MvxbPOghYt4o5GonTAAWEk2DXXhMtf/CLuiCRbkSUCd99gZucAkwnDR29z9zlmdgVQ6e6TCE1B7YD7LAwh+cDdj4gqJsmvBQvguedg6FDo3TvuaCQfrroKPvoo7DLXubOGiRaLSMduuPujwKO1zl2adn1YlK8v8dm4Ef72t7CYnJaVTg6zMDqsqirsaNahQ9wRSTYKorNYSs+TT8KiRWFdmjZt4o5G8qlFC7jvvtBUNGYMvPRS3BFJQzSaW3JuwYIwe3jAANhrr8Y/XsMMi0NDM4//9S846qiwmc369XDwwfmLTRpHNQLJKXf4wQ+gWbNQG5DkatsWJk2CPfeEu+8OPw40mqgwKRFITk2YEH4Jfvvbmjgm0KpV6DDebz94+GH4y1/CLHMpLGoakpxZtixsdL7PPmGkkBSnXDfNlZWFGeXbbht2ofv441Bb3Gab3L6ObD7VCCRnzj8/7Dp2yy2haUikmhkcdlhYZ2rBglBDeOeduKOSavq4Sk5MnhxWFr3wQth997ijkUI1aBD8+MfwySdhKfJnnok7IgElAsmB1avDzOF+/cKuVSL16dMnLF293XbwzW/CDTfEHZEoEUiTnXsuvP9+mEjUunXc0Ugx2HFHmDIFDj0Uzj47/JBYvz7uqJJLiUCaZMKEsO3kJZeEjUpEstWhQ1iL6qKL4OabQ+1Aq8zHQ4lANturr4bOvwMOCGvLiDRWWRn8+tdhhdrp00MfwmuvxR1V8mj4qDQo0z6zixfDEUeEuQITJ2rHMfmqxgxDHT0aZs+GG2+EwYPh1FNh4MBwnxaui55qBNJoH34YNpn55JMwLlyb0Esu9OoFP/85bL99aCp66CFtcpMvSgTSKHPnwkEHhRrBv/5V86tNJBc6doSf/AT23x8efRR++9swEEGiZV5ki39UVFR4ZWVl3GGUhExNPnWVcw8jgs47L6wm+uCDYRx4pucTyYXp0+Guu8IyFTfeCCtXholpoOaizWFmL7t7Rab71LIr9XKHt94K68S8/XboGJ4wAbp3b/ixIk0xeHDY0Oif/wzbYPbvH5am6Nw57shKj5qG5CtWroQXXoDLLgsfvuuuC7tOjRsHzz+vJCD5U14e9jP4wx9g/ny44opQG12+PO7ISotqBDmUbVNLvtQXz4YNsGRJOI49tub6ypXhSx9CNfzAA8OQvsGDw/LS9b2OSBTKysKkxbVrwwi1xx6Dnj3DJLTzzw+dy02V/je8OZ/fQvvsN5YSQQKsXAmVlTBrVs3xxhtfnsm5xRZh9M+IEWGpiF12CZ3CW2+tL3opDB07wmmnhdnI8+bB734Xjq9/PTQZHXWUlj7fXEoEJejTT+HFF8Ovp7ffDnvHbtoU7ttuu7Bz2PDhsHRpWBq4c2do1y7cX6y/aCQ5tt8eLr8c/vd/w6z2CRPCxMYzzgg/YPbbL9Ri+/YNS1l06xZqFVI3JYISsHx5+OJ//vlwzJwZvvibNw+dbZdcEjp599wzfPFX0y99KWY77ghXXhn6DV5+OQxnnjIF7r8f/vznmnLNm4cfQNtvD126fPWyS5dQa27XLrnLp0c6fNTMRgC/B8qAW939qlr3twL+CuwNLAe+4+7v1fechTR8dPXqMJ6++njggTDJqrwcPvsMPv88jIFu1iz8MTZvHppg2rWD9u3Dccwx4cu5+mjXrmaIXG3uYfOXuXNhzpzQxPPCC6GZB2q++HfaKTTv9O4NLVvm7t+bXltQEpFCU/33edNN4XNYVRVqvcuXh1py9bFiRfh81tasWVj/qE+fzMmiS5fw+WzduuZo1Soct9zy5RgKUSzDR82sDBgHfBNYBMwws0nu/kZasdOAT9y9j5kdD1wNfCeqmNK5h1mLGzeGjtONG2HdOli1KnzBr1oVjqVLazpSlywJs2qrv/hXrfrq87ZpAz16hD+Ytm3DH8mmTWF7vjVrwuNXrw6vBWEN/3StW4eEsNVWISGYhfiWLQtH+jZ/HTqEiTdjxoQ/7p49oUWLyN4ykaLQrFnY/WybbWDnnTOX+eKLUAs4+ODwmbz33ppE0aEDvPdeqF1kuwheixbhR9cvfxl+7LVtW/Ojr2PH8JzZXLZtG5qxqo9mzer+YZhLUTYNDQbmuft8ADO7GzgSSE8ERwKXp65PBP5kZuYRVFPuvz98YVZ/+Ve3mWerTZvQlr7ddrDrrqGNvWvXcGy/fbicPLlmj9Zqdf1yXr8+JIRhw2p+uSxdWnN9xYoQo3v4gxg8ONQ0OncO7aD9+4e2z+o/Ev1CF8leixYhUVRPikz/wk///K5fX/MD8MMPw4+5tWvDsW5dzfUpU0LZXr1CmTVrQq2jqip0bK9cGZLM2rWNj7VZs5rE8NOfhuawXIusacjMRgEj3P301O2TgH3c/Zy0Mq+nyixK3X43VWZZrecaC1T/9+wGvB5J0LnVCVjWYKn4Kc7cUpy5pThzp6e7l2e6oyg6i919PDAewMwq62rnKiSKM7cUZ24pztwqljjrEmUf+WIgfQ5qt9S5jGXMrDnQkdBpLCIieRJlIpgB9DWz3mbWEjgemFSrzCTglNT1UcDTUfQPiIhI3SJrGnL3DWZ2DjCZMHz0NnefY2ZXAJXuPgn4M3Cnmc0DPiYki4YUS7eo4swtxZlbijO3iiXOjIpuGWoREcmthM6jExGRakoEIiIJVzCJwMxuM7OlqbkF1ee2NrMnzOyd1OVWdTz2lFSZd8zslExlCiTOjWb2auqo3XGejziPNbM5ZrbJzOoc6mZmI8zsLTObZ2YXFnCc75nZ7NT7Gem6I3XE+Rsze9PMXjOzB8xsyzoeG/f7mW2ccb+fV6ZifNXMHjezjAtMF8DnPds48/Z5bzJ3L4gD+BowEHg97dw1wIWp6xcCV2d43NbA/NTlVqnrWxVanKn7Vsf8fu4C9AOeBSrqeFwZ8C6wA9ASmAX0L7Q4U+XeAzrF+H4OB5qnrl9dx99nIbyfDcZZIO9nh7Tr5wE3ZXhcIXzeG4wzdV/ePu9NPQqmRuDuzxNGDqU7Ergjdf0O4NsZHnoI8IS7f+zunwBPACMKMM68yhSnu89197caeOh/lwZx9/VA9dIgkWhCnHlVR5yPu/uG1M2phLkytRXC+5lNnHlVR5wr025uAWQayRL75z3LOItKwSSCOnR29w9T1z8CMu1W2hVYmHZ7UepcPmUTJ0BrM6s0s6lm9u38hNZohfB+ZsuBx83s5dQyJHH6HvBYhvOF9n7WFScUwPtpZr8ys4XAGODSDEUK4v3MIk4ojs87UPiJ4L881LUKPvM2EGdPD9PQTwCuN7Md8xdZSTrQ3QcChwJnm9nX4gjCzC4GNgB3xfH62coiztjfT3e/2N27E2I8p6HycckyzqL5vBd6IlhiZl0AUpdLM5TJZimLqGUTJ+6+OHU5n9D+vVe+AmyEQng/s5L2fi4FHiA0w+SVmZ0KHA6MSf0IqK0g3s8s4iyI9zPNXcAxGc4XxPuZpq44i+XzDhR+IkhfguIU4KEMZSYDw81sq9RoneGpc/nUYJyp+FqlrncCDuDLS3IXimyWBomdmW1hZu2rrxP+3/O6Kq2FjZd+Bhzh7mvqKBb7+5lNnAXyfvZNu3kk8GaGYrF/3rOJs4g+70HcvdXVBzAB+BD4gtDudxqwDfAU8A7wJLB1qmwFYcez6sd+D5iXOr5biHEC+wOzCaNGZgOnxRDnUanr64AlwORU2e2BR9MeexjwNmG0y8WFGCdhFM6s1DEnpjjnEdqrX00dNxXo+9lgnAXyfv6DkHxeA/4JdK39OUrdjvvz3mCc+f68N/XQEhMiIglX6E1DIiISMSUCEZGEUyIQEUk4JQIRkYRTIhARSTglAkkkM7s4tcJp9SqS+9RT9nYzG9XA891uZgtSzzXTzParo9wVZjasqfGL5FJkW1WKFKrUl/ThwEB3X5ea8NMyB0/9P+4+0cyGAzcDe9R63TJ3r2tdGpHYqEYgSdQFWObu6wDcfZm7/8fMLjWzGWb2upmNNzOr/UAz29vMnkstzDa5emmRWp4H+qTKv2dmV5vZTODY9NqFmQ0ys3+b2Swzm25m7c2szML+ATNStZUzo3sbRAIlAkmix4HuZva2md1gZkNS5//k7oPcfTegDaHW8F9m1gL4IzDK3fcGbgN+leH5RxJmk1Zb7u4D3f3utOdqCdwD/NDdBwDDgM8JM1c/dfdBwCDgDDPrnYN/s0id1DQkiePuq81sb+Ag4GDgHgs7h60ys58BbQkbn8whLCFQrR+wG/BEqrJQRlh+oNpvzOwSoIrwhV7tngxh9AM+dPcZqZhWAqSalfZI65PoCPQFFmz+v1ikfkoEkkjuvpGwIuSzZjYbOJPQpl/h7gvN7HKgda2HGTDH3TN2BJPqI8hw/rNGhGbAue6e74UTJcHUNCSJY2b9aq0guSdQvSPaMjNrB2QaJfQWUF49IsjMWpjZrpsZxltAFzMblHqu9mbWnLCS5vdTzVCY2U6p1UBFIqMagSRRO+CPFjZx30BYxXIssIKwquRHhOWjv8Td16eabP5gZh0Jn5/rCU1IjZJ6ru+k4mhD6B8YBtwK9AJmpjqrqyiArU+ltGn1URGRhFPTkIhIwikRiIgknBKBiEjCKRGIiCScEoGISMIpEYiIJJwSgYhIwv0/l8nvqqO8PRgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "sns.distplot(train['SalePrice'], color='b', bins=100, hist_kws={'alpha': 0.4})\n", + "plt.show()\n", + "sns.distplot(np.log(train['SalePrice']), color='b', bins=100, hist_kws={'alpha': 0.4})" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ],\n", + " [,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ],\n", + " [,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ],\n", + " [,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ],\n", + " [,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ],\n", + " [,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ],\n", + " [,\n", + " ,\n", + " ,\n", + " ,\n", + " , ]],\n", + " dtype=object)" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "df_num = train.select_dtypes(include = ['float64', 'int64'])\n", + "df_num.hist(figsize=(16, 20), bins=50, xlabelsize=8, ylabelsize=8)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "corrmat = train.corr()\n", + "f, ax = plt.subplots(figsize=(20, 9))\n", + "sns.heatmap(corrmat, vmax=0.8, square=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "df_not_num = train.select_dtypes(include = ['O'])\n", + "fig, axes = plt.subplots(round(len(df_not_num.columns) / 3), 3, figsize=(12, 30))\n", + "for i, ax in enumerate(fig.axes):\n", + " if i < len(df_not_num.columns):\n", + " ax.set_xticklabels(ax.xaxis.get_majorticklabels(), rotation=45)\n", + " sns.countplot(x=df_not_num.columns[i], alpha=0.7, data=df_not_num, ax=ax)\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[,\n", + " ],\n", + " [,\n", + " ]], dtype=object)" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "df_num = train.select_dtypes(include = ['float64', 'int64'])\n", + "df_num = df_num[df_num.columns.tolist()[1:5]]\n", + "df_num.hist(figsize=(14, 8), bins=50, xlabelsize=8, ylabelsize=8)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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G/Ii8Oabb6pfv353XA36+++/NXXqVENnDENDQzVx4kTVrFlTNptNu3bt0sCBA9WkSROn13Ln5s23335bw4YNc8lehNQChJ1RQaJ///767bffVKNGDfn7++vFF1807CrJ7Vq1aqWlS5cqMDDQ8eLTvHlzrVixwrCa7tgw6g7uarhw5MgRbd++XTabTb6+vipdurQhdeyvmzabTW+++aZjeaadka+frlS/fn1ZLBbHm32LxaJcuXKpZs2a6tWrl6HNLF577TXHXj6jw+H58+cd6/P37dsnq9WqZ599VlWqVJGPj4+h2eSVV17RypUr9csvv2jhwoV69913NXDgQEM24/bt21fp06dXbGysrly5ojJlyqhevXravXu3Dh06pK+++srpNaXkoG/fr2P/t7R/30Z69913VaFCBa1YsUKrV69WbGys2rVr59Tn+Mdi6c60adM0ceLEFLe9//77d9zmTH369NHy5ctTbALp27evli1bZljNdOnS6dVXX3Ws0TfS3YLh8ePHJRkTDK9fv57qi0uRIkV08eJFp9eTkmfUU1vyVa5cubu+gXSWL7/8UkuWLHHM4sfExKhz586GBH375s3o6GiFh4erdu3akqQdO3bIx8fH0KDvyr0I3333ndMf816ef/55jRo1ytDAcDfuWFrnyg2jffv21aeffnrX2WcjZ53d0XDh0qVLypMnj/z9/R23JSQkGDK5cutzbcaMGV0a7F966SV5e3urevXqql69uqEHZrpi6cPduHIvX758+dSoUSPHc3lsbKyWLl2qGTNm6PTp0zp48KAhdaWU+3YCAwMN3bdz5MgRrV69WomJiXrhhRf0/fffS0rufvbKK68YUlOSChUqpD/++EMWi0UJCQmaN2+eYW/Cb3Xy5ElNmzZNISEhkqQsWbI4/Wf7WAT9w4cPp/jYarVq//79htQ6cuSIDh8+rKtXr6YIw9euXdPNmzcNqemOS6zuCIZXrly56+fi4uKcXk9K7qzh6pp2NpstxVKdXLlyGfbkaF/P2LVrV4WEhCh//vySpKioKMM33rljL8Lttm7dqlmzZmn27NlOf2z7xklXhhe7QYMG6e2339bJkyfVrl07x9I6I7lyw6i9faaRV9buxh0NF1q2bKmIiAjlyJFDUvJzYt68eZU3b16NGTPGrV2InCk0NFR79+7Vrl27NGHCBB07dkzlypXT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7NmzLml1aVejRg19+eWXiouL09atW/XDDz+ofv36htSqWbNmip9pjRo1Unxs5O/xSy+9pJdeeilFeJk1a5Yh4cWuTJkyOn/+/B2b0ow0bdo0LV++PMWaeaOXeuzbt08hISEKCwtT0aJF1bZtW0O6U92qR48ejqs0Rp8Gbufp6XnXq6VGhHxJWrJkifbs2aNatWpJSj67oEKFCjp9+rR69uxpyDKwd955R4cOHVKxYsVUrVo1jR8/Xt7e3k6vIyW39/X19dX58+dVt25dx5vipKQkQ5csSlK2bNkcnfJ+//139e/fXx9//LEaN26snj17GvJv6uoTYyX3HHI5btw4ffPNN3r77bclJXfFsk9IOpN9EtDPz09BQUF6+eWXZbFYFBoa6pLTnV988UUlJiaqa9euun79umbOnKmSJUs6tcZjEfSPHz+eosPF008/raNHjxq65KNevXouO2HULkuWLIqPj9czzzyjCRMmKH/+/EpKSjKkVvv27bVgwQL5+PikmC202WyyWCyGLhfau3evVq1apcKFC7tsdsCVm+DsXVkqVqxo6AmmqXFlq0u7AQMGaMmSJSpbtqwWLVqkF154wbDvu169ei59U3orV4YX+8bi69evO/49b736ZOS/508//aR169Y5vfNDaqZMmaLQ0FDlzJlT/v7+WrBggQoWLGh4XSn5YB6jO1LZufMEYKvVqtDQUOXNm1dS8r6lDz74QD/++KNee+01Q4J+jx499Oyzz7rkdNpLly6pRIkSKlGihOLj4xUfHy8pebO10cvrrFarNm3apGXLlunMmTPq2rWrmjVrpl27dqlHjx6GLCt09YmxUnKr1l27dunEiRNq1aqVYmJidP36dcPrFipUKMXHRrwhb9myZYpzGG69AmSxWAybaB0zZkyK7HX16lV5eXlp/vz5kpx7ZfGxCPpPP/20RowYkWJdY+nSpRUfH2/YGv0BAwaoe/fujhNGM2fOrM8//9yQWnYTJkyQzWbT8OHDNWfOHEVERGjGjBmG1FqwYIGk5MtjruaO2YG9e/c6du9LyR1xxo8fr9GjRzteGJxt2bJlOn36tKpVq6aqVau6ZA2wO1pdpkuXToGBgapcubIsFotKlixp2FKTL774wm1B35XhpWvXrobXuJuyZcvq6tWrLjnFOWPGjJo1a5aKFSt2x4v4rZvajdC8eXP9+OOPevHFF1PUMaIdo/3fMywsTBcuXHD0Aw8JCTH85xwREeEI+ZKUJ08eRUREKFeuXIa9fpYvX17z589PsSS0Xbt2hiyVvD2o3cpisWjDhg1Or2nXqFEj1apVS926dUvR4rJJkyaGzD5L7jnobebMmfrrr7907NgxtWrVSgkJCXr//fcNXRblqr727jqH4fZVBEYujXws1ujHxcXphx9+SLGu8X//+58yZcqk2NhYQ7q1uOJY4tTExMRIkmHHPNvFxsYqffr0jifeo0ePasuWLSpSpIjha/TtoqOjU5xNYORyE3dsgjt16pR2796tXbt2ae/evcqQIYOqV6+uIUOGOL2WO23atEkjRoxQsWLFZLPZdPr0aY0aNcqQy563b8Z1pYSEBC1YsMAl4cXu1KlTyp8/v+NqYlxcnC5cuGBoZ5g///xTPXv2VNmyZV12FWHw4MEpZiqvX7+unj17GtK5yW7+/PmaOnWqYzmfZHwwbNmy5R2HLqZ2mzONHDlSERERjqVQa9euVcGCBTVw4EC9+eabhhxCN3ToUCUmJjquFqxcuVLp0qXT2LFjnV7Lna5du+byg/RceRqvXfPmzRUcHKwWLVoY3nDBLiYmRmPHjtW2bdtks9lUt25dDR061LCrNParM7fv2enSpYsh9VzpsZjRz5w5s7p27ZrqLJcRIV9KfuGpUKGCY8a7QIEC6tu3ryFB32azaebMmfr+++9ls9lks9nk4eGh1157Tb1793Z6PUnq3r27xo4dqxIlSujEiRNq166dmjVrpp9//ll79+7VgAEDDKkrSRs2bND48eMVFRUlT09PnT17VqVLl3acDGeESZMm6bPPPlOvXr0kJb9ZnDx5sqxWq6ZNm2ZITS8vL2XKlEkZMmRQhgwZtGPHDh05csSQWnbu6Nz0ySefaN68eY71qCdPnlSPHj0MCfpHjx79zwPmjHzhGTlypBITE9W+fXtJyeHFfsiKUfr27Zti1ixdunSOzfpGGTRokN544w2Xrl0vUKCARo4cqZEjR+ry5ct68803DV/29u233yosLMzwSZVbxcbG6tSpU45lp6dOnVJsbKyhNUeMGKG1a9c6ngMCAwPVuHFjWSwWw06a/vPPP1NsYvT19TXsVNOePXvKx8dHVatWVaVKlVyy5MyuRYsWypMnj2O5ZrVq1Qzfz+eOg94yZMggi8XiuFJ748YNQ+tJru9r/9ZbbylTpkwufd6TpN27d2vmzJk6e/asEhMTHcunnTnh8FgE/dt/EHZGzry44lhiuzlz5uiPP/7QkiVLUrwAjBw5UnPmzFHnzp2dXvPKlSsqUaKEpOS+tf7+/ho2bJji4+PVqlUrQ4P+p59+qkWLFqlLly4KDg7W9u3bDd/Z7o5NcC+99JJy586tgIAAtW7dWsOGDTP8CWT06NGpdm4yUtasWVP8DL28vAx7A160aFG3HDAnuTa82Fmt1hTBJWPGjIadUmuXOXNmvf7664bWuN27776rCRMmaPjw4dq/f7969Oihxo0bG1qzePHiLmnbd6vBgwerY8eO8vLyks1m09mzZzVq1ChDa1osFjVp0sTwzc1SchML+0FZJ0+edGzoPnXqlGFhtE2bNgoPD9e0adN06NAhlS5d2hH8fXx8UixbcrZ169bp7Nmz2rVrlzZt2qTRo0cre/bshvbud8dBb02bNtXw4cN15coV/fjjj1q6dKnhrW9jYmL0448/6syZMylyn1F7FM6dO+fyboBS8tWvwYMHq2LFioblg8ci6LviB3E7VxxLbLdixQp9++23KWaWvLy8NHHiRHXt2tWQoH+r7du3q3v37pKSv2+jW/mlT59euXPnVlJSkpKSklS7dm3DTtlz5ya4jh07avfu3QoJCdHBgwdVo0YN1ahRw/ATQF3Vucnetq9ixYp644031LRpU1ksFq1Zs0aVKlVyej0peWapSJEihjz2vbgyvNh5enpqw4YNatCggaTkFp9GbzCsXr26Jk+erPr166d4zjNiDemtrR+9vb31+eefO/Z6hIWFGbofI0uWLAoMDFStWrVSfJ9Gttf08/NTWFiYjh49KkkqVaqUYa8rtzdauJ0RV/natGmj5cuXa+DAgXr99ddTvKEx6jm+Xr16jivtVqtVBw4c0O+//64JEybo9OnTOnjwoCF1peRw+Mcff2jXrl36+++/9fTTT6tatWqG1ZNcexqvXbdu3bR161ZlzZpVx44dU58+fVS3bl1Da/bs2VPVqlWTr6+vS/ZF+fn56ddffzWki+N/yZ49u+HdfR6LoO+KH8TtXHEssV1iYmKql489PT1TvJN1pnLlymn8+PEqUKCATp486fijvXLliiH1bpUjRw5dv35d1atX14ABA+Tp6ZmiH7AzuXNTY6dOndSpUyddv35dy5Yt08yZM3Xu3DlDX3hc2bnp1raWefPmdRzS5enpmWLvhTPduuHN1VwZXuxGjRqlAQMGaMyYMZKkggULasKECYbWPHDggKTkZWB2RrXXvL397LPPPqvExETH7a5ol3oroyY5tm3bJl9f3zt62p88eVKSMd+nfdnptGnTlC9fPsdhjytXrtT58+edXk+S46q3/Xt1xRsaKXn2Nzw8XOHh4dq7d69u3rwpX19fw0+3f/HFF1WpUiW9+eabhp+QbVe4cGHNmTPHpQe9SVLdunXl7e3tyCSXLl0yZOO6XWxsrN5//33DHv92VapUUe/evZWUlKT06dO7pAOhJNWqVUvjx49Xo0aNDJtYeSw2406aNElWq9XQH0RqLl68mOJYYqPWcv7XBkOjNh/GxcVp3rx5ioqKUuvWrVW+fHlJybM8J0+eNKTlmt2NGzeUOXNmJSUlOfrNN2vWzCUnjbrSJ598ot27d+vGjRvy8fFRtWrVVL16dUPbwp45c0Z58+ZVQkKC5syZo6tXr+p///ufYcuT3OGff/7RrFmzdPjwYUnJvea7dOni+B02Unx8vMvCiyTHem57K7usWbOmWOPtKhcuXDB0CURMTIxL18qnJiIiQiEhIY6rm840ffp09enT567tPI1smZjaoT9GHQTk5+f3n5sXjdjY2KhRI2XLlk2NGzeWt7e3KlWqZNjSwdsdOnRIu3fv1s6dOx0HntWoUcPQ/SXuOI134cKFmjFjhjJlyuTocGT0xvWpU6eqatWqLpvkrV+/vj7//HOVK1fOpQcUduzY8Y7bnD2x8lgEfVf8IG5nn528nf1obWd65plnHHsAbu9pHx8fr/379zu9pt1ff/11R5unn3/+2fDuQmfOnNGJEydUp04dxcbGymq1GjIz8V8bNyVjN2+uWbNG1atXNzQg3W7btm3y8fFR5syZDa91a7vS1BixBGL9+vWaMGGCevTo4fi9/euvvxQUFKSBAwfeMUPrDKmdLHorI2edU3ujb3SXFrsrV65o7dq1Wr16tY4cOaJff/3V6TXsp46nT59e6dKl07Rp01x61SYmJkY//fSTQkJCFBUVpYYNG6Y4edkM2rVrp//973/y9/eXxWLR6tWr9cMPPxjSGvG5555Tu3bt7vp5I5pLfPXVV9qzZ48iIyNVsmRJValSRVWqVHFZK9zr169r9+7d2r17t+PNU2oHJTqLO07jbdSokRYuXOjSN+M+Pj6KjY1VhgwZHG1gjZxh79Chg7777juXbsR1lcci6LvDrWu7b968qX379qlChQqGvrlwhxYtWmj8+PEqW7asJGn16tWaO3euUw+Qut2PP/6oRYsW6fLly1q/fr2OHz+uESNGGNJK78yZM//5eaPXe1++fFknTpxIsZTFiDeLdh988IH27NmjnDlzqlq1aqpRo4aqVaumnDlzOr3Wva40tWjRwuk1X3nlFX3++ed3tJe0n/JpxCylfRY2Ojpa4eHh8vX1lc1m044dO+Tj46OvvvrK6TWPHDmiw4cPa+LEiRo4cKDj9mvXrumbb74xrENVXFycNmzYoFWrVungwYO6fv26PvvsM9WoUcOQF8BmzZpp2rRpKl26tPbu3auJEyfq+++/d3qdW127dk3r1q3T6tWrdezYMTVq1EihoaHasmWLoXWtVqsuX77sCEvx8fFavny55syZo59++smwuqdPn9bYsWMdPcmrVq2qIUOGGNKi1Z3tb6XkHvPh4eHas2ePdu/erdy5cxv6+9SyZUslJCSkuGLryj1E9tN4r169auhpvN26ddPMmTNdvnndlQYNGqRTp07Jz88vxZVao9trXrhwQVOmTFFUVJTjSnV4eLhTrwo9Fmv0r1y5ouDg4Dv6mxq5aer2TZoRERGGrse1Wq3y9/fXmjVrDKuRGvsl5UmTJmn37t0KDg7Wt99+a2jN+fPna/HixY5d+yVKlHCcH+Bstz7pXrhwQX/++ackqXLlyoYfVLN48WLNmzdP586dU/ny5bV3715VqVLF0DeL48ePlyRFRkZq7dq1Gj16tKKiohzrrp3JiCB/L1arNdWAUrRoUcP2s9iXVXTt2lUhISHKnz+/JCkqKsqwk1WPHTumTZs26erVqylmB7NmzepYr+9s/fv3165du1S3bl117NhRtWvXVsOGDVWrVi1D6knJG/Ptyw+8vb1dctpmnTp1VLlyZb377ruqVq2aLBaL1q1bZ2jNkJAQDR8+XFmyZFGJEiX01ltvaciQIapUqZLhGymLFi2qL774wtAadu6cNzx16pT27dunvXv3as+ePYqOjjb0vAlJmjVrlsuXnLnjNN7+/furXbt28vb2dtnGdSm5s6L93JKaNWsautKgaNGiKlq0qBISEgzvbHarQYMGqWXLlo7MWaJECb333ntPXtDv0aOHvL29Xd7f9FYFCxY0tAe6h4eHSpYsqbNnzxp6cNTtvLy8NGXKFPXq1UuFChXSt99+a/iyj4wZM6Z4sjAqoN0qNDRUEydOVM2aNWWz2TRmzBgNHDjQ0JZz8+bN05IlS/Tqq6/qu+++05EjRzR16lTD6knJHZx2796tf/75R7ly5VKHDh1UvXp1Q2qNHTtWQ4cOvWtnIyM6Gnl4eKT6N3LmzBnDL9NHREQ4Qr6UvAH57NmzhtSybxQNDw83fEOh3eHDh5UjRw6VLl1apUuXloeHh+FrVaOjozV79uy7fmzEbFq/fv0UGhqqUaNGyd/fXy+//LLTa9zuiy++0LJly1S8eHHt379fbdu21fTp01W/fn3Dan799dd64403NGbMmFT/HY0Iad9++60uXbp0188bsXmzV69e2rt3r7Jly+Zoq/n666+7ZP26p6enNm3apH///TfFVVujzr+R3HMa7/Dhw1W7dm2XZrBJkybpzz//dCy/nTdvnv744w9DOshJxv6b/ZeLFy/q5ZdfVlBQkCQ5ljE602MR9G/evGnYzNnd3PrkmJSUpIMHD+rZZ581tOaVK1fk7++vypUrp7hEZkRgun3t+uXLl2W1Wh3vIo1cu16jRg19+eWXiouL09atW/XDDz8Y+oInJf8MlyxZ4pjFj4mJUefOnQ0N+hkzZnScaBofH6/SpUvr2LFjhtWTpHHjxqlYsWJq166datWqZeiMlr2Lx62djex/M0bN7PXp00ddunTRm2++6diMb1+jb3SHBl9fX3Xr1i3F6cp16tQxpJY9pK1evTrVZTpGhLQVK1boyJEjCgkJUefOnZU7d25dv37d0I24r776aopZ/Ns/NkLnzp3VuXNnnTp1SiEhIerVq5eioqIUFBSkhg0bqmTJkk6vmSFDBseSigoVKqhEiRKGP+fZg+7te7CM1Lp1a8dmzYiICMepw1euXFGhQoW0ceNGp9d85plnNHToUMeb/+XLl2vSpEkqUqSIevfubWhnmOHDhysuLk47duxQmzZttHbtWsNaC9utWLHiruvxjZphT0xMdHkG27x5s1asWOEIvS1atDCkVfTo0aM1fPhwl05Y3eqpp57SxYsXHa+de/bscfqha4/FGv05c+boqaee0osvvphiJtjIP+Bb1xl6eHioSJEihvfH/f3331O9vWbNmk6v5c6160lJSVqyZIljc99zzz2nNm3aGDp7ePtx3UlJSWrevLmhb2h69eqljz/+WHPnztX27duVI0cOJSYm6uuvvzaspiT9+++/2rlzp3bv3q0TJ06oZMmSmjhxotPrrF+/XpGRkerQoYOk5Bf5mJgYWSwWDRgwQE2bNnV6TSm508W3337r6LpTunRpdevWzSVdd8LCwhyzZjVq1FDDhg0NqbNx40bVr1//P7txGe2vv/7S6tWrtWbNGhUsWNCQzZuPin/++UchISEKDQ01ZBnP7d1oZs+eneJjo9YBW61WTZo0yeUbjD/88EM1bNjQ0TFl8+bN2rBhgyEtKFu0aKHZs2crV65c2rlzp9577z0NGzZMBw8e1NGjRzV9+nSn17Szv67Y///69et644039MMPPxhWs2HDhi4/jXfKlCkqUqSI6tWr57IM1qxZM3333XeOGpcuXVLHjh2d/ppdtWpV/fHHHy7NX7fav3+/xowZo3///VdlypTRxYsX9emnnzr19eyxCPrz58/X1KlTHbMDkgxv7eRuMTExyp07t6Hh1137Atxh/Pjx+ueff1LMxpYrV85lfXp///13Xb16Vc8//7yhLRmvXbvmaPe2a9cuXbx4UVWqVHGs3Xemdu3aaerUqSpUqJCk5Bn+OXPmKDY2VoMHDzZkczVcz2azadeuXYZsIp85c+ZdP2exWNSrVy+n16xVq5YqV67sODnV29vb8E2G//V9SsYuG2jbtq0WLVpk2OOn5vaJlbvd5gzNmzd3nEQ7atQoeXp66p133rnjc0Zo06aNY7/ZjBkzlCtXLgUEBBi+58N+Gu8ff/yhLVu2GH4ab2pXn4zOYKtXr9bkyZNVq1Yt2Ww27dy5U/3793e8hjtLYGCggoODnfqYDyoxMVHHjh2TzWZTyZIllSFDBqc+/mOxdOfbb79VWFiYSza9uKMd4549ezR58mTlzJlTPXv21MCBA3Xx4kUlJSVp/Pjx8vPzc3pNyfX7Atzxsz1x4oQuXLigDz74QGFhYdq9e7ek5MMxXnnlFafXs7v9TZTRswJ27du3V7Vq1VStWjW99tprKliwoGG1EhISHCFfkqpVq6bcuXMrd+7cio2NNazu8uXL9d1336XoZ//6668bevaDlPx3OmbMGB09elQJCQmyWq3KkiWLIe3e3HGi873CqBFBP7WD8m7cuKGlS5fq0qVLhgT9DRs2aM+ePQoPD1dQUJD279+vIkWKqGrVqqpataoha/bdtf5XksqXL6+33npLTZo0SfHzNrItbP78+fX55587nmNXrVqVYn+LM1mtViUmJip9+vTatm1bis3qtzbvMMKLL76oK1euqHv37mrVqpWk5CubRnLHabxGLLm6l4CAANWsWdPRQGPAgAHKly+f0+vExMSk2Bd0O6O77sTGxmr27Nk6e/asPvroIx0/flzHjh1z6sbjxyLoFy9e3GVtnYxej5Wa0aNHq1+/frp69ao6deqkr7/+WlWqVNGRI0fUv39/w4K+5Np9ATNmzNCFCxdShEMpeZOjEX/AUvKa9X79+klKfmGzv7j9/fffGjdunGH/3u7aXG3kUqTb3X6K8vDhwx3/bVQXpeXLl2vu3LkaNGiQKlSoIJvNpv379zuWJhkZ9kePHq2pU6eqb9++Wrp0qYKDg3X8+HFDau3Zs0eFChWSv7+/vL29XdLN5F6h24iweuv+jmvXrmnevHlatmyZXn75ZcNOtc6WLZuee+45x1H3N27c0LJlyzR37lzNnz/f0M25586d05gxYxxvDqtXr66hQ4ca+oY8Pj5euXPn1o4dO1LcbmTQnzx5smbOnOn4nalevbomT55sSC1/f3+99tpryp07tzJnzuxoPnDixAnDesvv27dPhQoVcrwRvXHjhsqWLatSpUqpc+fOhtS0c8dpvAkJCVqwYEGKDjht27Z1+szzrTp16qS5c+eqQYMGd9zmTElJSS7p9nU3gwcPVoUKFRwnkRcoUEB9+/Z98oJ+lixZFBgYqFq1ahne2im1telGL6OxWq2OF53p06erSpUqkuSSrgF9+/Y1vIbdxx9/rH79+t3xM7527Zo+/vhjQ0L3hQsXVK5cuTtuL1eu3D33KTwsV76JcscMcOXKlfXjjz862qTaLVy4UJUrV3Z6PUlasGCBZs6cmWKTsa+vr6ZPn65+/foZPqtfvHhxWa1WeXh4qFWrVoZsDpOkrVu3auvWrQoJCdHq1av1wgsvKCAgQGXKlHF6LTt3hG4pee3t7NmztWrVKkcfdiPOfbCLjIxUeHi4wsPDHbOFFSpU0Lvvvut47jXK4MGDFRAQoE8//VSStHLlSg0ePPg/ZxQflpGn7t5Nrly5DG+9aPf222/L19dX58+fV926dVM00Rg2bJghNUeMGOH4N9u5c6cmTZrk2BcwfPhwQ/cFBAcHa/fu3Vq9erW+/vprl5zGO3LkSCUmJqp9+/aSkn9vR44cqbFjxzq91s2bNxUbG6uLFy/q8uXLjkmOa9euKTIy0un18uXL59YrbidPntS0adMcTRfsh6c602MR9O2t5lzBHctobm2ldHtrS6Pb2926pMToNzTuCN1Xr1696+fi4uIMqWlfLnT7m6hdu3YZduXCHsTCwsJ04cIFxyXzkJAQw84LGDJkiHr16qVVq1Y5OuDs379f8fHx+uyzzwypee3atbv20b927ZohNe2yZMmi+Ph4PfPMM5owYYLy58+vpKQkQ2p5eHjIz89Pfn5+io+P1+rVq9WxY0f17t1br732miE1JdeH7vHjx2vdunV69dVXtWrVKmXNmtWwWnYvvPCCnn32WXXu3Fn9+/c3dM/M7WJiYhxLPKTkA5eM2svijv0P7phwkJTqGzQjuifZWa1WxybR0NBQtW3bVo0bN1bjxo0d3ciMUr58eXl5ecnLy8txGu/OnTsNDfp//vlnisMIfX19DVv6unDhQs2dO1dRUVFq2bKl4/asWbMa8tzn7m2qGTNmVFxcnCN3nTx50unPSY9F0HflwTzuWEZz6NAhVa1aVTabTTdv3nT0xrXZbIqPj3d6Pck9b2jcEborVqyY6qzz4sWLHeHU2ezLhW5/U5MzZ05NnTrVkCdk+xu2Tz75RMuWLXPcXr9+/RRPls6UJ08eLVy4UNu2bXN0wHnhhRfk6+trSD3pzjfC9/s5Z5gwYYJsNpuGDx+uOXPmKCIiQjNmzDCsXnx8vDZt2qTVq1frzJkz6tixo2FdfiT3hO7Zs2crY8aM+uKLL1KEQJvNZthx9wsWLNCePXu0fv16zZkzR0WKFFGVKlVUpUoVVapUydDgnytXLq1YsUIBAQGSkjccGtW5xB37H+wTDrGxsTp58qQsFouKFStm+N+mqyUlJbltX8Dtp/F+//33hp/G6+HhoZMnT6pYsWKSkg8nM+rcEh8fHzVt2lRr165Vx44dtXz5cq1du1ZFixZ1/N0405w5c5z+mPfDfo5H79691b17d0VERKh///4KDw93+lW4R7rrjjs2b966S79p06YpjiZ/FHZnO0vLli0db2iGDx9+xxsaI77Pfv36qXbt2qmG7q1bt2ratGlOr3nhwgX17t1bGTJkSNF3PSEhQTNnzjRkhr1Vq1ZaunRpqp8zqvOEXdOmTRUUFCQvLy9JyU/IPXr0SPF7/Djz9vZ2vNjc7tSpU451jkaJi4vT2bNnVapUKUPrDBw4UP/++6/8/Pzk7++vsmXLGlpPSp4pzJgx4x0HZRkZuh8Fp0+f1s8//+w4xdq+nMcIZ86c0ZgxY7Rnzx5ZLBb5+Pjoww8/NHwfj30p1pIlS9S0aVN17drVkCt9CQkJmjp1qpYuXer4niIiItSiRQv169fP0DXdrvTFF19o8+bNyp07tyIiIrR8+XJZLBadOHFCH3zwgaGtaGNiYlx+Gu+2bds0ePBgx+vKmTNnNG7cONWuXdvptdzZLtWV5s6dq9DQUJ0/f1516tRRoUKFVKFCBVWuXNnp/76P9Iy+OzbGunMZjSu5Y1/AkCFD1Lt37xRLPW4N3UbImzevFi5cqO3bt+vff/+VZPysszuuXNgNHjxYHTt2lJeXl2w2m86ePatRo0YZWtOVQkND3VZ748aNGj9+vBISErRx40YdPHhQn376qSHPUytXrlSWLFl0/Phxfffdd47bjQzdhw4dcvpjPqqOHDniWKf/xx9/6OrVq/L29la7du0MrVukSBGXvq65einWxIkTdePGDW3YsMGxEfbatWsaP368JkyYoKFDhxpW25XcsS/AzpWn8do3Hfv6+iosLEwLFy7U+vXrVbduXcPOLXHnsihX6tSpkzp16qQzZ844zvBYtWqVAgIC5O/v79SlZ4900HfHxlh3LKNxB3e8oXFH6LarXbu2IbMPqXHHciE7Pz8/hYWFpWg96co1yEYz+hL1f5k5c6aWLFmijh07Sko+kdOovSVPUuh2tVq1ail//vzy8fFRjRo11KNHD8eptUZxx3p5dyzF2rRpk9auXZviNSRbtmwaOXKkmjZtapqgL7l+X4CdK0/jvXXT8d69exUUFGT4pmN3LotyhyJFiqhHjx7q0aOHDhw4oCFDhuizzz7TwYMHnVbjkQ767lhH7swf7qPMnW9oXBm63cEdVy6+/vprvfHGG5KS+4TfeirtlClTHC1GH3c+Pj6pvhF1xfKS9OnTG34CJYy3fv16l/87umO9vDv2P1gsllT/Pm9fDoa0Cw8Pd5zG27t3b3Xp0sXx3O9s7phdd0e7VHdKTEzUli1bFBISou3bt6tmzZpOvzrzSAd9d/aXN7sn5Q2NO7jjykVoaKjjyT4oKChF0P/ll19ME/TDw8PdVvvpp5/WqlWrZLVaHUtqfHx83DYepM1/dbkxanbdHa1L3XFVqHTp0goODr6jze2KFStcMtv9JLBfgc+SJYsiIyOVK1cunT9/3pBa7phdd+eyKFfaunWrVq9erS1btqhSpUry9/fXmDFjUp0UeFiPdNB3Z3954GG58srFrXvqb99f/wjvt3+sDBs2TF9++aUyZsyofv366fnnn1fPnj3dPSw8oNReSGNjY7VkyRLDZtcl16+Xd4cRI0aod+/eWrp0aYqrmXFxcYa13H3SuPI0XnfNrrtrWZQrffXVV2rWrJkGDRpk+PPAIx30n5SNscDDuvXv4fa/Df5WnCNLlix677339N5777l7KHgIqc2uL1261NDZdXesl3eHAgUKaPHixS5tufukcMdpvE/K7Lo7zJs3z2W1Hun2ms8884zjlLCbN286wr59Hfn+/fvdPELg0cDfinHcdQgQjHP77Prrr79u6Kzak9q6FM7zpLSdhPM90jP6rCMH7g9/K8bZs2ePChUqJH9/f3l7e7MU6jHnjtl1uijhYT0pbSfhfI900AcAd9u6dau2bt2qkJAQrV69Wi+88IICAgJUpkwZdw8NaeCObjTAw3rS2k7CeQj6APAfPDw85OfnJz8/P8XHx2v16tXq2LGjevfurddee83dw8MDYnYdj6Mnre0knOeRXqMPAI+C+Ph4bdq0SatXr9aZM2dUv359tW7dWgUKFHD30AA8Ifbs2ePYGGvvHnXs2DHduHHD8MMY8fgi6APAfxg4cKD+/fdf+fn5yd/fX2XLlnX3kAAAuC8EfQD4D+XLl1eWLFkkiY4pAIDHCkEfAAAAMKF09/4SAAAAAI8bgj4AAABgQgR9AHChX3/9Vd27d1etWrVUqVIlNW7cWBMnTtTly5fdPbT/VK5cOc2YMcPx8YwZM1SuXLk7vu7y5cuaPHmyGjdurEqVKqlmzZrq1q2btm7davgYly1bpnLlyun06dOG1wKAxwFBHwBc5Msvv1S3bt2UKVMmffTRR/rmm2/Url07LV++XK1bt1ZERIS7h/hQIiIi1Lp1ay1ZskRt27bVrFmzNG7cOD311FPq2rWrvv76a3cPEQCeKByYBQAusH37dk2bNk2dOnXSkCFDHLfXrFlTL730klq1aqWBAwfqu+++c8l44uPjlTFjRqc+5sCBA3XlyhUtWbJEXl5ejttfeukljRs3TpMnT5aPj4/jsB8AgLGY0QcAF5g1a5Zy5syp/v373/E5Ly8vvfHGG/r999+1d+9e+fv7q3fv3nd83b59+1SuXDmtW7fOcduhQ4f01ltvqUaNGqpcubLatWunXbt2pbjfoEGD5Ofnp/DwcLVr106VK1fWhAkTJEkhISF6/fXXVbt2bfn4+CgwMFDLly9/4O9v7969+v333/XGG2+kCPl2/fv3V86cOTVr1qwU46pfv/4dX9uxY0d17NjR8fHNmzc1btw4BQQEyMfHR3Xr1tVbb72lI0eOPPA4AeBJQtAHAIMlJiZq586dqlu3rjJlypTq19gD7/bt29W8eXNt3rz5jnX7K1asUK5cufTCCy9Ikvbv36927drp8uXLGjNmjGbMmKFcuXKpc+fO+uuvv1Lc9+rVq+rXr5/8/f319ddfq1mzZpKkU6dOqXHjxpo0aZI+++wz1atXTx9++KEWLFjwQN/jtm3bUnwft8uUKZPq1KmjHTt2KCkp6YEeOz4+XtevX9fbb7+tr776SiNHjtTNmzfVrl07nT9//oEeCwCeJCzdAQCDXbp0SXFxcSpSpMhdv6Zo0aKSkte5v/nmm5o6dap++ukntWvXTpKUkJCgkJAQNW3a1LHkZsKECSpUqJDmzp3ruO25555TQECAPv/8c33++eeOx79x44YmTpyol156KUXdt956y/HfSUlJqlmzps6fP68FCxaoffv29/092vcX2L+P1BQpUkQ3btzQpUuX5Onped+PnT17do0dO9bxsdVq1XPPPac6deooJCREnTt3vu/HAoAnCUEfAB4xhQoVUs2aNbVixQpH0P/ll1908eJFNW/eXJIUFxennTt36s0331S6dOmUmJjouH+dOnW0atWqFI+ZIUMG1atX745ax48f1/Tp07Vz505duHDBMdvu7PX7t7r1hOH7FRoaqtmzZ+vYsWO6evWq4/ajR486c2gAYCoEfQAwWK5cuZQpUyadOXPmrl9jbwlZqFAhSVLz5s01ePBgnTp1Sl5eXlqxYoWKFy8uHx8fScltLK1W6x0z97dKSkpSunTJKzRz584tDw+PFJ+/fv26unbtqsyZM6t///4qVqyYMmTIoAULFmjp0qUP9D0WLFjQ8X2UKlUq1a85c+aMMmbMqNy5cz/QY2/cuFHvvfeeWrRood69eyt37tyyWCzq0aOH4uPjH+ixAOBJQtAHAIOlT59eNWrU0NatW3Xz5s1U1+lv3LhRklS7dm1JUqNGjTR69GitXLlSr7/+un7++Wf16NHD8fXZs2dXunTp1KFDB8cs/+3sIV9KfRZ9z549OnPmjObPn5+iE87333//wN+jr6+vpk2bpo0bN6Ya9G/evKnffvtNNWvWdNyWMWPGVIP6pUuXlCtXLsfHISEhKl68uD755BPHbQkJCY/82QMA4G5sxgUAF+jWrZsuXbqkKVOm3PG5U6dOadasWapRo4a8vb0lSdmyZVODBg20cuVKrVmzRvHx8SkC/VNPPaXq1avr0KFDqlChgipVqnTH/+4lNjZWUvKyHrvLly9rw4YND/z9ValSRdWrV9fXX3+tU6dO3fH5yZMn69KlSynW/RcpUkTR0dGKiYlx3Hby5EkdO3YsxX3j4uLuuBqxYsUKWa3WBx4nADxJmNEHABeoU6eO3nnnHc2YMUNnzpxRYGCgcuTIoQMHDigoKEjZsmVztLy0a968uVavXq0ZM2aoatWqd7StHDRokF577TV169ZNrVu3Vr58+XTx4kUdOHBAVqtVAwYM+M8xVa1aVdmyZdOoUaPUp08f3bhxQ1988YVy586dYh38/Zo4caI6duyotm3bqnv37qpYsaKuXLmiFStWKCwsTO3bt0+xGbhJkyb69NNP9f7776tz5866ePGigoKCUszmS9Lzzz+v9evXa9y4capXr57+/PNPff/998qRI8cDjxEAniQEfQBwkd69e6ty5cqaO3euBg8erNjYWBUuXFjNmzfXm2++eUfArVu3rvLly6fIyEj16tXrjserUKGClixZopkzZ+qjjz7S1atX5enpqWefffa+OuZ4enpq5syZGj9+vPr06aP8+fPr9ddf1+XLlzVz5swH/v4KFy6spUuX6uuvv9aiRYs0ZcoUJSQkSJJGjRrl2FhsV7x4cU2fPl3Tpk1Tr169VKJECQ0aNEhfffVViq979dVXFRERoaVLl2rRokWqVKmSvvzyy1TPGgAA/B+LzWazuXsQAABzOnDggDp06CA/Pz9NnTo1xb4BAICxeMYFABjm2Wef1bRp07Ru3TqNHj3a3cMBgCcKM/oAAACACTGjDwAAAJgQQR8AAAAwIYI+AAAAYEIEfQAAAMCECPoAAACACRH0AQAAABMi6AMAAAAm9P8A7Kts1dO6ftgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.style.use('seaborn-white')\n", + "type_cluster = train.groupby(['Neighborhood','OverallQual']).size()\n", + "type_cluster.unstack().plot(kind='bar',stacked=True, colormap= 'PuBu', figsize=(13,11), grid=False)\n", + "plt.xlabel('OverallQual', fontsize=16)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAK2CAYAAAABnL2IAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAACNz0lEQVR4nOzdfZjcdX0v/PcuQ+SZ7C558AFpDdhzbkHpua1CQdLuurtCDATEY3XqXbm1tJFisYlVxAJHMdaepOVUrkPlPueoPZ3bu4UKqMHshl0loFbbU5GuDz1lLeGhJoHZTQJCgMnO/UeaLQ9J2A0zOzO7r9d1cYX55bef3yfZzezsez6/77etWq1WAwAAAAAANJX2RjcAAAAAAAA8nwAfAAAAAACakAAfAAAAAACakAAfAAAAAACakAAfAAAAAACaUKHRDTTCrl27MjIykgULFuSQQw5pdDsAAAAAAMxBu3fvzsMPP5yTTz45hx122PN+f04G+CMjIykWi41uAwAAAAAAUiqV8vrXv/55x+dkgL9gwYIke/5SFi9e3OBuAAAAAACYi7Zs2ZJisTiZWT/XnAzw9y6bs3jx4rziFa9ocDcAAAAAAMxl+1vq3Sa2AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4wH6Vy+WsWrUqY2NjjW4FAAAAAOYcAT6wX6VSKSMjIymVSo1uBQAAAADmHAE+sE/lcjmDg4OpVqsZGBgwhQ8AAAAAM0yAD+xTqVTKxMREkmRiYsIUPgAAAADMMAE+sE/Dw8OpVCpJkkqlkqGhoQZ3BAAAAABziwAf2Kfu7u4UCoUkSaFQSE9PT4M7AgAAAIC5RYAP7FOxWEx7+56niPb29hSLxQZ3BAAAAABziwAf2Keurq709fWlra0t/f396ezsbHRLAAAAADCnCPCB/TrnnHNy+OGHZ9myZY1uBQAAAADmHAE+sF+33XZbnnjiiaxfv77RrQAAAADAnCPAB/apXC5ncHAw1Wo1AwMDGRsba3RLAAAAADCnCPCBfSqVSpmYmEiSTExMpFQqNbgjAAAAAJhbBPjAPg0PD6dSqSRJKpVKhoaGGtwRAAAAAMwtAnxgn7q7u1MoFJIkhUIhPT09De4IAAAAAOYWAT6wT8ViMe3te54i2tvbUywWG9wRAAAAAMwtAnxgn7q6utLX15e2trb09/ens7Oz0S0BAAAAwJxSaHQDQPMqFovZvHmz6XsAAAAAaAABPrBfXV1dWbduXaPbAAAAAIA5yRI6AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhAT4AAAAAADQhOoW4P/kJz/JeeedN/nff/gP/yGf//zns3379lx00UXp6+vLRRddlB07diRJqtVqrrnmmvT29mb58uX5wQ9+MFnr5ptvTl9fX/r6+nLzzTdPHh8ZGcny5cvT29uba665JtVqNUn2ew0AAAAAAGgVdQvwX/WqV+XWW2/Nrbfemi996Us5/PDD09vbmxtuuCGnn356BgcHc/rpp+eGG25IkmzatCn33XdfBgcH84lPfCJXX311kj1h/HXXXZe/+qu/yo033pjrrrtuMpC/+uqr84lPfCKDg4O57777smnTpiTZ7zUAAAAAAKBVzMgSOt/+9rdz/PHH5+Uvf3mGhoayYsWKJMmKFSty++23J8nk8ba2tpx66qnZuXNntm3blrvuuitnnHFG5s+fn2OPPTZnnHFG7rzzzmzbti2PPfZYTj311LS1tWXFihUZGhp6Vq3nXgMAAAAAAFrFjAT469evz1vf+tYkSblczsKFC5MkCxYsSLlcTpJs3bo1ixcvnvyYxYsXZ+vWrc87vmjRon0e33v+ga4BAAAAAACtou4B/lNPPZXh4eG85S1ved7vtbW1pa2tra7Xn4lrAAAAAABArdU9wN+0aVNe85rX5LjjjkuSdHV1Zdu2bUmSbdu2pbOzM8meyfotW7ZMftyWLVuyaNGi5x3funXrPo/vPf9A1wAAAAAAgFZR9wB//fr1WbZs2eTj7u7u3HLLLUmSW265JT09Pc86Xq1Wc/fdd+foo4/OwoULc+aZZ+auu+7Kjh07smPHjtx1110588wzs3Dhwhx11FG5++67U61W91nrudcAAAAAAIBWUahn8ccffzzf+ta38vGPf3zy2MUXX5zLLrssN910U172spfl2muvTZIsXbo0d9xxR3p7e3P44YdnzZo1SZL58+fn/e9/fy688MIkySWXXJL58+cnSa666qpcfvnl2bVrV84666ycddZZB7wGAAAAAAC0irZqtVptdBMz7cEHH0xPT0+Ghobyile8otHtAAAAAAAwB71QVl33JXQAAAAAAIDpE+ADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuADAAAAAEATEuAD+1Uul7Nq1aqMjY01uhUAAAAAmHME+MB+lUqljIyMpFQqNboVAAAAAJhzBPjAPpXL5QwODqZarWZgYMAUPgAAAADMMAE+sE+lUikTExNJkomJCVP4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7LI89dRTSZKnnnoql112WXp7e/P2t789Dz744GSNz372s+nt7U1/f3/uvPPOyeObNm1Kf39/ent7c8MNN0we3981AAAAAACgVRxwAv8DH/hAJiYm8qY3vSnnnHPOQV3gz//8z7NkyZI89thjSZK1a9fmPe95T5YtW5Yrr7wyN910U971rnflxhtvzDHHHJONGzdm/fr1Wbt2ba699trce++9Wb9+fdavX5+tW7fmoosuysDAQJLk4x//eD73uc9l0aJFufDCC9Pd3Z0TTzxxv9cAAAAAAIBWccAJ/CRpb2/Pf/tv/+2gim/ZsiXf+MY3cuGFFyZJqtVq/uZv/ib9/f1JkvPPPz9DQ0NJkuHh4Zx//vlJkv7+/nz7299OtVrN0NBQli1blnnz5uX444/PCSeckHvuuSf33HNPTjjhhBx//PGZN29eli1blqGhoQNeAwAAAAAAWsULBvhJ8su//Mv57//9v+enP/1ptm/fPvnfC1mzZk0+9KEPpb19z2XGx8dzzDHHpFDYM/i/ePHibN26NUmydevWvPSlL02SFAqFHH300RkfH8/WrVuzePHiyZqLFi3K1q1b93v8QNcAAAAAAIBWccAldPa67bbbkiSlUmnyWFtb2wEn27/+9a+ns7MzJ598cr7zne+8yDYBAAAAAGBumVKAPzw8PO3Cf//3f5/h4eFs2rQpTz75ZB577LF88pOfzM6dO1OpVFIoFLJly5YsWrQoyZ4J+p/+9KdZvHhxKpVKHn300XR0dGTRokXZsmXLZN2tW7dOfsy+jnd0dOz3GgAAAAAA0CoOuITO97///Zx77rn5xV/8xbzjHe/I6OjolAuvWrUqmzZtyvDwcP74j/84p512WtatW5c3vvGNk5vQ3nzzzenu7k6SdHd35+abb06SDAwM5LTTTktbW1u6u7uzfv36PPXUU3nggQdy33335bWvfW1OOeWU3HfffXnggQfy1FNPZf369enu7k5bW9t+rwG1Ui6Xs2rVqoyNjTW6FQAAAABgljpggP+f/tN/yoc//OF85zvfyXve856sWbPmRV/wQx/6UD73uc+lt7c327dvz9vf/vYkyYUXXpjt27ent7c3n/vc57J69eokyUknnZSzzz4755xzTt73vvflyiuvzCGHHJJCoZArr7wy73vf+3LOOefk7LPPzkknnXTAa0CtlEqljIyMPGtZ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AAAAgCY0awP8TZs2pb+/P729vbnhhhsa3Q4AAAAAAEzLrAzwd+/enY9//OP5b//tv2X9+vX56le/mnvvvbfRbQEAAAAAwJTNygD/nnvuyQknnJDjjz8+8+bNy7JlyzI0NNTotgAAAAAAYMoKjW6gHrZu3ZrFixdPPl60aFHuueeeyce7d+9OkmzZsmXGewMAAAAAgOTfMuq9mfVzzcoA/4U8/PDDSZJisdjgTgAAAAAAmOsefvjhnHDCCc87PisD/EWLFj1run7r1q1ZtGjR5OOTTz45pVIpCxYsyCGHHNKIFgEAAAAAmON2796dhx9+OCeffPI+f39WBvinnHJK7rvvvjzwwANZtGhR1q9fn3Xr1k3+/mGHHZbXv/71DewQAAAAAACyz8n7vWZlgF8oFHLllVfmfe97X3bv3p23ve1tOemkkxrdFgAAAAAATFl7oxuol6VLl2ZgYCC33357Vq5cWZOaDz/8cD74wQ/mzW9+cy644IL85m/+Zv75n/95n+c++OCDeetb35ok+c53vpPf+q3fqmn9Wnr3u9+dO++881nHPv/5z+eqq66qSf0vfelL+fjHP16TWrVQ77/nv/u7v8uyZcty3nnnZdeuXTWrO1W/8Au/kD/8wz+cfPzf//t/z2c+85kDfsxnPvOZvOlNb8p5552Xc845J1/96lcnf++//Jf/km9961vP+5ipfl3PhF/4hV/I6tWrJx9XKpWcdtppk/0NDQ3lhhtuSJLcfvvtuffeew9Y72D+bB/5yEeyYcOGaXY+dddff32WLVuW5cuX57zzzsv3v//9ul1rKr70pS9l69atNam1v6/ZnTt35o1vfGOq1WqS5Hvf+15+4Rd+YXKJtEcffTRveMMbMjExkWTP89Ypp5ySRx99tCZ91cuWLVuycuXK9PX15c1vfnOuueaaPPXUUwf8uuvu7s4jjzyS8847L+edd17OOOOMyX+z5513Xp566qkZ/lNMzb//9/8+5513Xs4999ycf/75+fu///t9nvfM75nP9MQTT2TVqlVZvnx53vrWt+ad73xnfvaznyVJfu3Xfm2fter9b/FAmu3faa1N5/vLM593n+sXf/EXX/Ba7373u/MP//APk4+f+TXyox/9KHfcccd0Wj8oU/nznnfeefngBz9Y915qpVm+RqvVat74xjdmx44dSZJt27blF37hF/J3f/d3k+ecdtppGR8fr8n19n7NPfjgg3nta1+bFStW5Oyzz86FF16YL33pS8869/bbb8/y5ctz9tlnZ/ny5bn99tuTJP/pP/2nyddJr33tayeffzds2JCPfOQj6e7unny++/a3vz1Z793vfnf6+/tz7rnn5m1ve1t+9KMf1eTP9EKvfV6sF/tcevPNN+f3fu/3nnVsbGwsp5122kF/z9r7PWXvf/t6jpmJ16cz9X1mfHx8n9/3f/EXfzFXX331tGp1d3dnbGysPo3WyCOPPJJVq1alp6cnF1xwQd7xjndk48aN+Yd/+Idcc801SZ79c+WL/Tzs3LkzpVKpJr1Pxb6efz//+c/niSeeqPm1vvSlL+W0007Leeedl2XLluUDH/hAXa6zP/v7eXtfr/UO1s9+9rNceeWVk9d497vf3ZDvaQd6rfDMn7Hf+ta3ZmhoKMm+v3avu+66Z60akex5vXP22Wdn69at+cAHPpDk2c9x9cxZ9vcafn+v2Wvtz/7sz+p+jX2Z6s8uzzx32bJlOffcc/M//sf/mPy5dH9m6u9vKp77nHTdddfl/e9//+Tvf/azn01vb+/k4+Hh4fz2b/92kmd/T9nfa8vnft+ZyfxoJr9+p5Iv1cqsnMCvh2q1mt/5nd/JihUr8id/8idJkh//+Mcpl8v5+Z//+RmrX6lUUijU9tP21re+Nbfddlve9KY3TR677bbb8qEPfaim1zkYu3fvruk+BVP5e36xf8df/vKXc/HFF+e8886b0vm1/pzOmzcvg4ODufjii9PZ2Tnlj3vPe96T9773vbnvvvtywQUXpL+/P4ceemh+93d/t2a91csRRxyRf/qnf8quXbty2GGH5Zvf/Oaz9r3o6elJT09Pkj1PsL/yK7+SE088sVHtTtv3vve9fOMb38jNN9+cefPmZWxsLE8//XTdr1utVlOtVtPe/vz3em+++eacdNJJz/p7Plj7+5o95phjsmDBgoyOjubEE0/M9773vfwf/8f/kb//+7/POeeck7vvvjunnHLKZH/r16/PKaecksHBwbztbW970X3Vw97noHe+8525/vrrs3v37vzBH/xB/uRP/iS/8iu/csCPbW9vz6233ppkzw8ERxxxRN773vfOQNcH77DDDpvs+c4778wf//Ef5y/+4i+edU6lUtnvx//5n/95jjvuuMkfaH7yk5/k0EMPTZL8f//f/1enrg9Oo/6d7ks9XiskU//+UqlUnvW8W2s/+tGPMjIykqVLl9al/l4v9OcdHR3NxMRE/u7v/i6PP/54jjjiiLr282I109fo7t27c+qpp+buu+/O0qVLJ5/fv/e97+X1r399fvKTn2T+/Pnp6Oio+bVf+cpX5pZbbkmSPPDAA/md3/mdVKvVvO1tb8uPf/zjfPrTn87/+B//I8cff3weeOCB/N//9/+dV7ziFZODLQ8++GB++7d/e/K5LUm+8Y1v5Pd///fzlre8JX/zN3+TK6+8MoODg5O/v3bt2pxyyin567/+6/zRH/1RPve5z73oP8cLvfZptN7e3vzhH/5hnnjiiRx++OFJkoGBgfzqr/5q5s2bd1A1n/k9ZabU6/l0Kjo6Opri+/5M/B1Uq9VccsklWbFixeT3/IceeijDw8Pp7e3NKaecUtPrVSqV7Ny5M1/84hdTLBZrWntf9vf8++d//uc599xzJ/+N1NI555yTK6+8MkmyatWq3Hbbbc97fVyPz+2Bft6uVf1qtZqPfexjecUrXpHBwcG0t7fngQceyOjo6D7P3dfPMrXyQq8V9v6MPTo6mne9613PeoP3mZYtW5b3ve99WbVq1eSx9evXZ9myZVm0aFH+9E//tG5/hn2Zymv4evrsZz87GRbPpKn+7FIoFJ51brlczqpVq/LYY49NvtnSzPb1nPTEE0/ki1/84rPOOeqoo1Iul9PV1ZXvfe97+Q//4T+8YJ1GvbZ8ppn8+p3JfGnWTuDX2t/8zd+kUCjkne985+Sxf/fv/l3+z//z/8ynP/3pvPWtb83y5ctz2223HbDO2NhYLrrooixbtixXXHFFfvVXfzVjY2P7rf/6178+3/nOd/Kud70rv/3bv51ly5bV/M/W39+fb3zjG5PTMA8++GC2bduWXbt25R3veEfOP//8fOADH5icehwZGcmv//qv54ILLsh73/vebNu2LcmeCaP//J//cy688ML09/c/a4pqr2984xt5xzvekbGxsdx11137rN/d3Z3//J//c84///yaT7fs7+959+7dz/s7fv/7358LLrggy5Yty1/+5V8mSb72ta/lU5/6VJLkC1/4wmQ48cADD+TXfu3XcuONN2bDhg35L//lv2TVqlWpVqv7/Pqo5+e0UCjkHe94R77whS887/cefPDB/F//1/+V5cuX5/9v787joqr3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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "var = 'Neighborhood'\n", + "data = pd.concat([train['SalePrice'], train[var]], axis=1)\n", + "f, ax = plt.subplots(figsize=(26, 12))\n", + "fig = sns.boxplot(x=var, y=\"SalePrice\", data=data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 特征工程" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "test = pd.read_csv(\"test.csv\")\n", + "train = pd.read_csv(\"train.csv\")\n", + "ntrain = train.shape[0]\n", + "ntest = test.shape[0]\n", + " \n", + "data = pd.concat([train, test], axis=0, sort=False)\n", + "# 删除缺失值比例大于50%的特征列\n", + "missing_cols = [c for c in data if data[c].isna().mean()*100 > 50]\n", + "data = data.drop(missing_cols, axis=1)\n", + " \n", + "# 对object型的缺失特征进行填充\n", + "object_df = data.select_dtypes(include=['object'])\n", + "numerical_df = data.select_dtypes(exclude=['object'])\n", + " \n", + "object_df = object_df.fillna('unknow')" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "# 对数值型特征使用中位数进行填充\n", + "missing_cols = [c for c in numerical_df if numerical_df[c].isna().sum() > 0]\n", + "for c in missing_cols:\n", + " numerical_df[c] = numerical_df[c].fillna(numerical_df[c].median())" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "object_df = object_df.drop(['Heating','RoofMatl','Condition2','Street','Utilities'],axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "# 日期先后顺序不一致的数据修改\n", + "numerical_df.loc[numerical_df['YrSold'] < numerical_df['YearBuilt'], 'YrSold' ] = 2009\n", + "numerical_df['Age_House']= (numerical_df['YrSold'] - numerical_df['YearBuilt'])" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "# 业务相关特征汇总\n", + "numerical_df['TotalBsmtBath'] = numerical_df['BsmtFullBath'] +numerical_df['BsmtHalfBath']*0.5\n", + "numerical_df['TotalBath'] = numerical_df['FullBath'] + numerical_df['HalfBath']*0.5 \n", + "numerical_df['TotalSA'] = numerical_df['TotalBsmtSF'] + numerical_df['1stFlrSF'] +\\\n", + "numerical_df['2ndFlrSF']" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "# 特征编码\n", + "bin_map = {'TA':2,'Gd':3, 'Fa':1,'Ex':4,'Po':1,'None':0,\n", + "'Y':1,'N':0,'Reg':3,'IR1':2,'IR2':1, \\\n", + "'IR3':0,\"None\" : 0,\"No\" : 2, \"Mn\" : 2, \n", + "\"Av\": 3,\"Gd\" : 4,\"Unf\" : 1, \"LwQ\": 2, \\\n", + "\"Rec\" : 3,\"BLQ\" : 4, \"ALQ\" : 5, \"GLQ\" : 6}\n", + "object_df['ExterQual'] = object_df['ExterQual'].map(bin_map)\n", + "object_df['ExterCond'] = object_df['ExterCond'].map(bin_map)\n", + "object_df['BsmtCond'] = object_df['BsmtCond'].map(bin_map)\n", + "object_df['BsmtQual'] = object_df['BsmtQual'].map(bin_map)\n", + "object_df['HeatingQC'] = object_df['HeatingQC'].map(bin_map)\n", + "object_df['KitchenQual'] = object_df['KitchenQual'].map(bin_map)\n", + "object_df['FireplaceQu'] = object_df['FireplaceQu'].map(bin_map)\n", + "object_df['GarageQual'] = object_df['GarageQual'].map(bin_map)\n", + "object_df['GarageCond'] = object_df['GarageCond'].map(bin_map)\n", + "object_df['CentralAir'] = object_df['CentralAir'].map(bin_map)\n", + "object_df['LotShape'] = object_df['LotShape'].map(bin_map)\n", + "object_df['BsmtExposure'] = object_df['BsmtExposure'].map(bin_map)\n", + "object_df['BsmtFinType1'] = object_df['BsmtFinType1'].map(bin_map)\n", + "object_df['BsmtFinType2'] = object_df['BsmtFinType2'].map(bin_map)\n", + " \n", + "PavedDrive = {\"N\" : 0, \"P\" : 1, \"Y\" : 2}" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "object_df['PavedDrive'] = object_df['PavedDrive'].map(PavedDrive)\n", + "# 选择剩余的object特征\n", + "rest_object_columns = object_df.select_dtypes(include = ['object'])\n", + "# 进行one-hot编码\n", + "object_df = pd.get_dummies(object_df, columns = rest_object_columns.columns) \n", + " \n", + "data = pd.concat([object_df, numerical_df], axis=1, sort=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 特征选择" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "def correlation(data, threshold):\n", + " col_corr = set() \n", + " corr_matrix = data.corr()\n", + " for i in range(len(corr_matrix.columns)):\n", + " for j in range(i):\n", + " if abs(corr_matrix.iloc[i, j]) > threshold: # 相似性分数与阈值对比\n", + " colname = corr_matrix.columns[i] # 获取列名\n", + " col_corr.add(colname)\n", + " return col_corr" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "all_cols = [c for c in data.columns if c not in ['SalePrice']]\n", + "corr_features = correlation(data[all_cols], 0.9)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 模型融合" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "train = pd.read_csv('train.csv')\n", + "test = pd.read_csv('test.csv')\n", + "\n", + "all_data = pd.concat((train,test))\n", + "all_data = pd.get_dummies(all_data)\n", + "# 填充缺失值\n", + "all_data = all_data.fillna(all_data.mean())\n", + "# 数据切分\n", + "x_train = all_data[:train.shape[0]]\n", + "x_test = all_data[train.shape[0]:]\n", + "y_train = train.SalePrice" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "# 依然采用5折交叉验证 \n", + "kf = KFold(n_splits=5, shuffle=True, random_state=2020) \n", + "\n", + "class SklearnWrapper(object):\n", + " def __init__(self, clf, seed=0, params=None):\n", + " params['random_state'] = seed\n", + " self.clf = clf(**params)\n", + "\n", + " def train(self, x_train, y_train):\n", + " self.clf.fit(x_train, y_train)\n", + "\n", + " def predict(self, x):\n", + " return self.clf.predict(x)\n", + "\n", + "def get_oof(clf):\n", + " oof_train = np.zeros((x_train.shape[0],))\n", + " oof_test = np.zeros((x_test.shape[0],))\n", + " oof_test_skf = np.empty((5, x_test.shape[0]))\n", + "\n", + " scores = []\n", + " \n", + " for i, (train_index, valid_index) in enumerate(kf.split(x_train, y_train)):\n", + " trn_x, trn_y, val_x, val_y = x_train.iloc[train_index], y_train[train_index],\\\n", + " x_train.iloc[valid_index], y_train[valid_index]\n", + " clf.train(trn_x, trn_y)\n", + "\n", + " oof_train[valid_index] = clf.predict(val_x)\n", + " oof_test_skf[i, :] = clf.predict(x_test)\n", + "\n", + " scores.append(sqrt(mean_squared_error(val_y, oof_train[valid_index])))\n", + "\n", + " oof_test[:] = oof_test_skf.mean(axis=0)\n", + " print(np.mean(scores))\n", + " return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12273.777563550315\n", + "17232.460895702447\n", + "4.3574589107847246e-07\n", + "349.17978414062225\n" + ] + } + ], + "source": [ + "et_params = {\n", + " 'n_estimators': 100,\n", + " 'max_features': 0.5,\n", + " 'max_depth': 12,\n", + " 'min_samples_leaf': 2,\n", + "}\n", + "rf_params = {\n", + " 'n_estimators': 100,\n", + " 'max_features': 0.2,\n", + " 'max_depth': 12,\n", + " 'min_samples_leaf': 2,\n", + "}\n", + "rd_params={'alpha': 10}\n", + "ls_params={ 'alpha': 0.005}\n", + "et = SklearnWrapper(clf=ExtraTreesRegressor, seed=2020, params=et_params)\n", + "rf = SklearnWrapper(clf=RandomForestRegressor, seed=2020, params=rf_params)\n", + "rd = SklearnWrapper(clf=Ridge, seed=2020, params=rd_params)\n", + "ls = SklearnWrapper(clf=Lasso, seed=2020, params=ls_params)\n", + "\n", + "et_oof_train, et_oof_test = get_oof(et)\n", + "rf_oof_train, rf_oof_test = get_oof(rf)\n", + "rd_oof_train, rd_oof_test = get_oof(rd)\n", + "ls_oof_train, ls_oof_test = get_oof(ls)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1/5 2.6532774194782623e-07\n", + "2/5 5.282703996771267e-07\n", + "3/5 2.5293081112111894e-07\n", + "4/5 9.557751273232116e-05\n", + "5/5 4.003457174546628e-07\n", + "mean: 1.9404877480504378e-05\n" + ] + } + ], + "source": [ + "def stack_model(oof_1, oof_2, oof_3, oof_4, predictions_1, predictions_2, predictions_3, predictions_4, y):\n", + " train_stack = np.hstack([oof_1, oof_2, oof_3, oof_4])\n", + " test_stack = np.hstack([predictions_1, predictions_2, predictions_3, predictions_4])\n", + " \n", + " oof = np.zeros((train_stack.shape[0],))\n", + " predictions = np.zeros((test_stack.shape[0],))\n", + " scores = []\n", + "\n", + " for fold_, (trn_idx, val_idx) in enumerate(kf.split(train_stack, y)): \n", + " trn_data, trn_y = train_stack[trn_idx], y[trn_idx]\n", + " val_data, val_y = train_stack[val_idx], y[val_idx]\n", + " \n", + " clf = Ridge(random_state=2020)\n", + " clf.fit(trn_data, trn_y)\n", + "\n", + " oof[val_idx] = clf.predict(val_data)\n", + " predictions += clf.predict(test_stack) / 5\n", + " \n", + " score_single = sqrt(mean_squared_error(val_y, oof[val_idx]))\n", + " scores.append(score_single)\n", + " print(f'{fold_+1}/{5}', score_single)\n", + " print('mean: ',np.mean(scores))\n", + " \n", + " return oof, predictions\n", + "\n", + "oof_stack , predictions_stack = stack_model(et_oof_train, rf_oof_train, rd_oof_train, ls_oof_train, et_oof_test, rf_oof_test, rd_oof_test,ls_oof_test, y_train)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.10 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "vscode": { + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/file/ndarray-axis.ipynb b/file/ndarray-axis.ipynb new file mode 100644 index 000000000..82e95aecd --- /dev/null +++ b/file/ndarray-axis.ipynb @@ -0,0 +1,278 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[ 0 1 2 3 4]\n", + " [ 5 6 7 8 9]\n", + " [10 11 12 13 14]] \n", + " (3, 5) \n", + " 2\n" + ] + } + ], + "source": [ + "a1 = np.arange(15).reshape(3, 5)\n", + "print(a1,'\\n',a1.shape,'\\n',a1.ndim)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14] \n", + " (15,) \n", + " 1\n" + ] + } + ], + "source": [ + "a2 = np.arange(15)\n", + "print(a2,'\\n',a2.shape,'\\n',a2.ndim)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[ 0 1 2 3]\n", + " [ 4 5 6 7]]\n", + "\n", + " [[ 8 9 10 11]\n", + " [12 13 14 15]]\n", + "\n", + " [[16 17 18 19]\n", + " [20 21 22 23]]] \n", + " (3, 2, 4) \n", + " 3\n" + ] + } + ], + "source": [ + "a3 = np.arange(24).reshape(3,2,4)\n", + "print(a3,'\\n',a3.shape,'\\n',a3.ndim)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[[ 0 1 2 3]]\n", + "\n", + " [[ 4 5 6 7]]]\n", + "\n", + "\n", + " [[[ 8 9 10 11]]\n", + "\n", + " [[12 13 14 15]]]\n", + "\n", + "\n", + " [[[16 17 18 19]]\n", + "\n", + " [[20 21 22 23]]]] \n", + " (3, 2, 1, 4) \n", + " 4\n" + ] + } + ], + "source": [ + "a4 = np.arange(24).reshape(3,2,1,4)\n", + "print(a4,'\\n',a4.shape,'\\n',a4.ndim)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "origin\n", + "[[[ 0 1 2 3]\n", + " [ 4 5 6 7]]\n", + "\n", + " [[ 8 9 10 11]\n", + " [12 13 14 15]]\n", + "\n", + " [[16 17 18 19]\n", + " [20 21 22 23]]] (3, 2, 4)\n", + "axis=0\n", + "[[24 27 30 33]\n", + " [36 39 42 45]] (2, 4)\n", + "axis=1\n", + "[[ 4 6 8 10]\n", + " [20 22 24 26]\n", + " [36 38 40 42]] (3, 4)\n", + "axis=2\n", + "[[ 6 22]\n", + " [38 54]\n", + " [70 86]] (3, 2)\n", + "axis=(0,1)\n", + "[60 66 72 78] (4,)\n", + "axis=(1,2)\n", + "[ 28 92 156] (3,)\n", + "axis=(0,2)\n", + "[114 162] (2,)\n", + "axis=(0,1,2)\n", + "276 ()\n" + ] + } + ], + "source": [ + "print('origin')\n", + "print(a3,a3.shape)\n", + "print('axis=0')\n", + "print(a3.sum(axis=0),a3.sum(axis=0).shape)\n", + "print('axis=1')\n", + "print(a3.sum(axis=1),a3.sum(axis=1).shape)\n", + "print('axis=2')\n", + "print(a3.sum(axis=2),a3.sum(axis=2).shape)\n", + "print('axis=(0,1)')\n", + "print(a3.sum(axis=(0,1)),a3.sum(axis=(0,1)).shape)\n", + "print('axis=(1,2)')\n", + "print(a3.sum(axis=(1,2)),a3.sum(axis=(1,2)).shape)\n", + "print('axis=(0,2)')\n", + "print(a3.sum(axis=(0,2)),a3.sum(axis=(0,2)).shape)\n", + "print('axis=(0,1,2)')\n", + "print(a3.sum(axis=(0,1,2)),a3.sum(axis=(0,1,2)).shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "axis=(0,1)\n", + "[[[60 66 72 78]]] (1, 1, 4)\n" + ] + } + ], + "source": [ + "print('axis=(0,1)')\n", + "print(a3.sum(axis=(0,1),keepdims=True),a3.sum(axis=(0,1),keepdims=True).shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[ 0 1 2 3]\n", + " [ 4 5 6 7]]\n", + "\n", + " [[ 8 9 10 11]\n", + " [12 13 14 15]]\n", + "\n", + " [[16 17 18 19]\n", + " [20 21 22 23]]] (3, 2, 4)\n", + "[[[24 25 26 27]]\n", + "\n", + " [[28 29 30 31]]\n", + "\n", + " [[32 33 34 35]]] (3, 1, 4)\n" + ] + } + ], + "source": [ + "ta = np.arange(24).reshape(3,2,4)\n", + "tb = np.arange(24,36).reshape(3,1,4)\n", + "print(ta,ta.shape)\n", + "print(tb,tb.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[ 0 1 2 3]\n", + " [ 4 5 6 7]\n", + " [24 25 26 27]]\n", + "\n", + " [[ 8 9 10 11]\n", + " [12 13 14 15]\n", + " [28 29 30 31]]\n", + "\n", + " [[16 17 18 19]\n", + " [20 21 22 23]\n", + " [32 33 34 35]]] (3, 3, 4)\n" + ] + } + ], + "source": [ + "print(np.concatenate((ta,tb),axis=1),np.concatenate((ta,tb),axis=1).shape)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.10 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/avatar.png b/img/avatar.png new file mode 100644 index 000000000..ffd1c7793 Binary files /dev/null and b/img/avatar.png differ diff --git a/img/default.png b/img/default.png new file mode 100644 index 000000000..2bc2cd744 Binary files /dev/null and b/img/default.png differ diff --git a/img/fluid.png b/img/fluid.png new file mode 100644 index 000000000..368a58ace Binary files /dev/null and b/img/fluid.png differ diff --git a/img/loading.gif b/img/loading.gif new file mode 100644 index 000000000..c5126ed9c Binary files /dev/null and b/img/loading.gif differ diff --git a/img/police_beian.png b/img/police_beian.png new file mode 100644 index 000000000..60190da03 Binary files /dev/null and b/img/police_beian.png differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..802b4313c --- /dev/null +++ b/index.html @@ -0,0 +1,1028 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/boot.js b/js/boot.js new file mode 100644 index 000000000..26c3a1a31 --- /dev/null +++ b/js/boot.js @@ -0,0 +1,22 @@ +/* global Fluid */ + +Fluid.boot = {}; + +Fluid.boot.registerEvents = function() { + Fluid.events.billboard(); + Fluid.events.registerNavbarEvent(); + Fluid.events.registerParallaxEvent(); + Fluid.events.registerScrollDownArrowEvent(); + Fluid.events.registerScrollTopArrowEvent(); + Fluid.events.registerImageLoadedEvent(); +}; + +Fluid.boot.refresh = function() { + Fluid.plugins.fancyBox(); + Fluid.plugins.codeWidget(); + Fluid.events.refresh(); +}; + +document.addEventListener('DOMContentLoaded', function() { + Fluid.boot.registerEvents(); +}); diff --git a/js/color-schema.js b/js/color-schema.js new file mode 100644 index 000000000..5b1b75c2a --- /dev/null +++ b/js/color-schema.js @@ -0,0 +1,286 @@ +/* global Fluid */ + +/** + * Modified from https://blog.skk.moe/post/hello-darkmode-my-old-friend/ + */ +(function(window, document) { + var rootElement = document.documentElement; + var colorSchemaStorageKey = 'Fluid_Color_Scheme'; + var colorSchemaMediaQueryKey = '--color-mode'; + var userColorSchemaAttributeName = 'data-user-color-scheme'; + var defaultColorSchemaAttributeName = 'data-default-color-scheme'; + var colorToggleButtonSelector = '#color-toggle-btn'; + var colorToggleIconSelector = '#color-toggle-icon'; + var iframeSelector = 'iframe'; + + function setLS(k, v) { + try { + localStorage.setItem(k, v); + } catch (e) {} + } + + function removeLS(k) { + try { + localStorage.removeItem(k); + } catch (e) {} + } + + function getLS(k) { + try { + return localStorage.getItem(k); + } catch (e) { + return null; + } + } + + function getSchemaFromHTML() { + var res = rootElement.getAttribute(defaultColorSchemaAttributeName); + if (typeof res === 'string') { + return res.replace(/["'\s]/g, ''); + } + return null; + } + + function getSchemaFromCSSMediaQuery() { + var res = getComputedStyle(rootElement).getPropertyValue( + colorSchemaMediaQueryKey + ); + if (typeof res === 'string') { + return res.replace(/["'\s]/g, ''); + } + return null; + } + + function resetSchemaAttributeAndLS() { + rootElement.setAttribute(userColorSchemaAttributeName, getDefaultColorSchema()); + removeLS(colorSchemaStorageKey); + } + + var validColorSchemaKeys = { + dark : true, + light: true + }; + + function getDefaultColorSchema() { + // 取默认字段的值 + var schema = getSchemaFromHTML(); + // 如果明确指定了 schema 则返回 + if (validColorSchemaKeys[schema]) { + return schema; + } + // 默认优先按 prefers-color-scheme + schema = getSchemaFromCSSMediaQuery(); + if (validColorSchemaKeys[schema]) { + return schema; + } + // 否则按本地时间是否大于 18 点或凌晨 0 ~ 6 点 + var hours = new Date().getHours(); + if (hours >= 18 || (hours >= 0 && hours <= 6)) { + return 'dark'; + } + return 'light'; + } + + function applyCustomColorSchemaSettings(schema) { + // 接受从「开关」处传来的模式,或者从 localStorage 读取,否则按默认设置值 + var current = schema || getLS(colorSchemaStorageKey) || getDefaultColorSchema(); + + if (current === getDefaultColorSchema()) { + // 当用户切换的显示模式和默认模式相同时,则恢复为自动模式 + resetSchemaAttributeAndLS(); + } else if (validColorSchemaKeys[current]) { + rootElement.setAttribute( + userColorSchemaAttributeName, + current + ); + } else { + // 特殊情况重置 + resetSchemaAttributeAndLS(); + return; + } + + // 根据当前模式设置图标 + setButtonIcon(current); + + // 设置代码高亮 + setHighlightCSS(current); + + // 设置其他应用 + setApplications(current); + } + + var invertColorSchemaObj = { + dark : 'light', + light: 'dark' + }; + + function getIconClass(scheme) { + return 'icon-' + scheme; + } + + function toggleCustomColorSchema() { + var currentSetting = getLS(colorSchemaStorageKey); + + if (validColorSchemaKeys[currentSetting]) { + // 从 localStorage 中读取模式,并取相反的模式 + currentSetting = invertColorSchemaObj[currentSetting]; + } else if (currentSetting === null) { + // 当 localStorage 中没有相关值,或者 localStorage 抛了 Error + // 先按照按钮的状态进行切换 + var iconElement = document.querySelector(colorToggleIconSelector); + if (iconElement) { + currentSetting = iconElement.getAttribute('data'); + } + if (!iconElement || !validColorSchemaKeys[currentSetting]) { + // 当 localStorage 中没有相关值,或者 localStorage 抛了 Error,则读取默认值并切换到相反的模式 + currentSetting = invertColorSchemaObj[getSchemaFromCSSMediaQuery()]; + } + } else { + return; + } + // 将相反的模式写入 localStorage + setLS(colorSchemaStorageKey, currentSetting); + + return currentSetting; + } + + function setButtonIcon(schema) { + if (validColorSchemaKeys[schema]) { + // 切换图标 + var icon = getIconClass('dark'); + if (schema) { + icon = getIconClass(schema); + } + var iconElement = document.querySelector(colorToggleIconSelector); + if (iconElement) { + iconElement.setAttribute( + 'class', + 'iconfont ' + icon + ); + iconElement.setAttribute( + 'data', + invertColorSchemaObj[schema] + ); + } else { + // 如果图标不存在则说明图标还没加载出来,等到页面全部加载再尝试切换 + Fluid.utils.waitElementLoaded(colorToggleIconSelector, function() { + var iconElement = document.querySelector(colorToggleIconSelector); + if (iconElement) { + iconElement.setAttribute( + 'class', + 'iconfont ' + icon + ); + iconElement.setAttribute( + 'data', + invertColorSchemaObj[schema] + ); + } + }); + } + if (document.documentElement.getAttribute('data-user-color-scheme')) { + var color = getComputedStyle(document.documentElement).getPropertyValue('--navbar-bg-color').trim() + document.querySelector('meta[name="theme-color"]').setAttribute('content', color) + } + } + } + + function setHighlightCSS(schema) { + // 启用对应的代码高亮的样式 + var lightCss = document.getElementById('highlight-css'); + var darkCss = document.getElementById('highlight-css-dark'); + if (schema === 'dark') { + if (darkCss) { + darkCss.removeAttribute('disabled'); + } + if (lightCss) { + lightCss.setAttribute('disabled', ''); + } + } else { + if (lightCss) { + lightCss.removeAttribute('disabled'); + } + if (darkCss) { + darkCss.setAttribute('disabled', ''); + } + } + + setTimeout(function() { + // 设置代码块组件样式 + document.querySelectorAll('.markdown-body pre').forEach((pre) => { + var cls = Fluid.utils.getBackgroundLightness(pre) >= 0 ? 'code-widget-light' : 'code-widget-dark'; + var widget = pre.querySelector('.code-widget-light, .code-widget-dark'); + if (widget) { + widget.classList.remove('code-widget-light', 'code-widget-dark'); + widget.classList.add(cls); + } + }); + }, 200); + } + + function setApplications(schema) { + // 设置 remark42 评论主题 + if (window.REMARK42) { + window.REMARK42.changeTheme(schema); + } + + // 设置 cusdis 评论主题 + if (window.CUSDIS) { + window.CUSDIS.setTheme(schema); + } + + // 设置 utterances 评论主题 + var utterances = document.querySelector('.utterances-frame'); + if (utterances) { + var utterancesTheme = schema === 'dark' ? window.UtterancesThemeDark : window.UtterancesThemeLight; + const message = { + type : 'set-theme', + theme: utterancesTheme + }; + utterances.contentWindow.postMessage(message, 'https://utteranc.es'); + } + + // 设置 giscus 评论主题 + var giscus = document.querySelector('iframe.giscus-frame'); + if (giscus) { + var giscusTheme = schema === 'dark' ? window.GiscusThemeDark : window.GiscusThemeLight; + const message = { + setConfig: { + theme: giscusTheme, + } + }; + giscus.style.cssText += 'color-scheme: normal;'; + giscus.contentWindow.postMessage({ 'giscus': message }, 'https://giscus.app'); + } + } + + // 当页面加载时,将显示模式设置为 localStorage 中自定义的值(如果有的话) + applyCustomColorSchemaSettings(); + + Fluid.utils.waitElementLoaded(colorToggleIconSelector, function() { + applyCustomColorSchemaSettings(); + var button = document.querySelector(colorToggleButtonSelector); + if (button) { + // 当用户点击切换按钮时,获得新的显示模式、写入 localStorage、并在页面上生效 + button.addEventListener('click', function() { + applyCustomColorSchemaSettings(toggleCustomColorSchema()); + }); + var icon = document.querySelector(colorToggleIconSelector); + if (icon) { + // 光标悬停在按钮上时,切换图标 + button.addEventListener('mouseenter', function() { + var current = icon.getAttribute('data'); + icon.classList.replace(getIconClass(invertColorSchemaObj[current]), getIconClass(current)); + }); + button.addEventListener('mouseleave', function() { + var current = icon.getAttribute('data'); + icon.classList.replace(getIconClass(current), getIconClass(invertColorSchemaObj[current])); + }); + } + } + }); + + Fluid.utils.waitElementLoaded(iframeSelector, function() { + applyCustomColorSchemaSettings(); + }); + +})(window, document); diff --git a/js/events.js b/js/events.js new file mode 100644 index 000000000..bc6b77301 --- /dev/null +++ b/js/events.js @@ -0,0 +1,184 @@ +/* global Fluid */ + +HTMLElement.prototype.wrap = function(wrapper) { + this.parentNode.insertBefore(wrapper, this); + this.parentNode.removeChild(this); + wrapper.appendChild(this); +}; + +Fluid.events = { + + registerNavbarEvent: function() { + var navbar = jQuery('#navbar'); + if (navbar.length === 0) { + return; + } + var submenu = jQuery('#navbar .dropdown-menu'); + if (navbar.offset().top > 0) { + navbar.removeClass('navbar-dark'); + submenu.removeClass('navbar-dark'); + } + Fluid.utils.listenScroll(function() { + navbar[navbar.offset().top > 50 ? 'addClass' : 'removeClass']('top-nav-collapse'); + submenu[navbar.offset().top > 50 ? 'addClass' : 'removeClass']('dropdown-collapse'); + if (navbar.offset().top > 0) { + navbar.removeClass('navbar-dark'); + submenu.removeClass('navbar-dark'); + } else { + navbar.addClass('navbar-dark'); + submenu.removeClass('navbar-dark'); + } + }); + jQuery('#navbar-toggler-btn').on('click', function() { + jQuery('.animated-icon').toggleClass('open'); + jQuery('#navbar').toggleClass('navbar-col-show'); + }); + }, + + registerParallaxEvent: function() { + var ph = jQuery('#banner[parallax="true"]'); + if (ph.length === 0) { + return; + } + var board = jQuery('#board'); + if (board.length === 0) { + return; + } + var parallax = function() { + var pxv = jQuery(window).scrollTop() / 5; + var offset = parseInt(board.css('margin-top'), 10); + var max = 96 + offset; + if (pxv > max) { + pxv = max; + } + ph.css({ + transform: 'translate3d(0,' + pxv + 'px,0)' + }); + var sideCol = jQuery('.side-col'); + if (sideCol) { + sideCol.css({ + 'padding-top': pxv + 'px' + }); + } + }; + Fluid.utils.listenScroll(parallax); + }, + + registerScrollDownArrowEvent: function() { + var scrollbar = jQuery('.scroll-down-bar'); + if (scrollbar.length === 0) { + return; + } + scrollbar.on('click', function() { + Fluid.utils.scrollToElement('#board', -jQuery('#navbar').height()); + }); + }, + + registerScrollTopArrowEvent: function() { + var topArrow = jQuery('#scroll-top-button'); + if (topArrow.length === 0) { + return; + } + var board = jQuery('#board'); + if (board.length === 0) { + return; + } + var posDisplay = false; + var scrollDisplay = false; + // Position + var setTopArrowPos = function() { + var boardRight = board[0].getClientRects()[0].right; + var bodyWidth = document.body.offsetWidth; + var right = bodyWidth - boardRight; + posDisplay = right >= 50; + topArrow.css({ + 'bottom': posDisplay && scrollDisplay ? '20px' : '-60px', + 'right' : right - 64 + 'px' + }); + }; + setTopArrowPos(); + jQuery(window).resize(setTopArrowPos); + // Display + var headerHeight = board.offset().top; + Fluid.utils.listenScroll(function() { + var scrollHeight = document.body.scrollTop + document.documentElement.scrollTop; + scrollDisplay = scrollHeight >= headerHeight; + topArrow.css({ + 'bottom': posDisplay && scrollDisplay ? '20px' : '-60px' + }); + }); + // Click + topArrow.on('click', function() { + jQuery('body,html').animate({ + scrollTop: 0, + easing : 'swing' + }); + }); + }, + + registerImageLoadedEvent: function() { + if (!('NProgress' in window)) { return; } + + var bg = document.getElementById('banner'); + if (bg) { + var src = bg.style.backgroundImage; + var url = src.match(/\((.*?)\)/)[1].replace(/(['"])/g, ''); + var img = new Image(); + img.onload = function() { + window.NProgress && window.NProgress.inc(0.2); + }; + img.src = url; + if (img.complete) { img.onload(); } + } + + var notLazyImages = jQuery('main img:not([lazyload])'); + var total = notLazyImages.length; + for (const img of notLazyImages) { + const old = img.onload; + img.onload = function() { + old && old(); + window.NProgress && window.NProgress.inc(0.5 / total); + }; + if (img.complete) { img.onload(); } + } + }, + + registerRefreshCallback: function(callback) { + if (!Array.isArray(Fluid.events._refreshCallbacks)) { + Fluid.events._refreshCallbacks = []; + } + Fluid.events._refreshCallbacks.push(callback); + }, + + refresh: function() { + if (Array.isArray(Fluid.events._refreshCallbacks)) { + for (var callback of Fluid.events._refreshCallbacks) { + if (callback instanceof Function) { + callback(); + } + } + } + }, + + billboard: function() { + if (!('console' in window)) { + return; + } + // eslint-disable-next-line no-console + console.log(` +------------------------------------------------- +| | +| ________ __ _ __ | +| |_ __ |[ | (_) | ] | +| | |_ \\_| | | __ _ __ .--.| | | +| | _| | |[ | | | [ |/ /'\`\\' | | +| _| |_ | | | \\_/ |, | || \\__/ | | +| |_____| [___]'.__.'_/[___]'.__.;__] | +| | +| Powered by Hexo x Fluid | +| https://github.com/fluid-dev/hexo-theme-fluid | +| | +------------------------------------------------- + `); + } +}; diff --git a/js/img-lazyload.js b/js/img-lazyload.js new file mode 100644 index 000000000..c0c8e4ef7 --- /dev/null +++ b/js/img-lazyload.js @@ -0,0 +1,10 @@ +/* global Fluid, CONFIG */ + +(function(window, document) { + for (const each of document.querySelectorAll('img[lazyload]')) { + Fluid.utils.waitElementVisible(each, function() { + each.removeAttribute('srcset'); + each.removeAttribute('lazyload'); + }, CONFIG.lazyload.offset_factor); + } +})(window, document); diff --git a/js/leancloud.js b/js/leancloud.js new file mode 100644 index 000000000..ab901cec4 --- /dev/null +++ b/js/leancloud.js @@ -0,0 +1,192 @@ +/* global CONFIG */ +// eslint-disable-next-line no-console + +(function(window, document) { + // 查询存储的记录 + function getRecord(Counter, target) { + return new Promise(function(resolve, reject) { + Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify({ target }))) + .then(resp => resp.json()) + .then(({ results, code, error }) => { + if (code === 401) { + throw error; + } + if (results && results.length > 0) { + var record = results[0]; + resolve(record); + } else { + Counter('post', '/classes/Counter', { target, time: 0 }) + .then(resp => resp.json()) + .then((record, error) => { + if (error) { + throw error; + } + resolve(record); + }).catch(error => { + console.error('Failed to create: ', error); + reject(error); + }); + } + }).catch((error) => { + console.error('LeanCloud Counter Error: ', error); + reject(error); + }); + }); + } + + // 发起自增请求 + function increment(Counter, incrArr) { + return new Promise(function(resolve, reject) { + Counter('post', '/batch', { + 'requests': incrArr + }).then((res) => { + res = res.json(); + if (res.error) { + throw res.error; + } + resolve(res); + }).catch((error) => { + console.error('Failed to save visitor count: ', error); + reject(error); + }); + }); + } + + // 构建自增请求体 + function buildIncrement(objectId) { + return { + 'method': 'PUT', + 'path' : `/1.1/classes/Counter/${objectId}`, + 'body' : { + 'time': { + '__op' : 'Increment', + 'amount': 1 + } + } + }; + } + + // 校验是否为有效的 Host + function validHost() { + if (CONFIG.web_analytics.leancloud.ignore_local) { + var hostname = window.location.hostname; + if (hostname === 'localhost' || hostname === '127.0.0.1') { + return false; + } + } + return true; + } + + // 校验是否为有效的 UV + function validUV() { + var key = 'LeanCloud_UV_Flag'; + var flag = localStorage.getItem(key); + if (flag) { + // 距离标记小于 24 小时则不计为 UV + if (new Date().getTime() - parseInt(flag, 10) <= 86400000) { + return false; + } + } + localStorage.setItem(key, new Date().getTime().toString()); + return true; + } + + function addCount(Counter) { + var enableIncr = CONFIG.web_analytics.enable && !Fluid.ctx.dnt && validHost(); + var getterArr = []; + var incrArr = []; + + // 请求 PV 并自增 + var pvCtn = document.querySelector('#leancloud-site-pv-container'); + if (pvCtn) { + var pvGetter = getRecord(Counter, 'site-pv').then((record) => { + enableIncr && incrArr.push(buildIncrement(record.objectId)); + var ele = document.querySelector('#leancloud-site-pv'); + if (ele) { + ele.innerText = (record.time || 0) + (enableIncr ? 1 : 0); + pvCtn.style.display = 'inline'; + } + }); + getterArr.push(pvGetter); + } + + // 请求 UV 并自增 + var uvCtn = document.querySelector('#leancloud-site-uv-container'); + if (uvCtn) { + var uvGetter = getRecord(Counter, 'site-uv').then((record) => { + var incrUV = validUV() && enableIncr; + incrUV && incrArr.push(buildIncrement(record.objectId)); + var ele = document.querySelector('#leancloud-site-uv'); + if (ele) { + ele.innerText = (record.time || 0) + (incrUV ? 1 : 0); + uvCtn.style.display = 'inline'; + } + }); + getterArr.push(uvGetter); + } + + // 如果有页面浏览数节点,则请求浏览数并自增 + var viewCtn = document.querySelector('#leancloud-page-views-container'); + if (viewCtn) { + var path = eval(CONFIG.web_analytics.leancloud.path || 'window.location.pathname'); + var target = decodeURI(path.replace(/\/*(index.html)?$/, '/')); + var viewGetter = getRecord(Counter, target).then((record) => { + enableIncr && incrArr.push(buildIncrement(record.objectId)); + var ele = document.querySelector('#leancloud-page-views'); + if (ele) { + ele.innerText = (record.time || 0) + (enableIncr ? 1 : 0); + viewCtn.style.display = 'inline'; + } + }); + getterArr.push(viewGetter); + } + + // 如果启动计数自增,批量发起自增请求 + if (enableIncr) { + Promise.all(getterArr).then(() => { + incrArr.length > 0 && increment(Counter, incrArr); + }); + } + } + + var appId = CONFIG.web_analytics.leancloud.app_id; + var appKey = CONFIG.web_analytics.leancloud.app_key; + var serverUrl = CONFIG.web_analytics.leancloud.server_url; + + if (!appId) { + throw new Error('LeanCloud appId is empty'); + } + if (!appKey) { + throw new Error('LeanCloud appKey is empty'); + } + + function fetchData(api_server) { + var Counter = (method, url, data) => { + return fetch(`${api_server}/1.1${url}`, { + method, + headers: { + 'X-LC-Id' : appId, + 'X-LC-Key' : appKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + }; + + addCount(Counter); + } + + var apiServer = serverUrl || `https://${appId.slice(0, 8).toLowerCase()}.api.lncldglobal.com`; + + if (apiServer) { + fetchData(apiServer); + } else { + fetch('https://app-router.leancloud.cn/2/route?appId=' + appId) + .then(resp => resp.json()) + .then((data) => { + if (data.api_server) { + fetchData('https://' + data.api_server); + } + }); + } +})(window, document); diff --git a/js/local-search.js b/js/local-search.js new file mode 100644 index 000000000..0784a80a0 --- /dev/null +++ b/js/local-search.js @@ -0,0 +1,159 @@ +/* global CONFIG */ + +(function() { + // Modified from [hexo-generator-search](https://github.com/wzpan/hexo-generator-search) + function localSearchFunc(path, searchSelector, resultSelector) { + 'use strict'; + // 0x00. environment initialization + var $input = jQuery(searchSelector); + var $result = jQuery(resultSelector); + + if ($input.length === 0) { + // eslint-disable-next-line no-console + throw Error('No element selected by the searchSelector'); + } + if ($result.length === 0) { + // eslint-disable-next-line no-console + throw Error('No element selected by the resultSelector'); + } + + if ($result.attr('class').indexOf('list-group-item') === -1) { + $result.html('
Loading...

Loading...
'); + } + + jQuery.ajax({ + // 0x01. load xml file + url : path, + dataType: 'xml', + success : function(xmlResponse) { + // 0x02. parse xml file + var dataList = jQuery('entry', xmlResponse).map(function() { + return { + title : jQuery('title', this).text(), + content: jQuery('content', this).text(), + url : jQuery('url', this).text() + }; + }).get(); + + if ($result.html().indexOf('list-group-item') === -1) { + $result.html(''); + } + + $input.on('input', function() { + // 0x03. parse query to keywords list + var content = $input.val(); + var resultHTML = ''; + var keywords = content.trim().toLowerCase().split(/[\s-]+/); + $result.html(''); + if (content.trim().length <= 0) { + return $input.removeClass('invalid').removeClass('valid'); + } + // 0x04. perform local searching + dataList.forEach(function(data) { + var isMatch = true; + if (!data.title || data.title.trim() === '') { + data.title = 'Untitled'; + } + var orig_data_title = data.title.trim(); + var data_title = orig_data_title.toLowerCase(); + var orig_data_content = data.content.trim().replace(/<[^>]+>/g, ''); + var data_content = orig_data_content.toLowerCase(); + var data_url = data.url; + var index_title = -1; + var index_content = -1; + var first_occur = -1; + // Skip matching when content is included in search and content is empty + if (CONFIG.include_content_in_search && data_content === '') { + isMatch = false; + } else { + keywords.forEach(function (keyword, i) { + index_title = data_title.indexOf(keyword); + index_content = data_content.indexOf(keyword); + + if (index_title < 0 && index_content < 0) { + isMatch = false; + } else { + if (index_content < 0) { + index_content = 0; + } + if (i === 0) { + first_occur = index_content; + } + } + }); + } + // 0x05. show search results + if (isMatch) { + resultHTML += '' + orig_data_title + ''; + var content = orig_data_content; + if (first_occur >= 0) { + // cut out 100 characters + var start = first_occur - 20; + var end = first_occur + 80; + + if (start < 0) { + start = 0; + } + + if (start === 0) { + end = 100; + } + + if (end > content.length) { + end = content.length; + } + + var match_content = content.substring(start, end); + + // highlight all keywords + keywords.forEach(function(keyword) { + var regS = new RegExp(keyword, 'gi'); + match_content = match_content.replace(regS, '' + keyword + ''); + }); + + resultHTML += '

' + match_content + '...

'; + } + } + }); + if (resultHTML.indexOf('list-group-item') === -1) { + return $input.addClass('invalid').removeClass('valid'); + } + $input.addClass('valid').removeClass('invalid'); + $result.html(resultHTML); + }); + } + }); + } + + function localSearchReset(searchSelector, resultSelector) { + 'use strict'; + var $input = jQuery(searchSelector); + var $result = jQuery(resultSelector); + + if ($input.length === 0) { + // eslint-disable-next-line no-console + throw Error('No element selected by the searchSelector'); + } + if ($result.length === 0) { + // eslint-disable-next-line no-console + throw Error('No element selected by the resultSelector'); + } + + $input.val('').removeClass('invalid').removeClass('valid'); + $result.html(''); + } + + var modal = jQuery('#modalSearch'); + var searchSelector = '#local-search-input'; + var resultSelector = '#local-search-result'; + modal.on('show.bs.modal', function() { + var path = CONFIG.search_path || '/local-search.xml'; + localSearchFunc(path, searchSelector, resultSelector); + }); + modal.on('shown.bs.modal', function() { + jQuery('#local-search-input').focus(); + }); + modal.on('hidden.bs.modal', function() { + localSearchReset(searchSelector, resultSelector); + }); +})(); diff --git a/js/plugins.js b/js/plugins.js new file mode 100644 index 000000000..2a364b04c --- /dev/null +++ b/js/plugins.js @@ -0,0 +1,164 @@ +/* global Fluid, CONFIG */ + +HTMLElement.prototype.wrap = function(wrapper) { + this.parentNode.insertBefore(wrapper, this); + this.parentNode.removeChild(this); + wrapper.appendChild(this); +}; + +Fluid.plugins = { + + typing: function(text) { + if (!('Typed' in window)) { return; } + + var typed = new window.Typed('#subtitle', { + strings: [ + ' ', + text + ], + cursorChar: CONFIG.typing.cursorChar, + typeSpeed : CONFIG.typing.typeSpeed, + loop : CONFIG.typing.loop + }); + typed.stop(); + var subtitle = document.getElementById('subtitle'); + if (subtitle) { + subtitle.innerText = ''; + } + jQuery(document).ready(function() { + typed.start(); + }); + }, + + fancyBox: function(selector) { + if (!CONFIG.image_zoom.enable || !('fancybox' in jQuery)) { return; } + + jQuery(selector || '.markdown-body :not(a) > img, .markdown-body > img').each(function() { + var $image = jQuery(this); + var imageUrl = $image.attr('data-src') || $image.attr('src') || ''; + if (CONFIG.image_zoom.img_url_replace) { + var rep = CONFIG.image_zoom.img_url_replace; + var r1 = rep[0] || ''; + var r2 = rep[1] || ''; + if (r1) { + if (/^re:/.test(r1)) { + r1 = r1.replace(/^re:/, ''); + var reg = new RegExp(r1, 'gi'); + imageUrl = imageUrl.replace(reg, r2); + } else { + imageUrl = imageUrl.replace(r1, r2); + } + } + } + var $imageWrap = $image.wrap(` + ` + ).parent('a'); + if ($imageWrap.length !== 0) { + if ($image.is('.group-image-container img')) { + $imageWrap.attr('data-fancybox', 'group').attr('rel', 'group'); + } else { + $imageWrap.attr('data-fancybox', 'default').attr('rel', 'default'); + } + + var imageTitle = $image.attr('title') || $image.attr('alt'); + if (imageTitle) { + $imageWrap.attr('title', imageTitle).attr('data-caption', imageTitle); + } + } + }); + + jQuery.fancybox.defaults.hash = false; + jQuery('.fancybox').fancybox({ + loop : true, + helpers: { + overlay: { + locked: false + } + } + }); + }, + + imageCaption: function(selector) { + if (!CONFIG.image_caption.enable) { return; } + + jQuery(selector || `.markdown-body > p > img, .markdown-body > figure > img, + .markdown-body > p > a.fancybox, .markdown-body > figure > a.fancybox`).each(function() { + var $target = jQuery(this); + var $figcaption = $target.next('figcaption'); + if ($figcaption.length !== 0) { + $figcaption.addClass('image-caption'); + } else { + var imageTitle = $target.attr('title') || $target.attr('alt'); + if (imageTitle) { + $target.after(``); + } + } + }); + }, + + codeWidget() { + var enableLang = CONFIG.code_language.enable && CONFIG.code_language.default; + var enableCopy = CONFIG.copy_btn && 'ClipboardJS' in window; + if (!enableLang && !enableCopy) { + return; + } + + function getBgClass(ele) { + return Fluid.utils.getBackgroundLightness(ele) >= 0 ? 'code-widget-light' : 'code-widget-dark'; + } + + var copyTmpl = ''; + copyTmpl += '
'; + copyTmpl += 'LANG'; + copyTmpl += '
'; + jQuery('.markdown-body pre').each(function() { + var $pre = jQuery(this); + if ($pre.find('code.mermaid').length > 0) { + return; + } + if ($pre.find('span.line').length > 0) { + return; + } + + var lang = ''; + + if (enableLang) { + lang = CONFIG.code_language.default; + if ($pre[0].children.length > 0 && $pre[0].children[0].classList.length >= 2 && $pre.children().hasClass('hljs')) { + lang = $pre[0].children[0].classList[1]; + } else if ($pre[0].getAttribute('data-language')) { + lang = $pre[0].getAttribute('data-language'); + } else if ($pre.parent().hasClass('sourceCode') && $pre[0].children.length > 0 && $pre[0].children[0].classList.length >= 2) { + lang = $pre[0].children[0].classList[1]; + $pre.parent().addClass('code-wrapper'); + } else if ($pre.parent().hasClass('markdown-body') && $pre[0].classList.length === 0) { + $pre.wrap('
'); + } + lang = lang.toUpperCase().replace('NONE', CONFIG.code_language.default); + } + $pre.append(copyTmpl.replace('LANG', lang).replace('code-widget">', + getBgClass($pre[0]) + (enableCopy ? ' code-widget copy-btn" data-clipboard-snippet>' : ' code-widget">'))); + + if (enableCopy) { + var clipboard = new ClipboardJS('.copy-btn', { + target: function(trigger) { + var nodes = trigger.parentNode.childNodes; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].tagName === 'CODE') { + return nodes[i]; + } + } + } + }); + clipboard.on('success', function(e) { + e.clearSelection(); + e.trigger.innerHTML = e.trigger.innerHTML.replace('icon-copy', 'icon-success'); + setTimeout(function() { + e.trigger.innerHTML = e.trigger.innerHTML.replace('icon-success', 'icon-copy'); + }, 2000); + }); + } + }); + } +}; diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 000000000..d61bc2642 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,245 @@ +/* global Fluid, CONFIG */ + +window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; + +Fluid.utils = { + + listenScroll: function(callback) { + var dbc = new Debouncer(callback); + window.addEventListener('scroll', dbc, false); + dbc.handleEvent(); + return dbc; + }, + + unlistenScroll: function(callback) { + window.removeEventListener('scroll', callback); + }, + + listenDOMLoaded(callback) { + if (document.readyState !== 'loading') { + callback(); + } else { + document.addEventListener('DOMContentLoaded', function () { + callback(); + }); + } + }, + + scrollToElement: function(target, offset) { + var of = jQuery(target).offset(); + if (of) { + jQuery('html,body').animate({ + scrollTop: of.top + (offset || 0), + easing : 'swing' + }); + } + }, + + elementVisible: function(element, offsetFactor) { + offsetFactor = offsetFactor && offsetFactor >= 0 ? offsetFactor : 0; + var rect = element.getBoundingClientRect(); + const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + return ( + (rect.top >= 0 && rect.top <= viewportHeight * (1 + offsetFactor) + rect.height / 2) || + (rect.bottom >= 0 && rect.bottom <= viewportHeight * (1 + offsetFactor) + rect.height / 2) + ); + }, + + waitElementVisible: function(selectorOrElement, callback, offsetFactor) { + var runningOnBrowser = typeof window !== 'undefined'; + var isBot = (runningOnBrowser && !('onscroll' in window)) + || (typeof navigator !== 'undefined' && /(gle|ing|ro|msn)bot|crawl|spider|yand|duckgo/i.test(navigator.userAgent)); + if (!runningOnBrowser || isBot) { + return; + } + + offsetFactor = offsetFactor && offsetFactor >= 0 ? offsetFactor : 0; + + function waitInViewport(element) { + Fluid.utils.listenDOMLoaded(function() { + if (Fluid.utils.elementVisible(element, offsetFactor)) { + callback(); + return; + } + if ('IntersectionObserver' in window) { + var io = new IntersectionObserver(function(entries, ob) { + if (entries[0].isIntersecting) { + callback(); + ob.disconnect(); + } + }, { + threshold : [0], + rootMargin: (window.innerHeight || document.documentElement.clientHeight) * offsetFactor + 'px' + }); + io.observe(element); + } else { + var wrapper = Fluid.utils.listenScroll(function() { + if (Fluid.utils.elementVisible(element, offsetFactor)) { + Fluid.utils.unlistenScroll(wrapper); + callback(); + } + }); + } + }); + } + + if (typeof selectorOrElement === 'string') { + this.waitElementLoaded(selectorOrElement, function(element) { + waitInViewport(element); + }); + } else { + waitInViewport(selectorOrElement); + } + }, + + waitElementLoaded: function(selector, callback) { + var runningOnBrowser = typeof window !== 'undefined'; + var isBot = (runningOnBrowser && !('onscroll' in window)) + || (typeof navigator !== 'undefined' && /(gle|ing|ro|msn)bot|crawl|spider|yand|duckgo/i.test(navigator.userAgent)); + if (!runningOnBrowser || isBot) { + return; + } + + if ('MutationObserver' in window) { + var mo = new MutationObserver(function(records, ob) { + var ele = document.querySelector(selector); + if (ele) { + callback(ele); + ob.disconnect(); + } + }); + mo.observe(document, { childList: true, subtree: true }); + } else { + Fluid.utils.listenDOMLoaded(function() { + var waitLoop = function() { + var ele = document.querySelector(selector); + if (ele) { + callback(ele); + } else { + setTimeout(waitLoop, 100); + } + }; + waitLoop(); + }); + } + }, + + createScript: function(url, onload) { + var s = document.createElement('script'); + s.setAttribute('src', url); + s.setAttribute('type', 'text/javascript'); + s.setAttribute('charset', 'UTF-8'); + s.async = false; + if (typeof onload === 'function') { + if (window.attachEvent) { + s.onreadystatechange = function() { + var e = s.readyState; + if (e === 'loaded' || e === 'complete') { + s.onreadystatechange = null; + onload(); + } + }; + } else { + s.onload = onload; + } + } + var ss = document.getElementsByTagName('script'); + var e = ss.length > 0 ? ss[ss.length - 1] : document.head || document.documentElement; + e.parentNode.insertBefore(s, e.nextSibling); + }, + + createCssLink: function(url) { + var l = document.createElement('link'); + l.setAttribute('rel', 'stylesheet'); + l.setAttribute('type', 'text/css'); + l.setAttribute('href', url); + var e = document.getElementsByTagName('link')[0] + || document.getElementsByTagName('head')[0] + || document.head || document.documentElement; + e.parentNode.insertBefore(l, e); + }, + + loadComments: function(selector, loadFunc) { + var ele = document.querySelector('#comments[lazyload]'); + if (ele) { + var callback = function() { + loadFunc(); + ele.removeAttribute('lazyload'); + }; + Fluid.utils.waitElementVisible(selector, callback, CONFIG.lazyload.offset_factor); + } else { + loadFunc(); + } + }, + + getBackgroundLightness(selectorOrElement) { + var ele = selectorOrElement; + if (typeof selectorOrElement === 'string') { + ele = document.querySelector(selectorOrElement); + } + var view = ele.ownerDocument.defaultView; + if (!view) { + view = window; + } + var rgbArr = view.getComputedStyle(ele).backgroundColor.replace(/rgba*\(/, '').replace(')', '').split(/,\s*/); + if (rgbArr.length < 3) { + return 0; + } + var colorCast = (0.213 * rgbArr[0]) + (0.715 * rgbArr[1]) + (0.072 * rgbArr[2]); + return colorCast === 0 || colorCast > 255 / 2 ? 1 : -1; + }, + + retry(handler, interval, times) { + if (times <= 0) { + return; + } + var next = function() { + if (--times >= 0 && !handler()) { + setTimeout(next, interval); + } + }; + setTimeout(next, interval); + } + +}; + +/** + * Handles debouncing of events via requestAnimationFrame + * @see http://www.html5rocks.com/en/tutorials/speed/animations/ + * @param {Function} callback The callback to handle whichever event + */ +function Debouncer(callback) { + this.callback = callback; + this.ticking = false; +} + +Debouncer.prototype = { + constructor: Debouncer, + + /** + * dispatches the event to the supplied callback + * @private + */ + update: function() { + this.callback && this.callback(); + this.ticking = false; + }, + + /** + * ensures events don't get stacked + * @private + */ + requestTick: function() { + if (!this.ticking) { + requestAnimationFrame(this.rafCallback || (this.rafCallback = this.update.bind(this))); + this.ticking = true; + } + }, + + /** + * Attach this as the event listeners + */ + handleEvent: function() { + this.requestTick(); + } +}; diff --git a/lib/hbe.js b/lib/hbe.js new file mode 100644 index 000000000..71205dd75 --- /dev/null +++ b/lib/hbe.js @@ -0,0 +1,297 @@ +(() => { + 'use strict'; + + const cryptoObj = window.crypto || window.msCrypto; + const storage = window.localStorage; + + const storageName = 'hexo-blog-encrypt:#' + window.location.pathname; + const keySalt = textToArray('hexo-blog-encrypt的作者们都是大帅比!'); + const ivSalt = textToArray('hexo-blog-encrypt是地表最强Hexo加密插件!'); + +// As we can't detect the wrong password with AES-CBC, +// so adding an empty div and check it when decrption. +const knownPrefix = ""; + + const mainElement = document.getElementById('hexo-blog-encrypt'); + const wrongPassMessage = mainElement.dataset['wpm']; + const wrongHashMessage = mainElement.dataset['whm']; + const dataElement = mainElement.getElementsByTagName('script')['hbeData']; + const encryptedData = dataElement.innerText; + const HmacDigist = dataElement.dataset['hmacdigest']; + + function hexToArray(s) { + return new Uint8Array(s.match(/[\da-f]{2}/gi).map((h => { + return parseInt(h, 16); + }))); + } + + function textToArray(s) { + var i = s.length; + var n = 0; + var ba = new Array() + + for (var j = 0; j < i;) { + var c = s.codePointAt(j); + if (c < 128) { + ba[n++] = c; + j++; + } else if ((c > 127) && (c < 2048)) { + ba[n++] = (c >> 6) | 192; + ba[n++] = (c & 63) | 128; + j++; + } else if ((c > 2047) && (c < 65536)) { + ba[n++] = (c >> 12) | 224; + ba[n++] = ((c >> 6) & 63) | 128; + ba[n++] = (c & 63) | 128; + j++; + } else { + ba[n++] = (c >> 18) | 240; + ba[n++] = ((c >> 12) & 63) | 128; + ba[n++] = ((c >> 6) & 63) | 128; + ba[n++] = (c & 63) | 128; + j += 2; + } + } + return new Uint8Array(ba); + } + + function arrayBufferToHex(arrayBuffer) { + if (typeof arrayBuffer !== 'object' || arrayBuffer === null || typeof arrayBuffer.byteLength !== 'number') { + throw new TypeError('Expected input to be an ArrayBuffer') + } + + var view = new Uint8Array(arrayBuffer) + var result = '' + var value + + for (var i = 0; i < view.length; i++) { + value = view[i].toString(16) + result += (value.length === 1 ? '0' + value : value) + } + + return result + } + + async function getExecutableScript(oldElem) { + let out = document.createElement('script'); + const attList = ['type', 'text', 'src', 'crossorigin', 'defer', 'referrerpolicy']; + attList.forEach((att) => { + if (oldElem[att]) + out[att] = oldElem[att]; + }) + + return out; + } + + async function convertHTMLToElement(content) { + let out = document.createElement('div'); + out.innerHTML = content; + out.querySelectorAll('script').forEach(async (elem) => { + elem.replaceWith(await getExecutableScript(elem)); + }); + + return out; + } + + function getKeyMaterial(password) { + let encoder = new TextEncoder(); + return cryptoObj.subtle.importKey( + 'raw', + encoder.encode(password), + { + 'name': 'PBKDF2', + }, + false, + [ + 'deriveKey', + 'deriveBits', + ] + ); + } + + function getHmacKey(keyMaterial) { + return cryptoObj.subtle.deriveKey({ + 'name': 'PBKDF2', + 'hash': 'SHA-256', + 'salt': keySalt.buffer, + 'iterations': 1024 + }, keyMaterial, { + 'name': 'HMAC', + 'hash': 'SHA-256', + 'length': 256, + }, true, [ + 'verify', + ]); + } + + function getDecryptKey(keyMaterial) { + return cryptoObj.subtle.deriveKey({ + 'name': 'PBKDF2', + 'hash': 'SHA-256', + 'salt': keySalt.buffer, + 'iterations': 1024, + }, keyMaterial, { + 'name': 'AES-CBC', + 'length': 256, + }, true, [ + 'decrypt', + ]); + } + + function getIv(keyMaterial) { + return cryptoObj.subtle.deriveBits({ + 'name': 'PBKDF2', + 'hash': 'SHA-256', + 'salt': ivSalt.buffer, + 'iterations': 512, + }, keyMaterial, 16 * 8); + } + + async function verifyContent(key, content) { + const encoder = new TextEncoder(); + const encoded = encoder.encode(content); + + let signature = hexToArray(HmacDigist); + + const result = await cryptoObj.subtle.verify({ + 'name': 'HMAC', + 'hash': 'SHA-256', + }, key, signature, encoded); + console.log(`Verification result: ${result}`); + if (!result) { + alert(wrongHashMessage); + console.log(`${wrongHashMessage}, got `, signature, ` but proved wrong.`); + } + return result; + } + + async function decrypt(decryptKey, iv, hmacKey) { + let typedArray = hexToArray(encryptedData); + + const result = await cryptoObj.subtle.decrypt({ + 'name': 'AES-CBC', + 'iv': iv, + }, decryptKey, typedArray.buffer).then(async (result) => { + const decoder = new TextDecoder(); + const decoded = decoder.decode(result); + + // check the prefix, if not then we can sure here is wrong password. + if (!decoded.startsWith(knownPrefix)) { + throw "Decode successfully but not start with KnownPrefix."; + } + + const hideButton = document.createElement('button'); + hideButton.textContent = 'Encrypt again'; + hideButton.type = 'button'; + hideButton.classList.add("hbe-button"); + hideButton.addEventListener('click', () => { + window.localStorage.removeItem(storageName); + window.location.reload(); + }); + + document.getElementById('hexo-blog-encrypt').style.display = 'inline'; + document.getElementById('hexo-blog-encrypt').innerHTML = ''; + document.getElementById('hexo-blog-encrypt').appendChild(await convertHTMLToElement(decoded)); + document.getElementById('hexo-blog-encrypt').appendChild(hideButton); + + // support html5 lazyload functionality. + document.querySelectorAll('img').forEach((elem) => { + if (elem.getAttribute("data-src") && !elem.src) { + elem.src = elem.getAttribute('data-src'); + } + }); + + // support theme-next refresh + window.NexT && NexT.boot && typeof NexT.boot.refresh === 'function' && NexT.boot.refresh(); + + // TOC part + var tocDiv = document.getElementById("toc-div"); + if (tocDiv) { + tocDiv.style.display = 'inline'; + } + + var tocDivs = document.getElementsByClassName('toc-div-class'); + if (tocDivs && tocDivs.length > 0) { + for (var idx = 0; idx < tocDivs.length; idx++) { + tocDivs[idx].style.display = 'inline'; + } + } + + // trigger event + var event = new Event('hexo-blog-decrypt'); + window.dispatchEvent(event); + + return await verifyContent(hmacKey, decoded); + }).catch((e) => { + alert(wrongPassMessage); + console.log(e); + return false; + }); + + return result; + + } + + function hbeLoader() { + + const oldStorageData = JSON.parse(storage.getItem(storageName)); + + if (oldStorageData) { + console.log(`Password got from localStorage(${storageName}): `, oldStorageData); + + const sIv = hexToArray(oldStorageData.iv).buffer; + const sDk = oldStorageData.dk; + const sHmk = oldStorageData.hmk; + + cryptoObj.subtle.importKey('jwk', sDk, { + 'name': 'AES-CBC', + 'length': 256, + }, true, [ + 'decrypt', + ]).then((dkCK) => { + cryptoObj.subtle.importKey('jwk', sHmk, { + 'name': 'HMAC', + 'hash': 'SHA-256', + 'length': 256, + }, true, [ + 'verify', + ]).then((hmkCK) => { + decrypt(dkCK, sIv, hmkCK).then((result) => { + if (!result) { + storage.removeItem(storageName); + } + }); + }); + }); + } + + mainElement.addEventListener('keydown', async (event) => { + if (event.isComposing || event.keyCode === 13) { + const password = document.getElementById('hbePass').value; + const keyMaterial = await getKeyMaterial(password); + const hmacKey = await getHmacKey(keyMaterial); + const decryptKey = await getDecryptKey(keyMaterial); + const iv = await getIv(keyMaterial); + + decrypt(decryptKey, iv, hmacKey).then((result) => { + console.log(`Decrypt result: ${result}`); + if (result) { + cryptoObj.subtle.exportKey('jwk', decryptKey).then((dk) => { + cryptoObj.subtle.exportKey('jwk', hmacKey).then((hmk) => { + const newStorageData = { + 'dk': dk, + 'iv': arrayBufferToHex(iv), + 'hmk': hmk, + }; + storage.setItem(storageName, JSON.stringify(newStorageData)); + }); + }); + } + }); + } + }); + } + + hbeLoader(); + +})(); diff --git a/links/index.html b/links/index.html new file mode 100644 index 000000000..2aaa77089 --- /dev/null +++ b/links/index.html @@ -0,0 +1,657 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 友链 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + + +
+ +
+ + + + +
+ + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local-search.xml b/local-search.xml new file mode 100644 index 000000000..87afcc777 --- /dev/null +++ b/local-search.xml @@ -0,0 +1,3987 @@ + + + + + + + Leetcode-栈与队列 + + /2024/06/01/Leetcode/Leetcode-sq/ + + Leetcode-栈与队列

栈与队列

295. 数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。
  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10<sup>-5</sup> 以内的答案将被接受。
class MedianFinder {public:    priority_queue<int, vector<int>, less<int>> queMin;    priority_queue<int, vector<int>, greater<int>> queMax;    MedianFinder() {          }      void addNum(int num) {        if (queMin.empty() || num <= queMin.top()) {            queMin.push(num);            if (queMax.size() + 1 < queMin.size()) {                queMax.push(queMin.top());                queMin.pop();            }        } else {            queMax.push(num);            if (queMax.size() > queMin.size()) {                queMin.push(queMax.top());                queMax.pop();            }        }    }      double findMedian() {        if (queMin.size() > queMax.size()) {            return queMin.top();        }        return (queMin.top() + queMax.top()) / 2.0;     }};
]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode-图论 + + /2024/06/01/Leetcode/Leetcode-gt/ + + Leetcode-图论

图论

207. 课程表

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a<sub>i</sub>, b<sub>i</sub>] ,表示如果要学习课程 a<sub>i</sub>必须 先学习课程 b<sub>i</sub> ~ ~ 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

class Solution {public:    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {        vector<int> totalnum(numCourses,0);        vector<vector<int> > matrix(numCourses, vector<int>(0,0));        for(int i=0;i<prerequisites.size();i++){            totalnum[prerequisites[i][0]] += 1;            matrix[prerequisites[i][1]].push_back(prerequisites[i][0]);        }        bool judge = true;        while(judge){            judge = false;            for(int i=0;i<numCourses;i++){                if(totalnum[i] == 0){                    judge = true;                    for(int j=0;j<matrix[i].size();j++){                        totalnum[matrix[i][j]] -= 1;                    }                    totalnum[i] = -1;                }            }        }        for(int i=0;i<numCourses;i++){            if(totalnum[i] != -1){                return false;            }        }        return true;    }};
]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + LLM强化学习 + + /2024/06/01/LLM-RL/ + + LLM强化学习相关学习资料

相关链接

图解大模型RLHF系列之:人人都能看懂的PPO原理与源码解读:https://zhuanlan.zhihu.com/p/677607581

原理解析:https://blog.csdn.net/v_JULY_v/article/details/134242910

KL散度详解:https://blog.csdn.net/Rocky6688/article/details/103470437

简要版本解析:https://blog.csdn.net/u014386899/article/details/136633074

代码解读:https://zhuanlan.zhihu.com/p/696044978

强化学习

NLP中的强化学习

  • :模型根据上文,产生一个token
  • :即时收益,指语言模型当下产生token的收益
  • :实际期望总收益(即时+未来),指对语言模型“当下产生token ,一直到整个response生产结束”的后期收益预估。因为当下语言模型还没产出后的token,所以我们只是对它之后一系列动作的收益做了估计,因而称为“期望总收益”。

在RLHF-PPO阶段,一共有四个主要模型 ,分别是:

  • Actor Model:演员模型 ,这就是我们想要训练的目标语言模型
  • Critic Model:评论家模型 ,它的作用是预估总收益
  • Reward Model:奖励模型 ,它的作用是计算即时收益
  • Reference Model:参考模型 ,它的作用是在RLHF阶段给语言模型增加一些“约束”,防止语言模型训歪(朝不受控制的方向更新,效果可能越来越差)

Actor/Critic Model在RLHF阶段是需要训练的(图中给这两个模型加了粗边,就是表示这个含义);而Reward/Reference Model参数冻结的。

Critic/Reward/Reference Model共同组成了一个“奖励-loss”计算体系(我自己命名的,为了方便理解),我们综合它们的结果计算loss,用于更新Actor和Critic Model

Actor Model (演员模型)

Actor就是我们想要训练的目标语言模型。我们一般用SFT阶段产出的SFT模型来对它做初始化。

我们的最终目的是让Actor模型能产生符合人类喜好的response。所以我们的策略是,先喂给Actor一条prompt (这里假设batch_size = 1,所以是1条prompt),让它生成对应的response。然后,我们再将“prompt + response"送入我们的“奖励-loss”计算体系中去算得最后的loss,用于更新actor。

Reference Model(参考模型)

Reference Model(以下简称Ref模型)一般也用SFT阶段得到的SFT模型做初始化,在训练过程中,它的参数是冻结的。 Ref模型的主要作用是防止Actor”训歪”

我们希望训练出来的Actor模型既能达到符合人类喜好的目的,又尽量让它和SFT模型不要差异太大 。因此我们使用KL散度来衡量输出分布的相似度

img

  • 对Actor模型 ,我们喂给它一个prompt,它正常输出对应的response。那么response中每一个token肯定有它对应的log_prob结果,我们把这样的结果记为log_probs
  • 对Ref模型 ,我们把Actor生成的"prompt + response"喂给它,那么它同样能给出每个token的log_prob结果,我们记其为ref_log_probs
  • 那么这两个模型的输出分布相似度就可以用 ref_log_probs - log_probs 来衡量,就是KL散度的公式
    • ref_log_probs越高,说明Ref模型对Actor模型输出的肯定性越大。即Ref模型也认为Actor模型较Ref模型没有训歪

Critic Model(评论家模型)

Critic Model用于预测期望总收益,和Actor模型一样,它需要做参数更新

时刻,我们给不出客观存在的总收益,我们只能训练一个模型去预测它。

在RLHF中,我们不仅要训练模型生成符合人类喜好的内容的能力(Actor),也要提升模型对人类喜好量化判断的能力(Critic)

deepspeed-chat采用了Reward模型作为它的初始化,可以简单理解成,Reward/Critic模型和Actor模型的架构是很相似的(毕竟输入都一样),同时,它在最后一层增加了一个Value Head层,该层是个简单的线形层,用于将原始输出结果映射成单一的值。

Reward Model(奖励模型)

Reward Model用于计算生成token的即时收益,它就是RW阶段所训练的奖励模型,在RLHF过程中,它的参数是冻结的。

Reward模型是站在上帝视角的。这个上帝视角有两层含义:

  • 第一点,Reward模型是经过和“估算收益”相关的训练的,因此在RLHF阶段它可以直接被当作一个能产生客观值的模型。
  • 第二点,Reward模型代表的含义就是“即时收益”,你的token已经产生,因此即时收益自然可以立刻算出。

reward是对actor模型进行了某一个action之后的直接打分;而critic则是对这个actor模型的整体预估得分。每次actor模型更新后,critic模型都要对这个新的actor模型重新打分,所以critic模型也要更新参数。critic模型对actor模型的整体预估得分,是根据reward模型的每一次实时打分来预估的。当critic模型的预估得分达到了一定的基准,就代表actor模型训练完成。

RLHF-PPO的训练过程

  • 第一步,我们准备一个batch的prompts
  • 第二步,我们将这个batch的prompts喂给Actor模型,让它生成对应的responses
  • 第三步,我们把prompt+responses喂给我们的Critic/Reward/Reference模型,让它生成用于计算actor/critic loss的数据,按照强化学习的术语,我们称这些数据为经验(experiences)。
  • 第四步,我们根据这些经验,实际计算出actor/critic loss,然后更新Actor和Critic模型

Loss

Actor Loss

直观设计

  • Actor接收到当前上文,产出token ,概率是
  • Critic model 根据,产出对总收益的预测
  • actor loss =
    • 时,意味着Critic对Actor当前采取的动作给了正向反馈,因此我们就需要在训练迭代中提高,这样就能达到减小loss的作用。
    • 时,意味着Critic对Actor当前采取的动作给了负向反馈,因此我们就需要在训练迭代中降低,这样就能到达到减小loss的作用。

引入优势

如果Critic对的总收益预测为,但实际执行后的总收益是 ,我们就定义优势为:

,替换上面的

actor loss =

本来是即时收益,但是可以调整一下:(是最后一个时刻)

  • 时,我们更加关心Actor是否有在Ref的约束下生产token
  • 时,我们不仅关心Actor是否遵从了Ref的约束,也关心真正的即时收益

为什么只有最后一个时刻的被纳入了考量呢?这是因为在Reward模型训练阶段,就是用这个位置的的 来表示对完整的prompt + response的奖励预测(但不妨碍你理解成是执行完的即时奖励),然后用这个指标来做模型eval的(但是Reward训练阶段算loss时,还是考虑了response部分所有token输出的reward值)。所以到了RLHF的场景下,其余时刻的即时奖励,我们就用“Actor是否遵循了Ref的约束”来进行评价。

改造优势

新引入的也是一个常量,可将其理解为权衡因子,直觉上看它控制了在计算当前优势时对未来优势的考量。

对于最后一个时刻,它的未来收益和未来优势都是0,也就是,这是可以直接算出来的。而有了 ,我们可以通过动态规划的方法,把所有时刻的优势算出来

重复使用

太慢了,所以一个batch的经验值将被用于n次模型更新

1个batch的经验值被使用ppo_epochs次,在这ppo_epochs中,Actor是不吃任何新数据,不做任何交互的,所以我们只能让Actor“模拟”一下和环境交互的过程,吐出一些新数据出来。

还是保证新的数据和旧的差不多,还是使用KL散度

actor loss =

在Actor想通过模拟交互的方式,使用一个batch的经验值更新自己时,它需要收到真正吃到batch的那个时刻的Actor的约束,这样才能在有效利用batch,提升训练速度的基础上,保持训练的稳定。

设置一个范围,差距太大就不要更新了

Critic Loss

  • :Critic对时刻的总收益的预估,这个总收益包含即时和未来的概念(预估收益)
  • :Reward计算出的即时收益,Critic预测出的及之后时候的收益的折现,这是比更接近时刻真值总收益的一个值(实际收益)

第一想法:Critic loss =$ (𝑅_𝑡+ \gamma ∗𝑉_{𝑡+1}-V_t)^2$

实际收益优化:

预估收益优化:类比于Actor,Critic模型在ppo_epochs的过程中也是不断更新的。所以这个可以理解成是 ,也就是真正吃了batch,参与产出经验的那个时候的Critic产出的收益预测结果。

用老设计了了一个变动范围,然后用这个变动范围去约束新

最终我们就取实际收益和预估收益的MSE做为loss就好,这里注意,计算实际收益时都是老Critic(真正吃了batch的那个)产出的结果,而预估收益是随着ppo_epochs而变动的。

DPO

DPO通过简单的分类目标直接优化最满足偏好的策略,而没有明确的奖励函数或RL

DPO的本质在于增加了被首选的response相对不被首选的response的对数概率,但它包含了一个动态的、每个示例的重要性权重,以防止设计的概率比让模型的能力退化。

img

变种

IPO相当于在DPO的损失函数上添加了一个正则项,从而可以使得不使用early stopping技巧就可以使模型收敛。

KTO定义的损失函数只需要将样本标注为"好(good)“或"坏(bad)”,从而使得获取标注样本的成本更低。(就是不需要一对一对标注了)

CPO在训练期间不需要加载参考策略模型。通过省略内存的参考模型,CPO提高了操作效率,与DPO相比,能够以更低的成本训练更大的模型。

ORPO整合SFT和DPO,且不需要额外的参考模型

SimPO 包含两个主要组件:(1)在长度上归一化的奖励,其计算方式是使用策略模型的奖励中所有 token 的平均对数概率;(2)目标奖励差额,用以确保获胜和失败响应之间的奖励差超过这个差额。

SimPO 不需要参考模型,性能却明显优于 DPO 及其最新变体,且不会显著增加响应长度

]]>
+ + + + + Study + + + + + + + Algorithm + + Python + + Pytorch + + + +
+ + + + + Leetcode-动态规划 + + /2024/04/30/Leetcode/Leetcode-dp/ + + Leetcode-动态规划

动态规划

64. 最小路径和

给定一个包含非负整数的 <em>m</em> x <em>n</em> 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明: 每次只能向下或者向右移动一步。

class Solution {public:    int minPathSum(vector<vector<int>>& grid) {        int m = grid.size();        int n = grid[0].size();        vector<vector<int> > dp(m, vector<int>(n,0));        dp[0][0] = grid[0][0];        for(int i=1;i<m;i++){            dp[i][0] = dp[i-1][0] + grid[i][0];        }        for(int j=1;j<n;j++){            dp[0][j] = dp[0][j-1] + grid[0][j];        }        for(int i=1;i<m;i++){            for(int j=1;j<n;j++){                dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];            }        }        return dp[m-1][n-1];    }};
]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode-二叉树 + + /2024/04/14/Leetcode/Leetcode-bt/ + + Leetcode-二叉树

二叉树

94. 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回 它的 中序 遍历

class Solution {public:    vector<int> result;    void inorder(TreeNode* root){        if(root == NULL){            return;        }        inorder(root->left);        result.push_back(root->val);        inorder(root->right);    }    vector<int> inorderTraversal(TreeNode* root) {        inorder(root);        return result;    }};

103. 二叉树的锯齿形层序遍历

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

class Solution {public:    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {        vector<vector<int> > result;        if(root == NULL){            return result;        }        queue<TreeNode*> q;        q.push(root);        int sign = 0;        while(!q.empty()){            int t = q.size();            vector<int> temp;            for(int i=0;i<t;i++){                TreeNode* x = q.front();                q.pop();                temp.push_back(x->val);                if(x->left != NULL){                    q.push(x->left);                }                if(x->right != NULL){                    q.push(x->right);                }            }            if(sign == 1){                reverse(temp.begin(), temp.end());            }            result.push_back(temp);            sign = 1 - sign;        }        return result;    }};
]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode-回溯算法 + + /2024/04/14/Leetcode/Leetcode-bkt/ + + Leetcode-回溯算法

回溯

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合。

class Solution {public:    vector<string> result;    void backtracking(vector<string> temp, int n, int nowleft, int nowright){        if(temp.size() == n * 2){            string res = "";            for(int i=0;i<temp.size();i++){                res += temp[i];            }            result.push_back(res);            return;        }        if (nowleft < n){            temp.push_back("(");            backtracking(temp,n,nowleft+1,nowright);            temp.pop_back();        }        if(nowright < n && nowright < nowleft){            temp.push_back(")");            backtracking(temp,n,nowleft,nowright+1);            temp.pop_back();        }    }    vector<string> generateParenthesis(int n) {        vector<string> temp;        backtracking(temp,n,0,0);        return result;    }};

31. 下一个排列

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须原地修改,只允许使用额外常数空间。

class Solution {public:    void nextPermutation(vector<int>& nums) {        int n = nums.size();        int index = n-1;        for(;index>=1;index--){            if(nums[index] > nums[index-1]){                break;            }        }        if(index != 0){            index -= 1;            for(int i=n-1;i>=0;i--){                if(nums[i] > nums[index]){                    swap(nums[index], nums[i]);                    reverse(nums.begin()+index+1,nums.end());                    return;                }            }        }        else{            reverse(nums.begin(), nums.end());        }    }};

79. 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

class Solution {public:    bool result = false;    void DFS(int i, int j, int m, int n,int now, vector<vector<char>>& board,vector<vector<bool> > &visited,  string word){        if(i < 0 || j < 0 || i >= m || j >= n || visited[i][j] == true || board[i][j] != word[now]){            return;        }        if(now == word.size() - 1){            result = true;            return;        }        visited[i][j] = true;        DFS(i+1,j,m,n,now+1,board,visited, word);        DFS(i-1,j,m,n,now+1,board,visited, word);        DFS(i,j+1,m,n,now+1,board,visited, word);        DFS(i,j-1,m,n,now+1,board,visited, word);        visited[i][j] = false;    }    bool exist(vector<vector<char>>& board, string word) {        int m = board.size();        int n = board[0].size();        for(int i=0;i<m;i++){            for(int j=0;j<n;j++){                vector<vector<bool> > visited(m, vector<bool>(n, false));                DFS(i,j,m,n,0,board,visited, word);                if(result){                    return result;                }            }        }        return result;    }};
]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode-双指针法 + + /2024/04/02/Leetcode/Leetcode-tp/ + + Leetcode-双指针法

双指针法

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

class Solution {public:    int lengthOfLongestSubstring(string s) {        int result = 0;        unordered_set<char> st;        int left = 0;        for(int right=0; right<s.size(); right++){            while(st.find(s[right]) != st.end()){                st.erase(s[left]);                left++;            }            st.insert(s[right]);            result = max(result, right - left + 1);        }        return result;    }};

4. 寻找两个正序数组的中位数

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

class Solution {public:    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {        int m = nums1.size();        int n = nums2.size();        bool next;        int sign = (m + n) / 2;         if((m + n) % 2 == 1){            next = false;        } else{            sign -= 1;            next = true;        }        int res1 = 0;        int res2 = 0;        int globalindex = 0;        int nums1index = 0;        int nums2index = 0;        while(globalindex <= sign){            if(nums1index < m && nums2index < n){                if(nums1[nums1index] < nums2[nums2index]){                    res1 = nums1[nums1index];                    nums1index += 1;                } else{                    res1 = nums2[nums2index];                    nums2index += 1;                }            } else if (nums1index < m){                res1 = nums1[nums1index];                nums1index += 1;            } else {                res1 = nums2[nums2index];                nums2index += 1;            }            globalindex += 1;        }        if(next == false){            return res1;        }        if(nums1index < m && nums2index < n){            if(nums1[nums1index] < nums2[nums2index]){                res2 = nums1[nums1index];            } else{                res2 = nums2[nums2index];            }        } else if (nums1index < m){            res2 = nums1[nums1index];        } else {            res2 = nums2[nums2index];        }        return ((double)res1 + (double)res2) / 2.0;    }};

5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

class Solution {public:    string longestPalindrome(string s) {        vector<int> result = {0,0};        for(int i=0;i<s.size()-1;i++){            int left = i;            int right = i;            while(left >= 0 && right < s.size()){                if(s[left] == s[right]){                    if(result[1] - result[0] < right - left){                        result[1] = right;                        result[0] = left;                    }                    right += 1;                    left -= 1;                } else{                    break;                }            }            left = i;            right = i+1;            while(left >= 0 && right < s.size()){                if(s[left] == s[right]){                    if(result[1] - result[0] < right - left){                        result[1] = right;                        result[0] = left;                    }                    right += 1;                    left -= 1;                } else{                    break;                }            }        }        return s.substr(result[0], result[1]-result[0] + 1);    }};

9. 回文数

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

  • 例如,121 是回文,而 123 不是。
class Solution {public:    bool isPalindrome(int x) {        string s = to_string(x);        int left = 0;        int right = s.size() - 1;        while(left < right){            if(s[left] != s[right]){                return false;            }            left += 1;            right -= 1;        }        return true;    }};

11. 盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明: 你不能倾斜容器。

class Solution {public:    int maxArea(vector<int>& height) {        int maxarea = 0;        int left = 0;        int right = height.size() - 1;        while(left < right) {            if(height[left] < height[right]){                maxarea = max(maxarea, (right - left) * height[left]);                left += 1;            } else{                maxarea = max(maxarea, (right - left) * height[right]);                right -= 1;            }        }        return maxarea;    }};

88. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意: 最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n

class Solution {public:    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {        int index = m + n - 1;        m -= 1;        n -= 1;        while(index >= 0){            if(m < 0){                nums1[index] = nums2[n];                n--;            } else if (n < 0){                nums1[index] = nums1[m];                m--;            } else{                if(nums1[m] > nums2[n]){                    nums1[index] = nums1[m];                    m--;                } else{                    nums1[index] = nums2[n];                    n--;                }            }            index--;        }        return;    }};

23. 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

class Solution {public:    ListNode* merge(vector<ListNode*>& lists, int start, int end){        if(start > end){            return NULL;        }        if(start == end){            return lists[start];        }        int mid = (end - start) / 2 + start;        ListNode* first = merge(lists, start, mid);        ListNode* second = merge(lists, mid + 1, end);        if(first == NULL){            return second;        }        if(second == NULL){            return first;        }        ListNode* head = new ListNode(0);        ListNode* p = head;        while(first != NULL && second != NULL){            if(first->val < second->val){                p->next = first;                first = first->next;            } else{                p->next = second;                second = second->next;            }            p = p->next;        }        if(first != NULL){            p->next = first;        } else{            p->next = second;        }        return head->next;    }    ListNode* mergeKLists(vector<ListNode*>& lists) {        return merge(lists, 0, lists.size()-1);    }};
]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode-基本数据结构 + + /2024/04/02/Leetcode/Leetcode-ds/ + + Leetcode-基本数据结构

数组

7. 整数反转

给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。

如果反转后整数超过 32 位的有符号整数的范围 [−2<sup>31</sup>,  2<sup>31 </sup>− 1] ,就返回 0。

假设环境不允许存储 64 位整数(有符号或无符号)。

class Solution {public:    int reverse(int x) {        int rev = 0;        while (x != 0) {            if (rev < INT_MIN / 10 || rev > INT_MAX / 10) {                return 0;            }            int digit = x % 10;            x /= 10;            rev = rev * 10 + digit;        }        return rev;    }};

215. 数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 <strong>k</strong> 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

class Solution {public:    void quicksort(vector<int>& nums, int start, int end, int k){        if(start >= end){            return;        }        int x = nums[start];        int left = start;        int right = end;        while(left < right){            while(left < right && nums[right] <= x){                right--;            }            nums[left] = nums[right];            while(left < right && nums[left] > x){                left++;            }            nums[right] = nums[left];        }        nums[left] = x;        // start    left    end           // k        if(k == left + 1){            return;        } else if(k < left+1){            quicksort(nums, start, left-1,k);        } else{            quicksort(nums,left+1,end,k);        }    }    int findKthLargest(vector<int>& nums, int k) {        quicksort(nums, 0, nums.size()-1,k);        return nums[k-1];    }};

347. 前K个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

class Solution {public:    void quicksort(vector<pair<int,int> >& nums, int start, int end, int k){        if(start >= end){            return;        }        pair<int,int> x = nums[start];        int left = start;        int right = end;        while(left < right){            while(left < right && nums[right].second <= x.second){                right--;            }            swap(nums[left],nums[right]);            while(left < right && nums[left].second > x.second){                left++;            }            swap(nums[left],nums[right]);        }        nums[left].first = x.first;        nums[left].second = x.second;        // start  left  end;        if(k == left){            return;        } else if(k > left){            quicksort(nums, left+1, end,k);        } else{            quicksort(nums, start, left-1,k);        }    }    vector<int> topKFrequent(vector<int>& nums, int k) {        vector<pair<int, int> > result;        unordered_map<int, int> mp;        for(int i=0;i<nums.size();i++){            mp[nums[i]]++;        }        for(auto it=mp.begin();it!= mp.end();it++){            result.push_back({it->first, it->second});        }        quicksort(result, 0, result.size()-1,k);        vector<int> res;        for(int i=0;i<k;i++){            res.push_back(result[i].first);        }        return res;    }};

32. 最长有效括号

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

class Solution {public:    int longestValidParentheses(string s) {        vector<bool> visit(s.size(),false);        stack<pair<char,int> > st;        for(int i=0;i<s.size();i++){            if(st.empty() || s[i] == '(' || (s[i] == ')' && st.top().first == ')')){                st.push({s[i],i});                continue;            }            for(int j=st.top().second;j<=i;j++){                visit[j] = true;            }            st.pop();        }        int maxlength = 0;        int index = 0;        while(index < visit.size()){            int counttemp = 0;            while(index < visit.size() && visit[index]){                counttemp += 1;                index += 1;            }            maxlength = max(maxlength, counttemp);            while(index < visit.size() && !visit[index]){                index += 1;            }        }        return maxlength;    }};

2570. 合并两个二维数组 - 求和法

给你两个 二维 整数数组 nums1nums2.

  • nums1[i] = [id<sub>i</sub>, val<sub>i</sub>] 表示编号为 id<sub>i</sub> 的数字对应的值等于 val<sub>i</sub>
  • nums2[i] = [id<sub>i</sub>, val<sub>i</sub>] 表示编号为 id<sub>i</sub> 的数字对应的值等于 val<sub>i</sub>

每个数组都包含 互不相同 的 id ,并按 id 以 递增 顺序排列。

请你将两个数组合并为一个按 id 以递增顺序排列的数组,并符合下述条件:

  • 只有在两个数组中至少出现过一次的 id 才能包含在结果数组内。
  • 每个 id 在结果数组中 只能出现一次 ,并且其对应的值等于两个数组中该 id 所对应的值求和。如果某个数组中不存在该 id ,则认为其对应的值等于 0

返回结果数组。返回的数组需要按 id 以递增顺序排列。

class Solution {public:    static bool cmp(const vector<int> &a, const vector<int> &b){        return a[0] < b[0];    }    vector<vector<int>> mergeArrays(vector<vector<int>>& nums1, vector<vector<int>>& nums2) {        unordered_map<int, int> mp;        for(int i=0;i<nums1.size();i++){            if(mp.find(nums1[i][0]) != mp.end()){                mp[nums1[i][0]] += nums1[i][1];            } else{                mp[nums1[i][0]] = nums1[i][1];            }        }        for(int i=0;i<nums2.size();i++){            if(mp.find(nums2[i][0]) != mp.end()){                mp[nums2[i][0]] += nums2[i][1];            } else{                mp[nums2[i][0]] = nums2[i][1];            }        }        vector<vector<int> > result;        for(auto it = mp.begin(); it != mp.end();it++){            result.push_back(vector<int> {it->first, it->second});        }        sort(result.begin(), result.end(), cmp);        return result;    }};

33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

class Solution {public:    int search(vector<int>& nums, int target) {        int n = nums.size();        int left = 0;        int right = n-1;        while(left <= right){            int mid = (right - left) / 2 + left;            if(nums[mid] == target){                return mid;            }            if(nums[0] <= nums[mid]){                if(nums[0] <= target && target < nums[mid]){                    right = mid - 1;                } else{                    left = mid + 1;                }            } else{                if(nums[mid] < target && target <= nums[n-1]){                    left = mid + 1;                } else{                    right = mid - 1;                }            }        }        return -1;    }};

48. 旋转图像

给定一个 *n * × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。**请不要 **使用另一个矩阵来旋转图像。

class Solution {public:    void rotate(vector<vector<int>>& matrix) {        int n = matrix.size();        for(int i=0;i< n / 2;i++){            for(int j=0;j<(n+1) / 2;j++){                int temp = matrix[i][j];                matrix[i][j] = matrix[n-j-1][i];                matrix[n-j-1][i] = matrix[n-i-1][n-j-1];                matrix[n-i-1][n-j-1] = matrix[j][n-i-1];                matrix[j][n-i-1] = temp;            }        }    }};

75. 颜色分类

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

class Solution {public:    void sortColors(vector<int>& nums) {        int left = 0;        for(int i=0;i<nums.size();i++){            if(nums[i] == 0){                swap(nums[i], nums[left]);                left += 1;            }        }        for(int i=left;i<nums.size();i++){            if(nums[i] == 1){                swap(nums[i], nums[left]);                left += 1;            }        }    }};

73. 矩阵置零

给定一个 <em>m</em> x <em>n</em> 的矩阵,如果一个元素为 0 ** ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。**

class Solution {public:    void setZeroes(vector<vector<int>>& matrix) {        int m = matrix.size();        int n = matrix[0].size();        vector<bool> rowvector(m,false);        vector<bool> colvector(n,false);        for(int i=0;i<m;i++){            for(int j=0;j<n;j++){                if(matrix[i][j] == 0){                    rowvector[i] = true;                    colvector[j] = true;                }            }        }        for(int i=0;i<m;i++){            for(int j=0;j<n;j++){                if(rowvector[i] == true || colvector[j] == true){                    matrix[i][j] = 0;                }            }        }        return;    }};

118. 杨辉三角

给定一个非负整数 numRows 生成「杨辉三角」的前 *numRows *行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

class Solution {public:    vector<vector<int>> generate(int numRows) {        vector<vector<int> > result;        vector<int> temp;        temp.push_back(1);        result.push_back(temp);        if(numRows == 1){            return result;        }        for(int i=2;i<=numRows;i++){            vector<int> t;            t.push_back(1);            for(int j=1;j<i-1;j++){                t.push_back(result[i-2][j-1] + result[i-2][j]);            }            t.push_back(1);            result.push_back(t);        }        return result;    }};

153. 寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:* 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]

  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

class Solution {public:    int findMin(vector<int>& nums) {        int n = nums.size();        int left = 0;        int right = n-1;        while(left < right){            int mid = (right - left) / 2 + left;            if(nums[mid] < nums[right]){                right = mid;            } else {                left = mid + 1;            }        }        return nums[left];    }};

链表

2. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

class Solution {public:    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {        int cur = 0;        ListNode* head = new ListNode(-1);        ListNode* l3 = head;        while(l1 != NULL || l2 != NULL){            int target;            if(l1 == NULL){                target = l2->val + cur;                l2 = l2->next;            } else if(l2 == NULL){                target = l1->val + cur;                l1 = l1->next;            } else{                target = l1->val + l2->val + cur;                l1 = l1->next;                l2 = l2->next;            }            if (target >= 10){                cur = 1;                target -= 10;            } else{                cur = 0;            }            l3->next = new ListNode(target);            l3 = l3->next;        }        if(cur == 1){            l3->next = new ListNode(1);        }        return head->next;    }};

21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

class Solution {public:    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {        ListNode* dummy = new ListNode(0);        ListNode* head = dummy;        while(list1 != NULL && list2 != NULL){            if(list1->val < list2->val){                dummy->next = list1;                dummy = dummy->next;                list1 = list1->next;            } else{                dummy->next = list2;                dummy = dummy->next;                list2 = list2->next;            }        }        if(list1 != NULL){            dummy->next = list1;        }        if(list2 != NULL){            dummy->next = list2;        }        return head->next;    }};

哈希表

字符串

6. Z 字形变换

将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

P   A   H   NA P L S I I GY   I   R

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);
class Solution {public:    string convert(string s, int numRows) {        if(numRows == 1){            return s;        }        vector<queue<char> > vt(numRows);        int nowindex = 0;        int reverse = false;        for(int i=0;i<s.size();i++){            vt[nowindex].push(s[i]);            if(nowindex == 0 && reverse == true){                reverse = false;                nowindex += 1;            } else if (nowindex == numRows-1 && reverse == false){                reverse = true;                nowindex -= 1;            } else {                if(reverse == false){                    nowindex += 1;                } else{                    nowindex -= 1;                }            }        }        string resultstring = "";        for(int i=0;i<vt.size();i++){            while(!vt[i].empty()){                resultstring += vt[i].front();                vt[i].pop();            }        }        return resultstring;    }};

8. 字符串转换整数 (atoi)

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

函数 myAtoi(string s) 的算法如下:

  1. 读入字符串并丢弃无用的前导空格
  2. 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
  3. 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
  4. 将前面步骤读入的这些数字转换为整数(即,“123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
  5. 如果整数数超过 32 位有符号整数范围 [−2<sup>31</sup>,  2<sup>31 </sup>− 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2<sup>31</sup> 的整数应该被固定为 −2<sup>31</sup> ,大于 2<sup>31 </sup>− 1 的整数应该被固定为 2<sup>31 </sup>− 1
  6. 返回整数作为最终结果。

注意:

  • 本题中的空白字符只包括空格字符 ' '
  • 除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。
class Solution {public:    int myAtoi(string s) {        int res = 0;        bool havenumsign = false;        bool negativesign = false;        bool fakebigsign = false;        for(int i=0;i<s.size();i++){            if(s[i] == ' ' && havenumsign == false){                continue;            }            if(s[i] == '-' && havenumsign == false){                negativesign = true;                havenumsign = true;                continue;            }            if(s[i] == '+' && havenumsign == false){                havenumsign = true;                continue;            }            if(s[i] >= '0' && s[i] <= '9'){                havenumsign = true;                if(res > (INT_MAX - s[i] + '0') / 10){                    res = INT_MAX;                    fakebigsign = true;                    break;                }                res = res * 10 - '0' + s[i];                continue;            }            if(havenumsign == true){                break;            }            break;        }        if (negativesign){            if(res == INT_MAX && fakebigsign){                return INT_MIN;            }            return -res;        }        return res;    }};

12. 整数转罗马数字

罗马数字包含以下七种字符: IVXLCDM

字符          数值I             1V             5X             10L             50C             100D             500M             1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给你一个整数,将其转为罗马数字。

class Solution{public:    string intToRoman(int num)    {        pair<int, string> valueSymbols[] = {            {1000, "M"},            {900, "CM"},            {500, "D"},            {400, "CD"},            {100, "C"},            {90, "XC"},            {50, "L"},            {40, "XL"},            {10, "X"},            {9, "IX"},            {5, "V"},            {4, "IV"},            {1, "I"},        };        string roman;        for (auto &[value, symbol] : valueSymbols)        {            while (num >= value)            {                num -= value;                roman += symbol;            }            if (num == 0)            {                break;            }        }        return roman;    }};
]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 杂谈-20231121 + + /2023/11/21/diary/diary20231121/ + + 2023年11月21日,周二

又感冒了(or 发烧?)没什么区别吧,反正现在感冒必发烧。

我还记得上初中的时候,有一次去辽工大打篮球,碰见了有一段时间没有见面的小学同学。他当时问了我一句话:“你还像小时候那样总生病吗?“当时我很奇怪,因为在我的印象里面小学时候生病不算很多。这个小学同学我至今也没有再见过了,也没有联系方式,但是这一次见面我始终都会记得,可能就因为他问了我这一句话吧。

初中我已经不太记得了,但是高中确实一直在生病。几乎每一个月我都要感冒或者发烧一次。尤其是刚刚保送的一个月中,我还记得应该是周四的物理晚自习(当时物理老师给我的印象很恐怖),正好我也在生病,我就把卷子都扔给了我同桌,美美的回去休息了一个晚上,第二天就基本好的差不多了。从那之后我渐渐意识到,生病也并不是纯客观原因,其实自己的情绪、压力等主观因素才是生病的必要条件。

上了大学之后我的感冒的次数就少很多了,但是每次有一些让我非常伤心难过的事情的时候,总会发一次烧。发烧逐渐成为了我宣泄的一个出口。心情难过了,无处抱怨,用较高的体温促使自己休息一下,帮助自己放松心情缓解压力。

前一段刚刚发烧了一次,在床上躺了一天的同时出去吃了一些想吃的,完全没有看电脑。然而短暂的放松过后,自己的任务也并没有随之减轻,还是要一点一点继续推进。虽然发烧可以帮助我休息,但是实际上并没有对我的目标等起到任何的作用,短暂的麻痹过后还是要继续前行。可能我就是这样的人吧,目标很现实,丝毫不敢放松,完成一个目标后开心的同时又向着下一个目标推进,因此我现在过的并不快乐。

如果有一天,我能真正放下一切好好休息一下,才算与自己达成了和解,内心可能才能真正快乐一些?

写的稍微有点丧,心情不太好。

]]>
+ + + + + Life + + + + + + + Diary + + + +
+ + + + + 杂谈-20231119 + + /2023/11/19/diary/diary20231119/ + + 2023年11月19日,周日

今天看了一些自己博客的文章,发现对外公开的居然全都是刷题或者学习的内容,对于外人来说是不是太枯燥了一些hh。

于是挑了几篇过了很长时间的,或者已经没有隐藏的必要的文章,放出来也可以让其他人对我有更多的了解。

当然没放出来的文章还有很多,没办法很多的内容利益相关,或者写的时候直呼其名,要是公开感觉对其他人不太好,后续我会慢慢调整一下。

这些文章基本都在Life的标签下。

文笔不好,请见谅。

]]>
+ + + + + Life + + + + + + + Diary + + + +
+ + + + + Stance Detection + + /2023/10/07/Stance-Detection/ + + 立场检测相关内容总结整理

数据集

SemEval 2016

论文:Stance and Sentiment in Tweets

数据集可视化:http://www.saifmohammad.com/WebPages/StanceDataset.htm

VAST

Zero-shot数据集

New data released in this submission. Short column descriptions

  • author: username of the comment author
  • post: original comment, unprocessed
  • ori_topic: heuristically extracted topic
  • ori_id: id generated to link post and heuristically extracted topics
  • new_topic: updated topic from crowdsourced annotations
  • label: stance label, 0=con, 1=pro, 2=neutral
  • type_idx: type number, 1=HeurTopic, 2=CorrTopic, 3=ListTopic, 4=Synthetic neutral
  • new_id: unique id for every comment-topic-label pair
  • arc_id: id of the original article on NYT
  • text: sentence and word tokenized and lowercased text, with punctuation and stopwords removed
  • text_s: string version of text
  • topic: tokenized and lowercased version topic, with punctuation and stopwords removed
  • topic_str: string version of topic
  • seen?: indicator for zero-shot or few-shot example, 0=zero-shot, 1=few-shot
  • contains_topic?: indicator for whether topic is contained in the text, 0=no, 1=yes
  • change_lst: list of swapped words (unique to vast_test-sentswap.csv)
  • change_type: type of sentiment swapping
  • LexSim: a list of lexically similar training topics (if a zero-shot topic)
  • Qte: whether the example contains quotes (1=yes, 0=no)
  • Sarc: whether the example contains sarcasm (1=yes, 0=no)
  • Imp: whether the text contains the topic and the label is non-neutral (1=yes, 0=no)
  • mlS: whether there are other examples with the same document and different, non-neutral, stance labels (1=yes, 0=no)
  • mlT: whether there are other examples with the same document and different topics (1=yes, 0=no)

WT-WT

相关链接:https://github.com/BinLiang-NLP/TPDG

51284条英文Tweet

关于公司的兼并收购的信息,第一个金融领域的数据集

四个标签:

  • Support:两个公司会合并成一个公司
  • Refute:对两个公司要合并成一个的消息表示怀疑
  • Comment:对合并消息的评论,中立态度
  • Unrelated:完全不相关

P-stance

21574条英文Tweet

对三个target(Donald Trump(7953),Joe Biden(7296),Bernie Sanders(6325))的立场

按照8:1:1进行划分

UKP

论文

2017

A Dataset for Multi-Target Stance Detection

时间:2017年4月

等级:EACL 2017

2020

Will-They-Won’t-They: A Very Large Dataset for Stance Detection on Twitter

时间:2020年5月1日

等级:ACL 2020

pPvtXj0.md.png

思想:

  • 第一个金融领域的立场数据集,描述公司的兼并收购的信息
  • 首先爬取关于公司、兼并等内容的Tweet
  • 定义四个标签(support, refute, comment, unrelated),其中一个Tweet的不同的target可能会有不同的标签
  • 找人进行标注,评估了标注的质量,并与之前的数据集进行了对比
  • 对目前的一些模型进行了这个数据集上面的测试

Zero-Shot Stance Detection: A Dataset and Model using Generalized Topic Representations

时间:2020年10月7日

等级:EMNLP 2020(CCF B)

思想:提出了VAST数据集

  • 纽约时报辩论区的评论内容
  • 选择了3365条评论,包括304个主题,找人工进行主题标注
  • 中立的立场很少,从支持与反对两种类别中选一些可能性较低的加入到中立标签中

同时提出了一个方法解决Zero-shot问题

pPz7FzQ.md.png

  • 文档和主题联合输入
  • 对主题进行聚类,获取注意力

数据集:VAST

2021

Target-adaptive Graph for Cross-target Stance Detection

时间:2021年4月

等级:WWW 2021(CCF A)

tWT–WT: A Dataset to Assert the Role of Target Entities for Detecting Stance of Tweets

时间:2021年6月

等级:NAACL 2021(CCF B)

Adversarial Learning for Zero-Shot Stance Detection on Social Media

时间:2021年6月

等级:NAACL 2021(CCF B)

思想:使用对抗学习增强zero-shot的立场检测的效果

piUUZ6J.md.png

  • 使用BiCond编码text
  • 将编码的向量进行正则化
  • 对立场进行分类
  • 对topic进行鉴别
  • 增加对抗训练的技巧

数据集:Sem-16

Stance Detection in COVID-19 Tweets

时间:2021年8月

等级:ACL 2021(CCF A)

思想:

  • 构建了一个COVID-19数据集,包括四个target,例如关闭学校、居家、戴口罩等
  • 用无标签的数据做预训练
  • 对不同的监督学习方法进行了比较

数据集:自行构建的COVID-19数据集

Enhancing Zero-shot and Few-shot Stance Detection with Commonsense Knowledge Graph

时间:2021年8月

等级:ACL 2021 Findings (CCF A)

思想:topic在文本中是可以通过图推断出来的

piU6EUH.png

  • 用Bert对文本和topic进行编码
  • 使用ConceptNet获取文本之间的关系
  • 进行立场分类检测

数据集:

MeLT: Message-Level Transformer with Masked Document Representations as Pre-Training for Stance Detection

时间:2021年09月16日

等级:EMNLP 2021 Findings

pPj2v6S.png

思想:

  • 在Twitter数据集上做预训练,将word级别的mask更改为message级别的mask,message的表示是word的表示取平均,按照时间顺序进行排列,对某个人的一些message进行随机mask(Bert的方式),让模型预测该位置的message。
  • 后续进行分类任务的微调

数据集:SemEval 2016

P-Stance: A Large Dataset for Stance Detection in Political Domain

时间:2021年08月

等级:ACL 2021 Findings

思想:

  • 现有数据集局限
    • 明确提及的目标和可能暴露立场的表层词汇线索在数据中显式存在
    • 社交媒体的数据太短了,模型不需要理解就可以找出立场
  • 通过#的标签收集三个总统候选人的Tweet,收集了2.8 million条数据
    • 选取10-128长度的Tweet
    • 移除重复数据
    • 只保留英文数据
    • 减少到2 million,为PSTANCE-EXT数据
    • 每个人采样10000,共30000条数据构成最终的数据集
    • 人工标注,并去除I don’t know类别的数据
  • 构建一个#词典,删除文本后面的#,同时更改内部的#为中性的标记,防止暴露立场信息
  • 微调BERTweet预测CLS进行分类任务
  • 可以进行跨目标的立场检测、跨主题的立场检测(在2016年的数据上训练,预测2020年的立场)
  • 采用半监督方法(UST)提升跨主题的立场检测性能(没详细介绍)

数据集:SemEval-2016、Multi-Target stance datasets

2022

Zero-Shot Stance Detection via Contrastive Learning

时间:2022年4月

等级:WWW 2022(CCF A)

pPxUfET.md.png

思想:

  • 将数据分为两种类型:
    • target-invariant:即使目标或目标相关词被屏蔽,仍然可以识别上下文中表达的立场。
    • target-specific:如果目标和与目标相关的词语被屏蔽,则很难理解立场信息。
  • 训练一个普通的立场检测模型,训练到过拟合
  • 用主题模型找到与target最相关的词语,然后将其MASK掉,用上面的模型进行预测。如果预测对了就是target-invariant,错了就是target-specific,加一个标签给这个数据
  • 重新训练主模型
    • target-invariant与target-specific之间作对比学习
    • 不同的label之间做对比学习
  • 数据集: VAST、SEM-16、WT-WT

Infusing Knowledge from Wikipedia to Enhance Stance Detection

时间:2022年5月

等级:ACL 2022 Workshop(WASSA)

思想:从Wikipedia上预先查询到target的相关知识,融合到模型中进行立场检测

数据集:P-Stance、COVID-19-Stance、VAST

Few-Shot Stance Detection via Target-Aware Prompt Distillation

时间:2022年6月27日

等级:SIGIR 2022(CCF A)

pPvJ5xP.md.png

思想:

  • 动机:target通常是随时间变化的,对每一个target都获取充足的数据进行训练是很不现实的,立场检测方法需要获得few-shot的能力
  1. 多目标训练:训练一个模型,可以准确预测不同的target的label
  2. 设计三个Prompt,输入到Bert等模型的预训练任务中,让其预测label
  3. 预测的时候不映射到具体的label的词语,而是提前通过预训练模型获取label的表示向量,最终将target的向量与label的向量相乘计算损失
  4. teacher-student model融合三个prompt的结果,迭代进行预测,对比真实标签与预测标签之间的差距。

数据集:SemEval-2016、UKP

JointCL: A Joint Contrastive Learning Framework for Zero-Shot Stance Detection

时间:2022年5月

等级:ACL 2022(CCF A)

思想:

pPzo3hF.md.png

图相关

  • 一个没有出现过的target的信息是可以通过其他已知的target表示出来的(从target-aware的视角来看)
  • 提出了由立场对比学习与原型图网络对比学习。通过构建原形图,可以在未知target和已知target之间建立关系,从而用已学习到的信息表示未知target,从而提升对未知target的立场学习能力。

数据集:VAST、SEM-16、WT-WT

A Survey on Stance Detection for Mis- and Disinformation Identification

时间:2022年7月

等级:NAACL 2022 Findings(CCF B)

思想:虚假新闻的立场检测,一篇综述性质的文章

数据集:没有做实验,只是汇总之前人的数据、方法与结果

Enhancing Zero-Shot Stance Detection via Targeted Background Knowledge

时间:2022年7月

等级:SIGIR 2022(CCF A)

pPxUhUU.png

思想:

  • 用相关信息进行增强
    • 根据target在网络上爬取相关信息,找最相关的top k个主题
    • 用NLTK的工具提取关键词,找到爬取的信息中与关键词最相关的部分,作为额外知识
  • 其他的模型训练非常普通

数据集:VAST、SEM-16、WT-WT

Exploiting Sentiment and Common Sense for Zero-shot Stance Detection

时间:2022年10月

等级:COLING 2022

思想:

pPzolkT.md.png

图相关

  1. 使用图自动编码的模块将target的普遍信息融合进立场检测的模型
  2. 立场检测是被情感词汇影响的,使用Bert单独提取文档中的情感的词汇。

Generative Data Augmentation with Contrastive Learning for Zero-Shot Stance Detection

时间:2022年12月

等级:EMNLP 2022(CCF B)

思想:在看见过的target的基础之上生成没有看见过的target的数据

piU6WM6.md.png

  • 使用GAN网络进行对抗生成
  • 添加对比学习的策略
  • 在立场检测任务上进行微调

数据集:VAST、Sem-16

How would Stance Detection Techniques Evolve after the Launch of ChatGPT?

时间:2022年12月30日

等级:Arxiv

思想:

  • 加个Prompt的立场检测效果可以达到SOTA
  • 多轮对话理论上可以增强背景知识等
  • 没有和很多的SOTA进行比较,没啥说服力

数据集:P-Stance

2023

Hashtag-Guided Low-Resource Tweet Classification

时间:2023年2月20日

等级:WWW 2023(CCF A)

思想:

piSBw1U.png

  • Hash Tag是很重要的
  • Tweet注意力模块:获取Tweet之间的相关性从而借鉴已有的标签
  • 实体注意力模块:实体图获取Tweet中的实体
  • 融合两个模块生成HashTag
  • 通过原始的Tweet与HashTag一起输入到预训练模型中进行训练

数据集:

Few-shot Learning for Cross-Target Stance Detection by Aggregating Multimodal Embeddings

时间:2023年3月31日

等级:IEEE Transactions on Computational Social Systems(CCF C)

思想:

pipi3YF.png

  • 通过发Tweet的人之间的关系网络增强立场检测的效果
  • 包括Follower、Like和Friend的信息

数据集:P-Stance,额外找到了作者的关系信息

Investigating Chain-of-thought with ChatGPT for Stance Detection on Social Media

时间:2023年4月6日

等级:Arxiv

思想:通过思维链的方式,给一个例子帮助ChatGPT进行分析,在多个数据集上达到了SOTA(假)效果

pipF4D1.md.png

数据集:SEM-16、VAST、P-Stance

Claim Extraction and Dynamic Stance Detection in COVID-19 Tweets

时间:2023年4月

等级:WWW 2023 Companion

思想:

  • 是否存在主张:作者是否在Tweet中提出了客观事实的主张?并进一步分析是否值得检查。
    • 微调Bert系列的模型来完成
  • 主张提取:识别Tweet中的哪些部分对应于事实主张,哪些部分对应于作者的评论
    • 使用IOB2方式进行标注,也是微调Bert进行,尝试了多种模型结构
  • 动态立场检测:识别作者对事实主张的立场。不过主张是上面识别出来的,因此变化很大,基本上之前都没有见过
  • 数据集:自行收集的COVID-19的数据集

Can ChatGPT Reproduce Human-Generated Labels? A Study of Social Computing Tasks

时间:2023年4月22日

等级:无

思想:

  • 将一些NLP任务的数据集通过ChatGPT进行标注,标注后评估效果
  • 在立场检测的任务上面大概0.5-0.6左右

Examining Temporalities on Stance Detection Towards COVID-19 Vaccination

时间:2023年5月7日

等级:ICWSM Data Challenge

思想:

  • 划分数据集是以时间顺序进行划分的,更接近于真实的情况
  • 用单语言的Bert和多语言的Bert进行测试

数据集:COVID数据集

Robust Integration of Contextual Information for Cross-Target Stance Detection

(Contextual information integration for stance detection via cross-attention)

时间:2023年5月25日

等级:SEM2023(Co-located with ACL 2023)

pPxUqDx.png

思想:

  • 一个灵活的结合外部知识的方法
    • 一个Input+Target的Encoder和另外一个Context的Encoder,相当于Cross Attention
    • 直接连接Context与Text,相当于Self Attention
  • 尝试了多种获取外部知识的方法。例如ConceptNet、CauseNet、预训练模型等
  • 多个数据集测试效果

Guiding Computational Stance Detection with Expanded Stance Triangle Framework

时间:2023年5月31日

等级:ACL 2023

思想:

pipkSVP.md.png

  • 从语言学的角度考虑立场检测,使用很早就提出过的立场检测三角形
  • 语言学看不太懂,效果也没有很SOTA
  • 感觉就是方法比较新颖

数据集:SEM-16、P-Stance、VAST、Tweet-COVID

Knowledge-enhanced Prompt-tuning for Stance Detection

时间:2023年6月

等级:2023 ACM Transactions on Asian and Low-Resource Language Information Processing(SCI 4区 CCF C)

pPjWQbQ.md.png

思想:

  • 将立场检测的任务通过Bert中MASK的方式转换成一个MLM任务
  • 自动空间映射器:用SenticNet扩充词汇,自动选择相关的词语进行答案的映射(涉及一个树模型)
  • 背景知识
    1. 将target送入ConceptGraph中获得target的背景知识
    2. 使用neural topic model学习利用#符号的语义信息(涉及变分自编码器VAE)
  • 将上述的知识一起作为Prompt送入到预训练模型中进行微调,得到类别

数据集:SEM16、VAST、P-stance、自己的数据集(ISD)

Topic-Guided Sampling For Data-Efficient Multi-Domain Stance Detection

时间:2023年6月

等级:ACL 2023 Oral(CCF A)

思想:

pPz522d.png

  • 适用于跨主题(领域)的立场检测
  • 方法
    • 通过主题模型进行训练数据的采样
    • 将立场检测看成序列分类问题(d, t),加个Prompt
    • 对比学习计算损失

数据集:16个benchmark数据集

Voting Booklet Bias: Stance Detection in Swiss Federal Communication

时间:2023年6月15日

等级:Arxiv

思想:

  • 分析的目标是面向选民的小册子中的Topic的立场是否为中立的立场
  • 模型结构没有创新,评价了一些方法的性能
  • 这个任务与普通的立场检测任务不同

数据集:x-stance

C-STANCE: A Large Dataset for Chinese Zero-Shot Stance Detection

时间:2023年7月

等级:ACL 2023(CCF A)

思想:第一个中文的Zero-shot数据集

  • 微博的数据
  • 人工进行标注
  • 在多个立场检测的领域,使用多种方法进行评测

数据集:C-STANCE

A New Direction in Stance Detection: Target-Stance Extraction in the Wild

时间:2023年7月

等级:ACL 2023(CCF A)

思想:

piUJzO1.png

  • 在立场检测中,target可能是隐含在text中的,大规模标注target不太现实
  • 从文本中获取target-stance的对
  • Target Identification:
    • 训练一个分类器对target进行分类
    • 用BART对target进行生成,然后map到已知的target上面
  • Stance Detection
    • 建立一个分类器,并用target预测作为辅助任务

数据集:SemEval-2016、AM、COVID-19、P-Stance、自己构建的zero-shot数据集

Distilling Calibrated Knowledge for Stance Detection

时间:2023年7月

等级:ACL 2023 Findings(CCF A)

思想:与知识蒸馏等相关

数据集:AM、COVID-19、P-Stance

Target-Oriented Relation Alignment for Cross-Lingual Stance Detection

时间:2023年7月

等级:ACL 2023 Findings(CCF A)

思想:将单语言的立场检测迁移到多语言上

piUcLtJ.md.png

  • 使用mBert获取文本的表示
  • 构建target的关系图

也是图相关的工作

数据集:X-Stance-all

Exploration of Contrastive Learning Strategies toward more Robust Stance Detection

时间:2023年7月

等级:ACL 2023 Workshop(WASSA)

思想:使用对比学习增强立场检测系统的鲁棒性

  • 词表相似的句子也能通过对比学习获取语义从而发现他们之间的区别
  • 选择anchor的策略可以有多种方法
  • 使用拼写错误、同义重复和同义词替换三种策略来对数据集进行增强
  • 不同的构造方法(数据集中的所有topic或者一部分的topic)进行不同的数据集下的训练,仅考虑二分类

数据集:DebateForum (DF), SemEval2016 (SE) ,ARC, Perspectrum, FNC-1, KSD-Biden, KSD-Trump

Zero-Shot and Few-Shot Stance Detection on Varied Topics via Conditional Generation

时间:2023年7月

等级:ACL 2023(CCF A)

思想:

  • 用生成的思想做立场检测的问题,使用BART作为基础架构
  • 使用联合训练,不仅生成标签,同时生成target
  • Unlikelihood Training 数据增强方法提升性能
  • 结合Wiki的知识(其他论文的工作)

数据集:VAST

Ladder-of-Thought: Using Knowledge as Steps to Elevate Stance Detection

时间:2023年8月31日

等级:Arxiv

pPvJTr8.md.png

思想:

  • CoT利用的是大模型内部的知识,但是立场检测相关的知识大模型可能没有见过
  • 首先在Google上面搜到target的相关信息
  • 用Text,Target和上面搜集到的信息微调一个预训练模型,让其产生更好的外部信息 Generation Finetuning
  • 在上面的模型基础上,将text,target,和上面产生的外部信息连接在一起输入到模型中,以预测label为目标进行微调

数据集:VAST

Use of Large Language Models for Stance Classification

时间:2023年9月24日

等级:ICWSM 2024 (CCF B)

思想:

  • 尝试了四种Prompt,用上下文包裹text和target,加一些few shot的例子,最后让其解释原因
  • 尝试了几种开源的大模型
  • 总结:大模型不太行,不如微调过的小模型,甚至不如zero-shot

数据集:covid-lies、election2016、phemerumors、semeval2016、wtwt

STANCE-C3: Domain-adaptive Cross-target Stance Detection via Contrastive Learning and Counterfactual Generation

时间:2023年9月26日

等级:无

思想:

pPzQSNn.md.png

  • 跨领域的知识迁移
  • 反事实数据增强

StanceReCL: Zero-Shot Stance Detection Based on Relevance and Contrastive Learning

时间:2023年10月

等级:投稿 EMNLP 2023 没中

思想:

pPvYprT.md.png

  • 提出了几个概念:stance indicator(support,against和neutral),stance pattern(由stance indicator和target组成)
  • 两种表达:句子层面的表达([CLS]对应的最后一层的表示)与词语层面的表达(单个token的最后一层的隐藏状态)
  • 相关性的计算:
    • 上面的CLS与下面的CLS计算句子层面的相关性
    • 上面的stance indicator与下面的sentence中的最相关的词语计算相关性
  • 句子层面计算对比学习的损失,然后与词语层面的损失加权重融合计算

数据集:SEM-16、VAST、WT-WT

Stance Detection with Collaborative Role-Infused LLM-Based Agents

时间:2023年10月

等级:Arxiv

思想:多个LLM的Agent一起分析文本的各个方面,最后一正一反对立场进行推断,完全的Zero-shot

piUkrSe.png

数据集:Sem-16、WT-WT、VAST

TATA: Stance Detection via Topic-Agnostic and Topic-Aware Embeddings

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:

piUk7Os.png

  • topic-aware/TAW embeddings and generalized topic-agnostic/TAG stance embeddings
  • 使用T5-Flan作为基座模型
  • 收集了一个新的数据集,包括相关的passage对与相关的topic对,Topic-Aware/TAW Dataset
    • 使用T5对topic进行预测从而做预训练任务
    • 使用MPNet LLM 识别其他数据集中相同的topic
  • 用TAW Dataset对VAST数据集进行扩充
  • Topic-Aware/TAW Embedding Layer:对整个的text-topic对进行训练
  • Topic-Agnostic/TAG Embedding Layer:topic看不到
  • 后面加两个注意力层

数据集:VAST

Support or Refute: Analyzing the Stance of Evidence to Detect Out-of-Context Mis- and Disinformation

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:多模态的信息不匹配会造成误解

pFuEtgA.md.png

分别训练图片的立场检测分类器、文本的立场检测分类器,外加一些实体的知识进行识别

数据集:NewsCLIPpings

Why Should This Article Be Deleted? Transparent Stance Detection in Multilingual Wikipedia Editor Discussions

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:在文本审核中加入立场检测从而进行自动判断其是否应该被删除

pFlQIR1.png

数据集:提出了多语言的Wiki的审核数据集

ORCHID: A Chinese Debate Corpus for Target-Independent Stance Detection and Argumentative Dialogue Summarization

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:

  • 提出中文的辩论的立场检测数据集,且是与目标无关的
  • 提出立场相关的摘要任务,可以提升摘要的效果

数据集:

Cross-Lingual Cross-Target Stance Detection with Dual Knowledge Distillation Framework

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:

pFlQoxx.png

pFlQ7M6.png

  • 提出了新的跨语言cross target的立场检测任务
  • 一个跨语言的老师,一个跨target的老师
  • 大量的目标语言的无标签数据如何利用
  • 使用mBert作为跨语言的teacher,翻译prompt和label构建文本对,使得两个文本对的预测结果更为接近
  • 使用上面的跨语言的teacher作为跨target的encoder
  • 使用GAT等将target分类,然后做与类别相关的对比学习
  • 用无标签的目标语言数据+两个teacher的伪标签训练

数据集:X-Stance、Semeval-2016、R-ita、Czech

Identification of Multimodal Stance Towards Frames of Communication

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:文字和图片的多模态立场检测,主要的贡献是数据集

  • 在疫苗场景下
  • 收集了关于疫苗或者新冠的Twitter的数据集,包括文字与图片数据
  • 选择了一些多模态的模型作为baseline,通过OCR等方式提取图片中的文字
  • 分一些图片与文字不吻合的情况

数据集:MMVAX-STANCE

From Values to Opinions: Predicting Human Behaviors and Stances Using Value-Injected Large Language Models

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:与价值观相关,不算立场检测任务

  • 使用Argument Generation和Question Answering两种方法对LLM进行微调

数据集:非立场检测

Stance Detection on Social Media with Background Knowledge

时间:2023年12月

等级:EMNLP 2023(CCF B)

思想:补充两种知识增强立场检测的效果

pFlQbqO.png

  • Episodic knowledge:情景知识,只能从背景知识中推断出来
  • discourse knowledge:口语知识,代号、hashtag等
  • 在网络上搜索最相关的top10的wiki知识
  • 通过主题模型和关键词检索最相关的部分、使用大模型进行过滤
  • 使用大模型对口语知识进行扩充
  • 既微调了小模型,也在大模型上面做zero-shot看效果

数据集:Sem-16、P-Stance、VAST

EZ-STANCE: A Large Dataset for Zero-Shot Stance Detection

时间:2023年12月

等级:EMNLP 2023 Findings

思想:提出了与VAST对标的EZ-Stance数据集

数据集:EZ-Stance

Multi-label and Multi-target Sampling of Machine Annotation for Computational Stance Detection

时间:2023年12月

等级:EMNLP 2023 Findings

思想:思维链等zero-shot来增强直接使用大模型进行立场检测的效果

pFlQvid.png

数据集:

Chain-of-Thought Embeddings for Stance Detection on Social Media

时间:2023年12月

等级:EMNLP 2023 Findings

思想:用大模型对立场进行预测,然后输入到Roberta中进行再次预测

piUklJU.md.png

数据集:Tweet-Stance、P-Stance

Toxicity, Morality, and Speech Act Guided Stance Detection

时间:2023年12月

等级:EMNLP 2023 Findings

思想:关注一些情绪倾向

pFllSzt.png

数据集:SemEval、P-Stance、Climate、COVID

Multilingual Coarse Political Stance Classification of Media. The Editorial Line of a ChatGPT and Bard Newspaper

时间:2023年12月

等级:EMNLP 2023 Findings

思想:使用大模型对人工编写的新闻的倾向进行判断,不算立场检测

数据集:与立场检测无关

]]>
+ + + + + Study + + + + + + + Python + + Stance Detection + + + +
+ + + + + Pytorch分布式训练 + + /2023/08/12/Pytorch-distributed/ + + Pytorch分布式训练学习整理

参考资料

源码解析:PyTorch 源码解读之 DP & DDP:模型并行和分布式训练解析

简单小模型示例:pytorch中分布式训练DDP教程(新手快速入门!)

Pytorch - 弹性训练极简实现(附源码)

系列文章:【分布式训练】单机多卡的正确打开方式(一):理论基础

【分布式训练】基于PyTorch进行多GPU分布式模型训练(补充)

较新较详细的教程:torch分布式训练

博客:pytorch弹性分布式训练

模型并行(流水线)

把模型隔成不同的层,每一层都放到一块GPU上

(1)GPU利用度不够。

如图,阴影部分所表示的时间段里,总有GPU在空转。GPU的数量越多时,空置的比例接近1

(2)中间结果占据大量内存

在做backward计算梯度的过程中,我们需要用到每一层的中间结果z。假设我们的模型有L层,每一层的宽度为d,则对于每块GPU,不考虑其参数本身的存储,额外的空间复杂度为 。从这个复杂度可以看出,随着模型的增大,N,L,d三者的增加可能会平滑掉K增加带来的GPU内存收益。因此,这也是需要优化的地方。

Gpipe

流水线并行的核心思想是: 在模型并行的基础上,进一步引入数据并行的办法,即把原先的数据再划分成若干个batch,送入GPU进行训练 。未划分前的数据,叫 mini-batch 。在mini-batch上再划分的数据,叫 micro-batch

其中,第一个下标表示GPU编号,第二个下标表示micro-batch编号。假设我们将mini-batch划分为M个,则流水线并行下,bubble的时间复杂度为: 。Gpipe通过实验证明,当时,bubble产生的空转时间占比对最终训练时长影响是微小的,可以忽略不计。

将batch切好,并逐一送入GPU的过程,就像一个流水生产线一样(类似于CPU里的流水线),因此也被称为Pipeline Parallelism。

Gpipe采用用时间换空间的方法,几乎不存中间结果,等到backward的时候,再重新算一遍forward

每块GPU上只保存来自上一块的最后一层输入z,其余的中间结果我们算完就废。等到backward的时候再由保存下来的z重新进行forward来算出。

空间复杂度为

在实际应用中,流水线并行并不特别流行,主要原因是模型能否均匀切割,影响了整体计算效率,这就需要算法工程师做手调。

数据并行

数据并行的核心思想是: 在各个GPU上都拷贝一份完整模型,各自吃一份数据,算一份梯度,最后对梯度进行累加来更新整体模型 。理念不复杂,但到了大模型场景, 巨大的存储和GPU间的通讯量, 就是系统设计要考虑的重点了。在本文中,我们将递进介绍三种主流数据并行的实现方式:

  • DP(Data Parallelism) :最早的数据并行模式,一般采用参数服务器(Parameters Server)这一编程框架。实际中多用于单机多卡
  • DDP(Distributed Data Parallelism) :分布式数据并行,采用Ring AllReduce的通讯方式,实际中多用于多机场景
  • ZeRO: 零冗余优化器。由微软推出并应用于其DeepSpeed框架中。严格来讲ZeRO采用数据并行+张量并行的方式,旨在降低存储。

DP

一个经典数据并行的过程如下:

  • 若干块 计算GPU ,如图中GPU0~GPU2;1块 梯度收集GPU ,如图中AllReduce操作所在GPU。
  • 在每块计算GPU上都拷贝一份完整的模型参数。
  • 把一份数据X(例如一个batch)均匀分给不同的计算GPU。
  • 每块计算GPU做一轮FWD和BWD后,算得一份梯度G。
  • 每块计算GPU将自己的梯度push给梯度收集GPU,做聚合操作。这里的聚合操作一般指 梯度累加 。当然也支持用户自定义。
  • 梯度收集GPU聚合完毕后,计算GPU从它那pull下完整的梯度结果,用于更新模型参数W。更新完毕后,计算GPU上的模型参数依然保持一致。
  • 聚合再下发梯度的操作,称为AllReduce

流程

../_images/ps.svg

DP 基于单机多卡,所有设备都负责计算和训练网络,除此之外, device[0] (并非 GPU 真实标号而是输入参数 device_ids 首位) 还要负责整合梯度,更新参数。从图中我们可以看出,有三个主要过程:

  • 过程一(图中红色部分):各卡分别计算损失和梯度
  • 过程二(图中蓝色部分):所有梯度整合到 device[0]
  • 过程三(图中绿色部分):device[0] 进行参数更新,其他卡拉取 device[0] 的参数进行更新

所有卡都并行运算(图中红色),将梯度收集到 device[0](图中浅蓝色)和 device[0] 分享模型参数给其他 GPU(图中绿色)三个主要过程。

更详细的流程如下图所示:

分析

  • 存储开销大 。每块GPU上都存了一份完整的模型,造成冗余。
  • 通讯开销大 。Server需要和每一个Worker进行梯度传输。当Server和Worker不在一台机器上时,Server的带宽将会成为整个系统的计算效率瓶颈。

梯度异步更新:Worker并不会实际等到把聚合梯度拿回来,更新完参数W后再做计算。而是直接拿旧的W,吃新的数据,继续第11轮的计算。这样就保证在通讯的时间里,Worker也在马不停蹄做计算,提升计算通讯比。

但是模型收敛的速度不会变快,只是多用了一些数据

受通讯负载不均的影响, DP一般用于单机多卡场景

DDP

DDP作为一种更通用的解决方案出现了,既能多机,也能单机。DDP首先要解决的就是通讯问题:将Server上的通讯压力均衡转到各个Worker上。实现这一点后,可以进一步去Server,留Worker。

简介

随着大模型的出现,简单的数据并行已经无法满足需求,毕竟一个模型的大小就有可能超过显卡的显存,更不可能将其复制多份。因此需要让每一张卡仅负责模型的一部分计算,承载模型的一小部分。

使用DDP进行分布式训练有以下几个优势:

  1. 加速训练:通过数据并行,DDP能够在多个设备或节点上同时处理不同批次的数据,从而加快训练速度。
  2. 内存效率:DDP在每个设备上只保存模型的局部副本和相应的梯度,而不是整个模型的副本,这样可以节省内存。
  3. 不需要额外的代码:在PyTorch中,使用DDP进行分布式训练几乎不需要修改您的原始模型和训练代码。

流程:Ring All Reduce

Scatter Reduce过程:首先将参数分为份,相邻的GPU传递不同的参数,在传递次之后,可以得到每一份参数的累积(在不同的GPU上)。

动图

All Gather:得到每一份参数的累积之后,再做一次传递,同步到所有的GPU上。

动图

假设有个GPU, 传输总量是,每一次的通信上限是,则完成一次通信需要时间,那么总共需要花费时间,可以看到通信成本与GPU数量无关。

DP和DDP的总通讯量相同,但因负载不均的原因,DP需要耗费更多的时间搬运数据,但是DP不一定就比DDP差

代码

分析

DDP采用多进程控制多GPU,共同训练模型,一份代码会被pytorch自动分配到n个进程并在n个GPU上运行。 DDP运用Ring-Reduce通信算法在每个GPU间对梯度进行通讯,交换彼此的梯度,从而获得所有GPU的梯度。对比DP,不需要在进行模型本体的通信,因此可以加速训练。

需要注意以下几点:

  1. 设置DistributedSampler来打乱数据,因为一个batch被分配到了好几个进程中,要确保不同的GPU拿到的不是同一份数据。
  2. 要告诉每个进程自己的id,即使用哪一块GPU。
  3. 如果需要做BatchNormalization,需要对数据进行同步。

Torchrun使用及参数详解

核心概念
  • rank:进程号,在多进程上下文中,我们通常假定rank 0是第一个进程或者主进程,其它进程分别具有1,2,3不同rank号,这样总共具有4个进程。
  • node:物理节点,可以是一个容器也可以是一台机器,节点内部可以有多个GPU;nnodes指物理节点数量, nproc_per_node指每个物理节点上面进程的数量
  • local_rank:指在一个node上进程的相对序号,local_rank在node之间相互独立
  • WORLD_SIZE:全局进程总个数,即在一个分布式任务中rank的数量
  • Group:进程组,一个分布式任务对应了一个进程组。只有用户需要创立多个进程组时才会用到group来管理,默认情况下只有一个group
  • backend:通信后端,可选的包括:nccl(NVIDIA推出)、gloo(Facebook推出)、mpi(OpenMPI)。一般建议GPU训练选择nccl,CPU训练选择gloo
  • master_addr与master_port:主节点的地址以及端口,供init_method 的tcp方式使用。 因为pytorch中网络通信建立是从机去连接主机,运行ddp只需要指定主节点的IP与端口,其它节点的IP不需要填写。

如下图所示,共有3个节点(机器),每个节点上有4个GPU,每台机器上起4个进程,每个进程占一块GPU,那么图中一共有12个rank,nproc_per_node=4,nnodes=3,每个节点都有一个对应的node_rank

在这里插入图片描述

rank与GPU之间没有必然的对应关系,一个rank可以包含多个GPU;一个GPU也可以为多个rank服务(多进程共享GPU),在torch的分布式训练中习惯默认一个rank对应着一个GPU,因此local_rank可以当作GPU号

简介

torchrun相当于原来的torch.distributed.launch,有一些额外增加的功能:

  • 通过重启优雅处理某一个worker运行过程中的错误
  • worker的RANK和WORLD_SIZE都是被自动分配的
  • Node的数量允许从最小值到最大值中间弹性伸缩

torchrun命令与 python -m torch.distributed.run命令完全等同,为命令行命令

从旧版本迁移 --use_env

有一个参数 --use_env在目前版本的torchrun中是不存在的,因此需要做一点处理

  1. 将原始指定的–local-rank参数修改为从环境变量中读取
  2. 命令行不需要再次指定 --use_env参数

旧版本代码:

$ python -m torch.distributed.launch --use-env train_script.pyimport argparseparser = argparse.ArgumentParser()parser.add_argument("--local-rank", type=int)args = parser.parse_args()local_rank = args.local_rank

新版本代码:

$ torchrun train_script.pyimport oslocal_rank = int(os.environ["LOCAL_RANK"])
命令行参数
参数名称含义示例
–nnodes节点数量,一个节点对应一个主机1或MIN_SIZE:MAX_SIZE(弹性训练)
–nproc-per-node一个节点中的进程数量,一般一个进程使用一个显卡,故也通常表述为一个节点中显卡的数量[auto, cpu, gpu, int]
–rdzv-backendrendezvous 后端c10d etcd
–rdzv-endpointrendezvous 后端地址<host>:<port>
–rdzv-id用户可以指定当前rendezvous的id,所有的node都要使用这同一个id
–rdzv-conf希望传入rendezvous的其他参数<key1>=<value1>
–standalone单节点多卡的默认配置,不需要再传入上述的rendezvous参数,默认为C10d TCP 29400(–master-addr等也会失效)选项
–max-restartsworker group重启的最大次数
–monitor-interval检测worker状态的时间间隔(以秒为单位)
–start-method创建子进程的方式{spawn,fork,forkserver}
–roleUser-defined role for the workers.
-m与python -m相同,将模块当作脚本运行选项
–no-python不使用python命令而是直接执行(如果这个文件并不是一个py文件会使用这个)
–run-path使用runpy.run_path执行文件
–log-dir日志文件存放目录
–redirects将控制台输出的日志信息重定向到日志目录中的文件[-r 3] 将所有worker的标准输出和标准错误进行重定向,[-r 0:1,1:2] 将rank 0的标准输出重定向,将rank 1的标准错误重定向
–tee除将日志输出到控制台外也输出到日志文件日志文件流
–node-rank多节点分布式训练的时候该节点的Rank
–master-addrmaster 节点的 IP 地址,也就是 rank=0 对应的主机地址
–master-portmaster 节点的端口号,用于通信
–local-addr本地节点的IP地址

torchrun主要是对多节点作了分布式的优化,从而可以满足容错性和弹性伸缩。如果是单节点就不需要很复杂。

环境变量
名称含义示例
LOCAL_RANKGPU在单节点中的序号01
RANKGPU在全部节点的序号01
GROUP_RANKworker组的rank00
ROLE_RANK相同ROLE的worker的rank01
LOCAL_WORLD_SIZE与–nproc-per-node相同22
WORLD_SIZEjob中worker的总数22
ROLE_WORLD_SIZE相同角色的worker的数量12
MASTER_ADDRrank为0的worker的地址127.0.0.1127.0.0.1
MASTER_PORTrank为0的worker的端口2950029500
TORCHELASTIC_RESTART_COUNT最近重启的worker组的数量00
TORCHELASTIC_MAX_RESTARTS配置的最大重启次数00
TORCHELASTIC_RUN_ID与–rdzv-id相同nonenone
PYTHON_EXEC执行这个脚本的python的位置没有没有

代码示例

import torchimport torch.nn as nnimport torch.nn.functional as Ffrom torch.utils.data import Dataset, DataLoaderfrom torch.utils.data.distributed import DistributedSamplerfrom torch.nn.parallel import DistributedDataParallelfrom torch.distributed import init_process_group, destroy_process_groupimport osimport timeclass ToyModel(nn.Module):    def __init__(self):        super(ToyModel, self).__init__()        self.net1 = nn.Linear(10, 10)        self.relu = nn.ReLU()        self.net2 = nn.Linear(10, 5)    def forward(self, x):        return self.net2(self.relu(self.net1(x)))class MyTrainDataset(Dataset):    def __init__(self, size):        self.size = size        self.data = [(torch.rand(10), 0) for _ in range(size)]    def __len__(self):        return self.size      def __getitem__(self, index):        return self.data[index]class Trainer:    def __init__(        self,        model: torch.nn.Module,        train_data: DataLoader,        optimizer: torch.optim.Optimizer,        save_every: int,        snapshot_path: str,    ) -> None:        self.gpu_id = int(os.environ["LOCAL_RANK"])        self.model = model.to(self.gpu_id)        self.train_data = train_data        self.optimizer = optimizer        self.save_every = save_every        self.epochs_run = 0        self.snapshot_path = snapshot_path        if os.path.exists(snapshot_path):            print("Loading snapshot")            self._load_snapshot(snapshot_path)        self.model = DistributedDataParallel(self.model, device_ids=[self.gpu_id])    def _load_snapshot(self, snapshot_path):        loc = f"cuda:{self.gpu_id}"        snapshot = torch.load(snapshot_path, map_location=loc)        self.model.load_state_dict(snapshot["MODEL_STATE"])        self.epochs_run = snapshot["EPOCHS_RUN"]        print(f"Resuming training from snapshot at Epoch {self.epochs_run}")    def _run_batch(self, source, targets):        self.optimizer.zero_grad()        output = self.model(source)        # print(output,targets)        loss = F.cross_entropy(output, targets)        print(f"[GPU{self.gpu_id}] Loss {loss.item()}")        loss.backward()        self.optimizer.step()    def _run_epoch(self, epoch):        b_sz = len(next(iter(self.train_data))[0])        print(f"[GPU{self.gpu_id}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")        self.train_data.sampler.set_epoch(epoch)        for source, targets in self.train_data:            source = source.to(self.gpu_id)            targets = targets.to(self.gpu_id)            self._run_batch(source, targets)    def _save_snapshot(self, epoch):        snapshot = {            "MODEL_STATE": self.model.module.state_dict(),            "EPOCHS_RUN": epoch,        }        torch.save(snapshot, self.snapshot_path)        print(f"Epoch {epoch} | Training snapshot saved at {self.snapshot_path}")    def train(self, max_epochs: int):        for epoch in range(self.epochs_run, max_epochs):            self._run_epoch(epoch)            if self.gpu_id == 0 and epoch % self.save_every == 0:                self._save_snapshot(epoch)            time.sleep(1)def ddp_setup():    init_process_group(backend="nccl")    print("Parameters")    print(f"LOCAL_RANK:{os.environ['LOCAL_RANK']}")    print(f"RANK:{os.environ['RANK']}")    print(f"GROUP_RANK:{os.environ['GROUP_RANK']}")    print(f"ROLE_RANK:{os.environ['ROLE_RANK']}")    print(f"LOCAL_WORLD_SIZE:{os.environ['LOCAL_WORLD_SIZE']}")    print(f"WORLD_SIZE:{os.environ['WORLD_SIZE']}")    print(f"ROLE_WORLD_SIZE:{os.environ['ROLE_WORLD_SIZE']}")    print(f"MASTER_ADDR:{os.environ['MASTER_ADDR']}")    print(f"MASTER_PORT:{os.environ['MASTER_PORT']}")    print("")    torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))def load_train_objs():    train_set = MyTrainDataset(2048)  # load your dataset    model = ToyModel()    optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)    return train_set, model, optimizerdef prepare_dataloader(dataset: Dataset, batch_size: int):    return DataLoader(        dataset,        batch_size=batch_size,        pin_memory=True,        shuffle=False,        sampler=DistributedSampler(dataset)    )def main(save_every: int, total_epochs: int, batch_size: int, snapshot_path: str = "snapshot.pt"):    ddp_setup()    dataset, model, optimizer = load_train_objs()    train_data = prepare_dataloader(dataset, batch_size)    trainer = Trainer(model, train_data, optimizer, save_every, snapshot_path)    trainer.train(total_epochs)    destroy_process_group()if __name__ == "__main__":    import argparse    parser = argparse.ArgumentParser(description='simple distributed training job')    parser.add_argument('--total_epochs', default=10, type=int, help='Total epochs to train the model')    parser.add_argument('--save_every', default=2, type=int, help='How often to save a snapshot')    parser.add_argument('--batch_size', default=512, type=int, help='Input batch size on each device (default: 32)')    args = parser.parse_args()      main(args.save_every, args.total_epochs, args.batch_size)

与单卡有几点不同:

  1. 初始化进程组:init_process_group(backend="nccl"),后端一般选择nccl
  2. 分布式数据采样器:sampler=DistributedSampler(dataset)
  3. 封装模型:self.model = DistributedDataParallel(self.model, device_ids=[self.gpu_id])
  4. 启动torchrun脚本进行训练

训练脚本:

  1. 单机多卡
torchrun \    --nnodes=1 \    --nproc_per_node=2 \--master-addr=127.0.0.1 \--master-port=29500 \main.py
  1. 多机多卡
export NCCL_DEBUG=infoexport NCCL_SOCKET_IFNAME=bond0export NCCL_IB_DISABLE=1torchrun \    --nnodes=2 \    --nproc_per_node=2 \--master-addr=10.208.58.27 \--master-port=29602 \--node-rank=0 \main.py
export NCCL_DEBUG=infoexport NCCL_SOCKET_IFNAME=bond0export NCCL_IB_DISABLE=1torchrun \    --nnodes=2 \    --nproc_per_node=1 \--master-addr=10.208.58.27 \--master-port=29602 \--node-rank=1 \main.py

注意事项:

  1. 多进程训练,也就是会同时运行多份代码,因此训练时候要想好GPU的序号等需要自己指定的变量
  2. 数据是按照进程数量分的,比如总共2048条,如果三个进程就每一个进程683

测试环境:

master:10.208.58.27 2*V100

ParametersLOCAL_RANK:0RANK:0GROUP_RANK:0ROLE_RANK:0LOCAL_WORLD_SIZE:2WORLD_SIZE:3ROLE_WORLD_SIZE:3MASTER_ADDR:10.208.58.27MASTER_PORT:29602ParametersLOCAL_RANK:1RANK:1GROUP_RANK:0ROLE_RANK:1LOCAL_WORLD_SIZE:2WORLD_SIZE:3ROLE_WORLD_SIZE:3MASTER_ADDR:10.208.58.27MASTER_PORT:29602

worker:1*A100

ParametersLOCAL_RANK:0RANK:2GROUP_RANK:1ROLE_RANK:2LOCAL_WORLD_SIZE:1WORLD_SIZE:3ROLE_WORLD_SIZE:3MASTER_ADDR:10.208.58.27MASTER_PORT:29602

ZeRO(零冗余优化)

数据并行中,每个GPU上都复制了一份完整模型,当模型变大时,很容易打爆GPU的显存

存储消耗

存储主要分为两大块:Model States和Residual States

Model States指和模型本身息息相关的,必须存储的内容,具体包括:

  • optimizer states :Adam优化算法中的momentum和variance
  • gradients :模型梯度
  • parameters :模型参数W

Residual States指并非模型必须的,但在训练过程中会额外产生的内容,具体包括:

  • activation :激活值。在流水线并行中我们曾详细介绍过。在backward过程中使用链式法则计算梯度时会用到。有了它算梯度会更快,但它不是必须存储的,因为可以通过重新做Forward来算它。
  • temporary buffers: 临时存储。例如把梯度发送到某块GPU上做加总聚合时产生的存储。
  • unusable fragment memory :碎片化的存储空间。虽然总存储空间是够的,但是如果取不到连续的存储空间,相关的请求也会被fail掉。对这类空间浪费可以通过内存整理来解决。

精度混合训练

  • 存储一份fp32的parameter,momentum和variance(统称model states)
  • 在forward开始之前,额外开辟一块存储空间,将fp32 parameter减半到fp16 parameter。
  • 正常做forward和backward,在此之间产生的activation和gradients,都用fp16进行存储。
  • 用fp16 gradients去更新fp32下的model states。
  • 当模型收敛后,fp32的parameter就是最终的参数输出。

存储大小

img

其中很大的momentum和variance是Adam保存的,首先就优化他们

ZeRO-DP

优化状态分割

将optimizer state分成若干份,每块GPU上各自维护一份。这样就减少了相当一部分的显存开销。

得到G是与DP一样的通信,然后还要聚合W

显存和通讯量的情况如下:

img

优化状态与梯度分割

把梯度也拆开,每个GPU格子维护一块梯度。

img

此时,数据并行的整体流程如下:

对梯度做一次 Reduce-Scatter ,保证每个GPU上所维持的那块梯度是聚合梯度。例如对GPU1,它负责维护G1,因此其他的GPU只需要把G1对应位置的梯度发给GPU1做加总就可。

每块GPU用自己对应的O和G去更新相应的W。更新完毕后, 每块GPU维持了一块更新完毕的W 。同理,对W做一次 All-Gather ,将别的GPU算好的W同步到自己这来。单卡通讯量 Φ

img

优化状态、梯度与参数分割

每块GPU只维持对应的optimizer states,gradients和parameters

  • 做forward时,对W做一次 All-Gather ,取回分布在别的GPU上的W,得到一份完整的W,立刻把不是自己维护的W抛弃。
  • 做backward时,对W做一次 All-Gather ,取回完整的W,**backward做完,立刻把不是自己维护的W抛弃。
  • 做完backward,算得一份完整的梯度G,对G做一次 Reduce-Scatter ,从别的GPU上聚合自己维护的那部分梯度,聚合操作结束后,立刻把不是自己维护的G抛弃

到这一步, 我们用1.5倍的通讯开销,换回近120倍的显存 。只要梯度计算和异步更新做的好,通讯时间大部分可以被计算时间隐藏,因此这样的额外通讯开销,也是划算的。

ZeRO VS 模型并行

ZeRO是模型并行的形式,数据并行的实质

模型并行,是指在forward和backward的过程中,我只需要用自己维护的那块W来计算就行。即 同样的输入X,每块GPU上各算模型的一部分,最后通过某些方式聚合结果

对ZeRO来说,它做forward和backward的时候,是需要把各GPU上维护的W聚合起来的,即本质上还是用完整的W进行计算。 它是不同的输入X,完整的参数W,最终再做聚合

ZeRO-Offload与ZeRO-Infinity

核心思想是: 显存不够,内存来凑

把要存储的大头卸载(offload)到CPU上,而把计算部分放到GPU上

ZeRO-Offload的做法是:

  • forward和backward计算量高 ,因此和它们相关的部分,例如参数W(fp16),activation,就全放入GPU。
  • update的部分计算量低 ,因此和它相关的部分,全部放入CPU中。例如W(fp32),optimizer states(fp32)和gradients(fp16)等。

具体切分如下图:

ZeRO-infinity也是同理,它们在解决的事情都是:找个除GPU之外的地方,存数据。感兴趣的朋友可以深入研究,这里就不展开了。

张量模型并行

把模型的参数纵向切开,放到不同的GPU上进行独立计算,然后再做聚合。

假设现在W太大,导致单卡装不下。我们需要把W切开放到不同的卡上,则我们面临三个主要问题:

  • 怎么切分W。
  • 切完W后,怎么做forward。
  • 做完forward后,怎么做backward,进而求出梯度,更新权重。

按行切分权重

forward

我们用N来表示GPU的数量。有几块GPU,就把W按行维度切成几份。下图展示了N=2时的切割方式:

W按照行维度切开后,X的维度和它不对齐了,这可怎么做矩阵乘法呢?很简单,再把X“按列切开”就行了,如下图所示:

backward

做完forward,取得预测值Y,进而可计算出损失L,接下来就能做backward了。我们重画一下forward的过程,并在其中加入backward的部分,整体流程图如下:

img

按列切分权重

forward

按列切分权重后,forward计算图如下:

backward

img

具体模型拆分方式:https://zhuanlan.zhihu.com/p/622212228

在实际应用中,对Transformer类的模型,采用最经典方法是张量模型并行 + 数据并行,并在数据并行中引入ZeRO做显存优化。具体的架构如下:

其中,node表示一台机器, 一般我们在同一台机器的GPU间做张量模型并行。在不同的机器上做数据并行 。图中颜色相同的部分,为一个数据并行组。凭直觉,我们可以知道这么设计大概率和两种并行方式的通讯量有关。具体来说, 它与TP和DP模式下每一层的通讯量有关,也与TP和DP的backward计算方式有关

]]>
+ + + + + Study + + + + + + + Python + + Pytorch + + + +
+ + + + + 杂谈-20230502 + + /2023/05/02/diary/diary20230502/ + + 2023年5月2日,周二

干什么事情都没有动力,学习也不知道学什么,玩也不知道去哪,打球也略显尴尬,聊天也不知道找谁,刷剧也没有看下去的动力。

不管了,好久没有刷剧了,先刷一刷比较火的悬疑剧吧

]]>
+ + + + + Life + + + + + + + Diary + + + +
+ + + + + 字节跳动青训营-抖音项目 + + /2023/03/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project/ + + 字节跳动青训营-抖音项目

一、项目介绍

“NoBugVideo”基于Gin Web框架,采用微服务架构,使用Kong集成Consul做服务发现,实现了“抖音”的基本功能,包括视频流的推送、视频投稿、用户的注册与登录,以及社交(用户之间的关注与聊天)与互动(用户对视频点赞及评论)等功能。

项目服务地址:http://124.221.120.88:8000

Github地址:https://github.com/xu-jq/simple-DY

我们团队实现了包括基础功能在内的两大方向:互动方向社交方向 ,根据项目考核的4个标准,自评如下:

评价项实现情况
功能实现微服务与其他资源能够正常运行,完全实现文档中定义的全部接口,边界情况处理良好
代码质量项目结构清晰,包划分合理,代码符合编码规范
服务性能数据表设置了合理的索引,代码中尽量使用并行处理提高性能
安全可靠通过GORM框架防止SQL注入,通过JWT进行用户的认证,防止越权

二、项目分工

团队成员主要贡献
@汪辉开发社交模块,搭建Kong集成Consul做服务发现
@许珺琪开发用户互动相关模块包括点赞评论等相关接口、搭建redis服务
@张兆开发用户模块与视频模块相关接口、搭建MySQL、RabbitMQ等服务

三、项目实现

3.1 技术选型与相关开发文档

抖音上线于2016年9月26日,一开始是定位于专注于新生代的音乐创意短视频App,视频时常限制在15s内。年轻人比较爱赶新潮,乐于尝试新鲜事物,通过清晰明确定位在“潮流”“炫酷”“技术流”的方式,抖音吸引了第一批忠实粉丝。当产品功能逐渐完善后,抖音在运营方面开始发力,用户迎来大幅增长。抖音的主力用户群体年龄段上升,已经从早期的18岁到24岁,上升到了25岁到30岁用户。随着用户的快速增长,在内容层面也向着更加主流化、多元化的方向转变。

架构方面比较常见的有三种:

  1. 单体应用

所有的模块打包到一起部署运行,在开发小型项目上有独特优势:易于调试、部署,运维方便。缺点是容错性低,不可靠。只能通过运行更多的服务器水平扩展, 而不同的应用服务对资源的需求不同,且不可持续发展。

  1. SOA面向服务架构

面向服务架构是一种设计方法,设计上通常是自上而下的,服务间松散耦合。ESB集成不同协议的服务,做消息的转化、解释、路由从而联通各个服务,解决企业通信问题,服务松耦合、可扩展。缺点是SOA更多的面向企业服务,服务拆分粒度很大,更多的是为了复用。

  1. 微服务

微服务是去中心化的SOA的扩展,强调服务彻底的组件化,一个组件就是一个产品,服务切分力度更小,设计上更多的是自下而上的。服务间通过轻量级的协议进行通信,并根据服务本身需要独立化部署。从产品视角出发,更多聚焦可扩展性,兼顾可维护性。

综合上述几种服务的对比,我们最终选择了微服务架构,并使用下面的技术栈:

  • 分布式中间件:Consul
  • 网关:Kong
  • 数据库:MySQL
  • orm框架:GORM
  • 缓存:Redis
  • 消息队列:RabbitMQ
  • 对象存储:七牛云对象存储Kodo
  • Web框架:Gin
  • RPC 框架:GRPC
  • 数据传输协议:protobuf
  • 用户鉴权中间件:JWT
  • 配置文件:viper

3.1.1 需求分析

一、用户模块

用户模块包括用户注册、用户登录和用户信息三个部分。

  1. 用户注册接口 POST-/douyin/user/register/

新用户注册时提供用户名,密码,昵称即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token。

接口定义:

message douyin_user_register_request{    string username = 1; // 注册用户名,最长32个字符    string password = 2; // 密码,最长32个字符}message douyin_user_register_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    int64 user_id = 3; // 用户id    string token = 4; // 用户鉴权token}
  1. 用户登录接口 POST-/douyin/user/login/

通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token

接口定义:

message douyin_user_login_request{    string username = 1; // 登录用户名    string password = 2; // 登录密码}message douyin_user_login_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    int64 user_id = 3; // 用户id    string token = 4; // 用户鉴权token}
  1. 用户信息接口 GET-/douyin/user/

获取登录用户的 id、昵称,如果实现社交部分的功能,还会返回关注数和粉丝数。

接口定义:

message douyin_user_request{    int64 user_id = 1; // 用户id    string token = 2; // 用户鉴权token}message douyin_user_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    User user = 3; // 用户信息}
二、视频模块

视频模块包括包括视频Feed流获取、视频投稿和获取用户投稿列表三个模块

  1. 视频流接口 GET-/douyin/feed/

不限制登录状态,返回按投稿时间倒序的视频列表,视频数由服务端控制,单次最多30个。

接口定义:

message douyin_feed_request{    int64 latest_time = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间    string token = 2;  // 可选参数,登录用户设置}message douyin_feed_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    repeated Video video_list = 3; // 视频列表    int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time}
  1. 发布列表接口 GET-/douyin/publish/list/

登录用户的视频发布列表,直接列出用户所有投稿过的视频。

接口定义:

message douyin_publish_list_request{    int64 user_id = 1; // 用户id    string token = 2; // 用户鉴权token}message douyin_publish_list_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    repeated Video video_list = 3; // 用户发布的视频列表}
  1. 视频投稿接口 POST-/douyin/publish/action/

登录用户选择视频上传。

接口定义:

message douyin_publish_action_request{    string token = 1; // 用户鉴权token    bytes data = 2; // 视频数据    string title = 3; // 视频标题}message douyin_publish_action_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述}
三、点赞模块
  1. 点赞操作接口 POST-/douyin/favorite/action/

登录用户对视频进行点赞与取消点赞操作。

接口定义:

message douyin_favorite_action_request {   string token = 1; // 用户鉴权token   int64 video_id = 2; // 视频id   int32 action_type = 3; // 1-点赞,2-取消点赞}message douyin_favorite_action_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述}
  1. 点赞列表接口 GET-/douyin/favorite/list/

登录用户的所有点赞视频。

接口定义:

message douyin_favorite_list_request {   int64 user_id = 1; // 用户id   string token = 2; // 用户鉴权token}message douyin_favorite_list_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述   repeated Video video_list = 3; // 用户点赞视频列表}
四、评论模块
  1. 评论操作接口 POST-/douyin/comment/action/

登录用户对视频进行评论。

接口定义:

message douyin_comment_action_request {   string token = 1; // 用户鉴权token   int64 video_id = 2; // 视频id   int32 action_type = 3; // 1-发布评论,2-删除评论   string comment_text = 4; // 用户填写的评论内容,在action_type=1的时候使用   int64 comment_id = 5; // 要删除的评论id,在action_type=2的时候使用}message douyin_comment_action_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述   Comment comment = 3; // 评论成功返回评论内容,不需要重新拉取整个列表}
  1. 视频评论列表接口 GET-/douyin/comment/list/

查看视频的所有评论,按发布时间倒序。

接口定义:

message douyin_comment_list_request {   string token = 1; // 用户鉴权token   int64 video_id = 2; // 视频id}message douyin_comment_list_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述   repeated Comment comment_list = 3; // 评论列表}
五、关注模块
  1. 关注操作接口 POST-/douyin/relation/action/

登录用户对其他用户进行关注或取消关注。实现用户之间的关注关系维护,登录用户能够关注或取关其他用户,同时自己能够看到自己关注过的所有用户列表,以及所有关注自己的用户列表。

接口定义:

message douyin_favorite_list_request {   int64 user_id = 1; // 用户id   string token = 2; // 用户鉴权token}message douyin_favorite_list_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述   repeated Video video_list = 3; // 用户点赞视频列表}
  1. 用户关注列表 GET-/douyin/relatioin/follow/list/

登录用户关注的所有用户列表。

message douyin_favorite_list_request {   int64 user_id = 1; // 用户id   string token = 2; // 用户鉴权token}message douyin_favorite_list_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述   repeated Video video_list = 3; // 用户点赞视频列表}
  1. 用户粉丝列表 GET-/douyin/relation/follower/list/

所有关注登录用户的粉丝列表。

message douyin_favorite_list_request {   int64 user_id = 1; // 用户id   string token = 2; // 用户鉴权token}message douyin_favorite_list_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述   repeated Video video_list = 3; // 用户点赞视频列表}
  1. 用户好友列表 GET-/douyin/relation/friend/list/

互相关注的用户列表。

message douyin_favorite_list_request {   int64 user_id = 1; // 用户id   string token = 2; // 用户鉴权token}message douyin_favorite_list_response {   int32 status_code = 1; // 状态码,0-成功,其他值-失败   string status_msg = 2; // 返回状态描述   repeated Video video_list = 3; // 用户点赞视频列表}
六、消息模块

客户端通过定时轮询服务端接口查询消息记录

  1. 聊天记录 GET-/douyin/message/chat/

当前登录用户和其他指定用户的聊天消息记录

message douyin_message_chat_request{    required string token=1;//用户鉴权token    required int64 to_user_id=2;//对方用户id    required int64 pre_msg_time=3;//上次最新消息的时间}message douyin_message_chat_response {    required int:32 status_code=1;//状态码,g-成功,其他值-失败    optional string status._msg=2;//返回状态描述    repeated Message message_list=3;//消息列表}message Message{    required int64 id=1;//消息id    required int64 to_user_id=2;//该消息接收者的d    required int64 from_user_id=3;//该消息发送者的id    required string content=4;//消息内容    optional int64 create_time=5;//消息创建时间}
  1. 消息操作 POST-/douyin/message/action/

登录用户对消息的相关操作,目前只支持消息发送

message douyin_relation_action_request{    required string token=1;//用户鉴权token    required int64 to_user_id=2;//对方用户id    required int32 action_type=3;//1-发送消息    required string content=4;//消息内容}message douyin_relation_action_response{    required int32 status._code=1;//状态码,g-成功,其他值-失败    optional string status_msg=2;//返回状态描述}

3.2 架构设计

运行流程:

  1. 后端服务启动,根据注册中心consul的地址(1.1.1.1:8500),将自己注册到注册中心 。
  2. 客户端访问域名,根据解析找到kong网关地址(2.2.2.2:8000)。
  3. kong网关根据客户端传过来的服务名匹配到对应的Routes,再根据Routes找到对应的Service details 。
  4. 然后拿着Service details里面配置Host,去找consul地址(1.1.1.1:8600)。
  5. 根据名称查询consul的dns表,进而找到对应的ip+端口 。
  6. 找到对应的服务,然后通信。

3.2.1 用户模块

1. 整体架构设计

2. 详细设计
2.1. 用户注册

用户注册的逻辑比较简单,请求的参数中只包含用户的用户名与密码,不支持手机注册以及各种验证码操作。因此用户的唯一识别信息为用户名。如果数据库中存在相同的用户名,则认为这个用户已经存在,拒绝注册;否则则允许用户注册,并在数据库中分配给这个用户唯一的id。最后调用JWT生成Token返回响应,作为在Token生效期间的用户的唯一标识符。

用户注册流程:

  1. DY-api.UserRegister处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserRegister
  2. 服务端根据用户名查询数据库,如果发现重名用户名,则直接返回错误
  3. 未发现重名用户名,则通过md5加盐(用户名)对密码进行加密,加密后插入数据库,数据库返回唯一自增ID
  4. 服务端返回成功响应给DY-api.UserRegister
  5. DY-api.UserRegister利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
2.2. 用户登录

用户登录请求的参数中只包含用户的用户名与密码,不支持手机登录以及各种验证码操作。因此用户的唯一识别信息为用户名。如果数据库中不存在相同的用户名,则认为这个用户不存在,拒绝登录;否则则允许用户登录,并返回数据库中这个用户的唯一id。同时调用JWT生成Token返回响应,作为在Token生效期间的用户的唯一标识符。

用户登录流程:

  1. DY-api.UserLogin处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserLogin
  2. 服务端根据用户名查询数据库,如果未发现相同用户名,则直接返回错误,否则返回通过用户名查询出来的用户id和密码
  3. 对用户输入的密码进行md5加盐(用户名)加密,与上一步返回的密码进行比较,如果不匹配直接返回错误
  4. 密码匹配,则服务端返回成功响应给DY-api.UserLogin
  5. DY-api.UserLogin利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端
2.3. 用户信息

用户信息请求的参数包括要请求的用户的id和当前登录的用户的Token。返回的用户信息应该包括用户的名称,用户的关注人数和粉丝人数,以及用户与当前登录用户的关注关系。因此除了调用DY-api.UserInfo获取用户的基本信息之外,还需要调用DY-srv.GetFollowList与DY-srv.GetFollowerList获取用户的关注人和用户的粉丝列表。两个Count数值可以通过查看切片的大小获得,关注关系需要遍历切片进行搜索。

在对不同的服务进行调用的时候采取并行调用的方式,服务全部返回后在api层进行拼接,从而提高效率。

用户信息流程:

  1. DY-api.UserInfo处理请求,将请求中带有的id字段传递到服务端DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList
  2. 并行请求三个服务,其中DY-srv.UserInfo根据id字段查询数据库,如果id有效,则返回用户姓名,否则返回错误
  3. 等待三个服务全部成功返回后,填充响应中的User的五个字段
    1. id与name字段通过DY-srv.UserInfo的响应直接获取
    2. followcount通过获取DY-srv.GetFollowList返回的切片长度获取
    3. followercount通过获取DY-srv.GetFollowerList返回的切片长度获取
    4. 通过Token获取当前的登录用户id,在DY-srv.GetFollowerList切片内部查询,如果查询到为True,否则为False
  4. 构建响应结构体并返回给客户端

3.2.2 视频模块

1. 整体架构设计

img

2. 详细设计
2.1. 视频流

获取视频流的请求参数包括视频的最新时间和当前用户的Token信息。如果当前用户在登录的状态下请求视频流,则通过最新时间在数据库中查询前30个视频的信息,包括视频本身的id和作者的id。获得最多30个视频的信息后,创建多个协程(视频数量个数),根据每个id获得视频的其他信息,如作者的详细信息,视频的点赞数量,作者的关注人数等等。在调用不同服务的时候也采用并行调用的方式。最后对返回的全部响应进行组织返回。

如果用户没有登录,则Token信息为空,那么返回的响应中缺少一些交互信息,如当前用户是否已经对当前视频点赞等等。

获取视频流流程:

  1. DY-api.Feed处理请求,准备请求服务

  2. 首先请求DY-srv.Feed服务,根据时间戳查询数据库,查询出不超过时间戳的前30个视频,查询后返回视频列表

  3. 随后并行请求视频列表中的每一个视频(即最大并发数为30)

  4. 对每一个视频,根据前一个服务响应的作者的id并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录Author响应相关的5个字段

  5. 对每一个视频,根据视频id并行请求DY-srv.douyinCommentList和DY-srv.douyinLikeVideo,对于每个视频

    1. commentCount通过获取DY-srv.douyinCommentList返回的切片长度获取
    2. favoriteCount通过获取DY-srv.douyinLikeVideo返回的切片长度获取
    3. 通过Token获取当前的登录用户id,在DY-srv.douyinLikeVideo切片内部查询,如果查询到为True,否则为False
  6. 等待全部的视频返回响应后,构建响应结构体并返回给客户端

2.2. 发布列表

获取用户视频发布列表的请求参数包括用户的id和当前用户的Token信息。两者不一定是相同的用户,因为用户在观看视频的同时点击用户头像即可以看到这个视频作者的信息和作者的视频发布列表。

如果当前用户是查看自己的视频发布列表,则通过用户的id在数据库中查询发布的视频的信息。获得最多视频的信息后,创建多个协程(视频数量个数),根据每个id获得视频的其他信息,如视频的点赞数量,作者的关注人数等等。在调用不同服务的时候也采用并行调用的方式。最后对返回的全部响应进行组织返回。

如果Token信息为空,则当前场景是用户查看其他用户的发布视频列表。那么返回的响应中缺少一些交互信息,如当前用户是否已经对当前作者的视频点赞等等。

获取视频发布列表流程:

  1. DY-api.PublishList处理请求,准备请求服务

  2. 首先请求DY-srv.PublishList服务,根据id查询数据库,如果id在数据库中不存在,则直接返回错误,然后根据用户id查询发布的视频列表并返回

  3. 随后并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录User响应相关的5个字段

  4. 对每一个视频,根据视频id并行请求DY-srv.douyinCommentList和DY-srv.douyinLikeVideo,对于每个视频

    1. commentCount通过获取DY-srv.douyinCommentList返回的切片长度获取
    2. favoriteCount通过获取DY-srv.douyinLikeVideo返回的切片长度获取
    3. 通过Token获取当前的登录用户id,在DY-srv.douyinLikeVideo切片内部查询,如果查询到为True,否则为False
  5. 等待全部的视频返回响应后,构建响应结构体并返回给客户端

2.3. 视频投稿

视频投稿的请求参数中包括用户的Token,上传的视频流数据以及视频的标题。其中视频流是用户从本地上传得到的,视频的标题是用户自行输入得到的。上传视频必须是在登录的状态下,因此必须包含用户的Token信息。获得参数后,根据Token信息解析出当前用户的id,然后根据用户id判断是否存在这个用户的文件夹。如果不存在文件夹则新建用户文件夹。创建文件夹后将视频流写入这个文件夹下的视频文件,同时调用ffmpeg对视频的封面进行截取从而获得视频的首图。确认视频文件与图片文件都保存在本地后,构建返回的响应,并将上传文件的消息推送到消息队列中,此时消息队列将视频文件和图片文件异步上传到对象存储当中,上传结束后将视频信息写入数据库,在下次请求视频流的过程中就可以请求到这个视频了。

其中使用RabbitMQ进行异步处理,在服务器带宽有限的情况下,上传视频对用户来说基本无感,增加了用户的体验。且上传到对象存储后视频和图片的展示和下载速度也会更快,方便用户查看视频。

视频投稿流程:

  1. DY-api.PublishAction处理请求,将请求中的字段传递到服务端DY-srv.PublishAction
  2. 服务端从Token中获取id信息,如果无法获取id,直接返回错误
  3. 服务端根据id信息查询数据库,获取用户信息,如果id并不存在于数据库,则直接返回错误
  4. 服务端判断本地存放视频与图片文件的文件夹是否存在,如果不存在则创建文件夹
  5. 服务端将接收到的请求中的字节流写入文件,并调用ffmpeg对视频的第一帧进行截图作为封面,同样写入图片文件
  6. 服务端将文件上传信息传递给消息队列,直接返回成功响应给客户端
  7. 消息队列接收到消息后并行上传视频和图片文件,两者都上传成功后将视频信息写入数据库

3.2.3 点赞模块

1. 整体架构设计

img

2. 详细设计
2.1 点赞操作

点赞操作分为对未点赞的视频点赞以及对已点赞的视频取消点赞。点赞操作接口的请求参数包括,用户token;视频id;操作类型(1–点赞,2–取消点赞)。通过解析用户token可获得用户id。构建一个redis集合,将用户已经点赞的视频将其按照k-v形式存入redis。

2.1.1 对视频点赞

当请求参数操作类型的值为1时,即为点赞操作,点赞操作是要对用户未点赞的视频进行点赞,首先在redis集合中查询该用户是否对此视频点赞过,若点赞过则返回视频已点赞,若未点赞,则将该条点赞记录先插入redis再插入数据库中,最后返回成功的响应码。

2.1.2 对视频取消点赞

当请求参数操作类型的值为2时,即为取消点赞操作,取消点赞操作是要对用户点赞的视频进行取消,首先在redis集合中查询该用户是否对此视频点赞过,若未点赞过则返回视频暂未点赞,若点赞了,则将该条点赞记录先从redis中删除再从数据库中删除,最后返回成功的响应码。

2.2 喜欢列表

喜欢列表接口的请求参数为用户id和用户token,先根据token验证用户身份与登录状态,若成功,则根据用户id查询用户的喜欢列表,将喜欢列表封装进响应结构体中,返回参数中还需要视频相关信息,通过调用视频服务接口,获取视频相关信息,并封装到响应结构体中,最终将响应结构体返回。

3.2.4 评论模块

1. 整体架构设计

img

2. 详细设计
2.1 评论操作

评论操作分为发表评论和删除评论,评论操作接口的请求参数包括用户token,视频id,操作类型(1–发表评论,2–删除评论),评论内容(发表评论时),评论id(删除评论时)。首先根据token验证用户身份与登录状态,若成功,则解析token获取用户id。

2.1.1 发表评论

当操作类型等于1时,表示是发表评论,将对应评论内容,用户id,视频id,添加进数据库,并且将评论列表封装进响应结构体,同时调用社交服务,获取对应的用户信息,将用户信息也封装进响应结构体,最后将其返回。

2.1.2 删除评论

当操作类型等于2时,表示是删除评论,将评论id对应的数据从数据库中删除,并返回删除成功的信息。

2.2 评论列表

评论列表接口的请求参数为视频id和用户token,先根据token验证用户身份与登录状态,若成功,则根据视频id查询视频的评论列表,将评论列表封装进响应结构体中,返回参数中还需要用户相关信息,通过调用社交服务接口,获取用户相关信息,并封装到响应结构体中,最终将响应结构体返回。

3.2.5 社交模块

社交模块的整体设计如下图:

其中 social-api程序是使用Gin框架搭建的Web服务。主要接受url请求,通过路由绑定handler处理函数,添加授权中间件。social-api部署了多个,并将自己注册在Consule服务上,支持负载均衡,并通过服务发现调用gRPC服务。

social-srv是业务处理代码,主要和MySQL数据库打交道。social-srv可以部署在多个不同服务器上,并将自己注册到Consul上来实现负载均衡,提供被其他服务发现。

详细设计:

  1. 关注模块

关注接口的请求参数为用户ID和被关注的用户ID,先根据token验证用户身份与登录状态,若成功,则向数据库插入数据,同时互相关注的用户会成为朋友,在朋友界面显示朋友列表,并展现最近的一条消息。用户也可以在信息详情页面来查看关注的用户和粉丝。

  1. 消息模块

通过用户ID和朋友ID可以新增一条消息。使用定时调用接口的方式来获取消息。

3.3 数据库设计

3.3.1 videos表

字段如下:

名称类型说明
idbigint视频唯一id,自增主键
author_idbigint视频作者id
file_namevarchar文件名称
publish_timebigint发布时间
titlevarchar视频标题

索引设置:

  1. 视频唯一id的自增主键索引
  2. 发布时间的索引,用户在数据库中查询指定时间范围的视频
  3. 作者id的索引,用于查询指定作者的视频列表

3.3.2 users表

名称类型说明
idbigint用户id,自增主键
namevarchar用户名
passwordvarchar用户密码

索引设置:

  1. 用户id的自增主键索引
  2. 用户名与密码的联合索引,用于在数据库中匹配用户

3.3.3 comments表

名称类型说明
idbigint评论唯一id,自增主键
user_idbigint评论发布者的id
video_idbigint评论发布位置的视频id
comment_textvarchar评论内容
create_timedatetime评论创建时间

索引设置:

  1. 评论id的自增主键索引
  2. 视频id的索引,用于在数据库中查询某条视频对应的评论内容

3.3.4 follows表

名称类型说明
idbigint关注关系id,自增主键
user_idbigint用户id
follower_idbigint关注的用户id

索引设置:

  1. 关注关系id的自增主键索引
  2. 用户id和关注的用户id的联合索引,用于在数据库中查询两个用户之间的关注关系
  3. 关注的用户id索引,用于在数据库中查询用户的关注关系

3.3.5 likes表

名称类型说明
idbigint喜欢关系id,自增主键
user_idbigint点赞用户的id
video_idbigint被点赞的视频id

索引设置:

  1. 喜欢关系id的自增主键索引
  2. 用户和点赞视频的联合索引,用于在数据库中查询某个用户是否对某个视频点赞
  3. 用户id索引,用于在数据库中查询某个用户的点赞的视频的id
  4. 视频id索引,用于在数据库中查询某个视频的点赞用户的id

3.3.6 messages表

名称类型说明
idbigint消息唯一id,自增主键
user_idbigint发送消息的用户id
to_user_idbigint接收消息的用户id
sent_timedatetime消息发送时间
contentvarchar消息内容

索引设置:

  1. 消息id的自增主键索引
  2. 发送用户id索引,用于查询数据库中指定的用户发送的消息
  3. 接收用户id索引,用于查询数据库中指定的用户接收的消息

3.4 项目代码介绍

后端项目总体分为两个大部分:

  1. web项目(simple-DY/DY-api/):使用Gin框架来获取用户请求,连接GRPC远程调用服务,最后返回数据。
  2. service项目(simple-DY/DY-srvs/):GRPC编写的微服务。

项目的总体结构如下所示:

├── simple-DY│   ├── db.sql        // 数据库初始化文件│   ├── DY-api           // web项目│   │   ├── interact-web    // 互动模块│   │   ├── social-web      // 社交模块│   │   └── video-web       // 视频模块│   ├── DY-srvs          // service项目│   │   ├── interact-srv    // 互动模块│   │   ├── social-srv      // 社交模块│   │   └── video-srv       // 视频模块│   ├── go.mod│   ├── go.sum│   └── README.md

3.4.1 video服务(包括视频模块和用户模块)

  1. api层

代码结构:

video-web├── api│   ├── base.go│   ├── feed.go│   ├── info.go│   ├── otherapi.go│   ├── publishaction.go│   ├── publishlist.go│   ├── userinfo.go│   ├── userlogin.go│   └── userregister.go├── config│   └── config.go├── config-debug.yaml├── config-pro.yaml├── global│   └── global.go├── initialize│   ├── config.go│   ├── logger.go│   ├── router.go│   ├── srv_conn.go│   └── validator.go├── logs│   └── video-web.log├── main.go├── middlewares│   ├── cors.go│   └── jwt.go├── models│   ├── base.go│   ├── jwt.go│   ├── other.go│   ├── request.go│   └── response.go├── proto│   ├── simpledy_grpc.pb.go│   ├── simpledy.pb.go│   └── simpledy.proto├── README.md└── utils    └── consul        └── register.go

详细说明:

  • api:编写路由的Handler处理函数
  • config:读取yaml文件时的接收结构体
  • *.yaml:配置文件
    • config-debug.yaml:线下开发使用的配置文件
    • config-pro.yaml: 线上配置文件
  • global:存放全局变量,例如config信息,连接信息等
  • initialize:初始化程序代码
    • config.go:读取配置文件
    • logger.go:日志配置
    • router.go:gin路由
    • srv_conn.go:连接微服务
    • validator.go:翻译器
  • logs:日志文件
  • main.go:主程序入口
  • middlewares:gin的自定义中间件
    • cors.go:跨域中间件
    • jwt.go:JWT中间件
  • models:用户请求参数的结构体
  • proto:编写和生成proto文件
  • README.md:说明文件
  • utils:工具类
    • consul:调用consul api进行服务注册发现等操作
  1. srv层

代码结构:

.├── config│   └── config.go├── config-debug.yaml├── config-pro.yaml├── global│   └── global.go├── handler│   ├── base.go│   ├── feed.go│   ├── publishaction.go│   ├── publishlist.go│   ├── userinfo.go│   ├── userlogin.go│   ├── userregister.go│   └── videoinfo.go├── initialize│   ├── config.go│   ├── db.go│   ├── handler.go│   └── logger.go├── logs│   └── video-srv.log├── main.go├── models│   ├── base.go│   └── db.go├── proto│   ├── simpledy_grpc.pb.go│   ├── simpledy.pb.go│   └── simpledy.proto├── README.md└── utils    ├── backup    │   └── backup.go    ├── consul    │   └── register.go    ├── dao    │   ├── followdao.go    │   ├── userdao.go    │   └── videodao.go    ├── ffmpeg    │   └── extractFirstFrame.go    ├── freeport    │   └── port.go    ├── jwt    │   └── token.go    ├── md5salt    │   └── md5.go    ├── oss    │   └── upload.go    └── rabbitmq        ├── base.go        ├── consumer.go        └── producer.go

详细说明:

  • config:读取yaml文件时的接收结构体
  • *.yaml:配置文件
    • config-debug.yaml:线下开发使用的配置文件
    • config-pro.yaml: 线上配置文件
  • global:存放全局变量,例如config信息,连接信息等
  • handler:主要的逻辑代码,proto的service的实现类
  • initialize:初始化程序代码
    • config.go:读取配置文件
    • db.go:数据库全局连接
    • handler.go:监听客户端连接
    • logger.go:日志配置
  • logs:日志文件
  • main.go:主程序入口
  • models:用户请求参数的结构体
  • proto:编写和生成proto文件
  • README.md:说明文件
  • utils:工具类
    • backup:备份用户上传的视频和图片文件
    • consul:调用consul api进行服务注册发现等操作
    • dao:数据库相关操作
    • ffmpeg:视频首页截图
    • freeport:获取空闲网络端口
    • jwt:鉴权Token的生成与解析
    • md5salt:密码加密存储
    • oss:七牛云对象存储相关操作
    • rabbitmq:消息队列相关操作

3.4.2 interact服务(包括点赞模块和评论模块)

  1. api层
interact-web├── api│   ├── base.go│   ├── comment.go│   └── like.go├── config│   └── config.go├── global│   └── global.go├── initialize│   ├── config.go│   ├── logger.go│   ├── router.go│   ├── srv_conn.go│   └── validator.go├── main.go├── middlewares│   ├── cors.go│   └── jwt.go├── models│   └── request.go├── proto│   ├── simpledy_grpc.pb.go│   ├── simpledy.pb.go│   └── simpledy.proto├── router│   ├── comment.go│   └── like.go└── utils    └── register        └── consul            └── register.go
  1. srv层
interact-srv├── build.sh├── config│   └── config.go├── global│   └── global.go├── handler│   └── interact.go├── initalize│   ├── config.go│   ├── db.go│   ├── logger.go│   ├── rdb.go│   └── srvs_conn.go├── main.go├── model│   ├── base.go│   ├── comment.go│   ├── like.go│   └── video.go├── proto│   ├── simpledy_grpc.pb.go│   ├── simpledy.pb.go│   └── simpledy.proto└── utils    ├── addr.go    ├── jwt    │   └── token.go    ├── key    │   └── key.go    └── register        └── consul            └── register.go

3.4.3 social服务(包括关注模块和消息模块)

  1. api层
social-web├── api│   ├── base.go│   ├── message.go│   └── relation.go├── config│   └── config.go├── config-debug.yaml├── config-pro.yaml├── forms│   ├── message.go│   └── relation.go├── global│   └── global.go├── initialize│   ├── config.go│   ├── logger.go│   ├── router.go│   ├── srv_conn.go│   └── validator.go├── main.go├── middlewares│   ├── cors.go│   └── jwt.go├── models│   └── request.go├── proto│   ├── simpledy_grpc.pb.go│   ├── simpledy.pb.go│   └── simpledy.proto├── router│   ├── message.go│   └── relation.go└── utils    ├── addr.go    └── register        └── consul            └── register.go
  1. srv层
social-srv├── build.sh├── config│   └── config.go├── config-debug.yaml├── config-pro.yaml├── global│   └── global.go├── handler│   └── social.go├── initialize│   ├── config.go│   ├── db.go│   └── logger.go├── main.go├── model│   └── base.go└── proto    ├── simpledy_grpc.pb.go    ├── simpledy.pb.go    └── simpledy.proto

四、测试结果

4.1 功能测试

通过Apifox的自动化测试,构建不同实际使用中可能遇到的情况,对接口进行充分测试。

1. 用户注册接口 /douyin/user/register/

需要对如下的用例进行测试:

  1. 注册不存在的用户名-返回成功响应
  2. 注册已经存在的用户名-返回失败响应

测试结果:

img

2. 用户登录接口 /douyin/user/login/

需要对如下的用例进行测试:

  1. 登录已经存在的用户名且密码正确-返回成功响应
  2. 登录不存在的用户名-返回失败响应
  3. 登录已经存在的用户名,但是密码错误-返回失败响应

测试结果:

img

3. 用户信息接口 /douyin/user/

需要对如下的用例进行测试:

  1. 用户id存在且Token正确-返回成功响应
  2. 用户id存在但Token为空或不正确-返回成功响应(但是没有是否关注与是否点赞等关系信息)
  3. 用户id不存在-返回失败响应

测试结果:

img

4. 视频流接口 /douyin/feed/

需要对如下的用例进行测试:

  1. 未登录用户请求视频流(包括Token错误的情况)-返回成功响应(但是缺少是否对视频点赞等关系信息)
  2. 登录用户请求视频流-返回完整的成功响应

测试结果:

img

5. 发布列表接口 /douyin/publish/list/

需要对如下的用例进行测试:

  1. 用户id存在且Token正确-返回成功响应
  2. 用户id存在但Token为空或不正确-返回成功响应(但是没有是否点赞等关系信息)
  3. 用户id不存在-返回失败响应

测试结果:

img

6. 视频投稿接口 /douyin/publish/action/

需要对如下的用例进行测试:

  1. 正常上传视频-返回成功响应
  2. Token为空或Token不正确-返回错误响应

测试结果:

img

7. 社交模块

img

8. 互动模块

4.2 性能测试

五、其他资料

接口文档(旧版)

汇报文档

课程汇总

抖音项目方案说明

极简抖音App使用说明

青训营大项目答疑

六、项目总结与反思

1. 目前仍存在的问题

  • 在视频模块中,上传视频的大小有限制,如果超过了限制会返回网络错误,无法将视频字节流传递到服务器端。
  • 观看视频时,一个服务器的宽带顶不住,有点卡。
  • 获取消息的API由于是定时查询,消息会重叠。
  • 若出现对短时间内一个视频进行大量点赞操作,写入数据库操作会太频繁,可以考虑将点赞记录进行定期写入数据库。

2. 已识别出的优化项

  • 视频模块中可以对用户的视频习惯进行分类,每一次获取视频流的时候对用户进行视频推荐
  • 用户模块可以增加邮箱或手机号等验证方式,并添加密码找回的功能,增加安全性
  • 粉丝列表、用户的聊天记录、关注列表和朋友列表可以使用Redis的List数据结构来存储,来降低MySQL的压力
  • 用户聊天的消息推送可以使用websocket长连接来避免每次建立链接释放链接所消耗的资源。
  • 用户聊天的消息推送可以使用MQ消息队列来实现,不查表可以减低MySQL压力和消息的实时性。
  • 点赞功能将点赞记录存在redis中,减少数据库查询压力。

3. 架构演进的可能性

  • 微服务基本根据路由进行拆分,拆分不够合理,服务之间耦合的地方稍多。后续可以将微服务进行进一步拆分,真正做到将所有的功能打包成独立的单元。
  • 可以从微服务架构演进为Serverless。Serverless是一种构建和管理基于微服务架构的完整流程,允许你在服务部署级别而不是服务器部署级别来管理你的应用部署。它与传统架构的不同之处在于,完全由第三方管理,由事件触发,存在于无状态(Stateless)、暂存(可能只存在于一次调用的过程中)计算容器内。构建无服务器应用程序意味着开发者可以专注在产品代码上,而无须管理和操作云端或本地的服务器或运行时。Serverless真正做到了部署应用无需涉及基础设施的建设,自动构建、部署和启动服务。

4. 项目过程中的反思与总结

在参加青训营期间,官方提供了全面的课程,涵盖了创作技巧、内容制作、问题分析等多个方面。这些课程不仅提供了实用的知识和技能,还可以让我们更好地理解抖音平台和用户需求。抖音青训营项目还提供了多种资源支持,包括专业导师、团队合作等。这些资源可以帮助我们更好地实践和落地自己的创意。

回顾整个项目的过程,我们团队做了如下总结:

  • 在代码编写的过程中,保持良好的编码规范不仅对自己以后复习代码节省时间,同事对代码的理解也会更方便。
  • 在实践中学习新的知识和技能。
  • 好记性不如烂笔头。伴学笔记的习惯值得我们继续保持。
  • 在协作开发中,团队的活力来源于不断的交流。通过交流和合作,我们学到了很多新的创作思路和理念。

七、参考资料

https://grpc.io/

https://www.jianshu.com/p/4e4ff6be6af9

https://www.apifox.cn/apidoc/shared-09d88f32-0b6c-4157-9d07-a36d32d7a75c/api-50707523

https://juejin.cn/post/7174037539345399839

https://blog.csdn.net/cc18868876837/article/details/90672971

https://www.woshipm.com/evaluating/1552722.html

]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Kafka简单示例 + + /2023/03/03/Backend/Kafka/ + + Kafka简单示例

Kafka

安装

首先需要安装Java

sudo apt install openjdk-11-jdk

下载安装kafka

wget https://dlcdn.apache.org/kafka/3.4.0/kafka_2.13-3.4.0.tgztar -xzf kafka_2.13-3.4.0.tgzcd kafka_2.13-3.4.0

启动zookeeper和kafka:

bin/zookeeper-server-start.sh config/zookeeper.propertiesbin/kafka-server-start.sh config/server.properties

go连接Kafka使用

生产者

使用给定代理地址和配置创建一个同步生产者

// 使用给定代理地址和配置创建一个同步生产者SyncProducer, err := sarama.NewSyncProducer(    []string{conn},    config,)

config可以自由配置:

config := sarama.NewConfig()// 等待服务器所有副本都保存成功后的响应config.Producer.RequiredAcks = sarama.WaitForAll// 随机的分区类型:返回一个分区器,该分区器每次选择一个随机分区config.Producer.Partitioner = sarama.NewRandomPartitioner// 是否等待成功和失败后的响应config.Producer.Return.Successes = true

构建发送的消息:

// 构建发送的消息msg := &sarama.ProducerMessage{    Topic: topic,    Key:   sarama.StringEncoder(time.Now().String()),    Value: sarama.StringEncoder(content),}

生产者发送消息:

// SendMessage:该方法是生产者生产给定的消息// 生产成功的时候返回该消息的分区和所在的偏移量// 生产失败的时候返回errorpartition, offset, err := SyncProducer.SendMessage(msg)

消费者

创建一个消费者的实例

config := sarama.NewConfig()consumer, err := sarama.NewConsumer(c.Node, config)

查询这个 topic 有多少分区

partitions, err := consumer.Partitions(c.Topic)

每个分区开一个 goroutine 来消费

wg.Add(len(partitions))// 然后每个分区开一个 goroutine 来消费for _, partitionId := range partitions {    //不开异步会导致一个消费完才会消费另外一个    go c.consumeByPartition(consumer, c.Topic, partitionId, &wg)}

消费

partitionConsumer, err := consumer.ConsumePartition(topic, partitionId, sarama.OffsetNewest)// 然后可以通过partitionConsumer.Messages()打印得到的消息

主函数

func main() {    Conn := "127.0.0.1:9092"    topic := "test_log"    var wg sync.WaitGroup    wg.Add(2)    // 消费者    go func() {        defer wg.Done()        // 初始化consumer        var kafkaConsumer = consumer.KafkaConsumer{            Node:  []string{Conn},            Topic: topic,        }        // 消费        go kafkaConsumer.Consume()    }()    // 生产者    go func() {        defer wg.Done()        index := 0        for {            // 生产者发送消息            _, err := producer.Send(Conn, topic, fmt.Sprintf("lox_%d", index))            if err != nil {                log.Print("测试失败:" + err.Error())                return            }            index++            time.Sleep(1 * time.Second)        }    }()    wg.Wait()}
]]>
+ + + + + Study + + + + + + + Backend + + Kafka + + + +
+ + + + + Prometheus简单示例 + + /2023/03/02/Backend/Prometheus/ + + Prometheus简单示例

Prometheus

Prometheus 是一款基于时序数据库的开源监控告警系统。Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。

示例

下载安装启动

wget https://github.com/prometheus/prometheus/releases/download/v2.37.6/prometheus-2.37.6.linux-amd64.tar.gztar xvfz prometheus-2.37.6.linux-amd64.tar.gz cd prometheus-2.37.6.linux-amd64/./prometheus --config.file=prometheus.yml

此时打开http://localhost:9090/即可以看到监控界面

Go客户端编写

package mainimport (    "net/http"    "github.com/prometheus/client_golang/prometheus/promhttp")func main() {    //提供 /metrics HTTP 端点    http.Handle("/metrics", promhttp.Handler())    //端口号    http.ListenAndServe(":2112", nil)}

运行后访问http://localhost:2112/metrics可以看到采集的指标数据

注册自定义应用程序指定指标:

package mainimport (    "net/http"    "time"    "github.com/prometheus/client_golang/prometheus"    "github.com/prometheus/client_golang/prometheus/promauto"    "github.com/prometheus/client_golang/prometheus/promhttp")func recordMetrics() {    //每2秒,计数器增加1。    go func() {        for {            opsProcessed.Inc()            time.Sleep(2 * time.Second)        }    }()}// 公开了 myapp_processed_ops_total 计数器var (    opsProcessed = promauto.NewCounter(prometheus.CounterOpts{        Name: "myapp_processed_ops_total",        Help: "The total number of processed events",    }))func main() {    recordMetrics()    http.Handle("/metrics", promhttp.Handler())    http.ListenAndServe(":2112", nil)}

运行后访问http://localhost:2112/metrics可以看到自定义的指标,每2秒,计数器增加1

服务端看板

可以修改配置文件:prometheus.yml

# my global configglobal:  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.  # scrape_timeout is set to the global default (10s).# Alertmanager configurationalerting:  alertmanagers:    - static_configs:        - targets:          # - alertmanager:9093# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.rule_files:  # - "first_rules.yml"  # - "second_rules.yml"# A scrape configuration containing exactly one endpoint to scrape:# Here it's Prometheus itself.scrape_configs:  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.  - job_name: "prometheus"    # metrics_path defaults to '/metrics'    # scheme defaults to 'http'.    static_configs:      - targets: ["localhost:2112"]

将最后的targets修改成客户端启动的端口即可

]]>
+ + + + + Study + + + + + + + Backend + + Prometheus + + + +
+ + + + + Hertz和Thrift简单示例 + + /2023/02/24/Backend/HertzAndThrift/ + + Hertz和Thrift简单示例

Hertz

Hertz 是字节跳动服务框架团队研发的超大规模的企业级微服务 HTTP 框架,具有高易用性、易扩展、低时延等特点。

官方文档:https://www.cloudwego.io/zh/docs/hertz/

基本使用:

定义路由:

func main() {h := server.Default(server.WithHostPorts("127.0.0.1:50000"))h.GET("/ping", router.Deal)h.Spin()}

路由的Handler:

其中与其他框架最大的不同点是将Context分成了两个部分:

两个上下文主要有两点区别:

  1. 生命周期不同。RequestContext 的生命周期局限于一次 http 请求之内,而 context.Context 会在 RPC Client 或者日志、Tracing 等组件间传递,其生命周期可能是链路级别的;
  2. 协程安全性。RequestContext 协程不安全,不适合异步传递,但可以通过 Copy()方法获取一个协程安全的副本,而 context.Context 本身就是协程安全的。
func Deal(c context.Context, ctx *app.RequestContext) {ctx.JSON(consts.StatusOK, utils.H{"message": res})}

Thrift

Thrift是一个 轻量级跨语言远程服务调用框架,最初由 Facebook开发,后面进入 Apache开源项目。它通过自身的 IDL 中间语言 , 并借助代码生成引擎生成各种主流语言的 RPC 服务端 /客户端模板代码。

官方安装:https://thrift.apache.org/docs/BuildingFromSource.html

网上资料:

注意安装的thrift的版本与go的插件版本一定要相同!

安装go插件:

go get github.com/apache/thrift/lib/go/thrift

首先安装依赖:

apt install libboost-dev libboost-test-dev libboost-program-options-dev libboost-filesystem-dev libboost-thread-dev libevent-dev automake libtool flex bison pkg-config g++ libssl-dev

安装Thrift:

git clone https://github.com/apache/thriftcd thrift./bootstrap.sh./configure --without-qt4 --wihout-qt5makemake install

编译使用:

thrift -r --gen go compute.thrift

Thrift文件定义

namespace go computeservice MulRange {    string BigRange(1:i64 max)}

客户端

func Deal(c context.Context, ctx *app.RequestContext) {transportFactory := thrift.NewTTransportFactory()protocolFactory := thrift.NewTBinaryProtocolFactoryConf(nil)addr := "127.0.0.1:9999"cfg := &thrift.TConfiguration{}// 建立和服务器的连接socket,通过socket建立Transportvar transport thrift.TTransporttransport = thrift.NewTSocketConf(addr, cfg)transport, _ = transportFactory.GetTransport(transport)defer transport.Close()// 打开Transport,与服务器进行连接transport.Open()iprot := protocolFactory.GetProtocol(transport)oprot := protocolFactory.GetProtocol(transport)client := compute.NewMulRangeClient(thrift.NewTStandardClient(iprot, oprot))num, _ := client.BigRange(context.Background(), 10)fmt.Println(num)ctx.JSON(consts.StatusOK, utils.H{"message": num})}

服务端

// 尽量一个struct对应一个servicetype mulrangeThrift struct {}func (m *mulrangeThrift) BigRange(_ context.Context, max int64) (string, error) {result := max + 1253return strconv.FormatInt(result, 10), nil}func main() {// 创建服务器serverTransport, _ := thrift.NewTServerSocket(net.JoinHostPort("127.0.0.1", "9999"))// 创建二进制协议transportFactory := thrift.NewTTransportFactory()protocolFactory := thrift.NewTBinaryProtocolFactoryConf(nil)mulrangeProcessor := compute.NewMulRangeProcessor(new(mulrangeThrift))// 启动服务器server := thrift.NewTSimpleServer4(mulrangeProcessor, serverTransport, transportFactory, protocolFactory)server.Serve()// 退出时停止服务器defer server.Stop()}

Thrift深入学习

参考资料:https://juejin.cn/post/6844903622380093447

Thrift是一个 轻量级跨语言远程服务调用框架,最初由 Facebook开发,后面进入 Apache开源项目。它通过自身的 IDL 中间语言 , 并借助代码生成引擎生成各种主流语言的 RPC 服务端 /客户端模板代码。

Thrift的特性

(一) 开发速度快

通过编写 RPC接口 Thrift IDL文件,利用编译生成器自动生成 服务端骨架 (Skeletons)和 客户端桩 (Stubs)。从而省去开发者自定义维护接口编解码消息传输服务器多线程模型等基础工作。

  • 服务端:只需要按照服务骨架接口 ,编写好具体的 业务处理程序 (Handler)即实现类即可。
  • 客户端:只需要拷贝 IDL定义好的客户端桩服务对象 ,然后就像调用本地对象的方法一样调用远端服务。

(二) 接口维护简单

通过维护 Thrift格式的IDL( 接口描述语言 )文件(注意写好注释),即可作为给 Client使用的接口文档使用,也自动生成接口代码,始终保持代码和文档的一致性。且 Thrift协议可灵活支持接口可扩展性

(三) 学习成本低

因为其来自 Google Protobuf开发团队,所以其 IDL文件风格类似 Google Protobuf,且更加 易读易懂 ;特别是 RPC服务接口的风格就像写一个面向对象Class一样简单。

初学者只需参照:thrift.apache.org/,一个多小时就可以理解 Thrift IDL文件的语法使用。

(四) 多语言/跨语言支持

Thrift支持 C++JavaPythonPHPRubyErlangPerlHaskellC#CocoaJavaScriptNode.jsSmalltalk等多种语言,即可生成上述语言的服务器端客户端程序

对于我们经常使用的 JavaPHPPythonC++支持良好,虽然对 iOS环境的 Objective-C(Cocoa)支持稍逊,但也完全满足我们的使用要求。

(五) 稳定/广泛使用

Thrift在很多开源项目中已经被验证是稳定高效的,例如 CassandraHadoopHBase等;国外在 Facebook中有广泛使用,国内包括百度、美团小米、和饿了么等公司。

数据类型

  • 基本类型
    • bool : 布尔值
    • byte : 8位有符号整数
    • i16 : 16位有符号整数
    • i32 : 32位有符号整数
    • i64 : 64位有符号整数
    • double : 64位浮点数
    • string : UTF-8编码的字符串
    • binary : 二进制串
  • 结构体类型
    • struct : 定义的结构体对象
  • 容器类型
    • list : 有序元素列表
    • set : 无序无重复元素集合
    • map : 有序的key/value集合
  • 异常类型
    • exception : 异常类型
  • 服务类型
    • service : 具体对应服务的类

Thrift协议

Thrift可以让用户选择客户端服务端之间传输通信协议的类别,在传输协议上总体划分为 文本 (text)和 二进制 (binary)传输协议。为 节约带宽提高传输效率 ,一般情况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这需要根据项目/产品中的实际需求。常用协议有以下几种:

  • TBinaryProtocol:二进制编码格式进行数据传输
  • TCompactProtocol:高效率的、密集二进制编码格式进行数据传输
  • TJSONProtocol: 使用 JSON文本的数据编码协议进行数据传输
  • TSimpleJSONProtocol:只提供 JSON只写的协议,适用于通过脚本语言解析

Thrift与Protobuf的区别

Thrift和Protobuf的最大不同,在于Thrift提供了完整的RPC支持,包含了Server/Client,而Protobuf只包括了stub的生成器和格式定义。

Thrift示例

thrift语法

User.thrift

namespace go Samplestruct User {    1:required i32 id;    2:required string name;    3:required string avatar;    4:required string address;    5:required string mobile;}struct UserList {    1:required list<User> userList;    2:required i32 page;    3:required i32 limit;}

Service.thrift

include "User.thrift"namespace go Sampletypedef map<string, string> Datastruct Response {    1:required i32 errCode; //错误码    2:required string errMsg; //错误信息    3:required Data data;}//定义服务service Greeter {    Response SayHello(        1:required User.User user    )    Response GetUser(        1:required i32 uid    )}
  1. 文件引入

thrift支持引入另一个thrift文件:

include "User.thrift"

注意:

include 引入文件的使用,字段必须带文件名前缀:

1:required User.User user

不能直接写 User user,这样会提示找不到 User定义。

编译时只编译引用了其他文件的thrift文件即可:

thrift -r --gen go Service.thrift
  1. 定义命名空间或者包名
namespace go Samplenamespace php Sample

需要支持多个语言,则需要定义多行。

命名空间或者包名是多层级,使用 .号隔开。例如golang对于 Sample.Model会生成目录 Sample/Model,包名是 Model

  1. Field
struct User {    1:required i32 id = 0;    2:optional string name;}

字段选项 支持 requiredoptional两种。

一旦一个参数设置为 required,未来就一定不能删除或者改为 optional,否则就会出现版本不兼容问题,老客户端访问新服务会出现参数错误。不确定的情况可以都使用 optional

  1. 类型定义
  2. 基本类型
bool:布尔值(truefalsebyte8位有符号整数i1616位有符号整数i3232位有符号整数i6464位有符号整数double64位浮点数string:使用UTF-8编码编码的文本字符串
  1. 容器类型
list<t1>:一系列t1类型的元素组成的有序列表,元素可以重复set<t1>:一些t1类型的元素组成的无序集合,元素唯一不重复map<t1,t2>:key/value对,key唯一
  1. 类型别名
typedef map<string, string> Data
  1. 枚举类型
enum TweetType {    TWEET,    RETWEET = 2,    DM = 0xa,    REPLY}

默认从0开始赋值,枚举值可以赋予某个常量,允许常量是十六进制整数。末尾没有逗号。

不支持枚举类嵌套,枚举常量必须是32位正整数。

对于go,会生成 TweetType_开头的常量。

  1. 常量类型

Thrift允许用户定义常量,复杂的类型和结构体可以使用JSON形式表示:

const i32 INT_CONST = 1234const map<string,string> MAP_CONST = {"hello": "world", "goodnight": "moon"}
  1. 异常类型
exception BizException {    1:required i32 code    2:required string msg}
  1. 结构体

结构体可以包含其他结构体,但不支持继承结构体。

struct Response {    1:required i32 errCode; //错误码    2:required string errMsg; //错误信息    3:required Data data;}
  1. 服务

Thrift编译器会根据选择的目标语言为server产生服务接口代码,为client产生桩(stub)代码。

在go里是 interfaceservice里定义的方法必须由服务端实现。

service Greeter {    Response SayHello(        1:required User.User user    )}

参数是user,返回值是Response类型

服务端代码

服务端主要完成4个部分的工作:

  • Create a transport
  • Create input/output protocols for the transport
  • Create a processor based on the input/output protocols
  • Wait for incoming connections and hand them off to the processor

服务端最终要创建这样的一个server

func NewTSimpleServerFactory6(processorFactory TProcessorFactory, serverTransport TServerTransport, inputTransportFactory TTransportFactory, outputTransportFactory TTransportFactory, inputProtocolFactory TProtocolFactory, outputProtocolFactory TProtocolFactory) *TSimpleServer {    return &TSimpleServer{        processorFactory:       processorFactory,        serverTransport:        serverTransport,        inputTransportFactory:  inputTransportFactory,        outputTransportFactory: outputTransportFactory,        inputProtocolFactory:   inputProtocolFactory,        outputProtocolFactory:  outputProtocolFactory,    }}

说明:

  • 需要至少指定2个字段(processorFactory和serverTransport)
  • 常用是指定4个字段(包括TransportFactory和ProtocolFactory),默认input与output使用的协议相同
server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)err = server.Serve()
  1. processor:thrift定义服务的处理函数
// 定义服务type Greeter struct {}handler := &Greeter{}processor := Sample.NewGreeterProcessor(handler)
  1. serverTransport:在指定的端口上创建一个socket连接
var transport thrift.TServerTransporttransport, err = thrift.NewTServerSocket(*addr)
  1. transportFactory

不同类型可选

//bufferedvar transportFactory thrift.TTransportFactoryif *buffered {    transportFactory = thrift.NewTBufferedTransportFactory(8192)} else {    transportFactory = thrift.NewTTransportFactory()}//framedif *framed {    transportFactory = thrift.NewTFramedTransportFactory(transportFactory)}
  1. ProtocolFactory

不同类型可选

var protocolFactory thrift.TProtocolFactoryswitch *protocol {case "compact":    protocolFactory = thrift.NewTCompactProtocolFactory()case "simplejson":    protocolFactory = thrift.NewTSimpleJSONProtocolFactory()case "json":    protocolFactory = thrift.NewTJSONProtocolFactory()case "binary", "":    protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()

客户端代码

客户端定义好client后直接调用方法即可,如下所示:

client := GetClient()rep, err := client.GetUser(ctx, 100)rep, err := client.SayHello(ctx, &Sample.User{     Name:    "thrift",     Address: "address", })
  1. 定义client
iprot := protocolFactory.GetProtocol(transport)oprot := protocolFactory.GetProtocol(transport)client := Sample.NewGreeterClient(thrift.NewTStandardClient(iprot, oprot))

涉及到protocolFactory与transport

  1. protocolFactory
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()iprot := protocolFactory.GetProtocol(transport)oprot := protocolFactory.GetProtocol(transport)

注意要与服务端定义的protocolFactory要一致

  1. transport

创建socket连接:

var transport thrift.TTransportvar err errortransport, err = thrift.NewTSocket(addr)

注意要提前进行类型定义,否则后面类型不匹配

定义transportFactory:

transportFactory := thrift.NewTTransportFactory()transport, err = transportFactory.GetTransport(transport)transport.Open()

注意transportFactory的类型要与服务端相同

其他

可以添加key同时使用SSL进行Socket连接从而确保安全性

]]>
+ + + + + Study + + + + + + + Backend + + Hertz + + Thrift + + + +
+ + + + + 23岁的自己,生日快乐! + + /2023/02/19/diary/Happy-Birthday-2023/ + + 23岁的自己,生日快乐!

也许今天你很迷茫,不知道应该做一些什么事情

也许今天你很失落,努力了两周的结果是从头再来

也许今天你很懊恼,后悔自己之前的选择不够合适

也许今天你很伤心,并不会有人记得你的生日

但是今天是你的生日呀

在这个并不算很特殊的日子里,也值得你对自己说一声

张兆,生日快乐!

]]>
+ + + + + Life + + + + + + + Diary + + + +
+ + + + + CIickHouse - 你没有见过的列存储 + + /2023/02/14/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day18/ + + CIickHouse - 你没有见过的列存储

ClickHouse - 你没有见过的列存储

概述

本节课程分为四个部分

  1. 数据库基本概念
  2. 列式存储
  3. ClickHouse存储设计
  4. ClickHouse典型应用场景

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前 (必须)

数据库基本概念

  1. 数据库
  2. DBMS:数据库管理系统
  3. OLTP 数据库 OLTP(Online transactional processing)
  4. OLAP 数据库:OLAP (Online analytical processing)
  5. SQL (Structured Query Language)
  6. 词法分析
  7. 语法分析
  8. AST (Abstract syntax tree)

列式存储

  1. 行式存储
  2. 列式存储
  3. 数据压缩
    a. LZ4
    b. Run-length encoding
    c. Delta encoding
  4. 延迟物化
    a. 物化
    b. Cpu cache
    c. 内存带宽
  5. 向量化
    a. SIMD (single instruction multiple data)
    b. SSE指令集
    c. AVX指令集

ClickHouse存储设计

  1. Shard key
  2. 索引
    a. 哈希索引
    b. B-Tree
    c. B+Tree
    d. LSM-Tree

ClickHouse典型应用场景

  1. Kafka
  2. Spark
  3. Hdfs
  4. Bitmap
  5. 字典编码

课中

数据库基本概念

数据库是什么

数据库是结构化信息或数据的有序集合,一般以电子形式存储在计算机系统中。通常由数据库管理系统 (DBMS) 来控制。在现实中,数据、DBMS 及关联应用一起被称为数据库系统,通常简称为数据库。

一个简单的例子

  1. 数据解析整理成有序集合

  1. 数据的写入和读取,可以通过查询语言获取想要的信息

数据库的类型

  1. 数据库有很多种,至于各种数据库孰优孰劣,主要取决于企业希望如何使用数据。
  2. 关系数据库:关系型数据库是把数据以表的形式进行储存,然后再各个表之间建立关系,通过这些表之间的关系来操作不同表之间的数据。
  3. 非关系数据库 NoSQL 或非关系数据库,支持存储和操作非结构化及半结构化数据。相比于关系型数据库,NoSQL没有固定的表结构,且数据之间不存在表与表之间的关系,数据之间可以是独立的。NoSQL的关键是它们放弃了传统关系型数据库的强事务保证和关系模型,通过所谓最终一致性和非关系数据模型(例如键值对,图,文档)来提高Web应用所注重的高可用性和可扩展性。
  4. 单机数据库:在一台计算机上完成数据的存储和查询的数据库系统。
  5. 分布式数据库 分布式数据库由位于不同站点的两个或多个文件组成。数据库可以存储在多台计算机上,位于同一个物理位置,或分散在不同的网络上。
  6. OLTP 数据库 OLTP(Online transactional processing)数据库是一种高速分析数据库,专为多个用户执行大量事务而设计。
  7. OLAP 数据库:OLAP (Online analytical processing) 数据库旨在同时分析多个数据维度,帮助团队更好地理解其数据中的复杂关系

OLAP数据库

  1. 大量数据的读写,PB级别的存储
  2. 多维分析,复杂的聚合函数

  1. 离线/实时分析,对查询速度有要求

SQL

  1. 一种编程语言,目前几乎所有的关系数据库都使用 SQL (Structured Query Language ) 编程语言来查询、操作和定义数据,进行数据访问控制。
  2. SQL的结构

查询包含一系列含有最终结果的字段, 紧跟 SELECT关键词。星号(“*”)也可以用来指定查询应当返回查询表所有字段,可选的关键词和子句包括:

  • FROM子句指定了选择的数据表。FROM子句也可以包含 JOIN 二层子句来为数据表的连接设置规则。
  • WHERE子句后接一个比较谓词以限制返回的行。WHERE子句仅保留返回结果里使得比较谓词的值为True的行。
  • GROUP BY子句用于将若干含有相同值的行合并。 GROUP BY通常与SQL聚合函数连用,或者用于清除数据重复的行。GROUP BY子句要用在 WHERE子句之后。
  • HAVING子句后接一个谓词来过滤从 GROUP BY子句中获得的结果,由于其作用于 GROUP BY子句之上,所以聚合函数也可以放到其谓词中。
  • ORDER BY子句指明将哪个字段用作排序关键字,以及排序顺序(升序/降序),如果无此子句,那么返回结果的顺序不能保证有序。

  1. SQL的用途
    a. 定义数据模型
CREATE TABLE default.test_insert_local(   `p_date` Date,   `id` Int32)ENGINE = MergeTreePARTITION BY p_dateORDER BY idSETTINGS index_granularity = 8192复制代码

b. 读写数据库数据

insert into default.test_insert_local values ('2022-01-01', 1);select count() from default.test_insert_local;复制代码
  1. SQL的优点
  • 标准化,ISO和ANSI是长期建立使用的SQL数据库标准
  • 高度非过程化,用SQL进行数据操作,用户只需提出“做什么”,而不必指明“怎么做”,因此用户无须了解存取路径,存取路径的选择以及SQL语句的操作过程由系统自动完成。这不但大大减轻了用户负担,而且有利于提高数据独立性。
  • 以同一种语法结构提供两种使用方式,用户可以在终端上直接输入SQL命令对数据库进行操作。作为嵌入式语言,SQL语句能够嵌入到高级语言(如C、C#、JAVA)程序中,供程序员设计程序时使用。而在两种不同的使用方式下,SQL的语法结构基本上是一致的。
  • 语言简洁,易学易用:SQL功能极强,但由于设计巧妙,语言十分简洁,完成数据定义、数据操纵、数据控制的核心功能只用了9个动词:CREATE、ALTER、DROP、SELECT、INSERT、UPDATE、DELETE、GRANT、REVOKE。且SQL语言语法简单,接近英语口语,因此容易学习,也容易使用。

数据库的架构

  1. Client
  2. Parser
    词法分析,语法分析,生成AST树 (Abstract syntax tree)

  1. Analyzer
    变量绑定、类型推导、语义检查、安全、权限检查、完整性检查等,为生成计划做准备
  2. Analyzer
    变量绑定、类型推导、语义检查、安全、权限检查、完整性检查等,为生成计划做准备
  3. Optimizer
  • 为查询生成性能最优的执行计划
  • 进行代价评估
  • Executor 将执行计划翻译成可执行的物理计划
  • Storage engine
    a. 管理内存数据结构【index、内存数据、缓存(Query cache、Data cache、Index cache)】
    b. 管理磁盘数据【磁盘数据的文件格式、磁盘数据的增删查改】
    c. 读写算子【数据写入逻辑、数据读取逻辑】

一个sql的执行流程

设计数据库存储的要点

  1. 性能瓶颈在哪里:数据选择、数据读取、构造内存数据、计算
  2. 选择什么样的数据格式:是否可以并发处理、是否可以构建索引、行存,列存 或者 行列混合存储
  3. 选择什么样的索引:读写的方式:读多写少、读少写多、点查场景、分析型场景

列式存储

什么是列存

  1. 行存的存储

  1. 列存的存储

列存的优点

a. 数据压缩

  • 数据压缩可以使读的数据量更少,在IO密集型计算中获得大的性能优势
  • 相同类型压缩效率更高
  • 排序之后压缩效率更高
  • 可以针对不同类型使用不同的压缩算法
  • 几种常见的压缩算法

【LZ4】

输入:abcde_bcdefgh_abcdefghxxxxxxx输出:abcde_(5,4)fgh_(14,5)fghxxxxxxx复制代码

(5,4) 代表向前5个byte,匹配到的内容长度有4,即"bcde"是一个重复

重复项越多或者越长,压缩率就会越高

【Run-length encoding】

输入:WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWBWWWWWWWWWWWWWW输出:12W1B12W3B24W1B14W复制代码

压缩重复的数据

【Delta encoding】

输入:105, 135, 112, 135, 143, 147输出:105(base),30, -23, 23, 8, 4复制代码

将数据存储为连续数据之间的差异,而不是直接存储数据本身

b. 数据处理

【查询优化】1.可以选择特定的列做计算而不是读所有列 2.对聚合计算友好

【延迟物化】

  • 物化:将列数据转换为可以被计算或者输出的行数据或者内存数据结果的过程,物化后的数据通常可以用来做数据过滤,聚合计算,Join

image.png

  • 延迟物化:尽可能推迟物化操作的发生

image.png

  • 缓存友好
  • CPU / 内存带宽友好
  • 可以利用到执行计划和算子的优化,例如filter
  • 保留直接在压缩列做计算的机会

【向量化】

  • SIMD
    single instruction multiple data,对于现代多核CPU,其都有能力用一条指令执行多条数据
    对于代码
for (size_t i = 0; i < 100; ++i)  c[i] = a[i] + b[i];复制代码

非向量化执行

c[0] = a[0] + b[0]; c[1] = a[1] + b[1];... ... 复制代码

如果这时候CPU也可以并行的计算我们写的代码,那么理论上我们的处理速度就会是之前代码的100倍,幸运的是SIMD指令就是完成这样的工作的,用SIMD指令完成这样代码设计和执行就叫做向量化

image.png

  • 执行模型
    数据需要按批读取
    函数的调用需要明确数据类型

  • 列存数据库适合设计出这样的执行模型,从而使用向量化技术

列存 VS 行存

image.png

ClickHouse的存储设计

ClickHouse的架构

  1. 架构图

  1. 表定义和结构

  1. 集群架构

ClickHouse的存储架构

  1. 数据结构

a.文件组织

b.文件内容

对于表

CREATE TABLE test.test_insert_local(    `p_date` Date,    `id` Int32)ENGINE = MergeTreePARTITION BY p_dateORDER BY idSETTINGS index_granularity = 8192复制代码

它的文件组织

├── 20220101_1_1_0│   ├── checksums.txt│   ├── columns.txt│   ├── count.txt│   ├── data.bin│   ├── data.mrk3│   ├── default_compression_codec.txt│   ├── minmax_p_date.idx│   ├── partition.dat│   ├── primary.idx│   └── versions.txt├── 20220102_2_2_0│   ├── checksums.txt│   ├── columns.txt│   ├── count.txt│   ├── data.bin│   ├── data.mrk3│   ├── default_compression_codec.txt│   ├── minmax_p_date.idx│   ├── partition.dat│   ├── primary.idx│   └── versions.txt├── detached└── format_version.txt复制代码

c. part和partition

  • part是物理文件夹的名字
  • partition是逻辑结构

d. part和column

  • 每个column都是一个文件
  • 所有的column文件都在自己的part文件夹下

e. column和index

  • 一个part有一个主键索引
  • 每个column都有列索引

索引设计

  1. 主键索引
CREATE TABLE hits_UserID_URL(    `UserID` UInt32,    `URL` String,    `EventTime` DateTime)ENGINE = MergeTreePRIMARY KEY (UserID, URL)ORDER BY (UserID, URL, EventTime)SETTINGS index_granularity = 8192, index_granularity_bytes = 0;复制代码
  1. 数据按照主键顺序一次排序
    UserID首先做排序,然后是URL,最后是EventTime

  1. 数据被组织成granule
  • granule是引擎做数据处理的最小数据单位,引擎读数据的时候不是按照一行一行读取的,而是最少读取一个granule
  • 方便构建稀疏索引
  • 方便并行计算

  1. 每个granule都对应primary.idx里面的一行

  1. 默认每8192行记录主键的一行值,primary.idx需要被全部加载到内存里面

  1. 每个主键的一行数据被称为一个mark

  1. 每个列都有这样一个mark文件,mark文件存储所有granule在物理文件里面的地址,每一列都有一个mark文件

  1. mark文件里面的每一行存储两个地址
  • 第一个地址称为block_offset,用于定位一个granule的压缩数据在物理文件中的位置,压缩数据会以一个block为单位解压到内存中。
  • 第二个地址称为granule_offset,用于定位一个granule在解压之后的block中的位置。

索引的缺陷和优化

  1. 缺陷:数据按照key的顺序做排序,因此只有第一个key的过滤效果好,后面的key过滤效果依赖第一个key的基数大小

  1. 二级索引
  • 在URL列上构建二级索引

  1. 构建多个主键索引
  • 再建一个表(数据需要同步两份,查询需要用户判断查哪张表)

  • 建一个物化视图(数据自动同步到隐式表,查询需要用户判断查哪张表)

  • 使用Projection(数据自动同步到隐式表,查询自动路由到最优的表)

数据合并

  • 一个part内的数据是有序的

  • 不同part之间的数据是无序的

  • 数据合并是将多个part合并成一起的过程

  • part的合并发生在一个分区内

  • 数据的可见性
    数据合并过程中,未被合并的数据对查询可见
    数据合并完成后,新part可见,被合并的part被标记删除

数据查询

  1. 对于查询
SELECT    URL,    count(URL) AS CountFROM hits_UserID_URLWHERE UserID = 749927693GROUP BY URLORDER BY Count DESCLIMIT 10复制代码
  1. 通过主键找到需要读的mark
  2. 切分marks,然后并发的调度reader

  1. Reader 通过mark block_offset得到需要读的数据文件的偏移量
  2. Reader 通过mark granule_offset得到解压之后数据的偏移量

  1. 构建列式filter做数据过滤

ClickHouse的典型使用场景

大宽表存储和查询

  1. 动态表结构
CREATE TABLE test_multi_columns(    `p_date` Date,    `id` Int32,    `map_a` Map(String, Int32))ENGINE = MergeTreePARTITION BY p_dateORDER BY map_a复制代码

  1. map中的每个key都是一列

  2. map中的每一列都可以单独的查询

  3. 使用方式同普通列,可以做任何计算

  4. 大宽表查询

可以建非常多的列查询的时候引擎可以快速选择需要的列,查询的时候引擎可以快速选择需要的列

离线数据分析

  1. 数据导入

数据可以通过spark生成clickhouse格式的文件

导入到hdfs上由hive2ch导入工具完成数据导入

数据直接导入到各个物理节点

  1. 数据按列导入

保证查询可以及时访问已有数据

可以按需加载需要的列

实时数据分析

  1. 数据可以被立刻查询
  2. 使用memory table减少parts数量
  • 数据先缓存在内存中
  • 到达一定阈值再写到磁盘

复杂类型查询

  1. bitmap索引
  • 构建

  • 查询
  1. bitmap64类型
select countDistinct(uid)from user_detialwhere tag_id = 'a' and uid in (    select uid from user_detail    wherer tag_id = 'b')  复制代码

  1. lowcardinality
  • 对于低基数列使用字典编码
  • 减少数据存储和读写的IO使用
  • 可以做运行时的压缩数据过滤

课后

  1. 列存和行存的差别是什么,使用场景有什么不同
  2. 列存的优点有哪些
  3. 列存的缺点有哪些
  4. 列存适合什么样的索引
  5. ClickHouse的列存是什么样的存储架构
  6. ClickHouse的索引是怎么设计的
  7. ClickHouse的查询是怎么使用索引的
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Redis - 大厂程序员是怎么用的 + + /2023/02/13/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day17/ + + Redis - 大厂程序员是怎么用的

Redis - 大厂程序员是怎么用的

1.概述

本节课程主要分为三个方面:

  1. 为什么需要Redis,Redis的基本工作原理
  2. Redis应用案例
  3. 在字节跳动,使用Redis有哪些注意事项

2.课前(必须)

2.1 安装Golang开发环境

2.2 安装Redis

2.3 熟悉Redis基本操作

2.3.1 熟悉以下命令的操作

  • GET/SET/DEL/INCR/SETNX
  • HSET/HGET/HINCRBY
  • LPUSH/RPOP/LRANGE
  • ZADD/ZRANGEBYSCORE/ZREVRANGE/ZINCRBY/ZSCORE

2.3.2 了解pipelining概念

2.4 复习数据结构

  • 链表/FIFO
  • Hash Tale
  • Skip List

3.课中

3.1 Redis基本工作原理

  • Redis实现数据持久化的原理:AOF/RDB
  • Redis单线程处理命令的概念

3.2 Redis应用案例

  • 掘金连续签到,需要了解GET/SET,Key过期
  • 掘金用户计数,使用到HASH
  • 排行榜ZSET
  • 使用SETNX实现分布式锁

3.3 在字节跳动,使用Redis有哪些注意事项

  • 大Key:Value大于10KB就是大Key,使用大Key将导致Redis系统不稳定
  • 热Key:一个Key的QPS特别高,将导致Redis实例出现负载突增,负责均衡流量不均的情况。导致单实例故障
  • 慢查询:大Key、热Kye的读写;一次操作过多的Key(mset/hmset/sadd/zadd)
  • 导致缓存穿透、缓存雪崩的场景及避免方案
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + MySQL - 深入理解RDBMS + + /2023/02/11/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day16/ + + MySQL - 深入理解RDBMS

MySQL - 深入理解 RDBMS

课程概述

RDBMS(关系型数据库)是目前使用最为广泛的数据库之一,同时也是整个信息化时代的基石。本节课程通过生活中常见的场景向大家介绍RDBMS的作用、发展历程及其核心技术,最后以字节为例,展示了RDBMS的企业级实践。本节课程主要包含以下内容:

  1. 经典案例
  2. 发展历史
  3. 关键技术
  4. 企业实践

课前材料

RDBMS有相关的数据和材料都非常多,这里主要给大家提供几篇经典论文,从经典的论文中,能够更有效的帮助大家理解RDBMS。

  1. A Relational Model of Data for Large Shared Data Banks

暂时无法在飞书文档外展示此内容

这篇论文是RDBMS的奠基之作,由RDBMS之父E.F.Codd博士于1970年发表。在这篇论文中,E.F.Codd首次提出了用于管理数据的关系模型,并将数据独立于硬件来存储,用户使用一个非过程语言来访问数据。

  1. Readings in Database Systems(Fifth Edition)

暂时无法在飞书文档外展示此内容

这本书被称为数据库领域的“红宝书”,由著名的图灵奖获得者,数据库领域专家,Michael Stonebraker撰写。其中介绍了数据库的基本概念,传统的RDBMS以及新的数据库架构等等,是一本非常棒的数据库领域入门文章。

课程详情

经典案例

通过抖音红包雨的案例,介绍 RDBMS 中 ACID 的概念:

  • 原子性( Atomicity ):事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生。
  • 一致性( Consistency ):数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
  • 隔离性( Isolation ):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
  • 持久性( Durability ):在事务完成以后,该事务所对数据库所做的更改便持久的保存在数据库之中,并不会被回滚。

发展历史

数据库发展最初过程中,诞生过3种数据模型,最终关系型模型成为了应用最为广泛的数据库模型。

  • 网状模型:用有向图表示实体和实体之间的联系的数据结构模型称为网状数据模型。
  • 层次模型:层次数据模型是用树状<层次>结构来组织数据的数据模型。
  • 关系模型:使用表格表示实体和实体之间关系的数据模型称之为关系数据模型。
网状模型层次模型关系模型
优势能直接描述现实世界 存取效率较高结构简单 查询效率高 可以提供较好的完整性支持实体及实体间的的联系都通过二维表结构表示 可以方便的表示M:N关系 数据访问路径对用户透明
劣势结构复杂 用户不易使用 访问程序设计复杂无法表示M:N的关系 插入、删除限制多 遍历子节点必须经过父节点 访问程序设计复杂关联查询效率不够高 关系必须规范化

关键技术

SQL 执行流程

在SQL执行过程中,需要经历SQL引擎、存储引擎、以及事务引擎等模块。而其中SQL引擎又分为Parser、Optimizer、Executor几个部分:

SQL 引擎

SQL引擎包括了:

  • Paser:经过词法分析、语法分析生成语法树,然后对语法树进行合法性校验。
  • Optimizer:根据Parser产生的语法树,根据规则或者代价产生执行计划树。
  • Executor:根据计划树进行执行,常见的执行方式是火山模型。

存储引擎

存储引擎负责了数据的底层存储、管理和访问工作。各大RDBMS存储引擎的设计都有不少的差异,这里选择MySQL的InnoDB存储引擎来向大家做一个介绍:

  • Buffer Pool:存储引擎位于内存中的重要结构,用于缓存数据,减少磁盘IO的开销。
  • Page:数据存储的最基本单位,一般为16KB。
  • B+u Tree:InnoDB中最常用的索引结构。

事务引擎

事务引擎实现了数据库的ACID能力,这里还是以MySQL的InnoDB为例来介绍数据库内部是通过哪些技术来实现ACID:

  • Atomicity:InnoDB中通过undo日志实现了数据库的原子性,通过Undo Log,数据库可以回滚到事务开始的状态;
  • Isolation:通过Undo Log实现MVCC(多版本并发控制),降低读写冲突。
  • Durability:通过Redo Log(一种WAL实现方式)来保证事务在提交后一定能持久化到磁盘中。
  • Consistency:一致性本质上是一种业务层的限制。

企业实践

字节中是国内数据规模最大的互联网公司之一,公司内部有成千上万套RDBMS系统。这一章节还是以红包雨为案例,展示了字节是如何解决大流量、流量突增、高可靠等问题的。

课后大作业

  1. WAL 日志到底是如何保证数据的持久化,宕机后数据不丢失的?相比于其他方案,WAL 日志都有什么优势?
  2. 除了 Undo Log 之外,是否还有其他方案可以实现 MVCC?
  3. 基于代价的优化器一般需要考虑哪些代价?
  4. 执行器的执行模型,除了本课中提到的火山模型是否还有其他模型?相比于火山模型有什么优劣势?
  5. InnoDB 的 B+ Tree 是怎么实现的?
  6. InnoDB 的 buffer pool 是怎么实现页面管理和淘汰的?
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 带你认识存储的本质 - 状态 + + /2023/02/10/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day15/ + + 带你认识存储的本质 - 状态

带你认识存储的本质 - 状态

课程概述

存储系统和数据库系统往往是后端服务的最后一环,提供数据存储、查询能力。本课程会先用模拟案例导入,向学员介绍存储系统、数据库系统的特点,然后解析多个主流产品,最后分享存储和数据库结合新技术演进的方向。本节课程主要包含以下内容:

  1. 模拟案例
  2. 存储 & 数据库简介
  3. 主流产品剖析
  4. 新技术演进

课前材料 (必须)

跟存储 & 数据库系统相关的材料很多,涵盖开源项目、博客、论文等。下面提供部分资料作为参考

  1. The Google File System

static.googleusercontent.com/media/resea…

作为各种开源分布式文件系统的鼻祖,GFS论文里面提到的架构非常经典,值得一学。

  1. The Linux Programming Interface(第13章 FILE I/O BUFFERING)

本书介绍了很多Linux内核子系统的实现,其中第13章着重讲了单机的文件IO。学习完Linux中的文件IO栈,对单机存储系统会有更深的认识。

课程详情

经典案例

通过一个模拟案例,描述了数据是怎么产生,在后端系统里怎么流通,最后怎么写入到存储/数据库系统。

存储 & 数据库简介

  • 存储系统概览
    • 存储系统特点
    • 存储器层级结构
    • 单机存储栈
    • RAID技术
  • 数据库系统概览
    • 关系型数据库特点
    • 非关系型数据库特点
    • 数据库 vs 经典存储
    • 数据库使用方式

主流产品剖析

  • 单机存储产品
    • 单机文件系统
    • 单机key-value存储
  • 分布式存储产品
    • HDFS
    • Ceph
  • 单机数据库产品
    • 关系型数据库 —— PG、MySQL
    • 非关系型数据库 —— ES、MongoDB、Redis
    • Elasticsearch使用案例
  • 分布式数据库产品
    • 问题与挑战
    • 解决方案

新技术演进

  • SPDK
  • 人工智能
  • 新硬件加速

课后思考

  1. 写入存储系统的粒度太大,会不会导致数据原子性问题?例如一次性写100MB,如果系统突然crash,会不会只有一部分数据持久化了,另一部分丢失了?如果要解决原子性问题,一般会设计什么机制?
  2. 在从应用程序到存储介质的链路上,无论读还是写,数据可能要被拷贝好几次,这几次拷贝能不能去掉?如果我们去掉大部分拷贝操作,会有什么副作用,要怎么缓解副作用?
  3. 一个关系型数据库大概率是会被并发访问的,如果要保证并发安全,除了在行数据上加悲观锁还有其他方式吗?
  4. 在数据库领域,把数据按行存和按列存各有好处,你能从性能优先的角度设计出一种混合存储格式吗?
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + RPC 原理与实现 + + /2023/02/08/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day14/ + + RPC 原理与实现

RPC 原理与实践

概述

本节课程主要分为四个方面:

  1. RPC 相关的基本概念
  2. RPC 框架的分层设计
  3. 衡量 RPC 框架的一些核心指标
  4. 字节内部 RPC 框架 Kitex 实践分享

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;

课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;

课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前

RPC 的基本概念

  • RPC的概念模型:User、User-Stub、RPC-Runtime、Server-Stub、Server

  • IDL(Interface Definition Language) 文件

    • Thrift
    • Protobuf
  • 生成代码

  • 编解码(序列化/反序列化)

  • 通信协议

    • 应用层协议
  • 网络通信

    • IO 网络模型
      • blocking IO
      • unblocking IO
      • IO multiplexing
      • signal driven IO
      • asynchronous IO
    • 传输层协议
      • TCP
      • UDP

RPC 框架分层设计

  • 编解码层

    • 数据格式:
      • 语言特定格式
      • 文本格式
      • 二进制编码
        • TLV 编码:Thrift 使用 TLV 编码
        • Varint 编码:Protobuf 使用 Varint 编码
    • 选项:
      • 兼容性
      • 通用型
      • 性能
  • 传输协议层

    • 消息切分
      • 特殊结束符
      • 变长协议:length+body
    • 协议构造
      • 以 Thrift 的 THeader 协议为例讲解
  • 网络通信层

    • 网络库
    • 核心指标
      • 吞吐高
      • 延迟低

RPC 框架的核心指标

  • 稳定性

    • 保障策略
      • 熔断
      • 限流
      • 超时
    • 请求成功率
      • 负载均衡
      • 重试
    • 长尾请求
      • BackupRequest
  • 易用性

    • 开箱即用
    • 周边工具
  • 扩展性

  • 观测性

    • Log
    • Metric
    • Tracing
    • 内置观测性服务
  • 高性能

字节内部 Kitex 实践分享

课中

基本概念

  • 相比本地函数调用,RPC调用需要解决的问题

    • 函数映射
    • 数据转换成字节流
    • 网络传输
  • 一次 RPC 的完整过程

  • RPC 带来的问题将由 RPC 框架来解决

    • 服务宕机如何感知?
    • 遇到网络异常应该如何应对?
    • 请求量暴增怎么处理?

RPC 框架分层设计

编解码层

  • 数据格式

    • 语言特定格式:例如 java.io.Serializable
    • 文本格式:例如 JSON、XML、CSV 等
    • 二进制编码:常见有 Thrift 的 BinaryProtocol,Protobuf,实现可以有多种形式,例如 TLV 编码 和 Varint 编码
  • 选型考察点

    • 兼容性
    • 通用型
    • 性能
      • 空间开销
      • 时间开销
  • 生成代码和编解码层相互依赖,框架的编解码应当具备扩展任意编解码协议的能力

协议层

  • 以 Thrift 的 THeader 协议为例

-   LENGTH 字段 32bits,包括数据包剩余部分的字节大小,不包含 LENGTH 自身长度-   HEADER MAGIC 字段16bits,值为:0x1000,用于标识 协议版本信息,协议解析的时候可以快速校验-   FLAGS 字段 16bits,为预留字段,暂未使用,默认值为 0x0000-   SEQUENCE NUMBER 字段 32bits,表示数据包的 seqId,可用于多路复用,最好确保单个连接内递增-   HEADER SIZE 字段 16bits,等于头部长度字节数/4,头部长度计算从第14个字节开始计算,一直到 PAYLOAD 前(备注:header 的最大长度为 64K)-   PROTOCOL ID 字段 uint8 编码,取值有: - ProtocolIDBinary = 0 - ProtocolIDCompact = 2-   NUM TRANSFORMS 字段 uint8 编码,表示 TRANSFORM 个数-   TRANSFORM ID 字段 uint8 编码,表示压缩方式 zlib or snappy-   INFO ID 字段 uint8 编码,具体取值参考下文,用于传递一些定制的 meta 信息-   PAYLOAD 消息内容
  • 协议解析

网络通信层

  • 阻塞 IO 下,耗费一个线程去阻塞在 read(fd) 去等待用足够多的数据可读并返回。
  • 非阻塞 IO 下,不停对所有 fds 轮询 read(fd) ,如果读取到 n <= 0 则下一个循环继续轮询。

第一种方式浪费线程(会占用内存和上下文切换开销),第二种方式浪费 CPU 做大量无效工作。而基于 IO 多路复用系统调用实现的 Poll 的意义在于将可读/可写状态通知和实际文件操作分开,并支持多个文件描述符通过一个系统调用监听以提升性能。

网络库的核心功能就是去同时监听大量的文件描述符的状态变化(通过操作系统调用),并对于不同状态变更,高效,安全地进行对应的文件操作。

RPC 框架核心指标

稳定性

  • 保障策略
    • 熔断
    • 限流
    • 超时控制

从某种程度上讲超时、限流和熔断也是一种服务降级的手段 。

  • 请求成功率

    • 负载均衡
    • 重试
  • 长尾请求

    • BackupRequest

易用性

  • 开箱即用

    • 合理的默认参数选项、丰富的文档
  • 周边工具

    • 生成代码工具、脚手架工具

扩展性

  • Middleware:middleware 会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等
  • Option:作为初始化参数
  • 核心层是支持扩展的:编解码、协议、网络传输层
  • 代码生成工具也支持插件扩展

观测性

  • 三件套:Log、Metric 和 Tracing

  • 内置观测性服务,用于观察框架内部状态
    • 当前环境变量
    • 配置参数
    • 缓存信息
    • 内置 pprof 服务用于排查问题

高性能

  • 连接池和多路复用:复用连接,减少频繁建联带来的开销
  • 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap’n Proto 等
  • 高性能网络库:Netpoll 和 Netty 等

字节内部 Kitex 实践分享

  1. 框架文档 Kitex

  2. 自研网络库 Netpoll,背景:
    a. 原生库无法感知连接状态

    b. 原生库存在 goroutine 暴涨的风险

  3. 扩展性:支持多协议,也支持灵活的自定义协议扩展

  4. 性能优化,参考 字节跳动 Go RPC 框架 KiteX 性能优化实践
    a. 网络优化

    • i. 调度优化
    • ii. LinkBuffer 减少内存拷贝,从而减少 GC
    • iii. 引入内存池和对象池

    b. 编解码优化

    • i. Codegen:预计算提前分配内存,inline,SIMD等
    • ii. JIT:无生产代码,将编译过程移到了程序的加载(或首次解析)阶段,可以一次性编译生成对应的 codec 并高效执行
  5. 合并部署
    a. 微服务过微,引入的额外的传输和序列化开销越来越大

    b. 将强依赖的服务统计部署,有效减少资源消耗

课后

  1. 行业内各个流行的 RPC 框架的优劣对比
  2. 从第三章节 RPC 的核心指标来看,Kitex 还有哪些功能是欠缺或者需要加强的?
  3. 了解微服务的新趋势 ServiceMesh,以及 RPC 框架和 ServiceMesh 的关系
  4. 关于 RPC 框架,业界有哪些新的趋势和概念?
  5. Netpoll 的优势在哪?相比其他高性能网络库例如 Netty 还有什么不足?
  6. Flatbuffer 和 Cap’n Proto 等编解码协议为什么高性能?

参考文献

  1. 官方文档 Kitex Netpoll
  2. 字节跳动 Go RPC 框架 KiteX 性能优化实践_架构_字节跳动技术团队_InfoQ精选文章
  3. 字节跳动微服务架构体系演进_架构_字节跳动技术团队_InfoQ精选文章
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 消息队列原理与实战 + + /2023/02/07/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day13/ + + 消息队列原理与实战

消息队列原理与实战

概述

本节课程主要分为五个方面:

  1. 消息队列的前世今生
  2. 消息队列-Kafka
  3. 消息队列-BMQ
  4. 消息队列-RocketMQ
  5. 最佳实践

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前

消息队列的前世

  • 消息队列应用场景
  • 消息队列的发展历史

常见消息队列

  • Kafka使用场景、架构、高级特性
  • Pulsar使用场景、架构、高级特性
  • Rocket使用场景、架构、高级特性

课中

消息队列是什么

  • 解耦
  • 削峰
  • 异步
  • 日志处理

消息队列的前世今生

消息队列-Kafka

kafka使用场景,业务日志、用户行为数据、Metrics数据

基本概念,Producer、Cluster、Consumer、Topic、Partition

数据迁移、Offset、Partition选主

一条消息从生产到消费是如何处理的,Producer端逻辑、Broker端逻辑、Consumer端逻辑

消息队列-BMQ

Kafka在使用中遇到问题

BMQ架构

BMQ各模块是如何工作的,Broker、Proxy、HDFS、MetaStorage

BMQ多机房容灾

消息队列-RocketMQ

RocketMQ使用场景

RocketMQ和Kafka对比

RocketMQ架构介绍,Producer、Broker、Nameserver、Consumer

一条消息从生产到消费是如何处理的,Producer端逻辑、Broker端逻辑、Consumer端逻辑

消息队列在字节

一些最佳实践的场景,包括数据展示

课后

  1. 消息队列的应用场景有哪些?
  2. Kafka的哪些Feature让其可以支撑大吞吐写入的场景?
  3. Kafka Consumer Rebalance的流程简述?
  4. BMQ相比较Kafka有哪些优势?
  5. RocketMQ有哪些特有的Feature?
  6. RocketMQ事务消息处理流程简述?
  7. 你认为MQ后面应该如何发展?(开放题)
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 分布式定时任务 + + /2023/02/06/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day12/ + + 分布式定时任务

分布式定时任务

概述

本节课程主要分为五个方面:

  1. 分布式定时任务整体架构
  2. 控制台Admin详细设计
  3. 触发器Trigger详细设计
  4. 调度器Scheduler详细设计
  5. 执行器Executor详细设计

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前

分布式定时任务发展历史

  • Linux命令-CronJob
  • 单机定时任务-Timer、Ticker
  • 单机定时任务-ScheduledExecutorService
  • 任务调度- Quartz
  • 分布式定时任务

分布式定时任务核心架构

  • 控制台Admin
  • 触发器Trigger
  • 调度器Scheduler
  • 执行器Executor

知识点扩充

  • 时间轮
  • 延时消息
  • 离线计算引擎 Hive
  • 实时计算引擎 Flink

课中

前言

  • 每年春节抖音都会有很多有意思的玩法,如果同学们是字节的后端同学,怎么设计今年春节集卡瓜分20亿的技术方案?

  • 业务流程

    • 定时扫描抖音用户集卡状态
    • 汇总计算用户的瓜分金额
    • 定时开奖
  • 技术体量

    • 亿级用户规模
    • 十亿级资金规模
    • 百万级读写QPS
  • 方案引出

    • 自动化 + 定时执行 + 海量数据 + 高效稳定 = 分布式定时任务

发展历程

  • 发展历史

  • Linux命令-CronJob

  • 单机定时任务-Timer、Ticker

  • 单机定时任务-ScheduledExecutorService

  • 任务调度- Quartz

  • 分布式定时任务

  • 概述

    • 定义
      • 定时任务是指系统为了自动完成特定任务,实时、延时、周期性完成任务调度的过程。
      • 分布式定时任务是把分散的、可靠性差的定时任务纳入统一的 平台 ,并实现集群管理调度和分布式部署的一种定时任务的管理方式。
    • 特点
    • 执行模式
      • 单机任务
      • 广播任务
      • Map任务
      • MapReduce任务
    • 现状
      • 业内流行框架| | Xxl-job | SchedulerX | TCT | Elastic-job | Saturn |
        | ---------- | ---------- | ---------- | ---- | ----------- | ------ |
        | 来源公司 | 美团点评 | 阿里巴巴 | 腾讯 | 当当网 | 唯品会 |
        | 是否开源 | 是 | 否 | 否 | 是 | 是 |
        | 任务编排 | 子任务依赖 | 支持 | 支持 | 不支持 | 不支持 |
        | 任务分片 | 支持 | 支持 | 支持 | 支持 | 支持 |
        | 高可用 | 支持 | 支持 | 支持 | 支持 | 支持 |
        | 故障转移 | 支持 | 支持 | 支持 | 支持 | 支持 |
        | 可视化运维 | 支持 | 支持 | 支持 | 支持 | 支持 |
      • 美团点评Xxl-job
      • 阿里巴巴SchedulerX
      • 腾讯TCT
  • 关联方案

    • 单机定时任务
    • 大数据处理引擎

实现原理

  • 整体架构

    • 核心架构

  • 数据流

  • 功能架构

控制台Admin

触发器Trigger

方案一:腾讯字节方案

方案二:Quartz方案——时间轮

调度器Scheduler

资源来源

  • 业务系统
  • 定时任务平台

执行器Executor

业务应用

  • 业务应用
    • 所有需要定时、延时、周期性执行任务的业务场景,都可以考虑使用分布式定时任务
  • 知识面扩充
    • 分布式定时任务
    • 单机定时任务
    • 延时消息
    • 离线计算引擎Hive
    • 实时计算引擎Flink

课后

  1. 分布式定时任务可以帮助我们处理哪些业务场景?
  2. 春节集卡瓜分20亿的玩法,发奖金额计算、实时开奖两个阶段分别用到分布式定时任务什么执行模式?
  3. 有了分布式定时任务,单机定时任务还有适用场景么?
  4. 时间轮这种数据结构,在定时/延时场景相比其他数据结构有哪些优势?
  5. 分布式定时任务的调度中心怎么判断一台执行器的机器处于可被调度状态?
  6. 你能想到哪些业务场景,实时计算引擎优于分布式定时任务?
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 黑灰产监控与防御 + + /2023/02/04/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day11/ + + 黑灰产监控与防御

黑灰产监控与防御

1、概述

企业的信息安全体系是非常庞大的,任何一个环节都可能会出现安全风险。其中,黑灰产是安全人员最为关注的一个风险来源,也是历年来导致企业和用户损失最大的因素。

如果某个平台或者业务被黑灰产盯上,可能是因为这个业务存在安全隐患被黑灰产利用,也可能只是被黑灰产当做牟利的垫脚石。对黑灰产的监控和防御,就是要了解他们的意图、手段和行为模式,避免被黑灰产攻击或者利用。

本次可能会给大家简单介绍国内黑灰产的情况,挑选了几种比较经典的黑产作弊手段进行详细分析,希望能帮助大家对黑灰产这个群体有一定的了解,提升各位的安全意识,在日后的工作和生活中,多一些安全角度的思考。

2、课前预习

本次课程偏科普性质,但内容不是大家在网络上可以随便看到的,课前可以阅读一些国内黑灰产的调研报告

推荐 Freebuf 黑镜调查系列 ,其中部分内容是讲师参与调查编写,不一定权威,但内容和数据都比较真实

3、思考

  • 身边是否有一些事情是可能与黑产有关的,如何辨别?
  • 你当前所学习和研究的技术,是否存在一些公开的安全问题,比如漏洞或者设计缺陷?如何避免他人利用这些问题来攻击你?
  • 如果无法避免被攻击,如何将损失降低到最小?

4、相关阅读

关于业务风控

《风控要略 互联网业务反欺诈之路》讲师参与编写

《互联网平台智能风控实战》

关于安全攻防

《白帽子讲web安全》

《Web安全深度剖析》

《Web安全机器学习入门》

上述几本都是入门级的书,挑一本即可

《 SQL注入攻击与防御》数据库安全进阶

《 linux服务器安全攻防》 主机安全进阶

关于安全体系建设

《互联网企业安全高级指南》

《大型互联网企业安全架构》

]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Consul与Kong联合配置理解 + + /2023/02/03/Backend/Consul_kong/ + + Consul与Kong联合配置理解

Consul与Kong联合配置理解

Consul介绍(实习-百度-Go后端开发-2023.02.09)

Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案,Consul的方案更“一站式”,内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如ZooKeeper等)。使用起来也较 为简单。Consul使用Go语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与Docker等轻量级容器可无缝配合 。

consul主要由server和client两种组件组成。

server负责核心数据的存储和处理请求,server可以部署多个实例(通常推荐3-5个),server只有一个leader实例,就是主节点,主节点是通过选举产生的,主节点负责数据的写入处理,同时将数据同步至其他server节点

client负责跟server通信,处理转发服务注册、服务发现请求到server节点,client还负责服务的健康检查,client节点可以部署多个实例,甚至每个微服务节点都部署一个client实例。

以开发模式启动consul,同时具备server和client的功能,不需要单独部署server和client

consul健康检查机制制运行在consul client中,会定期的根据服务健康检查配置,去检测服务是否正常,如果服务异常,就将服务的实例标记为不用, 如果恢复了,就标记为可用。

  • 基于http请求:定时以GET请求方式,请求指定url,http请求返回状态码200表示正常,其他状态代表异常。
  • 基于tcp请求:基于tcp请求方式,就是定时向指定的地址,建立tcp链接,连接成功就代表服务正常,否则就代表异常。
  • 基于grpc请求:如果微服务是基于grpc协议,可以使用grpc协议监测服务是否正常。
  • 基于命令:consul支持定期执行一个命令或脚本,来检测服务是否正常,consul通过监测命令退出状态判断服务是否正常,命令退出状态0代表正常,其他代表异常。
  • 基于TTL(服务主动向consul报告自己的健康状况):一个健康的APP可以周期性的将状态put到HTTP端

Kong介绍

Kong是一款基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,由Mashape公司开源的API Gateway项目。Kong是基于NGINX和Apache Cassandra或PostgreSQL构建的,能提供易于使用的RESTful API来操作和配置API管理系统,所以它可以水平扩展多个Kong服务器,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。

Konga是可以管理Kong的图形界面,带来的一个最大的便利就是可以很好地通过UI观察到现在kong的所有的配置,并且可以对于管理kong节点情况进行查看、监控和预警。

传统架构

微服务架构是由多个服务端和多个api端组成,客户端发起请求,需要单独的api进行接收和路由转发,然后通过与不同的服务端建立连接从而获得服务。

这个过程中需要在程序中记忆大量的端口,且一旦有节点失效,整个服务都将不可用。

Consul+Kong架构

pSsIlXF.png

Kong

  • 将不同api的ip和端口配置到Kong中(如果与Consul结合,直接配置consul_api服务名称.service.consul即可)
  • 在Kong中设置路由匹配规则
  • 客户端的请求首先发送给Kong,由Kong进行路由规则的匹配,随后转发到不同的api上
  • 客户端在请求的时候的ip地址和端口号使用任意一台api的ip地址和端口号即可,所有日志都会发送到该台服务器上,实际请求的日志会转发到其他的api上

Consul

  • 一个服务下面可以启动多个实例,收到请求会平均发送给每一个实例
  • 服务发现:请求服务时只需得知服务名称、consul的ip与端口号即可,无需知道服务具体细节
  • 健康检查:服务注册后consul每间隔一段时间发送响应给服务的实例,确认在线情况
  • 服务注册:服务向consul报告自己的ip和端口号

Consul代码示例

api端

服务注册:

registerClient := consul.NewRegistryClient(global.GlobalConfig.Consul.Address, global.GlobalConfig.Consul.Port)err = registerClient.Register(global.GlobalConfig.MainServer.Address, global.GlobalConfig.MainServer.Port, "video-api", []string{"api", "video"})

健康检查(注意是HTTP类型的):

check := &api.AgentServiceCheck{HTTP:                           "http://" + address + ":" + port + "/health",Timeout:                        "5s",Interval:                       "5s",DeregisterCriticalServiceAfter: "10s",}

连接服务端:

conn, err = grpc.Dial("consul://"+global.GlobalConfig.Consul.Address+":"+global.GlobalConfig.Consul.Port+"/"+name+"?wait=14s",grpc.WithTransportCredentials(insecure.NewCredentials(),),grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*size),grpc.MaxCallSendMsgSize(1024*1024*size),),grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),)

服务端

注意监听的时候要监听内网地址

lis, err := net.Listen("tcp", global.GlobalConfig.Address.In+":"+port)

服务注册:

register_client := consul.NewRegistryClient(global.GlobalConfig.Consul.Address, global.GlobalConfig.Consul.Port)register_client.Register(global.GlobalConfig.Address.Out, port, name, []string{"srv", "video"})

健康检查(注意是GRPC类型的):

grpc_health_v1.RegisterHealthServer(s, health.NewServer())
check := &api.AgentServiceCheck{GRPC:                           address + ":" + port,Timeout:                        "5s",Interval:                       "5s",DeregisterCriticalServiceAfter: "10s",}

连接api端:

s := grpc.NewServer()
]]>
+ + + + + Study + + + + + + + Backend + + Consul + + Kong + + + +
+ + + + + 【实践课】手把手教你做系统设计 + + /2023/02/03/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day10/ + + 【实践课】手把手教你做系统设计

【实践课】手把手教你做系统设计

手把手教你做系统设计之秒杀系统

概述

本节课程主要分为四个方面:

  1. 系统设计方法论
  2. 电商秒杀业务介绍
  3. 课程实践
  4. 课程总结

课前部分主要罗列课程中涉及到的中间件和相关背景知识。对于使用到的中间件,同学们需要体验了解概念,安装并正确使用。课中部分会详细讲解系统设计的方法论和秒杀系统实践,帮助同学们入门系统设计。课后部分会做一些总结,梳理核心思想和重点。

课前 (必须)

了解基本的电商概念和流程

  • 电商平台业务
  • 秒杀业务特点

MySQL

  • 安装MySQL,推荐使用MySQL8及以上版本
  • 熟悉ddl,dml等基础语法
  • 了解sql优化

Redis

  • 安装Redis,推荐最新版本
  • 了解Redis的基本数据类型和使用场景
  • 熟悉常用命令
  • 了解Lua脚本的使用
  • 了解Redis分布式锁

RocketMQ

  • 安装RocketMQ,推荐最新版本
  • 了解RocketMQ的基础概念和架构
  • 了解MQ的使用场景
  • 了解生产者如何保证消息的可靠性发送
  • 了解消费者如何保证幂等
  • 了解消费者pull和push模式的区别

OpenResty

  • 安装OpenResty,推荐最新版本
  • 了解Nginx的基础概念和使用
  • 了解Lua脚本的语法

Linux

  • 熟悉常用命令
  • 熟悉进程和线程
  • 了解Linux调优

Java

  • 按照JDK,推荐JDK11
  • 熟悉Java基础语法和lambda表达式
  • 熟悉idea的使用
  • 了解并发编程
  • 了解springboot框架的使用
  • 了解maven的使用

Jmeter

  • 安装Jmeter
  • 了解使用Jmeter压测

课中

引言

  • 为什么要做系统设计
    • 个人?
    • 工作?
  • 系统设计的概念是什么
  • 如何做系统设计
    • 4S分析法
  • 如何分析系统瓶颈和优化
    • 火焰图分析
    • 链路分析
    • 全链路压测
  • 如何验证系统的可用性和稳定性
    • 链路梳理
    • 可观测性
    • 全链路测试
    • 稳定性控制
    • 容灾演练

电商和秒杀

基本概念

  • Spu
  • Sku
  • 秒杀业务的特点

秒杀的挑战

  • 资源有限性
  • 反欺诈
  • 高性能
  • 防止超卖
  • 流量管控
  • 扩展性
  • 鲁棒性

设计秒杀系统

4S分析

  • 场景
  • 存储
  • 功能
  • 扩展

系统架构图

实践

秒杀流程

总结

高性能系统的通用设计思想

课后

  • 秒杀课程的总结
  • 秒杀系统的扩展
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 微服务框架 - 不变的基建 + + /2023/02/02/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day09/ + + 微服务框架 - 不变的基建

微服务框架 - 不变的基建

概述

本课程内容主要分为以下4个方面:

  • 微服务架构介绍

    • 微服务架构的背景由来、架构概览、基本要素
  • 微服务架构原理及特征

    • 微服务架构的基本组件、工作原理、流量特征
  • 核心服务治理功能

    • 核心的服务治理功能,包括流量治理、服务均衡、稳定性治理
  • 字节跳动服务治理实践

    • 字节跳动在微服务架构稳定性治理中,对请求重试策略的探索及实践

为了帮助大家更好地预习及理解本节课程,该学员手册列出了课前、课中、及课后这三个阶段所涉及到的专业内容大纲,其中课前部分供同学们提前预习参考,课中部分给出了课程大纲,帮助同学们整理思路,课后部分列出一些扩展性的问题让同学们进一步延伸思考。

课前

微服务架构介绍

  • 系统架构的演进历史

    • 单体架构
    • 垂直应用架构
    • 分布式架构
    • SOA架构
    • 微服务架构
  • 微服务架构的三大要素

    • 服务治理
    • 可观测性
    • 安全

微服务架构原理及特征

  • 微服务架构中的基本概念及组件

    • 服务、实例…
  • 服务间通信

    • RPC、HTTP
  • 服务注册及服务发现

核心服务治理功能

  • 服务发布

    • 蓝绿部署
    • 灰度发布(金丝雀发布)
  • 流量治理

  • 负载均衡

    • Round Robin
    • Ring Hash
    • Random
  • 稳定性治理

    • 限流
    • 熔断
    • 过载保护
    • 降级

字节跳动服务治理实践

  • 请求重试的意义
  • 请求重试的难点

课中

微服务架构介绍

系统架构的演进历史

  • 单体架构

    • All in one process
  • 垂直应用架构

    • 按照业务线垂直划分
  • 分布式架构

    • 抽出与业务无关的公共模块
  • SOA架构

    • 面向服务
  • 微服务架构

    • 彻底的服务化

微服务架构概览

  • 网关
  • 服务配置和治理
  • 链路追踪和监控

微服务架构的三大要素

  • 服务治理(本课程内容)

    • 服务注册
    • 服务发现
    • 负载均衡
    • 扩缩容
    • 流量治理
    • 稳定性治理
  • 可观测性

    • 日志采集
    • 日志分析
    • 监控打点
    • 监控大盘
    • 异常报警
    • 链路追踪
  • 安全

    • 身份验证
    • 认证授权
    • 访问令牌
    • 审计
    • 传输加密
    • 黑产攻击

微服务架构原理及特征

微服务架构中的基本概念及组件

  • 服务

    • 一组具有相同逻辑的运行实体
  • 实例

    • 一个服务中的每个运行实体
  • 实例与进程的关系

    • 没有必然对应关系,一般一对一或者一对多
  • 常见的实例承载形式

    • 进程、VM、k8s pod…

服务间通信

  • 微服务之间通过网络进行通信
  • 常见的通信协议包括 HTTP、RPC

服务注册及服务发现

  • 基本问题

    • 服务间调用中,如何指定下游服务实例的地址?
  • 简单方案

    • 直接指定 ip:port?
      • 没有任何动态能力
      • 有多个实例下游实例怎么办?
    • 使用 DNS?
      • 本地 DNS 存在缓存,导致延迟
      • DNS 没有负载均衡
      • 不支持服务探活检查
      • DNS 不能指定端口
  • 服务注册发现

    • 新增一个统一的服务注册中心,用于存储服务名到服务实例之间的映射关系
    • 旧服务实例下线前,从服务注册中心删除该实例,下线流量
    • 新服务实例上线后,在服务注册中心注册该实例,上线流量
  • 微服务流量特征

    • 统一网关入口
    • 外网通信多数采用 HTTP,内网通信多数采用 RPC(Thrift, gRPC)

核心服务治理功能

服务发布

  • 何为服务发布

    • 让一个服务升级运行新的代码的过程
  • 服务发布难点

    • 服务不可用
    • 服务抖动
    • 服务回滚
  • 蓝绿部署

    • 将服务分成两个部分,分别先后发布
    • 简单、稳定
    • 但需要两倍资源
  • 灰度发布(金丝雀发布)

    • 先发布少部分实例,接着逐步增加发布比例
    • 不需要增加资源
    • 回滚难度大,基础设施要求高

流量治理

  • 流量控制

    • 在微服务架构中,可以从各个维度对端到端的流量在链路上进行精确控制
  • 控制维度

    • 地区维度
    • 集群维度
    • 实例维度
    • 请求维度

负载均衡

  • Round Robin
  • Random
  • Ring Hash
  • Least Request

稳定性治理

  • 限流

    • 限制服务处理的最大 QPS,拒绝过多请求
  • 熔断

    • 中断请求路径,增加冷却时间从而让故障实例尝试恢复
  • 过载保护

    • 在负载高的实例中,主动拒绝一部分请求,防止实例被打挂
  • 降级

    • 服务处理能力不足时,拒绝低级别的请求,只响应线上高优请求

字节跳动服务治理实践

  • 请求重试的意义

    • 本地函数调用
      • 通常没有重试意义
    • 远程函数调用
      • 网络抖动、下游负载高、下游机器宕机…
      • 重试是有意义的,可以避免偶发性的错误,提高 SLA
    • 重试的意义
      • 降低错误率
      • 降低长尾延时
      • 容忍暂时性错误
      • 避开下游故障实例
  • 请求重试的难点

    • 幂等性
      • POST 请求可以重试吗?
    • 重试风暴
      • 随着调用链路的增加,重试次数呈指数级上升
    • 超时设置
      • 假设调用时间一共1s,经过多少时间开始重试?
  • 重试策略

    • 限制重试比例
      • 设定一个重试比例阈值(例如 1%),重试次数占所有请求比例不超过该阈值
    • 防止链路重试
      • 返回特殊的 status code,表示“请求失败,但别重试”
    • Hedged Requests
      • 对于可能超时(或延时高)的请求,重新向另一个下游实例发送一个相同的请求,并等待先到达的响应
  • 重试效果验证

    • 字节跳动重试组件能够极大限制重试发生的链路放大效应

课后

  1. 结合 CAP 等原理,思考微服务架构有哪些缺陷?
  2. 微服务是否拆分得越“微”越好?为什么?
  3. Service Mesh 这一架构是为了解决微服务架构的什么问题?
  4. 有没有可能有这样一种架构,从开发上线运维体验上是微服务,但实际运行又类似单体服务?

参考文献

  1. A Design Analysis of Cloud-based Microservices Architecture at Netflix
  2. 字节跳动微服务架构体系演进
  3. 微服务架构的一知半解
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 分布式理论 - 现代架构基石 + + /2023/01/31/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day08/ + + 分布式理论 - 现代架构基石

分布式理论 - 现代架构基石

概述

本节课程主要分为6个方面:

  1. 概述
  2. 系统模型
  3. 理论基础
  4. 分布式事务
  5. 共识协议
  6. 分布式实践

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前 (必须)

概述

  • 什么是分布式?
  • Why-How-What
  • 常见的分布式系统

系统模型

  • 故障模型
  • 拜占庭将军问题
  • 共识和一致性
  • 时间和事件顺序

理论基础

  • CAP理论
  • ACID理论
  • BASE理论

分布式事务

  • 两阶段提交
  • 三阶段提交
  • MVCC

共识协议

  • Quorum NWR模型
  • RAFT协议
  • Paxos协议

分布式实践

  • MapReduce
  • 分布式KV

课中

概述

  • 什么是分布式?
    • 分布式系统定义:跨多个节点的计算机程序的集合
    • 使用分布式系统的五大优势:去中心化、低成本、弹性、资源共享、可靠性高
    • 分布式系统的挑战:故障、网络、环境、安全
  • Why-How-What
    • 使用者视角:大规模计算存储的述求
    • 学习者视角:后端开发必备技能
  • 常见的分布式系统
    • 分布式存储:GFS、Ceph、HDFS、Zookeeper
    • 分布式数据库:Spanner、TiDB、HBase、MangoDB
    • 分布式计算:Hadoop、YARN、Spark

系统模型

故障模型

  • 六种故障模型,从处理的难易程度分类
    • Byzantine failure:节点可以任意篡改发送给其他节点的数据,是最难处理的故障
    • Authentication detectable byzantine failure (ADB):节点可以篡改数据,但不能伪造其他节点的数据
    • Performance failure:节点未在特定时间段内收到数据,即时间太早或太晚
    • Omission failure:节点收到数据的时间无限晚,即收不到数据
    • Crash failure:节点停止响应,持续性的故障
    • Fail-stop failure:错误可检测,是最容易处理的故障
  • 故障模型举例,按照模型分类
    • 磁盘、主板、交换机、网络分区、cpu、内存、线缆、电源等故障详细说明

拜占庭将军问题

  • 两将军问题
    • 定义:
      • 两支军队的将军只能派信使穿越敌方领土互相通信,以此约定进攻时间。该问题希望求解如何在两名将军派出的任何信使都可能被俘虏的情况下,就进攻时间达成共识
    • 结论:
      • 两将军问题是被证实无解的电脑通信问题,两支军队理论上永远无法达成共识
    • TCP是两将军问题的一个工程解
  • 三将军问题:
    • 两个“忠将”A和B,一个“叛徒”C,互相传递消息,消息可能丢失,也可能被篡改,当有一个将军是“叛徒”(即出现拜占庭故障)时,整个系统无法达成一致。
    • 由于“叛徒”C的存在,将军A和将军B获得不同的信息。这样将军A获得2票进攻1票撤退的信息,将军B获得1票进攻2票撤退的信息,产生了不一致
  • 四将军问题:
    • 将军D作为消息分发中枢,约定如果没收到消息则执行撤退
    • 步骤:
      • 如果D为“叛徒”,ABC无论收到任何消息,总能达成一致
      • D为“忠将”,ABC有2人将D的消息进行正确的传递,同样能保证最终决策符合大多数。
    • 进而能够证明,当有3m+1个将军,m个“叛徒”时,可以进行m轮协商,最终达成一致

共识和一致性

  • 不同客户端A和B看到客户端C写入,因为时机的不同,产生数据读取的偏差。引导出最终一致性的详细说明
  • 要保证所有客户端看到相同的值,需要多节点进行“协商”,达成共识,来保证线性一致性
  • 一致性和可用性是对矛盾

时间和事件顺序

  • 1978年Leslie Lamport发表《Time, Clocks, and the Ordering of Events in a Distributed System》
    • 定义了计算机系统中的时间和事件顺序,引入happened before和并发的定义,可以以此对分布式系统中的事件进行推导
    • 根据上述推导,创造了Lamport逻辑时钟的概念,这个概念在分布式理论中具有革命性的意义,帮助我们在一系列分布式事件当中梳理出逻辑的先后关系。利用逻辑时钟,我们可以对整个系统中的事件进行全序排序

理论基础

CAP理论

  • CAP的定义,分别代表一致性、可用性、分区容错性。三者无法同时达到
  • CAP诞生了三类系统:
    • CA系统:传统数据库的代表
    • AP系统:放弃强一致性,保证高可用,不少nosql存储系统采用
    • CP系统:放弃可用性,保证数据一致性
  • 举例说明两个分布式进程之间同步数据,当出现故障的时候,如何选择不同的CAP系统,以及带来的影响
    • CP系统:故障发生时,为了避免读到不一致的数据,可能拒绝访问
    • AP系统:故障发生时,为了保证可用性,允许不同进程读到不同的数据
  • 针对故障场景,可以通过故障转移的方式,做一个相对较优的解决方式:
    • 允许一个进程作为Master,其他进程作为Backup,当故障时将请求转移给Backup进行处理

ACID理论

  • ACID理论是针对CA系统而言的,通常在数据库中具有广泛意义
  • 事务是数据库系统中非常重要的概念,它是数据库管理系统执行过程中的一个逻辑单元,它能够保证一个事务中的所有操作要么全部执行,要么全都不执行
  • 数据库事务拥有四个特性ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)

BASE理论

  • BASE理论是针对AP系统而言的,其来源于对大型互联网分布式实践的总结
    • Basically Available(基本可用):假设系统,出现了不可预知的故障,但还是能用
    • Soft state(软状态):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性
    • Eventually consistent(最终一致性):数据最终一定能够达到一致的状态

分布式事务

二阶段提交

  • 定义:
    • 二阶段提交(Two-phase Commit):为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种演算法。
  • 三个假设:
    • 协调者和参与者进行通信
    • 预写式日志被保持在可靠的存储设备上
    • 所有节点不会永久性损坏,即使损坏后仍然可以恢复
  • 正常流程:Prepare阶段和Commit阶段
  • 异常流程:Prepare阶段失败 -> 回滚;协调者宕机 -> 重新启用新的协调者;双故障重启 -> 数据库管理员介入
  • 两阶段提交需解决的问题:
    • 性能问题:需要多次网络通信,资源需要等待并锁定
    • 新协调者:如何确定状态选出新协调者
    • Commit阶段网络分区带来的数据不一致:非所有节点都收到Commit请求
  • 两个思考:
    • 日志被保存在「可靠」的存储设备上。如何保证这一点?
    • 参与者Commit了,但Ack信息协调者没收到。怎么办?

三阶段提交

  • 针对两阶段提交的补充,将两阶段提交中的Prepare阶段,拆成两部分:CanCommit和PreCommit机制
  • CanCommit阶段:询问是否可以执行;PreCommit阶段:重新确认是否可以执行
  • DoCommit阶段:向所有人提交事务

MVCC

  • MVCC:多版本并发控制的方法。维持一个数据的多个版本使读写操作没有冲突。所以既不会阻塞写,也不阻塞读。提高并发性能的同时也解决了脏读的问题。
  • 悲观锁和乐观锁
    • 悲观锁:操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据
    • 乐观锁:不会上锁,只是在执行更新时判断别人是否修改数据,只有冲突时才放弃操作
  • 版本的选取:使用物理时钟或逻辑时钟
    • 物理时钟:提供TrueTime API,有Master节点维持一个绝对时间,保证各个服务器之间时钟误差控制在ϵ内,通常ϵ<7ms。
    • 逻辑时钟:中心化授时的方式–时间戳预言机(TSO),好处是无需硬件的支持

共识协议

Quorum NWR模型

  • 三要素:
    • N:在分布式存储系统中,有多少份备份数据
    • W:代表一次成功的更新操作要求至少有w份数据写入成功
    • R: 代表一次成功的读数据操作要求至少有R份数据成功读取
    • 为了保证强一致性,需要保证 W+R>N
  • Quorum NWR模型将CAP的选择交给用户,是一种简化版的一致性模型
  • 引起的并发更新问题
    • 如果允许数据被覆盖,则并发更新容易引起一致性问题

RAFT协议

  • 概述
    • Raft协议是一种分布式一致性算法(共识算法),即使出现部分节点故障,网络延时等情况,也不影响各节点,进而提高系统的整体可用性。Raft是使用较为广泛的分布式协议。
  • 三种角色
    • Leader - 领导者:Leader 负责处理所有的客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后,通知Follower提交日志
    • Follower - 跟随者:接受并持久化Leader同步的日志,在Leader告知日志可以提交后,提交日志
    • Candidate - 备选者:Leader选举过程中的临时角色。向其他节点发送请求投票信息
  • 四种定义:
    • Log(日志):节点之间同步的信息,以只追加写的方式进行同步,解决了数据被覆盖的问题
    • Term(任期号):单调递增,每个Term内最多只有一个Leader
    • Committed:日志被复制到多数派节点,即可认为已经被提交
    • Applied:日志被应用到本地状态机:执行了log中命令,修改了内存状态
  • 状态转移:
  • Leader选举过程:
    • 初始全部为Follower
    • Current Term + 1
    • 选举自己
    • 向其它参与者发起RequestVote请求,retry直到
      • 收到多数派请求,成为Leader,并发送心跳
      • 收到其它Leader的请求,转为Follower,更新自己的Term
      • 收到部分,但未达到多数派,选举超时,随机timeout开始下一轮
  • Log Replication过程:
    • 新Leader产生,Leader和Follower不同步,Leader强制覆盖Followers的不同步的日志
  • 切主:当Leader出现问题时,就需要进行重新选举
    • Leader发现失去Follower的响应,失去Leader身份
    • 两个Follower之间一段时间未收到心跳,重新进行选举,选出新的Leader,此时发生了切主
    • Leader自杀重启,以Follower的身份加入进来
  • Stale读:
    • 发生Leader切换,old leader收到了读请求。如果直接响应,可能会有Stale Read

Paxos协议

  • Paxos算法与RAFT算法区别:
    • Multi-Paxos 可以并发修改日志,而Raft写日志操作必须是连续的
    • Multi-Paxos 可以随机选主,不必最新最全的节点当选Leader
  • 优劣势
    • 优势:写入并发性能高,所有节点都能写
    • 劣势:没有一个节点有完整的最新的数据,恢复流程复杂,需要同步历史记录

分布式实践

MapReduce

  • 设计一个简易的MapReduce系统,思考如何应对故障?

分布式KV

  • 设计一个简易的分布式键值系统,要求具备弹性的能力和达成线性一致

课后

  1. 分布式系统有哪些优势和挑战?
  2. 两将军问题为什么理论上永远达不成共识?
  3. 为什么TCP采用三次握手?而不是两次和四次?
  4. 为什么在4将军问题中,增加1轮协商就可以对抗拜占庭故障?
  5. 什么是最终一致性?什么是线性一致性?
  6. CAP理论中,请举例说明可用性和一致性的矛盾?
  7. 数据库里的一致性和分布式系统中的一致性有什么区别?
  8. 两阶段提交中,什么场景需要数据库管理员介入?
  9. 三阶段提交缓和两阶段提交的哪两个问题?
  10. 什么场景适合乐观锁?什么场景适合悲观锁?
  11. 在共识协议中,为什么说允许数据被覆盖会带来数据一致性问题?
  12. RAFT协议中,Leader写成功日志Log20但未同步给Followers后宕机,Follower重新选举后产生一条新日志Log20,这时Leader重启,整个系统发现两种不一样的Log20的记录,请问如何区分并拒掉前面的Log20?
  13. RAFT协议中,Stale读是如何产生的?该如何解决Stale读的问题?
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 架构初探 - 谁动了我的蛋糕 + + /2023/01/30/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day07/ + + 架构初探 - 谁动了我的蛋糕

架构初探 - 谁动了我的蛋糕

使用指南

为了帮助同学们更好地理解本课程,我为大家准备了本学员手册。它包含以下几大模块内容:

  • 课程目标,本课程主要框架的简单介绍,便于同学们抓住课程的框架结构,把握听课节奏;
  • 课前,本课程的重要前置知识点,便于同学们在听课过程中快速理解、跟紧思路;
  • 课中,本课程各章节涉及的关键概念和知识点,帮助同学们加深核心内容的理解和认识;
  • 课后,本课程的内容提炼,便于同学们总结课程要点,争取达到举一反三的效果。

课程目标

本课程的包含以下四个方面:

  • 什么是架构
    • 围绕架构的定义和演进两部分内容展开
  • 企业级后端架构剖析
    • 详细介绍企业级后端架构的形态
  • 企业级后端架构的挑战
    • 企业级架构都面临着哪些挑战,如何解决
  • 后端架构实战
    • 结合前三部分的知识点,以第三部分中的一个挑战为例,讲解如何做架构设计

课前

什么是架构

常见软件架构:

  • 单机
  • 单体
  • 垂直应用
  • SOA (Service Oriented Architecture)
  • 微服务 (Microservice)

一些小问题:

  • 如何给架构下定义?
  • 架构的重要性?
  • 架构演进的初衷?
  • 架构演进的思路?

企业级后端架构剖析

  • 云计算
    • 基础
      • 虚拟化
      • 编排
    • 架构
      • IaaS
      • SaaS
      • PaaS
      • FaaS
  • 云原生
    • 弹性资源
      • 计算资源
      • 存储资源
    • 微服务架构
      • 通信协议
      • 中间件
    • DevOps
      • 软件生命周期
    • 服务网格

企业级后端架构的挑战

  • 离线任务
  • 在线任务
  • IO 密集型
  • CPU 密集型
  • 服务治理
  • IPC (Inter-Process Communication)
  • RPC (Remote Procedure Call)

后端架构实战

  • 负载均衡 Load Balancing
  • 服务发现 Service Discovery
  • 服务注册 Service Registry
  • 宿主机 Host
  • 容器 Container
  • 时序数据 Time Series
  • 一致性哈希 Consistent Hash

课前思考题

  1. 软件架构演进至今都有哪些形态?它们分别解决了什么问题?仍然存在什么问题?
  2. 云计算有哪些基础技术?云计算服务的形态又有哪些?
  3. 云原生是什么?它跟云计算的关系是?
  4. 云原生的代表技术有哪些?
  5. 企业级后端架构面临的挑战有哪些?

课中

什么是架构

架构定义

Q:如何给架构下定义?

A:架构,又称软件架构:

  • 是有关软件整体结构与组件的抽象描述
  • 用于指导软件系统各个方面的设计

Q:架构的重要性?

A:那盖房子来做举例子。

我们都知道,地基对于一栋楼房的主要性,架构对于一个软件的重要性也是类似的:

  • 架构没设计好,软件容易崩,用户体验上不去。最终要么重构,要么放弃
  • 架构设计好了,软件的稳定性上去了,用户体验高了,口碑一点点就打造出来了
  • 良好的架构基础,也为软件的未来发展提供了更多的可能。为用户赋能,实现自身价值

单机架构

All in one,所有的东西都在一个进程里,部署在一个机器上。

优点:

  • 简单

缺点:

  • 运维需要停服,用户体验较差
  • 承载能力有限。了解下 c10k 问题

单体架构

在单机架构的基础上,将进程部署到多个机器上。

优点:

  • 具备水平扩容能力
  • 运维不需要停服

缺点:

  • 后端进程职责太多,越来越臃肿
  • 爆炸半径较大,进程中一个很小的模块出现问题,都可能导致整个进程崩溃

垂直应用架构

在单机架构基础上,将进程按照某种依据切分开。比如,A 软件和 B 软件的后端原先采用单机架构部署,那就是一个进程部署在多个机器上;如果用垂直应用架构,可以将 A 和 B 的后端拆分为 A、B 两个进程,然后再按照单体模式的思路,部署在多个机器上。

优点:

  • 一定程度上减少了后端进程职责
  • 一定程度上缩小爆炸半径

缺点:

  • 没有根本解决单体架构的问题

SOA (面向服务架构)

SOA 架构中,服务为一等公民,将进程按照不同的功能单元进行抽象,拆分为『服务』。有了服务之后,SOA 还为服务之间的通信定义了标准,保证各个服务之间通讯体验的一致性。

优点:

  • 各服务的职责更清晰
  • 运维粒度减小到服务,爆炸半径可控

缺点:

  • ESB (企业服务总线) 往往需要一整套解决方案

微服务

在 SOA 架构中,ESB 起到了至关重要的作用。但从架构拓扑来看,它更像是一个集中式的模块。有一个 SOA 分布式演进的分支,最终的形态便是微服务。

优点:

  • 兼具 SOA 解决的问题
  • 服务间的通信更敏捷、灵活

缺点:

  • 运维成本

小结

  • 架构演进的初衷:满足软件迭代诉求,提高迭代效率
  • 架构演进的思路:垂直切分——分布式,水平切分——分层/模块化

企业级后端架构剖析

云计算

云计算基础:

  • 虚拟化技术
    • 硬件层面(VM 虚拟机)- KVM/Xen/VMware
    • 操作系统层面(Container 容器)- LCX/Docker/Kata Container
    • 网络层面 - Linux Bridge/Open v Switch
  • 编排方案
    • VM - OpenStack/VMWare Workstation
    • Container - Kubernetes/Docker Swarm

云计算架构:

  • 云服务
    • IaaS - 云基础设施,对底层硬件资源池的抽象
    • PaaS - 基于资源池抽象,对上层提供的弹性资源平台
    • SaaS - 基于弹性资源平台构建的云服务
    • FaaS - 更轻量级的函数服务。好比 LeetCode 等 OJ,刷题时只需要实现函数,不需要关注输入输出流
  • 云部署模式(拓展)
    • 私有云 - 企业自用
    • 公有云 - AWS/Azure/Google Cloud/Huawei
    • 混合云

云原生

云原生,实际是云原生(计算)的简称,它是云计算发展到现在的一种形态。

云原生技术为组织(公司)在公有云、自由云、混合云等新型的动态环境中,构建和运行可弹性拓展的应用提供了可能。 它的代表技术:

  • 弹性资源
  • 微服务架构
  • DevOps
  • 服务网格
弹性资源

基于虚拟化技术,提供的可以快速扩缩容的能力。可以分为弹性计算资源和弹性存储资源两个方面。

弹性计算资源:

  • 计算资源调度
    • 在线计算 - 互联网后端服务
    • 离线计算 - 大数据分析。Map-Reduce/Spark/Flinnk
  • 消息队列
    • 在线队列 - 削峰、解耦
    • 离线队列 - 结合数据分析的一整套方案,如 ELK

弹性存储资源:

  • 经典存储
    • 对象存储 - 视频、图片等。结合 CDN 等技术,可以为应用提供丰富的多媒体能力
    • 大数据存储 - 应用日志、用户数据等。结合数据挖掘、机器学习等技术,提高应用的体验
  • 关系型数据库
  • 元数据
    • 服务发现
  • NoSQL
    • KV 存储 - Redis
    • 文档存储 - Mongo

在云原生的大背景下,不论是计算资源还是存储资源,他们都像是服务一样供用户使用。

微服务架构

微服务架构下,服务之间的通讯标准是基于协议而不是 ESB 的。

  • HTTP - H1/H2
  • RPC - Apache Thrift/gRPC

如何在 HTTP 和 RPC 之间选择?

  • 性能 - RPC 协议往往具备较好的压缩率,性能较高。如 Thrift, Protocol Buffers
  • 服务治理 - RPC 中间件往往集成了丰富的服务治理能力。如 熔断、降级、超时等
  • 可解释性 - HTTP 通信的协议往往首选 JSON,可解释性、可调试性更好
服务网格

什么是服务网格?

  • 微服务之间通讯的中间层
  • 一个高性能的 4 层网络代理
  • 将流量层面的逻辑与业务进程解耦

没有什么是加一层代理解决不了的问题,服务网格相比较于 RPC/HTTP 框架:

  • 实现了异构系统治理体验的统一化
  • 服务网格的数据平面代理与业务进程采取进程间通信的模式,使得流量相关的逻辑(包含治理)与业务进程解耦,生命周期也更容易管理

企业级后端架构的挑战

挑战

基础设施层面:

Q:我们总说,云是弹性的,也就是说,在用户的角度,云提供的资源是无限的。然而,云背后的物理资源是有限的。在企业级后端架构里,云如何解决近乎无限的弹性资源和有限的物理资源之间的矛盾?

Q:闲事的资源就这么空着呢?如何提高资源利用率,提高物理资源的价值转换率?

用户层面:

Q:上了云原生微服务后,服务之间的通信开销较大,应该如何做成本优化?

Q:微服务看起来没有那么美好,抖动导致的运维成本较高,如何解决?

Q:异构的物理环境应该对用户是透明的,如何屏蔽这些细节?

离在线资源并池

考虑到在线业务的 潮汐性 ,物理资源的用量不是一成不变的。离在线资源并池,可以:

  • 提高物理资源利用率
  • 提供更多的弹性资源

image.png

微服务亲合性部署

微服务之间的通信成本较高,是否可以:

  • 形态上是微服务架构
  • 通信上是单体架构

亲合性部署,通过将微服务调用形态与资源调度系统结合,将一些调用关系紧密、通信量大的服务部署在同一个机器上,并且使用 IPC 代替 RPC 的方式,降低网络通信带来的开销

流量治理

Q:微服务之间的通信流量为什么需要治理?

Q:都有哪些常用的治理手段?

Q:微服务中心件和服务网格在其中扮演着怎样的角色?

屏蔽异构环境的算力差异

Q:基础设施层往往是个复杂的异构环境,比如,有些机器的 CPU 是英特尔的,而有些是 AMD 的。就算是同一个品牌,也可能是不同代际。如何将这些差异屏蔽掉,使用户尽可能不感知呢?

Q:什么情况下,我们觉得,服务需要扩容了?异构环境会对这个评判标准产生怎样的影响?

后端架构实战

问题

如何设计一个根据主机层面的资源信息,实时进行流量调度的系统,打平不同宿主机异构环境的算力差异。

关键点:

  • 紧急回滚能力
  • 大规模
  • 极端场景

image.png

课后

课后作业-兰师傅蛋糕房要支持线上售卖了!请帮忙做整套系统的架构设计

设计需求:

  1. 多端支持
    1. 微信/支付宝小程序
    2. App
    3. 网页
  2. 使用云原生基础设施
  3. 用户画像很重要
  4. 积极参加妇女节/光棍节等活动

注意: 不需要考虑与做蛋糕相关服务的交互

尾声

  1. 没有最好的架构,只有最合适的架构
  2. 做架构设计
    1. 先从需求出发。要满足什么样的需求?预期规模有多大?
    2. 做足够的业界调研。业界对于类似的需求是怎么做的?有无成熟的方案可以借鉴?直接拿来用有什么问题?
    3. 技术选型。涉及的技术组件是自研,还是使用开源的?
    4. 异常情况。任何时候,都不能做『输入合法』的假设。容灾能力一定要有
  3. 学好架构,是工程师成长的一个重要标志

参考文献

]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + MIT-6.824 Distributed Systems-Lab 2 Raft + + /2023/01/29/6.824/Distributed-Systems-MIT-6.824-Lab-2/ + + MIT-6.824(Spring 2022)Lab 2 Raft

6.824 Lab 2: Raft

简介

https://raft.github.io/

这是构建容错k/v存储系统的一系列实验室中的第一个。这个实验室将实现复制状态机协议Raft。

复制服务通过在多个副本服务器上存储其状态(即数据)的完整副本来实现容错。复制允许服务继续运行,即使某些服务器出现故障(崩溃或网络问题)。挑战在于,故障可能会导致复制副本保存不同的数据副本。

Raft将客户端请求组织成一个序列,称为日志,并确保所有副本服务器都看到相同的日志。每个副本按日志顺序执行客户端请求,并将它们应用于服务状态的本地副本。由于所有活动副本都看到相同的日志内容,因此它们都以相同的顺序执行相同的请求,从而继续具有相同的服务状态。如果服务器出现故障,但稍后恢复,Raft会负责更新其日志。只要有大多数服务器处于活动状态,并且可以相互通信,Raft就会继续运行。如果没有这样的大多数,Raft将会暂时停机,但一旦大多数服务器能够再次通信,Raft就会恢复原来的状态。

在这个实验中,将会把Raft实现为一个Go对象类型,并实现相关的方法,这意味着要在更大的服务中将Raft用作模块。一组Raft实例通过RPC相互通信,以维护复制的日志。Raft接口将支持无限序列的编号命令,也称为日志条目。条目用索引编号进行编号。具有给定索引的日志条目最终会被提交。此时,Raft应该将日志条目发送到更大的服务以供其执行。

您应该遵循扩展的Raft论文中的设计,尤其是图2。您将实现本文中的大部分内容,包括保存持久状态,并在节点发生故障后重新启动后读取该状态。不实现第6节提到的集群成员资格更改。

最具挑战性的部分可能不是实现解决方案,而是调试解决方案。为了帮助解决这一挑战,您可能需要花时间思考如何使实现更易于调试。

我们还提供了一个Raft交互的图表,可以帮助阐明Raft代码如何与上面的层进行交互。

pSdAYtS.jpg

参考资料

Students' Guide to Raft

(几年前编写,特别是2D部分已经发生了变化)

背景

Raft 是一种共识算法,旨在轻松理解。它与Paxos的容错和性能相当。不同的是,它被分解成相对独立的子问题,它干净地解决了所有主要部分的实际系统需求。我们希望Raft可供更广泛的受众使用,并且这些更广泛的受众将是能够开发各种更高质量的基于共识的系统。

可视化网站

与所有分布式共识协议一样,细节很难理解。在没有故障的稳定状态下,Raft 的行为易于理解,并且可以直观地解释。例如,从可视化中很容易看出, 假设没有失败,最终将选出Leader,并且最终,发送给Leader的所有操作都将由Follower按照顺序正确执行。但是,当消息延迟,网络分区或者服务故障,细节变得至关重要。特别是,我们可能一遍又一遍地重复许多错误,仅仅是由于阅读论文时的误解或疏忽。这个问题并非Raft所独有。

实现Raft

Raft 的最终指南在 Raft 论文的图 2 中。这个图片指定在Raft服务器之间交换的每个RPC的行为, 给出服务器必须维护的各种不变量,并指定何时应执行某些操作。我们将在本文的其余部分大量讨论图 2。它需要一字不差地遵循。

图 2 定义了每个服务器在各种状态下应该对每个传入的 RPC应该做什么,以及何时发生某些其他事情(例如就像在日志中应用条目是安全的一样)。图 2 非常精确,每一条语句在规范术语中,它应该被视为必须,而不是应该。例如,您可以合理地重置一台服务器的选举计时器,只要您收到或RPC,都表明其他服务器要么认为它是Leader,或者是努力成为Leader。直觉上,这意味着我们不应该干扰。但是,如果您仔细阅读图 2,它会说:如果选举超时过去而没有收到当前Leader的RPC或投票给其他的服务器,则转换为Candidate。

事实证明,区别很重要,因为前一种实现在某些情况下,可能导致活性显著降低。

细节的重要性

考虑一个例子。Raft论文在许多地方提到了心跳RPC。具体来说,领导者将偶尔(每个检测信号间隔至少一次)向所有服务器发送 RPC,以防止它们启动新的选举。如果领导者没有要发送到特定对等方的新条目, RPC 不包含任何条目,被视为心跳。

我们的许多学生认为心跳在某种程度上是“特别的”,当服务器收到心跳时,它应该以不同的方式对待它。特别是,许多人会只在收到心跳时重置他们的选举计时器,然后返回成功,而不执行图2中指定的任何检查。这是极其危险的。通过接受 RPC, Follower隐式地告诉Leader他们的日志与Leader匹配并包括参数中包含的内容。收到回复后,领导可能错误地确定某个条目已被复制到大多数服务器,并开始提交它。

许多人遇到的另一个问题是在收到心跳时,他们会截断Follower的记录,然后添加参数中包含的日志条目。这也是不正确的。图 2说明,如果现有条目与新条目冲突(相同的索引但 不同的任期),删除现有条目及其后面的所有条目。

这里的如果至关重要。如果Follower拥有Leader的所有条目,Follower不得截断其日志。必须保留Leader发送的条目之后的任何元素。这是因为我们可能从Leader收到过期的RPC,截断日志将意味着“收回”我们可能已经告诉Leader的我们的日志。

调试Raft

在调试时,Raft通常有四个主要的错误来源: 活锁、不正确或不完整的 RPC 处理程序、未能遵循规则和术语混淆。死锁也是一个常见问题,但它们通常可以通过记录所有锁和解锁来调试,并且弄清楚你正在占有哪些锁且没有释放。

活锁

当系统活锁时,系统中的每个节点都在执行一些东西,但总的来说,你的节点没有取得进展。一个活锁场景特别频繁出现:没有领导人被选举出来,或者一个领导者被选举出来后另一个节点马上开始选举,迫使最近当选的领导人立即退位。

出现这种情况的原因有很多:

确保在图 2说明的时候准确重置选举计时器。具体来说,有三种情况:

  • 从当前Leader那里获得 RPC (如果参数中的任期已过时,则不应重置计时器)
  • 正在开始选举
  • 向其他服务器投票。

最后一种情况在不可靠的网络中尤其重要,其中Follower可能有不同的日志,在这些情况下, 只有少量的服务器使得大多数服务器都愿意投票支持。如果每当有人要求您投票给他们时都重置选举计时器,会使日志过时的服务器同样有可能向前迈进

事实上,因为很少的服务器有足够的最新的日志,这些服务器不太可能在足够和平的情况下进行选举。如果您遵循图 2,具有最新日志的服务器不会被过时的服务器选举打断,因此更有可能完成选举并成为Leader。

按照图 2 的说明操作了解何时应开始选举。 特别要注意的是,如果您是Candidate,但选举计时器触发,应该开始另一次选举。这对于避免由于 RPC 延迟或丢弃而导致系统停止非常重要。

在处理传入的 RPC 之前 ,请确保遵循“服务器规则”中的第二条规则。第二条规则规定:如果 RPC 请求或响应包含术语set ,则转换为Follower

例如,如果您已经在当前任期内投票,并且传入的RPC有一个更高的任期号,你应该首先下台并采用他们的任期(从而重置),然后处理RPC,处理的过程中就会进行投票

不正确的 RPC 处理程序

尽管图 2 准确地说明了每个 RPC 处理程序应该执行的操作, 一些细节仍然很容易被忽略。

如果步骤显示“回复错误”,这意味着您应该立即回复,不要执行任何后续步骤。

如果你得到一个指向日志末尾的RPC,应该像确实有该条目,但该任期不匹配处理这个。

如果领导者没有发送任何条目,RPC处理程序的检查 2 应执行。

#5 是必要的, 并且需要使用最后一个新条目的索引进行计算。 这是因为日志中可能存在与领导者日志不同的条目。因为 #3 规定您只有在有冲突的条目情况下才会截断日志,这些条目不会被删除,如果超出领导发送给您的条目,您可能会应用不正确的条目。

实施“最新日志”检查非常重要。只是检查长度!

不遵守规则

虽然 Raft 论文非常明确地说明了如何实现每个 RPC 处理程序,它还留下了许多规则的实现和未指定的不变量。这些列在“服务器规则”中 图 2 右侧的块。虽然其中一些是不言自明的,也有一些需要非常小心地设计,以免违反规则:

如果在执行过程中的任何时候应用特定的日志条目。请务必确保仅由一个实体完成此应用程序。具体来说,您需要有一个专门的 “应用器”,或者锁定这些应用,以便其他一些例程不会同时检测到需要应用条目。

确保定期更新,或更新后进行检查。例如,如果您在发送给同行的同时进行检查,您可能需要等到下一个条目追加到日志中后再应用您刚刚发送并得到确认的条目。

如果领导者发出 RPC,并且被拒绝,但不是因为日志不一致(这只有在我们的任期中才会发生),那么您应该立即下台并且不更新。

领导者不允许更新到上一任期(或就此而言,未来任期)的某个地方。因此特别需要检查。这是因为如果这不是他们目前的任期,Raft 领导者无法确定条目是否实际提交(并且将来永远不会更改)。

一个常见的问题来源是nextIndex和matchIndex之间的区别。特别是,你可能会观察到matchIndex = nextIndex - 1,而干脆不实现matchIndex。这是不安全的。虽然nextIndex和matchIndex通常在同一时间被更新为类似的值(具体来说,nextIndex = matchIndex + 1),但两者的作用完全不同。它通常是相当乐观的(我们分享一切),并且只在消极的反应中向后移动。例如,当一个领导者刚刚当选时,nextIndex被设置为日志末尾的索引指数。在某种程度上,nextIndex是用于性能的–你只需要将这些东西发送给这个对等体。

matchIndex是用于安全的。MatchIndex不能被设置为一个太高的值,因为这可能会导致commitIndex被向前移动得太远。这就是为什么matchIndex被初始化为-1(也就是说,我们不同意任何前缀),并且只在跟随者肯定地确认AppendEntries RPC时才更新。

任期混淆

任期混淆是指服务器被来自旧任期的RPC所迷惑。一般来说,在收到RPC时,这不是一个问题,因为图2中的规则确切地说明了当你看到一个旧任期时你应该做什么。然而,图2一般没有讨论当你收到旧的RPC回复时你应该做什么。根据经验,我们发现到目前为止,最简单的做法是首先记录回复中的任期(它可能比你当前的任期高),然后将当前任期与你在原始RPC中发送的任期进行比较。如果两者不同,就放弃回复并返回。只有当这两个任期相同时,你才应该继续处理回复。

一个相关但不完全相同的问题是,预设你的状态在你发送RPC和你收到回复之间没有变化。这方面的一个很好的例子是,当你收到RPC的响应时,设置matchIndex = nextIndex - 1,或者matchIndex = len(log)。这并不安全,因为这两个值都可能在你发送RPC后被更新。相反,正确的做法是将 matchIndex 更新为你最初在 RPC 中发送的参数中 prevLogIndex + len( entries[]) 。

Raft的结构

一个Raft实例必须处理外部事件的到来(Start()调用、AppendEntries和RequestVote RPC以及RPC回复),它必须执行定期任务(选举和心跳)。有许多方法可以构造Raft代码来管理这些活动,下面是一些想法。

  • 每个Raft实例都有一组状态(日志、当前索引、&c) 必须根据在goroutine并行同时发生的事件进行更新。Go文档指出,goroutine可以使用共享数据结构和锁直接执行更新操作,或者通过在channel上传递消息。经验表明,对于Raft使用共享数据和锁是最简单的。
  • Raft实例有两个时间驱动的活动:Leader必须发送心跳信号,如果距离上一次接收到心跳信号的时间太长,其他人必须开始选举。每一个活动最好单独启动一个专门的长时间运行的goroutine,而不是将多个活动组合成一个单独的goroutine
  • 选举超时的管理是很头痛的。最简单的方法是在Raft数据结构中包括上一次Follower接收到Leader消息的时间,然后让负责选举的goroutine定期检查这个时间是否超时。使用time.Sleep()和一个小常量参数驱动定期检查是很容易的。不要使用time.Ticker和time.Timer,它们很难正确使用。
  • 需要有一个单独的长时间运行的goroutine在applyCh上按顺序提交日志条目。它必须是单独的,因为在applyCh上发送可以被阻止;而且必须是单个
    goroutine,否则很难确保发送日志是按照日志顺序的。advance commitIndex的代码需要kick apply goroutine;使用sync.Cond可能最简单。
  • 每个RPC应该以自己的方式发送(并处理其回复)自己的goroutine,原因有两个:这样无法访问的服务器不会延迟大多数回复的收集,而且心跳信号和
    选举计时器可以一直计时。如果RPC应答处理在同一个goroutine中就很容易做到,而不是通过channel发送回复的信息。
  • 请记住,网络可能会延迟RPC和RPC响应,而且如果发送并行的RPC,网络可以对请求和答复进行重新排序。图2很好地指出了RPC处理程序必须对此小心(例如,RPC处理程序应该忽略具有旧日志条目的RPC)。图2并不总是明确说明RPC响应的处理过程。Leader在处理RPC响应时必须小心,它必须检查自从发送RPC之后日志条目没有改变,并且必须考虑对同一Follower的并发的RPC改变了Leader的状态(例如nextIndex)。

Raft中的锁

  1. 当有多个goroutine使用的数据时,且至少有一个goroutine可以修改数据,那么goroutine应该使用锁防止同时使用数据。Go race检测器非常擅长检测违反此规则的情况。
  2. 每当代码对共享数据进行一系列修改时,如果其他goroutine查看了数据,可能会出错,因此在整个过程中都应该使用锁。
  3. 每当代码对共享数据进行一系列读取时(或读取和写入),如果另一个goroutine在中途修改数据,则会发生错误。因此在整个过程中都应该使用锁。真正的Raft代码需要使用很长代码的锁,例如,一个Raft RPC处理程序可能需要在整个处理过程都要加锁。
  4. 在做一些可能会等待的事情的时候都加锁是个坏主意,例如:读取Go channel,在channel上发送,等待计时器、调用time.Sleep()或发送RPC并等待回复。一个原因是你可能想让其他的goroutine在等待期间照常执行。另一个原因是避免死锁。想象两个服务器在保持锁的同时彼此发送RPC;两个RPC
    处理程序需要接收对方的锁;两个RPC处理程序都不能完成,因为它需要等待的RPC调用所持有的锁。等待的代码应该首先释放锁。如果这不方便,有时创建一个单独的goroutine来执行等待是很有用的。
  5. 要小心扔掉和重新获取锁的情况。一个可能出现这种情况的地方是避免带锁等待。例如,下面的发送投票RPC的代码是不正确的:
rf.mu.Lock() rf.currentTerm += 1 rf.state = Candidate for <each peer> {   go func() {     rf.mu.Lock()     args.Term = rf.currentTerm     rf.mu.Unlock()     Call("Raft.RequestVote", &args, ...)     // handle the reply...   } () } rf.mu.Unlock()

这个代码在单独的goroutine中发送每个RPC。这是不正确的,因为如果周围的代码是决定成为Candidate,args.Term可能与rf.currentTerm不同。当周围的代码创建goroutine和当goroutine读取rf.currentTerm时可能过去了很多的时间,这台服务器也可能不再是Candidate。一种方法是当外部代码持有锁的时候创建rf.currentTerm的副本从而让goroutine去使用。同样的,在调用之后的回复处理代码重新获取锁后必须重新检查所有相关的假设,例如,它应该检查自从决定成为Candidate后rf.currentTerm没有再次改变。

一种方法是从没有锁的代码开始,然后仔细考虑需要在哪里添加锁以变得正确。另一个更务实的方法从观察开始,如果没有并发性(没有同时执行goroutine)则根本不需要锁。但是当RPC系统创建goroutine以执行RPC处理程序时,以及
因为您需要在单独的goroutine中发送RPC以避免等待,并发性就有了。可以通过识别所有goroutine开始的位置(RPC处理程序、在Make()中创建的后台goroutine,&c),并且在每个goroutine开始的时候获得锁,只有当goroutine
完全完成并返回的时候才释放锁,从而消除并发性。这个锁定协议确保任何重要的事情都不会并行执行;锁确保每个goroutine在其他goroutine执行之前完成,没有并行执行,很难违反规则1、2、3或5。如果每个goroutine的代码正确,在使用锁抑制并发时仍然是正确的。

然而,规则4可能是一个问题。所以下一步是找到代码等待的位置,然后根据需求添加锁释放和重新获取(或goroutine的创建),记得小心重新建立和重新获取后的情况。

代码相关

框架代码:src/raft/raft.go

测试代码:src/raft/test_test.go,运行go test即可

通过在src/raft/raft.go中增加代码实现Raft,必须遵循下面的接口:

// create a new Raft server instance:rf := Make(peers, me, persister, applyCh)// start agreement on a new log entry:rf.Start(command interface{}) (index, term, isleader)// ask a Raft for its current term, and whether it thinks it is leaderrf.GetState() (term, isLeader)// each time a new entry is committed to the log, each Raft peer// should send an ApplyMsg to the service (or tester).type ApplyMsg

服务调用 Make(peers, me, ...)创建一个 Raft peer。peers 参数是所有 Raft peers(包括这一个)的网络标识符数组,用于 RPC。me参数是网络标识符数组中,属于这个peer的网络标识符的下标。Start(command) 要求 Raft 启动处理,将命令追加到日志副本中。Start()应立即返回,无需等待日志追加完成。该服务希望你将每个新的日志条目,封装为 ApplyMsg,发送给Make函数中的 applyCh参数(这是一个channel)。

raft.go包含发送 RPC sendRequestVote()和处理传入 RPC RequestVote()的样例代码。您的 Raft peers 应该使用 labrpc Go 包(源代码在 src/labrpc)交换 RPC。测试代码可以告诉 labrpc 延迟 RPC请求,重新排列它们,并丢弃它们以模拟各种网络故障。Raft 实例必须仅与 RPC 交互;例如,不允许它们使用共享的 Go 变量或文件进行通信。

后续的实验也在此实验上进行构建。

参考翻译:https://zhuanlan.zhihu.com/p/248686289

Part 2A:选举Leader

指导

实现Raft算法中的Leader选举和心跳机制(AppendEntries RPC 且没有日志条目)。确保只有一个Leader被选中,且若无错误该Leader会一直唯一存在,当该Leader下线或发生其他错误导致发出的数据无法被成功接收,则会产生新的Leader来替代。

  1. 运行 go test -run 2A 来验证代码的正确性
  2. 参考论文的Figure 2实现,需要关注发送和接收RequestVote RPCs,与选举相关的服务器的规则,和与选举相关的服务器的状态
  3. raft.go中添加Figure 2的Leader选举的状态,同时也需要定义一个结构体保留日志条目的信息
  4. 填充 RequestVoteArgsRequestVoteReply结构。修改 Make()以创建一个后台 go 协程,该协程将在一段时间未从其他 peers 那里听到请求投票 RPC 时,发送 RequestVote RPC 来定期启动 Leader 选举。这样,如果已经有一个 Leader,或者自己成为 Leader,其他 peers 就会知道谁是Leader。实现 RequestVote() RPC 函数,以便服务器投票给别人。
  5. 为了实现心跳检测,请提前定义 AppendEntries RPC 结构(尽管您可能还不需要所有参数),并让 Leader 定期发送它们。AppendEntries RPC 函数需要重置选举超时时间,以便其他服务器已当选时,不会以 Leader 的身份继续运行。
  6. 确保不同 Peers 不会在同一时间选举超时,否则所有 Peers 将只为自己投票,没有人会成为 Leader。
  7. 测试要求 Leader 发送心跳检测 RPC 的频率不超过 10 次/秒。
  8. 测试要求您的 Raft 在旧 Leader 失败后5秒内选出新 Leader(如果大多数同行仍然可以沟通)。但是,请记住,在发生分裂投票的情况下(如果数据包丢失或候选人不幸地选择相同的随机回票时间,则可能发生),领导人选举可能需要多轮投票。您必须选择足够短的选举超时(心跳间隔也是如此),确保即使选举需要多次轮断,也能在5秒内完成。
  9. 论文第 5.2 节提到选举超时应该在 150 到 300 毫秒范围内。只有当 Leader 发送一次心跳包的远小于 150 毫秒,这种范围才有意义。由于测试将您发送心跳包的频率限制在 10 次/秒内(译者注:也就是大于 100 毫秒),因此您必须使用比论文 150 到 300 毫秒更大的选举超时时间,但请不要太大,因为那可能导致无法在 5 秒内选出 Leader。
  10. Go 的 rand 很有用。
  11. 您将需要定期执行某些操作,或在一段时间后做些什么。最简单的方法是新起一个协程,在协程的循环中调用time.Sleep()。不要使用 time.Timertime.Ticker,这两个并不好用,容易出错。
  12. 如果代码在通过测试时遇到问题,请再次阅读论文的 Figure 2 ;Leader 选举的逻辑分布在Figure 2 的多个部分。
  13. 别忘了实现 GetState()
  14. 测试调用您的 Raft 的 rf.Kill()时,您可以先调用 rf.killed()再检查是否 Kill()。您可能希望在所有循环中执行此功能,以避免已经死亡的 Raft 实例打印令人困惑的信息。
  15. 调试代码的一个好方法,就是在 Peer 发送或收到消息时打印自己的状态,并在测试时运行 go test -run 2A > out,将日志收集到文件中。然后,通过研究 out 文件,可以确定实现中不正确的地方。您可能会喜欢用 util.go中的 Dprintf函数来调试,其可以在不同情况下打开和关闭日志。
  16. Go RPC 仅发送以大写字母为首的结构体字段(译者注:可导出的字段)。子结构体还必须具有大写字段名称(例如数组中的日志记录字段)。labgob包会警告您这一点,不要忽略警告。
  17. go test -race测试你的代码,并修复它报告的任何问题。

输出应该如下面所示:

$ go test -run 2ATest (2A): initial election ...  ... Passed --   3.5  3   58   16840    0Test (2A): election after network failure ...  ... Passed --   5.4  3  118   25269    0Test (2A): multiple elections ...  ... Passed --   7.3  7  624  138014    0PASSok  6.824/raft16.265s$

每一个“通过”的测试用例会输出五个数字;他们分别是

  1. 测试所用的时间(单位:秒)
  2. Raft Peer 的数量(通常为 3 或 5)
  3. 测试期间发送 RPC 的次数
  4. RPC 消息中的字节总数
  5. Raft 确定并提交的日志条目数。

实现

参考资料

定义 global.go

首先需要对代码中不完整的结构体进行填充,论文中的Figure 2有的字段一定保留,其他的字段看情况保留

首先定义服务器的状态,用字符串常量表示:

// 定义Peer的状态type State stringconst (Follower  State = "follower"Candidate State = "candidate"Leader    State = "leader")

然后定义Raft结构体:

type Raft struct {mu        sync.Mutex          // Lock to protect shared access to this peer's statepeers     []*labrpc.ClientEnd // RPC end points of all peerspersister *Persister          // Object to hold this peer's persisted stateme        int                 // this peer's index into peers[]dead      int32               // set by Kill()// Your data here (2A, 2B, 2C).// Look at the paper's Figure 2 for a description of what// state a Raft server must maintain.// 在所有peer上面的持久性的状态// 在对RPC进行响应之后要在稳定存储上更新currentTerm int // this peer 看到的最新的任期号votedFor    int // 在当前任期获得选票的Candidate的id(如果没有则为-1)log []LogEntry // 日志信息// 在所有peer上面的变化的状态commitIndex int // 已知的已经被提交的日志条目的最大索引值lastApplied int // 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增)// 在Leader上面的变化的状态// 每一次选举后都要重新进行初始化nextIndex  []int // 对于每⼀个服务器,需要发送给他的下⼀个日志条目的索引值(初始化为Leader最后索引值加1)matchIndex []int // 对于每⼀个服务器,已经复制给他的日志的最高索引值// 与时间相关的变量electTimeout     int64 // 选举超时时间randomTimeout    int64 // 随机时间heartBeatTimeout int64 // 心跳周期// 当前状态state        State // 当前Peer所处的状态(Leader、Candidate或Follower)majorityVote int   // 成为Leader需要获得的最少票数lastReceive  int64}

其中多定义了6个变量,3个变量与时间相关,分别表示选举超时时间、随机的时间上限和Leader发送心跳的周期时间

// 与时间相关的变量electTimeout     int64 // 选举超时时间randomTimeout    int64 // 随机时间heartBeatTimeout int64 // 心跳周期

最后3个变量,第1个表示服务器当前所处的状态,第2个表示成为Leader需要获得的最少票数,这个值提前计算出来,最后一个值表示最后一次接收到Leader的心跳信号的时间

// 当前状态state        State // 当前Peer所处的状态(Leader、Candidate或Follower)majorityVote int   // 成为Leader需要获得的最少票数lastReceive  int64 // 最后一次接收到Leader的心跳信号的时间

工具 util.go

服务器不同状态之间的转换比较频繁,因此可以将这些服务器状态转换的代码提取出来编写成工具函数,方便后续直接调用

// 转为Leaderfunc (rf *Raft) toLeader() {DPrintf("[%d]: convert from [%s] to [%s], term [%d]", rf.me, rf.state, Leader, rf.currentTerm)rf.state = Leader// rf.lastReceive = time.Now().Unix()}// 转为Followerfunc (rf *Raft) toFollower(newTerm int) {DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Follower)rf.state = Followerrf.currentTerm = newTermrf.votedFor = -1rf.lastReceive = time.Now().Unix()}// 转为Candidatefunc (rf *Raft) toCandidate() {DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Candidate)rf.state = Candidaterf.currentTerm += 1rf.votedFor = rf.me// rf.lastReceive = time.Now().Unix()}
  1. 转为Leader只需更新自己的状态即可,不需要对其他值做任何的操作。
  2. 转为Follower除更新自己的状态之外,要更新自己的任期(因为变为Follower就是因为自己的任期落后),然后要初始化自己的投票状态,并且这个变化的过程隐含了从Leader那里收到心跳包,因此要更新自己的时间。
  3. 转为Follower除更新自己的状态之外,要将自己的任期+1(因为变为Candidate是因为接收不到Leader的心跳信息了,认为Leader已经挂了,这个任期不能再用了),然后要初始化自己的投票投给自己。

然后补充一个预定义的获取服务器状态的方法

// return currentTerm and whether this server// believes it is the leader.func (rf *Raft) GetState() (int, bool) {var term intvar isleader bool// Your code here (2A).rf.mu.Lock()defer rf.mu.Unlock()isleader = falseterm = rf.currentTermif rf.state == Leader {isleader = true}return term, isleader}

请求投票RPC requestVote.go

结构体定义完全按照论文即可,目前不需要其他字段

// example RequestVote RPC arguments structure.// field names must start with capital letters!type RequestVoteArgs struct {// Your data here (2A, 2B).Term         int // Candidate的任期号CandidateId  int // Candidate的 IdLastLogIndex int // Candidate最后一条日志条目的索引LastLogTerm  int // Candidate最后一条日志条目的任期}// example RequestVote RPC reply structure.// field names must start with capital letters!type RequestVoteReply struct {// Your data here (2A).Term        int  // 当前的任期,接收到了之后Candidate可以更新自己VoteGranted bool // 是否给这个Candidate投票}

核心RPC:

// example RequestVote RPC handler.func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {// Your code here (2A, 2B).// RPC 请求不一定在什么时候应用,因此必须加锁rf.mu.Lock()defer rf.mu.Unlock()DPrintf("[%d]: received vote request from [%d]", rf.me, args.CandidateId)reply.VoteGranted = false// 如果参数的任期号还没有我的大,不投票,直接默认值返回即可if args.Term < rf.currentTerm {// 响应中包含当前自己的任期号reply.Term = rf.currentTermreturn}// 如果参数的任期号比我的大,则我在这个任期内就只能是它的Follower,则更改我的任期号,而且在这个任期内我要投票给它if args.Term > rf.currentTerm {rf.toFollower(args.Term)}reply.Term = rf.currentTerm // 注意这里任期号已经变化了,因此要重新赋值DPrintf("[%d]: status: term [%d], state [%s], vote for [%d]", rf.me, rf.currentTerm, rf.state, rf.votedFor)// 如果参数的任期号和我的相同,则任期号不变,需要通过日志确定是否投票给它// 这里论文要求的 rf.VotedFor == args.CandidateId 不是很明白if rf.votedFor == -1 || rf.votedFor == args.CandidateId {// Todo:判断日志是否至少更新才可以投票rf.votedFor = args.CandidateIdrf.lastReceive = time.Now().Unix() // 更新时间,上面操作相当于与可能的Leader通信过了reply.VoteGranted = trueDPrintf("[%d]: voted to [%d]", rf.me, args.CandidateId)}}

核心就是计算返回的reply中的两个值,第一个是是否投票,第二个是当前服务器的任期号。其中任期号一定要小心,可能服务器自己的状态改变后任期号会随之改变,因此一定要及时更新。

  1. 如果请求我投票的任期号还没有我的大,不投票,直接默认值返回即可
if args.Term < rf.currentTerm {// 响应中包含当前自己的任期号reply.Term = rf.currentTermreturn}
  1. 如果参数的任期号比我的大,则我在这个任期内就只能是它的Follower,则更改我的任期号,而且在这个任期内我要投票给它
if args.Term > rf.currentTerm {rf.toFollower(args.Term)}

(这个结构不返回,投票的逻辑在下一个结构)

  1. 如果参数的任期号和我的相同,则任期号不变,需要通过日志确定是否投票给它

rf.votedFor == -1 承接上面的投票逻辑,把情况2的票投了

rf.VotedFor == args.CandidateId 在后面要加上对于日志的判断,这里仅仅是简单投票给它

if rf.votedFor == -1 || rf.votedFor == args.CandidateId {// Todo:判断日志是否至少更新才可以投票rf.votedFor = args.CandidateIdrf.lastReceive = time.Now().Unix() // 更新时间,上面操作相当于与可能的Leader通信过了reply.VoteGranted = trueDPrintf("[%d]: voted to [%d]", rf.me, args.CandidateId)}

在调用的时候,Candidate请求每一台服务器投票给它,如果得到的响应说我的任期号比你还大,也就是上面的情况2,也自动放弃Candidate的地位成为Follower。否则这个Candidate就会得到自己的票。

// 向每一个Peer请求投票func (rf *Raft) requestVoteToPeer(index int, args *RequestVoteArgs, votesSum *int, votesGet *int, cond *sync.Cond) {reply := RequestVoteReply{}ok := rf.sendRequestVote(index, args, &reply)rf.mu.Lock()defer rf.mu.Unlock()defer cond.Broadcast()*votesSum += 1if !ok {return}if reply.Term > rf.currentTerm {rf.toFollower(reply.Term)// } else if reply.VoteGranted && reply.Term == rf.currentTerm {} else if reply.VoteGranted {*votesGet += 1}}

追加日志RPC appendEntries.go

结构体定义完全按照论文即可,目前不需要其他字段

type AppendEntriesArgs struct {// Your data here (2A, 2B).Term         int        // Leader的任期号LeaderId     int        // Follower可以通过这个LeaderId重定向客户端PrevLogIndex int        // 新的日志条目紧随之前的索引值PrevLogTerm  int        // PrevLogIndex日志条目的任期Entries      []LogEntry // 存储的日志条目,如果是心跳包则为空LeaderCommit int        // Leader的提交索引}type AppendEntriesReply struct {// Your data here (2A).Term    int  // 当前的任期,接收到了之后Leader可以更新自己Success bool // Follower包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真}

这个RPC既作为日志更新的来源,在没有日志携带的时候也作为心跳包用于维持Leader的地位

func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {// Your code here (2A, 2B).// RPC 请求不一定在什么时候应用,因此必须加锁rf.mu.Lock()defer rf.mu.Unlock()// 更新至少为当前的任期reply.Term = rf.currentTermreply.Success = false// 如果Leader的任期还没有我的大,则直接拒绝请求if args.Term < rf.currentTerm {return}// 如果Leader的任期比我的大,则我转为这个任期的Followerif args.Term >= rf.currentTerm || rf.state == Candidate {rf.toFollower(args.Term)}// 如果Leader的任期和我的相同,则操作日志// Todo:日志操作rf.lastReceive = time.Now().Unix()reply.Term = rf.currentTermreply.Success = true}

核心也是计算返回的reply中的两个值,第一个是是否更新成功,第二个是当前服务器的任期号。其中任期号一定要小心,可能服务器自己的状态改变后任期号会随之改变,因此一定要及时更新。

  1. 如果Leader的任期还没有我的大,则直接拒绝请求
if args.Term < rf.currentTerm {return}
  1. 如果Leader的任期比我的大,则如果我是Candidate,放弃Candidate的地位,转为这个任期的Follower
// 如果Leader的任期比我的大,则我转为这个任期的Followerif args.Term >= rf.currentTerm || rf.state == Candidate {rf.toFollower(args.Term)}

(同时要对我自己的日志进行更新,目前还没有实现)

  1. 如果Leader的任期和我的相同,则操作日志(这里没有操作)
  2. 更新服务器的时间和返回的参数
rf.lastReceive = time.Now().Unix()reply.Term = rf.currentTermreply.Success = true

主要是要对服务器的收到Leader的请求的时间进行更新,从而避免Follower转为Candidate,在Leader存在的情况下发起选举

在调用的时候,Leader向其他的每一台服务器发送这个RPC,如果得到的响应说我的任期号比你还大,也就是上面的情况2,也自动放弃Leader的地位成为Follower。

// 向指定的Peer增加日志条目或者发送心跳包func (rf *Raft) appendEntriesToPeer(index int, args *AppendEntriesArgs) {reply := AppendEntriesReply{}if ok := rf.sendAppendEntries(index, args, &reply); ok {rf.mu.Lock()defer rf.mu.Unlock()// Todo:log相关// 如果响应的任期比Leader更大了,说明Leader需要退位成Follower了if reply.Term > rf.currentTerm {rf.toFollower(reply.Term)}}}

主函数 raft.go

初始化

每一台服务器初始化的时候都是一个Follower,任期号为0

除此之外还要设置选举超时时间,心跳发送时间等

以及根据服务器的数量计算好需要多少张选票才能达成共识

然后直接开始选举

rf.toFollower(0)rf.electTimeout = 200     // 初始化选举超时时间rf.heartBeatTimeout = 100 // 初始化心跳间隔时间rf.randomTimeout = 100    // 设置随机时间的最大范围// 初始化成为Leader需要得到的票数if len(rf.peers)%2 == 0 {rf.majorityVote = len(rf.peers)/2 + 1} else {rf.majorityVote = (len(rf.peers) + 1) / 2}// start ticker goroutine to start electionsgo rf.leaderElection()

所有的协程都不设置退出条件,因此内部要么是无限循环,要么是有状态变量等进行控制

选举Leader

选举Leader是一个无限循环,在每一次循环的时候记录当前的时间后进行睡眠(固定时间+随机时间),然后在循环内部进行判断,如果上一次循环到这里的实时时间比上一次接收到心跳包的时间还大,说明在睡眠时间内一直没有接收到心跳包,则认为超时,此时就要放弃自己的Follower身份,转为Candidate开始竞选。

// The ticker go routine starts a new election if this peer hasn't received// heartsbeats recently.func (rf *Raft) leaderElection() {lastElectTime := time.Now().Unix()for !rf.killed() {// Your code here to check if a leader election should// be started and to randomize sleeping time using// time.Sleep().time.Sleep(time.Duration(rf.electTimeout+rand.Int63n(rf.randomTimeout)) * time.Millisecond)rf.mu.Lock()// lastStartTime := startTime// 如果上一次循环到这里的实时时间比上一次接收到心跳包的时间还大,说明在睡眠时间内一直没有接收到心跳包,则认为超时if lastElectTime > rf.lastReceive {//DPrintf("[%d]: current state is [%s].", rf.me, rf.state)if rf.state != Leader {DPrintf("[%d]: is not leader, start election.", rf.me)rf.tryLeader()}}lastElectTime = time.Now().Unix() // 更新“上一次”的时间rf.mu.Unlock()}}

然后在 rf.tryLeader()中,首先将服务器的状态转为Candidate,然后构建请求,向其他的peer发送请求投票的RPC,收到响应后对收到的投票进行统计。如果得到了大多数的选票,则这个Candidate可以转为Leader,同时向其他的服务器发送心跳包说明自己已经成为了Leader,其他的peer需要放弃竞选。

func (rf *Raft) tryLeader() {rf.toCandidate()votesSum := 1                // 总共的票的数量votesGet := 1                // 收到的票数,自己首先给自己投票cond := sync.NewCond(&rf.mu) // 条件变量,控制投票结果的返回args := RequestVoteArgs{Term:        rf.currentTerm,CandidateId: rf.me,}for i := 0; i < len(rf.peers); i++ {if i != rf.me {go rf.requestVoteToPeer(i, &args, &votesSum, &votesGet, cond)}}// 等待票数统计完毕并判断是否能成为Leadergo func() {rf.mu.Lock()defer rf.mu.Unlock()for votesGet < rf.majorityVote && votesSum < len(rf.peers) && rf.state == Candidate {cond.Wait()}if votesGet >= rf.majorityVote && rf.state == Candidate {rf.toLeader()// 发送心跳包go rf.logReplication()}}()}

内部的协程同步使用状态变量控制(虽然不明白为什么使用WaitGroup不可以实现功能)

心跳包发送

心跳包发送(或与日志更新一起)是只有Leader才可以发起的动作。

注意定时发起请求即可

// Leader定时发送更新log的请求,同时也作为心跳包func (rf *Raft) logReplication() {for !rf.killed() {rf.mu.Lock()if rf.state == Leader {args := AppendEntriesArgs{Term:     rf.currentTerm,LeaderId: rf.me,}for i := 0; i < len(rf.peers); i++ {if i != rf.me {go rf.appendEntriesToPeer(i, &args)}}}rf.mu.Unlock()time.Sleep(time.Duration(rf.heartBeatTimeout) * time.Millisecond)}}

运行结果

目前最快的结果:

Test (2A): initial election ...  ... Passed --   3.0  3   72   18660    0Test (2A): election after network failure ...  ... Passed --   4.9  3  166   31952    0Test (2A): multiple elections ...  ... Passed --   5.3  7  522  111880    0PASSok      6.824/raft      13.335s

运行10次后均成功

Part 2B:日志

指导

完善 Leader 和 Follower 的代码,使他们可以追加新的日志条目,并通过 go test -run 2B

  • 你的第一个目标应该是通过 TestBasicAgree2B()。首先实现 Start(),然后按照 Figure 2,实现 RPC 函数 AppendEntries来收发新的日志条目。通过 applyCh发送每一个新提交的日志条目。
  • 您需要实现选举限制(论文第 5.4.1 节)。
  • 在早期的 2B 实验中,测试中未能达成协议的解决办法是:即使领导人还活着,也举行重复的选举。在选举计时器中找到并修复这个 bug ,或在赢得选举后不要立即发送心跳包。
  • 您的代码可能需要循环检测变量。不要让这些循环不间断连续执行,这将使您的服务运行变慢,最终导致测试失败。使用Go的条件变量或在循环中插入 time.Sleep(10 * time.Millisecond)

如果运行太慢,可能会没法通过接下来的测试。您可以使用 time命令检查您的解决方案使用了多少实时时间和CPU时间。这是典型的输出:

$ time go test -run 2BTest (2B): basic agreement ...  ... Passed --   0.9  3   16    4572    3Test (2B): RPC byte count ...  ... Passed --   1.7  3   48  114536   11Test (2B): agreement after follower reconnects ...  ... Passed --   3.6  3   78   22131    7Test (2B): no agreement if too many followers disconnect ...  ... Passed --   3.8  5  172   40935    3Test (2B): concurrent Start()s ...  ... Passed --   1.1  3   24    7379    6Test (2B): rejoin of partitioned leader ...  ... Passed --   5.1  3  152   37021    4Test (2B): leader backs up quickly over incorrect follower logs ...  ... Passed --  17.2  5 2080 1587388  102Test (2B): RPC counts aren't too high ...  ... Passed --   2.2  3   60   20119   12PASSok  6.824/raft35.557sreal0m35.899suser0m2.556ssys0m1.458s$

“ok 6.824/raft 35.557s” 意味着 Go 运行 2B 的测试所用的实时时间为 35.557 秒。“user 0m2.556s” 表示代码运行了 2.556 秒的 CPU 时间,或实际运行(而不是等待或睡眠)所花费的时间。如果测试 2B 使用超过 1 分钟的实时时间,或超过 5 秒的 CPU 时间,则以后的实验可能会遇到麻烦。检查睡眠时间、等待 RPC 超时所花费的时间、没有睡眠或等待地检查条件或channel信息的循环、或发送大量 RPC 的地方。

实现

参考资料

2A完善 util.go

无论是转为Leader、Follower或者转为Candidate,实际上都可以看成是有一个隐含存在的Leader告诉他们这样做的,因此都要同步更新自己的选举超时时间,防止在有Leader的时候就已经超时,导致Leader的存在时间过短。

// 转为Leaderfunc (rf *Raft) toLeader() {DPrintf("[%d]: convert from [%s] to [%s], term [%d]", rf.me, rf.state, Leader, rf.currentTerm)rf.state = Leaderrf.lastReceive = time.Now().Unix()// 选举为Leader后重新对所有的peer进行初始化for i := 0; i < len(rf.peers); i++ {rf.nextIndex[i] = len(rf.log)rf.matchIndex[i] = -1}}// 转为Followerfunc (rf *Raft) toFollower(newTerm int) {DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Follower)rf.state = Followerrf.currentTerm = newTermrf.votedFor = -1rf.lastReceive = time.Now().Unix()}// 转为Candidatefunc (rf *Raft) toCandidate() {DPrintf("[%d]: convert from [%s] to [%s]", rf.me, rf.state, Candidate)rf.state = Candidaterf.currentTerm += 1rf.votedFor = rf.merf.lastReceive = time.Now().Unix()}

结构体字段理解

首先要注意由于论文中的索引是从1开始计算的,而计算机上切片的索引是从0开始算的,因此论文说明的初始化为0的地方都要初始化为-1

nextIndex[]:leader要发送给follower的下一条log entry(各follower不同),follower与leader一致的时候只发最新一条log,有不一致的时候,nextIndex要减,一次发多条log。把不一致的部分都修正过来。

matchIndex[]:已知follower上,从0开始有多少条连续的log entry与leader一致。即: 有多少条log entry已经被成功replicate到follower上了。如果过半数,就可以增加commitIndex, apply到状态机, 答复客户端操作成功了

commitIndex: 已知被提交的最高日志项对应的index。当日志项被提交(committed)了,意味着该日志项已经成功复制到了集群中的多数派server上,属于“集体记忆”了。如果当前的leader宕机再次发生选举,只有拥有完整已提交日志的server才能够获得多数派选票,才能被选举为leader。根据Leader完整性(Leader Completeness),如果一个日志项在某个term被提交了,则该Entry会存在于所有更高term的leader日志中。

lastApplied: 应用(apply)给状态机的最高日志项的index,也就是上层应用“消费”到Raft日志项的最新index。Leader使用nextIndex和matchIndex两个数组来维护集群中其它server的日志状态。

其他结构体字段:

  • applyCh: 由实验提供,通过该channel将ApplyMsg发送给上层应用。
  • moreApply: 示意有更多的日志项已经被提交,可以apply。
  • applyCond: apply时用于多goroutine之间同步的Condition。

Start函数

Start函数是raft顶层的服务最开始调用的类似初始化的函数

如果server不是leader则返回false。如果是leader的话,那么将command组装成LogEntry后追加到自己的日志中。此处要同时更新leader自己的matchIndex(由于自己就是Leader,自己肯定与自己一致)和nextIndex(如果自己是Follower,这条日志肯定就不能改了)

func (rf *Raft) Start(command interface{}) (int, int, bool) {index := -1term := -1isLeader := false// Your code here (2B).if !rf.killed() {rf.mu.Lock()defer rf.mu.Unlock()if rf.state == Leader {isLeader = true// 只有是Leader才可以接收日志信息// 添加日志信息rf.log = append(rf.log, LogEntry{Term:    rf.currentTerm,Command: command,})index = len(rf.log) - 1term = rf.currentTermrf.matchIndex[rf.me] = index    // 已经复制给他的日志的最高索引值rf.nextIndex[rf.me] = index + 1 // 需要发送给他的下⼀个日志条目的索引值}// 论文与代码起始位置索引不同index += 1}return index, term, isLeader}

两个RPC的新增字段

请求投票RPC:新增了最后一个日志项的信息

  • LastLogIndex 是 candidate 最后一个日志项的 index
  • LastLogTerm 是 candidate 最后一个日志项的 term

新增日志RPC:(只有Leader才可能发出)

  • Entries[]: 发送给对应server的新日志,如果是心跳则为空。这里要发送给对应server日志的index,是从nextIndex到最后一个日志项的index,注意也可能为空。
  • PrevLogIndex: 紧跟在新日志之前的日志项的index,是leader认为follower当前可能已经同步到了的最高日志项的index。对于第i个server,就是nextIndex[i] - 1。
  • PrevLogTerm: prevLogIndex对应日志项的term。
  • LeaderCommit: leader已经提交的commit index。用于通知follower更新自己的commit index。

AppendEntryReply结构体新增了XTerm、XIndex和XLen几个变量用于nextIndex的快速回退。

论文中的nextIndex在AppendEntry RPC返回不匹配后,默认只是回退一个日志项(nextIndex[i]=PrevLogIndex)。如果follower能够返回更多信息,那么leader可以根据这些信息使对应server的nextIndex快速回退,减少AppendEntry RPC通信不匹配的次数,从而加快同步日志的步伐。这几个变量的具体含义:

  • XLen: 当前follower所拥有的的日志长度。
  • XTerm: 当前follower的日志中,PrevLogIndex所对应日志项的term。可能为空。
  • XIndex: 当前follower的日志中,拥有XTerm的日志项的最低index,可能为空。

主函数 Make

make()函数中除做一些初始化的工作之外,新增了将已经被提交的日志项返回给上层应用的goroutine

// 初始化日志相关rf.log = make([]LogEntry, 0)rf.commitIndex = -1rf.lastApplied = -1rf.nextIndex = make([]int, len(peers))rf.matchIndex = make([]int, len(peers))rf.applyCh = applyChrf.moreApply = falserf.applyCond = sync.NewCond(&rf.mu)go rf.appMsgApplier()

这个新增的goroutine无限循环判断rf.moreApply字段,一旦发现为真,则触发返回的操作,返回新的提交过的日志给上层应用

func (rf *Raft) sendApplyMsg() {rf.moreApply = truerf.applyCond.Broadcast()}func (rf *Raft) appMsgApplier() {for {rf.mu.Lock()// 等待这个字段为真才可以继续for !rf.moreApply {rf.applyCond.Wait()}rf.moreApply = falsecommitIndex := rf.commitIndexlastApplied := rf.lastAppliedentries := rf.logrf.mu.Unlock()// 发送已经提交但是还没有返回的日志字段for i := lastApplied + 1; i <= commitIndex; i++ {msg := ApplyMsg{CommandValid: true,Command:      entries[i].Command,CommandIndex: i + 1,}DPrintf("[%d]: apply index %d - 1", rf.me, msg.CommandIndex)rf.applyCh <- msg// 及时加锁更新,否则可能会变化rf.mu.Lock()rf.lastApplied = irf.mu.Unlock()}}}

返回给上层应用的情况两种:

  • Leader在将日志项复制到多数派后更新commitIndex的同时,要调用sendApplyMsg()
  • Follower在AppendEntry RPC收到LeaderCommit的更新时,也要调用sendApplyMsg()

选举限制

在前面选举Leader时,并没有对日志做限制,在这里需要补充日志层面的选举限制

首先要在请求投票的结构体中附带自己最后一条日志的信息

// Candidate最后一条日志的信息lastLogIndex := len(rf.log) - 1lastLogTerm := -1// 如果日志为空需要添加判断if lastLogIndex != -1 {lastLogTerm = rf.log[lastLogIndex].Term}args := RequestVoteArgs{Term:         rf.currentTerm,CandidateId:  rf.me,LastLogIndex: lastLogIndex,LastLogTerm:  lastLogTerm,}

然后严格按照论文说明对请求投票的双方进行判断即可:

总体原则:candidate的log是否至少和接受者的log一样新

  1. 我的log长度为0,那我肯定投票给他了 len(rf.log) ==0
  2. candidate的最后的log的任期比我的最后的log的任期大 args.LastLogTerm > rf.log[len(rf.log)-1].Term
  3. candidate的最后的log的任期和我的最后的log的任期相同 args.LastLogTerm == rf.log[len(rf.log)-1].Term,但是它的日志长度比我长或一样(它先请求我投票,那么我就投票给他吧)args.LastLogIndex >=len(rf.log)-1
// 是否没投票或者投给的是这个candidateif rf.votedFor == -1 || rf.votedFor == args.CandidateId {// candidate的log是否至少和接受者的log一样新// 1. 我的log长度为0,那我肯定投票给他了// 2. candidate的最后的log的任期比我的最后的log的任期大// 3. candidate的最后的log的任期和我的最后的log的任期相同,但是它的日志长度比我长if len(rf.log) == 0 || (args.LastLogTerm > rf.log[len(rf.log)-1].Term) ||(args.LastLogTerm == rf.log[len(rf.log)-1].Term && args.LastLogIndex >= len(rf.log)-1) {rf.votedFor = args.CandidateIdrf.lastReceive = time.Now().Unix() // 更新时间,上面操作相当于与可能的Leader通信过了reply.VoteGranted = trueDPrintf("[%d]: voted to [%d]", rf.me, args.CandidateId)}}

日志复制

前期准备(构建请求)

// 找到日志的同步位置prevLogIndex := rf.nextIndex[index] - 1prevLogTerm := -1if prevLogIndex != -1 {prevLogTerm = rf.log[prevLogIndex].Term}// 找到要发送的日志var entries []LogEntryif len(rf.log)-1 >= rf.nextIndex[index] {entries = rf.log[rf.nextIndex[index]:]}// 补充结构体args := AppendEntriesArgs{Term:         rf.currentTerm,LeaderId:     rf.me,LeaderCommit: rf.commitIndex,PrevLogIndex: prevLogIndex,PrevLogTerm:  prevLogTerm,Entries:      entries,}

论文的日志匹配性质:

  • 如果来自不同日志的两个日志项有相同的index和term,那么它们存储了相同的command。
  • 如果来自不同日志的两个日志项有相同的index和term,那么它们前面的日志完全相同。

因此只需要判断PrevLogIndex和PrevLogTerm与follower的日志匹配的程度即可,这里只是Leader猜测一下,真正的判断在接收到RPC后完成

Follower处理请求

在处理AppendEntry RPC的代码中,新增了日志匹配的逻辑。

如果日志在prevLogIndex处不包含term为prevLogTerm的日志项,那么返回false,(需要回退才能找到对应的位置)。

  • 接收者的日志没有index为prevLogIndex的日志项
  • 有对应index的日志项但是term不匹配。

回退的逻辑:

  1. 记录Follower的日志的长度
  2. 找到prevLogIndex的索引位置的任期号并记录任期(一定比prevLogTerm更小)
  3. 往回遍历日志,找到第一个是上一步记录的任期的索引,那么这个位置之前一定是与Leader相同的日志,记录索引
// Reply false if log doesn’t contain an entry at prevLogIndex// whose term matches prevLogTerm (§5.3)if args.PrevLogIndex >= len(rf.log) || (args.PrevLogIndex >= 0 && rf.log[args.PrevLogIndex].Term != args.PrevLogTerm) {reply.Term = rf.currentTerm// 回退reply.XLen = len(rf.log)if args.PrevLogIndex >= 0 && args.PrevLogIndex < len(rf.log) {reply.XTerm = rf.log[args.PrevLogIndex].Termfor i := args.PrevLogIndex; i >= 0; i-- {if rf.log[i].Term == reply.XTerm {reply.XIndex = i} else {break}}}return}

此外还要注意prevLogIndex可能为-1,意味着日志全都没有匹配上,或者leader此刻还没有日志,此时接收者就要完全服从。

接下来是PreLogIndex与PrevLogTerm匹配到的情况,还要额外检查新同步过来的日志和已存在的日志是否存在冲突。如果一个已经存在的日志项和新的日志项冲突(相同index但是不同term),那么要删除这个冲突的日志项及其往后的日志。

// If an existing entry conflicts with a new one (same index// but different terms), delete the existing entry and all that// follow it (§5.3)misMatchIndex := -1for i := range args.Entries {if args.PrevLogIndex+1+i >= len(rf.log) || rf.log[args.PrevLogIndex+1+i].Term != args.Entries[i].Term {misMatchIndex = ibreak}}

将新的日志项追加到日志中

// Append any new entries not already in the logif misMatchIndex != -1 {rf.log = append(rf.log[:args.PrevLogIndex+1+misMatchIndex], args.Entries[misMatchIndex:]...)}

最后根据论文,如果 leaderCommit > commitIndex,说明follower的commitIndex也需要更新。为了防止越界,commitIndex取 min(leaderCommit, index of last new entry)。同时要向上层应用发回响应。

// If leaderCommit > commitIndex, set commitIndex = min(leaderCommit, index of last new entry)if args.LeaderCommit > rf.commitIndex {newEntryIndex := len(rf.log) - 1if args.LeaderCommit >= newEntryIndex {rf.commitIndex = newEntryIndex} else {rf.commitIndex = args.LeaderCommit}DPrintf("[%d]: commit index [%d]", rf.me, rf.commitIndex)rf.sendApplyMsg()}

Leader处理响应

由于RPC在网络中可能乱序或者延迟,我们要确保当前RPC发送时的term、当前接收时的currentTerm以及RPC的reply.term三者一致,丢弃过去term的RPC,避免对当前currentTerm产生错误的影响。

reply.Term == rf.currentTerm && rf.currentTerm == args.Term

当reply.Success为true,说明follower包含了匹配prevLogIndex和prevLogTerm的日志项,更新nextIndex[serverTo]和matchIndex[serverTo]。这里只能用prevLogIndex和entries来更新,而不能用nextIndex及len(log),因为后两者可能已经被别的RPC更新了,进而导致数据不一致。

由于matchIndex发生了变化,我们要检查是否更新commitIndex。根据论文,如果存在一个N,这个N大于commitIndex,多数派的matchIndex[i]都大于等于N,并且log[N].term等于currentTerm,那么更新commitIndex为N。这里必须注意,日志提交是有限制的,Raft从不提交过去term的日志项,即使已经复制达到了多数派。如果要更新commitIndex为N,那么N所对应的日志项的term必须是当前currentTerm。

在检查是否更新commitIndex的实现上,我们将matchIndex复制到了matches数组中,通过sort升序排序以方便遍历。然后对matches数组进行遍历,找到大多数都提交的索引位置,随后调用sendApplyMsg(),通知有更多的日志项已经被提交,上层应用可以应用。

if reply.Success {// 更新服务器的状态rf.nextIndex[index] = prevLogIndex + len(entries) + 1rf.matchIndex[index] = prevLogIndex + len(entries)// If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm:// set commitIndex = Nmatches := make([]int, len(rf.peers))copy(matches, rf.matchIndex)sort.Ints(matches)for i := rf.majorityVote - 1; i >= 0 && matches[i] > rf.commitIndex; i-- {if rf.log[matches[i]].Term == rf.currentTerm {rf.commitIndex = matches[i]DPrintf("[%d]: commit index [%d]", rf.me, rf.commitIndex)rf.sendApplyMsg()break}}}

当reply.Success为false,说明follower的日志不包含在prevLogIndex处并匹配prevLogTerm的日志项,要将nextIndex缩减。此处更新不宜采用自减的方式更新,因为RPC可能会重发,正确的方式是 rf.nextIndex[serverTo] = prevLogIndex

在AppendEntryReply中增加了几个变量,以使nextIndex能够快速回退(back up)。如果接下来要尝试匹配的prevLogIndex比follower当前所拥有的的日志长度(XLen)还要大,那么显然直接从XLen尝试匹配即可。如果接下来要尝试匹配的prevLogIndex在XLen以内,因为我们已经知道了follower的日志从XIndex到当前prevLogIndex的日志项的term都是XTerm,那么我们可以直接在leader侧遍历匹配一遍,而无需多次往返RPC通信。

} else {// In Test (2C): Figure 8 (unreliable), the AppendEntry RPCs are reordered// So rf.nextIndex[index]-- would be wrongrf.nextIndex[index] = prevLogIndex// 如果接下来要尝试匹配的prevLogIndex比follower当前所拥有的的日志长度(XLen)还要大,那么显然直接从XLen尝试匹配即可。if rf.nextIndex[index]-1 >= reply.XLen {rf.nextIndex[index] = reply.XLen} else {// 如果接下来要尝试匹配的prevLogIndex在XLen以内,因为我们已经知道了follower的日志从XIndex到当前prevLogIndex的日志项的term都是XTerm,那么我们可以直接在leader侧遍历匹配一遍,而无需多次往返RPC通信for i := rf.nextIndex[index] - 1; i >= reply.XIndex; i-- {if rf.log[i].Term != reply.XTerm {rf.nextIndex[index] -= 1} else {break}}}}

运行结果

Test (2B): basic agreement ...  ... Passed --   1.3  3   16    4546    3Test (2B): RPC byte count ...  ... Passed --   2.7  3   48  114510   11Test (2B): agreement after follower reconnects ...  ... Passed --   7.1  3  116   31767    8Test (2B): no agreement if too many followers disconnect ...  ... Passed --   4.1  5  160   37664    3Test (2B): concurrent Start()s ...  ... Passed --   1.2  3   12    3466    6Test (2B): rejoin of partitioned leader ...  ... Passed --   5.6  3  166   40233    4Test (2B): leader backs up quickly over incorrect follower logs ...  ... Passed --  34.1  5 2352 2038228  102Test (2B): RPC counts aren't too high ...  ... Passed --   2.5  3   42   12630   12PASSok      6.824/raft      58.652sreal    0m59.353suser    0m1.744ssys     0m1.630s

Part 2C:持久性

指导

如果基于 Raft 的服务器重新启动,它应该在中断的地方恢复服务。这要求 Raft 在重启后,依旧能确保数据持久化。本文的Figure 2 提到的那些状态应该被持久化。

真正的实现会在每次 persistent state 被修改时写磁盘,并在重新启动后从磁盘读取状态。您不需要使用磁盘,而应该通过 Persister 对象保存和恢复 persistent state (请参阅 persister.go)。调用 Raft.Make()时会提供一个 Persister, 其可能会包含 Raft 最近的 persistent state(也可能没有) 。Raft 应从 Persister 初始化其状态(对应方法 ReadRaftState()),并在每次 president state 更改后使用 Persister 保存(对应方法 SaveRaftState())。

完善 raft.go中的 persist()readPerisit()函数,实现保存和读取 persistent state。你可能需要使用 labgob encoder 来编码(或者说序列化)persistent state,让 Persister来存储二进制流。欢迎查看 persist()readPerisit()的注释了解更多。labgob很像 go 的 gob,只是会在序列化非导出字段时报错。实现完“ 在每次 persistent state 改变时调用 presist()”后,应通过其余测试。

您可能想优化为一次性保存多条日志。查看论文第7页的顶部到第 8 页顶部(用灰色线标记的地方)。论文没有描述清楚细节,你需要自己多考虑一下。 6.824 Raft 的讲座或许也能提供一些帮助。

您的代码应通过所有 2C 测试:

$ go test -run 2CTest (2C): basic persistence ...  ... Passed --   5.0  3   86   22849    6Test (2C): more persistence ...  ... Passed --  17.6  5  952  218854   16Test (2C): partitioned leader and one follower crash, leader restarts ...  ... Passed --   2.0  3   34    8937    4Test (2C): Figure 8 ...  ... Passed --  31.2  5  580  130675   32Test (2C): unreliable agreement ...  ... Passed --   1.7  5 1044  366392  246Test (2C): Figure 8 (unreliable) ...  ... Passed --  33.6  5 10700 33695245  308Test (2C): churn ...  ... Passed --  16.1  5 8864 44771259 1544Test (2C): unreliable churn ...  ... Passed --  16.5  5 4220 6414632  906PASSok  6.824/raft123.564s$

最好能多次运行:for i in {0..10}; do go test; done

实现

Part 2D:日志压缩

指导

就目前情况而言,重新启动的服务器会重放完整的Raft日志,以恢复其状态。然而,对于长期运行的服务来说,永远记住完整的Raft日志是不现实的。相反,您将修改Raft以与持久存储其状态的“快照”的服务协作,此时Raft将丢弃快照之前的日志条目。其结果是持久数据量更少,重启速度更快。然而,现在有可能一个追随者远远落后,以至于领导者放弃了需要追赶的日志条目;然后领导者必须发送快照以及快照时开始的日志。

您的Raft必须提供以下函数 Snapshot(index int, snapshot []byte),服务可以使用其状态的序列化快照调用该函数。

在Lab 2D中,测试代码定期调用 Snapshot()。在Lab 3中,您将编写一个k/v服务器调用 Snapshot();快照将包含k/v对的完整表。服务层对每个对等方(而不仅仅是Leader)调用 Snapshot()

index参数指示快照中包括的最高日志条目。raft应该在这个参数之前丢弃其日志条目。您需要修改Raft代码以只存储日志尾部。

您需要实现论文中讨论的 InstallSnapshot RPC,该RPC允许raft的Leader告诉落后的Raft服务器用快照替换其状态。您可能需要考虑 InstallSnapshot应该如何与图2中的状态和规则交互。

当Follower的Raft代码接收到 InstallSnapshot RPC时,它可以使用 applyCh将快照发送到 ApplyMsg中的服务。ApplyMsg结构定义已经包含了您需要的字段(并且是测试代码期望的)。请注意,这些快照只会增加服务的状态,而不会导致服务向后移动。

如果服务器崩溃,它必须从持久数据重新启动。您的Raft应该保持Raft状态和相应的快照。使用 persister.SaveStateAndSnapshot(),它对于Raft状态和相应的快照有单独的参数。如果没有快照,则传递nil作为快照参数。

当服务器重新启动时,应用程序层读取持久化快照并恢复其保存状态。

以前,建议您实现一个名为 CondInstallSnapshot的函数,以避免在 applyCh上发送的快照和日志条目需要协调。这个残留的API接口仍然存在,但不希望实现它:相反,我们建议您只需将其返回true。

任务:实现 Snapshot()InstallSnapshot RPC,以及对Raft的更改以支持这些(例如,使用修剪日志的操作)。

提示:

  1. 修改代码以便能够存储从某个索引X开始的日志部分是一个好的开始。最初,您可以将X设置为零并运行2B/2C测试。然后使用 Snapshot(index)放弃索引之前的日志,并将X设置为索引。如果一切顺利,您现在应该通过第一个2D测试。
  2. 您将无法将日志存储在Go切片中,并将Go切片索引与Raft日志索引互换使用;您需要以一种方式对切片进行索引,以说明日志中被丢弃的部分。
  3. 下一步:如果Leader没有更新Follower所需的日志条目,则让Leader发送 InstallSnapshot RPC
  4. 在单个 InstallSnapshot RPC中发送整个快照。不要实现图13的用于分割快照的偏移机制。
  5. Raft必须以允许Go垃圾收集器释放和重新使用内存的方式丢弃旧日志条目;这要求对丢弃的日志条目没有可访问的引用(指针)。
  6. 即使日志被修剪,您的实现仍然需要在 AppendEntries RPC中的新条目之前正确发送条目的术语和索引;这可能需要保存和引用最新快照的 lastIncludedTerm/lastIncludedIndex(请考虑是否应持久化)。
  7. 在不检测竞争的情况下,全套Lab 2测试(2A+2B+2C+2D)所需的合理时间是6分钟的实时时间和1分钟的CPU时间。使用–race运行时,大约需要10分钟的实时时间和2分钟的CPU时间。

输出示例:

$ go test -run 2DTest (2D): snapshots basic ...  ... Passed --  11.6  3  176   61716  192Test (2D): install snapshots (disconnect) ...  ... Passed --  64.2  3  878  320610  336Test (2D): install snapshots (disconnect+unreliable) ...  ... Passed --  81.1  3 1059  375850  341Test (2D): install snapshots (crash) ...  ... Passed --  53.5  3  601  256638  339Test (2D): install snapshots (unreliable+crash) ...  ... Passed --  63.5  3  687  288294  336Test (2D): crash and restart all servers ...  ... Passed --  19.5  3  268   81352   58PASSok      6.824/raft      293.456s

实现

]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + 【实践课】规则引擎设计与实现 + + /2023/01/29/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day06/ + + 【实践课】规则引擎设计与实现

【实践课】规则引擎设计与实现

一、概述

1.1 前言

规则引擎是一种嵌入在应用服务中的组件,可以将灵活多变的业务决策从服务代码中分离出来。通过使用预定义的语义模块来编写业务逻辑规则。在执行时接受数据输入、解释业务规则,并做出决策。规则引擎能大大提高系统的灵活性和扩展性。

在字节跳动,规则引擎已经在风控识别、活动运营、配置下发等场景得到了广泛的应用。开发人员可以将业务逻辑与服务代码解耦,实现灵活、高效的业务策略发布。目前公司内部基于规则引擎的动态决策系统已经承接了千万级别QPS的决策请求。

规则引擎的实现需要在满足大容量、高请求、低延迟的基础上尽可能做到简单易上手。本次课程将会带领大家实现一个简单版的规则引擎。

1.2 课程目标

  • 了解规则引擎的组成部分和应用场景。
  • 学习并掌握规则引擎的设计与实现原理。
  • 明确一个规则引擎的设计目标,并完成各部分的设计与实现步骤拆解。
  • 动手实现规则引擎项目,完成预定目标。
  • [课外扩展] 结合其他课程,完成一个在线 规则引擎 服务。

1.3 课程重难点

重点

  • 规则引擎的设计 。明确设计目标、完成步骤拆解、完成各部分状态机的详细设计
  • 规则引擎的实现。基于项目工程完成词法分析、语法分析、抽象语法树的执行功能

难点

  • 规则引擎的核心原理(理论)。词法分析、语法分析、类型检查、语法树执行

主要涉及到编译原理的部分

二、课前预习

课前必看!!!

本部分是需要大家在上课之前了解的内容,主要是一些基本的概念和原理。

在这门课程之前你可能根本没有听说过规则引擎这个东西,当然也可能是浅浅的大概知道这是个什么东西,或者是个规则引擎方面的资深专家(还没毕业,五年工作经验那种🐶,如果是这样请赶紧找我内推)。都没有关系,这门课包教包会!!!(学不会的下课后可以找我们运营人员联系我一对一教学)

当然,这门课程还是有一定的门槛的,这也就是我为什么要说这么多一定要让你仔细看看这部分的原因。经过实验,课程的内容如果只依赖于课上老师的讲解,只能做到:能听懂,能跟上,来不及思考。要想能够理解掌握这部分内容,能跟别人battle下,再向自己的知识山峰上加那么一块小石头,得好好预习。

开始之前先百度或者Google一下 “规则引擎”简单浏览下哈,📪📪📪另外掘金app上面也有许多不错的文章。可以先浏览看看。

2.1 数据结构基础

数据结构得学过吧,考多少分?😁

这块的预习目标呢,包括以下几个部分

  • 精通常用数据结构:数组、结构体、指针、队列、二叉树 等等等,课本上有的都得看看
  • 熟练掌握二叉树的各种遍历方式:前中后序遍历,层序遍历,打印二叉树,有时间可以自己写几个小demo,当然最基础的是需要知道各种遍历方式的区别

2.2 Go语言基础

  • 掌握Go语言的基础语法,能读懂项目代码

是的,就这一个要求,其实学完青训营的前几节课就可以达到了

2.3 编译原理基础

编译原理被誉为"程序员的三大浪漫"之一,足以可见这块知识的深度与广度,我们这次课程也是简单的介绍一下与规则引擎相关的概念。

那么可能会有疑问了,不是讲规则引擎么?为啥还得学编译原理?

规则引擎的本质呢就是我们自己定义一套语法,然后去解析用这套语法写的表达式,然后根据解析的内容执行表达式。这个过程其实就是编译和执行的过程。

因此呢需要自行了解以下的内容

  • 编译的概念:
    • 编译的过程发生了什么?
    • 一般分为哪几个步骤,每个步骤的中间结果是什么?
  • 词法分析:
    • 词法如何表示?| 正则文法
    • 词法分析阶段的输出是什么
    • 词法分析阶段是怎么做的?
    • 词法分析可能会产生什么问题?
    • 如何解决词法分析过程中产生的问题?| 左递归问题怎么解决
  • 语法分析
    • 语法如何表示?上下文无关语法、巴克斯范式怎么理解
    • 语法分析阶段的输出是什么? 一般怎么表示
    • 语法分析有哪些方式?什么是递归下降算法?
  • 抽象语法树
    • 抽象语法树是什么?
    • 抽象语法树如何执行?
  • 类型检查
    • 类型检查怎么做?有哪些方式?
    • 类型检查什么时候做?有什么区别?

2.4 环境搭建

课程之前,大家需要根据项目工程,来完成环境的搭建和Demo的运行

项目地址:

github.com/qimengxingy…

相信大家已经完成了Go环境的搭建,项目工程依赖了hertz框架,如果在之前的课程中完成了项目环境搭建可以直接复用。

项目环境:

  • go语言环境搭建

www.runoob.com/go/go-envir…

  • 需要安装docker环境

www.runoob.com/docker/wind…

  • 安装docker-compose工具

www.runoob.com/docker/dock…

项目clone到本地后,可以执行测试脚本来测试环境的可用性。如果有错误欢迎百度和Google解决

git clone https://github.com/qimengxingyuan/young_engine.gitchmod a+x ./setup.sh./setup.sh

脚本执行成功,则环境可以支持项目的执行

项目说明:

本项目是一个简单的规则引擎的实现,详细目录可以参考README.md

项目实现时间有限,没有做比较完备的测试,如果在demo执行的过程中出现某些bug或者执行异常可以直接在github提交issue或者修复后提起PR

juejin.cn/post/711798…

三、课中知识点补充

3.1 什么是编译

编译的过程就是 把某种语言的源程序, 在不改变语义的条件下 ,转换成另一种语言程序(目标语言程序)

  • 如果源代码编译后要在操作系统上运行,那目标代码就是汇编/机器代码。
  • 如果编译后是在虚拟机里执行,那目标代码就可以不是汇编代码,而是一种解释器可以理解的中间形式的代码即可。

解释型语言和编译型语言

  • 有的语言提前把代码一次性转换完毕,这种就是编译型语言,用的转换工具就叫编译器,比如C、C++、Go。一次编译可重复执行
    • 编译后产物不能跨平台,不同系统对可执行文件的要求不同。.exe
    • 特殊的,c、c++、汇编、源代码也不能跨平台
  • 有的语言则可以一边执行一边转化,用到哪里了就转哪里,这种就是解释性语言,用的转化工具叫虚拟机或者解释器,比如java python、javascript

关于 Java Python .

  • Java既有编译又有解释。但是编译并没有直接编译成机器码,而是编译成字节码,然后再放到虚拟机中执行。
  • Python执行过程也是经过两个阶段,先编译成字节码 .pyc 再放到虚拟机中去执行

JVM 和 Python解释器 | 为什么一个叫虚拟机一个叫解释器

  1. “虚拟机”对二进制字节码进行解释,而“解释器”是对程序文本进行解释。
  2. 从历史上看,Java 是为解释二进制字节码而设计和实现的,而 Python 最初是为解释程序文本而设计和实现的。因此,“Java 虚拟机”这个术语在 Java 社区中具有历史意义并且非常成熟,“Python 解释器”这个术语在 Python 社区中具有历史意义并且非常成熟。人们倾向于延长传统并使用很久以前使用的相同术语。
  3. 对于 Java,二进制字节码解释是程序执行的主要方式,而 JIT 编译只是一种可选的和透明的优化。而对于 Python,目前,程序文本解释是 Python 程序执行的主要方式,而编译成 Python VM 字节码只是一种可选的透明优化。

3.2 词法分析

把源代码字符串转换为词法单元(Token)的这个过程。

确定的有限自动机 DFA | Deterministic Finite Automaton

确定的有限自动机就是一个状态机,它的状态数量是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换。

3.3 语法分析

词法分析是识别一个个的单词,而语法分析就是在词法分析的基础上识别出程序的语法结构。这个结构是一个树状结构。这棵树叫做抽象语法树(Abstract Syntax Tree,AST)。树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫“语法”。每个节点还可以有下级节点。

Token -> AST

上下文无关语法 Context-Free Grammar

语言句子无需考虑上下文,就可以判断正确性

...a = 0;...这是一个赋值语句,无论此语句的前后是什么代码,此语句所代表的操作是确定的。即给变量a赋值等于0

编程语言为什么不用人类的语言(自然语言),而是用上下文无关的文法呢? 因为

  1. 便于设计编译器。 客观上技术目前无法实现,如果使用了上下文相关文法,那就是真正实现了人工智能,NLP领域将会有重大突破。
  2. 便于代码开发维护。 如果开发出来的代码像高考的语文阅读理解一样,每个人都有不同的理解,那么,到底哪个才是作者真正想要表达的?如果人类都确定不了含义,那计算机同样也确定不了,最终结果就是错误执行或无法执行。
  3. 汇编语言/机器语言是上下文无关的。CPU执行指令时,读到哪条执行哪条。如果CPU需要考虑上下文,来决定一个语句到底要做什么,那么CPU执行一条语句会比现在慢千倍万倍。考虑上下文的事情,完全可以用户在编程的时候用算法实现。既然机器语言是上下文无关的,那高级语言也基本上是上下文无关的,可能有某些个别语法为了方便使用,设计成了上下文相关的,比如脚本语言的弱类型。在便于使用的同时,增加了解析器的复杂度。

上下文无关语法G:终结符集合T + 非终结符集合N + 产生式集合P + 起始符号S

G由T、N、S和P组成,由语法G推导出来的所有句子的集合称为G语言!

终结符: 组成串的基本符号。可以理解为词法分析器产生的token集合。比如 + Id ( )

非终结符: 表示token的的集合的语法变量。比如 stmt varDecl 等等

start:blockStmts ;               //起始block : '{' blockStmts '}' ;      //语句块blockStmts : stmt* ;              //语句块中的语句stmt = varDecl | expStmt | returnStmt | block;   //语句varDecl : type Id varInitializer? ';' ;         //变量声明type : Int | Long ;                              //类型varInitializer : '=' exp ;                       //变量初始化expStmt : exp ';' ;                              //表达式语句returnStmt : Return exp ';' ;                    //return语句exp : add ;                                      //表达式   add : add '+' mul | mul;                         //加法表达式mul : mul '*' pri | pri;                         //乘法表达式pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式 

产生式:表示形式,S : AB ,就是说S的含义可以用语法AB进行表达

S : ABA : aA | εB : b | bB

展开(expand):将P(A->u )应用到符号串vAw中,得到新串vu **w

折叠(reduce):将P(A->uu )应用到符号串vuu w中,得到新串vAw

推导(derivate):符号串u 应用一系列产生式,变成符号串v ,则u =>v:S => ab | b | bb

巴科斯范式

BNF是描述上下文无关理论的一种具体方法,通过BNF可以实现上下文无关文法的具体化、公式化、科学化,是实现代码解析的必要条件。

<expr> ::= <expr> + <term>         | <expr> - <term>         | <term><term> ::= <term> * <factor>         | <term> / <factor>         | <factor><factor> ::= ( <expr> )           | Num

BNF本质上就是树形分解,分解成一棵抽象语法树

  • 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数。
  • 叶子节点叫做 终结符 ,非叶子节点叫做 非终结符

递归下降算法 Recursive Descent Parsing

基本思路就是按照语法规则去匹配 Token 串。比如说,变量声明语句的规则如下:

varDecl : types Id varInitializer? ';' ;        //变量声明varInitializer : '=' exp ;                       //变量初始化exp : add ;                                      //表达式   add : add '+' mul | mul;                         //加法表达式mul : mul '*' pri | pri;                         //乘法表达式pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式

如果写成产生式格式,是下面这样:

varDecl -> types Id varInitializer ';' varInitializer -> '=' exp      varInitializer -> εexp -> addadd -> add + muladd -> mulmul -> mul * primul -> pripri -> IntLiteralpri -> Idpri -> ( exp )

而基于这个规则做解析的算法如下:

匹配一个数据类型(types)匹配一个标识符(Id),作为变量名称匹配初始化部分(varInitializer),而这会导致下降一层,使用一个新的语法规则:   匹配一个等号   匹配一个表达式(在这个步骤会导致多层下降:exp->add->mul->pri->IntLiteral)   创建一个varInitializer对应的AST节点并返回如果没有成功地匹配初始化部分,则回溯,匹配ε,也就是没有初始化部分。匹配一个分号   创建一个varDecl对应的AST节点并返回

int a = 2

  • 对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。
  • 在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。
  • 如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。这种回退过程,叫做回溯(Backtracking)。

四、课后作业

4.1 实现一个在线规则引擎

课上我们重点讲了规则引擎的设计和实现,结合前面课程的内容课后实现一个在线版本的规则引擎

4.1.1 项目要求

使用Hertz框架开发一个HTTP服务,服务使用mysql,支持表达式的增删查改和编译执行。

并实现以下接口

直接表达式执行

请求参数为待执行的表达式和表达式中参数的值,并输出编译结果

实时编译并执行结果,不需要写入DB中

  • POST api/engine/run
  • Request
{    "exp": "uid == 12345 && did > 0",    "params": {        "uid": 123456,        "did": 0    }}
  • Response
{    "code": 0,    "message": "success",    "data": {  // 执行结果        "result": true    }}
新增表达式

新增一条表达式到DB中,并返回表达式在DB中的ID

需要检测表达式 是否已经存在 ,如果已经存在,直接返回表达式的ID

需要检测表达式是否合法(编译是否通过) ,如果编译失败,返回错误码 20001和编译错误

  • POST api/engine/exp/new
  • Request
{    "exp": "uid == 12345 && did > 0",}
  • Response
{    "code": 0,    "message": "success",    "data": {  // 表达式ID        "id": 1    }}// 编译失败时{    "code": -1,    "message": "compile error: xxxxx", // 编译失败的信息    "data": {  // 表达式ID        "id": 0    }}
查询表达式

查询数据库中所有的表达式

  • GET api/engine/exp/list
  • Response
{    "code": 0,    "message": "success",    "data": [          {            "id": 1,            "exp": "uid > 0"        }    ]}
删除表达式

根据ID删除表达式,表达式不存在时返回错误码 20002 , 和错误信息

删除成功返回被删除的表达式信息

  • DELETE api/engine/exp/:id
  • Response
// 删除成功时{    "code": 0,    "message": "success",    "data": {  // 表达式ID        "id": 1,        "exp": "uid > 0"    }}// 删除失败时{    "code": -1,    "message": "exp id 1 not exist", //查询失败的信息    "data": {}}
执行表达式

根据表达式的ID,查询出表达式内容,并编译执行。表达式不存在时返回错误码 20002 , 和错误信息

  • POST api/engine/exp/run
  • Request
{    "exp_id": 1,    "parmas": {        "uid": 123456,        "did": 0    }}
  • Response
{    "code": 0,    "message": "success",    "data": {  // 执行结果        "result": true    }}// 表达式不存在时{    "code": -1,    "message": "exp id 1 not exist", //查询失败的信息    "data": {}}
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + MIT-6.824 Distributed Systems-LEC 7 Fault Tolerance-Raft-2 + + /2023/01/28/6.824/Distributed-Systems-MIT-6.824-LEC-7/ + + MIT-6.824(Spring 2022)LEC 7 Fault Tolerance-Raft-2

Raft

Leader选举规则

  1. 获得大多数的投票
  2. 至少是最新的-最后一个term相同就可以给选票
  3. 任期号相同则最长的一个获得Leader
  4. 如果存在最后一个term号比当前发起选举的Candidate大,则Candidate自动放弃选举

日志追赶

  1. Leader发送心跳信号,连带自己的前一个term和前一个日志达到的索引号
  2. follower查看自己的前一个term,如果小于Leader的term,返回不允许追加的信息,因为自己落后了
  3. Leader的nextIndex减1,然后与Follower反复迭代,直到找到了两者第一个相同的位置
  4. 然后Leader更新自己与这个Follower的matchIndex。可以认为nextIndex是乐观的,从最后一个开始往前遍历,而matchIndex是悲观的,最开始的时候直接设置为0
  5. Leader与Follower进行日志同步

日志擦除可能会带来一些问题,论文中的Figure 8 说明了这个问题,因此需要有日志提交条件的额外限制Leader 在当前任期至少有一条日志被提交

前面的协议中一直是减1操作,因此如果Follower落后过多,通信开销会很大

追赶更快的优化算法:并不按照索引后退,而是按照term后退,然后再扫描相同的位置

此时Follower并不只是拒绝,而是返回前一个term以及这个term开始的索引

持久化

重启机器会发生什么?

  • 看成一台新机器加入,可能会复制大量的日志
  • 从自己的最后的持久化状态开始追赶

需要持久化什么信息?我们应该尽量不保存信息,因为需要存入磁盘,开销很大,只需要保留必要的信息

  • 投票的信息
  • 日志信息:承诺Leader这些条目都是已经提交过的
  • 当前的term:term不可以下降,需要监控term上升,获得自己的投票信息

服务恢复

  • 根据全部日志重建状态,一定会获得与之前完全相同的状态,太长了可能开销过大
  • 周期性进行快照操作,持久化到磁盘上,可以通过快照对日志进行裁剪,开销不会过大

状态机通过apply channel获得一个快照,然后使用它来进行恢复

使用Raft

步骤:

  1. 客户端发送操作给Leader的K/V服务器
  2. K/V服务器将操作传递给Raft
  3. Raft写入日志
  4. Raft与其他服务器通信传送日志
  5. 其他服务器发送响应给Leader
  6. Leader提交操作(其他的Followers需要等到下一次交互才确认前面的操作并提交)
  7. 操作按照顺序传送到K/V服务器
  8. K/V服务器执行操作
  9. Leader返回操作结果给客户端

客户端也需要保存Raft的Leader和Follower的信息,可以切换它的通信对象

客户端如果没有接收到服务器的响应会进行重试,而服务器可鞥已经执行过这些操作了,因此需要对这些重复的操作进行检测。

一种实现方法:客户端的每一个操作都带有一个id,通过id对重复的操作进行过滤

正确性

模糊定义:多台机器的行为如同单独的一台机器一样

精确定义:

线性一致性:

  • 有一个整体的顺序,操作按照顺序逐步执行
  • 实时匹配
  • 读取操作应该始终返回最后一次写入的值

查看历史操作,即使是并行的程序是否可以在一台机器上执行相同的结果,从而判断是否满足线性一致性。

]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + 简易抖音项目-视频模块 + + /2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Videos/ + + 简易抖音项目-视频模块

简易抖音项目-视频模块设计说明

需求分析

视频模块包括包括视频Feed流获取、视频投稿和获取用户投稿列表三个模块

1. 视频流接口 /douyin/feed/

不限制登录状态,返回按投稿时间倒序的视频列表,视频数由服务端控制,单次最多30个。

接口定义:

service Feed {    rpc Feed (douyin_feed_request) returns (douyin_feed_response) {}}message douyin_feed_request{    int64 latest_time = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间    string token = 2;  // 可选参数,登录用户设置}message douyin_feed_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    repeated Video video_list = 3; // 视频列表    int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time}

2. 发布列表 /douyin/publish/list/

登录用户的视频发布列表,直接列出用户所有投稿过的视频。

接口定义:

service PublishList {    rpc PublishList (douyin_publish_list_request) returns (douyin_publish_list_response) {}}message douyin_publish_list_request{    int64 user_id = 1; // 用户id    string token = 2; // 用户鉴权token}message douyin_publish_list_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    repeated Video video_list = 3; // 用户发布的视频列表}

3. 视频投稿 /douyin/publish/action/

登录用户选择视频上传。

接口定义:

service PublishAction {    rpc PublishAction (douyin_publish_action_request) returns (douyin_publish_action_response) {}}message douyin_publish_action_request{    string token = 1; // 用户鉴权token    bytes data = 2; // 视频数据    string title = 3; // 视频标题}message douyin_publish_action_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述}

整体架构设计

pSodXSP.png

返回的状态码(虽然客户端并没有逻辑进行处理):

  • 用户不存在,状态码为2
  • 应该携带Token但是没有携带,状态码为4
  • 备份文件夹操作失败,状态码为5
  • 无法写入视频文件,状态码为6
  • 无法写入图片文件,状态码为7
  • 无法上传文件到OSS,状态码为8

详细设计

视频流接口

pSowAS0.png

  1. DY-api.Feed处理请求,准备请求服务
  2. 首先请求DY-srv.Feed服务,根据时间戳查询数据库,查询出不超过时间戳的前30个视频,查询后返回视频列表
  3. 随后并行请求视频列表中的每一个视频(即最大并发数为30)
  4. 对每一个视频,根据前一个服务响应的作者的id并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录Author响应相关的5个字段
  5. 对每一个视频,根据视频id并行请求DY-srv.和DY-srv.,对于每个视频
    1. commentCount通过获取DY-srv.返回的切片长度获取
    2. favoriteCount通过获取DY-srv.返回的切片长度获取
    3. 通过Token获取当前的登录用户id,在DY-srv.切片内部查询,如果查询到为True,否则为False
  6. 等待全部的视频返回响应后,构建响应结构体并返回给客户端

发布列表

pSow3Sx.png

  1. DY-api.PublishList处理请求,准备请求服务
  2. 首先请求DY-srv.PublishList服务,根据id查询数据库,如果id在数据库中不存在,则直接返回错误,然后根据用户id查询发布的视频列表并返回
  3. 随后并行请求DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList,等待全部成功返回后记录User响应相关的5个字段
  4. 对每一个视频,根据视频id并行请求DY-srv.和DY-srv.,对于每个视频
    1. commentCount通过获取DY-srv.返回的切片长度获取
    2. favoriteCount通过获取DY-srv.返回的切片长度获取
    3. 通过Token获取当前的登录用户id,在DY-srv.切片内部查询,如果查询到为True,否则为False
  5. 等待全部的视频返回响应后,构建响应结构体并返回给客户端

视频投稿

pSsa6xK.png

  1. DY-api.PublishAction处理请求,将请求中的字段传递到服务端DY-srv.PublishAction
  2. 服务端从Token中获取id信息,如果无法获取id,直接返回错误
  3. 服务端根据id信息查询数据库,获取用户信息,如果id并不存在于数据库,则直接返回错误
  4. 服务端判断本地存放视频与图片文件的文件夹是否存在,如果不存在则创建文件夹
  5. 服务端将接收到的请求中的字节流写入文件,并调用ffmpeg对视频的第一帧进行截图作为封面,同样写入图片文件
  6. 服务端将文件上传信息传递给消息队列,直接返回成功响应给客户端
  7. 消息队列接收到消息后并行上传视频和图片文件,两者都上传成功后将视频信息写入数据库
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 简易抖音项目-用户模块 + + /2023/01/24/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Project-Users/ + + 简易抖音项目-用户模块

简易抖音项目-用户模块设计说明

需求分析

用户模块包括用户注册、用户登录和用户信息三个部分。

1. 用户注册接口 /douyin/user/register/

新用户注册时提供用户名,密码,昵称即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token.

接口定义:

service UserRegister {    rpc UserRegister (douyin_user_register_request) returns (douyin_user_register_response) {}}message douyin_user_register_request{    string username = 1; // 注册用户名,最长32个字符    string password = 2; // 密码,最长32个字符}message douyin_user_register_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    int64 user_id = 3; // 用户id    string token = 4; // 用户鉴权token}

2. 用户登录接口 /douyin/user/login/

通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token

接口定义:

service UserLogin {    rpc UserLogin (douyin_user_login_request) returns (douyin_user_login_response) {}}message douyin_user_login_request{    string username = 1; // 登录用户名    string password = 2; // 登录密码}message douyin_user_login_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    int64 user_id = 3; // 用户id    string token = 4; // 用户鉴权token}

3. 用户信息接口 /douyin/user/

获取登录用户的 id、昵称,如果实现社交部分的功能,还会返回关注数和粉丝数。

接口定义:

service UserInfo {    rpc UserInfo (douyin_user_request) returns (douyin_user_response) {}}message douyin_user_request{    int64 user_id = 1; // 用户id    string token = 2; // 用户鉴权token}message douyin_user_response{    int32 status_code = 1; // 状态码,0-成功,其他值-失败    string status_msg = 2; // 返回状态描述    User user = 3; // 用户信息}

整体架构设计

pSsNise.png

返回的状态码(虽然客户端并没有逻辑进行处理):

  • 注册时用户已经存在,状态码为1
  • 用户不存在,状态码为2
  • 登录时用户存在但是密码错误,状态码为3

详细设计

用户注册

pSsl600.png

  1. DY-api.UserRegister处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserRegister
  2. 服务端根据用户名查询数据库,如果发现重名用户名,则直接返回错误
  3. 未发现重名用户名,则通过md5加盐(用户名)对密码进行加密,加密后插入数据库,数据库返回唯一自增ID
  4. 服务端返回成功响应给DY-api.UserRegister
  5. DY-api.UserRegister利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端

用户登录

pSs80pD.png

  1. DY-api.UserLogin处理请求,将请求中带有的用户名和密码字段传递到服务端DY-srv.UserLogin
  2. 服务端根据用户名查询数据库,如果未发现相同用户名,则直接返回错误,否则返回通过用户名查询出来的用户id和密码
  3. 对用户输入的密码进行md5加盐(用户名)加密,与上一步返回的密码进行比较,如果不匹配直接返回错误
  4. 密码匹配,则服务端返回成功响应给DY-api.UserLogin
  5. DY-api.UserLogin利用响应中的ID信息,调用jwt进行Token生成,生成后构建客户端相应结构体给客户端

用户信息

pSst2rQ.png

  1. DY-api.UserInfo处理请求,将请求中带有的id字段传递到服务端DY-srv.UserInfo、DY-srv.GetFollowList和DY-srv.GetFollowerList
  2. 并行请求三个服务,其中DY-srv.UserInfo根据id字段查询数据库,如果id有效,则返回用户姓名,否则返回错误
  3. 等待三个服务全部成功返回后,填充响应中的User的五个字段
    1. id与name字段通过DY-srv.UserInfo的响应直接获取
    2. followcount通过获取DY-srv.GetFollowList返回的切片长度获取
    3. followercount通过获取DY-srv.GetFollowerList返回的切片长度获取
    4. 通过Token获取当前的登录用户id,在DY-srv.GetFollowerList切片内部查询,如果查询到为True,否则为False
  4. 构建响应结构体并返回给客户端
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Go 语言内存管理详解 + + /2023/01/20/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day05/ + + Go 语言内存管理详解

Go 语言内存管理详解

本节课程主要分为四个方面:

  1. 自动内存管理
  2. Go 内存管理及优化
  3. 编译器和静态分析
  4. Go 编译器优化

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前

自动内存管理

  • Auto memory management: 自动内存管理

  • Grabage collction: 垃圾回收

  • Mutator: 业务线程

  • Collector: GC 线程

  • Concurrent GC: 并发 GC

  • Parallel GC: 并行 GC

  • Tracing garbage collection: 追踪垃圾回收

    • Copying GC: 复制对象 GC
    • Mark-sweep GC: 标记-清理 GC
    • Mark-compact GC: 标记-压缩 GC
  • Reference counting: 引用计数

  • Generational GC: 分代 GC

    • Young generation: 年轻代
    • Old generation: 老年代

Go 内存管理及优化

  • TCMalloc
  • mmap() 系统调用
  • scan object 和 noscan object
  • mspan, mcache, mentral
  • Bump-pointer object allocation: 指针碰撞风格的对象分配

编译器和静态分析

  • 词法分析
  • 语法分析
  • 语义分析
  • Intermediate representation (IR) 中间表示
  • 代码优化
  • 代码生成
  • Control flow: 控制流
  • Data flow: 数据流
  • Intra-procedural analysis 过程内分析
  • Inter-procedural analysis: 过程间分析

Go 编译器优化

  • Function inlining: 函数内联
  • Escape analysis: 逃逸分析

课中

引言

  • 什么是性能优化?

    • 提升软件系统处理能力减少不必要的消耗 ,充分发掘计算机算力
  • 为什么要做性能优化?

    • 用户体验:带来用户体验的提升 —— 让刷抖音更丝滑,让双十一购物不再卡顿
    • 资源高效利用:降低成本,提高效率 —— 很小的优化乘以海量机器会是显著的性能提升和成本节约
  • 性能优化

    • 业务层优化
      • 针对特定场景,具体问题,具体分析
      • 容易获得较大性能收益
    • 语言运行时优化
      • 解决更通用的性能问题
      • 考虑更多场景
      • Tradeoffs
    • 数据驱动
      • 自动化性能分析工具 —— pprof
      • 依靠数据而非猜测
      • 首先优化最大瓶颈
  • 软件质量

    • 保证接口稳定的前提下改进实现

  • 测试驱动

  • 通过清晰的文档告诉用户这一项优化 做了什么没做什么能达到怎样的效果

  • 隔离,优化代码用选项和原先的路径隔离,保证优化未启用时的行为同以前一致

  • 可观测、可灰度、可回滚

自动内存管理

基本概念

  • 自动内存管理:由程序语言的运行时系统管理动态内存

  • 避免手动内存管理,专注于实现业务逻辑

  • 保证内存使用的正确性安全性 : double-free problem, use-after-free problem

  • 三个任务

    • 为新对象分配空间
    • 找到存活对象
    • 回收死亡对象的内存空间
  • 概念
    Mutator: 业务线程,分配新对象,修改对象指向关系
    Collector: GC 线程,找到存活对象,回收死亡对象的内存空间

Serial GC: 只有一个 collector

Parallel GC: 并行 GC,支持多个 collectors 同时回收的 GC 算法

Concurrent GC: 并发 GC,支持 mutator(s) 和 collector(s) 同时执行的 GC 算法

Collectors 必须感知对象指向关系的改变!

追踪垃圾回收

  • Tracing garbage collection: 追踪垃圾回收
    • 被回收的条件:不可达对象
    • 过程
      • 标记根对象 (GC roots): 静态变量、全局变量、常量、线程栈等
      • 标记:找到所有可达对象
      • 清理:回收所有不可达对象占据的内存空间
        • Copying GC: 将存活对象从一块内存空间复制到另外一块内存空间,原先的空间可以直接进行对象分配
        • Mark-sweep GC: 将死亡对象所在内存块标记为可分配,使用 free list 管理可分配的空间
        • Mark-compact GC: 将存活对象复制到同一块内存区域的开头

引用计数

  • 每个对象都有一个与之关联的引用数目

  • 对象存活的条件:当且仅当引用数大于 0

  • 优点

    • 内存管理的操作被 平摊到程序运行中 :指针传递的过程中进行引用计数的增减
    • 不需要了解 runtime 的细节:因为不需要标记 GC roots,因此不需要知道哪里是全局变量、线程栈等
  • 缺点

    • 开销大,因为对象可能会被多线程访问,对引用计数的修改需要原子****操作保证原子性和可见性
    • 无法回收环形数据结构
    • 每个对象都引入额外存储空间存储引用计数
    • 虽然引用计数的操作被平摊到程序运行过程中,但是回收大的数据结构依然可能引发暂停
  • 说明

    • 以上我们所讲述的技术的缺点并非是无法解决的问题。学术界和工业界在一直在致力于解决自动内存管理技术的不足之处。例如,最新的 PLDI’22 的文章 Low-Latency, High-Throughput Garbage Collection 感兴趣的同学可以阅读。

Go 内存管理及优化

Go 内存管理

  • TCMalloc: TC is short for thread caching

  • 目标:为对象在 heap 上分配内存

  • 提前将内存分块

    • 调用系统调用 mmap() 向 OS 申请一大块内存,例如 4 MB
    • 先将内存划分成大块,例如 8 KB,称作 mspan
    • 再将大块继续划分成特定大小的小块,用于对象分配
    • noscan mspan: 分配不包含指针的对象 —— GC 不需要扫描
    • scan mspan: 分配包含指针的对象 —— GC 需要扫描

  • 对象分配:根据对象的大小,选择最合适的块返回

  • 内存缓存

    • Go 内存管理构成了多级缓存机制,从 OS 分配得的内存被内存管理回收后,也不会立刻归还给 OS,而是在 Go runtime 内部先缓存起来,从而避免频繁向 OS 申请内存。内存分配的路线图如下。

Go 内存管理的问题

mspan, mcache 和 mcentral 构成了内存管理的多级缓存机制。

  • 对象分配是非常高频的操作:每秒分配 GB 级别的内存
  • 线上 profiling 发现,Go 的内存分配占用很多 CPU

可以看到,用于分配对象的函数 mallocgc() 占用 CPU 较高

  • 小对象分配占大多数

横轴是对象大小,纵轴是数目,可以看到绝大多数对象都小于 80 B。因此 优化小对象分配是关键

字节跳动的优化方案

  • Balanced GC
  • 核心:将 noscan 对象在 per-g allocation buffer (GAB) 上分配,并使用移动对象 GC 管理这部分内存,提高对象分配和回收效率

  • 每个 g 会附加一个较大的 allocation buffer (例如 1 KB) 用来分配小于 128 B 的 noscan 小对象
  • bump pointer 风格的对象分配。示意如下。
if g.ab.end - g.ab.top < size {    // Allocate a new allocation buffer}addr := g.ab.topg.ab.top += sizereturn addr
  • 分配对象时,根据对象大小移动 top 指针并返回,快速完成一次对象分配
  • 同原先调用 mallocgc() 进行对象分配的方式相比,balanced GC 缩短了对象分配的路径,减少了对象分配执行的指令数目,降低 CPU 使用

从 Go runtime 内存管理模块的角度看,一个 allocation buffer 其实是一个大对象。本质上 balanced GC 是 将多次小对象的分配合并成一次大对象的分配 。因此,当 GAB 中哪怕只有一个小对象存活时,Go runtime 也会认为整个大对象(即 GAB)存活。为此,balanced GC 会根据 GC 策略, 将 GAB 中存活的对象移动到另外的 GAB 中 ,从而压缩并清理 GAB 的内存空间,原先的 GAB 空间由于不再有存活对象,可以全部释放,如下图所示。

上图上方是两个 GAB,其中虚线表示 GAB 中对象的分界线。黑色表示 GAB 中存活的对象,白色表示死掉的对象。由于 GAB 中有存活对象,整个 GAB 无法被回收。

Balanced GC 会将 GAB 中存活的对象移动到下面的 GAB 中,这样原先的两个 GABs 就可以被释放,压缩并清理 GAB 的内存空间。

Balanced GC 只负责 noscan 对象的分配和移动,对象的标记和回收依然依赖 Go GC 本身,并和 Go GC 保持兼容。

编译器和静态分析

  • 编译器的结构

  • 静态分析: 不执行代码 ,推导程序的行为,分析程序的性质。
  • 控制流:程序的执行流程
  • 数据流:数据在控制流上的传递

上图的程序转换成控制流图 (control-flow graph)

  • 通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties) ,这些事实可以帮助我们做编译优化。
    • 例如上面的程序。我们通过分析数据流和控制流,知道这个程序始终返回 4。编译器可以根据这个结果做出优化。

  • Intra-procedural analysis: 函数内分析:在函数内进行控制流和数据流的分析
  • Inter-procedural analysis: 函数间分析:除了函数内的分析,还需要考虑跨函数的数据流和控制流,例如参数传递,函数返回值等

Go 编译器优化

目的

  • 用户无感知,重新编译即可获得性能收益
  • 通用的优化手段

现状

  • 采用的优化较少
  • 追求编译时间短,因此没有进行复杂的代码分析和优化

思路

  • 面向后端长期执行的任务
  • 用适当增加编译时间换取更高性能的代码

函数内联

  • 定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定

  • 优点

    • 消除调用开销
    • 将过程间分析的问题转换为过程内分析,帮助其他分析
  • 缺点

    • 函数体变大
    • 编译生成的 Go 镜像文件变大
  • 函数内联在大多数情况下是正向优化,即多内联,会提升性能

  • 采取一定的策略决定是否内联

    • 调用和被调用函数的规模
  • Go 内联的限制

    • 语言特性:interface, defer 等等,限制了内联优化
    • 内联策略非常保守
  • 字节跳动的优化方案

    • 修改了内联策略,让更多函数被内联
    • 增加了其他优化的机会:逃逸分析
  • 开销

    • Go 镜像大小略有增加
    • 编译时间增加
    • 运行时栈扩展开销增加

逃逸分析

  • 定义:分析代码中指针的动态作用域,即指针在何处可以被访问

  • 大致思路

    • 从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s:
      • 作为参数传递给其他函数;
      • 传递给全局变量;
      • 传递给其他的 goroutine;
      • 传递给已逃逸的指针指向的对象;
    • 则指针 p 逃逸出 s,反之则没有逃逸出 s.
  • 优化:未逃逸出当前函数的指针指向的对象可以在栈上分配

    • 对象在栈上分配和回收很快:移动 sp 即可完成内存的分配和回收;
    • 减少在堆上分配对象,降低 GC 负担。

课后

  1. 从业务层和语言运行时层进行优化分别有什么特点?
  2. 从软件工程的角度出发,为了保证语言 SDK 的可维护性和可拓展性,在进行运行时优化时需要注意什么?
  3. 自动内存管理技术从大类上分为哪两种,每一种技术的特点以及优缺点有哪些?
  4. 什么是分代假说?分代 GC 的初衷是为了解决什么样的问题?
  5. Go 是如何管理和组织内存的?
  6. 为什么采用 bump-pointer 的方式分配内存会很快?
  7. 为什么我们需要在编译器优化中进行静态代码分析?
  8. 函数内联是什么,这项优化的优缺点是什么?
  9. 什么是逃逸分析?逃逸分析是如何提升代码性能的?
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Go 高质量编程与性能调优 + + /2023/01/19/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day04/ + + Go 高质量编程与性能调优

Go 高质量编程与性能调优

课程概述

  • 介绍编码规范,帮助大家写出高质量程序
  • 介绍 Go 语言的性能优化建议,分析对比不同方式对性能的影响和背后的原理
  • 讲解常用性能分析工具 pprof 的使用和工作原理,熟悉排查程序性能问题的基本流程
  • 分析性能调优实际案例,介绍实际性能调优时的工作内容

课前

  • 课程内容概要

image.png

实践准备 (必须)

推荐阅读

课程笔记

高质量编程

简介

  • 编写的代码能够达到正确可靠、简洁清晰、无性能隐患的目标就能称之为高质量代码
  • 实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的
  • 高质量的编程需要注意以下原则:简单性、可读性、生产力

常见编码规范

代码格式
  • 使用 gofmt 自动格式化代码,保证所有的 Go 代码与官方推荐格式保持一致

总结

  • 提升可读性,风格一致的代码更容易维护、需要更少的学习成本、团队合作成本,同时可以降低 Review 成本
注释

总结

  • 代码是最好的注释
  • 注释应该提供代码未表达出的上下文信息
命名规范
  • variable

    • 简洁胜于冗长
    • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
    • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
  • function

    • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
    • 函数名尽量简短
    • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
    • 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息
  • package

    • 只由小写字母组成。不包含大写字母和下划线等字符
    • 简短并包含一定的上下文信息。例如 schema、task 等
    • 不要与标准库同名。例如不要使用 sync 或者 strings

总结

  • 关于命名的大多数规范核心在于考虑上下文
  • 人们在阅读理解代码的时候也可以看成是计算机运行程序,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本
控制流程
  • 避免嵌套,保持正常流程清晰

  • 如果两个分支中都包含 return 语句,则可以去除冗余的 else

  • 尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性

总结

  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  • 提高代码的可读性
错误和异常处理
  • 简单错误处理

    • 优先使用 errors.New 来创建匿名变量来直接表示该错误。有格式化需求时使用 fmt.Errorf
    • github.com/golang/go/b…
  • 错误的 Wrap 和 Unwrap

    • 在 fmt.Errorf 中使用 %w 关键字来将一个错误 wrap 至其错误链中
    • github.com/golang/go/b…
    • Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Iserrors.As 、errors.Unwrap 以及 fmt.Errorf 的 %w。如果项目运行在小于 Go1.13 的版本中,导入 golang.org/x/xerrors 来使用。以下语法均已 Go1.13 作为标准。
  • 错误判定

  • panic

    • 不建议在业务代码中使用 panic
    • 如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃
    • 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
    • github.com/Shopify/sar…
  • recover

总结

  • panic 用于真正异常的情况
  • error 尽可能提供简明的上下文信息,方便定位问题
  • recover 生效范围,在当前 goroutine 的被 defer 的函数中生效

性能优化建议

  • 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率

  • 性能对比测试代码,可参考 github.com/RaymondCode…

  • slice 预分配内存
    • 在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时
    • 原理
      • ueokande.github.io/go-slice-tr…
      • 切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最大长度)
      • 切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的
      • 切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景:
        • 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
        • 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
      • 因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能
    • 另一个陷阱:大内存得不到释放
      • 在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组
      • 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
      • 推荐的做法,使用 copy 替代 re-slice
  • map 预分配内存
    • 原理
      • 不断向 map 中添加元素的操作会触发 map 的扩容
      • 根据实际需求提前预估好需要的空间
      • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
  • 使用 strings.Builder
    • 常见的字符串拼接方式
      *
      • strings.Builder
      • bytes.Buffer
    • strings.Builder 最快,bytes.Buffer 较快,+ 最慢
    • 原理
      • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
      • strings.Builder,bytes.Buffer 的内存是以倍数申请的
      • strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回
  • 使用空结构体节省内存
    • 空结构体不占据内存空间,可作为占位符使用
    • 比如实现简单的 Set
      • Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。对于集合场景,只需要用到 map 的键而不需要值
  • 使用 atomic 包
    • 原理
      • 锁的实现是通过操作系统来实现,属于系统调用,atomic 操作是通过硬件实现的,效率比锁高很多
      • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
      • 对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface{}
总结
  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能

性能调优实战

性能调优简介

  • 性能调优原则
    • 要依靠数据不是猜测
    • 要定位最大瓶颈而不是细枝末节
    • 不要过早优化
    • 不要过度优化

性能分析工具

性能调优的核心是性能瓶颈的分析,对于 Go 应用程序,最方便的就是 pprof 工具

性能调优案例

  • 基本概念
    • 服务:能单独部署,承载一定功能的程序
    • 依赖:Service A 的功能实现依赖 Service B 的响应结果,称为 Service A 依赖 Service B
    • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
    • 基础库:公共的工具包、中间件
  • 业务优化
    • 流程
      • 建立服务性能评估手段
      • 分析性能数据,定位性能瓶颈
      • 重点优化项改造
      • 优化效果验证
    • 建立压测评估链路
      • 服务性能评估
      • 构造请求流量
      • 压测范围
      • 性能数据采集
    • 分析性能火焰图,定位性能瓶颈
      • pprof 火焰图
    • 重点优化项分析
      • 规范组件库使用
      • 高并发场景优化
      • 增加代码检查规则避免增量劣化出现
      • 优化正确性验证
    • 上线验证评估
      • 逐步放量,避免出现问题
    • 进一步优化,服务整体链路分析
      • 规范上游服务调用接口,明确场景需求
      • 分析业务流程,通过业务流程优化提升服务性能
  • 基础库优化
    • 适应范围更广,覆盖更多服务
    • AB 实验 SDK 的优化
      • 分析基础库核心逻辑和性能瓶颈
      • 完善改造方案,按需获取,序列化协议优化
      • 内部压测验证
      • 推广业务服务落地验证
  • Go 语言优化
    • 适应范围最广,Go 服务都有收益
    • 优化方式
      • 优化内存分配策略
      • 优化代码编译流程,生成更高效的程序
      • 内部压测验证
      • 推广业务服务落地验证

课后

  • 了解下其他语言的编码规范,是否和 Go 语言编码规范有相通之处,注重理解哪些共同点
  • 编码规范或者性能优化建议大部分是通用的,有没有方式能够自动化对代码进行检测?
  • github.com/golang/go/t… 中选择感兴趣的包,看看官方代码是如何编写的
  • 使用 Go 进行并发编程时有哪些性能陷阱或者优化手段?
  • 在真实的线上环境中,每个场景或者服务遇到的性能问题也是各种各样,搜索下知名公司的官方公众号或者博客,里面有哪些性能优化的案例?比如 eng.uber.com/category/os…
  • Go 语言本身在持续更新迭代,每个版本在性能上有哪些重要的优化点?
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Go 框架三件套详解(Web/RPC/ORM) + + /2023/01/17/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day03/ + + Go 框架三件套详解(Web/RPC/ORM)

Go 框架三件套详解(Web/RPC/ORM)

环境搭建部分

搭建课程所需要的开发环境以及安装需要用到的软件。

学习如何安装 Docker/Postman/Git/Golang

  • 安装 Minikube 或 Docker Desktop 用于使用 Docker 安装教程
    • 可以使用 Minikube 或者使用 Docker Desktop 启动 Docker
  • 安装 Postman(使用更新的Apifox替代)
  • 安装 Git 安装教程
  • 安装 Go(Golang >= 1.15) 安装教程

框架体验部分

提前体验一下课程涉及的 HTTP/RPC/ORM 框架

HTTP 框架 Hertz 初体验

通过阅读 www.cloudwego.io/zh/docs/her… 尝试运行 Hertz 的示例代码(Hertz 框架地址: github.com/cloudwego/h…

  1. 首先安装命令行工具hz:go install github.com/cloudwego/hertz/cmd/hz@latest
  2. 生成代码 hz new -module github.com/cloudwego/hertz-examples
  3. 整理 & 拉取依赖 go mod tidy
  4. 编译并启动 go build -o hertz_demo && ./hertz_demo

RPC 框架 Kitex 初体验

通过阅读 www.cloudwego.io/zh/docs/kit… 尝试运行 Kitex 的示例代码(KItex 框架地址: github.com/cloudwego/k…

  1. 安装 kitex:go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
  2. 安装 thriftgo:go install github.com/cloudwego/thriftgo@latest
  3. 克隆该示例仓库到本地 git clone https://github.com/cloudwego/kitex-examples.git
  4. 进入示例仓库的 hello 目录 cd kitex-examples/hello
  5. 运行 server go run .
  6. 运行 client 另起一个终端后,go run ./client

ORM 框架 Gorm 初体验

通过阅读 gorm.cn/docs/#Insta… 尝试运行 Gorm 的示例代码(Gorm 框架地址: github.com/go-gorm/gor…

go get -u gorm.io/gormgo get -u gorm.io/driver/sqlite

其它知识

  • 了解一下什么IDL以及IDL的语法
  • 了解一下什么是 opentracing 以及 etcd

Etcd 与 Opentracing 是什么

  • 了解 etcd 是什么 参考文档
    • etcd是一种高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以在网络分区期间优雅地处理领导人选举,并且可以容忍机器故障,即使在领导人节点中也是如此。
  • 了解 opentracing 是什么 参考文档
    • OpenTracing是一种分布式系统链路跟踪的设计原则、规范、标准。

IDL 是什么

  • 了解 IDL 是什么 zh.m.wikipedia.org/zh-hans/%E6…
    • 接口描述语言 (Interface description language,缩写 IDL ),是用来描述软件组件介面 “介面 (程式设计)”)的一种计算机语言。IDL通过一种独立于编程语言的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Java写成。
    • IDL通常用于远程调用软件。在这种情况下,一般是由远程客户终端调用不同操作系统上的对象组件,并且这些对象组件可能是由不同计算机语言编写的。IDL建立起了两个不同操作系统间通信的桥梁。
  • Thrift IDL 语法 thrift.apache.org/docs/idl
  • proto3 IDL 语法 developers.google.com/protocol-bu…

课程笔记

直播链接:https://live.juejin.cn/4354/9899243

课程目标

  1. 将前面几节课所学到的知识应用到项目中。
  2. 掌握 Hertz/Kitex/Gorm 的基本用法。
  3. 通过学习实战案例,可以使用 Hertz/Kitex/Gorm 完成日常后端开发任务

三件套介绍

Gorm是一个已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。

Kitex是字节内部的Golang微服务RPC框架,具有高性能、强可扩展的主要特点,支持多协议并且拥有丰富的开源扩展。

Hertz是字节内部的HTTP框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用性、高性能、高扩展性特点。

Gorm的基本使用

CRUD

pS1dwvD.pngpS1wJsg.pngpS10Mm4.pngpS10sht.pngpS1bm36.png

其他操作

pS1bDEj.pngpS1bo5R.pngpS1bb26.pngpS1qixf.png

Gorm拥有丰富的扩展生态,可以使用代码生成工具、分片库方案、手动索引、乐观锁、读写分离、OpenTelemetry 扩展等等

Kitex

Hertz

]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Go 语言进阶 - 工程进阶 + + /2023/01/16/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day02/ + + Go 语言进阶 - 工程进阶

Go 语言进阶 - 工程进阶

概述

本节课程主要分为四个方面:

  1. 并发编程
  2. 依赖管理
  3. 单元测试
  4. 项目实战

详述

  • 罗列课程中涉及到的概念和相关资料,对于不熟悉的知识点,希望同学们可以提前查询预习,届时跟上直播课程进度。
  • 【必须】课程内容相关代码链接:github.com/Moonlight-Z…

并发编程

属于编程进阶内容,考虑到工程项目的可用性和可靠性,工程实践中经常会用到。

依赖管理

了解Go依赖管理演进的历程,通过课程学习以及课后实践能能够熟练使用go module 管理依赖。

单元测试

项目实战

需求模型来源

青训营话题页forum.juejin.cn/youthcamp/p…

需求

  1. 实现一个展示话题(标题,文字描述)和回帖列表的后端http接口;
  2. 本地文件存储数据

组件及技术点

课程笔记

课程链接:

语言进阶

Go可以充分发挥多核的优势,高效运行

线程:内核态,比较重量级

协程:用户态,线程可以跑多个协程,比较轻量

Goroutine

快速打印:

func hello(i int) {println("hello goroutine : " + fmt.Sprint(i))}func HelloGoRoutine() {for i := 0; i < 5; i++ {go func(j int) {hello(j)}(i)}time.Sleep(time.Second)}

最后是使用time.sleep进行阻塞,防止在协程未运行结束前主线程先运行结束了。

Channel

协程通过通信来共享内存

func CalSquare() {src := make(chan int)dest := make(chan int, 3)go func() {defer close(src)for i := 0; i < 10; i++ {src <- i}}()go func() {defer close(dest)for i := range src {dest <- i * i}}()for i := range dest {//复杂操作println(i)}}

var (x    int64lock sync.Mutex)func addWithLock() {for i := 0; i < 2000; i++ {lock.Lock()x += 1lock.Unlock()}}func addWithoutLock() {for i := 0; i < 2000; i++ {x += 1}}func Add() {x = 0for i := 0; i < 5; i++ {go addWithoutLock()}time.Sleep(time.Second)println("WithoutLock:", x)x = 0for i := 0; i < 5; i++ {go addWithLock()}time.Sleep(time.Second)println("WithLock:", x)}

WaitGroup并发同步

func ManyGoWait() {var wg sync.WaitGroupwg.Add(5)for i := 0; i < 5; i++ {go func(j int) {defer wg.Done()hello(j)}(i)}wg.Wait()}

依赖管理

GOPATH:环境变量,项目代码直接依赖src下的代码,go get下载最新的包到src目录下

Go Vendor:增加vendor文件,存放依赖包的副本,优先从vendor文件里面查找,但是仍然无法控制依赖的版本

Go Module:go.mod:依赖管理基本单元、原生库、单元依赖

测试

单元测试

  • 所有测试文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain中
func HelloTom() string {return "Tom"}func TestHelloTom(t *testing.T) {output := HelloTom()expectOutput := "Tom"assert.Equal(t, expectOutput, output)}

添加–cover参数可以评价测试代码的覆盖率

Mock测试

一些函数对本地的数据库、文件等有强依赖,在测试的同时找到这些依赖要求过高

可以使用Mock进行测试,在函数执行的时候替换成另外一个函数(打桩),从而规避掉对本地其他的强依赖

func ReadFirstLine() string {open, err := os.Open("log")defer open.Close()if err != nil {return ""}scanner := bufio.NewScanner(open)for scanner.Scan() {return scanner.Text()}return ""}func ProcessFirstLine() string {line := ReadFirstLine()destLine := strings.ReplaceAll(line, "11", "00")return destLine}func TestProcessFirstLine(t *testing.T) {firstLine := ProcessFirstLine()assert.Equal(t, "line00", firstLine)}func TestProcessFirstLineWithMock(t *testing.T) {monkey.Patch(ReadFirstLine, func() string {return "line110"})defer monkey.Unpatch(ReadFirstLine)line := ProcessFirstLine()assert.Equal(t, "line000", line)}

基准测试

对函数的运行时间进行测试:go test -bench=.

var ServerIndex [10]intfunc InitServerIndex() {for i := 0; i < 10; i++ {ServerIndex[i] = i+100}}func Select() int {return ServerIndex[rand.Intn(10)]}func FastSelect() int {return ServerIndex[fastrand.Intn(10)]}func BenchmarkSelect(b *testing.B) {InitServerIndex()b.ResetTimer()for i := 0; i < b.N; i++ {Select()}}func BenchmarkSelectParallel(b *testing.B) {InitServerIndex()b.ResetTimer()b.RunParallel(func(pb *testing.PB) {for pb.Next() {Select()}})}func BenchmarkFastSelectParallel(b *testing.B) {InitServerIndex()b.ResetTimer()b.RunParallel(func(pb *testing.PB) {for pb.Next() {FastSelect()}})}

项目实战:社区话题页面

需求

  1. 实现一个展示话题(标题,文字描述)和回帖列表的后端http接口;
  2. 本地文件存储数据

分层结构

  1. 数据层:数据Model,处理外部数据的增删改查
  2. 逻辑层:业务Entity,处理核心业务逻辑输出
  3. 视图层:视图View,处理和外部的交互逻辑

组件及技术点

具体逻辑见代码

课后实践

  1. 支持对话题发布回帖。
  2. 回帖id生成需要保证不重复、唯一性。
  3. 新加回帖追加到本地文件,同时需要更新索引,注意Map的并发安全问题
]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + Go 语言基础 - 基础语法 + + /2023/01/15/ByteDanceYouthTrainCamp/ByteDanceYouthTrainCamp-Day01/ + + Go 语言基础 - 基础语法

Go 语言基础 - 基础语法

概述

本节课程主要分为四个方面:

  1. Go 语言简介
  2. Go 语言开发入门,包括开发环境配置、基础语法、标准库
  3. Go 实战,包括三个实战项目

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前

安装 Go 语言

  1. 访问 go.dev/ ,点击 Download ,下载对应平台安装包,安装即可
  2. 如果无法访问上述网址,可以改为访问 studygolang.com/dl 下载安装
  3. 如果访问 github 速度比较慢,建议配置 go mod proxy,参考 goproxy.cn/ 里面的描述配置,下载第三方依赖包的速度可以大大加快

配置 Go 语言开发环境

可以选择安装 VS Code , 或者 Goland ,对于 VS Code,需要安装 Go 插件

下载课程示例代码

  1. Windows 平台建议安装 git,其它系统自带,安装教程
  2. 打开 github.com/wangkechun/… 克隆课程示例项目
  3. 进入课程示例项目代码目录,运行 go run example/01-hello/main.go 如果正确输出 hello world,则说明环境配置正确

学习 Go 语言基础语法

空余时间阅读 Go语言圣经(中文版)

课程笔记

课程链接:

Go语言的优势

  1. 高性能、高并发:不需要另外的库对并发进行支持
  2. 语法简单、学习曲线平缓:一周时间即可上手
  3. 丰富的标准库:与Python一样有大量的标准库,非常稳定
  4. 完善的工具链:保证代码正确稳定运行
  5. 静态链接:只需要编译后的一个文件就可以运行
  6. 快速编译:静态语言几乎最快的编译速度
  7. 跨平台:几乎支持所有设备
  8. 垃圾回收:无需考虑内存的分配释放

基础语法

1. Hello World

package mainimport ("fmt")func main() {fmt.Println("hello world")}

2. 变量

注意常量没有类型,会根据使用的上下文自动推断类型

package mainimport ("fmt""math")func main() {var a = "initial"var b, c int = 1, 2var d = truevar e float64f := float32(e)g := a + "foo"fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0fmt.Println(g)                // initialappleconst s string = "constant"const h = 500000000const i = 3e20 / hfmt.Println(s, h, i, math.Sin(h), math.Sin(i))}

3. 循环

package mainimport "fmt"func main() {i := 1for {fmt.Println("loop")break}for j := 7; j < 9; j++ {fmt.Println(j)}for n := 0; n < 5; n++ {if n%2 == 0 {continue}fmt.Println(n)}for i <= 3 {fmt.Println(i)i = i + 1}}

4. if else

package mainimport "fmt"func main() {if 7%2 == 0 {fmt.Println("7 is even")} else {fmt.Println("7 is odd")}if 8%4 == 0 {fmt.Println("8 is divisible by 4")}if num := 9; num < 0 {fmt.Println(num, "is negative")} else if num < 10 {fmt.Println(num, "has 1 digit")} else {fmt.Println(num, "has multiple digits")}}

5. switch

默认不需要添加break

可以使用任意的变量类型

package mainimport ("fmt""time")func main() {a := 2switch a {case 1:fmt.Println("one")case 2:fmt.Println("two")case 3:fmt.Println("three")case 4, 5:fmt.Println("four or five")default:fmt.Println("other")}t := time.Now()switch {case t.Hour() < 12:fmt.Println("It's before noon")default:fmt.Println("It's after noon")}}

6. 数组

真实场景下很少用,一般使用切片

package mainimport "fmt"func main() {var a [5]inta[4] = 100fmt.Println("get:", a[2])fmt.Println("len:", len(a))b := [5]int{1, 2, 3, 4, 5}fmt.Println(b)var twoD [2][3]intfor i := 0; i < 2; i++ {for j := 0; j < 3; j++ {twoD[i][j] = i + j}}fmt.Println("2d: ", twoD)}

7. 切片

package mainimport "fmt"func main() {s := make([]string, 3)s[0] = "a"s[1] = "b"s[2] = "c"fmt.Println("get:", s[2])   // cfmt.Println("len:", len(s)) // 3s = append(s, "d")s = append(s, "e", "f")fmt.Println(s) // [a b c d e f]c := make([]string, len(s))copy(c, s)fmt.Println(c) // [a b c d e f]fmt.Println(s[2:5]) // [c d e]fmt.Println(s[:5])  // [a b c d e]fmt.Println(s[2:])  // [c d e f]good := []string{"g", "o", "o", "d"}fmt.Println(good) // [g o o d]}

8. map

实际中使用最频繁,完全无序

package mainimport "fmt"func main() {m := make(map[string]int)m["one"] = 1m["two"] = 2fmt.Println(m)           // map[one:1 two:2]fmt.Println(len(m))      // 2fmt.Println(m["one"])    // 1fmt.Println(m["unknow"]) // 0r, ok := m["unknow"]fmt.Println(r, ok) // 0 falsedelete(m, "one")m2 := map[string]int{"one": 1, "two": 2}var m3 = map[string]int{"one": 1, "two": 2}fmt.Println(m2, m3)}

9. range

package mainimport "fmt"func main() {nums := []int{2, 3, 4}sum := 0for i, num := range nums {sum += numif num == 2 {fmt.Println("index:", i, "num:", num) // index: 0 num: 2}}fmt.Println(sum) // 9m := map[string]string{"a": "A", "b": "B"}for k, v := range m {fmt.Println(k, v) // b 8; a A}for k := range m {fmt.Println("key", k) // key a; key b}}

10. 函数

一般返回两个值,第一个值是真正需要的,第二个值是错误信息

package mainimport "fmt"func add(a int, b int) int {return a + b}func add2(a, b int) int {return a + b}func exists(m map[string]string, k string) (v string, ok bool) {v, ok = m[k]return v, ok}func main() {res := add(1, 2)fmt.Println(res) // 3v, ok := exists(map[string]string{"a": "A"}, "a")fmt.Println(v, ok) // A True}

11. 指针

对传入的参数进行修改

功能比较有限,不如C++丰富

package mainimport "fmt"func add2(n int) {n += 2}func add2ptr(n *int) {*n += 2}func main() {n := 5add2(n)fmt.Println(n) // 5add2ptr(&n)fmt.Println(n) // 7}

12. 结构体

传入指针避免传递的开销过大,同时也可以对结构体进行修改

package mainimport "fmt"type user struct {name     stringpassword string}func main() {a := user{name: "wang", password: "1024"}b := user{"wang", "1024"}c := user{name: "wang"}c.password = "1024"var d userd.name = "wang"d.password = "1024"fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}fmt.Println(checkPassword(a, "haha"))   // falsefmt.Println(checkPassword2(&a, "haha")) // false}func checkPassword(u user, password string) bool {return u.password == password}func checkPassword2(u *user, password string) bool {return u.password == password}

13. 结构体方法

相当于一个类成员函数

带指针就能对结构体进行修改

package mainimport "fmt"type user struct {name     stringpassword string}func (u user) checkPassword(password string) bool {return u.password == password}func (u *user) resetPassword(password string) {u.password = password}func main() {a := user{name: "wang", password: "1024"}a.resetPassword("2048")fmt.Println(a.checkPassword("2048")) // true}

14. 错误处理

package mainimport ("errors""fmt")type user struct {name     stringpassword string}func findUser(users []user, name string) (v *user, err error) {for _, u := range users {if u.name == name {return &u, nil}}return nil, errors.New("not found")}func main() {u, err := findUser([]user{{"wang", "1024"}}, "wang")if err != nil {fmt.Println(err)return}fmt.Println(u.name) // wangif u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {fmt.Println(err) // not foundreturn} else {fmt.Println(u.name)}}

15. 字符串操作

package mainimport ("fmt""strings")func main() {a := "hello"fmt.Println(strings.Contains(a, "ll"))                // truefmt.Println(strings.Count(a, "l"))                    // 2fmt.Println(strings.HasPrefix(a, "he"))               // truefmt.Println(strings.HasSuffix(a, "llo"))              // truefmt.Println(strings.Index(a, "ll"))                   // 2fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llofmt.Println(strings.Repeat(a, 2))                     // hellohellofmt.Println(strings.Replace(a, "e", "E", -1))         // hEllofmt.Println(strings.Split("a-b-c", "-"))              // [a b c]fmt.Println(strings.ToLower(a))                       // hellofmt.Println(strings.ToUpper(a))                       // HELLOfmt.Println(len(a))                                   // 5b := "你好"fmt.Println(len(b)) // 6}

16. 字符串格式化

+和#号可以打印更为详细的信息

package mainimport "fmt"type point struct {x, y int}func main() {s := "hello"n := 123p := point{1, 2}fmt.Println(s, n) // hello 123fmt.Println(p)    // {1 2}fmt.Printf("s=%v\n", s)  // s=hellofmt.Printf("n=%v\n", n)  // n=123fmt.Printf("p=%v\n", p)  // p={1 2}fmt.Printf("p=%+v\n", p) // p={x:1 y:2}fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}f := 3.141592653fmt.Println(f)          // 3.141592653fmt.Printf("%.2f\n", f) // 3.14}

17. json

注意结构体要保证大写,小写传参的问题使用反射解决

package mainimport ("encoding/json""fmt")type userInfo struct {Name  stringAge   int `json:"age"`Hobby []string}func main() {a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}buf, err := json.Marshal(a)if err != nil {panic(err)}fmt.Println(buf)         // [123 34 78 97...]fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}buf, err = json.MarshalIndent(a, "", "\t")if err != nil {panic(err)}fmt.Println(string(buf))var b userInfoerr = json.Unmarshal(buf, &b)if err != nil {panic(err)}fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}}

18. 时间处理

package mainimport ("fmt""time")func main() {now := time.Now()fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTCfmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36diff := t2.Sub(t)fmt.Println(diff)                           // 1h5m0sfmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")if err != nil {panic(err)}fmt.Println(t3 == t)    // truefmt.Println(now.Unix()) // 1648738080}

19. 数字解析

package mainimport ("fmt""strconv")func main() {f, _ := strconv.ParseFloat("1.234", 64)fmt.Println(f) // 1.234n, _ := strconv.ParseInt("111", 10, 64)fmt.Println(n) // 111n, _ = strconv.ParseInt("0x1000", 0, 64)fmt.Println(n) // 4096n2, _ := strconv.Atoi("123")fmt.Println(n2) // 123n2, err := strconv.Atoi("AAA")fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax}

20. 进程信息

package mainimport ("fmt""os""os/exec")func main() {// go run example/20-env/main.go a b c dfmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...fmt.Println(os.Setenv("AA", "BB"))buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()if err != nil {panic(err)}fmt.Println(string(buf)) // 127.0.0.1       localhost}

实战案例

猜谜游戏

  1. 生成随机数之前需要生成不同的随机种子,否则每一次运行都会输出相同的数字,一般使用时间戳来初始化
rand.Seed(time.Now().UnixNano())secretNumber := rand.Intn(maxNum)
  1. bufio.NewReader读取输入并对输入进行处理,是工程中比较常用的做法。注意读取后需要对字符串进行处理
reader := bufio.NewReader(os.Stdin)input, err := reader.ReadString('\n')input = strings.Trim(input, "\r\n")

最终代码:

package mainimport ("bufio""fmt""math/rand""os""strconv""strings""time")func main() {maxNum := 100rand.Seed(time.Now().UnixNano())secretNumber := rand.Intn(maxNum)// fmt.Println("The secret number is ", secretNumber)fmt.Println("Please input your guess")reader := bufio.NewReader(os.Stdin)for {input, err := reader.ReadString('\n')if err != nil {fmt.Println("An error occured while reading input. Please try again", err)continue}input = strings.Trim(input, "\r\n")guess, err := strconv.Atoi(input)if err != nil {fmt.Println("Invalid input. Please enter an integer value")continue}fmt.Println("You guess is", guess)if guess > secretNumber {fmt.Println("Your guess is bigger than the secret number. Please try again")} else if guess < secretNumber {fmt.Println("Your guess is smaller than the secret number. Please try again")} else {fmt.Println("Correct, you Legend!")break}}}

命令行在线词典

抓包:找到翻译网站,提交一个翻译后去控制台抓包,然后copy as cURL,可以将这个请求转到本地进行运行

为了方便,可以将这个请求copy到一些在线将请求转换为Go代码的网站,最终得到可以直接运行的代码,运行代码获得与网页返回相同的结果。

将请求的部分单独提取出来,通过用户的输入进行序列化

解析返回的响应,进行反序列化提取真正需要的部分

返回的响应也使用在线工具转换为go代码,减少工作量

最后通过命令行读入即可

最终代码:

package mainimport ("bytes""encoding/json""fmt""io""log""net/http""os")type DictRequest struct {TransType string `json:"trans_type"`Source    string `json:"source"`UserID    string `json:"user_id"`}type DictResponse struct {Rc   int `json:"rc"`Wiki struct {KnownInLaguages int `json:"known_in_laguages"`Description     struct {Source string      `json:"source"`Target interface{} `json:"target"`} `json:"description"`ID   string `json:"id"`Item struct {Source string `json:"source"`Target string `json:"target"`} `json:"item"`ImageURL  string `json:"image_url"`IsSubject string `json:"is_subject"`Sitelink  string `json:"sitelink"`} `json:"wiki"`Dictionary struct {Prons struct {EnUs string `json:"en-us"`En   string `json:"en"`} `json:"prons"`Explanations []string      `json:"explanations"`Synonym      []string      `json:"synonym"`Antonym      []string      `json:"antonym"`WqxExample   [][]string    `json:"wqx_example"`Entry        string        `json:"entry"`Type         string        `json:"type"`Related      []interface{} `json:"related"`Source       string        `json:"source"`} `json:"dictionary"`}func query(word string) {client := &http.Client{}request := DictRequest{TransType: "en2zh", Source: word}buf, err := json.Marshal(request)if err != nil {log.Fatal(err)}var data = bytes.NewReader(buf)req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)if err != nil {log.Fatal(err)}req.Header.Set("Authority", "api.interpreter.caiyunai.com")req.Header.Set("Accept", "application/json, text/plain, */*")req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")req.Header.Set("App-Name", "xy")req.Header.Set("Content-Type", "application/json;charset=UTF-8")req.Header.Set("Origin", "https://fanyi.caiyunapp.com")req.Header.Set("Os-Type", "web")req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")req.Header.Set("Sec-Ch-Ua", "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"")req.Header.Set("Sec-Ch-Ua-Mobile", "?0")req.Header.Set("Sec-Ch-Ua-Platform", "\"Windows\"")req.Header.Set("Sec-Fetch-Dest", "empty")req.Header.Set("Sec-Fetch-Mode", "cors")req.Header.Set("Sec-Fetch-Site", "cross-site")req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76")req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")resp, err := client.Do(req)if err != nil {log.Fatal(err)}defer resp.Body.Close()bodyText, err := io.ReadAll(resp.Body)if err != nil {log.Fatal(err)}if resp.StatusCode != 200 {log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))}var dictResponse DictResponseerr = json.Unmarshal(bodyText, &dictResponse)if err != nil {log.Fatal(err)}fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)for _, item := range dictResponse.Dictionary.Explanations {fmt.Println(item)}}func main() {if len(os.Args) != 2 {fmt.Fprintf(os.Stderr, `usage: simpleDict WORDexample: simpleDict hello`)os.Exit(1)}word := os.Args[1]query(word)}

Socks5代理服务器

正常浏览器访问一个网站,先和对方的网站建立TCP连接,然后正常发起HTTP请求,服务器返回响应

如果设置了代理服务器,浏览器要先和代理服务器建立TCP连接,然后代理服务器再去和真正的网站建立TCP连接,可以分为4个阶段:

  1. 协商(握手):用户的浏览器会向Socks5服务器发起请求,发送一个报文,这个报文里面包括协议版本号,支持的认证的种类等,代理服务器会从里面选择一个它自己支持的认证方式,返回给浏览器,如果返回00表示不需要认证。
  2. 认证:(这个代理不加密,认证步骤跳过)
  3. 请求:认证通过之后,浏览器会向Socks5服务器发送下一个报文,包括协议的版本号,请求的类型,一般是Connection请求,代表浏览器命令代理服务器要和某个域名,某个端口建立连接。代理服务器收到后会去和真正的网站后端服务器建立TCP连接,然后返回一个报文告诉用户浏览器已经成功建立连接了。
  4. Relay:浏览器正常发送请求,代理服务器收到请求后将请求转发到真正的服务器上,将返回的响应转发到浏览器。代理服务器并不关注流量的类别,可以是TCP或者HTTP

实现流程:

  1. 实现TCP echo server,就是发送什么就回复什么,用来测试server写的是否正确(使用 nc 127.0.0.1 1080)进行测试
  2. 实现协商阶段:测试时可以使用 curl --socks5 127.0.0.1:1080 -v http://www.qq.com进行测试,但是仅为协商,因此不会成功,但是服务器端会有正确的输出。
  3. 实现请求阶段
  4. 实现Relay阶段

最终代码:

package mainimport ("bufio""context""encoding/binary""errors""fmt""io""log""net")const socks5Ver = 0x05const cmdBind = 0x01const atypIPV4 = 0x01const atypeHOST = 0x03const atypeIPV6 = 0x04func main() {server, err := net.Listen("tcp", "127.0.0.1:1080")if err != nil {panic(err)}for {client, err := server.Accept()if err != nil {log.Printf("Accept failed %v", err)continue}go process(client)}}func process(conn net.Conn) {defer conn.Close()reader := bufio.NewReader(conn)err := auth(reader, conn)if err != nil {log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)return}err = connect(reader, conn)if err != nil {log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)return}}func auth(reader *bufio.Reader, conn net.Conn) (err error) {// +----+----------+----------+// |VER | NMETHODS | METHODS  |// +----+----------+----------+// | 1  |    1     | 1 to 255 |// +----+----------+----------+// VER: 协议版本,socks5为0x05// NMETHODS: 支持认证的方法数量// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:// X’00’ NO AUTHENTICATION REQUIRED// X’02’ USERNAME/PASSWORDver, err := reader.ReadByte()if err != nil {return fmt.Errorf("read ver failed:%w", err)}if ver != socks5Ver {return fmt.Errorf("not supported ver:%v", ver)}methodSize, err := reader.ReadByte()if err != nil {return fmt.Errorf("read methodSize failed:%w", err)}method := make([]byte, methodSize)_, err = io.ReadFull(reader, method)if err != nil {return fmt.Errorf("read method failed:%w", err)}// +----+--------+// |VER | METHOD |// +----+--------+// | 1  |   1    |// +----+--------+_, err = conn.Write([]byte{socks5Ver, 0x00})if err != nil {return fmt.Errorf("write failed:%w", err)}return nil}func connect(reader *bufio.Reader, conn net.Conn) (err error) {// +----+-----+-------+------+----------+----------+// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |// +----+-----+-------+------+----------+----------+// | 1  |  1  | X'00' |  1   | Variable |    2     |// +----+-----+-------+------+----------+----------+// VER 版本号,socks5的值为0x05// CMD 0x01表示CONNECT请求// RSV 保留字段,值为0x00// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。//   0x01表示IPv4地址,DST.ADDR为4个字节//   0x03表示域名,DST.ADDR是一个可变长度的域名// DST.ADDR 一个可变长度的值// DST.PORT 目标端口,固定2个字节buf := make([]byte, 4)_, err = io.ReadFull(reader, buf)if err != nil {return fmt.Errorf("read header failed:%w", err)}ver, cmd, atyp := buf[0], buf[1], buf[3]if ver != socks5Ver {return fmt.Errorf("not supported ver:%v", ver)}if cmd != cmdBind {return fmt.Errorf("not supported cmd:%v", ver)}addr := ""switch atyp {case atypIPV4:_, err = io.ReadFull(reader, buf)if err != nil {return fmt.Errorf("read atyp failed:%w", err)}addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])case atypeHOST:hostSize, err := reader.ReadByte()if err != nil {return fmt.Errorf("read hostSize failed:%w", err)}host := make([]byte, hostSize)_, err = io.ReadFull(reader, host)if err != nil {return fmt.Errorf("read host failed:%w", err)}addr = string(host)case atypeIPV6:return errors.New("IPv6: no supported yet")default:return errors.New("invalid atyp")}_, err = io.ReadFull(reader, buf[:2])if err != nil {return fmt.Errorf("read port failed:%w", err)}port := binary.BigEndian.Uint16(buf[:2])dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))if err != nil {return fmt.Errorf("dial dst failed:%w", err)}defer dest.Close()log.Println("dial", addr, port)// +----+-----+-------+------+----------+----------+// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |// +----+-----+-------+------+----------+----------+// | 1  |  1  | X'00' |  1   | Variable |    2     |// +----+-----+-------+------+----------+----------+// VER socks版本,这里为0x05// REP Relay field,内容取值如下 X’00’ succeeded// RSV 保留字段// ATYPE 地址类型// BND.ADDR 服务绑定的地址// BND.PORT 服务绑定的端口DST.PORT_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})if err != nil {return fmt.Errorf("write failed: %w", err)}ctx, cancel := context.WithCancel(context.Background())defer cancel()go func() {_, _ = io.Copy(dest, reader)cancel()}()go func() {_, _ = io.Copy(conn, dest)cancel()}()<-ctx.Done()return nil}

课后

Go 语言学习路线图

]]>
+ + + + + Study + + + + + + + Backend + + ByteDance + + + +
+ + + + + 常用软件常用命令 + + /2023/01/13/Software-Commands/ + + 常用软件常用命令

产生随机字符串

head -c 32 /dev/random | base64

sudo免密码设置

sudo visudo

压缩相关

tar -cvf temp.tar temp.txt temp/tar -xvf temp.tar -C temp/tar cvzf - pic | split -b 10m -d - piccat pic* > pic.tar.gztar xvzf pic.tar.gz

Conda

描述命令
查看都有什么环境conda info --envs
查看当前环境有什么包conda list
创建环境conda create -n env_name python=version package_names
安装包conda install name=version
离线安装包conda install --use-local name
导出当前环境conda env export > name.yaml
导出base环境需要更换名称conda create -n new_name --clone base
复制后导入conda env create -f name.yaml
删除环境下的某个包conda env remove -n your_env_name package_name
删除环境conda env remove --name your_env_name
打包环境conda pack -n my_env -o out_name.tar.gz
利用打包的环境复现tar -xzf my_env.tar.gz -C my_env && source my_env/bin/activate

Anaconda环境离线迁移移植

Linux 系统安装

sudo apt updatesudo apt install vimsudo cp /etc/apt/sources.list /etc/apt/sources.list.baksudo vim /etc/apt/sources.listsudo apt updatesudo apt upgradesudo apt install python3-pipsudo mkdir ~/.pipcd ~/.pipsudo vim pip.conf# [global]# index-url=https://mirrors.aliyun.com/pypi/simple/ # https://pypi.org/simplevim ~/.bashrcexport PATH=~/mypath/bin:$PATHsource ~/.bashrc

添加%zhangzhao ALL=(ALL:ALL) NOPASSWD: ALL 到最后一行 , 后 ctrl+o, 回车 , ctrl+x 退出

VSCode

How can I install vscode-server in linux offline

科学上网相关

V2ray安装

安装说明:https://www.v2fly.org/guide/install.html

脚本下载地址:https://github.com/v2fly/fhs-install-v2ray

执行脚本前首先临时更改hosts,避免github连接不上的情况

参考仓库:https://github.com/521xueweihan/GitHub520

执行脚本下载安装V2ray

bash install-release.sh

安装后的文件位置:

installed: /usr/local/bin/v2rayinstalled: /usr/local/share/v2ray/geoip.datinstalled: /usr/local/share/v2ray/geosite.datinstalled: /usr/local/etc/v2ray/config.jsoninstalled: /var/log/v2ray/installed: /var/log/v2ray/access.loginstalled: /var/log/v2ray/error.loginstalled: /etc/systemd/system/v2ray.serviceinstalled: /etc/systemd/system/v2ray@.service

按照上面的配置文件路径写入配置文件(可以直接从Windows客户端中copy过来)

V2ray命令:

# 启动V2raysystemctl start v2ray# 检查V2ray状态systemctl status v2ray# 设置V2ray开机自启动systemctl enable v2ray

测试:

curl -x socks5://127.0.0.1:10808 https://www.google.com -v

clash安装

下载地址:https://github.com/Dreamacro/clash/releases/tag/v1.12.0

解压并赋予权限:

gzip -d clash-linux-amd64-v1.11.4.gzchmod a+x clash-linux

有的代理服务商会直接给出配置文件config.yaml,如果没有,可以将订阅链接直接粘贴在浏览器网址栏,然后搜索,会直接下载下来文件或者展示出配置文件,如果搜索到的是一大堆字符则需要在订阅链接的后面添加 &flag=clash ,然后会下载下来一个文件,将其更名为config.yaml即可

然后替换~/.config/clash下自动生成的config.yaml,删除Country.mmdb文件,然后再次执行 ./clash-linux

即可以使用

释放9090端口后可以通过Web端查看:http://clash.razord.top

(WSL2 git push时候可能会遇到错误,解决方法:将下述代码粘贴到~/.ssh/config文件中)

Host github.comHostname ssh.github.comPort 443

proxychains安装

安装proxychains从而避免全局代理

apt install proxychains4

配置文件:(/etc/proxychains.conf)

# proxychains.conf  VER 4.x##        HTTP, SOCKS4a, SOCKS5 tunneling proxifier with DNS.# The option below identifies how the ProxyList is treated.# only one option should be uncommented at time,# otherwise the last appearing option will be accepted## dynamic_chain## Dynamic - Each connection will be done via chained proxies# all proxies chained in the order as they appear in the list# at least one proxy must be online to play in chain# (dead proxies are skipped)# otherwise EINTR is returned to the app#strict_chain## Strict - Each connection will be done via chained proxies# all proxies chained in the order as they appear in the list# all proxies must be online to play in chain# otherwise EINTR is returned to the app##round_robin_chain## Round Robin - Each connection will be done via chained proxies# of chain_len length# all proxies chained in the order as they appear in the list# at least one proxy must be online to play in chain# (dead proxies are skipped).# the start of the current proxy chain is the proxy after the last# proxy in the previously invoked proxy chain.# if the end of the proxy chain is reached while looking for proxies# start at the beginning again.# otherwise EINTR is returned to the app# These semantics are not guaranteed in a multithreaded environment.##random_chain## Random - Each connection will be done via random proxy# (or proxy chain, see  chain_len) from the list.# this option is good to test your IDS :)# Make sense only if random_chain or round_robin_chain#chain_len = 2# Quiet mode (no output from library)# quiet_mode# Proxy DNS requests - no leak for DNS dataproxy_dns# set the class A subnet number to use for the internal remote DNS mapping# we use the reserved 224.x.x.x range by default,# if the proxified app does a DNS request, we will return an IP from that range.# on further accesses to this ip we will send the saved DNS name to the proxy.# in case some control-freak app checks the returned ip, and denies to # connect, you can use another subnet, e.g. 10.x.x.x or 127.x.x.x.# of course you should make sure that the proxified app does not need# *real* access to this subnet. # i.e. dont use the same subnet then in the localnet section#remote_dns_subnet 127 #remote_dns_subnet 10remote_dns_subnet 224# Some timeouts in millisecondstcp_read_time_out 15000tcp_connect_time_out 8000### Examples for localnet exclusion## localnet ranges will *not* use a proxy to connect.## Exclude connections to 192.168.1.0/24 with port 80# localnet 192.168.1.0:80/255.255.255.0## Exclude connections to 192.168.100.0/24# localnet 192.168.100.0/255.255.255.0## Exclude connections to ANYwhere with port 80# localnet 0.0.0.0:80/0.0.0.0## RFC5735 Loopback address range## if you enable this, you have to make sure remote_dns_subnet is not 127## you'll need to enable it if you want to use an application that ## connects to localhost.# localnet 127.0.0.0/255.0.0.0## RFC1918 Private Address Ranges# localnet 10.0.0.0/255.0.0.0# localnet 172.16.0.0/255.240.0.0# localnet 192.168.0.0/255.255.0.0# ProxyList format#       type  ip  port [user pass]#       (values separated by 'tab' or 'blank')##       only numeric ipv4 addresses are valid###        Examples:##            socks5192.168.67.781080lamersecret#http192.168.89.38080justuhidden# socks4192.168.1.491080#        http192.168.39.938080###       proxy types: http, socks4, socks5#        ( auth types supported: "basic"-http  "user/pass"-socks )#[ProxyList]# add proxy here ...# meanwile# defaults set to "tor"socks5 127.0.0.1 10808http 127.0.0.1 10809

需要走代理的命令在命令开头添加proxychains即可

全局代理:(似乎对软件内部,例如go没有作用)

export http_proxy=http://127.0.0.1:10809export https_proxy=https://127.0.0.1:10809

Go安装

下载安装

网站说明:https://golang.google.cn/doc/install

wget https://golang.google.cn/dl/go1.19.5.linux-amd64.tar.gz

删除旧版本并解压安装包:

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.5.linux-amd64.tar.gz

编辑配置文件,增加环境变量:

vim ~/.bashrcexport PATH=$PATH:/usr/local/go/binsource ~/.bashrc

验证安装:

go version

配置

查看配置:

go env

修改配置:

go env -w GO111MODULE=ongo env -w GOPROXY=https://goproxy.io,direct

拉取私有仓库的包

go env -w GOPRIVATE=gitlab.appshahe.com

除配置私有仓库的地址外,还需要将走http或者https的协议转到git协议上

具体命令:

git config --global url."git@gitlab.appshahe.com:".insteadOf "https://gitlab.appshahe.com/"git config --global url."git@gitlab.appshahe.com:".insteadOf "http://gitlab.appshahe.com/"

具体的更改会体现在 ~/.gitconfig里面

MySQL

docker安装直接可以远程访问,不需要任何配置操作

apt install mysql-server

运行mysql服务并查看是否正在运行

service mysql startservice mysql status

刚开始安装不能使用用户名和密码访问,需要更换为原来的密码验证方式

mysqlALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';FLUSH PRIVILEGES;

创建数据库:

CREATE DATABASE simpledy

增加远程访问的用户

取消bind-address=127.0.0.1

vim /etc/mysql/mysql.conf.d/mysqld.cnf

密码生成为随机字符串:

head -c 8 /dev/random | base64

创建用户:

CREATE USER 'dymysql'@'%' IDENTIFIED BY 'gxnw21XxRhY';

更改密码验证方式:

ALTER USER 'dymysql'@'%' IDENTIFIED WITH mysql_native_password BY 'gxnw21XxRhY';

授予用户某个数据库的全部权限:

GRANT ALL PRIVILEGES ON `simpledy`.* TO `dymysql`@`%` WITH GRANT OPTION;

撤销某个用户对某个数据库的全部权限:

REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'dymysql';

刷新缓存:

FLUSH PRIVILEGES;

展示某个用户的权限:

SHOW GRANTS FOR 'dymysql'@'%';

查看已有用户以及是否可以远程登录:

select host,user,plugin from mysql.user;

Redis

docker安装配置成功

apt install redis-server

运行并查看是否正在运行

service redis-server startservice redis-server status

设置redis密码

打开redis配置文件 /etc/redis/redis.conf

找到requirepass,修改即可

配置 Redis 远程访问

默认情况下,Redis 不允许远程连接。只能从127.0.0.1(localhost)连接 Redis 服务器

打开redis配置文件 /etc/redis/redis.conf

注释掉 bind 127.0.0.1 ::1

关闭保护模式 protected-mode no

重启Redis服务:service redis-server restart

(注意WSL的ip要到WSL里面去看)

RabbitMQ

官网安装脚本:https://www.rabbitmq.com/install-debian.html#apt-cloudsmith

注意修改apt-get为apt,将软件源设置为对应版本(如Ubuntu22.04为jammy)

查看安装状态:

service rabbitmq-server status

打开管理界面:

rabbitmq-plugins enable rabbitmq_management

通过http://localhost:15672/#/进行查看

默认的guest用户,密码为guest,具有超级管理员权限,无法远程登录

创建用户并设置密码:

add_user root QxdkQeiIUNY

管理用户角色:

  • 超级管理员(administrator):可登陆管理控制台(启用management plugin的情况下),可查看所有的信息,并且可以对用户,策略(policy)进行操作。
  • 监控者(monitoring):可登陆管理控制台(启用management plugin的情况下),同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)。
  • 策略制定者(policymaker):可登陆管理控制台(启用management plugin的情况下), 同时可以对policy进行管理。但无法查看节点的相关信息。
  • 普通管理者(management):仅可登陆管理控制台(启用management plugin的情况下),无法看到节点信息,也无法对策略进行管理。
  • 其他:无法登陆管理控制台,通常就是普通的生产者和消费者。(最后项目中使用的)
rabbitmqctl set_user_tags root administrator

查看当前的用户及角色:

rabbitmqctl list_users

不需要开启远程连接,自动支持

然后进入到管理页面中,对virtual hosts进行设置(相当于数据库中的db)

然后即可使用程序等跑通

FFmpeg

apt install ffmpeg

Nginx

apt install nginx

配置文件:/etc/nginx/nginx.conf

增加mp4支持:

apt install nginx-extras

vsftpd

apt install vsftpd

Protobuf

下载protobuf官方的protoc工具(tar.gz版本

编译安装:

# 安装需要的工具包apt install autoconf automake libtool curl make g++ unzip# 解压安装包tar xvf protobuf-21.12.tar.gz && cd protobuf-21.12# 编译安装./autogen.sh./configuremake && make installldconfig# 验证安装protoc --version

安装go语言插件:

go get -u google.golang.org/protobuf/cmd/protoc-gen-gogo get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc

将执行文件添加到环境变量中:

export PATH=$PATH:/root/go/bin

执行:

protoc --go_out=. video.proto

Consul

下载地址:https://developer.hashicorp.com/consul/downloads

解压后直接执行即可

Docker

官网安装方法

核心思想:

  1. Add Docker’s official GPG key
root@hecs-296470:/etc/apt/keyrings# cd /etc/apt/keyringsroot@hecs-296470:/etc/apt/keyrings# lsdocker.gpg
  1. 添加可以下载docker的源
root@hecs-296470:/etc/apt/sources.list.d# lsdocker.listroot@hecs-296470:/etc/apt/sources.list.d# cat docker.listdeb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu   jammy stableroot@hecs-296470:/etc/apt/sources.list.d#
  1. 安装docker
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

换源:

vim /etc/docker/daemon.json

写入源:

{    "registry-mirrors": ["https://hub-mirror.c.163.com","https://ustc-edu-cn.mirror.aliyuncs.com","https://ghcr.io","https://mirror.baidubce.com"    ]}

重启docker:

systemctl daemon-reloadsystemctl restart docker

环境相关

准备重装编程环境,以Docker为基础,既能开发,又能方便部署,同时不损害原有的其他环境

但是Docker Desktop坑点太多,且占用资源巨大,因此不安装Windows环境下面的Docker,而是在WSL内部安装Docker,VSCode通过SSH方式跨过WSL访问容器。

Docker安装

与上面的Docker安装基本相同,不过注意每一次重启WSL的时候要手动重启Docker,否则无法使用Docker

service docker start

网络桥接

由于WSL的ip会总变化,这里准备配桥接模式,我的理解是WSL与主机的地位相同,在内网中都有自己的ip,这样无论是互相访问还是访问外网都没有什么问题。

参考资料:

官方文档

WSL2 网络的最终解决方案

WSL2 静态IP(固定IP)不需要自动化脚本的设置方案

常用命令

描述命令
查询容器sudo docker ps -a
删除容器sudo docker rm 容器ID
查询镜像sudo docker images
删除镜像sudo docker rmi 镜像ID(要先删除掉容器才能删除掉镜像)
拉取镜像sudo docker pull python:3.8.13(去dockerhub上找合适的版本)
根据镜像启动容器并挂载数据docker run -v 绝对路径:/mnt --gpus all --shm-size=6g -it python:3.8.13 /bin/bash
启动已经停止的容器sudo docker start ID
进入某个容器的终端sudo docker exec -it ID /bin/bash
将容器转为镜像并上传到dockerhub-登录docker login
将容器转为镜像并上传到dockerhub-提交sudo docker commit 容器ID zhangzhao219/仓库名(也是将容器写回到镜像中的操作)
将容器转为镜像并上传到dockerhub-打标签sudo docker tag zhangzhao219/仓库名 zhangzhao219/TAG
将容器转为镜像并上传到dockerhub-上传sudo docker push zhangzhao219/仓库名:TAG
导出容器到文件sudo docker export -o *.tar 容器ID
从文件导入容器(会直接变为镜像)sudo docker import IR.tar 名称
]]>
+ + + + + Tools + + + + + + + Backend + + Softwarer + + + +
+ + + + + MIT-6.824 Distributed Systems-LEC 5 Fault Tolerance-Raft-1 + + /2023/01/10/6.824/Distributed-Systems-MIT-6.824-LEC-5/ + + MIT-6.824(Spring 2022)LEC 5 Fault Tolerance-Raft-1

In Search of an Understandable Consensus Algorithm (Raft) 论文阅读

参考翻译

参考总结

摘要

一致性算法,或者说 共识算法 ,让⼀组机器像⼀个整体⼀样工作,即使其中⼀些机器出现故障也能够继续工作。

Raft 是⼀种为了管理复制日志的⼀致性算法。

它将⼀致性算法分解成了几个关键模块:领导人选举、日志复制和安全性。同时它通过更强的⼀致性来 减少状态机的数量

总之,对比传统的一致性算法 Paxos,Raft 更清晰易懂,易于实现。

1. 简介

一致性算法允许多台机器作为一个集群协同工作,并且在其中的某几台机器出故障时集群仍然能正常工作。正因为如此,一致性算法在建立可靠的大规模软件系统方面发挥了重要作用。在过去十年中,Paxos 主导了关于一致性算法的讨论:大多数一致性的实现都是基于 Paxos 或受其影响,Paxos 已经成为教授学生关于一致性知识的主要工具。然而尽管很多人一直在努力尝试使 Paxos 更易懂,Paxos 还是太难理解了。此外,Paxos 的架构需要复杂的改变来支持实际系统。

我们开始着手寻找一个新的一致性算法,希望可以为系统开发和教学提供更好的基础。 我们的方法是不寻常的,因为我们的主要目标是可理解性。在该算法的设计中,重要的不仅是如何让算法起作用,还要清晰地知道该算法为什么会起作用。这项工作的结果是一个称为 Raft 的一致性算法。在设计 Raft 时,我们使用了特定的技术来提高它的可理解性,包括:

  • 分解(Raft 分离出三个关键点:leader election、log replication、safety)
  • 减少状态空间(相比于 Paxos,Raft 降低了不确定性的程度和服务器之间的不一致)

一项针对 2 所大学共 43 名学生的用户研究表明,Raft 比 Paxos 更容易理解:在学习两种算法后,其中 33 名学生能够更好地回答 Raft 的相关问题。

Raft 在许多方面类似于现有的公式算法,但它有几个新特性:

  • Strong leader(强领导性):相比于其他算法,Raft 使用了更强的领导形式。比如,日志条目只能从 leader 流向 follower(集群中除 leader 外其他的服务器)。这在使 Raft 更易懂的同时简化了日志复制的管理流程。
  • Leader election(领导选举):Raft 使用随机计时器来进行领导选举。任何一致性算法都需要心跳机制,Raft 只需要在这个基础上,添加少量机制,就可以简单快速地解决冲突。
  • Membership changes(成员变更):Raft 在更改集群中服务器集的机制中使用了 联合一致性 的方法。在联合一致性下,在集群配置的转换过程中,新旧两种配置大多数是重叠的,这使得集群在配置更改期间可以继续正常运行。

我们认为 Raft 跟 Paxos 以及其他一致性算法相比是更优的,这不仅体现在教学方面,还体现在工程实现方面。

  • 它比其他算法更简单且更易于理解
  • 它被描述得十分详细足以满足实际系统的需要
  • 它有多个开源实现,并被多家公司使用
  • 它的安全性已被正式规定和验证
  • 它的效率与其他算法相当

2. 复制状态机

一致性算法基于复制状态机

一致性算法一般都是在 复制状态机 的背景下实现的。在这种方法下,一组服务器在的状态机计算相同状态的相同副本,即使某些服务器崩溃,它们也可以继续运行。

复制状态机是用来解决分布式系统中的各种容错问题。比如说,具有单个 leader 的大规模的系统,如 GFS,HDFS 和 RAMCloud ,他们通常都使用单独的复制状态机来管理 leader election 和保存 leader 崩溃后重新选举所需的配置信息。像 Chubby 和 ZooKeeper 都是复制状态机。

复制状态机通常都是使用日志复制(log replication)来实现。

如图:每个服务器都保存着一份拥有一系列命令的日志,然后服务器上的状态机会按顺序执行日志中的命令。每一份日志中命令相同并且顺序也相同,因此每个状态机可以处理相同的命令序列。所以状态机是可确定的,每个状态机都执行相同的状态和相同的输出序列。

一致性算法的主要工作就是保证复制日志(replicated log)的一致性 。每台服务器上的一致性模块接收来自客户端的命令,并将这些命令添加到其日志当中。一致性模块与其他服务器上的一致性模块进行通信,以确保每台服务器上最终以相同的顺序包含相同的命令,即使部分服务器崩溃了,这个条件也可以满足。一旦命令被正确复制,每台服务器上的状态机就会按日志顺序处理它们,并将输出返回给客户端。这样就形成了高可用的复制状态机。

适用于实际系统的 一致性算法通常都包含以下几点特征

  • 安全性:非拜占庭错误(出现故障(crash 或 fail-stop,即不响应)但不会伪造信息)情况下,绝不会返回错误的结果
  • 可用性:只要大多数机器(过半)正常就可保证可用。假设服务器崩溃了,一小段时间后,它们很可能会根据已经稳定存储的状态来进行恢复,并重新加入集群。
  • 不依赖时序保证一致性:错误的时钟和极端消息延迟在最坏的情况下会产生影响可用性的一系列问题。
  • 在通常情况下,只要集群中大部分(过半)服务器已经响应了单轮远程过程调用(RPC),命令就可以被视为完成, 小部分慢节点不影响整体性能

3. Paxos 算法的问题

在过去的十年间,Leslie Lamport 的 Paxos 协议 几乎成为一致性的同义词。它是课堂上被教授最多的一致性协议,大多数一致性的实现也是以它为起点。Paxos 首先定义了能在单个决策问题(例如单个复制日志条目)上达成一致性的协议。我们将这个子集称为 single-decree Paxos 。然后 Paxos 组合该协议的多个实例去实现一系列决策,比如日志(multi-Paxos)。Paxos 保证了安全性和活性,它也支持改变集群中的成员,它的安全性也已经被论证了,并且大多数情况下都是高效的。

美中不足的是,Paxos 有两个严重的缺点:

Paxos 非常难理解

众所周知,Paxos 非常晦涩难懂,除非下了很大的功夫,很少有人能够成功理解它。因此,尽管目前已经有几个尝试希望将 Paxos 解释得通俗易懂一些,而且这些解释都集中在 single-decree Paxos,但是它们还是很难懂。在对 NSDI 2012 参会者的非正式调查中,我们发现很少人会喜欢 Paxos,即使是经验丰富的研究人员。我们自己也一直在跟 Paxos 作斗争,我们也无法完全理解整个 Paxos 协议,直到阅读了几个更简单的描述和自己设计了替代 Paxos 的协议,我们才对 Paxos 有了比较深刻的理解。但这个过程,花了将近一年。我们推测 Paxos 这么晦涩难懂,主要是因为作者选择了 Single-decree Paxos 来作为基础。Single-decree Paxso 非常搞人:它分为两个阶段,但是并没有对这两个阶段进行简单直观的说明,而且这两个阶段也不能分开了单独理解,所以使用者将就很难理解为什么该算法能起作用。Multi-Paxos 的合成规则又增加了许多复杂性。我们相信,对多个决定(日志,并非单个日志条目)达成一致性的总体问题可以用其他更直接和更明显的方式进行分解。

Paxos 没有为实际实现提供一个良好的基础

其中一个原因是没有广泛认同的针对 Multi-Paxos 的算法。Lamport 的描述主要是针对 signle-decree Paxos 的,他描述了针对 multi-Paxos 的可能方法,但缺少了很多细节。目前已经有人在尝试具体化和优化 Paxos,但是这些尝试都互不相同并且它们跟 Lamport 描述的也不尽相同。虽然像 Chubby 这样的系统已经实现了类 Paxos 算法,但是他们并没有透露出很多的实现细节。

此外,Paxos 的架构对于构建实际系统来说其实是一个糟糕的设计,这是 single-decree Paxos 分解的另一个结果。举个例子,这对于独立选择地日志条目的集合,然后再将它们合并到顺序日志当中没有任何好处,这只会增加复杂性。围绕日志来设计系统是更加简单和高效的方法,其中新条目按受约束的顺序依次附加。另外一个问题是 Paxos 在其核心使用了 对称对等方法 (尽管它最终表明了这会被用作一种性能优化的弱领导模式)。这在只有一个决策的情况下是有意义的,但是尽管如此,还是很少有实际系统采用了这种方法。如果有一系列的决策需要制定,更简单和更快速的方法应该是首先选择一个 leader,然后由 leader 去协调这些决策。

因此,按照 Paxos 来实现的实际系统往往跟 Paxos 相差很大。几乎所有的实现都是从 Paxos 开始,然后在实现的过程中发现了一系列的难题,在解决难题的过程中,开发出了跟 Paxos 完全不一样的架构。这样既费时又容易出错,而且 Paxos 本身的晦涩难懂又使得问题变得更加严重。Paxos 公式可能是证明其正确性的一个很好的公式,但真正的实现与 Paxos 又相差很大,这证明了它其实没有什么价值。来自 Chubby 作者的评论非常典型:在 Paxos 算法描述和现实实现系统之间有着巨大的鸿沟,如果一直按照 Paxos 算法走下去,最终的系统往往会建立在一个还未被证明的协议之上。

综合上述问题,我们觉得 Paxos 在教学端和系统构建端都没有提供一个良好的基础。考虑到共识性在大规模软件系统中的重要性,我们决定去尝试一下看看能不能设计一个替代 Paxos 并且具有更好特性的共识算法。

Raft 算法就是尝试克服以上缺点,替代 Paxos 的一致性算法。

4. 为了可理解性的设计

设计 Raft 的初衷:

  • 提供⼀个完整的实际的系统实现基础:大大减少开发者的工作
  • 任何情况下都是安全的
  • 大多数的情况下都是可用的
  • 大部分操作必须是高效的
  • 可理解性:(最重要、最大挑战)保证大多数人都可以容易理解。
  • 能够让人形成直观的认识:使系统的构建者能够在现实中进行必然的扩展。

在设计 Raft 算法的过程中,很多情况下我们需要在多个备选方案下做出抉择。在这种情况下,我们往往会基于可理解性来进行抉择:

  • 解释各个备选方案的难度有多大?例如,它的状态空间有多复杂?它是否具有难以理解的含义?
  • 对于一个读者来说,完成理解这个方案和方案中的各种含义是否简单?

我们意识到这一的分析具有高度的主观性。所以我们采取了两种通用的措施来解决这个问题。

  1. 第一个措施就是众所周知的 问题分解 :只要有可能,我们就将问题划分成几个相对独立地解决、解释和理解的子问题。例如,Raft 算法被我们划分成 leader 选举、日志复制、安全性和成员变更几个部分。
  2. 第二个措施是 通过减少状态的数量来简化状态空间 ,尽可能地使系统变得更加连贯和尽可能地消除不确定性。很明显的一个例子就是,所有的日志都是不允许有空挡的,并且 Raft 限制了日志之间可能不一样的方式。尽管在大多数情况下我们都极力去消除不确定性,但是在某些情况下不确定性却可以提高可理解性。一个重要的例子就是随机化方法,它们虽然引入了不确定性,但是它们往往能够通过以类似的方式处理所有可能的选择来减少状态空间(随便选,没关系)。所以我们使用了随机化来简化 Raft 中的 leader election 算法。

5. Raft 一致性算法

Raft 是一种用来管理第2节中提到的复制日志(replicated log)的算法

Raft算法的关键特性:

pSn0Ve0.png

Raft算法的简略版:

pSnwXLt.jpg

Raft 选举一个 Leader ,给予管理所有复制日志的权限,由此实现一致性。

Leader 从客户接受指令,写入日志,复制到其他 Backup Server 上,在保证安全性时通知其他 Server 根据日志执行指令更新状态机。

Leader 大大简化了对复制日志的管理。leader 可以自行决定新日志写入位置,数据都从 Leader 流向其他 Server。当 Leader 宕机,从其他 Server 中选举一个新 Leader。

Raft 将一致性问题分解为 三个子问题

  • Leader election(领导选举):一个 leader 倒下之后,一定会有一个新的 leader 站起来。
  • Log replication(日志复制):leader 必须接收来自客户端的日志条目然后复制到集群中的其他节点,并且强制其他节点的日志和自己的保持一致。
  • Safety(安全性):Raft 中安全性的关键是状态机的安全性:只要有任何服务器节点将一个特定的日志条目应用到它的状态机中,那么其他服务器节点就不能在同一个日志索引位置上存储另外一条不同的指令。此处还涉及一个额外的选举机制上的限制。

5.0 Raft算法的关键特性与简略说明

State(状态)

所有服务器上持久存在的:

(在响应RPCs之前已在稳定存储上进行更新)

状态变量说明
currentTerm服务器最后⼀次知道的最新的任期号(初始化为 0,持续递增)
votedFor在当前任期获得选票的候选人的id(如果没有则为 null)
log[]日志条目集;每⼀个条目包含⼀个用户状态机执行的指令,和收到时的任期号

所有服务器上经常变的:

状态变量说明
commitIndex已知的最大的已经被提交的日志条目的索引值
lastApplied最后被应用到状态机的日志条目索引值(初始化为 0,持续递增)

在leader里面经常改变的:

(选举后重新初始化)

状态变量说明
nextIndex[]对于每⼀个服务器,需要发送给他的下⼀个日志条目的索引值(初始化为领导人最后索引值加1)
matchIndex[]对于每⼀个服务器,已经复制给他的日志的最高索引值

AppendEntries RPC(追加待同步日志 RPC)

由 Leader 负责调用来复制日志(5.3);也会用作心跳机制(5.2)

传入参数:

状态变量说明
termLeader的任期号
leaderIdLeader的 id,以便于跟随者重定向请求
prevLogIndex新的日志条目紧随之前的索引值
prevLogTermprevLogIndex 条目的任期号
entries[]准备存储的日志条目(表示心跳时为空;⼀次性发送多个是为了提高效率)
leaderCommitLeader已经提交的日志的索引值

返回值:

状态变量说明
term当前的任期号,用于Leader去更新自己
success跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真

接收者实现:

  1. 如果 term < currentTerm 就返回 false (5.1 节)
  2. 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false (5.3 节)
  3. 如果现有的日志条目和新的产⽣冲突(索引值相同但是任期号不同),删除现有的和之后所有的条目 (5.3 节)
  4. 追加日志中尚未存在的任何新条目
  5. 如果 leaderCommit > commitIndex ,令 commitIndex = min(leaderCommit, 新日志条目索引)

RequestVote RPC(请求投票 RPC)

由候选人调用用来征集选票(5.2 节)

传入参数

状态变量说明
term候选人的任期号
candidateId请求选票的候选人的 Id
lastLogIndex候选人的最后日志条目的索引值
lastLogTerm候选人最后日志条目的任期号

返回值

状态变量说明
term当前任期号,以便于候选人去更新自己的任期号
voteGranted候选人赢得了此张选票时为 true

接收者实现:

  1. 如果 term < currentTerm 返回 false (5.2 节)
  2. 如果 votedFornull 或者为 candidateId,并且候选人的日志至少和接受者一样新,那么就给它投票(5.2 节,5.4 节)

Rules for Servers(服务器的规则)

所有服务器

  • 如果 commitIndex > lastApplied,那么就将 lastApplied 加一,并把 log[lastApplied] 应用到状态机中(5.3 节)
  • 如果接收到的 RPC 请求或响应中,任期号 T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为 Follower(5.1 节)

Followers(跟随者)(5.2 节):

  • 响应来自候选人和 Leader 的 RPC 请求
  • 如果选举超时,都没有收到现任 Leader 的 AppendEntries RPC,也没有给候选人投票:则自己转变成候选人。

Candidates(候选人)(5.2 节):

  • 在转变成候选人后就立即开始选举过程
    • 自增当前的任期号(currentTerm
    • 给自己投票
    • 重置选举超时计时器
    • 发送 RequestVote RPC 给其他所有服务器
  • 如果接收到大多数服务器的选票,那么就变成 Leader
  • 如果接收到来自新的 Leader 的 AppendEntries RPC,转变成 follower
  • 如果选举过程超时,再次发起一轮选举

Leader(领导人):

  • 一旦成为Leader:发送初始的空 AppendEntries RPCs(心跳)给每个服务器;在空闲期间重复发送,防止选举超时(5.2 节)
  • 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节)
  • 如果一个 follower 最后日志条目的索引值 index ≥ nextIndex,那么:使用 AppendEntries RPC 发送从 nextIndex 开始的所有日志条目:
    • 如果成功:更新相应跟随者的 nextIndexmatchIndex
    • 如果 AppendEntries 因为日志不一致而失败,减少 nextIndex 并重试
  • 如果存在一个满足 N > commitIndex 的 N,并且大多数的 matchIndex[i] ≥ N 成立,并且 log[N].term == currentTerm 成立,那么令 commitIndex = N (5.3 和 5.4 节)

关键特性

特性解释
选举安全对于一个给定的任期号,最多只会有一个 Leader 被选举出来(5.2 节)
Leader 只追加Leader 绝对不会删除或者覆盖自己的日志,只会增加(5.3 节)
日志匹配特性如果两个日志在相同的索引位置的日志条目的任期号相同,那么我们就认为这个日志从头到这个索引位置之间全部完全相同(5.3 节)
领导人完全特性如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有领导人中(5.4 节)
状态机安全特性如果一个 Leader 已经在给定的索引值位置的日志条目应用到状态机中,那么其他任何的服务器在这个索引位置不会提交一个不同的日志(5.4.3 节)

5.1 Raft 基础

一个 Raft 集群通常包含 5 个节点,能容忍 2 个节点宕机。

Raft 集群的服务器都处于三个状态之一:

  • Leader :只有一个,响应所有客户端请求
  • Follower :其余都是,不发送只响应 Leader 或 Candidate 的请求。若客户向其请求,会重定向到 Leader。
  • Candidate :选举新 Leader 时使用(5.2)

服务器状态。Follower 只响应来自其他服务器的请求。如果 Follower 接收不到消息,那么他就会变成 Candidate 并发起一次选举。获得集群中大多数选票的 Candidate 将成为 Leader。在一个任期内,Leader 保持身份直到自己宕机。

Raft 把时间分割成任意长度的 任期(term) ,用 连续递增整数编号 ,任期开始即选举。Raft 保证一个任期只有一个 Leader。在某些情况下,一次选举无法选出 leader,这个时候这个任期会以没有 leader 而结束。同时一个新的任期(包含一次新的选举)会很快重新开始。

时间被划分成一个个的任期(term),每个任期开始都是一次选举。在选举成功后,领导人会管理整个集群直到任期结束。有时候选举会失败,那么这个任期就会没有领导人而结束。任期之间的切换可以在不同的时间不同的服务器上观察到。

任期编号在 Raft 算法中充当逻辑时钟,每个节点都储存当前任期号, 节点之间通信会交换任期号 ,当一个节点:

  • 当前任期号比其他节点小,更新自己的任期号
  • Leader 或 Candidate 发现自己任期号过期,立即转为 Follower(也就是放弃成为Leader的机会)
  • 收到过期的任期号请求,拒绝请求。

节点之间通信使用远程过程调用(RPCs) ,包含两种(第7节还增加了第三种传送快照的):

  • 请求投票(RequestVote) RPCs:Candidate 在选举期间发起(5.2)
  • 追加条目(AppendEntries)RPCs:Leader 发起,用于复制日志和心跳(5.3)

当节点没有及时的收到 RPC 的响应时,会进行重试,而且节点之间都是以并行的方式发送 RPC 请求,以此来获得更好的性能。

5.2 Leader 选举

  • 服务器启动时所有节点都是 Follower
    • Follower 一段时间没接收到消息即 选举超时 ,发起新选举。
    • Leader 周期性发送 心跳包(不含日志的 AE RPC) 给所有 Follower 来维持自己地位。
    • Follower 只要能收到 Leader 或 Candidate 的 RPC 就保持当前状态。
  • 开始选举 。Follower 自增 term(任期号)并转为 Candidate,并行向其他节点发送 RV RPC 等待给自己投票。
    • 等待时 收到 Leader 的心跳 ,且心跳中的任期不小于自己的当前任期,则自己变为 Follower。若小于自己的任期,则拒绝并保持 Candidate。
    • 如果同时出现多个 Candidate,选票可能被瓜分, 没有人得到多数选票 。则等待超时后重新选举。
    • Raft 使用 随机选举超时时间 (例如 150-300 毫秒)防止多次无人上任。每个节点 开始选举时重制超时时间 。可以让多数情况只有一个节点超时,进入下一轮赢得选举。
  • 获得多数选票的 Candidate 变为 Leader
    • 每个节点在一个任期内,按先来先服务(5.4节还有额外限制) 最多为一个 Candidate 投票
    • 成为 Leader 后向其他节点发送心跳建立权威。

5.3 日志复制

5.3.1 Leader 日志复制流程

  • 把客户端请求指令追加到日志,然后并行发 AE RPC 给其他节点让其追加日志。
  • 在日志被其他节点安全复制后(多数节点已复制),Leader 应用该指令到状态机并返回结果给客户端。
  • 如果中途出现问题,Leader 会不断重复 AE RPC(甚至已回复客户端后)直到所有 Follower 都追加了该日志。

5.3.2 日志提交

  • 一条日志包含当前任期号一条指令 ,也都有一个整数索引来表明它在日志中的位置。
  • Leader 决定什么时候能把日志安全应用到状态机,这样的日志条目为 已提交committed )。Raft 保证所有已提交日志都是持久化并最终被所有状态机执行。
  • Leader 把日志设为已提交后,还需要 通知 Follower 应用日志到状态机 ,这个通知通过下一次 AE RPC(也许是心跳)附加 commitIndex
  • 日志条目复制到大多数节点上时,就是 已提交 ,且 Leader 中当前条目 之前的日志也都已提交 ,包括其他 Leader 创建的条目(5.4)。Leader 记录最大已提交索引 leaderCommit,并放进所有 AE PRCs,其他节点由此得知 Leader 已提交位置,并按日志顺序应用到自己的状态机。

日志由序号标记的条目组成。每个条目都包含创建时的任期号和一个状态机需要执行的指令。一个条目当可以安全的被应用到状态机中去的时候,就认为是可以提交了。

5.3.3 日志一致性

这样 Raft 能维持 日志的一致性日志匹配特性):

  • 在不同的日志中的两个条目拥有 相同的索引和任期号 ,那么他们 存储了相同的指令
  • 在不同的日志中的两个条目拥有 相同的索引和任期号 ,那么他们 之前的所有日志条目也全部相同
  • 追加日志的一致性检查 :每次新条目 AE RPC 给 Follower,如果上一条索引任期不一致,则拒收新条目。所以 一旦 AE RPC 返回成功,说明 Follower 所有日志和 Leader 相同

5.3.4 日志不一致情况

正常情况下一致性检查不会失败,能一直保持一致。 但是 Leader 在未完全复制日志时宕机会使日志不一致 。例如 Follower 可能没有新 Leader 有的条目,也可能有新 Leader 没有的条目,或者都有,如下图。

当一个领导人成功当选时,跟随者可能是任何情况(a-f)。每一个盒子表示是一个日志条目;里面的数字表示任期号。跟随者可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。

例如,场景 f 可能会这样发生:f 对应的服务器在任期2的时候是 Leader,它追加了一些日志条目到自己的日志中,一条日志还没提交就宕机了,但是它很快就恢复重启了,然后再在任期3重新被选举为 Leader,又追加了一些日志条目到自己的日志中,在这些任期2和任期3的日志还没有被提交之前,该服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。

5.3.5 不一致的恢复

Raft 中处理这种不一致方法是, Leader 强制 Follower 复制自己的日志,即覆盖 Follower 中所有冲突日志 (安全性在5.4)。

Leader 找到最后和 Follower 一致的地方,删除 Follower 之后的冲突日志,发送自己的日志附加给 Follower。这些操作 在 AE RPCs 一致性检查时完成

  • Leader 对每个 Follower 维护下一个需发送的条目索引 nextIndex,在刚上任时初始化为最新日志索引+1。
  • Follower 日志不一致则拒绝 AE PRC ,Leader 减小 nextIndex 重试直到成功 ,Follower 删除冲突日志并追加 Leader 日志。日志即保持一致。
    这里可以优化,Follower 拒绝时返回冲突任期号的最早地址,Leader 下次就可以越过同任期号的冲突。但是此优化不一定有必要,因为实际很少发生。

所以 Leader 无需特殊操作就能恢复一致性 ,Leader 也从不会覆盖删除自己的日志(图3 Leader 只追加特性)。

日志复制机制展示了一致性特征:

  • 只要大部分的机器是工作的就能正常复制日志和应用保证可用性;
  • 一条指令大多数节点可一轮 RPC 完成,小部分慢节点不影响整体性能

5.4 安全性

目前为止所讨论的机制并不能充分地保证每一个状态机会按相同的顺序执行相同的指令。比如说,一个 follower 可能会进入不可用状态,在此期间,leader 可能提交了若干的日志条目, 然后这个 follower 可能被选举为新的 leader 并且用新的日志条目去覆盖这些日志条目 。这样就会造成不同的状态机执行不同的指令的情况。

故需 增加选举限制 ,保证图 3 中的领导人完整性,即 Leader 一定包含所有已提交日志条目

5.4.1 选举限制

某些一致性算法中需要额外复杂机制把缺少的日志传给 Leader。但是 Raft 保证 Leader 本来就有所有日志,所有日志都是单向从 Leader 传出去。

Raft 在等待投票时,RV PRC 包含 Candidate 的日志信息, 投票人会拒绝日志没有自己新的 Candidate 的投票请求

投票人 比较最后一条日志的索引值和任期号

  • 任期号不同,则任期号大的比较新
  • 任期号相同,索引值大的(日志较长的)比较新

5.4.2 提交之前任期内的日志条目

(本小节是一种错误情况)

前面介绍,一旦当前任期内的某个日志条目以及存储到过半的服务器节点上,Leader 就知道此日志在自己任期已提交。

Leader 可能在提交之前崩溃 ,新 Leader 不知道保存在多数节点的的条目是否提交。例如下图,存在多数节点的老日志仍可能被覆盖。

  • 在(a)中,S1是Leader,复制了索引位置2的日志条目给S2,这时还没过半。
  • 在(b)中,S1宕机了,然后S5在任期3中通过S3、S4和它自己的投票赢得了选举,然后从客户端接收了一条不一样的日志条目放在了索引位置2上面。
  • 在©中,S5宕机了,S1重启,此时S1和S2都可能成为leader,假如 S1贏得选举,然后 S1继续复制它之前在任期2中放在索引2上的日志条目。此时,来自任期2的那条日志已经被复制到了集群中过半的节点上了,但是它还没被提交。
  • 情况一,在(d)中,假如S1在提交日志之前宕机了,然后S5重启,这个时候S5最后的日志条目上的任期号比S2、S3和S4都大,所以它可以获得到S2、S3、S4和自己的投票成功当选leader。S5当选leader后,它就继续复制在任期3期间存储在索引位置2上的日志条目,那么该日志条目就会覆盖之前引复制在节点S1、S2、S3索引2处的日志中 。
  • 情况二, 在(e)中,如果在宕机之前,S1在自己任期内复制了日志条目到人多数机器上。那么S5就不可能贏得选举,这种情况下,之前的所有日志也被提交了。

所以 Raft 对日志提交条件增加一个额外限制Leader 在当前任期至少有一条日志被提交 (即超过半数节点复制),如图 8 中的(e)所示。而©中并没有提交4任期的日志。

所以新上任的 Leader 在接受客户写入命令前先提交一个 no-op(空命令),携带自己任期号的日志复制到多数节点,这样能保证选举限制成立。

5.4.3 安全性证明

img

假设:

假设任期 T 的 leaderT 在任期内提交了一个日志条目,但是该日志条目没有存在未来某些任期的 leader 中,假设 U 是大于 T 的没有存储该日志条目的最小任期号,处在任期 U 的 leader 称为 leaderU。

反证法论证:

  1. 因为 leader 从来不删除或重写自己的日志条目,所以如果一个已提交的日志要做到不存在未来的 leaderU 中的话,那么它只可能在 leaderU 选举的过程中被丢失。
  2. leaderT 将该日志复制给了集群中过半的节点,leaderU 从集群中过半的节点得到了投票。因此,至少有一个节点(这里称它为 voter)同时接收了来自 leaderT 的日志条目并且给 leaderU 投票了。
  3. voter 必然在给 leaderU 投票之前就已经接收了这个已经提交的日志条目了。否则,它就会拒绝来自 leaderT 的 AppendEntries RPC 请求,因为如果它在给 leaderU 投票之后再接收条目的话,那么它的当前任期号会比 T 大。
    译者注:因为要举行 Leader election 的话需要开一轮新的任期,这个时候前一轮任期已经结束了。我们这里假设了 T < U,上述所说的已提交日志条目是在任期 T 中的,如果 voter 先投票的话,那么就说明它已经进入了任期 U 了,而 U > T,voter 是不可能接受 leaderT 的 AppendEntries 请求的。
  4. 而且,voter 在给 leaderU 投票的时候,它依旧保有该日志条目,因为任何 U、T 之间的 leader 都包含该日志条目(因为我们前面假设了 U 是大于 T 的没有存储该日志条目的最小任期号),而且 leader 从来不会删除条目,并且 follower 只有再跟 leader 冲突的时候才会删除条目。
  5. 该投票者把自己的选票投给 leaderU 的时候,leaderU 的日志至少跟 voter 一样新(可以更新),这就导致了以下的两个矛盾之一了。
  6. 第一个矛盾:如果 voter 和 leaderU 最后一个日志条目的任期号相同的话,那么 leaderU 的日志至少和 voter 的一样长,所以 leaderU 的日志一定包含 voter 日志中的所有日志条目。 这是一个矛盾,因为 voter 包含了该已提交的日志条目,所以 leaderU 必定也包含该日志条目,而前面我们假设了 leaderU 是不包含的,这就产生了矛盾。
  7. 第二个矛盾:如果不是上面描述的情况的话,那么 leaderU 最后一个日志条目的任期号必然需要比 voter 的更大。此外,它还比 T 要大,因为 voter 拥有在任期号为 T 提交的日志条目,所以 voter 最后一个日志条目的任期号至少为 T。创建了 leaderU 的最后一个日志条目的之前的 leader 一定已经包含了该已被提交的日志条目(因为我们上面假设了 leaderU 是第一个没有该日志条目的 leader)。所以,根据日志匹配特性,leaderU 一定也包含了该已被提交的日志条目,这样也产生了矛盾
  8. 上述讨论就证明了假设是不成立的。因此,所有比 T 大的任期的 leader 一定包含了任期 T 中提交的所有日志条目。
  9. 日志匹配特性保证了未来的 leader 也会包含被间接提交的日志条目,如图中的索引 2。

通过 leader 的完整性特性,我们就可以证明状态机安全特性了,即如果某个节点已经将某个给定的索引处的日志条目应用到自己的状态机里了,那么其他的节点就不会在相同的索引处应用一个不同的日志条目。在一个节点应用一个日志条目到自己的状态机中时,它的日志和 leader 的日志从开始到该日志条目都是相同的,并且该日志条目必须被提交。现在考虑一个最小的任期号,在该任期中任意节点应用了一个给定的最小索引上面的日志条目,那么 Log 的完整性特性就会保证该任期之后的所有 leader 将存储相同的日志条目,因此在后面的任期中应用该索引上的日志条目的节点会应用相同的值。所以,状态机安全特性是可以得到保证的。

因为 Raft 要求服务器节点按照日志索引顺序应用日志条目,再加上状态机安全特性,这样就意味着我们可以保证所有的服务器都会按照相同的顺序应用相同的日志条目到自己的状态机中了。

5.5 Follower 和 Candidate 崩溃

前面都是讨论 Leader 崩溃,Follower和 Candidate 崩溃后的处理方式简单的多,Raft 只需要不断重试发送 RPCs 即可,崩溃重启后再执行 RPC。

Raft 的 RPCs 都是幂等的,重试不会产生问题。如果 Follower 发现 AE RPC 中的日志已经有了,它直接忽略这个请求。

5.6 时间和可用性

Raft 的要求之一就是 安全性不能依赖时间 :整个系统不能因为某些事件运行的比预期快一点或者慢一点就产生了错误的结果。

但可用性不可避免要依赖时间,最关键在于 Leader 选举,需要满足如下时间要求:

broadcastTime<<electionTimeout<<MTB

  • 广播时间(broadcastTime):一个节点并行发送 RPCs 给其他节点并接收响应的平均时间
    • 应比选举超时时间小一个数量级才能保证稳定的心跳。
    • 广播时间大约是 0.5 毫秒到 20 毫秒,取决于存储的技术
  • 选举超时时间(electionTimeout):选举超时时间限制。随机化使之难以瓜分选票。
    • 只有选举超时时间是我们自己选择的 。可能需要在 10 毫秒到 500 毫秒之间。
    • 应比平均故障时间小几个数量级。Leader 崩溃后系统将在一个选举超时时间中不可用,此情况应很少出现。
  • 平均故障间隔时间(MTBF):一个节点两次故障之间的平均时间。
    • 大多数服务器平均故障时间在几个月甚至更长,很容易满足时间的需求。

6. 集群成员变化

到目前为止,我们都假设集群的配置(参与共识算法的服务器节点集合)是固定不变的。但是在实际情况中,我们有时候是需要去改变集群配置的,比如说在服务器崩溃的时候去更换服务器或者是更改副本的数量。尽管可以通过下线整个集群,更新所有配置,然后重启整个集群的方式来实现这个需求,但是这会导致集群在更改过程中是不可用的。另外,如果这个过程中存在一些操作需要人工干预,那么就会有操作失误的风险。为了避免这些问题,我们决定将配置变更自动化并将其纳入到 Raft 的共识算法中来。

6.1 两阶段提交:Joint Consensus

为了让配置修改机制安全,在转换的过程中同一个任期里 不能够存在两个 Leader 同时当选 。问题在于, 一次性自动的转换所有服务器是不可能的 ,任何切换方法都是不安全的,所以在转换期间 整个集群可能分裂成两个独立的多数

直接从一种配置转到新的配置是十分不安全的,因为各个机器可能在不同时候进行转换。在中间位置 Server1 可以通过自身和 Server2 的选票成为 leader(满足旧配置下收到大多数选票的原则);Server3 可以通过自身和 Server4、Server5 的选票成为 leader(满足新配置线,即集群有 5 个节点的情况下的收到大多数选票的原则);此时整个集群可能在同一任期中出现了两个 leader,这和 Raft 协议是违背的。

为了保证安全性,配置更改必须使用 两阶段方法 。有些系统在第一阶段停掉旧的配置,集群就不能处理客户端请求;然后在第二阶段在启用新的配置。

在 Raft 中,集群先切换到一个过渡性配置,我们称之为 Joint Consensus联合共识 );一旦联合共识被提交,那么系统就切换到新的配置上。

Joint Consensus 是老配置和新配置的结合:

  • 日志条目被复制给集群中新、老配置的所有服务器。
  • 新、旧配置的服务器都可以成为领导人。
  • 达成一致(针对选举和提交)需要 分别在两种配置上获得大多数的支持

Joint Consensus 允许独立的服务器在不影响安全性的前提下,在不同的时间进行配置转换,还可以让集群在配置转换的过程中依然响应客户端的请求。

6.2 实现细节

集群配置在复制日志中以特殊的日志条目来存储和通信。下图展示了配置转换的过程:

  • 当一个 Leader 接收到一个改变配置从 C-old 到 C-new 的请求,他将创建联合共识的配置(图中的 C-old,new)并存储为一个新日志条目。
  • Leader 将 C-old,new 发给所有 Follower 进行复制。
    • 如果 C-old,new 被半数以上节点同步,则此配置已提交,之后遵循 Raft 安全性机制, 只有拥有 C-old,new 日志条目的服务器才有可能被选为新 Leader
    • 如果半数同步前 Leader 崩溃,新 Leader 可能有 C-old,new 也可能没有,若没有则退回老配置重试更新即可
    • 在这一时期,C-new 不会有任何影响。
  • C-old,new 已提交后,C-old 已不会产生影响,Leader 再创建和提交 C-new 就是安全的了。

在整个过程中 没有哪个时候让 C-old 和 C-new 同时产生影响 ,保证了安全性。

img

6.3 问题讨论

  • 没有存储任何的日志条目新节点加入,复制日志条目需要时间,此时无法作为提交和选举决策的节点。
    • 新节点设置保护期,此期间以没有投票权身份加入到集群中来,不参加选举投票和日志提交决策,直到日志同步完毕。
  • Leader 不是新配置成员。
    • Leader 在 提交了 C-new 日志之后主动退位 (回到 Follower 状态)。并且在 复制提交 C-new 时自己不算半数之一
  • 被移除的服务器未关闭,可能会扰乱集群。因为它们不再收到心跳,就会一直超时发起带有新任期号的选举。
    • 集群中节点在 未达到选举超时时间前,不响应 RV RPC 。即如果当前 Leader 能够在超时时间内发送心跳,Follwer 就能确认当前 Leader 存在而不响应新的投票请求。

7. 日志压缩

7.1 快照基本思路

日志不能无限增长, Snapshotting快照 )是最简单的压缩方法。在快照系统中,整个系统的状态都以快照的形式写入到稳定的持久化存储中,那个时间点之前的日志全部丢弃。

增量压缩 ,例如日志清理或者日志结构合并树也可行,这些方法每次只对一小部分数据进行操作,分散了负载压力。首先,选择一个已经积累的大量已经被删除或者被覆盖对象的区域,然后重写那个区域还活跃的对象,之后释放那个区域。

增量压缩需要增加复杂的机制来实现,而快照总是简单操作整个数据集合,简化了这个问题。日志清除方法需要修改 Raft,但是 状态机可以使用和快照相同的接口实现 LSM tree(日志结构合并树)

上图展示了 Raft 中快照的基本思路:

  • 每个服务器独立的创建快照 ,只包括已经被提交的日志。大部分由状态机将当前状态写入到快照中,也包括少量元数据:
  • lastIncludedIndex:被快照取代的最后的条目在日志中的索引值(状态机最后应用的日志)
  • lastIncludedTerm:该条目的任期号

保留这些数据是为了支持快照后第一个 AE RPC 时的一致性检查,因为这个条目需要前一日志条目的索引值和任期号。

  • 为了支持集群成员更新(第 6 节),快照中也将 最后的集群配置作为最后一个状态条目存下来 。一旦服务器完成一次快照,他就可以删除最后索引位置之前的所有日志和快照了。

7.2 InstallSnapshot RPC

Leader 必须偶尔 通过 RPC 发送快照给一些落后的 Follower 。一般发生于当 Leader 已经删除下一条需要发送给某 Follower 的日志条目的时候。例如一个运行非常缓慢的 Follower 或者新加入集群的服务器(第 6 节),这时让这个 Follower 更新到最新的状态的方式就是通过网络把快照发送给他们。

当 Follower 接收到 IS RPC 时,自己决定对于已经存在的日志该如何处理。

  • 通常快照会 包含没有在接收者日志中存在的信息 。此时跟随者 丢弃其整个日志,全部被快照取代 ,即使包含与快照冲突的未提交条目。
  • 如果接收到的 快照是自己日志的前面部分 (由于网络重传或者错误),那么被快照包含的条目将会被全部删除,但是 快照后面的条目仍然有效,必须保留

pSu1L80.png

由 Leader 调用,将快照的分块发送给 Follower。Leader 总是按顺序发送分块。

参数解释
term领导人的任期号
leaderId领导人的 Id,以便于跟随者重定向请求
lastIncludedIndex快照中包含的最后日志条目的索引值
lastIncludedTerm快照中包含的最后日志条目的任期号
offset分块在快照中的字节偏移量
data[]原始数据
done如果这是最后一个分块则为 true
返回结果解释
term当前任期号(currentTerm),便于领导人更新自己

接收者实现

  1. 如果 term < currentTerm 就立即回复
  2. 如果是第一个分块(offset = 0)就创建一个新的快照
  3. 在指定偏移量写入数据
  4. 如果 done = false,则继续等待更多的数据
  5. 保存快照文件,丢弃具有较小索引的任何现有或部分快照
  6. 如果现存的日志条目与快照中最后包含的日志条目具有相同的索引值和任期号,则保留其后的日志条目并进行回复
  7. 丢弃整个日志
  8. 使用快照重置状态机(并加载快照的集群配置)

7.3 问题讨论

这种快照的方式背离了 Raft 的强 Leader 原则,因为 Follower 可以在 Leader 不知情情况下创建快照,但是这是值得的。Leader 的存在,是为了解决在达成一致性的时候的冲突,创建快照的时候一致性已经达成,不存在冲突了,所以没有 Leader 也是可以的。数据依然是从 Leader 传给 Follower,只是Follower 可以重新组织他们的数据。

而只有 Leader 创建快照,发送给所有的 Follower 的方案有三个问题:

  • 浪费网络带宽并且延缓了快照处理的时间,Follower 已有快照所需信息显然更经济。
  • Leader 的实现会更加复杂。例如需要发送快照的同时并行的将新的日志条目发送给跟随者,这样才不会阻塞新的客户端请求。

还有两个问题影响快照性能:

  • 什么时候应该创建快照?过于频繁会浪费大量的磁盘带宽和其他资源;频率太低要承受耗尽存储容量的风险,也增加了从日志重建的时间。
    • 日志大小达到一个固定大小的时候就创建一次快照 。如果这个阈值设置的显著大于期望的快照的大小,那么快照对磁盘压力的影响就会很小了。
  • 写入快照需要花费显著的一段时间,并且我们还不希望影响到正常操作,如何处理?
    • 写时复制的技术 ,这样新的更新就可以被接收而不影响到快照。具有函数式数据结构的状态机天然支持这样的功能。另外,操作系统的写时复制技术的支持(如 Linux 上的 fork)可以被用来创建完整的状态机的内存快照(我们的实现就是这样的)。

8. 客户端交互

这一节将介绍客户端是如何和 Raft 进行交互的,包括:

  • 客户端如何发现 Leader
  • Raft 如何支持线性化语义

这些问题对于所有基于一致性的系统都存在,并且 Raft 的解决方案和其他的也差不多。

  • 客户端发送所有请求都要给 Leader
    • 第一次通信会 随机联系一个节点 ,如果不是 Leader ,会被拒绝并提供最近接收的 Leader 信息(AE RPC 包含 Leader 地址),即 重定向
      如果 Leader 宕机,请求超时,客户重试即可。
  • Raft 的目标是要实现线性化语义 (每次操作立即执行,在调用和收到回复之间只执行一次)
    • 若 Leader 提交了客户端的操作日志,在回复客户端之前宕机,客户端重试。此时该指令可能执行两次。
      解决方案是 客户端对每条指令赋予唯一序列号,状态机接受的序列号被执行的指令直接返回结果
  • 只读操作可以不需要记录日志,但是旧 Leader 响应客户端时可能已经卸任,此时返回的是脏数据。需要两个额外机制 保证不返回脏数据
  1. Leader 必须有关于被提交日志的最新信息,刚上任时可能不知道哪些已提交,所以需要提交一个 no-op(空命令) 日志条目。
  2. Leader 在响应选举请求前,检查自己是否已被卸任。只需要和集群中大多数节点交换一次心跳信息即可。

可选项: Leader 可以通过心跳机制实现租约机制 ,但是这种方法依赖时间来保证安全性(假设时间误差是有界的)。

9. 算法实现与评估

10. 相关工作

11. 结论

算法的设计通常以正确性、效率和简洁性为主要目标。虽然这些都是有价值的目标,但我们相信可理解性同样重要。在开发人员将算法转化为实际实现之前,其他任何目标都不能实现,而实际实现将不可避免地偏离和扩展发布的形式。除非开发人员对算法有深刻的理解,并能对算法有直观的认识,否则他们很难在实现中保留算法理想的特性。

在本文中,我们讨论了分布式共识的问题,在这个问题上,一个被广泛接受但难以理解的算法:Paxos,多年来一直让学生和开发人员非常挣扎。我们开发了一种新的算法:Raft,我们已经证明它比 Paxos 更容易理解。我们也相信 Raft 会为系统建设提供更好的基础。将可理解性作为主要设计目标改变了我们处理 Raft 设计的方式。随着设计的进展,我们发现自己反复使用了一些技术,比如分解问题和简化状态空间。这些技术不仅提高了 Raft 的可理解性,而且使我们更容易证实它的正确性。

LEC 5

模式

前面的系统都有单点故障:例如Coordinator、Master等等。因为要避免脑裂问题,因此并不设计成分布式的。

这种在一般情况下是没有问题的,出错的概率很小,即使出错了也可以在很短的时间内恢复回来。

Raft协议就是处理这种类型的问题,不允许单点故障产生,即使产生了也会更快恢复。

客户端访问两台服务器,一台得到了响应,另一台没有得到响应,如果另一台服务器挂掉了最好,但是如果仅仅是网络不通,会造成网络分区的问题,也就是脑裂,导致服务器不一致。因此前面的方案中都使用单点服务器的方式。

网络分区问题

处理原则:少数服从多数

客户端的操作需要在大多数服务器都成功,否则一直等待恢复,这样可以实现强一致性

大多数:全部服务器,无论是开机的还是停机的,需要获得一半以上的服务器同意

两种前协议:Paxos和View-stamped replication

Raft

构建复制状态机

pSUHKcd.md.png

步骤:

  1. 客户端发送操作给Leader的K/V服务器
  2. K/V服务器将操作传递给Raft
  3. Raft写入日志
  4. Raft与其他服务器通信传送日志
  5. 其他服务器发送响应给Leader
  6. Leader提交操作(其他的Followers需要等到下一次交互才确认前面的操作并提交)
  7. 操作按照顺序传送到K/V服务器
  8. K/V服务器执行操作
  9. Leader返回操作结果给客户端

如果失败,需要选举新的Leader,重试操作

日志

为什么需要日志?

K/V服务器是保留操作表的,为什么还需要日志呢?

  • 重传:Leader发送的时候可能会丢失,因此Leader必须保留所有的日志条目从而具有重传的能力
  • 顺序:每一个操作需要按照顺序传送,日志可以非常方便做到
  • 持久化:服务器都有可能挂掉,因此需要持久化保留所有的操作
  • 空间:需要空间进行一些试探性的操作,日志可以很方便做到

最终需要保证日志在所有的服务器上都是相同的

日志基本结构

日志条目包括序号、操作和Leader的任期(隐含表示了这个日志条目是哪个Leader追加的)

选举Leader

Follower如果接收不到Leader发送的周期性的心跳信号,就认为Leader挂掉了,开始选举Leader

具体实施:Follower自己有计时器,如果在一段的时间之内既没有接收到新的日志条目,也没有接收到Leader的心跳信号,则认为选举超时,开始进行选举。

  1. 增加任期号,并给自己投票
  2. 联系其他的服务器(包括Follower和Leader)
  3. 收到大多数的选票,成为Leader

此时新的Leader的任期号要大于原来的Leader的任期号,如果此时客户端与旧的Leader进行交互,Leader给新的Leader发送了增加日志的请求,会被拒绝,发送给旧的Leader自己的任期号。旧的Leader发现任期号比自己大,不会再成为Leader。从而避免了脑裂的问题。

挑战:两个Follower几乎同时发起选举,选不出Leader(分裂选举)

因此设置选举超时时间,但是是随机的,如果选不出Leader,经过一段时间后就不会同时开始选举Leader,就可以最终选出Leader了。

选举超时时间

  • 不能小于心跳信号的间隔时间
  • 三到四次RPC的时间
  • 随机值越大停机的时间越长,越小可能仍然会选举失败
  • 250-300ms
]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + 2022年终总结 + + /2022/12/31/diary/diary20221231/ + + 2022年终总结

到了一年的末尾,伴着客厅的电视声音和窗外若有若无的鞭炮声,还是要写一点总结。

我本想用“高开低走”来对这一年做一个精炼的总结,虽然说目前确实是“低”的状态,可是年初似乎也并没有什么“高”的事情,故这个词语还是不怎么恰当。

回想去年的这个时候,应该是在科一招和两位同学一起跑赛车吧,当时虽然屋子里面很冷,心是火热的,幻想着这样的生活可以一直持续下去。今天屋子里面还是很冷,不同的是心也很冷,目前过的不怎么样,也看不到什么未来。

再回想几年前,已经想不起来什么印象深刻的事情了,可能大多数都是在准备考试吧hh。

现在我自己的状态,或许和2018年初是相同的,又或许是2019年,又或许不同,只是我自己的内心深处偏要找一个相同的历史时刻才能让我自己获得某种慰藉。

我不知道应该写些什么关于今年的事情,写一写可能又写到了感情生活上,而这是我现在最不愿触及的部分之一。

突然想起了五年前看到过的一篇文章,翻出来,最后就用它做一个总结吧:

小时候,过年是头等大事。我们家的人不多,但是和父母一起,准备小零食,准备年夜饭,包饺子,看春晚。年少时的我总觉得,日子一天天过去,没有开端也没有终结。

那时我总以为,过完了今天,明天还是一样的会来,过完了今年,还有明天这个时候的“今年”。可曾经那个心心念念的过年,曾经的那个“今年”,都像天上的云彩和海上的浪花一样,早已不知所踪。

人不能两次踏进同一条河流,也不能,重新过一遍2022。

季节流转,日升月落,星移斗转,世事如白衣苍狗。这一年有多少遗憾和侥幸,有多少悲恼和欣欢,多少披星染雾的启程和多少戴月荷锄的归途。新的一年终将随着初生的太阳喷薄而出,我们如同站在两个世界的边缘,愧疚地送别过去,紧张地等候未来。

我不愿意用一句“新年新气象”,就将过去一年的得失通通扫净,尽管它们终将消失在记忆的犄角旮旯。

新的一年,不是一切归零的重新开局,也不是一成不变的延续。

回头再看看2022,我们有伤感的时候,有无奈的时候,有纠结的时候,也有骄傲的时候。总结过去,才能展望未来。

2023,不是新的开始,而是新的征程。

]]>
+ + + + + Life + + + + + + + Diary + + + +
+ + + + + 研一上学期闭卷三科目考试重点 + + /2022/12/27/UCAS/exam-final-summary/ + + 研一上学期闭卷三科目考试重点

模式识别与机器学习

25道选择题,5道大题(简答题与计算题结合在一起)

2022.12.27 二班苏荔老师

非监督学习:不考死记硬背的概念,看一下作业题的例子,比如给一些具体样本分布的图,如果使用KMeans聚类会怎么样,Kmeans的判别界面应该是一个中垂线,两个簇的中垂线,如果不是大小均匀的球体可能KMeans就不适合,如果是有疏有密的,一个椭球形的,是不是考虑基于混合高斯的模型,或者簇是一个不规则的形状,比如是字母S或者C型,甚至有的簇是一个同心圆的,最适合的聚类算法应该是什么样的,可以考虑一下基于密度的聚类是不是可以,基于密度的聚类有没有不适合的情况,包括基于层次聚类会遇到什么问题,结合一些实际的例子请大家分析一下。

降维:PCA每年都考,可能和前面讲K-L变换的知识结合起来,比如PCA为什么要先做零均值的平移,特征值的选择,为什么先做倒序的排序,选排在前面的特征值最大的几个特征值对应的特征向量来构成对应的变换核,基本原理希望掌握。后面的非线性降维,核PCA的主要思想是什么,先升维再降维,基于流形的非线性降维方法,说出一些经典方法的名字就行,不可能考太细。(前面讲了一些降维,这里不可能考大题,可以看看Fisher线性判别怎么做降维,选择最佳投影方向等等)

半监督学习:半监督学习是基于一些假设的,最基本的假设有哪些,不同的半监督算法各自基于什么假设?比如半监督的SVM应该是基于低密度分割的假设,半监督SVM里面有一些比较关键的参数,C1,C2等等,分别表达什么意思,C2很小表达对未标注样本错分的容忍度比较大,很大表示不容忍错分,每一个未标注样本也要分对,但是所有的样本都必须要严格分对这种也可能有问题,过拟合等等,过拟合怎么办呢?可以把模型参数调简单点,怎么让模型参数调简单?不同的模型方法不一样,比如半监督SVM就可以把C2调小一点,或者把C1调小也行,如果是神经网络可以让结构简单一点,层数和节点变少参数就变少,欠拟合怎么办,如何让模型变复杂,除了从模型参数上调,增加数据样本是不是一定有用?

概率图模型每年都会有大题,要么是考贝叶斯球判断条件独立性,或者大题问维特比解码,一般只考前向的推导。

集成学习只考经典的bagging和boosting两种,其他的不考,考的时候从基本原理出发,比如bagging的基本原理是降低方差,但是不能改变偏差,boosting主要是降低偏差,考的时候可能会给实际的例子去判断哪种更适用,比如模型的单个基学习器都是偏差比较高的情况,要把多个集成起来增加学习能力,到底是用boosting还是bagging,如果选择boosting,不同的基学习器的权重是完全一样的吗,谁的权重大,或者是boosting作迭代训练的时候,训练样本每一次的权重在迭代的时候是不是都要改变,为什么要让分错的样本权重更大,分对的样本下一次权重就调小,要知道这些基本的调节权重的原则。

人工神经网络只考基本原理和卷积神经网络,可能让你设计一个人工神经网络的结构,和前面几章的内容有结合,比如线性分类器,非线性分类器,两类分类的,多类分类的,都怎么来设计,能画出大概的结构,写出能用什么(激励?)函数就可以。卷积神经网络主要是考察基本原理,比如为什么卷积神经网络能够进一步降低(深度,参数量?)但是又不会降低性能。可能是基于局部连接和参数重用,池化等等技术,这几个技术的基本的动机是什么,是因为有一些很有判别力的局部模式,他是之前局部出现,并且能够在不同的位置出现,还有有的时候下采样也不会影响这种局部模式的识别。可能会问一些深度神经网络训练的时候遇到一些问题怎么解决,比如说层数很深的时候,会遇到梯度消失、梯度爆炸的问题,遇到问题怎么办呢,激活函数从sigmoid换成relu为什么这样能解决这个问题,比如说使用batch normalization也可以缓解梯度消失的问题,甚至还能带来一些正则化的效果,或者是残差网络的技术,将前层的信息引入进来,可能还带有一些集成学习的思想,把这些基本的原理说清楚。人工神经网络的训练可能也会遇到过拟合,模型的参数可能就过于复杂,除了简化模型的结构之外,还有什么其他的技术,比如是不是可以考虑添加正则化项,正则化也分为L1和L2,L1正则能让系数=0也能做特征选择,L2可以让参数趋向于变小,对整体的影响就变小了,相当于参数变简单了,也能防止过拟合,包括做数据增强,增加训练样本集尽可能让他多样化,也是可以增加模型的泛化能力,还有做梯度下降的时候收敛速度变慢怎么办,陷入局部极值点怎么办,一般是这种实际一些的问题

2022.12.28 三班卿来云老师

不考:

势函数、决策树、贝叶斯参数估计、后向算法、马尔科夫随机场、SMO、拉普拉斯平滑

没有证明题,但是会有一点公式推导

EM算法不作为重点考察,最多知道概念是拿来干什么的就行,是对含有隐含变量的模型做优化,不会考很细节的

零碎考点:

判别器有讲过贝叶斯风险,是每年都会考的,感知器算法、朴素贝叶斯、Fisher判别分析,都有可能考到的,K-L变换作特征提取或者后面的PCA,还有像LR,SVM,还有线性回归,另外就是机器学习一般的概念,过拟合、欠拟合应该怎么办,怎么判断模型处于哪种状态,正则的概念,可能放到多种模型中,都要能理解。

降维需要掌握的知识点:PCA是怎么做的,Fisher判别分析要会

Fisher判别分析是要考的但是不考计算,除了两类也要看一下多类的

深度学习有一道大题,偏简答性质的

HMM有计算题,判断独立性是选择题

概率图掌握贝叶斯球就可以,概率的分布表示还是要的

多类LR不需要特别掌握,知道有这回事就行,比如用softmax函数做最后一层就可以

多类分类问题在SVM或者LR可以转化为两两的分类问题,算术题,转成多少个需要知道

支持向量考的挺多的,给一个图,哪些点是支持向量,或者自己画图

偏差方差分解具体的证明不考,但是要知道泛化误差是三部分,会判断模型什么时候偏差比较大,什么时候方差比较大,应该怎么办

高斯判别分析今年没有大题,Fisher判别分析多看看

SVM软间隔硬间隔都会有

一般对线性回归、LR、SVM公式推导方面严一点,比如损失函数是怎么推导来的,极大似然估计是怎么回事,MAP估计是怎么样,这些基本概念需要掌握,SVM的模型推导可能少一些。SVM更多理解margin、支持向量、不同的C的影响等等、核函数,RBF核模型复杂度的参数是核函数的宽度,多项式核模型复杂度还有多项式的项数。

类内类间散度矩阵应该怎么算是要求掌握的,与Fisher是什么关系,但是不会考察具体的数值计算

多类感知器不考,感知器是有的

PCA没有计算,需要知道PCA的计算过程,另外目的和达到的效果也需要知道。首先要减均值,然后再算协方差矩阵作矩阵分解,或者是作SVD分解都可以,测试的过程也一样,目的是去掉各个维度之间的相关性

bagging知道功效和怎么做就行,是多个模型做平均,目的是偏差不变,降低模型的方差,boosting是降低模型的偏差,方差不变

聚类:比如像K均值,GMM,DBSCAN这三类聚类方法需要掌握他的特点,我们什么时候用哪种方法来做,选择题

降维:今年考的不是很多,稍微看一看就行

半监督学习:不会考的特别细,但是半监督学习是基于一些假设的,最基本的假设有哪些,不同的半监督算法各自基于什么假设?

概率图模型一方面考对于有向图模型,判断给定观测到某些节点的情况下,这个变量和那个变量是否独立,可以通过贝叶斯球的方式做,需要记忆贝叶斯球的规则,根据一个图模型把联合分布写出来,HMM要求掌握前向算法求观测概率和利用维特比算法求给定一个观测后最可能的隐含状态的序列

集成学习主要是bagging和boosting,要知道bagging的原理是通过对训练样本进行bootstrap采样,然后训练多个模型,最后对多个模型作平均,得到最后的融合模型。它的好处是可以降低模型的方差,偏差不变。boosting对于Adaboost的过程,比如一开始初始化的时候每个样本的权重相同,经过一轮迭代之后哪些样本的权重会增加,哪些样本的权重会减小,最后模型是怎么融合的,没有计算题,知道Adabooost的过程就可以。对于boosting一般的原理也要知道,每次迭代顺序的把一些模型加进去,最后一些子模型的加权平均是我们最后的集成模型,boosting的好处是降低模型的偏差,方差不变。

深度学习:知道神经元的结构(线性组合+激活函数),一个神经元可以起到一个什么样的作用,神经网络是把神经元组合起来,卷积神经网络为什么要局部连接,为什么要权重共享,这样做的基于对数据的假设是什么,局部连接我们认为是一个模式在一个比较小的范围内,而不是要看全局,权值共享是说不同位置可能都有这样的模式,这样做可以使得模型的参数变少,另外多层感知器很多层并不能带来性能提升,现在的模型采用哪些技术使得训练深度模型成为可能?比如说激活函数,sigmoid容易梯度消失,使用relu使得梯度消失的问题会减弱,这样网络层数可以更深,另外batch normalization会使得我们的训练会和好多因素(学习率、初始化)的要求没有那么高,这样也是一种技术,另外采用预训练网络作初始化也是一种方式,或者核初始化等等,也可以让模型的层数更深。另外Resnet,或者叫skip-connect,通过跳接的方式使得梯度消失的问题能减弱,使得模型可以很深很深甚至上千层。神经网络设计的时候也讲了一些其他的技术。不要求全部掌握,至少知道几点。为什么要用mini-batch的梯度下降,随机的梯度下降,有什么样的好处或者特点等等。

2022.12.31

第一章:模式识别和机器学习我们并不是很区分它们,可以看成一个问题的两个方面

第二章:统计判别,主要是讲了错误率最小,错误率最小对应到分类问题等价于后验概率最大,后验概率怎么算需要大家一定掌握,后面也把风险带进来

第三章:判别函数,作判别的时候一种方式可以使用生成式分类器,高斯分布的贝叶斯分类器采用的实际上是生成式分类器,指的是我们的联合分布可以由先验和似然相乘得到,有了联合分布可以从联合分布进行采样从而得到新的数据,也就是我们知道数据的产生过程,因此叫做生成式分类器。朴素贝叶斯,高斯判别分析,概率图模型,HMM都属于生成式分类器。好处是因为我们对数据的产生过程有比较强的假设,如果我们的数据符合这个假设,通常用很少量的数据就能得到很好的模型,或者说收敛的比较快,缺点是如果数据不符合这个假设,模型的性能就很不好。另外是判别式分类器,就是直接对后验概率进行建模,或者是定义一个判别函数,根据判别函数的值判断是哪一个类别。像逻辑斯蒂回归,决策树,SVM,深度学习都是判别式的。

线性判别函数如何将多类分类任务转化成两类分类任务或者是直接用多类分类任务去做。线性判别函数处理的都是线性可分的问题,如果不是线性可分的需要用广义的线性判别函数,或者是分段线性判别函数实现。模式空间和权空间只是一些基本概念。重点掌握Fisher线性判别,协方差矩阵、类内类间,向量等都是怎么定义的,Fisher线性判别的准则是什么,都需要掌握,两类的情况,多类的情况,大家都去想想,感知器算法需要重点掌握,它是线性判别函数,只能在线性可分的时候可以使用,感知器多类也需要大家掌握。

第四章:特征选择是很重要的内容,但是不作为重点考察。特征提取重点掌握K-L变换,比较简单,实际也比较有用,PCA实际上和K-L变换就是一回事。需要知道K-L变换的目的和过程,做了K-L变换之后能达到一个什么样的效果。

第五章:期望风险:在没有见过的数据上的误差,也就是期望风险。结构风险:正则项,L1正则和L2正则,泛化误差的分解,证明过程不要求,结论要知道,用VC维计算泛化误差的上界,基本概念比较多

第六章:线性回归的损失函数,目标函数包括两部分,L1损失或者L2损失,负log似然损失,需要掌握似然是怎么定义的,由负log似然损失怎么推到了L2损失,L2损失的问题是对噪声比较敏感,因为是平方的关系,预测差距比较大的时候损失也比较大,可以采用L1损失,但是是不连续的,因此一般采用Huber损失,实际上是L1损失和L2损失的分段的情况。正则可以是L1正则或者L2正则,目的是为了限制模型的复杂度,L2会使得w的绝对值会缩小,也叫权重缩减,但是一般不为0。L1会为0,可以起到特征选择的作用。然后讲了逻辑斯蒂回归,是一个分类问题,直接计算后验概率,多类分类需要softmax函数,损失也是负log似然,只是称为交叉熵损失,推导的时候都是从负log似然推导过来的。然后生成式分类器,高斯判别分析和朴素贝叶斯,两种分类器的先验是相同的,两类是伯努利分布,多类是多项式分布,两者不同的地方是类条件的定义不一样,高斯是在给定类别的条件下是高斯分布,朴素贝叶斯是独立的,

第七章:SVM,考察的重点,需要掌握线性的SVM,核化的SVM,回归了解一下就行,首先要知道间隔的概念,硬间隔和软间隔的目标函数分别是什么,可以写成损失+正则的情况,合页损失+正则,什么样的样本是支持向量,了解原问题和对偶问题,核化也是很重要的,后面的PCA,包括逻辑斯蒂回归都可以核化,但是模型比较复杂,大家用的比较少,线性核,多项式核的复杂度体现在基数,RBF核的复杂度是核函数的宽度,软间隔的复杂度还有松弛因子C,概念需要掌握

第八章:聚类:重点掌握K均值,GMM,DBSCAN,知道聚类的过程,对什么样的数据比较适合,K均值每一个类的都是高斯,方差相同等等,GMM更宽泛,不球形,DBSCAN不要求每一类的具体形式,找密度最大的作为中心,还可以处理带有噪声的情况。

第九章降维:PCA就是K-L变换,目标是重构误差最小,MDS不作重点考察,非线性降维知道一些常用的非线性降维方法的做法就可以了

第十章半监督学习:重点掌握三种基本假设,半监督学习算法了解就行,想一下每一种算法的背后是基于哪些假设在做

第十一章概率图模型,考试时候主要是考察有向的图模型,无向图模型了解一下就行,给定一个概率图模型,能不能写出概率分布来,根据有向图模型,判断给定观测到某些节点的情况下,这个变量和那个变量是否独立,可以通过贝叶斯球的方式做,需要记忆贝叶斯球的规则,缺失的边实际上也蕴含了独立的关系,HMM要求掌握前向算法求观测概率和利用维特比算法求给定一个观测后最可能的隐含状态的序列。HMM的模型参数学习,全监督的用极大似然估计,隐含状态不知道用EM算法。

第十二章集成学习主要是bagging和boosting,要知道bagging的原理是通过对训练样本进行bootstrap采样,然后训练多个模型,最后对多个模型作平均或者投票,得到最后的融合模型。可以并行训练,它的好处是可以降低模型的方差,偏差不变。基学习器的选择可以选方差比较大的模型。比如随机森林里面选取的决策树就是选层数比较深或者叶子节点比较多的。或者多个神经网络进行。boosting是多个基学习器采用顺序的方式进行训练,不能并行训练,每一个新加入的学习器希望能纠正前面的学习器的一些错误,实现的时候每个基学习器都是一样的,融合的方式是加权平均,权重和每个基学习器是相关的。boosting的好处是降低模型的偏差,方差不变。因此基学习器可以比较简单,决策树就树比较浅。重点考察Adaboost的过程,需要掌握。

第十三章深度学习:知道神经元的结构(多个输入线性组合+激活函数得到输出),与人类神经元比较像,线性回归实际上也可以用一个神经元来表示,相当于激活函数是一个线性的激活函数(恒等映射函数)。逻辑斯蒂回归就相当于一个神经元+sigmoid函数,SVM也可以写成这样的形式。多层叠加就可以实现非常复杂的非线性函数的拟合。80年代层数不深,当时的训练方法有一些问题,sigmoid或者tanh会产生梯度消失的问题,relu可以解决梯度消失,计算简单。梯度消失是梯度传几层就没了,一个原因是激活函数,另外的原因是乘很多次,也会没了。神经网络之间的连接方式:全连接,每一个神经元都和其他的神经元相连。为了简化,图像可以采用卷积神经网络,通常只是局部连接,为什么要局部连接,因为图像里面一些局部的输入就可以判断模式的存在。为什么要权重共享,权值共享是说不同位置可能都有这样的模式,这样做可以使得模型的参数变少,计算量并不会减少。数据不满足这种特点不能使用卷积做。另外多层感知器很多层并不能带来性能提升,现在的模型采用哪些技术使得训练深度模型成为可能?Resnet,或者叫skip-connect,通过跳接的方式使得梯度消失的问题能减弱,使得模型可以很深很深甚至上千层。另外batch normalization相当于做一个标准化,使得输入的范围的差异不至于这么大。相当于在神经网络里面增加了一个层,学习这个层的参数。会使得模型训练更快更稳定,性能更好。dropout使得某些输出是0,不往下传,相当于集成学习的高效的实现方式,主要使用在全连接层里面。另外讲了一些神经网络的训练技巧,基本算法是梯度下降,梯度爆炸就是乘多了很大很大,梯度的计算首先是利用网络结构进行反向传播,批处理梯度下降是所有的样本一起,算一次很慢,一般不采用,随机梯度下降是每一个只选取一个样本,比较快,但是受单个样本的影响比较大。梯度的方向可能是锯齿的形状。通常使用小批量梯度下降,两者取中间。走一步的步长叫做学习率,下降的方法不一定是梯度,因为鞍点可能不动,平滑的区域也不怎么动,因此考虑动量法,进行自适应调整。参数初始化也比较重要。另外关于抗过拟合的问题,每一层的节点很多,模型就非常复杂,需要抗过拟合。及早停止,监控验证集上的误差,正则,数据增广,收集更多的训练样本,但是收集样本需要费用,可以采用一些图像处理的方法。还有dropout。

非线性降维基本掌握流程就行

大题没有画模型结构图

RNN不作为重点考察内容

极大似然:给一个应用场景,写似然的式子,概率分布都写的,无非是取一个log然后对所有的样本求和

SVM二维计算量不大,侧重于概念,考了很多选择题

考试不涉及求矩阵的逆、特征值的具体计算

没有问概念的简答题

选择题重点:过拟合欠拟合、偏差方差、概率图、深度学习等等

MAP的概念需要知道,大题里面没有

LDA需要知道

K-L变换需要掌握过程,能达到什么目的,但是没有数值计算

逻辑斯蒂回归需要知道正则,损失函数的推导,实际上是负log似然,关于似然的计算,这个过程需要大家知道。

极大似然或贝叶斯求参数今年没有

线性回归逻辑斯蒂回归损失函数的推导需要掌握,+正则

Adaboost知道过程就行

核函数表示两个样本的相似度,常用的核函数,比如多项式核,RBF核,还有核函数控制模型复杂度的超参数,就差不多了

DBSCAN具体过程不考,需要知道对哪些数据合适

EM算法不考察

泛化误差不会考具体的公式,知道泛化误差的上界由两部分构成,一部分是和训练误差有关系,另外一部分是模型复杂度(VC维)有关系,是一个正关系还是反关系就行了

贝叶斯估计、高斯分布参数求解不作为大题考试内容,MLE的计算过程希望大家了解,考试可能不考

高级人工智能

20道选择题

3道简答题

3道综合应用题

2022.11.17 罗平老师

确定性的知识:

命题逻辑:语法和语义,蕴含和形式推演

三种形式推演的系统:

  • 11条规则
  • 归结原理:完备性和可靠性,计算机实现是一个搜索问题,联系前面的搜索算法考察
  • Modus Ponens规则:完备性和可靠性,Forward Chaining和Backward Chaining

一阶谓词逻辑:与命题逻辑对应复习,不考证明

Logic Programming:一种新的编程的思路

  • Logic Programming与正常的编程有什么差异?选择题
  • Prolog要能读得懂
    • Prolog有时候会推出错误的答案,实现的时候并不可靠
    • 有时候正确的答案也推不出来,也不完备

不确定性的知识:

模糊集合之间的运算,交并补、模糊关系、模糊关系的合成,用模糊逻辑表示自然语言

模糊逻辑比一阶谓词逻辑多了模糊谓词、模糊量词和模糊修饰词

2022.12.29

深度学习部分:

受限玻尔兹曼机原理理解就可以了

卷积神经网络、循环神经网络用的比较多,对于具体模型来说,要了解模型的原理,为什么采用这种结构就可以

更倾向于概念

不会考公式,梯度下降应该熟练掌握的

综合应用题分三个小问题,每一个是一个方面,各自独立

A*树搜索和图搜索的最优性证明最好是了解一下

简答题是搜索方面的,搜索这些算法相应的原理了解一下就可以了

简答题不是要求证明的,没有证明题

简答题是单个题目

没有考公式推导

野人传教士问题,实际上是考你搜索问题的形式化描述,形式化描述了解的话应该是没问题的

对于GAN,基本概念和原理掌握,考试掌握基本原理就可以了

对于启发式搜索,主要是设计一个合适的启发式函数(可采纳性和一致性),针对实际问题用松弛问题的解来作为启发式函数就可以

综合应用题是神经网络相关

综合应用题有要求画神经网络结构的,说明具体采用的算法

选择题都是一些基本概念

机器学习

单选30题,每题1分

多选15题,每题1分

简答3题,每题5分

计算3题,每题10分

设计1题,每题10分

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + Pattern Recognition and Machine Learning + + Advanced AI + + + +
+ + + + + MIT-6.824 Distributed Systems-LEC 4 Primary-Backup Replication + + /2022/12/18/6.824/Distributed-Systems-MIT-6.824-LEC-4/ + + MIT-6.824(Spring 2022)LEC 4 Primary-Backup Replication

Fault-Tolerant Virtual Machines 论文阅读

参考翻译

摘要

通过提供故障容错性的虚拟机,我们实现了一个商业化的企业级系统,建立在复制一个主虚拟机的执行过程到另一个服务器上的备份虚拟机的基础上。系统很容易使用,同时保证了应用的性能仅有少于10%的降低。另外,为了让主VM和二级VM的执行活动保持一致,对于几个实际的应用而言,需要的数据带宽少于20Mbit/s,这也允许实现更长距离的故障容错的可能性。一种容易使用,在故障后自动恢复备份的商业化系统,在复制VM执行之前需要额外的组件。我们已经设计并且实现了这些额外的组件,并且解决了在支持VM运行企业级应用的时候,遇到的许多实际问题。

1. 简介

一个实现故障容忍服务器的常见方法是主备机制,主服务器失败的同时另外一个备份服务器立即进行接管,此时对于外部客户端而言,故障就相当于被隐藏了起来,并且不会造成数据丢失。因此在任何时间,备份服务器的状态必须和主服务器几乎保持一致,在备份服务器上复制状态的一种方法是将主服务器的所有状态,包括CPU、memory、IO设备,连续地送给备份服务器。然而,这种发送状态的方法,尤其是涉及到内存中的变更,其需要的带宽非常大。

另一种可以用更少带宽复制服务器的方法类似于状态机。这种思路是将服务器建模为确定性的状态机,他们从相同的初始状态开始,并且确保以相同的顺序接收相同的输入请求,这样就能保持同步。因为大多数服务器或服务有一些不确定性的操作,因此必须使用额外的协调机制来确保主备同步。然而,需要保持主备一致性的额外信息数目,远远少于正在变更的主服务器上状态(主要是内存更新)的数目。

实现协调机制来确保物理服务器的确定性操作是困难的,尤其随着处理器频率增长。反之,一个运行在管理程序(hypervisor)上的VM,是一个实现状态机方法的很好的平台。 一个VM可以被当作一个定义好的状态机,它的操作是机器被虚拟化的操作(包括它所有的设备) 。和物理服务器一样,VM有相同的非确定性操作(例如读取时钟或发送中断),因此为了保持同步,额外的信息必须被发送给备份服务器。管理程序(hypervisor)有VM的全权控制权利,包括处理所有输入,因此它能够获得所有与主VM上的非确定性操作有关的必要信息,并且能正确地重放这些操作。

因此,这个状态机方法可以通过商业化软件上的VM来实现,它不需要硬件更改,允许在最新的微处理器上立刻实现故障容错。另外,状态机方法需要的低带宽允许了主备服务器能更好地进行物理分隔。例如,被复制的VM可以运行在横跨一个学校的物理机器上,相比于运行在同一建筑内的VM而言,可以提供更多的可靠性。

我们在VMware vSphere 4.0平台上使用主备机制实现了故障容忍的VMs,VMware vSphere实现了一个完整的x86虚拟机,所以我们自动地能够为任何x86操作系统和应用提供故障容忍。这种允许我们记录一个主服务器执行,并确保备份服务器一致执行的基础技术是确定性重放。VMware vSphere Fault Tolerance(FT)是基于确定性重放(Deterministic Replay) 的,但是为了建立一个完整的故障容忍系统,还增加了必要的额外协议和功能。除了提供硬件故障容忍,我们的系统在一次失败后,通过在局部集群中任何可接受的服务器上开始一个新的备份虚拟机,进行自动地存储备份。目前确定性重放和VMare FT的产品版本只支持单处理器的VMs。多处理器VM的操作记录和重放还在开发中,因为每个共享内存的操作都是一个非确定性的操作,因此还有重要的性能问题待解决。

Bressoud和Schneider描述了一个针对HP PA-RISC平台的故障容忍VMs的原型实现。我们的方法是相似的,但是出于性能原因,以及在调查了许多可替代设计后,我们已经做了一些基础性的改变。另外,为了建立一个完整的系统,而这个系统是有效的并且能够被正在运行企业级应用的客户使用,我们已经设计并实现了系统中许多额外的组件,可以处理许多实际问题。与大多数其他实际系统讨论的类似, 我们只尝试应付fail-stop的故障 ,这是一种服务器故障,可以在故障服务器造成一次不正确的外部可见行为之前被检测。( Hades注:fail-stop故障指的是,如果某些东西出现故障,只是单纯的停止运行,而不是运算出错误结果。比如电源线、服务器风扇导致CPU过热停止运行、网络等故障

2. 基本的FT设计

图1展示了我们系统在故障容忍VMs的基本步骤。对于一个给定的VM,我们希望提供故障容忍(主VM),我们在一个完全不同的物理机器上运行一个备份VM,保持和主VM同步并且执行一致,虽然存在短时间的滞后。我们说这两个VMs是虚拟的步调一致。VMs的虚拟磁盘是在一个共享存储中的(例如一个Fibre Channel或者iSCSI磁盘阵列),因此可以接受主备服务器的输入和输出。(我们将在4.1节中讨论带有分隔的非共享虚拟磁盘的主备VM的设计)只有主VM会说明它在网络中的存在,因此所有网络输入都会来到主VM上。相似地,所有其他输入(例如键盘和鼠标)也只会来到主VM上。

所有主VM接收到的输入都会通过名为logging channel的网络连接,被发送到备份VM上。对于几个工作负载而言,主要的输入途径是网络和磁盘。为了保证备份VM和主VM使用相同的方式执行非确定性操作,下面2.1节讨论的额外的信息也需要发送。最终备份VM总是执行和主VM一致的操作。然而,备份VM的输出会被管理程序扔掉,因此只有主VM产生实际输出,并被返回给客户端。和2.2节中描述的一样,为了确保主VM失败后没有数据丢失,主备VM遵循一个具体的协议,包括备份VM明确的确认信息。

为了检测主或备份虚拟机是否失败,我们的系统既使用相关服务器间的心跳机制,同时也监测 logging channel 上的流量。另外,我们我们必须确保只有主或备份VM执行操作,即使存在脑裂(split brain)的场景(在这种场景中主备服务器互相之间会失去通信)。

2.1 确定性重放的实现

正如我们已经提到的,复制服务器(或者VM)的操作可以被建模为确定性状态机的复制。如果两个确定性的状态机以相同的初始状态开始,并且以相同的顺序提供确切的输入,它们将经历相同的状态序列并且产生相同的输出。一个虚拟机有很宽泛的输入,包括到来的网络包,磁盘读,以及来自键盘和鼠标的输入。非确定性事件(例如虚拟中断)和非确定性操作(例如处理器的时钟周期计数器)也会影响虚拟机的状态。这显示了对于正在运行任何操作系统和工作负载的任何虚拟机而言,复制执行有 三个挑战

  1. 为了保证一个备份虚拟机的确定性执行,正确地得到所有输入以及非确定性执行是必要的。
  2. 正确地将输入与非确定性执行应用到备份虚拟机上。
  3. 以一种不会引起性能退化的方式执行。

另外,许多在x86处理器上的复杂操作还未被定义,因此会引起非确定性以及副作用。捕获这些未定义的操作并且重放它们产生相同的状态是一个额外的挑战。

针对在VMare vSphere平台上的x86虚拟机,VMware确定性地重放恰好提供了这个功能。确定性重放记录了 VM 的输入以及与 VM执行相关的所有可能的不确定性的日志条目流,这些条目会被写入日志文件。在读取日志文件中的条目后,VM 操作会被精确地重放。 对于非确定性操作,为了允许操作以相同的状态变化和输出再现,需要记录足够的信息。 对于非确定性事件,例如定时器或 IO 完成中断,事件发生的确切指令也会被记录下来。 在重播期间,事件被传递在指令流中的同一位置。 VMware 确定性重放采用各种技术,实现了高效的事件记录和事件传递机制,包括使用AMD和英特尔联合开发的硬件性能计数器。

Bressoud 和 Schneider提到将VM执行切分成不同的epoch,其中非确定性事件,例如中断仅在一个epoch结束时传递。 epoch的概念似乎被用作批处理机制,因为在它发生的确切指令处单独传递每个中断的成本太高。然而,我们的事件传递机制足够高效,以至于 VMware确定性重放不需要使用epochs。 每次中断在发生时被记录,并且在重放时有效地传递到适当的指令处。

2.2 FT协议

对于 VMware FT而言,我们使用确定性重放来生成必要的日志条目来记录主VM的执行情况,但是不是将日志条目写入磁盘,而是通过日志通道将它们发送到备份VM。备份 VM 实时重放日志条目,因此与主 VM 的执行保持一致。 然而,我们必须在日志通道中使用严格的 FT 协议以增强日志条目,从而确保我们实现故障容忍。 我们的基本要求如下:

输出要求 :如果备份VM在主VM发生故障后接管,那么备份VM将继续以一种与主虚拟机发送到外部世界的所有输出完全一致的方式执行。

请注意,在发生故障转移后(即备份 VM 需要在主VM故障后接管),备份VM开始执行的方式可能与主 VM 相当不同,因为在执行期间发生了许多非确定性事件。但是,只要备份VM满足输出要求,在故障转移到备份 VM期间 没有外部可见状态或数据的丢失 ,客户端将注意到他们的服务没有中断或不一致。

可以通过延迟任何外部输出(通常是网络数据包)直到备份VM 已收到重放的所有信息来确保输出要求,这些信息允许它至少执行到该输出操作的点。一个必要条件是备份 VM 必须接收到输出操作之前生成的所有日志条目。这些日志条目将允许它执行到最后一个日志条目的点。但是,假设失败是在主VM执行输出操作后立即发生。备份 VM 必须知道它必须继续重播到输出操作点,并且到那时只能“上线”(停止重播并作为主VM接管,如2.3 节所述)。如果备份将在输出操作之前的最后一个日志条目点上线,一些非确定性事件(例如计时器传递给 VM 的中断)可能会在执行输出操作之前改变其执行路径。

给定上述的限制,强制满足输入要求的最容易的方式是在每个输出操作时创建一个特殊的日志条目。然后,输出要求一定被下面特殊的规则限制:

输出规则 :主VM可能不发送一个输出到外部世界,直到备份VM已收到并确认与产生输出的操作相关的日志条目。

如果备份 VM 已收到所有日志条目,包括生成输出操作的日志条目,然后备份 VM 将能够准确地重现主 VM在输出点的状态,所以如果主VM死了, 备份将正确地达到一个与输出一致的状态 。相反,如果备份VM在没有收到所有必要的日志条目的情况下接管,那么它的状态可能会 迅速分歧 ,以至于与主服务器的输出不一致。输出规则在某些方面类似于 [11] 中描述的方法,其中“外部同步” IO 实际上可以被缓存,只要它在下一次外部通信之前确实被写入磁盘了。

请注意,输出规则没有说明关于停止主VM执行的任何事。我们只需要延迟输出发送,但 VM 本身可以继续执行。由于操作系统通过异步中断来指示完成,因此可以执行非阻塞的网络和磁盘输出,VM可以轻松地继续执行并且不一定会立即受到输出延迟的影响。相比之下,以前的工作 [3, 9] 通常必须在执行输出之前完全停止主VM,直到备份 VM 已确认来自主 VM 的所有必要信息。

作为一个例子,我们在图2中展示了 FT 协议的需求。该图显示了一个主VM和备份VM上的事件时间线。从主线到备份线的箭头表示日志条目的传输,从备份线路到主线路的箭头表示确认。有关异步事件、输入和输出操作的信息必须作为日志条目发送到备份VM并确认。如图所示,到外部世界的输出被延迟,直到主VM收到来自备份 VM 的确认,它已经收到与输出操作相关的日志条目。鉴于遵循输出规则,备份VM将能够以这样一种状态接管,即与主VM最后的输出一致。

我们不能保证一旦出现故障转移情况,所有输出都准确地产生一次。当主VM打算发送输出时, 没有使用两阶段提交事务 ,备份VM无法确定主VM是在发送它的最后一个输出之前还是之后立即崩溃。 幸运的是,网络基础设施(包括常用的TCP)旨在处理丢失的数据包和相同(重复)的数据包 。 请注意传入到主VM的数据包也可能在其故障的期间丢失,因此不会被传递给备份VM。 但是,传入的数据包可能会由于与服务器故障无关的任何原因被丢弃,因此网络基础设施、操作系统和应用程序都被写入,以确保他们可以弥补丢失的数据包。

2.3 检测与故障响应

如上所述,如果另一个 VM 出现故障,主备VMs必须快速响应。如果备份VM出现故障,主VM将上线,即离开记录模式(因此停止发送条目到日志通道)并开始正常执行。如果主VM失败,备份VM应该同样上线(go live),但过程更为复杂。由于其执行的滞后,备份 VM 可能会有许多它已收到并确认,但尚未消耗的日志条目,因为备份 VM 尚未达到执行的适当点。 备份VM必须继续重放日志条目,直到它消耗了最后一个日志条目 。此时,备份 VM 将停止重放模式并开始作为正常VM执行。本质上备份VM被提升为主VM(现在缺少备份VM)。由于它不再是备份 VM,当操作系统执行输出操作时,新的主VM现在将向外部世界生产输出。在过渡到正常模式期间,可能会有一些特定设备的操作需要允许正确地发送输出。特别是, 出于联网目的,VMware FT 自动在网络上通告新的主VM的MAC 地址,以便物理网络交换机知道新的主 VM 所在的服务器 。此外,新提升的主VM可能需要重做一些磁盘 IO(如第 3.4 节所述)。

有许多可能的方法来尝试检测主备VMs的故障。VMware FT在运行容错VMs的服务器之间使用 UDP心跳 ,来检测服务器何时崩溃。此外,VMware FT 监控日志流量,包括从主到备的发送以及从备到主的确认。因为定时器中断,日志流量应该是有规律的,并且永远不会停止。因此,在日志条目或确认流中的中断可能表明VM故障。如果心跳或记录流量已停止超过特定超时时间(大约几秒钟),就可能发生故障了。

但是,任何此类故障检测方法都容易受到脑裂(split brain)问题的影响 。如果备份服务器停止接收来自主服务器的心跳,这可能表明主服务器出现故障,或者可能只是意味着所有仍在运行的服务器之间的网络连接丢失。如果备份VM随后上线,而主VM也仍然在运行,对于与VM通信的客户端而言可能会有数据损坏以及其他问题。因此,我们必须确保当检测到故障时,主VM和备份VM只有一个在线。为了避免脑裂问题,我们利用共享存储,来存储VM的虚拟磁盘。 当任一主或备份VM想要上线时,它会在共享存储中执行一个原子性的测试设置操作 。 如果操作成功,VM 被允许上线。 如果操作失败,那么另一个 VM 一定已经上线,所以当前虚拟机实际上停止了自己(“自杀”)。 如果尝试执行此原子操作时,VM 无法访问共享存储,然后它只是等待,直到可以访问。 注意如果由于存储网络上的某些故障而无法访问共享存储时,那么虚拟机可能无法做有用的工作,因为虚拟磁盘在同样的共享存储中,因此,为了解决脑裂问题而使用共享存储不会引入任何额外的不可接受性。( Hades注:使用共享存储这种解决方案本身使得主备又得以通信了,只不过是通过信号量,而非socket。

这个设计的一个最终方面是一旦故障发生并且一个VM已经上线,VMware FT自动地通过在另一个主机上开始一个新的备份VM,来恢复备份。虽然这个过程不能覆盖过去大部分的工作,但是对于故障容忍的VM有用,它是基础,需要仔细设计。

3. FT的实际执行

第二节描述了我们基础的设计以及FT协议。然而,为了创建一个有用的、健壮的以及自动化的系统,有许多其他组件必须设计实现。

3.1 启动与重启 FT VMs

一个必须被设计的最大的额外组件是这种机制,即 启动一个拥有和主VM状态一样的备份VM 。当故障发生后重启一个备份VM时,这个机制也将变得很有用。因此,这个机制一定可用于一个处于任意状态的正在运行中的主VM。此外,我们希望该机制不会显著地中断主VM的执行,因为这会影响 VM 的任何当前客户端。

对于 VMware FT而言,我们调整了VMware vSphere上现有的 VMotion 功能。 VMware VMotion [10] 允许以最少中断的方式,将正在运行的 VM 从一台服务器迁移到另一台服务器,VM的暂停时间通常不到一秒钟。我们创建了一个VMotion的修改形式,可在远程服务器上创建准确的 VM 运行副本,但不会破坏本地服务器的虚拟机。也就是说,我们修改后的 FT VMotion 将VM克隆到远程主机上而不是迁移它。 FT VMotion还设置了一个日志记录通道,并导致源VM作为主VM进入日志记录模式,而目的VM 作为备份进入重放模式。像平常的VMotion一样,FT VMotion 通常会中断主VM的执行不到一秒。因此,启用 FT在正在运行的 VM 上是一个简单的、无中断的操作。

启动备份 VM 的另一个方面是选择一个运行它的服务器。容错 VM 在服务器集群中运行,可以访问共享存储,因此所有 VM通常可以运行在集群上的任何服务器中。这种灵活性允许VMware vSphere恢复FT冗余,即使一个或多个服务器失效。 VMware vSphere 实现了一种集群服务,用于维护管理以及资源信息 。 当发生故障并且主VM 现在需要一个新的备份 VM 来重新建立冗余时,主 VM 通知集群服务它需要一个新的备份。 集群服务基于资源利用率以及其他约束,决定运行备份VM最好的服务器,并调用 FT VMotion 以创建新的备份 VM。 结果是 VMware FT通常可以在几分钟内重新建立VM冗余,在一个故障容忍VM的执行上,所有这些都没有任何明显的中断。

3.2 管理日志通道

在管理日志通道上的流量时,有许多有趣的实现细节。在我们的实现中,管理程序为主备 VM 的日志记录条目维持了一个大的 缓冲区 。当主 VM 执行时,它生成日志条目到缓冲区中,类似地,备份VM从它的日志缓冲区中消耗日志条目。主日志缓冲区的内容会被尽快刷新到日志记录通道,这些日志条目一到日志通道,就会被读取到备份的日志缓冲区。备份每次从网络上读取一些日志条目到它的日志缓冲区时,都会发送确认返回给主VM。这些确认允许 VMware FT 确定一个被输入规则延迟的输出何时可以被发送。图3说明了这个过程。

如果备份 VM 在需要读取下一个日志条目时,遇到空的日志缓冲区,它将停止执行直到有新的日志条目可用。由于备份 VM 是不与外部通信的,此暂停不会影响任何VM 的客户端。同样地,当主VM需要写入一个日志条目时,如果主VM遇到一个完整的日志缓冲区,它必须停止执行,直到可以刷新日志条目。这种执行的停止是一种自然的流控制机制,当主VM生产日志条目太快了,它会减慢主VM。但是,此暂停可能会影响VM的客户端,因为主 VM 将完全停止并且无响应,直到它可以记录其条目并继续执行。因此,我们的实现必须设计为尽量减少主日志缓冲区填满的可能性。

主日志缓冲区可能填满的原因之一是备份 VM 执行速度太慢,因此消耗日志条目太慢。 一般来说,备份VM必须能够以与正在记录执行的主VM大致相同的速度重放执行 。幸运的是,在 VMware 确定性重放中,记录和重放的开销大致相同。然而,如果由于其他VMs,托管备份 VM 的服务器负载很重(因此过度使用资源),备份VM 可能无法获得足够的 CPU 和内存资源,来与主 VM 一样快地执行,尽管备份管理程序的VM调度器已经尽了最大努力。

如果日志缓冲区填满,除了避免意外暂停,还有另一个原因是我们不希望滞后变得太大。如果主VM出现故障,备份VM必须通过重放它在上线和开始与外部世界交流之前已经确认的所有日志条目来“赶上”。完成重放的时间基本上是失败点的执行延迟时间,所以 备份上线的时间大约等于故障检测时间加上当前执行时差 。因此,我们不希望执行滞后时间太大(超过一秒),因为这将显著地增加故障转移时间。

因此,我们有一个额外的机制减慢主VM,以防止备份 VM 获取太滞后了。在我们的发送和确认日志条目的协议中,我们发送附加信息来确定主备VM之间的实时执行滞后。通常执行滞后小于 100 毫秒。 如果备份 VM 有一个显著的执行滞后(例如,超过 1 秒),VMware FT 通过通知调度程序给它稍微少一点的CPU(最初只是百分之几)来减慢主 VM 。我们使用一个缓慢的反馈循环,这将尝试逐步确定适当的 CPU 限制,将允许主备 VM同步执行。如果备份 VM 继续滞后,我们继续逐步降低主VM的 CPU 限制。反之,如果备份VM赶上,我们逐渐增加主VM的 CPU 限制,直到备份虚拟机恢复轻微的滞后。

请注意,主VM的这种减速很少见,通常只在系统处于低压力时发生。第 5 节的所有性能编号包括任何此类放缓的成本。

3.3 FT VMs上的操作

另一个实际问题是处理各种控制操作,它们可以应用于主 VM 。例如,如果主VM明确关闭电源,备份 VM 也应该停止,而不是尝试上线。 再举一个例子,任何主VM上的资源管理更改(例如增加 CPU 份额)应该 也适用于备份。 对于此类操作,为了影响备份进行合适的操作,特殊的控制条目通过日志通道从主发送到备份。

一般来说,VM 上的大部分操作都应该仅在主 VM 上初始化。 VMware FT 然后发送任何必要的控制条目以造成备份VM上适当的更改。 唯一可以独立在主VM和备份VM上完成的操作是 VMotion。 那即,主VM和备份VM可以独立被 VMotioned到其他主机。 请注意,VMware FT 确保两个 VM 都不会移动到另一个 VM 所在的服务器,因为这种场景将不再提供故障容忍。

主VM的VMotion增加了比普通VM更多的复杂性,因为备份VM一定会与源主VM失去连接以及在适当的时间重连。备份VM的VMotion有一个相似的问题,但是只增加了一个额外的复杂性。对于一个正常的VMotion而言,我们需要当VMotion上最后的切换发生时,所有的磁盘IO停止(或完成)。对于一个主VM而言,这种停顿是容易应付的,通过等待直到物理IO完成并将这些完成信息发送给VM。然而,对于一个备份VM而言,没有容易的方式来使得所有IO在任何需要的时刻完成,因为备用VM必须重放主VM的执行过程,并在相同的执行点完成IO。主VM可能正运行在一个工作负载上,在正常执行过程中总是有磁盘IO。VMware FT有一个独一无二的方法来解决这个问题。当一个备份VM是在VMotion最后的切换点时,它需要通过日志通道来告知主VM临时停止所有IO。备份VM的IO将自然地被停止在一个单独的执行点,因为它需要重放主VM的停止操作的过程。

3.4 磁盘IO的实现问题

有许多与磁盘IO相关的微小的实现问题。首先,假设磁盘操作是非阻塞的,因此访问相同磁盘位置的并行、同时执行的磁盘操作将引起非确定性。此外,我们的磁盘 IO 实现使用DMA 直接from/to虚拟机的内存,所以同时访问相同内存页的磁盘操作也可能导致不确定性。我们的解决方案是 经常检测任何此类 IO 竞争 (很少见),以及强制此类竞争磁盘操作在主备VM上按顺序执行。

第二,通过 VM 中的应用程序(或操作系统)时,磁盘操作与内存访问也会存在竞争,因为磁盘操作通过 DMA 直接访问 VM 的内存。例如,如果一个VM 中的应用程序/操作系统正在读取内存块,同时对该块进行磁盘读取。( Hades注:这里的意思应该是,该块内存作为DMA操作的目的地址。 )这个情况也不太可能发生,但如果它发生,我们必须检测它并处理它。一种解决方案是临时设置页保护,在作为磁盘操作目标的页面上。如果VM 碰巧访问一个页,同时该页面也是磁盘操作的目标,页保护将导致一个陷阱( Hades注:trap,陷入系统调用 ),VM将暂停直到磁盘操作完成。 因为改变页上的MMU 保护是一项昂贵的操作,所以我们选择使用 弹跳缓冲区(Bounce Buffer) 代替 。bounce buffer是临时缓冲区,与正在被磁盘操作访问的内存大小相同。磁盘读取操作被修改为读取指定数据到bounce buffer,并在在IO完成时将数据复制到内存中。相似地,对于磁盘写操作,首先将要发送的数据复制到bounce buffer,磁盘写入修改为向bounce buffer写入数据。bounce buffer的使用会减慢磁盘操作,但我们还没有看到它会导致任何明显的性能损失。( Hades注:bounce buffer存在的意义是在内存访问这个操作之前加了一个拦截器,其最本质的意义是为了supervisor监控DMA操作,使得数据从bounce buffer拷贝到到内存和系统中断这两个步骤,能够同时在backup VM上被复制, 否则网卡直接将网络数据包DMA到Primary虚机中这个操作是无法通过log channel进行复制的

第三,有一些与故障发生并且备份VM接管时,主VM未完成的磁盘 IO 相关的问题。对于新上线的主VM,没有办法确定磁盘IO是有问题的还是成功完成了。另外,由于磁盘IO没有从外部发布到备用VM上,而是通过主备传递,因此对于继续运行的新上任的主VM来说,将没有明确的IO完成信息,最终将导致VM上的操作系统开始中止或者重调度程序。我们能够发送一个错误完成,表示每个IO失败,因为即使IO成功完成了,它可以接受返回一个错误。然而,操作系统可能不能对这些来自本地磁盘的错误有很好的响应。反之,我们在备份VM上线的过程中,重新发送这些悬挂着的IO。因为我们已经限制了所有的竞争和所有的直接指定内存和磁盘的IO,这些磁盘操作可以被重新发送,即使它们已经成功完成了(即他们是幂等的)。

3.5 网络IO的实现问题

VMware vSphere针对VM网络提供了很多性能优化。一些优化是基于管理程序(supervisor) 异步更新虚拟机的网络设备状态 。例如,当VM正在执行时,接收缓冲区可以由管理程序直接更新。不幸的是这些对 VM 状态的 异步更新会增加不确定性 。除非我们可以保证所有更新都发生在主备指令流上的同一点,否则备份VM的执行可能与主VM的执行不同。

对于FT而言,网络仿真代码的最大变化是禁用异步网络优化。异步更新带有传入数据包的VM环形缓冲区的代码已被修改,以强制管理程序捕获到操作系统,它可以在其中记录更新然后将它们应用到 VM。同样,异步地将数据包从传输队列中拉出也被修改了,取而代之的是通过管理程序traps来完成传输(如下所述)。

网络设备异步更新的消除结合第 2.2 节中描述的发送数据包的延迟带来了一些网络性能的挑战。我们采取了两种方法在运行 FT 时提高 VM 的网络性能。第一,我们实施了集群优化以减少 VM 的陷阱和中断。当 VM 以足够的比特率流式传输数据时,管理程序可以对每组数据包做一个传输trap,在最好的情况下零trap,因为它可以传输所接收新数据包的一部分数据包。同样地,通过仅对于一组数据包发布中断,管理程序可以将接收包的中断数量减少。

我们对网络的第二个性能优化涉及 减少传输数据包的延迟 。如前所述,管理程序必须延迟所有发送的包直到它得到备份VM对于某些日志条目的确认。减少发送延迟的关键在于减少发送/接收备份VM信息的所需时间。我们的主要优化包括 保证收发信息在无需任何线程上下文切换的情形下就可以被执行 。VMware vSphere管理程序允许函数被注册到TCP栈中,只要TCP数据被接收到了,函数就会被一个延期执行的上下文调用(和Linux中的tasklet类似)。这允许我们快速处理备份VM上任何即将到来的日志消息,以及主VM接收的任何确认消息,而不需要任何线程上下文的切换。另外,当主VM有一个包要寄出去时,我们强制一次相关输出日志条目的日志刷出(正如2.2节中所描述的),通过调度一个延迟执行的上下文来执行这次刷出。

4. 替代设计

在我们VMware FT的实现中,我们已经探索了许多有趣的替代设计。在这节中,我们探索一些替代设计。

4.1 共享 vs. 非共享磁盘

在我们默认的设计中,主备VM共享相同的虚拟磁盘。因此,如果一次故障转移发生,共享磁盘的内容自然是正确、可接受的。必要地,对于主备VM来说,共享磁盘被认为是外部的,因此任何共享磁盘的写入被认为是一次与外部世界的沟通。因此,只有主VM做这种实际的磁盘写入,并且为了遵循输出规则,这种写入必须被延迟。

对于主备VM而言,一种可替代的选择是分隔的虚拟磁盘。在这种设计中,备份VM要执行所有虚拟磁盘的写入操作。而且这样做的话自然要保持它的虚拟磁盘内容与主VM虚拟磁盘内容一致。图4阐述了这种配置。在非共享磁盘的情况下,虚拟磁盘必须被认为是每个VM的内部状态。因此,依据输出规则, 主VM的磁盘写入不必延迟 。在共享存储不能被主备VM接受的情况下,非共享的设计是相当有用的。这种情况可能是由于共享存储不可接受或者太昂贵,或者由于运行主备VM的服务器相隔太远(“长距离FT”)。非共享设计的一个缺点是在首次启动故障容错时,虚拟磁盘的两个复制必须以相同的方式进行显示同步。另外,发生故障后磁盘 可能会不同步 ,因此当在一次失败后备份VM重启的时候,他们必须再显式地同步。FT VMotion必须不止同步主备VM的运行状态,还要同步他们的磁盘状态。

在这种非共享磁盘的配置中,他们也能应付脑裂场景。在这种场景中,系统能够 使用一些其他的外部决策者 ,例如所有服务器可以沟通的一个第三方服务。如果服务器是超过两个节点的集群的一部分,这个系统能够基于集群关系使用一种majority算法。在这个例子中,一个VM能够被允许上线,如果它正在一个服务器上运行,这个服务器是包含大多数原始节点的正在通信的子集群的一部分。

4.2 在备份VM上执行磁盘读

在我们默认的设计中,备份的VM从不会从它自己的虚拟磁盘上读取(无论共享还是非共享)。 因为磁盘读取被认为是一个输入 ,它是自然地通过日志通道将磁盘读取的结果发送到备份VM上。

一种替代的设计是 让备份VM执行磁盘读取 ,因此消除了磁盘读取的日志。对于大多数时候都做磁盘读取的工作负载而言,这种方法可以很好地降低日志通道上的流量。然而,这种方法有很多小问题。它可能会减慢备份VM的执行速度,因为备份VM必须执行所有的磁盘读取,当到达VM执行中主VM已经完成的位置时,如果备份上的磁盘读取还没完成就必须等待。

同样地, 为了处理失败的磁盘读取操作,必须做一些额外的工作 。如果一个主VM的磁盘读取成功了,但是相应的备份VM磁盘读取失败了,备份VM的磁盘读取必须重试直到成功。因为备份VM必须获得和主VM一样的数据到内存中。相反地,如果一个主VM的磁盘读取失败了,目标内存的内容必须通过日志通道发送给备份服务器,因此内存的内容将被破坏,不能被备份VM成功的磁盘读取复制。

最后,如果这种磁盘读取被用于共享磁盘配置的话,还有一个小问题。如果主VM做了一次对具体磁盘位置的读取,然后紧跟相同磁盘位置的写入,然后这个磁盘写必须被延迟到备份VM已经执行了第一次磁盘读取。这种依赖可以被检测和正确处理,但是需要增加实现上额外的复杂性。

在5.1节中,对于实际的应用而言,我们给出一些性能结果以表示在备份VM上执行磁盘读取会造成一些轻微的吞吐量减少(1-4%),因此在日志通道的带宽被限制的情况下,在备份VM上执行磁盘读取可能是有用的。

5. 性能评估

在这节中,我们做了一次VMware FT性能的基础评估,针对许多应用负载以及网络基准。为了得到这些结果,我们在一样的服务器上运行主备VM,每个都带9个Intel Xeon 2.8Ghz CPUs and 8Gbytes of RAM。服务器间通过10 Gbit/s的交换机连接,但是在所有的例子中都能看到被使用的网络带宽远远少于1Gbit/s。从一个通过标准的4Gbit/s的光纤通道网络连接的EMC Clariion中,服务器可以连接他们的共享虚拟磁盘。客户端通过1 Gbit/s的网络来驱动一些连接服务器的工作负载。

我们评估性能结果的应用如下所示。SPECJbb2005是工业标准的Java应用基准,非常耗费CPU和内存,但是IO非常少。Kernel Compile是一种运行Linux核编译的工作负载。由于许多编译过程的创建和毁灭,这个工作负载做很多磁盘读取和写入,是非常耗费CPU和MMU的。Oracle Swingbench是被Swingbench OLTP工作负载(在线事务处理)驱动的一个Oracle 11g的数据库。这个工作负载做连续的磁盘和网络IO,有80个同时在线的数据库会话。MS-SQL DVD Store是一种工作负载,运行了一个Microsoft SQL Server 2005的数据库,有60个同时在线的客户端。

5.1 基本性能结果(Basic Performance Results)

表 1 列出了基本的性能结果。对于每个应用程序,第二列给出了应用程序的性能比例,运行服务器工作负载的虚拟机上启用和未启用FT的情况。性能比小于 1 表示带FT的工作负载更慢。显然,这些有代表性的工作负载上启用FT 的开销小于10%。 SPECJbb2005 完全受计算限制,没有空闲时间,但其表现性能良好,因为它具有最小的除定时器中断以外的不确定性事件。另一个工作负载做磁盘 IO 有一些空闲时间,所以一些FT 开销可能被 FT虚拟机的空闲时间更少的真实情况隐藏。然而,一般的结论是VMware FT 能够支持故障容忍VM,并且具备相当低的性能开销。

在表的第三列中,我们给出了当应用程序正在运行时,在日志通道上发送数据的平均带宽。对于这些应用程序,日志带宽相当合理,1 Gbit/s的网络就能满足 。事实上,低带宽要求表明多个 FT 工作负载可以共享相同的 1 Gbit/s网络,同时没有任何负面的性能影响。

对于运行常见操作系统的 VM,例如Linux 和 Windows,我们发现当操作系统空闲时,通常的日志记录带宽为 0.5-1.5 Mbits/sec。"空闲"带宽主要是记录定时器中断发送的结果。对于具有活动中工作负载的 VM而言,日志带宽由网络和必须发送到备份的磁盘输入主导—网络收到的数据包和从磁盘读取的磁盘块。因此,对于非常高的网络接收或者磁盘读取带宽的应用而言,日志带宽高于表1中的测量值。对于这类应用而言,日志通道的带宽可能是瓶颈,特别是日志通道还有其他使用时。

对于许多实际应用程序而言, 日志记录所需的带宽相对较低,这使得基于重放的故障容忍对于使用非共享磁盘的长距离配置非常有吸引力 。对于远距离配置而言,其主备VM可能相隔1-100公里,光纤可以轻松地支持延迟小于 10 毫秒的100-1000 Mbit/s带宽。对于表 1 中的应用而言,主备之间的额外往返延迟,可能会导致网络和磁盘输出最多延迟 20 毫秒。远距离配置仅适用于这类应用程序:他的客户端可以容忍每个请求的额外延迟。

对于两个最占用磁盘空间的应用程序,我们测量了在备份 VM上执行磁盘读取(如第 4.2 节所述)与通过日志记录通道发送磁盘读取数据相比,对于性能的影响。对于 Oracle Swingbench来说,在备份VM上执行磁盘读取时的吞吐量降低约 4%;对于 MS-SQL DVD 存储,吞吐量约降低 1%。同时,Oracle Swingbench的日志带宽从 12 Mbits/sec 降低到 3 Mbits/sec,MS-SQL DVD 存储从 18 Mbits/sec 降低到 8 Mbits/sec。显然,对于具有更大磁盘读取带宽的应用程序,带宽可能会节省很多。如第 4.2 节所述,预计在备份 VM 上执行磁盘读取时,性能可能会更差。但是,对于日志通道的带宽是有限的(例如,远程配置)情况下,在备份 VM 上执行磁盘读取可能有用。

5.2 网络基准测试(Network Benchmarks)

出于多种原因。网络基准测试对我们的系统来说非常具有挑战性。第一,高速网络会有一个非常高的中断率,这需要以非常高的速度记录和重放异步事件。 第二,以高速率接收数据包的基准将导致高速率的日志流量,因为所有这些数据包必须通过日志通道发送到备份。第三,发送数据包的基准测试将受制于输出规则,延迟网络数据包的发送直到已收到来自备份VM的确认。 此延迟会增加对客户端测量的延迟。这种延迟还可能会降低到客户端的网络带宽,因为网络协议(如 TCP)由于往返延迟增加,可能不得不降低网络传输速率。

表 2 给出了我们通过标准的netperf 基准测试,多次测量的结果。在所有这些测量中,客户端 VM 和主 VM 通过 1 Gbit/s 网络连接。前两行给出了主备主机间通过1 Gbit/s 的日志通道连接时,发送和接收的性能。第三行和第四行给出当主备服务器通过10 Gbit/s的日志通道连接时,发送和接收的性能,不仅带宽更高,延迟也低于 1 Gbit/s。作为一个粗略的测量,在1 Gbit/s 网络连接的管理程序之间, ping 时间约为 150 微秒,而对于 10 Gbit/s 连接,ping时间大约需要 90 微秒。

未启用 FT 时,主 VM 对于接收和发送,可以实现接近 (940 Mbit/s) 1 Gbit/s 的线路传输速率。当为接收工作负载启用 FT 时,日志记录带宽非常大,因为所有传入的网络数据包必须在日志通道上发送。因此,日志记录通道可能成为瓶颈,正如1 Gbit/s 日志网络的结果。对于 10 Gbit/s 的日志网络,影响则小了很多。当为上传工作负载启用 FT 时,上传数据包的数据不会记录,但仍必须记录网络中断。日志带宽要低得多,因此可实现的网络上传带宽高于网络接收带宽。 总的来说,我们看到 FT 在非常高的上传和接收速率情况下,可以显著地限制网络带宽,但仍然可以实现很高的速率

7. 结论与今后的工作

我们在VMware vSphere 中设计并实施了一个高效完整的系统(FT) ,用于为服务器上运行的虚拟机提供容错。我们的设计基于复制主VM中的执行,再通过另一台主机上的备份VM执行VMware确定性重放。如果运行主 VM的服务器出现故障,备份 VM 能立即接管且不会中断或丢失数据。

总体而言,在商业硬件上运行VMware FT时,故障容错VM的性能非常出色,并且对于某些典型应用程序,其开销低于 10%。大多数 VMware FT 的性能成本来自于使用 VMware 确定性重放来保持主备VM同步。因此,VMware FT 的低开销源自 VMware 确定性重放的效率。此外,保持主备同步所需的日志带宽非常小,通常小于 20 Mbit/s。因为日志带宽在大多数情况下很小,主备相隔很长的距离(1-100公里)似乎也是可行的实施配置。因此,VMware FT 可用于这种场景:可以防止整个站点发生故障的灾难。值得注意的是,日志流通常是可压缩的,因此简单的压缩技术可以显著地减少日志带宽,虽然有少量额外的 CPU 开销。

我们对 VMware FT 的结果表明, 一个高效的故障容错VM的实现可以建立在确定性重放的基础上这样的系统可以透明地为运行任何操作系统和应用的虚拟机提供容错能力,仅会带来极小的开销 。然而,对客户有用的故障容错VM系统而言,它必须还具有强大、易于使用和高度自动化的特点。一个可用的系统除了复制虚拟机执行之外,还需要许多其他组件。特别是VMware FT 故障后自动地恢复冗余,通过在本地集群中找到合适的服务器并在其上创建一个新的备份VM。通过解决所有必要的问题,我们已经展示了一个在客户的数据中心可用于实际应用的系统。

通过确定性重放实现容错的权衡之一是当前确定性重放仅针对单处理器VM 。然而,单处理器虚拟机足够应付各种各样的工作负载,特别是因为物理处理器不断变得更加强大。此外,许多工作负载可以通过使用许多单处理器的虚拟机来扩展,而不是通过使用一个更大的多处理器虚拟机来扩展。多处理器 VM 的高性能重放是一种活跃的研究领域,并且可以潜在地被微处理器中的一些额外硬件支持。一个有趣的方向可能是扩展事务内存模型以促进多处理器重放。

将来,我们也有兴趣扩展我们的系统处理部分硬件故障。通过部分硬件故障,我们的意思是服务器上功能或冗余的部分丢失,不会导致损坏或丢失数据。一个例子是到 VM所有网络连接的丢失,或在物理服务器中备用电源丢失。如果在运行主 VM 的服务器上发生部分硬件故障,在许多情况下(但不是all) 故障转移到备份 VM 将是有利的。这样的故障转移对于关键VM而言,可以立即恢复完整服务,并确保虚拟机从可能不可靠的服务器上快速地移走。

LEC 4

故障

我们希望复制方案可以处理的故障:

  1. 只处理Fail-Stop类型的故障,也就是基础设施的故障导致计算机不能正常运行的类型的失败。因此失败是一瞬间发生的,这样的失败也不会产生一些奇怪的结果。
    1. 排除了逻辑错误(也就是代码错误)
    2. 排除了配置错误
    3. 排除了恶意错误(不能处理黑客、攻击者模拟出来的错误等)
  2. 可能处理的:比如地震等,但是我们不关注,因为主从机器都在一个机房中

挑战

如果发生了故障,主机器真的挂掉了吗?

在分布式系统中,没有办法区分网络分区和机器故障的区别,因此很有可能主机器并没有挂掉,有一些客户端还能访问主机器,但是从机器和主机器之间的网络有问题,无法互相访问到,所以从机器认为主机器已经挂掉了。因此不能有两个主机器同时存在的情况,也就是脑裂问题。

如何保持主从同步?

如果主机器挂了,从机器要从主机器挂掉的地方直接开始,这就意味着从机器的状态与主机器的状态相同,都是最新的。从客户端的角度感知不到这种变化。

非常困难:

  • 我们在主机器上的所有改变都要按照相同的顺序应用到从机器上
  • 解决非确定性问题,也就是相同的更改在两台机器上作的改变必须相同
  • 故障转移:要弄明白主机器在挂掉之前有没有发送过数据包,再发送一次是否可行(或者是如果所有机器都挂掉了,回来之后哪个机器上有最新的状态呢?)

两种主从复制方法

  • 状态转移:客户端与主机器进行交互,主机器更新状态,每隔一段时间有一个检查点,将状态传给从机器。因此一旦主机器有了状态的改变,这个状态就要马上传递给从机器。
  • 状态机复制:不发送状态给从机器,而是将对主机器进行更改的操作发送给从机器。

两种方法都是目前流行的方法,状态转移的缺点是如果一个操作生成了很多状态,这个传输的数据量非常大,因此如果只发送操作过去就很轻松。

复制操作的级别

应用级别:文件追加写入,需要在应用程序上进行修改

机器级别:寄存器指令级别的复制,只有x86指令,不涉及应用程序方面的更改,可以使用虚拟机实现,从而不用再硬件级别上实现。

VM-FT

利用虚拟化技术,使得复制操作对应用程序是透明的,应用程序认为仅有一台服务器,并且也同时提供了很强的一致性。

概览

虚拟机监控器(hypervisor):在实际硬件上运行,虚拟出多个虚拟的硬件

任何我们看到的外部事件实际上都经过了hypervisor,例如一个外部中断,hypervisor会先观察到并决定什么时候传递给虚拟机

多个hypervisor之间通过logging channel进行通信,从而进行操作的精确复制

pSnSrQO.md.png

storage server可以对谁当主机器进行仲裁

如果主机器和从机器不能相互通信,但是都能看到storage server,两台机器都会进行test-and-set操作,比较早的那一个就会成为主机器。

设计

目标:多台虚拟机对外表现为单一的机器

问题:差异来源导致两台机器表现不一样

非确定性指令:

  • 获取时间的指令
  • 数据的输入顺序需要相同
  • 中断指令的顺序需要相同
  • 多核——这篇论文中不允许

中断

确定性指令不需要通过logging channel进行通信

中断发生后,会传递给从机器中断发生的前一个指令号,但是从机器并不会马上去执行,而是缓存下来,等到下一条中断指令传递过来之后,再执行前一条指令。这样会落后一条指令

非确定性指令

在机器启动之前会遍历全部的指令,确保浏览到全部的非确定性指令,不会直接执行,而会交给hypervisor进行控制。hypervisor执行的时候会额外记录下这些指令操作后的对应结果。传递的时候会同时对结果进行传递,这样从机器不需要真正去执行,直接修改结果就可以。

性能

指令级别的复制会付出性能的代价

论文的实验表明带宽会降低大概30%左右,由于主机器接收来自客户端的输入,然后传递给从机器,这个过程中主机器必须等待,才能将响应传递给客户端。

因此状态机复制的方法并不常用的原因之一是性能会下降。

]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + MIT-6.824 Distributed Systems-LEC 3 GFS + + /2022/12/17/6.824/Distributed-Systems-MIT-6.824-LEC-3/ + + MIT-6.824(Spring 2022)LEC 3 GFS

GFS论文阅读

参考资料(感谢Alex!这篇论文翻译得非常有质量!)

摘要

Google GFS文件系统是一个面向大规模数据密集型应用的、可伸缩的分布式文件系统。GFS运行在廉价的普遍硬件设备上,但是依然了提供容错机制,为大量客户提供了高性能的服务。

GFS的设计目标与许多传统的分布式文件系统有很多相同之处,不过还是以我们对自己的应用的负载情况和技术环境的分析为基础进行设计,和早期的分布式文件系统有明显的不同。

GFS完全满足了我们对存储的需求。GFS作为存储平台已经被广泛的部署在Google内部,存储我们的服务产生和处理的数据,同时还用于那些需要大规模数据集的研究和开发工作。目前为止,最大的一个集群利用数千台机器的数千个硬盘,提供了数百TB的存储空间,同时为数百个客户机服务。

在本论文中,我们展示能够支持分布式应用的文件系统接口扩展,讨论我们设计的许多方面,最后列出了小规模性能测试以及真实生产系统中性能的相关数据。

1. 简介

GFS与传统的分布式文件系统有着很多相同的设计目标,比如,性能、可伸缩性、可靠性以及可用性。但是,我们的设计还基于我们对我们自己的应用的负载情况和技术环境的观察的影响,和早期文件系统的假设都有明显的不同。

所以我们重新审视了传统文件系统在设计上的折衷选择,衍生出了完全不同的设计思路。

首先,组件失效被认为是常态事件,而不是意外事件。GFS组件的数量和质量导致在任何给定时间内都有可能发生某些组件无法工作,且某些组件无法从它们目前的失效状态中恢复。因此,持续的监控、错误侦测、容错以及自动恢复的机制必须集成在GFS中。

其次,我们的文件非常巨大,GB的文件非常普遍。当我们经常需要处理快速增长的、并且由数亿个对象构成的、数以TB的数据集时,采用管理数亿个KB大小的小文件的方式是非常不明智的。因此,设计的假设条件和参数,比如I/O操作和Block的尺寸等都需要重新考虑。

第三,绝大部分文件的修改是采用在文件尾部追加数据,而不是覆盖原有数据的方式。一旦写完之后,对文件的操作就只有读,而且通常是按顺序读。对于这种针对海量文件的访问模式,客户端对数据块缓存是没有意义的,数据的追加操作是性能优化和原子性保证的主要考量因素。

第四,应用程序和文件系统API的协同设计提高了整个系统的灵活性。比如,我们放松了对GFS一致性模型的要求,这样就减轻了文件系统对应用程序的苛刻要求,大大简化了GFS的设计。我们引入了原子性的记录追加操作,从而保证多个客户端能够同时进行追加操作,不需要额外的同步操作来保证数据的一致性。

2. 设计概述

2.1 设计预期

  • 系统的组件失效是一种常态。系统必须持续监控自身的状态,迅速侦测、容忍并恢复失效的组件。
  • 系统要能存储一定数量的大文件。系统也必须支持小文件,但是不需要针对小文件做专门的优化。
  • 系统的工作负载主要由两种读操作组成:大规模的流式读取和小规模的随机读取。
    • 大规模的流式读取通常一次读取1MB甚至更多的数据。来自同一个客户机的连续操作通常是读取同一个文件中连续的一个区域。
    • 小规模的随机读取通常是在文件某个随机的位置读取几个KB数据。通常的做法是把小规模的随机读取操作合并并排序,之后按顺序批量读取。
  • 系统的工作负载还包括许多大规模的、顺序的、数据追加方式的写操作。每次写入的数据的大小和大规模读类似。数据一旦被写入后,文件就很少会被修改了。
  • 系统必须高效的、行为定义明确的。实现多客户端并行追加数据到同一个文件里的功能。
  • 高性能的稳定网络带宽远比低延迟重要。我们的目标程序绝大部分要求能够高速率的、大批量的处理数据,极少有程序对单一的读写操作有严格的响应时间要求。

2.2 接口

GFS提供了一套类似传统文件系统的API接口函数,虽然并不是严格按照POSIX等标准API的形式实现的。文件以分层目录的形式组织,用路径名来标识。支持常用的操作如创建新文件、删除文件、打开文件、关闭文件、读和写文件。

另外,GFS提供了快照记录追加操作。快照以很低的成本创建一个文件或者目录树的拷贝。记录追加操作允许多个客户端同时对一个文件进行数据追加操作,同时保证每个客户端的追加操作都是原子性的。多个客户端可以在不需要额外的同步锁定的情况下,同时对一个文件追加数据。这些类型的文件对于构建大型分布应用是非常重要的。

2.3 架构

zbm43T.md.png

一个GFS集群包含一个单独的Master节点 (alex注:这里的一个单独的Master节点的含义是GFS系统中只存在一个逻辑上的Master组件。后面还会提到Master节点复制,因此,为了理解方便,我们把Master节点视为一个逻辑上的概念,一个逻辑的Master节点包括两台物理主机,即两台Master服务器)、 和多台Chunk服务器,并且同时被多个客户端访问。Chunk服务器和客户端也可以放在同一台机器上。

GFS存储的文件都被分割成固定大小的Chunk。在Chunk创建的时候,Master服务器会给每个Chunk分配一个唯一不变的64位的Chunk标识。Chunk服务器把Chunk以linux文件的形式保存在本地硬盘上,并且根据指定的Chunk标识和字节范围来读写块数据。出于可靠性的考虑,每个块都会复制到多个块服务器上,默认是3份。

Master节点管理所有的文件系统元数据。这些元数据包括名字空间、访问控制信息、文件和Chunk的映射信息、以及当前Chunk的位置信息。Master节点还管理着系统范围内的活动,比如,Chunk租用管理、孤儿Chunk的回收、以及Chunk在Chunk服务器之间的迁移。Master节点使用心跳信息周期地和每个Chunk服务器通讯,发送指令到各个Chunk服务器并接收Chunk服务器的状态信息。

GFS客户端代码以库的形式链接到客户程序里。客户端代码实现了GFS文件系统的API接口函数、应用程序与Master节点和Chunk服务器通讯、以及对数据进行读写操作。客户端和Master节点的通信只获取元数据,所有的数据操作都是由客户端直接和Chunk服务器进行交互的。

无论是客户端还是Chunk服务器都不需要缓存文件数据。客户端缓存数据几乎没有什么用处,因为大部分程序要么以流的方式读取一个巨大文件,要么工作集太大根本无法被缓存。无需考虑缓存相关的问题也简化了客户端和整个系统的设计和实现。(不过,客户端会缓存元数据。)Chunk服务器不需要缓存文件数据的原因是,Chunk以本地文件的方式保存,Linux操作系统的文件系统缓存会把经常访问的数据缓存在内存中。

2.4 单一Master节点

单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策。不过我们必须减少对Master节点的读写,避免Master节点成为系统的瓶颈。客户端并不通过Master节点读写文件数据。而是向Master节点询问它应该联系的Chunk服务器。客户端将这些元数据信息缓存一段时间,后续的操作将直接和Chunk服务器进行数据读写操作。

一次简单读取的流程:首先,客户端把文件名和程序指定的字节偏移,根据固定的Chunk大小,转换成文件的Chunk索引。然后,它把文件名和Chunk索引发送给Master节点。Master节点将相应的Chunk标识和副本的位置信息发还给客户端。客户端用文件名和Chunk索引作为key缓存这些信息。之后客户端发送请求到其中的一个(一般是最近的)副本处。请求信息包含了Chunk的标识和字节范围。在对这个Chunk的后续读取操作中,客户端不必再和Master节点通讯了,除非缓存的元数据信息过期或者文件被重新打开。实际上,客户端通常会在一次请求中查询多个Chunk信息,Master节点的回应也可能包含了紧跟着这些被请求的Chunk后面的Chunk的信息。在实际应用中,这些额外的信息避免了客户端和Master节点未来可能会发生的几次通讯。

2.5 Chunk尺寸

Chunk的大小是关键的设计参数之一。我们选择了64MB,远远大于一般文件系统的Block size。每个Chunk的副本都以普通Linux文件的形式保存在Chunk服务器上,只有在需要的时候才扩大。惰性空间分配策略避免了因内部碎片造成的空间浪费,内部碎片或许是对选择这么大的Chunk尺寸最具争议的一点。

选择较大的Chunk尺寸有几个重要的优点。首先,它减少了客户端和Master节点通讯的需求,一次和Master节点的通信就可以获取Chunk的位置信息,之后就可以对同一个Chunk进行多次的读写操作。即使是小规模的随机读取,客户端可以轻松的缓存一个数TB的工作数据集所有的Chunk位置信息。其次,采用较大的Chunk尺寸,客户端能够对一个块进行多次操作,这样就可以通过与Chunk服务器保持较长时间的TCP连接来减少网络负载。第三,选用较大的Chunk尺寸减少了Master节点需要保存的元数据的数量。

另一方面,即使配合惰性空间分配,采用较大的Chunk尺寸也有其缺陷。小文件包含较少的Chunk,甚至只有一个Chunk。当有许多的客户端对同一个小文件进行多次的访问时,存储这些Chunk的Chunk服务器就会变成热点。在实际应用中,热点不是主要问题。

然而,当我们第一次把GFS用于批处理队列系统的时候,热点的问题还是产生了:一个可执行文件在GFS上保存为single-chunk文件,之后这个可执行文件在数百台机器上同时启动。存放这个可执行文件的几个Chunk服务器被数百个客户端的并发请求访问导致系统局部过载。我们通过将这个文件复制更多份,并错开批处理队列系统程序的启动时间的方法解决了这个问题。一个可能的长效解决方案是,在这种的情况下,允许客户端从其它客户端读取数据。

2.6 元数据

Master服务器(alex注:注意逻辑的Master节点和物理的Master服务器的区别。后续我们谈的是每个Master服务器的行为,如存储、内存等等,因此我们将全部使用物理名称)存储3种主要类型的元数据,包括:文件和Chunk的命名空间、文件和Chunk的对应关系、每个Chunk副本的存放地点。所有的元数据都保存在Master服务器的内存中。前两种类型的元数据(命名空间、文件和Chunk的对应关系)同时也会以记录变更日志的方式记录在操作系统的系统日志文件中,日志文件存储在本地磁盘上,同时日志会被复制到其它的远程Master服务器上。采用保存变更日志的方式,我们能够简单可靠的更新Master服务器的状态,并且不用担心Master服务器崩溃导致数据不一致的风险。Master服务器不会持久保存Chunk位置信息。Master服务器在启动时,或者有新的Chunk服务器加入时,向各个Chunk服务器轮询它们所存储的Chunk的信息。

2.6.1 内存中的数据结构

因为元数据保存在内存中,所以Master服务器可以在后台简单而高效的周期性扫描自己保存的全部状态信息。这种周期性的状态扫描也用于实现Chunk垃圾收集、在Chunk服务器失效的时重新复制数据、通过Chunk的迁移实现跨Chunk服务器的负载均衡以及磁盘使用状况统计等功能。

将元数据全部保存在内存中的方法的问题:Chunk的数量以及整个系统的承载能力都受限于Master服务器所拥有的内存大小。但是在实际应用中,这并不是一个严重的问题。Master服务器只需要不到64个字节的元数据就能够管理一个64MB的Chunk。每个文件的在命名空间中的数据大小通常在64字节以下,因为保存的文件名是用前缀压缩算法压缩过的。

即便是需要支持更大的文件系统,为Master服务器增加额外内存的费用是很少的,增强了系统的简洁性、可靠性、高性能和灵活性。

2.6.2 Chunk位置信息

Master服务器并不保存持久化保存哪个Chunk服务器存有指定Chunk的副本的信息。Master服务器只是在启动的时候轮询Chunk服务器以获取这些信息。Master服务器能够保证它持有的信息始终是最新的,因为它控制了所有的Chunk位置的分配,而且通过周期性的心跳信息监控Chunk服务器的状态。

最初设计时,我们试图把Chunk的位置信息持久的保存在Master服务器上,但是后来我们发现在启动的时候轮询Chunk服务器,之后定期轮询更新的方式更简单。这种设计简化了在有Chunk服务器加入集群、离开集群、更名、失效、以及重启的时候,Master服务器和Chunk服务器数据同步的问题。

可以从另外一个角度去理解这个设计决策:只有Chunk服务器才能最终确定一个Chunk是否在它的硬盘上。我们从没有考虑过在Master服务器上维护一个这些信息的全局视图,因为Chunk服务器的错误可能会导致Chunk自动消失(比如,硬盘损坏了或者无法访问了),亦或者操作人员可能会重命名一个Chunk服务器。

2.6.3 操作日志

操作日志包含了关键的元数据变更历史记录。这对GFS非常重要。这不仅仅是因为操作日志是元数据唯一的持久化存储记录,它也作为判断同步操作顺序的逻辑时间基线 (alex注:也就是通过逻辑日志的序号作为操作发生的逻辑时间,类似于事务系统中的LSN) 。文件和Chunk,连同它们的版本,都由它们创建的逻辑时间唯一的、永久的标识。

操作日志非常重要,我们必须确保日志文件的完整,确保只有在元数据的变化被持久化后,日志才对客户端是可见的。否则,即使Chunk本身没有出现任何问题,我们仍有可能丢失整个文件系统,或者丢失客户端最近的操作。所以,我们会把日志复制到多台远程机器,并且只有把相应的日志记录写入到本地以及远程机器的硬盘后,才会响应客户端的操作请求。Master服务器会收集多个日志记录后批量处理,以减少写入磁盘和复制对系统整体性能的影响。

Master服务器在恢复时,通过重演操作日志把文件系统恢复到最近的状态。为了缩短Master启动的时间,我们必须使日志足够小 (alex注:即重演系统操作的日志量尽量的少)。 Master服务器在日志增长到一定量时对系统状态做一次Checkpoint (alex注:Checkpoint是一种行为,一种对数据库状态作一次快照的行为), 将所有的状态数据写入一个Checkpoint文件 (alex注:并删除之前的日志文件)。 在灾难恢复的时候,Master服务器就通过从磁盘上读取这个Checkpoint文件,以及重演Checkpoint之后的有限个日志文件就能够恢复系统。Checkpoint文件以压缩B-树形势的数据结构存储,可以直接映射到内存,在用于命名空间查询时无需额外的解析。这大大提高了恢复速度,增强了可用性。

由于创建一个Checkpoint文件需要一定的时间,所以Master服务器的内部状态被组织为一种格式,这种格式要确保在Checkpoint过程中不会阻塞正在进行的修改操作。Master服务器使用独立的线程切换到新的日志文件和创建新的Checkpoint文件。新的Checkpoint文件包括切换前所有的修改。对于一个包含数百万个文件的集群,创建一个Checkpoint文件需要1分钟左右的时间。创建完成后,Checkpoint文件会被写入在本地和远程的硬盘里。

Master服务器恢复只需要最新的Checkpoint文件和后续的日志文件。旧的Checkpoint文件和日志文件可以被删除,但是为了应对灾难性的故障 (alex注:catastrophes,数据备份相关文档中经常会遇到这个词,表示一种超出预期范围的灾难性事件), 我们通常会多保存一些历史文件。Checkpoint失败不会对正确性产生任何影响,因为恢复功能的代码可以检测并跳过没有完成的Checkpoint文件。

2.7 一致性模型

GFS支持一个宽松的一致性模型,这个模型能够很好的支撑我们的高度分布的应用,同时还保持了相对简单且容易实现的优点。本节我们讨论GFS的一致性的保障机制,以及对应用程序的意义。我们也着重描述了GFS如何管理这些一致性保障机制。

2.7.1 GFS一致性保障机制

文件命名空间的修改(例如,文件创建)是原子性的。它们仅由Master节点的控制:命名空间锁提供了原子性和正确性的保障;Master节点的操作日志定义了这些操作在全局的顺序。

数据修改后文件region (alex注:region这个词用中文非常难以表达,我认为应该是修改操作所涉及的文件中的某个范围) 的状态取决于操作的类型、成功与否、以及是否同步修改。如果所有客户端,无论从哪个副本读取,读到的数据都一样,那么我们认为文件region是“一致的”;如果对文件的数据修改之后,region是一致的,并且客户端能够看到写入操作全部的内容,那么这个region是“已定义的”。当一个数据修改操作成功执行,并且没有受到同时执行的其它写入操作的干扰,那么影响的region就是已定义的(隐含了一致性):所有的客户端都可以看到写入的内容。并行修改操作成功完成之后,region处于一致的、未定义的状态:所有的客户端看到同样的数据,但是无法读到任何一次写入操作写入的数据。通常情况下,文件region内包含了来自多个修改操作的、混杂的数据片段。失败的修改操作导致一个region处于不一致状态(同时也是未定义的):不同的客户在不同的时间会看到不同的数据。

数据修改操作分为写入或者记录追加两种。写入操作把数据写在应用程序指定的文件偏移位置上。即使有多个修改操作并行执行时,记录追加操作至少可以把数据原子性的追加到文件中一次,但是偏移位置是由GFS选择的 (alex注:这句话有点费解,其含义是所有的追加写入都会成功,但是有可能被执行了多次,而且每次追加的文件偏移量由GFS自己计算) 。(相比而言,通常说的追加操作写的偏移位置是文件的尾部。)GFS返回给客户端一个偏移量,表示了包含了写入记录的、已定义的region的起点。另外,GFS可能会在文件中间插入填充数据或者重复记录。这些数据占据的文件region被认定是不一致的,这些数据通常比用户数据小的多。

经过了一系列的成功的修改操作之后,GFS确保被修改的文件region是已定义的,并且包含最后一次修改操作写入的数据。GFS通过以下措施确保上述行为:(a) 对Chunk的所有副本的修改操作顺序一致(3.1章),(b)使用Chunk的版本号来检测副本是否因为它所在的Chunk服务器宕机(4.5章)而错过了修改操作而导致其失效。失效的副本不会再进行任何修改操作,Master服务器也不再返回这个Chunk副本的位置信息给客户端。它们会被垃圾收集系统尽快回收。

由于Chunk位置信息会被客户端缓存,所以在信息刷新前,客户端有可能从一个失效的副本读取了数据。在缓存的超时时间和文件下一次被打开的时间之间存在一个时间窗,文件再次被打开后会清除缓存中与该文件有关的所有Chunk位置信息。而且,由于我们的文件大多数都是只进行追加操作的,所以,一个失效的副本通常返回一个提前结束的Chunk而不是过期的数据。当一个Reader (alex注:本文中将用到两个专有名词,Reader和Writer,分别表示执行GFS读取和写入操作的程序) 重新尝试并联络Master服务器时,它就会立刻得到最新的Chunk位置信息。

即使在修改操作成功执行很长时间之后,组件的失效也可能损坏或者删除数据。GFS通过Master服务器和所有Chunk服务器的定期“握手”来找到失效的Chunk服务器,并且使用Checksum来校验数据是否损坏(5.2章)。一旦发现问题,数据要尽快利用有效的副本进行恢复(4.3章)。只有当一个Chunk的所有副本在GFS检测到错误并采取应对措施之前全部丢失,这个Chunk才会不可逆转的丢失。在一般情况下GFS的反应时间 (alex注:指Master节点检测到错误并采取应对措施) 是几分钟。即使在这种情况下,Chunk也只是不可用了,而不是损坏了:应用程序会收到明确的错误信息而不是损坏的数据。

2.7.2 程序的实现

使用GFS的应用程序可以利用一些简单技术实现这个宽松的一致性模型,这些技术也用来实现一些其它的目标功能,包括:尽量采用追加写入而不是覆盖,Checkpoint,自验证的写入操作,自标识的记录。

在实际应用中,我们所有的应用程序对文件的写入操作都是尽量采用数据追加方式,而不是覆盖方式。一种典型的应用,应用程序从头到尾写入数据,生成了一个文件。写入所有数据之后,应用程序自动将文件改名为一个永久保存的文件名,或者周期性的作Checkpoint,记录成功写入了多少数据。Checkpoint文件可以包含程序级别的校验和。Readers仅校验并处理上个Checkpoint之后产生的文件region,这些文件region的状态一定是已定义的。这个方法满足了我们一致性和并发处理的要求。追加写入比随机位置写入更加有效率,对应用程序的失败处理更具有弹性。Checkpoint可以让Writer以渐进的方式重新开始,并且可以防止Reader处理已经被成功写入,但是从应用程序的角度来看还并未完成的数据。

我们再来分析另一种典型的应用。许多应用程序并行的追加数据到同一个文件,比如进行结果的合并或者是一个生产者-消费者队列。记录追加方式的“至少一次追加”的特性保证了Writer的输出。Readers使用下面的方法来处理偶然性的填充数据和重复内容。Writers在每条写入的记录中都包含了额外的信息,例如Checksum,用来验证它的有效性。Reader可以利用Checksum识别和抛弃额外的填充数据和记录片段。如果应用不能容忍偶尔的重复内容(比如,如果这些重复数据触发了非幂等操作),可以用记录的唯一标识符来过滤它们,这些唯一标识符通常用于命名程序中处理的实体对象,例如web文档。这些记录I/O功能 (alex注:These functionalities for record I/O) (除了剔除重复数据)都包含在我们的程序共享的库中,并且适用于Google内部的其它的文件接口实现。所以,相同序列的记录,加上一些偶尔出现的重复数据,都被分发到Reader了。

3. 系统交互

我们在设计这个系统时,一个重要的原则是最小化所有操作和Master节点的交互。带着这样的设计理念,我们现在描述一下客户机、Master服务器和Chunk服务器如何进行交互,以实现数据修改操作、原子的记录追加操作以及快照功能。

3.1 租约(lease)和变更顺序

(alex注:lease是数据库中的一个术语)

变更是一个会改变Chunk内容或者元数据的操作,比如写入操作或者记录追加操作。变更操作会在Chunk的所有副本上执行。我们使用租约(lease)机制来保持多个副本间变更顺序的一致性。Master节点为Chunk的一个副本建立一个租约,我们把这个副本叫做主Chunk。主Chunk对Chunk的所有更改操作进行序列化。所有的副本都遵从这个序列进行修改操作。因此,修改操作全局的顺序首先由Master节点选择的租约的顺序决定,然后由租约中主Chunk分配的序列号决定。

设计租约机制的目的是为了最小化Master节点的管理负担。租约的初始超时设置为60秒。不过,只要Chunk被修改了,主Chunk就可以申请更长的租期,通常会得到Master节点的确认并收到租约延长的时间。这些租约延长请求和批准的信息通常都是附加在Master节点和Chunk服务器之间的心跳消息中来传递。有时Master节点会试图提前取消租约(例如,Master节点想取消在一个已经被改名的文件上的修改操作)。即使Master节点和主Chunk失去联系,它仍然可以安全地在旧的租约到期后和另外一个Chunk副本签订新的租约。

在图中,依据步骤编号,展现写入操作的控制流程。

zbbzEn.png

  1. 客户机向Master节点询问哪一个Chunk服务器持有当前的租约,以及其它副本的位置。如果没有一个Chunk持有租约,Master节点就选择其中一个副本建立一个租约(这个步骤在图上没有显示)。
  2. Master节点将主Chunk的标识符以及其它副本(又称为secondary副本、二级副本)的位置返回给客户机。客户机缓存这些数据以便后续的操作。只有在主Chunk不可用,或者主Chunk回复信息表明它已不再持有租约的时候,客户机才需要重新跟Master节点联系。
  3. 客户机把数据推送到所有的副本上。客户机可以以任意的顺序推送数据。Chunk服务器接收到数据并保存在它的内部LRU缓存中,一直到数据被使用或者过期交换出去。由于数据流的网络传输负载非常高,通过分离数据流和控制流,我们可以基于网络拓扑情况对数据流进行规划,提高系统性能,而不用去理会哪个Chunk服务器保存了主Chunk。3.2章节会进一步讨论这点。
  4. 当所有的副本都确认接收到了数据,客户机发送写请求到主Chunk服务器。这个请求标识了早前推送到所有副本的数据。主Chunk为接收到的所有操作分配连续的序列号,这些操作可能来自不同的客户机,序列号保证了操作顺序执行。它以序列号的顺序把操作应用到它自己的本地状态中 (alex注:也就是在本地执行这些操作,这句话按字面翻译有点费解,也许应该翻译为“它顺序执行这些操作,并更新自己的状态”)
  5. 主Chunk把写请求传递到所有的二级副本。每个二级副本依照主Chunk分配的序列号以相同的顺序执行这些操作。
  6. 所有的二级副本回复主Chunk,它们已经完成了操作。
  7. 主Chunk服务器 (alex注:即主Chunk所在的Chunk服务器) 回复客户机。任何副本产生的任何错误都会返回给客户机。在出现错误的情况下,写入操作可能在主Chunk和一些二级副本执行成功。(如果操作在主Chunk上失败了,操作就不会被分配序列号,也不会被传递。)客户端的请求被确认为失败,被修改的region处于不一致的状态。我们的客户机代码通过重复执行失败的操作来处理这样的错误。在从头开始重复执行之前,客户机会先从步骤(3)到步骤(7)做几次尝试。

如果应用程序一次写入的数据量很大,或者数据跨越了多个Chunk,GFS客户机代码会把它们分成多个写操作。这些操作都遵循前面描述的控制流程,但是可能会被其它客户机上同时进行的操作打断或者覆盖。因此,共享的文件region的尾部可能包含来自不同客户机的数据片段,尽管如此,由于这些分解后的写入操作在所有的副本上都以相同的顺序执行完成,Chunk的所有副本都是一致的。这使文件region处于2.7节描述的一致的、但是未定义的状态。

3.2 数据流

为了提高网络效率,我们采取了把数据流和控制流分开的措施。在控制流从客户机到主Chunk、然后再到所有二级副本的同时,数据以管道的方式,顺序的沿着一个精心选择的Chunk服务器链推送。我们的目标是充分利用每台机器的带宽,避免网络瓶颈和高延时的连接,最小化推送所有数据的延时。

为了充分利用每台机器的带宽,数据沿着一个Chunk服务器链顺序的推送,而不是以其它拓扑形式分散推送(例如,树型拓扑结构)。线性推送模式下,每台机器所有的出口带宽都用于以最快的速度传输数据,而不是在多个接受者之间分配带宽。

为了尽可能的避免出现网络瓶颈和高延迟的链接(eg,inter-switch最有可能出现类似问题),每台机器都尽量的在网络拓扑中选择一台还没有接收到数据的、离自己最近的机器作为目标推送数据。假设客户机把数据从Chunk服务器S1推送到S4。它把数据推送到最近的Chunk服务器S1。S1把数据推送到S2,因为S2和S4中最接近的机器是S2。同样的,S2把数据传递给S3和S4之间更近的机器,依次类推推送下去。我们的网络拓扑非常简单,通过IP地址就可以计算出节点的“距离”。

最后,我们利用基于TCP连接的、管道式数据推送方式来最小化延迟。Chunk服务器接收到数据后,马上开始向前推送。管道方式的数据推送对我们帮助很大,因为我们采用全双工的交换网络。接收到数据后立刻向前推送不会降低接收的速度。在没有网络拥塞的情况下,传送B字节的数据到R个副本的理想时间是 B/T+RL ,T是网络的吞吐量,L是在两台机器数据传输的延迟。通常情况下,我们的网络连接速度是100Mbps(T),L将远小于1ms。因此,1MB的数据在理想情况下80ms左右就能分发出去。

3.3 原子的记录追加

GFS提供了一种原子的数据追加操作–记录追加。传统方式的写入操作,客户程序会指定数据写入的偏移量。对同一个region的并行写入操作不是串行的:region尾部可能会包含多个不同客户机写入的数据片段。使用记录追加,客户机只需要指定要写入的数据。GFS保证至少有一次原子的写入操作成功执行(即写入一个顺序的byte流),写入的数据追加到GFS指定的偏移位置上,之后GFS返回这个偏移量给客户机。这类似于在Unix操作系统编程环境中,对以O_APPEND模式打开的文件,多个并发写操作在没有竞态条件时的行为。

记录追加在我们的分布应用中非常频繁的使用,在这些分布式应用中,通常有很多的客户机并行地对同一个文件追加写入数据。如果我们采用传统方式的文件写入操作,客户机需要额外的复杂、昂贵的同步机制,例如使用一个分布式的锁管理器。在我们的工作中,这样的文件通常用于多个生产者/单一消费者的队列系统,或者是合并了来自多个客户机的数据的结果文件。

记录追加是一种修改操作,它也遵循3.1节描述的控制流程,除了在主Chunk有些额外的控制逻辑。客户机把数据推送给文件最后一个Chunk的所有副本,之后发送请求给主Chunk。主Chunk会检查这次记录追加操作是否会使Chunk超过最大尺寸(64MB)。如果超过了最大尺寸,主Chunk首先将当前Chunk填充到最大尺寸,之后通知所有二级副本做同样的操作,然后回复客户机要求其对下一个Chunk重新进行记录追加操作。(记录追加的数据大小严格控制在Chunk最大尺寸的1/4,这样即使在最坏情况下,数据碎片的数量仍然在可控的范围。)通常情况下追加的记录不超过Chunk的最大尺寸,主Chunk把数据追加到自己的副本内,然后通知二级副本把数据写在跟主Chunk一样的位置上,最后回复客户机操作成功。

如果记录追加操作在任何一个副本上失败了,客户端就需要重新进行操作。重新进行记录追加的结果是,同一个Chunk的不同副本可能包含不同的数据–重复包含一个记录全部或者部分的数据。GFS并不保证Chunk的所有副本在字节级别是完全一致的。它只保证数据作为一个整体原子的被至少写入一次。这个特性可以通过简单观察推导出来:如果操作成功执行,数据一定已经写入到Chunk的所有副本的相同偏移位置上。这之后,所有的副本至少都到了记录尾部的长度,任何后续的记录都会追加到更大的偏移地址,或者是不同的Chunk上,即使其它的Chunk副本被Master节点选为了主Chunk。就我们的一致性保障模型而言,记录追加操作成功写入数据的region是已定义的(因此也是一致的),反之则是不一致的(因此也就是未定义的)。正如我们在2.7.2节讨论的,我们的程序可以处理不一致的区域。

3.4 快照

(alex注:这一节非常难以理解,总的来说依次讲述了什么是快照、快照使用的COW技术、快照如何不干扰当前操作)

快照操作几乎可以瞬间完成对一个文件或者目录树(“源”)做一个拷贝,并且几乎不会对正在进行的其它操作造成任何干扰。我们的用户可以使用快照迅速的创建一个巨大的数据集的分支拷贝(而且经常是递归的拷贝拷贝),或者是在做实验性的数据操作之前,使用快照操作备份当前状态,这样之后就可以轻松的提交或者回滚到备份时的状态。

就像AFS (alex注:AFS,即Andrew File System,一种分布式文件系统), 我们用标准的copy-on-write技术实现快照。当Master节点收到一个快照请求,它首先取消作快照的文件的所有Chunk的租约。这个措施保证了后续对这些Chunk的写操作都必须与Master交互交互以找到租约持有者。这就给Master节点一个率先创建Chunk的新拷贝的机会。

租约取消或者过期之后,Master节点把这个操作以日志的方式记录到硬盘上。然后,Master节点通过复制源文件或者目录的元数据的方式,把这条日志记录的变化反映到保存在内存的状态中。新创建的快照文件和源文件指向完全相同的Chunk地址。

在快照操作之后,当客户机第一次想写入数据到Chunk C,它首先会发送一个请求到Master节点查询当前的租约持有者。Master节点注意到Chunke C的引用计数超过了1 (alex注:不太明白为什么会大于1.难道是Snapshot没有释放引用计数?) 。Master节点不会马上回复客户机的请求,而是选择一个新的Chunk句柄C 。之后,Master节点要求每个拥有Chunk C当前副本的Chunk服务器创建一个叫做C的新Chunk。通过在源Chunk所在Chunk服务器上创建新的Chunk,我们确保数据在本地而不是通过网络复制(我们的硬盘比我们的100Mb以太网大约快3倍)。从这点来讲,请求的处理方式和任何其它Chunk没什么不同:Master节点确保新Chunk C`的一个副本拥有租约,之后回复客户机,客户机得到回复后就可以正常的写这个Chunk,而不必理会它是从一个已存在的Chunk克隆出来的。

4. Master节点的操作

Master节点执行所有的名称空间操作。此外,它还管理着整个系统里所有Chunk的副本:它决定Chunk的存储位置,创建新Chunk和它的副本,协调各种各样的系统活动以保证Chunk被完全复制,在所有的Chunk服务器之间的进行负载均衡,回收不再使用的存储空间。

4.1 名称空间管理和锁

Master节点的很多操作会花费很长的时间:比如,快照操作必须取消Chunk服务器上快照所涉及的所有的Chunk的租约。我们不希望在这些操作的运行时,延缓了其它的Master节点的操作。因此,我们允许多个操作同时进行,使用名称空间的region上的锁来保证执行的正确顺序。

不同于许多传统文件系统,GFS没有针对每个目录实现能够列出目录下所有文件的数据结构。GFS也不支持文件或者目录的链接(即Unix术语中的硬链接或者符号链接)。在逻辑上,GFS的名称空间就是一个全路径和元数据映射关系的查找表。利用前缀压缩,这个表可以高效的存储在内存中。在存储名称空间的树型结构上,每个节点(绝对路径的文件名或绝对路径的目录名)都有一个关联的读写锁。

每个Master节点的操作在开始之前都要获得一系列的锁。通常情况下,如果一个操作涉及/d1/d2/…/dn/leaf,那么操作首先要获得目录/d1,/d1/d2,…,/d1/d2/…/dn的读锁,以及/d1/d2/…/dn/leaf的读写锁。注意,根据操作的不同,leaf可以是一个文件,也可以是一个目录。

我们演示一下在/home/user被快照到/save/user的时候,锁机制如何防止创建文件/home/user/foo。快照操作获取/home和/save的读取锁,以及/home/user和/save/user的写入锁。文件创建操作获得/home和/home/user的读取锁,以及/home/user/foo的写入锁。这两个操作要顺序执行,因为它们试图获取的/home/user的锁是相互冲突。文件创建操作不需要获取父目录的写入锁,因为这里没有”目录”,或者类似inode等用来禁止修改的数据结构。文件名的读取锁足以防止父目录被删除。

采用这种锁方案的优点是支持对同一目录的并行操作。比如,可以在同一个目录下同时创建多个文件:每一个操作都获取一个目录名的上的读取锁和文件名上的写入锁。目录名的读取锁足以的防止目录被删除、改名以及被快照。文件名的写入锁序列化文件创建操作,确保不会多次创建同名的文件。

因为名称空间可能有很多节点,读写锁采用惰性分配策略,在不再使用的时候立刻被删除。同样,锁的获取也要依据一个全局一致的顺序来避免死锁:首先按名称空间的层次排序,在同一个层次内按字典顺序排序。

4.2 副本的位置

GFS集群是高度分布的多层布局结构,而不是平面结构。典型的拓扑结构是有数百个Chunk服务器安装在许多机架上。Chunk服务器被来自同一或者不同机架上的数百个客户机轮流访问。不同机架上的两台机器间的通讯可能跨越一个或多个网络交换机。另外,机架的出入带宽可能比机架内所有机器加和在一起的带宽要小。多层分布架构对数据的灵活性、可靠性以及可用性方面提出特有的挑战。

Chunk副本位置选择的策略服务两大目标:最大化数据可靠性和可用性,最大化网络带宽利用率。为了实现这两个目的,仅仅是在多台机器上分别存储这些副本是不够的,这只能预防硬盘损坏或者机器失效带来的影响,以及最大化每台机器的网络带宽利用率。我们必须在多个机架间分布储存Chunk的副本。这保证Chunk的一些副本在整个机架被破坏或掉线(比如,共享资源,如电源或者网络交换机造成的问题)的情况下依然存在且保持可用状态。这还意味着在网络流量方面,尤其是针对Chunk的读操作,能够有效利用多个机架的整合带宽。另一方面,写操作必须和多个机架上的设备进行网络通信,但是这个代价是我们愿意付出的。

4.3 创建,重新复制,重新负载均衡

Chunk的副本有三个用途:Chunk创建,重新复制和重新负载均衡。

当Master节点创建一个Chunk时,它会选择在哪里放置初始的空的副本。Master节点会考虑几个因素。(1)我们希望在低于平均硬盘使用率的Chunk服务器上存储新的副本。这样的做法最终能够平衡Chunk服务器之间的硬盘使用率。(2)我们希望限制在每个Chunk服务器上”最近”的Chunk创建操作的次数。虽然创建操作本身是廉价的,但是创建操作也意味着随之会有大量的写入数据的操作,因为Chunk在Writer真正写入数据的时候才被创建,而在我们的”追加一次,读取多次”的工作模式下,Chunk一旦写入成功之后就会变为只读的了。(3)如上所述,我们希望把Chunk的副本分布在多个机架之间。

当Chunk的有效副本数量少于用户指定的复制因数的时候,Master节点会重新复制它。这可能是由几个原因引起的:一个Chunk服务器不可用了,Chunk服务器报告它所存储的一个副本损坏了,Chunk服务器的一个磁盘因为错误不可用了,或者Chunk副本的复制因数提高了。每个需要被重新复制的Chunk都会根据几个因素进行排序。一个因素是Chunk现有副本数量和复制因数相差多少。例如,丢失两个副本的Chunk比丢失一个副本的Chunk有更高的优先级。另外,我们优先重新复制活跃(live)文件的Chunk而不是最近刚被删除的文件的Chunk(查看4.4节)。最后,为了最小化失效的Chunk对正在运行的应用程序的影响,我们提高会阻塞客户机程序处理流程的Chunk的优先级。

Master节点选择优先级最高的Chunk,然后命令某个Chunk服务器直接从可用的副本”克隆”一个副本出来。选择新副本的位置的策略和创建时类似:平衡硬盘使用率、限制同一台Chunk服务器上的正在进行的克隆操作的数量、在机架间分布副本。为了防止克隆产生的网络流量大大超过客户机的流量,Master节点对整个集群和每个Chunk服务器上的同时进行的克隆操作的数量都进行了限制。另外,Chunk服务器通过调节它对源Chunk服务器读请求的频率来限制它用于克隆操作的带宽。

最后,Master服务器周期性地对副本进行重新负载均衡:它检查当前的副本分布情况,然后移动副本以便更好的利用硬盘空间、更有效的进行负载均衡。而且在这个过程中,Master服务器逐渐的填满一个新的Chunk服务器,而不是在短时间内用新的Chunk填满它,以至于过载。新副本的存储位置选择策略和上面讨论的相同。另外,Master节点必须选择哪个副本要被移走。通常情况,Master节点移走那些剩余空间低于平均值的Chunk服务器上的副本,从而平衡系统整体的硬盘使用率。

4.4 垃圾回收

GFS在文件删除后不会立刻回收可用的物理空间。GFS空间回收采用惰性的策略,只在文件和Chunk级的常规垃圾收集时进行。我们发现这个方法使系统更简单、更可靠。

4.4.1 机制

当一个文件被应用程序删除时,Master节点像对待其它修改操作一样,立刻把删除操作以日志的方式记录下来。但是,Master节点并不马上回收资源,而是把文件名改为一个包含删除时间戳的、隐藏的名字。当Master节点对文件系统命名空间做常规扫描的时候,它会删除所有三天前的隐藏文件(这个时间间隔是可以设置的)。直到文件被真正删除,它们仍旧可以用新的特殊的名字读取,也可以通过把隐藏文件改名为正常显示的文件名的方式“反删除”。当隐藏文件被从名称空间中删除,Master服务器内存中保存的这个文件的相关元数据才会被删除。这也有效的切断了文件和它包含的所有Chunk的连接 (alex注:原文是This effectively severs its links to all its chunks)

在对Chunk名字空间做类似的常规扫描时,Master节点找到孤儿Chunk(不被任何文件包含的Chunk)并删除它们的元数据。Chunk服务器在和Master节点交互的心跳信息中,报告它拥有的Chunk子集的信息,Master节点回复Chunk服务器哪些Chunk在Master节点保存的元数据中已经不存在了。Chunk服务器可以任意删除这些Chunk的副本。

4.4.2 讨论

虽然分布式垃圾回收在编程语言领域是一个需要复杂的方案才能解决的难题,但是在GFS系统中是非常简单的。我们可以轻易的得到Chunk的所有引用:它们都只存储在Master服务器上的文件到块的映射表中。我们也可以很轻易的得到所有Chunk的副本:它们都以Linux文件的形式存储在Chunk服务器的指定目录下。所有Master节点不能识别的副本都是”垃圾”。

垃圾回收在空间回收方面相比直接删除有几个优势。首先,对于组件失效是常态的大规模分布式系统,垃圾回收方式简单可靠。Chunk可能在某些Chunk服务器创建成功,某些Chunk服务器上创建失败,失败的副本处于无法被Master节点识别的状态。副本删除消息可能丢失,Master节点必须重新发送失败的删除消息,包括自身的和Chunk服务器的 (alex注:自身的指删除metadata的消息) 。垃圾回收提供了一致的、可靠的清除无用副本的方法。第二,垃圾回收把存储空间的回收操作合并到Master节点规律性的后台活动中,比如,例行扫描和与Chunk服务器握手等。因此,操作被批量的执行,开销会被分散。另外,垃圾回收在Master节点相对空闲的时候完成。这样Master节点就可以给那些需要快速反应的客户机请求提供更快捷的响应。第三,延缓存储空间回收为意外的、不可逆转的删除操作提供了安全保障。

根据我们的使用经验,延迟回收空间的主要问题是,延迟回收会阻碍用户调优存储空间的使用,特别是当存储空间比较紧缺的时候。当应用程序重复创建和删除临时文件时,释放的存储空间不能马上重用。我们通过显式的再次删除一个已经被删除的文件的方式加速空间回收的速度。我们允许用户为命名空间的不同部分设定不同的复制和回收策略。例如,用户可以指定某些目录树下面的文件不做复制,删除的文件被即时的、不可恢复的从文件系统移除。

4.5 过期失效的副本检测

当Chunk服务器失效时,Chunk的副本有可能因错失了一些修改操作而过期失效。Master节点保存了每个Chunk的版本号,用来区分当前的副本和过期副本。

无论何时,只要Master节点和Chunk签订一个新的租约,它就增加Chunk的版本号,然后通知最新的副本。Master节点和这些副本都把新的版本号记录在它们持久化存储的状态信息中。这个动作发生在任何客户机得到通知以前,因此也是对这个Chunk开始写之前。如果某个副本所在的Chunk服务器正好处于失效状态,那么副本的版本号就不会被增加。Master节点在这个Chunk服务器重新启动,并且向Master节点报告它拥有的Chunk的集合以及相应的版本号的时候,就会检测出它包含过期的Chunk。如果Master节点看到一个比它记录的版本号更高的版本号,Master节点会认为它和Chunk服务器签订租约的操作失败了,因此会选择更高的版本号作为当前的版本号。

Master节点在例行的垃圾回收过程中移除所有的过期失效副本。在此之前,Master节点在回复客户机的Chunk信息请求的时候,简单的认为那些过期的块根本就不存在。另外一重保障措施是,Master节点在通知客户机哪个Chunk服务器持有租约、或者指示Chunk服务器从哪个Chunk服务器进行克隆时,消息中都附带了Chunk的版本号。客户机或者Chunk服务器在执行操作时都会验证版本号以确保总是访问当前版本的数据。

5. 容错和诊断

我们在设计GFS时遇到的最大挑战之一是如何处理频繁发生的组件失效。组件的数量和质量让这些问题出现的频率远远超过一般系统意外发生的频率:我们不能完全依赖机器的稳定性,也不能完全相信硬盘的可靠性。组件的失效可能造成系统不可用,更糟糕的是,还可能产生不完整的数据。我们讨论我们如何面对这些挑战,以及当组件失效不可避免的发生时,用GFS自带工具诊断系统故障。

5.1 高可用性

在GFS集群的数百个服务器之中,在任何给定的时间必定会有些服务器是不可用的。我们使用两条简单但是有效的策略保证整个系统的高可用性:快速恢复和复制。

5.1.1 快速恢复

不管Master服务器和Chunk服务器是如何关闭的,它们都被设计为可以在数秒钟内恢复它们的状态并重新启动。事实上,我们并不区分正常关闭和异常关闭;通常,我们通过直接kill掉进程来关闭服务器。客户机和其它的服务器会感觉到系统有点颠簸 (alex注:a minor hiccup) ,正在发出的请求会超时,需要重新连接到重启后的服务器,然后重试这个请求。

5.1.2 Chunk复制

正如之前讨论的,每个Chunk都被复制到不同机架上的不同的Chunk服务器上。用户可以为文件命名空间的不同部分设定不同的复制级别。缺省是3。当有Chunk服务器离线了,或者通过Chksum校验(参考5.2节)发现了已经损坏的数据,Master节点通过克隆已有的副本保证每个Chunk都被完整复制 (alex注:即每个Chunk都有复制因子制定的个数个副本,缺省是3)。 虽然Chunk复制策略对我们非常有效,但是我们也在寻找其它形式的跨服务器的冗余解决方案,比如使用奇偶校验、或者Erasure codes (alex注:Erasure codes用来解决链接层中不相关的错误,以及网络拥塞和buffer限制造成的丢包错误) 来解决我们日益增长的只读存储需求。我们的系统主要的工作负载是追加方式的写入和读取操作,很少有随机的写入操作,因此,我们认为在我们这个高度解耦合的系统架构下实现这些复杂的冗余方案很有挑战性,但并非不可实现。

5.1.3 Master服务器的复制

为了保证Master服务器的可靠性,Master服务器的状态也要复制。Master服务器所有的操作日志和checkpoint文件都被复制到多台机器上。对Master服务器状态的修改操作能够提交成功的前提是,操作日志写入到Master服务器的备节点和本机的磁盘。简单说来,一个Master服务进程负责所有的修改操作,包括后台的服务,比如垃圾回收等改变系统内部状态活动。当它失效的时候,几乎可以立刻重新启动。如果Master进程所在的机器或者磁盘失效了,处于GFS系统外部的监控进程会在其它的存有完整操作日志的机器上启动一个新的Master进程。客户端使用规范的名字访问Master(比如gfs-test)节点,这个名字类似DNS别名,因此也就可以在Master进程转到别的机器上执行时,通过更改别名的实际指向访问新的Master节点。

此外,GFS中还有些“影子”Master服务器,这些“影子”服务器在“主”Master服务器宕机的时候提供文件系统的只读访问。它们是影子,而不是镜像,所以它们的数据可能比“主”Master服务器更新要慢,通常是不到1秒。对于那些不经常改变的文件、或者那些允许获取的数据有少量过期的应用程序,“影子”Master服务器能够提高读取的效率。事实上,因为文件内容是从Chunk服务器上读取的,因此,应用程序不会发现过期的文件内容。在这个短暂的时间窗内,过期的可能是文件的元数据,比如目录的内容或者访问控制信息。

“影子”Master服务器为了保持自身状态是最新的,它会读取一份当前正在进行的操作的日志副本,并且依照和主Master服务器完全相同的顺序来更改内部的数据结构。和主Master服务器一样,“影子”Master服务器在启动的时候也会从Chunk服务器轮询数据(之后定期拉数据),数据中包括了Chunk副本的位置信息;“影子”Master服务器也会定期和Chunk服务器“握手”来确定它们的状态。在主Master服务器因创建和删除副本导致副本位置信息更新时,“影子”Master服务器才和主Master服务器通信来更新自身状态。

5.2 数据完整性

每个Chunk服务器都使用Checksum来检查保存的数据是否损坏。考虑到一个GFS集群通常都有好几百台机器、几千块硬盘,磁盘损坏导致数据在读写过程中损坏或者丢失是非常常见的(第7节讲了一个原因)。我们可以通过别的Chunk副本来解决数据损坏问题,但是跨越Chunk服务器比较副本来检查数据是否损坏很不实际。另外,GFS允许有歧义的副本存在:GFS修改操作的语义,特别是早先讨论过的原子纪录追加的操作,并不保证副本完全相同 (alex注:副本不是byte-wise完全一致的) 。因此,每个Chunk服务器必须独立维护Checksum来校验自己的副本的完整性。

我们把每个Chunk都分成64KB大小的块。每个块都对应一个32位的Checksum。和其它元数据一样,Checksum与其它的用户数据是分开的,并且保存在内存和硬盘上,同时也记录操作日志。

对于读操作来说,在把数据返回给客户端或者其它的Chunk服务器之前,Chunk服务器会校验读取操作涉及的范围内的块的Checksum。因此Chunk服务器不会把错误数据传递到其它的机器上。如果发生某个块的Checksum不正确,Chunk服务器返回给请求者一个错误信息,并且通知Master服务器这个错误。作为回应,请求者应当从其它副本读取数据,Master服务器也会从其它副本克隆数据进行恢复。当一个新的副本就绪后,Master服务器通知副本错误的Chunk服务器删掉错误的副本。

Checksum对读操作的性能影响很小,可以基于几个原因来分析一下。因为大部分的读操作都至少要读取几个块,而我们只需要读取一小部分额外的相关数据进行校验。GFS客户端代码通过每次把读取操作都对齐在Checksum block的边界上,进一步减少了这些额外的读取操作的负面影响。另外,在Chunk服务器上,Chunksum的查找和比较不需要I/O操作,Checksum的计算可以和I/O操作同时进行。

Checksum的计算针对在Chunk尾部的追加写入操作作了高度优化(与之对应的是覆盖现有数据的写入操作),因为这类操作在我们的工作中占了很大比例。我们只增量更新最后一个不完整的块的Checksum,并且用所有的追加来的新Checksum块来计算新的Checksum。即使是最后一个不完整的Checksum块已经损坏了,而且我们不能够马上检查出来,由于新的Checksum和已有数据不吻合,在下次对这个块进行读取操作的时候,会检查出数据已经损坏了。

相比之下,如果写操作覆盖已经存在的一个范围内的Chunk,我们必须读取和校验被覆盖的第一个和最后一个块,然后再执行写操作;操作完成之后再重新计算和写入新的Checksum。如果我们不校验第一个和最后一个被写的块,那么新的Checksum可能会隐藏没有被覆盖区域内的数据错误。

在Chunk服务器空闲的时候,它会扫描和校验每个不活动的Chunk的内容。这使得我们能够发现很少被读取的Chunk是否完整。一旦发现有Chunk的数据损坏,Master可以创建一个新的、正确的副本,然后把损坏的副本删除掉。这个机制也避免了非活动的、已损坏的Chunk欺骗Master节点,使Master节点认为它们已经有了足够多的副本了。

5.3 诊断工具

详尽的、深入细节的诊断日志,在问题隔离、调试、以及性能分析等方面给我们带来无法估量的帮助,同时也只需要很小的开销。没有日志的帮助,我们很难理解短暂的、不重复的机器之间的消息交互。GFS的服务器会产生大量的日志,记录了大量关键的事件(比如,Chunk服务器启动和关闭)以及所有的RPC的请求和回复。这些诊断日志可以随意删除,对系统的正确运行不造成任何影响。然而,我们在存储空间允许的情况下会尽量的保存这些日志。

RPC日志包含了网络上发生的所有请求和响应的详细记录,但是不包括读写的文件数据。通过匹配请求与回应,以及收集不同机器上的RPC日志记录,我们可以重演所有的消息交互来诊断问题。日志还用来跟踪负载测试和性能分析。

日志对性能的影响很小(远小于它带来的好处),因为这些日志的写入方式是顺序的、异步的。最近发生的事件日志保存在内存中,可用于持续不断的在线监控。

7. 经验

在建造和部署GFS的过程中,我们经历了各种各样的问题,有些是操作上的,有些是技术上的。

起初,GFS被设想为我们的生产系统的后端文件系统。随着时间推移,在GFS的使用中逐步的增加了对研究和开发任务的支持。我们开始增加一些小的功能,比如权限和配额,到了现在,GFS已经初步支持了这些功能。虽然我们生产系统是严格受控的,但是用户层却不总是这样的。需要更多的基础架构来防止用户间的相互干扰。

我们最大的问题是磁盘以及和Linux相关的问题。很多磁盘都声称它们支持某个范围内的Linux IDE硬盘驱动程序,但是实际应用中反映出来的情况却不是这样,它们只支持最新的驱动。因为协议版本很接近,所以大部分磁盘都可以用,但是偶尔也会有由于协议不匹配,导致驱动和内核对于驱动器的状态判断失误。这会导致数据因为内核中的问题意外的被破坏了。这个问题促使我们使用Checksum来校验数据,同时我们也修改内核来处理这些因为协议不匹配带来的问题。

较早的时候,我们在使用Linux 2.2内核时遇到了些问题,主要是fsync()的效率问题。它的效率与文件的大小而不是文件修改部分的大小有关。这在我们的操作日志文件过大时给出了难题,尤其是在我们尚未实现Checkpoint的时候。我们费了很大的力气用同步写来解决这个问题,但是最后还是移植到了Linux2.4内核上。

另一个和Linux相关的问题是单个读写锁的问题,也就是说,在某一个地址空间的任意一个线程都必须在从磁盘page in(读锁)的时候先hold住,或者在mmap()调用(写锁)的时候改写地址空间。我们发现即使我们的系统负载很轻的情况下也会有偶尔的超时,我们花费了很多的精力去查找资源的瓶颈或者硬件的问题。最后我们终于发现这个单个锁在磁盘线程交换以前映射的数据到磁盘的时候,锁住了当前的网络线程,阻止它把新数据映射到内存。由于我们的性能主要受限于网络接口,而不是内存copy的带宽,因此,我们用pread()替代mmap(),用了一个额外的copy动作来解决这个问题。

尽管偶尔还是有其它的问题,Linux的开放源代码还是使我们能够快速探究和理解系统的行为。在适当的时候,我们会改进内核并且和公开源码组织共享这些改动。

9. 结束语

Google文件系统展示了一个使用普通硬件支持大规模数据处理的系统的特质。虽然一些设计要点都是针对我们的特殊的需要定制的,但是还是有很多特性适用于类似规模的和成本的数据处理任务。

首先,我们根据我们当前的和可预期的将来的应用规模和技术环境来评估传统的文件系统的特性。我们的评估结果将我们引导到一个使用完全不同于传统的设计思路上。根据我们的设计思路,我们认为组件失效是常态而不是异常,针对采用追加方式(有可能是并发追加)写入、然后再读取(通常序列化读取)的大文件进行优化,以及扩展标准文件系统接口、放松接口限制来改进整个系统。

我们系统通过持续监控,复制关键数据,快速和自动恢复提供灾难冗余。Chunk复制使得我们可以对Chunk服务器的失效进行容错。高频率的组件失效要求系统具备在线修复机制,能够周期性的、透明的修复损坏的数据,也能够第一时间重新建立丢失的副本。此外,我们使用Checksum在磁盘或者IDE子系统级别检测数据损坏,在这样磁盘数量惊人的大系统中,损坏率是相当高的。

我们的设计保证了在有大量的并发读写操作时能够提供很高的合计吞吐量。我们通过分离控制流和数据流来实现这个目标,控制流在Master服务器处理,而数据流在Chunk服务器和客户端处理。当一般的操作涉及到Master服务器时,由于GFS选择的Chunk尺寸较大 (alex注:从而减小了元数据的大小), 以及通过Chunk Lease将控制权限移交给主副本,这些措施将Master服务器的负担降到最低。这使得一个简单、中心的Master不会成为成为瓶颈。我们相信我们对网络协议栈的优化可以提升当前对于每客户端的写入吞吐量限制。

GFS成功的实现了我们对存储的需求,在Google内部,无论是作为研究和开发的存储平台,还是作为生产系统的数据处理平台,都得到了广泛的应用。它是我们持续创新和处理整个WEB范围内的难题的一个重要工具。

LEC 3

存储系统

存储系统是容错系统的基础构件

如果可以建立一个持久的存储系统,应用程序不需要特殊对自己的状态进行保存,因为存储系统已经存好了,从而简化了应用程序的设计。

因此存储系统本身必须有很高的容错性能,设计这个并不容易。

  • 高性能:需要跨服务器对数据分片
  • 多服务器:出错概率非常大
  • 容错机制:复制数据到其他的机器上
  • 多份数据:带来数据的不一致性
  • 强一致性:持久性协议,写入存储系统会降低性能

因此形成了一个环,主要矛盾是一致性和性能之间的矛盾

一致性

理想情况下的一致性:分布式系统与单机系统在表现上完全相同

然而在实际情况下很难实现

  1. 并发问题:

两个线程为同一个变量写入了不同的值,此时有两个线程读取。

此时读取的值应该是中的任意一个,而的值应该与相同,才是我们希望看到的结果。

  1. 故障问题

解决故障一般是通过使用复制数据到其他机器上的方式。

一个很烂的服务器之间复制数据的方案:客户端写入数据的时候,同时向两个服务器写入数据,不需要服务器之间同步。

此时两个线程为同一个变量写入了不同的值,两个线程读取不一定读出什么。

GFS

相当于一个分布式系统的案例研究,包括了高性能、复制和容错、一致性等等主题

GFS是第一个在上千台计算机上构建的分布式系统,后续的HDFS等都受到了GFS的启发。

两个非标准做法:

  • 单一的master节点
  • 存在不一致的地方

关键属性

  • 大数据集:可能是从整个互联网上爬取的数据集
  • 快速:自动分片到多个磁盘
  • 全局共享:所有应用程序都看到的是相同的文件系统
  • 容错:自动容错性(或者容错能力很强)

设计

zbm43T.md.png

Master

  • 文件名到chunk的对应关系(存在日志中-持久存储)
  • 对每一个chunk有一个版本号(持久存储,恢复时候才能找到正确的版本)和服务器列表
  • 日志(首先的操作,建立持久存储)+checkpoints(持久存储)

读取文件

  1. 客户端发送消息
  2. master通过消息返回chunk信息等
  3. 客户端缓存信息(在一段时间内不需要再次与master进行通信)
  4. 客户端从信息中的最近的服务器中读取文件
  5. 最近的服务器检查版本号,无误后发送数据

写文件(追加操作)

zbbzEn.png

  1. 客户端发送消息
  2. master增加版本号,选取primary服务器,服务器增加版本号(持久存储),发送租约,返回消息给客户端
  3. 客户端发送写数据的请求给最近的服务器,然后服务器之间通过网络传递数据
  4. 客户端给primary服务器端发送写数据的消息
  5. primary服务器检查版本号和租约,无误后写入数据,发送写数据的信息给其他的服务器
  6. 其他服务器反馈写成功的消息给primary服务器
  7. 服务器反馈成功消息给客户端

如果中间过程有错误,客户端一般会重试,希望下一次可以正常运行(也就是最少一次)

这可能会造成在一个磁盘中有两份数据的拷贝。会有id和checksum协助控制不会将相同的数据读取两次。

一致性

一个服务器暂时挂掉了,导致版本号没有更新,同时一个客户端的版本号也是一个老版本号,结果正好匹配到了这个刚刚挂掉的服务器,最终导致读取的数据和期望的不同。

通过租约机制确保只会存在一个primary服务器,不会产生“脑裂”现象

获得强一致性?更新所有的除primary外的其他服务器或者全部都不更新,GFS没有实现这个。

]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + MIT-6.824 Distributed Systems-LEC 2 RPC and Threads + + /2022/12/15/6.824/Distributed-Systems-MIT-6.824-LEC-2/ + + MIT-6.824(Spring 2022)LEC 2 RPC and Threads

Go快速入门

How do Go channels work? How does Go make sure they are synchronized between the many possible goroutines?

https://golang.org/src/runtime/chan.go

At a high level, a chan is a struct holding a buffer and a lock. Sending on a channel involves acquiring the lock, waiting (perhaps releasing the CPU) until some thread is receiving, and handing off the message. Receiving involves acquiring the lock and waiting for a sender. You could implement your own channels with Go sync.Mutex and sync.Cond.

LEC 2

为什么使用Go?

  • 对线程和RPC有很好的支持(更适合分布式编程)
  • 垃圾收集器,不需要用户自己释放内存
  • 简单易学
  • 自带编译器,不是 Python 那样的解释型语言

线程

在一个进程中并行运行多个线程

线程原语:开启线程、退出线程(隐式)、停止线程(挂在一边不懂)、恢复线程

为什么需要线程?

支持并发

  • 输入/输出并发
  • 多核并行
  • 方便(例如定期执行后台活动等)

数量可以不考虑,按照需求创建线程即可

线程编程挑战

  • 竞争情况(同时对某一个变量进行写操作)
    • 可能大多数情况运行都很好,但是确实在某些条件下得不到想要的结果
    • 解决的两种方法
      • 避免共享变量(channels)go推荐使用
      • 使用锁(mutex)
  • 协调问题:一个线程必须等待另一个线程完成后才能继续进行
    • channels
    • condition variables
  • 死锁问题:两边都在等待对方

Go应对挑战的机制

channels和condition variables

  • 如果不共享内存,只想让线程互相进行通信,则应该使用channels
  • 如果需要共享内存,应该使用锁和condition variables

条件变量和channel实例

分配条件变量并且和锁关联,不满足条件进入睡眠状态,并释放关联的锁。

在goroutine运行的最后唤醒睡眠状态的线程,重新进行判断

package mainimport "sync"import "time"import "math/rand"func main() {rand.Seed(time.Now().UnixNano())count := 0finished := 0var mu sync.Mutexcond := sync.NewCond(&mu)for i := 0; i < 10; i++ {go func() {vote := requestVote()mu.Lock()defer mu.Unlock()if vote {count++}finished++cond.Broadcast()}()}mu.Lock()for count < 5 && finished != 10 {cond.Wait()}if count >= 5 {println("received 5+ votes!")} else {println("lost")}mu.Unlock()}func requestVote() bool {time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)return rand.Int() % 2 == 0}
package mainimport "time"import "math/rand"func main() {rand.Seed(time.Now().UnixNano())count := 0ch := make(chan bool)for i := 0; i < 10; i++ {go func() {ch <- requestVote()}()}for i := 0; i < 10; i++ {v := <-chif v {count += 1}}if count >= 5 {println("received 5+ votes!")} else {println("lost")}}func requestVote() bool {time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)return rand.Int()%2 == 0}

go tour 爬虫练习

package mainimport ("fmt""sync")//// Several solutions to the crawler exercise from the Go tutorial// https://tour.golang.org/concurrency/10////// Serial crawler//func Serial(url string, fetcher Fetcher, fetched map[string]bool) {if fetched[url] {return}fetched[url] = trueurls, err := fetcher.Fetch(url)if err != nil {return}for _, u := range urls {Serial(u, fetcher, fetched)}return}//// Concurrent crawler with shared state and Mutex//type fetchState struct {mu      sync.Mutexfetched map[string]bool}func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {f.mu.Lock()already := f.fetched[url]f.fetched[url] = truef.mu.Unlock()if already {return}urls, err := fetcher.Fetch(url)if err != nil {return}var done sync.WaitGroupfor _, u := range urls {done.Add(1)go func(u string) {defer done.Done()ConcurrentMutex(u, fetcher, f)}(u)}done.Wait()return}func makeState() *fetchState {f := &fetchState{}f.fetched = make(map[string]bool)return f}//// Concurrent crawler with channels//func worker(url string, ch chan []string, fetcher Fetcher) {urls, err := fetcher.Fetch(url)if err != nil {ch <- []string{}} else {ch <- urls}}func coordinator(ch chan []string, fetcher Fetcher) {n := 1fetched := make(map[string]bool)for urls := range ch {for _, u := range urls {if fetched[u] == false {fetched[u] = truen += 1go worker(u, ch, fetcher)}}n -= 1if n == 0 {break}}}func ConcurrentChannel(url string, fetcher Fetcher) {ch := make(chan []string)go func() {ch <- []string{url}}()coordinator(ch, fetcher)}//// main//func main() {fmt.Printf("=== Serial===\n")Serial("http://golang.org/", fetcher, make(map[string]bool))fmt.Printf("=== ConcurrentMutex ===\n")ConcurrentMutex("http://golang.org/", fetcher, makeState())fmt.Printf("=== ConcurrentChannel ===\n")ConcurrentChannel("http://golang.org/", fetcher)}//// Fetcher//type Fetcher interface {// Fetch returns a slice of URLs found on the page.Fetch(url string) (urls []string, err error)}// fakeFetcher is Fetcher that returns canned results.type fakeFetcher map[string]*fakeResulttype fakeResult struct {body stringurls []string}func (f fakeFetcher) Fetch(url string) ([]string, error) {if res, ok := f[url]; ok {fmt.Printf("found:   %s\n", url)return res.urls, nil}fmt.Printf("missing: %s\n", url)return nil, fmt.Errorf("not found: %s", url)}// fetcher is a populated fakeFetcher.var fetcher = fakeFetcher{"http://golang.org/": &fakeResult{"The Go Programming Language",[]string{"http://golang.org/pkg/","http://golang.org/cmd/",},},"http://golang.org/pkg/": &fakeResult{"Packages",[]string{"http://golang.org/","http://golang.org/cmd/","http://golang.org/pkg/fmt/","http://golang.org/pkg/os/",},},"http://golang.org/pkg/fmt/": &fakeResult{"Package fmt",[]string{"http://golang.org/","http://golang.org/pkg/",},},"http://golang.org/pkg/os/": &fakeResult{"Package os",[]string{"http://golang.org/","http://golang.org/pkg/",},},}

RPC-远程过程调用

RPC:在客户端上调用在服务器端实现的函数-传递参数并返回结果

实际过程:

  • 在客户端上调用stub过程:构建一个消息,包括调用哪个函数,函数的参数,参数类型等等。
  • 通过网络发送给服务器上对应的stub
  • 在服务器上调用函数
  • 返回给服务器的stub
  • 返回给客户端的stub(这个期间一直在等待)
  • 返回结果

示例

package mainimport ("fmt""log""net""net/rpc""sync")//// Common RPC request/reply definitions//type PutArgs struct {Key   stringValue string}type PutReply struct {}type GetArgs struct {Key string}type GetReply struct {Value string}//// Client//func connect() *rpc.Client {client, err := rpc.Dial("tcp", ":1234")if err != nil {log.Fatal("dialing:", err)}return client}func get(key string) string {client := connect()args := GetArgs{"subject"}reply := GetReply{}err := client.Call("KV.Get", &args, &reply)if err != nil {log.Fatal("error:", err)}client.Close()return reply.Value}func put(key string, val string) {client := connect()args := PutArgs{"subject", "6.824"}reply := PutReply{}err := client.Call("KV.Put", &args, &reply)if err != nil {log.Fatal("error:", err)}client.Close()}//// Server//type KV struct {mu   sync.Mutexdata map[string]string}func server() {kv := new(KV)kv.data = map[string]string{}rpcs := rpc.NewServer()rpcs.Register(kv)l, e := net.Listen("tcp", ":1234")if e != nil {log.Fatal("listen error:", e)}go func() {for {conn, err := l.Accept()if err == nil {go rpcs.ServeConn(conn)} else {break}}l.Close()}()}func (kv *KV) Get(args *GetArgs, reply *GetReply) error {kv.mu.Lock()defer kv.mu.Unlock()reply.Value = kv.data[args.Key]return nil}func (kv *KV) Put(args *PutArgs, reply *PutReply) error {kv.mu.Lock()defer kv.mu.Unlock()kv.data[args.Key] = args.Valuereturn nil}//// main//func main() {server()put("subject", "6.824")fmt.Printf("Put(subject, 6.824) done\n")fmt.Printf("get(subject) -> %s\n", get("subject"))}

RPC失败

  • 至少一次:失败后(没有接到服务器的响应)会自动重试
    • 有可能多次执行
  • 最多一次:服务器端实现过滤重复,确保最多只能执行一次(Go的RPC实现)
  • 正好一次:很难实现
]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + MIT-6.824 Distributed Systems-Lab 1 MapReduce + + /2022/12/15/6.824/Distributed-Systems-MIT-6.824-Lab-1/ + + MIT-6.824(Spring 2022)Lab 1 MapReduce

6.824 Lab 1: MapReduce

简介

构建一个MapReduce系统

  1. 实现一个worker进程,调用Map和Reduce函数、处理读写文件,
  2. 实现coordinator进程,向worker分发任务并提供容错机制。

准备开始

src/main/mrsequential.go 中提供了串行的mapreduce程序,在单进程里面直接顺序执行Map操作和Reduce操作

同时提供了一些MapReduce的应用程序:

mrapps/wc.go:WordCount程序

mrapps/indexer.go:text-indexer

按照如下的方式运行串行的mapreduce程序:

cd src/maingo build -race -buildmode=plugin ../mrapps/wc.gorm mr-out*go run -race mrsequential.go wc.so pg*.txtmore mr-out-0

输出的文件中是对文件的WordCount结果

代码理解

插件模式编译

参考资料

Go是静态编译型语言,在编译时就将所有引用的包(库)全部加载打包到最终的可执行程序(或库文件)中,因此并不能在运行时动态加载其他共享库。Go Plugin提供了这样一种方式,能够让你在运行时动态加载外部功能。

  • 可插拔:有了Plugin,我的程序可以根据需要随时替换其中某些部件而不用修改我的程序;
  • 动态加载的需要:有些模块只有在运行时才能确定,需要动态加载外部的功能模块;
  • 独立开发:Plugin 可以和主程序独立建设,主程序只需要制定好框架,实现默认(模版)功能。Plugin 可根据用户需求随时自行扩展开发,运行时随意替换,提高了程序的可定制性;

type Plugin即Golang加载的插件,与之有关的两个方法:

  • Open: 根据参数path提供的插件路径加载这个插件,并返回插件这个插件结构的指针*Plugin
  • Lookup: *Plugin的惟一方法,通过名称symName在插件中寻找对应的变量或方法,以Symbol的形式返回

因此这一行命令将 wc.go文件编译成了一个插件 wc.so(默认文件名),从而可以插入到MapReduce主程序中运行。

wc.go-Map函数

// The map function is called once for each file of input. The first// argument is the name of the input file, and the second is the// file's complete contents. You should ignore the input file name,// and look only at the contents argument. The return value is a slice// of key/value pairs.func Map(filename string, contents string) []mr.KeyValue {// function to detect word separators.ff := func(r rune) bool { return !unicode.IsLetter(r) }// split contents into an array of words.words := strings.FieldsFunc(contents, ff)kva := []mr.KeyValue{}for _, w := range words {kv := mr.KeyValue{w, "1"}kva = append(kva, kv)}return kva}

对每一个传进来的字符串,通过 strings.FieldsFunc函数找到字符串的分割点,分割成单独的单词,构造成KeyValue结构体并合并成切片返回

wc.go-Reduce函数

// The reduce function is called once for each key generated by the// map tasks, with a list of all the values created for that key by// any map task.func Reduce(key string, values []string) string {// return the number of occurrences of this word.return strconv.Itoa(len(values))}

直接以字符串的形式返回values的长度

串行MapReduce运行

导入插件

// load the application Map and Reduce functions// from a plugin file, e.g. ../mrapps/wc.sofunc loadPlugin(filename string) (func(string, string) []mr.KeyValue, func(string, []string) string) {p, err := plugin.Open(filename)if err != nil {log.Fatalf("cannot load plugin %v", filename)}xmapf, err := p.Lookup("Map")if err != nil {log.Fatalf("cannot find Map in %v", filename)}mapf := xmapf.(func(string, string) []mr.KeyValue)xreducef, err := p.Lookup("Reduce")if err != nil {log.Fatalf("cannot find Reduce in %v", filename)}reducef := xreducef.(func(string, []string) string)return mapf, reducef}

从编译好的*.so文件中查找Map函数和Reduce函数,通过函数的返回值类型进行类型推断,最终返回两个函数通过主函数里面的变量进行接收

mapf, reducef := loadPlugin(os.Args[1])

打开文件,进行Map操作

       //// read each input file,// pass it to Map,// accumulate the intermediate Map output.//intermediate := []mr.KeyValue{}for _, filename := range os.Args[2:] {file, err := os.Open(filename)if err != nil {log.Fatalf("cannot open %v", filename)}content, err := ioutil.ReadAll(file)if err != nil {log.Fatalf("cannot read %v", filename)}file.Close()kva := mapf(filename, string(content))intermediate = append(intermediate, kva...)}

pg-*.txt会匹配到所有满足条件的文件,将文件逐个打开,读取文件内容,通过Map函数处理成中间数据格式,存入中间变量intermediate

排序

对中间变量的切片按照键的字典序进行排序

sort.Sort(ByKey(intermediate))// for sorting by key.func (a ByKey) Len() int           { return len(a) }func (a ByKey) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key }

这里是通过实现Sort的接口实现了自定义排序

统计Reduce

//// call Reduce on each distinct key in intermediate[],// and print the result to mr-out-0.//i := 0for i < len(intermediate) {j := i + 1for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {j++}values := []string{}for k := i; k < j; k++ {values = append(values, intermediate[k].Value)}output := reducef(intermediate[i].Key, values)// this is the correct format for each line of Reduce output.fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)i = j}

由于已经排好顺序了,从左到右遍历一遍就可以统计每一个键出现的数量,然后输出到文件即可。

我的工作

实现一个分布式MapReduce,由coordinator和worker两个程序组成。

coordinator进程只有一个,worker进程有一个或多个并行执行。

worker进程将通过RPC与coordinator进程进行通信。每个worker进程将向coordinator进程请求任务,从一个或多个文件中读取任务的输入,执行任务,并将任务的输出写入一个或更多个文件。

coordinator进程应该注意到一个worker进程是否没有在合理的时间内完成其任务(10秒),并将相同的任务交给另一个worker进程。

coordinator和worker的“main”函数在 main/mrcordinator.gomain/mrworker.go

实现应该在 mr/coordinator.gomr/worker.gomr/rpc.go中。

测试运行:

go build -race -buildmode=plugin ../mrapps/wc.gorm mr-out*go run -race mrcoordinator.go pg-*.txtgo run -race mrworker.go wc.sogo run -race mrworker.go wc.so

测试脚本:

bash test-mr.sh

代码理解

待补充的代码提供了一个RPC的示例

启动Worker后,会调用CallExample()函数

// example function to show how to make an RPC call to the coordinator.//// the RPC argument and reply types are defined in rpc.go.func CallExample() {// declare an argument structure.args := ExampleArgs{}// fill in the argument(s).args.X = 99// declare a reply structure.reply := ExampleReply{}// send the RPC request, wait for the reply.// the "Coordinator.Example" tells the// receiving server that we'd like to call// the Example() method of struct Coordinator.ok := call("Coordinator.Example", &args, &reply)if ok {// reply.Y should be 100.fmt.Printf("reply.Y %v\n", reply.Y)} else {fmt.Printf("call failed!\n")}}

函数构建了RPC的结构体,然后调用call函数并接收响应

在这里体现了RPC的核心思想:在这里看起来就是调用的本地函数call,但是实际上call内部是与coordinator进行通信,然后在远程得到返回值后返回给reply结构体,因此为“远程过程调用”

call函数:

// send an RPC request to the coordinator, wait for the response.// usually returns true.// returns false if something goes wrong.func call(rpcname string, args interface{}, reply interface{}) bool {// c, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")sockname := coordinatorSock()c, err := rpc.DialHTTP("unix", sockname)if err != nil {log.Fatal("dialing:", err)}defer c.Close()err = c.Call(rpcname, args, reply)if err == nil {return true}fmt.Println(err)return false}

注意coordinatorSock()方法,会获取一个临时文件,通信是通过这个临时文件进行的。

在coordinator.go内部,RPC指定的方法"Coordinator.Example":

// an example RPC handler.//// the RPC argument and reply types are defined in rpc.go.func (c *Coordinator) Example(args *ExampleArgs, reply *ExampleReply) error {reply.Y = args.X + 1return nil}

因此返回的结构体中reply.Y的值就为100

在启动Worker前要先启动Coordinator,启动后首先创建一个Coordinator结构:

// create a Coordinator.// main/mrcoordinator.go calls this function.// nReduce is the number of reduce tasks to use.func MakeCoordinator(files []string, nReduce int) *Coordinator {c := Coordinator{}// Your code here.c.server()return &c}

其中调用server方法,监听Worker的RPC:

// start a thread that listens for RPCs from worker.gofunc (c *Coordinator) server() {rpc.Register(c)rpc.HandleHTTP()//l, e := net.Listen("tcp", ":1234")sockname := coordinatorSock()os.Remove(sockname)l, e := net.Listen("unix", sockname)if e != nil {log.Fatal("listen error:", e)}go http.Serve(l, nil)}

Coordinator会不断检测Done方法的返回值,一旦为true,Coordinator就会退出:

// main/mrcoordinator.go calls Done() periodically to find out// if the entire job has finished.func (c *Coordinator) Done() bool {ret := false// Your code here.return ret}

Map简单实现

首先考虑简单一些,不考虑并行、容错处理等,先把整个的流程跑通。

首先跑通Map流程

Coordinator的数据结构:

type Coordinator struct {// Your definitions here.MapTask    []MapTaskInformation    // Map任务列表ReduceTask []ReduceTaskInformation // Reduce任务列表}

内部有两个切片,分别对应Map的任务列表和Reduce的任务列表。

两个任务列表是在Coordinator启动的时候就设置好:

// create a Coordinator.// main/mrcoordinator.go calls this function.// nReduce is the number of reduce tasks to use.func MakeCoordinator(files []string, nReduce int) *Coordinator {mapTaskSlice := []MapTaskInformation{}for id, fileName := range files {mapTaskSlice = append(mapTaskSlice, MapTaskInformation{Id:                   id + 1,State:                0,NReduce:              nReduce,OriginFileName:       fileName,IntermediateFileName: "mr-" + strconv.Itoa(id+1) + "-",})}reduceTaskSlice := []ReduceTaskInformation{}for i := 0; i < nReduce; i++ {reduceTaskSlice = append(reduceTaskSlice, ReduceTaskInformation{Id:             i + 1,State:          0,OriginFileName: "mr-0-" + strconv.Itoa(i+1),OutputFileName: "mr-" + strconv.Itoa(i+1),})}c := Coordinator{MapTask:    mapTaskSlice,ReduceTask: reduceTaskSlice,}// Your code here.c.server()return &c}

其中为Map和Reduce暂时设计的数据结构:

type MapTaskInformation struct {Id                   int    // 任务唯一编码State                int    // 0表示未开始,1表示正在进行,2表示已经完成NReduce              int    // 分成Reduce任务的数量OriginFileName       string // 原始文件名称IntermediateFileName string // Map任务完成后的文件名称(中间文件)}type ReduceTaskInformation struct {Id             int    // 任务唯一编码State          int    // 0表示未开始,1表示正在进行,2表示已经完成OriginFileName string // Reduce的初始文件名称(中间文件)OutputFileName string // Reduce任务完成后的最终文件名称}

Worker启动时,通过RPC向Coordinator要一个任务

// main/mrworker.go calls this function.func Worker(mapf func(string, string) []KeyValue,reducef func(string, []string) string) {args := TaskInformation{}reply := TaskInformation{}ok := call("Coordinator.AsssignTask", &args, &reply)

Coordinator会遍历自己内部的所有任务列表,找到第一个还没有完成的任务分配给这个Worker:

// 分配任务func (c *Coordinator) AsssignTask(args *TaskInformation, reply *TaskInformation) error {isMapfinished := true//遍历所有的Map任务信息,将未开始的分配给这个节点for i, mapTask := range c.MapTask {if mapTask.State == 0 {isMapfinished = falsereply.Id = mapTask.Idreply.TaskType = "map"reply.InputFileName = mapTask.OriginFileNamereply.OutputFileName = mapTask.IntermediateFileNamereply.NReduce = mapTask.NReducec.MapTask[i].State = 1return nil} else if mapTask.State == 1 {isMapfinished = false}}// 如果所有的Map任务都完成了,就遍历Reduce任务if isMapfinished {for _, reduceTask := range c.ReduceTask {if reduceTask.State == 0 {return nil}}}return nil}

Worker接收到任务后使用插件中的Map函数进行处理,并将成功完成任务的消息通过RPC的方式返回给Coordinator

if ok {fmt.Println("Call Success!")if reply.TaskType == "map" {fmt.Printf("Map Task!\n")intermediate := []KeyValue{}file, err := os.Open(reply.InputFileName)if err != nil {log.Fatalf("cannot open %v", reply.InputFileName)}content, err := io.ReadAll(file)if err != nil {log.Fatalf("cannot read %v", reply.InputFileName)}file.Close()kva := mapf(reply.InputFileName, string(content))intermediate = append(intermediate, kva...)// 排序sort.Sort(ByKey(intermediate))fmt.Println(intermediate)args = replycall("Coordinator.TaskFinish", &args, &reply)

Coordinator接收消息,将自己内部的任务状态修改,后续就不会再将这个任务分配给Worker了。

// 接收任务已经完成的信息func (c *Coordinator) TaskFinish(args *TaskInformation, reply *TaskInformation) error {if args.TaskType == "map" {c.MapTask[args.Id-1].State = 2} else if args.TaskType == "reduce" {c.ReduceTask[args.Id-1].State = 2}return nil}

问题:

  1. Worker要任务的时候Coordinator去列表中遍历是不是有点太傻了,有更好的办法吗?比如Coordinator维护未完成的和已完成的任务列表,然后动态更新?
  2. 定义的struct数据结构不一定合理,还要看后面怎么用
  3. RPC传递的数据结构不是很合理,而且有大量的冗余,比如后面的消息args和reply几乎完全相同,后面需要修改

Reduce简单实现

首先在Worker的主函数增加一层循环,从而使Worker不断请求任务,由Coordinator按需分配

首先要构造中间文件,也就是map结束后的文件需要存起来,然后才能用reduce去处理

// 循环创建NReduce个文件准备保存encoderList := make([]*json.Encoder, 0)for i := 0; i < reply.NReduce; i++ {fileName := reply.OutputFileName + strconv.FormatInt(int64(i+1), 10)tempFile, err := os.Create(fileName)if err != nil {log.Fatalf("cannot create %v", fileName)}defer tempFile.Close()encoderList = append(encoderList, json.NewEncoder(tempFile))}for i, v := range intermediate {encoderList[ihash(v.Key)%reply.NReduce].Encode(&intermediate[i])}

map在保存的时候要直接分成NReduce的文件,文件的内容是由哈希函数对键进行映射后得到的,保证键大致平均分到NReduce个节点上

保存文件的时候使用的是json的格式,保存的过程有些慢,需要对整个map的结果全部遍历一遍,后续可以考虑并行处理?

Reduce内容:

} else if reply.TaskType == "reduce" {ofile, _ := os.Create(reply.OutputFileName)fmt.Printf("Reduce Task!\n")kva := make([]KeyValue, 0)for p := 1; p <= 8; p++ {filename := strings.Replace(reply.InputFileName, "*", strconv.FormatInt(int64(p), 10), 1)fmt.Println(filename)file, err := os.Open(filename)if err != nil {log.Fatalf("cannot open %v", filename)}dec := json.NewDecoder(file)for {var kv KeyValueif err := dec.Decode(&kv); err != nil {break}kva = append(kva, kv)}}// 排序sort.Sort(ByKey(kva))//// call Reduce on each distinct key in intermediate[],// and print the result to mr-out-0.//i := 0for i < len(kva) {j := i + 1for j < len(kva) && kva[j].Key == kva[i].Key {j++}values := []string{}for k := i; k < j; k++ {values = append(values, kva[k].Value)}output := reducef(kva[i].Key, values)// this is the correct format for each line of Reduce output.fmt.Fprintf(ofile, "%v %v\n", kva[i].Key, output)i = j}

循环读取map保存下来的内容,这里写死了,后面需要调整。

读取内容后汇总并排序,排序后直接使用串行的Reduce代码即可

对于Coordinator,将Reduce的内容添加进去即可:

// 如果所有的Map任务都完成了,就遍历Reduce任务if isMapfinished {for i, reduceTask := range c.ReduceTask {if reduceTask.State == 0 {reply.Id = reduceTask.Idreply.TaskType = "reduce"reply.InputFileName = reduceTask.OriginFileNamereply.OutputFileName = reduceTask.OutputFileNamemu.Lock()c.ReduceTask[i].State = 1mu.Unlock()return nil}}}

Reduce结束后需要告知主Coordinator在无限循环的Done(),返回True让其退出:

// main/mrcoordinator.go calls Done() periodically to find out// if the entire job has finished.func (c *Coordinator) Done() bool {ret := truemu.Lock()// Your code here.for _, v := range c.ReduceTask {if v.State != 2 {ret = falsebreak}}mu.Unlock()return ret}

中间添加了锁,但是添加的有些问题,后面需要调整。

到这里的代码除了异常处理外已经都能测试通过了,只不过是有data race问题

问题:

  1. Worker的无限循环退不出去,需要Coordinator通过RPC的方式告知才可以
  2. Reduce的遍历文件写死了,需要动态变化去判断
  3. Coordinator存在data race问题,是循环遍历任务和对任务的完成情况进行更改后两者的锁加的不太好导致的,需要对数据结构进行修改
  4. 没有异常处理,不能处理有Worker异常退出的情况,实际测试中陷入了死循环,需要进行调整

问题及解决

首先将Coordinator对于任务的数据结构更改,内部维护三个双向链表,分别表示未开始的任务,正在进行的任务和已经结束的任务,链表外面使用map的数据结构,从而支持快速查找。在生成任务的时候自动赋值一个全局唯一的id。

数据结构中要包括全部的信息,主要变化部分是对输入和输出的信息,将Map的输入、输出和Reduce的输出都在初始化的时候直接写在结构体中,避免后续进行多次判断和修改。

结构体:

// Coordinator存储的主要信息,包括Map和Reduce两部分任务的信息以及工作节点的信息type Coordinator struct {UniqueIdSlice     []*list.Element // 通过任务Id找到任务信息的切片,相当于一个MapMapTaskNum        int             // map任务总数量ReduceTaskNum     int             // reduce任务总数量WorkerNum         int             // 目前正在工作的节点数量MapTask                           // Map任务信息链表ReduceTask                        // Reduce任务信息链表WorkerInformation                 // Worker的信息}// Map任务信息链表,包括三个链表,分别表示未开始、正在进行和已经完成的任务type MapTask struct {MapListReady    *list.List // 未开始的Map任务MapListRunning  *list.List // 正在进行的Map任务MapListComplete *list.List // 已经完成的Map任务}// Reduce任务信息链表,包括三个链表,分别表示未开始、正在进行和已经完成的任务type ReduceTask struct {ReduceListReady    *list.List // 未开始的Reduce任务ReduceListRunning  *list.List // 正在进行的Reduce任务ReduceListComplete *list.List // 已经完成的Reduce任务}// Map任务具体信息type MapTaskInformation struct {Id                   int      // 任务唯一编码OriginFileName       string   // 原始文件名称IntermediateFileName []string // Map任务完成后中间文件列表}// Reduce任务具体信息type ReduceTaskInformation struct {Id                   int      // 任务唯一编码IntermediateFileName []string // Reduce的初始中间文件列表(从Map处获得)OutputFileName       string   // Reduce任务完成后的最终文件名称}

Worker中分为几个步骤:

  1. 告知Coordinator自己已经上线
  2. 向Coordinator请求任务
  3. 向Coordinator返回自己的Map任务已经完成
  4. 向Coordinator返回自己的Reduce任务已经完成
  5. 向Coordinator返回自己退出的消息

主程序如下:

// main/mrworker.go 调用的函数func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) {// 1. 告知Coordinator自己已经上线args := WorkerArgs{TaskType: "None"}reply := WorkerReply{TaskType: "None"}call("Coordinator.WorkerOnline", &args, &reply)// 无限循环向Coordinator请求任务for {// 2. 向Coordinator请求任务args = WorkerArgs{TaskType: "None"}reply = WorkerReply{TaskType: "None"}ok := call("Coordinator.AsssignTask", &args, &reply)if ok {fmt.Println("Call Success!")if reply.TaskType == "map" {fmt.Printf("Map Task!\n")// 读取文件,调用map函数进行处理intermediate := []KeyValue{}file, err := os.Open(reply.MapInput)if err != nil {log.Fatalf("cannot open %v", reply.MapInput)}content, err := io.ReadAll(file)if err != nil {log.Fatalf("cannot read %v", reply.MapInput)}file.Close()kva := mapf(reply.MapInput, string(content))intermediate = append(intermediate, kva...)// 循环创建NReduce个文件准备保存encoderList := make([]*json.Encoder, 0)for _, fileName := range reply.MapOutput {tempFile, err := os.Create(fileName)if err != nil {log.Fatalf("cannot create %v", fileName)}defer tempFile.Close()encoderList = append(encoderList, json.NewEncoder(tempFile))}// 将map后的结果存入文件中(最费时间)for i, v := range intermediate {encoderList[ihash(v.Key)%len(reply.MapOutput)].Encode(&intermediate[i])}// 3. 向Coordinator返回自己的Map任务已经完成args.TaskType = "map"args.Taskid = reply.Idcall("Coordinator.TaskFinish", &args, &reply)} else if reply.TaskType == "reduce" {fmt.Printf("Reduce Task!\n")// 创建输出文件ofile, _ := os.Create(reply.ReduceOutput)// 遍历输入文件,汇总Map产生的所有结果kva := make([]KeyValue, 0)for _, filename := range reply.ReduceInput {// fmt.Println(filename)file, err := os.Open(filename)if err != nil {log.Fatalf("cannot open %v", filename)}dec := json.NewDecoder(file)for {var kv KeyValueif err := dec.Decode(&kv); err != nil {break}kva = append(kva, kv)}}// 排序sort.Sort(ByKey(kva))// 在已经排好序的键值对上进行统计,并写入到文件中i := 0for i < len(kva) {j := i + 1for j < len(kva) && kva[j].Key == kva[i].Key {j++}values := []string{}for k := i; k < j; k++ {values = append(values, kva[k].Value)}output := reducef(kva[i].Key, values)fmt.Fprintf(ofile, "%v %v\n", kva[i].Key, output)i = j}// 4. 向Coordinator返回自己的Reduce任务已经完成args.Taskid = reply.Idargs.TaskType = "reduce"call("Coordinator.TaskFinish", &args, &reply)} else if reply.TaskType == "finish" {// 5. 向Coordinator返回自己退出的消息call("Coordinator.WorkerFinish", &args, &reply)fmt.Printf("Bye!\n")return}} else {fmt.Printf("Call failed!\n")}// 间隔1秒请求一次time.Sleep(time.Second)}}

其中将RPC的发送和接收的结构体更改的更为合理:

// Worker向Coordinator传递的信息type WorkerArgs struct {Id       int    // Worker的唯一IDTaskid   int    // 任务全局唯一IDTaskType string // 任务类型}// Coordinator向Worker传递的信息type WorkerReply struct {Id           int      // 任务idTaskType     string   // 任务类型MapInput     string   // Map任务的输入MapOutput    []string // Map任务的输出ReduceInput  []string // Reduce任务的输入ReduceOutput string   // Reduce任务的输出}
  • 告知Coordinator自己已经上线:
// Worker告知Coordinator自己上线了func (c *Coordinator) WorkerOnline(args *WorkerArgs, reply *WorkerReply) error {mu.Lock()if c.WorkerNum == -1 {c.WorkerNum = 0}c.WorkerNum += 1mu.Unlock()return nil}

这里暂时比较简单,后续需要进行处理,以进行异常处理

  • 向Coordinator请求任务:
// Worker向Coordinator请求任务func (c *Coordinator) AsssignTask(args *WorkerArgs, reply *WorkerReply) error {mu.Lock()// 首先查看map任务是否已经全部完成,如果全部完成了就去完成Reduce任务,如果也全部完成了就发送Worker可以退出的消息// 判断方式:通过完成链表的节点数量与初始化时侯计算的数量是否相同if c.MapListComplete.Len() != c.MapTaskNum {// 分配map任务if c.MapListReady.Len() == 0 {// 没有没开始的Map任务reply.TaskType = "waiting"} else {// 将一个未完成的任务从未开始的链表中取出,插入到正在进行的链表里面e := c.MapListReady.Front()c.MapListReady.Remove(e)c.MapListRunning.PushBack(e)// 构建返回消息,告知Worker这个任务的信息reply.TaskType = "map"value := e.Value.(MapTaskInformation)reply.Id = value.Idreply.MapInput = value.OriginFileNamereply.MapOutput = value.IntermediateFileName}} else if c.ReduceListComplete.Len() != c.ReduceTaskNum {// 分配reduce任务if c.ReduceListReady.Len() == 0 {// 没有没开始的Reduce任务reply.TaskType = "waiting"} else {// 将一个未完成的任务从未开始的链表中取出,插入到正在进行的链表里面e := c.ReduceListReady.Front()c.ReduceListReady.Remove(e)c.ReduceListRunning.PushBack(e)// 构建返回消息,告知Worker这个任务的信息reply.TaskType = "reduce"value := e.Value.(ReduceTaskInformation)reply.Id = value.Idreply.ReduceInput = value.IntermediateFileNamereply.ReduceOutput = value.OutputFileName}} else {//告知Worker已经没有任务了,可以退出了reply.TaskType = "finish"}mu.Unlock()return nil}

收到请求后操作全局链表,构建消息并返回即可

  • 向Coordinator返回自己的任务已经完成
// Worker告知Coordinator刚才分配的任务已经完成func (c *Coordinator) TaskFinish(args *WorkerArgs, reply *WorkerReply) error {mu.Lock()// 将节点从正在进行的链表中取出,插入到已经完成的链表中if args.TaskType == "map" {// 操作节点e := c.UniqueIdSlice[args.Taskid]c.MapListRunning.Remove(e)c.MapListComplete.PushBack(e)// 如果是Map任务,需要将产生的nReduce个中间文件分配给Reduce节点for _, file := range e.Value.(MapTaskInformation).IntermediateFileName {// 计算是哪个Reduce节点reduceTaskNum, err := strconv.Atoi(strings.Split(file, "-")[2])if err != nil {log.Fatalf("cannot parseInt %v", file)}// 将产生的nReduce个中间文件分配给Reduce节点(需要重新构建节点)value := c.UniqueIdSlice[reduceTaskNum].ValuetempSlice := append(value.(ReduceTaskInformation).IntermediateFileName, file)c.UniqueIdSlice[reduceTaskNum].Value = ReduceTaskInformation{Id:                   value.(ReduceTaskInformation).Id,IntermediateFileName: tempSlice,OutputFileName:       value.(ReduceTaskInformation).OutputFileName,}}} else if args.TaskType == "reduce" {// 操作节点e := c.ReduceListRunning.Remove(c.UniqueIdSlice[args.Taskid])c.ReduceListComplete.PushBack(e)}mu.Unlock()return nil}

对于Map任务需要传递Map输出,Reduce输入的文件信息,将结构体填充完整

  • 向Coordinator返回自己退出的消息
// Worker告知Coordinator自己退出了func (c *Coordinator) WorkerFinish(args *WorkerArgs, reply *WorkerReply) error {mu.Lock()// 退出时将Coordinator内部存储的Worker数量-1c.WorkerNum -= 1mu.Unlock()return nil}

将全局的WorkerNum减去1,后续需要进行处理。

经测试,除异常检测完已经都能顺利pass,多次运行的结果也完全相同

有一个小问题是它的脚本给的超时时间不够,调大一些后才能顺利运行,后续可以进行更改。

异常处理

原文与异常处理相关的部分:

The coordinator should notice if a worker hasn’t completed its task in a reasonable amount of time (for this lab, use ten seconds), and give the same task to a different worker.

The best you can do is have the coordinator wait for some amount of time, and then give up and re-issue the task to a different worker. For this lab, have the coordinator wait for ten seconds; after that the coordinator should assume the worker has died (of course, it might not have).

To test crash recovery, you can use the mrapps/crash.go application plugin. It randomly exits in the Map and Reduce functions.

可以先查看crash.go,看看是如何模拟线程崩溃的:

func maybeCrash() {max := big.NewInt(1000)rr, _ := crand.Int(crand.Reader, max)if rr.Int64() < 330 {// crash!os.Exit(1)} else if rr.Int64() < 660 {// delay for a while.maxms := big.NewInt(10 * 1000)ms, _ := crand.Int(crand.Reader, maxms)time.Sleep(time.Duration(ms.Int64()) * time.Millisecond)}}

阅读代码,可以发现这个设置是有1/3的概率直接崩溃掉,有2/3的概率线程睡眠不到10s,模拟的环境还是比较简单的。

实现:

Worker部分:

Worker上线后,由Coordinator为其分配一个ID,随后在Worker的每一个rpc请求中都带有这个ID

WorkerID := reply.WorkerID

Worker上线后每5秒发送心跳信号给Coordinator,表明自己在线

// 心跳信号go func() {for {args := WorkerArgs{TaskType: "None"}args.Id = WorkerIDreply := WorkerReply{TaskType: "None"}time.Sleep(time.Second * 5)call("Coordinator.WorkerAlive", &args, &reply)}}()

Coordinator部分:

维护一个切片结构体,索引表示Worker的ID,结构体内部包括任务ID和上一次心跳信号的时间

type HeartBeat struct {WorkID intTime   int64}var WorkerList []HeartBeat

接收到Worker上线的RPC后,记录当前的时间戳,记录任务ID为-1,即表示这个索引ID已经分配给Worker了

// 分配任务ID并记录时间WorkerList = append(WorkerList, HeartBeat{WorkID: -1,Time:   time.Now().Unix(),})reply.WorkerID = len(WorkerList)

接收心跳信号后更新切片结构体

// Coordinator接收心跳信号func (c *Coordinator) WorkerAlive(args *WorkerArgs, reply *WorkerReply) error {mu.Lock()WorkerList[args.Id-1].Time = time.Now().Unix()fmt.Printf("接收到%d心跳信号\n", args.Id-1)mu.Unlock()return nil}

分配任务后在切片结构体内更新任务ID信息

WorkerList[args.Id-1].WorkID = value.Id

开启协程每10秒检查切片结构体的时间戳,如果时间戳与当前时间间隔大于10秒,将任务的状态更改为未完成,重新分配。

// Worker信息存储WorkerList = make([]HeartBeat, 0)// 每间隔10秒进行验证go func() {for {time.Sleep(10 * time.Second)mu.Lock()for i := 0; i < len(WorkerList); i++ {if WorkerList[i].WorkID != -1 && time.Now().Unix()-WorkerList[i].Time > 10 {fmt.Printf("%d心跳信号过期\n", i)e2 := *(c.UniqueIdSlice[WorkerList[i].WorkID])// 这里不太懂为什么要这样写if WorkerList[i].WorkID < c.MapTaskNum {c.MapListRunning.Remove(&e2)c.MapListReady.PushBack(e2.Value)} else {c.ReduceListRunning.Remove(&e2)c.ReduceListReady.PushBack(e2.Value)}c.WorkerNum -= 1WorkerList[i].WorkID = -1}}mu.Unlock()}}()

结束

至此,可以单独通过全部的test,但是仍然存在一些问题

  • 代码可读性不高,不够规范,自己都看不太明白
  • 第一个wc的test和第二个index的test结合在一起通不过,但是可以单独通过两个test
  • 最后异常检测的时候会有worker退不出去
  • 代码运行时间整体比较长,不能满足脚本的运行时间
  • 加锁的地方考虑的比较少,有点过于简单粗暴了

总之基本功能已经没有什么问题了,以后有时间再进行重构。

]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + MIT-6.824 Distributed Systems-LEC 1 Introduction + + /2022/12/13/6.824/Distributed-Systems-MIT-6.824-LEC-1/ + + MIT-6.824(Spring 2022)LEC 1 Introduction

MapReduce论文阅读

参考翻译

方法提出

  • 大量的数据分布在不同的机器上,为了能让某些算法在可以接受的时间内完成,需要将算法分配到不同的机器上一起并行运行
  • 受Lisp的启发,我们发现大多数的操作都可以分为两个部分,map和reduce
    • 首先将输入中的逻辑记录应用map操作转化为过渡的键值对
    • 然后将相同的键对应的值应用reduce操作,从而合并上一步产生的过渡数据

编程模型

(WordCount)

map(String key, String value):// key: document name// value: document contents    for each word w in value:        EmitIntermediate(w, "1");reduce(String key, Iterator values):// key: a word// values: a list of counts    int result = 0;    for each v in values:        result += ParseInt(v);    Emit(AsString(result));

map函数输出每个单词和计数的数量,reduce汇总其中某个特定单词的数量并输出。

  • 分布式查找:map函数匹配到了就直接输出,reduce函数不发挥作用
  • 计数URL访问频率:map函数对网页的日志进行处理并输出中间键值对,reduce函数再进行汇总处理
  • 反转网页-链接图:map函数在source网页中寻找target URL,输出 <target, source>键值对,reduce函数对目标URL汇总source并输出
  • 节点的主干词向量
  • 倒排索引
  • 分布式排序:map从每一条记录中提取键,reduce输出所有的键值对(后面详细说明)

Google对MapReduce的一种实现

zIslPU.md.png

如上图所示,map的过程是在多机器上调用的,其中分配的过程是自动化的,共分配了个节点进行。reduce过程是通过用户指定的节点数量,通过某种方法(如计算哈希值等)分配台机器进行。

其中有一个master节点,这个节点负责将任务进行分配,有些机器进行map操作,有些机器进行reduce操作等。

被分配到map任务的节点读取输入,将处理好的内容写入缓存,周期性的存入硬盘。存入时直接分为部分,并将数据存放的位置告知master

当一个节点被master通知要进行reduce时,通过RPC的方式从硬盘中读取数据到缓存中,进行处理并排序,保证相同的key出现在相同的位置

最终输出的时的文件,但是并不需要用户进行手动合并,因为这些文件通常是作为下一阶段的输入。

Master数据结构

对于每一个map任务或者reduce任务,都要保存任务的状态(已经完成或者未完成)以及工作节点的信息

对于每一个完成后的map任务,还要保存完成后的中间数据的位置和大小等信息

容错机制

机器太多了肯定有的机器会失效

Worker失效:Master会定期ping每一个Worker,如果没有得到响应,将这个节点标记为失效

  • 如果节点的任务正在进行,将分配给它的任务还原到初始状态,给没有失效的节点去完成
  • 如果节点的任务已经完成,对于map任务要重做,因为无法访问这个节点的存储。对于reduce来说不需要,因为已经输出到文件了
  • map任务重做时会通知所有的reduce任务的节点

Master失效:Master的数据要经常备份,且由于只有一个Master,不太可能失效(因为被保护好了?),因此如果Master失效了会终止整个任务

故障时处理的机制:用户提供的Map和Reduce操作是输入确定性函数时,分布式的计算要保证任何情况下的输出都要一致没有错误.

使用map和reduce的原子提交特点来实现。map和reduce操作都写入临时文件中,完成操作后通知Master节点。如果Master节点被通知了另外一次,则直接忽略掉。reduce操作结束后将临时文件重命名为最终输出的文件,重命名操作也是原子性,最终只会有一个符合条件的文件名。

存储位置

尽量存储在本地的硬盘中,通过GFS把每个文件按64MB一个块,并在不同的机器上存储三份冗余的数据。

任务粒度

理想情况下都应该比物理节点数量大得多,在每台机器都执行大量的不同任务能够提高集群的动态的负载均衡能力,并且能够加快故障恢复的速度。

在我们的具体实现中对的取值有一定的限制,因为master必须执行)次调度,并且在内存中保存个状态(一个字节一个状态)

值通常由用户指定,实际使用中选择合适的值,以使得每一个独立任务都是处理大约的输入数据

MapReduce的合适执行比例:,使用台机器节点

备份任务

在运算过程中,如果有一台机器花了很长的时间才完成最后几个Map或Reduce任务,会导致MapReduce操作总的执行时间超过预期。

当一个MapReduce操作接近完成的时候,master会调度备用任务进程来一起执行最后的任务,谁完成了整个任务都算完成。

任务细节

在具体的实现上,对上面描述的简单mapreduce过程可以进行优化

  1. reduce前需要先分配map的结果,使用哈希函数的方式分配的比较均衡,但是可能有一些场景下需要将特定的键值对分配到一起,因此用户可以传入自定义的类似于哈希的函数进行分配
  2. 确保在给定的分区中,键值对数据的处理顺序是按照键进行排序后的。排序后对后面的任务都有利
  3. Map函数产生的中间key值的重复数据会占很大的比重(成千上万个<the,1>),因此允许用户指定一个可选的combiner函数,combiner函数首先在本地将这些记录进行一次合并,然后将合并的结果再通过网络发送出去。一般情况下,Combiner和Reduce函数相同。区别在于输出到最终文件还是中间文件。
  4. MapReduce支持不同的格式的输入数据,如文本或者键值对等,同时提供Reader接口使用户可以自定义输出,只要保证输入是可以分割的就可以
  5. 某些情况下,在Map或Reduce操作过程中增加辅助的输出文件会比较省事。(但是这里不支持?)
  6. 用户程序中的bug导致Map或者Reduce函数在处理某些记录的时候会崩溃掉。这个bug可能很难找。因此提供了一种执行模式,在这种模式下,为了保证保证整个处理能继续进行,MapReduce会检测哪些记录导致确定性的crash,并且跳过这些记录不处理。
  7. 在远程分布式节点上调试程序非常困难,因此开发了一套MapReduce库的本地实现版本,可以调试使用
  8. master使用嵌入式的HTTP服务器(如Jetty)显示一组状态信息页面,用户可以监控各种执行状态
  9. MapReduce库使用计数器统计不同事件发生次数。比如,用户可能想统计已经处理了多少个单词、已经索引的多少篇German文档等等。可以用于MapReduce操作的完整性检查。

实验表现

  • 在大约1TB的数据中进行特定的模式匹配(从海量数据中抽取感兴趣的数据)
  • 对大约1TB的数据进行排序(对数据的形式进行转换)

应用

  1. 大规模机器学习问题
  2. Google News和Froogle产品的集群问题
  3. 从公众查询产品(比如Google的Zeitgeist)的报告中抽取数据。
  4. 从大量的新应用和新产品的网页中提取有用信息(比如,从大量的位置搜索网页中抽取地理位置信息)。
  5. 大规模的图形计算。

MapReduce的成功取决于采用MapReduce库能够在不到半个小时时间内写出一个简单的程序,这个简单的程序能够在上千台机器的组成的集群上做大规模并发处理,极大的加快了开发和原形设计的周期。另外,采用MapReduce库,可以让完全没有分布式和/或并行系统开发经验的程序员很容易的利用大量的资源,开发出分布式和/或并行处理的应用。

结论

MapReduce的成功有几个方面:

  1. MapReduce封装了并行处理、容错处理、数据本地化优化、负载均衡等等技术难点的细节,使得MapReduce库易于使用。
  2. 大量不同类型的问题都可以通过MapReduce简单解决。
  3. 实现了在数千台计算机组成的大型集群上灵活部署运行的MapReduce,使得有效利用这些计算资源变得非常简单,适合用来解决其他需要大量计算的问题。

从MapReduce开发过程中也学到了不少东西。

  1. 使用固定的编程模式使得并行和分布式计算非常容易,也易于构造容错的计算环境;
  2. 网络带宽是稀有资源。大量的系统优化是针对减少网络传输量为目的的:本地优化策略使大量的数据从本地磁盘读取,中间文件写入本地磁盘、并且只写一份中间文件也节约了网络带宽
  3. 备份服务器执行相同的任务可以减少性能缓慢的机器带来的负面影响(硬件配置的不平衡),同时解决了由于机器失效导致的数据丢失问题。

LEC 1

什么是分布式系统

  • 多个计算机通过网络连接,因此只能通过发送和接收数据包的形式进行交互,不能共享内存等等。
  • 支持应用程序的基础设施主干架构

分布式系统的作用

  • 连接物理上分离的机器-允许用户之间的数据共享
  • 通过并行提升性能
  • 容错机制-挂掉的机器不能影响服务
  • 通过将程序分布在不同的机器上获得安全性(例如一台机器只用于登录服务的验证)

分布式系统的发展历程

  • 起始于局域网出现(AFS)-DNS、Email
  • 数据中心(大量数据)和大型网站(大量用户)
  • 云计算
  • 很难跟上时代发展节奏,一直在不断努力

分布式系统的挑战

  • 很多并行的部分
  • 容错机制
  • 很难实现分布式的性能优势

判断系统是否正常工作非常困难,例如两台机器间的网络挂掉,两边都认为对方挂掉了,因此对外提供了两份服务。

课程关注的内容

课程不关注应用程序,只关注基础设施,也就是支撑这些应用程序正确工作的部分。

关注的三个方面:存储、计算和通信

抽象:分布式系统的抽象与单机系统的抽象基本相同

重点内容

容错机制

  • 可用性:使系统高可用的技术,某个节点挂掉仍然可以正常工作
    • 关键:复制
  • 可恢复性:挂掉的机器重启后还能回到分布式系统中继续工作
    • 关键:日志或事务

一致性:分布式系统与单机的行为相同

性能:不同类型的一致性和容错机制与性能相关

  • 吞吐量
  • 低延迟:某些很慢的机器会拖慢整个程序的运行过程

实现细节:如何实现并发、远程过程调用等等

MapReduce

背景

在Google早期的数据中心,有一个搜索引擎,需要构建万维网的倒排索引,允许用户上网查询。

在这个过程中处理TB级别的数据需要耗费几个小时。

为每一个应用都编写一个这种系统很困难,因此提出了MapReduce,使得构建不同应用的分布式程序比较轻松

不过这些应用必须要能分成map和reduce两个部分,然后放到MapReduce框架下运行,不需要再关注其他细节(如容错机制等等)

框架图

zIslPU.md.png

  1. Map操作统计所有的输入文件,不同机器节点之间没有通信
  2. Shuffle:从每个Map获取输出,按照键进行排序(最难的操作)
  3. 在键相同的字段上运行Reduce

主要的网络通信在于传输map产生的中间文件给reduce使用

容错机制

如果一个机器在一定的时间内没有对Coordinator作出响应,就认为这个机器已经挂掉了,因此Coordinator会重新安排其他机器重启它的任务。

map和reduce任务可能会运行两次,例如Coordinator认为这个机器挂掉了,把它的任务分配给别人了,但是实际上这个机器并没有挂掉。最终使用重命名操作的原子性确保只存储一个结果。

Coordinator会挂掉吗?挂掉了整个任务就都要重新跑了,一般不会挂掉。

一些机器可能会运行很慢从而拖累整个任务的进程。当整个任务快要结束的时候,会复制任务到其他的空闲节点上一起做,谁先做完取谁的。

]]>
+ + + + + Study + + + + + + + Go + + Distributed Systems + + + +
+ + + + + 研究生课程:机器学习-期末复习 + + /2022/12/11/UCAS/machine-learning/machine-learning-final/ + + 《机器学习》期末复习

选择题

各种分类

监督学习:贝叶斯分类器、支持向量机、Logistic回归、决策树、线性回归、最大熵、CRF

无监督学习:主成分分析、K-Means、高斯混合聚类、层次聚类

线性分类方法:感知机、线性鉴别分析、最小距离分类器

非线性分类方法:决策树、最近邻、集成学习、核SVM

线性分类器最佳准则:感知准则函数、支持向量机、Fisher准则

生成式模型:朴素贝叶斯、隐马尔可夫模型、高斯混合模型

判别式模型:支持向量机、线性分类器、神经网络、线性判别分析

回归

Logistic回归使用最大似然估计

回归问题和分类问题的区别:前者预测函数值为连续值,后者为离散值

最小二乘回归方法的等效回归方法:线性均值和正态误差的最大似然回归

正则化的回归分析,可以避免过拟合

假如使用一个较复杂的回归模型来拟合样本数据,使用岭回归,调试正则化参数λ,来降低模型复杂度。若λ较大时,偏差增大,方差减小

在线性回归中使用正则项,你发现解的不少coefficient都是0,这个正则项可能是L0-norm或L1-norm

LR模型的损失函数是交叉熵

在Logistic Regression 中,如果同时加入L1和L2范数,可以做特征选择,并在一定程度上防止过拟合

逻辑斯蒂回归没有利用回归的思想

共轭分布

二项式分布的共轭分布是Beta分布

多项式分布的共轭分布是Dirichlet分布

贝叶斯

  • 以贝叶斯定理为基础
  • 可以解决有监督学习的问题
  • 可以用极大似然估计法解贝叶斯分类器

朴素贝叶斯分类器的特点是假设样本各维属性独立

最大似然估计没有考虑先验分布

对于正态密度的贝叶斯分类器,各类协方差矩阵相同时,决策函数为线性决策函数

下面关于贝叶斯分类器描述错误:是基于后验概率,推导出先验概率

朴素贝叶斯模型属于生成式模型

贝叶斯分类器参数估计的准则:最大高斯后验、最大beta后验、极大似然

错误:以贝叶斯估计的角度来看朴素贝叶斯时,其没有估计联合概率

以下模型中属于贝叶斯网络的有( BD )

A.马尔可夫随机场

B.隐马尔可夫模型

C.条件随机场

D.朴素贝叶斯分类器

SVM

支持向量机属于判别式模型

SVM的原理:最大间隔分类

SVM的算法性能取决于:核函数的选择、核函数的参数、软间隔参数C

支持向量机的对偶问题是凸二次优化

支撑向量:最大间隔支撑面上的向量

避免直接的复杂非线性变换,采用线性手段实现非线性学习的方法是:核函数方法

软间隔SVM的阈值趋于无穷:只要最佳分类超平面存在,它就能将所有数据全部正确分类

核函数并不是把特征映射到的空间维度越高越好

如果SVM模型欠拟合, 以下方法哪些可以改进模型:增大惩罚参数C的值,增大核系数(gamma参数)

聚类

密度聚类方法充分考虑了样本间的密度可达关系

混合高斯聚类使用了EM算法

k-means算法初始值不同,最终结果可能不同

k-means不适合处理非凸型数据

以下可用于聚类性能测量的评估方法:Jaccard系数、FM指数、Rand指数、DB指数

降维

主成分分析方法是一种降维方法

PCA在做降维处理时,优先选取中心化样本的协方差矩阵的最大特征值对应特征向量

可以用于特征降维的:SVD、PCA和LDA

不可以用于特征降维的:蒙特卡洛方法

特征降维带来的好处:节省数据通信开销、节省数据存储资源、加快模型计算速度

决策树

关于决策树节点划分指标描述正确的是信息增益越大越好

决策树不受数据归一化影响,SVM、神经网络、Logistic回归都会受影响

增加决策树的深度可能导致随机森林模型过拟合数据

我们想在大数据集上训练决策树, 为了使用较少时间, 我们可以减少树的深度,减少树的数量

集成学习

Bootstrap数据:有放回地从总共N个样本中抽样n个样本

集成学习中基分类器多样,差异大,学习效率通常越好,每个基分类器的正确率的最低要求50%以上

Bagging方法的特点:构造训练集时采用Bootstraping的方式

Boosting方法的特点:预测结果时,分类器的比重不同

随机森林方法属于Bagging方法

Adaboost算法:

  • 是弱分类器的线性组合
  • 提升树是以分类树或者回归树为基本分类器的提升办法
  • 该算法实际上是前向分步算法的一个实现,在这个方法里,模型是加法模型,损失函数是指数损失,算法是前向分步算法。

Adaboost方法中,需要迭代调整的两个重要参数是:样本权重和分类器权重

深度学习

以下关于深度网络训练的说法正确的:

  • 训练过程需要用到梯度,梯度衡量了损失函数相对于模型参数的变化率
  • 损失函数衡量了模型预测结果与真实值之间的差异
  • 训练过程基于一种叫做反向传播的技术

在训练神经网络时,如果出现训练error过高,增加训练数据不能大幅度降低训练error

Tanh可以导致梯度消失

ReLU在神经网络中引入了非线性

关于CNN,Pooling层用于减少图片的空间分辨率

卷积神经网络可以有多个卷积核,可以不同大小

GRU和LSTM的说法正确的是:GRU的参数比LSTM的参数少

与普通反向传播不同的是,BPTT会在每个时间步长内叠加所有对应权重的梯度

在RNN中,梯度裁剪可以较好地处理梯度爆炸问题

循环神经网络有反馈连接并常被用来处理序列数据

过拟合和欠拟合

数据增强会增加模型的欠拟合风险

过拟合现象中训练样本的测试误差最小,测试样本的正确识别率却很低

过拟合:训练误差小,测试误差大

容易引起过拟合:SVM算法中使用高斯核代替线性核

不容易引起过拟合:增加训练集量、减少神经网络隐藏层节点数、删除稀疏的特征

神经网络处理过拟合:Dropout、Batch Normalization、regularization

概率图模型

在HMM中,如果已知观察序列和产生观察序列的状态序列,那么可用极大似然估计直接进行参数估计

解决隐马模型中预测问题的算法是维特比算法

其他

K-NN最近邻方法在什么情况下效果好:样本较少但典型性较好

以下可行的最近邻分类的加速方案:分层搜索和训练样本缩减

线性鉴别分析:找到一个投影方向,使得类内距离最小,类间距离最大

KL散度是根据类概率密度构造的可分性判据

最大似然估计没有考虑先验分布

多层感知机方法中,可用作神经元的非线性激活函数:logistic 函数

在有限支撑集上,均匀分布的熵最大

已知均值和方差,高斯分布的熵最大

受限玻尔兹曼机属于概率图模型

余弦距离会侧重考虑向量的方向

除了EM算法,梯度下降也可求混合高斯模型的参数

下列哪个不属于常用的文本分类的特征选择算法(D)

A. 卡方检验值

B. 互信息

C. 信息增益

D. 主成分分析

解决样本类别不平衡的手段:欠采样、过采样、使用focal loss

对于k折交叉验证, 以下对k的说法正确的是:

A.k越大, 不一定越好, 选择大的k会加大评估时间

B.选择更大的k, 就会有更小的bias ,因为训练集更加接近总数据集

C.在选择k时, 要最小化数据集之间的方差

下列选项中,关于KNN算法说法不正确的是(D)

A.能找出与待测样本相近的K个样本

B.可以使用欧氏距离度量相似度

C.实现过程相对简单,但是可解释性不强

D.效率很高

73.关于特征预处理,下列说法中错误的是(B )

A.包含标准化和归一化

B.标准化在任何场景下受异常值的影响都很小

C.归一化利用了样本中的最大值和最小值

D.标准化实际上是将数据在样本的标准差上做了等比例的缩放操作

交叉验证不能够提升模型的准确率

76.EM算法(Expectation Maximization Algorithm)是机器学习领域的一个经典算法,下面关于EM算法的说法中不正确的有:(A)

A.EM算法属于一种分类算法

B.EM算法可用于隐马尔科夫模型的参数估计

C.EM算法可以分为E-step和M-step两步

D.EM算法可用于从不完整的数据中计算最大似然估计

将一个k分类问题分解成一对一问题时总共需要k(k-1)/2个分类器

在有限支撑集上,下面分布中熵最大的是均匀分布

在机器学习中,当模型的参数量大于样本量时参数估计使用梯度下降法

  1. GRU和LSTM的说法正确的是(D)

A. GRU通过output gate控制memory;

B. LSTM对memory不做控制,直接传递给下一个unit

C. GRU不对上一时刻的信息做任何控制;

D. GRU的参数比LSTM的参数少;

以下哪些算法, 可以用神经网络去构造( BD )

A.KNN

B.Logistic回归

C.决策树

D.最小二乘估计

简答题

原题目

试阐述LDA(线性鉴别分析)的分类思想

给定训练样例集,设法将样例投影到一条直线上,使得同类样例的投影点尽可能接近,异类样例的投影点尽可能远离;

在对新样本进行分类时,将其投影到同样的这条直线上,再根据投影点的位置来判断新样本的类别。

请简要介绍SVM的设计思想

答案:SVM是一个分类算法,它的目标为确定一个分类超平面,从而将不同类别的数据分隔开达到分类的目标。

当训练数据线性可分时,通过硬间隔最大化,学习一个线性的分类器,即线性可分支持向量机,又称为硬间隔支持向量机;

当训练数据近似线性可分时,通过软间隔最大化,也学习一个线性的分类器,即线性支持向量机,又称为软间隔支持向量机;

当训练数据线性不可分时,通过使用核技巧及软间隔最大化,学习非线性支持向量机。

试分析SVM对噪声敏感的原因

给定训练集,SVM最优决策边界由支持向量决定。

当增加噪声时,那么该噪声有极高的可能是含噪声训练集的一个支持向量,这意味着决策边界需要变。

简要介绍在深度神经网络中引入非线性激活函数的作用

不引入非线性激活函数的情况下,不管神经网络有多少层其输出都是输入的线性组合,与没有隐藏层的效果相当

在数据处理时,为什么通常要进行标准化处理

在实际问题中,我们使用的样本通常是多维数据,每一维对应一个特征,这些特征的量纲和数量级都是不一样的

这时需要对数据进行标准化处理,试所有的特征具有同样的尺度

试述将线性函数用作神经元激活函数的缺陷

如果单用线性函数作为激活函数,无论多少层的神经网络会退化成一个线性回归,不能处理非线性分类任务。

试述学习率的取值对神经网络训练的影响

如果学习率太低,每次下降的很慢,使得迭代次数非常多。

如果学习率太高,在后面迭代时会出现震荡现象,在最小值附近来回波动。

神经网络为什么会产生梯度消失,有什么解决方案

前面层上的梯度是来自于后面层上梯度的乘积。当存在过多的层次时,且激活函数的梯度小于1时,就会使前面层的梯度变得很小,更新速度过慢,导致梯度消失。

一种解决方案是使用Relu激活函数替换sigmoid,relu函数的梯度不会随着x的增大而变小,sigmoid在x取值较大时梯度趋近于0。

卷积核尺度和参数的计算

对3个32×32的特征图进行卷积层操作,卷积核10个5×5,Stride是1,pad为2,输出特征图的尺度是多少?卷积层的参数是多少?写出公式和结果。

输出尺度:(N+2P-F)/stride+1

卷积层的参数:(F×F×n+1)×N

答案:输出尺度( 32+2×2-5)/1+1 = 32

卷积层的参数 (5×5×3+1)×10=760

试析随机森林为何比决策树Bagging集成的训练速度更快

随机森林是Bagging算法的一个扩展变体,以决策树为基学习器构建Bagging集成,

Bagging在选择划分属性时需要考察结点的所有属性,而随机森林只需随机地考察一个属性子集

所以随机森林比决策树Bagging训练速度更快,泛化能力越强。

请给出L1范数和L2范数的计算方法及他们的使用场景。

L1范数为向量各个元素绝对值之和可以使权值稀疏,方便特征提取。

L2 范数为向量各个元素平方和的1/2次方可以防止过拟合,提升模型的泛化能力。

试述为什么基于L1范数可以进行特征选择。

基于L1范数的特征选择:不能直接设置最终选择特征的个数k;通过设置正则化系数λ来隐式控制k;

λ值越大,模型越关注稀疏性,得到的非零系数个数越少;

反之,非零稀疏个数越多;

可以设置一个选择特征个数的上限,通过设置不同λ值,得到满足要求的特征。

从有条件极值问题的角度来看,L1范数相当于将模型界空间限制在了L1-ball上,目标函数的等高线有很大的概率与坐标轴和边相交,这样的解具有稀疏性。

请指出数据聚类存在哪些挑战性问题

  • 能够处理高维数据:在高维空间聚类更具挑战性,随着维数的增加,具有相同距离的两个样本其相似程度可以相差很远。对于高维稀疏数据,这一点更突出。
  • 对噪声鲁棒:在实际中,绝大多数样本集都包含噪声、空缺、部分未知属性、孤立点、甚至错误数据。
  • 具有约束的聚类:在实际应用中,通常需要在某种约束条件下进行聚类,既满足约束条件,以希望有高聚类精度,是一个挑战性问题。
  • 对初始输入参数鲁棒:具有自适应的簇数判定能力,对初始聚类中心鲁棒。
  • 能够解决用户的问题:聚类结果能被用户所理解,并能带来经济效益,特别是在数据挖掘领域。

描述主成分分析的主要步骤

  1. 数据标准化
  2. 计算协方差矩阵,求协方差的特征值和特征向量。
  3. 将特征值按照从大到小的顺序排序,选择其中最大的k个,然后将其对应的k个特征向量分别作为列向量组成特征向量矩阵。
  4. 将样本点投影到选取的特征向量上。

请描述机器学习中的分类任务

根据给定的训练集,其中,要求寻找上的决策函数

请给出你对泛化误差的理解

泛化误差 = 偏差+方差+噪声

偏差:度量了学习算法的期望预测与真实结果的偏离程度,刻画了学习算法本身的拟合能力

方差:度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响

噪声:表达了在当前任务上任何学习算法所能达到的期望泛化误差的下界,即刻画了学习问题本身的难度

模型评估过程中,欠拟合和过拟合现象是什么。

过拟合是指模型对于训练数据拟合呈过当的情况,反映到评估指标上,就是模型在训练集上的表现很好,但在测试集和新数据上的表现较差。

欠拟合是模型在训练和预测时表现都不好的情况。

说出几种降低过拟合和欠拟合的方法。

降低过拟合:

  1. 从数据入手,获得更多的训练数据。使用更多的训练数据是解决过拟合问题最高效的手段,因为更多的样本能够让模型学习到更多更高效的特征。当然,直接增加实验数据一般是很困难的,但是可以通过一定的规则来扩充训练数据。比如在图像分类的问题上,可以通过图像的平移、旋转、缩放等方式扩充数据,更进一步地,可以使用生成式对抗网络来合成大量的新训练数据。
  2. 降低模型复杂度。在数据较少时,模型过于复杂是产生过拟合的主要因素,适当降低模型复杂度可以避免模型拟合过多的采样噪声。例如,在神经网络模型中减少网络层数、神经元个数等;在决策树模型中降低树的深度、进行剪枝等。
  3. 正则化方法。给模型的参数加上一定的正则约束,比如将权值的大小加入到损失函数中。
  4. 集成学习方法。集成学习是把多个模型集成在一起,来降低单一模型的过拟合风险,如Bagging方法。

降低欠拟合:

  1. 添加新特征。当特征不足或者现特征与样本标签的相关性不强时,模型容易出现欠拟合。通过挖掘“上下文特征”“ ID 类特征”“组合特征”等新的特征,往往能够取得更好的效果。
  2. 增加模型复杂度。简单模型的学习能力较差,通过增加模型的复杂度可以便模型拥高更强的拟合能力。例如,在线性模型中添加高次项,在神经网络模型中增加网络层数或神经元个数等。
  3. 减小正则化系数。正则化是用来防止过拟合的,但当模型出现欠拟合现象时,则需要针对性地减小正则化系数。

K均值算法的优缺点是什么,如何对其调优。

K均值算法缺点:例如受初值和离群点的影响每次的结果不稳定、结果通常不是全局最优而是局部最优解、无法很好地解决数据簇分布差别比较大的情况、不太适用于离散分类等。

K均值聚类的优点:主要体现在对于大数据集,K均值聚类算法相对是高效的,计算复杂度是 O(NKt) 接近于线性,其中N是数据对象的数目,K是聚类的簇数,t 是迭代的轮数。

调优方法:数据归一化,离群点预处理,采用核函数,合理选择K值。

请简述relu激活函数的优缺点

优点:

  1. 从计算的角度上,Sigmoid与Tanh激活函数均需要计算指数,复杂度高。而ReLU 只需要一个阈值即可得到激活值。
  2. ReLU的非饱和性可以有效地解决梯度消失的问题。
  3. ReLU的单侧抑制提供了网络的稀疏表达能力。

缺点:

在较大学习率设置下Relu可能会出现大量神经元死亡问题。后面神经元方向传播梯度为正,且学习率较大,Relu的梯度为1,梯度下降此时会导致该神经元的参数为负值,可能之后不会再被激活,造成神经元死亡。

补充题目

生成式模型和判别式模型的区别

生成模型估计的是联合概率分布,然后求出条件概率分布P(Y|X)作为预测的模型,即生成模型:P(Y|X)= P(X,Y)/ P(X)。

生成方法关心的是给定输入x产生输出y的生成关系。

判别模型估计的是条件概率分布,有数据直接学得决策函数P(X)或者条件概率分布P(Y|X)作为预测的模型。

判别式方法关心的是给定输入X,应该预测什么样的输出Y

逻辑回归和线性回归的异同

不同之处:

  1. 逻辑回归解决的是分类问题,因此因变量是离散的;而线性回归解决的是回归问题,因此因变量是连续的。这是两者最本质的区别
  2. 在自变量和超参数确定的情况下逻辑回归可看作广义的线性模型在因变量下服从二元分布的一个特殊情况
  3. 使用最小二乘法求解线性回归时我们认为因变量服从正态分布

相同之处:

  1. 二者在求解超参数的过程中都使用梯度下降的方法
  2. 二者都使用了极大似然估计对训练样本进行建模

距离函数的四个基本性质

  1. 非负性:
  2. 同一性:
  3. 对称性:
  4. 直递性:

随机变量x的支撑集(也就是非零值域)定义为[a,b],没有别的限制加在x上,该随机变量的最大熵分布是什么

根据最大熵模型, 推导出x概率密度函数是一个常函数,所以最大熵分布为均匀分布。

随机变量x的给定均值和方差限制在x上,该随机变量的最大熵分布是什么

根据最大熵模型推导出x概率密度函数是一个高斯分布 。

计算题

概率图

写出概率图模型联合分布的因子分解式

无向图看团,有向图看条件概率

贝叶斯网络计算概率

HMM

前向算法

后向算法

维特比解码

聚类

Kmeans:

  • 确定初始中心点
  • 计算聚类结果
  • 根据结果更新中心点

层次聚类自底向上:初始每一个点为一类,逐步合并更新中心即可,注意更新的时候要使用原始的点重新进行计算

贝叶斯

贝叶斯最小错误分类

贝叶斯最小风险

决策树

  • ID3:最大信息增益:根据类别计算经验熵,然后按照特征对类别算条件熵,两者相减,取比较大的特征作为划分的节点
  • C4.5:最大信息增益比:在ID3计算后的基础上除以每一个特征的经验熵
  • CART:最小基尼指数:外层是特征比例,内层是特征内部的类别比例

Maximum Likelihood

抛一枚硬币问题,观察数据情况是:一枚硬币包括正反两面,共抛了30次,其中12次是正面,18次是反面。采用Maximum Likelihood方法,估计正面出
现的概率和反面出现的概率。

pS96XQO.md.png

Fisher

设计题

10万张图片分类,说明模型结构和训练方法

在机器学习中常常采用基于数据驱动的方法进行图像分类。所谓基于数据驱动的方法,就是给计算机很多数据,然后实现学习算法,让计算机学习到每个类的外形的方法。基于这种方法的完整流程如下

  1. 输入:输入是包含 N 个图像的集合,每个图像的标签是 K 种分类标签中的一种。这个集合称为训练集。
  2. 学习:这一步的任务是使用训练集来学习每个类到底长什么样。一般该步骤叫做训练分类器或者学习一个模型。
  3. 评价:让分类器来预测它未曾见过的图像的分类标签,并以此来评价分类器的质量。我们会把分类器预测的标签和图像真正的分类标签对比。毫无疑问,分类器预测的分类标签和图像真正的分类标签如果一致,那就是好事,这样的情况越多越好。

[pS.md.png

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:高级人工智能-期末复习 + + /2022/12/11/UCAS/advanced-ai/advanced-ai-final/ + + 《高级人工智能》期末复习

概述部分

人工智能的三大主义:行为主义、联结主义、符号主义

pSpBeD1.md.png

图灵测试是做什么的?给几个论断,哪些是哪些不是?

图灵测试:一个人(C)在完全不接触对方(A和B)的情况下,和对方进行一系列的问答,如果在相当长时间内,他无法根据这些问题判断对方是人(B)还是计算机(A),那么,就认为该计算机具有同人相当的智能(即计算机是能思维的)。

pSpBZuR.md.png

搜索和优化部分

选择题

g(x)为从根节点到x节点的代价总和

h(x)为从x节点到目标节点的估计代价总和

代价一致搜索 f(x) = g(x)

  • 完备性:肯定能找到最优解
  • 最优性:找到的解花费最小
  • 比A*慢一些
  • 广度优先搜索是代价一致搜索的特例

贪婪搜索 f(x) = h(x)

  • 不完备
  • 不保证能找到最优解
  • 深度优先搜索是贪婪搜索的特例

A*搜索 f(x) = g(x) + h(x)

  • 启发函数可采纳的,那么,其中是到最近目标的真实耗散。
  • 启发函数可采纳的,那么A* 树搜索是最优的
  • A*图搜索与树搜索的区别在于图搜索不允许访问相同结点
  • 一致的:启发函数不仅仅要是可采纳的,沿路径的节点估计耗散值单调递增。
  • 图搜索中,如果启发函数是一致的,A* 搜索是最优的。

pSpsd56.md.png

遗传算法

pSpy9sJ.md.png

简答题

蚁群优化算法和粒子群优化算法是群体智能优化算法的两个代表,请从蚁群优化算法和粒子群优化算法中任选一个阐述其基本原理、算法过程及适用范围。

粒子群优化算法

基本原理:

粒子群优化算法中的每个粒子模拟一只鸟,代表待求解问题搜索解空间中的一个潜在解,“飞行信息”包括粒子当前的位置和速度两个状态量。每个粒子都可以获得其邻域内其它个体的信息,对所经过的位置进行评价,并根据这些信息和位置速度更新规则,改变自身的两个状态量,随着这一过程的不断进行,粒子群最终能够找到问题的近似最优解。

算法过程:

  • 初始化
    • 初始化粒子群:每个粒子的位置和速度,即
  • 循环执行如下三步直至满足结束条件
    • 计算每个粒子的适应度:
    • 更新每个粒子历史最好适应度及其相应的位置,更新当前全局最好适应度及其相应的位置
    • 更新每个粒子的速度和位置

适用范围:适用于求解连续解空间的优化问题

蚁群优化算法

基本原理:

蚁群算法是一种用来寻找优化路径的概率型算法。用蚂蚁的行走路径表示待优化问题的可行解,整个蚂蚁群体的所有路径构成待优化问题的解空间。路径较短的蚂蚁释放的信息素量较多,随着时间的推进,较短的路径上累积的信息素浓度逐渐增高,选择该路径的蚂蚁个数也愈来愈多。最终,整个蚂蚁会在正反馈的作用下集中到最佳的路径上,此时对应的便是待优化问题的最优解。

算法过程:

  • 首先将只蚂蚁随机放置在个城市,位于城市的第只蚂蚁选择下一个城市的概率为:

其中表示边上的信息素浓度,是根据距离定义的启发信息,反映了信息素与启发信息的相对重要性

  • 当所有蚂蚁完成周游后,按以下公式进行信息素更新:

其中: 为常数, 表示第只蚂蚁在本轮迭代中走过的路径,为路径长度,为小于1的常数,反映信息素挥发速度

适用范围:适用于求解离散解空间的优化问题,适用于在图上寻找最优路径

应用题

A*树搜索的最优性条件

  • 启发函数可采纳的,那么,其中是到最近目标的真实耗散。
  • 启发函数可采纳的,那么A* 树搜索是最优的

A*图搜索的最优性条件

  • 一致的:启发函数不仅仅要是可采纳的,沿路径的节点估计耗散值单调递增。
  • 图搜索中,如果启发函数是一致的,A* 搜索是最优的。

pSPRY7D.md.jpg

传教士和野人问题通常描述如下:三个传教士和三个野人在河的一边,还有一条能载一个人或者两个人的船,找到一个方法让所有的人都渡到河的另一岸,要求在任何地方野人数都不能多于传教士的人数(可以只有野人没有传教士)。

(1) 精确地形式化该问题,只描述确保该问题有解所必须的特性,画出该问题的完全状态图

pSPRNAe.md.jpg

(2) 用一个合适的算法实现和最优地求解该问题,检查重复状态是个好主意吗?

采用先深搜索、先广搜索以及图搜索都可以,注意检查重复状态,重复状态的检测避免程序陷入死循环。

(3) 这个问题的状态空间如此简单,你认为为什么人们求解他却很困难?

虽然状态空间比较简单,但是要检测重复状态是一个困难:另外,在当前状态选取下一个合法状态,要能够不漏举所有合法状态也存在困难,当在某个状态无下一个合法状态时,需要回溯,这些都使得人为求解它变得困难

逻辑部分

选择题

pSpsjiV.md.pngpSpyldI.md.pngpSpyGJf.md.png

简答题

命题逻辑

已知知识库里包含如下的句子:

请用归结原理证明该知识库蕴含如下的句子:$\neg A \land \neg B $

Forward chain 证明7<3+9

pSPdsmR.md.jpg

kb中所有句子都为definite子句,请构造一种真值指派使得kb中所有子句为真

将所有的原子命题指派为True即可。

  1. 由于是definite子句,不可能包含负文字,只能包含正文字,因此单独的文字一定为正文字,也就一定为True
  2. 由于是definite子句,每一个非文字的子句中一定有一个文字是正文字,且子句内部一定使用析取符号连接,因此正文字一定为True,子句也一定为True
  3. 综上,所有子句都为True

pSPdafU.md.jpg

归结原理及证明:

pSPdwpF.md.jpgpSPdB6J.md.jpg

设计一个可靠但不完备的规则

  • 知识库中是全部有理数的集合
  • 算法:,为全部自然数的集合
  • 因此算法是可靠的,但是并不完备,因为算法无法计算出任何的小数

描述语义蕴含、的作用

  • 语义蕴含指的是有了知识表示后,额外推出其他的知识
  • 是命题逻辑里面的连接词,用于知识表示(实际上是可以替代的,但是引入这个符号进行知识表示比较方便)

设计A*启发式函数来使归结次数最少

构想一个A启发式函数,使得A归结结果为最优,并证明

h(n)为集合中的最短子句的长度

一阶谓词逻辑

胜者为王,败者为寇

不到长城非好汉,到了长城就是好汉;两个句子是否语义等价,并证明

成绩好的人都很刻苦,刻苦的人,一定成绩好;两个句子是否语义等价,并证明

理发师只给不给自己理发的人理发

pSPd0l4.md.jpg

将如下的一阶谓词逻辑的句子转化为合取范式:(不需要包含存在量词)

构造一个一阶谓词逻辑的知识库和句子,使得的归结过程永远不会停止。

pSPdUYT.md.jpg

模糊逻辑

(刻画模糊量词、模糊修饰词等)

很少有成绩好的学生特别贪玩

  • 模糊谓词:贪玩、成绩好
  • 模糊修饰词:很、特别
  • 模糊量词:很少

很少有成绩好的学生特别喜欢玩游戏

  • 模糊谓词:贪玩、喜欢玩游戏
  • 模糊修饰词:很、特别
  • 模糊量词:很少

Prolog

普通编程的步骤:了解问题-收集条件-寻找解决方法-编程解决-将问题数据化-用程序运行数据-debug

逻辑编程的步骤:了解问题-收集条件-不寻找解决方法-将条件写进KB-将问题转换为fact-问query-寻找错误的事实

C :- A,B 如果AB,则implyC(definite 子句)

[E | L]:将list拆解成第一个是E,后面的剩下

trace 和 notrace是debug的过程

DFS+backward chaining

不教程序怎么算,只列出事实

Prolog缺点:

  • 不做occur check,因此有些事实是错的但是也可能推导出来,也就是不sound
  • DFS可能造成无穷递归,对的也导不出来,不complete。与语句编写的顺序也有关系

深度学习部分

选择题

pSpsvGT.md.pngpSpsxRU.pngpSpyEi6.md.png

GNN

谱方法:在谱空间中定义卷积:

  • 通过图傅里叶变换和卷积原理定义卷积
    • 图数据符合幂律分布,造成了极大的挑战
  • 主要挑战是在谱空间定义的卷积在结点空间并没有局部化

空间方法:在向量空间中定义卷积

  • 卷积被定义为目标结点到它的所有邻居的一个加权平均函数
  • 主要挑战是邻域的大小在结点之间差异很大,可能服从幂律分布

谱方法是空间方法的特例

  • 谱方法通过特别的空间变换定义核函数
  • 空间方法直接定义核函数

聚合,更新是什么?

图神经网络的框架:聚合邻居节点的信息从而更新中心节点的表示

GraphSAGE:从每个结点开始随机游走,采样个结点,不用临近度指标判断。然后通过聚合函数进行参数共享

图卷积网络(GCN):通过归一化的拉普拉斯矩阵从不固定数量的邻居结点中聚合信息,通过特征变换共享参数

应用题

证明感知机不能表示异或逻辑

异或的逻辑为:

000
101
011
110

两个变量的感知机模型为

代入上面的异或逻辑:

  1. ,则
  2. ,则
  3. ,则
  4. ,根据上面三个式子是明显不可满足的

因此感知机不能表示异或逻辑

设计用于异或问题的二层感知机

z4tj3V.md.jpg

(以下简答题目答案来源于shmily

描述BP算法

BP算法由正向传播与反向传播两个过程组成。正向传播时,输入由输入层经过隐藏层到输出层;反向传播时,输出结果与真实结果通过损失函数计算误差,误差信号再沿相反方向传播至输入层,获得各层各单元的误差信号(梯度),并将其作为修正权值的依据。通过梯度下降算法更新权值,使得网络的整体误差迭代减小。

试论述在深度神经网络中BP算法遇到的困难,并说明为什么会出现“梯度消失”问题

当网络变深时,BP算法会遇到梯度消失或者梯度爆炸的现象,此时浅层的神经元几乎接受不到来自输出层的误差信号或者误差太大,无法更新其参数或参数剧烈波动。

根据链式求导法则,浅层参数的梯度来源于深层参数梯度的乘积。由于中间梯度矩阵的范数可能远小于1,再加上许多激活函数的导数小于1,随着传播层数的增多,误差信号反向传播的过程中以指数形式衰减,当传播到浅层时便出现了梯度消失现象。

简述对抗式生成网络(GAN)的基本原理及其学习算法

GAN的思想来源于博弈论当中的均衡理论,其由生成器G与判别器D构成。生成器G希望生成更接近于真实分布的数据,判别器则希望尽可能分辨所给数据是由生成器生成的还是从真实分布中采样的。

GAN的学习算法交替地更新判别器D与生成器G:

首先训练判别器D,

  1. 从真实分布采样数据
  2. 从高斯分布采样数据,送入生成器G生成数据
  3. 最小化损失函数

接着训练生成器G,

  1. 从高斯分布采样数据,送入生成器G生成数据
  2. 最小化损失函数

重复进行以上各步骤直至收敛。

描述ResNet(ResNet的原理和结构图)

ResNet由如下多个Residual Block堆叠构成

pSPRJ0O.png

残差网络容易优化恒等式函数,学习优化残差映射比原始映射更加容易,随着网络加深,网络至少不会变得更差,有效地缓解了梯度消失等现象;此外,残差连接隐式地扩展了模型的特征空间,可以看作一种模型集成。

利用RNN构建一个翻译器

采用编码器-解码器结构,二者都是RNN网络,示意图如下:

pSPRGnK.png

其中,编码器RNN接受输入(原文token) ,并通过RNN结构编码隐藏状态。编码器编码完成后所有隐藏状态聚合为背景向量

解码器的RNN同样编码隐藏状态,并将编码的隐藏状态映射到预测结果,计算间的损失来完成模型的训练

预测时,通过自回归与束搜索的方式得到翻译序列。

强化学习部分

选择题

强化学习基础

多臂赌博机:

一台赌博机有多个摇臂,每个摇臂摇出的奖励大小不确定,玩家希望摇固定次数的臂所获得的期望累积奖励最大

优化目标:期望累计奖励最大化

探索和利用的关系:

  • 利用:按照贪心策略进行选择,最大化即时奖励
  • 探索:选择贪心策略之外的行为,短期奖励会比较低,长期奖励会比较高

策略:

  • 贪心策略
  • 贪心策略
    • 以概率按照贪心策略进行行为选择(利用)
    • 以概率在所有行为中随机选择一个(探索)
  • 乐观初值法:未发生之前,保持乐观的心态。每次摇完臂都会失望,所以下次会换个臂摇,鼓励探索
  • UCB行为选择策略:对Qt(a)做估计,但因为估不准(估不准与之前尝试的次数有关,尝试次数越多估的越准),所以对它做一个上界

pSpWfmD.md.png

马尔可夫状态过程的要素:

  • 智能体(Agent)和环境(Environment)按照离散的时间步进行交互
  • 智能体的状态S、智能体采取的行为A、获得的奖励R

pSpfnhR.md.png

奖励假设:最终目标是通过最大化累积的Reward实现的

策略学习方法:

  • 动态规划
    • 策略迭代:从初始策略开始,迭代进行策略估值和策略提升,最终得到最优策略
      • 策略估值:解给定的策略下的值函数,也就是预测当前策略下所能拿到的值函数问题。
      • 策略提升:根据当前策略的估值函数,寻找更优的策略(如果存在)
    • 估值迭代:值迭代算法是策略评估过程只进行一次迭代的策略迭代算法,从初始状态估值开始,进行估值迭代,找到最优状态估值,按照贪心方式得到最优策略
    • 从运算量角度看,值迭代方法中策略评估只需要一次迭代,需要的运算量更小,应该比策略迭代更快收敛。但是,通常在策略提升中间插入需要多次迭代的策略评估的算法,收敛的更快!这可能与值迭代算法的终止条件有关。值迭代算法的终止条件对象为值函数,策略迭代算法的终止条件对象为策略,结合之前gridworld中观察的现象(策略可能比值函数收敛的更快),所以策略迭代可能比值迭代更快收敛。
  • 蒙特卡洛:(通过采样的方式,最后用样本的平均值作估值,是一种从经验中获得的方法)
    • 从真实或者模拟的经验中计算状态(行动估值函数)不需要关于环境的完整模型
    • 直接根据真实经验或模拟经验计算状态估值函数
    • 不同状态的估值在计算时是独立的,不依赖于“自举”方法
  • 时序差分:非平稳情形下的蒙特卡洛方法(恒定步长)
  • 参数近似

pSpyJW8.md.png

博弈部分

博弈的要素

  • 局中人:在博弈中有权决定自己行动方案的博弈参加者
  • 重要假设:局中人是自私的理性人
  • 策略:博弈中可供局中人选择的行动方案
  • 效用函数:对每个参与博弈的局中人,都有一个相应的效用函数,每个局中人的目的都是最大化自己的效用

剪刀石头布:所有玩家的收益之和为0-零和博弈

最佳应对:针对局中人2的策略t,若局中人1用策略s产生的收益大于或等于其任何其他策略,则称策略s是局中人1对局中人2的策略t的最佳应对

纳什均衡:如果一个局势下,每个局中人的策略都是相对其他局中人当前策略的最佳应对,则称该局势是一个纳什均衡

帕累托最优:对于一组策略选择(局势)若不存在其他策略选择使所有参与者得到至少和目前一样高的回报,且至少一个参与者会得到严格较高的回报,则这组策略选择为帕累托最优。(“不可能再改善某些人的境况,而不使任何其他人受损。”)

社会最优:使参与者的回报之和最大的策略选择,社会最优的结果一定也是帕累托最优的结果

pSpytSS.md.png

应用案例:

  • 首价密封报价拍卖
    • 纳什均衡:每个竞拍者的报价低于其对商品的估价
    • 最优报价低于估价,竞拍者越多,报价越接近于估价
  • 次价密封报价拍卖
    • 纳什均衡:每个竞拍者会倾向于采用其对商品的估价进行报价

讨价的对象是双方对商品估价之差

pSpyFd1.md.png

maxmin策略:最大化自己最坏情况时的效用

  • 最小化损失,控制风险
  • 预防其它局中人的不理性给自己带来损失

minmax策略:最小化对手的最大效用

零和博弈情况下:

  • minmax和maxmin是对偶的
  • minmax策略和maxmin策略等价于纳什均衡策略

pSpyNQg.md.png

匹配市场:

  • 完全匹配:对于两类节点集合大小一样的二部图,选择数目和节点个数一样的边,使得每类节点中的任意一个节点在另一类节点中都有唯一的对应者
  • 最优匹配:效用最大的匹配,最优匹配对于个体而言不一定最优,甚至是最差的

市场结清价格:给定买方报价的情况下,如果卖方的某种价格使得对应的买方偏好图中存在完全匹配,则称卖方的这组价格为市场结清价格。市场结清价格总是存在,且使得买卖双方总效用最优。

pSpyaLj.md.png

议价权:

不稳定边:对于结局中未参与配对的边,如果边的两个端点获得的收益之和小于1,则称这条边为不稳定边,不稳定边的存在意味着其两个端点可以通过改变报价而改变结局

稳定结局:如果一个结局中不存在不稳定边,则称该结局为稳定结局

纳什议价解:

  • A的备选项收益为
  • B的备选项收益为
  • 分配剩余价值
  • 纳什议价解:一人一半就好
    • A的收益是
    • B的收益是

均衡结局:给定一个结局,如果结局中的任意一个参与配对的边都满足纳什议价解的条件,则称该结局是均衡结局

均衡结局一定是稳定结局

pSpykIx.md.png

因果学习

画一个图,什么什么路径,上课那种,阻断、D分离

pSp5cUe.md.png

后门准则:Z满足关于(X,Y)的后门准则

  • Z阻断了X与Y之间的每条含有指向X的路径(后门路径)
  • Z中没有X的后代节点

pSp5g4H.md.png

应用题:格子游戏

zf5LVA.md.png

zf71aj.md.jpg

zf7lZQ.md.jpg

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + 研究生课程:模式识别与机器学习-期末复习 + + /2022/12/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-final/ + + 《模式识别与机器学习》期末复习

第1章 引言

模式识别:利用计算机对物理对象进行分类,在错误概率最小的条件下,使识别的结果尽量与客观物体相符合

在特征空间和解释空间之间找到一种映射关系:

机器学习:利用大量的训练数据,获得产生数据的模式或预测

第2章 统计判别

贝叶斯

pSirOKS.md.png

作为统计判别问题的模式分类

zIkB5j.md.jpgzIk0aQ.md.jpgzIkUr8.md.jpg

正态分布模式的贝叶斯分类器

zbf478.md.jpgzbfh0f.md.jpg

第3章 判别函数

线性判别函数

什么是线性判别函数?

统计模式识别中用以对模式进行分类的一种最简单的判别函数称为线性判别函数。线性判别函数的一般形式是,其中是特征向量的增广形式,是权重系数。根据的取值进行分类,这个函数在几何上一般表现为直线(高维空间的超平面),所以称之为线性判别函数。

为什么需要非线性判别函数?

对于复杂的实际应用,线性分类器往往无法满足要求,不同类别的样本之间并不总是线性可分的,比如著名的异或问题,这就需要寻找能够实现非线性分类的判别函数分类器。

多类情况:

  • 多类情况1:用M个判别函数将属于这一类的和不属于这一类的分开,也就是分类成功只能有一个大于0的
  • 多类情况2:用M*(M-1)/2个判别函数,两两进行分类,只有这一类关于其他所有类的判别函数都大于0时才算分类成功
  • 多类情况3:M个判别函数,没有不确定区域的多类情况2,判别函数比较大小即可

权重分量数量计算:的维度,为多项式次数。

Fisher线性判别

pSPdNkV.md.jpg

感知器

z5CLon.md.jpgzIkaqS.md.jpg

多类情况增广向量不需要变为负数,要求这个类别的比其他的类别都要大,否则这个类别+样本,其他的类别-样本

H-K算法可以发现类别不可分的情况

第4章 特征选择和提取

K-L变换

zIkwVg.md.jpg

第5章 统计机器学习基础

期望风险:机器学习算法的目标就是降低式所示的期望泛化误差(这个数据量被称为风险),选择期望风险最小的模型。

经验风险:用训练集的分布代替真实情况下的数据分布,最小化训练集上的期望损失

结构风险:在经验风险最小化的基础上再引入参数的正则化来限制模型能力,使其不要过度地最小化经验风险

偏差方差和噪声

简述偏差方差分解及其推导过程,并说明偏差、方差和噪声三部分的内在含义

pSpCJmD.md.pngzqsmin.md.jpg

过拟合和欠拟合

pSi6VhR.md.pngpSi6QBD.md.png

过拟合:当学习器把训练样本学的“太好”了的时候,很可能已经把训练样本自身的一些特点当作了所有潜在样本都会具有的一般性质,在训练集上效果好。但是在测试集上效果差,这样就会导致模型的泛化性能下降。

欠拟合:模型尚未学习到数据的真实结构。在训练集和验证集上的性能都很差。

如何判断一个模型处在过拟合状态还是欠拟合状态?

  • 欠拟合情况 :随着训练样本数增大,训练集得分和验证集得分相差不大,并且得分都不高。
  • 过拟合情况 :随着训练样本数增大,训练集得分上升的同时和验证集得分下降。

给出3种减轻模型过拟合的方法:

过拟合:

  • 获得更多数据
  • 降低模型复杂度
  • 特征选择
  • 早停
  • 正则化
  • 添加噪声

欠拟合:

  • 增加特征数
  • 增加模型复杂度
  • 减小正则化参数

假设某研究者在 ImageNet 数据上使用线性支持向量机 Linear SVM 来做文本分类的任务,请说明在如下情况下分别如何操作才能得到更好的结果, 并说明原因。

  • 训练误差5%,验证误差10%,测试误差10%
    • 训练、验证和测试误差都很大,模型处于欠拟合状态,可以选择将正则化参数C值适当调大,增大模型的复杂度
  • 训练误差1%,验证误差10%,测试误差10%
    • 训练误差比较小,验证和测试误差比较大,模型处于过拟合状态,可以选择进行数据增强、或者将C值适当调小,增加模型泛化能力
  • 训练误差1%,验证误差3%,测试误差10%
    • 训练和验证误差比较小,测试误差比较大,说明训练数据和测试数据的分布差别比较大,可以重新采样或者shuffle数据

如果使用SVM做二分类问题得到如下结果,分别应该采取什么措施以取得更好的结果?并说明原因。

  • 训练集的分类准确率90%,验证集的分类准确率90%,测试集的分类准确率88%
    • 训练、验证和测试准确率都很低,模型处于欠拟合状态,可以选择将正则化参数C值适当调大,增大模型的复杂度
  • 训练集的分类准确率98%,验证集的分类准确率90%,测试集的分类准确率88%
    • 训练准确率比较高,验证和测试准确率比较低,模型处于过拟合状态,可以选择进行数据增强、或者将C值适当调小,增加模型泛化能力

如果使用逻辑回归算法做二分类问题得到如下结果,分别应该采取什么措施以取得更好的结果?并说明理由。

  • 训练集的分类准确率85%,验证集的分类准确率80%,测试集的分类准确率75%
    • 训练、验证和测试准确率都很低,模型处于欠拟合状态,可以选择增加训练特征,使用更多的训练参数
  • 训练集的分类准确率99%,验证集的分类准确率80%,测试集的分类准确率78%
    • 训练准确率比较高,验证和测试准确率比较低,模型处于过拟合状态,可以选择减少训练特征,添加正则项,增加数据量等等

第6章 有监督学习方法

pSiybnS.md.pngpSiyXkj.md.pngpSiyjts.md.png

公式推导相关

2018-2019

pSit5Af.md.pngpSitgcd.md.jpgpSCUIW4.md.pngpSiNZE6.md.pngpSiNm4O.md.pngpSiN1KA.md.pngpSiNUPS.md.png

2021-2022

pSCdBvj.md.png

pSitc1H.md.jpg

第7章 支持向量机

pSi6S10.md.pngpSi6pcV.md.pngpSi6Y9I.md.png

径向基函数(RBF)gamma和C的影响:

  • 参数gamma定义了单个训练样本的影响大小,值越小影响越大,值越大影响越小。参数gamma可以看作被模型选中作为支持向量的样本的影响半径的倒数。gamma越大半径越窄,因此如果欠拟合需要增大gamma,分的更准
  • 参数C在误分类样本和分界面之间进行权衡。低的C值使分界面平滑,而高的C值通过增加模型自由度以选择更多支持向量来确保所有样本都被正确分类。因此如果欠拟合要增大C

最小化VC维h等价于最大化间隔,使分类器的复杂度小!

简述SVM算法的原理

z5CjJ0.md.jpgz5CXiq.md.jpg

第8章 聚类

pSi6s4s.md.png

K均值:CE

密度:AF

高斯混合:BD

Kmeans:Kmeans的判别界面应该是簇的中垂线

  • 一种经典的聚类算法,简单、快速
  • 假定簇为球形且每个簇的概率相等
  • 能处理大规模数据,可扩展型好
  • 当簇接近高斯分布时,效果较好
  • 当簇具有不同的尺寸、密度、非球形,Kmeans可能得不到理想的聚类结果
  • 硬划分数据点到簇,当数据上出现一些小的扰动,可能导致一个点划分到另外的簇

K-Means与GMM

K-Means

  • 损失函数:最小化平方距离的和
  • 样本点硬划分到某个簇
  • 假定样本属于每个簇的概率相等,且为球形簇

GMM

  • 最小化负对数似然
  • 点到簇的从属关系为软分配
  • 可以被用于椭球形簇,且各个簇概率不同

层次聚类:最小距离层次聚类可以做同心圆相关聚类

  • 对噪声和离群点敏感
  • 比较难处理不同尺寸的簇和凸的簇
  • 成链,误把大簇分裂

DBSCAN

  • 各种大小、各种形状的簇,不需要明确簇的数量
  • 具有一定的抗噪音特性
  • 参数选择比较困难
  • 不适合密度差异较大的数据集
  • 时间慢

pSCaYhF.md.png

第9章 降维

PCA的优化目标:

  • 最大化映射后的样本方差角度
  • 最小重建误差角度

第10章 半监督学习

基本假设

平滑假设:如果高密度区域中两个点距离较近, 那么对应的输出也应该接近

聚类假设:如果两个点在同一个簇,那么它们很有可能属于同一个类别

  • 等价形式:低密度分割(决策边界应该在低密度区域)

流形假设:输入空间由所有数据点所在的多个低维流形构成,位于同一流形上的数据点具有相同的标签,流形上距离近的点的标签相似

具体算法

自我训练算法:假设输出的高度置信的预测是正确的

协同训练:假设特征可分裂单独对于训练一个好的分类器是充分的,在给定类别后是条件独立的

生成式模型:假设所有数据(带标签&不带标签)都由一个潜在的模型生成(GMM,HMM,朴素贝叶斯)

半监督支持向量机:假设来自不同类别的无标记数据之间会被较大的间隔隔开

  • C2很小表达对未标注样本错分的容忍度比较大,很大表示不容忍错分,每一个未标注样本也要分对

基于干扰的半监督:基于连续性假设:考虑对输入稍加改变,得到其增广表示,模型对的预测和对原始数据点的预测相似。

基于图的半监督学习:假设在所有数据点(标注数据和无标注数据)定义一个相似性图,相似的数据点之间存在边,边的权重表示两个数据点之间的相似程度,相似图中“紧密”连接的点趋向于有相同的标签

第11章 概率图模型

贝叶斯球:

pSSLlbq.md.png

z7Vp0P.md.jpg
z7VSmt.md.jpg

HMM

zoUqK0.md.jpg

前向算法

zoUjVU.md.jpg
zoUObT.md.jpg

维特比算法

zoULrV.md.jpgzoUvaF.md.jpg

第12章 集成学习

bagging

降低模型的方差,偏差不变

原理:通过对训练样本进行bootstrap采样(有放回的随机采样),然后训练多个模型,最后对多个模型作平均,得到最后的融合模型。

Bagging适合对偏差低、方差高的模型进行融合,如决策树、神经网络等

boosting

降低模型的偏差,方差不变

原理:每次迭代顺序的把一些模型加进去,最后一些子模型的加权平均是我们最后的集成模型

Adaboost

Adaboost:在弱学习器失败的样本上,学习第二个弱学习器

开始初始化的时候每个样本的权重相同

分对的样本,其权重除以,权重减小

分错的样本,其权重乘以,权重增大

最后对模型进行加权融合

Adaboost 原理:先从初始训练集训练出一个学习器,再根据基学习器的表现来对训练样本分布进行调整,使得先前基学习器做错的训练样本在后续得到更多的关注,然后基于调整后的样本分布来训练下一个基学习器;如此重复进行,直到基学习器达到事先指定的值T,最终将这T个基学习器进行加权结合。

Adaboost 损失函数:使用指数损失函数

Adaboost算法流程:

pSigIpR.md.png

为什么AdaBoost经常可以在训练误差为0后继续训练还可能带来测试误差的持续下降?

在训练误差下降到接近0的时候,更多的训练,会增加分类器的分类margin,这个过程也能够防止测试误差的上升,随着Margin的变大,测试误差会逐渐收敛。

AdaBoost优缺点:

优点:实现快速简单、灵活、通用性高

缺点:AdaBoost性能取决于数据和弱学习器,如果弱分类器过于复杂,可能会产生过拟合情况,如果弱分类器太弱有可能造成欠拟合,还容易收到均匀噪声的影响。

第13章 深度学习

神经元的结构

pSpSD6U.md.png

激活函数

Sigmoid函数:

在早期的神经网络中较为普遍,逐渐被更简单的ReLU函数取代

容易导致梯度消失问题:

  • 导数最大值为0.25:反向传播时,返回网络的误差将会在每一层收缩至少75%
  • 尾部是饱和的,对应的梯度接近0,导致消失梯度问题

Tanh函数:形状和sigmoid函数的形状很像,但tanh函数在坐标系的原点上对称:使用tanh激活函数收敛会更快,减轻消失梯度的现象

ReLU函数:

  • 计算量小,不涉及除法
  • 一部分神经元的输出为0:网络稀疏,减少了参数的相互依存关系,缓解过拟合
  • 时,导数为,解决了梯度消失问题,收敛速度更快
  • 时,导数为,无法更新权重

神经网络

pSpSLtI.md.pngpSpSOht.md.png

梯度消失和梯度爆炸

梯度爆炸:梯度值超出范围:无穷大值

对学习率敏感

  • 学习率较大-更大的权重-更大的梯度
  • 学习率太小-模型训练没有进展
  • 可能需要在训练期间大幅改变学习率

梯度消失:梯度值趋近0

无论如何选择学习率,训练都没有进展

只有顶层训练有效,底层训练基本无效,使网络更深可能并没有更好

模型的深度增加,梯度会逐渐消失:

  • 将sigmoid激活函数换成其他的激活函数
  • Resnet:通过跳接的方式,优化残差映射比直接优化原始映射更容易,带有集成学习的思想
  • Batch normalization,还能带来正则化的效果

其他技巧:

  • batch normalization 会使得我们的训练对好多因素(学习率、初始化)的要求没有那么高
  • 参数初始化,或者采用预训练网络作初始化或者核初始化
  • mini-batch的梯度下降
  • 动量法梯度下降:移动量不仅与梯度有关,还与前一时刻的移动量有关。
  • Adam:同时利用一阶动量和二阶动量进行优化

过拟合

  • 早停
  • 正则:L1正则能让系数=0,L2可以让参数趋向于变小,对整体的影响就变小了,相当于参数变简单了
  • Dropout:随机删除一部分神经元(可视为一种集成学习)
  • 数据增强:增加训练样本集尽可能让他多样化,也可以增加模型的泛化能力

卷积神经网络

  • 局部连接:我们认为是一个模式在一个比较小的范围内,而不是要看全局,有些模式比整个图像小得多,神经元不需要看整幅图像就能发现模式,与一个小区域连接所需的参数更少
  • 权值共享:同一个模式会在图像中不同的区域出现,不同位置可能都有这样的模式,这样做可以使得模型的参数变少
  • 池化:对像素进行下采样不影响目标,通过下采样可以让图像更小,网络处理图像需要的参数更少
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Pattern Recognition and Machine Learning + + + +
+ + + + + Go项目-分布式缓存GeeCache + + /2022/12/05/Go/Go-Project-Geecache/ + + Go项目-分布式缓存GeeCache

GeeCache

完成的功能

  • 单机缓存和基于 HTTP 的分布式缓存
  • 最近最少访问(Least Recently Used, LRU) 缓存策略
  • 使用 Go 锁机制防止缓存击穿
  • 使用一致性哈希选择节点,实现负载均衡

LRU 缓存淘汰策略

FIFO:先淘汰缓存中最早添加的记录

LFU:淘汰缓存中访问频率最低的记录,需要维护一个访问频率的表

LRU:最近最少使用,认为如果数据最近被访问过,那么将来被访问的概率也会更高。维护一个队列,如果一条记录被访问,则移动到队列尾端,这样保证队首一定是最近最少访问的数据

package lruimport "container/list"// LRU cache 结构体type Cache struct {maxBytes  int64                         // 允许使用的最大内存nbytes    int64                         // 当前已使用的内存ll        *list.List                    // cache链表cache     map[string]*list.Element      // 查找键值对的字典OnEvicted func(key string, value Value) // 某条记录被移除时的回调函数}// 双向链表节点的数据类型// 主要目的是为了删除节点后能从字典中删除该键值对type entry struct {key   stringvalue Value}// 值的类型可以是任意的,定义一个空接口,实现Len()方法返回值的占用空间大小// Len the number of cache entriesfunc (c *Cache) Len() int {return c.ll.Len()}type Value interface {Len() int // 包含一个方法返回值占用的内存大小}// 工厂模式,返回实例化的cachefunc New(maxBytes int64, onEvicted func(string, Value)) *Cache {return &Cache{maxBytes:  maxBytes,ll:        list.New(),cache:     make(map[string]*list.Element),OnEvicted: onEvicted,}}// 查找功能,在字典中进行查找,然后移动到队尾(Front)func (c *Cache) Get(key string) (value Value, ok bool) {if ele, ok := c.cache[key]; ok {c.ll.MoveToFront(ele)kv := ele.Value.(*entry)return kv.value, true}return}// LRU删除策略:从队首(Back)拿到节点,然后将其删除func (c *Cache) RemoveOldest() {ele := c.ll.Back()if ele != nil {c.ll.Remove(ele)kv := ele.Value.(*entry)delete(c.cache, kv.key)c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) // 更新当前已经使用的内存if c.OnEvicted != nil {c.OnEvicted(kv.key, kv.value)}}}// 新增节点/修改节点func (c *Cache) Add(key string, value Value) {// 如果在链表中找到则将其更新,同时更新占用的空间大小等,并移动到队列尾端if ele, ok := c.cache[key]; ok {c.ll.MoveToFront(ele)kv := ele.Value.(*entry)c.nbytes += int64(value.Len()) - int64(kv.value.Len())kv.value = value} else { // 如果找不到则直接插入ele := c.ll.PushFront(&entry{key, value})c.cache[key] = elec.nbytes += int64(len(key)) + int64(value.Len())}// 如果占用空间超过了链表的最大空间,则删除掉队首的节点for c.maxBytes != 0 && c.maxBytes < c.nbytes {c.RemoveOldest()}}

单机并发缓存

多个协程(goroutine)同时读写同一个变量,在并发度较高的情况下,会发生冲突。确保一次只有一个协程(goroutine)可以访问该变量以避免冲突,这称之为 互斥,互斥锁可以解决这个问题。

当一个协程调用了 Lock() 方法时,其他协程被阻塞了,直到 Unlock()调用将锁释放。因此被包裹部分的代码就能够避免冲突,实现互斥。

抽象了一个只读数据结构 ByteView 用来表示缓存值:

package geecache// 只读数据结构用来表示缓存值type ByteView struct {b []byte}// 返回缓存值的长度func (v ByteView) Len() int {return len(v.b)}// 返回拷贝从而防止这个值被外部操作修改func (v ByteView) ByteSlice() []byte {return cloneBytes(v.b)}// 将数据作为一个字符串进行返回func (v ByteView) String() string {return string(v.b)}func cloneBytes(b []byte) []byte {c := make([]byte, len(b))copy(c, b)return c}

为 lru.Cache 添加并发特性(加锁):

package geecacheimport ("Go-Projects/GeeCache/lru""sync")type cache struct {mu         sync.Mutexlru        *lru.CachecacheBytes int64}func (c *cache) add(key string, value ByteView) {c.mu.Lock()defer c.mu.Unlock()// 延迟初始化if c.lru == nil {c.lru = lru.New(c.cacheBytes, nil)}c.lru.Add(key, value)}func (c *cache) get(key string) (value ByteView, ok bool) {c.mu.Lock()defer c.mu.Unlock()if c.lru == nil {return}if v, ok := c.lru.Get(key); ok {return v.(ByteView), ok}return}

Group 是 GeeCache 最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。

在缓存不存在时,调用这个函数,得到源数据:

type Getter interface {Get(key string) ([]byte, error)}// 定义函数类型 GetterFunc,并实现 Getter 接口的 Get 方法type GetterFunc func(key string) ([]byte, error)func (f GetterFunc) Get(key string) ([]byte, error) {return f(key)}

核心Group:

// 缓存的命名空间type Group struct {name      string // 每个Group拥有一个唯一的名称getter    Getter // 缓存未命中时的回溯mainCache cache  // 并发缓存}var (mu     sync.RWMutexgroups = make(map[string]*Group))// 创建Group实例,并且将group的名称存在全局变量中func NewGroup(name string, cacheBytes int64, getter Getter) *Group {if getter == nil {panic("nil Getter")}mu.Lock()defer mu.Unlock()g := &Group{name:      name,getter:    getter,mainCache: cache{cacheBytes: cacheBytes},}groups[name] = greturn g}// 获取指定的groupfunc GetGroup(name string) *Group {mu.RLock()g := groups[name]mu.RUnlock()return g}

Group的Get方法,完成对缓存的查找以及未命中后的回调操作

// 找到缓存值func (g *Group) Get(key string) (ByteView, error) {// 如果没有键则报错if key == "" {return ByteView{}, fmt.Errorf("key is required")}// 从 mainCache 中查找缓存,如果存在则返回缓存值if v, ok := g.mainCache.get(key); ok {log.Println("[GeeCache] hit")return v, nil}return g.load(key)}// 缓存不存在,则调用 load 方法func (g *Group) load(key string) (value ByteView, err error) {return g.getLocally(key)}// getLocally 调用用户回调函数 g.getter.Get() 获取源数据,并且将源数据添加到缓存 mainCache 中func (g *Group) getLocally(key string) (ByteView, error) {bytes, err := g.getter.Get(key)if err != nil {return ByteView{}, err}value := ByteView{b: cloneBytes(bytes)}g.populateCache(key, value)return value, nil}func (g *Group) populateCache(key string, value ByteView) {g.mainCache.add(key, value)}

HTTP 服务端

分布式缓存需要实现节点间通信,建立基于 HTTP 的通信机制是比较常见和简单的做法。如果一个节点启动了 HTTP 服务,那么这个节点就可以被其他节点访问。

承载节点间 HTTP 通信的核心数据结构:

package geecacheconst defaultBasePath = "/_geecache/"type HTTPPool struct {self     string // 记录自己的地址,包括主机名/IP 和端口basePath string // 节点间通讯地址的前缀}// 返回HTTP实例func NewHTTPPool(self string) *HTTPPool {return &HTTPPool{self:     self,basePath: defaultBasePath,}}

实现最为核心的 ServeHTTP 方法:

// 使用服务器登录func (p *HTTPPool) Log(format string, v ...interface{}) {log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...))}// 处理HTTP请求func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {// 判断访问路径的前缀是否是 basePath,不是返回错误if !strings.HasPrefix(r.URL.Path, p.basePath) {panic("HTTPPool serving unexpected path: " + r.URL.Path)}p.Log("%s %s", r.Method, r.URL.Path)// /<basepath>/<groupname>/<key> requiredparts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2)if len(parts) != 2 {http.Error(w, "bad request", http.StatusBadRequest)return}groupName := parts[0]key := parts[1]// 通过 groupname 得到 group 实例,再使用 group.Get(key) 获取缓存数据group := GetGroup(groupName)if group == nil {http.Error(w, "no such group: "+groupName, http.StatusNotFound)return}view, err := group.Get(key)if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.Header().Set("Content-Type", "application/octet-stream")// 将缓存值作为 httpResponse 的 body 返回w.Write(view.ByteSlice())}

一致性哈希

一致性哈希算法将 key 映射到 2^32 的空间中,将这个数字首尾相连,形成一个环

package consistenthashimport ("hash/crc32""sort""strconv")// 定义了函数类型 Hash,采取依赖注入的方式,允许用于替换成自定义的 Hash 函数type Hash func(data []byte) uint32// 一致性哈希算法的主数据结构type Map struct {hash     Hashreplicas int            // 虚拟节点倍数keys     []int          // 哈希环hashMap  map[int]string // 虚拟节点与真实节点的映射表}// 允许自定义虚拟节点倍数和 Hash 函数func New(replicas int, fn Hash) *Map {m := &Map{replicas: replicas,hash:     fn,hashMap:  make(map[int]string),}if m.hash == nil {m.hash = crc32.ChecksumIEEE}return m}// 实现添加真实节点/机器的 Add() 方法func (m *Map) Add(keys ...string) {for _, key := range keys {for i := 0; i < m.replicas; i++ {hash := int(m.hash([]byte(strconv.Itoa(i) + key)))m.keys = append(m.keys, hash)m.hashMap[hash] = key}}sort.Ints(m.keys)}// 实现选择节点的 Get() 方法func (m *Map) Get(key string) string {if len(m.keys) == 0 {return ""}hash := int(m.hash([]byte(key))) // 计算 key 的哈希值// 顺时针找到第一个匹配的虚拟节点的下标 idxidx := sort.Search(len(m.keys), func(i int) bool {return m.keys[i] >= hash})return m.hashMap[m.keys[idx%len(m.keys)]]}

分布式节点

抽象 PeerPicker

package geecache// PeerPicker 的 PickPeer() 方法用于根据传入的 key 选择相应节点 PeerGettertype PeerPicker interface {PickPeer(key string) (peer PeerGetter, ok bool)}// 接口 PeerGetter 的 Get() 方法用于从对应 group 查找缓存值type PeerGetter interface {Get(group string, key string) ([]byte, error)}

节点选择与 HTTP 客户端

const (defaultBasePath = "/_geecache/"defaultReplicas = 50)type HTTPPool struct {self        string                 // 记录自己的地址,包括主机名/IP 和端口basePath    string                 // 节点间通讯地址的前缀mu          sync.Mutex             // 锁peers       *consistenthash.Map    // 新增成员变量 peers,类型是一致性哈希算法的 Map,用来根据具体的 key 选择节点httpGetters map[string]*httpGetter // 映射远程节点与对应的 httpGetter}// 实现 PeerGetter 接口type httpGetter struct {baseURL string}
// 使用 http.Get() 方式获取返回值,并转换为 []bytes 类型func (h *httpGetter) Get(group string, key string) ([]byte, error) {u := fmt.Sprintf("%v%v/%v",h.baseURL,url.QueryEscape(group),url.QueryEscape(key),)res, err := http.Get(u)if err != nil {return nil, err}defer res.Body.Close()if res.StatusCode != http.StatusOK {return nil, fmt.Errorf("server returned: %v", res.Status)}bytes, err := ioutil.ReadAll(res.Body)if err != nil {return nil, fmt.Errorf("reading response body: %v", err)}return bytes, nil}

实现 PeerPicker 接口

// Set() 方法实例化了一致性哈希算法,并且添加了传入的节点,为每一个节点创建了一个 HTTP 客户端 httpGetterfunc (p *HTTPPool) Set(peers ...string) {p.mu.Lock()defer p.mu.Unlock()p.peers = consistenthash.New(defaultReplicas, nil)p.peers.Add(peers...)p.httpGetters = make(map[string]*httpGetter, len(peers))for _, peer := range peers {p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath}}}// PickerPeer() 包装了一致性哈希算法的 Get() 方法,根据具体的 key,选择节点,返回节点对应的 HTTP 客户端func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) {p.mu.Lock()defer p.mu.Unlock()if peer := p.peers.Get(key); peer != "" && peer != p.self {p.Log("Pick peer %s", peer)return p.httpGetters[peer], true}return nil, false}

修改主方法

// 将 实现了 PeerPicker 接口的 HTTPPool 注入到 Group 中func (g *Group) RegisterPeers(peers PeerPicker) {if g.peers != nil {panic("RegisterPeerPicker called more than once")}g.peers = peers}// 使用实现了 PeerGetter 接口的 httpGetter 从访问远程节点,获取缓存值func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) {bytes, err := peer.Get(g.name, key)if err != nil {return ByteView{}, err}return ByteView{b: bytes}, nil}// 缓存不存在,则调用 load 方法// 若非本机节点,则调用 getFromPeer() 从远程获取。若是本机节点或失败,则回退到 getLocally()func (g *Group) load(key string) (value ByteView, err error) {if g.peers != nil {if peer, ok := g.peers.PickPeer(key); ok {if value, err = g.getFromPeer(peer, key); err == nil {return value, nil}log.Println("[GeeCache] Failed to get from peer", err)}}return g.getLocally(key)}

防止缓存击穿

并发了 N 个请求,假设对数据库的访问没有做任何限制的,很可能向数据库也发起 N 次请求,容易导致缓存击穿和穿透。针对相同的 key,如何做到只向远端节点发起一次请求呢?

实现了一个名为 singleflight 的 package 来解决这个问题

package singleflightimport "sync"// call 代表正在进行中,或已经结束的请求。使用 sync.WaitGroup 锁避免重入type call struct {wg  sync.WaitGroupval interface{}err error}// Group 是 singleflight 的主数据结构,管理不同 key 的请求(call)type Group struct {mu sync.Mutex // protects mm  map[string]*call}

实现 Do 方法

// 针对相同的 key,无论 Do 被调用多少次,函数 fn 都只会被调用一次,等待 fn 调用结束了,返回返回值或错误。func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {g.mu.Lock()if g.m == nil {g.m = make(map[string]*call)}if c, ok := g.m[key]; ok {g.mu.Unlock()c.wg.Wait()         // 如果请求正在进行中,则等待return c.val, c.err // 请求结束,返回结果}c := new(call)c.wg.Add(1)  // 发起请求前加锁g.m[key] = c // 添加到 g.m,表明 key 已经有对应的请求在处理g.mu.Unlock()c.val, c.err = fn() // 调用 fn,发起请求c.wg.Done()         // 请求结束g.mu.Lock()delete(g.m, key) // 更新 g.mg.mu.Unlock()return c.val, c.err // 返回结果}
]]>
+ + + + + Study + + + + + + + Go + + Backend + + Project + + + +
+ + + + + 研究生课程:高级人工智能-第14讲 强化学习 + + /2022/12/01/UCAS/advanced-ai/advanced-ai-14/ + + 《高级人工智能》课程笔记:第14讲 强化学习

第14讲 强化学习

强化学习

目标:学习从环境状态到行为的映射(即策略),智能体选择能够获得环境最大奖赏的行为,使得外部环境对学习系统在某种意义下的评价为最佳。

区别于监督学习:监督学习是从标注中学习;强化学习是从交互中学习

两种反馈

评价性反馈

  • 当智能体采取某个行为时,对该行为给出一个评价,但并不知道哪个行为是最好的
  • 强化学习经常面临的是评价性反馈

指导性反馈

  • 直接给出某个状态下的正确或最好行为
  • 独立于智能体当前采取的行为
  • 监督学习使用的是指导性反馈

强化学习的两大特性

试错搜索和延迟奖励,用于判断某一问题是否适用于强化学习求解。

强化学习需要应对的挑战

利用和探索之间的矛盾

强化学习的要素

主体:智能体和环境-状态、行为和奖励

要素:

  • 策略:状态到行为的映射,包括确定策略和随机策略两种
  • 奖励:关于状态和行为的函数,通常具有一定的不确定性
  • 价值:累积奖励或长期目标
  • 环境模型:刻画环境对行为的反馈

强化学习发展历程

  • 1911年,Thorndike 提出效果律(Law of effect),从心理学的角度探讨了强化思想:动物感到舒服的行为会被强化,动物感到不舒服的行为会被弱化
  • 1954年,马文 · 明斯基(Marvin Minsky)在其博士论文中实现了计算上的试错学习
  • 1957年,Bellman提出求解最优控制问题的动态规划方法,并提出了最优控制问题的随机离散版本,即著名的马尔科夫决策过程
  • 1960年,Howard提出马尔科夫决策过程的策略迭代方法
  • 1961年,明斯基在其论文“Steps toward artificial intelligence”中首次使用“Reinforcement learning”一词
  • 1989年,Watkins提出了Q-learning,将动态规划、时序差分、蒙特卡洛模拟三条线结合在了一起
  • 1992年,Tesauro 将强化学习成功应用到西洋双陆棋
  • 2015年,强化学习和深度学习结合: AlphaGo
  • 2017年,AlphaGo Zero

多臂赌博机

一台赌博机有多个摇臂 ,每个摇臂摇出的奖励(reward)大小不确定 ,玩家希望摇固定次数的臂所获得的期望累积奖励最大

问题形式化

行为:摇哪个臂

奖励:每次摇臂获得的奖金

表示第轮的行为,表示第轮获得的奖励

轮采取行为的期望奖励为:

假如摇臂次, 那么按照什么策略摇臂,才能使期望累积奖励最大呢?

已知时, 每次都选择最大的(贪心策略)

但是一般情况下,对于玩家而言是未知的或具有不确定性,玩家在第轮时只能依赖于当时对的估值进行选择,此时,贪心策略是在第轮 选择最大的

利用和探索

利用:

  • 按照贪心策略进行选择,即选择最大的行为
  • 优点:最大化即时奖励
  • 缺点:由于只是对的估计,估计的不确定性导致按照贪心策略选择的行为不一定是最大的行为

探索:

  • 选择贪心策略之外的行为
  • 缺点:短期奖励会比较低
  • 优点:长期奖励会比较高 ,通过探索可以找出奖励更大的行为,供后续选择

每步选择在“利用”和“探索”中二选一

如何平衡“利用”和“探索” 是关键

贪心策略形式化地表示为:,当有多个行为的同时为最大时,随机选择一个

贪心策略:

  • 以概率按照贪心策略进行行为选择(Exploitation)
  • 以概率在所有行为中随机选择一个(Exploration)
  • 的取值取决于的方差,方差越大取值应越大

行为估值方法

根据历史观测样本的均值对进行估计

约定:

  • 当分母等于0时,
  • 当分母趋于无穷大时,收敛到

行为估值时,一个行为被选择了次后的估值记为,该估值方式需要记录个奖励值

乐观初值法

行为的初始估值

  • 前述贪心策略中,每个行为的初始估值为0
  • 每个行为的初始估值可以帮助我们引入先验知识
  • 初始估值还可以帮助我们平衡exploitation 和 exploration

乐观初值法:Optimistic Initial Values

  • 为每个行为赋一个高的初始估值
  • 好处:初期每个行为都有较大机会被explore

小结

  • 多臂赌博机是强化学习的一个简化场景,行为和状态之间没有关联关系
  • 扩展情形
    • 有上下文的多臂赌博机
      • 存在多个多臂赌博机,状态表示赌博机
      • 学习状态到行为的映射
      • 但行为不改变状态
  • 更一般的情形
    • 马尔科夫决策过程

马尔科夫决策过程

  • 常用于建模序列化决策过程
  • 行为不仅获得即时奖励,还能改变状态,从而影响长期奖励
  • 学习状态到行为的映射-策略
    • 多臂赌博机学习
    • MDP学习

奖励设置

  • 设置奖励是希望智能体能达到我们期望的目标
    • 下围棋
      • 目标:赢棋
      • 奖励需要是能够实现赢棋这一目标才合适
        • 吃子多少?占领棋盘的中心?
    • 迷宫
      • 目标:尽快走出去
      • 奖励:每走一步,奖励为-1(相当于惩罚)
    • 垃圾回收机器人
      • 目标:在尽可能少的人工干预的情况下回收尽可能多的垃圾
      • 奖励:回收一个垃圾奖励+1 (等待和主动寻找获得奖励的概率不同),人工干预一次奖励-3

贝尔曼方程的作用

贝尔曼方程定义了状态估值函数的依赖关系

  • 给定策略下,每个状态的估值视为一个变量
  • 所有状态(假如有个)的估值根据贝尔曼方程形成了一个具有个方程和个变量的线性方程组
  • 求解该方程组即可得到该策略下每个状态的估值

寻找最优策略

  • 基于状态估值函数的贝尔曼最优性方程
    • 第一步:求解状态估值函数的贝尔曼最优性方程得到最优策略对应的状态估值函数
    • 第二步:根据状态估值函数的贝尔曼最优性方程,进行一步搜索找到每个状态下的最优行为
      • 注意:最优策略可以存在多个
      • 贝尔曼最优性方程的优势,可以采用贪心局部搜索即可得到全局最优解
  • 基于行为估值函数的贝尔曼最优性方程
    • 直接得到最优策略

寻找最优策略小结

求解贝尔曼最优性方程寻找最优策略的局限性

  • 需要知道环境模型
  • 需要高昂的计算代价和内存(存放估值函数)
  • 依赖于马尔科夫性
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + 研究生课程:高级人工智能-第13讲 群体智能 + + /2022/11/24/UCAS/advanced-ai/advanced-ai-13/ + + 《高级人工智能》课程笔记:第13讲 群体智能

第13讲 群体智能

群体智能

  • 群体智能指的是无智能或者仅具有相对简单智能的主体通过合作涌现出更高智能行为的特性
    • 其中的个体并非绝对的无智能或只具有简单智能,而是相对于群体表现出来的智能而言是简单的。
  • 单个复杂个体可以实现的功能,同样可以由大量简单的个体通过群体合作实现,后者的优势在于它更健壮、灵活和经济。
  • 群体智能利用群体优势,在没有中心控制的条件下,寻找解决复杂问题的新思路

集群智能:众多无智能的个体,通过相互之间的简单合作所表现出来的智能行为

博弈:具备一定智能的理性个体,按照某种机制行动,在群体层面体现出的智能

众包:设计合适的机制,激励个体参与,从而实现单个个体不具备的社会智能

集群智能

分布式 、 自组织的(自然/人造)系统表现出的一种群体智能

集群智能系统一般由一群简单的智能体构成,智能体按照简单的规则彼此进行局部交互,智能体也可以环境交互

灵感通常来自生物系统(蚁群、鸟群、兽群、粒子群)

特点:

  • 分布式:无中心控制
  • 随机性:非确定性
  • 自适应:个体根据环境进行策略调整
  • 正反馈:个体好的尝试会对个体产生正反馈
  • 自发涌现:会在群体层面涌现出一种智能

蚁群优化算法

一种解空间搜索方法,适用于在图上寻找最优路径

算法形式化:

  • 每个蚂蚁对应一个计算智能体
  • 蚂蚁依概率选择候选位置进行移动
  • 在经过的路径上留下“信息素”
  • “信息素”随时间挥发
  • “信息素”浓度大的路径在后续的选择中会以更高的概率被选取

TSP问题蚁群算法流程

蚁群大小:一般情况下,蚁群中的蚂蚁个数不超过TSP图中节点的个数

终止条件:

  • 设定迭代轮数
  • 设定最优解连续保持不变的迭代轮数

思想:局部随机搜索+自增强

缺点:

  • 收敛速度慢
  • 易于陷入局部最优
  • 对于解空间为连续的优化问题不适用

粒子群优化算法

  • 粒子群优化算法是一种基于种群寻优的启发式搜索算法 。在 1995年由Kennedy和Eberhart首先提出来的。
  • 它的主要启发来源于对鸟群群体运动行为的研究。我们经常可以观察到鸟群表现出来的同步性,虽然每只鸟的运动行为都是互相独立的,但是在整个鸟群的飞行过程中却表现出了高度一致性的复杂行为,并且可以自适应的调整飞行的状态和轨迹。
  • 鸟群具有这样的复杂飞行行为的原因,可能是因为每只鸟在飞行过程中都遵循了一定的行为规则,并能够掌握邻域内其它鸟的飞行信息。
  • 粒子群优化算法借鉴了这样的思想,每个粒子代表待求解问题搜索解空间中的一个潜在解,它相当于一只鸟,“飞行信息”包括粒子当前的位置和速度两个状态量。
  • 每个粒子都可以获得其邻域内其它个体的信息,对所经过的位置进行评价,并根据这些信息和位置速度更新规则,改变自身的两个状态量,在“飞行”过程中传递信息和互相学习,去更好地适应环境。
  • 随着这一过程的不断进行,粒子群最终能够找到问题的近似最优解。

是一种随机优化方法,通过粒子群在解空间中进行搜索,寻找最优解(适应度最大的解)

构成要素

  • 粒子群:
    • 每个粒子对应所求解问题的一个可行解
    • 粒子通过其位置和速度表示
      • 粒子在第轮的位置:
      • 粒子在第轮的速度:
  • 记录:
    • :粒子的历史最好位置
    • :全局历史最好位置
  • 计算适应度的函数-适应度:

算法过程描述

  • 初始化
    • 初始化粒子群:每个粒子的位置和速度,即
  • 循环执行如下三步直至满足结束条件
    • 计算每个粒子的适应度:
    • 更新每个粒子历史最好适应度及其相应的位置,更新当前全局最好适应度及其相应的位置
    • 更新每个粒子的速度和位置

粒子速度更新公式:

  1. 惯性项:保持原速度不变的倾向
  2. 记忆项:回到历史最好位置的倾向
  3. 社会项:走向粒子群全局最好位置的倾向
  4. 权重参数:一般取值为2
  5. 随机参数:0和1之间的随机数

算法终止条件:

  • 迭代的轮数
  • 最佳位置连续未更新的轮数
  • 适应度函数的值到达预期要求

速度更新参数:又称加速度参数,用来控制粒子当前最优位置和粒子群当前最优位置对粒子飞行速度的影响

  • :每个微粒执行局部搜索;
  • :微粒群转化为一个随机爬山法
  • :微粒逐渐移向的加权均值
  • :算法比较适合于单峰优化问题
  • :算法比较适合于多峰优化问题

惯性权重:速度冲量导致微粒按照先前速度方向继续移动。提出一个惯性权重来控制先前微粒速度的影响

粒子群优化算法和遗传算法相比

  • 遗传算法强调“适者生存”,不好的个体在竞争中被淘汰; PSO 强调“协同合作”,不好的个体通过学习向好的方向转变。
  • 遗传算法中最好的个体通过产生更多的后代来传播基因;PSO 中的最好个体通过吸引其它个体向它靠近来施加影响。
  • 遗传算法的选择概率只与上一代群体相关,而与历史无关,群体的信息变化过程是一个Markov链过程;而PSO中的个体除了有位置和速度外,还有着过去的历史信息(pBest, gBest)。

优点:

  • 易于实现
  • 可调参数较少
  • 所需种群或微粒群规模较小
  • 计算效率高,收敛速度快

缺点:和其它演化计算算法类似,不保证收敛到全局最优解

粒子群优化算法代码

import numpy as npdef cal(x):    return x*x*x-5*x*x-2*x+3x_min = -2x_max = 5p_num = 1000g_best_max = 1g_best_max_i = 0g_best_min = 1g_best_min_i = 0x_MAX = (x_max - x_min) * np.random.random_sample((p_num,)) + x_minv_MAX = (x_max - x_min) * np.random.random_sample((p_num,)) + x_minx_MIN = (x_max - x_min) * np.random.random_sample((p_num,)) + x_minv_MIN = (x_max - x_min) * np.random.random_sample((p_num,)) + x_minp_best_max = np.ones_like(x_MAX)p_best_min = np.ones_like(x_MAX)for i in range(1,10000):    f_max = cal(x_MAX)    f_min = cal(x_MIN)    t_max = cal(p_best_max)    t_min = cal(p_best_min)    p_best_max = np.where(t_max > f_max, p_best_max, x_MAX)    p_best_min = np.where(t_min < f_min, p_best_min, x_MIN)    if np.max(f_max) > g_best_max:        g_best_max = np.max(f_max)        g_best_max_i = x_MAX[np.argmax(f_max)]    if np.min(f_min) < g_best_min:        g_best_min = np.min(f_min)        g_best_min_i = x_MIN[np.argmin(f_min)]    v_MAX = v_MAX + (p_best_max - x_MAX) + (g_best_max - x_MAX)    x_MAX = x_MAX + v_MAX    x_MAX = np.where(x_MAX > x_max,x_max,x_MAX)    x_MAX = np.where(x_MAX < x_min,x_min,x_MAX)    v_MIN = v_MIN + (p_best_min - x_MIN) + (g_best_min - x_MIN)    x_MIN = x_MIN + v_MIN    x_MIN = np.where(x_MIN > x_max,x_max,x_MIN)    x_MIN = np.where(x_MIN < x_min,x_min,x_MIN)print(g_best_max_i)print(g_best_min_i)
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + Go项目-Gee Web框架 + + /2022/11/21/Go/Go-Project-Gee/ + + Go项目-Gee Web框架

完成的功能

  • 简单介绍 net/http库以及 http.Handler接口
  • 路由(router)独立出来,方便之后增强。
  • 设计 上下文(Context),封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。
  • 使用 Trie 树实现动态路由(dynamic route)解析。
  • 实现路由分组控制(Route Group Control)
  • 设计并实现 Web 框架的中间件(Middlewares)机制。
  • 实现通用的 Logger中间件,能够记录请求到响应所花费的时间,
  • 实现静态资源服务(Static Resource)。
  • 支持HTML模板渲染。
  • 实现错误处理机制。

http.Handler

Go语言内置了 net/http库,封装了HTTP网络编程的基础的接口,使用这个库:

package mainimport ("fmt""net/http")func main() {// 设置两个路由http.HandleFunc("/", indexHandler)http.HandleFunc("/hello", helloHandler)// 启动Web服务,在9999端口进行监听,处理所有的HTTP请求的实例http.ListenAndServe("localhost:9999", nil)// 最后的nil即为实现框架的入口}// 根路由func indexHandler(w http.ResponseWriter, req *http.Request) {fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)}// hello路由func helloHandler(w http.ResponseWriter, req *http.Request) {for k, v := range req.Header {fmt.Fprintf(w, "Header[%q] = %q\n", k, v)}}

使用curl进行测试:

> curl http://localhost:9999/URL.Path="/"> curl http://localhost:9999/helloHeader["User-Agent"] = ["curl/7.68.0"]Header["Accept"] = ["*/*"]

其中代码的nil就是一个接口,需要实现方法 ServeHTTP ,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。

拦截一下请求进行尝试

package mainimport ("fmt""net/http")// 定义一个空结构体,因为后面实现的是一个方法,比如在一个结构体的基础上进行实现type Engine struct{}// 实现ServeHTTP方法func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {switch req.URL.Path {case "/":fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)case "/hello":for k, v := range req.Header {fmt.Fprintf(w, "Header[%q] = %q\n", k, v)}default:fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)}}func main() {engine := &Engine{}// 多设置一个路由http.HandleFunc("/hi", indexHandler)// 启动Web服务,在9999端口进行监听,处理所有的HTTP请求的实例http.ListenAndServe("localhost:9999", engine)// 最后的nil即为实现框架的入口}// 根路由func indexHandler(w http.ResponseWriter, req *http.Request) {fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)}

测试:

> curl http://localhost:9999/helloHeader["User-Agent"] = ["curl/7.68.0"]Header["Accept"] = ["*/*"]> curl http://localhost:9999/URL.Path="/"> curl http://localhost:9999/hi404 NOT FOUND: /hi

因此就将所有的HTTP请求转向了自己的处理逻辑,代码的运行结果与之前的是一致的。

我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

因此就可以从这里入手完成这个Web框架,最终的代码结构是这样的

.├── gee│   └── gee.go└── main.go

main.go:

使用 New()创建 gee 的实例,使用 GET()方法添加路由,最后使用 Run()启动Web服务。

package mainimport ("Go-Projects/Gee/gee""fmt""net/http")func main() {r := gee.New()r.Get("/", func(w http.ResponseWriter, req *http.Request) {fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)})r.Get("/hello", func(w http.ResponseWriter, req *http.Request) {for k, v := range req.Header {fmt.Fprintf(w, "Header[%q] = %q\n", k, v)}})r.Run("localhost:9999")}

gee.go

package geeimport ("fmt""net/http")// 定义一个普遍使用的函数类型,避免后面再次定义type HandlerFunc func(http.ResponseWriter, *http.Request)// 定义路由表type Engine struct {router map[string]HandlerFunc}// 工厂模式的构造方法,返回一个实例func New() *Engine {return &Engine{router: make(map[string]HandlerFunc),}}// 将路由添加到路由表中func (engine *Engine) addRoute(method, pattern string, handler HandlerFunc) {key := method + "-" + patternengine.router[key] = handler}// 实现GET方法func (engine *Engine) GET(pattern string, handler HandlerFunc) {engine.addRoute("GET", pattern, handler)}// 实现POST方法func (engine *Engine) POST(pattern string, handler HandlerFunc) {engine.addRoute("POST", pattern, handler)}// 实现Run方法func (engine *Engine) Run(addr string) (err error) {return http.ListenAndServe(addr, engine)}// 完成统一的控制入口方法ServeHTTPfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {key := req.Method + "-" + req.URL.Pathif handler, ok := engine.router[key]; ok {handler(w, req)} else {fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)}}

测试后的效果和之前完全相同。

整个 Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。

上下文Context

最终调用的效果:

package mainimport ("Go-Projects/Gee/gee""net/http")func main() {r := gee.New()r.GET("/", func(c *gee.Context) {c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")})r.GET("/hello", func(c *gee.Context) {c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)})r.POST("/login", func(c *gee.Context) {c.JSON(http.StatusOK, gee.H{"username": c.PostForm("username"),"password": c.PostForm("password"),})})r.Run("localhost:9999")}
  • Handler的参数变成成了 gee.Context,提供了查询Query/PostForm参数的功能。
  • gee.Context封装了 HTML/String/JSON函数,能够快速构造HTTP响应。
  1. 对Web服务来说,无非是根据请求 *http.Request,构造响应 http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
  2. 针对使用场景,封装 *http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由 /hello/:name,参数 :name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

context.go

package geeimport ("encoding/json""fmt""net/http")// 给map[string]interface{}起了一个别名gee.H,构建JSON数据时,显得更简洁。type H map[string]interface{}type Context struct {// 原始的两个参数Writer http.ResponseWriterReq    *http.Request// 请求信息Path   stringMethod string// 响应信息StatusCode int}// 创建一个Context实例func newContext(w http.ResponseWriter, req *http.Request) *Context {return &Context{Writer: w,Req:    req,Path:   req.URL.Path,Method: req.Method,}}// 根据key返回用户输入的value,属于POST方法的工具func (c *Context) PostForm(key string) string {return c.Req.FormValue(key)}// 根据key返回用户输入的value,属于GET方法的工具func (c *Context) Query(key string) string {return c.Req.URL.Query().Get(key)}// 写入状态码并更改Context的状态码func (c *Context) Status(code int) {c.StatusCode = codec.Writer.WriteHeader(code)}// 帮助下面的方法快速构造响应func (c *Context) SetHeader(key, value string) {c.Writer.Header().Set(key, value)}// 构造字符串类型的响应func (c *Context) String(code int, format string, values ...interface{}) {c.SetHeader("Content-Type", "text/plain")c.Status(code)c.Writer.Write([]byte(fmt.Sprintf(format, values...)))}// 构造JSON类型的响应func (c *Context) JSON(code int, obj interface{}) {c.SetHeader("Content-Type", "application/json")c.Status(code)encoder := json.NewEncoder(c.Writer) // 流数据构造jsonif err := encoder.Encode(obj); err != nil {http.Error(c.Writer, err.Error(), 500)}}// 构造data类型的响应func (c *Context) Data(code int, data []byte) {c.Status(code)c.Writer.Write(data)}// 构造HTML类型的响应func (c *Context) HTML(code int, html string) {c.SetHeader("Content-Type", "text/html")c.Status(code)c.Writer.Write([]byte(html))}

将和路由相关的方法和结构提取出来,放到了一个新的文件中 router.go,方便我下一次对 router 的功能进行增强,

package geeimport ("log""net/http")type router struct {handlers map[string]HandlerFunc}func newRouter() *router {return &router{handlers: make(map[string]HandlerFunc),}}// 将路由添加到路由表中func (r *router) addRoute(method, pattern string, handler HandlerFunc) {log.Printf("Route %4s - %s", method, pattern)key := method + "-" + patternr.handlers[key] = handler}// 路由处理func (r *router) handle(c *Context) {key := c.Method + "-" + c.Pathif handler, ok := r.handlers[key]; ok {handler(c)} else {c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)}}

调整主框架入口gee.go

package geeimport ("net/http")// 定义一个普遍使用的函数类型,避免后面再次定义type HandlerFunc func(*Context)// 定义路由表type Engine struct {router *router}// 工厂模式的构造方法,返回一个实例func New() *Engine {return &Engine{router: newRouter(),}}// 实现GET方法func (engine *Engine) GET(pattern string, handler HandlerFunc) {engine.router.addRoute("GET", pattern, handler)}// 实现POST方法func (engine *Engine) POST(pattern string, handler HandlerFunc) {engine.router.addRoute("POST", pattern, handler)}// 实现Run方法func (engine *Engine) Run(addr string) (err error) {return http.ListenAndServe(addr, engine)}// 完成统一的控制入口方法ServeHTTPfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := newContext(w, req)engine.router.handle(c)}

测试代码

启动程序后:

2022/11/21 21:05:40 Route  GET - /2022/11/21 21:05:40 Route  GET - /hello2022/11/21 21:05:40 Route POST - /login
> curl -i http://localhost:9999/HTTP/1.1 200 OKContent-Type: text/htmlDate: Mon, 21 Nov 2022 13:05:47 GMTContent-Length: 19<h1>Hello Gee</h1>> curl "http://localhost:9999/hello?name=geektutu"hello geektutu, you're at /hello> curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'{"password":"1234","username":"geektutu"}> curl "http://localhost:9999/xxx"404 NOT FOUND: /xxx

前缀树路由

之前,我们用了一个非常简单的 map结构存储了路由表,使用 map存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。那如果我们想支持类似于 /hello/:name这样的动态路由怎么办呢?

实现动态路由最常用的数据结构,被称为前缀树(Trie树),每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。

首先设计树节点上应该存储哪些信息

type node struct {pattern  string  // 待匹配路由,例如 /p/:langpart     string  // 路由中的一部分,例如 :langchildren []*node // 子节点,例如 [doc, tutorial, intro]isWild   bool    // 是否精确匹配,part 含有 : 或 * 时为true}

将匹配的逻辑,包装为一个辅助函数:

// 查找第一个匹配的节点,用于插入func (n *node) matchChild(part string) *node {for _, child := range n.children {if child.part == part || n.isWild {return child}}return nil}// 查找全部匹配的节点,用于查找func (n *node) matchChildren(part string) []*node {nodes := make([]*node, 0)for _, child := range n.children {if child.part == part || n.isWild {nodes = append(nodes, child)}}return nodes}

实现节点的递归插入和查找

// 插入节点func (n *node) insert(pattern string, parts []string, height int) {// 到达高度了就停止if len(parts) == height {n.pattern = patternreturn}part := parts[height]       // 获取当前的规则child := n.matchChild(part) // 尝试用当前的规则进行匹配// 如果没有匹配成功,就新建一个节点,并加入到当前节点的孩子们中去if child == nil {child = &node{part:   part,isWild: part[0] == ':' || part[0] == '*',}n.children = append(n.children, child)}// 递归进行插入child.insert(pattern, parts, height+1)}// 查询节点func (n *node) search(parts []string, height int) *node {if len(parts) == height || strings.HasPrefix(n.part, "*") {if n.pattern == "" {return nil}return n}part := parts[height]             // 获取当前的规则children := n.matchChildren(part) // 尝试用当前的规则进行匹配// 遍历所有当前匹配的节点进行递归匹配for _, child := range children {result := child.search(parts, height+1)if result != nil {return result}}return nil}

使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中解析了 :*两种匹配符的参数,返回一个 map 。

// 将字符串解析成一个切片func parsePattern(pattern string) []string {vs := strings.Split(pattern, "/")parts := make([]string, 0)for _, item := range vs {if item != "" {parts = append(parts, item)if item[0] == '*' {break}}}return parts}// 将路由添加到路由表中func (r *router) addRoute(method, pattern string, handler HandlerFunc) {parts := parsePattern(pattern)key := method + "-" + pattern// 先看看是不是Get或者Post方法_, ok := r.roots[method]if !ok {r.roots[method] = &node{}}r.roots[method].insert(pattern, parts, 0)r.handlers[key] = handler}// 从路由表中查找路由func (r *router) getRoute(method, path string) (*node, map[string]string) {searchParts := parsePattern(path)params := make(map[string]string)root, ok := r.roots[method]if !ok {return nil, nil}n := root.search(searchParts, 0)if n != nil {parts := parsePattern(n.pattern)for index, part := range parts {if part[0] == ':' {params[part[1:]] = searchParts[index]}if part[0] == '*' && len(part) > 1 {params[part[1:]] = strings.Join(searchParts[index:], "/")break}}return n, params}return nil, nil}

对 Context 对象增加一个属性和方法,来提供对路由参数的访问。我们将解析后的参数存储到 Params

更改路由处理的方法

// 路由处理func (r *router) handle(c *Context) {n, params := r.getRoute(c.Method, c.Path)if n != nil {c.Params = paramskey := c.Method + "-" + n.patternr.handlers[key](c)} else {c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)}}

测试:

> curl "http://localhost:9999/hello/geektutu"hello geektutu, you're at /hello/geektutu> curl "http://localhost:9999/assets/css/geektutu.css"{"filepath":"css/geektutu.css"}

分组控制Group

真实的业务场景中,往往某一组路由需要相似的处理。例如:

  • /post开头的路由匿名可访问。
  • /admin开头的路由需要鉴权。
  • /api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。

一个 Group 对象需要具备哪些属性呢?

首先是前缀(prefix),比如 /,或者 /api

要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;

中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。

还需要有访问 Router的能力

// 分组路由type RouterGroup struct {prefix      stringmiddlewares []HandlerFuncparent      *RouterGroupengine      *Engine}

Engine作为最顶层的分组,也就是说 Engine拥有 RouterGroup所有的能力。

// 扩展Enginetype Engine struct {*RouterGrouprouter *routergroups []*RouterGroup}

更改下面的其他Engine方法即可

// 工厂模式的构造方法,返回一个实例func New() *Engine {engine := &Engine{router: newRouter(),}engine.RouterGroup = &RouterGroup{engine: engine,}engine.groups = []*RouterGroup{engine.RouterGroup}return engine}

增加一个Group的方法,创建一个新的RouterGroup

// 创建一个新的RouterGroupfunc (group *RouterGroup) Group(prefix string) *RouterGroup {engine := group.enginenewGroup := &RouterGroup{prefix: group.prefix + prefix,parent: group,engine: engine,}engine.groups = append(engine.groups, newGroup)return newGroup}

后面的Get方法和Post方法就都换成RouterGroup的方法就可以了

测试:

> curl "http://localhost:9999/v1/hello?name=geektutu"hello geektutu, you're at /v1/hello> curl "http://localhost:9999/v2/hello/geektutu"hello geektutu, you're at /v2/hello/geektutu> curl "http://localhost:9999/index"<h1>Index Page</h1>

中间件Middleware

中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2个比较关键的点:

  • 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
  • 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。

Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是 Context对象。插入点是框架接收到请求初始化 Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对 Context进行二次加工。另外通过调用 (*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,c.Next()表示等待执行其他的中间件或用户的 Handler

package geeimport ("log""time")func Logger() HandlerFunc {return func(c *Context) {t := time.Now()                                                            // 开始计时c.Next()                                                                   // 等待用户自己的Handler处理结束log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t)) // 打印时间}}

中间件是应用在 RouterGroup上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。

Context添加了2个参数,定义了 Next方法:

func (c *Context) Next() {c.index++s := len(c.handlers)for ; c.index < s; c.index++ {c.handlers[c.index](c)}}

index是记录当前执行到第几个中间件,当在中间件中调用 Next方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在 Next方法之后定义的部分。

定义 Use函数,将中间件应用到某个 Group 。

func (group *RouterGroup) Use(middlewares ...HandlerFunc) {group.middlewares = append(group.middlewares, middlewares...)}

ServeHTTP 函数也有变化,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 c.handlers

// 完成统一的控制入口方法ServeHTTPfunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {var middlewares []HandlerFuncfor _, group := range engine.groups {if strings.HasPrefix(req.URL.Path, group.prefix) {middlewares = append(middlewares, group.middlewares...)}}c := newContext(w, req)c.handlers = middlewaresengine.router.handle(c)}

handle 函数中,将从路由匹配得到的 Handler 添加到 c.handlers列表中,执行 c.Next()

// 路由处理func (r *router) handle(c *Context) {n, params := r.getRoute(c.Method, c.Path)if n != nil {key := c.Method + "-" + n.patternc.Params = paramsc.handlers = append(c.handlers, r.handlers[key])} else {c.handlers = append(c.handlers, func(c *Context) {c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)})}c.Next()}

测试:

> go run Go-Projects/Gee2022/11/22 15:45:00 Route  GET - /2022/11/22 15:45:00 Route  GET - /v2/hello/:name>2022/11/22 15:45:11 [200] / in 3µs>2022/11/22 15:45:25 [500] /v2/hello/geektutu in 39.4µs for group v22022/11/22 15:45:25 [500] /v2/hello/geektutu in 77.6µs
> curl http://localhost:9999/<h1>Hello Gee</h1>> curl http://localhost:9999/v2/hello/geektutu{"message":"Internal Server Error"}

模板(HTML Template)

Web 框架如何支持服务端渲染的场景

解析请求的地址,映射到服务器上文件的真实地址:

// 解析请求的地址,映射到服务器上文件的真实地址func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {absolutePath := path.Join(group.prefix, relativePath)fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))return func(c *Context) {file := c.Param("filepath")// Check if file exists and/or if we have permission to access itif _, err := fs.Open(file); err != nil {c.Status(http.StatusNotFound)return}fileServer.ServeHTTP(c.Writer, c.Req)}}func (group *RouterGroup) Static(relativePath string, root string) {handler := group.createStaticHandler(relativePath, http.Dir(root))urlPattern := path.Join(relativePath, "/*filepath")// Register GET handlersgroup.GET(urlPattern, handler)}

HTML 模板渲染

// 扩展Enginetype Engine struct {*RouterGrouprouter        *routergroups        []*RouterGrouphtmlTemplates *template.Template // 模板渲染funcMap       template.FuncMap   // 模板渲染}func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {engine.funcMap = funcMap}func (engine *Engine) LoadHTMLGlob(pattern string) {engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(pattern))}

对原来的 (*Context).HTML()方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。

// 构造HTML类型的响应func (c *Context) HTML(code int, name string, data interface{}) {c.SetHeader("Content-Type", "text/html")c.Status(code)if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {c.Fail(500, err.Error())}}

进行测试:

package mainimport ("Go-Projects/Gee/gee""fmt""html/template""net/http""time")type student struct {Name stringAge  int8}func FormatAsDate(t time.Time) string {year, month, day := t.Date()return fmt.Sprintf("%d-%02d-%02d", year, month, day)}func main() {r := gee.New()r.Use(gee.Logger())r.SetFuncMap(template.FuncMap{"FormatAsDate": FormatAsDate,})r.LoadHTMLGlob("Gee/templates/*")r.Static("/assets", "./static")stu1 := &student{Name: "Geektutu", Age: 20}stu2 := &student{Name: "Jack", Age: 22}r.GET("/", func(c *gee.Context) {c.HTML(http.StatusOK, "css.tmpl", nil)})r.GET("/students", func(c *gee.Context) {c.HTML(http.StatusOK, "arr.tmpl", gee.H{"title":  "gee","stuArr": [2]*student{stu1, stu2},})})r.GET("/date", func(c *gee.Context) {c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{"title": "gee","now":   time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC),})})r.Run("localhost:9999")}

错误恢复(Panic Recover)

错误处理也可以作为一个中间件,增强 gee 框架的能力

package geeimport ("fmt""log""net/http""runtime""strings")// print stack trace for debugfunc trace(message string) string {var pcs [32]uintptrn := runtime.Callers(3, pcs[:]) // skip first 3 callervar str strings.Builderstr.WriteString(message + "\nTraceback:")for _, pc := range pcs[:n] {fn := runtime.FuncForPC(pc)file, line := fn.FileLine(pc)str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))}return str.String()}func Recovery() HandlerFunc {return func(c *Context) {defer func() {if err := recover(); err != nil {message := fmt.Sprintf("%s", err)log.Printf("%s\n\n", trace(message))c.Fail(http.StatusInternalServerError, "Internal Server Error")}}()c.Next()}}
]]>
+ + + + + Study + + + + + + + Go + + Backend + + Project + + + +
+ + + + + Go项目-海量用户通讯系统 + + /2022/11/11/Go/Go-Project-Mass-Communication-System/ + + Go项目-海量用户通讯系统

项目开发流程

  1. 实现客户端登录菜单以及简单的用户登录逻辑
  2. 实现用户登录(与服务器端进行通信验证用户的信息)
  3. 客户端可以发送消息的长度,服务器端可以接收消息的长度
  4. 客户端可以发送消息本身,服务器端可以接收消息
  5. 改进服务器端和客户端的结构,更易读
  6. 增加数据库验证,增加一层models,同时实现用户的注册和登录
  7. 维护用户在线列表
  8. 客户端发送消息
  9. 服务器端转发消息

项目需求说明

用户注册、用户登录、显示在线用户列表、群聊(广播)、点对点聊天、离线留言

项目代码编写

实现客户端登录菜单以及简单的用户登录逻辑

package mainimport "fmt"// 定义两个变量,一个表示用户ID,一个表示用户密码var userId intvar userPwd stringfunc main() {// 接收用户的选择var key int// 判断是否还能继续显示菜单var loop = true// 循环展示菜单for loop {fmt.Println("---------------欢迎登录多人聊天系统---------------")fmt.Println("---------------   1 登录聊天室")fmt.Println("---------------    2 注册用户")fmt.Println("---------------    3 退出系统")fmt.Println("请选择(1-3):")fmt.Scanln(&key)switch key {case 1:fmt.Println("登录聊天室")loop = falsecase 2:fmt.Println("注册用户")case 3:fmt.Println("退出系统")loop = falsedefault:fmt.Println("输入有误,请重新输入")}}if key == 1 {fmt.Println("请输入用户ID")fmt.Scanln(&userId)fmt.Println("请输入用户密码")fmt.Scanln(&userPwd)// 先把登录的函数写在另外一个文件err := login(userId, userPwd)if err != nil {fmt.Println("登录失败")} else {fmt.Println("登录成功")}} else if key == 2 {fmt.Println("进行用户注册的逻辑")}}

登录逻辑的判断首先写在另外的文件中,后续再进行修改

package mainimport "fmt"func login(userId int, userPwd string) (err error) {fmt.Printf("userId=%d, userPed=%s\n", userId, userPwd)return nil}

注意这种在同一个包下引用函数的方式需要在src文件夹之外进行编译,然后手动运行

实现用户登录(与服务器端进行通信验证用户的信息)

重点是如何发送包以及如何对包进行校验,同时要保证多线程

zESgG6.md.png

消息长度的发送与接收

要对发送的消息进行序列化等操作,首先定义好处理这些数据的结构体

package message// 确定消息类型const (LoginMesType    = "LoginMes"LoginResMesType = "LoginResMes")type Message struct {Type string `json:"type"` // 消息类型Data string `json:"data"` // 消息内容}// 定义两个消息,后面需要再增加type LoginMes struct {UserId   int    `json:"userId"`   // 用户IdUserPwd  string `json:"userPwd"`  // 用户密码UserName string `json:"userName"` // 用户名}type LoginResMes struct {Code  int    `json:"code"`  // 返回的状态码 500 表示用户未注册,200 表示成功Error string `json:"error"` // 返回错误信息}

客户端发送消息(消息的长度)

package mainimport ("Go-Projects/Mass-Communication-System/common/message""encoding/binary""encoding/json""fmt""net")func login(userId int, userPwd string) (err error) {// fmt.Printf("userId=%d, userPed=%s\n", userId, userPwd)// return nil// 连接到服务器端conn, err := net.Dial("tcp", "localhost:8889")if err != nil {fmt.Println("net.Dial err=", err)return}defer conn.Close()// 准备通过conn发送消息给服务var mes message.Messagemes.Type = message.LoginMesType// 创建一个LoginMes结构体var loginMes message.LoginMesloginMes.UserId = userIdloginMes.UserPwd = userPwd// 将loginMes序列化data, err := json.Marshal(loginMes)if err != nil {fmt.Println("json Marshal err=", err)return}mes.Data = string(data)// 将mes进行序列化data, err = json.Marshal(mes)if err != nil {fmt.Println("json Marshal err=", err)return}// data为发送的消息// 先把data的长度发送给服务器var pkgLen = uint32(len(data))var buf [4]bytebinary.BigEndian.PutUint32(buf[0:4], pkgLen)//  发送长度n, err := conn.Write(buf[:4])if n != 4 || err != nil {fmt.Println("conn.Write err=", err)return}fmt.Println("客户端发送的消息长度为", len(data))fmt.Println("客户端发送的消息内容为", string(data))return}

服务器端接收消息

package mainimport ("fmt""net")func process(conn net.Conn) {// 延时关闭连接defer conn.Close()// 读取客户端发送的信息for {buf := make([]byte, 1024*4)fmt.Println("等待读取客户端发送的数据.....")n, err := conn.Read(buf[:4])if n != 4 || err != nil {fmt.Println("conn.Read err=", err)return}fmt.Println("读到的长度为", buf[:4])}}func main() {fmt.Println("服务器在8889端口监听.....")listen, err := net.Listen("tcp", "localhost:8889")defer listen.Close()if err != nil {fmt.Println("net.Listen err=", err)return}// 一旦监听成功,等待客户端连接服务器for {fmt.Println("等待客户端连接服务器.....")conn, err := listen.Accept()if err != nil {fmt.Println("listen.Accept err=", err)}// 一旦连接成功,则启动一个协程和客户端保持通讯go process(conn)}}

客户端发送消息本身,服务器端进行接收

将服务器端的消息接收封装成一个函数

func readPkg(conn net.Conn) (mes message.Message, err error) {buf := make([]byte, 1024*4)fmt.Println("等待读取客户端发送的数据.....")_, err = conn.Read(buf[:4])if err != nil {fmt.Println("conn.Read err=", err)return}// fmt.Println("读到的长度为", buf[:4])// 转换为一个uint32类型var pkgLen = binary.BigEndian.Uint32(buf[0:4])//  发送长度n, err := conn.Read(buf[:pkgLen])if n != int(pkgLen) || err != nil {fmt.Println("conn.Read err=", err)return}// 把pkgLen反序列化成messageerr = json.Unmarshal(buf[:pkgLen], &mes)if err != nil {fmt.Println("json.Unmarshal err=", err)return}return}

客户端发送消息

// 发送消息本身_, err = conn.Write(data)if err != nil {fmt.Println("conn.Write err=", err)return}

完成登录的验证功能(相当于服务器发送消息,客户端接收)

服务器端封装一个发送消息的函数

func writePkg(conn net.Conn, data []byte) (err error) {// 先发送一个长度var pkgLen = uint32(len(data))var buf [4]bytebinary.BigEndian.PutUint32(buf[0:4], pkgLen)//  发送长度_, err = conn.Write(buf[:4])if err != nil {fmt.Println("conn.Write err=", err)return}//发送data本身n, err := conn.Write(data)if n != int(pkgLen) || err != nil {fmt.Println("conn.Write err=", err)return}return}

将这种请求通用化,为后面的其他消息做准备

// 编写serverProcessLogin函数,专门处理登录的请求func serverProcessLogin(conn net.Conn, mes *message.Message) (err error) {// 从mes中取出data,并反序列化var loginMes message.LoginMeserr = json.Unmarshal([]byte(mes.Data), &loginMes)if err != nil {fmt.Println("json.Unmarshal error, err=", err)return}// 先声明一个resMesvar resMes message.MessageresMes.Type = message.LoginResMesType// 声明一个LoginResMesvar loginResMes message.LoginResMes// 如果用户的id为100,密码为123456,认为合法,否则不合法if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {//合法loginResMes.Code = 200} else {//不合法loginResMes.Code = 500loginResMes.Error = "该用户不存在,请注册再使用..."}// 将loginResMes序列化data, err := json.Marshal(loginResMes)if err != nil {fmt.Println("json.Marshal error, err=", err)return}// 将data赋值给resMesresMes.Data = string(data)// 对resMes进行序列化,准备发送data, err = json.Marshal(resMes)if err != nil {fmt.Println("json.Marshal error, err=", err)return}// 发送data,封装到writePkg函数err = writePkg(conn, data)return}// 根据客户端发送消息种类不同,决定调用哪个函数来实现func serverProcessMes(conn net.Conn, mes *message.Message) (err error) {switch mes.Type {case message.LoginMesType:// 处理登录的逻辑err = serverProcessLogin(conn, mes)case message.RegisterMesType:// 处理注册的逻辑default:fmt.Println("消息类型不存在,无法处理")}return}

客户端对消息进行处理

// 处理服务器端返回的消息mes, err = readPkg(conn)if err != nil {fmt.Println("readPkg(conn) error, err=", err)return}// 将mes的data部分反序列化var loginResMes message.LoginResMeserr = json.Unmarshal([]byte(mes.Data), &loginResMes)if loginResMes.Code == 200 {fmt.Println("登录成功")} else if loginResMes.Code == 500 {fmt.Println(loginResMes.Error)}

改进服务器端和客户端的结构,更易读

zEDSBt.png

改进主要是将前面编写的函数封装进方法之中,减少不同函数之间参数的传递,通过结构体直接调用即可

客户端的改进增加了一个与服务器端保持联系的函数

// 和服务器端保持通讯func serverProcessMes(conn net.Conn) {tf := &utils.Transfer{Conn: conn,}for {fmt.Println("客户端正在等待读取服务器发送的消息")mes, err := tf.ReadPkg()if err != nil {fmt.Println("tf.ReadPkg err=", err)return}// 如果读取到消息,下一步进行处理fmt.Println(mes)}}

增加数据库验证,增加一层models,同时实现用户的注册和登录

MVC开发模式,增加models,从而从数据库中进行读取和接收,验证用户的有效性

models层

package modelimport ("Go-Projects/Mass-Communication-System/common/message""encoding/json""fmt""github.com/gomodule/redigo/redis")// 使用工厂模式创建一个UserDao的实例func NewUserDao(pool *redis.Pool) (userDao *UserDao) {userDao = &UserDao{pool: pool,}return}// 在服务器启动后初始化一个userDao实例var (MyUserDao *UserDao)// 定义一个userDao的结构体type UserDao struct {pool *redis.Pool}// 根据用户id返回user实例func (ud *UserDao) getUserById(conn redis.Conn, id int) (user *User, err error) {res, err := redis.String(conn.Do("HGET", "users", id))if err != nil {if err == redis.ErrNil {err = ERROR_USER_NOTEXISTS}return}user = &User{}// 把res反序列化成User实例err = json.Unmarshal([]byte(res), user)if err != nil {fmt.Println("json.Unmarshal err=", err)return}return}// 完成登录的校验func (ud *UserDao) Login(userId int, userPwd string) (user *User, err error) {conn := ud.pool.Get()defer conn.Close()user, err = ud.getUserById(conn, userId)if err != nil {return}if user.UserPwd != userPwd {err = ERROR_USER_PWDreturn}return}// 注册func (ud *UserDao) Register(user *message.User) (err error) {conn := ud.pool.Get()defer conn.Close()_, err = ud.getUserById(conn, user.UserId)if err == nil {err = ERROR_USER_EXISTSreturn}// 说明该用户还没有注册过,则可以完成注册data, err := json.Marshal(user)if err != nil {return}_, err = conn.Do("HSET", "users", user.UserId, string(data))if err != nil {fmt.Println("保存注册用户错误,err=", err)return}return}

处理注册的请求

// 编写ServerProcessRegister函数,专门处理注册的请求func (u *UserProcess) ServerProcessRegister(mes *message.Message) (err error) {// 从mes中取出data,并反序列化var registerMes message.RegisterMeserr = json.Unmarshal([]byte(mes.Data), &registerMes)if err != nil {fmt.Println("json.Unmarshal error, err=", err)return}// 先声明一个resMesvar resMes message.MessageresMes.Type = message.RegisterResMesType// 声明一个RegisterResMesvar registerResMes message.RegisterResMeserr = model.MyUserDao.Register(&registerMes.User)if err != nil {if err == model.ERROR_USER_EXISTS {registerResMes.Code = 505registerResMes.Error = err.Error()} else {registerResMes.Code = 506registerResMes.Error = "注册发生未知错误"}} else {registerResMes.Code = 200}// 将loginResMes序列化data, err := json.Marshal(registerResMes)if err != nil {fmt.Println("json.Marshal error, err=", err)return}// 将data赋值给resMesresMes.Data = string(data)// 对resMes进行序列化,准备发送data, err = json.Marshal(resMes)if err != nil {fmt.Println("json.Marshal error, err=", err)return}// 发送data,封装到writePkg函数tf := &utils.Transfer{Conn: u.Conn,}err = tf.WritePkg(data)return}

维护用户在线列表

完成对当前在线用户的增删改查

package process2import "fmt"// 在服务器端实例只有一个,在很多的地方都会使用到var (userMgr *UserMgr)type UserMgr struct {onlineUsers map[int]*UserProcess}// 完成对userMgr初始化工作func init() {userMgr = &UserMgr{onlineUsers: make(map[int]*UserProcess, 1024),}}// 完成对onlineUsers的增删改查func (um *UserMgr) AddOnlineUser(up *UserProcess) {um.onlineUsers[up.UserId] = up}func (um *UserMgr) DelOnlineUser(userId int) {delete(um.onlineUsers, userId)}func (um *UserMgr) GetAllOnlineUser() map[int]*UserProcess {return um.onlineUsers}func (um *UserMgr) GetOnlineUserById(userId int) (up *UserProcess, err error) {up, ok := um.onlineUsers[userId]if !ok {err = fmt.Errorf("用户%d不存在", userId)return}return}

显示当前在线用户列表

// 因为用户登录成功,要将用户放入全局变量中以返回列表u.UserId = loginMes.UserIduserMgr.AddOnlineUser(u)// 将当前在线用户的id放入到loginResMes.UsersIdsfor id := range userMgr.onlineUsers {loginResMes.UsersIds = append(loginResMes.UsersIds, id)}fmt.Println(user, "登录成功")
// 显示当前在线用户列表fmt.Println("当前在线用户列表如下:")for _, v := range loginResMes.UsersIds {fmt.Println("用户id,\t", v)}

服务器端对用户列表进行处理

// 通知所有用户在线func (u *UserProcess) NotifyOthersOnlineUser(userId int) {for id, up := range userMgr.onlineUsers {if id == userId {continue}up.NotifyMeOnline(userId)}}func (u *UserProcess) NotifyMeOnline(userId int) {var mes message.Messagemes.Type = message.NotifyUserStatusMesTypevar notifyUserStatusMes message.NotifyUserStatusMesnotifyUserStatusMes.UserId = userIdnotifyUserStatusMes.Status = message.UserOnlinedata, err := json.Marshal(notifyUserStatusMes)if err != nil {fmt.Println("json.Marshal err=", err)return}mes.Data = string(data)data, err = json.Marshal(mes)if err != nil {fmt.Println("json.Marshal err=", err)return}tf := &utils.Transfer{Conn: u.Conn,}err = tf.WritePkg(data)if err != nil {fmt.Println("NotifyMeOnline err=", err)}}

客户端对用户列表进行处理

package processimport ("Go-Projects/Mass-Communication-System/common/message""fmt")// 客户端要维护的mapvar onlineUsers map[int]*message.User = make(map[int]*message.User, 10)// 在客户端显示当前在线的用户func outputOnlineUser() {fmt.Println("当前在线用户列表")for id, user := range onlineUsers {fmt.Println(id, user)}}// 处理返回的NotifyUserStatusMesfunc updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {user, ok := onlineUsers[notifyUserStatusMes.UserId]if !ok {user = &message.User{UserId: notifyUserStatusMes.UserId,}}user.UserStatus = notifyUserStatusMes.StatusonlineUsers[notifyUserStatusMes.UserId] = useroutputOnlineUser()}

客户端显示用户列表

// 显示当前在线用户列表fmt.Println("当前在线用户列表如下:")for _, v := range loginResMes.UsersIds {fmt.Println("用户id,\t", v)user := &message.User{UserId:     v,UserStatus: message.UserOnline,}onlineUsers[v] = user}

客户端发送消息

直接调用前面写好的就行,代码很少了

package processimport ("Go-Projects/Mass-Communication-System/client/utils""Go-Projects/Mass-Communication-System/common/message""encoding/json""fmt")type SmsProecss struct {}func (sp *SmsProecss) SendGroupSms(content string) (err error) {var mes message.Messagemes.Type = message.SmsMesTypevar smsMes message.SmsMessmsMes.Content = contentsmsMes.UserId = CurUser.UserIdsmsMes.UserStatus = CurUser.UserStatusdata, err := json.Marshal(smsMes)if err != nil {fmt.Println("json.Marshal err=", err)return}mes.Data = string(data)data, err = json.Marshal(mes)if err != nil {fmt.Println("json.Marshal err=", err)return}tf := &utils.Transfer{Conn: CurUser.Conn,}err = tf.WritePkg(data)if err != nil {fmt.Println("tf.WritePkg err=", err)return}return}

服务器端转发消息

也是和上面的差不多

package process2import ("Go-Projects/Mass-Communication-System/common/message""Go-Projects/Mass-Communication-System/server/utils""encoding/json""fmt""net")type SmsProecss struct {}func (sp *SmsProecss) SendGroupSms(mes *message.Message) (err error) {var smsMes message.SmsMeserr = json.Unmarshal([]byte(mes.Data), &smsMes)if err != nil {fmt.Println("json.Unmarshal err=", err)return}data, err := json.Marshal(mes)if err != nil {fmt.Println("json.Marshal err=", err)return}for id, up := range userMgr.onlineUsers {if id == smsMes.UserId {continue}sp.SendMesToEachOnlineUser(data, up.Conn)}return}func (sp *SmsProecss) SendMesToEachOnlineUser(data []byte, conn net.Conn) (err error) {tf := &utils.Transfer{Conn: conn,}err = tf.WritePkg(data)if err != nil {fmt.Println("tf.WritePkg err=", err)return}return}
]]>
+ + + + + Study + + + + + + + Go + + Backend + + Project + + + +
+ + + + + 研究生课程:现代信息检索-期末复习 + + /2022/11/09/UCAS/information-retrieval/information-retrieval-final/ + + 《现代信息检索》期末复习

考试主要涉及概念上的问题,可能没有特别复杂的计算的内容

第1讲 布尔检索

倒排索引基本结构:

对每个词项t,记录所有包含t的文档列表。每篇文档用一个唯一的docID来表示,通常是正整数,如1,2,3…

为什么要用倒排索引:

当用户发起查询时(假设查询为一个关键词),搜索引擎会扫描索引库中的所有文档,找出所有包含关键词的文档,这样依次从文档中去查找是否含有关键词的方法叫做正向索引 。

为了增加效率, 搜索引擎会把正向索引变为倒排索引即把“文档→单词”的形式变为“单词→文档”的形式 。

倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。

布尔查询的处理优化:

  • 每个布尔表达式都能转换成合取范式
  • 获得每个词项的df
  • 通过将词项的df相加,估计每个OR表达式对应的倒排记录表的大小
  • 按照上述估计从小到大依次处理每个OR表达式

问题:什么是倒排索引?为什么说倒排索引能加快检索的速度?假设“信息”、“检索”在倒排索引中是两个独立的term,试说明检索短语“信息检索”的基本流程。

答案:倒排索引指的是从词项到文档的一种索引结构。由于它直接可以从查询词定位到文档,所以能够大大加快检索的速度。检索短语“信息检索”的基本流程:从词典中分别查找到“信息”和“检索”这两个词,分别返回它们的倒排记录表,然后求这两个表的交集,在求交集时要考虑它们在文档中的位置相对关系。

词条 :一段文本中有效词的子序列,其中每个子序列称为一个词条。

词条类 :相同词条构成的集合。

词项 :一个词项指的是在信息检索系统词典中所包含的某个可能经过归一化处理的词条类。(词项集合和词条集合可以完全不同,比如可以采用某一个分类体系中的类别标签作为词项。当然,在实际的信息检索系统中,词项往往和词条密切相关)

注意:①文档-词项关联矩阵只包含01②要按字典序进行排序

zC3hFS.png

第2讲 索引构建

基于排序的索引构建方法存在的问题

在构建索引时,每次解析一篇文档,因此对于每个词项而言,其倒排记录表不到最后一篇文档都是不完整的。

如果每个 (termID, docID)对占用 8个字节, 那么处理大规模语料需要大量的空间。

一般内存的容量比较小,没有办法将前面产生的倒排记录表全部放在内存中,需要在磁盘上存储中间结果。

BSBI算法

一种减少寻道操作的排序:Blocked sort-based Indexing

将所有记录划分为每个大小约为10M的块,收集每一块的倒排记录,排序,将倒排索引写入硬盘,最后将不同的分块合并为一个大的倒排索引。

SPIMI算法

内存式单遍扫描索引构建算法:Single-pass in-memory indexing

关键思想:

  • 对每个块都产生一个独立的词典(不需要在块之间进行 term-termID的映射)
  • 对倒排记录表不排序,按照它们出现的先后顺序排列,只对词典排序(实际上由于指针的存在,倒排记录表没有排序的必要)。

在扫描文档的同时,直接在内存中维护一个不断更新的倒排索引

因此对每个块生成一个完整的倒排索引,这些独立的索引最后合并成一个大索引

最终合并词典的过程中,需要进行词项字符串的比较,因为此时没有全局词典提供词项-整数ID的映射。

BSBI算法和SPIMI算法的主要区别

BSBI算法:在分块索引阶段,BSBI算法维护一个全局Term (String) – Termid (int) 的映射表,局部索引为Termid及其倒排记录表,仍然按词典顺序排序。

SPIMI算法:分块索引阶段与BSBI算法不同在于建立局部词典和索引,无需全局词典。在合并阶段,将局部索引两两合并,最后产生全局词典建立Term – Termid的映射。

使用文本预处理步骤可以大大减小系统所需要存储的倒排记录表的数目,从而提高索引构建和检索的速度

第3讲 索引压缩

有损压缩:丢弃一些信息-很多常用的预处理步骤可以看成是有损压缩

无损压缩:所有信息都保留-索引压缩中通常都使用无损压缩

词典压缩

定长数组方式下的词典存储:每个词项需要20(字符串)+4(词频)+4(指向倒排索引表的指针)=28个字节。

将整部词典看成单一字符串:4(词频)+4(指向倒排索引表的指针)+3(指向字符串的指针,按照实际大小决定,例如8*400000个位置需要$log_2(8 * 400000)< 24 $位来表示)+8(每个字符串平均需要8个字节)=19个字节

按块存储,假设块大小k=4,此时每4个词项只需要保留1个词项指针,但是同时需要增加4个字节(比较短,1个字节就可以)来表示每个词项的长度,因此每4个词项需要3+4=7B,比之前的节省了12-7=5B

前端编码:每个块当中 (k = 4)会有公共前缀,可以采用前端编码方式继续压缩

如果使用词干还原,由于将同一词汇的不同形式还原到词根,因此前端编码的压缩效果有限

倒排记录表压缩

倒排记录表的压缩:两种经典编码VB和γ编码(注意对gap进行编码,第一个id,后面都是gap

可变字节(VB)码:设定一个专用位 (高位) c作为延续位(continuation bit),如果间隔表示少于7比特,那么c置1,将间隔编入一个
字节的后7位中;否则将高7位放入当前字节中,并将c置0,剩下的位数采用同样的方法进行处理,最后一个字节的c置1(表
示结束)

编码

  • 将G (Gap, 间隔) 表示成长度(length)和偏移(offset)两部分
  • 偏移对应G的二进制编码,只不过将首部的1去掉(因为所有的编码第一位都是1)
  • 长度部分给出的是偏移的位数,采用一元编码
  • 手动计算的时候先计算偏移,再根据偏移计算长度

zC8kTK.png

第4讲 拼写矫正

通道模型:

若有包含个词条的大文本语料,则是词频。(一元先验概率)

通道模型概率-计算错误概率:混淆“矩阵”(计算一个字符变为另一个字符的概率如何)

轮排索引:(主要思想:让星号出现在词汇的末尾)

  • 将每个通配查询旋转,使*出现在末尾
  • 将每个旋转后的结果存放在词典中,即B-树中

轮排索引的查找过程:

  • 将查询进行旋转,将通配符旋转到右部
  • 同以往一样查找B-树,得到匹配的所有词项,将这些词项对应的倒排记录表取出

相对于通常的B-树,轮排索引(轮排树)的空间要大4倍以上 (经验值)

k-gram索引:枚举一个词项中所有连读的k个字符构成k-gram(在首尾添加k-1个首尾符号)

  • 构建一个倒排索引,此时词典部分是所有的k-gram,倒排记录表部分是包含某个k-gram的所有词项
  • 相当于对词项再构建一个倒排索引(二级索引)
  • 比轮排索引空间开销要小
  • 但是可能返回一些伪正例,需要进行后过滤

zC8KOI.png

k-gram索引 vs. 轮排索引

  • k-gram索引的空间消耗小
  • 轮排索引不需要进行后过滤

第5讲 TF-IDF

tf-idf词频及log词频

TF是词项t的词项频率,是与文档相关的一个量,可以认为是文档内代表度的一个量,也可以认为是一种局部信息。

IDF是反映词项t的信息量的一个指标,是一种全局性指标,反应的是词项在全局的区别性,可视为一种词项全局信息量的指标。

向量空间模型基本思想:把查询和文本表示成向量(早期表示成TF-IDF权重)

向量空间模型的不同实现方案(不用背表,但是有很多情况,要看好题)(比如有时候idf不用算):

z9prND.md.png

注意:看好题目,不说对数、归一化什么的就不要做

zC8Jfg.png

第6讲 概率检索模型

主要是BM25模型的基本概念,IDF是怎么计算的,以及它的基本假设,伯努利分布

BIM的基本假设,BM25的二重泊松分布,考虑了哪些因素,如长度归一等等。

参考资料

以往的向量空间模型是将query和文档使用向量表示然后计算其内容相似性来进行相关性估计的,而概率检索模型是一种直接对用户需求进行相关性的建模方法,一个query进来,将所有的文档分为两类-相关文档、不相关文档,这样就转为了一个相关性的分类问题。

对于某个文档来说,表示该文档属于相关文档的概率,则表示该文档属于不相关文档的概率,如果query属于相关文档的概率大于不相关文档,则认为这个文档是与用户查询相关的。

使用贝叶斯公式转换一下,则在搜索排序过程中不需要真正的分类,只需要保证相关性由高到底排序即可,所以只需要降序即可,
这样就最终转为计算的值即可。

二值独立概率模型BIM

为了能够使得上述两个计算因子可行,二元独立模型做出了两个假设

  1. 二元假设

类似于布尔模型中的文档表示方法,一篇文档在由特征(或者单词)进行表示的时候,以特征(或者单词)出现和不出现两种情况来表示,不考虑词频等其他因素。

  1. 词汇独立性假设

指文档里出现的单词之间没有任何关联,任意一个单词在文档的分布概率不依赖于其他单词是否出现。因为词汇之间没有关联,所以可以将文档概率转换为单词概率的乘积。

上述提到的文档D表示为,用来表示第个单词在相关文档出现的概率,则在已知相关文档集合的情况下,观察到D的概率为:

同理在不相关文档中出现的概率为

可以推导出:

设文档统计量如下:

相关文档不相关文档文档数量
文档数量

则可以得出(加1平滑):

因此最终的公式为:

其代表的含义是:对于同时出现在用户查询Q和文档D中的单词,累加每个单词的估值,其和就是文档D和查询的相关性度量。

在不确定哪些文档是相关的,哪些文档是不相关的的时候,可以给公式的估算因子直接赋予固定值,则该公式将会退化为IDF因子。

优点:BIM模型建立在数学基础上,理论性较强

缺点:

  • 需要估计参数
  • 原始的BIM没有考虑TF、文档长度因素
  • BIM中同样存在词项独立性假设
  • BIM实质上是一个idf权重公式,仅考虑了全局信息,缺少局部信息。因此需要和TF权重配合使用

BM25模型

BM25模型计算公式其实融合了4个考虑因素:IDF因子,文档长度因子,文档词频和查询词频。并对3个自由调节因子进行权值的调整。

IDF因子:设BIM模型中的相关文档数量为0,则退化为

查询权重:,考虑查询词频

TF权重(基于二重泊松分布):,考虑文档中词频和文档长度

最终形式为三项相乘

例题:

zCMO8s.png

zCMxK0.png

优点:

  • 一定程度上的理论化模型
  • 基于二重泊松假设——适用于绝大多数文本语料上的IR检索应用
  • 实验证明有效

缺点:

  • 待调参数多且参数敏感性高
  • 必须去停用词

问题:BM25和向量空间模型(VSM)为何需要长度归一?语言模型为何需要平滑处理?两个问题之间有何联系?

答案:由于长文挡中词项反复出现的可能性大,包含更多的不同词项,所以词项频率和词汇量可能更大。这显然是不公平的。长度归一化,可以使长文档和短文档的向量中的权重都处于同一数量级。平滑处理是为了解决数据稀疏引起的0概率问题。两者都是常见的数据预处理方法,提高了数据质量,为了保证模型的鲁棒性。

第7讲 语言建模的检索模型

流行的是基于多项式分布,对于生成模型的计算有零概率的问题,需要进行平滑,基本概念要知道

zC8stU.png

第8讲 信息检索的评价

指标计算,如正确率召回率等等,F1,未插值的AP

题目:什么是非插值的MAP?为什么说它在引入序的作用的同时考虑了召回率?

答案:单个查询的非插值MAP指的是所有相关文档(不论是否在结果出现,若不出现就假定出现在无穷远处)在结果出现位置上的正确率的算术平均值。系统的非插值MAP是所有查询上的非插值AP的算术平均值。从非插值AP的定义看,一方面,如果出现在结果中的相关文档越多,求和结果也越大,那么非插值AP值也越大。另一方面,如果相关文档在结果中出现位置越靠前,那么非插值AP值也越大。因此,可以认为非插值MAP同时考底了召回率和序的作用。

Bpref:在相关性判断不完全的情况下,计算在进行了相关性判断的文档集合中,在判断到相关文档前,需要判断的不相关文档的篇数。相关性判断完全的情况下,利用Bpref和MAP进行评价的结果很一致,但是相关性判断不完全的情况下,Bpref更鲁棒。

NDCG:每个文档不仅仅只有相关和不相关两种情况,而是有相关度级别,比如0,1,2,3。我们可以假设,对于返回结果:相关度级别越高的结果越多越好,相关度级别越高的结果越靠前越好

优点:

  • 图形直观,易解释
  • 支持非二值的相关度定义,比P-R曲线更精确
  • 能够反映用户的行为特征(如:用户的持续性persistence)

缺点:

  • 相关度的定义难以一致
  • 需要参数设定

zCKQeO.png

zC8476.png

zC8g1J.png

zC8fn1.png

第9讲 完整搜索系统中的评分计算

考试基本不涉及

第10讲 查询扩展

相关反馈的本质是将检索返回的文档的相关性判定(不同的判定来源:人工或非人工)作为返回信息,希望提升检索效果(召回率和正确率)

反馈信息的来源:显式(用户点击)、隐式(用户行为等)、伪相关反馈(返回的前几个结果算相关)

Rocchio算法

查询扩展的最初含义是对查询进行扩充,近年来越来越向查询重构偏移,即现在的查询扩展是指对原有查询进行修改。

通过在查询中加入同义或者相关的词项来提高检索结果。

相关词项的来源: 人工编辑的同义词词典、自动构造的同义词词典、查询日志等等。

查询扩展和相关反馈对检索效果的提升是非常有用的经验性的方法

问题:什么是伪相关反馈?为什么说有时候伪相关反馈会降低某个查询的检索效果?

答案:伪相关反馈指的是系统对上次返回的检索结采进行“伪”判定(比如假设前几个结果是相关的),然后根据这个结果进行反馈。伪相关反馈依赖于上次检索的结果,那么在上次检索结果不可靠情况下,假设成立的可能性很小,此时就进行伪相关反馈反而可能降低后一次检索的效果。

注意:负权重要记为0,同时也要进行排序

zC8jBt.png

第11、12、13讲 文本分类

问题:文本分类当中,什么是宏平均?什么是微平均?为什么说微平均计算时易受大类影响?

答案:宏平均指的是在每个类别上分类效果的平均值,也即将每个类别看成一个单位。而微平均是将所有类别看成一个类别后求到的效果值,即将每篇文档看成一个单位。由于微平均将文档看成单位,而大类文档数目较多,因此它在计算时易受大类影响。

朴素贝叶斯(线性分类器)

使用log将乘积计算变为求和计算

最大似然估计(零概率的情况下怎么进行加一平滑)

Rocchio分类(线性分类器)

计算每个类的中心向量(所有文档向量的算术平均)

将每篇测试文档分到离它最近的那个中心向量

Rocchio分类器是要训练的

KNN(非线性分类器)

kNN分类决策取决于k个邻居类中的多数类

类别之间的分类面是分段线性的

kNN分类器几乎不需要训练

但是像kNN这种非线性学习方法在某些情况下也会产生一个线性分类器

SVM

SVM分线性SVM和非线性SVM,SVM本身是一个线性决策,但是核函数可以是线性或非线性的

算法本身是转化成一个线性公式,但是最终得到的是一个非线性的决策面,只不过把样本投射到高维空间里面

问题:总结SVM中处理线性不可分数据的方法,给出其基本原理。

  • 广义最优分类面:在条件中增加一个松弛项,容纳少量线性不可分的噪声样本。
  • 核函数:从低维空间非线性映射到线性可分的高维空间。

问题:什么是核函数?它的作用是什么?为什么核函数的引入常常称为核技巧?

答案:核函数是满足若干性质的相似度计算函数。它的主要作用是计算两个对象的相似度,具体地说,它可以基于原始空间上的点来定义映射后空间上的内积函数。核函数避免知道空间映射的具体函数形式,能够直接基于核函数进行映射后的对象相似度计算,所以它的引入常常称为核技巧。

偏差和方差

对于像Rocchio和NB一样的线性方法来说,对于非线性问题它们的偏差就比较大

像kNN一样的非线性方法的偏差较小,方差较大

如果拥有的训练数据非常少,而又要训练出一个基于监督学习的分类器,应该采用具有高偏差的分类器,在这种情况下NB能取得较好的结果,诸如kNN的低偏差模型大概是不可取的。

分类题目

zCGEBq.png

zCGn4U.png

第12讲 排序学习

现有检索排序算法存在哪些问题,怎么改进?

很多传统的IR权重计算机制中都包含了基本指标的非线性缩放过程(比如词项频率或idf 的对数权重计算)。目前为止,机器学习非常擅长特征线性组合(或者其他类似的受限模型)中的权重优化,但是并不擅长基本指标的非线性缩放。这个领域仍然需要人工的特征工程方法。

基于布尔权重的学习

给定训练样例集合,每个样例表示为三元组,相关或者不相关

从上述样例中学习权重,使得学到的评分接近训练集中的相关性判定结果。

基于实数权重的学习(pointwise)

设置评分函数是两个因子的线性组合:查询和文档的向量空间相似度评分和查询词项在文档中存在的最小窗口宽度

相关记为1,不相关记为0,我们的目标是寻找一个评分函数,该函数能够组合特征因子的值,并尽量接近0或1,希望该函数的结果尽量与训练集上的结果保持一致

基于序回归的排序学习(pairwise)

为什么将IR排序问题看成一个序回归问题?

  • 对于同一查询,文档之间可以按照相对得分排序即可,并不一定要求每篇文档有一个全局的绝对得分
  • 因此,只需要一个排序,而不要得到相关度的绝对得分,问题空间可以减小
  • pairwise 方法相对 pointwise 方法对噪声标注更敏感,即一个错误标注会引起多个 doc pair 标注错误。

方法:

  • 给定一些已经判定的查询
  • 对训练集中的每条查询, 我们都有针对该查询的一系列文档集合,这些文档已经由人工按照其与查询的相关度排序
  • 对每个文档、查询对,构造特征向量,这里的特征可以采用前面的特征
  • 对于两篇文档,可以计算特征向量之间的差异向量
  • 依据假设,中的一个更相关
  • 如果更相关,记为(在检索结果中,应该出现在前面), 那么分配给向量的类别为,否则为
  • 学习的目标是建立一个分类器,满足:

第14、15讲

词项表示:通过分析文档集来自动生成同义词库-基于共现的同义词库

词嵌入:得到每个词的低维密集向量表示

Neural IR 模型分类

Representation based(基于表示学习的模型):学习文本的分布式表示,在高维空间匹配

  • 词表示:one hot → distributed
  • 句子表示:bag of words → distributed
  • 匹配能力取决于学习文本表示的算法能力
  • 代表模型:DSSM, CDSSM

Matching function(基于交互匹配的模型):文本之间先进行交互匹配,再对匹配信号进行融合

  • 输入:比较底层的输入
  • 匹配函数:cosine, dot product → NN
  • 优点:可以考虑更加丰富的匹配信号, 如软匹配 (soft matching)
  • 代表模型:MatchPyramid , DRMM, K NRM, PACRR, NPRF

Combination of both: 既考虑 Representation 又考虑 Matching function

BERT在检索应用中的特点:

  1. 在高频查询上,BM25的偏差比BERT更大,导致BM25的效果不好
  2. BERT可以检索到更稀有的词项
  3. 在长查询上,BERT的表现不如BM25更好

问题:简述BERT的基本结构?如何预训练一个BERT(涉及什么任务)?

BERT的基本结构:

  1. 词向量
  2. 多层Transformer Encoder结构:包括自注意力和Feed-Forward
  3. 任务特定的输出层

BERT的训练任务有两类:

  1. masked language model 随机掩盖掉一些单词,然后通过上下文预测该单词。BERT中有15%的wordpiece token会被随机掩盖,这15%的token中80%用[MASK]这个token来代替,10%用随机的一个词来替换,10%保持这个词不变。这种设计使得模型具有捕捉上下文关系的能力,同时能够有利于token-level tasks,例如序列标注等。
  2. next sentence prediction 语料中50%的句子,选择其相应的下一句一起形成上下句,作为正样本;其余50%的句子Embedding随机选择一句非下一句一起形成上下句,作为负样本。这种设定,有利于sentence-level tasks,例如问答,注意:作者特意说了语料的选取很关键,要选用document-level的而不是sentence-level的,这样可以具备抽象连续长序列特征的能力。

第16讲 Web搜索

Google次高竞标价格拍卖机制:

zS0Wkt.png

bid:每个广告商为每次点击给出的最大投标价格

CTR:一旦被显示后被点击的比率

ad rank=bid × CTR:这种做法可以在广告商愿意支付的价钱和广告的相关度高低之间进行平衡。

排名第1的C,需要支付的价格是它的下一位的

排名第2的B,需要支付的价格是它的下一位的

这样做避免了“保底”行为的产生,可以使收益更大化。

第17讲 爬虫

采集器必须做到

  • 礼貌性
    • 不要高频率采集某个网站
    • 仅仅采集robots.txt所规定的可以采集的网页
      • robots.txt协议不让采集,不过写程序还是可以采集到的,但是不能这样做,一定要遵守协议
  • 鲁棒性
    • 能够处理采集器陷阱、重复页面、超大页面、超大网站、动态页面等问题

第18讲 链接分析

锚文本是人为创建的超链接,可以理解为质量认证的信号。

BV算法-邻接表压缩的经典算法

邻接表:一个节点的邻居集合,可以视为一个结点(URL)所有指向它的页面的集合

假设每个URL由一个整数表示,对于40亿页的网站,每个结点需要32位甚至64位,存储开销非常大

BV算法可以降低到平均3位

压缩中使用到的属性:

  • 相似度(邻接表之间)
  • 位置(一个页面中的许多链接都连接到“附近”的页面)-将所有URL按照字母顺序排序,同一个网站的页面的链接相似
  • 在已排序的邻接表中使用间隔编码
  • gap value 的分布

BV算法主要思想:由于模板的缘故,一个节点的邻接列表类似于字典顺序中的7个先前的URL之一,根据这7个中的之一表示邻接表,否则重新编码。

BV算法的主要优势

  • 仅依赖于位置的规范顺序-字典顺序对web十分适用
  • 邻接查询可以被非常高效地回答
  • 易于实现one-pass算法
    • 顺序读取,不需要无限缓冲。读取复杂度与网页数量是线性关系

PageRank

起源 : 引用分析

特点:

  1. 一个网页如果它的入链越多,那么它也越重要(PageRank越高)
  2. 一个网页如果被越重要的网页所指向,那么它也越重要(PageRank越高)

PageRank背后的假设:

  1. Web 上的链接是网页质量的标志-链出网页的作者认为链向的网页具有很高的质量
  2. 锚文本能够描述链向网页的内容

PageRank的计算:迭代法计算

如果存在循环通路,需要虚拟一个结点,或者以一定的概率选取一个其他结点到达

HITS: Hub节点&Authority节点

每个网页计算两个值:

  • Hub:目录型或导航型网页的权重
  • Authority:权威型网页的权重

计算方法:

,其中是所有链接到的页面

,其中是所有页面链接到的页面

  • 一个网页被越重要的导航型网页指向越多,那么它的Authority越大
  • 一个网页指向的高重要度权威型网页越多,那么它的Hub越大

实际计算过程:

  1. 首先进行Web 搜索,搜索的结果称为根集(从搜索结果中选择一部分排名靠前的网页作为根集,也叫做种子集合)
  2. 将所有链向种子集合和种子集合链出的网页加入到种子集合,新的更大的集合称为基本集
  3. 最后,在基本集上计算每个网页的hub值和authority值

PageRank vs. HITS

PageRank算法是Google提出的一个链接分析的算法,它可以根据节点之间的链接关系计算出每个节点的重要性,反映的是“越多越重要的节点指向该节点则该节点越重要”这个事实。

HITS是IBM提出的另一种链接分析算法,它根据节点之间的链接关系对每个节点计算出两个值:权威度(authority值)和导航度(hub值).

相同点:两者都是基于链接分析的排序算法,并且在算法中两者都利用了特征向量作为理论基础和收敛性依据。

不同点:网页的PageRank是一种静态评分,与查询主题无关,可以事先算好,因此适合于大型搜索引擎;HITS算法的计算与查询主题相关,检索之后再进行计算,因此不适合于大型搜索引擎。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + Go项目-客户信息管理系统 + + /2022/11/03/Go/Go-Project-Customer-Management-System/ + + Go项目-客户信息管理系统

项目开发流程

xHx5Pe.md.png

项目需求说明

  1. 模拟实现基于文本界面的《客户信息管理软件》。
  2. 该软件能够实现对客户对象的插入、修改和删除(用切片实现),并能够打印客户明细表

项目代码编写

编写Customer.go

主要是用于表示一个客户的信息,包含结构体以及在其他地方如果调用它的工厂模式的方法

package modelimport "fmt"// 定义Customer结构体,表示一个客户信息type Customer struct {Id     intName   stringGender stringAge    intPhone  stringEmail  string}// 工厂模式返回Customer的结构体,在CustomerService里面使用// 感觉就是新建一个Customer的实例func NewCustomer(id int, name string, gender string, age int, phone string, email string) *Customer {return &Customer{Id:     id,Name:   name,Gender: gender,Age:    age,Phone:  phone,Email:  email,}}func (cu *Customer) GetInfo() string {return fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v", cu.Id, cu.Name, cu.Gender, cu.Age, cu.Phone, cu.Email)}

以及如果我们要返回一个客户的信息,操作也是在Customer的实例上进行的,因此后面的方法也要写在这个结构体的下面

完成对Customer结构体的操作的代码在CustomerService里面,定义另外一个结构体,里面包含一个切片,存储全部实例化的Customer

// 完成对Customer的操作,包括增删改查type CustomerService struct {// 存储当前的客户Customers []model.Customer// 声明一个字段,表示当前切片含有多少个客户CustomerNum int}

主界面 customerView.go

主菜单:

func (cv *CustomerView) MainMenu() {for {var username, password stringfmt.Print("请输入用户名:")fmt.Scanln(&username)fmt.Print("请输入密码:")fmt.Scanln(&password)if cv.login(username, password) {break} else {fmt.Println("用户名或密码错误!")}}// 显示主菜单for cv.loop {fmt.Println("\n---------------------客户信息管理软件---------------------")fmt.Println("                         1 添加客户")fmt.Println("                         2 修改客户")fmt.Println("                         3 删除客户")fmt.Println("                         4 客户列表")fmt.Println("                         5 退    出")fmt.Print("请选择(1-5):")// 接收用户的输入fmt.Scanln(&cv.key)// 对用户的输入进行判断switch cv.key {case "1":cv.addCustomer()case "2":cv.changeCustomer()case "3":cv.deleteCustomer()case "4":cv.showCustomer()case "5":cv.exit()default:fmt.Println("请输入正确的选项------")}}}

主菜单里面有的变量是需要定义在结构体中的

type CustomerView struct {key             string                   // 接收用户输入loop            bool                     // 表示是否循环的显示主菜单username        string                   // 用户的用户名password        string                   // 用户的密码customerService *service.CustomerService // 获取用户服务}

同时也编写一个工厂模式的方法,方便main.go文件进行调用

func NewCustomerView() *CustomerView {return &CustomerView{key:             "",loop:            true,username:        "admin",password:        "password",customerService: service.NewCustomerService(),}}

main.go:

package mainimport ("Go-Projects/Customer-Management-System/view")func main() {view.NewCustomerView().MainMenu()}

完成增删改查的功能

要注意,全部的功能实现细节都应该是在customerService里面进行编写的,customerView.go 文件只负责调用,并对返回的结果进行判断等。

首先要对CustomerService进行初始化,也是相当于工厂模式了

// 初始化CustomerServicefunc NewCustomerService() *CustomerService {customerService := &CustomerService{} // 初始化customerService.CustomerNum = 0return customerService}

展示客户列表

func (cv *CustomerView) showCustomer() {if cv.customerService.CustomerNum == 0 {fmt.Println("没有客户!")return}fmt.Println("\n-------------------------客户列表-------------------------")fmt.Println("编号\t姓名\t性别\t年龄\t电话\t电子邮件")for _, eachCustomer := range cv.customerService.ShowCustomerSlice() {fmt.Println(eachCustomer.GetInfo())}}
func (cs *CustomerService) ShowCustomerSlice() []model.Customer {return cs.Customers}

添加客户

对切片增加一个客户的实例,然后将记录的数量+1

func (cv *CustomerView) addCustomer() {id := cv.customerService.CustomerNum + 1var name, gender, phone, email stringvar age intfmt.Print("请输入姓名:")fmt.Scanln(&name)fmt.Print("请输入性别:")fmt.Scanln(&gender)fmt.Print("请输入年龄:")fmt.Scanln(&age)fmt.Print("请输入电话:")fmt.Scanln(&phone)fmt.Print("请输入电子邮件:")fmt.Scanln(&email)if cv.customerService.AddCustomer(*model.NewCustomer(id, name, gender, age, phone, email)) {fmt.Println("-------------------------添加成功-------------------------")} else {fmt.Println("-------------------------添加失败-------------------------")}}
func (cs *CustomerService) AddCustomer(customer model.Customer) bool {cs.Customers = append(cs.Customers, customer)cs.CustomerNum += 1return true}

删除客户

根据客户的ID寻找客户在切片中的位置,然后将它删除即可。

func (cv *CustomerView) changeCustomer() {var id intfmt.Print("请输入修改的ID号:")fmt.Scanln(&id)if cv.customerService.ChangeCustomer(id) {fmt.Println("-------------------------修改成功-------------------------")} else {fmt.Println("-------------------------添加失败-------------------------")}}
func (cs *CustomerService) DeleteCustomer(id int) bool {for index, cus := range cs.Customers {if cus.Id == id {cs.Customers = append(cs.Customers[:index], cs.Customers[index+1:]...)cs.CustomerNum -= 1return true}}return false}

修改客户

根据客户的ID寻找客户在切片中的位置,然后修改需要修改的字段即可。

func (cv *CustomerView) changeCustomer() {var id intfmt.Print("请输入修改的ID号:")fmt.Scanln(&id)if cv.customerService.ChangeCustomer(id) {fmt.Println("-------------------------修改成功-------------------------")} else {fmt.Println("-------------------------添加失败-------------------------")}}
func (cs *CustomerService) ChangeCustomer(id int) bool {reader := bufio.NewReader(os.Stdin) // 标准输入输出for index, cus := range cs.Customers {if cus.Id == id {fmt.Printf("请输入修改的姓名(%v):", cus.Name)name, _ := reader.ReadString('\n')name = strings.TrimSpace(name)if len(name) != 0 {cs.Customers[index].Name = name}fmt.Printf("请输入修改的性别(%v):", cus.Gender)gender, _ := reader.ReadString('\n')gender = strings.TrimSpace(gender)if len(gender) != 0 {cs.Customers[index].Gender = gender}fmt.Printf("请输入修改的年龄(%v):", cus.Age)age, _ := reader.ReadString('\n')age = strings.TrimSpace(age)if len(age) != 0 {t, _ := strconv.ParseInt(age, 10, 64)cs.Customers[index].Age = int(t)}fmt.Printf("请输入修改的电话(%v):", cus.Phone)phone, _ := reader.ReadString('\n')phone = strings.TrimSpace(phone)if len(phone) != 0 {cs.Customers[index].Phone = phone}fmt.Printf("请输入修改的电子邮件(%v):", cus.Email)email, _ := reader.ReadString('\n')email = strings.TrimSpace(email)if len(email) != 0 {cs.Customers[index].Email = email}return true}}return false}

修改的时候回车表示对这个字段不修改,因此要调一个reader的包来完成这个工作,自己无法作出这种判断。

完整源代码

.
├── Customer-Management-System
│ ├── main
│ │ └── main.go
│ ├── model
│ │ └── customer.go
│ ├── service
│ │ └── customerService.go
│ └── view
│ └── customerView.go

main.go

package mainimport ("Go-Projects/Customer-Management-System/view")func main() {view.NewCustomerView().MainMenu()}

customer.go

package modelimport "fmt"// 定义Customer结构体,表示一个客户信息type Customer struct {Id     intName   stringGender stringAge    intPhone  stringEmail  string}// 工厂模式返回Customer的结构体,在CustomerService里面使用// 感觉就是新建一个Customer的实例func NewCustomer(id int, name string, gender string, age int, phone string, email string) *Customer {return &Customer{Id:     id,Name:   name,Gender: gender,Age:    age,Phone:  phone,Email:  email,}}func (cu *Customer) GetInfo() string {return fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v", cu.Id, cu.Name, cu.Gender, cu.Age, cu.Phone, cu.Email)}

customerService.go

package serviceimport ("Go-Projects/Customer-Management-System/model""bufio""fmt""os""strconv""strings")// 完成对Customer的操作,包括增删改查type CustomerService struct {// 存储当前的客户Customers []model.Customer// 声明一个字段,表示当前切片含有多少个客户CustomerNum int}// 初始化CustomerServicefunc NewCustomerService() *CustomerService {customerService := &CustomerService{} // 初始化customerService.CustomerNum = 0return customerService}func (cs *CustomerService) ShowCustomerSlice() []model.Customer {return cs.Customers}func (cs *CustomerService) AddCustomer(customer model.Customer) bool {cs.Customers = append(cs.Customers, customer)cs.CustomerNum += 1return true}func (cs *CustomerService) DeleteCustomer(id int) bool {for index, cus := range cs.Customers {if cus.Id == id {cs.Customers = append(cs.Customers[:index], cs.Customers[index+1:]...)cs.CustomerNum -= 1return true}}return false}func (cs *CustomerService) ChangeCustomer(id int) bool {reader := bufio.NewReader(os.Stdin) // 标准输入输出for index, cus := range cs.Customers {if cus.Id == id {fmt.Printf("请输入修改的姓名(%v):", cus.Name)name, _ := reader.ReadString('\n')name = strings.TrimSpace(name)if len(name) != 0 {cs.Customers[index].Name = name}fmt.Printf("请输入修改的性别(%v):", cus.Gender)gender, _ := reader.ReadString('\n')gender = strings.TrimSpace(gender)if len(gender) != 0 {cs.Customers[index].Gender = gender}fmt.Printf("请输入修改的年龄(%v):", cus.Age)age, _ := reader.ReadString('\n')age = strings.TrimSpace(age)if len(age) != 0 {t, _ := strconv.ParseInt(age, 10, 64)cs.Customers[index].Age = int(t)}fmt.Printf("请输入修改的电话(%v):", cus.Phone)phone, _ := reader.ReadString('\n')phone = strings.TrimSpace(phone)if len(phone) != 0 {cs.Customers[index].Phone = phone}fmt.Printf("请输入修改的电子邮件(%v):", cus.Email)email, _ := reader.ReadString('\n')email = strings.TrimSpace(email)if len(email) != 0 {cs.Customers[index].Email = email}return true}}return false}

customerView.go

package viewimport ("Go-Projects/Customer-Management-System/model""Go-Projects/Customer-Management-System/service""fmt")type CustomerView struct {key             string                   // 接收用户输入loop            bool                     // 表示是否循环的显示主菜单username        string                   // 用户的用户名password        string                   // 用户的密码customerService *service.CustomerService // 获取用户服务}func NewCustomerView() *CustomerView {return &CustomerView{key:             "",loop:            true,username:        "admin",password:        "password",customerService: service.NewCustomerService(),}}func (cv *CustomerView) login(username, password string) bool {if username == cv.username && password == cv.password {return true}return false}func (cv *CustomerView) addCustomer() {id := cv.customerService.CustomerNum + 1var name, gender, phone, email stringvar age intfmt.Print("请输入姓名:")fmt.Scanln(&name)fmt.Print("请输入性别:")fmt.Scanln(&gender)fmt.Print("请输入年龄:")fmt.Scanln(&age)fmt.Print("请输入电话:")fmt.Scanln(&phone)fmt.Print("请输入电子邮件:")fmt.Scanln(&email)if cv.customerService.AddCustomer(*model.NewCustomer(id, name, gender, age, phone, email)) {fmt.Println("-------------------------添加成功-------------------------")} else {fmt.Println("-------------------------添加失败-------------------------")}}func (cv *CustomerView) changeCustomer() {var id intfmt.Print("请输入修改的ID号:")fmt.Scanln(&id)if cv.customerService.ChangeCustomer(id) {fmt.Println("-------------------------修改成功-------------------------")} else {fmt.Println("-------------------------添加失败-------------------------")}}func (cv *CustomerView) deleteCustomer() {var id intfmt.Print("请输入删除的ID号:")fmt.Scanln(&id)if cv.customerService.DeleteCustomer(id) {fmt.Println("-------------------------删除成功-------------------------")} else {fmt.Println("-------------------------删除失败-------------------------")}}func (cv *CustomerView) showCustomer() {if cv.customerService.CustomerNum == 0 {fmt.Println("没有客户!")return}fmt.Println("\n-------------------------客户列表-------------------------")fmt.Println("编号\t姓名\t性别\t年龄\t电话\t电子邮件")for _, eachCustomer := range cv.customerService.ShowCustomerSlice() {fmt.Println(eachCustomer.GetInfo())}}func (cv *CustomerView) exit() {var choice bytefor {fmt.Print("确定退出?(y/n):")fmt.Scanf("%c\n", &choice)if choice == 'y' {cv.loop = falsebreak} else if choice == 'n' {break} else {fmt.Println("输入有误!!请重新输入")}}}func (cv *CustomerView) MainMenu() {for {var username, password stringfmt.Print("请输入用户名:")fmt.Scanln(&username)fmt.Print("请输入密码:")fmt.Scanln(&password)if cv.login(username, password) {break} else {fmt.Println("用户名或密码错误!")}}// 显示主菜单for cv.loop {fmt.Println("\n---------------------客户信息管理软件---------------------")fmt.Println("                         1 添加客户")fmt.Println("                         2 修改客户")fmt.Println("                         3 删除客户")fmt.Println("                         4 客户列表")fmt.Println("                         5 退    出")fmt.Print("请选择(1-5):")// 接收用户的输入fmt.Scanln(&cv.key)// 对用户的输入进行判断switch cv.key {case "1":cv.addCustomer()case "2":cv.changeCustomer()case "3":cv.deleteCustomer()case "4":cv.showCustomer()case "5":cv.exit()default:fmt.Println("请输入正确的选项------")}}}
]]>
+ + + + + Study + + + + + + + Go + + Backend + + Project + + + +
+ + + + + Go项目-家庭收支记账软件 + + /2022/11/02/Go/Go-Project-Family-Ledger/ + + Go项目-家庭收支记账软件

项目开发流程

xHqQNd.md.png

项目需求说明

  1. 模拟实现基于文本界面的《家庭记账软件》
  2. 软件能够记录家庭的收入、支出,并能够打印收支明细表

项目代码编写

主菜单的设计

func main() {// 声明一个变量保存用户的输入key := ""// 声明一个变量,控制是否退出for循环loop := true// 显示主菜单for loop {fmt.Println("---------------------家庭收支记账软件---------------------")fmt.Println("                       1 收支明细")fmt.Println("                       2 登记收入")fmt.Println("                       3 登记输出")fmt.Println("                       4 退出软件")fmt.Print("请选择(1-4):")// 接收用户的输入fmt.Scanln(&key)// 对用户的输入进行判断switch key {case "1":fmt.Println("---------------------当前收支明细记录---------------------")case "2":case "3":fmt.Println("登记支出------")case "4":loop = falsedefault:fmt.Println("请输入正确的选项------")}}fmt.Println("-------------------退出家庭收支记账软件-------------------")}

没啥有意思的,基础编程,效果如下:

xHOuYd.png

显示明细与登记输入

case "1":fmt.Println("---------------------当前收支明细记录---------------------")fmt.Println(details)case "2":fmt.Println("-------------------------登记收入-------------------------")fmt.Print("本次收入金额:")fmt.Scanln(&money)fmt.Print("本次收入说明:")fmt.Scanln(&note)balance += moneydetails += fmt.Sprintf("收  入\t%v\t%v\t%v\n", balance, money, note)fmt.Println("收入登记成功!")

其中明细是用字符串拼接实现的,实际中应该是要操作数据库的

登记支出

case "3":fmt.Println("-------------------------登记支出-------------------------")fmt.Print("本次支出金额:")fmt.Scanln(&money)if money > balance {fmt.Println("余额的金额不足!")break} else if money <= 0 {fmt.Println("支出金额应为正数!")}fmt.Print("本次支出说明:")fmt.Scanln(&note)balance -= moneydetails += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", balance+money, money, balance, note)fmt.Println("支出登记成功!")

注意支出的金额要小于账户余额,也要注意收入和支出的时候用户输入的数字需要为正数。

完善代码

退出时增加确认条件

case "4":var choice bytefor {fmt.Print("确定退出?(y/n):")fmt.Scanf("%c\n", &choice)if choice == 'y' {loop = falsebreak} else if choice == 'n' {break} else {fmt.Println("输入有误!!请重新输入")}}

注意scanf字符的时候与C语言是差不多的,需要考虑回车符号

没有记录时不输出收支详情字符串

// 判断当前是否有输入或者输出的记录flag := false

也没啥好说的,加个标志位,有记录的时候将这个标志位改掉就可以了

面向对象

将上面的面向过程的代码修改成面向对象的代码

主要思想:将记账软件的功能封装到结构体中,然后调用这个结构体的方法完成功能。

定义结构体

// 定义结构体type FamilyAccount struct {// 声明一个变量保存用户的输入key string// 声明一个变量,控制是否退出for循环loop bool// 定义账户的初始值balance float64// 定义每次收支的金额和说明money float64note  string// 收支的详情使用字符串来记录// 当有记录时对这个字符串进行拼接details string// 判断当前是否有输入或者输出的记录flag bool}

注意定义结构体的时候不能进行初始化

工厂模式返回结构体的指针

// 编写一个工厂模式的构造方法,返回结构体的指针func NewFamilyAcount() *FamilyAccount {return &FamilyAccount{key:     "",loop:    true,balance: 10000.0,money:   0.0,note:    "",details: "收  支\t收支前账户余额\t收支金额\t收支后账户余额\t说  明\n",flag:    false,}}

注意如果结构体是私有的是一定要有的,公开的也可以有,以后就要记得一定要有这样的一个方法

编写各种方法

简单改造一下面向过程的代码即可完成面向对象的效果

将显示明细写成一个方法

func (fa *FamilyAccount) showDetails() {if !fa.flag {fmt.Println("当前没有任何收支记录!")} else {fmt.Println("---------------------当前收支明细记录---------------------")fmt.Println(fa.details)}}

将登记收入写成一个方法

func (fa *FamilyAccount) income() {fmt.Println("-------------------------登记收入-------------------------")fmt.Print("本次收入金额:")fmt.Scanln(&fa.money)// 收入金额不能是负数if fa.money <= 0 {fmt.Println("收入金额应为正数!")return}fmt.Print("本次收入说明:")fmt.Scanln(&fa.note)fa.balance += fa.moneyfa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance-fa.money, fa.money, fa.balance, fa.note)fmt.Println("收入登记成功!")fa.flag = true}

将登记支出写成一个方法

func (fa *FamilyAccount) pay() {fmt.Println("-------------------------登记支出-------------------------")fmt.Print("本次支出金额:")fmt.Scanln(&fa.money)if fa.money > fa.balance {fmt.Println("余额的金额不足!")return} else if fa.money <= 0 {fmt.Println("支出金额应为正数!")}fmt.Print("本次支出说明:")fmt.Scanln(&fa.note)fa.balance -= fa.moneyfa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance+fa.money, fa.money, fa.balance, fa.note)fmt.Println("支出登记成功!")fa.flag = true}

将退出系统写成一个方法

func (fa *FamilyAccount) exit() {var choice bytefor {fmt.Print("确定退出?(y/n):")fmt.Scanf("%c\n", &choice)if choice == 'y' {fa.loop = falsebreak} else if choice == 'n' {break} else {fmt.Println("输入有误!!请重新输入")}}}

显示主菜单

func (fa *FamilyAccount) MainMenu() {// 显示主菜单for fa.loop {fmt.Println("\n---------------------家庭收支记账软件---------------------")fmt.Println("                       1 收支明细")fmt.Println("                       2 登记收入")fmt.Println("                       3 登记输出")fmt.Println("                       4 退出软件")fmt.Print("请选择(1-4):")// 接收用户的输入fmt.Scanln(&fa.key)// 对用户的输入进行判断switch fa.key {case "1":fa.showDetails()case "2":fa.income()case "3":fa.pay()case "4":fa.exit()default:fmt.Println("请输入正确的选项------")}}}

主函数中进行调用

func main() {utils.NewFamilyAcount().MainMenu()}

增加扩展功能

添加一个用户登录的功能,即只有将用户名和密码输入正确后才能打开软件,否则无法看到主界面

实现:在显示菜单之前增加一个无限循环要求用户输入用户名和密码,只有输入正确才能退出循环

for {var username, password stringfmt.Print("请输入用户名:")fmt.Scanln(&username)fmt.Print("请输入密码:")fmt.Scanln(&password)if fa.login(username, password) {break} else {fmt.Println("用户名或密码错误!")}}

用户登录的方法

func (fa *FamilyAccount) login(username string, password string) bool {if (username == fa.username) && (password == fa.password) {return true}return false}

完整源代码

面向过程的代码

package mainimport "fmt"func main() {// 声明一个变量保存用户的输入key := ""// 声明一个变量,控制是否退出for循环loop := true// 定义账户的初始值balance := 10000.0// 定义每次收支的金额和说明var money float64var note string// 收支的详情使用字符串来记录// 当有记录时对这个字符串进行拼接details := "收  支\t收支前账户余额\t收支金额\t收支后账户余额\t说  明\n"// 判断当前是否有输入或者输出的记录flag := false// 显示主菜单for loop {fmt.Println("\n---------------------家庭收支记账软件---------------------")fmt.Println("                       1 收支明细")fmt.Println("                       2 登记收入")fmt.Println("                       3 登记输出")fmt.Println("                       4 退出软件")fmt.Print("请选择(1-4):")// 接收用户的输入fmt.Scanln(&key)// 对用户的输入进行判断switch key {case "1":if !flag {fmt.Println("当前没有任何收支记录!")} else {fmt.Println("---------------------当前收支明细记录---------------------")fmt.Println(details)}case "2":fmt.Println("-------------------------登记收入-------------------------")fmt.Print("本次收入金额:")fmt.Scanln(&money)// 收入金额不能是负数if money <= 0 {fmt.Println("收入金额应为正数!")break}fmt.Print("本次收入说明:")fmt.Scanln(&note)balance += moneydetails += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", balance-money, money, balance, note)fmt.Println("收入登记成功!")flag = truecase "3":fmt.Println("-------------------------登记支出-------------------------")fmt.Print("本次支出金额:")fmt.Scanln(&money)if money > balance {fmt.Println("余额的金额不足!")break} else if money <= 0 {fmt.Println("支出金额应为正数!")}fmt.Print("本次支出说明:")fmt.Scanln(&note)balance -= moneydetails += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", balance+money, money, balance, note)fmt.Println("支出登记成功!")flag = truecase "4":var choice bytefor {fmt.Print("确定退出?(y/n):")fmt.Scanf("%c\n", &choice)if choice == 'y' {loop = falsebreak} else if choice == 'n' {break} else {fmt.Println("输入有误!!请重新输入")}}default:fmt.Println("请输入正确的选项------")}}fmt.Println("-------------------退出家庭收支记账软件-------------------")}

面向对象的代码

.
├── Family-Ledger
│ ├── main
│ │ └── main.go
│ └── utils
│ └── familyAccount.go

main.go

package mainimport ("Go-Projects/Family-Ledger/utils")func main() {utils.NewFamilyAcount().MainMenu()}

familyAccount.go

package utilsimport "fmt"// 定义结构体type FamilyAccount struct {// 用户名和密码username stringpassword string// 声明一个变量保存用户的输入key string// 声明一个变量,控制是否退出for循环loop bool// 定义账户的初始值balance float64// 定义每次收支的金额和说明money float64note  string// 收支的详情使用字符串来记录// 当有记录时对这个字符串进行拼接details string// 判断当前是否有输入或者输出的记录flag bool}// 编写一个工厂模式的构造方法,返回结构体的指针func NewFamilyAcount() *FamilyAccount {return &FamilyAccount{username: "admin",password: "password",key:      "",loop:     true,balance:  10000.0,money:    0.0,note:     "",details:  "收  支\t收支前账户余额\t收支金额\t收支后账户余额\t说  明\n",flag:     false,}}// 给结构体绑定相应的方法// 将显示明细写成一个方法func (fa *FamilyAccount) showDetails() {if !fa.flag {fmt.Println("当前没有任何收支记录!")} else {fmt.Println("---------------------当前收支明细记录---------------------")fmt.Println(fa.details)}}// 将登记收入写成一个方法func (fa *FamilyAccount) income() {fmt.Println("-------------------------登记收入-------------------------")fmt.Print("本次收入金额:")fmt.Scanln(&fa.money)// 收入金额不能是负数if fa.money <= 0 {fmt.Println("收入金额应为正数!")return}fmt.Print("本次收入说明:")fmt.Scanln(&fa.note)fa.balance += fa.moneyfa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance-fa.money, fa.money, fa.balance, fa.note)fmt.Println("收入登记成功!")fa.flag = true}// 将登记支出写成一个方法func (fa *FamilyAccount) pay() {fmt.Println("-------------------------登记支出-------------------------")fmt.Print("本次支出金额:")fmt.Scanln(&fa.money)if fa.money > fa.balance {fmt.Println("余额的金额不足!")return} else if fa.money <= 0 {fmt.Println("支出金额应为正数!")}fmt.Print("本次支出说明:")fmt.Scanln(&fa.note)fa.balance -= fa.moneyfa.details += fmt.Sprintf("收  入\t%v\t%v\t%v\t%v\n", fa.balance+fa.money, fa.money, fa.balance, fa.note)fmt.Println("支出登记成功!")fa.flag = true}// 将退出系统写成一个方法func (fa *FamilyAccount) exit() {var choice bytefor {fmt.Print("确定退出?(y/n):")fmt.Scanf("%c\n", &choice)if choice == 'y' {fa.loop = falsebreak} else if choice == 'n' {break} else {fmt.Println("输入有误!!请重新输入")}}}// 用户登录的功能func (fa *FamilyAccount) login(username string, password string) bool {if (username == fa.username) && (password == fa.password) {return true}return false}// 显示主菜单func (fa *FamilyAccount) MainMenu() {for {var username, password stringfmt.Print("请输入用户名:")fmt.Scanln(&username)fmt.Print("请输入密码:")fmt.Scanln(&password)if fa.login(username, password) {break} else {fmt.Println("用户名或密码错误!")}}// 显示主菜单for fa.loop {fmt.Println("\n---------------------家庭收支记账软件---------------------")fmt.Println("                       1 收支明细")fmt.Println("                       2 登记收入")fmt.Println("                       3 登记输出")fmt.Println("                       4 退出软件")fmt.Print("请选择(1-4):")// 接收用户的输入fmt.Scanln(&fa.key)// 对用户的输入进行判断switch fa.key {case "1":fa.showDetails()case "2":fa.income()case "3":fa.pay()case "4":fa.exit()default:fmt.Println("请输入正确的选项------")}}}
]]>
+ + + + + Study + + + + + + + Go + + Backend + + Project + + + +
+ + + + + 研究生课程:现代信息检索-第18讲 链接分析 + + /2022/10/28/UCAS/information-retrieval/information-retrieval-18/ + + 《现代信息检索》课程笔记:第18讲 链接分析

第18讲 链接分析

链接无处不在

  • 真实性和权威性的有效来源
    • 垃圾邮件-哪些电子邮件帐户是垃圾邮件发送者?
    • host质量-哪些 host 质量不好?
    • 电话呼叫记录
  • 好节点、坏节点和未知节点
    • 好节点不会指向坏节点
      • 如果一个节点指向了坏节点,那么这个节点是坏节点
      • 如果一个好节点指向这个节点,那么这个节点是好节点
    • 所有其他貌似合理的组合

为什么我们对链接分析感兴趣?

链接分析对目前为止的完全基于文本的IR任务进行了补充

  • (文档)评分和排序
  • 基于链接的聚类-来自链接的主题结构
  • 链接作为分类特征-彼此链接的文档可能是同一主题
  • 爬虫-根据已看到的链接,我们下一步要爬取哪里?

Web可以看成一个有向图

  • 假设1: 超链接代表了某种质量认可信号
  • 假设2: 锚文本描述了文档d2 的内容

对锚文本构建索引

  • 因此,锚文本往往比网页本身更能揭示网页的内容
  • 在计算过程中,锚文本应该被赋予比文档中文本更高的权重

PageRank背后的假设

  • 假设1:Web 上的链接是网页质量的标志-链出网页的作者认为链向的网页具有很高的质量
  • 假设2:锚文本能够描述链向网页的内容

Google炸弹:指由于人为恶意构造锚文本而导致的结果很差的搜索。用户群体有意创建链接误导搜索引擎

锚文本索引:将从指向文档D的链接的锚文本(也可能包含锚文本附近的文本)包含在D的索引中

有时会产生低于期望的效果,例如:垃圾邮件过滤应用全然失败

可以根据锚页面网站的权威性对锚文本进行加权

链接服务器:低成本地获取所有链接信息

  • 支持网络图上的快速查询
  • 将映射存储在内存中
  • 应用:链接分析、网络图分析、爬虫控制

Boldi and Vigna:基本目标-维护内存中的节点邻接表

邻接表压缩中利用到的属性:

  • 相似度(邻接表之间)
  • 位置(一个页面中的许多链接都连接到“附近”的页面)
  • 在已排序的邻接表中使用间隔编码
  • gap value的分布

间隔编码

给出整数x,y,z 的已排序列表,用 x y-x z-y 来对 x,y,z 进行表示

使用编码来压缩整数

BV算法的主要优势

  • 仅依赖于位置的规范顺序
    • 字典顺序对web十分适用
  • 邻接查询可以被非常高效地回答
    • 要获取外部邻居,需要回溯到链的原型
    • 在实践中,这条链通常很短(因为相似性主要基于host 内部)
    • 编码过程中也可以明确限制链的长度
  • 易于实现one pass 算法
    • 顺序读取,不需要无限缓冲。读取复杂度与网页数量是线性关系

引用分析

引用分析:科技文献中的引用分析

另一个应用:引用频率可以用度量一篇文档的影响度

更好的度量方法:对不同网页来的引用频率进行加权

PageRank

  • 一个网页如果它的入链越多,那么它也越重要(PageRank 越高)
  • 一个网页如果被越重要的网页所指向,那么它也越重要(PageRank 越高 )

原始PageRank的一个不足:图中存在一个循环通路,每次迭代,该循环通路中的每个节点的 PageRank不断增加,但是它们并不指出去,即不将PageRank分配给其他节点!

改进的PageRank公式:随机冲浪或随机游走(Random Walk)模型

HITS: Hub节点&Authority节点

每个网页计算两个值:

Hub:作为目录型或导航型网页的权重

Authority:作为权威型网页的权重

一个网页被越重要的导航型网页指向越多,那么它的Authority越大;

一个网页指向的高重要度权威型网页越多,那么它的Hub越大。

HITS算法也是收敛的,也可以通过迭代的方式计算。

HITS算法的实际计算过程

  • 首先进行Web 搜索;
  • 搜索的结果称为根集(从搜索结果中选择一部分排名靠前的网页作为根集,也叫做种子集合)
  • 将所有链向种子集合和种子集合链出的网页加入到种子集合;
  • 新的更大的集合称为基本集
  • 最后,在基本集上计算每个网页的hub值和authority值(该基本集可以看成一个小的Web图)。

PageRank vs. HITS

网页的PageRank 与查询主题无关,可以事先算好,因此适合于大型搜索引擎的应用。

HITS算法的计算与查询主题相关,检索之后再进行计算,因此,不适合于大型搜索引擎。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:现代信息检索-第17讲 信息采集 + + /2022/10/25/UCAS/information-retrieval/information-retrieval-17/ + + 《现代信息检索》课程笔记:第17讲 信息采集

第17讲 信息采集

一个简单的采集器

基本的采集过程

  • 初始化采集URL 种子队列
  • 重复如下过程
    • 从队列中取出URL
    • 下载并分析网页
    • 从网页中抽取更多的URL
    • 将这些URL 放到队列中

上述简单采集器的问题:

  • 规模问题:必须要分布式处理
  • 我们不可能索引所有网页,必须要从中选择部分网页,如何选择?
  • 重复网页:必须要集成重复检测功能
  • 作弊网页和采集器陷阱:必须要集成作弊网页检测功能
  • 礼貌性问题:对同一网站的访问按遵照协议规定,并且访问的间隔必须要足够
  • 新鲜度问题:必须要定期更新或者重采
    • 由于Web 的规模巨大,我们只能对一个小的网页子集频繁重采
    • 同样,这也存在一个选择或者优先级问题

采集器必须做到

  • 礼貌性
    • 不要高频率采集某个网站
    • 仅仅采集robots.txt 所规定的可以采集的网页
  • 鲁棒性
    • 能够处理采集器陷阱、重复页面、超大页面、超大网站、动态页面等问题v

任意一个采集器应该做到:

  • 能够进行分布式处理
  • 支持规模的扩展:能够通过增加机器支持更高的采集速度
  • 优先采集高质量网页
  • 能够持续运行:对已采集网页进行更新

一个真实的采集器

待采集URL池:

  • 待采集URL池是一个数据结构,它存放并管理那些已经看到但是还没有采集的URL集合
  • 可能包含来自同一主机的不同页面
  • 必要要避免在同一时间采集这些来自同一主机的页面
  • 必须要保证采集线程任务饱和

基本的采集架构

URL规范化

内容重复判别

  • 对每个抓取的页面,判断它是否已在索引当中
  • 可以采用文档指纹或者shingle 的方法判别
  • 忽略那些已经在索引中的重复页面

分布式采集

  • 运行多个采集线程,这些线程可以分布在不同节点上
    • 这些节点往往在地理上分散在不同位置
  • 将采集的主机分配到不同节点上

分布式采集器

待采集URL池 : 主要考虑两点

  • 礼貌性: 不要非常频繁第访问某个 Web 服务器
    • 比如,可以在两次服务器访问之间设置一个时间间隔
  • 新鲜度: 对某些网站的采集频率如新闻网站要高于其他网站

采集器陷阱

  • 一些恶意的服务器可以产生无穷的链接网页序列
  • 一些复杂的采集器陷阱产生的页面不能简单地判断为动态页面
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:现代信息检索-第16讲 Web搜索 + + /2022/10/22/UCAS/information-retrieval/information-retrieval-16/ + + 《现代信息检索》课程笔记:第16讲 Web搜索

第16讲 Web搜索

互联网上的搜索

搜索是Web上使用最多的应用之一

没有搜索引擎,Web甚至无法运转

  • 没有搜索,很难找到所需的内容
  • 没有搜索,在Web上创建内容也就缺了动机
    • 如果没人看为什么要发布内容?
    • 如果没有任何回报为什么要发布内容?
  • Web运转必须要有人买单
    • 服务器、Web 基础设施、内容创建过程等需要费用支持
    • 这些费用的相当大一部分都是通过搜索广告支付
    • 可以说,搜索为Web 买单

兴趣聚合:具有相同兴趣的人,即使所处地理位置分散,也可以通过Web找到对方。

搜索引擎是实现兴趣聚合的关键事物

在Web上,搜索不仅仅是一个好的特点

Web是一个充满噪声数据且组织失调的集合体→大量的重复需要检测

用户可以(某种意义上)无控制和无限制地发布内容→大量作弊内容需要检测

互联网广告

传统广告:品牌广告、直接营销、

传统广告的不足:

  • 广告投放场地或媒介相对有限:报纸、电视、杂志、橱窗、公汽、电梯等
  • 广告场地的费用一般不菲:CCTV 标王
  • 很难进行个性化
  • 投放效果取决于广告商的智慧
  • 投放效果很难度量

互联网广告的优点:

  • 无限机会
  • 无限创意
  • 完全可以个性化处理
  • 每次点击花费的代价很低
  • 定量度量程度高

互联网广告的主要形式:图片广告、文本广告、搜索广告、网页广告、

第一代搜索广告:Goto

第二代搜索广告:Google

如何对广告排序?

  • 简单的方法:按照类似 Goto 的方式,即按照投标价格排序
  • 替代方法:按照投标价格和相关性排序(相关度度量的关键指标:点击率)

Web查询“长尾”现象:基于AOL查询频次的统计、基于查询频次的流量统计

长尾效应的解释

  • 大多数用户搜索“常见”查询;一小部分用户搜索“罕见”查询
  • 大量用户使用“常见”查询;同时大量用户也会使用一些“罕见”查询

重复检测

  • Web上充斥重复内容
  • 相对其它文档集合,Web 上的重复内容更多
  • 完全重复:易剔除,比如采用哈希指纹的方法
  • 近似重复:Web上存在大量近似重复,很难剔除
  • 对用户而言,如果搜索结果中存在不少几乎相同的页面,那么体验非常不好
  • 边缘相关度(Marginal relevance) 为 0 :如果一篇高度相关的文档出现在另一篇高度近似的文档之后,那么该文档变得不相关
  • 必须要去除这些近似重复

近似重复的检测:采用编辑距离指标计算页面之间的相似度

将每篇文档表示成一个shingle 集合

每个shingle 是一个基于词语的 n-gram

使用shingle 来计算文档之间的语法相似度

两个文档的相似度定义为它们的shingle 集合的Jaccard距离

每篇文档的shingle的个数非常大

为提高效率,接下来我们使用文档的梗概来表示文档,它由文档的shingle集合中精巧挑选出的子集构成

高效的近似重复检测:局部敏感哈希或排序

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:高级人工智能-第8讲 逻辑 + + /2022/10/20/UCAS/advanced-ai/advanced-ai-8/ + + 《高级人工智能》课程笔记:第8讲 逻辑

第8讲 逻辑

什么是“数理逻辑”?

  • 一种“算法”:输入+输出,不仅要得到算法,还要证明是正确的
  • 一种“关于证明、推理等思考活动”的算法

一个算法,以任何作为输入,输出的都是正确答案

输入:

  • 知识库:任意的问题假设,前提条件等
  • 查询:想要解决的问题

输出答案:该查询在此知识库上的正确答案

如果有上面的算法,那么所有难题都能得到解决

如果有这样的一种“终极算法”,首先要将自然语言表达的知识库和查询表示成形式语言表达的知识库和查询,然后通过自动的知识推理,得到形式语言表达的答案

逻辑的研究内容

解决如下问题:

  • 关于逻辑的形式语言是什么
  • 在形式语言上的自动推理的算法是什么
    • 该算法复杂度如何,是否可以更高效
  • 自动推理的算法是否正确
    • 算法正确性的严格证明

研究形式化定义的sentences之间的关系

x6PrGD.md.png

左侧是语义的蕴含关系(逻辑推导),,从知识库出发一定正确的知识

右侧是语法的演绎关系(形式推演),,通过算法可以从知识库推出的

如果左侧的是右侧的子集,说明正确的结论都在算法推导的里面,那么说明这个算法是完备的,但是有一些结论可能算法计算出来是错误的

如果右侧的是左侧的子集,说明算法推出来的结论都是正确的,因此算法是可靠的,但是有可能有一些正确的结论算法算不出来

如果兼具完备性和可靠性,那么证明这个算法是正确的。

语义

如果的条件下是 true,那么称是句子的一个 model,句子的所有model的集合是

KB指的是一些句子的集合

:在任意的条件下(一个真值指派)只要成立,一定成立,称为 KB蕴含

因此完全等价(当且仅当)(是不可满足的)

命题逻辑

语法(逻辑推导)

命题是一种声明,要么是真的,要么是假的,不存在第三种可能

命题逻辑通常不考虑时间

原子命题指的是最小的命题,用大写字母表达

文字是原子命题,或者是原子命题的否

一个句子是一个原子句或者复杂句

一个原子句表示为:

复杂句有五种表示形式,与复杂句之间的真值表:

falsefalsetruefalsefalsetruetrue
falsetruetruefalsetruetruefalse
truefalsefalsefalsetruefalsefalse
truetruefalsetruetruetruetrue

连接词和集合之间的联系:

两个句子是逻辑等价的-两个句子的model相同: 当且仅当

定理:

KB: 满足命题逻辑语法的sentence的集合

假设:这组sentence中,一共有n个原子命题

真值指派(truth assignment):对每个原子名字赋值

一共有种真值指派,其中:使得KB中的每个sentence都为真的真值指派,就是KB的model

在此基础上,在命题逻辑中,我们可以明确的定义

  • 如果一个句子在任意的model下面都为true,则这个句子是永真的
  • 演绎定理: 当且仅当是永真的
  • 如果一个句子在某些model下为真,则称这个句子是可满足的
  • 如果一个句子在任何model下都为假,则称这个句子是不可满足的
  • 当且仅当是不可满足的

蕴含,不是连接词:描述的是蕴含的一种关系,有了知识表示后,额外推出其他的知识

命题逻辑里面的连接词,用于知识表示(实际上是可以替代的,但是引入这个符号进行知识表示比较方便)

形式推演

推出:,通过算法可以从知识库推出的

共有两套规则(11条规则和归结原理)

11条形式推演规则:(不需要背诵)

形式可推演性:A在命题逻辑中由形式可推演的,记作,当且仅当能由(有限次使用)命题逻辑的形式推演规则生成

句子可以通过规则从KB中得出,记作

可靠性:任意时刻当时,同时成立,那么说是可靠的

完备性:任意时刻当时,同时成立,那么说是完备的

归结原理

合取范式:子句(文字和析取符号)的合取形式,子句内部是没有合取的(CNF)转换为合取范式是多项式时间复杂度的

归结原理:互补文字可以互相消去(但是每一次只能消去一对)

归结原理是既可靠又完备的

证明:若当且仅当,其中仅使用归结法则获得新子句

使用上述证明来证明知识库可以推出某个子句

证明:归结原理既可靠又完备

在研究可靠性与完备性问题时,应当把语法层面的知识理解为Groundtruth

因此可靠性可以大概表述为:语义上推演得到的知识在语法上正确。因此要证明归结原理的可靠性,即证明

xqkuwj.md.png

使用真值表进行证明即可

完备性可以大概表述为:如果语法上能够推理得到的,那么语义上正确。

即证明:如果,则

RC(S):对S内部的全部子句进行归结后的集合。

完备性证明

等价于永假,等价于是不可满足的。

等价于可以归结出空子句,即RC(S)包含空子句

则只需要证明:如果是不可满足的,则RC(S)包含空子句

等价于证明逆否命题:如果RC(S)不包含空子句,则是可满足的

证明:针对S中的原子命题,我们构造如下的model:

首先,因为RC(S)中不包含空集,即RC(S)中不包含永假的子句。

, 顺序指派的真值:

如果RC(S)中包含一个子句,此子句包含,且此子句的其它文字都已经被指派为False(在之前的步骤中进行的)或不包含其它文字,则把指派为False;否则,把指派为True

我们用反证法证明:这个真值指派使得RC(S)中的子句都为真

假设,在此过程的第i步,我们这样来指派使得某个子句C为False,且假设这是首次出现False的子句;此时,子句C只能是如下两种形式之一:或者

显然,如果RC(S)中只包含以上两个子句之一,子句C是不会在此真值指派中为False的。因此, RC(S)此时应该同时包含了以上两个子句。

以上两个子句显然是满足归结条件的,也就是说,它归结后的子句也应该在RC(S)中;同时,该子句已经被指派为False了;这与我们之前的假设矛盾。

因此这个真值指派使得RC(S)中的子句都为真,进而S是可满足的。

可以转换为搜索问题,如何使用A*搜索实现呢?

Modus Ponens规则

以限制知识库里面的句子形式为代价,获得时间复杂度上的提升

上述提到的归结原理具有完备性,这是很好的性质,对于许多现实世界的应用,如果添加一些限制,可以实现更高效的推理。为了换取更好的inference的时间效率,缩小命题逻辑的表达范围,得到适用于Horn Form的Modus Ponens规则,是另外一种形式的归结原理。

KB为Definite clause的合取形式:

xq1HJS.png

Definite clause: 每一个句子有且只有一个文字是正文字的析取形式

只有两种形式:①原子命题②命题的合取另外一个命题

Horn clause: 每一个句子最多一个文字是正文字的析取形式

PPT例子:KB是全部句子的情况下是否能推出Q

前向推理:从条件出发去推结论

前向推理是数据驱动的,可能推出一些结论与我们要推出的结论是无关的

后向推理:从结论返回推出条件

后向推理是目的驱动的,找为了推出这个结论所需要的条件,因此通常情况下后向推理比前向推理好,但是也存在某种情况前向推理比后向推理好

(全连接神经网络)

Modus Ponens规则证明

证明是可靠的,即证明

通过真值表进行证明即可

证明是完备的:

。此时,中仅包含definite子句,仅使用Modus Ponens规则,且是一个正文字

证明:RC(KB)是KB中原始的句子和通过Modus Ponens推出的句子的全部集合

  1. 构造如下的真值指派:对于任意的symbol a,a指派为True当且仅当

(如果一个正文字在中,就设为True,不在就设置为False)

  1. 接下来证明:在下,为真。

反证:若此时为False,那么:必存在一个definite子句,在下为False。

若该子句为 也就是说,在m中,均为True,且为False。根据1中的定义, ,又根据Modus Ponens规则,根据1中的定义,在中, 为True。推出矛盾。

若该子句为,在下为为False,则,矛盾

  1. ,根据蕴含的定义:在中,为真;则根据1中的定义,,也就是说:

命题逻辑的缺点:能表达的东西比较有限。

一阶谓词逻辑

语法和语义

命题逻辑假设世界上都是事实(fact),一阶谓词逻辑认为世界上还包括对象、关系和函数等等。

基本元素:

  1. 常量和变量
  2. 谓词(哥哥、大于等)Predicates
  3. 函数
  4. 连接词(与命题逻辑的连接词完全相同)
  5. 为真当且仅当指向现实世界中的同一个对象
  6. 量词:全称量词和存在量词

简单句与复杂句

简单句:

或常量或变量

嵌套函数会造成很大的问题。命题逻辑的算法一定会停止(decidable可判定的),但是由于嵌套函数的存在,谓词逻辑只是半可判定的。

复杂句:使用连接词对简单句进行连接构成复杂句

量词

在谓词逻辑中,要将每一个符号指派到现实世界中,将常量转化为对象、将谓词转化为关系、将函数符号转化为真正的函数

量词与变量是对应的,有变量一定要有量词来量化

全称量词:变量所有实例的合取形式

错误的形式:

存在量词:变量所有实例的析取形式

错误的形式:

量词的属性关系

两种量词之间可以相互转换

一阶谓词的形式推演(命题化)

全称实例化:实例化全称量词蕴含的每一个实例

注意在实例化的过程中,第n次循环只能嵌套n次函数

因此算法可能不会停止,为semi-decidable的

存在实例化:赋予一个新的常量符号

一阶谓词逻辑的归结原理

去掉存在量词和存在量词修饰的变量,使得句子里面的每一个变量都是全称量词修饰的变量,且为合取范式

合一算子:替换后等价的替换方式(只能将常量赋值给变量)

归结原理:

尤其注意要赋值

归结原理既完备又可靠,证明比较复杂不讲

归结策略

可能有很多的归结策略,选择哪种方式进行归结呢?

没有一种归结策略适用于全部情况

广度优先策略:扩展所有可能的情况然后归结

优点:

  • 当问题有解时保证能找到最短归结路径。
  • 是一种完备的归结策略。

缺点:

  • 归结出了许多无用的子句
  • 既浪费时间,又浪费空间

广度优先对大问题的归结容易产生组合爆炸,但对小问题却仍是一种比较好的归结策略。

常用的归结策略可分为两大类:

  • 删除策略是通过删除某些无用的子句来缩小归结范围
  • 限制策略是通过对参加归结的子句进行某些限制,来减少归结的盲目性,以尽快得到空子句。
删除策略

删除法主要想法是:把子句集中无用的子句删除掉,这就会缩小搜索范围,减少比较次数,从而提高归结效率。

删除纯文字:

  • 如果某文字在子句集中不存在可与其互补的文字,则称该文字为纯文字。
  • 在归结过程中,纯文字不可能被消除,用包含纯文字的子句进行归结也不可能得到空子句
  • 对子句集而言,删除包含纯文字的子句,是不影响其不可满足性的。

重言式删除法:

  • 如果一个子句中包含有互补的文字对,则称该子句为重言式。
  • 重言式是真值为真的子句。对一个子句集来说,不管是增加还是删除一个真值为真的子句,都不会影响该子句集的不可满足性。
  • 因此,可从子句集中删去重言式。
限制策略

限制策略要慎重,防止可以得到空子句但是限制后就得不到空子句了

支持集策略:每一次参加归结的两个亲本子句中,至少应该有一个是由目标公式的否定所得到的子句或它们的后裔。(就是别自己本身进行归结,带上一起归结)

支持集策略是完备的,即当子句集为不可满足时,则由支持集策略一定能够归结出一个空子句。

  • 可以把支持集策略看成是在广度优先策略中引入了某种限制条件,这种限制条件代表一种启发信息,因而有较高的效率
  • 支持集策略限制了子句集元素的剧增,但会增加空子句所在的深度(结果可能不是最优)。
  • 支持集策略具有逆向推理的含义,由于进行归结的亲本子句中至少有一个与目标子句有关,因此推理过程可以看作是沿目标、子目标的方向前进的。

单文字子句策略:每次参加归结的两个亲本子句中至少有一个子句是单文字子句

采用单文字子句策略,归结式包含的文字数将少于其非单文字亲本子句中的文字数,这将有利于向空子句的方向发展,因此会有较高的归结效率。

单文字子句策略是不完备的,即当子句集为不可满足时,用这种策略不一定能归结出空子句。原因: 没有可用的单文字字句

祖先过滤策略:满足以下两个条件中的任意一个就可进行归结:

  • 两个亲本子句中至少有一个是初始子句集中的子句。
  • 如果两个亲本子句都不是初始子句集中的子句,则一个子句应该是另一个子句的先辈子句。

祖先过滤策略是完备的

Generalized Modus Ponens(前见推理)

GMP的可靠性证明:将量词去掉变量替换为,使用命题逻辑的Modus Ponens证明即可

同样有前向推理和后向推理,同样是半可判定的

但是,如果仅包含一阶谓词的definite子句且没有函数,那么是decidable的(也叫Datalog)

模糊计算

清晰的概念:对象是否属于这个概念是明确的。

模糊性的概念:对象从属的界限是模糊的,随判断人的思维而定

取得精确数据不可能或很困难,也没有必要获取精确数据

要使计算机能够模仿人脑,对复杂系统进行识别和判断,出路何在?

1965年扎德(Zadeh)教授开创了对“模糊数学”的研究。他认为数学是可以模糊的,主张从精度方面“后退”一步。他提出用隶属函数使模糊概念数学化。

模糊集的定义

是给定论域,是把任意映射为上某个实值的函数,即,则称为定义在上的一个隶属函数,由(对所有)所构成的集合称为上的一个模糊集,称为的隶属度。

模糊集完全是由隶属函数来刻画的,中的每一个元素都映射为上的一个值

的值表示隶属于的程度,其值越大,表示隶属于的程度越高。当仅取时,模糊集便退化为一个普通集合。

模糊性:事件发生的程度,而不是一个事件是否发生

随机性:描述事件发生的不确定性,即一个事件发生与否

模糊集的表示

离散且为有限论域的表示方法

设论域为离散论域,则其模糊集可表示为:

为了能够表示出论域中的元素与其隶属度之间的对应关系,扎德引入了一种模糊集的表示方式:先为论域中的每个元素都标上其隶属度,然后再用“+”号把它们连接起来,即,其中的隶属度;“”不是相除关系,只是一个记号;“+”也不是算术意义上的加,只是一个连接符号。

连续论域的表示方法:如果论域是连续的,则其模糊集可用一个实函数来表示。

模糊集的运算

分别是上的两个模糊集,对任意,都有成立,则称等于,记为

分别是上的两个模糊集,对任意,都有成立,则称包含,记为

分别是上的两个模糊集,则分别称为的并集、交集,它们的隶属函数分别为:

上的模糊集,称的补集,其隶属函数为:

两个模糊集之间的运算实际上就是逐点对隶属函数作相应的运算

模糊关系

经典集合的关系:

笛卡尔积:设是两个普通集合,的笛卡尔乘积为

的关系上的一个子集,即,记为

对于中的元素,若,则称有关系;若,则称没有关系

模糊集合的关系:在二元关系上定义隶属度函数

上的模糊集,则称

的笛卡尔乘积,它是上的一个模糊集

上的一个元模糊关系是指以为论域的一个模糊集,记为

分别是上的两个模糊关系,则的合成是从的一个模糊关系,记为。其隶属函数为,其中其中,分别表示取最小和取最大

模糊逻辑

模糊逻辑:定义模糊谓词、模糊量词、模糊修饰语等

模糊谓词:设为模糊谓词,即U中的一个模糊关系,则模糊命题可表示为,其中的模糊谓词可以是大、小、年轻、年老、冷、暖、长、短等。

模糊量词:模糊逻辑中使用的模糊量词,如极少、很少、几个、少数、多数、大多数、几乎所有等。

模糊修饰语:

是模糊修饰语,是变量,是模糊谓词,则模糊命题可表示为为,模糊修饰语也称为程度词,常用的程度词有“很”、“非常”、“有些”、“绝对”等。

模糊修饰语的四种主要运算:

  1. 求补:表示否定,如“不”、“非”等,其隶属函数的表示为:
  2. 集中:表示“很”、“非常”等,其效果是减少隶属函数的值:
  3. 扩张:表示“有些”、“稍微”等,其效果是增加隶属函数的值:
  4. 加强对比:表示“明确”、“确定”等,其效果是增加0.5以上隶属函数的值,减少0.5以下隶属函数的值:

演化计算

演化计算(Evolutionary Computation, EC):

  • 在基因和种群层次上模拟自然界生物进化过程与机制的问题求解技术和计算模型。
  • 思想源于生物遗传学和适者生存的自然规律
  • 基于达尔文(Darwin)的进化论和孟德尔(Mendel)的遗传变异理论
    • 达尔文的自然选择学说是一种被人们广泛接受的生物进化学说:
      • 生物要生存下去,就必须进行生存斗争。
      • 具有有利变异的个体容易存活下来,并且有更多的机会将有利变异传给后代;具有不利变异的个体就容易被淘汰,产生后代的机会也少的多。
      • 适者生存,不适者淘汰:自然选择。
      • 遗传和变异是决定生物进化的内在因素。(相对稳定+新的物种)

典型代表:

  • 遗传算法(Genetic Algorithm, GA)
  • 进化策略(Evolutionary Strategy, ES)
  • 进化规划(Evolutionary Programming, EP)
  • 遗传规划(Genetic Programming, GP)

演化计算:一种模拟自然界生物进化过程与机制进行问题求解的自组织、自适应的随机搜索技术。

演化规则:“物竞天择、适者生存”

演化操作:繁殖(Reproduction)、变异(Mutation)、竞争(Competition)、选择(Selection)

遗传算法

遗传算法的基本思想是从初始种群出发,采用优胜劣汰、适者生存的自然法则选择个体,并通过杂交、变异来产生新一代种群,如此逐代进化,直到满足目标为止

基本概念:

  • 种群(Population):多个备选解的集合。
  • 个体(Individual):种群中的单个元素,通常由一个用于描述其基本遗传结构的数据结构来表示。例如,长度为L的0、1串。
  • 适应度(Fitness)函数:用来对种群中各个个体的环境适应性进行度量的函数,函数值是遗传算法实现优胜劣汰的主要依据
  • 遗传操作(Genetic Operator):作用于种群而产生新的种群的操作。选择(Selection)、交叉(Cross-over)、变异(Mutation)

遗传算法主要由染色体编码、初始种群设定、适应度函数设定、遗传操作设计等几大部分所组成,

算法基本步骤:

  1. 选择编码策略,将问题搜索空间中每个可能的点用相应的编码策略表示出来,即形成染色体;
  2. 定义遗传策略,包括种群规模N,交叉、变异方法,以及选择概率Pr、交叉概率Pc、变异概率Pm等遗传参数;
  3. 令t=0,随机选择N个染色体初始化种群P(0);
  4. 定义适应度函数f;
  5. 计算P(t)中每个染色体的适应值;
  6. t=t+1;
  7. 运用选择算子,从P(t-1)中得到P(t);
  8. 对P(t)中的每个染色体,按概率Pc参与交叉;
  9. 对染色体中的基因,以概率Pm参与变异运算;
  10. 判断群体性能是否满足预先设定的终止标准,若不满足返回(5)。

遗传编码

二进制编码

二进制编码是将原问题的结构变换为染色体的位串结构。假设某一参数的取值范围是。用长度为的二进制编码串来表示该参数,将等分成个子部分,记每一个等分的长度为

优点:易于理解和实现,可表示的模式数最多

缺点:海明悬崖。当算法从7改进到8时,就必须改变所有的位

格雷编码

要求两个连续整数的编码之间只能有一个码位不同,其余码位都是完全相同的。有效地解决了海明悬崖问题。

基本原理:

  • 二进制码->格雷码(编码):从最右边一位起,依次将每一位与左边一位异或,作为对应格雷码该位的值,最左边一位不变;
  • 格雷码->二进制码(解码):从左边第二位起,将每位与左边一位解码后的值异或,作为该位解码后的值,最左边一位依然不变。

符号编码

个体染色体编码串中的基因值取自一个无数值含义,而只有代码含义的符号集。

适应度函数

适应度函数是一个用于对个体的适应性进行度量的函数。个体的适应度值越大,它被遗传到下一代种群中的概率越大

常用的适应度函数

  • 原始适应度函数:直接将待求解问题的目标函数定义为遗传算法的适应度函数。
    • 例如:求最大值时,即为的原始适应度函数。
    • 优点:能够直接反映出待求解问题的最初求解目标
    • 缺点:有可能出现适应度值为负的情况
  • 标准适应度函数
    • 在遗传算法中,一般要求适应度函数非负,并其适应度值越大越好。这就往往需要对原始适应函数进行某种变换,将其转换为标准的度量方式,以满足进化操作的要求,这样所得到的适应度函数被称为标准适应度函数

基本遗传操作

选择(selection)操作:根据选择概率按某种策略从当前种群中挑选出一定数目的个体,使它们能够有更多的机会被遗传到下一代中

  • 比例选择:各个个体被选中的概率与其适应度大小成正比。
  • 轮盘赌选择:个体被选中的概率取决于该个体的相对适应度。,其中,是个体的相对适应度,即个体被选中的概率,是个体的原始适应度。

交叉(crossover)操作:按照某种方式对选择的父代个体的染色体的部分基因进行交配重组,从而形成新的个体。

二进制交叉:二进制编码情况下所采用的交叉操作

  • 单点交叉:先在两个父代个体的编码串中随机设定一个交叉点,然后对这两个父代个体交叉点前面或后面部分的基因进行交换,并生成子代中的两个新的个体。
  • 两点交叉:先在两个父代个体的编码串中随机设定两个交叉点,然后再按这两个交叉点进行部分基因交换,生成子代中的两个新的个体。
  • 均匀交叉:先随机生成一个与父串具有相同长度的二进制串(交叉模版),然后再利用该模版对两个父串进行交叉,即将模版中1对应的位进行交换,而0对应的位不交换,依此生成子代中的两个新的个体。

实值交叉:在实数编码情况下所采用的交叉操作,主要包括离散交叉和算术交叉

  • 部分离散交叉:先在两个父代个体的编码向量中随机选择一部分分量,然后对这部分分量进行交换,生成子代中的两个新的个体。
  • 整体交叉:对两个父代个体的编码向量中的所有分量,都以的概率进行交换,从而生成子代中的两个新的个体。

变异(Mutation)操作:对选中个体的染色体中的某些基因进行变动,以形成新的个体。遗传算法中的变异操作增加了算法的局部随机搜索能力,从而可以维持种群的多样性。

  • 二进制变异:先随机地产生一个变异位,然后将该变异位置上的基因值由“0”变为“1”,或由“1”变为“0”,产生一个新的个体。
  • 实值变异:用另外一个在规定范围内的随机实数去替换原变异位置上的基因值,产生一个新的个体。
    • 基于次序的变异:先随机地产生两个变异位置,然后交换这两个变异位置上的基因。

精英主义 (Elitism)

仅仅从产生的子代中选择基因去构造新的种群可能会丢失掉上一代种群中的很多信息。也就是说当利用交叉和变异产生新的一代时,我们有很大的可能把在某个中间步骤中得到的最优解丢失。

使用精英主义(Elitism)方法,在每一次产生新的一代时,我们首先把当前最优解原封不动的复制到新的一代中,其他步骤不变。这样任何时刻产生的一个最优解都可以存活到遗传算法结束。

遗传算法特点

  • 自组织、自适应和自学习性—概率转移准则,非确定性规则
    • 确定进化方案后,算法将利用进化过程中得到的信息自行组织搜索;基于自然的选择策略,优胜劣汰;
    • 遗传算法很快就能找到良好的解,即使是在很复杂的解空间中
      • 采用随机方法进行最优解搜索,选择体现了向最优解迫近
      • 交叉体现了最优解的产生,变异体现了全局最优解的复盖
  • 本质并行性—群体搜索
    • 算法本身非常适合大规模并行,各种群分别独立进化,不需要相互间交换信息
    • 可以同时搜索解空间的多个区域并相互间交流信息,使得演化计算能以较少的计算获得较大的收益。
  • 不需要其他知识,只需要影响搜索方向的目标函数和相应的适应度函数
    • 对待求解问题的指标函数没有什么特殊的要求,如不要求连续性、导数存在、单峰值等假设
    • 容易形成通用算法程序
    • 遗传算法不能解决那些“大海捞针”的问题,所谓“大海捞针”问题就是没有一个确切的适应度函数表征个体好坏的问题,遗传算法对这类问题无法找到收敛的路径。
  • 理论上证明算法的收敛性很困难
  • 多用于解决实际问题
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + KMP算法详解 + + /2022/10/19/KMP/ + + KMP算法详解

KMP算法详解

一直都没弄明白,也没下决心去弄明白。昨天感觉基本上差不多了,整理一下,再加深一下印象。

问题

给你两个字符串 haystackneedle,请你在 haystack字符串中找出 needle字符串的第一个匹配项的下标(下标从 0开始)。如果 needle不是 haystack的一部分,则返回 -1

AC代码:

func strStr(haystack string, needle string) int {    needlelen := len(needle)    haystacklen := len(haystack)    next := make([]int,needlelen)    next[0] = 0    j := 0    for i:=1;i<needlelen;i++{        for j > 0 && needle[i] != needle[j]{            j = next[j-1]        }        if needle[i] == needle[j]{            j++        }        next[i] = j    }    j = 0    for i:=0;i<haystacklen;i++{        for j > 0 && needle[j] != haystack[i]{            j = next[j-1]        }        if needle[j] == haystack[i]{            j++        }        if j == needlelen{            return i-j+1;        }    }    return -1}

简介

判断一个字符串(模式串)是不是另外一个字符串(文本串)的子串,怎么做?

最容易想到的:暴力匹配。

比如有下面的两个字符串:

abacacac

开始肯定是第一个 a开始和 ac进行匹配,匹配失败了,然后从 b再开始匹配。最坏情况,每一个都要判断到匹配字符串的最后一个字符,两层循环,时间复杂度很容易想到就是

但是事实上,如果从人工匹配的角度来看,我们都知道 b不可能匹配成功,让你用肉眼匹配,傻子才会去看 b。但是计算机程序为了全部判断还是要去尝试一下。

那么怎么把这种无效的匹配让开呢?直观上可能想到,我判断第一个能不能匹配上不就行了,应该能降低时间复杂度?

那么再举一个例子:aaaaaaaaaaab,时间复杂度一样是

所以不仅仅要看第一个,看第一个也无法完全抹去无效的匹配。这时候需要一种高效的匹配算法,核心思想就是在匹配的过程中要记录,匹配失败后从第一个可能成功的地方开始即可,不要做无效工作。

因此就有了超难理解的KMP算法以及各种比KMP还要复杂的算法。这里就先好好的讲一下KMP,希望以后可以真正理解,抬手就来。

概念

前缀表:记录下标 i之前(包括 i)的字符串中,有多大长度的相同前缀后缀。

前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

啥意思?举例子就好了

模式串下标0123456789101112
字符串abcdabcabcdab
前缀表0000123123456

怎么算的?

下标为 0,字符串为 a,前缀为空(因为不包含最后一个字符,因此字符就没了),后缀为空(因为不包含第一个字符,因此字符就没了),因此相同前缀后缀长度为0(因为都是空串)

下标为 1,字符串为 ab,前缀为 a,后缀为 b,因此相同前缀后缀长度为 0

下标为 2,字符串为 abc,前缀为 ab,后缀为 bc,因此相同前缀后缀长度为 0

下标为 3,字符串为 abcd,前缀为 abc,后缀为 bcd,因此相同前缀后缀长度为 0

下标为 4,字符串为 abcda,前缀为 abcd,后缀为 bcda,因此相同前缀后缀长度为 1,也就是 a

下标为 5,字符串为 abcdab,前缀为 abcda,后缀为 bcdab,因此相同前缀后缀长度为 2,也就是 ab

下标为 6,字符串为 abcdabc,前缀为 abcdab,后缀为 bcdabc,因此相同前缀后缀长度为 3,也就是 abc

下标为 7,字符串为 abcdabca,前缀为 abcdabc,后缀为 bcdabca,因此相同前缀后缀长度为 1,也就是 a

下标为 12,字符串为 abcdabcabcdab,前缀为 abcdabcabcda,后缀为 bcdabcabcdab,因此相同前缀后缀长度为 6,也就是 abcdab

人工计算还是挺好算的,用眼睛看看简单算算就行了。网上有些资料是从 -1开始,然后右移一位,我认为不好理解,不如保留前缀表的本意

所以算来算去,前缀表有什么用处呢?

前缀表可以帮助我们在匹配不成功的时候找到前面最佳的重新开始的位置,从而保证我们只遍历文本串一次就能判断模式串与文本串是否匹配。(废话)

先举例:后面的 i指文本串的下标,j指模式串的下标。(文本串下标保证递增,绝对不回退)

文本串下标012345
字符串acbaba
模式串下标01234
字符串acbac
前缀表00012

开始匹配,ij匹配的很顺利,转眼就到了 i=j=4,然后发现糟了,匹配不上了,现在 j要回退,找到重新开始匹配的位置。

j退到哪里呢?因为 j是没有匹配上的,而 j-1如果有意义(j≠0),一定是能匹配上的!(为什么?因为只有匹配上了 j才会移动,j移动过的位置一定是之前匹配好了的)

那么 j-1是匹配上的又说明了什么呢?说明对于 0~j-1的字符串,如果有相同的前缀后缀,一定也是能和i-1匹配的上的,因此就不需要回退超过前缀的位置!

还是上面的例子,模式串的 j-1前缀表的值是 1,说明 j-1位置的 a在模式串的前面也出现过,就是模式串 0位置的 a。由于 j-1是和 i-1匹配上了的,因此 j=0i-1也是匹配上了的,不需要再去看模式串 0的位置,只需要看0的后一个位置 1i是否能匹配上就好了!

流程步骤:

  1. ij匹配不上了,隐含条件是 i-1j-1是可以匹配的
  2. 看一下 j-1后缀的相同长度的前缀长度,也就是 next[j-1]的值
  3. 回退 jnext[j-1]的位置,隐含了这一步将相同长度的前缀绕过

然后 j=next[j-1]后就去判断 ji是不是相同就好了,很不幸的是,还是不相同,i指向的是 bj指向的是 c

那么没办法,留着这个前缀也无法匹配了,只好再次回退,这一退就退到 j=0了,但是还是不相等。

j=0就没有办法再次回退了,只好 i++,舍弃这一个部分的文本串,开始新的文本串。

到这里应该明白了前缀表的作用了,字面上很难理解,跟着流程走一遍就明白它的思想了,确实精妙

字符串匹配

所以在已知 next数组的前提下,这个字符串匹配的代码就很简单了。虽然简单,但是结构一点都不可以修改,循环和顺序都是精心设计的。

j = 0for i:=0;i<haystacklen;i++{    for j > 0 && needle[j] != haystack[i]{ // 匹配不上就一直回退,j=0说明真的匹配不上了,跳出来i++        j = next[j-1]    }    // j=0也会跳到这里尝试一下    if needle[j] == haystack[i]{ // 匹配上的就能j++去看模式串的下一个字符了,然后进入下一个循环i++,判断文本串的下一个字符能不能和模式串的这个字符匹配上        j++    }}

还有一个问题,next数组怎么求?

next数组

首先要明确一点,next数组是针对模式串而言的,与文本串半毛钱关系没有

模式串下标0123456
字符串abaabae
前缀表0011230

其实思想和匹配是相同的,不同的地方在于上面的是用模式串和文本串进行匹配,这里是用自己和自己进行匹配,匹配的过程中看看能匹配上多少,就能得出 next数组的数值了

next[0]=0,初始化毫无争议,因为空串一定是0

指针 i同样一直向前,指针 j会进行回退,因为 next[0]确定了,因此直接初始化i=1

最开始,j指向的是 0的位置,就在这里等着到底哪个 i能和我这个可爱的 j匹配上

到了 i=2,匹配上了!这时候 j不满足了,是不是 i+1也能和 j+1匹配上呢?所以就 j++,尝试匹配下一个

要是匹配不上了怎么办呢?比如 j=3,i=6匹配不上了,也隐含了条件,就是 j=2是能和 i-1匹配上的(要是匹配不上j也不可能不等于0

那么j=2时候的相同长度的前后缀在哪里呢?因为如果相同也不需要去看了,所以更新j=next[j-1]就可以了,和上面的字符串的匹配思想是完全相同的。

如果还是匹配不上,那么j只好乖乖变为0,等待着下一个能匹配上的将j+1

代码如下:

next[0] = 0 // 初始化j := 0 // j指向首位for i:=1;i<needlelen;i++{ // 遍历模式串,不回退    for j > 0 && needle[i] != needle[j]{        j = next[j-1] // 匹配不上了,绕过已知的相同长度的前后缀,直到变为j=0的初始状态    }    // 如果j=0还是有一次判断的机会的    if needle[i] == needle[j]{ // 匹配上了将j解放出来,+1再试试        j++    }    next[i] = j // 赋值next数组}

时间复杂度分析

n为文本串长度,m为模式串长度

在匹配的过程中,根据前缀表不断调整匹配的位置,如果没有一个字符能匹配的上,时间复杂度就是文本串的指针从头移到尾,也就是

如果能匹配上一些字符,回退的次数也不可能超过 n次。因此时间复杂度是

生成 next数组,不会比匹配的时间复杂度高(因为如果模式串比文本串还要长,根本就不需要匹配了)

所以从平方级别的时间复杂度直接降到了线性的时间复杂度。

总结

看过很多遍,应该也曾经懂过,就是从来没有整理过,因此可能也没有真正懂过。

希望这次能真真正正懂了,后面忘记了再来看看这篇文章,希望能快一些想起来。

]]>
+ + + + + Study + + + + + + + Algorithm + + Go + + + +
+ + + + + 研究生课程:机器学习-第10章 神经网络与深度学习 + + /2022/10/18/UCAS/machine-learning/machine-learning-10/ + + 《机器学习》课程笔记:第10章 神经网络与深度学习

第10章 神经网络与深度学习

概述

背景与现状

ANN到DL的技术发展

  • ANN始于1890年:开始于美国心理学家W.James对于人脑结构与功能的研究。
  • M-P模型 (1943 年):神经科学家麦卡洛克和数学家皮兹建立了神经网络和数学模型(MP模型),人工神经网络的大门由此开启。
  • Hebb学习规则(1949年):加拿大著名心理学家唐纳德·赫布提出了Hebb学习规则,这是一种无监督的学习规则。 Hebb学习规则表明了网络是可以学习的,这启发了后面一系列关于神经网络的研究工作。
  • 感知机(1958 年):心理学家Frank Rosenblatt受到Hebb思想的启发提出了感知机。感知机是最早的人工神经网络,也是具有学习功能M-P模型。整个1958 年-1969年期间,有许多科学家和学者都投入到了感知机研究。但是由于当时的计算水平相对落后,计算也显得很吃力。
  • 1969年进入冰河期:马文明斯基在发表《 Perceptrons 》时,证明了感知器的致命弱点:不能够解决异或问题。
  • 神经网络(1986 年)BP 算法:Rumelhar和Hinton提出了反向传播算法(BP 算法),是一种监督学习算法,解决了两层神经网络计算的复杂性。
  • 卷积神经网络(1989年):1989年, LeCun发明了卷积神经网络LeNet,并将其用于数字识别,且取得了较好的成绩,不过当时并没有引起足够的注意。
  • RNN模型:递归(recurrent)的现代定义由Jordan(1986 年),随后Elman(1990 年)的RNN网络。
  • LSTM模型(1997年):LSTM的提出,尽管该模型在序列建模上的特性非常突出,但由于正处于ANN 的下坡期,也没有引起足够的重视。
  • 深层信度网络(2006 年):2006DL元年,Hinton提出了深层网络训练中梯度消失问题的解决方案: 无监督预训练对权值进行初始化,并
    进行有监督训练微调 。但是由于没有特别有效的实验验证,该论文并没有引起重视。
  • ReLU激活函数(2011 年):该激活函数能够有效的抑制梯度消失问题。
  • 语音识别突破(2011 年):微软首次将DL 应用在语音识别上,取得了重大突破。
  • ImageNet竞赛夺冠(2012 年):Hinton团队首次参加ImageNet图像识别比赛,其通过构建的AlexNet网络一举夺得冠军。
  • AlphaGo (强化学习):2016年 3 月人工智能围棋比赛,谷歌( Google )旗下 DeepMind 公司的戴维 · 西尔弗、艾佳 · 黄和戴密斯 · 哈萨比斯与他们的团队开发的 AlphaGo 战胜了世界围棋冠军、职业九段选手李世石,并以 4:1 的总比分获胜。
  • 深度学习的技术突破:生成对抗、注意力机制、预训练模型

DL在AI的成功应用

语音识别

2009年, Hinton把深层神经网络介绍给做语音识别的学者们。2010年,语音识别就产生了巨大突破。本质上是把传统的混合高斯模型(GMM)替换成了
深度神经网络(DNN)模型,但相对识别错误率一下降低20%多,这个改进幅度超过了过去很多年的总和。这里的关键是把原来模型中通过 GMM 建模的手工特征换成了通过 DNN 进行更加复杂的特征学习。

在此之后,在深度学习框架下,人们还在不断利用更好的模型和更多的训练数据进一步改进结果。现在语音识别已经真正变得比较成熟,并且被广泛商用,目前所有的商用语音识别算法没有一个不是基于深度学习的。

计算视觉:通过组合低层特征形成更加抽象的高层特征

DL在图像识别

Yann LeCun早在1989年就开始了卷积神经网络的研究,取得了在一些小规模(手写字)的图像识别的成果,但在像素丰富的图片上迟迟没有突破,直到2012年Hinton和他学生在ImageNet上的突破,使识别精度提高了一大步;截至2015年最好的模型ResNet

2012年 Google Brain 用 16000 个 CPU 核的计算平台训练 10 亿神经元的深度网络,无外界干涉下自动识别了“Cat”

2014年香港中文大学教授汤晓鸥研究组DeepID的深度学习模型,在 LFW 数据库上获得了99.15%的识别率,人用肉眼在LFW上的识别率为97.52%,深度学习在学术研究层面上已经超过了人用肉眼的识别 。

自然语言处理

词向量表示学习

词向量是指通过对大量文本的无监督学习,根据前后文自动学习到每个词的紧凑向量表达,包括NNML 、 Word2Vector 、预训练模型等。

预训练模型:ELMo、 GPT和BERT 等,全线提升自然语言领域的多项任务的Baseline

递归神经网络 RNN:文本的各个词之间是有顺序的,RNN能更好的挖掘和利用这个性质,在自然语言各个领域都在尝试进行中。 已经有BPTT 、 LSTM等。

神经网络模型概述

神经网络模型学习框架

xuQS9e.md.png

损失函数:

平方损失:

交叉熵损失:

单个神经元模型:

xuQHPS.md.png

单个神经元模型:

  • 感知机
  • 最小方差回归
  • Logistic模型

多层感知机

卷积网络

核函数网络:单隐层神经网络、非线性体现在径向基核函数

  • 径向基网络
  • 支持向量机

自组织映射

RBM

  • 同层神经元间无连接,并彼此相互独立
  • 是一个无向图(权值对称),即连接可看作双向的
  • 层为隐层,层为可见层

递归网络

深度网络模型概述

深度前馈网络

常见的结构:

  • 全连接网络DFL
  • 预训练+全连接网络 Au+FL
  • 卷积+全连接网络 CNN+FL
  • CNN + FL+ ReLu + Tricks

递归神经网络

常见的结构:

  • Bi结构
  • Deep结构
  • CNN+RNN结构

生成对抗网络(GAN)

两个网络博弈:G(Generator)和D(Discriminator)

  • G是一个生成图片的网络,它接收一个随机的噪声z,通过这个噪声生成图片,记做G(z)。
  • D是一个判别网络,判别一张图片是不是“真实的”。它输入一张图片x,输出D(x)代表x为真实图片的概率,如果为1,就代表100%是真实的图片,而输出为0,就代表不可能是真实的图片。

深度强化学习

强化学习:学习目标:策略概率

值函数网络:Deep Q-Learning

策略网络:Deep Policy Network

多层感知机

含有数据输入层、1个以上隐藏层、 1个输出层;各层神经元全连接,同一层神经元之间无连接。

xu1LBn.md.png

多层感知机的运算:

xu3VN6.md.png

激活函数(包括硬门限阈值函数),是导致网络运算非线性的直接原因。

问题描述

学习问题:

学习目标:调整神经元连接权重值,使得平均误差能量最小。

两种方法:批量学习和在线学习。

目标:最小化损失函数

批量学习(Batch Learning)

  • N个样本(一个batch)
  • 随机采样 batch 训练样本集
  • Batch by Batch 调整权值
  • 优点:梯度向量形式固定,有利于并行处理
  • 缺点:需要内存资源大

在线学习(Online Learning):sample by sample 调整权值

xu8Hwn.png
优点:容易执行、存储量小、有效解决大规模和困难模式的分类。

缺点:学习过程随机、不稳定。

BP基本思想

两个方向的信号流、两个方向的函数运算

函数信号:计算输出函数信号

误差信号:计算梯度向量

数据前馈运算

xuGRB9.md.png

梯度反馈运算

xuGhA1.md.png

BP 算法小结

  1. 数据初始化
  2. Epoch 采样
  3. 前向计算
  4. 反向梯度计算
  5. 求参数梯度
  6. 迭代

激活函数

异或问题

改善性能的试探法

函数逼近

卷积网络

卷积层:卷积层具有局部连接和权重共享特点。

一维、二维卷积

卷积层的输出尺度

卷积层的参数个数

子采样层:每个通道,通过下采样,缩减尺度。

典型实例:LeNet-5

Recurrent 网络

四种基本递归结构

  1. 输入-输出递归模型(NARX 模型)
  2. 状态空间模型
  3. 递归多层感知机
  4. 二阶网络

通用逼近定理:如果网络具有充分多的隐藏神经元,任意的非线性动态系统可以由递归神经网络以期望的精度来逼近,对于状态空间的紧致性没有限制。

计算能力

Recurrent 网络

RNN分回合训练

RNN连续训练

RNN长期依赖

RNN扩展的递归结构

前沿概述

深度学习

深层结构:神经网络 + 深层结构 + 优化 + 计算资源 + 人工智能应用

梯度消失:解决梯度消失

  • 前馈网络:自编码、ReLU 激活函数
  • Recurrent 网络:二次优化、非线性逐次状态估计、ReLU 激活函数

视觉识别

自然语言处理

生成对抗学习

生成对抗模型原理

生成器(Generator):尽可能去学习真实样本的分布,迷惑鉴别器。

鉴别器(Discriminator):尽可能的正确判断输入数据是来自真实数据还是来自生成器。

损失函数:

训练过程:生成器与鉴别器交替训练,互相提升各自的生成能力和鉴别能力,最终寻找二者之间的一个纳什均衡。

强化学习

马尔科夫决策过程:

智能体环境交互-智能体的目标是最大化将来的期望累积奖励

知识图谱

背景

知识图谱的概念最早出现于Google公司的知识图谱项目,体现在使用Google搜索引擎时,出现于搜索结果右侧的相关知识展示。

截止到2016 年底,Google知识图谱中的知识数量已经达到了600亿条,关于1500个类别的5.7亿个实体,以及它们之间的3.5万种关系。

实体、关系和事实:

  • 实体(entity):现实世界中可区分、可识别的事物或概念。
  • 关系(relation):实体和实体之间的语义关联。
  • 事实(fact): (head entity, relation, tail entity) 三元组形式。

狭义知识图谱

狭义知识图谱:具有图结构的三元组知识库。

节点:实体。 边:事实(由头实体指向尾实体)。 边的类型:关系。

链接预测、三元组分类:知识图谱上的链接预测

分布式知识表示方法分类:

  • 位移距离模型 (translational distance models):采用基于距离的打分函数来衡量三元组成立的可能性。
  • 语义匹配模型 (semantic matching models):采用基于相似度的打分函数来衡量三元组成立的可能性。
    • 简单匹配模型:RESCAL及其变种-将头实体和尾实体的表示进行组合后再与关系的表示进行匹配
    • 复杂匹配模型:深度神经网络-利用较为复杂的神经网络结构完成实体和关系的语义匹配
]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:现代信息检索-第15讲 基于深度神经网络的IR模型 + + /2022/10/17/UCAS/information-retrieval/information-retrieval-15/ + + 《现代信息检索》课程笔记:第15讲 基于深度神经网络的IR模型

第15讲 基于深度神经网络的IR模型

深度神经网络基础

最简单的神经网络-神经元

激活函数:主要作用是引入非线性,增强网络的表示能力。

最简单的多层神经网络-多层感知机

Softmax归一化是在使用神经网络进行分类时常用的方法,对于分类问题,通常需要给出可能属于每一个类别的概率,即需要输出介于 0 和 1 之间,且加和为 1

参数的学习

正则化

卷积神经网络

循环神经网络

Neural IR Model

Neural IR 模型分类

Representation based:学习文本的分布式表示 在高维空间匹配

  • 词表示:one hot → distributed
  • 句子表示:bag of words → distributed
  • 匹配能力取决于学习文本表示的算法能力
  • 代表模型:DSSM, CDSSM

Matching function:文本之间先进行交互匹配,再对匹配信号进行融合

  • 输入:比较底层的输入
  • 匹配函数:cosine, dot product → NN
  • 优点:可以考虑更加丰富的匹配信号, 如软匹配 (soft matching)
  • 代表模型:MatchPyramid , DRMM, K NRM, PACRR, NPRF

Combination of both: 既考虑 Representation 又考虑 Matching function

  • 代表模型:Duet

DSSM:Deep Structured Semantic Models

word hashing: Bag of letter trigrams representation

模型:DNN学习查询,文本的语义表示, cosine相似度作为匹配评分

MatchPyramid:

考虑各种层次的匹配信号,包括单词层次、短语层次以及句子层次等等;

在图像领域,基于 CNN 特征提取的图像金字塔被证明是有效的

DRMM:相比普通的文本匹配任务,检索任务更需要关注相关性匹配

通过显式地对精确匹配信号,查询词重要度以及多样匹配要求进行建模,得到的模型更加适合于检索任务。

DRMM是第一个在 TREC 数据集能够取得比传统检索模型更好效果的基于 DNN 模型

DRMM的设计思路在一定程度上借鉴了传统的 TF-IDF

K-NRM:使用kernel pooling 技术提取多层次的软匹配 (soft match)特征

PACRR:通过将具有不同大小(k= lg 卷积核的卷积层作用于查询与文档间的单词-单词相似度矩阵,来对 k gram 匹配信息进行建模。

DUET:Representation与Matching function 的方法是互补的

SNRM:监督学习得到文本稀疏表示,解决效率问题

NPRF:将反馈文档视为原始查询的扩充表示,通过增强与查询相关的信息匹配信号获得更好的交互矩阵

总结与展望

  • 基于DNN 的检索模型的研究虽然目前取得了一定的成果,但还有许多问题没有解决
    • 尚未得到明显优于传统模型(如BM25+QE )的结果
    • 很多论文回避了与传统PRF 模型的比较
  • CNN、统计直方图:有用; RNN :没有效果
  • 长文本IR 应用中往往 DNN 方法效果有限
  • 但是在商品推荐、基于title 的检索、 microblog retrieval 等短文本应用中效果不错
  • 通过CNN 等方法提取的特征 Vs 基于信息理论进行概率估计得到的特征
  • 很多在NLP 领域证明非常有效的方法,在 IR 领域尚未发挥威力

BERT

基于BERT的检索模型

稠密向量检索模型

直接改变了原有第一阶段的检索模式,通过BERT等预训练语言模型,将查询和文档都映射到语义空间中,编码成单个稠密向量表示,用ANN 算法来进行检索。在一定程度上缓解了词汇不匹配问题,并将检索阶段的匹配效果推到了一个新的台阶

模型框架:一般采用双塔结构对查询和文档单独地编码得到二者独立的表达,从而使文档可以进行离线索引。

RepBERT:平均词项表示作为文档的单个向量

ANCE:利用k-1的模型来检索得到top-n文档并随机采样负样本,每隔一段时间都需要对训练数据中的负样本进行更新,因此该方法的训练代价较大。

RocketQA:与ANCE相比,做了额外的denoised操作;

TCT-ColBERT:通过蒸馏技术,将ColBERT的强建模能力蒸馏到类似于RepBERT这样的双塔架构上去

Condenser:为了将更完整的序列信息压缩到CLS 位置上

DAR:通过插值、扰动的方式在文档表示层面进行数据增强

JPQ:除了直接采用乘积量化(Product Quantization, PQ )方法来压缩向量外,将乘积量化后的文档d†表示用于模型训练,通过排序排序训练目标来优化 PQ 的聚类中心表示

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:高级人工智能-第7讲 图卷积神经网络 + + /2022/10/13/UCAS/advanced-ai/advanced-ai-7/ + + 《高级人工智能》课程笔记:第7讲 图卷积神经网络

第7讲 图卷积神经网络

卷积神经网络在欧式数据(图像、文本、声音和视频等)上获得了巨大的成功,广泛应用于图像分类、目标检测、机器翻译等

卷积神经网络可以学习局部小结构,使用局部的卷积核,然后形成多维的模式

卷积如何迁移到非欧空间上去?

卷积是在函数和函数上的数学运算,从而得到函数

连续形式:

离散形式:

在图上定义卷积的方法:

谱方法:在谱空间中定义卷积:

  • 通过图傅里叶变换和卷积原理定义卷积
    • 图数据符合幂律分布,造成了极大的挑战
  • 主要挑战是在谱空间定义的卷积在结点空间并没有局部化

空间方法:在向量空间中定义卷积

  • 卷积被定义为目标结点到它的所有邻居的一个加权平均函数
  • 主要挑战是邻域的大小在结点之间差异很大,可能服从幂律分布

谱方法

定义一个图(结点、边、邻接矩阵)

图上的每个结点上都有维的特征,因此是结点的特征矩阵,每一列是结点的一个信号

图的拉普拉斯算子:,其中

归一化的拉普拉斯算子:

的傅里叶变换:

的正交特征向量是,对应的非负特征值是,可以对拉普拉斯矩阵进行分解:

对于一个信号的图傅里叶变换为

两个信号的卷积的傅里叶变换是两个信号的傅里叶变换的逐点相乘,卷积核就是

xd8xQU.md.png

图卷积神经网络:

xdGCw9.md.png

缺点:

  • 需要拉普拉斯矩阵的特征分解,特征向量不太好获得
  • 计算成本高,傅里叶乘法的时间复杂度是
  • 在结点空间上不是局部化的(操作的是全局信号)

ChebyNet:参数化-将参数的数量从n降为K

xdJCjS.md.png

优点:

  • 不再需要特征分解
  • 时间复杂度从下降到
  • 卷积在结点空间上是局部化的(卷积严格定位在半径为 K 的球中)

Graph wavelet neural network:图小波神经网络

将傅里叶基换为小波基:稀疏、局部化、计算代价低

空间方法

方法类比卷积:

  1. 对于每个节点,根据某些邻近度指标选择固定数量的结点作为其相邻结点
  2. 根据邻近度指标给邻居排序
  3. 共享参数

GraphSAGE:从每个结点开始随机游走,采样个结点,不用临近度指标判断。然后通过聚合函数进行参数共享

图卷积网络(GCN):通过归一化的拉普拉斯矩阵从不固定数量的邻居结点中聚合信息,通过特征变换共享参数

GAT:Graph Attention Network:通过注意力机制学习聚合矩阵

MoNet:空间方法的一般意义框架:所有的空间方法都是定义多个核函数,来测量目标结点和其他结点之间的相似度

谱方法与空间方法的关系

谱方法是空间方法的特例

  • 谱方法通过特别的空间变换定义核函数
  • 空间方法直接定义核函数

图池化

图粗化:将结点进行聚类,每一类作为一个超级结点

结点选择:学习一个评价标准去挑选比较重要的结点

图神经网络的表达能力

图神经网络在结点分类、链接预测、图分类上取得了巨大的成功,但是图神经网络的设计大多基于直觉、启发式方法或者实验试错,缺少对于图神经网络的理论理解。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + 研究生课程:模式识别与机器学习-第5章 统计机器学习 + + /2022/10/13/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-5/ + + 《模式识别与机器学习》课程笔记:第5章 统计机器学习

第5章 统计机器学习

机器学习简介

桑克(R.Shank)“一台计算机若不会学习,就不能说它具有智能。”

机器学习更强调面向算法,而统计学更偏重于面向模型。换而言之,机器学习强调算法的结果要好,所以机器学习很关注损失函数。而统计学要先扔出来一大堆模型假设,然后站在模型上面通过严格的数学推导做出结果。

统计机器学习:是基于数据构建概率统计模型并运用模型对数据进行预测分析的一门学科。

机器学习的学习过程:

  • 经验(E):训练数据
  • 模型(T):需要学习的目标函数
  • 学习算法:怎么样从经验中推断出模型
  • 评价(P):测试数据

机器学习的特点:

  • 数据大量且廉价,知识昂贵而稀少
  • 数据产生过程的细节是未知的,但是数据产生的过程不是完全随机的。
  • 通过利用数据中的某些模式或规律从数据中学习模型:反推数据生成路径。
  • 模型通常不是完整过程的精确复制品,而是一种良好且有用的近似。
  • 模型可以描述从数据中获取知识,或对预测将来(具有预测性),或者两者兼而有之。
  • 几乎所有的科学都关注于用模型拟合数据:推理。

机器学习方法分类:

  • 有监督学习:有标记数据 e.g. Fisher,、感知器算法、线性判别分析
  • 无监督学习:无标注数据,降维方法K-L
  • 半监督学习:无标注数据+有标注数据
  • 多任务学习:共享相关任务之间的表征
  • 迁移学习:训练数据与测试数据不是同分布的
  • 强化学习:间接的标注数据(状态和对应的reward)
  • 主动学习:主动选择训练数据
  • 自监督学习:从无标注数据提取监督信号。

自监督学习是自主监督学习。它提取并使用自然可用的相关上下文和嵌入式元数据作为监督信号。

统计机器学习

框架

输入训练样本,目标是损失函数期望风险最小化

期望风险最小化:

经验风险最小化:(导致过拟合)

结构风险最小化:

过拟合及正则化

怎么样在测试数据上预测得好?

两方面:

  • 模型对训练数据拟合得好-需要复杂的模型
  • 模型具有一定的能力来容忍测试数据的不同行为-需要稳定的模型

正则项:在原来的经验损失函数中添加一个惩罚项,不鼓励复杂的模型

泛化能力分析

偏差-方差分解:expected loss=bias2+variance+noise

偏差:度量了模型的期望预测和真实结果的偏离程度

方差:刻画了数据扰动所造成的影响

噪声:与f相互独立,刻画了问题的难易程度

由正则化参数控制的偏差和方差对模型复杂性的依赖性说明:

大的值将权重参数拉至零导致较大偏差,较小的值允许对噪声进行微调,从而导致较大的方差

  • 简单模型:低方差、高偏差
  • 复杂模型:高方差、低偏差

对模型复杂度问题的深刻理解:

  • 非常灵活的模型具有低偏差和高方差。
  • 相对刚性的模型有大的偏差和低的方差。
  • 具有最佳预测能力的模型是使得偏差和方差之间最佳平衡的模型。
  • 偏差-方差分解的实际应用价值有限:
    • 偏差和方差无法计算,因为它依赖于了解x和y的真实分布。
    • 偏差-方差分解基于数据集集合的平均值,而实际上我们只有单个观测数据集。
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Pattern Recognition and Machine Learning + + + +
+ + + + + 研究生课程:机器学习-第9章 概率图模型 + + /2022/10/12/UCAS/machine-learning/machine-learning-9/ + + 《机器学习》课程笔记:第9章 概率图模型

第9章 概率图模型

有向图模型:贝叶斯网络

图结构:有向无环图

结点:一个或一组随机变量

边:随机变量之间的单向、直接影响

联合概率分布分解形式:,其中, 所有父结点构成的集合

条件独立性 D-分离准则(D-separation criterion):判断贝叶斯网络结点之间的条件独立性。

贝叶斯网络的全局马尔科夫性:给定结点集合A,B,C,若A到B中结点的所有无向路径都是被C阻塞的(blocked),则称A和B被C D-分离(D-separated),即A和B关于C条件独立。

若一条无向路径包含结点x满足以下条件之一,则称其是阻塞的:

  • x 是tail-to-tail 或head-to-tail 结点,并且x包含在C中。
  • x 是head-to-head 结点,并且x(及x 的任意后代均)不包含在C中。

贝叶斯网络的局部马尔科夫性:

  • 给定某变量的父结点,则该变量条件独立于所有其他非其后代结点。
  • 给定某变量的马尔可夫毯(父结点,子结点,子结点的父结点),则该变量条件独立于其他变量。

无向图模型:马尔可夫随机场

图结构:无向图

结点:一个或一组随机变量。

边:随机变量之间的相互依赖(非“因果关系”)。

团:对于图中的结点子集,若其中任意两个节点之间都有连边,则称该结点子集为一个团(clique)。

极大团:若在团中加入其他任意一个结点都不再形成团,则称该团为极大团(maximal clique)。

分解形式:

其中, 为团集合, 为团 对应的变量集合, 为定义在团 上的非负势函数,是归一化因子

条件独立性:

马尔可夫随机场的全局马尔科夫性:给定结点集合A,B,C,若从A中的结点到B中结点必须经过C中的结点,则称A和B被C分离,即A和B关于C条件独立。

局部马尔科夫性:给定某变量的马尔可夫毯(邻接变量),则该变量条件独立于其他变量。

成对马尔科夫性:给定其他所有变量,两个非相邻变量条件独立。 if

学习与推断

基本定义

推断:已知联合概率分布 ,估计 ,其中 是集合 的子集。 是问题变量, 是证据变量。

学习:从观测数据 中学习联合概率分布 ,寻找最符合观测数据的概率图模型。

推断:已知联合概率分布 ,估计,其中

枚举 : 假设 个变量,每个变量的取值个数的期望是 ,则时间复杂度为

推断的核心问题 : 如何高效地计算边际分布

推断方法

精确推断:计算的精确值。

变量消去(variable elimination)

思路:利用图模型的紧凑概率分布形式来削减计算量。

优点:简单直观,代数上的消去对应图中结点的消去。

缺点:针对不同证据变量会造成大量冗余计算。

信念传播(belief propagation)

思路:将变量消去过程中产生的中间结果视为可复用的消息,避免重复计算。

消息传递仅在邻接变量之间发生,与边的方向性无关。

树结构:有向树=无向树

树结构上的消息传递:

消息计算公式:

边际分布:

二次扫描算法:

  • 指定一个根结点,从所有叶结点开始向根节点传递消息,直到根结点收到所有邻接结点的消息。
  • 从根结点开始向叶结点传递消息,直到所有叶结点均收到消息。

近似推断

近似推断:在较低的时间复杂度下获得原问题的近似解。通过采样一组服从特定分布的样本,来近似原始分布,适用范围更广,操作性更强。

前向采样(forward sampling)

思路:依据贝叶斯网络的(条件)概率直接采样。采样后,进行需要的概率统计。

缺点:对于小概率事件采样困难,可能经过很多次采样也无法获得足够多的样本

仅适用于贝叶斯网络,不适用于马尔可夫随机场。

吉布斯采样(Gibbs sampling)

思路:直接依照条件概率采样。

马尔可夫毯的性质:

优点:

  • 直接从采样,解决小概率事件采样难的问题
  • 同时适用于贝叶斯网络和马尔可夫随机场
  • 简单易推导,时间复杂度低。

实例模型

隐马尔可夫模型

隐马尔可夫模型是关于时序的概率模型,是最简单的动态贝叶斯网络模型。

状态变量 表示第 时刻的系统状态,观测变量 表示第 时刻的观测值。

观测变量仅依赖于当前时刻的状态变量,当前状态仅依赖于前一时刻的状态。状态集合 ,观测值集合

联合概率:

状态转移矩阵,其中表示 时刻处于状态 的条件下, 时刻转移到状态 的概率

观测概率矩阵,其中表示 时刻处于状态 的条件下观测到 的概率

初始状态概率向量 ,其中表示系统初始状态为的概率。

生成过程:

给定 ,生成观测序列

  1. 设置,并根据初始状态概率生成初始状态
  2. 根据和观测概率矩阵B生成
  3. 根据和状态转移矩阵A 生成
  4. ,则设置,并转到第(2)步;否则,停止。

三个基本问题

  • 概率计算问题:给定模型和观测序列,计算在模型下观测到的概率。(评估模型与观测序列之间的匹配程度)
  • 直接计算法:给定模型和观测序列,求使得最大的状态观测序列。(根据观测序列推测状态序列)
  • 学习问题:给定观测序列,调整模型参数,使得该序列出现的概率最大。(训练模型使其更好地描述观测序列)

条件随机场

条件随机场(Conditional Random Field) 是给定随机变量的条件下,随机变量的马尔可夫随机场。中的随机变量构成的无向图,图中每个变量在给定的条件下都满足马尔可夫性:

线性链条件随机场(linear-chain CRF)是随机变量为线性链时的条件随机场

是观测序列。 是标记序列(或称状态序列 ),在给定的条件下,的条件分布构成条件随机场。

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:现代信息检索-第14讲 面向信息检索的分布式词项表示 + + /2022/10/12/UCAS/information-retrieval/information-retrieval-14/ + + 《现代信息检索》课程笔记:第14讲 面向信息检索的分布式词项表示

第14讲 面向信息检索的分布式词项表示

怎样更鲁棒的匹配用户搜索意图?

查询扩展/Query expansion:

  • 相关反馈/Relevance feedback 能够通过向查询中添加扩展词,从而对捕捉用户搜索意图有所帮助
  • 也可以利用 词项相似度/word similarities 信息:
    • 基于人工 同义词表/thesaurus of synonyms 的查询扩展
    • 词项相似度指标
      • 基于大规模文档语料计算
      • 基于查询日志挖掘(Web上的常见做法)计算

文档扩展/Document expansion:

使用锚文本/anchor text可以通过提供人工创作的同义词(即锚文本)来解决此问题,但不适用于新的或不太受欢迎的网页(注:链接稀疏,锚文本少)或无超链接的语料

基于查询日志的查询扩展

不考虑上下文语境的查询扩展可能会导致问题

从查询日志学习考虑上下文语境的查询重写:识别同一用户基于同一信息需求的多次查询请求

自动同义词库生成

  • 尝试通过分析文档集来自动生成同义词库
  • 基本概念:两个词之间的相似性
  • 假设1:如果两个单词与相似单词同时出现,则它们是相似的。
  • 假设2:如果两个单词与同一个词在给定的语法关系中出现,则它们是相似的。
  • 基于共现的相似度更鲁棒,基于语法关系的相似度更准确。

表示词项之间的关系

使用词项的标准符号编码,每个词项都是一个维度

不同的词项没有内在的相似性

基于分布式相似度的表示:用相邻词项的意义来表示一个词项

解决方案:低维向量

基本思想: 将“大部分的”重要信息存储在一个维度固定的低维向量中 - 即“密集向量”

传统方法:潜在语义索引/分析

使用奇异值分解(Singular Value Decomposition,SVD)–或只是随机映射(random projection)以找到低维基向量或正交向量

神经嵌入

词项的意义由向量表示:为每个词类构建一个密集向量,该向量应当能够准确的预测其上下文词项

学习神经词嵌入:基本思路

  • 定义一个向量表示的模型,该模型预测一个中心词 wt 的 上下文词(或者反过来)
  • 同时也有一个损失函数
  • 不断调整(所有词的)向量表示使得损失最小化
  • 最终得到每个词的低维密集向量表示

思路:直接基于预测能力学习低维词向量

Word2Vec包含一组算法预测每个词的上下文(或者反过来)

神经网络的优化:(求导的)链式法则

Word2vec里的线性关系

Word2vec的向量表示非常善于对相似性和相似性的维度编码!

仅通过在嵌入空间中进行向量减法就可以很好地解决类比测试相似度的问题

Dual Embedding Space Model (DESM)

一种简单的利用词嵌入的检索模型

文档由其词项嵌入的中心向量表示

查询-文档相似度:查询词向量与文档向量的平均相似度

DESM 是一个弱排序模型,但是具有发现微妙相似性/关联性的能力

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + Go语言圣经-复合数据类型 + + /2022/10/11/Go/go-basic-4/ + + Go语言圣经-复合数据类型

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。

一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0

如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。

一个slice可以用来模拟一个stack

最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:

stack = append(stack, v) // push v

stack的顶部位置对应slice的最后一个元素:

top := stack[len(stack)-1] // top of stack

通过收缩stack可以弹出栈顶的元素

stack = stack[:len(stack)-1] // pop

要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:

func remove(slice []int, i int) []int {    copy(slice[i:], slice[i+1:])    return slice[:len(slice)-1]}func main() {    s := []int{5, 6, 7, 8, 9}    fmt.Println(remove(s, 2)) // "[5 6 8 9]"}

如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:

func remove(slice []int, i int) []int {    slice[i] = slice[len(slice)-1]    return slice[:len(slice)-1]}func main() {    s := []int{5, 6, 7, 8, 9}    fmt.Println(remove(s, 2)) // "[5 6 9 8]}

JSON

type Movie struct {    Title  string    Year   int  `json:"released"`    Color  bool `json:"color,omitempty"`    Actors []string}var movies = []Movie{    {Title: "Casablanca", Year: 1942, Color: false,        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},    {Title: "Cool Hand Luke", Year: 1967, Color: true,        Actors: []string{"Paul Newman"}},    {Title: "Bullitt", Year: 1968, Color: true,        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},    // ...}

这样的数据结构特别适合JSON格式,并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:

data, err := json.Marshal(movies)if err != nil {    log.Fatalf("JSON marshaling failed: %s", err)}fmt.Printf("%s\n", data)

另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

data, err := json.MarshalIndent(movies, "", "    ")if err != nil {    log.Fatalf("JSON marshaling failed: %s", err)}fmt.Printf("%s\n", data)

编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫unmarshaling,通过json.Unmarshal函数完成。

var titles []struct{ Title string }if err := json.Unmarshal(data, &titles); err != nil {    log.Fatalf("JSON unmarshaling failed: %s", err)}fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"

文本和HTML模板

]]>
+ + + + + Study + + + + + + + Go + + Backend + + + +
+ + + + + Go语言圣经-程序结构-基础数据类型 + + /2022/10/10/Go/go-basic-2-3/ + + Go语言圣经-程序结构-基础数据类型

var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

简短变量声明语句中必须至少要声明一个新的变量

如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是 *int,指针被称之为“指向int类型的指针”。

指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。

// Echo4 prints its command-line arguments.package mainimport (    "flag"    "fmt"    "strings")var n = flag.Bool("n", false, "omit trailing newline")var sep = flag.String("s", " ", "separator")func main() {    flag.Parse()    fmt.Print(strings.Join(flag.Args(), *sep))    if !*n {        fmt.Println()    }}

程序中的 sepn变量分别是指向对应命令行标志参数变量的指针,因此必须用 *sep*n形式的指针语法间接引用它们。

另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为 *T

p := new(int)   // p, *int 类型, 指向匿名的 int 变量fmt.Println(*p) // "0"*p = 2          // 设置 int 匿名变量的值为 2fmt.Println(*p) // "2"

下面的两个newInt函数有着相同的行为:

func newInt() *int {    return new(int)}func newInt() *int {    var dummy int    return &dummy}

变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

type 类型名字 底层类型

在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。

strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。

将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”)

如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

type Weekday intconst (    Sunday Weekday = iota    Monday    Tuesday    Wednesday    Thursday    Friday    Saturday)
]]>
+ + + + + Study + + + + + + + Go + + Backend + + + +
+ + + + + Go语言圣经-入门 + + /2022/10/09/Go/go-basic-1/ + + Go语言圣经-入门

命令行参数

os 包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从 os 包的 Args 变量获取;os 包外部使用 os.Args 访问该变量。

os.Args 的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数。

package mainimport ("fmt""os")func main() {var s, sep stringfor i := 1; i < len(os.Args); i++ {s += sep + os.Args[i]sep = " "}fmt.Println(s)}
> go run main.go 4 2 5 4 14 2 5 4 1

for 循环的另一种形式,在某种数据类型的区间(range)上遍历

func main() {var s, sep stringfor _, j := range os.Args[1:] {s += sep + jsep = " "}fmt.Println(s)}

使用 strings 包的 Join 函数:

func main() {fmt.Println(strings.Join(os.Args[1:], " "))}

查找重复的行

// Dup1 prints the text of each line that appears more than// once in the standard input, preceded by its count.package mainimport ("bufio""fmt""os")func main() {counts := make(map[string]int)input := bufio.NewScanner(os.Stdin)for input.Scan() {counts[input.Text()]++}// NOTE: ignoring potential errors from input.Err()for line, n := range counts {if n > 1 {fmt.Printf("%d\t%s\n", n, line)}}}
> abc> abc> def> efd> efd2       abc2       efd

Printf的多种转换形式:

%d          十进制整数%x, %o, %b  十六进制,八进制,二进制整数。%f, %g, %e  浮点数: 3.141593 3.141592653589793 3.141593e+00%t          布尔:true或false%c          字符(rune) (Unicode码点)%s          字符串%q          带双引号的字符串"abc"或带单引号的字符'c'%v          变量的自然形式(natural format)%T          变量的类型%%          字面上的百分号标志(无操作数)

ln 结尾的格式化函数,则遵循 Println 的方式,以跟 %v 差不多的方式格式化参数,并在最后添加一个换行符。

第二个版本,可以接收文件并判断重复行

// Dup2 prints the count and text of lines that appear more than once// in the input.  It reads from stdin or from a list of named files.package mainimport ("bufio""fmt""os")func main() {counts := make(map[string]int)files := os.Args[1:]if len(files) == 0 {countLines(os.Stdin, counts)} else {for _, arg := range files {f, err := os.Open(arg)if err != nil {fmt.Fprintf(os.Stderr, "dup2: %v\n", err)continue}countLines(f, counts)f.Close()}}for line, n := range counts {if n > 1 {fmt.Printf("%d\t%s\n", n, line)}}}func countLines(f *os.File, counts map[string]int) {input := bufio.NewScanner(f)for input.Scan() {counts[input.Text()]++}// NOTE: ignoring potential errors from input.Err()}

map 是一个由 make 函数创建的数据结构的引用。map 作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对 map 底层数据结构的任何修改,调用者函数都可以通过持有的 map 引用看到。

package mainimport ("fmt""os""strings")func main() {counts := make(map[string]int)for _, filename := range os.Args[1:] {data, err := os.ReadFile(filename)if err != nil {fmt.Fprintf(os.Stderr, "dup3: %v\n", err)continue}for _, line := range strings.Split(string(data), "\n") {counts[line]++}}for line, n := range counts {if n > 1 {fmt.Printf("%d\t%s\n", n, line)}}}

引入了 ReadFile 函数(来自于 os包),其读取指定文件的全部内容,strings.Split 函数把字符串分割成子串的切片。

GIF动画

// Lissajous generates GIF animations of random Lissajous figures.package mainimport ("image""image/color""image/gif""io""math""math/rand""os""time")var palette = []color.Color{color.White, color.Black}const (whiteIndex = 0 // first color in paletteblackIndex = 1 // next color in palette)func main() {// The sequence of images is deterministic unless we seed// the pseudo-random number generator using the current time.// Thanks to Randall McPherson for pointing out the omission.rand.Seed(time.Now().UTC().UnixNano())lissajous(os.Stdout)}func lissajous(out io.Writer) {const (cycles  = 5     // number of complete x oscillator revolutionsres     = 0.001 // angular resolutionsize    = 100   // image canvas covers [-size..+size]nframes = 64    // number of animation framesdelay   = 8     // delay between frames in 10ms units)freq := rand.Float64() * 3.0 // relative frequency of y oscillatoranim := gif.GIF{LoopCount: nframes}phase := 0.0 // phase differencefor i := 0; i < nframes; i++ {rect := image.Rect(0, 0, 2*size+1, 2*size+1)img := image.NewPaletted(rect, palette)for t := 0.0; t < cycles*2*math.Pi; t += res {x := math.Sin(t)y := math.Sin(t*freq + phase)img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),blackIndex)}phase += 0.1anim.Delay = append(anim.Delay, delay)anim.Image = append(anim.Image, img)}gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors}

获取URL

// Fetch prints the content found at a URL.package mainimport ("fmt""io""net/http""os")func main() {for _, url := range os.Args[1:] {resp, err := http.Get(url)if err != nil {fmt.Fprintf(os.Stderr, "fetch: %v\n", err)os.Exit(1)}b, err := io.ReadAll(resp.Body)resp.Body.Close()if err != nil {fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)os.Exit(1)}fmt.Printf("%s", b)}}

改进:

避免申请缓冲区、url参数没有 http:// 前缀、打印出HTTP协议的状态码

// Fetch prints the content found at a URL.package mainimport ("fmt""io""net/http""os""strings")func main() {for _, url := range os.Args[1:] {if !strings.HasPrefix(url, "http://") {url = "http://" + url}resp, err := http.Get(url)if err != nil {fmt.Fprintf(os.Stderr, "fetch: %v\n", err)os.Exit(1)}_, err = io.Copy(os.Stdout, resp.Body)if err != nil {fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)os.Exit(1)}fmt.Println(resp.Status)resp.Body.Close()}}

并发获取多个URL

// Fetchall fetches URLs in parallel and reports their times and sizes.package mainimport ("fmt""io""net/http""os""time")func main() {start := time.Now()ch := make(chan string)for _, url := range os.Args[1:] {go fetch(url, ch) // start a goroutine}for range os.Args[1:] {fmt.Println(<-ch) // receive from channel ch}fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())}func fetch(url string, ch chan<- string) {start := time.Now()resp, err := http.Get(url)if err != nil {ch <- fmt.Sprint(err) // send to channel chreturn}nbytes, err := io.Copy(io.Discard, resp.Body)resp.Body.Close() // don't leak resourcesif err != nil {ch <- fmt.Sprintf("while reading %s: %v", url, err)return}secs := time.Since(start).Seconds()ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)}

goroutine是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数。

Web服务

// Server1 is a minimal "echo" server.package mainimport (    "fmt"    "log"    "net/http")func main() {    http.HandleFunc("/", handler) // each request calls handler    log.Fatal(http.ListenAndServe("localhost:8000", nil))}// handler echoes the Path component of the request URL r.func handler(w http.ResponseWriter, r *http.Request) {    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)}

为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算

// Server2 is a minimal "echo" and counter server.package mainimport ("fmt""log""net/http""sync")var mu sync.Mutexvar count intfunc main() {http.HandleFunc("/", handler)http.HandleFunc("/count", counter)log.Fatal(http.ListenAndServe("localhost:8000", nil))}// handler echoes the Path component of the requested URL.func handler(w http.ResponseWriter, r *http.Request) {mu.Lock()count++mu.Unlock()fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)}// counter echoes the number of calls so far.func counter(w http.ResponseWriter, r *http.Request) {mu.Lock()fmt.Fprintf(w, "Count %d\n", count)mu.Unlock()}

handler函数会把请求的http头和请求的form数据都打印出来,这样可以使检查和调试这个服务更为方便

// handler echoes the HTTP request.func handler(w http.ResponseWriter, r *http.Request) {    fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)    for k, v := range r.Header {        fmt.Fprintf(w, "Header[%q] = %q\n", k, v)    }    fmt.Fprintf(w, "Host = %q\n", r.Host)    fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)    if err := r.ParseForm(); err != nil {        log.Print(err)    }    for k, v := range r.Form {        fmt.Fprintf(w, "Form[%q] = %q\n", k, v)    }}
]]>
+ + + + + Study + + + + + + + Go + + Backend + + + +
+ + + + + 研究生课程:现代信息检索-第13讲 决策树与面向文档的机器学习 + + /2022/10/07/UCAS/information-retrieval/information-retrieval-13/ + + 《现代信息检索》课程笔记:第13讲 决策树与面向文档的机器学习

第13讲 决策树与面向文档的机器学习

面向文本分类的决策树

  • 树结构:内部节点由词项作为标记
  • 分支标记:词项权重的“测试” (test),或仅仅是出现/不出现
  • 叶节点标记:类别
  • 分类器
    • 分类器通过“测试”后的降序树对文档进行分类
    • 然后将叶节点的标签分配给文档
  • 大多数决策树都是二叉树

决策树的学习

学习一个序列的特征测试,典型的做法是由上到下的贪心搜索,每一步选择具有最高信息收益的未使用特征

叶节点标记:yes/no 类别标记,或连续值

如果有个特征,决策树的节点数量上限是(太大了,会有计算开支等方面的问题)

我们可以通过在每个节点上递归选择最佳拆分特征,以贪心的方式创建树

属性选择基本思想:(理想情况下)一个好的特征能够把所有样本划分成“全部正样本”和“全部负样本”两个子集

利用信息论:

信息熵(Entropy):考虑每个节点的类分解

信息增益

对每个节点,我们选择使信息增益最大的特征f

数值特征 (例如tf-idf):通常使用二元的切分 (f < t), t怎样确定?

穷尽式(搜索):评估观察值之间的每个分割点的信息增益。

  • 慢;通过优化计数方法可以稍微提高效率

分箱(Discretize into bins)

  • 将所有的数值切分到k个箱中
  • (连续的数值)特征被离散化
  • 分箱操作可以基于整个语料集的统计

(树的构建)什么时候停止?

  • 当一个节点的所有样本都属于同一个类别
  • 当树的深度d达到一个固定阈值
  • 如果没有合适的属性可以拆分中区分具有统计意义的类(例如,使用卡方检验或Fisher精确检验),则停止构建。
  • 最常用/最佳方法:使用单独的验证数据
    • 构建一个较大的树(可以给树的深度设定阈值)
    • 自下而上的修剪未能(显著)改善验证数据分类性能的节点

面向文本的决策树学习

宏平均:计算每个类别的性能指标,然后取平均值

微平均:收集所有类别的决策(分类)结果,计算列联表,评价

判别式 (discriminative) 分类方法: Logistic Regression (逻辑回归) 与 Support vector machines (支持向量机)

Ensemble 方法

随机森林 (Random Forests)

从原始数据集重复采样(bootstrap采样),在采样数据上构建K个树,p=特征数量

  • 获得K个大小为N的bootstrap采样,其中N是原始数据集大小
  • 通过在每个节点的p个特征中随机选择m个,并选择最佳特征来扩展每个决策树。
  • m的典型取值: sqrt(p)
  • 预测(Runtime):汇总树的预测(最受欢迎的投票)以产生最终分类

原则:我们希望在不同的学习器(learner)之间进行投票,因此我们不希望这些模型过于相似。这两个标准确保了各个树的多样性

优点:

  • 在实践中非常流行,有段时间是密集数据(dense data)上最流行的分类器(<=几千个特征)
  • 容易实现 (训练多个树).
  • 容易并行化 (但并不意味着效率高)。适合用于 MapReduce.

缺点:

  • 现在和一些新方法相比准确度并不高 – Gradient-boosted trees (特征少) 与 深度神经网络(视觉, 语音, 语言, …)通常更好
  • 需要多次遍历数据 – 至少是树的最大深度 (虽然远小于boosted trees )
  • 容易过拟合 – 需要权衡准确度(accuracy)与拟合度(fit)

Boosted Decision Trees (BDT, 增强决策树)

  • 一个比随机森林(RF)提出更晚的算法
  • 与独立训练树的RF不同,BDT的树通过增强(boosting)的方式依次(sequetially)训练树:
    • 每个树都在加权数据上训练,通过加权强调之前(训练)
      的树错误标记的样本
  • 这两个方法都能够通过训练产生高质量模型
  • 但是BDT通常都更适用于中等数量特征的数据集

随机森林(RF) vs 增强树(BDT)

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:机器学习-第8章 信息论模型 + + /2022/10/05/UCAS/machine-learning/machine-learning-8/ + + 《机器学习》课程笔记:第8章 信息论模型

第8章 信息论模型

熵、最大熵

信息量(信息增益量)定义:

信息量性质:概率越小的状态,信息量越大

信息熵定义:信息量在全部数值域上的概率平均值

  • 离散熵:
  • 微分熵:(微分熵不是严格意义的信息熵)

微分熵性质:平移不变、尺度变化,且可以是负值

当根据不完整的信息作为依据进行推断时,应该由满足分布限制条件的具有最大熵的概率分布推得。

最大微分熵问题:

已知均值和方差,高斯分布的微分熵最大

互信息

条件信息量:

条件熵:

  • 给定的条件熵:
  • 给定的条件熵:

联合熵:

  • 联合概率密度:
  • 联合信息量:
  • 联合微分熵:

互信息:信息熵与条件熵的差:

互信息性质:非负性、对称性、不变性

相对熵是衡量两个分布的平均信息差异

相对熵和互信息之间的关系:

信息论优化模型

最大熵模型:最大化 , 求取类别后验概率分布 , 用于分类、预测等

最大互信息模型: 最大化 ; 最大化

最小互信息模型:最小化 ; 最小化 , 独立分析

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + Go基础学习(微软教程) + + /2022/10/03/Go/go-basic-ms/ + + Go基础学习(微软教程)

什么是 Go?

Go 语言表现力强,且简单明了。 它在设计时考虑了惯用语言,这使程序员能够高效地编写高效且可靠的代码。 以 Go 语言编写的程序可以在 Unix 系统上运行,例如 Linux 和 macOS,还有 Windows。 Go 语言之所以值得注意,部分原因在于它独特的并发机制,使得编写可同时利用多个内核的程序非常容易。 它主要是一种强化静态类型的语言,这意味着变量类型在编译时是已知的。 不过,它确实具有一些动态类型化功能。

下面是 Go 编程语言的基本原理优势:

  • Go 许可证是完全开放源代码的。
  • Go 程序编译为单独的二进制文件,这样更易于共享和分发。
  • 交叉编译到各种平台和操作系统
  • Go 语言致力于使语言变得简单,并用更少的代码行执行更多操作。
  • 并发是头等概念,使任何函数可以作为轻量级线程运行,而程序员只需少量工作。
  • Go 语言提供自动内存管理,包括垃圾回收。
  • 编译和执行速度很快。
  • Go 语言需要使用所有代码,否则会引发错误。
  • 有一种官方格式设置可帮助保持项目之间的一致性。
  • Go 语言具有大量全面标准库,并且可以在不使用第三方依赖项的情况下生成多个应用程序。
  • Go 保证语言与以前版本的后向兼容性。

安装Go

如果不想在本地安装 Go,可以使用 Go Playground。 Go Playground 是一款 Web 服务,可在浏览器中运行 Go 应用程序。

本地安装包下载地址

wget https://go.dev/dl/go1.19.1.linux-amd64.tar.gz // 版本号可能改变

提取本地安装包

sudo tar -C /usr/local -xzf go1.19.1.linux-amd64.tar.gz

编辑配置文件,添加到环境变量:

vim ~/.bashrcexport PATH=$PATH:/usr/local/go/binsource ~/.bashrc

确认是否已经安装好:

go version
go version go1.19.1 linux/amd64

配置Go工作区

Go 在组织项目文件方面与其他编程语言不同。 首先,Go 是在工作区的概念下工作的。 工作区就是应用程序源代码所在的位置。 所有 Go 项目共享同一个工作区。 不过,从版本 1.11 开始,Go 已开始更改此方法。 你尚且不必担心,因为我们将在下一个模块中介绍工作区。 现在,Go 工作区位于 $HOME/go,但如果需要,可以为所有项目设置其他位置。

若要将工作区设置为其他位置,可以使用 $GOPATH 环境变量。 在处理更复杂的项目时,此环境变量有助于避免将来出现问题。

export GOPATH=/mnt/d/Programming_Design/Go

Go 工作区文件夹

每个 Go 工作区都包含三个基本文件夹:

  • bin :包含应用程序中的可执行文件。
  • src :包括位于工作站中的所有应用程序源代码。
  • pkg :包含可用库的已编译版本。 编译器可以链接这些库,而无需重新编译它们。

例如,工作站文件夹结构树可能与下面的示例类似:

bin/    hello    coolapppkg/    github.com/gorilla/        mux.asrc/    github.com/golang/example/        .git/    hello/        hello.go

VSCode Go 插件

在安装插件之前要先更改go的源

The "gopls" command is not available. Run "go install -v golang.org/x/tools/gopls@latest" to install.

然后点击上边的窗口的install All,即可完成插件的安装

第一个Go应用

文件夹组织形式:

xQJIts.png

package mainimport "fmt"func main() {fmt.Println("Hello World!")}

运行:

go run main.go
Hello World!

只生成二进制文件但是不运行:

go build main.go

代码解释

我们在 package main 语句中告诉 Go,我们将要创建的应用是一个可执行程序(可以运行的文件)。 我们的“Hello World!”应用是 main 包的一部分。

包是一组常用的源代码文件。 每个可执行应用都具有此第一行,即使项目或文件具有不同的名称。

import 语句使你的程序可以访问其他包中的其他代码。 在本例中,fmt 为标准库包。

你需要此 import 语句,因为你将在此程序的稍后部分使用此包中的函数将消息打印到屏幕上。 可以在程序中包含你想要或需要的任意数量的 import 语句。 但是,Go 在这方面是惯用的。 如果导入包,但不使用包中的相应函数,应用将不会进行编译Visual Studio Code 的一大功能是,当你保存文件时,它会自动删除程序中未使用的导入

VSCode 是自动帮助我们删除的,但是和Python什么的不一样,如果多了冗余的包程序是无法运行的

# command-line-arguments./main.go:4:8: imported and not used: "math"

func 语句是用于声明函数的保留字。 第一个函数名为“main”,因为它是程序的起始点。 整个 package main 中只能有一个 main() 函数(在第一行中定义的那个)。 在 main() 函数中,你调用了 fmt 包中的 Println 函数。 你发送了你希望在屏幕上看到的文本消息。

声明和使用变量

声明变量

var firstName string            // 声明单一变量var secondName, lastName string // 如果多个变量的类型相同,可以用逗号分隔一起声明多个变量var age int// 也可以在一个括号内批量声明变量var (thirdName, fourthName stringsecondage             int)

(VSCode会自动进行格式化,完全不用担心格式的问题)

初始化变量

可以在声明的时候直接对变量进行初始化,会自动对变量的类型进行推断,不用显式指定类型

var (    firstName string = "John"    lastName  string = "Doe"    age       int    = 32)

等价于下面的写法:

var (    firstName, lastName, age = "John", "Doe", 32)

在main函数内部声明+初始化(更加常用)

package mainimport "fmt"func main() {firstName, lastName := "John", "Doe"age := 32fmt.Println(firstName, lastName, age)}
John Doe 32

请注意,在定义变量名称后,需要在此处加入一个冒号等于号 (:=) 和相应的值。 使用冒号等于号时, 要声明的变量必须是新变量 。 如果使用冒号等于号并已经声明该变量,将不会对程序进行编译。

声明常量

用于声明常量的关键字是 const

const HTTPStatusOK = 200const (StatusOK              = 0StatusConnectionReset = 1StatusOtherError      = 2)

常量和变量之间既有相似之处,也有一些重要差异。 例如,你可以在不使用常量的情况下声明常量。 你不会收到错误消息。 不能使用冒号等于号来声明常量。 如果采用这种方式,Go 会发出警告。

在 Go 中,当你(在函数内部)声明一个变量但不使用它时,Go 会抛出错误,而不是像某些其他编程语言一样抛出警告。

基本数据类型

整数数字

一般来说,定义整数类型的关键字是 int。 但 Go 还提供了 int8int16int32int64 类型,其大小分别为 8、16、32 或 64 位的整数。 使用 32 位操作系统时,如果只是使用 int,则大小通常为 32 位。 在 64 位系统上,int 大小通常为 64 位。 但是,此行为可能因计算机而不同。 可以使用 uint。 但是,只有在出于某种原因需要将值表示为无符号数字的情况下,才使用此类型。 此外,Go 还提供 uint8uint16uint32uint64 类型。

var integer8 int8 = 127var integer16 int16 = 32767var integer32 int32 = 2147483647var integer64 int64 = 9223372036854775807

不能进行隐式转换,如果两个变量的类型不同,需要进行强制转换,否则编译不能通过。

浮点数字

Go 提供两种浮点数大小的数据类型:float32float64。 如果需要存储较大的数字,则可以使用这些类型,这些类型无法适应前面提到的任何一个整数类型。 这两种类型的区别是它们可以容纳的最大位数。

var float32 float32 = 2147483647var float64 float64 = 9223372036854775807fmt.Println(float32, float64)

可以使用 math 包中提供的 math.MaxFloat32math.MaxFloat64 常量来查找这两种类型的限制。

package mainimport ("fmt""math")func main() {fmt.Println(math.MaxFloat32, math.MaxFloat64)}
3.4028234663852886e+38 1.7976931348623157e+308

布尔型

布尔类型仅可能有两个值:truefalse。 你可以使用关键字 bool 声明布尔类型。 Go 不同于其他编程语言,在 Go 中,你不能将布尔类型隐式转换为 0 或 1。

var featureFlag bool = true

字符串

最后,让我们看一下编程语言中最常见的数据类型:string。 在 Go 中,关键字 string 用于表示字符串数据类型。 若要初始化字符串变量,你需要在双引号(")中定义值。 单引号(')用于单个字符(以及 runes,正如我们在上一节所述)。

var firstName string = "John"lastName := "Doe"fmt.Println(firstName, lastName)
John Doe

默认值

到目前为止,几乎每次声明变量时,都使用值对其进行了初始化。 但与在其他编程语言中不同的是,在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。 此功能非常方便,因为在使用之前,你无需检查变量是否已初始化。

下面列出了我们目前浏览过类型的几个默认值:

  • int 类型的 0(及其所有子类型,如 int64
  • float32float64 类型的 +0.000000e+000
  • bool 类型的 false
  • string 类型的空值

类型转换

Go 中隐式强制转换不起作用。 接下来,需要显式强制转换。 Go 提供了将一种数据类型转换为另一种数据类型的一些本机方法。

一种方法是对每个类型使用内置函数,如下所示:

var integer16 int16 = 127var integer32 int32 = 32767fmt.Println(int32(integer16) + integer32)

Go 的另一种转换方法是使用 strconv 包。 将 stringint

package mainimport ("fmt""strconv")func main() {i, _ := strconv.Atoi("-42")s := strconv.Itoa(-42)fmt.Println(i, s)}
-42 -42

有一个下划线 (_) 用作变量的名称。 在 Go 中(或Python中),这意味着我们不会使用该变量的值,而是要将其忽略。

创建函数

在 Go 中,函数允许你将一组可以从应用程序的其他部分调用的语句组合在一起。 你可以使用函数来组织代码并使其更易于阅读,而不是创建包含许多语句的程序。 更具可读性的代码也更易于维护。

与之交互的函数是 main() 函数。 Go 中的所有可执行程序都具有此函数,因为它是程序的起点。 你的程序中只能有一个 main() 函数。

命令行参数

package mainimport ("fmt""os""strconv")func main() {number1, _ := strconv.Atoi(os.Args[1])number2, _ := strconv.Atoi(os.Args[2])fmt.Println("Sum:", number1+number2)}

os.Args 变量包含传递给程序的每个命令行参数。 由于这些值的类型为 string,因此需要将它们转换为 int 以进行求和。

> go run main.go 3 5Sum: 8

自定义函数

使用 func 关键字来定义函数,然后为其指定名称。 在命名后,指定函数的参数列表。 你可以指定零个或多个参数。 你还可以定义函数的返回类型,该函数也可以是零个或多个。 (我们将在下一节中讨论如何返回多个值)。在定义所有这些值之后,你可以编写函数的正文内容。

package mainimport ("fmt""os""strconv")func main() {sum := sum(os.Args[1], os.Args[2])fmt.Println("Sum:", sum)}func sum(number1 string, number2 string) int {int1, _ := strconv.Atoi(number1)int2, _ := strconv.Atoi(number2)return int1 + int2}

此代码创建一个名为 sum 的函数,该函数采用两个 string 参数,并将它们强制转换为 int,然后返回求和所得的结果。 定义返回类型时,函数需要返回该类型的值。

在 Go 中,你还可以为函数的返回值设置名称,将其当作一个变量。

func sum(number1 string, number2 string) (result int) {int1, _ := strconv.Atoi(number1)int2, _ := strconv.Atoi(number2)result = int1 + int2return}

返回多个值

package mainimport ("fmt""os""strconv")func main() {sum, mul := calc(os.Args[1], os.Args[2])fmt.Println("Sum:", sum)fmt.Println("Mul:", mul)}func calc(number1 string, number2 string) (sum int, mul int) {int1, _ := strconv.Atoi(number1)int2, _ := strconv.Atoi(number2)sum = int1 + int2mul = int1 * int2return}
> go run main.go 3 5Sum: 8Mul: 15

更改函数参数值(指针)

将值传递给函数时,该函数中的每个更改都不会影响调用方。 Go 是“按值传递”编程语言。 每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。 在函数中对该变量所做的更改都不会影响你向函数发送的更改。

指针是包含另一个变量的内存地址的变量。 当你发送指向某个函数的指针时,不会传递值,而是传递地址内存。 因此,对该变量所做的每个更改都会影响调用方。

在 Go 中,有两个运算符可用于处理指针:

  • & 运算符使用其后对象的地址。
  • * 运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。
package mainimport "fmt"func main() {firstName := "John"updateName(&firstName)fmt.Println(firstName)}func updateName(name *string) {*name = "David"}

首先要做的就是修改函数的签名,以指明你要接收指针。 为此,请将参数类型从 string 更改为 *string。 (后者仍是字符串,但现在它是指向字符串 的 指针。)然后,将新值分配给该变量时,需要在该变量的左侧添加星号 (*) 以暂停该变量的值。 调用 updateName 函数时,系统不会发送值,而是发送变量的内存地址。 这就是前面的代码在变量左侧带有 & 符号的原因。

了解包

Go 包与其他编程语言中的库或模块类似。 你可以打包代码,并在其他位置重复使用它。 包的源代码可以分布在多个 .go 文件中。 到目前为止,我们已编写 main 包,并对其他本地包进行了一些引用。

main 包

你可能注意到,在 Go 中,甚至最直接的程序都是包的一部分。 通常情况下,默认包是 main 包,即目前为止一直使用的包。 如果程序是 main 包的一部分,Go 会生成二进制文件。 运行该文件时,它将调用 main() 函数。

换句话说,当你使用 main 包时,程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时,Go 不会生成二进制文件。 它生成包存档文件(扩展名为“.a”的文件)。

在 Go 中,包名称需遵循约定。 包使用其导入路径的最后一部分作为名称。 例如,Go 标准库包含名为 math/cmplx 的包,该包提供用于处理复数的有用代码。 此包的导入路径为 math/cmplx,导入包的方式如下所示:

import "math/cmplx"

创建包

在名为 calculator 的目录中 创建名为 sum.go 的文件。 树目录应如下列目录所示:

xQUdgI.png

用包的名称初始化 sum.go 文件:

package calculator

你现在可以开始编写包的函数和变量。 不同于其他编程语言,Go 不会提供 publicprivate 关键字,以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则:

  • 如需将某些内容设为专用内容,请以小写字母开始。
  • 如需将某些内容设为公共内容,请以大写字母开始。

接下来,让我们将以下代码添加到我们要创建的计算器包:

package calculatorvar logMessage = "[LOG]"// Version of the calculatorvar Version = "1.0"func internalSum(number int) int {    return number - 1}// Sum two integer numbersfunc Sum(number1, number2 int) int {    return number1 + number2}

让我们看一下该代码中的一些事项:

  • 只能从包内调用 logMessage 变量。
  • 可以从任何位置访问 Version 变量。 建议你添加注释来描述此变量的用途。 (此描述适用于包的任何用户。)
  • 只能从包内调用 internalSum 函数。
  • 可以从任何位置访问 Sum 函数。 建议你添加注释来描述此函数的用途。

若要确认一切正常,可在 calculator 目录中运行 go build 命令。 如果执行此操作,请注意系统不会生成可执行的二进制文件。

创建模块

你已将计算器功能放入包中。 现在可以将包放到模块中。 Go 模块通常包含可提供相关功能的包。 包的模块还指定了 Go 运行你组合在一起的代码所需的上下文。 此上下文信息包括编写代码时所用的 Go 版本。

此外,模块还有助于其他开发人员引用代码的特定版本,并更轻松地处理依赖项。 另一个优点是,我们的程序源代码无需严格存在于 $GOPATH/src 目录中。 如果释放该限制,则可以更方便地在其他项目中同时使用不同包版本。

(下面与教程不同,自己探索出了一个可用不报错的方法)

VSCode GOPATH设置:"go.gopath": "/mnt/d/Programming_Design/Go"

首先设置 go env -w GO111MODULE=on

如果 helloworld要引用 calculator,则文件夹的组织形式如下:

xlDdl4.png

$GOPATH/src/calculator创建 go.mod文件,其中文件第一行与文件夹同名

module calculatorgo 1.19

$GOPATH/src/helloworld创建 go.mod文件,其中文件第一行与文件夹同名,下面要写好版本号和包的路径

module helloworldgo 1.19require "calculator" v1.0.0replace "calculator" => "../calculator"

然后可以导入这个包并运行主文件

package mainimport ("calculator""fmt")func main() {total := calculator.Sum(3, 5)fmt.Println(total)fmt.Println("Version: ", calculator.Version)}
8Version:  1.0

引用外部(第三方)包

有时,程序需要引用其他开发人员编写的包。

测试后不是很明白,基本上是在主文件和 .mod文件中写入包的名称和版本即可。然后根据控制台的输出将包安装好即可使用

main.go

package mainimport ("calculator""fmt""rsc.io/quote")func main() {total := calculator.Sum(3, 5)fmt.Println(total)fmt.Println("Version: ", calculator.Version)fmt.Println(quote.Hello())}

go.mod

module helloworldgo 1.19require (calculator v1.0.0rsc.io/quote v1.5.2)require (golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirectrsc.io/sampler v1.3.0 // indirect)replace calculator => ../calculator

输出:

8Version:  1.0Ahoy, world!

使用控制流

if 语句的语法

与其他编程语言不同的是,在 Go 中,你不需要在条件中使用括号。 else 子句可选。 但是,大括号仍然是必需的。 此外,为了减少行,Go 不支持三元 if 语句,因此每次都需要编写完整的 if 语句。

package mainimport "fmt"func givemeanumber() int {return -1}func main() {if num := givemeanumber(); num < 0 {fmt.Println(num, "is negative")} else if num < 10 {fmt.Println(num, "has only one digit")} else {fmt.Println(num, "has multiple digits")}}

其中,有一个在 Go 中常见的约定进行高效编程的方式 if num := givemeanumber(); num < 0,同时接收函数的返回值,但是不重复进行接收,然后使用到if语句中进行判断。当然这个 num变量在 if的外部是无法使用的。

使用 switch 语句控制流

像其他编程语言一样,Go 支持 switch 语句。 可以使用 switch 语句来避免链接多个 if 语句。 使用 switch 语句,就不需维护和读取包含多个 if 语句的代码。 这些语句还可以让复杂的条件更易于构造。 请参阅以下部分的 switch 语句。

普通的switch语句:

package mainimport ("fmt""math/rand""time")func main() {sec := time.Now().Unix()rand.Seed(sec)i := rand.Int31n(10)switch i {case 0:fmt.Print("zero...")case 1:fmt.Print("one...")case 2:fmt.Print("two...")default:fmt.Print("no match...")}fmt.Println("ok")}

有时,多个表达式仅与一个 case 语句匹配。 在 Go 中,如果希望 case 语句包含多个表达式,请使用逗号 (,) 来分隔表达式。 此方法可避免代码重复。

package mainimport "fmt"func location(city string) (string, string) {var region stringvar continent stringswitch city {case "Delhi", "Hyderabad", "Mumbai", "Chennai", "Kochi":region, continent = "India", "Asia"case "Lafayette", "Louisville", "Boulder":region, continent = "Colorado", "USA"case "Irvine", "Los Angeles", "San Diego":region, continent = "California", "USA"default:region, continent = "Unknown", "Unknown"}return region, continent}func main() {region, continent := location("Irvine")fmt.Printf("John works in %s, %s\n", region, continent)}
John works in California, USA

case 语句的表达式中包含的值对应于 switch 语句验证的变量的数据类型。

调用函数

switch 还可以调用函数。 在该函数中,可以针对可能的返回值编写 case 语句。

第一种是在switch上调用函数,对返回值进行判断

package mainimport ("fmt""time")func main() {switch time.Now().Weekday().String() {case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":fmt.Println("It's time to learn some Go.")default:fmt.Println("It's weekend, time to rest!")}fmt.Println(time.Now().Weekday().String())}
It's time to learn some Go.Wednesday

第二种是在case上调用函数

package mainimport ("fmt""regexp")func main() {var email = regexp.MustCompile(`^[^@]+@[^@.]+\.[^@.]+`)var phone = regexp.MustCompile(`^[(]?[0-9][0-9][0-9][). \-]*[0-9][0-9][0-9][.\-]?[0-9][0-9][0-9][0-9]`)contact := "foo@bar.com"switch {case email.MatchString(contact):fmt.Println(contact, "is an email")case phone.MatchString(contact):fmt.Println(contact, "is a phone number")default:fmt.Println(contact, "is not recognized")}}
foo@bar.com is an email

上面的 switch 语句中省略了条件,就像在 if 语句中那样。 此模式类似于比较 true 值,就像强制 switch 语句一直运行一样。

一个条件 switch 块比一长串的 ifelse if 语句更易于维护。

使逻辑进入到下一个 case

在某些编程语言中,你会在每个 case 语句末尾写一个 break 关键字。 但在 Go 中,当逻辑进入某个 case 时,它会退出 switch 块,除非你显式停止它。 若要使逻辑进入到下一个紧邻的 case,请使用 fallthrough 关键字。

package mainimport ("fmt")func main() {switch num := 15; {case num < 50:fmt.Printf("%d is less than 50\n", num)fallthroughcase num > 100:fmt.Printf("%d is greater than 100\n", num)fallthroughcase num < 200:fmt.Printf("%d is less than 200\n", num)}}
15 is less than 5015 is greater than 10015 is less than 200

请注意,由于 num 为 15(小于 50),因此它与第一个 case 匹配。 但是,num 不大于 100。 由于第一个 case 语句包含 fallthrough 关键字,因此逻辑会立即转到下一个 case 语句,而不会对该 case 进行验证。 因此,在使用 fallthrough 关键字时必须谨慎。 该代码产生的行为可能不是你想要的。

for 表达式

另一个常用控制流是循环。 Go 只使用一个循环构造,即 for 循环。 但是,你可以通过多种方式表示循环。

package mainimport ("fmt")func main() {sum := 0for i := 1; i <= 100; i++ {sum += i}fmt.Println("sum of 1..100 is", sum)}
sum of 1..100 is 5050

空预处理语句和后处理语句

package mainimport ("fmt""math/rand""time")func main() {var num int64rand.Seed(time.Now().Unix())for num != 5 {num = rand.Int63n(15)fmt.Println(num)}}

只要 num 变量保存的值与 5 不同,程序就会输出一个随机数。

无限循环和 break 语句

可以在 Go 中编写的另一种循环模式是无限循环。 在这种情况下,你不编写条件表达式,也不编写预处理语句或后处理语句, 而是采取退出循环的方式进行编写。 否则,逻辑永远都不会退出。 若要使逻辑退出循环,请使用 break 关键字。

package mainimport ("fmt""math/rand""time")func main() {var num int32sec := time.Now().Unix()rand.Seed(sec)for {fmt.Print("Writing inside the loop...")if num = rand.Int31n(10); num == 5 {fmt.Println("finish!")break}fmt.Println(num)}}

在 Go 中,可以使用 continue 关键字跳过循环的当前迭代。 例如,可以使用此关键字在循环继续之前运行验证。 也可以在编写无限循环并需要等待资源变得可用时使用它。

package mainimport "fmt"func main() {sum := 0for num := 1; num <= 100; num++ {if num%5 == 0 {continue}sum += num}fmt.Println("The sum of 1 to 100, but excluding numbers divisible by 5, is", sum)}
The sum of 1 to 100, but excluding numbers divisible by 5, is 4000

使用 defer、panic 和 recover 函数进行控制

defer 函数

在 Go 中,defer 语句会推迟函数(包括任何参数)的运行,直到包含 defer 语句的函数完成。 通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟某个函数的运行。

可以根据需要推迟任意多个函数。 defer 语句按逆序运行,先运行最后一个,最后运行第一个。

package mainimport "fmt"func main() {for i := 1; i <= 4; i++ {defer fmt.Println("deferred", -i)fmt.Println("regular", i)}}
regular 1regular 2regular 3regular 4deferred -4deferred -3deferred -2deferred -1

在此示例中,请注意,每次推迟 fmt.Println("deferred", -i) 时,都会存储 i 的值,并会将其运行任务添加到队列中。 在 main() 函数输出完 regular 值后,所有推迟的调用都会运行。 这就是你看到输出采用逆序(后进先出)的原因。

defer 函数的一个典型用例是在使用完文件后将其关闭。

package mainimport ("fmt""io""os")func main() {newfile, error := os.Create("learnGo.txt")if error != nil {fmt.Println("Error: Could not create file.")return}defer newfile.Close()if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {fmt.Println("Error: Could not write to file.")return}newfile.Sync()}

创建或打开某个文件后,可以推迟 .Close() 函数的执行,以免在你完成后忘记关闭该文件。

panic 函数

运行时错误会使 Go 程序崩溃,例如尝试通过使用超出范围的索引或取消引用 nil 指针来访问数组。 你也可以强制程序崩溃。

内置 panic() 函数可以停止 Go 程序中的正常控制流。 当你使用 panic 调用时,任何延迟的函数调用都将正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。 此消息包含错误信息和堆栈跟踪,有助于诊断问题的根本原因。

调用 panic() 函数时,可以添加任何值作为参数。 通常,你会发送一条错误消息,说明为什么会进入紧急状态。

例如,下面的代码将 panicdefer 函数组合在一起。 尝试运行此代码以了解控制流的中断。 请注意,清理过程仍会运行。

package mainimport "fmt"func highlow(high int, low int) {if high < low {fmt.Println("Panic!")panic("highlow() low greater than high")}defer fmt.Println("Deferred: highlow(", high, ",", low, ")")fmt.Println("Call: highlow(", high, ",", low, ")")highlow(high, low+1)}func main() {highlow(2, 0)fmt.Println("Program finished successfully!")}
Call: highlow( 2 , 0 )Call: highlow( 2 , 1 )Call: highlow( 2 , 2 )Panic!Deferred: highlow( 2 , 2 )Deferred: highlow( 2 , 1 )Deferred: highlow( 2 , 0 )panic: highlow() low greater than highgoroutine 1 [running]:main.highlow(0x4b8018?, 0xc000012018?)        /mnt/d/Programming_Design/Go/src/helloworld/main.go:8 +0x285main.highlow(0x2, 0x2)        /mnt/d/Programming_Design/Go/src/helloworld/main.go:13 +0x211main.highlow(0x2, 0x1)        /mnt/d/Programming_Design/Go/src/helloworld/main.go:13 +0x211main.highlow(0x2, 0x0)        /mnt/d/Programming_Design/Go/src/helloworld/main.go:13 +0x211main.main()        /mnt/d/Programming_Design/Go/src/helloworld/main.go:17 +0x25exit status 2

下面是运行代码时会发生的情况:

  1. 一切正常运行。 程序将输出传递到 highlow() 函数中的高值和低值。
  2. 如果 low 的值大于 high 的值,则程序会崩溃。 会显示“Panic!”消息。 此时,控制流中断,所有推迟的函数都开始输出“Deferred...”消息。
  3. 程序崩溃,并显示完整的堆栈跟踪。 不会显示“Program finished successfully!”消息。

recover 函数

有时,你可能想要避免程序崩溃,改为在内部报告错误。 或者,你可能想要先清理混乱情况,然后再让程序崩溃。 例如,你可能想要关闭与某个资源的连接,以免出现更多问题。

Go 提供内置 recover() 函数,让你可以在程序崩溃之后重新获得控制权。 你只会在你同时调用 defer 的函数中调用 recover。 如果调用 recover() 函数,则在正常运行的情况下,它会返回 nil,没有任何其他作用。

尝试修改前面的代码中的 main 函数,以添加对 recover() 的调用,如下所示:

package mainimport "fmt"func highlow(high int, low int) {if high < low {fmt.Println("Panic!")panic("highlow() low greater than high")}defer fmt.Println("Deferred: highlow(", high, ",", low, ")")fmt.Println("Call: highlow(", high, ",", low, ")")highlow(high, low+1)}func main() {defer func() {handler := recover()if handler != nil {fmt.Println("main(): recover", handler)}}()highlow(2, 0)fmt.Println("Program finished successfully!")}
Call: highlow( 2 , 0 )Call: highlow( 2 , 1 )Call: highlow( 2 , 2 )Panic!Deferred: highlow( 2 , 2 )Deferred: highlow( 2 , 1 )Deferred: highlow( 2 , 0 )main(): recover highlow() low greater than high

main() 函数中,你会将一个可以调用 recover() 函数的匿名函数推迟。 当程序处于紧急状态时,对 recover() 的调用无法返回 nil。 你可以在此处执行一些操作来清理混乱,但在本例中,你只是简单地输出一些内容。

panicrecover 函数的组合是 Go 处理异常的惯用方式。 其他编程语言使用 try/catch 块。 Go 首选此处所述的方法。

练习 - 在 Go 中使用控制流

编写 FizzBuzz 程序

首先,编写一个用于输出数字(1 到 100)的程序,其中有以下变化:

  • 如果数字可被 3 整除,则输出 Fizz
  • 如果数字可被 5 整除,则输出 Buzz
  • 如果数字可同时被 3 和 5 整除,则输出 FizzBuzz
  • 如果前面的情况都不符合,则输出该数字。

尝试使用 switch 语句。

package mainimport (    "fmt"    "strconv")func fizzbuzz(num int) string {    switch {    case num%15 == 0:        return "FizzBuzz"    case num%3 == 0:        return "Fizz"    case num%5 == 0:        return "Buzz"    }    return strconv.Itoa(num)}func main() {    for num := 1; num <= 100; num++ {        fmt.Println(fizzbuzz(num))    }}

查找质数

编写一个程序来查找小于 20 的所有质数。 质数是大于 1 的任意数字,只能被它自己和 1 整除。 “整除”表示经过除法运算后没有余数。 与大多数编程语言一样,Go 还提供了一种方法来检查除法运算是否产生余数。 我们可以使用模数 %(百分号)运算符。

在本练习中,你将更新一个名为 findprimes 的函数,以检查数值是否为质数。 该函数有一个整数参数,并返回一个布尔值。 函数通过检查是否有余数来测试输入数字是否为质数。 如果数字为质数,则该函数返回 true。

package mainimport "fmt"func findprimes(number int) bool {for i := 2; i < number; i++ {if number%i == 0 {return false}}if number > 1 {return true} else {return false}}func main() {fmt.Println("Prime numbers less than 20:")for number := 1; number < 20; number++ {if findprimes(number) {fmt.Printf("%v ", number)}}fmt.Println()}
Prime numbers less than 20:2 3 5 7 11 13 17 19

要求用户输入一个数字,如果该数字为负数,则进入紧急状态

编写一个要求用户输入一个数字的程序。 在开始时使用以下代码片段:

此程序要求用户输入一个数字,然后将其输出。 修改示例代码,使之符合以下要求:

  • 持续要求用户输入一个整数。 此循环的退出条件应该是用户输入了一个负数。
  • 当用户输入负数时,让程序崩溃。 然后输出堆栈跟踪错误。
  • 如果数字为 0,则输出“0 is neither negative nor positive”。 继续要求用户输入数字。
  • 如果数字为正数,则输出“You entered: X”(其中的 X 为输入的数字)。 继续要求用户输入数字。
package mainimport "fmt"func main() {val := 0for {fmt.Print("Enter number: ")fmt.Scanf("%d", &val)if val < 0 {panic("Negative!")} else if val == 0 {fmt.Println("0 is neither negative nor positive")} else {fmt.Printf("You entered: %d\n", val)}}}

使用数组

Go 中的数组是一种特定类型且长度固定的数据结构。 它们可具有零个或多个元素,你必须在声明或初始化它们时定义大小。 此外,它们一旦创建,就无法调整大小。 鉴于这些原因,数组在 Go 程序中并不常用,但它们是切片和映射的基础。

声明数组

要在 Go 中声明数组,必须定义其元素的数据类型以及该数组可容纳的元素数目。 然后,可采用下标表示法访问数组中的每个元素,其中第一个元素是 0,最后一个元素是数组长度减去 1(长度 - 1)。

package mainimport "fmt"func main() {var a [3]inta[1] = 10fmt.Println(a[0])fmt.Println(a[1])fmt.Println(a[len(a)-1])}
0100

已声明的数组访问其元素时不会遇到错误。 默认情况下,Go 会用默认数据类型初始化每个元素。 这样的话,int 的默认值为零。 不过,你可为特定位置分配值。 这就是为什么你会看到 a[1] = 10。 你可采用上述表示法来访问该元素。 另请注意,为了打印出第一个元素,我们使用了 a[0]。 为了打印出最后一个元素,我们使用了 a[len(a)-1]len 函数是 Go 中的内置函数,用于获取数组、切片或映射中的元素数。

初始化数组

声明数组时,还可使用非默认值来初始化数组。

package mainimport "fmt"func main() {cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}fmt.Println("Cities:", cities)}
Cities: [New York Paris Berlin Madrid ]

数组中的省略号

如果你不知道你将需要多少个位置,但知道你将具有多少数据,那么还有一种声明和初始化数组的方法是使用省略号 (...)

package mainimport "fmt"func main() {cities := [...]string{"New York", "Paris", "Berlin", "Madrid"}fmt.Println("Cities:", cities)}

另一种有趣的数组初始化方法是使用省略号并仅为最新的位置指定值。

package mainimport "fmt"func main() {numbers := [...]int{99: -1}fmt.Println("First Position:", numbers[0])fmt.Println("Last Position:", numbers[99])fmt.Println("Length:", len(numbers))}
First Position: 0Last Position: -1Length: 100

注意数组的长度是 100,因为你为第 99 个位置指定了一个值。

多维数组

如果需要处理复杂数据结构,请记住 Go 支持多维数组。

package mainimport "fmt"func main() {var twoD [3][5]intfor i := 0; i < 3; i++ {for j := 0; j < 5; j++ {twoD[i][j] = (i + 1) * (j + 1)}fmt.Println("Row", i, twoD[i])}fmt.Println("\nAll at once:", twoD)}
Row 0 [1 2 3 4 5]Row 1 [2 4 6 8 10]Row 2 [3 6 9 12 15]All at once: [[1 2 3 4 5] [2 4 6 8 10] [3 6 9 12 15]]

了解切片

与数组一样,切片也是 Go 中的一种数据类型,它表示一系列类型相同的元素。 不过,与数组更重要的区别是切片的大小是动态的,不是固定的。

切片是数组或另一个切片之上的数据结构。 我们将源数组或切片称为基础数组。 通过切片,可访问整个基础数组,也可仅访问部分元素。

切片只有 3 个组件:

  • 指向基础数组中第一个可访问元素的指针 。 此元素不一定是数组的第一个元素 array[0]
  • 切片的长度 。 切片中的元素数目。
  • 切片的容量 。 切片开头与基础数组结束之间的元素数目。

声明和初始化切片

要声明切片,可采用与声明数组相同的方式操作。

package mainimport "fmt"func main() {months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}fmt.Println(months)fmt.Println("Length:", len(months))fmt.Println("Capacity:", cap(months))}
[January February March April May June July August September October November December]Length: 12Capacity: 12

切片项

Go 支持切片运算符 s[i:p],其中:

  • s 表示数组。
  • i 表示指向要添加到新切片的基础数组(或另一个切片)的第一个元素的指针。 变量 i 对应于数组 array[i] 中索引位置 i 处的元素。 请记住,此元素不一定是基础数组的第一个元素 array[0]
  • p 表示创建新切片时要使用的基础数组中的元素数目。 变量 p 对应于可用于新切片的基础数组中的最后一个元素。 可在位置 array[i+1] 找到基础数组中位置 p 处的元素。 请注意,此元素不一定是基础数组的最后一个元素 array[len(array)-1]
package mainimport "fmt"func main() {months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}quarter1 := months[0:3]quarter2 := months[3:6]quarter3 := months[6:9]quarter4 := months[9:12]fmt.Println(quarter1, len(quarter1), cap(quarter1))fmt.Println(quarter2, len(quarter2), cap(quarter2))fmt.Println(quarter3, len(quarter3), cap(quarter3))fmt.Println(quarter4, len(quarter4), cap(quarter4))}
[January February March] 3 12[April May June] 3 9[July August September] 3 6[October November December] 3 3

注意,切片的长度不变,但容量不同。 我们来了解 quarter2 切片。 声明此切片时,你指出希望切片从位置编号 3 开始,最后一个元素位于位置编号 6。 切片长度为 3 个元素,但容量为 9,原因是基础数组有更多元素或位置可供使用,但对切片而言不可见。

切片容量仅指出切片可扩展的程度。 因此可从 quarter2 创建扩展切片

package mainimport "fmt"func main() {months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}quarter2 := months[3:6]quarter2Extended := quarter2[:4]fmt.Println(quarter2, len(quarter2), cap(quarter2))fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))}
[April May June] 3 9[April May June July] 4 9

追加项

切片与数组之间有何不同? 第一个区别是切片的大小不是固定的,而是动态的。 创建切片后,可向其添加更多元素,这样切片就会扩展。

Go 提供了内置函数 append(slice, element),便于你向切片添加元素。 将要修改的切片和要追加的元素作为值发送给该函数。 然后,append 函数会返回一个新的切片,将其存储在变量中。 对于要更改的切片,变量可能相同。

package mainimport "fmt"func main() {var numbers []intfor i := 0; i < 10; i++ {numbers = append(numbers, i)fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)}}
0       cap=1   [0]1       cap=2   [0 1]2       cap=4   [0 1 2]3       cap=4   [0 1 2 3]4       cap=8   [0 1 2 3 4]5       cap=8   [0 1 2 3 4 5]6       cap=8   [0 1 2 3 4 5 6]7       cap=8   [0 1 2 3 4 5 6 7]8       cap=16  [0 1 2 3 4 5 6 7 8]9       cap=16  [0 1 2 3 4 5 6 7 8 9]

当切片容量不足以容纳更多元素时,Go 的容量将翻倍。 它将新建一个具有新容量的基础数组。 无需执行任何操作即可使容量增加。 Go 会自动扩充容量。

删除项

Go 没有内置函数用于从切片中删除元素。 可使用上述切片运算符 s[i:p] 来新建一个仅包含所需元素的切片。

package mainimport "fmt"func main() {letters := []string{"A", "B", "C", "D", "E"}remove := 2if remove < len(letters) {fmt.Println("Before", letters, "Remove ", letters[remove])letters = append(letters[:remove], letters[remove+1:]...)fmt.Println("After", letters)}}
Before [A B C D E] Remove  CAfter [A B D E]

创建切片的副本

Go 具有内置函数 copy(dst, src []Type) 用于创建切片的副本。

更改切片中的元素时,基础数组将随之更改。

package mainimport "fmt"func main() {letters := []string{"A", "B", "C", "D", "E"}fmt.Println("Before", letters)slice1 := letters[0:2]slice2 := letters[1:4]slice1[1] = "Z"fmt.Println("After", letters)fmt.Println("Slice2", slice2)}
Before [A B C D E]After [A Z C D E]Slice2 [Z C D]

创建副本则不会产生影响

package mainimport "fmt"func main() {letters := []string{"A", "B", "C", "D", "E"}fmt.Println("Before", letters)slice1 := letters[0:2]slice2 := make([]string, 3)copy(slice2, letters[1:4])slice1[1] = "Z"fmt.Println("After", letters)fmt.Println("Slice2", slice2)}
Before [A B C D E]After [A Z C D E]Slice2 [B C D]

使用映射

Go 中的映射是一个哈希表,是键值对的集合。 映射中所有的键都必须具有相同的类型,它们的值也是如此。 不过,可对键和值使用不同的类型。 例如,键可以是数字,值可以是字符串。 若要访问映射中的特定项,可引用该项的键。

声明和初始化映射

若要声明映射,需要使用 map 关键字。 然后,定义键和值类型,如下所示:map[T]T

package mainimport "fmt"func main() {studentsAge := map[string]int{"john": 32,"bob":  31,}fmt.Println(studentsAge)}
map[bob:31 john:32]

如果不想使用项来初始化映射,可使用内置函数 make() 在上一部分创建切片。

添加项

要添加项,无需像对切片一样使用内置函数。 映射更加简单。 你只需定义键和值即可。 如果没有键值对,则该项会添加到映射中。

package mainimport "fmt"func main() {studentsAge := make(map[string]int)studentsAge["john"] = 32studentsAge["bob"] = 31fmt.Println(studentsAge)}

访问项

若要访问映射中的项,可使用常用的下标表示法 m[key]

在映射中使用下标表示法时,即使映射中没有键,你也总会获得默认值的响应。

package mainimport "fmt"func main() {studentsAge := make(map[string]int)studentsAge["john"] = 32studentsAge["bob"] = 31fmt.Println("Bob's age is", studentsAge["bob"])fmt.Println("Christy's age is", studentsAge["christy"])}
Bob's age is 31Christy's age is 0

在很多情况下,访问映射中没有的项时 Go 不会返回错误,这是正常的。 但有时需要知道某个项是否存在。 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。

package mainimport "fmt"func main() {studentsAge := make(map[string]int)studentsAge["john"] = 32studentsAge["bob"] = 31age, exist := studentsAge["christy"]if exist {fmt.Println("Christy's age is", age)} else {fmt.Println("Christy's age couldn't be found")}}
Christy's age couldn't be found

删除项

若要从映射中删除项,请使用内置函数 delete()

package mainimport "fmt"func main() {studentsAge := make(map[string]int)studentsAge["john"] = 32studentsAge["bob"] = 31delete(studentsAge, "bob")delete(studentsAge, "christy")fmt.Println(studentsAge)}
map[john:32]

如果你尝试删除不存在的项,Go 不会执行 panic

映射中的循环

最后,让我们看看如何在映射中进行循环来以编程方式访问其所有的项。 为此,可使用基于范围的循环

package mainimport ("fmt")func main() {studentsAge := make(map[string]int)studentsAge["john"] = 32studentsAge["bob"] = 31for name, age := range studentsAge {fmt.Printf("%s\t%d\n", name, age)}}
john    32bob     31

range 会首先生成项的键,然后再生成该项的值。 可使用 _ 变量忽略其中任何一个

使用结构

有时,你需要在一个结构中表示字段的集合。在 Go 中,可使用结构将可能构成记录的不同字段组合在一起。Go 中的结构也是一种数据结构,它可包含零个或多个任意类型的字段,并将它们表示为单个实体。

声明和初始化结构

若要声明结构,需要使用 struct 关键字,还要使用希望新的数据类型具有的字段及其类型的列表。

若要访问结构的各个字段,可使用点表示法 (.) 做到这一点

可使用 & 运算符生成指向结构的指针以修改结构中的项

package mainimport "fmt"type Employee struct {ID        intFirstName stringLastName  stringAddress   string}func main() {employee := Employee{LastName: "Doe", FirstName: "John"}fmt.Println(employee)employeeCopy := &employeeemployeeCopy.FirstName = "David"fmt.Println(employee)}
{0 John Doe }{0 David Doe }

结构嵌入

通过 Go 中的结构,可将某结构嵌入到另一结构中。

package mainimport "fmt"type Person struct {ID        intFirstName stringLastName  stringAddress   string}type Employee struct {PersonManagerID int}type Contractor struct {PersonCompanyID int}func main() {employee := Employee{Person: Person{FirstName: "John",},}employee.LastName = "Doe"employee.ManagerID = 2fmt.Println(employee)}
{{0 John Doe } 2}

用 JSON 编码和解码结构

最后,可使用结构来对 JSON 中的数据进行编码和解码。 Go 对 JSON 格式提供很好的支持,该格式已包含在标准库包中。

你还可执行一些操作,例如重命名结构中字段的名称。 例如,假设你不希望 JSON 输出显示 FirstName 而只显示 name,或者忽略空字段, 可使用如下例所示的字段标记:

然后,若要将结构编码为 JSON,请使用 json.Marshal 函数。 若要将 JSON 字符串解码为数据结构,请使用 json.Unmarshal 函数。 下例将所有内容组合在一起,将员工数组编码为 JSON,并将输出解码为新的变量:

package mainimport ("encoding/json""fmt")type Person struct {ID        intFirstName string `json:"name"`LastName  stringAddress   string `json:"address,omitempty"`}type Employee struct {PersonManagerID int}type Contractor struct {PersonCompanyID int}func main() {employees := []Employee{{Person: Person{LastName: "Doe", FirstName: "John",},},{Person: Person{LastName: "Campbell", FirstName: "David",},},}data, _ := json.Marshal(employees)fmt.Printf("%s\n", data)var decoded []Employeejson.Unmarshal(data, &decoded)fmt.Printf("%v\n", decoded)}
[{"ID":0,"name":"John","LastName":"Doe","ManagerID":0},{"ID":0,"name":"David","LastName":"Campbell","ManagerID":0}][{{0 John Doe } 0} {{0 David Campbell } 0}]

练习 - 数据类型

编写一个程序来计算斐波纳契数列

在这第一个挑战中,你将编写一个程序来计算某个数字的斐波纳契数列。 这是在学习新语言时要编码的一个典型的编程练习。 你将编写一个函数,它返回一个包含按斐波纳契数列排列的所有数字的切片,而这些数字是通过根据用户输入的大于 2 的数字计算得到的。 让我们假设小于 2 的数字会导致错误,并返回一个 nil 切片。

请记住,斐波那契数列是一个数字列表,其中每个数字是前两个斐波那契数字之和。 例如,数字 6 的序列是 1,1,2,3,5,8,数字 7 的序列是 1,1,2,3,5,8,13,数字 8 的序列是 1,1,2,3,5,8,13,21,以此类推。

package mainimport "fmt"func fibonacci(n int) []int {if n < 2 {return make([]int, 0)}nums := make([]int, n)nums[0], nums[1] = 1, 1for i := 2; i < n; i++ {nums[i] = nums[i-1] + nums[i-2]}return nums}func main() {var num intfmt.Print("What's the Fibonacci sequence you want? ")fmt.Scanln(&num)fmt.Println("The Fibonacci sequence is:", fibonacci(num))}

创建罗马数字转换器

编写一个程序来转换罗马数字(例如将 MCLX 转换成 1,160)。 使用映射加载要用于将字符串字符转换为数字的基本罗马数字。 例如,M 将是映射中的键,其值将为 1000。 使用以下字符串字符映射表列表:

  • M => 1000
  • D => 500
  • C => 100
  • L => 50
  • X => 10
  • V => 5
  • I => 1

如果用户输入的字母与上述列表中的不同,则打印一个错误。

请记住在有些情况下,较小的数字会排在较大的数字前面,因此不能仅仅将数字相加。 例如,数字 MCM 应打印为 1,900

package mainimport ("fmt")func romanToArabic(numeral string) int {romanMap := map[rune]int{'M': 1000,'D': 500,'C': 100,'L': 50,'X': 10,'V': 5,'I': 1,}arabicVals := make([]int, len(numeral)+1)for index, digit := range numeral {if val, present := romanMap[digit]; present {arabicVals[index] = val} else {fmt.Printf("Error: The roman numeral %s has a bad digit: %c\n", numeral, digit)return 0}}total := 0for index := 0; index < len(numeral); index++ {if arabicVals[index] < arabicVals[index+1] {arabicVals[index] = -arabicVals[index]}total += arabicVals[index]}return total}func main() {fmt.Println("MCLX is", romanToArabic("MCLX"))fmt.Println("MCMXCIX is ", romanToArabic("MCMXCIX"))fmt.Println("MCMZ is", romanToArabic("MCMZ"))}

如何在 Go 中处理错误

编写程序时,需要考虑程序失败的各种方式,并且需要管理失败。 无需让用户看到冗长而混乱的堆栈跟踪错误。 让他们看到有关错误的有意义的信息更好。 正如你所看到的,Go 具有 panicrecover 之类的内置函数来管理程序中的异常或意外行为。 但错误是已知的失败,你的程序应该可以处理它们。

Go 的错误处理方法只是一种只需要 ifreturn 语句的控制流机制。 例如,在调用函数以从 employee 对象获取信息时,可能需要了解该员工是否存在。 Go 处理此类预期错误的一贯方法如下所示:

employee, err := getInformation(1000)if err != nil {    // Something is wrong. Do something.}

注意 getInformation 函数返回了 employee 结构,还返回了错误作为第二个值。 该错误可能为 nil。 如果错误为 nil,则表示成功。 如果错误不是 nil,则表示失败。 非 nil 错误附带一条错误消息,你可以打印该错误消息,也可以记录该消息(更可取)。

错误处理策略

当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。

你可能还需要在传播错误之前添加更多信息。 为此,可以使用 fmt.Errorf() 函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误,如下所示:

func getInformation(id int) (*Employee, error) {    employee, err := apiCallEmployee(1000)    if err != nil {        return nil, fmt.Errorf("got an error when getting the employee information: %v", err)    }    return employee, nil}

另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟

func getInformation(id int) (*Employee, error) {    for tries := 0; tries < 3; tries++ {        employee, err := apiCallEmployee(1000)        if err == nil {            return employee, nil        }        fmt.Println("Server is not responding, retrying ...")        time.Sleep(time.Second * 2)    }    return nil, fmt.Errorf("server has failed to respond to get the employee information")}

创建可重用的错误

有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在 Go 中,你可以使用 errors.New() 函数创建错误并在若干部分中重复使用这些错误,如下所示:

var ErrNotFound = errors.New("Employee not found!")func getInformation(id int) (*Employee, error) {    if id != 1001 {        return nil, ErrNotFound    }    employee := Employee{LastName: "Doe", FirstName: "John"}    return &employee, nil}

最后,如果你具有错误变量,则在处理调用方函数中的错误时可以更具体。 errors.Is() 函数允许你比较获得的错误的类型

employee, err := getInformation(1000)if errors.Is(err, ErrNotFound) {    fmt.Printf("NOT FOUND: %v\n", err)} else {    fmt.Print(employee)}

用于错误处理的推荐做法

在 Go 中处理错误时,请记住下面一些推荐做法:

  • 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
  • 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
  • 创建尽可能多的可重用错误变量。
  • 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
  • 在记录错误时记录尽可能多的详细信息(我们将在下一部分介绍记录方法),并打印出最终用户能够理解的错误。

如何在 Go 中记录

日志在程序中发挥着重要作用,因为它们是在出现问题时你可以检查的信息源。 通常,发生错误时,最终用户只会看到一条消息,指示程序出现问题。 从开发人员的角度来看,我们需要简单错误消息以外的更多信息。 这主要是因为我们想要再现该问题以编写适当的修补程序。

log

对于初学者,Go 提供了一个用于处理日志的简单标准包。 可以像使用 fmt 包一样使用此包。 该标准包不提供日志级别,且不允许为每个包配置单独的记录器。 如果需要编写更复杂的日志记录配置,可以使用记录框架执行此操作。

package mainimport ("log")func main() {log.Print("Hey, I'm a log!")}
2022/10/05 15:37:16 Hey, I'm a log!

默认情况下,log.Print() 函数将日期和时间添加为日志消息的前缀。 你可以通过使用 fmt.Print() 获得相同的行为,但使用 log 包还能执行其他操作,例如将日志发送到文件。 稍后我们将详细介绍 log 包功能。

你可以使用 log.Fatal() 函数记录错误并结束程序,就像使用 os.Exit(1) 一样。

package mainimport ("fmt""log")func main() {log.Fatal("Hey, I'm an error log!")fmt.Print("Can you see me?")}
2022/10/05 15:38:56 Hey, I'm an error log!exit status 1

使用 log.Panic() 函数时会出现类似行为,但是还会获取错误堆栈跟踪。

另一重要函数是 log.SetPrefix()。 可使用它向程序的日志消息添加前缀。

package mainimport ("log")func main() {log.SetPrefix("main(): ")log.Print("Hey, I'm a log!")log.Fatal("Hey, I'm an error log!")}
main(): 2022/10/05 15:40:36 Hey, I'm a log!main(): 2022/10/05 15:40:36 Hey, I'm an error log!exit status 1

记录到文件

除了将日志打印到控制台之外,你可能还希望将日志发送到文件,以便稍后或实时处理这些日志。

package mainimport ("log""os")func main() {file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)if err != nil {log.Fatal(err)}defer file.Close()log.SetOutput(file)log.Print("Hey, I'm a log!")}

最后,可能有 log 包中的函数不足以处理问题的情况。 你可能会发现,使用记录框架而不编写自己的库很有用。 Go 的几个记录框架有 LogruszerologzapApex

在 Go 中使用方法

面向对象编程 (OOP) 是一种广受欢迎的编程模式,大部分编程语言都支持(至少部分支持)。 Go 是其中一种语言,但它并不完全支持所有 OOP 原则。

Go 中的方法是一种特殊类型的函数,但存在一个简单的区别:你必须在函数名称之前加入一个额外的参数。 此附加参数称为 接收方

如你希望分组函数并将其绑定到自定义类型,则方法非常有用。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。

声明方法

到目前为止,你仅将结构用作可在 Go 中创建的另一种自定义类型。 在此模块中你将了解到,通过添加方法你可以将行为添加到你所创建的结构中。

在声明方法之前,必须先创建结构。 假设你想要创建一个几何包,并决定创建一个名为 triangle 的三角形结构作为此程序包的一个组成部分。 然后,你需要使用一种方法来计算此三角形的周长。 你可以在 Go 中将其表示为:

type triangle struct {size int}func (t triangle) perimeter() int {return t.size * 3}

结构看起来像普通结构,但 perimeter() 函数在函数名称之前有一个类型 triangle 的额外参数。 也就是说,在使用结构时,你可以按如下方式调用函数:

func main() {t := triangle{3}fmt.Println("Perimeter:", t.perimeter())}

如果尝试按平常的方式调用 perimeter() 函数,则此函数将无法正常工作,因为此函数的签名表明它需要接收方。 正因如此,调用此方法的唯一方式是先声明一个结构,获取此方法的访问权限。 这也意味着,只要此方法属于不同的结构,你甚至可以为其指定相同的名称。 例如,你可以使用 perimeter() 函数声明一个 square 结构,具体如下所示:

package mainimport "fmt"type triangle struct {size int}type square struct {size int}func (t triangle) perimeter() int {return t.size * 3}func (s square) perimeter() int {return s.size * 4}func main() {t := triangle{3}s := square{4}fmt.Println("Perimeter (triangle):", t.perimeter())fmt.Println("Perimeter (square):", s.perimeter())}
Perimeter (triangle): 9Perimeter (square): 16

通过对 perimeter() 函数的两次调用,编译器将根据接收方类型来确定要调用的函数。 这有助于在各程序包之间保持函数的一致性和名称的简短,并避免将包名称作为前缀。

方法中的指针

有时,方法需要更新变量,或者,如果参数太大,则可能需要避免复制它。 在遇到此类情况时,你需要使用指针传递变量的地址。 在之前的模块中,当我们在讨论指针时提到,每次在 Go 中调用函数时,Go 都会复制每个参数值以便使用。

如果你需要更新方法中的接收方变量,也会执行相同的行为。 例如,假设你要创建一个新方法以使三角形的大小增加一倍。 你需要在接收方变量中使用指针,具体如下所示:

func (t *triangle) doubleSize() {t.size *= 2}

如果方法仅可访问接收方的信息,则不需要在接收方变量中使用指针。 但是,依据 Go 的约定,如果结构的任何方法具有指针接收方,则此结构的所有方法都必须具有指针接收方,即使某个方法不需要也是如此。

声明其他类型的方法

方法的一个关键方面在于,需要为任何类型定义方法,而不只是针对自定义类型(如结构)进行定义。 但是,你不能通过属于其他包的类型来定义结构。 因此,不能在基本类型(如 string)上创建方法。

尽管如此,你仍然可以利用一点技巧,基于基本类型创建自定义类型,然后将其用作基本类型。 例如,假设你要创建一个方法,以将字符串从小写字母转换为大写字母。 你可以按如下所示写入方法:

package mainimport ("fmt""strings")type upperstring stringfunc (s upperstring) Upper() string {return strings.ToUpper(string(s))}func main() {s := upperstring("Learning Go!")fmt.Println(s)fmt.Println(s.Upper())}

嵌入方法

在之前的模块中,您已了解到可以在一个结构中使用属性,并将同一属性嵌入另一个结构中。 也就是说,可以重用来自一个结构的属性,以避免出现重复并保持代码库的一致性。 类似的观点也适用于方法。 即使接收方不同,也可以调用已嵌入结构的方法。

例如,假设你想要创建一个带有逻辑的新三角形结构,以加入颜色。 此外,你还希望继续使用之前声明的三角形结构。 然后,你可以初始化 coloredTriangle 结构,并从 triangle 结构调用 perimeter() 方法(甚至访问其字段)

package mainimport "fmt"type triangle struct {size int}type coloredTriangle struct {trianglecolor string}func (t triangle) perimeter() int {return t.size * 3}func main() {t := coloredTriangle{triangle{3}, "blue"}fmt.Println("Size:", t.size)fmt.Println("Perimeter", t.perimeter())}
Size: 3Perimeter 9

重载方法

让我们回到之前讨论过的 triangle 示例。 如果要在 coloredTriangle 结构中更改 perimeter() 方法的实现,会发生什么情况? 不能存在两个同名的函数。 但是,因为方法需要额外参数(接收方),所以,你可以使用一个同名的方法,只要此方法专门用于要使用的接收方即可。 这就是重载方法的方式。

如果你仍需要从 triangle 结构调用 perimeter() 方法,则可通过对其进行显示访问来执行此操作

package mainimport "fmt"type triangle struct {size int}type coloredTriangle struct {trianglecolor string}func (t coloredTriangle) perimeter() int {return t.size * 3 * 2}func (t triangle) perimeter() int {return t.size * 3}func main() {t := coloredTriangle{triangle{3}, "blue"}fmt.Println("Size:", t.size)fmt.Println("Perimeter (colored)", t.perimeter())fmt.Println("Perimeter (normal)", t.triangle.perimeter())}

方法中的封装

“封装”表示对象的发送方(客户端)无法访问某个方法。 通常,在其他编程语言中,你会将 privatepublic 关键字放在方法名称之前。 在 Go 中,只需使用大写标识符,即可公开方法,使用非大写的标识符将方法设为私有方法

Go 中的封装仅在程序包之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。

package geometrytype Triangle struct {size int}func (t *Triangle) doubleSize() {t.size *= 2}func (t *Triangle) SetSize(size int) {t.size = size}func (t *Triangle) Perimeter() int {t.doubleSize()return t.size * 3}
package mainimport ("fmt""geometry")func main() {t := geometry.Triangle{}t.SetSize(3)fmt.Println("Perimeter", t.Perimeter())}

在 Go 中使用接口

Go 中的接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。 在你使用接口时,你的基本代码将变得更加灵活、适应性更强,因为你编写的代码未绑定到特定的实现。 因此,你可以快速扩展程序的功能。

与其他编程语言中的接口不同,Go 中的接口是满足隐式实现的。 Go 并不提供用于实现接口的关键字,因此,如果你之前使用的是其他编程语言中的接口,但不熟悉 Go,那么此概念可能会造成混淆。

声明接口

Go 中的接口是一种抽象类型,只包括具体类型必须拥有或实现的方法。 正因如此,我们说接口类似于蓝图。

假设你希望在几何包中创建一个接口来指示形状必须实现的方法。 你可以按如下所示定义接口:

type Shape interface {    Perimeter() float64    Area() float64}

Shape 接口表示你想要考虑 Shape 的任何类型都需要同时具有 Perimeter()Area() 方法。 例如,在创建 Square 结构时,它必须实现两种方法,而不是仅实现一种。 另外,请注意接口不包含这些方法的实现细节(例如,用于计算某个形状的周长和面积)。 接口仅表示一种协定。 三角形、圆圈和正方形等形状有不同的计算面积和周长方式。

实现接口

正如上文所讨论的内容,你没有用于实现接口的关键字。 当 Go 中的接口具有接口所需的所有方法时,则满足按类型的隐式实现。

让我们创建一个 Square 结构,此结构具有 Shape 接口中的两个方法

type Square struct {size float64}func (s Square) Area() float64 {return s.size * s.size}func (s Square) Perimeter() float64 {return s.size * 4}

请注意 Square 结构的方法签名与 Shape 接口的签名的匹配方式。

func main() {var s Shape = Square{3}fmt.Printf("%T\n", s)fmt.Println("Area: ", s.Area())fmt.Println("Perimeter:", s.Perimeter())}
main.SquareArea:  9Perimeter: 12

此时,无论你是否使用接口,都没有任何区别。 接下来,让我们创建另一种类型,如 Circle,然后进行相同的操作:

package mainimport ("fmt""math")type Shape interface {Perimeter() float64Area() float64}type Square struct {size float64}func (s Square) Area() float64 {return s.size * s.size}func (s Square) Perimeter() float64 {return s.size * 4}type Circle struct {radius float64}func (c Circle) Area() float64 {return math.Pi * c.radius * c.radius}func (c Circle) Perimeter() float64 {return 2 * math.Pi * c.radius}func printInformation(s Shape) {fmt.Printf("%T\n", s)fmt.Println("Area: ", s.Area())fmt.Println("Perimeter:", s.Perimeter())fmt.Println()}func main() {var s Shape = Square{3}printInformation(s)c := Circle{6}printInformation(c)}
main.SquareArea:  9Perimeter: 12main.CircleArea:  113.09733552923255Perimeter: 37.69911184307752

使用接口的优点在于,对于 Shape的每个新类型或实现,printInformation 函数都不需要更改。 正如之前所述,当你使用接口时,代码会变得更灵活、更容易扩展。

扩展现有实现

假设你具有以下代码,并且希望通过编写负责处理某些数据的 Writer 方法的自定义实现来扩展其功能。

通过使用以下代码,你可以创建一个程序,此程序使用 GitHub API 从 Microsoft 获取三个存储库:

package mainimport ("fmt""io""net/http""os")func main() {resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")if err != nil {fmt.Println("Error:", err)os.Exit(1)}io.Copy(os.Stdout, resp.Body)}

改写后:

package mainimport ("encoding/json""fmt""io""net/http""os")type GitHubResponse []struct {FullName string `json:"full_name"`}type customWriter struct{}func (w customWriter) Write(p []byte) (n int, err error) {var resp GitHubResponsejson.Unmarshal(p, &resp)for _, r := range resp {fmt.Println(r.FullName)}return len(p), nil}func main() {resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")if err != nil {fmt.Println("Error:", err)os.Exit(1)}writer := customWriter{}io.Copy(writer, resp.Body)}

编写自定义服务器 API

最后,我们一起来探讨接口的另一种用例,如果你要创建服务器 API,你可能会发现此用例非常实用。 编写 Web 服务器的常用方式是使用 net/http 程序包中的 http.Handler 接口

package mainimport ("fmt""log""net/http")// 创建 float32 类型的自定义类型,然后编写 String() 方法的自定义实现type dollars float32func (d dollars) String() string {return fmt.Sprintf("$%.2f", d)}// 写入 http.Handler 可使用的 ServeHTTP 方法的实现。type database map[string]dollars// 通过使用 database 类型作为接收方来写入 ServeHTTP 方法。func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {for item, price := range db {fmt.Fprintf(w, "%s: %s\n", item, price)}}// 在 main() 函数中,我们将 database 类型实例化,并使用一些值对其进行初始化。 我们使用 http.ListenAndServe 函数启动了 HTTP 服务器,在其中定义了服务器地址,包括要使用的端口和实现 ServerHTTP 方法自定义版本的 db 对象。func main() {db := database{"Go T-Shirt": 25, "Go Jacket": 55}log.Fatal(http.ListenAndServe("localhost:8000", db))}

练习 - 方法和接口

创建用于管理在线商店的程序包

编写一个程序,此程序使用自定义程序包来管理在线商店的帐户。 你的挑战包括以下四个要素:

  1. 创建一个名为 Account 的自定义类型,此类型包含帐户所有者的名字和姓氏。 此类型还必须加入 ChangeName 的功能。
  2. 创建另一个名为 Employee 的自定义类型,此类型包含用于将贷方数额存储为类型 float64 并嵌入 Account 对象的变量。 类型还必须包含 AddCreditsRemoveCreditsCheckCredits 的功能。 你需要展示你可以通过 Employee 对象更改帐户名称。
  3. 将字符串方法写入 Account 对象,以便按包含名字和姓氏的格式打印 Employee 名称。
  4. 最后,编写使用已创建程序包的程序,并测试此挑战中列出的所有功能。 也就是说,主程序应更改名称、打印名称、添加贷方、删除贷方以及检查余额。
package mainimport ("errors""fmt")type Account struct {firstname stringlastname  string}func (a *Account) ChangeName(afterfirstname string) {a.firstname = afterfirstname}type Employee struct {Accountcredit float64}func (e Employee) String() string {return fmt.Sprintf("Firstname:%s,Lastname:%s,Credit:%.2f\n", e.firstname, e.lastname, e.credit)}func CreateEmployee(firstname, lastname string, credit float64) (*Employee, error) {return &Employee{Account{firstname, lastname}, credit}, nil}func (e *Employee) AddCredits(amount float64) (float64, error) {if amount > 0.0 {e.credit += amountreturn e.credit, nil}return 0.0, errors.New("invalid amount")}func (e *Employee) RemoveCredits(amount float64) (float64, error) {if e.credit-amount < 0 {return 0.0, errors.New("too much")}if amount < 0 {return 0.0, errors.New("invalid amount")}e.credit -= amountreturn e.credit, nil}func (e Employee) CheckCredits() float64 {return e.credit}func main() {bruce, _ := CreateEmployee("Bruce", "Lee", 500)fmt.Println(bruce.CheckCredits())credits, err := bruce.AddCredits(250)if err != nil {fmt.Println("Error:", err)} else {fmt.Println("New Credits Balance = ", credits)}_, err = bruce.RemoveCredits(2500)if err != nil {fmt.Println("Can't withdraw or overdrawn!", err)}bruce.ChangeName("Mark")fmt.Println(bruce)}

goroutine(轻量线程)

并发是独立活动的组合,就像 Web 服务器虽然同时处理多个用户请求,但它是自主运行的。 并发在当今的许多程序中都存在。 Web 服务器就是一个例子,但你也能看到,在批量处理大量数据时也需要使用并发。

Go 有两种编写并发程序的样式。 一种是在其他语言中通过线程实现的传统样式。

Go 实现并发的方法

通常,编写并发程序时最大的问题是在进程之间共享数据。 Go 采用不同于其他编程语言的通信方式,因为 Go 是通过 channel 来回传递数据的。 这意味着只有一个活动 (goroutine) 有权访问数据,设计上不存在争用条件。 学完本模块中的 goroutine 和 channel 之后,你将更好地理解 Go 的并发方法。

可以使用下面的标语来概括 Go 的方法:“不是通过共享内存通信,而是通过通信共享内存。”

Goroutine

goroutine 是轻量线程中的并发活动,而不是在操作系统中进行的传统活动。 假设你有一个写入输出的程序和另一个计算两个数字相加的函数。 一个并发程序可以有数个 goroutine 同时调用这两个函数。

我们可以说,程序执行的第一个 goroutine 是 main() 函数。 如果要创建其他 goroutine,则必须在调用该函数之前使用 go 关键字

func main(){    login()    go launch()}

许多程序喜欢使用匿名函数来创建 goroutine

func main(){    login()    go func() {        launch()    }()}

编写并发程序

由于我们只想将重点放在并发部分,因此我们使用现有程序来检查 API 终结点是否响应。

串行程序:

package mainimport ("fmt""net/http""time")func main() {start := time.Now()apis := []string{"https://management.azure.com","https://dev.azure.com","https://api.github.com","https://outlook.office.com/","https://api.somewhereintheinternet.com/","https://graph.microsoft.com",}for _, api := range apis {_, err := http.Get(api)if err != nil {fmt.Printf("ERROR: %s is down!\n", api)continue}fmt.Printf("SUCCESS: %s is up and running!\n", api)}elapsed := time.Since(start)fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())}
SUCCESS: https://management.azure.com is up and running!SUCCESS: https://dev.azure.com is up and running!SUCCESS: https://api.github.com is up and running!SUCCESS: https://outlook.office.com/ is up and running!ERROR: https://api.somewhereintheinternet.com/ is down!SUCCESS: https://graph.microsoft.com is up and running!Done! It took 5.163787068 seconds!

同时检查所有站点?我们需要并发运行的代码部分是向站点进行 HTTP 调用的部分。 换句话说,我们需要为程序要检查的每个 API 创建一个 goroutine。为了创建 goroutine,我们需要在调用函数前使用 go 关键字。

首先创建一个新函数:

func checkAPI(api string) {    _, err := http.Get(api)    if err != nil {        fmt.Printf("ERROR: %s is down!\n", api)        return    }    fmt.Printf("SUCCESS: %s is up and running!\n", api)}

修改 main() 函数中的代码,为每个 API 创建一个 goroutine

for _, api := range apis {go checkAPI(api)}
Done! It took 3.42e-05 seconds!

即使看起来 checkAPI 函数没有运行,它实际上是在运行。 它只是没有时间完成。

添加 time.Sleep(3 * time.Second)

ERROR: https://api.somewhereintheinternet.com/ is down!SUCCESS: https://api.github.com is up and running!SUCCESS: https://management.azure.com is up and running!SUCCESS: https://dev.azure.com is up and running!SUCCESS: https://outlook.office.com/ is up and running!SUCCESS: https://graph.microsoft.com is up and running!Done! It took 3.001536063 seconds!

将 channel 用作通信机制

Go 中的 channel 是 goroutine 之间的通信机制。 这就是为什么我们之前说过 Go 实现并发的方式是:“不是通过共享内存通信,而是通过通信共享内存。”需要将值从一个 goroutine 发送到另一个时,可以使用通道。

Channel 语法

由于 channel 是发送和接收数据的通信机制,因此它也有类型之分。 这意味着你只能发送 channel 支持的数据类型。 除使用关键字 chan 作为 channel 的数据类型外,还需指定将通过 channel 传递的数据类型,如 int 类型。

每次声明一个 channel 或希望在函数中指定一个 channel 作为参数时,都需要使用 chan <type>,如 chan int。 要创建 channel,需使用内置的 make() 函数,如下所示:

ch := make(chan int)

一个 channel 可以执行两项操作:发送数据和接收数据。 若要指定 channel 具有的操作类型,需要使用 channel 运算符 <-。 此外,在 channel 中发送数据和接收数据属于阻止操作。

如果希望 channel 仅发送数据,则必须在 channel 之后使用 <- 运算符。 如果希望 channel 接收数据,则必须在 channel 之前使用 <- 运算符

ch <- x // sends (or write) x through channel chx = <-ch // x receives (or reads) data sent to the channel ch<-ch // receives data, but the result is discarded

可在 channel 中执行的另一项操作是关闭 channel

close(ch)

关闭 channel 时,你希望数据将不再在该 channel 中发送。 如果试图将数据发送到已关闭的 channel,则程序将发生严重错误。 如果试图通过已关闭的 channel 接收数据,则可以读取发送的所有数据。 随后的每次“读取”都将返回一个零值。

使用 channel 来删除睡眠功能并稍做清理:

package mainimport ("fmt""net/http""time")// 通过 channel 发送该消息,而不是在 checkAPI 函数中打印结果func checkAPI(api string, ch chan string) {_, err := http.Get(api)if err != nil {ch <- fmt.Sprintf("ERROR: %s is down!\n", api)return}ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)}func main() {// 创建channelch := make(chan string)start := time.Now()apis := []string{"https://management.azure.com","https://dev.azure.com","https://api.github.com","https://outlook.office.com/","https://api.somewhereintheinternet.com/","https://graph.microsoft.com",}for _, api := range apis {go checkAPI(api, ch)}fmt.Print(<-ch)elapsed := time.Since(start)fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())}
ERROR: https://api.somewhereintheinternet.com/ is down!Done! It took 0.088759104 seconds!

但是事实上并没有实现功能

无缓冲 channel

使用 make() 函数创建 channel 时,会创建一个无缓冲 channel,这是默认行为。 无缓冲 channel 会阻止发送操作,直到有人准备好接收数据。 这就是为什么我们之前说发送和接收都属于阻止操作。 这也是上面的程序在收到第一条消息后立即停止的原因。

我们可以说 fmt.Print(<-ch) 会阻止程序,因为它从 channel 读取,并等待一些数据到达。 一旦有任何数据到达,它就会继续下一行,然后程序完成。

其他 goroutine 发生了什么? 它们仍在运行,但都没有在侦听。 而且,由于程序提前完成,一些 goroutine 无法发送数据。

读取数据和接收数据都属于阻止操作

要解决此问题,只需更改循环的代码,然后只接收确定要发送的数据

package mainimport ("fmt""net/http""time")// 通过 channel 发送该消息,而不是在 checkAPI 函数中打印结果func checkAPI(api string, ch chan string) {_, err := http.Get(api)if err != nil {ch <- fmt.Sprintf("ERROR: %s is down!\n", api)return}ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)}func main() {// 创建channelch := make(chan string)start := time.Now()apis := []string{"https://management.azure.com","https://dev.azure.com","https://api.github.com","https://outlook.office.com/","https://api.somewhereintheinternet.com/","https://graph.microsoft.com",}for _, api := range apis {go checkAPI(api, ch)}for i := 0; i < len(apis); i++ {fmt.Print(<-ch)}elapsed := time.Since(start)fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())}
ERROR: https://api.somewhereintheinternet.com/ is down!SUCCESS: https://api.github.com is up and running!SUCCESS: https://management.azure.com is up and running!SUCCESS: https://graph.microsoft.com is up and running!SUCCESS: https://dev.azure.com is up and running!SUCCESS: https://outlook.office.com/ is up and running!Done! It took 1.029620196 seconds!

无缓冲 channel 在同步发送和接收操作。 即使使用并发,通信也是同步的。

有缓冲 channel

默认情况下 channel 是无缓冲行为。 这意味着只有存在接收操作时,它们才接受发送操作。 否则,程序将永久被阻止等待。

有时需要在 goroutine 之间进行此类同步。 但是,有时你可能只需要实现并发,而不需要限制 goroutine 之间的通信方式。

有缓冲 channel 在不阻止程序的情况下发送和接收数据,因为有缓冲 channel 的行为类似于队列。 创建 channel 时,可以限制此队列的大小

package mainimport ("fmt")func send(ch chan string, message string) {ch <- message}func main() {size := 4ch := make(chan string, size)send(ch, "one")send(ch, "two")send(ch, "three")send(ch, "four")fmt.Println("All data sent to the channel ...")for i := 0; i < size; i++ {fmt.Println(<-ch)}fmt.Println("Done!")}
All data sent to the channel ...onetwothreefourDone!

channel 与 goroutine 有着紧密的联系。 如果没有另一个 goroutine 从 channel 接收数据,则整个程序可能会永久处于被阻止状态。

func main() {    size := 2    ch := make(chan string, size)    send(ch, "one")    send(ch, "two")    go send(ch, "three")    go send(ch, "four")    fmt.Println("All data sent to the channel ...")    for i := 0; i < 4; i++ {        fmt.Println(<-ch)    }    fmt.Println("Done!")}

无缓冲 channel 与有缓冲 channel

现在,你可能想知道何时使用这两种类型。 这完全取决于你希望 goroutine 之间的通信如何进行。 无缓冲 channel 同步通信。 它们保证每次发送数据时,程序都会被阻止,直到有人从 channel 中读取数据。

相反,有缓冲 channel 将发送和接收操作解耦。 它们不会阻止程序,但你必须小心使用,因为可能最终会导致死锁(如前文所述)。 使用无缓冲 channel 时,可以控制可并发运行的 goroutine 的数量。 例如,你可能要对 API 进行调用,并且想要控制每秒执行的调用次数。 否则,你可能会被阻止。

Channel 方向

Go 中 channel 的一个有趣特性是,在使用 channel 作为函数的参数时,可以指定 channel 是要发送数据还是接收数据。 随着程序的增长,可能会使用大量的函数,这时候,最好记录每个 channel 的意图,以便正确使用它们。 或者,你要编写一个库,并希望将 channel 公开为只读,以保持数据一致性。

要定义 channel 的方向,可以使用与读取或接收数据时类似的方式进行定义。 但是你在函数参数中声明 channel 时执行此操作。 将 channel 类型定义为函数中的参数的语法如下所示:

chan<- int // it's a channel to only send data<-chan int // it's a channel to only receive data

通过仅接收的 channel 发送数据时,在编译程序时会出现错误。

让我们使用以下程序作为两个函数的示例,一个函数用于读取数据,另一个函数用于发送数据:

package mainimport "fmt"func send(ch chan<- string, message string) {    fmt.Printf("Sending: %#v\n", message)    ch <- message}func read(ch <-chan string) {    fmt.Printf("Receiving: %#v\n", <-ch)}func main() {    ch := make(chan string, 1)    send(ch, "Hello World!")    read(ch)}

运行程序时,将看到以下输出:

Sending: "Hello World!"Receiving: "Hello World!"

程序阐明每个函数中每个 channel 的意图。 如果试图使用一个 channel 在一个仅用于接收数据的 channel 中发送数据,将会出现编译错误。 例如,尝试执行如下所示的操作:

func read(ch <-chan string) {    fmt.Printf("Receiving: %#v\n", <-ch)    ch <- "Bye!"}

运行程序时,将看到以下错误:

# command-line-arguments./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)

编译错误总比误用 channel 好。

多路复用

最后,让我们讨论一个关于如何在使用 select 关键字的同时与多个 channel 交互的简短主题。 有时,在使用多个 channel 时,需要等待事件发生。 例如,当程序正在处理的数据中出现异常时,可以包含一些逻辑来取消操作。

select 语句的工作方式类似于 switch 语句,但它适用于 channel。 它会阻止程序的执行,直到它收到要处理的事件。 如果它收到多个事件,则会随机选择一个。

select 语句的一个重要方面是,它在处理事件后完成执行。 如果要等待更多事件发生,则可能需要使用循环。

package mainimport ("fmt""time")func process(ch chan string) {time.Sleep(3 * time.Second)ch <- "Done processing!"}func replicate(ch chan string) {time.Sleep(1 * time.Second)ch <- "Done replicating!"}func main() {ch1 := make(chan string)ch2 := make(chan string)go process(ch1)go replicate(ch2)for i := 0; i < 2; i++ {select {case process := <-ch1:fmt.Println(process)case replicate := <-ch2:fmt.Println(replicate)}}}
Done replicating!Done processing!

请注意,replicate 函数先完成。 这就是你在终端中先看到其输出的原因。 main 函数存在一个循环,因为 select 语句在收到事件后立即结束,但我们仍在等待 process 函数完成。

练习 - 利用并发方法更快地计算斐波纳契数

实现并发的改进版本。 完成此操作需要几秒钟的时间(不超过 15 秒),应使用有缓冲 channel。

package mainimport ("fmt""math/rand""time")func fib(number float64, ch chan string) {x, y := 1.0, 1.0for i := 0; i < int(number); i++ {x, y = y, x+y}r := rand.Intn(3)time.Sleep(time.Duration(r) * time.Second)ch <- fmt.Sprintf("Fib(%v): %v\n", number, x)}func main() {ch := make(chan string, 15)start := time.Now()for i := 1; i < 15; i++ {go fib(float64(i), ch)}for i := 1; i < 15; i++ {fmt.Printf(<-ch)}elapsed := time.Since(start)fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())}

编写一个新版本以计算斐波纳契数,直到用户使用 fmt.Scanf() 函数在终端中输入 quit。 如果用户按 Enter,则应计算新的斐波纳契数。

使用两个无缓冲 channel:一个用于计算斐波纳契数,另一个用于等待用户的“退出”消息。 你需要使用 select 语句。

package mainimport ("fmt""time")var quit = make(chan bool)func fib(c chan int) {x, y := 1, 1for {select {case c <- x:x, y = y, x+ycase <-quit:fmt.Println("Done calculating Fibonacci!")return}}}func main() {start := time.Now()command := ""data := make(chan int)go fib(data)for {num := <-datafmt.Println(num)fmt.Scanf("%s", &command)if command == "quit" {quit <- truebreak}}time.Sleep(1 * time.Second)elapsed := time.Since(start)fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())}
]]>
+ + + + + Study + + + + + + + Go + + Backend + + + +
+ + + + + 研究生课程:现代信息检索-第12讲 支持向量机和排序学习 + + /2022/10/02/UCAS/information-retrieval/information-retrieval-12/ + + 《现代信息检索》课程笔记:第12讲 支持向量机和排序学习

第12讲 支持向量机和排序学习

支持向量机

线性可分情况下,不仅要区分开,而且要使得区分间隔最大

最优超平面)是使得两类的分类间隔(Margin)最大的超平面,即每类中离超平面最近的样本到超平面的距离最大。距离这个最优超平面最近的样本被称为支持向量。

求解最优超平面就相当于,在上述约束条件下,求2/||W||的最大值 ,即以下损失函数最小值

二次优化问题可以采用Lagrange方法求解

非线性可分情况下的处理

广义最优分类面方法:在线性不可分的情况下,就是某些训练样本不能满足约束条件,因此可以在条件中增加一个松弛项ζ(发音Zeta,也称
引入Soft Margin,软边界),变换约束条件。

变换到高维空间的支持向量机

  • SVM训练相对较慢,分类速度一般。但是分类效果较好。
  • 在面对非线性可分情况时,可以引入松弛变量进行处理或者通过空间变换到另一个线性可分空间进行处理。
  • SVM有很多实现工具,SMO/SVM light/SVM torch/LibSVM等等

为什么要使间隔最大化?

  • 分界面附近的点代表了不确定的分类决策,分类器会以两边各50%的概率做出决策
  • 具有很大分类间隔的分类器不会做出确定性很低的决策,它给出了一个分类的安全间隔
  • 度量中的微小错误和文档中的轻微变化不会导致错误分类
  • SVM 分类器:在决策面周围有大的间隔
  • 与放置(无穷的)决策超平面相比,如果要在类别间放置一个宽间隔,那么选择会少很多
  • 减少记忆容量、增加测试文档分类泛化能力

SVM用于支持多类问题:结构化SVM

排序学习

基于布尔权重的学习

  • 词项权重(如tfidf)的目标是为了度量词项的重要性
    • 将一篇文档中所有词项的权重加起来便可以计算文档和查询的相关度,基于该相关度可以对所有文档排序
  • 上述过程可以想象成一个文本分类问题
    • 词项权重可以从已判定的训练集合中学习得到
  • 上述研究方法被归入一类称为机器学习的相关度或排序学习

权重学习主要方法:

给定训练样例集合,每个样例表示为三元组<q, d, R(d,q)>

从上述样例中学习权重,使得学到的评分接近训练集中的相关性判定结果。

基于实数权重的学习

评分函数是两个因子的线性组合:

  • 查询和文档的向量空间相似度评分
  • 查询词项在文档中存在的最小窗口宽度

我们的一个因子取决于查询词项在文档中的词袋统计量,另一个因子取决于邻近度权重

基于机器学习的检索结果排序

基于序回归的排序学习

将IR排序问题看成序回归

对于同一查询,文档之间可以按照相对得分排序即可,并不一定要求每篇文档有一个全局的绝对得分。因此,只需要一个排序,而不要得到相关度的绝对得分,问题空间可以减小。

排序SVM的构建

  • 给定一些已经判定的查询
  • 对训练集中的每条查询q, 我们都有针对该查询的一系列文档集合,这些文档已经由人工按照其与查询的相关度排序
  • 对每个文档、查询对,构造特征向量 ψj = ψ(dj , q),这里的特征可以采用前面讨论的特征
  • 对于两篇文档di 和dj ,可以计算特征向量之间的差异向量

排序学习总结

排序学习算法现在一般分为以下三类

  • Pointwise (即本讲介绍的权重学习方法):每个文档是一个训练样本,预测文档相关/不相关
  • Pairwise (即本讲介绍的序回归方法):文档对构成一个训练样本,预测一个文档相关性是否高于另一个文档
  • Listwise(基于列表的排序学习,未介绍):一个文档排序列表构成一个训练样本,预测最优排序

虽然近年来基于深度学习和大规模预训练语言模型的方法已成功应用于IR,排序学习仍然是一种整合不同文本特征的有效方法。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:机器学习-第7章 降维与特征选择 + + /2022/09/30/UCAS/machine-learning/machine-learning-7/ + + 《机器学习》课程笔记:第7章 降维与特征选择

第7章 降维与特征选择

概述

机器学习算法的有效性和计算复杂度是敏感于数据的特征表达和维度。

特征降维的意义:

数据压缩:简化数据表示,加快数据通信传输、节省存储资源、…

学习算法效率:

  • 计算上,简化计算,加快速度
  • 性能上,提升精确度
  • 可理解性,发现数据的潜在本质特征

特征选择:从D个特征中选择d个,来表达模式

特征提取:采用特征变换的方法,生成d个新的特征

特征选择

特征选择框架

特征选择问题:从D维特征中选择d维(d<D)特征子集

  • 使数据的压缩率高
  • 使学习机预测性能最佳
  • 使学习机学习速度加快

特征选择的处理过程:

xFNOw8.md.png

特征子集生成

特征子集生成问题:D维特征中,选择d维(d<D)特征子集,子集个数为

  1. 穷举(最优子集搜索):计算特征的所有可能组合,并逐一评价。
  2. 单独最优特征组合:对每个特征分别评估,找前d个单独最优特征。优点:算法简单,缺点:没有考虑特征之间的关系,存在特征冗余
  3. SFS(Sequential forward selection, 前向序贯):每次加入一个特征,该特征使得新的特征组合最优。
  4. GSFS (广义SFS):每次加入k个特征,使加入特征后的组合最优。
  5. SBS(Sequential backward selection, 后向序贯):每次减掉一个特征,使剩余特征组合最优。
  6. GSBS(广义SBS):每次减k个特征,使剩余特征组合最优。
  7. L-R 法(增加L个,减R个)每次增加L个再减R个(L > R),或减R个增加L个(L < R)
  8. 广义的L-R(ZL , ZR):增L和减R分Z步进行

特征评价准则

  1. 可分性度量:在选择的特征集下,采用类别可分性的程度,评价特征选择的好与坏。常用于Filter框架下。
  2. 学习算法精度的度量:在选择的特征集下,通过学习算法的精确度,评价特征选择的好与坏。常用于wrapper框架下。

基于距离的可分性判据:

通常依赖于类内类间的距离度量,前提是数据具有类别标签。可分性评估是在选择的特征子集维度上计算数据统计量。

距离的可分性判据的特点:

  • 容易理解和实现
  • 与错误率无直接关系,不敏感于数据交叠情况
  • 常用于Filter特征选择框架下

基于概率分布的可分性判据:从类别概率密度的角度,讨论两个类别的交叠程度

常见的概率距离准则:

熵可分性判据:

特征选择方法

Filter 方法:

不依赖于学习算法(如分类器)的结果,直接由数据构建评估函数,对选择的特征子集进行评估。

通常方法:根据特征评价准则进行评估,选择最优的特征子集。

评价准则:距离准则、概率可分、熵可分准则。

优点:计算复杂度低,效率高。

缺点:选择的特征之间存在冗余信息。

Wrapper 方法:

原理:通过学习算法(如分类器),对选择的特征子集进行评估。

优点:选择的特征可以支持学习算法。

缺点:算法的计算复杂度高。

Embedded 方法:

原理:特征选择过程在学习算法中完成,目标是完成学习过程。

特点:不是专门的特征选择过程

缺点:计算复杂度高。

特征提取

优点:

  • 数据更紧致的压缩
  • 优化预测性能
  • 加快学习速度

不同的应用问题会有不同的特征提取研究问题

线性变换

特征提取目标:学习变换矩阵

给定 , 通过某种降维准则, 学习变换矩阵

两种降维表示途径:

  • 投影:
  • 矩阵分解:低秩表示:

主成分分析PCA

目标函数:均方误差最小原则(求最优重构子空间)

s.t.

最小误差等价于最大投影

求解目标函数:

特征值的意义:样本在w方向的投影平均值(或和)最大

PCA算法流程:

  1. 标准化样本
  2. 求样本的协方差矩阵特征值,并降排序对应非零特征向量
  3. 变换矩阵
  4. 降维表示

线性鉴别分析LDA

PCA能保证类别区分的有效性,LDA特征的优点:类内最小、类间最大。

特征方向的提取:

非线性变换

核主成分分析KPCA

  1. 求核矩阵的特征值,对应特征向量的问题:
  2. 核矩阵的特征值降序,前个特征值对应特征向量
  3. 高维空间中的投影方向$w_i=\Phi \boldsymbol{\alpha}_i \boldsymbol{\Lambda}=\left(\boldsymbol{\alpha}_1, \boldsymbol{\alpha}_2, \ldots, \boldsymbol{\alpha}_d\right)W=\Phi \boldsymbol{\Lambda}$
  4. 降维表示
    1. 训练集低维表示:
    2. 新样本的低维表示:
    3. 其中

局部线性变换LLE

LLE方法是一种流形学习,保持样本间的局部线性关系,整体实现非线性映射。

非负矩阵分解

基本思想:通过矩阵分解,进行数据降维;分解后的矩阵为非负矩阵

不同的目标函数情况:

  1. 范数误差最小
  2. KL误差
]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:现代信息检索-第11讲 文本分类 + + /2022/09/30/UCAS/information-retrieval/information-retrieval-11/ + + 《现代信息检索》课程笔记:第11讲 文本分类

第11讲 文本分类

常设查询(Standing Queries)

从检索到文本分类:假设某用户有一个经常关注的信息需求,用户会经常输入这个查询来寻找关于这个主题的新内容,关注于浏览新内容,此时排序问题变成了一个分类问题(相关 vs. 不相关)

需要构建分类函数

人工分类

专家分类一般都是准确的

当数据规模不大、标注者人数较少时,分类一致

当数据规模变大,人工分类困难且代价昂贵

人工编写的基于规则的分类器

新闻机构,情报机构等使用的一个技术,广泛部署于政府和企业

供应商提供“ IDE”来编写此类规则,商业系统具有复杂的查询语言

如果领域专家花时间精心完善规则,则准确性会很高,但是建立和维护这些规则非常昂贵

有监督学习

监督学习分类器可以使用各种特征

词袋模型

仅使用词项特征,使用文本中的所有词项

特征选择的意义

  • 文本语料具有大量的词项/特征
  • 特征选择可以使得某些分类器可用
  • 减少训练时间
  • 使运行时模型更小,更快
  • 可以提高模型泛化能力

最简单的特征选择方法:

  • 仅使用最常见词项
  • 没有特别的(理论)依据
  • 但是很好理解:
    • 这些词的概率可以被很好地估计(因为词频高),并且最常被用作相关性的证据
    • 在实际应用中,词频特征选择往往能达到一些更高的方法的90%的性能

更聪明的特征选择方法:卡方(chi-square)等

朴素贝叶斯分类器

朴素贝叶斯分类的目标是寻找具有最大后验概率的类别

对数计算:通过取对数将原来的乘积计算变成求和计算

参数估计:极大似然估计

避免零概率:加一平滑

朴素贝叶斯对于训练集的大小和测试文档的大小而言是线性的,在某种意义上是最优的。

  • 相对于其他很多更复杂的学习方法,朴素贝叶斯对不相关特征更具鲁棒性
  • 相对于其他很多更复杂的学习方法,朴素贝叶斯对概念漂移更鲁棒(概念漂移是指类别的定义随时间变化)
  • 当有很多同等重要的特征时,该方法优于决策树类方法
  • 如果满足独立性假设,那么朴素贝叶斯是最优的
  • 速度非常快、存储开销少

分类结果的评价:评估必须在独立于训练数据的测试数据上完成

评价指标:正确率(Precision),召回率(Recall),F1,分类准确率r/n ,其中 n 是所有测试文档的数量,r是正确分类的测试文档数量

向量空间分类

训练集包含一系列文档,每篇都标记着它的类别

在向量空间分类中,该集合对应着空间中一系列标记的点或向量。

利用Rocchio方法进行向量空间分类

基本思想:计算每个类的中心向量(所有文档向量的算术平均),将每篇测试文档分到离它最近的那个中心向量

Rocchio简单地将每个类别表示成其中心向量,分类基于文档向量到原型的相似度或聚类来进行,并不保证分类结果与训练集一致,即得到分类器后,不能保证训练集中的文档能否正确分类。

很多情况下,Rocchio的效果不如朴素贝叶斯:Rocchio算法不能正确处理非凸、多模式类别问题

kNN分类器

将每篇测试文档分给训练集中离它最近的那篇文档所属的类别。

  • 不需要训练过程,但是文档的线性预处理过程和朴素贝叶斯的训练开销相当。对于训练集来说我们一般都要进行预处理,因此现实当中
    kNN的训练时间是线性的。
  • 当训练集非常大的时候,kNN分类的精度很高
  • 如果训练集很小, kNN可能效果很差。
  • kNN倾向于大类,可以将相似度考虑在内来缓解这个问题。

线性分类器

线性分类器计算特征值的一个线性加权和

很多常用的文本分类器都是线性分类器:朴素贝叶斯、Rocchio、logistic回归、线性SVM等等

不同的方法选择超平面的策略不同,造成了在测试文档分类性能的巨大差异

不能通过更强大的非线性分类器来获得更好的分类性能

不存在某个学习方法对于任何分类问题都最优

kNN高方差低偏差,而朴素贝叶斯分类器低方差高偏差

单标签问题:类别之间互斥,每篇文档属于且仅属于某一个类

多标签分类问题:一篇文档可以属于0、1或更多个类,针对某个类的决策并不影响其他类别上的决策

对于给定的分类问题,要考虑很多因素从而选择合适的分类器算法。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 机器学习算法竞赛实战-自然语言处理 + + /2022/09/28/UCAS/machine-learning/machine-learning-competition-nlp/ + + 机器学习算法竞赛实战-自然语言处理

第15章 自然语言处理

自然语言处理的发展历程

  1. 1950年到1970年:基于经验、规则的阶段
  2. 1970年到2008年:基于统计方法的阶段
  3. 2008年至今:基于深度学习技术的阶段

自然语言处理的常见场景

  1. 分类、回归任务
  2. 信息检索、文本匹配等任务
  3. 序列对序列、序列标注
  4. 机器阅读

自然语言处理的常见技术

  1. 基于词袋模型、TF-IDF的特征提取
  2. N-Gram模型
  3. 词嵌入模型
  4. 上下文相关预训练模型
  5. 常用的深度学习模型结构:TextCNN、BiLSTM+Attention、DPCNN

第16章 实战案例

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Competition + + + +
+ + + + + 机器学习算法竞赛实战-计算广告 + + /2022/09/28/UCAS/machine-learning/machine-learning-competition-advertisement/ + + 机器学习算法竞赛实战-计算广告

第12章 计算广告

什么是计算广告

计算广告是指借助大数据的分析建模,使得广告能够覆盖广泛区域和实现消费者的多跨度精准曝光,让同一份广告尽可能接触到更多有效的流量和更多对广告感兴趣的人,从而用同样低的成本,让广告的效果尽可能更好,使产品和服务获得更多商业上的成功。

主要问题

如何协调广告主、平台和消费者三方之间的利益

计算广告系统架构

在线投放引擎:

  • 广告检索:Web端发来广告请求时,系统根据该广告位的页面标签或者用户标签从广告索引中查找符合条件的广告。
  • 广告排序:当出现多个广告主抢夺一个广告位的情况时,需要对投放各个广告可能会产生的效益分别进行预估,对广告进行排序

分布式计算平台:

  • 行为定向:挖掘广告投放日志中的用户行为属性
  • 点击率建模:在分布式计算平台上训练并得到点击率模型的参数和相应特征,用以辅助广告投放系统进行决策

流式计算平台:

  • 实时受众定向:将最近一段短时间内发生的用户行为和广告投放日志及时地加工成实时用户标签,用以辅助广告检索模块。
  • 实时点击反馈:实时反馈用户行为和广告投放日志的变化,主要生成实时点击率相关特征,用以辅助广告检索模块。

广告类型

合约广告:包括CPT广告和定向广告。CPT广告指的是按照时间成本计算,广告主以固定的价格买断一段时间内的广告位来展示自己的广告;定向广告指的是广告主选择自己要投放的兴趣标签,然后算法为其匹配相应的受众人群并进行广告投放。

竞价广告:采用“价高者得”的方案来决策每次展示哪个广告,使得媒体主可以实时对不同广告进行比价,从而最大化收益。

程序化交易广告:广告主可以实时地在每一次广告展示中选择自己的目标受众,并且参与竞价。

广告召回

根据用户或商品属性以及页面上下文属性从广告索引中检索符合投放条件的候选广告。

广告召回模块

布尔表达式召回:根据广告主设置的定向标签组合成布尔表达式。

向量检索召回:通过传统的Word2Vec方式获取广告的向量表示,然后通过相似度计算对受众人群进行召回;或者通过深度学习模型获取广告的向量表示。

基于TDM(深度树匹配模型)的召回:基于深度学习的大规模推荐系统算法框架。

目前的找回策略大多是多路召回与权重检索相结合。

DSSM语义召回

为用户侧特征和广告侧特征构建不同的塔,在经过多层全连接后,计算相似度并进行广告检索。

广泛应用于搜索、推荐等领域的召回和排序问题中。

广告排序

对广告召回模块送来的广告候选集计算值,并按照所得值的大小倒排序。

点击率预估:向用户投放一个广告,然后预测用户点击广告的概率

特征处理:特征交叉组合、连续值特征的处理、点击率平滑、向量化表示

常见模型:

  • FM:隐向量学习提升模型表达
  • Wide&Deep:记忆性与泛化性的信息互补
  • DeepFM:在FM基础上引入神经网络隐式高阶交叉信息
  • DIN:融合Attention机制的深度学习模型

广告竞价

在广告竞拍机制中,广告的实际曝光量取决于广告的流量覆盖大小和在竞争广告中的相对竞争力水平,其中前者取决于广告的人群定向(匹配对应特征的用户数量)、广告素材尺寸(匹配的广告位)以及投放时段、预算等设置项;影响后者的因素主要有出价、广告质量、以及对用户体验的控制策略等。

第13章 实战案例

第14章 实战案例

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Competition + + + +
+ + + + + 研究生课程:高级人工智能-第5讲 序列数据的深度学习模型 + + /2022/09/28/UCAS/advanced-ai/advanced-ai-5/ + + 《高级人工智能》课程笔记:第5讲 序列数据的深度学习模型

第5讲 序列数据的深度学习模型

循环神经网络

序列数据建模:

  • 学习序列数据,常需要转换输入序列到不同领域的输出序列
  • 如果没有分离的目标序列,可以通过预测输入序列中的下一项来得到“教师信号”
  • 预测序列的下一项,模糊了监督学习与非监督学习的差别

为什么不使用标准的神经网络?

  • 输入和输出数据在不同例子中可以有不同的长度
  • 不共享从文本的不同位置上学到的特征

RNN的特点:

  • 隐藏状态可以高效存储过去的很多信息
  • 非线性的状态转移可以允许通过很复杂的方式更新他们的隐藏状态

一般来说,RNN每一时间的输入和输出是不一样的

序列学习:对于序列数据是将序列项依次传入,每个序列项再对应不同的输出

时序展开:在RNN中每一个时间步骤用到的参数都是一样的

RNN可看作权值共享的多层、前向网络,训练权值约束的前向网络

Back Propagation Through Time:前向传播和反向传播

示例:

语言模型

新序列采样

字符级别的语言模型

序列生成

长序列的循环神经网络

训练长序列 (100 time steps) RNN中,梯度很容易膨胀或消散

即使好的初始化,也难以检测当前目标输出对很多步之前的输入的依赖关系

GRU

LSTM:

解决了RNN长期(like hundreds of time steps)记忆的问题

LSTM是一个存储单元,使用logistic和linear单元执行乘法运算

记忆单元:存储RNN的长期信息

LSTM vs GRU

GRU是更加简单的模型,更容易创建一个更大的网络,而且它只有两个门,在计算性上也运行得更快,可以扩大模型的规模。

LSTM更加强大和灵活,有三个门而不是两个。

双向循环神经网络(Bidirectional RNN)

深层循环神经网络(Deep RNNs)

序列模型

机器翻译

图片说明

使用集束搜索(Beam search algorithm)而不使用贪心搜索

改进集束搜索(Refinements to Beam Search),序列长度归一化

注意力模型

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + 研究生课程:现代信息检索-第10讲 相关反馈及查询扩展 + + /2022/09/27/UCAS/information-retrieval/information-retrieval-10/ + + 《现代信息检索》课程笔记:第10讲 相关反馈及查询扩展

第10讲 相关反馈及查询扩展

动机

考虑查询q: [aircraft] . . .

某篇文档 d 包含“plane”, 但是不包含 “aircraft”

显然对于查询q,一个简单的IR系统不会返回文档d,即使d是和q最相关的文档

提高召回率的方法:

局部(local)方法:对用户查询进行局部的即时的分析

全局(Global)方法: 进行一次性的全局分析(比如分析整个文档集)来产生同/近义词词典

关于相关反馈和查询扩展:

相关反馈的本质是将检索返回的文档的相关性判定(不同的判定来源:人工或非人工)作为返回信息,希望提升检索效果(召回率和正确率)。

相关反馈常常用于查询扩展,所以提到相关反馈往往默认为有查询扩展

而查询扩展的最初含义是对查询进行扩充,近年来越来越向查询重构偏移,即现在的查询扩展是指对原有查询进行修改。

  • 基于相关反馈(局部方法的代表)进行查询扩展/重构
  • 基于本讲的全局方法进行查询扩展/重构
  • 局部和全局方法相结合的方法

相关反馈基础

相关反馈的基本思想

  • 用户提交一个(简短的)查询
  • 搜索引擎返回一系列文档
  • 用户或系统将部分返回文档标记为相关的,将部分文档标记为不相关的
  • 搜索引擎根据标记结果计算得到信息需求的一个新查询表示。当然我们希望该表示好于初始的查询表示
  • 搜索引擎对新查询进行处理,返回新结果,会有更高的召回率

显式相关反馈:用户显式参加交互过程

隐式相关反馈:系统跟踪用户的行为来推测返回文档的相关性,从而进行反馈。

伪相关反馈或盲相关反馈:没有用户参与,系统直接假设返回文档的前k篇是相关的,然后进行反馈。

相关反馈详细介绍

相关反馈中的核心概念:矩心

矩心是一系列点的中心

Rocchio算法是向量空间模型中相关反馈的实现方式

相关反馈中的假设:

假设 A1: 对于某初始查询,用户知道在文档集中使用哪些词项来表达

假设A2: 相关文档中出现的词项类似 (因此,可以基于相关反馈,从一篇相关文档跳到另一篇相关文档)

相关反馈的评价:

基于存留文档集(residual collection):用户没有判断的文档集

一轮相关反馈往往非常有用,相对一轮相关反馈,两轮相关反馈效果的提高有限。

用户相关反馈存在的问题:

  • 用户相关反馈开销很大
    • 相关反馈生成的新查询往往很长
    • 长查询的处理开销很大
  • 用户不愿意提供显式的相关反馈
  • 很难理解,为什么会返回(应用相关反馈之后)某篇特定文档
  • Excite搜索引擎曾经提供完整的相关反馈功能,但是后来废弃了这一功能

隐式相关反馈

通过观察用户对当前检索结果采取的行为来给出对检索结果的相关性判定。

判定不一定很准确,但是省却了用户的显式参与过程。

用户行为种类:鼠标键盘动作和用户眼球动作

隐式相关反馈小结:

优点:

  • 不需要用户显式参与,减轻用户负担
  • 用户行为某种程度上反映用户的兴趣,具有可行性

缺点:

  • 对行为分析有较高要求
  • 准确度不一定能保证
  • 某些情况下需要增加额外设备

伪相关反馈

伪相关反馈对于真实相关反馈的人工部分进行自动化

伪相关反馈算法:对于用户查询返回有序的检索结果,假定前 k 篇文档是相关的进行相关反馈 (如 Rocchio)

优点:

  • 不用考虑用户的因素,处理简单
  • 很多实验也取得了较好效果

缺点:

  • 没有通过用户判断,所以准确率难以保证
  • 不是所有的查询都会提高效果

相关反馈小结:

  • 文档选择:从检索结果中选择相关或不相关文档。用户显式/隐式,或者系统假设。
  • 词项选择:从相关不相关文档中选择需要处理的词项
  • 查询扩展/重构:修改原始查询

查询扩展

查询扩展是另一种提高召回率的方法

使用 “全局查询扩展” 来指那些 “查询重构(query reformulation)的全局方法”

在全局查询扩展中,查询基于一些全局的资源(同义词或近义词)进行修改,这些资源是与查询无关的

查询扩展的方法

  • 基于相关反馈的查询扩展
  • 人工词典法:通过人工构建的同(近)义词词典 (人工编辑人员维护的词典,如 PubMed)来扩展原始查询
  • 自动词典法:自动导出的同(近)义词词典 (比如,基于词语的共现统计信息)
  • 其他外部资源法:比如基于查询日志挖掘出查询等价类(Web上很普遍,比如上面的 “palm” 例子)

交互式查询扩展 (Interactive QE):用户通常很懒,用户提交的扩展词项并不一定有用

基于词项相似度的查询扩展:

基于候选词和原始查询词项共现 (co-occurrences)的查询扩展

查询扩展的优点:

  • 通常可以检索到更多的相关文档
  • 统计测试表明MAP显著提高
  • 在伪相关反馈的应用场景下,如果反馈集文档质量很差,会严重降低检索效果
  • 可能会产生查询漂移
  • 对于某些查询任务,例如主页搜索,由于相关文档总数非常少,查询扩展通常无效

使用外部资源进行查询扩展(External QE)

选择性查询扩展(Selective QE)

  • 在伪相关反馈应用场景,如果预测反馈集质量很低,则不再执行QE
  • 适用于对排名靠前文档查准率(early precision)有要求的任务

搜索引擎中的查询扩展主要依赖的资源:查询日志

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 杂谈-20220925 + + /2022/09/25/diary/diary20220925/ + + 公开于2023年11月19日

四年相识、三年相恋、抵不过些许距离。

并没有表现得太过于悲伤,甚至都没有留下眼泪。可能是因为从日常的点点滴滴中已经知道这个结果了,最后的两三个月完全就是在硬撑着,我一厢情愿地在努力,但是她的心里早就已经有了答案。

相识的第一天,2018年9月24日,中秋节。两个人走进教室,拿出简历,面试。面试后一起下楼,简单的说了第一句打招呼的话语,分开。那是第一次见面,内心里有一种悸动,真的似乎有点喜欢。此时的我,刚刚经历了高考的失利,急于在这个看起来与我的能力并不匹配的学校中证明我自己。去竞选班干部,去参加各种学生组织,去认识更多的人,同时也不再压抑内心的感情,积极去找寻自己的爱情。当初对爱情只是懵懂,被拒绝了一次,拒绝了别人一次,有点怕了。有时候我也毫不掩饰我对她的喜欢,去车站接,送奶茶,约出来走走等等。就这样默默暗恋了一年。

第二年的中秋节,2019年9月13日,我终于鼓起勇气,约出来转到了湖大再转回来,说出了压在心底一年的话。这样就收获了我的初恋。当时的我,并不优秀,对未来一片迷茫,不知道四年大学毕业后要去到哪里。“我们在一无所有的情况下选择去尝试”,我同时也坚定了要共度一生的想法,想要给她今后一个更好的生活,于是我努力学习,从一个将将摸到保研边的中等生,逐渐变成了一个强者,拿下了很好的成绩排名,拿到了学校里面的绝大部分奖项,拿到了国家奖学金,成功保研。因为有了动力,一切都变得理所应当,再苦再累真的值得。

我们之间的感情没有那么多的激情燃烧,更多的是平淡。我尽量在她需要我的时候出现在她的身边,平时四周转一转,一起去图书馆学习,感冒了送她去医院,脚伤了每天接送,中午晚上点好饭送到身边。我很享受这种平淡的生活,因为我已经认准了她,什么东西都不能减少我对她的爱。我也认为她是和我一样性格的人,有自己的个性,有上进心进取心,不安于现状希望改变。就这样过了两年的美好时光,我们走入了大四的毕业季。

大四开始的我,松弛了下来,暂时与紧张的学习生活说了再见,开始无底线的放松。而她却要每天准备考研,还有两节课要上。而且由于搬校区的缘故,我见到她并不是很容易了。在这期间有了一些她不怎么讲话的迹象,甚至在我离开长沙和她吃的最后一顿饭上也是心不在焉。我把它归结为考研焦虑,并没有太过在意。也还是因为我对这段感情太有信心了吧,我相信时间距离都不是问题,我们只要努力把自己变得更好,总有一天会克服种种困难生活在一起衣食无忧。这也导致了大四下学期去实习的时候有点忽略了对她的关心,感觉是因为都忙,说的话也变少了。这种下了分手的种子。

6月正式本科毕业,2022年6月21日,突然的完全不理我,突然的提出分手,我直接崩溃掉。原来她并没有任何的信心,只是我自己自作多情罢了。原来这半年我基本不知道任何有关她的生活,我不知道她实习的工作怎么样,不知道她去面试了教师岗位,不知道她成功考上教师编制。我终于发现了这个问题,但是事实上已经晚了。虽然这一次分手我用回忆挽回,但是并没有打消她的念头,也并没有增加很多她对我的爱。而且由于距离,也阻隔了表达爱的方式。就好像“inception”一样,动了念头就很难再忘记掉了。

然后是短短四天的青岛旅行,差不多一年以来的首次见面。尤其是最后一天的晚上,最后一次吃饭基本上全程都在看手机。虽然是在修朋友圈的照片,但是我当然也是有一点点不高兴的。从上次几乎分手后我就十分在乎她的感受,但是我从来都没有勇气当面问出这些话语。这样过了两个月,我不断询问她的感受,不断讲给她我现在的想法。然而一切都是没有作用的。不爱了真的就不爱了。2022年9月24日,正式分手。我拼了命的想要挽回,我真的放不下,也不可能放得下三年的感情,换回来的仅仅是“不甜”、“不爱了”如此冰冷的字眼。我也并没有像我想象中的那么悲伤绝望,甚至一滴眼泪都没有落下。也许是因为早已经知道了这个结果吧。但是还是一夜没有睡着觉,真的无法接受这个冰冷的事实。

人,真的是会变的,会根据环境而变化。上大学的时候我们周围什么都没有,只有彼此。而步入社会,找到了稳定的工作,接触了各种各样的有趣的人,就会重新审视自己之前的生活,自己之前爱过的人。“我想换人了”“我倾向于比较条件,你的条件不如我”“及时止损”如此冰冷的话语,真的很难相信是从她的聊天框里面弹出来的。或许她发现自己面前存在着无数种可能性,为什么还要等着可能一年仅能见几次面,至少还要等上三年的远方的人呢?总之她不再怀念我们共度的三年时光了,毅然放手投入了新生活的怀抱,只能留下我在这里独自悲伤。

所以什么是爱情?我这几天不断在问自己这个问题。我一直认为爱情是一份承诺,是能克服重重困难一起走下去的勇气。现在我觉得这个想法确实太过于理想化了。可能我自己是这种想法,但是我不能要求别人有完全相同的想法。女孩子可能需要的并不是这种承诺,也不愿意有勇气,更愿意的是就在此时此刻,能有一个人在身边照顾她,关心她,两个人在一起的样子才是爱情。爱情也不可能没有物质需求,如果没有面包,只有爱情 ,那么这段爱情能撑到什么时候呢?如果能有一个人在身边照顾她,不愁吃穿,稳定工作,未来立刻触手可及,有人会不希望过上这种生活吗?可能以前觉得,两个人向着一个目标而努力,最终实现了理想,爱情自然修成了正果。但是如果不努力就能得到爱情,还努力做什么呢?为什么还要体验那种拼搏痛苦的生活,为什么不能躺在现实中直接享受呢?我这个人,对待每一件事情都很认真,对待每一个人也很认真,过于认真就过于理想化,理想化的目标,我能坚持但是并不能保证别人也坚持。世界是很残酷的,人也是很残酷的,坚持初心的人真的很少。

我的第一段恋爱之旅就这样结束了。我不恨她,她没有什么错误,也从来没有对我做出过任何的承诺,也没有做任何对不起我的事情。只能说,我们的爱情观确实不一致。好的恋爱让我们都成长了很多,学会更好地爱自己、爱他人。如果我还能有下一段爱情,我会更加谨慎地做出选择,没有结果,或者是短期内看不到结果的爱情,我宁愿不要,也不会去轻易去做出承诺,即使我知道我的承诺我一定坚持。

我不能这样悲伤下去,我要抬头向前看。虽然可能以后都不会有合适的人,合适的爱情,但,还是要过好每一天,珍惜自己现在的生活。最近纠结于这段感情,对父母疏远了一些,但其实他们才是这个世界上真的真的无条件爱我的人,我又有什么理由不爱他们呢?

放下过去,原谅自己,弥补过错,重新开始。

]]>
+ + + + + Life + + + + + + + Diary + + + +
+ + + + + 研究生课程:机器学习-第6章 聚类分析 + + /2022/09/25/UCAS/machine-learning/machine-learning-6/ + + 《机器学习》课程笔记:第6章 聚类分析

第6章 聚类分析

概述

聚类是无监督机器学习问题

  • 目标:感知样本间的相似度,进行类别归纳
  • 聚类研究的重要应用:1. 潜在类别预测,2. 数据压缩
  • 既可以作为一个单独过程,用于寻找数据内在的分布结构,也可以作为分类、稀疏表示等其他学习任务的前驱过程。

影响聚类结果的因素:

  1. 属性选择导致不同结果
  2. 相似性度量是判断样本间、类别间的相似的标准
  3. 聚类规则是样本聚集条件,例如,近邻、损失函数

相似性度量

样本-样本:(向量相似性)

样本-集合:

  1. 集合为离散点集

到集合最远点距离:

到集合最近点距离:

到集合平均点距离:

  1. 集合为连续区域

集合为平面:

集合为圆:

集合-集合:(类间距离)

集合间最远点距离:

集合间最近点距离:

集合间所有点平均距离:

集合表征点间距离(如平均值):

集合内样本间距离(类内距离):

性能度量

聚类性能的外部指标指通过已知类簇划分,对聚类结果进行评价;判别同类别样本对标签一致与否,避免相同类簇划分,不同标签名称导致的不一致。

Jaccard系数、FM系数和Rand系数

聚类性能的内部指标:没有已知的类簇划分进行参考,通过聚类具有的类内相似和类间相异的特点进行评价。

DB指数:,越小越好

Dunn指数:,越大越好

序贯方法

基本思想:逐一比较单个样本与类簇的相似性,有相似类则归类,无相似类则建立新类。

优点:一种简单的,快速算法

相似性的关键度量:类别相似性:样本—类簇(样本—集合)。

缺点:所有样本过滤一遍后才知道类别总数,而先出现的样本不能找到(后出现的)合适类别

改进算法:采用两个阶段,类别确定、分类。

两阶段序贯方法:

缺点:以上两种方法依赖于阈值

改进方法:弱化阈值作用,采用两个阈值,形成灰色带。

双阈值序贯方法

三种算法缺点:

  1. 当类别一旦产生,不可变,尽管后来类簇增加,类别很相近也无法合并。
  2. 敏感于样本顺序,样本类别未必是最合适的。

增强算法

增强处理1:对类别集合进行合并操作

增强处理2:对样本类别重置

层次聚类

基本思想:

聚类嵌套定义:是样本集上的两种聚类划分,如果中所有的类簇都是中类簇的子集,则称嵌套在内,记作

层次聚类策略:类簇之间(依据相似性)不断合并、或不断的分化, 直到满足聚类停止条件。

自底向上/归并算法:

次迭代:计算所有两个类簇的相似性,归并最相似的两个类簇,更新类别划分

缺点:没有归并的类簇间相似性,被重复计算

基于矩阵的归并算法

利用矩阵记录类簇间的相似性

  • 删除对应合并的两行和列
  • 增加一行和列: 新类簇与其他类簇的相似度

优点:不必重新计算“没有合并的类簇间”的相似性

分化算法:过程与归并相反

次迭代:在所有类簇的所有划分中,计算所有两个类簇相似性,选择最不相似的类簇集合划分,更新类别划分

缺点:没有划分的类簇间相似性,被重复计算

如何确定聚类个数?

K均值聚类

Kmeans:将样本分给最近的类心,然后重新调整类心;通过多次迭代,逐步进行类别划分。

最优准则:最小化误差平方和 是第个类簇的样本。

一般方法:最近类心原则,批量划分后修正类心

改进方法:单个划分最优原则,单个划分后修正类心

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:现代信息检索-第9讲 完整搜索系统中的评分计算 + + /2022/09/24/UCAS/information-retrieval/information-retrieval-9/ + + 《现代信息检索》课程笔记:第9讲 完整搜索系统中的评分计算

第9讲 完整搜索系统中的评分计算

不排序的问题严重性

  • 用户只希望看到一些而不是成千上万的结果
  • 很难构造只产生一些结果的查询
  • 即使是专家也很难
  • 排序能够将成千上万条结果缩减至几条结果,因此非常重要

排序的重要性:

  • 摘要阅读(Viewing abstracts):用户更可能阅读第一页的结果的摘要
  • 点击(Clicking):点击的分布甚至更有偏向性
    • 一半情况下,用户点击排名最高的页面
    • 即使排名最高的页面不如排名第二的页面相关,仍然有接近30%的用户会点击它。
  • 正确排序相当重要
  • 排对最高的页面非常重要

结果排序的实现

倒排索引中的词项频率存储

  • 每条倒排记录中,除了docIDd 还要存储tft,d
  • 通常存储的是原始的整数词频,而不是对数词频对应的实数值
    • 这是因为实数值不易压缩
  • 对tf采用一元码编码效率很高
  • 总体而言,额外存储tf所需要的开销不是很大:采用位编码压缩方式,每条倒排记录增加不到一个字节的存储量
  • 或者在可变字节码方式下每条倒排记录额外需要一个字节即可

两种常见的评分累加算法:

以词项为单位(term-at-a-time, TAAT),首先获得词项t的posting list,然后累加得分

以文档为单位的计算,首先获得包含查询词的所有文档,将这些文档按照静态评分排序,然后依次累加得分

精确top K检索及其加速办法:

目标:从文档集的所有文档中找出K个离查询最近的文档

步骤:对每个文档评分(余弦相似度),按照评分高低排序,选出前K个结果

加速方法:

快速计算余弦:不考虑查询词项的权重

堆法N中选K:不对所有文档进行排序,只需要挑出最高的K个结果

提前终止计算:得到了top K结果,不需要再进行后续计算

精确topK检索的问题:仍然无法避免大量文档参与计算

非精确topK检索:非精确topK的结果如果和精确topK的结果相似度相差不大,应该也能让用户满意

找一个文档集合A,K<|A|<<N,利用A中的top K结果代替整个文档集的top K结果

方法一:索引去除

从查询词的角度:只考虑那些包含高idf查询词项的文档

从文档的角度:只考虑那些包含多个查询词项的文档

仅考虑高idf词项、仅考虑包含多个词项的文档

方法二:胜者表

对每个词项t,预先计算出其倒排记录表中权重最高的r篇文档,如果采用tfidf机制,即tf最高的r篇

方法三:静态质量得分排序方式

为每篇文档赋予一个与查询无关的[0,1]之间的值,记为g(d),例如Pagerank

最终文档排名基于g(d)和相关度的线性组合

目标是找net-score最高的top K文档

方法四:影响度(Impact)排序

提前结束法:

遍历倒排记录表时,可以在如下情况之一发生时停止:

  • 遍历了固定的文档数目r
  • wft,d 低于某个预定的阈值
  • 将每个词项的结果集合合并
  • 仅计算合并集合中文档的得分

将词项按照idf排序:

  • 对于多词项组成的查询,按照idf从大到小扫描词项
  • 在此过程中,会不断更新文档的得分(即本词项的贡献),如果文档得分基本不变的话,停止
  • 可以应用于余弦相似度或者其他组合得分

方法五: 簇剪枝

随机选 篇文档作为先导者,对于其他文档,计算和它最近的先导者

非docID的倒排记录表排序方法

与查询无关的一种反映结果好坏程度的指标

以文档为单位(Document-at-a-time)的处理、以词项为单位(Term-at-a-time)的处理方式

WAND(Weak AND) 评分算法

  • 实验表明, WAND 可以降低 90% 以上的评分计算开支
  • WAND并非仅仅适用于cosine评分排序
  • WAND 及其不同的改进版能够满足安全排序(Safe Ranking, 即精确排序)

完整的搜索系统

多层次索引基本思路:

  • 建立多层索引,每层对应索引词项的重要性
  • 查询处理过程中,从最高层索引开始
  • 如果最高层索引已经返回至少k (比如, k = 100)个结果,那么停止处理并将结果返回给用户
  • 如果结果 < k 篇文档,那么从下一层继续处理,直至索引用完或者返回至少k 个结果为止
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 机器学习算法竞赛实战-时间序列 + + /2022/09/24/UCAS/machine-learning/machine-learning-competition-time/ + + 机器学习算法竞赛实战-时间序列

第9章 时间序列

什么是时间序列

时间序列是按时间顺序索引(或列出或图示)的一系列数据点。组成时间序列的数据由相对确定的时间戳组成。

对时间序列的分析基于以下假设:数据文件中标签的数据值表示以等间隔时间进行的连续测量值。假设数据存在相关性,然后通过建模找到对应的相关性,并利用它预测未来的数据走向。

可以从变量角度将这些问题归纳为单变量时间序列和多变量时间序列

可以从预测目标角度将这些问题归纳为单步预测和多步预测

单变量时间序列仅具有单个时间相关变量,所以仅受时间因素的影响。这类问题重点在于分析数据的变化特点,受相关性、趋势性、周期性和循环性等因素的影响。

多变量时间序列具有多个时间相关变量,除了受时间因素的影响,还受其他变量的影响。需要考虑更多的因素,挑战也更大。

单步预测问题比较基础,仅在训练集的时间基础上添加一个时间单位便可以作为测试集

多步预测问题比较复杂,是在训练集的时间基础上添加多个时间单位作为测试集

交叉验证的时候为了保留时间相关性,需要采用滚动交叉验证的方式:

  • 首先使用初始时间到t时刻的数据来训练模型
  • 然后用从t到t+n时刻的数据进行线下验证,并计算评价指标的分数
  • 将训练样本扩展到t+n时刻,用从t+n到t+2n时刻的数据进行验证
  • 不断重复,直到达到最后一个可用的标签值

基本规则方法

加权平均:离当前时间点越近的数据的重要性越高

指数平滑:将每个时间单位的权重按照指数级进行衰减(指数平滑像是拥有无限记忆且权值呈指数级递减的移动平均法)

时间序列模式

趋势性:在很长一段时间内呈现的数据持续上升或持续下降的变动

周期性:在一段时间序列内重复出现的波动,是各种因素综合影响的结果。

相关性:在某一段序列往往存在正相关或负相关,前后时间点会有很大的关联

随机性:除了上述三种模式外的随机扰动

特征提取方式

历史平移:直接将历史记录作为特征

窗口统计:从多个序列单位中提取特征

序列熵特征:描述序列的确定性和不确定性

还有时间特征与统计特征

模型的多样性

传统的时序模型:ARIMA(差分自回归滑动平均模型)

树模型:对时间序列进行平稳性调整

深度学习模型

  • 卷积神经网络
  • 长短期记忆网络

第10章 实战案例

第11章 实战案例

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Competition + + + +
+ + + + + 研究生课程:机器学习-第5章 回归分析 + + /2022/09/22/UCAS/machine-learning/machine-learning-5/ + + 《机器学习》课程笔记:第5章 回归分析

第5章 回归分析

概述

回归问题:

根据给定的训练集,其中(预测的结果是连续函数值)

要求寻找上的决策函数

性能评价:

均方误差:

泛化误差可分解为偏差、方差和噪声之和

线性回归原理:使用线性函数来预测数据的分布

最小二乘估计

目标函数:最小误差平方和

求解:

最大似然估计

正态分布假设的似然函数

误差服从正态分布:

似然函数:,可以转换为对数的形式

高斯误差的最大似然估计=最小二乘估计

优化学习:梯度下降方法

最大后验估计

正态分布的先验似然函数:

最大后验估计目标函数:

高斯分布的最大后验估计 = 正则化最小二乘估计

正则化最小二乘估计解:

正则项解决过拟合问题

扩展的非线性模型

线性基函数回归

线性回归:

扩展的非线性回归:

基函数形式:多项式函数、高斯分布函数、sigmoid类型的函数、tanh类型的函数

多项式回归:

误差分析

正则项对Bias和Variance的影响

参数估计

最小二乘估计是无偏估计

正则化最小二乘估计是有偏估计

使得参数估计更加稳定

相当于增加正则项

相当于加入白噪声

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:高级人工智能-第4讲 图像数据的深度学习模型 + + /2022/09/22/UCAS/advanced-ai/advanced-ai-4/ + + 《高级人工智能》课程笔记:第4讲 图像数据的深度学习模型

第4讲 图像数据的深度学习模型

卷积神经网络

计算机视觉需要应用大量的图像数据

卷积神经网络是一种特殊的深层神经网络模型

  • 它的神经元间的连接是非全连接的
  • 同一层中某些神经元之间的连接的权重是共享的(即相同的)。

20世纪60年代,Hubel和Wiesel研究猫脑皮层

  • 用于局部敏感和方向选择的神经元,其独特的网络结构可以有效地降低反馈神经网络的复杂性
  • 基于猫的初级视皮层(VI区)的研究:简单细胞和复杂细胞
  • 两层神经网络模拟初级视皮层中的简单细胞和复杂细胞
    • 每层的神经元被组织成二维平面
    • “简单细胞”层提取其输入中的局部特征
    • “复杂细胞”层组合“简单细胞”层中相应的子区域,使得整个网络对局部变换具有一定的不变性。

局部连接

局部感知野:图像的空间联系也是局部的像素联系较为紧密,而距离较远的像素相关性则较弱,减少了需要训练的权值数目

参数共享:图像的一部分的统计特性与其他部分是一样的。在输入的不同位置检测同一种特征具有平移不变性

一维、二维、三维卷积

其中三维卷积:假设输入数据的大小为a1×a2×a3,过滤器大小为f,即过滤器维度为f×f×f。三维卷积最终的输出为(a1−f+1)×(a2−f+1)×(a3−f+1)。

多卷积核:

  • 每个卷积核都会将图像生成为另一幅图像。
  • 两个卷积核就可以生成两幅图像,这两幅图像可以看做是一张图像的不同的通道。

边缘检测示例:卷积运算是输入图像与过滤器(也叫核)进行的运算,得到输出图像。卷积核与图像对应的位置相乘求和得到一个新值。

假定要识别图像中的特定曲线,也就是说,对这种曲线有很高的输出,对其他形状则输出很低,这也就像是神经元的激活。

Padding:边缘不填充

  • 随着不断卷积,图像会变得越来越小,有时你可不想让它变小
  • 最角落的点只被使用了一次,这意味着在下传的过程中丢掉了图像边缘位置的信息。

卷积步长:卷积中的步幅是另一个构建卷积神经网络的基本操作

输入与输出的尺寸关系:

单层卷积网络:每一个卷积核的输出对应一个实数b(偏差),然后在进行激活函数的非线性转换得到输出

Pooling池化:

通过卷积获得了特征之后,下一步利用这些特征去做分类。

  • 使用卷积时是利用了图像的“静态”特征
  • Pooling对不同位置的特征进行聚合统计

池化层中没有需要学习的参数,所以通常不把池化层当做独立的一层来看。

池化层是一般不会设置padding,即一般padding为0。

fitter为2,stride为2是最常见的参数设置,尺寸图像缩小为原来的一半。

卷积时用的尺寸计算公式同样适用于池化层。

CNN

CNN基本结构:卷积层和子采样层

卷积神经网络是一个多层的神经网络

  • 每层由多个二维平面组成
  • 每个平面由多个独立神经元组成

CNN训练过程

监督训练:Bp算法

向前传播

  • 从样本集中取一个样本,将输入网络
  • 计算相应的实际输出

反向传播

  • 计算实际输出与相应的理想输出的差
  • 按极小化误差的方法反向传播调整权矩阵
  • 代价函数
    • 最小化平方误差(MSE),最小化相对熵(Relative Entropy)
  • 反向传播主要考虑三个方面:
    • 输出层,代价函数的确定及求导
    • Pooling,数据的下采样及残差的上采样
    • 卷积层,数据的卷积运算及残差的反卷积运算

卷积网络的核心思想:将局部感受野、权值共享以及时间或空间亚采样这三种结构思想结合起来获得了某种程度的位移、尺度、形变不变性。

层间联系和空域信息的紧密关系,使其适于图像处理和理解:图像和网络的拓扑结构能很好的吻合

避免了显式的特征抽取,而隐式地从训练数据中进行学习:特征提取和模式分类同时进行,并同时在训练中产生;权重共享可以减少网络的训练参数,使神经网络结构变得更简单,适应性更强。

CNN的改进:

Rectified linear function:加速收敛和稀疏化

dropout:将隐层节点以一定概率清0

局部对比归一

非线性变换、池化

残差网络(Residual Networks(ResNets))

  • 因为残差网络很容易学习恒等式函数,所以随着网络加深,至少不会让网络变差。
  • 学习结果对网络权重的波动变化更敏感

图像数据应用

  • 目标定位
  • 特征点检测
  • 目标检测
  • 人脸识别
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + 研究生课程:现代信息检索-第8讲 检索评价 + + /2022/09/21/UCAS/information-retrieval/information-retrieval-8/ + + 《现代信息检索》课程笔记:第8讲 检索评价

第8讲 检索评价

检索评价

  • 通过评估可以评价不同技术的优劣,不同因素对系统的影响,从而促进本领域研究水平的不断提高
  • 信息检索系统的目标是较少消耗情况下尽快、全面返回准确的结果。
  • 计算机应用学科偏重于研究“更好的”方法/算法/模型,需要一种公平可靠的方法和指标体系进行评价

评价什么?

  • 效率:时间开销、空间开销、响应速度
  • 效果:
    • 返回的文档中有多少相关文档
    • 所有相关文档中返回了多少
    • 返回得靠不靠前
  • 其他指标:覆盖率、访问量、数据更新速度

使用相同的文档集合,相同的查询主题集合,相同的评价指标,对不同的检索系统进行比较。

评价指标:某个或某几个可衡量、可比较的值

评价过程:设计上保证公平、合理

IR中评价的难点:相关性(Relevance)是一个主观概念,文档相关性依赖于查询(数据标记工作量庞大)

评价指标

  1. 对单个查询进行评估的指标:在单个查询上检索系统的得分

召回率(Recall):返回的相关结果数占实际相关结果总数的比率

正确率(Precision):返回的结果中真正相关结果的比率

虽然Precision和Recall都很重要,但是不同的应用、不用的用户可能会对两者的要求不一样。

  • 垃圾邮件过滤:宁愿漏掉一些垃圾邮件,但是尽量少将正常邮件判定成垃圾邮件。
  • 有些用户希望返回的结果全一点,他有时间挑选;有些用户希望返回结果准一点,他不需要结果很全就能完成任务。

问题①:召回率难以计算:

对于大规模语料集合,列举每个查询的所有相关文档是不可能的事情,因此,这种情况几乎不可能准确地计算召回率可以采用Pooling方法,或者不考虑召回

缓冲池(Pooling)方法:对多个检索系统的Top k个结果组成的集合(并集)进行人工标注,标注出的相关文档集合作为整个相关文档集合。这种做法被验证是可行的(可以比较不同系统的相对效果),在TREC会议中被广泛采用。

问题②:两个指标需要融成一个指标,或者只采用单一指标

F值(F-measure):召回率R和正确率P的调和平均值

Fβ:表示召回率的重要程度是正确率的β(>=0)倍,β>1更重视召回率, β<1更重视正确率

E(Effectiveness)值:召回率R和正确率P的加权平均值,b>1表示更重视P

精确率是所有判定中正确的比率,一般不使用这一评价指标

  • 由于和查询相关毕竟占文档集的极少数,所以即使什么都不返回,可能对大部分查询来说可以得到 99.99%以上的精确率
  • 信息检索用户希望找到某些文档并且能够容忍结果中有一定的不相关性,返回一些即使不好的文档也比不返回任何文档强

问题③:两个指标都是基于(无序)集合进行计算,并没有考虑(排)序的作用

R-Precision:检索结果中,在所有相关文档总数位置上的准确率,如某个查询的相关文档总数为80,则计算检索结果中在前80篇文档的正确率。

正确率-召回率 曲线:检索结果以排序方式排列,用户不可能马上看到全部文档,因此,在用户观察的过程中,正确率和召回率在不断变化。

在上面的曲线对应的系统结果更好,也就是线下的面积(AUC)

P-R 曲线的插值问题:利用存在的召回率点对不存在的召回率点进行插值

优点:

  • 简单直观
  • 既考虑了检索结果的覆盖度,又考虑了检索结果的排序情况

缺点:单个查询的P-R曲线虽然直观,但是难以明确表示两个查询的检索结果的优劣

基于P-R曲线的单一指标:P-R曲线上P=R的那个点(Break Point)

平均正确率(Average Precision, AP):对不同召回率点上的正确率进行平均

不考虑召回率的指标:

Precision@N:在第N个位置上的正确率,对于搜索引擎,大量统计数据表明,大部分搜索引擎用户只关注前一、两页的结果,

  1. 对多个查询进行评估的指标:在多个查询上检索系统的得分

平均的求法:

  • 宏平均(Macro Average): 对每个查询求出某个指标,然后对这些指标进行算术平均
  • 微平均(Micro Average): 将所有查询视为一个查询,将各种情况的文档总数求和,然后进行指标的计算
  • 宏平均对所有查询一视同仁,微平均受返回相关文档数目比较大的查询影响

MAP(Mean AP):对所有查询的AP求宏平均

整个IR系统的P-R曲线:

在每个召回率点上,对所有的查询在此点上的正确率进行算术平均,得到系统在该点上的正确率的平均值。

两个检索系统可以通过P-R曲线进行比较。位置在上面的曲线代表的系统性能占优。

MRR(Mean Reciprocal Rank): 对于某些IR系统(如问答系统或主页发现系统),只关心第一个标准答案返回的位置(Rank),越前越好,这个位置的倒数称为RR,对问题集合求平均,则得到MRR

Bpref:在相关性判断不完全的情况下,计算在进行了相关性判断的文档集合中,在判断到相关文档前,需要判断的不相关文档的篇数。

相关性判断完全的情况下,利用Bpref和MAP进行评价的结果很一致,但是相关性判断不完全的情况下,Bpref更鲁棒

GMAP:几何平均值

NDCG:对于返回结果,相关度级别越高的结果越多越好,相关度级别越高的结果越靠前越好。

优点:

  • 图形直观,易解释
  • 支持非二值的相关度定义,比P-R曲线更精确
  • 能够反映用户的行为特征(如:用户的持续性)

缺点:

  • 相关度的定义难以一致
  • 需要参数设定

现有评价体系远没有达到完美程度

  • 对评价的评价研究
  • 指标的相关属性(公正性、敏感性)的研究
  • 新的指标的提出(新特点、新领域)
  • 指标的计算(比如Pooling方法中如何降低人工代价?查询集或文档集合发生变化怎么办?)

相关评测

TREC

总目标:支持在信息检索领域的基础研究,提供对大规模文本检索方法的评估办法

  1. 鼓励对基于大测试集合的信息检索方法的研究
  2. 提供一个可以用来交流研究思想的论坛,增进工业界、学术界和政府部门之间的互相了解;
  3. 示范信息检索理论在解决实际问题方面的重大进步,提高信息检索技术从理论走向商业应用的速度;
  4. 为工业界和学术界提高评估技术的可用性,并开发新的更为适用的评估技术。

实验设计

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 机器学习算法竞赛实战-用户画像 + + /2022/09/20/UCAS/machine-learning/machine-learning-competition-people/ + + 机器学习算法竞赛实战-用户画像

第7章 用户画像

参考资料:《机器学习算法竞赛实战》整理 | 七、用户画像

用户:产品的使用者

数据收集方为了推广产品同时持续维护和改善用户体验需要对由用户操作而产生的数据进行挖掘,以期从中发现群体乃至个体的行为偏好,形成数据层面上的所谓画像。

用户画像

用于商业分析和数据挖掘的用户画像。基于给定的数据对用户属性及行为进行描述,然后提取用户的个性化指标,再以此分析可能存在的群体共性,并落地应用到各种业务场景中。

标签系统

核心就是给用户打标签,用来分析社会属性、社会习惯、生活习惯、消费行为。

标签分类方式

通过分析一个用户的特征来展示标签分类方式:

xiDaxH.md.png

多渠道获取标签

标签获取方式也可以看作特征获取方式

事实类:直接来自原始数据,比如性别、年龄、会员等级。也可以进行简单统计,比如用户行为次数、消费总额。

规则类:由运营人员和数据人员经过共同协商设定。例如,地域属性、家庭类型、年龄层等。所用技术知识:数理统计类,如基础统计、数值分层、概率分布、均值分析、方差分析等。

模型类:经过机器学习和深度学习等模型处理后,二次加工生成的洞察性标签。比如预测用户状态、预测用户信用分、划分兴趣人群和对评论文本进行分类。特点:综合程度高、复杂,依托数学建模,多种算法组合。

标签体系框架

xifcBF.md.png

用户画像数据特征

常见的数据形式

  • 数值型变量
  • 类别型变量
  • 多值型变量:用户在某个维度具有多个取值的变量
  • 文本型变量:利用文本记录的变量。需要NLP知识,例如jieba中文分词工具

文本挖掘算法

LSA:非概率主题模型,与词向量有关,主要用于文档的话题分析。其核心思想是通过矩阵分解的方式发现文档和词之间基于话题的语义关系。

具体:将文档集表示为词-文档矩阵,对矩阵进行SVD(奇异值分解),从而得到话题向量以及文档在话题向量的表示。

举例:2020腾讯广告大赛,首先构造用户点击的广告素材id序列(creative_id),然后进行TF-IDF计算,最后经过SVD得到结果。

(代码与书中不同,未验证)

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer# 稀疏特征降维 TruncatedSVDfrom sklearn.decomposition import TruncatedSVDfrom sklearn.pipeline import Pipeline# 提取用户点击序列docs = data_df.groupby(['user_id'])['creative_id'].agg(lambda x:"".join(x)).reset_index()['creative_id']# tf-idftfd = TfidfVectorizer()svd = TruncatedSVD(n_components=100, n_iter=50, random_state=2020)

PLSA:PLSA(概率潜在语义分析)模型是为了克服LSA模型潜在存在的一些缺点而提出的。通过一个生成模型来为LSA赋予概率意义上的解释。该模型假设每一篇文档都包含一系列可能的潜在话题,文档中的每一个词都不是凭空产生的,而是在潜在话题的指引下通过一定的概率生成的。

LDA:LDA(潜在狄利克雷分布)是一种概率主题模型,与词向量无关,可以将文档集中的每篇文档的主题以概率分布的形式给出。通过分析一批文档集,抽取出他们的主题分布,就可以根据主题进行聚类或分类。同时,它是一种典型的词袋模型,即一篇文档由一组相互独立的词构成,词和词之间没有先后顺序。

神奇的嵌入表示

word2Vec:可调用gensim包,参数:窗口大小、模型类型选择、生成词向量长度

对于Skip-Gram和CBOW:

  • CBOW在训练时比Skip-Gram快
  • CBOW可以更好地表示常见单词
  • Skip-Gram在少量的训练集中可以表示稀有单词或短语

DeepWalk

对于Word2Vec的衍生Item2Vec以及更多图嵌入方法,比如LINE、Node2Vec和SDNE

相似度计算方法

  • 欧式距离
  • 余弦相似度
  • Jaccard相似度

用户画像的应用

用户分析

  1. 京东JDATA平台2019年的“用户对品类下店铺的购买预测”
  2. 腾讯广告“2020腾讯广告大赛”

精准营销

  1. 2018科大讯飞AI营销算法大赛
  2. 2018腾讯广告算法大赛

风控领域

  • DF竞赛平台的“消费者人群画像-信用智能评分”
  • 拍拍贷“第四届魔镜杯大赛”

特点:

  • 业务对模型解释性比较高,对时效性有一定要求,需要权衡模型复杂度和精度,并且适当优化算法内核
  • 业务模型多样,需要紧密结合业务
  • 负样本极少,均衡学习算法

第8章 实战案例

参考资料:《机器学习算法竞赛实战》整理 | 八、实战案例:Elo Merchant Category Recommendation

赛题理解

Imagine being hungry in an unfamiliar part of town and getting restaurant recommendations served up, based on your personal preferences, at just the right moment. The recommendation comes with an attached discount from your credit card provider for a local place around the corner!

Right now, Elo, one of the largest payment brands in Brazil, has built partnerships with merchants in order to offer promotions or discounts to cardholders. But do these promotions work for either the consumer or the merchant? Do customers enjoy their experience? Do merchants see repeat business? Personalization is key.

Elo has built machine learning models to understand the most important aspects and preferences in their customers’ lifecycle, from food to shopping. But so far none of them is specifically tailored for an individual or profile. This is where you come in.

In this competition, Kagglers will develop algorithms to identify and serve the most relevant opportunities to individuals, by uncovering signal in customer loyalty. Your input will improve customers’ lives and help Elo reduce unwanted campaigns, to create the right experience for customers.

赛题数据

  • train.csv 训练数据集,包括 first_active_month,card_id,feature_1,feature_2,feature_3,target字段
  • test.csv 测试数据集,包括 first_active_month,card_id,feature_1,feature_2,feature_3字段

first_active_month表示的是信用卡产生第一笔交易的时间,feature是信用卡类型的脱敏特征。最后一列 target是要预测的数值

historical_transactions.csv 信用卡在给定商家的历史交易记录,文件比较大,基本都是一些脱敏的特征

merchants.csv所有商家的附加信息

new_merchant_transactions.csv two months’ worth of data for each card_id containing ALL purchases that card_id made at merchant_ids that were not visited in the historical data .(每张信用卡在新商家的购物数据)

评价指标使用RMSE

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Competition + + + +
+ + + + + 机器学习算法竞赛实战-基础篇 + + /2022/09/19/UCAS/machine-learning/machine-learning-competition-basic/ + + 机器学习算法竞赛实战-基础篇

开始学习

一直想学,前面看过觉得太难,这回一定要坚持看完!

第1章 初见竞赛

参考资料:《机器学习算法竞赛实战》学习笔记1.竞赛简介

竞赛流程

x9y9YD.md.png

问题建模

分析数据进而抽象出建模目标和方案。自行利用主办方提供的数据构造训练集与测试集。

数据探索

EDA(探索性数据分析),Exploratory Data Analysis。在大致了解问题建模方式后,需结合对赛题背景业务的理解去看数据长什么样子、数据是否和描述相符、包含哪些信息等。首先需要对数据有清晰认知,主要是宽表中各个字段的取值含义、范围和数据结构等。然后更深层次地结合标签分析特征的分布状态、训练集与测试集的同分布情况、特征之间的业务关联以及隐含信息表征等。

特征工程

Feature Engineering。特征决定机器学习预测效果上限,算法不断逼近这个上限。最费时的模块。

模型训练

选模型、调参数

模型融合

找找队友,看看Code

第2章 问题建模

参考资料:《机器学习算法竞赛实战》学习笔记2.问题建模

赛题理解

从直观上梳理问题,分析问题可解的方法、赛题背景等

业务理解:从个人生活的直观角度对业务进行分析

数据理解:在问题建模阶段,只需对数据做基本的分析。可以将数据理解分为数据基础层和数据描述层两个部分。主办方提供的原始数据质量良莠不齐,往往需要对原始数据进行清洗、加工和计算等处理。

  • 数据基础层:重点关注每个数据字段的来源、生产过程、取数逻辑、计算逻辑等,了解这些才能正确理解、选取并使用每一个原始字段,从而加工计算得出所需的更多衍生字段,数据最终通常以数据表格形式呈现。
  • 数据描述层:主要是在处理好的数据基础层上进行统计分析和概括描述,该层重点在于尽可能地通过一些简单统计量(如均值、最值、分布、增幅、趋势等)来概括整体数据的状况。具体使用哪些统计量依据数据呈现的具体情况而定。例如,对于时间序列问题,可以统计其增幅、趋势和周期;对于常规的数值特征,则可以观察期均值、最值和方差等统计量;对于存在多类别的样本集合,则可以使用分布、分位点等进行描述。

评价指标

分类指标:

  1. 错误率(error rate)与精度(accuracy)

错误率:分类错误的样本数占样本总数的比例

精度:分类正确的样本数占样本总数的比例

精度=1-错误率

  1. 查准率/准确率(precision)、查全率/召回率(recall)

查准率P反映真实情况与预测结果都为正例的样例在预测结果为正例的样例中的占比

查全率R反映真实情况与预测结果都为正例的样例在真实情况为正例的样例中的占比

(查准率与查全率是一对矛盾的度量,一般来讲,查准率高时,查全率偏低;查全率高时,查准率偏低)

,在查准率与查全率之间取得一个平衡

# 构建一个计算准确率、召回率和F1-score的评价代码y_train = np.array([1,0,1,0,1,0,1,0,1,1])y_pred = np.array([1,1,1,1,0,0,0,0,1,0])precision = precision_score(y_train,y_pred) #准确率recall = recall_score(y_train,y_pred) #召回率f1 = f1_score(y_train,y_pred) #f1度量print(precision,recall,f1)
0.6 0.5 0.5454545454545454
  1. ROC与AUC

先根据学习器的预测结果对样例进行排序,按此顺序逐个把样例作为正例进行预测,每次计算出“真正例率”(True Positive Rate,简称TPR)和“假正例率”(False Positive Rate,简称FPR),分别以他们为纵、横轴作图,就得到了ROC曲线。

,真正例率TPR反映真正例在实际情况为正例的样例中的占比

,假正例率FPR反映假正例在实际情况为反例的样例中的占比

ROC曲线对正负样本的数量和分布不敏感。

AUC定义为ROC下方的面积,在互联网的搜索、推荐和广告的排序业务中都极为常见。AUC作为一个数值,其值越大就代表分类器的效果越好。

值得一提的还有AUC的排序特性。相对于准确率、召回率等指标,AUC指标本身和模型预测的概率绝对值无关,它只关注样本间的排序效果,因此特别适合用作排序相关问题建模的评价指标。AUC是一个概率值,我们随机挑选一个正样本与一个负样本,由当前分类算法根据计算出的分数将这个正样本排在负样本前面的概率就是AUC值。

为什么AUC与模型预测的分数值无关是个很好的特性?假设采用的是准确率等指标,而模型预测的分数是个概率值,那么必须选择一个阈值来决定把哪些样本预测为1,哪些预测为0。阈值的选择不同,准确率的值就会不同。而AUC可以直接使用模型预测分数本身,参考的是相对顺序。在竞赛中,省去了参赛者试探阈值的麻烦。

  1. 对数损失

对数损失可用于评价分类器的概率输出。对数损失通过惩罚错误的分类来实现对分类器的准确度的量化。最小化对数损失基本等价于最大化分类器的准确度。为了计算对数损失,分类器必须提供概率结果,即把输入样本喂入模型后,预测得到每个类别的概率值(0~1),而不只是预测最可能的类别。

AUC与对数损失的区别

对数损失主要评价模型预测的概率是否足够准确,更关注和观察数据的吻合程度;AUC评价的则是模型把正样本排列到前面的能力。两者侧重不同,故应用不同。对于广告CTR问题,如果考虑广告排序效果,则可以选择AUC,这样不会受极端值影响。此外,对数损失反映了评价偏差,更偏向于将样本数量多的那类划分准确。由于使用AUC或对数损失可以避免把预测概率转换成类别的麻烦,在各种数据竞赛的分类问题中,AUC和对数损失基本是最常见的模型评价指标。

回归指标:

  1. 平均绝对误差,又称L1范数损失

MAE不是二阶连续可微的,其二阶导数总为0。

  1. 均方误差,又称L2范数损失

MSE的量纲与数据标签不一致,为了保证量纲的一致性,通常需要对均方误差进行开方(均方根误差RMSE)

平均绝对误差MAE与均方误差MSE的区别

均方误差对误差(真实值-预测值)取了平方,若误差>1,则均方误差会进一步增大误差。如果数据中存在异常点,那误差值就会很大,而误差的平方则会远大于误差的绝对值。因此,相对于使用平均绝对误差计算损失,使用均方误差的模型会赋予异常点更大的权重。简而言之,均方误差对异常值更加敏感

为什么在XGBoost里通常选择Huber损失替换MAE?

由于MAE不是连续可导的(0处不可导),所以需要使用可导目标函数来逼近平均绝对误差。而对于均方误差MSE,梯度又会随着损失的减小而减小,使预测结果更加精确。在这种情况下,Huber损失就非常有用,它会由于梯度的减小而落在最小值附近。比起均方误差MSE,Huber损失对异常点更加健壮。因此,Huber损失结合了MAE和MSE的优点。但是Huber损失可能需要我们不断调整超参数delta。

  1. 平均绝对百分比误差

MAPE与MAE一样,不存在二阶导数。但不用于MAE,平均绝对百分比误差MAPE除了考虑预测值与真实值的误差,还考虑了误差与真实值之间的比例。因此真实值越大,误差会越小。

样本选择

主办方提供的数据往往令人脑壳疼,主要是以下四个原因:

  • 数据集过大严重影响了模型性能:过大的数据集会严重影响各种特征工程和建模方式的快速验证
    • 对数据进行采样处理,然后在小数据集上建模分析
    • 特定业务场景下,可以过滤一些对建模没有意义的数据
  • 噪声和异常数据导致准确率不够
    • 采集数据时操作不当导致信息表征出现错误
    • 数据本身的特性存在合理范围内的抖动导致噪声与异常-看是否能够解码出正确数据
    • 数据噪声的存在具有两面性,噪声的存在会导致数据的质量变低,影响模型效果;另一方面,可以通过在训练集中引入噪声数据的方法使模型健壮性更强。
    • 当处理噪声数据时,首先考虑是否为采集错误导致的,再去权衡模型的泛化性和当前效果。有时去噪会导致模型泛化性能变差。要去噪,首先要识别出噪声,然后采取直接过滤或者修改噪声数据等多种办法,噪声数据可能是特征值不对,比如特征值缺失、超出特征值域范围等;也可能是标注不对,比如二分类问题的正样本标注成了负样本。
  • 样本数据冗余或不相关数据没有给模型带来收益
    • 数据中存在的冗余不仅会影响模型性能,更会引入噪声与异常。数据冗余的一个典型解决方案就是进行特征选择。
  • 正负样本分布不均衡导致数据存在倾斜-进行数据采样

问题1:在数据量非常大的情况下,为了降低成本,如何提高模型的训练速度?

  • 方法1:简单随机抽样,分为有放回与无放回
  • 方法2:分层采样-按照规定的比例从不同类别中随机抽取样本

问题2:针对正负样本分布不均衡的问题,如何通过数据采样解决这类问题?

  • 方法1:评分加权处理
    • 分布不均衡的问题包括欺诈交易识别和垃圾邮件识别等,其正负样本的数据分布差距极大。考虑正负样本的重要性,在模型训练以及评价的时候可以设计相应的得分权重,使得模型能够学习到需要获得关注的部分。
    • 此方法的具体操作步骤是:首先遍历所有样本,根据样本是否满足某个要求来给予其权重。
    • 加权的直观含义从业务上理解就是认为一个正样本的价值大于多个负样本的,因此希望模型在训练的时候能够更多地从正样本身上学到关键信息,当它学得不好的时候,就要对它加大惩罚力度。
  • 方法2:欠采样
    • 从数量较多的一类样本中随机选取一部分并剔除,使得最终样本的目标类别不太失衡。常用方法有随机欠采样和Tomek Links,其中Tomek Links先是找出两个各相指标都非常接近的相反类样本,然后删除这类样本中标签(label)占比高的,这类算法能够为分类器提供一个非常好的决策边界。
  • 方法3:过采样
    • 主要是对样本较少的类别进行重新组合,构造新样本。常用的方法有随机过采样和SMOTE算法。SMOTE算法并不是简单地复制已有的数据,而是在原有数据的基础上通过算法产生新生数据。

思考:在什么场景下需要处理样本的不均衡问题?

  • 如果竞赛任务对于召回有特别大的需求,即对每个正样本的预测都远远比负样本的预测更重要,那么这时候假如不做任何处理,对结果影响较大
  • 如果竞赛的评价指标是AUC,那么在实战中会发现这时处理样本不均衡问题与否对于结果的差别不太大。(但细微提升也是好的)
  • 如果在竞赛任务中正负样本同等重要,即预测正确一个正样本与预测准确一个负样本是同等重要的,那么不做处理问题也不大

线下评估

由于需要数据集对模型的效果进行线下验证,所以需要考虑如何对数据进行划分,构建合适的线下验证集。针对不同类型的问题,需要不同的线下验证方式。

书中将这些问题大致分为强时序性与弱时序性两类,然后以此确定线下验证方式。

  • 强时序性问题:对于含有明显时间序列因素的赛题,可看作强时序性问题,即线上数据的时间都在离线数据集之后。因此要将最接近测试集的数据作为验证集对模型的效果进行评估(采用时间上最接近测试集的数据做验证集,且验证集的时间分布在训练集之后)
  • 弱时序性问题:这类问题的验证方式主要为K折交叉验证(K-fold Cross Validation)

定义:先将总数据集D划分为k个大小相似的互斥子集,每个子集都尽可能保持数据分布的一致性(即从D中分层采样得到)。然后每次用K-1个子集的并集作为训练集,余下的自己作为测试集。这样可以获得K组训练/测试集,从而可进行k次训练和测试,最终返回这k个测试结果的均值。

注意:

  • 交叉验证法评估结果的稳定性和保真性很依赖K的取值,K通常取10,常用有5,20等
  • 给定k值,仍有多种划分方式。故通常要随机使用不同的划分重复p次,最终的评估结果是这p次k折交叉验证结果的均值,常见有10次10折交叉验证

以下为交叉验证代码,其中参数NFOLDS用来控制折数**(未实际验证)**

from sklearn.model_selection import KFoldNFOLDS = 5 #五折交叉验证folds = KFold (n_split = NFOLDS,shuffle=True,random_state=2021)#random_state只要是一个固定的值就可以了,不一定是2021for trn_idx,val_idx in folds.split(X_train,y_train):train_df,train_label = X_train.iloc[trn_idx, :],y_train[trn_idx]valid_df,valid_label = X_train.iloc[val_idx, :],y_train[val_idx]

参数random_state默认设置为None,这意为着每次进行KFold(…, shuffle=True)时,打散都是不同的。

为了保证结果的可重复性(在相同的平台上),应该给random_state设定一个固定的值。

第3章 数据探索

参考资料:《机器学习算法竞赛实战》学习笔记3.数据探索

如何确保自己准备好竞赛使用的算法模型?如何为数据集选择最合适的算法?如何定义可用于算法模型的特征变量?数据探索可以帮助回答以上三点。

一般而言,数据探索可以分为三个部分:

  1. 首先是赛前数据探索,帮助我们对数据有个整体性的认识,并发现数据中存在的问题,比如缺失值、异常值和数据冗余等
  2. 其次是竞赛中的数据探索,通过分析数据发现变量的特点,帮助提取有价值的特征,这里可以从单变量、多变量和变量分布进行分析
  3. 最后是模型的分析,可以分为重要性分析和结果误差分析,帮助我们从结果发现问题,并进一步优化

数据初探

赛前数据探索,主要包含分析思路、分析方法和明确目的。

  1. 分析思路

在实际竞赛中,最好使用多种探索思路和方法来探索每个变量并比较结果。在完全理解数据集后,就可以进入数据预处理阶段和特征提取阶段了,以便根据所期望的业务结果转换数据集。此步骤的目标是确信数据集已准备好应用于机器学习算法。

  1. 分析方法

数据探索的分析主要采用以下方法:

  • 单变量可视化分析:提供原始数据集中每个字段的摘要统计信息
  • 多变量可视化分析:用来了解不同变量之间的交互关系
  • 降维分析:有助于发现数据中特征变量之间方差最大的字段,并可以在保留最大信息量的同时减少数据维度。

可以检查每个变量的分布,定义一些丢失值,最终找到替换它们的可能方法。

  1. 明确目的

在竞赛中跳过数据探索阶段可能会导致数据倾斜、出现异常值和过多的缺失值,产生以下糟糕结果:

  • 生成不准确的模型
  • 在错误的数据上生成精确的模型
  • 为模型选择错误的变量
  • 资源的低效利用,包括模型的重建

数据探索阶段必须要明确:

  1. 数据集基本情况:比如数据有多大,每个字段各是什么类型
  2. 重复值、缺失值和异常值:去除重复值,缺失值是否严重,缺失值是否有特殊含义,如何发现异常值
  3. 特征之间是否冗余:可以通过特征间相似性特征来找出冗余特征
  4. 是否存在时间信息:当存在时间信息时,通常要进行相关性、趋势性、周期性和异常点的分析,同时有可能涉及潜在的数据穿越问题
  5. 标签分布:对于分类问题,是否存在类别分布不均衡。对于回归问题,是否存在异常值,整体分布如何,是否需要进行目标转换
  6. 训练集与测试集的分布:是否有很多在测试集中存在的特征字段在训练集中没有
  7. 单变量/多变量分布:熟悉特征的分布情况,以及特征和标签的关系

数据探索最基本的步骤之一是获取对数据的基本描述,通过获取对数据的基本描述从而获得对数据的基本感觉。以下方法有助于我们认识数据:

  • DataFrame.describe():查看数据的基本分布,具体是对每列数据进行统计,统计值包含频次、均值、方差、最小值、分位数、最大值等。
  • DataFrame.head(n):可以直接加载数据集的前n行,n默认为5
  • DataFrame.shape:得到数据集的行列情况
  • DataFrame.info():可以快速获得对数据集的简单描述,比如每个变量的类型、数据集的大小和缺失值情况。

下面通过一段代码展示nunique和缺失值的情况:

stats = []for col in train.columns:    stats.append((col, train[col].nunique(), train[col].isnull().sum() * 100 / train.shape[0], train[col].value_counts(normalize=True, dropna=False).values[0] * 100, train[col].dtype))stats_df = pd.DataFrame(stats, columns=['Feature', 'Unique_values', 'Percentage of missing values', 'Percentage of values in the biggest category', 'type'])stats_df.sort_values('Percentage of missing values', ascending=False)[:10]

xCG6L4.md.png

上图展示了经过上述代码生成的数据基本信息,我们从中找到特殊变量进行细致分析,这里选择nunique值低和缺失值多的变量进行观察。一般而言,nunique为1是不具备任何意义的,表示所有值都一样,不存在区分性,需要进行删除。可以发现有些变量的缺失值很多,比如缺失比例达到95%以上,我们可以考虑将其删除。

用柱状图的形式可以更加直观地展示变量的缺失值分布情况,以下为变量缺失值可视化图的具体生成代码:

missing = train.isnull().sum()missing = missing[missing > 0]missing.sort_values(inplace=True)missing.plot.bar()

变量分析

单变量分析

单变量可以分为标签、连续型和类别型

  1. 标签

标签是最重要的变量,首先应当观察标签的分布情况。对于房屋价格预测,其标签SalePrice为连续型变量。

通过可视化的方式观察SalePrice的分布情况

sns.distplot(train['SalePrice'], color='b', bins=100, hist_kws={'alpha': 0.4})

可见,SalePrice呈偏离正态分布,属于向右倾斜类型,存在峰值状态,一些异常值在500000以上。我们最终会想办法去掉这些异常值,得出能够让算法模型很好学习的、符合正态分布的变量。

xCJw0H.png

下面对SalePrice进行对数转换,并生成可视化图

sns.distplot(np.log(train['SalePrice']), color='b', bins=100, hist_kws={'alpha': 0.4})

xCJsht.png

可以看出 ,对数转换后的标签的分布为正态分布形式,比较适合算法模型学习。

  1. 连续型

类似于标签的查看方式,这里主要使用直方图这种可视化方式观察值的分布、每个值出现的频率等。以下为连续型变量的分布可视化的生成代码:

df_num = train.select_dtypes(include = ['float64', 'int64'])df_num.hist(figsize=(16, 20), bins=50, xlabelsize=8, ylabelsize=8)

xCYZEd.md.png

实际中要对全部的变量进行查看,分析每一个变量的分布情况。

接着进行更加科学的分析,首先是相关性分析。相关性分析只能比较数值间特征,所以对于字母或字符串特征,需要先进行编码,并将其转换为数值,然后再看有什么关联。在实际竞赛中,相关性分析可以很好地过滤掉与标签没有直接关系的特征。

正相关和负相关

  • 正相关:如果一个特征增加导致另一个特征增加,则它们呈正相关。值1表示完全正相关
    • 多重线性:现在假设特征A和特征B完全正相关,这意味着这两个特征值包含高度相似的信息,信息几乎没有或完全没有差异。这称为多重线性,因为两个特征包含几乎相同的信息。
  • 负相关:如果一个特征增加导致另一个特征减少,则它们呈负相关。值-1表示完全负相关

在搭建或训练模型时,如果同时使用这两个特征,可能其中一个会是多余的。我们应尽量消除冗余特征,因为它会使训练时间变长,同时影响其他优势

以下代码为生成有关SalePrice的相似性矩阵图

corrmat = train.corr()f, ax = plt.subplots(figsize=(20, 9))sns.heatmap(corrmat, vmax=0.8, square=True)

xCY4KO.md.png

从生成的相似性矩阵中,可以找出与房价相关性最强的变量,其中OverallQual(总评价)、GarageCars(车库)、TotalBsmtSF(地下室面积)、GrLivArea(生活面积)等特征与SalePrice呈正相关

从相似性矩阵中,我们还能发现变量之间的关系,如何利用相似性矩阵进行分析就成为了关键

  1. 类别型

数据探索的目的是为了帮助我们了解数据并且构建有效特征。

比如,我们找到了与标签有着强相关的特征,那么就可以围绕着这个强相关特征进行一系列的扩展,具体可以进行交叉组合,比如强相关加弱相关、强相关加强相关等组合,挖掘更高维度的潜在信息。

首先,观察类别型变量的基本分布情况,即观察每个属性的频次。根据频次,我们不仅可以发现热点属性和极少出现的属性,还可以进一步分析出现这些情况的原因,比如淘宝网的女性用户多于男性,主要是因为平台在服饰和美妆业务方面拥有强大的影响力。这是从业务角度考虑,自然也有可能是数据采样的原因。

对部分类别变量的分布进行可视化展示

df_not_num = train.select_dtypes(include = ['O'])fig, axes = plt.subplots(round(len(df_not_num.columns) / 3), 3, figsize=(12, 30))for i, ax in enumerate(fig.axes):    if i < len(df_not_num.columns):        ax.set_xticklabels(ax.xaxis.get_majorticklabels(), rotation=45)        sns.countplot(x=df_not_num.columns[i], alpha=0.7, data=df_not_num, ax=ax)fig.tight_layout()

xCU8bD.md.png

多变量分析

单变量分析太过于单一,不足以挖掘变量之间的内在联系,获取更加细粒度的信息,所以有必要进行多变量分析。分析特征变量与特征变量之间的关系有助于构建更好的特征,同时降低构建冗余特征的概率值。

此处选用本赛题中需要特别关注的特征变量进行分析

从相似性矩阵中,我们已知房屋评价与SalePrice呈正相关。进一步扩展分析,通过可视化来考虑房屋评价和房屋位置是否存在某种联系。

plt.style.use('seaborn-white')type_cluster = train.groupby(['Neighborhood','OverallQual']).size()type_cluster.unstack().plot(kind='bar',stacked=True, colormap= 'PuBu', figsize=(13,11),  grid=False)plt.xlabel('OverallQual', fontsize=16)plt.show()

xCaFJA.md.png

上图为不同房屋位置的评价分布条状图,我们可发现颜色越深代表评价越高。NoRidge、NridgHt和StoneBr都有不错的评价

再进一步看看不同位置房屋的SalePrice

var = 'Neighborhood'data = pd.concat([train['SalePrice'], train[var]], axis=1)f, ax = plt.subplots(figsize=(26, 12))fig = sns.boxplot(x=var, y="SalePrice", data=data)

xCalJs.md.png

高评价位置对应高SalePrice,说明房屋位置评价与房屋售价有比较强的相关性。除了通过这样的分析证明原始特征与SalePrice强相关外,还可以通过分析来构建新的特征。

既然房屋位置和房屋评价的组合能够出现更高售价的房屋,那么我们可以构造这两个类别特征的交叉组合特征来进行更细致的描述,也可以构造这个组合特征下的房屋均价等。

模型分析

学习曲线

学习曲线是机器学习中被广泛使用的效果评估工具,能够反映训练集和验证集在训练迭代中的分数变化情况,帮助我们快速了解模型的学习效果。我们可以通过学习曲线来观察模型是否过拟合,通过判断拟合程度来确定如何改进模型

学习曲线广泛应用于机器学习中的模型评估,模型会随着训练迭代逐步学习(优化其内部参数),例如神经网络模型。这时用于评估学习的指标可能会最大化(分类准确率)或者最小化(回归误差),这也意味着得分越高(低)表示学习到的信息越多(少)。

以下是学习曲线图中观察到的一些常见形状

  1. 欠拟合学习模型

欠拟合是指模型无法学习到训练集中数据所展现的信息,这里可以通过训练损失的学习曲线来确定是否发生欠拟合。在通常情况下,欠拟合学习曲线可能是一条平坦的线或者有着相对较高的损失,也就表明该模型根本无法学习训练集

  1. 过拟合学习模型

过拟合是指模型对训练集学习得很好,包括统计噪声或训练集中的随机波动。过拟合的问题在于,模型对于训练数据的专业化程度越高,对新数据的泛化能力就越差,这会导致泛化误差增加。泛化误差的增加可以通过模型在验证集上的表现来衡量。如果模型的容量超出了问题所需的容量,而灵活性又过多,则会经常发生这种情况。如果模型训练时间过长,也会发生过拟合。

特征重要性分析

通过模型训练可以得到特征重要性。对于树模型(如LightGBM和XGBoost),通过计算特征的信息增益或分裂次数得到特征的重要性得分。对于模型LR和SVM,则是使用特征系数作为特征重要性得分,例如LR(逻辑回归),每个特征各对应一个特征系数w,w越大,那么改特征对模型预测结果的影响就会越大,就可以认为该特征越重要。我们假定特征性得分和特征系数w都是在衡量特征在模型中的重要性,都可以起到特征选择的作用。

误差分析

误差分析是通过模型预测结果来发现问题的关键。

一般而言,回归问题中看预测结果的分布,分类问题中看混淆矩阵等。

在真实问题中,误差分析会更加细致。比如,在进行一个用户违约预估的二分类任务中,验证集结果中有200个错误分类样本,进一步分析发现有70%的错误分类样本是由于大量特征缺失而导致的误判,这时就需要调整,既可以通过挖掘更多能够描述这些误判样本的特征信息帮助增强模型的预测能力,还可以在模型训练中赋予这些误判样本更高的权重。

第4章 特征工程

参考资料:《机器学习算法竞赛实战》学习笔记4.特征工程

数据预处理

尽量得到标准、干净、连续的数据,供数据统计、数据挖掘等使用,视情况尝试对缺失值进行处理,比如是否要填充,填充什么。此外,有些竞赛提供的数据集以及对应的存储方式可能使得需要占用超过参赛者本身硬件条件的内存,故有必要进行一定的内存优化,这也有助于在有限的内存空间对更大的数据集进行操作。

缺失值处理

除了XGBoost和LightGBM等算法在训练时可以直接处理缺失值以外,其他很多例如LR、DNN、CNN、RNN等都并不能对缺失值进行直接处理。故而在数据准备阶段,要比构建算法阶段花更多时间,因为像填补缺失值这样的操作需要细致处理。

  1. 区分缺失值

首先,需找到缺失值表现形式。除了None、NA和NaN外,还有例如-1或-999来填充的缺失值。还有一种看上去像缺失值,但实际上有实际意义的业务,此时需特殊对待。例如没有填“婚姻状态”的用户可能是对自己隐私比较敏感,应为其单独设为一个分类;没有“驾龄”可能是没有车,为其填充0比较合适。

  1. 处理方法

数据缺失可以分为类别特征的缺失和数值特征的缺失两种。

  • 对于类别特征,通常会填充一个新类别,如0,-1等。
  • 对于数值特征,可以均值填充(但对异常值较为敏感),中位数填充(对异常值不敏感)。填充时一定要考虑所选择的填充方法会不会影响数据的准确性。

填充方法总结如下:

  • 类别特征:可选择最常见的一类填充方法,即众数;或直接填一个新类别
  • 数值特征:可填平均数、中位数、最大最小值等,具体情况具体分析
  • 有序数据(如时间序列):可填充相邻值next或previous
  • 模型预测填充:普通的填充仅是一个结果的常态,并未考虑其他特征间相互作用的影响,可以对含有缺失值的那一列进行建模并预测其中缺失值的结果。方法虽然复杂但随后得到的结果直觉上比直接填充要好。

异常值处理

实际数据中可能会发现某个或某些字段(特征)根据某个变量(如时间序列问题中的时间)排序后,经观察存在一些数值远高于或低于其一定范围内的其他数值。还有些不合理的存在,这些都可以视作异常值,他们可能会给算法性能带来负面影响。

  1. 寻找异常值

首先,找到异常值,总结了两种方法:

  • 通过可视化分析。简单使用散点图(Matplotlib),严重偏离密集区域的点都可当作异常值来处理
  • 通过简单的统计分析,即根据基本的统计方法来判断数据是否异常,例如四分位数间距、极差、均差、标准差等,这种方法适合于挖掘单变量的数值型数据。(seaborn库的箱型图)
  1. 处理异常值
  • 删除含有异常值的记录。优点:可消除含有异常值的样本带来的不确定性。缺点:减少了样本量
  • 视为缺失值。优点:将异常值集中化为一类,增加了数据的可用性。缺点:将异常值和缺失值混为一谈,会影响数据的准确性、
  • 平均值(中位数修正)。用对应同类别的数值使用平均值修正该异常值。优缺点同“视为缺失值”
  • 不处理。直接在有异常值的数据集上进行数据挖掘。这就听天由命看异常值来源了。

离散型异常值(离散属性定义范围以外的所有值均为异常值)、知识型异常值(如大学生脱发情况:从无)等,都可以当做类别缺失值来处理。

  1. 优化内存

数据集太大而自己的硬件条件有限就有可能会因为内存不够导致代码出现memory error,介绍Python的内存回收机制和数值类型优化这两种有助于优化内存的常见方法。

  • 内存回收机制:在Python的内存回收机制中,gc模块主要运用“引用计数”来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记清除”来解决容器对象可能产生的循坏引用问题,通过“隔代回收”以空间换取时间来进一步提高垃圾回收的效率。一般来讲,在我们删除一些变量时,使用gc.collect()来释放内存。(慎用)
  • 数值类型优化。竞赛中常使用的数据保存格式是csv以及txt,在进行处理时,需要将其读取为表格型数据,即DataFrame格式。需要利用pandas进行操作。pandas可以在底层将数值型数据表示成NumPy数组,并使其在内存中连续存储。这种存储方式不仅消耗的空间较少,还使我们能够快速访问数据。

我们可以用np.iinfo类来确认每一个int型子类型的最大值和最小值

import numpy as npnp.iinfo(np.int8).minnp.iinfo(np.int8).max
  • 对于类别型的变量,若其编码ID的数字较大、极不连续且种类较少,则可以从0开始编码(自然数编码),这样可以减少变量的内存占用。
  • 对于数值型的变量,常常由于存在浮点数使得内存占用过多,可以考虑先将其最小值和最大值归一化,然后再乘以100、1000等,再取整,节省内存空间。

特征变换

连续变量无量纲化

无量纲化指的是将不同规格的数据转换到同一规格。常见无量纲化方法有标准化和区间缩放法。标准化的前提是特征值服从正态分布,标准化后,特征值服从标准正态分布。区间缩放法利用了边界信息,将特征的取值区间缩放到某个特定的范围,例如[0,1]

单特征转换是构建一些模型(如线性回归、KNN、神经网络)的关键,对于决策树相关模型并无影响。还有些纯粹的工程原因,即在进行回归预测时,对目标取对数处理,不仅可以缩小数据范围,而且压缩变量尺度使数据更平稳。

然而,数据要求不仅是通过参数化方法施加的。如果特征没有被规范化,例如当一个特征的分布位于0附近且范围不超过(-1,1),而另一个特征的分布范围在数十万数量级时,会导致分布于0附近的特征变得完全无用。

  • 标准化:最简单的转换是标准化(零-均值规范化)。标准化需要计算特征的均值和标准差。
  • 区间缩放:区间缩放思路有很多种,常见的一种使利用最大最小值进行缩放。
    2.2 连续变量数据变换
    1.log变换
    进行log变换可以将倾斜数据变得接近正态分布,因为大多数机器学习模型不能很好地处理非正态分布数据,比如右倾数据。可以应用log(x+1)变换来修正倾斜,其中+1的目的是防止数据等于0,同时保证x都是正的。取对数不会改变数据的性质和相关关系,但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。

扩展:cbox-cox变换,一种自动寻找最佳正态分布变换函数的方法。

连续变量数据变换

log变换可以将倾斜数据变得接近正态分布。

离散化后的特征对异常数据有很强的健壮性,更便于探索数据的相关性。常用的离散化分为无监督和有监督两种。

无监督的离散化分桶操作可以将连续变量离散化,同时使数据平滑,即降低噪声的影响。一般分为等频和等距两种分桶方式。

  • 等频:区间的边界值要经过选择,使得每个区间包含数量大致相等的变量实例。这种分桶方式可以将数据变成均匀分布。
  • 等距:将实例从最小到最大值,均分为N等份,每份的间距是相等的。这里只考虑边界,每等份的实例数量可能不等。等距可以保持数据原有的分布,并且区间越多,对数据原貌保持得越好。

有监督的离散化对目标有很好的区分能力,常用的是使用树模型返回叶子节点来进行离散化。如在GBDT+LR经典模型中,就是先使用GBDT来将连续值转化为离散值。具体方法:用训练集中的所有连续值和标签输出来训练LightGBM,共训练两棵决策树,第一棵有4个叶子节点,第二棵树有3个叶子节点。如果某一个样本落在第一棵树的第三个叶子节点上,落在第二棵树的第一个叶子节点上,那么它的编码就是0010100,一共7个离散特征,其中会有两个取值为1的位置,分别对应每棵树中样本落点的位置。最终我们会获得num_trees*num_leaves维特征。

类别特征转换

在实际数据中,特征并不总是数值,还有可能是类别。对于离散型的类别特征进行编码,一般分为两种情况:自然数编码(特征有意义)和独热(one-hot)编码(特征无意义)。

自然数编码:一列有意义的类别特征(即有顺序关系)可以使用自然数进行编码,利用自然数的大小关系可以保留其顺序关系。以下是两种自然数编码的常用方式:

①调用sklearn中函数:

from sklearn import preprocessingfrom f in columns:le = preprocessing.LableEncoder()le.fit(data[f})

②自定义实现(速度快)

for f in columns:data[f] = data[f].fillna(-999)data[f] = data[f].map(dict(zip(data[f].unique(),range(0,data[f].nunique()))))

独热编码:当类别特征没有意义(即无顺序关系)时,需要使用独热编码。例如,红>蓝>绿不代表任何东西,进行独热编码后,每个特征的取值对应一维特征,最后得到一个样本数×类别数大小的0~1矩阵。可直接调用sklearn中API进行生成(或者是使用哑变量的方式)

不规则特征变换

不规则特征可能包含样本的很多信息,比如身份证号,各段表示不同的信息。一般不会提供这种比较敏感的信息。

特征提取

机器学习模型很难识别复杂的模式,特别是很难学习到不同特征组合交叉的信息,所以我们可以基于对数据集的直觉分析和业务理解创建一些特征来帮助模型有效学习。下面我们将介绍结构化数据的特征提取方式。

(结构化数据由明确定义的数据类型组成,非结构化数据由音频、视频和图片等不容易搜索的数据组成。)

类别相关的统计特征

类别特征又可以称为离散特征,除了每个类别属性的特定含义外,还可以构造连续型的统计特征,以挖掘更多有价值的信息,比如构造目标编码、count、nunique和ratio等特征。另外,也可以通过类别特征间的交叉组合构造更加细粒度的特征。

  1. 目标编码

目标编码可以理解为用目标变量(标签)的统计量对类别特征进行编码,即根据目标变量进行有监督的特征构造。如果是分类问题,可以统计目标均值、中位数和最值。目标编码的方式可以很好地替代类别特征,或者作为新特征。

使用目标变量时,非常重要的一点是不能泄露任何验证集的信息。所有基于目标编码的特征都应该在训练集上计算,测试集则由完整的训练集来构造。更严格一点,我们在统计训练集的特征时,需要采用K折交叉统计法构造目标编码特征,从而最大限度地防止信息泄露。如用五折交叉统计构造特征时,我们将样本划分为五份,对于其中每一份数据,我们都将用另外四份来计算每个类别取值对应目标变量的频次、比例或者均值,简单来说就是未知的数据(一份)在已知的数据(四份)里面取特征。

目标编码方法对于基数较低的类别特征通常很有效,但对于基数较高的类别特征,可能会有过拟合的风险。因为会存在一些类别出现频次非常低,统计出来的结果不具有代表性。一般会加入平滑性来降低过拟合风险。在处置妥当的情况下,无论是线性模型,还是非线性模型,目标编程都是最佳的编码方式和特征构造方式。

  1. count nunique ratio

count:计数特征,用于统计类别特征的出现频次

nunique和ratio常常会涉及多个类别特征的联合构造。例如在广告点击率预测问题中,对于用户ID和广告ID,使用nunique可以反映用户对广告的兴趣宽度,也就是统计用户ID看过几种广告ID;使用ratio可以反映用户对某类广告的偏好程度,即统计用户ID点击某类广告ID的频次占用户点击所有广告ID频次的比例。

  1. 类别特征之间交叉组合

交叉组合能够描述更细粒度的内容。对类别特征进行交叉组合在竞赛中是一项非常重要的工作,这样可以进行很好的非线性特征拟合。如用户年龄和用户性别可以组合成“年龄_性别”这样的新特征。一般我们可以对两个类别或三个类别特征进行组合,也称作二阶组合或三阶组合。简单来讲,就是对两个类别特征进行笛卡尔积的操作,产生新的类别特征。

并非所有组合都是需要考虑的,我们会从两个方面进行分析。

  • 业务逻辑方面:比如用户操作系统版本与用户所在城市的组合是没有实际意义的。
  • 类别特征的基数:如果基数过大,那么可能导致很多类别只会出现一次,在一轮训练中,每个类别只会被训练一次,显然特征对应权重的置信度是很低的。

数值相关的统计特征

这里所说的数值特征,我们认为是连续的。数值特征的大小是有意义的,通常不需要处理就可以直接“喂”给模型进行训练。除了之前对数值特征进行各种变换外,还存在一些其他常见的数值特征构造方式。

  • 数值特征之间的交叉组合:一般对数值特征进行加减乘除等算术操作类的交叉组合。这需要我们结合业务理解和数据分析进行构造。
  • 类别特征和数值特征之间的交叉组合:除了类别特征之间和数值特征之间的交叉组合外,还可以构造类别特征与数值特征之间的交叉组合。这类特征通常是在类别特征的某个类别中计算数值特征的一些统计量,比如均值、中位数和最值等。
  • 按行统计相关特征:行统计在构造时会包含更多的列,直接对多列进行统计。

时间特征

在实际数据中,通常给出的时间特征是时间戳属性,所以首先需要将其分离成多个维度,比如年月日小时分钟秒钟。如果你的数据源来自于不同的地理数据源,还需要利用时区将数据标准化。除了分离出来的基本时间特征外,还可以构造时间差特征,即计算出各个样本的时间与未来某一个时间的数值差距,这样这个差距是UTC的时间差,从而将时间特征转换为连续值,比如用户首次行为日期与用户注册日期的时间差、用户当前行为与用户上次行为的时间差等。

多值特征

在竞赛中,可能会遇到某一列特征中每行都包含多个属性的情况,这就是多值特征。例如广告大赛中的兴趣类目,其中包含5个兴趣特征组,每个兴趣特征组都包含若干个兴趣ID。对于多值特征,通常可以进行稀疏化或者向量化的处理,这种操作一般出现在自然语言处理中,比如文本分词后使用TF-IDF(词频-逆文档频率)、LDA(隐含狄利克雷分布)、NMF(非负矩阵分解)等方式进行处理,这里则可以将多值特征看作文本分词后的结果,然后做相同的处理。

对多值特征最基本的处理办法是完全展开,即把这列特征所包含的n个属性展开成n维稀疏矩阵。使用sklearn中的CountVectorizer函数,可以方便地将多值特征展开,只考虑每个属性在这个特征的出现频次。

还有一种情况,比如在广告算法大赛中,需要根据用户的历史点击行为预测用户的属性标签。这时候用户的点击序列尤为重要,当我们构造好用户对应的历史点击序列后,除了使用上述的TF-IDF等方法外,还可以提取点击序列中商品或广告的嵌入表示,比如用Word2Vec、DeepWalk等方法获取embedding向量表示。因为要提取用户单个特征,所以可以对序列中的嵌入向量进行聚合统计,这种方法本质上是假设用户点击过的商品或广告等同重要,是一种比较粗糙的处理方式。我们可以引入时间衰减因素,或者使用序列模型,如RNN、LSTN、GRU,套用NLP的方法进行求解。

特征选择

当我们添加新特征时,需要验证它是否确实能够提高模型预测的准确度,以确定不是加入了无用的特征,因为这样只会增加算法运算的复杂度,这时候就要通过特征选择算法自动选择出特征集中的最优子集,帮助模型提供更好的性能。特征选择算法用于从数据中识别并删除不需要、不相关以及冗余特征。这些特征可能会降低模型的准确度和性能,特征选择的方法主要有先验的特征关联性分析以及后验的特征重要性分析。、

特征关联性分析

特征关联性分析是使用统计量来为特征之间的相关性进行评分。特征按照分数进行排序,要么保留,要么从数据集中删除。关联性分析方法通常是针对单变量的,并独立考虑特征或者因变量。常见的特征关联性分析方法有皮尔逊相关系数、卡方检验、互信息法和信息增益等。这些方法速度快、使用方便,但是忽略了特征之间的关系,以及特征和模型之间的关系。

  1. 皮尔逊相关系数

不仅可以衡量变量之间的线性相关性,解决共线变量问题,还可以衡量特征与标签的相关性。共线变量是指变量之间存在高度相关关系,这会降低模型的学习可用性,可解释性以及测试集的泛化性能。但这三个特性都是我们想增加的,所以删除共线变量是一个有价值的步骤。我们将为删除共线变量建立一个基本的阈值(根据想要保留的特征数量决定)。

下面代码用于解决特征与标签不具有相关性的问题,根据皮尔逊相关系数的计算提取top300的相似特征:

def feature_select_pearson(train,features):featureSelect = features[:]#进行皮尔逊相关性计算corr=[]for feat in featureSelect:corr.append(abs(train[[feat,'target']].fillna(0).corr().values[0][1]))se = pd.Series(corr,index=featureSelect).sort_values(ascending=False)feature_select = se[:300}.index.tolist()#返回特征选择后的训练集return train[feature_select]
  1. 卡方检验

用于检验特征变量与因变量的相关性。对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验恰好可以进行独立性检验,所以使用与特征选择。如果检验结果是某个特征与标签独立,则可以去除该特征。

  1. 互信息法

互信息是对一个联合分布中两个变量之间相互影响关系的度量,也可以用于评价两个变量之间的相关性。互信息法之所以能够用于特征选择,可以从两个角度进行解释:基于KL散度和基于信息增益。互信息越大说明两个变量相关性越高。

但是想把互信息直接用于特征选择其实不太方便,由于:

  • 它不属于度量方式,也没有办法归一化,无法对不同数据集上的结果进行比较
  • 对于连续变量的计算不是很方便(X和Y都是集合,xi和y都是离散的取值),通常连续变量需要先离散化,而互信息的结果对离散化的方式很敏感。

特征重要性分析

在实际竞赛中,经常用到的一种特征选择方法是基于树模型评估特征的重要性分数。特征的重要性分数越高,说明特征在模型中被用来构建决策树的次数越多。这里我们以XGBoost为例来介绍树模型评估特征重要性的三种计算方法(weight、gain和cover)。(LightGBM也可以返回特征重要性)

  • weight计算方式:该方式比较简单,计算特征在所有树中被选为分裂特征的次数,并将以此作为评估特征重要性的依据
params ={'max_depth':10,'subsample':1,'verbose_eval':True,'seed':12,'objective':'binary:logistic'}xgtrain = xgb.DMatrix(x,label=y)bst = xgb.train(params,xgtrain,numm_boost_round=10)importance = bst.get_score(fmap='',importance_type='weight')
  • gain计算方式:gain表示平均增益。在进行特征重要性评估时,使用gain表示特征在所有树中作为分裂节点的信息增益之和再除以该特征出现的频次。
importance =bst.get_score(fmap='',importance_type='gain')
  • cover计算方式:cover是特征对每棵树的覆盖率,即特征被分到该节点的样本的二阶导数之和,而特征度量的标准就是平均覆盖率值。
importance = bst.get_score(fmap='',importance_type='cover')

技巧:虽然特征重要性可以帮助我们快速分析特征在模型训练过程中的重要性,但不能将其当做绝对的参考依据。一般而言,只要特征不会导致过拟合,我们就可以选择重要性高的特征进行分析和扩展,对于重要性低的特征,可以考虑将之从特征集中移除,然后观察线下效果,再做进一步判断。

封装方法

可以将一组特征的选择视作一个搜索问题,在这个问题中,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集,搜索过程可以是系统性的,比如最佳优先搜索;也可以是随机的,比如随机爬山算法,或者启发式方法,比如通过向前和向后搜索来添加和删除特征(类似前剪枝和后剪枝算法)。这种方法比较耗时。

  • 启发式方法:分为前向搜索和后向搜索。前向搜索是每次增量地从剩余未选中的特征中选出一个并将其加入特征集中,待特征集中的特征数量达到初设阈值时,意味着贪心选出了错误率最小的特征子集。既然有增量加,就会有增量减,后者称为后向搜索,即从特征全集开始,每次删除其中的一个特征并评价,知道特征集中的特征数量达到初设阈值,就选出了最佳的特征子集
    • 因为启发式方法会导致局部最优,所以加入模拟退火方式进行改善,这种方式不会因为新加入的特征不能改善效果而舍弃该特征,而是对其添加权重后放入已选特征集。这种启发式方法是很耗时间耗资源的。
  • 递归消除特征法:用一个基模型来进行多轮训练,每轮训练都会先消除若干权值系数的特征,再基于新特征集进行下一轮训练。可以使用feature_selection库的RFE类来进行特征选择
from sklearn.feature_selection import RFEfrom sklearn.linear_model import LogisticRegression#递归消除特征法,返回特征选择后的数据#参数estimator为基模型#参数n_feature_to_select 为选择的特征个数RFE(estimator=LogisticRegression(),n_features_to_select=2).fit_transform(data,target)

技巧:在使用封装方法进行特征选择时,用全量数据训练并不是最明智的选择。应先对大数据进行采样,再对小数据使用封装方法

以上三种特征选择方法按需使用或组合使用,建议优先考虑特征重要性,其次是特征关联性。

此外还有null importance。其思想:将构建好的特征和正确的标签喂给树模型得到一个特征重要性分数,再将特征和打乱后的标签喂给树模型得到一个特征重要性分数,然后对比两个分数,如果前者没有超过后者,那么这个特征就是一个无用的特征。

第5章 模型训练

参考资料 :《机器学习算法竞赛实战》整理 | 五、模型训练

线性模型

Lasso回归

Lasso回归是对普通的线性回归采用L1正则化进行优化,通过惩罚或限制估计值的绝对值之和,可以使某些系数为零,从而达到特征稀疏化和特征选择的效果。当我们需要一些自动的特征、变量选择,或者处理高度相关的预测因素时,很方便。

from sklearn.linear_model import Lassolasso_model = Lasso(alpha = 0.1, normalize = True)

只保留不相关的特征,其他为0,可能会导致信息损失

Ridge回归

Ridge回归是对普通的线性回归采用L2正则化进行优化,对特征的权重系数设置了惩罚项。

from sklearn.linear_model import Ridgeridge_model = Ridge(alpha = 0.05, normalize = True)

不会减少特征数量,不利于特征缩减。

两者合并:Elastic Net Regression

树模型

本节将介绍竞赛中常见的树模型,这些模型简单易用,能够带来高收益。

可将树模型分为随机森林(Random Forest, RF)和梯度提升树(GBDT), 这两者最大的差异是前者并行、后者串行。在梯度提升树部分我们将介绍如今竞赛中大火的三种树模型: XGBoost、 LightGBM 和CatBoost。能够灵活运用这三种模型是竞赛中的必备技能。接下来将详细介绍各种树模型的数学形式、优缺点、使用细节和应用场景。

随机森林

随机森林就是通过集成学习的思想将多个决策树集成在一起,各个决策树之间没有任何关联。随机森林算法对多个决策树的结果进行投票得到最终结果,也是最简单的bagging思想 。

随机森林的优点:

  • 不仅可以解决分类和回归问题,还可以同时处理类别特征和数值特征;
  • 不容易过拟合,通过平均决策树的方式,降低过拟合的风险;
  • 非常稳定,即使数据集中出现了一个新的数据点,整个算法也不会受到过多影响,新的数据点只会影响到一棵决策树,很难对所有决策树都产生影响。

很多缺点都是相对而言的:

  • 随机森林算法虽然比决策树算法更复杂,计算成本更高,但是其拥有天然的并行特性,在分布式环境下可以很快地训练。
  • 梯度提升树需要不断地训练残差,进行所以结果准确度更高,但是随机森林更不容易过拟合,更加稳定,这也是因为其Bagging的特性。
from sklearn.ensemble import RandomForestClassifierrf = RandomForestClassifier(max_ features=' auto', oob_ score=True, random state=1, n_ jobs=-1)

梯度提升树

梯度提升树(GBDT)是基于Boosting改进而得的,在Boosting算法中,一系列基学习器都需要串行生成,每次学习一棵树,学习目标是上棵树的残差。和AdaBoost 一样,梯度提升树也是基于梯度下降函数。梯度提升树算法已被证明是Boosting算法集合中最成熟的算法之一,它的特点是估计方差增加,对数据中的噪声更敏感(这两个问题都可以通过使用子采样来减弱),以及由于非并行操作而导致计算成本显著,因此要比随机森林慢很多。

梯度提升树是XGBoost、LightGBM和CatBoost的基础。

XGBoost

  • 采用稀疏感知算法,XGBoost可以利用稀疏矩阵,节省内存(不需要密集矩阵)和节省计算时间(零值以特殊方式处理)。
  • 近似树学习(加权分位数略图),这类学习方式能得到近似的结果,但比完整的分支切割探索要省很多时间。
  • 在一台机器上进行并行计算(在搜索最佳分割阶段使用多线程),在多台机器上进行类似的分布式计算。
  • 利用名为核外计算的优化方法,解决在磁盘读取数据时间过长的问题。将数据集分成多个块存放在磁盘中,使用一个独立的线程专门从磁盘读取数据并加载到内存中,这样一来,从磁盘读取数据和在内存中完成数据计算就能并行运行。
  • XGBoost还可以有效地处理缺失值,训练时对缺失值自动学习切分方向。基本思路是在每次的切分中,让缺失值分别被切分到决策树的左节点和右节点,然后通过计算增益得分选择增益大的切分方向进行分裂,最后针对每个特征的缺失值,都会学习到一个最优的默认切分方向。
import xgboost as xgbparams = {'eta': 0.01, ' max depth': 11, 'objective': 'reg:linear', 'eval_ metric': 'rmse' }dtrain = xgb.DMatrix(data=X_train, label=y_train)dtest = xgb.DMatrix(data=X_valid, label=y_valid)watchlist = [(train.data, 'train'), (valid_data, 'valid_ data')]model=xgb. train(params, train_data,num_boost_round=20000,evals=watchlist,early_stopping_rounds=200,verbose_eval=500)y_pred = model. predict(xgb .DMatrix(X_test), ntree_limit=model.best_ntree_limit)

LightGBM

LightGBM是微软的一个团队在Github上开发的一个开源项目,高性能的LightGBM算法具有分布式和可以快速处理大量数据的特点。LightGBM虽然基于决策树和XGBoost而生,但它还遵循其他不同的策略。

XGBoost使用决策树对一个变量进行拆分,并在该变量上探索不同的切割点(按级别划分的树生长策略),而LightGBM则专注于按叶子节点进行拆分,以便获得更好的拟合(这是按叶划分的树生长策略)。这使得LightGBM能够快速获得很好的数据拟合,并生成能够替代XGBoost的解决方案。从算法上讲,XGBoost将决策树所进行的分割结构作为一个图来计算,使用广度优先搜索(BFS),而LightGBM使用的是深度优先搜索(DFS)。

主要特点

  • 比XGBoost准确性更高,训练时间更短。
  • 支持并行树增强,即使在大型数据集上也能提供比 XGBoost更好的训练速度。
  • 通过使用直方图算法将连续特征提取为离散特征,实现了惊人的快速训练速度和较低的内存使用率。
  • 通过使用按叶分割而不是按级别分割来获得更高精度,加快目标函数收敛过程,并在非常复杂的树中捕获训练数据的底层模式。使用num_leaves和max_depth超参数控制过拟合。
import lightgbm as lgbparams = {'num_leaves': 54, 'objective': 'regression', 'max_depth': 18,'learning_rate': 0.01, 'boosting': 'gbdt', 'metric': 'rmse', 'lambda_11': 0.1}model = lgb.LGBMRegressor(**params, n_estimators = 20000, nthread = 4, n_jobs = -1)model.fit(x_train, y_train, eval_set=[(X_train, y_train), (X_valid, y_valid)], eval_metric='rmse', verbose=1000, early_stopping_rounds=200)y_pred= model.predict(X_test, num_iteration=model.best_iteration_)

CatBoost

CatBoost是由俄罗斯搜索引擎Yandex在2017年7月开源的一个GBM算法,它最强大的点是能够采用将独热编码和平均编码混合的策略来处理类别特征。

CatBoost用来对类别特征进行编码的方法并不是新方法,是均值编码,该方法已经成为一种特征工程方法,被广泛应用于各种数据科学竞赛中,如Kaggle。

均值编码,也称为似然编码、影响编码或目标编码,可将标签转换为基于它们的数字,并与目标变量相关联。如果是回归问题,则基于级别典型的平均目标值转换标签;如果是分类问题,则仅给定标签的目标分类概率(目标概率取决于每个类别值)。均值编码可能看起来只是一个简单而聪明的特征工程技巧,但实际上它也有副作用,主要是过拟合,因为会把目标信息带入预测中。

主要特点

  • 支持类别特征,因此我们不需要预处理类别特征(例如通过label encoding或独热编码)。事实上,CatBoost文档中讲到不要在预处理期间使用独热编码,因为“这会影响训练速度和结果质量”。
  • 提出了一种全新的梯度提升机制(Ordered Boosting),不仅可以减少过拟合的风险,也大大提高了准确性。
  • 支持开箱即用的GPU训练(只需设置task_type=“GPU”)。
  • 训练中使用了组合类别特征,利用了特征之间的联系,极大丰富了特征维度。
  • 在树分裂选择节点的时候能够将所有类别特征之间的组合考虑进来,即能够对两个类别特征进行组合。
  • 目前还支持输入文本特征,因此不需要像以前那样先进行烦琐的操作获得标准化输入,再喂给模型。
from catboost import CatBoostRegressorparams = {'learning_rate': 0.02, 'depth': 13,'bootstrap_type': 'Bernoulli', 'od_type': 'Iter', 'od_wait': 50, 'random_seed': 11}model = CatBoostRegressor(iterations=20000, eval_metric='RMSE', **params)model.fit(X_train, y_train, eval_set=(X_valid, y_valid), cat_features=[], use_best_model=True, verbose=False)y_pred = model.predict(X_test)

模型深入对比

每类树模型都其与众不同的地方,接下来将从决策树的生长策略、梯度偏差、类别特征处理和参数对比四个方面深入理解这些树模型,帮助参赛者更好地将它们应用到竞赛中。
XGBoost,LightGBM 和 CatBoost是三个非常核心的树模型,本节将对它们进行分析,因为三者之间有着千丝万缕的关系,只有厘清其中的关系,才能更好地运用这三个模型。

  1. 决策树生长策略
  • XGBoost使用的是Level-wise按层生长,可以同时分裂同一层的叶子,从而进行多线程优化,不容易过拟合,但很多叶子节点的分裂增益较低,会影响性能。
  • LightGBM使用的是Leaf-wise分裂方式,每次都从当前叶子中选择增益最大的结点进行分裂,循环迭代,但会生长出非常深的决策树,从而导致过拟合,这时可以调整参数max_depth来防止过拟合。
  • CatBoost 使用的是oblivious-tree(对称树),这种方式使得节点是镜像生长的。相对于传统的生长策略,oblivious-tree能够简单拟合方案,快速生成模型,这种树结构起到了正则化的作用,因此并不容易过拟合。
  1. 梯度偏差(Gradient bias)
  • XGBoost和LightGBM中的提升树算法都是有偏梯度估计,在梯度估计中使用的数据与目前建立的模型所使用的数据是相同的,这样会导致数据发生泄漏,从而产生过拟合。
  • CatBoost改进了提升树算法,将原来的有偏梯度估计转换为了无偏梯度估计。具体做法是利用所有训练集(除第i条)建立模型,然后使用第1条到第i-1条数据来建一个修正树M,累加到原来的模型上。
  1. 类别特征处理
  • XGBoost并不能处理类别特征,因此需要我们根据数据实际情况进行独热编码、count编码和目标编码。
  • LightGBM 直接支持类别特征,不需要独热展开。这里使用many-vs-many的切分方式来处理类别特征,并且可以把搜索最佳分割点的时间复杂度控制在线性级别,和原来one-vs-other方式的时间复杂度几乎一致。该算法先按照每个类别对应的标签均值(即avg(y)=Sum(y)/Count(y))进行排序,然后根据排序结果依次枚举最优分割点。和数值型特征的切分方式不同,它是将某一类别当作一类,然后将其余所有类别作为一类。
  • CatBoost在处理类别特征方面做了更细致的操作。或许在使用LightGBM时,还需要对类别特征进行更多的编码方式,但对于CatBoost,则可以选择不进行多余的编码方式。具体实现流程是首先对输入的样本集随机排序,然后针对类别特征中的某个取值,在将每个样本的该特征转换为数值型时,都基于排在该样本之前的类别标签取均值。对所有的类别特征值结果都进行如式(5-10)所示的运算,使之转化为数值结果,
  1. 参数对比

xPuQVP.png

神经网络

随着拥有数据量的增加,神经网络战胜传统机器学习模型的可能性也会加大。

  • 多层感知机:含有多个隐藏层的神经网络
  • 卷积神经网络 :广泛应用于计算机视觉领域
  • 循环神经网络:更擅长对序列数据进行建模处理

实战案例(未实际运行)

#接第5章实战案例代码,构造训练集和测试集x_train = data[:ntrain][all_cols]x_test = data[ntrain:][all_cols]#对售价进行log处理y_train = np.log1p(data[data.SalePrice.notnull()]['SalePrice'].values)

XGBoost:使用比较常规的五折交叉验证

import xgboost as xgbfrom sklearn.model_selection import KFoldkf = KFold(n_splits=5,shuffle=True,random_state=2020)for i,(train_index,valid_index)in enumerate(kf.split(x_train,y_train)):    trn_x,trn_y,val_x,val_y = x_train.iloc[train_index],y_train[train_index],x_train.iloc[valid_index],y_train[valid_index]    params ={'eta':0.01,'max_depth':11,'objective':'reg:linear','eval_metric':'mae'}    dtrain = xgb.DMatrix(data=trn_x,label=trn_y)    dtest = xgb.DMatrix(data=val_x,label=val_y)    watchlist =[(dtrain,'train'),(dtest,'valid_data')]    model=xgb.train(params,dtrain,num_boost_round=20000,evals=watchlist,early_stopping_rounds=200,verbose_eval=500)

多层感知机:要确保数据中没有缺失值,并且要对数据进行归一化处理。

from sklearn. model_selection import train_test_splitfrom sklearn.preprocessing import StandardScalerx_train = x_train. fillna(0)x_train = StandardScaler(). fit_transform(x_train)trn_x, val_x, trny, val_y = train_test_split(x_train, y_train, random_state=2020)def create_mlp(shape):    x_input = Input((shape, ))    X = Dropout(0.2)(BatchNormalization()(        Dense(256, activation=' relu')(X_input)))    X = Dropout(0.2)(BatchNormalization()(Dense(128, activation=' relu')(X)))    X = Dropout(0.2)(BatchNormalization()(Dense(64, activation=' relu')(X)))    X = Dense(1)(X)    model = Model(inputs=X_input, outputs=X)    model. compile(optimizer=' adam', loss=' mse', metrics=[' mae'])    return modelmlp_model = create_mlp(trn_x. shape[1])mlp_model.fit(x=trn_x, y=trn_y, validation_data=(val_x, val_y), epochs=30, batch_size=16)

第6章 模型融合

参考资料:《机器学习算法竞赛实战》整理 | 六、模型融合

本章将向大家介绍在算法竞赛中提分的关键步骤,这也是最后阶段的惯用方法,即模型融合(或者集成学习),通过结合不同子模型的长处进行模型融合,当然这是在理想状态下。

本章主要分为构建多样性、训练过程融合和训练结果融合三部分。

模型融合常常是竞赛取得胜利的关键,相比之下具有差异性的模型融合往往能给结果带来很大提升。了解的模型融合方法越多,最后取胜的概率就会越高。

本章从这三个部分介绍不同模型融合方法的应用场景,同时给出使用技巧和应用代码。

构建多样性

介绍三种模型融合中构建多样性的方式,分别是特征多样性、样本多样性和模型多样性。其中多样性是指子模型之间存在着差异,可以通过降低子模型融合的同质性来构建多样性,好的多样性有助于模型融合效果的提升。

特征多样性

构建多个有差异的特征集并分别建立模型,可使特征存在于不同的超空间(hyperspace),从而建立的多个模型有不同的泛化误差,最终模型融合时可以起到互补的效果。在竞赛中,队友之间的特征集往往是不一样的,在分数差异不大的情况下,直接进行模型融合基本会获得不错的收益。

另外,像随机森林中的max_features,XGBoost中的colsample_bytree 和LightGBM中的feature_fraction都是用来对训练集中的特征进行采样的,其实本质上就是构建特征的多样性。

样本多样性

样本多样性也是竞赛中常见的一种模型融合方式,这里的多样性主要来自不同的样本集。

具体做法是将数据集切分成多份,然后分别建立模型。我们知道很多树模型在训练的时候会进行采样(sampling),主要目的是防止过拟合,从而提升预测的准确性。

有时候将数据集切分成多份并不是随机进行的,而是根据具体的赛题数据进行切分,需要考虑如何切分可以构建最大限度的数据差异性,并用切分后的数据分别训练模型。

例如,在天池“全球城市计算AI挑战赛”中,竞赛训练集包含从2019年1月1日到1月25日共25天的地铁刷卡数据记录,要求预测1月26日每个地铁站点每十分钟的平均出入客流量(2019年1月26日是周六)。显然,工作日和周末的客流量分布具有很大差异,这时会面临一个问题,若只保留周末的数据进行训练,则会浪费掉很多数据;若一周的数据全部保留,则会对工作日的数据产生一定影响。这时候就可以尝试构建两组有差异性的样本分别训练模型,即整体数据保留为一组,周末数据为一组。当然,模型融合后的分数会有很大提升。

模型多样性

不同模型对数据的表达能力是不同的,比如FM能够学习到特征之间的交叉信息,并且记忆性较强;树模型可以很好地处理连续特征和离散特征(如LightGBM 和CatBoost),并且对异常值也具有很好的健壮性。把这两类在数据假设、表征能力方面有差异的模型融合起来肯定会达到一定的效果。

对于竞赛而言,传统的树模型(XGBoost,LightGBM、CatBoost)和神经网络都需要尝试一遍,然后将尝试过的模型作为具有差异性的模型融合在一起。

还有很多其他构建多样性的方法,比如训练目标多样性、参数多样性和损失函数选择的多样性等,这些都能产生非常好的效果。

训练过程融合

模型融合的方式有两种,第一种是训练过程融合,比如我们了解到的随机森林和XGBoost,基于这两种模型在训练中构造多个决策树进行融合,这里的多个决策树可以看作多个弱学习器。其中随机森林通过Bagging的方式进行融合,XGBoost通过Boosting的方式进行融合。

Bagging

Bagging的思想很简单,即从训练集中有放回地取出数据(Bootstrapping),这些数据构成样本集,这也保证了训练集的规模不变,然后用样本集训练弱分类器。重复上述过程多次,取平均值或者采用投票机制得到模型融合的最终结果。

当在不同的样本集上训练模型时,Bagging通过减小误差之间的差来减少分类器的方差,因此Bagging可以降低过拟合的风险。Bagging算法的效率在于训练数据的不同,各模型之间存在着很大的差异,并且在加权融合的过程中可以使训练数据的错误相互抵消。

Boosting

Boosting的思想其实并不难理解,首先训练一个弱分类器,并把这个弱分类器分错类的样本记录下来,同时给予这个弱分类器一定的权重;然后建立一个新的弱分类器,新的弱分类器基于前面记录的错误样本进行训练,同样,我们也给予这个分类器一个权重。重复上面的过程,直到弱分类器的性能达到某一指标,例如当再建立的新弱分类器并不会使准确率显著提升时,就停止选代。最后,把这些弱分类器各自乘上相应的权重并全部加起来,就得到了最后的强分类器。其实,基于Boosting的算法是比较多的,有Adaboost、LightGBM、XGBoost和CatBoost等。

训练结果融合

模型融合的第二种方式是训练结果融合,主要分为加权法、Stacking和Blending,这些方法都可以有效地提高模型的整体预测能力,在竞赛中也是参赛者必须要掌握的方法。

加权法

加权法对于一系列任务(比如分类和回归)和评价指标(如AUC,MSE 或 Logloss)都是很有效的,比如我们有10个算法模型并都预测到了结果,直接对这10个结果取平均值或者给予每个算法不同的权重,即得到了融合结果。加权法通常还能减少过拟合,因为每个模型的结果可能存在一定的噪声,加权法能够平滑噪声,提高模型的泛化性。

  1. 分类问题:对于分类问题,需要注意不同分类器的输出结果范围一致,因为输出的预测结果可以是0/1值,也可以是介于0和1之间的概率。另外,投票法(Voting)也是一种特殊的加权法。

  2. 回归问题:对于回归问题,使用加权法会非常简单。这里主要介绍算术平均和几何平均。

  • 在2019腾讯广告算法大赛中,选择几何平均的效果远远好于选择算术平均,这是由于评分规则是平均绝对百分比误差(SMAPE),此时如果选择算术平均则会使模型融合的结果偏大,这不符合平均绝对百分比误差的直觉,越小的值对评分影响越大,算术平均会导致出现更大的误差,所以选择几何平均,能够使结果偏向小值。

算术平均:基于算术平均数的集成方法在算法中是用得最多的,因为它不仅简单,而且基本每次使用该算法都有较大概率能获得很好的效果。

几何平均:根据很多参赛选手的分享,基于几何平均数的加权法在算法中使用得还不是很多,但在实际情况中,有时候基于几何平均数的模型融合效果要稍好于基于算术平均数的效果。

  1. 排序问题

一般推荐问题中的主要任务是对推荐结果进行排序,常见的评价指标有mAP(mean Average Precision),NDCG(Normalized Discounted Cumulative Gain),MRR(Mean Reciprocal Rank)和AUC,这里主要介绍MRR和AUC。

MRR:给定推荐结果q,如果q在推荐序列中的位置是r,那么MRR(q)就是1/r。可以看出,如果向用户推荐的产品在推荐序列中命中,那么命中的位置越靠前,得分也就越高。显然,排序结果在前在后的重要性是不一样的,因此我们不仅要进行加权融合,还需要让结果偏向小值。这时候就要对结果进行转换,然后再用加权法进行融合,一般而言使用的转换方式是log变换。
其基本思路如下:首先,输人三个预测结果文件,每个预测结果文件都包含M条记录,每条记录各对应N个预测结果,最终输出三个预测结果文件的整合结果。统计三个预测结果文件中记录的所有推荐商品(共N个商品)出现的位置,例如商品A,在第一份文件中的推荐位置是1,在第二个文件的推荐位置是3,在第三个文件中未出现,此时我们计算商品A的得分为log1+log3+log(N+1),此处我们用N+1来表示未出现,即在N个推荐商品中是找不到商品A的,所以只能是N+1。对每条记录中的商品按计算得分由小到大排序,取前N个作为这条记录的最终推荐结果。

AUC:作为排序指标,一般使用排序均值的融合思路,使用相对顺序来代替原先的概率值。很多以AUC为指标的比赛均取得了非常不错的成绩。使用过程如下:对每个分类器中分类的概率进行排序,然后用每个样本排序之后得到的排名值(rank)作为新的结果。对每个分类器的排名值求算术平均值作为最终结果。

Stacking 融合

使用加权法进行融合虽然简单,但需要人工来确定权重,因此可以考虑更加智能的方式,通过新的模型来学习每个分类器的权重。这里我们假设有两层分类器,如果在第一层中某个特定的基分类器错误地学习了特征空间的某个区域,则这种错误的学习行为可能会被第二层分类器检测到,这与其他分类器的学习行为一样,可以纠正不恰当的训练。上述过程便是Stacking融合的基本思想。

这里需要注意两点:第一,构建的新模型一般是简单模型,比如逻辑回归这样的线性模型;第二,使用多个模型进行Stacking融合会有比较好的结果。

Stacking融合使用基模型的预测结果作为第二层模型的输入。然而,我们不能简单地使用完整的训练集数据来训练基模型,这会产生基分类器在预测时就已经“看到”测试集的风险,因此在提供预测结果时出现过度拟合问题。所以我们应该使用Out-of-Fold的方式进行预测,也就是通过K折交叉验证的方式来预测结果。这里我们将Stacking融合分为训练阶段和测试阶段两部分,将并以流程图的形式展示每部分的具体操作。如图6.2所示为训练阶段。

特征加权的线性堆叠,可参考相应论文“Feature-Weighted Linear Stacking two layer stacking",其实就是对传统的Stacking融合方法在深度上进行扩展。通过传统的Stacking融合方法得到概率值,再将此值与基础特征集进行拼接,重新组成新的特征集,进行新一轮训练。

Blending 融合

不同于Stacking融合使用K折交叉验证方式得到预测结果,Blending融合是建立一个Holdout集,将不相交的数据集用于不同层的训练,这样可以在很大程度上降低过拟合的风险。

假设构造两层Blending,训练集等分为两部分(train_one和train_two),测试集为test。第一层用train_one训练多个模型,将train_two和test的预测结果合并到原始特征集合中,作为第二层的特征集。第二层用train_two的特征集和标签训练新的模型,然后对test预测得到最终的融合结果。

实战案例

以stacking为例。选择ExtraTreesRegressor、RandomForestRegressor、Ridge、Lasso作为基学习器,Ridge为最终分类器。

依然采用5折交叉验证

kf = KFold(n_splits=5, shuffle=True, random_state=2020)

然后构建一个sklearn中模型的功能类,初始化参数然后训练和预测。这段代码可复用性很高,建议完善、储存。

class SklearnWrapper(object):    def __init__(self, clf, seed=0, params=None):        params['random_state'] = seed        self.clf = clf(**params)    def train(self, x_train, y_train):        self.clf.fit(x_train, y_train)    def predict(self, x):        return self.clf.predict(x)

封装交叉验证函数。可复用性也很高。

def get_oof(clf):    oof_train = np.zeros((x_train.shape[0],))    oof_test = np.zeros((x_test.shape[0],))    oof_test_skf = np.empty((5, x_test.shape[0]))      for i, (train_index, valid_index) in enumerate(kf.split(x_train, y_train)):        trn_x, trn_y, val_x, val_y = x_train.iloc[train_index], y_train[train_index],\            x_train.iloc[valid_index], y_train[valid_index]        clf.train(trn_x, trn_y)        oof_train[valid_index] = clf.predict(val_x)        oof_test_skf[i, :] = clf.predict(x_test)    oof_test[:] = oof_test_skf.mean(axis=0)    return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)

预测四个模型的验证集结果和测试集结果。并辅助最后一步的stacking融合操作:

et_params = {   'n_estimators': 100,    'max_features': 0.5,    'max_depth': 12,    'min_samples_leaf': 2,}rf_params = {    'n_estimators': 100,    'max_features': 0.2,    'max_depth': 12,    'min_samples_leaf': 2,}rd_params={'alpha': 10}ls_params={ 'alpha': 0.005}et = SklearnWrapper(clf=ExtraTreesRegressor, seed=2020, params=et_params)rf = SklearnWrapper(clf=RandomForestRegressor, seed=2020, params=rf_params)rd = SklearnWrapper(clf=Ridge, seed=2020, params=rd_params)ls = SklearnWrapper(clf=Lasso, seed=2020, params=ls_params)et_oof_train, et_oof_test = get_oof(et)rf_oof_train, rf_oof_test = get_oof(rf)rd_oof_train, rd_oof_test = get_oof(rd)ls_oof_train, ls_oof_test = get_oof(ls)

最后就是stacking部分,使用ridge模型。

def stack_model(oof_1, oof_2, oof_3, oof_4, predictions_1, predictions_2, predictions_3, predictions_4, y):    train_stack = np.hstack([oof_1, oof_2, oof_3, oof_4])    test_stack = np.hstack([predictions_1, predictions_2, predictions_3, predictions_4])      oof = np.zeros((train_stack.shape[0],))    predictions = np.zeros((test_stack.shape[0],))    scores = []    for fold_, (trn_idx, val_idx) in enumerate(kf.split(train_stack, y)):         trn_data, trn_y = train_stack[trn_idx], y[trn_idx]        val_data, val_y = train_stack[val_idx], y[val_idx]              clf = Ridge(random_state=2020)        clf.fit(trn_data, trn_y)        oof[val_idx] = clf.predict(val_data)        predictions += clf.predict(test_stack) / 5              score_single = sqrt(mean_squared_error(val_y, oof[val_idx]))        scores.append(score_single)        print(f'{fold_+1}/{5}', score_single)    print('mean: ',np.mean(scores))       return oof, predictionsoof_stack , predictions_stack  = stack_model(et_oof_train, rf_oof_train, rd_oof_train, ls_oof_train, et_oof_test, rf_oof_test, rd_oof_test,ls_oof_test, y_train)

实际运行后发现,基分类器的分类效果差别很大,且最终融合后的模型效果确实要比基分类器的模型效果好很多。

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Competition + + + +
+ + + + + 研究生课程:模式识别与机器学习-第4章 特征选择和提取 + + /2022/09/18/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-4/ + + 《模式识别与机器学习》课程笔记:第4章 特征选择和提取

第4章 特征选择和提取

特征选择和提取是模式识别中的一个关键问题,前面讨论分类器设计的时候,一直假定已给出了特征向量维数确定的样本集,其中各样本的每一维都是该样本的一个特征;这些特征的选择是很重要的,它强烈地影响到分类器的设计及其性能;假若对不同的类别,这些特征的差别很大,则比较容易设计出具有较好性能的分类器。

例如,描述人可以用好多特征,如肤色,体重,身高等,但是如果要判断软件工程师,显然编程这个特征比较有判别性;如果要判断是不是篮球员,则体重、身高有很强的判别性。

特征选择和提取是构造模式识别系统时的一个重要课题。在很多实际问题中,往往不容易找到那些最重要的特征,或受客观条件的限制,不能对它们进行有效的测量;因此在测量时,由于人们心理上的作用,只要条件许可总希望把特征取得多一些;另外,由于客观上的需要,为了突出某些有用信息,抑制无用信息,有意加上一些比值、指数或对数等组合计算特征;如果将数目很多的测量值不做分析,全部直接用作分类特征,不但耗时,而且会影响到分类的效果,产生“特征维数灾难”问题。

为了设计出效果好的分类器,通常需要对原始的测量值集合进行分析,经过选择或变换处理,组成有效的识别特征;在保证一定分类精度的前提下,减少特征维数,即进行“降维”处理,使分类器实现快速、准确和高效的分类。为达到上述目的,关键是所提供的识别特征应具有很好的可分性,使分类器容易判别。为此,需对特征进行选择:

  • 应去掉模棱两可、不易判别的特征;
  • 所提供的特征不要重复,即去掉那些相关性强且没有增加更多分类信息的特征。

特征选择和提取这一任务应在设计分类器之前进行;

xpsWjI.png

所谓特征选择,就是从个度量值集合中,按某一准则选取出供分类用的子集,作为降维(维,)的分类特征;

所谓特征提取,就是使通过某种变换,产生个特征 ,作为新的分类特征(或称为二次特征);

其目的都是为了在尽可能保留识别信息的前提下,降低特征空间的维数,以达到有效的分类效果。

模式类别可分性的测度

距离和散布矩阵:

  • 点到点之间的距离:,其中, 维向量, 其第 个分量分别是
  • 点到点集之间的距离:点到点集之间的距离为

类内距离:维空间中同一类内各模式样本点集,其内部各点的均方距离为,其中

类内散布矩阵:考虑一类内模式点集,其类内散布矩阵为:,其中

对属于同一类的模式样本,类内散布矩阵表示各样本点围绕其均值周围的散布情况。

在考虑有两个以上的类别,如集合时,类间距离对类别的可分性起着重要作用,此时应计算

为简化起见,常用两类样本各自质心间的距离作为类间距离,并假设两类样本出现的概率相等,则

其中为两类模式样本集各自的均值向量,的第个分量,为维数。

两类模式的类间散布矩阵:

对三个以上的类别,类间散布矩阵常写成,其中,为多类模式(如共有类)分布的总体均值向量,即

多类情况的类内散布矩阵可写成各类的类内散布矩阵的先验概率的加权和,即,其中是第类的协方差矩阵。

有时,用多类模式总体分布的散布矩阵来反映其可分性,即:,其中为多类模式分布的总体均值向量。

,即总体散布矩阵是各类类内散布矩阵与类间散布矩阵之和。

特征选择

设有个可用作分类的测量值,为了在不降低(或尽量不降低)分类精度的前提下,减小特征空间的维数以减少计算量,需从中直接选出个作为分类的特征。

个测量值中选出个特征,一共有种可能的选法,需寻找一种简便的可分性准则,间接判断每一种子集的优劣。

对于独立特征的选择准则:类别可分性准则应具有这样的特点,即不同类别模式特征的均值向量之间的距离应最大,而属于同一类的模式特征,其方差之和应最小。假设各原始特征测量值是统计独立的,此时,只需对训练样本的个测量值独立地进行分析,从中选出个最好的作为分类特征即可。

对于 两类训练样本,假设其均值向量为 维方向的分量为 ,方差为 ,定义可分性准则函数,则为正值。 值越大,表示测度值的第个分量对分离 类越有效。将按大小排队, 选出最大的个对应测度值作为分类特征,即达到特征选择的目的。

上述基于距离测度的可分性准则,其适用范围与模式特征的分布有关。假若类概率密度函数不是或不近似正态分布,均值和方差就不足以用来估计类别的可分性,此时该准则函数不完全适用。

一般特征的散布矩阵准则:

  • 类内:
  • 类间:

直观上,类间离散度越大且类内离散度越小,则可分性越好。因此,可推导出散布矩阵准则采用如下形式:

  • 行列式形式:
  • 迹形式:

其中, 是矩阵 的特征值。使 最大的子集可作为选择的分类特征。

离散K-L变换(Karhunen-Loeve变换(卡洛南-洛伊变换))

前面讨论的特征选择是在一定准则下,从个特征中选出个来反映原有模式。这种简单删掉某个特征的做法并不十分理想,因为一般来说,原来的个数据各自在不同程度上反映了识别对象的某些特征,简单地删去某些特征可能会丢失较多的有用信息。如果将原来的特征做正交变换,获得的每个数据都是原来个数据的线性组合,然后从新的数据中选出少数几个,使其尽可能多地反映各类模式之间的差异,而这些特征间又尽可能相互独立,则比单纯的选择方法更灵活、更有效。

K-L变换就是一种适用于任意概率密度函数的正交变换。

离散的有限K-L展开

离散的有限K-L展开式的形式:

设一连续的随机实函数,则 可用已知的正交函数集 的线性组合来展开,即:。式中,为展开式的随机系数,为一连续的正交函数,它应满足:,其中的共轭复数式。

将上式写成离散的正交函数形式,使连续随机函数和连续正交函数在区间内被等间隔采样为个离散点,即:

写成向量形式:

将展开式写成离散形式:,其中为展开式中随机系数的向量形式维矩阵,其中,每一列为正交函数集中的一个函数,小括号内的序号为正交函数的采样点次序。因此,实质上是由向量组成的正交变换矩阵,
它将变换成

对各个模式类别,正交函数都是相同的,但其展开系数向量则因类别的不同模式分布而异。

K-L展开式的根本性质是将随机向量展开为另一组正交向量的线性和,且其展开式系数(即系数向量的各个分量)具有不同的性质。

正交向量集的确定:

设随机向量的总体自相关矩阵为,则,要求系数向量的各个不同分量应统计独立,则应使,其中为对角形矩阵,其互相关成分均为0

因为是实对称矩阵,其不同特征值对应的特征向量应正交,即:

K-L展开式系数的计算步骤:

  1. 求随机向量的自相关矩阵:
  2. 求出矩阵的特征值和对应的特征向量,得矩阵:
  3. 计算展开式系数:

按K-L展开式选择特征

K-L展开式用于特征选择相当于一种线性变换。若从个特征向量中取出个组成变换矩阵,即,此时是一个维矩阵,维向量,经过变换,即得到降维为的新向量。

结论

从K-L展开式的性质和按最小均方差的准则来选择特征,应使。由于,故应使。基于这一条件,在将整体模式进行K-L变换之前,应先将其均值作为新坐标轴的原点,采用协方差矩阵或自相关矩阵来计算特征值。如果,则只能得到“次最佳”的结果。

将K-L展开式系数(亦即变换后的特征)用表示,写成向量形式:,此时变换矩阵个特征向量组成。为使误差最小,不采用的特征向量,其对应的特征值应尽可能小。因此,将特征值按大小次序标号,即。若首先采用前面的个特征向量,便可使变换误差最小。此时的变换矩阵为

K-L变换是在均方误差最小的意义下获得数据压缩(降维)的最佳变换,且不受模式分布的限制。对于一种类别的模式特征提取,它不存在特征分类问题,只是实现用低维的个特征来表示原来高维的个特征,使其误差最小,亦即使其整个模式分布结构尽可能保持不变。

通过K-L变换能获得互不相关的新特征。若采用较大特征值对应的特征向量组成变换矩阵,则能对应地保留原模式中方差最大的特征成分,所以K-L变换起到了减小相关性、突出差异性的效果。在此情况下,K-L变换也称为主成分变换(PCA变换)。

需要指出的是,采用K-L变换作为模式分类的特征提取时,要特别注意保留不同类别的模式分类鉴别信息,仅单纯考虑尽可能代表原来模式的主成分,有时并不一定有利于分类的鉴别。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Pattern Recognition and Machine Learning + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第16章 复杂数据结构 + + /2022/09/18/Leetcode/Leetcode-101/Leetcode-101-16/ + + Leetcode 刷题笔记-Leetcode 101 第16章 复杂数据结构

复杂数据结构

并查集

并查集(union-find, 或disjoint set)可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。假设存在n个节点,我们先将所有节点的父亲标为自己;每次要连接节点i和j时,我们可以将i的父亲标为j;每次要查询两个节点是否相连时,我们可以查找i和j的祖先是否最终为同一个人。

Leetcode 684

在无向图找出一条边,移除它之后该图能够成为一棵树(即无向无环图)。如果有多个解,返回在原数组中位置最靠后的那条边。

class Solution {public:    int Find(vector<int>& parent, int index) {        if (parent[index] != index) {            parent[index] = Find(parent, parent[index]);        }        return parent[index];    }    void Union(vector<int>& parent, int index1, int index2) {        parent[Find(parent, index1)] = Find(parent, index2);    }    vector<int> findRedundantConnection(vector<vector<int>>& edges) {        int n = edges.size();        vector<int> parent(n + 1);        for (int i = 1; i <= n; ++i) {            parent[i] = i;        }        for (auto& edge: edges) {            int node1 = edge[0], node2 = edge[1];            if (Find(parent, node1) != Find(parent, node2)) {                Union(parent, node1, node2);            } else {                return edge;            }        }        return vector<int>{};    }};

分析:在一棵树中,边的数量比节点的数量少1。如果一棵树有n个节点,则这棵树有n−1条边。这道题中的图在树的基础上多了一条附加的边,因此边的数量也是n。树是一个连通且无环的无向图,在树中多了一条附加的边之后就会出现环,因此附加的边即为导致环出现的边。可以通过并查集寻找附加的边。初始时,每个节点都属于不同的连通分量。遍历每一条边,判断这条边连接的两个顶点是否属于相同的连通分量。如果两个顶点属于不同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间不连通,因此当前的边不会导致环出现,合并这两个顶点的连通分量。如果两个顶点属于相同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,将当前的边作为答案返回。

错误:不知道怎么使用并查集

复合数据结构

Leetcode 146

请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。

class LRUCache {public:    //定义双链表    struct Node{        int key,value;        Node* left ,*right;        Node(int _key,int _value): key(_key),value(_value),left(NULL),right(NULL){}    }*L,*R;//双链表的最左和最右节点,不存贮值。    int n;    unordered_map<int,Node*>hash;    void remove(Node* p)    {        p->right->left = p->left;        p->left->right = p->right;    }    void insert(Node *p)    {        p->right = L->right;        p->left = L;        L->right->left = p;        L->right = p;    }    LRUCache(int capacity) {        n = capacity;        L = new Node(-1,-1),R = new Node(-1,-1);        L->right = R;        R->left = L;      }      int get(int key) {        if(hash.count(key) == 0) return -1; //不存在关键字 key         auto p = hash[key];        remove(p);        insert(p);//将当前节点放在双链表的第一位        return p->value;    }      void put(int key, int value) {        if(hash.count(key)) //如果key存在,则修改对应的value        {            auto p = hash[key];            p->value = value;            remove(p);            insert(p);        }        else         {            if(hash.size() == n) //如果缓存已满,则删除双链表最右侧的节点            {                auto  p = R->left;                remove(p);                hash.erase(p->key); //更新哈希表                delete p; //释放内存            }            //否则,插入(key, value)            auto p = new Node(key,value);            hash[key] = p;            insert(p);        }    }};

分析:采用一个链表 list<pair<int, int>>来储存信息的 keyvalue,链表的链接顺序即为最近使用的新旧顺序,最新的信息在链表头节点。同时我们需要一个嵌套着链表的迭代器的 unordered_map<int, list<pair<int, int>>::iterator>进行快速搜索,存迭代器的原因是方便调用链表的 splice函数来直接更新查找成功(cash hit)时的信息,即把迭代器对应的节点移动为链表的头节点。

错误:不明白

练习

Leetcode 1135

付费题目

Leetcode 380

设计一个插入、删除和随机取值均为时间复杂度的数据结构

class RandomizedSet {private:    vector<int> nums;    unordered_map<int, int> indices;public:    RandomizedSet() {        srand((unsigned)time(NULL));    }      bool insert(int val) {        if (indices.count(val)) {            return false;        }        int index = nums.size();        nums.emplace_back(val);        indices[val] = index;        return true;    }      bool remove(int val) {        if (!indices.count(val)) {            return false;        }        int index = indices[val];        int last = nums.back();        nums[index] = last;        indices[last] = index;        nums.pop_back();        indices.erase(val);        return true;    }         int getRandom() {        return nums[rand()%nums.size()];    }};

分析:变长数组 + 哈希表可以实现

错误:随机数不太会,剩下的自己实现了

Leetcode 432

设计一个increaseKey,decreaseKey,getMaxKey,getMinKey 均为时间复杂度的数据结构。

class AllOne {    list<pair<unordered_set<string>, int>> lst;    unordered_map<string, list<pair<unordered_set<string>, int>>::iterator> nodes;public:    AllOne() {}    void inc(string key) {        if (nodes.count(key)) {            auto cur = nodes[key], nxt = next(cur);            if (nxt == lst.end() || nxt->second > cur->second + 1) {                unordered_set<string> s({key});                nodes[key] = lst.emplace(nxt, s, cur->second + 1);            } else {                nxt->first.emplace(key);                nodes[key] = nxt;            }            cur->first.erase(key);            if (cur->first.empty()) {                lst.erase(cur);            }        } else { // key 不在链表中            if (lst.empty() || lst.begin()->second > 1) {                unordered_set<string> s({key});                lst.emplace_front(s, 1);            } else {                lst.begin()->first.emplace(key);            }            nodes[key] = lst.begin();        }    }    void dec(string key) {        auto cur = nodes[key];        if (cur->second == 1) { // key 仅出现一次,将其移出 nodes            nodes.erase(key);        } else {            auto pre = prev(cur);            if (cur == lst.begin() || pre->second < cur->second - 1) {                unordered_set<string> s({key});                nodes[key] = lst.emplace(cur, s, cur->second - 1);            } else {                pre->first.emplace(key);                nodes[key] = pre;            }        }        cur->first.erase(key);        if (cur->first.empty()) {            lst.erase(cur);        }    }    string getMaxKey() {        return lst.empty() ? "" : *lst.rbegin()->first.begin();    }    string getMinKey() {        return lst.empty() ? "" : *lst.begin()->first.begin();    }};

分析:双向链表+哈希表

错误:好难

Leetcode 716

付费题目

总结

基本上都是要自己写数据结构的题目,应该也不是很常见了。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第15章 图 + + /2022/09/17/Leetcode/Leetcode-101/Leetcode-101-15/ + + Leetcode 刷题笔记-Leetcode 101 第15章 图

二分图

二分图算法也称为染色法,是一种广度优先搜索。如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么图为二分。

Leetcode 785

判断一个图是不是二分图

class Solution {private:    static constexpr int UNCOLORED = 0;    static constexpr int RED = 1;    static constexpr int GREEN = 2;    vector<int> color;public:    bool isBipartite(vector<vector<int>>& graph) {        int n = graph.size();        vector<int> color(n, UNCOLORED);        for (int i = 0; i < n; ++i) {            if (color[i] == UNCOLORED) {                queue<int> q;                q.push(i);                color[i] = RED;                while (!q.empty()) {                    int node = q.front();                    int cNei = (color[node] == RED ? GREEN : RED);                    q.pop();                    for (int neighbor: graph[node]) {                        if (color[neighbor] == UNCOLORED) {                            q.push(neighbor);                            color[neighbor] = cNei;                        }                        else if (color[neighbor] != cNei) {                            return false;                        }                    }                }            }        }        return true;    }};

分析:广度优先遍历,需要判断

错误:想简单了

拓扑排序

拓扑排序(topological sort)是一种常见的,对有向无环图排序的算法。给定有向无环图中的N个节点,我们把它们排序成一个线性序列;若原图中节点i指向节点j,则排序结果中i一定在j之前。拓扑排序的结果不是唯一的,只要满足以上条件即可。

Leetcode 210

给定N个课程和这些课程的前置必修课,求可以一次性上完所有课的顺序。

class Solution {public:    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {        vector<int> result;        vector<int> indegree(numCourses,0);        vector<vector<int>> graph(numCourses,vector<int>(numCourses,0));        int m = prerequisites.size();        for(int i=0;i<m;++i){            graph[prerequisites[i][1]][prerequisites[i][0]] = 1;            ++indegree[prerequisites[i][0]];        }        while(1){            if(result.size() == numCourses){                return result;            }            int sign = 0;            for(int i=0;i<numCourses;++i){                if(indegree[i] == 0){                    indegree[i] = -1;                    result.push_back(i);                    sign = 1;                    for(int j=0;j<numCourses;++j){                        if(graph[i][j] == 1){                            graph[i][j] = 0;                            --indegree[j];                        }                    }                }            }            if(sign == 0){                break;            }        }        result.clear();        return result;    }};

分析:经典拓扑排序

错误:有一点小错误,基本一遍AC

练习

Leetcode 1059

付费题目

Leetcode 1135

付费题目

Leetcode 882

经典的节点最短距离问题

class Solution {public:    int reachableNodes(vector<vector<int>>& edges, int maxMoves, int n) {        // 先构建图        vector<vector<pair<int, int>>> graph(n);        for (vector<int>& edge : edges)        {            int s = edge[0];            int e = edge[1];            int cnt = edge[2];            graph[s].emplace_back(e, cnt);            graph[e].emplace_back(s, cnt);        }        // 保持一个从起点到当前点的距离        unordered_map<int, int> distances;        distances[0] = 0;        for (int i = 1; i < n; ++i)        {            distances[i] = maxMoves + 1;        }        // 点到点的“额外扩展距离”,最大是cnt        // 二维变一维 int<<32 + int        unordered_map<long, int> extras;        // 结果记录        int res = 0;        // 从起点到改点的距离的小顶堆        priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int, int>>> q;        q.push({0, 0});        while (!q.empty())        {            int dist = q.top().first;            int curr = q.top().second;            q.pop();                      // 忽略更大的距离            if (dist > distances[curr])            {                continue;            }            distances[curr] = dist;            ++res;            for (auto& pair : graph[curr])            {                int next = pair.first;                int cnt = pair.second;                // 这里取最小的距离, 取(cnt和 maxMoves-dist)的最小值                extras[((long)curr << 32) + next] = min(cnt, maxMoves - dist);                // 计算基于当前点到下一个结点的距离,额外走一步,如果找到更小,则插入队列里                int dist2 = dist + cnt + 1;                if (dist2 < distances[next])                {                    q.emplace(dist2, next);                    distances[next] = dist2;                }            }        }        // 最后加上“额外扩展距离”        for (vector<int>& edge : edges)        {            int s = edge[0];            int e = edge[1];            res += min(edge[2], extras[((long)s<< 32) +e] + extras[((long)e<<32) +s]);        }        return res;    }};

总结

各种高级用法,还比较简单,但是应该不是很常见

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:现代信息检索-第7讲 基于语言建模的IR模型 + + /2022/09/17/UCAS/information-retrieval/information-retrieval-7/ + + 《现代信息检索》课程笔记:第7讲 基于语言建模的IR模型

第7讲 基于语言建模的IR模型

语言模型

统计语言模型(Statistical Language Modeling,SLM)

SLM广泛使用于语音识别和统计机器翻译领域,利用概率统计理论研究语言。

规则方法:词、句、篇章的生成比如满足某些规则,不满足该规则就不应存在。

统计方法:任何语言片断都有存在的可能,只是可能性大小不同

对于n-gram,n越大,则模型越复杂,估计的参数(即估计的概率)也越多。当然,当数据量足够大的情况下,模型阶数越高越对片段概率的计算也越准确。

理论上说,在数据充足的情况下,利用更多的历史高阶的模型更准确,但是总计算量也越大

数据规模总是有限的,即用于训练模型参数的语料存在稀疏性 (Data Sparseness ,即某参数在训练语料中没有出现问题。

数据稀疏性导致零概率问题,但是训练集上不出现的事件并不代表在新的语料上不出现。

SLM的一个重要工作就是进行平滑重新分配概率,即使没出现的事件也会赋予一个概率。

基于统计建模的IR模型

  • 查询似然模型:把相关度看成是每篇文档对应的语言下生成该查询的可能性
  • 翻译模型:假设查询经过某个噪声信道变形成某篇文章,则由文档还原成该查询的概率翻译模型可以视为相关度
  • KL距离模型 :查询对应某种语言,每篇文档对应某种语言,查询语言和文档语言的KL距离作为相关度度量

总体分布&抽样

文档的模型风格实际上是某种总体分布

(待评分)文档和查询都是该总体分布下的一个抽样样本实例

根据文档,估计文档的模型,即求出该总体分布(一般假设某种总体分布,然后求出其参数),然后计算该总体分布下抽样出查询的概率

查询似然模型(Query Likelihood Model)

文本生成的多项式模型

数据平滑的一般形式

其它SLMIR 模型

  • 查询似然类:文档建模、计算查询的似然、基本QLM 模型、翻译模型等
  • 文档似然类:查询建模、计算文档的似然、BIM模型、相关性模型等
  • 模型比较类:文档建模、查询建模,计算两个模型的距离,KL距离模型

基于翻译模型的IR模型:

基本的QLM模型不能解决词语失配(word mismatch)问题,即查询中的用词和文档中的用词不一致

翻译概率P(qi|wj)在计算时可以将词项之间的关系融入。

  • 基于词典来计算(人工或者自动构造的同义词/近义词/翻译词典)
  • 基于语料库来计算(标题、摘要vs. 文本;文档锚文本vs. 文档)

KL距离(相对熵)模型

统计语言建模IR模型优缺点

优点:

  • 理论上具有解释性,有扩展空间
  • 有些模型虽然计算上仍然依赖于term 独立性假设,
  • 但是模型本身并不依赖于 term 独立性假设。

缺点:数据稀疏性,需要参数估计

SLMIR模型讨论

  • SLMIR中有一些东西和VSM一样
  • 词项频率直接在模型中使用
    • 但是在SLMIR 中没有进行放缩变化
  • 本质上概率表示已经进行了长度归一化
    • VSM中的余弦归一化也做了类似工作
  • 文档中的词项频率和文档集频率混合以后和idf的效果相当
    • 那些文档集中比较罕见,但是某些文档中比较普遍的词项将对排序起更重要的影响。

SLMIR vs. VSM :

共性:

  • 模型中都直接使用了词项频率
  • 本质上概率表示已经进行了长度归一化
  • 文档中词项频率和文档集频率混合以后和idf的效果相当

不同:

  • SLMIR:基于概率论
  • VSM:基于相似度,一个线性代数中的概念
  • 文档集频率、文档概率、词项频率、归一化等计算细节

基于统计建模的IR模型 : 假设

  • 简化假设:查询和文档是同一类对象,与实际并不相符
    • 已经出现了一些不采用上述假设的SLMIR模型
    • VSM也基于同一假设
  • 简化假设:词项之间是独立的
    • VSM 中也采用了词项独立性假设
  • 比向量空间中的假设表述更清晰
  • SLMIR比VSM 具有更好的理论基础,但是纯语言模型的效果会大大低于经过精心调参的向量模型的效果。
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第14章 树 + + /2022/09/16/Leetcode/Leetcode-101/Leetcode-101-14/ + + Leetcode 刷题笔记-Leetcode 101 第14章 树

树的递归

Leetcode 104

给定一个二叉树,找出其最大深度。

class Solution {public:    static int DFS(TreeNode* &root,int sum){        if(root == nullptr){            return sum;        }        return max(DFS(root->left,sum+1),DFS(root->right,sum+1));    }    int maxDepth(TreeNode* root) {        return DFS(root,0);    }};

分析:递归计算最大高度即可

错误:开始递归写的有问题,变成引用传参了,后面改对后调通。

Leetcode 110

给定一个二叉树,判断它是否是高度平衡的二叉树。

class Solution {public:    static int DFS(TreeNode* &root){        if(root == nullptr){            return 0;        }        int left = DFS(root->left);        int right = DFS(root->right);        if(left == -1 || right == -1 || abs(left - right) > 1){            return -1;        }        return max(left,right)+1;    }    bool isBalanced(TreeNode* root) {        return DFS(root) != -1;    }};

分析:解法类似于求树的最大深度,但有两个不同的地方:一是我们需要先处理子树的深度再进行比较,二是如果我们在处理子树时发现其已经不平衡了,则可以返回一个-1,使得所有其长辈节点可以避免多余的判断

错误:思路不对,看了解析

Leetcode 543

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

class Solution {public:    static int DFS(TreeNode* &root,int &maxsum){        if(root == nullptr){            return 0;        }        int left = DFS(root->left,maxsum);        int right = DFS(root->right,maxsum);        maxsum = max(maxsum,left+right+1);        return max(left,right)+1;    }    int diameterOfBinaryTree(TreeNode* root) {        int maxsum = 0;        int a = DFS(root,maxsum);        return maxsum-1;    }};

分析:还是递归,要留两个变量进行记录

错误:没看解析调通,但是自己想的挺艰难的。

Leetcode 437

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

class Solution {public:    static long long DFS(TreeNode* &root, long long sum){        if(root == nullptr){            return 0;        }        long long count;        if(root->val == sum){            count = 1;        }        else{            count = 0;        }        return count + DFS(root->left,sum-root->val) + DFS(root->right,sum-root->val);    }    int pathSum(TreeNode* root, int targetSum) {        if(root == nullptr){            return 0;        }        return DFS(root,targetSum)+pathSum(root->left,targetSum)+pathSum(root->right,targetSum);    }};

分析:递归每个节点时,需要分情况考虑:(1)如果选取该节点加入路径,则之后必须继续加入连续节点,或停止加入节点(2)如果不选取该节点加入路径,则对其左右节点进行重新进行考虑。因此一个方便的方法是我们创建一个辅函数,专门用来计算连续加入节点的路径。

错误:两层的递归有点做不了

Leetcode 101

给你一个二叉树的根节点 root , 检查它是否轴对称。

class Solution {public:    static bool DFS(TreeNode* &left,TreeNode* &right){        if(left == nullptr && right != nullptr){            return false;        }        if(left != nullptr && right == nullptr){            return false;        }        if(left == nullptr && right == nullptr){            return true;        }        if(left->val != right->val){            return false;        }        return DFS(left->left,right->right) && DFS(left->right,right->left);    }    bool isSymmetric(TreeNode* root) {        if(root == nullptr){            return true;        }        return DFS(root->left,root->right);    }};

分析:判断一个树是否对称等价于判断左右子树是否对称。笔者一般习惯将判断两个子树是否相等或对称类型的题的解法叫做“四步法”:(1)如果两个子树都为空指针,则它们相等或对称(2)如果两个子树只有一个为空指针,则它们不相等或不对称(3)如果两个子树根节点的值不相等,则它们不相等或不对称(4)根据相等或对称要求,进行递归处理。

错误:不明白

Leetcode 1110

给出二叉树的根节点 root,树上每个节点都有一个不同的值。如果节点值在 to_delete 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。返回森林中的每棵树。你可以按任意顺序组织答案。

class Solution {public:    void DFS(TreeNode* &root, vector<int>& to_delete,vector<TreeNode*>& result){        if(root == nullptr){            return;        }        DFS(root->left,to_delete,result);        DFS(root->right,to_delete,result);        auto it = find(to_delete.begin(),to_delete.end(),root->val);        if(it != to_delete.end()){            if(root->left != nullptr){                result.push_back(root->left);            }            if(root->right != nullptr){                result.push_back(root->right);            }            root->left = nullptr;            root->right = nullptr;            root = nullptr;        }        return;    }    vector<TreeNode*> delNodes(TreeNode* root, vector<int>& to_delete) {        vector<TreeNode*> result;        DFS(root,to_delete,result);        if(root != nullptr){            result.push_back(root);        }        return result;    }};

分析:遍历,然后置为空指针就好

错误:开始的判断条件不太够,后来自己调通。

层次遍历

Leetcode 637

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10<sup>-5</sup> 以内的答案可以被接受。

class Solution {public:    vector<double> averageOfLevels(TreeNode* root) {        vector<double> result;        queue<TreeNode*> q;        q.push(root);        while(!q.empty()){            int num = 0;            double sum = 0.0;            int nowsize = q.size();            while(nowsize--){                TreeNode* t = q.front();                q.pop();                num += 1;                sum += t->val;                if(t->left != nullptr){                    q.push(t->left);                }                if(t->right != nullptr){                    q.push(t->right);                }            }            result.push_back(sum/num);        }        return result;    }};

分析:层序遍历即可

一遍AC

前中后序遍历

Leetcode 105

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的 先序遍历inorder 是同一棵树的 中序遍历 ,请构造二叉树并返回其根节点。

class Solution {private:    unordered_map<int, int> index;public:    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {        if (preorder_left > preorder_right) {            return nullptr;        }          // 前序遍历中的第一个节点就是根节点        int preorder_root = preorder_left;        // 在中序遍历中定位根节点        int inorder_root = index[preorder[preorder_root]];          // 先把根节点建立出来        TreeNode* root = new TreeNode(preorder[preorder_root]);        // 得到左子树中的节点数目        int size_left_subtree = inorder_root - inorder_left;        // 递归地构造左子树,并连接到根节点        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);        // 递归地构造右子树,并连接到根节点        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);        return root;    }    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {        int n = preorder.size();        // 构造哈希映射,帮助我们快速定位根节点        for (int i = 0; i < n; ++i) {            index[inorder[i]] = i;        }        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);    }};

分析:很老的题,好好判断,数据结构设计对即可

错误:太久远了忘记怎么判断了

Leetcode 144

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

class Solution {public:    static void dfs(TreeNode* &root,vector<int> &result){        if(root == nullptr){            return;        }        result.push_back(root->val);        dfs(root->left,result);        dfs(root->right,result);    }    vector<int> preorderTraversal(TreeNode* root) {        vector<int> result;        dfs(root,result);        return result;    }};

分析:递归遍历即可

一遍AC

二叉查找树

Leetcode 99

给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。 请在不改变其结构的情况下,恢复这棵树。

class Solution {public:    void inorder(TreeNode* root, TreeNode*& mistake1, TreeNode*& mistake2, TreeNode*& prev) {        if (!root) {            return;        }        if (root->left) {            inorder(root->left, mistake1, mistake2, prev);        }        if (prev && root->val < prev->val) {            if (!mistake1) {                mistake1 = prev;                mistake2 = root;            }            else {                mistake2 = root;            }            cout << mistake1->val;            cout << mistake2->val;        }        prev = root;        if (root->right) {            inorder(root->right, mistake1, mistake2, prev);        }    }    void recoverTree(TreeNode* root) {        TreeNode *mistake1 = nullptr, *mistake2 = nullptr, *prev = nullptr;        inorder(root, mistake1, mistake2, prev);        if (mistake1 && mistake2) {            int temp = mistake1->val;            mistake1->val = mistake2->val;            mistake2->val = temp;        }    }};

分析:我们可以使用中序遍历这个二叉查找树,同时设置一个prev 指针,记录当前节点中序遍历时的前节点。如果当前节点大于prev 节点的值,说明需要调整次序。有一个技巧是如果遍历整个序列过程中只出现了一次次序错误,说明就是这两个相邻节点需要被交换;如果出现了两次次序错误,那就需要交换这两个节点。

错误:没有思路

Leetcode 669

给你二叉搜索树的根节点 root ,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在 [low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案

class Solution {public:    TreeNode* trimBST(TreeNode* root, int low, int high) {        if(root == nullptr){            return root;        }        if(root->val > high){            return trimBST(root->left,low,high);        }        if(root->val < low){            return trimBST(root->right,low,high);        }        root->left = trimBST(root->left, low, high);        root->right = trimBST(root->right, low, high);        return root;    }};

分析:利用二叉查找树的大小关系递归进行树的处理。

错误:看了解析

字典树

Leetcode 208

尝试建立一个字典树,支持快速插入单词、查找单词、查找单词前缀的功能。

class Trie {private:    vector<Trie*> children;    bool isEnd;    Trie* searchPrefix(string prefix) {        Trie* node = this;        for (char ch : prefix) {            ch -= 'a';            if (node->children[ch] == nullptr) {                return nullptr;            }            node = node->children[ch];        }        return node;    }public:    Trie() : children(26), isEnd(false) {}    void insert(string word) {        Trie* node = this;        for (char ch : word) {            ch -= 'a';            if (node->children[ch] == nullptr) {                node->children[ch] = new Trie();            }            node = node->children[ch];        }        node->isEnd = true;    }    bool search(string word) {        Trie* node = this->searchPrefix(word);        return node != nullptr && node->isEnd;    }    bool startsWith(string prefix) {        return this->searchPrefix(prefix) != nullptr;    }};

分析:字典树的典型实现方法

错误:没做过,尝试理解

练习

Leetcode 226

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

class Solution {public:    TreeNode* invertTree(TreeNode* root) {        if(root == nullptr){            return nullptr;        }        TreeNode* left = invertTree(root->left);        TreeNode* right = invertTree(root->right);        root->left = right;        root->right = left;        return root;    }};

分析:递归反转即可

错误:翻转值是不对的,需要反转结点

Leetcode 617

给你两棵二叉树: root1root2 。想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。返回合并后的二叉树。

class Solution {public:    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {        if (t1 == nullptr) {            return t2;        }        if (t2 == nullptr) {            return t1;        }        auto merged = new TreeNode(t1->val + t2->val);        merged->left = mergeTrees(t1->left, t2->left);        merged->right = mergeTrees(t1->right, t2->right);        return merged;    }};

分析:递归处理即可

错误:自己尝试的方法有问题,不太明白错在哪

Leetcode 572

给你两棵二叉树 rootsubRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

class Solution {public:    static bool check(TreeNode* root, TreeNode* subRoot){        if(root == nullptr && subRoot == nullptr){            return true;        }        if(root == nullptr && subRoot != nullptr){            return false;        }        if(root != nullptr && subRoot == nullptr){            return false;        }        if(root->val != subRoot->val){            return false;        }        return check(root->left,subRoot->left) && check(root->right,subRoot->right);    }    static bool DFS(TreeNode* root, TreeNode* subRoot){        if(root == nullptr){            return false;        }        return check(root,subRoot) || DFS(root->left,subRoot) || DFS(root->right,subRoot);    }    bool isSubtree(TreeNode* root, TreeNode* subRoot) {        bool judge = DFS(root,subRoot);        return judge;    }};

分析:递归判断即可

错误:自己写了前半部分,看了一眼后写了后半部分

Leetcode 404

给定二叉树的根节点 root ,返回所有左叶子之和。

class Solution {public:    bool isLeafNode(TreeNode* node) {        return !node->left && !node->right;    }    int dfs(TreeNode* node) {        int ans = 0;        if (node->left) {            ans += isLeafNode(node->left) ? node->left->val : dfs(node->left);        }        if (node->right && !isLeafNode(node->right)) {            ans += dfs(node->right);        }        return ans;    }    int sumOfLeftLeaves(TreeNode* root) {        return dfs(root);    }};

分析:递归判断结点

错误:没有思路

Leetcode 513

给定一个二叉树的 根节点 root,请找出该二叉树的最底层最左边节点的值。

class Solution {public:    int findBottomLeftValue(TreeNode* root) {        queue<TreeNode*> q;        q.push(root);        int result = root->val;        while(!q.empty()){            int tempsize = q.size();            int sign = 0;            while(tempsize--){                TreeNode* t = q.front();                q.pop();                if(sign == 0){                    result = t->val;                    sign = 1;                }                if(t->left != nullptr){                    q.push(t->left);                }                if(t->right != nullptr){                    q.push(t->right);                }            }        }        return result;    }};

分析:广度优先遍历即可

一遍AC

Leetcode 538

给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

class Solution {public:    void DFS(TreeNode* root,int &sum){        if(root == nullptr){            return;        }        DFS(root->right,sum);        root->val = root->val + sum;        sum = root->val;        DFS(root->left,sum);        return;    }    TreeNode* convertBST(TreeNode* root) {        int sum = 0;        DFS(root,sum);        return root;    }};

分析:反向的中序遍历

错误:开始顺序弄反,后面修正了

Leetcode 235

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

class Solution {public:    vector<TreeNode*> getPath(TreeNode* root, TreeNode* target) {        vector<TreeNode*> path;        TreeNode* node = root;        while (node != target) {            path.push_back(node);            if (target->val < node->val) {                node = node->left;            }            else {                node = node->right;            }        }        path.push_back(node);        return path;    }    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {        vector<TreeNode*> path_p = getPath(root, p);        vector<TreeNode*> path_q = getPath(root, q);        TreeNode* ancestor;        for (int i = 0; i < path_p.size() && i < path_q.size(); ++i) {            if (path_p[i] == path_q[i]) {                ancestor = path_p[i];            }            else {                break;            }        }        return ancestor;    }};

分析:从根节点开始遍历;如果当前节点就是p,那么成功地找到了节点;如果当前节点的值大于p的值,说明p应该在当前节点的左子树,因此将当前节点移动到它的左子节点;如果当前节点的值小于p的值,说明p应该在当前节点的右子树,因此将当前节点移动到它的右子节点。对于节点q同理。在寻找节点的过程中,我们可以顺便记录经过的节点,这样就得到了从根节点到被寻找节点的路径。

错误:没有思路

Leetcode 530

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值

class Solution {public:    void DFS(TreeNode* &root,vector<int>& result){        if(root == nullptr){            return;        }        DFS(root->left,result);        result.push_back(root->val);        DFS(root->right,result);    }    int getMinimumDifference(TreeNode* root) {        vector<int> result;        DFS(root,result);        int minval = 100000;        for(int i=0;i<result.size()-1;++i){            if(result[i+1]-result[i] < minval){                minval = result[i+1]-result[i];            }        }        return minval;    }};

分析:中序遍历存在数组内部,然后遍历判断即可

一遍AC

Leetcode 889

给定两个整数数组,preorderpostorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。

class Solution {    int preIdx = 0, postIdx = 0;public:    TreeNode* constructFromPrePost(vector<int>& preorder, vector<int>& postorder) {        TreeNode *node = new TreeNode(preorder[preIdx++]);        if(node->val != postorder[postIdx]){            node->left = constructFromPrePost(preorder, postorder);        }        if(node->val != postorder[postIdx]){            node->right = constructFromPrePost(preorder, postorder);        }        postIdx++;        return node;    }};

分析:利用前序遍历来构建Tree,然后通过后续遍历来检验当前树是否构建完毕

错误:思路不对

Leetcode 106

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

class Solution {public:    TreeNode* DFS(vector<int>& inorder, int inleft,int inright,vector<int>& postorder,int postleft,int postright){        if(inleft > inright){            return nullptr;        }        TreeNode* root = new TreeNode(postorder[postright]);        int k;        for(k=inleft;k<=inright;++k){            if(inorder[k] == postorder[postright]){                break;            }        }        int rightsize = inright - k;        root->left = DFS(inorder,inleft,k-1,postorder,postleft,postright-rightsize-1);        root->right = DFS(inorder,k+1,inright,postorder,postright-rightsize,postright-1);        return root;    }    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {        int n = inorder.size();        TreeNode* root = DFS(inorder,0,n-1,postorder,0,n-1);        return root;    }};

分析:与前面的题目相同

一遍AC

Leetcode 94

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

class Solution {public:    static void dfs(TreeNode* &root,vector<int> &result){        if(root == nullptr){            return;        }        dfs(root->left,result);        result.push_back(root->val);        dfs(root->right,result);    }    vector<int> inorderTraversal(TreeNode* root) {        vector<int> result;        dfs(root,result);        return result;    }};

分析:普通递归

一遍AC

Leetcode 145

给你一棵二叉树的根节点 root ,返回其节点值的后序遍历。

class Solution {public:    static void dfs(TreeNode* &root,vector<int> &result){        if(root == nullptr){            return;        }        dfs(root->left,result);        dfs(root->right,result);        result.push_back(root->val);    }    vector<int> postorderTraversal(TreeNode* root) {        vector<int> result;        dfs(root,result);        return result;    }};

分析:普通递归

一遍AC

Leetcode 236

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

class Solution {public:    TreeNode* ans;    bool dfs(TreeNode* root, TreeNode* p, TreeNode* q) {        if (root == nullptr) return false;        bool lson = dfs(root->left, p, q);        bool rson = dfs(root->right, p, q);        if ((lson && rson) || ((root->val == p->val || root->val == q->val) && (lson || rson))) {            ans = root;        }         return lson || rson || (root->val == p->val || root->val == q->val);    }    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {        dfs(root, p, q);        return ans;    }};

分析:不太明白

错误:不太明白

Leetcode 109

给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。

class Solution {public:    ListNode* getMedian(ListNode* left, ListNode* right) {        ListNode* fast = left;        ListNode* slow = left;        while (fast != right && fast->next != right) {            fast = fast->next;            fast = fast->next;            slow = slow->next;        }        return slow;    }    TreeNode* buildTree(ListNode* left, ListNode* right) {        if (left == right) {            return nullptr;        }        ListNode* mid = getMedian(left, right);        TreeNode* root = new TreeNode(mid->val);        root->left = buildTree(left, mid);        root->right = buildTree(mid->next, right);        return root;    }    TreeNode* sortedListToBST(ListNode* head) {        return buildTree(head, nullptr);    }};

分析:每一次找中位数,然后递归构造两边就可以了

错误:以为要调整平衡,没有思路

Leetcode 897

给你一棵二叉搜索树的 root ,请你 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。

class Solution {public:    void inorder(TreeNode *node, vector<int> &res) {        if (node == nullptr) {            return;        }        inorder(node->left, res);        res.push_back(node->val);        inorder(node->right, res);    }    TreeNode* increasingBST(TreeNode* root) {        vector<int> res;        inorder(root, res);        TreeNode *dummyNode = new TreeNode(-1);        TreeNode *currNode = dummyNode;        for (int value : res) {            currNode->right = new TreeNode(value);            currNode = currNode->right;        }        return dummyNode->right;    }};

分析:遍历建树就可以,注意不要在函数中建树,原因没明白

错误:在函数中建树不行

Leetcode 653

给定一个二叉搜索树 root 和一个目标结果 k,如果二叉搜索树中存在两个元素且它们的和等于给定的目标结果,则返回 true

class Solution {public:    void DFS(TreeNode* root,vector<int> &result){        if(root == nullptr){            return;        }        DFS(root->left,result);        result.push_back(root->val);        DFS(root->right,result);        return;    }    bool findTarget(TreeNode* root, int k) {        vector<int> result;        DFS(root,result);        int left = 0;        int right = result.size()-1;        while(left < right){            if(result[left] + result[right] == k){                return true;            }            else if(result[left] + result[right] < k){                ++left;            }            else{                --right;            }        }        return false;    }   };

分析:读出来二分就可以了

一遍AC

Leetcode 450

给定一个二叉搜索树的根节点root和一个值key,删除二叉搜索树中的key对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

class Solution {public:    TreeNode* deleteNode(TreeNode* root, int key) {        if (root == nullptr) {            return nullptr;        }        if (root->val > key) {            root->left = deleteNode(root->left, key);            return root;        }        if (root->val < key) {            root->right = deleteNode(root->right, key);            return root;        }        if (root->val == key) {            if (!root->left && !root->right) {                return nullptr;            }            if (!root->right) {                return root->left;            }            if (!root->left) {                return root->right;            }            TreeNode *successor = root->right;            while (successor->left) {                successor = successor->left;            }            root->right = deleteNode(root->right, successor->val);            successor->right = root->right;            successor->left = root->left;            return successor;        }        return root;    }};

分析:解析

错误:不明白应该怎么调整

总结

看起来树的题目并没有特别复杂的。主要的难度在于递归的思路,想明白后就简单了。另外就是各种边界条件的判断,也要多想多练。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:机器学习-第3章 线性分类 + + /2022/09/15/UCAS/machine-learning/machine-learning-3/ + + 《机器学习》课程笔记:第3章 线性分类

第3章 线性分类

基础知识

个数组成的有序数组, 称为一个维向量

向量空间:所有分量为实数的维向量构成的集合称为一个维向量空间,又称线性空间。

超平面表达式:

线性判别函数表达式:

线性函数刻画了样本到超平面的距离

相似性测度:

  • Minkovski Metric 闵氏距离(p-范数)
  • 欧氏距离(p=2)(2-范数)
  • 城市块(p=1)、曼哈顿距离(1-范数)
  • Chobychev 距离(p=inf)
  • 平方距离\马氏距离
  • 余弦相似性

常用的统计量:

  • 类均值向量
  • 总均值向量
  • 类内散度矩阵
  • 总类内离散度矩阵
  • 类间散度矩阵

分类问题

  1. 定义:根据给定的训练集,其中,要求寻找上的决策函数
  2. 评估方法
    1. 留出法数据集分成两类,交叉验证。
    2. 交叉验证法数据集分成类,其中类做测试,类做训练;进行次实验取平均。
    3. 自助法次随机取一个样本, 共个样本,放入中;由训练,测试。
  3. 性能评价
    1. 错误率与精度:
    2. 查准率、查全率与F1
    3. ROC 与AUC
    4. 代价敏感错误率与代价曲线
  4. 比较检验
    1. 假设检验
    2. 交叉验证检验
    3. McNemar检验
    4. Friedman检验与Nemenyi检验

线性分类问题

  1. 线性分类器描述:
    1. 线性判别函数:
    2. 分类界为超平面:
  2. 线性分类器的任务:通过已知的训练样本集, 构造线性判别函数
  3. 线性可分性

线性决策的多分类问题:

类问题,需要至少预先训练多少个二分类器?

需要训练好个分类器(所有可能的分类器),然后采用二叉树比对测试。

根据最大相似性决定类别。

感知机

基本知识:

  1. 神经网络形成阶段(1943-1958),开拓性的贡献
  2. 线性分类:
    1. 决策函数:
    2. 增广表示:,其中
    3. 决策超平面:
    4. 分类判别:根据是否大于0进行判断
    5. 决策函数几何含义:刻画了样本到超平面的距离
    6. 验证函数:
  3. 优化方法:梯度下降
    1. 随机梯度下降:

感知机结构

vz4erd.md.png

感知机学习准则:目标:最小化错分样本的误差代价。

代价函数(错分样本的误差函数):(只统计错分的样本,是错分的样本到超平面的距离之和)

的含义:错分样本到分类超平面误差距离的总和

感知机优化:Batch Perception和Online Perception

误差修正基本规则:

  1. 固定增量的感知机修正:若训练样本是线性可分,则感知器训练算法在有限次迭代后可以收敛到正确的解向量
  2. 增量自适应调整:当错分样本的正确标签为,修正;当错分样本的正确标签为,修正

线性鉴别分析

基本思想:求线性变换,使得样本集${x_i} {y_i} $后,类别间距大,类内间距小。

目标函数:

样本投影后的类别间距离: ; 其中, 表示第 类样本投影后的均值

样本投影后的类别内距离:投影后的各类样本方差

计算:

logistic 模型

基本思想:假设likelihood ratio的对数为线性判别函数

两类问题:

学习目标:

标签 类, 越大, 越小,标签 类, 越大, 越小。

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:机器学习-第4章 非线性分类 + + /2022/09/15/UCAS/machine-learning/machine-learning-4/ + + 《机器学习》课程笔记:第4章 非线性分类

第4章 非线性分类

概述

非线性问题:对于线性不可分数据,采用非线性决策的方法

线性扩展的思想:线性扩展模型,核函数方法

非线性的思想:最近邻、决策树、神经网络、集成学习

决策树

决策问题一定是一个二判决问题

样本根据问题一定可以分成两部分,两部分之间没有交集,两部分的并集包括所有的情况

决策树的目标:在树结构上,根据节点的判断,搜索类别。

树结构的优点:可以不必测试所有特征和区域。

问题数

  1. 离散值情况:以特征或特征的可能离散值作为问题:

设属性的可能离散取值个数为

  • 方法1:每个特征可以作为候选问题,例如ID3、C4.5,属性产生的候选问题数为(切分太快,容易过拟合)
  • 方法2:每个特征的每个离散值作为候选问题,例如CART,属性产生的候选问题数为
  1. 连续值情况:以每个维度的样本特征值作为问题

属性上出现的样本特征值个数为

方法:每个特征上的样本特征值作为候选问题,属性产生的候选问题数为

无论特征值是连续还是离散,确定每个属性所产生的候选问题,候选的问题总数为

划分(问题)选择

非纯度(Impurity Measure)需要满足两条性质:

  • IM最大值时,各类别概率相等
  • IM最小时为0,只有一类(期望的目标)

非纯度的熵度量(C4.5):

非纯度的基尼度量(CART):

划分目标:选择最大减少类别非纯度的问题作为划分节点。

基于非纯度变化量的三个指标:

  • 信息增益(ID3):越大越好
  • 增益率(C4.5):越大越好
  • 基尼指数:越小越好

信息增益(熵度量):是问题导致的决策划分数目

倾向于选择划分集合个数多的节点。区间划分的越细,区间内纯度越高,极端情况每个区间只有一个样本,则熵为0。

增益率(信息增益与数据集关于问题的熵值之比)

增益率改善信息增益:对划分集合个数少的属性有所偏好,越小则越小

基尼指数(基尼度量):

节点类别设置:叶子节点纯度达到预设阈值后,停止划分,并对叶子节点进行类别设置。(按概率最大的类别设定)

决策树生成

决策树生成过程

从顶向下(不断增加一个节点)

  • 准则:所有划分中选择一个使(非纯度减少量)最大的划分为节点,加入决策树。
  • 贪婪学习:根据划分准则,在问题集上进行划分,直到Impurity不能再改善,或达到较小的改善。
  • 停止规则:设定阈值

ID3 决策树:属性特征作为结点问题,划分选择实际是特征选择过程,最大化信息增益作为划分选择依据

C4.5 决策树:属性特征作为结点问题,划分选择实际是特征选择过程,最大化信息增益率作为划分选择依据

CART 决策树:属性特征离散值作为结点问题,本质是二叉树,最小化基尼指数作为划分选择依据

连续值二叉决策树

剪枝处理

ID3、C4.5决策树剪枝

  • 代价函数
  • 剪枝算法

泛化性能评估法

最近邻方法

最近邻法

原理:将样本分类为离之最近的样本类别

类判别函数:

决策规则:

最近邻分类隐含的决策边界是非线性的

k-近邻法

原理:将样本分给个近邻中类别样本个数最多的类

个近邻中属于的样本数

判别函数:

决策规则:

误差讨论

近邻法的缺点:

  • 存储量大:训练样本需要存到内存
  • 计算量大:每次决策都要计算所有样本的相似性

近邻法的快速算法

快速算法一:快速搜索近邻法(不减少的情况下怎么样才能更快)

原理:将样本分成不相交的子集,基于子集的搜索

  1. 样本分级分层为多个子集
  2. 逐层搜出一个最优子集
  3. 在最后的子集中局部找最近样本点

规则1-找最近子集:如果的距离 > 当前最近子集距离,则被忽略。

规则2-找最近样本:如果的距离>已存在的最近点,则样本被忽略。

k 近邻快速搜索推广:子集搜索过程与最近邻一致,样本搜索时,存有个最近距离值。

快速算法二:剪辑近邻法

原理:通过剪掉边界样本(错误分类样本),缩减样本规模

剪辑规则:两分剪辑近邻法

  • 将训练样本集,分成两个子集
  • 做分类参考,对进行剪辑(错分样本被剪掉)
  • 剪辑后的作为最终的训练集训练近邻分类器

快速算法三:压缩近邻法

原理:去掉中心附近样本,保留错误样本,在剪辑基础上进行压缩

基本思想:分类中通常被正确分类的样本,较少支持决策,将常分误的样本保留。

压缩规则:

  1. 初始化,训练集分为中仅个样本;
  2. 作为训练,分类中第个样本;如果错误,将该样本放入
  3. 对每一个样本重复2
  4. 直到无错分样本,或为空
  5. 中样本放弃,是最终压缩样本集

拒绝决策近邻法

原理:对于与各类别相似度较低的样本,不做判断

优点:在样本压缩时,给可是可非的样本机会。

  • 算法1:可拒绝的k近邻法(分类决策)-k近邻中,各类样本的个数小于 , 拒绝分类
  • 算法2:可拒绝的编辑近邻法(样本压缩)-与编辑近邻法比较的不同之处:除保留正确分类样本外,还保留了拒绝样本。

集成学习

结合策略

原理:不同的分类器对样本有不同的鉴别力;综合优势,使错误率最小。

问题描述:已知一组训练分类器,分类器的类别后验为,其中为索引类别,为索引分类器.

目标是对进行分类,求

概率分布相似性的计算:

  1. 期望之间的相似度:
  2. 在每个维度上的log比值:
  3. 内积运算:

几种集成学习准则

Geometric Average Rule

  • 目标函数:最小化KL平均
  • 集成方法:
  • 决策规则:

Arithmetic Average Rule

  • 目标函数:最小化Alternative KL平均
  • 集成方法:
  • 决策规则:

Majority Voting Rule

  • 原理:对两类问题,多个分类器进行决策投票,票数过半的类别为样本最终标签。
  • 基分类器要求相互独立且正确率p>50%,且最好具有多样性

Bagging和随机森林

Bagging:通过随机采样,训练分类器,保证分类器的差异。从训练集中不断随机抽取样本构造分类器,分类时通过投票进行类别判断。

随机森林:多决策树的Bagging;决策树随机属性选择;从训练集中不断随机构造决策树分类器,分类时通过投票进行类别判断。

随机森林较一般Bagging效果好

Boosting: AdaBoost

Boosting原理:一系列弱分类器,在不同子集上学习,得到增强分类器。

AdaBoost加权分类器

AdaBoost 目标函数

非线性SVM

SVM 原理

两个核心思想

  • 最大间隔:找到最大间隔分类超平面;
  • 核函数方法:样本升维映射到高维空间后,采用线性决策。升维映射由核技巧实现。

数学问题

KKT:任何目标函数有解的充要条件

一个原始问题总有它的对偶问题

对于特殊的凸优化来说,原始问题的对偶问题是,两个函数的极值相等,也就是最优解是相等的

如果原始问题和它的对偶问题都满足KKT条件,对于条件好的凸优化,可以构造的关系,从而将不好求解的原始问题转化为好求的对偶问题

最大间隔

目标:找到最大间隔分类超平面(类别集合到分类超平面的最小距离最大化)

函数间隔:给定的训练数据集和超平面

  • 超平面关于样本的函数间隔定义为
  • 超平面关于训练数据集的函数间隔定义为,即所有样本点的函数间隔的最小值。
  • 存在问题:只要成比例的改变,函数间隔会相应变化。

几何间隔:给定的训练数据集和超平面

  • 超平面关于样本的几何间隔定义为
  • 超平面关于训练数据集的几何间隔定义为,即所有样本点的几何间隔的最小值。
  • 成比例的改变,几何间隔不会相应变化。

最大几何间隔等价的问题:

函数间隔的取值并不影响最优化问题的解。

支撑向量(SV):支撑最小距离最大化的样本

支撑超平面:通过支持向量,平行于分类面的超平面

间隔:支撑向量到分类面的距离

支持向量机学习的基本想法是求解能够正确划分训练数据集并且几何间隔最大的分离超平面。

对偶问题

问题的求解

根据KKT条件成立求解

核函数方法

避免直接求非线性映射,由核函数替代内积运算

SVM 算法

硬间隔SVM

软间隔SVM

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第13章 链表 + + /2022/09/14/Leetcode/Leetcode-101/Leetcode-101-13/ + + Leetcode 刷题笔记-Leetcode 101 第13章 链表

链表

(单)链表是由节点和指针构成的数据结构,每个节点存有一个值,和一个指向下一个节点的指针,因此很多链表问题可以用递归来处理。不同于数组,链表并不能直接获取任意节点的值,必须要通过指针找到该节点后才能获取其值。同理,在未遍历到链表结尾时,我们也无法知道链表的长度,除非依赖其他数据结构储存长度。

链表的基本操作

Leetcode 206

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

class Solution {public:    ListNode* reverseList(ListNode* head) {        ListNode* p = nullptr;        ListNode* q = head;        while(q){            ListNode* r = q->next;            q->next = p;            p = q;            q = r;        }        return p;    }};

分析:两种方式,迭代法和递归法反转链表。

错误:算法忘记了,稍稍看了一眼后明白了

Leetcode 21

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

class Solution {public:    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {        ListNode* result = new ListNode(-1);        ListNode* head = result;        while(list1 != nullptr && list2 != nullptr){            if(list1->val > list2->val){                              head->next = list2;                list2 = list2->next;            }            else{                head->next = list1;                list1 = list1->next;            }            head = head->next;        }        if(list1 != nullptr){            head->next = list1;        }        else{            head->next = list2;        }        return result->next;    }};

分析:按照顺序一点一点合并即可,前面设置一个头结点,后面把它扔掉返回。

错误:链表操作忘记了

Leetcode 24

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

class Solution {public:    ListNode* swapPairs(ListNode* head) {        ListNode* pre = new ListNode(-1);        pre->next = head;        head = pre;        while(pre->next != nullptr && pre->next->next != nullptr){            ListNode* p = pre->next;            ListNode* q = p->next;            pre->next = q;            p->next = q->next;            q->next = p;            pre = p;        }        return head->next;    }};

分析:链表操作

错误:已经不熟练了,不知道什么时候加结点什么的。

其它链表技巧

Leetcode 160

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

class Solution {public:    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {        if(headA == nullptr || headB == nullptr){            return nullptr;        }        ListNode* pa = headA;        ListNode* pb = headB;        while(pa != pb){            if(pa == nullptr){                pa = headB;                pb = pb->next;            }            else if(pb == nullptr){                pb = headA;                pa = pa->next;            }            else{                pa = pa->next;                pb = pb->next;            }        }        return pa;    }};

分析:当链表headA和headB都不为空时,创建两个指针pA和pB,初始时分别指向两个链表的头节点headA和headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:每步操作需要同时更新指针pA和pB。如果指针pA不为空,则将指针pA移到下一个节点;如果指针 pB不为空,则将指针pB移到下一个节点。如果指针pA为空,则将指针pA移到链表headB的头节点;如果指针pB为空,则将指针pB移到链表headA的头节点。当指针pA和pB指向同一个节点或者都为空时,返回它们指向的节点或者null。

错误:不会做

Leetcode 234

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

class Solution {public:    bool isPalindrome(ListNode* head) {        vector<int> vt;        while(head != nullptr){            vt.push_back(head->val);            head = head->next;        }        int n = vt.size();        for(int i=0;i<n/2;++i){            if(vt[i] != vt[n-i-1]){                return false;            }        }        return true;    }};

分析:复制到数组中判断

一遍AC

练习

Leetcode 83

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。

class Solution {public:    ListNode* deleteDuplicates(ListNode* head) {        ListNode* p = head;        if(p == nullptr){            return nullptr;        }        while(p->next != nullptr){            ListNode* q = p->next;            if(p->val == q->val){                p->next = q->next;                q = p->next;            }            else{                q = q->next;                p = p->next;            }        }        return head;    }};

分析:遍历判断即可

错误:没有考虑链表中没有结点的情况。

Leetcode 328

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。

class Solution {public:    ListNode* oddEvenList(ListNode* head) {        if(head == nullptr){            return head;        }        ListNode* odd = head;        ListNode* even = head->next;        ListNode* evenhead = even;        while(even != nullptr && even->next != nullptr){            odd->next = even->next;            even->next = even->next->next;            odd = odd->next;            even = even->next;        }        odd->next = evenhead;        return head;    }};

分析:单独存储奇偶结点即可。

错误:还是不熟练

Leetcode 19

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

class Solution {public:    ListNode* removeNthFromEnd(ListNode* head, int n) {        ListNode* p = head;        int sum = 0;        while(p != nullptr){            ++sum;            p = p->next;        }        p = head;        int num = sum - n;        if(num == 0){            return head->next;        }        ListNode* pre = new ListNode(-1);        pre->next = p;        for(int i=0;i<num;++i){            pre = p;            p = p->next;        }        pre->next = p->next;        return head;    }};

分析:先数一遍一共有多少个结点,然后再遍历一遍删掉即可。

一遍AC

Leetcode 148

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

class Solution {public:    ListNode* sortList(ListNode* head) {        if(head == nullptr){            return head;        }        vector<int> result;        ListNode* p = head;        while(head != nullptr){            result.push_back(head->val);            head = head->next;        }        sort(result.begin(),result.end());        head = p;        int index = 0;        while(head != nullptr){            head->val = result[index++];            head = head->next;        }        return p;    }};

分析:可以用一些比较高大上的链表排序方法,也可以耍赖,直接读入数组中排序即可。

一遍AC

总结

链表不难,就是太容易忘记了,后面要经常复习。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:现代信息检索-第6讲 概率检索模型 + + /2022/09/14/UCAS/information-retrieval/information-retrieval-6/ + + 《现代信息检索》课程笔记:第6讲 概率检索模型

第6讲 概率检索模型

向量空间模型的优缺点

优点:

  • 简洁直观,可以应用到很多其他领域(文本分类、生物信息学)。
  • 支持部分匹配和近似匹配,结果可以排序
  • 检索效果不错

缺点:

  • 理论上不够严谨,往往基于直觉的经验性公式
  • 词项之间的独立性假设与实际不符:实际上,词项的出现之间是有关系的,并不是完全独立的。

基本概率统计知识

检索系统中,给定查询,计算每个文档的相关度

检索系统对用户查询的理解是非确定的(uncertain),对返回结果的猜测也是非确定的

而概率理论为非确定推理提供了坚实的理论基础,可以计算文档和查询相关的可能性

概率检索模型是通过概率的方法将查询和文档联系起来

定义3个随机变量R、Q、D:相关度R={0,1},查询Q可以是q1,q2,…中的一个查询,文档D可以是d1,d2,…中的一篇文档,则可以通过计算条件概率P(R=1|Q=q,D=d)来度量文档和查询的相关度。

概率排序原理(PRP):

  • 如果文档按照与查询的相关概率大小返回,那么该返回结果是所有可能获得结果中效果最好的。
  • 如果文档按照与查询的相关概率大小返回,而这些相关概率又能够基于已知数据进行尽可能精确的估计,那么该返回结果是所有基于已知数据获得的可能的结果中效果最好的。

Logistic回归模型

回归分析:回归分析是处理变量之间相关关系的一种工具,回归的结果可以用于预测或者分类

一元线性回归:根据观测点,拟合出一条直线,使得某种损失 (如离差平方和)最小

Logistic回归是一种非线性回归,可以转化成线性回归来实现。

基本思想:为了求Q和D相关的概率P(R=1|Q,D),通过定义多个特征函数fi(Q,D),认为P(R=1|Q,D)是这些函数的组合。

求解和使用过程:通过训练集合拟和得到相应系数 ,对于新的文档,代入公式计算得到概率P

优缺点:

  • 优点:直接引入数学工具,形式简洁。
  • 缺点:特征选择非常困难,实验中效果一般。
    • 以文档为样本(Pointwise)训练模型,无法解决不同查询之间的差异

BIM模型

二值独立概率模型

BIM模型通过贝叶斯公式对所求条件概率P(R=1|Q,D)展开进行计算,是一种生成式(generative)模型

对每个Q定义排序(Ranking)函数RSV(Q,D)

其中,P(D|R=1)、P(D|R=0)分别表示在相关和不相关情况下生成文档D的概率。Ranking函数显然是随着P(R=1|D)的增长而增长。

两种常用的文档生成的总体分布:多元贝努利分布和多项式分布

BIM中P(D|R=1)或P(D|R=0)的计算:类比M次独立试验

BIM模型公式的推导:pi qi参数的计算,RSJ权重

BIM计算过程:目标是求排序函数P(D|R=1)/P(D|R=0)

  • 首先估计或计算每个term分别在相关文档和不相关文档中的出现概率pi=P(t|R=1)及qi=P(t|R=0)
  • 然后根据独立性假设,将P(D|R=1)/P(D|R=0) 转化为pi和qi的某种组合,将pi和qi代入即可求解。

优点:

  • BIM模型建立在数学基础上,理论性较强

缺点:

  • 需要估计参数
  • 原始的BIM没有考虑TF、文档长度因素
  • BIM中同样存在词项独立性假设
  • BIM实质上是一个idf权重公式,仅考虑了全局信息,缺少局部信息。因此需要和TF权重配合使用

BM25模型

二重泊松分布

泊松分布是一个经典的随机分布:分布公式参数:均值 λ,分布形式随参数取值变化

关于文本中词频分布的一个经典结论:在高质量精英文档集(Elite Set)中:均值较高,接近正态分布;在整个语料中:均值低,接近指数分布

优点:

  • 一定程度上的理论化模型
  • 基于二重泊松假设——适用于绝大多数文本语料上的IR检索应用
  • 实验证明有效

缺点:

  • 待调参数多且参数敏感性高
  • 必须去停用词

BM25被视为现实应用中最好的IR模型之一。即便现在基于BERT预训练语言模型的方法可以获得更好的效果,仍然需要使用BM25进行无监督过滤来保证检索精度。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:高级人工智能-第3讲 人工神经网络 + + /2022/09/13/UCAS/advanced-ai/advanced-ai-3/ + + 《高级人工智能》课程笔记:第3讲 人工神经网络

第3讲 人工神经网络

联结主义学派:又称仿生学派或生理学派

  • 认为人的思维基元是神经元,而不是符号处理过程
  • 认为人脑不同于电脑

核心:智能的本质是联接机制。

原理:神经网络及神经网络间的连接机制和学习算法

什么是神经网络

  • 所谓的人工神经网络就是基于模仿生物大脑的结构和功能而构成的一种信息处理系统(计算机)。
  • 个体单元相互连接形成多种类型结构的图
    • 循环、非循环
    • 有向、无向
  • 自底向上(Bottom-Up)AI
    • 起源于生物神经系统
    • 从结构模拟到功能模拟

发展历史

  • 1940年代
    • 心理学家McCulloch和数学家Pitts建立了阈值加权和模型(1943)
    • 心理学家Hebb提出神经元之间突触联系是可变(可学习)的假说——Hebb学习律(1949)
  • 1950年代、1960年代
    • 提出并完善了单级感知器(Perceptron)
    • 代表性人物:Marvin Minsky,Frank Rosenblatt,Bernard Widrow
  • 1980年代
    • J.Hopfield提出Hopfield网络(1984)
    • Hinton、Sejnowsky、Rumelhart等人提出了著名的Boltzmann机(1985)
    • Rumelhart等提出多层网络的学习算法—BP算法(1986)
  • 2000年代
    • Hinton et al. Deep Neural Networks (2007)

生物学启示

生物神经元

  • 神经元组成:细胞体,轴突,树突,突触
  • 神经元之间通过突触两两相连。信息的传递发生在突触。
  • 突触记录了神经元间联系的强弱。
  • 只有达到一定的兴奋程度,神经元才向外界传输信息。

神经元特性

  • 信息以预知的确定方向传递:一个神经元的树突-细胞体-轴突-突触-另一个神经元树突
  • 时空整合性
    • 对不同时间通过同一突触传入的信息具有时间整合功能
    • 对同一时间通过不同突触传入的信息具有空间整合功能

工作状态

  • 兴奋状态,对输入信息整合后使细胞膜电位升高,当高于动作电位的阈值时,产生神经冲动,并由轴突输出。
  • 抑制状态,对输入信息整合后使细胞膜电位降低,当低于动作电位的阈值时,无神经冲动产生。

结构的可塑性:神经元之间的柔性连接:突触的信息传递特性是可变的——学习记忆的基础

神经元模型

从生物学结构到数学模型

人工神经元

vxtgc4.md.png

为激活函数,为组合函数

组合函数:

权重和:

径向距离:

激活函数

vxtH3D.md.png

人工神经网络(ANN)

  • 多个人工神经元按照特定的网络结构联接在一起,就构成了一个人工神经网络。
  • 神经网络的目标就是将输入转换成有意义的输出。

生物系统中的学习:

  • 自适应学习:适应的目标是基于对环境信息的响应获得更好的状态
  • 在神经层面上,通过突触强度的改变实现学习:消除某些突触,建立一些新的突触
  • Hebb学习律:神经元同时激活,突触强度增加,异步激活,突触强度减弱
  • 学习律符合能量最小原则:保持突触强度需要能量,所以在需要的地方保持,在不需要的地方不保持。

ANN的学习规则:能量最小

对人工神经网络,需要确定合适的能量定义;可以使用数学上的优化技术来发现如何改变神经元间的联接权重。

两个主要问题:结构和学习方法

ANN结构

  • 前馈结构:没有循环,静态的
  • 反馈/循环结构:有循环,动态的

ANN的学习方法:通过神经网络所在环境的模拟过程,调整网络中的自由参数。

学习策略:Error Correction:最小化实际输出与期望输出之间的误差,属于监督学习。

多层感知机

vzp70x.md.png

感知机实质上是一种神经元模型

阈值激活函数:

判别规则:

输入空间中

  • 样本是空间中的一个点
  • 权向量是一个超平面
  • 超平面一边对应,另一边对应

单层感知机学习:用现在的权重进行分类,如果分类正确,权重不改变;如果分类错误,用分类错误的样本调整权重

感知机收敛定理:若训练数据集是线性可分的,则感知机模型收敛。

感知机存在的问题:如果存在噪声,或样本不是线性可分的,不会收敛。(例如不能处理异或操作),且泛化性比较差。

多层感知机:三层可以学习全部连续的函数,四层就可以学习全部的函数。层间神经元全连接,层内神经元不连接。

学习方法:反向传播

  • 输入数据从输入层经过隐藏层传递到输出层
  • 误差信息从输出层反向传播,通过隐藏层传递到输入层

全局误差度量:(最小平方误差)

权值更新规则采用梯度下降的方法:

vzCYi8.md.png

vzCtJS.md.png

vzCdMj.md.png

vzCwss.md.png

误差反向传播:

vzC0Ln.md.png

实际应用中要对数据进行归一化,并且选择合适的学习率

优点:

  • 很强的表达能力
  • 容易执行

缺点:

  • 收敛速度慢(采用Newton法)
  • 过拟合(Over-fitting)(加正则化项,约束权值的平滑性;采用更少(但足够数量)的隐层神经元)
  • 局部极小(尝试不同的初始化,增加扰动)

多层感知机解决了一般性学习问题,并且与生物系统相联系。

层数增加使用BP算法会存在梯度消失的问题:在后面的几层,误差反向传播后可能变得非常小,权重不太好更新。

采用sigmoid函数,多个相乘使得传递过来的残差会越来越小。

深度学习

时代背景:数据爆炸、计算性能提升

传统机器学习解决问题的思路:

  • 良好的特征表达,对最终算法的准确性起了非常关键的作用,而且系统主要的计算和测试工作都耗在这一大部分。
  • 但实际中一般都是人工完成的。

使用深度学习去自动学习特征!

人脑视觉机理

  • “视觉系统的信息处理”:可视皮层是分级的
  • 神经-中枢-大脑的工作过程,或许是一个不断迭代、不断抽象的过程。
  • 关键词:一个是抽象,一个是迭代。
  • 从原始信号,做低级抽象,逐渐向高级抽象迭代。人类的逻辑思维,经常使用高度抽象的概念。

为什么使用深度学习?

  • 深层结构能够有效被表达
    • 对相同的函数需要更少的计算单元
    • 深层网络结构中,高层可以综合应用低层信息
  • 深层结构可产生层次化特征表达
    • 可解释性,更具有语义化信息
  • 多层隐变量允许统计上的组合共享
  • 深层结构有效(vision, audio, NLP等)!

深层 vs 浅层神经网络

  • 多隐层的人工神经网络具有优异的特征学习能力,学习得到的特征对数据有更本质的刻画,从而有利于可视化或分类
    • 深层网络结构中,高层可以综合应用低层信息
    • 低层关注“局部”,高层关注“全局”、更具有语义化
  • “深度模型”是手段,“特征学习”是目的。
    • 强调了模型结构的深度,通常有5层、6层,甚至10多层的隐层节点;
    • 明确突出了特征学习的重要性,也就是说,通过逐层特征变换,将样本在原空间的特征表示变换到一个新特征空间,从而使分类或预测更加容易。
  • 与人工规则构造特征的方法相比,利用大数据来学习特征,更能够刻画数据的丰富内在信息。

BP算法的问题:

  • 需要带标签训练数据
    • 几乎所有的数据是无标签的
    • 人脑可以从无标签数据中学习
  • 局部极小
    • 对深层网络远离了最优解
  • 梯度消失

Deep learning训练:

自下向上的非监督学习(greedy layer-wise training)

  • 把网络逐层进行预训练,或者找一个足够好的初始权重。

自顶向下的监督学习

  • 就是通过带标签的数据去训练,误差自顶向下传输,对网络进行微调
  • 微调特征(中间层),使得与问题更相关。

对输入的结构建模:建立产生输入的生成式模型,调整参数使得生成式模型的概率最大。

Deep Learning的常用模型

AutoEncoder自动编码器

学习过程:无标签数据,用非监督学习学习特征

  • 将input输入一个encoder编码器,就会得到一个code,这个code也就是输入的一个表示
  • 增加一个decoder解码器
  • 通过调整encoder和decoder的参数,使得重构误差最小,这样就得到输入input信号的一个表示了,也就是编码code。
  • 输入无标签数据,误差的来源就是直接重构后与原输入相比得到。

利用人工神经网络本身的层次结构特点

  • 如果给定一个神经网络,假设其输出与输入是相同的,然后训练调整其参数,得到每一层中的权重。
  • 自然地,就得到了输入I的几种不同表示(每一层代表一种表示),这些表示就是特征。

自动编码器就是一种尽可能复现输入信号的神经网络。

为了实现这种复现,自动编码器就必须捕捉可以代表输入数据的最重要的因素

网络结构

  • 三层结构:输入层,隐藏层,输出层
  • 限定神经元的数量
    • 输入层神经元数=输出层神经元数
    • 隐层神经元数量<输入层神经元数量
    • 意义:迫使隐藏层节点学习得到输入数据的压缩表示方法

自动编码器可以只训练单组参数,不需要关心另一半的参数。

Deep结构——逐层训练

  • 自编码器“栈化”
  • 通过编码器产生特征,然后训练下一层。得到第一层的code,重构误差最小让我们相信这个code就是原输入信号的良好表达了,或者牵
    强点说,它和原信号是一模一样的(表达不一样,反映的是一个东西)。将第一层输出的code当成第二层的输入信号,同样最小化重构误差,就会得到第二层的参数,并且得到第二层输入的code,也就是原输入信息的第二个表达了。其他层也以同样的方法进行。

监督学习

  • Deep结构,每一层都会得到原始输入的不同层次的表达。
  • 有监督微调
    • 为了实现分类,可以在AutoEncoder的最顶的编码层添加一个分类器(例如Logistic回归、SVM等),然后通过标准的多层神经网络的监督训练方法(梯度下降法)去训练。
  • 最后层的特征code输入到分类器中,基于有标签样本,通过监督学习对网络进行微调
    • 只调整分类器
    • 通过有标签样本,微调整个系统

两隐层自编码网络MNIST手写数字识别:

训练一个包含两个隐含层的栈式自编码网络,用来进行MNIST手写数字分类

  1. 用原始输入训练第一个自编码器,学习得到原始输入的一阶特征表示
  2. 把上一层的一阶特征作为另一个稀疏自编码器的输入,使用它们来学习二阶特征
  3. 将二阶特征作为softmax分类器的输入,训练得到一个能将二阶特征映射到数字标签的模型
  4. 将这三层结合起来构成一个栈式自编码网络,通过反向传播算法(BP)同时调整所有层的参数以改善学习结果(称为整体微调finetuning)

栈式自编码器神经网络

  • 栈式自编码神经网络具有强大的表达能力及深度神经网络的所有优点。
  • 通常能够获取到输入的“层次型分组”或者“部分-整体分解”结构。
    • 学习方式:前层的输出作为下一层输入的方式依次训练。
    • 如果网络的输入数据是图像,网络的第一层会学习如何去识别边,第二层一般会学习如何去组合边,从而构成轮廓、角等。更高层会学习如何去组合更形象且有意义的特征。
    • 如果输入数据集包含人脸图像,更高层会学习如何识别或组合眼睛、鼻子、嘴等人脸器官。

Deep Belief Networks(DBN)和Deep Boltzmann Machine(DBM)

Hopfield Network

结构:

  • 单层全互连、对称权值的反馈网络
  • 状态:-1(0),+1

vzFHYV.png

Hopfield网络按动力学方式运行,其工作过程为状态的演化过程,即从初始状态按能量减小的方向进行演化,直到达到稳定状态。稳定状态即为网络的输出。

二值随机神经元(Bernoulli variables):以一定的概率产生1

波尔兹曼机(Boltzmann Machine):

  • 结构类似于Hopfield 网络,但它是具有隐单元的反馈互联网络
  • 遵循波尔兹曼分布,学习数据的固有内在表示。
  • 结构:一个可见层+一个隐层,层内有连接

BM基本原理:

  1. Hopfield网络的神经元的结构功能及其在网络中的地位是一样的。但BM中一部分神经元与外部相连,可以起到网络的输入、输出功能,或者严格地说可以受到外部条件的约束。另一部分神经元则不与外部相连,因而属于隐单元
  2. 每个神经元只取1或0这两种状态:状态1代表该神经元处于接通状态,状态0代表该神经元处于断开状态

缺点:网络结构复杂、训练代价大、局部极小

受限波尔兹曼机(Restricted Boltzmann Machines):

  • 通过输入数据集学习概率分布的随机生成神经网络
  • 结构:一个可见层+一个隐层,层内无连接
  • RBM中,隐单元在给定可视单元情况下,条件独立

Deep Belief Networks:

  • 概率生成模型
  • 深层结构——多层
  • 非监督的预学习提供了网络好的初始化
  • 监督微调(fine-tuning)

Deep Boltzmann Machines:

  • 所有层间无向连接
    • 同层神经元间无连接
  • 高层表示由无标注数据建立
  • 标注数据仅用来微调网络
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第12章 字符串 + + /2022/09/13/Leetcode/Leetcode-101/Leetcode-101-12/ + + Leetcode 刷题笔记-Leetcode 101 第12章 字符串

字符串

字符串比较

Leetcode 242

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的字母异位词。注意:st 中每个字符出现的次数都相同,则称 st互为字母异位词。

class Solution {public:    bool isAnagram(string s, string t) {        sort(s.begin(),s.end());        sort(t.begin(),t.end());        if(s == t){            return true;        }        return false;    }};

分析:哈希表或者直接排序

一遍AC

Leetcode 205

给定两个字符串 st ,判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

class Solution {public:    bool isIsomorphic(string s, string t) {        unordered_map<char,char> mp1;        unordered_map<char,char> mp2;        for(int i=0;i<s.size();++i){            if(mp1.find(s[i]) == mp1.cend()){                mp1[s[i]] = t[i];            }            if(mp2.find(t[i]) == mp2.cend()){                mp2[t[i]] = s[i];            }            if(mp1[s[i]] != t[i] || mp2[t[i]] != s[i]){                return false;            }        }        return true;    }};

分析:通过字典比较即可

错误:开始想用统计的方法去做,后面用字符字典的方式也有一些小错误,应该是比较两遍的。

Leetcode 647

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

class Solution {public:    int countSubstrings(string s) {        int countsum = 0;        int n = s.size();        for(int i=0;i<n;++i){            countsum += 1;            int leftindex = i-1;            int rightindex = i+1;            while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){                ++countsum;                --leftindex;                ++rightindex;            }        }        for(int i=0;i<n-1;++i){            if(s[i] == s[i+1]){                ++countsum;                int leftindex = i-1;                int rightindex = i+2;                while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){                    ++countsum;                    --leftindex;                    ++rightindex;                }            }        }        return countsum;    }};

分析:遍历扩展即可,注意分两种情况讨论一下

一遍AC

Leetcode 696

给定一个字符串 s,统计并返回具有相同数量 01 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。重复出现(不同位置)的子串也要统计它们出现的次数。

class Solution {public:    int countBinarySubstrings(string s) {        int n = s.size();        int countsum = 0;        for(int i=0;i<n-1;++i){            if(s[i] != s[i+1]){                ++countsum;                int leftindex = i-1;                int rightindex = i+2;                while(leftindex >= 0 && rightindex < n && s[leftindex] == s[leftindex+1] && s[rightindex] == s[rightindex-1]){                    ++countsum;                    --leftindex;                    ++rightindex;                }            }        }        return countsum;    }};

分析:和上一道题目相同,甚至只考虑一种情况就可以了,比上一道题目还要简单一点。

一遍AC

字符串理解

Leetcode 227

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。整数除法仅保留整数部分。

class Solution {public:    int calculate(string s) {        vector<int> stk;        char preSign = '+';        int num = 0;        int n = s.length();        for (int i = 0; i < n; ++i) {            if (isdigit(s[i])) {                num = num * 10 + int(s[i] - '0');            }            if (!isdigit(s[i]) && s[i] != ' ' || i == n - 1) {                switch (preSign) {                    case '+':                        stk.push_back(num);                        break;                    case '-':                        stk.push_back(-num);                        break;                    case '*':                        stk.back() *= num;                        break;                    default:                        stk.back() /= num;                }                preSign = s[i];                num = 0;            }        }        return accumulate(stk.begin(), stk.end(), 0);    }};

分析:栈和字符串的应用

错误:最后的运算顺序有问题,没有能自己实现。

字符串匹配

Leetcode 28

给你两个字符串 haystackneedle,请你在 haystack字符串中找出 needle字符串的第一个匹配项的下标(下标从 0开始)。如果 needle不是 haystack的一部分,则返回 -1

class Solution {public:    int strStr(string haystack, string needle) {        int m = haystack.size();        int n = needle.size();        for(int i=0;i<m-n+1;++i){            if(haystack.substr(i,n) == needle){                return i;            }        }        return -1;    }};

分析:可以使用KMP算法,但是不会,简单一点就直接字符串匹配即可。

一遍AC

练习

Leetcode 409

给定一个包含大写字母和小写字母的字符串 s ,返回通过这些字母构造成的最长的回文串。在构造过程中,请注意区分大小写 。比如 "Aa" 不能当做一个回文字符串。

class Solution {public:    int longestPalindrome(string s) {        unordered_map<char,int> mp;        int n = s.size();        for(int i=0;i<s.size();++i){            if(mp.find(s[i]) == mp.cend()){                mp[s[i]] = 1;            }            else{                ++mp[s[i]];            }        }        int ans = 0;        int sign = 0;        for(auto it : mp){            if(it.second % 2 == 0){                ans += it.second;            }            else{                if(sign == 0){                    ans += it.second;                    sign = 1;                }                else{                    ans = ans + it.second / 2 * 2;                }            }        }        return ans;    }};

分析:统计数数即可

一遍AC

Leetcode 3

给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

class Solution {public:    int lengthOfLongestSubstring(string s) {        map<char,int> mp;        int n = s.size();        int right = 0;        int maxlen = 0;        int left = 0;        while(left < n){            while(right < n && (mp.find(s[right]) == mp.cend() || mp[s[right]] == 0)){                ++mp[s[right]];                ++right;            }            maxlen = max(maxlen,right-left);            --mp[s[left]];            ++left;        }        return maxlen;    }};

分析:滑动窗口经典算法

错误:与或非的括号忘记添加了

Leetcode 772

付费题目

Leetcode 5

给你一个字符串 s,找到 s 中最长的回文子串。

class Solution {public:    string longestPalindrome(string s) {        int countsum = 1;        string result = s.substr(0,1);        int n = s.size();        for(int i=0;i<n;++i){            int temp = 1;            int leftindex = i-1;            int rightindex = i+1;            while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){                temp += 2;                --leftindex;                ++rightindex;            }            if(temp > countsum){                countsum = temp;                result = s.substr(leftindex+1,rightindex-1-(leftindex+1)+1);            }        }        for(int i=0;i<n-1;++i){            if(s[i] == s[i+1]){                int temp = 2;                int leftindex = i-1;                int rightindex = i+2;                while(leftindex >= 0 && rightindex < n && s[leftindex] == s[rightindex]){                    temp += 2;                    --leftindex;                    ++rightindex;                }                if(temp > countsum){                    countsum = temp;                    result = s.substr(leftindex+1,rightindex-1-(leftindex+1)+1);                }            }        }        return result;    }};

分析:还是这种题,都第三道了

错误:开始有些索引没考虑好错了一些,后来调通了。

总结

字符串还可以,主要是熟悉一下字符串的处理过程,其余的知识点其他的数据结构中都有。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:现代信息检索-第5讲 文档评分、词项权重计算及向量空间模型 + + /2022/09/12/UCAS/information-retrieval/information-retrieval-5/ + + 《现代信息检索》课程笔记:第5讲 文档评分、词项权重计算及向量空间模型

第5讲 文档评分、词项权重计算及向量空间模型

布尔检索

布尔检索的优点:

  • 对自身需求和文档集性质非常了解的专家而言,布尔查询是不错的选择
  • 对应用开发来说也非常简单,很容易就可以返回1000多条结果

布尔检索的不足:

  • 对大多数用户来说不方便
  • 大部分用户不能撰写布尔查询或者他们认为需要大量训练才能撰写出合适的布尔查询
  • 大部分用户不愿意逐条浏览1000多条结果,特别是对Web搜索
  • 布尔查询常常会导致过少(=0)或者过多(>1000)的结果

在布尔检索中,需要大量技巧来生成一个可以获得合适规模结果的查询

排序式检索

排序式检索会对查询和文档的匹配程度进行排序,即给出一个查询和文档匹配评分

自由文本查询:与布尔查询不同,在排序式检索应用中,用户查询通常都是一个或几个关键字

排序式检索可以解决返回结果过少或过多的问题,可以把相关的结果排在前面

希望文档集中相关度高的文档排名高于相关度低的文档:对每个查询-文档对赋一个[0, 1]之间的分值,度量了文档和查询的匹配程度

Jaccard系数:计算两个集合重合度的常用方法,也就是计算查询文档之间的词项重合度——交集/并集

Jaccard系数的不足:

  • 不考虑词项频率 ,即词项在文档中的出现次数
  • 一般而言,罕见词比高频词的信息量更大,Jaccard系数没有考虑这个信息
  • 没有仔细考虑文档的长度因素

词项频率

查询-文档匹配评分计算:

从单词项查询(查询只包含一个词项)开始,若该词项不出现在文档当中,该文档得分应该为0,该词项在文档中出现越多,则得分越高。

即为词项频率 (term frequency,TF)评分

词袋(Bag of words)模型:不考虑词在文档中出现的顺序

利用tf来计算文档评分的方法:采用原始的tf值(raw tf)

但是原始tf不太合适:某个词项在A文档中出现十次,即tf = 10,在B文档中tf = 1,那么A比B更相关,但是相关度不会相差10倍。

替代原始tf的方法:对数词频

tf-idf权重计算

罕见词项比常见词所蕴含的信息更多

考虑查询中某个词项,它在整个文档集中非常罕见,但是某篇包含该词项的文档很可能相关,因此需要提高权重

常见词项的信息量不如罕见词,一篇包含该词项的文档当然比不包含该词项的文档的相关度要高,但是,这些词对于相关度而言并不是非常强的指示词。

文档频率(Document frequency, df):出现词项的文档数目

  • 对于罕见词项我们希望赋予高权重
  • 对于常见词我们希望赋予正的低权重

idf 权重

是出现词项的文档数目

是和词项的信息量成反比的一个值

于是可以定义词项t的idf权重(逆文档频率):,其中是文档集中文档的数目

是反映词项的信息量的一个指标,是一种全局性指标,反应的是词项在全局的区别性。

对于单词项查询,idf对文档排序没有任何影响,idf 会影响至少包含2个词项的查询的文档排序结果

词项的tf-idf权重是tf权重和idf权重的乘积:

tf-idf权重:

  • 随着词项频率的增大而增大(局部信息)
  • 随着词项罕见度的增加而增大(全局信息)

向量空间模型

二值-tfidf矩阵

文档表示成向量:每篇文档表示成一个基于tfidf权重的实值向量 ∈ R|V|。有一个|V|维实值空间,空间的每一维都对应词项,文档都是该空间下的一个点或者向量。

查询看成向量:

  • 关键思路1:对于查询做同样的处理,即将查询表示成同一高维空间的向量
  • 关键思路2:按照文档对查询的邻近程度排序,邻近度 = 相似度,邻近度≈ 距离的反面

向量空间下相似度:利用余弦相似度

文档长度归一化:一个向量可以通过除以它的长度进行归一化处理(防止长度影响)

问题:

余弦归一化倾向于短文档,即对短文档产生的归一化因子太大,而平均而言对长文档产生的归一化因子太小,因此余弦归一化对长文档的惩罚过重,实际上长文档中虽然词频较高,但也会包含较多的信息。

可以先找到一个支点(pivot,平衡点),然后通过这个支点对余弦归一化操作进行线性调整。因此短文档的相似度降低,而长文档的相似度增大,可以去除原来余弦归一化偏向短文档的问题

回转归一化:基本思想是旋转归一化曲线,使得两条曲线尽量重合

向量空间模型小结:

  • 将查询表示成tf-idf权重向量
  • 将每篇文档表示成同一空间下的 tf-idf权重向量
  • 计算两个向量之间的某种相似度(如余弦相似度)
  • 按照相似度大小将文档排序
  • 将前K(如K =10)篇文档返回给用户
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:模式识别与机器学习-第3章 判别函数 + + /2022/09/11/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-3/ + + 《模式识别与机器学习》课程笔记:第3章 判别函数

第3章 判别函数

线性判别函数

模式识别系统的主要作用:判别各个模式(也称样本)所属的类别

模式分类若可用任一个线性函数来划分,则这些模式就称为线性可分的,否则就是非线性可分的。

一旦线性函数的系数被确定,这些函数就可用作模式分类的基础。

对一个两类问题的判别,就是将模式划分成两类

vOaAmt.md.png

这两类可以通过一个直线方程来划分

,则,若,则

称为决策面/判别界面方程**(判别函数和判别界面是否等于0要注意)**

用判别函数进行模式分类依赖的两个因素:

  • 判别函数的几何性质:线性的(一条直线)和非线性的函数(曲线、折线等)。
    • 线性判别函数建立起来比较简单(实际应用较多);
    • 非线性判别函数建立起来比较复杂。
  • 判别函数的形式确定后,主要就是确定判别函数的系数问题,只要被研究的模式是可分的,就能用给定的模式样本集来确定判别函数的系数。

一个维线性判别函数的一般形式:

权向量(参数向量):

维线性判别函数也可以表示为

增广模式向量:,增广权向量:

多类情况1:用线性判别函数将属于类的模式与不属于类的模式分开,称为 两分法,即把类多类问题分成个两类问题,因此共有个判别函数。会存在分类失败的问题:

vOdk34.png

多类情况2:采用每对划分,即 两分法,此时一个判别界面只能分开两种类别,但不能把它与其余所有的界面分开。

判别函数为,若 ,则

因此要分开类模式,共需个判别函数。也会存在不确定区域,即分类失败。

多类情况1和多类情况2的比较

  • 对于类模式的分类,多类情况1需要个判别函数,而多类情况2需个判别函数,当较大时,后者需要更多的判别式
  • 采用多类情况1时,每一个判别函数都要把一种类别的模式与其余种类别的模式分开,而不是将一种类别的模式仅与另一种类别的模式分开。
  • 由于一种模式的分布要比种模式的分布更为聚集,因此多类情况2对模式是线性可分的可能性比多类情况1更大一些。

多类情况3:没有不确定区域的 两分法

,此时,对类情况应有个判别函数。

广义线性判别函数

线性判别函数简单,容易实现,而非线性判别函数复杂,不容易实现。

若能将非线性判别函数转换为线性判别函数,则有利于模式分类的实现。

设有一个训练用的模式集,在模式空间中线性不可分,但在模式空间中线性可分,其中的各个分量是的单值实函数,的维数高于的维数,即若取,则分类界面在中是线性的,在中是非线性的,此时只要将模式进行非线性变换,使之变换后得到维数更高的模式,就可以用线性判别函数来进行分类。

一个非线性判别函数可如下表示:,其中是模式的单值实函数。

若定义成广义形式:

此时有:。其中

非线性判别函数已被变换成广义线性,因此只讨论线性判别函数不会失去一般性意义。

是模式的二次多项式函数时:

式中各项的组成应包含的各个分量的二次项、一次项和常数项,其中平方项个,二次项个,一次项个,常数项1个,其总项数为:

是模式次多项式函数,总项数为

分段线性判别函数

  • 线性判别函数在进行分类决策时是最简单有效的,但在实际应用中,常常会出现不能用线性判别函数直接进行分类的情况。
  • 采用广义线性判别函数的概念,可以通过增加维数来得到线性判别,但维数的大量增加会使在低维空间里在解析和计算上行得通的方法在高维空间遇到困难,增加计算的复杂性。
  • 引入分段线性判别函数的判别过程,它比一般的线性判别函数的错误率小,但又比非线性判别函数简单。

也就是说,可以使用一个二次判别函数进行分类的地方,也可以使用一个分段线性判别函数来逼近这个二次曲线。

可以采用最小距离分类的方法,只有在类别密集地分布在其均值附近时才有效。

对于各类交错分布的情况,若再用每类一个均值代表点产生最小距离分类器,就会产生很明显的错误率。在这种情况下,可以运用聚类方法将一些类分解成若干个子类,再用最小距离分类。

  • 寻找交遇区—找到互为最小距离的原型对,组成“交遇区”。
  • 用局部训练模式产生分段线性判别函数并迭代优化决策面。
  • 撤走已分类正确的样本,从剩下的样本集合中,寻找交遇区,产生分段线性判别函数。

模式空间和权空间

模式空间:

对一个线性方程,它在三维空间中是一个平面方程式,是方程的系数。

向量作为该平面的法线向量,则该线性方程决定的平面通过原点且与垂直

是二维的增广向量,为非增广的权向量,它与直线AB垂直

模式空间即为增广向量决定的平面或非增广向量决定的直线。

权空间:

若将方程绘在权向量的三维空间中,则为方程的系数

Fisher线性判别

  • 应用统计方法解决模式识别问题时,一再碰到的问题之一就是维数问题。
  • 在低维空间里解析上或计算上行得通的方法,在高维空间里往往行不通。
  • 因此,降低维数有时就会成为处理实际问题的关键。

问题描述:

  • 考虑把维空间的样本投影到一条直线上,形成一维空间,即把维数压缩到一维。
  • 然而,即使样本在维空间里形成若干紧凑的互相分得开的集群,当把它们投影到一条直线上时,也可能会是几类样本混在一起而变得无法识别。
  • 但是,在一般情况下,总可以找到某个方向,使在这个方向的直线上,样本的投影能分得开。

Fisher判别方法所要解决的基本问题:如何根据实际情况找到一条最好的、最易于分类的投影线。

维空间到一维空间的一般数学变换方法:

假设有一集合包含维样本,其中个属于类的样本记为子集个属于类的样本记为子集,若对的分量做线性组合可得标量:,这样便得到个一维样本组成的集合,并可分为两个子集

实际上,的值是无关紧要的,它仅是乘上一个比例因子,重要的是选择的方向。的方向不同,将使样本投影后的可分离程度不同,从而直接影响分类效果。因此,上述寻找最佳投影方向的问题,在数学上就是寻找最好的变换向量的问题。

Fisher准则函数中的基本参量:

空间:

各类样本的均值向量

样本类内离散度矩阵:

总样本类内离散度矩阵:(对称半正定矩阵)

样本类间离散度矩阵:(对称半正定矩阵)

在一维空间:

各类样本的均值:

样本类内离散度:

总样本类内离散度:

我们希望投影后,在一维空间中各类样本尽可能分得开些,即希望两类均值之差越大越好,同时希望各类样本内部尽量密集,即希望类内离散度越小越好。

Fisher准则函数:将其推导为的显函数:

然后使用Lagrange乘数法求解,最终解得

事实上,Fisher的降维就相当于找一个线性判别函数。投影后的变化得来的,就相当于线性判别。

多类情形:

类间散度矩阵与两类情形略有不同:原来度量的是两个均值点的散列情况,现在度量的是每类均值点相对于样本中心的散列情况

推导可得:

感知器算法

一旦判别函数的形式确定下来,不管它是线性的还是非线性的,剩下的问题就是如何确定它的系数。在模式识别中,系数确定的一个主要方法就是通过对已知样本的训练和学习来得到。感知器算法就是通过训练样本模式的迭代和学习,产生线性(或广义线性)可分的模式判别函数。

基本思想:采用感知器算法能通过对训练模式样本集的“学习”得到判别函数的系数。不需要对各类别中模式的统计性质做任何假设,因此称为确定性的方法。

感知器作为人工神经网络中最基本的单元,由多个输入和一个输出组成。

已知两个训练模式集分别属于类和类,权向量的初始值为,可任意取值。

,若

次的训练步骤为:

,则分类器对第个模式做了错误分类,此时应校正权向量,使得,其中为一个校正增量。

,则分类器对第个模式做了错误分类,此时应校正权向量,使得,其中为一个校正增量。

若以上情况不符合,则表明该模式样本在第次中分类正确,因此权向量不变

  • 对正确分类的模式则“赏”,实际上是“不罚”,即权向量不变。
  • 对错误分类的模式则“罚”,使加上一个正比于的分量。
  • 当用全部模式样本训练过一轮以后,只要有一个模式是判别错误的,则需要进行下一轮迭代,即用全部模式样本再训练一次。
  • 如此不断反复直到全部模式样本进行训练都能得到正确的分类结果为止。

感知器算法的收敛性:只要模式类别是线性可分的,就可以在有限的迭代步数里求出权向量。

采用感知器算法的多类模式的分类

采用多类情况3,将感知器算法推广到多类模式。

多类情况3:对类模式存在个判别函数,若, 则

设有种模式类别,若在训练过程的第次迭代时,一个属于类的模式样本送入分类器,则应先计算出个判别函数:。若的条件成立,则权向量不变,即

若其中第个权向量使得,则相应的权向量应做调整,即

其中是一个正常数。权向量的初始值可视情况任意选择。

这里的分类算法都是通过模式样本来确定判别函数的系数,但一个分类器的判断性能最终要受并未用于训练的那些未知样本来检验。要使一个分类器设计完善,必须采用有代表性的训练数据,它能够合理反映模式数据的整体。

要获得一个判别性能好的线性分类器,直观上训练样本越多越好,但实际上能收集到的样本数目会受到客观条件的限制,且过多的训练样本在训练阶段会使计算机需要较长的运算时间。一般来说,合适的样本数目可如下估计:若是模式的维数,令,则通常选用的训练样本数目约为的10~20倍。

感知器算法的解与初值的选择和迭代过程中误分类点的选择顺序有关。

可训练的确定性分类器的迭代算法

梯度法

设函数 是向量 的函数, 则 的梯度定义为

导出的一般关系式是一个正的比例因子(步长)

梯度是一个向量,它的最重要性质就是指出了函数在其自变量增加时最大增长率的方向。负梯度指出的最陡下降方向,利用这个性质可以设计一个迭代方案来寻找函数的最小值。

定义一个对错误分类敏感的准则函数。先任选一个初始权向量,计算准则函数的梯度,然后从出发,在最陡方向(梯度方向)上移动某一距离得到下一个权向量

C值的选择是很重要的。若C值太小,则收敛太慢;若C值太大,则搜索可能过头,引起发散。

固定增量的逐次调整算法

设取准则函数为:

的微分式:,其中

则由梯度法中的关系有:

其中是训练模式样本,是指第次迭代。

若模式是线性可分的,选择合适的准则函数,算法就能给出解。若模式不是线性可分的,算法的结果就会来回摆动,得不到收敛。

最小平方误差(LMSE)算法

感知器算法只是当被分模式可用一个特定的判别界面分开时才收敛,在不可分情况下,只要计算程序不终止,它就始终不收敛。即使在模式可分的情况下,也很难事先算出达到收敛时所需要的迭代次数。这样,在模式分类过程中,有时候会出现一次又一次迭代却不见收敛的情况,白白浪费时间。为此需要知道:发生迟迟不见收敛的情况时,到底是由于收敛速度过慢造成的呢,还是由于所给的训练样本集不是线性可分造成的呢?

最小平方误差(LMSE)算法,除了对可分模式是收敛的以外,对于类别不可分的情况也能指出来。

求两类问题的解相当于求一组线性不等式的解,因此,若给出分别属于的两个模式样本的训练样本集,即可求出其权向量的解。

设两类模式的训练样本总数为,写成增广形式,则有不等式组

H-K算法:

模式类别可分性的判别:

当不等式组有解时,该算法对收敛,可求得解

  • ,即,有解。
  • ,此时隐含的条件,有解。若继续进行迭代,可使
  • 的全部分量停止变为正值(但不是全部为零),表明该模式类别线性不可分。因此,若没有一个分量为正值,则不会再变化,所以不能求得解。

固定增量算法与LMSE算法的比较:

  • 固定增量算法:实现相对简单,可直接引伸到多类模式的分类情况,但未提供模式线性可分的测试特征;
  • LMSE算法:相对复杂,需要对求逆(维数高时求逆比较困难),但对两类情况,提供了线性可分的测试特征。

势函数法-一种确定性的非线性分类算法

用势函数的概念来确定判别函数划分类别界面

基本思想:

  • 假设要划分属于两种类别的模式样本,这些样本可看成是分布在维模式空间中的点
  • 把属于的点比拟为某种能源点,在点上,电位达到峰值。
  • 随着与该点距离的增大,电位分布迅速减小,即把样本附近空间点上的电位分布,看成是一个势函数
  • 对于属于的样本集群,其附近空间会形成一个“高地”,这些样本点所处的位置就是“山头”。
  • 同理,用电位的几何分布来看待属于的模式样本,在其附近空间就形成“凹地”。
  • 只要在两类电位分布之间选择合适的等高线,就可以认为是模式分类的判别函数。

判别函数的产生

模式分类的判别函数可由分布在模式空间中的许多样本向量的势函数产生。任意一个样本所产生的势函数以表征,则判别函数可由势函数序列来构成,序列中的这些势函数相应于在训练过程中输入机器的训练模式样本。在训练状态,模式样本逐个输入分类器,分类器就连续计算相应的势函数,在第步迭代时的积累位势决定于在该步前所有的单独势函数的累加。以表示积累位势函数,若加入的训练样本是错误分类,则积累函数需要修改,若是正确分类,则不变。

从势函数可以看出,积累位势起着判别函数的作用:

  • 属于时,
  • 属于时,,则积累位势不做任何修改就可用作判别函数。

由于一个模式样本的错误分类可造成积累位势在训练时的变化,因此势函数算法提供了确定两类判别函数的迭代过程。

判别函数表达式:取,则有

势函数的选择

选择势函数的条件:一般来说,若两个维向量的函数同时满足下列三个条件,则可作为势函数。

  • ,并且当且仅当时达到最大值;
  • 当向量的距离趋于无穷时,趋于零;
  • 是光滑函数,且是之间距离的单调下降函数。

第一类势函数:可用对称的有限多项式展开:

在模式定义域内为正交函数集。

将这类势函数代入判别函数:,其中

因此,积累位势可写成可用迭代式求得。

第二类势函数:选择双变量的对称函数作为势函数,即,并且它可展开成无穷级数。

例如:

是正常数

用第二类势函数,当训练样本维数和数目都较高时,需要计算和存储的指数项较多。

因为势函数由许多新项组成,因此有很强的分类能力。

决策树简介

决策树,或称多级分类器,是模式识别中进行分类的一种有效方法,对于多类或多峰分布问题,这种方法尤为方便。利用树分类器可以把一个复杂的多类别分类问题,转化为若干个简单的分类问题来解决。它不是企图用一种算法、一个决策规则去把多个类别一次分开,而是采用分级的形式,使分类问题逐步得到解决。

一般来讲,一个决策树由一个根节点,一组非终止节点和一些终止节点组成,可对标以各种类别标签,有时不同的终止节点上可以出现相同的类别标签。

如果用表示决策树,则一个决策树对应于特征空间的一种划分,它把特征空间分成若干个区域,在每个区域中,某类的样本占优势,因此可以标出该类样本的类别标签。

决策树的一种简单形式是二叉树,它是指除叶结点外,树的每个节点仅分为两个分支,即每个非终止节点都有且仅有两个子节点

二叉树结构分类器可以把一个复杂的多类别分类问题转化为多级多个两类问题来解决,在每个非终止节点都把样本集分成左右两个子集。分成的每一部分仍然可能包含多个类别的样本,可以把每一部分再分成两个子集,如此下去,直至分成的每一部分只包含同一类别的样本,或某一类样本占优势为止。

二叉树结构分类器概念简单、直观、便于解释,而且在各个节点上可以选择不同的特征和采用不同的决策规则,因此设计方法灵活多样,便于利用先验知识来获得一个较好的分类器。

在设计一个决策树时,主要应解决以下几个问题:

  • 选择一个合适的树结构,即合理安排树的节点和分支;
  • 确定在每个非终止节点上要使用的特征;
  • 在每个非终止节点上选择合适的决策规则。

把一个多类别分类问题转化为两类问题的形式是多种多样的,因此,对应的二叉树的结构也是各不相同的。通常的目的是要找一个最优的决策树。一个性能良好的决策树结构应该具有小的错误率和低的决策代价。但是由于很难把错误率的解析表达式和树的结构联系起来,而且在每个节点上所采用的决策规则也仅仅是在该节点上所采用的特征观测值的函数,因此,即使每个节点上的性能都达到最优,也不能说整个决策树的性能达到最优。在实际问题中,人们往往提出其它一些优化准则,例如极小化整个树的节点数目,或从根节点到叶结点的最大路经长度,或从根节点到叶结点的平均路经长度等,然后采用动态规划的方法,力争设计出能满足某种准则的“最优”决策树。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Pattern Recognition and Machine Learning + + + +
+ + + + + 研究生课程:高级人工智能-第2讲 搜索 + + /2022/09/08/UCAS/advanced-ai/advanced-ai-2/ + + 《高级人工智能》课程笔记:第2讲 搜索

第2讲 搜索

搜索问题:有策略有规律的探索

搜索问题是对原问题的建模

搜索问题的构成:状态空间➡后继函数(状态转化为另一个状态,采取的动作,付出的代价)➡初始状态和目标测试

解是一个行动序列,将初始状态转换成目标状态

例1:罗马尼亚旅行:

vqG77Q.md.png

  • ①状态空间:所有城市
  • ②后继函数:沿着道路从一个城市到达另外一个城市,损失函数是距离
  • ③初始状态:这个人现在在Arad
  • ④目标测试:目前是否到达了Bucharest

解:从Arad到Bucharest的最短路径

例2:吃豆子游戏

vqJwNj.png

状态空间包含了环境中的每一个细节:Agent,Ghost,大的豆子和小的豆子

搜索状态只保留行动需要的细节:

对于走到终点来说:

  • ①状态空间:Agent的位置信息
  • ②后继函数:四个方向进行行走,更新位置信息
  • ③目标测试:是否到达了终点

对于吃掉所有豆子来说:

  • ①状态空间:Agent的位置信息和每一个点的状态(豆子吃没吃掉)
  • ②后继函数:四个方向进行行走,更新位置信息,更新豆子的信息
  • ③目标测试:全部豆子是否都被吃光

状态数量计算:

  • Agent的状态:120
  • 食物数量:30
  • 鬼魂的位置:12*12
  • 朝向:4
  • 世界状态:
  • 路线规划状态:120
  • “吃光豆子”状态:

例3:三个传教士和三个野人

状态空间:{(M, C, B)},表示河左岸的传教士数量、野人数量和船目前的方位

后继函数:{P01, P10, P02, P20, P11, Q01, Q10, Q02, Q20, Q11},P表示现在是从左岸到右岸,后面两个数字表示船上的传教士数量和野人数量

初始状态:(3, 3, 1)

目标状态:(0, 0, 0)

状态空间图:搜索问题的数学表示,在状态空间图中,每个状态只出现一次

搜索树:

  • 根节点对应了初始状态
  • 子节点对应了父节点的后继
  • 节点显示状态,但对应的是到达这些状态的行动
  • 对大多数问题,实际上不会构建整个树,一般都会剪枝

状态空间图的每一个结点表示每一个状态

搜索树的每一个结点不表示状态,而是从初始状态到这个状态的一个路径(因此要尽量少构建搜索树的结点)

无信息搜索

基于搜索树的搜索:

  • 扩展出潜在的行动 (tree nodes)
  • 维护所考虑行动的边缘(fringe)节点
  • 试图扩展尽可能少的树节点

搜索算法特性:

  • 完备性: 当问题有解时,保证能找到一个解?
  • 最优性: 保证能找到最优解(最小耗散路径)?
  • 时间复杂度和空间复杂度?

所有搜索算法都是相同的,除了对边缘的处理策略

深度优先搜索

  • 在找到目标之前,搜索到整个树左侧的一些子树
  • 可以遍历整个树
  • 分支因子为,最大深度为时间复杂度为空间复杂度为(因为只保留了路径上的结点)
  • 完备性:不完备。如果无穷大,无法在可以接受的时间内找到解
  • 不是最优的:只去找最左边的结点

广度优先搜索

  • 在找到目标之前,搜索到全部更浅的结点
  • 分支因子为,最大深度为,解的深度为时间复杂度为空间复杂度为
  • 完备性:完备。因为如果解存在,一定是有限的
  • 只有所有的路径代价都相同时才是最优的

迭代深入搜索(Iterative Deepening)

结合DFS的空间优势与BFS的时间优势

深度优先按照层数进行约束,不要搜索到

通常绝大多数的节点都在底层,所以上层的节点生成多次影响不是很大

代价敏感搜索(Cost-Sensitive Search)

代价一致搜索(Uniform Cost Search):将之前的走过的路径的代价进行一个累加,然后寻找其代价最低的路径。

可以看成代价敏感搜索的一种实现。

  • 在找到目标之前,搜索到比代价最小的方式更小代价的结点
  • 解的代价为,每条结点间连线的代价大概为时间复杂度为,空间复杂度为
  • 完备性:完备。前提是代价都是有限且都为正数。
  • 最优的

启发式搜索

启发策略:估计一个状态到目标距离的函数,问题给予算法的额外信息,为特定搜索问题而设计。

贪婪搜索

策略:扩展你认为最接近目标状态的节点

启发式:对每个状态估计到最近目标的距离(曼哈顿距离或者欧氏距离),只使用启发函数来评价节点

通常情况下最佳优先使你直接(或很快)到达目标,最坏情况类似DFS

A* 搜索

结合代价一致搜索和贪婪搜索

重点搜索评价函数:

表示路径的代价,或者称为后向的代价

表示前方距离目标的距离,或者称为前向的代价

A* 搜索将两个代价进行组合

A* 搜索结束条件是目标出列的时候,而不是目标入列的时候,因为目标入列的时候可能路径并不是最优的。

A*搜索不一定是最优的,启发函数要好好选择

启发函数可采纳的,那么,其中是到最近目标的真实耗散。(例如曼哈顿距离)

前提:启发函数可采纳的,那么A* 树搜索是最优的。

  • 代价一致搜索在所有“方向”上等可能的扩展
  • A*搜索主要朝着目标扩展,而且能够保证最优性

对于解决难的搜索问题,大部分工作就是想出可采纳的启发函数。通常可采纳启发函数是松弛问题的解的耗散

A*图搜索与树搜索的区别在于图搜索不允许访问相同结点

图搜索中,如果启发函数是一致的,A* 搜索是最优的。

一致的:启发函数不仅仅要是可采纳的,同时在每一个局部的位置也要合理。

也就是:如果沿路径的节点估计耗散值单调递增,即,那么A*图搜索具备最优性。

通常,天然的可采纳启发函数是倾向于一致的,特别是从松弛问题中获得的启发函数

局部搜索

树搜索在边缘集合中保留未探索的替代路径(确保完备性)

局部搜索: 改进单一选项直到不能再改善为止

爬山法搜索

模拟退火搜索:避免局部极大(允许向山下移动)

遗传算法——自然选择

  • 基于适应度函数,在每步中保留N个最好状态
  • 配对杂交操作
  • 产生可选的变异
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + 研究生课程:现代信息检索-第4讲 通配查询与拼写矫正 + + /2022/09/08/UCAS/information-retrieval/information-retrieval-4/ + + 《现代信息检索》课程笔记:第4讲 通配查询与拼写矫正

第4讲 通配查询与拼写矫正

词典

词典是指存储词项词汇表的数据结构:作用:存储词项以及定位词项

词项词汇表指的是具体数据,而词典指的是数据结构

采用定长数组的词典结构对每个词项需要存储文档频率和指向倒排记录表的指针

词项定位(查词典):在词典中查找给定关键字

用于词项定位的数据结构:主要是哈希表和树

有些IR系统用哈希表,有些系统用树结构

采用哈希表或树的准则:

  • 词项数目是否固定(词项数目是否持续增长)(固定采用哈希表更好,因为快,但是动态更新的代价比较高)
  • 词项的相对访问频率如何
  • 词项的数目有多少

哈希函数:输入词项,输出正整数(通常是地址)

  • 每个词项通过哈希函数映射成一个整数
  • 尽可能避免冲突
  • 查询处理时: 对查询词项进行哈希,如果有冲突,则解决冲突,最后在定长数组中定位
  • 优点:
    • 在哈希表中的定位速度快于树中的定位速度
    • 查询时间是常数
  • 缺点:
    • 无法处理词项的微小变形
    • 不支持前缀搜索
    • 如果词汇表不断增大,需要定期对所有词项重新哈希

树可以支持前缀查找(相当于对词典再建一层索引)

最简单的树结构:二叉树,搜索速度略低于哈希表方式,时间复杂度为, 其中是词汇表大小,即所有词项的数目

仅仅对平衡树成立,使二叉树重新保持平衡开销很大

B-树:每个内部节点的子节点数目在之间,其中为合适的正整数

通配查询

通配查询:包含通配符的查询

mon*: 找出所有包含以mon开头的词项的文档

如果采用B-树词典结构,那么实现起来非常容易,只需要返回区间mon ≤ t < moo上的词项t

*mon: 找出所有包含以mon结尾的词项的文档

将所有的词项倒转过来,然后基于它们建一棵附加的树,返回区间nom ≤ t < non上的词项t

词项中间的*号处理:mnchen

  • 在B-树中分别查找满足m*和 *nchen的词项集合,然后求交集(开销很大)

轮排索引:(主要思想:让星号出现在词汇的末尾)

  • 将每个通配查询旋转,使*出现在末尾
  • 将每个旋转后的结果存放在词典中,即B-树中

轮排索引的查找过程:

  • 将查询进行旋转,将通配符旋转到右部
  • 同以往一样查找B-树,得到匹配的所有词项,将这些词项对应的倒排记录表取出

相对于通常的B-树,轮排索引(轮排树)的空间要大4倍以上 (经验值)

k-gram索引:枚举一个词项中所有连读的k个字符构成k-gram

  • 构建一个倒排索引,此时词典部分是所有的k-gram,倒排记录表部分是包含某个k-gram的所有词项
  • 相当于对词项再构建一个倒排索引(二级索引)
  • 比轮排索引空间开销要小
  • 但是可能返回一些伪正例,需要进行后过滤

k-gram存在两个倒排索引:

  • 词典-文档的倒排索引基于词项返回文档
  • k-gram索引用于查找词项,即基于查询所包含的k-gram来查找所有的词项

k-gram索引 vs. 轮排索引

  • k-gram索引的空间消耗小
  • 轮排索引不需要进行后过滤

拼写矫正

涉及的任务:拼写错误检测和拼写错误矫正(并不是先后的关系)

错误种类:非词汇错误(纠正的时候不需要考虑上下文)和真实词汇错误(纠正的时候需要考虑上下文)

两个主要用途

  • 纠正待索引文档
  • 纠正用户的查询

非词汇拼写错误检测:词典中不存在的词均视为错误

  • 一般来说,词典越大越好
  • Web很大,但是充满了拼写错误,因此并不是一个很好的词典

非词汇拼写错误矫正:

  • 产生候选:与错误书写的单词相似的真实词汇
  • 选择最好的候选词:最短加权编辑距离和最高噪声通道概率
  • 候选集:找到发音相似的候选词、找到拼写相似的候选词、将 w 也包括在候选集里

词独立法:

  • 词典中不存在的词均视为错误
  • 只检查每个单词本身的拼写错误
  • 但是如果某个单词拼写错误后变成另外一个单词,则无法查出

采用拼写噪声通道模型:通过贝叶斯定理求解:

正确拼写为,错误拼写为,则

可以通过文档进行估计

  • 拼写相近的词:Damerau-Levenshtein编辑距离(插入、删除、替换、两个相邻字母的替换)
    • 80% 的拼写错误到正确拼写的编辑距离 = 1,几乎所有拼写错误到正确拼写的编辑距离 <= 2

产生候选词的方法:

  1. 遍历词典,计算每一个词的编辑距离
  2. 生成所有编辑距离 ≤ k (例如, k = 1 或 2)的词,然后与词典取交集
  3. 建立一个字符k-gram索引,从词典中找到共享最多k-grams的词项(例如,基于Jaccard系数计算)
  4. 使用Levenshtein 有限状态转换机快速计算
  5. 预先计算一个词项到可能的 正确词项/拼写错误的映射表

语言模型

若有包含个词条的大文本语料,则是词频。(一元先验概率)

通道模型概率-计算错误概率:混淆“矩阵”(计算一个字符变为另一个字符的概率如何)

  • 混淆矩阵构建也可以考虑键盘的邻近型

然后可以计算噪声通道模型

计算的过程中可以添加加一概率平滑:上述混淆矩阵的例子很难避免某种操作样本数为0,要避免这种概率为0的情况

真实词汇错误的纠正通常需要考虑上下文

上下文敏感法:

  • 纠错时要考虑周围的单词
  • 产生候选:与错误书写的单词相似的真实词汇
    • 找到发音相似的候选词
    • 找到拼写相似的候选词
    • 选择最好的候选词:最短加权编辑距离、最高噪声通道概率

真实词汇拼写矫正的噪声通道:二元语言模型,将一元模型与二元模型插值

  • 给定句子,为每个词产生一个候选词集合,最后选择序列使得概率最大

通道模型的改进:

  • 为概率增加一个权重
  • 允许更丰富的编辑操作
  • 将发音融入到通道模型中
  • 将设备融入到通道模型中
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第11章 数据结构 + + /2022/09/07/Leetcode/Leetcode-101/Leetcode-101-11/ + + Leetcode 刷题笔记-Leetcode 101 第11章 数据结构

数据结构

数组

Leetcode 448

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

class Solution {public:    vector<int> findDisappearedNumbers(vector<int>& nums) {        int n = nums.size();        vector<bool> vt(n+1,false);        for(int i=0;i<n;++i){            vt[nums[i]] = true;        }        vector<int> result;        for(int i=1;i<=n;++i){            if(vt[i] == false){                result.push_back(i);            }        }        return result;    }};

分析:扫一遍确认一下,再扫一遍找出结果。

一遍AC

Leetcode 48

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像原地顺时针旋转 90 度。

class Solution {public:    void rotate(vector<vector<int>>& matrix) {        int temp = 0, n = matrix.size()-1;        for (int i = 0; i <= n / 2; ++i) {            for (int j = i; j < n - i; ++j) {                temp = matrix[j][n-i];                matrix[j][n-i] = matrix[i][j];                matrix[i][j] = matrix[n-j][i];                matrix[n-j][i] = matrix[n-i][n-j];                matrix[n-i][n-j] = temp;            }        }    }};

分析:转转转

错误:没想到原地旋转的思路。

Leetcode 240

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:每行的元素从左到右升序排列,每列的元素从上到下升序排列。

class Solution {public:    bool searchMatrix(vector<vector<int>>& matrix, int target) {        int m = matrix.size();        int n = matrix[0].size();        int x = 0;        int y = n-1;        while(x >= 0 && x < m && y >= 0 && y < n){            if(matrix[x][y] == target){                return true;            }            else if(target < matrix[x][y]){                y -= 1;            }            else{                x += 1;            }        }        return false;    }};

分析:从右上角开始查找,若当前值大于待搜索值,我们向左移动一位;若当前值小于待搜索值,我们向下移动一位。如果最终移动到左下角时仍不等于待搜索值,则说明待搜索值不存在于矩阵中。

错误:找到思路后一遍AC

Leetcode 769

给定一个长度为 n 的整数数组 arr ,它表示在 [0, n - 1] 范围内的整数的排列。我们将 arr 分割成若干 (即分区),并对每个块单独排序。将它们连接起来后,使得连接的结果和按升序排序后的原数组相同。返回数组能分成的最多块数量。

class Solution {public:    int maxChunksToSorted(vector<int>& arr) {        int n = arr.size();        int result = 0;        int maxnum = 0;        for(int i=0;i<n;++i){            maxnum = max(maxnum,arr[i]);            if(maxnum == i){                ++result;            }        }        return result;    }};

分析:从左往右遍历,同时记录当前的最大值,每当当前最大值等于数组位置时,我们可以多一次分割。

错误:看了思路后实现的

栈和队列

Leetcode 232

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty

class MyQueue {    stack<int> st1;    stack<int> st2;public:    MyQueue() {    }      void push(int x) {        st1.push(x);    }      int pop() {        while(!st1.empty()){            st2.push(st1.top());            st1.pop();        }        int a = st2.top();        st2.pop();        while(!st2.empty()){            st1.push(st2.top());            st2.pop();        }        return a;    }      int peek() {        while(!st1.empty()){            st2.push(st1.top());            st1.pop();        }        int a = st2.top();        while(!st2.empty()){            st1.push(st2.top());            st2.pop();        }        return a;    }      bool empty() {        return st1.empty();    }};

分析:比较简单,也没有算法

错误:全局变量没定义好,返回值漏掉了,调通了。

Leetcode 155

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

class MinStack {    stack<int> s1;    stack<int> mins;public:    MinStack() {    }      void push(int val) {        if(mins.empty() || val <= mins.top()){            mins.push(val);        }        s1.push(val);    }      void pop() {        int a = s1.top();        s1.pop();        if(mins.top() == a){            mins.pop();        }    }      int top() {        return s1.top();    }      int getMin() {        return mins.top();    }};

分析:可以额外建立一个新栈,栈顶表示原栈里所有值的最小值。每当在原栈里插入一个数字时,若该数字小于等于新栈栈顶,则表示这个数字在原栈里是最小值,我们将其同时插入新栈内。每当从原栈里取出一个数字时,若该数字等于新栈栈顶,则表示这个数是原栈里的最小值之一,我们同时取出新栈栈顶的值。

错误:没有思路

Leetcode 20

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

class Solution {public:    bool isValid(string s) {        stack<char> st;        int n = s.size();        for(int i=0;i<n;++i){            if(s[i] == '(' || s[i] == '{' || s[i] == '['){                st.push(s[i]);            }            else{                if(st.empty()){                    return false;                }                else if(st.top() == '[' && s[i] == ']'){                    st.pop();                }                else if(st.top() == '(' && s[i] == ')'){                    st.pop();                }                else if(st.top() == '{' && s[i] == '}'){                    st.pop();                }                else{                    return false;                }            }        }        if(st.empty()){            return true;        }        return false;    }};

分析:用栈进行匹配即可

错误:没有考虑只有一个左括号的情况,改正后调通了

单调栈

Leetcode 739

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

class Solution {public:    vector<int> dailyTemperatures(vector<int>& temperatures) {        int n = temperatures.size();        vector<int> answer(n);        stack<int> s;        for(int i=0;i<n;++i){            while (!s.empty()) {                int pre_index = s.top();                if (temperatures[i] <= temperatures[pre_index]) {                    break;                }                s.pop();                answer[pre_index] = i - pre_index;            }            s.push(i);        }        return answer;    }};

分析:我们可以维持一个单调递减的栈,表示每天的温度;为了方便计算天数差,我们这里存放位置(即日期)而非温度本身。我们从左向右遍历温度数组,对于每个日期p,如果p的温度比栈顶存储位置q的温度高,则我们取出q,并记录q需要等待的天数为p-q;我们重复这一过程,直到p的温度小于等于栈顶存储位置的温度(或空栈)时,我们将p插入栈顶,然后考虑下一天。在这个过程中,栈内数组永远保持单调递减,避免了使用排序进行比较。最后若栈内剩余一些日期,则说明它们之后都没有出现更暖和的日期。

错误:感觉并不是非常理解。

优先队列

Leetcode 23

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

class Solution {public:    struct Comp{        bool operator()(ListNode* l1,ListNode* l2){            return l1->val > l2->val;        }    };    ListNode* mergeKLists(vector<ListNode*>& lists) {        if(lists.empty()){            return nullptr;        }        priority_queue<ListNode*,vector<ListNode*>,Comp> q;        for(ListNode* list:lists){            if(list){                q.push(list);            }        }        ListNode* dummy = new ListNode(0), *cur = dummy;        while (!q.empty()) {            cur->next = q.top();            q.pop();            cur = cur->next;            if (cur->next) {                q.push(cur->next);            }        }        return dummy->next;    }};

分析:即把所有的链表存储在一个优先队列中,每次提取所有链表头部节点值最小的那个节点,直到所有链表都被提取完为止。

错误:优先队列不是很熟悉

Leetcode 218

给定建筑物的起止位置和高度,返回建筑物轮廓(天际线)的拐点。

Hard难度,想不太明白,暂时不做了

分析:使用优先队列储存每个建筑物的高度和右端(这里使用pair,其默认比较函数是先比较第一个值,如果相等则再比较第二个值),从而获取目前会拔高天际线、且妨碍到前一个建筑物(的右端端点)的下一个建筑物。

错误:没有思路

双端队列

Leetcode 239

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

class Solution {public:    vector<int> maxSlidingWindow(vector<int>& nums, int k) {        vector<int> result;        deque<int> dq;        int n = nums.size();        for(int i=0;i<n;++i){            if(!dq.empty() && nums[i] > nums[dq.back()]){                while(!dq.empty() && nums[dq.back()] < nums[i]){                    dq.pop_back();                }            }            dq.push_back(i);            if(i >= k-1){                result.push_back(nums[dq.front()]);                if(nums[i-k+1] == nums[dq.front()]){                    dq.pop_front();                }            }        }        return result;    }};

分析:利用双端队列进行操作:每当向右移动时,把窗口左端的值从队列左端剔除,把队列右边小于窗口右端的值全部剔除。这样双端队列的最左端永远是当前窗口内的最大值。

错误:理解了思路后调通了。

哈希表

Leetcode 1

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target的那两个整数,并返回它们的数组下标。

class Solution {public:    vector<int> twoSum(vector<int>& nums, int target) {        vector<int> result;        unordered_map<int, int> hash;        int n = nums.size();        for(int i=0;i<n;++i){            if(hash.count(target - nums[i])){                result.push_back(hash[target - nums[i]]);                result.push_back(i);                break;            }            hash[nums[i]] = i;        }        return result;    }};

分析:利用哈希表存储遍历过的值以及它们的位置,每次遍历到位置i 的时候,查找哈希表里是否存在target - nums[i],若存在,则说明这两个值的和为target。

一遍AC

Leetcode 128

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

class Solution {public:    int longestConsecutive(vector<int>& nums) {        unordered_set<int> hash;        for(const int & num:nums){            hash.insert(num);        }        int ans = 0;        while(!hash.empty()){            int cnt = *(hash.begin());            hash.erase(cnt);            int pre = cnt - 1;            int next = cnt + 1;            while(!hash.empty() && hash.count(pre)){                hash.erase(pre);                --pre;            }            while(!hash.empty() && hash.count(next)){                hash.erase(next);                ++next;            }            ans = max(ans,next-pre-1);        }        return ans;    }};

分析:把所有数字放到一个哈希表,然后不断地从哈希表中任意取一个值,并删除掉其之前之后的所有连续数字,然后更新目前的最长连续序列长度。重复这一过程,我们就可以找到所有的连续数字序列。

错误:看了思路后实现了

Leetcode 149

给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。

class Solution {public:    int maxPoints(vector<vector<int>>& points) {        unordered_map<double, int> hash; // <斜率, 点个数>        int max_count = 0, same = 1, same_y = 1;        for (int i = 0; i < points.size(); ++i) {            same = 1, same_y = 1;            for (int j = i + 1; j < points.size(); ++j) {                if (points[i][1] == points[j][1]) {                    ++same_y;                    if (points[i][0] == points[j][0]) {                        ++same;                    }                }                else {                    double dx = points[i][0] - points[j][0], dy = points[i][1] -                    points[j][1];                    ++hash[dx/dy];                }            }            max_count = max(max_count, same_y);            for (auto item : hash) {                max_count = max(max_count, same + item.second);            }            hash.clear();        }        return max_count;    }};

分析:对于每个点,我们对其它点建立哈希表,统计同一斜率的点一共有多少个。这里利用的原理是,一条线可以由一个点和斜率而唯一确定。另外也要考虑斜率不存在和重复坐标的情况。

错误:好麻烦先算了

多重集合和映射

Leetcode 332

给你一份航线列表 tickets ,其中 tickets[i] = [from<sub>i</sub>, to<sub>i</sub>] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

class Solution {public:    vector<string> findItinerary(vector<vector<string>>& tickets) {        vector<string> ans;        if (tickets.empty()) {            return ans;        }        unordered_map<string, multiset<string>> hash;        for (const auto & ticket: tickets) {            hash[ticket[0]].insert(ticket[1]);        }        stack<string> s;        s.push("JFK");        while (!s.empty()) {            string next = s.top();            if (hash[next].empty()) {                ans.push_back(next);                s.pop();            }             else {                s.push(*hash[next].begin());                hash[next].erase(hash[next].begin());            }        }        reverse(ans.begin(), ans.end());        return ans;    }};

分析:本题可以先用哈希表记录起止机场,其中键是起始机场,值是一个多重集合,表示对应的终止机场。因为一个人可能坐过重复的线路,所以我们需要使用多重集合储存重复值。储存完成之后,我们可以利用栈来恢复从终点到起点飞行的顺序,再将结果逆序得到从起点到终点的顺序。

错误:多重集合的第一道题,也是唯一一道题,不是很明白

前缀和和积分图

Leetcode 303

设计一个数据结构,使得其能够快速查询给定数组中,任意两个位置间所有数字的和。

class NumArray {    vector<int> frontsum;public:    NumArray(vector<int>& nums) {        for(int i=0;i<nums.size();++i){            if(i == 0){                frontsum.push_back(nums[i]);            }            else{                frontsum.push_back(nums[i] + frontsum[i-1]);            }        }    }      int sumRange(int left, int right) {        if(left == 0){            return frontsum[right];        }        return frontsum[right] - frontsum[left-1];    }};

分析:前缀和即可

一遍AC

Leetcode 304

设计一个数据结构,使得其能够快速查询给定矩阵中,任意两个位置包围的长方形中所有数字的和。

class NumMatrix {    vector<vector<int>> frontmatrix;public:    NumMatrix(vector<vector<int>>& matrix) {        int m = matrix.size();        int n = matrix[0].size();        for(int i=0;i<m;++i){            vector<int> temp;            for(int j=0;j<n;++j){                if(i == 0 && j == 0){                    temp.push_back(matrix[i][j]);                }                else if(i == 0){                    temp.push_back(matrix[i][j] + temp[j-1]);                }                else if(j == 0){                    temp.push_back(matrix[i][j] + frontmatrix[i-1][j]);                }                else{                    temp.push_back(matrix[i][j] + frontmatrix[i-1][j] + temp[j-1] - frontmatrix[i-1][j-1]);                }            }            frontmatrix.push_back(temp);        }    }      int sumRegion(int row1, int col1, int row2, int col2) {        if(row1 == 0 && col1 == 0){            return frontmatrix[row2][col2];        }        else if(row1 == 0){            return frontmatrix[row2][col2]-frontmatrix[row2][col1-1];        }        else if(col1 == 0){            return frontmatrix[row2][col2]-frontmatrix[row1-1][col2];        }        return frontmatrix[row2][col2]-frontmatrix[row2][col1-1]-frontmatrix[row1-1][col2]+frontmatrix[row1-1][col1-1];    }};

分析:二维上的前缀和(积分图)即可

一遍AC

Leetcode 560

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。

class Solution {public:    int subarraySum(vector<int>& nums, int k) {        int count = 0, psum = 0;        unordered_map<int, int> hashmap;        hashmap[0] = 1; // 初始化很重要        for (int i: nums) {            psum += i;            count += hashmap[psum-k];            ++hashmap[psum];        }        return count;    }};

分析:本题同样是利用前缀和,不同的是这里我们使用一个哈希表 hashmap,其键是前缀和,而值是该前缀和出现的次数。在我们遍历到位置i 时,假设当前的前缀和是 psum ,那么 hashmap[psum-k]即为以当前位置结尾、满足条件的区间个数。

错误:直接使用前缀和会超时,然而这个短代码挺难理解的样子。

练习

Leetcode 566

在 MATLAB 中,有一个非常有用的函数 reshape ,它可以将一个 m x n 矩阵重塑为另一个大小不同(r x c)的新矩阵,但保留其原始数据。给你一个由二维数组 mat 表示的 m x n 矩阵,以及两个正整数 rc ,分别表示想要的重构的矩阵的行数和列数。重构后的矩阵需要将原始矩阵的所有元素以相同的行遍历顺序填充。如果具有给定参数的 reshape 操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。

class Solution {public:    vector<vector<int>> matrixReshape(vector<vector<int>>& mat, int r, int c) {        int m = mat.size();        int n = mat[0].size();        if(m*n != r*c){            return mat;        }        int rowindex = 0;        int colindex = 0;        vector<vector<int>> result(r,vector<int>(c));        for(int i=0;i<r;++i){            for(int j=0;j<c;++j){                result[i][j] = mat[rowindex][colindex];                ++colindex;                if(colindex == n){                    ++rowindex;                    colindex = 0;                }            }        }        return result;    }};

分析:很简单的小题,没有任何难度。

一遍AC

Leetcode 225

用两个队列实现一个栈

class MyStack {    queue<int> q1;    queue<int> q2;public:    MyStack() {    }      void push(int x) {        q1.push(x);        return;    }      int pop() {        while(q1.size() != 1){            q2.push(q1.front());            q1.pop();        }        int a = q1.front();        q1.pop();        while(!q2.empty()){            q1.push(q2.front());            q2.pop();        }        return a;    }      int top() {        while(q1.size() != 1){            q2.push(q1.front());            q1.pop();        }        int a = q1.front();        q2.push(q1.front());        q1.pop();        while(!q2.empty()){            q1.push(q2.front());            q2.pop();        }        return a;    }      bool empty() {        return q1.empty();    }};

分析:也是很简单的题,倒腾倒腾数字就行了

一遍AC

Leetcode 503

给定一个循环数组 numsnums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的下一个更大元素

class Solution {public:    vector<int> nextGreaterElements(vector<int>& nums) {        int n = nums.size();        stack<int> st;        vector<int> result(n,-1);        for(int i=0;i<2*n-1;++i){            while(!st.empty() && nums[i%n] > nums[st.top()]){                result[st.top()] = nums[i%n];                st.pop();            }            st.push(i%n);        }        return result;    }};

分析:使用单调栈解决本题。单调栈中保存的是下标,从栈底到栈顶的下标在数组 nums中对应的值是单调不升的。每次我们移动到数组中的一个新的位置 i,我们就将当前单调栈中所有对应值小于 nums[i]的下标弹出单调栈,这些值的下一个更大元素即为 nums[i]。随后我们将位置 i入栈。

错误:没有想到单调栈,看了一下思路后自己实现的。

Leetcode 217

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false

class Solution {public:    bool containsDuplicate(vector<int>& nums) {        unordered_map<int,int> mp;        int n = nums.size();        for(int i=0;i<n;++i){            if(mp.find(nums[i]) == mp.end()){                mp[nums[i]] = 1;            }            else{                return true;            }        }        return false;    }};

分析:非常简单的哈希表,没什么难度

错误:下标和数字插入看的不太对

Leetcode 697

给定一个非空且只包含非负数的整数数组 nums,数组的的定义是指数组里任一元素出现频数的最大值。你的任务是在 nums 中找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度。

class Solution {public:    int findShortestSubArray(vector<int>& nums) {        unordered_map<int,vector<int>> mp;        int n = nums.size();        for(int i=0;i<n;++i){            if(mp.find(nums[i]) == mp.end()){                mp[nums[i]].push_back(i);                mp[nums[i]].push_back(i);                mp[nums[i]].push_back(1);            }            else{                if(i < mp[nums[i]][0]){                    mp[nums[i]][0] = i;                }                if(i > mp[nums[i]][1]){                    mp[nums[i]][1] = i;                }                ++mp[nums[i]][2];            }        }        int maxnum = 0;        int result = n+1;        for(auto it = mp.cbegin();it != mp.cend();++it){            if(it->second[2] > maxnum){                maxnum = it->second[2];                result = it->second[1]-it->second[0]+1;            }            else if (it->second[2] == maxnum){                result = min(result,it->second[1]-it->second[0]+1);            }        }        return result;    }};

分析:比较简单的数据结构应用题

错误:语法问题,还有下标数字问题,后面自己调通

Leetcode 594

和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。现在,给你一个整数数组 nums ,请你在所有可能的子序列中找到最长的和谐子序列的长度。数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。

class Solution {public:    int findLHS(vector<int>& nums) {        int n = nums.size();        int ans = 0;        unordered_map<int,int> mp;        for(int i=0;i<n;++i){            if(mp.find(nums[i]) == mp.end()){                mp[nums[i]] = 1;            }            else{                ++mp[nums[i]];            }        }        for(auto it = mp.cbegin();it != mp.cend();++it){            if(mp.find(it->first-1) != mp.end()){                ans = max(ans,it->second + mp[it->first-1]);            }            if(mp.find(it->first+1) != mp.end()){                ans = max(ans,it->second + mp[it->first+1]);            }        }        return ans;    }};

分析:看起来挺像动态规划,实际上并不是,统计一下就好了

错误:还是map迭代器不太熟练,后面调通。

Leetcode 287

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

class Solution {public:    int findDuplicate(vector<int>& nums) {        int n = nums.size();        int len = nums.length;        for (int num : nums) {            int idx = Math.abs(num);            if (nums[idx] < 0) {                return idx;            }            nums[idx] = -nums[idx];        }        return len;    }};

分析:考虑到数组元素值的范围是 [1,n],但数组长度为 n+1,那么很显然在遍历数组的时候,我们将数组的值变为其对应的负数,那么再次遇到负数就得到了答案。

错误:上面不是最优解,没有想到最优解

Leetcode 313

超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。给你一个整数 n 和一个整数数组 primes ,返回第 n超级丑数 。题目数据保证第 n超级丑数32-bit 带符号整数范围内。

class Solution {public:    int nthSuperUglyNumber(int n, vector<int>& primes) {        vector<long> dp(n + 1);        int m = primes.size();        vector<int> pointers(m, 0);        vector<long> nums(m, 1);        for (int i = 1; i <= n; i++) {            long minNum = INT_MAX;            for (int j = 0; j < m; j++) {                minNum = min(minNum, nums[j]);            }            dp[i] = minNum;            for (int j = 0; j < m; j++) {                if (nums[j] == minNum) {                    pointers[j]++;                    nums[j] = dp[pointers[j]] * primes[j];                }            }        }        return dp[n];    }};

分析:动态规划,没有思路

错误:没有思路

Leetcode 870

给定两个大小相等的数组 nums1nums2nums1 相对于 nums优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。返回 nums1 任意排列,使其相对于 nums2 的优势最大化。

class Solution {public:    vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {        sort(nums1.begin(),nums1.end());        vector<pair<int,int>> vt;        for(int i=0;i<nums2.size();i++){            vt.push_back(make_pair(nums2[i],i));        }        sort(vt.begin(),vt.end());        vector<int> ans(nums2.size());        int l1=0,r1=nums1.size()-1,l2=0,r2=nums2.size()-1;        while(r2>=0){            if(nums1[r1]>vt[r2].first){                ans[vt[r2].second]=nums1[r1];                r1--;            }            else{                 ans[vt[r2].second]=nums1[l1];                 l1++;            }            r2--;        }              return ans;    }};

分析:田忌赛马,能打就打,打不过让最菜的送人头。

错误:没思路

Leetcode 307

线段树先算了

总结

数据结构是最最基础的算法,没有合适的数据结构就不可能有高效的算法。普通的数据结构掌握的还不错,但是有一些比较高级的数据结构练的比较少,掌握的不太好。今后要注重这些比较高级的数据结构,并尽量去在实际中应用。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:模式识别与机器学习-第2章 统计判别 + + /2022/09/06/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-2/ + + 《模式识别与机器学习》课程笔记:第2章 统计判别

第2章 统计判别

统计学(statistics)是用以收集数据,分析数据和由数据得出结论的一组概念、原则和方法。

作为统计判别问题的模式分类

  • 模式识别的目的就是要确定某一个给定的模式样本属于哪一类。
  • 可以通过对被识别对象的多次观察和测量,构成特征向量,并将其作为某一个判决规则的输入,按此规则来对样本进行分类。
  • 在获取模式的观测值时,有些事物具有确定的因果关系,即在一定的条件下,它必然会发生或必然不发生。
    • 例如识别一块模板是不是直角三角形,只要凭“三条直线边闭合连线和一个直角”这个特征,测量它是否有三条直线边的闭合连线并有一个直角,就完全可以确定它是不是直角三角形。这种现象是确定性的现象。
  • 但在现实世界中,由许多客观现象的发生,就每一次观察和测量来说,即使在基本条件保持不变的情况下也具有不确定性。
  • 只有在大量重复的观察下,其结果才能呈现出某种规律性,即对它们观察到的特征具有统计特性。
  • 特征值不再是一个确定的向量,而是一个随机向量
  • 此时,只能利用模式集的统计特性来分类,以使分类器发生错误的概率最小

给定观测值,判断其属于类还是类,作出某次判断时的错误率是:

最小化误差概率条件下,若,则;若,则

贝叶斯判别原则

两类模式集的分类:

目的:要确定是属于类还是类,要看是来自于类的概率大还是来自类的概率大。

根据概率判别规则,若,则;若,则

由贝叶斯定理,后验概率可由类别的先验概率的条件概率密度来计算,即:

,其中也称为似然函数。

与概率判别规则结合,则若,则;若,则

不等式转换一下:

,则

,则

其中,称为似然比,称为似然比的判决阈值

此判别称为贝叶斯判别。

贝叶斯判别的推广:

  • 允许使用多于一个特征:标量、向量、多种特征向量
  • 允许多于两种类别状态的情形
  • 允许有其他行为而不仅仅是判定类别:如后验概率接近的情况下,如果拒绝判断的代价不大,可以拒绝判断。

可以通过引入一个更一般的损失函数来替代误差概率

朴素贝叶斯

特征是多维向量时,假设各个特征之间相互独立

贝叶斯最小风险判别

当考虑到对于某一类的错误判决要比对另一类的判决更为关键时,就需要把最小错误概率的贝叶斯判别做一些修正,提出条件平均风险

类问题,如果观察样本被判定属于类,则条件平均风险

为将本应属于类的模式判别成属于类的是非代价。

,即判别正确,得分,可以取负值或零,表示不失分。

,即判别错误,失分,应取正值。

意义:

  • 对于自然属性是属于类的模式来说,它来自类的概率应为
  • 如果分类器判别是属于类,但它实际上来自类,也就是说分类器失败,这时为失分,对应的条件风险为后验概率进行的加权运算。
  • 由于模式的自然属性可能来自类中的任一类,因此可将观察样本指定为类的条件平均风险用的公式运算。

分类器对每一个模式种可能的类别可供选择,若对每一个计算出全部类别的平均风险值,并且将指定为是具有最小风险值的那一类,则这种分类器称为最小平均条件风险分类器。

按贝叶斯公式,最小平均条件风险可写成:

可以舍去公共项,则可以简化为:

也是贝叶斯分类器,只是它的判别方法不是按错误概率最小作为标准,而是按平均条件风险作为标准。

举例若

当分类器将判别为时:

当分类器将判别为时:

,则被判定为属于

此时:

即:

通常,因此

时,

左边为似然比:,右边为阈值

因此两类模式的贝叶斯判别条件为:

  • ,则
  • ,则
  • ,则可以做任意判别。

通常,当判别正确时,不失分,可选常数

判别错误时,可选常数

此时:

对于类情况来说,若仍按判对失分为0,判错失分为1记,则

贝叶斯最小错误判别是计算得到某个类别的概率,而最小风险判别是计算得到某个类别后存在风险的概率。两者正好相反。

正态分布模式的贝叶斯分类器

出发点:当已知或者有理由设想类概率密度函数是多变量的正态分布时,贝叶斯分类器可以导出一些简单的判别函数。由于正态密度函数易于分析,且对许多重要的实际应用又是一种合适的模型,因此受到很大的重视。

种模式类别的多变量正态类密度函数:(参考数学推导

其中,每一类模式的分布密度都完全被其均值向量和协方差矩阵所规定

当协方差矩阵的全部非对角线上的元素都为零时,多变量正态类密度函数可简化为个单变量正态类密度函数的乘积,个单变量为互相独立的

已知类别的判别函数可写成如下形式:

可以取自然对数的形式以方便计算:

代入正态类密度函数,可以得到:

去掉与无关的项,最终可以得到:

即为正态分布模式的贝叶斯判别函数。

因此判别函数是一个超二次曲面,对于正态分布模式的贝叶斯分类器,两个模式类别之间用一个二次判别界面分开,就可以求得最优的分类效果。

当M=2且类模式都是正态分布的情况

  1. 时:

两类模式的正态分布:表示为表示为两类的判别函数对应为:

判别界面的二次型方程,即两类模式可用二次判别界面分开。

是二维时,判别界面为二次曲线,如椭圆,圆,抛物线或双曲线等

  1. 时:

为对称矩阵,上式可简化为:

由此可导出类别间的判别界面为:

判别界面为的线性函数,为一超平面。

是二维时,判别界面为一直线

决策边界的特征:

  • 如果两种分布的协方差矩阵相等并且与单位阵成比例,且先验概率相等。则决策边界垂直于两个中心的连线。
  • 协方差矩阵相等,判决边界同样是超平面。随着先验概率的改变,判决边界也随之改变;对于差别较大的离散先验概率而言,判决边界不会落于中心点之间。

贝叶斯分类规则是基于统计概念的。如果只有少数模式样本,一般较难获得最优的结果。

实际代码编写

defBayesian(data,label,P):    if data.shape[0] != label.shape[0]: # 如果数据和标签的数量不相同        print('Error!')        sys.exit()    M = data[0].shape[0] # 获取数据的维度    data_list = [[],[]] # 将不同类别的数据分开存储    data_list[0] = np.array([data[i] for i inrange(len(label)) if label[i] ==0])    data_list[1] = np.array([data[i] for i inrange(len(label)) if label[i] ==1])    # 计算均值向量    m0 = np.sum(data_list[0],axis=0) / data_list[0].shape[0]    m1 = np.sum(data_list[1],axis=0) / data_list[1].shape[0]    # 计算协方差矩阵    C0 = np.sum(np.array([np.dot((data_list[0][i] - m0).reshape(-1,1), \        (data_list[0][i] - m0).reshape(1,-1)) for i inrange(data_list[0].shape[0])]),axis=0) / data_list[0].shape[0]    C1 = np.sum(np.array([np.dot((data_list[1][i] - m1).reshape(-1,1),\        (data_list[1][i] - m1).reshape(1,-1)) for i inrange(data_list[1].shape[0])]),axis=0) / data_list[1].shape[0]    return np.dot(m0-m1,np.linalg.inv(C0)),np.log(P[0]) - np.log(P[1]) +0.5* (np.dot(np.dot(m1.reshape(1,-1),\        np.linalg.inv(C0)),m1.reshape(-1,1)) - np.dot(np.dot(m0.reshape(1,-1),np.linalg.inv(C0)),m0.reshape(-1,1)))

均值向量和协方差矩阵的参数估计

在贝叶斯分类器中,构造分类器需要知道类概率密度函数,如果按先验知识已知其分布,则只需知道分布的参数即可。(例如:类概率密度是正态分布,它完全由其均值向量和协方差矩阵所确定)。

对均值向量和协方差矩阵的估计即为贝叶斯分类器中的一种参数估计问题。

参数估计的两种方式:

  • 将参数作为非随机变量来处理,例如矩估计就是一种非随机参数的估计。
  • 随机参数的估计,即把这些参数看成是随机变量,例如贝叶斯参数估计。

均值和协方差矩阵的非随机参数的估计

均值和协方差矩阵的估计量定义

设模式的类概率密度函数为,则其均值向量定义为:

,其中

若以样本的平均值作为均值向量的近似值,则均值估计量

,其中为样本的数目

协方差矩阵

其中的每个元素

其中,分别为的第个分量。

协方差矩阵写成向量形式为:,(后面这样算更简单一点)

协方差矩阵的估计量(当时)为:

均值和协方差矩阵估计量的迭代运算形式

假设已经计算了个样本的均值估计量,若再加上一个样本,其新的估计量为:

其中为从个样本计算得到的估计量。迭代的第一步应取

协方差矩阵估计量的迭代运算与上述相似:

均值向量和协方差矩阵的贝叶斯学习

将概率密度函数的参数估计量看成是随机变量,它可以是纯量、向量或矩阵。按这些估计量统计特性的先验知识,可以先粗略地预选出它们的密度函数。通过训练模式样本集,利用贝叶斯公式设计一个迭代运算过程求出参数的后验概率密度。当后验概率密度函数中的随机变量的确定性提高时,可获得较准确的估计量。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Pattern Recognition and Machine Learning + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第10章 位运算 + + /2022/09/05/Leetcode/Leetcode-101/Leetcode-101-10/ + + Leetcode 刷题笔记-Leetcode 101 第10章 位运算

位运算

常用技巧

按位异或:x ^ 0s = x, x ^ 1s = ~x, x ^ x = 0

按位与:x & 0s = 0, x & 1s = x, x & x = x

按位或:x | 0s = x, x | 1s = 1s, x | x = x

n & (n - 1)可以去除 n的位级表示中最低的那一位,例如对于二进制表示 11110100,减去 1得到 11110011,这两个数按位与得到 11110000

n & (-n)可以得到n的位级表示中最低的那一位,例如对于二进制表示 11110100,取负得到 00001100,这两个数按位与得到 00000100

位运算基础问题

Leetcode 461

给定两个十进制数字,求它们二进制表示的汉明距离(Hamming distance,即不同位的个数)。

class Solution {public:    int hammingDistance(int x, int y) {        int diff = x ^ y;        int ans = 0;        while(diff){            ans += diff & 1;            diff >>= 1;        }        return ans;    }};

分析:将xy按位异或,则不同的位置为1,相同的位置为0。然后将得到的结果与1进行与操作,为0说明是0,为1说明是1,就计数了1。然后将这个结果逐步右移就可以看出下一位了。

错误:第一道题不太熟悉。

Leetcode 190

颠倒给定的 32 位无符号整数的二进制位

class Solution {public:    uint32_t reverseBits(uint32_t n) {        uint32_t ans = 0;        for(int i=0;i<32;++i){            ans <<= 1;            ans += n & 1;            n >>= 1;        }        return ans;    }};

分析:摆出一个0,然后左移,逐步加上n右移的数字。

错误:不太明白左右移这种东西

Leetcode 136

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

class Solution {public:    int singleNumber(vector<int>& nums) {        int ret = 0;        for (auto e: nums) ret ^= e;        return ret;    }};

分析:一个数字和 0进行按位异或会得到本身,一个数字和本身进行按位异或会得到0。因此在数组内部进行循环,两次的元素出现了一定会变为0,最后剩下的一个就是这个数字本身。

错误:不熟练

二进制特性

Leetcode 342

给定一个整数,判断它是否是4 的次方。

class Solution {public:    bool isPowerOfFour(int n) {        return n > 0 && !(n & (n - 1)) && (n & 1431655765);    }};

分析:首先我们考虑一个数字是不是2 的(整数)次方:如果一个数字n 是2 的整数次方,那么它的二进制一定是0…010…0 这样的形式;考虑到n - 1 的二进制是0…001…1,这两个数求按位与的结果一定是0。因此如果n & (n - 1) 为0,那么这个数是2 的次方。如果这个数也是4 的次方,那二进制表示中1 的位置必须为奇数位。我们可以把n 和二进制的10101…101(即十进制下的1431655765)做按位与,如果结果不为0,那么说明这个数是4的次方。

错误:不理解

Leetcode 318

给你一个字符串数组 words ,找出并返回 length(words[i]) * length(words[j]) 的最大值,并且这两个单词不含有公共字母。如果不存在这样的两个单词,返回 0

class Solution {public:    int maxProduct(vector<string>& words) {        unordered_map<int, int> hash;        int ans = 0;        for (const string & word : words) {            int mask = 0, size = word.size();            for (const char & c : word) {                mask |= 1 << (c - 'a');            }            hash[mask] = max(hash[mask], size);            for (const auto& [h_mask, h_len]: hash) {                if (!(mask & h_mask)) {                    ans = max(ans, size * h_len);                }            }        }        return ans;    }};

分析:怎样快速判断两个字母串是否含有重复数字呢?可以为每个字母串建立一个长度为26的二进制数字,每个位置表示是否存在该字母。如果两个字母串含有重复数字,那它们的二进制表示的按位与不为0

错误:看了思路后自己实现的。

Leetcode 338

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

class Solution {public:    vector<int> countBits(int n) {        vector<int> ans(n+1,0);        for (int i = 1; i <= num; ++i){            dp[i] = i & 1? dp[i-1] + 1: dp[i>>1];        }        return ans;    }};

分析:本题可以利用动态规划和位运算进行快速的求解。定义一个数组dp,其中dp[i] 表示数字i的二进制含有1 的个数。对于第i 个数字,如果它二进制的最后一位为1,那么它含有1 的个数
则为dp[i-1] + 1;如果它二进制的最后一位为0,那么它含有1 的个数和其算术右移结果相同,即dp[i>>1]。

练习

Leetcode 268

给定一个包含 [0, n]n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

class Solution {public:    int missingNumber(vector<int>& nums) {        int n = nums.size();        int total = n * (n + 1) / 2;        int arrSum = 0;        for (int i = 0; i < n; i++) {            arrSum += nums[i];        }        return total - arrSum;    }};

分析:高斯求和后相减即可

Leetcode 693

给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。

class Solution {public:    bool hasAlternatingBits(int n) {        int pre = 0;        int sign = 0;        while(n){            int ans = n & 1;            if(sign == 1){                if(pre == ans){                    return false;                }            }            pre = ans;            sign = 1;            n >>= 1;        }        return true;    }};

分析:存储并判断即可

错误:有一点小问题,很快调通

Leetcode 476

给你一个整数 num ,输出它的补数。

class Solution {public:    int findComplement(int num) {        uint t = 1u << 31;        while (! (t & num)) {            num |= t;            t >>= 1;        }        return ~num;    }};

分析:前边补1,然后就可以直接取反了

错误:没有思路

Leetcode 260

给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

class Solution {public:    vector<int> singleNumber(vector<int>& nums) {        map<int,int> mp;        for(int i=0;i<nums.size();++i){            ++mp[nums[i]];        }        vector<int> result;        for(const auto &[a,b] : mp){            if(b == 1){                result.push_back(a);            }        }        return result;    }};

分析:哈希表算了。。。

一遍AC

总结

这东西和计组挺相关的,面试中应该不会怎么考察这种数学题,但不失为一种运算加速的好办法。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第9章 数学问题 + + /2022/09/05/Leetcode/Leetcode-101/Leetcode-101-9/ + + Leetcode 刷题笔记-Leetcode 101 第9章 数学问题

数学问题

公倍数与公因数

利用辗转相除法求得两个数的最大公因数,将两个数相乘再除以最大公因数即可得到最小公倍数

int gcd(int a, int b) {    return b == 0 ? a : gcd(b, a% b);}int lcm(int a, int b) {    return a * b / gcd(a, b);}

进一步也可以通过扩展欧几里得算法在求得 ab最大公因数的同时,也得到它们的系数 xy,从而使 ax + by = gcd(a, b)

int xGCD(int a, int b, int &x, int &y) {    if (!b) {        x = 1, y = 0;        return a;    }    int x1, y1, gcd = xGCD(b, a % b, x1, y1);    x = y1, y = x1 - (a / b) * y1;    return gcd;}

质数

Leetcode 204

给定整数 n ,返回所有小于非负整数 n 的质数的数量 。

class Solution {public:    int countPrimes(int n) {        if(n <= 2){            return 0;        }        vector<bool> nums(n,true);        for(int i=2;i<n;++i){            if(nums[i] == true){                for(int j=2*i;j<n;j += i){                    nums[j] = false;                }            }        }        return accumulate(nums.begin(),nums.end(),0) - 2;    }};

分析:使用埃拉托斯特尼筛法即可。

错误:有点忘记算法了。

数字处理

给定一个整数 num,将其转化为7进制,并以字符串形式输出。

class Solution {public:    string convertToBase7(int num) {        int sign = 0;        if(num < 0){            num = -num;            sign = 1;        }        if(num == 0){            return "0";        }        string result = "";        while(num/7){            char c = num%7 + '0';            result =  c + result;            num /= 7;        }        if(num != 0){            char b = '0' + num;            result =  b + result;        }        if(sign == 1){            return '-' + result;        }        return result;    }};

分析:直接进制转换就行,注意进制转换的时候用十进制进行过渡比较方便。

错误:磕磕绊绊调通了。

Leetcode 172

给定一个整数 n ,返回 n! 结果中尾随零的数量。

class Solution {public:    int trailingZeroes(int n) {        return n == 0? 0: n / 5 + trailingZeroes(n / 5);    }};

分析:每个尾部的0由2*5 = 10而来,因此我们可以把阶乘的每一个元素拆成质数相乘,统计有多少个2和5。明显的,质因子2的数量远多于质因子5的数量,因此我们可以只统计阶乘结果里有多少个质因子5。

错误:没想到这么好的思路

Leetcode 415

给定两个字符串形式的非负整数 num1num2 ,计算它们的和并同样以字符串形式返回。

class Solution {public:    string addStrings(string num1, string num2) {        int n1 = num1.size();        int n2 = num2.size();        --n1;        --n2;        string result = "";        int cnt = 0;        while(n1 >= 0 && n2 >= 0){            int temp = num1[n1] - '0' + num2[n2] - '0' + cnt;            if(temp >= 10){                cnt = 1;            }            else{                cnt = 0;            }            char c = temp%10 + '0';            result = c + result;            --n1;            --n2;        }        while(n1 >= 0){            int temp = num1[n1] - '0' + cnt;            if(temp >= 10){                cnt = 1;            }            else{                cnt = 0;            }            char c = temp%10 + '0';            result = c + result;            --n1;        }        while(n2 >= 0){            int temp = num2[n2] - '0' + cnt;            if(temp >= 10){                cnt = 1;            }            else{                cnt = 0;            }            char c = temp%10 + '0';            result = c + result;            --n2;        }        if(cnt == 1){            return '1' + result;        }        return result;    }};

分析:大数相加,没什么新的东西

一遍AC

Leetcode 326

给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false

class Solution {public:    bool isPowerOfThree(int n) {        if(n == 1){            return true;        }        for(long long i=3;i<INT_MAX;i*=3){            if(i == n){                return true;            }        }        return false;    }};

分析:比较简单,有更好的解法,需要数学能力

错误:n=1没有考虑

随机与取样

Leetcode 384

给定一个数组,要求实现两个指令函数。第一个函数“shuffle”可以随机打乱这个数组,第二个函数“reset”可以恢复原来的顺序。

class Solution {public:    Solution(vector<int>& nums) {        this->nums = nums;        this->original.resize(nums.size());        copy(nums.begin(), nums.end(), original.begin());    }      vector<int> reset() {        copy(original.begin(), original.end(), nums.begin());        return nums;    }      vector<int> shuffle() {        if (nums.empty()) return {};        vector<int> shuffled(nums);        int n = nums.size();        for (int i = n - 1; i >= 0; --i) {            swap(shuffled[i], shuffled[rand() % (i + 1)]);        }        // 正向洗牌:        // for (int i = 0; i < n; ++i) {        // int pos = rand() % (n - i);        // swap(shuffled[i], shuffled[i+pos]);        // }        return shuffled;    }private:    vector<int> nums;    vector<int> original;};

分析:经典的Fisher-Yates洗牌算法,原理是通过随机交换位置来实现随机打乱,有正向和反向两种写法

错误:类什么的还是不太会写

Leetcode 528

给定一个数组,数组每个位置的值表示该位置的权重,要求按照权重的概率去随机采样。

class Solution {    vector<int> W;public:    Solution(vector<int>& w) {        partial_sum(w.begin(), w.end(), back_inserter(W));    }      int pickIndex() {        int pos = rand() % W.back();        return upper_bound(W.begin(), W.end(), pos) - W.begin();    }};

分析:我们可以先使用 partial_sum求前缀和(即到每个位置为止之前所有数字的和),这个结果对于正整数数组是单调递增的。每当需要采样时,我们可以先随机产生一个数字,然后使用二分法查找其在前缀和中的位置,以模拟加权采样的过程。

错误:没思路

Leetcode 382

给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点被选中的概率一样 。

class Solution {    vector<int> arr;public:    Solution(ListNode* head) {        while (head) {            arr.emplace_back(head->val);            head = head->next;        }    }      int getRandom() {        return arr[rand() % arr.size()];    }};

分析:用一个数组记录链表中的所有结点值,然后随机输出即可。

错误:思路简单就是不会写

练习

Leetcode 168

给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。

class Solution {public:    string convertToTitle(int columnNumber) {        string ans;        while (columnNumber > 0) {            int a0 = (columnNumber - 1) % 26 + 1;            ans += a0 - 1 + 'A';            columnNumber = (columnNumber - a0) / 26;        }        reverse(ans.begin(), ans.end());        return ans;    }};

分析:进制转换的变形题

错误:减法操作没想好

Leetcode 67

给你两个二进制字符串,返回它们的和(用二进制表示)。

class Solution {public:    string addBinary(string a, string b) {        int a_size = a.size();        int b_size = b.size();        --a_size;        --b_size;        int cnt = 0;        int sign;        string result = "";        while(a_size >= 0 && b_size >= 0){            sign = a[a_size] - '0' + b[b_size] - '0' + cnt;            if(sign == 0){                result = "0" + result;                cnt = 0;            }            else if(sign == 1){                result = "1" + result;                cnt = 0;            }            else if(sign == 2){                result = "0" + result;                cnt = 1;            }            else if(sign == 3){                result = "1" + result;                cnt = 1;            }            --a_size;            --b_size;        }        while(a_size >= 0){            sign = a[a_size] - '0' + cnt;            if(sign == 0){                result = "0" + result;                cnt = 0;            }            else if(sign == 1){                result = "1" + result;                cnt = 0;            }            else if(sign == 2){                result = "0" + result;                cnt = 1;            }            else if(sign == 3){                result = "1" + result;                cnt = 1;            }            --a_size;        }        while(b_size >= 0){            sign = b[b_size] - '0' + cnt;            if(sign == 0){                result = "0" + result;                cnt = 0;            }            else if(sign == 1){                result = "1" + result;                cnt = 0;            }            else if(sign == 2){                result = "0" + result;                cnt = 1;            }            else if(sign == 3){                result = "1" + result;                cnt = 1;            }            --b_size;        }        if(cnt == 1){            result = "1" + result;        }        return result;    }};

分析:还是大数加法

错误:忘记了,应该没什么错误

Leetcode 238

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

class Solution {public:    vector<int> productExceptSelf(vector<int>& nums) {        int n = nums.size();        vector<int> left(n);        vector<int> right(n);        int start = 1;        left[0] = start;        for(int i=1;i<n;++i){            left[i] = start * nums[i-1];            start = left[i];        }        int end = 1;        right[n-1] = end;        for(int i=n-2;i>=0;--i){            right[i] = end * nums[i+1];            end = right[i];        }        vector<int> result(n);        for(int i=0;i<n;++i){            result[i] = left[i] * right[i];        }        return result;    }};

分析:前缀积+后缀积

错误:看了一下思路,后面自己想通了实现了

Leetcode 462

给你一个长度为 n 的整数数组 nums ,返回使所有数组元素相等需要的最少移动数。在一步操作中,你可以使数组中的一个元素加 1 或者减 1

class Solution {public:    int minMoves2(vector<int>& nums) {        int n = nums.size();        sort(nums.begin(),nums.end());        int num = nums[n/2];        int sum2 = 0;        for(int i=0;i<n;++i){            if(nums[i] > num){                sum2 += nums[i] - num;            }            else{                sum2 += num - nums[i];            }        }        return sum2;    }};

分析:如果仅仅考虑最大的数字和最小的数字,那么这个数字一定在这两个数字中间,去除掉后这个数字也一定在次大的和次小的数字之间。因此是中位数

错误:思路不对,开始想成平均数了

Leetcode 169

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

class Solution {public:    int majorityElement(vector<int>& nums) {        int candidate = -1;        int count = 0;        for (int num : nums) {            if (num == candidate)                ++count;            else if (--count < 0) {                candidate = num;                count = 1;            }        }        return candidate;    }};

分析:Boyer-Moore 算法:维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0;我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x:如果 x 与 candidate 相等,那么计数器 count 的值增加 1;如果 x 与 candidate 不等,那么计数器 count 的值减少 1。在遍历完成后,candidate 即为整个数组的众数。

错误:算法想的不太好,没有想到最优的解法。

Leetcode 470

给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数,试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。

class Solution {public:    int rand10() {        int row, col, idx;        do {            row = rand7();            col = rand7();            idx = col + (row - 1) * 7;        } while (idx > 40);        return 1 + (idx - 1) % 10;    }};

分析:调用两次rand7(),找到一些等概率的数字,然后拒绝掉另外的数字。

错误:想当然认为是直接乘法了。

Leetcode 202

编写一个算法来判断一个数 n 是不是快乐数。

class Solution {public:    bool isHappy(int n) {        int sum = 6;        while(sum--){            string s = to_string(n);            int t = 0;            for(int i=0;i<s.size();++i){                t += (s[i] - '0') * (s[i] - '0');            }            if(t == 1){                return true;            }            n = t;        }        return false;    }};

分析:看看会不会跳出循环

一遍AC,但是解法不够好,后面要用更好的方法进行尝试。

总结

数学问题需要有数学基础,一般面试中应该用的比较少,有些问题还是挺有意思的。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第8章 分治法 + + /2022/09/05/Leetcode/Leetcode-101/Leetcode-101-8/ + + Leetcode 刷题笔记-Leetcode 101 第8章 分治法

分治法

顾名思义,分治问题由“分”(divide)和“治”(conquer)两部分组成,通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。我们在排序章节展示的归并排序就是典型的分治问题,其中“分”即为把大数组平均分成两个小数组,通过递归实现,最终我们会得到多个长度为1的子数组;“治”即为把已经排好序的两个小数组合成为一个排好序的大数组,从长度为1 的子数组开始,最终合成一个大数组。

表达式问题

Leetcode 241

给定一个只包含加、减和乘法的数学表达式,求通过加括号可以得到多少种不同的结果

class Solution {public:    vector<int> diffWaysToCompute(string expression) {        vector<int> ways;        for(int i=0;i<expression.size();++i){            char c = expression[i];            if(c == '+' || c == '-' || c == '*'){                vector<int> left = diffWaysToCompute(expression.substr(0,i));                vector<int> right = diffWaysToCompute(expression.substr(i+1));                for(const int &l : left){                    for(const int &r : right){                        if(c == '+'){                            ways.push_back(l+r);                        }                        else if(c == '-'){                            ways.push_back(l-r);                        }                        else{                            ways.push_back(l*r);                        }                    }                }            }        }        if (ways.empty()){            ways.push_back(stoi(expression));        }        return ways;    }};

分析:利用分治思想,我们可以把加括号转化为,对于每个运算符号,先执行处理两侧的数学表达式,再处理此运算符号。注意边界情况,即字符串内无运算符号,只有数字。

错误:想不通的

练习

Leetcode 932

class Solution {public:    vector<int> beautifulArray(int n) {        vector<int> ans;        if(n==1){            ans.push_back(1);            return ans;        }        int odd_num=(n+1)/2;        int even_num=n/2;        vector<int> left_arry=beautifulArray(odd_num);        vector<int> right_arry=beautifulArray(even_num);        //将左侧数组映射为奇数        for(auto &val:left_arry){            ans.push_back(val*2-1);        }        //将右侧数组映射为偶数        for(auto &val:right_arry){            ans.push_back(val*2);        }        return ans;    }};

分析:不懂

错误:不懂

Leetcode 312

class Solution {public:    int maxCoins(vector<int>& nums) {        int n = nums.size();        vector<vector<int>> rec(n + 2, vector<int>(n + 2));        vector<int> val(n + 2);        val[0] = val[n + 1] = 1;        for (int i = 1; i <= n; i++) {            val[i] = nums[i - 1];        }        for (int i = n - 1; i >= 0; i--) {            for (int j = i + 2; j <= n + 1; j++) {                for (int k = i + 1; k < j; k++) {                    int sum = val[i] * val[k] * val[j];                    sum += rec[i][k] + rec[k][j];                    rec[i][j] = max(rec[i][j], sum);                }            }        }        return rec[0][n + 1];    }};

分析:不懂

错误:不懂

总结

不懂不懂不懂啊啊啊啊啊

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:现代信息检索-第3讲 索引压缩 + + /2022/09/05/UCAS/information-retrieval/information-retrieval-3/ + + 《现代信息检索》课程笔记:第3讲 索引压缩

第3讲 索引压缩

压缩

举例:将长编码串用短编码串来代替:111111111111111111➡18个1

为什么要压缩?

  • 减少磁盘空间占用(节省开销)
  • 增加内存存储内容(加快速度)
  • 加快从磁盘到内存的数据传输速度(同样加快速度)
    • 读压缩数据到内存+在内存中解压,比直接读入未压缩数据到内存要快很多

为什么在IR中需要压缩?

  • 占用更少的硬盘空间
    • 更经济,节省空间
  • 将更多数据载入内存
    • 加快处理速度(内存中读写很快)
  • 减少从磁盘读入内存的时间
    • 大型搜索引擎将相当比例的倒排记录表都放入内存(硬盘?)

IR中压缩的两个基本要求:无损压缩和随机访问

压缩的一个基本问题:对齐,即建立不同压缩单元之间的分界标识

有损压缩:丢弃一些信息-很多常用的预处理步骤可以看成是有损压缩

无损压缩:所有信息都保留-索引压缩中通常都使用无损压缩

词项统计量

词典压缩中词典的大小即词汇表的大小是关键

词汇表大小会随着文档集的大小增长而增长,没有办法估计数量。

存在一个经验规律可以进行估计:

Heaps定律:,其中是词汇表大小, 是文档集的大小。参数的一个经典取值是:

Heaps定律在对数空间下是线性的。

在容许拼写错误或者对拼写错误自动纠错的情况下,Heaps定律的效果如何?

  • 存在拼写错误:会增加词项数目
  • 自动纠错:总体词项数目趋于正常
  • 对效果有一定影响,但是除非存在大量拼写错误,否则不会有显著影响。

倒排记录表压缩中词项的分布情况是关键

我们还需要知道在文档集中有多少高频词项和低频词项

Zipf定律:第常见的词项的频率成正比

是语料中词项频率:词项在所有文档中出现的次数

实际统计中可以发现拟合度并不是很高,但是可以发现高频词项很少,低频罕见词项很多。

词典压缩

一般而言,相对于倒排记录表,词典所占空间较小。但是我们想将词典放入内存,另外满足一些特定领域特定应用的需要,如手机、机
载计算机上的应用或要求快速启动等需求。因此,压缩词典也很重要。

定长数组方式下的词典存储:每个词项需要20(字符串)+4(词频)+4(指向倒排索引表的指针)=28个字节。

不足之处:

  • 大量存储空间被浪费
    • 即使是长度为1的词项,我们也分配20个字节,但是英语中每个词项的平均长度为8个字符
  • 不能处理长度大于20字节的词项

将整部词典看成单一字符串:4(词频)+4(指向倒排索引表的指针)+3(指向字符串的指针,按照实际大小决定,例如8*400000个位置需要$log_2(8 * 400000)< 24 $位来表示)+8(每个字符串平均需要8个字节)=19个字节

按块存储,假设块大小k=4,此时每4个词项只需要保留1个词项指针,但是同时需要增加4个字节(比较短,1个字节就可以)来表示每个词项的长度,因此每4个词项需要3+4=7B,比之前的节省了12-7=5B

但是不采用块存储方式下的词项查找是典型的二叉查找,而采用块存储方式下的词项查找需要进行顺序查找,块如果太大会影响效率。

每个块当中,会有公共前缀,可以采用前端编码方式继续压缩。

哪些前缀应该用于前端编码?需要在哪些方面有所权衡?

  • 同一个单词的不同形式适合使用这种前端编码
  • 没有什么公共前缀的话,压缩效果不太好,而且还会导致检索速度下降

倒排记录表压缩

倒排记录表空间远大于词典,压缩关键是对每条倒排记录进行压缩

关键思想:存储 docID间隔而不是 docID本身

设计一个变长编码(variable length encoding):可变长编码对于小间隔采用短编码而对于长间隔采用长编码

可变字节(VB)码:设定一个专用位 (高位) c作为延续位(continuation bit),如果间隔表示少于7比特,那么c置1,将间隔编入一个
字节的后7位中;否则将高7位放入当前字节中,并将c置0,剩下的位数采用同样的方法进行处理,最后一个字节的c置1(表
示结束)

  • 除字节外,还可以采用不同的对齐单位:比如32位(word)、16位及4位(nibble)等等
  • 如果有很多很小的间隔,那么采用可变字节码会浪费很多空间,而此时采用4位为单位将会节省空间

一元码:将n表示成n个1和最后一个0

基于位的编码:

编码:(不考虑0)

  • 将G (Gap, 间隔) 表示成长度(length)和偏移(offset)两部分
  • 偏移对应G的二进制编码,只不过将首部的1去掉(因为所有的编码第一位都是1)
  • 长度部分给出的是偏移的位数,采用一元编码
  • 手动计算的时候先计算偏移,再根据偏移计算长度

偏移部分是比特位,长度部分需要比特位,因此全部编码需要比特位。

  • 编码是前缀无关的,也就是说一个合法的编码不会是任何一个其他的合法编码的前缀,也保证了解码的唯一性。
  • 编码在最优编码的2或3倍之内
  • 编码适用于任何分布,是通用性编码
  • 编码是无参数编码,不需要通过拟合得到参数

组变长整数编码:

  • 按块存储,每块大小为5-17个字节,存放4个整数编码
  • 每块首字节:4个2位的二进制长度,
  • 使用 字节(在4–16之间)存放4个整数

Simple9编码:每块4字节,前4位标识块内结构,剩余28位存储若干个数字,每个数字占用相同的位数。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:现代信息检索-第2讲 索引构建 + + /2022/09/04/UCAS/information-retrieval/information-retrieval-2/ + + 《现代信息检索》课程笔记:第2讲 索引构建

语料通常很大,而服务器内存通常相对较小,因此需要在内存有限的情况下的索引构建策略。

第2讲 索引构建

词项:一个语料中不同的词的数量

词条:一个语料中所有词的数量(包括重复的)

基于排序的索引构建方法存在的问题

在构建索引时,每次解析一篇文档,因此对于每个词项而言,其倒排记录表不到最后一篇文档都是不完整的。

如果每个 (termID, docID)对占用 8个字节, 那么处理大规模语料需要大量的空间。

一般内存的容量比较小,没有办法将前面产生的倒排记录表全部放在内存中,需要在磁盘上存储中间结果。

内存和硬盘

内存的典型配置是几G ~ 几十G的内存或上百G或1-2T

磁盘空间通常有几T(小型服务器)或10T以上(磁盘阵列)

硬盘空间更大,但是在内存中访问数据会比从硬盘访问数据快很多(大概10倍以上的差距)

硬盘寻道时间是闲置时间:磁头在定位时不发生数据传输(假设使用的是机械硬盘)

因此一个大(连续)块的传输会比多个小块(非连续)的传输速度快

硬盘 I/O是基于块的:读写时是整块进行的。块大小:8KB到256KB不等

不能在硬盘上对倒排索引表进行排序,因为寻道的时间很慢,导致排序的时间也很慢。

BSBI算法

一种减少寻道操作的排序:Blocked sort-based Indexing

将所有记录划分为每个大小约为10M的块,收集每一块的倒排记录,排序,将倒排索引写入硬盘,最后将不同的分块合并为一个大的倒排索引。

关键决策:块的大小-块越大,最后的合并操作就越少

合并的过程中需要在磁盘中同时保存数据的两份拷贝(合并前与正在合并),因此磁盘空间要足够大。

vTB5VS.png

词项字符串的占用空间比较大,因此维护一个全局词典来将字符串映射到唯一的全局ID

合并的过程中,将每一个小块的一点点数据放入内存中进行排序,排序好了就放在写缓冲区中,写缓冲区满了就写回硬盘,直到排序完成。

可以将两两合并的方式优化为多项合并(multi-way merge):

  • 从所有块同时读取,并且为每块保持一个读缓冲区(read buffer)
  • 为输出文件(即合并后的索引)保持一个写缓冲区(write buffer)
  • 维护一个待处理 termid的优先级队列(priority queue),每次迭代从队列中选取一个最小的未处理 termid
  • 合并不同块中所有的该 termid的倒排记录,并写入磁盘。
  • 因此每次迭代均处理较小规模的数据(一个词项的倒排记录)。

BSBI算法的问题:

  • 假定词典可以在内存中放下
  • 通常需要一部词典(动态增长)来将 term映射成 termID。实际上倒排记录表可以直接采用 (term,docID)方式而不是
    (termID,docID)方式,但是此时中间文件(即待合并的倒排记录表)将会变得很大(字符串比整型数空间消耗更大)

SPIMI算法

内存式单遍扫描索引构建算法:Single-pass in-memory indexing

关键思想:

  • 对每个块都产生一个独立的词典(不需要在块之间进行 term-termID的映射)
  • 对倒排记录表不排序,按照它们出现的先后顺序排列,只对词典排序(实际上由于指针的存在,倒排记录表没有排序的必要)。

在扫描文档的同时,直接在内存中维护一个不断更新的倒排索引

因此对每个块生成一个完整的倒排索引,这些独立的索引最后合并成一个大索引

最终合并词典的过程中,需要进行词项字符串的比较,因为此时没有全局词典提供词项-整数ID的映射。

vTDP2R.png

BSBI算法和SPIMI算法的主要区别

BSBI算法:在分块索引阶段,BSBI算法维护一个全局Term (String) – Termid (int) 的映射表,局部索引为Termid及其倒排记录表,仍然按词典顺序排序。

SPIMI算法:分块索引阶段与BSBI算法不同在于建立局部词典和索引,无需全局词典。在合并阶段,将局部索引两两合并,最后产生全局词典建立Term – Termid的映射。

动态索引构建

实际中文档会增加、删除和修改,因此词典和倒排记录表必须要动态更新。

最简单的方法:主索引(Main index)+辅助索引(Auxiliary index)

  • 在磁盘上维护一个大的主索引(Main index)
  • 新文档放入内存中较小的辅助索引中
  • 同时搜索两个索引,然后合并结果
  • 定期将辅助索引合并到主索引中

删除的处理:

  • 采用无效位向量(Invalidation bit-vector)来表示删除的文档
  • 利用该维向量过滤返回的结果,以去掉已删除文档

问题:

  • 合并过于频繁
  • 合并时如果正好在搜索,那么搜索的性能将很低

辅助索引方式: 每次合并都需要处理每个倒排记录,索引构建时间为,其中是所有倒排记录的个数

对数合并(Logarithmic merge):

对数合并算法能够缓解(随时间增长)索引合并的开销 → 用户并不感觉到响应时间上有明显延迟。

  • 维护一系列索引,其中每个索引是前一个索引的两倍大小
  • 将最小的索引置于内存
  • 将其他更大的索引 置于磁盘
  • 如果 ,则将它作为 $I_0 $写到磁盘中(如果 $I_0 $不存在)
  • 或者和合并(如果已经存在),并将合并结果作为写到磁盘中(如果不存在),或者和合并(如果已经存在),依此类推

因此每次两两合并中两个索引的大小相同

索引数目的上界为 ,因此查询处理时需要合并个索引,因此每个倒排记录需要合并次,则索引构建时间为,时间复杂度相比较辅助索引方式小了一个数量级。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:机器学习-第2章 贝叶斯学习 + + /2022/09/02/UCAS/machine-learning/machine-learning-2/ + + 《机器学习》课程笔记:第2章 贝叶斯学习

第2章 贝叶斯学习

概述

  1. 依赖先验的决策:

某地全年365天,晴朗265天,非晴朗100天。判断明天天气如何?

,则:

,因此,明天晴天的概率更大。

  1. 若增加可观测信息:晴朗(非晴朗)天气前一天特征(是否有晚霞)的统计。

今天有晚霞,判断明天天气如何? 即计算

今天没有晚霞,判断明天天气如何? 即计算

利用贝叶斯决策原理:

的联合概率:

因此可以求得,则在前一天有晚霞的条件下晴天的概率要大于不是晴天的概率。

贝叶斯决策论

贝叶斯公式:

因此

贝叶斯决策:

基于观察特征、类别的贝叶斯公式:

也就是:

因此,即

如果存在两个变量进行决策,即计算,则可以转换为计算

更改为比值的形式:

可以定义类别相似性函数

分母都是相同的,因此可以将转化为

概率有很多都是的形式,因此可以将转化为,将乘积的形式转换为和的形式。

对于两变量决策问题来说,可以计算决策边界,绘制后可以直观看出边界的形状,可能是直线也可能是曲线,这样实现了贝叶斯决策方法。

贝叶斯分类器

  • 朴素贝叶斯分类器:假设特征向量的各维属性独立;
  • 半朴素贝叶斯分类器:假设的各维属性存在依赖;
  • 正态分布的贝叶斯分类器:假设服从正态分布;

朴素贝叶斯分类器

采用了“属性条件独立性假设”

关键问题:由训练样本学习类别条件概率和类别先验概率

包括个属性和个类别,加上,共有个概率分布需要统计。

类别先验概率

类别概率密度

对于来说,若是离散的变量,则 ,其中表示中在第个属性上取值为的样本组成的集合。

是连续的变量,则 (由某一概率分布估计类别概率)

拉普拉斯平滑:避免因训练集样本不充分而导致概率估计值为零。

平滑后:为类别数;的可能取值个数。

正态分布的贝叶斯分类器

是连续的变量,则 (设置其为正态分布的概率密度)

多维正态分布的概率密度:

在每个维度上都是正态分布:

贝叶斯学习将公式化简为对数的形式:

不同的高斯参数情况:

:均为正态分布(当各个类别先验相等时,退化为最小距离分类器,退化为垂直平分面)

vL14KO.md.png

:各类分布都相同

vL1ORP.png

贝叶斯学习与参数估计问题

推导

]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + 研究生课程:机器学习-第1章 绪论 + + /2022/09/02/UCAS/machine-learning/machine-learning-1/ + + 《机器学习》课程笔记:第1章 绪论

  • 了解机器学习研究问题
    • 有监督学习:分类、回归
    • 无监督学习:聚类、降维、特征提取等;
  • 掌握基本的统计和优化方法
    • 统计学习基础:最大似然估计、最小均方等;
    • 优化基础:梯度下降 、随机梯度下降等;
  • 掌握机器学习的基础理论和算法
    • Bayes、 SVM、鉴别分析、 logistic、决策树、感知机、多层感知机、 Adaboost、线性回归、kmeans、 PCA、 概率图模型、知识图谱、深度学习及前沿等;
  • 能够针对任务设计机器学习方案

第1章 绪论

机器学习研究背景:人工智能

什么是人工智能?

“人工智能就是让机器来完成那些如果由来做则需要智能的事情的科学”;

“人工智能就是研究如何使计算机去做只有才能做的智能工作

“人工智能是研究使计算机来模拟人的某些思维过程和智能行为 (如学习、推理、思考、规划等)的学科 ”

图灵测试思考的问题:

  • 人的智能非常复杂: 例如 直觉 、顿悟、理解,等等
  • 人的智能具有“人”性:例如 情绪、伪装、狡猾,等等;
  • 人的智能缺陷:不依赖于数学工具,无法实现高难度、大规模的运算;不依赖于词典和存储工具,信息的记忆量、精准性有限;

我们研究的是弱人工智能

人工智能的发展

  • 孕育期(~1956):1950 年图灵测试
  • 推理期(1956~1965):1956 年逻辑理论家程序、 1960 年 Lisp 语言
  • 知识期(1965~1983):1965 年分子结构的专家系统 DENDRAL、1972年细菌感染专家系统MYCIN
  • 学习期(1983~2006):解决知识工程瓶颈, 统计机器学习主导
  • 黄金期(2006~):以深度学习为 代表的人工智能核心技术不断取得新突破

对人工智能的期望

  • 在人工智能的第一波中,你必须成为一名程序员;
  • 在人工智能的第二次浪潮中,你必须是一名数据科学家;
  • 人工智能的第三次浪潮,你越道德越好。。。

人工智能创新发展引领新一轮产业变革之势,推动人类社会进入智能化时代,人工智能成为世界各国竞相战略布局的新高地,我国人工智能综合实力不断提升。

机器学习的发展

机器学习是一门人工智能的科学

“机器学习是一门人工智能的科学,该领域的主要研究对象是人工智能,特别是如何在经验学习中改善具体算法的性能 。 Langley(1996)“

“机器学习是对能通过经验自动改进的计算机算法的研究 。 Tom Mitchell (1997)“

“机器学习是用数据或以往的经验,以此优化计算机程序的性能标准”。 Alpaydin (2004)

机器学习发展时期

推理期➡知识期➡学科形成➡蓬勃发展期

应用领域

  • 航空航天、军事、国防
  • 机器人、无人车、 NASA-JPL 火星机器人
  • 互联网应用
  • 信息安全
  • 生物信息学
  • 天气预报、地震预警、环境污染检测
  • 智能识别
  • 金融、经贸、管理 、 公共安全 、 医学 、 交通 、

机器学习研究意义

  • 机器学习是人工智能的基石
  • 机器学习引领人工智能的前沿
  • 支持宽泛的学科领域

机器学习研究的问题

机器学习的一般过程

vIfz4K.png

  1. 监督学习:学习输入 x到输出 y的映射,训练数据会有标签 y,分为回归问题和分类问题。
  2. 无监督学习:学习数据之间的关联,训练数据是没有标签的,典型问题是聚类。
  3. 强化学习:学习输入 x到输出 y的映射,不会提供标签,但是会给一个反馈表示目前的选择有多好。

机器学习流程:

  1. 收集数据
  2. 选择模型(选择合适的模型,确定优化函数)
  3. 训练模型:找到可以优化损失函数的合适的参数集
  4. 应用训练好的模型
]]>
+ + + + + Study + + + + + + + Postgraduate + + Machine Learning + + UCAS + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第7章 动态规划 + + /2022/09/02/Leetcode/Leetcode-101/Leetcode-101-7/ + + Leetcode 刷题笔记-Leetcode 101 第7章 动态规划

动态规划

动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。同时也可以对动态规划进行空间压缩,起到节省空间消耗的效果。

基本动态规划:一维

Leetcode 70

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

class Solution {public:    int climbStairs(int n) {        vector<int> num(n+1);        if(n <= 2){            return n;        }        else{            num[1] = 1;            num[2] = 2;            for(int i=3;i<=n;++i){                num[i] = num[i-1] + num[i-2];            }        }        return num[n];    }};

分析:num[i]表示在第 i阶的方法数,则到达第 i阶的方法是到达第 i-1阶的方法和到达第 i-2阶的方法数之和。因此 num[i] = num[i-1] + num[i-2]。判断边界条件即可。

一遍AC

Leetcode 198

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

class Solution {public:    int rob(vector<int>& nums) {        int n = nums.size();        vector<int> dp(n+1);        if(n == 1){            return nums[0];        }        else if(n == 2){            return max(nums[0],nums[1]);        }        dp[1] = nums[0];        dp[2] = max(nums[0],nums[1]);        for(int i=3;i<=n;++i){            dp[i] = max(dp[i-1],dp[i-2] + nums[i-1]);        }        return dp[n];    }};

分析:定义一个数组 dpdp[i]表示抢劫到第i个房子时,可以抢劫的最大数量。我们考虑 dp[i],此时可以抢劫的最大数量有两种可能,一种是我们选择不抢劫这个房子,此时累计的金额即为 dp[i-1];另一种是我们选择抢劫这个房子,那么此前累计的最大金额只能是 dp[i-2]。因此本题的状态转移方程为 dp[i] = max(dp[i-1],nums[i-1] + dp[i-2])。然后判断边界条件即可。

一遍AC

Leetcode 413

给定一个数组,求这个数组中连续且等差的子数组一共有多少个

class Solution {public:    int numberOfArithmeticSlices(vector<int>& nums) {        int n = nums.size();        if(n == 1 || n == 2){            return 0;        }        vector<int>dp(n+1);        dp[0] = 0;        dp[1] = 0;        dp[2] = 0;        for(int i=2;i < n;++i){            if(nums[i] - nums[i-1] == nums[i-1] - nums[i-2]){                dp[i+1] = dp[i] + 1;            }        }        return accumulate(dp.begin(),dp.end(),0);    }};

分析:这道题略微特殊,因为要求是等差数列,可以很自然的想到子数组必定满足 num[i] - num[i-1] = num[i-1] - num[i-2]。然而由于我们对于 dp数组的定义通常为以 i结尾的,满足某些条件的子数组数量,而等差子数组可以在任意一个位置终结,因此此题在最后需要对 dp数组求和。

错误:最开始写的时候越界了

基本动态规划:二维

Leetcode 64

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

class Solution {public:    int minPathSum(vector<vector<int>>& grid) {        int m = grid.size();        int n = grid[0].size();        vector<vector<int>> dp(m,vector<int>(n,0));        dp[0][0] = grid[0][0];        for(int i=1;i<m;++i){            dp[i][0] = dp[i-1][0] + grid[i][0];        }        for(int j=1;j<n;++j){            dp[0][j] = dp[0][j-1] + grid[0][j];        }        for(int i=1;i<m;++i){            for(int j=1;j<n;++j){                dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j];            }        }        return dp[m-1][n-1];    }};

分析:定义一个同样是二维的 dp数组,其中 dp[i][j]表示从左上角开始到 (i, j)位置的最优路径的数字和。因为每次只能向下或者向右移动,我们可以很容易得到状态转移方程 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j],其中 grid表示原数组。

错误:注意区间,开多大的 dp数组以及怎么进行状态转移,不要把自己转蒙。

Leetcode 542

给定一个由 01 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。两个相邻元素间的距离为 1

class Solution {public:    vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {        int m = mat.size();        int n = mat[0].size();        vector<vector<int>> dp(m,vector<int>(n,0));        if(mat[0][0] == 0){            dp[0][0] = 0;        }        else{            dp[0][0] = 10002;        }        for(int i=1;i<m;++i){            if(mat[i][0] == 0){                dp[i][0] = 0;            }            else{                dp[i][0] = dp[i-1][0] + 1;            }        }        for(int j=1;j<n;++j){            if(mat[0][j] == 0){                dp[0][j] = 0;            }            else{                dp[0][j] = dp[0][j-1] + 1;            }        }        for(int i=1;i<m;++i){            for(int j=1;j<n;++j){                if(mat[i][j] == 0){                    dp[i][j] = 0;                }                else{                    dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + 1;                }            }        }        for(int i=m-2;i>=0;--i){            if(mat[i][n-1] == 0){                dp[i][n-1] = 0;            }            else{                dp[i][n-1] = min(dp[i][n-1],dp[i+1][n-1] + 1);            }        }        for(int j=n-2;j>=0;--j){            if(mat[m-1][j] == 0){                dp[m-1][j] = 0;            }            else{                dp[m-1][j] = min(dp[m-1][j],dp[m-1][j+1] + 1);            }        }        for(int i=m-2;i>=0;--i){            for(int j=n-2;j>=0;--j){                if(mat[i][j] != 0){                    dp[i][j] = min(dp[i][j],min(dp[i+1][j],dp[i][j+1])+1);                }            }        }        return dp;    }};

分析:从左上到右下进行一次动态搜索,再从右下到左上进行一次动态搜索。两次动态搜索即可完成四个方向上的查找。

错误:看了一下题解的思路,还是有点不敢想。另外要细心,注意越界!!!

Leetcode 221

在一个由 '0''1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

class Solution {public:    int maximalSquare(vector<vector<char>>& matrix) {        int m = matrix.size();        int n = matrix[0].size();        vector<vector<int>> dp(m+1,vector<int>(n+1,0));        int maxside = 0;        for(int i=1;i<=m;++i){            for(int j=1;j<=n;++j){                if(matrix[i-1][j-1] == '1'){                    dp[i][j] = min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j])) + 1;                }                maxside = max(maxside,dp[i][j]);            }        }        return maxside * maxside;    }};

分析:dp[i][j]表示以 (i, j)为右下角的全由 1构成的最大正方形边长。

错误:状态转移方程没有想太好。

分割类型题

对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。

Leetcode 279

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

class Solution {public:    int numSquares(int n) {        vector<int> dp(n+1,100000000);        dp[0] = 0;        for(int i=1;i<=n;++i){            for(int j=1;j*j<=i;++j){                dp[i] = min(dp[i],dp[i-j*j]+1);            }        }        return dp[n];    }};

分析:dp[i]表示数字 i最少可以由几个完全平方数相加构成。

错误:没有思路

Leetcode 91

输入是一个由数字组成的字符串,输出是满足条件的解码方式总数。

class Solution {public:    int numDecodings(string s) {        int n = s.size();        if(s[0] == '0'){            return 0;        }        if(n == 1){            return 1;        }        vector<int>dp(n+1,1);        int prev = s[0] - '0';        for(int i=2;i<=n;++i){            int cur = s[i-1] - '0';            if((prev == 0 || prev > 2) && cur == 0){                return 0;            }            if((prev == 1) || (prev == 2 && cur < 7)){                if(cur){                    dp[i] = dp[i-1] + dp[i-2];                }                else{                    dp[i] = dp[i-2];                }            }            else{                dp[i] = dp[i-1];            }            prev = cur;        }        return dp[n];    }};

分析:dp[i]表示以当前第i个位置上的数字为结尾的表示方法总数。dp[i]取决于两个数字,当前的数字和前一个数字。如果当前数字是 0,而前一个数字不是 1或者 2,说明这两个数字不可能构成字符,因此直接返回 0。如果前一个数字是 1,当前的数字是什么都行,或者前一个数字是 2,而当前的数字是 0-6的某一个数,说明这两个能构成一种组合。同时如果当前的数字不是 0,那么这个数字自己也能构成一种。如果前一个数字是其他,说明不能和当前的数字产生关系了,就只能是当前的数字自己了。

错误:不明白

Leetcode 139

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s

class Solution {public:    bool wordBreak(string s, vector<string>& wordDict) {        int n = s.size();        vector<bool> dp(n+1,false);        dp[0] = true;        for(int i=1;i<=n;++i){            for(const string & word : wordDict){                int len = word.size();                if(i >= len && s.substr(i-len,len) == word){                    dp[i] = dp[i] || dp[i-len];                }            }        }        return dp[n];    }};

分析:类似于完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置0,需要初始化值为真。

子序列问题

对于子序列问题,第一种动态规划方法是,定义一个 dp数组,其中 dp[i]表示以 i结尾的子序列的性质。在处理好每个位置后,统计一遍各个位置的结果即可得到题目要求的结果。第二种动态规划方法是,定义一个 dp数组,其中 dp[i]表示到位置 i为止的子序列的性质,并不必须以 i结尾。这样 dp数组的最后一位结果即为题目所求,不需要再对每个位置进行统计。

Leetcode 300

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

class Solution {public:    int lengthOfLIS(vector<int>& nums) {        int n = nums.size();        vector<int> dp(n+1,1);        dp[0] = 0;        for(int i=1;i<=n;++i){            for(int j=0;j<i;j++){                if(nums[i-1] > nums[j]){                    dp[i] = max(dp[i],dp[j+1]+1);                }            }        }        return *max_element(dp.begin(),dp.end());    }};

分析: dp[i]表示以 i结尾的子序列的性质。简单动态规划即可。

错误:下标指代不清,初始化应该全部为1

Leetcode 1143

给定两个字符串 text1text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0

class Solution {public:    int longestCommonSubsequence(string text1, string text2) {        int m = text1.size();        int n = text2.size();        vector<vector<int>> dp(m+1,vector<int>(n+1,0));        for(int i=1;i<=m;++i){            for(int j=1;j<=n;++j){                if(text1[i-1] == text2[j-1]){                    dp[i][j] = dp[i-1][j-1] + 1;                }                else{                    dp[i][j] = max(dp[i][j-1],dp[i-1][j]);                }            }        }        return dp[m][n];    }};

分析:建立一个二维数组 dp,其中 dp[i][j]表示到第一个字符串位置 i为止、到第二个字符串位置 j为止、最长的公共子序列长度。

错误:没想到是二维的动态规划。

背包问题

给定一个正整数数组,求是否可以把这个数组分成和相等的两部分。

class Solution {public:    bool canPartition(vector<int>& nums) {        int n = nums.size();        int sum = accumulate(nums.begin(),nums.end(),0);        if(sum % 2 == 1){            return false;        }        sum /= 2;        vector<vector<int>> dp(n+1,vector<int>(sum+1,false));        dp[0][0] = true;        for(int i=1;i<=n;++i){            for(int j=0;j<=sum;++j){                if(j < nums[i-1]){                    dp[i][j] = dp[i-1][j];                }                else{                    dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];                }            }        }        return dp[n][sum];    }};

分析:背包问题,价值是一半,背包容量没有限制。比较重要的是 dp[0][0] =true,后续的判断都是从这个 true继承过来的。

错误:思路不够完善

Leetcode 474

给你一个二进制字符串数组 strs 和两个整数 mn ,请你找出并返回 strs 的最大子集的长度,该子集中最多m0n1

class Solution {public:    static vector<int> getzerosandones(string &str){        vector<int> result(2);        int n = str.size();        for(int i=0;i<n;++i){            if(str[i] == '0'){                ++result[0];            }            else{                ++result[1];            }        }        return result;    }    int findMaxForm(vector<string>& strs, int m, int n) {        int l = strs.size();        vector<vector<vector<int>>> dp(l+1,vector<vector<int>>(m+1,vector<int>(n+1,0)));        for(int i=1;i<=l;++i){            vector<int> && zerosones = getzerosandones(strs[i-1]);            int zero = zerosones[0];            int one = zerosones[1];            for(int j=0;j<=m;++j){                for(int k=0;k<=n;++k){                    dp[i][j][k] = dp[i-1][j][k];                    if(j >= zero && k >= one){                        dp[i][j][k] = max(dp[i][j][k],dp[i-1][j-zero][k-one]+1);                    }                }            }        }        return dp[l][m][n];    }};

分析:三维的背包问题,要同时考虑两个背包的容量。

错误:还是不理解

Leetcode 322

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1 ,可以认为每种硬币的数量是无限的。

class Solution {public:    int coinChange(vector<int>& coins, int amount) {        int n = coins.size();        vector<vector<int>> dp(n+1,vector<int>(amount+1,amount+1));        dp[0][0] = 0;        for(int i=0;i<=amount;++i){            for(int j=1;j<=n;++j){                if(coins[j-1] <= i){                    dp[j][i] = min(dp[j-1][i],dp[j][i-coins[j-1]]+1);                }                else{                    dp[j][i] = dp[j-1][i];                }            }        }        return dp[n][amount] == amount+1 ? -1 : dp[n][amount];    }};

分析:完全背包问题。

错误:就是不理解

字符串编辑

Leetcode 72

给定两个字符串,已知你可以删除、替换和插入任意字符串的任意字符,求最少编辑几步可以将两个字符串变成相同。

class Solution {public:    int minDistance(string word1, string word2) {        int m = word1.size();        int n = word2.size();        vector<vector<int>> dp(m+1,vector<int>(n+1,0));        for(int i=0;i<=m;++i){            dp[i][0] = i;        }        for(int j=0;j<=n;++j){            dp[0][j] = j;        }        for(int i=1;i<=m;++i){            for(int j=1;j<=n;++j){                if(word1[i-1] == word2[j-1]){                    dp[i][j] = dp[i-1][j-1];                }                else{                    dp[i][j] = min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j])) + 1;                }            }        }        return dp[m][n];    }};

分析:使用一个二维数组 dp[i][j],表示将第一个字符串到位置 i为止,和第二个字符串到位置 j为止,最多需要几步编辑。当第 i位和第 j位对应的字符相同时,dp[i][j]等于 dp[i-1][j-1];当二者对应的字符不同时,修改的消耗是 dp[i-1][j-1]+1,插入 i位置/删除 j位置的消耗是 dp[i][j-1] + 1,插入 j位置/删除 i位置的消耗是 dp[i-1][j] + 1

错误:初始化没有做好。

Leetcode 650

给定一个字母A,已知你可以每次选择复制全部字符,或者粘贴之前复制的字符,求最少需要几次操作可以把字符串延展到指定长度。

class Solution {public:    int minSteps(int n) {        vector<int> dp(n+1,0);        for(int i=2;i<=n;++i){            dp[i] = i;            for(int j=2;j * j <= i;++j){                if(i % j == 0){                    dp[i] = dp[j] + dp[i/j];                    break;                }            }        }        return dp[n];    }};

分析:我们使用一个一维数组dp,其中位置i表示延展到长度i的最少操作次数。对于每个位置j,如果j可以被i整除,那么长度i就可以由长度j操作得到,其操作次数等价于把一个长度为1的A延展到长度为i/j。因此我们可以得到递推公式dp[i] = dp[j] + dp[i/j]

错误:还是不会想。

Leetcode 10

给定一个字符串和一个正则表达式,求该字符串是否可以被匹配。

class Solution {public:    bool isMatch(string s, string p) {        int m = s.size();        int n = p.size();        vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));        dp[0][0] = true;        for(int i=1;i<=n;++i){            if(p[i-1] == '*'){                dp[0][i] = dp[0][i-2];            }        }        for(int i=1;i<=m;++i){            for(int j=1;j<=n;++j){                if(p[j-1] == '.'){                    dp[i][j] = dp[i-1][j-1];                }                else if (p[j-1] != '*') {                    dp[i][j] = dp[i-1][j-1] && p[j-1] == s[i-1];                }                else if (p[j-2] != s[i-1] && p[j-2] != '.') {                    dp[i][j] = dp[i][j-2];                }                 else {                    dp[i][j] = dp[i][j-1] || dp[i-1][j] || dp[i][j-2];                }            }        }        return dp[m][n];    }};

分析:使用一个二维数组 dp,其中 dp[i][j]表示以 i截止的字符串是否可以被以 j截止的正则表达式匹配。

错误:没有思路

股票交易

Leetcode 121

给定一段时间内每天某只股票的固定价格,已知你只可以买卖各一次,求最大的收益。

class Solution {public:    int maxProfit(vector<int>& prices) {        int sell = 0, buy = INT_MIN;        for (int i = 0; i < prices.size(); ++i) {            buy = max(buy, -prices[i]);            sell = max(sell, buy + prices[i]);        }        return sell;    }};

分析:遍历一次就行,记录一下最小的价格,然后遍历到每个价格的时候看看是不是比这个价格更大就行了。

错误:简单的问题也不会想了。。。

Leetcode 188

给定一段时间内每天某只股票的固定价格,已知你只可以买卖各 k次,且每次只能拥有一支股票,求最大的收益。

Leetcode 309

给定一段时间内每天某只股票的固定价格,已知每次卖出之后必须冷却一天,且每次只能拥有一支股票,求最大的收益。

class Solution {public:    int maxProfit(vector<int>& prices) {        int n = prices.size();        if(n == 0){            return 0;        }        vector<int> buy(n),sell(n),s1(n),s2(n);        s1[0] = buy[0] = -prices[0];        sell[0] = s2[0] = 0;        for(int i=1;i<n;++i){            buy[i] = s2[i-1] - prices[i];            s1[i] = max(buy[i-1],s1[i-1]);            sell[i] = max(buy[i-1],s1[i-1]) + prices[i];            s2[i] = max(s2[i-1],sell[i-1]);        }        return max(sell[n-1],s2[n-1]);    }};

分析:状态机求解

错误:完全不懂

练习

Leetcode 213

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

class Solution {public:    int rob(vector<int>& nums) {        int n = nums.size();        if(n == 1){            return nums[0];        }        else if(n == 2){            return max(nums[0],nums[1]);        }        vector<int> dp(n+1);        int answer_a;        dp[0] = dp[1] = dp[2] = nums[0];        for(int i=3;i<n;++i){            dp[i] = max(dp[i-1],dp[i-2] + nums[i-1]);        }        answer_a = dp[n-1];        dp[0] = dp[1] = 0;        dp[2] = nums[1];        for(int i=3;i<=n;++i){            dp[i] = max(dp[i-1],dp[i-2] + nums[i-1]);        }        return max(answer_a,dp[n]);    }};

分析:分两种情况进行讨论,选第一个和不选第一个。

错误:看了一下思路,最后调通了

Leetcode 53

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

class Solution {public:    int maxSubArray(vector<int>& nums) {        int n = nums.size();        vector<int> dp(n+1);        dp[0] = -20000;        for(int i=1;i<=n;++i){            dp[i] = max(nums[i-1],dp[i-1] + nums[i-1]);        }        return *max_element(dp.begin(),dp.end());    }};

分析:dp数组记录以当前位置为结尾的子数组的最大和,因此后面再加一位有两种可能,一是和这个一起,二是自己一组。最后取最大的部分即可。

错误:开始没想太懂,后来自己调通了。

Leetcode 343

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

class Solution {public:    int integerBreak(int n) {        vector<int> dp(n+1);        for(int i=2;i<=n;++i){            for(int j=1;j<i;++j){                dp[i] = max(dp[i],max(j*(i-j),j*dp[i-j]));            }        }        return dp[n];    }};

分析:对于正整数n,当n≥2时,可以拆分成至少两个正整数的和。令x是拆分出的第一个正整数,则剩下的部分是n-x,n−x可以不继续拆分,或者继续拆分成至少两个正整数的和。每个正整数对应的最大乘积取决于比它小的正整数对应的最大乘积。

错误:分割问题还是没有什么思路

Leetcode 583

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的 最小步数 。每步可以删除任意一个字符串中的一个字符。

class Solution {public:    int minDistance(string word1, string word2) {        int m = word1.size();        int n = word2.size();        vector<vector<int>> dp(m+1,vector<int>(n+1,0));        for(int i=0;i<=m;++i){            dp[i][0] = i;        }        for(int i=0;i<=n;++i){            dp[0][i] = i;        }        for(int i=1;i<=m;++i){            for(int j=1;j<=n;++j){                if(word1[i-1] == word2[j-1]){                    dp[i][j] = dp[i-1][j-1];                }                else{                    dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + 1;                }            }        }        return dp[m][n];    }};

分析:不相等的时候看两边的字符串,相等的时候看前一位

错误:字符相等的时候有些没想明白,后来调通了

Leetcode 646

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。现在,我们定义一种跟随关系,当且仅当 b < c 时,数对 (c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

class Solution {public:    static bool cmp(vector<int> &a,vector<int> &b){        return a[0] < b[0];    }    int findLongestChain(vector<vector<int>>& pairs) {        int n = pairs.size();        sort(pairs.begin(),pairs.end(),cmp);        vector<int> dp(n+1,1);        for(int i=1;i<=n;++i){            for(int j=0;j<i-1;++j){                if(pairs[i-1][0] > pairs[j][1]){                    dp[i] = max(dp[i],dp[j+1]+1);                }            }        }        return dp[n];    }};

分析:排序后进行动态规划即可

错误:排序有问题。

Leetcode 376

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。给你一个整数数组 nums ,返回 nums 中作为摆动序列的最长子序列的长度

class Solution {public:    int wiggleMaxLength(vector<int>& nums) {        int n = nums.size();        if(n == 1 || (n == 2 && nums[0] != nums[1])){            return n;        }        if(n == 2 && nums[0] == nums[1]){            return 1;        }        vector<int> up(n),down(n);        up[0] = down[0] = 1;        for(int i=1;i<n;++i){            if(nums[i] > nums[i-1]){                up[i] = max(up[i-1],down[i-1] + 1);                down[i] = down[i-1];            }            else if(nums[i] < nums[i-1]){                up[i] = up[i-1];                down[i] = max(down[i-1],up[i-1] + 1);            }            else{                up[i] = up[i-1];                down[i] = down[i-1];            }        }        return max(up[n-1],down[n-1]);    }};

分析:每当我们选择一个元素作为摆动序列的一部分时,这个元素要么是上升的,要么是下降的,这取决于前一个元素的大小。那么列出状态表达式为:up[i]表示以前 i个元素中的某一个为结尾的最长的「上升摆动序列」的长度。down[i]表示以前i个元素中的某一个为结尾的最长的「下降摆动序列」的长度。

错误:没有思路

Leetcode 494

class Solution {public:    int findTargetSumWays(vector<int>& nums, int target) {        int sum = 0;        for (int& num : nums) {            sum += num;        }        int diff = sum - target;        if (diff < 0 || diff % 2 != 0) {            return 0;        }        int n = nums.size(), neg = diff / 2;        vector<vector<int>> dp(n + 1, vector<int>(neg + 1));        dp[0][0] = 1;        for (int i = 1; i <= n; i++) {            int num = nums[i - 1];            for (int j = 0; j <= neg; j++) {                dp[i][j] = dp[i - 1][j];                if (j >= num) {                    dp[i][j] += dp[i - 1][j - num];                }            }        }        return dp[n][neg];    }};

分析:转化为0-1背包问题

错误:背包问题一直都不怎么理解,就先这样,后续再补充。

Leetcode 714

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

class Solution {public:    int maxProfit(vector<int>& prices, int fee) {        int n = prices.size();        vector<vector<int>> dp(n, vector<int>(2));        dp[0][0] = 0, dp[0][1] = -prices[0];        for (int i = 1; i < n; ++i) {            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);        }        return dp[n - 1][0];    }};

分析:股票问题的变形,比较类似于状态机,不是很能想得到

错误:股票问题后面也要再做一做

总结

动态规划比较有难度,一是状态转移方程的写法,二是在实现状态转移中的各种细节。以后对于动态规划还要勤加练习,多练习思考方法。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:高级人工智能-第1讲 人工智能概述 + + /2022/09/01/UCAS/advanced-ai/advanced-ai-1/ + + 《高级人工智能》课程笔记:第1讲 人工智能概述

首先讲授人工智能基础知识,进而分三个专题(联结主义、符号主义、行为主义)介绍人工智能的新进展。

第1讲 人工智能概述

智能和人工智能

智能:个体适应环境并能在不同环境中实现其目标的能力。

蕴含众多方面的能力

  • 创造、推理、学习
  • 归纳、演绎、类比
  • 优化、规划、知识
  • 模式识别、问题求解

人工智能:

  • 机器智能:使机器具备计算和“判别”的行为能力
  • 类脑智能:仿生智能,让机器像人或生物一样思考
  • 群体智能:社会智能的机器重现与利用、涌现智能

人工智能的发展历史

机械智能 ➡ 理性思考 ➡ 数理逻辑 ➡ 计算思维

萌芽期

  • 机械自动化
    • 希腊,蒸汽驱动的“会唱歌”的乌鸦
    • 中国,鲁班的“木鸢”,诸葛亮的“木牛流马”
  • 逻辑推理
    • 亚里士多德的“三段论”:从一般前提到具体论断

孕育期(文艺复兴以来)

  • 理性主义
    • 笛卡尔:mind/body二象性,不相信机器会具有智能
  • 数理逻辑学科
    • 莱布尼茨:演算推论器,符号逻辑,提出将人的知识汇成“知识库”
    • 弗雷治:谓词演算
  • 计算思维
    • 巴贝奇:差分机
    • 图灵:图灵机

形成期(1956年-1961年)

  • 1956年,首次人工智能研讨会
  • IBM的西洋跳棋程序、文法体系、逻辑推理机、行动计划咨询系统、通用问题求解器

发展期(60年代)

  • 研究领域拓展
    • 问题求解、博弈、定理证明、程序设计、机器视觉、自然语言理解、知识表示、专家系统、神经网络、智能机器人……
  • 1969年,第一届国际人工智能联合会议(IJCAI)
  • 1970年,《人工智能》国际杂志创刊,《Artificial Intelligence 》

寒冬期(60年代末到70年代初)

  • 1966年,美国政府取消了机器翻译项目的所有投资
  • 英国政府取消了几乎所有人工智能研究投入
  • 神经网络的研究经费缩减到几乎没有

艰难前行(70年代)

  • 弱方法:构建搜索机制,试图找出完全解
    • 下棋:搜索解空间
  • 强方法:构建领域知识库
    • 专家系统:知识表示开始成为研究热点

走向工业(80年代)

  • 1982年,第一个商用专家系统RI
  • 1981年,日本启动“第五代计算机”计划,运行prolog语言的智能计算机
  • 美国、英国恢复对人工智能的投入

今天

  • 大数据利用、计算能力提升、网络泛在化
  • 神经网络的复兴
    • 多层感知机及其学习算法(BP算法)的提出
    • 隐马尔科夫模型(HMM)在语音识别上取得成功
    • 贝叶斯网络
  • 专家系统逐渐成熟
    • 知识发现、数据挖掘兴起
  • 人工智能开始成为科学
    • 学科边界开始明晰
    • 并开始借鉴其他学科的理论,如控制论、心里学、统计学

人工智能:研究如何像人一样行动?

考试内容:图灵测试

Can Machine Think?

图灵测试:一个人(C)在完全不接触对方(A和B)的情况下,和对方进行一系列的问答,如果在相当长时间内,他无法根据这些问题判断对方是人(B)还是计算机(A),那么,就认为该计算机具有同人相当的智能(即计算机是能思维的)。

质疑:

  • 图灵测试不是可构造的
    • 例如:“完全不接触”的环境难以构建
  • 图灵测试不是可重现的
    • 例如:问题是开放的,答案正确性的判定是主观的
  • 图灵测试无法进行数学分析
    • 只是一种操作式测试,缺少形式化描述不严谨

图灵预言:到2000年,机器可以做到5分钟内以30%的可能性让普通人分辨不出其是机器还是人。

图灵测试案例

  • Master横空出世:Master在围棋对战网站上出现连胜30多场,才开始有人怀疑这是“机器人”。
  • 人工智能机器人Sophia:电视节目主持人查理•罗斯在节目《60分钟》中采访了Sophia机器人时,索菲亚不但对答如流,还与他开起了玩笑。

神经网络模拟器

  • Snare:1951年由马文·明斯基提出,学习如何穿过迷宫
  • 他是多智能体的最早尝试者之一,使机器能基于过去行为的知识,预测其当前行为的结果

人工智能三大学派

达特茅斯会议:1956年在达特茅斯学院发起

发起人

  • 约翰·麦卡锡(人工智能之父,Lisp语言发明者,1971年获图灵奖)
  • 马文·明斯基(1969年获图灵奖,首个获图灵奖的人工智能学者)
  • 克劳德·香农(信息论之父)
  • 纳撒尼尔·罗彻斯特(IBM 700系列计算机首席工程师,发明了首个汇编语言)

会议成就

  • 首次提出了“人工智能”一词
  • 会议三大亮点
    • 明斯基的Snare
    • 麦卡锡的𝛼-𝛽搜索法
    • 西蒙和纽厄尔的“逻辑理论家”

并且出现了人工智能三大学派:

  • 符号主义学派
  • 联结主义学派
  • 行为主义学派

符号主义学派(逻辑学派):规则驱动的确定性智能

  • 认为“人的认知基元是符号,认知过程即符号操作过程
  • 认为人和计算机都是物理符号系统,可以用计算机来模拟人的智能行为
  • 认为人工智能的核心是知识表示、知识推理和知识运用
  • 代表人物
    • 西蒙(1975年获图灵奖、1978年获诺贝尔经济学奖)
    • 纽厄尔

衍生出:逻辑、专家系统、知识库

联结主义学派(仿生学派或生理学派):数据驱动的不确定性智能

  • 认为人的思维基元是神经元,而不是符号处理过程
  • 认为人脑不同于电脑
  • 原理:神经网络及神经网络间的连接机制和学习算法
  • 代表人物
    • 麦卡洛克(McCulloch)
    • 皮茨(Pitts)

衍生出:人工神经网络、认知科学、类脑计算

行为主义学派(进化主义或控制论学派):交互驱动的涌现智能

  • 认为智能取决于感知和行动
  • 主张利用机器对环境作用后的响应或反馈为原型来实现智能化
  • 认为人工智能可以像人类智能一样通过进化、学习来逐渐提高和增强
  • 代表人物:布鲁克斯

衍生出:控制论、多智能体、强化学习等

人工智能研究的课题

三大层次

  • 基础理论:数学、思维科学、认知科学等
  • 原理技术:启发式搜索、演化计算
  • 工程应用:模式识别、计算机视觉、自然语言理解、问答系统

四大问题

  • 知识科学、问题求解、机器学习、系统构成

人工智能之哲学基础

弱人工智能

  • 机器表现得像具有智能一样
  • 图灵测试

强人工智能

  • 机器实际具有智能
  • 机器具有自我意识吗?
  • 自由意志悖论
    • 受物理法则严格支配的思想会是自由的吗?
    • 如果不能够说出我下一步会做什么,就说明我具有自由意志?

人工智能恐慌

  • 会不会造成人们失业?
    • 目前来看,人工智能技术带来的自动化,其创造的就业就会大于其减少的就业机会
  • 对隐私权的侵害?
  • 是否导致可审计的丧失?
    • 例如:听从了医疗诊断专家系统的建议而带来的医疗事故,责任归谁?

人工智能实现了会怎样?

  • 人工智能的成功是否会意味着人类灭亡
    • 人工演化取代自然选择
    • 机器智能一旦超过人类智能,他就能设计出更聪明的机器
    • 智力爆炸和技术奇点,人类时代的终结
  • 怎么办?
    • 让机器保持可控
    • 使用人工智能拓展人类智能,将人工智能合并到人类智能中

人工智能伦理

  • 机器人三法则
    • 第一法则:机器人不得伤害人类,或袖手旁观坐视人类受到伤害
    • 第二法则:除非违背第一法则,机器人必须服从人类的命令
    • 第三法则:在不违背第一及第二法则下,机器人必须保护自己

人工智能的目标

  • 近期目标
    • 研究如何使机器做过去只有依靠人的智力才能完成的工作
  • 远期目标
    • 研究如何利用自动机模拟人的思维过程和智能行为,从而造出智能机器
  • 终极目标
    • 机器智能实现甚至超过生物智能

“准人”水平的人工智能:手写识别、物体识别、语音识别、自然语言处理、词义消歧、机器翻译

“过人”水平的人工智能:游戏、双陆棋、国际象棋、桥牌、填词、拼字、七巧板、自动驾驶、智力竞赛问答、OCR字符识别

“许多尖端的人工智能由于应用广泛,已经不再被称为人工智能。因为,人们一旦觉得某些东西非常有用并广泛使用,就不再称之为人工智能了。”

人工智能案例实践

  • 定理证明
    • 50年代中期,西蒙和纽厄尔提出的“逻辑理论家”,证明了《数学原理》书中的38个定理
    • 1962年,改进后证明了书中全部52个定理,被认为是用计算机探讨人类智能的第一个真正成果
  • 案例
    • 四色定理
      • 1852年提出,一直无人给出理论证明
      • 1976年6月,哈肯在伊利诺伊用两台计算机,用时1200个小时,通过100亿次判断,完成了证明,轰动世界
    • 吴方法:吴文俊教授提出的“数学机器化”
  • 通用问题求解器(GPS:General Problem Solver)
    • 1957年开始,纽厄尔等人开始研究不依赖于具体领域的通用解题程序
    • 模仿人类问题求解过程,第一个实现了“像人一样思考”的程序
  • 专家系统
    • 将领域专家的知识整理出来,让计算机利用这些知识求解专门领域的问题
    • DENDRAL:第一个专家系统,1968年问世,斯坦福大学完成,用于推断化学分子结构
    • MYCIN:著名的医疗诊断专家系统
    • RI:第一个商用专家系统,DEC公司于1982年正式使用
  • 海湾战争中的专家系统
    • 1991年的海湾战争,美国将专家系统用于后勤规划和运输日程安排
    • 涉及50000个车辆、货物和人,需要考虑起点、目的地、路径以及解决参数冲突问题
    • 该系统使一个计划可以在几个小时内产生,而旧方法需要几个星期
  • 数字识别
    • 清华大学智能技术与系统国家重点实验室采用神经元网络研制了数字识别系统
    • 用于2000年我国的人口普查
    • 错误率达到低于万分之一的水平
  • 古籍数字化(OCR技术):《四库全书》
  • 国际象棋:IBM的“深蓝”
    • 1997年,IBM公司的“深蓝”在美国纽约公平大厦以3.5:2.5击败了国际象棋世界冠军卡斯帕罗夫
  • 围棋
    • AlphaGo: DeepMind
      • 使用深度学习技术(CNN:卷积神经网络)对棋局的局势进行估值
      • 在和其他围棋程序的对弈中取得99.8%的胜率
      • 和李世石的人机大战中以4:1取胜,在人机对战中60连胜,以3:0战胜柯洁
    • AlphaGo背后的技术
      • 深度学习(联结主义)+ 强化学习(行为主义)
      • 利用残差神经网络(ResNet)训练深度模型
      • 利用马尔科夫树搜索技术解决围棋的搜索空间爆炸问题
      • 采用**“自我对弈”**策略进行无人工标注的自我训练
  • 自动驾驶
    • 在高速公路上,自动识别道路,自动躲避障碍物
    • 平均时速达到100公里/小时,最高速度可达150公里/小时
    • 从匹兹堡到圣地亚哥,98%的时间自动驾驶
  • 自然语言处理
    • 神经语言模型和词嵌入技术:word2vec
    • 机器翻译:统计机器翻译(SMT)到神经机器翻译(NMT)
    • 文本生成技术:给图像或视频加标题、聊天机器人、机器人写新闻报道、BERT和GPT-3
  • 生成式预训练语言模型:GPT
  • IBM仿人脑芯片:TrueNorth
    • DARPA的研究项目SyNapse(自适应可塑可伸缩电子神经系统)的最新成果
    • 邮票大小、重量只有几克,集成54 亿个硅晶体管,内置4096 个内核,100 万个“神经元”、2.56 亿个“突触”,能力相当于一台超级计算机,功耗只有65 毫瓦
    • 目标:突破冯·诺依曼体系
  • 脑科学
    • 2013年1月,欧盟启动“人类大脑计划”
    • 2013年4月,奥巴马宣布启动“大脑基金计划”
    • 2014年,我国着手启动“脑科学计划”
  • 互联网大脑:知识图谱+深度学习,利用网络大数据推断目标间的潜在关联关系等关系,为用户提供查询推荐、搜索导航等知识获取和深度理解功能。
  • 系统论
    • 复杂自适应系统
      • 1984年,美国圣塔菲研究所成立
      • 诺贝尔物理学将得主盖尔曼认为智能体现为个体的自适应能力,大量智能体(agent)积极地相互竞争和合作,在没有中央指挥的情况下,通过彼此相互作用和相互适应也能形成整体的有序状态

人工智能的今天

  1. 自然语言理解(主战场之一):聊天机器人:小冰
  2. 智能阅卷:安庆会考全学科智能阅卷
  3. 考试机器人:美国华盛顿大学图灵中心和日本Todai高考机器人
  4. 人工智能三级跳:运算智能(能存会算)➡感知智能(能听会说、能看会认)➡认知智能(能理解会思考)
  5. 深度学习技术:DNN、RNN、CNN
  6. 生物特征识别技术(刷脸、瞳仁、声纹……)
  7. 中国创业公司:Face++

人工智能的发展趋势

  • 从“人机对抗”走向“人机协作”
    • AI 1.0
      • 让机器在某些任务上“战胜”人
    • AI 2.0:人本计算(human computation)
      • 让机器和人相互协作,完成更复杂的任务
      • 机器做机器擅长的:计算
      • 人做人擅长的:思考
  • 从单点智能走向网络智能
    • AI 1.0
      • 单个机器具备人的某些智能,例如:听、说、读、写、感知、认知……
    • AI 2.0
      • 借助互联网实现智能网络化
  • 从专用人工智能走向通用人工智能
    • AI 1.0
      • 在具体的任务上,让机器具备智能,例如:围棋、自动驾驶……
    • AI 2.0
      • 研究通用人工智能,包括探索智能形成的机制,AlphaGo到Master是一种初步尝试,让机器具备能够自我学习、形成概念的能力

人工智能是国家战略:2017年,国务院印发了《新一代人工智能发展规划》,人工智能成为国家战略,大数据在人工智能中将扮演越来越重要的角色。

人工智能经过60余年的发展取得了长足进步,近年来呈现出爆发之势,但总体上还处于初级阶段,通用智能之路任重道远。

]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Advanced AI + + + +
+ + + + + 研究生课程:模式识别与机器学习-第1章 概论 + + /2022/08/31/UCAS/pattern-recognition-and-machine-learning/pattern-recognition-and-machine-learning-1/ + + 《模式识别与机器学习》课程笔记:第1章 概论

  • 着重讲述模式识别与机器学习的基本概念,基本理论和方法,关键算法原理以及典型应用情况。
  • 注重理论与实践紧密结合
    • 实例教学:通过实例讲述如何将所学知识运用到实际应用之中
  • 尽量避免引用过多的、繁琐的数学推导

第1章 概论

什么是模式

  • 广义地说,存在于时间和空间中可观察的物体,如果我们可以区别它们是否相同或是否相似,都可以称之为模式。
  • 模式所指的不是事物本身,而是从事物获得的信息,因此,模式往往表现为具有时间和空间分布的信息。
  • 模式的直观特性:①可观察性②可区分性③相似性

模式识别的目的:利用计算机对物理对象进行分类,在错误概率最小的条件下,使识别的结果尽量与客观物体相符合。

模式识别的数学化:Y= F(X)X的定义域取自特征集,Y的值域为类别的标号集,F是模式识别的判别方法。

机器学习:研究如何构造理论、算法和计算机系统,让机器通过从数据中学习后可以进行分类和识别事物、推理决策、预测未来等工作。

模式识别系统的目标

在特征空间和解释空间之间找到一种映射关系,这种映射也称之为假说。

  • 特征空间:从模式得到的对分类有用的度量、属性或基元构成的空间。
  • 解释空间:c个类别的集合表示为Ω,称为解释空间。

机器学习的目标:针对某类任务 T,用 P衡量性能,根据经验 E来学习和自我完善,提高性能。

假说的两种获得方法:

  • 监督学习、概念驱动或归纳假说:在特征空间中找到一个与解释空间的结构相对应的假说。在给定模式下假定一个解决方案,任何在训练集中接近目标的假说也都必须在“未知”的样本上得到近似的结果。
    • 依靠已知所属类别的训练样本集,按它们特征向量的分布来确定假说(通常为一个判别函数),在判别函数确定之后能用它对未知的模式进行分类
    • 对分类的模式要有足够的先验知识,通常需要采集足够数量的具有典型性的样本进行训练。
  • 非监督学习、数据驱动或演绎假说:在解释空间中找到一个与特征空间的结构相对应的假说。这种方法试图找到一种只以特征空间中的相似关系为基础的有效假说。
    • 在没有先验知识的情况下,通常采用聚类分析方法,基于“物以类聚”的观点,用数学方法分析各特征向量之间的距离及分散情况;
    • 如果特征向量集聚集若干个群,可按群间距离远近把它们划分成类;
    • 这种按各类之间的亲疏程度的划分,若事先能知道应划分成几类,则可获得更好的分类结果。

主要分类和学习方法

数据聚类

  • 用某种相似性度量的方法将原始数据组织成有意义的和有用的各种数据集。
  • 是一种非监督学习的方法,解决方案是数据驱动的。

统计分类

  • 基于概率统计模型得到各类别的特征向量的分布,以取得分类的方法。
  • 特征向量分布的获得是基于一个类别已知的训练样本集。
  • 是一种监督分类的方法,分类器是概念驱动的。

结构模式识别

  • 该方法通过考虑识别对象的各部分之间的联系来达到识别分类的目的。
  • 识别采用结构匹配的形式,通过计算一个匹配程度值(matching score)来评估一个未知的对象或未知对象某些部分与某种典型模式的关系如何。
  • 当成功地制定出了一组可以描述对象部分之间关系的规则后,可以应用一种特殊的结构模式识别方法-句法模式识别,来检查一个模式基元的序列是否遵守某种规则,即句法规则或语法。

神经网络

  • 神经网络是受人脑组织的生理学启发而创立的。
  • 由一系列互相联系的、相同的单元(神经元)组成。相互间的联系可以在不同的神经元之间传递增强或抑制信号。
  • 增强或抑制是通过调整神经元相互间联系的权重系数来(weight)实现。
  • 神经网络可以实现监督和非监督学习条件下的分类。

监督学习

  • 监督学习是从有标记的训练数据来推断或建立一个模型,并依此模型推测新的实例。
  • 训练数据包括一套训练实例。在监督学习中,每个实例是由一个输入对象(通常为矢量)和一个期望的输出值(也称为监督信号)组成。
  • 一个最佳的模型将能够正确地决定那些看不见的实例的标签。常用于分类和回归。

无监督学习

  • 无监督学习是我们不告诉计算机怎么做,而是让它自己去学习怎样做一些事情。
  • 无监督学习与监督学习的不同之处在于,事先没有任何训练样本,需要直接对数据进行建模,寻找数据的内在结构及规律,如类别和聚类。
  • 常用于聚类、概率密度估计。

半监督学习

  • 半监督学习(Semi-supervised Learning)是模式识别和机器学习领域研究的重点问题,是监督学习与无监督学习相结合的一种学习方法。
  • 它主要考虑如何利用少量的标注样本和大量的未标注样本进行训练和分类的问题。
  • 半监督学习的主要算法有五类:基于概率的算法;在现有监督算法基础上改进的方法;直接依赖于聚类假设的方法;基于多视图的方法;基于图的方法。

强化学习

  • 强化学习要解决的问题:一个能够感知环境的自治机器人,怎样通过学习选择能达到其目标的最优动作。
  • 机器人选择一个动作用于环境,环境接受该动作后状态发生变化,同时产生一个强化信号(奖或惩)反馈回来。
  • 机器人根据强化信号和环境当前状态再选择下一个动作,选择的原则是使受到正强化(奖)的概率增大。

集成学习

  • 集成学习(Ensemble Learning)是机器学习中一类学习算法,指联合训练多个弱分类器并通过集成策略将弱分类器组合使用的方法。
  • 由于整合了多个分类器,这类算法通常在实践中会取得比单个若分类器更好的预测结果。
  • 常见的集成策略有:Boosting、Bagging、 Random subspace 、Stacking等。
  • 常见的算法主要有:决策树、随机森林、Adaboost、GBDT、DART等。

深度学习

  • 深度学习的概念源于人工神经网络的研究,除输入层和输出层外,含多个隐藏层的神经网络就是一种深度学习结构。
  • 深度学习通过层次化模型结构可从低层原始特征中逐渐抽象出高层次的语义特征,以发现复杂、灵活、高效的特征表示。
  • 常见的深度学习模型有:卷积神经网络,递归神经网络,深度信任网络,自编码器,变分自编码器等。

元学习

  • 元学习(Meta Learning)或者叫做“学会学习”(Learning to Learn),它是要“学会如何学习”,即利用以往的知识经验来指导新任务的学习,具有学会学习的能力。
  • 当前的机器学习模型往往只局限于从头训练已知任务并使用精调来学习新任务,耗时较长,且性能提升较为有限。
  • Meta Learning 就是研究如何让元模型记忆理解以往学习知识,使算法能在小样本训练的情况下完成新任务的学习。

多任务学习

  • 多任务学习是指通过共享相关任务之间的表征,联合训练多个学习任务的学习范式。
  • 在通常的机器学习范式中,不同任务的学习过程往往分别处理,任务间的关系完全被割裂。而在多任务学习范式中,联系学习机制使不同任务的学习过程充分共享,可显著减少每个任务所需的训练样本。
  • 多任务学习的主要形式有:联合学习、自主学习和带有辅助任务的学习。

多标记学习

  • 多标记学习问题为一种特殊的有监督分类问题,其所处理的数据集中的每个样本可同时存在多个真实类标。
  • 多标记学习主要用于处理多种标签的语义重叠,如预测歌曲的音乐流派,预测图书、商品的属性标签。
  • 多标记学习算法主要分为两类:
    • 问题转换法:把多标签问题转为其它学习场景,比如转为二分类、标签排序、多分类等。
    • 算法改编法:通过改编流行的学习算法去直接处理多标签数据,比如改编决策树、核技巧等。

对抗学习

  • 对抗学习是针对传统机器学习的一种攻击性方法,是机器学习和计算机安全领域都十分关注的交叉问题。
  • 对抗学习主要通过恶意输入来误导机器学习算法或模型使其得到错误结果,并在该过程中暴露机器学习算法存在的脆弱性,帮助设计适应复杂环境的鲁棒学习方法。
  • 常见的对抗学习方法主要有针对训练阶段的毒害式攻击以及针对测试阶段的躲避式攻击,常见的对抗学习场景主要有:垃圾邮件过滤、身份识别以及恶意软件检测等。

模式识别系统构成

模式识别系统与机器学习系统构成对比

v4Ot91.png
v4OUc6.png

模式识别系统组成单元

  • 数据获取:用计算机可以运算的符号来表示所研究的对象
    • 二维图像:文字、指纹、地图、照片等
    • 一维波形:脑电图、心电图、季节震动波形等
    • 物理参量和逻辑值:体温、化验数据、参量正常与否的描述
  • 预处理单元:去噪声,提取有用信息,并对输入测量仪器或其它因素所造成的退化现象进行复原
  • 特征提取和选择:对原始数据进行变换,得到最能反映分类本质的特征
    • 测量空间:原始数据组成的空间
    • 特征空间:分类识别赖以进行的空间
    • 模式表示:维数较高的测量空间->维数较低的特征空间
  • 分类决策:在特征空间中用模式识别方法把被识别对象归为某一类别
    • 基本做法:在样本训练集基础上确定某个判决规则,使得按这种规则对被识别对象进行分类所造成的错误识别率最小或引起的损失最小。

机器学习系统组成单元

  • 环境:是系统的工作对象(包括外界条件),代表信息来源。
    • 信息水平:相对于执行环节要求而言,由学习环节消除差距
    • 信息质量:实例示教是否正确、实例次序是否合理等
  • 知识库:存储学习到的知识
    • 知识的表示要合理
    • 推理方法的实现不要太难
    • 存储的知识是否支持修改(更新)
  • 学习环节:是系统的核心模块,是和外部环境的交互接口。
    • 对环境提供的信息进行整理、分析、归纳或类比,生成新的知识单元,或修改知识库。
    • 接收从执行环节来的反馈信号,通过知识库修改,进一步改善执行环节的行为。
  • 执行:根据知识库执行一系列任务
    • 把执行结果或执行过程中获得的信息反馈给学习环节

模式识别过程实例

在传送带上用光学传感器件对鱼按品种分类

  1. 数据获取:架设一个摄像机,采集一些样本图像,获取样本数据
  2. 预处理:去噪声,用一个分割操作把鱼和鱼之间以及鱼和背景之间分开
  3. 特征提取和选择:对单个鱼的信息进行特征选择,从而通过测量某些特征来减少信息量
  4. 分类决策:把特征送入决策分类器

相关数学概念

  • 随机向量及其分布
    • 数学期望和方差
    • 协方差矩阵
  • 正态分布
    • 一维正态密度函数
    • 多维正态密度函数
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Pattern Recognition and Machine Learning + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第6章 搜索 + + /2022/08/30/Leetcode/Leetcode-101/Leetcode-101-6/ + + Leetcode 刷题笔记-Leetcode 101 第6章 搜索

搜索

深度优先搜索和广度优先搜索是两种最常见的优先搜索方法,它们被广泛地运用在图和树等结构中进行搜索。

深度优先搜索

Leetcode 695

岛屿是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直的四个方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。岛屿的面积是岛上值为 1 的单元格的数目。计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0

class Solution {public:    static int DFS(vector<vector<int>> & grid,int x,int y,int m,int n){        if(x < 0 || x >= m || y < 0 || y >= n || grid[x][y] == 0){            return 0;        }        grid[x][y] = 0;        return 1 + DFS(grid,x+1,y,m,n) + DFS(grid,x-1,y,m,n) + DFS(grid,x,y+1,m,n) + DFS(grid,x,y-1,m,n);    }    int maxAreaOfIsland(vector<vector<int>>& grid) {        int m = grid.size();        int n = grid[0].size();        int maxarea = 0;        for(int i=0;i<m;++i){            for(int j=0;j<n;++j){                if(grid[i][j] == 1){                    maxarea = max(maxarea,DFS(grid,i,j,m,n));                }            }        }        return maxarea;    }};

分析:标准的DFS,重点要判断是否越界以及返回值的处理。

错误:基本思路是正确的,返回值的处理有问题,以及想的有些复杂。

Leetcode 547

n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中 省份 的数量。

class Solution {public:    static void DFS(vector<vector<int>>& isConnected, vector<bool>& visit,int x,int n){        if(x < 0 || x >= n || visit[x] == true){            return;        }        visit[x] = true;        for(int i=0;i<n;i++){            if(isConnected[x][i] == 1){                DFS(isConnected,visit,i,n);            }        }        return;    }    int findCircleNum(vector<vector<int>>& isConnected) {        int n = isConnected.size();        int sumCount = 0;        vector<bool> visit(n);        fill(visit.begin(),visit.end(),false);        for(int i=0;i<n;i++){            if(visit[i] == false){                DFS(isConnected,visit,i,n);                ++sumCount;            }        }        return sumCount;    }};

分析:还是比较基本的DFS,只不过是一个一维的DFS,比较简单

错误:开始的思路有一些偏差,后面纠正过来没什么问题了。

Leetcode 417

有一个 m × n 的矩形岛屿,与太平洋大西洋相邻。 太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heightsheights[r][c] 表示坐标 (r, c) 上单元格高于海平面的高度 。岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。返回网格坐标 result2D 列表 ,其中 result[i] = [r<sub>i</sub>, c<sub>i</sub>] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋

class Solution {public:    static void DFS(vector<vector<int>>& heights,vector<vector<bool>>& Ocean,int x,int y,int m,int n){        Ocean[x][y] = true;        if(x+1 < m && Ocean[x+1][y] == false && heights[x+1][y] >= heights[x][y]){            DFS(heights,Ocean,x+1,y,m,n);        }        if(x-1 >= 0 && Ocean[x-1][y] == false && heights[x-1][y] >= heights[x][y]){            DFS(heights,Ocean,x-1,y,m,n);        }        if(y+1 < n && Ocean[x][y+1] == false && heights[x][y+1] >= heights[x][y]){            DFS(heights,Ocean,x,y+1,m,n);        }        if(y-1 >= 0 && Ocean[x][y-1] == false && heights[x][y-1] >= heights[x][y]){            DFS(heights,Ocean,x,y-1,m,n);        }        return;    }    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {        int m = heights.size();        int n = heights[0].size();        vector<vector<bool>> pOcean(m,vector<bool>(n,false));        vector<vector<bool>> aOcean(m,vector<bool>(n,false));        for(int i=0;i<m;++i){            if(pOcean[i][0] == false){                DFS(heights,pOcean,i,0,m,n);            }            if(aOcean[i][n-1] == false){                DFS(heights,aOcean,i,n-1,m,n);            }        }        for(int i=0;i<n;++i){            if(pOcean[0][i] == false){                DFS(heights,pOcean,0,i,m,n);            }            if(aOcean[m-1][i] == false){                DFS(heights,aOcean,m-1,i,m,n);            }        }        vector<vector<int>> result;        for(int i=0;i<m;++i){            for(int j=0;j<n;++j){                if(pOcean[i][j] == true && aOcean[i][j] == true){                    result.push_back(vector<int>{i,j});                }            }        }        return result;    }};

分析:仍然是比较普通的DFS,不过只要对周围的一圈进行DFS就足够了,不需要全部遍历。

错误:细节问题,写的时候一定好好检查 mn有没有用反。

回溯法

Leetcode 46

给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。

class Solution {public:    static void backtracking(vector<int>& nums,int level,vector<vector<int>>&result){        if(level == nums.size()-1){            result.push_back(nums);            return;        }        for(int i=level;i<nums.size();i++){            swap(nums[i],nums[level]);            backtracking(nums,level+1,result);            swap(nums[i],nums[level]);        }    }    vector<vector<int>> permute(vector<int>& nums) {        vector<vector<int>> result;        backtracking(nums,0,result);        return result;    }};

分析:对于每一个当前位置 i,我们可以将其于之后的任意位置交换,然后继续处理位置 i+1,直到处理到最后一位。为了防止我们每此遍历时都要新建一个子数组储存位置 i之前已经交换好的数字,我们可以利用回溯法,只对原数组进行修改,在递归完成后再修改回来。

错误:学习一下回溯法的基本框架。

Leetcode 77

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

class Solution {public:    static void backtracking(vector<vector<int>> &result,vector<int> &temp,int n,int level,int k){        if(temp.size() == k){            result.push_back(temp);            return;        }        for(int i=level+1;i<=n;++i){            temp.push_back(i);            backtracking(result,temp,n,i,k);            temp.pop_back();        }    }    vector<vector<int>> combine(int n, int k) {        vector<vector<int>> result;        vector<int> temp;        backtracking(result,temp,n,0,k);        return result;    }};

分析:类似于排列问题,也可以进行回溯。排列回溯的是交换的位置,而组合回溯的是是否把当前的数字加入结果中。

错误:需要有一个记录状态的数值,要不然就变成全排列了。

Leetcode 79

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

class Solution {public:    static void backtracking(vector<vector<char>>& board, string &word,int x,int y,int m,int n,vector<vector<bool>> &visited,bool &find,int level){        if(x < 0 || x >= m || y < 0 || y >= n){            return;        }        if(visited[x][y] == true || word[level] != board[x][y] || find == true){            return;        }        if(level == word.size() - 1){            find = true;            return;        }        visited[x][y] = true;        backtracking(board,word,x+1,y,m,n,visited,find,level+1);        backtracking(board,word,x-1,y,m,n,visited,find,level+1);        backtracking(board,word,x,y+1,m,n,visited,find,level+1);        backtracking(board,word,x,y-1,m,n,visited,find,level+1);        visited[x][y] = false;        return;    }    bool exist(vector<vector<char>>& board, string word) {        int m = board.size();        int n = board[0].size();        vector<vector<bool>> visited(m, vector<bool>(n, false));        bool find = false;        for(int i=0;i<m;++i){            for(int j=0;j<n;++j){                backtracking(board,word,i,j,m,n,visited,find,0);            }        }        return find;    }};

分析:典型回溯题,判断条件需要多一些

错误1:回溯法不要有返回值,都使用引用传参

错误2:判断条件:①是否越界②访问过③不匹配④已经确定对的了

Leetcode 51

n皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n ,返回所有不同的n皇后问题的解决方案。

class Solution {public:    void backtracking(vector<vector<string>> & result,vector<string> tempresult,vector<bool> &column,vector<bool> &ldiag,vector<bool> &rdiag,int n,int level){        if(level == n){            result.push_back(tempresult);            return;        }        for(int i=0;i<n;++i){            if (column[i] || ldiag[n-level+i-1] || rdiag[level+i]) {                continue;            }            tempresult[level][i] = 'Q';            column[i] = ldiag[n-level+i-1] = rdiag[level+i] = true;            backtracking(result,tempresult,column,ldiag,rdiag,n,level+1);            column[i] = ldiag[n-level+i-1] = rdiag[level+i] = false;            tempresult[level][i] = '.';        }        return;    }    vector<vector<string>> solveNQueens(int n) {        vector<vector<string>> result;        string tempstring = "";        for(int i=0;i<n;i++){            tempstring += ".";        }        vector<string> tempresult(n,tempstring);        vector<bool> column(n,false);        vector<bool> ldiag(2*n-1,false);        vector<bool> rdiag(2*n-1,false);        backtracking(result,tempresult,column,ldiag,rdiag,n,0);        return result;    }};

分析:最典型的回溯法之一。类似于在矩阵中寻找字符串,本题也是通过修改状态矩阵来进行回溯。不同的是,我们需要对每一行、列、左斜、右斜建立访问数组,来记录它们是否存在皇后。本题需要判断满足条件的结果中每一行或列有且仅有一个皇后。这是因为我们一共只有 n行和 n列。所以如果我们通过对每一行遍历来插入皇后,我们就不需要对行建立访问数组了。

错误:再理解吧。

广度优先搜索

Leetcode 934

在给定的二维二进制数组 A 中,存在两座岛。(岛是由四面相连的 1 形成的一个最大组。)现在,我们可以将 0 变为 1,以使两座岛连接起来,变成一座岛。返回必须翻转的 0 的最小数目。(可以保证答案至少是 1 。)

class Solution {public:    static void DFS(vector<vector<int>>& grid,queue<pair<int,int>> &points,int x,int y,int n){        if(x < 0 || x >= n || y < 0 || y >= n || grid[x][y] == 2){            return;        }        if(grid[x][y] == 0){            points.push({x,y});            return;        }        grid[x][y] = 2;        DFS(grid,points,x+1,y,n);        DFS(grid,points,x-1,y,n);        DFS(grid,points,x,y+1,n);        DFS(grid,points,x,y-1,n);        return;    }    int shortestBridge(vector<vector<int>>& grid) {        int n = grid.size();        queue<pair<int,int>> points;        bool find = false;        for(int i=0;i<n;++i){            if(find == true){                break;            }            for(int j=0;j<n;++j){                if(grid[i][j] == 1){                    find = true;                    DFS(grid,points,i,j,n);                    break;                }            }        }        int level = 0;        vector<vector<int>> d = {{0,1},{0,-1},{1,0},{-1,0}};        while(!points.empty()){            ++level;            int n_points = points.size();            while(n_points--){                auto [r,c] = points.front();                grid[r][c] = 2;                points.pop();                for(int k=0;k<4;k++){                    int x = r + d[k][0];                    int y = c + d[k][1];                    if(x >= 0 && y >= 0 && x < n && y < n){                        if(grid[x][y] == 1){                            return level;                        }                        else if(grid[x][y] == 0){                            grid[x][y] = 2;                            points.push({x,y});                        }                    }                }            }        }        return 0;    }};

分析:先通过任意搜索方法找到其中一个岛屿,然后利用广度优先搜索,查找其与另一个岛屿的最短距离

错误:BFS好久没有练习了,也是生疏了。

Leetcode 126

给定一个起始字符串和一个终止字符串,以及一个单词表,求是否可以将起始字符串每次改一个字符,直到改成终止字符串,且所有中间的修改过程表示的字符串都可以在单词表里找到。若存在,输出需要修改次数最少的所有更改方式。

class Solution {public:    vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &wordList) {        vector<vector<string>> res;        // 因为需要快速判断扩展出的单词是否在 wordList 里,因此需要将 wordList 存入哈希表,这里命名为「字典」        unordered_set<string> dict = {wordList.begin(), wordList.end()};        // 修改以后看一下,如果根本就不在 dict 里面,跳过        if (dict.find(endWord) == dict.end()) {            return res;        }        // 特殊用例处理        dict.erase(beginWord);        // 第 1 步:广度优先搜索建图        // 记录扩展出的单词是在第几次扩展的时候得到的,key:单词,value:在广度优先搜索的第几层        unordered_map<string, int> steps = {{beginWord, 0}};        // 记录了单词是从哪些单词扩展而来,key:单词,value:单词列表,这些单词可以变换到 key ,它们是一对多关系        unordered_map<string, set<string>> from = {{beginWord, {}}};        int step = 0;        bool found = false;        queue<string> q = queue<string>{{beginWord}};        int wordLen = beginWord.length();        while (!q.empty()) {            step++;            int size = q.size();            for (int i = 0; i < size; i++) {                const string currWord = move(q.front());                string nextWord = currWord;                q.pop();                // 将每一位替换成 26 个小写英文字母                for (int j = 0; j < wordLen; ++j) {                    const char origin = nextWord[j];                    for (char c = 'a'; c <= 'z'; ++c) {                        nextWord[j] = c;                        if (steps[nextWord] == step) {                            from[nextWord].insert(currWord);                        }                        if (dict.find(nextWord) == dict.end()) {                            continue;                        }                        // 如果从一个单词扩展出来的单词以前遍历过,距离一定更远,为了避免搜索到已经遍历到,且距离更远的单词,需要将它从 dict 中删除                        dict.erase(nextWord);                        // 这一层扩展出的单词进入队列                        q.push(nextWord);                        // 记录 nextWord 从 currWord 而来                        from[nextWord].insert(currWord);                        // 记录 nextWord 的 step                        steps[nextWord] = step;                        if (nextWord == endWord) {                            found = true;                        }                    }                    nextWord[j] = origin;                }            }            if (found) {                break;            }        }        // 第 2 步:回溯找到所有解,从 endWord 恢复到 beginWord ,所以每次尝试操作 path 列表的头部        if (found) {            vector<string> Path = {endWord};            backtrack(res, endWord, from, Path);        }        return res;    }    void backtrack(vector<vector<string>> &res, const string &Node, unordered_map<string, set<string>> &from,             vector<string> &path) {        if (from[Node].empty()) {            res.push_back({path.rbegin(), path.rend()});            return;        }        for (const string &Parent: from[Node]) {            path.push_back(Parent);            backtrack(res, Parent, from, path);            path.pop_back();        }    }};

分析:比较复杂的BFS+回溯法

错误:太复杂暂时还理解不了,慢慢来吧。。。

练习

Leetcode 130

给你一个 m x n 的矩阵 board ,由若干字符 'X''O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充。

class Solution {public:    static void DFS(vector<vector<char>>& board,int x,int y,int m,int n){        if(x < 0 || y < 0 || x >= m || y >= n){            return;        }        if(board[x][y] == 'X' || board[x][y] == 'A'){            return;        }        board[x][y] = 'A';        DFS(board,x+1,y,m,n);        DFS(board,x-1,y,m,n);        DFS(board,x,y-1,m,n);        DFS(board,x,y+1,m,n);        return;    }    void solve(vector<vector<char>>& board) {        int m = board.size();        int n = board[0].size();        for(int i=0;i<m;++i){            DFS(board,i,0,m,n);            DFS(board,i,n-1,m,n);        }        for(int i=0;i<n;++i){            DFS(board,0,i,m,n);            DFS(board,m-1,i,m,n);        }        for(int i=0;i<m;++i){            for(int j=0;j<n;++j){                if(board[i][j] == 'A'){                    board[i][j] = 'O';                }                else{                    board[i][j] = 'X';                }            }        }        return;    }};

分析:也是比较普通的DFS,注意记录是否访问过即可。

一遍AC

Leetcode 257

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

class Solution {public:    static void DFS(TreeNode* &root,vector<string> &paths,string temp){        if(root != nullptr){            temp += to_string(root->val);            if(root->left == nullptr && root->right == nullptr){                paths.push_back(temp);                return;            }            else{                temp += "->";                DFS(root->left,paths,temp);                DFS(root->right,paths,temp);            }        }    }    vector<string> binaryTreePaths(TreeNode* root) {        vector<string> paths;        DFS(root,paths,"");        return paths;    }};

分析:使用深度优先搜索。在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。如此,当遍历完整棵二叉树以后我们就得到了所有从根节点到叶子节点的路径。

错误:陷入回溯法的坑了。

Leetcode 47

给定一个可包含重复数字的序列 nums按任意顺序返回所有不重复的全排列。

class Solution {public:    static void backtracking(vector<int>& nums,vector<vector<int>> &result,int level){        if(level == nums.size() - 1){            result.push_back(nums);            return;        }        set<int> st;        for(int i=level;i<nums.size();++i){            if(st.find(nums[i]) == st.end()){                st.insert(nums[i]);                swap(nums[level],nums[i]);                backtracking(nums,result,level+1);                swap(nums[level],nums[i]);            }        }        return;    }    vector<vector<int>> permuteUnique(vector<int>& nums) {        vector<vector<int>> result;        backtracking(nums,result,0);        return result;    }};

分析:与全排列基本相同,添加一个set用于记录曾经交换过的数字,如果这个数字曾经交换过就不换了

错误:看了网上的思路。

Leetcode 40

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次

class Solution {public:    static void backtracking(vector<int>& candidates,vector<vector<int>> &result,vector<int> &path,vector<bool> &used,int level,int sum,int target){        if(sum > target){            return;        }        else if(sum == target){            result.push_back(path);            return;        }        else{            for(int i=level;i<candidates.size() && sum + candidates[i] <= target;++i){                if(i > 0 && candidates[i] == candidates[i-1] && used[i - 1] == false){                    continue;                }                sum += candidates[i];                path.push_back(candidates[i]);                used[i] = true;                backtracking(candidates,result,path,used,i+1,sum,target);                used[i] = false;                sum -= candidates[i];                path.pop_back();            }        }    }    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {        vector<vector<int>> result;        vector<int> path;        vector<bool> used(candidates.size(),false);        sort(candidates.begin(),candidates.end());        backtracking(candidates,result,path,used,0,0,target);        return result;    }};

分析:还是组合数,但是数字内部有重复的,因此需要对同一树层上的“使用过”进行去重。

错误:没什么思路。

Leetcode 37

编写一个程序,通过填充空格来解决数独问题。

class Solution {public:    static bool isValid(int i,int j,char k,vector<vector<char>>& board){        set<char> st;        for(int x=0;x<9;x++){            st.insert(board[i][x]);            st.insert(board[x][j]);        }        int p = i / 3;        int q = j / 3;        for(int x = p*3;x < p*3+3;++x){            for(int y = q * 3;y < q*3+3;++y){                st.insert(board[x][y]);            }        }        if(st.find(k) == st.end()){            return true;        }        return false;    }    static bool backtracking(vector<vector<char>>& board){        for(int i=0;i<9;++i){            for(int j=0;j<9;++j){                if(board[i][j] != '.'){                    continue;                }                for(char k = '1';k <= '9';++k){                    if(isValid(i,j,k,board)){                        board[i][j] = k;                        if(backtracking(board) == true){                            return true;                        }                        board[i][j] = '.';                    }                }                return false;            }        }        return true;    }    void solveSudoku(vector<vector<char>>& board) {        bool judge = backtracking(board);        return;    }};

分析:二维的回溯问题,说白了就是去尝试填充每一个数字,合理就填上,不合理就删掉之前填充的重新进行尝试。

错误:看题解。

Leetcode 310

给你一棵包含 n 个节点的树,标记为 0n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 aibi 之间存在一条无向边。可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。

class Solution {public:    vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {        int m = edges.size();        vector<int> result;        if(m == 0){            result.push_back(0);            return result;        }        vector<int> degree(n,0);        vector<vector<int>> tree(n);        for(int i=0;i<m;++i){            ++degree[edges[i][0]];            ++degree[edges[i][1]];            tree[edges[i][0]].push_back(edges[i][1]);            tree[edges[i][1]].push_back(edges[i][0]);        }        queue<int> q;        for(int i=0;i<n;++i){            if(degree[i] == 1){                q.push(i);            }        }        while(!q.empty()){            int size = q.size();            result.clear();            for(int i=0;i<size;++i){                int top = q.front();                result.push_back(top);                q.pop();                --degree[top];                for(int j=0;j<tree[top].size();++j){                    --degree[tree[top][j]];                    if(degree[tree[top][j]] == 1){                        q.push(tree[top][j]);                    }                }            }        }        return result;    }};

分析:拓扑排序的思想,从多端同时BFS到中心点,直到到达最后一层,输出这一层的结点即为最小的高度。

错误:看了思路后自己实现,注意判断边界条件。

总结

深度优先、广度优先和回溯法,理解的还是并不是非常深入,今后还要多加练习。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 研究生课程:现代信息检索-第0讲 课程简介 + + /2022/08/29/UCAS/information-retrieval/information-retrieval-0/ + + 《现代信息检索》课程笔记:第0讲 课程简介

第0讲 课程简介

什么是信息检索

信息检索应用例子的共同特征:

给定需求或者是对象,从信息库中找出与之最匹配的信息或对象。

数据形式是无固定结构的自由文本(谷歌搜索)或者结构化数据(京东商品)

信息检索的定义

  1. 信息检索是给定用户需求返回满足该需求信息的一门学科。通常涉及信息的获取、存储、组织和访问。
  2. 信息检索是从大规模非结构化数据(通常是文本)的集合(通常保存在计算机上)中找出满足用户信息需求的资料(通常是文档)的过程。
  3. 信息检索是“找对象”的学科,即定义并计算某种匹配“相似度”的学科。

信息检索与其他的学科关系密切,包括自然语言处理、数据挖掘和机器学习。

信息检索技术广泛应用于搜索、推荐、挖掘、舆情分析、情报处理和内容安全。

从信息规模上分类,信息检索可以分为:

  1. 个人信息检索:个人相关信息的组织、整理、搜索等,包括桌面搜索、个人信息管理、个人数字记忆等
  2. 企业级信息检索:在企业内容文档的组织、管理、搜索等。企业级信息检索是内容管理的重要组成部分。
  3. Web信息检索:在超大规模数据集上的检索。

为什么要学习信息检索

  1. 用户国家、企业、个人等需要信息检索技术:互联网的信息量太大、噪音太多,寻找所需要的信息非常不容易。互联网的不只是搜索引擎才需要信息检索技术,电子商务、社交网、数字图书馆、大规模数据分析、金融证券行业等都需要信息检索技术。
  2. 公司需要信息检索技术:搜索引擎改变了很多传统的生活方式,互联网五大盈利模式或多或少都依赖信息检索技术的支撑,目前搜索引擎公司甚至整个互联网正常运转的计算广告的核心技术是信息检索技术。
  3. 应用需求:移动搜索、产品搜索、专利搜索、广告推荐、社会网络分析、消费行为分析、网络评论分析、SEO营销

信息检索学科的特点

  1. 应用性:目标非常实际,例如提升网络搜索引擎返回结果准确率、商品推荐转化率。
  2. 经验性:理论上漂亮的方法并不一定有用,理论需要结合实践。
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + 研究生课程:现代信息检索-第1讲 布尔检索 + + /2022/08/29/UCAS/information-retrieval/information-retrieval-1/ + + 《现代信息检索》课程笔记:第1讲 布尔检索

第1讲 布尔检索

信息检索概述

现在提到信息检索,通常会首先想到Web搜索,但是除此之外还有很多其它的搜索应用,如电子邮件搜索、笔记本电脑(桌面)搜索、知识库搜索、法律文献搜索等。

本课程主要关注文本检索,因为文本检索是最早的检索应用,也仍然是目前最主要的应用,且文本检索理论可以用于其他领域。

信息检索与数据库的区别主要在于数据的区别,信息检索关注的是非结构化的数据,而数据库关注的是结构化的数据。

数据库常常支持范围或者精确匹配查询。

非结构化数据通常指自由文本,允许关键词加上操作符号的查询和更复杂的概念性查询,经典的检索模型一般都针对自由文本进行处理。

信息检索的一些基本概念

文档集(Collection): 由固定数目的文档组成

目标:返回与用户需求相关的文档并辅助用户来完成某项任务

相关性(Relevance):主观的概念,反映对象的匹配程度不同,应用相关性不同。

检索效果的评价:准确率和召回率(准确率是自己的,召回率才是真正的)

布尔检索:针对布尔查询的检索,布尔查询是指利用 ANDOR或者 NOT操作符将词项连接起来的查询。

索引方法

需求:莎士比亚的哪部剧本包含Brutus及Caesar但是不包含Calpurnia

将需求表示为布尔表达式: Brutus AND Caesar AND NOT Calpurnia

暴力索引方法

从头到尾扫描所有剧本,对每部剧本判断它是否包含Brutus AND Caesar ,同时又不包含Calpurnia

暴力方法的优点:①实现简单②很容易支持文档动态变化

暴力方法的不足:

  1. 速度超慢 (特别是大型文档集)
  2. 处理NOT Calpurnia 并不容易(不到末尾不能停止判断)
  3. 不太容易支持其他操作 (e.g., 寻找靠近countrymen的单词Romans)
  4. 不支持检索结果的灵活排序 (排序时只返回较好的结果)

倒排索引

关联矩阵:

Antony and CleopatraJulius CaesarThe TempestHamletOthelloMacbeth
Antony110001
Brutus110100
Caesar110111
Calpurnia010000
Cleopatra100000
mercy101111
worser101110

行表示单词,列表示文本,若文本中包含这个单词,则记录为1,反之记录为0

使用关联矩阵进行查询的时候,即将关联矩阵有关单词的行向量取出来后进行按位与或非操作即可。

但是这种词项-文档的关联矩阵将非常大,由于是 one-hot存储,矩阵高度稀疏,需要更好的表示方式,因此有了倒排索引。

对每个词项 t,记录所有包含 t的文档列表,每篇文档用一个唯一的 docID来表示,通常是正整数。

词典 ➡ 倒排记录(Posting)

Brutus ➡ 1 2 4 11 31 45 173

Calpurnia ➡ 1 2 4 5 6 16 57 132

Caesar ➡2 31 54 101

倒排索引的存储通常采用变长表方式

  1. 磁盘上,顺序存储方式比较好,便于快速读取
  2. 内存中,采用链表或者可变长数组方式,便于节省空间

构建倒排索引的流程

文本预处理:

  1. 词条化(Tokenization):将字符序列切分为词条
  2. 规范化(Normalization):将文档和查询中的词项映射到相同的形式
  3. 词干还原(Stemming):将同一词汇的不同形式还原到词根
  4. 停用词去除(Stopwords removal):去除高频词项

构建词条序列:<词条,docID> 类型的二元组

按词项排序:每个词项按 docID排序

某个词项在单篇文档中的多次出现会被合并

拆分成词典和倒排记录表两部分

每个词项出现的文档数目(doc.frequency, DF)会被加入

最终构成倒排索引:

v4BM1f.png

布尔查询的处理

对于布尔查询来说,对倒排记录表进行操作即可。

每个倒排记录表都有一个定位指针,两个指针同时从前往后扫描, 每次比较当前指针对应倒排记录,然后移动某个或两个指针。合并时间为两个表长之和的线性时间。时间复杂度为 O(m+n)

这也是倒排记录表按照 docID排序的关键原因!

查询处理中存在处理的顺序问题:n个词项的 AND我们希望查询的次数越少越好,因此要按照表从小到大(即 df从小到大)的顺序进行处理,每次从最小的开始合并(这样可以尽量提前结束合并)

按照直接加和的方式对 Ordf进行估计。

合并策略

每个布尔表达式都能转换成(合取范式)

获得每个词项的 df

通过将词项的 df相加,估计每个 OR表达式对应的倒排记录表的大小

按照上述估计从小到大依次处理每个 OR表达式

布尔检索的优点

构建简单,是构建信息检索系统的一种最简单方式

  • 在30多年中是最主要的检索工具
  • 当前许多搜索系统仍然使用布尔检索模型
  • 有一些扩展的布尔操作符
  • 如果非常清楚想要查什么、能得到什么,很多专业人士喜欢使用布尔搜索

布尔检索的缺点

  • 布尔查询构建复杂,不适合普通用户。构建不当,检索结果过多或者过少
  • 没有充分利用词项的频率信息。因为词通常出现的越多越好,需要利用词项在文档中的词项频率(term frequency, tf)信息
  • 不能对检索结果进行排序
]]>
+ + + + + Study + + + + + + + Postgraduate + + UCAS + + Information Retrieval + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第5章 排序算法 + + /2022/08/29/Leetcode/Leetcode-101/Leetcode-101-5/ + + Leetcode 刷题笔记-Leetcode 101 第5章 排序算法

排序算法

排序自然都有C++的STL搞定了,但是在实际中仍然需要这些排序算法,一方面夯实基础,另一方面有一些题目是从这些排序算法中引申出来的,掌握这些排序算法对于做题也会有很大的帮助。

常用排序算法

调用

int main(void){    vector<int> nums = {1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0};    vector<int> temp(nums.size());    sort(nums.begin(), nums.end());    quick_sort(nums, 0, nums.size());    print(nums);    merge_sort(nums, 0, nums.size(), temp);    print(nums);    insertion_sort(nums, nums.size());    print(nums);    bubble_sort(nums, nums.size());    print(nums);    selection_sort(nums, nums.size());    print(nums);    return 0;}

快速排序

void quick_sort(vector<int> &nums,int left,int right){    int l = left;    int r = right;    if(left+1 >= right){        return;    }    int k = nums[left];    while(left+1 < right){        while(left+1 < right && nums[right-1] >= k){            --right;        }        nums[left] = nums[right-1];        while(left+1 < right && nums[left] < k){            ++left;        }        nums[right-1] = nums[left];    }    nums[left] = k;    quick_sort(nums,l,left);    quick_sort(nums,left+1,r);}

错误:while内部的 left < right的条件没有加,导致内部会出问题,而且也是要+1的

归并排序

void merge_sort(vector<int> &nums,int left,int right,vector<int> &temp){    if(left + 1 >= right){        return;    }    int mid = (right - left) / 2 + left;    merge_sort(nums,left,mid,temp);    merge_sort(nums,mid,right,temp);    int p = left;    int q = mid;    int i = left;    while(p < mid && q < right){        if(nums[p] <= nums[q]){            temp[i++] = nums[p++];        }        else{            temp[i++] = nums[q++];        }    }    while(p < mid){        temp[i++] = nums[p++];    }    while(q < right){        temp[i++] = nums[q++];    }    for(int j=left;j<right;++j){        nums[j] = temp[j];    }}

错误:应该是 left + 1 >= right,只剩下一个数字后就应该返回了。

插入排序

void insertion_sort(vector<int> &nums,int n){    for(int i=1;i<n;i++){        int a = i;        while(a - 1 >= 0 && nums[a] < nums[a-1]){            swap(nums[a],nums[a-1]);            --a;        }    }    return;}

冒泡排序

void bubble_sort(vector<int> &nums,int n){    for(int i=0;i<n-1;i++){        for(int j=0;j<n-i-1;j++){            if(nums[j] > nums[j+1]){                swap(nums[j],nums[j+1]);            }        }    }    return;}

选择排序

void selection_sort(vector<int> &nums,int n){    for(int i=0;i<n;i++){        int minnum = nums[i];        int minindex = i;        for(int j=i+1;j<n;j++){            if(nums[j] < minnum){                minnum = nums[j];                minindex = j;            }        }        swap(nums[i],nums[minindex]);    }    return;}

快速排序

Leetcode 215

在一个未排序的数组中,找到第 k大的数字

class Solution {public:    static void quick_selection(vector<int> &nums,int left,int right,int k){        int l = left;        int r = right;        int k2 = nums[left];        if(left + 1 > right){            return;        }        while(left+1 < right){            while(left+1 < right && nums[right-1] < k2){                --right;            }            nums[left] = nums[right-1];            while(left+1 < right && nums[left] >= k2){                ++left;            }            nums[right-1] = nums[left];        }        nums[left] = k2;        if(k <= left){            quick_selection(nums,l,left,k);        }        else{            quick_selection(nums,left+1,r,k);        }        return;    }    int findKthLargest(vector<int>& nums, int k) {        quick_selection(nums,0,nums.size(),k-1);        return nums[k-1];    }};

分析:与快速排序相同的思路,但是不需要对没有用的一侧进行快速排序,只需要对k在的区间一侧进行快速排序即可。

错误:开始快速排序有问题,然后k的值想不清楚造成错误。

桶排序

Leetcode 347

class Solution {public:    static bool cmp(pair<int,int> &a,pair<int,int> &b){        return a.first > b.first;    }    vector<int> topKFrequent(vector<int>& nums, int k) {        map<int,int> mp1,mp2;        for(auto i : nums){            ++mp1[i];        }        vector<pair<int,int>> pr;        vector<int> result;        for(auto i = mp1.cbegin();i != mp1.cend();++i){            pr.push_back(make_pair(i->second,i->first));        }        sort(pr.begin(),pr.end(),cmp);        for(auto i = pr.cbegin();i != pr.cend();++i){            if(k != 0){                result.push_back(i->second);            }            else{                break;            }            k--;        }        return result;    }};

分析:也是比较简单的一道题,通过这道题可以复习一下各种 STL数据结构,总也不用生疏了。

错误:STL有一些生疏,调了一段时间才调好。

练习

Leetcode 451

给定一个字符串 s ,根据字符出现的频率对其进行降序排序。一个字符出现的频率是它出现在字符串中的次数。返回已排序的字符串

class Solution {public:    static bool cmp(pair<char,int> &a,pair<char,int> &b){        return a.second > b.second;    }    string frequencySort(string s) {        string result = "";        map<char,int> mp;        for(auto i : s){            mp[i] += 1;        }        vector<pair<char,int>> pr;        for(auto i = mp.cbegin();i != mp.cend();i++){            pr.push_back(make_pair(i->first,i->second));        }        sort(pr.begin(),pr.end(),cmp);        for(auto i = pr.cbegin();i != pr.cend();i++){            for(int j=0;j<i->second;j++){                result += i->first;            }        }        return result;    }};

分析:桶排序的变形题,没有什么新意,还是数据结构

一遍AC

Leetcode 75

void sortColors(vector<int>& nums) {    int n = nums.size();    int p0 = 0;    int p1 = 0;    for(int i=0;i<n;++i){        if(nums[i] == 1){            swap(nums[i],nums[p1]);            ++p1;        }        else if(nums[i] == 0){            swap(nums[i],nums[p0]);            if(p0 < p1){                swap(nums[i],nums[p1]);            }            ++p0;            ++p1;        }    }}

分析:荷兰国旗问题,双指针一次遍历就可以得到三个数字的排序。

错误:想复杂了。

总结

排序算法基本都可以写,就是变形的题目还是有些不太熟练。还是要多多练习。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第4章 二分查找 + + /2022/08/28/Leetcode/Leetcode-101/Leetcode-101-4/ + + Leetcode 刷题笔记-Leetcode 101 第4章 二分查找

二分查找

二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。

一点点细节小笔记

  1. 最基本的二分查找算法:

因为我们初始化 right = nums.length - 1,所以决定了我们的「搜索区间」是 [left, right],所以决定了 while (left <= right),同时也决定了 left = mid+1right = mid-1,因为我们只需找到一个 target 的索引即可,所以当 nums[mid] == target 时可以立即返回。

  1. 寻找左侧边界的二分查找:

因为我们初始化 right = nums.length,所以决定了我们的「搜索区间」是 [left, right),所以决定了 while (left < right),同时也决定了 left = mid + 1right = mid,因为我们需找到 target 的最左侧索引,所以当 nums[mid] == target 时不要立即返回,而要收紧右侧边界以锁定左侧边界。

  1. 寻找右侧边界的二分查找:

因为我们初始化 right = nums.length,所以决定了我们的「搜索区间」是 [left, right),所以决定了 while (left < right),同时也决定了 left = mid + 1right = mid,因为我们需找到 target 的最右侧索引,所以当 nums[mid] == target 时不要立即返回,而要收紧左侧边界以锁定右侧边界,又因为收紧左侧边界时必须 left = mid + 1,所以最后无论返回 left 还是 right,必须减一。

求开方

Leetcode 69

给定一个非负整数,求它的开方,向下取整。

class Solution {public:    int mySqrt(int x) {        long long left = 0;        long long right = sqrt(x) + 1;        while(left <= right){            long long mid = (right - left) / 2 + left;            if(mid * mid < x){                left = mid + 1;            }            else if(mid * mid > x){                right = mid - 1;            }            else{                return mid;            }        }        return left - 1;    }};

思路很简单,主要是细节问题,已经整理了笔记。

查找区间

Leetcode 34

给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。

class Solution {public:    vector<int> searchRange(vector<int>& nums, int target) {        vector<int> result;        int n = nums.size();        if (n == 0){            return vector<int>{-1, -1};        }        int left = 0;        int right = n;        while(left < right){            int mid = (right - left) / 2 + left;            if(nums[mid] >= target){                right = mid;            }            else if(nums[mid] < target){                left = mid + 1;            }        }        if(right >= n || nums[right] != target){            return vector<int>{-1, -1};        }        else{            result.push_back(right);        }        left = 0;        right = n;        while(left < right){            int mid = (right - left) / 2 + left;            if(nums[mid] > target){                right = mid;            }            else if(nums[mid] <= target){                left = mid + 1;            }        }        result.push_back(left - 1);        return result;    }};

分析:也是最基础的二分查找,实现了 upper_boundlower_bound两个函数。

错误:判断的时候忘记判断是否越界。

旋转数组查找数字

Leetcode 81

一个原本增序的数组被首尾相连后按某个位置断开(如[1,2,2,3,4,5] - [2,3,4,5,1,2],在第一位和第二位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个旋转数组中。

class Solution {public:    bool search(vector<int>& nums, int target) {        int n = nums.size();        int left = 0;        int right = n;        while(left < right){            int mid = (right - left) / 2 + left;            if(nums[mid] == target){                return true;            }            else if(nums[mid] < nums[right-1]){                // 说明右端是排好序的                if(target >= nums[mid] && target <= nums[right-1]){                    left = mid + 1;                }                else{                    right = mid;                }            }            else if(nums[mid] > nums[right-1]){                // 说明左端是排好序的                if(target <= nums[mid] && target >= nums[left]){                    right = mid;                }                else{                    left = mid + 1;                }            }            else{                --right;            }        }        return false;    }};

分析:旋转数组是一类经典题目,需要抓住旋转后二分会有一个区间是单调的性质进行判断,从而对所查找的数字进行区间的锁定。

错误:条件考虑不全面,没有对旋转数组充分理解。

练习

Leetcode 154

寻找旋转排序数组中的最小值

class Solution {public:    int findMin(vector<int>& nums) {        int n = nums.size();        int left = 0;        int right = n;        int minnum = 10000;        while(left < right){            int mid = (right - left) / 2 + left;            if(nums[mid] > nums[left]){                // 左边一定有序                minnum = min(minnum,nums[left]);                left = mid + 1;            }            else if(nums[mid] < nums[left]){                // 右边一定有序                minnum = min(minnum,nums[mid]);                right = mid;            }            else{                minnum = min(minnum,nums[mid]);                ++left;            }        }        return minnum;    }};

分析:比查找还要稍稍简单一点,只需要想好最小值可能出现的位置即可。

错误:相等的时候没有判断,会导致漏掉元素。

Leetcode 540

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。请你找出并返回只出现一次的那个数。

class Solution {public:    int singleNonDuplicate(vector<int>& nums) {        int n = nums.size();        int left = 0;        int right = n;        if(nums.size()==1){            return nums[0];        }        while(left < right){            int mid  = (right - left) / 2 + left;            if(mid % 2 == 0){                if(nums[mid] == nums[mid+1]){                    left = mid + 2;                }                else{                    right = mid;                }            }            else{                if(nums[mid] == nums[mid-1]){                    left = mid + 1;                }                else{                    right = mid;                }            }        }        return nums[left];    }};

分析:如果mid是偶数,则比较nums[mid]和nums[mid+1]是否相等;如果mid是奇数,则比较nums[mid−1]和nums[mid]是否相等。

错误:感觉需要判断很多条件?其实不用,只需要考虑长度为1的数组,然后根据下标寻找规律就可以。

Leetcode 4

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的中位数

分析:二分的解法太难了。。后续补充吧

错误:没有思路。。。

总结

二分查找是非常好的降低时间复杂度的方法之一,整体的思想不是很难,但是细节的部分需要多多注意。当然也有难题,还要多练习。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第3章 双指针 + + /2022/08/28/Leetcode/Leetcode-101/Leetcode-101-3/ + + Leetcode 刷题笔记-Leetcode 101 第3章 双指针

双指针

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

Two Sum

Leetcode 167

在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。

class Solution {public:    vector<int> twoSum(vector<int>& numbers, int target) {        vector<int> result;        int left = 0;        int right = numbers.size() - 1;        while(left < right){            if(numbers[left] + numbers[right] < target){                ++left;            }            else if(numbers[left] + numbers[right] > target){                --right;            }            else{                result.push_back(left+1);                result.push_back(right+1);                break;            }        }        return result;    }};

分析:左右两个指针分别进行移动,加和小了就把左边的指针往右移动一下,加和大了就把右边的指针往左移动一下。这道题比较特殊,限定了一定有答案而且答案只会有一个,因此不需要添加任何其他的额外条件。

错误:没看清下标的表示方式,直接输出数组下标了。

归并两个有序数组

Leetcode 88

给定两个有序数组,把两个数组合并为一个。

class Solution {public:    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {        if(n == 0){            return;        }        if(m == 0){            nums1 = nums2;            return;        }        int mi = m - 1;        int ni = n - 1;        int numIndex = m+n-1;        while(numIndex >= 0){            if(mi >= 0 && ni >= 0){                if(nums1[mi] > nums2[ni]){                    swap(nums1[mi],nums1[numIndex]);                    --mi;                }                else{                    swap(nums2[ni],nums1[numIndex]);                    --ni;                  }                      }            else if(mi == -1){                while(ni != -1){                    nums1[numIndex] = nums2[ni];                    --ni;                    --numIndex;                }                break;            }            --numIndex;        }    }};

分析:从后边开始安排数字,填充0的空位

错误:挺简单的一道题,首先是刚开始没有想到非常好的解法,看了答案后双指针又有一些问题。。真的是生疏了。

快慢指针

Leetcode 142

给定一个链表,如果有环路,找出环路的开始点。

class Solution {public:    ListNode *detectCycle(ListNode *head) {        ListNode* slow = head;        ListNode* fast = head;        do{            if(fast == nullptr || fast->next == nullptr){                return nullptr;            }            slow = slow->next;            fast = fast->next->next;        }while(slow != fast);        fast = head;        while(slow != fast){            slow = slow->next;            fast = fast->next;        }        return fast;    }};

分析:有一个通用的解法——快慢指针(Floyd判圈法)。给定两个指针,分别命名为slow和fast,起始位置在链表的开头。每次fast前进两步,slow前进一步。如果fast可以走到尽头,那么说明没有环路;如果fast可以无限走下去,那么说明一定有环路,且一定存在一个时刻slow 和fast 相遇。当slow和fast第一次相遇时,我们将fast重新移动到链表开头,并让slow和fast每次都前进一步。当slow和fast第二次相遇时,相遇的节点即为环路的开始点。

错误:算法忘记了,没有思路。

滑动窗口

Leetcode 76

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

class Solution {public:    string minWindow(string s, string t) {        size_t s_size = s.size();        size_t t_size = t.size();        map<char,int> mp1;        for(size_t i=0;i<t_size;i++){            if(mp1.count(t[i])){                mp1[t[i]] += 1;            }            else{                mp1[t[i]] = 1;            }        }        int left = 0,cnt = 0,min_l = 0,min_size = s_size+1;        for(int r=0;r<s_size;++r){            if(mp1.count(s[r])){                --mp1[s[r]];                if(mp1[s[r]] >= 0){                    ++cnt;                }                while(cnt == t_size){                    if(r - left + 1 < min_size){                        min_size = r - left + 1;                        min_l = left;                    }                    if(mp1.count(s[left]) && ++mp1[s[left]] > 0){                           --cnt;                    }                     ++left;                }            }        }        return min_size > s_size ? "" : s.substr(min_l,min_size);    }};

分析:滑动窗口典型题目

首先对子字符串进行计数,记录是否出现,以及出现的次数。然后采取滑动窗口的策略,两个指针都从左开始滑动,以右指针为基准构成外侧的大循环。右指针滑动的过程中,对之前的计数进行更改,滑动到了一个字符就减小1。等到0的时候,说明右指针滑动过了的字符串一定包含子字符串的全部字符,然后将左指针向右滑动来减小这个字符串的长度。左指针碰到了某个子字符串内部的字符,就会将计数+1,从而不满足这个字符串包含整个子字符串的要求,因此重新开始移动右字符串,以尝试再次包含整个子字符串。

错误:算法忘记了,没有思路。

练习

Leetcode 633

给定一个非负整数 c ,你要判断是否存在两个整数 ab,使得 a^2 + b^2 = c

class Solution {public:    bool judgeSquareSum(int c) {        long long left = 0;        long long right = sqrt(c);        while(left <= right){            if(left * left + right * right < c){                ++left;            }            else if(left * left + right * right > c){                --right;            }            else{                return true;            }        }        return false;    }};

分析:仍然是双指针的问题,多了一点点细节问题。

错误:left = right,right的范围考虑的不太好。

Leetcode 680

给你一个字符串 s最多可以从中删除一个字符。请你判断 s是否能成为回文字符串:如果能,返回 true ;否则,返回 false

class Solution {public:    bool judge(string &s,int left,int right){        while(left <= right){            if(s[left] == s[right]){                ++left;                --right;            }            else{                return false;            }        }        return true;    }    bool validPalindrome(string s) {        size_t s_size = s.size();        int left = 0;        int right = s_size - 1;        while(left <= right){            if(s[left] == s[right]){                ++left;                --right;            }            else{                return judge(s,left+1,right) || judge(s,left,right-1);            }        }        return true;    }};

分析:双指针移动就好

错误:没有考虑到删除一个字符后有两种情况,应该共同考虑而不是仅仅使用某一种情况进行判断。

Leetcode 524

给你一个字符串 s 和一个字符串数组 dictionary ,找出并返回 dictionary 中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。

class Solution {public:    static bool cmp(string &a,string &b){        if(a.size() != b.size()){            return a.size() > b.size();        }        return a < b;    }    string findLongestWord(string s, vector<string>& dictionary) {        sort(dictionary.begin(),dictionary.end(),cmp);        size_t s_size = s.size();        for(auto t : dictionary){            size_t t_size = t.size();            int si = 0,ti = 0;            while(si != s_size){                if(s[si] == t[ti]){                    ++ti;                }                ++si;                if(ti == t_size){                    return t;                }            }        }        return "";    }};

分析:先排序,然后双指针进行移动匹配,如果子字符串的指针移动到字符串的末尾了,说明已经匹配成功了,可以直接输出这个字符串。如果原始的字符串的指针移动到末尾了,说明没有匹配成功,因此转为匹配下一个字符串。

错误:题目要求的排序条件没有看好,返回了长度比较短的字符串。

Leetcode 340

给定一个字符串 s,找出至多包含 k个不同字符的最长子串 T

分析:还是滑动窗口的策略,以右边指针为基准,滑动一下就记录一下最长的长度,滑动到不满足条件了,就将左边的指针收回来,收到满足条件了就继续放右边的指针去滑动。

class Solution {public:    int lengthOfLongestSubstringKDistinct(string s, int k) {        size_t s_size - s.size();        map<char,int> mp;        int maxlen = 0;        int l = 0;        for(int r=0;r<s_size;r++){            if(mp.size() <= k){                ++mp[s[r]];            }            while(mp.size() > k){                if(--mp[s[l]] == 0){                    mp.erase(s[l]);                }                l++;            }            maxlen = max(maxlen,r-l+1);        }        return maxlen;    }};

错误:会员题,无法提交。

总结

双指针的题目还可以,感觉重要的是判断条件。滑动窗口的题目比较困难,可能也是做的题目比较少。后面还需要加强练习。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + Leetcode 刷题笔记-Leetcode 101 第2章 贪心算法 + + /2022/08/27/Leetcode/Leetcode-101/Leetcode-101-2/ + + Leetcode 刷题笔记-Leetcode 101 第2章 贪心算法

贪心算法

贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。

分配问题

Leetcode 455

有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子可以吃饱。

class Solution {public:    int findContentChildren(vector<int>& g, vector<int>& s) {        sort(g.begin(),g.end());        sort(s.begin(),s.end());        int childrenCount = 0;        for(size_t i = 0;i<s.size() && childrenCount < g.size();i++){            if(s[i] >= g[childrenCount]){                ++childrenCount;            }        }        return childrenCount;    }};

分析:用最小大小的饼干 (s)去满足最小饥饿度的孩子 (g),一直满足到饥饿度最大的孩子,相当于双指针的移动。

贪心策略是给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。

错误:忘记检查g是否越界,可能发生所有饼干都能满足所有孩子,然而饼干还剩着的情况。下标运算一定要确认是否越界。

Leetcode 135

一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比自己身旁的一个孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少个糖果。

class Solution {public:    int candy(vector<int>& ratings) {        vector<int> candyNum(ratings.size(),1);        for(size_t i = 0;i<ratings.size()-1;++i){            if(ratings[i+1] > ratings[i] && candyNum[i+1] <= candyNum[i]){                candyNum[i+1] = candyNum[i] + 1;            }        }        for(size_t i = ratings.size()-1;i>0;--i){            if(ratings[i-1] > ratings[i] && candyNum[i-1] <= candyNum[i]){                candyNum[i-1] = candyNum[i] + 1;            }        }        return accumulate(candyNum.cbegin(),candyNum.cend(),0);    }};

分析:首先至少有一个糖果分配好,然后从左向右扫一遍,如果右边的孩子评分高,则右边孩子的糖果=左边孩子的糖果+1,再从右往左扫一遍,如果左边的孩子评分高,则左边孩子的糖果=右边孩子的糖果+1。最后求和即可。

贪心策略:在每次遍历中,只考虑并更新相邻一侧的大小关系

错误:没有更新为相邻孩子+1,而是仅仅加了1,考虑不够完整。

区间问题

Leetcode 435

给定一个区间的集合 intervals ,返回 需要移除区间的最小数量,使剩余区间互不重叠。

class Solution {public:    static bool cmp(vector<int> &a,vector<int> &b){        return a[1] < b[1];    }    int eraseOverlapIntervals(vector<vector<int>>& intervals) {        sort(intervals.begin(),intervals.end(),cmp);        int intervalsCount = 1;        int searchBack = intervals[0][1];        size_t n = intervals.size();        for(size_t i=0;i<n;++i){            if (intervals[i][0] >= searchBack){                intervalsCount += 1;                searchBack = intervals[i][1];            }         }        return n - intervalsCount;    }};

分析:假设第一个区间是 kk的左边没有任何区间,因此使用其他任何一个区间,只要右端点小于 k的右端点就可以了。而且右端点向左移动,比 k更优。因此首个区间就是所有可以选择的区间中右端点最小的那个区间 。后面只要去寻找其中与首个区间不重合并且右端点最小的区间即可。

贪心策略:优先保留结尾小且不相交的区间

错误1:没想明白右端点的问题

错误2:函数要加 static(但是不太明白)

错误3:使用引用传参,防止拷贝浪费时间

建议:一些比如数组大小的数字提前计算出来,避免反复计算。

练习

Leetcode 605

有一个很长的花坛,一部分地块种植了花,另一部分却没有。花不能种植在相邻的地块上。 flowerbed 表示花坛,由若干 01 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false

class Solution {public:    bool canPlaceFlowers(vector<int>& flowerbed, int n) {        int flowerCount = 0;        size_t m = flowerbed.size();        if(m == 1){            if(flowerbed[0] == 0){                return true;            }            else if (flowerbed[0] == 1 && n == 1){                return false;            }        }        if(flowerbed[0] == 0 && flowerbed[1] == 0){            flowerbed[0] = 1;            flowerCount += 1;        }        if(flowerbed[m-1] == 0 && flowerbed[m-2] == 0){            flowerbed[m-1] = 1;            flowerCount += 1;        }        for(size_t i=1;i<m-1;i++){            if(flowerbed[i] == 0 && flowerbed[i-1] == 0 && flowerbed[i+1] == 0){                flowerbed[i] = 1;                flowerCount += 1;            }        }        return flowerCount >= n;    }};

分析:遍历即可,尤其注意开头部分和结尾部分。

错误:最后没有考虑等于条件也为 true

建议:判断太多,有更为简洁的解法,大致思路是计算相邻的 1之间能种多少个 0

Leetcode 452

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,不知道具体位置,但是知道一个位置的范围。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,射进了气球的位置范围后,该气球就会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。给你一个数组 points , 返回引爆所有气球所必须射出的最小弓箭数 。

class Solution {public:    static bool cmp(vector<int> &a,vector<int> &b){        return a[1] < b[1];    }    int findMinArrowShots(vector<vector<int>>& points) {        sort(points.begin(),points.end(),cmp);        int n = points.size();        int arrowCount = 1;        int endPoint = points[0][1];        for(size_t i=0;i<n;i++){            if(points[i][0] > endPoint){                ++arrowCount;                endPoint = points[i][1];            }        }        return arrowCount;    }};

分析:拿第一个气球来说,要是想射爆,最佳的方法就是射最右侧的位置,这样能射到的其他的气球数量也会增加,以此类推,构成贪心算法。

一遍AC

Leetcode 763

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

class Solution {public:    vector<int> partitionLabels(string s) {        vector<int> last(26,-1);        for(int i = s.size() - 1;i >= 0;i--){            if(last[s[i] - 'a'] == -1){                last[s[i] - 'a'] = i;            }        }        vector<int> result;        int start = 0;        int end = 0;        for(int i=0;i<s.size();i++){            end = max(end,last[s[i] - 'a']);            if(i == end){                result.push_back(end - start + 1);                start = i + 1;            }        }        return result;    }};

分析:首先得到字符出现的最后的下标位置,然后重新遍历字符串,得到每个字符最后出现的位置。一旦前面的所有字符都出现完了,就算一个区间。

上述做法使用贪心的思想寻找每个片段可能的最小结束下标,因此可以保证每个片段的长度一定是符合要求的最短长度,如果取更短的片段,则一定会出现同一个字母出现在多个片段中的情况。由于每次取的片段都是符合要求的最短的片段,因此得到的片段数也是最多的。

错误:思路有问题,没有做对

Leetcode 122

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。返回你能获得的最大利润 。

class Solution {public:    int maxProfit(vector<int>& prices) {        int stockSum = 0;        for(size_t i=0;i<prices.size()-1;i++){            if(prices[i+1] > prices[i]){                stockSum = stockSum+prices[i+1]-prices[i];            }        }        return stockSum;    }};

分析:什么都不限制,涨了就卖就完事了,比较简单。贪心策略就是只要价格上涨就直接出售。

一遍AC

Leetcode 406

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面正好ki个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

class Solution {public:    static bool cmp(vector<int> &a,vector<int> &b){        if(a[0] != b[0]){            return a[0] < b[0];        }        return a[1] > b[1];    }    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {        vector<vector<int>> result(people.size());        sort(people.begin(),people.end(),cmp);        for(size_t i=0;i<people.size();++i){            int pos = people[i][1];            for(size_t j=0;j<result.size();j++){                if(result[j].empty()){                    pos--;                }                if(pos == -1){                    result[j] = people[i];                    break;                }            }        }        return result;    }};

分析:将人员从低往高先排列,然后一个个进行插入。插入的人只会对后面的人有影响,因为后面的人的身高都会大于等于他。而对已经插入的人没有影响。因此插入的时候给后面的人要留出空位置,以便后面的人插入进去。如果身高相同,就去比较 kiki更小一点的,说明这个人在靠前一点,也就是最小的 ki前面是不会有相同身高的人的,由于相同身高也会算在内,因此要先插入大 ki

错误:思路有问题,没有做对

Leetcode 665

给你一个长度为 n 的整数数组 nums ,请你判断在最多改变 1 个元素的情况下,该数组能否变成一个非递减数列。

class Solution {public:    bool checkPossibility(vector<int>& nums) {        int count = 0;        for (int i = 1; i < nums.size(); i++) {            if (nums[i] < nums[i - 1]) {                if(i == 1 || nums[i] >= nums[i - 2]){                    nums[i - 1] = nums[i];                }                else{                    nums[i] = nums[i - 1];                }                ++count;            }        }        return count <= 1;    }};

分析:要多种情况一起考虑。。。。

错误:思路有问题,没有做对。另外不要去改 i+1啊。。判断什么修改什么好吧,要不就乱套了。

总结

贪心算法确实是比较好理解的,但是怎么贪心?什么时候贪心?这些问题都要去详细认真的思考,真正出题的时候不会如此直白,要多练多想。

]]>
+ + + + + Study + + + + + + + Algorithm + + C++ + + Leetcode + + + +
+ + + + + 杂谈-20220826 + + /2022/08/26/diary/diary20220826/ + + 开学第一周,或者算是第二周,开学但是没有任何课程,也没有什么活动,开了一次班会,然后一个学院的开学典礼。新认识的人呢,也就一个室友+之前室友的同学,也就寥寥几人,同一个套间住着的人几乎都不认识。这几天就一直有些不太舒服,写些文字简单发泄一下。

总的来说,这里确实是一群学霸。首先可以拿我室友来说,早上7点起床,晚上11点左右睡觉,几乎每时每刻都在看论文做实验,甚至在看比赛的过程中间也会去看论文。他的目标就是要发文章,发一篇顶会文章,因此现在在努力完成这个目标。之后的方向他还没有想好,可能出国或者找音频算法相关的工作。其次是图书馆的同学们,才开学没有几天,图书馆就已经爆满了。大家都是思维缜密且有计划的人,昨天一窝蜂去抢机房,抢各种台式电脑去选课,选过课后去找相关的书籍,这在之前都是我的标准操作,在这里却被其他人不断模仿甚至比我做的更好。我有一种压力感,同时也有一种恐惧。

我的内心真的很脆弱。感觉其他人都还很适应的,我表面上也是这样,但是内心里已经稍稍有点崩溃了。我不禁回想我本科阶段,如果我高考真的考的好了,去了一些顶级985的学校,那么我是会坚持住学下去拼下去,还是会基本上崩溃掉,完全没有任何的竞争实力了呢?或许去了中南大学,并不是考的不好,而是帮我减轻了同龄人的压力。现在研究生的阶段,我是真真正正感受到了同龄人的压力。这么多优秀的人当中,我又能排到一个什么水平?如果真的在各个方面都比不上别人,我会不会崩溃呢?这些都是我现在所担心的。

其实换个角度来想,我没有必要去和任何人去比较。大家的人生道路都是不一样的,也无所谓好与不好,只是适不适合,以及过的是否开心罢了。对于我现在来说,虽然我知道不要去和其他人比较,总有人比你更强,比你过的更好。但是我还是时不时会看看想想别人现在在做什么,看看别人取得的成就,想想自己有没有可能赶得上甚至超过。这样就造成了现在每一天都非常不开心,学习也没有什么动力,学到后面甚至有一点混时间的感觉。这种想法困扰了我很长的一段时间,目前仍然在困扰着我。

我现在能做的,就是找准自己的目标,制定好计划,坚定不移地实施下去。至于我脆弱的内心,慢慢调解吧。没有人能帮助我,最后能靠得住的只有我自己。

]]>
+ + + + + Life + + + + + + + Diary + + + +
+ + + + + C++ Primer - 第二部分 C++标准库 + + /2022/08/21/c-plus-stl/ + + C++ Primer - 第二部分 C++标准库

第8章 IO库

IO类

iostream定义了用于读写流的基本类型

fstream定义了读写命名文件的类型

sstream定义了读写内存 string对象的类型

用法都是完全相同的,得益于继承机制

流的状态:

auto old_state = cin.rdstate(); // 获取流的当前状态cin.clear(); // 将所有条件状态复位,将流的状态置为有效cin.setstate(old_state); // 根据给定的标志位对流进行复位

输出缓冲:每个输出流都管理一个缓冲区,保存程序读写的数据。

控制输出缓冲:

cout << "hi!" << endl; // 多输出一个换行符,然后刷新缓冲区cout << "hi!" << flush; // 输出后直接刷新缓冲区cout << "hi!" << ends; // 多输出一个空字符,然后刷新缓冲区cout << unitbuf; // 所有输出操作后都立即刷新缓冲区cout << nounitbuf; // 回到正常的缓冲方式

关联输入和输出流:如果某一个输入流和输出流关联,则从输入流读取的操作会对这个输出流进行刷新。

标准库将 coutcin关联在一起

cin.tie(&cerr); // 将cin和cerr关联在一起

文件输入输出

ifstream in("infile");ofstream output("outfile");string s;while(getline(in,s)){    output << s << endl;}return 0;

显式打开或者关闭文件流:

ofstream output; // 空文件流对象output.open("outfile"); // 调用open进行关联output.close(); // 关闭文件流

文件模式:

ofstream output("outfile");

这种方式其实隐含了以输出模式打开文件并进行截断,显式控制如下:

ofstream output("outfile",ofstream::out | ofstream::trunc);

为了保留之前的文件内容,需要显式指定 app模式

ofstream output("outfile",ofstream::out | ofstream::app);

string流

ifstream in("infile");ofstream output("outfile",ofstream::out | ofstream::app);string s;while(getline(in,s)){    string a,b,c;    istringstream s1(s);    ostringstream s2;    s1 >> a;    s2 << a;    s1 >> b;    s2 << b;    s1 >> c;    s2 << c;    cout << s2.str() << endl;}return 0;

第9章 顺序容器

一个容器就是一些特定类型对象的集合,顺序容器为程序员提供了控制元素存储和访问顺序的能力。

顺序容器种类

vector是可变大小数组,支持快速随机访问。但是在尾部之外的位置插入或者删除元素可能很慢。

deque是双端队列,支持快速随机访问,在头尾部插入或者删除元素的速度很快。

list是双向链表,只支持双向顺序访问,在 list中任意位置进行插入/删除的速度都很快

forward_list是单向链表,只支持单向顺序访问,在链表中任意位置进行插入/删除的速度都很快

array是固定大小的数组,支持快速随机访问,不能添加或者删除元素

string是与 vector相似的容器,专门用于保存字符,随机访问快,在尾部插入/删除的速度很快

顺序容器几乎可以保存任意类型的元素

各种迭代器:

auto it1 = a.begin(); // list<string>::iteratorauto it2 = a.rbegin(); // list<string>::reverse_iteratorauto it3 = a.cbegin(); // list<string>::const_iteratorauto it4 = a.crbegin(); // list<string>::const_reverse_iterator

元素的拷贝初始化:

list<string> a = {"Milton","SHakespeare","Austen"};list<string> a2(a);

array具有固定的大小,并且可以进行拷贝

array<int,10> ia1 = {0,1,2,3,4,5,6,7,8,9};array<int,10> ia2 = ia1;

使用 assign对不同但相容的类型进行赋值

list<string> names;vector<const char*> oldstyle;names.assign(oldstyle.cbegin(),oldstyle.cend());

添加元素三种方法:push_front()insert()push_back()

新标准对应了三种直接构造元素的方法:emplace_front()emplace()emplace_back()

更安全的访问元素的方法:svec.at(0)

改变容器大小并使用某个元素填充更大的部分:ilist.resize(15,-1)

管理容量的成员函数:

c.capacity(); // 不重新分配内存空间的话最多能保存多少元素c.reserve(n); // 分配至少能容纳n个元素的内存空间c.shrink_to_fit() // 请求将capacity()减小为size()一样的大小

数值转换:

int i = 42;string s = to_string(i);double d = stod(s);cout << s << " " << d << endl;
42 42

第10章 泛型算法

对于容器的其他操作,并没有通过定义成员函数的方式实现,而是定义一套泛型算法,实现了一些算法的公共接口。

在容器中对值进行查找使用 find,返回查找元素的指针的位置

auto result = find(vec.cbegin(),vec.cend(),val)

返回元素在容器中出现的次数:

vector<int> a;int temp;for(int i=0;i<10;i++){    cin >> temp;    a.push_back(temp);}cin >> temp;auto result = count(a.cbegin(),a.cend(),temp);cout << result << endl;

泛型算法本身不会执行容器的操作,只会运行于迭代器之上,执行迭代器的操作。

因此泛型算法永远不会改变底层容器的大小。

各种泛型算法

元素求和:

vector<int> a{1,2,3,4,5,6,7,8};int sum = accumulate(a.cbegin(),a.cend(),0);cout << sum << endl;
36

可以推广到字符串中用来连接字符串:

vector<int> a{1,2,3,4,5,6,7,8};vector<string> b{"df","fsfds","rte"};string sum = accumulate(b.cbegin(),b.cend(),string(""));cout << sum << endl;
dffsfdsrte

确定两个序列中保存的值是否相同(假定第二个序列至少与第一个序列一样长)

vector<int> a{1,2,3,4,5,6,7,8};vector<string> b{"df","fsfds","rte"};vector<string> c{"df","fsfds","rte","fdsf"};auto sum = equal(b.cbegin(),b.cend(),c.cbegin());cout << sum << endl;
1

使用 fillfill_n填充元素:

vector<int> a{1,2,3,4,5,6,7,8};fill(a.begin(),a.end(),0);for(auto i : a){    cout << i << " ";}cout << endl;fill_n(a.begin(),a.size(),1);for(auto i : a){    cout << i << " ";}cout << endl;
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1

算法是不会检查写操作的,泛型算法也不能更改容器的大小。因此需要自行检查容器是否越界等问题。

安全的方式:插入迭代器

vector<int> a{1,2,3,4,5,6,7,8};vector<string> b{"df","fsfds","rte"};vector<string> c{"df","fsfds","rte","fdsf"};fill_n(back_inserter(a),10,1);for(auto i : a){    cout << i << " ";}cout << endl;
1 2 3 4 5 6 7 8 1 1 1 1 1 1 1 1 1 1

a1的内容拷贝到 a2copy(begin(a1),end(a1),a2)

对元素进行排序去重:

vector<string> d{"the","quick","red","fox","jumps","over","the","slow","red","turtle"};sort(d.begin(),d.end()); // 排序auto end_unique = unique(d.begin(),d.end()); // 将重复的移到末尾,并同时返回最后一个不重复的元素的后一位置d.erase(end_unique,d.end()); // 使用容器操作删除重复的元素(因为泛型算法无法改变容器大小)for(auto i : d){    cout << i << " ";}cout << endl;
fox jumps over quick red slow the turtle

lambda表达式:

一个 lambda表达式表示一个可调用的代码单元,可以理解为一个未命名的内联函数。

auto f = []{return 42;};cout << f() << endl;
42
int a = 0, b = 1;auto f6 = [&a, &b]{ return a + b; };cout << f6() << endl;
1

lambda表达式还有其他的一些用法。

其他迭代器

插入迭代器:

back_inserter:创建一个使用 push_back的迭代器

front_inserter:创建一个使用 push_front的迭代器

inserter:创建一个使用 insert的迭代器

list<int> lst = {1,2,3,4};list<int> lst2 = {5,6};list<int> lst3 = {9,10,11};list<int> lst4 = {12};copy(lst.cbegin(),lst.cend(),front_inserter(lst2));for(auto i : lst2){    cout << i << " ";}cout << endl;copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin()));for(auto i : lst3){    cout << i << " ";}cout << endl;copy(lst.cbegin(),lst.cend(),back_inserter(lst4));for(auto i : lst4){    cout << i << " ";}cout << endl;
4 3 2 1 5 6 1 2 3 4 9 10 11 12 1 2 3 4

流迭代器:

istream_iterator<int> in(cin),eof;cout << accumulate(in,eof,0) << endl;
> 2 1 4 5 6 7 8 942
ostream_iterator<int> out(cout," ");for(int i=0;i<10;i++){    out = i;}cout << endl;
0 1 2 3 4 5 6 7 8 9

反向迭代器:

vector<int> vi{1,2,3,4,5,6,7,8,9,10};for(auto i = vi.crbegin();i != vi.crend();i++){    cout << *i << " ";}cout << endl;
10 9 8 7 6 5 4 3 2 1

要注意反向迭代器真的是反的。。。比如下面的例子:

string line = "first,middle,end";auto rcomma = find(line.crbegin(),line.crend(),',');cout << string(line.crbegin(),rcomma) << endl;cout << string(rcomma.base(),line.cend()) << endl;
dneend

第11章 关联容器

关联容器中的元素是按关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。

map是关键字-值对的结合

map<string,size_t> word_count;string word;while(cin >> word){    ++word_count[word];}for(const auto &w : word_count){    cout << w.first << " " << w.second << endl;}
> a b c d e d b ca 1b 2c 2d 2e 1

关联容器的元素都是根据关键字存储的,因此不支持位置相关的操作。

multimapmultiset允许相同关键字:

vector<int> vi{1,2,3,4,5,5,4,3,2,1};set<int> iset(vi.cbegin(),vi.cend());multiset<int> miset(vi.cbegin(),vi.cend());cout << iset.size() << " " << miset.size() << endl;

关联容器的迭代器:

vector<int> vi{1,2,3,4,5,5,4,3,2,1};set<int> iset(vi.cbegin(),vi.cend());for(auto set_it = iset.cbegin();set_it != iset.cend();set_it++){    cout << *set_it << " ";}cout << endl;
1 2 3 4 5

插入元素:

iset.insert(8);

查找元素的下标操作:

c[k]; // 如果没有会添加,并对值进行初始化c.at(k); // 如果没有会抛出异常

访问元素:findcount

multimap中查找元素:

multimap<string,int> mi{make_pair("as",1),make_pair("as",2),make_pair("ab",2),make_pair("ac",2),make_pair("ac",5)};for(auto pos = mi.equal_range("as");pos.first != pos.second;++pos.first){    cout << pos.first->second << " ";}cout << endl;

根据转换规则对文件内容进行转换:

转换规则:

brb be right backk okay?y whyr areu youpic picturethk thanks!l8r later

文件内容:

where r uy dont u send me a pic k thk l8r

转换代码:

// 实际的转换工作,生成转换文本const string & transform(const string &s, const map<string,string> &m){    auto map_it = m.find(s);    if (map_it != m.cend()){        return map_it->second;    }    else{        return s;    }}// 读入给定文件,建立转换映射map<string,string> buildMap(ifstream &map_file){    map<string,string> trans_map;    string key,value;    // 读取第一个单词存入key,剩余内容存入value    while(map_file >> key && getline(map_file,value)){        if(value.size() > 1){            trans_map[key] = value.substr(1);        }        else{            throw runtime_error("No rule for " + key);        }    }    return trans_map;}int main(void){    ifstream map_file("rules");    ifstream input("text");    auto trans_map = buildMap(map_file); // 保存转换规则    string text; // 保存输入中的每一行    while(getline(input,text)){        istringstream stream(text); // 读取每个单词        string word;        bool firstword = true; // 控制是否打印空格        while(stream >> word){            if(firstword){                firstword = false;            }            else{                cout << " ";            }            cout << transform(word,trans_map);        }        cout << endl;    }    return 0;}

输出:

where are youwhy dont you send me a pictureokay? thanks! later

无序容器:不适用比较运算符来组织元素,而是使用哈希函数组织元素。

一般情况下的性能要比有序容器更好,但是不能按照顺序输出。

第12章 动态内存

动态内存与智能指针

前面都是静态对象,由程序自动分配内存并销毁。而动态对象需要被显式进行释放。

动态内存需要显式进行分配和释放,因此很容易忘记释放导致一些问题。因此定义了两种智能指针来管理这些动态对象,自动进行释放。

shared_ptr<string> p1; // 指向string的shared_ptrshared_ptr<list<int>> p2; // 指向int的list的shared_ptr

默认初始化的智能指针中保存着一个空指针。

最安全的分配和使用动态内存的方式是调用 make_shared的标准库函数。

shared_ptr<int> p3 = make_shared<int>(42);shared_ptr<string> p4 = make_shared<string>(10,'9');shared_ptr<int> p5 = make_shared<int>();

shared_ptr会自动记录有多少个其他 shared_ptr指向相同的对象,如果没有了,会自动销毁所管理的对象并自动释放相关联的内存。

离开作用域也会被销毁。如果返回这个指针,也不会被销毁(就是挺智能的)

直接管理内存:使用 newdelete

int *pi = new int;string *ps = new string(10,'9');const string *pcs = new const string;delete pi;delete ps;delete pcs;

delete不会抛出任何异常,尽管可能已经释放过了,甚至有可能都不是指针也会释放,会造成一些问题。

delete还可能会造成空悬指针,因此这个 delete只提供了有限的保护。

不要混用智能指针和普通指针,不要使用 get初始化另一个智能指针或者赋值。

unique_ptr“拥有”它所指向的对象,某个时刻只能由一个 unique_ptr指向一个给定对象。销毁指针就一起销毁了。

weak_ptr指向一个 shared_ptr管理的对象,不会改变 shared_ptr的计数,计数为 0后会自动释放。

动态数组

使用 new分配一个 int数组:

int *p = new int[42];

实际上并没有得到一个数组类型的对象,而是得到一个数据元素类型的指针。

动态分配并初始化数组:

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};

动态分配的数组的大小可以为0,会返回一个类似于尾后迭代器的指针。

释放动态数组:delete [] p

智能指针管理动态数组:

unique_ptr<int[]> up(new int[10]);for(size_t i = 0;i != 10;++i){    up[i] = i;}up.release();

allocator将内存分配和对象构造分离开来,提供一种类型感知的内存分配方法。

int n = 5;allocator<string> alloc;auto const p = alloc.allocate(n);

这个 allocator为5个 string分配了内存

在内存中构造对象:

auto q = p;alloc.construct(q++,"hi");cout << *p << endl;
hi

案例:文本查询程序

在一个给定文件中查询单词,最终返回单词在文件中出现的次数及其所在行的列表。

]]>
+ + + + + Study + + + + + + + C++ + + + +
+ + + + + C++ Primer - 第一部分 C++基础 + + /2022/08/09/c-plus-basic/ + + C++ Primer 阅读笔记 - 第一部分 C++基础

开始学习

大一学过C语言,当时学的不是很好,但是后面接触到算法竞赛的时候就慢慢补上来了,而且增加了一些C++特性以及STL标准模板库,也靠着半吊子C++拿了一些小奖,但是确实没有系统的学过C++。总之听说C++比较难,这次准备半系统性的学习一下。之前会的东西就做做题简单过一下,不会的重点看,尤其是指针和面向对象方面。希望以后能更加得心应手地使用C++,也为后面求职打打基础。

第1章 开始

注释

std::cout << "/*";std::cout << "*/";std::cout << /*  "*/" *.;std::cout << /* "*/" /* "/*" */;

前两行没问题,注释只有一边,编译运行顺利通过

第三行注释全,但是字符串不全,缺少右边的",编译运行不能通过

第四行两边分别有两组注释,且中间的字符串是全的,因此编译运行顺利通过

读取数量不定的输入

int sum = 0,value = 0;while(std::cin >> value){    sum += value;}std::cout << sum << std::endl;

读取数量不定的整数,将其加和。

std::cin属于一种 istream对象,将其作为条件时是检测流的状态,遇到文件结束符或者无效输入时会变为无效,从而退出循环。

在Ubuntu中输入 Ctrl+D来输入一个文件结束符。

类简介

定义好的头文件:

#ifndef SALESITEM_H// we're here only if SALESITEM_H has not yet been defined #define SALESITEM_H// Definition of Sales_item class and related functions goes here#include <iostream>#include <string>class Sales_item {// these declarations are explained section 7.2.1, p. 270 // and in chapter 14, pages 557, 558, 561friend std::istream& operator>>(std::istream&, Sales_item&);friend std::ostream& operator<<(std::ostream&, const Sales_item&);friend bool operator<(const Sales_item&, const Sales_item&);friend bool operator==(const Sales_item&, const Sales_item&);public:    // constructors are explained in section 7.1.4, pages 262 - 265    // default constructor needed to initialize members of built-in type    Sales_item() = default;    Sales_item(const std::string &book): bookNo(book) { }    Sales_item(std::istream &is) { is >> *this; }public:    // operations on Sales_item objects    // member binary operator: left-hand operand bound to implicit this pointer    Sales_item& operator+=(const Sales_item&);      // operations on Sales_item objects    std::string isbn() const { return bookNo; }    double avg_price() const;// private members as beforeprivate:    std::string bookNo;      // implicitly initialized to the empty string    unsigned units_sold = 0; // explicitly initialized    double revenue = 0.0;};// used in chapter 10inlinebool compareIsbn(const Sales_item &lhs, const Sales_item &rhs) { return lhs.isbn() == rhs.isbn(); }// nonmember binary operator: must declare a parameter for each operandSales_item operator+(const Sales_item&, const Sales_item&);inline bool operator==(const Sales_item &lhs, const Sales_item &rhs){    // must be made a friend of Sales_item    return lhs.units_sold == rhs.units_sold &&           lhs.revenue == rhs.revenue &&           lhs.isbn() == rhs.isbn();}inline bool operator!=(const Sales_item &lhs, const Sales_item &rhs){    return !(lhs == rhs); // != defined in terms of operator==}// assumes that both objects refer to the same ISBNSales_item& Sales_item::operator+=(const Sales_item& rhs) {    units_sold += rhs.units_sold;     revenue += rhs.revenue;     return *this;}// assumes that both objects refer to the same ISBNSales_item operator+(const Sales_item& lhs, const Sales_item& rhs) {    Sales_item ret(lhs);  // copy (|lhs|) into a local object that we'll return    ret += rhs;           // add in the contents of (|rhs|)     return ret;           // return (|ret|) by value}std::istream& operator>>(std::istream& in, Sales_item& s){    double price;    in >> s.bookNo >> s.units_sold >> price;    // check that the inputs succeeded    if (in)        s.revenue = s.units_sold * price;    else         s = Sales_item();  // input failed: reset object to default state    return in;}std::ostream& operator<<(std::ostream& out, const Sales_item& s){    out << s.isbn() << " " << s.units_sold << " "        << s.revenue << " " << s.avg_price();    return out;}double Sales_item::avg_price() const{    if (units_sold)         return revenue/units_sold;     else         return 0;}#endif

暂时不用怎么管,先试着使用:

  1. 读取单价和数量,输出总价格
Sales_item book; // 创建一个对象std::cin >> book;std::cout << book << std::endl;return 0;
0-201-70353-x 4 24.99> 0-201-70353-x 4 99.96 24.99
  1. 对象相加,输出总价格和平均价格
Sales_item book1,book2; // 创建一个对象std::cin >> book1 >> book2;std::cout << book1+book2 << std::endl;return 0;
0-201-70353-x 3 20.000-201-70353-x 2 25.00> 0-201-70353-x 5 110 22
  1. 增加成员函数,加和之前先判断两书的序列号是否相等
Sales_item book1,book2; // 创建一个对象std::cin >> book1 >> book2;if(book1.isbn() == book2.isbn()){    std::cout << book1+book2 << std::endl;}else{    std::cerr << "Error!" << std::endl;}return 0;
0-201-70353-x 3 20.000-201-70343-x 2 25.00> Error!
  1. 读取销售记录,生成每本书的销售报告
#include <bits/stdc++.h>#include "Sales_item.h"int main(void){    Sales_item total;    if(std::cin >> total){ // 读取第一条数据,确保有数据可以处理        Sales_item trans;        while(std::cin >> trans){            if(total.isbn() == trans.isbn()){                total += trans;            }            else{                std::cout << total << std::endl;                total = trans;            }        }        std::cout << total << std::endl; // 打印最后一本书    }    else{        std::cerr << "No data!" << std::endl;    }    return 0;}
0-201-70353-X 4 24.990-201-82470-1 4 45.390-201-88954-4 2 15.00 0-201-88954-4 5 12.00 0-201-88954-4 7 12.00 0-201-88954-4 2 12.00 0-399-82477-1 2 45.390-399-82477-1 3 45.390-201-78345-X 3 20.000-201-78345-X 2 25.00> 0-201-70353-X 4 99.96 24.990-201-82470-1 4 181.56 45.390-201-88954-4 16 198 12.3750-399-82477-1 5 226.95 45.390-201-78345-X 5 110 22

这个程序的局限性在于,必须是连号的输入,不连号的输入就失效了。

当然这个时候学到的还不多,后面会将这个程序继续完善。

第2章 变量和基本类型

整型可以分为带符号类型和无符号类型(在前面添加 unsigned

选择类型的原则:

  1. 明确知道不可能为负值时,选用无符号类型
  2. 整数运算使用int,超过范围了使用long long
  3. 浮点数运算使用double
  4. 不要在算术表达式中使用char或者bool
  5. 不要混用无符号类型和带符号类型,因为带符号类型会自动转换为无符号类型,运算过程中出现负值即错误

初始化

创建变量时赋予其一个初始值(赋值指的是将对象的当前值用一个新值来替代,含义不同)

初始化的4种方式:

  1. int a = 0;
  2. int a = {0};
  3. int a{0}; // 列表初始化
  4. int a(0);

变量声明:“一个文件如果想使用别处定义的名字,必须包含对那个名字的声明”

与定义的区别在于不赋初值

extern int i;

作用域

  1. 作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字
  2. 允许在内层作用域中重新定义外层作用域已有的名字

如:

int a = 0;int main(void){    std::cout << a << std::endl;    int a = 1;    std::cout << a << std::endl;    std::cout << ::a << std::endl; // 显式指定访问全局变量    return 0;}
010

引用

相当于为对象起一个另外的名字,通过 &符号来定义

int ival = 1024;int &refVal = ival;

引用必须初始化,因为引用需要和它的初始化对象一起绑定在一起,不能重新绑定到其他对象。

定义引用之后,对其进行的所有操作都是在它的绑定对象上进行的

refVal = 12;std::cout << refVal << std::endl;
12

引用本身不是一个对象,不能定义引用的引用

如下面的方式,实际上是绑定到了该引用对应的绑定对象上:

int &refVal2 = refVal;std::cout << refVal2 << std::endl;
12

引用的类型要与绑定的对象严格匹配

引用不能绑定到字面值上

指针

指针也实现了对其他对象的间接访问,但是指针本身也是一个对象,通过 *符号来定义

  1. 指针存放某个对象的地址,如果获取这个地址,需要使用取地址符 &
int ival = 42;int *p = &ival;

指针的类型也要与它所指向的对象严格匹配

  1. 如果指针指向了一个对象,可以使用解引用符 *来访问这个对象
int ival = 42;int *p = &ival;std::cout << *p << std::endl;
42

符号的多重含义:

int i = 42;int &r = i; // &随类型名出现,是声明的一部分,r是一个引用int *p; // *随类型名出现,是声明的一部分,p是一个指针p = &i; // &出现在表达式中,是一个取地址符*p = i; // *出现在表达式中,是一个解引用符int &r2 = *p; // r2是一个引用,*是一个解引用符std::cout << i << std::endl << r << std::endl << *p << std::endl << r2 << std::endl;
42424242

空指针:int *p1 = nullptr

建议:初始化所有的指针

指针与引用不同,是可以赋值的。赋值的时候永远改变的是等号左侧的对象。

void*指针,可以用于存放任意类型对象的地址

double obj = 3.14;double *pd = &obj;void *pv = &obj;pv = pd;std::cout << *pv << std::endl;
error: ‘void*’ is not a pointer-to-object type

void*指针只能与其他指针作比较,作为函数的输入和输出,或者赋值给另外一个 void*指针。

甚至连访问对象都不可以

指向指针的指针

int ival = 1024;int *pi = &ival;int **ppi = πstd::cout << ival << std::endl;std::cout << *pi << std::endl;std::cout << **ppi << std::endl;
102410241024

指向指针的引用

int i = 42;int *p;int *&r = p;r = &i; // p = &i;std::cout << i << std::endl;std::cout << *r << std::endl;std::cout << *p << std::endl;
424242

阅读定义要从右往左,离变量名最近的符号对变量类型有最直接的影响

最近的是 &,因此 r是一个引用

然后是 *,说明 r引用的是一个指针

const

const对象一旦创建,值不可以再改变,因此在创建的时候必须初始化

const int a = 45;

只能在 const类型的对象上执行不改变其内容的操作

可以添加extern关键字,使const变量在文件间共享

extern const int bufSize = fcn(); // file.cpp定义并初始化了这个常量,可以被其他文件访问extern const int bufSize; // file.h 和上面的变量是同一个,只是一个声明,说明定义会在其他地方出现

const的引用是对常量的引用,不能改变引用的值,引用的时候也要添加 const限定符

const int ci = 1024;const int &r1 = ci;

初始化常量引用时可以使用任意的表达式,只要表达式的结果能转化成引用的类型即可

int i = 42;const int &r1 = i;const int &r2 = 42;const int &r3 = r1 * 2;std::cout << r1 << std::endl;std::cout << r2 << std::endl;std::cout << r3 << std::endl;i = 56;std::cout << r1 << std::endl;std::cout << r2 << std::endl;std::cout << r3 << std::endl;
424284564284

因此,对 const的引用可以并非一个 const的对象,不能通过这种引用改变被引用的对象的值,但是可以通过其他方式改变这个对象的值

指向常量的指针也不能用于改变其所指对象的值,且指向常量的指针所指的对象也不一定是一个常量

const double pi = 3.14;const double *cptr = &pistd::cout << *cptr << std::endl;double dval = 3.14;cptr = &dval;std::cout << *cptr << std::endl;
3.143.14

const指针:将指针本身定义为常量,也就是指针所指的地址不变

int errNumb = 0;int *const curErr = &errNumb; // curErr将一直指向errNumb,不能改变const double pi = 3.14159;const double *const pip = &pi // 一个指向常量对象的常量指针*curErr = 56;std::cout << errNumb << std::endl;
56

指针所指的地址不变,但是如果指向的不是常量,还是可以改变指向的值的

顶层 const可以表示任意的对象是一个常量,底层 const与复合类型有关,指的是下一层对象是常量。

常量表达式:值不会改变且在编译过程就能得到计算结果的表达式

将变量声明为 constexpr来由编译器验证是否为一个常量表达式:constexpr int limit = mf + 1;

constexpr中如果声明了一个指针,那么一定是常量指针,即顶层 const

处理变量类型

类型别名的两种定义方式:

typedef double wages;using wages = double;

如果别名是一个复合类型,不能仅仅将其替换进行理解。

auto类型:将类型交给编译器自己去分析,一般会忽略掉顶层 const,如果需要保留要加 const auto进行推断

decltype类型指示符:通过表达式的类型推断出要定义的变量的类型

const int a = 0,b = 0;decltype(a+b) x = 0;std::cout << x << std::endl;
0

如果希望得到引用类型,可以添加两层括号,即 decltype((a+b))

自定义数据结构

struct Sales_data{    std::string bookNo;    unsigned units_sold = 0;    double revenue = 0.0;};

读取单价和数量,输出总价格

Sales_item book; // 创建一个对象std::cin >> book;std::cout << book << std::endl;return 0;
0-201-70353-x 4 24.99> 0-201-70353-x 4 99.96 24.99

头文件:包含只能被定义一起的实体

通过头文件保护符来确保不允许重复包含:

#ifndef SALES_DATA_H#define SALES_DATA_H#include <string>struct Sales_data{    std::string bookNo;    unsigned units_sold = 0;    double revenue = 0.0;};#endif

第3章 字符串、向量和数组

using声明:使用命名空间中的成员

using std::cin;

头文件不应包含 using声明

标准库类型string

定义和初始化

string s1;string s2(s1); // string s2 = s1;string s3("value"); // string s3 = "value";string s4(10,'c');cout << s1 << endl;cout << s2 << endl;cout << s3 << endl;cout << s4 << endl;
valuecccccccccc

初始化分为直接初始化和拷贝初始化,有 =的为拷贝初始化,一般只用于单个初始值的情况下

string对象的操作

输入输出与对整数等的操作相同

使用getline读入一整行(可以带空格)

string line;while(getline(cin,line)){    cout << line << endl;}return 0;
> fds fdsfdsf dsffds fdsfdsf dsf> dsfdsfdsfds fdsfds dsfdsfdsfds fdsfds

string.size()返回的是无符号整形数,不要去负数值混用

字面值不为字符串,不能将字面值相加,只能将字符串相加,如 "df"+"fdsfs"是不合法的

基于范围的 for语句:遍历给定序列中的每一个元素

string str("some string");for (auto c : str){    cout << c;}cout << endl;
some string

如果要改变字符,需要使用引用类型:

string str("some string");for (auto &c : str){    c = toupper(c);}cout << str << endl;
SOME STRING

标准库类型vector

vector属于一个类模板,模板不是类或者函数,但是可以看作编译器生成类或函数编写的一份说明,编译器根据模板创建类或函数的过程称为实例化

定义和初始化vector对象

vector<int> ivec;vector<int> ivec2(ivec);vector<int> ivec3 = ivec;vector<string> articles{"a","an","the"};vector<string> svec(10,"hi");for(auto i : svec){    cout << i << " ";}cout << endl;
hi hi hi hi hi hi hi hi hi hi

值初始化:只初始化 vector的大小,不赋值具体数值 vector<int> i(10)

其他vector操作

vector在设计上事先指定容量是不好的做法,比较适合运行时再添加具体的值

循环内部如果包含向 vector添加元素的语句,不能使用范围 for循环

不能用下标形式添加元素,也就是下标操作只能对确知已经存在的元素进行

迭代器

vector<int> vi = {1,2,4,5,7,8,9,5,6,4};for(auto it1 = vi.begin();it1 != vi.end();it1++){    *it1 *= 2;}for(auto it2 = vi.cbegin();it2 != vi.cend();it2++){    cout << *it2 << " ";}cout << endl;
2 4 8 10 14 16 18 10 12 8

数组

定义与初始化

int a2[] = {0,1,2};int a3[5] = {1,2,4}; // 多余的初始化成默认值char a4[8] = "Daniel"; // 至少是7,要有一个空字符for (auto i: a3){    cout << i << " ";}cout << endl;for (auto i: a4){    cout << i << " ";}cout << endl;
1 2 4 0 0 D a n i e l

数组不允许拷贝和赋值

复杂的数组声明:

int *ptrs[10]; // 含有10个整型指针的数组int arr[10];int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组int *(&arry)[10] = ptrs; // arry是数组的引用,该数组含有10个指针

指针和数组

使用数组的时候编译器一般将其转化为指针

string nums[] = {"one","two","three"};string *p = &nums[0]; // p指向nums的第1个元素string *p2 = nums // 等价于上面的语句

使用 auto推断时会返回一个指针,但是只用 decltype推断的时候会返回数组

利用指针对数组可以起到迭代器的效果

int ia[] = {1,2,3,4,5,6,7,8,9};int *beg = begin(ia); // 指向ia的第一个元素int *last = end(ia); // 指向ia的最后一个元素的下一个位置for(auto it = beg;it != last;it++){    cout << *it << " ";}cout << endl;

C风格字符串

string转化为C风格字符串

string s("Hello World!");const char *str = s.c_str();cout << *str << endl;

使用数组初始化vector

int int_arr[] = {0,1,2,3,4,5};vector<int> ivec(begin(int_arr),end(int_arr));for(auto it : ivec){    cout << it << " ";}cout << endl;
0 1 2 3 4 5

指针和多维数组

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};int (*p)[4] = ia; // p指向含有4个整数的数组p = &ia[2]; // p 指向ia的尾元素for(auto p = ia;p != ia+3;++p){    for(auto q = *p;q != *p+4;q++){        cout << *q << " ";    }}cout << endl;for(auto p = begin(ia);p != end(ia);++p){    for(auto q = begin(*p);q != end(*p);q++){        cout << *q << " ";    }}cout << endl;

第4章 表达式

通俗的讲,左值就是能够出现在赋值符号左面的东西,而右值就是那些可以出现在赋值符号右面的东西.

左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象

右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。

当一个对象被用作右值的时候,使用的是对象的值(内容);当一个对象被用作左值的时候,用的是对象的身份(在内存中的位置)

  1. 算术运算符的运算结果和求值对象都是右值

m%n的符号与 m相同

  1. 逻辑和关系运算符的运算结果和求值对象都是右值
  2. 赋值运算符的左侧运算对象必须是一个可修改的左值,结果是他的左侧运算对象,并且是一个左值
  3. 递增和递减运算符必须作用于左值运算对象,前置版本将对象本身作为左值返回,后置版本将对象原始值的副本作为右值返回
  4. 箭头运算符作用于一个指针类型的运算对象,结果是一个左值
  5. 点运算符的结果与成员所属的对象相同
  6. 条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值,否则运算的结果是右值

强制类型转换:

int i = 52;int j = 9;double slope = static_cast<double>(j) / i;cout << slope << endl;
0.173077

第5章 语句

switch语句:

int a;while(cin >> a){    switch(a){        case 0: cout << '1' << endl;break;        case 1: cout << '2' << endl;break;        case 2: cout << '3' << endl;break;        case 3: cout << '4' << endl;break;        case 4: cout << '5' << endl;break;        case 5: cout << '6' << endl;break;        case 6: cout << '7' << endl;break;        case 7: cout << '8' << endl;break;        case 8: cout << '9' << endl;break;        case 9: cout << '0' << endl;break;        default: cout << 'N' << endl;break;    }}
> 01> 90> 45N

try语句块和异常处理:

throw语句抛出异常:

int a = 1;throw runtime_error("fdsdfds");
terminate called after throwing an instance of 'std::runtime_error'  what():  fdsdfdsAborted

catch语句捕捉异常:

double m, n;cin >> m >> n;try {    if (n == 0)        throw - 1;  //抛出整型异常    else if (m == 0)        throw - 1.0;  //拋出 double 型异常    else        cout << m / n << endl;}catch (double d) {    cout << "catch (double)" << d << endl;}catch (...) {    cout << "catch (...)" << endl;}
> 0 6catch (double)-1> 6 0catch (...)

第6章 函数

局部静态对象:程序第一次经过时被初始化,直到程序终止时才被销毁。

int count_calls(){    static int ctr = -1;    return ++ctr;}int main(void){    for(int i = 0;i != 10; ++i){        cout << count_calls() << " ";    }    cout << endl;    return 0;}
0 1 2 3 4 5 6 7 8 9

函数声明(函数原型):在使用函数之前对函数的名字进行声明

函数声明可以忽略形参的名字,也可以加上形参的名字。

函数声明最好写在头文件中

分离式编译:编译和链接多个源文件

参数传递

指针形参:

void reset(int *ip){    *ip = 0;}int main(void){    int i = 42;    reset(&i);    cout << i << endl;    return 0;}
0

传引用参数:

void reset(int &i){    i = 0;}int main(void){    int i = 42;    reset(i);    cout << i << endl;    return 0;}
0

尽量使用引用形式从而避免拷贝

还可以通过引用形式返回一些额外信息。因为函数只能返回一个返回值,但是如果某个值是引用的形式传到函数中的,也会保留下修改后的值。

不修改的变量尽量使用常量引用

数组形参:

void Print(const int i[]){    cout << i[0] << endl;}void Print(const int *i){    cout << i[0] << endl;}void Print(const int i[10]){    cout << i[0] << endl;}int main(void){    int i = 5;    int j[2] = {6,7};    Print(&i);    Print(j);}
56

数组不能直接进行传递,直接作为指针的形式传递,因此丢掉了数组大小的信息

可以使用指针的形式进行提示,也可以传入一个表示数组大小的参数。

void print(const int *beg,const int *end){    while(beg != end){        cout << *beg++ << " ";    }}void print(const int i[] ,size_t size){    for(size_t a=0;a<size;a++){        cout << i[a] << " ";    }}int main(void){    int i[10] = {6,7,5,4,7,8,9,6,5,4};    print(begin(i),end(i));    cout << endl;    return 0;}
6 7 5 4 7 8 9 6 5 4

数组引用形参:(缺点是只能作用于大小固定的数组)

void print(int (&arr)[10]){    for(auto elem : arr){        cout << elem << " ";    }}

含有可变形参的函数:

void error_msg(initializer_list<string> il){    for(auto beg = il.begin();beg != il.end();++beg){        cout << *beg << " ";    }    cout << endl;}int main(void){    error_msg({"a","b"});    error_msg({"a","b","c"});}
a b a b c

函数的返回值

函数返回时不要返回局部对象的引用或指针

调用一个返回引用的函数会得到左值

char &get_val(string &str,string::size_type ix){    return str[ix];}int main(void){    string s("a value");    cout << s << endl;    get_val(s,0) = 'A';    cout << s << endl;    return 0;}
a valueA value

返回值也可以是一个花括号包围起来的列表

函数重载

定义相同名称的函数,但是形参列表不同,可能是数量上的不同,也可能是类型上的不同。使得函数调用的时候根据不同的形参列表自动判断指定哪一个函数。

顶层 const不影响传入的参数

在不同的作用域中无法重载函数,会覆盖掉

特殊用途语言特性

默认实参:在函数的声明中给一个默认值,调用时可以覆盖掉,也可以不写以使用默认值。

内联函数:将函数在调用点展开,但是编译器不一定支持

constexpr函数:能用于常量表达式的函数,函数的返回值和所有形参的类型都要是字面值类型,函数体中有且只有一条 return语句。

assert表达式:用于调试的时候对程序进行检查 assert(s == "dfdsf");如果不满足条件程序会中断退出。

函数指针

完全不明白。。。没有示例程序看不懂

第7章 类

定义抽象数据类型

成员函数是类定义的一部分,通过特定的对象来调用。非成员函数就是普通的函数。

成员函数的声明必须在类的内部,定义可以在类的内部或者外部。非成员函数的声明和定义都在类的外部。

构造函数:控制对象的初始化过程

访问控制与封装:

定义在public说明符后的成员在整个程序内可被访问,public成员定义类的接口。

定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。

使用class和struct定义类的区别在于默认的访问权限不同,struct默认访问权限都是public的

友元:令其他类或成员成为访问它的非公有成员。但是友元只算一个权限控制,在类外一样要进行声明。

上述代码:

#include <bits/stdc++.h>using std::string;using std::vector;using std::cin;using std::cout;using std::endl;using std::begin;using std::end;using std::runtime_error;using std::initializer_list;using std::istream;using std::ostream;class Sales_data{friend Sales_data add(const Sales_data&,const Sales_data&);friend std::ostream &print(std::ostream&,const Sales_data&);friend std::istream &read(std::istream&,Sales_data&);public:    // 构造函数    Sales_data() = default; // 默认构造函数    // 构造函数初始值列表    Sales_data(const std::string &s):bookNo(s){ }    Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){ }    Sales_data(std::istream &);    // 常量成员函数    std::string isbn() const {        return bookNo;    }    Sales_data &combine(const Sales_data&);private:    double avg_price() const;    std::string bookNo;    unsigned units_sold = 0;    double revenue = 0.0;};Sales_data add(const Sales_data&,const Sales_data&);std::ostream &print(std::ostream&,const Sales_data&);std::istream &read(std::istream&,Sales_data&);// 在类的外部定义成员函数double Sales_data::avg_price() const {    if(units_sold){        return revenue / units_sold;    }    else{        return 0;    }}// 定义一个返回this对象的函数Sales_data& Sales_data::combine(const Sales_data &rhs){    units_sold += rhs.units_sold;    revenue += rhs.revenue;    return *this;}// 类相关的非成员函数istream &read(istream &is, Sales_data &item){    double price = 0;    is >> item.bookNo >> item.units_sold >> price;    item.revenue = price * item.units_sold;    return is;}ostream &print(ostream &os,const Sales_data &item){    os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();    return os;}Sales_data add(const Sales_data &lhs,const Sales_data &rhs){    Sales_data sum = lhs;    sum.combine(rhs);    return sum;}// 在类的外部定义构造函数Sales_data::Sales_data(std::istream &is){    read(is,*this);}int main(void){    return 0;}

类的其他特性

  1. 定义一个类型成员:一般写在最开头的位置
  2. 令成员作为内联函数:可以将 inline写在类内或者类外,一般写在类外
  3. 重载成员函数
  4. 可变数据成员:在 const里面也可以变化
  5. 类数据成员的初始值:一个类里面由另外一个类提供初始值
class Screen{    public:        typedef std::string::size_type pos; // 定义类型的成员        Screen() = default;        Screen(pos ht,pos wd,char c): height(ht), width(wd),contents(ht*wd,c){ }        char get() const {            return contents[cursor]; // 隐式内联函数        };        inline char get(pos ht,pos wd) const; // 显式内联函数        Screen &move(pos r, pos c); // 后面设置为内联函数        size_t some_member() const;        Screen &set(char);        Screen &set(pos,pos,char);        Screen &display(std::ostream &os){            do_display(os);            return *this;        }        const Screen &display(std::ostream &os) const {            do_display(os);            return *this;        }    private:        pos cursor = 0;        pos height = 0,width = 0;        std::string contents;        mutable size_t access_ctr = 0;        void do_display(std::ostream &os) const {            os << contents;        }};inline Screen &Screen::move(pos r,pos c){    pos row = r * width;    cursor = row + c;    return *this;}char Screen::get(pos r,pos c) const {    pos row = r * width;    return contents[row + c];}size_t Screen::some_member() const{    ++access_ctr;    return access_ctr;}inline Screen &Screen::set(char c){    contents[cursor] = c;    return *this;}inline Screen &Screen::set(pos r,pos col,char ch){    contents[r*width+col] = ch;    return *this;}// 类数据成员的初始值class Window_mgr{    private:        std::vector<Screen> screens{Screen(24,80,' ')};};int main(void){    Screen myscreen;    char ch = myscreen.get();    cout << myscreen.some_member() << endl;    ch = myscreen.get(0,0);    cout << myscreen.some_member() << endl;    myscreen.move(4,0);    myscreen.set('#');    cout << myscreen.get() << endl;    Screen myScreen(5,3,'!');    const Screen blank(5,3,'?');    myScreen.set(2,1,'#').display(cout);    cout << endl;    blank.display(cout);    cout << endl;    return 0;}
12#!!!!!!!#!!!!!!!???????????????

类之间的友元关系:不存在传递性

class Screen{    friend class Window_mgr;// 类数据成员的初始值class Window_mgr{    public:        using ScreenIndex = std::vector<Screen>::size_type;        void clear(ScreenIndex);    private:        std::vector<Screen> screens{Screen(24,80,' ')};};void Window_mgr::clear(ScreenIndex i){    Screen &s = screens[i];    s.contents = string(s.height * s.width, ' ');}

类的作用域

class Screen{    friend class Window_mgr;    public:        typedef std::string::size_type pos; // 定义类型的成员        Screen() = default;        Screen(pos ht,pos wd,char c): height(ht), width(wd),contents(ht*wd,c){ }        char get() const {            return contents[cursor]; // 隐式内联函数        };        inline char get(pos ht,pos wd) const; // 显式内联函数        Screen &move(pos r, pos c); // 后面设置为内联函数        size_t some_member() const;        Screen &set(char);        Screen &set(pos,pos,char);        Screen &display(std::ostream &os){            do_display(os);            return *this;        }        const Screen &display(std::ostream &os) const {            do_display(os);            return *this;        }    private:        pos cursor = 0;        pos height = 0,width = 0;        std::string contents;        mutable size_t access_ctr = 0;        void do_display(std::ostream &os) const {            os << contents;        }};inline Screen &Screen::move(pos r,pos c){    pos row = r * width;    cursor = row + c;    return *this;}char Screen::get(pos r,pos c) const {    pos row = r * width;    return contents[row + c];}size_t Screen::some_member() const{    ++access_ctr;    return access_ctr;}inline Screen &Screen::set(char c){    contents[cursor] = c;    return *this;}inline Screen &Screen::set(pos r,pos col,char ch){    contents[r*width+col] = ch;    return *this;}// 类数据成员的初始值class Window_mgr{    public:        using ScreenIndex = std::vector<Screen>::size_type;        void clear(ScreenIndex);        ScreenIndex addScreen(const Screen&);    private:        std::vector<Screen> screens{Screen(24,80,' ')};};void Window_mgr::clear(ScreenIndex i){    Screen &s = screens[i];    s.contents = string(s.height * s.width, ' ');}Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s){    screens.push_back(s);    return screens.size() - 1;}int main(void){    Screen myscreen;    char ch = myscreen.get();    cout << myscreen.some_member() << endl;    ch = myscreen.get(0,0);    cout << myscreen.some_member() << endl;    myscreen.move(4,0);    myscreen.set('#');    cout << myscreen.get() << endl;    Screen myScreen(5,3,'!');    const Screen blank(5,3,'?');    myScreen.set(2,1,'#').display(cout);    cout << endl;    blank.display(cout);    cout << endl;    return 0;}

构造函数进阶

构造函数初始值列表:定义变量的时候习惯对其立即进行初始化,有时初始化的值是必不可少的,且要注意成员初始化的顺序。

委托构造函数:使用它所属类的其他构造函数执行它自己的初始化过程

默认构造函数

类类型转换

聚合类:就是比较简单的结构体

字面值常量类

类的静态成员

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据

]]>
+ + + + + Study + + + + + + + C++ + + + +
+ + + + + Numpy中axis的理解 + + /2022/08/06/ndarray-axis/ + + Numpy是个好东西,但是ndarray的轴感觉弄不太明白。可能二维三维数组还好,要是再增加几维就无法在脑海中想象这个东西,对于一些有关轴的操作就稀里糊涂,只能一个个尝试。现在准备把它彻底弄明白!

思路

首先从二维入手,然后扩展到三维以及更高的维度(从特殊到一般),然后找出普遍的规律,再进行验证(从一般到特殊)

官方文档应该是最权威的,首先看官方文档是怎么说明的,然后查找一些资料,看看其他人是怎么理解的,最后总结出自己的一套规律

import numpy as np

ndarray.shape

感受一个ndarray,最简单的方法就是打印ndarray的shape。

官方文档里面是这样写的:

the dimensions of the array. This is a tuple of integers indicating the size of the array in each dimension. For a matrix with n rows and m columns, shape will be (n,m). The length of the shape tuple is therefore the number of axes, ndim.

只列举了矩阵的例子,尝试一下:

a1 = np.arange(15).reshape(3, 5)print(a1,'\n',a1.shape,'\n',a1.ndim)

输出结果:

[[ 0  1  2  3  4] [ 5  6  7  8  9] [10 11 12 13 14]]  (3, 5)  2
  1. reshape成什么样,最后打印出来的shape就会是什么样,这一点可以确定。
  2. 官方文档里面写道“对于一个n行m列的矩阵来说,shape将会是(n,m)”。经验证,打印出来了一个3行5列的矩阵,shape是(3,5)。
  3. 官方文档里面写道“shape元组的长度就是轴的数量,也就是ndim”。经验证,ndim=2

简单推断:最开始有2个方括号,因此矩阵是2维的,且第1个方括号内部有3个“2级方括号”,每一个“2级方括号”内部都有5个元素,因此这个shape可能是从外向里数的。

尝试1维ndarray:

a2 = np.arange(15)print(a2,'\n',a2.shape,'\n',a2.ndim)

输出结果:

[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14]  (15,)  1
  1. 打印ndim为1,最开始有1个方括号,因此数组是1维的。结论得到验证。
  2. 打印shape为(15,)(一维元组),第1个方括号内部没有“2级方括号”shape从外向里数只有15。结论得到验证。

尝试3维ndarray:

a3 = np.arange(24).reshape(3,2,4)print(a3,'\n',a3.shape,'\n',a3.ndim)

输出结果:

[[[ 0  1  2  3]  [ 4  5  6  7]] [[ 8  9 10 11]  [12 13 14 15]] [[16 17 18 19]  [20 21 22 23]]]  (3, 2, 4)  3
  1. 打印ndim为3,最开始有3个方括号,因此数组是3维的。结论得到验证。
  2. 打印shape为(3, 2, 4),第1个方括号内部有3个“2级方括号”,“2级方括号”内部有2个“3级方括号”,“3级方括号”内部有4个元素。满足shape从外向里数,结论得到验证。

尝试4维ndarray:

a4 = np.arange(24).reshape(3,2,1,4)print(a4,'\n',a4.shape,'\n',a4.ndim)

输出结果:

[[[[ 0  1  2  3]]  [[ 4  5  6  7]]] [[[ 8  9 10 11]]  [[12 13 14 15]]] [[[16 17 18 19]]  [[20 21 22 23]]]]  (3, 2, 1, 4)  4
  1. 打印ndim为4,最开始有4个方括号,因此数组是4维的。结论得到验证。
  2. 打印shape为(3, 2, 1, 4),第1个方括号内部有3个“2级方括号”,“2级方括号”内部有2个“3级方括号”,“3级方括号”内部有1个“4级方括号”,“4级方括号”内部有4个元素。满足shape从外向里数,结论得到验证。
  3. 有一个维度是1,也就是这个维度实际上并没有任何的作用。但是在实际中可能会有“凑维度”的操作,需要手动增加或者减少维度,会出现这种维度为1的情况。(增加维度使用reshape()实现,减小维度使用squeeze()实现)

因此可以得出结论:对于给定的ndarray,判断ndim就是计数最前面有多少个相连的方括号,判断shape就是从外向内看,每一层分别有多少个“元素”。

也可以看出,数组超过4维后,肉眼就有些难以区分了。

索引

索引就是取数组中的某些元素,官方文档有下面的举例:

>>> a = np.arange(30).reshape(2, 3, 5)>>> aarray([[[ 0,  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]]])>>> a[0, 2, :]array([10, 11, 12, 13, 14])>>> a[0, :, 3]array([ 3,  8, 13])

索引操作是与shape相对应的。如上述例子,a[0]即为取数组的第1个维度(2)的第1个元素,这样原来3维的数组就降到了2维;a[0, :]就是在a[0]的基础上取数组的第2个维度(3)的全部元素,数组的维度不变,还是2维;a[0, :, 3]就是在a[0, :]的基础上取数组的第3个维度(5)的第4个元素,即可得出上面的结果。

索引操作后的维度与索引的数量以及是否有“:”相关。如果索引的数量与ndim相同,则最后取出来的是一个数。如果数量不同或者有“:”(数量不同可以看成在后面补“:”),则最终取得的数组的维度与“:”对应的原数组的维度相同。

以numpy.sum为例:

官方文档:

Axis or axes along which a sum is performed. The default, axis=None, will sum all of the elements of the input array. If axis is negative it counts from the last to the first axis.

If axis is a tuple of ints, a sum is performed on all of the axes specified in the tuple instead of a single axis or all the axes as before.

以三维数组为例:

print('origin')print(a3,a3.shape)print('axis=0')print(a3.sum(axis=0),a3.sum(axis=0).shape)print('axis=1')print(a3.sum(axis=1),a3.sum(axis=1).shape)print('axis=2')print(a3.sum(axis=2),a3.sum(axis=2).shape)print('axis=(0,1)')print(a3.sum(axis=(0,1)),a3.sum(axis=(0,1)).shape)print('axis=(1,2)')print(a3.sum(axis=(1,2)),a3.sum(axis=(1,2)).shape)print('axis=(0,2)')print(a3.sum(axis=(0,2)),a3.sum(axis=(0,2)).shape)print('axis=(0,1,2)')print(a3.sum(axis=(0,1,2)),a3.sum(axis=(0,1,2)).shape)
origin[[[ 0  1  2  3]  [ 4  5  6  7]] [[ 8  9 10 11]  [12 13 14 15]] [[16 17 18 19]  [20 21 22 23]]] (3, 2, 4)axis=0[[24 27 30 33] [36 39 42 45]] (2, 4)axis=1[[ 4  6  8 10] [20 22 24 26] [36 38 40 42]] (3, 4)axis=2[[ 6 22] [38 54] [70 86]] (3, 2)axis=(0,1)[60 66 72 78] (4,)axis=(1,2)[ 28  92 156] (3,)axis=(0,2)[114 162] (2,)axis=(0,1,2)276 ()

axis为多少,就是在这个维度上进行操作,最终的结果就是这个维度消失

不要从行列什么的去思考怎么变化,直接从shape的角度入手。设置axis为多少,这个维度就没有了!比如原来是(3,2,4)的维度,要是axis=0,第一个维度就没有了,加和得到的矩阵就是(2,4)。

如果希望保留维度,可以增加keepdims=True的选项,这样被操作的维度就会变为1而不是直接消失。

print('axis=(0,1)')print(a3.sum(axis=(0,1),keepdims=True),a3.sum(axis=(0,1),keepdims=True).shape)
axis=(0,1)[[[60 66 72 78]]] (1, 1, 4)

这样想应该会比较好理解,尤其是对于更高维的数组来说,行列的概念基本失效,从shape的角度思考会好。

np.concatenate

另外一个比较常用的操作是np.concatenate,可以将数组进行合并,在数据处理或者神经网络中很常用。

在np.concatenate上检验一下对于axis的理解:

ta = np.arange(24).reshape(3,2,4)tb = np.arange(24,36).reshape(3,1,4)print(ta,ta.shape)print(tb,tb.shape)
[[[ 0  1  2  3]  [ 4  5  6  7]] [[ 8  9 10 11]  [12 13 14 15]] [[16 17 18 19]  [20 21 22 23]]] (3, 2, 4)[[[24 25 26 27]] [[28 29 30 31]] [[32 33 34 35]]] (3, 1, 4)

两者合并,第2个维度不相同,应该是可以合并的,合并后的shape应该为(3,3,4)

print(np.concatenate((ta,tb),axis=1),np.concatenate((ta,tb),axis=1).shape)
[[[ 0  1  2  3]  [ 4  5  6  7]  [24 25 26 27]] [[ 8  9 10 11]  [12 13 14 15]  [28 29 30 31]] [[16 17 18 19]  [20 21 22 23]  [32 33 34 35]]] (3, 3, 4)

np.concatenate除了在待合并的axis上之外,必须具有相同的shape

之前的结论也得到了验证。

总结

我们处在三维空间中,二维和三维是比较直观的,可以在脑海中想象出来。因此我们会觉得axis的设计有些反直觉。以后应该从shape的角度去看待axis的设计思想,首先理解上比较直观,其次在更高维度的数组上也能合理的进行操作。不要去思考数组实际中应该是个什么样子,直接观察axis就足够了。

参考资料

Code

Numpy官方文档

]]>
+ + + + + Study + + + + + + + Python + + Numpy + + + +
+ + + + + Unsupervised Learning, Recommenders, Reinforcement Learning + + /2022/08/04/Coursera/Unsupervised-Learning-Recommenders-Reinforcement-Learning/ + + 机器学习-无监督学习,推荐系统与强化学习

课程简介

In the third course of the Machine Learning Specialization, you will:

• Use unsupervised learning techniques for unsupervised learning: including clustering and anomaly detection.
• Build recommender systems with a collaborative filtering approach and a content-based deep learning method.
• Build a deep reinforcement learning model.

The Machine Learning Specialization is a foundational online program created in collaboration between DeepLearning.AI and Stanford Online. In this beginner-friendly program, you will learn the fundamentals of machine learning and how to use these techniques to build real-world AI applications.

This Specialization is taught by Andrew Ng, an AI visionary who has led critical research at Stanford University and groundbreaking work at Google Brain, Baidu, and Landing.AI to advance the AI field.

This 3-course Specialization is an updated and expanded version of Andrew’s pioneering Machine Learning course, rated 4.9 out of 5 and taken by over 4.8 million learners since it launched in 2012.

It provides a broad introduction to modern machine learning, including supervised learning (multiple linear regression, logistic regression, neural networks, and decision trees), unsupervised learning (clustering, dimensionality reduction, recommender systems), and some of the best practices used in Silicon Valley for artificial intelligence and machine learning innovation (evaluating and tuning models, taking a data-centric approach to improving performance, and more.)

By the end of this Specialization, you will have mastered key concepts and gained the practical know-how to quickly and powerfully apply machine learning to challenging real-world problems. If you’re looking to break into AI or build a career in machine learning, the new Machine Learning Specialization is the best place to start.

无监督学习

无监督学习是在没有标签的数据中自动寻找某些规律

聚类任务是典型的无监督学习任务,通过某些特征将相似的人或事物自动归为一类

无监督学习任务还有异常检测(找出一些不寻常的数据)和维度降低(使用更少的数字对数据进行压缩)

聚类

聚类是一种典型的无监督学习算法,不定义标签,让算法自己去寻找数据中有趣的特征

聚类可以在下面几个方面得到应用:

  1. 找出比较相似的新闻
  2. 对用户或者市场进行分析
  3. 分析DNA
  4. 分析宇宙数据

K-means聚类步骤:

  1. 随机初始化个中心点预先定义)
  2. 计算其余的点与中心点的距离,与最近的中心点归为一类
  3. 更改中心点为类别中所有点的平均值
  4. 迭代上述步骤直到所有点的类别不再变化

如何决定聚类的数量?Elbow method

多种聚类数量都尝试一下,找到“肘点”,也就是增加聚类数量后代价函数也不能明显减小的点

如何随机初始化最初的类别中心点?

  1. 随机选择几个训练样本作为中心点
  2. 随机选取中心点多次,运行一轮算法,寻找代价最小的作为初始化的中心点

异常检测

已经拥有一些数据,增加一条数据,判断其是否符合已有的数据的特征,如果不符合则为异常数据

正态分布:

异常检测:,计算点的是否满足大于预先定义的阈值

实际应用中,可以找一些有标记的异常点,指导算法选取合适的阈值

在某种类别(异常)的数据量很少的情况下,且异常的种类较多,特征无法很好区分出来的时候,使用异常检测算法比较好。

推荐系统

场景:很多用户对电影进行打分,分数从0-5,如何向用户推荐合适的电影?

设用户的数量为,电影的数量为

如果用户对电影进行了打分,那么,反之

表示用户对电影打分的分数(0-5)

表示电影的特征数量(如浪漫程度、武打程度等等),则用户对应的特征向量为

表示的是用户打分的电影数目

预测用户对电影的打分:

代价函数:

对所有用户来说,是定值,忽略不计

协同过滤算法

前面是有特征,通过类似于线性回归的方式可以进行预测,但是如果没有特征应该怎么做呢?

已知,预测

代价函数:

将两个代价函数结合到一起:

如果评分是二值化的,则类似于线性回归与逻辑回归的区别:

如果一个人没有对任何电影进行评分,则选取其他所有人的评分平均值作为他的评分。

协同过滤算法的局限性:

  1. 对于新加进来的事物不太好办,没有办法与其他的一起排名,且推荐后有一点讲不出道理
  2. 不能使用一些已有的其他特征

基于内容的过滤算法

协同过滤算法是基于用户的评分,根据比较相似的评分情况来进行推荐

基于内容的过滤算法是基于用户和物品的特征来寻找比较合适的匹配对

设用户对应的特征是,电影对应的特征是

比较两个特征之间相似度的方法是点乘,但是两者的维度不同,因此要对输入的特征增加几层神经网络,使其输出相同,再进行点乘。

通过神经网络后,的32维向量,的32维向量,

代价函数为::

大型推荐系统

检索和排序策略:

  1. 检索策略会从大规模中选择可信的候选者,如对于电影来说找这个国家最流行的20个电影等等,然后汇总、去重
  2. 然后对去重后的列表使用算法进行排序,按照排名的先后顺序向用户推荐。

强化学习

强化学习不告诉应该怎么做,而是只告诉做什么,如果做的好有奖励,做的不好有惩罚,从而让算法自动向着奖赏最多的方向优化,最终学习出最好的结果。

目前的状态、动作、奖励、下一个状态,下一个状态的动作

每一个时间步后,会有一个权重,最终的返回值(Return)是权重与奖励的乘积

一般来说,权重按照幂的方式变化,如第一步是,第二步是,第步是

措施指的是在状态应该采取什么样的动作

强化学习的目标就是找到合适的措施从而最大化返回的奖励(Return)

马尔可夫决策过程:未来只取决于现在所处的位置,与之前是怎么到达现在这个位置的无关。

状态-动作方程:表示从状态开始进行动作,然后后面采取最优化的动作

因此,可以得出两个结论:

  1. 从状态开始的最佳的返回奖励是
  2. 从状态开始的最好的动作是能达到的动作

贝尔曼方程:

在更为复杂的环境下,状态之间的转移可能并不是确定的,有一定的几率到达其他的状态

因此得到的返回奖励实际上是期望的返回奖励,即

状态空间可能是连续的,对于月球车来说,有方向(前后左右和旋转)和速度两种变量,因此

强化学习神经网络(DQN)

训练神经网络:输入是,输出目的是找到最合适的动作使得最大。其中,神经网络的最后一层输出的神经元数量可以修改为的数量,就可以对所有可能情况的进行同时训练。

训练步骤:

  1. 随意进行一个动作,得到
  2. 采集大量的
  3. 使用作为输入,作为输出训练使得
  4. ,重复上述步骤

算法优化

-贪心策略:在DQN的第一步中,以的概率随意选取,以的概率选取能使最大化的

mini-batch:在只选取一部分进行训练

soft update:步骤中,并不直接修改,而是使用权重对新旧参数进行组合

强化学习的现状

  1. 用于实验室模拟的效果比较好,实际中有些困难
  2. 目前的应用比监督学习或者无监督学习要少很多
  3. 在未来应用上还是有很大的潜力的

资料

第一周课件

第二周课件

第三周课件和代码

Notebooks Week 3

作业代码

Exercise 1

Exercise 2

Exercise 3

Exercise 4

Exercise 5

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + + +
+ + + + + Advanced Learning Algorithms + + /2022/08/01/Coursera/Advanced-Learning-Algorithms/ + + 机器学习-高级学习算法

课程简介

In the second course of the Machine Learning Specialization, you will:

• Build and train a neural network with TensorFlow to perform multi-class classification
• Apply best practices for machine learning development so that your models generalize to data and tasks in the real world
• Build and use decision trees and tree ensemble methods, including random forests and boosted trees

The Machine Learning Specialization is a foundational online program created in collaboration between DeepLearning.AI and Stanford Online. In this beginner-friendly program, you will learn the fundamentals of machine learning and how to use these techniques to build real-world AI applications.

This Specialization is taught by Andrew Ng, an AI visionary who has led critical research at Stanford University and groundbreaking work at Google Brain, Baidu, and Landing.AI to advance the AI field.

This 3-course Specialization is an updated and expanded version of Andrew’s pioneering Machine Learning course, rated 4.9 out of 5 and taken by over 4.8 million learners since it launched in 2012.

It provides a broad introduction to modern machine learning, including supervised learning (multiple linear regression, logistic regression, neural networks, and decision trees), unsupervised learning (clustering, dimensionality reduction, recommender systems), and some of the best practices used in Silicon Valley for artificial intelligence and machine learning innovation (evaluating and tuning models, taking a data-centric approach to improving performance, and more.)

By the end of this Specialization, you will have mastered key theoretical concepts and gained the practical know-how to quickly and powerfully apply machine learning to challenging real-world problems. If you’re looking to break into AI or build a career in machine learning, the new Machine Learning Specialization is the best place to start.

生物神经网络

生物神经元:通过树突接收到来自不同地方的输入,然后通过轴突将神经冲动传递出去。

但是目前对于人脑的运作方式了解的还不是很透彻。

Tensorflow搭建神经网络

  1. 定义神经网络:
model = Sequential(    [           tf.keras.Input(shape=(400,)),    #specify input size        tf.keras.layers.Dense(25, activation='sigmoid'),        tf.keras.layers.Dense(15, activation='sigmoid'),        tf.keras.layers.Dense(1, activation='sigmoid')    ], name = "my_model" )
  1. 训练神经网络
model.compile(    loss=tf.keras.losses.BinaryCrossentropy(),    optimizer=tf.keras.optimizers.Adam(0.001),)model.fit(    X,y,    epochs=20)
  1. 预测
prediction = model.predict(X[0].reshape(1,400))  # a zeroprint(f" predicting a zero: {prediction}")prediction = model.predict(X[500].reshape(1,400))  # a oneprint(f" predicting a one:  {prediction}")

Python搭建神经网络

  1. 定义网络层
def my_dense_v(A_in, W, b, g):    """    Computes dense layer    Args:      A_in (ndarray (m,n)) : Data, m examples, n features each      W    (ndarray (n,j)) : Weight matrix, n features per unit, j units      b    (ndarray (1,j)) : bias vector, j units        g    activation function (e.g. sigmoid, relu..)    Returns      A_out (ndarray (m,j)) : m examples, j units    """    z = np.matmul(A_in,W)+b    A_out = g(z)    return(A_out)
  1. 组合不同的层
def my_sequential_v(X, W1, b1, W2, b2, W3, b3):    A1 = my_dense_v(X,  W1, b1, sigmoid)    A2 = my_dense_v(A1, W2, b2, sigmoid)    A3 = my_dense_v(A2, W3, b3, sigmoid)    return(A3)
  1. 预测
Prediction = my_sequential_v(X, W1_tmp, b1_tmp, W2_tmp, b2_tmp, W3_tmp, b3_tmp )Prediction.shape

通用人工智能(AGI)

人工智能(AI)可以分为两种,ANI和AGI:

ANI指在某一特定领域应用的人工智能,目前已经取得了很好的效果;

AGI指通用人工智能,人工智能可以做任何人类可以做到的事情。

鉴于对人脑的了解还不够,如果通过模拟人脑的方式达到通用人工智能比较困难。

不过目前有一些进展,让通用人工智能看到了一点点希望。

训练神经网络

  1. 决定输入变量、模型的数学形式、参数以及最终输出的结果形式
  2. 定义损失函数和代价函数(损失函数是针对一个训练样本而言的,代价函数是结合全部训练数据的损失函数得来的)
  3. 在数据上使用某种方法(如梯度下降法)进行训练,从而使代价函数最小

激活函数

如果不使用激活函数,那么不管多么复杂的神经网络都会退化成线性回归方法可以实现的效果。

Sigmoid激活函数:

ReLU激活函数:

如何选择输出层的激活函数:

  1. 二分类问题,选择Sigmiod激活函数
  2. 线性回归问题不使用激活函数,如果确保没有负数值出现,可以使用ReLU激活函数

隐藏层中大多数使用ReLU激活函数而非Sigmoid激活函数

  1. ReLU激活函数比Sigmoid激活函数计算更快
  2. ReLU激活函数与x轴平行的部分更少,使用梯度下降算法运行更快

多类别分类

多类别分类是指输出不止两种情况的分类问题,如对手写数字进行分类,输出的类别会有10个

可以使用Softmax回归算法:

损失函数:,也就是

多标签分类:可以看成很多多类别分类问题,也可以使用一个神经网络预测所有的类别

优化方法

Adam优化方法:自动调节学习率

如果梯度下降的方向一直是同一方向则增大学习率,让算法运行更快

如果梯度下降的方向一直在波动,则减小学习率。

机器学习问题诊断

如果发现训练好的模型在预测上存在很大的问题,可以从以下几个方面入手查找原因:

  1. 采集更多的训练样本——高方差
  2. 尝试减小特征数目——高方差
  3. 尝试增加额外的特征——高偏差
  4. 尝试增加一些其他多项式的特征,如等等——高偏差
  5. 尝试增加或减少正则项——高方差、高偏差

训练时对训练集进行划分,可以划分为训练集和测试集,如果希望使用交叉验证的方式,可以划分为训练集、验证集和测试集。通过测试集的表型评估模型的效果。模型的选择上,可以从多项式的次数从低到高依次进行选择,找出测试集误差最小的模型。

更大规模的神经网络的偏差往往更小

如果恰当选择正则化参数,更大规模的神经网络的表现比小规模的神经网络表现更好

偏差和方差

欠拟合:函数对于训练集的拟合效果不好——高偏差(high bias)

过拟合:函数对于训练集的拟合效果好,对于测试集的效果不好——高方差(high variance)

避免过拟合的方法:

  1. 收集更多的训练数据
  2. 从全部的特征中挑选最相关的特征进行训练
  3. 正则化——减小某一参数对拟合函数的影响

正则项参数对模型的影响

  1. 太大的导致模型的训练集拟合效果不好——高偏差(high bias)
  2. 太小的导致模型对于训练集的拟合效果好,对于测试集的效果不好——高方差(high variance)

学习曲线:

  1. 正常的学习曲线,随着训练集样本数量的增加,训练集的误差会逐渐增大,验证集的误差会逐渐减小,但是验证集的误差会始终大于训练集的误差
  2. 如果一个模型偏差比较大,增加更多的训练数据不会帮助提升效果
  3. 如果一个模型的方差比较大,可以考虑增加更多的训练数据

评价分类(尤其针对分布不平衡的数据)

决策树

熵(Entropy)

信息增益

  1. 在根结点处使用所有的数据示例
  2. 对每一种可能的分类方式计算信息增益,选择信息增益最高的分类方式
  3. 使用上一步选择的分类方式对数据进行划分,划分成为左子树和右子树
  4. 重复上述的操作,直到①某一个节点仅有一种类别②决策树高度超过阈值③信息增益小于阈值

如果一个决策结点有三个可选项,可以通过独热编码的方式将其转换为多个二分类形式。

如果变量是连续的数值,可以计算从那里开始划分的信息增益最高,从而转化为判断大小于的二分类形式。

决策树解决回归问题,则将熵替换为节点上数据的方差进行计算。

随机森林:

  1. 有放回采样训练数据,并且分别使用采样后的训练数据训练决策树
  2. 为了使决策树的决策结点不完全相同,每一次选取特征的时候只选取一部分子集的特征
  3. 最后使用投票法确定最终的分类

XGBoost:采样训练数据的时候更倾向于采样前面的树中被分类错误的数据

决策树更适用于结构化的数据,快速,但是不适用于其他类似于图片文本等的数据

神经网络适用于所有类型的数据,运行可能稍慢一些,可以迁移学习,更适合将不同的神经网络结合到一起。

资料

第一周课件和代码

Notebooks Week 1

第二周课件和代码

Notebooks Week 2

第三周课件

第四周课件

作业代码

Exercise 1

Exercise 2

Exercise 3

Exercise 4

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + + +
+ + + + + Mathematics for Machine Learning: Multivariate Calculus + + /2022/07/31/Coursera/Mathematics-for-Machine-Learning-Multivariate-Calculus/ + + 数学在机器学习领域的应用二:多元微积分

课程简介

This course offers a brief introduction to the multivariate calculus required to build many common machine learning techniques. We start at the very beginning with a refresher on the “rise over run” formulation of a slope, before converting this to the formal definition of the gradient of a function. We then start to build up a set of tools for making calculus easier and faster. Next, we learn how to calculate vectors that point up hill on multidimensional surfaces and even put this into action using an interactive game. We take a look at how we can use calculus to build approximations to functions, as well as helping us to quantify how accurate we should expect those approximations to be. We also spend some time talking about where calculus comes up in the training of neural networks, before finally showing you how it is applied in linear regression models. This course is intended to offer an intuitive understanding of calculus, as well as the language necessary to look concepts up yourselves when you get stuck. Hopefully, without going into too much detail, you’ll still come away with the confidence to dive into some more focused machine learning courses in future.

函数

函数是从输入到输出的映射,选择函数来建模世界的过程是伟大天才的科学目的,微积分只是对这些函数如何相对于它们的输入变量如何变化的研究。

导数(derivative)

对于线性函数而言,斜率(梯度、gradient)=‘rise over run’,也就是任意取两点,方向的距离与方向的距离之比即为梯度。

对于梯度一直在变化的函数来说,设函数为,任意取两点

即,

导数的求和法则:

幂函数求导法则:令,则

不连续(discontinuity)的函数,例如,在处没有定义,导数处也没有定义.

例如这种函数,,这种类型的函数与导数始终相等,因此有两个特点:

  1. 函数必须恒大于0或者恒小于0,如果函数改变符号,导数也会改变符号,会使得函数不改变符号,与定义不符
  2. 函数是单调的,因为函数永远不可能达到其原来的值

三角函数:

导数乘积法则:令,则

求导的链式法则:若,且,则

偏导数求导法则:

偏导数仍然遵循导数的求导法则

雅可比行列式(Jacobian)

设函数,它的雅可比行列式为

这样给予一组的值,可以快速得出函数在该点指向此函数最陡斜率方向的向量。

设函数,则它的雅可比行列式为

海森矩阵(The Hessian)

对雅可比行列式再求一次偏导数,构成的二阶偏导数矩阵为海森矩阵

设函数,它的雅可比行列式为,则海森矩阵为

雅可比行列式求得的值为0的情况下,首先求海森矩阵的行列式,如果行列式为正数,说明目前的点是一个极值点;然后看海森矩阵的第一个数字,如果第一个数字是正数,说明目前在极小值点,否则在极大值点;如果海森矩阵的行列式为负,说明目前的点是一个鞍点。

神经网络

最简单的神经网络:,其中,表示活动,表示权重,表示偏差,表示激活函数

输入可能不仅仅是一个,设输入的神经元有个,则

输出可能也不仅仅是一个,设输出的神经元有个,总体的神经网络表示为:

可以简化表示为:

如果神经网络不止一层,则可以表示为:

神经网络(分类任务)的损失函数为

泰勒展开式

泰勒展开式是对一个复杂函数的简化估计函数

(麦克劳林形式,需要知道零点)

泰勒形式:

(泰勒形式,知道任意一点即可)

二维泰勒展开

(零阶泰勒展开)
(一阶泰勒展开-雅可比行列式)

(二阶泰勒展开-海森矩阵)

牛顿迭代法(Newton-Raphson)

迭代求解方程的近似根:

这种方法会存在一些问题,如果选取的点比较靠近函数的拐点,会得不到正确的结果,或者得到的结果并不是与选取的点最接近的。

梯度下降

如何使用梯度找到多元函数的最大值或者最小值

函数的梯度:,即为函数值增加最快的方向

如果希望找到最大值,将梯度与它的单位向量相乘,则

梯度下降:

拉格朗日乘子法(Lagrange multipliers)

计算函数在某些约束下的最大值或者最小值

为拉格朗日乘子

即:

多元微积分在回归问题中的应用

线性回归

设函数

计算平方误差:

求解使得误差最小:

则可以解得:

非线性回归

  1. 梯度下降法
  2. 泰勒展开式+海森矩阵
  3. 莱文贝格-马夸特方法(Levenberg-Marquardt)
  4. 高斯-牛顿迭代法 (Gauss-Newton iteration method)
  5. 拟牛顿法(BFGS)

资料

Formula Sheet: Sheet summarising all the formulae covered in this course.

Code and Notebooks

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Calculus + + + +
+ + + + + Supervised Machine Learning: Regression and Classification + + /2022/07/28/Coursera/Supervised-Machine-Learning-Regression-and-Classification/ + + 机器学习-监督学习:回归和分类

开始学习

吴恩达的机器学习课程终于更新了!!!想当初应该是大二的时候,看了吴恩达的课程,对机器学习有了初步的了解。当时听的不是很明白,英语看不太懂,一些给了充分提示的代码也写不太好,也就是入了一个门而已。这次有一些时间,正好捡一捡机器学习的基础知识,推一推那些一直在调包的数学公式。注重记录!

课程简介

In the first course of the Machine Learning Specialization, you will:

• Build machine learning models in Python using popular machine learning libraries NumPy and scikit-learn.
• Build and train supervised machine learning models for prediction and binary classification tasks, including linear regression and logistic regression

The Machine Learning Specialization is a foundational online program created in collaboration between DeepLearning.AI and Stanford Online. In this beginner-friendly program, you will learn the fundamentals of machine learning and how to use these techniques to build real-world AI applications.

This Specialization is taught by Andrew Ng, an AI visionary who has led critical research at Stanford University and groundbreaking work at Google Brain, Baidu, and Landing.AI to advance the AI field.

This 3-course Specialization is an updated and expanded version of Andrew’s pioneering Machine Learning course, rated 4.9 out of 5 and taken by over 4.8 million learners since it launched in 2012.

It provides a broad introduction to modern machine learning, including supervised learning (multiple linear regression, logistic regression, neural networks, and decision trees), unsupervised learning (clustering, dimensionality reduction, recommender systems), and some of the best practices used in Silicon Valley for artificial intelligence and machine learning innovation (evaluating and tuning models, taking a data-centric approach to improving performance, and more.)

By the end of this Specialization, you will have mastered key concepts and gained the practical know-how to quickly and powerfully apply machine learning to challenging real-world problems. If you’re looking to break into AI or build a career in machine learning, the new Machine Learning Specialization is the best place to start.

什么是机器学习

  • 在谷歌、必应或百度上进行网络搜索会出现想要的答案。这是因为他们的机器学习软件已经解决了如何对网页进行排名。
  • 上传照片到Instagram或Snapchat,并且想标记我的朋友,让他们可以看到他们的照片。这些应用程序可以识别你照片中的朋友,并给他们贴上标签。
  • 刚刚在视频流服务上看完一部星球大战电影,流媒体服务可能会使用机器学习来推荐您可能喜欢的内容。
  • 用语音短信在手机上写短信时,手机会做出你希望的行为
  • 收到一封赢了一百万美元的电子邮件,你的电子邮件服务很可能会将其标记为垃圾邮件。
  • 除了消费者应用之外,人工智能也在迅速进入大公司和工业应用。机器学习已经有望优化风力涡轮机发电,开始进入医院,帮助医生做出准确的诊断,将计算机视觉应用到工厂中,以帮助检查生产线中的产品是否有任何缺陷。
  • 机器学习是一门让计算机在没有明确编程的情况下学习的科学-1950

监督学习

监督学习是学习从输入到输出标签的一个函数映射,主要特征是给予算法示例去学习,也就是从被给予的正确答案中学习。

监督学习的基本类型有两种:回归和分类

回归任务是在大量的数值空间中,对某一个具体数值进行预测

分类任务是在给定的数值空间中(如0和1),对某一个具体数据进行预测

符号表示方法

表示输入的变量或者特征

表示输出的实际目标变量,表示预测的变量

表示训练样本总数

表示一个训练样本,表示第个训练样本

线性回归的机器学习模型可以表示为:

损失函数

度量预测值与实际目标值之间的差异

线性回归中使用的平方损失函数:,将机器学习模型代入,则表示为

目标就是要找出最合适的,使得最小

使用梯度下降算法:

为学习率

梯度下降在更新的时候需要同时更新,因此在计算的过程中,首先要计算,然后再相减,保证同步更新。

具体计算:

学习率的选择:

如果学习率过小,梯度下降算法运行会比较慢

如果学习率过大,梯度下降算法可能运行过头,最终导致算法不能收敛

如果使用固定的学习率,梯度下降算法运行到局部最小值后不会再变化。因为到达局部最小值的附近后,梯度下降的每一步会变得更小,更新的值也会逐渐变小。

通过损失值随着迭代次数的变化可以看出一些错误:

  1. 随着迭代次数增加,损失值波动上升下降——代码有问题或者学习率过大
  2. 随着迭代次数增加,损失值一直上升——学习率过大或代码有问题(可能每一次的计算符号反了)
  3. 如果很长时间不收敛,可能是学习率太小了

将学习率调整足够小,损失值在每一次迭代的过程中都会减小

多元线性回归

表示第个特征,表示特征的数量

表示第个训练样本的全部特征,表示第个训练样本中的第个特征

,则

可以通过Numpy的向量化进行计算

特征缩放

当具有不同的值范围的不同特征时,可能会导致梯度下降算法运行较慢

需要对不同的特征重新缩放到相同或相似的范围

均值归一化:,可以缩放到的范围内

Z-score归一化:

逻辑回归(分类问题)

sigmoid函数:

逻辑回归:,用概率的形式表达:

不同的决策边界:

逻辑回归损失函数:

简化写法:

欠拟合和过拟合

欠拟合:函数对于训练集的拟合效果不好——高偏差(high bias)

过拟合:函数对于训练集的拟合效果好,对于测试集的效果不好——高方差(high variance)

避免过拟合的方法:

  1. 收集更多的训练数据
  2. 从全部的特征中挑选最相关的特征进行训练
  3. 正则化——减小某一参数对拟合函数的影响

正则化

通过将损失函数加上特别大的常数与某一参数的乘积,使得这个参数在优化的过程中变得非常小

例如回归问题:

由于不知道哪些特征是比较重要的,哪些特征不重要,因此加上参数平方求和的正则项,让优化算法自行选择。

对于线性回归来说:

进一步推导:

因此正则项的加入实际上相当于将参数略微减小

资料

第一周课件和代码

Notebooks Week 1

第二周课件和代码

Notebooks Week 2

第三周课件和代码

Notebooks Week 3

作业代码

Exercise 1

Exercise 2

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Supervised Learning + + + +
+ + + + + My Previous Love + + /2022/07/28/about-my-previous-love/ + + 记于2022年7月28日,已于2022年9月24日正式分手,公开于2023年11月19日

青岛的五天旅行结束了,251天后的初次见面,美好的时光总是短暂。

回家后心里一直不太舒服,一直在胡思乱想,想着想着有时还偷偷抹抹眼泪。父母也是真的了解我,虽然并没有表现出什么,一直在不断追问我怎么了。当然就算有明确的原因也不会说,对爸妈只能是报喜不报忧,何况我现在也不知道我为什么这样。

可能是不舍得吧,分开了251天,再次见面的时间只有短短的五天,下一次见面还不知道什么时候。

可能是由于毕业季的几乎分手吧,可能现在自己的信心没有那么足了。

可能是对自己未来的迷茫吧,本科取得了不错的成绩,研究生一切从头开始,不知该从何做起。

这一段时间,对我影响最大的就是那一次的几乎分手。女孩子真的需要陪伴,异地太久了,感情是真的会变淡的。而且之前并没有很明确的聊过未来的规划。可能随口的一句“杭州南京”,就成为了一道跨不过去的坎。

我出生在东北的一个小城,从小的梦想就是要走出去,给我自己,甚至给我的下一代创造一个更好的生活环境。高二那年清华暑校遇到全国的优秀学生,发现不同地区顶尖学生之间的差异居然也有如此之大,更加坚定了我走出去的决心。我也很庆幸在高考失利的情况下能选到一个好专业,在房价居高不下的大环境下,至少目前来看毕业后的薪资还是非常有竞争力的。

我很开心可以遇到我的女朋友,我们在一无所有的情况下愿意去尝试。我也从此有了另外的一个前进动力,从高考失利和大一的挫折中走了出来,拿下了年级排名和无数的竞赛奖项、荣誉称号,成功保研。保研的时候也没有选择华师大,想着自己应该获得更好的学历,以后赚更多的钱,才能和她一起有更好的生活。我按照我的道路一步一步在走。

然而由于我早去北京的提前异地,我们之间的沟通就少了许多。地理上的距离造成了心的距离,找到了一个很好的教师编职位后,她便产生了分手的想法。整个过程我甚至都是毫不知情的状态。虽然靠着一条时间轴挽回,但是我需要对自己做一个深刻的反思。我自认为我的爱没有变,但是异地半年多,确实很难将爱表达出来,同时也忽略了她的感受,我们之间的交流变得更少,最终导致了单方面无吵架的分手。

能有一个爱人时刻陪伴在身边,确实是一件非常美好的事情。才分开两天,五天的回忆一波一波涌上心头,真的很难受。想起她忘记带伞的时候,只能躲在小店内等待雨停,却无法等到一个送伞的我。异地恋真的难熬。然而这还不到一年的时间。最少需要三年才能奔现,要是找一份更高薪的工作,甚至需要五年的时间,我才能在合肥站稳脚跟,真正地和她在一起。“所以你就选定我了是嘛”“是的”“为什么呢”“。。。”是啊,为什么呢,我回答不上来。后来我也认真考虑了很久,我是一个纯理性思维的人,这一次我选择听从我的心。我相信我三年前的选择,不管是现在,三年后,三十年后,我还会作出同样的选择。

我是一个很坚定的人,我作出了选择,就会坚定的走下去。这几年我会尽全力维护这一段感情,改正掉我之前的错误,尽量多见面,尽量提升自己以后拿到更好的薪资,尽量多关心,多询问她的感受。三年前我还是一个懵懂无知的学生,我不知道三年后,甚至五年后我会成为什么样的人,但是我的爱是永远不变的。

如果她熬不住了,我会坦然接受。因为我知道,我才是那个最对不起她的人。长三角省会城市工作稳定,我又何德何能拴住她数年的时间,忍受着屏幕那边可有可无的关心,忍受着几个月甚至半年才有的一次短短几天的见面。

我真的希望最终我们可以幸福地走到一起。

为你,千千万万遍。

]]>
+ + + + + Life + + + + + + + Ziyaooo + + Love + + + +
+ + + + + Travel List + + /2022/07/13/travel-list/ + + 旅行物品清单

高铁新规

jqsL6K.md.png

必备物品

  1. 身份证、学生证(本科的估计也行)、手机(足够流量)
  2. 足量的口罩
  3. 一点点现金

生活用品

  1. 手纸、面巾纸等、塑料袋
  2. 洗漱包(牙具)、毛巾
  3. 水杯(可选)
  4. 雨伞(或雨衣)

药品

  1. 消炎药
  2. 腹泻药
  3. 感冒发烧药

衣物

  1. 2-3套换洗的内衣、袜子等
  2. 应季适量外衣外裤
  3. 被褥、蚊帐等(若目的地不提供)

电子产品

  1. 手机充电线(器)、充电宝、充电宝充电器
  2. 笔记本电脑(充电器)、iPad(充电器)
  3. 耳机
  4. 电蚊香(液)
  5. 剃须刀
  6. 插排(若目的地不提供)
]]>
+ + + + + Travel + + + + +
+ + + + + Mathematics for Machine Learning: Linear Algebra + + /2022/07/12/Coursera/Mathematics-for-Machine-Learning-Linear-Algebra/ + + 数学在机器学习领域的应用一:线性代数

开始学习

总是觉得自己数学有一点差,可能是因为上大学学习的时候题目做的比较少,我的脑子又不太灵光,因此一直不能很好的理解数学相关的一些公式、定理等,平时编程的时候尽量找简单的方法绕开复杂的数学公式。假期有时间了,试一下帝国理工的线性代数课程,注重记录,注重理解。这也是第一次看没有中文字幕的全英文课。加油!

课程简介

In this course on Linear Algebra we look at what linear algebra is and how it relates to vectors and matrices. Then we look through what vectors and matrices are and how to work with them, including the knotty problem of eigenvalues and eigenvectors, and how to use these to solve problems. Finally we look at how to use these to do fun things with datasets - like how to rotate images of faces and how to extract eigenvectors to look at how the Pagerank algorithm works.

Since we’re aiming at data-driven applications, we’ll be implementing some of these ideas in code, not just on pencil and paper. Towards the end of the course, you’ll write code blocks and encounter Jupyter notebooks in Python, but don’t worry, these will be quite short, focussed on the concepts, and will guide you through if you’ve not coded before.

At the end of this course you will have an intuitive understanding of vectors and matrices that will help you bridge the gap into linear algebra problems, and how to apply these concepts to machine learning.

什么是线性代数

Linear algebra is a mathematical system for manipulating vectors in the spaces described by vectors.

Linear algebra is linear, because it just takes input values, and multiplies them by constants, everything is linear.

Linear algebra is algebra, that is it’s a notation describing mathematical objects and a system of manipulating those notations.

How vectors are transformed by matrices is the heart of linear algebra.

为什么我们需要线性代数?

  1. 让计算机快速求解多元方程组
    例如:多元方程组,可以转换为,然后进行求解。
  2. 为数据拟合方程
    随着神经网络和机器学习的发展,并不仅仅是拟合方程,最好还能在已有方程曲线的前提下,找到最佳的拟合参数,从而更适用于当前的数据。描述一个方程的各种参数可以使用一个向量来表示,我们希望通过某种方式,数据科学或者机器学习的方式来找到最佳的拟合参数。

向量(Vector)

在计算机科学中,向量被认为是描述一个物体的属性的集合。

向量的基本操作

向量有两种操作:向量与向量之间的加法,以及向量与标量之间的乘法。

向量与向量之间的加法满足结合律(associativity)。

向量与标量之间的乘法,要将标量与向量中的每一个属性相乘

向量的其他运算

如果不以坐标系的角度去观察向量,那么一个向量由两个属性构成:向量的方向和向量的模长

向量的模长指的是向量各组成成分的平方和开根号

向量的点乘指的是向量对应位置的数值相乘之和,满足交换律(commutative)

同时满足向量的加法分配律(distributive over addition),即

向量与标量相乘满足结合律和交换律,即

向量模长与点乘之间的关系:向量自身的点乘与模长的平方相等,即

向量的余弦定理:

向量投影(projection):

上的投影标量(scalar projection)=

上的投影向量(vector projection)= scalar projection * 单位向量 =

向量投影是一个标量,但是,如果需要投影向量的方向,直接与被投影的单位向量相乘即可。

向量的坐标系

两个不共线的向量可以确定一个坐标系(coordinate system)。要描述一个向量,首先要定义一个坐标系,决定坐标系的是基向量

基向量是维的向量集合,需要满足3个条件:

  1. 维的向量彼此之间不线性相关,也就是线性独立的维向量。
  2. 可以扩展到整个空间。
  3. 空间是维的。

虽然并不要求基向量正交,但是如果它们正交,会为解决数学问题带来很大的方便。

如果二维的基向量互相垂直,转换坐标系只需将向量投影到转换后的基向量,计算数值即可。

设原始坐标系,转换后的基向量

首先验证是否垂直,

然后将待转换的向量,对的投影为,这个投影除以的模长,即方向的投影为2个长度。同理,即方向的投影为0.5个长度。

从而得出,最终计算得

找到一个合适的坐标系,帮助我们解决数学问题,是非常重要的。

矩阵(Matrices)

矩阵与向量相乘,相当于将向量转换到不同的坐标系。

矩阵的乘法满足结合律,但是不满足交换律.

,相当于将转换到了

,相当于将转换到了

通过矩阵的转换实际上可以看作不同转换向量之间的和。

如果我们对做这个矩阵的变换,则可以推导:

.

单位矩阵(identity matrix)不对向量做任何变换

设单位矩阵为待求根,

根据逆矩阵的定义,

因此,即

通过初等行变换求解逆矩阵:

对于二维矩阵来说,它的逆矩阵是

二维行列式(determinant):

行列式为0的矩阵,维度不满足当前矩阵的维度,因此在矩阵操作前要首先检查行列式

矩阵的转置:,正交矩阵,则,且正交矩阵的行列式为-1或1。

爱因斯坦求和约定(Einstein summation convention)

,则

是由中的某一行与中的某一列相乘求和后填充的矩阵。

因此即为爱因斯坦求和约定的表示法。

矩阵坐标系的转换

设原始坐标系,现在有另外一个坐标系,坐标系在原始坐标系下基向量表示为

如果将坐标系下的向量转换到原始坐标系中,则为

反之,将原始坐标系中的向量转换到坐标系下,则

如果基向量是正交的,可以使用投影来实现坐标系的转换:

设原始坐标系,现在有另外一个坐标系,坐标系在原始坐标系下基向量表示为

则将坐标系下的向量转换到原始坐标系中,通过投影实现:

,因此在原始坐标系下的向量为

施密特正交化(Gram–Schmidt process)

正交的基向量会给我们解决问题带来很多的方便,需要一种方法将基向量转换为正交的基向量。

设原始的维基向量为

特征问题(Eigenproblems)

对特征向量的直观感受:在进行变换的时候方向仍然保持不变的向量。

为特征向量,为特征值。

求特征值,即的行列式为0

对角矩阵(diagonal matrix)会使矩阵的乘法变得更加容易,

因此可以通过特征值与特征向量的转换,将矩阵转化为对角矩阵,然后求矩阵的幂。

设特征向量,特征值的对角矩阵

矩阵

编程练习

判断一个矩阵是奇异矩阵(singular)还是非奇异矩阵

# GRADED FUNCTIONimport numpy as np# Our function will go through the matrix replacing each row in order turning it into echelon form.# If at any point it fails because it can't put a 1 in the leading diagonal,# we will return the value True, otherwise, we will return False.# There is no need to edit this function.def isSingular(A) :    B = np.array(A, dtype=np.float_) # Make B as a copy of A, since we're going to alter it's values.    try:        fixRowZero(B)        fixRowOne(B)        fixRowTwo(B)        fixRowThree(B)    except MatrixIsSingular:        return True    return False# This next line defines our error flag. For when things go wrong if the matrix is singular.# There is no need to edit this line.class MatrixIsSingular(Exception): pass# For Row Zero, all we require is the first element is equal to 1.# We'll divide the row by the value of A[0, 0].# This will get us in trouble though if A[0, 0] equals 0, so first we'll test for that,# and if this is true, we'll add one of the lower rows to the first one before the division.# We'll repeat the test going down each lower row until we can do the division.# There is no need to edit this function.def fixRowZero(A) :    if A[0,0] == 0 :        A[0] = A[0] + A[1]    if A[0,0] == 0 :        A[0] = A[0] + A[2]    if A[0,0] == 0 :        A[0] = A[0] + A[3]    if A[0,0] == 0 :        raise MatrixIsSingular()    A[0] = A[0] / A[0,0]    return A# First we'll set the sub-diagonal elements to zero, i.e. A[1,0].# Next we want the diagonal element to be equal to one.# We'll divide the row by the value of A[1, 1].# Again, we need to test if this is zero.# If so, we'll add a lower row and repeat setting the sub-diagonal elements to zero.# There is no need to edit this function.def fixRowOne(A) :    A[1] = A[1] - A[1,0] * A[0]    if A[1,1] == 0 :        A[1] = A[1] + A[2]        A[1] = A[1] - A[1,0] * A[0]    if A[1,1] == 0 :        A[1] = A[1] + A[3]        A[1] = A[1] - A[1,0] * A[0]    if A[1,1] == 0 :        raise MatrixIsSingular()    A[1] = A[1] / A[1,1]    return A# This is the first function that you should complete.# Follow the instructions inside the function at each comment.def fixRowTwo(A) :    # Insert code below to set the sub-diagonal elements of row two to zero (there are two of them).    A[2] = A[2] - A[2,0] * A[0]    A[2] = A[2] - A[2,1] * A[1]    # Next we'll test that the diagonal element is not zero.    if A[2,2] == 0 :        # Insert code below that adds a lower row to row 2.        A[2] = A[2] + A[3]        # Now repeat your code which sets the sub-diagonal elements to zero.        A[2] = A[2] - A[2,0] * A[0]        A[2] = A[2] - A[2,1] * A[1]    if A[2,2] == 0 :        raise MatrixIsSingular()    # Finally set the diagonal element to one by dividing the whole row by that element.    A[2] = A[2] / A[2,2]    return A# You should also complete this function# Follow the instructions inside the function at each comment.def fixRowThree(A) :    # Insert code below to set the sub-diagonal elements of row three to zero.    A[3] = A[3] - A[3,0] * A[0]    A[3] = A[3] - A[3,1] * A[1]    A[3] = A[3] - A[3,2] * A[2]    # Complete the if statement to test if the diagonal element is zero.    if A[3,3] == 0:        raise MatrixIsSingular()    # Transform the row to set the diagonal element to one.    A[3] = A[3] / A[3,3]    return A
A = np.array([        [2, 0, 0, 0],        [0, 3, 0, 0],        [0, 0, 4, 4],        [0, 0, 5, 5]    ], dtype=np.float_)isSingular(A)A = np.array([        [0, 7, -5, 3],        [2, 8, 0, 4],        [3, 12, 0, 5],        [1, 3, 1, 3]    ], dtype=np.float_)isSingular(A)fixRowZero(A)fixRowOne(A)fixRowTwo(A)fixRowThree(A)

施密特正交化

# GRADED FUNCTIONimport numpy as npimport numpy.linalg as laverySmallNumber = 1e-14 # That's 1×10⁻¹⁴ = 0.00000000000001# Our first function will perform the Gram-Schmidt procedure for 4 basis vectors.# We'll take this list of vectors as the columns of a matrix, A.# We'll then go through the vectors one at a time and set them to be orthogonal# to all the vectors that came before it. Before normalising.# Follow the instructions inside the function at each comment.# You will be told where to add code to complete the function.def gsBasis4(A) :    B = np.array(A, dtype=np.float_) # Make B as a copy of A, since we're going to alter it's values.    # The zeroth column is easy, since it has no other vectors to make it normal to.    # All that needs to be done is to normalise it. I.e. divide by its modulus, or norm.    B[:, 0] = B[:, 0] / la.norm(B[:, 0])    # For the first column, we need to subtract any overlap with our new zeroth vector.    B[:, 1] = B[:, 1] - B[:, 1] @ B[:, 0] * B[:, 0]    # If there's anything left after that subtraction, then B[:, 1] is linearly independant of B[:, 0]    # If this is the case, we can normalise it. Otherwise we'll set that vector to zero.    if la.norm(B[:, 1]) > verySmallNumber :        B[:, 1] = B[:, 1] / la.norm(B[:, 1])    else :        B[:, 1] = np.zeros_like(B[:, 1])    # Now we need to repeat the process for column 2.    # Insert two lines of code, the first to subtract the overlap with the zeroth vector,    # and the second to subtract the overlap with the first.    B[:, 2] = B[:, 2] - B[:, 2] @ B[:, 0] * B[:, 0]    B[:, 2] = B[:, 2] - B[:, 2] @ B[:, 1] * B[:, 1]      # Again we'll need to normalise our new vector.    # Copy and adapt the normalisation fragment from above to column 2.    if la.norm(B[:, 2]) > verySmallNumber :        B[:, 2] = B[:, 2] / la.norm(B[:, 2])    else :        B[:, 2] = np.zeros_like(B[:, 2])    # Finally, column three:    # Insert code to subtract the overlap with the first three vectors.    B[:, 3] = B[:, 3] - B[:, 3] @ B[:, 0] * B[:, 0]    B[:, 3] = B[:, 3] - B[:, 3] @ B[:, 1] * B[:, 1]       B[:, 3] = B[:, 3] - B[:, 3] @ B[:, 2] * B[:, 2]      # Now normalise if possible    if la.norm(B[:, 3]) > verySmallNumber :        B[:, 3] = B[:, 3] / la.norm(B[:, 3])    else :        B[:, 3] = np.zeros_like(B[:, 3])    # Finally, we return the result:    return B# The second part of this exercise will generalise the procedure.# Previously, we could only have four vectors, and there was a lot of repeating in the code.# We'll use a for-loop here to iterate the process for each vector.def gsBasis(A) :    B = np.array(A, dtype=np.float_) # Make B as a copy of A, since we're going to alter it's values.    # Loop over all vectors, starting with zero, label them with i    for i in range(B.shape[1]) :        # Inside that loop, loop over all previous vectors, j, to subtract.        for j in range(i) :            # Complete the code to subtract the overlap with previous vectors.            # you'll need the current vector B[:, i] and a previous vector B[:, j]            B[:, i] = B[:, i] - B[:, i] @ B[:, j] * B[:, j]        # Next insert code to do the normalisation test for B[:, i]        if la.norm(B[:, i]) > verySmallNumber :            B[:, i] = B[:, i] / la.norm(B[:, i])        else :                B[:, i] = np.zeros_like(B[:, i])    # Finally, we return the result:    return B# This function uses the Gram-schmidt process to calculate the dimension# spanned by a list of vectors.# Since each vector is normalised to one, or is zero,# the sum of all the norms will be the dimension.def dimensions(A) :    return np.sum(la.norm(gsBasis(A), axis=0))
V = np.array([[1,0,2,6],              [0,1,8,2],              [2,8,3,1],              [1,-6,2,3]], dtype=np.float_)gsBasis4(V)# Once you've done Gram-Schmidt once,# doing it again should give you the same result. Test this:U = gsBasis4(V)gsBasis4(U)# Try the general function too.gsBasis(V)# See what happens for non-square matricesA = np.array([[3,2,3],              [2,5,-1],              [2,4,8],              [12,2,1]], dtype=np.float_)gsBasis(A)dimensions(A)B = np.array([[6,2,1,7,5],              [2,8,5,-4,1],              [1,-6,3,2,8]], dtype=np.float_)gsBasis(B)dimensions(B)# Now let's see what happens when we have one vector that is a linear combination of the others.C = np.array([[1,0,2],              [0,1,-3],              [1,0,2]], dtype=np.float_)gsBasis(C)dimensions(C)

镜面投影

# PACKAGE# Run this cell first once to load the dependancies.import numpy as npfrom numpy.linalg import norm, invfrom numpy import transposefrom readonly.bearNecessities import *# GRADED FUNCTION# You should edit this cell.# In this function, you will return the transformation matrix T,# having built it out of an orthonormal basis set E that you create from Bear's Basis# and a transformation matrix in the mirror's coordinates TE.def build_reflection_matrix(bearBasis) : # The parameter bearBasis is a 2×2 matrix that is passed to the function.    # Use the gsBasis function on bearBasis to get the mirror's orthonormal basis.    E = gsBasis(bearBasis)    # Write a matrix in component form that performs the mirror's reflection in the mirror's basis.    # Recall, the mirror operates by negating the last component of a vector.    # Replace a,b,c,d with appropriate values    TE = np.array([[1, 0],                   [0, -1]])    # Combine the matrices E and TE to produce your transformation matrix.    T = E @ TE @ inv(E)    # Finally, we return the result. There is no need to change this line.    return T# First load Pyplot, a graph plotting library.%matplotlib inlineimport matplotlib.pyplot as plt# This is the matrix of Bear's basis vectors.# (When you've done the exercise once, see what happns when you change Bear's basis.)bearBasis = np.array(    [[1,   -1],     [1.5, 2]])# This line uses your code to build a transformation matrix for us to use.T = build_reflection_matrix(bearBasis)# Bear is drawn as a set of polygons, the vertices of which are placed as a matrix list of column vectors.# We have three of these non-square matrix lists: bear_white_fur, bear_black_fur, and bear_face.# We'll make new lists of vertices by applying the T matrix you've calculated.reflected_bear_white_fur = T @ bear_white_furreflected_bear_black_fur = T @ bear_black_furreflected_bear_face = T @ bear_face# This next line runs a code to set up the graphics environment.ax = draw_mirror(bearBasis)# We'll first plot Bear, his white fur, his black fur, and his face.ax.fill(bear_white_fur[0], bear_white_fur[1], color=bear_white, zorder=1)ax.fill(bear_black_fur[0], bear_black_fur[1], color=bear_black, zorder=2)ax.plot(bear_face[0], bear_face[1], color=bear_white, zorder=3)# Next we'll plot Bear's reflection.ax.fill(reflected_bear_white_fur[0], reflected_bear_white_fur[1], color=bear_white, zorder=1)ax.fill(reflected_bear_black_fur[0], reflected_bear_black_fur[1], color=bear_black, zorder=2)ax.plot(reflected_bear_face[0], reflected_bear_face[1], color=bear_white, zorder=3);

jhnVED.md.png

PageRank

# PACKAGE# Here are the imports again, just in case you need them.# There is no need to edit or submit this cell.import numpy as npimport numpy.linalg as lafrom readonly.PageRankFunctions import *np.set_printoptions(suppress=True)# GRADED FUNCTION# Complete this function to provide the PageRank for an arbitrarily sized internet.# I.e. the principal eigenvector of the damped system, using the power iteration method.# (Normalisation doesn't matter here)# The functions inputs are the linkMatrix, and d the damping parameter - as defined in this worksheet.# (The damping parameter, d, will be set by the function - no need to set this yourself.)def pageRank(linkMatrix, d) :    n = linkMatrix.shape[0]    M = d * linkMatrix + (1-d)/n * np.ones([n, n])    r = 100 * np.ones(n) / n    lastR = r    r = M @ r    i = 0    while la.norm(lastR - r) > 0.01 :        lastR = r        r = M @ r        i += 1      return r
# Use the following function to generate internets of different sizes.generate_internet(5)# Test your PageRank method against the built in "eig" method.# You should see yours is a lot faster for large internetsL = generate_internet(10)pageRank(L, 1)# Do note, this is calculating the eigenvalues of the link matrix, L,# without any damping. It may give different results that your pageRank function.# If you wish, you could modify this cell to include damping.# (There is no credit for this though)eVals, eVecs = la.eig(L) # Gets the eigenvalues and vectorsorder = np.absolute(eVals).argsort()[::-1] # Orders them by their eigenvalueseVals = eVals[order]eVecs = eVecs[:,order]r = eVecs[:, 0]100 * np.real(r / np.sum(r))# You may wish to view the PageRank graphically.# This code will draw a bar chart, for each (numbered) website on the generated internet,# The height of each bar will be the score in the PageRank.# Run this code to see the PageRank for each internet you generate.# Hopefully you should see what you might expect# - there are a few clusters of important websites, but most on the internet are rubbish!%pylab notebookr = pageRank(generate_internet(100), 0.9)plt.bar(arange(r.shape[0]), r);

资料

Formula Sheet: Sheet summarising all the formulae covered in this course.

Code and Notebooks

]]>
+ + + + + Study + + + + + + + Python + + Machine Learning + + Linear Algebra + + + +
+ + + + + Trip To Qingdao + + /2022/07/11/trip-to-qingdao/ + + 青岛旅行计划

防疫政策

理论上的防疫政策:低风险地区提前三天向酒店等报备,第一天和第三天两次核酸。

实际:基本只看“青岛一码通”的绿码和7天内核酸阴性报告(有的地方可能要48小时核酸阴性报告)

具体措施:

  1. 在家做好核酸,时间越晚越好(当然上车前必须要出结果),带好电子版或者纸质版报告,提前申请“青岛一码通”。
  2. Day0 从青岛北站出来应该有核酸检测的点位,如果没有就去台东北侧的“青岛市海慈医疗集团”(公众号:青岛市海慈医疗集团)做24小时核酸。
  3. 保证 Day2 和 Day3 至少分别做一次核酸,青岛出结果比较慢,要第二天才能出。
  4. 公交地铁景区基本都要看“青岛一码通”的绿码和7天内核酸阴性报告,不要下载“青岛地铁APP”(青岛地铁APP要核验山东省电子健康卡,而申请山东省电子健康卡需要“入鲁申报”,为了减少不必要的麻烦这个不做),公交可以在支付宝或者云闪付申请电子公交码,地铁直接在地铁站买票进站,景区提前预约
  5. 要是“青岛一码通”变黄码就BBQ了,应该不会的。

总计划

日期计划备注
Day0晚上到达青岛,做核酸、住酒店可以去旁边的丽达生活超市买一些水和吃的
Day1上午信号山公园、栈桥
Day1中午王姐烧烤午餐
Day1下午小青岛公园、鲁迅公园、小鱼山公园、青岛第一海水浴场、八大关风景区
Day1晚上台东步行街小吃晚餐、回酒店可以去大商场买一点点吃的和水果等
Day2上午小麦岛公园
Day2中午船歌鱼水饺午餐
Day2下午燕儿岛公园、奥帆中心、情人坝、五四广场、海天云上艺术中心看天气,太热了就先去海天云上艺术中心
Day2晚上探海岛海鲜自助(探鲜岛海鲜自助餐厅)晚餐、栈桥附近的夜景、回酒店
Day3上午去崂山风景区
Day3中午吃一些提前买的面包等,景区内应该也有一些吃的
Day3下午崂山风景区、回市区
Day3晚上前海沿晚餐、回酒店
Day4返程

备注:

  1. 景点备选:鲁迅公园附近的青岛海底世界和海军博物馆(太热了可以去避避暑)(可能都预约不上的)
  2. 美食备选:火车站附近:无名小吃(似乎关门了,推荐油焖大虾,扇贝)和白玉餐厅(鱿鱼、茄子);石老人海水浴场附近的马扎子烧烤;五四广场附近的开海;台东的湘西石锅鱼和大叹号韩式烤肉;双和园水饺;还可以去买活海鲜(团岛农贸市场、营口海鲜市场、埠西市场)。要 no 尝尝崂山可乐和崂山白花蛇草水。早上要吃点东西,面包或者出去买一点点早餐。
  3. 预计支出:车票1800;吃饭128+100+158+178+150+…≈800;酒店200*4≈800;门票、交通≈500。总共3900。小荷包还有4405.73,应该可以cover全部支出
  4. 实际支出:车票391.5+462.5+412.5+19.5+63+339.82+15=1703.82;酒店838;门票10+298+40;

青岛景点总览图

jcdeWF.md.png

酒店附近地图

jqylcV.md.png

Day 1

信号山公园

  • 交通方式:酒店——信号山公园,公交前往,37分钟,步行680米

    jq66MV.md.png

    jqg8nf.md.png

  • 预约:已经预约好 7月24日 6:00-20:30,包括收费5元的旋转观景楼

  • 时间:1个小时左右

  • 简介:信号山公园位于青岛市中心,因曾在山顶建有为船只引航的信号台而得名。信号山海拔98米,山顶三幢红顶蘑菇楼尤为显眼,其中较高的一幢是旋转观景楼,在这里你可以360度俯看青岛“红瓦绿树,碧海蓝天”的景色。进入景区大门,南坡上有踏浪观景台,就在连心桥下面一点,是拍摄南边德国古堡式建筑迎宾馆的好位置。连心桥上一把把红色爱心造型的锁挂在绿色栏杆上,情侣们可以在此买一把同心锁把彼此的山盟海誓锁在信号山上,据说手拉手走过连心桥可以得到祝福,单身的话自个儿的左手拉右手一样很好!再往前,可以看看五龙潭喷泉等景点,周围四条小龙围着中间一条大龙,与信号山又叫五龙山对应,因为山周边有龙江路、龙华路、龙口路、龙山路、伏龙路五条带“龙”字的路而得此别名。最后到达山顶的旋转观景楼,登上楼上观景台观景,一幢幢掩映在绿树中红瓦黄墙的德式建筑令人惊叹。往西南看,近处有绿色钟楼屋顶的基督教堂在一片红屋顶中非常出挑。

栈桥

  • 交通方式:信号山公园——栈桥,步行前往,2公里路程,10分钟。
    jq2aVO.md.png
  • 预约:无需预约,包括栈桥与回澜阁,一说回澜阁8:30-16:30开放
  • 时间:预计半小时左右
  • 简介:栈桥位于青岛中心城区的南部海滨,是一条440米长的海上长廊,从陆地延伸入海中。回澜阁里面有一块无字碑,这块石碑的来历至今众说纷纭。现在,阁内通过主题展陈的方式,全面展示青岛近现代历史、人文、民俗等独特城市风貌。栈桥两边有铁链护栏和莲花路灯,游人漫步于栈桥海滨,风平浪静时,可观看海鸥在此自由翱翔。走到桥的尽头还可远眺小青岛。岛上树影婆娑、绿荫成群,一座白灯塔亭亭玉立。涨潮时,惊涛拍打着防波堤,激起簇簇浪花,可驻足观看。退潮时,赭色岩礁和金色沙滩露出水面,可走下栈桥,漫步在海滩上赶海拾贝。

王姐烧烤

  • 交通方式:栈桥——王姐烧烤,步行前往,1.2公里路程,16分钟。

    jqRFeK.md.png

  • 美团可以直接订座,重点菜:辣炒蛤蜊

小青岛公园——鲁迅公园——小鱼山公园——青岛第一海水浴场

  • 交通方式:步行,共3公里左右

    j6slDJ.md.png

  • 预约:小鱼山公园开放时间08:00-17:00,网上找不到预约入口

  • 时间:2-3小时

  • 简介:小青岛故称为“琴岛”,是青岛湾内的一座小岛,青岛这个城市的名称就来源于它。小青岛与陆地之间有长长的海堤相接,岛上矗立着德国人建于1898年的灯塔,是青岛的标志之一。小青岛面积很小,岛上绿树成荫,岛的四周礁石环绕,海水清澈、蔚蓝,岛上常能见到来垂钓的游客。坐在礁石上吹吹海风,赤脚踩踩海水,看看四周青岛湾边林立的高楼和红顶的小洋房,仿佛置身于海上花园。每当夕阳西下时景色尤其美,阳光把整个海湾都镀成了金色。小青岛的南侧有一尊姿态优美的琴女雕像,雕像周围是花坛,种植着五颜六色的鲜花。岛的较高处有当年德国人建造的灯塔,整个岛的海拔也不高,才17米,走到灯塔脚下不需要爬多少路。灯塔通体洁白,由大理石构筑,是海上过往船只进出胶州湾的重要航标。每当夜幕降临,灯塔与岛上的灯光倒映在海面上,像一匹飘动的彩绸,形成青岛的一大胜景“琴屿飘灯”,在这里拍摄夜景很不错。鲁迅公园是青岛市内一处对外开放的临海公园,海边有大片的红褐色礁石,景色很特别,常有不少新人在此拍摄婚纱照。沿着海边步道慢慢走、听听海浪拍壁之声,或是走上岩石高处的亭子远眺大海,很是惬意。公园的东部紧邻青岛海底世界,再往东走是第一海水浴场,沿途风光很美。小鱼山公园是青岛佳风景眺望点之一,一是因为它位于市中心,是青岛离海近的一座山,地理位置;二是因为它的海拔仅60米,爬山不累,登到山顶能看到“红瓦绿树,碧海蓝天”具青岛代表性的景色。

八大关风景区

  • 交通方式:步行,毗邻青岛第一海水浴场

  • 景区图:

    j6g5lV.md.png

  • 预约:无需预约,内部场馆单独售票,营业时间:9:00-17:00;换票时间:9:00-15:00

  • 时间:2小时

  • 简介:八大关是青岛市区南部相交错的十几条马路的总称,它们大多取名于我国知名关隘的名称。这里环境清幽,春季有碧桃盛开、夏季有紫薇盛放,秋季可见银杏红枫夹道,还坐落着许多各国风格的别墅,是摄影胜地。在这里,你可以进入欧洲古堡风格的“花石楼”参观、登上楼顶遥望大海,或者外观开国元帅住过的日式洋楼“元帅楼”、流传着唯美爱情故事的丹麦建筑“公主楼”等经典别墅,让你仿佛身处欧洲的某个角落。

台东步行街

  • 交通方式:地铁,24分钟,步行248m

    j6WRFH.md.png

  • 预约:无需预约

  • 时间:晚上

  • 简介:“朝观壁画夜赏灯,购物休闲在台东”,台东步行街是青岛有名的街区,街内有国内外知名的沃尔玛、万达广场、百信鞋业、利群集团、苏宁电器、三联家电、亚泰电器、新华书店、医保城等各类业态的企业245家。步行街两侧的21座楼6万余平方米的墙面为统一彩绘,精心绘制了色彩斑斓、造型生动的大型壁画,形成了独特的彩色画廊,这是大型的手工彩绘一条街。台东三路经过精心的景观设置,夜景迷人。这里还有男士、女士特色广场,营造出优美的购物和休闲环境,使市民在购物消费的同时,还享受着文化特色的盛宴。

  • 美食推荐(有人排队多的肯定好吃):

    • 一家烤猪蹄,好像是叫黄金猪蹄。
    • 一家烤冷面,旁边一个蜜雪冰城,对面一家章鱼小丸子。烤冷面、烤粉丝、还有对面的章鱼烧都很好。
    • 一家炸鸡腿,在一个路口上,然后附近有一个杨国福麻辣烫。
    • 有一家面包,就是三角形的,好像是叫三脚猫。
    • 买酱猪蹄带回去(周钦公流亭酱猪蹄)

Day 2

小麦岛公园

  • 交通方式:地铁+公交(打车)(或公交),58分钟,步行1.2公里

    jqI21e.md.png

  • 预约:无需预约

  • 时间:1-2小时

  • 简介:小麦岛公园位于崂山区麦岛路西50米,小麦岛属环形岛屿,有大片平坦宽广的绿化草地,远处就是湛蓝的海水,可在这里眺望到遥远的海岸线,一派海滨美景,非常适宜拍照。

船歌鱼水饺

就在小麦岛公园的公交站旁边,逛后吃午餐。

重点菜:鲅鱼、墨鱼、三鲜、虾仁水饺,海菜凉粉

燕儿岛公园

  • 交通方式:公交34分钟,步行883米

    jcn8gS.md.png

  • 预约:无需预约

  • 时间:1-3小时

  • 简介:燕儿岛山公园位于山东省青岛市南部,处在浮山湾东端,是一个突出海中的岬角。园内环境优美,集生态、景观、文化、健身、休闲等为一体,是市民休闲锻炼、观光游玩的好地方。公园里的海滨栈道是一大亮点,木栈道与阶梯相连,一边是大海,一边是峭壁,峭壁底下鲜花盛开,在这里拍照仿佛置身于美丽的垦丁,有着独特的韵味。登上阶梯高处的平台放眼望去,可以将整个大海纳入眼帘,景色十分迷人。

奥帆中心——情人坝——五四广场

  • 交通方式:步行,直线距离2公里左右

    jcQYR0.md.png

  • 预约:无需预约,奥帆中心其他景点待确定

  • 时间:2-3小时

  • 简介:青岛奥帆中心景区位于青岛市浮山湾畔,与青岛市标志性景点——五四广场近海相望,总占地面积约45公顷,是2008年北京第29届奥运会奥帆赛和第13届残奥会帆船比赛举办场地,奥帆中心景区依山面海,景色宜人,是全国唯一“国家滨海旅游休闲示范区”。青岛被誉为“帆船之都”,作为最能体现青岛城市特色和展示城市形象的景区,奥帆中心景区内不仅有飞帆渺渺的优雅,有青岛十大旧景代表燕岛秋潮,有青岛新时代景观鸥翔彩虹,更有众多惊险刺激的娱乐体验,是游客来青必看的景点。

海天云上艺术中心

  • 交通方式:公交或地铁,20分钟

    jcQ7Wt.md.png

  • 预约:已经预约好 7月25日 9:00-20:00,80F+81F联票

  • 时间:没查到。。。

  • 简介:海天中心城市观光厅是山东省超高层垂直建筑之上的高空观光平台。在这里,向西可揽胜八大关老城区红瓦绿树,向东承接新城区璀璨繁华,360°俯瞰壮美海景、山景、城景,全方位感受身处"天空之城"的独特体验。其内部设置的透明观景区、沉浸式体验区、多媒体展示区与空中水吧等多个功能空间,将内部游览体验与外部自然景观融为一体。站在369米之上的城市观光厅,可以看尽因海而生、向海而兴的魅力青岛在时间长河中的风貌变迁与发展动线。随着观光者的漫步,不同姿态的青岛都将尽收眼底。

探海岛海鲜自助(探鲜岛海鲜自助餐厅)

回青岛站附近吃晚餐,美团可以订座,顺便可以游览栈桥附近的夜景。

Day 3

崂山风景区

  • 交通方式:地铁接公交

  • 预约:已经预约好 7月26日 6:00-12:00太清,12:01-17:30 华严和仰口

  • 时间:一天

  • 路线:大河东检票——第三站下车游览太清宫、太清索道——索道往返——走到垭口乘坐公交618路前往华严(或仰口)——景区游览车到仰口(或华严)——原路返回大河东(或者直接从仰口出去)

  • 崂山风景区地图

    jcaA8e.md.png

    jcUjgJ.md.png

前海沿

这个位置暂定,美团可以排队

jIirk9.md.png

重点菜:蒜蓉粉丝虾、手锤茄子卷饼

]]>
+ + + + + Travel + + + + + + + Ziyaooo + + Love + + + +
+ + + + + 2022保研经历 + + /2022/07/03/postgraduate-recommendation/ + + 2022级推免基本告一段落,刚开始夏令营的时候其实还没有什么疫情,但是各大高校仍然基本选择了线上夏令营,因此造成了夏令营非常非常卷。我本人将比我们学校好的985基本全部报了一遍,总共报了20多个,最后入营的非常少。不幸中的万幸我以为根本不可能考虑我的中科院计算所在我没入营没联系老师的情况下仍然打电话叫我过去考试,最终拿到了offer。计算所offer确认的时间比较早,是9月10日,而经过夏令营入营惨痛的经历,我决定就不参加预推免了。

本人情况

  • 某中流985大数据专业
  • rank:3/65,一等奖学金,优秀学生
  • 竞赛:天梯赛个人二等奖,程序设计竞赛校二,CCF250分;数学建模国赛省三,亚太杯二等奖,美赛H奖;大英赛三等奖;服创国二,计算机设计大赛省二
  • 科研经历:0论文,1国家级大创1校级大创(因为参与度不高全程夏令营都没有提及),跟本科老师做的项目(1专利1软著,论文正在写)
  • 英语:六级540,四级560
  • 最终去向:中科院计算所网数重点实验室学硕
  • 参加的夏令营(均为学硕):
    南京大学计算机科学与技术系(笔试挂)
    北京理工大学计算机学院(优营)
    北京航空航天大学计算机学院(替补)
    天津大学智能与计算学部(优营)
    华东师范大学计算机科学与技术学院(优营)
    华中科技大学计算机科学与技术学院(优营)
    中山大学计算机学院(软件学院)(替补)
  • 没有入营但是去面试的:中科院计算所
  • 入营但是没有去面试的:东南大学计算机科学与工程学院
  • 没有参加预推免

入营经历

南京大学计算机科学与技术系(线上)

南京大学今年养了一个大鱼塘,就拿我们专业来说,65人的小专业前三名都通过了南大的初审。当时接到了南大的邮件激动坏了,然而南大先搞了一波笔试。。。

  • 笔试:笔试的内容是408,27道选择题,一个小时,双机位监考。选择题有单选有多选,多选的选项超过4个。

我实在是太菜了,有一半的题里面的名词都没听说过。。。所以就挂了,也没有然后了。

北京理工大学计算机学院(线上)

  • 宣讲:第一天上午是学院的宣讲,在宣讲的过程中所有的实验室都会拉一个群,下午是实验室的宣讲。不同实验室不一样,大部分也是老师宣讲,有一个实验室的老师直接让想来的在会议中作自我介绍,问问题(公开处刑)。
    有一个印象最深的,一个中北大学的学生作了自我介绍,老师直接问“我有个顾虑:中北大学不太好,所以学生的质量可能也不太好,你来讲讲实验室录取你的理由”就特别直白。。
  • 面试:第二天分组面试,面试分组随机,一个人10分钟左右。自我介绍要使用PPT,可以全程使用英文,也可以先中文再英文(当然是用英文啊。。。混着说多麻烦)。自我介绍完问的问题也很迷,比如“自我介绍为什么用英文”(是你们要求的好吧??)“北理有你的学长学姐吗”等等,有一个和项目相关的,没有专业知识,基本就是在随便聊。。。

面试后就结束了,几周后公布了优营名单,一共三四百入营的,优营给了不到二百个,承诺优营一定录取。

北京航空航天大学计算机学院(线上)

  • 宣讲:北航没有宣讲,给了一个百度网盘的链接,里面是宣讲的视频,可以自己下载看看。
  • 机试:北航的机试可以用CCF认证成绩抵,但是CCF证书上必须标明使用的是C /C++,ALL是不能抵的(所以我考的CCF就没什么用处了)机试是完全闭卷,两个小时两道题,一个是关于结构体排序的40分,一个是关于最小生成树的60分。有一个平台提交试题,但是只验证是否能编译通过,不能返回结果和得分。
  • 面试:机试后刷掉了一小批人,然后面试。面试20分钟,首先是英文自我介绍,然后是数学问题,专业问题,性格测试等。数学问题“请说一下积分和微分含义”,专业问题“满二叉树和完全二叉树的区别”“什么是大数据”“分布式系统主要有哪些方面的内容”“分布式与集群有什么区别”等等。当时好紧张,数学专业问题基本都没答对什么。。老师还一直在大数据的概念上给我扔问题。

北航最后拿到了候补,一共500个入营的,过了机试有400左右,优营给了110多个,候补给了100个。不过整个夏令营的阶段都没有北航的同学参与,而看去年的录取名单基本都是北航的。。不知道是什么原因

天津大学智能与计算学部(线上)

  • 宣讲:学部整体宣讲+实验室宣讲,宣讲之后填志愿,根据志愿安排面试小组
  • 面试:没有专业知识,自我介绍后随便提两个问题就结束了

入营有五百左右,给了三百的优营,要自己联系老师,8月底联系不到的认为放弃优营资格。

华东师范大学计算机科学与技术学院(线上)

  • 宣讲:上午是中目会议宣讲,宣讲过程中有签到,下午是实验室自由宣讲,要填报志愿决定面试小组
  • 机试:华师大的在线oj平台,acm难度,4道题400分,按通过的测试点给分。总共三个小时,可以查阅纸质材料。我只做出来了第一题,剩下的都是大模拟骗分,最终得了273分。平台上可以看到大家的平均成绩,160分左右,不能看到实时排名。
  • 面试:PPT中文自我介绍,专业知识问答,英文文献翻译,自由问答。专业知识包括“TCP与UDP的区别”“如何构造哈夫曼树”,自由问答基本都是项目相关,总体来说难度不大。
    面试后老师打了电话询问能否确定报考华师大,我说不能确定,一个月后又打了电话,当时已经拿到计算所的offer了,就放弃了华师大,最终官网的优营名单中也没有我。入营的只有一百左右,给了五六十优营(应该是打电话后不放弃的都给了我觉得)。

华中科技大学计算机科学与技术学院(线上)

  • 宣讲:上午分实验室B站宣讲
  • 面试:自我介绍+项目问题,没有专业知识,时间比较短

入营只有200左右,大多数给了优营,但是是唯一一个没有后续的学校,没人说过优营有什么用接下来干什么。。。

中山大学计算机学院(软件学院)(线上)

吐槽吐槽!!!!中山真的是太烦人了,就算过了也真心不想去。
入营资格要一个一个电话确认,还没开始夏令营就开了三场会,一个面试环境检查会,一个笔试环境检查会,一个面试分组抽签会。更为奇葩的双机位要求:两个机位互相能看见,次机位看清电脑屏幕,主机位能看到脑袋+肩膀且能看到双手???你来教教我咋能主机位看到双手???我双手举起来编程吗???更为奇葩的机试监考,要共享屏幕到腾讯会议中。好家伙总共五百多人参加机试你找了五百多个研究生坐五百多个电脑前面开五百多个腾讯会议盯着我们???面试环境检查会都已经查看完了承诺书,正式面试还要再看一遍???程序无比繁琐,而且充斥着学校对学生的满满的不信任感!

  • 宣讲:无数宣讲,还要签到
  • 机试:用的中山自己开发的Matrix平台,没有自动补全,三个小时10道题,根本就不是那种oj题,更像C++考试题,评测速度也慢。还有机试考察面向对象的内容不允许用Java不允许用Python奇葩不???没学过C++就直接踢一边了呗?
  • 面试:中文自我介绍,英文问答一个,然后随便问,一些项目相关的知识。最后有个老师问“看你一个本科生搞了这么多竞赛,是不是基础知识掌握的不好啊”然后让我结束会议了???无力吐槽了

五百多人给了300优营和100候补,还承诺优营一定录取,还说不搞预推免了。祝愿中山被鸽穿!

中科院计算所(线下)

中科院计算所本来是没有入营的(意料之中),看QQ群里面的报名号有六千多人报名,入营名单发了400多。但是实验室秘书有一天早上打电话过来希望我能去北京参加机试面试,难得的机会就过去了。在报名后和入营名单公布之前会在QQ群里面让选意向导师,实际上就是意向实验室,才会有实验室秘书联系你,所以要多关注群消息。

  • 宣讲:B站整体宣讲,一个实验室20分钟左右,可线上可线下
  • 机试:宣讲的下午网数实验室组织了机试。两个小时六道题,题目打印好了发给你,在自己笔记本上做,做完后学长学姐用U盘拷贝走,不会的也可以写思路。大家都在一个屋子里面做题,有几个学长学姐在巡视,完全闭卷不允许参考任何资料。
  • 面试:安排的是一个人20分钟,但是普遍延后,我等了超出预定时间一个小时才轮到我。一个长条桌,有十多个老师在对面,英文自我介绍然后就是项目问题、性格测试和政治问题,没有专业知识,面试全程比较愉悦。

面试结束当天晚上就打来了电话并发送了拟录取的邮件。一个月后在官网公布了拟录取的名单,入营的优营与没入营的优营(比如我)各占一半,总共给了200优营,承诺一定录取。其中查了一下,网数实验室面试38进12。

经验总结

  1. 机试能力相当重要,推荐PTA平台,《算法笔记》和《算法笔记上机训练指南》。我在大学期间将《算法笔记》刷了三遍,最后一遍是在天梯赛比赛前一个月与《算法笔记上机训练指南》一起刷完的,收获非常多。由于我们学院选拔ACM队只招大一的,大一当时学的不太好,我没有接触过ACM,但是大三的天梯赛我的分数也能超过一半的ACM队员。如果你是一个大一萌新,一定要尽早接触编程,最好能参加ACM集训体验一下,多练多刷题,毕竟这是互联网行业的敲门砖,硬实力的体现。
  2. 英文能力很重要,至少能清晰表达自己的含义,自我介绍要背熟,中英文的都要准备,也要准备中英文的PPT。
  3. 要多投学校,面试多了自然就不紧张了,而且该背的在面试期间也都背熟了。(如果计算所是第一个面试的我感觉我可能都紧张得说不出话来)。
  4. 夏令营入营的门槛就是学校的title和个人的rank,竞赛什么的一点用都没有,论文不知道能不能有点用。
  5. 夏令营是否要联系老师?我觉得如果你真的想去,可以联系联系,不想去的就不要联系了,免得到时候有心理负担。录取我的计算所我也是全程没有联系老师,甚至在拟录取后玩了一个月我才和研究生导师加上微信说了两句话。
  6. 避坑:明显超发offer的:天津大学智能与计算学部和中山大学计算机学院(软件学院),千万不要堵在这两个学校上,会死得很惨的。
]]>
+ + + + + Experience + + + + + + + Postgraduate + + + +
+ + + + + 隐藏博客 + + /2022/07/03/hidden-blogs/ + +
]]>
+ + + + + Life + + + + + + + diary/Diary + + + +
+ + + + +
diff --git a/page/10/index.html b/page/10/index.html new file mode 100644 index 000000000..a1d8d64ab --- /dev/null +++ b/page/10/index.html @@ -0,0 +1,1036 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/11/index.html b/page/11/index.html new file mode 100644 index 000000000..1cde8be68 --- /dev/null +++ b/page/11/index.html @@ -0,0 +1,1036 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + +
+ + + +
+ + + +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/12/index.html b/page/12/index.html new file mode 100644 index 000000000..958aa0ac0 --- /dev/null +++ b/page/12/index.html @@ -0,0 +1,1036 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/13/index.html b/page/13/index.html new file mode 100644 index 000000000..3f52000a3 --- /dev/null +++ b/page/13/index.html @@ -0,0 +1,1028 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/14/index.html b/page/14/index.html new file mode 100644 index 000000000..5f265218f --- /dev/null +++ b/page/14/index.html @@ -0,0 +1,1011 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/15/index.html b/page/15/index.html new file mode 100644 index 000000000..363e24433 --- /dev/null +++ b/page/15/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 000000000..c9230a217 --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,1014 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 000000000..c86c497f3 --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,1018 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/4/index.html b/page/4/index.html new file mode 100644 index 000000000..f50c89243 --- /dev/null +++ b/page/4/index.html @@ -0,0 +1,1016 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/5/index.html b/page/5/index.html new file mode 100644 index 000000000..d55e106d7 --- /dev/null +++ b/page/5/index.html @@ -0,0 +1,1020 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/6/index.html b/page/6/index.html new file mode 100644 index 000000000..2be63a208 --- /dev/null +++ b/page/6/index.html @@ -0,0 +1,1036 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/7/index.html b/page/7/index.html new file mode 100644 index 000000000..0b707c547 --- /dev/null +++ b/page/7/index.html @@ -0,0 +1,1034 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/8/index.html b/page/8/index.html new file mode 100644 index 000000000..049cfabf2 --- /dev/null +++ b/page/8/index.html @@ -0,0 +1,1028 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/9/index.html b/page/9/index.html new file mode 100644 index 000000000..6eff9ea2f --- /dev/null +++ b/page/9/index.html @@ -0,0 +1,1032 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/placeholder b/placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/tags/Advanced-AI/index.html b/tags/Advanced-AI/index.html new file mode 100644 index 000000000..b1aaa9e8d --- /dev/null +++ b/tags/Advanced-AI/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Advanced AI - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Advanced-AI/page/2/index.html b/tags/Advanced-AI/page/2/index.html new file mode 100644 index 000000000..3300d4dff --- /dev/null +++ b/tags/Advanced-AI/page/2/index.html @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Advanced AI - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Algorithm/index.html b/tags/Algorithm/index.html new file mode 100644 index 000000000..b580ff58c --- /dev/null +++ b/tags/Algorithm/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Algorithm - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Algorithm/page/2/index.html b/tags/Algorithm/page/2/index.html new file mode 100644 index 000000000..c7f0f10d9 --- /dev/null +++ b/tags/Algorithm/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Algorithm - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Algorithm/page/3/index.html b/tags/Algorithm/page/3/index.html new file mode 100644 index 000000000..bc12bf9cd --- /dev/null +++ b/tags/Algorithm/page/3/index.html @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Algorithm - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Backend/index.html b/tags/Backend/index.html new file mode 100644 index 000000000..659d8ce49 --- /dev/null +++ b/tags/Backend/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Backend - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Backend/page/2/index.html b/tags/Backend/page/2/index.html new file mode 100644 index 000000000..6328dd9d6 --- /dev/null +++ b/tags/Backend/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Backend - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Backend/page/3/index.html b/tags/Backend/page/3/index.html new file mode 100644 index 000000000..6e8d99aef --- /dev/null +++ b/tags/Backend/page/3/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Backend - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Backend/page/4/index.html b/tags/Backend/page/4/index.html new file mode 100644 index 000000000..bbb2f1037 --- /dev/null +++ b/tags/Backend/page/4/index.html @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Backend - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/ByteDance/index.html b/tags/ByteDance/index.html new file mode 100644 index 000000000..d83179dbd --- /dev/null +++ b/tags/ByteDance/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - ByteDance - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/ByteDance/page/2/index.html b/tags/ByteDance/page/2/index.html new file mode 100644 index 000000000..8915635d6 --- /dev/null +++ b/tags/ByteDance/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - ByteDance - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/ByteDance/page/3/index.html b/tags/ByteDance/page/3/index.html new file mode 100644 index 000000000..9c8207387 --- /dev/null +++ b/tags/ByteDance/page/3/index.html @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - ByteDance - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 21 篇文章

+
+ + + + +

2023

+ + + +
Go 语言基础 - 基础语法
+
+ +
+ + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/C/index.html b/tags/C/index.html new file mode 100644 index 000000000..a270a7b3c --- /dev/null +++ b/tags/C/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - C++ - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/C/page/2/index.html b/tags/C/page/2/index.html new file mode 100644 index 000000000..94afd1b3c --- /dev/null +++ b/tags/C/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - C++ - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/C/page/3/index.html b/tags/C/page/3/index.html new file mode 100644 index 000000000..c08f7926f --- /dev/null +++ b/tags/C/page/3/index.html @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - C++ - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Calculus/index.html b/tags/Calculus/index.html new file mode 100644 index 000000000..24ec5f544 --- /dev/null +++ b/tags/Calculus/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Calculus - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Competition/index.html b/tags/Competition/index.html new file mode 100644 index 000000000..0f9b0b6d7 --- /dev/null +++ b/tags/Competition/index.html @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Competition - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Consul/index.html b/tags/Consul/index.html new file mode 100644 index 000000000..8a259ec66 --- /dev/null +++ b/tags/Consul/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Consul - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
Consul与Kong联合配置理解
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Diary/index.html b/tags/Diary/index.html new file mode 100644 index 000000000..3b8b68a9e --- /dev/null +++ b/tags/Diary/index.html @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Diary - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Distributed-Systems/index.html b/tags/Distributed-Systems/index.html new file mode 100644 index 000000000..2d521d0f8 --- /dev/null +++ b/tags/Distributed-Systems/index.html @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Distributed Systems - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Go/index.html b/tags/Go/index.html new file mode 100644 index 000000000..ca6c50af3 --- /dev/null +++ b/tags/Go/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Go - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Go/page/2/index.html b/tags/Go/page/2/index.html new file mode 100644 index 000000000..abc882ac1 --- /dev/null +++ b/tags/Go/page/2/index.html @@ -0,0 +1,443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Go - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Hertz/index.html b/tags/Hertz/index.html new file mode 100644 index 000000000..1e3d98b2e --- /dev/null +++ b/tags/Hertz/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Hertz - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
Hertz和Thrift简单示例
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Information-Retrieval/index.html b/tags/Information-Retrieval/index.html new file mode 100644 index 000000000..ed97e10bb --- /dev/null +++ b/tags/Information-Retrieval/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Information Retrieval - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Information-Retrieval/page/2/index.html b/tags/Information-Retrieval/page/2/index.html new file mode 100644 index 000000000..a0efbc42a --- /dev/null +++ b/tags/Information-Retrieval/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Information Retrieval - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Kafka/index.html b/tags/Kafka/index.html new file mode 100644 index 000000000..8ae708be8 --- /dev/null +++ b/tags/Kafka/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Kafka - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
Kafka简单示例
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Kong/index.html b/tags/Kong/index.html new file mode 100644 index 000000000..8774adfd1 --- /dev/null +++ b/tags/Kong/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Kong - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
Consul与Kong联合配置理解
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Leetcode/index.html b/tags/Leetcode/index.html new file mode 100644 index 000000000..b3820efd3 --- /dev/null +++ b/tags/Leetcode/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Leetcode - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Leetcode/page/2/index.html b/tags/Leetcode/page/2/index.html new file mode 100644 index 000000000..f0a5a2029 --- /dev/null +++ b/tags/Leetcode/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Leetcode - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Leetcode/page/3/index.html b/tags/Leetcode/page/3/index.html new file mode 100644 index 000000000..f1567336e --- /dev/null +++ b/tags/Leetcode/page/3/index.html @@ -0,0 +1,407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Leetcode - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Linear-Algebra/index.html b/tags/Linear-Algebra/index.html new file mode 100644 index 000000000..8ddea1162 --- /dev/null +++ b/tags/Linear-Algebra/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Linear Algebra - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2022

+ + + +
Mathematics for Machine Learning: Linear Algebra
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Love/index.html b/tags/Love/index.html new file mode 100644 index 000000000..4682a8d93 --- /dev/null +++ b/tags/Love/index.html @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Love - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 2 篇文章

+
+ + + + +

2022

+ + + +
My Previous Love
+
+ + + + +
Trip To Qingdao
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Machine-Learning/index.html b/tags/Machine-Learning/index.html new file mode 100644 index 000000000..3207b83e3 --- /dev/null +++ b/tags/Machine-Learning/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Machine Learning - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Machine-Learning/page/2/index.html b/tags/Machine-Learning/page/2/index.html new file mode 100644 index 000000000..dc69b8b84 --- /dev/null +++ b/tags/Machine-Learning/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Machine Learning - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Machine-Learning/page/3/index.html b/tags/Machine-Learning/page/3/index.html new file mode 100644 index 000000000..e31547ddb --- /dev/null +++ b/tags/Machine-Learning/page/3/index.html @@ -0,0 +1,407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Machine Learning - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Numpy/index.html b/tags/Numpy/index.html new file mode 100644 index 000000000..f2e0a7da3 --- /dev/null +++ b/tags/Numpy/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Numpy - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2022

+ + + +
Numpy中axis的理解
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Pattern-Recognition-and-Machine-Learning/index.html b/tags/Pattern-Recognition-and-Machine-Learning/index.html new file mode 100644 index 000000000..e2ce71a82 --- /dev/null +++ b/tags/Pattern-Recognition-and-Machine-Learning/index.html @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Pattern Recognition and Machine Learning - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Postgraduate/index.html b/tags/Postgraduate/index.html new file mode 100644 index 000000000..f172876f5 --- /dev/null +++ b/tags/Postgraduate/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Postgraduate - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Postgraduate/page/2/index.html b/tags/Postgraduate/page/2/index.html new file mode 100644 index 000000000..3925a2315 --- /dev/null +++ b/tags/Postgraduate/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Postgraduate - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Postgraduate/page/3/index.html b/tags/Postgraduate/page/3/index.html new file mode 100644 index 000000000..c93b84a3e --- /dev/null +++ b/tags/Postgraduate/page/3/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Postgraduate - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Postgraduate/page/4/index.html b/tags/Postgraduate/page/4/index.html new file mode 100644 index 000000000..30b2017d0 --- /dev/null +++ b/tags/Postgraduate/page/4/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Postgraduate - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Postgraduate/page/5/index.html b/tags/Postgraduate/page/5/index.html new file mode 100644 index 000000000..a0d1a7d1e --- /dev/null +++ b/tags/Postgraduate/page/5/index.html @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Postgraduate - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Project/index.html b/tags/Project/index.html new file mode 100644 index 000000000..e48edefcc --- /dev/null +++ b/tags/Project/index.html @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Project - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Prometheus/index.html b/tags/Prometheus/index.html new file mode 100644 index 000000000..8e18ebf79 --- /dev/null +++ b/tags/Prometheus/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Prometheus - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
Prometheus简单示例
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Python/index.html b/tags/Python/index.html new file mode 100644 index 000000000..25a94eeea --- /dev/null +++ b/tags/Python/index.html @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Python - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Python/page/2/index.html b/tags/Python/page/2/index.html new file mode 100644 index 000000000..15d72edb5 --- /dev/null +++ b/tags/Python/page/2/index.html @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Python - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Pytorch/index.html b/tags/Pytorch/index.html new file mode 100644 index 000000000..b9aa0ef64 --- /dev/null +++ b/tags/Pytorch/index.html @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Pytorch - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 2 篇文章

+
+ + + + +

2024

+ + + +
LLM强化学习
+
+ + + +

2023

+ + + +
Pytorch分布式训练
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Softwarer/index.html b/tags/Softwarer/index.html new file mode 100644 index 000000000..baf85f678 --- /dev/null +++ b/tags/Softwarer/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Softwarer - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
常用软件常用命令
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Stance-Detection/index.html b/tags/Stance-Detection/index.html new file mode 100644 index 000000000..00bf7f21b --- /dev/null +++ b/tags/Stance-Detection/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Stance Detection - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
Stance Detection
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Supervised-Learning/index.html b/tags/Supervised-Learning/index.html new file mode 100644 index 000000000..c4420c5bb --- /dev/null +++ b/tags/Supervised-Learning/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Supervised Learning - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Thrift/index.html b/tags/Thrift/index.html new file mode 100644 index 000000000..dc8365bd0 --- /dev/null +++ b/tags/Thrift/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Thrift - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2023

+ + + +
Hertz和Thrift简单示例
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/UCAS/index.html b/tags/UCAS/index.html new file mode 100644 index 000000000..7684dc144 --- /dev/null +++ b/tags/UCAS/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - UCAS - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/UCAS/page/2/index.html b/tags/UCAS/page/2/index.html new file mode 100644 index 000000000..6ea7367a9 --- /dev/null +++ b/tags/UCAS/page/2/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - UCAS - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/UCAS/page/3/index.html b/tags/UCAS/page/3/index.html new file mode 100644 index 000000000..04e448301 --- /dev/null +++ b/tags/UCAS/page/3/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - UCAS - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/UCAS/page/4/index.html b/tags/UCAS/page/4/index.html new file mode 100644 index 000000000..c169a7493 --- /dev/null +++ b/tags/UCAS/page/4/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - UCAS - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/UCAS/page/5/index.html b/tags/UCAS/page/5/index.html new file mode 100644 index 000000000..f5d267d9f --- /dev/null +++ b/tags/UCAS/page/5/index.html @@ -0,0 +1,443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - UCAS - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Ziyaooo/index.html b/tags/Ziyaooo/index.html new file mode 100644 index 000000000..8c5dfb026 --- /dev/null +++ b/tags/Ziyaooo/index.html @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Ziyaooo - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 2 篇文章

+
+ + + + +

2022

+ + + +
My Previous Love
+
+ + + + +
Trip To Qingdao
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/diary-Diary/index.html b/tags/diary-Diary/index.html new file mode 100644 index 000000000..c0db3973b --- /dev/null +++ b/tags/diary-Diary/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - diary/Diary - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

共计 1 篇文章

+
+ + + + +

2022

+ + + +
隐藏博客
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 000000000..95e689e73 --- /dev/null +++ b/tags/index.html @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 - Zostanzo's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xml/local-search.xml b/xml/local-search.xml new file mode 100644 index 000000000..d7d0c01cf --- /dev/null +++ b/xml/local-search.xml @@ -0,0 +1,45 @@ + + + {% if posts %} + {% for post in posts.toArray() %} + {% if post.indexing == undefined or post.indexing %} + + {{ post.title }} + + {{ [url, post.path] | urlJoin | uriencode }} + {% if content %} + + {% endif %} + {% if post.categories and post.categories.length>0 %} + + {% for cate in post.categories.toArray() %} + {{ cate.name }} + {% endfor %} + + {% endif %} + {% if post.tags and post.tags.length>0 %} + + {% for tag in post.tags.toArray() %} + {{ tag.name }} + {% endfor %} + + {% endif %} + + {% endif %} + {% endfor %} + {% endif %} + {% if pages %} + {% for page in pages.toArray() %} + {% if post.indexing == undefined or post.indexing %} + + {{ page.title }} + + {{ [url, post.path] | urlJoin | uriencode }} + {% if content %} + + {% endif %} + + {% endif %} + {% endfor %} + {% endif %} +