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

