맞춤형 케라 레이어 및 모델을 구축하기 전에 이것을 읽으십시오

맞춤형 케라 레이어 및 모델을 구축하기 전에 이것을 읽으십시오

콘텐츠 개요

  • 설정
  • 레이어 클래스 : 상태 (가중치)와 일부 계산의 조합
  • 레이어는 비전되지 않는 가중치를 가질 수 있습니다
  • 모범 사례 : 입력의 모양이 알려질 때까지 체중 생성을 연기
  • 레이어는 재귀 적으로 합성 가능합니다
  • add_loss () 메소드
  • 선택적으로 레이어에서 직렬화를 활성화 할 수 있습니다
  • Call () 메소드의 특권 교육 인수
  • Call () 메소드의 권한이있는 마스크 인수
  • 모델 클래스
  • 모든 것을 정리하십시오 : 엔드 투 엔드 예

\

설정

import tensorflow as tf
from tensorflow import keras

그만큼Layer클래스 : 상태 (가중치)와 일부 계산의 조합

Keras의 중심 추상화 중 하나는입니다Layer수업. 레이어는 상태 (레이어의 “가중치”)와 입력에서 출력 ( “호출”, 레이어의 순방향 패스)으로 변환을 모두 캡슐화합니다.

밀도가 높은 레이어는 다음과 같습니다. 상태가 있습니다 : 변수w그리고b.

\

class Linear(keras.layers.Layer):
    def __init__(self, units=32, input_dim=32):
        super().__init__()
        self.w = self.add_weight(
            shape=(input_dim, units), initializer="random_normal", trainable=True
        )
        self.b = self.add_weight(shape=(units,), initializer="zeros", trainable=True)

    def call(self, inputs):
        return tf.matmul(inputs, self.w) + self.b

파이썬 함수와 마찬가지로 텐서 입력에서 호출하여 레이어를 사용합니다.

\

x = tf.ones((2, 2))
linear_layer = Linear(4, 2)
y = linear_layer(x)
print(y)

\

tf.Tensor(
[[-0.02419483 -0.06813122  0.00395634 -0.03124779]
 [-0.02419483 -0.06813122  0.00395634 -0.03124779]], shape=(2, 4), dtype=float32)

가중치에 유의하십시오w그리고b레이어 속성으로 설정되면 레이어에 의해 자동으로 추적됩니다.

\

assert linear_layer.weights == [linear_layer.w, linear_layer.b]

레이어는 비전되지 않는 가중치를 가질 수 있습니다

훈련 가능한 무게 외에도 층에 비계 할 수없는 무게를 추가 할 수 있습니다. 이러한 가중치는 층을 훈련 할 때 역설 중에 고려되지 않아야합니다.

비전 할 수없는 무게를 추가하고 사용하는 방법은 다음과 같습니다.

\

class ComputeSum(keras.layers.Layer):
    def __init__(self, input_dim):
        super().__init__()
        self.total = self.add_weight(
            initializer="zeros", shape=(input_dim,), trainable=False
        )

    def call(self, inputs):
        self.total.assign_add(tf.reduce_sum(inputs, axis=0))
        return self.total


x = tf.ones((2, 2))
my_sum = ComputeSum(2)
y = my_sum(x)
print(y.numpy())
y = my_sum(x)
print(y.numpy())

\

[2. 2.]
[4. 4.]

그것은의 일부입니다layer.weights그러나 비계 할 수없는 무게로 분류됩니다.

\

print("weights:", len(my_sum.weights))
print("non-trainable weights:", len(my_sum.non_trainable_weights))

# It's not included in the trainable weights:
print("trainable_weights:", my_sum.trainable_weights)

\

weights: 1
non-trainable weights: 1
trainable_weights: []

모범 사례 : 입력의 모양이 알려질 때까지 체중 생성을 연기

우리의Linear위의 층이 가져 갔다input_dim무게의 모양을 계산하는 데 사용 된 인수w그리고b~에__init__():

\

class Linear(keras.layers.Layer):
    def __init__(self, units=32, input_dim=32):
        super().__init__()
        self.w = self.add_weight(
            shape=(input_dim, units), initializer="random_normal", trainable=True
        )
        self.b = self.add_weight(shape=(units,), initializer="zeros", trainable=True)

    def call(self, inputs):
        return tf.matmul(inputs, self.w) + self.b

대부분의 경우 입력의 크기를 미리 알지 못할 수도 있으며, 그 값이 알려지면 층을 인스턴스화 한 후에도 값을 느낄 때 가중치를 만들고 싶습니다.

Keras API에서는build(self, inputs_shape)레이어의 방법. 이와 같이:

\

class Linear(keras.layers.Layer):
    def __init__(self, units=32):
        super().__init__()
        self.units = units

    def build(self, input_shape):
        self.w = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer="random_normal",
            trainable=True,
        )
        self.b = self.add_weight(
            shape=(self.units,), initializer="random_normal", trainable=True
        )

    def call(self, inputs):
        return tf.matmul(inputs, self.w) + self.b

그만큼__call__()레이어의 방법은 처음으로 호출 될 때 자동으로 빌드를 실행합니다. 이제 게으르고 사용하기 쉬운 레이어가 있습니다.

\

# At instantiation, we don't know on what inputs this is going to get called
linear_layer = Linear(32)

# The layer's weights are created dynamically the first time the layer is called
y = linear_layer(x)

구현build()위에서 볼 수 있듯이 별도로는 모든 통화에서 가중치를 사용하여 한 번만 가중치 생성을 멋지게 분리합니다. 그러나 일부 고급 커스텀 레이어의 경우 상태 생성 및 계산을 분리하는 것이 비현실적 일 수 있습니다. 레이어 구현자는 첫 ​​번째로 체중 생성을 연기 할 수 있습니다.__call__()그러나 나중에 통화가 같은 가중치를 사용하도록 조심해야합니다. 또한 그 이후로__call__()내부에서 처음으로 실행될 가능성이 높습니다.tf.function발생하는 모든 가변 생성__call__()atf.init_scope.

레이어는 재귀 적으로 합성 가능합니다

레이어 인스턴스를 다른 레이어의 속성으로 할당하면 외부 레이어가 내부 레이어로 생성 된 가중치를 추적하기 시작합니다.

그러한 하위 계이를 만드는 것이 좋습니다__init__()방법을 먼저 두십시오__call__()무게를 건축하는 것을 유발합니다.

\

class MLPBlock(keras.layers.Layer):
    def __init__(self):
        super().__init__()
        self.linear_1 = Linear(32)
        self.linear_2 = Linear(32)
        self.linear_3 = Linear(1)

    def call(self, inputs):
        x = self.linear_1(inputs)
        x = tf.nn.relu(x)
        x = self.linear_2(x)
        x = tf.nn.relu(x)
        return self.linear_3(x)


mlp = MLPBlock()
y = mlp(tf.ones(shape=(3, 64)))  # The first call to the `mlp` will create the weights
print("weights:", len(mlp.weights))
print("trainable weights:", len(mlp.trainable_weights))

\

weights: 6
trainable weights: 6

그만큼add_loss()방법

쓸 때call()레이어 방법, 훈련 루프를 작성할 때 나중에 사용할 수있는 손실 텐서를 만들 수 있습니다. 이것은 전화로 가능합니다self.add_loss(value):

\

# A layer that creates an activity regularization loss
class ActivityRegularizationLayer(keras.layers.Layer):
    def __init__(self, rate=1e-2):
        super().__init__()
        self.rate = rate

    def call(self, inputs):
        self.add_loss(self.rate * tf.reduce_mean(inputs))
        return inputs

주목하십시오add_loss()일반적인 텐서 플로 작동의 결과를 얻을 수 있습니다. a를 호출 할 필요가 없습니다Loss여기에 물체.

이러한 손실 (내부 층에 의해 생성 된 손실 포함)은layer.losses. 이 속성은 모든 시작시 재설정됩니다__call__()최상위 레이어로layer.losses항상 마지막 포워드 패스 중에 생성 된 손실 값이 포함됩니다.

\

class OuterLayer(keras.layers.Layer):
    def __init__(self):
        super().__init__()
        self.activity_reg = ActivityRegularizationLayer(1e-2)

    def call(self, inputs):
        return self.activity_reg(inputs)


layer = OuterLayer()
assert len(layer.losses) == 0  # No losses yet since the layer has never been called

_ = layer(tf.zeros(1, 1))
assert len(layer.losses) == 1  # We created one loss value

# `layer.losses` gets reset at the start of each __call__
_ = layer(tf.zeros(1, 1))
assert len(layer.losses) == 1  # This is the loss created during the call above

또한loss속성은 또한 모든 내부 층의 중량에 대해 생성 된 정규화 손실도 포함합니다.

\

class OuterLayerWithKernelRegularizer(keras.layers.Layer):
    def __init__(self):
        super().__init__()
        self.dense = keras.layers.Dense(
            32, kernel_regularizer=keras.regularizers.l2(1e-3)
        )

    def call(self, inputs):
        return self.dense(inputs)


layer = OuterLayerWithKernelRegularizer()
_ = layer(tf.zeros((1, 1)))

# This is `1e-3 * sum(layer.dense.kernel ** 2)`,
# created by the `kernel_regularizer` above.
print(layer.losses)

\

[<tf.Tensor: shape=(), dtype=float32, numpy=0.0017542194>]

이러한 손실은 다음과 같이 훈련 루프를 작성할 때 고려됩니다.

\

# Instantiate an optimizer.
optimizer = keras.optimizers.SGD(learning_rate=1e-3)
loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# Iterate over the batches of a dataset.
for x_batch_train, y_batch_train in train_dataset:
    with tf.GradientTape() as tape:
        logits = layer(x_batch_train)  # Logits for this minibatch
        # Loss value for this minibatch
        loss_value = loss_fn(y_batch_train, logits)
        # Add extra losses created during this forward pass:
        loss_value += sum(model.losses)

    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))

교육 루프 작성에 대한 자세한 안내서는 처음부터 교육 루프 작성에 대한 안내서를 참조하십시오.

이러한 손실도 완벽하게 작동합니다fit()(자동으로 합산되어 주 손실에 추가됩니다.) :

\

import numpy as np

inputs = keras.Input(shape=(3,))
outputs = ActivityRegularizationLayer()(inputs)
model = keras.Model(inputs, outputs)

# If there is a loss passed in `compile`, the regularization
# losses get added to it
model.compile(optimizer="adam", loss="mse")
model.fit(np.random.random((2, 3)), np.random.random((2, 3)))

# It's also possible not to pass any loss in `compile`,
# since the model already has a loss to minimize, via the `add_loss`
# call during the forward pass!
model.compile(optimizer="adam")
model.fit(np.random.random((2, 3)), np.random.random((2, 3)))

\

1/1 [==============================] - 0s 75ms/step - loss: 0.1081
1/1 [==============================] - 0s 31ms/step - loss: 0.0044
<keras.src.callbacks.History at 0x7fb23c0e3f40>

선택적으로 레이어에서 직렬화를 활성화 할 수 있습니다

기능 모델의 일부로 세 사용자 정의 레이어가 직렬화 할 수있는 경우 선택적으로 A를 구현할 수 있습니다.get_config()방법:

\

class Linear(keras.layers.Layer):
    def __init__(self, units=32):
        super().__init__()
        self.units = units

    def build(self, input_shape):
        self.w = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer="random_normal",
            trainable=True,
        )
        self.b = self.add_weight(
            shape=(self.units,), initializer="random_normal", trainable=True
        )

    def call(self, inputs):
        return tf.matmul(inputs, self.w) + self.b

    def get_config(self):
        return {"units": self.units}


# Now you can recreate the layer from its config:
layer = Linear(64)
config = layer.get_config()
print(config)
new_layer = Linear.from_config(config)

\

{'units': 64}

주목하십시오__init__()베이스의 방법Layer클래스는 일부 키워드 인수, 특히 a를 취합니다name그리고 adtype. 이러한 주장을 부모 수업에 전달하는 것이 좋습니다.__init__()그리고 레이어 구성에 포함시키기 위해 :

\

class Linear(keras.layers.Layer):
    def __init__(self, units=32, **kwargs):
        super().__init__(**kwargs)
        self.units = units

    def build(self, input_shape):
        self.w = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer="random_normal",
            trainable=True,
        )
        self.b = self.add_weight(
            shape=(self.units,), initializer="random_normal", trainable=True
        )

    def call(self, inputs):
        return tf.matmul(inputs, self.w) + self.b

    def get_config(self):
        config = super().get_config()
        config.update({"units": self.units})
        return config


layer = Linear(64)
config = layer.get_config()
print(config)
new_layer = Linear.from_config(config)

\

{'name': 'linear_7', 'trainable': True, 'dtype': 'float32', 'units': 64}

구성에서 레이어를 실시 할 때 더 많은 유연성이 필요하면from_config()수업 방법. 이것이 기본 구현입니다from_config():

\

def from_config(cls, config):
  return cls(**config)

직렬화 및 저장에 대한 자세한 내용은 모델 저장 및 직렬화에 대한 완전한 안내서를 참조하십시오.

특권training인수call()방법

일부 층, 특히BatchNormalization레이어와Dropout층은 훈련 및 추론 중에 다른 행동을합니다. 그러한 층의 경우, 노출하는 것은 표준 관행입니다.training(부울) 논쟁call()방법.

이 주장을 노출시킴으로써call()내장 교육 및 평가 루프 (예 :fit()) 훈련 및 추론에 레이어를 올바르게 사용하려면.

\

class CustomDropout(keras.layers.Layer):
    def __init__(self, rate, **kwargs):
        super().__init__(**kwargs)
        self.rate = rate

    def call(self, inputs, training=False):
        if training:
            return tf.nn.dropout(inputs, rate=self.rate)
        return inputs

특권mask인수call()방법

뒷받침되는 다른 특권 주장call()입니다mask논쟁.

모든 keras rnn 층에서 찾을 수 있습니다. 마스크는 타임 서리 데이터를 처리 할 때 특정 입력 타임 스텝을 건너 뛰는 데 사용되는 부울 텐서 (입력의 타임 스텝 당 1 개의 부울 값)입니다.

Keras는 자동으로 올바른 전달됩니다mask논쟁__call__()이를지지하는 층의 경우, 마스크가 이전 레이어에 의해 생성 될 때. 마스크 생성 레이어는 다음과 같습니다Embedding구성된 레이어mask_zero=True그리고Masking층.

마스킹 및 마스킹 가능 레이어 작성 방법에 대한 자세한 내용은 “패딩 및 마스킹 이해”가이드를 확인하십시오.

그만큼Model수업

일반적으로 사용합니다Layer내부 계산 블록을 정의하는 클래스는Model클래스 외부 모델 – 훈련 할 객체를 정의하는 클래스.

예를 들어 RESNET50 모델에는 여러 RESNET 블록 서브 클래싱이 있습니다.Layer그리고 싱글Model전체 RESNET50 네트워크를 포함합니다.

그만큼Model클래스는 API와 동일합니다Layer다음 차이점으로 :

  • 내장 교육, 평가 및 예측 루프를 노출시킵니다 (model.fit(),,,model.evaluate(),,,model.predict()).
  • 그것은 내부 층의 목록을model.layers재산.
  • 저축 및 직렬화 API를 노출시킵니다 (save(),,,save_weights()…)

효과적으로,Layer클래스는 문헌에서 “컨퍼런스 레이어”또는 “재발 레이어”에서와 같이 “층”또는 “블록”( “Resnet Block”또는 “Inceple Block”에서와 같이)으로 언급 한 내용에 해당합니다.

한편,Model클래스는 문헌에서 “모델”( “딥 러닝 모델”에서와 같이) 또는 “네트워크”( “Deep Neural Network”에서와 같이)로 언급 된 내용에 해당합니다.

그러니 궁금한 점이 있다면 “내가 사용해야합니다Layer클래스 또는Model클래스? “, 스스로에게 물어보세요 : 전화해야합니다fit()그것에? 전화해야합니다save()그것에? 그렇다면 함께 가십시오Model. 그렇지 않은 경우 (클래스가 더 큰 시스템의 블록이거나 직접 교육 및 코드 저장을 작성하고 있기 때문에) 사용하십시오.Layer.

예를 들어, 우리는 위의 미니 레즈넷 예제를 가져 와서 그것을 만들 수 있습니다.Model우리가 훈련 할 수있는 것fit()그리고 우리가 저축 할 수 있습니다save_weights():

\

class ResNet(keras.Model):

    def __init__(self, num_classes=1000):
        super().__init__()
        self.block_1 = ResNetBlock()
        self.block_2 = ResNetBlock()
        self.global_pool = layers.GlobalAveragePooling2D()
        self.classifier = Dense(num_classes)

    def call(self, inputs):
        x = self.block_1(inputs)
        x = self.block_2(x)
        x = self.global_pool(x)
        return self.classifier(x)


resnet = ResNet()
dataset = ...
resnet.fit(dataset, epochs=10)
resnet.save(filepath.keras)

모든 것을 정리하십시오 : 엔드 투 엔드 예

지금까지 배운 내용은 다음과 같습니다.

  • 에이Layer상태를 캡슐화하십시오 (생성__init__()또는build()) 및 일부 계산 (정의call()).
  • 새롭고 더 큰 계산 블록을 생성하기 위해 레이어를 재귀 적으로 중첩 할 수 있습니다.
  • 레이어는add_loss().
  • 외부 용기, 훈련하고 싶은 것은Model. 에이Model그냥 aLayer교육 및 직렬화 유틸리티가 추가되었습니다.

이 모든 것들을 엔드 투 엔드 예제로합시다. 우리는 변형 자동 인코딩 (VAE)을 구현할 것입니다. 우리는 mnist 숫자로 훈련 할 것입니다.

우리 VAE는 서브 클래스가 될 것입니다Model서브 클래스의 중첩 된 층 구성으로 제작되었습니다Layer. 정규화 손실 (KL 발산)이 특징입니다.

\

from keras import layers


@keras.saving.register_keras_serializable()
class Sampling(layers.Layer):
    """Uses (z_mean, z_log_var) to sample z, the vector encoding a digit."""

    def call(self, inputs):
        z_mean, z_log_var = inputs
        batch = tf.shape(z_mean)[0]
        dim = tf.shape(z_mean)[1]
        epsilon = keras.backend.random_normal(shape=(batch, dim))
        return z_mean + tf.exp(0.5 * z_log_var) * epsilon


@keras.saving.register_keras_serializable()
class Encoder(layers.Layer):
    """Maps MNIST digits to a triplet (z_mean, z_log_var, z)."""

    def __init__(self, latent_dim=32, intermediate_dim=64, name="encoder", **kwargs):
        super().__init__(name=name, **kwargs)
        self.dense_proj = layers.Dense(intermediate_dim, activation="relu")
        self.dense_mean = layers.Dense(latent_dim)
        self.dense_log_var = layers.Dense(latent_dim)
        self.sampling = Sampling()

    def call(self, inputs):
        x = self.dense_proj(inputs)
        z_mean = self.dense_mean(x)
        z_log_var = self.dense_log_var(x)
        z = self.sampling((z_mean, z_log_var))
        return z_mean, z_log_var, z


@keras.saving.register_keras_serializable()
class Decoder(layers.Layer):
    """Converts z, the encoded digit vector, back into a readable digit."""

    def __init__(self, original_dim, intermediate_dim=64, name="decoder", **kwargs):
        super().__init__(name=name, **kwargs)
        self.dense_proj = layers.Dense(intermediate_dim, activation="relu")
        self.dense_output = layers.Dense(original_dim, activation="sigmoid")

    def call(self, inputs):
        x = self.dense_proj(inputs)
        return self.dense_output(x)


@keras.saving.register_keras_serializable()
class VariationalAutoEncoder(keras.Model):
    """Combines the encoder and decoder into an end-to-end model for training."""

    def __init__(
        self,
        original_dim,
        intermediate_dim=64,
        latent_dim=32,
        name="autoencoder",
        **kwargs
    ):
        super().__init__(name=name, **kwargs)
        self.original_dim = original_dim
        self.encoder = Encoder(latent_dim=latent_dim, intermediate_dim=intermediate_dim)
        self.decoder = Decoder(original_dim, intermediate_dim=intermediate_dim)

    def call(self, inputs):
        z_mean, z_log_var, z = self.encoder(inputs)
        reconstructed = self.decoder(z)
        # Add KL divergence regularization loss.
        kl_loss = -0.5 * tf.reduce_mean(
            z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1
        )
        self.add_loss(kl_loss)
        return reconstructed

MNIST에 간단한 교육 루프를 작성해 봅시다 :

\

original_dim = 784
vae = VariationalAutoEncoder(original_dim, 64, 32)

optimizer = keras.optimizers.Adam(learning_rate=1e-3)
mse_loss_fn = keras.losses.MeanSquaredError()

loss_metric = keras.metrics.Mean()

(x_train, _), _ = keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype("float32") / 255

train_dataset = tf.data.Dataset.from_tensor_slices(x_train)
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

epochs = 2

# Iterate over epochs.
for epoch in range(epochs):
    print("Start of epoch %d" % (epoch,))

    # Iterate over the batches of the dataset.
    for step, x_batch_train in enumerate(train_dataset):
        with tf.GradientTape() as tape:
            reconstructed = vae(x_batch_train)
            # Compute reconstruction loss
            loss = mse_loss_fn(x_batch_train, reconstructed)
            loss += sum(vae.losses)  # Add KLD regularization loss

        grads = tape.gradient(loss, vae.trainable_weights)
        optimizer.apply_gradients(zip(grads, vae.trainable_weights))

        loss_metric(loss)

        if step % 100 == 0:
            print("step %d: mean loss = %.4f" % (step, loss_metric.result()))

\

Start of epoch 0
WARNING:tensorflow:5 out of the last 5 calls to <function _BaseOptimizer._update_step_xla at 0x7fb220066af0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to  and  for  more details.
WARNING:tensorflow:6 out of the last 6 calls to <function _BaseOptimizer._update_step_xla at 0x7fb220066af0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to  and  for  more details.
step 0: mean loss = 0.3433
step 100: mean loss = 0.1257
step 200: mean loss = 0.0994
step 300: mean loss = 0.0893
step 400: mean loss = 0.0844
step 500: mean loss = 0.0810
step 600: mean loss = 0.0788
step 700: mean loss = 0.0772
step 800: mean loss = 0.0760
step 900: mean loss = 0.0750
Start of epoch 1
step 0: mean loss = 0.0747
step 100: mean loss = 0.0741
step 200: mean loss = 0.0736
step 300: mean loss = 0.0731
step 400: mean loss = 0.0727
step 500: mean loss = 0.0723
step 600: mean loss = 0.0720
step 700: mean loss = 0.0717
step 800: mean loss = 0.0715
step 900: mean loss = 0.0712

VAE가 서브 클래싱이기 때문에 주목하십시오Model내장 교육 루프가 있습니다. 그래서 당신은 다음과 같이 훈련했을 수도 있습니다.

\

vae = VariationalAutoEncoder(784, 64, 32)

optimizer = keras.optimizers.Adam(learning_rate=1e-3)

vae.compile(optimizer, loss=keras.losses.MeanSquaredError())
vae.fit(x_train, x_train, epochs=2, batch_size=64)

\

Epoch 1/2
938/938 [==============================] - 4s 3ms/step - loss: 0.0746
Epoch 2/2
938/938 [==============================] - 3s 3ms/step - loss: 0.0676
<keras.src.callbacks.History at 0x7fb1e0533580>

\ \

::: 정보
원래 게시텐서 플로웹 사이트,이 기사는 새 헤드 라인 아래에 표시되며 CC에 따라 4.0으로 라이센스가 부여됩니다. Apache 2.0 라이센스에 따라 공유 된 코드 샘플.

:::

\

출처 참조

Post Comment

당신은 놓쳤을 수도 있습니다