跳到主要内容

AI 生成的一些图片

· 阅读需 3 分钟

这几天在电脑上运行了 Stable Diffusion 玩了玩。这是我正的测试页面:https://qizhen.xyz/genimg

这个模型比 Dall.E 的小很多,所以才能在配置不高的个人电脑上跑。而且,我的电脑也只能勉强生成 512*512 的照片。虽然效果可能不如专业网站用的大模型,但有些作品已经很逼真了。我主要试试了生成真实人像以及和二次元人像之间的转化。Diffusion 模型不太擅长逼真的人像,程序生成的人像使用 GFPGAN 改善过的。在人像中,个人感觉这个模型最擅长生成白人年轻女性的照片。我也尝试了让它生成一些中国人的照片,但感觉都不是那么美,实际上多数生成的中国人照片都看起来比较别扭。很可能是因为它的训练集中就有比较多的白人年轻女性照片,或者也可能是因为我生长在中国,对中国人的面貌更敏感,更容易发觉异常。

先贴两张 AI 生成的仿旧照片,看起来还真挺像真的:

接下来几组图片,都是首先生成了左边的真实照片风格的图片,再根据照片转换成了右边二次元图片:

真实照片风格二次元风格

下面这两组图片是先生成的左边的二次元风格图片,再根据它生成右侧的真实照片风格图片:

二次元风格真实照片风格

可以明显感觉到,生成真实照片要困难很多。可能是因为人脑对所谓“真实”照片更挑剔吧。

下面两组是风景图片,感觉两种风格生成出来的图片区别远没有人像的区别那么大

二次元风格真实照片风格
查看或添加留言

权力的毒瘾

· 阅读需 3 分钟

于是废先王之道,燔百家之言,以愚黔首;隳名城,杀豪俊,收天下之兵,聚之咸阳,销锋铸鐻,以为金人十二,以弱天下之民。然后践华为城,因河为池,据亿丈之城,临不测之渊以为固。良将劲弩,守要害之处;信臣精卒,陈利兵而谁何!天下已定,始皇之心,自以为关中之固,金城千里,子孙帝王万世之业也。

秦二世而亡的时候,大家就意识到了,始皇帝这套制度不那么灵验。本来计划是千秋万代的,结果却是断子绝孙。但是,之后的两千年里,各朝各代的君王们还是不由自主的采用了秦制。这在中国接触到西方现代文明之前,也还可以理解:秦制就算不好,也没见过更好的制度啊。没办法,只能拿它来修修补补继续用吧。修补妥当,可以有两百来年国运;修补不好,可能比秦朝还短。

两百年前,中国人终于开始意识到世界上还有其他文明的存在,甚至还有更好的政治制度。然而,这之后的领袖们依然还是摆脱不了秦始皇的魔咒,处心积虑就是想登基。有人在野的时候,说起民主自由来,比唱的还好听。一旦掌权,焚书坑儒、专制独裁有过之而无不及。我不禁想起了宋美龄评价政敌的一句话:“他们还没有尝过权力的滋味”。这么看起来,权力和毒品倒是挺像的,即便是原本有美好理想和追求的青年,一旦尝过了无上的权利,这世间其它的一切都立刻就变得索然无味了。唯一能安抚内心躁动的,就只有更大剂量的权力。

查看或添加留言

《独行月球》

· 阅读需 2 分钟

我有一阵子没看电影了,今天比较空闲,看了这部《独行月球》。电影非常一般,不过我发现我有一点跟男主人公一样:我的保险也是在齐齐哈尔上的。

沈腾马丽的巅峰是《夏洛特烦恼》,据说拍成电影之前,这部话剧他们已经表演过无数次了,细节都打磨的恰到好处。估计他们再也不会有动力在一部戏里花费这么大的精力了吧。相比起来,这部《独行月球》基本上没什么好笑的地方。幽默最好是贴近大众生活的,把主场景放到月球上,本身就比较难以让观众共情,又还把主角跟别人都分开了。马丽在戏里饰演冷血严肃的指挥官,完全没有可以幽默的余地。

最后这段炸小行星的情节实在是太烂了,在电影里,只要是个关键炸药包,就肯定没法远程引爆,必须要主角或者配角去手动引爆。我第一次看到这类情节还是在《董存瑞》的故事里面,初次读到确实震撼,感动了好几天。但是再震撼的情节多了也会审美疲劳啊,同样的炸小行星,几十年前就在美国大片里看过了,之后炸地心,炸外星飞船,炸火山,炸月亮...... 数都数不过来,怎么还要用这么烂的梗呢?

查看或添加留言

Scratch 编程语言 2

· 阅读需 7 分钟

很早之前,大约是两千零几年的时候,我曾经看别人演示过 LabVIEW 专为儿童教育,以及乐高玩具开发的特别版本。对于少年儿童来说,图形化编程比文本编程要更有吸引力。可惜的是,LabVIEW 起个大早,赶个晚集。现在再提起儿童教育或玩具领域的图形化编程语言,多数人只会想到 Scratch。

Scratch 是 2003 年才诞生的一个新语言。它能够一出现就挤掉 LabVIEW,迅速占领整个儿童教育领域,主要因为具有以下一些优点:

  • 开源。Scratch 是由 MIT 开发的,它从一开始就采用了开源的策略。儿童使用的编程语言,一个重要的功能是控制各种玩具。反过来说,能被玩具厂商广泛采纳的编程语言,会更容易被推广开来。玩具厂商想把自己的产品与 LabVIEW 结合,甚至直接发布一个定制版的 LabVIEW 成本是非常高的。即便是在工业界,LabVIEW 具有统治地位的测试测量领域,LabVIEW 也主要是与 NI 公司自己的硬件结合使用。玩具行业的利润更低,厂商们自然倾向于便宜的软件。更何况 Scratch 还是开源的,可以很方便的就对其进行改造以适应自己的产品。现在,除了最著名的乐高,很多中国厂商的玩具搭配的也是 Scratch 编程语言,比如小米的玩具。
  • 语法简单。Scratch 的语法与 LabVIEW 有着非常大的不同,它隐藏了更多的编程细节,让初学者可以更容易的入手。LabVIEW 虽然也号称容易入门,但相比比起来, Scratch 才真正算得上是“傻瓜”型编程语言。
  • 采用了更主流的技术。Scratch 是采用了 HTML 5 标准,使用 JavaScript 作为开发语言。LabVIEW 也曾经有过网页版,尽管当时已经可以明显看出 HTML 5 是发展趋势,LabVIEW 却采用了 SilverLight 作为开发平台。后来微软彻底抛弃了 SilverLight ,肯定也会对 LabVIEW 的开发推广造成影响的。

下面是一个具体的示例程序:

这段代码中的积木(一种颜色的近似长方形的一个条形块)分成了三堆,这三堆之间是并行的关系。每一堆积木都从一个事件开始。左边这一大堆是主线程,当接受到用户点击绿色旗帜的事件时开始运行,它在运行过程中会发出一些事件,去启动另外两堆积木。这段程序的主要功能是运行一个循环“repeat 20”,在循环内调用“move”功能,让屏幕上的一只小狐狸(绘制在“costume”里面)向前移动一段距离。同时还让小狐狸发出“喵呜”的声音。

从上面这段程序可以看出来,虽然也是图形化编程语言,Scratch 相比与 LabVIEW 还是有一些明显不同的。

  • Scratch 图形化方面没有 LabVIEW 彻底,它借鉴一些文本编程语言的编程方式,同时在编程时也更依赖文本。编写 LabVIEW 的程序更像是绘图,而编写 Scratch 程序更像是搭积木。
  • LabVIEW 中的基本功能模块多以函数和 VI 的形式存在,它们的外观看上去是一个个的正方形方块。LabVIEW 中的结构的外观会更复杂一些,像是尺寸可变的框架。在 Scratch 中,函数和结构都被称作 block(翻译成模块或者积木),它们看上去都是一个个长条。
  • Scratch 中由于每个积木长得都一样(或者十分类似),它只能用来文字区分不同功能的积木。LabVIEW 中推荐给每个子 VI 都绘制一个有意义的图标,这看起来当然是比 Scratch 的代码美观的多。但是,也有很多程序员非常讨厌这个规范,他们宁可把时间用于改进程序的逻辑,而不是美观程度。
  • 对于程序流程的控制,LabVIEW 使用数据线来控制,按照数据在数据线上的流动顺序来控制程序运行顺序。Scratch 没有数据线。凡是挨在一起的积木,它就是按照顺序从上至下执行每一个积木。没有粘连在一起的积木是可以并行执行的。
  • 因为没有数据线,Scratch 只能使用全局变量来传递数据。
  • 积木使用不同颜色表示不同的功能分类,比如浅黄色的用于发送接收事件;深黄色的用于控制程序流程(比如循环结构,条件结构等);紫红色的用于控制声音,蓝色的控制运动等等。
  • 用不同形状表示不同数据类型,比如数值类型数据放在一个两侧是圆弧的长条里;而布尔型数据放在两侧是尖角的长条里。这保证了程序具有一定的数据类型安全,比如某个积木上有一个两侧尖角的长条凹槽,那么在这个凹槽里就只能嵌入布尔型数据(相当于一个函数,具有一个布尔型的输入参数)。
查看或添加留言

Scheme 编程语言

· 阅读需 4 分钟

Scheme 是第一门我真正系统学习过的函数式编程语言。Scheme 语言的标志是一个 Lambda 字符“λ”,一眼就可以看出这门语言的出处。Scheme 是 LISP 语言的两大方言之一,而 LISP 又是人类开发出的第二款高级编程语言(第一个是 FORTRAN)。

因为太古老,Scheme 的编程思路和现在常见的语言差距还是非常大的。我刚开始接触 Scheme 时的困惑不亚于刚接触 LabVIEW。Scheme 的语法定义是比较简单的,比时下流行的编程语言都简单得多,但毕竟也是一门完整编程语言,不可能写一小段就介绍全面,这里就只能介绍一些最基本功能了。

Scheme 直观上最明显的特点是括号多,它的所有数据(比如列表)和程序结构(比如函数、判断语句)等都被包裹在括号内,因此,一段代码里会有数不清的层层括号。在编程思想上的最大特点就是函数式编程。再 Scheme 程序中,一切都是函数。

在 Scheme 中写 Hello, world 的代码如下: (display "Hello, World!")

在这段中 display 是一个函数,用于在屏幕上打印文字,而后面的字符串则是 display 的参数。单这一句,与常见编程语言的用法差距也不算太大。

在 Scheme 语句中,函数名总是要放在参数之前,运算符也是一个函数。所以,如果要计算 “2+3”,写出来的程序是这样的: (+ 2 3) 。 Scheme 的函数很多是可以跟多个参数的,比如 (max 2 6 3) 或者 (+ 6 4 8) 等等。如果是一组数据,比如一个 list,那么就在括号前加个单引号,比如 '(a b c d),这就不再是函数调用,而是一个列表了。

函数的定义一般类似如下:(lambda (x) (+ x 2))。lambda 是关键字,后面跟着函数参数,在后面是函数体。在 Scheme 语句中使用关键字 define 给常量命名,使用关键字 let 给变量命名。函数也可以是一种变量,比如下面的语句就给了新定义的函数一个名字“square”:

(define square (lambda (n) (* n n)) )

Scheme 语言还有一特点就是没有循环,所有需要循环的地方都要使用递归来完成。这和早期的 LabVIEW 正相反,早期的 LabVIEW 不支持递归,所有要用到递归的地方都必须转换成循环。比如,在 Scheme 中计算阶乘,只能采用递归的形式:

(define factorial (lambda (n) (if (= n 0) 1 (* n (factorial (- n 1))))))

我在学习 Scheme 的过程中,最大的收获是把递归彻底弄清楚了。递归有时候还是比较容易绕的,比如把一个列表从头开始归并,和从尾开始归并要采用不同的递归策略。以前都没有深入考虑过,学习了 Scheme 才真正系统的研究清楚了。

查看或添加留言

Lambda Calculus 编程语言

· 阅读需 8 分钟

我刚成为程序员的时候,有一次调试一段 C 语言代码。我一层一层的进入到被调用的子函数中去,想看看一个数据到底是怎么产生的。终于在遇到一个库函数的时候,调试器无法再跟踪进去了。C 语言程序通常会调用很多已经编译好的库函数,程序员只知道这些函数的接口,但看不到它们的实现代码。我知道这是处于效率的考虑,但还是忍不住想,一种编程语言,可不可以不调用任何编译好的库,所有功能都以源代码的形式提供给程序员,方便学习啊。后来进而又想,也许很多关键字,运算符都不是必须提前编译好的,有没有编程语言可以以源代码的形式提供这些关键字,运算符呢?

这些问题,当时也只是一想,没有去研究。多年之后,我在帮老婆做编程作业的时候发现,她们在课上居然学到了这样一种编程语言,叫做 Lambda Calculus。

Lambda Calculus 是一类非常精简的编程语言中的代表。这类语言中还包含 SKI,Iota 和 Jot 等,不过 Lambda Calculus 还是最经典的。Lambda Calculus 小到什么程度呢?只需要用几行文字就可以把 Lambda Calculus 的全部语法描述的清清楚楚,所以这里就介绍一下:

  • Lambda Calculus 中用到的全部字符包括:小写英文字母,英文句号,小括号和一个希腊字母 lambda “λ”。
  • 名字 name:由单个英文小写字母构成,格式为 <name\>。 比如 x,y,a 等;
  • 函数定义 function:由“λ”字符跟一个变量名,跟一个英文句号,再跟一个表达式,结构为 λ<name\>.<expression\>。比如 λx.x,这个函数写成数学的形式是 f(x) = x;
  • 函数调用 application:由函数定义加另一段表达式构成,格式<function\><expression\>。比如: (λx.x)a,这表示,把 a 作为参数传递给前面那个函数,运算结果就是 a。
  • name, function, application 又被统称为 expression (表达式)。
  • 括号用于控制计算的优先级。有时代码里也会加入空格以方便阅读。

以上就是这个编程语言的全部语法了。当时的要求是给这个语言编写一个编译器,其中最核心的部分只用了十来行代码就实现了,恐怕很难有比这更简单的编程语言了。可以直观的看出,这个编程语言的一些简单运算规则:

  • λx.xλy.y 是完全等价的,或者可以写成 λx.x ≡ λy.y
  • (λx.x)a 可以简化成(推演运算得到) a,或者写成 (λx.x)a = a
  • (λx.y)a = y
  • (λx.(λy.x))a = λy.a

大家已经发现了,这个语言,连循环、判断等结构都没有,对了,这些都要自己编程实现;甚至连数字也没有,加减法也没有,这些也通通都要自己定义和实现。下面就介绍一下如何使用 Lambda Calculus 编写一些基本的功能。

  • 实现多参数函数,这个方法有个专用名叫函数柯里化。比如实现函数f(x, y, z) = ((x, y), z) 可以使用如下的代码: λx.λy.λz.xyz
  • 逻辑运算中的“真”被定义为:TRUE ≡ λx.λy.x。这里对于“真”的定义是:输入两个参数,返回第一个。推算一下
    • TRUE a b ≡ (λx.λy.x) a b = (λy.a) b = a
  • 逻辑运算中的“假”被定义为:FALSE ≡ λx.λy.y。也就是输入两个参数,返回第二个。推算一下
    • FALSE a b ≡ (λx.λy.y) a b = (λy.y) b = b
  • 判断语句 if,假设我们需要当变量 b 为 真时,返回 t;当 b 为假时返回 f。那么可以定义 IF ≡ λx.xIF b t f ≡ λx.x b t f。 推算一下
    • IF TRUE t f ≡ (λx.x) (λx.λy.x) t f = (λx.λy.x) t f = t
    • IF FALSE t f ≡ (λx.x) (λx.λy.y) t f = (λx.λy.y) t f = f
  • 有了以上的基础,逻辑运算的定义就简单多了,比如:
    • AND a b ≡ IF a b FALSE
    • OR a b ≡ IF a TRUE b
    • NOT a ≡ IF a FALSE TRUE
  • 定义数字:
    • 我们可以只考虑自然数,其它数值都可以从自然数推算得到。自然数的定义是:
      • 0 是自然数
      • 每一个确定的自然数 a,都有一个确定的后继数 a' ,a' 也是自然数
      • 对于每个自然数 b、c,b = c 当且仅当 (b 的后继数) = (c 的后继数)
      • 0 不是任何自然数的后继数
    • 基于自然数定义,我们可以在 Lambda Calculus 如下定义自然数
      • 0 ≡ λf.λx.x 可发现 0 ≡ FALSE
      • 1 ≡ λf.λx.f x
      • 2 ≡ λf.λx.f (f x)
      • 3 ≡ λf.λx.f (f (f x))
      • ……
  • 为了方便数字运算还要先定义一个辅助的“后继函数”:S = λn.λf.λx.f((n f) x) 。调用这个函数会得到输入参数的后继数,比如 S4 = 5。试验一下:
    • S 0 ≡ (λn.λf.λx.f((n f) x)) (λf.λx.x) = λf.λx.f(λx.x f) x) = λf.λx.f x ≡ 1
  • 加法就可以定义为: ADD ≡ λa λb.(a S) b 。 拭一下:
    • ADD 2 3 = (λa λb.(a S) b) 2 3 = 2 S 3 ≡ (λf.λx.f (f x)) S 3 = λx. S (S x) 3 = S (S 3) = S 4 = 5

以上是 Lambda Calculus 一些最基本的功能,作为一个图灵完全的语言,它能做的远不止这些,其它编程语言能做的,它基本也都可以做。但是我们也发现了,如果一个语言什么预先定义都没有,一切都需要开发者自己从头做起,那么在实际应用中就效率太低了。

Lambda Calculus 的发明人是 Alonzo Church,他有个大名鼎鼎的学生,图灵。Lambda Calculus 对于后来编程语言的发展产生了深远的影响。函数式编程就是受此启发而来。如今,Lambda 函数更是成了主流编程语言的标配。

查看或添加留言

训练一个黑白棋模型

· 阅读需 7 分钟

之前用 LabVIEW 编写了一个黑白棋程序,作为学习 XControl 的示例。那个程序基本完整,但是缺少一个 AI 自动走子的功能。最近抽空尝试训练了一个网络模型,添加到了演示程序中。

所有AI下棋的思路都是非常类似的:根据当前的盘面,给每个可以落子的位置打分,选取分数最高的那个走法。这里的关键就是设计一个最为合理的打分算法。

黑白棋最终判定胜负的标准是看谁的子多,所以最符合直觉的打分方法是:在一个位置落子后,把己方的棋子数目作为这个位置的分数。这个算法有个缺陷,就是当前棋子数最多的走法不见得就能保证将来也棋子数最多。

解决这个缺陷的思路有两条:其一是,不要为当前这一步打分,而是向后预测几步。比如,把双方各下三步棋(也就是总够六步)后的所有可能出现的局面都列出来,然后看那个局面得分最高。考虑的步数越深,棋力也就越高,极端情况,如果预测的足够深,甚至可以找到必胜的下法。这个方法的缺点是预算量非常高,增加预测的深度,计算量会指数级增加。可以通过剪枝做一些优化,但效果有限。

第二条思路是采用更复杂的打分算法,如果只考虑棋子数量还不够好,那么就也同时考虑落子的位置,稳定的棋子的个数,周围空间的个数等等。在这众多的因素中哪些更重要,如何分配权重,对于我这种非专业棋手来说是很难做选择的。不过这部分可以利用机器学习算法,让电脑自己找到最优解。

当然以上两条思路可以结合使用:先找到一个最优的打分算法,再尽量预测到更深的步数。

我尝试训练了一个基于神经网路的打分模型。

我采用的是一个只有一层隐藏层,64节点的全链接神经网络。单隐藏层64节点的神经网络对于解决黑白棋来说,有点太小了。我也不能确定多大才合适,但估计至少也应该采用一个七八层的CNN,才能达到不错的效果。不过,我的目标不是最好的效果,而是只要试验一下可行性。并且,我需要把训练好的模型移植到 LabVIEW 代码上,复杂模型移植起来太麻烦。所以我选择了这个最简单的模型。

模型的输入数据是当前棋盘状态(每个位置棋子的颜色)和一个准备落子的位置,模型输出一个实数表示得分。

训练模型的大致思路是:让模型分别持黑子和白子,自己跟自己下棋,并且记录下每一步的走法。最终胜利一方所有走过的位置都获得正标签,失败一方所有走过的位置都是负标签。用这些标签训练模型,然后重复以上过程。

在训练模型的过程中,我遇到了一些以前从未考虑过的问题。比如,采用何种激活函数。我开始采用的 ReLU 函数,但模型始终无法被训练到预期效果。调试后发现,是 ReLU 的神经元坏死造成的。ReLU 在输入值小于零时,梯度永远为0,这个神经元很可能再也不会被任何数据激活了。这对于目前常见的有着几万乃至几百亿节点的大规模模型来说,根本不是问题。但是对于一个本来就没几个节点的小型模型来说,损失神经元是非常致命的。我把激活函数换成 Sigmoid 之后,效果就好了很多。

我训练模型的方法效率非常低下。在下棋过程中,大多数的步骤可能都不是太重要,不论走这还是走那,对最终结果影响都不大。关键的就那么少数几步,可以决定输赢。但是在训练时候,我也不知道每一步的权重应该有多大,只能假设所有步数都是一样的权重。那些垃圾步不但占用资源,也会影响训练结果。

模型训练好之后,我把它移植到了之前编写好的 LabVIEW 黑白棋 VI 中。棋力算不上很好,但至少是明显优于随机落子。等我空闲的时候,看看还能不能进一步优化下。

程序在: https://github.com/ruanqizhen/labview_book/tree/main/code/%E7%95%8C%E9%9D%A2%E8%AE%BE%E8%AE%A1/Xcontrol/Othello

查看或添加留言

DALL·E 绘图

· 阅读需 1 分钟

申请到了 DALL·E 的账号,赶紧玩了几下。DALL·E 是个人工智能绘图程序,可以根据输入的文字绘制图片。它的水平肯定比不上经过训练的画师,但是对于我这种没有绘图能力的人来说,可能会有些帮助,比如,以后制作文档时,也许可以让它帮忙绘制一些插图。

我先画了一幅程序猿自画像: (Photo of a chimpanzee sitting in front of a computer and programming)

儿子也画了一幅自己喜欢的图画: (Cartoon of apples, peaches, grapes and many kinds of fruits growing on one tree under a clear sky with the moon shining on the background)

查看或添加留言

福特皮卡为啥卖的那么好

· 阅读需 2 分钟

最近开车自驾游了一圈,总共开了4千公里,最远到了密西根州的北部。一路上明显感受到了不同地区居民对于汽车品牌的偏好是非常不同的。我居住的波士顿附近,日韩品牌的汽车是最常见的,其次欧洲品牌,美国本土品牌的占有率反而不高。等我到了密西根境内,日韩品牌的车居然成了极少数,路上看到的不是福特就是通用。

以前看统计数据,在美国市场,家用轿车卖的最多的是丰田家的;而皮卡卖的最多的确是福特f150。我当时还在想,为什么福特做轿车不行,皮卡却比丰田做得好?现在有点想明白了:皮卡的客户主要分布在美国中部以及农村地区,这些地区的消费者明显比城里那些更爱国,它们会首选美国品牌,于是成就了福特的皮卡。

查看或添加留言