赞
踩
20世纪90年代以前的语言模型都是基于语法分析这种方法,效果一直不佳。到了20世纪90年代,采用统计学方法分析语言,取得了重大进展。但是在庞大而复杂的语言信息上,基于传统统计的因为计算量巨大,难以进一步提升计算机语言分析的性能。2023年首度将基于神经网络的深度学习引入了语言分析模型中,计算机理解语言的准确性达到了前所未有的高度。依然是因为计算量巨大,基于深度学习的语言模型难以进一步提升准确性和普及应用。随着2018年,研究人员将Transformer引入神经网络,大幅缩减了计算量,而且提升了语言的前后关联度,再一次提升了自然语言处理的准确性,并且将计算机处理自然语言的成本大幅降低。
随着语言模型参数规模的提升,语言模型在各种任务中展现出惊人的能力(这种能力也称为“涌现能力”),自此进入了大语言模型(Large Language Model, LLM)时代。大语言模型 (LLM) 指包含数百亿(或更多)参数的语言模型,这些模型在大量的文本数据上进行训练,例如国外的有GPT-3 、GPT-4、PaLM 、Galactica 和 LLaMA 等,国内的有ChatGLM、文心一言、通义千问、讯飞星火等。
早期的LLM多用于自然语言处理领域的问答、翻译,进一步延伸到写文章,编写代码等。随着多模态能力的增加,大语言模型逐步展现出统都一人工智能的趋势,做到真正的通用人工智能(AGI)。LLM逐步成为一个基础模型,人们可以在LLM的基础上做进一步的优化,完成更加专业精细的任务。
Transformer模型是由谷歌团队在2017年发表的论文《Attention is All You Need》所提出。这篇论文的主体内容只有几页,主要就是对下面这个模型架构的讲解。
传输的RNN用于处理系列时,会增加一个隐藏状态用来记录上一个时刻的序列信息。在处理翻译文本时,一个字的意思可能和前面序列的内容相关,通过隐藏状态,RNN能够很好地翻译上下文相关性较大的文本。但是如果文本内容非常大的时候,隐藏状态无法完全包括之前的所有状态(如果包括,其计算量非常巨大,难以实现)。
自注意力机制(Self-Attention)是在注意力机制上优化得来的,其只注意输入信息本身。即输入向量中每一个成员都和其他成员经过一个注意力函数处理之后,形成一个相关性的权重向量表。如:
A | B | C | |
A | 1.0 | 0.7 | 0.4 |
B | 0.7 | 1 | 0.3 |
C | 0.4 | 0.3 | 1 |
这样一张权重向量表的计算量相比在RNN中隐藏状态的计算量少很多。
通过这个权重向量表,无论需要翻译的原始文件多大,都能够很好地找到之前信息对当前翻译信息的影响,可以翻译得更加准确。
自注意力机制默认是计算当前内容与上下单个内容的关联性,可以理解为单头注意力。多头注意力(Multi-Head Attention)机制,则是当前内容与前面多个内容同时存在的关联性,这样关联性计算会更加准确。这里的多头,一般为2到128个,这是一个超参数,可由训练者修改。这个超参数设置得越大,语义关注度会更好,但是相应地会增加更多计算力,所以需要综合考虑设置此参数。
掩码多头注意力(Masked Multi-Head Attention)提供掩码表。在解码器中,当生成第t个输出时,模型只应该关注t时刻之前的输入序列,而不应该关注未来的输入。这样不仅提升准确性,也减少计算量提升性能。
残差与归一化(Add & Norm),它通常出现在Transformer的各个子层之后。它包含两个主要操作:
前馈神经网络(Feed Forward ),最基础的神经网络,其由输入层、多个隐藏层和输出层组成的简单前馈结构。
位置编码(Positional Encoding)是一种用于给输入序列中的每个元素添加位置信息,因为信息不仅与其内容有关,与其所在位置也有关。在此论文中使用的是正弦和余弦函数来能位置进行编码,简单高效。但是因为正弦和余弦函数的周期性,在大量输入时,无法区分前后周期的位置,导致泛化能力不足。后续又有其他位置编码的算法,如旋转位置编码(RoPE)。
Linear的作用是将词汇表大小的分类预测, Softmax则将其以概率分布的形式展现出来。
Transformer模型由两部分组成,分别为编码器和解码器,设计之初是用于翻译语言的。
如下图是chatgpt使用的嵌入向量表模型,其总共有100255个向量,表示100255个Token(1个英文单词或中文需要1个或2个Token表示)。因为一个Token可能有多个语义(一个字在不同的句子中意思可能不一样),此处默认一个Token有64种语义,这是一个超参数,训练过程中可以修改(chatgpt2使用的是512)。为了避免梯度消失(矩阵相乘时两个值相乘变得过小)或梯度爆炸(多次相乘变得数据过大),所以数据默认都是初始为1左右。
解码器的主要作用是用来预测输出的。chatgpt研发人员在看完Transformer模型后,意识到Transformer模型不仅可以用于翻译,同样可以用于文字生成,而且只需要解码器即可以用于语言生成。
通过大量的源语言文本和目标语言文本行训练,得到相关模型的参数。训练后,模型的参数可以保存出来给其他人直接使用。训练可能进行多轮,会不断评估结果,修改权重参数。
通过交叉熵验证结果,如果不正确,修改结果对应d_model中的权重,一直训练到结果准确为止。
预测又被称为推理,解码器输入<Begin>标记开始,此时结合编码器输入的“我”,就可以输出"I"。然后解码器输入”<Begin> I“,结合编码器输出的”有“,就可以输出”have"。也可以不用Encoder,直接从用户输入的信息开始预测接下来的文字,这就是ChatGPT。
Transformer模型的目的是解决机器翻译中遇到的问题,即循环神经网络(RNN)在处理长序列数据时的局限性。其主要有两个创新,引入了位置编码和注意力机制。位置编码和注意力也不是新的技术,但是其结合起来,确实大大优化了RNN在处理长序列时的局限,大幅提升了语言翻译的效率。其在NPL上的应用更是产生了大语言模型。
如下图是chatgpt的结构图,其总的参数量:vocab_size*d_model + 4*(num_heads*d_model*d_model) + 2*(d_model*d_model*4) + d_model*vocab_size。
Inputs如果过大,Input Embddings就会变成一个非常大的矩阵,在注意力计算以及前馈网络计算时的计算量就非常大。做基座预训练时,使用4K上下文就可以。在做微调时,可以使用更大的上下文大小,如100万上下文。针对超大上下文,正弦和余统位置编码因为周期性的原因,性能不太好,业界使用ALiBi来优化位置住友,可以在微调和推理时使用超大上下文。
Hugging Face是一个知名的开源社区和平台,专注于自然语言处理(NLP)和机器学习。该平台提供了各种强大的工具、预训练模型和资源,旨在帮助研究人员、开发者和数据科学家快速构建和部署NLP应用。大部分开源模型都会将相关部署的方法上传到此社区,并且此社区也会对模型进行评估。
还可以分类为:
大模型训练的时候,为了缩减训练时间,需要更多的算力。推理的时候,相应的内存需求只需要比实际参数的模型稍大即可。
大语言模型测试排名,因为测试的问题是公开的,所以模型如果针对问题进行了微调,那么模型就会表现出过拟合的情况,也即评测成绩很好,但是实际的应用效率不好。所以测试排名只是做一个大概的参考。
Hugging Face的排名:https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard
C-Eval是一个中文大模型的评估网站,以下大模型都是开源的。
在机器学习中,图像、视频、文本、语音等每一种数据形式都是一种模态,所谓多模态,就是利用模型去同时处理多个模态数据,例如图生文本、文本生图等。多模态大模型则是在大规模语料上预训练能更好地理解和处理复杂的多模态数据。多模态也是在大语言模型的基础上来实现的。如图生文,其有一个Image Encoder然后再通过Q-Former来学习其图片的特征,再进入LLM解码器输出文字。
大量的图生文再经过交叉熵和人工标签来比较反复训练直到LLM输出的信息符合要求为止。训练完成之后,就可以将输入的文字信息转换为图片的特征信息,再通过一个Image Decoder转换为图片。
现在的文生成图中使用了扩散模型Duffison来更准确地提取特征。开源文生图大模型Stable Diffusion即使用增强的扩展模型Duffision。
这里有一篇有关多模态的论文综述,对多模态的现状以及发展趋势讲解得比较全面:https://arxiv.org/pdf/2401.13601.pdf
Stable Diffusion可以直接下载安装部署,也可以通过modelscope或transformers来代码部署。此处基于阿里云和modelscope来部署测试。
- from modelscope.utils.constant import Tasks
- from modelscope.pipelines import pipeline
- import cv2
- from IPython.display import Image
-
- pipe = pipeline(task=Tasks.text_to_image_synthesis,
- model='AI-ModelScope/stable-diffusion-v1-5',
- model_revision='v1.0.0')
-
- prompt = 'a photo of an astronaut riding a horse on mars'
- output = pipe({'text': prompt})
- cv2.imwrite('result.png', output['output_imgs'][0])
-
- Image(filename='result.png')
下列网页详细列举了用于预训练、评估的数据库。
GitHub - lmmlzn/Awesome-LLMs-Datasets: Summarize existing representative LLMs text datasets.,最全的中英文数据集
https://github.com/XueFuzhao/InstructionWild colossal-ai的self-instruct数据集,中英。
https://github.com/LianjiaTech/BELLE/blob/main/zh_seed_tasks.json lianjia.tech的self-instruct数据集
https://huggingface.co/datasets/BelleGroup/generated_train_1M_CN lianjia.tech参考Stanford Alpaca 生成的中文数据集1M
https://huggingface.co/datasets/BelleGroup/generated_train_0.5M_CN lianjia.tech参考Stanford Alpaca 生成的中文数据集0.5M
预训练中文语料汇总(附数据) - 知乎 HelloNLP总结的多个中文预训练语料
https://github.com/dbiir/UER-py/wiki/%E9%A2%84%E8%AE%AD%E7%BB%83%E6%95%B0%E6%8D%AE CLUECorpusSmall,News Commentary v13
目前性能比较好的大模型都是商用的,如GPT-3.5、GPT-4、Claude-3、Gemini Pro都是通过Web或API调用来使用。如果想在此基础上定制大模型,可通过申请微调或向量数据库两种方法。
需要准备很多数据,并且需要对数据处理,这是大模型的一个难点。数据不好,哪怕模型参数再大,效果也不好,如马斯克开源的Grok,虽然有3140亿参数,但是测试效果很差。
另外,训练还需要较多的GPT来加快训练进程,投入非常高。
直接以别训练好的模型作为基础模型,在此基础上进行再训练或微调。SFT(有监督微调)和 RLHF(强化学习人类反馈)是优化预模型的方法,这两种技术统称为 "对齐"。投入相对从头开始训练成本小。
微调的方法目前很多,业界也在继续探索。基于监督学习的这种微调比较容易实现,基于无监督学习的微调有一定技术难度。
- from transformers import AutoTokenizer, AutoModel
- tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
- model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True, device='cuda')
- model = model.eval()
- response, history = model.chat(tokenizer, "你好,请用C写一个快速排序算法", history=[])
- print(response)
- from modelscope import AutoTokenizer, AutoModelForCausalLM
- tokenizer = AutoTokenizer.from_pretrained("vivo-ai/BlueLM-7B-Chat-4bits", trust_remote_code=True, use_fast=False)
- model = AutoModelForCausalLM.from_pretrained("vivo-ai/BlueLM-7B-Chat-4bits", device_map="cuda:0", trust_remote_code=True)
- model = model.eval()
- inputs = tokenizer("[|Human|]:用C++写一个快速排序?[|AI|]:", return_tensors="pt")
- inputs = inputs.to("cuda:0")
- outputs = model.generate(**inputs, max_new_tokens=2048)
- print(tokenizer.decode(outputs.cpu()[0], skip_special_tokens=True))
- cd train
- sh script/bluelm-7b-sft.sh
Agent就是代理,就像房产中介一样。买房人只需要提出需求,中介帮助找房,过户,报税等操作。同样使用大模型完成一些复杂的任务时,有一定的技术难度。Agent就可以简化这种操作,让用户更容易地通过大模型实现需求。如AutoGen Agents,就可以将用户需要逐个分解,然后再调用LLM逐个实现,最终实现用户需求。
微软开源的AutoGen Agents框架,目前引起很多人的关注。
ChatGPT-2是开源的,因为其后续版本的效果好,有猜测很多未开源的大语言模型其实是基于ChatGPT-2进行的优化,扩大参数模型实现的。ChatGPT-2的结构如下,在Transformer的Decoder的基础上,添加Layer Norm层(归一化层),其结构很简单。
GPT2是基于tensorflow实现的,前馈神经网络tensorflow已经有实现,GPT2只需要调用即可。CUDA的调用,tensorflow也实现了,只需要将矩阵参数传递即可。所以GPT2的代码很简单。总共只有近千行代码。主代码如下:
- import numpy as np
- import tensorflow as tf
- from tensorflow.contrib.training import HParams
-
- def default_hparams():
- return HParams(
- n_vocab=0,
- n_ctx=1024,
- n_embd=768,
- n_head=12,
- n_layer=12,
- )
-
- def shape_list(x):
- """Deal with dynamic shape in tensorflow cleanly."""
- static = x.shape.as_list()
- dynamic = tf.shape(x)
- return [dynamic[i] if s is None else s for i, s in enumerate(static)]
-
- def softmax(x, axis=-1):
- x = x - tf.reduce_max(x, axis=axis, keepdims=True)
- ex = tf.exp(x)
- return ex / tf.reduce_sum(ex, axis=axis, keepdims=True)
-
- def gelu(x):
- return 0.5*x*(1+tf.tanh(np.sqrt(2/np.pi)*(x+0.044715*tf.pow(x, 3))))
-
- def norm(x, scope, *, axis=-1, epsilon=1e-5):
- """Normalize to mean = 0, std = 1, then do a diagonal affine transform."""
- with tf.variable_scope(scope):
- n_state = x.shape[-1].value
- g = tf.get_variable('g', [n_state], initializer=tf.constant_initializer(1))
- b = tf.get_variable('b', [n_state], initializer=tf.constant_initializer(0))
- u = tf.reduce_mean(x, axis=axis, keepdims=True)
- s = tf.reduce_mean(tf.square(x-u), axis=axis, keepdims=True)
- x = (x - u) * tf.rsqrt(s + epsilon)
- x = x*g + b
- return x
-
- def split_states(x, n):
- """Reshape the last dimension of x into [n, x.shape[-1]/n]."""
- *start, m = shape_list(x)
- return tf.reshape(x, start + [n, m//n])
-
- def merge_states(x):
- """Smash the last two dimensions of x into a single dimension."""
- *start, a, b = shape_list(x)
- return tf.reshape(x, start + [a*b])
-
- def conv1d(x, scope, nf, *, w_init_stdev=0.02):
- with tf.variable_scope(scope):
- *start, nx = shape_list(x)
- w = tf.get_variable('w', [1, nx, nf], initializer=tf.random_normal_initializer(stddev=w_init_stdev))
- b = tf.get_variable('b', [nf], initializer=tf.constant_initializer(0))
- c = tf.reshape(tf.matmul(tf.reshape(x, [-1, nx]), tf.reshape(w, [-1, nf]))+b, start+[nf])
- return c
-
- def attention_mask(nd, ns, *, dtype):
- """1's in the lower triangle, counting from the lower right corner.
- Same as tf.matrix_band_part(tf.ones([nd, ns]), -1, ns-nd), but doesn't produce garbage on TPUs.
- """
- i = tf.range(nd)[:,None]
- j = tf.range(ns)
- m = i >= j - ns + nd
- return tf.cast(m, dtype)
-
-
- def attn(x, scope, n_state, *, past, hparams):
- assert x.shape.ndims == 3 # Should be [batch, sequence, features]
- assert n_state % hparams.n_head == 0
- if past is not None:
- assert past.shape.ndims == 5 # Should be [batch, 2, heads, sequence, features], where 2 is [k, v]
-
- def split_heads(x):
- # From [batch, sequence, features] to [batch, heads, sequence, features]
- return tf.transpose(split_states(x, hparams.n_head), [0, 2, 1, 3])
-
- def merge_heads(x):
- # Reverse of split_heads
- return merge_states(tf.transpose(x, [0, 2, 1, 3]))
-
- def mask_attn_weights(w):
- # w has shape [batch, heads, dst_sequence, src_sequence], where information flows from src to dst.
- _, _, nd, ns = shape_list(w)
- b = attention_mask(nd, ns, dtype=w.dtype)
- b = tf.reshape(b, [1, 1, nd, ns])
- w = w*b - tf.cast(1e10, w.dtype)*(1-b)
- return w
-
- def multihead_attn(q, k, v):
- # q, k, v have shape [batch, heads, sequence, features]
- w = tf.matmul(q, k, transpose_b=True)
- w = w * tf.rsqrt(tf.cast(v.shape[-1].value, w.dtype))
-
- w = mask_attn_weights(w)
- w = softmax(w)
- a = tf.matmul(w, v)
- return a
-
- with tf.variable_scope(scope):
- c = conv1d(x, 'c_attn', n_state*3)
- q, k, v = map(split_heads, tf.split(c, 3, axis=2))
- present = tf.stack([k, v], axis=1)
- if past is not None:
- pk, pv = tf.unstack(past, axis=1)
- k = tf.concat([pk, k], axis=-2)
- v = tf.concat([pv, v], axis=-2)
- a = multihead_attn(q, k, v)
- a = merge_heads(a)
- a = conv1d(a, 'c_proj', n_state)
- return a, present
-
-
- def mlp(x, scope, n_state, *, hparams):
- with tf.variable_scope(scope):
- nx = x.shape[-1].value
- h = gelu(conv1d(x, 'c_fc', n_state))
- h2 = conv1d(h, 'c_proj', nx)
- return h2
-
-
- def block(x, scope, *, past, hparams):
- with tf.variable_scope(scope):
- nx = x.shape[-1].value
- a, present = attn(norm(x, 'ln_1'), 'attn', nx, past=past, hparams=hparams)
- x = x + a
- m = mlp(norm(x, 'ln_2'), 'mlp', nx*4, hparams=hparams)
- x = x + m
- return x, present
-
- def past_shape(*, hparams, batch_size=None, sequence=None):
- return [batch_size, hparams.n_layer, 2, hparams.n_head, sequence, hparams.n_embd // hparams.n_head]
-
- def expand_tile(value, size):
- """Add a new axis of given size."""
- value = tf.convert_to_tensor(value, name='value')
- ndims = value.shape.ndims
- return tf.tile(tf.expand_dims(value, axis=0), [size] + [1]*ndims)
-
- def positions_for(tokens, past_length):
- batch_size = tf.shape(tokens)[0]
- nsteps = tf.shape(tokens)[1]
- return expand_tile(past_length + tf.range(nsteps), batch_size)
-
-
- def model(hparams, X, past=None, scope='model', reuse=False):
- with tf.variable_scope(scope, reuse=reuse):
- results = {}
- batch, sequence = shape_list(X)
-
- wpe = tf.get_variable('wpe', [hparams.n_ctx, hparams.n_embd],
- initializer=tf.random_normal_initializer(stddev=0.01))
- wte = tf.get_variable('wte', [hparams.n_vocab, hparams.n_embd],
- initializer=tf.random_normal_initializer(stddev=0.02))
- past_length = 0 if past is None else tf.shape(past)[-2]
- h = tf.gather(wte, X) + tf.gather(wpe, positions_for(X, past_length))
-
- # Transformer
- presents = []
- pasts = tf.unstack(past, axis=1) if past is not None else [None] * hparams.n_layer
- assert len(pasts) == hparams.n_layer
- for layer, past in enumerate(pasts):
- h, present = block(h, 'h%d' % layer, past=past, hparams=hparams)
- presents.append(present)
- results['present'] = tf.stack(presents, axis=1)
- h = norm(h, 'ln_f')
-
- # Language model loss. Do tokens <n predict token n?
- h_flat = tf.reshape(h, [batch*sequence, hparams.n_embd])
- logits = tf.matmul(h_flat, wte, transpose_b=True)
- logits = tf.reshape(logits, [batch, sequence, hparams.n_vocab])
- results['logits'] = logits
- return results
Llama2是Facebook开源的大模型,原代码是Python加Pytorch实现的,总代码也就二千多行。Github上有Llama2的纯C代码的预训练代码,也就一千多行。本身Transformer的结构也不复杂,再者很多代码由基本库如Pytorch实现了,所以整个大模型做的事情并不复杂。
目前的大模型,基本都是在Transformer这个架构上进行了改进,改进位置编码,改进注意力机制,增加层级,优化损失函数等。这样看来,国产大语言模型性能低的原因主要还是在高质量训练数据不足上。因为理论上的差异没有那么多,花时间是可以追上的。
之前字节跳动大量使用chatgpt的API生成数据用来训练,后来被OpenAI发现,然后禁止了。后来大量购买Microsoft Azure的资源,然后又可以使用chatgpt的API生成数据用来训练。
从理论上来看,大模型研发确实不需要太多人。相反像挖掘数据清洗数据需要一些人,另外就是训练是大量的GPU资源。OpenAi在研发GPT3时,也只有100多从,包括研究、工程、产品等。目前很多的Kimi AI的月之暗面团队目前只有80来人。
月之暗面的创始人杨植麟认为大语言模型的“第一性原理”是Scaling law。Scaling law是指模型大小、数据集大小和用于训练的计算浮点数(大模型的权重参数是浮点类型,需要浮点计算,所以大模型的理论算力会用TFLOPS<每秒浮点运算次数>来表示)的增加,模型的性能会提高。
工程应用机器学习,并不会涉及非常深入的数据知识,多数只需要知道一个基本的概念和公式计算即可。其主要有:
推荐吴军的《数学之美》,一本简单易懂的来讲解数学知识的应用。其本质就是机器学习算法的基本原理。
本文重在介绍机器学习和大语言模型的基本原理。在科学研究上,机器学习和大语言模型的每一个环节都可以优化研究。如何提升训练的效率,降低训练过程中的过拟合是大语言模型的关键,Transformer的出现为大语言模型的可行性提供了技术支持,OpenAI的chatgpt的出现,证明了加大模型参数可以让模型的能力出现质的提升。大语言模型未来将会继承发展,会让人工智能走进世界每一个角落。
几个开源模型的部署,都是基于云计算机验证的。在本地计算,需要安装一些环境,如CUDA等。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。