在国庆节放完假后回来后,感觉其实十月份也就是做三次物理实验的轮回。不过回想一下,又觉得似乎还有些漫长。遂小回顾一下。
在家开摆,有点无聊。期间就出去 date 了一次。
家里的事情还是很令人头疼。
至少出去自习的次数增加了不少。开始约卷
不过感觉效率提升也不是很大,经常感觉在图书馆坐半天但是也没干啥事。
目前还是有一种被赶着走的感觉,没有一点主动的想法。想必长远来看会寄。
物理课彻底摆了,但时间并没有很好的利用上。
物理,最优化,系统,这三门课的东西现在脑子里是一团糊(物理是啥都没有)
最优化吃了数分基础不行的亏,也没有一点系统的认知。
这里不得不提到上期我的困惑,虽说在 qua 的时候没有得到很好的解答,但现在我自己明白了打基础的重要性。之前脑子里就是一点东西都没有的感觉,现在学新东西的时候,就难以更加深入。所以就重要性而言的话,之前的我还是低估了。但现在希望能把这一块给补回来,至少是把最优化给学明白得。毕竟以后还得再用上的。这大概也需要更加强的时间管理和自律。
另外,感觉对事物的兴趣已经越来越淡了。看着别人学子讲坛讲一些自己喜欢的东西,可以讲得很深的时候也还是比较感慨,感觉自己没有哪方面是了解足够深入的。
不过上课不让自己玩手机了。
双倍的工作,双倍的工资
说实话感觉打工带来的额外的压力其实并不大。boyu 那边和以前没什么区别。
今年当上了下一届程序设计的助教,看着现在的小朋友就会想到当年的我们。
不是自己负责的部分的时候,感觉几乎没什么事情。
(精力充沛且实力强大的 dark 教授是另一回事)
而到了自己负责的大作业的时候,好像也没什么事
主要是没小朋友来答疑或者交流,也就时不时看看作业页面。
不过后面还会负责小作业和机考以及期末机考出题,好日子还在后头呢。
为了买 saber 大套,在某些方面的自律倒是还可以,比如每天记账。
有了泡沫般的工资后,花钱倒是大手大脚了一些,具体体现在买玩具上。
不过11月要考虑装机了。
似乎一个月没怎么额外的运动。
还是需要更努力一些。主要体现在时间管理上面。很多时候还是坐那刷刷手机啥的一个半天就过去了,事后又觉得挺后悔的,但当时就是感觉没有干劲。这样的状态还是不太行。所以说这方面还是得加把劲吧,估计得调整状态。
]]>在上上周,我们的分类器实现主要是基于机器学习库 sklearn
,使用的也都是已经封装好了的类。在这周,我们将探索深度学习与神经网络,利用 bert 预训练模型并手动实现下游模型,来实现我们的分类器。
在上上周,我们的分类器实现主要是基于机器学习库 sklearn
,使用的也都是已经封装好了的类。在这周,我们将探索深度学习与神经网络,利用 bert 预训练模型并手动实现下游模型,来实现我们的分类器。
参考:
在 https://www.bilibili.com/video/BV1a44y1H7Jc/ 中,我们可以跟着视频提供的代码实现一个训练一轮并测试的分类器。视频已经说的比较清楚了,这里再回顾一下:我们首先用 bert-base-chinese
将文本处理成 token,再将 token 传入我们的下游模型进行训练。而我们的下游模型是基于预训练模型实现的,在视频中也是选取的 bert-base-chinese
作为预训练模型。而在训练一轮之后,我们发现我们的结果非常不理想,测试集上的预测准确率只有 70% 不到。因此,我们需要进一步优化。
首先感谢 jpp 同学指出训练应该不止一轮,而要反复多轮次地训练。经过实践,在进行多轮训练,也就是把训练集多次喂给模型后,在测试集上的预测准确率确实有所提升。而这时一个整体上的方法。在此基础上,还有以下参数可以调整(也是我有所尝试的):
预训练模型的选择
bert-base-chinese
algolet/bert-large-chinese
allenai/longformer-base-4096
下游模型中的神经网络层
batch_size
max_length(预训练模型接受一句话的最大长度)
algolet/bert-large-chinese
可用)algolet/bert-large-chinese
可用)预训练模型限制了能接受的最大的长度,而我们有不少数据都是超过这个长度的,所以实际上模型接受的只是句子的一部分。而默认情况下,则是从句首截取指定长度。这也是一开始遇到的一个问题。而可能的解决办法有三种:
requires_grad(是否梯度回传,即是否修改预训练模型的参数,也就是微调 bert)
learning_rate(梯度下降时的学习率)
weight_decay(减少过拟合的可能,设置过大可能导致欠拟合)
接下来就是对于每种参数测试了。由于算力原因,小编只能使用 Kaggle 和 Google colab 云端平台来运行代码。而迭代次数设置为了 100,所以一份代码的运行时间也非常久。
]]>在上上周我们了解了 python 爬虫的基本操作,这次就让小编带大家来了解更多的爬虫吧!
]]>在上上周我们了解了 python 爬虫的基本操作,这次就让小编带大家来了解更多的爬虫吧!
这次我们的目标也是两个:
简单观察网页,我们会发现,随着页面往下滑动,会出现更多进入具体问答页面的链接。在控制台中的 Elements
项中,也能观察到出现了更多的类似于 <div class="question-content-wrapper" data-v-09010672="" data-v-2e2ddf27=""></div>
的标签。但当我们进入 network
项中,却发现并没有能够得到相关的数据。因此,这和上次下滑出现更多页面的分 p
发包的原理不同,这些链接是通过页面的滑动,用 js 动态渲染出来的,和上次的答案部分一样。
当然,我们的爬虫技艺也不会止步于此。既然能在 Elements
中看到,那想必还是有办法爬取的。之前我们爬虫利用的原理是发送请求后从返回的响应中找信息。那能不能让爬虫直接像我们平时使用浏览器一样获得最后渲染出的页面呢?
自然是可以的。这次我们使用的原理就是在爬虫中模拟一个浏览器,通过浏览器打开页面,来直接得到页面中显示的所有数据。而这些自然也是有很多造好了的轮子。一个非常经典的便是 selenuim
。可以参考入门指南。但小编这里用的不是这个,而是另一个更加新一点的,叫做 playwright
。学习资料参考 这篇文章。同时这里是 api 文档。
于是,我们可以在模拟的浏览器中打开刚刚的 CSDN 精华 的网页了。并且,阅读 api 文档后,我们发现可以用 page.mouse.wheel()
函数来模拟鼠标滚轮。接下来,只需要一直滑动到页面底端爬取所有进入具体问答页面的链接就行了。
在此过程中,有两种选择:一种在一边滚轮的同时一边进入得到的链接并爬去具体信息;另一种则是先滚到底部,爬取所有的链接并存入文件中。接下来我们只需要在那个文件中读入所有链接就能进行后续的爬取。在写爬虫时,调试也是经常需要遇到的,所有我觉得这里第二种更优,毕竟鼠标滚轮滚完一遍后,就不需要再进行这样的操作了。否则,我们反复运行程序调试,每次都要一遍滚一边爬,还是比较麻烦。
在获得所有链接后,接下来就是进入链接爬取问答详情了。由于我们已经有了模拟浏览器的手段,动态渲染的答案对我们来说也已经不算问题。但当我们尝试用之前的 asyncio
进行并发爬取时,至少小编的电脑是直接炸了。原来当我们把所有链接加入任务后,如果没有限制,爬下来的链接数量是 3000 左右,则相当于有 3000 个任务并发。于是一种处理的手段是利用信号量(Semaphore)来限制并发的数量。这自然是可行的。
不过,除了协程,还有其他并发的手段,那就是多线程和多进程。
参考
参考
学会了这些手段后,我们的爬虫优化也能变得更加多样。
这个网站的爬取将更加艰难,因为它设置了一些反爬虫的机制。
但我们还是先把爬虫的大体思路梳理一下。进入网站后,由于这是全英文的,所有需要先翻译我们要搜索的关键词。接下来,搜索关键词,我们会要进行一步人机验证。之后的感觉就和上上周的 CSDN 一样了。我们获得进入详情页面的链接,再在详情页获取具体的问答信息即可。在这里我们选择下方的第一个回答作为答案。
但问题是,我们的爬虫无法完成人机验证。这也就意味着无法进入搜索关键词后显示所有具体链接的页面。此时似乎陷入了僵局。但在多次访问和搜索后,发现在进行一次人机验证后的一段时间内,搜索将不需要进行人机验证。而更具体地,不需要进行验证的时间为五分钟。所以一个简单的想法就是我们在浏览器中手动点一下人机验证,然后让爬虫爬五分钟,再手动点一下,以此循环。但在大量数据面前,这对我们而言似乎不太友好。
于是,我们希望能提升爬虫的速度,让它在五分钟内能爬尽量多的数据。但当我们上了高并行的爬虫后,我们发现没过多久爬虫就收不到响应了。进入浏览器再人工查看网页,会有一个提示,说我们的 ip 在同一时间发送了太多请求,这是不正常的,所以把我们的 ip 封了。
这个问题理论上可以通过 ip 池随机代理来解决,但是处理起来比较困难。而经过探索,我们发现,只有搜索关键词后才会跳转人机验证,而进入具体问答页面的链接是不会跳人机验证的。再联系上文 CSDN 精华的处理方式,便能得到一个简单点的想法。首先我们以人工辅助人机验证的方式来为爬虫获取五分钟时间,在这五分钟内只把跳转问答详情页的链接爬下来并存在文件中。获取所有链接后,我们的爬虫便能以一个合理的速度,慢慢地,不被封 ip 地爬取所有的详情问答了。
scrapy
是一个实现的框架。参考:
在上周,我们爬取了 CSDN 和 Wikipedia 上的部分数据。那么在这周,我们将对数据进行处理,并用于训练我们的分类器。本文将介绍如何进一步对爬下来存储为 .jsonl
文件的数据进行处理,以及后续的随机森林算法来训练我们的分类器。
在上周,我们爬取了 CSDN 和 Wikipedia 上的部分数据。那么在这周,我们将对数据进行处理,并用于训练我们的分类器。本文将介绍如何进一步对爬下来存储为 .jsonl
文件的数据进行处理,以及后续的随机森林算法来训练我们的分类器。
在自然语言处理等领域,一个最基本的问题就是,如何让计算机“认识”对我们而言十分日常十分熟悉的语言。虽然我们能够很自如的运用,但计算机可是看不懂一点。在我们的数据中的体现就是问题和回答等,这些直接给计算机的话,显然是不行的。因此,要将语言喂给计算机,我们肯定是要对其进行一些处理。
这个视频 对从 one-hot 到 word2vec 都有介绍,并且给出了许多可以探索的学习资料。这些部分在此就不再赘述。在视频中是用的 python 的 gensim
库,这自然不失为一种选择。
而另一种 word2vec 的选择是 TfidfVectorizer
。这篇文章对其进行了简单的介绍,从用 jieba
分词到一些参数的含义都已经说明了,这里也就不展开讲述。
当然,可供选择的模型还有很多,但作者并没有一一尝试了。但总之,通过 word2vec,我们将数据中的若干个句子转化为了计算机可以运算的向量,来进行接下来的训练。
让我们回顾一下,我们最终的任务究竟是要做什么?
实际上,到这一步,我才理清我们的任务的关系。对于我们爬下来的问答对,自然是有好的问题和回答,也有不好的问题和回答。而我们最终其实是要实现一个分类器,能够用于判断一对问答是“好的”还是“坏的”。这其实就归类到了机器学习中的分类算法了。这个视频的 p1 到 p4 可以让我们初步了解一点机器学习。而具体训练时,我们需要先喂给模型一些训练集,给它一些问答对,告诉它“这些是好的”,“这些是坏的”。之后它将通过算法学习分类,并对我们给出的测试集进行预测。当然,我们自己手中也是有测试集的实际“答案”的,因此可以对模型的预测进行评价打分。自然,我们是希望它的得分越高越好。
而模型究竟是如何进行学习和分类的呢?这就是各种算法大展神通的时候了。本次我们采用的是随机森林算法。它的前置知识决策树在这个视频中有介绍,而随机森林算法在这个视频中同样也已经介绍了,这里就不再展开讲。
而具体到代码实现上面,sklearn
已经给我们造好了轮子。我们只需使用其中的 RandomForestClassifier
类便能实现随机森林算法。它的 fit()
函数能够接受我们的数据集 (X, y),在这里 X 代表我们的问答对,y 表示我们对问答对的标注(好坏)。而 predict()
函数则能接受问答对的数据,并给出预测。再通过 accuracy_score()
函数传入预测的结果和实际的好坏标注,便能给出模型的得分,为 0~1 之间的一个实数。
或许上文讲的还是非常抽象,可以参考我的代码。
至此,我们的分类器训练就基本可以宣布结束了。
细心的读者可能会发现,对于究竟是如何把向量化后的数据丢给RandomForestClassifier
的这件事情,我讲的非常含糊。事实的确如此。让我们回顾一下分类算法,我们的数据应该是有若干个特征,落到我们的问答对中应该就至少是 Question
和 Answer
(在实际操作时发现Knowledge_Point
和 Tag
对于效果并没有什么影响)。这也就意味着我们的数据集至少得有两个特征。但是我目前还没有找到在向量化后实现多个特征的方法。那么经过与同学间的交流,我发现同学们都是采用的讲 Question
和 Answer
拼成一个句子,然后当做一个向量来进行后续操作的。那么与多个特征相比究竟效果如何呢?这依旧有待进一步的探索。
在问题和回答中,都可能出现代码块。而代码和我们平时使用的语言又有所不同。比如同样效果的代码变量名称可能差别很大,也可能递归和循环看上去完全不同,但实际上是一样的效果。以上其实是我的猜测。但在我们的处理中,我们是直接无视了代码相对的特殊性,直接一股脑处理的。但一种可能更好的方案则是将其处理为一个特殊的 token,再进行训练。但很遗憾,对于给定的数据,如何将代码从文本中分离出来(即知道哪段是代码)就难倒了我们。因此到目前对于这个问题我们还没有应对的办法。
]]>爬虫这个词想必我们都不陌生,但它究竟是如何实现的?就让小编带大家看看吧!
]]>爬虫这个词想必我们都不陌生,但它究竟是如何实现的?就让小编带大家看看吧!
当我们访问一个网页的时候,实际上是浏览器向对应网址解析后的 ip 的服务器发送了一些请求,然后通过获取的回复来构建出的页面。
了解一些 html,css,js 相关知识即可
而对于爬虫而言,还要了解一下 dom tree 和 xpath 等等,正则表达式也可
这样一来,我们的爬虫的思路就清晰了。我们可以在 python 程序中也向服务器发送请求,再根据响应来寻找我们需要的数据即可。而寻找数据则需要先人工分析对应网站的页面的 html 结构,找到需要的数据所在的位置。因此实际上爬虫大部分算是体力活。
由于我们要爬取的是 QA 问答对的形式,并且为了保证质量,所以我们进入 CSDN 搜索关键字后,点击下方的问答
并选中已采纳
。接下来我们可以看到下面列出了若干个问题和最佳答案。我们点开控制台,进入 Network 项并四处翻找,可能会看到一个 search? q开头的包。进入 response 项,我们便能看到服务器给我们的响应了。可以看到这是一个 json 文件,是一个字典。再在里面翻找,我们会发现在result_vos
这项(也是一个字典)中有我们需要的更多的信息,比如单个问答的 url。复制打开后,我们会发现我们进入了单个问答的网页。因此,我们的思路就逐渐明确了。首先进入搜索后的页面得到刚刚这个包,再在响应中找到每个问答的网页,再进去继续获取具体的信息。
在 python 程序中如果我们发送和刚刚这个包一样的请求,得到的响应便也是我们看到的那样。在哪里看发送的请求长啥样呢?我们进入刚刚那个包的 Headers 项便能看到了。一点进去,在 General 出就有一个 Request URL,这个便是我们需要发送的请求。
接下来,我们再在单个问答的具体网页中获取问题和答案。再次进入控制台,在 Elements 项中找到点击标题,问题,答案等,可以发现它们在 html 中的位置。再在 Network 项中找到一串数字开头的包,发现其响应中就是我们的页面。我们在程序中获取这个响应,并以此构建 dom tree。此时就要用到 xpath 相关的知识了。我们刚刚在 Elements 页面探索了一番后,可以写出对应的 xpath,比如
title = tree.xpath("//section[@class='title-box']/h1/text()")[0]
question = tree.xpath('//section[@class="question_show_box"]//div[@class="md_content_show"]//text()')
answer = tree.xpath('//section[@div="@class=answer_box"]//div[@class="md_content_show"]//text()')
就是标题,问题和答案描述的 xpath。于是,对于这一个问答网页,我们应该就能通过程序获取对应的问题和答案了。python,启动!
这里值得补充的一点是,在控制台的 Element 项中右键点击某个元素其实可以直接复制对应的 xpath,不过它是从根一个个节点一路下来的,可能会比较繁琐,而且遇到 tbody 这种还会出错,具体原因后面细说。
当我们启动程序后,我们大概会发现,title 和 question 确实都被爬下来了,但是 answer 却啥都没有。是我们的 xpath 写错了吗?检查几遍后发现没有。这时事情便变得奇怪起来。
让我们回顾一下我们刚刚爬虫的过程,这时细心的读者可能会发现,在第一次进入搜索到的页面的时候,我们是在 Network 的 response 中寻找需要的数据的,但是第二次却直接在 Elements 里面去找了。实际上这是作者在写爬虫的时候犯的一个错误。那么当我们尝试在第二次的具体问答页面的 response 中寻找时,我们会发现,问题和标题的确没什么区别,但是答案部分却不见了。而当我们尝试 Ctrl+F
搜索答案中的某些字时,会发现它们是被套在一个 script
块内的函数中的,这意味着它是被动态渲染出来的。这样一来,我们便不能直接在收到的包中找到它。
此时便出现了僵局,一种通用的解决方法是使用 selenuim 或者类似的库,去模拟一个浏览器出来,先渲染一波,再在生成的页面去找。但对于 CSDN,还有一种更简单的方法。
我们回忆起,在刚搜到关键词,显示许多问答的页面时,每个问答的最佳答案是已经显示出来了的。此时,当我们回过头再去翻一翻那个页面获得的包,会惊讶地发现,在 result_vos
对应的字典内,有关键字 answer
已经对应了最佳回答的全部文本。也就是说,我们可以在这个页面就获得答案。但可惜的是,这个页面的问题描述是显示不全的,所以依旧需要进入具体页面去爬取问题的标题和具体描述。
最后再总结一下我们爬取 CSDN 的思路:首先进入 CSDN 搜索关键词,进入到显示许多问答的页面,在这个页面获得每个问答的答案和显示单个问答的页面的 url,再进入到单个问答的页面获得问题描述。对于我们的程序,则是通过发送两次请求完成。
而我们要爬取的关键字自然不止一个。仔细观察搜索之后的页面的 url,可以发现在 q=
后面的就是我们的关键字。于是我们可以先列出要爬的关键字列表,再用 python 的 .format
替换 url 中 q=
后面的部分。
最后是一个小小的细节:在第一次进入的页面中,一个收到的包里的问答是不全的。具体来说,当我们下拉网页并一边观察 Network 项中的 search?q=
开头的包,我们会发现这样的包会不断增加。也就是说,网页中显示的问答变多,实际上是发送了更多的请求获得了更多的包,以包含更多的问答。那怎么把这些包全部爬下来呢?我们选中不同的包,在 Payload 项中可以看到,它们的区别在于 p=
后面的数字不同。因此,对于一个关键词,我们改变 p=
后面的数字,多发一些请求,就能把包都收到了。至此,我们爬取 CSDN 的过程就结束了。
首先,我们进入 https://zh.wikipedia.org/wiki/ 页面,在搜索框输入关键字跳转。多搜几个关键字后,可以发现情况有两种:一种是输入关键字后直接进入了一个具体的词条的页面,比如搜索 斐波那契数
;另一种则是进入的页面中列举了许多词条,需要我们进一步点击进入对应词条的页面,比如 大O表示法
。对于第一种情况,我们直接进一步处理即可;而对于第二种,我目前采取的策略则是选择进入搜索出来的第一个词条再进一步处理。找到并进入第一个词条的方法则比较简单,分析网页结构再发送一次请求即可,想必在爬完 CSDN 后这已经不成问题。当然,这样的缺陷是可能第一个词条实际上和我们要搜索的东西毫不相关,也可能后面有更多的的词条的相关性更大。这则一方面是我们搜索的关键字的问题,另一方面,我们也可以考虑选择进入更多的词条再进一步处理(先把数据爬下来再说)。
而进入一个词条的页面后, wikipedia 上本没有问答的形式,因此我们需要手动将其设置为问答的形式。具体来说,比如我们来到了 素性测试 的页面,那么可以将“什么是素性测试?”作为问题,对应的介绍作为答案。通过观察,可以发现 wikipedia 的页面由若干级标题构成,有一个页面的大标题(h1),和各部分的小标题 (h2,h3)等。对应标题下方则是具体介绍。那么我们的问题可以设计成类似于“什么是h1的h2的h3?”的形式,再在相邻的标题间寻找答案即可。
本以为事情将会非常简单地解决,可没想到,wikipedia 的页面远比我想象的复杂nt。最初,一个简单的思路是把各级标题的位置找到,这很好办,寻找 @class='mw-headline'
即可。然后把两个标题之间的文本爬下来作为答案。一个来自于 lpr 同学的类似的思路则是利用 wikipedia 标题旁边的 编辑
字来找,并且可以通过其链接跳转到的页面获取文本,也非常方便。
但当我打开斐波那契数的页面的时候,这个方法便出现了问题。原因是在 h3 标题初等代数解法
下的各个步骤中,还用到了 h4 标题来表示步骤中的每一步。但实际上,问题到 初等代数解法
应该就已经需要作为一个最小的单位了。也就是说,按照上面的方法,我们会把 h4 标题首先构建等比数列
单独作为一个问题爬下来,但实际上它应该是 初等代数解法
中的一步。至此,一个问题是如何将标题区分开来,它究竟是一个问题,还是只是某一个步骤?
通过标题的等级来区分的办法并行不通。比如 堆栈 的页面中,h4 依旧是作为一个独立的问题存在的。此时似乎陷入了僵局。但当我们对比 斐波那契数 和 堆栈 的页面时,或许会观察到,它们的最上方的目录
的显示似乎有区别。斐波那契数的目录到 初等代数解法
后就是最后一级了,并没有包含 h4 的标题,而堆栈的目录中则是也列出了那些 h4 标题。此时,便自然能得出根据目录来寻找问题的想法。而在控制台模式中,仔细观察后,会发现目录框可以通过 div[@id='toc']
获得。而在堆栈的页面中,它的上一级是 div[@class="mw-parser-output"]
,但在斐波那契数的页面中,二者之间还夹了一个 div[@class="toclimit-3"]
。根据字面意思,有理由怀疑这个标签是用来限制目录大小的。而将其中的 3
改成 4
后,果然本来没有显示的 h4 标题也显示在目录中了。至此,通过这个标签的有无和其中的数字,我们便可以获得目录中的所有关键词,也就是一个问题的最小单位。
此时还有一个小细节:如果我们获取的是目录中的文本内容的话,依旧会得到 斐波那契数页面的制裁。它的目录中有一个 模n的周期性
的关键字,而在 html 的中,却变成了模<i>n</i>的周期性
。因此使用 text() 的话,则会被拆成三个,这也不好区分了。此时再一次陷入僵局。再次观察目录的成分后,发现上方还有一个 <a href="#模n的週期性">
。在 href
属性中这个词是完整的。因此,我们获取这里的词即可,而这写成 xpath 就是//div[@id='toc']/ul/li/a/@href
。当然,在后面的处理中还要去掉最前面的 #
。
在将我们需要的标题的关键词都找到后,便可以去寻找对应的答案了。这里我的想法是,找到下一个和它同级的标题,再把中间的文本都爬下来作为答案。这样做的一个好处是,对于 h2 ,它的回答便能包括它的介绍里面的所有 h3 等更低级的标题。当然,这里要特判一下处于最后的情况,比如一个 h2 下的最后一个 h3 ,它后面可能是另一个 h2,此时就不能找下一个 h3 了,而是下面的 h2,或者直接到了页面的结尾。
本以为对 wikipedia 的爬取就到此为止了,然而,在爬取的过程中,却又出现了一些神奇的页面。某个页面的 h2 标题下紧接着的是 h4 而非 h3,因此判断标题的时候不能直接判断相邻的等级,而需要考虑所有的。另外一个特殊情况是,在 编辑距离 页面中,压根没有目录。这时就要使用最开始的方法直接把所有标题爬下来处理了。
参考 https://cuiqingcai.com/202271.html
在我们之前的爬虫过程中,会发现,搜索许多关键字的话,爬虫的运行时间将非常久。有没有优化的方法呢?自然是有的。我们的爬虫在发送请求后,需要等待服务器的回复。而在我们的朴素爬虫中,我们只能在那干等。而并行爬虫的基本原理就是在等待响应的时候去做别的事情,比如发送其他的请求。这样,如果一个网站必须在 5 秒后返回响应,那么我们 10 个请求本来需要等待 50 秒,但并行化后可以直接发出 10 个请求,在等待 5 秒后,所有的请求都得到了响应。
而这一点可以基于 python 的 asyncio 库通过协程实现。上面的参考链接中其实已经讲的很清楚了,这里就不再赘述。
]]>RISC-V 模拟器是一个用 c++ 模拟 cpu 来实现 RV32I 指令集的 PPCA 大作业。本文将从 0 开始介绍作者在 PPCA 期间对此的实现。
这个作业,以我目前的认识来说,就是用 c++ 代码去模拟一个 cpu,接受一系列的指令(输入数据)然后大模拟,再返回数据。
由于模拟的是 cpu 的硬件,所以首先需要了解 cpu 的硬件架构,可以参考 计算机科学速成课 的第五集到第九集。也可以参考速成课的速通笔记。
然后就可以去看 RV32I 的指令集了(反正我是这个顺序)。这里感谢 crm 已经整理了部分需要的指令集**这里,同时也可以在线查找这里**。
当然,一开始看到这个表的时候我是一脸懵的,完全看不懂,所以这里解释一下:
首先这张图表面了六大类指令的格式。最上方的数字是32位二进制数的位置。 opcode 表示指令的大类,rd 表示 目标寄存器,funct3 和 funct7 表示在大类里面细分时的依据,rs1 和 rs2 则是作为参数的寄存器,imm 表示立即数,imm[4:1] 表示32位二进制数的这几位所对应的是立即数的第4位到第1位。是的,可以发现立即数被拆成了好几个部分,相当于还要在解码的时候再拼起来。我尝试搜索了其原因,但是以我现在的知识无法理解。
然后这张图就是所有指令的参数了。同时类似于 imm[12|10:5] 则是表示立即数的 12 位以及 10 位到 5 位,没错还是奇怪的拆开。
而对于每个指令的具体操作,上面给出的参考链接里已经有了一部分解释。而似乎更全的解释在 RISC-V 手册的附录可以查找。
以及,更普遍地能看到的一种描述指令的方式其实是,比如 add x1 x2 x3
,这里 rd
是最前面的,rs1
和 rs2
是后面的两个。似乎默认的都是这样写的,我也不清楚为什么。
在了解指令后,让我们把目光放到输入数据上(面向数据(雾)):
#include "io.inc"
int main() {
printInt(177);
return judgeResult; // 94
}
这是 sample.c
,是编译之前的源代码。~~实际上我们并不用管它。~~最后 return 后的注释则是我们需要输出的结果。
@00000000
37 01 02 00 EF 10 00 04 13 05 F0 0F B7 06 03 00
23 82 A6 00 6F F0 9F FF
@00001000
37 17 00 00 83 27 C7 06 33 45 F5 00 13 05 D5 0A
23 26 A7 06 67 80 00 00 83 47 05 00 63 82 07 02
37 17 00 00 83 26 C7 06 B3 C7 D7 00 93 87 97 20
23 26 F7 06 13 05 15 00 83 47 05 00 E3 94 07 FE
67 80 00 00 13 01 01 FF 23 26 11 00 13 05 10 0B
EF F0 1F FB B7 17 00 00 03 A5 C7 06 83 20 C1 00
13 01 01 01 67 80 00 00
@00001068
FD 00 00 00
这是下发的 sample.data
。而在我们实现的时候,我们是先直接把它全部读入到我们模拟的内存中。@00000000
意味着内存中的地址。后面的十六进制数则是指令。每次读四个,前面读进来的其实是二进制数的低位,然后再解码成指令。
./test/test.om: file format elf32-littleriscv
Disassembly of section .rom:
00000000 <.rom>:
0: 00020137 lui sp,0x20
4: 040010ef jal ra,1044 <main>
8: 0ff00513 li a0,255
c: 000306b7 lui a3,0x30
10: 00a68223 sb a0,4(a3) # 30004 <__heap_start+0x2e004>
14: ff9ff06f j c <printInt-0xff4>
Disassembly of section .text:
00001000 <printInt>:
1000: 00001737 lui a4,0x1
1004: 06c72783 lw a5,108(a4) # 106c <__bss_end>
1008: 00f54533 xor a0,a0,a5
100c: 0ad50513 addi a0,a0,173
1010: 06a72623 sw a0,108(a4)
1014: 00008067 ret
00001018 <printStr>:
1018: 00054783 lbu a5,0(a0)
101c: 02078263 beqz a5,1040 <printStr+0x28>
1020: 00001737 lui a4,0x1
1024: 06c72683 lw a3,108(a4) # 106c <__bss_end>
1028: 00d7c7b3 xor a5,a5,a3
102c: 20978793 addi a5,a5,521
1030: 06f72623 sw a5,108(a4)
1034: 00150513 addi a0,a0,1
1038: 00054783 lbu a5,0(a0)
103c: fe0794e3 bnez a5,1024 <printStr+0xc>
1040: 00008067 ret
00001044 <main>:
1044: ff010113 addi sp,sp,-16 # 1fff0 <__heap_start+0x1dff0>
1048: 00112623 sw ra,12(sp)
104c: 0b100513 li a0,177
1050: fb1ff0ef jal ra,1000 <printInt>
1054: 000017b7 lui a5,0x1
1058: 06c7a503 lw a0,108(a5) # 106c <__bss_end>
105c: 00c12083 lw ra,12(sp)
1060: 01010113 addi sp,sp,16
1064: 00008067 ret
Disassembly of section .srodata:
00001068 <Mod>:
1068: 00fd addi ra,ra,31
...
Disassembly of section .sbss:
0000106c <judgeResult>:
106c: 0000 unimp
...
Disassembly of section .comment:
00000000 <.comment>:
0: 3a434347 fmsub.d ft6,ft6,ft4,ft7,rmm
4: 2820 fld fs0,80(s0)
6: 29554e47 fmsub.s ft8,fa0,fs5,ft5,rmm
a: 3820 fld fs0,112(s0)
c: 332e fld ft6,232(sp)
e: 302e fld ft0,232(sp)
...
这是 sample.dump
,可以说是对上面输入的解释,第一列代表内存的位置,后面可以清楚的看到对应的指令和参数。当然有一些细节部分我也没有深入追究。
细心的读者可能会发现,在 .dump
文件中,关于指令的名称和操作数似乎有一些令人迷惑的地方。首先,这是因为有一些指令实际上是等价于另外一些指令的。比如说,beqz
这个指令在表中就没有出现过,而它就等价于 beq
的一个寄存器为 x0
,再与另一个寄存器的值比较。bnez
也是同理。除此之外,还有 li
,ret
等,具体也可以参考 RISC-V 手册。其次,一个小小的细节是 .dump
文件中的操作数使用的是寄存器的别名,比如说 sp
,a0
等等,关于这个也可以去看 crm的笔记。
当我们掌握所有的指令后,我们就可以动手写一个一级流水的笨蛋模拟器了。用 c++ 模拟各个硬件,处理每个指令即可。当然,还有一些小小的细节值得注意(我自己写的时候遇到的):
x0
寄存器的值永远都是 0,哪怕有指令试图将其改变也是无效的。到此,至少本人已经用一级流水能跑模拟器了。(虽说模拟地并不完全并且第一次提交的时候忘记删debug输出导致stdout挤爆了然后oj炸了)
当然,写一个一级流水的模拟器并不是必要的,并且对于接下来马上提到的 Tomasulo 算法而言似乎并不能很好地进行代码复用。(或许还是可以作为辅助调试的方法吧(雾))
当然,光有笨蛋的一级流水可是不行的呐,毕竟有些情况下性能会比较低。比如说以下情况:
add x1 x2 x3
sub x10 x1 x4
add x5 x6 x7
那么当第一行的指令在执行的时候,理论上第三行的指令是完全不受影响的,但是还是得老实等在那,被卡住了。所以,之后要了解的 Tomasulo 算法便是能够乱序执行以提高效率的手段。
在进一步了解 Tomasulo 之前,让我们回顾一个概念:时钟周期。回头看一下我们的一级流水,我们会发现其实它并没有太与时钟周期挂钩,因为一直都是读入指令->解析指令->处理指令来驱动的。但是 Tomasulo 则不然,其后将提到的各个元件和乱序执行等将会以及作业要求提高我们对于时钟周期的要求。因此在处理之前把这个问题理清楚是有必要的。
我们知道,cpu 里实际上是以时钟来驱动运行的。在一个时钟内,各个元件都并行地进行“一步”操作。当然,在我们这个作业中也不需要更加细致地深入了解其物理原理和硬件实现。但是由于 c++ 模拟只能串行不能并行,所以对于时钟的模拟显得比较抽象nitian。具体来说,我们设一个变量 clock 代表时钟,然后每过一个周期就 clock++ 已经开始抽象了。那么在我们手动增加 clock 的间隔中,便可以认为其中运行的程序都是在一个周期内并行的。这也意味着在一个时钟周期内其中运行的元件可以以任意顺序执行。
要实现这一点的话,一个简单而泥潭的方法是对于每个通用寄存器(可以理解为所有东西)都存一个当前的状态和下一时刻的状态,然后再在每个周期末将“下一时刻”更新至“当前时刻”。
而对于一个时钟周期内具体能干什么事情,则是一个更加抽象nitian的问题。 我目前了解到的也比较模糊。目前有(有些涉及到具体的元件目前还没讲):
让我们回到 Tomasulo 上来,在上面的例子中,第三行的 add
明显是可以在第一行的指令执行的同时执行的(如果有多个 ALU 并行的话),但是第二行的 sub
必须等第一行执行完后才能执行,否则会有 RAW(read after write) 问题。那么这里就体现了一些依赖关系,某些指令必须在一些指令之后执行。而这个关系能够比较自然地让我们联想到拓扑序。实际上,Tomasulo 采取的也是类似的方法:记录每个指令所依赖的指令,待其依赖的指令执行完毕,操作数准备好之后再执行。
同时,Tomasulo 还有一个概念:寄存器重命名。这是一种类似于缓存的思想。在前面的一级流水中,我们是在处理指令的时候,直接在寄存器上操作。但是,当一条指令的操作数所需的寄存器没有被占用时,我们可以直接将操作数取出来并存放起来,这样这条指令就与其操作数所需的寄存器没有关系了。在之后的 Reservation station 部分,我们将详细讨论这一部分。
我的 Tomasulo 的架构参考的是 Computer Architecture:A Quantitative Approach(计算机体系结构:量化研究方法)第 3.4 章到第 3.6 章的内容,并加上个人的理解魔改。以下是架构图:
接下来将对各个元件进行介绍。
这个部分从内存取出指令(fetch),进行解码(decode)再发射(issue)到 RoB。没什么好说的
前面提到了 Tomasulo 的思想是根据依赖来执行指令和寄存器重命名。在保留站中我们将看到它具体的工作。首先,(我的)保留站的一个元素有以下内容:
而 RS 实际上是一个类似于表格的形式:
busy | op | vj | vk | qj | qk | dest |
---|---|---|---|---|---|---|
由于没有例子我就不往里面填东西了
而在这些元素中,值得关注的就是 vj, vk 和 qj, qk。首先看到 vj, vk,它们就相当于寄存器重命名,把本来在寄存器中的操作数存在了 RS 中。这样一来,这条指令在读完操作数后就与寄存器没有关系了。而 qj, qk 则维护了指令操作数所在的寄存器的依赖关系。当 qj, qk 均为 0 的时候,代表这条指令已经没有依赖,操作数准备完毕,可以执行了。反之,则意味着对应的寄存器在前面还有指令尚未执行完毕,因此需要等待。而此时 qj, qk 则就设置为 占用对应寄存器的指令在 RoB 中的编号。而什么时候获得这个值呢?我们将在后面 RoB 的部分提到。
这个也被称为 qi,记录的是某一寄存器目前是被 RoB 中的哪个指令占用的,能够使 RS 获得 qj, qk。
如果熟悉指令的话,我们会发现有两类指令load
和store
的特点是要依靠内存操作。这就不像寄存器一样可以记录依赖了,毕竟内存那么大嘛。而且读写内存也是连续的几个字节。所以对于这两类指令,一种可能的做法是,使这两种指令顺序执行。稍微优化一点的话,可以发现如果 load
指令前没有 store
指令的话就可以执行了,而 store
指令前面必须没有 load
或 store
指令才能执行。基于这种特性,我们能够提出一种更加简单的架构。但是我摆了所以如果有机会再改或者介绍
可以先思考一下,光有前面的结构,是不是也能跑了?答案是是的。
但是存在一个问题,当遇到条件跳转指令的时候,由于乱序执行,必须等条件跳转指令所需的寄存器依赖消除后,才能继续计算应该跳转的分支。所以在此之前 fetch 和 decode 都会停止。这自然是不太优雅的。因此一个可能的手段是分支预测。预测的方式将在后面介绍。但是分支预测又会带来一个问题:预测错误之后的补救。如果每条指令都是直接修改寄存器的值的话,在预测错误后执行的那些指令带来的影响将难以消除。因此,我们有了 RoB。正如其名字,它将乱序执行后的指令重新按照发射的顺序排序。它使用了循环队列的结构,也就意味着先进先出。在指令发射后,它就进入 RoB。在执行完后,指令并不直接修改寄存器,而是将指令需要修改的寄存器编号(或内存位置)(dest)和对应的值(value)记录在 RoB 中。而每个周期检测队首的指令是否已经执行完毕,如果执行完毕的话,就按照记录修改寄存器或内存,这一过程也称为提交(commit)。这样一来,乱序执行完毕后的结果将按照顺序来修改寄存器。
而在上文提到的寄存器依赖中,当新发射的指令依赖的寄存器被某个已经执行完毕的指令占用,却还没有在队首被提交的话,那么这时就直接从 RoB 中获取所需的操作数即可。
可以参考wikipedia上的分支预测器介绍。
我的实现是,对 pc 取6位进行 hash (3到8位),再对 hash 的值使用四级自适应预测器,再在里面套一层二位饱和预测器。具体的实现方式可以看 wiki,也比较简单,这里就不详细展开。
对于 pc 指针的移动是一个需要注意的地方,稍有不慎, fetch 和 decode 就会乱掉。这里稍微讨论一下我的实现。
首先,需要更改 pc 的值的指令有 B 型指令,J 型指令和 jail 指令。同时,正常情况下 pc 会自动 +4。那么同样地,在不正常的情况下,如指令发射失败,也是需要注意的地方。以及,在第一个周期,只有 fetch 指令运行而没有 decode 也是需要注意的地方。
对于 B 和 J 型指令,我们在解码之后立即能得到 pc 需要跳转的位置(B 型通过预测),但同一周期 fetch 到的指令则是无效的。因此在下一个周期 decode 的时候处理的指令也是无效的,需要跳过。
而对于 jail ,pc 之后要跳转的位置是难以预测的所以就不预测了。因此,在 jail commit 之前,fetch 和 decode 都需要停掉,一直等到计算出需要跳转的位置为止。
对于发射失败而言,下一个周期需要继续发射这条指令。更优化的方法是采取 instruction queue 和 多发射的手段来解决这个问题,但是那需要更深入的理解摆了,因此这里讨论的是一种比较粗暴的方法。
在简单分析了这几种情况之后,就可以开始具体的讨论了。首先,在正常情况下,pc 会自动 +4 的操作似乎有两种选择,一种是在 fetch 之后,另一种则是在 decode 之后。由于第一个周期没有 decode ,因此我选择的是第一种。(事实上我不清楚真正的架构是如何实现的)
现在来看,对于 B 和 J ,似乎没有停掉 fetch 的必要。下个周期的 fetch 能够直接从修改后的 pc 处读入。但是下个周期的 decode 则是需要中断,因为指令失效了。对于 jalr 则是整个都中断了。对于发射失败的话,下个周期 decode 则是继续发射这个周期的指令,而下个周期的 fetch 就要停掉。但是注意到,这个周期的 fetch 已经读入下个周期需要 decode 的指令了,这个指令则需要延后在我这里就是失效了。而由于要求硬件并行,也就是 c++模拟时各个函数可以乱序执行,那则是有可能在 decode 之后才执行 fetch ,所以并不能直接在 decode 里面直接改下一个周期的指令,而是需要置一个 tag 表示下个周期的指令应该修改。
感谢 DarkShrapness ,Wankupi 等人的帮助,对于我对这个作业的理解和思考提供了很大的帮助。
]]>关键词:导数,连续,可导,函数项级数
在学完一元函数的微分后,我们就已经了解了连续和可微的概念。连续性指的是,当自变量 的变化很小时,所引起的因变量 的变化也很小。严格来说,如果 在 的某个领域有定义,且 ,则称 在 处连续。若函数在区间 有定义,且 ,则称函数在 处左连续。右连续同理。如果函数在开区间 内每点连续,则为在 连续;若又在 点右连续,在 点左连续,则在闭区间 连续。如果函数在整个定义域内连续,则称为连续函数。
而可微分函数(英语:Differentiable function)在微积分学中是指那些在定义域中所有点都存在导数的函数。在一元时,可微和可导是等价的描述,因此本文中也不做区分。
而可微性与连续性的关系则是:若 在 点可微,则 在该点必连续。特别的,所有可微函数在其定义域内任一点必连续。逆命题则不成立:一个连续函数未必可微。比如,一个有折点、尖点或垂直切线的函数可能是连续的,但在异常点不可微。
实践中运用的函数大多在所有点可微,或几乎处处可微。反之的话,一个点出连续不可微的例很好举,那么有多少函数是处处连续处处不可微呢?本文就将梳理几种处处连续处处不可导的函数,并给出其连续性和不可导性的证明,以及做出总结。它们具有不同的构造方法和性质,展示了连续函数的多样性和复杂性。
人们发现的第一个处处连续但处处不可微的函数是魏尔斯特拉斯(Weierstrass)函数,其构造为:
其中 , 为正的奇数,使得 。
对, ,而 收敛,由 Weierstrass 判别法可知, 一致收敛。
而 ,所以 。
对于 ,下证 不存在。
对于 ,可以找到 ,使得 。
记 。
那么有 。
即 ,
且 ,
以及 。
所以 和 都趋近于 ,下面进行计算:
将两个加和记为 ,可知 为有限项加和, 是无穷级数。
由和差化积公式,
由于 且 ,所以
。
所以 。
而对于 ,首先有
( 为奇数)
所以
而其中 。
所以 。
合并后,则有
又由 知 ,且 知 。
所以 的符号由 奇偶性决定,且 。
同理,也可得出
以及 。
由于 ,所以导数不存在。
连续性的证明在使用 Weierstrass 判别法后很简单,而可导性本质上其实是表明函数在某点处的振幅无穷大。运用了放缩来证明,总的来说技巧性也很强。
首先,设 ,再令 使其以周期 延拓至整个数轴。
再令 。
设 ,则构造出的 在 上有定义且连续,但处处没有导数。这个例子是由范德瓦尔登提出的。
首先有 ,从而 ,由 Weierstrass 判别法可知, 一致收敛,再由一致收敛的性质可知 在 上连续。
证明的思路同样是找一个数列使得导数对应的极限不存在。
对于 ,可以构造一个数列 使得 (正负号后面决定)。考虑极限
由定义知, 的周期为 ,所以 时,有 。
所以上式化为 。
同时,考虑到 都是周期重复的斜率绝对值为 的折线,且半锯齿区间(半个周期且斜率不变的区间)的长度为 。则对于 时,通过控制 中的正负号,可以使得 和 落在同一个半锯齿区间内,则 。
则
其中 。这个级数是发散的,则由海涅定理可知 不存在,即在 处不可导。
这个函数的构造取自无穷多锯齿的叠加,似乎也并不是很形象。而其连续性利用 Weierstrass 判别法同样能轻松证得,而其可导性的证明也比较简洁。
本文给出了几种处处连续但处处不可导的函数的例子。可以发现,它们都是用函数项级数构造的,并且共同点是在每一个点处的“振荡”都无限大从而不可导。
有好奇的读者可能会认为,以上这些函数可能是个例,那么一个自然的问题就呼之欲出:在连续函数中,处处不可导的函数究竟占多少呢?事实十分惊人:处处连续处处不可导函数的集合,在连续函数空间中是一个稠密子集。在测度论意义上,在配备了经典维纳测度 的连续函数空间 中,至少有一处可导的函数所构成的集合的测度是 ,也就是说和处处不可导的函数相比是可以"忽略”的。下面将试图给出证明:
首先,我们要证明贝尔类型定理:
令 为一个完备的度量空间,若 为 的一个稠密开子集的序列,则 在 中是稠密的。
证明:我们需要证明,对于任意非空开集 ,都有 。有了这一点的话,就有对于 ,由于 可以任意小,却不能只包含一个点,所以 中都存在点离 任意近,所以 是稠密的。
而由于 是稠密的, 为非空开集,所以 包含一个球 ,其中设 。
对于 ,归纳地选取 和 。假设我们已经选好了 时的所有 和 ,由于 ,所以可选取 使得 且 。
对于 ,有 ,又因为 ,所以 为一个Cauchy 列。由于 是完备的,所以存在 。
对于 ,,我们有 。所以 。#
其次,我们需要证明贝尔类型定理的一个推论:
在一个度量空间 中,若 满足 ,则它被称为无处稠密的子集。若 为一个完备的度量空间,且 为一个无处稠密子集的序列,则 在 中稠密。
证明:首先证明, 是稠密的当且仅当 ,满足 在 中稠密。
等价于 。
这又等价于 ,即 在 中稠密。
令 ,那么 是稠密开集,根据贝尔类型定理, 是稠密的。
而 ,所以 是稠密的。#
接下来,我们就要用贝尔类型定理和其推论证明:处处连续处处不可导函数的集合,在连续函数空间中是一个稠密子集。
令 ,
并对于 ,令 。
我们要证明,对于 为闭集。
令序列 在 中一致收敛于 。对于 , 使得 。
根据 Bolzano-Weierstrass 定理, 存在收敛子列 使得 。
所以对于 ,有
则 时, ,故 。
我们要证明,对于 , 为无处稠密的,故 为稠密的。
而为了证明 为无处稠密的,我们需要证明 为稠密的。
任取 和 ,我们要构造 使得 。
根据维尔斯特拉斯近似定理(Weierstrass approximation theorem),存在多项式 使得 。
令 且 。定义函数 为
则 为周期 ,值域 的函数。特别地,。
令 ,则 。
此外, 为分段连续可导的,且 ,这对于任意可导的点 都成立。对于不可导的点,由于 的构造, 仍存在单侧导数,对应的单侧极限仍满足上述不等式。
所以,对于 , 使得 ,即 。
我们证明了 为稠密的,也就是 为无处稠密的,故 为稠密的。
我们要证明, ,故 C([a,b])\setminusD 在 中稠密。
令 ,并假设 在 处可导,那么 。
所以 使得 。
即 。
另一方面,若 ,有 。
若选取 ,我们有 。故对于某个 ,,即 。
所以 在 中稠密。#
本文整理了几种处处连续处处不可导的函数,并对其连续性和可导性进行分析,说明了其原因。最后,本文尝试证明了不仅存在处处连续处处不可导的函数,而且“很大一部分“连续函数都是这样的函数,这确实令人惊讶。这意味这我们平时接触的连续可导函数,在引入函数项级数等概念后的所有连续函数空间中只占有微不足道的一部分。因此,在这些方面,想必还有十分 广阔的宇宙等待我们探索。
连续函数(百度百科)https://baike.baidu.com/item/连续函数
可微函数(维基百科)https://zh.wikipedia.org/zh-hans/可微函数
魏尔施特拉斯函数(维基百科)https://zh.wikipedia.org/wiki/魏尔施特拉斯函数
如何证明魏尔斯特拉斯函数处处不可导? - Shinnku的回答 - 知乎 https://www.zhihu.com/question/384506061/answer/1132666699
一个处处连续处处不可微的函数 - wKy2008的文章 - 知乎
https://zhuanlan.zhihu.com/p/476553312
处处连续处处不可导的函数 - 斯宾王的文章 - 知乎
https://zhuanlan.zhihu.com/p/512900866
Jarnicki M , Pflug P .Continuous Nowhere Differentiable Functions[M].Springer International Publishing,2015.
]]>首先,进入教学信息网->信息查询->学生成绩明细查询->课程成绩合并显示
如下图所示:
此时右键检查 或 F12 或 Ctrl+Shift+C,选中那一栏的元素,
可以看到:
将这里的这串十六进制数复制下来。
进入教学信息网->信息查询->学生成绩查询
按右键检查 或 F12 或 Ctrl+Shift+C,选择网络一项,并选中全部(all)
之后再任意点击某一门科目的“查看”。
如图:
接下来会在 request 中看到一个以 cjcx 开头的请求,如图:
右键点击其,并以复制为 fetch。
接下来进入控制台,将刚刚复制的东西粘贴进去。
然后看到图中框的部分,将 jxb_id 换成step 1中复制下来的那一串十六进制数。
按回车发送再回到网络,会发现最底下多了一个cjcx开头的request,左键点击其,选择"预览"即可。
这样就能查到尚未开放的成绩辣!
]]>(每行独立,行与行无关)
年初立 flag。
过年那啥整烂活,无果。
开学考退步。
发现并不像想象的那样能了断。
之后不知道怎么回事起飞。
然后就是高考,赢,但没完全赢(事后)。
终于能放松下来,推完 sp 所有线。
强基培训,搞感觉是在搞,但是又搞得没什么感觉。
然后跑去集训,成为了或许唯一一个没进队的。
之后回家调节放松摆烂。
出去旅游了一次。
推了 君与彼女与彼女之恋 和 素晴日。
报到,军训。
整烂活 again,成功!
第一节课之后被秒,线上上课,开摆。
恢复线下,开卷(误)(并没有)。
第一次氪 648。
中途得知了某些消息。
之后又是疫情,回老家。
线上上课,开摆。
年末总结。
实际上,虽然能列出来这样一条轨迹,
但其中,每一个节点,众多细节,确实难以言说。
像是晚自习时的 emo,高考前的水群聊天,强基培训和小天天在那卷,中秋吃必胜客,国庆提瓦特线上,
月光下的奔跑,倚柱时的沉思,聊天框中的文字,电话中的争吵。
这无数细节,假以再过些时日,是否还会在脑海中留存?
倘若事情能被记录下来,回顾之时,又是否能回忆起当初那份情感。
曾经到处开游戏的我,今年一直玩着的也只有o了。
比较奇妙,当年感觉会被复杂的机制弄晕而不敢入,现在居然能一直坚持着不退。
但是抽卡和圣遗物确实搞心态。
以及,来SJTU后,联谊时唱出《神女劈观》,学子讲坛报名原神建筑考据,还真是op上大分(雾)。
不过,就在不久前,看到了某人眼中对o的印象,似乎又有了新的认识。
游戏之于我们的意义,可能要更加广阔一些。
漫无目的的闲逛,坐在风神像手中远眺,看绝云间的云海翻涌,
登上覆灭的鹤观,听阿瑠唱最后一支歌;走进荒废的渊下宫,探寻天空岛的禁忌调教渊上(雾);下到神秘的层岩巨渊,探索深邃的地下世界,见证志琼的绝笔(我们终将重逢(确信))。
以及,提瓦特成了唯一的线上的载体,提供了机会。
所以说,风行迷踪,是为了迷踪币换点奖励,还是躲猫猫本身呢?
高考完后的一点闲暇,把咕了不知道多久的 sp 推完了。
当年推白羽线的时候,其实也并没有太多的感觉(所以也一直咕)。
但是,之后的一些个人线,以及最终的TE,却给我带来了不小的震撼。
和鸥搜索藏宝图中的海盗船,和紬过完一生的节日,和苍在山中寻找七影蝶,
以及,和██████████████████。
之后的 君与彼女与彼女之恋 则开拓了我的视野以及由于出了点小问题并没有完全推完。
素晴日则是给我带来一些奇幻的哲思谜语。
而偶然间推的万五则的确改变了我曾经的刻板印象。
然而,我也终究只是透过“万华镜”,观赏着另一个世界,当我醒来时,则要继续书写我自己的故事。
似乎是今年成为了真·车万人。
指开始玩原作。
以及云剧情。
可能我没有遇上曾经的那个时代吧,所以感触可能不是很深(?)
但是看到身边的人和广义身边的人,有着如此深的情结,也不由得有所思考。
那究竟是如何的一种感觉?
2020,2021,2022,
自我开始写年度总结起,每年,都会遇到新的一群人,每年都会发生新的故事。
而这是否存在“命运”,使得某些时刻,变得尤为巧妙。
其中,的确有些人,似乎,自此,我们的命运自此交织在一起。
而我,则感到无比幸运。
以前说过,感觉一年一下子就过去了。
感觉上次写年度总结还是在昨天。——hst的2021年度总结
不过今年倒确实,感觉一年的跨度确实挺大的。
无论是前半的高中最后的时光,还是后半的初入大学校园,都充满了回忆。
而在这样的路途中,或许不知不觉间,我也有所改变了吧。
那么,哪些是迭代,哪些是磨损呢?
可能,这个问题,也得留到明年去想咯。
其实 测试我在以前学竞赛的时候就有接触过,而回顾我之前的笔记,发现实际上,有很多地方都未完全弄懂,仅是照搬结论,甚至有错误的部分。因而,这一次,我希望能更深入透彻的了解这个算法。
给一个数 ,要求判断 是否为质数。这是一个非常常见,也应用十分广泛的问题。而对其的解决方法也值得思考。
首先,一个几乎显然的暴力算法:枚举 中的所有数判断是否能被 整除。如果不存在能被整除的数,则 是质数。而我们也能稍微优化一下,假设 ,则 。也就是说,我们枚举的范围可以缩小到 。
当然,即便优化之后,面对很大的质数,这样的时间消耗也是难以接受的。而接下来要介绍的,则是一个比较高效的质数测试算法:。
在进入 之前,首先来介绍一下著名的费马小定理:
若为质数,则对于有
这里给出我用数学归纳法尝试的一个证明:
显然 时结论成立。
若 时结论成立,
当 时,有:
。
那么除了 和 这两项外,
其它的都有一个系数。
由于 为质数,所以 中的 并不会在分母中被除掉,所以这些系数都能被整除。
而 ,
所以 ,结论成立。
因而,当存在 使得 时,则 必定不是质数。
至此,可以得到判断 不为质数的一个条件。
可能很自然会想到,如果费马小定理的逆命题成立,我们便能很方便的做出判断。但事实上,有一些合数也能巧妙的满足这样的性质。这类数被称为卡迈克尔数。
这类数的性质非常接近质数,并且证明有无限多个。因此,他们的干扰使得仅靠费马小定理的判断的正确性无法保证。
实际上,我对这类数的条件及性质,以及相关证明也比较感兴趣。但由于本文着重的是 相关的方面,便不具体阐述。
费马小定理的局限性使得其会产生失误。那么,可能会自然的想到,能不能加强我们判否的条件,进而使得没有“漏网之鱼”,或者说使其更少呢?
答案是有的,那就是二次探测定理:
对于一个质数 , 若 , 则 或
对其的证明比较简单:
若 , 则
即 .
所以只有当 或 时上式才成立。
所以它也能在 非质数的时候判否,但同样也会有失误而放过合数。
而令我感到有趣的便是,它是如何与费马小定理结合起来的,以及,它能多大程度的加强我们的测试的正确性(或是减小出错的可能性)。
首先,仅考虑 的情形,则 首先是奇数(否则显然),那么 就是偶数。
这意味着 。
假设 通过了费马小定理的判断,那么
也就是说,我们可以对 进行二次探测定理的判断。
而且,若其亦为平方数,且 ,则我们又可以对其开方,再继续判断,直至其指数为奇数或 。
在这其中,只要出现不符合二次探测定理的情况,便能立即判否,否则到最后,便有较大概率判断 是质数。
从直观上感觉,我们似乎加上了层层条件,将我们的“网”变得更密了,然而,目前的效果究竟如何?还有没有“鱼”能够漏掉?我们似乎依旧不能确保。接下来,我们就将分析此算法失误的可能性。
有趣的是,我浏览了目前网络上许多对于 的介绍,却并未看到过对其正确性和失误概率的完整证明,包括 wikipedia。大多仅是给出结论:一次测试失误的概率不超过 。我觉得这可能也是只知其然而不知其所以然的体现,毕竟应用起来确实不需要知道它为什么是对的。
但要深入了解这个算法,则不能避开这个问题。因此,我希望能在这里给出其证明。最终,我在算法导论中找到了对此的证明。然而,这里也仅能证明出错概率不超过 ,而更强的结论,需要对需要判断的数组 的选择有所要求,但遗憾的是,我并未找到更加深入探讨这个问题的参考资料。
因此,接下来我将给出关于出错率不超过 的证明。
对于数 ,我们把那些能够在测试中确定 不是质数,即 或 ,的数 称为证据数,反之,则称为非证据数。
则接下来将证明:对于随机选取的数 ,其非证据数的个数不超过 。
记 为模 乘法群的简写。
首先,对于任意非证据数 ,有 ,这是因为
是非证据数
$\Rightarrow a^{p-1}\equiv1(\bmod p) $
有解
所以
取
则 在 的乘法下封闭,且对于 非证据 ,有 。那么我们接下来的思路就是限制 的大小。
由群论的拉格朗日定理,
如果是一个有限阶的群,而是的一个子群,那么的阶就能整除的阶。
如果我们能说明 是 的一个真子群,则 的大小不超过 。接下来,我们将试图证明这一点。而讨论的思路大致如下图所示:
graph LR
p --> A(不是 Carmichael 数)
A --> B(B是真子群)
p --> C(是 Carmichael 数)
C --> D(p 是素数幂)
D --> F(不可能)
C --> E(p 不是素数幂)
E --> G(B 是真子群)
如果 不是 数,由定义, 使得 。也就是说, 且。可得 是 真子群。
否则,接下来证明 不可能是素数幂。
假设 ,则 是奇素数,由原根定理:
设p是奇素数,则对任意 ,模 的原根存在。
所以存在原根 使得其阶为 。
又有 ,
由离散对数定理:
如果 是 的一个原根,则当且仅当等式 成立时,有等式 成立。
于是,令 即可得到 ,即 。
而此时便产生了矛盾,因为 但 。
所以 不可能是素数幂。
此处的证明可能涉及较多数论的定义以及相关定理,而由于其涉及到的知识较多,又因篇幅关系,不便全部展开一一解释。所以对于此处用到,而能够在他处查证的工具,便没有具体阐述。
最后,我们限制在了 是 数,但不是素数幂的情形下。
若 为合数,则 可分解为 ,其都为奇数且互质。
由算法操作过程,假设 , 为奇数。
则可以得到一个模 下的序列:。
其有两种情况:
并且,第二种情况一定存在,
取 ,则 。
因此,存在一个 ,使得出现 的位置(记为 )最靠后,也就是说,,且 。
则由中国剩余定理的推论:
如果 两两互质,且 ,则对于任意整数 ,关于未知量 的联立方程组 对模有唯一解。
存在 使得
由于 ,故有
所以 ,若 ,则在上面的序列中若 则 ,与 是出现 的位置最靠后的而相矛盾。故 ,则可以得出 是 的真子集。
至此,我们完成了对于 的失误概率分析。那么在足够多次测试后,失误的概率就能被限制到足够小。
从这个证明过程中可以看出,对于其失误概率的分析,却用到了许多如原根,群论等更加深入的数学工具,是将其结合,才得到的这个算法的正确性。但实际上,我自己目前的知识水平有限,对于其中涉及到的知识还了解不深,因此可能目前也还是没有完全吃透。但至少这次,我尝试着把与此算法相关的部分研究了一遍。也算是对自己的一点突破。
而我也不禁好奇,这样的一个算法的提出,究竟是先足够熟练,深刻地了解了各个定理的本质,才足以想到这样的方法,还是先提出了这样的方法后,再用各种工具加以论证?目前的我不得为知,希望今后会有更深的理解。
同样地,在OIwiki和wikipedia中都仅写了结论,而并未给出相关分析。
在一次测试中,对于 及其之后的二次探测至多共有 次,
而每次探测时,若采用快速幂计算 ( 为该次的指数),则一次计算为 ,
同时,对于大数的乘法,需要用类似于快速幂的计算方式,此处有一个 ,但亦可以用快速傅里叶变换优化。
同时,为保证正确性,总共进行 次测试。
则复杂度为 。
一个比较常见的应用是,在RSA中,需要用到足够大的质数,而此时,用质数筛法等可能耗时较多。而由质数分布,我们知道质数的密度并不算小,因此,我们可以枚举一段连续的数字,而判断其中某个数是否为质数,进而得到我们需要的大质数。利用 ,能使这个算法的效率较高。
从最开始的暴力算法,到一步步完善,其实有一个核心的思想:我们并非直接判断 是质数,而是选择判断 不是合数,而更进一步地,将所有 是合数的条件判否。这其中思路的转换,可能有“避其锋芒”的作用,才为整个算法开辟了道路。
而之后, 体现出了概率算法的巧妙姓。将判断一个数的问题转换为在一个小的概率下失误,而在足够多的尝试后将概率压缩至足够小。实际上,这也是概率算法的魅力所在。而概率算法与确定性算法的关系,至今也是人们还在思考的问题。
同时,一个看上去比较普通的问题,却涉及到如此多较复杂的数学工具,也不得不令人感叹数学的神奇,数字的优美。在此背后,是否蕴含着更加深层的奥秘等待人们揭开?是否有一天,我们能够掌握宇宙的真相,似乎值得思考。
以及,在撰写本文时,阅读我自己的笔记,以及网络上相关资料时,我也发现,似乎并没有哪里,将这个算法讲的足够透彻。无论是 wikipdia 还是 OIwiki,或者是各种人们写的 blog,都总是有令人疑惑而无法在其中得到解答的地方。一方面,可能涉及的东西确实过多,但另一方面,也是否体现人们在研究知识时的局限,仅抓住了核心的结论部分,而忽略了一些证明上,或者别的细节?
同样,本文在某些细节上,可能也没有做到完全的清晰,但我还是希望,能尽量把每个部分讲清楚,能让人在看完后不留疑惑,或者是有更多的兴趣研究更深入的问题。
https://www.cnblogs.com/AstatineAi/p/Miller-Rabin-and-Pollard-Rho.html
https://oi-wiki.org/math/number-theory/prime/#miller-rabin-%E7%B4%A0%E6%80%A7%E6%B5%8B%E8%AF%95
https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test
https://www.cnblogs.com/zhixingr/p/6750174.html
Thomas H.Cormen.算法导论[M],第三版,殷建平等译,北京:机械工业出版社,2013:567-571.
]]>