深度學習 | 分類任務中類別不均衡解決策略(附程式碼)
0.前言
在解決一個分類問題時,遇到樣本不平衡問題。CSDN後,發現網上有很多類似於欠取樣 ,重複取樣,換模型等等巨集觀的概念,並沒有太多可實際應用(程式碼)的策略。經過一番查詢和除錯,最終找到3個相對靠譜的策略,故總結此文給有需要同志,策略均來自網路,本人只是進行了可用性測試並總結於此。以下將簡單介紹各個策略的機制以及對應程式碼(親測能跑通)。
NOTE:下述程式碼均是基於caffe的,而且實現策略都是通過新增自定義層。主要流程大致為:修改caffe.proto-->匯入hpp/cpp/cu-->重新編譯。具體請看:Caffe | 自定義欄位和層。
1.帶權重的softmaxLoss
在樣本不均衡分類問題中,樣本量大的類別往往會主導訓練過程,因為其累積loss會比較大。帶權重的softmaxloss函式通過加權來決定主導訓練的類別。具體為增加pos_mult(指定某類的權重乘子)和pos_cid(指定的某類的類別編號)兩個引數來確定類別和當前類別的係數。(若pos_mult=0.5,就表示當然類別重要度減半)。
程式碼實現github傳送門
(1)修改caffe.proto檔案
編輯src/caffe/proto/caffe.proto,主要是在原有的SoftmaxParameter上新增了pos_mul和pos_cid欄位。
// Message that stores parameters used by SoftmaxLayer, SoftmaxWithLossLayer
message SoftmaxParameter {
enum Engine {
DEFAULT = 0;
CAFFE = 1;
CUDNN = 2;
}
optional Engine engine = 1 [default = DEFAULT];
// The axis along which to perform the softmax -- may be negative to index
// from the end (e.g., -1 for the last axis).
// Any other axes will be evaluated as independent softmaxes.
optional int32 axis = 2 [default = 1];
optional float pos_mult = 3 [default = 1];
optional int32 pos_cid = 4 [default = 1];
}
(2)匯入hpp/cpp/cu檔案
weighted_softmax_loss_layer.hpp
#ifndef CAFFE_WEIGHTED_SOFTMAX_LOSS_LAYER_HPP_
#define CAFFE_WEIGHTED_SOFTMAX_LOSS_LAYER_HPP_
#include <vector>
#include "caffe/blob.hpp"
#include "caffe/layer.hpp"
#include "caffe/proto/caffe.pb.h"
#include "caffe/layers/loss_layer.hpp"
#include "caffe/layers/softmax_layer.hpp"
namespace caffe {
/**
* @brief A weighted version of SoftmaxWithLossLayer.
*
* TODO: Add description. Add the formulation in math.
*/
template <typename Dtype>
class WeightedSoftmaxWithLossLayer : public LossLayer<Dtype> {
public:
/**
* @param param provides LossParameter loss_param, with options:
* - ignore_label (optional)
* Specify a label value that should be ignored when computing the loss.
* - normalize (optional, default true)
* If true, the loss is normalized by the number of (nonignored) labels
* present; otherwise the loss is simply summed over spatial locations.
*/
explicit WeightedSoftmaxWithLossLayer(const LayerParameter& param)
: LossLayer<Dtype>(param) {}
virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
virtual inline const char* type() const { return "WeightedSoftmaxWithLoss"; }
virtual inline int ExactNumBottomBlobs() const { return -1; }
virtual inline int MinBottomBlobs() const { return 1; }
virtual inline int MaxBottomBlobs() const { return 2; }
virtual inline int ExactNumTopBlobs() const { return -1; }
virtual inline int MinTopBlobs() const { return 1; }
virtual inline int MaxTopBlobs() const { return 2; }
protected:
/// @copydoc WeightedSoftmaxWithLossLayer
virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
/**
* @brief Computes the softmax loss error gradient w.r.t. the predictions.
*
* Gradients cannot be computed with respect to the label inputs (bottom[1]),
* so this method ignores bottom[1] and requires !propagate_down[1], crashing
* if propagate_down[1] is set.
*
* @param top output Blob vector (length 1), providing the error gradient with
* respect to the outputs
* -# @f$ (1 \times 1 \times 1 \times 1) @f$
* This Blob's diff will simply contain the loss_weight* @f$ \lambda @f$,
* as @f$ \lambda @f$ is the coefficient of this layer's output
* @f$\ell_i@f$ in the overall Net loss
* @f$ E = \lambda_i \ell_i + \mbox{other loss terms}@f$; hence
* @f$ \frac{\partial E}{\partial \ell_i} = \lambda_i @f$.
* (*Assuming that this top Blob is not used as a bottom (input) by any
* other layer of the Net.)
* @param propagate_down see Layer::Backward.
* propagate_down[1] must be false as we can't compute gradients with
* respect to the labels.
* @param bottom input Blob vector (length 2)
* -# @f$ (N \times C \times H \times W) @f$
* the predictions @f$ x @f$; Backward computes diff
* @f$ \frac{\partial E}{\partial x} @f$
* -# @f$ (N \times 1 \times 1 \times 1) @f$
* the labels -- ignored as we can't compute their error gradients
*/
virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);
virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);
/// Read the normalization mode parameter and compute the normalizer based
/// on the blob size. If normalization_mode is VALID, the count of valid
/// outputs will be read from valid_count, unless it is -1 in which case
/// all outputs are assumed to be valid.
virtual Dtype get_normalizer(
LossParameter_NormalizationMode normalization_mode, int valid_count);
/// The internal SoftmaxLayer used to map predictions to a distribution.
shared_ptr<Layer<Dtype> > softmax_layer_;
/// prob stores the output probability predictions from the SoftmaxLayer.
Blob<Dtype> prob_;
/// bottom vector holder used in call to the underlying SoftmaxLayer::Forward
vector<Blob<Dtype>*> softmax_bottom_vec_;
/// top vector holder used in call to the underlying SoftmaxLayer::Forward
vector<Blob<Dtype>*> softmax_top_vec_;
/// Whether to ignore instances with a certain label.
bool has_ignore_label_;
/// The label indicating that an instance should be ignored.
int ignore_label_;
/// How to normalize the output loss.
LossParameter_NormalizationMode normalization_;
int softmax_axis_, outer_num_, inner_num_;
float pos_mult_;
int pos_cid_;
};
} // namespace caffe
#endif // CAFFE_WEIGHTED_SOFTMAX_LOSS_LAYER_HPP_
weighted_softmax_loss_layer.cpp
#include <algorithm>
#include <cfloat>
#include <vector>
#include "caffe/layers/weighted_softmax_loss_layer.hpp"
#include "caffe/util/math_functions.hpp"
namespace caffe {
template <typename Dtype>
void WeightedSoftmaxWithLossLayer<Dtype>::LayerSetUp(
const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {
LossLayer<Dtype>::LayerSetUp(bottom, top);
LayerParameter softmax_param(this->layer_param_);
softmax_param.set_type("Softmax");
softmax_layer_ = LayerRegistry<Dtype>::CreateLayer(softmax_param);
softmax_bottom_vec_.clear();
softmax_bottom_vec_.push_back(bottom[0]);
softmax_top_vec_.clear();
softmax_top_vec_.push_back(&prob_);
softmax_layer_->SetUp(softmax_bottom_vec_, softmax_top_vec_);
pos_mult_ = this->layer_param_.softmax_param().pos_mult();
pos_cid_ = this->layer_param_.softmax_param().pos_cid();
LOG(INFO) << "mult: " << pos_mult_ << ", id: " << pos_cid_;
has_ignore_label_ =
this->layer_param_.loss_param().has_ignore_label();
if (has_ignore_label_) {
ignore_label_ = this->layer_param_.loss_param().ignore_label();
}
if (!this->layer_param_.loss_param().has_normalization() &&
this->layer_param_.loss_param().has_normalize()) {
normalization_ = this->layer_param_.loss_param().normalize() ?
LossParameter_NormalizationMode_VALID :
LossParameter_NormalizationMode_BATCH_SIZE;
} else {
normalization_ = this->layer_param_.loss_param().normalization();
}
}
template <typename Dtype>
void WeightedSoftmaxWithLossLayer<Dtype>::Reshape(
const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {
LossLayer<Dtype>::Reshape(bottom, top);
softmax_layer_->Reshape(softmax_bottom_vec_, softmax_top_vec_);
softmax_axis_ =
bottom[0]->CanonicalAxisIndex(this->layer_param_.softmax_param().axis());
outer_num_ = bottom[0]->count(0, softmax_axis_);
inner_num_ = bottom[0]->count(softmax_axis_ + 1);
//LOG(INFO) << "outer_num_: " << outer_num_ << ", inner_num_: " << inner_num_;
CHECK_EQ(outer_num_ * inner_num_, bottom[1]->count())
<< "Number of labels must match number of predictions; "
<< "e.g., if softmax axis == 1 and prediction shape is (N, C, H, W), "
<< "label count (number of labels) must be N*H*W, "
<< "with integer values in {0, 1, ..., C-1}.";
if (top.size() >= 2) {
// softmax output
top[1]->ReshapeLike(*bottom[0]);
}
}
template <typename Dtype>
Dtype WeightedSoftmaxWithLossLayer<Dtype>::get_normalizer(
LossParameter_NormalizationMode normalization_mode, int valid_count) {
Dtype normalizer;
switch (normalization_mode) {
case LossParameter_NormalizationMode_FULL:
normalizer = Dtype(outer_num_ * inner_num_);
break;
case LossParameter_NormalizationMode_VALID:
if (valid_count == -1) {
normalizer = Dtype(outer_num_ * inner_num_);
} else {
normalizer = Dtype(valid_count);
}
break;
case LossParameter_NormalizationMode_BATCH_SIZE:
normalizer = Dtype(outer_num_);
break;
case LossParameter_NormalizationMode_NONE:
normalizer = Dtype(1);
break;
default:
LOG(FATAL) << "Unknown normalization mode: "
<< LossParameter_NormalizationMode_Name(normalization_mode);
}
// Some users will have no labels for some examples in order to 'turn off' a
// particular loss in a multi-task setup. The max prevents NaNs in that case.
return std::max(Dtype(1.0), normalizer);
}
template <typename Dtype>
void WeightedSoftmaxWithLossLayer<Dtype>::Forward_cpu(
const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {
// The forward pass computes the softmax prob values.
softmax_layer_->Forward(softmax_bottom_vec_, softmax_top_vec_);
const Dtype* prob_data = prob_.cpu_data();
const Dtype* label = bottom[1]->cpu_data();
int dim = prob_.count() / outer_num_;
int count = 0;
Dtype loss = 0;
LOG(INFO) << "dim:" << dim;
for (int i = 0; i < outer_num_; ++i) {
for (int j = 0; j < inner_num_; j++) {
const int label_value = static_cast<int>(label[i * inner_num_ + j]);
if (has_ignore_label_ && label_value == ignore_label_) {
continue;
}
DCHECK_GE(label_value, 0);
DCHECK_LT(label_value, prob_.shape(softmax_axis_));
Dtype w = (label_value == pos_cid_) ? pos_mult_ : 1;
loss -= w * log(std::max(prob_data[i * dim + label_value * inner_num_ + j],
Dtype(FLT_MIN)));
++count;
}
}
top[0]->mutable_cpu_data()[0] = loss / get_normalizer(normalization_, count);
if (top.size() == 2) {
top[1]->ShareData(prob_);
}
}
template <typename Dtype>
void WeightedSoftmaxWithLossLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {
if (propagate_down[1]) {
LOG(FATAL) << this->type()
<< " Layer cannot backpropagate to label inputs.";
}
if (propagate_down[0]) {
Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
const Dtype* prob_data = prob_.cpu_data();
caffe_copy(prob_.count(), prob_data, bottom_diff);
const Dtype* label = bottom[1]->cpu_data();
int dim = prob_.count() / outer_num_;
int count = 0;
for (int i = 0; i < outer_num_; ++i) {
for (int j = 0; j < inner_num_; ++j) {
const int label_value = static_cast<int>(label[i * inner_num_ + j]);
if (has_ignore_label_ && label_value == ignore_label_) {
for (int c = 0; c < bottom[0]->shape(softmax_axis_); ++c) {
bottom_diff[i * dim + c * inner_num_ + j] = 0;
}
} else {
bottom_diff[i * dim + label_value * inner_num_ + j] -= 1;
Dtype w = (label_value == pos_cid_) ? pos_mult_ : 1;
for (int k = 0; k < bottom[0]->shape(softmax_axis_); ++k) {
bottom_diff[i * dim + k * inner_num_ + j] *= w;
}
++count;
}
}
}
// Scale gradient
Dtype loss_weight = top[0]->cpu_diff()[0] /
get_normalizer(normalization_, count);
caffe_scal(prob_.count(), loss_weight, bottom_diff);
}
}
#ifdef CPU_ONLY
STUB_GPU(WeightedSoftmaxWithLossLayer);
#endif
INSTANTIATE_CLASS(WeightedSoftmaxWithLossLayer);
REGISTER_LAYER_CLASS(WeightedSoftmaxWithLoss);
} // namespace caffe
weighted_softmax_loss_layer.cu
#include <algorithm>
#include <cfloat>
#include <vector>
#include "caffe/layers/weighted_softmax_loss_layer.hpp"
#include "caffe/util/math_functions.hpp"
namespace caffe {
template <typename Dtype>
__global__ void WeightedSoftmaxLossForwardGPU(const int nthreads,
const Dtype* prob_data, const Dtype* label, Dtype* loss,
const Dtype pos_mult_, const int pos_cid_,
const int num, const int dim, const int spatial_dim,
const bool has_ignore_label_, const int ignore_label_,
Dtype* counts) {
CUDA_KERNEL_LOOP(index, nthreads) {
const int n = index / spatial_dim;
const int s = index % spatial_dim;
const int label_value = static_cast<int>(label[n * spatial_dim + s]);
Dtype w = (label_value == pos_cid_) ? pos_mult_ : 1;
if (has_ignore_label_ && label_value == ignore_label_) {
loss[index] = 0;
counts[index] = 0;
} else {
loss[index] = -w * log(max(prob_data[n * dim + label_value * spatial_dim + s],
Dtype(FLT_MIN)));
counts[index] = 1;
}
}
}
template <typename Dtype>
void WeightedSoftmaxWithLossLayer<Dtype>::Forward_gpu(
const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {
softmax_layer_->Forward(softmax_bottom_vec_, softmax_top_vec_);
const Dtype* prob_data = prob_.gpu_data();
const Dtype* label = bottom[1]->gpu_data();
const int dim = prob_.count() / outer_num_;
const int nthreads = outer_num_ * inner_num_;
// Since this memory is not used for anything until it is overwritten
// on the backward pass, we use it here to avoid having to allocate new GPU
// memory to accumulate intermediate results in the kernel.
Dtype* loss_data = bottom[0]->mutable_gpu_diff();
// Similarly, this memory is never used elsewhere, and thus we can use it
// to avoid having to allocate additional GPU memory.
Dtype* counts = prob_.mutable_gpu_diff();
// NOLINT_NEXT_LINE(whitespace/operators)
WeightedSoftmaxLossForwardGPU<Dtype><<<CAFFE_GET_BLOCKS(nthreads),
CAFFE_CUDA_NUM_THREADS>>>(nthreads, prob_data, label, loss_data,
pos_mult_, pos_cid_,
outer_num_, dim, inner_num_, has_ignore_label_, ignore_label_, counts);
Dtype loss;
caffe_gpu_asum(nthreads, loss_data, &loss);
Dtype valid_count = -1;
// Only launch another CUDA kernel if we actually need the count of valid
// outputs.
if (normalization_ == LossParameter_NormalizationMode_VALID &&
has_ignore_label_) {
caffe_gpu_asum(nthreads, counts, &valid_count);
}
top[0]->mutable_cpu_data()[0] = loss / get_normalizer(normalization_,
valid_count);
if (top.size() == 2) {
top[1]->ShareData(prob_);
}
}
template <typename Dtype>
__global__ void WeightedSoftmaxLossBackwardGPU(const int nthreads, const Dtype* top,
const Dtype* label, Dtype* bottom_diff,
const Dtype pos_mult_, const int pos_cid_,
const int num, const int dim,
const int spatial_dim, const bool has_ignore_label_,
const int ignore_label_, Dtype* counts) {
const int channels = dim / spatial_dim;
CUDA_KERNEL_LOOP(index, nthreads) {
const int n = index / spatial_dim;
const int s = index % spatial_dim;
const int label_value = static_cast<int>(label[n * spatial_dim + s]);
Dtype w = (label_value == pos_cid_) ? pos_mult_ : 1;
if (has_ignore_label_ && label_value == ignore_label_) {
for (int c = 0; c < channels; ++c) {
bottom_diff[n * dim + c * spatial_dim + s] = 0;
}
counts[index] = 0;
} else {
bottom_diff[n * dim + label_value * spatial_dim + s] -= 1;
counts[index] = 1;
for (int c = 0; c < channels; ++c) {
bottom_diff[n * dim + c * spatial_dim + s] *= w;
}
}
}
}
template <typename Dtype>
void WeightedSoftmaxWithLossLayer<Dtype>::Backward_gpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {
if (propagate_down[1]) {
LOG(FATAL) << this->type()
<< " Layer cannot backpropagate to label inputs.";
}
if (propagate_down[0]) {
Dtype* bottom_diff = bottom[0]->mutable_gpu_diff();
const Dtype* prob_data = prob_.gpu_data();
const Dtype* top_data = top[0]->gpu_data();
caffe_gpu_memcpy(prob_.count() * sizeof(Dtype), prob_data, bottom_diff);
const Dtype* label = bottom[1]->gpu_data();
const int dim = prob_.count() / outer_num_;
const int nthreads = outer_num_ * inner_num_;
// Since this memory is never used for anything else,
// we use to to avoid allocating new GPU memory.
Dtype* counts = prob_.mutable_gpu_diff();
// NOLINT_NEXT_LINE(whitespace/operators)
WeightedSoftmaxLossBackwardGPU<Dtype><<<CAFFE_GET_BLOCKS(nthreads),
CAFFE_CUDA_NUM_THREADS>>>(nthreads, top_data, label, bottom_diff,
pos_mult_, pos_cid_, outer_num_, dim, inner_num_, has_ignore_label_,
ignore_label_, counts);
Dtype valid_count = -1;
// Only launch another CUDA kernel if we actually need the count of valid
// outputs.
if (normalization_ == LossParameter_NormalizationMode_VALID &&
has_ignore_label_) {
caffe_gpu_asum(nthreads, counts, &valid_count);
}
const Dtype loss_weight = top[0]->cpu_diff()[0] /
get_normalizer(normalization_, valid_count);
caffe_gpu_scal(prob_.count(), loss_weight , bottom_diff);
}
}
INSTANTIATE_LAYER_GPU_FUNCS(WeightedSoftmaxWithLossLayer);
} // namespace caffe
(3)編譯
(4)使用方法
layer {
name: "loss"
type: "WeightedSoftmaxWithLoss"
bottom: "fc_end"
bottom: "label"
top: "loss"
softmax_param {
pos_cid: 1
pos_mult: 0.5
}
}
需要注意的是pos_cid也是從0開始的,若指定為0表示pos_mult的引數將乘到對應的類別中,簡而言之就是和標籤對應,對應程式碼如下。
Dtype w = (label_value == pos_cid_) ? pos_mult_ : 1;
2.OHEMLoss
OHEM被稱為難例挖掘,針對模型訓練過程中導致損失值很大的一些樣本(即使模型很大概率分類錯誤的樣本),重新訓練它們.維護一個錯誤分類樣本池, 把每個batch訓練資料中的出錯率很大的樣本放入該樣本池中,當積累到一個batch以後,將這些樣本放回網路重新訓練。通俗的講OHEM就是加強loss大樣本的訓練。
(1)修改caffe.proto檔案
(2)匯入hpp/cpp/cu檔案
(3)編譯
(4)使用方法
3.focalLoss
該loss就是是在帶權重的基礎上作出了改進,解決樣本不平衡問題的,總體思想和帶權重的有點類似, focal loss首先解決的就是樣本不平衡的問題,類似於softmaxloss。即在CE上加權重,當class為1的時候,乘以權重alpha,當class為0的時候,乘以權重1-alpha,這是最基本的解決樣本不平衡的方法,也就是在loss計算時乘以權重。
在此基礎上,focalloss的核心就是在CE的前面乘上了(1-pt)的gama次方。pt就是準確率,因此該公式表示的含義為:準確率越高 ,整個loss值就越小。所以我們把引數gama稱為衰減係數,準確率越高的類衰減的越厲害。這就是的準確率低的類能夠佔據loss的大部分,並主導訓練。
而第二種方法OHEM是讓loss大的進行主導。兩者在這個機制上殊途同歸。但OHEM的缺點是其只取一部分多數樣本進行loss計算來實現上述功能,而focalloss則作用於所有樣本。最終focalloss的公式如下:
程式碼實現github傳送門
(1)修改caffe.proto檔案
(2)匯入hpp/cpp/cu檔案
(3)編譯
(4)使用方法
相關文章
- 分類任務中效能度量及程式碼
- 分類任務中的樣本不均衡問題
- 分類任務loss不變
- pytorch深度學習分類程式碼簡單示例PyTorch深度學習
- 基於深度學習的時間序列分類[含程式碼]深度學習
- 機器學習中的類別不均衡問題機器學習
- 基於PaddlePaddle的影象分類實戰 | 深度學習基礎任務教程系列(一)深度學習
- 基於PaddlePaddle的影像分類實戰 | 深度學習基礎任務教程系列(一)深度學習
- 基於PaddlePaddle的圖片分類實戰 | 深度學習基礎任務教程系列深度學習
- 深度學習(二)之貓狗分類深度學習
- 【火爐煉AI】深度學習005-簡單幾行Keras程式碼解決二分類問題AI深度學習Keras
- tensorflow 學習筆記使用CNN做英文文字分類任務筆記CNN文字分類
- 如何用機器學習處理二元分類任務?機器學習
- 深度學習——如何用LSTM進行文字分類深度學習文字分類
- 深度學習(四)之電影評論分類深度學習
- 深度學習(一)之MNIST資料集分類深度學習
- 【長篇乾貨】深度學習在文字分類中的應用深度學習文字分類
- 【機器學習】--xgboost初始之程式碼實現分類機器學習
- 利用機器學習進行惡意程式碼分類機器學習
- 深度學習專案實戰:垃圾分類系統深度學習
- keras框架下的深度學習(二)二分類和多分類問題Keras框架深度學習
- 如何優雅而時髦的解決不均衡分類問題
- 如何用 Python 和深度遷移學習做文字分類?Python遷移學習文字分類
- 萬字總結Keras深度學習中文文字分類Keras深度學習文字分類
- 強化學習分類強化學習
- bert分類的程式碼
- 網路安全學習中,原始碼審計有哪些分類?原始碼
- 深度推薦系統十大分類類別 -James Le
- [乾貨]如何從不均衡類中進行機器學習機器學習
- 機器學習--有監督學習--分類演算法(預測分類)機器學習演算法
- 機器學習--分類變數編碼方法機器學習變數
- 深度學習之電影二分類的情感問題深度學習
- 計算機視覺經典任務分類計算機視覺
- 指南:不平衡分類的成本敏感決策樹(附程式碼&連結)
- 機器學習(三):理解邏輯迴歸及二分類、多分類程式碼實踐機器學習邏輯迴歸
- 深度學習中的正則化技術(附Python程式碼)深度學習Python
- 【火爐煉AI】機器學習008-簡單線性分類器解決二分類問題AI機器學習
- [解決] spring service 呼叫當前類方法事務不生效Spring