| import Foundation |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| final class UnigramTokenizer { |
|
|
| |
| static let bosId: Int32 = 0 |
| static let padId: Int32 = 1 |
| static let eosId: Int32 = 2 |
|
|
| |
| private static let metaspace: Unicode.Scalar = "\u{2581}" |
|
|
| |
| private static let unkPenalty: Float = 10.0 |
|
|
| private let pieceToId: [String: Int32] |
| private let scores: [Float] |
| let unkId: Int32 |
| private let unkScore: Float |
| private let maxPieceScalars: Int |
|
|
| |
| struct Encoding { |
| |
| var ids: [Int32] |
| |
| |
| var charEnds: [Int] |
| } |
|
|
| |
| static func bundled() throws -> UnigramTokenizer { |
| guard let url = Bundle.module.url(forResource: "xlmr_unigram", withExtension: "bin") else { |
| throw KitError.missingResource("xlmr_unigram.bin") |
| } |
| return try UnigramTokenizer(vocabURL: url) |
| } |
|
|
| |
| |
| |
| |
| init(vocabURL: URL) throws { |
| let data = try Data(contentsOf: vocabURL, options: .mappedIfSafe) |
| var pieceToId = [String: Int32]() |
| var scores = [Float]() |
| var unkId: Int32 = 3 |
| var maxScalars = 1 |
|
|
| try data.withUnsafeBytes { (raw: UnsafeRawBufferPointer) in |
| guard raw.count >= 16 else { throw KitError.corruptResource("xlmr_unigram.bin too small") } |
| var off = 0 |
| func u32() -> UInt32 { |
| let v = raw.loadUnaligned(fromByteOffset: off, as: UInt32.self); off += 4 |
| return UInt32(littleEndian: v) |
| } |
| func u16() -> UInt16 { |
| let v = raw.loadUnaligned(fromByteOffset: off, as: UInt16.self); off += 2 |
| return UInt16(littleEndian: v) |
| } |
| func f32() -> Float { |
| let bits = raw.loadUnaligned(fromByteOffset: off, as: UInt32.self); off += 4 |
| return Float(bitPattern: UInt32(littleEndian: bits)) |
| } |
|
|
| let magic = raw.loadUnaligned(fromByteOffset: 0, as: UInt32.self); off = 4 |
| |
| guard magic == 0x56505457 else { throw KitError.corruptResource("bad vocab magic") } |
| _ = u32() |
| let count = Int(u32()) |
| unkId = Int32(u32()) |
|
|
| pieceToId.reserveCapacity(count) |
| scores = [Float](repeating: 0, count: count) |
| let base = raw.baseAddress! |
|
|
| for id in 0..<count { |
| let len = Int(u16()) |
| let score = f32() |
| let piece = String(decoding: UnsafeRawBufferPointer(start: base + off, count: len), |
| as: UTF8.self) |
| off += len |
| scores[id] = score |
| pieceToId[piece] = Int32(id) |
| let n = piece.unicodeScalars.count |
| if n > maxScalars { maxScalars = n } |
| } |
| } |
|
|
| self.pieceToId = pieceToId |
| self.scores = scores |
| self.unkId = unkId |
| self.maxPieceScalars = maxScalars |
| let minScore = scores.min() ?? 0 |
| self.unkScore = minScore - Self.unkPenalty |
| } |
|
|
| |
| func encode(scalars original: [Unicode.Scalar]) -> Encoding { |
| var enc = Encoding(ids: [], charEnds: []) |
| if original.isEmpty { return enc } |
|
|
| |
| let (norm, origIdx) = normalize(original) |
| let n = norm.count |
| if n == 0 { return enc } |
|
|
| |
| var i = 0 |
| while i < n { |
| if CharClasses.isSpace(norm[i]) { i += 1; continue } |
| var j = i |
| while j < n && !CharClasses.isSpace(norm[j]) { j += 1 } |
| encodeWord(norm: norm, origIdx: origIdx, wordStart: i, wordEnd: j, into: &enc) |
| i = j |
| } |
| return enc |
| } |
|
|
| |
|
|
| private func encodeWord(norm: [Unicode.Scalar], origIdx: [Int], |
| wordStart: Int, wordEnd: Int, into enc: inout Encoding) { |
| |
| let wordLen = wordEnd - wordStart |
| let L = wordLen + 1 |
| var s = [Unicode.Scalar](repeating: Self.metaspace, count: L) |
| for k in 0..<wordLen { s[k + 1] = norm[wordStart + k] } |
|
|
| |
| let negInf = -Float.greatestFiniteMagnitude |
| var best = [Float](repeating: negInf, count: L + 1) |
| var backPrev = [Int](repeating: -1, count: L + 1) |
| var backId = [Int32](repeating: unkId, count: L + 1) |
| best[0] = 0 |
|
|
| for pos in 0..<L { |
| if best[pos] == negInf { continue } |
| let maxLen = min(maxPieceScalars, L - pos) |
| var view = String.UnicodeScalarView() |
| view.reserveCapacity(maxLen) |
| if maxLen >= 1 { |
| for len in 1...maxLen { |
| view.append(s[pos + len - 1]) |
| if let id = pieceToId[String(view)] { |
| let cand = best[pos] + scores[Int(id)] |
| if cand > best[pos + len] { |
| best[pos + len] = cand |
| backPrev[pos + len] = pos |
| backId[pos + len] = id |
| } |
| } |
| } |
| } |
| |
| let cand = best[pos] + unkScore |
| if cand > best[pos + 1] { |
| best[pos + 1] = cand |
| backPrev[pos + 1] = pos |
| backId[pos + 1] = unkId |
| } |
| } |
|
|
| |
| var pieces: [(end: Int, id: Int32)] = [] |
| var pos = L |
| while pos > 0 { |
| pieces.append((pos, backId[pos])) |
| pos = backPrev[pos] |
| } |
| pieces.reverse() |
|
|
| |
| for pc in pieces { |
| enc.ids.append(pc.id) |
| if pc.end >= 2 { |
| |
| let normIdx = wordStart + pc.end - 2 |
| enc.charEnds.append(origIdx[normIdx]) |
| } else { |
| |
| |
| enc.charEnds.append(origIdx[wordStart]) |
| } |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| private func normalize(_ original: [Unicode.Scalar]) -> (norm: [Unicode.Scalar], origIdx: [Int]) { |
| var norm = [Unicode.Scalar]() |
| var origIdx = [Int]() |
| norm.reserveCapacity(original.count) |
| origIdx.reserveCapacity(original.count) |
| for (i, sc) in original.enumerated() { |
| let mapped = String(sc).precomposedStringWithCompatibilityMapping |
| for ns in mapped.unicodeScalars { |
| norm.append(ns) |
| origIdx.append(i) |
| } |
| } |
| return (norm, origIdx) |
| } |
| } |
|
|