2023年4月

NIM游戏

先看一下一维 NIM游戏。

有一堆大小为
\(n\)
的石子,甲和乙轮流从石堆里面拿石子,不能一次拿掉所有石子,取走最后一个石子的人获胜,甲先开始,谁是必胜的?

显然,谁先手,谁就获胜。那么推广到二维呢?

有两堆大小为
\(n\)
\(m\)
的石子,甲和乙轮流从两个石堆里拿石子,每次从一个石堆里拿石子,不能一次拿掉一堆中所有石子,取走最后一个石子的获胜,甲先开始拿,谁是必胜的?


\(n=m\)
的时候,先手必输。因为先手从一堆中拿
\(Y\)
颗,后手也可以从另外一个堆中拿
\(Y\)
颗。循环下去,后手必胜。
而如果
\(n != m\)
,先手就可以制造两堆相等的石子,使得后手必败。

引入点新概念。当两堆相等时,先手必败,我们将这种状态叫做
必败态
。记为
\(P\)

当两堆不相等时,先手必胜,将这种状态叫做
必胜态
,记为
\(N\)

那么,推广到多维,如何确定谁是必胜的呢?
由前两种NIM游戏可以知道,如果所有石堆大小都相等,那么先手是不能直接取得胜利的。这种状态称为
平衡态
。反之,可以进行一次操作就取得胜利的状态就是
非平衡态
。平衡态也就是必胜态,非平衡态也就是必败态。
考虑将所有石堆的大小异或起来,如果结果为
\(0\)
,那么这就是一个平衡态。如果结果不为零,那就是非平衡态。
我们每拿一次石子,都可以将异或时某一位上的值由这位上的
\(1\)
的奇偶性决定。因此我们拿石子时可以控制每一位上
\(1\)
的奇偶性,也就因此能控制异或出的总结果了。同样的,对手每操作一次,由于必须拿至少一颗石头,就势必将会影响某一位上
\(1\)
的数量,状态必然会改变。这就意味着我们在操作的时候,可以实现
平衡态和非平衡态之间的转化

如果一开始我们接手的是一个不平衡态,要取胜,就可以反复给对手构造一个平衡态,这是必胜的。
如果一开始接手的是一个平衡态,只要对手足够聪明,就可以让我们每次都拿到一个平衡态。这就是必败的。

SG函数

由刚才的论述可知,必胜态和必败态之间是可以相互转化的。必败态经过一次操作必然会转化为必胜态,必胜态经过一次操作可能是必胜态,也可能是必败态(想想异或的过程)。当一个状态已经转化为能够分出胜负的必败态时,称这个状态是
0级必胜点
,记作
\(SG(x)=0\)

\(x\)
描述了当前的状态)。
如果某个状态最少操作一次就能变为0级必胜点,那么这个状态就是1级必胜点,以此类推,有2级,3级……必胜点。而SG函数就是用来描述每个状态到达终末态时所需要的最少的步骤,即描述每个状态是几级必胜点。定义为:

\[SG(x) = MEX\{SG(y)|x->y\}
\]

SG定理

  • 两个同级必胜点(SG函数值相等)组成的游戏是必败的。因为先手如果降低其中一个必胜点的等级,后手可以降低另一个必胜点的相同数量的等级,使先手一直面对两个同级必胜点,最后面对两个1级必胜点,只能将其中一个必胜点变成必败点,这样先手必败。

  • 两个不同级必胜点(SG函数值不同)组合成的的游戏是必胜的,因为先手可以将等级高的必胜点的等级降低到与另一个必胜点相同,这样后手面对的就是由两个同级必胜点构成局面,先手必胜。

对于一个游戏,可以将组成它的每一个游戏的SG函数值异或起来,为
\(0\)
,则对于先手来说必败,反之对于先手就是必胜的。这就是
SG定理
了!

例题:

Fibonacci again and again

三堆大小分别为
\(n\)

\(m\)

\(p\)
的石子,每堆大小均不超过
\(1000\)
,两个人拿,令
\(x\)
为菲波那契数列中的一项,每个人每次只能从一堆里拿
\(x\)
个石子,问谁是必胜的。

板题,主要想说说怎么记忆化搜索求SG函数值。

int sg[MAXN + 5],f[MAXN + 5];//f为菲波那契数列
int getsg(int num){
    if(num == 0)return sg[num] = 0;
	if(sg[num] != -1)return sg[num];
	bool vis[MAXN + 5];//表示从石子数为num可以转换到哪些状态
	for(int i = 1; i <= MAXN: i++){
		vis[num - f[i]] = 1;
		getsg(num - f[i]);
	}
	for(int i = 0; ; i++){
		if(!vis[i])return sg[num] = i;//找mex,求出是几级必胜点
	}
	return sg[num];
}

求出三个数的SG值后,看
\(SG(n) ^ SG(m) ^ SG(p)\)
是否为
\(0\)
即可得出答案。

A multiplication game

给定一个
\(n\)
,令
\(p = 1\)
,甲和乙可以每次将
\(p\)
乘上一个属于
\([2,9]\)
的数,谁使
\(p\)
大于
\(n\)
谁就赢。求谁是必胜的。

#include<iostream>
#include<cstring>
#include<map>
#define int long long
using namespace std;
const int MAXN = 1e5;
int n;
map<int,int> sg,vis;
int getsg(int x){
    if(x >= n)return sg[x] = 0;
    if(vis.find(x) != vis.end())return sg[x];
    vis[x] = 1;
    int s[10];//9的十次方已经大于n的最大值了,故sg函数的值最大为9
    memset(s,0,sizeof s);
    for(int i = 2; i <= 9; i++){
        s[getsg(x * i)] = 1;
    }
    for(int i = 0; i < 10; i++){
        if(!s[i])return sg[x] = i;
    }
    return sg[x];
}
signed main(){
    while(~scanf("%lld",&n)){
        sg.clear();
        vis.clear();
        getsg(1);
        if(sg[1])cout << "Stan wins.\n";
        else cout << "Ollie wins.\n";
    }
}

Cutting Game

两个人在一张大小为
\(h * w\)
纸上面切,每个人每次可以横着切一刀,也可以竖着切一刀,谁切出了
\(1 * 1\)
大小的纸,谁就获胜,问谁必胜。

注意到状态是二维的,即当前纸的长与宽。注意下SG函数的记录方法。

#include<iostream>
#include<cstring>
using namespace std;
const int MAXN = 1e3;
int sg[MAXN + 5][MAXN + 6],n,m;
int getsg(int x,int y){
    if(x < y)swap(x,y);
    if(sg[x][y] != -1)return sg[x][y];
    if(x == 1 && y == 1)return sg[x][y] = 1;
    bool vis[4001];
    memset(vis,0,sizeof vis);
    for(int i = 2; i <= x - 2; i++){//横着切
        vis[getsg(i,y) ^ getsg(x - i,y)] = 1;//每切一刀,就将当前纸分成了两张,也就是分成了两个子游戏,因此取异或的值
    }
    for(int i = 2; i <= y - 2; i++){//竖着切
        vis[getsg(x,i) ^ getsg(x,y - i)] = 1;
    }
    for(int i = 0; i <= 4000; i++){
        if(!vis[i])return sg[x][y] = i;
    }
    return sg[x][y];
}
int main(){
    memset(sg,-1,sizeof sg);
    for(int i = 2; i <= MAXN; i++){
        sg[1][i] = 0;
    }
    while(~scanf("%d%d",&n,&m)){
        int ans = getsg(n,m);
        if(ans)cout << "WIN\n";
        else cout << "LOSE\n";;
    }
}

现在有四张卡,但是部署在windows10系统上,想尝试下在windows上使用单机多卡进行分布式训练,网上找了一圈硬是没找到相关的文章。以下是踩坑过程。

首先,pytorch的版本必须是大于1.7,这里使用的环境是:

pytorch==1.12+cu11.6
四张4090显卡
python==3.7.6

使用nn.DataParallel进行分布式训练

这一种方式较为简单:
首先我们要定义好使用的GPU的编号,GPU按顺序依次为0,1,2,3。gpu_ids可以通过命令行的形式传入:

gpu_ids = args.gpu_ids.split(',')
gpu_ids = [int(i) for i in gpu_ids]
torch.cuda.set_device('cuda:{}'.format(gpu_ids[0]))

创建模型后用nn.DataParallel进行处理,

 model.cuda()
 r_model = nn.DataParallel(model, device_ids=gpu_ids, output_device=gpu_ids[0])

对,没错,只需要这么两步就行了。需要注意的是保存模型后进行加载时,需要先用nn.DataParallel进行处理,再加载权重,不然参数名没对齐会报错。

checkpoint = torch.load(checkpoint_path)
model.cuda()
r_model = nn.DataParallel(model, device_ids=gpu_ids, output_device=gpu_ids[0])
r_model.load_state_dict(checkpoint['state_dict'])

如果不使用分布式加载模型,你需要对权重进行映射:

new_start_dict = {}
for k, v in checkpoint['state_dict'].items():
    new_start_dict["module." + k] = v
model.load_state_dict(new_start_dict)

使用Distributed进行分布式训练

首先了解一下概念:
node:主机数,单机多卡就一个主机,也就是1。
rank:当前进程的序号,用于进程之间的通讯,rank=0的主机为master节点。
local_rank:当前进程对应的GPU编号。
world_size:总的进程数。
在windows中,我们需要在py文件里面使用:

import os
os.environ["CUDA_VISIBLE_DEVICES]='0,1,3'

来指定使用的显卡。
假设现在我们使用上面的三张显卡,运行时显卡会重新按照0-N进行编号,有:

[38664] rank = 1, world_size = 3, n = 1, device_ids = [1]
[76032] rank = 0, world_size = 3, n = 1, device_ids = [0]
[23208] rank = 2, world_size = 3, n = 1, device_ids = [2]

也就是进程0使用第1张显卡,进行1使用第2张显卡,进程2使用第三张显卡。
有了上述的基本知识,再看看具体的实现。

使用torch.distributed.launch启动

使用torch.distributed.launch启动时,我们必须要在args里面添加一个local_rank参数,也就是:
parser.add_argument("--local_rank", type=int, default=0)
1、初始化:

import torch.distributed as dist

env_dict = {
        key: os.environ[key]
        for key in ("MASTER_ADDR", "MASTER_PORT", "RANK", "WORLD_SIZE")
}
current_work_dir = os.getcwd()
    init_method = f"file:///{os.path.join(current_work_dir, 'ddp_example')}"
dist.init_process_group(backend="gloo", init_method=init_method, rank=int(env_dict["RANK"]),
                                world_size=int(env_dict["WORLD_SIZE"]))

这里需要重点注意,这种启动方式在环境变量中会存在RANK和WORLD_SIZE,我们可以拿来用。backend必须指定为gloo,init_method必须是file:///,而且每次运行完一次,下一次再运行前都必须删除生成的ddp_example,不然会一直卡住。
2、构建模型并封装
local_rank会自己绑定值,不再是我们--local_rank指定的。

 model.cuda(args.local_rank)
 r_model = torch.nn.parallel.DistributedDataParallel(model, device_ids=device_ids)

3、构建数据集加载器并封装

  train_dataset = dataset(file_path='data/{}/{}'.format(args.data_name, train_file))
  train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)
  train_loader = DataLoader(train_dataset, batch_size=args.train_batch_size,
                              collate_fn=collate.collate_fn, num_workers=4, sampler=train_sampler)

4、计算损失函数
我们把每一个GPU上的loss进行汇聚后计算。

def loss_reduce(self, loss):
        rt = loss.clone()
        dist.all_reduce(rt, op=dist.ReduceOp.SUM)
        rt /= self.args.local_world_size
        return rt

loss = self.criterion(outputs, labels)
torch.distributed.barrier()
loss = self.loss_reduce(loss)

注意打印相关信息和保存模型的时候我们通常只需要在local_rank=0时打印。同时,在需要将张量转换到GPU上时,我们需要指定使用的GPU,通过local_rank指定就行,即data.cuda(args.local_rank),保证数据在对应的GPU上进行处理。
5、启动
在windows下需要把换行符去掉,且只变为一行。

python -m torch.distributed.launch \
--nnode=1 \
--node_rank=0 \
--nproc_per_node=3 \
main_distributed.py \
--local_world_size=3 \
--bert_dir="../model_hub/chinese-bert-wwm-ext/" \
--data_dir="./data/cnews/" \
--data_name="cnews" \
--log_dir="./logs/" \
--output_dir="./checkpoints/" \
--num_tags=10 \
--seed=123 \
--max_seq_len=512 \
--lr=3e-5 \
--train_batch_size=64 \
--train_epochs=1 \
--eval_batch_size=64 \
--do_train \
--do_predict \
--do_test

nproc_per_node、local_world_size和GPU的数目保持一致。

使用torch.multiprocessing启动

使用torch.multiprocessing启动和使用torch.distributed.launch启动大体上是差不多的,有一些地方需要注意。

mp.spawn(main_worker, nprocs=args.nprocs, args=(args,))

main_worker是我们的主运行函数,dist.init_process_group要放在这里面,而且第一个参数必须为local_rank。即main_worker(local_rank, args)
nprocs是进程数,也就是使用的GPU数目。
args按顺序传入main_worker真正使用的参数。
其余的就差不多。
启动指令:

python main_mp_distributed.py \
--local_world_size=4 \
--bert_dir="../model_hub/chinese-bert-wwm-ext/" \
--data_dir="./data/cnews/" \
--data_name="cnews" \
--log_dir="./logs/" \
--output_dir="./checkpoints/" \
--num_tags=10 \
--seed=123 \
--max_seq_len=512 \
--lr=3e-5 \
--train_batch_size=64 \
--train_epochs=1 \
--eval_batch_size=64 \
--do_train \
--do_predict \
--do_test

最后需要说明的,假设我们设置的batch_size=64,那么实际上的batch_size = int(batch_size / GPU数目)。
附上完整的基于bert的中文文本分类单机多卡训练代码:
https://github.com/taishan1994/pytorch_bert_chinese_text_classification

参考

https://github.com/tczhangzhi/pytorch-distributed
https://murphypei.github.io/blog/2020/09/pytorch-distributed
https://pytorch.org/docs/master/distributed.html?highlight=all_gather#torch.distributed.all_gather
https://github.com/lesliejackson/PyTorch-Distributed-Training
https://github.com/pytorch/examples/blob/ddp-tutorial-code/distributed/ddp/example.py
996黄金一代:[原创][深度][PyTorch] DDP系列第一篇:入门教程
「新生手册」:PyTorch分布式训练 - 知乎 (zhihu.com)

2021年的一场危机,给园子来了个做梦也没想到的突然袭击,让园子一片狼藉。

2022年的双重困境,让园子在暴风雨之后再添恶劣天气,不给园子一点喘息之机。

2023年困难重重的开局,将园子陷于看不到雨后彩虹的绝地,逼着园子唯有奋起出击。

今年是园子商业化努力生死攸关的一年,我们将在自己认可的商业化方向上奋力探索,全力以赴找到园子的商业化出路,并开启园子现代化升级的新阶段。

期待大家的理解与支持,更期待大家的献计献策,帮助园子找到最适合的商业模式。

目前在推进的商业化努力项目有3个:

1)IT人才出海服务:3月底已推出第一站,详见
https://brands.cnblogs.com/haoron/

2)VIP会员服务:预计4月底上线。

3)云市场精品店:与阿里云云市场合作,预计5月推出。

3 月的碎碎念,大致总结了商业人生、付费软件、创业方向选择、创业感性还是理性、如何解决复杂问题及如何成长这几个方面的内容。

商业人生

商业人生需要试错能力和快速信息收集与验证校准;

商业逻辑需要试错能力,收集各种渠道信息后整理决策。快速信息收集和验证校准很重要。

付费软件

付费软件产品可以依托大平台,而不仅仅局限于 APP Store 和 Chrome 插件 Store,比如刚出的 ChatGPT Plugin...

还有很多平台可以做:

file

创业方向及心法

创业方向的选择需要从自己的职场锻炼能力或兴趣爱好出发,注意低风险的选择;

创业是一场理性的游戏,需要绝对的客观、理性和实事求是;需要理性思考,遵循市场规律。

找到自己专属的路。同时需要注意低风险,避免现金和生活压力过大。

如何解决复杂问题?

解决复杂问题时,需要先整体再局部,以假设为驱动、以事实为基础、符合逻辑的真知灼见;

避免只见树木,不见森林

解决复杂问题需要先围绕战略、战术和任务进行整体考虑;

成长自己的途径有读书、行路、学徒和做事;

冯唐在书中说过这个:

“尽管任何事都没有完美解决方案,但是任何事在某个时间范围内一定有最佳解决方案。”

  • 先以时间维度切里程碑
  • 定位该里程碑,最大的问题或目的
  • 寻找达到目的最佳解决方案

不要恋战,不要试图解决所有问题,全面应用二八原则。尽百分之一百的力气,每个“二”达成。百分之百的力气最终达成百分之四百的成果。

如何成长

成长自己,冯唐在书中说过这个:

"修炼成事有四个常规途径:读书、行路、学徒、做事。读万卷书,行万里路,跟定两三个师父,不间断地在实际做事中锻炼自己。"

对我而言,收获最大的从高到低:

  • 实战中学,边干边学,边学边干
  • 认定某个领域你要模仿的师傅,通过人去建联去深交去学到
  • 通过筛选有效的信息来源,常看常新,好的内容收藏到笔记

成长团队,空降也好,团队有新成员也罢

每个人理解维度和认知高度不同,最好的协作是 包容 状态

节奏可能是“先理解,再融入,再影响”

原文链接:

https://bysocket.com/saas-think-202303/

出处:公号「程序员泥瓦匠」
博客:
https://bysocket.com/

内容涵盖 Java 后端技术、Spring Boot、Spring Cloud、微服务架构、运维开发、系统监控等相关的研究与知识分享。

本文已收录到
AndroidFamily
,技术和职场问题,请关注公众号 [彭旭锐] 提问。

声明:此图片由 MidJourney 生成
未经训练,不属于任何真实人物

大家好,我是小彭。

2023 开年以来,全球媒体最火爆的热点莫过于一个生成式 AI 聊天机器人 —— ChatGPT,我们都被大量的信息刷屏了。在这些信息中,你或许看过这样一则新闻 《ChatGPT Passes Google Coding Interview for Level 3 Engineer With $183K Salary》,它说 ChatGPT 通过了谷歌工程师面试,则这个职位的年薪有 18.3 万美元。

让会不会让一些小伙伴产生焦虑的想法?

图片截图自新闻:
https://www.pcmag.com/news/chatgpt-passes-google-coding-interview-for-level-3-engineer-with-183k-salary

谷歌面试是会考算法的,ChatGPT 已经具备这么强的算法能力了吗?如果答案是肯定的,那么我们借助 ChatGPT 的力量帮助提高算法能力,是不是可行的想法。

试想一下:我们要学习一个算法或者新知识新领域,直接将问题抛给 AI,让 AI 直接传道授业解惑,直接向你总结出全社会沉淀下来的最有价值的经验、方法论、内容或观点,你甚至都不需要去上学、找资料、看书。遇到不理解的地方可以继续向 AI 追问,AI 还会非常有耐心的帮你解释……

未来,固然需要想象。

现实,ChatGPT 能做到吗?

正好,最近有群友让我写一篇算法的入门文章,借此机会,就让我们来学习如何使用 ChatGPT 辅助算法学习:

在接下来的几篇文章中,小彭将继续为你介绍 AI 技术的使用攻略以及实践感悟。

如果你对 ChatGPT 还不够熟悉,希望我能够为你提供一些指引。

让我们开始吧!


今天文章比较长,写个简单的大纲:

1、LeetCode 算法题学习链路简要分析

2、ChatGPT 助手从入门到放弃

2.1 ChatGPT 的能力和限制

2.2 ChatGPT 的使用原则

3、面向 ChatGPT 编程的正确打开方式

4、总结


1. LeetCode 算法题学习链路简要分析

首先,请你思考:完整地学习一道算法题包含哪些步骤或动作:



  • 步骤一:复制代码