인공지능 학습하기

이 문서는 작성중인 문서입니다.

이 문서에서는 entry-ml 라이브러리를 사용한 ‘인공지능 학습하기’ 기능 구현을 설명합니다.

목록






설치 및 세팅

2024.06.04 기준 entry-ml 레포지토지토리는 압축된 번들 파일로 제공됩니다.
entry-ml 파일을 제공받는 방법에 대해서는 엔트리 고객센터로 문의 부탁드립니다.

제공받은 압축파일을 해제하면 아래와 같은 구조로 되어 있습니다.
아래의 파일들을 모두 같은 위치의 디렉토리에 두고 사용해야 합니다.

External JavaScript로 가져오기

html에서 script태그를 사용하여 번들js와 css파일을 불러와 사용할 수 있습니다.
불러올 js와 css는 entry-ml.js와 entry-ml.css입니다.

# 설치경로에서 가져오기: 직접 추가한 경우
<link rel='stylesheet' href='파일경로/entry-ml.css'/>
<link rel='stylesheet' href='파일경로/bb.css'/>
<script type="text/javascript" src='파일경로/entry-ml.js'></script>
추가로 bb.css 파일도 아래 코드를 복사하여 생성 후 추가해주시기 바랍니다.
bb.css
.bb-color-pattern {
background-image: url('#2d51ac;#4f80ff;#9fbaff;#a23941;#f16670;#fcad93;#423496;#6e5ae6;#c5b4ff;#2a7d7f;#00b6b1;#b3c3cd;');
}

/*-- Chart --*/
.bb svg {
font-size: 12px;
font-family: 'Meiryo', sans-serif, Arial, 'nanumgothic', 'Dotum';
line-height: 1;
}

.bb path,
.bb line {
fill: none;
stroke: #2c313d;
}

.bb text,
.bb .bb-button {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
fill: #2c313d;
font-size: 11px;
}

.bb-legend-item-title,
.bb-xgrid-focus,
.bb-ygrid,
.bb-event-rect,
.bb-bars path {
shape-rendering: crispEdges;
}

/*-- Axis --*/
.bb-axis {
shape-rendering: crispEdges;
}

.bb-axis-y text,
.bb-axis-y2 text {
fill: #2c313d;
}

.bb-event-rects {
fill-opacity: 1 !important;
}
.bb-event-rects .bb-event-rect {
fill: transparent;
}
.bb-event-rects .bb-event-rect._active_ {
fill: rgba(39, 201, 3, 0.05);
}

.tick._active_ text {
fill: #00c83c !important;
}

/*-- Grid --*/
.bb-grid line {
stroke: #f1f1f1;
}

.bb-grid .bb-ygrid:last-child {
stroke: #e9e9e9;
}

.bb-xgrid-focus line {
stroke: #ddd;
}

/*-- Text on Chart --*/
.bb-text.bb-empty {
fill: #767676;
}

/*-- Line --*/
.bb-line {
stroke-width: 1px;
}

/*-- Point --*/
.bb-circle._expanded_ {
fill: #fff !important;
stroke-width: 2px;
stroke: red;
}

rect.bb-circle._expanded_,
use.bb-circle._expanded_ {
stroke-width: 1px;
}

.bb-selected-circle {
fill: white;
stroke-width: 2px;
}

/*-- Bar --*/
.bb-bar {
stroke-width: 0;
}
.bb-bar._expanded_ {
fill-opacity: 0.75;
}

/*-- Focus --*/
.bb-target.bb-focused {
opacity: 1;
}
.bb-target.bb-focused path.bb-line,
.bb-target.bb-focused path.bb-step {
stroke-width: 2px;
}

.bb-target.bb-defocused {
opacity: 0.3 !important;
}

/*-- Region --*/
.bb-region {
fill: steelblue;
fill-opacity: 0.1;
}
.bb-region.selected rect {
fill: #27c903;
}

/*-- Zoom region --*/
.bb-zoom-brush {
fill-opacity: 0.1;
}

/*-- Brush --*/
.bb-brush .extent {
fill-opacity: 0.1;
}

/*-- Select - Drag --*/
/*-- Legend --*/
.bb-legend-item {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

.bb-legend-item-hidden {
opacity: 0.15;
}

.bb-legend-background {
opacity: 0.75;
fill: white;
stroke: lightgray;
stroke-width: 1;
}

/*-- Title --*/
.bb-title {
font-size: 14px;
}

/*-- Tooltip --*/
.bb-tooltip-container {
z-index: 10;
font-family: 'Meiryo', sans-serif, Arial, 'nanumgothic', 'Dotum';
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

.bb-tooltip {
border-collapse: separate;
border-spacing: 0;
empty-cells: show;
border: 1px solid #999;
background-color: #fff;
text-align: left;
font-size: 11px;
}
.bb-tooltip th {
font-size: 12px;
padding: 4px 8px;
text-align: left;
border-bottom: solid 1px #eee;
}
.bb-tooltip td {
padding: 4px 6px;
background-color: #fff;
}
.bb-tooltip td:first-child {
padding-left: 8px;
}
.bb-tooltip td:last-child {
padding-right: 8px;
}
.bb-tooltip td > span,
.bb-tooltip td > svg {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 6px;
border-radius: 5px;
vertical-align: middle;
}
.bb-tooltip td.value {
border-left: 1px solid transparent;
}
.bb-tooltip .bb-tooltip-title {
display: inline-block;
color: #aaa;
line-height: 20px;
}
.bb-tooltip .bb-tooltip-detail table {
border-collapse: collapse;
border-spacing: 0;
}
.bb-tooltip .bb-tooltip-detail .bb-tooltip-name,
.bb-tooltip .bb-tooltip-detail .bb-tooltip-value {
font-size: 11px;
line-height: 13px;
padding: 4px 0 3px;
color: #444;
text-align: left;
font-weight: normal;
}
.bb-tooltip .bb-tooltip-detail .bb-tooltip-value {
padding-left: 5px;
font-weight: 800;
font-size: 12px;
}

/*-- Area --*/
.bb-area {
stroke-width: 0;
opacity: 0.2;
}

/*-- Arc --*/
.bb-chart-arcs-title {
dominant-baseline: middle;
font-size: 1.3em;
}

.bb-chart-arcs-gauge-title {
dominant-baseline: middle;
font-size: 2.7em;
}

.bb-chart-arcs .bb-chart-arcs-background {
fill: #e0e0e0;
stroke: none;
}

.bb-chart-arcs .bb-chart-arcs-gauge-unit {
fill: #000;
font-size: 16px;
}

.bb-chart-arcs .bb-chart-arcs-gauge-min,
.bb-chart-arcs .bb-chart-arcs-gauge-max {
fill: #777;
}

.bb-chart-arcs .bb-chart-arcs-title {
font-size: 16px !important;
fill: #000;
font-weight: 600;
}

.bb-chart-arcs path.empty {
fill: #eaeaea;
stroke-width: 0;
}

.bb-chart-arc .bb-gauge-value {
fill: #000;
}

.bb-chart-arc path {
stroke: #fff;
}

.bb-chart-arc text {
fill: #fff;
font-size: 13px;
}

/*-- Radar --*/
.bb-chart-radars .bb-levels polygon {
fill: none;
stroke: #848282;
stroke-width: 0.5px;
}

.bb-chart-radars .bb-levels text {
fill: #848282;
}

.bb-chart-radars .bb-axis line {
stroke: #848282;
stroke-width: 0.5px;
}

.bb-chart-radars .bb-axis text {
font-size: 1.15em;
cursor: default;
}

.bb-chart-radars .bb-shapes polygon {
fill-opacity: 0.2;
stroke-width: 1px;
}

/*-- Button --*/
.bb-button {
position: absolute;
top: 10px;
right: 10px;
}
.bb-button .bb-zoom-reset {
border: solid 1px #ccc;
background-color: #fff;
padding: 5px;
border-radius: 5px;
cursor: pointer;
}



공통

entry-ml 모듈은 학습하기 팝업을 제어하는 Popup과 예측팝업을 제어하는 Predict를 제공합니다.

이 2가지 기능 모두에서 공통으로 사용하는 기능을 기술합니다.

전반적을 entry-tool의 Popup 공통기능과 동일합니다.

인스턴스명은 예시를 위해 임의로 learningPopup로 지정합니다.


get, set

인스턴스 생성시 생성자 함수의 파라미터로 넣어준 값들의 js get, set 함수를 지원합니다.

// 팝업을 렌더할 돔 리턴 : HTMLDivElement 
learningPopup.container;
// 팝업을 렌더할 돔 할당(HTMLDivElement)
learningPopup.container(container);

// 팝업의 노출여부 리턴 : boolean
learningPopup.isShow

show

팝업을 노출시킵니다.

내부적으로 ‘show’ 이벤트를 emit합니다.

파라미터 data는 타입정의 : data에서 기술합니다.

변경사항이 적용된 본인 인스턴스를 리턴합니다.

learningPopup.show(data);

hide

팝업을 갑춥니다.

내부적으로 ‘hide’ 이벤트를 emit합니다.

파라미터 data는 타입정의 : data에서 기술합니다.

변경사항이 적용된 본인 인스턴스를 리턴합니다.

learningPopup.hide(data);

setData

팝업에서 표시해줄 데이터를 세팅합니다.

내부적으로 render함수를 호출해서 data변경후 리랜더합니다.

파라미터 data는 타입정의 : data에서 기술합니다.

변경사항이 적용된 본인 인스턴스를 리턴합니다.

learningPopup.setData(data);

remove

팝업의 컨테이너와 내부 데이터를 모두 초기화 후 제거합니다.

learningPopup.remove();

타입정의 : data

파라미터 타입 선택적 사용처 기본값 설명
model String ✔️ Popup undefined ‘현재 작품에 적용’된 모델의 id.
값이 없을경우 신규 모델을 생성합니다.
title String Predict 팝업의 상단에 노출될 타이틀값입니다.
predictInfo Object : { type, predict, recordTime } Predict 학습된 모델의 예측에 관한 정보를 포함하는 객체입니다.
project String ✔️ Popup undefined 현재 작품 프로젝트의 id입니다.
tables Array< Object > ✔️ Popup undefined 학습에 사용할 테이블 데이터입니다.
일부 모델에서는 사용하지 않습니다.
availables Array< string > ✔️ Popup undefined 생성 가능한 모델종류를 제한합니다.
값이 없을경우 모든 종류의 모델을 제공합니다.
tokenizerPath String ✔️ Popup ‘/lib/entry-ml/dist/resources’ ‘분류:텍스트’학습하기에서 사용되는 형태소 분석 모듈의 경로입니다. ‘파일경로/resources’ 경로에 해당 파일이 있습니다.


학습하기 팝업 제어(Popup)

ml_popup_main

ml_popup_text

entrylabs/ml은 사용자가 원하는 학습모을 선택하고 학습하는 팝업을 제공합니다.

이 팝업은 Popup 인스턴스를 사용해서 제어할 수 있습니다.


사용자들이 인공지능 모델을 학습하는 UI를 팝업형태로 생성합니다.

entry-tool을 활용한 팝업과 유사합니다.

Vanilla JS

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>EntryJS Base Example</title>
<link rel="stylesheet" href="./test/ai/entry-ml.css" />
</head>
<body>
<button onClick={loadPopup()}>show</button>
<div id="popupContainer"></div>
<script type="text/javascript" src="./test/ai/entry-ml.js"></script>
<script>
learningPopup = new Entryml.Popup({
container: document.getElementById('popupContainer'),
isShow: false,
data: {
tokenizerPath: '/test/ai/resources',
},
});
const loadPopup = () => {
learningPopup.show();
}
</script>
</body>
</html>

next.js

webpack externals. 설정을 이용하여 아래와 같이 사용 가능합니다.

webpack 설정.

config.externals = [
{
'@entrylabs/tool': 'EntryTool',
'@entrylabs/tool/component': 'EntryTool.Component',
'@entrylabs/ml': 'Entryml',
},
];
const learningPopup = {};
const learningContainer = {};

// entrylabs/ml 라이브러리 import
const { Popup } = await import('@entrylabs/ml');
// 팝업 인스턴스가 없는 경우, 팝업을 띄울 DOM 컨테이너 생성
if (!Popup.instance) {
this.learningContainer = this.createContainer('EntryLearningContainer');
}
// 팝업 초기화에 파라미터로 넣어줄 테이블 데이터를 WS에서 로드
const tables = Entry.playground.dataTable.getTableJSON();

// 팝업을 초기화하고 생성
this.learningPopup = new Popup({
container: this.learningContainer,
isShow: false,
dataApi: this.dataApi,
i18n: {
ko: {
learning: {
menu_my_model: 'my model',
},
},
},
data: {
tables,
tokenizerPath: '[entry-ml 파일경로]/resources',
},
})
파라미터 타입 선택적 기본값 설명
container HTMLDivElement ✔️ document.createElement('div') 팝업을 띄울 DOM 컨테이너
isShow Boolean ✔️ true 팝업 생성시 노출여부
dataApi Class 학습 데이터를 관리하기 위한 api 클래스 입니다.
자세한 내용은 dataApi에서 기술합니다.
i18n Object 다국어 정보입니다. 다국어 에셋(i18n)에서 데이터를 제공합니다.
data Object ✔️ { } 팝업에 띄울 여러 데이터. 타입정의: data에 정의되어 있습니다.

ml_popup_open

WS 블록메뉴에서 ‘인공지능 모델 학습하기’ 클릭시, entryjs에서 ‘openAIUtilizeTrainManager’ 이벤트가 dispatch됩니다.

해당 이벤트를 핸들링하여 Popup인스턴스를 적절히 초기화해주는 로직이 필요합니다.

const learningPopup = {}
const project = {};

initLearningPopup() {
// Popup 인스턴스 초기화
this.learningPopup = new Popup({});
}

addEventListener('openAIUtilizeTrainManager', () => {
// Popup 인스턴스 생성 및 초기화. 위쪽 예제 참고
initLearningPopup();
// WS에서 현재 작품에 적용된 모델의 id값을 가져옵니다. 적용된 모델이 없는경우 undefined가 리턴됩니다.
const model = Entry.aiLearning.getId();
// 학습에 사용할 테이블정보를 WS에서 가져옵니다.
const tables = Entry.playground.dataTable.getTableJSON();
// 생성 가능한 모델종류를 제한합니다. WS가 교과형인 경우 이미지, 소리, 텍스트를 제공하고 일반인경우 모두 제공합니다.
const availables = EntryStatic.isPracticalCourse ? ['image', 'speech', 'text'] : undefined;
// 학습하기 팝업에 데이터를 추가하고 노출합니다.
this.learningPopup.show({
tables,
model,
project: this.project,
availables,
})
}

생성한 팝업이 유저 인터액션에 대응할 수 있게, 이벤트를 핸들링합니다.


submit

사용자가 ‘학습하기’페이지에서 생성한 모델을 ‘적용하기’ 버튼으로 제출시 발생하는 이벤트입니다.

학습된 모델을 기반으로 ‘인공지능 학습 블록’을 WS에 등록합니다.

const learningPopup = new Popup({});

learningPopup.on('submit', (aiLearningBlockItem) => {
// WS에 인공지능 블럭메뉴 초기화
Entry.dispatchEvent('popupAddBlocks', { category: 'ai_utilize' });
// 파라미터로 받아온 인공지능블럭 정보로 ai블럭 추가
Entry.playground.setAiLearningBlock(aiLearningBlockItem);
})

샘플 데이터 : aiLearningBlockItem

sample.json
{
"name": "새로운 모델",
"model": "모델id",
"version": 6003,
"_id": "id",
"url": "learning/models/60a723bc7173aa2b29387fb3/6630b642aa6fdf26c344df64/0",
"labels": [null],
"type": "logisticRegression",
"recordTime": 3000,
"trainParam": {
"epochs": 30,
"batchSize": 16,
"learningRate": 0.001,
"validationRate": 0.25,
"k": 4,
"optimizer": "adam",
"initialCentroids": "kmpp",
"neighbors": 2,
"testRate": 0.2,
"maxDepth": 3,
"minNumSamples": 5,
"C": 0.00001,
"kernel": "linear",
"degree": 3,
"gamma": 1
},
"classes": [
{
"name": null,
"id": "y33l",
"samples": [
{
"data": {
"_id": "63aaa00fd660420030fee255",
"id": "yytv",
"fields": ["연도", "연평균", "봄", "여름", "가을", "겨울"],
"name": "계절별 기온",
"object": null,
"data": [
[1973, 12.4, 11.6, 24.5, 12.9, -1.4],
[1974, 11.4, 10.8, 22.4, 13, -0.1],
[1975, 12.6, 11.2, 23.9, 15.5, 0.3],
...
],
"origin": [
[1973, 12.4, 11.6, 24.5, 12.9, -1.4],
[1974, 11.4, 10.8, 22.4, 13, -0.1],
[1975, 12.6, 11.2, 23.9, 15.5, 0.3],
...
],
"chart": [],
"isCloud": false,
"cloudDate": null,
"summary": "우리나라의 연평균 기온과 계절별 평균 기온입니다. (℃)",
"updated": "2024-04-30T08:55:26.285Z",
"fieldsInfo": [
{
"name": "연도",
"index": 0,
"isNumber": true
},
{
"name": "연평균",
"index": 1,
"isNumber": true
},
{
"name": "봄",
"index": 2,
"isNumber": true
},
{
"name": "여름",
"index": 3,
"isNumber": true
},
{
"name": "가을",
"index": 4,
"isNumber": true
},
{
"name": "겨울",
"index": 5,
"isNumber": true
}
],
"select": [[1, 3], [5]]
}
}
],
"idx": 1
}
],
"result": {
"select": [[1, 3], [5]],
"fields": ["연도", "연평균", "봄", "여름", "가을", "겨울"],
"confusionMatrix": [
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0
],
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0
],
...
],
"accuracy": 0,
"f1": 0,
"valueMap": {
"1": "-1.4",
"2": "-0.1",
"3": "0.3",
"4": "-1.7",
...
},
"precision": 0,
"recall": 0
}
}

타입정의 : aiLearningBlockItem

파라미터 타입 설명
name String 학습한 모델의 이름입니다. 사용자가 직접 작성합니다.
model String 선택된 모델의 id값입니다.
version Number 모델의 버전값입니다. 재학습할 때마다 값이 높아집니다.
_id String aiLearningBlockItem을 구분하는 유니크 id 값입니다.
url String 저장된 모델파일의 경로입니다.
labels Array< String > ‘classes’ 파라미터에 대한 라벨정보입니다.
type String 학습한 모델의 타입입니다. ex) text, speech
recordTime Number ‘분류:소리’ 모델을 학습할 경우, 녹음시간 값입니다.
trainParam Object 학습하기 팝업에서 ‘학습(입력한 데이터로 모델을 학습합니다.)’ 박스에서 사용한 파라미터입니다.
classes Array< Object > ‘분류:이미지’ 처럼 데이터 입력을 ‘클래스’로 받는 모델의 클래스정보입니다.
WS에서 클래스를 변경하여 재학습하는 블럭에서 사용합니다.
result Object ‘군집:k-평균’처럼 학습하기 팝업에서 결과(학습한 모델의 결과를 확인합니다.)
박스에 자체적인 결과값을 가지는 경우의 결과값입니다.
일반적으로 직접 사용하는 경우는 없습니다.

remove

사용자가 프로젝트에 적용된 학습모델을 제거하는 등, 블록메뉴의 ‘인공지능 블록’을 제거해야 할 때 발생합니다.

const learningPopup = new Popup({});

learningPopup.on('remove', () => {
// 인공지능 블록을 블록메뉴에서 제거
Entry.aiLearning.removeLearningBlocks();
}


예측 팝업 제어(Predict)

‘분류: 이미지’, ‘분류: 텍스트’, ‘분류: 소리’ 학습하기는 작품 실행 중 ‘학습한 모델로 분류하기’ 블럭을 실행하면 Predict 팝업을 제공합니다.

ml_predict_text

ml_predict_image

ml_predict_sound

Predict 인스턴스 초기화

const learningPredictContainer = {};
const learningPredict = {};

// entrylabs/ml 라이브러리 import
const { Predict } = await import('@entrylabs/ml');
// Predict 인스턴스가 없는 경우, Predict를 띄울 DOM 컨테이너 생성
if (!Predict.instance) {
this.learningPredictContainer = this.createContainer('EntryLearningPredictContainer');
}

// 팝업을 초기화하고 생성
this.learningPredict = new Predict({
container: this.learningPredictContainer,
isShow: false,
i18n: {
ko: {
learning: {
menu_my_model: 'my model',
},
},
},
})

Predict 팝업 호출

ml_predict_open

entryjs의 WS는 ‘학습한 모델로 분류하기’ 블럭이 실행될 때, ‘openMLInputPopup’ 이벤트를 dispatch합니다.

해당 이벤트의 핸들러에서 Predict 팝업을 초기화하고 show 하는 로직을 추가해 주어야 합니다.

addEventListener('openMLInputPopup', (predictInfoItem) => {
// Predict 인스턴스 생성 및 초기화. 위쪽 예제 참고
initLearningPredict();
// 작품이 실행중인 경우, 작품을 일시정지
if (Entry.engine.state == 'run') {
Entry.engine.togglePause({ visible: false });
}
// 데이터 예측에 필요한 값인 predictInfoItem를 매개변수로 Predict.data를 수정하고 팝업 노출
this.learningPredict.show({
title: '데이터 입력',
predictInfo: {
...predictInfoItem,
},
});
});

샘플 데이터 : predictInfoItem

sample.json
{
"type": "text",
"url": "/uploads/learning/models/60a723bc7173aa2b29387fb3/6630ca751f4a6cf7dc69c6c7/0/model.json?1714474637659",
"predict": ƒ (t)
"setResult": ƒ (e)
"labels": [
"클래스이름 1",
"클래스이름 2"
]
}

타입정의 : predictInfoItem

파라미터 타입 설명
labels Array< string > 학습에 사용한 클래스의 이름값입니다.
predict Function(data: any):
Promise<Array<{ className, probability }>>;
입력받은 데이터로 예측값을 산출해주는 함수입니다. 직접 사용하진 않습니다.
setResult Function(result: any): void; 결과값을 세팅하기 위한 함수입니다. 직접 사용하진 않습니다.
type String 학습한 모델의 종류입니다.
url String 저장된 모델파일의 경로입니다.

Predict 인스턴스 이벤트

show

Predict 팝업을 노출할때 발생합니다.

‘학습한 모델로 분류하기’ 블럭 실행시 이벤트가 emit됩니다.

const learningPredict = new Predict({});

this.learningPredict.on('show', () = > {
this.isPauseClicked = false;
// 작품이 실행되는 중이라면, 작품을 일시정지합니다.
if (Entry.engine.state == 'run') {
// 작품 일시정지 여부를 토글하는 함수입니다.
Entry.engine.togglePause({ visible: false });
}
})

hide

Predict 팝업을 감출때 발생합니다.

Predict 팝업의 우측상단 닫기버튼을 누르거나, ‘작품 정지하기’를 클릭하면 동작합니다.(이 경우, stop이벤트 이후에 hide이벤트가 발생합니다.)

const learningPredict = new Predict({});
// Predict 팝업에서 작품 일시정지를 눌렀는지 자체 판단하기 위한 예시 변수입니다.
const isPauseClicked = false;

this.learningPredict.on('hide', () = > {
// 작품이 멈춤 상태고, Predict 팝업에서 '작품 일시정지' 상태가 아니라면 작품을 재실행합니다.
if (Entry.engine.state == 'pause' && !this.isPauseClicked) {
// 작품 일시정지 여부를 토글하는 함수입니다.
Entry.engine.togglePause({ visible: false });
}
})

pause

Predict 팝업의 ‘작품 일시정지’, ‘작품 다시 시작’ 버튼을 클릭하면 발생합니다.

isPauseClicked : 엔트리의 일시정지와 Predict 팝업의 일시정지 버튼의 상태를 동기화 하기 위해 사용됩니다.
const learningPredict = new Predict({});
// Predict 팝업에서 작품 일시정지를 눌렀는지 자체 판단하기 위한 예시 변수입니다.
const isPauseClicked = false;

this.learningPredict.on('pause', () = > {
// Predict 팝업에서 '작품 일시정지' 상태에 따라서 작품을 일시정지할지 재실행할지 분기처리 합니다.
if (!this.isPauseClicked) {
this.isPauseClicked = true;
// 작품 일시정지 여부를 토글하는 함수입니다.
Entry.engine.togglePause({ visible: false });
}
Entry.engine.togglePause();
})

stop

Predict 팝업의 ‘작품 정지하기’ 클릭시 발생합니다.

const learningPredict = new Predict({});

this.learningPredict.on('stop', () = > {
// 작품을 정지하는 함수입니다.
Entry.engine.toggleStop();
})


dataApi

entry-ml은 Popup 인스턴스를 초기화할 때 dataApi 값을 설정해야 합니다.

dataApi는 학습 데이터를 관리하기 위한 api들을 포함하는 클래스입니다.

dataApi클래스는 ‘인공지능 학습하기’에서 필요한 api호출 함수를 가지고 있으며, 이 함수들은 주로 데이터 저장, 불러오기, 수정과 같은 작업시 api를 호출하는 역할을 합니다.

dataApi 클래스 세팅하기

Popup 인스턴스에 dataApi를 세팅하는 방법은 2가지가 있습니다.

첫번째는 entry-ml에서 제공하는 기본 클래스를 사용하는 방법입니다.

기본 클래스 사용시 브라우저의 indexedDB를 사용해서 모델 데이터를 저장하고 관리합니다.

아래는 기본 클래스를 사용하는 코드입니다.

const { MlDataApi } = await import('@entrylabs/ml');
const dataApi = new MlDataApi();

// entryjs의 인공지능 dataApi를 세팅
Entry.aiLearning.setDataApi(dataApi);
// entry-ml의 popup 인스턴스를 초기화할때 혹은 setData로 dataApi 세팅
const { Popup } = await import('@entrylabs/ml');

// 인스턴스 초기화시 세팅
const learningPopup = new Popup({
...
dataApi: dataApi
});
// setData로 세팅
const learningPopup.setData({
...
dataApi : dataApi
});


아래는 위 예시에서 받아오는 MlDataApi의 구현내용입니다.

indexedDB를 사용하되, 로직을 수정하고 싶다면, 아래 클래스를 참고해서 작성해서 사용하시면 됩니다.


indexedDB 클래스 예시
import { IDataApi, IUploadModel, apiData } from 'entry-ml';
import _throttle from 'lodash/throttle';
import { API_PREFIX, CDN_PREFIX } from 'constant';
import saveAs from 'file-saver';
import localforage from 'localforage';
import { CommonUtil } from 'utils/common';
import pako from 'pako';
import untar from 'js-untar';
import utf8 from 'utf8';
import Tar from 'tar-js';

const ONE_DATA_TYPE = ['decisionTree', 'cluster', 'text', 'number', 'svm', 'regression', 'logisticRegression'];
const LOCAL_STORAGE_KEY = 'entryml_filesystem';

class FileSystemApi implements IDataApi {
temp:{[key:string]: any} = {};
removeTemp: Array<string> = [];
store: any;

async createDb() {
if (this.store) {
return;
}
this.store = {
data: localforage.createInstance({
name: LOCAL_STORAGE_KEY,
driver: localforage.INDEXEDDB,
storeName: 'learningData',
description : 'entry ml learning data',
version : 1.0,
}),
models: localforage.createInstance({
name: LOCAL_STORAGE_KEY,
driver: localforage.INDEXEDDB,
storeName: 'models',
description : 'entry ml models',
version : 1.0,
}),
tfModelInfo: localforage.createInstance({
name: 'tensorflowjs',
driver: localforage.INDEXEDDB,
storeName: 'model_info_store',
}),
tfModel: localforage.createInstance({
name: 'tensorflowjs',
driver: localforage.INDEXEDDB,
storeName: 'models_store',
})
};
}

async getItem(table: string, id: string) {
if (this.temp[`${table}_${id}`]) {
return this.temp[`${table}_${id}`];
}
await this.createDb();
return await this.store[table].getItem(id);
}

async getAll(table: string) {
await this.createDb();
const keys = await this.store[table].keys();
return await Promise.all(keys.map((key: string) => this.store[table].getItem(key)));
}

async setItem(table: string, id: string, data: any) {
this.temp[`${table}_${id}`] = data;
await this.createDb();
this.store[table].setItem(id, data);
}

async deleteItem(table: string, id: string) {
delete this.temp[`${table}_${id}`];
await this.createDb();
await this.store[table].removeItem(id);
}

getModelSaveUrl = (modelId: string) => {
const url = `indexeddb://${modelId}`;
this.saveModel(modelId, { url });
return url;
};

getModelDownloadUrl(basePath: string) {
if (basePath?.indexOf('indexeddb') > -1) {
return basePath;
}
return `https://entry-cdn.pstatic.net/uploads/${basePath}/model.json`;
}

loadModel = async ({
model,
weight,
url,
modelId
}: {
model?: File;
weight?: File;
url?: string;
modelId?: string;
}): Promise<any> => {
if (modelId) {
const model = await this.getItem('models', modelId);
return model?.file;
}
return ;
};
async loadModels(project?: string): Promise<Array<any>> {
return await this.getAll('models') || [];
}
saveModel = async (modelId: string, project: IUploadModel): Promise<{ data:IUploadModel }> => {
const loadedModel = await this.getItem('models', modelId) || {};
const data = {
...loadedModel,
...project,
version: 1,
};
if (!data._id) {
data._id = data.id;
}
await this.setItem('models', modelId, data);
return {
data,
};
};
saveModelFile = async (url: string, formData: FormData) => {
const modelId = url.replace(API_PREFIX, '').replace('indexeddb://', '');
const form = Object.fromEntries(formData);
const file = form.file as Blob;
const data = await blobToStr(file) as string;
const savedUrl = `indexeddb://${modelId}`;
await this.saveModel(modelId, {
file : JSON.parse(data),
url: savedUrl,
});
return savedUrl;
};
async deleteModel(modelId: string): Promise<any> {
await this.createDb();
const keys = await this.store.data.keys();
await Promise.all(keys.map((key:string) => {
if (key.indexOf(modelId) >= 0) {
return this.store.data.removeItem(key);
}
}));
await this.deleteItem('models', modelId);
}
async copyModel(modelId: string): Promise<any> {
const { file, modelJSON, weightJSON, info } = await this.exportModel(modelId);
const newId = CommonUtil?.generateObjectId();
const newModelInfo = {
...info,
name: `${info.name}의 복사본`,
_id: newId,
id: newId,
url: `indexeddb://${newId}`,
parent: info._id,
};
await Promise.all(newModelInfo.classes.map(async (clazz: any) => {
const saveDatas = await this.getGroupData(info.id, clazz.id);
saveDatas.map(async (saveData: any) => {
await this.saveData({ projectId: newId, classId: clazz.id, saveData });
});
}));
if (file) {
await this.saveModel(newId, {
file,
...newModelInfo
});
return newModelInfo;
} else {
if (modelJSON) {
saveWithkeyPath(
this.store.tfModel,
{ ...modelJSON, modelPath: newId }
);
}
if (weightJSON) {
saveWithkeyPath(
this.store.tfModelInfo,
{ ...weightJSON, modelPath: newId }
);
}
await this.saveModel(newId, newModelInfo);
return newModelInfo;
}
}
async exportModel(modelId: string) {
const loadedModel = await this.getItem('models', modelId);
if (!loadedModel) {
throw new Error('no model');
}
const { file, ...info } = loadedModel;
if (file) {
return {
info,
file
};
} else {
const modelData = await this.store.tfModel.getItem(info.id);
const modelInfoData = await this.store.tfModelInfo.getItem(info.id);
return {
info,
modelJSON: modelData,
weightJSON: modelInfoData,
};
}
}

async downloadModel(modelId: string): Promise<any> {
const { file, modelJSON, weightJSON, info } = await this.exportModel(modelId);
const files: Array<{ name: string, content: string }> = [
{ name: 'temp/info.json', content: JSON.stringify(info)}
];
if (file) {
files.push({ name: 'temp/model.json', content: JSON.stringify(file)});
} else if (modelJSON && weightJSON) {
files.push({ name: 'temp/model.json', content: JSON.stringify(modelJSON)});
files.push({ name: 'temp/weight.json', content: JSON.stringify(weightJSON)});
}
const tarData = createTar(files);
const gzippedData = pako.gzip(new Uint8Array(tarData));
const blob = new Blob([gzippedData], { type: 'application/gzip' });
saveAs(blob, `${info.name}.entm`);
}

async uploadModel(data: FormData): Promise<any> {
await this.createDb();
const zipFile = data.get('files') as File;
if (!zipFile) {
console.log('no file');
return ;
}
const blob = zipFile.slice(0, zipFile.size, 'application/zip');
const newFile = new File([blob], 'temp.zip', { type: 'application/zip' });
const { key, saveData, info } = await loadZip(newFile, this.store);
await this.saveModel(key, saveData);
return info;
}
async getGroupData(modelId: string, groupId: string): Promise<any> {
const id = modelId + groupId;
const items = await this.getItem('data', id) || [];
return items;
}

async saveData({ projectId, classId, saveData }: apiData) {
const id = `${projectId}_${classId}`;
const { type } = saveData;
const items = await this.getItem('data', id) || [];
const _id = CommonUtil?.generateObjectId();
const data = {
...saveData,
class: classId,
model: projectId,
_id,
id:_id,
};
if (data.classes) {
data.classes = JSON.parse(data.classes);
await this.saveModel(projectId, {
classes: data.classes,
id: projectId,
type,
});
delete data.classes;
}
if (ONE_DATA_TYPE.includes(type)) {
items[0] = data;
} else {
items.push(data);
}
await this.setItem('data', id, items);
return { data };
}
async deleteData(key: string): Promise<void> {
await this.deleteItem('data', key);
}
async getImageFile({ filename = '', _id: id, data }:{ filename:string; _id: string; data: any; }) {
return dataURLtoFile(data, `${filename}.png`);
}
async getSoundData({ filename = '', _id: id, data }:{ filename:string; _id: string; data: any; }) {
const blob = dataURLtoBlob(data);
const buffer = await blob.arrayBuffer();
return { dataUrl: data, buffer };
}
}

export default FileSystemApi;

const blobToStr = (blob: Blob) => new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function() {
resolve(reader.result);
};
reader.readAsText(blob);
});

function dataURLtoFile(dataurl: string, filename: string) {
const arr = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)?.[1];
const bstr = atob(arr[arr.length - 1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}

function dataURLtoBlob(dataurl: string) {
const arr = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)?.[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type:mime });
}

const saveWithkeyPath = (store: any, data: any) => {
store.ready().then(() => {
const dbInfo = store._dbInfo;
const tx = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
const objectStore = tx.objectStore(dbInfo.storeName);
if (data[objectStore.keyPath]) {
objectStore.put(data);
} else {
throw new Error(`no key ${objectStore.keyPath}`);
}
});
};

const unzipModel = (fileBuffer: ArrayBuffer): Promise<{ info?: any; model?: any; weight?: any; }> =>
new Promise((resolve) => {
const pBuffer = pako.ungzip(fileBuffer);
untar(pBuffer.buffer).then((extractedFiles: Array<any>) => {
const result: { [key:string]: any} = {};
extractedFiles.forEach((file) => {
if (file.size > 0) {
const key = file.name.replace('temp', '').replace('/', '').replace('.json', '');
const str = file.readAsString();
if (key === 'model.weights.bin') {
return;
}
try {
const utf8Str = utf8.decode(str);
result[key] = JSON.parse(utf8Str);
} catch (e) {
console.log('no decode', key, e);
result[key] = JSON.parse(str);
}
}
});
resolve(result);
});
});

const loadZip = async (file: File, store: any) => {
const fileBuffer = await file.arrayBuffer();
const { info, model, weight } = await unzipModel(fileBuffer);
const newId = CommonUtil?.generateObjectId();
const modelInfoJSON = { ...info };
modelInfoJSON.id = newId;
modelInfoJSON._id = newId;
modelInfoJSON.uploaded = new Date();
modelInfoJSON.isActive = true;
let saveData = modelInfoJSON;
if (weight) {
if (model) {
saveWithkeyPath(
store.tfModel,
{ ...model, modelPath: newId }
);
}
if (weight) {
saveWithkeyPath(
store.tfModelInfo,
{ ...weight, modelPath: newId }
);
}
} else if (info && model) {
saveData = {
...modelInfoJSON,
file: model,
};
}
return {
key: newId,
info: modelInfoJSON,
saveData,
};
};

function createTar(files) {
const tar = new Tar();

files.forEach(file => {
const encoder = new TextEncoder();
const encodedStr = encoder.encode(file.content);
tar.append(file.name, encodedStr);
});

return tar.out;
}

기본 클래스를 사용하지 않고, 직접 클래스를 만들어서 사용하는 방법이 있습니다.

직접 클래스를 구현할 경우, api호출 함수별로 어떤 url로 호출할지를 지정이 가능해집니다.

그리고, 개발사는 호출된 url에 서버 api를 구현하여 모델정보와 유저정보를 관리할 수 있습니다.

구현한 datApi 클래스 사용하는 방법은 위의 기본 클래스 적용방법과 동일합니다.

아래는 예시로 구현한 클래스입니다.


예시 클래스
import axios from 'axios';
import Utils from 'utils';

class CustomEntryApi {
getModelSaveUrl = (modelId) => `/rest/learning/${modelId}`;
getModelDownloadUrl(basePath) {
const time = new Date().getTime();
return `/uploads/${basePath}/model.json?${time}`;
}

loadModel = async ({ url }) => {
if (url) {
const { data } = await axios.get(url);
return data;
}
throw new Error('not fully param');
};

async loadModels(project) {
const param = project ? `?project=${project}` : '';
const { data } = await axios.get(`/api/learning/models${param}`);
return data || [];
}

saveModel = async (modelId, modelData) =>
await axios.post(`/api/learning/${modelId}`, modelData);
saveModelFile = async (url, formData) => {
const { data } = await axios.post(url, formData);
return data?.basePath;
};

async deleteModel(modelId) {
await axios({
url: `/rest/learning/${modelId}`,
method: 'DELETE',
});
}

async copyModel(modelId) {
const { data } = await axios({
url: `/rest/learning/copy/${modelId}`,
method: 'POST',
});
return data;
}

async downloadModel(modelId) {
const response = await axios({
url: `/rest/learning/download/${modelId}`,
method: 'POST',
responseType: 'blob',
});
// response 기반으로 다운로드 로직
}

async uploadModel(data) {
const { data: model } = await axios({
url: '/rest/learning/upload',
data,
method: 'POST',
});
return model;
}

async getGroupData(modelId, groupId) {
const { data } = await axios.get(`/api/learning/${modelId}/${groupId}`);
return data;
}

async saveData({ projectId, classId, saveData }) {
const { data, classes, type, recordTime } = saveData;
let dataType = type;
const formData = new FormData();
formData.append('data', data);
if (type === 'speech') {
formData.append('recordTime', `${recordTime}`);
}
if (type === 'image' || type === 'speech') {
formData.append('classes', classes);
} else {
formData.append('classes', JSON.stringify(classes));
}
if (
[
'regression',
'cluster',
'number',
'logisticRegression',
'decisionTree',
'svm',
].includes(type)
) {
dataType = 'table';
}
return await axios({
url: `/rest/learning/${dataType}/${projectId}/${classId}`,
data: formData,
method: 'POST',
});
}

async deleteData(key) {
await axios({
url: `/rest/learning/data/${key}`,
method: 'DELETE',
});
}

async _urlToFile(src, fileName, fileType) {
const { data } = await axios.get(`${src}`, { responseType: 'blob' });
try {
return new File([data], fileName, { type: fileType });
} catch {
return data;
}
}

async getImageFile({ filename = '', _id: id, data }) {
const url = `/uploads/learning/${filename.substr(0, 2)}/${filename.substr(2, 2)}/${filename}`;
return await this._urlToFile(url, id, 'png');
}

// url?: string;
// dataUrl?: string;
// buffer?: ArrayBuffer
async getSoundData({ filename = '', _id: id, data }) {
const url = `/uploads/learning/${filename.substr(0, 2)}/${filename.substr(2, 2)}/${filename}`;
return { url };
}
}

export default CustomEntryApi;


Api 함수 리스트

이 문단에서는 dataApi의 각 함수에 대해 기술합니다.

함수가 호출되는 조건, 파라미터 정의 등과 함께 해당 api을 다루는 서버측 예시코드를 일부 기술합니다.


getModelSaveUrl

tensorflow JS 가 사용된 모델학습이 완료되었을 때 사용합니다.
모델을 저장할 api url을 반환합니다.

이 함수가 리턴한 api주소는 entry-ml 내부에서, TensorflowJS를 사용하는 타입의 모델을 학습할 경우 모델을 저장하기 위해 호출합니다.

파라미터 타입 선택적 기본값 설명
modelId String 저장할 모델의 id

이 함수가 리턴하는 url의 호출을 받은 서버에서는 모델을 서버에 저장하는 로직을 구현해야 합니다.

modelId는 url에 포함되고 데이터는 multipart/form-data 형태로 포함됩니다.

서버에서는 req에서 modelId와 model파일을 추출하여 저장소에 저장해야 합니다.

saveModelFile api와 동일한 url로 요청합니다.

아래는 예시 코드입니다.

app.route('getModelSaveUrl리턴 주소 url').post(uploadModel);

const uploadModel = async(req, res) => {
const { modelId } = req.params;
const files = req.files
const basePath = '서버 basePath경로'
for (const file of files) {
const srcPath = file.path;
// file의 데이터들을 원하는 저장소에 복사
const destPath = path.join(basePath, file.originalname);
copy(srcPath, destPath);
}
const model = db.model.fineOne({ id: modelId });

if(model){
// 기존에 이미 모델이 있는 경우엔 업데이트
db.model.update({ id: modelId, basePath });
}else{
// 모델이 없는 경우엔 새로 생성
const newModel = { id: modelId, basePath };
db.model.insert(newModel)
}

res.status(200).jsonp({ modelId, basePath });
}


getModelDownloadUrl

모델정보를 불러오기 위해 사용합니다.

저장소에 저장된 모델정보를 호출하는 api 주소를 반환합니다.

파라미터는 getModelSaveUrl api에서 저장한 model.json, model.weight.json 파일의 상위 디렉토리 주소를 의미합니다.

파라미터 타입 선택적 기본값 설명
basePath String MODEL_ID를 포함한 base경로

loadModel

학습하기 팝업 제어(Popup)의 ‘모델 학습하기’ 팝업에서 선택한 데이터 로드에 사용합니다.

‘saveModel’ api 함수로 저장한 학습할 모델 데이터를 로드합니다.

파라미터 타입 선택적 기본값 설명
{
url?: string;
modelId?: string;
}
{
url : getModelDownloadUrl 경로
modelId: 로드할 model의 id값
}
가져오는 방식
1. url
2. modelId

loadModels

학습하기 팝업 제어(Popup) 팝업에서 필요한 모델 메타데이터 리스트를 로드합니다.

‘saveModel’ api 함수로 저장한 데이터들을 로드합니다.

파라미터 타입 선택적 기본값 설명
project string ✔️ 현재 작성중인 엔트리 작품id

이 api의 호출을 받은 서버에서는 인자로 받은 project값이나 자체적인 방법으로 models 정보를 리턴해주어야 합니다.

리턴하는 값의 타입은 Array< IUploadModel> 입니다.

// loadModels 서버 예시코드
app.route('loadModels 호출 경로').get(getModels);

const getModels = async (req, res) => {
const { projectId } = req.params;
// projectId 혹은 기타 자료를 기반으로 models정보 추출
const models = db.model.getModels(projectId);

// models 타입: Array<IUploadModel>
res.status(200).send(models);
}

saveModel

모델의 정보를 db에 저장하는 api입니다.

모델 학습이 완료되었을 때와 모델정보(이름, 모델data)가 수정되었을 때, 사용됩니다.

파라미터 타입 선택적 기본값 설명
modelId string 저장할 모델 id
modelInfo IUploadModel 저장할 모델 정보

이 함수의 호출을 받은 서버측 api에는 파라미터로 전달받은 모델정보를 저장하는 로직이 구현되어야 합니다.

// 서버측 예시코드
app.route('saveModel에서 호출하는 api경로').post(saveModel);

const saveModel = async(req, res) => {
const { modelId } = req.params;
const { trainParam, name, isActive, classes, type, version, recordTime, result } = req.body;

const model = db.model.getModel(modelId);
}

saveModelFile

tensorflow JS를 사용하지 않는 모델 학습이 완료되었을 때 호출됩니다.

파라미터로 넘겨준 url로 모델파일 model.json을 저장합니다.

파라미터 타입 선택적 기본값 설명
url string model.json을 저장할 경로입니다.
formData FormData model.json이 포함된 formData입니다.

이 함수의 호출을 받은 서버측 api는 formData형태로 받은 모델정보를 저장하는 로직이 구현되어야 합니다.

formData형태로 데이터를 전송하기에 content-type은 multipart/form-data 입니다.

getModelSaveUrl 의 서버측 예시코드와 동일합니다.


deleteModel

ml_popup_deleteModel1

ml_popup_deleteModel2

모델을 삭제할때 사용합니다.

  1. 나의 모델 > 우클릭(컨텍스트 팝업) > 삭제하기
  2. 나의 모델 > 모든 모델 표시 > 체크되지 않은 모델 우측상단 x 클릭

파라미터로 입력받은 id의 모델을 제거합니다.

파라미터 타입 선택적 기본값 설명
modelId string 제거할 모델 id입니다.

이 함수의 호출을 받은 서버측 api는 파라미터로 받은 modelId의 모델을 저장소에서 제거하는 로직을 구현해야 합니다.

app.route('deleteModel에서 호출하는 api경로').delete(deleteModel)

const deleteModel = async (req, res) => {
const { modelId } = req.params;
// 삭제할 모델과 연관된 data들을 조회
const datas = await db.data.find({ model: modelId });
// 조회한 datas를 순회하며 제거
if(datas && datas.length) {
await Promise.all(
datas.map(async (data) => {
const { filename } = data;
if (filename) {
const filePath = getFilePath(filename);
await deleteFile(filePath);
}
})
)
}
// model정보를 db에서 제거
const result = await db.model.delete(modelId);
res.status(200).jsonp(result);
}

copyModel

ml_popup_copyModel1

‘나의 모델’ > 우클릭(컨텍스트 팝업) > ‘복사하기’ 에서 사용합니다.

파라미터로 입력받은 id의 모델을 복사합니다.

파라미터 타입 선택적 기본값 설명
modelId string 복사할 모델 id입니다.

이 함수의 호출을 받은 서버측 api는 파라미터로 받은 modelId의 모델을 저장소에서 제거하는 로직을 구현해야 합니다.

조회한 모델과 유니크한 값이 겹치지 않는 새로운 모델을 생성하여 저장소에 추가해 주어야 합니다.

아래는 예시코드로, 실제 어떤식으로 복제하여 저장할지는 개발사 환경에 맞춰주시면 됩니다.

const DATA_LIST_TYPES = ['image', 'speech'];
const TFJS_MODEL_TYPES = ['image', 'speech', 'regression', 'logisticRegression'];

app.route('copyModel에서 호출하는 api경로').post(deleteModel)

const copyModel = async (req, res) => {
const { modelId } = req.params;
const model = await db.model.find({ id: modelId });

// copyModel(): 조회한 모델의 내용을 복사하고 유니크한 값 제거
const modelJSON = copyModel(model);
const newModel = db.insert(modelJSON);

// 해당 모델에서 사용하는 model.json과 model.weights.json 파일도 복사
copyFiles(model.url, newModel.url);

// model의 data 복사
// 기존 model에서 data를 복제
const copyData = db.data.find({ model: modelId });
if (DATA_LIST_TYPES.includes(model.type)) {
copyData.forEach((data) => {
const clazz = model.classes.find(({ id }) => id === data.class);
if (!clazz.count) {
clazz.count = 0;
}
clazz.count = clazz.count + 1;
});
} else if (model.type === 'text') {
copyData.forEach(({ class: classId, data }) => {
const clazz = model.classes.find(({ id }) => id === classId);
clazz.count = data.split(',').length;
});
}

// 복제한 copyData를 기반으로 저장소에 data저장
copyData.map((data) => async{
delete data._id
const newData = { ...data, model: newModel.id };
if (DATA_LIST_TYPES.includes(newModel.type)) {
if (data.filename) {
// 실제 data파일을 복제하여 저장
const newFilename = createFileId();
newData.filename = newFilename;
const filePath = getFilePath(filename);
const newFilePath = getFilePath(newFilename);
await copyFile(filePath, newFilePath)
}
}
// data에 대한 db정보 추가
db.data.insert(newData);
})
}

downloadModel

ml_popup_downloadModel1

‘나의 모델’ > 우클릭(컨텍스트 팝업) > ‘파일로 내보내기’ 에서 사용합니다.

파라미터로 입력받은 id의 모델을 다운로드 합니다.

model.json(file)과 model.weights.bin(TFJS를 사용하는 타입인 경우) 그리고 info.json(모델의 정보)를 압축해서 다운로드합니다.

파라미터 타입 선택적 기본값 설명
modelId string 다운로드할 모델 id입니다.

이 함수의 호출을 받은 서버측 api는 파라미터로 받은 modelId의 json파일을 압축하여 리턴하는 로직을 구현해야 합니다.

아래는 예시 코드입니다.

const TFJS_MODEL_TYPES = ['image', 'speech', 'regression', 'logisticRegression'];

app.route('downloadModel에서 호출하는 api경로').post(exportModel)

const exportModel = async (req, res) => {
const { modelId } = req.params;
// 저장소에서 model 정보 조회
const model = db.model.find({ id: modelId });;

// 다운로드 할 json tempDir에 저장.
const tempDirPath = '임시저장 경로';
// copyModel(): 조회한 모델의 내용을 복사하고 유니크한 값 제거
const modelJSON = await copyModel(model);
// 임시저장경로에 modelJSON 저장
await fsp.writeFile(tempDirPath, JSON.stringify(modelJSON));
// 임시저장경로에 model.json 정보를 저장
if (model.url) {
await copyFile(path.join(model.url, 'model.json'), tempDirPath);
if (TFJS_MODEL_TYPES.includes(model.type)) {
// TFJS를 지원하는 타입인경우 임시저장경로에 model.weights 정보도 함께 저장
await copyFile(path.join(model.url, 'model.weights.bin'), tempDirPath);
}
}
// 압축 후 다운로드
// compressDir() : 경로의 파일들을 단일 압축파일로 만드는 함수
compressDir(tempDirPath);
res.download(tempDirPath, encodeURI('다운로드 파일명'));
}

uploadModel

ml_popup_uploadModel1

‘학습할 모델 선택하기’ > ‘파일 올리기’ 에서 사용합니다.

downloadModel api로 다운받은 entm형식의 파일을 업로드하여 모델을 추가합니다.

파라미터 타입 선택적 기본값 설명
data FormData 업로드할 entm형식의 파일을 포함한 formData입니다.

이 함수의 호출 받은 서버측 api는 formData를 저장소에 저장하는 로직을 구현해야 합니다.

formData에는 entm압축파일이 존재하고. 이 파일 하위엔 model.json, model.weights.bin(TFJS를 사용하는 타입인경우), info.json이 temp디렉토리로 묶여서 포함되어 있습니다.

formData에 포함된 모델정보는 압축해제가 필요합니다.

아래는 예시코드입니다.

app.route('uploadModel에서 호출하는 api 경로').post(importModel);

const TFJS_MODEL_TYPES = ['image', 'speech', 'regression', 'logisticRegression'];

const importModel = async (req, res) => {
const uploadedDir = '압축을 해제한 파일들을 저장할 임시 경로';
const file = req.files[0];
await fsp.mkdir(uploadedDir);
await decompressDir(file.path, uploadedDir);
const tempDir = path.join(uploadedDir, 'temp');
const jsonPath = path.join(tempDir, 'info.json');
const data = await fsp.readFile(jsonPath, 'utf8');
// copyModel() : 모델의 유니크 값과 내부 model.classes 값을 조정하는 함수. copyModel api의 예시코드 참고
const modelJSON = await copyModel(JSON.parse(data));
// tempDir의 model.json 데이터를 개발사별 저장소에 복사
const destDir = '개발사별 저장소'
modelJSON.url = destDir;
copy(tempDir, destDir, 'model.json');
// TFJS 사용하는 타입인경우 model.weights.bin를 개바라별 저장소에 복사
if (TFJS_MODEL_TYPES.includes(modelJSON.type)) {
copy(tempDir, destDir, 'model.weights.bin');
}
// 저장소(db)에 신규 모델정보 추가
const newModel = await db.model.insert(modelJSON);
res.status(200).send(newModel);
}

// 압축해제 함수
const decompressDir = (target, dest) => {
return new Promise((resolve, reject) => {
압축해제 라이브러리.압축({
file: target,
cwd: dest,
filter: (path, entry) => {
const { type, size } = entry;
return type !== 'SymbolicLink';
},
})
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
})
}

getGroupData

학습된 모델의 데이터그룹 클래스들을 가져옵니다.

데이터그룹은 모델을 학습할때 입력데이터로 사용된 그룹(클래스)를 의미합니다.

‘학습할 모델 선택하기’ 팝업의 ‘나의 모델’ 탭에서 필요한 모델 그룹을 호출합니다.

파라미터 타입 선택적 기본값 설명
modelId string 조회할 데이터그룹을 포함하는 모델의 id입니다.
groupId string 조회할 데이터그룹의 id입니다.

이 함수의 호출을 받은 서버측 api는 params정보로 모델의 데이터그룹을 리턴하는 로직을 구현해야 합니다.

app.route('getGroupData에서 호출하는 api 경로').get(getGroupData);

const getGroupData = async (req, res) => {
const { modelId, group } = req.params;

const learningDatas = await db.data.find({ model: modelId, class: group });
res.status(200).send(learningDatas);
}


saveData

ml_popup_saveData1

‘모델 학습하기’에서 생성한 입력 데이터들을 단건으로 서버에 저장합니다.

‘분류: 이미지’, ‘분류: 텍스트’, ‘분류: 소리’ 등의 타입에서 클래스 내부에 단건 데이터를 생성할 수 있습니다.

saveData에서 의미하는 데이터는 이 데이터이며, 타입에 따라서 데이터의 내부구조또한 달라질 수 있습니다.

파라미터 타입 선택적 기본값 설명
data apiData

이 함수의 요청을 받은 서버측 api는 학습모델의 타입에 따라 data를 처리해주어야 합니다.

엔트리측에서는 ‘분류: 이미지’(image), ‘분류: 텍스트’(text), ‘분류: 소리’(speech) 타입을 구분하며, 나머지 타입은 모두 table타입으로 처리합니다.

자세한 분류법은 dataApi 클래스 세팅하기 의 예시 구현 클래스에 saveData함수를 참고 바랍니다.

아래는 서버측 예시 코드입니다.

app.route('saveData에서 호출하는 api 경로 > image 타입인 경우').get(saveImageData);
app.route('saveData에서 호출하는 api 경로 > text 타입인 경우').get(saveTextData);
app.route('saveData에서 호출하는 api 경로 > speech 타입인 경우').get(saveSpeechData);
app.route('saveData에서 호출하는 api 경로 > table 타입인 경우').get(saveTableData);

const saveImageData = async (req, res) => {
const filePath = '저장할 filePath';
const filename = '저장할 파일이름';
const { model, classId } = req.params;
const { classes: classesStr } = req.body;
const { data, svg, ext = 'png' } = req.body;

// 저장소에 저장할 형태로 image전처리 후 저장
const imageData = convertToBuffer(image)
saveImage(filePath, imageData);

// db저장소에 모델, 데이터 업데이트
const classes = JSON.parse(classesStr);
db.model.update({ id: model }, { classes, type: 'image'} );
const id = db.data.insert({ model, classId, filename });

res.status(200).jsonp({ id, class: classId, filename, filePath });
}

const saveSpeechData = async (req, res) => {
const filePath = '저장할 filePath';
const filename = '저장할 파일이름';
const { model, classId } = req.params;
const { classes: classesStr, recordTime = 2000, data, svg, ext = 'wav' } = req.body;

const audio = convertToBuffer(data);
const classes = JSON.parse(classesStr);
db.model.update({ id: model }, { id: model, classes, type: 'speech', recordTime });
saveSound(filePath, audio);

const id = db.data.insert({ model, classId, filename });
res.status(200).jsonp({ id, class: classId, filename, filePath, recordTime });
}

const saveTextData = async (req, res) => {
const { model, classId } = req.params;
const { classes: classesStr, data } = req.body;
const classes = JSON.parse(classesStr);
db.model.update({ id: model}, { id: model, classes, type: 'text' });
const id = db.data.insert({ model, class: classId, data });

res.status(200).jsonp({ id, class: classId, data });;
}

const saveTableData = async (req, res) => {
const { model, classId } = req.params;
const { classes: classesStr, type, saveTable } = req.body;
let { data } = req.body;
const table = JSON.parse(data);
const classes = JSON.parse(classesStr);

db.model.update(
{ id: model },
{ id: model, user, classes, type }
);

if (saveTable === 'true') {
data = JSON.stringify(table);
}
const id = db.data.insert({ model, class: classId, data })

res.status(200).jsonp({ id, class: classId, data });
}


deleteData

ml_popup_deleteData1

‘모델 학습하기’에서 생성한 입력 데이터들을 단건으로 제거합니다.

파라미터 타입 선택적 기본값 설명
key string 작성중

이 함수의 호출을 받은 서버 api에서는 선택된 단건 데이터를 제거하는 로직을 구현해야 합니다.

app.route('deleteData에서 요청한 url').delete(deleteData);

const deleteData = async (req, res) => {
const { dataId } = req.params;
const data = db.data.findOne({ id: dataId });

const { model, filename } = data;
if (filename) {
const filePath = getFilePath(filename);
// 선택된 파일을 저장소에서 제거
deleteDataFile(filePath);
}
// db저장소에서 제거
const result = db.data.delete({ id: dataId });
// 만약 제거한 데이터의 모델에 데이터가 하나도 없다면 모델정보도 함께 제거

res.status(200).jsonp(result);
}

getImageFile

입력받은 파라미터를 조합하여 이미지를 불러오는 url로 변환해주는 함수입니다.

url을 만들어주는 유틸성 함수로, 이 함수가 리턴하는 주소를 기반으로 api요청을 할때 사용합니다.

파라미터 타입 선택적 기본값 설명
data {
filename:string;
_id: string;
}
url 생성을 위한 데이터 정보를 포함하는 파라미터입니다.

getSoundData

입력받은 파리미터를 조합하여 사운드를 불러오는 url로 변환해주는 함수입니다.

url을 만들어주는 유틸성 함수로, 이 함수가 리턴하는 주소를 기반으로 api요청을 할때 사용합니다.

파라미터 타입 선택적 기본값 설명
data {
filename:string;
_id: string;
}
url 생성을 위한 데이터 정보를 포함하는 파라미터입니다.


타입 정의

IUploadModel

파라미터 타입 선택적 기본값 설명
_id string ✔️ db에서 사용하는 모델의 id값입니다.
id string ✔️ 모델의 id값입니다.
name string ✔️ 학습한 모델의 이름입니다. 사용자가 직접 작성합니다.
isActive boolean ✔️ 활성중인 모델 여부
url string ✔️ 저장된 모델파일의 경로입니다.
type ClassificationType ✔️ 학습한 모델의 타입입니다. ex) text, speech
trainParam TrainOptions ✔️ 학습하기 팝업에서 ‘학습(입력한 데이터로 모델을 학습합니다.)’ 박스에서 사용한 파라미터입니다.
version number ✔️ 모델의 버전값입니다. 재학습할 때마다 값이 높아집니다.
project string ✔️ 현재 작성중인 엔트리 작품id
recordTime number ✔️ ‘분류:소리’ 모델을 학습할 경우 사용합니다. 녹음시간 값입니다.
classes {
name: string;
id: string;
}
✔️ ‘분류:이미지’ 처럼 데이터 입력을 ‘클래스’로 받는 모델의 클래스정보입니다.
WS에서 클래스를 변경하여 재학습하는 블럭에서 사용합니다.
parent any ✔️ 복사한 모델인 경우, 원본 모델
result any ✔️ ‘군집:k-평균’처럼 학습하기 팝업에서 결과(학습한 모델의 결과를 확인합니다.)
박스에 자체적인 결과값을 가지는 경우의 결과값입니다.

IGroupData

파라미터 타입 선택적 기본값 설명
_id string db에서 사용하는 모델의 id
data string 모델 학습에 사용된 데이터 값
filename string 데이터가 파일인 경우 파일명

apiData

파라미터 타입 선택적 기본값 설명
projectId string data를 포함하는 프로젝트의 id
classId string 데이터가 저장된 dataGroupId
saveData any 저장할 데이터


기타

다국어 에셋(i18n)

ko > learning.json
{
"header_title": "엔트리 - 인공지능 모델 학습하기" ,
"select_learning_title": "학습할 모델 선택하기" ,
"image_learning_title": "분류: 이미지 모델 학습하기" ,
"speech_learning_title": "분류: 소리 모델 학습하기" ,
"text_learning_title": "분류: 텍스트 모델 학습하기" ,
"regression_learning_title": "예측: 숫자 (선형 회귀) 모델 학습하기" ,
"cluster_learning_title": "군집: 숫자 (k-평균) 모델 학습하기" ,
"number_learning_title": "분류: 숫자 (kNN) 모델 학습하기" ,
"decisionTree_learning_title": "분류: 숫자 (결정 트리) 모델 학습하기" ,
"svm_learning_title": "분류: 숫자 (SVM) 모델 학습하기" ,
"logisticRegression_learning_title": "분류: 숫자 (로지스틱 회귀) 모델 학습하기" ,
"menu_new_project": "새로 만들기" ,
"upload_model": "파일 올리기" ,
"menu_my_model": "나의 모델" ,
"create_new_model": "나의 모델 새로 만들기" ,
"create_desc": "학습할 모델의 종류를 선택해 주세요." ,
"model_type_image": "분류: 이미지" ,
"model_type_image_desc": "업로드 또는 웹캠으로 촬영한 이미지를 분류할 수 있는 모델을 학습합니다." ,
"model_type_text": "분류: 텍스트" ,
"model_type_text_desc": "직접 작성하거나 파일로 업로드한 텍스트를 분류할 수 있는 모델을 학습합니다." ,
"model_type_speech": "분류: 소리" ,
"model_type_speech_desc": "마이크로 녹음하거나 파일로 업로드한 소리를 분류할 수 있는 모델을 학습합니다." ,
"model_type_regression": "예측: 숫자 (선형 회귀)" ,
"model_type_regression_desc": "테이블의 숫자 데이터를 특성값과 예측값으로 삼아 선형 회귀 모델을 학습합니다." ,
"model_type_cluster": "군집: 숫자 (k-평균)" ,
"model_type_cluster_desc": "테이블의 숫자 데이터를 특성값으로 삼아 정한 수(k)만큼의 묶음으로 만드는 모델을 학습합니다." ,
"model_type_number": "분류: 숫자 (kNN)" ,
"model_type_number_desc": "테이블의 숫자 데이터를 가장 가까운 이웃(k개)을 기준으로 각각의 클래스로 분류하는 모델을 학습합니다." ,
"model_type_logisticRegression": "분류: 숫자 (로지스틱 회귀)" ,
"model_type_logisticRegression_desc": "테이블의 숫자 데이터를 로지스틱 회귀 알고리즘을 활용해 각각의 클래스로 분류하는 모델을 학습합니다." ,
"model_type_svm": "분류: 숫자 (SVM)" ,
"model_type_svm_desc": "테이블의 숫자 데이터를 서포트 벡터 머신 알고리즘을 활용해 각각의 클래스로 분류하는 모델을 학습합니다." ,
"model_type_decisionTree": "분류: 숫자 (결정 트리)" ,
"model_type_decisionTree_desc": "테이블의 숫자 데이터를 예/아니오로 나누는 트리를 만들어 각각의 클래스로 분류하는 모델을 학습합니다." ,
"icon_new_project": "새로 만들기" ,
"my_model_list": "나의 모델 리스트" ,
"my_model_list_desc": "작품에 적용할 모델을 선택해 주세요." ,
"show_all_my_models": "비활성화 모델 함께 보기" ,
"feed_reject_message": "모델을 먼저 학습해 주세요." ,
"feed_reject_confrim_message": "확인하기" ,
"delete_thumb": "썸네일 지우기" ,
"train_comp_title": "학습" ,
"train_comp_dsc": "입력한 데이터로 모델을 학습합니다." ,
"train_option_title": "학습 조건" ,
"train_status": "학습 상태" ,
"label_neighbor": "이웃 개수" ,
"train_option_label_neighbors": "이웃 개수" ,
"train_option_pop_label_neighbors": "이웃 개수 (Number of neighbors)" ,
"train_option_help_neighbors": "가장 가까운 데이터 몇 개까지를 이웃으로 삼을 것인지 정합니다." ,
"train_option_pop_label_optimizer": "최적화 알고리즘 (Optimizer)" ,
"train_option_help_optimizer": "손실 함수의 결괏값을 최소화하는 모델의 가중치를 찾는 알고리즘을 최적화 알고리즘이라고 부릅니다. (옵티마이저라고도 불러요.)\n\n엔트리의 로지스틱 회귀 모델에서는 SGD(Stochastic gradient descent, 확률적 경사하강법)와 Adam(Adaptive moment estimation)을 제공합니다." ,
"train_option_label_epochs": "에포크" ,
"train_option_pop_label_epochs": "에포크(Epoch)" ,
"train_option_help_epochs": "입력한 데이터 전체를 몇 번 반복하여 학습할지 정하는 부분입니다. 입력한 모든 데이터 전체를 1번 학습하는 것을 1에포크라고 불러요. \n\n다양한 문제(데이터)가 담긴 문제집 1권을 총 몇 번을 푸냐는 것과 같은 것이지요. 그러나 문제집을 1번만 봐서 문제를 잘 푸는 사람이 없듯이 1번만 학습하면 정확하지 않은 결과가 나올 수도 있어요. 반면 하나의 문제집만 너무 반복해서 학습하면 계속 같은 문제만 학습하다 보니 응용력이 부족해지겠죠?\n\n적당히 학습해야 좋은 결과를 얻을 수 있습니다." ,
"train_option_label_batchSize": "배치 크기" ,
"train_option_pop_label_batchSize": "배치 크기(Batch size)" ,
"train_option_help_batchSize": "입력한 데이터 전체를 얼마큼 작은 부분으로 쪼개서 학습할지 정하는 부분입니다.\n\n다양한 문제(데이터)가 담긴 문제집 1권에서 숙제를 어디서부터 어디까지 할지 범위를 정하는 것과 비슷해요. 숙제의 범위가 넓으면 문제집 한 권을 빠르게 끝낼 수 있고, 숙제의 범위가 좁으면 숙제를 여러 번 해야 문제집을 다 끝낼 수 있는 것과 같습니다." ,
"train_option_label_learningRate": "학습률" ,
"train_option_pop_label_learningRate": "학습률(Learning rate)" ,
"train_option_help_learningRate": "데이터를 얼마나 세세하게 학습할지를 정하는 부분입니다.\n\n모델이 계속 학습하면서 가장 나은 결과를 갖는 점을 찾아간다고 상상하면, 그 때의 보폭의 크기를 정하는 것과 같아요. 보폭이 작으면 가장 나은 결과를 찾을 수는 있겠지만 시간이 오래 걸릴 것이고, 보폭이 크면 가장 나은 결과를 자꾸 지나치면서 제대로 된 결과를 찾아내지 못 할 수 있어요." ,
"train_option_label_validationRate": "검증 데이터 비율" ,
"train_option_pop_label_validationRate": "검증 데이터 비율(Validation data rate)" ,
"train_option_help_validationRate": "입력한 데이터 중 어느 정도 비율을 학습한 모델을 검증하는 데에 사용할지 정하는 부분입니다.\n\n검증 데이터 비율을 0.3으로 정했다면 10개의 데이터를 입력했을 때 7개는 학습용으로, 3개는 검증용으로 사용하겠다는 뜻이 돼요." ,
"train_option_label_maxDepth": "트리의 최대 깊이" ,
"train_option_pop_label_maxDepth": "트리의 최대 깊이 (Maximum tree depth)" ,
"train_option_help_maxDepth": "트리의 최대 깊이를 정합니다. \n\n트리의 깊이가 깊으면 모델이 입력된 학습 데이터는 잘 분류하더라도 새로 입력된 데이터는 제대로 분류하지 못할 수 있어요. (이를 과대적합이라 불러요.)" ,
"train_option_label_minNumSamples": "노드의 최소 데이터 수" ,
"train_option_pop_label_minNumSamples": "노드의 최소 데이터 수\n(Min. number of instances in leaves)" ,
"train_option_help_minNumSamples": "트리의 맨 끝인 단말 노드에 꼭 포함되어야 하는 데이터의 최소 개수를 정합니다. \n\n단말 노드의 최소 데이터 수가 작으면 모델이 입력된 학습 데이터는 잘 분류하더라도 새로 입력된 데이터는 제대로 분류하지 못할 수 있어요. (이를 과대적합이라 불러요.)" ,
"train_option_label_C": "C" ,
"train_option_pop_label_C": "C (Cost)" ,
"train_option_help_C": "데이터가 다른 클래스로 잘못 분류되는 것을 얼마만큼 허용할 것인지를 정하는 부분입니다.\nC값이 작을 수록 많이 허용하고, 클 수록 적게 허용합니다.\n\n잘못 분류되는 것을 적게 허용하면 모델이 입력된 학습 데이터는 잘 분류하더라도 새로 입력된 데이터는 제대로 분류하지 못할 수 있어요. (이를 과대적합이라 불러요.)" ,
"train_option_label_gamma": "감마" ,
"train_option_label_degree": "차수" ,
"train_option_label_kernel": "커널" ,
"train_option_pop_label_kernel": "커널 (Kernel)" ,
"train_option_help_kernel": "커널은 입력한 데이터를 더 쉽게 분류할 수 있도록 고차원의 특징 공간으로 사상(mapping)하는 기법을 말합니다.\n\n엔트리의 서포트 벡터 머신 모델에서는 선형, 다항식, RBF(Radial basis function, 방사형 기저 함수) 커널을 제공합니다." ,
"group_title": "데이터 입력" ,
"group_description": "모델이 학습할 데이터를 입력합니다." ,
"group_warn": "모델 학습은 인터넷이 연결되어 있어야 정상적으로 동작합니다." ,
"default_model_name": "새로운 모델" ,
"help_tooltip_msg": "도움말" ,
"train_message_done": "모델 학습을 완료했습니다." ,
"train_message_ready": "모델을 학습할 수 있습니다." ,
"train_message_not_ready": "데이터를 먼저 입력해 주세요." ,
"train_button_message": "모델 학습하기" ,
"train_algorithm_image": "-" ,
"train_algorithm_speech": "-" ,
"train_algorithm_text": "-" ,
"train_algorithm_cluster": "k-평균" ,
"train_algorithm_number": "k-최근접 이웃(kNN)" ,
"train_algorithm_regression": "선형 회귀" ,
"train_algorithm_logisticRegression": "로지스틱 회귀" ,
"train_algorithm_decisionTree": "결정 트리" ,
"train_algorithm_svm": "서포트 백터 머신(SVM)" ,
"result_title": "결과" ,
"result_dsc": "학습한 모델의 결과를 확인합니다." ,
"image_type_video": "촬영" ,
"image_type_upload": "업로드" ,
"speech_type_upload": "업로드" ,
"speech_type_record": "녹음" ,
"text_type_upload": "업로드" ,
"text_type_write": "직접 작성" ,
"add_class": "클래스 추가하기" ,
"delete_data_message": "삭제" ,
"no_data": "클릭해서 데이터를 입력해 주세요." ,
"data_align_noti_image": "모델이 학습할 이미지 데이터를 아래에 입력해 주세요.\n클래스 당 5개 이상의 데이터를 입력해야 합니다." ,
"data_align_noti_speech": "모델이 학습할 소리 데이터를 아래에 입력해 주세요.\n클래스 당 5개 이상의 데이터를 입력해야 합니다." ,
"data_align_noti_text": "모델이 학습할 텍스트 데이터를 아래에 작성해 주세요.\n클래스 당 5개 이상의 데이터를 입력해야 합니다.\n각각의 데이터는 쉼표로 구분합니다. (예: 맛있다, 맛있어, 맛있네)" ,
"not_selected_model": "작품에서 사용할 모델을 선택해 주세요." ,
"test_result": "분류 결과" ,
"file_input_error": "올바른 파일을 업로드해 주세요." ,
"file_input_error_blocked": "이용 정책을 위반하는 이미지로 의심되어\n업로드 할 수 없습니다.\n문제가 없다고 생각하시는 경우\n'문의하기'를 통해 이미지를 전달해 주세요." ,
"file_input": "파일 업로드" ,
"image_file_input_noti": "10MB 이하의 jpg, png,\nbmp 형식의 파일을\n추가할 수 있습니다." ,
"speech_file_input_noti": "10MB 이하의 wav, mp3 형식의 파일을\n추가할 수 있습니다." ,
"text_file_input_noti": "10MB 이하의 txt, csv 형식의 파일을 추가할 수 있습니다." ,
"video_shoot": "촬영하기" ,
"text_shoot": "결과 확인하기" ,
"number_shoot": "입력하기" ,
"speech_shoot": "입력하기" ,
"number_count": "개" ,
"submit_error_not_completed_learn": "학습이 완료되지 않았습니다." ,
"cant_create_msg": "모델은 10개까지 활성화 할 수 있습니다." ,
"cant_active_msg": "모델은 10개까지 활성화 할 수 있습니다." ,
"sample_group_title": "클래스" ,
"delete_model_warn": "모델을 삭제할까요?\n확인을 누르면 학습한 모델과 학습 데이터가\n완전히 삭제됩니다." ,
"text_placeholder": "텍스트 데이터를 작성해 주세요." ,
"text_file_upload": "파일 업로드" ,
"text_file_upload_warn": "10MB 이하의 txt, csv 형식의 파일을 업로드할 수 있습니다.\n업로드한 파일의 내용은 데이터의 맨 마지막에 추가됩니다." ,
"tutorial_link": "튜토리얼 보기" ,
"delete_button": "삭제하기" ,
"play_button": "재생하기" ,
"stop_play_button": "정지" ,
"speech_learning_time": "길이" ,
"number_second": "초" ,
"record_loop_active": "녹음 활성화" ,
"record_not_allowed_msg": "마이크 권한을 허용해 주세요." ,
"login_error_msg": "로그아웃 상태에서는\n모델 학습 기능을 사용할 수 없습니다.\n모델 학습하기 창을 종료합니다." ,
"max_record_time_msg": "녹음 시간은 1~3초만 가능합니다." ,
"supervised_learning": "지도학습" ,
"unsupervised_learning": "비지도학습" ,
"table_select_message": "테이블을 선택해 주세요." ,
"table_select_placeholder": "테이블을 선택하면 속성이 표시됩니다." ,
"regression_attr_label": "핵심 속성" ,
"regression_attr_placeholder": "여기에 속성을 끌어다 놓을 수 있습니다." ,
"regression_attr_selected": "핵심 속성을 설정했습니다." ,
"regression_attr_button_1": "핵심 속성 1" ,
"regression_attr_button_2": "핵심 속성 2" ,
"regression_attr_button_3": "핵심 속성 3" ,
"regression_attr_button_4": "핵심 속성 4" ,
"regression_attr_button_5": "핵심 속성 5" ,
"regression_attr_button_6": "핵심 속성 6" ,
"regression_predict_label": "예측 속성" ,
"regression_predict_placeholder": "여기에 속성을 끌어다 놓을 수 있습니다." ,
"regression_predict_selected": "예측 속성을 선택했습니다." ,
"regression_predict_button": "예측 속성" ,
"cluster_centroids_kmpp": "가장 먼 거리" ,
"cluster_centroids_random": "무작위" ,
"cluster_k_count_label": "군집 개수" ,
"cluster_k_count_placeholder": "선택해 주세요." ,
"cluster_centroids_label": "중심점 기준" ,
"cluster_centroids_placeholder": "선택해 주세요." ,
"regression_graph_disable_msg_1": "핵심 속성이 2개 이상이라\n2차원 좌표평면의 차트로\n표현할 수 없어요." ,
"regression_graph_disable_msg_2": "작품에서 직접 모델을\n확인해 보세요." ,
"regression_equation_label": "회귀식" ,
"cluster_graph_disable_msg_1": "핵심 속성이 2개일 때만 2차원 좌표평면의 차트로 표현할 수 있어요." ,
"cluster_graph_disable_msg_2": "작품에서 직접 모델을\n확인해 보세요." ,
"cluster_index_centroids_label": "군집 %1의 중심점" ,
"cluster_attr_label": "핵심 속성" ,
"number_class_attr": "클래스 속성" ,
"number_class_attr_button": "클래스로 삼을 속성" ,
"number_predict_placeholder": "아래에서 분류할 클래스로 삼을 속성을 선택해 주세요." ,
"number_predict_selected": "클래스 속성을 선택했습니다." ,
"invalid_neighbor_count_range_msg": "0 ~ 1000개 사이의 이웃을 선택해 주세요." ,
"result_class_label": "분류한 클래스" ,
"table_updated_message": "※ 입력 데이터가 변경되기 전에\n학습된 모델입니다. 주의해 주세요." ,
"table_postfix": "학습 완료" ,
"min_value": "{{number}} 이상의 값을 입력해 주세요." ,
"max_value": "{{number}} 이하의 값을 입력해 주세요." ,
"min_max_value": "입력 가능한 값은 {{min}} ~ {{max}}입니다." ,
"svm_option_linear": "선형" ,
"svm_option_polynomial": "다항식" ,
"svm_option_rbf": "RBF" ,
"svm_option_adam": "Adam" ,
"svm_option_sgd": "SGD" ,
"label_algorithm": "알고리즘" ,
"label_state_traincomplete": "모델 학습을 완료했습니다." ,
"label_state_training": "모델 학습 중" ,
"show_tree_btn": "트리 보기" ,
"tree_popup_title": "트리 보기" ,
"feedback_test": "테스트" ,
"feedback_evaluate": "평가" ,
"feedback_test_result": "결과 확인하기" ,
"feedback_accuacy": "정확도" ,
"feedback_precision": "정밀도" ,
"feedback_recall": "재현율" ,
"feedback_f1": "F1" ,
"feedback_cannot_show_matrix_msg": "클래스가 5개를 초과해\n오차 행렬을 표현할 수 없어요." ,
"error_message": "오류가 발생했습니다." ,
"train_option_label_optimizer": "최적화 알고리즘" ,
"test_placeholder_msg": "값을 입력하고\n '결과 확인하기'를 눌러주세요." ,
"matrix_disable_msg": "오차 행렬을 그릴 수 없습니다." ,
"active_model_count": "활성화한 모델 {{count}} 개" ,
"detach_model_warn": "모델 선택을 해제하면 작품에 조립한 인공지능 모델 블록이 모두 삭제됩니다. \n모델을 해제할까요?" ,
"upload_addfile": "파일 업로드" ,
"apply_to_this": "현재 작품에 적용" ,
"apply_to_other": "다른 작품에 적용" ,
"uploaded_train_disable_message": "파일 올리기로 불러온 모델입니다. \n다시 학습할 수 없습니다." ,
"upload_model_disable_insert_table_data": "파일 올리기로 불러온 모델입니다. \n데이터를 입력하거나 속성을 다시 설정할 수 없습니다." ,
"upload_model_disable_insert_data": "파일 올리기로 불러온 모델입니다. \n데이터를 입력하거나 클래스를 수정할 수 없습니다." ,
"loading": "로딩 중입니다. 기다려 주세요." ,
"download_model": "파일로 내보내기" ,
"delete_model": "삭제하기" ,
"copy_model": "복사하기" ,
"show_models": "모든 모델 표시" ,
"caution_upload": "<li>entm 형식의 파일을 업로드해 모델을 추가할 수 있습니다.</li><li>파일 올리기로 추가한 모델은 테스트 데이터를 넣어 결과를 확인하거나 작품에 적용할 수 있습니다.<br/>단, 학습 시 사용한 데이터를 확인하거나 추가 입력할 수 없고, 학습 조건 등을 변경해 다시 학습할 수는 없습니다.</li>" ,
"caution_list_edit": "<li>'모든 모델 표시'상태에서는 학습할 모델을 선택할 수 없습니다. 기본 상태로 돌아가 모델을 선택해 주세요.</li><li>나의 모델은 <strong>최대 10개까지</strong> 활성화할 수 있습니다. 왼쪽 위의 체크 표시로 모델의 활성화/비활성화를 설정합니다.<br />작품에 적용한 모델은 활성화한 나의 모델에 포함되지 않습니다.</li><li>비활성화한 모델은 오른쪽 위의 X버튼을 눌러 삭제할 수 있습니다. 삭제한 모델은 복구할 수 없으니 주의해 주세요.</li><li>다른 작품에 적용한 모델은 해당 작품의 코드 보기에서만 적용 해제하거나 다시 학습할 수 있습니다.<br />썸네일을 클릭하면 해당 작품의 코드 보기를 새 창으로 엽니다.</li>" ,
"caution_list": "<li>현재 작품에 적용한 모델과 활성화한 모델만 표시됩니다.<br /> 비활성화한 모델이나 다른 작품에 적용한 모델을 확인하거나,학습한 모델을 삭제하려면 <strong>'모든 모델 표시'</strong> 버튼을 눌러주세요.</li><li>작품에 적용한 모델을 해제하려면 모델을 한 번 더 클릭해 선택을 해제하고 팝업을 닫아 주세요.<br /><strong>모델 선택을 해제하면 작품에 조립한 인공지능 모델 블록이 모두 삭제됩니다.</strong></li>" ,
"my_model_edit_list_desc": "모델을 활성화/비활성화 하거나 삭제할 수 있습니다." ,
"max_size_upload_model_alert": "오프라인 모델 파일은 최대 {{size}}MB까지만 업로드 할 수 있습니다." ,
"upload_fail_msg": "파일 업로드에 실패했습니다." ,
"remove_current_model_fail": "작품에 적용된 모델은 삭제할 수 없습니다.\n먼저 적용을 해제해 주세요"
}
ko > common.json
{
"button_cancel": "취소",
"button_submit": "적용하기",
"button_submit_disabled": "적용하기",
"button_learn": "학습하기",
"button_learn_disabled": "학습하기",
"button_back_tooltip": "뒤로가기",
"button_apply": "적용하기",
"popup_title": "알림",
"popup_confirm": "확인하기",
"popup_close": "닫기",
"workspace_stop": "작품 정지하기",
"workspace_restart": "작품 다시 시작",
"workspace_pause": "작품 일시정지"
}
en > learning.json
{
"header_title": "Entry - Train AI model" ,
"select_learning_title": "Choose a model to train" ,
"image_learning_title": "Train image classificion model" ,
"speech_learning_title": "Train sound classificion model" ,
"text_learning_title": "Train text classificion model" ,
"regression_learning_title": "Train predictive model" ,
"cluster_learning_title": "Train clustering model" ,
"number_learning_title": "Train classificion model (kNN)" ,
"decisionTree_learning_title": "Train classificion model (Decision tree)" ,
"svm_learning_title": "Train classificion model (SVM)" ,
"logisticRegression_learning_title": "Train classificion model (Logistic regression)" ,
"menu_new_project": "New project" ,
"upload_model": "Upload file" ,
"menu_my_model": "My models" ,
"create_new_model": "Create new model" ,
"create_desc": "Please select the type of model you want to train." ,
"model_type_image": "Classification: Image" ,
"model_type_image_desc": "Train a model that can classify images uploaded or captured by webcam." ,
"model_type_text": "Classification: Text" ,
"model_type_text_desc": "Train a model that can classify texts uploaded or written by user." ,
"model_type_speech": "Classification: Sound" ,
"model_type_speech_desc": "Train a model that can classify sounds uploaded or recorded by user." ,
"model_type_regression": "Prediction: Number" ,
"model_type_regression_desc": "Train a linear regression model that uses the numeric data in the table as feature values to find predicted values." ,
"model_type_cluster": "Clustering: Number" ,
"model_type_cluster_desc": "train a model that uses the numeric data of the table as a feature value and creates K number of clusters." ,
"model_type_number": "Classification: Number (kNN)" ,
"model_type_number_desc": "Train a model that can classify the numeric data of the table with k-nearest neighbors algorithm." ,
"model_type_logisticRegression": "Classification: Number (Logistic regression)" ,
"model_type_logisticRegression_desc": "Train a model that can classify the numeric data of the table with logistic regression algorithm." ,
"model_type_svm": "Classification: Number (SVM)" ,
"model_type_svm_desc": "Train a model that can classify the numeric data of the table with support vector machine algorithm." ,
"model_type_decisionTree": "Classification: Number (Decision tree)" ,
"model_type_decisionTree_desc": "Train a model that can classify the numeric data of the table with decision tree algorithm." ,
"icon_new_project": "New project" ,
"my_model_list": "My models list" ,
"my_model_list_desc": "Please select a model to use in your project." ,
"show_all_my_models": "Show deactivated model" ,
"feed_reject_message": "Please train the model first." ,
"feed_reject_confrim_message": "Confirm" ,
"delete_thumb": "Delete thumbnail" ,
"train_comp_title": "Train" ,
"train_comp_dsc": "Train the model from the input data." ,
"train_option_title": "Conditions" ,
"train_status": "Chart" ,
"label_neighbor": "Number of neighbors" ,
"train_option_label_neighbors": "Number of neighbors" ,
"train_option_pop_label_neighbors": "Number of neighbors" ,
"train_option_help_neighbors": "Determine the number of nearest data points to be considered as neighbors." ,
"train_option_pop_label_optimizer": "Optimizer" ,
"train_option_help_optimizer": "The algorithm that seeks to minimize the outcome of the loss function to find the weights of a model is refferred to as an optimization algoritm. (It is also called an optimizer.) \n\nIn the logistic regression model of the Entry, both SGD and Adam optimizers are available." ,
"train_option_label_epochs": "Epoch" ,
"train_option_pop_label_epochs": "Epoch" ,
"train_option_help_epochs": "Epoch determines how many times to train the entire data. Feeding all the data to the model once is called 1 epoch.\n\nThis is similar to deciding how many times to solve a workbook containing various problems (data). However, just as no one can solve the problem well by looking at the workbook only once, 1epoch may yield inaccurate results. On the other hand, if you only study through one workbook numerous times, you may lack application skills. Therefore, it is important to set an appropriate epoch for good results." ,
"train_option_label_batchSize": "Batch size" ,
"train_option_pop_label_batchSize": "Batch size" ,
"train_option_help_batchSize": "Batch size determines the size of the small sets in the entire data to be trained.\n\nThis is similar to determining the scope of where to do homework in one workbook containing various problems (data). If the scope of the homework is wide, you will be able to finish the workbook quickly, whereas if the scope of the homework is narrow, you will have to do the homework several times to finish the entire workbook." ,
"train_option_label_learningRate": "Learning rate" ,
"train_option_pop_label_learningRate": "Learning rate" ,
"train_option_help_learningRate": "Learning rate determines how detailed the data is to be trained.\n\nIf you imagine a model continuously training to find the point with the best results, it's like determining the size of the stride at that time. If the stride length is small, you may find the best result, but it will take a long time. On the other hand, if the stride length is large, you may keep passing the best result and not find the correct result." ,
"train_option_label_validationRate": "Validation data rate" ,
"train_option_pop_label_validationRate": "Validation data rate" ,
"train_option_help_validationRate": "This part determines how much of the input data will be used to validate the trained model.\n\nIf the validation data rate is set at 0.3 then it means that when you enter 10 data, you will use 7 for learning and 3 for validation." ,
"train_option_label_maxDepth": "Maximum tree depth" ,
"train_option_pop_label_maxDepth": "Maximum tree depth" ,
"train_option_help_maxDepth": "Determine the maximum depth of the tree. \n\nIf the depth of the tree is deep, the model may effectively classify the training data, but it might struggle to correctly classify new input data. (This is called overfitting.)" ,
"train_option_label_minNumSamples": "Min. number of instances in leaves" ,
"train_option_pop_label_minNumSamples": "Min. number of instances in leaves" ,
"train_option_help_minNumSamples": "Determine the minimum number of data points that must be included in the leaf nodes, which are the final nodes of the tree. \n\nIf the minimum number of data points in the leaf nodes is low, the model may effectively classify the training data, but it might struggle to correctly classify new input data. (This is called overfitting.)" ,
"train_option_label_C": "C" ,
"train_option_pop_label_C": "C (Cost)" ,
"train_option_help_C": "This option determines how much the data can be allowed to be misclassified into different classes. \nThe smaller C value is, the more model misclassifies and the larger C values is, the less model misclassifies. \n\nAllowing fewer misclassification could cause overffiting." ,
"train_option_label_gamma": "Gamma" ,
"train_option_label_degree": "Degree" ,
"train_option_label_kernel": "Kernel" ,
"train_option_pop_label_kernel": "Kernel" ,
"train_option_help_kernel": "Kernels are trick that map input data into high-dimensional feature spaces for classification. \n\nSVM model in Entry provides three types of kernels: linear, polynomial, and RBF(Radial Basis Function)." ,
"group_title": "Input data" ,
"group_description": "Input the data for the model to train." ,
"group_warn": "Training model is available only when the Internet is connected." ,
"default_model_name": "New model" ,
"help_tooltip_msg": "Help" ,
"train_message_done": "Completed." ,
"train_message_ready": "Ready." ,
"train_message_not_ready": "Please input the data first." ,
"train_button_message": "Train model" ,
"train_algorithm_image": "-" ,
"train_algorithm_speech": "-" ,
"train_algorithm_text": "-" ,
"train_algorithm_cluster": "k-means" ,
"train_algorithm_number": "kNN" ,
"train_algorithm_regression": "Linear regression" ,
"train_algorithm_logisticRegression": "Logistic regression" ,
"train_algorithm_decisionTree": "Decision tree" ,
"train_algorithm_svm": "Support vector machine" ,
"result_title": "Result" ,
"result_dsc": "Check the results of the trained model." ,
"image_type_video": "Capture" ,
"image_type_upload": "Upload" ,
"speech_type_upload": "Upload" ,
"speech_type_record": "Record" ,
"text_type_upload": "Upload" ,
"text_type_write": "Write" ,
"add_class": "Add a class" ,
"delete_data_message": "Delete" ,
"no_data": "Insert data to train the model." ,
"data_align_noti_image": "Input image data below.\n You must train at least 5 data per class" ,
"data_align_noti_speech": "Input sound data below.\n You must insert at least 5 data per class." ,
"data_align_noti_text": "Write the text data below.\nYou must insert at least 5 data per class.\n Separate each data with a comma. (e.g. monkey, apple, banana)" ,
"not_selected_model": "Please select a model to use in your project." ,
"test_result": "Classification result" ,
"file_input_error": "Please upload an appropriate file." ,
"file_input_error_blocked": "Please upload an appropriate file." ,
"file_input": "Upload file" ,
"image_file_input_noti": "You can add files in jpg,\npng, and bmp formats that are\nless than 10 MB." ,
"speech_file_input_noti": "You can add files in wav, mp3 formats that are\nless than 10 MB." ,
"text_file_input_noti": "You can add files in txt, csv formats that are less than 10MB." ,
"video_shoot": "Capture" ,
"text_shoot": "Test" ,
"number_shoot": "Input" ,
"speech_shoot": "Input" ,
"number_count": " Data(s)" ,
"submit_error_not_completed_learn": "Training has not been complete." ,
"cant_create_msg": "There are 10 activated models already." ,
"cant_active_msg": "There are 10 activated models already." ,
"sample_group_title": "Class" ,
"delete_model_warn": "Are you sure you want to delete the model?" ,
"text_placeholder": "Please write the text data." ,
"text_file_upload": "Upload file" ,
"text_file_upload_warn": "You can add files in txt, csv formats that are less than 10MB.\nThe text of the uploaded file are added at the end of the data." ,
"tutorial_link": "Tutorial" ,
"delete_button": "Delete" ,
"play_button": "Play" ,
"stop_play_button": "Stop" ,
"speech_learning_time": "Length" ,
"number_second": "s" ,
"record_loop_active": "Record" ,
"record_not_allowed_msg": "You must allow access to the microphone." ,
"login_error_msg": "Training model is not available\nwhen logged out.\nExit this window." ,
"max_record_time_msg": "Recording time is only 1~3 seconds." ,
"supervised_learning": "Supervised learning" ,
"unsupervised_learning": "Unsupervised learning" ,
"table_select_message": "Please select a table." ,
"table_select_placeholder": "When you select a table, its attributes are displayed." ,
"regression_attr_label": "Feature" ,
"regression_attr_placeholder": "You can drag and drop attributes here." ,
"regression_attr_selected": "Selected feature." ,
"regression_attr_button_1": "Feature 1" ,
"regression_attr_button_2": "Feature 2" ,
"regression_attr_button_3": "Feature 3" ,
"regression_attr_button_4": "Feature 4" ,
"regression_attr_button_5": "Feature 5" ,
"regression_attr_button_6": "Feature 6" ,
"regression_predict_label": "Predictive attribute" ,
"regression_predict_placeholder": "You can drag and drop attributes here." ,
"regression_predict_selected": "Selected predictive attribute." ,
"regression_predict_button": "Predictive attribute" ,
"cluster_centroids_kmpp": "Farthest distance" ,
"cluster_centroids_random": "Random" ,
"cluster_k_count_label": "Number of clusters" ,
"cluster_k_count_placeholder": "Please select the number." ,
"cluster_centroids_label": "Centroid standard" ,
"cluster_centroids_placeholder": "Please select." ,
"regression_graph_disable_msg_1": "Because there are 2 or more feature values. \nIt cannot be expressed \nin a two-dimensional coordinate plane chart." ,
"regression_graph_disable_msg_2": "Please check the model \nin the project." ,
"regression_equation_label": "Regression formula" ,
"cluster_graph_disable_msg_1": "Only when there are two feature values, it can be expressed in a two-dimensional coordinate plane chart." ,
"cluster_graph_disable_msg_2": "Please check the model \nin the project." ,
"cluster_index_centroids_label": "Centroid of cluster %1" ,
"cluster_attr_label": "Feature" ,
"number_class_attr": "Class attribute" ,
"number_class_attr_button": "Attribute to use as a class" ,
"number_predict_placeholder": "You can select the class attribute here." ,
"number_predict_selected": "Selected class attribute." ,
"invalid_neighbor_count_range_msg": "Please select between 0 and 1000 neighbors" ,
"result_class_label": "Classified class" ,
"table_updated_message": "※ This is a model that was trained \nbefore the table was modified. \nPlease be careful of use." ,
"table_postfix": "Copy" ,
"min_value": "Enter a value greater than or equal to {{number}}" ,
"max_value": "Enter a value smaller than or equal to {{number}}" ,
"min_max_value": "You can enter values from {{min}} to {{max}}." ,
"svm_option_linear": "Linear" ,
"svm_option_polynomial": "Polynomial" ,
"svm_option_rbf": "RBF" ,
"svm_option_adam": "Adam" ,
"svm_option_sgd": "SGD" ,
"label_algorithm": "Algorithm" ,
"label_state_traincomplete": "Completed" ,
"label_state_training": "Training" ,
"show_tree_btn": "Show decision tree" ,
"tree_popup_title": "Decision tree" ,
"feedback_test": "Test" ,
"feedback_evaluate": "Evaluation" ,
"feedback_test_result": "Test" ,
"feedback_accuacy": "Accuacy" ,
"feedback_precision": "Precision" ,
"feedback_recall": "Recall" ,
"feedback_f1": "F1" ,
"feedback_cannot_show_matrix_msg": "The confusion matrix can display no more than five classes." ,
"error_message": "Error." ,
"train_option_label_optimizer": "Optimizer" ,
"test_placeholder_msg": "Enter the value and click 'Test'." ,
"matrix_disable_msg": "Unable to draw the confusion matrix." ,
"active_model_count": "Activated Model: {{count}}" ,
"detach_model_warn": "If you unapply the model, all AI model blocks will be deleted. \nAre you sure to unapply the model?" ,
"upload_addfile": "Upload file" ,
"apply_to_this": "This project" ,
"apply_to_other": "Other project" ,
"uploaded_train_disable_message": "Unable to retrain the imported models." ,
"upload_model_disable_insert_table_data": "Unable to input data or set the features \nto the imported models." ,
"upload_model_disable_insert_data": "Unable to input data or edit the classes \nto the imported models." ,
"loading": "Now loading." ,
"download_model": "Export" ,
"delete_model": "Delete" ,
"copy_model": "Copy" ,
"show_models": "Show all models" ,
"caution_upload": "<li>A model can be added by uploading a file in the '.entm' format.</li><li>Models added via file upload can be tested with data or applied to a project. <br/>However, they cannot be retrained by altering the training conditions.</li>" ,
"caution_list_edit": "<li>Unable to select the model to train in the state of 'Show all models'</li><li>My models can be activated <strong>up to 10</strong>. You can activate/deactivate the model with the check mark on the upper left.<br />The model you applied to your work is not included in my activated model.</li><li>Diactivated models can be deleted by pressing the X button on the upper right. Please be aware that deleted models cannot be recovered.</li><li>Models applied to other works can only be unapplied or retrained from the work's code view.<br />Click the thumbnail to open the work's code view in a new window.</li>" ,
"caution_list": "<li>Only the activated models are displayed. <br />To check deactivated models and models applied to other projects, or to delete trained models, please press the <strong>'Show All Models'</strong> button.</li><li>To unapply the model from the project, click the model once again to deselect it, and the close the pop-up.<br /><strong>If you deselect the model, all AI model blocks will be deleted from project.</strong></li>" ,
"my_model_edit_list_desc": "You can activate/deactivate or delete the model." ,
"max_size_upload_model_alert": "Unable to upload files more than {{size}}MB." ,
"upload_fail_msg": "Upload failed." ,
"remove_current_model_fail": "The model applied to the project cannot be deleted. \nPlease unapply model first."
}
en > common.json
{
"button_cancel": "Cancel",
"button_submit": "Apply",
"button_submit_disabled": "Apply",
"button_learn": "Train",
"button_learn_disabled": "Train",
"button_back_tooltip": "Back",
"button_apply": "Apply",
"popup_title": "Notice",
"popup_confirm": "Confirm",
"popup_close": "Close",
"workspace_stop": "Project Stop",
"workspace_restart": "Project Resume",
"workspace_pause": "Project Pause"
}