
안녕하세요. 이번 글에서는 2025년에 진행 중인 과일 신선도 판별 프로젝트 FreshGuard의 AI 모델(YOLOv8 × EfficientNet-B0) 부분을 정리해 보려고 합니다.
“냉장고 문 열고, 먹어도 될지 고민하는 시간 줄여보자.”
FreshGuard는 과일 사진 한 장으로 과일 종류 + 신선도를 판별하는 프로젝트입니다.
이 글에서는 그중에서 FreshGuard의 AI 모델 파트를 중심으로 정리해 보려고 합니다.
전체 팀 구성
이 글은 AI 모델 파트가 중심이고,
마지막에 앱·서버 연동 구조도 간단히 정리하였습니다.
코드와 로그는 여기 정리해 두었습니다. → GitHub – FreshGuard
FreshGuard가 목표로 하는 기능은 다음과 같습니다.
입력: 과일 이미지 1장
출력
fresh / normal / rotten)최종 목표
나중에 앱에서 사진만 찍으면,
이번 글은 모델에 집중하여 정리했고, 팀원의 앱과 서버도 간단하게 기록하였습니다.
개인적으로 떠올렸던 키워드는 1인 가구 + 음식물 쓰레기였습니다.
한국농촌경제연구원 자료에서도
1인 가구가 부적절한 보관으로 상한 음식물 비율이 높다는 내용이 나옵니다.
결국 이런 상황으로 정리할 수 있다.
“지금 이 과일 상태가 어느 정도인지, 눈으로만 보기에 애매하다.”
여기서 출발한 아이디어는 비교적 단순하다.
과일을 카메라로 찍고
AI 모델이 과일 종류 + 신선도를 추정한 뒤
그 결과를 바탕으로
까지 연결하는 것을 목표로 했다.
이번 글에서는 그중에서도 신선도 판별 AI 모델과 추론 파이프라인에 집중해서 정리했고, 레시피 추천은 이후 버전에서 확장할 방향으로만 설계해 둔 상태다.
최종적으로 FreshGuard가 목표로 하는 흐름은 다음과 같다.
현재 1차 버전에서는 다음까지 구현한 상태다.
이 글에서는 이 중에서 AI 모델(EfficientNet-B0 멀티태스크)와 추론 파이프라인에 초점을 맞춰 정리한다.
과일 종류와 신선도 라벨이 함께 들어 있는 구조라
처음부터 멀티태스크 모델로 접근하기 좋은 데이터셋이었다.
과일 클래스 (10종)
apple, banana, bell_pepper, carrot, cucumber,
mango, orange, potato, strawberry, tomato
신선도 클래스 (원본 라벨)
fresh / normal / rotten
이미지를 직접 쭉 확인해보면,
fresh와 rotten은 비교적 구분이 잘 되지만normal은 두 쪽(fresh/rotten)의 경계에 걸쳐 있는 경우가 많다라벨 자체가 애매한 상태에서 3-class를 그대로 쓰면
모델이 애매한 경계를 억지로 나누느라 학습이 꼬일 수 있다고 판단했다.
그래서 신선도는 다음처럼 바꿔서 사용했다.
p_fresh를 이용해서 다시 3단계로 매핑if p_fresh ≥ 0.7 → fresh
if p_fresh ≤ 0.3 → rotten
그 사이 → normal
완벽한 라벨이 아니라고 생각했기 때문에,
Input size: 224 × 224
Normalize: ImageNet mean / std
Augmentation
실제 환경에서 과일이 찍힐 때 생기는
각도, 조명, 약간의 위치 차이 정도는 버틸 수 있도록 설정했다.
모델의 백본은 EfficientNet-B0 (ImageNet pretrained) 하나로 고정했다.
그 위에 Head를 두 개 얹어 멀티태스크 분류 형태로 구성했다.
Backbone
EfficientNet-B0Head 1 – 과일 종류 (Head_fruit)
Head 2 – 신선도 (Head_fresh)
fresh / rotten)Loss는 두 태스크의 loss를 단순 합으로 사용했다.
loss = loss_fruit + loss_fresh
과일 종류와 신선도는 둘 다 같은 이미지를 기반으로 한 판단이라
피처를 공유하는 멀티태스크 구조가
중요한 학습 설정만 정리하면 다음과 같다.
사용 이미지 수: 69,105장
Epoch: 7
Best Epoch: 6
val_f1_fruit + val_f1_fresh 합이 최대였던 시점Best weight 경로
models/efficientnet_b0_freshguard_multitask.pt로그 관리 방식:
logs/log.txtlogs/*.jpeg나중에 다시 볼 때도
“어떻게 학습했는지”를 바로 떠올릴 수 있도록
코드 / 모델 / 로그 / 노트북을 폴더 단위로 나눠 두었다.
멀티태스크 EfficientNet-B0 모델의 Validation 결과는 다음과 같다.
과일 종류는 거의 다 맞는 수준이고,
신선도도 0.98대면 1차 버전 기준으로는 꽤 안정적으로 나온 편이라고 느꼈다.

그래프를 보면:
전체적으로 봤을 때
학습 과정은 비교적 안정적으로 잘 진행된 편이라고 판단했다.

val_f1_fruit
val_f1_fresh
train과 val 지표를 같이 봤을 때,
대표적인 overfitting 패턴은 크게 보이지 않고
2~3 epoch 이후부터는
실제 동작 플로우는 다음과 같다.
앱 (Android, Kotlin)
서버 (Ubuntu + Flask + MySQL)
efficientnet_b0_freshguard_multitask.pt에 넣어서 응답 포맷
[
{"fruit": "apple", "state": "rotten", "confidence": 0.92},
{"fruit": "banana", "state": "fresh", "confidence": 0.88}
]앱 UI 표시
앱 메인 – 식재료 검사 진입

보관 중인 식재료와 최근 검사 기록을 한눈에 보고,
하단식재료 검사버튼으로 촬영 화면으로 이동한다.
식재료 검사 화면
![]() 식재료 검사 화면 |
![]() 식재료 검사 완료된 화면 |
카메라 촬영 또는 갤러리 불러오기를 통해
신선도 판별에 사용할 이미지를 선택하고,
결과를 다음 화면에서 확인할 수 있다.
검사 결과 리스트

AI가 판별한 결과가 히스토리 형태로 쌓여,
어떤 식재료를 먼저 소비해야 할지 관리할 수 있다.
식재료 상세 확인

개별 식재료에 대해 이미지, 촬영 날짜, 신선도 결과,
소비기한 메모를 함께 관리할 수 있도록 구성했다.
추천 레시피 확인 – 목록 + 상세
![]() 개별 레시피 목록 화면 |
![]() 추천 레시피 상세 화면 |
![]() 레시피 화면 |
남아 있는 과일을 활용할 수 있는 레시피를 추천하고,
목록에서 하나를 선택하면 재료와 조리 순서까지 확인할 수 있다.
현재 모델에도 분명 한계점이 있다.
지금 기준으로 생각하고 있는 부분은 아래와 같다.
데이터 도메인 갭
normal 라벨의 애매함
p_fresh 0.3~0.7 구간을 전부 normal로 묶고 있어서,카테고리 확장
지금은 과일만 다루고 있다.
냉장고 전체 관리로 확장하려면
앞으로 보완하고 싶은 방향은 다음과 같다.
실제 촬영 데이터(집/마트/편의점 등)로 에러 케이스 수집
수집된 케이스를 기반으로
p_fresh threshold 재조정normal 구간 재설계앱/서버와 연결된 상태에서 실제 사용 피드백을 받아 보고,
필요하다면 추가적인 fine-tuning이나 domain adaptation 진행
FreshGuard 모델 버전 1을 만들면서,
개인적으로 정리된 포인트는 다음 세 가지였다.
라벨이 애매하면, 문제 정의부터 다시 정리하는 게 낫다.
멀티태스크가 더 자연스러운 문제라면, 한 번에 묶어서 보는 것도 좋다.
결과뿐 아니라, 과정과 구조를 같이 남겨두는 게 중요하다.
docs/, logs/, notebooks/ 등으로 레포 구조를 나눠 두었다.앱/서버까지 붙어서 실제로
“냉장고 정리 도우미”에 가까운 형태가 되면,
까지 한 번 더 정리해 볼 생각이다.
여기까지가
FreshGuard – YOLOv8 × EfficientNet-B0로 구현한 과일 신선도 판별 시스템 v1을 만든 기록
입니다.
혹시 비슷한 문제를 고민 중이거나,
코드가 궁금한 분들은 레포를 참고해 보셔도 좋을 것 같습니다. 😊
긴 글 읽어주셔서 감사합니다 :)