learning_model 1.0.33 → 1.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.bundle.js +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -5
- package/dist/learning/base.d.ts +5 -0
- package/dist/learning/mobilenet.d.ts +5 -0
- package/dist/learning/mobilenet.js +21 -0
- package/dist/lib/index.d.ts +1 -3
- package/dist/lib/learning/base.d.ts +5 -0
- package/dist/lib/learning/mobilenet.d.ts +5 -0
- package/lib/index.ts +2 -4
- package/lib/learning/base.ts +3 -0
- package/lib/learning/mobilenet.ts +24 -2
- package/package.json +1 -1
- package/dist/learning/image.d.ts +0 -45
- package/dist/learning/image.js +0 -285
- package/dist/learning/mobilenet_image.d.ts +0 -47
- package/dist/learning/mobilenet_image.js +0 -276
- package/dist/learning/mobilenet_image.test.d.ts +0 -1
- package/dist/learning/mobilenet_image.test.js +0 -77
- package/dist/lib/learning/image.d.ts +0 -45
- package/dist/lib/learning/mobilenet_image.d.ts +0 -47
- package/dist/lib/learning/mobilenet_image.test.d.ts +0 -1
- package/lib/learning/image.ts +0 -283
- package/lib/learning/mobilenet_image.test.ts +0 -44
- package/lib/learning/mobilenet_image.ts +0 -277
package/lib/learning/image.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import * as tf from '@tensorflow/tfjs';
|
|
2
|
-
import { io } from '@tensorflow/tfjs-core';
|
|
3
|
-
import LearningInterface from './base';
|
|
4
|
-
|
|
5
|
-
class LearningImage implements LearningInterface {
|
|
6
|
-
model: tf.LayersModel | null;
|
|
7
|
-
epochs: number;
|
|
8
|
-
batchSize: number;
|
|
9
|
-
learningRate: number;
|
|
10
|
-
validateRate: number;
|
|
11
|
-
labels: string[];
|
|
12
|
-
isRunning: boolean;
|
|
13
|
-
isReady: boolean;
|
|
14
|
-
isTrainedDone: boolean;
|
|
15
|
-
limitSize: number;
|
|
16
|
-
trainImages: tf.Tensor3D[] = [];
|
|
17
|
-
|
|
18
|
-
readonly MOBILE_NET_INPUT_WIDTH = 224;
|
|
19
|
-
readonly MOBILE_NET_INPUT_HEIGHT = 224;
|
|
20
|
-
readonly MOBILE_NET_INPUT_CHANNEL = 3;
|
|
21
|
-
readonly IMAGE_NORMALIZATION_FACTOR = 255.0;
|
|
22
|
-
|
|
23
|
-
constructor({
|
|
24
|
-
epochs = 10,
|
|
25
|
-
batchSize = 16,
|
|
26
|
-
limitSize = 2,
|
|
27
|
-
learningRate = 0.001,
|
|
28
|
-
validateRate = 0.2,
|
|
29
|
-
}: { modelURL?: string, epochs?: number, batchSize?: number, limitSize?: number, learningRate?: number, validateRate?: number} = {}) {
|
|
30
|
-
this.model = null;
|
|
31
|
-
this.epochs = epochs;
|
|
32
|
-
this.batchSize = batchSize;
|
|
33
|
-
this.learningRate = learningRate;
|
|
34
|
-
this.validateRate = validateRate;
|
|
35
|
-
this.labels = [];
|
|
36
|
-
this.isRunning = false;
|
|
37
|
-
this.isReady = false;
|
|
38
|
-
this.isTrainedDone = false;
|
|
39
|
-
this.limitSize = 2;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public onProgress: (progress: number) => void = () => {};
|
|
43
|
-
|
|
44
|
-
public onLoss: (loss: number) => void = () => {};
|
|
45
|
-
|
|
46
|
-
public onEvents: (logs: any) => void = () => {};
|
|
47
|
-
|
|
48
|
-
public onTrainBegin: (log: any) => void = () => {};
|
|
49
|
-
|
|
50
|
-
public onTrainEnd: (log: any) => void = () => {};
|
|
51
|
-
|
|
52
|
-
public onEpochEnd: (epoch: number, logs: any) => void = () => {};
|
|
53
|
-
|
|
54
|
-
// 학습 데이타 등록
|
|
55
|
-
public async addData(label: string, data: any): Promise<void> {
|
|
56
|
-
try {
|
|
57
|
-
const tensor = tf.browser.fromPixels(data);
|
|
58
|
-
console.log('addData', tensor);
|
|
59
|
-
this.trainImages.push(tensor);
|
|
60
|
-
this.labels.push(label);
|
|
61
|
-
if(this.labels.length >= this.limitSize) {
|
|
62
|
-
this.isReady = true;
|
|
63
|
-
}
|
|
64
|
-
return Promise.resolve();
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error('Model training failed', error);
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 모델 학습 처리
|
|
72
|
-
public async train(): Promise<tf.History> {
|
|
73
|
-
if (this.isRunning) {
|
|
74
|
-
return Promise.reject(new Error('Training is already in progress.'));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 콜백 정의
|
|
78
|
-
const customCallback = {
|
|
79
|
-
onTrainBegin: (log: any) => {
|
|
80
|
-
this.isTrainedDone = false;
|
|
81
|
-
this.onTrainBegin(log);
|
|
82
|
-
console.log('Training has started.');
|
|
83
|
-
},
|
|
84
|
-
onTrainEnd: (log: any) => {
|
|
85
|
-
this.isTrainedDone = true;
|
|
86
|
-
this.onTrainEnd(log);
|
|
87
|
-
console.log('Training has ended.');
|
|
88
|
-
this.isRunning = false;
|
|
89
|
-
},
|
|
90
|
-
onBatchBegin: (batch: any, logs: any) => {
|
|
91
|
-
console.log(`Batch ${batch} is starting.`);
|
|
92
|
-
},
|
|
93
|
-
onBatchEnd: (batch: any, logs: any) => {
|
|
94
|
-
console.log(`Batch ${batch} has ended.`);
|
|
95
|
-
},
|
|
96
|
-
onEpochBegin: (epoch: number, logs: any) => {
|
|
97
|
-
console.log(`Epoch ${epoch+1} is starting.`, logs);
|
|
98
|
-
},
|
|
99
|
-
onEpochEnd: (epoch: number, logs: any) => {
|
|
100
|
-
console.log(`Epoch ${epoch+1} has ended.`);
|
|
101
|
-
this.onLoss(logs.loss);
|
|
102
|
-
console.log('Loss:', logs);
|
|
103
|
-
this.onEvents(logs);
|
|
104
|
-
this.onProgress(epoch+1);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
this.isRunning = true;
|
|
110
|
-
if (this.labels.length < this.limitSize) {
|
|
111
|
-
return Promise.reject(new Error('Please train Data need over 2 data length'));
|
|
112
|
-
}
|
|
113
|
-
this.model = await this._createModel(this.labels.length);
|
|
114
|
-
const inputData = this._preprocessedInputData(this.model);
|
|
115
|
-
console.log('inputData', inputData);
|
|
116
|
-
const targetData = this._preprocessedTargetData();
|
|
117
|
-
console.log('targetData', targetData);
|
|
118
|
-
const history = await this.model.fit(inputData, targetData, {
|
|
119
|
-
epochs: this.epochs,
|
|
120
|
-
batchSize: this.batchSize,
|
|
121
|
-
validationSplit: this.validateRate, // 검증 데이터의 비율 설정
|
|
122
|
-
callbacks: customCallback
|
|
123
|
-
});
|
|
124
|
-
console.log('Model training completed', history);
|
|
125
|
-
return history;
|
|
126
|
-
} catch (error) {
|
|
127
|
-
this.isRunning = false;
|
|
128
|
-
console.error('Model training failed', error);
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 추론하기
|
|
134
|
-
public async infer(data: any): Promise<Map<string, number>> {
|
|
135
|
-
if (this.model === null) {
|
|
136
|
-
throw new Error('Model is null');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const tensor = tf.browser.fromPixels(data);
|
|
141
|
-
const resizedTensor = tf.image.resizeBilinear(tensor, [this.MOBILE_NET_INPUT_WIDTH, this.MOBILE_NET_INPUT_HEIGHT]);
|
|
142
|
-
const reshapedTensor = resizedTensor.expandDims(0); // 배치 크기 1을 추가하여 4차원으로 변환
|
|
143
|
-
const predictions = this.model.predict(reshapedTensor) as tf.Tensor;
|
|
144
|
-
const predictionsData = await predictions.data(); // 예측 텐서의 데이터를 비동기로 가져옴
|
|
145
|
-
const classProbabilities = new Map<string, number>(); // 클래스별 확률 누적값을 저장할 맵
|
|
146
|
-
for (let i = 0; i < predictionsData.length; i++) {
|
|
147
|
-
const className = this.labels[i]; // 클래스 이름
|
|
148
|
-
const probability = predictionsData[i];
|
|
149
|
-
const existingProbability = classProbabilities.get(className);
|
|
150
|
-
if (existingProbability !== undefined) {
|
|
151
|
-
classProbabilities.set(className, existingProbability + probability);
|
|
152
|
-
} else {
|
|
153
|
-
classProbabilities.set(className, probability);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
console.log('Class Probabilities:', classProbabilities);
|
|
157
|
-
return classProbabilities;
|
|
158
|
-
} catch (error) {
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// 모델 저장
|
|
165
|
-
public async saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void> {
|
|
166
|
-
console.log('saved model');
|
|
167
|
-
if (!this.isTrainedDone) {
|
|
168
|
-
return Promise.reject(new Error('Train is not done status'));
|
|
169
|
-
}
|
|
170
|
-
await this.model?.save(handlerOrURL, config);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// 진행중 여부
|
|
175
|
-
public running(): boolean {
|
|
176
|
-
return this.isRunning;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
public ready(): boolean {
|
|
181
|
-
return this.isReady;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// target 라벨 데이타
|
|
186
|
-
private _preprocessedTargetData(): tf.Tensor<tf.Rank> {
|
|
187
|
-
// 라벨 unique 처리 & 배열 리턴
|
|
188
|
-
console.log('uniqueLabels.length', this.labels, this.labels.length);
|
|
189
|
-
const labelIndices = this.labels.map((label) => this.labels.indexOf(label));
|
|
190
|
-
console.log('labelIndices', labelIndices);
|
|
191
|
-
const oneHotEncode = tf.oneHot(tf.tensor1d(labelIndices, 'int32'),this.labels.length);
|
|
192
|
-
console.log('oneHotEncode', oneHotEncode);
|
|
193
|
-
return oneHotEncode;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// 입력 이미지 데이타
|
|
198
|
-
private _preprocessedInputData(model: tf.LayersModel): tf.Tensor<tf.Rank> {
|
|
199
|
-
// 이미지 배열을 배치로 변환 - [null, 224, 224, 3]
|
|
200
|
-
const inputShape = model.inputs[0].shape;
|
|
201
|
-
console.log('inputShape', inputShape);
|
|
202
|
-
// inputShape를 이와 같이 포멧 맞춘다. for reshape to [224, 224, 3]
|
|
203
|
-
const inputShapeArray = inputShape.slice(1) as number[];
|
|
204
|
-
console.log('inputShapeArray', inputShapeArray);
|
|
205
|
-
const inputBatch = tf.stack(this.trainImages.map((image) => {
|
|
206
|
-
// 이미지 전처리 및 크기 조정 등을 수행한 후에
|
|
207
|
-
// 모델의 입력 형태로 변환하여 반환
|
|
208
|
-
const xs = this._preprocessData(image); // 전처리 함수는 사용자 정의해야 함
|
|
209
|
-
return tf.reshape(xs, inputShapeArray);
|
|
210
|
-
}));
|
|
211
|
-
return inputBatch;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// 모델 학습하기 위한 데이타 전처리 단계
|
|
216
|
-
private _preprocessData(tensor: tf.Tensor3D): tf.Tensor4D {
|
|
217
|
-
try {
|
|
218
|
-
// mobilenet model summary를 하면 위와 같이 224,224 사이즈의 입력값 설정되어 있다. ex) input_1 (InputLayer) [null,224,224,3]
|
|
219
|
-
const resizedImage = tf.image.resizeBilinear(tensor, [this.MOBILE_NET_INPUT_WIDTH, this.MOBILE_NET_INPUT_HEIGHT]);
|
|
220
|
-
// 이미지를 [0,1] 범위로 정규화 255로 나뉜 픽셀값
|
|
221
|
-
const normalizedImage = resizedImage.div(this.IMAGE_NORMALIZATION_FACTOR);
|
|
222
|
-
// expandDims(0)을 하여 차원을 추가하여 4D텐서 반환
|
|
223
|
-
return normalizedImage.expandDims(0) as tf.Tensor4D;
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error('Failed to _preprocessData data', error);
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 모델 저장
|
|
231
|
-
private async _createModel(numClasses: number): Promise<tf.Sequential> {
|
|
232
|
-
try {
|
|
233
|
-
const inputShape = [this.MOBILE_NET_INPUT_WIDTH, this.MOBILE_NET_INPUT_HEIGHT, this.MOBILE_NET_INPUT_CHANNEL];
|
|
234
|
-
const model = tf.sequential();
|
|
235
|
-
|
|
236
|
-
model.add(tf.layers.conv2d({
|
|
237
|
-
activation: "relu",
|
|
238
|
-
filters: 32,
|
|
239
|
-
inputShape: inputShape,
|
|
240
|
-
kernelSize: 3,
|
|
241
|
-
}));
|
|
242
|
-
model.add(tf.layers.conv2d({
|
|
243
|
-
activation: "relu",
|
|
244
|
-
filters: 32,
|
|
245
|
-
kernelSize: 3,
|
|
246
|
-
}));
|
|
247
|
-
model.add(tf.layers.maxPooling2d({poolSize: [2, 2]}));
|
|
248
|
-
model.add(tf.layers.conv2d({
|
|
249
|
-
activation: "relu",
|
|
250
|
-
filters: 64,
|
|
251
|
-
kernelSize: 3,
|
|
252
|
-
}));
|
|
253
|
-
model.add(tf.layers.conv2d({
|
|
254
|
-
activation: "relu",
|
|
255
|
-
filters: 64,
|
|
256
|
-
kernelSize: 3,
|
|
257
|
-
}));
|
|
258
|
-
model.add(tf.layers.maxPooling2d({poolSize: [2, 2]}));
|
|
259
|
-
model.add(tf.layers.flatten());
|
|
260
|
-
model.add(tf.layers.dropout({rate: 0.25}));
|
|
261
|
-
model.add(tf.layers.dense({units: 512, activation: "relu"}));
|
|
262
|
-
model.add(tf.layers.dropout({rate: 0.5}));
|
|
263
|
-
model.add(tf.layers.dense({
|
|
264
|
-
units: numClasses,
|
|
265
|
-
activation: 'softmax'
|
|
266
|
-
}));
|
|
267
|
-
|
|
268
|
-
const optimizer = tf.train.adam(this.learningRate); // Optimizer를 생성하고 학습률을 설정합니다.
|
|
269
|
-
model.compile({
|
|
270
|
-
loss: (numClasses === 2) ? 'binaryCrossentropy': 'categoricalCrossentropy',
|
|
271
|
-
optimizer: optimizer,
|
|
272
|
-
metrics: ['accuracy', 'acc']
|
|
273
|
-
});
|
|
274
|
-
model.summary();
|
|
275
|
-
return model;
|
|
276
|
-
} catch (error) {
|
|
277
|
-
console.error('Failed to load model', error);
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export default LearningImage;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as tf from '@tensorflow/tfjs';
|
|
3
|
-
import LearningMobilenetImage from './mobilenet_image';
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
const { createCanvas, loadImage } = require('canvas');
|
|
6
|
-
|
|
7
|
-
let imageTensor1: tf.Tensor3D;
|
|
8
|
-
let imageTensor2: tf.Tensor3D;
|
|
9
|
-
|
|
10
|
-
// 이미지경로를 기준으로
|
|
11
|
-
async function ImagePathToTensor(imagePath: string): Promise<tf.Tensor3D> {
|
|
12
|
-
const imageBuffer = fs.readFileSync(imagePath);
|
|
13
|
-
const image = await loadImage(imageBuffer);
|
|
14
|
-
const canvas = createCanvas(image.width, image.height);
|
|
15
|
-
const ctx = canvas.getContext('2d');
|
|
16
|
-
ctx.drawImage(image, 0, 0);
|
|
17
|
-
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
|
18
|
-
return tf.tensor3d(imageData.data, [imageData.height, imageData.width, 4], 'int32');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('LearningMobilenetImage', () => {
|
|
22
|
-
const learning = new LearningMobilenetImage({});
|
|
23
|
-
|
|
24
|
-
beforeAll(async () => {
|
|
25
|
-
const image1Path = path.join(__dirname, '../../public/images/image1.jpeg');
|
|
26
|
-
const image2Path = path.join(__dirname, '../../public/images/image2.jpeg');
|
|
27
|
-
imageTensor1 = await ImagePathToTensor(image1Path);
|
|
28
|
-
imageTensor2 = await ImagePathToTensor(image2Path);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('loads an image and converts it to a tensor', () => {
|
|
32
|
-
expect(imageTensor1).toBeDefined();
|
|
33
|
-
expect(imageTensor1 instanceof tf.Tensor).toBe(true);
|
|
34
|
-
expect(imageTensor2).toBeDefined();
|
|
35
|
-
expect(imageTensor2 instanceof tf.Tensor).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('mobilenet add data', () => {
|
|
39
|
-
learning.addData("라벨1", imageTensor1);
|
|
40
|
-
learning.addData("라벨1", imageTensor1);
|
|
41
|
-
learning.addData("라벨2", imageTensor2);
|
|
42
|
-
learning.addData("라벨2", imageTensor2);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////
|
|
2
|
-
///////////////////////////////////////////////////////////////////////////
|
|
3
|
-
///////////////////////////////////////////////////////////////////////////
|
|
4
|
-
// mobilenet 모델을 이용한 전이학습 방법
|
|
5
|
-
///////////////////////////////////////////////////////////////////////////
|
|
6
|
-
|
|
7
|
-
import * as tf from '@tensorflow/tfjs';
|
|
8
|
-
import { io } from '@tensorflow/tfjs-core';
|
|
9
|
-
import LearningInterface from './base';
|
|
10
|
-
import { ImageToTensor } from './util';
|
|
11
|
-
|
|
12
|
-
class LearningMobilenetImage implements LearningInterface {
|
|
13
|
-
model: tf.LayersModel | null;
|
|
14
|
-
epochs: number;
|
|
15
|
-
batchSize: number;
|
|
16
|
-
learningRate: number;
|
|
17
|
-
validateRate: number;
|
|
18
|
-
labels: string[];
|
|
19
|
-
modelURL: string;
|
|
20
|
-
isRunning: boolean;
|
|
21
|
-
isReady: boolean;
|
|
22
|
-
isTrainedDone: boolean;
|
|
23
|
-
limitSize: number;
|
|
24
|
-
trainImages: tf.Tensor3D[] = [];
|
|
25
|
-
|
|
26
|
-
readonly MOBILE_NET_INPUT_WIDTH = 224;
|
|
27
|
-
readonly MOBILE_NET_INPUT_HEIGHT = 224;
|
|
28
|
-
readonly MOBILE_NET_INPUT_CHANNEL = 3;
|
|
29
|
-
readonly IMAGE_NORMALIZATION_FACTOR = 255.0;
|
|
30
|
-
|
|
31
|
-
constructor({
|
|
32
|
-
modelURL = 'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json', // 디폴트 mobilenet 이미지
|
|
33
|
-
epochs = 10,
|
|
34
|
-
batchSize = 16,
|
|
35
|
-
limitSize = 2,
|
|
36
|
-
learningRate = 0.001,
|
|
37
|
-
validateRate = 0.2,
|
|
38
|
-
}: { modelURL?: string, epochs?: number, batchSize?: number, limitSize?: number, learningRate?: number, validateRate?: number} = {}) {
|
|
39
|
-
this.model = null;
|
|
40
|
-
this.epochs = epochs;
|
|
41
|
-
this.batchSize = batchSize;
|
|
42
|
-
this.learningRate = learningRate;
|
|
43
|
-
this.validateRate = validateRate;
|
|
44
|
-
this.labels = [];
|
|
45
|
-
this.modelURL = modelURL;
|
|
46
|
-
this.isRunning = false;
|
|
47
|
-
this.isReady = false;
|
|
48
|
-
this.isTrainedDone = false;
|
|
49
|
-
this.limitSize = limitSize;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 진행 상태를 나타내는 이벤트를 정의합니다.
|
|
53
|
-
public onProgress: (progress: number) => void = () => {};
|
|
54
|
-
|
|
55
|
-
public onLoss: (loss: number) => void = () => {};
|
|
56
|
-
|
|
57
|
-
public onEvents: (logs: any) => void = () => {};
|
|
58
|
-
|
|
59
|
-
public onTrainBegin: (log: any) => void = () => {};
|
|
60
|
-
|
|
61
|
-
public onTrainEnd: (log: any) => void = () => {};
|
|
62
|
-
|
|
63
|
-
public onEpochEnd: (epoch: number, logs: any) => void = () => {};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// 학습 데이타 등록
|
|
67
|
-
public async addData(label: string, data: any): Promise<void> {
|
|
68
|
-
try {
|
|
69
|
-
const tensor = ImageToTensor(data)
|
|
70
|
-
console.log('addData', tensor);
|
|
71
|
-
this.trainImages.push(tensor)
|
|
72
|
-
this.labels.push(label);
|
|
73
|
-
if(this.labels.length >= this.limitSize) {
|
|
74
|
-
this.isReady = true;
|
|
75
|
-
}
|
|
76
|
-
return Promise.resolve();
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error('Model training failed', error);
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 모델 학습 처리
|
|
84
|
-
public async train(): Promise<tf.History> {
|
|
85
|
-
if (this.isRunning) {
|
|
86
|
-
return Promise.reject(new Error('Training is already in progress.'));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 콜백 정의
|
|
90
|
-
const customCallback = {
|
|
91
|
-
onTrainBegin: (log: any) => {
|
|
92
|
-
this.isTrainedDone = false;
|
|
93
|
-
this.onTrainBegin(log);
|
|
94
|
-
console.log('Training has started.');
|
|
95
|
-
},
|
|
96
|
-
onTrainEnd: (log: any) => {
|
|
97
|
-
this.isTrainedDone = true;
|
|
98
|
-
this.onTrainEnd(log);
|
|
99
|
-
console.log('Training has ended.');
|
|
100
|
-
this.isRunning = false;
|
|
101
|
-
},
|
|
102
|
-
onBatchBegin: (batch: any, logs: any) => {
|
|
103
|
-
console.log(`Batch ${batch} is starting.`);
|
|
104
|
-
},
|
|
105
|
-
onBatchEnd: (batch: any, logs: any) => {
|
|
106
|
-
console.log(`Batch ${batch} has ended.`);
|
|
107
|
-
},
|
|
108
|
-
onEpochBegin: (epoch: number, logs: any) => {
|
|
109
|
-
console.log(`Epoch ${epoch+1} is starting.`, logs);
|
|
110
|
-
},
|
|
111
|
-
onEpochEnd: (epoch: number, logs: any) => {
|
|
112
|
-
console.log(`Epoch ${epoch+1} has ended.`);
|
|
113
|
-
console.log('Loss:', logs);
|
|
114
|
-
this.onLoss(logs.loss);
|
|
115
|
-
this.onProgress(epoch+1);
|
|
116
|
-
this.onEvents(logs);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
this.isRunning = true;
|
|
122
|
-
if (this.labels.length < this.limitSize) {
|
|
123
|
-
return Promise.reject(new Error('Please train Data need over 2 data length'));
|
|
124
|
-
}
|
|
125
|
-
this.model = await this._createModel(this.labels.length);
|
|
126
|
-
const inputData = this._preprocessedInputData(this.model);
|
|
127
|
-
const targetData = this._preprocessedTargetData();
|
|
128
|
-
const history = await this.model.fit(inputData, targetData, {
|
|
129
|
-
epochs: this.epochs,
|
|
130
|
-
batchSize: this.batchSize,
|
|
131
|
-
validationSplit: this.validateRate, // 검증 데이터의 비율 설정
|
|
132
|
-
callbacks: customCallback
|
|
133
|
-
});
|
|
134
|
-
console.log('Model training completed', history);
|
|
135
|
-
return history;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
this.isRunning = false;
|
|
138
|
-
console.error('Model training failed', error);
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 추론하기
|
|
144
|
-
public async infer(data: any): Promise<Map<string, number>> {
|
|
145
|
-
if (this.model === null) {
|
|
146
|
-
return Promise.reject(new Error('Model is Null'));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const tensor = tf.browser.fromPixels(data);
|
|
151
|
-
const resizedTensor = tf.image.resizeBilinear(tensor, [this.MOBILE_NET_INPUT_WIDTH, this.MOBILE_NET_INPUT_HEIGHT]);
|
|
152
|
-
const reshapedTensor = resizedTensor.expandDims(0); // 배치 크기 1을 추가하여 4차원으로 변환
|
|
153
|
-
const predictions = this.model.predict(reshapedTensor) as tf.Tensor;
|
|
154
|
-
const predictionsData = await predictions.data(); // 예측 텐서의 데이터를 비동기로 가져옴
|
|
155
|
-
const classProbabilities = new Map<string, number>(); // 클래스별 확률 누적값을 저장할 맵
|
|
156
|
-
for (let i = 0; i < predictionsData.length; i++) {
|
|
157
|
-
const className = this.labels[i]; // 클래스 이름
|
|
158
|
-
const probability = predictionsData[i];
|
|
159
|
-
const existingProbability = classProbabilities.get(className);
|
|
160
|
-
if (existingProbability !== undefined) {
|
|
161
|
-
classProbabilities.set(className, existingProbability + probability);
|
|
162
|
-
} else {
|
|
163
|
-
classProbabilities.set(className, probability);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
console.log('Class Probabilities:', classProbabilities);
|
|
167
|
-
return classProbabilities;
|
|
168
|
-
} catch (error) {
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// 모델 저장
|
|
175
|
-
public async saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void> {
|
|
176
|
-
console.log('saved model');
|
|
177
|
-
if (!this.isTrainedDone) {
|
|
178
|
-
return Promise.reject(new Error('Train is not done status'));
|
|
179
|
-
}
|
|
180
|
-
await this.model?.save(handlerOrURL, config);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// 진행중 여부
|
|
186
|
-
public running(): boolean {
|
|
187
|
-
return this.isRunning;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
public ready(): boolean {
|
|
192
|
-
return this.isReady;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// target 라벨 데이타
|
|
197
|
-
private _preprocessedTargetData(): tf.Tensor<tf.Rank> {
|
|
198
|
-
// 라벨 unique 처리 & 배열 리턴
|
|
199
|
-
console.log('uniqueLabels.length', this.labels, this.labels.length);
|
|
200
|
-
const labelIndices = this.labels.map((label) => this.labels.indexOf(label));
|
|
201
|
-
console.log('labelIndices', labelIndices);
|
|
202
|
-
const oneHotEncode = tf.oneHot(tf.tensor1d(labelIndices, 'int32'),this.labels.length);
|
|
203
|
-
console.log('oneHotEncode', oneHotEncode);
|
|
204
|
-
return oneHotEncode
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// 입력 이미지 데이타
|
|
209
|
-
private _preprocessedInputData(model: tf.LayersModel): tf.Tensor<tf.Rank> {
|
|
210
|
-
// 이미지 배열을 배치로 변환 - [null, 224, 224, 3]
|
|
211
|
-
const inputShape = model.inputs[0].shape;
|
|
212
|
-
console.log('inputShape', inputShape);
|
|
213
|
-
// inputShape를 이와 같이 포멧 맞춘다. for reshape to [224, 224, 3]
|
|
214
|
-
const inputShapeArray = inputShape.slice(1) as number[];
|
|
215
|
-
console.log('inputShapeArray', inputShapeArray);
|
|
216
|
-
const inputBatch = tf.stack(this.trainImages.map((image) => {
|
|
217
|
-
// 이미지 전처리 및 크기 조정 등을 수행한 후에
|
|
218
|
-
// 모델의 입력 형태로 변환하여 반환
|
|
219
|
-
const xs = this._preprocessData(image); // 전처리 함수는 사용자 정의해야 함
|
|
220
|
-
return tf.reshape(xs, inputShapeArray);
|
|
221
|
-
}));
|
|
222
|
-
return inputBatch
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// 모델 학습하기 위한 데이타 전처리 단계
|
|
227
|
-
private _preprocessData(tensor: tf.Tensor3D): tf.Tensor4D {
|
|
228
|
-
try {
|
|
229
|
-
// mobilenet model summary를 하면 위와 같이 224,224 사이즈의 입력값 설정되어 있다. ex) input_1 (InputLayer) [null,224,224,3]
|
|
230
|
-
const resizedImage = tf.image.resizeBilinear(tensor, [this.MOBILE_NET_INPUT_WIDTH, this.MOBILE_NET_INPUT_HEIGHT]);
|
|
231
|
-
// 이미지를 [0,1] 범위로 정규화 255로 나뉜 픽셀값
|
|
232
|
-
const normalizedImage = resizedImage.div(this.IMAGE_NORMALIZATION_FACTOR);
|
|
233
|
-
// expandDims(0)을 하여 차원을 추가하여 4D텐서 반환
|
|
234
|
-
return normalizedImage.expandDims(0) as tf.Tensor4D;
|
|
235
|
-
} catch (error) {
|
|
236
|
-
console.error('Failed to _preprocessData data', error);
|
|
237
|
-
throw error;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// 모델 저장
|
|
242
|
-
private async _createModel(numClasses: number): Promise<tf.Sequential> {
|
|
243
|
-
try {
|
|
244
|
-
const load_model = await tf.loadLayersModel(this.modelURL);
|
|
245
|
-
// 기존 MobileNet 모델에서 마지막 레이어 제외
|
|
246
|
-
const truncatedModel = tf.model({
|
|
247
|
-
inputs: load_model.inputs,
|
|
248
|
-
outputs: load_model.layers[load_model.layers.length - 2].output
|
|
249
|
-
});
|
|
250
|
-
// 모델을 학습 가능하게 설정하고 선택한 레이어까지 고정
|
|
251
|
-
for(let layer of truncatedModel.layers) {
|
|
252
|
-
layer.trainable = false;
|
|
253
|
-
}
|
|
254
|
-
const model = tf.sequential();
|
|
255
|
-
model.add(truncatedModel);
|
|
256
|
-
model.add(tf.layers.flatten()); // 필요한 경우 Flatten 레이어 추가
|
|
257
|
-
model.add(tf.layers.dense({
|
|
258
|
-
units: numClasses,
|
|
259
|
-
activation: 'softmax'
|
|
260
|
-
}));
|
|
261
|
-
|
|
262
|
-
const optimizer = tf.train.adam(this.learningRate); // Optimizer를 생성하고 학습률을 설정합니다.
|
|
263
|
-
model.compile({
|
|
264
|
-
loss: (numClasses === 2) ? 'binaryCrossentropy': 'categoricalCrossentropy',
|
|
265
|
-
optimizer: optimizer,
|
|
266
|
-
metrics: ['accuracy', 'acc']
|
|
267
|
-
});
|
|
268
|
-
model.summary();
|
|
269
|
-
return model;
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.error('Failed to load model', error);
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export default LearningMobilenetImage;
|