LLM的三大要素:
- 算力:算力的本質是拼財力,普通人是無力改變的;
- 演算法/模型結構:目前最流行的還是transformer架構, 各種LLM都是基於transformer改細節,暫時沒有用新的框架替代transformer。至於後續manba會不會替代transformer架構,有待觀察!
- 資料:這塊是做LLM pre-train或fine-tune最大的苦力活,做這塊業務的研發堪比苦力強~~~ 以前做傳統資料探勘和NLP,80%的時間都花在了資料的採集和清洗上。沒想到現在搞LLM,雖說比以前好些,還是要花大量時間來採集和清洗各種資料,真的是造孽啊.......
根據scanling law,tokens越多、模型越大、計算量越大,loss就越小!所以大量的優質資料是必不可少的!
在安全垂直領域,資料清洗的核心思路總結如下:
- 質量過濾:資料質量低,模型生成的資料會前言不搭後語,前後沒任何邏輯性可言
- 分類器過濾:用傳統的分類器對文章打分,接近1的就是質量好的文章,可以人為設定一個閾值,比如0.8,超過閾值的就是高質量文章。傳聞GPT-3在訓練時用的資料就是使用了分類器精選優質資料;問題又來了:分類器也需要訓練資料啊,”冷啟動“階段的資料哪來了?可以從一些權威的網站,比如維基百科、arxiv.org等,這些站點的文章作為正樣本,人工選一些 不入流的N線網站文章作為作為負樣本訓練分類器
- 人為設定規則:比如去掉網頁標籤、 、%20、\u3000等無用字元;
- Perplexity困惑度:Perplexity本質是根據之前所有的token預測下一個token生成的機率值,比如 I am a 這三個token後面跟上student的機率較大,跟上air的機率很小,如果後面街上了air,那麼這份語料的質量就大打折扣了,可以直接去掉啦!困惑都計算公式:
示例程式碼如下:
import torch from transformers import GPT2LMHeadModel, GPT2Tokenizer import math # 載入預訓練的GPT-2模型和分詞器 model_name = 'gpt2' model = GPT2LMHeadModel.from_pretrained(model_name) tokenizer = GPT2Tokenizer.from_pretrained(model_name) # 準備測試集文字 test_texts = [ "I am a student. I go to school everyday cause I like every of my classmates and teachers", "Mike is a software engineer,he programs on working days. he has very high skill to produce good quality software!", "haha, I adrt gqsf fgwf dgwa wuklala.", "The quick brown fox jumps over a lazy dog, the dog didn't take any actions." ] def calculate_perplexity(model, tokenizer, text): model.eval() inputs = tokenizer(text, return_tensors='pt') with torch.no_grad(): outputs = model(**inputs, labels=inputs['input_ids']) loss = outputs.loss perplexity = torch.exp(loss) return perplexity.item() # 設定困惑度閾值 perplexity_threshold = 200 filtered_texts = [] for text in test_texts: ppl = calculate_perplexity(model, tokenizer, text) if ppl < perplexity_threshold: filtered_texts.append((text, ppl)) print(f"Text: {text}, Perplexity: {ppl}") print("\nFiltered Texts:") for text, ppl in filtered_texts: print(f"Text: {text}, Perplexity: {ppl}")
結果:質量差的文字被正確篩選出來!
注意:GPT2對中文支援很差,實際操作時建議換成千問、chatglm等國產大模型!
- 利用統計特徵過濾:安全領域有些出名的論壇,比如看雪、52pojie、先知等,有部分網友發帖並不是做技術分享,而是灌水/打廣告等,這部分語料也是要去掉的,可以根據標點符號分佈、符號字比(Symbol-to-WordRatio)、文字長度等過濾
- 冗餘去重:網際網路有部分人自己創作內容的能力差,只會轉載、洗稿,導致爬蟲爬取的資料大量重複。這些資料用於訓練時會讓loss明顯降低,但這是過擬合,模型生成資料時會明顯傾向於重複的資料,陷入重複迴圈(RepetitionLoops),無法回答其他問題,所以一定要去重。具體操作時,面臨一個顆粒度的選擇:句子、段落和文章!句子顆粒度去重最精細,但是數量巨大,我的算力不夠。所以我個人最終選擇段落和文字的顆粒度去重!大量文字去重的演算法思路有:
- 利用向量資料庫:先用embedding模型把文字轉成向量,然後儲存向量資料庫;每個新向量存入向量資料庫時先計算一下庫裡面有沒有相似度大於閾值(比如0.8)的向量,沒有再入庫;有就說明重複了,直接丟棄。等所有的文字都這麼操作一次後,向量資料庫的資料就是唯一的啦!這種思路本質是利用向量資料庫去重
- 和google對網頁去重的演算法一樣:simhash!
總的來講:simhash的計算量小於使用向量資料庫的計算量,所以這裡選擇simhash去重!核心demo程式碼如下:
from simhash import Simhash, SimhashIndex # 示例中文文字資料 texts = [ "rerank: 經過第一步使用cosin餘弦相似度從密集向量資料庫 + keyword search(稀疏向量召回)初步召回top K相似度的文字,按理來說就可以讓LLM根據使用者的query + 召回的context生成最終答案了,還要這個rerank幹啥了?實際操作時,還是會發現一些問題:包含正確答案的文字在context中的排名可能並不靠前。比如query = “清華大學在哪座城市?” 。正確答案肯定是“北京”啦!但實際召回的context中包含北京的文字不一定排在前面,可能在中間甚至後面,給最後一個LLM輸入的context會很大,直接導致LLM需要處理很長的文字,推理效率低不說,還容易出錯,核心問題還是在於:初步召回的context還是有進一步壓縮提煉的空間!造成這種現象的原因是啥了", "rerank: 經過第一步使用sim相似度從密集向量資料庫 +關鍵詞初步召回K個相似度的文字,按理來說就可以讓LLM根據使用者的問題 + 召回的文字生成最終答案了,還要這個重排幹啥了?實際操作時,還是會發現一些問題:包含正確答案的文字在context中的排名可能並不靠前。比如問題 = “北京大學在哪座城市?” 。正確答案肯定是“北京”啦!但實際召回的context中包含北京的文字不一定排在前面,可能在中間甚至後面,給最後一個LLM輸入的context會很大,直接導致LLM需要處理很長的文字,推理效率低不說,還容易出錯,核心問題還是在於:初步召回的context還是有進一步壓縮提煉的空間!造成這種現象的原因是啥了", "利用cosin求兩個向量的相似度,本質是看兩個向量的距離。比如“北京”、“上海”、“深圳”這些都是中國的一線大城市,這3個詞的嵌入向量的餘弦相似度會很近,所以使用cosin召回的時候也可能把“上海”、“深圳”這些不是正確答案的sentence召回,所以要用tf-idf這類稀疏向量補充召回部分向量,整個過程稱為 hybrid search。經過hybird search後,召回的context變多,給最後一步的LLM生成最終答案帶來了麻煩,所以需要進一步從context中繼續提煉,優中選優!比如初步召回20條,需要透過重排選擇更接近的3~5條,這個過程就是rerank!", "利用距離求兩個向量的相似度,本質是看兩個向量的距離。比如“北京”、“上海”、“深圳”這些都是中國的一線大城市,這3個詞的embedding的cosin會很近,所以使用cosin召回的時候也可能把“上海”、“深圳”這些不是正確答案的sentence召回,所以要用tf-idf這類稀疏向量補充召回部分向量,整個過程稱為 混合檢索。經過混合檢索後,召回的上下文變多,給最後一步的LLM生成最終答案帶來了麻煩,所以需要進一步從context中繼續提煉,優中選優!比如初步召回20條,需要透過rerank選擇更接近的3~5條,這個過程就是rerank!", "確定要做rerank後,怎麼做才能達到既定的目的?要想明白這個問題,還要回到最初的動機:cosin計算的是兩個向量的距離,只考慮語義相似,不考慮字面符號是否一致;而稀疏檢索tf-idf只考慮字面的符號, 不考慮語義,怎麼整合這兩種retrieve的優勢,摒棄其劣勢了?這就需要用到傳統NLP常見的手段了:classifier", ] # 計算每個文字的Simhash值 simhashes = [(str(i), Simhash(text)) for i, text in enumerate(texts)] # 建立Simhash索引 index = SimhashIndex(simhashes, k=10) # 檢查文字是否重複 def is_duplicate(new_text, index): new_simhash = Simhash(new_text) duplicates = index.get_near_dups(new_simhash) return duplicates # 檢查每個文字是否重複 for i, text in enumerate(texts): duplicates = is_duplicate(text, index) if len(duplicates) > 1: # 因為自己也會在重複列表中,所以長度大於1 print(f"文字 {i} 是重複的,重複文字索引:{duplicates}") else: print(f"文字 {i} 沒有重複。") # 例如,新增一個新文字並檢查是否重複 new_text = "按理來說就可以讓LLM根據使用者的query + 召回的context生成最終答案了,還要這個rerank幹啥了?實際操作時,還是會發現一些問題:包含正確答案的文字在context中的排名可能並不靠前." if is_duplicate(new_text, index): print("新文字是重複的。") else: print("新文字沒有重複。") # 如果沒有重複,則將其新增到索引中 index.add(str(len(texts)), Simhash(new_text))
我這裡執行的結果:
文字 0 是重複的,重複文字索引:['1', '0'] 文字 1 是重複的,重複文字索引:['1', '0'] 文字 2 是重複的,重複文字索引:['3', '2'] 文字 3 是重複的,重複文字索引:['3', '2'] 文字 4 沒有重複。 新文字沒有重複。
注意事項:
- 看了很多資料,說是k=3就能判斷是否重複。但就我個人的實測來看,3顯然不夠,我這裡把k提升到了10就能準確找到重複的文件了!當然,如果只是簡單複製貼上,沒有對內容做實質性地更改,k=3也能查重
- 對文字計算hash前,建議先用正則去掉換行、回車、逗號、句號、感嘆號、問號等符號,只保留文字和數字,減少對hash值的干擾
- 對於長篇的文章分段,可以使用自然段落,也可以使用語義聚類的方式劃分段落,詳見:https://www.cnblogs.com/theseventhson/p/18279980
參考:
1、https://arxiv.org/pdf/2001.08361 scaling law
2、https://geek.digiasset.org/pages/affiliate/text-simhash-good-re-process-deep_21Apr03114628313403/ https://www.cnblogs.com/maybe2030/p/5203186.html#_label4 simhash原理
3、https://leons.im/posts/a-python-implementation-of-simhash-algorithm/ A Python Implementation of Simhash Algorithm