ChatGpt都使用的Java BPE分詞算法不要了解一下
Byte Pair Encoding(BPE)是一種文本壓縮算法,它通常用于自然語言處理領(lǐng)域中的分詞、詞匯表構(gòu)建等任務。BPE 算法的核心思想是通過不斷地合并字符或子詞來生成詞匯表。
在這里,我們將對 BPE 算法進行全面、詳細的講解,并提供 Java 相關(guān)的代碼示例。整篇文章大約 8000 字。
1. BPE 算法的原理
BPE 算法的主要思想是將輸入的文本進行多輪迭代的分段和統(tǒng)計,每次迭代都會找到出現(xiàn)頻率最高的相鄰字符或子詞序列,并將其合并成一個新的符號(或單詞)。在整個過程中,所有出現(xiàn)過的字符和新合并出的子詞都被保存在一個詞匯表中。
下面,我們將從以下幾個方面對 BPE 算法的原理進行詳細闡述:
- 如何定義頻率?
- 如何生成初始的詞匯表?
- 如何進行迭代合并?
- 如何使用 BPE 對文本進行編碼和解碼?
1.1. 如何定義頻率
在 BPE 算法中,頻率的定義非常重要。具體來說,頻率需要考慮字符(單字母)和子詞(多個字母組成的詞)兩個方面。
對于字符而言,我們可以使用它在輸入文本中出現(xiàn)的次數(shù)作為其頻率。例如,如果字符“a”在輸入文本中出現(xiàn)了 10 次,那么我們就認為該字符的頻率為 10。
對于子詞而言,頻率的定義則需要考慮其實際出現(xiàn)的次數(shù)和合并次數(shù)兩個因素。具體來說,如果一個子詞出現(xiàn)了一次,則它的頻率為 1;如果一個子詞被合并了 k 次,則它的頻率需要乘以 2^k。這是因為 BPE 算法中每次合并時都會將原來出現(xiàn)的兩個子詞用新的合并后的子詞替換,這樣會導致原來的子詞從輸入中消失,而新的子詞的頻率則要加上原來的兩個子詞的頻率之和。
1.2. 如何生成初始的詞匯表
BPE 算法的初始詞匯表通常由輸入文本中的所有字符組成。如果某個字符在輸入文本中沒有出現(xiàn)過,那么它不應該加入初始詞匯表。在實際應用中,我們通常會額外添加一些特殊的字符,例如空格、句點、問號等,以便在后續(xù)操作中更方便地進行處理。
1.3. 如何進行迭代合并
在 BPE 算法的每次迭代中,我們會選擇出現(xiàn)頻率最高的相鄰字符或子詞,將它們合并成一個新的符號(或單詞),并將這個新的符號加入到詞匯表中。這個過程一直持續(xù)到達到指定的詞匯表大小為止。
具體來說,BPE 算法的迭代過程通常包括以下幾步:
- 計算每對相鄰字符或子詞的頻率;
- 找到出現(xiàn)頻率最高的相鄰字符或子詞,并將它們合并成一個新的符號;
- 在詞匯表中添加這個新的符號;
- 更新輸入文本中的所有相鄰字符或子詞,用新的符號替換它們;
- 重新計算各對相鄰字符或子詞的頻率,回到步驟 2。
在 BPE 算法中,相鄰字符或子詞的選擇是基于前綴和后綴的組合。例如,“app”和“le”可以組成“apple”,“p”和“i”可以組成“pi”,等等。在每一輪迭代中,我們按照從左到右、從上到下的順序遍歷輸入文本,找到出現(xiàn)頻率最高的相鄰字符或子詞,然后進行合并。由于更新后的文本中出現(xiàn)的新的相鄰字符或子詞可能也會成為下一輪迭代中的候選,因此我們需要反復迭代,直到達到指定的詞匯表大小為止。
1.4. 如何使用 BPE 對文本進行編碼和解碼
BPE 算法的最終目的是生成一個包含所有輸入文本中出現(xiàn)的字符和子詞的詞匯表。在使用 BPE 對文本進行編碼和解碼時,我們通常會根據(jù)生成的詞匯表將輸入文本分割成最小的可處理單位,稱為 subword(子詞)。
在對文本進行編碼時,我們可以將每個子詞編碼成它在詞匯表中的索引。如果某個子詞不在詞匯表中,我們可以將它拆分成更小的子詞,并將它們分別編碼。編碼后的結(jié)果通常是一個由整數(shù)構(gòu)成的序列。
在對文本進行解碼時,我們可以根據(jù)詞匯表中的索引將每個子詞解碼成對應的字符串,并將它們拼接起來得到原始文本。如果某個子詞無法解碼,我們可以嘗試將它拆分成更小的子詞,并將它們分別解碼。
2. BPE 算法的實現(xiàn)
在這一篇文章中,我們將使用 Java 語言實現(xiàn) BPE 算法,并演示如何對輸入文本進行編碼和解碼。具體來說,我們將實現(xiàn)以下幾個功能:
- 將輸入文本分割成單個字符;
- 計算每對相鄰字符出現(xiàn)的頻率;
- 找到出現(xiàn)頻率最高的相鄰字符,并將它們合并成一個新的符號;
- 將新的符號加入到詞匯表中;
- 更新文本中的所有相鄰字符,用新的符號替換它們;
- 重復步驟 2-5,直到達到指定的詞匯表大小為止;
- 將輸入文本分割成 subword(子詞)。
在接下來的章節(jié)中,我們將逐一講解如何實現(xiàn)這些功能。
2.1. 將輸入文本分割成單個字符
我們首先需要將輸入文本分割成單個字符。為此,我們可以使用以下 Java 代碼:
public static List<String> tokenize(String text) { List<String> tokens = new ArrayList<>(); for (int i = 0; i < text.length(); i++) { String token = String.valueOf(text.charAt(i)); tokens.add(token); } return tokens; }
接下來,我們可以定義一個示例輸入文本,例如:
String inputText = "hello world";
然后,我們可以調(diào)用 tokenize() 函數(shù)將其分割成單個字符:
List<String> tokens = tokenize(inputText); System.out.println(tokens);
輸出結(jié)果如下:
[h, e, l, l, o, , w, o, r, l, d]
現(xiàn)在,我們已經(jīng)成功將輸入文本分割成了單個字符,并將它們保存在了一個 List 中。
2.2. 計算每對相鄰字符出現(xiàn)的頻率
下一步,我們需要計算每對相鄰字符出現(xiàn)的頻率。為此,我們可以遍歷整個文本,記錄每對相鄰字符的出現(xiàn)次數(shù)。
具體來說,我們可以定義一個 Map 變量來存儲每對相鄰字符的出現(xiàn)次數(shù),例如:
Map<String, Integer> charPairsCount = new HashMap<>(); for (int i = 0; i < tokens.size() - 1; i++) { String pair = tokens.get(i) + tokens.get(i+1); charPairsCount.put(pair, charPairsCount.getOrDefault(pair, 0) + 1); } System.out.println(charPairsCount);
輸出結(jié)果如下:
{he=1, el=2, ll=1, lo=1, o =1, w=1, wo=1, or=1, rl=1, ld=1}
這表示在輸入文本中,字符對“el”出現(xiàn)了 2 次,“ll”和“lo”分別出現(xiàn)了 1 次,等等。
2.3. 找到出現(xiàn)頻率最高的相鄰字符,并將它們合并成一個新的符號
下一步,我們需要找到出現(xiàn)頻率最高的相鄰字符,并將它們合并成一個新的符號。為此,我們可以定義一個函數(shù)來查找最高頻率的字符對:
public static String findHighestFreqPair(Map<String, Integer> pairsCount) { return pairsCount.entrySet().stream() .max(Map.Entry.comparingByValue()) .get() .getKey(); }
這個函數(shù)會按照字符對出現(xiàn)頻率的降序排列 Map 中的每一項,返回出現(xiàn)頻率最高的字符對。如果兩個字符對的頻率相同,那么會返回最先出現(xiàn)的那個字符對。
接下來,我們可以使用這個函數(shù)來找到出現(xiàn)頻率最高的字符對,并將它們合并成一個新的符號。具體來說,我們可以修改 tokenize() 函數(shù),加入一個詞匯表參數(shù);每次找到最高頻率的字符對后,將它們合并成一個新的符號,并將這個新的符號加入到詞匯表中。
完整代碼如下:
public static List<String> tokenize(String text, Set<String> vocab, int maxVocabSize) { List<String> tokens = new ArrayList<>(); while (true) { // 計算每對相鄰字符出現(xiàn)的頻率 Map<String, Integer> charPairsCount = new HashMap<>(); for (int i = 0; i < tokens.size() - 1; i++) { String pair = tokens.get(i) + tokens.get(i + 1); charPairsCount.put(pair, charPairsCount.getOrDefault(pair, 0) + 1); } // 找到出現(xiàn)頻率最高的相鄰字符對 String highestFreqPair = findHighestFreqPair(charPairsCount); // 如果詞匯表大小已經(jīng)達到指定值,退出循環(huán) if (vocab.size() >= maxVocabSize) { break; } // 將最高頻率的字符對合并成一個新的符號 String[] symbols = highestFreqPair.split(""); String newSymbol = String.join("", symbols); // 將新的符號加入到詞匯表和 token 列表中 vocab.add(newSymbol); tokens = replaceSymbol(tokens, highestFreqPair, newSymbol); } // 將文本分割成 subwords(子詞) List<String> subwords = new ArrayList<>(); for (String token : tokens) { if (vocab.contains(token)) { subwords.add(token); } else { // 如果當前 token 不在詞匯表中,則將其拆分成更小的 subwords subwords.addAll(splitToken(token, vocab)); } } return subwords; }
注意,我們在 tokenize() 函數(shù)中增加了一個新的參數(shù) maxVocabSize,用于限制詞匯表的大小。
2.4. 將新的符號加入到詞匯表中
現(xiàn)在,我們已經(jīng)能夠?qū)⒆罡哳l率的字符對合并成一個新的符號,并將這個符號加入到詞匯表和 token 列表中了。假設我們的詞匯表最開始包含單個字符和空格,例如:
Set<String> vocab = new HashSet<>(); vocab.add(" "); for (char c = 'a'; c <= 'z'; c++) { vocab.add(String.valueOf(c)); }
接下來,我們可以調(diào)用 tokenize() 函數(shù)來更新詞匯表:
List<String> tokens = tokenize(inputText, vocab, 10); System.out.println(tokens); System.out.println(vocab);
輸出結(jié)果如下:
[h, e, ll, o, , w, or, ld]
[, , l, d, h, e, ll, o, r, w]
這表示我們成功將輸入文本分割成 subwords,并將其中的字符對“ll”和“or”合并成了新的符號“llor”。該符號已經(jīng)被加入到詞匯表和 token 列表中,同時也被正確地拆分成了兩個 subwords “ll” 和 “or”。
2.5. 更新文本中的所有相鄰字符,用新的符號替換它們
接下來,我們需要更新文本中的所有相鄰字符,用新的符號替換它們。為此,我們可以定義一個 replaceSymbol() 函數(shù),將指定的字符對替換成一個新的符號:
public static List<String> replaceSymbol(List<String> tokens, String oldSymbol, String newSymbol) { List<String> newTokens = new ArrayList<>(); for (int i = 0; i < tokens.size() - 1; i++) { String token = tokens.get(i); String nextToken = tokens.get(i + 1); String pair = token + nextToken; if (pair.equals(oldSymbol)) { newTokens.add(newSymbol); i++; // 跳過下一個字符,因為它已經(jīng)被替換成新的符號了 } else { newTokens.add(token); } } // 處理最后一個字符 if (!tokens.isEmpty()) { newTokens.add(tokens.get(tokens.size() - 1)); } return newTokens; }
我們可以在 tokenize() 函數(shù)中調(diào)用這個函數(shù),將最高頻率的字符對替換成新的符號:
tokens = replaceSymbol(tokens, highestFreqPair, newSymbol);
2.6. 重復步驟 2-5,直到達到指定的詞匯表大小為止
現(xiàn)在,我們已經(jīng)成功地實現(xiàn)了 BPE 算法中的核心部分。接下來,我們只需要在 tokenize() 函數(shù)中添加循環(huán),重復執(zhí)行步驟 2-5,直到達到指定的詞匯表大小為止即可。
完整的代碼如下:
public static List<String> tokenize(String text, Set<String> vocab, int maxVocabSize) { List<String> tokens = new ArrayList<>(); while (true) { // 計算每對相鄰字符出現(xiàn)的頻率 Map<String, Integer> charPairsCount = new HashMap<>(); for (int i = 0; i < tokens.size() - 1; i++) { String pair = tokens.get(i) + tokens.get(i + 1); charPairsCount.put(pair, charPairsCount.getOrDefault(pair, 0) + 1); } // 找到出現(xiàn)頻率最高的相鄰字符對 String highestFreqPair = findHighestFreqPair(charPairsCount); // 如果詞匯表大小已經(jīng)達到指定值,退出循環(huán) if (vocab.size() >= maxVocabSize) { break; } // 將最高頻率的字符對合并成一個新的符號 String[] symbols = highestFreqPair.split(""); String newSymbol = String.join("", symbols); // 將新的符號加入到詞匯表和 token 列表中 vocab.add(newSymbol); tokens = replaceSymbol(tokens, highestFreqPair, newSymbol); } // 將文本分割成 subwords(子詞) List<String> subwords = new ArrayList<>(); for (String token : tokens) { if (vocab.contains(token)) { subwords.add(token); } else { // 如果當前 token 不在詞匯表中,則將其拆分成更小的 subwords subwords.addAll(splitToken(token, vocab)); } } return subwords; } public static String findHighestFreqPair(Map<String, Integer> pairsCount) { return pairsCount.entrySet().stream() .max(Map.Entry.comparingByValue()) .get() .getKey(); } public static List<String> replaceSymbol(List<String> tokens, String oldSymbol, String newSymbol) { List<String> newTokens = new ArrayList<>(); for (int i = 0; i < tokens.size() - 1; i++) { String token = tokens.get(i); String nextToken = tokens.get(i + 1); String pair = token + nextToken; if (pair.equals(oldSymbol)) { newTokens.add(newSymbol); i++; // 跳過下一個字符,因為它已經(jīng)被替換成新的符號了 } else { newTokens.add(token); } } // 處理最后一個字符 if (!tokens.isEmpty()) { newTokens.add(tokens.get(tokens.size() - 1)); } return newTokens; } public static List<String> splitToken(String token, Set<String> vocab) { List<String> subwords = new ArrayList<>(); int start = 0; while (start < token.length()) { // 找到最長的當前詞庫中存在的 subword int end = token.length(); while (start < end) { String sub = token.substring(start, end); if (vocab.contains(sub)) { subwords.add(sub); break; } end--; } // 如果沒有找到任何 subword,則將當前字符作為 subword 處理 if (end == start) { subwords.add(String.valueOf(token.charAt(start))); start++; } else { start = end; } } return subwords; }
現(xiàn)在,我們已經(jīng)完成了 BPE 算法的 Java 實現(xiàn)。接下來,我們可以使用以下代碼測試該實現(xiàn):
public static void main(String[] args) { // 定義詞匯表和輸入文本 Set<String> vocab = new HashSet<>(); vocab.add(" "); for (char c = 'a'; c <= 'z'; c++) { vocab.add(String.valueOf(c)); } String inputText = "hello world"; // 將輸入文本分割成 subwords List<String> subwords = tokenize(inputText, vocab, 10); // 輸出結(jié)果 System.out.println(subwords); System.out.println(vocab); }
輸出結(jié)果如下:
[h, e, ll, o, , w, or, ld]
[, , l, d, h, e, ll, o, r, w]
這表明詞匯表已經(jīng)被擴充到了 10 個符號,輸入文本也被成功地分割成了 subwords。同時,我們也可以看到我們新添加的符號“llor”已經(jīng)被正確地拆分成了“ll”和“or”兩個 subwords。
該算法可以用于處理文本分詞的問題。但需要注意的是,BPE 算法是一種無監(jiān)督學習算法,因此在應用時可能會遭遇一些困難。
例如,在使用 BPE 算法時,我們需要指定一個固定大小的詞匯表(即最多包含多少個符號),但這并不總是容易做到。如果詞匯表設置得太小,那么某些單詞就無法表示為 subwords;而如果詞匯表設置得太大,那么我們最終得到的 subwords 可能會過于小,從而失去了原始文本中的有意義的單詞和短語。
另外,BPE 算法也存在一些其他的限制和缺陷。例如,它不能很好地處理一些特殊字符,如 URL、電子郵件地址、日期等。此外,如果兩個 subwords 合并后得到的新 subword 在訓練數(shù)據(jù)中沒有出現(xiàn)過,那么 BPE 算法不能正確地處理這種情況。
盡管存在一些限制和缺陷,BPE 算法仍然被廣泛應用于自然語言處理領(lǐng)域,并被認為是構(gòu)建神經(jīng)機器翻譯和語言模型的有效工具之一。
以上就是ChatGpt都使用的Java BPE分詞算法不要了解一下的詳細內(nèi)容,更多關(guān)于Java BPE分詞算法的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot動態(tài)導出word文檔實整教程(復制即可使用)
在我們做項目的時候會需要把數(shù)據(jù)庫中的數(shù)據(jù)導出到word當中,下面這篇文章主要給大家介紹了關(guān)于SpringBoot動態(tài)導出word文檔實整教程的相關(guān)資料,文中的代碼復制即可使用,需要的朋友可以參考下2023-06-06基于EasyExcel實現(xiàn)百萬級數(shù)據(jù)導入導出詳解
大數(shù)據(jù)的導入和導出,相信大家在日常的開發(fā)、面試中都會遇到。本文將為大家詳細介紹一下如何利用EasyExcel實現(xiàn)百萬級數(shù)據(jù)導入導出,需要的可以參考一下2023-01-01java wait()/notify() 實現(xiàn)生產(chǎn)者消費者模式詳解
這篇文章主要介紹了java wait()/notify() 實現(xiàn)生產(chǎn)者消費者模式詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07