分类目录归档:人工智能

Python 教你3分钟用Bert搭建问答搜索引擎

 

鼎鼎大名的 Bert 算法相信大部分同学都听说过,它是Google推出的NLP领域“王炸级”预训练模型,其在NLP任务中刷新了多项记录,并取得state of the art的成绩。

但是有很多深度学习的新手发现BERT模型并不好搭建,上手难度很高,普通人可能要研究几天才能勉强搭建出一个模型。

没关系,今天我们介绍的这个模块,能让你在3分钟内基于BERT算法搭建一个问答搜索引擎。它就是 bert-as-service 项目。这个开源项目,能够让你基于多GPU机器快速搭建BERT服务(支持微调模型),并且能够让多个客户端并发使用。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install bert-serving-server  # 服务端
pip install bert-serving-client  # 客户端

请注意,服务端的版本要求:Python >= 3.5Tensorflow >= 1.10 。

此外还要下载预训练好的BERT模型,在 https://github.com/hanxiao/bert-as-service#install 上可以下载,如果你无法访问该网站,也可以在 bert-serving 模型及源代码 此处下载。

也可在Python实用宝典后台回复 bert-as-service 下载这些预训练好的模型。

下载完成后,将 zip 文件解压到某个文件夹中,例如 /tmp/uncased_L-24_H-1024_A-16/.

2.Bert-as-service 基本使用

安装完成后,输入以下命令启动BERT服务:

bert-serving-start -model_dir /tmp/uncased_L-24_H-1024_A-16/ -num_worker=4 

-num_worker=4 代表这将启动一个有四个worker的服务,意味着它最多可以处理四个并发请求。超过4个其他并发请求将在负载均衡器中排队等待处理。

下面显示了正确启动时服务器的样子:

使用客户端获取语句的编码

现在你可以简单地对句子进行编码,如下所示:

from bert_serving.client import BertClient
bc = BertClient()
bc.encode(['First do it', 'then do it right', 'then do it better'])

作为 BERT 的一个特性,你可以通过将它们与 |||(前后有空格)连接来获得一对句子的编码,例如

bc.encode(['First do it ||| then do it right'])

远程使用 BERT 服务

你还可以在一台 (GPU) 机器上启动服务并从另一台 (CPU) 机器上调用它,如下所示:

# on another CPU machine
from bert_serving.client import BertClient
bc = BertClient(ip='xx.xx.xx.xx')  # ip address of the GPU machine
bc.encode(['First do it', 'then do it right', 'then do it better'])

3.搭建问答搜索引擎

我们将通过 bert-as-service 从FAQ 列表中找到与用户输入的问题最相似的问题,并返回相应的答案。

FAQ列表你也可以在 Python实用宝典后台回复 bert-as-service 下载。

首先,加载所有问题,并显示统计数据:

prefix_q = '##### **Q:** '
with open('README.md') as fp:
    questions = [v.replace(prefix_q, '').strip() for v in fp if v.strip() and v.startswith(prefix_q)]
    print('%d questions loaded, avg. len of %d' % (len(questions), np.mean([len(d.split()) for d in questions])))
    # 33 questions loaded, avg. len of 9

一共有33个问题被加载,平均长度是9.

然后使用预训练好的模型:uncased_L-12_H-768_A-12 启动一个Bert服务:

bert-serving-start -num_worker=1 -model_dir=/data/cips/data/lab/data/model/uncased_L-12_H-768_A-12

接下来,将我们的问题编码为向量:

bc = BertClient(port=4000, port_out=4001)
doc_vecs = bc.encode(questions)

最后,我们准备好接收用户的查询,并对现有问题执行简单的“模糊”搜索。

为此,每次有新查询到来时,我们将其编码为向量并计算其点积 doc_vecs;然后对结果进行降序排序,返回前N个类似的问题:

while True:
    query = input('your question: ')
    query_vec = bc.encode([query])[0]
    # compute normalized dot product as score
    score = np.sum(query_vec * doc_vecs, axis=1) / np.linalg.norm(doc_vecs, axis=1)
    topk_idx = np.argsort(score)[::-1][:topk]
    for idx in topk_idx:
        print('> %s\t%s' % (score[idx], questions[idx]))

完成!现在运行代码并输入你的查询,看看这个搜索引擎如何处理模糊匹配:

完整代码如下,一共23行代码(在后台回复关键词也能下载):

import numpy as np
from bert_serving.client import BertClient
from termcolor import colored

prefix_q = '##### **Q:** '
topk = 5

with open('README.md') as fp:
    questions = [v.replace(prefix_q, '').strip() for v in fp if v.strip() and v.startswith(prefix_q)]
    print('%d questions loaded, avg. len of %d' % (len(questions), np.mean([len(d.split()) for d in questions])))

with BertClient(port=4000, port_out=4001) as bc:
    doc_vecs = bc.encode(questions)

    while True:
        query = input(colored('your question: ', 'green'))
        query_vec = bc.encode([query])[0]
        # compute normalized dot product as score
        score = np.sum(query_vec * doc_vecs, axis=1) / np.linalg.norm(doc_vecs, axis=1)
        topk_idx = np.argsort(score)[::-1][:topk]
        print('top %d questions similar to "%s"' % (topk, colored(query, 'green')))
        for idx in topk_idx:
            print('> %s\t%s' % (colored('%.1f' % score[idx], 'cyan'), colored(questions[idx], 'yellow')))

够简单吧?当然,这是一个基于预训练的Bert模型制造的一个简单QA搜索模型。

你还可以微调模型,让这个模型整体表现地更完美,你可以将自己的数据放到某个目录下,然后执行 run_classifier.py 对模型进行微调,比如这个例子:

https://github.com/google-research/bert#sentence-and-sentence-pair-classification-tasks

它还有许多别的用法,我们这里就不一一介绍了,大家可以前往官方文档学习:

https://github.com/hanxiao/bert-as-service

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

【AI试试水】关于美股Copart公司的走势预测

以下是二七阿尔量化的AI小机器人关于Copart公司的分析。

科帕特(Copart)是一家美国德克萨斯州达拉斯的在线车辆拍卖和再营销服务商,在北美、欧洲和中东都有业务。

[积极因素]:

  1. Copart最近的表现令人印象深刻,股价在2023年上涨了50%,显示出投资者对公司的强烈信心。
  2. 公司在2024财年第一季度实现了实质性的增长,收入和净利润大幅增长,表明公司正在扩大全球业务,并增加市场份额。
  3. 公司强大的财务指标,包括高每股息税前利润(EBIT)和正面的净利润率,表明公司盈利能力健康。
  4. 公司的高流动性,如快速比率所示,表明它有足够的资源来满足短期债务。

[潜在问题]:

  1. 公司高的负债与净资产比率和总负债与总资产比率表明存在大量债务,如果公司的现金流不足以支付利息费用,这可能成为一个问题。
  2. 公司高的存货周转率可能表明存货管理效率低下,可能导致更高的成本和较低的盈利能力。
  3. 公司高的应收账款周转率可能表明较长的收款周期,可能影响其流动性。

分析:基于积极的发展和潜在的担忧,我预测Copart(CPRT)的股价将在未来一周内上涨3-4%(从2023年11月17日到2023年11月24日)。公司强劲的财务表现和增长,以及高流动性,表明它是一个稳健的投资。然而,高负债水平和潜在的存货管理效率不足是可能影响公司盈利能力的潜在问题。

总体而言,公司最近的表现和未来的增长潜力表明它是一个强大的投资。然而,投资者应密切关注公司的债务水平和存货管理,以确保这些问题不会显现出来。在短期内,公司的股价预计将继续上升,受到强烈的市场情绪和公司基本面的推动。

 

OpenAI又一神器!Whisper 语音转文字手把手教程

语音转文字在许多不同领域都有着广泛的应用,其中一些应用与金钱相关。以下是一些例子:

1.字幕制作:语音转文字可以帮助视频制作者快速制作字幕,这在影视行业和网络视频领域非常重要。通过使用语音转文字工具,字幕制作者可以更快地生成字幕,从而缩短制作时间,节省人工成本,并提高制作效率。

2.法律文书:在法律领域,语音转文字可以帮助律师和律所将听证会、辩论和其他法律活动的录音转化为文字文档。这些文档可以用于研究、起草文件和法律分析等目的,从而提高工作效率。

3.医疗文档:医疗专业人员可以使用语音转文字技术来记录病人的医疗记录、手术记录和其他相关信息。这可以减少错误和遗漏,提高记录的准确性和完整性,为患者提供更好的医疗服务。

4.市场调查和分析:语音转文字可以帮助企业快速收集和分析消费者反馈、电话调查和市场研究结果等数据。这可以帮助企业更好地了解其目标受众和市场趋势,从而制定更有效的营销策略和商业计划。

总之,语音转文字技术在许多不同的行业和场景中都有着广泛的应用,可以提高工作效率、减少成本和错误,并为企业和个人带来更多商业价值。

语音转文字是一项重要的技术,但市场上大部分语音转文字工具存在诸多问题。很多人会遇到付费工具难用的情况,效果非常差。如果你需要高效而准确的语音转文字解决方案,你应该考虑使用Whisper。下面是whisper的一段转换示例:

https://pythondict-1252734158.file.myqcloud.com/home/www/pythondict/wp-content/uploads/2023/04/2023042306165455.wav
", ".join([i["text"] for i in result["segments"] if i is not None])
# Out[12]: '我赢了啊你说你看到没有没有这样没有减息啊我们后面是降息, 你不要去博这个东西, 我真是害怕你啊, 你不要去博不确定性, 是不是不确定性是我们的敌人, 听到没有朋友们, 好吧, 来朋友们, 你们的预约点好了啊, 朋友们, 你们的预约一定要给我点好了吧, 晚上八点钟是准时开播的, 朋友们关注点好了, 我们盘中视频见啊, 朋友们大家再见'

可以看到,即便是语速这么快的情况下,Whisper 依然实现了近乎完美的转换。

在接下来的教程中,我们将介绍如何使用Whisper来轻松地完成语音转文字任务。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install openai-whisper

此外你还需要安装ffmpeg:

安装ffmpeg

Mac (打开终端(Terminal), 用 homebrew 安装):

brew install ffmpeg --with-libvorbis --with-sdl2 --with-theora

Linux:

apt-get install ffmpeg libavcodec-extra

Windows:

1. 进入 http://ffmpeg.org/download.html#build-windows,点击 windows 对应的图标,进入下载界面点击 download 下载按钮,
2. 解压下载好的zip文件到指定目录
3. 将解压后的文件目录中 bin 目录(包含 ffmpeg.exe )添加进 path 环境变量中
4. DOS 命令行输入 ffmpeg -version, 出现以下界面说明安装完成:

2.使用Whisper进行语音转文字

简单的使用例子:

import whisper
whisper_model = whisper.load_model("large")
result = whisper_model.transcribe(r"C:\Users\win10\Downloads\test.wav")
print(", ".join([i["text"] for i in result["segments"] if i is not None]))

首先,我们建议使用Whisper的large-v2模型。根据我的实测结果,这个模型的表现非常优秀,它可以识别多种语言,包括中文,而且中文识别效果非常出色。在某些文字转换的场景中,它的表现甚至优于腾讯云、阿里云。

如果你无法下载到模型,可以用我们的模型镜像下载地址:https://pythondict.com/download/openai-whisper-large-v2/

使用前将模型文件放到指定位置:

Windows: C:\Users\你的用户名\.cache\whisper/large-v2.pt

Linux/MacOS: ~/.cache/whisper/large-v2.pt

然后重新运行程序即可得到转换结果。比如我们转换下面这个音频:

https://pythondict-1252734158.file.myqcloud.com/home/www/pythondict/wp-content/uploads/2023/04/2023042306165455.wav

效果如下:

import whisper
whisper_model = whisper.load_model("large")
result = whisper_model.transcribe(r"C:\Users\win10\Downloads\test.wav")
print(", ".join([i["text"] for i in result["segments"] if i is not None]))
# 我赢了啊你说你看到没有没有这样没有减息啊我们后面是降息, 你不要去博这个东西, 我真是害怕你啊, 你不要去博不确定性, 是不是不确定性是我们的敌人, 听到没有朋友们, 好吧, 来朋友们, 你们的预约点好了啊, 朋友们, 你们的预约一定要给我点好了吧, 晚上八点钟是准时开播的, 朋友们关注点好了, 我们盘中视频见啊, 朋友们大家再见

此外,不建议一次性转换长音频。如果你要转换长度很长的音频,建议先做切割并降低码率。

3.Whisper转换结果分析

Whisper的生成结果是一个字典:

{'text': '我赢了啊你说你看到没有没有这样没有减息啊我们后面是降息你不要去博这个东西我真是害怕你啊你不要去博不确定性是不是不确定性是我们的敌人听到没有朋友们好吧来朋友们你们的预约点好了啊朋友们你们的预约一定要给我点好了吧晚上八点钟是准时开播的朋友们关注点好了我们盘中视频见啊朋友们大家再见', 'segments': [{'id': 0, 'seek': 0, 'start': 0.0, 'end': 4.8, 'text': '我赢了啊你说你看到没有没有这样没有减息啊我们后面是降息', 'tokens': [50364, 1654, 5266, 95, 2289, 4905, 42405, 16529, 4511, 17944, 17944, 21209, 17944, 6336, 237, 26460, 4905, 15003, 13547, 8833, 1541, 47421, 26460, 50604], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 1, 'seek': 0, 'start': 4.8, 'end': 6.7, 'text': '你不要去博这个东西', 'tokens': [50604, 2166, 11962, 6734, 5322, 248, 15368, 38409, 16220, 50699], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 2, 'seek': 0, 'start': 6.7, 'end': 8.2, 'text': '我真是害怕你啊', 'tokens': [50699, 1654, 6303, 1541, 14694, 21164, 2166, 4905, 50774], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 3, 'seek': 0, 'start': 8.2, 'end': 10.9, 'text': '你不要去博不确定性', 'tokens': [50774, 2166, 11962, 6734, 5322, 248, 1960, 38114, 106, 12088, 21686, 50909], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 4, 'seek': 0, 'start': 10.9, 'end': 13.200000000000001, 'text': '是不是不确定性是我们的敌人', 'tokens': [50909, 23034, 1960, 38114, 106, 12088, 21686, 1541, 15003, 1546, 7017, 234, 4035, 51024], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 5, 'seek': 0, 'start': 13.200000000000001, 'end': 14.4, 'text': '听到没有朋友们', 'tokens': [51024, 31022, 4511, 17944, 19828, 9497, 51084], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 6, 'seek': 0, 'start': 14.4, 'end': 15.1, 'text': '好吧', 'tokens': [51084, 40221, 51119], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 7, 'seek': 0, 'start': 15.1, 'end': 15.6, 'text': '来朋友们', 'tokens': [51119, 6912, 19828, 9497, 51144], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 8, 'seek': 0, 'start': 15.6, 'end': 17.0, 'text': '你们的预约点好了啊', 'tokens': [51144, 29806, 1546, 12501, 226, 16853, 99, 12579, 12621, 4905, 51214], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 9, 'seek': 0, 'start': 17.0, 'end': 17.3, 'text': '朋友们', 'tokens': [51214, 19828, 9497, 51229], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 10, 'seek': 0, 'start': 17.3, 'end': 18.900000000000002, 'text': '你们的预约一定要给我点好了吧', 'tokens': [51229, 29806, 1546, 12501, 226, 16853, 99, 48161, 49076, 12579, 12621, 6062, 51309], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 11, 'seek': 0, 'start': 18.900000000000002, 'end': 21.0, 'text': '晚上八点钟是准时开播的', 'tokens': [51309, 50157, 33453, 12579, 50064, 1541, 6336, 228, 15729, 18937, 49993, 1546, 51414], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 12, 'seek': 0, 'start': 21.0, 'end': 22.6, 'text': '朋友们关注点好了', 'tokens': [51414, 19828, 9497, 28053, 26432, 12579, 12621, 51494], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 13, 'seek': 0, 'start': 22.6, 'end': 24.1, 'text': '我们盘中视频见啊', 'tokens': [51494, 15003, 5419, 246, 5975, 40656, 39752, 23813, 4905, 51569], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}, {'id': 14, 'seek': 0, 'start': 24.1, 'end': 25.400000000000002, 'text': '朋友们大家再见', 'tokens': [51569, 19828, 9497, 6868, 44176, 51634], 'temperature': 0.0, 'avg_logprob': -0.2088493855794271, 'compression_ratio': 1.649402390438247, 'no_speech_prob': 0.5881261825561523}], 'language': 'zh'}

text参数是没有做任何分词处理的纯语音原文本。

我们要重点关注的是segments参数。segments参数对音频内人物语言做了”分段”操作,比如这一段话:

{
  'id': 1,
  'seek': 0, 
  'start': 4.8, 
  'end': 6.7,
  'text': '你不要去博这个东西', 
  'tokens': [50604, 2166, 11962, 6734, 5322, 248, 15368, 38409, 16220, 50699],
  'temperature': 0.0, 
  'avg_logprob': -0.2088493855794271,
  'compression_ratio': 1.649402390438247, 
  'no_speech_prob': 0.5881261825561523
}

它就相当于人一样,去一帧帧校对每个词说话的时间:start是起始时间,end是结束时间。即”你不要去博这个东西”发生在音频的4.8秒到6.7秒之间。其他参数:

temperature 是指在语音转文本模型生成结果时,控制输出随机性和多样性的参数。

avg_logprob参数是语音转文字模型预测的置信度评分的平均值。

compression_ratio参数是指音频信号压缩的比率。

no_speech_prob参数是指模型在某段时间内检测到没有语音信号的概率。

重点在于如何应用。start和end参数你可以用来直接生成视频的字幕。大大提高生产效率。

置信度参数你可以用来提高识别准确率,如果说置信度一直不高,可以单独拎出来人工优化。

总之,Whisper的Large-v2模型绝对是目前中文语音转文字的顶级存在,有兴趣的朋友赶紧试试吧。

模型镜像下载地址:https://pythondict.com/download/openai-whisper-large-v2/

如果你存在算力计算或使用上的困难,也可以私信联系我帮你处理。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Wolfram: ChatGPT是怎么实现的?为什么它这么有效?

ChatGPT 能够自动生成类似于人类写作的文本,这一点非常引人注目,也令人意外。但它是如何实现的?为什么它能够如此出色地生成我们认为有意义的文本?我的目的是在这里概述ChatGPT内部的运行情况,并探讨它能够如此出色地产生有意义文本的原因。

首先需要解释的是,ChatGPT的基本目标是尝试产生一个“合理的延续”,无论它当前所拥有的文本是什么。这里的“合理”是指“在浏览了数十亿网页等人类书写的内容后,人们可能会写什么”。

那么假设我们有文本“AI的牛逼之处在于它能够…”,我们可以想象一下扫描数十亿页人类写作的文本(比如在网上和数字化的书籍中),找到所有这个文本的实例,然后看下一个单词出现的频率是多少。ChatGPT实际上是在类似地寻找在某种意义上“匹配”的内容,以生成一个排名列表,列出可能的后续单词和相应的“概率”。

ChatGPT写作时的一个显著特点是,它实际上只是一遍又一遍地问自己:“在当前文本的情况下,下一个词应该是什么?”然后每次添加一个单词。更准确地说,它添加的是一个“标记”,可能只是一个单词的一部分,这就是它有时可以“创造新词”的原因。

好的,在每一步中,ChatGPT都会得到一个带有概率的单词列表。但是它应该选择哪个单词来添加到正在撰写的文章(或其他文本)中呢?也许人们认为应该选择“排名最高”的单词(即被分配最高“概率”的单词)。但是这就是玄学开始的地方。因为某种原因(也许有一天我们会对此有科学式的理解),如果我们总是选择排名最高的单词,我们通常会得到一个非常“平淡”的文章,似乎从未“展现出任何创意”(有时甚至是逐字重复)。但是,如果有时(随机地)选择排名较低的单词,我们就会得到一个“更有趣”的文章。

这里有随机性,这意味着如果我们多次使用相同的问题提问,每次都可能得到不同的回答。同时,存在一个特定的所谓“temperature”参数,它决定了低排名单词被使用的频率。对于文章生成来说,实践中发现使用“temperature”为0.8最佳。(需要强调的是,这里没有运用任何“理论”;这只是经验上被发现的有效方法。)

在继续之前,我需要说明的是,出于阐述的目的,我通常不会使用ChatGPT中的完整系统,而是使用更简单的GPT-2系统。该系统有一个很好的特点,即其大小足够小,可以在标准台式计算机上运行。

例如,以下是获取上述概率表格的步骤。首先,我们需要检索底层的“语言模型”神经网络:

稍后,我们将会深入探讨这个神经网络,并讲解它是如何工作的。但现在,我们可以把这个“网络模型”视为黑匣子,应用到我们目前的文本中,并请求该模型认为应该跟随的前五个概率最高的单词:

这个步骤将结果转换为一个格式明确的“数据集”:

如果反复“应用模型”,每一步都添加具有最高概率的单词(在此代码中指定为模型的“决策”),则会发生以下情况:

如果继续下去会发生什么?在这种零“temperature”的情况下,输出很快就会变得混乱而重复。

如果不总是选择“顶部”单词,而是有时随机选择“非顶部”单词(其中“随机性”对应于 temperature=0.8),会发生什么呢?我们再次可以构建出文本:

每次执行时,会做出不同的随机选择,因此文本也会不同。以下是5个示例:

那么,如何计算下一个单词的概率?

好的,ChatGPT总是根据概率选择下一个单词。但是这些概率从哪里来呢?让我们从一个更简单的问题开始,考虑逐个字母(而不是单词)生成英语文本。如何确定每个字母的概率呢?

我们可以采取的最简单的方法就是从英文文本中随机取样,并计算不同字母的出现频率。例如,以下是在“猫”(Cat)维基百科文章中计算字母出现次数的结果:

这是狗(dog)维基百科文章中不同字母的出现次数:

结果是相似的,但并非完全相同(因为“o”在“dogs”文章中更常见,毕竟它出现在单词“dog”中)。不过,如果我们取样的英语文本足够大,最终我们可以期望得到相当一致的结果:

这是我们只是根据这些概率生成字母序列时得到的样本:

我们可以通过将空格模拟为字母添加进可选项中,从而将这些序列分为多个“单词”组成的序列:

我们可以通过让“单词长度”的分布与英语中的分布一致来更好地模拟“单词”的生成过程:

这里我们没有得到任何“实际单词”,但结果看起来稍微好了一点。但是,要进一步往下,我们需要做的不仅仅是随机选择每个字母。

例如,我们知道如果有一个“q”,下一个字母基本上必须是“u”。

这是字母本身的出现概率图示:

下面这张图展示了英语文本中一对字母(“2-grams”)的概率分布。图表的横轴为可能的第一个字母,纵轴为可能的第二个字母:

我们可以看到,例如,“q”列除了“u”行之外为空(概率为零)。好的,现在,我们不再一个字母一个字母地生成我们的“单词”,而是使用这些“2-gram”概率,一次生成两个字母来生成它们。以下是结果的一个样本 – 其中恰好包括一些真实存在的单词:

通过足够多的英文文本,我们不仅可以对单个字母或字母对(2-grams)的概率进行相当准确的估计,还可以对更长的字母序列进行估计。如果我们使用逐渐更长的n-gram概率生成“随机单词”,我们会发现它们逐渐变得“更真实”。

让我们现在假设 – 就像 ChatGPT 一样 – 我们处理的是整个单词,而不是单个字母。英语中大约有40,000个常用单词。通过查看大量的英语文本(例如几百万本书,总共几千亿个单词),我们可以估计每个单词的使用频率。利用这个估计,我们可以开始生成“句子”,其中每个单词都是独立随机选择的,其出现概率与在文本语料库中的出现概率相同。以下是我们得到的一个样本:

我们可以预料到,这样生成的句子毫无意义。那么我们应该如何改进呢?就像处理字母时一样,我们可以考虑不仅单个单词的概率,而是成对或更长的n-gram单词的概率。对于成对的情况,以下是5个例子,它们都以单词“cat”为起点:

这看起来稍微有点合理。如果我们使用足够长的n-gram,就有可能得到一个能够生成具有“正确整体文章概率”的单词序列,从而“得到一个ChatGPT”。但是问题在于,目前可用的英语文本量远远不足以推断出这些概率。在网络爬取的数百亿单词和数字化书籍中的数百亿单词中,对于40,000个常用单词,可能的2-grams数量就高达16亿,而3-grams的数量则达到60万亿。

因此,我们无法从现有的文本中估算出所有这些概率。即使是20个单词的“文章片段”,其可能性数量也已经超过了宇宙中的粒子数量,从某种意义上讲,这些可能性永远都不能全部写下来。

那么我们该怎么办呢?重要的想法是创建一个模型,使我们能够估计序列应该发生的概率,即使我们在我们查看的文本语料库中从未明确看到这些序列。而ChatGPT的核心正是一个被称为“大型语言模型”(LLM)的模型,它已经被构建成能够很好地估计这些概率的模型。

什么是模型?

假设你想知道(就像加利略在16世纪晚期那样)从比萨斜塔的每层落下的炮弹需要多长时间才能落到地面。那么,你可以在每种情况下进行测量并制作结果表。或者你可以做理论科学的本质:建立一个模型,给出一些计算答案的过程,而不是仅仅测量和记忆每种情况。

让我们想象我们有(有些理想化的)数据,了解炮弹从不同楼层掉落需要多长时间:

我们如何计算出从我们没有明确数据的楼层掉落所需的时间?在这种特定情况下,我们可以使用已知的物理定律来计算。但是假设我们只有数据,而不知道其背后的规律是什么。那么我们可以做出一个数学猜测,例如,也许我们应该使用一条直线作为模型:

我们可以选择不同的直线,但这条直线平均来看最接近我们所给的数据。从这条直线中,我们可以估计任何一层的落地时间。

我们如何知道在这里尝试使用一条直线呢?在某种程度上,我们并不确定。这只是数学上的一些简单东西,而且我们已经习惯了我们所测量的很多数据能够被数学上的简单事物很好地拟合。当然,我们也可以尝试使用数学上更复杂的东西,例如a + bx + cx^2,在这种情况下,我们可以得到更好的拟合效果:

事情有时会变得非常糟糕。就像在这里,我们尝试使用 a + b/x + c sin(x) 可以得到的最好结果:

永远不存在“model-less model”。您使用的任何模型都具有某些特定的基本结构,然后有一定数量的“旋钮可供调节”(即可以设置的参数)来拟合您的数据。在ChatGPT的情况下,使用了许多这样的“旋钮”——实际上有1750亿个。

值得注意的是,相比于3-grams的60万亿的组合可能性,ChatGPT 的这1750亿个参数,却已足够生成让我们惊讶的文本。

如何制作能完成人类任务的模型

我们上面给出的例子涉及到制作数值数据模型,这些数据基本上来自于简单的物理,是几个世纪以来我们已经知道的“简单数学”。但是对于ChatGPT,我们必须制作一种人类大脑产生的语言文本模型。对于这样的任务,我们还没有类似于“简单的数学”工具。那么,这个模型可能是什么样子的呢?

在讨论语言之前,我们先来谈谈另一个类似于人类任务:图像识别。作为一个简单的例子,我们考虑数字图像识别(这是一个经典的机器学习例子)。

首先,我们给每个数字收集一堆样本图像:

然后,为了找出我们输入的图像是否对应于特定的数字,我们可以使用样本图像进行像素逐个比较。但是作为人类,我们显然比像素逐个比较样本更擅长数字识别,因为即使数字是手写的,或者存在各种修改和扭曲,我们仍能够识别它们。

当我们上面为数值数据建立模型时,我们能够接收给定的数值x,然后为特定的a和b计算a+bx。那么,如果我们将这里每个像素的灰度值视为某个变量xi,是否有某个函数涉及所有这些变量,当我们对其进行求值时,可以告诉我们图像表示的是哪个数字?事实证明,可以构造这样的函数。但并不出人意料,它并不是特别简单。一个典型的例子可能涉及大约五十万次数学运算。

但最终结果是,如果我们将图像的像素值集合输入到此函数中,输出将是指定图像所代表的数字。稍后,我们将讨论如何构建这样的函数和神经网络的概念。但现在,让我们将该函数视为一个黑盒子,在其中输入手写数字图像(作为像素值数组),并输出它们所对应的数字:

但是这里真正发生了什么?假设我们逐渐模糊一个数字。一段时间内,我们的函数仍然可以“识别”它,例如这里被识别为“2”。但很快它就“失去了”,开始给出“错误”的结果:

但是,究竟发生了什么呢?假设我们逐渐模糊一个数字。在一段时间内,我们的函数仍然可以“识别”它,比如说是“2”。但很快,它就会“失效”,并开始给出“错误”的结果。

但是,我们为什么认为这是“错误”的结果呢?在这种情况下,我们知道我们通过模糊“2”来得到所有这些图像。但如果我们的目标是生成一个能够模拟人类识别图像能力的模型,那么真正需要问的问题是:如果向人类展示这些模糊的图像,而他们并不知道这些图像是如何产生的,那么人类会如何做出判断?

如果我们的函数得到的结果通常与人类的结果一致,那么我们就拥有了一个“好的模型”。而这个非常重要的科学事实是,对于像这样的图像识别任务,我们现在基本上知道如何构造这样的函数。

我们能否“数学证明”它们的有效性?嗯,不行。因为要做到这一点,我们必须拥有一个关于人类视觉感知的数学理论。我们拿“2”图像并改变几个像素。我们可能想象,即使只有几个像素“错位”,我们仍然应该认为这是一个“2”。但这该怎样才算合理呢?这是一个关于人类视觉感知的问题。是的,对于蜜蜂或章鱼,答案无疑会有所不同。

神经网络

那么,像图像识别这样的任务,我们的典型模型实际上是如何工作的呢?最流行且最成功的当前方法使用神经网络。神经网络的发明可以看作是大脑如何工作的简单理想化,而它们的形式与现在的使用方式非常接近。

人脑中大约有1000亿个神经元(神经细胞),每个神经元都能够每秒产生一千次的电脉冲。这些神经元以复杂的网络连接在一起,每个神经元都有树状的分支,使其能够向成千上万个其他神经元传递电信号。在粗略的近似中,任何给定神经元是否在某一时刻产生电脉冲取决于它从其他神经元接收到的脉冲,不同的连接会以不同的“权重”进行贡献。

当我们“看到图像”时,光子从图像中落在我们眼睛后面的(“光感受器”)细胞上,这些细胞会在神经细胞中产生电信号。这些神经细胞与其他神经细胞相连接,最终信号经过一系列神经元层次的传递。在这个过程中,我们“识别”图像,最终“形成思想”,认为我们“看到了一个2”(或许最终会像大声说“two”这样的动作)。

前一节中的“黑匣子”函数是这种神经网络的“数学化”版本。它恰好有11个层次(虽然只有4个“核心层”):

这个神经网络并没有什么特别“理论推导”的地方;它只是在1998年作为一项工程构建而成,并被发现可以工作。(当然,这与我们可能会描述我们的大脑是通过生物进化的过程产生的并没有太大的不同。)

好的,但是这样的神经网络如何“识别事物”呢?关键是”吸引子”的概念。想象一下我们有手写的1和2的图像:

我们希望所有的1“被吸引到一个地方”,所有的2“被吸引到另一个地方”。换句话说,如果一幅图像在某种程度上“更接近1”而不是2,我们希望它最终进入“1的位置”,反之亦然。

一个直观的类比,假设我们有平面上的某些位置,用点表示(在现实中,它们可能是咖啡店的位置)。然后我们可以想象,从平面上的任何一个点开始,我们总是想到达最近的点(也就是去最近的咖啡店)。我们可以通过将平面划分为由理想化的分隔区域来表示这一点:

所以我们可以将这看作是实现了一种“识别任务”,我们不是像识别给定图像最像哪个数字那样做某些事情,而是直接看哪个点最接近

那么如何使神经网络“执行识别任务”呢?让我们考虑这个非常简单的情况:

我们的目标是将与位置 {x,y} 相对应的“输入”识别为最接近的三个点之一。或者换句话说,我们希望神经网络计算关于 {x,y} 的函数,如下所示:

那么我们如何用神经网络实现这个任务呢?神经网络是由一系列理想化的“神经元”连接而成的,通常排列在层中。一个简单的例子是:

每个“神经元”实际上是建立在简单的数值函数上的。而要“使用”网络,我们只需在顶部输入数字(如我们的坐标 x 和 y),然后让每层神经元“评估其函数”,并将结果向前传递到网络的底部,最终产生最终结果:

在传统的(受生物学启发)设置中,每个神经元实际上都有来自前一层神经元的某些“传入连接”,每个连接都被分配一个特定的“权重”(可以是正数或负数)。一个给定神经元的值是由“前一个神经元”的值乘以它们对应的权重然后相加并加上一个常数,最后应用一个“阈值”(或“激活”)函数来确定的。在数学上,如果一个神经元有输入x = {x1,x2 …},那么我们计算f[w.x+b],其中权重w和常数b通常对于网络中的每个神经元选择不同;函数f通常是相同的。

计算 w.x + b 就是矩阵乘法和加法的问题。激活函数 f 引入了非线性,从而产生非平凡的行为。常见的激活函数有很多种,这里我们将使用 Ramp(或 ReLU)函数。

我们想要神经网络执行每个任务(或等价地,评估每个总体函数)时,我们将有不同的权重选择。 (正如我们稍后将讨论的那样,这些权重通常是通过使用机器学习从我们想要的输出示例来训练神经网络确定的。)

最终,每个神经网络都对应于某个总体数学函数,尽管它可能很混乱。 对于上面的示例,它将是:

ChatGPT的神经网络也仅仅对应着这样一个数学函数,但是实际上有数十亿个项式。

但是让我们回到单个神经元。以下是具有两个输入(表示坐标x和y)的神经元在各种权重和常量选择(以及Ramp作为激活函数)下可以计算的函数示例:

那么上面那个函数的表现如何呢?好吧,它是这样的:

这不是完全“正确”的,但它接近我们上面展示的“最近点”函数。

让我们看看其他一些神经网络会发生什么。在每种情况下,正如我们稍后将解释的那样,我们使用机器学习来找到最佳的权重选择。然后在这里展示神经网络使用这些权重计算出的结果:

较大的神经网络通常能更好地逼近我们的目标函数。如果要判断的目标在每个”吸引子”盆地的中心附近,我们通常可以获得我们想要的答案。但在边界处,神经网络“难以作出决策”,情况可能会更加混乱。

对于这种简单的数学式“识别任务”,正确答案是明确的。但在识别手写数字的问题上,情况并不那么明确。如果有人将“2”写得像“7”等等,怎么办呢?尽管如此,我们可以询问神经网络如何区分数字,这可以提供一些线索:

我们能“数学地”说明网络是如何进行区分的吗?实际上不行。它只是“按照神经网络的方式进行操作”。但事实证明,这通常与我们人类所做出的区分相当吻合。

让我们看一个更复杂的例子。假设我们有猫和狗的图像,并有一个被训练用于区分它们的神经网络。以下是它在一些示例上的表现:

现在,“正确答案”更加不明确了。那么,如果一只狗穿着猫装呢?等等。无论输入什么,神经网络都会生成一个答案。而且,事实证明,它以一种相当一致的方式生成答案,与人类可能做出的相符。正如我之前所说,这不是我们可以从第一原理中推导出来的事实。这只是在某些领域经验上被证明是正确的关键原因。但这也是神经网络有用的一个关键原因:它们以某种方式捕捉了“人类式”的做事方式。

以猫和狗的识别为例,现在“正确答案”更加不明确了。比如,一只穿着猫装的狗,该怎么识别呢?无论输入什么,神经网络都会生成一个答案。而且,事实证明,它以一种相当一致的方式生成答案,与人类可能做出的相符。正如我之前所说,这不是我们可以从第一原理中推导出来的事实。这只是在某些领域经验上被证明是正确的关键原因。但这也是神经网络有用的一个关键原因:它们以某种方式捕捉了“人类式”的做事方式。

假设你看到了一张猫的图片,你会问自己“为什么这是一只猫?”你可能会说:“我看到了它尖耳朵等特征。”但很难解释你是如何识别出这张图片的。这只是你的大脑以某种方式弄清楚了。但对于大脑来说,(至少目前)没有办法“进入内部”并查看其如何解决问题。那么对于(人工)神经网络呢?当你展示一张猫的图片时,每个“神经元”做的事情很容易看出来。但即使获得基本的可视化通常也非常困难。

在上面用于“最近点”问题的最终网络中有17个神经元。在用于识别手写数字的网络中有2190个神经元。在用于识别猫和狗的网络中有60,650个神经元。通常情况下,将60,650维空间可视化相当困难。但由于这是一个用于处理图像的网络,它的许多神经元层被组织成像素数组的形式。因此,我们可以拿一张典型的猫的图片为例进行说明:

那么,我们可以通过一系列派生图像来表示第一层神经元的状态——其中许多我们可以轻松解释为“没有背景的猫”或“猫的轮廓”之类的东西:

到了第10层,很难解释正在发生什么:

但总的来说,我们可能会说神经网络正在“挑选某些特征”(也许尖耳朵是其中之一),并使用它们来确定图像的内容。但这些特征是否具有像“尖耳朵”这样的名称?大多数情况下并不是。

我们的大脑是否也使用类似的特征?大多数情况下我们不知道。但值得注意的是,像我们在这里展示的神经网络的前几层似乎会挑选出图像的一些方面(如物体的边缘),这些方面似乎类似于我们知道的大脑视觉处理的第一层所挑选的方面。

但是假设我们想要一个神经网络的“猫识别理论”。我们可以说:“看,这个特定的网络可以做到”,这立即让我们对“它有多难”的问题有了一些感觉(例如需要多少个神经元或层来解决这个问题)。但至少目前我们没有办法“给出一个叙述性描述”来解释网络正在做什么。也许这是因为它确实是计算上不可化简的,没有一般的方法来找出它所做的事情,除非我们明确地跟踪每一步。或者也许只是因为我们还没有“找到科学的方法”,并确定了允许我们总结正在发生的事情的“自然法则”。

当我们谈论使用ChatGPT生成语言时,我们将遇到同样的问题。同样,目前尚不清楚是否有方法“总结它在做什么”。但是,语言的丰富性和细节(以及我们对它的经验)可能使我们比使用图像更进一步。

机器学习和神经网络的训练

到目前为止,我们讨论了已经“知道”如何执行特定任务的神经网络。但是神经网络如此有用(大脑也很可能如此),不仅可以原则上执行各种任务,而且可以逐步地从示例中进行“训练”以执行这些任务。

当我们制作一个神经网络来区分猫和狗时,我们不需要有效地编写一个程序,该程序明确地查找胡须; 相反,我们只展示了很多关于什么是猫和什么是狗的例子,然后让网络从中“机器学习”如何区分它们。

而且重要的是,训练后的网络从所显示的特定示例中“推广”。就像我们上面看到的那样,网络并不仅仅是识别其显示的示例猫图像的特定像素模式; 而是神经网络以某种方式成功地基于我们认为的某种“一般猫”的特征来区分图像。

那么神经网络训练的实际工作是如何进行的呢?基本上,我们一直在尝试找到使神经网络成功复制我们给出的示例的权重。然后我们依靠神经网络以一种“合理”的方式“插值”(或“推广”)这些示例之间。

让我们看一个比上面的最近点问题更简单的问题。让我们尝试让神经网络学习以下函数:

对于这个任务,我们需要一个只有一个输入和一个输出的神经网络,如下所示:

但是我们应该使用什么权重?对于每个可能的权重集合,神经网络都会计算一些函数。例如,以下是使用几组随机选择的权重计算出来的结果:

可以看到,基本没有符合我们图形的结果,那么我们到底如何找到能符合我们图形的权重?

基本思想是提供大量的“输入→输出”示例供神经网络进行“学习”,然后尝试找到能够重现这些示例的权重。下面是使用逐渐增加的示例进行训练的结果:

到目前为止,我们一直在谈论“已经知道”如何执行特定任务的神经网络。但神经网络如此有用的原因(在大脑中也是如此),不仅是因为它们原则上可以执行各种任务,而且还可以从示例中逐步“训练”以执行这些任务。

当我们制作一个神经网络来区分猫和狗时,我们不需要编写一个(比如)显式查找胡须的程序,而是展示许多关于什么是猫和什么是狗的示例,然后让网络从中“机器学习”如何区分它们。

关键是训练后的网络从它所展示的特定示例中“泛化”。正如我们上面所看到的,神经网络不仅仅识别它所展示的示例猫图像的特定像素模式;相反,神经网络以某种“一般的猫”的标准来区分图像。

那么神经网络的训练实际上是如何工作的呢?基本上,我们总是试图找到使神经网络成功重现我们所给出示例的权重。然后,我们依赖神经网络以“合理的”方式在这些示例之间“插值”(或“泛化”)。

让我们来看一个比上面最近点问题更简单的问题。让我们尝试让神经网络学习函数:

好的,我们要解释的最后一个关键点是如何调整权重以减小损失函数。正如我们所说,损失函数给出了我们获得的值和真实值之间的“距离”。但是“我们获得的值”是由当前版本的神经网络和其中的权重确定的。但是现在想象一下,权重是变量,比如wi。我们想找出如何调整这些变量的值以最小化依赖它们的损失。

例如,想象一下(极度简化了实际使用的典型神经网络),我们只有两个权重w1和w2。那么我们可能会有一个损失函数,它关于w1和w2的函数如下:

数值分析提供了各种技术来在这种情况下找到最小值。但是典型的方法就是沿着从之前的 w1, w2 开始的最陡峭的路径逐步前进:

就像水流下山一样,这个过程只能保证最终会到达曲面的某个局部最小值(“山上的湖泊”),但不一定能够到达最终的全局最小值。

在“权重空间”上找到最陡的下降路径并不容易。但是,微积分可以解决这个问题。正如前面提到的那样,我们可以将神经网络看作是计算一个依赖于其输入和权重的数学函数。现在考虑对这些权重进行微分。结果表明,微积分的链式法则让我们可以“展开”神经网络的逐层操作。结果是,至少在某种局部近似情况下,我们可以“反演”神经网络的操作,并逐步找到最小化与输出相关联的损失的权重。

上面的图片展示了我们可能需要在仅有2个权重的不现实的简单情况下进行的最小化操作。但是,实际证明,即使有更多的权重(ChatGPT使用了1750亿个),仍然可以进行最小化,至少可以进行一定程度的近似。事实上,“深度学习”在2011年左右的重大突破与发现有关,即在某种程度上,涉及许多权重时近似最小化可能比涉及较少权重时更容易。

换句话说,有点出人意料的是,用神经网络解决更复杂的问题可能比解决更简单的问题更容易。这种现象的粗略原因是,当有许多“权重变量”时,就有了高维空间,有“许多不同的方向”可以引导人到达最小值,而如果变量较少,则更容易陷入局部最小值(“山湖”),从中没有“摆脱的方向”。

值得指出的是,在典型情况下,有许多不同的权重集合可以给出基本相同性能的神经网络。通常,在实际的神经网络训练中,会有许多随机选择,导致出现“不同但等效的解决方案”,如下所示:

但是,每个这样的“不同解”都会有略微不同的行为。如果我们要求在我们提供训练示例的区域之外进行“外推”,我们可能会得到截然不同的结果:

(本文由公众号Python实用宝典翻译)

但是哪一个是“正确”的呢?实际上没有办法说。它们都“与观测数据一致”。但它们都对应于不同的“内在”方式来“思考”如何在“盒子外面”进行操作。有些可能对我们人类来说比其他的看起来更“合理”。

神经网络训练的实践

在过去的十年中,神经网络的训练艺术有了很多进展。而且,训练确实基本上是一门艺术。有时候-尤其是回顾时-人们可以看到至少一丝“科学解释”是为了做某事。但是大多数情况下,这些技巧和想法是通过试错发现的,逐步积累了许多有关如何使用神经网络的知识。

神经网络训练有几个关键部分。首先,有一个问题,即针对特定任务应该使用什么神经网络结构。然后,关键问题是如何获取用于训练神经网络的数据。越来越多的情况是,人们不再从头开始训练网络:新网络可以直接包含另一个已经训练好的网络,或者至少可以使用该网络为自己生成更多的训练样例。

可能会认为,对于每种特定的任务,都需要不同的神经网络结构。但是发现同一种结构似乎经常适用于看似完全不同的任务。在某种程度上,这让人想起了通用计算的概念,但是,正如我稍后将讨论的那样,我认为这更反映了我们通常试图让神经网络执行“类似人类”的任务的事实,而神经网络可以捕捉相当普遍的“类似人类过程”。

在神经网络的早期阶段,人们倾向于认为应该“尽量让神经网络做最少的事情”。例如,在将语音转换为文本时,认为应该先分析语音,将其分解成音素等。但是发现,对于“类似人类的任务”,通常最好只是尝试训练神经网络解决“端到端问题”,让它“发现”必要的中间特征、编码等。

还有一种想法是将复杂的单个组件引入到神经网络中,以让它实际上“明确实施特定的算法思想”。但是再次发现,这通常不值得;相反,最好只处理非常简单的组件,并让它们“自组织”(尽管通常以我们无法理解的方式)以实现等效的算法思想。

这并不是说没有适用于神经网络的“构造性思想”。例如,在图像处理的早期阶段,具有局部连接的神经元的2D阵列似乎非常有用。而且在“顺序回顾”的连接模式下集中处理看起来也很有用-正如我们稍后将在ChatGPT中处理人类语言中看到的那样。

但是神经网络的一个重要特征是,与计算机一样,它们最终只是处理数据。现有的神经网络-以及当前的神经网络训练方法-具体处理数字数组。但是在处理过程中,这些数组可以完全重组和重塑。例如,我们用于识别上述数字的网络从2D“类似图像”的数组开始,迅速“加厚”到许多通道,但然后“集中到”一个1D数组中,该数组最终包含表示不同可能输出数字的元素。

但是,如何确定一个特定任务需要多大的神经网络呢?这是一种艺术。在某种程度上,关键是知道“任务的难度”。但对于类似于人类的任务来说,通常很难估计。是的,可能有一种系统化的方法可以让计算机非常“机械化”地完成任务。但是很难知道是否存在可以让人们以更轻松的方式实现“类人级别”的技巧或捷径。可能需要列举一个巨大的博弈树来“机械化”地玩某个游戏;但是可能有一种更容易(“启发式”)实现“类人水平游戏”的方法。

当处理微小的神经网络和简单的任务时,有时可以明确地看到无法从这里到达目标。例如,以下是使用一些小型神经网络完成上一节任务的最佳结果:

而且我们可以看到,如果网络太小,它就无法重现我们想要的函数。但是在某个大小以上,它没有问题——至少如果训练足够长时间并且使用足够多的示例。顺便提一下,这些图片说明了神经网络传说中的一件事情:如果中间有一个“挤压”,强制所有东西都通过较少的中间神经元,通常可以用更小的网络来实现同样的效果。 (值得一提的是,“无中间层”或所谓的“感知器”网络只能学习基本上是线性的函数——但只要有一个中间层,理论上就总是可以任意好地逼近任何函数,至少如果有足够多的神经元,尽管为了使其可行地训练,通常需要一些正则化或规范化技巧。)

好的,假设我们已经选择了一种神经网络结构。现在的问题是如何获取用于训练网络的数据。许多关于神经网络和机器学习的实际挑战都集中在获取或准备必要的训练数据上。在许多情况下(“监督学习”),我们希望获取输入的明确示例以及预期的输出。例如,我们可能希望标记图像中包含的内容或某些其他属性。可能需要通过付出巨大的努力明确地进行打标签。但通常情况下,发现可以利用已经完成的事情作为训练集的标签,例如,可以使用Web上提供的alt标签来标记图像。或者在不同的领域中,可以使用为视频创建的字幕。或者对于语言翻译训练,可以使用存在于不同语言中的网页或其他文档的平行版本。

需要多少数据来训练神经网络以完成特定任务?同样地,从基本原理上估计很困难。通过使用“转移学习”/”预训练”将已经在另一个网络中学习的重要特征列表“转移进来”,可以大大减少要求。但是通常神经网络需要“看到很多例子”才能训练得好。至少对于某些任务而言,神经网络需要的示例可以是极其重复的。实际上,一种标准的策略就是反复地向神经网络展示所有的示例。在每个“训练轮次”中,神经网络都会处于至少稍微不同的状态,某种程度上“提醒它”特定的示例有助于它“记住那个示例”。(是的,也许这类似于人类记忆中重复的有用性。)

通常,只是反复重复相同的例子是不够的。还需要向神经网络展示示例的变化。神经网络的特征是,这些“数据增强”变化并不需要很复杂就可以发挥作用。只需使用基本图像处理轻微修改图像即可使其对神经网络训练基本“同样有效”。同样地,当用于自动驾驶汽车的实际视频等数据用尽时,可以继续在类似于模拟视频游戏的环境中运行模拟,并获得数据,而不需要所有实际场景的细节。

ChatGPT是如何训练的呢?它有一个很好的特点,它可以进行“无监督学习”,这使得训练样本的获取变得更加容易。回想一下,ChatGPT的基本任务是找出如何继续给定的一段文本。因此,要获取它的“训练样本”,我们只需获得一段文本,然后将其结尾部分遮盖,这就成为了“训练输入”——“输出”则是完整的、未被遮盖的文本。我们稍后会详细讨论这个问题,但主要的一点是:与学习图像内容所需的“显式标记”不同,ChatGPT实际上可以直接从它所得到的文本样本中进行学习。

好的,那么神经网络中的实际学习过程呢?归根结底,它都是要确定哪些权重能最好地捕捉到已给定的训练示例。而有各种各样的详细选择和“超参数设置”(因为权重可以被看作是“参数”)可以用来调整如何做到这一点。有不同的损失函数(平方和,绝对值和等)。有不同的损失最小化方法(每步在权重空间中移动多远等)。然后还有诸如每次显示多少个“批次”示例来获得每个连续估计的损失等问题。

但最终训练的整个过程可以通过观察损失的逐步减少来描述:

但通常可以看到损失会一直下降一段时间,但最终会在某个常数值上趋于平稳。如果该值足够小,则可以认为训练是成功的;否则,这可能表明应尝试更改网络结构。

人们能够判断“学习曲线”变平稳需要多长时间吗?像许多其他事情一样,似乎存在依赖于所使用的神经网络大小和数据量的近似幂律缩放关系。但总的结论是训练神经网络很难,并且需要大量的计算功率。作为实际问题,绝大部分工作量用于对数字数组进行操作,这是GPU擅长的领域。这就是为什么神经网络训练通常受GPU的可用性限制的原因。

未来会有更基本的方法来训练神经网络,或者说做神经网络的事情吗?我认为几乎肯定会有。神经网络的基本思想是利用大量简单(本质上相同)的组件创建一个灵活的“计算结构”,并让这个“结构”能够通过增量修改来从示例中学习。在当前的神经网络中,本质上使用了微积分的思想 – 应用于实数,以进行增量修改。但越来越清楚的是,拥有高精度数字并不重要;即使使用当前的方法,8位或更少的位数可能足够。

神经网络——也许有点像大脑——被设置为具有基本固定的神经元网络,其中被修改的是它们之间连接的强度(“权重”)。 (或许在至少年轻的大脑中,还有可能增加相当数量的全新连接)。但虽然这可能是生物学上的一个便利设置,但它显然离实现我们所需的功能最好的方式还有很远的路要走。而且可能会涉及相当于渐进网络重写的东西(也许类似于我们的物理项目),最终可能会更好。

但是,即使在现有神经网络的框架内,目前还存在一个关键限制:神经网络训练目前是根本顺序的,即每批示例的影响都被传播回来以更新权重。实际上,即使考虑了GPU,也是这样的计算机硬件的大部分时间都处于“闲置”状态,只有一部分在更新时才处于活动状态。从某种意义上说,这是因为我们当前的计算机倾向于具有与CPU(或GPU)分离的内存。但在大脑中,情况可能不同——每个“存储单元”(即神经元)也是一个潜在的活动计算元素。如果我们能够这样设计未来的计算机硬件,可能就可以更有效地进行训练。

“大的足够的神经网络一定可以做任何事情!”

像ChatGPT这样的东西的能力非常令人印象深刻,以至于人们可能会想象,如果能够不断训练更大的神经网络,那么它们最终将能够“做任何事情”。如果关心的是对人类思维直接可见的事物,这种可能性是相当大的。但是过去几百年科学的教训是,有些可以通过形式过程得出的东西,但对人类思维不易获得。

非平凡数学就是一个很好的例子。但是一般情况是计算。最终问题在于计算的不可约性现象。有些计算似乎需要很多步骤才能完成,但实际上可以“缩减”成非常直接的过程。但计算不可约性的发现意味着这种情况并不总是成立。相反,存在一些过程,可能像下面的过程一样,必须基本上追踪每个计算步骤才能弄清发生了什么:

我们通常使用大脑进行的事情可能是有选择地避免计算不可约的。人们需要特别的努力才能在脑中进行数学运算。在实践中,仅凭人脑“思考”非平凡程序操作的每个步骤几乎是不可能的。

但当然,我们有计算机。使用计算机,我们可以轻松地进行长时间的计算不可约的事情。关键是,这些通常没有捷径。

是的,我们可以记住许多特定的计算系统示例。或许我们甚至可以看到一些(“计算可约”)模式,从而能够进行一些泛化。但是,计算不可约意味着我们永远不能保证不会出现意外情况,只有通过显式计算才能了解每种特定情况的真实情况。

最终,学习和计算不可约之间存在根本性的张力。学习实际上是通过利用规律压缩数据。但计算不可约意味着最终规律性存在局限性。

作为实际问题,人们可以想象将小型计算设备(如元胞自动机或图灵机)构建到可训练系统(如神经网络)中。事实上,这样的设备可以作为神经网络的良好“工具”, 但是计算不可简约性意味着我们不能指望“深入”这些设备并使其学习。

换句话说,能力和可训练性之间存在根本的权衡:你越想让系统真正利用其计算能力,它就会越表现出计算不可简约性,就越难以训练。而它越是基本可训练,就越难以进行复杂的计算。

(对于当前的 ChatGPT,情况实际上要更加极端,因为用于生成每个输出令牌的神经网络是一个纯“前馈”网络,没有循环,因此无法使用非平凡的“控制流”进行任何计算。)

当然,人们可能会想知道是否重要能够进行不可简约的计算。事实上,在人类历史的大部分时间里,这并不特别重要。但是我们现代的技术世界是建立在利用至少数学计算(越来越多地包括更一般的计算)的工程学上的。如果我们看看自然界,它充满了不可简约的计算,我们正在慢慢理解如何模拟和利用它们用于我们的技术目的。

没错,神经网络当然可以注意到自然世界中的那些规律,这些规律我们也可以很容易地用“非辅助人类思维”注意到。但是,如果我们想解决数学或计算科学的问题,除非神经网络有效地“使用”一个“普通”的计算系统,否则它无法完成这些任务。

但是,所有这些都有一些潜在的混淆。过去有很多任务,包括写作文,我们认为计算机在根本上无法完成。现在我们看到像ChatGPT这样的东西完成了这些任务,我们倾向于突然认为计算机一定变得更加强大了——特别是超越了它们已经基本能够做到的事情(比如渐进式地计算细胞自动机等计算系统的行为)。

但这不是正确的结论。计算不可化简过程仍然是计算不可化简的,对于计算机来说仍然是基本困难的,即使计算机可以轻松计算它们的各个步骤。相反,我们应该得出结论:我们人类能够做到的任务(例如写作文),但我们认为计算机无法完成的任务,在某种意义上实际上比我们想象的更容易计算。

换句话说,神经网络之所以能够成功地写出一篇文章,是因为写文章的问题在某种意义上比我们想象的更为“浅显易懂”。从某种意义上说,这使我们更接近“拥有一个理论”,解释我们人类如何完成诸如写作等任务,或者如何处理语言。

如果你有一个足够大的神经网络,那么你可能能够做到人类能够轻松完成的任何事情。但你不会捕捉到自然界总体上所能做到的事情——或者我们从自然界中塑造出来的工具所能做到的事情。正是这些工具——实际上的和概念上的——使我们在近几个世纪内超越了“纯粹不受帮助的人类思维”所能接触到的边界,并为人类目的捕捉到更多存在于物理和计算宇宙中的东西。

嵌入层 Embeddings 概念

神经网络——至少目前的神经网络——基本上是基于数字的。因此,如果我们要使用它们来处理文本之类的内容,我们需要一种用数字表示文本的方法。当然,我们可以(就像ChatGPT那样)从字典中为每个单词分配一个数字。但有一个重要的想法——例如对于ChatGPT来说非常重要——超越了这一点。这就是“嵌入”(embedding)的概念。可以将嵌入看作是通过一组数字来尝试表示某物的“本质”,并具有“附近的事物”由相邻数字表示的属性。

例如,我们可以将单词嵌入看作是试图在“意义空间”中排列单词,使得在嵌入中彼此“含义相似”的单词彼此相邻。实际使用的嵌入(例如在ChatGPT中)通常涉及大量数字的列表。但是,如果我们将其投影到二维空间中,就可以展示单词如何通过嵌入进行排列:

没错,我们看到这样的嵌入在捕捉典型的日常印象方面表现得非常出色。但是我们如何构建这样的嵌入?粗略地说,这个想法是查看大量文本(这里是来自网络的50亿个单词),然后看不同单词出现在其中的“环境”有多相似。因此,例如,“alligator”(鳄鱼)和“crocodile”(鳄鱼的另一种写法)经常几乎可以互换地出现在其他类似的句子中,这意味着它们将在嵌入中附近放置。但是,“turnip”(萝卜)和“eagle”(老鹰)不会在其他类似的句子中出现,因此它们将在嵌入中远离放置。

但是,如何使用神经网络实际实现这样的嵌入呢?让我们先讨论不是单词而是图像的嵌入。我们希望找到一种方法,通过数字列表来表征图像,使得“我们认为相似的图像”被分配类似的数字列表。

我们如何判断是否应该“认为图像相似”?好吧,如果我们的图像是手写数字的图像,如果它们是相同的数字,我们可能会“认为两个图像相似”。早些时候我们讨论过一个神经网络,它被训练来识别手写数字。我们可以将这个神经网络看作是设置在其最终输出中将图像放入10个不同的箱中的神经网络,每个箱子代表一个数字。

在开始时,我们将实际图像馈送到第一层,由像素值的二维数组表示。最后——从最后一层我们得到一个包含10个值的数组,我们可以认为它表示网络对图像对应于数字0到9的“确定性”。

输入图像 :

那么最后一层神经元的值为:

但是如果我们再往前看一步呢?网络的最后一个操作是一个所谓的softmax函数,它试图”强制确定”。但在应用它之前,神经元的值是:

表示“4”的神经元仍然具有最高的数值。但是其他神经元的值中也包含了信息。我们可以预期,这个数字列表可以用某种方式来描述图像的“本质”,从而提供我们可以用作嵌入的东西。例如,这里的每个4都有略微不同的“特征嵌入”或“特征签名”,与8完全不同:

我们基本上是用10个数字来描述图像特征。但使用更多的数字通常更好。例如,在我们的数字识别网络中,我们可以通过接入前面的层获得一个包含500个数字的数组。这可能是一个合理的“图像嵌入”数组。

如果我们想要对手写数字的“图像空间”进行明确的可视化,我们需要通过将我们获得的500维向量投影到三维空间中来“减少维度”。

我们刚才讨论了如何创建一种图像的特征化(从而得到Embedding),基本上是通过确定(根据我们的训练集)它们是否对应于相同的手写数字来确定图像的相似性。如果我们有一个标识出每个图像是哪个常见对象(猫、狗、椅子等)的训练集,我们可以更普遍地为图像做同样的事情。通过这种方式,我们可以创建一个图像Embedding,该Embedding通过我们对常见对象的识别进行“锚定”,但然后根据神经网络的行为“泛化”。关键是,只要该行为与我们人类感知和解释图像的方式一致,这将最终成为一种“看起来正确”的嵌入,对于执行“类似于人类判断”的任务实际上是有用的。

那么我们如何遵循同样的方法来找到单词的Embedding呢?关键是从一个关于单词的任务开始,我们可以很容易地进行训练。标准的这样的任务是“单词预测”。想象一下,我们得到了“the ___ cat”。基于大量的文本语料库(比如网页的文本内容),可能有哪些单词“填空”呢?或者,给定“___ black ___”,不同“侧翼单词”的概率是什么?

我们如何为单词创建嵌入呢?关键是从一个关于单词的任务开始,这个任务可以很容易地进行训练。标准的这种任务是“单词预测”。想象一下,我们被给出“the ___ cat”。根据大量的文本语料库(比如网络内容),哪些单词“填空”最有可能呢?或者换句话说,给定“___ black ___”,不同“flanking words”的概率是多少?

我们如何为神经网络设置这个问题?最终,我们必须用数字来表达一切。一种方法是为英语中的每个大约50,000个常见单词分配一个唯一的数字。因此,“the”可能是914,“ cat”(前面有一个空格)可能是3542。(这些是GPT-2实际使用的数字。)因此,在“the ___ cat”问题中,我们的输入可能是{914, 3542}。输出应该是一个由大约50,000个数字组成的列表,这些数字有效地给出了每个可能的“填充”单词的概率。为了找到嵌入,我们要“截取”神经网络“达到结论之前”的“内部” – 然后获取发生在那里的数字列表,我们可以认为它们“描述每个单词”。

那么这些特征长什么样呢?在过去的10年中,已经开发出了一系列不同的系统(例如word2vec,GloVe,BERT,GPT等),每个系统都基于不同的神经网络方法。但最终,它们都采用了将单词用由几百到几千个数字组成的列表来描述的方法。

在它们的原始形式中,这些“Embedding向量”并不具备信息性。例如,这是 GPT-2 为三个特定单词生成的原始嵌入向量:

(本文由公众号Python实用宝典翻译)

如果我们进行像测量这些向量之间的距离这样的事情,那么我们可以找到单词的“接近程度”。稍后我们将更详细地讨论这种Embedding的“认知”意义。但现在主要的观点是,我们有一种有用的方式将单词转化为“神经网络友好”的数字集合。

但实际上我们可以进一步将单词序列或整个文本块用这种方式进行表征。而ChatGPT内部也是这样处理的。它会将其目前所拥有的文本转化为一个嵌入向量来表示它。然后它的目标是找到可能出现的不同单词的概率。它会将其答案表示为一系列数字,这些数字本质上给出了每个可能单词的概率,大约有50,000个左右。

(严格来说,ChatGPT不仅处理单词,还会处理类似于“词根”的东西,后文我们把这个东西叫做标记(token)。使用标记使ChatGPT更容易处理罕见的、复合的和非英语单词,并且有时可以创造新单词,有利有弊。)

ChatGPT的原理

好的,我们终于准备好讨论 ChatGPT 的内部了。实际上,它最终是一个巨大的神经网络,目前是一个带有 1750 亿个权重的所谓 GPT-3 网络版本。在许多方面,这是一个与我们讨论过的其他神经网络非常相似的神经网络。但它是一个特别为处理语言而设置的神经网络,其最显着的特征是一个名为“transformer”的神经网络架构。

在我们上面讨论的第一个神经网络中,每个神经元在任何给定层上基本上都与前一层的每个神经元(至少以某种权重)连接。但是,如果处理具有特定已知结构的数据,这种全连接的网络(大概)可能是过度的。因此,例如,在处理图像的早期阶段,通常使用所谓的卷积神经网络(“convnets”),其中神经元实际上排列在类似于图像中的像素的网格上,仅与网格上附近的神经元相连接。

transformer 的想法是对组成文本的标记序列做类似的事情。但是,transformer 并不仅仅是定义了一个在序列中可以存在连接的固定区域,而是引入了“注意力”的概念,并且有时“更多关注”序列的某些部分。也许有一天,只需启动通用神经网络并通过训练进行所有定制就会有意义。但是至少到目前为止,在实践中将事物“模块化”似乎至关重要,正如 transformers 所做的那样,也可能是我们的大脑所做的那样。

好的,那么ChatGPT(或者说它基于的GPT-3网络)到底是做什么的呢?回想一下,它的总体目标是根据它从训练数据(即来自网络等的数十亿页文本)中所看到的内容,合理地”继续”文本。因此,在任何给定时刻,它都有一定量的文本——它的目标是选择一个适当的下一个token来添加。

它分为三个基本阶段。首先,它获取与到目前为止的文本相对应的标记序列,并找到表示这些标记的embedding(即一组数字)。然后,它在这个embedding上进行操作——以“标准神经网络方式”,值会“涟漪”到网络的连续层中,以生成一个新的embedding(即一个新的数字数组)。然后,它取这个数组的最后一部分,并从中生成一个大约包含50,000个值的数组,这些值会转化为不同可能的下一个标记的概率。(是的,碰巧标记的数量与英语中常用单词的数量大致相同,尽管只有大约3000个标记是整个单词,其余都是片段。)

关键点在于,这个管道的每个部分都由神经网络实现,其权重是通过端到端训练网络确定的。换句话说,实际上除了整体架构外,没有任何部分是“明确设计”的;一切都是从训练数据中“学习”的。

然而,在架构设置方面有很多细节——反映了各种经验和神经网络知识。虽然这肯定会很深入细节,但我认为讨论一些这些细节是有用的,至少可以让人们了解构建ChatGPT这样的东西需要哪些内容。首先是嵌入模块。以下是GPT-2的一个草图表示:

(本文由公众号Python实用宝典翻译)

输入是一个由n个标记组成的向量(如前一节所述,表示为从1到约50,000的整数)。其中每个标记通过一个单层神经网络转换为一个嵌入向量(对于GPT-2长度为768,对于ChatGPT的GPT-3长度为12,288)。同时,还有一个“次要路径”,它获取标记的(整数)位置序列,并从这些整数创建另一个嵌入向量。最后,标记值和标记位置的嵌入向量被加在一起,以生成来自嵌入模块的最终嵌入向量序列。

为什么要将标记值和标记位置嵌入向量加在一起呢?我认为这并没有特定的科学依据。只是尝试了各种不同的方法,这种方法似乎是有效的。并且神经网络的传统认为,在某种程度上,只要设置大致正确,通常可以通过进行足够的训练来精细调整细节,而无需真正理解神经网络是如何配置自己的。

这是Embedding模块对字符串”hello hello hello hello hello hello hello hello hello hello bye bye bye bye bye bye bye bye bye bye”的操作:

好的,在Embdding模块之后是transformer的“主要事件”:所谓的“注意力块”序列(对于GPT-2为12个,对于ChatGPT的GPT-3为96个)。这一切相当复杂,让人想起典型的大型难以理解的工程系统或生物系统。但无论如何,这里是单个“注意力块”的原理图(适用于GPT-2):

在每个这样的注意力块中,都有一组“注意力头”(对于GPT-2来说是12个,对于ChatGPT的GPT-3来说是96个),每个头独立地操作嵌入向量中不同的值块。 (是的,我们不知道将嵌入向量拆分为若干部分的好处,或者不同部分的含义是什么;这只是那些“被发现有效”的东西之一。)

那么这些注意力头具体做什么呢?基本上它们是一种“回顾”标记序列的方式(即回顾到迄今为止已生成的文本),并将“过去的信息”以一种对查找下一个标记有用的方式打包起来。在上面的第一部分中,我们谈到了使用二元概率来选择基于其直接前驱的单词。Transformer中的“注意力”机制允许“注意力”甚至回到更早的单词,从而可能捕捉动词可以引用在句子中出现在它们前面的名词的方式,等等。

更详细地说,注意力头所做的是重新组合与不同标记相关联的嵌入向量的块,并赋予一定的权重。因此,例如,在第一个注意力块(GPT-2中)中的12个注意力头中,对于上述“hello,bye”字符串,具有以下(“回顾到标记序列开头”)“重新组合权重”的模式:

在通过注意力头处理后,结果的“重新加权嵌入向量”(GPT-2的长度为768,ChatGPT的GPT-3的长度为12,288)被传递到一个标准的“全连接”神经网络层。很难掌握这个层正在做什么。但是这里有一个使用它的768×768权重矩阵的绘图(这里是针对GPT-2):

通过64×64移动平均处理,一些(随机游走的)结构开始出现:

什么决定了这个结构?最终,这可能是人类语言特征的“神经网络编码”。但是,就目前而言,这些特征可能是相当未知的。实际上,我们正在“打开ChatGPT的大脑”(或者至少是GPT-2),发现了一些繁琐的内容,而我们并不理解,即使最终它产生了可识别的人类语言。

好的,在经过一个注意力块之后,我们得到了一个新的嵌入向量,然后连续地通过附加的注意块进行传递(GPT-2总共有12个;GPT-3有96个)。每个注意块都有自己特定的“关注”和“完全连接”权重模式。这是GPT-2的第一个注意头的“hello,bye”输入序列的注意权重序列:

以下是全连接层的(移动平均后的)“矩阵”:

有趣的是,即使在不同的attention block中,这些“权重矩阵”看起来相似,权重大小的分布也可能有所不同(而且并不总是高斯分布)。

那么,经过所有这些注意力块之后,转换器的净效应是什么呢?实质上,它将标记序列的原始嵌入集合转换为最终集合。 ChatGPT 的特定工作方式是选择此集合中的最后一个嵌入,并“解码”它以产生下一个标记应该是什么的概率列表。

这就是 ChatGPT 内部的概述。它可能看起来很复杂(尤其是因为它有许多不可避免的有点随意的“工程选择”),但实际上所涉及的最终元素非常简单。因为最终我们所处理的只是一个由“人造神经元”构成的神经网络,每个神经元都执行将一组数字输入与某些权重结合的简单操作。

ChatGPT的原始输入是一个数字数组,即迄今为止标记的嵌入(Embedding)向量。当ChatGPT“运行”以产生新的标记时,这些数字会“涟漪”通过神经网络的层,每个神经元“执行其任务”并将结果传递给下一层的神经元。没有循环或“回溯”。一切都只是通过网络“前馈”。

这与典型的计算系统(如图灵机)非常不同,在这种系统中,结果会被同一计算元素重复“重新处理”。在这里,至少在生成给定输出标记方面,每个计算元素(即神经元)仅使用一次。

但从某种程度上来说,即使在ChatGPT中,仍然存在一种在计算元素之间重复使用的“外部循环”。因为当ChatGPT准备生成一个新的标记时,它总是“读取”(即将其作为输入)在其之前出现的所有标记序列,包括ChatGPT自己先前“写入”的标记。我们可以认为这种设置意味着ChatGPT至少在其最外层级别上涉及一个“反馈循环”,尽管每次迭代都明确可见为在其生成的文本中出现的标记。

但让我们回到ChatGPT的核心:被重复使用以生成每个标记的神经网络。在某种程度上,它非常简单:是一整个相同人工神经元的集合。网络的某些部分只包含(“全连接”)神经元层,其中每个给定层上的神经元都与前一层上的每个神经元连接(具有一定的权重)。但特别是在其Transformer体系结构中,ChatGPT具有更多结构的部分,其中仅在不同层上的特定神经元相连接。(当然,仍然可以说“所有神经元都相连”——但有些只有零权重。)

此外,ChatGPT中的神经网络还有一些方面不太容易被认为只包含“同质”的层。例如,正如上面的标志性摘要所示,在attention block中,会有“复制多个副本”的地方,每个副本都会经过不同的“处理路径”,可能涉及不同数量的层,并在稍后重新组合。但虽然这可能是对正在发生的事情的一种方便的表示,但在原则上总是可以认为是“密集填充”层,只是有一些权重为零。

如果观察ChatGPT中最长的路径,涉及大约400个(核心)层-在某些方面并不是很多。但有数百万个神经元,共计1750亿个连接,因此有1750亿个权重。一个要意识到的事情是,每次ChatGPT生成一个新的标记时,都必须进行涉及到每个权重的计算。在实现上,这些计算可以通过高度并行的数组操作“按层”有序地组织在GPU上完成。但对于每个生成的标记,仍然必须执行1750亿个计算(最后还要多一点)-因此,毫不奇怪,使用ChatGPT生成一篇长文本可能需要一段时间。

但最终,值得注意的是,所有这些操作,尽管它们各自都很简单,但总能一起完成如此出色的“类人”文本生成工作。必须再次强调的是,(至少就我们目前所知),没有“最终的理论原因”说明为什么类似于ChatGPT的神经网络应该能够工作。实际上,正如我们将要讨论的,我认为我们必须将其视为一项潜在惊人的科学发现:在像ChatGPT这样的神经网络中,某种程度上可以捕捉到人类大脑在生成语言方面所能做到的本质。

ChatGPT的训练

好的,现在我们概述了ChatGPT被训练完成后的工作方式。但是它是如何训练的呢?所有那1750亿个神经网络权重是如何确定的?基本上,它们是通过基于人类撰写的大量文本语料库(网络、书籍等)进行的大规模训练得出的结果。正如我们所说,即使给定所有这些训练数据,神经网络能够成功地产生“类似人类”的文本也并不明显。并且,再次强调,似乎需要详细的工程细节才能实现这一点。但是,ChatGPT的惊喜和发现是,这是可能的。实际上,“只有”1750亿个权重的神经网络可以对人类写作的文本进行“合理的建模”。

ChatGPT训练的基本过程就像我们在上面简单的例子中讨论的那样。你提供一批例子,然后调整网络中的权重以最小化网络在这些例子上的误差(”损失”)。关于“反向传播”的主要昂贵之处在于,每次执行此操作时,网络中的每个权重通常都会至少微调一点,而要处理的权重数量就是非常庞大的。(实际的“反向计算”通常只比前向计算复杂一个小常数因子。)

通过现代 GPU 硬件,可以轻松并行计算数千个示例的结果。但是,当涉及到实际更新神经网络中的权重时,当前的方法需要基本上批量更新。 (是的,这可能是实际大脑目前至少在体系结构上具有优势的地方,因为它们具有结合计算和存储元件。)

在我们之前讨论过的学习数值函数的看似简单的情况下,我们发现我们通常需要使用数百万个示例来成功训练网络,至少是从头开始。那么,为了训练一个“类人语言”的模型,我们需要多少个示例呢?似乎没有任何根本的“理论”方法可以知道。但实际上,ChatGPT是在数千亿个单词的文本上成功训练的。

有些文本被多次输入,有些文本只被输入了一次。但是不知何故,它从它看到的文本中“得到了它所需要的”。但是,考虑到这么多文本要学习,它需要多大的网络才能“学得好”呢?同样,我们还没有一个根本的理论方法来回答这个问题。最终——正如我们将在下面进一步讨论的——人类语言及其通常使用的算法内容可能具有某种“总算法内容”。但是下一个问题是,神经网络在实现基于该算法内容的模型时有多高效。同样,我们不知道——尽管ChatGPT的成功表明它是相当有效的。

最终我们可以注意到,ChatGPT使用了几百亿个权重来完成它的任务,这个数量与它所接收的训练数据的单词(或标记)总数相当。在某些方面,这可能是令人惊讶的(尽管在ChatGPT的较小模型中也观察到了这一点),即“能够良好运行的网络规模”与“训练数据规模”是如此相似。毕竟,在ChatGPT内部并不是所有来自Web和图书等内容的文本都被“直接存储”。因为ChatGPT内部实际上包含了一堆数字——精度不到10个数字——它们是所有这些文本的聚合结构的某种分布式编码。

换句话说,我们可以问一下人类语言的“有效信息内容”是什么,以及通常使用它来表达什么。有原始的语言样本语料库,还有ChatGPT神经网络中的表示。这个表示很可能远不是“算法上最小化”的表示方式(正如我们将在下面讨论的那样)。但它是一个神经网络可以方便使用的表示方式。在这个表示中,训练数据似乎在最后很少被“压缩”;平均而言,基本上只需要不到一个神经网络权重来承载一个单词的“信息内容”。

当我们运行ChatGPT生成文本时,我们基本上需要使用每个权重一次。因此,如果有n个权重,我们需要做约n个计算步骤,尽管在实践中,许多计算可以在GPU中并行完成。但是,如果我们需要约n个单词的训练数据来设置这些权重,那么根据上面的说法,我们可以得出结论:我们需要约n^2个计算步骤来训练网络——这就是为什么用现有方法需要数十亿美元来进行训练的原因。

超越基本的训练

在训练ChatGPT中,大部分的工作量都花费在“向它展示”来自网络、书籍等大量的现有文本上。但事实证明,还有另一个看起来相当重要的部分。

完成原始文本语料库的训练后,ChatGPT即可从提示信息中生成自己的文本。尽管在许多情况下结果似乎合理,但特别是在生成较长的文本时,往往会出现偏离人类思维方式的情况。这种问题不是通过对文本进行传统的统计分析可以轻松检测到的,但读者却很容易注意到这一点。

ChatGPT的构建中的一个关键思想是,在“被动阅读”像网络这样的事物之后,有另一步是让实际人类与ChatGPT进行积极互动,查看它所生成的内容,并实际上对其进行反馈,告诉它“如何成为一个好的聊天机器人”。但神经网络如何利用这个反馈呢?第一步是让人类对神经网络生成的结果进行评分。然后建立另一个神经网络模型来尝试预测这些评分。但现在可以在原始网络上运行这个预测模型,实际上就像一个损失函数,从而允许该网络通过人类反馈进行“调整”。实践中的结果似乎对于ChatGPT在生成“类人”输出方面的成功有很大影响。

总的来说,有趣的是,“原始训练”网络似乎需要很少的“干预”即可使其有条不紊地朝着特定方向发展。人们可能认为,为了使网络表现得好像“学到了新东西”,需要进行训练算法、调整权重等操作。但事实并非如此。相反,基本上只需要告诉ChatGPT一次东西 – 作为您提供的提示的一部分 – 然后它就可以成功地在生成文本时利用您告诉它的内容。再次说明,这种方法的成功是我认为了解ChatGPT“真正做什么”以及它与人类语言和思维结构之间的关系的重要线索。

这其中肯定有某种类人的特点:至少在进行了所有这些预训练后,您只需告诉它一次东西,它就可以“记住” – 至少“足够长时间”以使用它生成一段文本。在这种情况下发生了什么?可能是“您可能告诉它的所有内容已经在某个地方了”,而您只是在引导它到正确的位置。但是这似乎不可行。相反,更可能的是,是的,这些元素已经在那里了,但具体细节是由类似于“这些元素之间的轨迹”的东西定义的,而这就是您在告诉它某些东西时所介绍的内容。

ChatGPT与人类一样,如果你告诉它一些奇怪和出乎意料的东西,完全不符合它所知道的框架,它似乎无法成功地“整合”它。只有当它基本上在已有的框架之上以相当简单的方式进行运行时,它才能够“整合”它。

再次指出的值得注意的是,神经网络的“捕捉”能力不可避免地有“算法限制”。告诉它形式为“这个东西对应那个”的“浅层规则”,神经网络很可能可以很好地表示和复制这些规则,事实上,它从语言中“已知的”将给它一个立即遵循的模式。但是,如果试图为涉及许多潜在的计算不可约简步骤的实际“深度”计算提供规则,它就行不通了。(请记住,在每一步中,它始终只是在其网络中“向前传递数据”,除了通过生成新的标记之外,从未循环。)

当然,网络可以学习特定的“不可约”计算的答案。但是,一旦存在组合数量的可能性,就不可能使用这种“表格查找式”的方法。因此,就像人类一样,现在神经网络需要“伸手”并使用实际的计算工具。

是什么让ChatGPT真正地工作起来

人类语言——以及产生语言的思维过程——一直被认为是复杂程度的巅峰。确实,人类的大脑只有“仅仅”1000亿左右的神经元(和可能达到100万亿的连接),就能够完成这项任务,这似乎相当不可思议。或许,人类大脑不仅仅是由神经元组成——也许还有一些尚未被发现的新物理层面。但现在我们有了 ChatGPT,这为我们提供了一个重要的新信息:我们知道了一个拥有与人类神经元数目相近的连接数的纯人工神经网络可以出乎意料地成功生成人类语言。

是的,这仍然是一个庞大而复杂的系统——其神经网络权重数目大约与目前世界上可用文本中的单词数目相当。但在某种程度上,似乎仍然难以相信所有语言的丰富性和它所能谈论的事物都可以被包含在这样一个有限的系统中。这背后的部分原因毫无疑问是 ubiquitous 现象,即即使基础规则很简单,计算过程也能在实际中大大放大系统的表象复杂性。但实际上,正如我们上面讨论的那样,ChatGPT 中使用的神经网络类型通常是有针对性的构造,以限制这种现象的影响,以及与之相关的计算不可约简性,以便使其训练更易于理解。

那么,这些规律会是什么样子呢?它们最终必须给我们提供一些关于语言及其表达方式的指导。稍后我们将讨论如何“深入了解ChatGPT”可能会为我们提供一些线索,以及从构建计算语言所知道的内容如何为我们指明一条前进的道路。但首先,让我们讨论两个长期以来已知的“语言规律”的例子,以及它们与ChatGPT的操作方式的关系。

第一个例子是语言的语法。语言不仅仅是随意组合的单词。相反,对于不同种类的单词,有(相当)明确的语法规则:例如,在英语中,名词可以由形容词前置并由动词后置,但通常两个名词不能紧挨在一起。这样的语法结构可以(至少近似地)通过定义如何组合“解析树”的规则来捕捉到:

ChatGPT 没有任何明确的对这些规则的“知识”。但不知何故,在其训练中,它隐式地“发现”了它们,然后似乎擅长遵循它们。那么这是如何工作的呢?在“大局”层面上,这还不清楚。但是,为了获得一些洞见,也许看一个更简单的例子会有帮助。

考虑一个由序列构成的“语言”,其语法规定括号始终应该是平衡的,如下面所示的解析树所代表的那样:

我们能训练一个神经网络生成“符合语法”的括号序列吗?神经网络中处理序列的方法有很多种,但我们可以使用转换器网络(transformer nets),就像ChatGPT一样。我们可以将规范的括号序列作为训练样本来训练一个简单的转换器网络。一个微妙之处(实际上也出现在ChatGPT生成人类语言的过程中)是,除了我们的“内容标记”(这里是“(”和“)”)之外,我们还必须包含一个“结束”标记,以表示输出不应继续下去(即对于ChatGPT,已经到达“故事的结尾”)。

如果我们只设置一个具有8个头和长度为128的特征向量的注意力块的转换器网络(ChatGPT也使用长度为128的特征向量,但具有96个注意力块,每个块具有96个头),那么似乎不可能使其学习到关于括号语言的很多知识。但是当我们使用2个注意力块时,学习过程似乎会收敛——至少在提供了大约1000万个样本之后会如此(并且,像转换器网络一样,提供更多的样本只会降低其性能)。

因此,我们可以用这个网络做类似于ChatGPT的事情,并询问下一个标记应该是什么的概率——在括号序列中:

(本文由公众号Python实用宝典翻译)

在第一个案例中,神经网络“相当确定”这个序列不能在这里结束,这很好,因为如果它结束了,括号就会不平衡。然而,在第二个案例中,它“正确地识别”到序列可以在这里结束,尽管它还指出“可以重新开始”,放下一个“(”,接着是“)”。但是,糟糕的是,即使是用它训练了约400,000个繁琐的权重,它也说有15%的概率将“)”作为下一个标记,这是不正确的,因为那必然会导致不平衡的括号。

如果我们询问网络逐渐增加( 的序列的最高概率完成情况,我们将得到以下结果:

这就意味着像ChatGPT和英语这样的语言的语法有什么含义呢?括号语言很“简单”,更像是一种“算法故事”。但在英语中,基于单词和其他提示的局部选择,我们很有可能“猜测”什么在语法上是匹配的。是的,神经网络在这方面表现得更好,尽管它也可能会漏掉一些“形式上正确”的情况,但这可能与人类的漏洞相似。

但主要观点是,语言的整体语法结构——及其所涉及的规则性——在某种程度上限制了神经网络需要学习的“范围”。而关键的“自然科学式”的观察是,像ChatGPT中的神经网络这样的 Transformer 体系结构似乎成功地学习了类似于嵌套树形的句法结构,这种结构似乎在所有人类语言中都存在(至少近似如此)。

语法提供了一种对语言的限制,但显然还有其他的限制。像“nquisitive electrons eat blue theories for fish”(探究性电子吃蓝色理论以换取鱼) 这样的句子在语法上是正确的,但并不是我们通常会说的,如果ChatGPT生成了这样的句子,也不会被认为是成功的——因为,嗯,就是用其中的单词的正常含义而言,它基本上是没有意义的。

但是有没有一种通用的方法来判断一个句子是否有意义呢?传统上没有一个总体的理论。但可以认为,在被训练了数十亿个(可能有意义的)句子后,ChatGPT在隐含地“发展了一个理论”

这个理论会是什么样子呢?有一个微小的角落基本上已经被知道了两千年,那就是逻辑。尤其是在亚里士多德发现的三段论形式中,逻辑基本上是一种说法,即遵循某些模式的句子是合理的,而其他句子则不是。因此,例如,“所有X都是Y。这个不是Y,所以它不是X”是合理的(如“所有的鱼都是蓝色的。这个不是蓝色的,所以它不是一条鱼。”)。就像可以有些任性地想象亚里士多德通过大量的修辞例子(“机器学习式”)发现三段论逻辑一样,人们也可以想象在 ChatGPT 的训练中,它将能够通过查看网络上的大量文本等来“发现三段论逻辑”。(是的,虽然可以因此期望 ChatGPT 产生包含基于三段论逻辑等的“正确推理”的文本,但当涉及到更复杂的形式逻辑时,情况就大不相同了——我认为人们可以期望它因与括号匹配相同的原因而失败。)

但除了逻辑的狭窄例子之外,还有什么可以系统地构建(或识别)即使是合理的有意义文本的方法呢?是的,有像 Mad Libs 这样使用非常具体的“短语模板”的东西。但不知何故,ChatGPT 无形中有了一个更普遍的方法来做到这一点。也许除了“当你有 1750 亿个神经网络权重时,它就会发生”之外,我们无法说出它如何做到。但我强烈怀疑,这背后存在一个更简单、更有力的故事。

意义空间和语义动力学

我们上面讨论了在ChatGPT内部,任何一段文本实际上都由一组数字表示,我们可以将其视为某种“语言特征空间”中的点的坐标。因此,当ChatGPT继续一段文本时,这相当于在语言特征空间中跟踪轨迹。但现在我们可以问,是什么让这条轨迹对应于我们认为有意义的文本?也许可能存在某种“语义运动规律”,定义或者至少限制了语言特征空间中的点如何移动,同时保持“有意义性”?

那么这个语言特征空间是什么样的?以下是一个示例,展示了单个单词(这里是普通名词)在我们将这样的特征空间投影到二维平面时的分布情况:

以下是不同词类的单词在特征空间中的布局方式:

当然,通常来说,一个词并不只有“一个意思”(也不一定只对应一种词性)。通过查看包含一个单词的句子在特征空间中的排布,我们通常可以“区分”不同的含义,就像这里的“crane”一词(是指鸟还是机器)的例子一样:

好的,我们可以将这个特征空间想象为将“语义相似的词”放置在附近的空间。但是,我们能够在这个空间中识别出什么样的额外结构呢?例如,是否有一种“平行传送”的概念,反映了空间的“平坦性”?研究类比问题可能有助于理解这一点:

那么,轨迹呢?我们可以观察ChatGPT在特征空间中跟随的提示轨迹,然后再看看它如何继续下去:

这里显然没有什么“几何明显”的运动规律。这一点并不令人惊讶;我们完全期望这将是一个相当复杂的故事。例如,即使存在“语义运动定律”,什么样的嵌入(或者实际上是什么“变量”)最自然也远非显而易见。

在上面的图片中,我们展示了“轨迹”的几个步骤,其中每个步骤我们选择了ChatGPT认为最有可能的单词(“temperature=0”情况)。但我们还可以问,在给定点时下一个可能出现的单词是什么,以及它们的概率是多少:

在这个案例中,我们可以看到一个在特征空间中有着相对明确方向的高概率词汇的“扇形”分布。如果我们继续下去会发生什么呢?下面是随着轨迹的“移动”而出现的连续的“扇形”:

这里是一个总共有40步的三维展示:

上面这个表现看起来有些混乱,没有特别鼓励这种想法,即通过实证研究“ChatGPT内部的运作”来确定“数学物理一样的”“语义运动定律”。但也许我们只是在看“错误的变量”(或错误的坐标系),如果我们只看正确的变量,我们就会立即看到ChatGPT正在做一些“数学物理简单”的事情,比如遵循测地线。但是,目前为止,我们还没有准备好从其“内部行为”“实证解码”ChatGPT已经“发现”的有关人类语言“组装”的内容。

总结

ChatGPT的基本概念在某种程度上相当简单。从网络、书籍等来源中获取大量人类创造的文本样本,然后训练神经网络生成“类似”的文本。特别地,使它能够从“提示”开始,然后继续生成“与它所受过的训练相似”的文本。

正如我们所见,ChatGPT中的实际神经网络由非常简单的元素组成。神经网络的基本操作也非常简单,本质上是对每个新单词(或单词的一部分)生成文本时,对其已生成的文本产生的输入“通过其元素”(没有任何循环等)进行一次传递。

但是,这个过程的卓越而意想不到的一点是,它可以产生成功“类似”网络、书籍等中存在的文本。它不仅是连贯的人类语言,而且“说出”了“遵循提示”的事情,利用它所“读取”的内容。它并不总是说出“全局上有意义”的话(或与正确的计算相符),它只是根据它训练材料中的内容“听起来对”的话。

ChatGPT的具体工程设计使其相当令人信服。但是,至少在它可以使用外部工具之前,它“仅仅”是从其积累的“传统智慧的统计数据”中提取出一些“连贯的文本线索”。但是,其结果与人类产生的非常相似,这很令人惊奇。正如我所讨论的,这表明某些东西至少在科学上非常重要:人类语言(及其背后的思维模式)在其结构上比我们想象的更简单、更“法则化”。ChatGPT已经隐式地发现了这一点。但我们可以通过语义语法、计算语言等,可能会明确地暴露它。

ChatGPT在生成文本方面所做的工作非常令人印象深刻,其结果通常与我们人类产生的非常相似。那么,这是否意味着ChatGPT像大脑一样工作?其基本的人工神经网络结构最终是以大脑的理想化模型为基础建模的。而且很可能当我们人类生成语言时,许多方面的情况非常相似。

但是,值得注意的是,尽管 ChatGPT 在生成文本方面表现出人类的能力,但其实现方式与大脑有所不同。与人类的大脑和当前计算机的“硬件”有所不同,ChatGPT 不得不采用一种可能非常不同(在某些方面更少效率)的策略进行训练(也就是学习)。此外,还有一点:与典型算法计算中的情况不同,ChatGPT 没有内部的“循环”或“重新计算数据”。这必然限制了它的计算能力,甚至对于当前的计算机也是如此,但对于大脑来说,更是如此。

但就目前而言,看到 ChatGPT 已经能够做到的事情令人兴奋。从某种程度上说,它是基本科学事实的一个很好的例子,即大量的简单计算元素可以做出引人注目和出乎意料的事情。但它也提供了我们在两千年里最好的推动力,去更好地理解人类语言以及其背后的思维过程的基本特征和原则。

总之,ChatGPT 是一种极其强大的自然语言生成系统,它的训练过程基于大量的人类语言文本样本,并采用了深度学习技术。尽管其基本结构与人类大脑的工作方式有所不同,但它已经展示了大量简单计算元素的组合可以产生出令人惊讶的结果。这为我们深入理解人类语言和思维模式的本质提供了有力的推动。

本文翻译自:https://writings.stephenwolfram.com/2023/02/what-is-chatgpt-doing-and-why-does-it-work

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

教你一行代码自动绘制艺术画 (Discoart)

DiscoArt 是一个很牛逼的开源模块,它能根据你给定的关键词自动绘画。

绘制过程是完全可见的,你可以在 jupyter 页面上看见这个绘制的过程:

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install discoart

为了运行 Discoart, 你需要Python 3.7+ 和支持 CUDA 的 PyTorch.

2.开始使用 Discoart

你可以在Jupyter中运行Discoart,这样能方便地实时展示绘制过程:

from discoart import create

da = create()

这样将使用默认的 文本描述 和参数创建图像:

text_prompts:
  - A beautiful painting of a singular lighthouse, shining its light across a tumultuous sea of blood by greg rutkowski and thomas kinkade, Trending on artstation.
  - yellow color scheme

init_image:
width_height: [ 1280, 768 ]

skip_steps: 0
steps: 250

init_scale: 1000
clip_guidance_scale: 5000

tv_scale: 0
range_scale: 150
sat_scale: 0
cutn_batches: 4

diffusion_model: 512x512_diffusion_uncond_finetune_008100
use_secondary_model: True
diffusion_sampling_mode: ddim

perlin_init: False
perlin_mode: mixed
seed:
eta: 0.8
clamp_grad: True
clamp_max: 0.05

randomize_class: True
clip_denoised: False
rand_mag: 0.05

cut_overview: "[12]*400+[4]*600"
cut_innercut: "[4]*400+[12]*600"
cut_icgray_p: "[0.2]*400+[0]*600"
cut_ic_pow: 1.

save_rate: 20
gif_fps: 20
gif_size_ratio: 0.5
n_batches: 4
batch_size: 1
batch_name:
clip_models:
  - ViT-B-32::openai
  - ViT-B-16::openai
  - RN50::openai
clip_models_schedules:

use_vertical_symmetry: False
use_horizontal_symmetry: False
transformation_percent: [0.09]

on_misspelled_token: ignore
diffusion_model_config:
cut_schedules_group:
name_docarray:
skip_event:
stop_event:
text_clip_on_cpu: False
truncate_overlength_prompt: False
image_output: True
visualize_cuts: False
display_rate: 1

创建出来的就是这个图:

Create 支持的所有参数如下:

text_prompts:
  - A beautiful painting of a singular lighthouse, shining its light across a tumultuous sea of blood by greg rutkowski and thomas kinkade, Trending on artstation.
  - yellow color scheme

init_image:
width_height: [ 1280, 768 ]

skip_steps: 0
steps: 250

init_scale: 1000
clip_guidance_scale: 5000

tv_scale: 0
range_scale: 150
sat_scale: 0
cutn_batches: 4

diffusion_model: 512x512_diffusion_uncond_finetune_008100
use_secondary_model: True
diffusion_sampling_mode: ddim

perlin_init: False
perlin_mode: mixed
seed:
eta: 0.8
clamp_grad: True
clamp_max: 0.05

randomize_class: True
clip_denoised: False
rand_mag: 0.05

cut_overview: "[12]*400+[4]*600"
cut_innercut: "[4]*400+[12]*600"
cut_icgray_p: "[0.2]*400+[0]*600"
cut_ic_pow: 1.

save_rate: 20
gif_fps: 20
gif_size_ratio: 0.5
n_batches: 4
batch_size: 1
batch_name:
clip_models:
  - ViT-B-32::openai
  - ViT-B-16::openai
  - RN50::openai
clip_models_schedules:

use_vertical_symmetry: False
use_horizontal_symmetry: False
transformation_percent: [0.09]

on_misspelled_token: ignore
diffusion_model_config:
cut_schedules_group:
name_docarray:
skip_event:
stop_event:
text_clip_on_cpu: False
truncate_overlength_prompt: False
image_output: True
visualize_cuts: False
display_rate: 1

你可以这么使用:

from discoart import create

da = create(
    text_prompts='A painting of sea cliffs in a tumultuous storm, Trending on ArtStation.',
    init_image='https://d2vyhzeko0lke5.cloudfront.net/2f4f6dfa5a05e078469ebe57e77b72f0.png',
    skip_steps=100,
)

如果你不是用jupyter运行的,你也可以看到中间结果,因为最终结果和中间结果都会被创建在当前工作目录下,即

./{name-docarray}/{i}-done.png
./{name-docarray}/{i}-step-{j}.png
./{name-docarray}/{i}-progress.png
./{name-docarray}/{i}-progress.gif
./{name-docarray}/da.protobuf.lz4
  • name-docarray是运行时定义的名称,如果没有定义,则会随机生成。
  • i-* 第几个Batch。
  • *-done-* 是当前Batch完成后的最终图像。
  • *-step-* 是某一步的中间图像,实时更新。
  • *-progress.png 是到目前为止所有中间结果的png图像,实时更新。
  • *-progress.gif 是到目前为止所有中间结果的动画 gif,实时更新。
  • da.protobuf.lz4 是到目前为止所有中间结果的压缩 protobuf,实时更新。

3.显示/保存/加载配置

如果你想知道你当前绘图的配置,有三种方法:

from discoart import show_config

show_config(da)  # show the config of the first run
show_config(da[3])  # show the config of the fourth run
show_config(
    'discoart-06030a0198843332edc554ffebfbf288'
)  # show the config of the run with a known DocArray ID

要保存 Document/DocumentArray 的配置:

from discoart import save_config

save_config(da, 'my.yml')  # save the config of the first run
save_config(da[3], 'my.yml')  # save the config of the fourth run

从配置中导入:

from discoart import create, load_config

config = load_config('my.yml')
create(**config)

此外,你还能直接把配置导出为图像的形式

from discoart.config import save_config_svg

save_config_svg(da)

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

AutoGrad 这个Python神器能够帮你自动计算函数斜率和梯度

AutoGrad 是一个老少皆宜的 Python 梯度计算模块。

对于初高中生而言,它可以用来轻易计算一条曲线在任意一个点上的斜率。

对于大学生、机器学习爱好者而言,你只需要传递给它Numpy这样的标准数据库下编写的损失函数,它就可以自动计算损失函数的导数(梯度)。

我们将从普通斜率计算开始,介绍到如何只使用它来实现一个逻辑回归模型。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install autograd

2.AutoGrad 计算斜率

对于初高中生同学而言,它可以用来轻松计算斜率,比如我编写一个斜率为0.5的直线函数:

# 公众号 Python实用宝典
import autograd.numpy as np
from autograd import grad


def oneline(x):
    y = x/2
    return y

grad_oneline = grad(oneline)
print(grad_oneline(3.0))

运行代码,传入任意X值,你就能得到在该X值下的斜率:

(base) G:\push\20220724>python 1.py
0.5

由于这是一条直线,因此无论你传什么值,都只会得到0.5的结果。

那么让我们再试试一个tanh函数:

# 公众号 Python实用宝典
import autograd.numpy as np
from autograd import grad

def tanh(x):
    y = np.exp(-2.0 * x)
    return (1.0 - y) / (1.0 + y)
grad_tanh = grad(tanh)
print(grad_tanh(1.0))

此时你会获得 1.0 这个 x 在tanh上的曲线的斜率:

(base) G:\push\20220724>python 1.py
0.419974341614026

我们还可以绘制出tanh的斜率的变化的曲线:

# 公众号 Python实用宝典
import autograd.numpy as np
from autograd import grad


def tanh(x):
    y = np.exp(-2.0 * x)
    return (1.0 - y) / (1.0 + y)
grad_tanh = grad(tanh)
print(grad_tanh(1.0))

import matplotlib.pyplot as plt
from autograd import elementwise_grad as egrad
x = np.linspace(-7, 7, 200)
plt.plot(x, tanh(x), x, egrad(tanh)(x))
plt.show()

图中蓝色的线是tanh,橙色的线是tanh的斜率,你可以非常清晰明了地看到tanh的斜率的变化。非常便于学习和理解斜率概念。

3.实现一个逻辑回归模型

有了Autograd,我们甚至不需要借用scikit-learn就能实现一个回归模型:

逻辑回归的底层分类就是基于一个sigmoid函数:

import autograd.numpy as np
from autograd import grad

# Build a toy dataset.
inputs = np.array([[0.52, 1.12,  0.77],
                   [0.88, -1.08, 0.15],
                   [0.52, 0.06, -1.30],
                   [0.74, -2.49, 1.39]])
targets = np.array([True, True, False, True])

def sigmoid(x):
    return 0.5 * (np.tanh(x / 2.) + 1)

def logistic_predictions(weights, inputs):
    # Outputs probability of a label being true according to logistic model.
    return sigmoid(np.dot(inputs, weights))

从下面的损失函数可以看到,预测结果的好坏取决于weights的好坏,因此我们的问题转化为怎么优化这个 weights 变量:

def training_loss(weights):
    # Training loss is the negative log-likelihood of the training labels.
    preds = logistic_predictions(weights, inputs)
    label_probabilities = preds * targets + (1 - preds) * (1 - targets)
    return -np.sum(np.log(label_probabilities))

知道了优化目标后,又有Autograd这个工具,我们的问题便迎刃而解了,我们只需要让weights往损失函数不断下降的方向移动即可:

# Define a function that returns gradients of training loss using Autograd.
training_gradient_fun = grad(training_loss)

# Optimize weights using gradient descent.
weights = np.array([0.0, 0.0, 0.0])
print("Initial loss:", training_loss(weights))
for i in range(100):
    weights -= training_gradient_fun(weights) * 0.01

print("Trained loss:", training_loss(weights))

运行结果如下:

(base) G:\push\20220724>python regress.py
Initial loss: 2.772588722239781
Trained loss: 1.067270675787016

由此可见损失函数以及下降方式的重要性,损失函数不正确,你可能无法优化模型。损失下降幅度太单一或者太快,你可能会错过损失的最低点。

总而言之,AutoGrad是一个你用来优化模型的一个好工具,它可以给你提供更加直观的损失走势,进而让你有更多优化想象力。有兴趣的朋友还可以看官方的更多示例代码:

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 量化投资的强化学习神器!FinRL 入门指南

关于强化学习的基础知识,可以阅读我们以前发表的一篇基础文章:

什么是强化学习?预测股票的效果如何?

使用强化学习预测股价,类似于心理学中的操作性条件反射原理,你需要在决策的时候采取合适的行动 (Action) 使奖励最大化。与监督学习预测未来的数值不同,强化学习根据输入的状态(如当日开盘价、收盘价等),输出系列动作(例如:买进、持有、卖出),并对好的动作结果不断进行奖励,对差的动作结果不断进行惩罚,使得最后的收益最大化,实现自动交易。

如果你从头开始编写一套强化学习的代码,时间成本和试错成本会比较高。而本文的主角 FinRL 框架,能够帮助你极大地减少学习成本、时间成本和试错成本。下面就介绍一下 FinRL 的使用方法。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

# 首先克隆项目
git clone https://github.com/AI4Finance-Foundation/FinRL.git
# 进入刚克隆的项目,安装依赖
cd FinRL
pip install 

请注意 Python 版本要大于等于 3.7。此外,如果你的当前Python环境下安装了 zipline,请 pip uninstall 掉 zipline,因为Zipline与FinRL不兼容。

可能出现的错误:

如果你出现以下红字提示:

error: command 'swig.exe' failed: No such file or directory

请使用conda安装swig:

conda install swig

然后重新执行 pip install . 即可。

2.模型训练

运行官方示例的时候会使用到雅虎财经的数据,雅虎财经在中国已经关闭服务,因此你会需要VPN才能下载雅虎财经的数据。

cd FinRL
python Stock_NeurIPS2018.py

运行的时候大概率会遇到这个问题(2022-07-03):

FileNotFoundError: Please set your own ALPACA_API_KEY and ALPACA_API_SECRET in config_private.py

这是官网的一个不严谨实现导致的,你可以将 finrl/main.py 中25~30行的代码移动到第100行,如下所示:

此外,在运行代码的时候,你可能会遇到无法下载数据的问题,这是因为雅虎财经在中国已经关闭服务,你需要在 Stock_NeurIPS2018.py 的第172行代码 fetch_data 函数中添加proxy参数:

# 公众号:二七阿尔量化
# 此处我的代理是10809端口,你应该按需修改
df = YahooDownloader(start_date = '2009-01-01',
                     end_date = '2021-10-31',
                     ticker_list = config_tickers.DOW_30_TICKER
                     ).fetch_data(proxy={"http": "http://127.0.0.1:10809", "https": "https://127.0.0.1:10809"})

此外,在 finrl/finrl_meta/preprocessor/preprocessors.py 的第191行,你也需要增加proxy参数:

# 公众号:二七阿尔量化
# 此处我的代理是10809端口,你应该按需修改
df_vix = YahooDownloader(
            start_date=df.date.min(), end_date=df.date.max(), ticker_list=["^VIX"]
        ).fetch_data(proxy={"http": "http://127.0.0.1:10809", "https": "https://127.0.0.1:10809"})

正常运行起来的模型训练如下图所示:

下面是我简化版的到SAC模型训练为止的全部代码:

# 公众号:二七阿尔量化
from finrl import config
from finrl import config_tickers
from finrl.main import check_and_make_directories

import pandas as pd

from finrl.finrl_meta.preprocessor.yahoodownloader import YahooDownloader
from finrl.finrl_meta.preprocessor.preprocessors import FeatureEngineer, data_split
from finrl.finrl_meta.env_stock_trading.env_stocktrading import StockTradingEnv
from finrl.agents.stablebaselines3.models import DRLAgent

import sys
sys.path.append("../FinRL-Library")

import itertools

from finrl.config import (
    DATA_SAVE_DIR,
    TRAINED_MODEL_DIR,
    TENSORBOARD_LOG_DIR,
    RESULTS_DIR,
)

check_and_make_directories([DATA_SAVE_DIR, TRAINED_MODEL_DIR, TENSORBOARD_LOG_DIR, RESULTS_DIR])


'''
# Part 1. 下载数据
'''

df = YahooDownloader(
    start_date='2009-01-01',
    end_date='2021-10-31',
    ticker_list=config_tickers.DOW_30_TICKER
).fetch_data(proxy={"http": "http://127.0.0.1:10809", "https": "https://127.0.0.1:10809"})


print(f"config_tickers.DOW_30_TICKER: {config_tickers.DOW_30_TICKER}")


print(f"df.shape: {df.shape}")


df.sort_values(['date','tic'],ignore_index=True).head()


'''
# Part 2: 数据预处理
'''

fe = FeatureEngineer(
                    use_technical_indicator=True,
                    tech_indicator_list=config.INDICATORS,
                    use_vix=True,
                    use_turbulence=True,
                    user_defined_feature = False)

processed = fe.preprocess_data(df)


list_ticker = processed["tic"].unique().tolist()
list_date = list(pd.date_range(processed['date'].min(),processed['date'].max()).astype(str))
combination = list(itertools.product(list_date,list_ticker))

processed_full = pd.DataFrame(combination,columns=["date","tic"]).merge(processed,on=["date","tic"],how="left")
processed_full = processed_full[processed_full['date'].isin(processed['date'])]
processed_full = processed_full.sort_values(['date','tic'])

processed_full = processed_full.fillna(0)


processed_full.sort_values(['date','tic'],ignore_index=True).head(10)

# 训练集
train = data_split(processed_full, '2009-01-01','2020-07-01')
# 测试集
trade = data_split(processed_full, '2020-07-01','2021-10-31')

print(f"len(train): {len(train)}")
print(f"len(trade): {len(trade)}")
print(f"train.tail(): {train.tail()}")
print(f"trade.head(): {trade.head()}")
print(f"config.INDICATORS: {config.INDICATORS}")
stock_dimension = len(train.tic.unique())
state_space = 1 + 2*stock_dimension + len(config.INDICATORS)*stock_dimension
print(f"Stock Dimension: {stock_dimension}, State Space: {state_space}")

buy_cost_list = sell_cost_list = [0.001] * stock_dimension
num_stock_shares = [0] * stock_dimension

env_kwargs = {
    "hmax": 100,
    "initial_amount": 1000000,
    "num_stock_shares": num_stock_shares,
    "buy_cost_pct": buy_cost_list,
    "sell_cost_pct": sell_cost_list,
    "state_space": state_space,
    "stock_dim": stock_dimension,
    "tech_indicator_list": config.INDICATORS,
    "action_space": stock_dimension,
    "reward_scaling": 1e-4
}


e_train_gym = StockTradingEnv(df = train, **env_kwargs)

env_train, _ = e_train_gym.get_sb_env()
print(f"type(env_train): {type(env_train)}")


'''
# Part 3: 模型训练
'''
agent = DRLAgent(env = env_train)
SAC_PARAMS = {
    "batch_size": 128,
    "buffer_size": 1000000,
    "learning_rate": 0.0001,
    "learning_starts": 100,
    "ent_coef": "auto_0.1",
}

model_sac = agent.get_model("sac", model_kwargs = SAC_PARAMS)


trained_sac = agent.train_model(model=model_sac,
                             tb_log_name='sac',
                             total_timesteps=60000)

3.模型测试

在这一部分,我们将使用测试集进行模拟交易,检验模型的效果。

在env_kwargs中,我们设置了初始资金为1000000美元,测试也会以这个初始资金为起点。

# 测试
e_trade_gym = StockTradingEnv(df=trade, turbulence_threshold=70, risk_indicator_col='vix', **env_kwargs)
df_account_value, df_actions = DRLAgent.DRL_prediction(
    model=trained_sac,
    environment=e_trade_gym
)
print(f"df_account_value.tail(): {df_account_value.tail()}")

如下:

此外,df_actions内保存了每天的持仓记录:

print(f"df_actions.head(): {df_actions.head()}")

调用 backtest_stats 函数,能获得完整的回测结果:

print("==============Get Backtest Results===========")
now = datetime.datetime.now().strftime('%Y%m%d-%Hh%M')

perf_stats_all = backtest_stats(account_value=df_account_value)
perf_stats_all = pd.DataFrame(perf_stats_all)
perf_stats_all.to_csv("./"+config.RESULTS_DIR+"/perf_stats_all_"+now+'.csv')

结果如下所示:

可以见到,模型的年化收益为30%,累计净值收益为43%.

但是这段时间为美股的牛市,我们还需要以道琼斯指数为基准计算超额收益,才能更直观地展示模型的效果:

print("==============Get Baseline Stats===========")
baseline_df = get_baseline(
        ticker="^DJI",
        start = df_account_value.loc[0,'date'],
        end = df_account_value.loc[len(df_account_value)-1,'date'])

stats = backtest_stats(baseline_df, value_col_name = 'close')

可见模型还是具有超额收益的,我们将其绘制为图表更清晰地表达:

backtest_result = backtest_plot(df_account_value, 
             baseline_ticker = '^DJI', 
             baseline_start = df_account_value.loc[0,'date'],
             baseline_end = df_account_value.loc[len(df_account_value)-1,'date'])
with open("backtest_result.html", "w") as file:
    file.write(backtest_result)

作者给我们内置了许多漂亮的回测图表,非常好用。但我们只需要看最关键的cumulative returns. 从图中可以看到这个模型(绿色的线条)一开始的表现并不如指数,但是到了后面,它的表现渐渐优于指数。

当然,这是官方给的示例数据,大家可以用自己的因子补充数据,将模型完善地更好。本文的示例中使用的是SAC模型,你也可以尝试其他的强化学习模型。

总之,Finrl 只能提供你一双”巨人的肩膀“,你应该根据自己的实际业务场景和数据类型使用不同的优化方法。

4.其他

FinRL不只能支持美股,它还支持A股的部分数据源,如聚宽、米筐和Tushare:

以downloader为例,用法很简单,库中提供了 Tushare 的 downloader, 你只需要把:

from finrl.finrl_meta.preprocessor.yahoodownloader import YahooDownloader

替换为:

from finrl.finrl_meta.preprocessor.tusharedownloader import TushareDownloader

并进行相应的代码修改即可,当然,除此之外还有许多细节问题需要处理,由于文章篇幅的问题,我们留到下篇文章再给大家介绍。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 大规模深度学习分布式框架 DeepSpeed 使用指南

最常见的深度学习框架应该是TensorFlow、Pytorch、Keras,但是这些框架在面向大规模模型的时候都不是很方便。

比如Pytorch的分布式并行计算框架(Distributed Data Parallel,简称DDP),它也仅仅是能将数据并行,放到各个GPU的模型上进行训练。

也就是说,DDP的应用场景在你的模型大小大于显卡显存大小时,它就很难继续使用了,除非你自己再将模型参数拆散分散到各个GPU上。

今天要给大家介绍的DeepSpeed,它就能实现这个拆散功能,它通过将模型参数拆散分布到各个GPU上,以实现大型模型的计算,弥补了DDP的缺点,非常方便,这也就意味着我们能用更少的GPU训练更大的模型,而且不受限于显存。

DeepSpeed入门并不简单,尽管是微软开源的框架,文档却写的一般,缺少条理性,也没有从零到一的使用示例。下面我就简单介绍一下怎么使用DeepSpeed这个框架。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install deepspeed

此外,你还需要下载 Pytorch,在官网选择自己对应的系统版本和环境,按照指示安装即可:

https://pytorch.org/get-started/locally/

2.使用 DeepSpeed 分布式框架

使用DeepSpeed其实和写一个pytorch模型只有部分区别,一开始的流程是一样的。

2.1 载入数据集:

import torch
import torchvision
import torchvision.transforms as transforms

trainset = torchvision.datasets.CIFAR10(root='./data',
                                        train=True,
                                        download=True,
                                        transform=transform)
trainloader = torch.utils.data.DataLoader(trainset,
                                          batch_size=16,
                                          shuffle=True,
                                          num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data',
                                       train=False,
                                       download=True,
                                       transform=transform)
testloader = torch.utils.data.DataLoader(testset,
                                         batch_size=4,
                                         shuffle=False,
                                         num_workers=2)

2.2 编写模型:

import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()
criterion = nn.CrossEntropyLoss()

这里我写了一个非常简单的模型作测试。

2.3 初始化Deepspeed

DeepSpeed 通过输入参数来启动训练,因此需要使用argparse解析参数:

import argparse


def add_argument():
    parser = argparse.ArgumentParser(description='CIFAR')
    parser.add_argument('-b',
                        '--batch_size',
                        default=32,
                        type=int,
                        help='mini-batch size (default: 32)')
    parser.add_argument('-e',
                        '--epochs',
                        default=30,
                        type=int,
                        help='number of total epochs (default: 30)')
    parser.add_argument('--local_rank',
                        type=int,
                        default=-1,
                        help='local rank passed from distributed launcher')

    parser.add_argument('--log-interval',
                        type=int,
                        default=2000,
                        help="output logging information at a given interval")

    parser = deepspeed.add_config_arguments(parser)
    args = parser.parse_args()
    return args

此外,模型初始化的时候除了参数,还需要model及其parameters,还有训练集:

args = add_argument()
net = Net()
parameters = filter(lambda p: p.requires_grad, net.parameters())
model_engine, optimizer, trainloader, __ = deepspeed.initialize(
    args=args, model=net, model_parameters=parameters, training_data=trainset)

2.4 训练逻辑

下面的部分和我们平时训练模型是几乎一样的代码,请注意 local_rank 是你不需要管的参数,在后面启动模型训练的时候,DeepSpeed会自动给这个参数赋值。

for epoch in range(2):
    running_loss = 0.0
    for i, data in enumerate(trainloader):
        inputs, labels = data[0].to(model_engine.local_rank), data[1].to(
            model_engine.local_rank)
        outputs = model_engine(inputs)
        loss = criterion(outputs, labels)
        model_engine.backward(loss)
        model_engine.step()

        # print statistics
        running_loss += loss.item()
        if i % args.log_interval == (args.log_interval - 1):
            print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / args.log_interval))
            running_loss = 0.0

2.5 测试逻辑

模型测试和模型训练的逻辑类似:

correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images.to(model_engine.local_rank))
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels.to(
            model_engine.local_rank)).sum().item()

print('Accuracy of the network on the 10000 test images: %d %%' %
      (100 * correct / total))

2.6 编写模型参数

在当前目录下新建一个 config.json 里面写好我们的调优器、训练batch等参数:

 {
   "train_batch_size": 4,
   "steps_per_print": 2000,
   "optimizer": {
     "type": "Adam",
     "params": {
       "lr": 0.001,
       "betas": [
         0.8,
         0.999
       ],
       "eps": 1e-8,
       "weight_decay": 3e-7
     }
   },
   "scheduler": {
     "type": "WarmupLR",
     "params": {
       "warmup_min_lr": 0,
       "warmup_max_lr": 0.001,
       "warmup_num_steps": 1000
     }
   },
   "wall_clock_breakdown": false
 }

完整的开发流程就结束了,可以看到其实和我们平时使用pytorch开发模型的区别不大,就是在初始化的时候使用 DeepSpeed,并以输入参数的形式初始化。完整代码可以在Python实用宝典后台回复 Deepspeed 下载。

3. 测试代码

现在就来测试我们上面的代码能不能正常运行。

在这里,我们需要用环境变量控制使用的GPU,比如我的机器有10张GPU,我只使用6, 7, 8, 9号GPU,输入命令:

export CUDA_VISIBLE_DEVICES="6,7,8,9"

然后开始运行代码:

deepspeed test.py --deepspeed_config config.json

看到下面的输出说明开始正常运行,在下载数据了:

开始训练的时候 DeepSpeed 通常会打印更多的训练细节供用户监控,包括训练设置、性能统计和损失趋势,效果类似于:

worker-0: [INFO 2020-02-06 20:35:23] 0/24550, SamplesPerSec=1284.4954513975558
worker-0: [INFO 2020-02-06 20:35:23] 0/24600, SamplesPerSec=1284.384033658866
worker-0: [INFO 2020-02-06 20:35:23] 0/24650, SamplesPerSec=1284.4433482972925
worker-0: [INFO 2020-02-06 20:35:23] 0/24700, SamplesPerSec=1284.4664449792422
worker-0: [INFO 2020-02-06 20:35:23] 0/24750, SamplesPerSec=1284.4950124403447
worker-0: [INFO 2020-02-06 20:35:23] 0/24800, SamplesPerSec=1284.4756105952233
worker-0: [INFO 2020-02-06 20:35:24] 0/24850, SamplesPerSec=1284.5251526215386
worker-0: [INFO 2020-02-06 20:35:24] 0/24900, SamplesPerSec=1284.531217073863
worker-0: [INFO 2020-02-06 20:35:24] 0/24950, SamplesPerSec=1284.5125323220368
worker-0: [INFO 2020-02-06 20:35:24] 0/25000, SamplesPerSec=1284.5698818883018
worker-0: Finished Training
worker-0: GroundTruth:    cat  ship  ship plane
worker-0: Predicted:    cat   car   car plane
worker-0: Accuracy of the network on the 10000 test images: 57 %

当你运行到最后,出现了这样的输出,恭喜你,完成了你的第一个 DeepSpeed 模型,可以开始你的大规模训练之路了。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

NNI 一个Python帮你自动做机器学习调参的神器

NNI 自动机器学习调参,是微软开源的又一个神器,它能帮助你找到最好的神经网络架构或超参数,支持各种训练环境

它常用的使用场景如下:

  • 想要在自己的代码、模型中试验不同的机器学习算法
  • 想要在不同的环境中加速运行机器学习。
  • 想要更容易实现或试验新的机器学习算法的研究员或数据科学家,包括:超参调优算法,神经网络搜索算法以及模型压缩算法。

它支持的框架有:

  • PyTorch
  • Keras
  • TensorFlow
  • MXNet
  • Caffe2
  • Scikit-learn
  • XGBoost
  • LightGBM

基本上市面上所有的深度学习和机器学习的框架它都支持。

下面就来看看怎么使用这个工具。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install --upgrade nni

2.NNI 机器学习调参运行示例

让我们运行一个示例来验证是否安装成功,首先克隆项目:

git clone -b v2.6 https://github.com/Microsoft/nni.git

(如果你无法成功克隆项目,请在Python实用宝典后台回复nni下载项目)

运行 MNIST-PYTORCH 示例,Linux/macOS:

nnictl create --config nni/examples/trials/mnist-pytorch/config.yml

Windows:

nnictl create --config nni\examples\trials\mnist-pytorch\config_windows.yml

出现这样的界面就说明安装成功,示例运行正常:

访问 http://127.0.0.1:8080 可以配置运行时间、实验次数等:

3.模型自动调参配置

那么如何让它和我们自己的模型适配呢?

观察 config_windows.yaml 会发现:

searchSpaceFile: search_space.json
trialCommand: python mnist.py
trialGpuNumber: 0
trialConcurrency: 1
tuner:
  name: TPE
  classArgs:
    optimize_mode: maximize
trainingService:
  platform: local

我们先看看 trialCommand, 这很明显是训练使用的命令,训练代码位于 mnist.py,其中有部分代码如下:

def get_params():
    # Training settings
    parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
    parser.add_argument("--data_dir", type=str,
                        default='./data', help="data directory")
    parser.add_argument('--batch_size', type=int, default=64, metavar='N',
                        help='input batch size for training (default: 64)')
    parser.add_argument("--batch_num", type=int, default=None)
    parser.add_argument("--hidden_size", type=int, default=512, metavar='N',
                        help='hidden layer size (default: 512)')
    parser.add_argument('--lr', type=float, default=0.01, metavar='LR',
                        help='learning rate (default: 0.01)')
    parser.add_argument('--momentum', type=float, default=0.5, metavar='M',
                        help='SGD momentum (default: 0.5)')
    parser.add_argument('--epochs', type=int, default=10, metavar='N',
                        help='number of epochs to train (default: 10)')
    parser.add_argument('--seed', type=int, default=1, metavar='S',
                        help='random seed (default: 1)')
    parser.add_argument('--no_cuda', action='store_true', default=False,
                        help='disables CUDA training')
    parser.add_argument('--log_interval', type=int, default=1000, metavar='N',
                        help='how many batches to wait before logging training status')


    args, _ = parser.parse_known_args()
    return args

如上所示,这个模型里提供了 10 个参数选择。也就是说 NNI 可以帮我们自动测试这10个参数。

那么这些参数在哪里设定?答案是在 searchSpaceFile 中,对应的值也就是 search_space.json:

{
    "batch_size": {"_type":"choice", "_value": [16, 32, 64, 128]},
    "hidden_size":{"_type":"choice","_value":[128, 256, 512, 1024]},
    "lr":{"_type":"choice","_value":[0.0001, 0.001, 0.01, 0.1]},
    "momentum":{"_type":"uniform","_value":[0, 1]}
}

这里有4个选项,NNI 是怎么组合这些参数的呢?这就是tuner参数干的事,为了让机器学习和深度学习模型适应不同的任务和问题,我们需要进行超参数调优,而自动化调优依赖于优秀的调优算法。NNI 内置了先进的调优算法,并且提供了易于使用的 API。

在 NNI 中,调优算法被称为“tuner”。Tuner 向 trial 发送超参数,接收运行结果从而评估这组超参的性能,然后将下一组超参发送给新的 trial。

下表简要介绍了 NNI 内置的调优算法。

Tuner算法简介
TPETree-structured Parzen Estimator (TPE) 是一种基于序列模型的优化方法 (sequential model-based optimization, SMBO)。SMBO方法根据历史数据来顺序地构造模型,从而预估超参性能,并基于此模型来选择新的超参。参考论文
Random Search (随机搜索)随机搜索在超算优化中表现出了令人意外的性能。如果没有对超参分布的先验知识,我们推荐使用随机搜索作为基线方法。参考论文
Anneal (退火)朴素退火算法首先基于先验进行采样,然后逐渐逼近实际性能较好的采样点。该算法是随即搜索的变体,利用了反应曲面的平滑性。该实现中退火率不是自适应的。
Naive Evolution(朴素进化)朴素进化算法来自于 Large-Scale Evolution of Image Classifiers。它基于搜索空间随机生成一个种群,在每一代中选择较好的结果,并对其下一代进行变异。朴素进化算法需要很多 Trial 才能取得最优效果,但它也非常简单,易于扩展。参考论文
SMACSMAC 是基于序列模型的优化方法 (SMBO)。它利用使用过的最突出的模型(高斯随机过程模型),并将随机森林引入到SMBO中,来处理分类参数。NNI 的 SMAC tuner 封装了 GitHub 上的 SMAC3参考论文注意:SMAC 算法需要使用 pip install nni[SMAC] 安装依赖,暂不支持 Windows 操作系统。
Batch(批处理)批处理允许用户直接提供若干组配置,为每种配置运行一个 trial。
Grid Search(网格遍历)网格遍历会穷举搜索空间中的所有超参组合。
HyperbandHyperband 试图用有限的资源探索尽可能多的超参组合。该算法的思路是,首先生成大量超参配置,将每组超参运行较短的一段时间,随后抛弃其中效果较差的一半,让较好的超参继续运行,如此重复多轮。参考论文
Metis大多数调参工具仅仅预测最优配置,而 Metis 的优势在于它有两个输出:(a) 最优配置的当前预测结果, 以及 (b) 下一次 trial 的建议。大多数工具假设训练集没有噪声数据,但 Metis 会知道是否需要对某个超参重新采样。参考论文
BOHBBOHB 是 Hyperband 算法的后续工作。 Hyperband 在生成新的配置时,没有利用已有的 trial 结果,而本算法利用了 trial 结果。BOHB 中,HB 表示 Hyperband,BO 表示贝叶斯优化(Byesian Optimization)。 BOHB 会建立多个 TPE 模型,从而利用已完成的 Trial 生成新的配置。参考论文
GP (高斯过程)GP Tuner 是基于序列模型的优化方法 (SMBO),使用高斯过程进行 surrogate。参考论文
PBTPBT Tuner 是一种简单的异步优化算法,在固定的计算资源下,它能有效的联合优化一组模型及其超参来最优化性能。参考论文
DNGODNGO 是基于序列模型的优化方法 (SMBO),该算法使用神经网络(而不是高斯过程)去建模贝叶斯优化中所需要的函数分布。

可以看到示例中,选择的是TPE tuner.

其他的参数比如 trialGpuNumber,指的是使用的gpu数量,trialConcurrency 指的是并发数。trainingService 中 platform 为 local,指的是本地训练。

当然,还有许多参数可以选,比如:

trialConcurrency: 2                 # 同时运行 2 个 trial
maxTrialNumber: 10                  # 最多生成 10 个 trial
maxExperimentDuration: 1h           # 1 小时后停止生成 trial

不过这些参数在调优开始时的web页面上是可以进行调整的。

所以其实NNI干的事情就很清楚了,也很简单。你只需要在你的模型训练文件中增加你想要调优的参数作为输入,就能使用NNI内置的调优算法对不同的参数进行调优,而且允许从页面UI上观察调优的整个过程,相对而言还是很方便的。

不过,NNI可能不太适用一些数据量极大或模型比较复杂的情况。比如基于DDP开发的模型,在NNI中可能无法实现大型的分布式计算。

当然,绝大多数情况下的训练任务,NNI都可以用得上。大家有兴趣深入使用的可以阅读NNI官方文档:https://nni.readthedocs.io/

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典