![]() 결과물을 음식에 비유할 때, 데이터는 하나의 재료에 불과합니다. 이 재료를 어떻게 바라보고 어떤 방법으로 요리해 어떤 맛을 내게 할지는 요리사의 역할을 하는 Data Scientist에게 달린 일이죠. 그렇다면 피플펀드의 Data Scientist들은 데이터로 어떤 요리를 만들어 내고 있을까요? 즉 어떤 문제를 해결하기 위해 데이터를 활용하고 있을까요? 질문에 답하기 위해 피플펀드 Data Strategy Team의 민재님께서 작성자로 나섰습니다. |
안녕하세요, 저는 피플펀드 Data Strategy Team(DST)의 Data Scientist 김민재입니다.
지난 6개월간, 피플펀드 DST는 ‘최적의 대출 전략’을 키워드로 ‘가장 적합한 금리와 한도’를 찾아내는 최적화 문제를 푸는 데 몰두해 왔습니다. 이 글에서는 저희 팀이 최적화 문제를 정의한 방법과 그 과정에서 만난 어려움들, 그리고 마침내 문제를 해결해 낸 방법을 공유해 볼까 합니다.
우선, ‘최적’이라는 키워드부터 살펴보자
기업을 운영하는 데 있어 최적의 상황을 도출해 내는 것은 보편의 목표입니다. 가령, 영업이익을 극대화하는 것, 고객 만족을 최대치로 끌어올리는 것, 공정에 걸리는 시간을 최소화하는 것 등이 각 기업이 목표로 하는 최적의 상황이 될 수 있죠.
피플펀드는 투자금을 바탕으로 대출을 실행하는 온라인투자연계금융업을 영위하고 있습니다. 그런 저희에게 최적의 상황이란, 투자자와 차입자 사이에서 균형 잡힌 대출 상품을 만들어 내는 것일 수밖에 없는데요. 투자자만을 위할 수도, 차입자만을 위할 수도 없는 위치에 있다 보니 이해관계의 대척점에 있는 양측 모두를 만족시킬 수 있는 ‘최적의 대출 전략’을 설정하는 것이 저희가 풀어야 하는 미션이라고 할 수 있습니다.
그렇다면, ‘최적의 대출 전략’이란 무엇일까요?
좀 더 자세히 살펴봅시다. 피플펀드 사업구조에서 차입자와 투자자의 니즈는 정반대에 있습니다. 차입자는 가능한 금리를 낮게, 한도를 높게 받고 싶어하는 반면, 투자자는 가능한 이율을 높게, 리스크는 적게 가져가고 싶어 하죠.

피플펀드가 비즈니스를 지속하려면 차입자와 투자자, 각각의 입장 사이에서 균형을 찾아야만 합니다. 투자자의 니즈를 충족하지 못하면 대출을 내보내지 못할 것이고, 마찬가지로 차입자의 니즈를 충족하지 못하면 투자할 상품이 없어지게 되니까요. 둘 모두의 니즈를 충족할 수 있는 최적의 금리와 한도를 가진 대출 상품을 만들어 내야만 하는 것입니다.
이때, 투자자가 보는 상품 매력도는 기대수익률로 수치화하고, 차입자가 보는 상품 매력도는 예상신청률로 수치화할 수 있는데요. 궁극적으로 기대수익률과 예상신청률을 바탕으로 ‘가장 적합한 금리와 한도를 찾아내는 것’이 저희가 정의한 ‘최적의 대출 전략’이라 할 수 있습니다.
문제 설계에 대한 고민: ‘풀리는 문제를 만들자’
자 이렇게 목표는 설정되었으니, 이제 목표값을 도출해 내기 위한 문제를 설계할 차례입니다.
일반적으로 최적화 문제에서 가장 중요한 것은 목적 함수(objective function)의 설정입니다. 최적의 변숫값을 찾기 위해 어떤 요소를 최대화/최소화할 것인지 정하는 과정인데요.
저희는 여기서의 목적 함수를 대출 실행을 통해 발생하는 기대수익금액으로 결정했습니다. 투자자의 수익율을 극대화하면서 차입자에게 매력 있는 금리/한도를 제시하는 방안을 찾기 위해, 기대수익금액을 최대화하는 문제를 풀기로 한 것이죠.
일반적으로 기대수익금액은 신청률 예측이 선행되어야 계산할 수 있는 값으로, 아래와 같은 산식을 가집니다.
기대수익금액 = Σ [(승인여부) x (예상신청률) x {(한도 x (1 - 부도율) x (마진율)) - (한도 x 부도율 x LGD)}]
위 식에서 저희는 ‘승인여부’, ‘부도율’, ‘LGD’ 값을 미리 알 수 있습니다. 먼저, 승인여부 값은 피플펀드의 승인전략 자동 최적화 알고리즘 ‘AGOS’에서 나온 결괏값을 토대로 판단하면 됩니다. 또, 부도율 값은 자체 AI 신용평가시스템으로 계산할 수 있으며, LGD는 부도채권의 손실율로, 금융사의 니즈에 맞춰 상숫값으로 지정할 수 있습니다.
이제 결정해야 하는 변수는 ‘마진율’, ‘한도’, 그리고 ‘예상신청률’인데요. 마진율은 최종 금리에서 조달원가, 운영원가, 신용원가가 빠진 값으로, 피플펀드가 컨트롤할 수 있는 함수입니다. 한도 또한 마찬가지로 피플펀드가 컨트롤해야 하는 함수이고요.
남은 녀석은 ‘예상신청률’입니다. 여기서부터 문제 설계에 대해 진지하게 고민하며 ‘풀리는 문제’를 만들기 위한 방법을 찾아 나섰는데요.
처음에는 문제 간소화를 위해 지난달의 평균 신청률을 예상신청률로 사용하는 등 상숫값으로 근사하려 했습니다. 마진율과 한도가 각각 parameter를 가진 함수의 개형이다 보니, 이미 문제가 꽤 복잡해 보였기 때문인데요. 이런 식으로 문제의 복잡도를 줄여나가는 것은 일반적으로 굉장히 유용한 방법입니다.
그러나 이내, 신청률을 상수로 가정했을 때, 저희의 상황에서는 큰 문제가 발생한다는 것을 깨닫게 되었습니다. 기대수익금액이 금리와 한도에 대해 선형적인 관계가 되어버리는 것인데요. 이 말인즉슨, 최적화 문제를 풀었을 때, 최적의 금리나 한도가 0 또는 ∞(무한대)라는 결론이 나올 수밖에 없음을 의미합니다. 당연히 이는 잘못된 문제 설계라고 할 수 있죠.
따라서 저희는 현실을 반영한, 즉 금리가 오를 때 신청률이 내려가고, 한도가 오를 때 신청률이 올라가는 신청예측모델을 만드는 것이 필요했습니다. 이렇게 피플펀드론을 조회한 고객의 신청률을 예측하는 것은 기존에 해본 적 없는 태스크였고, 금리와 한도에 대한 도메인 지식이 적용되어야 하는 예측모델이라 더 어렵게 느껴지기도 했습니다. 지금부터는 이러한 어려움을 어떻게 극복했는지 더 자세히 말씀드리도록 하겠습니다.
신청예측모델: 도메인 지식의 적용
여기까지 글을 읽으신 분들이라면 ‘단조증가(monotonically increasing)’의 개념에 대해선 어느 정도 알고 계실 거라 생각합니다. 한 변수 x가 증가할 때, 다른 변수 y가 감소하지 않는다면, y가 x에 대해 단조증가 한다고 하는데요. 앞서 ‘금리가 오를 때 신청률이 내려가고, 한도가 오를 때 신청률이 올라가는’이라고 언급한 것에 이 개념을 적용해 보면, 신청률은 금리에 대해 단조감소 하고, 한도에 대해 단조증가 해야 합니다.
이러한 금리/한도와 신청률의 관계도 도메인 지식의 일종인데요. 다양한 분야의 비즈니스에서 도메인 지식들은 이렇게 ‘단조성(monotonicity)’의 성질을 갖고 있습니다. 그런데 XGboost, LightGBM, CatBoost 등 gradient boosting 계열의 모델들은 decision tree들을 연결한 형태이기 때문에 기본적으로는 단조성을 보장하지 않습니다. 다만 도메인 지식의 중요성은 머신러닝 분야에서 이전부터 대두되어 왔기 때문에, monotone_constraints 기능을 지원하고 있죠.
예시를 통해 구체적으로 설명해 보겠습니다. 일반적으로 XGboost에서 monotone 조건을 부여하는 방법은 다음과 같습니다.
monotone_direction = list()
for col in X_train.columns:
if col == "loan_size":
monotone_direction.append("1") # monotonically increasing
elif col == "interest_rate":
monotone_direction.append("-1") # monotonically decreasing
else:
monotone_direction.append("0")
monotone_constraints = "(" + ",".join(monotone_direction) + ")"
params = {
"objective": "binary:logistic",
"tree_method": "gpu_hist",
"monotone_constraints": monotone_constraints,
}
dtrain = xgb.DMatrix(X_train, y_train, enable_categorical=True)
dvalid = xgb.DMatrix(X_valid, y_valid, enable_categorical=True)
model = xgb.train(
params=params,
dtrain=dtrain,
evals=[(dtrain, "train"), (dvalid, "valid")],
num_boost_round=1000,
early_stopping_rounds=20,
verbose_eval=1
)
이때, test set에서 10명을 무작위로 추출하고, 각 고객의 금리와 한도를 바꿔가며 예상신청률이 어떻게 변하는지 그래프를 그려 보았는데요.
monotone_constraints 적용 전에는 아래와 같이 신청률이 금리/한도에 대해 단조성을 보이지 않는, 그러니까 증가했다 감소했다 하는 모습을 살펴볼 수 있었습니다.


그런데 여기에 monotone_constraints를 적용하고 나니, 아래와 같이 신청률이 금리/한도에 대해 단조성을 보이는 모습을 볼 수 있었습니다.


결과를 받아보고 처음엔 신이 났지만, 곧이어 이 또한 잘못된 방식이라는 점을 깨달았습니다. 현 상황은 금리/한도 항목과 그 외 고객의 특성을 나타내는 feature들, 예를 들면 카드 한도소진율, 대출보유건수 등을 한데 넣고 모델을 학습시킨 것인데, 이는 통계적인 관점에서 옳은 방법이 아닙니다.
왜냐하면 피플펀드는 각 고객의 특성을 반영해 금리와 한도를 부여하기 때문인데요. 이는 다시 말해 모델이 학습한 피플펀드의 데이터셋에서 금리/한도와 다른 feature들이 서로 종속적이라는 걸 의미합니다. 머신러닝 모델을 학습할 때는 feature들 간의 종속성을 일정 수준 이하로 줄여야 한다는 걸 간과했다는 점에서 아쉬움이 남았습니다.
이걸 깨닫게 된 이상 XGboost의 monotone_contraints 기능은 더 이상 좋은 옵션이 될 수 없었습니다. 다시 방법을 찾아 나서야 했습니다.
외부 데이터에서 답을 찾다
저희는 금리/한도에 대해 피플펀드 밖에서 얻을 수 있는 데이터로 눈을 돌렸습니다. 그리고 여신업체별 평균 금리 등의 공개된 데이터를 가공해 자체적인 금리/한도 결합확률분포를 생성하는 방식을 택했습니다. 이렇게 더 크고 방대한 범위의 데이터를 활용함으로써 대출비교서비스라는 경쟁적인 환경에서 피플펀드론이 대출조회고객으로부터 선택받을 확률을 보다 정확히 계산해 낼 수 있었는데요.
결합확률분포의 구체적인 생성 방식은 밝힐 수 없지만, 이를 시각화한 모습을 공유하려 합니다. 아래 이미지에서 x축은 금리, y축은 한도, z축은 비율을 나타내며 각각 🟪 – 은행업권 / 🟦 – 카드업권 / 🟩 – 캐피탈업권 / 🟥 – 저축은행업권을 의미합니다.

이 결합확률분포를 활용해 학습시킨 신청률 예측모델을 사용하면, 금리 한도에 대한 monotonicity가 아래 이미지와 같이 나타납니다. 마찬가지로 무작위 10명에 대한 그래프를 그려보면, 훨씬 더 현실적인 경향성을 만들어 내는 데 성공한 것을 볼 수 있습니다.


이때, 신청률-금리 그래프에서 또 하나를 데이터로 증명해 낼 수 있었는데요. 그래프를 보면 약 7% ~ 10% 금리 사이에 공통적으로 평평한 구간이 나타나는 것을 볼 수 있습니다. 이는 그 사이 금리로 대출을 내어주는 기관이 없다는 것을 의미하고, 이것이 바로 피플펀드가 해결하고자 하는 ‘금리절벽(금리단층)’에 해당함을 확인할 수 있었습니다.
새로운 시도: 금리/한도 최적화에 Optuna를 사용하다
긴 여정이었네요. 다시 원래의 목적으로 돌아가 볼까요? 저희는 기대수익금액 최대화 문제를 풀고 있었고, 목적 함수의 산식은 아래와 같았습니다.
기대수익금액 = Σ [(승인여부) x (예상신청률) x {(한도 x (1 - 부도율) x (마진율)) - (한도 x 부도율 x LGD)}]
결합확률분포를 활용해 신청률 예측모델을 만들면서 마침내 기대수익금액 계산에 들어가는 모든 변수를 알 수 있게 되었습니다. 이제 본격적으로 최적화 모델링에 돌입할 수 있게 된 것이죠.
저희 팀은 이때 사용할 툴로 머신러닝 hyperparameter tuning을 위한 프레임워크로 알려진 Optuna를 채택했습니다. hyperparameter tuning은 제약 조건이 변수들의 범위밖에 없는 최적화 문제인데요. 이 때문에 Optuna를 사용할 때, suggest_float 등의 함수에서 각 변수의 범위를 지정하게 됩니다.
그런데, 저희가 풀어내고 있는 금리/한도 최적화 문제의 제약 조건은 한층 복잡합니다. 첫째, 피플펀드가 더 우량하다고 판단한 고객에게 더 낮은 금리를 제공해야 한다는 것과 둘째, 전체 리스크 수준이 특정 값 이하여야 한다는 조건을 가지고 있죠.
만약 이렇게 다소 복잡한 관계식을 제약 조건에 추가해야 한다면 어떻게 될까요? 당연히 앞에 얘기한 방식으로는 해결이 불가능한 문제가 발생하는데요. 또다시 방법을 찾아야만 했습니다.
Optuna에서 이러한 제약 조건을 부여하기 위해 저희가 찾은 방법은 이렇습니다. 첫째, objective 함수에서 trial 객체의 user_attrs에 constraint 값을 저장해야 한다. 둘째, constraint 값을 return하는 별도의 함수를 만든 뒤 Sampler 선언 시 넣어주어야 한다.
백문이 불여일견, 예시 코드는 아래와 같습니다.
from typing import List, Dict
import optuna
from optuna.samplers import NSGAIISampler
param_ranges = dict(
param1=(0, 1),
param2=(1, 5),
param3=(0, 0.1)
)
population_size = 50
num_generations = 40
def objective_func(trial: optuna.Trial) -> float:
params = dict()
for name, range_ in param_ranges.items():
params[name] = trial.suggest_float(name, *range_)
# calculate and save constraint values
constraints = calculate_constraints(params)
trial.set_user_attr("constraint", constraints)
# calculate objective value
obj = calculate_objective(params)
return obj
def constraint_func(trial: optuna.Trial) -> List[float]:
return trial.user_attrs["constraint"]
def calculate_objective(params: Dict[str, float]) -> float: ...
def calculate_constraints(params: Dict[str, float]) -> List[float]: ...
sampler = NSGAIISampler(
population_size=population_size,
constraints_func=constraint_func
)
study = optuna.create_study(
sampler=sampler,
direction="maximize"
)
study.optimize(
func=objective_func,
n_trials=population_size * num_generations,
show_progress_bar=True
)
그런데 Optuna로 constraint가 있는 최적화 문제를 풀 때 주의할 점이 하나 있습니다. Optuna는 기본적으로 각 trial 종료 시 ‘지금까지의 trial 중 가장 최적인 해의 목적함숫값’을 로그에 남기는데요. 그러나 이때 로그에 찍힌 최적해는 constraint 만족 여부가 체크되지 않은, 단지 목적함숫값이 가장 큰 해입니다. 즉 아래 예시처럼 constraint를 만족하지 않는 해인 것이죠.

따라서 진짜 Best Solution을 찾기 위해선 최적화가 모두 완료된 뒤, 직접 모든 trial들의 constraint 만족 여부를 체크하고 constraint를 만족하는 해들 중에서 목적함숫값이 가장 큰 해를 찾아야만 합니다. 그리고 그런 일련의 과정을 거쳐야만 마침내, 금리/한도 전략의 최적 parameter을 찾을 수 있는 것입니다.
마치며: 매듭은 아직 다 풀리지 않았다
지금까지 저를 비롯한 피플펀드 DST가 ‘최적의 대출 전략’을 키워드로 최적화 문제를 풀어낸 방법에 대해 공유드렸습니다. 이런 과정을 통해 앞서 선언한 ‘예상신청률과 기대수익률을 바탕으로 최적의 금리/한도 전략을 찾아낸다’는 미션을 달성하는 최적화 모델 1.0을 만들 수 있었는데요.
이 최적화 모델이 산출하는 최적의 금리/한도 전략은 올 하반기 피플펀드가 실제 개인신용대출 금리/한도 전략을 운영할 때 ‘가이드라인’으로 사용되기 시작할 예정이고, 이후 몇 가지 테스트를 진행한 뒤 ‘실제 금리/한도 산출‘에도 사용할 계획이 있습니다.
‘최적화’라는 키워드가 의미하는 바가 있듯, 사실 여전히 업그레이드와 고도화의 여지가 많이 남아 있습니다. 저희도 당연히 여기서 그치지 않고 모형을 계속해서 발전시킬 예정이고요.
약간의 힌트를 드리자면, 장기적으로는 강화학습을 도입해 볼 생각입니다. 기준금리 등 경제 상황이 시간의 흐름에 따라 계속 변화하니, 그에 따라 피플펀드의 금리/한도 전략 역시 계속해서 변화해야 하기 때문인데요. 매달 거시경제 변수를 측정하고 전략을 업데이트하는 대신, 자동으로 거시경제 변화에 대응해 나가도록 모델을 설계하는 것이 저희 DST의 원대한 목표라 할 수 있습니다.
개인적으로는 최적화의 밑바탕을 계획하는 것부터 최적해를 뽑아내기까지의 모든 과정이 매우 유익한 경험이었습니다. 신청예측모델을 만들어 나가는 과정에서는 도메인 지식의 중요성을 실감했고, 세심하게 문제를 정의하며 우리의 비즈니스에 대한 이해도 또한 자연스레 올라갈 수밖에 없었죠.
제가 연구해 만들어 낸 결과가 실제 상품 취급에 적용되는 모습을 곧 볼 수 있다고 생각하니 매우 뿌듯한 마음인데요. 여기서 더 나아가 이 글을 공유함으로써 비슷한 고민을 하는 많은 ML 엔지니어/데이터 사이언티스트들에게도 도움이 되었으면 좋겠다는 생각과 함께 글을 마치고 싶습니다.
마지막으로, 끊임없이 의견을 공유하며 난제들을 함께 해결해 온 CRO 승우님과 동료 한결님, 우찬님께 진심을 담은 감사 인사를 전합니다.
written by Minjae
edited by Hoonjung
최적의 동료들과 최적화 문제를 풀어내고 싶다면 🦋