diff --git a/MLTraining/train.py b/MLTraining/train.py index c8871c2..ce716db 100644 --- a/MLTraining/train.py +++ b/MLTraining/train.py @@ -19,6 +19,12 @@ HANGUL_BASE = 0xAC00 HANGUL_LAST = 0xD7A3 +# 한글 외 문자: 숫자, 영문 대소문자, 상호명에 흔히 쓰이는 특수문자 +DIGITS = list("0123456789") +ALPHA_LOWER = list("abcdefghijklmnopqrstuvwxyz") +ALPHA_UPPER = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") +MISC_CHARS = [" ", "&", "+", "-", ".", ",", "'"] + def decompose_hangul(char): """완성형 한글 한 글자를 (초성, 중성, 종성) 튜플로 분해한다. 한글이 아니면 None.""" @@ -48,12 +54,11 @@ def tokenize(text): return tokens -def build_vocabulary(memos): - """시드 데이터에 등장하는 모든 자모/문자를 모아 정렬된 vocabulary를 만든다.""" - tokens = set() - for memo in memos: - tokens.update(tokenize(memo)) - return sorted(tokens) +def build_vocabulary(): + """이론상 가능한 전체 자모와 비한글 문자를 고정된 vocabulary로 만든다.""" + jongsung_tokens = ["_" + j for j in JONGSUNG if j] # 종성 없음("") 제외 + tokens = CHOSUNG + JUNGSUNG + jongsung_tokens + DIGITS + ALPHA_LOWER + ALPHA_UPPER + MISC_CHARS + return sorted(set(tokens)) def vectorize(memo, vocabulary): @@ -69,7 +74,7 @@ def main(): memos = [item["memo"] for item in seed_data] labels = [item["category"] for item in seed_data] - vocabulary = build_vocabulary(memos) + vocabulary = build_vocabulary() print(f"Vocabulary 크기: {len(vocabulary)}개 (자모/문자 단위)") vectors = [vectorize(memo, vocabulary) for memo in memos] diff --git a/SpendLearning/SpendLearning/Resource/CategoryClassifier.mlmodel b/SpendLearning/SpendLearning/Resource/CategoryClassifier.mlmodel index 84558c7..3c78442 100644 Binary files a/SpendLearning/SpendLearning/Resource/CategoryClassifier.mlmodel and b/SpendLearning/SpendLearning/Resource/CategoryClassifier.mlmodel differ diff --git a/SpendLearning/SpendLearning/Resource/vocabulary.json b/SpendLearning/SpendLearning/Resource/vocabulary.json index 92a23a3..e464a8a 100644 --- a/SpendLearning/SpendLearning/Resource/vocabulary.json +++ b/SpendLearning/SpendLearning/Resource/vocabulary.json @@ -1,7 +1,11 @@ [ " ", "&", + "'", "+", + ",", + "-", + ".", "0", "1", "2", @@ -9,47 +13,88 @@ "4", "5", "6", + "7", + "8", + "9", "A", + "B", "C", + "D", + "E", + "F", "G", "H", "I", + "J", "K", "L", "M", "N", "O", "P", + "Q", "R", "S", "T", "U", "V", + "W", "X", + "Y", + "Z", "_ㄱ", "_ㄲ", + "_ㄳ", "_ㄴ", + "_ㄵ", + "_ㄶ", + "_ㄷ", "_ㄹ", + "_ㄺ", + "_ㄻ", + "_ㄼ", + "_ㄽ", + "_ㄾ", + "_ㄿ", + "_ㅀ", "_ㅁ", "_ㅂ", + "_ㅄ", "_ㅅ", + "_ㅆ", "_ㅇ", + "_ㅈ", "_ㅊ", + "_ㅋ", "_ㅌ", + "_ㅍ", + "_ㅎ", "a", "b", "c", "d", "e", + "f", "g", "h", "i", + "j", "k", + "l", + "m", "n", + "o", + "p", + "q", "r", "s", "t", "u", + "v", + "w", + "x", + "y", + "z", "ㄱ", "ㄲ", "ㄴ", @@ -72,6 +117,7 @@ "ㅏ", "ㅐ", "ㅑ", + "ㅒ", "ㅓ", "ㅔ", "ㅕ", @@ -89,4 +135,4 @@ "ㅡ", "ㅢ", "ㅣ" -] +] \ No newline at end of file diff --git a/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift b/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift index f4a5ec7..306ed82 100644 --- a/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift +++ b/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift @@ -113,14 +113,18 @@ private extension CategoryPredictor { let writableURL = supportDir.appendingPathComponent("CategoryClassifier.mlmodelc") self.modelURL = writableURL - guard let bundledCompiledURL = Bundle.main.url(forResource: "CategoryClassifier", withExtension: "mlmodelc") else { + // 이미 온디바이스로 학습된 사본이 있으면 그대로 사용해 학습 결과를 보존한다. + // 최초 설치 등으로 사본이 없을 때만 번들 모델을 복사한다. + if fileManager.fileExists(atPath: writableURL.path) { self.model = try? MLModel(contentsOf: writableURL) return } - try? fileManager.removeItem(at: writableURL) - try? fileManager.copyItem(at: bundledCompiledURL, to: writableURL) + guard let bundledCompiledURL = Bundle.main.url(forResource: "CategoryClassifier", withExtension: "mlmodelc") else { + return + } + try? fileManager.copyItem(at: bundledCompiledURL, to: writableURL) self.model = try? MLModel(contentsOf: writableURL) } diff --git a/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift b/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift index 9da033b..08ab2fe 100644 --- a/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift +++ b/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift @@ -26,6 +26,7 @@ final class ExpenseInputViewController: UIViewController { private let amountFieldLabel = UILabel() private let amountTextField = UITextField() + private let amountUnitLabel = UILabel() private let amountSeparator = UIView() private let memoFieldLabel = UILabel() @@ -92,7 +93,7 @@ private extension ExpenseInputViewController { let digitsOnly = (amountTextField.text ?? "").filter { $0.isNumber } let amount = Int(digitsOnly) ?? 0 viewModel.didInputAmount(amount) - amountTextField.text = amount == 0 ? "" : "\(amount.formatted())원" + amountTextField.text = amount == 0 ? "" : amount.formatted() } } @@ -167,16 +168,20 @@ private extension ExpenseInputViewController { amountFieldLabel.font = .systemFont(ofSize: 13, weight: .regular) amountFieldLabel.textColor = .DesignSystem.subtitle - amountTextField.text = viewModel.initialAmount == 0 ? "" : "\(viewModel.initialAmount.formatted())원" + amountTextField.text = viewModel.initialAmount == 0 ? "" : viewModel.initialAmount.formatted() amountTextField.placeholder = "0" amountTextField.font = .systemFont(ofSize: 20, weight: .semibold) amountTextField.textColor = .DesignSystem.primary amountTextField.keyboardType = .numberPad amountTextField.addTarget(self, action: #selector(amountDidChange), for: .editingChanged) + amountUnitLabel.text = "원" + amountUnitLabel.font = .systemFont(ofSize: 20, weight: .semibold) + amountUnitLabel.textColor = .DesignSystem.primary + amountSeparator.backgroundColor = .DesignSystem.separator - [amountFieldLabel, amountTextField, amountSeparator].forEach { + [amountFieldLabel, amountTextField, amountUnitLabel, amountSeparator].forEach { $0.translatesAutoresizingMaskIntoConstraints = false cardView.addSubview($0) } @@ -236,7 +241,10 @@ private extension ExpenseInputViewController { amountTextField.topAnchor.constraint(equalTo: amountFieldLabel.bottomAnchor, constant: 4), amountTextField.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16), - amountTextField.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16), + amountTextField.trailingAnchor.constraint(equalTo: amountUnitLabel.leadingAnchor, constant: -4), + + amountUnitLabel.firstBaselineAnchor.constraint(equalTo: amountTextField.firstBaselineAnchor), + amountUnitLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16), amountSeparator.topAnchor.constraint(equalTo: amountTextField.bottomAnchor, constant: 16), amountSeparator.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),