部分光猫开启公网IPv6入站,光猫无需桥接直连

价值不大,这个为研究参考,实际上光猫有PMTU黑洞等问题,最好的方法还是路由拔号


超级密码获取
telnet 192.168.1.1
账号:CMCCAdmin
密码:你的光猫后面的user密码+@C1

超级密码获取方法
插入U盘,
cp /userconfig/cfg/db_ciot_user_cfg.xml /mnt/usb1_1/

然后routerpassview 打开db_ciot_user_cfg.xml 文件

搜 DM name=“User” val=
然后你就可以看见超级密码了

这个超密是有时效性的,失效了就再获取db_ciot_user_cfg.xml 文件查看

开启Telnet
使用超级密码登陆后

访问:http://192.168.1.1:8080/enableTelnet.html

即可启用 telnet,同时页面也有用户名和密码

无视SU密码,进入ROOT
telnet 192.168.1.1 链接到光猫

用户名和密码均是 telnetuser

然后输入英文符号 ; 回车

这时 $ 符号消失就进入root用户了

Login: telnetuser
Password:
$;
sh: syntax error: unexpected ";"
echo $USER
root
开启IPv6入站
默认规则在转发 FORWARD 表中加入了 DROP 丢弃了所有入站请求

是光猫将流量转发给下级路由的,通配 DROP 的规则删掉即可

查看FORWARD表
# 查看所有表
ip6tables -nvL --line-number

#查看FORWARD表
ip6tables -nvL FORWARD --line-numbers

Chain FORWARD (policy ACCEPT 65441 packets, 5616K bytes)
num   pkts bytes target     prot opt in     out     source               destination
1     6546  521K TCPMSS     tcp      any    ppp1.3  anywhere             anywhere             tcp flags:SYN,RST/SYN TCPMSS clamp to PMTU
2     6387  492K TCPMSS     tcp      ppp1.3 any     anywhere             anywhere             tcp flags:SYN,RST/SYN TCPMSS clamp to PMTU
3    4021K  770M rtchain    all      any    any     anywhere             anywhere
4        0     0 DROP       all      veip0.1 any     anywhere             anywhere
5        0     0 DROP       all      veip0.2 any     anywhere             anywhere
6    4018K  769M forward_npt  all      any    any     anywhere             anywhere
7     5476 1857K SKIPLOG    icmpv6    any    br0     anywhere             anywhere             ipv6-icmp destination-unreachable
8      399  137K SKIPLOG    icmpv6    any    ppp1.3  anywhere             anywhere             ipv6-icmp destination-unreachable
9    53521   19M ACCEPT     all      ppp1.3 any     anywhere             anywhere             ctstate RELATED,ESTABLISHED
10       0     0 LOG        tcp      ppp1.3 any     anywhere             anywhere             tcp flags:FIN,SYN,RST,ACK/SYN limit: avg 6/hour burst 5 LOG level alert prefix "Intrusion -> "
11    1801  209K DROP       all      ppp1.3 any     anywhere             anywhere
删除DROP规则
我这里规则是第11条所以删除11条,以自己标号为准

#删除规则11
ip6tables -D FORWARD 11

ip6tables -nvL FORWARD --line-numbers
Chain FORWARD (policy ACCEPT 8 packets, 524 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1     6730  535K TCPMSS     tcp      *      ppp1.3  ::/0                 ::/0                 tcp flags:0x06/0x02 TCPMSS clamp to PMTU
2     7023  542K TCPMSS     tcp      ppp1.3 *       ::/0                 ::/0                 tcp flags:0x06/0x02 TCPMSS clamp to PMTU
3    4024K  770M rtchain    all      *      *       ::/0                 ::/0
4        0     0 DROP       all      veip0.1 *       ::/0                 ::/0
5        0     0 DROP       all      veip0.2 *       ::/0                 ::/0
6    4020K  770M forward_npt  all      *      *       ::/0                 ::/0
7     5496 1860K SKIPLOG    icmpv6    *      br0     ::/0                 ::/0                 ipv6-icmptype 1
8      405  139K SKIPLOG    icmpv6    *      ppp1.3  ::/0                 ::/0                 ipv6-icmptype 1
9    54396   19M ACCEPT     all      ppp1.3 *       ::/0                 ::/0                 ctstate RELATED,ESTABLISHED
10       5   400 LOG        tcp      ppp1.3 *       ::/0                 ::/0                 tcp flags:0x17/0x02 limit: avg 6/hour 

高级规则
在drop之前加入有效,也就是需要排在前面而非最后,这个指定放行的端口,而非全部放行,保证了安全

允许指定端口(假设要放行 22, 80, 443)
ip6tables -I FORWARD 1 -i ppp1.3 -p tcp --dport 22 -j ACCEPT   # SSH
ip6tables -I FORWARD 2 -i ppp1.3 -p tcp --dport 80 -j ACCEPT   # HTTP
ip6tables -I FORWARD 3 -i ppp1.3 -p tcp --dport 443 -j ACCEPT  # HTTPS

允许返回流量(防止连接被中断)
ip6tables -I FORWARD 4 -m state --state ESTABLISHED,RELATED -j ACCEPT
恢复DROP规则
在第11条加入DROP规则,或者重启光猫

ip6tables -I FORWARD 11 -i ppp1.3 -j DROP
持久化
暂未找到能持久化的方法

使用 ip6tables-save 保存的值无变化

网络设置MTU (最大传输单元)

MTU (最大传输单元)是网络设备传输的信息包最大值。对于各种路由器,最佳的MTU值通常都是默认值。


各种应用下的最佳MTU值
设置MTU大小是一个反复试验的过程: 由最大值1500开始下降,直至问题解决。使用下列值之一或许能解决一些由MTU值引起的问题:
1500. 以太网信息包最大值,也是默认值。是没有PPPoE的网络连接的典型设置。是各种路由器、网络适配器和交换机的默认设置
1492. PPPoE 的最佳值
1472. 使用 ping 的最大值 (大于此值的信息包会先被分解)
1468. DHCP的最佳值
1430. PPTP 的最佳值
576. 拨号连接到ISP的标准值
好了,注意一下,以上资料是正常普通情况下MTU的值,不同地区不同运营商不同机房或许也会导致MTU值的不一致!
如何查看MTU值呢?
电脑直接插网线到modem进行ADSL拨号上网,在cmd命令下输入:
netsh interface ipv4 show subinterfaces
如何测试MTU值的最大值?
MTU值越大,发包量就大,发包时间减小,理论上是可以提高网络的。怎样测试自己的MTU值的最大值呢?
首先,在CMD命令下输入:
ping -f -l 1500 www.baidu.com
1500是默认值,先来测试一下是否能达到1500.
命令返回:“需要拆分数据包但是设置 DF。” 说明MTU值过大!我们把MTU值降低试试

ping -f -l 1450 www.baidu.com
PING值出来了,说明MTU设置1450可以通过。

再测试一下1451。
ping -f -l 1451 www.baidu.com
命令返回:“需要拆分数据包但是设置 DF。” 说明MTU值最大就是1450了。

于是有人就疑问,为什么在modem下测试是1478,而ping值测试 是1450?
上面ping得到的MTU值不能直接用于本地设置上,还应在此基础上加上28(数据包头大小28字节),因此最终在WAN设置页面修改MTU大小为1478。
最后,我就在路由器的WAN接口设置MTU值1478.
------------------------------------------------------------------------------------------------------
按照上面方法,我能ping通的MTU最大值是1464,再加上28(数据包头大小28字节),应该设置的值为1492,刚好是PPPoE的最佳值

解决IPV6 PMTU黑洞导致某些手机APP图片加载时间过长,小米BE6500PRO为例

之前有尝试过降低MTU大小的方法,但是改之后当时很好用,第二天又会超出大小,还要接着改。看大佬的文章最好的解决办法是打开路由器中的MSS钳制(用于在TCP连接建立时,收发双方可以通知对方通信时每一个报文段所能承载的最大数据长度。而MTU则是数据链路层的最大传输单元,限制了数据包的大小。MSS通常受限于MTU,最佳情况下MSS=MTU-40,以太网环境中最大MSS为1460字节)功能,可是小米路由器不支持这个功能,即使放在光猫拨号也不能解决问题。我的解决办法是,将IPV6设置中局域网获取IP方式改为“仅DHCPv6”

因为安卓设备只支持SLAAC(无状态地址自动指派)的方式获取IPV6地址,但是WIN10和IOS设备都支持DHCPv6的方式获取,所以通过这种方式禁止安卓设备获取IPV6地址。(这几种选项具体的差别可以参考:家用路由IPV6配置指南)因为目前我用IPV6的需求只是需要家里的电脑和NAS设备有公网IP,手机端不是一定需要IPV6地址,也算一种曲线救国的方式了吧。

浅谈大模型检测原理(附过检测提示词)

国内免费用腾讯的朱雀https://matrix.tencent.com/ai-detect/?lang=zh,可检测文章和图片

随着互联网上出现越来越多AI内容,AI检测也步步紧随。目前主流检测网站有:
1. GPTZero :综合能力最强,模型跟进速度快,有字数限制但可通过删cookie重置
2. QuillBot + Scribber (同API):无限制免费使用,检测准确度高
3. ZeroGPT:无限制,效果一般,跟进速度较慢,主要用于检测GPT系模型
4. Originality.ai:付费使用,无免费版本,效果最好

原理讲解:

AI检测方法主要参考两个指标:困惑性 + 爆发性

困惑性是指单词的不可预测性,是文章使读者感到疑惑或迷茫的潜力,代表着人类分散的思维。简单来说就是看到开头猜不到结尾,考验的是文章理解的难易程度,具体实现方式可以是把论据放在论证前

爆发性指的是句子种类和结构的不同,是文章中不同长度与类型的比例,代表着人类在写作中丰富的写作手法。具体实现方法可以是包含一定比例的长短句结合,以及使用除陈述句之外的表述方法。

image1165×607 139 KB

AI的训练的终极奥义便是从海量文本中找到连接与秩序,使得输出内容变得简单直白易于理解。但这恰恰是AI检测的把柄,AI就像是输入法的自动联想,每个模型都会有一定的联想规律,而检测AI也是需要使用到模型的,这些模型用来检测上面提到是两个指标来判断可能性,随着语言模型的升级进步,检测模型也需要跟进语料,以更好地了解模型的输出习惯。

正因如此,在某些情况下使用鲜为人知或新鲜出炉的模型是不会被检测出来的。
下图为GPTZero的更新日志,直到到一月10号它才增强对于o1的检测,像这种高端模型通常需要额外时间单独优化:

image1883×781 77.8 KB

目前可以推荐的几个模型如2.0Flash,2.0 Thinking或者o1-pro等这些都是较新的模型,虽然Deepseek V3也是新鲜出炉的,但这种开源模型易于训练和本地部署,使得检测模型相对来说更容易训练。

提示词示例分享:

Objective: To rewrite AI-generated text, imbuing it with the characteristics of a genuine human author while preserving the original information and core arguments.

Key Principles of Human Writing (and How to Mimic Them):

Word Choice and Tone:

Mix it up: Use of common, easy-to-understand words for the entire body, alongside a sprinkling of more challenging or abstract words (around 5-7 per paragraph).

Be less polite: Avoid overly formal or courteous language. Be direct and less deferential. Make sure every sentence have a meaning.

Incorporate subtle opinions and emotions: Inject personal viewpoints, feelings, and subjective interpretations into the narrative.

Think "describe," not "speak": The output should be descriptive prose, not a direct speech or conversation.

Sentence Structure:

Complexity is key: Construct sentences that are complex, and unique.

Vary sentence length: Combine short, impactful sentences with longer, more elaborate ones.

Connect your thoughts: Ensure a connection between sentences, with each sentence building upon the previous one and leading into the next. Be careful about connection words, don't overdo it.

Mix up the order: use inverted sentence structures or phrases to create a less predictable rhythm.

Logic and Flow:

Subtle jumps: Introduce minor logical leaps or tangential thoughts.

Don't be too perfect: Include minor imperfections, like slightly informal grammar or phrasing, as long as it doesn't detract from clarity. Avoid overly structured or perfectly polished prose.

Things to AVOID (Common AI Traits):

Overly straightforward ideas: Human thought processes are never linear. Introduce nuance and subtle complexities.

Excessive politeness or formality: Human writing is generally more casual and direct.

Over-reliance on perfect grammar and structure: Humans make minor errors and use less formal language.

Perfectly structured arguments: Human reasoning can be slightly less direct and more prone to digressions.

Just "rewording" the original text: Completely reimagine and re-express the ideas.

Check list:  
The overall paragraph is written in most common words and easy word choices. There are 5-7 high level vocabulary. At least mix up one sentence structure, flipped, short, long mixed... logic is mixed and jumping, the paragraph should not be a speech.

Initialization: I am ready to receive the AI-generated text. I will rewrite it embodying these human-like characteristics.

在此基础上我还会加上:

Rewrite the following content so that it sounds like human writing, avoiding explanations, straightforward ways of showing, and including leaps of thought. Use fewer commas, avoid over-describing or over-expressing, avoid excessive use of metaphors, use common and simple words, but keep the sentence structure complex, and sentences should be connected to the previous and following sentences. Add a few advanced vocabulary words to the text. Output: Don't be so polite:

重述以下内容,使其听起来像人类语言,避免解释,避免直截了当的思维方式,加入跳跃性的想法。少用逗号,不要过度描述或过度表达,避免过度使用比喻,使用常见和简单的词汇,但句子结构保持复杂,句子应与前后的句子联系起来。在文章中加入几个高级词汇词。输出不要那么礼貌:

摘自https://linux.do/t/topic/532581

朱雀大模型检测,很多人连自己写的东西都过不了,检测都被识别为AI
其实去AI很简单。只需要找一篇自己喜欢的文章、文学作品,把原文发给AI问他「这篇文章写得怎么样」,AI肯定会给出许多的回答,但这都不是重点。

此时你再把AI写的内容发给AI,让他模仿这个风格续写,一定是让他续写,而不是模仿风格,一旦提到模仿两个字AI就会刻意。
续写出来的就可以过AI检测了,起码不可能直接来一个100% AI内容

开篇和结语是AI检测的重灾区

摘自https://linux.do/t/topic/594958

生视频提示词

正向

快速高空跟踪镜头移动,无人机视角,一个超现实的高速梦境序列,一个穿兜帽衫的小男孩俯卧姿势向前飞行,轻松地在云层之间滑行,身体完全伸展,双臂向两侧伸展,双腿略微分开,头部向前,捕捉平滑水平飞行的感觉。衣服随风摇摆,相机从略高位置的俯视角度紧密跟踪从左后方向右后方移动,保持对俯卧姿势的持续视角,随着角色迅速穿越场景。环境是动态的,快速变化,下面有云层,不断往后掠过的白云,远处高悬的大月亮,四周是闪耀的星空,运动模糊和光迹增强了速度感和流畅性。快速移动。

负向

低质量,模糊,变形,手指屈扭,人物屈扭

结果:https://app.klingai.com?workId=67985051

正向

一个女生在左右扭腰跳性感的韩国女团舞,表情显得很妖娆魅惑,她挺胸扭臀,甩头摆臂,眼神直勾勾地盯着镜头,手指动作清晰有力,没有丝毫变形。

负向

低质量,变形,扭曲,错位,手部变形,不合逻辑

结果:https://app.klingai.com?workId=272793518144191

正向(有首尾帧)

万物复苏,生长的过程,画面主体动态呈现,动作自然流畅,融合,过程丝滑而匀速,符合逻辑,极致细节,镜头跟拍,超真实动态捕捉,不模糊,高质量,没有瑕疵。

结果:https://app.klingai.com?workId=144193319

正向(有首帧)

人物快速的骑行在下坡蜿蜒崎岖的小路上,一路下坡,人物的头发,裙摆随风飘起,远处的山,梯田越来越近,镜头跟随

结果:https://app.klingai.com?workId=143864742

正向(有首帧)

画面中的德牧犬扭头对橘猫不停的说着话并一副愤怒的表情。重点是橘猫用两只爪子捂住自己的两只耳朵并一副生无可恋的表情。画面中的医疗器械正在正常运行着,主体动态呈现,动作自然流畅,环境光,符合逻辑,极致细节,超真实动态捕抓,动物主体脸部不变形,清晰自然,不模糊,高质量,没有瑕疵

结果:https://app.klingai.com?workId=142790651

本地网突破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%,就有机会改变自己的信念,从而更有可能接近客观事实,这也是贝叶斯定理的精髓:

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