心理科普 | 社会焦点效应

您是否有如下表现:

在公共场合感觉别人嘀嘀咕咕在议论自己。

课堂上回答不出问题,脸憋得通红,觉得别人在嘲笑你。

早起穿错袜子总怕被别人发现。

不洗头、不化妆就不敢出门。

聚餐时,不敢夹菜,也不好意思转桌。

坐公共交通时,坐得特别直,吸肚子,不靠椅背。

人多时,怕别人发现自己,总爱待在角落里。

这是“社会焦点效应”在起作用!

01

社会焦点效应是什么

社会焦点效应,又称为“社会想象受众”效应,是指个体倾向于高估周围人对自己的注意力和关注程度,以及过度估计他人对自己的评价。尽管人们常常认为他人在意自己的外表、行为和言语,但实际上,他人的注意力更多地集中在自身的事务和问题上。这种现象在日常社交互动中普遍存在,它揭示了个体与社会之间错综复杂的联系。

社会焦点效应形成的原因

社会焦点效应的形成涉及多种心理机制和认知偏差,这些因素相互作用,导致个体过分关注自己在他人眼中的形象和地位。以下是几个主要的原因:

自我中心主义:人们天生倾向于将自己放在社会的中心,这就是所谓的自我中心主义。这种倾向使得人们更容易将他人的行为、反应和评价解读为与自己有关,即使实际上并非如此。这种自我中心主义的倾向使得人们更容易过分关注自己,产生社会焦点效应。

注意力分配:个体在处理信息时,会更加敏感地关注与自己有关的信息。这可能导致他们过度解读他人的言行,认为他人更关注自己,而忽视了他人在关注其他事情。个体的注意力偏向会强化社会焦点效应,使其在社交互动中过于关注他人对自己的评价。

自我意识:自我意识是指个体对自身的思考和觉察。在社交情境中,个体会更加关注自己的外貌、行为和言行,因为他们担心这些方面会被他人评价。这种自我意识加剧了个体对他人评价的过度关注,从而促成社会焦点效应。

社会比较:个体倾向于与他人进行比较,以评估自己在社会中的地位。这种比较可能导致过分关注他人对自己的看法,尤其是在那些个体认为在某些方面处于不利地位时。个体可能会过度担心他人对自己的评价,因为他们希望与他人保持相对较高的地位。

自我认同:个体的自我认同和自尊心受到他人的看法和评价的影响。他们倾向于通过他人的反馈来建立自己的身份认同。因此,他们可能过于关注他人对自己的看法,以确保自己的自尊心得到维护。

社会焦点效应对个人的影响

社会焦点效应对个人的影响涵盖了情感、认知和社交层面,可能对情感健康、社交互动和自我感受产生深远影响。以下是一些主要影响:

社交焦虑:社会焦点效应可能导致个体在社交场合中感到紧张、不安和自我意识过强。他们可能会过度关注自己在他人眼中的形象,担心他人会对自己的外貌、言行和行为进行评价。这种社交焦虑可能阻碍个体与他人建立积极的互动,限制其在社交场合中的自由表达。

自尊感受波动:个体可能会过度依赖他人的评价来维持自尊感受。如果他人的反馈是积极的,他们可能会感到自尊心得到满足。然而,如果他人的评价是消极的,他们的自尊感受可能会受到负面影响。这种波动可能导致个体情感不稳定,更容易受到外界因素的影响。

回避行为:为了避免他人的注意和评价,个体可能会倾向于回避社交场合。他们可能会拒绝参加社交活动,避免公开表达自己的观点,或者在公众场合保持沉默。这种回避行为可能导致个体错失社交互动和机会,影响其个人成长和人际关系。

过度自我监控:社会焦点效应可能导致个体在社交互动中过于自我监控。他们会不断关注自己的外貌、言行和言辞,试图使自己表现得符合他人的期望。这种过度自我监控可能会使他们感到疲惫和紧张,降低自我表达的自然性。

情绪调节困难:社会焦点效应可能使个体更容易陷入情绪波动,特别是在面对他人的评价时。积极的反馈可能会使他们感到愉快,而消极的反馈可能会引发沮丧、焦虑和自我怀疑。这种情绪调节困难可能影响个体的情感健康和心理幸福感。

自我认同受损:个体过于关注他人的看法可能导致他们在一定程度上失去自己的独立性和真实性。他们可能会努力适应他人的期望,而忽视了自己真实的兴趣和价值观。这可能导致他们的自我认同受损,难以找到真正与自己内心一致的生活方式和目标。

个人如何应对社会焦点效应

应对社会焦点效应需要一系列积极的策略和心理技巧,以帮助个人更好地应对过度的自我意识和社交焦虑。以下是一些方法:

认知重构:学会识别并纠正自己的思维误区,特别是过度解读他人的行为和反应。通过问自己一些问题,如“我是否有确凿的证据表明他人真的在关注我?”,“我有那么重要吗?”来帮助你理性评估情况。

情感调节:学会有效地管理情绪,以减轻社交焦虑的情绪。使用深呼吸、冥想、放松技巧等方法来缓解焦虑情绪,从而更好地面对社交场合。

自我接纳:培养积极的自我认知和自尊感受。了解自己的价值和优点,不必过分依赖他人的评价来衡量自己的价值。

转移注意力:将注意力从自己转移到他人。关注他人的情感、兴趣和需要,可以减少对自己的过度关注,缓解社会焦点效应。

接受不完美:意识到没有人是完美的,每个人都有失误和尴尬的时候。接受自己的不完美,不要过分自责。

心理科普 | 斯德哥尔摩综合征

斯德哥尔摩综合征是一种心理现象,通常在人们与控制、虐待或威胁他们的人之间形成的特殊关系中出现。这种情况下,被控制或虐待的人表现出对施加虐待者的情感依赖、同情甚至忠诚,尽管这种依赖关系可能是非理性的。

“斯德哥尔摩综合征”这个名词来源于1973年瑞典斯德哥尔摩的一起银行劫持案。在此事件中,两名罪犯在银行里扣押了人质长达六天,并在此期间与人质建立了一种非寻常的情感联系。当警察最终解救了人质,一些人质竟然表现出不愿离开劫持者、为劫持者辩护的行为,这引起了心理学家的注意,将这种特殊情感联系现象命名为斯德哥尔摩综合征。

斯德哥尔摩综合征有哪些特征

正向情感:被虐待者对虐待者产生了情感依赖,甚至表现出积极的情感,如友好、爱意等。

负向情感:被虐待者可能对其他人产生敌意或不信任,而对虐待者却感到保护和信任。

同情虐待者:被虐待者可能会理解虐待者的行为,试图为虐待者辩护,甚至帮助虐待者逃脱处罚。

自我保护机制:被虐待者可能会采取策略来减轻自己的虐待,比如试图与虐待者建立亲密关系,以获得更好的待遇。

情感混乱和内心冲突:被虐待者可能会感到情感上的混乱和内心冲突。他们可能同时感受到对虐待者的恐惧和依赖,这可能导致情感上的紊乱。

自我责备:被虐待者可能会将责任归咎于自己,认为虐待是他们自己的错,从而加深他们对虐待者的依赖。

斯德哥尔摩综合征出现的必要条件

斯德哥尔摩综合征的出现是一个复杂的心理现象,涉及多种心理、情感和环境因素的交互作用。心理学家在研究斯德哥尔摩综合征的案例中提出了四个必要条件,如下:

被虐者的无力与威胁感知:被虐待者在一开始的时候感知到自己无力抵抗虐待者,或者感受到来自虐待者的威胁,可能是生理上的,也可能是心理上的。这种感觉会导致被虐待者试图采取措施来保护自己。

虐待者的善意行为:虐待者在某些情况下可能会表现出善意的行为,比如提供食物、关怀、关注等,虽然这些行为在整个关系中可能是不一致的。这些善意的行为可能导致被虐待者产生对虐待者的感情依赖和同情。

被虐待者对虐待者的感情反应:被虐待者可能会对虐待者产生情感上的反应,可能是积极的,如友好和同情,也可能是消极的,如恐惧。然而,即使存在负面情感,被虐待者仍然可能会寻求与虐待者建立情感联系,以减轻威胁。

被虐待者的感情依赖:随着时间的推移,被虐待者可能会对虐待者产生情感上的依赖,可能会感受到与虐待者的情感联系,尽管这种依赖关系可能是非理性的。被虐待者可能开始为虐待者辩护、保护虐待者,甚至对其他人产生敌意。

从心理视角看斯德哥尔摩综合征

从心理学的视角来看,斯德哥尔摩综合征涉及了一系列的心理和情感机制,这些机制在被虐待者与虐待者之间形成了特殊的情感联系。以下是从心理学角度解释斯德哥尔摩综合征的一些因素:

情感依赖:斯德哥尔摩综合征中的一个关键因素是情感依赖。被虐待者可能由于威胁、恐惧或无助感而开始对虐待者产生情感依赖。这种情感依赖可能是一种应对机制,被虐待者希望通过与虐待者建立情感联系来减轻威胁和提供一定程度的安全感。

情感悖论:斯德哥尔摩综合征在于其悖论性。被虐待者可能在感情上与虐待者产生联系,尽管虐待者是造成他们威胁和痛苦的原因。这种情感上的混乱和内心冲突可能是导致斯德哥尔摩综合征的一部分原因。

心理防御机制:在虐待的情况下,被虐待者可能会启动心理防御机制来减轻情感痛苦。其中一种机制是理性化,被虐待者可能会对虐待行为进行合理化,以减少不愉快的情绪。

情感捆绑:虐待者可能在某些时刻表现出关心、关注或善意,从而在被虐待者心中建立起一种情感捆绑。这种情感捆绑可能导致被虐待者对虐待者产生积极情感。

心理失调:在虐待关系中,被虐待者的心理可能出现失调。他们可能难以区分现实和虚幻,从而导致对虐待者产生情感上的依赖,试图适应虐待者的期望。

认同和同化:被虐待者可能为了减轻虐待带来的压力,开始认同虐待者的观点、价值观或行为方式。这可能是为了获得虐待者的认可和保护。

总的来说,斯德哥尔摩综合征涉及情感、认知和行为的复杂交互作用。它揭示了在极端情况下,个体在控制和虐待的环境中如何试图应对,并可能导致他们与虐待者之间建立特殊的情感联系。

受虐者如何摆脱这种心理?

摆脱斯德哥尔摩综合征是一个复杂的过程,需要时间、支持和心理帮助。以下是一些可能有助于受虐者摆脱这种心理状态的方法:

寻求专业帮助:心理咨询师可以提供专业的支持和指导。认知行为疗法、心理动力学疗法、情感焦点疗法等方法可以帮助受虐者理解自己的情感和行为,并找到应对策略。

建立社会支持:与家人、朋友、同事等建立健康的社会支持网络。分享自己的感受和经历,可以减轻孤立感,并获得情感支持和建议。

自我意识和教育:了解斯德哥尔摩综合征的特征和影响,可以帮助受虐者认识到自己所处的情况,并开始思考如何改变。

逐步恢复独立性:逐步恢复自己的独立性和自主性,重新建立对自己生活的控制感。这可能包括在日常生活中做出自主的决策和行动。

设立健康边界:学会设立健康的个人和情感边界,不再容忍不健康的关系和行为。

心理科普-自证陷阱

“自证陷阱”是指当一个人被误解或质疑时,试图通过解释或证明来消除误解。然而,这种解释行为在沟通双方存在认知偏差的情况下,往往会适得其反。越是试图证明自己的清白、正确或合理性,越是让对方找到新的“攻击”点,反而加深了误解。

理解自证陷阱的核心需要从心理学中的几个重要概念入手。这些概念揭示了为什么人在沟通中容易陷入这样的困境,以及“自证”行为为何适得其反。

1、认知偏差与先入为主

在沟通过程中,认知偏差会导致误解的形成与加深。尤其是先入为主的观念,会使对方对解释内容产生偏见。例如,对方如果已经相信某人“不可信”或“做错事”,他就更容易在沟通中倾向于筛选出那些符合这一观点的信息,忽略或扭曲其他解释。此时,解释者的“自证”行为,反而可能成为对方进一步质疑的依据,因为对方的“确认偏差”使得他更倾向于接收负面信息。

2、防御性沟通

防御性沟通是指沟通者在反复解释的过程中,可能会逐渐带有一种焦虑、防御或不安的情绪,而这种情绪恰恰会被对方察觉到,并误解为“心虚”或“掩饰”。这种误解加深了对方对自证者的怀疑,形成了自证陷阱的恶性循环。

3、归因错误

当双方存在冲突时,误解的一方往往会将自证者的行为归因于“人品问题”或“本质性缺陷”,而忽略情境或误解本身的作用。对方倾向于认为,自证行为是自证者试图掩盖真实问题的表现。这种归因偏差使得自证陷阱难以打破,因为自证者的每一次证明行为都被解读为“掩饰”。

以下几个方法可以帮助我们减少自证陷阱带来的负面影响:

1、减少不必要的解释

当被误解时,首先要避免进入反复解释的循环。如果对方怀有偏见,那么任何解释都有可能被对方曲解。因此,保持冷静,简单陈述事实,避免过度解释。过多的解释只会提供更多让对方质疑的材料,不如适可而止,将解释控制在合理的范围内。

2、设立沟通边界

在面对误解或质疑时,设立边界是避免自证陷阱的重要策略之一。我们不必为了他人的误解而一再证明自己,尤其是当对方态度固执、无法沟通时。学会在适当时刻拒绝过度解释,让对方意识到自己的界限,避免陷入不必要的心理消耗。

3、转换沟通焦点

一个有效的策略是将沟通的焦点从“自证”转向“共情与理解”。尝试去理解对方的情感需求,避免让自己陷入被动的防御角色。例如,可以询问对方的期望或顾虑,避免直接解释自己的行为。这样一来,沟通会变得更具建设性,减少对方质疑的可能性。

4、保持内心的平静与自信

当面对他人质疑时,内心的平静和自信非常重要。自证陷阱中,人们往往会因他人误解而急于解释,而急躁的情绪容易被对方察觉到,从而引发更深层次的怀疑。因此,保持内心的平和,相信自己的真实意图,不急于解释或辩护,往往能够有效缓解对方的疑虑。

5、借助第三方视角

当沟通进入僵局时,可以借助第三方的视角来提供公正的判断。例如,在工作中可以让团队中的其他成员参与讨论,或是在亲密关系中让双方共同信任的朋友给予意见。第三方的介入可以帮助双方跳出自证陷阱,获得更加客观的判断。

客户评价中的“坏比好强大”。

客户评价中的“坏比好强大”。

假设5分制的客户评价系统,不同的顾客,给你的服务打了不同分数,你应该重点关注谁、优先关注谁?昆士兰大学的心理学家罗伊建议如下:

  • 第一优先,是盯紧1分的差评,否则损害巨大。
  • 其次是把4分的,服务得更满意,争取下次让他打5分。
  • 效能最低的,是把2分变为3分,3分变为4分。


也就是说,首先要避免客户的最差体验,其次是要打造客户的最好体验。

本地网突破VLAN

VLAN是网络交换机的一种技术,可以把不同的电脑划分在不同的网络中,使其不能互相通信,以达到提高安全性和网络性能的目的。要跨越VLAN通信,必须借助路由器或者3层交换模块。

我市的ADSL网在接入层使用了Cisco的Catalyst6509型三层交换机,在端口上设置了VLAN,把同一IP段的用户相互隔开。这样一来,安全性倒是高了,可惜平时打游戏、共享文件都不方便,需要通过其他IP段的朋友中转。于是突破VLAN成了当务之急。

我所在的段,IP地址为10.145.254.x,网关为10.145.254.1,采用24位的掩码。因为是同一IP段,所以电脑不会自动通过路由器转发数据,而妄想通过直接通信的方式联络对方电脑。在VLAN环境下,ARP是收不到回应的,因此直接通信也就被禁止了。 

要想实现互通,必须强制电脑把数据发给路由器。

大家知道,路由器判断一个数据包是否应该转发,是以其2层地址和3层地址为依据的。如果该数据包的目标MAC地址不是路由器接口的MAC地址,那么这个包根本不是给路由器的,而是HUB产生的1层广播包;如果目标MAC地址与路由器接口的MAC地址相同,但目标IP地址并不是路由器接口的IP地址,这个包就应该被路由出去;如果目标MAC地址和IP地址都与路由器接口相匹配,那这个数据包是发给路由器本身的。

电脑在发送数据时,先判断目标和自己是否在同一IP网段。如果在同一网段,则发送ARP请求,查询对方的MAC地址,然后封包发送;如果不在同一IP网段,则发送数据包,并把包头中目标MAC地址设置为本子网的路由器接口的MAC地址,而目标IP地址则是最终接收数据的电脑的IP地址的数据包。路由器收到数据以后,就会把它转发出去。

要强制电脑把本来直接发送的数据发给路由器,可以从ARP协议上下手,也可以从判断是否同一网段的过程下手。

使用ARP的方法如下:

1、使用arp -a命令查看已知的MAC列表。因为目前VLAN里只有网关和本身,所以这里只显示网关的MAC地址:

  Internet Address      Physical Address      Type

10.145.254.1          00-d0-04-14-af-fc     dynamic

2、使用arp -s命令把要与之通信的电脑的IP地址和网关的MAC地址强行捆绑。这样,这台电脑就会把发给对方的数据发给路由器。对方的机器也要运行这个命令,不过IP地址要指定为这一台。比如10.145.254.a和10.145.254.b通信,要在10.145.254.a上运行arp -s 10.145.254.b 00-d0-04-14-af-fc ,而在10.145.254.b上运行arp -s 10.145.254.a 00-d0-04-14-af-fc 

经过这样的设置,电脑还以为自己在直接发送数据,而路由器以为自己收到了需要路由的数据包。这其实是一种ARP欺骗技术。

使用同子网判断的方法如下:

双方使用route命令,建立一条“主机路由”。所谓主机路由,就是针对一台电脑而不是一个IP网段的路由项目,其目标掩码为255.255.255.255。

在10.145.254.a上运行route ADD 10.145.254.b MASK 255.255.255.255  10.145.254.1 ,在10.145.254.b上运行route ADD 10.145.254.a MASK 255.255.255.255  10.145.254.1即可。根据路由的最长匹配原则,电脑在发送数据时会选择掩码里1比较多的那一项,也就是主机路由(32位掩码),而不会认为对方和自己同一子网(24位掩码)。

使用以上两种方法,就可以突破VLAN的限制了。除了CS的LAN Gane还不能正常运行(Internet Game正常)以外,其他软件均运行正常。

现在是一个高人的介绍哈:

第一:如果该数据包的目标MAC地址不是路由器接口的MAC地址,那么这个包根本不是给路由器的,而是HUB产生的1层广播包;

点评:这句话表达有失偏颇!问题的关键在于“HUB产生的1层广播报文”这是一句让人昏倒在地的说法哦!

分析:

1、路由器可以分解为一个独立的物理层+数据链路层+网络层构成转发决策机构!当然路由器还有自己的七层(严格的说是TCP/IP的第四层应用层)。现在我们仅仅需要关心的就是路由器的二层+三层这两个层面;

2、如果路由器一个interface接受到的报文的DMAC不等于interface的MAC,那么路由器认为这个报文不是自己的,或者这个报文不需要自己进行处理,所以路由器将直接discard这个报文!

所以“HUB产生的1层广播报文”犯下了严重的阶级路线错误!

第二:如果目标MAC地址与路由器接口的MAC地址相同,但目标IP地址并不是路由器接口的IP地址,这个包就应该被路由出去;

点评:这句话表达基本上没有什么错误!

分析:

1、路由器接口接收到报文如果DMAC=路由器接口自身的MAC,那么路由器将尽义务进行下一步的处理,那就是到底将这个报文进行路由转发还是给我CPU自己使用;

2、但是究竟是进行转发还是提交给CPU,这已经不是二层是事情了,各司其职嘛。二层协议栈还要将这个报文的二层帧头、FCS这个尾巴去掉提交给三层!

3、三层收到IP包之后,那么判断这个IP包的DIP等不等于接口IP地址,如果不等于,那么表示的是这个报文不需要提交CPU高层协议栈。需要进行路由转发处理!

一旦路由器作出路由转发处理,那么路由器接下来的工作是不是就是查询路由表,看看把这个瘟神从那个接口送走了之!

第三:如果目标MAC地址和IP地址都与路由器接口相匹配,那这个数据包是发给路由器本身的。

点评:这句话表达没有什么错误!

分析:

1、这个观点还是延续的上一个说法,那就是路由器二层协议栈将报文的帧头、FCS尾巴去掉之后,提交给协议栈的三层---网络层;

2、三层收到IP包之后,那么判断这个IP包的DIP等不等于接口IP地址,如果不等于,那么表示的是这个报文不需要提交CPU高层协议栈。需要进行路由转发处理!

3、那么如果DIP=接口IP,三层就认为这个报文需要提交给CPU来处理!

一旦路由器作出判定,这个报文需要提交给CPU,那么三层协议栈就需要将报文的IP包头删除,将剩余的部分(就是IP包的内容部分哈)提交给四层或者直接提交给ICMP等协议高层处理

下面是高人的一个交换原理的模型图片,出处[url]www.cditlab.com[/url]

摘自
https://blog.51cto.com/xiong/17780

区块链-可编程支付原理

摘自https://liaoxuefeng.com/books/blockchain/bitcoin/pay/index.html

区块链创建交易的方法是:小明声称他给了小红一万块钱,只要能验证这个声明确实是小明作出的,并且小明真的有1万块钱,那么这笔交易就被认为是有效的

如何验证这个声明确实是小明作出的呢?数字签名就可以验证这个声明是否是小明做的,并且,一旦验证通过,小明是无法抵赖的。

在比特币交易中,付款方就是通过数字签名来证明自己拥有某一笔比特币,并且,要把这笔比特币转移给指定的收款方

比特币的公钥是根据私钥计算出来的

比特币的地址并不是公钥,而是公钥的哈希,即从公钥能推导出地址,但从地址不能反推公钥,因为哈希函数是单向函数

签名算法是使用私钥签名,公钥验证的方法,对一个消息的真伪进行确认。如果一个人持有私钥,他就可以使用私钥对任意的消息进行签名,即通过私钥sk对消息message进行签名,得到signature

signature = sign(message, sk);

签名的目的是为了证明,该消息确实是由持有私钥sk的人发出的,任何其他人都可以对签名进行验证。验证方法是,由私钥持有人公开对应的公钥pk,其他人用公钥pk对消息message和签名signature进行验证:

isValid = verify(message, signature, pk);

如果验证通过,则可以证明该消息确实是由持有私钥sk的人发出的,并且未经过篡改。

对消息进行签名,实际上是对消息的哈希进行签名,这样可以使任意长度的消息在签名前先转换为固定长度的哈希数据。对哈希进行签名相当于保证了原始消息的不可伪造性。

下面来说可编程支付的原理

比特币的所有交易的信息都被记录在比特币的区块链中,任何用户都可以通过公钥查询到某个交易的输入和输出金额。当某个用户希望花费一个输出时,例如,小明想要把某个公钥地址的输出支付给小红,他就需要使用自己的私钥对这笔交易进行签名,而矿工验证这笔交易的签名是有效的之后,就会把这笔交易打包到区块中,从而使得这笔交易被确认。

但比特币的支付实际上并不是直接支付到对方的地址,而是一个脚本,这个脚本的意思是:谁能够提供另外一个脚本,让这两个脚本能顺利执行通过,谁就能花掉这笔钱:

FROM: UTXO Hash#index
AMOUNT: 0.5 btc
TO: OP_DUP OP_HASH160 <address> OP_EQUALVERIFY OP_CHECKSIG

所以,比特币交易的输出是一个锁定脚本(一般是资产转入方地址),而下一个交易的输入是一个解锁脚本(里面包括资产转入方的公钥和签名)。必须提供一个解锁脚本,让锁定脚本正确运行,那么该输入有效,就可以花费该输出。

我们以真实的比特币交易为例,某个交易的某个输出脚本是76a914dc...489c88ac这样的二进制数据,注意这里的二进制数据是用十六进制表示的,而花费该输出的某个交易的输入脚本是48304502...14cf740f这样的二进制数据,也是十六进制表示的:

   ┌─────────────────────────────────────────────────────────────────────┐
   │tx:ada3f1f426ad46226fdce0ec8f795dcbd05780fd17f76f5dcf67cfbfd35d54de  │
   ├──────────────────────────────────┬──────────────────────────────────┤
   │                                  │1M6Bzo23yqad8YwzTeRapGXQ76Pb9RRJYJ│──┐
   │                                  ├──────────────────────────────────┤  │
   │                                  │18gJ3jeLdMnr9g3EcbRzXwNssYEN5yFHKE│  │
   │3JXRVxhrk2o9f4w3cQchBLwUeegJBj6BEp├──────────────────────────────────┤  │
   │                                  │1A5Mp8jHcMJEqZUmcsbmtqXfsiGdWYmp6y│  │
   │                                  ├──────────────────────────────────┤  │
   │                                  │3JXRVxhrk2o9f4w3cQchBLwUeegJBj6BEp│  │
   └──────────────────────────────────┴──────────────────────────────────┘  │
┌───────────────────────────────────────────────────────────────────────────┘
│   script: 76a914dc5dc65c7e6cc3c404c6dd79d83b22b2fe9f489c88ac
│
│  ┌─────────────────────────────────────────────────────────────────────┐
│  │tx:55142366a67beda9d3ba9bfbd6166e8e95c4931a2b44e5b44b5685597e4c8774  │
│  ├──────────────────────────────────┬──────────────────────────────────┤
└─>│1M6Bzo23yqad8YwzTeRapGXQ76Pb9RRJYJ│13Kb2ykVGpNTJbxwnrfoyZAwgd4ZpXHv2q│
   └──────────────────────────────────┴──────────────────────────────────┘
    script: 4830450221008ecb5ab06e62a67e320880db70ee8a7020503a055d7c45b7
            3dcc41adf01ea9f602203a0d8f4314342636a6a473fc0b4dd4e49b62be28
            8f0a4d5a23a8f488a768fa9b012103dd8763f8c3db6b77bee743ddafd33c
            969a99cde9278deb441b09ad7c14cf740f

我们先来看锁定脚本,锁定脚本的第一个字节76翻译成比特币脚本的字节码就是OP_DUPa9翻译成比特币脚本的字节码就是OP_HASH16014表示这是一个20字节的数据,注意十六进制的14换算成十进制是20,于是我们得到20字节的数据。最后两个字节,88表示字节码OP_EQUALVERIFYac表示字节码OP_CHECKSIG,所以整个锁定脚本是:

        OP_DUP 76
    OP_HASH160 a9
          DATA 14 (dc5dc65c...fe9f489c)
OP_EQUALVERIFY 88
   OP_CHECKSIG ac

我们再来看解锁脚本。解锁脚本的第一个字节48表示一个72字节长度的数据,因为十六进制的48换算成十进制是72。接下来的字节21表示一个33字节长度的数据。因此,该解锁脚本实际上只有两个数据。

DATA 48 (30450221...68fa9b01)
DATA 21 (03dd8763...14cf740f)

接下来,我们就需要验证这个交易是否有效。要验证这个交易,首先,我们要把解锁脚本和锁定脚本拼到一块,然后,开始执行这个脚本:

          DATA 48 (30450221...68fa9b01)
          DATA 21 (03dd8763...14cf740f)
        OP_DUP 76
    OP_HASH160 a9
          DATA 14 (dc5dc65c...fe9f489c)
OP_EQUALVERIFY 88
   OP_CHECKSIG ac

比特币脚本是一种基于栈结构的编程语言,所以,我们要先准备一个空栈,用来执行比特币脚本。然后,我们执行第一行代码,由于第一行代码是数据,所以直接把数据压栈:

│                   │
│                   │
│                   │
│                   │
│                   │
│                   │
├───────────────────┤
│30450221...68fa9b01│
└───────────────────┘

紧接着执行第二行代码,第二行代码也是数据,所以直接把数据压栈:

│                   │
│                   │
│                   │
│                   │
├───────────────────┤
│03dd8763...14cf740f│
├───────────────────┤
│30450221...68fa9b01│
└───────────────────┘

接下来执行OP_DUP指令,这条指令会把栈顶的元素复制一份,因此,我们现在的栈里面一共有3份数据:

│                   │
│                   │
├───────────────────┤
│03dd8763...14cf740f│
├───────────────────┤
│03dd8763...14cf740f│
├───────────────────┤
│30450221...68fa9b01│
└───────────────────┘

然后,执行OP_HASH160指令,这条指令会计算栈顶数据的hash160,也就是先计算SHA-256,再计算RipeMD160。对十六进制数据03dd8763f8c3db6b77bee743ddafd33c969a99cde9278deb441b09ad7c14cf740f计算hash160后得到结果dc5dc65c7e6cc3c404c6dd79d83b22b2fe9f489c,然后用结果替换栈顶数据:

│                   │
│                   │
├───────────────────┤
│dc5dc65c...fe9f489c│
├───────────────────┤
│03dd8763...14cf740f│
├───────────────────┤
│30450221...68fa9b01│
└───────────────────┘

接下来的指令是一条数据,所以直接压栈:

├───────────────────┤
│dc5dc65c...fe9f489c│
├───────────────────┤
│dc5dc65c...fe9f489c│
├───────────────────┤
│03dd8763...14cf740f│
├───────────────────┤
│30450221...68fa9b01│
└───────────────────┘

然后,执行OP_EQUALVERIFY指令,它比较栈顶两份数据是否相同,如果相同,则验证通过,脚本将继续执行,如果不同,则验证失败,整个脚本就执行失败了。在这个脚本中,栈顶的两个元素是相同的,所以验证通过,脚本将继续执行:

│                   │
│                   │
│                   │
│                   │
├───────────────────┤
│03dd8763...14cf740f│
├───────────────────┤
│30450221...68fa9b01│
└───────────────────┘

最后,执行OP_CHECKSIG指令,它使用栈顶的两份数据,第一份数据被看作公钥,第二份数据被看作签名,这条指令就是用公钥来验证签名是否有效。根据验证结果,成功存入1,失败存入0

│                   │
│                   │
│                   │
│                   │
│                   │
│                   │
├───────────────────┤
│1                  │
└───────────────────┘

最后,当整个脚本执行结束后,检查栈顶元素是否为0,如果不为0,那么整个脚本就执行成功,这笔交易就被验证为有效的。

上述代码执行过程非常简单,因为比特币的脚本不含条件判断、循环等复杂结构。上述脚本就是对输入的两个数据视作签名和公钥,然后先验证公钥哈希是否与地址相同,再根据公钥验证签名,这种标准脚本称之为P2PKH(Pay to Public Key Hash)脚本。

输出

当小明给小红支付一笔比特币时,实际上小明创建了一个锁定脚本,该锁定脚本中引入了小红的地址。要想通过解锁脚本花费该输出,只有持有对应私钥的小红才能创建正确的解锁脚本(因为解锁脚本包含的签名只有小红的私钥才能创建),因此,小红事实上拥有了花费该输出的权利。

使用钱包软件创建的交易都是标准的支付脚本,但是,比特币的交易本质是成功执行解锁脚本和锁定脚本,所以,可以编写各种符合条件的脚本。比如,有人创建了一个交易,它的锁定脚本像这样:

OP_HASH256
      DATA 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000
  OP_EQUAL

这有点像一个数学谜题。它的意思是说,谁能够提供一个数据,它的hash256等于6fe28c0a...,谁就可以花费这笔输出。所以,解锁脚本实际上只需要提供一个正确的数据,就可以花费这笔输出。点这里查看谁花费了该输出。

比特币的脚本通过不同的指令还可以实现更灵活的功能。例如,多重签名可以让一笔交易只有在多数人同意的情况下才能够进行。最常见的多重签名脚本可以提供3个签名,只要任意两个签名被验证成功,这笔交易就可以成功。

FROM: UTXO Hash#index
AMOUNT: 10.5 btc
TO: P2SH: OP_2 pk1 pk2 pk3 OP_3 OP_CHECKMULTISIG

也就是说,3个人中,只要任意两个人同意用他们的私钥提供签名,就可以完成交易。这种方式也可以一定程度上防止丢失私钥的风险。3个人中如果只有一个人丢失了私钥,仍然可以保证这笔输出是可以被花费的。

支付的本质

从比特币支付的脚本可以看出,比特币支付的本质是由程序触发的数字资产转移。这种支付方式无需信任中介的参与,可以在零信任的基础上完成数字资产的交易,这也是为什么数字货币又被称为可编程的货币。

由此催生出了智能合约:当一个预先编好的条件被触发时,智能合约可以自动执行相应的程序,自动完成数字资产的转移。保险、贷款等金融活动在将来都可以以智能合约的形式执行。智能合约以程序来替代传统的纸质文件条款,并由计算机强制执行,将具有更低的信任成本和运营成本。

小结

比特币采用脚本的方式进行可编程支付:通过执行解锁脚本确认某个UTXO的资产可以被私钥持有人转移给其他人。

Android开发环境搭建

JDK下载

下载链接:https://www.oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html

或者直接华军下载,一般为Sun Java SE Development Kit (JDK)  文件然后安装
安装后环境变量的path中加入刚安装的地址,带bin

再在系统环境变量加一个JAVA_HOME,地址就是刚安装地址,不带bin

java -version提示安装结果,

AndroidStudio下载

https://www.androiddevtools.cn/android-studio.html

下载安装

安装SDK

国内网络有时tool是下载不下来的,这时就要设置代理

安装插件的时候发现,如果没有代理就无法安装插件,于是开始设置代理

在自动检测那一栏填 http://mirrors.tuna.tsinghua.edu.cn:80 参考

连接手机

https://blog.csdn.net/Winkyyyyyy/article/details/142604413

ReLU激活函数

激活函数在深度学习中起着至关重要的作用,它们将神经元的输入映射到输出,引入非线性因素,使得神经网络能够学习和表示复杂的模式。以下是几种常见的激活函数及其特点

ReLU(Rectified Linear Unit,修正线性单元)是一种在深度学习中广泛使用的激活函数。其数学定义为:

ReLU(x)=max⁡(0,x)ReLU(x)=max(0,x)

这意味着当输入 xx 大于零时,ReLU 函数的输出等于输入值本身;当输入 xx 小于或等于零时,输出为零

ReLU 的优点:

  1. 避免梯度消失问题:由于在正数区域的导数为1,ReLU 函数能够有效缓解梯度消失问题,从而帮助神经网络更快地收敛。
  2. 计算效率高:ReLU 的计算非常简单,只需要一个最大值操作,因此在深层网络中具有较高的计算效率
  3. 简单易实现:ReLU 函数的实现非常简单,通常只需要比较输入值和零的大小,然后取较大值。

ReLU 的缺点:

  1. 神经元死亡问题:当输入为负数时,ReLU 的输出为零,这可能导致部分神经元在训练过程中“死亡”,即这些神经元的输出永远为零,无法再对任何数据做出响应。
  2. 非零均值问题:由于负数部分被置为零,ReLU 的输出通常不具有零均值,这可能影响后续层的训练效果

ReLU 的变种:

为了克服 ReLU 的缺点,研究者提出了多种变种激活函数,如:

  • Leaky ReLU:在负数区域引入一个小的线性分量,以避免神经元死亡问题。
  • Parametric ReLU (PReLU) :在负数区域的斜率可以作为参数进行调整,进一步优化网络性能。
  • ELU (Exponential Linear Unit) :在负数区域引入指数函数,使得输出更接近零均值,并加速学习。

应用场景:

ReLU 及其变种在卷积神经网络(CNN)和深层神经网络中被广泛使用,尤其是在图像识别、自然语言处理等领域表现优异

ReLU 是一种高效且常用的激活函数,尽管存在一些缺点,但其优点使其成为深度学习中的首选激活函数之一

零基础AI入门指南

本文以工程师的视角从零开始搭建并运行一个AI小模型,并把它完全运行起来以理解AI的工作原理,非常接地气。

AI模型是如何工作的

神经网络是AI的一种重要的计算模型,深度学习是通过神经网络实现特征学习和模式分析,大量用于图像识别等领域。我们以最基础的手写数字识别为例,看看一个神经网络的AI模型是如何工作的。

MNIST(Modified National Institute of Stands and Technology)是一个开源的数据集,它包含了6万个手写的数字图像,每个图像都是28×28黑底白字:

mnist-preview

有了这个开源的数据集,我们就可以训练一个识别手写数字的AI模型,这个练习堪称AI界的“Hello, world”。

要编写这个AI模型,我们需要使用一种称为卷积神经网络(CNN:Convolutional Neural Network)的神经网络结构,具体到代码层面,则需要使用PyTorch这样的训练框架。PyTorch底层用C++开发,外层用Python调用,非常方便易用。先确保机器安装了Python3,然后,安装PyTorch 2:

pip install torch torchvision torchaudio

如果本机有CUDA环境,也可以安装GPU版本,训练速度更快。

编写模型

准备好环境后,我们开始编写模型。先让AI写一个用CNN识别MNIST数据集的PyTorch代码:

import torch.nn as nn

class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.fc1 = nn.Linear(in_features=64 * 5 * 5, out_features=128)
        self.fc2 = nn.Linear(in_features=128, out_features=10)

    def forward(self, x):
        x = nn.functional.relu(self.conv1(x))
        x = nn.functional.max_pool2d(x, kernel_size=2)
        x = nn.functional.relu(self.conv2(x))
        x = nn.functional.max_pool2d(x, kernel_size=2)
        x = x.view(-1, 64 * 5 * 5)
        x = nn.functional.relu(self.fc1(x))
        x = self.fc2(x)
        return x

看不懂不要紧,可以接着问AI,它会告诉我们,这个神经网络定义了两个CNN卷积层和两个全连接层,总的来说就是,这个模型定义了2层卷积网络加2层全连接层,输入为1通道图片,经过卷积和池化后进入全连接层,最后输出10个分类结果,分别代表0~9这10个数字。

训练

接下来我们要使用MNIST数据集来训练这个模型。受益于PyTorch这个框架,我们连下载和读取数据集都省了,因为PyTorch已经集成了这个数据集,直接下载、加载、训练,一步到位:

from time import time

import torch
import torch.nn as nn
import torch.optim as optim

from torchvision import datasets
from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor

from model import NeuralNetwork

def train(dataloader, device, model, loss_fn, optimizer):
    model.train()
    running_loss = 0.0
    for batch, (inputs, labels) in enumerate(dataloader):
        inputs = inputs.to(device)
        labels = labels.to(device)
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = loss_fn(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    print(f'loss: {running_loss/len(dataloader):>0.3f}')

def test(dataloader, device, model):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for inputs, labels in dataloader:
            inputs = inputs.to(device)
            labels = labels.to(device)
            outputs = model(inputs)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    print(f'accuracy: {100.0*correct/total:>0.2f} %')

def main():
    print('loading training data...')
    train_data = datasets.MNIST(
        root='./data', train=True, download=True, transform=ToTensor())
    print('loading test data...')
    test_data = datasets.MNIST(
        root='./data', train=False, download=True, transform=ToTensor())

    train_dataloader = DataLoader(train_data, batch_size=64)
    test_dataloader = DataLoader(test_data, batch_size=64)

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    print(f'using {device}')
    model = NeuralNetwork().to(device)
    print(model)

    loss_fn = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    epochs = 5
    for t in range(epochs):
        start_time = time()
        print(f'epoch {t+1} / {epochs}\n--------------------')
        train(train_dataloader, device, model, loss_fn, optimizer)
        test(test_dataloader, device, model)
        end_time = time()
        print(f'time: {end_time-start_time:>0.2f} seconds')
    print('done!')
    path = 'mnist.pth'
    torch.save(model.state_dict(), path)
    print(f'model saved: {path}')

if __name__ == '__main__':
    main()

数据集分两部分:一个用于训练,一个用于测试训练效果,用PyTorch的datasets.MNIST()自动下载、解压并加载数据集(解压后约55M数据,仅第一次需要下载)。然后,定义损失函数和优化器,用train()做训练,用test()测试训练效果,训练5次,运行结果如下:

$ python3 train.py
loading training data...
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
...第一次运行会自动下载数据到data目录并解压...

loading test data...
using cpu
NeuralNetwork(
  (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1))
  (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
  (fc1): Linear(in_features=1600, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)
epoch 1 / 5
--------------------
loss: 0.177
accuracy: 97.21 %
time: 30.96 seconds
epoch 2 / 5
--------------------
loss: 0.053
accuracy: 98.62 %
time: 32.24 seconds
epoch 3 / 5
--------------------
loss: 0.035
accuracy: 98.70 %
time: 33.70 seconds
epoch 4 / 5
--------------------
loss: 0.025
accuracy: 98.90 %
time: 35.10 seconds
epoch 5 / 5
--------------------
loss: 0.018
accuracy: 98.95 %
time: 32.02 seconds
done!
model saved: mnist.pth

经过5轮训练,每轮耗时约30秒(这里用CPU训练,如果是GPU则可以大大提速),准确率可以达到99%。训练结束后,将模型保存至mnist.pth文件。

使用模型

有了预训练的模型后,我们就可以用实际的手写图片测试一下。用PS手绘几张手写数字图片,测试代码如下:

import torch
from torchvision import transforms

from PIL import Image, ImageOps
from model import NeuralNetwork

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'using {device}')
model = NeuralNetwork().to(device)
path = './mnist.pth'
model.load_state_dict(torch.load(path))
print(f'loaded model from {path}')
print(model)

def test(path):
    print(f'test {path}...')
    image = Image.open(path).convert('RGB').resize((28, 28))
    image = ImageOps.invert(image)

    trans = transforms.Compose([
        transforms.Grayscale(1),
        transforms.ToTensor()
    ])
    image_tensor = trans(image).unsqueeze(0).to(device)
    model.eval()
    with torch.no_grad():
        output = model(image_tensor)
        probs = torch.nn.functional.softmax(output[0], 0)
    predict = torch.argmax(probs).item()
    return predict, probs[predict], probs

def main():
    for i in range(10):
        predict, prob, probs = test(f'./input/test-{i}.png')
        print(f'expected {i}, actual {predict}, {prob}, {probs}')


if __name__ == '__main__':
    main()

因为训练时输入的图片是黑底白字,而测试图片是白底黑字,所以先用PIL把图片处理成28×28的黑底白字,再测试,结果如下:

$ python3 test.py 
using cpu
loaded model from ./mnist.pth
NeuralNetwork(
  (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1))
  (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
  (fc1): Linear(in_features=1600, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)
test ./input/test-0.png...
expected 0, actual 0, 0.9999996423721313, tensor([1.0000e+00, 2.3184e-10, 1.7075e-08, 7.6250e-16, 1.2966e-12, 5.7179e-11,
        2.1766e-07, 1.8820e-12, 1.1260e-07, 2.2463e-09])
...

以图片0为例,我们要使用模型,需要把输入图片变成模型可接受的参数,实际上是一个Tensor(张量),可以理解为任意维度的数组,而模型的输出也是一个Tensor,它是一个包含10个元素的1维数组,分别表示每个输出的概率。对图片0的输出如下:

  • 1.0000e+00
  • 2.3184e-10
  • 1.7075e-08
  • 7.6250e-16
  • 1.2966e-12
  • 5.7179e-11
  • 2.1766e-07
  • 1.8820e-12
  • 1.1260e-07
  • 2.2463e-09

除了第一个输出为1,其他输出都非常接近于0,可见模型以99.99996423721313%的概率认为图片是0,是其他数字的概率低到接近于0。

因此,这个MNIST模型实际上是一个图片分类器,或者说预测器,它针对任意图片输入,都会以概率形式给出10个预测,我们找出接近于1的输出,就是分类器给出的预测。

产品化

虽然我们已经有了预训练模型,也可以用模型进行手写数字识别,但是,要让用户能方便地使用这个模型,还需要进一步优化,至少需要提供一个UI。我们让AI写一个简单的页面,允许用户在页面用鼠标手写数字,然后,通过API获得识别结果:

mnist-ui

因此,最后一步是把模型的输入输出用API封装一下。因为模型基于PyTorch,所以使用Python的Flask框架是最简单的。API实现如下:

import base64
import torch
from io import BytesIO
from PIL import Image
from flask import Flask, request, redirect, jsonify
from torchvision import transforms
from model import NeuralNetwork

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'using {device}')
model = NeuralNetwork().to(device)
path = './mnist.pth'
model.load_state_dict(torch.load(path))
print(f'loaded model from {path}')
print(model)
params = model.state_dict()
print(params)

app = Flask(__name__)

@app.route('/')
def index():
    return redirect('/static/index.html')

@app.route('/api', methods=['POST'])
def api():
    data = request.get_json()
    image_data = base64.b64decode(data['image'])
    image = Image.open(BytesIO(image_data))
    trans = transforms.Compose([
        transforms.Grayscale(1),
        transforms.ToTensor()
    ])
    image_tensor = trans(image).unsqueeze(0).to(device)
    model.eval()
    with torch.no_grad():
        output = model(image_tensor)
        probs = torch.nn.functional.softmax(output[0], 0)
    predict = torch.argmax(probs).item()
    prob = probs[predict]
    print(f'predict: {predict}, prob: {prob}, probs: {probs}')
    return jsonify({
        'result': predict,
        'probability': prob.item()
    })

if __name__ == '__main__':
    app.run(port=5000)

上述代码实现了一个简单的API服务,注意尚未对并发访问做处理,所以只能算一个可用的DEMO。

思考

对于AI程序,我们发现,模型定义非常简单,一共也就20行代码。训练代码也很少,不超过100行。它和传统的程序最大的区别在哪呢?

无论是传统的程序,还是AI程序,在计算机看来,本质上是一样的,即给定一个输入,通过一个函数计算,获得输出。不同点在于,对于传统程序,输入是非常简单的,例如用户注册,仅仅需要几个字段,而处理函数少则几千行,多则几十万行。虽然代码量很大,但执行路径却非常清晰,通过跟踪执行,能轻易获得一个确定的执行路径,从而最终获得一个确定性的结果。确定性就是传统程序的特点,或者说,传统程序的代码量虽然大,但输入简单,路径清晰:

f(x1, x2, x3)
  │
  ▼
 ┌─┐◀─┐
 └─┘  │
  │   │
  ▼   │
 ┌─┐  │
 └─┘  │
  │   │
  ▼   │
 ┌─┐──┘
 └─┘
  │
  ▼
 ┌─┐
 └─┘

AI程序则不同,它只经过几层计算,复杂的大模型也就100来层,就可以输出结果。但是,它的输入数据量大,每一层的数据量更大,就像一个扁平的巨大函数:

       f(x1, x2, x3, ... , x998, x999, x1000)
         │   │   │   │   │   │   │   │   │
         ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼
        ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
        └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
         │   │   │   │   │   │   │   │   │
 ┌───┬───┼───┼───┼───┼───┼───┼───┼───┼───┼───┬───┐
 ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼
┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
└─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
 │   │   │   │   │   │   │   │   │   │   │   │   │
 └───┴───┴───┴───┼───┼───┼───┼───┼───┴───┴───┴───┘
                 ▼   ▼   ▼   ▼   ▼
                ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
                └─┘ └─┘ └─┘ └─┘ └─┘

这个函数的计算并不复杂,每一层都是简单的矩阵计算,但并行程度很高,所以需要用GPU加速。复杂度在于每一层都有大量的参数,这些参数不是开发者写死的,而是通过训练确定的,每次对参数进行微调,然后根据效果是变得更好还是更坏决定微调方向。我们这个简单的神经网络模型参数仅几万个,训练的目的实际上就是为了把这几万个参数确定下来,目标是使得识别率最高。训练这几万个参数就花了几分钟时间,如果是几亿个甚至几百亿个参数,可想而知训练所需的时间和算力都需要百万倍的增长,所以,AI模型的代码并不复杂,模型规模大但本身结构并不复杂,但为了确定模型中每一层的成千上万个参数,时间和算力主要消耗在训练上。

比较一下传统程序和AI程序的差异:

传统程序AI程序
代码量
输入参数
输出结果精确输出不确定性输出
代码参数由开发设定由训练决定
执行层次可达数百万行仅若干层网络
执行路径能精确跟踪无法跟踪
并行串行或少量并行大规模并行
计算以CPU为主以GPU为主
开发时间主要消耗在编写代码主要消耗在训练
数据主要存储用户产生的数据需要预备大量训练数据
程序质量取决于设计架构、代码优化等取决于神经网络模型和训练数据质量

传统程序的特点是精确性:精确的输入可以实现精确地执行路径,最终获得精确的结果。而AI程序则是一种概率输出,由于模型的参数是训练生成的,因此,就连开发者自己也无法知道训练后的某个参数比如0.123究竟是什么意义,调大或者调小对输出有什么影响。传统程序的逻辑是白盒,AI程序的逻辑就是黑盒,只能通过调整神经网络的规模、层次、训练集和训练方式来评估输出结果,无法事先给出一个准确的预估。

源码下载

本文源码可通过GitHub下载

贝叶斯定理(原理篇)

转自https://liaoxuefeng.com/blogs/all/2023-08-27-bayes-explain/index.html
可以不用看文章,直接看油管上视频Bayes’ Theorem 贝叶斯定理

托马斯·贝叶斯(Thomas Bayes)是18世纪的英国数学家,也是一位虔诚的牧师。据说他为了反驳对上帝的质疑而推导出贝叶斯定理。贝叶斯定理是一个由结果倒推原因的概率算法,在贝叶斯提出这个条件概率公式后,很长一段时间,大家并没有觉得它有什么作用,并一直受到主流统计学派的排斥。直到计算机诞生后,人们发现,贝叶斯定理可以广泛应用在数据分析、模式识别、统计决策,以及最火的人工智能中,结果,贝叶斯定理是如此有用,以至于不仅应用在计算机上,还广泛应用在经济学、心理学、博弈论等各种领域,可以说,掌握并应用贝叶斯定理,是每个人必备的技能。

这里推荐两个视频,深入浅出地解释了贝叶斯定理:

Bayes’ Theorem 贝叶斯定理

Bayes theorem, the geometry of changing beliefs

如果你不想花太多时间看视频,可以继续阅读,我把视频内容编译成文字,以便快速学习贝叶斯定理。

为了搞明白贝叶斯定理究竟要解决什么问题,我们先看一个现实生活的例子:

已知有一种疾病,发病率是0.1%。针对这种疾病的测试非常准确:

  • 如果有病,则准确率是99%(即有1%未检出阳性);
  • 如果没有病,则误报率是2%(即有2%误报为阳性)。

现在,如果一个人测试显示阳性,请问他患病的概率是多少?

如果我们从大街上随便找一个人,那么他患病的概率就是0.1%,因为这个概率是基于历史统计数据的先验概率。

现在,他做了一次测试,结果为阳性,我们要计算他患病的概率,就是计算条件概率,即:在测试为阳性这一条件下,患病的概率是多少。

从直觉上这个人患病的概率大于0.1%,但也肯定小于99%。究竟是多少,怎么计算,我们先放一放。

为了理解条件概率,我们换一个更简单的例子:掷两次骰子,一共可能出现的结果有6×6=36种:

sample space

这就是所谓的样本空间,每个样本的概率均为1/36,这个很好理解。

如果我们定义事件A为:至少有一个骰子是2,那么事件A的样本空间如下图红色部分所示:

Event A

事件A一共有11种情况,我们计算事件A的概率P(A):

P(A)

我们再定义事件B:两个骰子之和为7,那么事件B的样本空间如下图绿色部分所示:

Event B

事件B一共有6种情况,我们计算事件B的概率P(B):

P(B)

接下来我们用P(A∩B)表示A和B同时发生的概率,A∩B就是A和B的交集,如下图蓝色部分所示:

P(A∩B)

显然A∩B只有两种情况,因此,计算P(A∩B):

P(A∩B)

接下来我们就可以讨论条件概率了。我们用P(A|B)表示在B发生的条件下,A发生的概率。由于B已经发生,所以,样本空间就是B的样本数量6,而要发生A则只能是A、B同时发生,即A∩B,有两种情况。

因此,计算P(A|B)如下:

P(A|B)

同理,我们用P(B|A)表示在A发生的条件下,B发生的概率。此时,分子仍然是A∩B的样本数量,但分母变成A的样本数量:

P(B|A)

可见,条件概率P(A|B)和P(B|A)是不同的。

我们再回到A、B同时发生的概率,观察P(A∩B)可以改写为:

P(B|A)xP(A)

同理,P(A∩B)还可以改写为:

P(A|B)xP(B)

因此,根据上述两个等式,我们推导出下面的等式:

P(AB)=P(ABP(B)=P(BAP(A)

把左边的P(A∩B)去掉,我们得到等式:

P(ABP(B)=P(BAP(A)

最后,整理一下等式,我们推导出贝叶斯定理如下:

P(AB)=P(B)P(BAP(A)​

这就是著名的贝叶斯定理,它表示,当出现B时,如何计算A的概率。

很多时候,我们把A改写为H,把B改写为E

P(HE)=P(E)P(EHP(H)​

H表示Hypothesis(假设),E表示Evidence(证据),贝叶斯定理的意义就在于,给定一个先验概率P(H),在出现了证据E的情况下,计算后验概率P(H|E)。

计算

有了贝叶斯定理,我们就可以回到开头的问题:

已知有一种疾病,发病率是0.1%。针对这种疾病的测试非常准确:

  • 如果有病,则准确率是99%(即有1%未检出阳性);
  • 如果没有病,则误报率是2%(即有2%误报为阳性)。

现在,如果一个人测试显示阳性,请问他患病的概率是多少?

用H表示患病,E表示测试为阳性,那么,我们要计算在测试为阳性的条件下,一个人患病的概率,就是计算P(H|E)。根据贝叶斯定理,计算如下:

P(HE)=P(E)P(EHP(H)​

P(H)表示患病的概率,根据发病率可知,P(H)=0.1%;

P(E|H)表示在患病的情况下,测试为阳性的概率,根据“如果有病,则准确率是99%”可知,P(E|H)=99%;

P(E)表示测试为阳性的概率。这个概率就稍微复杂点,因为它是指对所有人(包含病人和健康人)进行测试,结果阳性的概率。

我们可以把检测人数放大,例如放大到10万人,对10万人进行检测,根据发病率可知:

  • 有100人是病人,另外99900是健康人;
  • 对100个病人进行测试,有99人显示阳性,另有1人未检出(阴性);
  • 对99900个健康人进行测试,有2%=1998人显示阳性(误报),另有98%=97902人为阴性。

下图显示了检测为阳性的结果的分布:

           ┌───────┐
           │100000 │
           └───────┘
               │
       ┌───────┴───────┐
       ▼               ▼
   ┌───────┐       ┌───────┐
   │  100  │       │ 99900 │
   └───────┘       └───────┘
       │               │
   ┌───┴───┐       ┌───┴───┐
   ▼       ▼       ▼       ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ 99  │ │  1  │ │1998 │ │97902│
└─────┘ └─────┘ └─────┘ └─────┘
   │               │
   ▼               ▼
   +               +

所以,对于10万人的样本空间来说,事件E=显示阳性的概率为(99+1998)/100000=2.097%。

带入贝叶斯定理,计算P(H|E):

P(HE)=P(E)P(EHP(H)​=2.097%99%×0.1%​=0.020970.99×0.001​=0.04721=4.721%

计算结果为患病的概率为4.721%,这个概率远小于99%,且与大多数人的直觉不同,原因在于庞大的健康人群导致的误报数量远多于病人,当出现“检测阳性”的证据时,患病的概率从先验概率0.1%提升到4.721%,还远不足以确诊。

贝叶斯定理的另一种表示

在上述计算中,我们发现计算P(E)是比较困难的,很多时候,甚至无法知道P(E)。此时,我们需要贝叶斯定理的另一种表示形式。

我们用P(H)表示H发生的概率,用H表示H不发生,P(H)表示H不发生的概率。显然P(H)=1-P(H)。

下图红色部分表示H,红色部分以外则表示H:

P(H)

事件E用绿色表示:

P(E)

可见,P(E)可以分为两部分,一部分是E和H的交集,另一部分是E和H的交集:

P(E)=P(EH)+P(EH)

根据上文的公式P(A∩B)=P(A|B)xP(B),代入可得:

P(E)=P(EH)+P(EH)=P(EHP(H)+P(EHP(H)

把P(E)替换掉,我们得到贝叶斯定理的另一种写法:

P(HE)=P(EHP(H)+P(EHP(H)P(EHP(H)​

用这个公式来计算,我们就不必计算P(E)了。再次回到开头的问题:

已知有一种疾病,发病率是0.1%。针对这种疾病的测试非常准确:

  • 如果有病,则准确率是99%(即有1%未检出阳性);
  • 如果没有病,则误报率是2%(即有2%误报为阳性)。

现在,如果一个人测试显示阳性,请问他患病的概率是多少?

  • P(E|H)表示患病时检测阳性的概率=99%;
  • P(H)表示患病的概率=0.1%;
  • P(E|H)表示没有患病但检测阳性的概率=2%;
  • P(H)表示没有患病的概率=1-P(H)=99.9%。

代入公式,计算:

P(HE)=P(EHP(H)+P(EHP(H)P(EHP(H)​=99%×0.1%+2%×99.9%99%×0.1%​=0.04721=4.721%

检测为阳性这一证据使得患病的概率从0.1%提升到4.721%。假设这个人又做了一次检测,结果仍然是阳性,那么他患病的概率是多少?

我们仍然使用贝叶斯定理计算,只不过现在先验概率P(H)不再是0.1%,而是4.721%,P(E|H)和P(E|H)仍保持不变,计算新的P(H|E):

P(HE)=P(EHP(H)+P(EHP(H)P(EHP(H)​=99%×4.721%+2%×(1−4.721%)99%×4.721%​=0.71=71%

结果为71%,两次检测为阳性的结果使得先验概率从0.1%提升到4.721%再提升到71%,继续第三次检测如果为阳性则概率将提升至99.18%。

可见,贝叶斯定理的核心思想就是不断根据新的证据,将先验概率调整为后验概率,使之更接近客观事实。

关于信念

我们再回顾一下贝叶斯定理:

P(HE)=P(E)P(EHP(H)​

稍微改一下,变为:

P(HE)=P(HP(E)P(EH)​

P(H)是先验概率,P(H|E)是后验概率,P(E|H)/P(E)被称为调整因子,先验概率乘以调整因子就得到后验概率。

我们发现,如果P(H)=0,则P(H|E)=0;如果P(H)=1,则P(E|H)=P(E),P(H|E)=1。

也就是说,如果先验概率为0%或100%,那么,无论出现任何证据E,都无法改变后验概率P(H|E)。这对我们看待世界的认知有重大指导意义,因为贝叶斯概率的本质是信念,通过一次次事件,我们可能加强某种信念,也可能减弱某种信念,但如果信念保持100%或0%,则可以做到对外界输入完全“免疫”。

举个例子,十年前许多人都认为比特币是庞氏骗局,如果100%坚定地持有这种信念,那么他将无视用户越来越多、价格上涨、交易量扩大、机构入市等诸多证据,至今仍然会坚信比特币是骗局而错过无数次机会。(注:此处示例不构成任何投资建议)

对于新生事物,每个人都可以有非常主观的先验概率,但只要我们不把先验概率定死为0或100%,就有机会改变自己的信念,从而更有可能接近客观事实,这也是贝叶斯定理的精髓:

你相信什么并不重要,重要的是你别完全相信它。