自己動手做 chatGPT:向量的概念和相關操作

chatGPT 的橫空出世給人工智能注入一針強心劑,它是歷史上以最短時間達到一億用戶的應用。chatGPT 的能力相當驚人,它可以用相當流利的語言和人對話,同時能夠對用戶提出的問題給出相當順暢的答案。它的出現已經給各個行業帶來不小衝擊,據說有很多公司已經使用 chatGPT 來替代人工,於是引起了不少裁員事件。

chatGPT 是人類科技史上一個里程碑。它基於一種叫大語言模型的技術,使得計算機具備了相當於人乃至超越人的能力,chatGPT 的發明者 openAI 據說在推進下一代模型的開發,據說已經能達到通用 AI 的程度,我對此表示懷疑。無論如何基於大模型技術的 AI 將人類帶入一個新時期,我們必須有所準備,我們既不需要過分狂熱,以爲它又是一個暴富風口;也不能漠不關心,認爲它完全與自己無關,如果你從事信息技術行業,你必須要特意留一手,如果它真的是進入新紀元的鑰匙,那麼我們不會被落下,如果只是一陣騷動,那麼基於技多不壓身的原則,咱花點心思多學一門技術也不虧。

我們這個系列着重於探究發明出 chatGPT 的技術,我們基於可用的算力和數據從零開始做一個 “類”chatGPT,也就是我們做出來的模型不可能有 chatGPT 那麼厲害,但是我們掌握和使用的原理跟它一樣,只不過我們沒有對應的資源訓練它而已。同時 chatGPT 底層還有一種技術叫 transformer,基於這個技術我們可以把 chatGPT 的開源模型拿過來,然後使用小樣本數據就能將其訓練成某個特定領域的 AI 專家,於是 chatGPT 就能爲我所用。

這個系列分爲兩部分,首先是介紹 NLP(自然語言處理)的基本原理和技術,然後我們看看如何使用開源的大語言模型進行特定的開發,由此打造出屬於我們自己的 chatGPT. 首先需要聲明的是,涉及到人工智能和深度學習,它具有一定的門檻,那就是你至少要比較熟練大學階段的高數,你要了解微積分相關內容,熟悉向量,矩陣等線性代數概念,要不然很難在這個領域發展。

現在我們回到技術層面。人工智能要解決的主要是傳統算法處理不了的問題,傳統算法之所以對一些問題束手無措,主要是因爲要處理的對象無法使用結構化的數據結構進行表達。例如給定一張人臉圖片,我們如何使用傳統數據結構來描述呢,是使用鏈表,二叉樹,哈希表嗎,顯然不行。由於這個原因,傳統算法處理不了這些範疇的問題。那麼人工智能怎麼用數據區描述例如人臉,單詞都這些對象呢,方法是用向量,面對的對象性質越複雜,向量的長度就越大,例如人臉通常用長度爲 256 或者更大的實數向量來表示。對 NLP 而言,它處理的對象是文本,因此它會使用向量來表示文本的基本單位,如果文本是英語,那麼就用向量來表示單詞,如果是中文,那麼就用向量表示一個字。

我們看一個具體例子,假設我們有一段英語文本:

Times flies like an arrow 
Fruit flies like a banana.

顯然傳統數據結構是無法表達上面的句子和單詞,因此我們轉向向量來表達。首先我們把所有單詞轉換爲小寫,然後將其排列起來,單詞排列的先後順序沒有關係,於是有:

time fruit flies like a an arrow banana

接下來我們使用一種叫 one-hot-vector 的向量來表示單詞,可以看到上面有 8 個不同的單詞,因此向量包含 8 個元素,由於 time 排在第一個,於是我們把向量第一個元素設置爲 1,其他元素設置爲 0,因此 time 的向量表示就是 [1,0,0,0,0,0,0], 同理 fruit 排在第 2 位,因此它對應的向量就是第二個元素爲 1,其他元素爲 0,於是其對應向量爲[0,1,0,0,0,0,0,0],其他以此類推。這種對單詞的向量描述方式在我們後面的深度學習算法中會發揮很大作用。對於一個句子而言,它的向量描述方式就是把單詞對應的向量進行“或” 操作,例如句子 like a banana, 組成它三個單詞的向量是 [0,0,0,1,0,0,0,0], [0,0,0,0,1,0,0,0],[0,0,0,0,0,0,0,1], 進行“或” 操作後結果就是[0,0,0,1,1,0,0,1], 我們用代碼來實踐看看:

from sklearn.feature_extraction.text import CountVectorizer
import seaborn as sns 

corpus = ['Time flies flies like an arraw.', 'Friut flies like a banana']
one_hot_vectorizer = CountVectorizer(binary = True)
one_hot = one_hot_vectorizer.fit_transform(corpus).toarray()
vocab = one_hot_vectorizer.get_feature_names_out()
sns.heatmap(one_hot, annot=True, cbar = False, xticklabels = vocab, yticklabels=['Sentence 1','Sentence 2'])

上面代碼運行後結果如下:

從上圖我們能看到圖形化的,兩個句子對應的向量表示,如果給的單詞在句子中出現了,他們向量對應位置設置爲 1,要不然就設置爲 0.one-hot-vector 只是對單詞或句子最基本的數學描述方式,事實上在不同的文本或應用場景下,單詞或句子的向量絕對不會那麼簡單,他們依然需要以向量來表示,但是向量的長度和每個元素的取值都得靠深度學習算法來分析出來,具體情況在後面章節詳細闡明。

下面我們看看深度學習的基本原理。有過微積分基礎的同學會瞭解,對於一個連續函數 f(x), 如果在某一點求導所得結果爲 0:f’(x)=0,那麼這個點就可能是在局部範圍內的最大值或最小值。深度學習本質上就是通過微分求極小值的過程,只不過它對應的函數包含不止一個變量,例如 chatGPT 對應的模型就是一個包含 1750 億個參數的函數,訓練的目的就是找出這 1750 億參數的合適取值,這樣它才能根據輸入的句子給出合適的回覆,因此用於它訓練的算力和數據無疑是及其巨大的,以下我們給出深度學習網絡訓練的基本流程:

對深度學習基本原理不熟悉的同學可以參考《神經網絡與深度學習實戰》,或者我在雲課堂上的課程:http://m.study.163.com/provider/7600199/index.htm?share=2&shareId=7600199

下面我們看運算圖的概念。在上圖中 “模型” 其實可以使用傳統數據結構中的 “圖論” 來表示。“含有很多個參數的函數”其實可以使用鏈表來表示,當算法對函數的參數進行求導時,這些運算就可以通過鏈表來完成,我們看一個具體例子,對於函數 y = wx+b,我們可以用鏈表表示如下:

參數 x, w, b, y 使用矩形節點表示,運算符則使用圓形節點表示。箭頭上的值表示對應參數的值,他們經過圓形節點後執行對應運算然後輸出結果。前面我們提到過 chatGPT 的參數有 1370 億個,那意味着其對應的運算圖將非常龐大和複雜,因此我們通常使用特定框架來完成運算圖的構建以及執行基於其的運算,常用的框架有 tensorflow, pytorch 還有百度的飛槳,目前用的比較多的還是 meta 的 pytorch 框架。

在具體的深度學習應用中,參數節點往往不會像上面那麼簡單,他們通常是高維度向量,我們上面顯示的是 0 維度的向量,也就是他們是單個參數,在實際應用中 x,b 通常是一維向量,w 是二維向量也就是矩陣。如果我們要處理的輸入是圖片,那麼 x 可能就是二維向量,如果處理的是視頻,那麼可能就是三維向量,因爲視頻是具有時間維度的圖片,對於 NLP 而言,也就是自然語言處理而言,輸入的 x 通常是一維或者二維向量. 接下來我們看看如何在基於 pytorch 框架的基礎上實現向量的各自運算。

我們所有代碼將運行在谷歌的 colab 開發環境,這個環境好在於集成了 pytorch 框架,同時還能讓我們免費使用 gpu 加快運算效率。首先我們用一段代碼展示如何使用 pytorch 創建各種維度的向量:

import torch 
def describeTensor(tensor):
  #輸出向量的維度,類型,以及元素值
  print(f"Type: {tensor.type()}")
  print(f"shape/size: {tensor.shape}")
  print(f"values: {tensor}")

describeTensor(torch.Tensor(2,3)) #創建二維向量,也就是2*3矩陣

上面代碼執行後輸出如下:

Type: torch.FloatTensor
shape/size: torch.Size([2, 3])
values: tensor([[-7.9076e-20,  4.5766e-41, -7.7950e-20],
        [ 4.5766e-41, -7.7948e-20,  4.5766e-41]])

這裏需要注意的是,向量中每個元素的值是隨意初始化的,一般情況下向量初始值是什麼不重要。在深度學習中,我們往往需要對輸入數據進行正規化處理,也就是把向量元素進行加工,使得他們加總的值爲 1,我們看個例子:

#對向量進行正規化處理,也就是向量元素加起來等於1
describeTensor(torch.randn(2,3))

上面代碼運行後結果如下:

ype: torch.FloatTensor
shape/size: torch.Size([2, 3])
values: tensor([[ 0.5474,  0.7511,  0.7454],
        [ 0.7795, -1.8067,  0.4035]])

不難看到,上面輸出的每個向量,它對應元素加起來值正好等於 1.0. 在某些情況下,我們創建向量後,希望初始化向量中每個分量的值,因此我們可以如下操作:

#把向量每個元素初始化爲0
describeTensor(torch.zeros(2,3))
#把每個元素初始化爲1
x = torch.ones(2,3)
#把每個元素設置爲5,下劃線表示函數對應的操作會直接作用在給定的向量上
x.fill_(5)
describeTensor(x)

上面代碼執行後結果如下:

Type: torch.FloatTensor
shape/size: torch.Size([2, 3])
values: tensor([[0., 0., 0.],
        [0., 0., 0.]])
Type: torch.FloatTensor
shape/size: torch.Size([2, 3])
values: tensor([[5., 5., 5.],
        [5., 5., 5.]])

在 python 的數值應用中,numpy 是必不可少的函數庫,因此我們能把 numpy 對應的列表直接轉換成向量,例如下面的方式

import numpy as np
npy = np.random.rand(2,3)
#直接從numpy向量轉換爲pytorch向量
describeTensor(torch.from_numpy(npy))

上面代碼運行後輸出結果如下:

Type: torch.DoubleTensor
shape/size: torch.Size([2, 3])
values: tensor([[0.2552, 0.2467, 0.9570],
        [0.3357, 0.8942, 0.2779]], dtype=torch.float64)

注意看,這裏向量的類型變成了 double 而不是 float。另外我們還可以將運算作用在向量上,例如把向量的行相加得到一個一維向量,例如下面代碼:

x = torch.Tensor([[1, 2, 3], [4, 5, 6]])
#將向量按照行相加,
y = torch.sum(x, dim = 0)
describeTensor(y)
#將向量按照列相加,這個稍微有點抽象,它的做法是想取出一行,然後將所有元素加總,然後取出第二行,將所有元素加總
#第一行的元素爲[1,2,3]加總後就是1+2+3 = 6, 第二行是【4,5,6】加總後就是4+5+6=15,結果就是一個包含兩個元素的1維向量[6,15]
z =  torch.sum(x, dim = 1)
describeTensor(z)

上面代碼運行後結果如下:

Type: torch.FloatTensor
shape/size: torch.Size([3])
values: tensor([5., 7., 9.])
Type: torch.FloatTensor
shape/size: torch.Size([2])
values: tensor([ 6., 15.])

針對向量的運算,比較令人混亂的是對向量進行轉換,這些操作統稱爲 indexing, slicing, 和 joining,我們看幾個具體例子:

x = torch.Tensor([[0, 1, 2], [3,4,5]])
'''
:1是針對行進行選取,:1表示選取所有下標不超過1的行,由於向量只有兩行,因此只有第0行滿足條件,於是:1的作用是把第0行選取出來。
:2是針對列進行選取,它表示選取下標不超過2的列,由於前面我們已經選取了第0行,因此:2表示在第0行基礎上選出下標不超過2的列,於是
操作結果就是[0,1]
'''
describeTensor(x[:1, :2])

上面代碼運行後結果如下:

Type: torch.FloatTensor
shape/size: torch.Size([1, 2])
values: tensor([[0., 1.]])

我們再看看如何針對高維向量,選取它指定的列:

indices = torch.LongTensor([0, 2])
'''
dim = 1 ,表示操作將針對列進行,(行對應的dim爲0),index指定將給定下標的列選取出來
由於indices對應的值爲0,2,因此下面操作就是將第0列和第2列選取出來,於是結果就是[[0,2],[3,5]]
由於indices對應的數值必須是整形,因此我們設置向量的類型爲long,也就是每個分量的類型是int64
'''
describeTensor(torch.index_select(x, dim = 1, index = indices))

上面代碼運行後結果如下:

Type: torch.FloatTensor
shape/size: torch.Size([2, 2])
values: tensor([[0., 2.],
        [3., 5.]])

我們再看一個更令人困惑的操作:

indices = torch.LongTensor([0, 0])
'''
本次操作作用於向量的行,也就是dim = 0,我們要把下標爲indices的行選取出來,由於
indices對應的參數爲兩個0,因此下面操作把第0行選取兩次,於是形成結構就是[[0,1,2],[0,1,2]]
'''
describeTensor(torch.index_select(x, dim = 0, index = indices))

上面操作運行後結果如下;

Type: torch.FloatTensor
shape/size: torch.Size([2, 3])
values: tensor([[0., 1., 2.],
        [0., 1., 2.]])

我們還能同時選取向量的行和列,例如:

x = torch.Tensor([[0, 1, 2], [3,4,5]])
#用於設置下標的向量必須是整形,而向量默認是浮點型,因此如果向量用於存儲下標,那麼需要明確生成long型的向量
row_indecies = torch.LongTensor([0,1])
col_indecies = torch.LongTensor([0, 1])
'''
先選取第0,1兩行,然後選取第0行的第0列,接着選取第1行的第1列,所得結果就是[0,4]
'''
describeTensor(x[row_indecies, col_indecies])

上面代碼運行後所得結果爲:

Type: torch.FloatTensor
shape/size: torch.Size([2])
values: tensor([0., 4.])

不同向量之間還能進行組合,合併等操作,這些操作也很容易讓人頭疼和困惑,我們看幾個例子:

x = torch.Tensor([[1,2,3], [4,5,6]])
y = torch.Tensor([[7,8,9], [10, 11, 12]])
'''
將x,y在行的維度上疊加,這個操作要求兩個向量要有相同數量的列
'''
z = torch.cat([x, y], dim = 0)
describeTensor(z)

上面代碼運行後所得結果如下:

Type: torch.FloatTensor
shape/size: torch.Size([4, 3])
values: tensor([[ 1.,  2.,  3.],
        [ 4.,  5.,  6.],
        [ 7.,  8.,  9.],
        [10., 11., 12.]])

同樣我們可以將兩個向量的列進行疊加:

x = torch.Tensor([[1,2,3], [4,5,6]])
y = torch.Tensor([[7,8,9], [10, 11, 12]])
'''
將兩個向量在列的維度進行疊加,也就是把兩個向量的第一行合成一行[1,2,3]+[7,8,9]->[1,2,3,7,8,9],
然後把兩個向量的第二行合成一行[4,5,6]+[10,11,12]->[4,5,6,10,11,12],
老實說我對這個操作也感覺困惑
'''
z = torch.cat([x, y], dim = 1)
describeTensor(z)

上面代碼運行結果爲:

Type: torch.FloatTensor
shape/size: torch.Size([2, 6])
values: tensor([[ 1.,  2.,  3.,  7.,  8.,  9.],
        [ 4.,  5.,  6., 10., 11., 12.]])

我們還能將多個向量作爲新向量的分量,例如:

x = torch.Tensor([[1,2,3], [4,5,6]])
y = torch.Tensor([[7,8,9], [10, 11, 12]])
'''
把兩個向量疊起來,也就是把兩個向量分別作爲新向量的分量,由於現在向量維度是2行3列,
因此把這兩個向量作爲新向量的分量時,新向量就會有2個分量,同時每個分量的維度就是2行3列,
於是新向量的維度就是[2, 2, 3]
'''
z = torch.stack([x,y])
describeTensor(z)

上面操作結果爲:

Type: torch.FloatTensor
shape/size: torch.Size([2, 2, 3])
values: tensor([[[ 1.,  2.,  3.],
         [ 4.,  5.,  6.]],

        [[ 7.,  8.,  9.],
         [10., 11., 12.]]])

我們還能對向量進行運算,例如讓某一行或某一列乘以一個值,例如:

#初始化三行兩列的矩陣,並讓每個分量取值1
x = torch.ones(3,2)
'''
x[:, 1]表示選取向量所有行,同時選取行中下標爲1的元素,讓這些元素加上數值1
'''
x[:, 1] += 1
describeTensor(x)

上面代碼運行後結果爲:

Type: torch.FloatTensor
shape/size: torch.Size([3, 2])
values: tensor([[1., 2.],
        [1., 2.],
        [1., 2.]])

對應高維向量,我們還能把他們當做矩陣來相乘

x = torch.Tensor([[1,2], [3,4]])
y = torch.Tensor([[5,6], [7,8]])
#將兩個向量執行矩陣乘法,第一個元素是第一個向量的第一行乘以第二個向量的第一列,也就是[1,2] X [5,7]  = 1*5 + 2*7 = 19,以此類推
z = torch.mm(x, y)
describeTensor(z)

上面代碼執行後所得結果我:

Type: torch.FloatTensor
shape/size: torch.Size([2, 2])
values: tensor([[19., 22.],
        [43., 50.]])

下一節我們看看自然語言處理的深度學習算法中,我們需要涉及的一些概念和流程,更多信息請在 b 站搜索 coding 迪斯尼。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NBClLI-toPZcIfQXPO6q5g