ChatGPT最近大火?教你实现破产版ChatGPT(一)数据预处理
目錄
一.前言
二.下載數據文件
三.導包并設置使用GPU
四.加載和預處理數據
五.為模型準備數據
一.前言
最近ChatGPT大火,成功破圈,到底是個啥?
簡單說,它是一個模型,一個語言模型!它是以對話方式與人進行交互的AI語言模型!
在很早以前,國內的大廠百度,就開發過“文心·NLP模型”等語言處理模型,比如用文心·NLP模型生成段落和文章,能做到開篇和文末點題,首位呼應,對文本相關內容進行引用等……
文心·NLP模型總體效果一般,有點差強人意,特別是在人類文字和人類語言的理解方面,有很多不融洽,輸出的內容很多都存在銜接生硬牽強的問題。
也正因為如此,所以它也一直不溫不火,只有少數相關公司和研究人工智能與機器學習的用戶知道它,以前我了解后,覺得還有很長的路要走。
但這幾天的ChatGPT明顯不一樣,對很多不懂不關注相關技術的人,都展示出了極強的吸引力,ChatGPT短短一周不到的時間,用戶達到百萬級,成功破圈,說明它是有值得研究的地方的。
在本教程中,我們探索一個好玩有趣的循環的序列到序列(sequence-to-sequence)的模型用例。我們將用Cornell Movie-Dialogs Corpus 處的電影劇本來訓練一個簡單的聊天機器人。
在人工智能研究領域中,對話模型是一個非常熱門的話題。聊天機器人可以在各種設置中找到,包括客戶服務應用和在線幫助。這些機器人通常 由基于檢索的模型提供支持,這些模型的輸出是某些形式問題預先定義的響應。在像公司IT服務臺這樣高度受限制的領域中,這些模型可能足夠了, 但是,對于更一般的用例它們還不夠健壯。讓一臺機器與多領域的人進行有意義的對話是一個遠未解決的研究問題。最近,深度學習熱潮已經允許 強大的生成模型,如谷歌的神經對話模型Neural Conversational Model,這標志著向多領域生成對話模型邁出了一大步。 在本教程中,我們將在PyTorch中實現這種模型。
教程要點
- 對Cornell Movie-Dialogs Corpus數據集的加載和預處理
- 用Luong attention mechanism(s)實現一個sequence-to-sequence模型
- 使用小批量數據聯合訓練解碼器和編碼器模型
- 實現貪婪搜索解碼模塊
- 與訓練好的聊天機器人互動
二.下載數據文件
下載數據文件點擊這里并將其放入到當前目錄下的data/ 文件夾下。
三.導包并設置使用GPU
from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literalsimport torch from torch.jit import script, trace import torch.nn as nn from torch import optim import torch.nn.functional as F import csv import random import re import os import unicodedata import codecs from io import openimport itertools import mathUSE_CUDA = torch.cuda.is_available() device = torch.device("cuda" if USE_CUDA else "cpu")四.加載和預處理數據
#**********************************2.加載和預處理數據********************************** ''' 下一步就是格式化處理我們的數據文件并將數據加載到我們可以使用的結構中。 Cornell Movie-Dialogs Corpus是一個豐富的電影角色對話數據集: * 10,292 對電影角色之間的220,579次對話 * 617部電影中的9,035個電影角色 * 總共304,713發言量 這個數據集龐大而多樣,在語言形式、時間段、情感上等都有很大的變化。我們希望這種多樣性使我們的模型能夠適應多種形式的輸入和查詢。 首先,我們通過數據文件的某些行來查看原始數據的格式 ''' corpus_name = "cornell movie-dialogs corpus" corpus = os.path.join("data", corpus_name)def printLines(file, n=10):with open(file, 'rb') as datafile:lines = datafile.readlines()#輸出前n行的數據for line in lines[:n]:print(line)#printLines(os.path.join(corpus, "movie_lines.txt"))#**************************************2.1創建格式化數據文件 start ************************************** ''' 為了方便起見,我們將創建一個格式良好的數據文件,其中每一行包含一個由tab制表符分隔的查詢語句和響應語句對。 以下函數便于解析原始 movie_lines.txt 數據文件。 * loadLines:將文件的每一行拆分為字段(lineID, characterID, movieID, character, text)組合的字典 * loadConversations :根據movie_conversations.txt將loadLines中的每一行數據進行歸類 * extractSentencePairs: 從對話中提取句子對 ''' # 將文件的每一行拆分為字段字典 #fields中是各列字段的名字,即["lineID", "characterID", "movieID", "character", "text"] def loadLines(fileName, fields):lines = {}with open(fileName, 'r', encoding='iso-8859-1') as f:for line in f:values = line.split(" +++$+++ ")# Extract fieldslineObj = {}for i, field in enumerate(fields):lineObj[field] = values[i]#每一個鍵值對對應一行數據,鍵是這一行的lineID,值是這一行對應的數據對象,即lineObj是字典(lines)里的字典lines[lineObj['lineID']] = lineObj#lines是對整個movie_lines.txt文件操作之后返回的含有格式化原數據的字典return lines# 將 `loadLines` 中的行字段分組為基于 *movie_conversations.txt* 的對話 #utterance話語 #fields中是各列字段的名字,即["character1ID", "character2ID", "movieID", "utteranceIDs"] def loadConversations(fileName, lines, fields):conversations = []with open(fileName, 'r', encoding='iso-8859-1') as f:for line in f:values = line.split(" +++$+++ ")# Extract fields,和上面的那個函數的lineObj一個道理,即把movie_conversations.txt的每行的數據提取到一個字典中convObj = {}for i, field in enumerate(fields):convObj[field] = values[i]# Convert string to list (convObj["utteranceIDs"] == "['L598485', 'L598486', ...]")#eval函數的作用自行百度,就是會把字符串里的表達式進行計算lineIds = eval(convObj["utteranceIDs"])# Reassemble lines,Reassemble:重新組裝convObj["lines"] = [] #給字典新加一個鍵值對,此鍵為linesfor lineId in lineIds:#即根據lineId把上面那個函數格式化的每行對應的字典對象添加到字典convObj的lines對應的值中convObj["lines"].append(lines[lineId])conversations.append(convObj)# conversations是對整個movie_conversations.txt文件操作之后返回的含有格式化原數據的字典return conversations# 從對話中提取一對句子 def extractSentencePairs(conversations):qa_pairs = []for conversation in conversations:# Iterate over all the lines of the conversationfor i in range(len(conversation["lines"]) - 1): # We ignore the last line (no answer for it)#strip函數返回刪除前導和尾隨空格的字符串副本。如果給定了chars而不是None,則刪除chars中的字符。inputLine = conversation["lines"][i]["text"].strip()targetLine = conversation["lines"][i+1]["text"].strip()# Filter wrong samples (if one of the lists is empty)if inputLine and targetLine:qa_pairs.append([inputLine, targetLine])#提取了文件中的所有對話return qa_pairs#******************************現在我們將調用這些函數來創建文件,我們命名為formatted_movie_lines.txt。****************************** # 定義新文件的路徑(待生成) datafile = os.path.join(corpus, "formatted_movie_lines.txt")''' #delimiter:分隔符 delimiter = '\t' #print('delimiter:',delimiter) # codecs.decode()方法的使用參考https://zhuanlan.zhihu.com/p/377436438 delimiter = str(codecs.decode(delimiter, "unicode_escape")) #print('delimiter:',delimiter)# 初始化行dict,對話列表和字段ID lines = {} conversations = [] MOVIE_LINES_FIELDS = ["lineID", "characterID", "movieID", "character", "text"] MOVIE_CONVERSATIONS_FIELDS = ["character1ID", "character2ID", "movieID", "utteranceIDs"]# 加載行和進程對話,即調用函數進行格式化 print("\nProcessing corpus...") lines = loadLines(os.path.join(corpus, "movie_lines.txt"), MOVIE_LINES_FIELDS) print("\nLoading conversations...") conversations = loadConversations(os.path.join(corpus, "movie_conversations.txt"),lines, MOVIE_CONVERSATIONS_FIELDS)# 寫入新的csv文件 print("\nWriting newly formatted file...") with open(datafile, 'w', encoding='utf-8') as outputfile:writer = csv.writer(outputfile, delimiter=delimiter, lineterminator='\n')pairs=extractSentencePairs(conversations)print('句子對數為:',pairs.__len__())for pair in pairs:writer.writerow(pair)# 打印一個樣本的行 print("\nSample lines from file:") printLines(datafile) ''' #**************************************2.1創建格式化數據文件 end **************************************#*******************************2.2 加載和清洗數據 start ******************************* ''' 我們下一個任務是創建詞匯表并將查詢/響應句子對(對話)加載到內存。 注意我們正在處理詞序,這些詞序沒有映射到離散數值空間。因此,我們必須通過數據集中的單詞來創建一個索引。 為此我們創建了一個Voc類,它會存儲從單詞到索引的映射、索引到單詞的反向映射、每個單詞的計數和總單詞量。 這個類提供向詞匯表中添加單詞的方法(addWord)、添加所有單詞到句子中的方法 (addSentence) 和清洗不常見的單詞方法(trim)。更多的數據清洗在后面進行。 ''' # 默認詞向量 PAD_token = 0 # Used for padding short sentences SOS_token = 1 # Start-of-sentence token EOS_token = 2 # End-of-sentence tokenclass Voc:def __init__(self, name):self.name = nameself.trimmed = False#單詞的索引號(鍵是單詞,值是索引)self.word2index = {}#各個單詞的數量(鍵是單詞,值是數量)self.word2count = {}#通過索引可以找到單詞self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}#單詞序號,012已經被使用,所以就從3開始了self.num_words = 3 # Count SOS, EOS, PAD#把句子中的每一個單詞查出來執行添加單詞的操作def addSentence(self, sentence):for word in sentence.split(' '):self.addWord(word)def addWord(self, word):##如果之前沒遇到過這個單詞if word not in self.word2index:#這個單詞的序號self.word2index[word] = self.num_words#這個單詞的總數self.word2count[word] = 1#通過單詞序號找到單詞self.index2word[self.num_words] = wordself.num_words += 1else:#如果之前已經遇到了這個單詞那么只對單詞總數+1self.word2count[word] += 1# 刪除低于特定計數閾值的單詞,即單詞出現頻率太低def trim(self, min_count):if self.trimmed:returnself.trimmed = Truekeep_words = []for k, v in self.word2count.items():if v >= min_count:keep_words.append(k)print('keep_words {} / {} = {:.4f}'.format(#len(self.word2index)即代表總的單詞數len(keep_words), len(self.word2index), len(keep_words) / len(self.word2index)))# 重初始化字典self.word2index = {}self.word2count = {}self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}self.num_words = 3 # Count default tokensfor word in keep_words:self.addWord(word)''' 現在我們可以組裝詞匯表和查詢/響應語句對。在使用數據之前,我們必須做一些預處理。 首先,我們必須使用unicodeToAscii將 unicode 字符串轉換為 ASCII。 然后,我們應該將所有字母轉換為小寫字母并清洗掉除基本標點之 外的所有非字母字符 (normalizeString)。 最后,為了幫助訓練收斂,我們將過濾掉長度大于MAX_LENGTH 的句子 (filterPairs)。 ''' MAX_LENGTH = 10 # Maximum sentence length to consider# 將Unicode字符串轉換為純ASCII,多虧了https://stackoverflow.com/a/518232/2809427 def unicodeToAscii(s):return ''.join(c for c in unicodedata.normalize('NFD', s)if unicodedata.category(c) != 'Mn')# Lowercase, trim, and remove non-letter characters def normalizeString(s):s = unicodeToAscii(s.lower().strip())#re.sub函數作用:https://blog.csdn.net/weixin_44799217/article/details/115100715s = re.sub(r"([.!?])", r" \1", s)s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)s = re.sub(r"\s+", r" ", s).strip()return s# 初始化Voc對象 和 格式化pairs對話存放到list中 # datafile就是我們上面生成的formatted_movie_lines.txt #corpus_name就是"cornell movie-dialogs corpus" def readVocs(datafile, corpus_name):print("Reading lines...")# Read the file and split into lineslines = open(datafile, encoding='utf-8').read().strip().split('\n')# Split every line into pairs and normalizepairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]voc = Voc(corpus_name)return voc, pairs# 如果對 'p' 中的兩個句子都低于 MAX_LENGTH 閾值,則返回True,即合法 #p是句子對 def filterPair(p):# Input sequences need to preserve the last word for EOS token#一對句子中只要有一個句子里的單詞數超過≥MAX_LENGTH就不合法!所以很多句子都被篩去了return len(p[0].split(' ')) < MAX_LENGTH and len(p[1].split(' ')) < MAX_LENGTH# 過濾滿足條件的 pairs 對話 def filterPairs(pairs):# 返回合法句子對的集合return [pair for pair in pairs if filterPair(pair)]# 使用上面定義的函數,返回一個填充的voc對象和對列表 def loadPrepareData(corpus, corpus_name, datafile, save_dir):print("Start preparing training data ...")voc, pairs = readVocs(datafile, corpus_name)print("Read {!s} sentence pairs".format(len(pairs)))pairs = filterPairs(pairs)print("Trimmed to {!s} sentence pairs".format(len(pairs)))print("Counting words...")for pair in pairs:voc.addSentence(pair[0])voc.addSentence(pair[1])print("Counted words:", voc.num_words)return voc, pairs# 加載/組裝voc和對 save_dir = os.path.join("data", "save") voc, pairs = loadPrepareData(corpus, corpus_name, datafile, save_dir) # 打印一些對進行驗證 # print("\npairs:") # for pair in pairs[:10]: # print(pair)''' 另一種有利于讓訓練更快收斂的策略是去除詞匯表中很少使用的單詞。減少特征空間也會降低模型學習目標函數的難度。 我們通過以下兩個步 驟完成這個操作: * 使用voc.trim函數去除 MIN_COUNT 閾值以下單詞 。 * 如果句子中包含詞頻過小的單詞,那么整個句子也被過濾掉。 ''' MIN_COUNT = 3 # 修剪的最小字數閾值def trimRareWords(voc, pairs, MIN_COUNT):# 修剪來自voc的MIN_COUNT下使用的單詞,即單詞出現頻率低于MIN_COUNT的話那么就會篩掉voc.trim(MIN_COUNT)# Filter out pairs with trimmed wordskeep_pairs = []for pair in pairs:input_sentence = pair[0]output_sentence = pair[1]keep_input = Truekeep_output = True# 檢查輸入句子for word in input_sentence.split(' '):if word not in voc.word2index:keep_input = Falsebreak# 檢查輸出句子for word in output_sentence.split(' '):if word not in voc.word2index:keep_output = Falsebreak# 只保留輸入或輸出句子中不包含修剪單詞的對if keep_input and keep_output:keep_pairs.append(pair)print("Trimmed from {} pairs to {}, {:.4f} of total".format(len(pairs), len(keep_pairs), len(keep_pairs) / len(pairs)))return keep_pairs# 修剪voc和對 pairs = trimRareWords(voc, pairs, MIN_COUNT) # for pair in pairs[:10]: # print(pair) #*******************************2.2 加載和清洗數據 end ******************************* #**********************************2.加載和預處理數據**********************************五.為模型準備數據
盡管我們已經投入了大量精力來準備和清洗我們的數據,將它變成一個很好的詞匯對象和一系列的句子對,但我們的模型最終希望數據以 numerical torch張量作為輸入。可以在seq2seq translation tutorial 中找到為模型準備處理數據的一種方法。 在該教程中,我們使用batch size大小為1,這意味著我們所要做的就是將句子對中的單詞轉換為詞匯表中的相應索引,并將其提供給模型。
但是,如果你想要加速訓練或者想要利用GPU并行計算能力,則需要使用小批量mini-batches來訓練。
使用小批量mini-batches也意味著我們必須注意批量處理中句子長度的變化。為了容納同一batch中不同大小的句子,我們將使我們的批量輸 入張量大小(max_length,batch_size),其中短于max_length的句子在EOS_token之后進行零填充(zero padded)。
如果我們簡單地將我們的英文句子轉換為張量,通過將單詞轉換為索引indicesFromSentence和零填充zero-pad,我們的張量的大小將是 (batch_size,max_length),并且索引第一維將在所有時間步驟中返回完整序列。但是,我們需要沿著時間對我們批量數據進行索引并且包 括批量數據中所有序列。因此,我們將輸入批處理大小轉換為(max_length,batch_size),以便跨第一維的索引返回批處理中所有句子的時 間步長。 我們在zeroPadding函數中隱式處理這個轉置。
#*******************************3.為模型準備數據 start ******************************* def indexesFromSentence(voc, sentence):#print([3,3,3]+[2])輸出[3, 3, 3, 2],即輸出句子中每個單詞的序號,最后一個序號是結束符號符的數字2return [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token]# zip 對數據進行合并了,相當于行列轉置了 def zeroPadding(l, fillvalue=PAD_token):#參數前加一個星號,將傳遞進來的參數放在同一個元組中,該參數的返回值是一個元組# (在本案例中即把indexesFromSentence返回的列表轉化成元組)# itertools.zip_longest作用參考https://blog.csdn.net/yiweiwei516/article/details/118182889return list(itertools.zip_longest(*l, fillvalue=fillvalue))# 記錄 PAD_token的位置為0, 其他的為1 def binaryMatrix(l, value=PAD_token):m = []for i, seq in enumerate(l):#即m是一個二維矩陣,一行就對應一個句子,然后用0/1來表示句子中單詞是否為填充符m.append([])for token in seq:if token == PAD_token:m[i].append(0)else:m[i].append(1)return m# 返回填充前(加入結束index EOS_token做標記)的長度 和 填充后的輸入序列張量 def inputVar(l, voc):#返回值是列表里套列表,每個列表里是每個句子里的各單詞對應的索引序號indexes_batch = [indexesFromSentence(voc, sentence) for sentence in l]#indexes就是里層的單個列表,lengths也是個列表,然后轉換成了張量,里面每個值對應了每個句子的長度lengths = torch.tensor([len(indexes) for indexes in indexes_batch])# 填充加轉置,返回值是列表里面套一個個元組padList = zeroPadding(indexes_batch)#torch.LongTensor是64位整型padVar = torch.LongTensor(padList)return padVar, lengths# 返回填充前(加入結束index EOS_token做標記)最長的一個長度 和 填充后的輸出序列張量, 和 填充后的標記 mask def outputVar(l, voc):indexes_batch = [indexesFromSentence(voc, sentence) for sentence in l]max_target_len = max([len(indexes) for indexes in indexes_batch])padList = zeroPadding(indexes_batch)padVar = torch.LongTensor(padList)mask = binaryMatrix(padList)#torch.ByteTensor構建一個Byte類型的張量mask = torch.ByteTensor(mask)return padVar, mask, max_target_len# 返回給定batch對的所有項目 def batch2TrainData(voc, pair_batch):pair_batch.sort(key=lambda x: len(x[0].split(" ")), reverse=True)input_batch, output_batch = [], []#將語句對分開for pair in pair_batch:print(pair)input_batch.append(pair[0])output_batch.append(pair[1])# 返回填充前(加入結束index EOS_token做標記)的長度 和 填充后的輸入序列張量inp, lengths = inputVar(input_batch, voc)# 返回填充前(加入結束index EOS_token做標記)最長的一個長度 和 填充后的輸出序列張量, 和 填充后的標記 maskoutput, mask, max_target_len = outputVar(output_batch, voc)return inp, lengths, output, mask, max_target_len# 驗證例子 print('***********************************************************************************') small_batch_size = 5 #random模塊中choice()可以從序列中獲取一個隨機元素,并返回一個(列表,元組或字符串中的)隨機項 #batches是隨機從所有語句對中國選取的5個語句對 batches = batch2TrainData(voc, [random.choice(pairs) for _ in range(small_batch_size)]) input_variable, lengths, target_variable, mask, max_target_len = batchesprint("input_variable:", input_variable) print("lengths:", lengths) print("target_variable:", target_variable) print("mask:", mask) print("max_target_len:", max_target_len) #*******************************3.為模型準備數據 end *******************************以下代碼期間的一些測試,有的時候如果自己不太清楚到底輸入了什么輸出了什么,就直接打印出來看看,是一個不錯的方式:
import itertools#delimiter:分隔符 # import codecs # # delimiter = '\t' # print('delimiter:',delimiter) # # codecs.decode()方法的使用參考https://zhuanlan.zhihu.com/p/377436438 # delimiter = str(codecs.decode(delimiter, "unicode_escape")) # print('delimiter:',delimiter)#測試沒見過的語法: ''' lines=['abc','de','f'] def back_for_test():result=[line for line in lines]return result;print(back_for_test()) '''#indexesFromSentence測試 # print([3,3,3]+[2]) #輸出[3,3,3,2]#zeroPadding測試PAD_token=0 def zeroPadding(l, fillvalue=PAD_token):#參數前加一個星號,將傳遞進來的參數放在同一個元組中,該參數的返回值是一個元組# (在本案例中即把indexesFromSentence返回的列表轉化成元組)# itertools.zip_longest作用參考https://blog.csdn.net/yiweiwei516/article/details/118182889#print(itertools.zip_longest(*l, fillvalue=fillvalue))return list(itertools.zip_longest(*l, fillvalue=fillvalue))print(zeroPadding((['abc','kevin','kobe'])))#也相當于做了轉置 ''' 本來傳入的矩陣是['abc','kevin','kobe'],可以看作如下 [a b ck e v i nk o b e ] 輸出是[('a', 'k', 'k'), ('b', 'e', 'o'), ('c', 'v', 'b'), (0, 'i', 'e'), (0, 'n', 0)],可以看作如下 [a k kb e oc v b0 i e0 n 0 ] 相當于做了轉置還填充了PAD_token '''總結
以上是生活随笔為你收集整理的ChatGPT最近大火?教你实现破产版ChatGPT(一)数据预处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手写一个@MapperScan扫描器
- 下一篇: Guacamole 配置开启 Radiu