226 lines
5.6 KiB
Go
226 lines
5.6 KiB
Go
package marian
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// space is SentencePiece's visible word boundary ("▁").
|
|
const space = "▁"
|
|
|
|
// sentencePiece is a SentencePiece unigram model: it splits text into the
|
|
// pieces of the vocabulary that maximise the summed piece scores.
|
|
type sentencePiece struct {
|
|
scores map[string]float32
|
|
maxRunes int
|
|
unkScore float32
|
|
// addDummyPrefix starts the text with "▁", as if preceded by a space.
|
|
addDummyPrefix bool
|
|
}
|
|
|
|
// SentencePiece piece types (sentencepiece_model.proto).
|
|
const (
|
|
pieceNormal = 1
|
|
pieceUnknown = 2
|
|
pieceControl = 3
|
|
pieceUserDefined = 4
|
|
pieceUnused = 5
|
|
pieceByte = 6
|
|
)
|
|
|
|
// loadSentencePiece reads a serialized ModelProto. Only the fields needed
|
|
// to encode are decoded: the pieces (field 1), the model type (trainer
|
|
// spec, field 2) and the dummy-prefix flag (normalizer spec, field 3).
|
|
func loadSentencePiece(path string) (*sentencePiece, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sp := &sentencePiece{scores: map[string]float32{}, addDummyPrefix: true}
|
|
minScore := float32(0)
|
|
err = protoFields(data, func(field int, wire int, v uint64, b []byte) error {
|
|
switch field {
|
|
case 1:
|
|
var piece string
|
|
var score float32
|
|
typ := pieceNormal
|
|
err := protoFields(b, func(field, wire int, v uint64, b []byte) error {
|
|
switch field {
|
|
case 1:
|
|
piece = string(b)
|
|
case 2:
|
|
score = math.Float32frombits(uint32(v))
|
|
case 3:
|
|
typ = int(v)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if typ == pieceNormal || typ == pieceUserDefined {
|
|
sp.scores[piece] = score
|
|
sp.maxRunes = max(sp.maxRunes, len([]rune(piece)))
|
|
minScore = min(minScore, score)
|
|
}
|
|
case 2:
|
|
return protoFields(b, func(field, wire int, v uint64, b []byte) error {
|
|
if field == 3 && v != 1 {
|
|
return fmt.Errorf("unsupported SentencePiece model type %d (only unigram)", v)
|
|
}
|
|
return nil
|
|
})
|
|
case 3:
|
|
return protoFields(b, func(field, wire int, v uint64, b []byte) error {
|
|
if field == 3 {
|
|
sp.addDummyPrefix = v != 0
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading %s: %w", path, err)
|
|
}
|
|
if len(sp.scores) == 0 {
|
|
return nil, fmt.Errorf("reading %s: no pieces", path)
|
|
}
|
|
// SentencePiece gives unknown characters a score below every piece.
|
|
sp.unkScore = minScore - 10
|
|
return sp, nil
|
|
}
|
|
|
|
// protoFields calls fn for each field of a protobuf message: v holds
|
|
// varints and fixed-width values, b length-delimited ones.
|
|
func protoFields(data []byte, fn func(field, wire int, v uint64, b []byte) error) error {
|
|
for len(data) > 0 {
|
|
key, n := binary.Uvarint(data)
|
|
if n <= 0 {
|
|
return errors.New("malformed protobuf key")
|
|
}
|
|
data = data[n:]
|
|
field, wire := int(key>>3), int(key&7)
|
|
var v uint64
|
|
var b []byte
|
|
switch wire {
|
|
case 0:
|
|
v, n = binary.Uvarint(data)
|
|
if n <= 0 {
|
|
return errors.New("malformed protobuf varint")
|
|
}
|
|
data = data[n:]
|
|
case 1:
|
|
if len(data) < 8 {
|
|
return errors.New("truncated protobuf")
|
|
}
|
|
v, data = binary.LittleEndian.Uint64(data), data[8:]
|
|
case 2:
|
|
l, n := binary.Uvarint(data)
|
|
if n <= 0 || uint64(len(data)-n) < l {
|
|
return errors.New("truncated protobuf")
|
|
}
|
|
b, data = data[n:n+int(l)], data[n+int(l):]
|
|
case 5:
|
|
if len(data) < 4 {
|
|
return errors.New("truncated protobuf")
|
|
}
|
|
v, data = uint64(binary.LittleEndian.Uint32(data)), data[4:]
|
|
default:
|
|
return fmt.Errorf("unsupported protobuf wire type %d", wire)
|
|
}
|
|
if err := fn(field, wire, v, b); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// normalize approximates SentencePiece's nmt_nfkc normalisation for the
|
|
// characters scripts actually contain: every kind of space becomes a
|
|
// plain one, runs of spaces collapse, control characters go away and a
|
|
// few compatibility characters are decomposed.
|
|
func normalize(text string) string {
|
|
var b strings.Builder
|
|
pendingSpace := false
|
|
for _, r := range text {
|
|
switch {
|
|
case unicode.IsSpace(r):
|
|
pendingSpace = b.Len() > 0
|
|
continue
|
|
case unicode.IsControl(r) || r == '\u200b' || r == '\ufeff':
|
|
continue
|
|
}
|
|
if pendingSpace {
|
|
b.WriteByte(' ')
|
|
pendingSpace = false
|
|
}
|
|
switch r {
|
|
case '…':
|
|
b.WriteString("...")
|
|
case 'fi':
|
|
b.WriteString("fi")
|
|
case 'fl':
|
|
b.WriteString("fl")
|
|
case 'ff':
|
|
b.WriteString("ff")
|
|
default:
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// encode splits text into pieces (Viterbi search over the unigram scores).
|
|
func (sp *sentencePiece) encode(text string) []string {
|
|
text = normalize(text)
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
if sp.addDummyPrefix {
|
|
text = " " + text
|
|
}
|
|
runes := []rune(strings.ReplaceAll(text, " ", space))
|
|
n := len(runes)
|
|
best := make([]float64, n+1)
|
|
from := make([]int, n+1)
|
|
for i := 1; i <= n; i++ {
|
|
best[i] = math.Inf(-1)
|
|
}
|
|
for end := 1; end <= n; end++ {
|
|
for start := max(0, end-sp.maxRunes); start < end; start++ {
|
|
if math.IsInf(best[start], -1) {
|
|
continue
|
|
}
|
|
score, ok := sp.scores[string(runes[start:end])]
|
|
if !ok {
|
|
if end-start != 1 {
|
|
continue
|
|
}
|
|
score = sp.unkScore
|
|
}
|
|
if s := best[start] + float64(score); s > best[end] {
|
|
best[end], from[end] = s, start
|
|
}
|
|
}
|
|
}
|
|
var pieces []string
|
|
for end := n; end > 0; end = from[end] {
|
|
pieces = append(pieces, string(runes[from[end]:end]))
|
|
}
|
|
for i, j := 0, len(pieces)-1; i < j; i, j = i+1, j-1 {
|
|
pieces[i], pieces[j] = pieces[j], pieces[i]
|
|
}
|
|
return pieces
|
|
}
|
|
|
|
// decodePieces joins target pieces back into text.
|
|
func decodePieces(pieces []string) string {
|
|
return strings.TrimSpace(strings.ReplaceAll(strings.Join(pieces, ""), space, " "))
|
|
}
|