【老万】从0开始学chatGPT(三):调节 AI 的性格
本文是我的 chatGPT 学习心得《从0开始学chatGPT》系列第三篇。欢迎依次阅读:
~~~~
今天聊一聊调节 chatGPT 性格这件事。
前两篇文章中,我们一起学习了 chatGPT 的基本原理,知道它本质上就是一个条件概率计算器,聊天时不断根据前文算出下一个记号的概率分布,再从候选记号中选取一个合适的,一个字一个字往外蹦。我们也用 Python 堆了一个小程序,统计罗大佑歌词中相邻汉字组合的频度,并以此为根据随机生成新的歌词。
我们这个“络打油”语言模型非常小(仅仅基于罗大佑的 104 首歌词),也没有用到神经网络,但和 chatGPT 的工作原理在最基本的层面上别无二致,都可以看作马尔可夫链的应用。只不过 chatGPT 的模型实现是靠一个 1750 亿参数大约 400 层的神经网络,训练用的语料基本上包括了人类迄今为止产生出来的全部的公开文字材料,烧了几千万美金才练成。
很多人都注意到,chatGPT 说的话不可不信也不可全信。别看它啥话题都接得住,唬得人类一愣一愣的,但因为没有知识库支撑和逻辑推理引擎加持,它学到的只是文字之间的相关性,说的话形式正确,但内容可能已经跑飞了。
chatGPT 说话不靠谱还有一个原因:它在产生文字的时候,并不是每次都选概率最高的那个,不时也会选一些冷门的字词,这样每次回答问题才会有所变化,不至于呆若木鸡。如果你看过 openAI 公司的 API 手册,就会发现它的那些大语言模型都提供了两个参数(temperature 和 top_p)来控制胡说八道的程度。这两个参数的功能从概念上讲是重叠的,这就给初学者造成了很大困扰:它们到底有什么区别?什么时候用哪一个?一起用是什么效果?
今天我们就来梳理一下这两个参数的正确用法,再在我们的络打油小语言模型上编程实现它们。纸上得来终觉浅,绝知此事要编程。等编过程序之后,对它们的理解自然会刻骨铭心。
~~~~
我们先看温度(temperature)参数。这是一个 >= 0 的实数,你可以把它想象成语言模型有多发烧。如果温度是 0,模型每次都挑最佳候选字,从不偏离。星际迷航(Star Trek)里有个总是绝对冷静从不说谎的理性直男 Spock,差不多就是他了。
By http://media.giantbomb.com/uploads/0/6843/528235-elrond_silver_shirt.jpg, Fair use, https://en.wikipedia.org/w/index.php?curid=22826291
如果温度调得太高,那就是喜剧无厘头之王,满嘴跑火车,多不着调的话都讲得出来。
温度参数的上限是多少?我没有找到可靠的说法,官方文档也语焉不详甚至前后矛盾,有说 1 的,有说 2 的,还有说无穷大的。openAI 网站有个试验场(playground)让大家试不同参数的效果,在那里温度被限制在 0 到 1 的区间,但这不代表 API 不能接受更大的值。
我编程试了一下不同温度的效果:
1.1:可以。
1.3:也可以,但是出结果比较慢,说着说着就变成连语法都不通的胡话了。
1.4:更慢,而且更早进入胡话模式。
1.5:系统等了很久,然后报错:“抱歉,服务器出错。请重试或者联系我们。”估计是计算量超过了系统允许的上限。
100:系统瞬间报错:“100 太大,上限是 2。”
看来 openAI 的模型拒绝比 2 更二。不过,我严重怀疑 2 不是一个不可逾越的理论上限,而是 openAI 为了保护自己不被滥用而设的一个人为上限。
如果我们想让生成的文本概率分布符合语料的概率分布,应该选多高的温度呢?我把这个问题扔给 chatGPT,它说是 1。听上去还算靠谱,姑且信了。
我们说温度越高越不着调,这当然是一种很笼统的说法。什么是冷静?什么是不着调?对于神经网络来说,这些概念并没有一个清楚的定义。为了准确理解这个参数到底在干什么,我们必须从算法层面去了解它如何影响 bot 选择答案。
我们知道,m 阶马尔可夫链模型可以根据前 m 个记号算出下一个记号的概率分布。我们上次实现的打油歌词生成器在选择答案的时候严格遵循这个分布。要改变 AI 胡说八道的程度,就不能按这个原始的概率分布选字,而是要把这个频率调整一下再用。
也就是说,给定温度 t,我们可以选一个函数 p[t],用它把一个候选字的原始概率 x 换算成一个新的概率 p[t](x)。比如,t=1 时我们不希望改变候选字的概率,所以 p[1] 应该是 identity 函数,p[1](x)=x。这个函数的图像是一条经过原点的直线段:
p[1](x) = x
因为函数 p[t] 是用来调整候选字的概率分布的,我把它叫做概率调整函数,简称概调函数。
严格地说,p[t](x) 不能直接拿去做概率,还得把结果规范化(normalize)一下,也就是把每个候选字的概率乘上一个常系数,保证它们加起来是 1。
把温度调高的本质是让机会向弱势群体倾斜:要是一个字的原始概率非常低,就给它大一点的相对涨幅。同时,为了防止原始概率低的字调整后概率反而更高的不合理现象,我们还要求 p[t](x) 是单调上升的(要是 x' > x,那么 p[t](x') > x)。
一种简单粗暴的做法是把概调函数改成不经过原点。这好比某次考试题难,全班考崩了,王老师一合计:挂一半的人太不好看了,传出去以后没人选我的课了,干脆按 x/2 + 50 算分吧!这样 100 分还是 100 分,但是 20 分就能及格。于是喜大普奔,大家都传老王真是好人呐!
老王的分数调整函数,对语言模型不适用
不过,这样做虽然可以让王老师重获同学们的欢心,在语言模型上却是不行的。因为它违背了一个原则:p[t](0) 应该是 0(0 进 0 出)。p[t](0) 如果不是 0,就会让本来不是候选字的也变成了候选字,写出来的文字不合语言习惯,谁都看不懂。
李老师班上也出现了集体考崩的情况,他也想放水,但老李的做法不同:开平方根乘 10,这样 100 分还是 100 分,0 分还是 0 分,但 36 分就及格了。于是,李老师也得到了学生的爱戴。
老李的分数调整函数,对语言模型可用
李老师的算法不但起到了照顾低分的作用,还满足了 0 进 0 出,可以用来调节语言模型的概率。
直观地说,温度参数是 1 的时候,概调函数是一段直线。要是温度往上升,我们就把线条向上拉一拉,让它向上凸起。
要是温度下降,选择的时候要更保守,尽量选原始概率高的,也就是说要形成马太效应:锦上添花,雪上加霜,让概率向优胜者更集中。表现在概调函数的图像上,就是把线条压得向下凹。
符合基本要求的调整曲线形状的方法有很多,我们可以选一个数学上面最简单优美的方法,在我看来那就是指数函数 p[t](x) = x^(1/t),把 x 变成 x 的 1/t 次方。它完美满足了我们对概调函数的要求:0 进 0 出,单调上升,而且 p[1](x) = x。
我们来看几个例子。要是 t 很小,比如 0.1,曲线是这样的(小的原始概率被进一步压得更小,生僻的选择变得更难被选中):
p[0.1](x) = x^10
要是 t 比 1 大,比如 2,曲线是这样(小的原始概率被拔高,生僻的选择变得更容易被选中):
p[2](x) = x^0.5
接下来我们就用 Python 编程实现温度参数,看能不能影响“络打油”的歌词风格。
还记得我们用下面这个 WeightedSample() 函数随机选取一个候选字吗?
def WeightedSample(freq_map):
total_weight = sum(freq_map.values())
r = random.random() * total_weight
start = 0
for x, weight in freq_map.items():
if start <= r and r < start + weight:
return x
start += weight
return ""
这个函数的输入是一个从候选字到频度的 map,输出是一个随机选中的候选字,而返回某个字的概率跟这个字的频度成正比。
如果要考虑温度,我们只需先用概调函数把频度表预处理一下再调用原来的 WeightedSample() 函数就行了:
def AdjustWeightByTemperature(freq_map, temperature):
if temperature < 0.01:
temperature = 0.01 # 防止指数太大溢出。
total_freq = sum(freq_map.values())
return {
ch: math.pow(freq / total_freq, 1 / temperature)
for ch, freq in freq_map.items()
}
def WeightedSampleWithTemperature(freq_map, temperature):
return WeightedSample(
AdjustWeightByTemperature(freq_map, temperature))
然后我们把歌词生成器调用 WeightedSample() 函数的地方改成调用WeightedSampleWithTemperature() 就好了。
试验一下不同温度下以“往事不堪回首”开头作词的结果如何。
t=0:
岁月不堪回首的心情的你总是不敢看看那变成了一条龙
我们曾经年
我们曾经年
我们曾经年
我们曾经年
......
因为每次都选概率最高的那个字,结果一成不变,甚至死循环了。
t=0.1:
岁月不堪回首的心情多少的风雨中的话
我们不要一个被科学习一些面孔后的玻璃窗前
我们曾经年
我们曾经年
我们曾经年轻时相逢
我们曾经年轻时为了自己的现在与我同的梦想
......
虽然还是墨守成规,有一定的几率能跳出循环。
t=0.4:
岁月不堪回首已经无所谓
是否还是他们的未来的假定
不如归我的梦想
我们不要一点真理
你的永远爱你
一样的我的手
......
不再说车轱辘话。
t=2:
岁月不堪回首的文章
古依
浮生难有一天它花岗有问题
母亲这群患难的王
仿佛又多么难
寒山与火焰的流星星
注定要冒牌成电影儿飞翔在江河的摆
......
思维是不是更跳跃了?
~~~~
接下来我们再来看另外一个参数“脱皮”(top_p)。和温度类似,top_p 也能控制生成文本的狂野程度,也是越大越狂野,但是它的取值必须在 0 到 1 之间。
top_p 的作用是:拿到候选字的原始概率分布后,先把这些字按概率从高到低排序,按顺序依次选取,选到总概率超过 top_p 值的时候即停止,剩下的候选字彻底放弃。你可以理解为把不重要的皮毛脱去,只留下最核心的候选字。
要是 top_p=0,就是脱得干干净净,只剩最高频的那一个字。
top_p=0.5,就是脱一半,只考虑总概率占 50% 的那些最高频的字。
top_p=1,就是什么都不脱,全部候选字都考虑。
举例说明。假定我们有五个候选字:
“我”,概率 0.40
“你”,概率 0.30
“他”,概率 0.15
“她”,概率 0.10
“它”,概率 0.05
如果 top_p=0.60,我们需要保留“我”和“你”做候选字。因为:
只留“我”是不够的(总概率 0.50,小于 0.60)。
“我”和“你”加起来概率达到 0.70,超过了 0.60。
把 top_p 翻译成“脱皮”,是不是声音和意思都到位了?
和温度不同,脱皮参数搞的是一刀切:低于某条线的候选字全部放弃,而不仅是降低其概率。
为什么有了温度还要脱皮?
如果只用温度控制,除非把温度降到 0,不能保证那些极生僻的字绝对不会被选中。而温度为 0 的后果是完全失去变化,在实践中通常并不可行。用脱皮参数可以彻底杜绝小概率结果出现。你也可以理解为它比用温度控制更能保证基本的底线。
要是你两个参数都指定,openAI 模型会先脱皮,再对剩下的候选字调温度。
下面我们就来编程实现一下脱皮。首先,用这个函数挑出被保留的候选字:
def PickTopP(freq_map, top_p):
# 按原始概率从高到低排序。
sorted_list = sorted(
freq_map.items(), key=lambda ch_and_freq: ch_and_freq[1], reverse=True
)
total_freq = sum(freq_map.values())
threshold = total_freq * top_p
answer = {}
accumulated_freq = 0
# 依次选取,直到达标。
for x, freq in sorted_list:
answer[x] = freq
accumulated_freq += freq
if accumulated_freq >= threshold:
return answer
return answer
然后用这个函数代替 WeightedSampleWithTemperature():
def WeightedSampleWithTemperatureTopP(
freq_map, temperature, top_p
):
return WeightedSampleWithTemperature(
PickTopP(freq_map, top_p), temperature
)
我们再来观察一下温度固定为 1 时不同脱皮参数对“络打油”行为的影响。
top_p = 0:
岁月不堪回首的心情的你总是不敢看看那天雨天雨天雨
天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨
天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨
天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨天雨
......
不出意外,这时“络打油”走不出命运的安排。
top_p = 0.1:
岁月不堪回首的梦想
不如我归去的时候别的迟疑
我们曾经年
我们曾经年
是否这么样吧
我们曾经年
......
和 t=0.1 差不多的车轱辘话,但仍有机会突破。
top_p = 1:
岁月不堪回首已注定这样的事
坚强再空枕边
闭上的镜子
爱人同小的寂寞的父母只有蝴蝶停
......
明显放得更开了。
~~~~
有人问要是这两个参数双管齐下是什么效果?
选择多了不一定是好事,它可能会让我们无所适从。我建议一般情况下不要两者都调。比如,要么把温度固定为 1 ,只调脱皮;要么把脱皮设成 1 ,只调温度。这两种情况下模型的行为都比较好理解,通常也能达到想要的目的。
如果只调节一个参数达不到理想效果,不妨先把温度固定为 1 调节脱皮,到比较满意时再固定脱皮,微调温度。这种方法没有什么严格的科学道理,只是一种经验。说到底神经网络目前很大程度还是一种玄学。
还有一种思路是让 AI 帮助我们调参数。比如,让很多人和不同参数的 chatGPT 聊天并给答案打分。根据这个结果我们可以训练另外一个神经网络:从温度和脱皮度预测用户喜爱度。训练完后,我们就可以给这个神经网络不同参数的组合做输入,从中找出预计会让用户喜爱度最高的一组。
今天我们一起学习了 openAI GPT API 最重要的两个控制参数“温度”和“脱皮”。下一次我们来谈谈怎么调用这个 API,让它模仿罗大佑的风格写歌。这个结果自然会比我们的三阶马尔可夫链强得多。敬请关注更新。
I’ll be back.
~~~~~~~~~~
猜你会喜欢:
阳光灿烂的日子 - 我的美国兄弟大强
浮云游子意,落日故人情 - 回忆大学导师陈老板
从莫斯科郊外开始的回忆 - 纪念我的同学 L
虚白高人静,桃李下自蹊 - 回忆中学校长胡虚白先生
我的科大之最后的晚餐 - 毕业季的故事
程序员护发秘籍 - 掌握这些工作技巧,包你不脱发
程序员的核心技能 - 以脱口秀的方式讲解程序员最重要的技能
~~~~~~~~~~
关注老万故事会公众号:
码字不易,呕心沥血只是希望更多人看到。如果喜欢这篇文章,请不吝订阅、转发、评论。谢谢!🙏