Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions MLTraining/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
Expand All @@ -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]
Expand Down
Binary file modified SpendLearning/SpendLearning/Resource/CategoryClassifier.mlmodel
Binary file not shown.
48 changes: 47 additions & 1 deletion SpendLearning/SpendLearning/Resource/vocabulary.json
Original file line number Diff line number Diff line change
@@ -1,55 +1,100 @@
[
" ",
"&",
"'",
"+",
",",
"-",
".",
"0",
"1",
"2",
"3",
"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",
"ㄱ",
"ㄲ",
"ㄴ",
Expand All @@ -72,6 +117,7 @@
"ㅏ",
"ㅐ",
"ㅑ",
"ㅒ",
"ㅓ",
"ㅔ",
"ㅕ",
Expand All @@ -89,4 +135,4 @@
"ㅡ",
"ㅢ",
"ㅣ"
]
]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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),
Expand Down
Loading