learning_model 1.0.28 → 1.0.30
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 +2 -1
- package/dist/index.js +3 -1
- package/dist/learning/base.d.ts +3 -2
- package/dist/learning/image.d.ts +3 -1
- package/dist/learning/image.js +23 -12
- package/dist/learning/mobilenet.d.ts +49 -0
- package/dist/learning/mobilenet.js +317 -0
- package/dist/learning/mobilenet.test.d.ts +1 -0
- package/dist/learning/mobilenet.test.js +77 -0
- package/dist/learning/mobilenet_image.d.ts +3 -1
- package/dist/learning/mobilenet_image.js +5 -6
- package/dist/learning/util.js +1 -0
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/learning/base.d.ts +3 -2
- package/dist/lib/learning/image.d.ts +3 -1
- package/dist/lib/learning/mobilenet.d.ts +49 -0
- package/dist/lib/learning/mobilenet.test.d.ts +1 -0
- package/dist/lib/learning/mobilenet_image.d.ts +3 -1
- package/lib/index.ts +2 -1
- package/lib/learning/base.ts +4 -4
- package/lib/learning/image.ts +26 -13
- package/lib/learning/mobilenet.test.ts +44 -0
- package/lib/learning/mobilenet.ts +322 -0
- package/lib/learning/mobilenet_image.ts +7 -6
- package/lib/learning/util.ts +1 -0
- package/package.json +1 -1
- package/yarn-error.log +10 -10
package/dist/learning/util.js
CHANGED
package/dist/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import LearningImage from './learning/image';
|
|
2
2
|
import LearningMobilenetImage from './learning/mobilenet_image';
|
|
3
|
-
|
|
3
|
+
import LearningMobilenet from './learning/mobilenet';
|
|
4
|
+
export { LearningImage, LearningMobilenetImage, LearningMobilenet };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as tf from '@tensorflow/tfjs';
|
|
2
|
+
import { io } from '@tensorflow/tfjs-core';
|
|
2
3
|
interface LearningInterface {
|
|
3
4
|
model: tf.LayersModel | null;
|
|
4
5
|
labels: string[];
|
|
@@ -6,13 +7,13 @@ interface LearningInterface {
|
|
|
6
7
|
isReady: boolean;
|
|
7
8
|
onProgress(progress: number): void;
|
|
8
9
|
onLoss(loss: number): void;
|
|
9
|
-
onEvents(logs: any): void;
|
|
10
10
|
onTrainBegin(log: any): void;
|
|
11
11
|
onTrainEnd(log: any): void;
|
|
12
|
+
onEpochEnd(epoch: number, logs: any): void;
|
|
12
13
|
addData(label: string, data: any): Promise<void>;
|
|
13
14
|
train(): Promise<tf.History>;
|
|
14
15
|
infer(data: any): Promise<any>;
|
|
15
|
-
saveModel(): Promise<void>;
|
|
16
|
+
saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void>;
|
|
16
17
|
running(): boolean;
|
|
17
18
|
ready(): boolean;
|
|
18
19
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as tf from '@tensorflow/tfjs';
|
|
2
|
+
import { io } from '@tensorflow/tfjs-core';
|
|
2
3
|
import LearningInterface from './base';
|
|
3
4
|
declare class LearningImage implements LearningInterface {
|
|
4
5
|
model: tf.LayersModel | null;
|
|
@@ -29,10 +30,11 @@ declare class LearningImage implements LearningInterface {
|
|
|
29
30
|
onEvents: (logs: any) => void;
|
|
30
31
|
onTrainBegin: (log: any) => void;
|
|
31
32
|
onTrainEnd: (log: any) => void;
|
|
33
|
+
onEpochEnd: (epoch: number, logs: any) => void;
|
|
32
34
|
addData(label: string, data: any): Promise<void>;
|
|
33
35
|
train(): Promise<tf.History>;
|
|
34
36
|
infer(data: any): Promise<Map<string, number>>;
|
|
35
|
-
saveModel(): Promise<void>;
|
|
37
|
+
saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void>;
|
|
36
38
|
running(): boolean;
|
|
37
39
|
ready(): boolean;
|
|
38
40
|
private _preprocessedTargetData;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as tf from '@tensorflow/tfjs';
|
|
2
|
+
import { io } from '@tensorflow/tfjs-core';
|
|
3
|
+
import LearningInterface from './base';
|
|
4
|
+
declare class LearningMobilenet implements LearningInterface {
|
|
5
|
+
model: tf.LayersModel | null;
|
|
6
|
+
epochs: number;
|
|
7
|
+
batchSize: number;
|
|
8
|
+
learningRate: number;
|
|
9
|
+
validateRate: number;
|
|
10
|
+
labels: string[];
|
|
11
|
+
modelURL: string;
|
|
12
|
+
isRunning: boolean;
|
|
13
|
+
isReady: boolean;
|
|
14
|
+
isTrainedDone: boolean;
|
|
15
|
+
limitSize: number;
|
|
16
|
+
mobilenetModule: tf.LayersModel | null;
|
|
17
|
+
imageTensors: tf.Tensor<tf.Rank>[];
|
|
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;
|
|
22
|
+
constructor({ modelURL, // 디폴트 mobilenet 이미지
|
|
23
|
+
epochs, batchSize, limitSize, learningRate, validateRate, }?: {
|
|
24
|
+
modelURL?: string;
|
|
25
|
+
epochs?: number;
|
|
26
|
+
batchSize?: number;
|
|
27
|
+
limitSize?: number;
|
|
28
|
+
learningRate?: number;
|
|
29
|
+
validateRate?: number;
|
|
30
|
+
});
|
|
31
|
+
onProgress: (progress: number) => void;
|
|
32
|
+
onLoss: (loss: number) => void;
|
|
33
|
+
onEvents: (logs: any) => void;
|
|
34
|
+
onTrainBegin: (log: any) => void;
|
|
35
|
+
onTrainEnd: (log: any) => void;
|
|
36
|
+
onEpochEnd: (epoch: number, logs: any) => void;
|
|
37
|
+
addData(label: string, data: any): Promise<void>;
|
|
38
|
+
init(): Promise<void>;
|
|
39
|
+
train(): Promise<tf.History>;
|
|
40
|
+
infer(data: any): Promise<Map<string, number>>;
|
|
41
|
+
saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void>;
|
|
42
|
+
running(): boolean;
|
|
43
|
+
ready(): boolean;
|
|
44
|
+
private _preprocessedTargetData;
|
|
45
|
+
private _preprocessedInputData;
|
|
46
|
+
private loadModel;
|
|
47
|
+
private _createModel;
|
|
48
|
+
}
|
|
49
|
+
export default LearningMobilenet;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as tf from '@tensorflow/tfjs';
|
|
2
|
+
import { io } from '@tensorflow/tfjs-core';
|
|
2
3
|
import LearningInterface from './base';
|
|
3
4
|
declare class LearningMobilenetImage implements LearningInterface {
|
|
4
5
|
model: tf.LayersModel | null;
|
|
@@ -31,10 +32,11 @@ declare class LearningMobilenetImage implements LearningInterface {
|
|
|
31
32
|
onEvents: (logs: any) => void;
|
|
32
33
|
onTrainBegin: (log: any) => void;
|
|
33
34
|
onTrainEnd: (log: any) => void;
|
|
35
|
+
onEpochEnd: (epoch: number, logs: any) => void;
|
|
34
36
|
addData(label: string, data: any): Promise<void>;
|
|
35
37
|
train(): Promise<tf.History>;
|
|
36
38
|
infer(data: any): Promise<Map<string, number>>;
|
|
37
|
-
saveModel(): Promise<void>;
|
|
39
|
+
saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void>;
|
|
38
40
|
running(): boolean;
|
|
39
41
|
ready(): boolean;
|
|
40
42
|
private _preprocessedTargetData;
|
package/lib/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import LearningImage from './learning/image';
|
|
2
2
|
import LearningMobilenetImage from './learning/mobilenet_image';
|
|
3
|
+
import LearningMobilenet from './learning/mobilenet';
|
|
3
4
|
|
|
4
|
-
export { LearningImage, LearningMobilenetImage };
|
|
5
|
+
export { LearningImage, LearningMobilenetImage, LearningMobilenet };
|
package/lib/learning/base.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as tf from '@tensorflow/tfjs';
|
|
2
|
+
import { io } from '@tensorflow/tfjs-core';
|
|
2
3
|
|
|
3
4
|
// AI Learning 디폴트 인터페이스
|
|
4
5
|
interface LearningInterface {
|
|
@@ -17,15 +18,14 @@ interface LearningInterface {
|
|
|
17
18
|
// loss 정보
|
|
18
19
|
onLoss(loss: number): void;
|
|
19
20
|
|
|
20
|
-
// 이벤트 로그 정보
|
|
21
|
-
onEvents(logs: any): void;
|
|
22
|
-
|
|
23
21
|
// 학습 시작 이벤트
|
|
24
22
|
onTrainBegin(log: any): void;
|
|
25
23
|
|
|
26
24
|
// 학습 종료 이벤트
|
|
27
25
|
onTrainEnd(log: any): void;
|
|
28
26
|
|
|
27
|
+
onEpochEnd(epoch: number, logs: any): void
|
|
28
|
+
|
|
29
29
|
// 학습 데이타 추가 data는 다른 클래스에 맞게 any로 설정
|
|
30
30
|
addData(label: string, data: any): Promise<void>;
|
|
31
31
|
|
|
@@ -36,7 +36,7 @@ interface LearningInterface {
|
|
|
36
36
|
infer(data: any): Promise<any>;
|
|
37
37
|
|
|
38
38
|
// 모델 저장
|
|
39
|
-
saveModel(): Promise<void>;
|
|
39
|
+
saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void>;
|
|
40
40
|
|
|
41
41
|
// 학습 진행 여부
|
|
42
42
|
running(): boolean;
|
package/lib/learning/image.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as tf from '@tensorflow/tfjs';
|
|
2
|
+
import { io } from '@tensorflow/tfjs-core';
|
|
2
3
|
import LearningInterface from './base';
|
|
3
4
|
|
|
4
5
|
class LearningImage implements LearningInterface {
|
|
@@ -48,6 +49,8 @@ class LearningImage implements LearningInterface {
|
|
|
48
49
|
|
|
49
50
|
public onTrainEnd: (log: any) => void = () => {};
|
|
50
51
|
|
|
52
|
+
public onEpochEnd: (epoch: number, logs: any) => void = () => {};
|
|
53
|
+
|
|
51
54
|
// 학습 데이타 등록
|
|
52
55
|
public async addData(label: string, data: any): Promise<void> {
|
|
53
56
|
try {
|
|
@@ -129,7 +132,6 @@ class LearningImage implements LearningInterface {
|
|
|
129
132
|
|
|
130
133
|
// 추론하기
|
|
131
134
|
public async infer(data: any): Promise<Map<string, number>> {
|
|
132
|
-
const threshold = 1e-20; // 임계값 설정
|
|
133
135
|
if (this.model === null) {
|
|
134
136
|
throw new Error('Model is null');
|
|
135
137
|
}
|
|
@@ -141,16 +143,14 @@ class LearningImage implements LearningInterface {
|
|
|
141
143
|
const predictions = this.model.predict(reshapedTensor) as tf.Tensor;
|
|
142
144
|
const predictionsData = await predictions.data(); // 예측 텐서의 데이터를 비동기로 가져옴
|
|
143
145
|
const classProbabilities = new Map<string, number>(); // 클래스별 확률 누적값을 저장할 맵
|
|
144
|
-
console.log('predictionsData', predictionsData);
|
|
145
146
|
for (let i = 0; i < predictionsData.length; i++) {
|
|
146
147
|
const className = this.labels[i]; // 클래스 이름
|
|
147
148
|
const probability = predictionsData[i];
|
|
148
|
-
const result = Math.abs(probability) < threshold ? 0 : probability;
|
|
149
149
|
const existingProbability = classProbabilities.get(className);
|
|
150
150
|
if (existingProbability !== undefined) {
|
|
151
|
-
classProbabilities.set(className, existingProbability +
|
|
151
|
+
classProbabilities.set(className, existingProbability + probability);
|
|
152
152
|
} else {
|
|
153
|
-
classProbabilities.set(className,
|
|
153
|
+
classProbabilities.set(className, probability);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
console.log('Class Probabilities:', classProbabilities);
|
|
@@ -162,12 +162,12 @@ class LearningImage implements LearningInterface {
|
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
// 모델 저장
|
|
165
|
-
public async saveModel(): Promise<void> {
|
|
165
|
+
public async saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void> {
|
|
166
166
|
console.log('saved model');
|
|
167
167
|
if (!this.isTrainedDone) {
|
|
168
168
|
return Promise.reject(new Error('Train is not done status'));
|
|
169
169
|
}
|
|
170
|
-
await this.model?.save(
|
|
170
|
+
await this.model?.save(handlerOrURL, config);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
|
|
@@ -232,21 +232,34 @@ class LearningImage implements LearningInterface {
|
|
|
232
232
|
try {
|
|
233
233
|
const inputShape = [this.MOBILE_NET_INPUT_WIDTH, this.MOBILE_NET_INPUT_HEIGHT, this.MOBILE_NET_INPUT_CHANNEL];
|
|
234
234
|
const model = tf.sequential();
|
|
235
|
-
|
|
235
|
+
|
|
236
|
+
model.add(tf.layers.conv2d({
|
|
237
|
+
activation: "relu",
|
|
238
|
+
filters: 32,
|
|
239
|
+
inputShape: inputShape,
|
|
240
|
+
kernelSize: 3,
|
|
241
|
+
}));
|
|
236
242
|
model.add(tf.layers.conv2d({
|
|
237
|
-
|
|
243
|
+
activation: "relu",
|
|
238
244
|
filters: 32,
|
|
239
245
|
kernelSize: 3,
|
|
240
|
-
activation: 'relu'
|
|
241
246
|
}));
|
|
242
|
-
model.add(tf.layers.maxPooling2d({
|
|
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
|
+
}));
|
|
243
253
|
model.add(tf.layers.conv2d({
|
|
254
|
+
activation: "relu",
|
|
244
255
|
filters: 64,
|
|
245
256
|
kernelSize: 3,
|
|
246
|
-
activation: 'relu'
|
|
247
257
|
}));
|
|
248
|
-
model.add(tf.layers.maxPooling2d({
|
|
258
|
+
model.add(tf.layers.maxPooling2d({poolSize: [2, 2]}));
|
|
249
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}));
|
|
250
263
|
model.add(tf.layers.dense({
|
|
251
264
|
units: numClasses,
|
|
252
265
|
activation: 'softmax'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as tf from '@tensorflow/tfjs';
|
|
3
|
+
import LearningMobilenetImage from './mobilenet';
|
|
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
|
+
});
|
|
@@ -0,0 +1,322 @@
|
|
|
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 LearningMobilenet 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
|
+
mobilenetModule: tf.LayersModel | null;
|
|
25
|
+
imageTensors: tf.Tensor<tf.Rank>[] = [];
|
|
26
|
+
|
|
27
|
+
readonly MOBILE_NET_INPUT_WIDTH = 224;
|
|
28
|
+
readonly MOBILE_NET_INPUT_HEIGHT = 224;
|
|
29
|
+
readonly MOBILE_NET_INPUT_CHANNEL = 3;
|
|
30
|
+
readonly IMAGE_NORMALIZATION_FACTOR = 255.0;
|
|
31
|
+
|
|
32
|
+
constructor({
|
|
33
|
+
modelURL = 'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json', // 디폴트 mobilenet 이미지
|
|
34
|
+
epochs = 10,
|
|
35
|
+
batchSize = 16,
|
|
36
|
+
limitSize = 2,
|
|
37
|
+
learningRate = 0.001,
|
|
38
|
+
validateRate = 0.2,
|
|
39
|
+
}: { modelURL?: string, epochs?: number, batchSize?: number, limitSize?: number, learningRate?: number, validateRate?: number} = {}) {
|
|
40
|
+
this.model = null;
|
|
41
|
+
this.epochs = epochs;
|
|
42
|
+
this.batchSize = batchSize;
|
|
43
|
+
this.learningRate = learningRate;
|
|
44
|
+
this.validateRate = validateRate;
|
|
45
|
+
this.labels = [];
|
|
46
|
+
this.modelURL = modelURL;
|
|
47
|
+
this.isRunning = false;
|
|
48
|
+
this.isReady = false;
|
|
49
|
+
this.isTrainedDone = false;
|
|
50
|
+
this.limitSize = limitSize;
|
|
51
|
+
this.mobilenetModule = null;
|
|
52
|
+
this.imageTensors = [];
|
|
53
|
+
this.init();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 진행 상태를 나타내는 이벤트를 정의합니다.
|
|
57
|
+
public onProgress: (progress: number) => void = () => {};
|
|
58
|
+
|
|
59
|
+
public onLoss: (loss: number) => void = () => {};
|
|
60
|
+
|
|
61
|
+
public onEvents: (logs: any) => void = () => {};
|
|
62
|
+
|
|
63
|
+
public onTrainBegin: (log: any) => void = () => {};
|
|
64
|
+
|
|
65
|
+
public onTrainEnd: (log: any) => void = () => {};
|
|
66
|
+
|
|
67
|
+
public onEpochEnd: (epoch: number, logs: any) => void = () => {};
|
|
68
|
+
|
|
69
|
+
// 학습 데이타 등록
|
|
70
|
+
public async addData(label: string, data: any): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
const tensor = ImageToTensor(data)
|
|
75
|
+
|
|
76
|
+
const imgTensor = tf.tidy(() => {
|
|
77
|
+
if (this.mobilenetModule !== null) {
|
|
78
|
+
console.log('predict before', data);
|
|
79
|
+
const t = tf.browser.fromPixels(data)
|
|
80
|
+
// Resize the image to match the model's input shape
|
|
81
|
+
const resizedTensor = tf.image.resizeBilinear(t, [224, 224]);
|
|
82
|
+
const normalizedTensor = resizedTensor.div(255.0);
|
|
83
|
+
|
|
84
|
+
// Add an extra dimension to the tensor
|
|
85
|
+
const expandedTensor = normalizedTensor.expandDims(0);
|
|
86
|
+
|
|
87
|
+
console.log('predict extend', expandedTensor);
|
|
88
|
+
const predict = this.mobilenetModule.predict(expandedTensor);
|
|
89
|
+
console.log('predict', predict);
|
|
90
|
+
return predict;
|
|
91
|
+
} else {
|
|
92
|
+
throw new Error('mobilenetModule is null');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
console.log('imgTensor', imgTensor);
|
|
96
|
+
this.imageTensors.push(imgTensor as tf.Tensor<tf.Rank>);
|
|
97
|
+
this.labels.push(label);
|
|
98
|
+
if(this.labels.length >= this.limitSize) {
|
|
99
|
+
this.isReady = true;
|
|
100
|
+
}
|
|
101
|
+
return Promise.resolve();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Model training failed', error);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async init() {
|
|
109
|
+
const model = await this.loadModel();
|
|
110
|
+
// const load_model = await tf.loadLayersModel(this.modelURL);
|
|
111
|
+
this.mobilenetModule = model;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 모델 학습 처리
|
|
115
|
+
public async train(): Promise<tf.History> {
|
|
116
|
+
if (this.isRunning) {
|
|
117
|
+
return Promise.reject(new Error('Training is already in progress.'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 콜백 정의
|
|
121
|
+
const customCallback = {
|
|
122
|
+
onTrainBegin: (log: any) => {
|
|
123
|
+
this.isTrainedDone = false;
|
|
124
|
+
this.onTrainBegin(log);
|
|
125
|
+
console.log('Training has started.');
|
|
126
|
+
},
|
|
127
|
+
onTrainEnd: (log: any) => {
|
|
128
|
+
this.isTrainedDone = true;
|
|
129
|
+
this.onTrainEnd(log);
|
|
130
|
+
console.log('Training has ended.');
|
|
131
|
+
this.isRunning = false;
|
|
132
|
+
},
|
|
133
|
+
onBatchBegin: (batch: any, logs: any) => {
|
|
134
|
+
console.log(`Batch ${batch} is starting.`);
|
|
135
|
+
},
|
|
136
|
+
onBatchEnd: (batch: any, logs: any) => {
|
|
137
|
+
console.log(`Batch ${batch} has ended.`);
|
|
138
|
+
},
|
|
139
|
+
onEpochBegin: (epoch: number, logs: any) => {
|
|
140
|
+
console.log(`Epoch ${epoch+1} is starting.`, logs);
|
|
141
|
+
},
|
|
142
|
+
onEpochEnd: (epoch: number, logs: any) => {
|
|
143
|
+
this.onEpochEnd(epoch, logs);
|
|
144
|
+
console.log(`Epoch ${epoch+1} has ended.`);
|
|
145
|
+
console.log('Loss:', logs);
|
|
146
|
+
this.onLoss(logs.loss);
|
|
147
|
+
this.onProgress(epoch+1);
|
|
148
|
+
this.onEvents({
|
|
149
|
+
epoch: epoch,
|
|
150
|
+
logs: logs,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
this.isRunning = true;
|
|
157
|
+
if (this.labels.length < this.limitSize) {
|
|
158
|
+
return Promise.reject(new Error('Please train Data need over 2 data length'));
|
|
159
|
+
}
|
|
160
|
+
this.model = await this._createModel(this.labels.length);
|
|
161
|
+
const inputData = this._preprocessedInputData(this.model);
|
|
162
|
+
|
|
163
|
+
console.log('this.imageTensors', this.imageTensors, inputData);
|
|
164
|
+
const targetData = this._preprocessedTargetData();
|
|
165
|
+
const history = await this.model.fit(inputData, targetData, {
|
|
166
|
+
epochs: this.epochs,
|
|
167
|
+
batchSize: this.batchSize,
|
|
168
|
+
validationSplit: this.validateRate, // 검증 데이터의 비율 설정
|
|
169
|
+
callbacks: customCallback
|
|
170
|
+
});
|
|
171
|
+
console.log('Model training completed', history);
|
|
172
|
+
return history;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.isRunning = false;
|
|
175
|
+
console.error('Model training failed', error);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 추론하기
|
|
181
|
+
public async infer(data: any): Promise<Map<string, number>> {
|
|
182
|
+
if (this.model === null) {
|
|
183
|
+
return Promise.reject(new Error('Model is Null'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const imgTensor = tf.tidy(() => {
|
|
188
|
+
if (this.mobilenetModule !== null) {
|
|
189
|
+
console.log('predict before', data);
|
|
190
|
+
const t = tf.browser.fromPixels(data)
|
|
191
|
+
// Resize the image to match the model's input shape
|
|
192
|
+
const resizedTensor = tf.image.resizeBilinear(t, [224, 224]);
|
|
193
|
+
const normalizedTensor = resizedTensor.div(255.0);
|
|
194
|
+
|
|
195
|
+
// Add an extra dimension to the tensor
|
|
196
|
+
const expandedTensor = normalizedTensor.expandDims(0);
|
|
197
|
+
|
|
198
|
+
console.log('predict extend', expandedTensor);
|
|
199
|
+
const predict = this.mobilenetModule.predict(expandedTensor);
|
|
200
|
+
console.log('predict', predict);
|
|
201
|
+
return predict;
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error('mobilenetModule is null');
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const predictions = this.model.predict(imgTensor) as tf.Tensor;
|
|
207
|
+
const predictedClass = predictions.as1D().argMax();
|
|
208
|
+
const classId = (await predictedClass.data())[0];
|
|
209
|
+
console.log('classId', classId, this.labels[classId]);
|
|
210
|
+
const predictionsData = await predictions.data(); // 예측 텐서의 데이터를 비동기로 가져옴
|
|
211
|
+
const classProbabilities = new Map<string, number>(); // 클래스별 확률 누적값을 저장할 맵
|
|
212
|
+
|
|
213
|
+
const EPSILON = 1e-6; // 매우 작은 값을 표현하기 위한 엡실론
|
|
214
|
+
for (let i = 0; i < predictionsData.length; i++) {
|
|
215
|
+
let probability = Math.max(0, Math.min(1, predictionsData[i])); // 확률 값을 0과 1 사이로 조정
|
|
216
|
+
probability = probability < EPSILON ? 0 : probability; // 매우 작은 확률 값을 0으로 간주
|
|
217
|
+
const className = this.labels[i]; // 클래스 이름
|
|
218
|
+
const existingProbability = classProbabilities.get(className);
|
|
219
|
+
if (existingProbability !== undefined) {
|
|
220
|
+
classProbabilities.set(className, existingProbability + probability);
|
|
221
|
+
} else {
|
|
222
|
+
classProbabilities.set(className, probability);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
console.log('Class Probabilities:', classProbabilities);
|
|
226
|
+
predictedClass.dispose();
|
|
227
|
+
predictions.dispose();
|
|
228
|
+
return classProbabilities;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// 모델 저장
|
|
236
|
+
public async saveModel(handlerOrURL: io.IOHandler | string, config?: io.SaveConfig): Promise<void> {
|
|
237
|
+
console.log('saved model');
|
|
238
|
+
if (!this.isTrainedDone) {
|
|
239
|
+
return Promise.reject(new Error('Train is not done status'));
|
|
240
|
+
}
|
|
241
|
+
await this.model?.save(handlerOrURL, config);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
// 진행중 여부
|
|
247
|
+
public running(): boolean {
|
|
248
|
+
return this.isRunning;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
public ready(): boolean {
|
|
253
|
+
return this.isReady;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
// target 라벨 데이타
|
|
258
|
+
private _preprocessedTargetData(): tf.Tensor<tf.Rank> {
|
|
259
|
+
// 라벨 unique 처리 & 배열 리턴
|
|
260
|
+
console.log('uniqueLabels.length', this.labels, this.labels.length);
|
|
261
|
+
const labelIndices = this.labels.map((label) => this.labels.indexOf(label));
|
|
262
|
+
console.log('labelIndices', labelIndices);
|
|
263
|
+
const oneHotEncode = tf.oneHot(tf.tensor1d(labelIndices, 'int32'),this.labels.length);
|
|
264
|
+
console.log('oneHotEncode', oneHotEncode);
|
|
265
|
+
return oneHotEncode
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 입력 이미지 데이타
|
|
269
|
+
private _preprocessedInputData(model: tf.LayersModel): tf.Tensor<tf.Rank> {
|
|
270
|
+
// 이미지 배열을 배치로 변환 - [null, 224, 224, 3]
|
|
271
|
+
const inputShape = model.inputs[0].shape;
|
|
272
|
+
console.log('inputShape', inputShape);
|
|
273
|
+
// inputShape를 이와 같이 포멧 맞춘다. for reshape to [224, 224, 3]
|
|
274
|
+
const inputShapeArray = inputShape.slice(1) as number[];
|
|
275
|
+
console.log('inputShapeArray', inputShapeArray);
|
|
276
|
+
const inputBatch = tf.stack(this.imageTensors.map((image) => {
|
|
277
|
+
return tf.reshape(image, inputShapeArray);
|
|
278
|
+
}));
|
|
279
|
+
return inputBatch
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
private async loadModel(): Promise<tf.LayersModel> {
|
|
284
|
+
const load_model = await tf.loadLayersModel(this.modelURL);
|
|
285
|
+
load_model.summary();
|
|
286
|
+
const layer = load_model.getLayer('conv_pw_13_relu');
|
|
287
|
+
const truncatedModel = tf.model({
|
|
288
|
+
inputs: load_model.inputs,
|
|
289
|
+
outputs: layer.output
|
|
290
|
+
})
|
|
291
|
+
return truncatedModel
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 모델 저장
|
|
295
|
+
private async _createModel(numClasses: number): Promise<tf.Sequential> {
|
|
296
|
+
try {
|
|
297
|
+
const truncatedModel = await this.loadModel()
|
|
298
|
+
console.log('truncatedModel', truncatedModel, truncatedModel.outputs[0].shape.slice(1));
|
|
299
|
+
// 입력 이미지 크기에 맞게 모델 구조 수정
|
|
300
|
+
const model = tf.sequential();
|
|
301
|
+
model.add(tf.layers.flatten({
|
|
302
|
+
inputShape: truncatedModel.outputs[0].shape.slice(1)
|
|
303
|
+
}));
|
|
304
|
+
model.add(tf.layers.dense({ units: 100, activation: 'relu' }));
|
|
305
|
+
model.add(tf.layers.dense({ units: this.labels.length, activation: 'softmax' }));
|
|
306
|
+
|
|
307
|
+
const optimizer = tf.train.adam(this.learningRate); // Optimizer를 생성하고 학습률을 설정합니다.
|
|
308
|
+
model.compile({
|
|
309
|
+
loss: (numClasses === 2) ? 'binaryCrossentropy': 'categoricalCrossentropy',
|
|
310
|
+
optimizer: optimizer,
|
|
311
|
+
metrics: ['accuracy', 'acc']
|
|
312
|
+
});
|
|
313
|
+
model.summary();
|
|
314
|
+
return model;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error('Failed to load model', error);
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default LearningMobilenet;
|