2023年4月

阅读时长约 12 分钟,共计 2764 个字。

说到“内卷”,这个近几年很流行的词,大家都很熟悉了吧?

如果追根溯源,内卷其实也是一个舶来词,它的英文是“Involution”,即“内卷化”。如果用生动一些的图形来描述,内卷其实就是围绕圆心,由外向内旋转。

与之相反的则是“Evolution”,意思是“外卷”。依然以图形来描述,外卷也是围绕圆心,由内向外旋转。

看到这样的描述,你会联想到什么呢?

没错,是宇宙天文学。

无论外卷还是内卷,它们原本都是数学和天文学的概念。只是后来,这种规律也适合描述人类社会,所以才产生了我们今天所熟知的“行业内卷”之类的词。而且早在上世纪三十年代,西方美国的一些社会学家已经把“内卷化”用于描述社会学和历史学了。如果大家有兴趣的话,可以去图书馆翻看类似的书籍,在某些阐述农业方面的著作中都会发现“内卷化”这一词汇。所以从这一点上看,内卷并不是啥新鲜玩意儿,它在人类社会中,已经存在了大几十年了。

那么,我们究竟该如何定义内卷呢?我最近找到了《社会内卷化的根源》一书,其中将内卷的表现形式归纳为七个方面。我回想了一下自己的工作经历,分析了一下目前行业的现状,我觉得这七个方面概括得非常到位。我把它们罗列如下:

  1. 无意义的精益求精是内卷;
  2. 将简单问题复杂化是内卷;
  3. 为了免责,被动的应付工作也是内卷;
  4. 与预期的目标严重偏离的工作也是内卷;
  5. 低水平的模仿和复制是典型的内卷;
  6. 限制创造力的内部竞争是制度性的内卷;
  7. 在同一个问题上无休止的挖掘研究是内卷。

我们不妨一起复盘一下各自的工作经历,是不是在某些时候,不自觉地就开始卷起来了呢?

当然,这里面有一个重要的关键点,就是前提。

比如第一个,精益求精本来是好事儿,但如果出于无意义的目的,那好事儿也就不成为好事儿了。就好比前几年流行的那个段子:“我要个五彩斑斓的黑”,这不是神经病吗……

再比如第二个,用适当的方法解决问题,只要能解决问题,而且是合情理的方法,那就没必要再增加复杂度。就好比软件架构,没必要只为了用某种设计而使用它。我就写个Hello World,就不用整太复杂的架构了吧……

后面的我就不一一举例了,还是那句话,大家可以反思自己的工作,看看自己有没有在做内卷化的事情。

看过这七个方面之后,如果要用一句话来概括描述内卷,会是怎样的呢?

我的答案是:如果做某件事情,不会有人受益,那就要当心了,很可能正在处于内卷漩涡之中。

回顾这七个方面,你能找到受益的一方吗?没有的。陷入内卷漩涡,几乎不会有人受益。甚至连某些恶意竞争手段都不如,比如价格战,至少消费者还能多少获得点益处。内卷漩涡之中,所有人都是受害者。

“外卷”的英语原文:“Evolution”,它还有另外的含义,就是“进化”、“革命”。也就是说,当内卷到一定程度的时候,就会发生革命性的进化,这是社会发展的规律,是不受某个人或是某些人的意志而转移的。

就拿我国的某段历史来举例吧,早在二十世纪三十年代,在我国的农村就发生了土地恶性竞争的情况。地主以低价雇佣农民作为劳动力,而这些农民迫于生存,只能选择接受。地主和农民就陷入了内卷的漩涡,在这个循环里打转。有人说地主得到了好处,因为钱花得少了。其实没那么简单,虽然看上去雇佣成本低了,但是低价来的农民,怎么可能心甘情愿地付出劳动,肯定会想办法偷懒摸鱼,所以地主其实也没获得多少好处。

你看,这像不像现在的职场形势?像不像那些鸡娃式的父母?

后来发生了什么呢?

当内卷化发展到极限,就会转向外卷。还是二十世纪三十年代,很多农民受不了在内卷的环境里打转,于是选择参加红军。当时的国民党失去政权,时间上就是在三十年代农村内卷化的时候开始的。刚才我也提到了,内卷化的反面就是外卷,是进化,是革命。

早在1902年,梁启超在《释革》一文中提到:“Revolution者,若转轮然,从根柢处掀翻之,而别造一新世界,如法国一千七百八十九年之Revolution是也,日本人译之曰革命”。

你看,这个世界发展到今天,哪还有什么新鲜事儿。把历史翻开,总会找到些许影子。

话说回来,对于我等软件开发者,如何破解内卷漩涡之谜团呢?

我个人的建议,就是要认清形势。现在,ChatGPT在国内正火,很多人蜂拥而至。但是,清醒的人不会选择它,你知道为什么吗?

因为ChatGPT有硬伤。虽然它看上去很美好,但它消耗的能量巨大。昨天我看吴军老师的直播,他提到ChatGPT每训练一次,相当于3000台特斯拉电动车,每台开20万英里(折算为公里是32.19万左右)的能量消耗。可谓是十分惊人。我查了一下,果真如此。而且它不仅要消耗电量,还要消耗水,因为要水冷才能保证芯片不过热。而且这个水还必须是净化后的淡水,ChatGPT每与用户交流25-50个问题,就要消耗0.5L的水。上一代ChatGPT——GPT-3,它的训练消耗了近70万升水,这些水足够生产370辆宝马汽车。

有人说,算法会优化,会让消耗变少。这话一半对,另一半不好说。算法确实会提升效能,但是问题的复杂度也在上升,所以最后的能量消耗会上升还是下降,还真不一定。

所以这注定了ChatGPT没办法持久,除非迎来革命性的变化。作为软件开发者,如果看到的都是ChatGPT的优点,忽视这些缺点,一窝蜂地涌入,很可能不会有什么结果,只会相互竞争,陷入内卷。那怎么应对呢?

还是回看历史,美国有一段时间陷入淘金热,很多人争先恐后去淘金,结果真正发财的没几个。反倒是卖牛仔裤和水的,赚了很多。说到这,大家明白了吗?

既然ChatGPT消耗能源,那提供算力的云计算服务商肯定会在这个过程中赚到钱,因为需求巨大。所以为什么苹果公司没有跟着做,在ChatGPT大热的时候却默默无闻,因为它看到了这一点。苹果公司本身是一家伟大的企业,它的每一次决定,虽然在别人看来可能是很蠢的,但是很可能是那些人没把问题看“透”。其实不只是苹果公司,每一家伟大的企业做的每一次决定,都有其原因,看不透的人,往往需要先在自身上找问题。很可能是自己的格局和视野出了问题。

所以,你知道该怎么做了吗?

其实,很多时候我们陷入内卷,都是因为自己身在局中,无法以局外人的视角看到全局。有个成语叫做“旁观者清”,其实就是这样。

我记得自己曾经在写代码的时候遇到了问题,折腾了大半天也不行。想请教旁边的人,结果问题还没说完,就知道该怎么解了。自己应对问题的时候,面对的是问题,身在局中。向别人请教的时候,面对的是自己和问题,身在局外。换一种视角和思维方式,问题可能就解开了。

所以,如果你正深陷内卷漩涡中,不妨跳出来,探索其它的可能。卷下去,没有人获益。只要拥有了全局视角,我们就可以看到更多的可能性。

当然,光看到这些可能性还不够,还需要具备去尝试的能力,也就是自身的核心竞争力。关于如何打造自己的能力图谱,构建强健的核心竞争力,我日后再做分享。

总结一下,内卷(involution)起源于数学和天文学,后来用作描述社会现象。如果做某件事情,不会有人受益,很可能正在处于内卷漩涡之中。若要破解内卷,走出漩涡,可以请全局视角来帮忙。探索内卷之外的更多可能性,掀起一波进化的革命,方可打破内卷漩涡循环。

前言

本文介绍
vue3-element-admin
如何通过 Husky + Lint-staged + Commitlint + Commitizen + cz-git 来配置 Git 提交代码规范。

核心内容是配置 Husky 的
pre-commit

commit-msg
两个钩子:

pre-commit
:Husky + Lint-staged 整合实现 Git 提交前代码规范检测/格式化 (前提:
ESlint + Prettier + Stylelint 代码统一规范
);

commit-msg
: Husky + Commitlint + Commitizen + cz-git 整合实现生成规范化且高度自定义的 Git commit message。

Git 提交代码检测 Git 提交信息规范
image-20230417010758573 image-20230417010720749

Husky

Husky 是 Git 钩子工具,可以设置在 git 各个阶段(
pre-commit

commit-msg
等)触发。

官方网站:
https://typicode.github.io/husky

Husky 安装有
自动安装

手动安装
两种方式 。

在这里插入图片描述

官方推荐自动安装的方式,使用
husky-init
命令一次性完成依赖自动安装和配置

npx husky-init && npm install

自动生成的
.husky
目录和指令:

Lint-staged

lint-staged 是一个在 git add 到暂存区的文件运行 linters (ESLint/Prettier/StyleLint) 的工具,避免在 git commit 提交时在整个项目执行。

官方网站:
https://github.com/okonet/lint-staged

Lint-staged 安装

npm install -D lint-staged

Lint-staged 配置

检测/格式化配置

package.json 中添加不同文件在 git 提交执行的 lint 检测配置

"lint-staged": {
    "*.{js,ts}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{cjs,json}": [
      "prettier --write"
    ],
    "*.{vue,html}": [
      "eslint --fix",
      "prettier --write",
      "stylelint --fix"
    ],
    "*.{scss,css}": [
      "stylelint --fix",
      "prettier --write"
    ],
    "*.md": [
      "prettier --write"
    ]
  }

添加 lint-staged 指令

package.json 的 scripts 添加
lint-staged
指令

  "scripts": {
    "lint:lint-staged": "lint-staged"
  }

修改提交前钩子命令

根目录
.husky
目录下 pre-commit 文件中的
npm test
修改为
npm run lint:lint-staged

#npm test
npm run lint:lint-staged

Git 提交代码检测

Commitlint

Commitlint 检查您的提交消息是否符合 Conventional commit format。--
Commitlint 官网

Commitlint 安装

参考
官方安装文档

npm install -D @commitlint/cli @commitlint/config-conventional

Commitlint 配置

根目录创建
commitlint.config.cjs
配置文件,示例配置:
@commitlint/config-conventional

module.exports = {
  // 继承的规则
  extends: ["@commitlint/config-conventional"],
  // @see: https://commitlint.js.org/#/reference-rules
  rules: {
    "subject-case": [0], // subject大小写不做校验

    // 类型枚举,git提交type必须是以下类型
    "type-enum": [
      2,
      "always",
      [
    'feat', // 新增功能
        'fix', // 修复缺陷
        'docs', // 文档变更
        'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
        'refactor', // 代码重构(不包括 bug 修复、功能新增)
        'perf', // 性能优化
        'test', // 添加疏漏测试或已有测试改动
        'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
        'ci', // 修改 CI 配置、脚本
        'revert', // 回滚 commit
        'chore', // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
      ],
    ],
  },
};

添加提交信息校验钩子

执行下面命令生成
commint-msg
钩子用于 git 提交信息校验,命令来自:
@commitlint/README.md

npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"

生成的配置如下:

Commitlint 验证

正确的提交格式:
<type>(<scope>): <subject>
,type 和 subject 默认必填

不规范的 commit msg,提交失败 规范的 commit msg,提交成功

Commitizen & cz-git

  • commitizen
    : 基于Node.js的
    git commit
    命令行工具,辅助生成标准化规范化的 commit message。--
    官方文档
  • cz-git
    : 一款工程性更强,轻量级,高度自定义,标准输出格式的 commitizen 适配器。--
    官方文档

Commitizen & cz-git 安装

npm install -D commitizen cz-git

cz-git 配置

修改
package.json
指定使用的适配器

 "config": {
    "commitizen": {
      "path": "node_modules/cz-git"
    }
  }

cz-git 与
commitlint
进行联动给予校验信息

,所以可以编写于
commitlint
配置文件之中(
⇒ 配置模板
)。

// commitlint.config.cjs
module.exports = {
rule: {
...
},
prompt: {
messages: {
type: '选择你要提交的类型 :',
scope: '选择一个提交范围(可选):',
customScope: '请输入自定义的提交范围 :',
subject: '填写简短精炼的变更描述 :\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixesSelect: '选择关联issue前缀(可选):',
customFooterPrefix: '输入自定义issue前缀 :',
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
generatingByAI: '正在通过 AI 生成你的提交简短描述...',
generatedSelectByAI: '选择一个 AI 生成的简短描述:',
confirmCommit: '是否提交或修改commit ?',
},
// prettier-ignore
types: [
{ value: "feat", name: "特性: ✨ 新增功能", emoji: ":sparkles:" },
{ value: "fix", name: "修复:

安装Zookeeper和Kafka集群

本文介绍如何安装Zookeeper和Kafka集群。为了方便,介绍的是在一台服务器上的安装,实际应该安装在多台服务器上,但步骤是一样的。

安装Zookeeper集群

下载安装包

从官网上下载安装包:

curl https://dlcdn.apache.org/zookeeper/zookeeper-3.7.1/apache-zookeeper-3.7.1-bin.tar.gz -o apache-zookeeper-3.7.1-bin.tar.gz

解压:

tar xvf apache-zookeeper-3.7.1-bin.tar.gz 

配置

创建目录
zk1
,然后添加如下配置:

zk1/myid
:

1

zk1/zk.config
:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/Users/larry/IdeaProjects/pkslow-samples/other/install-kafka-cluster/src/main/zookeeper/zk1
clientPort=2181

server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

对于
zk2

zk3
也重复同样的步骤,并修改相应的配置:

zk2/myid
:

2

zk2/zk.config
:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/Users/larry/IdeaProjects/pkslow-samples/other/install-kafka-cluster/src/main/zookeeper/zk2
clientPort=2182

server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

zk3/myid
:

3

zk3/zk.config
:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/Users/larry/IdeaProjects/pkslow-samples/other/install-kafka-cluster/src/main/zookeeper/zk3
clientPort=2183

server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

启动集群

启动三个服务如下:

$ ./apache-zookeeper-3.7.1-bin/bin/zkServer.sh start ./zk1/zk.config 
ZooKeeper JMX enabled by default
Using config: ./zk1/zk.config
Starting zookeeper ... STARTED

$ ./apache-zookeeper-3.7.1-bin/bin/zkServer.sh start ./zk2/zk.config 
ZooKeeper JMX enabled by default
Using config: ./zk2/zk.config
Starting zookeeper ... STARTED

$ ./apache-zookeeper-3.7.1-bin/bin/zkServer.sh start ./zk3/zk.config 
ZooKeeper JMX enabled by default
Using config: ./zk3/zk.config
Starting zookeeper ... STARTED

查看状态

通过
status
命令查看:

$ ./apache-zookeeper-3.7.1-bin/bin/zkServer.sh status ./zk1/zk.config 
ZooKeeper JMX enabled by default
Using config: ./zk1/zk.config
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower


$ ./apache-zookeeper-3.7.1-bin/bin/zkServer.sh status ./zk2/zk.config 
ZooKeeper JMX enabled by default
Using config: ./zk2/zk.config
Client port found: 2182. Client address: localhost. Client SSL: false.
Mode: leader


$ ./apache-zookeeper-3.7.1-bin/bin/zkServer.sh status ./zk3/zk.config 
ZooKeeper JMX enabled by default
Using config: ./zk3/zk.config
Client port found: 2183. Client address: localhost. Client SSL: false.
Mode: follower

连接其中一个服务并添加数据:

$ ./apache-zookeeper-3.7.1-bin/bin/zkCli.sh -server localhost:2181

[zk: localhost:2181(CONNECTED) 0] create /pkslow
Created /pkslow
[zk: localhost:2181(CONNECTED) 1] create /pkslow/website www.pkslow.com
Created /pkslow/website

连接另外一个服务,并查看数据,发现与之前创建的是一样的:

$ ./apache-zookeeper-3.7.1-bin/bin/zkCli.sh -server localhost:2182

[zk: localhost:2182(CONNECTED) 1] get /pkslow/website
www.pkslow.com

目录结构如下:

安装Kafka集群

下载安装包

通过官网下载如下:

curl https://downloads.apache.org/kafka/3.4.0/kafka_2.13-3.4.0.tgz -o kafka_2.13-3.4.0.tgz

解压安装包:

tar -xzf kafka_2.13-3.4.0.tgz

配置

Broker 1
的配置如下:

broker.id=1
port=9091
listeners=PLAINTEXT://:9091
zookeeper.connect=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
log.dirs=/Users/larry/IdeaProjects/pkslow-samples/other/install-kafka-cluster/src/main/kafka/kafka1/kafka-logs

Broker 2
的配置如下:

broker.id=2
port=9092
listeners=PLAINTEXT://:9092
zookeeper.connect=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
log.dirs=/Users/larry/IdeaProjects/pkslow-samples/other/install-kafka-cluster/src/main/kafka/kafka2/kafka-logs

Broker 3
的配置如下:

broker.id=3
port=9093
listeners=PLAINTEXT://:9093
zookeeper.connect=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
log.dirs=/Users/larry/IdeaProjects/pkslow-samples/other/install-kafka-cluster/src/main/kafka/kafka3/kafka-logs

目录结构如下:

启动集群

启动
kafka
服务如下:

./kafka_2.13-3.4.0/bin/kafka-server-start.sh ./kafka1/server.properties
./kafka_2.13-3.4.0/bin/kafka-server-start.sh ./kafka2/server.properties
./kafka_2.13-3.4.0/bin/kafka-server-start.sh ./kafka3/server.properties

检查如测试

创建topic:

$ kafka_2.13-3.4.0/bin/kafka-topics.sh --create --topic pkslow-topic --bootstrap-server localhost:9091,localhost:9092,localhost:9093 --partitions 3 --replication-factor 3
Created topic pkslow-topic.

列出topic:

$ kafka_2.13-3.4.0/bin/kafka-topics.sh --list --bootstrap-server localhost:9091,localhost:9092,localhost:9093
pkslow-topic

查看topic:

$ kafka_2.13-3.4.0/bin/kafka-topics.sh --describe --topic pkslow-topic --bootstrap-server localhost:9091,localhost:9092,localhost:9093
Topic: pkslow-topic     TopicId: 7CLy7iZeRvm8rCrn8Dw_mA PartitionCount: 3       ReplicationFactor: 3    Configs: 
        Topic: pkslow-topic     Partition: 0    Leader: 3       Replicas: 3,1,2 Isr: 3,1,2
        Topic: pkslow-topic     Partition: 1    Leader: 1       Replicas: 1,2,3 Isr: 1,2,3
        Topic: pkslow-topic     Partition: 2    Leader: 2       Replicas: 2,3,1 Isr: 2,3,1

生产者发消息到brokers:

$ kafka_2.13-3.4.0/bin/kafka-console-producer.sh --broker-list localhost:9091,localhost:9092,localhost:9093 --topic pkslow-topic
>My name is Larry Deng.
>My website is www.pkslow.com.
>

消费者从brokers收消息:

$ kafka_2.13-3.4.0/bin/kafka-console-consumer.sh --bootstrap-server localhost:9091,localhost:9092,localhost:9093 --topic pkslow-topic --from-beginning
My name is Larry Deng.
My website is www.pkslow.com.

代码

配置可以参考
GitHub pkslow-samples

本文实现两个分类器: softmax分类器和感知机分类器

Softmax分类器

Softmax分类是一种常用的多类别分类算法,它可以将输入数据映射到一个概率分布上。Softmax分类首先将输入数据通过线性变换得到一个向量,然后将向量中的每个元素进行指数函数运算,最后将指数运算结果归一化得到一个概率分布。这个概率分布可以被解释为每个类别的概率估计。

定义

定义一个softmax分类器类:

class SoftmaxClassifier(nn.Module):
    def __init__(self,input_size,output_size):
        # 调用父类的__init__()方法进行初始化
        super(SoftmaxClassifier,self).__init__()
        # 定义一个nn.Linear对象,用于将输入特征映射到输出类别
        self.linear = nn.Linear(input_size,output_size)

    def forward(self,x):
        x = self.linear(x) # 传递给线性层
        return nn.functional.softmax(x,dim=1) # 得到概率分布

    def compute_accuracy(self,output,labels):
        preds = torch.argmax(output,dim=1) # 获取每个样本的预测标签
        correct = torch.sum(preds == labels).item() # 计算正确预测的数量
        accuracy = correct / len(labels) # 除以总样本数得到准确率
        return accuracy

如上定义三个方法:

  1. __init__(self)
    :构造函数,在类初始化时运行,调用父类的__init__()方法进行初始化
  2. forward(self)
    :模型前向计算过程
  3. compute_accuracy(self)
    :计算模型的预测准确率

训练

生成训练数据:

import numpy as np

# 生成随机样本(包含训练数据和测试数据)
def generate_rand_samples(dot_num=100):
    x_p = np.random.normal(3., 1, dot_num)
    y_p = np.random.normal(3., 1, dot_num)
    y = np.zeros(dot_num)
    C1 = np.array([x_p, y_p, y]).T
    x_n = np.random.normal(7., 1, dot_num)
    y_n = np.random.normal(7., 1, dot_num)
    y = np.ones(dot_num)
    C2 = np.array([x_n, y_n, y]).T
    x_n = np.random.normal(3., 1, dot_num)
    y_n = np.random.normal(7., 1, dot_num)
    y = np.ones(dot_num)*2
    C3 = np.array([x_n, y_n, y]).T
    x_n = np.random.normal(7, 1, dot_num)
    y_n = np.random.normal(3, 1, dot_num)
    y = np.ones(dot_num)*3
    C4 = np.array([x_n, y_n, y]).T
    data_set = np.concatenate((C1, C2, C3, C4), axis=0)
    np.random.shuffle(data_set)

    return data_set[:,:2].astype(np.float32),data_set[:,2].astype(np.int32)

X_train,y_train = generate_rand_samples()
y_train[y_train == -1] = 0

设置训练前的前置参数,并初始化分类器

num_inputs = 2  # 输入维度大小
num_outputs = 4  # 输出维度大小
learning_rate = 0.01  # 学习率
num_epochs = 2000 # 训练周期数

# 归一化数据 将数据特征减去均值再除以标准差
X_train = (X_train - X_train.mean(axis=0)) / X_train.std(axis=0)
y_train = y_train.astype(np.compat.long)

# 创建model并初始化
model = SoftmaxClassifier(num_inputs, num_outputs)
criterion = nn.CrossEntropyLoss() # 交叉熵损失
optimizer = optim.SGD(model.parameters(), lr=learning_rate)  # SGD优化器

训练:

# 遍历训练周期数
for epoch in range(num_epochs):
    outputs = model(torch.tensor(X_train))  # 前向传递计算
    loss = criterion(outputs,torch.tensor(y_train))  # 计算预测输出和真实标签之间的损失
    train_accuracy = model.compute_accuracy(outputs,torch.tensor(y_train))  # 计算模型当前训练周期中准确率

    optimizer.zero_grad()  # 清楚优化器中梯度
    loss.backward()  # 计算损失对模型参数的梯度
    optimizer.step()
	
    # 打印信息
    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}, Accuracy: {train_accuracy:.4f}")

运行:

Epoch [1820/2000], Loss: 0.9947, Accuracy: 0.9575
Epoch [1830/2000], Loss: 0.9940, Accuracy: 0.9600
Epoch [1840/2000], Loss: 0.9932, Accuracy: 0.9600
Epoch [1850/2000], Loss: 0.9925, Accuracy: 0.9600
Epoch [1860/2000], Loss: 0.9917, Accuracy: 0.9600
....

测试

生成测试并测试:

X_test, y_test = generate_rand_samples()  # 生成测试数据
X_test = (X_test- np.mean(X_test)) / np.std(X_test)  # 归一化
y_test = y_test.astype(np.compat.long)
predicts = model(torch.tensor(X_test))  # 获取模型输出
accuracy = model.compute_accuracy(predicts,torch.tensor(y_test))  # 计算准确度
print(f'Test Accuracy: {accuracy:.4f}')

输出:

Test Accuracy: 0.9725

绘制图像:

# 绘制图像
x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() + 1
y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1), np.arange(y_min, y_max, 0.1))
Z = model(torch.tensor(np.c_[xx.ravel(), yy.ravel()], dtype=torch.float32)).argmax(dim=1).numpy()
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.4)
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, s=20, edgecolor='k')
plt.show()

感知机分类器

实现与上述softmax分类器相似,此处实现sigmod感知机,采用sigmod作为分类函数,该函数可以将线性变换的结果映射为0到1之间的实数值,通常被用作神经网络中的激活函数

sigmoid感知机的学习算法与普通的感知机类似,也是采用随机梯度下降(SGD)的方式进行更新。不同之处在于,sigmoid感知机的输出是一个概率值,需要将其转化为类别标签。

通常使用阈值来决定输出值所属的类别,如将输出值大于0.5的样本归为正类,小于等于0.5的样本归为负类。

定义

# 感知机分类器
class PerceptronClassifier(nn.Module):
    def __init__(self, input_size,output_size):
        super(PerceptronClassifier, self).__init__()
        self.linear = nn.Linear(input_size,output_size)

    def forward(self, x):
        logits = self.linear(x)
        return torch.sigmoid(logits)

    def compute_accuracy(self, pred, target):
        pred = torch.where(pred >= 0.5, 1, -1)
        accuracy = (pred == target).sum().item() / target.size(0)
        return accuracy

给定一个输入向量(x1,x2,x3...xn),输出为
y
=
σ
(
w

x
+
b
)=1/(
e
^−(
w

x
+
b
))

训练

生成训练集:

def generate_rand_samples(dot_num=100):
    x_p = np.random.normal(3., 1, dot_num)
    y_p = np.random.normal(3., 1, dot_num)
    y = np.ones(dot_num)
    C1 = np.array([x_p, y_p, y]).T
    x_n = np.random.normal(6., 1, dot_num)
    y_n = np.random.normal(0., 1, dot_num)
    y = np.ones(dot_num)*-1
    C2 = np.array([x_n, y_n, y]).T
    data_set = np.concatenate((C1, C2), axis=0)
    np.random.shuffle(data_set)
    return data_set[:,:2].astype(np.float32),data_set[:,2].astype(np.int32)

X_train,y_train = generate_rand_samples()
X_test,y_test = generate_rand_samples()

该过程与上述softmax分类器相似:

num_inputs = 2
num_outputs = 1
learning_rate = 0.01
num_epochs = 200

# 归一化数据 将数据特征减去均值再除以标准差
X_train = (X_train - X_train.mean(axis=0)) / X_train.std(axis=0)

# 创建model并初始化
model = PerceptronClassifier(num_inputs, num_outputs)
optimizer = optim.SGD(model.parameters(), lr=learning_rate)  # SGD优化器

criterion = nn.functional.binary_cross_entropy

训练:

# 遍历训练周期数
for epoch in range(num_epochs):
    outputs = model(torch.tensor(X_train))
    labels = torch.tensor(y_train).unsqueeze(1)
    loss = criterion(outputs,labels.float())
    train_accuracy = model.compute_accuracy(outputs, labels)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}, Accuracy: {train_accuracy:.4f}")

输出:

Epoch [80/200], Loss: -0.5429, Accuracy: 0.9550
Epoch [90/200], Loss: -0.6235, Accuracy: 0.9550
Epoch [100/200], Loss: -0.7015, Accuracy: 0.9500
Epoch [110/200], Loss: -0.7773, Accuracy: 0.9400
....

测试

X_test, y_test = generate_rand_samples() # 生成测试集
X_test = (X_test - X_test.mean(axis=0)) / X_test.std(axis=0)

test_inputs = torch.tensor(X_test)
test_labels = torch.tensor(y_test).unsqueeze(1)
with torch.no_grad():
    outputs = model(test_inputs)
    accuracy = model.compute_accuracy(outputs, test_labels)
    print(f"Test Accuracy: {accuracy:.4f}")

绘图:

x_min, x_max = X_test[:, 0].min() - 1, X_test[:, 0].max() + 1
y_min, y_max = X_test[:, 1].min() - 1, X_test[:, 1].max() + 1
xx, yy = torch.meshgrid(torch.linspace(x_min, x_max, 100), torch.linspace(y_min, y_max, 100))

# 预测每个点的类别
Z = torch.argmax(model(torch.cat((xx.reshape(-1,1), yy.reshape(-1,1)), 1)), 1)
Z = Z.reshape(xx.shape)

# 绘制分类图
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral,alpha=0.0)

# 绘制分界线
w = model.linear.weight.detach().numpy()  # 权重
b = model.linear.bias.detach().numpy()  # 偏置
x1 = np.linspace(x_min, x_max, 100)
x2 = (-b - w[0][0]*x1) / w[0][1]
plt.plot(x1, x2, 'k-')

# 绘制样本点
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=plt.cm.Spectral)
plt.show()

概述

C++的模板相比于C#的泛型,有很多地方都更加的灵活(虽然代价是降低了编译速度),比如C++支持变长参数模板、支持枚举、int等类型的值作为模板参数。
C++支持枚举、int等类型的值作为模板参数,为C++的静态多态编程提供了很好的帮助,比如根据枚举值编译期确定某个对象的行为策略等(下文举例)。但是C#对这些都是不支持,但是C#天然支持反射,这种需求可以使用反射特性来实现。

需求示例

定义枚举 enum EPlant {Tree, Flower},根据枚举的值打印Tree,Flower字符串。注意,这里的应用场景是编译器时的多态,即编码时便确定使用的对象的类型。

C++的实现

上述的例子,C++的语法支持可以天然的实现,如下:

#include <iostream>

enum class EPlant
{
    Tree = 0,
    Flower,
};

template<EPlant ...Args>
class PrintPlant
{
    
};

template<>
class PrintPlant<>
{
public:
    void Print()
    {
        std::cout << "Plant" << std::endl;;
    }
};

template<>
class PrintPlant<EPlant::Tree>
{
public: 
    void Print()
    {
        std::cout << "Tree" << std::endl;;
    }
};

template<>
class PrintPlant<EPlant::Flower>
{
public:
    void Print()
    {
        std::cout << "Flower" << std::endl;
    }
};

int main()
{
    auto plant = new PrintPlant<>();
    plant->Print();
    auto flower = new PrintPlant<EPlant::Flower>();
    flower->Print();
    auto tree = new PrintPlant<EPlant::Tree>();
    tree->Print();
}

输出:
image

  • template<EPlant ...Args> 这里使用变长参数模板,来支持没有传入模板参数的情况,特化类型Print函数打印"plant"
  • template<> class PrintPlant<EPlant::Tree> 模板特化的类型,在main里使用了new PrintPlant<EPlant::Tree>();语句创建该类型的对象。该对象打印"Tree"。

C# 实现

C#的模板不支持枚举的值作为模板参数,使用反射进行模拟。

using System;
using System.Reflection;
using System.Collections.Generic;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ABTEX : Attribute
{
    public object key;

    public ABTEX(object k)
    {
        key = k;
    }
}

public class TEX
{
    static Dictionary<Type, Dictionary<Type, Dictionary<string, object>>> dict;
    public static void Init(Type[] types)
    {
        dict = new();
        foreach (var t in types)
        {
            var ABTEX = t.GetCustomAttribute<ABTEX>();
            var bt = t.BaseType;
            if (ABTEX != null && bt != null)
            {
                AddInst(t, bt, ABTEX.key);
            }
        }
    }

    static string FmtKey(object key)
    {
        return $"{key}";
    }

    static void AddInst(Type ty, Type bt, object key)
    {
        if (!dict.ContainsKey(bt))
        {
            dict[bt] = new();
        }

        var kt = key.GetType();
        string k = FmtKey(key);

        if (!dict[bt].ContainsKey(kt))
        {
            dict[bt][kt] = new();
        }

        dict[bt][kt][k] = Activator.CreateInstance(ty);
    }

    static public R T<R>(object key)
    {
        if (dict.TryGetValue(typeof(R), out Dictionary<Type, Dictionary<string, object>> dbt))
        {
            var kt = key.GetType();
            string k = FmtKey(key);
            if (dbt.TryGetValue(kt, out Dictionary<string, object> kbt))
            {
                if (kbt.TryGetValue(k, out object ins))
                {
                    return (R)ins;
                }
            }
        }

        return default(R);
    }
}

public enum EPlant : int
{
    None = 0,
    Tree,
    Flower,
}

public class APrintPlant
{
    public virtual void Print()
    {
        Console.WriteLine("Plant");
    }
}

[ABTEX(EPlant.Tree)]
public class PrintTree : APrintPlant
{
    public override void Print()
    {
        Console.WriteLine("Tree");
    }
}

[ABTEX(EPlant.Flower)]
public class PrintFlower : APrintPlant
{
    public override void Print()
    {
        Console.WriteLine("Flower");
    }
}

class Program
{
    static void Main(string[] args)
    {
        var all = Assembly.GetExecutingAssembly().GetTypes();
        TEX.Init(all);
        TEX.T<APrintPlant>(EPlant.Tree).Print();
        TEX.T<APrintPlant>(EPlant.Flower).Print();
    }
}

输出:
image
C#可以保存类型信息到运行期,通过运行期分析类型信息创建对象实现静态多态。

  • TEX类分析传入的所有类型,筛选父类和ABTEX特性,使用父类型,ABTEX的key的类型和值来索引该类型。(这里索引是实例对象,有需求的话可以保存类型Type,使用类型通过反射创建对象)
  • ABTEX标记需要反射分析的类型,并且标记key。
  • Main入口获取当前程序集下所有的类型信息,初始化TEX
  • 通过TEX.T<抽象类>(key).Func 调用方法(注意: 这里使用这些类作为纯函数的类,故使用类似单例的用法。也可以在初始化记录类型,通过反射创建多个实例。)